<?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>Backend on Tarragon</title><link>https://tarrragon.github.io/blog/tags/backend/</link><description>Recent content in Backend on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 23 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/backend/index.xml" rel="self" type="application/rss+xml"/><item><title>Chaos Mesh：Workflow、Scope Control 與 Steady State Probe</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/chaos-mesh/workflow-experiment-scope-and-steady-state-probe/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/chaos-mesh/workflow-experiment-scope-and-steady-state-probe/</guid><description>&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>單一 ChaosExperiment（PodChaos pod-kill、NetworkChaos delay）只能驗證一個故障面向。真實的可靠性驗證需要多步驟編排：先注入依賴延遲，觀察 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a> 是否維持，再注入節點失效，最後驗證恢復路徑。Chaos Workflow 提供這個編排能力，把多個 fault injection 與 health check 組成可重播的驗證流程。&lt;/p>
&lt;p>experiment scope 的精準控制同樣關鍵。selector 選到 production 全部 pod 的 chaos experiment 會變成真實事故。scope control 的責任是讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 從最小範圍開始，逐步放大，每一步都有停止條件。&lt;/p>
&lt;h2 id="chaos-workflow-設計">Chaos Workflow 設計&lt;/h2>
&lt;p>Chaos Workflow 是多個 ChaosExperiment 與 StatusCheck 組成的 DAG（有向無環圖），用 YAML 定義步驟順序與分支條件。&lt;/p>
&lt;h3 id="步驟類型">步驟類型&lt;/h3>
&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>Serial&lt;/td>
 &lt;td>順序執行，前一步完成才進下一步&lt;/td>
 &lt;td>依賴故障 → 觀察 → 節點故障&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Parallel&lt;/td>
 &lt;td>平行執行多個注入&lt;/td>
 &lt;td>同時打多個依賴驗證交叉影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Suspend&lt;/td>
 &lt;td>暫停等待人工確認後再繼續&lt;/td>
 &lt;td>高風險步驟前的 approval gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>StatusCheck&lt;/td>
 &lt;td>對 HTTP / gRPC / custom script 做 probe&lt;/td>
 &lt;td>注入前後的 steady state 驗證&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>StatusCheck 是 workflow 的核心控制面。它在故障注入前後對目標 endpoint 做 health check，pass/fail 決定 workflow 是否繼續。StatusCheck 的 success condition 對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition&lt;/a> 的穩態門檻：success rate、latency、queue lag 都能作為 probe 判準。&lt;/p>
&lt;p>典型 workflow 編排：NetworkChaos(delay 200ms) → StatusCheck(api-latency-ok) → PodChaos(pod-kill) → StatusCheck(recovery-within-30s)。第一個 StatusCheck 驗證延遲注入後服務仍可用；第二個 StatusCheck 驗證節點失效後恢復時間可接受。&lt;/p>
&lt;h3 id="suspend-的使用時機">Suspend 的使用時機&lt;/h3>
&lt;p>Suspend 步驟適合放在 blast radius 擴大之前。例如先在 canary namespace 跑完 chaos + StatusCheck，通過後 Suspend 等待值班工程師確認，再擴大到 production namespace。Suspend 讓自動化 workflow 在關鍵決策點保留人工判斷。&lt;/p>
&lt;h2 id="experiment-scope-control">Experiment Scope Control&lt;/h2>
&lt;p>Scope control 的責任是讓每個 ChaosExperiment 的影響面可預測、可限制。Chaos Mesh 用 selector + mode 兩層控制。&lt;/p>
&lt;h3 id="selector">Selector&lt;/h3>
&lt;p>Selector 決定哪些 pod 是實驗目標。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Selector 類型&lt;/th>
 &lt;th>作用&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>namespace&lt;/td>
 &lt;td>限制在特定 namespace&lt;/td>
 &lt;td>&lt;code>namespaces: [canary]&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>labelSelector&lt;/td>
 &lt;td>按 label 篩選&lt;/td>
 &lt;td>&lt;code>app: checkout, tier: backend&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>annotationSelector&lt;/td>
 &lt;td>按 annotation 篩選&lt;/td>
 &lt;td>&lt;code>chaos-eligible: &amp;quot;true&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fieldSelector&lt;/td>
 &lt;td>按 field 篩選（如 node name）&lt;/td>
 &lt;td>&lt;code>spec.nodeName: node-3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>podPhase&lt;/td>
 &lt;td>只選特定狀態的 pod&lt;/td>
 &lt;td>&lt;code>Running&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最安全的起點是 namespace + labelSelector + annotation 三層組合：只在 canary namespace、只選帶 &lt;code>chaos-eligible&lt;/code> annotation 的特定服務 pod。annotation-based opt-in 讓團隊明確標記哪些 pod 可以被 chaos 觸及。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題情境">問題情境</h2>
<p>單一 ChaosExperiment（PodChaos pod-kill、NetworkChaos delay）只能驗證一個故障面向。真實的可靠性驗證需要多步驟編排：先注入依賴延遲，觀察 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 是否維持，再注入節點失效，最後驗證恢復路徑。Chaos Workflow 提供這個編排能力，把多個 fault injection 與 health check 組成可重播的驗證流程。</p>
<p>experiment scope 的精準控制同樣關鍵。selector 選到 production 全部 pod 的 chaos experiment 會變成真實事故。scope control 的責任是讓 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 從最小範圍開始，逐步放大，每一步都有停止條件。</p>
<h2 id="chaos-workflow-設計">Chaos Workflow 設計</h2>
<p>Chaos Workflow 是多個 ChaosExperiment 與 StatusCheck 組成的 DAG（有向無環圖），用 YAML 定義步驟順序與分支條件。</p>
<h3 id="步驟類型">步驟類型</h3>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>責任</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Serial</td>
          <td>順序執行，前一步完成才進下一步</td>
          <td>依賴故障 → 觀察 → 節點故障</td>
      </tr>
      <tr>
          <td>Parallel</td>
          <td>平行執行多個注入</td>
          <td>同時打多個依賴驗證交叉影響</td>
      </tr>
      <tr>
          <td>Suspend</td>
          <td>暫停等待人工確認後再繼續</td>
          <td>高風險步驟前的 approval gate</td>
      </tr>
      <tr>
          <td>StatusCheck</td>
          <td>對 HTTP / gRPC / custom script 做 probe</td>
          <td>注入前後的 steady state 驗證</td>
      </tr>
  </tbody>
</table>
<p>StatusCheck 是 workflow 的核心控制面。它在故障注入前後對目標 endpoint 做 health check，pass/fail 決定 workflow 是否繼續。StatusCheck 的 success condition 對應 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a> 的穩態門檻：success rate、latency、queue lag 都能作為 probe 判準。</p>
<p>典型 workflow 編排：NetworkChaos(delay 200ms) → StatusCheck(api-latency-ok) → PodChaos(pod-kill) → StatusCheck(recovery-within-30s)。第一個 StatusCheck 驗證延遲注入後服務仍可用；第二個 StatusCheck 驗證節點失效後恢復時間可接受。</p>
<h3 id="suspend-的使用時機">Suspend 的使用時機</h3>
<p>Suspend 步驟適合放在 blast radius 擴大之前。例如先在 canary namespace 跑完 chaos + StatusCheck，通過後 Suspend 等待值班工程師確認，再擴大到 production namespace。Suspend 讓自動化 workflow 在關鍵決策點保留人工判斷。</p>
<h2 id="experiment-scope-control">Experiment Scope Control</h2>
<p>Scope control 的責任是讓每個 ChaosExperiment 的影響面可預測、可限制。Chaos Mesh 用 selector + mode 兩層控制。</p>
<h3 id="selector">Selector</h3>
<p>Selector 決定哪些 pod 是實驗目標。</p>
<table>
  <thead>
      <tr>
          <th>Selector 類型</th>
          <th>作用</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>namespace</td>
          <td>限制在特定 namespace</td>
          <td><code>namespaces: [canary]</code></td>
      </tr>
      <tr>
          <td>labelSelector</td>
          <td>按 label 篩選</td>
          <td><code>app: checkout, tier: backend</code></td>
      </tr>
      <tr>
          <td>annotationSelector</td>
          <td>按 annotation 篩選</td>
          <td><code>chaos-eligible: &quot;true&quot;</code></td>
      </tr>
      <tr>
          <td>fieldSelector</td>
          <td>按 field 篩選（如 node name）</td>
          <td><code>spec.nodeName: node-3</code></td>
      </tr>
      <tr>
          <td>podPhase</td>
          <td>只選特定狀態的 pod</td>
          <td><code>Running</code></td>
      </tr>
  </tbody>
</table>
<p>最安全的起點是 namespace + labelSelector + annotation 三層組合：只在 canary namespace、只選帶 <code>chaos-eligible</code> annotation 的特定服務 pod。annotation-based opt-in 讓團隊明確標記哪些 pod 可以被 chaos 觸及。</p>
<h3 id="mode">Mode</h3>
<p>Mode 決定在 selector 命中的 pod 中選多少個。</p>
<table>
  <thead>
      <tr>
          <th>Mode</th>
          <th>行為</th>
          <th>Blast radius</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>one</td>
          <td>隨機選 1 個</td>
          <td>最小</td>
      </tr>
      <tr>
          <td>fixed</td>
          <td>固定選 N 個</td>
          <td>可控</td>
      </tr>
      <tr>
          <td>fixed-percent</td>
          <td>選命中 pod 的 N%</td>
          <td>比例控制</td>
      </tr>
      <tr>
          <td>random-max-percent</td>
          <td>隨機選最多 N%</td>
          <td>有隨機性</td>
      </tr>
      <tr>
          <td>all</td>
          <td>選全部命中的 pod</td>
          <td>最大</td>
      </tr>
  </tbody>
</table>
<p>從 <code>mode: one</code> 開始驗證基礎假設，確認 StatusCheck 通過後，逐步升級到 <code>fixed-percent: 25</code> → <code>fixed-percent: 50</code>。每一步放大前檢查 steady state 是否仍維持，這個節奏對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a> 的漸進放大原則。</p>
<h3 id="duration-與-schedule">Duration 與 Schedule</h3>
<p>duration 控制單次故障注入持續多久，schedule 控制實驗重複頻率。duration 太短可能看不到系統完整的退化與恢復循環；太長則增加實際風險。初始建議：duration 設為 recovery SLA 的 2-3 倍（例如 RTO 30s 則 duration 設 60-90s），讓觀測窗涵蓋完整恢復。</p>
<h2 id="實作範例">實作範例</h2>
<p>一個完整的 Chaos Workflow：先對 checkout 服務注入網路延遲，驗證 API 仍可用，再 kill pod 驗證恢復。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">chaos-mesh.org/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Workflow</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-resilience-验证</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">chaos-testing</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">entry</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">templates</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">templateType</span><span class="p">:</span><span class="w"> </span><span class="l">Serial</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">children</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span>- <span class="l">network-delay</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span>- <span class="l">check-api-health</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span>- <span class="l">pod-kill</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span>- <span class="l">check-recovery</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">network-delay</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">templateType</span><span class="p">:</span><span class="w"> </span><span class="l">NetworkChaos</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">      </span><span class="nt">networkChaos</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">delay</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span><span class="nt">delay</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">          </span><span class="nt">latency</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;200ms&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">        </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">          </span><span class="nt">namespaces</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">canary]</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">          </span><span class="nt">labelSelectors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">            </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">checkout</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">one</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">        </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;60s&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">check-api-health</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span><span class="nt">templateType</span><span class="p">:</span><span class="w"> </span><span class="l">StatusCheck</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">      </span><span class="nt">statusCheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">        </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://checkout.canary/health&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">          </span><span class="nt">criteria</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">            </span><span class="nt">statusCode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;200&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">        </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">        </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pod-kill</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">      </span><span class="nt">templateType</span><span class="p">:</span><span class="w"> </span><span class="l">PodChaos</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">      </span><span class="nt">podChaos</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">pod-kill</span><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w">        </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="w">          </span><span class="nt">namespaces</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">canary]</span><span class="w">
</span></span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="w">          </span><span class="nt">labelSelectors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="w">            </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">checkout</span><span class="w">
</span></span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="w">        </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">one</span><span class="w">
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">check-recovery</span><span class="w">
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="w">      </span><span class="nt">templateType</span><span class="p">:</span><span class="w"> </span><span class="l">StatusCheck</span><span class="w">
</span></span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="w">      </span><span class="nt">statusCheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="w">        </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="w">          </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://checkout.canary/health&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="w">          </span><span class="nt">criteria</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="w">            </span><span class="nt">statusCode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;200&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">55</span><span class="cl"><span class="w">        </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="w">        </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">5</span></span></span></code></pre></div><h3 id="gitops-整合">GitOps 整合</h3>
<p>Workflow 定義存在 git repo，用 ArgoCD 或 Flux sync 到 cluster。變更 chaos experiment 走 PR review，跟 code 變更同樣的 approval 流程。這讓 experiment 的修改歷史可追蹤、可審計。</p>
<h3 id="rbac-約束">RBAC 約束</h3>
<p>Chaos Mesh 的 ServiceAccount 權限需要最小化。production namespace 的 chaos experiment 應使用獨立 ServiceAccount，只授予目標 namespace 的 ChaosExperiment create/get/list 權限。避免使用 cluster-admin 角色跑 chaos — 權限過大會讓 selector 誤配時的影響面不可控。</p>
<h2 id="邊界與陷阱">邊界與陷阱</h2>
<p><strong>StatusCheck timeout 太短</strong>：服務在 pod-kill 後需要 readiness probe 通過、load balancer 更新、cache 預熱。若 StatusCheck 的 timeoutSeconds 設太短，服務還在恢復中就被判失敗，產生 false negative。初始 timeout 建議設為預期恢復時間的 2 倍。</p>
<p><strong>Selector 太寬</strong>：namespace-level selector 不加 labelSelector 會命中該 namespace 所有 pod，包含 sidecar、monitoring agent 等非目標 pod。永遠用 labelSelector 或 annotationSelector 收窄範圍。</p>
<p><strong>Privilege 需求</strong>：Chaos Mesh 的 IOChaos 和 StressChaos 需要 container 的 SYS_ADMIN / SYS_PTRACE capability。安全團隊可能限制這些 capability 的使用。若無法取得 privilege，可以先用 PodChaos + NetworkChaos（不需額外 capability）建立 chaos 習慣，再逐步推進。</p>
<p><strong>K8s-only 限制</strong>：Chaos Mesh 只能注入 Kubernetes 上的故障。非 K8s 環境的依賴（外部 SaaS、bare-metal DB、第三方 API）需要用 <a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a>（TCP-level fault）或 <a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a>（跨平台 SaaS）補充。</p>
<h2 id="整合路由">整合路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a> — selector + mode 對應 blast radius 設計</li>
<li>上游概念：<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a> — StatusCheck 對應穩態門檻</li>
<li>下游交接：<a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a> — Workflow 結果作為 release gate 證據</li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos</a>、<a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a>、<a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></li>
<li>案例回寫：<a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix N1</a>（steady state hypothesis）、<a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">Netflix N2</a>（business-hours guardrails 對應 scope control）</li>
</ul>
]]></content:encoded></item><item><title>k6：Threshold CI Gate 與 Scenario 設計</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/k6/threshold-ci-gate-and-scenario-design/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/k6/threshold-ci-gate-and-scenario-design/</guid><description>&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Load test 跑完會產生大量指標，但 CI pipeline 需要的是 pass/fail 訊號。若沒有 threshold 把指標轉成判讀結論，效能退化只能靠人工看 dashboard 發現，等到看見時通常已經累積數個版本。&lt;/p>
&lt;p>另一面，threshold 的判讀品質取決於 workload model 的真實度。用 &lt;code>--vus 10 --duration 30s&lt;/code> 跑出來的結果跟 production 流量結構差距太大時，threshold 通過也無法證明 production 安全。&lt;/p>
&lt;p>這篇處理兩個問題：怎麼設 threshold 讓 CI gate 可靠，怎麼設 scenario 讓 workload 接近真實。&lt;/p>
&lt;h2 id="threshold-設計">Threshold 設計&lt;/h2>
&lt;p>Threshold 的責任是把 load test 指標轉成 CI 的 pass/fail 訊號。k6 在所有 threshold 都通過時回傳 exit code 0，任一 threshold 失敗就回傳非零 — CI pipeline 直接用 exit code 判斷。&lt;/p>
&lt;h3 id="多指標-threshold">多指標 threshold&lt;/h3>
&lt;p>單一指標 threshold 容易漏風險。latency 正常但 error rate 偏高代表系統在丟請求；throughput 正常但 latency 偏高代表排隊開始堆積。完整的 threshold 至少涵蓋三個面向：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">thresholds&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">http_req_duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;p(95)&amp;lt;500&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;p(99)&amp;lt;1000&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">http_req_failed&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rate&amp;lt;0.01&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">http_reqs&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rate&amp;gt;100&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">};&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>latency threshold 用 percentile 而不是 average — average 會被長尾稀釋，p95/p99 更接近使用者感知的最差體驗。&lt;/p>
&lt;h3 id="門檻來源">門檻來源&lt;/h3>
&lt;p>Threshold 的門檻從 production baseline 出發。先從 observability 系統（Grafana / Datadog）取最近 7-30 天的 p95/p99 latency 與 error rate，加上可接受退化幅度（通常 10-20%）作為 threshold。門檻太緊會讓 CI 環境噪音觸發 false positive；門檻太寬會讓真退化滑過去。&lt;/p>
&lt;p>校準節奏：每月或每次重大架構變更後重新對齊 production baseline，避免 threshold 跟真實系統漂移。&lt;/p>
&lt;h3 id="path-level-threshold">Path-level threshold&lt;/h3>
&lt;p>不同 API path 的效能特徵不同。checkout 路徑的 latency 容忍度可能比 listing 路徑低很多。k6 的 group + tag 機制讓 threshold 可以按 path 設定：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">group&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;k6&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;checkout&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// checkout 請求
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;listing&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// listing 請求
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">thresholds&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="s1">&amp;#39;http_req_duration{group:::checkout}&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;p(95)&amp;lt;300&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="s1">&amp;#39;http_req_duration{group:::listing}&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;p(95)&amp;lt;800&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="p">};&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>path-level threshold 讓 gate 的判讀粒度從「整體效能」細化到「關鍵路徑效能」。&lt;/p>
&lt;h2 id="scenario-設計">Scenario 設計&lt;/h2>
&lt;p>Scenario 的責任是讓壓測的流量結構接近 production。k6 提供五種 scenario executor，選擇取決於要控制什麼變量。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Executor&lt;/th>
 &lt;th>控制變量&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>constant-vus&lt;/td>
 &lt;td>並發使用者數&lt;/td>
 &lt;td>簡單 smoke test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ramping-vus&lt;/td>
 &lt;td>並發使用者數&lt;/td>
 &lt;td>階梯式升壓找 saturation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>constant-arrival-rate&lt;/td>
 &lt;td>固定 RPS&lt;/td>
 &lt;td>CI regression（穩定輸入）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ramping-arrival-rate&lt;/td>
 &lt;td>變化 RPS&lt;/td>
 &lt;td>模擬 production peak/off-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>externally-controlled&lt;/td>
 &lt;td>外部 API&lt;/td>
 &lt;td>結合 production 流量 replay&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="executor-選擇判準">Executor 選擇判準&lt;/h3>
&lt;p>constant-vus 最簡單，但 throughput 會隨 response time 波動 — 伺服器變慢時 RPS 自動下降，掩蓋了真正的壓力。constant-arrival-rate 控制 RPS 穩定，能讓 threshold 的判讀基準一致，但需要設定足夠的 preAllocatedVUs 避免 k6 因為 VU 不足而主動降速。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題情境">問題情境</h2>
<p>Load test 跑完會產生大量指標，但 CI pipeline 需要的是 pass/fail 訊號。若沒有 threshold 把指標轉成判讀結論，效能退化只能靠人工看 dashboard 發現，等到看見時通常已經累積數個版本。</p>
<p>另一面，threshold 的判讀品質取決於 workload model 的真實度。用 <code>--vus 10 --duration 30s</code> 跑出來的結果跟 production 流量結構差距太大時，threshold 通過也無法證明 production 安全。</p>
<p>這篇處理兩個問題：怎麼設 threshold 讓 CI gate 可靠，怎麼設 scenario 讓 workload 接近真實。</p>
<h2 id="threshold-設計">Threshold 設計</h2>
<p>Threshold 的責任是把 load test 指標轉成 CI 的 pass/fail 訊號。k6 在所有 threshold 都通過時回傳 exit code 0，任一 threshold 失敗就回傳非零 — CI pipeline 直接用 exit code 判斷。</p>
<h3 id="多指標-threshold">多指標 threshold</h3>
<p>單一指標 threshold 容易漏風險。latency 正常但 error rate 偏高代表系統在丟請求；throughput 正常但 latency 偏高代表排隊開始堆積。完整的 threshold 至少涵蓋三個面向：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">thresholds</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">http_req_duration</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;p(95)&lt;500&#39;</span><span class="p">,</span> <span class="s1">&#39;p(99)&lt;1000&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">http_req_failed</span><span class="o">:</span>   <span class="p">[</span><span class="s1">&#39;rate&lt;0.01&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">http_reqs</span><span class="o">:</span>         <span class="p">[</span><span class="s1">&#39;rate&gt;100&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">};</span></span></span></code></pre></div><p>latency threshold 用 percentile 而不是 average — average 會被長尾稀釋，p95/p99 更接近使用者感知的最差體驗。</p>
<h3 id="門檻來源">門檻來源</h3>
<p>Threshold 的門檻從 production baseline 出發。先從 observability 系統（Grafana / Datadog）取最近 7-30 天的 p95/p99 latency 與 error rate，加上可接受退化幅度（通常 10-20%）作為 threshold。門檻太緊會讓 CI 環境噪音觸發 false positive；門檻太寬會讓真退化滑過去。</p>
<p>校準節奏：每月或每次重大架構變更後重新對齊 production baseline，避免 threshold 跟真實系統漂移。</p>
<h3 id="path-level-threshold">Path-level threshold</h3>
<p>不同 API path 的效能特徵不同。checkout 路徑的 latency 容忍度可能比 listing 路徑低很多。k6 的 group + tag 機制讓 threshold 可以按 path 設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">group</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;k6&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">group</span><span class="p">(</span><span class="s1">&#39;checkout&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// checkout 請求
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nx">group</span><span class="p">(</span><span class="s1">&#39;listing&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// listing 請求
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">thresholds</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="s1">&#39;http_req_duration{group:::checkout}&#39;</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;p(95)&lt;300&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="s1">&#39;http_req_duration{group:::listing}&#39;</span><span class="o">:</span>  <span class="p">[</span><span class="s1">&#39;p(95)&lt;800&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">};</span></span></span></code></pre></div><p>path-level threshold 讓 gate 的判讀粒度從「整體效能」細化到「關鍵路徑效能」。</p>
<h2 id="scenario-設計">Scenario 設計</h2>
<p>Scenario 的責任是讓壓測的流量結構接近 production。k6 提供五種 scenario executor，選擇取決於要控制什麼變量。</p>
<table>
  <thead>
      <tr>
          <th>Executor</th>
          <th>控制變量</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>constant-vus</td>
          <td>並發使用者數</td>
          <td>簡單 smoke test</td>
      </tr>
      <tr>
          <td>ramping-vus</td>
          <td>並發使用者數</td>
          <td>階梯式升壓找 saturation</td>
      </tr>
      <tr>
          <td>constant-arrival-rate</td>
          <td>固定 RPS</td>
          <td>CI regression（穩定輸入）</td>
      </tr>
      <tr>
          <td>ramping-arrival-rate</td>
          <td>變化 RPS</td>
          <td>模擬 production peak/off-peak</td>
      </tr>
      <tr>
          <td>externally-controlled</td>
          <td>外部 API</td>
          <td>結合 production 流量 replay</td>
      </tr>
  </tbody>
</table>
<h3 id="executor-選擇判準">Executor 選擇判準</h3>
<p>constant-vus 最簡單，但 throughput 會隨 response time 波動 — 伺服器變慢時 RPS 自動下降，掩蓋了真正的壓力。constant-arrival-rate 控制 RPS 穩定，能讓 threshold 的判讀基準一致，但需要設定足夠的 preAllocatedVUs 避免 k6 因為 VU 不足而主動降速。</p>
<p>CI regression 測試建議用 constant-arrival-rate：輸入固定、輸出可比較、版本間的差異才有意義。</p>
<h3 id="production-traffic-shape-對齊">Production traffic shape 對齊</h3>
<p>用 ramping-arrival-rate 模擬 production 的流量形狀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">scenarios</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">peak_simulation</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="nx">executor</span><span class="o">:</span> <span class="s1">&#39;ramping-arrival-rate&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">startRate</span><span class="o">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">stages</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">{</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;2m&#39;</span> <span class="p">},</span>  <span class="c1">// ramp up
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>        <span class="p">{</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5m&#39;</span> <span class="p">},</span>  <span class="c1">// sustain peak
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>        <span class="p">{</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">50</span><span class="p">,</span>  <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;1m&#39;</span> <span class="p">},</span>  <span class="c1">// ramp down
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>      <span class="p">],</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">preAllocatedVUs</span><span class="o">:</span> <span class="mi">300</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">};</span></span></span></code></pre></div><p>流量形狀的參數（startRate / target / duration）從 production access log 的 peak 時段推算。Shopify 的 BFCM 準備流程把 game day 的 load test scenario 跟實際峰值形狀對齊 — 短時間爆量加高寫入比例需要特別設計 scenario 來覆蓋。</p>
<h3 id="cohort-模擬">Cohort 模擬</h3>
<p>Production 流量不是單一類型。用多 scenario 並行模擬不同 cohort：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">scenarios</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">read_traffic</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="nx">executor</span><span class="o">:</span> <span class="s1">&#39;constant-arrival-rate&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">rate</span><span class="o">:</span> <span class="mi">150</span><span class="p">,</span> <span class="nx">exec</span><span class="o">:</span> <span class="s1">&#39;readFlow&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">preAllocatedVUs</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5m&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">write_traffic</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nx">executor</span><span class="o">:</span> <span class="s1">&#39;constant-arrival-rate&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">rate</span><span class="o">:</span> <span class="mi">30</span><span class="p">,</span> <span class="nx">exec</span><span class="o">:</span> <span class="s1">&#39;writeFlow&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nx">preAllocatedVUs</span><span class="o">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5m&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">};</span>
</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="kr">export</span> <span class="kd">function</span> <span class="nx">readFlow</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* GET 請求 */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">writeFlow</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* POST 請求 */</span> <span class="p">}</span></span></span></code></pre></div><p>讀寫比例從 production 的 access log 或 APM 資料推算。比例偏差會讓瓶頸位置失真 — 讀為主的模型抓不到寫入引起的 lock contention。</p>
<h3 id="資料驅動">資料驅動</h3>
<p>測試資料用 SharedArray 載入，避免每個 VU 各自載入造成記憶體浪費：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SharedArray</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;k6/data&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SharedArray</span><span class="p">(</span><span class="s1">&#39;users&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">open</span><span class="p">(</span><span class="s1">&#39;./users.json&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>資料來源可以是 production sample（脫敏後）或 synthetic generation。資料分佈需要接近 production — ID 範圍、key 分佈、payload 大小都會影響 query plan 與 cache 行為。</p>
<h2 id="ci-整合實務">CI 整合實務</h2>
<h3 id="fast-path每次-push">Fast path（每次 push）</h3>
<p>固定 scenario + 短 duration（30s-2min），用 constant-arrival-rate 做 regression 偵測。threshold 設在 production baseline + 10%。這一層的目的是快速攔住明顯退化，不需要模擬完整峰值。</p>
<h3 id="slow-pathmerge-gate">Slow path（merge gate）</h3>
<p>完整 scenario + 較長 duration（5-15min），包含多 cohort 與 ramping 模擬。threshold 涵蓋 path-level 指標。這一層的目的是深層驗證變更在接近真實壓力下的行為。</p>
<h3 id="結果留存">結果留存</h3>
<p>k6 結果預設輸出到 stdout。CI 整合時用 <code>--out</code> flag 把結果送到時序資料庫（InfluxDB / Prometheus Remote Write / Grafana Cloud k6），讓歷史趨勢可查詢。趨勢比較能偵測 threshold 內但持續惡化的 slow drift。</p>
<p>LinkedIn 的自動化壓測實踐把 load test 結果跟容量預測接在一起 — saturation point 隨時間的變化趨勢直接驅動擴容決策。</p>
<h2 id="邊界與陷阱">邊界與陷阱</h2>
<p><strong>Threshold variance</strong>：CI runner 的硬體差異（shared runner 的鄰居效應、network jitter、GC pause）會讓同一份 code 在不同 run 產生不同結果。控制方式：dedicated runner 消除鄰居效應、warmup iteration 丟棄前幾輪結果、多次 run 取中位數。若 variance 超過 threshold 的退化幅度，gate 判讀就不可信。</p>
<p><strong>門檻過寬或過緊</strong>：threshold 永遠通過代表 gate 形同虛設；threshold 頻繁 false positive 會讓團隊忽略 CI 結果。兩者都會讓 gate 失去判讀價值。校準的判準是：過去 30 天的 threshold 結果中，真正需要關注的退化是否都被攔住，同時 false positive 率低於 5%。</p>
<p><strong>Scenario 跟 production drift</strong>：production 的流量結構會隨產品演進改變。定期（每月或每次重大功能上線）用 access log 校準 scenario 的 RPS、cohort 比例與資料分佈，避免模型越跑越偏。</p>
<h2 id="整合路由">整合路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load testing</a> 的 workload model 設計</li>
<li>下游能力：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a> 的 baseline 管理與退化定位</li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a>、<a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a>、<a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></li>
<li>案例回寫：<a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify BFCM 容量治理</a>（game day load test 對齊峰值形狀）、<a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">LinkedIn Automated Load Testing</a>（持續壓測驅動容量預測）</li>
</ul>
]]></content:encoded></item><item><title>Sloth：SLO YAML 與 Multi-burn-rate Alert 生成</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/sloth/slo-yaml-and-multi-burn-rate-alert-generation/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/sloth/slo-yaml-and-multi-burn-rate-alert-generation/</guid><description>&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>SLO 從定義到 Prometheus 落地需要多層 rule。一個 SLO 對應 4 組 time window 的 recording rule（計算各窗口的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a>），再對應 fast burn 和 slow burn 兩組 alerting rule。手動維護這些 rule 容易出錯：window 參數不一致、新增 SLO 忘記補 alert、修改 SLI expression 只改了部分 rule。&lt;/p>
&lt;p>Sloth 的責任是把這個過程自動化。輸入一份 SLO YAML，產出一組完整的 Prometheus recording + alerting rules，讓 SLO 維護回到宣告式：改 YAML、重新生成、載入 Prometheus。&lt;/p>
&lt;h2 id="slo-yaml-設計">SLO YAML 設計&lt;/h2>
&lt;p>Sloth YAML 的核心結構是 &lt;code>version&lt;/code> → &lt;code>service&lt;/code> → &lt;code>slos[]&lt;/code>。每個 SLO 定義三件事：目標數字（objective）、量測方式（SLI）、告警等級（alerting）。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prometheus/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">checkout-api&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">slos&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">availability&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">objective&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">99.9&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;checkout API 的請求成功率&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sli&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">events&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">error_query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sum(rate(http_requests_total{service=&amp;#34;checkout&amp;#34;,code=~&amp;#34;5..&amp;#34;}[{{.window}}]))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">total_query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sum(rate(http_requests_total{service=&amp;#34;checkout&amp;#34;}[{{.window}}]))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">alerting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CheckoutAvailability&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">page_alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ticket_alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SLI 有兩種類型。events-based SLI 用 error/total ratio 定義，Sloth 自動把 &lt;code>{{.window}}&lt;/code> 參數代入各 recording rule 的 range vector。raw SLI 直接寫 PromQL expression 算 error ratio，適合非 request-based 的 SLO（如 data freshness、replication lag）。&lt;/p>
&lt;p>raw SLI 範例 — data freshness：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">data-freshness&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">objective&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">99.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sli&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">raw&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">error_ratio_query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="sd"> 1 - clamp_max(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="sd"> replication_lag_seconds{service=&amp;#34;checkout-db&amp;#34;} / 60,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="sd"> 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>objective 數字的來源是 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策&lt;/a> — 先從使用者旅程定義服務承諾，再把承諾轉成 objective。Sloth 不負責決定 objective 該是多少，只負責把 objective 轉成可執行的 Prometheus rule。&lt;/p>
&lt;p>alerting 分 page（嚴重，觸發即時通知）和 ticket（一般，產生工單）。兩者的 burn rate 門檻不同：page 用 fast burn window，ticket 用 slow burn window。label 設計跟 Alertmanager routing 對齊 — &lt;code>severity: critical&lt;/code> 走 PagerDuty / Slack alert channel，&lt;code>severity: warning&lt;/code> 走 ticket system（Jira / Linear）。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題情境">問題情境</h2>
<p>SLO 從定義到 Prometheus 落地需要多層 rule。一個 SLO 對應 4 組 time window 的 recording rule（計算各窗口的 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>），再對應 fast burn 和 slow burn 兩組 alerting rule。手動維護這些 rule 容易出錯：window 參數不一致、新增 SLO 忘記補 alert、修改 SLI expression 只改了部分 rule。</p>
<p>Sloth 的責任是把這個過程自動化。輸入一份 SLO YAML，產出一組完整的 Prometheus recording + alerting rules，讓 SLO 維護回到宣告式：改 YAML、重新生成、載入 Prometheus。</p>
<h2 id="slo-yaml-設計">SLO YAML 設計</h2>
<p>Sloth YAML 的核心結構是 <code>version</code> → <code>service</code> → <code>slos[]</code>。每個 SLO 定義三件事：目標數字（objective）、量測方式（SLI）、告警等級（alerting）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l">prometheus/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-api</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">slos</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">availability</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">objective</span><span class="p">:</span><span class="w"> </span><span class="m">99.9</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;checkout API 的請求成功率&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">sli</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">events</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">error_query</span><span class="p">:</span><span class="w"> </span><span class="l">sum(rate(http_requests_total{service=&#34;checkout&#34;,code=~&#34;5..&#34;}[{{.window}}]))</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">total_query</span><span class="p">:</span><span class="w"> </span><span class="l">sum(rate(http_requests_total{service=&#34;checkout&#34;}[{{.window}}]))</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">alerting</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CheckoutAvailability</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">page_alert</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">critical</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">ticket_alert</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">warning</span></span></span></code></pre></div><p>SLI 有兩種類型。events-based SLI 用 error/total ratio 定義，Sloth 自動把 <code>{{.window}}</code> 參數代入各 recording rule 的 range vector。raw SLI 直接寫 PromQL expression 算 error ratio，適合非 request-based 的 SLO（如 data freshness、replication lag）。</p>
<p>raw SLI 範例 — data freshness：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">data-freshness</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">objective</span><span class="p">:</span><span class="w"> </span><span class="m">99.5</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">sli</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">      </span><span class="nt">raw</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">error_ratio_query</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="sd">          1 - clamp_max(
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="sd">            replication_lag_seconds{service=&#34;checkout-db&#34;} / 60,
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="sd">            1
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="sd">          )</span></span></span></code></pre></div><p>objective 數字的來源是 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策</a> — 先從使用者旅程定義服務承諾，再把承諾轉成 objective。Sloth 不負責決定 objective 該是多少，只負責把 objective 轉成可執行的 Prometheus rule。</p>
<p>alerting 分 page（嚴重，觸發即時通知）和 ticket（一般，產生工單）。兩者的 burn rate 門檻不同：page 用 fast burn window，ticket 用 slow burn window。label 設計跟 Alertmanager routing 對齊 — <code>severity: critical</code> 走 PagerDuty / Slack alert channel，<code>severity: warning</code> 走 ticket system（Jira / Linear）。</p>
<h2 id="multi-window-multi-burn-rate-alert">Multi-window Multi-burn-rate Alert</h2>
<p>Sloth 預設產生 Google SRE 推薦的 4-window alert 結構。每個 SLO 生成以下 recording rules 和 alerting rules。</p>
<table>
  <thead>
      <tr>
          <th>Window 組合</th>
          <th>責任</th>
          <th>觸發行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5m / 1h</td>
          <td>Fast burn 偵測</td>
          <td>短時間大量消耗 → page 通知</td>
      </tr>
      <tr>
          <td>30m / 6h</td>
          <td>Moderate burn 偵測</td>
          <td>中速消耗 → page 或 ticket</td>
      </tr>
      <tr>
          <td>2h / 1d</td>
          <td>Slow burn 偵測</td>
          <td>緩慢消耗 → ticket</td>
      </tr>
      <tr>
          <td>6h / 3d</td>
          <td>Very slow 偵測</td>
          <td>長期趨勢退化 → ticket 或 review</td>
      </tr>
  </tbody>
</table>
<p>fast burn alert 回答「error budget 是否正在被快速吃掉」。當 5 分鐘窗口的 burn rate 超過 14.4 倍（代表如果持續下去，1 小時會消耗完整個月的 budget），觸發 page。這個門檻的設計邏輯是：越短的窗口允許越高的 burn rate 容忍，因為短窗口的 false positive 率較高，需要搭配較長窗口的確認。</p>
<p>slow burn alert 回答「error budget 是否在不被注意的情況下被緩慢消耗」。6 小時窗口的 burn rate 超過 1 倍（代表月底會剛好用完 budget），觸發 ticket。slow burn 常被忽略，但它是高變更頻率服務最常見的可靠性退化模式 — 每次小回歸都不夠大到觸發 fast burn，累積到月底才發現 budget 已透支。</p>
<p>burn rate alert 跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO error budget 政策</a> 直接對應：fast burn → 凍結變更；slow burn → 提高驗證門檻；budget 健康 → 正常發版。</p>
<p>Sloth 產出的 recording rule 範例（5m window）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl">- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">slo:sli_error:ratio_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">    sum(rate(http_requests_total{service=&#34;checkout&#34;,code=~&#34;5..&#34;}[5m]))
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">    /
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">    sum(rate(http_requests_total{service=&#34;checkout&#34;}[5m]))</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">sloth_service</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-api</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nt">sloth_slo</span><span class="p">:</span><span class="w"> </span><span class="l">availability</span></span></span></code></pre></div><p>對應的 alerting rule（fast burn）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl">- <span class="nt">alert</span><span class="p">:</span><span class="w"> </span><span class="l">CheckoutAvailabilityFastBurn</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">    slo:sli_error:ratio_rate5m{sloth_slo=&#34;availability&#34;} &gt; (14.4 * 0.001)
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">    and
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">    slo:sli_error:ratio_rate1h{sloth_slo=&#34;availability&#34;} &gt; (14.4 * 0.001)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">critical</span></span></span></code></pre></div><p>fast burn alert 要求 5m 和 1h 兩個窗口同時超過門檻，短窗口防止 spike false positive、長窗口確認趨勢持續。</p>
<h2 id="實作流程">實作流程</h2>
<h3 id="cli-生成">CLI 生成</h3>





<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">sloth generate -i slo.yaml -o rules.yaml
</span></span><span class="line"><span class="ln">2</span><span class="cl">sloth validate -i slo.yaml</span></span></code></pre></div><p><code>generate</code> 產出的 <code>rules.yaml</code> 包含所有 recording rules 和 alerting rules，直接放入 Prometheus 的 <code>rule_files</code> 載入。<code>validate</code> 在 CI 中先行檢查 YAML 格式與 SLI expression 語法。</p>
<h3 id="k8s-operator-mode">K8s Operator mode</h3>
<p>Sloth 提供 K8s Operator，用 <code>PrometheusServiceLevel</code> CRD 定義 SLO。Operator 自動 reconcile，把 CRD 轉成 Prometheus rules 並同步到 Prometheus Operator 的 <code>PrometheusRule</code> 資源。</p>
<p>Operator mode 適合 K8s-native 環境：SLO 定義跟 service deployment 放在同一個 GitOps repo，變更 SLO 跟變更服務走同一套 PR review + CI 流程。</p>
<h3 id="ci--gitops-整合">CI / GitOps 整合</h3>
<p>在 CI pipeline 中跑 <code>sloth validate</code> 驗證 YAML，再跑 <code>sloth generate</code> 產出 rules，commit 進 GitOps repo。Prometheus 透過 config reload 或 Operator reconcile 載入新 rules。這條流程讓 SLO 變更有版本歷史、有 review、有 rollback 能力。</p>
<h2 id="邊界與陷阱">邊界與陷阱</h2>
<p>Sloth 只支援 Prometheus 作為後端。若觀測平台是 Datadog、New Relic、Honeycomb 或 Grafana Cloud，需要各平台自己的 SLO 功能或 <a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a> 的 multi-source 整合。</p>
<p>SLI expression 錯誤是最常見的問題。分母為零（service 沒有流量）會產生 NaN，cascading 到所有 recording rule。label 不匹配（<code>service</code> label 拼錯）會產生空 series，alert 永遠不觸發。<code>sloth validate</code> 檢查語法但不檢查 Prometheus 中是否真的有對應 series — 上線後需要用 Prometheus query 確認 recording rule 產出非空結果。</p>
<p>SLO 數量增長會累積 recording rule 成本。每個 SLO 產生約 30 條 recording rule（4 windows × 多組 aggregation）。100 個 SLO 產生 3000 條 rule，Prometheus 的 rule evaluation 會消耗明顯的 CPU 和記憶體。定期監控 <code>prometheus_rule_evaluation_duration_seconds</code> 和 <code>prometheus_rule_group_rules</code>，在 rule 數量影響 evaluation latency 前調整。</p>
<p>升級路徑：Sloth YAML 跟 OpenSLO spec 部分相容。從 Sloth 移到 Nobl9 時，SLO 定義的語意可以保留，SLI expression 需要改寫成 Nobl9 的 data source query。這條路徑適合從 Prometheus-only 環境逐步擴展到 multi-source SLO governance。</p>
<h2 id="整合路由">整合路由</h2>
<ul>
<li>上游：<a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 與 Error Budget 政策</a> — SLO 定義與 objective 來源</li>
<li>下游：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> — burn rate alert 觸發凍結</li>
<li>平行：<a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a>（SaaS multi-source）、Pyrra（K8s-native + UI）</li>
<li>案例回寫：<a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google G1</a>（error budget policy 原典）、<a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb HC1</a>（burn rate 驅動可靠性操作）</li>
</ul>
]]></content:encoded></item><item><title>10.1 服務拆分與邊界判讀</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/</guid><description>&lt;p>Monolith 與 microservice 是兩種耦合策略、各自承擔代價：monolith 用單一程式碼庫換低協作成本、microservice 用獨立邊界換團隊與部署彈性。本章處理「演進速度跟組織能力對齊」這個決策邊界 — 起點是辨識當下壓力來源、再選擇拆分軸、流行度與堅持習慣都是次要訊號。&lt;/p>
&lt;h2 id="monolith-與-microservice-的責任差異">Monolith 與 Microservice 的責任差異&lt;/h2>
&lt;p>Monolith 用「同一個程式碼庫、同一個部署單位、同一個資料庫」換取低協作成本與簡單事務語意。Microservice 用「獨立程式碼庫、獨立部署、獨立資料邊界」換取團隊獨立性、技術選型彈性與局部故障隔離。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Monolith&lt;/th>
 &lt;th>Microservice&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>本地 transaction 就解決&lt;/td>
 &lt;td>跨服務需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox&lt;/a> 或最終一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障隔離&lt;/td>
 &lt;td>單點失敗會整個服務掛掉&lt;/td>
 &lt;td>一個服務掛了，其他可能還能服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署單位&lt;/td>
 &lt;td>整個應用一次部署&lt;/td>
 &lt;td>各服務獨立部署，發布節奏不互相阻擋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>運維複雜度&lt;/td>
 &lt;td>一組基礎設施&lt;/td>
 &lt;td>N 組基礎設施 + 服務間通訊監控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Debug 路徑&lt;/td>
 &lt;td>同一個 stack trace 看到底&lt;/td>
 &lt;td>跨服務 trace context、log 聚合不可省&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;/p>
&lt;p>這張表是兩端對比、實際系統常落在中間。常見折衷形態：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/modular-monolith/" data-link-title="Modular Monolith" data-link-desc="單一部署單位 &amp;#43; 模組化內部邊界的架構、是 monolith 跟 microservice 之間的折衷形態">Modular monolith&lt;/a>&lt;/strong>（單一部署 + 模組化邊界）：保留 monolith 的部署簡單、用模組邊界防止程式碼互相穿透。Shopify、Basecamp、Stack Overflow 是大規模長期維持的代表 — monolith 不是進化中段、是 valid endgame。&lt;/li>
&lt;li>&lt;strong>Macro-services&lt;/strong>（少量大服務、5-15 個）：避免 microservice 的極端碎片化、保留拆分帶來的部署獨立性。是多數中型團隊的實際終點、不是過渡形態。&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cell-based-architecture/" data-link-title="Cell-Based Architecture" data-link-desc="把系統拆成多個 isolated cell、控制 blast radius、跨 cell 共用標準介面">Cell-based architecture&lt;/a>&lt;/strong>（多 cell 各自獨立、跨 cell 共用標準介面）：AWS、Slack、DoorDash 用來控制 blast radius — 把整個系統複製成多個 isolated cell、每個 cell 內可以是 monolith 或 microservice。&lt;/li>
&lt;/ul>
&lt;p>拆分不是進化方向、是壓力應對工具。維持 monolith 在某些情境（極小團隊、PMF 前期、無 DevOps 能力）是更負責任的選擇。&lt;/p>
&lt;h2 id="拆分軸的判讀">拆分軸的判讀&lt;/h2>
&lt;p>服務邊界不只一條軸。常見的四條軸對應不同的壓力來源，正確的拆法是「壓力在哪裡、就沿那條軸拆」，不是同時動四條軸。&lt;/p>
&lt;h3 id="資料邊界">資料邊界&lt;/h3>
&lt;p>當兩塊業務的資料&lt;strong>生命週期不同、一致性需求不同、查詢模式不同&lt;/strong>時，資料邊界已經形成。例如訂單資料需要強一致性與長期保留，瀏覽紀錄可以最終一致性、定期清理。把這兩類資料放同一個 schema 會讓 backup、migration、index 策略互相干擾。&lt;/p>
&lt;p>判讀訊號：同一張表上不同欄位的 read/write QPS 差三個數量級、同一個 transaction 同時寫入多種業務概念、schema migration 一動就要鎖住整個業務的寫入。&lt;/p>
&lt;h3 id="團隊邊界">團隊邊界&lt;/h3>
&lt;p>當兩塊業務由不同團隊維護、發布節奏不同、技術棧偏好不同時，團隊邊界已經形成。Conway&amp;rsquo;s Law 反過來操作：用服務邊界保護團隊邊界，避免一隊改動觸發另一隊重 review。&lt;/p>
&lt;p>判讀訊號：PR review 跨團隊比例過半、發版需要協調多個團隊、技術升級（語言版本、framework 升級）因為其他團隊未準備好而被擋住。&lt;/p>
&lt;h3 id="部署邊界">部署邊界&lt;/h3>
&lt;p>當部分功能需要&lt;strong>獨立的部署節奏、獨立的擴展策略、獨立的可用性等級&lt;/strong>時，部署邊界已經形成。背景批次工作要按小時排程、API 服務要 7×24 線上、報表服務只在工作日運行，三者放同一個部署單位會讓最嚴格的可用性要求拖累其他。&lt;/p>
&lt;p>判讀訊號：高峰時某個功能擴展速度跟不上、低峰時某個功能浪費資源、單一發版策略覆蓋不了所有功能的風險等級。&lt;/p>
&lt;h3 id="流量邊界">流量邊界&lt;/h3>
&lt;p>當不同功能的&lt;strong>流量形狀、失敗代價、SLO 等級不同&lt;/strong>時，流量邊界已經形成。付款 API 一秒 100 個請求、商品搜尋一秒 10000 個請求、後台報表一天 100 個請求，三者放同一個服務會讓彼此爭資源，付款被搜尋擠掉是業務災難。&lt;/p>
&lt;p>判讀訊號：高頻 endpoint 壓爆低頻 endpoint 共用的連線池、不同 endpoint 的 latency 分布同時惡化、無法針對核心交易設定獨立的 SLO 跟 alert。&lt;/p>
&lt;h3 id="其他常見拆分軸">其他常見拆分軸&lt;/h3>
&lt;p>上面四條是技術驅動的主要拆分軸。實務上還有其他軸常成為真實驅動力、要一併納入判讀：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失敗代價 / blast radius 軸&lt;/strong>：核心交易（掛了會有業務災難）跟邊緣推薦（掛了沒人在意）的可用性等級差距大、適合拆開降低 blast radius。跟 SLO 軸高相關但不同 — 重點在「失敗時誰受影響」的範圍隔離。&lt;/li>
&lt;li>&lt;strong>變更頻率 / 風險軸&lt;/strong>：high-velocity 實驗功能跟 stable 核心應拆開、降低實驗對核心穩定性的牽連。跟團隊軸高相關但獨立 — 同一團隊也可能維持兩種變更頻率的程式碼。&lt;/li>
&lt;li>&lt;strong>資料敏感度 / 合規邊界&lt;/strong>：PCI / PII / 醫療資料的隔離常是合規硬要求（GDPR data residency 強制資料拆境），不是技術選擇。這類軸跟資料邊界相關但服從不同壓力。&lt;/li>
&lt;li>&lt;strong>組織非技術約束&lt;/strong>：併購整合、外部合規節奏、團隊 reorg、預算切分都會強制拆分 — 比 metric 訊號更早觸發、技術上不一定最佳但無法繞過。&lt;/li>
&lt;/ul>
&lt;p>這些軸跟前四條可以同時生效、也可能彼此衝突（合規逼資料拆境、但流量軸建議聚合）。處理衝突時優先順序通常是「合規 &amp;gt; 失敗代價 &amp;gt; 部署 / 流量 &amp;gt; 團隊 &amp;gt; 資料 &amp;gt; 變更頻率」、但每個組織會有自己的權重。&lt;/p></description><content:encoded><![CDATA[<p>Monolith 與 microservice 是兩種耦合策略、各自承擔代價：monolith 用單一程式碼庫換低協作成本、microservice 用獨立邊界換團隊與部署彈性。本章處理「演進速度跟組織能力對齊」這個決策邊界 — 起點是辨識當下壓力來源、再選擇拆分軸、流行度與堅持習慣都是次要訊號。</p>
<h2 id="monolith-與-microservice-的責任差異">Monolith 與 Microservice 的責任差異</h2>
<p>Monolith 用「同一個程式碼庫、同一個部署單位、同一個資料庫」換取低協作成本與簡單事務語意。Microservice 用「獨立程式碼庫、獨立部署、獨立資料邊界」換取團隊獨立性、技術選型彈性與局部故障隔離。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Monolith</th>
          <th>Microservice</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>變更速度</td>
          <td>單庫改完直接上線</td>
          <td>跨服務協調，需要契約對齊</td>
      </tr>
      <tr>
          <td>事務一致性</td>
          <td>本地 transaction 就解決</td>
          <td>跨服務需要 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a>、<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox</a> 或最終一致性</td>
      </tr>
      <tr>
          <td>故障隔離</td>
          <td>單點失敗會整個服務掛掉</td>
          <td>一個服務掛了，其他可能還能服務</td>
      </tr>
      <tr>
          <td>部署單位</td>
          <td>整個應用一次部署</td>
          <td>各服務獨立部署，發布節奏不互相阻擋</td>
      </tr>
      <tr>
          <td>運維複雜度</td>
          <td>一組基礎設施</td>
          <td>N 組基礎設施 + 服務間通訊監控</td>
      </tr>
      <tr>
          <td>Debug 路徑</td>
          <td>同一個 stack trace 看到底</td>
          <td>跨服務 trace context、log 聚合不可省</td>
      </tr>
      <tr>
          <td>適合規模</td>
          <td>早期、單一團隊、業務尚未分化</td>
          <td>多團隊、業務已分化、可獨立演進</td>
      </tr>
  </tbody>
</table>
<p>讀者要從這張表反推自己的真實壓力來源。如果痛點是「部署互相卡住、發布頻率被別人拖慢」，拆分能解決；如果痛點是「程式碼太亂、新人看不懂」，拆服務只會把亂的範圍擴大成跨服務契約混亂。</p>
<p>這張表是兩端對比、實際系統常落在中間。常見折衷形態：</p>
<ul>
<li><strong><a href="/blog/backend/knowledge-cards/modular-monolith/" data-link-title="Modular Monolith" data-link-desc="單一部署單位 &#43; 模組化內部邊界的架構、是 monolith 跟 microservice 之間的折衷形態">Modular monolith</a></strong>（單一部署 + 模組化邊界）：保留 monolith 的部署簡單、用模組邊界防止程式碼互相穿透。Shopify、Basecamp、Stack Overflow 是大規模長期維持的代表 — monolith 不是進化中段、是 valid endgame。</li>
<li><strong>Macro-services</strong>（少量大服務、5-15 個）：避免 microservice 的極端碎片化、保留拆分帶來的部署獨立性。是多數中型團隊的實際終點、不是過渡形態。</li>
<li><strong><a href="/blog/backend/knowledge-cards/cell-based-architecture/" data-link-title="Cell-Based Architecture" data-link-desc="把系統拆成多個 isolated cell、控制 blast radius、跨 cell 共用標準介面">Cell-based architecture</a></strong>（多 cell 各自獨立、跨 cell 共用標準介面）：AWS、Slack、DoorDash 用來控制 blast radius — 把整個系統複製成多個 isolated cell、每個 cell 內可以是 monolith 或 microservice。</li>
</ul>
<p>拆分不是進化方向、是壓力應對工具。維持 monolith 在某些情境（極小團隊、PMF 前期、無 DevOps 能力）是更負責任的選擇。</p>
<h2 id="拆分軸的判讀">拆分軸的判讀</h2>
<p>服務邊界不只一條軸。常見的四條軸對應不同的壓力來源，正確的拆法是「壓力在哪裡、就沿那條軸拆」，不是同時動四條軸。</p>
<h3 id="資料邊界">資料邊界</h3>
<p>當兩塊業務的資料<strong>生命週期不同、一致性需求不同、查詢模式不同</strong>時，資料邊界已經形成。例如訂單資料需要強一致性與長期保留，瀏覽紀錄可以最終一致性、定期清理。把這兩類資料放同一個 schema 會讓 backup、migration、index 策略互相干擾。</p>
<p>判讀訊號：同一張表上不同欄位的 read/write QPS 差三個數量級、同一個 transaction 同時寫入多種業務概念、schema migration 一動就要鎖住整個業務的寫入。</p>
<h3 id="團隊邊界">團隊邊界</h3>
<p>當兩塊業務由不同團隊維護、發布節奏不同、技術棧偏好不同時，團隊邊界已經形成。Conway&rsquo;s Law 反過來操作：用服務邊界保護團隊邊界，避免一隊改動觸發另一隊重 review。</p>
<p>判讀訊號：PR review 跨團隊比例過半、發版需要協調多個團隊、技術升級（語言版本、framework 升級）因為其他團隊未準備好而被擋住。</p>
<h3 id="部署邊界">部署邊界</h3>
<p>當部分功能需要<strong>獨立的部署節奏、獨立的擴展策略、獨立的可用性等級</strong>時，部署邊界已經形成。背景批次工作要按小時排程、API 服務要 7×24 線上、報表服務只在工作日運行，三者放同一個部署單位會讓最嚴格的可用性要求拖累其他。</p>
<p>判讀訊號：高峰時某個功能擴展速度跟不上、低峰時某個功能浪費資源、單一發版策略覆蓋不了所有功能的風險等級。</p>
<h3 id="流量邊界">流量邊界</h3>
<p>當不同功能的<strong>流量形狀、失敗代價、SLO 等級不同</strong>時，流量邊界已經形成。付款 API 一秒 100 個請求、商品搜尋一秒 10000 個請求、後台報表一天 100 個請求，三者放同一個服務會讓彼此爭資源，付款被搜尋擠掉是業務災難。</p>
<p>判讀訊號：高頻 endpoint 壓爆低頻 endpoint 共用的連線池、不同 endpoint 的 latency 分布同時惡化、無法針對核心交易設定獨立的 SLO 跟 alert。</p>
<h3 id="其他常見拆分軸">其他常見拆分軸</h3>
<p>上面四條是技術驅動的主要拆分軸。實務上還有其他軸常成為真實驅動力、要一併納入判讀：</p>
<ul>
<li><strong>失敗代價 / blast radius 軸</strong>：核心交易（掛了會有業務災難）跟邊緣推薦（掛了沒人在意）的可用性等級差距大、適合拆開降低 blast radius。跟 SLO 軸高相關但不同 — 重點在「失敗時誰受影響」的範圍隔離。</li>
<li><strong>變更頻率 / 風險軸</strong>：high-velocity 實驗功能跟 stable 核心應拆開、降低實驗對核心穩定性的牽連。跟團隊軸高相關但獨立 — 同一團隊也可能維持兩種變更頻率的程式碼。</li>
<li><strong>資料敏感度 / 合規邊界</strong>：PCI / PII / 醫療資料的隔離常是合規硬要求（GDPR data residency 強制資料拆境），不是技術選擇。這類軸跟資料邊界相關但服從不同壓力。</li>
<li><strong>組織非技術約束</strong>：併購整合、外部合規節奏、團隊 reorg、預算切分都會強制拆分 — 比 metric 訊號更早觸發、技術上不一定最佳但無法繞過。</li>
</ul>
<p>這些軸跟前四條可以同時生效、也可能彼此衝突（合規逼資料拆境、但流量軸建議聚合）。處理衝突時優先順序通常是「合規 &gt; 失敗代價 &gt; 部署 / 流量 &gt; 團隊 &gt; 資料 &gt; 變更頻率」、但每個組織會有自己的權重。</p>
<h2 id="拆分時機的判讀">拆分時機的判讀</h2>
<p>拆分時機不能等到「已經痛到動不了」才開始，那時候拆分要付的代價最高。也不能在「還沒長出邊界」時提早拆，那會用 microservice 的協調成本懲罰一個還沒到規模的系統。</p>
<p>提早訊號（可以開始準備但不一定立刻動手）：</p>
<ul>
<li>程式碼裡同一份邏輯被三個 PR 同時修改、merge conflict 增加</li>
<li>同一個 service 的不同功能開始有不同的擴展需求</li>
<li>不同團隊對同一個發版視窗的需求開始衝突</li>
</ul>
<p>該動手訊號（再拖就要付高昂代價）：</p>
<ul>
<li>任何一個功能改動需要 freeze 整個服務發版</li>
<li>局部高峰擴展時整個服務一起擴展，成本翻倍</li>
<li>一個團隊的事故會直接影響另一個團隊的營運指標</li>
<li>跨團隊 deadlock：A 等 B 改完才能上、B 等 A 改完才能上</li>
</ul>
<p>過晚訊號（拆分要付遷移代價）：</p>
<ul>
<li>已經出現過跨團隊事故、且復盤結論是「無法分離責任」</li>
<li>DB 連線池在多個業務間爭搶、無法用 connection pool 隔離解決</li>
<li>部署平台跑不動：CI 太慢、build 太大、本地開發無法啟動完整環境</li>
</ul>
<h2 id="拆分代價與回退路徑">拆分代價與回退路徑</h2>
<p>拆分不是免費操作。每多一個服務，就多一份運維成本、跨服務 trace 成本、契約治理成本。讀者要在拆分前認知這些代價，而不是事後才發現。</p>
<table>
  <thead>
      <tr>
          <th>代價類型</th>
          <th>具體表現</th>
          <th>緩解方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>分散事務</td>
          <td>一筆業務動作跨多個服務、需要 saga 或最終一致性</td>
          <td><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 的 outbox、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></td>
      </tr>
      <tr>
          <td>運維複雜度</td>
          <td>N 個服務 × M 個環境 × K 個版本，組合爆炸</td>
          <td>收斂部署平台、用 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s 部署策略</a> 統一管理</td>
      </tr>
      <tr>
          <td>跨服務 debug</td>
          <td>一個請求跨多個服務、不知道在哪一段失敗</td>
          <td><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">04 trace context</a>、結構化 log 聚合</td>
      </tr>
      <tr>
          <td>契約治理</td>
          <td>服務 A 的 API 改動會影響服務 B、C、D</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract test</a>、版本化 API</td>
      </tr>
      <tr>
          <td>資料一致性</td>
          <td>各服務 DB 獨立，跨服務查詢需要 join 或 read model</td>
          <td>CQRS、event-driven projection、reconciliation</td>
      </tr>
  </tbody>
</table>
<p>拆分失敗的回退路徑要在拆分前設計好。常見回退策略：保留原 monolith 程式碼一段時間（雙寫期），新服務出問題可以切回；先拆<strong>讀路徑</strong>驗證流量，再拆寫路徑；用 feature flag 控制是否走新服務。沒有回退路徑的拆分一旦撞牆，會比不拆更難收拾。</p>
<h3 id="拆分後的通訊優先級事件--同步-rpc">拆分後的通訊優先級：事件 &gt; 同步 RPC</h3>
<p>拆完後跨服務通訊有兩條路：同步 RPC（gRPC、REST）跟異步事件（<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a>、event bus）。預設應該選事件、保留 RPC 給「真的需要同步回應的查詢」。</p>
<p>理由：</p>
<ul>
<li><strong>失敗代價隔離</strong>：服務 A 發事件給 B、B 掛了不影響 A — 事件留在 queue 等。同步 RPC 下、B 掛了 A 也跟著掛</li>
<li><strong>流量解耦</strong>：事件本身就是 buffer、能吸收 burst。同步 RPC 是 throughput 的硬上限、A 的尖峰 = B 的尖峰</li>
<li><strong>可重放</strong>：事件可以重放（replay）做資料修補、debug、新服務 backfill。同步 RPC 過了就過了</li>
<li><strong>服務獨立演進</strong>：事件 schema 可以加欄位向下相容、consumer 慢慢 adapt。RPC interface 改動是 breaking change</li>
</ul>
<p>該用同步 RPC 的少數場景：使用者請求路徑需要立即回應（「使用者按下查詢、顯示結果」）、且兩個服務都在同一個 latency budget 內。其他都優先事件。</p>
<p>詳見 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a> 跟 <a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步與事件傳遞選型</a>。</p>
<h2 id="反例拆分過度的收回">反例：拆分過度的收回</h2>
<p>服務拆分的反向動作是合併。當拆分後發現「服務間呼叫太頻繁、近乎同步、跨服務事務太多」時，代表這條邊界拆錯了。處理方式是把這兩個服務合回去，繼續增加跨服務工具只會堆疊複雜度。</p>
<p>判讀「該合併」的訊號：服務 A 與 B 之間每秒幾百次同步呼叫且失敗會連鎖、A 改動必定觸發 B 改動且兩者由同一團隊維護、跨服務事務佔總業務動作比例過高、跨服務 latency 是 SLO 主要消耗者。</p>
<p>合併不是失敗。它代表團隊已經理解這條邊界不該存在，及時收回比硬撐更負責任。Modular monolith（單一部署、模組化邊界）是常見的折衷形態：保留模組邊界、避免分散事務代價、未來壓力出現時再分拆。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多團隊發版互相阻擋</td>
          <td>部署邊界已形成、但服務仍綁在一起</td>
          <td>從 CI/部署單位開始拆，先讓發布獨立</td>
      </tr>
      <tr>
          <td>同一服務不同功能擴展需求差距大</td>
          <td>流量邊界已形成</td>
          <td>沿流量軸拆，高頻 endpoint 獨立服務 + 獨立 auto scaling</td>
      </tr>
      <tr>
          <td>DB 寫入鎖跨業務互相影響</td>
          <td>資料邊界已形成</td>
          <td>沿資料軸拆，獨立 schema 與獨立 DB instance</td>
      </tr>
      <tr>
          <td>拆分後跨服務同步呼叫激增</td>
          <td>邊界拆錯、實際耦合並未被服務界線解開</td>
          <td>評估合併、或改用事件驅動把同步呼叫變成非同步交接</td>
      </tr>
      <tr>
          <td>拆分後事故 MTTR 拉長</td>
          <td>跨服務觀測能力跟不上</td>
          <td>補 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">04 trace context</a> 與 service topology</td>
      </tr>
      <tr>
          <td>拆分後 dev velocity 反而下降</td>
          <td>契約治理跟跨服務協作成本超過拆分收益</td>
          <td>評估合併或建立 shared kernel</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「technical debt」當成拆分理由。Monolith 程式碼髒亂的解法是重構，不是拆服務。拆服務只是把髒亂從單庫變成跨服務契約混亂，問題並沒有消失。</p>
<p>把「跟風 microservice」當成決策。沒有業務壓力、團隊規模不到位、運維能力不夠的情況下拆服務，新的協作成本會壓垮整個團隊，這比 monolith 的痛苦更大。</p>
<p>把拆分當成單向操作。沒有設計回退路徑、沒有保留合併選項，拆錯了就只能硬撐。成熟的服務演進策略要把「拆」跟「合」當成雙向可逆操作。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「該不該拆、沿哪條軸拆、拆完怎麼收尾」。當問題進入具體拆分後的部署、流量、觀測責任，分別交給以下模組：</p>
<ul>
<li>服務獨立部署 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform</a></li>
<li>跨服務交接與事件 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a></li>
<li>跨服務觀測與 trace → <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a></li>
<li>跨服務一致性與冪等性 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 idempotency-replay</a> + outbox pattern</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>服務拆分判讀可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 +75%、成本 -28%</a> — 反例方向：原本各 microservice 各自 DB 造成運維碎片化、最後做 consolidation；對照本章「拆分過度的收回」段。</li>
<li><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：EKS 平台整併與標準化</a> — Condé Nast 把多 brand 各自的 K8s cluster 整併到統一 EKS 控制面、降低跨團隊運維分歧。對照本章「拆分代價 / 運維複雜度」段：拆出去快、合回來慢、設計時就要評估這種非對稱性。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理</a> — Riot 的拆分軸是「遊戲 × 地區 × 環境」三維交集、246 個 cluster 是這三軸的笛卡兒積取一個 subset。對照本章「拆分軸 / 部署邊界」段：實務上的拆分常常是多軸交集、不是單軸推進。</li>
</ul>
<p>Netflix Aurora consolidation 是反例最有教學價值的一筆 — 它證明「拆 microservice 各自 DB → consolidation 回 Aurora」是 valid endgame、拆服務不是單向操作。Condé Nast 跟 Riot Games 補充另兩條維度：碎片化的運維代價、多軸交集的設計複雜度。把這三筆放回「拆分時機判讀」框架的不同節點上、能看出拆分決策的本質是「沿哪幾條軸 + 接受哪些代價」的組合。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1 後端服務能力地圖</a> 的交接：拆分前要先理解每塊責任屬於哪種能力分類，避免拆出語意混亂的服務。</li>
<li>與 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5 流量與資料量評估</a> 的交接：流量軸拆分要先有流量基線。</li>
<li>與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 的交接：拆分後跨服務通訊優先用事件、不是同步 RPC。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：拆分常常是水平擴展的前提（無狀態服務拆分後才能獨立水平擴展）。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a></strong>：拆分後接著要為每個服務選擇擴展軸。</p>
<p>其他延伸方向：</p>
<ul>
<li>實作層：服務如何獨立部署 → <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a></li>
<li>事件層：拆分後跨服務通訊設計 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a></li>
</ul>
]]></content:encoded></item><item><title>DB3 Vendor Selection：document / KV / multi-model 三方選型 + workload shape 前置判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/</guid><description>&lt;p>DB3 vendor selection 的核心責任是把讀者從「我該選 MongoDB / DynamoDB / Cosmos DB 哪一家」這個問題、推到「我的 workload 是 document / KV / multi-model 哪一類」這個更前置的問題。三家文件都標榜 scalable schema-less、但實際取捨在 &lt;em>資料形狀、access pattern 穩定度、consistency 可接受度&lt;/em> 三軸決定 — 不識別 workload shape 直接比 vendor 是源頭錯誤。本文是 DB3 reader 進來的第一站：先做 workload shape 三軸前置判讀、再過 migration path 三型 + federated DB 視角、最後落到三 vendor 對比 10 軸。&lt;/p>
&lt;p>本文 &lt;em>不&lt;/em> 展開 vendor 機制細節（partition key 設計 / consistency level / RU sizing / connection management 等）— 那些屬 per-vendor deep article 的責任、本文在每個軸後 cross-link 過去。本文也 &lt;em>不&lt;/em> 比較三家「誰比較強」— 三 vendor 在 workload-by-workload 適配光譜上各有位置、寫成優劣比較會誤導讀者把選型壓成單軸。&lt;/p>
&lt;h2 id="問題情境讀者進來時的真實壓力">問題情境：讀者進來時的真實壓力&lt;/h2>
&lt;p>典型啟動壓力分兩類：&lt;/p>
&lt;p>第一類、團隊評估 document / KV / multi-model NoSQL 三家、文件都說「scalable schema-less」、看不出實際取捨。讀者徵兆是「我的資料是 document-shaped 還是 KV-shaped？」「partition key 該怎麼選？」「Atlas 跟 Cosmos DB MongoDB API 不一樣的點在哪？」「Cosmos DB multi-model 是真用得到還是行銷話術？」「on-demand vs provisioned 怎麼選？」&lt;/p>
&lt;p>第二類、既有 PostgreSQL / MySQL workload 撞 connection limit（surge 下 1K-5K pool 是隱性天花板、F1.7）、想換 KV 但不知道是否適合。讀者徵兆是「我已經有 Memcached、還要再加 MongoDB cache 層嗎？」「DynamoDB 適合當 OLTP 嗎？」「換 NoSQL 是不是解 connection 問題的銀彈？」&lt;/p>
&lt;p>這兩類讀者進來時的 &lt;em>真實問題&lt;/em> 不在 vendor 之間、在 &lt;em>workload 自己屬哪一型&lt;/em>。Case anchor 覆蓋六個 unique 角度：&lt;/p>
&lt;ul>
&lt;li>多型 document workload — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a>（車載 sensor schema 隨車型演進、20 個 Atlas DB blast radius 切分）&lt;/li>
&lt;li>Document 跨雲 hedging — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a>（自管 → Atlas、6 個月遷移、跨雲彈性）&lt;/li>
&lt;li>同 model 換 vendor 的 dogfood signal — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>（MongoDB → Cosmos DB MongoDB API、保留 driver、wire compat 限制）&lt;/li>
&lt;li>KV-as-buffer 正向用例 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a>（DynamoDB 寫入緩衝、6750x 彈性、後端慢消費）&lt;/li>
&lt;li>PK 天然均勻典範 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a>（90M reads/sec 年度峰值、KV pattern 純粹）&lt;/li>
&lt;li>Federated DB 真實系統 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a>（MongoDB + DynamoDB + Memcached + mongobetween + freshness token）&lt;/li>
&lt;/ul>
&lt;h2 id="workload-shape--access-pattern--consistency-三軸前置判讀">Workload shape × access pattern × consistency 三軸前置判讀&lt;/h2>
&lt;p>進三家 vendor 對比前先回答：你的 workload 屬哪一型？三軸的組合決定 vendor 候選清單、軸不識別清楚直接比 vendor 是把選型壓成「品牌偏好」、不是工程決策。&lt;/p></description><content:encoded><![CDATA[<p>DB3 vendor selection 的核心責任是把讀者從「我該選 MongoDB / DynamoDB / Cosmos DB 哪一家」這個問題、推到「我的 workload 是 document / KV / multi-model 哪一類」這個更前置的問題。三家文件都標榜 scalable schema-less、但實際取捨在 <em>資料形狀、access pattern 穩定度、consistency 可接受度</em> 三軸決定 — 不識別 workload shape 直接比 vendor 是源頭錯誤。本文是 DB3 reader 進來的第一站：先做 workload shape 三軸前置判讀、再過 migration path 三型 + federated DB 視角、最後落到三 vendor 對比 10 軸。</p>
<p>本文 <em>不</em> 展開 vendor 機制細節（partition key 設計 / consistency level / RU sizing / connection management 等）— 那些屬 per-vendor deep article 的責任、本文在每個軸後 cross-link 過去。本文也 <em>不</em> 比較三家「誰比較強」— 三 vendor 在 workload-by-workload 適配光譜上各有位置、寫成優劣比較會誤導讀者把選型壓成單軸。</p>
<h2 id="問題情境讀者進來時的真實壓力">問題情境：讀者進來時的真實壓力</h2>
<p>典型啟動壓力分兩類：</p>
<p>第一類、團隊評估 document / KV / multi-model NoSQL 三家、文件都說「scalable schema-less」、看不出實際取捨。讀者徵兆是「我的資料是 document-shaped 還是 KV-shaped？」「partition key 該怎麼選？」「Atlas 跟 Cosmos DB MongoDB API 不一樣的點在哪？」「Cosmos DB multi-model 是真用得到還是行銷話術？」「on-demand vs provisioned 怎麼選？」</p>
<p>第二類、既有 PostgreSQL / MySQL workload 撞 connection limit（surge 下 1K-5K pool 是隱性天花板、F1.7）、想換 KV 但不知道是否適合。讀者徵兆是「我已經有 Memcached、還要再加 MongoDB cache 層嗎？」「DynamoDB 適合當 OLTP 嗎？」「換 NoSQL 是不是解 connection 問題的銀彈？」</p>
<p>這兩類讀者進來時的 <em>真實問題</em> 不在 vendor 之間、在 <em>workload 自己屬哪一型</em>。Case anchor 覆蓋六個 unique 角度：</p>
<ul>
<li>多型 document workload — <a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a>（車載 sensor schema 隨車型演進、20 個 Atlas DB blast radius 切分）</li>
<li>Document 跨雲 hedging — <a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas、6 個月遷移、跨雲彈性）</li>
<li>同 model 換 vendor 的 dogfood signal — <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（MongoDB → Cosmos DB MongoDB API、保留 driver、wire compat 限制）</li>
<li>KV-as-buffer 正向用例 — <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（DynamoDB 寫入緩衝、6750x 彈性、後端慢消費）</li>
<li>PK 天然均勻典範 — <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a>（90M reads/sec 年度峰值、KV pattern 純粹）</li>
<li>Federated DB 真實系統 — <a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（MongoDB + DynamoDB + Memcached + mongobetween + freshness token）</li>
</ul>
<h2 id="workload-shape--access-pattern--consistency-三軸前置判讀">Workload shape × access pattern × consistency 三軸前置判讀</h2>
<p>進三家 vendor 對比前先回答：你的 workload 屬哪一型？三軸的組合決定 vendor 候選清單、軸不識別清楚直接比 vendor 是把選型壓成「品牌偏好」、不是工程決策。</p>
<h3 id="軸-1--資料形狀document--kv--不清楚">軸 1 — 資料形狀：document / KV / 不清楚</h3>
<p>資料形狀的核心判讀是 <em>aggregate root 邊界是否明確</em> 跟 <em>schema 是否會隨產品演進新增欄位</em>。document 適合的場景是資料天然多型、單筆記錄欄位差異大、應用層用 aggregate root 模式存取；KV 適合的場景是資料形狀固定、access pattern 數量少（&lt; 5 種）、固定 lookup by key。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>適配資料模型</th>
          <th>對應 case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料天然多型（不同記錄欄位不同）、隨產品演進 schema 增刪欄位、aggregate root 邊界明確</td>
          <td>Document（MongoDB / Cosmos DB SQL API / MongoDB API）</td>
          <td>Toyota sensor schema 隨車型演進、Forbes CMS article 欄位多型</td>
      </tr>
      <tr>
          <td>資料形狀固定、access pattern &lt; 5 種、固定 lookup by key（meeting_id / message_id / user_id）</td>
          <td>KV（DynamoDB / Cosmos DB Table API / Redis 持久化變體）</td>
          <td>Amazon Ads 用 ad_id 查、Disney+ 用 user_id 查 watchlist、PayPay 用 message_id 查通知</td>
      </tr>
      <tr>
          <td>資料形狀還在探索、access pattern 變動頻繁、未來 6 個月會加 5+ 種新 query</td>
          <td>暫緩 NoSQL 選型、用 PostgreSQL + JSONB 過渡</td>
          <td>屬讀者誤判常見模式、case 沒揭露但 F1.3 / F1.6 推論：NoSQL 假設 access pattern 穩定、未穩定就上 NoSQL 會撞 single-table 設計天花板</td>
      </tr>
  </tbody>
</table>
<p>第三列的「暫緩 NoSQL」是反指標。NoSQL（特別是 DynamoDB single-table design）的核心假設是「access pattern 在設計時已知、後續變動有限」。資料模型還在探索、access pattern 半年內會大幅增減的場景、PostgreSQL + JSONB 給的彈性遠高於 NoSQL — JSONB 欄位可以演進、ad-hoc query 可以用 SQL 跑、未來釐清穩定 access pattern 後再選 NoSQL 不遲。</p>
<h3 id="軸-2--access-pattern-穩定度kv-適用度前置判讀">軸 2 — Access pattern 穩定度（KV 適用度前置判讀）</h3>
<p>KV 適用度的核心判讀是 <em>partition key 天然均勻度</em>。partition key 不均勻會讓 vendor 廣告的「scale infinitely」變成「scale 到 hot partition 為止」、單一 logical key 流量超過該 partition 上限就 throttle 或 latency spike（F1.1）。</p>
<ul>
<li><strong>天然均勻 PK + 穩定 access pattern</strong>（meeting_id / player_id / message_id / user_id）→ DynamoDB / Cosmos DB Table API 適用、PK 不需 composite key 修補。Amazon Ads 用 ad_id 撐 90M reads/sec、Zoom 用 meeting_id、Capcom 用 player_id、PayPay 用 message_id、Disney+ 用 user_id — 五個 case 都揭露同一 frame：<em>業務天然存在均勻 key 時 KV 是最自然的選擇</em>。</li>
<li><strong>天然不均勻 PK</strong>（event_id 一場演唱會集中 / date 時間序集中）→ 需 composite key 或 write sharding 修補。Tixcraft（9.C15）用 <code>event_id + user_id_hash</code> composite key 把單一熱門演唱會的 6750x spike 攤平到 partition 上 — 不是 DynamoDB 自身彈性、是 partition key 均勻分散的結果（F1.2）。</li>
<li><strong>Access pattern 變動頻繁</strong>（探索期、&lt; 5 種 query 還會增加）→ 不適合 DynamoDB single-table design、回 RDB。Single-table 把 access pattern 編進 PK / SK 結構、增加新 query 等於改 schema、改 schema 等於重新 load 資料、成本不對。</li>
</ul>
<p>KV 適用度判讀的延伸細節（hot partition 反模式 / composite key 設計 / adaptive capacity）見 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key antipatterns</a>。</p>
<h3 id="軸-3--consistency-需求是否可接受-eventual">軸 3 — Consistency 需求是否可接受 eventual</h3>
<p>Consistency 需求的核心判讀是 <em>跨 partition / 跨 region transaction 是否為產品契約</em>。三家 vendor 都支援單 partition / 單 region 強一致、但 cross-partition / cross-region transaction 的機制跟限制差異大。</p>
<ul>
<li><strong>可接受 eventual / session consistency</strong>：DynamoDB（default eventually consistent reads、可選 strong）、Cosmos DB（5 個 consistency level、default session）、MongoDB（read concern 多級）— 三家都可以、選擇看其他軸。多數 KV / document workload 屬此類（social timeline、watchlist、message queue、analytics aggregation）。</li>
<li><strong>需要強一致 cross-partition transaction</strong>：DynamoDB 跨 partition transaction 限制（單一 transaction 最多 100 個 action、跨 region 不支援）、MongoDB 4.0+ 支援 multi-document transaction 但 sharded cluster 仍有 limitation、Cosmos DB 跨 logical partition transaction 受限 — 都不如 SQL／distributed SQL 自然、應回 DB4 entry point 評估 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB</a>。</li>
<li><strong>跨 region active-active write</strong>：三家機制完全不同 — Cosmos DB multi-region write 跟 Strong consistency 是 <em>互斥</em> 設定（CAP 取捨硬約束、見 <a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region write conflict</a> SSoT 主寫位置）；DynamoDB Global Tables 走 LWW（last-writer-wins）conflict resolution；MongoDB Atlas 跨 region 需手動 conflict 處理。三家不在同一光譜、選擇前必看各 vendor outline 的機制段。</li>
</ul>
<h2 id="migration-path-三型跨-case-合成-frame">Migration path 三型（跨 case 合成 frame）</h2>
<blockquote>
<p>本段是 <em>跨 case 合成 frame</em>、不是單一 case 揭露 — 從 Coinbase（9.C36）/ Forbes（9.C37）/ Microsoft 365（9.C30）三 case 萃取的共通結構（F2.1）。</p></blockquote>
<p>讀者進來時通常不是綠地、是 <em>既有系統演進</em>。三型遷移路徑的風險、ROI、適用條件完全不同、選錯路徑會推到錯的 vendor。</p>
<h3 id="第一型保留原-db--補周邊工具">第一型：保留原 DB + 補周邊工具</h3>
<p>不換 vendor、加 connection proxy（mongobetween / pgbouncer 類）、加 cache（Memcached + freshness token）、加 predictive scaling — 主資料層不動、應用層跟 ops 層補強。</p>
<ul>
<li><strong>代表 case</strong>：Coinbase（9.C36）保留 MongoDB Atlas、自建 mongobetween 把 60K connections/min 降到 ~2K（一個量級）、用 Memcached + freshness token 撐 1.5M reads/sec、用 ML predictive scaling 把擴容時間從 70 → 25 分鐘提前 60 分鐘</li>
<li><strong>路徑成本</strong>：中（自建工具、需要工程資源 build &amp; operate proxy / cache layer / ML model）</li>
<li><strong>風險</strong>：低（主資料層不動、回滾代價小）</li>
<li><strong>ROI</strong>：保留主資料 schema + access pattern、解 driver / 部署模型 / cache 一致性瓶頸</li>
<li><strong>適合</strong>：MongoDB（或主 DB）資料層撐得住、但應用層 connection storm / cache miss / 擴容慢卡瓶頸；團隊有工程能力 build 跟 maintain 周邊工具</li>
</ul>
<p>延伸實作細節見 MongoDB connection management（per-vendor article、cross-link 待寫稿）。</p>
<h3 id="第二型同-db-換託管">第二型：同 DB 換託管</h3>
<p>自管 → managed（Atlas / Cosmos DB / DocumentDB）、保留 schema 跟 access pattern、遷移期 6 個月量級。</p>
<ul>
<li><strong>代表 case</strong>：Forbes（9.C37）自管 MongoDB → MongoDB Atlas、保留 CMS schema、6 個月遷移、揭露「TCO 改善 25%」</li>
<li><strong>路徑成本</strong>：中（dual-write + shadow read 驗證、driver 行為差異、operation runbook 重寫）</li>
<li><strong>風險</strong>：中（dual-write 期間雙寫一致性、cutover 時點選擇）</li>
<li><strong>ROI</strong>：operation transfer（DBA bandwidth 釋放給 schema design / query tuning）+ TCO 改善</li>
<li><strong>適合</strong>：自管 ops burden 大（DBA bandwidth 被 backup / patching / replica lag 吃光）、不想換 model</li>
</ul>
<p><strong>Scope warning（Forbes 25% TCO）</strong>：「25% TCO 改善」是 Forbes 特定流量規模（120M MAU、70+ Atlas region）下的數字、<em>不普適</em>。引用要帶條件 — 不要寫成「Atlas 比自管便宜 25%」這種 vendor-neutral 結論。實際省多少要看自管當下的 license / hardware / ops 工時分配、跟 Atlas 在你流量規模下的 pricing tier。</p>
<h3 id="第三型換-vendor-保留-model">第三型：換 vendor 保留 model</h3>
<p>MongoDB → Cosmos DB MongoDB API、或 MongoDB → DocumentDB — wire protocol + driver 不變、底層架構整個換、ops 模型整個換。</p>
<ul>
<li><strong>代表 case</strong>：Microsoft 365（9.C30）MongoDB → Cosmos DB MongoDB API、保留 MongoDB driver</li>
<li><strong>路徑成本</strong>：高（dual-write per query pattern 驗證、wire compat ≠ 100% 行為相同、aggregation pipeline 跟 transaction 行為要逐項驗證）</li>
<li><strong>風險</strong>：高（每個 query pattern 都可能踩到不相容 edge case、cutover 點選擇難）</li>
<li><strong>ROI</strong>：跨 vendor 換（Azure 生態 / multi-model API / global distribution）+ 保留應用層 driver code</li>
</ul>
<p><strong>Scope warning（Microsoft 365 dogfood）</strong>：Microsoft 365 是 Microsoft 自家 dogfood、case 沒揭露具體 throughput / latency / cost 數字（F2.17）。dogfood 是 <em>高權重 selection signal</em>（雲商賭自家旗艦產品）、但 <em>不是 production benchmark</em>（沒公開數字可比對）。引用要明示「dogfood signal」而非「production proof」。</p>
<p><strong>Scope warning（100% wire compat）</strong>：Cosmos DB MongoDB API 廣告「100% wire compatibility」是 <em>vendor 行銷話術</em>、實際是「在某些 query pattern 下相容」（F2.9）。遷移時必須 <em>dual-write per query pattern</em> 驗證 — 不是看 vendor 文件 spec list、是用 production query corpus 跑一遍實測行為。Phase 0 audit checklist 應列出 unsupported aggregation stage、transaction edge case、index behavior 差異、change stream 跟 Change Feed 對應關係。</p>
<p>延伸 Cosmos DB MongoDB API vs SQL API 選型見 <a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>。</p>
<h3 id="第四型不在-db3-範圍paradigm-shift-換引擎">第四型不在 DB3 範圍：paradigm shift 換引擎</h3>
<p>KV → SQL 或 SQL → distributed SQL 屬 paradigm shift、應進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a>。本文範圍是 DB3 三家內部選型、不展開 paradigm shift。</p>
<h2 id="從-rdb-撞牆來的快速路徑">從 RDB 撞牆來的快速路徑</h2>
<p>讀者若從 PostgreSQL / Aurora connection limit 撞牆過來、想評估 KV 替代、依撞牆訊號直接 route 到對應 article、不必先跑完三軸前置判讀：</p>
<ul>
<li><strong>撞 connection limit</strong>（surge 下 pool 1K-5K 隱性天花板、long-lived TCP 占滿）→ HTTP API 模型（no long-lived connection）的 KV 直接接寫入緩衝、進 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">dynamodb/single-table-design-pattern</a> 的「durable queue / write buffer」段（Tixcraft 9.C15 路徑：DynamoDB 接訂單、傳統 server 慢消費）、或評估 <a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB Table API</a></li>
<li><strong>撞單 primary 寫入上限</strong>（單 leader 寫吞吐天花板、read replica 無法分擔寫）→ multi-primary distributed SQL 路徑、進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a> 的 Path A（DoorDash 1.636 M QPS 單主寫入撞牆）</li>
<li><strong>撞單一 DB 撐不下 + 多 workload 形狀並存</strong>（read-heavy / write-heavy / analytics 混在一個 DB）→ federated DB 模式、看 <a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（MongoDB + DynamoDB + Memcached + mongobetween）+ <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>（PostgreSQL → DynamoDB 揭露 RDB connection limit 隱性 bottleneck）</li>
</ul>
<p>進 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">dynamodb/single-table-design-pattern</a> 前先確認軸 1 / 軸 2 的 access pattern 穩定度跟 PK 天然均勻度 — connection limit 訊號 <em>必要但不充分</em>、KV 適用度 4 軸還是要走完、避免「為了解 connection 把不穩定 access pattern 硬塞 single-table」反模式。</p>
<h2 id="federated-db--system-role-視角跨-case-合成-frame">Federated DB + system role 視角（跨 case 合成 frame）</h2>
<blockquote>
<p>本段也是 <em>跨 case 合成 frame</em>（F2.18 + F1.6）— 三個 rich case（Coinbase / Toyota / Forbes）都揭露 production 系統是 <em>DB + 周邊工具</em> 組合、不是單一 DB monolithic 撐起來。</p></blockquote>
<p>讀者常誤以為「全用 X」是正解 — 全用 MongoDB、或全遷 DynamoDB、或全換 Cosmos DB。真實 production case 揭露兩個更前置的事實：(a) production 系統是 <em>federated</em>（多 DB 按 workload 分流）、不是 monolithic；(b) 每個 vendor 在系統中扮演 <em>特定角色</em>（control plane vs data plane vs cache）、不是 all-purpose store。</p>
<h3 id="federated-db-by-workload">Federated DB by workload</h3>
<p>Coinbase（9.C36）production 配置：MongoDB Atlas（document 主資料、identity service）+ DynamoDB（部分固定 KV workload）+ Memcached（read cache）+ mongobetween（connection proxy）+ Kinesis（event stream）。不是「全用 MongoDB」也不是「全遷 DynamoDB」、是按 workload shape 分流。</p>
<p>Toyota Connected（9.C38）：MongoDB Atlas 20 個 DB（microservice 拆 blast radius）+ Lambda + Kinesis + Redis + Kubernetes。20 個 DB 不是吞吐撐不住（18B txn/月 ≈ 7K txn/sec、單一 cluster 撐得下）、是 <em>microservice ownership</em> + <em>blast radius</em> 切分（F2.6）。</p>
<p>Forbes（9.C37）：MongoDB Atlas + 中介 abstraction layer + 50+ microservice。abstraction layer 隔離 schema 變動、避免 50 個服務都依賴 DB schema 細節（F2.3）。</p>
<p>三 case 揭露的共同 frame 是：<strong>寫 production 系統時假設「DB 一個服務搞定」、忽略 cache / queue / proxy / abstraction layer 跨層責任、會撞 connection limit / cache miss / cross-region replication 等隱性瓶頸</strong>。</p>
<h3 id="system-rolecontrol-plane-vs-data-plane">System role：control plane vs data plane</h3>
<p>DynamoDB 在 surge 場景能撐 nearly infinitely 不是 DynamoDB 自己神奇、是 <em>系統架構解耦</em> 的結果（F1.6）：</p>
<ul>
<li><strong>Control plane（metadata、state、user record）</strong>：DynamoDB / MongoDB / Cosmos DB 適合 — 流量是 small payload + high QPS pattern</li>
<li><strong>Data plane（影音、大型 BLOB、media stream）</strong>：CDN / S3 / object storage、<em>不在 DB3 範圍</em> — 流量是 large payload + bandwidth-bound</li>
<li><strong>Cache layer</strong>：Redis / Memcached / DAX（DynamoDB 補位）— 跟主 DB 形成跨層架構、處理讀峰值 + read-your-own-write 一致性</li>
</ul>
<p>三個 case 揭露同一 frame：Zoom 視訊 metadata 走 DynamoDB、影音走 WebRTC / edge servers；Disney+ watchlist 走 DynamoDB、影片串流走 CDN + S3；Capcom game state 走 DynamoDB + DAX、game server 走 EKS。<strong>把影音串流塞 DynamoDB 是違反 control plane vs data plane 分離、容量規劃會錯</strong>（每筆 1KB 的 KV vs 每筆 100MB 的 media chunk 是不同 workload）。</p>
<h2 id="三-vendor-對比-10-軸">三 vendor 對比 10 軸</h2>
<p>下表是三 vendor 在 selection 階段的 10 軸對比。每個軸後續都有 per-vendor deep article 展開機制、本文不重複展開。</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>MongoDB</th>
          <th>DynamoDB</th>
          <th>Cosmos DB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>資料模型核心</strong></td>
          <td>Document（aggregate root）+ aggregation pipeline</td>
          <td>KV with optional document fields + GSI / LSI</td>
          <td>Multi-model（SQL / MongoDB / Cassandra / Gremlin / Table API）</td>
      </tr>
      <tr>
          <td><strong>部署 topology</strong></td>
          <td>跨雲（Atlas AWS / GCP / Azure）+ self-hosted</td>
          <td>AWS-only managed</td>
          <td>Azure-only managed</td>
      </tr>
      <tr>
          <td><strong>跨雲 hedging</strong></td>
          <td>高（Atlas 跨雲、Forbes case）</td>
          <td>無（AWS lock-in）</td>
          <td>無（Azure lock-in）</td>
      </tr>
      <tr>
          <td><strong>Capacity 抽象</strong></td>
          <td>CPU + IOPS + working set RAM 三軸</td>
          <td>WCU/RCU + on-demand/provisioned + adaptive capacity</td>
          <td>RU（Request Unit）+ 5 consistency level</td>
      </tr>
      <tr>
          <td><strong>Contract layer</strong></td>
          <td>DB 層 <code>$jsonSchema</code> validator / app 層 abstraction / 混合</td>
          <td>DynamoDB Stream + app 層 validator</td>
          <td>DB 層 stored procedure + app 層 validator</td>
      </tr>
      <tr>
          <td><strong>Partition / shard key 可逆性</strong></td>
          <td><code>reshardCollection</code> 4.4+ 可改、成本高</td>
          <td>可改用 backfill</td>
          <td>不可改、必 export-recreate</td>
      </tr>
      <tr>
          <td><strong>Consistency model</strong></td>
          <td>Read concern（local / majority / linearizable）+ causal consistency session</td>
          <td>Eventually / strongly consistent reads</td>
          <td>5 level spectrum（Strong / Bounded staleness / Session / Consistent prefix / Eventual）</td>
      </tr>
      <tr>
          <td><strong>Multi-region write</strong></td>
          <td>Atlas 跨 region 手動 conflict 處理</td>
          <td>Global Tables LWW</td>
          <td>Multi-region write（Strong 互斥、見 cosmosdb/multi-region-write-conflict SSoT）</td>
      </tr>
      <tr>
          <td><strong>Dogfood signal</strong></td>
          <td>無（MongoDB 是獨立公司、不適用）</td>
          <td>Amazon 自家高頻使用（9.C5 Amazon Ads / 9.C27 Disney+ etc）</td>
          <td>Microsoft 365 dogfood（9.C30、<strong>Scope warning</strong>：dogfood 數字不公開、是 selection signal 不是 benchmark）</td>
      </tr>
      <tr>
          <td><strong>Multi-model 差異化</strong></td>
          <td>單一 document model</td>
          <td>單一 KV-with-document model</td>
          <td>唯一單服務支援 5 API（差異化價值、F2.16）</td>
      </tr>
  </tbody>
</table>
<h3 id="軸的延伸子段">軸的延伸子段</h3>
<p><strong>部署 topology / 跨雲 hedging</strong>：三家 topology 是 <em>vendor lock-in</em> 跟 <em>跨雲彈性</em> 的硬取捨。Forbes 選 Atlas 不是當下省錢（自管 MongoDB 也可以、TCO 改善是副作用）、是 <em>未來雲商策略尚未底定</em> 的 hedging — Atlas 提供 AWS / GCP / Azure 三家部署、未來換雲不用換 DB（F2.10）。對照 DynamoDB / Cosmos DB / Spanner / Aurora 都是單雲鎖定 — 選了就跟著該雲商生態走。團隊雲商策略已底定（深度用 AWS / Azure / GCP 其一）時、單雲 vendor 通常較划算（更好的 IAM 整合、更深的 ops 工具、單一 support 通道）。跨雲價值真正成立是 <em>策略不確定</em> 或 <em>合規要求多雲</em> 場景。</p>
<p><strong>Capacity 抽象</strong>：三家 capacity 抽象的 <em>思維遷移成本</em> 可能高過 vendor 廣告的價差（F2.12）。MongoDB 用 CPU + IOPS + working set RAM 三軸思維、跟自管 PostgreSQL / MySQL 類似、團隊轉換成本低。DynamoDB 用 WCU/RCU 抽象、要學「估每個操作消耗多少 unit」、加上 on-demand / provisioned / adaptive capacity 三模式選擇。Cosmos DB 用 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a>（RU）抽象、1 RU ≈ 1 KB document 的 strong read 成本、寫 ~5 RU、複雜 query 數百 RU — 工程師要學會用 RU 思考、不是用 CPU 思考、團隊知識遷移成本可能高。容量規劃延伸見對應 vendor 的 sizing article。</p>
<p><strong>Partition / shard key 可逆性</strong>：三家 <em>不在同一光譜</em>、是選 vendor 前必做的 access pattern audit 重點（F2.15）。MongoDB <code>reshardCollection</code>（4.4+）可改、但成本高、需要 cluster downtime 或長時間 background migration。DynamoDB partition key 技術上可改、實作上用 backfill（建新 table、新 PK、雙寫舊新、cutover）— ops 工作量大但可逆。Cosmos DB partition key <em>不可改</em>、改 partition key 等於 export-recreate-import — 對 1TB+ 資料是大型 migration 工程。三家不可逆性遞增、選 Cosmos DB 前必須前期完整 access pattern audit、不能「先上 production 之後再調」。</p>
<p><strong>Consistency model</strong>：三家機制設計哲學不同。MongoDB read concern 是 <em>per-operation</em> 選擇（同一 client connection 可以混用）；DynamoDB strong vs eventual 是 <em>per-read</em> 選項（write 端統一強一致）；Cosmos DB 5 個 level 是 <em>account-level default + per-request override</em>、且 Strong 跟 multi-region write 互斥（CAP 硬約束）。設計上 MongoDB 最 flexible、Cosmos DB 最 explicit、DynamoDB 介於中間。延伸機制細節見 <a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">Cosmos DB consistency levels engineering</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region write conflict</a>（SSoT 主寫位置）。</p>
<p><strong>Multi-model 差異化</strong>：Cosmos DB 是 <em>唯一單一服務支援 5 API</em> 的雲商 DB（SQL / MongoDB / Cassandra / Gremlin / Table）— 對照 AWS 走多產品覆蓋（DynamoDB KV + DocumentDB MongoDB-compat + Neptune graph + Keyspaces Cassandra-compat）、GCP 走多產品覆蓋（Firestore + Spanner + Bigtable）。multi-model 的差異化價值是 <em>減少多 DB 並存運維</em> — 一個產品團隊只養一個 service、一套 IAM、一套 backup / DR、一套 monitoring。但 <em>是否真用上 multi-model</em> 要看團隊實際 workload — 多數團隊只用 1-2 個 API、單一 model 的競品（DynamoDB / MongoDB）可能更專注（F2.16）。</p>
<h2 id="失敗模式cross-vendor-反模式">失敗模式（cross-vendor 反模式）</h2>
<p>下列七條是三 vendor 都會踩、跨 case 共通的反模式。Per-vendor 特定反模式（例如 DynamoDB on-demand 隱性 hot partition、MongoDB schema 三代並存）在 per-vendor deep article。</p>
<h3 id="反模式-1把-dynamodb-當-oltp">反模式 1：把 DynamoDB 當 OLTP</h3>
<p>訊號：access pattern 還在探索期、5+ 種 query 還會增加、強一致 cross-partition transaction 是產品契約。應回 PostgreSQL / Aurora、不是繼續加碼 DynamoDB single-table design。</p>
<p>DynamoDB 的 <em>正確</em> 用法包含 control plane KV（Zoom / Disney+ / Capcom）跟 durable queue / write buffer（Tixcraft 9.C15 揭露的非 OLTP 正向用例、F1.3）— DynamoDB 接「訂單」寫入、不是即時生效、是讓 traditional server（金流 / 票庫）用自己能承受的速度消費。這層解耦讓「前端可以擴 130 倍、後端不用同步擴」。</p>
<h3 id="反模式-2把-mongodb-當-kv">反模式 2：把 MongoDB 當 KV</h3>
<p>訊號：access pattern 固定、PK 天然均勻、不需要 aggregation pipeline、document 內部從不展開（只查 root 欄位）。</p>
<p>應改 DynamoDB / Cosmos DB Table API。MongoDB 在這場景的 overhead（document overhead / connection model / aggregation engine 未用上）不划算 — KV vendor 的單筆讀寫成本更低、scaling 模型更簡單。</p>
<h3 id="反模式-3把-cosmos-db-當跨雲服務">反模式 3：把 Cosmos DB 當跨雲服務</h3>
<p>訊號：團隊評估 multi-cloud DR / 跨雲 portability、看到 Cosmos DB 文件強調「global distribution」就以為支援跨雲。</p>
<p>Cosmos DB 是 <em>Azure-only</em>、global distribution 指 Azure 內跨 region。想跨雲應改 MongoDB Atlas。multi-model 差異化是 <em>Azure 生態內</em> 的價值（F2.16）— 一旦離開 Azure、Cosmos DB 的所有獨特優勢都不存在。</p>
<h3 id="反模式-4federated-db-假設全用-x">反模式 4：federated DB 假設「全用 X」</h3>
<p>訊號：寫架構設計時假設「DB 一個服務搞定」、不規劃 cache / queue / proxy / abstraction layer。</p>
<p>Production 真實系統都是 federated（Coinbase / Toyota / Forbes 都是）。寫架構時假設一個 DB 搞定會撞 connection limit（surge 下 RDB 第一個爆點、F1.7）/ cache miss（單靠 DB 撐不住讀峰值）/ cross-region replication（跨 region 一致性處理錯）等隱性瓶頸。預先設計 federated topology + 跨層責任分配、不是事後補。</p>
<h3 id="反模式-5誤判-dogfood-case-數字">反模式 5：誤判 dogfood case 數字</h3>
<p>訊號：引用 Microsoft 365 / Amazon Prime Day 等 dogfood case 時、把它當 production benchmark、抄具體數字當 sizing 依據。</p>
<p>Dogfood case 數字常 <em>不公開</em> 或 <em>不適用 customer-facing</em>（F2.17 + F1.10）— Amazon Prime Day 「90M reads/sec」是年度峰值最高一秒不是平均、Microsoft 365 直接沒給數字、Google Spanner「10 億 req/sec」是 Google 全使用者加總不是單客戶配額。寫架構時引用要明示 selection signal（雲商賭身家、值得當高權重 vendor 訊號）vs production benchmark（具體 sizing 數字）— 兩者不可混為一談。</p>
<h3 id="反模式-6partition-key-一上-production-才發現不可逆">反模式 6：partition key 一上 production 才發現不可逆</h3>
<p>訊號：選 Cosmos DB / DynamoDB 時、partition key 設計沒做完整 access pattern audit、上 production 一段時間後發現 hot partition、想改 PK。</p>
<p>三家不在同一光譜（見前段對比表）— MongoDB shard key 4.4+ 可改但成本高、DynamoDB 可 backfill 改、Cosmos DB <em>不可改</em> 必 export-recreate。選 Cosmos DB 前要前期完整 access pattern audit、列所有預期 query 跟對應 PK 訪問頻率、確認最熱 PK 流量在單一 partition 容量上限內（F2.15）。</p>
<h3 id="反模式-7wire-compatibility-當-100-行為相同">反模式 7：wire compatibility 當 100% 行為相同</h3>
<p>訊號：選 Cosmos DB MongoDB API 或 DocumentDB、看到「MongoDB compatible」就假設 MongoDB driver 跑得起來就是相容、跳過 query pattern 驗證。</p>
<p>Wire compat ≠ 行為 100% 相同（F2.9）。Cosmos DB MongoDB API 廣告「100% wire compatibility」是行銷話術、實際是「在某些 query pattern 下相容」— aggregation pipeline 某些 stage 不支援、transaction edge case 行為差異、index 行為差異都會踩到。遷移必須 dual-write per query pattern 驗證、不是看 vendor spec list。</p>
<h2 id="不該選-db3-的訊號升-sql--升-distributed-sql-路徑">不該選 DB3 的訊號（升 SQL / 升 distributed SQL 路徑）</h2>
<p>下列四條訊號出現時、選擇應跳出 DB3 範圍。</p>
<ul>
<li><strong>JOIN-heavy + 強 normalize workload</strong>：應留 PostgreSQL（包括 PostgreSQL + JSONB 混合方案）、不該塞 NoSQL 再 <code>$lookup</code>。aggregation pipeline 的 <code>$lookup</code> 性能遠不如 SQL JOIN、在 sharded cluster 還有限制。</li>
<li><strong>強一致 cross-region transaction 是產品契約</strong>：應進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point</a> 評估 distributed SQL（CockroachDB / Spanner / Aurora DSQL）。三家 NoSQL 的 cross-region transaction 都有 limitation、不該當主路徑。</li>
<li><strong>大流量 + 跨業務 fleet 治理</strong>：Aurora 200 cluster 模式（9.C4 DraftKings 揭露的 business sharding fleet）可能更合適、進 Aurora fleet 治理。NoSQL 的 fleet 治理工具鏈（cluster lifecycle / cross-cluster query / unified IAM）通常不如 managed SQL 成熟。</li>
<li><strong>資料模型還在探索 + access pattern 變動快</strong>：暫緩 NoSQL 選型、用 PostgreSQL + JSONB 過渡。JSONB 給 document-like flexibility、SQL 給 ad-hoc query power、未來釐清穩定 access pattern 後再選 NoSQL 不遲。</li>
</ul>
<h2 id="下一步路由per-vendor-outline-子組">下一步路由（per-vendor outline 子組）</h2>
<p>讀者識別 workload type（軸 1-3）+ migration path（三型）+ system role（federated / control plane）後、進對應 per-vendor 子組繼續深化。</p>
<h3 id="mongodb-子組">MongoDB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a>（contract layer 三選一：DB 層 validator / app 層 abstraction / 混合）</li>
<li>容量：<a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key selection</a>（單 cluster vs 多 cluster blast radius、Toyota 20 DB 模式）</li>
<li>Migration：<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">migrate to Atlas</a>（同 DB 換託管型）</li>
</ul>
<h3 id="dynamodb-子組">DynamoDB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table design pattern</a>（access pattern 設計 + 適用度前置判讀）</li>
<li>機制：<a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency model optimization</a>（strong vs eventually consistent 取捨）</li>
</ul>
<h3 id="cosmos-db-子組">Cosmos DB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">MongoDB API vs SQL API</a>（API model 選型、四層 framing）</li>
</ul>
<h3 id="跨層架構federated-db--cache--proxy">跨層架構（federated DB / cache / proxy）</h3>
<p>跨層架構的延伸內容見對應 per-vendor connection management / cache layer article（後續會寫）— 本文只在軸 2 / federated frame 點到、不展開機制。</p>
<h3 id="進-db4-evaluation">進 DB4 evaluation</h3>
<p>若需要強一致 cross-region SQL / paradigm shift（KV → distributed SQL 或 SQL → distributed SQL）、進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a>。</p>
<h2 id="knowledge-card-路由">Knowledge card 路由</h2>
<p>本文涉及的 knowledge card：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a> — document model 的核心概念跟 aggregate root 邊界</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a> — KV vendor 的 partition 容量上限機制</li>
<li><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a> — shard key 跟 partition key 設計</li>
<li><a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency-level</a> — strong / eventual / session 三類取捨</li>
<li><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor-lock-in</a> — 單雲 vs 跨雲的 hedging 取捨</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed-sql</a> — 跳出 DB3 進 DB4 的概念入口</li>
</ul>
]]></content:encoded></item><item><title>後端 migration、rollout 與 rollback 流程</title><link>https://tarrragon.github.io/blog/ci/backend-deploy/migration-rollout-rollback-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/backend-deploy/migration-rollout-rollback-flow/</guid><description>&lt;p>後端部署流程的核心責任是讓程式、資料與流量在相容窗口內推進。後端服務通常會同時依賴 database、queue、cache、外部 API 與 runtime config；CI/CD 需要把 build 成功、migration 安全、readiness 可信、rollback 可執行分成不同 gate。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>後端部署的主要風險是有狀態依賴。前端 artifact 可以直接回退上一份靜態檔，後端服務一旦寫入新資料、消費 queue message 或呼叫外部 side effect，rollback 就不再只是換回舊 image。發布流程要先定義新舊版本如何短暫共存，再決定 migration 與流量切換順序。&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>Build&lt;/td>
 &lt;td>產生 binary、package 或 image&lt;/td>
 &lt;td>版本是否可追到 commit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Contract test&lt;/td>
 &lt;td>驗證 API、queue、DB 相容性&lt;/td>
 &lt;td>新舊 schema / message 是否可共存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>推進資料結構與資料狀態&lt;/td>
 &lt;td>是否可漸進、可重試、可停止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a>&lt;/td>
 &lt;td>分批接流量&lt;/td>
 &lt;td>readiness、error rate、latency 是否可信&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a>&lt;/td>
 &lt;td>縮小錯誤版本影響&lt;/td>
 &lt;td>程式、資料、queue 與 config 是否可回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可部署服務。服務版本要能從 runtime 反查 commit、workflow run、image digest 與 migration 版本，讓事故時能快速定位哪一次變更進入環境。&lt;/p>
&lt;p>Contract test 階段負責驗證跨邊界相容。API response、database schema、queue message 與 config key 都是契約；只測 service 內部函式，通常抓不到新舊版本並存時的破壞性變更。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 階段負責推進資料狀態。安全 migration 通常採 expand-and-contract：先加相容欄位或表、部署可讀新舊格式的程式、回填資料，最後移除舊格式。直接在同一次 release 刪欄位與切程式，會讓 rollback 失去空間。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a> 階段負責控制新版本接到的流量。Rolling、canary 與 blue-green 都需要可信 readiness；readiness 應檢查服務能否接流量，而不只是 process alive。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a> 階段負責定義失敗時的處理路由。後端 rollback 常見做法是 app rollback、config rollback、traffic rollback 或 forward fix；資料已被新程式寫入時，forward fix 往往比直接資料回滾安全。&lt;/p>
&lt;h2 id="migration-順序">Migration 順序&lt;/h2>
&lt;p>Migration 順序的責任是保留相容窗口。資料結構變更應讓至少兩個相鄰程式版本能共存，避免部署中途任何一端先完成都造成服務不可用。&lt;/p>
&lt;ol>
&lt;li>新增向前相容 schema，例如新增 nullable column 或新表。&lt;/li>
&lt;li>部署可同時讀舊欄位與新欄位的程式。&lt;/li>
&lt;li>執行 backfill 或 background migration。&lt;/li>
&lt;li>切換讀取來源或寫入路徑。&lt;/li>
&lt;li>觀察穩定後移除舊欄位、舊 index 或舊 message 格式。&lt;/li>
&lt;/ol>
&lt;p>這個順序的價值是可停止。若第 3 步回填異常，可以暫停 backfill，不必立即回退 app；若第 4 步切換後錯誤率升高，可以先切回舊讀取路徑，再評估資料修補。&lt;/p>
&lt;h2 id="rollout-判讀">Rollout 判讀&lt;/h2>
&lt;p>Rollout 判讀要同時看技術指標與業務副作用。服務能啟動不代表能安全接流量；API error、queue lag、database lock、第三方 API 錯誤與核心業務漏斗都可能是發布問題。&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>readiness 未通過&lt;/td>
 &lt;td>新版本尚未能接流量&lt;/td>
 &lt;td>暫停 rollout，查 config / 依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error rate 上升&lt;/td>
 &lt;td>新版本或相依服務契約出錯&lt;/td>
 &lt;td>降低流量或切回舊版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>migration lock 久&lt;/td>
 &lt;td>schema 變更影響正常查詢&lt;/td>
 &lt;td>停止 migration，改成分段方案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>consumer lag 上升&lt;/td>
 &lt;td>worker 消費速度或 message 壞&lt;/td>
 &lt;td>暫停新版 worker 或降速&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback 後仍錯&lt;/td>
 &lt;td>資料或外部 side effect 已變動&lt;/td>
 &lt;td>進入 forward fix / repair 流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些訊號要先接到發布流程。若指標只存在 dashboard 裡、workflow 不知道如何判讀，團隊仍會在事故當下靠人工臨場決策。&lt;/p></description><content:encoded><![CDATA[<p>後端部署流程的核心責任是讓程式、資料與流量在相容窗口內推進。後端服務通常會同時依賴 database、queue、cache、外部 API 與 runtime config；CI/CD 需要把 build 成功、migration 安全、readiness 可信、rollback 可執行分成不同 gate。</p>
<h2 id="流程定位">流程定位</h2>
<p>後端部署的主要風險是有狀態依賴。前端 artifact 可以直接回退上一份靜態檔，後端服務一旦寫入新資料、消費 queue message 或呼叫外部 side effect，rollback 就不再只是換回舊 image。發布流程要先定義新舊版本如何短暫共存，再決定 migration 與流量切換順序。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 binary、package 或 image</td>
          <td>版本是否可追到 commit</td>
      </tr>
      <tr>
          <td>Contract test</td>
          <td>驗證 API、queue、DB 相容性</td>
          <td>新舊 schema / message 是否可共存</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>推進資料結構與資料狀態</td>
          <td>是否可漸進、可重試、可停止</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a></td>
          <td>分批接流量</td>
          <td>readiness、error rate、latency 是否可信</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a></td>
          <td>縮小錯誤版本影響</td>
          <td>程式、資料、queue 與 config 是否可回復</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可部署服務。服務版本要能從 runtime 反查 commit、workflow run、image digest 與 migration 版本，讓事故時能快速定位哪一次變更進入環境。</p>
<p>Contract test 階段負責驗證跨邊界相容。API response、database schema、queue message 與 config key 都是契約；只測 service 內部函式，通常抓不到新舊版本並存時的破壞性變更。</p>
<p><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 階段負責推進資料狀態。安全 migration 通常採 expand-and-contract：先加相容欄位或表、部署可讀新舊格式的程式、回填資料，最後移除舊格式。直接在同一次 release 刪欄位與切程式，會讓 rollback 失去空間。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a> 階段負責控制新版本接到的流量。Rolling、canary 與 blue-green 都需要可信 readiness；readiness 應檢查服務能否接流量，而不只是 process alive。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a> 階段負責定義失敗時的處理路由。後端 rollback 常見做法是 app rollback、config rollback、traffic rollback 或 forward fix；資料已被新程式寫入時，forward fix 往往比直接資料回滾安全。</p>
<h2 id="migration-順序">Migration 順序</h2>
<p>Migration 順序的責任是保留相容窗口。資料結構變更應讓至少兩個相鄰程式版本能共存，避免部署中途任何一端先完成都造成服務不可用。</p>
<ol>
<li>新增向前相容 schema，例如新增 nullable column 或新表。</li>
<li>部署可同時讀舊欄位與新欄位的程式。</li>
<li>執行 backfill 或 background migration。</li>
<li>切換讀取來源或寫入路徑。</li>
<li>觀察穩定後移除舊欄位、舊 index 或舊 message 格式。</li>
</ol>
<p>這個順序的價值是可停止。若第 3 步回填異常，可以暫停 backfill，不必立即回退 app；若第 4 步切換後錯誤率升高，可以先切回舊讀取路徑，再評估資料修補。</p>
<h2 id="rollout-判讀">Rollout 判讀</h2>
<p>Rollout 判讀要同時看技術指標與業務副作用。服務能啟動不代表能安全接流量；API error、queue lag、database lock、第三方 API 錯誤與核心業務漏斗都可能是發布問題。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>readiness 未通過</td>
          <td>新版本尚未能接流量</td>
          <td>暫停 rollout，查 config / 依賴</td>
      </tr>
      <tr>
          <td>error rate 上升</td>
          <td>新版本或相依服務契約出錯</td>
          <td>降低流量或切回舊版本</td>
      </tr>
      <tr>
          <td>migration lock 久</td>
          <td>schema 變更影響正常查詢</td>
          <td>停止 migration，改成分段方案</td>
      </tr>
      <tr>
          <td>consumer lag 上升</td>
          <td>worker 消費速度或 message 壞</td>
          <td>暫停新版 worker 或降速</td>
      </tr>
      <tr>
          <td>rollback 後仍錯</td>
          <td>資料或外部 side effect 已變動</td>
          <td>進入 forward fix / repair 流程</td>
      </tr>
  </tbody>
</table>
<p>這些訊號要先接到發布流程。若指標只存在 dashboard 裡、workflow 不知道如何判讀，團隊仍會在事故當下靠人工臨場決策。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是把後端部署當成單一 deploy 動作。後端發布的本質是多個相依狀態的協調流程。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>app 與 destructive migration 同步</td>
          <td>rollback 後舊程式失去讀取契約</td>
          <td>expand-and-contract</td>
      </tr>
      <tr>
          <td>readiness 只檢查 process alive</td>
          <td>流量進入尚未準備好的服務</td>
          <td>檢查依賴、config 與初始化狀態</td>
      </tr>
      <tr>
          <td>rollback 只切 image tag</td>
          <td>資料與 queue side effect 留下</td>
          <td>定義 app / data / config 路由</td>
      </tr>
      <tr>
          <td>migration 沒有 dry run</td>
          <td>發布時才發現權限或鎖表問題</td>
          <td>staging 或 shadow 環境先跑驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>後端部署總覽：回 <a href="../">後端部署 CI/CD</a>。</li>
<li>Migration 術語：讀 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Cloudflare WAF</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/</guid><description>&lt;p>Cloudflare WAF 是 &lt;em>edge-deployed&lt;/em> 的 Web Application Firewall、跑在 Cloudflare 全球 anycast 網路上、攔截 HTTP/HTTPS 攻擊在抵達 origin 之前。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &amp;#43; Managed Rule Group &amp;#43; Rate-based Rule、Shield Standard 內含">AWS WAF&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &amp;#43; ATO &amp;#43; Bot 一體">Fastly Next-Gen WAF&lt;/a> 的核心差異是 &lt;em>跟其他 Cloudflare 產品深度整合&lt;/em>：DDoS protection、Bot Management、Rate Limiting、Page Shield（JS supply chain）、API Shield（schema validation）、Zero Trust、Workers 邊緣計算共用同一個控制面。客戶選 Cloudflare WAF 通常不只是要 WAF、是要 &lt;em>整套 edge security suite&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Cloudflare WAF 的核心定位是 &lt;em>把攻擊擋在 origin 之前的一站式 edge security&lt;/em>。流量打到 Cloudflare anycast IP、經過 WAF / DDoS / Bot / Rate Limit / Page Shield 多層處理、再 proxy 到 origin。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &amp;#43; Managed Rule Group &amp;#43; Rate-based Rule、Shield Standard 內含">AWS WAF&lt;/a> 跑在 AWS 內部 ALB / CloudFront / API Gateway 前是不同部署模型 — AWS WAF 流量 &lt;em>已經進到 AWS&lt;/em>、Cloudflare WAF 流量 &lt;em>還沒到 origin&lt;/em>。對 origin 是 &lt;em>任意雲 / on-prem&lt;/em> 的客戶、Cloudflare 是天然選項；對 AWS-only 客戶、AWS WAF 整合更深但 edge 範圍小。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &amp;#43; ATO &amp;#43; Bot 一體">Fastly Next-Gen WAF&lt;/a>（前 Signal Sciences）相比、Cloudflare 走 &lt;em>signature + managed rule + ML&lt;/em> 混合、Fastly NG-WAF 走 &lt;em>語意分析 + behavioral detection&lt;/em>（不靠 regex signature）。Cloudflare managed rule 覆蓋廣但 false positive 較常見、需要 &lt;em>sensitivity tuning&lt;/em>；Fastly NG-WAF 預設較低 FP 但需要 &lt;em>自己定義業務 anomaly&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<p>Cloudflare WAF 是 <em>edge-deployed</em> 的 Web Application Firewall、跑在 Cloudflare 全球 anycast 網路上、攔截 HTTP/HTTPS 攻擊在抵達 origin 之前。它跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a> 的核心差異是 <em>跟其他 Cloudflare 產品深度整合</em>：DDoS protection、Bot Management、Rate Limiting、Page Shield（JS supply chain）、API Shield（schema validation）、Zero Trust、Workers 邊緣計算共用同一個控制面。客戶選 Cloudflare WAF 通常不只是要 WAF、是要 <em>整套 edge security suite</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>Cloudflare WAF 的核心定位是 <em>把攻擊擋在 origin 之前的一站式 edge security</em>。流量打到 Cloudflare anycast IP、經過 WAF / DDoS / Bot / Rate Limit / Page Shield 多層處理、再 proxy 到 origin。這跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> 跑在 AWS 內部 ALB / CloudFront / API Gateway 前是不同部署模型 — AWS WAF 流量 <em>已經進到 AWS</em>、Cloudflare WAF 流量 <em>還沒到 origin</em>。對 origin 是 <em>任意雲 / on-prem</em> 的客戶、Cloudflare 是天然選項；對 AWS-only 客戶、AWS WAF 整合更深但 edge 範圍小。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a>（前 Signal Sciences）相比、Cloudflare 走 <em>signature + managed rule + ML</em> 混合、Fastly NG-WAF 走 <em>語意分析 + behavioral detection</em>（不靠 regex signature）。Cloudflare managed rule 覆蓋廣但 false positive 較常見、需要 <em>sensitivity tuning</em>；Fastly NG-WAF 預設較低 FP 但需要 <em>自己定義業務 anomaly</em>。</p>
<p>關鍵張力：客戶信任的不只是 <em>WAF rule 攔截能力</em>、還包括 <em>Cloudflare control plane 的安全性</em>。<a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare 2023 control plane token</a> 跟 <a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">Cloudflare 2026 route leak</a> 兩個事件展示：vendor 自己被打進去 / 自動化配置失誤時、客戶側 <em>直接修不了</em>、只能等公告 + 客戶側 token rotation + emergency bypass。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Cloudflare WAF 在 edge security stack 中承擔哪一段（DDoS / WAF / Bot / Page Shield / API Shield）、哪些要靠 origin 自己做</li>
<li>Managed Rule vs Custom Rule 的取捨、sensitivity tuning 跟 false positive curve</li>
<li>Cloudflare control plane 出事時的客戶側補強路徑（API token rotation、Origin Rules bypass、第二邊界 fallback）</li>
<li>何時用 Cloudflare、何時走 <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly NG-WAF</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Cloudflare WAF 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能改 WAF 規則</strong>：Cloudflare account 的 admin / member role 配置、API token scope（不要用 Global API Key、用 scoped API token + 限定 zone / 限定 permission）、Audit Log 是否同步到 SIEM</li>
<li><strong>規則覆蓋面</strong>：Managed Ruleset（OWASP Core Ruleset + Cloudflare Managed Ruleset + Exposed Credentials Check）是否開、Sensitivity（Low / Medium / High）對應的 FP rate 是否監控、Custom Rule 是否進版控（Terraform provider）</li>
<li><strong>入口暴露</strong>：origin IP 是否曝光（DNS 直查 / 歷史 SAN cert / 子域名）、Argo Tunnel / Authenticated Origin Pull 是否強制、繞過 Cloudflare 直連 origin 的路徑是否封住</li>
<li><strong>證據可回查</strong>：Security Events Log 是否同步到 SIEM（Logpush 推到 R2 / S3 / Splunk）、Page Shield 偵測異常 script 是否 alert、API token 異常操作（特別 zone settings 變更）是否 alert</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">Entry Point Protection</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Managed Ruleset 分層</strong>：Cloudflare 提供三類 managed rule — <em>OWASP Core Ruleset</em>（OWASP CRS、寬覆蓋、FP 較多）、<em>Cloudflare Managed Ruleset</em>（Cloudflare 維護、針對熱門 CMS / framework）、<em>Exposed Credentials Check</em>（檢測登入流量中的已洩漏 credential）。production 通常開全部三套 + 各設適當 sensitivity。Sensitivity 不是「敏感度越高越好」— High sensitivity 攔截更多 borderline traffic、business-critical endpoint 可能誤殺合法請求。建議從 <em>Log Mode</em> 開始、觀察 1-2 週的 FP pattern、再切到 <em>Block</em>。</p>
<p><strong>Custom Rule（Cloudflare Rules）</strong>：用 Rules language（類 SQL 表達式）定義條件 + 動作（Block / Challenge / Log / JS Challenge / Managed Challenge）。常見用法：geo block（特定國家）、known bad IP（threat intel feed）、URI path-based limit（admin endpoint 限定 IP）、header anomaly（缺 User-Agent / 異常 Referer）。所有 Custom Rule 走 Terraform provider 進版控、避免 console 直接改、變更走 PR review。</p>
<p><strong>Rate Limiting</strong>：跟 WAF rule 是 <em>獨立 product</em>、配置是 <em>threshold + window + action</em>（例：1000 req/min per IP → challenge）。Rate Limiting 比 WAF 適合處理 <em>legitimate-looking high volume</em>（credential stuffing、scraping、API abuse）。注意 <em>NAT pool IP</em> 的問題 — 一個公司 / ISP NAT 出口可能合法產生高 QPS、簡單 per-IP rate limit 會誤殺、需要組合 <em>cf.threat_score</em> 或 <em>cookie-based identification</em>。</p>
<p><strong>Bot Management（單獨 SKU）</strong>：免費版 WAF 不含 Bot Management、需要 Pro / Business / Enterprise 才有。Bot Management 用 ML + behavioral fingerprint 區分 <em>human / good bot（搜尋引擎）/ likely bot / verified bot</em>、給 bot score（1-99）。客戶在 Custom Rule 用 <code>cf.bot_management.score &lt; 30</code> 之類條件挑出 likely bot 處理。簡單 user-agent 過濾擋不住現代 headless browser、必須走 Bot Management。</p>
<p><strong>Page Shield（JS supply chain 防護）</strong>：Page Shield 監測客戶網頁載入的 JS / connect 來源、發現 <em>新出現的腳本</em> 或 <em>已洩漏的 script</em>（CT log + threat intel）就 alert。意義是 <em>防 third-party script 被供應鏈攻擊</em>（類 <a href="https://en.wikipedia.org/wiki/Magecart">Magecart</a>）— WAF 攔不住、因為攻擊發生在 <em>browser 端</em> 而非 <em>origin 流量</em>。需要在 Page 載入 Page Shield 的 monitoring script。</p>
<p><strong>API Shield</strong>：用 OpenAPI schema validation、auto-discovery API endpoint、mTLS 驗證、JWT validation。對於有 schema 的 API、可以擋掉 <em>schema 不符的請求</em>（多餘欄位、型別錯誤、缺必要欄位）— 比 generic WAF rule 精準。</p>
<p><strong>Origin 暴露面收緊</strong>：Cloudflare 唯一有效的前提是 <em>流量必須經過 Cloudflare</em>。如果攻擊者拿到 origin 真實 IP（DNS 歷史記錄、漏洞披露網站、SSL cert SAN）、可以繞過 Cloudflare 直打 origin。控制方法：origin firewall 只允許 Cloudflare IP range 入站、Argo Tunnel（origin 主動建 outbound 連線到 Cloudflare、不開任何入站 port）、Authenticated Origin Pull（origin 用 cert 驗證請求來自 Cloudflare）三選一或組合。</p>
<p><strong>API token 治理</strong>：避免 Global API Key（全帳號 root token）、改用 <em>scoped API token</em>（限 zone + 限 permission + 限 IP + 限 TTL）。token 進 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>、定期 rotate。對應 <a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare control plane token 2023</a> 揭示的 lesson：Cloudflare 自己也踩過 token 治理不足、客戶側不能假設 vendor 完美。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Cloudflare WAF</th>
          <th>AWS WAF</th>
          <th>Fastly Next-Gen WAF</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署位置</td>
          <td>Cloudflare global edge（300+ POP）</td>
          <td>AWS region 內 ALB / CloudFront / API Gateway 前</td>
          <td>Fastly edge + Agent + Module（自管 Nginx / Apache / Envoy / IIS）+ Cloud WAF proxy、三模型可混</td>
      </tr>
      <tr>
          <td>Origin 中立性</td>
          <td>強 — origin 可以是任何雲 / on-prem</td>
          <td>弱 — 跟 AWS 緊耦合（限 AWS service 前）</td>
          <td>強 — Fastly CDN / 任何 origin</td>
      </tr>
      <tr>
          <td>偵測模型</td>
          <td>Signature + Managed Rule + ML</td>
          <td>Signature + Managed Rule + Lambda 自訂</td>
          <td>Signal / behavioral（語意分析、低 FP）</td>
      </tr>
      <tr>
          <td>DDoS 內建</td>
          <td>是 — 跟 WAF 同套餐</td>
          <td>AWS Shield Standard 內建、Advanced 加價</td>
          <td>內建 + Fastly DDoS</td>
      </tr>
      <tr>
          <td>Bot Management</td>
          <td>加價 add-on（Pro / Business / Enterprise）</td>
          <td>AWS WAF Bot Control</td>
          <td>加價 add-on</td>
      </tr>
      <tr>
          <td>JS supply chain</td>
          <td>Page Shield（Business+）</td>
          <td>無原生、靠後端 CSP / 第三方</td>
          <td>Inline JS monitoring（Next-Gen WAF 部分）</td>
      </tr>
      <tr>
          <td>API schema</td>
          <td>API Shield（Enterprise）</td>
          <td>AWS WAF + API Gateway request validator</td>
          <td>NG-WAF inline + sigsci-agent</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — UI / Rules language 易上手、Terraform 完整</td>
          <td>較陡 — JSON policy + 跟 AWS service 整合多軌</td>
          <td>中 — agent 安裝 + Signal 語意設定</td>
      </tr>
      <tr>
          <td>第三方信任成本</td>
          <td>高 — Cloudflare 控制面（2023、2026 自家事件）</td>
          <td>中 — AWS 控制面、跟 IAM 同套</td>
          <td>中 — Fastly 控制面（規模小、事件少但社群影響也小）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Multi-cloud / on-prem origin、要整套 edge security</td>
          <td>AWS-heavy、ALB / CloudFront 是主要入口</td>
          <td>高 FP 容忍度低、業務有 schema、想避 regex signature</td>
      </tr>
  </tbody>
</table>
<p>選 Cloudflare WAF 的核心訴求：<em>多雲 / on-prem origin</em> + 需要 <em>整套 edge security suite</em>（DDoS + WAF + Bot + Page Shield + API Shield） + 接受 Cloudflare 控制面風險、且有預算做 Enterprise tier 才能拿到完整功能。純 AWS-internal app + ALB origin 用 AWS WAF 整合更直接。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Workers + Workers AI 作為 custom logic</strong>：當 managed rule + custom rule 表達力不夠（例：根據 user account tier 決定 challenge 強度、整合內部 risk score API）、可以用 Cloudflare Workers 寫 JavaScript / TypeScript / Rust 在 edge 執行。Workers AI 提供 edge ML inference、可以做 inline content moderation 或 anomaly detection。代價是 <em>Workers code 進 Cloudflare 控制面</em>、變更要走部署流程、debug 跟 origin 是兩條 trace。</p>
<p><strong>Logpush 跟 SIEM 整合</strong>：Cloudflare Security Events 量大、free / Pro 在 dashboard 看、Business / Enterprise 走 Logpush 到 R2 / S3 / Splunk / Datadog / Sumo Logic。production 必須走 Logpush、不能只在 dashboard — 事件 30 天保留期是 Cloudflare 端、SIEM 留更久。Logpush 也是 SIEM 上做 <em>跨來源 correlation</em> 的前提（WAF event + origin app log + IdP log）。</p>
<p><strong>Multi-account / Tenant</strong>：大企業有多個 Cloudflare account（不同 BU / 不同產品線）、要走 <em>Cloudflare for SaaS</em> 或 <em>Account-level access</em>、API token scope 要限定 account。Single account 多 zone 是常見小組織配置、但跨組織 / 跨產品線必須拆 account 隔離 admin compromise blast radius。</p>
<p><strong>Magic Transit / Zero Trust integration</strong>：Magic Transit 是 L3 DDoS（不只 HTTP、TCP / UDP 也 anycast）、Zero Trust 是 employee access（取代 VPN）。跟 WAF 是不同產品、但常一起部署 — Magic Transit 防 L3/L4 attack、WAF 防 L7、Zero Trust 防內部 east-west。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Managed Rule 誤殺合法請求</strong>：High sensitivity 開後 business endpoint 變慢 / 報錯 — 看 Security Events 找 rule_id、用 Custom Rule skip 該 rule 在特定 path / 特定 user-agent、不要全 zone 關 rule</li>
<li><strong>Bot Management 太嚴 / 太鬆</strong>：bot score threshold 設不對、合法 API client 被當 bot、或攻擊者拿到 verified bot 假冒 — 用 <em>Bot Analytics</em> 看分數分布、調整 threshold 同時加白名單（API key + IP CIDR）</li>
<li><strong>Rate Limit 誤殺 NAT 用戶</strong>：per-IP rate limit 在 NAT 出口 IP 上炸 — 改 per-session（cookie-based）或 cf.threat_score 條件</li>
<li><strong>Origin IP 外洩</strong>：DNS 歷史 + 漏洞披露 + cert SAN 揭露真實 origin、攻擊繞 Cloudflare 直打 — 換 IP + 開 origin firewall（只允許 Cloudflare CIDR）+ Argo Tunnel</li>
<li><strong>API token over-scoped</strong>：CI / 第三方 SaaS 拿到 Global API Key、整 account 都被改 — 改 scoped token、限 zone + permission + IP、進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></li>
<li><strong>Security Events 沒進 SIEM</strong>：事件只在 dashboard、跨來源 correlation 沒法做 — 配 Logpush + alert 規則</li>
<li><strong>Page Shield 沒裝</strong>：客戶端 JS 被植入、伺服器端日誌看不到攻擊、第三方 script CDN 被打 — 啟用 Page Shield + CSP report-uri 雙軌</li>
<li><strong>第二邊界沒設</strong>：完全依賴 Cloudflare、Cloudflare 出事流量全停（<a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">2023 / 2026 自家事件</a>）— 高 SLA 服務應該設 fallback origin / secondary DNS（如 Route53 health check failover 到 Fastly 或直連 origin）</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only + ALB / CloudFront origin</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></td>
      </tr>
      <tr>
          <td>低 FP 容忍 / 業務有 schema</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></td>
      </tr>
      <tr>
          <td>純內部 mTLS / east-west</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> + service mesh</td>
      </tr>
      <tr>
          <td>Cert lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>客戶端 JS supply chain</td>
          <td>Page Shield + <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain integrity</a></td>
      </tr>
      <tr>
          <td>DDoS L3/L4</td>
          <td>Cloudflare Magic Transit / AWS Shield Advanced</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Cloudflare 完整 product line（Workers / Pages / R2 / D1 / Magic Transit / Zero Trust 各自細節）</li>
<li>WAF Rules language 完整語法 reference</li>
<li>Page Shield / API Shield Enterprise tier 完整功能對照</li>
<li>各 PCI DSS / SOC 2 / FedRAMP 合規矩陣</li>
<li>Cloudflare 在中國的部署模式（JD Cloud Union 合作）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Cloudflare WAF 在 07 案例庫有 <em>兩個直接 vendor-level 事件</em> + 多個 edge-exposure 對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Cloudflare WAF 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare Control Plane Token 2023</a></td>
          <td>直接 — Cloudflare 自家 API token 治理不足、客戶側必須假設 vendor 也會被打、API token rotation 跟 IP allowlist 必做</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">Cloudflare Route Leak 2026</a></td>
          <td>直接 — 自動化路由配置錯誤導致流量擁塞、客戶側應有 secondary DNS / failover origin 預案</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — WAF 攔不住 edge appliance zero-day、需要「修補 + session 失效 + 異常清查」三同步</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet SSL-VPN CVE 2023-27997</a></td>
          <td>對照啟示 — vendor patch 前的臨時 WAF rule + 收斂可達來源是修補窗口期的標準動作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — WAF rule 是 emergency mitigation、但 exploitation 過 WAF 後在後端執行、不能單靠 WAF 防後端 supply chain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta-Cloudflare 2023 Support Supply Chain</a></td>
          <td>對照啟示 — 上游 IdP 出事傳導到 Cloudflare admin 帳號、API token / admin session 要立即 rotate、不等供應商公告</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a>、<a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>（WAF block 不夠時、資料層也要遮罩）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Cloudflare API token 存放）、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（Cloudflare admin 走 SSO）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（WAF block 事件 / Cloudflare 自家事件如何 routing 進 IR）</li>
<li>官方：<a href="https://developers.cloudflare.com/waf/">Cloudflare WAF Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>HashiCorp Vault</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/</guid><description>&lt;p>HashiCorp Vault 是 self-hosted 的 secret management 控制面、解決三個核心問題：&lt;em>static secret 集中保管&lt;/em>（KV engine、跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a> 卡同概念）、&lt;em>dynamic credential 即用即發即收&lt;/em>（database / cloud / SSH engine 在請求時動態建立短期憑證）、&lt;em>encryption-as-a-service 與內部 PKI&lt;/em>（transit engine 把加解密外包給 Vault、PKI engine 自簽憑證）。三件事在 cloud-native 替代品（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a>）裡通常拆成不同 service、且綁單一雲。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Vault 的核心定位是 &lt;em>跨雲 + 跨環境 + 跨 secret 形態的單一 secret 控制面&lt;/em>。當組織同時跑 AWS + GCP + on-prem K8s、又需要 dynamic database credential + 內部 PKI + envelope encryption、用三個 cloud-native service 拼起來會出現 &lt;em>secret 治理鏈不連續&lt;/em>（AWS 的 secret 怎麼授權 GCP service 取用、on-prem app 怎麼拿短期 cloud credential、內部 CA 跟外部 ACM 怎麼分工）。Vault 把這層 &lt;em>統一抽象&lt;/em> — 應用端只跟 Vault 講話、Vault 後端接各雲 KMS / database / PKI。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a> 相比、Vault 多了：&lt;em>dynamic credential engine&lt;/em>（cloud-native 對應產品有限）、&lt;em>transit engine&lt;/em> 做 encryption-as-a-service、&lt;em>PKI engine&lt;/em> 自簽內部憑證、&lt;em>跨雲統一介面&lt;/em>。代價是 &lt;em>自管運維&lt;/em>（HA cluster、auto-unseal、replication、upgrade）— 跟自管 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a> 的取捨同類。HCP Vault（HashiCorp Cloud Platform）是 HashiCorp 託管版、把運維交還、但綁 HashiCorp。&lt;/p></description><content:encoded><![CDATA[<p>HashiCorp Vault 是 self-hosted 的 secret management 控制面、解決三個核心問題：<em>static secret 集中保管</em>（KV engine、跟 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 卡同概念）、<em>dynamic credential 即用即發即收</em>（database / cloud / SSH engine 在請求時動態建立短期憑證）、<em>encryption-as-a-service 與內部 PKI</em>（transit engine 把加解密外包給 Vault、PKI engine 自簽憑證）。三件事在 cloud-native 替代品（<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a>）裡通常拆成不同 service、且綁單一雲。</p>
<h2 id="服務定位">服務定位</h2>
<p>Vault 的核心定位是 <em>跨雲 + 跨環境 + 跨 secret 形態的單一 secret 控制面</em>。當組織同時跑 AWS + GCP + on-prem K8s、又需要 dynamic database credential + 內部 PKI + envelope encryption、用三個 cloud-native service 拼起來會出現 <em>secret 治理鏈不連續</em>（AWS 的 secret 怎麼授權 GCP service 取用、on-prem app 怎麼拿短期 cloud credential、內部 CA 跟外部 ACM 怎麼分工）。Vault 把這層 <em>統一抽象</em> — 應用端只跟 Vault 講話、Vault 後端接各雲 KMS / database / PKI。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> 相比、Vault 多了：<em>dynamic credential engine</em>（cloud-native 對應產品有限）、<em>transit engine</em> 做 encryption-as-a-service、<em>PKI engine</em> 自簽內部憑證、<em>跨雲統一介面</em>。代價是 <em>自管運維</em>（HA cluster、auto-unseal、replication、upgrade）— 跟自管 <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a> 的取捨同類。HCP Vault（HashiCorp Cloud Platform）是 HashiCorp 託管版、把運維交還、但綁 HashiCorp。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 secret 適合 Vault（dynamic credential、跨雲、PKI、encryption-as-a-service）、哪些直接用雲端 native service 即可</li>
<li>Vault deployment 的最低安全需求（auto-unseal、HA、audit device、policy、replication）</li>
<li>Vault 自己出事時的降級路徑（seal storm、root token 復原、audit log gap）</li>
<li>何時用 Vault、何時走 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Vault deployment 是否健康、最少看五件事：</p>
<ul>
<li><strong>誰能做什麼</strong>：root token 是否已 revoke、policy 是否走 path-based least privilege、admin 是否走 OIDC / <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM auth</a> 而不是 token、break-glass token 是否離線存</li>
<li><strong>Auth method 收緊</strong>：AppRole / Kubernetes / OIDC / JWT auth 哪些開、role 對應的 policy 是不是過寬、TTL 是否短、<code>bound_*</code> 條件是否鎖（namespace / audience / subject）</li>
<li><strong>Secret engine 設定</strong>：KV v2 開 versioning？dynamic engine（database / aws / pki）lease TTL 多久、max TTL 限制是什麼、revocation 是否驗證生效</li>
<li><strong>Seal / unseal 治理</strong>：是否走 auto-unseal（KMS-backed）、recovery key 持有者跟 Shamir threshold、replication 跟 DR cluster 是否同步</li>
<li><strong>證據是否可回查</strong>：audit device（file / syslog / socket）是否多 channel、是否同步到 SIEM、replay 攻擊防護是否開（HMAC + nonce）</li>
</ul>
<p>五件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Auth method 設計</strong>：AppRole 適合不在雲端 metadata 內的 workload（on-prem、CI runner）但 <em>secret_id</em> 本身要妥善保管；Kubernetes auth 適合 K8s 內 workload、用 ServiceAccount token + projected token；<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM auth</a> 適合 AWS 內 workload、走 STS 簽名驗證、不需要存 secret；OIDC / JWT 適合 human admin + CI（GitHub Actions / GitLab CI 走 OIDC token）。每個 auth method 對應 <em>一組 role</em>、role 綁 <em>policy</em> 跟 <em>TTL</em>。</p>
<p><strong>Secret engine 分層</strong>：KV v2（static secret + version history）作為基線；dynamic database engine（PostgreSQL / MySQL / MongoDB）發短期 DB user、<code>max_ttl = 1h</code> 級別、過期 Vault 自動 revoke；AWS / Azure / GCP secret engine 對 cloud account 發短期 STS credential / service account key；PKI engine 自簽憑證、跟 <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> 整合做 K8s workload mTLS；transit engine 做 envelope encryption — app 把資料丟給 Vault 加密、key 不離 Vault。</p>
<p><strong>Policy（path-based）</strong>：Vault policy 是 <em>path + capabilities</em>（create / read / update / delete / list / sudo）的 mapping。常見錯配：給 <code>secret/*</code> read 等於整個組織所有 secret 都看得到、應該用 <code>secret/data/{team}/*</code> 之類前綴限定；admin policy 不要給 <code>sudo</code> 太寬、policy 變更走 PR review + CI apply。</p>
<p><strong>Rotation 跟 lease 治理</strong>：static secret（KV）的 rotation 是 <em>app 自己做</em>（拿新 secret 後手動 update）；dynamic secret 是 <em>Vault 控制 lease 生命週期</em>、app 只要在 TTL 內續租即可。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a>：static secret 的 rotation 必須有 <em>scope map</em> — 哪些 service 用了同一把 secret、哪個 service 支援零停機 rotation、誰是 last to be rotated。沒這份 map 就會發生「rotate 後某個被遺忘的 cron job 認證失敗、整個下游崩」。</p>
<p><strong>Seal / unseal 設計</strong>：Vault 啟動時 sealed、必須 unseal 才能服務。Shamir secret sharing 是預設（5 key holders、3 threshold）— 任何重啟需要找齊 3 個人合 unseal、production 場景幾乎都該換 auto-unseal（用 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">GCP KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 當 master key custodian）。代價是 <em>把 master key 託給雲廠</em> — 不接受的組織保留 Shamir + 嚴格 key holder rotation。</p>
<p><strong>Audit device 是 <em>必開</em></strong>：Vault 預設不開 audit、要手動 enable（<code>vault audit enable file path=/var/log/vault_audit.log</code>）。沒 audit device 在 production = 事故時 <em>連 token 被誰用過都查不到</em>。建議多 channel（file + syslog + 推到外部 SIEM）— 單一 channel 失效（disk full、socket broken）Vault 會拒絕請求、影響 availability、所以多 channel 是必要冗餘。</p>
<p><strong>Break-glass 與 root token</strong>：初始化時產生的 root token 應該 <em>用完立刻 revoke</em>、改用 admin policy + OIDC auth。break-glass scenario 用 <em>recovery key 重新發 root token</em>、recovery key 走 Shamir 多人持有 + 離線存。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Vault (self-hosted)</th>
          <th>HCP Vault</th>
          <th>AWS Secrets Manager</th>
          <th>Google Secret Manager</th>
          <th>Azure Key Vault</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>自管 cluster（HA + replication）</td>
          <td>HashiCorp 託管</td>
          <td>AWS managed</td>
          <td>GCP managed</td>
          <td>Azure managed</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>強 — 同一介面跨 AWS / GCP / Azure / on-prem</td>
          <td>強</td>
          <td>弱 — 綁 AWS</td>
          <td>弱 — 綁 GCP</td>
          <td>弱 — 綁 Azure</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>DB / cloud / SSH engine 完整</td>
          <td>同 OSS</td>
          <td>無 — 僅 RDS / Redshift static rotation Lambda</td>
          <td>無 — 自寫 Cloud Function；secret-less 走 WIF</td>
          <td>無 — 純 static；secret-less 走 Managed Identity</td>
      </tr>
      <tr>
          <td>PKI / transit</td>
          <td>內建 PKI engine + transit engine</td>
          <td>同 OSS</td>
          <td>走 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> + <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">KMS</a></td>
          <td>走 cloud KMS + Certificate Authority Service</td>
          <td>走 <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> cert 功能</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>高 — HA、upgrade、replication、cert 自己顧</td>
          <td>低 — HashiCorp 顧</td>
          <td>低</td>
          <td>低</td>
          <td>低</td>
      </tr>
      <tr>
          <td>第三方信任成本</td>
          <td>低 — 自管</td>
          <td>中 — HashiCorp 控制面</td>
          <td>中 — AWS 控制面</td>
          <td>中 — GCP 控制面</td>
          <td>中 — Microsoft 控制面</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>跨雲、需要 dynamic credential、內部 PKI、預算允許</td>
          <td>想要 Vault 能力但不想自管</td>
          <td>AWS-heavy + 簡單 static secret</td>
          <td>GCP-heavy + Workload Identity 已主導</td>
          <td>Azure-heavy + Managed Identity 已主導</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — 自己掌握資料、但 dynamic engine 接線多</td>
          <td>中</td>
          <td>低</td>
          <td>低</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 Vault 的核心訴求：<em>跨雲 + dynamic credential + 內部 PKI + transit encryption 至少滿足兩項</em>、且能投入 SRE 量能跑 HA cluster、有 SIEM 接 audit log、能接受 self-hosted 的 upgrade / cert / DB 運維成本。單純需要 AWS-only static secret rotation、直接用 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a> 更便宜更簡單。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Dynamic credential 的 lease 生命週期治理</strong>：dynamic engine 發出的 credential 都帶 lease ID、Vault 在 TTL 到期時自動 revoke（database engine 真的會 DROP USER、cloud engine 真的會 DeleteAccessKey）。設計時要算清楚 <em>app 連線池的 connection lifetime</em> — DB connection 持續用同一組 credential、credential lease 過期但 connection 還在會出現 <em>staled credential</em> 問題。常見作法：lease TTL &gt; connection idle timeout * 2、加 lease renewal mechanism（app 在 TTL 50% 時主動 renew）。</p>
<p><strong>Transit engine（encryption-as-a-service）</strong>：app 不持 encryption key、把 plaintext 丟給 Vault <code>encrypt</code> API、拿 ciphertext 回來；解密時把 ciphertext 給 Vault <code>decrypt</code> API。Key 完全不離 Vault、所有 cryptographic operation 在 Vault 內、app 只需要 <em>encrypt / decrypt capability</em>。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558 signing key chain</a> 的對照啟示：key 不能 export 是減 blast radius 的關鍵設計 — transit 把這個原則內建。</p>
<p><strong>PKI engine + cert-manager 整合</strong>：Vault PKI engine 可以當內部 root CA + intermediate CA、issue 短期 cert（hours-level）給 K8s workload；<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> 用 Vault PKI issuer 自動更新 cert。比起手動跑 OpenSSL CA、Vault PKI 的優勢是 <em>cert lifecycle 進 Vault audit</em>、跟 secret rotation 用同一套 evidence chain（呼應 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">credential rotation scoped evidence</a>）。</p>
<p><strong>Namespace（Enterprise）跟 multi-tenancy</strong>：Enterprise 版 namespace 是 <em>tenant 邏輯隔離</em>、每個 namespace 有自己的 auth method、policy、secret engine。OSS 版沒 namespace — 多團隊共用 Vault 要靠 path 命名規約 + policy prefix 拼隔離、邊界較鬆。大組織通常需要 namespace 才能避免單一 admin 跨 team 越界。</p>
<p><strong>Replication（Enterprise）</strong>：Performance Replication（主從 + 多 region active）跟 DR Replication（純 standby）是兩個獨立功能。production HA 通常需要 <em>同 region 的 cluster + 跨 region 的 DR replication</em>、recovery key 跟 unseal 機制要跨 cluster 一致。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Audit device 沒開</strong>：production 啟動時忘了 enable audit、事故發生時無 forensic data — 啟動 checklist 必含「enable audit before serving traffic」、SRE runbook 用 health check 驗</li>
<li><strong>Policy 過寬</strong>：給整個 <code>secret/*</code> read、單一 token 等於拿到全公司 secret — 用 path prefix 限定到 <code>{team}/{env}/*</code>、policy review 走 PR</li>
<li><strong>Dynamic credential lease 太長 / 沒 max_ttl</strong>：DB user 跑了一週還沒收、攻擊者只要拿到一次就長期可用 — 設定 lease TTL = 1h、max_ttl = 24h</li>
<li><strong>Auto-unseal KMS access 沒監控</strong>：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">GCP KMS</a> 的 Vault auto-unseal key 沒 alert 異常使用 — KMS 端設 alert（GetKeyValue / Decrypt 突增）</li>
<li><strong>Replication lag 沒 alert</strong>：Performance / DR replication 落後幾分鐘到幾小時、failover 時拿到 stale state — Prometheus 監控 <code>vault.replication.*</code> metric</li>
<li><strong>Root token 未 revoke</strong>：初始化時的 root token 還在用、policy / audit / OIDC 全 bypass — 初始化 checklist 強制 revoke、CI 跑 <code>vault token lookup</code> 驗證 root 不可用</li>
<li><strong>Sealed 後 unseal key 找不到人</strong>：production cluster 緊急 restart、Shamir threshold 3 但有 1 個 key holder 在度假 — production 必須 auto-unseal、recovery key 走 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">break-glass</a> 流程</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only + 簡單 static secret</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a></td>
      </tr>
      <tr>
          <td>GCP-only + 已用 Workload Identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a></td>
      </tr>
      <tr>
          <td>Azure-only + 已用 Managed Identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></td>
      </tr>
      <tr>
          <td>大型 cryptographic / HSM 需求</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a>（FIPS 140-2 Level 3、Vault auto-unseal 後端）</td>
      </tr>
      <tr>
          <td>公開憑證 PKI（serving cert）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>K8s workload cert 自動化</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（可用 Vault 當 issuer）</td>
      </tr>
      <tr>
          <td>跨服務 workload identity (SPIFFE)</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
      </tr>
      <tr>
          <td>Secret 全公司 rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Vault 完整 API reference 跟 CLI 詳盡用法</li>
<li>每個 secret engine 的內部實作細節（DB connection pool、cloud SDK 呼叫順序）</li>
<li>Enterprise 各 license tier 的功能對照</li>
<li>Terraform / Ansible 跟 Vault 整合的完整步驟</li>
<li>各 auth method 的 OIDC / SAML provider 設定教學</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Vault 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Vault 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>static secret rotation 必須有 scope map — Vault KV 多 service 共用同一把 secret 時、rotation 要分批 + 雙軌驗證窗口、不能一次 push 全域更新</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>transit engine 的設計啟示 — key 不離保護邊界、即使被讀也搬不走、跟 HSM-bound 同 mindset</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation (red-team)</a></td>
          <td>CI 平台 secret 集中化的 blast radius — Vault AppRole secret_id 散落在 CI runner 時、CI 出事 = 大量 AppRole credential 一次外洩、需 scope tag + 優先級 rotation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System 2023</a></td>
          <td>對照啟示 — Vault 自己的 support / debug tooling（root token、recovery key）也是 secret leak vector、HAR 級別的事件可發生在任何 admin console</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>（Vault auto-unseal master key custodian）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（用 Vault PKI engine 作為 K8s workload cert issuer）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Vault 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://developer.hashicorp.com/vault/docs">Vault Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Okta</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/</guid><description>&lt;p>Okta 是 SaaS Identity Provider 的事實標準。它承擔三個責任：human identity 的 SSO 與 MFA、application / cloud account 的 federation gateway、SCIM-based lifecycle 自動化（joiners / movers / leavers）。當公司把 SSO 集中到 Okta、員工的工作信任邊界就從「每個應用各自的密碼」變成「Okta tenant + 客服流程 + signing key」三件事是否安全。在 &lt;a href="https://tarrragon.github.io/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 建&lt;/a> 的光譜上、把企業 SSO 交給 Okta 是認證 commodity「買」的代表選擇（feature SaaS 深度）；這個外包深度與遷出代價的權衡見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度&lt;/a> 卡。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Okta 是 &lt;em>人類身份的控制面&lt;/em>、不是 cloud resource permission engine。把 cloud IAM（AWS IAM、Google Cloud IAM、Azure RBAC）的角色指派交給 Okta 是常見組合 — Okta 負責「這個人是誰」、雲端 IAM 負責「這個身份能對 resource 做什麼」。Workforce Identity Cloud（員工）跟 Customer Identity Cloud（消費者、原 Auth0）是兩個產品線、安全模型跟事件分布都不同（本頁聚焦 Workforce、Auth0 見 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor&lt;/a>）。&lt;/p>
&lt;p>跟自管 IdP（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a>）相比、Okta 把 issuer 信任、signing key 生命週期、support tooling 都託管出去 — 代價是 &lt;em>第三方控制面的事故會直接打到自己&lt;/em>（Okta 2022 Sitel 環境洩漏、2023 support system HAR token 外洩、2023 cross-tenant impersonation）。跟 cloud-native SSO（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a>）相比、Okta 的核心優勢是 &lt;em>多雲 + SaaS app 數百個 integration 預先建好&lt;/em>、不是綁單一雲廠。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Okta 該承擔哪一段 identity 控制（SSO / MFA / lifecycle / federation）、哪一段該交給雲端 IAM&lt;/li>
&lt;li>Okta tenant 的信任邊界與最低稽核需求（admin role、API token、SCIM、support workflow）&lt;/li>
&lt;li>Okta 自己出事時的降級路徑（emergency access、break-glass、out-of-band MFA）&lt;/li>
&lt;li>何時用 Okta、何時走 Auth0 / Keycloak / AWS IAM Identity Center 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Okta 配置是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>Okta 是 SaaS Identity Provider 的事實標準。它承擔三個責任：human identity 的 SSO 與 MFA、application / cloud account 的 federation gateway、SCIM-based lifecycle 自動化（joiners / movers / leavers）。當公司把 SSO 集中到 Okta、員工的工作信任邊界就從「每個應用各自的密碼」變成「Okta tenant + 客服流程 + signing key」三件事是否安全。在 <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> 的光譜上、把企業 SSO 交給 Okta 是認證 commodity「買」的代表選擇（feature SaaS 深度）；這個外包深度與遷出代價的權衡見 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a> 卡。</p>
<h2 id="服務定位">服務定位</h2>
<p>Okta 是 <em>人類身份的控制面</em>、不是 cloud resource permission engine。把 cloud IAM（AWS IAM、Google Cloud IAM、Azure RBAC）的角色指派交給 Okta 是常見組合 — Okta 負責「這個人是誰」、雲端 IAM 負責「這個身份能對 resource 做什麼」。Workforce Identity Cloud（員工）跟 Customer Identity Cloud（消費者、原 Auth0）是兩個產品線、安全模型跟事件分布都不同（本頁聚焦 Workforce、Auth0 見 <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a>）。</p>
<p>跟自管 IdP（<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a>）相比、Okta 把 issuer 信任、signing key 生命週期、support tooling 都託管出去 — 代價是 <em>第三方控制面的事故會直接打到自己</em>（Okta 2022 Sitel 環境洩漏、2023 support system HAR token 外洩、2023 cross-tenant impersonation）。跟 cloud-native SSO（<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a>）相比、Okta 的核心優勢是 <em>多雲 + SaaS app 數百個 integration 預先建好</em>、不是綁單一雲廠。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Okta 該承擔哪一段 identity 控制（SSO / MFA / lifecycle / federation）、哪一段該交給雲端 IAM</li>
<li>Okta tenant 的信任邊界與最低稽核需求（admin role、API token、SCIM、support workflow）</li>
<li>Okta 自己出事時的降級路徑（emergency access、break-glass、out-of-band MFA）</li>
<li>何時用 Okta、何時走 Auth0 / Keycloak / AWS IAM Identity Center 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Okta 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能做什麼</strong>：Super Admin / Org Admin / Read-Only Admin 的人數、是否走 Okta 自己的 access request workflow、是否強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant 認證</a></li>
<li><strong>憑證在哪裡</strong>：API token 的 owner、scope、TTL、是否走 OAuth service app 而不是 personal API token；service account 是否獨立 audit</li>
<li><strong>入口如何暴露</strong>：SSO 是 SAML 還是 OIDC、IdP-initiated 是否關閉、admin console 是否限 IP / device trust、helpdesk reset 是否要 callback / out-of-band 驗證</li>
<li><strong>證據是否可回查</strong>：System Log 是否同步到 SIEM、admin / token / impersonation 事件是否 alert、是否保留 90 天以上</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Onboarding / lifecycle</strong>：HR 系統推 SCIM 進 Okta、Okta 推 SCIM 到下游 SaaS / 雲端 SSO。決策點是 <em>誰是 source of truth</em> — HRIS 還是 Okta 自己。混用會造成 stale account 與例外帳號無法收。</p>
<p><strong>Policy（authentication）</strong>：Sign-On Policy 跟 Authentication Policy（New Policy Framework）兩套並行、要避免規則交疊。高風險操作（admin login、寫權限應用）應該強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant</a> factor（WebAuthn / passkey）、不只是 push MFA（Uber 2022 揭露：純 push MFA 抗不過 fatigue）。</p>
<p><strong>MFA factor 選擇</strong>：避免 SMS / voice 作為主要 factor。Okta 2024 把 telephony 推給客戶 BYO（<a href="/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/" data-link-title="7.C7 Okta：BYO Telephony 的身份安全責任轉換" data-link-desc="MFA 簡訊/語音路徑從平台托管轉向客戶自管的治理案例。">Okta BYO Telephony case</a>）— 信任邊界從「Okta 全管」變成「客戶自己挑簡訊供應商」、若沒同步調整威脅模型會把 SMS swap 風險吃下來。</p>
<p><strong>API token / OAuth service app</strong>：personal API token 容易隨人員離職 stale、應該走 OAuth service app（client credentials）並把 scope 收到最小。token 不存 source code、走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 取用。</p>
<p><strong>Exception / break-glass</strong>：至少 2 個 break-glass admin、credential 離線存（紙本保險箱 / <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a> 隔離 tenant）、走獨立 MFA（hardware key、不依賴主要 Okta tenant 的 push）、季度驗證可用。Okta tenant 整個失聯時這是唯一退路。</p>
<p><strong>Audit / handoff</strong>：System Log 推進 SIEM、特別 alert 三類事件 — admin role 變更、API token 建立、impersonation / support access。Okta 2023 support system 事件展示：如果客戶沒 alert support impersonation 的 session、就只能等 Okta 公告。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Okta</th>
          <th>自管 Keycloak</th>
          <th>AWS IAM Identity Center</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制面責任</td>
          <td>Okta 託管 issuer / signing / support</td>
          <td>自己跑 issuer、key rotation、HA、support</td>
          <td>AWS 託管、限 AWS 帳號 + 已整合 SAML app</td>
      </tr>
      <tr>
          <td>Integration</td>
          <td>7000+ SaaS app 預建</td>
          <td>OIDC / SAML 通用、specific app 要自己接</td>
          <td>AWS 帳號 + 中等規模 SaaS</td>
      </tr>
      <tr>
          <td>第三方信任成本</td>
          <td>高 — Okta 出事客戶被動受害（2022 / 2023 多起）</td>
          <td>低 — 自管、自己承擔運維</td>
          <td>中 — 綁 AWS 信任邊界</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>低 — SaaS</td>
          <td>高 — HA、DR、cert、DB、upgrade 都要顧</td>
          <td>低 — AWS managed</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>多雲、大量 SaaS、需要 lifecycle 自動化</td>
          <td>預算 / 主權 / 自管要求、不接受 SaaS IdP</td>
          <td>AWS-heavy、員工數中等、SaaS 少</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — SAML / SCIM 接線分散在數百 app</td>
          <td>中 — 自己掌握資料</td>
          <td>中 — AWS 內部換</td>
      </tr>
  </tbody>
</table>
<p>選 Okta 的核心訴求：<em>跨雲 + 大量 SaaS app + lifecycle 要自動化</em>、且能接受第三方控制面風險、有預算做完整 SIEM / break-glass / 第三方應變流程。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Federation 跟 workload identity</strong>：Okta 對人類 SSO 強、對 workload identity 較弱。CI / 服務間用 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM role 的 OIDC trust</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google workload identity federation</a> 比把 Okta API token 散到服務裡更安全。</p>
<p><strong>Cross-tenant 邊界</strong>：B2B 合作（partner、contractor）要清楚是「partner 用自己 IdP 做 federation 進來」還是「partner 在我的 Okta tenant 開帳號」。2023 cross-tenant impersonation 事件（<a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">Okta Cross-Tenant case</a>）揭示：admin 工具若沒限定 tenant scope、單一 admin compromise 會跨多 tenant 擴散。</p>
<p><strong>Device trust / posture</strong>：Okta Device Trust + EDR signal 是補 phishing-resistant MFA 之後的下一層 — 確認 <em>使用者</em> 對之外、確認 <em>裝置</em> 健康。BYOD 比例高的組織這層做不起來就靠人類因子守。</p>
<p><strong>Identity Threat Protection / ITP</strong>：Okta 2024 推的事件偵測 add-on、補 session anomaly、credential stuffing、impossible travel 等場景。本質是把 SIEM detection 的一部分內建、不是取代外部 SIEM。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Admin account 過多</strong>：經常超過必要 — 用 Group Rules + Access Request workflow 收斂、把日常操作用 Read-Only Admin + 特定權限 group 替代</li>
<li><strong>API token stale / 散落</strong>：personal API token 跟著員工離職留下 — 季度盤點、改 OAuth service app</li>
<li><strong>SMS MFA 還是預設</strong>：MFA enrollment 沒強制 WebAuthn / passkey、新員工選最弱 factor — Authentication Policy 應該限制可選 factor</li>
<li><strong>System Log 沒進 SIEM</strong>：事件只在 Okta UI、alert 沒接 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> — 用 Log Streaming（CloudWatch / S3 / Splunk HEC）打進 SIEM、特定事件接 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
<li><strong>Helpdesk reset 無 callback</strong>：MGM 2023 / Caesars 2023 都是 helpdesk social engineering、需要 callback + out-of-band 驗證、不是 ticket 上看到「我忘記密碼」就 reset</li>
<li><strong>Support 工具 session 沒監控</strong>：Okta 2023 support 事件揭示需要 alert <em>support impersonation session 進入我的 tenant 的事件</em> — System Log 有對應事件、但通常沒 default alert</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a></td>
      </tr>
      <tr>
          <td>自管 / 不接受 SaaS IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a></td>
      </tr>
      <tr>
          <td>AWS-only 員工 SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
      </tr>
      <tr>
          <td>Microsoft 365 / Azure 重度組織</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID（Azure RBAC vendor 頁）</a> — Entra ID 是 Microsoft 自家 workforce IdP、跟 Okta 直接競爭、M365 + Azure 為主的組織通常直接用 Entra ID 而非疊一層 Okta</td>
      </tr>
      <tr>
          <td>Cloud resource permission（非人類身份）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>事件偵測（不只 Okta 內部）</td>
          <td>04 SIEM / detection 工具（<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 跟 07 SIEM 章節）</td>
      </tr>
      <tr>
          <td>Secret / API key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Okta 完整 SAML / OIDC 規格細節、SCIM schema 客製</li>
<li>Workforce vs Customer Identity Cloud 完整功能對照</li>
<li>Okta 各定價層級的功能差異</li>
<li>各 SaaS app 的 SSO 接線教學</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Okta 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System Incident 2023</a></td>
          <td>支援工具鏈納入身份治理、HAR session 透過個人 Chrome profile 同步外洩、客戶側必須 alert impersonation session</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">Okta Cross-Tenant Impersonation 2023</a></td>
          <td>admin tool 缺 tenant scope、單一 admin compromise 跨 tenant 擴散</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/" data-link-title="7.C7 Okta：BYO Telephony 的身份安全責任轉換" data-link-desc="MFA 簡訊/語音路徑從平台托管轉向客戶自管的治理案例。">Okta BYO Telephony Shift</a></td>
          <td>telephony 供應商責任轉移、客戶要重新評估 SMS 路徑威脅模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023 Okta Token Follow-Through</a></td>
          <td>上游 IdP 事件後客戶側的 token / session rotation 節奏、不該等供應商公告</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>純 push MFA 抗不過 fatigue、高風險操作要求 phishing-resistant factor</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 Identity Lateral Impact</a></td>
          <td>helpdesk social engineering 是 Okta-customer 通用入口、callback / out-of-band 驗證是控制面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022 Social Engineering</a></td>
          <td>員工身份即客戶風險面、IdP 對員工帳號異常的隔離速度決定下游受損規模</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Okta API token / OAuth service app credential 的 rotation 必須分域、不能把多 service app 共用同一批 rotation 命令打</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>（Okta 之後的 cloud resource permission 層）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Okta 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://help.okta.com/">Okta Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Splunk</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/</guid><description>&lt;p>Splunk 是 SIEM（Security Information and Event Management）的事實標準、大企業 / 金融 / 政府的 SOC 主流選擇。2024 年被 Cisco 收購、產品線維持獨立發展。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a> 的差異在 &lt;em>計費模型 + ecosystem maturity + detection content 深度&lt;/em>、偵測能力本身相近 — Splunk 的 ingestion-based pricing 是業界最貴的 SIEM 計費模式、但 detection content 跟 SOC tooling ecosystem 也是最成熟的。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Splunk 的核心定位是 &lt;em>任意 log source 的統一查詢平台&lt;/em>、SIEM 是其上的 &lt;em>application layer&lt;/em>（Splunk Enterprise Security app）。底層是 &lt;em>Splunk Enterprise&lt;/em>（自管）或 &lt;em>Splunk Cloud Platform&lt;/em>（SaaS）、頂層產品包含：&lt;em>Enterprise Security (ES)&lt;/em> — premium SIEM app、含 correlation rule、Risk-Based Alerting、ITSI 整合；&lt;em>SOAR&lt;/em>（前 Phantom）— security orchestration / automated response；&lt;em>UBA&lt;/em>（User Behavior Analytics）— ML-based anomaly detection。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a> 比、Splunk 走 &lt;em>deeper but more expensive&lt;/em> — SPL 比 KQL / EQL 表達力更強、detection content（Splunk Security Content 公開 YAML rules）覆蓋廣、ES app 的 Risk-Based Alerting 是業界先驅；但 ingestion-based pricing 在 TB/day 級別會痛。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 比、Splunk 走 &lt;em>security-first&lt;/em>、Datadog Cloud SIEM 是 &lt;em>observability platform 加上 security view&lt;/em>；Datadog 適合 cloud-native + 中等規模、Splunk 適合 enterprise + 跨 on-prem。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a>（前 Chronicle）比、Google Security Ops 走 &lt;em>fixed-price by data、massive scale&lt;/em>、Splunk 是 &lt;em>per-GB 累進&lt;/em>、超大規模反而 Google 划算。&lt;/p></description><content:encoded><![CDATA[<p>Splunk 是 SIEM（Security Information and Event Management）的事實標準、大企業 / 金融 / 政府的 SOC 主流選擇。2024 年被 Cisco 收購、產品線維持獨立發展。它跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 的差異在 <em>計費模型 + ecosystem maturity + detection content 深度</em>、偵測能力本身相近 — Splunk 的 ingestion-based pricing 是業界最貴的 SIEM 計費模式、但 detection content 跟 SOC tooling ecosystem 也是最成熟的。</p>
<h2 id="服務定位">服務定位</h2>
<p>Splunk 的核心定位是 <em>任意 log source 的統一查詢平台</em>、SIEM 是其上的 <em>application layer</em>（Splunk Enterprise Security app）。底層是 <em>Splunk Enterprise</em>（自管）或 <em>Splunk Cloud Platform</em>（SaaS）、頂層產品包含：<em>Enterprise Security (ES)</em> — premium SIEM app、含 correlation rule、Risk-Based Alerting、ITSI 整合；<em>SOAR</em>（前 Phantom）— security orchestration / automated response；<em>UBA</em>（User Behavior Analytics）— ML-based anomaly detection。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> 比、Splunk 走 <em>deeper but more expensive</em> — SPL 比 KQL / EQL 表達力更強、detection content（Splunk Security Content 公開 YAML rules）覆蓋廣、ES app 的 Risk-Based Alerting 是業界先驅；但 ingestion-based pricing 在 TB/day 級別會痛。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 比、Splunk 走 <em>security-first</em>、Datadog Cloud SIEM 是 <em>observability platform 加上 security view</em>；Datadog 適合 cloud-native + 中等規模、Splunk 適合 enterprise + 跨 on-prem。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（前 Chronicle）比、Google Security Ops 走 <em>fixed-price by data、massive scale</em>、Splunk 是 <em>per-GB 累進</em>、超大規模反而 Google 划算。</p>
<p>關鍵張力：<em>ingestion-based 計費</em> ↔ <em>偵測覆蓋率</em> 是 Splunk 客戶最大的 trade-off。為了省錢選擇性 ingest log（只進 Windows Event Log 不進 Linux auth log、只進 prod 不進 dev）、結果 Storm-0558 / Uber MFA 那種跨來源 correlation 抓不到。要看清楚自己 <em>容忍多少偵測盲點換多少預算</em>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Splunk 在 SOC stack 中承擔哪一段（log aggregation / SIEM / SOAR / UBA）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 管 service token、IdP log 來源治理）</li>
<li>SPL / correlation rule / detection content 的 ownership 設計（誰寫、誰 review、誰調 false positive）</li>
<li>Ingestion pricing trap 的應對（log priority tiering、Cribl / Cribl Stream 做 pre-filter、Splunk SmartStore 把冷資料丟 S3）</li>
<li>何時用 Splunk、何時走 Elastic / Datadog / Google Security Ops 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Splunk deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能改 correlation rule</strong>：Splunk admin / ES admin / KV store admin 的人數、SPL search 跟 saved search 是否走版控（Git → <code>git-fusion</code> / Splunk Cloud Versioned Configs）、rule change 是否經 PR review</li>
<li><strong>Ingestion 治理</strong>：哪些 source 進 Splunk（IdP audit log / cloud control plane log / endpoint log / network log / app log）、是否有 <em>log priority tier</em>（critical / standard / archive）、Cribl Stream 是否在前面做 pre-filter / routing</li>
<li><strong>Detection content coverage</strong>：Splunk Security Content（<a href="https://research.splunk.com/">公開 YAML rule library</a>）有多少 enabled、是否跟 MITRE ATT&amp;CK 對照、自家 custom rule 是否補 organization-specific anti-pattern</li>
<li><strong>Alert quality / SOC handoff</strong>：alert volume per day、SOC analyst triage time、false positive rate、alert 是否進 SOAR playbook 自動處理低風險、跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> 的 routing 是否定義</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Ingestion architecture</strong>：log 進 Splunk 三種路徑 — <em>Universal Forwarder</em> / <em>Heavy Forwarder</em>（agent-based，自管 host）、<em>HTTP Event Collector (HEC)</em>（push log via HTTP endpoint、SaaS / serverless workload 預設）、<em>Splunk Add-on for 各 cloud / SaaS</em>（cloud-native log pull）。production 通常混用：endpoint 用 Universal Forwarder、cloud control plane 用 Add-on（AWS / GCP / Azure / Okta）、自家 app 用 HEC。在前面接 <em>Cribl Stream</em> 做 routing / filtering / sampling 是大型 deployment 的標準補位。</p>
<p><strong>SPL（Search Processing Language）</strong>：類 Unix pipe 的 <code>|</code> 串接（<code>index=ids sourcetype=auth | stats count by user | where count &gt; 100</code>）、表達力強但學習曲線陡。SPL 是 first-class concept、不只是查詢工具 — saved search 變 correlation rule、scheduled search 變 alert、accelerated search 變 data model 加速。SPL 寫得好不好直接決定 <em>偵測規則品質 + 查詢成本</em>。</p>
<p><strong>Correlation rule / Notable Event</strong>：ES app 把 high-confidence finding 轉成 <em>Notable Event</em>、進 Incident Review queue。Correlation rule 的反例是 <em>single-event alert</em>（看到一個 SSH brute force attempt 就 alert、SOC analyst 一天看 10000 個沒意義）— production rule 應該是 <em>time-bounded aggregation</em>（過去 5min 內 100 個 brute force from same IP）+ <em>cross-source correlation</em>（brute force IP 同時出現在 cloud control plane access）。</p>
<p><strong>Detection content lifecycle</strong>：Splunk Security Content 是 Splunk 維護的 OSS detection rule library、YAML format、跟 MITRE ATT&amp;CK 對應。組織通常 <em>先 import 全部 baseline、再選擇性 disable noisy 規則 + 新增 organization-specific 規則</em>。Rule change 走 PR review、staging tenant 跑 24-48hr 觀察 false positive curve 才 promote 到 production。對應 <a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a> 的章節原則。</p>
<p><strong>Risk-Based Alerting (RBA)</strong>：ES app 7.0+ 引入、給每個 user / asset 累積 <em>risk score</em>（取代逐 finding alert）、累積到 threshold 才 alert。處理 alert fatigue 的工程化做法：5 個 low-confidence signal 加總超過 threshold 比單一 high-confidence alert 更接近真實 attack pattern。對應 <a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality</a>。</p>
<p><strong>SOAR integration</strong>：Splunk SOAR（前 Phantom）接 alert + playbook 自動執行 — 例如 leaked credential 自動 rotate（拉 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> API）、suspect IP 自動加 firewall block（拉 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> custom rule）、suspect user 自動 force MFA re-enroll（拉 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> API）。playbook 進版控、定期 dry-run、不能黑箱 production fire-and-forget。</p>
<p><strong>Ingestion pricing 治理</strong>：Splunk 按 ingestion volume（GB/day）計費、TB-scale deployment 年費千萬美元級別。實務治理：<em>tier 1 log</em>（IdP / cloud control plane / payment processor / DB audit）進 Splunk hot index、<em>tier 2 log</em>（app log / web access log）按 sampling / filtering 進 Splunk、<em>tier 3 log</em>（debug / verbose）走 <a href="https://docs.splunk.com/Documentation/Splunk/latest/Indexer/AboutSmartStore">SmartStore</a> 到 S3 / GCS 冷儲存、或繞過 Splunk 直接打到 Elastic / data lake。Cribl Stream 在 forwarder 前 pre-filter 是業界標準作法、可省 30-50% ingestion cost。</p>
<p><strong>SmartStore 跟冷熱分離</strong>：SmartStore 把 indexer 的 <em>warm + cold bucket</em> 放到 S3 / Azure Blob / GCS、indexer 只保留 hot data + cache。意義是 <em>retention 從幾個月延長到幾年但 cost 不線性漲</em>。production deployment 幾乎都該開、不開等於每年砸錢買 EBS。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Splunk</th>
          <th>Elastic Security</th>
          <th>Datadog Security</th>
          <th>Google Security Operations</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>Ingestion-based（GB/day、累進）</td>
          <td>Resource-based（node / cluster size）</td>
          <td>Per-host + per-event（events/month）</td>
          <td>Fixed price by data tier（PB-scale 划算）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>陡 — SPL 表達力強但 idiom 多</td>
          <td>中 — KQL / EQL 較直觀</td>
          <td>緩 — 沿用 Datadog observability 語法</td>
          <td>中 — YARA-L 是新語法但結構清楚</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted (Splunk Enterprise) / SaaS (Cloud)</td>
          <td>Self-hosted / Elastic Cloud / Serverless</td>
          <td>SaaS only</td>
          <td>SaaS only（Google Cloud）</td>
      </tr>
      <tr>
          <td>Detection content</td>
          <td>Splunk Security Content（最豐富、社群活躍）</td>
          <td>Elastic Prebuilt rules + Sigma 支援</td>
          <td>Datadog Security Rules（中等）</td>
          <td>Google YARA-L 內建 + Google threat intel</td>
      </tr>
      <tr>
          <td>SOAR / Response</td>
          <td>Splunk SOAR（前 Phantom、業界先驅）</td>
          <td>內建 Cases + Endpoint response（Elastic Defend）</td>
          <td>Workflow Automation（基本）</td>
          <td>SOAR 內建（前 Siemplify）</td>
      </tr>
      <tr>
          <td>跨來源 correlation</td>
          <td>強 — data model + SPL 支撐</td>
          <td>強 — EQL sequence + Lucene</td>
          <td>中 — log + metrics + trace 同 plane</td>
          <td>強 — UDM normalization + cross-tenant</td>
      </tr>
      <tr>
          <td>Multi-cloud</td>
          <td>強 — Add-on 覆蓋三大雲</td>
          <td>強 — Beats / Agent 跨雲</td>
          <td>強 — Datadog Agent 跨雲</td>
          <td>GCP-first、跨雲靠 Forwarder</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Enterprise + 跨 on-prem / 多雲、預算允許</td>
          <td>OSS-friendly、中大型、Elastic stack 已用</td>
          <td>Cloud-native、observability 已用 Datadog</td>
          <td>超大規模 ingestion、Google 雲 + 多雲 SOC</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — SPL / detection content / dashboard 量多</td>
          <td>中 — Sigma / Lucene 較可移植</td>
          <td>中</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<p>選 Splunk 的核心訴求：<em>Enterprise scale + 跨 on-prem + detection content 跟 SOC tooling ecosystem 成熟</em>、且能投入預算（千萬美元級別 license + Cribl pre-filter + SmartStore 冷儲存治理）+ 有 SOC team 維護 correlation rule 跟 SOAR playbook。中等規模 cloud-native 直接走 Datadog / Google Security Ops 更划算。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Enterprise Security app 的 Risk-Based Alerting</strong>：RBA 把「事件 → alert」改成「事件 → risk score → 累積 → alert」、是 alert fatigue 的工程化解法。實作要決定 <em>risk decay window</em>（多久後 risk score 衰減）、<em>risk attribution</em>（同一台 EC2 上多 user 的 risk 怎麼分）、<em>per-asset vs per-user threshold</em>。配對 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a> 的 lesson：單一 MFA fail 不該 alert、5min 內 50 個 fail + 新裝置 + 異常地理就是 high risk。</p>
<p><strong>Common Information Model (CIM) + Data Model</strong>：Splunk CIM 把不同 source 的欄位 normalize 到統一 schema（authentication / network_traffic / web 等 data model）。意義是 SPL 跨 source 寫一次、不用為 Okta log / Azure AD log / CrowdStrike log 各寫一份。CIM 配合 Add-on 自動 mapping、organization 寫 custom source 需要自己定 CIM mapping。</p>
<p><strong>Multi-tenant deployment</strong>：MSSP / 大型集團多 BU 共用一個 Splunk 部署、用 <em>index</em>（隔離 data）+ <em>role / capability</em>（隔離 access）+ <em>App</em>（隔離 dashboard / search）三層。注意 <em>Splunk admin</em> 在跨 tenant 場景是高權限角色、應該走 break-glass 流程 + audit。</p>
<p><strong>Cisco 整合（2024+）</strong>：Cisco 收購後 Splunk 跟 Cisco XDR / Talos threat intel / Cisco Secure Endpoint 整合加速。對 Cisco-heavy 環境是 ecosystem 一致性增加；對非 Cisco 環境暫時影響有限、但長期 roadmap 會有 Cisco-specific 加值。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Alert volume 爆炸 / SOC 看不完</strong>：correlation rule 寫成 single-event alert、或 false positive baseline 沒調 — 用 RBA 改 risk-based、staging tenant 跑 48hr 觀察再 promote</li>
<li><strong>Detection coverage 出事故時才發現缺</strong>：critical log source 沒進 Splunk（為了省錢）— 補回 tier 1 log priority、用 Cribl Stream 對 tier 2 / 3 做 sampling 而非整批不 ingest</li>
<li><strong>Ingestion cost 暴衝</strong>：新 source 加入沒 review、debug log 直接打進 Splunk — Cribl Stream 前置 + license usage dashboard alert + indexer ingestion quota</li>
<li><strong>SPL search 慢 / 卡 search head</strong>：full-fidelity search on 1TB raw event、沒用 data model acceleration — 改用 accelerated data model、限定 time range、用 <code>tstats</code> 而非 <code>stats</code></li>
<li><strong>Correlation rule false positive 多</strong>：rule 寫得太寬、env-specific noise 沒 tune — staging tenant 跑 1 週統計 FP、tune threshold、加 lookup table 排除已知合法 source</li>
<li><strong>SOAR playbook 黑箱 fire-and-forget</strong>：自動 disable account 結果誤殺 CEO — playbook 走 <em>approval gate</em> for high-impact action、defaults to <em>containment</em> not <em>deletion</em></li>
<li><strong>Splunk admin 太多 / 沒 break-glass</strong>：日常運維用 admin token、admin compromise blast radius 太大 — 收 admin 角色、改 power user + 特定 capability、break-glass 走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSS-friendly / 預算敏感</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>Cloud-native + observability 已用</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></td>
      </tr>
      <tr>
          <td>超大規模 ingestion + Google 雲</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>DLP / sensitive data discovery</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Endpoint detection 為主</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint</td>
      </tr>
      <tr>
          <td>Pre-filter / log routing</td>
          <td>Cribl Stream（前置 forwarder、不是替代 SIEM）</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>SPL 完整語法 reference、saved search 跟 macro 進階用法</li>
<li>Splunk Cloud Platform vs Splunk Enterprise 的功能對照細節</li>
<li>Splunk Observability Cloud（前 SignalFx 收購、跟 Datadog 直接競爭、屬 observability 不屬 security）</li>
<li>ITSI（IT Service Intelligence）— 屬 ITSM / observability、不在資安範圍</li>
<li>SOAR playbook 的具體實作（Phantom Python SDK）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Splunk 在 07 案例庫沒有直接 vendor-level 事件、但所有 detection-related case 都是 SIEM 偵測覆蓋率的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Splunk 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>MFA 請求密度應是 Splunk correlation rule first-class signal、5min window count &gt; N 直接 alert + RBA 升級高風險 user score</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>跨租戶 token 異常驗證需 Splunk Add-on for Azure AD + cloud control plane log 同時 ingest、跨來源 correlation 才能秒級偵測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>資料平台 query volume + 跨 schema scan + 來源 IP 異常的複合 correlation rule、不只看 audit log 也要 query metrics correlation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>簽章驗證通過但 runtime 行為異常需 endpoint log + network log correlation、不靠 IoC-only 規則</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>Splunk Security Content + 自家 custom rule 走 propose → staging tune → promote → review 的工程 lifecycle、不是 console 直改</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>RBA 是工程化解 alert fatigue、不是「忽略低風險」、要設 risk decay + threshold tuning lifecycle</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP signal 進 Splunk）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP log source）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（SOAR playbook 拉 API）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（WAF log + auto-block）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Notable Event → IR routing）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（log pipeline 共用）</li>
<li>官方：<a href="https://docs.splunk.com/">Splunk Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>k6</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/</guid><description>&lt;p>k6 的核心責任是把 workload model 轉成可重跑、可版本化、可接到 CI 的壓測 scenario。它適合 API、HTTP、gRPC、WebSocket 與 browser-style flow 的負載驗證，重點在用程式化腳本描述使用者行為、負載階段、threshold 與結果輸出。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>k6 是 Grafana Labs 旗下的 scriptable load testing 工具、2021 年被 Grafana 收購。產品線分兩層：&lt;em>k6 OSS&lt;/em>（Go 寫的 engine + JS API 描述 scenario、CLI 為主、output 可丟 Prometheus / InfluxDB / JSON / CSV）跟 &lt;em>Grafana Cloud k6&lt;/em>（前 k6 Cloud、SaaS 多 region runner + 結果保存 + 跟 Grafana Cloud dashboard / Loki / Tempo 同 plane）。底層 engine 是 Go、不是 JS — JS 只是 scenario 描述層、runtime 由 Go 跑、所以單機 VU 容量比 Python-based 工具高出一個量級。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter&lt;/a> 比、k6 走 &lt;em>code-first + CI-friendly&lt;/em>、JMeter 走 &lt;em>XML / GUI + plugin ecosystem&lt;/em>；JMeter 在 protocol 廣度（JDBC / LDAP / JMS / FTP）跟非工程團隊操作勝出、k6 在版控、PR review、artifact pipeline 勝出。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust&lt;/a> 比、k6 用 JS、Locust 用 Python；Locust 對 Python team 自然、但 Python GIL 讓單機 VU 容量受限、需多 worker、k6 單機可跑數千 VU。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling&lt;/a> 比、Gatling 走 JVM + Scala/Java/Kotlin DSL、適合 JVM-heavy 團隊；k6 的 threshold + Grafana ecosystem 整合在 release gate 場景更直接。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>k6 適合把壓測納入工程流程。當團隊已經能描述 traffic shape、endpoint mix、arrival rate、think time 與 stop condition，k6 可以把這些模型寫成腳本，讓每次 release、capacity review 或 peak-event readiness 都能重跑同一組驗證。&lt;/p>
&lt;p>這個定位讓 k6 接到三個主章。它從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 接收流量模型，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 接收 ramp-up 與 knee point 判讀，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 接收 canary、dark launch 或 production-like load test 的安全邊界。&lt;/p></description><content:encoded><![CDATA[<p>k6 的核心責任是把 workload model 轉成可重跑、可版本化、可接到 CI 的壓測 scenario。它適合 API、HTTP、gRPC、WebSocket 與 browser-style flow 的負載驗證，重點在用程式化腳本描述使用者行為、負載階段、threshold 與結果輸出。</p>
<h2 id="服務定位">服務定位</h2>
<p>k6 是 Grafana Labs 旗下的 scriptable load testing 工具、2021 年被 Grafana 收購。產品線分兩層：<em>k6 OSS</em>（Go 寫的 engine + JS API 描述 scenario、CLI 為主、output 可丟 Prometheus / InfluxDB / JSON / CSV）跟 <em>Grafana Cloud k6</em>（前 k6 Cloud、SaaS 多 region runner + 結果保存 + 跟 Grafana Cloud dashboard / Loki / Tempo 同 plane）。底層 engine 是 Go、不是 JS — JS 只是 scenario 描述層、runtime 由 Go 跑、所以單機 VU 容量比 Python-based 工具高出一個量級。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a> 比、k6 走 <em>code-first + CI-friendly</em>、JMeter 走 <em>XML / GUI + plugin ecosystem</em>；JMeter 在 protocol 廣度（JDBC / LDAP / JMS / FTP）跟非工程團隊操作勝出、k6 在版控、PR review、artifact pipeline 勝出。跟 <a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a> 比、k6 用 JS、Locust 用 Python；Locust 對 Python team 自然、但 Python GIL 讓單機 VU 容量受限、需多 worker、k6 單機可跑數千 VU。跟 <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a> 比、Gatling 走 JVM + Scala/Java/Kotlin DSL、適合 JVM-heavy 團隊；k6 的 threshold + Grafana ecosystem 整合在 release gate 場景更直接。</p>
<h2 id="定位">定位</h2>
<p>k6 適合把壓測納入工程流程。當團隊已經能描述 traffic shape、endpoint mix、arrival rate、think time 與 stop condition，k6 可以把這些模型寫成腳本，讓每次 release、capacity review 或 peak-event readiness 都能重跑同一組驗證。</p>
<p>這個定位讓 k6 接到三個主章。它從 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 接收流量模型，從 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 接收 ramp-up 與 knee point 判讀，從 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 接收 canary、dark launch 或 production-like load test 的安全邊界。</p>
<h2 id="適用場景">適用場景</h2>
<p>API 壓測是 k6 最穩定的入口。Checkout、login、search、order query、payment callback mock 與 internal API 都可以用 scenario 表達，並用 threshold 把 latency、error rate 與 throughput 轉成 pass / fail 訊號。</p>
<p>CI performance gate 是 k6 的常見價值。團隊可以在 merge、nightly、pre-release 或 game day 前跑固定 baseline，觀察 p95 / p99、error rate、throughput 與 regression trend，再把結果交給 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a>。</p>
<p>Peak readiness rehearsal 適合用 k6 表達階段式負載。活動前可以用 ramping arrival rate 模擬 T-90、T-30、T-7、T-1 與 T-0 的負載階段，並把結果回寫到 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a>。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 k6 deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Scenario design</strong>：用 <code>executor: ramping-arrival-rate</code> 而非 <code>constant-vus</code>、把 RPS / arrival rate 設成 first-class、VU 由 engine 自動算；scenario 描述跟 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 的 endpoint mix、think time、cohort 對得起來</li>
<li><strong>Threshold gate</strong>：<code>thresholds</code> 區塊明確寫 p95 / p99 / error rate / throughput、CI fail 條件清楚、不靠人眼看 summary 判斷 pass / fail</li>
<li><strong>Output 進 observability stack</strong>：<code>--out experimental-prometheus-rw</code> 把 metric remote-write 到 Prometheus、Grafana dashboard 接 k6 同 datasource、結果跟 target service 的 saturation metric 在同一張圖上看</li>
<li><strong>k6 Cloud vs CLI 邊界</strong>：本地 CLI 跑 baseline + CI、Grafana Cloud k6 跑跨 region / 大規模 / 結果 retention；不要把 CI gate 放 Cloud（成本 + 時間不對）、也不要本地單機硬跑 100k VU（runner 自身瓶頸假象）</li>
</ul>
<p>四件事任一缺失、就是 scenario 已經寫得不完整、threshold gate 失效、或 runner 觀測缺失。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>k6 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>腳本化</td>
          <td>scenario、threshold、setup / teardown 可版本化</td>
          <td>production traffic 抽樣與模型校正</td>
      </tr>
      <tr>
          <td>CI 友善</td>
          <td>CLI 與 artifact 容易接 pipeline</td>
          <td>長期趨勢儲存與 release gate 語意</td>
      </tr>
      <tr>
          <td>API 導向</td>
          <td>HTTP / gRPC / WebSocket 等常見 API 場景清楚</td>
          <td>複雜瀏覽器互動與端到端資料準備</td>
      </tr>
      <tr>
          <td>團隊學習成本</td>
          <td>JavaScript 腳本容易被多數 backend 團隊接手</td>
          <td>大型分散式 runner 與測試資料治理</td>
      </tr>
  </tbody>
</table>
<p>腳本化價值來自可重跑。一次性的壓測只能回答當天配置能撐多少；可版本化 scenario 可以回答 release 後容量曲線有沒有漂移，並讓退化調查回到同一份 workload model。</p>
<p>CI 友善價值來自交接成本低。壓測結果要能轉成 artifact、threshold、trend 與 gate decision，才會從「工程師手動跑工具」變成 release 流程的一部分。</p>
<p>API 導向價值來自後端路徑明確。k6 很適合 checkout API、search API、internal API 與 webhook receiver；如果主要問題是完整 browser UX、第三方真實支付或多裝置同步，文章要把資料準備、side effect 與環境隔離另外寫清楚。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>k6 和 JMeter 的主要差異是工作方式。k6 偏程式化腳本、CLI、CI artifact 與工程流程；JMeter 偏 GUI、protocol plugin、既有企業測試流程與非工程團隊協作。</p>
<p>k6 和 Gatling 的主要差異是生態與語言。k6 使用 JavaScript-style 腳本，Gatling 偏 JVM / Scala / Java / Kotlin 生態；團隊語言能力與既有 pipeline 會影響維護成本。</p>
<p>k6 和 Locust 的主要差異是團隊技能與模型表達。Locust 使用 Python，對 Python 團隊與 custom user behavior 很自然；k6 的 threshold、CLI 與雲端 / Grafana 生態讓 release gate 整合更直接。</p>
<p>k6 和 Vegeta 的主要差異是場景複雜度。Vegeta 適合簡單 HTTP load、CLI workflow 與快速 saturation 探測；k6 適合較完整的 multi-step scenario、threshold 與長期 baseline。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>k6</th>
          <th><a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a></th>
          <th><a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></th>
          <th><a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scenario 語言</td>
          <td>JavaScript（ES6+）</td>
          <td>XML（GUI 編輯）/ Groovy</td>
          <td>Python</td>
          <td>Scala / Java / Kotlin DSL</td>
      </tr>
      <tr>
          <td>Engine runtime</td>
          <td>Go</td>
          <td>JVM</td>
          <td>Python（gevent）</td>
          <td>JVM（Akka）</td>
      </tr>
      <tr>
          <td>單機 VU 容量</td>
          <td>高（thousands+）</td>
          <td>中（JVM heap-bound）</td>
          <td>中低（GIL、需 multi-worker）</td>
          <td>高（Akka actor）</td>
      </tr>
      <tr>
          <td>CI 友善度</td>
          <td>強 — CLI + threshold + JSON / Prometheus</td>
          <td>中 — 需 plugin / Jenkins integration</td>
          <td>中 — CLI 友善但 result reporting 較弱</td>
          <td>強 — CLI + HTML report + Maven/Gradle plugin</td>
      </tr>
      <tr>
          <td>Protocol 廣度</td>
          <td>HTTP / gRPC / WebSocket / Browser</td>
          <td>最廣（JDBC / LDAP / JMS / FTP / SMTP）</td>
          <td>HTTP 為主、其他靠 custom client</td>
          <td>HTTP / WebSocket / JMS / MQTT</td>
      </tr>
      <tr>
          <td>Browser test</td>
          <td>k6 Browser（Playwright-based）</td>
          <td>無原生（Selenium plugin）</td>
          <td>無原生</td>
          <td>無原生</td>
      </tr>
      <tr>
          <td>Distributed</td>
          <td>k6 Cloud / k6 Operator on k8s</td>
          <td>Master / Slave（運維重）</td>
          <td>Master / Worker</td>
          <td>Gatling Enterprise / FrontLine</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>API-first + CI gate + Grafana ecosystem</td>
          <td>企業 + protocol 多 + 非工程團隊</td>
          <td>Python team + custom user behavior</td>
          <td>JVM team + DSL 表達力</td>
      </tr>
  </tbody>
</table>
<p>選 k6 的核心訴求：API-first scenario + CI gate + Grafana / Prometheus ecosystem 已用、且團隊接受 JS DSL。Protocol 廣度需求大、走 JMeter；Python team、走 Locust；JVM-heavy、走 Gatling。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>k6 Browser</strong>：基於 Chromium + Playwright API、跑在 k6 同 scenario 內、可混 protocol-level 跟 browser-level load（前段 API call、後段真實 browser flow）。意義是「pure API load 跟 real user UX 在同一份 scenario」、不用維護兩套工具。但 browser VU 比 protocol VU 重幾十倍、runner cost 要重新算。</p>
<p><strong>xk6 extensions</strong>：用 Go 寫 k6 extension、補 protocol（Kafka / Redis / SQL / AMQP）或 output（custom backend）。<code>xk6 build</code> 生出客製 binary、organization 可維護自家 extension。意義是 k6 不只跑 HTTP — Kafka producer load / Redis hot-key probe 都能用同一個 scenario harness。</p>
<p><strong>Grafana Cloud k6（前 k6 Cloud）</strong>：SaaS 跑 multi-region runner、結果保存、跟 Grafana Cloud dashboard / Loki / Tempo / Prometheus 同 plane。適合 <em>跨 region 真實延遲驗證</em>、<em>大規模 distributed run</em>、<em>結果 retention + team share</em>。跟 Grafana Cloud 已用的團隊 ecosystem 一致；只用 OSS 的團隊走 k6 Operator on k8s。</p>
<p><strong>Distributed execution</strong>：自管 distributed 走 <a href="https://github.com/grafana/k6-operator">k6 Operator</a> on Kubernetes、scenario 拆 instance、結果 aggregate 到 output。意義是不需要 k6 Cloud 也能跑跨機器 load、但 runner pool 自管成本 + 結果 aggregation 自己處理。</p>
<p><strong>Output integration</strong>：<code>--out experimental-prometheus-rw</code> 直接 remote-write 到 Prometheus、Grafana dashboard 一張圖看 k6 client metric + target service saturation；<code>--out cloud</code> 上 Grafana Cloud k6；<code>--out json=...</code> 落地檔案給 CI artifact；<code>--out influxdb</code> 接 InfluxDB（legacy）。Loki 用來接 k6 console log、Tempo 用來接 k6 trace（若 scenario 帶 W3C trace context）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>VU 跑不上去 / runner CPU 滿</strong>：scenario 寫了重 JS 邏輯（big JSON parse、複雜 regex、crypto）— 把 setup-once 邏輯搬 <code>setup()</code>、不要每 VU iteration 重算</li>
<li><strong>Resource throttling 假象</strong>：runner 機器 CPU / network bandwidth / file descriptor 自身瓶頸、target service 還沒到 saturation — 換大機 / 多 runner / 看 runner 自身 saturation metric 排除</li>
<li><strong>Threshold 設過嚴 / CI 一直 red</strong>：threshold 抄 production SLO 不留 budget — staging tenant 跑 5-10 次抓 baseline distribution、threshold 設 baseline + buffer、不是 SLO 直接搬</li>
<li><strong>p95 看起來好但 user 抱怨慢</strong>：scenario endpoint mix 跟 production traffic shape 不符 — 補 production endpoint distribution、按 weight 配 scenario、跟 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 對齊</li>
<li><strong>Script logic 太重 / VU iteration 不穩</strong>：在 scenario 內做 token refresh / large payload 處理、iteration 時間漂移 — 用 <code>executor: ramping-arrival-rate</code> 鎖 RPS 而非 VU count、iteration 時間漂移由 engine 吸收</li>
<li><strong>結果無法回放 / 找不到 baseline</strong>：output 沒落 artifact、Grafana dashboard 沒存 time range — 每次 run 強制 <code>--out json</code> + tag scenario version + push 到 evidence package</li>
</ul>
<h2 id="操作成本">操作成本</h2>
<p>k6 的主要成本是 workload model 維護。腳本本身容易寫，真正的成本在 production endpoint mix、資料分布、tenant / region / user cohort、think time 與 peak shape 的持續校正。</p>
<p>Runner 成本會隨負載規模上升。單機 runner 適合小型 API baseline；跨 region、數十萬 RPS 或長時間 soak test 需要分散式 runner、網路成本、目標服務隔離與觀測儲存。</p>
<p>測試資料治理是高風險成本。Checkout、payment、order、email、notification 與 webhook 路徑都可能產生 side effect，因此 scenario 要明確定義 test tenant、idempotency key、mock boundary、cleanup 與 stop condition。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>k6 結果應回寫到 evidence package。最小欄位包括 scenario version、target environment、time range、VUs / arrival rate、threshold、p95 / p99、error rate、throughput、target service saturation metric、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>k6 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>k6 summary、JSON output、dashboard link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>test start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>Grafana / Prometheus / APM 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>scenario coverage、test data freshness</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>production similarity、runner capacity</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 endpoint、未模擬第三方、資料偏差</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 release gate 能判斷。k6 的 threshold pass 只是其中一個訊號；gate 還要看 target service 的 CPU、connection、DB latency、cache hit rate、queue lag 與 cloud cost。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>k6 目前在 09 案例庫中主要作為工具類承接點，案例主角仍是負載形狀與驗證節奏。它可回寫到 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 售票壓測</a> 的 pre-event load test 判讀、<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day readiness</a> 的 staged validation、<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 雙峰 workload</a> 的多模型壓測需求、<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech FIFA World Cup readiness</a> 的 54000 TPS @ 25ms p95 驗證、以及 <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 8x peak</a> 跨 100+ 微服務的獨立 threshold 設計。</p>
<p>這些案例提供的是負載形狀與工程節奏。k6 頁引用案例時，要把 case 轉成 workload model、ramp-up、threshold、runner 規模與 stop condition，並讓工具回到可替換的承載選項 — 例如 GR8 Tech 25ms p95 是 threshold pass / fail 的硬目標、Lyft 的「8x 是特定服務、不是全部 8x」要拆成 per-service scenario。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>官方：<a href="https://grafana.com/docs/k6/latest/">Grafana k6 documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.1 高併發下的 SQL 讀寫邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/</guid><description>&lt;p>高併發服務處理 SQL 的核心原則是共用資料庫 client、並讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 管理連線生命週期。當並發升高時、真正要控制的是連線數、交易範圍、查詢時間與下游壓力；每個 request 各自建立連線會放大握手、排隊與資源回收成本。&lt;/p>
&lt;p>本章是 01 模組的基礎章節之一、之後章節（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰&lt;/a>）都會回引這層的概念。跨模組對接 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後、讀者能夠：&lt;/p>
&lt;ol>
&lt;li>理解資料庫 client 為什麼應該共用&lt;/li>
&lt;li>分辨 query、exec、rows 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 的不同邊界&lt;/li>
&lt;li>了解連線池參數對高併發的影響&lt;/li>
&lt;li>設計多層 connection pool 架構（app + middleware + DB）&lt;/li>
&lt;li>識別 hot row / lock contention 並選擇對策&lt;/li>
&lt;li>用 read replica 擴 read traffic、注意 replication lag&lt;/li>
&lt;li>用 &lt;code>context&lt;/code> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 控制慢查詢&lt;/li>
&lt;li>判斷什麼情況該換 KV / 緩衝模式而非繼續硬擴 SQL&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察資料庫-client-通常代表連線池入口">【觀察】資料庫 client 通常代表連線池入口&lt;/h2>
&lt;p>多數後端語言的資料庫 client 都會包住連線池或連線管理能力。一般情況下、服務會在啟動時建立可重用的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> handle、讓 request handler、worker 或 service layer 共用它、並在需要時從池子裡取出可用連線。&lt;/p>
&lt;p>這種模型的好處是：&lt;/p>
&lt;ul>
&lt;li>呼叫端不用自己管理每個連線的生命週期&lt;/li>
&lt;li>多個 request 或 worker 可以同時發出資料庫操作&lt;/li>
&lt;li>連線回收與重用由 &lt;code>sql.DB&lt;/code> 處理&lt;/li>
&lt;/ul>
&lt;h2 id="判讀高併發需要有界連線">【判讀】高併發需要有界連線&lt;/h2>
&lt;p>高併發時的核心風險是把 application concurrency 誤解成 database concurrency。語言端的 thread、task、coroutine 或 goroutine 可能很容易建立、但資料庫有自己的容量上限；連線池只是把壓力從應用端平滑地送到下游、無法消滅壓力。&lt;/p></description><content:encoded><![CDATA[<p>高併發服務處理 SQL 的核心原則是共用資料庫 client、並讓 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 管理連線生命週期。當並發升高時、真正要控制的是連線數、交易範圍、查詢時間與下游壓力；每個 request 各自建立連線會放大握手、排隊與資源回收成本。</p>
<p>本章是 01 模組的基礎章節之一、之後章節（<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>）都會回引這層的概念。跨模組對接 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 跟 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後、讀者能夠：</p>
<ol>
<li>理解資料庫 client 為什麼應該共用</li>
<li>分辨 query、exec、rows 與 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 的不同邊界</li>
<li>了解連線池參數對高併發的影響</li>
<li>設計多層 connection pool 架構（app + middleware + DB）</li>
<li>識別 hot row / lock contention 並選擇對策</li>
<li>用 read replica 擴 read traffic、注意 replication lag</li>
<li>用 <code>context</code> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 控制慢查詢</li>
<li>判斷什麼情況該換 KV / 緩衝模式而非繼續硬擴 SQL</li>
</ol>
<hr>
<h2 id="觀察資料庫-client-通常代表連線池入口">【觀察】資料庫 client 通常代表連線池入口</h2>
<p>多數後端語言的資料庫 client 都會包住連線池或連線管理能力。一般情況下、服務會在啟動時建立可重用的 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> handle、讓 request handler、worker 或 service layer 共用它、並在需要時從池子裡取出可用連線。</p>
<p>這種模型的好處是：</p>
<ul>
<li>呼叫端不用自己管理每個連線的生命週期</li>
<li>多個 request 或 worker 可以同時發出資料庫操作</li>
<li>連線回收與重用由 <code>sql.DB</code> 處理</li>
</ul>
<h2 id="判讀高併發需要有界連線">【判讀】高併發需要有界連線</h2>
<p>高併發時的核心風險是把 application concurrency 誤解成 database concurrency。語言端的 thread、task、coroutine 或 goroutine 可能很容易建立、但資料庫有自己的容量上限；連線池只是把壓力從應用端平滑地送到下游、無法消滅壓力。</p>
<p>連線池調校的核心觀念是：</p>
<ul>
<li><code>SetMaxOpenConns</code> 太低、request 會在應用端排隊。</li>
<li><code>SetMaxOpenConns</code> 太高、可能把 DB 直接打滿。</li>
<li><code>SetMaxIdleConns</code> 影響高峰與尖峰之間的重用效率。</li>
<li><code>SetConnMaxLifetime</code> / <code>SetConnMaxIdleTime</code> 影響長連線與資源回收節奏。</li>
</ul>
<h3 id="第一個爆的通常是連線不是-cpu-或-disk">第一個爆的通常是連線、不是 CPU 或 disk</h3>
<p>SQL DB 在 surge 場景的 <em>first bottleneck</em> 不是 CPU、也不是 disk I/O、是 <em>連線數量</em>。原因：傳統 RDB（PostgreSQL、MySQL）每個連線吃記憶體 + 一個 process / thread、connection pool 上限通常 1K-5K。流量湧入時、application 想開更多連線、DB 直接拒絕（PostgreSQL：<code>FATAL: too many connections</code>）、看起來像 DB 故障、實際是連線數限制。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a> — NTT DOCOMO 串流平台選 DynamoDB 而非 RDB 的原因之一是「connection limit 在快速流量增加時變成 bottleneck」。DynamoDB 的 HTTP API 模型沒有 connection state、天然解決這個瓶頸。</p>
<p><strong>判讀順序</strong>：surge 期間 DB 看起來慢、先 <code>SHOW PROCESSLIST</code> / <code>pg_stat_activity</code> 看連線數、再看 CPU / disk。連線數已經滿、再加 CPU 沒用；要加 middleware pool（pgBouncer / ProxySQL）或換 HTTP-based DB。</p>
<h2 id="多層-connection-pool-架構">多層 Connection Pool 架構</h2>
<p>實務上 production-grade 服務的 connection pool 通常分三層：</p>
<h3 id="layer-1application-pool每個-instance-內">Layer 1：Application pool（每個 instance 內）</h3>
<ul>
<li>每個 application instance 維護自己的 driver-level pool</li>
<li>典型大小：30-50 connection / instance</li>
<li>工具：HikariCP（Java）、SQLAlchemy pool（Python）、<code>sql.DB</code>（Go）</li>
</ul>
<h3 id="layer-2middleware-pool共享層">Layer 2：Middleware pool（共享層）</h3>
<ul>
<li>PostgreSQL：<a href="https://www.pgbouncer.org/">pgBouncer</a>（最常見、transaction pooling）、<a href="https://github.com/postgresml/pgcat">PgCat</a>（rust、支援 sharding）</li>
<li>MySQL：<a href="https://proxysql.com/">ProxySQL</a>（query routing + pool）</li>
<li>為什麼需要：多個 application instance 同時打 DB、總 connection 數會爆</li>
<li>pgBouncer 把 1000 application connection mux 到 50 個 DB connection、應用感覺有 1000 connection、DB 只看到 50</li>
</ul>
<h3 id="layer-3database-端-max_connections">Layer 3：Database 端 max_connections</h3>
<ul>
<li>PostgreSQL default 100、實務常設 200-500</li>
<li>MySQL default 151、實務常設 1000-5000</li>
<li>每個 connection 吃記憶體（PG ~10MB、MySQL ~3MB）、設太高會 OOM</li>
</ul>
<p><strong>典型配置範例</strong>（中型網路服務）：</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">50 application instance × 30 connection (app pool)
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → pgBouncer transaction pool (4 instance × 100 connection)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  → PostgreSQL primary (max_connections = 200)</span></span></code></pre></div><p>1500 application connection mux 到 200 DB connection、4 倍 multiplexing。</p>
<p><strong>反模式</strong>：</p>
<ul>
<li>跳過 middleware pool、application 直連 DB</li>
<li>應用 instance 50 個 × 30 connection = 1500 connection、PostgreSQL 直接拒絕</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino case</a> — RDB connection limit 是 surge 場景的隱性 bottleneck、Lemino 選擇遷移到 DynamoDB 而不是擴 connection pool（因為 HTTP-based KV 沒這個問題）。</p>
<h3 id="query-反模式如何放大連線池壓力">Query 反模式如何放大連線池壓力</h3>
<p>連線池被占滿的根本原因不只是「連線數不夠」、還有「單一連線被占用的時間太長」。Query 反模式直接放大每筆 request 的連線占用時間：</p>
<ul>
<li><strong>N+1 query</strong> 讓一個 request 占用連線從 1 個 round trip 拉長到 N+1 個。同樣的 throughput、需要 N+1 倍的連線數來 sustain</li>
<li><strong>Long-running transaction</strong> 把一個連線從幾毫秒占用變成幾秒，相當於把連線池的有效容量除以幾百倍</li>
<li><strong>缺索引的 query</strong> 在熱表上跑 full scan、單筆 query 從 10ms 變成 1-5 秒、連線占用時間放大兩個數量級</li>
<li><strong><code>SELECT *</code> 載入大欄位</strong>：reader 在反序列化大物件期間連線一直 hold、不是 query 本身慢、是 serialization overhead 拉長占用</li>
</ul>
<p>這些反模式單獨看是「query 寫法問題」、但放到連線池語境就是「連線池容量被間接削減」。先用 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 的清單收回連線占用時間、再考慮加 <a href="/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 connection pooler</a> 中介層 — 順序顛倒會讓 pooler 治標不治本。</p>
<h2 id="策略讀取與寫入要分開看">【策略】讀取與寫入要分開看</h2>
<p>讀取的核心風險通常是慢查詢、掃描過大、N+1、熱點資料與連線被占住太久。寫入的核心風險則常常是 transaction 太大、衝突太高、鎖時間太長、重試邏輯不清楚。</p>
<h3 id="讀取">讀取</h3>
<ul>
<li>用索引支援常見查詢條件。</li>
<li>避免一次載入過多資料。</li>
<li>需要分頁時、先考慮游標或穩定排序。</li>
<li>熱讀資料可以在上層加 cache、同時保留資料庫作為正式狀態來源。</li>
</ul>
<h3 id="寫入">寫入</h3>
<ul>
<li>transaction 只包住真正需要一致性的範圍。</li>
<li>transaction 範圍只保留必要資料操作、外部 API 呼叫、使用者等待或長迴圈應放在交易外。</li>
<li>高衝突寫入要搭配重試、唯一鍵或明確去重策略。</li>
<li>需要高吞吐時、先評估批次化、分段處理與有界並發。</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 對 transaction 設計的深度討論。</p>
<h2 id="hot-row--lock-contention-識別與處理">Hot Row / Lock Contention 識別與處理</h2>
<p>當多個 request 同時想 update 同一筆資料、會在 DB 層出現 lock contention。這跟 KV 的 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a> 是同類問題、但 <em>機制不同</em>。</p>
<p><strong>典型 hot row 場景</strong>：</p>
<ul>
<li>inventory counter：所有用戶搶同一個 product 庫存</li>
<li>counter / metrics：實時計數器（view count、like count）</li>
<li>queue / job ledger：所有 worker 競爭同一個 job table</li>
<li>session：高頻 session 更新</li>
</ul>
<p><strong>識別訊號</strong>：</p>
<ul>
<li><code>pg_stat_activity</code> / SHOW PROCESSLIST 顯示大量 <code>lock waiting</code></li>
<li>整體 QPS 沒滿、但某些 endpoint p99 飆</li>
<li><code>pg_locks</code> / INFORMATION_SCHEMA.INNODB_LOCK_WAITS 有大量等待</li>
</ul>
<p><strong>對策</strong>：</p>
<p><strong>1. 分散熱點</strong>：</p>
<ul>
<li>counter shard：把 1 個 counter 拆成 N 個 sub-counter、寫入時隨機選一個、讀取時 SUM</li>
<li>例：<code>view_count_0</code> ~ <code>view_count_9</code> → 10 倍寫入吞吐</li>
<li>對應 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> 在 SQL DB 的對應做法</li>
</ul>
<p><strong>2. Asynchronous batching</strong>：</p>
<ul>
<li>不要每次點擊就 update counter、先進 in-memory buffer、定期 flush</li>
<li>應用層 Redis INCR + 定期同步回 SQL</li>
</ul>
<p><strong>3. Optimistic concurrency control</strong>：</p>
<ul>
<li>用 <code>WHERE version = ?</code> 樂觀鎖、避免 SELECT FOR UPDATE</li>
<li>衝突時應用層 retry</li>
</ul>
<p><strong>4. 換 KV / cache</strong>：</p>
<ul>
<li>counter workload 本來就不適合 SQL transaction</li>
<li>用 Redis INCR、DynamoDB 的 atomic counter</li>
</ul>
<p><strong>5. Queue + worker 序列化</strong>：</p>
<ul>
<li>把搶資源的 request 排隊、worker 序列化處理</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 案例</a> — 售票把 inventory 搶購塞進 DynamoDB queue、legacy server 慢慢消費、避免 SQL hot row</li>
</ul>
<h2 id="read-replica-scaling">Read Replica Scaling</h2>
<p>當 read traffic 超過 primary 吞吐、用 read replica 擴 read。</p>
<p><strong>Read replica 機制</strong>：</p>
<ul>
<li>PostgreSQL：streaming replication（async / sync）</li>
<li>MySQL：async replication（binlog）</li>
<li>Aurora：storage-level replication（lag 10-30ms）</li>
</ul>
<p><strong>Routing 策略</strong>：</p>
<p><strong>1. Read / write split（application-level）</strong>：</p>
<ul>
<li>應用層判斷 query 類型、寫走 primary、讀走 replica</li>
<li>工具：ProxySQL（MySQL）、application 自管</li>
</ul>
<p><strong>2. Routing 自動化（middleware）</strong>：</p>
<ul>
<li>pgBouncer + 路由規則</li>
<li>HAProxy + health check</li>
</ul>
<p><strong>3. Stale read 容忍策略</strong>：</p>
<ul>
<li>「能容忍秒級 stale」的 read → replica（用戶 profile、報表）</li>
<li>「不能 stale」的 read → primary（剛寫入後的查詢、餘額確認）</li>
<li>read-after-write consistency：用 session token 標記「剛寫過」、N 秒內讀走 primary</li>
</ul>
<p><strong>Replication lag 監控</strong>：</p>
<ul>
<li>PostgreSQL：<code>pg_stat_replication.replay_lag</code></li>
<li>MySQL：<code>SHOW SLAVE STATUS\G</code> 的 <code>Seconds_Behind_Master</code></li>
<li>Aurora：CloudWatch <code>AuroraReplicaLag</code></li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> — replication lag 從 30 秒降到 10-30ms、是切換到 Aurora 的關鍵改善</li>
</ul>
<p><strong>注意事項</strong>：</p>
<ul>
<li>replica 數量不是無限、Aurora 最多 15 個、PostgreSQL 通常 3-5 個（chain replication 更多但複雜）</li>
<li>跨 region replica 通常 async、不能保證 read-after-write</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> Super Bowl 5-10x peak、需要動態加 replica</li>
</ul>
<h3 id="儲存層-replication-vs-compute-層-replication">儲存層 replication vs compute 層 replication</h3>
<p>Aurora / Cosmos DB / Spanner 的 replication 跟傳統 PostgreSQL streaming replication 是兩種本質不同的設計、決定 read replica 怎麼擴、replication lag 落在什麼量級、容量規劃要顧哪些瓶頸。</p>
<p><strong>傳統 RDB（compute 層 replication）</strong>：</p>
<ul>
<li>primary 寫入後、把 WAL / binlog 流到 replica</li>
<li>replica 自己 replay log、消耗 CPU 跟 disk</li>
<li>primary 寫入量大、replica 跟不上、replication lag 飆</li>
<li>加 replica 增加 primary 的 <em>replication 負擔</em>、不能無限加</li>
</ul>
<p><strong>Aurora / Cosmos DB（storage 層 replication）</strong>：</p>
<ul>
<li>compute 跟 storage 分離、storage 是分散式 log-based</li>
<li>replication 在 <em>storage 層</em> 處理、不經過 compute</li>
<li>replica 不用自己 replay、直接讀同一份 storage</li>
<li>加 read replica 不增加 primary 寫入負擔</li>
<li>replication lag 從 30 秒級降到 10-30ms（Aurora）</li>
</ul>
<p><strong>為什麼這層差異反映在應用層設計</strong>：compute 層 replication 的 replication lag 通常在秒級、應用層必須處理「剛寫的資料 N 秒內讀不到」的情境 — 常見補丁是 read-after-write consistency（session token 標記「剛寫過」、N 秒內走 primary）、cache invalidation 延遲、或刻意走 primary 的關鍵查詢路徑。Storage 層 replication 的 lag 在毫秒級、這些補丁多半不需要、read 可以幾乎無條件走 replica。對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 從 30 秒到 10-30ms 不只是「快」、是讓整個應用層 cache invalidation 跟 session routing 邏輯大幅簡化。對應 <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 consolidation</a> — Aurora 75% performance improvement 主要來自 storage layer 設計、不是 CPU 改善。</p>
<p><strong>選型含義</strong>：如果應用層 <em>依賴 read-after-write</em>（餘額確認、剛寫的查詢、session 狀態）、storage 層 replication 比 compute 層 replication 大幅簡化設計。代價是 vendor lock-in 加深、應用層綁定特定雲商。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a> 跟 Aurora 是同類設計（log-structured 分散式 storage）、選哪家看 application 已在哪個 cloud、技術哲學一致。Sharding 觸發點（managed DB 容量上限）跟業務一致性需求決定 sharding 粒度的討論、見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 Sharding 粒度跟業務一致性需求</a>。</p>
<h2 id="執行查詢與-rows-的生命週期要收乾淨">【執行】查詢與 rows 的生命週期要收乾淨</h2>
<p>查詢回傳 rows 後、呼叫端要負責把它關掉、並檢查迭代錯誤。這不只是記憶體管理問題、也會影響連線何時能回到池子裡。</p>
<p>典型模式是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">rows</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">QueryContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;SELECT id, name FROM users WHERE status = ?&#34;</span><span class="p">,</span> <span class="nx">status</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">defer</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</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">for</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Next</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">var</span> <span class="nx">id</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kd">var</span> <span class="nx">name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">id</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">name</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Err</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="策略慢查詢要靠-timeout-與上層限流處理">【策略】慢查詢要靠 timeout 與上層限流處理</h2>
<p>在高併發服務裡、database timeout 應由 request timeout、client timeout 與資料庫 timeout 共同定義。語言端需要能把取消、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 或 timeout 往資料庫 client 傳遞、讓慢查詢在合理時間內釋放資源。</p>
<p>如果下游開始變慢、通常要搭配：</p>
<ul>
<li>request-level timeout</li>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 或 semaphore</li>
<li><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 長度限制</li>
<li>降級或拒絕策略</li>
</ul>
<p>這樣做的目標是避免應用自己堆出大量等待中的工作、最後把問題放大成整個服務卡死。</p>
<h2 id="什麼時候該換-kv--緩衝模式而非繼續硬擴-sql">什麼時候該換 KV / 緩衝模式而非繼續硬擴 SQL</h2>
<p>SQL 的 transactional 模型有結構性限制、超過某個規模硬擴 SQL 不如換工具。</p>
<p><strong>換工具的訊號</strong>：</p>
<ol>
<li>
<p><strong>Connection saturate 但 CPU / RAM 還閒</strong>：connection 是 SQL 的早期 bottleneck。對應 <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> — RDB connection limit 是 surge 場景的瓶頸、換 DynamoDB（HTTP-based、無 connection 概念）解決。</p>
</li>
<li>
<p><strong>Hot row contention 無法分散</strong>：應用層改不了 schema、無法把 counter shard、SQL 就是 contention 源頭。換 Redis atomic counter / DynamoDB atomic update。</p>
</li>
<li>
<p><strong>Write throughput &gt; 50K WPS 單機</strong>：sharding 工程成本變高、不如換 KV 或分散式 SQL。詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 或 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。</p>
</li>
<li>
<p><strong>Flash-sale spiky workload</strong>：用 SQL 接搶購、connection 跟 lock 都會爆。對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 用 DynamoDB 當 durable queue、legacy SQL 慢慢消費。</p>
</li>
<li>
<p><strong>跨 region 強一致 OLTP</strong>：傳統 PostgreSQL / MySQL 跨 region 是 async、滿足不了強一致。換 Spanner / Aurora DSQL / CockroachDB（<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11</a>）。</p>
</li>
</ol>
<p>不要因為「現在 SQL 慢」就跳結論換 NoSQL — 先確認問題是 <em>結構性的</em>（connection、contention、跨 region）、不只是 <em>調校問題</em>（index、query、cache）。</p>
<h2 id="延伸語言端的責任是邊界">【延伸】語言端的責任是邊界</h2>
<p>這一章不討論 PostgreSQL、MySQL、SQLite 的語法差異、也不討論 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> 工具本身。語言端需要掌握的是：怎麼共用 database client、怎麼控制並發、怎麼縮小 transaction、怎麼把 timeout 和取消傳下去。</p>
<p>具體 schema、index、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 與 migration 寫法、會放在這個模組的其他資料庫教材中。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>高併發場景重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day 2025</a></td>
          <td>DynamoDB 1.51 億 RPS + Aurora 5000 億 txn、可預期峰值的 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">dogfood baseline</a>（vendor 自家 production-critical workload 是 selection signal）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>1M ops/min、200 個獨立 cluster、replication lag 30s → 10-30ms</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered Aurora</a></td>
          <td>4000 TPS、7 個受監管市場、各自獨立 cluster</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 Aurora</a></td>
          <td>DB 統一後 +75% 效能、storage / compute 分離釋放 read replica</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>Super Bowl 5-10x peak、Aurora MySQL + read replica scaling</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>RDB connection limit 是 surge 瓶頸、改用 DynamoDB</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a></td>
          <td>5 億 txn/年、storage / compute 分離跟 Aurora 同類設計</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a> 是高併發章節的 <em>上限參考點</em>：Amazon 自家 Prime Day 在 24 小時內、DynamoDB 服務 1.51 億 RPS 毫秒級回應、Aurora 處理 5000 億次 transaction。這份數字的意義不是「要達到這個量級」、而是給定 <em>可預期峰值</em> 跟 <em>無限預算</em> 時、AWS 自家服務的設計上限長這樣。讀本章其他內部 baseline（connection pool、replica lag、isolation level）時、要記得最終物理上限遠高於大部分服務日常會碰到的水位。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>資料庫高併發邊界會受語言 runtime 影響。Thread-based runtime 要管理 thread pool 與 connection pool 的比例；async runtime 要確認 database driver 是否真正非阻塞（很多老 driver 只是包了 sync 在 thread pool 上、會吃 thread limit）；輕量 task runtime（Go、Erlang）要限制同時查詢數量、避免把大量 task 轉成下游連線壓力。強型別語言可以用型別保護 row mapping 與錯誤分類；動態語言則需要用 migration、runtime validation、<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test 與 fixture 保護 schema 邊界。</p>
<h2 id="小結">小結</h2>
<p>高併發下處理 SQL 的核心原則：</p>
<ol>
<li><strong>database client 共用</strong>、不要每 request 新建</li>
<li><strong>連線池可控</strong> — 三層架構（app pool + middleware + DB max_connections）</li>
<li><strong>transaction 要短</strong> — 詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a></li>
<li><strong>rows 要關</strong>、避免連線被占住</li>
<li><strong>timeout 要傳遞</strong> — 從 request 一路到 DB</li>
<li><strong>Hot row 要識別</strong> — counter shard、optimistic concurrency、async batching、或換 KV</li>
<li><strong>Read replica 要會用</strong> — 但注意 lag、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 容忍度</li>
<li><strong>下游壓力要限流</strong> — request timeout、worker pool、queue 長度、降級拒絕</li>
<li><strong>知道什麼時候換工具</strong> — connection saturation、hot contention、flash-sale、跨 region 強一致都是 SQL 結構性限制的訊號</li>
</ol>
<p>應用端並發可以很多、但資料庫連線必須受控、這兩者的邊界要分開管理。</p>
<h2 id="讀峰值數字的工程細節">讀「峰值」數字的工程細節</h2>
<p>容量規劃時看到「100 萬 ops/分鐘」、「150 萬 RPS」這類數字、要拆三個維度看、否則容量規劃會錯位。</p>
<h3 id="容量數字的三個口徑">容量數字的三個口徑</h3>
<table>
  <thead>
      <tr>
          <th>口徑</th>
          <th>含義</th>
          <th>用於規劃</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>最大瞬時</td>
          <td>某一秒的最高峰（單秒）</td>
          <td>不能拿這個訂 baseline、是 outlier</td>
      </tr>
      <tr>
          <td>99 百分位平均</td>
          <td>99% 時間在這個水位以下</td>
          <td>訂 capacity 上限的依據</td>
      </tr>
      <tr>
          <td>常態流量</td>
          <td>平均的日常水位</td>
          <td>訂 cost baseline、auto-scaling 起點</td>
      </tr>
  </tbody>
</table>
<p><strong>最大瞬時</strong> 是觀測得到的最高峰值、通常是年度某秒、不能拿來訂 baseline。在 Grafana / CloudWatch / Datadog 上看 <code>max</code> 指標就是這個數字 — 用來知道系統 <em>曾經</em> 撐過多少、不是 <em>日常</em> 要撐多少。</p>
<p><strong>99 百分位平均</strong> 是 capacity 規劃的主要依據。在監控工具看的是 <code>p99</code> 隨時間的平均值（rolling 30 天或 90 天）— 代表 99% 的時間流量低於這個水位。Auto-scaling 上限通常訂在這個值的 1.5-2 倍、確保 99% 時間有足夠 headroom。</p>
<p><strong>常態流量</strong> 是 average / median、訂 cost baseline 跟 auto-scaling 的下限。在 PaaS（Aurora Serverless、Cosmos DB serverless）這是「最低保留容量」的依據；在 IaaS 是「永遠開著的 instance 數量」。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 揭露這個議題：「9000 萬 reads / 秒」通常是年度峰值最高一秒、不是平均。讀案例時要區分這三個口徑、否則容量規劃會錯位。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 「100 萬 ops/分鐘」≈ 17K ops/秒、跨 200 個獨立 cluster 平均下來每 DB 約 80 ops/秒。讀峰值要看 <em>分散到多少 shard</em>、不只看總數。</p>
<h3 id="延遲改善要看-percentile不是平均">延遲改善要看 percentile、不是平均</h3>
<p>「延遲降 90%」這類敘述要追問：是 p50 還是 p99？兩者改善幅度通常差很多、平均值會掩蓋尾巴問題。</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%">9.C20 Zomato</a> — 「90% 延遲降」實際可能是 p50、p99 / p999 改善幅度通常較小。判讀重點：用戶體驗主要受 <em>p99 / p999</em> 影響、不是 p50。看到「平均 50ms 降到 5ms」要追問「p99 從多少降到多少」、否則可能用戶感受沒改善。</p>
<p>延遲監控的必要 percentile：p50、p95、p99、p99.9。p99.9 對 1000 個 request 才偵測一次、但通常代表系統最差表現、是 SLO breach 的早期訊號。</p>
<h2 id="headroom-budget事件型-vs-突發型峰值">Headroom budget：事件型 vs 突發型峰值</h2>
<p>Headroom budget 是 <em>提前預留的容量空間</em>、給可預期或不可預期的峰值用。讀「Super Bowl +50% no sweat」這種敘述、工程意義是團隊事前預留了 headroom、不是 vendor 神奇。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — Super Bowl 是已知事件、+50% 是歷史經驗、所以可以提前 pre-scale。整個 system headroom 預留至少 50%、加上 read replica 動態加減、才能讓 50% 增幅變成「不流汗」。</p>
<p>兩種峰值的 headroom budget 規劃完全不同：</p>
<p><strong>事件型峰值</strong>（已知時間 + 已知幅度）：</p>
<ul>
<li>例：Super Bowl、Black Friday、票券開賣、財報日</li>
<li>規劃做法：歷史 peak × 預期成長 × headroom（通常 1.5-2x）= baseline、事件前 scheduled scale-up</li>
<li>headroom 預算可以較低（20-30%）、因為峰值可預測、可在事件前測試</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
</ul>
<p><strong>突發型峰值</strong>（未知時間或未知幅度）：</p>
<ul>
<li>例：突發新聞、KOL 推廣、競爭對手出包導致流量湧入、病毒式擴散</li>
<li>規劃做法：常態 baseline 預留高 headroom（50-100%）、加 auto-scaling 跟動態 capacity</li>
<li>headroom 預算要高、因為事故發生前沒時間 scale</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech AI 預測式擴容</a></li>
</ul>
<p>判讀重點：事件型 headroom 適合可預測峰值、突發型 headroom 適合不可預測峰值；兩者預算邏輯不同。把事件型 headroom 套用在突發型場景、突發事件發生時容量會不足；把突發型的高 headroom 套用在事件型、會付大量浪費成本。</p>
<h2 id="讀寫峰值錯位dual-peak-workload">讀寫峰值錯位：dual peak workload</h2>
<p>部分業務有 <em>讀峰值跟寫峰值不同時段</em> 的特性、容量規劃要按 <em>peak 之和</em> 而非 <em>單一 peak</em>。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」。比賽進行時讀爆量（用戶看餘額、看下注狀態）、比賽結束 payout 時寫爆量（賠付寫進帳本）、兩個 peak 錯位。</p>
<p>容量規劃含義：</p>
<ul>
<li>不能只規劃「讀 peak + 寫常態」或「寫 peak + 讀常態」</li>
<li>要規劃「讀 peak 跟寫 peak 各自的容量」、即使不同時發生、底層 DB 都要撐</li>
<li>read replica 動態增減可以平滑讀 peak、但寫 peak 要靠 primary capacity 撐住</li>
</ul>
<p><strong>類似 dual peak 業務</strong>：</p>
<ul>
<li>體育博彩：比賽中讀、payout 時寫（DraftKings）</li>
<li>票券：開賣前 30 分鐘讀爆量（用戶看座位）、開賣瞬間寫爆量（搶票）</li>
<li>電商促銷：促銷前讀爆量（用戶看價格）、促銷瞬間寫爆量（下單）</li>
<li>股票交易：開盤前讀爆量（看開盤價）、開盤瞬間寫爆量（送單）</li>
</ul>
<p>判讀重點：dual peak workload 是業務 <em>天然</em> 特性、不是異常。容量規劃要識別這層、否則尖峰時段會踩到沒預期的瓶頸。</p>
<h2 id="關鍵路徑切分低頻流量保護">關鍵路徑切分：低頻流量保護</h2>
<p>當系統有「高頻流量（如選位、瀏覽）」跟「低頻但關鍵流量（如付款、結算）」共存時、必須切分、否則高頻流量會塞爆低頻路徑、讓低頻關鍵業務無法完成。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 拓元把 Payment EC2 拉出來、直連傳統金流 server、不放在搶票流量會打到的 ELB / DB 後面。讓「選位 + 下單」的高頻流量塞爆時、「付款」的低頻流量仍能跑。</p>
<p><strong>切分策略</strong>：</p>
<ul>
<li><strong>資料路徑切分</strong>：高頻 query 走 DynamoDB / read replica、低頻關鍵 query 走 primary</li>
<li><strong>連線池切分</strong>：高頻 service 跟低頻 service 用不同 connection pool、避免高頻吃光連線</li>
<li><strong>runtime 切分</strong>：低頻關鍵 service 部署到獨立 instance、不跟高頻共用 CPU / memory</li>
<li><strong>限流切分</strong>：高頻 endpoint 設高限流、低頻關鍵 endpoint 設保護性低限流（避免 cascading failure）</li>
</ul>
<p>判讀重點：切分前要先盤「哪些流量是業務關鍵但量小」、這些路徑要事先保護、不能等爆了再分開。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a></li>
<li>上游：<a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a>（connection saturation 常因 N+1 / long transaction 放大、先檢查 query 寫法）</li>
<li>平行：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（SQL 不夠用時的替代）/ <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（換 DB engine 的決策跟流程）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>、<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>（hot row 是不可分散瓶頸的 application 層表現）</li>
<li>Vendor：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</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></li>
<li>規模成長路線下一站 → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>（連線池 / replica 擴完後、進入應用層快取設計）</li>
<li>MongoDB connection storm 深入：<a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">MongoDB connection 管理與 cache 層</a> / <a href="/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/" data-link-title="MongoDB Replica Set Read Preference：DB 層 causal session vs cache 層 freshness token" data-link-desc="MongoDB read preference 五擇一 &#43; read concern &#43; causal consistency session 機制；DB 層機制解 cluster 內 read-your-own-write、cache 層 freshness token 解跨層 read-after-write、大規模 OLTP 必須兩層合用">replica set read preference</a></li>
<li>Aurora read replica 擴展：<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 / 合規）">Aurora read replica scaling</a>（reader endpoint / lag 治理）</li>
<li>Freshness token 卡片：<a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a>（read-after-write 保證選項）</li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/</guid><description>&lt;p>PostgreSQL 是 backend 預設關聯式資料庫的安全選擇。生態完整、SQL 功能豐富、MVCC 跟 transaction 模型穩定、新版本仍積極演進（pg17 加入 JSON_TABLE、平行 vacuum；pg18 加入 io_uring async）。Aurora（AWS managed）、CockroachDB、Aurora DSQL（2024-12 preview / 2025-05 GA）、Spanner（2024 PostgreSQL dialect）都把 PostgreSQL wire protocol 當作相容標的 — 它是 SQL DB 世界的 lingua franca。&lt;/p>
&lt;h2 id="教學路線sql-baseline-與交易演進">教學路線：SQL baseline 與交易演進&lt;/h2>
&lt;p>PostgreSQL 服務頁的教學目標是建立 SQL baseline。讀者讀完後要能用 PostgreSQL 理解 transaction、schema evolution、query boundary、connection pressure 與 managed / distributed SQL 的比較基準。&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>SQL baseline&lt;/td>
 &lt;td>PostgreSQL 為什麼常作為 OLTP 預設比較基準&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量邊界&lt;/td>
 &lt;td>connection、write throughput、replica、storage 如何限制服務&lt;/td>
 &lt;td>容量特性、容量規劃要點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交易與查詢&lt;/td>
 &lt;td>複雜 SQL、JSONB、GIS、全文檢索如何影響資料模型&lt;/td>
 &lt;td>適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>演進與維護&lt;/td>
 &lt;td>vacuum、partition、index、replication 如何成為長期責任&lt;/td>
 &lt;td>容量規劃要點、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時轉 Aurora、CockroachDB、Spanner、DynamoDB 或 OLAP&lt;/td>
 &lt;td>不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位oltp-預設sql-工程深度">定位：OLTP 預設、SQL 工程深度&lt;/h2>
&lt;p>PostgreSQL 跟 MySQL 是兩大 SQL OLTP 主流、但設計取捨明顯不同：&lt;/p>
&lt;ul>
&lt;li>PostgreSQL 偏 &lt;em>特性深度&lt;/em> — JSON、GIS、full-text search、partial index、CTE、window function 都成熟&lt;/li>
&lt;li>MySQL 偏 &lt;em>簡單 query 效能 + 分片生態&lt;/em> — Vitess / PlanetScale 提供超大規模 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>選 PostgreSQL 的核心訴求：需要進階 SQL 特性、需要長期 schema evolution 彈性、信任 community-driven 演進、想避免單一 vendor lock-in（PostgreSQL 是 open source、可跨雲 / on-prem）。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>PostgreSQL 沒有「vendor 給的容量數字」、要靠 instance 配置 + tuning 推估。但有幾個工程上限要知道：&lt;/p>
&lt;p>&lt;strong>單一 primary 寫吞吐&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一般 m5.4xlarge 級 instance：5K-10K WPS（依 schema、index、commit fsync）&lt;/li>
&lt;li>高階 r6i.16xlarge + io2 storage：30K-50K WPS&lt;/li>
&lt;li>超過這個級別 → 應用層 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a> 或換 Aurora / Spanner&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Connection 上限&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>預設 100 connection、每個 connection ~10MB RAM&lt;/li>
&lt;li>1000+ connection 必須 pgBouncer / PgCat 共享 pool&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">9.C29 Lemino case&lt;/a> — RDB connection limit 是 surge 場景的隱性 bottleneck&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Read replica&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL 是 backend 預設關聯式資料庫的安全選擇。生態完整、SQL 功能豐富、MVCC 跟 transaction 模型穩定、新版本仍積極演進（pg17 加入 JSON_TABLE、平行 vacuum；pg18 加入 io_uring async）。Aurora（AWS managed）、CockroachDB、Aurora DSQL（2024-12 preview / 2025-05 GA）、Spanner（2024 PostgreSQL dialect）都把 PostgreSQL wire protocol 當作相容標的 — 它是 SQL DB 世界的 lingua franca。</p>
<h2 id="教學路線sql-baseline-與交易演進">教學路線：SQL baseline 與交易演進</h2>
<p>PostgreSQL 服務頁的教學目標是建立 SQL baseline。讀者讀完後要能用 PostgreSQL 理解 transaction、schema evolution、query boundary、connection pressure 與 managed / distributed SQL 的比較基準。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL baseline</td>
          <td>PostgreSQL 為什麼常作為 OLTP 預設比較基準</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>容量邊界</td>
          <td>connection、write throughput、replica、storage 如何限制服務</td>
          <td>容量特性、容量規劃要點</td>
      </tr>
      <tr>
          <td>交易與查詢</td>
          <td>複雜 SQL、JSONB、GIS、全文檢索如何影響資料模型</td>
          <td>適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>演進與維護</td>
          <td>vacuum、partition、index、replication 如何成為長期責任</td>
          <td>容量規劃要點、常見陷阱</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時轉 Aurora、CockroachDB、Spanner、DynamoDB 或 OLAP</td>
          <td>不適用場景、跟其他 vendor 的取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="定位oltp-預設sql-工程深度">定位：OLTP 預設、SQL 工程深度</h2>
<p>PostgreSQL 跟 MySQL 是兩大 SQL OLTP 主流、但設計取捨明顯不同：</p>
<ul>
<li>PostgreSQL 偏 <em>特性深度</em> — JSON、GIS、full-text search、partial index、CTE、window function 都成熟</li>
<li>MySQL 偏 <em>簡單 query 效能 + 分片生態</em> — Vitess / PlanetScale 提供超大規模 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a></li>
</ul>
<p>選 PostgreSQL 的核心訴求：需要進階 SQL 特性、需要長期 schema evolution 彈性、信任 community-driven 演進、想避免單一 vendor lock-in（PostgreSQL 是 open source、可跨雲 / on-prem）。</p>
<h2 id="容量特性">容量特性</h2>
<p>PostgreSQL 沒有「vendor 給的容量數字」、要靠 instance 配置 + tuning 推估。但有幾個工程上限要知道：</p>
<p><strong>單一 primary 寫吞吐</strong>：</p>
<ul>
<li>一般 m5.4xlarge 級 instance：5K-10K WPS（依 schema、index、commit fsync）</li>
<li>高階 r6i.16xlarge + io2 storage：30K-50K WPS</li>
<li>超過這個級別 → 應用層 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 或換 Aurora / Spanner</li>
</ul>
<p><strong>Connection 上限</strong>：</p>
<ul>
<li>預設 100 connection、每個 connection ~10MB RAM</li>
<li>1000+ connection 必須 pgBouncer / PgCat 共享 pool</li>
<li>對應 <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 case</a> — RDB connection limit 是 surge 場景的隱性 bottleneck</li>
</ul>
<p><strong>Read replica</strong>：</p>
<ul>
<li>streaming replication：1 個 primary + 多個 standby（async / sync）</li>
<li>跨 AZ replication lag 通常 &lt; 100ms、跨 region 可能秒級</li>
<li>跟 Aurora 比、自管 PostgreSQL replication lag 較大</li>
</ul>
<p><strong>Storage 上限</strong>：</p>
<ul>
<li>單一 table 32 TB（PostgreSQL 設計上限）</li>
<li>實務上單表超過 1 TB 開始有 vacuum / index 問題、建議 partition</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. 多用途 OLTP、複雜查詢</strong>：</p>
<ul>
<li>複雜 JOIN、CTE、window function、subquery</li>
<li>訂單系統、會員系統、訂閱方案、權限 RBAC</li>
<li>需要 strong consistency + ACID transaction</li>
</ul>
<p><strong>2. JSON / 半結構化資料</strong>：</p>
<ul>
<li>JSONB column 支援 indexing、partial query</li>
<li>比 MongoDB 適合 <em>主要結構化 + 部分 JSON</em> workload</li>
<li>不適合主要 document workload（用 MongoDB / Cosmos DB）</li>
</ul>
<p><strong>3. 地理 / 全文檢索</strong>：</p>
<ul>
<li>PostGIS 是業界標準 GIS extension</li>
<li>全文檢索（ts_vector）對中等規模夠用、超大規模用 Elasticsearch</li>
</ul>
<p><strong>4. 進階特性需求</strong>：</p>
<ul>
<li>partial index（WHERE 條件下才建 index）</li>
<li>exclusion constraints（避免 booking 重疊）</li>
<li>range types（時間 / 數字範圍）</li>
<li>logical decoding / CDC（Debezium、pgcapture）</li>
<li>foreign data wrapper（query 跨 DB）</li>
</ul>
<p><strong>5. 跨雲 / on-prem 部署</strong>：</p>
<ul>
<li>不想 vendor lock-in</li>
<li>可用 Patroni / Stolon / pg_auto_failover 做 HA</li>
<li>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 CockroachDB / Aurora DSQL 比較段</li>
</ul>
<p><strong>6. 中小規模高峰場景</strong>：</p>
<ul>
<li>流量 &lt; 10K WPS 級別、PostgreSQL 自管或 RDS 通常夠</li>
<li>流量更高、考慮 Aurora（同 wire protocol、storage 升級）</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 極高寫入吞吐（單機 &gt; 50K WPS）</strong>：</p>
<ul>
<li>必須進入 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 或分散式 SQL</li>
<li>替代：CockroachDB、TiDB、Spanner、應用層 sharding</li>
</ul>
<p><strong>2. 全球 multi-region active-active write</strong>：</p>
<ul>
<li>PostgreSQL 是 single primary、不支援 multi-region active-active</li>
<li>替代：Aurora DSQL、Spanner、CockroachDB multi-region</li>
</ul>
<p><strong>3. KV 簡單查詢 + sub-10ms p99</strong>：</p>
<ul>
<li>PostgreSQL connection 開銷 + parsing + planning 已經 1-3ms</li>
<li>KV-pattern workload 用 DynamoDB / Redis / Cosmos DB 更便宜更快</li>
</ul>
<p><strong>4. 大規模 OLAP</strong>：</p>
<ul>
<li>PostgreSQL 定位在 OLTP，analytics workload 交給 OLAP 系統</li>
<li>大數據分析用 ClickHouse / BigQuery / Snowflake / Redshift / Synapse</li>
</ul>
<p><strong>5. 連線量極大 SaaS（每個用戶一個 connection）</strong>：</p>
<ul>
<li>即使有 pgBouncer、超大連線量仍是 PostgreSQL 結構性限制</li>
<li>對應 <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> 案例 — 流量上升 connection 爆是換 DynamoDB 的主因</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs MySQL</strong>：</p>
<ul>
<li>PostgreSQL：SQL 特性深、JSON / GIS / window 完整、replication 較簡單但 lag 較大</li>
<li>MySQL：簡單 query 效能好、replication 機制成熟、Vitess 分片生態強</li>
<li>選 PostgreSQL：需要進階 SQL、複雜 query、JSON workload</li>
<li>選 MySQL：高併發簡單 query、需要 sharding、已用 MySQL 生態</li>
</ul>
<p><strong>vs Aurora（同 PostgreSQL wire protocol）</strong>：</p>
<ul>
<li>PostgreSQL：自管 / RDS、特性接近 upstream、跨雲可用</li>
<li>Aurora：AWS managed、storage / compute 分離、更多 read replica</li>
<li>選 PostgreSQL：跨雲、想最新特性、預算敏感</li>
<li>選 Aurora：AWS 生態、需要更快 failover + 更多 read replica</li>
<li>詳見 <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 page</a></li>
</ul>
<p><strong>vs CockroachDB（PostgreSQL wire protocol 相容）</strong>：</p>
<ul>
<li>PostgreSQL：single-primary OLTP、SQL 特性完整</li>
<li>CockroachDB：multi-region 強一致 SQL、PostgreSQL wire 相容但部分特性缺</li>
<li>選 PostgreSQL：single-region 或 read replica 跨 region 夠</li>
<li>選 CockroachDB：必須 multi-region active-active write</li>
<li>詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
</ul>
<p><strong>vs Spanner / Aurora DSQL（全球分散式 SQL）</strong>：</p>
<ul>
<li>PostgreSQL：傳統設計、跨 region 是 async replication</li>
<li>Spanner / Aurora DSQL：全球線性化、跨 region 強一致</li>
<li>選 PostgreSQL：90% 場景夠用、便宜、容易</li>
<li>選 Spanner / Aurora DSQL：金融交易、ticketing inventory、必須全球強一致</li>
</ul>
<p><strong>vs DynamoDB</strong>：</p>
<ul>
<li>詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 的 connection model 對比段</li>
</ul>
<p><strong>vs Neon（PostgreSQL serverless）</strong>：</p>
<ul>
<li>PostgreSQL：standard、自管或 RDS</li>
<li>Neon：branch-based、scale-to-zero、適合 dev / preview environment</li>
<li>選 Neon：dev / preview、稀疏 workload、CI 用</li>
<li>選 PostgreSQL：production sustained workload</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. Connection pool 必須有</strong>：</p>
<ul>
<li>直接連 1000+ connection 會壓垮 PostgreSQL</li>
<li>pgBouncer（最簡單、transaction pooling）</li>
<li>PgCat（rust 寫的進階替代、支援 sharding）</li>
<li>application 層 pool（HikariCP、SQLAlchemy pool）</li>
<li>通常組合使用：application pool 30-50 connection × 多 instance → pgBouncer 共享 → PostgreSQL 200 connection</li>
<li>對應 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a></li>
</ul>
<p><strong>2. Replication 配置</strong>：</p>
<ul>
<li>streaming replication：async / sync / <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a></li>
<li>跨 AZ async：lag 通常 &lt; 100ms、failover 1-2 分鐘</li>
<li>跨 AZ sync：lag 接近 0、但寫入要等 standby ack、會降寫吞吐</li>
<li>跨 region 通常 async</li>
<li>HA 工具：Patroni（最常見）、pg_auto_failover、Stolon</li>
</ul>
<p><strong>3. Vacuum 跟 bloat 治理</strong>：</p>
<ul>
<li>PostgreSQL MVCC 會留下 dead tuples、必須 vacuum</li>
<li>autovacuum 配置：throttle 大表、避免在 peak 跑</li>
<li>bloat 監控：pg_stat_user_tables 看 dead_tup ratio</li>
<li>大表 vacuum 可能要 hours、影響 maintenance window</li>
</ul>
<p><strong>4. 大表 partitioning</strong>：</p>
<ul>
<li>單表 &gt; 1 TB 建議 partition（按時間、按 tenant）</li>
<li>partition pruning 讓 query 只掃需要的 partition</li>
<li>partition 限制：cross-partition unique constraint、跨 partition join 較慢</li>
</ul>
<p><strong>5. Index 策略</strong>：</p>
<ul>
<li>預設 B-tree、適合大多數 query</li>
<li>partial index 對 boolean / status column 特別有用</li>
<li>GIN / GiST 對 JSON / full-text / GIS</li>
<li>index 太多會拖累寫入、定期 review 未用 index（pg_stat_user_indexes）</li>
</ul>
<h2 id="安全dr-與角色分工">安全、DR 與角色分工</h2>
<p>PostgreSQL 的 production 完整性不只來自 SQL 特性，也來自資料存取、備份復原、升級責任與事故證據的分工。這一段補上 PG baseline 原本留在 limitation 的三個缺口：Security / RLS / audit logging、cross-region DR、application developer vs DBA / SRE 視角。</p>
<table>
  <thead>
      <tr>
          <th>責任面</th>
          <th>PostgreSQL 要回答的問題</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Access control / RLS</td>
          <td>table、row、function、extension 與 service account 權限如何切</td>
          <td><a href="security-rls-audit-logging/">Security / RLS / Audit Logging</a>、<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 Data Protection</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></td>
      </tr>
      <tr>
          <td>TLS / credential</td>
          <td>application 連線、DB user、憑證與 secret rotation 如何治理</td>
          <td><a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">Credential</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a></td>
      </tr>
      <tr>
          <td>Cross-region DR</td>
          <td>region 失效時要 async replica、PITR、Aurora Global Database 還是 distributed SQL</td>
          <td><a href="cross-region-dr/">Cross-region DR</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover</a>、<a href="pitr-wal-archiving/">PITR + WAL Archiving</a></td>
      </tr>
      <tr>
          <td>Developer / DBA split</td>
          <td>application schema、migration、query、index 與 rollback 誰負責</td>
          <td><a href="developer-dba-responsibility-split/">Developer / DBA Responsibility Split</a>、<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Migration Playbook</a></td>
      </tr>
      <tr>
          <td>Incident evidence</td>
          <td>資料事故中要留下哪些 query、timeline、restore 與 decision evidence</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></td>
      </tr>
  </tbody>
</table>
<p>Access control / RLS 的判讀重點是把資料責任放在資料層與 application 層之間分工。PostgreSQL 支援 role、grant、schema、function security 與 row-level security；但 RLS 會把授權邏輯拉進 database，適合 multi-tenant row isolation、資料平台或共享 reporting schema，日常 OLTP 仍要保留 application authorization 與 audit trail。</p>
<p>TLS / credential 的判讀重點是連線安全與憑證生命週期。Self-managed PostgreSQL 要處理 server cert、client cert、DB user rotation 與 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 重連；managed PostgreSQL 常把 certificate、IAM auth 或 secret integration 交給平台，但 application pool、migration tool 與 read replica 仍要一起更新。</p>
<p>Cross-region DR 的判讀重點是 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> 與資料一致性。自管 PostgreSQL 可用 streaming replication、WAL archiving、PITR 與 Patroni 做 region <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a>；Aurora 把 backup、PITR 與 Global Database 交給 AWS；真正 active-active 或 global strong consistency 需求要回到 CockroachDB、Spanner 或 Aurora DSQL，single-primary PostgreSQL 保留為 region failover 與 async DR 路線。</p>
<p>Developer / DBA split 的判讀重點是把日常責任寫進流程。Application developer 擁有 query shape、transaction boundary、repository adapter 與 migration contract；DBA / SRE 擁有 backup、replication、pooler、extension、vacuum、index maintenance 與 DR drill；release gate 需要把兩邊 evidence 合在同一份 decision log。</p>
<h2 id="managed-pg-與相容變體路由">Managed PG 與相容變體路由</h2>
<p>PostgreSQL wire protocol 已成為 managed SQL 與 distributed SQL 的相容目標。選型時要區分「PostgreSQL 本體」、「managed PostgreSQL」、「PostgreSQL-compatible distributed SQL」與「PostgreSQL extension ecosystem」四種不同責任。</p>
<table>
  <thead>
      <tr>
          <th>變體</th>
          <th>適合情境</th>
          <th>主要代價 / 檢查點</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RDS / self-managed PG</td>
          <td>想接近 upstream、保留跨雲與 extension 彈性</td>
          <td>團隊承擔 HA、backup、upgrade、vacuum 與 pooler</td>
          <td><a href="patroni-ha/">Patroni HA</a>、<a href="pitr-wal-archiving/">PITR + WAL Archiving</a></td>
      </tr>
      <tr>
          <td>Aurora PostgreSQL</td>
          <td>AWS 內 production OLTP、想轉移 HA / storage ops</td>
          <td>extension whitelist、cost model、cluster endpoint</td>
          <td><a href="migrate-to-aurora/">→ Aurora</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 vendor</a></td>
      </tr>
      <tr>
          <td>Cloud SQL / AlloyDB</td>
          <td>GCP 內 managed PostgreSQL 與 Google operation model</td>
          <td>extension / version matrix、IAM / backup / cost model</td>
          <td><a href="managed-pg-comparison/">Managed PG Comparison</a></td>
      </tr>
      <tr>
          <td>Azure Cosmos DB for PostgreSQL</td>
          <td>Citus-based distributed PostgreSQL、tenant / shard workload</td>
          <td>coordinator / worker topology、Citus 語意</td>
          <td><a href="citus-distributed/">Citus distributed</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a></td>
      </tr>
      <tr>
          <td>Neon / serverless PG</td>
          <td>preview、branch、稀疏 workload、dev environment</td>
          <td>cold start、connection、production sustained workload</td>
          <td>本頁 vs Neon 段、後續 serverless PG comparison</td>
      </tr>
      <tr>
          <td>Aurora DSQL / CockroachDB</td>
          <td>global write、distributed SQL、region resiliency</td>
          <td>transaction retry、extension gap、latency / cost</td>
          <td><a href="migrate-to-aurora-dsql/">→ Aurora DSQL</a>、<a href="migrate-to-cockroachdb/">→ CockroachDB</a></td>
      </tr>
  </tbody>
</table>
<p>Managed PG 變體的引用規則是先查 compatibility，再談 migration。Extension whitelist、backup / restore API、logical replication 支援、connection endpoint 行為與 pricing 都是時間敏感 claim；實作前要回到官方文件確認版本，並把確認日期留在 migration plan 或 decision log。</p>
<h2 id="deep-article--migration-playbook已完成">Deep article + Migration playbook（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Streaming replication topology + LSN + slot</td>
          <td><a href="replication-topology/">replication-topology</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>pg_repack / pg-osc 跟 PG 內建 ALTER 行為</td>
          <td><a href="online-schema-change/">online-schema-change</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Process-per-connection model + pooler 必要性</td>
          <td><a href="connection-scaling/">connection-scaling</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>pgBouncer + PgCat connection pool</td>
          <td><a href="pgbouncer-config/">pgbouncer-config</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Patroni HA + DCS-based failover</td>
          <td><a href="patroni-ha/">patroni-ha</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Autovacuum tuning + bloat 治理</td>
          <td><a href="autovacuum-tuning/">autovacuum-tuning</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Logical replication + Debezium CDC</td>
          <td><a href="logical-replication-debezium/">logical-replication-debezium</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Citus distributed extension</td>
          <td><a href="citus-distributed/">citus-distributed</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>BDR / pgEdge / Bucardo multi-master</td>
          <td><a href="bdr-multi-master/">bdr-multi-master</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>MVCC + lock model（PG 並行控制核心）</td>
          <td><a href="mvcc-lock-model/">mvcc-lock-model</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>EXPLAIN / auto_explain / pg_hint_plan</td>
          <td><a href="query-optimization/">query-optimization</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Index method 選型決策樹（B-tree / GIN / GiST / BRIN）</td>
          <td><a href="index-selection/">index-selection</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Declarative partitioning + pg_partman</td>
          <td><a href="declarative-partitioning/">declarative-partitioning</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>JSONB binary storage + GIN index</td>
          <td><a href="jsonb-deep-dive/">jsonb-deep-dive</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Full-text search（tsvector + pg_trgm）</td>
          <td><a href="full-text-search/">full-text-search</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Extension ecosystem（pgvector / TimescaleDB 等）</td>
          <td><a href="extension-ecosystem/">extension-ecosystem</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>TimescaleDB hypertable + CAGG + compression</td>
          <td><a href="timescaledb-deep-dive/">timescaledb-deep-dive</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>pgvector HNSW / IVFFlat ANN search</td>
          <td><a href="pgvector-deep-dive/">pgvector-deep-dive</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>PostGIS geometry / geography + GiST</td>
          <td><a href="postgis-deep-dive/">postgis-deep-dive</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>PITR + WAL archiving</td>
          <td><a href="pitr-wal-archiving/">pitr-wal-archiving</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Replication slot management（含 PG 17 failover slot）</td>
          <td><a href="replication-slot-management/">replication-slot-management</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>SQL features baseline + MySQL 對比</td>
          <td><a href="sql-features-baseline/">sql-features-baseline</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Hands-on 操作路線</td>
          <td><a href="hands-on/">hands-on</a></td>
          <td>操作型章節群</td>
      </tr>
      <tr>
          <td>Major version upgrade（N → N+1 pg_upgrade）</td>
          <td><a href="major-version-upgrade/">major-version-upgrade</a></td>
          <td>Migration playbook（5-type 漏類 / 接近 Type B 但需 upgrade-specific audit）</td>
      </tr>
      <tr>
          <td>→ Aurora PostgreSQL</td>
          <td><a href="migrate-to-aurora/">migrate-to-aurora</a></td>
          <td>Migration playbook（Type C）</td>
      </tr>
      <tr>
          <td>→ Aurora DSQL（PG wire-compat distributed）</td>
          <td><a href="migrate-to-aurora-dsql/">migrate-to-aurora-dsql</a></td>
          <td>Migration playbook（Type E）</td>
      </tr>
      <tr>
          <td>→ CockroachDB</td>
          <td><a href="migrate-to-cockroachdb/">migrate-to-cockroachdb</a></td>
          <td>Migration playbook（Type E）</td>
      </tr>
      <tr>
          <td>Multi-region + GDPR rollout</td>
          <td><a href="multi-region-gdpr-rollout/">multi-region-gdpr-rollout</a></td>
          <td>Migration playbook（Type F）</td>
      </tr>
      <tr>
          <td>Partition redesign</td>
          <td><a href="partition-redesign/">partition-redesign</a></td>
          <td>Migration playbook（Type F）</td>
      </tr>
  </tbody>
</table>
<h2 id="補充正文路由">補充正文路由</h2>
<p>當前 deep article、migration playbook、補充正文與 hands-on 已 cover replication / HA / OSC / connection / CDC / sharding / multi-master / MVCC / query opt / index / partitioning / JSONB / FTS / extension（含 TimescaleDB / pgvector / PostGIS）/ backup / slot / SQL features / upgrade / migration / security / DR / managed variant 等維度。下列補充正文用來承接 overview 中提到的延伸議題：</p>
<ul>
<li><strong><a href="logical-decoding-plugins/">Logical decoding plugins deep dive</a></strong>：wal2json / pgoutput / decoderbufs 對位、CDC pipeline 整合</li>
<li><strong><a href="pg-partman-advanced/">pg_partman advanced</a></strong>：retention 跟 child partition 自動 management</li>
<li><strong><a href="connection-pooler-comparison/">Connection pooler comparison</a></strong>：PgBouncer vs Pgcat vs Odyssey 細部對比</li>
<li><strong><a href="aurora-io-optimized-cost/">Aurora I/O-Optimized vs standard</a></strong>：cost model 取捨</li>
<li><strong><a href="managed-pg-comparison/">AlloyDB / Cloud SQL 比較</a></strong>：GCP managed PG 選型</li>
</ul>
<p>上述補充篇已完成正文，並保留既有引用路徑。Logical decoding 接 <a href="logical-replication-debezium/">Logical Replication + Debezium</a> 與 <a href="replication-slot-management/">Replication Slot Management</a>；pg_partman advanced 接 <a href="declarative-partitioning/">Declarative Partitioning</a>；pooler comparison 接 <a href="connection-scaling/">Connection Scaling</a> 與 <a href="pgbouncer-config/">pgBouncer Config</a>；Aurora cost 接 <a href="migrate-to-aurora/">→ Aurora</a>；AlloyDB / Cloud SQL 接 <a href="managed-pg-comparison/">Managed PG Comparison</a>。</p>
<h2 id="案例對照">案例對照</h2>
<p>PostgreSQL 沒有直接的 09 case（多數 09 case 用 managed vendor）、但作為 <em>baseline 跟遷移源頭</em> 在許多 case 出現：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 PostgreSQL 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a></td>
          <td>從多套 RDBMS（含 PostgreSQL）統一到 Aurora</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a></td>
          <td>Azure 生態替代 PostgreSQL 的選擇</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 RDB connection limit</a></td>
          <td>PostgreSQL/MySQL 都有的 connection 限制</td>
      </tr>
  </tbody>
</table>
<h2 id="已知-limitation-與-audit-紀錄">已知 Limitation 與 Audit 紀錄</h2>
<p>本 vendor 頁的 22 篇 deep article + 6 篇 migration playbook 經過 4-reviewer audit（A 寫作規範 / B 跨檔一致性 / C 技術準確性 / D 框架偏誤）、Phase 1-3 修法完成。承認以下 limitation：</p>
<ul>
<li><strong>PG narrative bias</strong>：pgvector / TimescaleDB / extension-ecosystem / Citus 四篇對「PG 取代專業 DB」描述偏 PG-favoring；對手 vendor（Pinecone / InfluxDB / Vitess）的優勢段相對簡短。讀者選型時、請以 cost / ops / scale 三軸綜合判斷、不依本 vendor 頁單一視角。</li>
<li><strong>Anti-recommendation 深度不一</strong>：bdr-multi-master / extension-ecosystem 有「99% 不需要」明確邊界、其他篇章邊界較柔（如「Vector 量 &gt; 5-20M」是粗略門檻）。實際 production 決策請參考多 vendor 對照 + 自家 workload 量測。</li>
<li><strong>Sibling cross-link 狀態</strong>：MySQL ↔ PG sibling、PG 既有 ↔ 新章節 cross-link 已補（refer <a href="/blog/report/sibling-vendor-cross-link-bidirectionality-audit/" data-link-title="Sibling Vendor Cross-Link 雙向性 Audit：寫 Vendor Batch 結束必跑" data-link-desc="當寫 sibling vendor batch（A vs B）、cross-link 容易單向 — A 提 B 多次、B 沒回提 A、形成 navigation asymmetry。Case：MySQL 18 篇對 PG sibling cross-link 9 條、PG 對 MySQL cross-link 0 條。機制：寫第二個 batch 時 reference 第一個 batch 是自然行為、但 reverse direction 必須主動補。修法：vendor batch 結束跑 bidirectional link audit、`A → B` 跟 `B → A` 對比、缺一邊就補。">#136 卡</a>）；本輪同步補 Aurora / CockroachDB / Spanner / Cosmos DB / DynamoDB vendor 頁的反向 sibling 路由，剩餘精修可在各 migration playbook 補更細的 step-by-step 對照。</li>
<li><strong>時間敏感 vendor claim</strong>：Aurora DSQL（2024-12 preview / 2025-05 GA）/ pgvector（0.8 iterative scan）/ TimescaleDB version matrix / DSQL extension 支援範圍持續演進、本 vendor 頁以 2025-2026 公開狀態為準、實作前請以 vendor 官方 docs 為準（refer <a href="/blog/report/vendor-feature-time-sensitivity-claim-verification/" data-link-title="Vendor Feature 時間敏感性：Claim Verification 必跑、寫作日期必標" data-link-desc="寫 vendor article 時、feature limitation claim（『不支援 X』『最多 Y』『預設 Z』）有時間敏感性 — vendor 持續演進、寫作後 N 個月可能 invalidate 整段 audit 邏輯。Case：PlanetScale FK 不支援是 2022 年的事實、2023 末 Vitess 18 加 FK 支援、寫作時若不 verify、Phase 1 audit「FK audit &#43; 全 drop」整段過時。機制：LLM training cutoff vs vendor changelog 速度差、且 LLM 預設不標 claim 的時間性。修法：每篇 vendor article 標 *Last verified* date、limitation claim 必要時加 *as of N* 註、claim 反轉 invalidates 整段 audit 時必須重寫不是修補。">#137 卡</a>）。</li>
<li><strong>補充維度已正文化</strong>：<a href="security-rls-audit-logging/">Security / RLS / audit logging</a>、<a href="cross-region-dr/">cross-region DR</a>、<a href="developer-dba-responsibility-split/">application developer vs DBA 視角分工</a>、<a href="migrate-to-yugabytedb-tidb/">YugabyteDB / TiDB migration playbook</a>、<a href="specialized-pg-variants/">specialized PG variants</a> 已補成正文。本輪也補上跨 vendor 反向連結與時間敏感 claim 路由；下一輪可集中在 migration playbook 的操作步驟與 lab 化。</li>
</ul>
<p>詳細 audit findings 跟修法見 <a href="/blog/report/sibling-vendor-cross-link-bidirectionality-audit/" data-link-title="Sibling Vendor Cross-Link 雙向性 Audit：寫 Vendor Batch 結束必跑" data-link-desc="當寫 sibling vendor batch（A vs B）、cross-link 容易單向 — A 提 B 多次、B 沒回提 A、形成 navigation asymmetry。Case：MySQL 18 篇對 PG sibling cross-link 9 條、PG 對 MySQL cross-link 0 條。機制：寫第二個 batch 時 reference 第一個 batch 是自然行為、但 reverse direction 必須主動補。修法：vendor batch 結束跑 bidirectional link audit、`A → B` 跟 `B → A` 對比、缺一邊就補。">#136 Sibling Vendor Cross-Link Bidirectionality</a> / <a href="/blog/report/vendor-feature-time-sensitivity-claim-verification/" data-link-title="Vendor Feature 時間敏感性：Claim Verification 必跑、寫作日期必標" data-link-desc="寫 vendor article 時、feature limitation claim（『不支援 X』『最多 Y』『預設 Z』）有時間敏感性 — vendor 持續演進、寫作後 N 個月可能 invalidate 整段 audit 邏輯。Case：PlanetScale FK 不支援是 2022 年的事實、2023 末 Vitess 18 加 FK 支援、寫作時若不 verify、Phase 1 audit「FK audit &#43; 全 drop」整段過時。機制：LLM training cutoff vs vendor changelog 速度差、且 LLM 預設不標 claim 的時間性。修法：每篇 vendor article 標 *Last verified* date、limitation claim 必要時加 *as of N* 註、claim 反轉 invalidates 整段 audit 時必須重寫不是修補。">#137 Vendor Feature 時間敏感性</a> / <a href="/blog/report/cross-reviewer-convergence-priority-weighting/" data-link-title="Cross-Reviewer Convergence：多 Reviewer 收斂的 finding 比單 Reviewer flag 信號強" data-link-desc="Multi-reviewer audit（4-reviewer / N-reviewer parallel）後、finding priority 不該是 *N 個 reviewer 報告平均合併*、應該按 *跨 reviewer convergence* 加權 — 兩個獨立 reviewer 從不同 axis 各自發現同一 finding 是 *信號收斂*、比單 reviewer flag 信號強 5-10x。Case：MySQL 17 篇 4-reviewer audit、Reviewer A（寫作規範）跟 Reviewer B（跨檔一致性）獨立 flag 同一 finding『4 篇 migration playbook 缺 weight &#43; banner』、是跨軸 convergence、是最 high-priority fix。機制：N 個獨立 axis 隨機 hit 同一 finding 的機率隨 N 增加而 exponential decline、convergence 排除噪音、是 signal-to-noise 的最高比訊號。修法：multi-reviewer audit 後做 *cross-reviewer matrix*、convergence column 自動標 priority bump。">#138 Cross-Reviewer Convergence</a>。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>connection 沒 pool 直接連</strong>：1000 application instance × 30 connection = 30K connection、PostgreSQL 撐不住</li>
<li><strong>沒 vacuum 治理</strong>：dead tuple 累積、table bloat、query 變慢</li>
<li><strong>大表沒 partition</strong>：&gt; 1 TB 單表的 vacuum / index rebuild 變成事故</li>
<li><strong>index 不 review</strong>：寫吞吐被舊 index 拖垮</li>
<li><strong>跨 AZ sync replication 給寫入吞吐高的 workload</strong>：每次 commit 等 standby ack、寫吞吐減半</li>
<li><strong>logical replication 拖太多 publication</strong>：可能造成 primary WAL 堆積、disk 爆</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor</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 vendor</a>（managed PostgreSQL）</li>
<li>操作：<a href="/blog/backend/01-database/vendors/postgresql/hands-on/" data-link-title="PostgreSQL Hands-on 操作路線" data-link-desc="PostgreSQL local lab、connection pool、PITR restore drill、schema migration evidence 與 HA failover 的操作型章節設計">PostgreSQL Hands-on</a>（local lab、pool、PITR、migration evidence、HA drill）</li>
<li>上游：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（PostgreSQL 不適用時的替代）/ <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（PostgreSQL 不夠用時的升級路徑）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> — connection / replication lag / vacuum 都是 PostgreSQL 常見 bottleneck 源</li>
<li>官方：<a href="https://www.postgresql.org/docs/">PostgreSQL Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.1 壓測理論與系統行為</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>壓測理論的角色是讓「加機器能不能解決」這個問題從直覺變成可推導。沒有理論基礎時、容量決策容易陷入「跑壓測 → 看數字 → 加機器」的盲試循環；有理論之後、可以從「現在的延遲 / 吞吐 / 並發量」反推「瓶頸在哪個資源、加什麼有效」。&lt;/p>
&lt;p>本章是 9.2-9.12 的共同基礎。後續章節的 workload modeling、saturation discovery、capacity planning、SLO 都會回引本章的數學工具。讀者可以把這章當作「容量規劃的最小詞彙表」、其他章節是這些詞彙的應用情境。&lt;/p>
&lt;p>本章不深入推導公式、聚焦在 &lt;em>工程意義&lt;/em>。讀完之後讀者能回答：為什麼系統在 80% utilization 就該擴、為什麼加機器會邊際效益遞減、為什麼 sub-ms 延遲需求會反推架構選擇。&lt;/p>
&lt;h2 id="littles-law穩態系統的最小數學工具">Little&amp;rsquo;s Law：穩態系統的最小數學工具&lt;/h2>
&lt;p>Little&amp;rsquo;s Law 用一條等式 L = λW 把三個變數綁在一起：L 是系統內平均並發數、λ 是請求到達率、W 是請求平均逗留時間。這個關係在 &lt;em>穩態&lt;/em>（流量已穩定、不在 warmup 階段）必然成立、不需要假設特定分布或服務模式。&lt;/p>
&lt;p>工程上最有價值的用法是「反推」。給定預期 RPS λ = 1000 跟 SLO latency 上限 W = 200ms、能算出系統最大穩態並發 L = 1000 × 0.2 = 200。這個 200 直接對應「connection pool size」「thread pool size」「async worker count」這類容量參數 — 訂得比 200 小、系統撐不住預期流量；訂得比 200 大太多、資源浪費。&lt;/p>
&lt;p>反向也成立。當 connection pool 卡死在某個 size L、latency budget W 已訂、能算出可支撐的 RPS。這個算法在 capacity planning 階段比 ramp-up 壓測更快、可以先用 Little&amp;rsquo;s Law 篩掉明顯撐不住的配置、再用壓測驗證剩下的候選。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms&lt;/a> 把 W 訂在 sub-millisecond、所有架構選擇都從這個 W 反推；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi ML p99 &amp;lt; 10ms&lt;/a> 從 W 反推 feature lookup 必須 cache hit 路徑、不能回到持久 store。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/little-law/" data-link-title="Little&amp;#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&amp;rsquo;s Law 卡片&lt;/a>。&lt;/p>
&lt;h2 id="queueing-theory為什麼-80-利用率就是-knee">Queueing Theory：為什麼 80% 利用率就是 knee&lt;/h2>
&lt;p>排隊論（M/M/c 模型）解釋了一個常見直覺：「系統在 50% utilization 看似還很閒、80% 就該擴、90% 已經太晚」。這個直覺不是經驗法則、是 &lt;em>數學必然&lt;/em>。&lt;/p>
&lt;p>M/M/c 系統的平均 queue length 跟 utilization 之間是非線性關係。當 utilization 從 50% 漲到 70%、queue length 約增加 2-3 倍；從 70% 漲到 90%、queue length 增加 10 倍以上。latency 跟 queue length 成正比（Little&amp;rsquo;s Law 又出現）、所以 latency 也呈現同樣的指數成長。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>壓測理論的角色是讓「加機器能不能解決」這個問題從直覺變成可推導。沒有理論基礎時、容量決策容易陷入「跑壓測 → 看數字 → 加機器」的盲試循環；有理論之後、可以從「現在的延遲 / 吞吐 / 並發量」反推「瓶頸在哪個資源、加什麼有效」。</p>
<p>本章是 9.2-9.12 的共同基礎。後續章節的 workload modeling、saturation discovery、capacity planning、SLO 都會回引本章的數學工具。讀者可以把這章當作「容量規劃的最小詞彙表」、其他章節是這些詞彙的應用情境。</p>
<p>本章不深入推導公式、聚焦在 <em>工程意義</em>。讀完之後讀者能回答：為什麼系統在 80% utilization 就該擴、為什麼加機器會邊際效益遞減、為什麼 sub-ms 延遲需求會反推架構選擇。</p>
<h2 id="littles-law穩態系統的最小數學工具">Little&rsquo;s Law：穩態系統的最小數學工具</h2>
<p>Little&rsquo;s Law 用一條等式 L = λW 把三個變數綁在一起：L 是系統內平均並發數、λ 是請求到達率、W 是請求平均逗留時間。這個關係在 <em>穩態</em>（流量已穩定、不在 warmup 階段）必然成立、不需要假設特定分布或服務模式。</p>
<p>工程上最有價值的用法是「反推」。給定預期 RPS λ = 1000 跟 SLO latency 上限 W = 200ms、能算出系統最大穩態並發 L = 1000 × 0.2 = 200。這個 200 直接對應「connection pool size」「thread pool size」「async worker count」這類容量參數 — 訂得比 200 小、系統撐不住預期流量；訂得比 200 大太多、資源浪費。</p>
<p>反向也成立。當 connection pool 卡死在某個 size L、latency budget W 已訂、能算出可支撐的 RPS。這個算法在 capacity planning 階段比 ramp-up 壓測更快、可以先用 Little&rsquo;s Law 篩掉明顯撐不住的配置、再用壓測驗證剩下的候選。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> 把 W 訂在 sub-millisecond、所有架構選擇都從這個 W 反推；<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi ML p99 &lt; 10ms</a> 從 W 反推 feature lookup 必須 cache hit 路徑、不能回到持久 store。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/little-law/" data-link-title="Little&#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&rsquo;s Law 卡片</a>。</p>
<h2 id="queueing-theory為什麼-80-利用率就是-knee">Queueing Theory：為什麼 80% 利用率就是 knee</h2>
<p>排隊論（M/M/c 模型）解釋了一個常見直覺：「系統在 50% utilization 看似還很閒、80% 就該擴、90% 已經太晚」。這個直覺不是經驗法則、是 <em>數學必然</em>。</p>
<p>M/M/c 系統的平均 queue length 跟 utilization 之間是非線性關係。當 utilization 從 50% 漲到 70%、queue length 約增加 2-3 倍；從 70% 漲到 90%、queue length 增加 10 倍以上。latency 跟 queue length 成正比（Little&rsquo;s Law 又出現）、所以 latency 也呈現同樣的指數成長。</p>
<p>工程意義：健康系統運轉在 50-70% utilization、超過 80% 就接近 knee、超過 90% 進入不可預測區。「為什麼明明還沒滿就 saturate」的答案就在這條曲線。autoscaler 的 target metric 通常訂在 60-70%、是 queueing theory 推導出的安全邊界、不是工程師憑感覺。</p>
<p>多 server 模型（M/M/c）比單 server（M/M/1）有顯著容量優勢：c 個 server 的有效容量遠超 1 個 server 容量 × c。這也解釋了為什麼水平擴容（多開幾個 instance）通常比垂直擴容（單機加 CPU）划算 — 不只是規模、是 queue 行為的本質差異。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 25ms p95</a> 把 p95 維持在 25ms 同時撐 54K TPS、靠的是 <em>永遠不讓系統進入 knee</em>、AI 預測讓擴容窗口縮短到 reaction time 內。</p>
<h2 id="universal-scalability-law擴容會邊際失效">Universal Scalability Law：擴容會邊際失效</h2>
<p>USL（Neil Gunther 提出）的公式 throughput(N) = N / (1 + α(N-1) + βN(N-1)) 解釋了「為什麼加機器到某個點之後 throughput 反而下降」。兩個常數 α 跟 β 描述系統的擴展限制：</p>
<ul>
<li>α 是必須序列化的部分（Amdahl&rsquo;s Law 的對應）。distributed lock、coordinator、單一 leader DB 都是 α 來源。α 越大、線性擴容越早 plateau。</li>
<li>β 是節點間互相通訊的成本（crosstalk）。cache invalidation broadcast、consensus <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、cross-region replication 都是 β。β 比 α 更危險、會讓 throughput 在 N 大到某點後 <em>反向下降</em>。</li>
</ul>
<p>工程上 α 比較好處理 — 把序列化部分拆細、用 partition 切分、用 sharded coordinator。β 比較難 — 通訊本質就需要協調、降低 β 通常要重新設計分散式協議（例如 Spanner 用 TrueTime 把跨節點交易的協調成本降低）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner 線性擴展到 10 億 req/sec</a> — TrueTime API 讓跨地區交易的 β 降到可接受、達成傳統 OLTP 做不到的線性；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase RAFT consensus</a> — RAFT 的 quorum 通訊讓 β 不可降、所以 <em>選擇不橫向擴</em>、改用 z1d + Cluster Placement Group 榨單機。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">USL 卡片</a>。</p>
<h2 id="saturation-curvelinear--knee--cliff">Saturation Curve：linear → knee → cliff</h2>
<p>實際系統的 latency vs throughput 曲線分三段。第一段是 linear region — utilization 低、latency 平穩、加流量幾乎不影響 latency。第二段是 knee — utilization 接近 80%、latency 開始指數成長、再加流量會明顯變慢。第三段是 cliff — 系統進入不穩定區、latency 不可預測、可能 timeout、可能 cascade failure。</p>
<p>容量規劃的關鍵概念是 <em>knee point = 設計容量上限</em>。健康系統運轉在 knee 以下 50-70%、留出 headroom 應付 burst 跟 forecast 誤差。沒有量過 knee 的系統等於「不知道距離崩潰多遠」 — 平日看起來穩、實際隨時可能因為一個小 spike 進入 cliff。</p>
<p>不同 system 的 knee 位置差異很大。stateless service 通常 knee 在 80% CPU；DB 因為 lock contention、knee 可能在 60% utilization；broker / queue 因為 disk I/O bottleneck、knee 可能在 50%。容量規劃時不能一概而論、必須個別量測。</p>
<p>每次重大改動後必須 re-test knee。新增功能、改 ORM、升級 library、調 GC tuning、改 cache 策略 — 任何一個都可能讓 knee 往不好的方向移。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft DynamoDB IOPS 20 → 135K</a> — partition 設計均勻時 saturation point 可以推到極遠（6750x 擴展）；<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9000 萬 RPS</a> — 線性擴展靠 partition key 均勻、不靠 vendor 神話。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point 卡片</a>。</p>
<h2 id="反推從業務-kpi-到系統參數">反推：從業務 KPI 到系統參數</h2>
<p>理論工具的真正價值在「反推」 — 不是先設計系統再量測 saturate 多少、是 <em>先訂業務目標再反推系統參數</em>。這層思維把容量規劃從 reactive（撐到撐不住才擴）變成 proactive（按業務需求預先配置）。</p>
<p>反推流程通常從 latency budget 開始（詳見 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>）：</p>
<ol>
<li>從 user-perceived end-to-end latency（例如 p99 500ms）開始</li>
<li>拆到每個 stage（網路、CDN、application、cache、DB、第三方）的 latency 配額</li>
<li>配額決定每個 stage 的設計選擇 — DB 配 50ms → 不能跨 region、application 配 100ms → 不能多層 microservice hop</li>
<li>配額 + 預期 RPS → Little&rsquo;s Law 算每個 stage 的並發</li>
<li>並發 → 每個 stage 的容量需求 → 實例數 / connection pool size / cache size</li>
</ol>
<p>反推失敗的常見徵兆：算出來的某個 stage 容量超過 vendor 提供的上限（例如「需要 50 萬 DynamoDB RCU」可能超過單一 table partition 上限）、或某個 stage latency 配額過短（例如 cross-AZ 網路至少 1-2ms、配 0.5ms 不可能達成）。這時要回頭調整 SLO 或重新設計架構。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget 卡片</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>sub-ms latency 反推所有架構選擇</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>TrueTime 降低 β 達成線性擴展</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>ML p99 &lt; 10ms 的 stage latency 配額</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>線性擴展靠 partition 均勻、不靠魔法</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>下游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a>（把模型量化成 production traffic）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（實測 knee point）</li>
<li>跨章節：<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>（latency budget 拆解）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/little-law/" data-link-title="Little&#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&rsquo;s Law</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
</ul>
]]></content:encoded></item><item><title>9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/</guid><description>&lt;p>這個案例的核心責任是提供「極端可預期峰值」的容量設計參考點。Prime Day 是 Amazon 每年最大的單一行銷事件、發生時間提前數月公告、所有相依服務都能進入準備階段、是最接近「教科書版本的容量規劃」的真實場景。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>2025 年 Prime Day 期間 AWS 主要服務的峰值數字（引自 &lt;a href="https://aws.amazon.com/blogs/aws/aws-services-scale-to-new-heights-for-prime-day-2025-key-metrics-and-milestones/">AWS News Blog&lt;/a>）：&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>Amazon SQS&lt;/td>
 &lt;td>1.66 億訊息 / 秒（新紀錄）&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS Lambda&lt;/td>
 &lt;td>每日 1.7 兆次呼叫&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon API Gateway&lt;/td>
 &lt;td>1 兆次內部請求&lt;/td>
 &lt;td>+30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon DynamoDB&lt;/td>
 &lt;td>1.51 億 RPS、毫秒級回應&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon ElastiCache&lt;/td>
 &lt;td>每日 1.5 quadrillion 請求&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon CloudFront&lt;/td>
 &lt;td>3 兆次 HTTP 請求&lt;/td>
 &lt;td>+43%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon Kinesis Streams&lt;/td>
 &lt;td>8.07 億 records / 秒峰值&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon EBS&lt;/td>
 &lt;td>20.3 兆次 I/O&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon Aurora&lt;/td>
 &lt;td>5000 億次 transaction&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon SageMaker AI&lt;/td>
 &lt;td>6260 億次推論請求&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon ECS on Fargate&lt;/td>
 &lt;td>每日 1840 萬個 task&lt;/td>
 &lt;td>+77%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS FIS（混沌實驗）&lt;/td>
 &lt;td>6800+ 次彈性測試&lt;/td>
 &lt;td>8 倍於 2024&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>基礎設施層面：AWS Graviton 處理器承擔超過 40% 的 EC2 compute、部署超過 87,000 顆 Inferentia / Trainium AI 晶片、AWS Outposts 對機器人下達 5.24 億條指令（年增 160%）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Prime Day 是「可預期極端峰值」的標竿。它的容量問題不是「會不會撐住」、而是「準備到什麼程度才划算」。對應主章問題節點：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Capacity Planning&lt;/strong>（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6&lt;/a>）：年度活動的容量計算可以用歷史 baseline × 預期成長 × headroom 三項相乘、但 Prime Day 規模下、每一項的不確定性放大都會變成數百萬美金成本差異。Amazon 公開的年增率（API Gateway +30%、CloudFront +43%、ECS on Fargate +77%）顯示連 Amazon 自己每年的成長預測都不能直線外推。&lt;/li>
&lt;li>&lt;strong>Performance Observability&lt;/strong>（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8&lt;/a>）：DynamoDB 「1.51 億 RPS、毫秒級回應」這種敘述同時包含吞吐與延遲、是 production-grade 容量地圖的最小單位。只說吞吐不說延伸分布、容量資訊不完整。&lt;/li>
&lt;li>&lt;strong>Improvement Loop&lt;/strong>（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9&lt;/a>）：FIS 混沌實驗 8 倍於 2024 顯示 Amazon 把「在 Prime Day 之前主動製造失敗」當成必修課、不是事後檢討。這層投資跟容量規劃同等重要。&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>這個案例可以抽出三個跨平台可重用的工程做法。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>把可預期峰值寫進服務級 SLO&lt;/strong>：Prime Day 在 SQS / Lambda / DynamoDB / Aurora 都建立了內部 SLO baseline、平日跑在 baseline 之下、峰值是擴張到「設計容量」而不是「實驗容量」。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 直接對齊。&lt;/li>
&lt;li>&lt;strong>pre-scaling + scheduled capacity&lt;/strong>：CloudFront 43%、API Gateway 30% 的年增率都是 &lt;em>提前算進&lt;/em> 容量計畫、不是當天 reactive 擴容。對應 EC2 Auto Scaling 的 &lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-predictive-scaling.html">predictive / scheduled scaling&lt;/a> 模式。&lt;/li>
&lt;li>&lt;strong>事前主動製造失敗、不靠當天 reactive&lt;/strong>：FIS 8x 成長代表「在 Prime Day 之前 6800 次 chaos test」、把驗證成本前置到容量規劃階段。這條跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">06.4 Chaos Testing&lt;/a> 形成閉環 — 06 講失敗模式驗證、09 講容量地圖、兩者在 Prime Day 級別的事件上必須一起做。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP 的 Compute Engine MIG + Predictive Autoscaler、Azure 的 VM Scale Sets + Predictive Autoscale、Kubernetes 生態的 KEDA + Karpenter 都可以實作同樣的 pre-scaling 策略。差異是 vendor 整合度、不是工程概念。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「極端可預期峰值」的容量設計參考點。Prime Day 是 Amazon 每年最大的單一行銷事件、發生時間提前數月公告、所有相依服務都能進入準備階段、是最接近「教科書版本的容量規劃」的真實場景。</p>
<h2 id="觀察">觀察</h2>
<p>2025 年 Prime Day 期間 AWS 主要服務的峰值數字（引自 <a href="https://aws.amazon.com/blogs/aws/aws-services-scale-to-new-heights-for-prime-day-2025-key-metrics-and-milestones/">AWS News Blog</a>）：</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>峰值</th>
          <th>年增率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Amazon SQS</td>
          <td>1.66 億訊息 / 秒（新紀錄）</td>
          <td>-</td>
      </tr>
      <tr>
          <td>AWS Lambda</td>
          <td>每日 1.7 兆次呼叫</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon API Gateway</td>
          <td>1 兆次內部請求</td>
          <td>+30%</td>
      </tr>
      <tr>
          <td>Amazon DynamoDB</td>
          <td>1.51 億 RPS、毫秒級回應</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon ElastiCache</td>
          <td>每日 1.5 quadrillion 請求</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon CloudFront</td>
          <td>3 兆次 HTTP 請求</td>
          <td>+43%</td>
      </tr>
      <tr>
          <td>Amazon Kinesis Streams</td>
          <td>8.07 億 records / 秒峰值</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon EBS</td>
          <td>20.3 兆次 I/O</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon Aurora</td>
          <td>5000 億次 transaction</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon SageMaker AI</td>
          <td>6260 億次推論請求</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Amazon ECS on Fargate</td>
          <td>每日 1840 萬個 task</td>
          <td>+77%</td>
      </tr>
      <tr>
          <td>AWS FIS（混沌實驗）</td>
          <td>6800+ 次彈性測試</td>
          <td>8 倍於 2024</td>
      </tr>
  </tbody>
</table>
<p>基礎設施層面：AWS Graviton 處理器承擔超過 40% 的 EC2 compute、部署超過 87,000 顆 Inferentia / Trainium AI 晶片、AWS Outposts 對機器人下達 5.24 億條指令（年增 160%）。</p>
<h2 id="判讀">判讀</h2>
<p>Prime Day 是「可預期極端峰值」的標竿。它的容量問題不是「會不會撐住」、而是「準備到什麼程度才划算」。對應主章問題節點：</p>
<ol>
<li><strong>Capacity Planning</strong>（<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6</a>）：年度活動的容量計算可以用歷史 baseline × 預期成長 × headroom 三項相乘、但 Prime Day 規模下、每一項的不確定性放大都會變成數百萬美金成本差異。Amazon 公開的年增率（API Gateway +30%、CloudFront +43%、ECS on Fargate +77%）顯示連 Amazon 自己每年的成長預測都不能直線外推。</li>
<li><strong>Performance Observability</strong>（<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8</a>）：DynamoDB 「1.51 億 RPS、毫秒級回應」這種敘述同時包含吞吐與延遲、是 production-grade 容量地圖的最小單位。只說吞吐不說延伸分布、容量資訊不完整。</li>
<li><strong>Improvement Loop</strong>（<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9</a>）：FIS 混沌實驗 8 倍於 2024 顯示 Amazon 把「在 Prime Day 之前主動製造失敗」當成必修課、不是事後檢討。這層投資跟容量規劃同等重要。</li>
</ol>
<h2 id="策略">策略</h2>
<p>這個案例可以抽出三個跨平台可重用的工程做法。</p>
<ol>
<li><strong>把可預期峰值寫進服務級 SLO</strong>：Prime Day 在 SQS / Lambda / DynamoDB / Aurora 都建立了內部 SLO baseline、平日跑在 baseline 之下、峰值是擴張到「設計容量」而不是「實驗容量」。這跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 直接對齊。</li>
<li><strong>pre-scaling + scheduled capacity</strong>：CloudFront 43%、API Gateway 30% 的年增率都是 <em>提前算進</em> 容量計畫、不是當天 reactive 擴容。對應 EC2 Auto Scaling 的 <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-predictive-scaling.html">predictive / scheduled scaling</a> 模式。</li>
<li><strong>事前主動製造失敗、不靠當天 reactive</strong>：FIS 8x 成長代表「在 Prime Day 之前 6800 次 chaos test」、把驗證成本前置到容量規劃階段。這條跟 <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">06.4 Chaos Testing</a> 形成閉環 — 06 講失敗模式驗證、09 講容量地圖、兩者在 Prime Day 級別的事件上必須一起做。</li>
</ol>
<p>跨平台等效：GCP 的 Compute Engine MIG + Predictive Autoscaler、Azure 的 VM Scale Sets + Predictive Autoscale、Kubernetes 生態的 KEDA + Karpenter 都可以實作同樣的 pre-scaling 策略。差異是 vendor 整合度、不是工程概念。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃年度活動容量 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a></li>
<li>想設計可預期峰值的 SLO → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget 政策</a></li>
<li>想做事前混沌驗證 → <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">06.4 Chaos Testing</a> + <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">06.22 Steady State Definition</a></li>
<li>對照不同形狀的峰值 → <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a>（事件型不可預期峰值）/ <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a>（無峰值低延遲）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/aws/aws-services-scale-to-new-heights-for-prime-day-2025-key-metrics-and-milestones/">AWS services scale to new heights for Prime Day 2025: key metrics and milestones</a></li>
<li><a href="https://aws.amazon.com/blogs/industries/conquering-peak-retail-events-with-aws/">Conquering Peak Retail Events with AWS</a></li>
<li><a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-predictive-scaling.html">Predictive scaling for Amazon EC2 Auto Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>2.C1 Meta：Cache Consistency 升級</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/</guid><description>&lt;p>這個案例的核心責任是說明快取轉換不只在容量與速度，還包括一致性治理能力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta 指出快取在 promotion、shard move、故障恢復時容易引入不一致，單靠傳統 invalidation 很難在大規模系統維持穩定。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當快取已是核心路徑，資料新鮮度問題會直接變成服務正確性問題。這時候轉換重點是把一致性追蹤與異常定位制度化，改一個 TTL 解決不了結構問題。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先定義 inconsistency 來源點與觀測點。&lt;/li>
&lt;li>將 mutation tracing 納入治理，而不是只看命中率。&lt;/li>
&lt;li>把一致性指標接到告警與回退條件。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction&lt;/a>，再接 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/">Cache made consistent&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取轉換不只在容量與速度，還包括一致性治理能力。</p>
<h2 id="觀察">觀察</h2>
<p>Meta 指出快取在 promotion、shard move、故障恢復時容易引入不一致，單靠傳統 invalidation 很難在大規模系統維持穩定。</p>
<h2 id="判讀">判讀</h2>
<p>當快取已是核心路徑，資料新鮮度問題會直接變成服務正確性問題。這時候轉換重點是把一致性追蹤與異常定位制度化，改一個 TTL 解決不了結構問題。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先定義 inconsistency 來源點與觀測點。</li>
<li>將 mutation tracing 納入治理，而不是只看命中率。</li>
<li>把一致性指標接到告警與回退條件。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>先回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction</a>，再接 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/">Cache made consistent</a></li>
</ul>
]]></content:encoded></item><item><title>3.C1 Meta：FOQS 從區域到全域佇列遷移</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/meta-foqs-global-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/meta-foqs-global-migration/</guid><description>&lt;p>這個案例的核心責任是說明 queue 轉換不只換 broker，還包含路由與可用性模型重整。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>FOQS 從區域安裝轉為全域架構，目標是讓災害期間佇列資料仍可被存取，並控制遷移期間的延遲與可用性風險。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 queue 成為跨區關鍵路徑，轉換焦點是 discoverability、routing freshness 與 tenant 遷移節奏。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先建立全域路由層，再分批搬遷租戶。&lt;/li>
&lt;li>針對 stale routing 做補貨延遲治理。&lt;/li>
&lt;li>用零停機遷移策略保留客戶端連續性。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2022/01/18/production-engineering/foqs-disaster-ready/">FOQS disaster-ready migration&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 queue 轉換不只換 broker，還包含路由與可用性模型重整。</p>
<h2 id="觀察">觀察</h2>
<p>FOQS 從區域安裝轉為全域架構，目標是讓災害期間佇列資料仍可被存取，並控制遷移期間的延遲與可用性風險。</p>
<h2 id="判讀">判讀</h2>
<p>當 queue 成為跨區關鍵路徑，轉換焦點是 discoverability、routing freshness 與 tenant 遷移節奏。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先建立全域路由層，再分批搬遷租戶。</li>
<li>針對 stale routing 做補貨延遲治理。</li>
<li>用零停機遷移策略保留客戶端連續性。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a> 與 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2022/01/18/production-engineering/foqs-disaster-ready/">FOQS disaster-ready migration</a></li>
</ul>
]]></content:encoded></item><item><title>5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/</guid><description>&lt;p>這個案例的核心責任是把平台遷移從「搬家」改寫成「流量與依賴分段切換」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tradeshift 從 self-hosted Kubernetes 遷移到 Amazon EKS，legacy 叢集上運行 409 個 service。遷移以零停機為硬性前提，且要求對應用程式碼零修改——遷移的複雜度由平台層吸收，服務團隊不改程式碼。&lt;/p>
&lt;p>遷移採用 parallel cluster 架構：新舊叢集同時運行，透過 Linkerd service mesh 的 multi-cluster 能力橋接。Linkerd 在新叢集中建立 mirrored service（帶叢集後綴），讓跨叢集服務呼叫對應用層透明。流量切換用 Linkerd 的 traffic splitting policy 分批控制，不需要修改個別服務的路由邏輯。&lt;/p>
&lt;p>跨叢集延遲實測：從 EKS 叢集存取 legacy 叢集的 gateway，P50=2ms、P95=8ms、P99=9ms。這個延遲水平足以支撐遷移期的跨叢集服務呼叫，但對延遲敏感的路徑仍需要在同一叢集內完成切換才能消除這層額外延遲。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這類遷移的難點在跨叢集服務依賴與流量切換，Kubernetes API 相容性反而是最容易處理的部分。Linkerd multi-cluster 在這個案例中解決了三個問題：跨叢集 service discovery（mirrored service 自動同步）、流量分批控制（traffic splitting 不改應用碼）、遷移期 rollback（切回舊叢集只需調整 traffic split 比例）。&lt;/p>
&lt;p>409 個 service 的遷移不是一次完成——service 之間有依賴關係，遷移順序要按依賴拓樸規劃。被多個服務依賴的基礎 service（auth、config）通常最後遷移或在兩邊都保留，避免跨叢集呼叫成為所有服務的共同瓶頸。&lt;/p>
&lt;p>遷移期最大的隱性風險是「跨叢集延遲累積」。單次跨叢集呼叫 P99=9ms 看似可接受，但一條請求路徑如果串接 5 個跨叢集呼叫，累積延遲可達 45ms。遷移規劃要把服務依賴鏈上的跨叢集呼叫次數納入切換順序考量。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>建立 parallel cluster + mesh bridge&lt;/strong>：新叢集用 EKS 建立，Linkerd multi-cluster 連接新舊叢集，mirrored service 讓跨叢集呼叫透明。&lt;/li>
&lt;li>&lt;strong>按依賴拓樸排序遷移批次&lt;/strong>：葉子服務（無下游依賴）先遷，基礎服務最後遷或雙邊保留。每批遷移後驗證跨叢集延遲是否在可接受範圍。&lt;/li>
&lt;li>&lt;strong>Traffic splitting 分批切流量&lt;/strong>：每個服務遷移後，用 traffic split 從 0% 開始逐步把流量導向新叢集。觀察 per-service error rate 與 latency，確認穩定後提高比例。&lt;/li>
&lt;li>&lt;strong>保留 rollback 路徑&lt;/strong>：舊叢集服務不立即下線，traffic split 隨時可切回 100% 舊叢集。rollback 操作是調整 split 比例，不需要重新部署。&lt;/li>
&lt;li>&lt;strong>遷移完成後拆除 mesh bridge&lt;/strong>：所有服務切換完成且穩定觀測後，移除跨叢集 Linkerd 連線，舊叢集下線。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a>：traffic split 的分批切換與回退策略&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 跨叢集 Discovery&lt;/a>：Linkerd mirrored service 是跨叢集 discovery 的 service mesh federation 做法&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>：每批切換的放行條件與停損訊號&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/containers/tradeshifts-migration-to-amazon-eks-without-downtime-using-linkerd/">Tradeshift migration to EKS without downtime using Linkerd&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台遷移從「搬家」改寫成「流量與依賴分段切換」。</p>
<h2 id="觀察">觀察</h2>
<p>Tradeshift 從 self-hosted Kubernetes 遷移到 Amazon EKS，legacy 叢集上運行 409 個 service。遷移以零停機為硬性前提，且要求對應用程式碼零修改——遷移的複雜度由平台層吸收，服務團隊不改程式碼。</p>
<p>遷移採用 parallel cluster 架構：新舊叢集同時運行，透過 Linkerd service mesh 的 multi-cluster 能力橋接。Linkerd 在新叢集中建立 mirrored service（帶叢集後綴），讓跨叢集服務呼叫對應用層透明。流量切換用 Linkerd 的 traffic splitting policy 分批控制，不需要修改個別服務的路由邏輯。</p>
<p>跨叢集延遲實測：從 EKS 叢集存取 legacy 叢集的 gateway，P50=2ms、P95=8ms、P99=9ms。這個延遲水平足以支撐遷移期的跨叢集服務呼叫，但對延遲敏感的路徑仍需要在同一叢集內完成切換才能消除這層額外延遲。</p>
<h2 id="判讀">判讀</h2>
<p>這類遷移的難點在跨叢集服務依賴與流量切換，Kubernetes API 相容性反而是最容易處理的部分。Linkerd multi-cluster 在這個案例中解決了三個問題：跨叢集 service discovery（mirrored service 自動同步）、流量分批控制（traffic splitting 不改應用碼）、遷移期 rollback（切回舊叢集只需調整 traffic split 比例）。</p>
<p>409 個 service 的遷移不是一次完成——service 之間有依賴關係，遷移順序要按依賴拓樸規劃。被多個服務依賴的基礎 service（auth、config）通常最後遷移或在兩邊都保留，避免跨叢集呼叫成為所有服務的共同瓶頸。</p>
<p>遷移期最大的隱性風險是「跨叢集延遲累積」。單次跨叢集呼叫 P99=9ms 看似可接受，但一條請求路徑如果串接 5 個跨叢集呼叫，累積延遲可達 45ms。遷移規劃要把服務依賴鏈上的跨叢集呼叫次數納入切換順序考量。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>建立 parallel cluster + mesh bridge</strong>：新叢集用 EKS 建立，Linkerd multi-cluster 連接新舊叢集，mirrored service 讓跨叢集呼叫透明。</li>
<li><strong>按依賴拓樸排序遷移批次</strong>：葉子服務（無下游依賴）先遷，基礎服務最後遷或雙邊保留。每批遷移後驗證跨叢集延遲是否在可接受範圍。</li>
<li><strong>Traffic splitting 分批切流量</strong>：每個服務遷移後，用 traffic split 從 0% 開始逐步把流量導向新叢集。觀察 per-service error rate 與 latency，確認穩定後提高比例。</li>
<li><strong>保留 rollback 路徑</strong>：舊叢集服務不立即下線，traffic split 隨時可切回 100% 舊叢集。rollback 操作是調整 split 比例，不需要重新部署。</li>
<li><strong>遷移完成後拆除 mesh bridge</strong>：所有服務切換完成且穩定觀測後，移除跨叢集 Linkerd 連線，舊叢集下線。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>：traffic split 的分批切換與回退策略</li>
<li><a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 跨叢集 Discovery</a>：Linkerd mirrored service 是跨叢集 discovery 的 service mesh federation 做法</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>：每批切換的放行條件與停損訊號</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/containers/tradeshifts-migration-to-amazon-eks-without-downtime-using-linkerd/">Tradeshift migration to EKS without downtime using Linkerd</a></li>
</ul>
]]></content:encoded></item><item><title>7.C1 Cloudflare：2026 Route Leak 事件</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/</guid><description>&lt;p>這個案例的核心責任是把網路控制面事件轉換成治理層可操作條件。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Cloudflare 在 2026-01-22 發生 route leak，成因是自動化路由政策配置錯誤，導致流量擁塞與延遲提升。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>控制面自動化帶來速度，也提高錯誤一次性放大的風險。關鍵是補強變更守門與回復策略，停止自動化會退回更差的狀態。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>路由政策變更要有 pre-check 與 blast radius 評估。&lt;/li>
&lt;li>建立快速撤回機制與明確責任路由。&lt;/li>
&lt;li>把同類事件寫入 tripwire，觸發強制重評估。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 governance exception/tripwire&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 containment/recovery&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/route-leak-incident-january-22-2026/">Cloudflare route leak incident (2026-01-23)&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把網路控制面事件轉換成治理層可操作條件。</p>
<h2 id="觀察">觀察</h2>
<p>Cloudflare 在 2026-01-22 發生 route leak，成因是自動化路由政策配置錯誤，導致流量擁塞與延遲提升。</p>
<h2 id="判讀">判讀</h2>
<p>控制面自動化帶來速度，也提高錯誤一次性放大的風險。關鍵是補強變更守門與回復策略，停止自動化會退回更差的狀態。</p>
<h2 id="策略">策略</h2>
<ol>
<li>路由政策變更要有 pre-check 與 blast radius 評估。</li>
<li>建立快速撤回機制與明確責任路由。</li>
<li>把同類事件寫入 tripwire，觸發強制重評估。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 governance exception/tripwire</a> 與 <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 containment/recovery</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/route-leak-incident-january-22-2026/">Cloudflare route leak incident (2026-01-23)</a></li>
</ul>
]]></content:encoded></item><item><title>Atlassian 2022 April Multi-tenant Deletion Outage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/</guid><description>&lt;p>Atlassian 2022 事故的核心教訓是：在多租戶 SaaS 中，誤刪不只是一個資料問題，而是恢復編排、客戶通訊與跨團隊協調同時失效的系統級事件。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Atlassian 官方 PIR 指出，2022-04-05 起有 775 客戶受影響，部分恢復歷時長達 14 天。事故起因是維運腳本使用了錯誤識別資訊，導致站點被刪除，後續需要多工作流並行恢復與驗證。&lt;/p>
&lt;p>事件特徵是「影響客戶數有限，但每一個客戶的恢復成本高」，因此恢復策略必須分批與分層。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>客戶站點直接不可用&lt;/td>
 &lt;td>已是 tenant 級資料生命週期事件&lt;/td>
 &lt;td>立即升級 major incident&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>恢復進度呈現長尾分布&lt;/td>
 &lt;td>不同租戶恢復難度差異大&lt;/td>
 &lt;td>改分批恢復與分層追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>初期通訊管道壓力高&lt;/td>
 &lt;td>客戶影響與資訊需求同步上升&lt;/td>
 &lt;td>固定通訊節奏，區分已知事實與待確認項&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;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>維運腳本操作錯誤導致多租戶站點被刪除。&lt;/li>
&lt;li>客戶無法存取產品並建立支援事件。&lt;/li>
&lt;li>事故升級後成立跨職能指揮團隊，24x7 推進恢復。&lt;/li>
&lt;li>恢復以分批方式進行，並持續更新 status 與客戶通訊。&lt;/li>
&lt;li>事後回寫到 soft delete、恢復自動化與通訊流程改善。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Script safety guardrail&lt;/td>
 &lt;td>腳本輸入與刪除對象校驗不足&lt;/td>
 &lt;td>高風險刪除操作增加雙重驗證與範圍確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-tenant restore orchestration&lt;/td>
 &lt;td>大規模租戶恢復缺少標準化分批流程&lt;/td>
 &lt;td>建立恢復編排工具與租戶優先序模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data restoration consistency&lt;/td>
 &lt;td>恢復點一致性在早期流程中不穩&lt;/td>
 &lt;td>增加恢復後一致性審核與回補流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident communication resilience&lt;/td>
 &lt;td>長事故中的客戶通訊節奏與聯絡資料治理&lt;/td>
 &lt;td>固定 cadence、改善受影響客戶聯絡資訊可得性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>客戶影響評估： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment&lt;/a>&lt;/li>
&lt;li>事中決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>穩態與恢復完成： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/post-incident-review-april-2022-outage">Post-Incident Review on the Atlassian April 2022 outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/april-2022-outage-update">Update on the Atlassian outage affecting some customers&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Atlassian 2022 事故的核心教訓是：在多租戶 SaaS 中，誤刪不只是一個資料問題，而是恢復編排、客戶通訊與跨團隊協調同時失效的系統級事件。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Atlassian 官方 PIR 指出，2022-04-05 起有 775 客戶受影響，部分恢復歷時長達 14 天。事故起因是維運腳本使用了錯誤識別資訊，導致站點被刪除，後續需要多工作流並行恢復與驗證。</p>
<p>事件特徵是「影響客戶數有限，但每一個客戶的恢復成本高」，因此恢復策略必須分批與分層。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶站點直接不可用</td>
          <td>已是 tenant 級資料生命週期事件</td>
          <td>立即升級 major incident</td>
      </tr>
      <tr>
          <td>恢復進度呈現長尾分布</td>
          <td>不同租戶恢復難度差異大</td>
          <td>改分批恢復與分層追蹤</td>
      </tr>
      <tr>
          <td>初期通訊管道壓力高</td>
          <td>客戶影響與資訊需求同步上升</td>
          <td>固定通訊節奏，區分已知事實與待確認項</td>
      </tr>
      <tr>
          <td>後續發現部分資料恢復點不一致</td>
          <td>恢復策略與資料一致性治理待補</td>
          <td>增加恢復後審核與補救流程</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>維運腳本操作錯誤導致多租戶站點被刪除。</li>
<li>客戶無法存取產品並建立支援事件。</li>
<li>事故升級後成立跨職能指揮團隊，24x7 推進恢復。</li>
<li>恢復以分批方式進行，並持續更新 status 與客戶通訊。</li>
<li>事後回寫到 soft delete、恢復自動化與通訊流程改善。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Script safety guardrail</td>
          <td>腳本輸入與刪除對象校驗不足</td>
          <td>高風險刪除操作增加雙重驗證與範圍確認</td>
      </tr>
      <tr>
          <td>Multi-tenant restore orchestration</td>
          <td>大規模租戶恢復缺少標準化分批流程</td>
          <td>建立恢復編排工具與租戶優先序模型</td>
      </tr>
      <tr>
          <td>Data restoration consistency</td>
          <td>恢復點一致性在早期流程中不穩</td>
          <td>增加恢復後一致性審核與回補流程</td>
      </tr>
      <tr>
          <td>Incident communication resilience</td>
          <td>長事故中的客戶通訊節奏與聯絡資料治理</td>
          <td>固定 cadence、改善受影響客戶聯絡資訊可得性</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>客戶影響評估： <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment</a></li>
<li>事中決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>穩態與恢復完成： <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/post-incident-review-april-2022-outage">Post-Incident Review on the Atlassian April 2022 outage</a></li>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/april-2022-outage-update">Update on the Atlassian outage affecting some customers</a></li>
</ul>
]]></content:encoded></item><item><title>AWS S3 2017 US-EAST-1 Service Disruption</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/</guid><description>&lt;p>2017 年 AWS S3 us-east-1 事故的核心教訓是：內部操作工具若能快速移除共享子系統容量，單一命令輸入錯誤就會變成區域級控制面事故。這類事故的第一責任是限制操作 blast radius，再把恢復順序與通訊入口從受影響依賴中拆出。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>AWS 在 2017-02-28 發生 Amazon S3 Northern Virginia（US-EAST-1）服務中斷。官方摘要指出，S3 團隊當時正在排查 billing system 進度偏慢問題；9:37AM PST，一位授權 S3 團隊成員依既有 playbook 執行命令，原本只要移除少量 billing 相關子系統 server，但其中一個輸入值錯誤，導致移除的 server set 比預期大。&lt;/p>
&lt;p>被移除的 server 同時支援 S3 的 index subsystem 與 placement subsystem。index subsystem 管理該 region 內所有 S3 object 的 metadata 與位置資訊，GET、LIST、PUT、DELETE 都依賴它；placement subsystem 負責新 object 的 storage allocation，PUT 還需要它才能運作。這兩個子系統被迫完整重啟，導致 S3 API 在重啟期間無法正常服務。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>GET / LIST / PUT / DELETE 同時異常&lt;/td>
 &lt;td>index subsystem 已成為共同故障點&lt;/td>
 &lt;td>優先判斷 metadata / index 層，而非單一 API&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PUT 恢復晚於 GET / LIST / DELETE&lt;/td>
 &lt;td>placement subsystem 仍未完成恢復&lt;/td>
 &lt;td>對外通訊要分操作類型描述恢復狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>EC2 launch、EBS snapshot、Lambda 受影響&lt;/td>
 &lt;td>S3 是多服務共享依賴&lt;/td>
 &lt;td>incident scope 需要擴到 dependent services&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service Health Dashboard 更新受阻&lt;/td>
 &lt;td>狀態頁管理入口依賴受影響服務&lt;/td>
 &lt;td>立即切到獨立通訊路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重啟時間超過預期&lt;/td>
 &lt;td>大型子系統多年未完整重啟與驗證&lt;/td>
 &lt;td>回寫 recovery rehearsal 與 cell partition&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>S3 團隊排查 billing system 進度偏慢問題。&lt;/li>
&lt;li>授權成員依既有 playbook 執行移除少量 server 的操作命令。&lt;/li>
&lt;li>命令輸入值錯誤，移除的 server set 比預期大。&lt;/li>
&lt;li>被移除容量同時支援 index subsystem 與 placement subsystem。&lt;/li>
&lt;li>兩個子系統需要完整重啟，S3 API 在重啟期間無法正常服務。&lt;/li>
&lt;li>依賴 S3 的其他 AWS 服務在 US-EAST-1 同步受影響。&lt;/li>
&lt;li>AWS 先用 AWS Twitter feed 與 Service Health Dashboard banner text 溝通，直到 SHD individual service status 可以更新。&lt;/li>
&lt;li>index subsystem 先恢復足夠容量，再逐步恢復 GET / LIST / DELETE；placement subsystem 完成後，PUT 才恢復正常。&lt;/li>
&lt;/ol>
&lt;p>這條路徑顯示：事故起點是內部操作工具缺少數量與容量下限保護，外部流量尖峰在此無關。真正放大事故的是共享子系統、區域依賴與通訊入口對同一服務的依賴。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>操作工具安全閘門&lt;/td>
 &lt;td>單一輸入錯誤可快速移除過多容量&lt;/td>
 &lt;td>對 remove / drain 類操作加速率、數量與 minimum capacity guardrail&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared subsystem blast radius&lt;/td>
 &lt;td>billing 操作影響 index 與 placement&lt;/td>
 &lt;td>對共享子系統建立 dependency map 與 blast radius review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery rehearsal&lt;/td>
 &lt;td>大型子系統多年未完整重啟，恢復時間超過預期&lt;/td>
 &lt;td>把 index / placement 類核心子系統納入定期 restart / restore rehearsal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cell partition&lt;/td>
 &lt;td>大型 region 子系統恢復成本過高&lt;/td>
 &lt;td>把核心子系統拆成較小 cell，降低單次恢復範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Status page dependency&lt;/td>
 &lt;td>SHD 管理入口依賴受影響服務&lt;/td>
 &lt;td>將 incident communication 工具跨 region 與跨依賴部署&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operation decision log&lt;/td>
 &lt;td>事中需要記錄重啟順序與 API 恢復差異&lt;/td>
 &lt;td>在 decision log 中分別記錄 index、placement 與 dependent services 狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>觀測證據包： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>&lt;/li>
&lt;li>實驗安全邊界： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>&lt;/li>
&lt;li>穩態與恢復完成： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>止血與回復： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy&lt;/a>&lt;/li>
&lt;li>事中決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/message/41926/">Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2017 年 AWS S3 us-east-1 事故的核心教訓是：內部操作工具若能快速移除共享子系統容量，單一命令輸入錯誤就會變成區域級控制面事故。這類事故的第一責任是限制操作 blast radius，再把恢復順序與通訊入口從受影響依賴中拆出。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>AWS 在 2017-02-28 發生 Amazon S3 Northern Virginia（US-EAST-1）服務中斷。官方摘要指出，S3 團隊當時正在排查 billing system 進度偏慢問題；9:37AM PST，一位授權 S3 團隊成員依既有 playbook 執行命令，原本只要移除少量 billing 相關子系統 server，但其中一個輸入值錯誤，導致移除的 server set 比預期大。</p>
<p>被移除的 server 同時支援 S3 的 index subsystem 與 placement subsystem。index subsystem 管理該 region 內所有 S3 object 的 metadata 與位置資訊，GET、LIST、PUT、DELETE 都依賴它；placement subsystem 負責新 object 的 storage allocation，PUT 還需要它才能運作。這兩個子系統被迫完整重啟，導致 S3 API 在重啟期間無法正常服務。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GET / LIST / PUT / DELETE 同時異常</td>
          <td>index subsystem 已成為共同故障點</td>
          <td>優先判斷 metadata / index 層，而非單一 API</td>
      </tr>
      <tr>
          <td>PUT 恢復晚於 GET / LIST / DELETE</td>
          <td>placement subsystem 仍未完成恢復</td>
          <td>對外通訊要分操作類型描述恢復狀態</td>
      </tr>
      <tr>
          <td>EC2 launch、EBS snapshot、Lambda 受影響</td>
          <td>S3 是多服務共享依賴</td>
          <td>incident scope 需要擴到 dependent services</td>
      </tr>
      <tr>
          <td>Service Health Dashboard 更新受阻</td>
          <td>狀態頁管理入口依賴受影響服務</td>
          <td>立即切到獨立通訊路徑</td>
      </tr>
      <tr>
          <td>重啟時間超過預期</td>
          <td>大型子系統多年未完整重啟與驗證</td>
          <td>回寫 recovery rehearsal 與 cell partition</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>S3 團隊排查 billing system 進度偏慢問題。</li>
<li>授權成員依既有 playbook 執行移除少量 server 的操作命令。</li>
<li>命令輸入值錯誤，移除的 server set 比預期大。</li>
<li>被移除容量同時支援 index subsystem 與 placement subsystem。</li>
<li>兩個子系統需要完整重啟，S3 API 在重啟期間無法正常服務。</li>
<li>依賴 S3 的其他 AWS 服務在 US-EAST-1 同步受影響。</li>
<li>AWS 先用 AWS Twitter feed 與 Service Health Dashboard banner text 溝通，直到 SHD individual service status 可以更新。</li>
<li>index subsystem 先恢復足夠容量，再逐步恢復 GET / LIST / DELETE；placement subsystem 完成後，PUT 才恢復正常。</li>
</ol>
<p>這條路徑顯示：事故起點是內部操作工具缺少數量與容量下限保護，外部流量尖峰在此無關。真正放大事故的是共享子系統、區域依賴與通訊入口對同一服務的依賴。</p>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>操作工具安全閘門</td>
          <td>單一輸入錯誤可快速移除過多容量</td>
          <td>對 remove / drain 類操作加速率、數量與 minimum capacity guardrail</td>
      </tr>
      <tr>
          <td>Shared subsystem blast radius</td>
          <td>billing 操作影響 index 與 placement</td>
          <td>對共享子系統建立 dependency map 與 blast radius review</td>
      </tr>
      <tr>
          <td>Recovery rehearsal</td>
          <td>大型子系統多年未完整重啟，恢復時間超過預期</td>
          <td>把 index / placement 類核心子系統納入定期 restart / restore rehearsal</td>
      </tr>
      <tr>
          <td>Cell partition</td>
          <td>大型 region 子系統恢復成本過高</td>
          <td>把核心子系統拆成較小 cell，降低單次恢復範圍</td>
      </tr>
      <tr>
          <td>Status page dependency</td>
          <td>SHD 管理入口依賴受影響服務</td>
          <td>將 incident communication 工具跨 region 與跨依賴部署</td>
      </tr>
      <tr>
          <td>Operation decision log</td>
          <td>事中需要記錄重啟順序與 API 恢復差異</td>
          <td>在 decision log 中分別記錄 index、placement 與 dependent services 狀態</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>觀測證據包： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>實驗安全邊界： <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>穩態與恢復完成： <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>止血與回復： <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy</a></li>
<li>事中決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/message/41926/">Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare 2019 Regex CPU Outage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/</guid><description>&lt;p>2019 年 Cloudflare regex 事故的核心教訓是：控制面配置錯誤可以在秒級擴散成全球可用性事故。這類事故的第一責任不是「加機器」，而是迅速切斷擴散路徑，讓錯誤停止被新流量放大。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cloudflare 在 2019-07-02 發布新的 WAF Managed Rule 後，規則中的 regex 觸發 catastrophic backtracking，導致 edge CPU 快速打滿。事故影響約 27 分鐘，症狀是大量 502/503 與延遲激增。&lt;/p>
&lt;p>這起事件屬於典型「控制面配置推送 → data plane 全網受影響」模式。錯誤並非單點節點故障，而是由一致推送機制把同一錯誤同步擴散到整個 edge 網路。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全球 CPU 同步飆升&lt;/td>
 &lt;td>問題來自共用規則或共用執行路徑&lt;/td>
 &lt;td>優先檢查最新全域配置變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5xx 與延遲同時惡化&lt;/td>
 &lt;td>非單純容量尖峰，更像執行成本突增&lt;/td>
 &lt;td>優先撤回新規則，避免持續放大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多區域同時報警&lt;/td>
 &lt;td>事故已跨區域，屬全網級控制面風險&lt;/td>
 &lt;td>啟動全域指揮節奏與高頻通訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回滾後指標快速回穩&lt;/td>
 &lt;td>根因與近期變更高度相關&lt;/td>
 &lt;td>立即凍結同批規則推送，改走分區驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件期間 rule path 命中異常增&lt;/td>
 &lt;td>單一規則造成 CPU 熱點&lt;/td>
 &lt;td>補 rule-level profiling 與上線前成本檢查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>控制面推送新 WAF 規則到全球 edge。&lt;/li>
&lt;li>規則 regex 在特定輸入下產生高計算成本。&lt;/li>
&lt;li>edge CPU 被規則執行成本吃滿，請求處理能力下降。&lt;/li>
&lt;li>5xx 與延遲擴散成全球可見症狀。&lt;/li>
&lt;li>回滾規則後，CPU 與可用性逐步恢復。&lt;/li>
&lt;/ol>
&lt;p>這條路徑顯示：事故擴散速度主要由「推送覆蓋範圍」決定，而不是由「單機故障率」決定。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>規則上線前靜態檢查&lt;/td>
 &lt;td>regex 風險模式未被擋下&lt;/td>
 &lt;td>補 regex 風險 lint 與拒絕規則（高 backtracking 風險直接阻擋）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>上線前效能測試&lt;/td>
 &lt;td>缺少 rule-level CPU 成本基線&lt;/td>
 &lt;td>補 rule replay 測試，用代表性 payload 驗證執行成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推送策略&lt;/td>
 &lt;td>全域一次推送讓 blast radius 過大&lt;/td>
 &lt;td>改成分區/分群 staged rollout，設回滾閘門&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故啟動門檻&lt;/td>
 &lt;td>全網症狀出現後才完整升級&lt;/td>
 &lt;td>以「跨區 CPU 同步異常 + 5xx 上升」作為自動升級條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision log&lt;/td>
 &lt;td>事中決策若缺時間線，復盤成本高&lt;/td>
 &lt;td>在事故期間即時記錄假設、回滾條件、責任人與驗證結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence write-back&lt;/td>
 &lt;td>事故教訓易停在 PIR 文本&lt;/td>
 &lt;td>回寫到 &lt;code>04&lt;/code> 觀測規則與 &lt;code>06&lt;/code> 實驗安全邊界，形成下次推送前硬性 gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>回寫訊號治理： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a>&lt;/li>
&lt;li>回寫規則成本訊號： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 Rule-level CPU Signal Governance&lt;/a>&lt;/li>
&lt;li>回寫規則推送閘門： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a>&lt;/li>
&lt;li>回寫驗證與安全邊界： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>&lt;/li>
&lt;li>回寫事中決策與證據： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>回寫跨模組閉環： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019">Details of the Cloudflare outage on July 2, 2019&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2019 年 Cloudflare regex 事故的核心教訓是：控制面配置錯誤可以在秒級擴散成全球可用性事故。這類事故的第一責任不是「加機器」，而是迅速切斷擴散路徑，讓錯誤停止被新流量放大。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Cloudflare 在 2019-07-02 發布新的 WAF Managed Rule 後，規則中的 regex 觸發 catastrophic backtracking，導致 edge CPU 快速打滿。事故影響約 27 分鐘，症狀是大量 502/503 與延遲激增。</p>
<p>這起事件屬於典型「控制面配置推送 → data plane 全網受影響」模式。錯誤並非單點節點故障，而是由一致推送機制把同一錯誤同步擴散到整個 edge 網路。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全球 CPU 同步飆升</td>
          <td>問題來自共用規則或共用執行路徑</td>
          <td>優先檢查最新全域配置變更</td>
      </tr>
      <tr>
          <td>5xx 與延遲同時惡化</td>
          <td>非單純容量尖峰，更像執行成本突增</td>
          <td>優先撤回新規則，避免持續放大</td>
      </tr>
      <tr>
          <td>多區域同時報警</td>
          <td>事故已跨區域，屬全網級控制面風險</td>
          <td>啟動全域指揮節奏與高頻通訊</td>
      </tr>
      <tr>
          <td>回滾後指標快速回穩</td>
          <td>根因與近期變更高度相關</td>
          <td>立即凍結同批規則推送，改走分區驗證</td>
      </tr>
      <tr>
          <td>事件期間 rule path 命中異常增</td>
          <td>單一規則造成 CPU 熱點</td>
          <td>補 rule-level profiling 與上線前成本檢查</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>控制面推送新 WAF 規則到全球 edge。</li>
<li>規則 regex 在特定輸入下產生高計算成本。</li>
<li>edge CPU 被規則執行成本吃滿，請求處理能力下降。</li>
<li>5xx 與延遲擴散成全球可見症狀。</li>
<li>回滾規則後，CPU 與可用性逐步恢復。</li>
</ol>
<p>這條路徑顯示：事故擴散速度主要由「推送覆蓋範圍」決定，而不是由「單機故障率」決定。</p>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規則上線前靜態檢查</td>
          <td>regex 風險模式未被擋下</td>
          <td>補 regex 風險 lint 與拒絕規則（高 backtracking 風險直接阻擋）</td>
      </tr>
      <tr>
          <td>上線前效能測試</td>
          <td>缺少 rule-level CPU 成本基線</td>
          <td>補 rule replay 測試，用代表性 payload 驗證執行成本</td>
      </tr>
      <tr>
          <td>推送策略</td>
          <td>全域一次推送讓 blast radius 過大</td>
          <td>改成分區/分群 staged rollout，設回滾閘門</td>
      </tr>
      <tr>
          <td>事故啟動門檻</td>
          <td>全網症狀出現後才完整升級</td>
          <td>以「跨區 CPU 同步異常 + 5xx 上升」作為自動升級條件</td>
      </tr>
      <tr>
          <td>Decision log</td>
          <td>事中決策若缺時間線，復盤成本高</td>
          <td>在事故期間即時記錄假設、回滾條件、責任人與驗證結果</td>
      </tr>
      <tr>
          <td>Evidence write-back</td>
          <td>事故教訓易停在 PIR 文本</td>
          <td>回寫到 <code>04</code> 觀測規則與 <code>06</code> 實驗安全邊界，形成下次推送前硬性 gate</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>回寫訊號治理： <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>回寫規則成本訊號： <a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 Rule-level CPU Signal Governance</a></li>
<li>回寫規則推送閘門： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a></li>
<li>回寫驗證與安全邊界： <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>回寫事中決策與證據： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>回寫跨模組閉環： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019">Details of the Cloudflare outage on July 2, 2019</a></li>
</ul>
]]></content:encoded></item><item><title>Fastly 2021 June Global Edge Config-triggered Outage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/</guid><description>&lt;p>Fastly 2021 事故的核心教訓是：在全球 edge 平台中，一個有效配置也可能觸發平台潛藏 bug，造成分鐘級全球擴散。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Fastly 官方摘要指出，2021-06-08 的全球 outage 由平台既有軟體 bug 觸發，觸發條件來自一個有效的客戶配置變更。故障在短時間內影響大範圍 edge 節點，並在隔離配置後逐步恢復。&lt;/p>
&lt;p>這類事故不是「客戶配置錯誤」或「平台單點故障」的二選一，而是配置與平台行為交互下的系統性風險。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全球 503 快速上升&lt;/td>
 &lt;td>edge 平台共同執行路徑失效&lt;/td>
 &lt;td>立即轉全域 incident，不走單區排障&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偵測時間短但影響面巨大&lt;/td>
 &lt;td>擴散速度高於人工逐站處理能力&lt;/td>
 &lt;td>優先做全域隔離與停傳播動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>關閉觸發配置後快速回線&lt;/td>
 &lt;td>觸發路徑明確、回退有效&lt;/td>
 &lt;td>建立配置觸發型事故的快速回退標準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故前已有潛藏 bug&lt;/td>
 &lt;td>變更驗證對交互條件覆蓋不足&lt;/td>
 &lt;td>回寫配置驗證與灰度策略&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>平台先前部署引入可被特定條件觸發的 bug。&lt;/li>
&lt;li>客戶推送有效配置，觸發 bug。&lt;/li>
&lt;li>大範圍 edge 節點回應錯誤，形成全球 outage。&lt;/li>
&lt;li>團隊定位並隔離觸發配置，服務逐步恢復。&lt;/li>
&lt;li>事後回寫驗證、隔離與恢復流程。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Config-trigger safety gate&lt;/td>
 &lt;td>有效配置也可觸發平台 bug&lt;/td>
 &lt;td>對配置與平台交互條件增加回放測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Global propagation brake&lt;/td>
 &lt;td>擴散速度遠快於局部人工止血&lt;/td>
 &lt;td>建立全域停傳播與快速隔離機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Canary and staged rollout&lt;/td>
 &lt;td>交互條件在前期驗證未被涵蓋&lt;/td>
 &lt;td>強化灰度策略與跨場景驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident communication timing&lt;/td>
 &lt;td>影響廣但恢復快，對外節奏需精準&lt;/td>
 &lt;td>以固定 cadence 說明影響範圍與恢復進度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>規則/配置成本訊號： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 Rule-level CPU Signal Governance&lt;/a>&lt;/li>
&lt;li>證據包： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>&lt;/li>
&lt;li>規則推送閘門： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.fastly.com/blog/summary-of-june-8-outage">Summary of June 8 outage&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Fastly 2021 事故的核心教訓是：在全球 edge 平台中，一個有效配置也可能觸發平台潛藏 bug，造成分鐘級全球擴散。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Fastly 官方摘要指出，2021-06-08 的全球 outage 由平台既有軟體 bug 觸發，觸發條件來自一個有效的客戶配置變更。故障在短時間內影響大範圍 edge 節點，並在隔離配置後逐步恢復。</p>
<p>這類事故不是「客戶配置錯誤」或「平台單點故障」的二選一，而是配置與平台行為交互下的系統性風險。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全球 503 快速上升</td>
          <td>edge 平台共同執行路徑失效</td>
          <td>立即轉全域 incident，不走單區排障</td>
      </tr>
      <tr>
          <td>偵測時間短但影響面巨大</td>
          <td>擴散速度高於人工逐站處理能力</td>
          <td>優先做全域隔離與停傳播動作</td>
      </tr>
      <tr>
          <td>關閉觸發配置後快速回線</td>
          <td>觸發路徑明確、回退有效</td>
          <td>建立配置觸發型事故的快速回退標準</td>
      </tr>
      <tr>
          <td>事故前已有潛藏 bug</td>
          <td>變更驗證對交互條件覆蓋不足</td>
          <td>回寫配置驗證與灰度策略</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>平台先前部署引入可被特定條件觸發的 bug。</li>
<li>客戶推送有效配置，觸發 bug。</li>
<li>大範圍 edge 節點回應錯誤，形成全球 outage。</li>
<li>團隊定位並隔離觸發配置，服務逐步恢復。</li>
<li>事後回寫驗證、隔離與恢復流程。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Config-trigger safety gate</td>
          <td>有效配置也可觸發平台 bug</td>
          <td>對配置與平台交互條件增加回放測試</td>
      </tr>
      <tr>
          <td>Global propagation brake</td>
          <td>擴散速度遠快於局部人工止血</td>
          <td>建立全域停傳播與快速隔離機制</td>
      </tr>
      <tr>
          <td>Canary and staged rollout</td>
          <td>交互條件在前期驗證未被涵蓋</td>
          <td>強化灰度策略與跨場景驗證</td>
      </tr>
      <tr>
          <td>Incident communication timing</td>
          <td>影響廣但恢復快，對外節奏需精準</td>
          <td>以固定 cadence 說明影響範圍與恢復進度</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>規則/配置成本訊號： <a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 Rule-level CPU Signal Governance</a></li>
<li>證據包： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>規則推送閘門： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.fastly.com/blog/summary-of-june-8-outage">Summary of June 8 outage</a></li>
</ul>
]]></content:encoded></item><item><title>FinTech：合規壓力下的後端選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cases/fintech-compliance-and-selection-pressure/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cases/fintech-compliance-and-selection-pressure/</guid><description>&lt;p>這個案例的核心責任是把合規壓力轉成選型條件。FinTech 場景下，資料保留、審計追溯與交易一致性通常比純效能優先。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>audit evidence gap&lt;/td>
 &lt;td>稽核證據是否連續&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>duplicate transaction risk&lt;/td>
 &lt;td>重試是否可能造成雙重結果&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>release freeze frequency&lt;/td>
 &lt;td>發布是否常因風險臨時凍結&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="風險與邊界">風險與邊界&lt;/h2>
&lt;p>把合規當成部署後補強會抬高長期成本。較穩定的做法是在選型時就定義證據鏈、資料邊界與回復順序，避免後續跨模組反覆返工。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先補 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12&lt;/a> 的審計訊號，再用 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a> 定義合規變更門檻。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是把合規壓力轉成選型條件。FinTech 場景下，資料保留、審計追溯與交易一致性通常比純效能優先。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>audit evidence gap</td>
          <td>稽核證據是否連續</td>
          <td><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
      </tr>
      <tr>
          <td>duplicate transaction risk</td>
          <td>重試是否可能造成雙重結果</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>release freeze frequency</td>
          <td>發布是否常因風險臨時凍結</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
  </tbody>
</table>
<h2 id="風險與邊界">風險與邊界</h2>
<p>把合規當成部署後補強會抬高長期成本。較穩定的做法是在選型時就定義證據鏈、資料邊界與回復順序，避免後續跨模組反覆返工。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先補 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a> 的審計訊號，再用 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 定義合規變更門檻。</p>
]]></content:encoded></item><item><title>FinTech：審計證據鏈的可觀測性設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/</guid><description>&lt;p>本案例的核心責任是讓審計證據與運維訊號共用同一套資料邊界。FinTech 場景下，觀測資料不只是除錯用途，也是合規證據基礎。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一家處理線上支付的金融科技公司，每日交易量約 200 萬筆，涵蓋信用卡收單、轉帳與退款。每季有外部稽核查核交易處理的完整性與存取控制，事故發生時法務需要在 48 小時內提供特定交易的完整處理鏈證據。&lt;/p>
&lt;p>初期系統把所有 log 寫到同一個 log group — application debug、request trace、交易狀態變更與使用者存取紀錄全混在一起。稽核人員要從數 TB 的 log 中撈出特定交易的完整軌跡，每次查詢耗時數小時。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="operational-log-與-audit-log-混合">Operational log 與 audit log 混合&lt;/h3>
&lt;p>Application log 記錄 debug 資訊（SQL timing、cache hit/miss、retry），audit log 記錄業務事件（交易建立、狀態變更、存取紀錄）。兩者混在同一個 pipeline 時，retention 策略互相衝突 — debug log 留 14 天夠用，但 audit log 法規要求保留 5 年。統一設成 5 年讓儲存成本暴增，統一設成 14 天則遺失合規證據。&lt;/p>
&lt;h3 id="pii-暴露在-log-中">PII 暴露在 log 中&lt;/h3>
&lt;p>早期 log 直接印出 request body，信用卡號跟身分證字號散落在各種 log entry。稽核指出 PII 在 log 系統中的暴露面超過業務需要，但 log 已經寫入後無法回溯修改。&lt;/p>
&lt;h3 id="event-correlation-斷裂">Event correlation 斷裂&lt;/h3>
&lt;p>交易從建立到完成經過多個服務（checkout-api → payment-gateway → settlement → notification），但各服務的 log 使用不同的 correlation key。Checkout 用 &lt;code>order_id&lt;/code>，payment-gateway 用 &lt;code>payment_ref&lt;/code>，settlement 用自己的 &lt;code>batch_id&lt;/code>。稽核要求「給我交易 X 的完整處理鏈」時，工程師需要手動在三個系統各自查詢再人工拼接。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="audit-log-分離">Audit log 分離&lt;/h3>
&lt;p>把 audit event 獨立到專屬 pipeline：交易狀態變更、使用者存取、權限變動、退款操作各自產生結構化 audit event，寫入 immutable storage（append-only、禁止刪除與修改）。Operational log 維持 14 天 retention，audit log 走 5 年 retention + cold archive。&lt;/p>
&lt;p>分離的判準是「這筆紀錄是否可能被稽核或法務要求提供」。是 → audit pipeline；否 → operational pipeline。灰色地帶（例如認證失敗 log）歸入 audit pipeline — 寧可多留不可少留。&lt;/p>
&lt;h3 id="pii-redaction-pipeline">PII redaction pipeline&lt;/h3>
&lt;p>在 log ingestion 階段加入 redaction processor：信用卡號遮罩為末四碼、身分證字號完全移除、email 保留 domain 遮罩使用者名稱。Redaction 發生在寫入儲存之前，原始資料不落地。&lt;/p>
&lt;p>需要完整 PII 的場景（如詐欺調查）走另一條授權存取管道，跟觀測 pipeline 分離。&lt;/p>
&lt;h3 id="統一-correlation-key">統一 correlation key&lt;/h3>
&lt;p>所有服務在交易入口處產生 &lt;code>trace_id&lt;/code> 和 &lt;code>transaction_id&lt;/code>，兩個 key 同時寫入每一筆 audit event 和 operational log。稽核查詢用 &lt;code>transaction_id&lt;/code> 就能撈出跨服務的完整處理鏈，不需要手動拼接。&lt;/p>
&lt;h2 id="取捨">取捨&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>混合 pipeline&lt;/th>
 &lt;th>分離 pipeline&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>建置成本&lt;/td>
 &lt;td>低（一套 pipeline）&lt;/td>
 &lt;td>中（兩套 pipeline + routing 邏輯）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存成本&lt;/td>
 &lt;td>高（全部用最長 retention）&lt;/td>
 &lt;td>可控（各自 retention）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢效率&lt;/td>
 &lt;td>低（audit event 淹沒在 debug log 中）&lt;/td>
 &lt;td>高（audit 獨立查詢）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>合規風險&lt;/td>
 &lt;td>高（PII 暴露面大、retention 可能不足）&lt;/td>
 &lt;td>低（PII redacted、retention 對齊法規）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維運複雜度&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>中（需維護 routing 規則與 redaction 規則）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分離 pipeline 的最大成本在 routing 規則的維護 — 新服務上線時要確認 audit event 走對 pipeline。解法是在 SDK 層提供 &lt;code>emit_audit_event()&lt;/code> 函式，讓 routing 在 producer 端決定，不依賴下游 pipeline 的內容判斷。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是讓審計證據與運維訊號共用同一套資料邊界。FinTech 場景下，觀測資料不只是除錯用途，也是合規證據基礎。</p>
<h2 id="業務背景">業務背景</h2>
<p>一家處理線上支付的金融科技公司，每日交易量約 200 萬筆，涵蓋信用卡收單、轉帳與退款。每季有外部稽核查核交易處理的完整性與存取控制，事故發生時法務需要在 48 小時內提供特定交易的完整處理鏈證據。</p>
<p>初期系統把所有 log 寫到同一個 log group — application debug、request trace、交易狀態變更與使用者存取紀錄全混在一起。稽核人員要從數 TB 的 log 中撈出特定交易的完整軌跡，每次查詢耗時數小時。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="operational-log-與-audit-log-混合">Operational log 與 audit log 混合</h3>
<p>Application log 記錄 debug 資訊（SQL timing、cache hit/miss、retry），audit log 記錄業務事件（交易建立、狀態變更、存取紀錄）。兩者混在同一個 pipeline 時，retention 策略互相衝突 — debug log 留 14 天夠用，但 audit log 法規要求保留 5 年。統一設成 5 年讓儲存成本暴增，統一設成 14 天則遺失合規證據。</p>
<h3 id="pii-暴露在-log-中">PII 暴露在 log 中</h3>
<p>早期 log 直接印出 request body，信用卡號跟身分證字號散落在各種 log entry。稽核指出 PII 在 log 系統中的暴露面超過業務需要，但 log 已經寫入後無法回溯修改。</p>
<h3 id="event-correlation-斷裂">Event correlation 斷裂</h3>
<p>交易從建立到完成經過多個服務（checkout-api → payment-gateway → settlement → notification），但各服務的 log 使用不同的 correlation key。Checkout 用 <code>order_id</code>，payment-gateway 用 <code>payment_ref</code>，settlement 用自己的 <code>batch_id</code>。稽核要求「給我交易 X 的完整處理鏈」時，工程師需要手動在三個系統各自查詢再人工拼接。</p>
<h2 id="解法">解法</h2>
<h3 id="audit-log-分離">Audit log 分離</h3>
<p>把 audit event 獨立到專屬 pipeline：交易狀態變更、使用者存取、權限變動、退款操作各自產生結構化 audit event，寫入 immutable storage（append-only、禁止刪除與修改）。Operational log 維持 14 天 retention，audit log 走 5 年 retention + cold archive。</p>
<p>分離的判準是「這筆紀錄是否可能被稽核或法務要求提供」。是 → audit pipeline；否 → operational pipeline。灰色地帶（例如認證失敗 log）歸入 audit pipeline — 寧可多留不可少留。</p>
<h3 id="pii-redaction-pipeline">PII redaction pipeline</h3>
<p>在 log ingestion 階段加入 redaction processor：信用卡號遮罩為末四碼、身分證字號完全移除、email 保留 domain 遮罩使用者名稱。Redaction 發生在寫入儲存之前，原始資料不落地。</p>
<p>需要完整 PII 的場景（如詐欺調查）走另一條授權存取管道，跟觀測 pipeline 分離。</p>
<h3 id="統一-correlation-key">統一 correlation key</h3>
<p>所有服務在交易入口處產生 <code>trace_id</code> 和 <code>transaction_id</code>，兩個 key 同時寫入每一筆 audit event 和 operational log。稽核查詢用 <code>transaction_id</code> 就能撈出跨服務的完整處理鏈，不需要手動拼接。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>混合 pipeline</th>
          <th>分離 pipeline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建置成本</td>
          <td>低（一套 pipeline）</td>
          <td>中（兩套 pipeline + routing 邏輯）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>高（全部用最長 retention）</td>
          <td>可控（各自 retention）</td>
      </tr>
      <tr>
          <td>查詢效率</td>
          <td>低（audit event 淹沒在 debug log 中）</td>
          <td>高（audit 獨立查詢）</td>
      </tr>
      <tr>
          <td>合規風險</td>
          <td>高（PII 暴露面大、retention 可能不足）</td>
          <td>低（PII redacted、retention 對齊法規）</td>
      </tr>
      <tr>
          <td>維運複雜度</td>
          <td>低</td>
          <td>中（需維護 routing 規則與 redaction 規則）</td>
      </tr>
  </tbody>
</table>
<p>分離 pipeline 的最大成本在 routing 規則的維護 — 新服務上線時要確認 audit event 走對 pipeline。解法是在 SDK 層提供 <code>emit_audit_event()</code> 函式，讓 routing 在 producer 端決定，不依賴下游 pipeline 的內容判斷。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：audit log 分離的設計原則與 PII 治理。</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：把 audit trail 包成可交接的 evidence package。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：audit pipeline 的 ownership 歸 platform team 還是 compliance team。</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 key 的 propagation 設計。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>稽核或法務要求提供某筆交易的完整處理鏈，工程師需要超過 1 小時才能拼出來</li>
<li>Log retention 設定跟法規要求不一致，但沒人確切知道差多少</li>
<li>PII 出現在 log search 結果中，但沒有系統性的遮罩機制</li>
<li>Application log 跟 audit log 用同一套 retention policy，儲存成本持續上升但沒人敢縮短</li>
<li>事故後法務要證據，發現關鍵時段的 log 已經因為 retention 過期而被刪除</li>
</ul>
]]></content:encoded></item><item><title>GCP 2019 US Network Congestion Multi-service Incident</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/</guid><description>&lt;p>2019 年 GCP 網路壅塞事故的核心教訓是：當共享網路容量被打滿，影響會跨越產品邊界，同一時間出現在 compute、storage、observability 與管理面。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Google Cloud 在 2019-06-02 發生美國多區域 network congestion，官方摘要指出多個 US region 出現 elevated packet loss，影響持續約 3 至 4 小時以上，並牽動多個 GCP 與非 Cloud 服務。&lt;/p>
&lt;p>這類事故本質是共享網路資源退化造成的跨產品連鎖事件，單一服務壞掉反而好處理。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多區域 packet loss 同時上升&lt;/td>
 &lt;td>共享網路層失衡，不是單服務 bug&lt;/td>
 &lt;td>優先走區域隔離與流量調整路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多產品錯誤率一起上升&lt;/td>
 &lt;td>事故已跨產品依賴鏈擴散&lt;/td>
 &lt;td>事故分級以跨產品影響為主，而非單團隊視角&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部分 region 正常、部分 region 退化&lt;/td>
 &lt;td>區域差異可用來做流量重新分配&lt;/td>
 &lt;td>啟動 region-aware mitigation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>status page 更新中提到 varied impact&lt;/td>
 &lt;td>影響面非均勻分布&lt;/td>
 &lt;td>對外更新要分 region / service 粒度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>美國多區域網路容量在高壓下出現壅塞與丟包。&lt;/li>
&lt;li>多個 GCP 產品受同一網路瓶頸影響，出現延遲與錯誤。&lt;/li>
&lt;li>工程團隊進行流量與容量調整，逐區域回復。&lt;/li>
&lt;li>狀態頁持續更新受影響範圍與恢復進度。&lt;/li>
&lt;li>事後回寫區域隔離、容量保留與跨產品協調流程。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Region-aware traffic control&lt;/td>
 &lt;td>區域壅塞時流量轉移策略不夠快&lt;/td>
 &lt;td>建立區域流量切換的預設策略與演練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-product incident command&lt;/td>
 &lt;td>多產品同時受影響時協調成本高&lt;/td>
 &lt;td>強化跨產品指揮節奏與共享 decision log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network dependency mapping&lt;/td>
 &lt;td>服務依賴共享網路層但判讀入口分散&lt;/td>
 &lt;td>補跨產品依賴圖與共同告警面板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Status communication granularity&lt;/td>
 &lt;td>對外說明若只寫全域狀態會失真&lt;/td>
 &lt;td>更新按 region 與 service 分層揭露&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>觀測證據包： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>事中決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>實驗安全邊界： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://status.cloud.google.com/incident/cloud-networking/19009">Google Cloud Networking Incident #19009&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://cloud.google.com/blog/topics/inside-google-cloud/an-update-on-sundays-service-disruption">An update on Sunday’s service disruption&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2019 年 GCP 網路壅塞事故的核心教訓是：當共享網路容量被打滿，影響會跨越產品邊界，同一時間出現在 compute、storage、observability 與管理面。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Google Cloud 在 2019-06-02 發生美國多區域 network congestion，官方摘要指出多個 US region 出現 elevated packet loss，影響持續約 3 至 4 小時以上，並牽動多個 GCP 與非 Cloud 服務。</p>
<p>這類事故本質是共享網路資源退化造成的跨產品連鎖事件，單一服務壞掉反而好處理。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多區域 packet loss 同時上升</td>
          <td>共享網路層失衡，不是單服務 bug</td>
          <td>優先走區域隔離與流量調整路徑</td>
      </tr>
      <tr>
          <td>多產品錯誤率一起上升</td>
          <td>事故已跨產品依賴鏈擴散</td>
          <td>事故分級以跨產品影響為主，而非單團隊視角</td>
      </tr>
      <tr>
          <td>部分 region 正常、部分 region 退化</td>
          <td>區域差異可用來做流量重新分配</td>
          <td>啟動 region-aware mitigation</td>
      </tr>
      <tr>
          <td>status page 更新中提到 varied impact</td>
          <td>影響面非均勻分布</td>
          <td>對外更新要分 region / service 粒度</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>美國多區域網路容量在高壓下出現壅塞與丟包。</li>
<li>多個 GCP 產品受同一網路瓶頸影響，出現延遲與錯誤。</li>
<li>工程團隊進行流量與容量調整，逐區域回復。</li>
<li>狀態頁持續更新受影響範圍與恢復進度。</li>
<li>事後回寫區域隔離、容量保留與跨產品協調流程。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Region-aware traffic control</td>
          <td>區域壅塞時流量轉移策略不夠快</td>
          <td>建立區域流量切換的預設策略與演練</td>
      </tr>
      <tr>
          <td>Cross-product incident command</td>
          <td>多產品同時受影響時協調成本高</td>
          <td>強化跨產品指揮節奏與共享 decision log</td>
      </tr>
      <tr>
          <td>Network dependency mapping</td>
          <td>服務依賴共享網路層但判讀入口分散</td>
          <td>補跨產品依賴圖與共同告警面板</td>
      </tr>
      <tr>
          <td>Status communication granularity</td>
          <td>對外說明若只寫全域狀態會失真</td>
          <td>更新按 region 與 service 分層揭露</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>觀測證據包： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>事中決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>實驗安全邊界： <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://status.cloud.google.com/incident/cloud-networking/19009">Google Cloud Networking Incident #19009</a></li>
<li><a href="https://cloud.google.com/blog/topics/inside-google-cloud/an-update-on-sundays-service-disruption">An update on Sunday’s service disruption</a></li>
</ul>
]]></content:encoded></item><item><title>GitHub 2018 Oct21 MySQL Topology Incident</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/</guid><description>&lt;p>2018 年 GitHub Oct21 事故的核心教訓是：跨區資料庫在 network partition 後，最困難的是如何在可用性與資料一致性之間做出可回放的決策，切換本身只是其中一步。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>GitHub 在 2018-10-21 22:52 UTC 因例行網路設備維護引發 network partition，導致跨區 MySQL replication topology 進入異常狀態。應用層在切換後持續寫入新主站，形成跨區未對齊寫入，事故最終歷時約 24 小時 11 分鐘。&lt;/p>
&lt;p>官方 post-incident analysis 指出，團隊選擇 fail-forward，而不是直接切回原主站，原因是要優先保護資料完整性，避免產生更大不一致。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多個服務同時顯示資料過舊或不一致&lt;/td>
 &lt;td>replication topology 已跨區失衡&lt;/td>
 &lt;td>先凍結變更與部署，避免拓撲再變化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Orchestrator 顯示非預期跨區主從關係&lt;/td>
 &lt;td>自動切換已進入複雜狀態&lt;/td>
 &lt;td>轉人工決策，先保資料一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>webhook / Pages backlog 快速累積&lt;/td>
 &lt;td>控制面與資料面都受影響&lt;/td>
 &lt;td>將積壓處理納入恢復計畫，而非只看 API 健康度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>status 更新頻率下降&lt;/td>
 &lt;td>指揮資訊與恢復節奏未對齊&lt;/td>
 &lt;td>補 decision log 與分階段狀態更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>例行網路設備維護造成 East 與主資料中心連線中斷。&lt;/li>
&lt;li>Orchestrator 在 partition 下進行主從重新選舉與切換。&lt;/li>
&lt;li>連線恢復後，應用寫入已落在新主站，形成跨站寫入差異。&lt;/li>
&lt;li>團隊凍結部署並轉人工處理拓撲與一致性風險。&lt;/li>
&lt;li>選擇 fail-forward，逐步恢復服務與處理 backlog。&lt;/li>
&lt;li>事故結束後回寫跨資料中心設計、通訊粒度與演練策略。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Cross-DC replication guardrail&lt;/td>
 &lt;td>partition 後拓撲變更過快&lt;/td>
 &lt;td>增加拓撲變更保護與人工切換門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency-first decision path&lt;/td>
 &lt;td>可用性與一致性取捨缺標準化準則&lt;/td>
 &lt;td>在 decision log 固定記錄 fail-forward / fail-back 判準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backlog recovery strategy&lt;/td>
 &lt;td>webhook / Pages 積壓恢復節奏缺共識&lt;/td>
 &lt;td>將 backlog drain 納入 recovery completion 定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident communication granularity&lt;/td>
 &lt;td>只用單一顏色狀態無法表達部分恢復&lt;/td>
 &lt;td>對外更新按子服務與恢復階段拆分&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>止血與回復： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy&lt;/a>&lt;/li>
&lt;li>事中決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>資料庫轉換實作： &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作&lt;/a>&lt;/li>
&lt;li>Migration rollout evidence： &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據&lt;/a>&lt;/li>
&lt;li>選型決策層： &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換&lt;/a>&lt;/li>
&lt;li>穩態與恢復完成： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.blog/2018-10-30-oct21-post-incident-analysis/">October 21 post-incident analysis&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.blog/news-insights/company-news/october21-incident-report/">October 21 Incident Report&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2018 年 GitHub Oct21 事故的核心教訓是：跨區資料庫在 network partition 後，最困難的是如何在可用性與資料一致性之間做出可回放的決策，切換本身只是其中一步。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>GitHub 在 2018-10-21 22:52 UTC 因例行網路設備維護引發 network partition，導致跨區 MySQL replication topology 進入異常狀態。應用層在切換後持續寫入新主站，形成跨區未對齊寫入，事故最終歷時約 24 小時 11 分鐘。</p>
<p>官方 post-incident analysis 指出，團隊選擇 fail-forward，而不是直接切回原主站，原因是要優先保護資料完整性，避免產生更大不一致。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多個服務同時顯示資料過舊或不一致</td>
          <td>replication topology 已跨區失衡</td>
          <td>先凍結變更與部署，避免拓撲再變化</td>
      </tr>
      <tr>
          <td>Orchestrator 顯示非預期跨區主從關係</td>
          <td>自動切換已進入複雜狀態</td>
          <td>轉人工決策，先保資料一致性</td>
      </tr>
      <tr>
          <td>webhook / Pages backlog 快速累積</td>
          <td>控制面與資料面都受影響</td>
          <td>將積壓處理納入恢復計畫，而非只看 API 健康度</td>
      </tr>
      <tr>
          <td>status 更新頻率下降</td>
          <td>指揮資訊與恢復節奏未對齊</td>
          <td>補 decision log 與分階段狀態更新</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>例行網路設備維護造成 East 與主資料中心連線中斷。</li>
<li>Orchestrator 在 partition 下進行主從重新選舉與切換。</li>
<li>連線恢復後，應用寫入已落在新主站，形成跨站寫入差異。</li>
<li>團隊凍結部署並轉人工處理拓撲與一致性風險。</li>
<li>選擇 fail-forward，逐步恢復服務與處理 backlog。</li>
<li>事故結束後回寫跨資料中心設計、通訊粒度與演練策略。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cross-DC replication guardrail</td>
          <td>partition 後拓撲變更過快</td>
          <td>增加拓撲變更保護與人工切換門檻</td>
      </tr>
      <tr>
          <td>Consistency-first decision path</td>
          <td>可用性與一致性取捨缺標準化準則</td>
          <td>在 decision log 固定記錄 fail-forward / fail-back 判準</td>
      </tr>
      <tr>
          <td>Backlog recovery strategy</td>
          <td>webhook / Pages 積壓恢復節奏缺共識</td>
          <td>將 backlog drain 納入 recovery completion 定義</td>
      </tr>
      <tr>
          <td>Incident communication granularity</td>
          <td>只用單一顏色狀態無法表達部分恢復</td>
          <td>對外更新按子服務與恢復階段拆分</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>止血與回復： <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy</a></li>
<li>事中決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>資料庫轉換實作： <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a></li>
<li>Migration rollout evidence： <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a></li>
<li>選型決策層： <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a></li>
<li>穩態與恢復完成： <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://github.blog/2018-10-30-oct21-post-incident-analysis/">October 21 post-incident analysis</a></li>
<li><a href="https://github.blog/news-insights/company-news/october21-incident-report/">October 21 Incident Report</a></li>
</ul>
]]></content:encoded></item><item><title>Roblox 2021 Oct Prolonged Core Infra Outage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/</guid><description>&lt;p>Roblox 2021 事故的核心教訓是：當核心基礎設施在高壓下進入非預期行為，真正困難的不只是修復，而是如何在不確定根因下維持可驗證的恢復節奏。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Roblox 在 2021-10-28 至 2021-10-31 經歷長時間服務中斷。官方更新指出問題來自內部系統在高負載下的細微通訊 bug 與連鎖壓力，不是外部攻擊或流量尖峰事件。&lt;/p>
&lt;p>這類 prolonged outage 的特徵是：初期根因不明、修復需分階段、恢復後仍有長尾調整。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>平台大面積連線與操作失敗&lt;/td>
 &lt;td>核心控制面/基礎設施層失衡&lt;/td>
 &lt;td>立即升級全域 incident&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復後效能仍不穩&lt;/td>
 &lt;td>長尾恢復尚未完成&lt;/td>
 &lt;td>分階段恢復，不一次全開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>根因定位時間長&lt;/td>
 &lt;td>觀測與依賴圖對核心路徑解釋力不足&lt;/td>
 &lt;td>把證據收集與假設驗證納入主流程&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;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>平台在高負載場景下出現核心基礎設施壓力失衡。&lt;/li>
&lt;li>使用者面大量失敗，服務不可用。&lt;/li>
&lt;li>團隊跨功能長時間排查、逐步恢復基礎能力。&lt;/li>
&lt;li>恢復後持續做長尾穩定化與後續結構改善。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Core dependency observability&lt;/td>
 &lt;td>核心依賴壓力與瓶頸判讀太慢&lt;/td>
 &lt;td>強化核心路徑監測與跨層證據對位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prolonged incident command&lt;/td>
 &lt;td>長事故下節奏與交班壓力高&lt;/td>
 &lt;td>強化 IC handoff 與長事故節奏治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery stage definition&lt;/td>
 &lt;td>恢復完成判準不足導致反覆調整&lt;/td>
 &lt;td>用 steady state 定義分階段恢復門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Post-incident structural write-back&lt;/td>
 &lt;td>根因修補之外缺少結構性改進路徑&lt;/td>
 &lt;td>把改進落到容量、架構隔離與演練題目&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>止血與回復： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>長事故交班： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12 IC Handoff&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>穩態與恢復完成： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://corp.roblox.com/newsroom/2021/10/update-recent-service-outage/">An Update on Our Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://corp.roblox.com/fr/salledepresse/2022/01/roblox-return-to-service-10-28-10-31-2021">Roblox Return to Service&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Roblox 2021 事故的核心教訓是：當核心基礎設施在高壓下進入非預期行為，真正困難的不只是修復，而是如何在不確定根因下維持可驗證的恢復節奏。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Roblox 在 2021-10-28 至 2021-10-31 經歷長時間服務中斷。官方更新指出問題來自內部系統在高負載下的細微通訊 bug 與連鎖壓力，不是外部攻擊或流量尖峰事件。</p>
<p>這類 prolonged outage 的特徵是：初期根因不明、修復需分階段、恢復後仍有長尾調整。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台大面積連線與操作失敗</td>
          <td>核心控制面/基礎設施層失衡</td>
          <td>立即升級全域 incident</td>
      </tr>
      <tr>
          <td>修復後效能仍不穩</td>
          <td>長尾恢復尚未完成</td>
          <td>分階段恢復，不一次全開</td>
      </tr>
      <tr>
          <td>根因定位時間長</td>
          <td>觀測與依賴圖對核心路徑解釋力不足</td>
          <td>把證據收集與假設驗證納入主流程</td>
      </tr>
      <tr>
          <td>後續公開長文回顧改善方向</td>
          <td>需要結構性回寫而非單次修補</td>
          <td>回寫到觀測、演練與基礎設施治理</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>平台在高負載場景下出現核心基礎設施壓力失衡。</li>
<li>使用者面大量失敗，服務不可用。</li>
<li>團隊跨功能長時間排查、逐步恢復基礎能力。</li>
<li>恢復後持續做長尾穩定化與後續結構改善。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core dependency observability</td>
          <td>核心依賴壓力與瓶頸判讀太慢</td>
          <td>強化核心路徑監測與跨層證據對位</td>
      </tr>
      <tr>
          <td>Prolonged incident command</td>
          <td>長事故下節奏與交班壓力高</td>
          <td>強化 IC handoff 與長事故節奏治理</td>
      </tr>
      <tr>
          <td>Recovery stage definition</td>
          <td>恢復完成判準不足導致反覆調整</td>
          <td>用 steady state 定義分階段恢復門檻</td>
      </tr>
      <tr>
          <td>Post-incident structural write-back</td>
          <td>根因修補之外缺少結構性改進路徑</td>
          <td>把改進落到容量、架構隔離與演練題目</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>止血與回復： <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>長事故交班： <a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12 IC Handoff</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>穩態與恢復完成： <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://corp.roblox.com/newsroom/2021/10/update-recent-service-outage/">An Update on Our Outage</a></li>
<li><a href="https://corp.roblox.com/fr/salledepresse/2022/01/roblox-return-to-service-10-28-10-31-2021">Roblox Return to Service</a></li>
</ul>
]]></content:encoded></item><item><title>AWS S3</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/</guid><description>&lt;p>AWS S3 是物件儲存的事實標準、區域控制面失效會大規模擴散到下游服務、是區域依賴 / blast radius / 控制面 vs 資料面分離的教學標竿。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>區域依賴擴散：S3 us-east-1 失效會牽動 console、IAM、ECR、CloudFormation 等控制面&lt;/li>
&lt;li>Blast radius 範例：subsystem 失效如何意外擴散到看似無關服務&lt;/li>
&lt;li>控制面 / 資料面分離設計：為何 S3 把兩者拆開、失效時表現差異&lt;/li>
&lt;li>Recovery 節奏：metadata service 重啟為何耗時、為何不能熱重啟&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2017&lt;/td>
 &lt;td>us-east-1 typo 4 小時&lt;/td>
 &lt;td>內部工具誤觸、區域依賴擴散&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2021&lt;/td>
 &lt;td>us-east-1 多服務退化&lt;/td>
 &lt;td>控制面與下游服務的隱性耦合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2023&lt;/td>
 &lt;td>其他 AWS 公開摘要&lt;/td>
 &lt;td>比對 AWS post-incident report 的格式變化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/" data-link-title="AWS 2021 US-EAST-1 Control Plane Degradation" data-link-desc="2021-12-07 AWS us-east-1 控制面退化案例：內部網路壅塞、API 錯誤率升高、跨服務依賴連鎖與通訊節奏調整。">2021 US-EAST-1 Control Plane Degradation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/" data-link-title="AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）" data-link-desc="以 AWS 2023 年公開事件樣式為主，整理 control plane 退化時如何建立責任邊界、決策紀錄與對外更新節奏。">2023 Control Plane Accountability and Communication Pattern&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/" data-link-title="AWS 2021 US-EAST-1 Control Plane Degradation" data-link-desc="2021-12-07 AWS us-east-1 控制面退化案例：內部網路壅塞、API 錯誤率升高、跨服務依賴連鎖與通訊節奏調整。">2021 US-EAST-1 Control Plane Degradation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/" data-link-title="AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）" data-link-desc="以 AWS 2023 年公開事件樣式為主，整理 control plane 退化時如何建立責任邊界、決策紀錄與對外更新節奏。">2023 Control Plane Accountability and Communication Pattern&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>AWS S3 這個案例在講的是區域控制面失效如何透過依賴鏈條放大成多服務事故。讀者先看懂控制面與資料面分離的責任，再把 us-east-1 這類事件當成 blast radius 與恢復節奏的教學範本。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當內部工具誤觸或控制面出現異常時，第一件事是先切開受影響的依賴路徑，擴容在此階段幫助有限。當服務恢復時，metadata service 與下游依賴通常不會同時回穩，所以恢復順序比單純重啟更重要。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否分辨故障落在控制面還是資料面&lt;/li>
&lt;li>能否指出哪個依賴把事故擴成區域事件&lt;/li>
&lt;li>能否把恢復順序寫成可執行的 runbook&lt;/li>
&lt;li>能否在復原後回頭檢查 blast radius 是否被正確限制&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>AWS S3 是區域控制面事故的基準頁，和 Cloudflare、Fastly、GCP 一起讀時，最能看出「小變更如何變成大擴散」。這頁也能拿來對照 GitHub 與 Azure AD，因為它們同樣在處理共享依賴被一個節點拖垮後的恢復節奏。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2017 年 us-east-1 typo 事故顯示單一控制面誤觸可以牽動整個區域。&lt;/li>
&lt;li>2021 年 us-east-1 多服務退化則示範了控制面與下游服務如何一起受影響。&lt;/li>
&lt;li>其他公開 PIR 可以拿來對照 AWS 的回顧格式如何隨時間演化。&lt;/li>
&lt;li>S3 的案例也能對照控制面與資料面拆分後的恢復順序。&lt;/li>
&lt;li>metadata service 的恢復節奏常常比使用者看到的 outage 更長。&lt;/li>
&lt;li>region dependency 讓看似獨立的 AWS 服務一起進入失效鏈。&lt;/li>
&lt;li>blast radius 的核心是依賴鏈條被拉長後的擴散，單一服務層面的評估不足以涵蓋。&lt;/li>
&lt;li>post-incident report 的寫法能對照 AWS 如何對外說明與內部修復。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/message/41926/">Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region&lt;/a>：2017 年 S3 us-east-1 事故的官方摘要與時間線。&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/about-aws/whats-new/2019/12/introducing-amazon-builders-library/">Introducing The Amazon Builders’ Library&lt;/a>：S3 類事故所屬的大型系統操作與恢復脈絡。&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding&lt;/a>：補 blast radius 與隔離思路。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>AWS S3 是物件儲存的事實標準、區域控制面失效會大規模擴散到下游服務、是區域依賴 / blast radius / 控制面 vs 資料面分離的教學標竿。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>區域依賴擴散：S3 us-east-1 失效會牽動 console、IAM、ECR、CloudFormation 等控制面</li>
<li>Blast radius 範例：subsystem 失效如何意外擴散到看似無關服務</li>
<li>控制面 / 資料面分離設計：為何 S3 把兩者拆開、失效時表現差異</li>
<li>Recovery 節奏：metadata service 重啟為何耗時、為何不能熱重啟</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2017</td>
          <td>us-east-1 typo 4 小時</td>
          <td>內部工具誤觸、區域依賴擴散</td>
      </tr>
      <tr>
          <td>2021</td>
          <td>us-east-1 多服務退化</td>
          <td>控制面與下游服務的隱性耦合</td>
      </tr>
      <tr>
          <td>2023</td>
          <td>其他 AWS 公開摘要</td>
          <td>比對 AWS post-incident report 的格式變化</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption</a></li>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/" data-link-title="AWS 2021 US-EAST-1 Control Plane Degradation" data-link-desc="2021-12-07 AWS us-east-1 控制面退化案例：內部網路壅塞、API 錯誤率升高、跨服務依賴連鎖與通訊節奏調整。">2021 US-EAST-1 Control Plane Degradation</a></li>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/" data-link-title="AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）" data-link-desc="以 AWS 2023 年公開事件樣式為主，整理 control plane 退化時如何建立責任邊界、決策紀錄與對外更新節奏。">2023 Control Plane Accountability and Communication Pattern</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption</a></li>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/" data-link-title="AWS 2021 US-EAST-1 Control Plane Degradation" data-link-desc="2021-12-07 AWS us-east-1 控制面退化案例：內部網路壅塞、API 錯誤率升高、跨服務依賴連鎖與通訊節奏調整。">2021 US-EAST-1 Control Plane Degradation</a></li>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/" data-link-title="AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）" data-link-desc="以 AWS 2023 年公開事件樣式為主，整理 control plane 退化時如何建立責任邊界、決策紀錄與對外更新節奏。">2023 Control Plane Accountability and Communication Pattern</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>AWS S3 這個案例在講的是區域控制面失效如何透過依賴鏈條放大成多服務事故。讀者先看懂控制面與資料面分離的責任，再把 us-east-1 這類事件當成 blast radius 與恢復節奏的教學範本。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當內部工具誤觸或控制面出現異常時，第一件事是先切開受影響的依賴路徑，擴容在此階段幫助有限。當服務恢復時，metadata service 與下游依賴通常不會同時回穩，所以恢復順序比單純重啟更重要。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否分辨故障落在控制面還是資料面</li>
<li>能否指出哪個依賴把事故擴成區域事件</li>
<li>能否把恢復順序寫成可執行的 runbook</li>
<li>能否在復原後回頭檢查 blast radius 是否被正確限制</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>AWS S3 是區域控制面事故的基準頁，和 Cloudflare、Fastly、GCP 一起讀時，最能看出「小變更如何變成大擴散」。這頁也能拿來對照 GitHub 與 Azure AD，因為它們同樣在處理共享依賴被一個節點拖垮後的恢復節奏。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2017 年 us-east-1 typo 事故顯示單一控制面誤觸可以牽動整個區域。</li>
<li>2021 年 us-east-1 多服務退化則示範了控制面與下游服務如何一起受影響。</li>
<li>其他公開 PIR 可以拿來對照 AWS 的回顧格式如何隨時間演化。</li>
<li>S3 的案例也能對照控制面與資料面拆分後的恢復順序。</li>
<li>metadata service 的恢復節奏常常比使用者看到的 outage 更長。</li>
<li>region dependency 讓看似獨立的 AWS 服務一起進入失效鏈。</li>
<li>blast radius 的核心是依賴鏈條被拉長後的擴散，單一服務層面的評估不足以涵蓋。</li>
<li>post-incident report 的寫法能對照 AWS 如何對外說明與內部修復。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/message/41926/">Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region</a>：2017 年 S3 us-east-1 事故的官方摘要與時間線。</li>
<li><a href="https://aws.amazon.com/about-aws/whats-new/2019/12/introducing-amazon-builders-library/">Introducing The Amazon Builders’ Library</a>：S3 類事故所屬的大型系統操作與恢復脈絡。</li>
<li><a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding</a>：補 blast radius 與隔離思路。</li>
</ul>
]]></content:encoded></item><item><title>GitHub Actions</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/</guid><description>&lt;p>GitHub Actions 是 GitHub 原生的 CI/CD 工具、承擔三個責任：PR check workflow（test / lint / coverage）、release 自動化 + environment protection rules、跨 platform matrix testing。設計取捨偏向「跟 GitHub 深度整合 + marketplace action 生態 + OIDC 認證雲端 + self-hosted runner」、是 GitHub-hosted 專案的預設 CI 選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 workflow（.github/workflows/*.yml）&lt;/li>
&lt;li>設計 PR check + matrix testing&lt;/li>
&lt;li>用 reusable workflows / composite actions 復用&lt;/li>
&lt;li>配置 environment protection + approval gate&lt;/li>
&lt;li>用 OIDC + cloud auth（無 long-lived secret）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-github-actions-跑起來">最短路徑：5 分鐘把 GitHub Actions 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c"># .github/workflows/ci.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">pull_request]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npm test&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="workflow-設計">Workflow 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>on triggers（push / pull_request / schedule / workflow_dispatch / repository_dispatch）&lt;/li>
&lt;li>job / step / action&lt;/li>
&lt;li>Matrix（OS / language version / test split）&lt;/li>
&lt;li>對應指令範例：&lt;code>gh workflow run&lt;/code>、&lt;code>gh run list&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="cache-策略">Cache 策略&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>actions/cache（語言依賴 / build cache）&lt;/li>
&lt;li>Cache key 設計（hashFiles + version）&lt;/li>
&lt;li>Cache scope（per branch / per repo）&lt;/li>
&lt;li>對應 build speed optimization&lt;/li>
&lt;/ul>
&lt;h3 id="reusable-workflows--composite-actions">Reusable workflows / composite actions&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Reusable workflow：跨 repo 引用整個 workflow&lt;/li>
&lt;li>Composite action：把多 step 包成 action&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge cards reusable-action&lt;/a> (對應 DRY)&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="self-hosted-runner">Self-hosted runner&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>內網資源 / 特殊硬體（GPU）/ macOS&lt;/li>
&lt;li>Runner group + scaling&lt;/li>
&lt;li>Security：ephemeral runner（每次新建）&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="oidc--cloud-auth">OIDC + cloud auth&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>GitHub OIDC provider&lt;/li>
&lt;li>AWS / GCP / Azure 信任 GitHub&lt;/li>
&lt;li>無 long-lived access key&lt;/li>
&lt;li>對應 supply chain security&lt;/li>
&lt;/ul>
&lt;h3 id="environment-protection">Environment protection&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>GitHub Actions 是 GitHub 原生的 CI/CD 工具、承擔三個責任：PR check workflow（test / lint / coverage）、release 自動化 + environment protection rules、跨 platform matrix testing。設計取捨偏向「跟 GitHub 深度整合 + marketplace action 生態 + OIDC 認證雲端 + self-hosted runner」、是 GitHub-hosted 專案的預設 CI 選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 workflow（.github/workflows/*.yml）</li>
<li>設計 PR check + matrix testing</li>
<li>用 reusable workflows / composite actions 復用</li>
<li>配置 environment protection + approval gate</li>
<li>用 OIDC + cloud auth（無 long-lived secret）</li>
</ol>
<h2 id="最短路徑5-分鐘把-github-actions-跑起來">最短路徑：5 分鐘把 GitHub Actions 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># .github/workflows/ci.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pull_request]</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm test</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="workflow-設計">Workflow 設計</h3>
<p>子議題：</p>
<ul>
<li>on triggers（push / pull_request / schedule / workflow_dispatch / repository_dispatch）</li>
<li>job / step / action</li>
<li>Matrix（OS / language version / test split）</li>
<li>對應指令範例：<code>gh workflow run</code>、<code>gh run list</code></li>
</ul>
<h3 id="cache-策略">Cache 策略</h3>
<p>子議題：</p>
<ul>
<li>actions/cache（語言依賴 / build cache）</li>
<li>Cache key 設計（hashFiles + version）</li>
<li>Cache scope（per branch / per repo）</li>
<li>對應 build speed optimization</li>
</ul>
<h3 id="reusable-workflows--composite-actions">Reusable workflows / composite actions</h3>
<p>子議題：</p>
<ul>
<li>Reusable workflow：跨 repo 引用整個 workflow</li>
<li>Composite action：把多 step 包成 action</li>
<li>對應 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge cards reusable-action</a> (對應 DRY)</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="self-hosted-runner">Self-hosted runner</h3>
<p>子議題：</p>
<ul>
<li>內網資源 / 特殊硬體（GPU）/ macOS</li>
<li>Runner group + scaling</li>
<li>Security：ephemeral runner（每次新建）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a></li>
</ul>
<h3 id="oidc--cloud-auth">OIDC + cloud auth</h3>
<p>子議題：</p>
<ul>
<li>GitHub OIDC provider</li>
<li>AWS / GCP / Azure 信任 GitHub</li>
<li>無 long-lived access key</li>
<li>對應 supply chain security</li>
</ul>
<h3 id="environment-protection">Environment protection</h3>
<p>子議題：</p>
<ul>
<li>environment（dev / staging / prod）</li>
<li>Required reviewers</li>
<li>Wait timer</li>
<li>Secrets per-environment</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
</ul>
<h3 id="workflow-security">Workflow security</h3>
<p>子議題：</p>
<ul>
<li>pull_request vs pull_request_target（後者有 secrets / 危險）</li>
<li>third-party action pinning（commit SHA）</li>
<li>GITHUB_TOKEN permissions（最小化）</li>
</ul>
<h3 id="deploy-workflow">Deploy workflow</h3>
<p>子議題：</p>
<ul>
<li>Deploy on tag / release</li>
<li>Rolling deploy / blue-green / canary</li>
<li>Rollback action</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="workflow-沒觸發">Workflow 沒觸發</h3>
<p>操作原則：on trigger 配置 / branch filter / paths filter。判讀：Actions tab 看 trigger event。</p>
<h3 id="permission-denied">Permission denied</h3>
<p>操作原則：GITHUB_TOKEN permissions 不夠。判讀：workflow 加 permissions: 區段。</p>
<h3 id="cache-miss">Cache miss</h3>
<p>操作原則：cache key 不穩定 / hashFiles input 變化。</p>
<h3 id="secret-沒生效">Secret 沒生效</h3>
<p>操作原則：secret name / environment 不對 / pull_request from fork 不能用 secret。</p>
<h3 id="self-hosted-runner-卡住">Self-hosted runner 卡住</h3>
<p>操作原則：runner offline / job queue 滿 / runner group 配置不對。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>進階 cache / parallelism</td>
          <td><a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a></td>
      </tr>
      <tr>
          <td>非 GitHub-hosted</td>
          <td>GitLab CI / Bitbucket Pipelines / CircleCI</td>
      </tr>
      <tr>
          <td>Self-hosted enterprise</td>
          <td>Jenkins / Buildkite / Tekton</td>
      </tr>
      <tr>
          <td>複雜 pipeline DAG</td>
          <td>Tekton / Argo Workflows</td>
      </tr>
      <tr>
          <td>Bazel-native CI</td>
          <td>BuildBuddy / EngFlow</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Marketplace action 細節</li>
<li>GitHub Enterprise self-host</li>
<li>Actions pricing</li>
<li>各語言 setup-* action 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 與 Release Gating</a></td>
          <td>把 SLO 消耗轉成 release gate / freeze 的 workflow 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>canary deploy / staged rollout 的 CI 節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft：變更治理與可靠性門檻</a></td>
          <td>environment protection + approval gate 對應變更分層</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 GitHub Actions customer case</strong>：大規模 monorepo Actions 採用、OIDC migration、self-hosted runner scaling 案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>（supply chain）、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment</a>（deploy gate）</li>
</ul>
]]></content:encoded></item><item><title>Google</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/</guid><description>&lt;p>Google 是 SRE 概念的原始來源、SRE Book 與 Workbook 是領域 canonical text。教學重點在 SRE 工程文化、量化方法與組織節奏，單一事故只是入口。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>SLI / SLO / Error Budget：可靠性目標的量化方法、為何選 SLO 而非 100%&lt;/li>
&lt;li>Postmortem 文化：blameless / action items / 行動追蹤的閉環設計&lt;/li>
&lt;li>Toil 量化：把運維工作變成可預算的工程資產&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 與 burnout：值班輪值、shadow / primary 結構、心理安全&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> review：服務上線前的 SRE 接管門檻&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SRE Book Ch.1-4&lt;/td>
 &lt;td>概念基礎、為何 SLO、為何 50/50&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Postmortem Culture&lt;/td>
 &lt;td>blameless 操作化、action items 追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Toil &amp;amp; Engineering Time&lt;/td>
 &lt;td>量化 toil、長期投資工程的政策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hierarchy of Reliability&lt;/td>
 &lt;td>Monitoring → IR → PIR → Testing → Capacity → Dev → Product&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedded SRE / Consulting&lt;/td>
 &lt;td>SRE 介入服務的多種模式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">G1&lt;/a>&lt;/td>
 &lt;td>Error Budget 與 Release Gating&lt;/td>
 &lt;td>把 SLO 消耗量轉成放行、限速與凍結決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2&lt;/a>&lt;/td>
 &lt;td>Postmortem Closure 治理&lt;/td>
 &lt;td>把事故改進項變成可追蹤、可驗證的治理節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">G3&lt;/a>&lt;/td>
 &lt;td>Toil Budget 投資政策&lt;/td>
 &lt;td>把手動運維工作轉成可預算、可回寫的工程投資&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Google 這個案例在講的是可靠性如何變成一套可操作的工程制度，而不是單一工具或單一事故。讀者先抓到 SLI / SLO、error budget、postmortem 與 toil 這幾個原語各自負責什麼，再把它們組成一條可執行的可靠性路徑。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當服務健康開始波動時，先看 SLO 是否真的被消耗，再看監控與告警是否能對應到使用者體感。當 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 壓力升高時，重點在團隊是否把重複性工作轉成可預算的工程投資，個人技巧層面的改善幫助有限。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否用一句話說明每個 SLI 對應的使用者行為&lt;/li>
&lt;li>能否從 postmortem 找到明確 owner 與完成條件&lt;/li>
&lt;li>能否把 toil 量化成可排程的工程時間&lt;/li>
&lt;li>能否把監控、測試、容量、開發與產品決策串成同一條路由&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Google 提供的是可靠性的語言層，其他案例提供的是具體場景層。當讀者先懂 SLI / SLO 與 postmortem 這組原語，再看 Honeycomb 的 burn rate、Atlassian 的復原節奏或 GitHub 的 status communication，就能把抽象制度接到實際事故上。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>SLO 與 error budget 讓團隊把可靠性變成可量化的工程目標。&lt;/li>
&lt;li>postmortem 將事故轉成可追蹤的 action items，而不是只留下檢討文字。&lt;/li>
&lt;li>toil budget 讓重複性工作變成可預算的工程投資。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> review 讓服務在上線前先過可靠性門檻。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 與 burnout 讓值班成為組織設計問題，脫離個人耐力測試的框架。&lt;/li>
&lt;li>hierarchy of reliability 讓 monitoring、testing、capacity、dev、product 串成一條路由。&lt;/li>
&lt;li>blameless culture 讓檢討聚焦在系統與流程，而不是個人責任。&lt;/li>
&lt;li>embedded SRE / consulting 讓可靠性能力可以以不同介入深度落到服務團隊。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sre.google/">sre.google&lt;/a>：Google SRE 官方資源入口，收錄 books 與主題更新。&lt;/li>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/devops-sre/the-sre-book-turns-6">The SRE book turns 6!&lt;/a>：整理 SRE Book / Workbook 與延伸資源的官方入口。&lt;/li>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/devops-sre/how-to-design-good-slos-according-to-google-sres">Adopting SRE: Standardizing your SLO design process&lt;/a>：補 SLO 設計方法與實務語境。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Google 是 SRE 概念的原始來源、SRE Book 與 Workbook 是領域 canonical text。教學重點在 SRE 工程文化、量化方法與組織節奏，單一事故只是入口。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>SLI / SLO / Error Budget：可靠性目標的量化方法、為何選 SLO 而非 100%</li>
<li>Postmortem 文化：blameless / action items / 行動追蹤的閉環設計</li>
<li>Toil 量化：把運維工作變成可預算的工程資產</li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 與 burnout：值班輪值、shadow / primary 結構、心理安全</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> review：服務上線前的 SRE 接管門檻</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SRE Book Ch.1-4</td>
          <td>概念基礎、為何 SLO、為何 50/50</td>
      </tr>
      <tr>
          <td>Postmortem Culture</td>
          <td>blameless 操作化、action items 追蹤</td>
      </tr>
      <tr>
          <td>Toil &amp; Engineering Time</td>
          <td>量化 toil、長期投資工程的政策</td>
      </tr>
      <tr>
          <td>Hierarchy of Reliability</td>
          <td>Monitoring → IR → PIR → Testing → Capacity → Dev → Product</td>
      </tr>
      <tr>
          <td>Embedded SRE / Consulting</td>
          <td>SRE 介入服務的多種模式</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">G1</a></td>
          <td>Error Budget 與 Release Gating</td>
          <td>把 SLO 消耗量轉成放行、限速與凍結決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2</a></td>
          <td>Postmortem Closure 治理</td>
          <td>把事故改進項變成可追蹤、可驗證的治理節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">G3</a></td>
          <td>Toil Budget 投資政策</td>
          <td>把手動運維工作轉成可預算、可回寫的工程投資</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Google 這個案例在講的是可靠性如何變成一套可操作的工程制度，而不是單一工具或單一事故。讀者先抓到 SLI / SLO、error budget、postmortem 與 toil 這幾個原語各自負責什麼，再把它們組成一條可執行的可靠性路徑。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當服務健康開始波動時，先看 SLO 是否真的被消耗，再看監控與告警是否能對應到使用者體感。當 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 壓力升高時，重點在團隊是否把重複性工作轉成可預算的工程投資，個人技巧層面的改善幫助有限。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否用一句話說明每個 SLI 對應的使用者行為</li>
<li>能否從 postmortem 找到明確 owner 與完成條件</li>
<li>能否把 toil 量化成可排程的工程時間</li>
<li>能否把監控、測試、容量、開發與產品決策串成同一條路由</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Google 提供的是可靠性的語言層，其他案例提供的是具體場景層。當讀者先懂 SLI / SLO 與 postmortem 這組原語，再看 Honeycomb 的 burn rate、Atlassian 的復原節奏或 GitHub 的 status communication，就能把抽象制度接到實際事故上。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>SLO 與 error budget 讓團隊把可靠性變成可量化的工程目標。</li>
<li>postmortem 將事故轉成可追蹤的 action items，而不是只留下檢討文字。</li>
<li>toil budget 讓重複性工作變成可預算的工程投資。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> review 讓服務在上線前先過可靠性門檻。</li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 與 burnout 讓值班成為組織設計問題，脫離個人耐力測試的框架。</li>
<li>hierarchy of reliability 讓 monitoring、testing、capacity、dev、product 串成一條路由。</li>
<li>blameless culture 讓檢討聚焦在系統與流程，而不是個人責任。</li>
<li>embedded SRE / consulting 讓可靠性能力可以以不同介入深度落到服務團隊。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sre.google/">sre.google</a>：Google SRE 官方資源入口，收錄 books 與主題更新。</li>
<li><a href="https://cloud.google.com/blog/products/devops-sre/the-sre-book-turns-6">The SRE book turns 6!</a>：整理 SRE Book / Workbook 與延伸資源的官方入口。</li>
<li><a href="https://cloud.google.com/blog/products/devops-sre/how-to-design-good-slos-according-to-google-sres">Adopting SRE: Standardizing your SLO design process</a>：補 SLO 設計方法與實務語境。</li>
</ul>
]]></content:encoded></item><item><title>Kubernetes</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/</guid><description>&lt;p>Kubernetes 是 container orchestration 事實標準、承擔三個責任：workload lifecycle（pod / deployment / probe / rolling update）、cluster networking（service / ingress / DNS）、resource scheduling（resource limit / QoS / autoscaling）。設計取捨偏向「declarative + control loop + extensible」、是 cloud-native 生態的核心抽象。可自管或用 cloud managed（GKE / EKS / AKS）。&lt;/p>
&lt;p>對「多服務多實例 container orchestration、需要 rolling update / blue-green / canary、跨雲 / 跨環境統一抽象」這條路徑、Kubernetes 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 kubectl 部署 Deployment + Service、配置 probe / resource limit&lt;/li>
&lt;li>設計 rolling update / pod disruption budget 避免服務中斷&lt;/li>
&lt;li>選 Ingress controller（nginx / traefik / GLBC / ALB Controller）&lt;/li>
&lt;li>看懂 pod stuck / probe fail / OOMKilled / drain timeout 訊號&lt;/li>
&lt;li>評估 managed（GKE / EKS / AKS）vs 自管 vs Operator 進階場景&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-kubernetes-跑起來">最短路徑：5 分鐘把 Kubernetes 跑起來&lt;/h2>





&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"># 1. 本機跑 kind（需先安裝 kind + docker）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">kind create cluster --name dev
&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"># 2. 部署 Deployment + Service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">kubectl create deployment nginx --image&lt;span class="o">=&lt;/span>nginx:stable-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">kubectl expose deployment nginx --port&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span> --type&lt;span class="o">=&lt;/span>ClusterIP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">kubectl get pods,svc,deploy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">kubectl port-forward svc/nginx 8080:80&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="kubectl-核心指令">kubectl 核心指令&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>資源生命週期：apply / create / delete / get / describe / logs / exec&lt;/li>
&lt;li>Rolling update：set image / rollout status / rollout undo&lt;/li>
&lt;li>Debug：events / port-forward / cp / top&lt;/li>
&lt;li>對應指令範例：&lt;code>kubectl get pods -A&lt;/code>、&lt;code>kubectl describe pod &amp;lt;name&amp;gt;&lt;/code>、&lt;code>kubectl logs -f&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="workload-設計">Workload 設計&lt;/h3>
&lt;p>Pod lifecycle 是 K8s 的核心抽象。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes 是 container orchestration 事實標準、承擔三個責任：workload lifecycle（pod / deployment / probe / rolling update）、cluster networking（service / ingress / DNS）、resource scheduling（resource limit / QoS / autoscaling）。設計取捨偏向「declarative + control loop + extensible」、是 cloud-native 生態的核心抽象。可自管或用 cloud managed（GKE / EKS / AKS）。</p>
<p>對「多服務多實例 container orchestration、需要 rolling update / blue-green / canary、跨雲 / 跨環境統一抽象」這條路徑、Kubernetes 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 kubectl 部署 Deployment + Service、配置 probe / resource limit</li>
<li>設計 rolling update / pod disruption budget 避免服務中斷</li>
<li>選 Ingress controller（nginx / traefik / GLBC / ALB Controller）</li>
<li>看懂 pod stuck / probe fail / OOMKilled / drain timeout 訊號</li>
<li>評估 managed（GKE / EKS / AKS）vs 自管 vs Operator 進階場景</li>
</ol>
<h2 id="最短路徑5-分鐘把-kubernetes-跑起來">最短路徑：5 分鐘把 Kubernetes 跑起來</h2>





<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. 本機跑 kind（需先安裝 kind + docker）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">kind create cluster --name dev
</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"># 2. 部署 Deployment + Service</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">kubectl create deployment nginx --image<span class="o">=</span>nginx:stable-alpine
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">kubectl expose deployment nginx --port<span class="o">=</span><span class="m">80</span> --type<span class="o">=</span>ClusterIP
</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"># 3. 驗證</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">kubectl get pods,svc,deploy
</span></span><span class="line"><span class="ln">10</span><span class="cl">kubectl port-forward svc/nginx 8080:80</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="kubectl-核心指令">kubectl 核心指令</h3>
<p>子議題：</p>
<ul>
<li>資源生命週期：apply / create / delete / get / describe / logs / exec</li>
<li>Rolling update：set image / rollout status / rollout undo</li>
<li>Debug：events / port-forward / cp / top</li>
<li>對應指令範例：<code>kubectl get pods -A</code>、<code>kubectl describe pod &lt;name&gt;</code>、<code>kubectl logs -f</code></li>
</ul>
<h3 id="workload-設計">Workload 設計</h3>
<p>Pod lifecycle 是 K8s 的核心抽象。子議題：</p>
<ul>
<li>Deployment（stateless）/ StatefulSet（stateful）/ DaemonSet（per-node）/ Job / CronJob</li>
<li>Pod 多 container（sidecar / init container）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
</ul>
<h3 id="probe--resource-limit--qos">Probe / Resource limit / QoS</h3>
<p>子議題：</p>
<ul>
<li>Liveness（活著嗎）/ Readiness（接流量嗎）/ Startup（啟動完了嗎）— 三 probe 各自責任</li>
<li>Resource limit（requests / limits）+ QoS class（Guaranteed / Burstable / BestEffort）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform lifecycle contract</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="rolling-update--disruption-budget">Rolling update / disruption budget</h3>
<p>對應案例 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：cutover without drain</a>。子議題：</p>
<ul>
<li>maxSurge / maxUnavailable 配置</li>
<li>PodDisruptionBudget 限制 voluntary disruption</li>
<li>Preemption / priority class</li>
</ul>
<h3 id="ingress--service-mesh-integration">Ingress / Service mesh integration</h3>
<p>子議題：</p>
<ul>
<li>Ingress controller 選擇（<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> / <a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a> / ALB Controller）</li>
<li>Gateway API（next gen Ingress）</li>
<li>Service mesh integration（<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a>-based Istio / Linkerd）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></li>
</ul>
<h3 id="operator-pattern--crd">Operator pattern / CRD</h3>
<p>子議題：</p>
<ul>
<li>CRD（CustomResourceDefinition）+ Controller 模式</li>
<li>Operator framework（OperatorSDK / kubebuilder）</li>
<li>常見 Operator：Prometheus / Cert-manager / Argo CD</li>
</ul>
<h3 id="managed-vs-self-managed">Managed vs self-managed</h3>
<p>對應案例 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a>、<a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a>、<a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a>。子議題：</p>
<ul>
<li>Self-managed（kubeadm / Cluster API）的 control plane 維運成本</li>
<li>Managed（GKE / EKS / AKS）的限制（版本鎖定 / managed addon）</li>
<li>遷移路徑跟回退設計</li>
</ul>
<h3 id="multi-cluster--federation">Multi-cluster / Federation</h3>
<p>子議題：</p>
<ul>
<li>Federation v2 / Cluster API multi-cluster</li>
<li>Cross-cluster service mesh（Istio multi-cluster）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb cluster scaling</a></li>
</ul>
<h3 id="cluster-autoscaling">Cluster autoscaling</h3>
<p>子議題：</p>
<ul>
<li>Horizontal Pod Autoscaler / Vertical Pod Autoscaler</li>
<li>Cluster Autoscaler / Karpenter</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> 對照</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="pod-stuckpending--crashloopbackoff">Pod stuck（Pending / CrashLoopBackOff）</h3>
<p>操作原則：先 <code>kubectl describe pod</code> 看 events、再 <code>kubectl logs</code> 看 container 訊息。</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">kubectl describe pod &lt;name&gt;           <span class="c1"># 看 Events 段的 scheduling / pull / probe 訊息</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kubectl logs &lt;name&gt; --previous        <span class="c1"># 看 crash 前一輪的 container log</span></span></span></code></pre></div><p>判讀路徑：Pending → resource 不足 / nodeSelector 不匹配；CrashLoopBackOff → exit code + log 找原因。</p>
<h3 id="probe-failure-造成不停-restart">Probe failure 造成不停 restart</h3>
<p>操作原則：probe path / initial delay / timeout 配置錯。判讀：<code>describe pod</code> 看 probe events。</p>
<h3 id="oomkilled">OOMKilled</h3>
<p>操作原則：memory limit 太低、container 被殺。判讀：<code>describe pod</code> 看 last state reason。修法：raise limit 或優化 application memory。</p>
<h3 id="rolling-update-stuck">Rolling update stuck</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。判讀路徑：新 pod 起不來 → readiness 失敗 → 舊 pod 不下線 → 卡住。</p>
<h3 id="drain-timeout">Drain timeout</h3>
<p>操作原則：<code>kubectl drain</code> 失敗、PDB 限制太緊。判讀：<code>kubectl describe pdb</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單機服務（VM / bare metal）</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
      </tr>
      <tr>
          <td>Local dev / CI</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a> Compose</td>
      </tr>
      <tr>
          <td>AWS managed runtime（不要 K8s）</td>
          <td>ECS / Fargate</td>
      </tr>
      <tr>
          <td>極簡 PaaS</td>
          <td>Cloud Run / Heroku / Fly.io</td>
      </tr>
      <tr>
          <td>替代 orchestrator</td>
          <td>Nomad / Rancher</td>
      </tr>
      <tr>
          <td>Edge / IoT 場景</td>
          <td>K3s / MicroK8s</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 kubectl 指令 reference</li>
<li>YAML manifest 完整 schema</li>
<li>各 Operator 細節</li>
<li>各語言 client-go API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>自管 K8s 遷 managed、零停機切流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多團隊異質集群整併到單一控制面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a></td>
          <td>平台重置不中斷產品的能力遷移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a></td>
          <td>大規模 workload 分批遷 EKS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed K8s 跟團隊維運模型對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb cluster scaling</a></td>
          <td>手動擴縮 → 自動化容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></td>
          <td>Service mesh 升級分批治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：cutover without drain</a></td>
          <td>Rolling update / drain 沒做的傷</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型 systemd → 中型 K8s → 大型 multi-cluster</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability</a>（release gate）、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a></li>
</ul>
]]></content:encoded></item><item><title>OpenTelemetry</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/</guid><description>&lt;p>OpenTelemetry（OTel）是 CNCF 開放標準、承擔三個責任：定義 traces / metrics / logs 的資料模型（spec）、提供 vendor-neutral 的 SDK 跟 auto-instrumentation、以 OTel Collector 作為 instrumentation 跟 backend 之間的抽象層。設計取捨偏向「抽象優於 vendor-specific feature」、避免 vendor lock-in 是核心動機。多數現代 observability 平台（Datadog / Honeycomb / Grafana Cloud / Cloud Operations）都接受 OTLP。&lt;/p>
&lt;p>本頁先給最短路徑、再展開日常 instrumentation 跟 Collector 部署、最後進階治理（sampling / semantic conventions / logs 成熟度）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 OTel SDK 或 auto-instrumentation 對應用程式做 instrumentation&lt;/li>
&lt;li>配置 OTLP exporter 把 telemetry 送到任一 backend&lt;/li>
&lt;li>部署 OTel Collector（agent / gateway 模式）作為 backend 切換抽象層&lt;/li>
&lt;li>區分 head-based vs tail-based sampling、選擇對應策略&lt;/li>
&lt;li>評估從 vendor SDK 遷移到 OTel SDK 的相容性風險&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-otel-跑起來">最短路徑：5 分鐘把 OTel 跑起來&lt;/h2>





&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"># 1. 應用程式加 auto-instrumentation（範例：Python）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: opentelemetry-bootstrap -a install&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: opentelemetry-instrument --traces_exporter otlp --metrics_exporter otlp python app.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 啟動 OTel Collector&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker run -p 4317:4317 -p 4318:4318 otel/opentelemetry-collector-contrib&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Collector 配置範例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: otel-collector-config.yaml with otlp receiver + exporter to backend&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證 telemetry 從 app → Collector → backend 串通。實際 production 要評估 sampling、retention、cardinality。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="instrumentation-模式">Instrumentation 模式&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Auto-instrumentation：Java / Python / Node / .NET / Ruby / Go 各語言成熟度不同&lt;/li>
&lt;li>Manual instrumentation：開發者寫 trace span / metric instrument&lt;/li>
&lt;li>Library instrumentation：opentelemetry-instrumentation-&lt;lib>（HTTP client / DB / framework）&lt;/li>
&lt;/ul>
&lt;h3 id="otlp-exporter-配置">OTLP exporter 配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>OTLP gRPC（4317）vs HTTP（4318）&lt;/li>
&lt;li>Endpoint / headers / authentication 配置&lt;/li>
&lt;li>對應指令範例：環境變數 &lt;code>OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code>、&lt;code>OTEL_EXPORTER_OTLP_HEADERS&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="collector-部署模式">Collector 部署模式&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent&lt;/strong>：跟應用程式同 host / pod、做 local buffer + enrichment&lt;/li>
&lt;li>&lt;strong>Gateway&lt;/strong>：集中部署、跨多 agent 接收、做 sampling / routing&lt;/li>
&lt;li>&lt;strong>Sidecar&lt;/strong>：K8s sidecar pattern、跟 pod 同生命週期&lt;/li>
&lt;li>對應配置：receivers / processors / exporters pipeline&lt;/li>
&lt;/ul>
&lt;p>深入：&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計&lt;/a>（三種位置責任分工、pipeline 設計、collector 失效 / 記憶體壓力 / backpressure 故障演練、容量成本邊界）。&lt;/p></description><content:encoded><![CDATA[<p>OpenTelemetry（OTel）是 CNCF 開放標準、承擔三個責任：定義 traces / metrics / logs 的資料模型（spec）、提供 vendor-neutral 的 SDK 跟 auto-instrumentation、以 OTel Collector 作為 instrumentation 跟 backend 之間的抽象層。設計取捨偏向「抽象優於 vendor-specific feature」、避免 vendor lock-in 是核心動機。多數現代 observability 平台（Datadog / Honeycomb / Grafana Cloud / Cloud Operations）都接受 OTLP。</p>
<p>本頁先給最短路徑、再展開日常 instrumentation 跟 Collector 部署、最後進階治理（sampling / semantic conventions / logs 成熟度）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 OTel SDK 或 auto-instrumentation 對應用程式做 instrumentation</li>
<li>配置 OTLP exporter 把 telemetry 送到任一 backend</li>
<li>部署 OTel Collector（agent / gateway 模式）作為 backend 切換抽象層</li>
<li>區分 head-based vs tail-based sampling、選擇對應策略</li>
<li>評估從 vendor SDK 遷移到 OTel SDK 的相容性風險</li>
</ol>
<h2 id="最短路徑5-分鐘把-otel-跑起來">最短路徑：5 分鐘把 OTel 跑起來</h2>





<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. 應用程式加 auto-instrumentation（範例：Python）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: opentelemetry-bootstrap -a install</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># TODO: opentelemetry-instrument --traces_exporter otlp --metrics_exporter otlp python app.py</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="c1"># 2. 啟動 OTel Collector</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: docker run -p 4317:4317 -p 4318:4318 otel/opentelemetry-collector-contrib</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"># 3. Collector 配置範例</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: otel-collector-config.yaml with otlp receiver + exporter to backend</span></span></span></code></pre></div><p>最短路徑驗證 telemetry 從 app → Collector → backend 串通。實際 production 要評估 sampling、retention、cardinality。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="instrumentation-模式">Instrumentation 模式</h3>
<p>子議題：</p>
<ul>
<li>Auto-instrumentation：Java / Python / Node / .NET / Ruby / Go 各語言成熟度不同</li>
<li>Manual instrumentation：開發者寫 trace span / metric instrument</li>
<li>Library instrumentation：opentelemetry-instrumentation-<lib>（HTTP client / DB / framework）</li>
</ul>
<h3 id="otlp-exporter-配置">OTLP exporter 配置</h3>
<p>子議題：</p>
<ul>
<li>OTLP gRPC（4317）vs HTTP（4318）</li>
<li>Endpoint / headers / authentication 配置</li>
<li>對應指令範例：環境變數 <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>、<code>OTEL_EXPORTER_OTLP_HEADERS</code></li>
</ul>
<h3 id="collector-部署模式">Collector 部署模式</h3>
<p>子議題：</p>
<ul>
<li><strong>Agent</strong>：跟應用程式同 host / pod、做 local buffer + enrichment</li>
<li><strong>Gateway</strong>：集中部署、跨多 agent 接收、做 sampling / routing</li>
<li><strong>Sidecar</strong>：K8s sidecar pattern、跟 pod 同生命週期</li>
<li>對應配置：receivers / processors / exporters pipeline</li>
</ul>
<p>深入：<a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計</a>（三種位置責任分工、pipeline 設計、collector 失效 / 記憶體壓力 / backpressure 故障演練、容量成本邊界）。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="auto-instrumentation-跨語言成熟度">Auto-instrumentation 跨語言成熟度</h3>
<p>子議題：</p>
<ul>
<li>Java：最成熟、auto-instrumentation 廣度最大</li>
<li>Python：成熟、覆蓋主流 framework</li>
<li>Node：成熟、async context propagation 較複雜</li>
<li>Go：較弱（runtime 不支援 monkey patching）、多用 manual</li>
<li>.NET：成熟、跟 Application Insights 對齊</li>
<li>Ruby / PHP：相對較弱、覆蓋主流 framework</li>
</ul>
<h3 id="sampling-策略">Sampling 策略</h3>
<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</a>。子議題：</p>
<ul>
<li><strong>Head-based sampling</strong>：trace 開始時決定保留與否、低成本但 lose context</li>
<li><strong>Tail-based sampling</strong>：trace 完成後決定（依錯誤 / 延遲）、Collector 要 buffer 整個 trace</li>
<li>Sampling rate 配置（global / per-service / probabilistic）</li>
<li>對應工具：OTel Collector 的 tail_sampling processor、Refinery（Honeycomb）</li>
</ul>
<h3 id="semantic-conventions">Semantic conventions</h3>
<p>子議題：</p>
<ul>
<li>HTTP / DB / messaging / RPC 等的 attribute 命名規範</li>
<li>Resource attributes（service.name / service.version / deployment.environment）</li>
<li>Span name / status code convention</li>
<li>Migration：應用層用 OTel semantic conventions、避免 vendor-specific naming</li>
</ul>
<h3 id="logs-in-otel">Logs in OTel</h3>
<p>子議題：</p>
<ul>
<li>Logs 比 metrics / traces 較晚進 OTel spec（v1.0 較新）</li>
<li>Log signal 設計：log record 跟 span 關聯（trace_id / span_id）</li>
<li>跟 Loki / Elastic / CloudWatch 的整合</li>
<li>從現有 logging library 移轉的路徑（log-forwarding vs SDK）</li>
</ul>
<h3 id="vendor-sdk-vs-otel-sdk-遷移">Vendor SDK vs OTel SDK 遷移</h3>
<p>對應案例 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OpenTelemetry</a> 與 <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</a>。子議題：</p>
<ul>
<li>動機：避免 vendor lock-in、多 backend 並存、開源治理</li>
<li>風險：vendor-specific feature 損失（profiling / RUM 整合）</li>
<li>遷移路徑：dual ship → cutover → cleanup</li>
<li>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 反例：OTel migration signal drift</a></li>
</ul>
<h3 id="resource-detection">Resource detection</h3>
<p>子議題：</p>
<ul>
<li>自動偵測 cloud provider（AWS / GCP / Azure）resource attributes</li>
<li>K8s resource detector（pod / namespace / cluster）</li>
<li>Container resource detector</li>
<li>對應配置：<code>OTEL_RESOURCE_ATTRIBUTES</code></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="telemetry-沒到-backend">Telemetry 沒到 backend</h3>
<p>操作原則：先確認 SDK 配置正確、再看 Collector 是否收到、最後看 exporter 是否成功。</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"># TODO: 設 OTEL_LOG_LEVEL=debug 看 SDK 內部 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: 看 Collector internal metrics（zPages / Prometheus exporter）</span></span></span></code></pre></div><p>判讀路徑：SDK → Collector → backend、三段各自獨立、要逐層 isolate。</p>
<h3 id="cardinality-explosion">Cardinality explosion</h3>
<p>操作原則：metric attribute 含 high-cardinality 值（user_id / session_id）會爆 backend 成本。判讀：看 backend 的 series 數量、找 attribute 來源。</p>
<h3 id="trace-span-gap">Trace span gap</h3>
<p>操作原則：trace 不完整、看 context propagation 是否在跨 service / 跨 thread 邊界丟失。</p>
<h3 id="auto-instrumentation-不生效">Auto-instrumentation 不生效</h3>
<p>操作原則：確認 SDK 版本跟 library version 對應、agent 啟動方式正確。對應 <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> 的踩坑經驗。</p>
<h3 id="sampling-過頭--不足">Sampling 過頭 / 不足</h3>
<p>操作原則：sampling rate 跟 backend 預算 + debug 需求對齊。判讀：debug 時找不到 trace（sampling 過頭）vs backend 成本爆（sampling 不足）。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 metrics 後端</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / Mimir</td>
      </tr>
      <tr>
          <td>需要 SaaS APM 整合</td>
          <td><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> / New Relic</td>
      </tr>
      <tr>
          <td>需要 logs 後端</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> / Loki</td>
      </tr>
      <tr>
          <td>需要 high-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> + X-Ray</td>
      </tr>
      <tr>
          <td>GCP-native</td>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a></td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 SDK 完整 API</li>
<li>OTLP protocol binary format</li>
<li>各 backend 的 OTel 整合細節（見各 backend vendor 頁）</li>
<li>OTel project governance / sig 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>從 vendor SDK 遷出 OTel</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></td>
          <td>GCP Cloud Trace 接受 OTLP</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>AWS Distro for OTel + EKS</td>
      </tr>
      <tr>
          <td><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></td>
          <td>OTLP ingestion / vendor SDK 移轉</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）雙軌遷移期的 signal 漂移</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 OTel 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></td>
          <td>K8s 規模化下 OTel Collector 拓撲 / 資源訊號分層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型直接 SDK / 中型加 Collector / 大型 multi-backend</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：所有 04 vendor 都可作 OTel backend</li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>PagerDuty</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/</guid><description>&lt;p>PagerDuty 是 on-call / alerting 的事實標準 SaaS、承擔三個責任：alert routing + escalation policy + schedule、incident workflow + response play + runbook automation、postmortem 整合（Jeli 收購）。從 paging 工具演化成完整 IR 平台。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>PagerDuty 的核心定位是 &lt;em>signal → human → action&lt;/em> 的中介層、把 alert source（觀測、SIEM、合成監控、cloud control plane）變成具體某個人手機震動 + 24 小時內可追蹤的 incident timeline。它是 &lt;em>routing engine + on-call schedule 的事實標準&lt;/em>、定位有別於 alert source 和溝通平台。&lt;/p>
&lt;p>跟上游 07 章的 detection stack 是直接 wire：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> ES app 產生的 Notable Event 透過 &lt;em>Splunk-PagerDuty integration&lt;/em> 或 SOAR playbook 變成 PagerDuty incident、severity 直接帶過來；&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> 的高分 rate-limit / bot block 透過 webhook 進 PagerDuty Event API v2、再經 Event Orchestration 判斷是丟 SecOps schedule 還是 platform schedule。這條鏈最常壞在 &lt;em>severity 對應不一致&lt;/em>（Splunk medium 在 PagerDuty 變 P1）、跟 &lt;em>integration 沒 deduplication key&lt;/em>（一次 attack 100 個 Notable Event 各起 100 個 incident）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &amp;#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall&lt;/a> 的差異在 &lt;em>ecosystem 跟 IR 模型&lt;/em> — PagerDuty 走 enterprise + AIOps + Process Automation 重資料堆疊、incident.io 走 Slack-native + collab-first、Opsgenie 綁 Atlassian、Grafana OnCall 是 OSS 自管。選 PagerDuty 的核心理由通常是 &lt;em>AIOps + Process Automation + Jeli postmortem 整合的 ecosystem maturity&lt;/em>、不是 paging 功能本身。&lt;/p></description><content:encoded><![CDATA[<p>PagerDuty 是 on-call / alerting 的事實標準 SaaS、承擔三個責任：alert routing + escalation policy + schedule、incident workflow + response play + runbook automation、postmortem 整合（Jeli 收購）。從 paging 工具演化成完整 IR 平台。</p>
<h2 id="服務定位">服務定位</h2>
<p>PagerDuty 的核心定位是 <em>signal → human → action</em> 的中介層、把 alert source（觀測、SIEM、合成監控、cloud control plane）變成具體某個人手機震動 + 24 小時內可追蹤的 incident timeline。它是 <em>routing engine + on-call schedule 的事實標準</em>、定位有別於 alert source 和溝通平台。</p>
<p>跟上游 07 章的 detection stack 是直接 wire：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> ES app 產生的 Notable Event 透過 <em>Splunk-PagerDuty integration</em> 或 SOAR playbook 變成 PagerDuty incident、severity 直接帶過來；<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 的高分 rate-limit / bot block 透過 webhook 進 PagerDuty Event API v2、再經 Event Orchestration 判斷是丟 SecOps schedule 還是 platform schedule。這條鏈最常壞在 <em>severity 對應不一致</em>（Splunk medium 在 PagerDuty 變 P1）、跟 <em>integration 沒 deduplication key</em>（一次 attack 100 個 Notable Event 各起 100 個 incident）。</p>
<p>跟 <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> / <a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a> 的差異在 <em>ecosystem 跟 IR 模型</em> — PagerDuty 走 enterprise + AIOps + Process Automation 重資料堆疊、incident.io 走 Slack-native + collab-first、Opsgenie 綁 Atlassian、Grafana OnCall 是 OSS 自管。選 PagerDuty 的核心理由通常是 <em>AIOps + Process Automation + Jeli postmortem 整合的 ecosystem maturity</em>、不是 paging 功能本身。</p>
<p>關鍵張力：<em>alert volume</em> ↔ <em>responder burnout</em> 是 PagerDuty 客戶最常見 trade-off。為了「不漏 alert」把 grouping / deduplication 設很寬、結果 on-call 一週被叫醒 20 次、3 個月後人員流失。要看清楚自己 <em>容忍多少漏報換多少 responder sustainability</em>、不是把 alert source 全開到 PagerDuty 當保險。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>PagerDuty 在 alert pipeline 中承擔哪一段（routing / schedule / incident workflow）、哪些要外接（Slack 通訊、Jeli postmortem、Process Automation 對接 runbook）</li>
<li>Service / escalation policy / schedule 的 ownership 設計（誰建 service、誰改 escalation、誰能 override schedule）</li>
<li>Event Orchestration 的 deduplication / grouping / dynamic routing 設計、跟上游 SIEM 的 severity mapping 一致性</li>
<li>何時用 PagerDuty、何時走 Opsgenie / incident.io / Grafana OnCall 的取捨</li>
</ol>
<p>本頁不教 PagerDuty console 操作步驟、也不列 pricing tier — 那些 vendor 官方文件已經完整。本頁重點在 <em>判讀問題</em>：怎麼看一個 PagerDuty deployment 健康與否、哪些 config 是 high blast radius、跟上下游（07 detection / 04 observability / Jeli postmortem）怎麼接。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 PagerDuty deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 ack / escalate / resolve</strong>：on-call rotation 有沒有人、escalation policy 第二層第三層是不是同一個人、有沒有 break-glass 流程（primary 失聯時誰補位）。schedule override 是否走 PR / approval、還是 console 直改沒留痕。</li>
<li><strong>Escalation policy 設計</strong>：每層 escalation timeout（5min / 10min / 15min）是否符合 SLO、是否有 <em>無人 ack 自動上報主管</em> 規則、跨時區 schedule 是否避免半夜 page 給 off-shift 區域</li>
<li><strong>Event Orchestration 設定</strong>：alert deduplication key 是否正確（同一 host + 同一 alert type 合併）、grouping rule 是否避免 alert storm、dynamic routing 是否依 service / severity / time 分軌到不同 schedule</li>
<li><strong>SOAR / Process Automation playbook 觸發點</strong>：哪些 incident 自動觸發 runbook（restart / rotate token / scale up）、approval gate 是否設在高風險動作、playbook 失敗有沒有 fallback 回 human page</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="service--team--escalation">Service / team / escalation</h3>
<p>PagerDuty 的 <em>service</em> 對應一個應用 / component、是 incident 的最小 ownership 單位。一個 service 綁一個 <em>escalation policy</em>（N 層、每層 X 分鐘 timeout）、一個 <em>schedule</em>（rotation + override）。production 部署用 <em>Terraform PagerDuty provider</em> 進版控、不在 console 直改 — 因為 schedule / escalation 是高 blast radius config、誤改可能讓半夜 alert 漏掉。Service 通常按 Service Ownership 對齊組織結構、不是按技術 stack 切：把一個微服務 stack 拆成 10 個 service 看似乾淨、但 incident 起來時 responder 要同時 ack 10 個 incident 對 SLO 不利、合理粒度通常是 <em>一個 product team 一個 service</em>。</p>
<h3 id="event-orchestration--response-play">Event Orchestration + Response Play</h3>
<p>Event Orchestration 是 alert → incident 的工程化路由層、處理 <em>deduplication / grouping / dynamic routing</em> 三件事。deduplication 用 <em>dedup_key</em>（同 host + 同 check type 合併、避免 100 個 alert 起 100 個 incident）、grouping 用 <em>time window + tag</em>（同一服務 5min 內多個 alert 合一）、dynamic routing 依 severity / time / service tag 分軌到不同 schedule。Response Play 則是 incident 起來後自動執行的動作 bundle — page additional responder、建 Slack channel、發 status page、call conference bridge。Response Play 應該走 PR review、不能 console 直加 — 一個誤設的 Response Play 可能在每個 P1 自動 page 整個 leadership。</p>
<h3 id="severity-mapping-跟上游一致性">Severity mapping 跟上游一致性</h3>
<p>上游 source（Splunk Notable Event / Datadog monitor / Cloudflare WAF alert）的 severity 跟 PagerDuty incident urgency 要 <em>對應表化</em>、不是各自為政。常見錯位：Splunk medium 在 PagerDuty 變成 high urgency（半夜被吵醒）、或 Cloudflare 高分 bot block 進來只標 low（真實 attack 漏報）。實務做法是寫一張 <em>severity translation table</em> 進 Event Orchestration、source severity → PagerDuty urgency 一對一寫死、變更走 PR review。對應 <a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">Incident Severity Trigger</a> 的判讀標準。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>PagerDuty</th>
          <th>Opsgenie</th>
          <th>incident.io</th>
          <th>Grafana OnCall</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>定位</td>
          <td>Enterprise IR platform、AIOps + automation</td>
          <td>Atlassian 生態 paging</td>
          <td>Slack-native IR collaboration</td>
          <td>OSS / 自管 OnCall</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>SaaS only</td>
          <td>SaaS（Atlassian Cloud）</td>
          <td>SaaS only</td>
          <td>Self-hosted（Grafana stack）/ SaaS</td>
      </tr>
      <tr>
          <td>Alert routing</td>
          <td>Event Orchestration（dedup + group + dyn）</td>
          <td>Alert policy + integration</td>
          <td>Slack-first、簡化 routing</td>
          <td>Integrations + routes（OSS 等效）</td>
      </tr>
      <tr>
          <td>Schedule</td>
          <td>強 — rotation / override / multi-tz</td>
          <td>強 — 跟 Jira / Confluence 整合</td>
          <td>中 — schedule 較簡化</td>
          <td>中 — 基本 rotation</td>
      </tr>
      <tr>
          <td>Workflow / Play</td>
          <td>Response Play + Process Automation</td>
          <td>Atlassian Automation</td>
          <td>Slack-driven workflow（強）</td>
          <td>基本 webhook</td>
      </tr>
      <tr>
          <td>Postmortem</td>
          <td>Jeli（收購、深度整合）</td>
          <td>Confluence template</td>
          <td>內建 postmortem + learning loop</td>
          <td>外接</td>
      </tr>
      <tr>
          <td>AIOps</td>
          <td>Machine Learning alert clustering、PRCC</td>
          <td>基本 grouping</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Pricing</td>
          <td>Per-user + 按 feature tier、enterprise 貴</td>
          <td>按 user、Atlassian bundle 划算</td>
          <td>Per-responder、中等</td>
          <td>OSS 免費 / Grafana Cloud 按 active</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Enterprise + 多 service + AIOps 需求</td>
          <td>Atlassian 已用 + 預算敏感</td>
          <td>Startup / mid-size + Slack-first 文化</td>
          <td>OSS-friendly + Grafana stack 已用</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — schedule / policy / Play 量多</td>
          <td>中 — Atlassian 內可遷</td>
          <td>中 — Slack 工作流綁深</td>
          <td>低 — OSS、可帶走 config</td>
      </tr>
  </tbody>
</table>
<p>選 PagerDuty 的核心訴求：<em>多 service 大組織 + AIOps 對 alert storm 有 ROI + Process Automation 對接 runbook + Jeli postmortem 整合需求</em>。Slack-first 小組直接 incident.io、Atlassian-heavy 走 Opsgenie、預算敏感 OSS 走 Grafana OnCall。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Event Orchestration deduplication / grouping</strong>：deduplication 跟 grouping 是兩個層次 — dedup 是 <em>同一事件多次發送只算一個</em>（用 dedup_key）、grouping 是 <em>多個相關事件合成一個 incident</em>（用 time window + service / tag）。設定太寬會漏 alert（不同 root cause 被合併、漏報重要事件）、設定太窄會 alert storm。實務做法是 <em>先寬後窄</em> — 上線初期用較寬 grouping 觀察、再依 false-merge 案例收窄。</p>
<p><strong>AIOps Machine Learning</strong>：PagerDuty AIOps 用 ML 做 <em>alert clustering + probable root cause + change correlation</em> — 多個 alert 自動歸成 cluster、推測 root cause、跟近期 deploy / config change 對照。風險是 <em>黑箱</em>：ML 把不相關 alert 合一、SOC analyst 看不到原始事件就 ack；或把真實 incident 歸到 noise cluster。production 應該開、但 <em>保留 manual ungroup 機制 + 定期 audit cluster accuracy</em>。</p>
<p><strong>Process Automation + Splunk SOAR 整合</strong>：PagerDuty Process Automation（前 Rundeck）做 runbook 自動執行 — restart / scale / rollback / rotate token。對接 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk SOAR</a> 形成 <em>incident enrichment + auto-remediation</em> 鏈：Splunk SOAR 在 incident 起來時自動拉 context（user / host / IP recent activity）寫進 PagerDuty incident note、再依 playbook 觸發 PagerDuty Process Automation 做動作。高風險動作（disable account、rotate prod credential）必走 <em>approval gate</em>、不能 fire-and-forget。</p>
<p><strong>Jeli postmortem 整合（2023 收購後）</strong>：PagerDuty incident resolve 後可以一鍵 import 進 Jeli、自動帶 timeline / responder list / Slack transcript、開始做 interview + narrative。對應 <a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli vendor</a> — Jeli 走「learning from incident」方法論、不是只生 root cause report、強調 <em>near miss</em> 跟 <em>human factor</em> 也要分析。</p>
<p><strong>Service ownership / Service Standards</strong>：PagerDuty Service Standards 把 service 的 <em>escalation policy / runbook link / business criticality / oncall coverage</em> 做成 checklist、organization 可以看哪些 service 沒達標。對 platform team 是治理工具、避免某 service「沒人 oncall 但有 alert source」。配對 <a href="/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">Repeated Incident Toil</a> 的反模式：service 沒人 own 但 alert 一直響、最後變 noise 被全部靜音、真實 incident 進來時也漏報。</p>
<p><strong>Status page 整合</strong>：PagerDuty incident 可以自動同步到 <a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> / <a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a> 對外 status page、但 <em>自動同步</em> 是雙刃刀 — internal P1 不一定是 customer-facing、誤公告影響品牌。實務做法是 <em>只同步 customer-facing severity 的 incident</em>、用 Event Orchestration 加 tag (<code>customer_facing: true</code>) 才觸發 statuspage update、其他 incident 走人工 publish。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Escalation 漏配 / primary 失聯沒人補</strong>：escalation policy 第二層第三層是同一個人、或 off-shift 時無人 ack — 改成跨層異人 + break-glass policy（自動 page manager-on-call）+ 半年 audit</li>
<li><strong>Schedule 跨時區算錯</strong>：把 UTC schedule 套到亞太工程師、結果半夜 page off-shift — schedule 用 follow-the-sun rotation、或在 schedule layer 加 time restriction</li>
<li><strong>Event Orchestration deduplication 太寬</strong>：不同 root cause 的 alert 被 dedup 成同一 incident、漏報 — 收窄 dedup_key（加 service + alert_type）、保留 manual unmerge</li>
<li><strong>Event Orchestration grouping 太窄</strong>：同一事故 100 個 alert 各起 100 個 incident、alert storm、on-call 看不完 — 放寬 time window grouping、或開 AIOps clustering</li>
<li><strong>AIOps ML 黑箱誤合</strong>：真實 incident 被歸到 noise cluster、responder 沒看到 — 開 ML cluster audit dashboard、每月 sample review、保留 manual ungroup 機制</li>
<li><strong>Slack notification stale</strong>：PagerDuty Slack app token 過期 / channel 改名、incident 通知沒進 Slack — Slack integration health check + fallback channel + on-call 應該收 mobile push 不只看 Slack</li>
<li><strong>Response Play 自動誤觸</strong>：Play 設成 P1 自動 page leadership、結果一個 noise P1 把整個 C-level 半夜叫起來 — Play 必走 PR review、defaults to <em>additional engineer</em> not <em>leadership</em>、leadership page 走人工升級</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<p>PagerDuty 不是所有 IR 場景都適合：</p>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Atlassian 生態</td>
          <td><a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a></td>
      </tr>
      <tr>
          <td>OSS / 預算敏感</td>
          <td><a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a></td>
      </tr>
      <tr>
          <td>Slack-first IR</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>Microsoft Teams</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>No-code workflow + AI</td>
          <td><a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></td>
      </tr>
      <tr>
          <td>Postmortem only</td>
          <td><a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a></td>
      </tr>
      <tr>
          <td>Status page only</td>
          <td><a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> / <a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a></td>
      </tr>
  </tbody>
</table>
<p>選對需求形狀比選 vendor 重要：startup 一開始走 Slack-native incident.io、規模上來 alert storm 多了再評 PagerDuty AIOps、Atlassian 重度用戶 Opsgenie bundle 划算。</p>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 integration 完整 setup / Pricing 細節 / AIOps ML 內部演算法</li>
<li>Response Play 跟 Process Automation 的具體 playbook 實作（Rundeck DSL）</li>
<li>Jeli 的 narrative + interview workflow（屬 postmortem 章節）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>PagerDuty 公開 customer 多為大型 SaaS / 平台、下列案例可作為「paging 設計如何影響事故 detect → ack → mitigate 時間 + 怎麼跟 07 detection 鏈起來」的閱讀脈絡：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 PagerDuty 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub cases</a></td>
          <td>大型平台事故的多輪 paging 與輪值、Event Orchestration grouping 設計 + 跨 service escalation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare cases</a></td>
          <td>控制面 vs data plane 的 paging 分軌、不同 severity 走不同 schedule + Response Play</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack cases</a></td>
          <td>通訊平台失效時 paging 通道的退路、PagerDuty mobile push 是 Slack-first IR 的 fallback</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog cases</a></td>
          <td>觀測平台事故的 self-paging 與外部 fallback、AIOps clustering 避免 self-incident alert storm</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>Splunk Notable Event 進 PagerDuty incident、SOAR playbook 自動 rotate Azure AD app credential、approval gate 在 force re-auth 動作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>異常 query volume 進 PagerDuty、Process Automation 觸發 Snowflake user disable + IP block、Response Play 同步 page legal / customer success</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/" data-link-title="Microsoft 365：套件級身分驗證事故" data-link-desc="企業套件在身份依賴失效時，如何同步處理跨產品影響與對外揭露。">Microsoft 365 2023 Auth Incident</a></td>
          <td>認證鏈事故跨多 service、Event Orchestration grouping + dynamic routing 把 auth alert 集中到 identity team schedule</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a>、<a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">Incident Severity Trigger</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a>、<a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a>、<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>、<a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a>（postmortem 接手）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（Notable Event source）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（WAF alert source）</li>
<li>官方：<a href="https://support.pagerduty.com/">PagerDuty Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/</guid><description>&lt;p>RabbitMQ 是 AMQP 協議實作的 classic broker、承擔三個責任：訊息持久化與重試（durable queue + ack/nack）、靈活路由（exchange + routing key + binding）、跨服務任務分派（worker pool + DLQ）。設計取捨偏向「處理即承諾、broker 負責重新投遞、consumer 負責 idempotency」、可靠性建立在 ack 機制而非 replication。&lt;/p>
&lt;p>對「任務隊列、worker pool、複雜 routing、RPC over messaging」這條路徑、RabbitMQ 是業界主流。本頁先給最短路徑、再展開日常 publisher / consumer 操作與 exchange 設計、最後進階治理（quorum queue、cluster、federation）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker 跑起 RabbitMQ + management UI、驗證 broker 健康&lt;/li>
&lt;li>用 CLI / Management API 建 exchange、queue、binding&lt;/li>
&lt;li>設計 exchange type（direct / fanout / topic / headers）對齊路由需求&lt;/li>
&lt;li>看懂 queue depth、unacked、connection / channel 數量訊號、定位故障層&lt;/li>
&lt;li>評估 quorum queue、stream、federation、shovel 等規模化議題&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-rabbitmq-跑起來">最短路徑：5 分鐘把 RabbitMQ 跑起來&lt;/h2>





&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"># 1. 啟動 RabbitMQ + management plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
&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"># 2. 建 exchange / queue / binding（rabbitmqadmin 可重現、Management UI 在 http://localhost:15672、預設 guest/guest）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqadmin &lt;span class="nb">declare&lt;/span> exchange &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>demo.direct &lt;span class="nv">type&lt;/span>&lt;span class="o">=&lt;/span>direct
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>demo.q
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqadmin &lt;span class="nb">declare&lt;/span> binding &lt;span class="nv">source&lt;/span>&lt;span class="o">=&lt;/span>demo.direct &lt;span class="nv">destination&lt;/span>&lt;span class="o">=&lt;/span>demo.q &lt;span class="nv">routing_key&lt;/span>&lt;span class="o">=&lt;/span>demo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 用 rabbitmqctl 驗證 broker 狀態&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqctl list_queues
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqctl list_exchanges
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> rabbitmq rabbitmqctl list_bindings&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「broker 起來、UI 能訪、能 enqueue/dequeue」。實際寫程式用 AMQP client、見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>CLI 指令對照表（rabbitmqctl / rabbitmq-diagnostics / rabbitmqadmin）&lt;/li>
&lt;li>Management API 形狀（HTTP API、適合自動化）&lt;/li>
&lt;li>AMQP client 配置：connection / channel / consumer prefetch / publisher confirm&lt;/li>
&lt;li>對應指令範例：&lt;code>rabbitmqctl list_queues name messages messages_unacknowledged consumers&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="exchange-types-與-routing-設計">Exchange types 與 routing 設計&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">Exchange&lt;/a> 承擔訊息分流責任、不同 type 對應不同路由語意。子議題：&lt;/p></description><content:encoded><![CDATA[<p>RabbitMQ 是 AMQP 協議實作的 classic broker、承擔三個責任：訊息持久化與重試（durable queue + ack/nack）、靈活路由（exchange + routing key + binding）、跨服務任務分派（worker pool + DLQ）。設計取捨偏向「處理即承諾、broker 負責重新投遞、consumer 負責 idempotency」、可靠性建立在 ack 機制而非 replication。</p>
<p>對「任務隊列、worker pool、複雜 routing、RPC over messaging」這條路徑、RabbitMQ 是業界主流。本頁先給最短路徑、再展開日常 publisher / consumer 操作與 exchange 設計、最後進階治理（quorum queue、cluster、federation）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker 跑起 RabbitMQ + management UI、驗證 broker 健康</li>
<li>用 CLI / Management API 建 exchange、queue、binding</li>
<li>設計 exchange type（direct / fanout / topic / headers）對齊路由需求</li>
<li>看懂 queue depth、unacked、connection / channel 數量訊號、定位故障層</li>
<li>評估 quorum queue、stream、federation、shovel 等規模化議題</li>
</ol>
<h2 id="最短路徑5-分鐘把-rabbitmq-跑起來">最短路徑：5 分鐘把 RabbitMQ 跑起來</h2>





<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. 啟動 RabbitMQ + management plugin</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
</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"># 2. 建 exchange / queue / binding（rabbitmqadmin 可重現、Management UI 在 http://localhost:15672、預設 guest/guest）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqadmin <span class="nb">declare</span> exchange <span class="nv">name</span><span class="o">=</span>demo.direct <span class="nv">type</span><span class="o">=</span>direct
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>demo.q
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqadmin <span class="nb">declare</span> binding <span class="nv">source</span><span class="o">=</span>demo.direct <span class="nv">destination</span><span class="o">=</span>demo.q <span class="nv">routing_key</span><span class="o">=</span>demo
</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="c1"># 3. 用 rabbitmqctl 驗證 broker 狀態</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqctl list_queues
</span></span><span class="line"><span class="ln">11</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqctl list_exchanges
</span></span><span class="line"><span class="ln">12</span><span class="cl">docker <span class="nb">exec</span> rabbitmq rabbitmqctl list_bindings</span></span></code></pre></div><p>最短路徑驗證「broker 起來、UI 能訪、能 enqueue/dequeue」。實際寫程式用 AMQP client、見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>CLI 指令對照表（rabbitmqctl / rabbitmq-diagnostics / rabbitmqadmin）</li>
<li>Management API 形狀（HTTP API、適合自動化）</li>
<li>AMQP client 配置：connection / channel / consumer prefetch / publisher confirm</li>
<li>對應指令範例：<code>rabbitmqctl list_queues name messages messages_unacknowledged consumers</code></li>
</ul>
<h3 id="exchange-types-與-routing-設計">Exchange types 與 routing 設計</h3>
<p><a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">Exchange</a> 承擔訊息分流責任、不同 type 對應不同路由語意。子議題：</p>
<ul>
<li>Direct：精準 routing key 匹配（point-to-point）</li>
<li>Fanout：忽略 routing key、廣播到所有 binding queue</li>
<li>Topic：層級式 routing key（<code>*</code> 單層、<code>#</code> 多層萬用字元）</li>
<li>Headers：依 message header 路由（少用）</li>
<li>對應指令：宣告 exchange / queue / binding 的 CLI 與 client 範例</li>
</ul>
<h3 id="queue-設計與-acknack-策略">Queue 設計與 ack/nack 策略</h3>
<p><a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack/nack</a> 是 RabbitMQ 的 delivery 控制點。子議題：</p>
<ul>
<li>Durable queue vs transient queue</li>
<li>Manual ack vs auto ack（後者等同 at-most-once）</li>
<li>Prefetch 設定（<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> + 併發控制）</li>
<li><a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-letter exchange</a>（DLX）配置</li>
<li>Message TTL 與 queue length limit</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>本段主題已展開為 deep article：<a href="queue-types-classic-quorum-stream/">classic vs quorum vs stream 選型</a>、<a href="network-partition-clustering/">network partition 與 cluster 一致性</a>、<a href="dlq-retry-escalation/">DLQ retry escalation</a>。下列子議題段保留選題判讀入口。</p>
<h3 id="classic-queue-vs-quorum-queue-vs-stream">Classic queue vs Quorum queue vs Stream</h3>
<p>子議題：</p>
<ul>
<li>Classic queue：原生持久化 queue、mirrored queue 已 deprecated</li>
<li>Quorum queue：Raft-based、取代 mirrored、跨節點一致性</li>
<li>Stream（3.9+）：append-only log、log-based 模型、類似 Kafka 但仍是 RabbitMQ 體系</li>
<li>三種模型的選擇判讀（throughput、retention、replay 需求）</li>
</ul>
<h3 id="federation-與-shovel">Federation 與 Shovel</h3>
<p>子議題：</p>
<ul>
<li>Federation：upstream / downstream broker 鏈接、適合鬆耦合跨資料中心</li>
<li>Shovel：點對點轉發、適合單純訊息搬運</li>
<li>跨區 / 多 cluster 場景的選擇</li>
</ul>
<h3 id="erlang-clustering-與-network-partition">Erlang clustering 與 network partition</h3>
<p>子議題：</p>
<ul>
<li>Cluster 拓樸（disc node、ram node）</li>
<li><code>cluster_partition_handling</code> 策略（ignore、autoheal、pause_minority）</li>
<li>腦裂偵測與處理</li>
</ul>
<h3 id="多-vhost--多租戶">多 vhost / 多租戶</h3>
<p>子議題：</p>
<ul>
<li>Vhost 隔離（namespace、ACL、user permission）</li>
<li>User / Role / Permission 設計</li>
<li>Per-vhost resource limit（max connection、max queue）</li>
</ul>
<h3 id="prefetch-與-consumer-併發控制">Prefetch 與 consumer 併發控制</h3>
<p>子議題：</p>
<ul>
<li>Prefetch count 對 throughput / fairness 的影響</li>
<li>Channel-level vs Consumer-level prefetch</li>
<li>配合 <a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a> 控制重試壓力</li>
</ul>
<h3 id="rabbitmq-cluster-operatork8s">RabbitMQ Cluster Operator（K8s）</h3>
<p>子議題：</p>
<ul>
<li>Cluster Operator vs 自管 StatefulSet</li>
<li>持久化卷（PVC）與資料保護</li>
<li>升級流程（rolling restart 與資料完整性）</li>
</ul>
<h3 id="plugin-機制與多協議">Plugin 機制與多協議</h3>
<p>子議題：</p>
<ul>
<li>MQTT plugin（IoT 場景、橋接 device-to-broker）</li>
<li>STOMP plugin</li>
<li>對應 <a href="/blog/backend/03-message-queue/broker-basics/#%e8%aa%9e%e6%84%8f%e4%bf%9d%e8%ad%89%e7%9a%84%e4%b8%8d%e5%90%8c%e5%af%a6%e4%bd%9c%e6%a9%9f%e5%88%b6" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics 的 QoS / ACK 機制橋接</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="queue-堆積messages-增加unacked-不收斂">Queue 堆積（messages 增加、unacked 不收斂）</h3>
<p>操作原則：先看 consumer 是否存在、再看 ack 速率 vs publish 速率、最後看 prefetch / poison message。</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">rabbitmqctl list_queues name messages messages_unacknowledged consumers</span></span></code></pre></div><p>判讀路徑：無 consumer（client crash）→ consumer 慢（下游卡）→ poison message 卡住（看單一 message redelivery 次數）。</p>
<h3 id="connection--channel-limit">Connection / Channel limit</h3>
<p>操作原則：client 設計不當會用滿 connection / channel，看每個 connection 的 channel 數。</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">rabbitmqctl list_connections
</span></span><span class="line"><span class="ln">2</span><span class="cl">rabbitmqctl list_channels</span></span></code></pre></div><h3 id="disk-alarm-觸發">Disk alarm 觸發</h3>
<p>操作原則：disk 低於 <code>disk_free_limit</code>、broker 暫停 publisher。判讀：保留期太長 / 訊息大小 / 未消費 queue 過大。</p>
<h3 id="memory-alarm-觸發">Memory alarm 觸發</h3>
<p>操作原則：記憶體超過 watermark、broker 觸發 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">paging</a>、publisher 變慢。判讀路徑：訊息累積、consumer 失聯、queue 設定錯誤。</p>
<h3 id="network-partition腦裂">Network partition（腦裂）</h3>
<p>操作原則：cluster 節點互相不可達、看 <code>cluster_partition_handling</code> 與 partition log。對應 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 語義誤配</a> 思路。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高吞吐事件流、長期 replay</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></td>
      </tr>
      <tr>
          <td>Managed queue（AWS 生態）</td>
          <td><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></td>
      </tr>
      <tr>
          <td>Managed pub/sub（GCP 生態）</td>
          <td><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></td>
      </tr>
      <tr>
          <td>輕量 messaging + 微服務</td>
          <td><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
      </tr>
      <tr>
          <td>Redis 生態 stream</td>
          <td><a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
      </tr>
      <tr>
          <td>IoT device 接入</td>
          <td>EMQX / HiveMQ / Mosquitto（MQTT broker、或用 RabbitMQ MQTT plugin）</td>
      </tr>
      <tr>
          <td>Workflow + durable execution</td>
          <td>Temporal（T4 候選）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 AMQP client 完整 API（依官方文件）</li>
<li>所有 plugin 細節（只列主流 plugin）</li>
<li>RabbitMQ Streams 跟 Kafka 的詳細對照（見 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="rabbitmq-專屬案例c23-c33">RabbitMQ 專屬案例（C23-C33）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23 Bloomberg vhost 多租戶</a></td>
          <td>多 vhost + 自助平台化</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-soundcloud-fanout-audio/" data-link-title="3.C24 SoundCloud：AMQP fan-out 音訊處理 pipeline" data-link-desc="SoundCloud 每秒 20-30K persistent message、不同處理類型分開隊列、各自獨立 scale。">3.C24 SoundCloud fan-out</a></td>
          <td>音訊處理 pipeline 分隊列</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25 Indeed Delay + DLQ</a></td>
          <td>三層 retry escalation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26 GoCardless Hutch</a></td>
          <td>單一 topic exchange 服務 mesh</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27 Zalando AWS</a></td>
          <td>雲端自動 master selection / federation 升級</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/" data-link-title="3.C28 WeWork：Consistent hash exchange 保證帳戶順序" data-link-desc="WeWork 固定數量 queue &#43; account ID hash 路由、每 queue 一個 worker &#43; exclusive consumer 保 partition-level ordering。">3.C28 WeWork hash ordering</a></td>
          <td>Consistent hash exchange / per-key ordering</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-bunny-channel-pool/" data-link-title="3.C29 WeWork：Bunny &#43; Puma 多執行緒 channel pool" data-link-desc="WeWork 從 Unicorn 切到 Puma 後遇 ConnectionClosedError、根因是 AMQP channel 跨執行緒共用、改用 connection_pool 管理。">3.C29 WeWork Bunny channel pool</a></td>
          <td>AMQP channel 不可跨執行緒</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30 Runtastic mirrored bottleneck</a></td>
          <td>Mirrored queue 網路成本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-mozilla-pulse-naming-isolation/" data-link-title="3.C31 Mozilla Pulse：命名前綴 &#43; ACL 取代 vhost 多租戶" data-link-desc="Mozilla Pulse 不用 vhost、改用權限 &#43; 命名前綴 (exchange/{user}/*) 做隔離、CloudAMQP 託管、PulseGuardian 管使用者。">3.C31 Mozilla Pulse</a></td>
          <td>ACL + naming 取代 vhost（反向）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-loyaltylion-monitoring-thousands/" data-link-title="3.C32 LoyaltyLion：監控數千 RabbitMQ queue" data-link-desc="LoyaltyLion 跑數千 queue、用 rabbitmqctl &#43; statsd 推 Datadog、揭露大規模 queue 拓樸下原生 plugin API 不夠用。">3.C32 LoyaltyLion monitoring</a></td>
          <td>大規模 queue topology 監控</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wargaming-game-portal-decoupling/" data-link-title="3.C33 Wargaming：World of Tanks 戰後 dossier 解耦" data-link-desc="Wargaming WoT server 全 Linux、戰後 dossier 寫 RabbitMQ、portal 顯示統計而不增 game server load。">3.C33 Wargaming game portal</a></td>
          <td>異步解耦 game server / portal</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 RabbitMQ 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配</a></td>
          <td>manual ack + DLX + idempotency 三層責任邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>小型直接用 / 中型補 idempotency / 大型分 vhost</td>
      </tr>
  </tbody>
</table>
<p><strong>MQTT plugin + Cluster Operator 缺直接 customer case</strong>：可補 RabbitMQ 官方 native MQTT blog 跟 K8s Operator docs、後續若有 customer 案例可加。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>、<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>下游能力：<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>、<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></li>
</ul>
]]></content:encoded></item><item><title>Redis</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/</guid><description>&lt;p>Redis 是 in-memory data structure store、承擔三個責任：cache serving layer（with eviction）、data structure operation（string / hash / list / sorted set / stream / hyperloglog / geo）、輕量持久化（AOF / RDB）。設計取捨偏向「記憶體優先 + data type rich + 可選持久化」、cache 是主用場、但 data type 讓它跨入 session store / counter / leaderboard / lock 等場景。2024 起授權變動為 RSALv2 / SSPL（OSI 不認）、引發 Valkey fork。&lt;/p>
&lt;p>對「通用快取、session store、rate limit counter、leaderboard、distributed lock」這條路徑、Redis 是事實標準。本頁先給最短路徑、再展開日常 CLI / API 與 key 設計、最後進階治理（cluster / persistence / modules）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker 跑起 Redis、用 redis-cli 驗證&lt;/li>
&lt;li>用 SET / GET / EXPIRE / DEL / KEYS 操作、區分 6 大 data types 適用場景&lt;/li>
&lt;li>設計 key naming + TTL + eviction policy 對齊 cache miss 行為&lt;/li>
&lt;li>看懂 hit rate / memory pressure / eviction / replication lag 訊號&lt;/li>
&lt;li>評估 Cluster vs Sentinel、AOF/RDB、modules、授權變動下的選擇&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-redis-跑起來">最短路徑：5 分鐘把 Redis 跑起來&lt;/h2>





&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"># 1. 啟動 Redis&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker run -d --name redis -p 6379:6379 redis:7&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="c1"># 2. 連線&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker exec -it redis redis-cli&lt;/span>
&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="c1"># 3. 驗證 SET / GET / EXPIRE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: SET foo bar / GET foo / EXPIRE foo 60 / TTL foo&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Redis 起來、能讀寫 + TTL」。實際應用見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>redis-cli 指令對照表（SET / GET / DEL / EXPIRE / TTL / KEYS / SCAN / MGET / MSET）&lt;/li>
&lt;li>Client library 配置：connection pool / timeout / pipeline / cluster mode&lt;/li>
&lt;li>Pub/Sub vs Streams 的選用判讀&lt;/li>
&lt;li>對應指令範例：&lt;code>INFO replication&lt;/code>、&lt;code>CLIENT LIST&lt;/code>、&lt;code>SLOWLOG GET&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="key-design-與-data-types">Key design 與 data types&lt;/h3>
&lt;p>不同 data type 對應不同&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">資料形狀&lt;/a>。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Redis 是 in-memory data structure store、承擔三個責任：cache serving layer（with eviction）、data structure operation（string / hash / list / sorted set / stream / hyperloglog / geo）、輕量持久化（AOF / RDB）。設計取捨偏向「記憶體優先 + data type rich + 可選持久化」、cache 是主用場、但 data type 讓它跨入 session store / counter / leaderboard / lock 等場景。2024 起授權變動為 RSALv2 / SSPL（OSI 不認）、引發 Valkey fork。</p>
<p>對「通用快取、session store、rate limit counter、leaderboard、distributed lock」這條路徑、Redis 是事實標準。本頁先給最短路徑、再展開日常 CLI / API 與 key 設計、最後進階治理（cluster / persistence / modules）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker 跑起 Redis、用 redis-cli 驗證</li>
<li>用 SET / GET / EXPIRE / DEL / KEYS 操作、區分 6 大 data types 適用場景</li>
<li>設計 key naming + TTL + eviction policy 對齊 cache miss 行為</li>
<li>看懂 hit rate / memory pressure / eviction / replication lag 訊號</li>
<li>評估 Cluster vs Sentinel、AOF/RDB、modules、授權變動下的選擇</li>
</ol>
<h2 id="最短路徑5-分鐘把-redis-跑起來">最短路徑：5 分鐘把 Redis 跑起來</h2>





<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. 啟動 Redis</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker run -d --name redis -p 6379:6379 redis:7</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="c1"># 2. 連線</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: docker exec -it redis redis-cli</span>
</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="c1"># 3. 驗證 SET / GET / EXPIRE</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: SET foo bar / GET foo / EXPIRE foo 60 / TTL foo</span></span></span></code></pre></div><p>最短路徑驗證「Redis 起來、能讀寫 + TTL」。實際應用見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>redis-cli 指令對照表（SET / GET / DEL / EXPIRE / TTL / KEYS / SCAN / MGET / MSET）</li>
<li>Client library 配置：connection pool / timeout / pipeline / cluster mode</li>
<li>Pub/Sub vs Streams 的選用判讀</li>
<li>對應指令範例：<code>INFO replication</code>、<code>CLIENT LIST</code>、<code>SLOWLOG GET</code></li>
</ul>
<h3 id="key-design-與-data-types">Key design 與 data types</h3>
<p>不同 data type 對應不同<a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">資料形狀</a>。子議題：</p>
<ul>
<li>String：cache / counter / config flag</li>
<li>Hash：object cache（避免反覆 serialize）</li>
<li>List：queue / activity feed（小規模）</li>
<li>Set：membership / tag</li>
<li>Sorted set：leaderboard / time-series sliding window</li>
<li>Stream：log-style queue / event stream</li>
<li>HyperLogLog / Geo：approximate count / 地理座標</li>
</ul>
<p>Key naming 規範：<code>&lt;service&gt;:&lt;entity&gt;:&lt;id&gt;:&lt;field&gt;</code>、用 <code>:</code> 分層、避免大 key（單 key &gt; 10KB / list 長度 &gt; 10K）。</p>
<h3 id="ttl-與-eviction-策略">TTL 與 eviction 策略</h3>
<p><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 跟 eviction</a> 是 cache 行為的核心旋鈕。子議題：</p>
<ul>
<li>顯式 EXPIRE vs SET EX 設 TTL</li>
<li>maxmemory + maxmemory-policy（allkeys-lru / allkeys-lfu / volatile-lru / volatile-ttl / noeviction）</li>
<li>TTL 設計：固定 TTL vs 動態 TTL vs 不設 TTL</li>
<li>對應指令：<code>CONFIG SET maxmemory 2gb</code>、<code>CONFIG SET maxmemory-policy allkeys-lfu</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="cluster-vs-sentinel">Cluster vs Sentinel</h3>
<p>子議題：</p>
<ul>
<li>Sentinel：HA 模式、無 sharding、適合單 master 容量足夠</li>
<li>Cluster：sharding 模式、16384 hash slot、橫向擴展容量</li>
<li>Hash tag <code>{...}</code> 強制 multi-key 同 shard</li>
<li>Cluster failover 對 PEL（Streams）跟 distributed lock 的影響</li>
</ul>
<h3 id="aof--rdb-持久化策略">AOF / RDB 持久化策略</h3>
<p>子議題：</p>
<ul>
<li>AOF（append-only file）：fsync 策略（always / everysec / no）、rewrite</li>
<li>RDB（snapshot）：save 策略、backup 還原</li>
<li>混合模式：AOF + RDB</li>
<li>持久化在 cache 場景的取捨（持久化是回填還是 source-of-truth）</li>
</ul>
<h3 id="eviction-policy-詳細">Eviction policy 詳細</h3>
<p>子議題：</p>
<ul>
<li>LRU vs LFU：access pattern 對選擇的影響</li>
<li>volatile-* vs allkeys-*：只淘汰有 TTL 的 vs 全 key</li>
<li>approximate LRU 的 sampling 影響</li>
<li>對應 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
</ul>
<h3 id="distributed-lock">Distributed lock</h3>
<p>子議題：</p>
<ul>
<li>SETNX + EXPIRE 模式</li>
<li>Redlock 算法（多 master quorum）+ 取捨爭議</li>
<li>Redlock 何時不夠：fence token / lease renewal</li>
<li>對應 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a></li>
</ul>
<h3 id="pubsub-vs-streams">Pub/Sub vs Streams</h3>
<p>子議題：</p>
<ul>
<li>Pub/Sub：fire-and-forget、訂閱者離線會錯過</li>
<li>Streams：append-only log、consumer group + PEL</li>
<li>何時用 Streams 取代 Pub/Sub</li>
<li>Redis Streams 細節見 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">03 messaging 模組 Redis Streams vendor</a></li>
</ul>
<h3 id="redis-modules">Redis Modules</h3>
<p>子議題：</p>
<ul>
<li>RedisJSON / RedisSearch / RedisTimeSeries / RedisBloom / RedisGraph</li>
<li>Module 隨授權變動受影響、Valkey 部分 fork</li>
<li>Module 在 ElastiCache 的支援限制</li>
</ul>
<h3 id="授權變動與選型影響">授權變動與選型影響</h3>
<p>子議題：</p>
<ul>
<li>2024 RSALv2 / SSPL 變動的影響範圍</li>
<li>對 managed service（ElastiCache 改 default 為 Valkey）的衝擊</li>
<li>從 Redis 遷 Valkey 的相容性路徑</li>
<li>商業 vs OSS 邊界</li>
</ul>
<h3 id="hot-key-處理">Hot key 處理</h3>
<p>子議題：</p>
<ul>
<li>Hot key 偵測（redis-cli &ndash;hotkeys、<code>MONITOR</code> 慎用）</li>
<li>Hot key 解法：local cache + Redis 兩層、key 拆分（讀多寫少場景）</li>
<li>對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="hit-rate-下降">Hit rate 下降</h3>
<p>操作原則：先看 cache pattern 是否變（新功能 / TTL 變短）、再看 origin 壓力是否擴大。</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"># TODO: INFO stats（看 keyspace_hits / keyspace_misses 比例）</span></span></span></code></pre></div><p>判讀路徑：TTL 太短 → eviction 太積極 → key 命名變動造成 cache miss → origin 失敗 retry storm。</p>
<h3 id="memory-pressure--eviction-異常">Memory pressure / eviction 異常</h3>
<p>操作原則：先看 maxmemory + maxmemory-policy 設定、再看 key size 分布。</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"># TODO: INFO memory / MEMORY USAGE &lt;key&gt; / --bigkeys</span></span></span></code></pre></div><h3 id="hot-key">Hot key</h3>
<p>對應案例 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify Write-Through</a>。判讀路徑：某 key 的 QPS 遠高於其他、單 shard CPU 接近 100%、其他 shard 閒置。</p>
<h3 id="replication-lag">Replication lag</h3>
<p>操作原則：replica 跟 master 差距、看 <code>INFO replication</code> 的 master_repl_offset vs slave_repl_offset。對 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency</a> 的對照。</p>
<h3 id="cache-stampede雷霆崩潰">Cache stampede（雷霆崩潰）</h3>
<p>對應反例 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede Rollout</a>。判讀路徑：TTL 同時過期 → 大量 cache miss → origin 被打爆 → 連鎖失敗。修法：jitter TTL、early refresh、singleflight 模式。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 OSI 認可開源授權</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>純 cache、不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>極高 throughput / 多核</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS 生態 managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></td>
      </tr>
      <tr>
          <td>Durable Redis-compatible</td>
          <td>AWS MemoryDB（介於 cache 與 DB）</td>
      </tr>
      <tr>
          <td>大規模 event stream</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
      </tr>
      <tr>
          <td>Process-local cache</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> / Guava Cache（JVM 內、無網路）</td>
      </tr>
      <tr>
          <td>Search / full-text</td>
          <td>Elasticsearch / OpenSearch（不在本模組）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Redis client 完整 API</li>
<li>Redis command 百科（詳查 redis.io/commands）</li>
<li>Redis Stack 商業 modules 細節</li>
<li>AOF / RDB 內部 binary format</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Redis 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Shopify Redis 上做 Marshal → MessagePack 雙軌遷移、payload 編碼演進</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Shopify 在 read-heavy 路徑用 Redis 做 write-through、對應 hot key / 命中率治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>invalidation / shard move 一致性議題、Redis Cluster 與 replica 場景共用判讀框架</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Redis 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>Redis TTL 切換 / key rename 都會觸發 stampede、需 jitter / singleflight / early refresh</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single instance + AOF / 中型 Sentinel + replica / 大型 Cluster + hash tag</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>Memcached 路由層案例、Redis 對應為 Cluster + proxy（Envoy / Twemproxy）或 client-side routing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>分層 cache（DRAM + flash）對照、Redis on flash（RoF / Speedb）的成本決策參考</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 基於 Memcached + 跨 AZ replication、Redis 對應為 active-active CRDB / Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>Graph cache 演進案例、Redis 對應為 RedisGraph（已 deprecated）或自建 graph 索引</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a></td>
          <td>Edge tiered（HTTP cache）對照、Redis 對應為 hot tier + S3 cold tier 自建分層</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>、<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a>、<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ul>
]]></content:encoded></item><item><title>0.1 後端服務能力地圖</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/</guid><description>&lt;p>後端服務能力地圖的核心原則是先辨識需求類型，再選擇服務分類。資料庫、快取、訊息佇列、觀測平台與部署平台都屬於後端能力，但它們分別回答「狀態放哪裡」、「讀取怎麼變快」、「工作怎麼跨 process」、「系統怎麼診斷」、「服務怎麼交付」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用需求類型辨識後端服務分類&lt;/li>
&lt;li>區分資料儲存、快取、訊息傳遞、觀測與部署平台&lt;/li>
&lt;li>判斷一個問題應先進入哪個 backend 模組&lt;/li>
&lt;li>避免把所有外部技術都混成同一種「基礎設施」&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察需求會先表現成系統症狀">【觀察】需求會先表現成系統症狀&lt;/h2>
&lt;p>後端服務選型通常從症狀開始。產品需求或事故描述裡會出現一些可觀察訊號：&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>資料需要長期保存、查詢、交易一致性&lt;/td>
 &lt;td>狀態真相與持久化&lt;/td>
 &lt;td>資料庫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>熱門資料讀取太頻繁、下游被打爆&lt;/td>
 &lt;td>讀取壓力與暫存&lt;/td>
 &lt;td>快取 / Redis&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>request 內完成工作太慢、需要重試或排隊&lt;/td>
 &lt;td>非同步處理與可靠傳遞&lt;/td>
 &lt;td>訊息佇列&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>出事時找不到原因、跨服務路徑不清楚&lt;/td>
 &lt;td>診斷與操作訊號&lt;/td>
 &lt;td>可觀測性平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署、擴容、流量入口與健康檢查不穩&lt;/td>
 &lt;td>服務交付與平台合約&lt;/td>
 &lt;td>部署平台&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是索引。真正的選型要看每個訊號背後的資料生命週期、流量形狀與操作需求。&lt;/p>
&lt;h2 id="判讀資料長期存在通常先看資料庫">【判讀】資料長期存在通常先看資料庫&lt;/h2>
&lt;p>資料庫解決的是「系統承認哪份資料是正式狀態」。如果資料需要長期保存、支援查詢、維持交易一致性、被多個 request 共同讀寫，選型應先進入資料庫與持久化模組。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>電商訂單需要保存付款狀態、出貨狀態與退款紀錄&lt;/li>
&lt;li>會員系統需要保存帳號、權限、登入方式與審計資料&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS&lt;/a> 產品需要保存 workspace、plan、billing 與使用量&lt;/li>
&lt;/ul>
&lt;p>這類問題的核心是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>。快取可以加速讀取，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 可以延後處理，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 可以協助診斷，但正式狀態仍需要清楚的資料模型與一致性邊界。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">資料庫與持久化&lt;/a>。&lt;/p>
&lt;h2 id="判讀讀取壓力集中通常先看快取">【判讀】讀取壓力集中通常先看快取&lt;/h2>
&lt;p>快取解決的是「同一類資料被重複讀取時，如何降低正式資料來源壓力」。如果資料本身已經有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，但熱門資料導致資料庫或下游 API 壓力過高，選型應先進入快取與 Redis 模組。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>商品詳情頁被大量瀏覽，但商品資料變更頻率低&lt;/li>
&lt;li>使用者權限或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag&lt;/a> 每個 request 都要查&lt;/li>
&lt;li>即時服務需要快速查詢 client presence 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 訂閱狀態&lt;/li>
&lt;/ul>
&lt;p>這類問題的核心是讀取路徑與失效策略。快取要回答資料何時過期、何時更新、下游失敗時如何回應、cache &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a> 尖峰如何保護系統。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">快取與 Redis&lt;/a>。&lt;/p>
&lt;h2 id="判讀工作跨出-request-通常先看訊息傳遞">【判讀】工作跨出 request 通常先看訊息傳遞&lt;/h2>
&lt;p>訊息佇列解決的是「工作離開目前 process 或 request 後，如何可靠地被處理」。如果一個 request 需要觸發後續工作、等待外部系統、重試、批次處理或跨服務通知，選型應先進入訊息佇列與事件傳遞模組。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>付款成功後要寄信、更新 CRM、發送推播與建立出貨任務&lt;/li>
&lt;li>使用者上傳影片後要轉檔、產生縮圖與通知完成&lt;/li>
&lt;li>IoT 裝置上報資料後要清洗、聚合與觸發告警&lt;/li>
&lt;/ul>
&lt;p>這類問題的核心是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics&lt;/a>。系統要決定是否需要持久化、是否允許重複投遞、失敗是否重試、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 如何水平擴展。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列與事件傳遞&lt;/a>。&lt;/p>
&lt;h2 id="判讀看不見系統行為通常先看觀測平台">【判讀】看不見系統行為通常先看觀測平台&lt;/h2>
&lt;p>可觀測性平台解決的是「服務發生什麼、為什麼發生、影響範圍多大」。如果事故發生後只能看單機 log，無法串起 request、事件、下游依賴與容量趨勢，選型應先進入可觀測性模組。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>API 偶爾變慢，但無法判斷是資料庫、外部 API 還是部署節點問題&lt;/li>
&lt;li>queue lag 上升，但不知道 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> 變快還是 consumer 變慢&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> client 斷線增加，但缺少連線生命週期與地區資訊&lt;/li>
&lt;/ul>
&lt;p>這類問題的核心是操作訊號。log、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 需要共用欄位與關聯方式，才能讓工程師從症狀回到原因。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>。&lt;/p>
&lt;h2 id="判讀服務交付不穩通常先看部署平台">【判讀】服務交付不穩通常先看部署平台&lt;/h2>
&lt;p>部署平台解決的是「服務如何被啟動、更新、擴容、接流量與停止」。如果問題集中在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">liveness&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery&lt;/a>、container image 或資源限制，選型應先進入部署平台與網路入口模組。&lt;/p></description><content:encoded><![CDATA[<p>後端服務能力地圖的核心原則是先辨識需求類型，再選擇服務分類。資料庫、快取、訊息佇列、觀測平台與部署平台都屬於後端能力，但它們分別回答「狀態放哪裡」、「讀取怎麼變快」、「工作怎麼跨 process」、「系統怎麼診斷」、「服務怎麼交付」。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用需求類型辨識後端服務分類</li>
<li>區分資料儲存、快取、訊息傳遞、觀測與部署平台</li>
<li>判斷一個問題應先進入哪個 backend 模組</li>
<li>避免把所有外部技術都混成同一種「基礎設施」</li>
</ol>
<hr>
<h2 id="觀察需求會先表現成系統症狀">【觀察】需求會先表現成系統症狀</h2>
<p>後端服務選型通常從症狀開始。產品需求或事故描述裡會出現一些可觀察訊號：</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>代表的工程問題</th>
          <th>優先評估方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料需要長期保存、查詢、交易一致性</td>
          <td>狀態真相與持久化</td>
          <td>資料庫</td>
      </tr>
      <tr>
          <td>熱門資料讀取太頻繁、下游被打爆</td>
          <td>讀取壓力與暫存</td>
          <td>快取 / Redis</td>
      </tr>
      <tr>
          <td>request 內完成工作太慢、需要重試或排隊</td>
          <td>非同步處理與可靠傳遞</td>
          <td>訊息佇列</td>
      </tr>
      <tr>
          <td>出事時找不到原因、跨服務路徑不清楚</td>
          <td>診斷與操作訊號</td>
          <td>可觀測性平台</td>
      </tr>
      <tr>
          <td>部署、擴容、流量入口與健康檢查不穩</td>
          <td>服務交付與平台合約</td>
          <td>部署平台</td>
      </tr>
  </tbody>
</table>
<p>這張表是索引。真正的選型要看每個訊號背後的資料生命週期、流量形狀與操作需求。</p>
<h2 id="判讀資料長期存在通常先看資料庫">【判讀】資料長期存在通常先看資料庫</h2>
<p>資料庫解決的是「系統承認哪份資料是正式狀態」。如果資料需要長期保存、支援查詢、維持交易一致性、被多個 request 共同讀寫，選型應先進入資料庫與持久化模組。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>電商訂單需要保存付款狀態、出貨狀態與退款紀錄</li>
<li>會員系統需要保存帳號、權限、登入方式與審計資料</li>
<li><a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS</a> 產品需要保存 workspace、plan、billing 與使用量</li>
</ul>
<p>這類問題的核心是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>。快取可以加速讀取，<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 可以延後處理，<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 可以協助診斷，但正式狀態仍需要清楚的資料模型與一致性邊界。</p>
<p>下一步可讀：<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">資料庫與持久化</a>。</p>
<h2 id="判讀讀取壓力集中通常先看快取">【判讀】讀取壓力集中通常先看快取</h2>
<p>快取解決的是「同一類資料被重複讀取時，如何降低正式資料來源壓力」。如果資料本身已經有 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，但熱門資料導致資料庫或下游 API 壓力過高，選型應先進入快取與 Redis 模組。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>商品詳情頁被大量瀏覽，但商品資料變更頻率低</li>
<li>使用者權限或 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a> 每個 request 都要查</li>
<li>即時服務需要快速查詢 client presence 或 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 訂閱狀態</li>
</ul>
<p>這類問題的核心是讀取路徑與失效策略。快取要回答資料何時過期、何時更新、下游失敗時如何回應、cache <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a> 尖峰如何保護系統。</p>
<p>下一步可讀：<a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">快取與 Redis</a>。</p>
<h2 id="判讀工作跨出-request-通常先看訊息傳遞">【判讀】工作跨出 request 通常先看訊息傳遞</h2>
<p>訊息佇列解決的是「工作離開目前 process 或 request 後，如何可靠地被處理」。如果一個 request 需要觸發後續工作、等待外部系統、重試、批次處理或跨服務通知，選型應先進入訊息佇列與事件傳遞模組。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>付款成功後要寄信、更新 CRM、發送推播與建立出貨任務</li>
<li>使用者上傳影片後要轉檔、產生縮圖與通知完成</li>
<li>IoT 裝置上報資料後要清洗、聚合與觸發告警</li>
</ul>
<p>這類問題的核心是 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>。系統要決定是否需要持久化、是否允許重複投遞、失敗是否重試、<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 如何水平擴展。</p>
<p>下一步可讀：<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列與事件傳遞</a>。</p>
<h2 id="判讀看不見系統行為通常先看觀測平台">【判讀】看不見系統行為通常先看觀測平台</h2>
<p>可觀測性平台解決的是「服務發生什麼、為什麼發生、影響範圍多大」。如果事故發生後只能看單機 log，無法串起 request、事件、下游依賴與容量趨勢，選型應先進入可觀測性模組。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>API 偶爾變慢，但無法判斷是資料庫、外部 API 還是部署節點問題</li>
<li>queue lag 上升，但不知道 <a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 變快還是 consumer 變慢</li>
<li><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> client 斷線增加，但缺少連線生命週期與地區資訊</li>
</ul>
<p>這類問題的核心是操作訊號。log、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 與 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 需要共用欄位與關聯方式，才能讓工程師從症狀回到原因。</p>
<p>下一步可讀：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</p>
<h2 id="判讀服務交付不穩通常先看部署平台">【判讀】服務交付不穩通常先看部署平台</h2>
<p>部署平台解決的是「服務如何被啟動、更新、擴容、接流量與停止」。如果問題集中在 <a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a>、<a href="/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">liveness</a>、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>、container image 或資源限制，選型應先進入部署平台與網路入口模組。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>發版時部分 request 失敗，舊 pod 和新 pod 切換不穩</li>
<li>服務需要水平擴展，但 client 不知道該連到哪個 instance</li>
<li>shutdown 時仍有背景工作或長連線尚未清理</li>
</ul>
<p>這類問題的核心是平台合約。程式要提供 health、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、shutdown 與資源使用訊號；平台要提供流量入口、排程、發版與回滾能力。</p>
<p>下一步可讀：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a>。</p>
<p>進入規模成長路線時、能力地圖之外還要看四條額外章節：<a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a> 處理「該不該拆服務」、<a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖</a> 處理「該選哪家 vendor」、<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a> 處理「該怎麼擴容」、<a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 處理「擴容前先優化什麼」。</p>
<h2 id="小結">小結</h2>
<p>後端服務選型先從需求類型開始。資料長期存在先看資料庫，讀取壓力集中先看快取，工作跨出 request 先看訊息傳遞，系統行為缺少可見性先看觀測平台，服務交付不穩先看部署平台。分類清楚後，後續產品選型與實作細節才會有正確位置。</p>
]]></content:encoded></item><item><title>3.1 broker 基礎與投遞模型</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/</guid><description>&lt;p>這一章先建立訊息佇列的基本模型，後面的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a>、outbox 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 設計都會建立在這些語意上。&lt;/p>
&lt;p>訊息代理（broker）的核心責任是解耦 producer 與 consumer，讓非同步工作具備可排隊、可重試、可隔離的傳遞路徑。它定位在傳遞與協調層。&lt;/p>
&lt;h2 id="broker-跟-protocol-是兩個獨立的軸">broker 跟 protocol 是兩個獨立的軸&lt;/h2>
&lt;p>Broker 是訊息分發的具體實作產品（RabbitMQ、Kafka、NATS、EMQX）、protocol 是訊息交換的線路規格（AMQP、MQTT、STOMP、Kafka wire protocol）。兩個軸獨立、形成多對多關係：&lt;/p>
&lt;ul>
&lt;li>一個 broker 可實作多個 protocol：RabbitMQ 主走 AMQP、透過 plugin 也支援 MQTT 跟 STOMP；NATS 主走自家 protocol、JetStream 額外提供 KV 與 Object Store API&lt;/li>
&lt;li>一個 protocol 可被多個 broker 實作：MQTT 由 EMQX / HiveMQ / Mosquitto / RabbitMQ MQTT plugin 各自實作；AMQP 主要是 RabbitMQ 跟 Apache Qpid&lt;/li>
&lt;/ul>
&lt;p>選型討論時要分清「我需要的是 protocol（如 device 端要 MQTT 因為輕量 / IoT 標準）」還是「broker 產品（如 RabbitMQ vs EMQX 的運維 / 生態取捨）」。當 protocol 跟 broker 都需要、會出現 protocol 橋接場景 — 例：device 端透過 MQTT 連 RabbitMQ MQTT plugin、broker 內部把 MQTT topic 自動映射成 AMQP routing key、AMQP-side consumer 用 routing key 訂閱。&lt;/p>
&lt;p>這層分離也影響故障判讀：device 連不上是 protocol 層問題、broker 之間 routing 錯是 broker 內部 plugin / mapping 問題、consumer 收不到是 AMQP binding 問題 — 三層各自獨立、不能混為一談。&lt;/p>
&lt;h2 id="brokerqueueconsumer-的分工">broker、queue、consumer 的分工&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 管理訊息儲存、分發與確認流程；queue 或 topic 承載傳遞單位；consumer 承擔業務處理。分工清楚後，故障判讀才能定位在正確層級：投遞故障、消費故障或下游依賴故障。&lt;/p>
&lt;p>producer 發送成功只代表 broker 已接收（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/publisher-confirm/" data-link-title="Publisher Confirm" data-link-desc="說明 producer 如何確認 broker 已接收並承擔訊息">publisher confirm&lt;/a>），不代表業務結果完成。業務完成需要 consumer 提交副作用並確認進度。&lt;/p>
&lt;h2 id="push-與-pull-模型">push 與 pull 模型&lt;/h2>
&lt;p>push 模型由 broker 主動推送訊息，適合低延遲場景；pull 模型由 consumer 主動拉取，適合吞吐控制與批次處理。實務上常結合使用：broker 管理可見性與重試，consumer 控制節流與併發。&lt;/p>
&lt;p>模型選擇重點是背壓控制。當下游變慢時，系統是否能限制消費速率並保留恢復空間，是穩定性的關鍵。&lt;/p>
&lt;h2 id="傳遞語意delivery-semantics">傳遞語意（delivery semantics）&lt;/h2>
&lt;p>三種常見 delivery semantics：&lt;/p>
&lt;ol>
&lt;li>at-most-once：可能丟失，不重送，低延遲低成本。&lt;/li>
&lt;li>at-least-once：可能重複，需冪等保護，最常見實務語意。&lt;/li>
&lt;li>exactly-once：語意成本高，通常在特定邊界內成立，需要嚴格協議與系統支持。&lt;/li>
&lt;/ol>
&lt;p>實務上多數後端系統採 at-least-once，再用 consumer 去重與補償達到業務可接受結果。&lt;/p>
&lt;h2 id="ack--nack-流程">ack / nack 流程&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a> 是 delivery 控制點。ack 代表該訊息可從待處理集合移除；nack 代表稍後重試或分流。ack 時機過早會造成資料遺失，過晚會造成重複處理與堆積。&lt;/p>
&lt;p>穩定流程是：完成核心副作用後再 ack，暫時故障走受控重試，持續故障走 DLQ 隔離。&lt;/p></description><content:encoded><![CDATA[<p>這一章先建立訊息佇列的基本模型，後面的 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>、outbox 與 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 設計都會建立在這些語意上。</p>
<p>訊息代理（broker）的核心責任是解耦 producer 與 consumer，讓非同步工作具備可排隊、可重試、可隔離的傳遞路徑。它定位在傳遞與協調層。</p>
<h2 id="broker-跟-protocol-是兩個獨立的軸">broker 跟 protocol 是兩個獨立的軸</h2>
<p>Broker 是訊息分發的具體實作產品（RabbitMQ、Kafka、NATS、EMQX）、protocol 是訊息交換的線路規格（AMQP、MQTT、STOMP、Kafka wire protocol）。兩個軸獨立、形成多對多關係：</p>
<ul>
<li>一個 broker 可實作多個 protocol：RabbitMQ 主走 AMQP、透過 plugin 也支援 MQTT 跟 STOMP；NATS 主走自家 protocol、JetStream 額外提供 KV 與 Object Store API</li>
<li>一個 protocol 可被多個 broker 實作：MQTT 由 EMQX / HiveMQ / Mosquitto / RabbitMQ MQTT plugin 各自實作；AMQP 主要是 RabbitMQ 跟 Apache Qpid</li>
</ul>
<p>選型討論時要分清「我需要的是 protocol（如 device 端要 MQTT 因為輕量 / IoT 標準）」還是「broker 產品（如 RabbitMQ vs EMQX 的運維 / 生態取捨）」。當 protocol 跟 broker 都需要、會出現 protocol 橋接場景 — 例：device 端透過 MQTT 連 RabbitMQ MQTT plugin、broker 內部把 MQTT topic 自動映射成 AMQP routing key、AMQP-side consumer 用 routing key 訂閱。</p>
<p>這層分離也影響故障判讀：device 連不上是 protocol 層問題、broker 之間 routing 錯是 broker 內部 plugin / mapping 問題、consumer 收不到是 AMQP binding 問題 — 三層各自獨立、不能混為一談。</p>
<h2 id="brokerqueueconsumer-的分工">broker、queue、consumer 的分工</h2>
<p><a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 管理訊息儲存、分發與確認流程；queue 或 topic 承載傳遞單位；consumer 承擔業務處理。分工清楚後，故障判讀才能定位在正確層級：投遞故障、消費故障或下游依賴故障。</p>
<p>producer 發送成功只代表 broker 已接收（<a href="/blog/backend/knowledge-cards/publisher-confirm/" data-link-title="Publisher Confirm" data-link-desc="說明 producer 如何確認 broker 已接收並承擔訊息">publisher confirm</a>），不代表業務結果完成。業務完成需要 consumer 提交副作用並確認進度。</p>
<h2 id="push-與-pull-模型">push 與 pull 模型</h2>
<p>push 模型由 broker 主動推送訊息，適合低延遲場景；pull 模型由 consumer 主動拉取，適合吞吐控制與批次處理。實務上常結合使用：broker 管理可見性與重試，consumer 控制節流與併發。</p>
<p>模型選擇重點是背壓控制。當下游變慢時，系統是否能限制消費速率並保留恢復空間，是穩定性的關鍵。</p>
<h2 id="傳遞語意delivery-semantics">傳遞語意（delivery semantics）</h2>
<p>三種常見 delivery semantics：</p>
<ol>
<li>at-most-once：可能丟失，不重送，低延遲低成本。</li>
<li>at-least-once：可能重複，需冪等保護，最常見實務語意。</li>
<li>exactly-once：語意成本高，通常在特定邊界內成立，需要嚴格協議與系統支持。</li>
</ol>
<p>實務上多數後端系統採 at-least-once，再用 consumer 去重與補償達到業務可接受結果。</p>
<h2 id="ack--nack-流程">ack / nack 流程</h2>
<p><a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 是 delivery 控制點。ack 代表該訊息可從待處理集合移除；nack 代表稍後重試或分流。ack 時機過早會造成資料遺失，過晚會造成重複處理與堆積。</p>
<p>穩定流程是：完成核心副作用後再 ack，暫時故障走受控重試，持續故障走 DLQ 隔離。</p>
<h2 id="語意保證的不同實作機制">語意保證的不同實作機制</h2>
<p>同一層 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>、不同 broker 用不同協議機制達成。讀懂 broker 行為的關鍵、是辨認「at-least-once」這個語意承諾、底下是哪種具體機制負責 — 故障訊號跟操作旋鈕跟著不同。</p>
<p>三種常見實作機制：</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>代表 broker</th>
          <th>達成方式</th>
          <th>主要操作旋鈕</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>QoS handshake</td>
          <td>MQTT 系列</td>
          <td>client 與 broker 之間的多次握手（QoS 0 / 1 / 2）</td>
          <td>QoS 等級、session persistence、retained message</td>
      </tr>
      <tr>
          <td>Broker ACK + retry</td>
          <td>RabbitMQ、SQS、NATS</td>
          <td>consumer 處理後回 ack、未 ack 由 broker 重新投遞</td>
          <td>ack / visibility timeout、prefetch、DLQ</td>
      </tr>
      <tr>
          <td>Replication + commit</td>
          <td>Kafka、Pulsar</td>
          <td>producer 寫入後等待 replica commit、consumer 用 offset</td>
          <td>acks 等級（0 / 1 / all）、min.insync.replicas、ISR</td>
      </tr>
  </tbody>
</table>
<p>三個機制的工程含義不同。QoS handshake 把可靠性責任拉到 wire protocol 層、適合 device-to-broker 場景但 broker-to-consumer 還要另外處理；broker ACK 把責任放在 consumer 處理完才確認、適合「處理即承諾」的任務隊列；replication 把責任放在訊息已被多份保存、適合「寫入即承諾」的事件流。</p>
<h3 id="機制差異的故障訊號">機制差異的故障訊號</h3>
<p>機制決定故障表現。同樣是「訊息重複投遞」、不同機制要看不同訊號：</p>
<ul>
<li>QoS handshake：QoS 1 重傳是設計、QoS 2 重傳代表握手失敗 — 看 broker 端的 PUBREL / PUBCOMP 完成率</li>
<li>Broker ACK：ack timeout 觸發 <a href="/blog/backend/knowledge-cards/redelivery/" data-link-title="Redelivery" data-link-desc="說明 broker 重新投遞訊息時 consumer 需要承擔的重入責任">redelivery</a> 是設計、頻繁 redelivery 代表 consumer 處理慢或下游卡 — 看 consumer 處理時間 vs ack timeout、視訊號為 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></li>
<li>Replication：producer retry 觸發 duplicate 是設計、ISR shrink 代表 broker 副本不穩 — 看 ISR 狀態 vs producer acks 設定</li>
</ul>
<h3 id="機制差異的操作旋鈕">機制差異的操作旋鈕</h3>
<p>挑 broker 等同於挑「可調的旋鈕集合」。把「業務需要的語意」轉成「實際要調的旋鈕」、是 broker 選型落地的關鍵步驟：</p>
<ul>
<li>想保證「不丟」用 MQTT：QoS 等級提到 2、開 session persistence</li>
<li>想保證「不丟」用 RabbitMQ：consumer 走 manual ack、配 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a>、設 prefetch 限併發</li>
<li>想保證「不丟」用 Kafka：producer acks=all、min.insync.replicas ≥ 2、consumer commit-after-process</li>
</ul>
<p>機制不同、可調旋鈕不同、operator 要熟悉的訊號也不同。這是「broker 系統複雜度」的真實來源 — 不是「broker 難安裝」、而是「broker 旋鈕集合的學習與調校曲線」。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>producer 發送成功但業務結果缺漏</td>
          <td>投遞成功與處理成功語意混淆</td>
          <td>補 consumer 確認與結果對帳</td>
      </tr>
      <tr>
          <td>queue depth 穩定但延遲持續上升</td>
          <td>消費速率不足或重試佔用主通道</td>
          <td>分離重試隊列、調整併發與節流</td>
      </tr>
      <tr>
          <td>ack 成功率高但 duplicate 增加</td>
          <td>ack 時機與副作用提交順序不對齊</td>
          <td>延後 ack、補 idempotency</td>
      </tr>
      <tr>
          <td>nack 事件集中在同類訊息</td>
          <td>payload 或下游契約失配</td>
          <td>分流到 DLQ、修復契約後定向重播</td>
      </tr>
      <tr>
          <td>消費重啟後堆積迅速擴大</td>
          <td>背壓與可見性控制不足</td>
          <td>限制拉取窗口、調整重試間隔</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 broker 當成保證業務正確性的元件，會把消費責任與補償責任遺漏。broker 保證傳遞語意，業務正確性要由 consumer 設計承擔。</p>
<p>把 exactly-once 當成預設目標，也容易過度設計。先定義可接受失敗代價，再選擇對應語意，通常更符合實務。</p>
<h2 id="broker-規模化的角色變化">Broker 規模化的角色變化</h2>
<p>Broker 在規模化服務承擔的責任從「單隊列工具」轉到「平台治理問題」— 容量規劃焦點從擴 broker 變成多租戶隔離、配額管理、跨團隊觀測標準化。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka Infrastructure Evolution</a> — Uber 事件平台服務眾多團隊、focus 從 broker 容量是否充足轉到 team 之間的隔離邊界。對應 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a> — 規模化必然分層 cluster、按業務特性跟可靠性需求分配不同叢集、高優先 workload 跟低優先 workload 各自獨立。</p>
<p><strong>規模化的三個角色階段</strong>（依據 3.C6 / 3.C4 / 早期服務對照、整理出三個典型階段）：</p>
<ul>
<li><strong>單隊列工具</strong>（規模尚小階段）：一個 Kafka cluster、所有 service 共用、broker 擴容是主要工作、團隊各自管理自己的 topic</li>
<li><strong>多租戶平台</strong>（中大型階段）：跨團隊共用 cluster、平台 team 設定 quota、topic 命名規範、容量配額、觀測標準。3.C6 描述 Uber 在這階段「標準化 topic 治理與故障處理流程」、把跨團隊運維責任收斂到平台層</li>
<li><strong>分層治理平台</strong>（規模化階段）：不同業務特性走不同 cluster（critical / standard / experimental）、跨 cluster 路由跟治理變主要工作。3.C4 描述 LinkedIn「依流量與可靠性需求分層」、高優先 workload 提供獨立保護</li>
</ul>
<p>判讀含義：當 broker incident 影響多個 team 不相關業務、屬於該分層的訊號。規模化後焦點要轉向跨 team 隔離跟跨 cluster 治理、單純擴 broker 處理不了多租戶共擠的結構性問題。攻擊面跟控制面見 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章 Multi-tenant broker 隔離邊界</a>。</p>
<h2 id="queue-變跨區關鍵路徑的特殊挑戰">Queue 變跨區關鍵路徑的特殊挑戰</h2>
<p>當 queue 變成跨區關鍵路徑（payment、order、notification 都靠它）、容量規劃焦點從 throughput 變成 <em>discoverability</em> 跟 <em>routing freshness</em>。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS</a> — FOQS 從區域升級到全域、目標是讓災害期間 queue 仍可被存取、控制遷移期間的延遲跟可用性風險。Focus 從 queue 吞吐量轉到災害時的 broker 可達性、routing 狀態新鮮度、tenant 遷移節奏。</p>
<p><strong>跨區 queue 的設計挑戰</strong>：</p>
<ul>
<li><strong>Discoverability</strong>：client 在 region failover 後需透過 service discovery + DNS / health check 動態解析 broker endpoint、找到新 primary broker</li>
<li><strong>Routing freshness</strong>：broker topology 變更後、client 多久能拿到新 routing 表、stale routing 期間 message 流向錯 broker、要設定 routing TTL + 主動 refresh</li>
<li><strong>Tenant 遷移節奏</strong>：規模化跨區 queue 採分批 cutover、保留 client 連續性</li>
<li><strong>Stale routing 補貨延遲治理</strong>：routing 過時造成 message 累積在錯誤 broker、要設定 timeout + 重新發現機制、讓 client 重新發現新 broker 並切換到健康路徑</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>投遞語意可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 做回寫。先判讀事件是 delivery 層失配，還是 processing/recovery 層失配，再回到本章檢查 ack 時機、重試節奏與隔離策略是否清楚。
這個案例主要支撐的是「語意分層與投遞責任」判讀，不直接支撐資料庫 schema 演進或 LB timeout；若問題在資料模型或連線生命週期，應轉到 1.2 或 5.3。</p>
<p>若投遞成功但業務結果缺漏，先補齊語意分層，再分別回寫 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a> 與 <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>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 3.2 的交接：持久化與重試節奏回到 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">durable queue 與重試策略</a>。</li>
<li>與 3.4 的交接：消費恢復與去重回到 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計與去重</a>。</li>
<li>與 4.20 的交接：投遞與消費訊號納入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.12 的交接：重播與冪等驗證回到 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">Idempotency 與 Replay 驗證</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理持久化與重試控制，接著讀 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue 與重試策略</a>。要處理交易與發佈一致性，接著讀 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 與發佈一致性</a>。</p>
]]></content:encoded></item><item><title>4.1 log schema 與搜尋規劃</title><link>https://tarrragon.github.io/blog/backend/04-observability/log-schema/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/log-schema/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>structured &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> fields&lt;/li>
&lt;li>index 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>&lt;/li>
&lt;li>query pattern&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 是把事件紀錄從文字輸出變成可查詢資料的契約，責任是讓不同服務在事故時能用同一組欄位還原脈絡。&lt;/p>
&lt;p>這一頁處理的是欄位與搜尋路徑。log 的價值在於事故時能用穩定欄位找到同一個 request、同一個 tenant、同一個 dependency call 與同一段錯誤鏈，寫得多本身沒有幫助。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 log schema 時，先看 correlation fields 是否穩定，再看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 是否對齊查詢需求。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a> 與 service name 是否跨服務一致&lt;/li>
&lt;li>high-cardinality 欄位是否被放進可控索引，並受查詢價值與成本預算約束&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 是否依 operational debug、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">audit&lt;/a>、compliance 分層&lt;/li>
&lt;li>query pattern 是否能支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>log 欄位 schema 漂移、跨服務 correlation id 對不上&lt;/li>
&lt;li>事故時靠 grep 拼湊事件、無結構化查詢入口&lt;/li>
&lt;li>log 索引爆量、查詢退化但無清理流程&lt;/li>
&lt;li>log 含大量 free-form text、無一致關鍵欄位&lt;/li>
&lt;li>retention 策略全平、舊事件查不到 / 不該留的還在留&lt;/li>
&lt;/ul>
&lt;h2 id="查詢模式設計">查詢模式設計&lt;/h2>
&lt;p>Log 的寫入格式跟讀取需求是兩個不同的設計問題。寫入追求 schema 穩定與吞吐效率；讀取要在不同時間壓力下，用不同的查詢形狀取回不同精度的資料。同一份 structured log 至少被三種查詢模式讀取，每種模式對索引、延遲與結果形狀的要求不同。&lt;/p>
&lt;h3 id="即席診斷查詢">即席診斷查詢&lt;/h3>
&lt;p>事故中的查詢要在秒級內定位問題。典型操作是拿到一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 或 error code，加上 time window，撈出相關事件鏈。&lt;/p>
&lt;p>即席查詢的索引策略是把高頻過濾欄位放進結構化索引：service name、log level、error code、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a>。這些欄位的共同特徵是有界或半有界（error code 有限、request id 雖然無界但查詢時一定帶精確值），查詢時用等值匹配或短範圍掃描。&lt;/p>
&lt;p>即席查詢的反模式是對 free-text 欄位做全文搜尋當作主要診斷入口。全文搜尋適合探索性調查（「最近有沒有出現某個未預期的 exception message」），但事故中的時間壓力下，結構化欄位的精確查詢比全文搜尋快一到兩個數量級。&lt;/p>
&lt;h3 id="聚合趨勢查詢">聚合趨勢查詢&lt;/h3>
&lt;p>Dashboard 跟告警的查詢是定期的聚合計算：過去 5 分鐘的 error count by service、過去 1 小時的 log volume by level、某個 tenant 的 warning 趨勢。這類查詢不需要看單筆 log 的內容，而是需要 count / rate / group by 的聚合結果。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>structured <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a></li>
<li><a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> / <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> fields</li>
<li>index 與 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a></li>
<li>query pattern</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 是把事件紀錄從文字輸出變成可查詢資料的契約，責任是讓不同服務在事故時能用同一組欄位還原脈絡。</p>
<p>這一頁處理的是欄位與搜尋路徑。log 的價值在於事故時能用穩定欄位找到同一個 request、同一個 tenant、同一個 dependency call 與同一段錯誤鏈，寫得多本身沒有幫助。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 log schema 時，先看 correlation fields 是否穩定，再看 <a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a> 與 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否對齊查詢需求。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a> 與 service name 是否跨服務一致</li>
<li>high-cardinality 欄位是否被放進可控索引，並受查詢價值與成本預算約束</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否依 operational debug、<a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">audit</a>、compliance 分層</li>
<li>query pattern 是否能支援 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>log 欄位 schema 漂移、跨服務 correlation id 對不上</li>
<li>事故時靠 grep 拼湊事件、無結構化查詢入口</li>
<li>log 索引爆量、查詢退化但無清理流程</li>
<li>log 含大量 free-form text、無一致關鍵欄位</li>
<li>retention 策略全平、舊事件查不到 / 不該留的還在留</li>
</ul>
<h2 id="查詢模式設計">查詢模式設計</h2>
<p>Log 的寫入格式跟讀取需求是兩個不同的設計問題。寫入追求 schema 穩定與吞吐效率；讀取要在不同時間壓力下，用不同的查詢形狀取回不同精度的資料。同一份 structured log 至少被三種查詢模式讀取，每種模式對索引、延遲與結果形狀的要求不同。</p>
<h3 id="即席診斷查詢">即席診斷查詢</h3>
<p>事故中的查詢要在秒級內定位問題。典型操作是拿到一個 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 或 error code，加上 time window，撈出相關事件鏈。</p>
<p>即席查詢的索引策略是把高頻過濾欄位放進結構化索引：service name、log level、error code、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a>。這些欄位的共同特徵是有界或半有界（error code 有限、request id 雖然無界但查詢時一定帶精確值），查詢時用等值匹配或短範圍掃描。</p>
<p>即席查詢的反模式是對 free-text 欄位做全文搜尋當作主要診斷入口。全文搜尋適合探索性調查（「最近有沒有出現某個未預期的 exception message」），但事故中的時間壓力下，結構化欄位的精確查詢比全文搜尋快一到兩個數量級。</p>
<h3 id="聚合趨勢查詢">聚合趨勢查詢</h3>
<p>Dashboard 跟告警的查詢是定期的聚合計算：過去 5 分鐘的 error count by service、過去 1 小時的 log volume by level、某個 tenant 的 warning 趨勢。這類查詢不需要看單筆 log 的內容，而是需要 count / rate / group by 的聚合結果。</p>
<p>聚合查詢的負載特性跟即席查詢不同。即席查詢讀少量資料、要求低延遲；聚合查詢掃大量資料、容忍較高延遲但執行頻率高（dashboard 每 30 秒刷新一次 = 每分鐘 2 次相同的重聚合）。當 log volume 成長，重複計算聚合的成本會推高 query engine 負擔。</p>
<p>應對策略有兩種。一是在 log pipeline 把常用聚合轉成 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> — collector 端做 log-to-metric 轉換（例：把 <code>level=error</code> 的 log 計數轉成 error_log_total counter），dashboard 讀 metric 而非重掃 log。二是在查詢層設定 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 或快取，讓重複查詢直接取用預計算結果。</p>
<h3 id="鑑識回溯查詢">鑑識回溯查詢</h3>
<p>事後分析與合規稽核的查詢範圍大（跨天、跨週甚至跨月）、對完整性要求高、但延遲容忍也高（分鐘級回應可接受）。鑑識查詢常見的形狀是「某個 tenant 在過去 30 天內所有 authentication failure」或「某個 API 的 error 分布演變」。</p>
<p>鑑識查詢的儲存設計跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 直接相關。Hot tier 保留最近數天的 full-index log，warm tier 保留數週的部分索引或壓縮 log，cold tier 保留數月到數年的歸檔 log。鑑識查詢命中 cold tier 時，系統可能需要 rehydrate（把歸檔資料暫時載回可查詢狀態），這個操作本身需要時間和臨時儲存空間。</p>
<p>鑑識場景的關鍵設計決策是「哪些欄位在 cold tier 仍可查詢」。全部欄位都保留索引成本太高；只保留 timestamp + service name + tenant 的最小索引，能支援基本的範圍掃描，細節再用 rehydrate 後的全文搜尋補。</p>
<h3 id="三種模式的資源隔離">三種模式的資源隔離</h3>
<p>三種查詢模式搶同一個 query engine 時，聚合查詢的持續負載會擠壓即席查詢的回應速度。事故中團隊最需要即席查詢的低延遲，但此時 dashboard 也在高頻刷新聚合查詢，兩者競爭 query 資源。</p>
<p>可操作的隔離方式是讓即席查詢跟聚合查詢走不同的 query priority 或 query queue。Elasticsearch 的 search thread pool、Loki 的 query-frontend queue、Datadog 的 query quota 都提供某種程度的查詢隔離。設計時要把即席查詢的延遲 SLA 當作硬性約束，聚合查詢的延遲可以被彈性排程。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.7 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> / cost：label 預算與保留階梯</li>
<li>04.8 訊號治理閉環：log-based alert 的生命週期</li>
<li>04.12 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>：稽核訊號跟 operational log 的邊界</li>
<li>04.23 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">觀測查詢設計</a>：跨訊號類型的讀取路徑系統設計</li>
</ul>
]]></content:encoded></item><item><title>5.1 container 與 runtime</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</guid><description>&lt;p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。&lt;/p>
&lt;h2 id="image-與建置責任">image 與建置責任&lt;/h2>
&lt;p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。&lt;/p>
&lt;p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。&lt;/p>
&lt;h3 id="基底映像選擇">基底映像選擇&lt;/h3>
&lt;p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語言官方映像&lt;/strong>（&lt;code>python:3.12&lt;/code>、&lt;code>node:20&lt;/code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。&lt;/li>
&lt;li>&lt;strong>slim / alpine 變體&lt;/strong>（&lt;code>python:3.12-slim&lt;/code>、&lt;code>node:20-alpine&lt;/code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。&lt;/li>
&lt;li>&lt;strong>distroless&lt;/strong>（&lt;code>gcr.io/distroless/base&lt;/code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。&lt;/li>
&lt;li>&lt;strong>自建基底&lt;/strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。&lt;/li>
&lt;/ul>
&lt;p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。&lt;/p>
&lt;h3 id="建置可重現性">建置可重現性&lt;/h3>
&lt;p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。&lt;/p>
&lt;p>可重現建置的關鍵實踐：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>鎖定依賴版本&lt;/strong>：&lt;code>go.sum&lt;/code>、&lt;code>package-lock.json&lt;/code>、&lt;code>poetry.lock&lt;/code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。&lt;/li>
&lt;li>&lt;strong>Multi-stage build&lt;/strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。&lt;/li>
&lt;li>&lt;strong>避免 image 中殘留敏感資訊&lt;/strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。&lt;/li>
&lt;li>&lt;strong>image 標記策略&lt;/strong>：&lt;code>latest&lt;/code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。&lt;/li>
&lt;/ol>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。&lt;/p>
&lt;h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為&lt;/h2>
&lt;p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。&lt;/p>
&lt;p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。&lt;/p>
&lt;h3 id="pid-1-與信號處理">PID 1 與信號處理&lt;/h3>
&lt;p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：&lt;/p>
&lt;p>&lt;strong>Shell 作為 PID 1&lt;/strong>：&lt;code>ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;java -jar app.jar&amp;quot;]&lt;/code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 &lt;code>exec&lt;/code> 或直接用 exec form：&lt;code>ENTRYPOINT [&amp;quot;java&amp;quot;, &amp;quot;-jar&amp;quot;, &amp;quot;app.jar&amp;quot;]&lt;/code>。&lt;/p>
&lt;p>&lt;strong>多進程容器&lt;/strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 &lt;code>shareProcessNamespace: true&lt;/code> 讓 kubelet 處理。&lt;/p></description><content:encoded><![CDATA[<p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。</p>
<h2 id="image-與建置責任">image 與建置責任</h2>
<p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。</p>
<p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。</p>
<h3 id="基底映像選擇">基底映像選擇</h3>
<p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：</p>
<ul>
<li><strong>語言官方映像</strong>（<code>python:3.12</code>、<code>node:20</code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。</li>
<li><strong>slim / alpine 變體</strong>（<code>python:3.12-slim</code>、<code>node:20-alpine</code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。</li>
<li><strong>distroless</strong>（<code>gcr.io/distroless/base</code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。</li>
<li><strong>自建基底</strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。</li>
</ul>
<p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。</p>
<h3 id="建置可重現性">建置可重現性</h3>
<p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。</p>
<p>可重現建置的關鍵實踐：</p>
<ol>
<li><strong>鎖定依賴版本</strong>：<code>go.sum</code>、<code>package-lock.json</code>、<code>poetry.lock</code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。</li>
<li><strong>Multi-stage build</strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。</li>
<li><strong>避免 image 中殘留敏感資訊</strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。</li>
<li><strong>image 標記策略</strong>：<code>latest</code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。</p>
<h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為</h2>
<p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。</p>
<p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。</p>
<h3 id="pid-1-與信號處理">PID 1 與信號處理</h3>
<p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：</p>
<p><strong>Shell 作為 PID 1</strong>：<code>ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java -jar app.jar&quot;]</code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 <code>exec</code> 或直接用 exec form：<code>ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code>。</p>
<p><strong>多進程容器</strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 <code>shareProcessNamespace: true</code> 讓 kubelet 處理。</p>
<p><strong>啟動腳本的信號遮蔽</strong>：entrypoint script 在初始化階段（下載 config、等依賴就緒）捕捉 SIGTERM 做清理，但如果清理邏輯卡住，整個 shutdown 會被阻塞。啟動腳本的 trap handler 要有 timeout，避免把 graceful shutdown 變成 ungraceful hang。</p>
<h3 id="啟動時間對部署策略的影響">啟動時間對部署策略的影響</h3>
<p>啟動時間直接影響 rollout 的最短觀察窗。一個啟動需 60 秒的服務，rollout 每批至少要等 60 秒 + 觀察窗口才能確認新版本穩定。啟動時間的組成與壓縮策略見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>image 體積也影響啟動時間——image pull 在冷啟動（節點上沒有這個 image 的快取）時占啟動時間的顯著比例。1GB image 在 100Mbps 網路下需要 ~80 秒 pull。壓縮 image 體積同時改善啟動速度與節省 registry 頻寬。</p>
<h2 id="resource-limit">resource limit</h2>
<p>CPU/memory <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a> 隔離資源競爭並保護叢集穩態。限制過低會導致頻繁節流與重啟，過高會壓縮同節點容量並放大鄰近工作負載風險。</p>
<p>限制設計要依服務流量型態與 GC/執行時特性調整，並與 autoscaling、rollout 批次策略一起評估。</p>
<h3 id="cpu-request-與-limit-的設定策略">CPU request 與 limit 的設定策略</h3>
<p>CPU 限制有兩個參數：request（排程保證）與 limit（硬上限）。兩者的關係決定服務在負載變動下的行為：</p>
<ul>
<li><strong>request = limit</strong>（guaranteed QoS）：CPU 用量穩定可預測，不會被 throttle 也不會超用。代價是無法在閒時借用節點剩餘 CPU。適合延遲敏感的 API 服務。</li>
<li><strong>request &lt; limit</strong>（burstable QoS）：平時用 request 保證的份額，高峰時可用到 limit。代價是當節點 CPU 競爭激烈時，所有 burstable pod 同時被 throttle，延遲會一起劣化。適合批次處理或對延遲要求不高的服務。</li>
<li><strong>不設 limit</strong>（只設 request）：服務可用到節點全部剩餘 CPU。Kubernetes 社群近年傾向這個做法——CPU throttle 常比 CPU contention 更難排查。代價是需要良好的觀測來偵測 noisy neighbor。</li>
</ul>
<h3 id="memory-limit-與-oom-的判讀">Memory limit 與 OOM 的判讀</h3>
<p>memory limit 是硬邊界——超過就 OOM kill，不走 graceful shutdown。OOM kill 的判讀分兩種情境：</p>
<p><strong>真正的 memory leak</strong>：記憶體使用量隨時間單調上升，GC 無法回收。修法在程式碼層。memory limit 只是延後問題爆發，不是解法。</p>
<p><strong>memory limit 設太低</strong>：服務在高峰流量下的正常記憶體使用超過 limit。常見於 JVM 服務——JVM heap + metaspace + native memory + thread stack 的總和超出 container memory limit。設 limit 時要用「峰值實際使用 + headroom」而非「平均使用」。</p>
<p>GC-based runtime（JVM、.NET、Go）要注意 container-aware memory 設定。早期 JVM 不認 cgroup memory limit，會按宿主機記憶體計算 heap 大小，導致 heap 配置超過 container limit。現代 JVM（Java 10+）預設啟用 container awareness（<code>-XX:+UseContainerSupport</code>），Go runtime 1.19+ 支援 <code>GOMEMLIMIT</code>。</p>
<h3 id="資源設定與-autoscaling-的協同">資源設定與 autoscaling 的協同</h3>
<p>resource request 同時決定 HPA（Horizontal Pod Autoscaler）的觸發基線。request 設太高時，CPU utilization % 會偏低，HPA 不會觸發擴容，導致服務在真正需要擴容前已經出現延遲。request 設太低時，utilization % 容易衝高，HPA 頻繁擴容，造成 pod 數量抖動。</p>
<p>穩定做法是先在 staging 環境跑負載測試確認服務的實際資源消耗曲線，再以 p90 負載的 CPU / memory 使用作為 request 基線。</p>
<h2 id="runtime-config">runtime config</h2>
<p>環境差異要顯式化才能追蹤——<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 承擔這個責任。配置來源、版本、更新節奏都應可追蹤。高風險設定需配合 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 策略，避免同批大規模變更。</p>
<p>runtime 配置與映像版本要保留相容窗口，讓部署與回退可分步進行。</p>
<h3 id="配置注入方式與取捨">配置注入方式與取捨</h3>
<p>配置注入容器有三條路徑，各自有不同的版本追蹤與更新語意：</p>
<table>
  <thead>
      <tr>
          <th>注入方式</th>
          <th>版本追蹤</th>
          <th>更新行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>環境變數</td>
          <td>跟 deployment spec 一起版控</td>
          <td>需要 pod restart 才生效</td>
          <td>啟動時固定的設定（DB URL、port）</td>
      </tr>
      <tr>
          <td>ConfigMap mount</td>
          <td>ConfigMap 版本</td>
          <td>自動更新（kubelet sync period 內）</td>
          <td>需要動態更新的非敏感設定</td>
      </tr>
      <tr>
          <td>Secret mount</td>
          <td>Secret 版本</td>
          <td>自動更新（同 ConfigMap）</td>
          <td>credential、cert、API key</td>
      </tr>
      <tr>
          <td>外部 config store</td>
          <td>config store 內版本</td>
          <td>應用主動拉取或 sidecar push</td>
          <td>feature flag、複雜設定邏輯</td>
      </tr>
  </tbody>
</table>
<p>環境變數最簡單但更新需要 restart。ConfigMap mount 可以動態更新但應用要能偵測檔案變化並 reload。外部 config store（Consul KV、AWS AppConfig、Feature Flag service）最靈活但引入了額外依賴。</p>
<p>設定變更跟 image 變更走不同路徑時，要確保兩者的版本可以交叉相容。版本 v2 的 image 搭版本 A 的 config 能跑、版本 v1 的 image 搭版本 B 的 config 也能跑——rollback image 但 config 沒回退、或 rollback config 但 image 沒回退的情境下、服務不應崩潰。這個相容窗口的設計責任見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Config Boundary</a>。</p>
<h2 id="遷移期的-runtime-穩定性">遷移期的 Runtime 穩定性</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a>：揭露「平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略」。遷移到 managed 平台後，runtime 層面的變化包含 container runtime 版本（containerd vs Docker shim）、node OS、storage driver、network plugin。這些變化可能改變 image pull 速度、filesystem 行為、DNS 解析路徑。</p>
<p>遷移前後的 runtime 驗證應包含：</p>
<ol>
<li><strong>image pull 時間比較</strong>：新 registry / 新 node 的 pull 速度是否在 startup timeout 內。</li>
<li><strong>filesystem 行為</strong>：log 寫入路徑、tmp 目錄、volume mount 行為在新 runtime 下是否一致。</li>
<li><strong>DNS 解析</strong>：新叢集的 CoreDNS / node-local DNS 設定是否影響服務的依賴連線建立速度。</li>
<li><strong>resource 行為</strong>：新 node type 的 CPU 架構（x86 vs ARM）、memory page size 是否影響服務性能特性。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新版本容器啟動時間顯著增加</td>
          <td>image 體積或初始化步驟膨脹</td>
          <td>優化映像層、拆分初始化流程</td>
      </tr>
      <tr>
          <td>rollout 初期出現 OOM/CPU throttle</td>
          <td>resource limit 與實際負載不匹配</td>
          <td>重設 request/limit、調整併發與批次</td>
      </tr>
      <tr>
          <td>配置變更後特定環境異常</td>
          <td>runtime config 管理不一致</td>
          <td>統一配置來源、補版本追蹤與差異檢查</td>
      </tr>
      <tr>
          <td>容器停止時請求中斷率上升</td>
          <td>signal/drain 協調不足</td>
          <td>補 shutdown hook、對齊 termination 流程</td>
      </tr>
      <tr>
          <td>同版本在不同節點行為差異大</td>
          <td>runtime 依賴未固定或環境漂移</td>
          <td>收斂基底映像、鎖定依賴與建置流程</td>
      </tr>
      <tr>
          <td>JVM 服務 OOM 但 heap 未用滿</td>
          <td>native memory / metaspace 超出 limit</td>
          <td>調整 MaxMetaspaceSize、限制 thread 數</td>
      </tr>
      <tr>
          <td>冷啟動節點上服務啟動超慢</td>
          <td>image pull 時間在啟動時間中占比高</td>
          <td>壓縮 image 體積、啟用 image cache</td>
      </tr>
      <tr>
          <td>rollback 後行為跟上次部署不同</td>
          <td>建置不可重現、tag 覆蓋</td>
          <td>改用 commit SHA 標記、鎖定依賴版本</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Container 常被簡化成「打包完就好」的步驟，結果是部署風險被後移到 rollout 階段。runtime 產物穩定性不足時，後續 probe、canary、rollback 都只能被動補救。</p>
<p>把資源限制設成平台預設值，也常造成高峰期不穩。限制應反映服務真實耗用模式，不應只追求表面資源利用率。</p>
<p>把 <code>latest</code> tag 當成版本標記，會讓 rollback 指向無法預測的 image。image tag 在 registry 上是 mutable——同一個 tag 可以被覆蓋指向新 image。用 immutable tag（commit SHA、content digest）才能保證 rollback 的確定性。</p>
<p>把所有配置都用環境變數注入，會讓設定變更跟 image 部署綁在一起。需要動態更新的設定（feature flag、rate limit 閾值）應該用 ConfigMap mount 或外部 config store，讓設定變更不需要 pod restart。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>runtime 穩定性可用 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a> 回寫。先看遷移期內啟動行為與資源限制如何影響切流，再對照本章檢查 image、entrypoint、limit 與 config 相容窗口。這個案例主要支撐的是「執行環境可重現性」判讀——遷移到新叢集時，image 不變但 runtime 環境變了（node OS、container runtime 版本、network plugin），runtime 穩定性的前提是 image 本身不依賴特定宿主環境的行為。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a> 從另一個角度支撐：managed 平台接管 runtime 基礎設施後，container runtime 版本升級由平台控制，團隊要能驗證自家 image 在新 runtime 版本下行為一致。</p>
<p>若同版容器在不同節點出現分歧行為，先追建置來源與 runtime config 版本鏈，確認是依賴漂移還是環境漂移，再把關鍵證據收斂到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。不直接支撐 service discovery TTL 或 queue replay 邏輯；若根因在定位鏈路或重播流程，應轉到 5.4 或 3.4。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：部署批次與探針策略回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：流量進出與連線收斂回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.6 的交接：startup / readiness / drain 的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 4.20 的交接：啟動與資源證據回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：放行與回退條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 7.3 的交接：image 安全基線與攻擊面回到 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 runtime 行為接到部署收斂，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看切流與退場條件，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看 runtime 層的生命週期如何被平台表達，接著讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
]]></content:encoded></item><item><title>6.1 CI pipeline</title><link>https://tarrragon.github.io/blog/backend/06-reliability/ci-pipeline/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/ci-pipeline/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline&lt;/a> 把快速回饋、慢速驗證與可重現產物切成不同層，讓每次變更都能在一致條件下被判讀。&lt;/p>
&lt;p>這一層關心的是「變更能不能被穩定驗證」。pipeline 的價值在於分層、隔離與可追蹤，讓 flaky 訊號不會直接污染放行判斷。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>CI 的健康度先看回饋節奏，再看訊號品質。fast path 應該覆蓋最常見的破壞面，slow path 負責深層驗證，artifact 則要能從同一份輸入重播。&lt;/p>
&lt;p>判讀時先看四件事：&lt;/p>
&lt;ul>
&lt;li>stage 是否按成本與風險分層&lt;/li>
&lt;li>artifact 是否重用，不是每次從 source 重建&lt;/li>
&lt;li>environment variables 是否封裝，避免跨環境漂移&lt;/li>
&lt;li>flaky test 是否有治理路徑，而不是只靠 retry&lt;/li>
&lt;/ul>
&lt;h2 id="分層策略">分層策略&lt;/h2>
&lt;p>CI 分層的責任是讓不同成本的驗證跑在不同時機，讓最常見的破壞面最快被攔住，高成本驗證只在值得時跑。&lt;/p>
&lt;h3 id="fast-path">Fast path&lt;/h3>
&lt;p>fast path 在每次 push 觸發，目標是 5 分鐘內回饋。涵蓋 lint、type check、unit test 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test。這一層只驗證單一變更的語法與邏輯正確性，不碰外部依賴。&lt;/p>
&lt;p>fast path 結果可信的條件是測試不依賴外部狀態。當 unit test 需要真實 DB 或 broker，它就不再屬於 fast path — 移到 slow path，或用 contract test 替代跨服務驗證。&lt;/p>
&lt;h3 id="slow-path">Slow path&lt;/h3>
&lt;p>slow path 在 merge request 觸發，允許較長執行時間（15-45 分鐘）。涵蓋 integration test、security scan、load baseline 與跨服務 schema 相容性。這一層用真實依賴驗證變更在服務邊界上的行為。&lt;/p>
&lt;p>Microsoft 的&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">變更治理實踐&lt;/a>把變更按風險分層，高風險變更（schema migration、payment path、config rollout）走更完整的 slow path，低風險變更只需 fast path 通過。這種分層讓 CI 資源集中在真正需要深層驗證的變更上，同時維持低風險變更的交付速度。&lt;/p>
&lt;h3 id="scheduled-path">Scheduled path&lt;/h3>
&lt;p>scheduled path 定期（每日或每週）執行，涵蓋 full regression、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/fuzz-campaign/" data-link-title="6.3 fuzz campaign" data-link-desc="用自動化輸入探索覆蓋未知邊界：target 設計、corpus 管理、crash reproduction 與 CI 整合">fuzz campaign&lt;/a>、chaos smoke test 與長時間 soak test。這一層驗證的是累積退化，而不是單次變更的破壞。&lt;/p>
&lt;p>scheduled path 的判讀不看單次 pass/fail，而是看趨勢：coverage delta 是否持續下降、fuzz corpus 是否收斂、regression 新增 failure 是否集中在特定模組。&lt;/p>
&lt;h2 id="artifact-管理">Artifact 管理&lt;/h2>
&lt;p>Artifact 讓同一份 build output 能從 CI 一路到 staging 到 production，每一步都可重播。&lt;/p>
&lt;p>immutable artifact 的核心約束是 build 一次、部署多次。CI 產出的 container image 或 binary 帶版本標籤（commit hash + build number），後續環境不重新 build，只替換 config。這樣才能確保 staging 驗證通過的產物跟 production 部署的產物是同一份。&lt;/p>
&lt;p>cache 策略影響 CI 回饋速度與可信度的平衡。dependency cache（npm / go mod / pip）加速 build，但需要定期 invalidation 避免過期依賴殘留。build output cache 則需要嚴格的 key 設計，確保 source 變更後不會沿用舊 artifact。&lt;/p>
&lt;p>Stripe 的&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">零停機遷移實踐&lt;/a>對 artifact 有額外要求：交易路徑的變更需要 artifact 能重播到相同狀態，確保 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 驗證在 CI 與 production 看到一致的行為。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a> 把快速回饋、慢速驗證與可重現產物切成不同層，讓每次變更都能在一致條件下被判讀。</p>
<p>這一層關心的是「變更能不能被穩定驗證」。pipeline 的價值在於分層、隔離與可追蹤，讓 flaky 訊號不會直接污染放行判斷。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>CI 的健康度先看回饋節奏，再看訊號品質。fast path 應該覆蓋最常見的破壞面，slow path 負責深層驗證，artifact 則要能從同一份輸入重播。</p>
<p>判讀時先看四件事：</p>
<ul>
<li>stage 是否按成本與風險分層</li>
<li>artifact 是否重用，不是每次從 source 重建</li>
<li>environment variables 是否封裝，避免跨環境漂移</li>
<li>flaky test 是否有治理路徑，而不是只靠 retry</li>
</ul>
<h2 id="分層策略">分層策略</h2>
<p>CI 分層的責任是讓不同成本的驗證跑在不同時機，讓最常見的破壞面最快被攔住，高成本驗證只在值得時跑。</p>
<h3 id="fast-path">Fast path</h3>
<p>fast path 在每次 push 觸發，目標是 5 分鐘內回饋。涵蓋 lint、type check、unit test 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test。這一層只驗證單一變更的語法與邏輯正確性，不碰外部依賴。</p>
<p>fast path 結果可信的條件是測試不依賴外部狀態。當 unit test 需要真實 DB 或 broker，它就不再屬於 fast path — 移到 slow path，或用 contract test 替代跨服務驗證。</p>
<h3 id="slow-path">Slow path</h3>
<p>slow path 在 merge request 觸發，允許較長執行時間（15-45 分鐘）。涵蓋 integration test、security scan、load baseline 與跨服務 schema 相容性。這一層用真實依賴驗證變更在服務邊界上的行為。</p>
<p>Microsoft 的<a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">變更治理實踐</a>把變更按風險分層，高風險變更（schema migration、payment path、config rollout）走更完整的 slow path，低風險變更只需 fast path 通過。這種分層讓 CI 資源集中在真正需要深層驗證的變更上，同時維持低風險變更的交付速度。</p>
<h3 id="scheduled-path">Scheduled path</h3>
<p>scheduled path 定期（每日或每週）執行，涵蓋 full regression、<a href="/blog/backend/06-reliability/fuzz-campaign/" data-link-title="6.3 fuzz campaign" data-link-desc="用自動化輸入探索覆蓋未知邊界：target 設計、corpus 管理、crash reproduction 與 CI 整合">fuzz campaign</a>、chaos smoke test 與長時間 soak test。這一層驗證的是累積退化，而不是單次變更的破壞。</p>
<p>scheduled path 的判讀不看單次 pass/fail，而是看趨勢：coverage delta 是否持續下降、fuzz corpus 是否收斂、regression 新增 failure 是否集中在特定模組。</p>
<h2 id="artifact-管理">Artifact 管理</h2>
<p>Artifact 讓同一份 build output 能從 CI 一路到 staging 到 production，每一步都可重播。</p>
<p>immutable artifact 的核心約束是 build 一次、部署多次。CI 產出的 container image 或 binary 帶版本標籤（commit hash + build number），後續環境不重新 build，只替換 config。這樣才能確保 staging 驗證通過的產物跟 production 部署的產物是同一份。</p>
<p>cache 策略影響 CI 回饋速度與可信度的平衡。dependency cache（npm / go mod / pip）加速 build，但需要定期 invalidation 避免過期依賴殘留。build output cache 則需要嚴格的 key 設計，確保 source 變更後不會沿用舊 artifact。</p>
<p>Stripe 的<a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">零停機遷移實踐</a>對 artifact 有額外要求：交易路徑的變更需要 artifact 能重播到相同狀態，確保 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 驗證在 CI 與 production 看到一致的行為。</p>
<h2 id="flaky-test-治理">Flaky test 治理</h2>
<p>flaky test 的責任是讓 CI 訊號維持可信度。當 flaky 率持續上升，團隊會開始忽略 CI 結果，pipeline 從可靠性 gate 退化成形式流程。</p>
<h3 id="識別">識別</h3>
<p>flaky 識別靠 retry 分析。當同一個 test case 在同一份 commit 上連續跑出不同結果，那就是 flaky 候選。按連續失敗 / 成功交替的頻率排序，比按失敗率排序更能抓到高噪音來源。</p>
<h3 id="隔離">隔離</h3>
<p>quarantine queue 是把已識別的 flaky test 從 gate-blocking path 移到 non-blocking path。quarantine 的目的是保護 gate 判讀可信度，同時維持 flaky 修復的追蹤壓力。quarantine 不是永久停靠 — 超過修復期限的 flaky test 必須決定是修復還是刪除。</p>
<h3 id="判讀門檻">判讀門檻</h3>
<p>flaky 率超過 5% 時，CI gate 的訊號開始失真：團隊無法確定 failure 是真回歸還是 flaky。超過 10% 時，CI pipeline 實質上失去 gate 功能 — retry 變成常態，failure 預設被忽略。此時應暫停新功能開發，集中修復 flaky backlog。這些門檻是基於中大型測試套件（500+ test cases）的經驗值。測試套件較小時，單一 flaky test 的比率衝擊更大，門檻應更低。</p>
<h2 id="environment-隔離">Environment 隔離</h2>
<p>CI 環境的隔離程度決定了測試結果的可信度下限。</p>
<h3 id="runner-隔離">Runner 隔離</h3>
<p>shared runner 會把不同 PR 的測試跑在同一台機器上。當 integration test 需要佔用 port、寫入 local state 或消耗大量記憶體，跨 job 干擾就會出現。ephemeral runner（每次 job 用乾淨環境）消除這類問題，但成本更高。判斷點是測試是否依賴 local state — 有依賴就用 ephemeral。</p>
<h3 id="secret-管理">Secret 管理</h3>
<p>CI secret（API key、DB credential、cloud token）需要按環境隔離。staging secret 不應該在 PR pipeline 可用，production secret 不應該在 staging pipeline 可用。secret 洩露的常見路徑是 CI log 輸出與 artifact 殘留 — 兩處都需要遮罩。</p>
<h3 id="load-test-資源池">Load test 資源池</h3>
<p>LinkedIn 的<a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">容量 headroom 實踐</a>把自動化壓測接進 CI。當 load test 跑在 CI 環境時，需要獨立資源池，避免壓測流量影響其他 pipeline job 的執行速度與穩定性。load test runner 的 quota 跟一般 CI runner 分開管理。</p>
<h2 id="ci-作為-release-gate-輸入">CI 作為 Release Gate 輸入</h2>
<p>CI 的最終產出不只是 pass/fail，而是一組可供 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 判讀的 evidence。</p>
<table>
  <thead>
      <tr>
          <th>產出</th>
          <th>判讀用途</th>
          <th>下游消費者</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>pipeline status</td>
          <td>所有 stage 是否通過</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a></td>
      </tr>
      <tr>
          <td>test coverage delta</td>
          <td>本次變更是否降低覆蓋率</td>
          <td><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 perf regression gate</a></td>
      </tr>
      <tr>
          <td>artifact checksum</td>
          <td>部署產物是否與 CI 產出一致</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 evidence handoff</a></td>
      </tr>
      <tr>
          <td>flaky rate snapshot</td>
          <td>gate 判讀可信度是否在可接受範圍</td>
          <td><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 reliability metrics</a></td>
      </tr>
  </tbody>
</table>
<p>Google 的 <a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">error budget 政策</a>把 CI 定位成 release gate 的前置訊號來源：CI pipeline 產出的 evidence 直接進入 error budget 判讀流程。當 budget 消耗加速時，CI gate 的門檻隨之提高 — 從只需 fast path 通過，升級到要求 slow path 全部通過加人工 review。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google</a>：CI pipeline status 是 error budget 政策的前置訊號，budget 消耗速度直接影響 CI gate 門檻高低。</li>
<li><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft</a>：按變更風險分層走不同 CI path，高風險變更需要更完整的 slow path 驗證。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn L1</a>：容量 headroom 綁值班分層，CI 回饋是容量決策的輸入。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">LinkedIn L2</a>：自動化壓測接進 CI，load test 需要獨立資源池避免干擾其他 pipeline job。</li>
<li><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe</a>：交易路徑的 idempotency 測試在 CI 跑，artifact 必須能重播到相同狀態。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>意義</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI 時長 &gt; 30 min</td>
          <td>fast path 混入了 slow path 測試</td>
          <td>重新分層，把 integration test 移到 merge gate</td>
      </tr>
      <tr>
          <td>fast / slow 沒分層</td>
          <td>每次 push 跑全部測試，回饋太慢</td>
          <td>拆 fast path（&lt; 5 min）與 slow path（&lt; 45 min）</td>
      </tr>
      <tr>
          <td>flaky 率 &gt; 5%</td>
          <td>gate 判讀可信度開始下降</td>
          <td>啟動 quarantine + 集中修復週期</td>
      </tr>
      <tr>
          <td>artifact 每次重建</td>
          <td>無法確認 staging 跟 production 同份</td>
          <td>改成 build once、deploy many</td>
      </tr>
      <tr>
          <td>env var 跨環境寫死</td>
          <td>staging 與 prod 行為不同</td>
          <td>改用 per-environment secret injection</td>
      </tr>
      <tr>
          <td>retry 成功率 &gt; 20% 且被視為 pipeline 通過</td>
          <td>真回歸被 flaky retry 遮蓋</td>
          <td>retry pass 不等於 gate pass，需人工確認</td>
      </tr>
      <tr>
          <td>flaky test 無 owner、修復靠志願者</td>
          <td>test 跟 team 責任未對齊</td>
          <td>建立 test ownership registry、每個 test file 或 suite 有明確 owner team</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a>：把跨服務契約納入 CI fast path</li>
<li><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 perf regression gate</a>：把效能 baseline 變成 CI slow path gate</li>
<li><a href="/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 environment parity</a>：CI 環境隔離是 parity 的前置條件</li>
<li><a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 test data</a>：把 fixture / seed 納入 CI artifact 管理</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：CI evidence 是 release gate 的主要輸入</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 evidence handoff</a>：CI artifact checksum 進入證據交接</li>
</ul>
]]></content:encoded></item><item><title>8.1 事故分級與啟動條件</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 與 trigger 是把事故從「有問題」變成「需要開始協作」的門檻。incident severity 定義的是這次事故應該用多大規模的協作來處理，trigger 定義的是什麼訊號足以啟動這個協作。當兩者被分開寫清楚，團隊就不會把所有異常都當成同一種事件，也不會在影響面已經擴大後才開始反應。&lt;/p>
&lt;p>這個節點先處理啟動，再處理升級。先定義什麼情況要 page、要不要拉 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a>、要不要進 status update，然後才處理 severity 分級的細節。這樣讀，會比先背 severity level 再找案例更接近真實事故運作。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> criteria&lt;/li>
&lt;li>user impact signals&lt;/li>
&lt;li>trigger thresholds&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> handoff&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>事故啟動延遲於擴散、影響面已擴大才升級&lt;/li>
&lt;li>severity 分級靠 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 直覺、無 user impact 量化&lt;/li>
&lt;li>升級條件不清、跨團隊重複 page 同事故&lt;/li>
&lt;li>同類事件不同 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 給不同 severity&lt;/li>
&lt;li>啟動門檻過高（漏判）或過低（噪音）、無校準流程&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 的責任是把影響面說清楚。當服務開始退化時，先看使用者是否真的受影響，再看影響是否跨產品、跨 region、跨 tenant，最後才決定 severity。這個順序很重要，因為它決定了團隊是先止血還是先爭論標籤。&lt;/p>
&lt;p>啟動條件的責任是把協作拉起來。當 trigger 被觸發時，團隊應該立刻知道誰要接手、誰要記錄、誰要對外通訊，以及下一次檢視的時間點。這種節奏不需要等事故結束才討論，因為事故本身就是路由。&lt;/p>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;p>AWS S3 適合用來看控制面事故如何把區域級影響迅速擴大，因為這類事件最容易讓 severity 上升到需要更大範圍協作。GitHub 適合用來看 replication 與 split-brain 的分級，因為資料一致性問題會直接拉長復原時間。Slack 與 Discord 則提供通訊平台事故的視角，讓我們看到「通訊工具本身失效」時 trigger 與 communication 是怎麼一起被啟動的。&lt;/p>
&lt;p>Atlassian 的長尾復原、GCP 的全球控制面失效、Azure AD 的 identity cascading 也都能回扣到同一件事：severity 根據 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a>、擴散速率與協作成本來路由，直覺標註的準確度不足以支撐後續流程。這樣的分級，才會讓後續的止血、通訊與復盤有一致的起點。&lt;/p>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>04.6 SLI/SLO：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 對應 severity 門檻&lt;/li>
&lt;li>08.14 multi-incident：跨事故優先序判準&lt;/li>
&lt;li>08.17 security vs operational：分流影響 severity 計算&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 與 trigger 是把事故從「有問題」變成「需要開始協作」的門檻。incident severity 定義的是這次事故應該用多大規模的協作來處理，trigger 定義的是什麼訊號足以啟動這個協作。當兩者被分開寫清楚，團隊就不會把所有異常都當成同一種事件，也不會在影響面已經擴大後才開始反應。</p>
<p>這個節點先處理啟動，再處理升級。先定義什麼情況要 page、要不要拉 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a>、要不要進 status update，然後才處理 severity 分級的細節。這樣讀，會比先背 severity level 再找案例更接近真實事故運作。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> criteria</li>
<li>user impact signals</li>
<li>trigger thresholds</li>
<li><a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> handoff</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故啟動延遲於擴散、影響面已擴大才升級</li>
<li>severity 分級靠 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 直覺、無 user impact 量化</li>
<li>升級條件不清、跨團隊重複 page 同事故</li>
<li>同類事件不同 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 給不同 severity</li>
<li>啟動門檻過高（漏判）或過低（噪音）、無校準流程</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 的責任是把影響面說清楚。當服務開始退化時，先看使用者是否真的受影響，再看影響是否跨產品、跨 region、跨 tenant，最後才決定 severity。這個順序很重要，因為它決定了團隊是先止血還是先爭論標籤。</p>
<p>啟動條件的責任是把協作拉起來。當 trigger 被觸發時，團隊應該立刻知道誰要接手、誰要記錄、誰要對外通訊，以及下一次檢視的時間點。這種節奏不需要等事故結束才討論，因為事故本身就是路由。</p>
<h2 id="案例對照">案例對照</h2>
<p>AWS S3 適合用來看控制面事故如何把區域級影響迅速擴大，因為這類事件最容易讓 severity 上升到需要更大範圍協作。GitHub 適合用來看 replication 與 split-brain 的分級，因為資料一致性問題會直接拉長復原時間。Slack 與 Discord 則提供通訊平台事故的視角，讓我們看到「通訊工具本身失效」時 trigger 與 communication 是怎麼一起被啟動的。</p>
<p>Atlassian 的長尾復原、GCP 的全球控制面失效、Azure AD 的 identity cascading 也都能回扣到同一件事：severity 根據 <a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a>、擴散速率與協作成本來路由，直覺標註的準確度不足以支撐後續流程。這樣的分級，才會讓後續的止血、通訊與復盤有一致的起點。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.6 SLI/SLO：<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 對應 severity 門檻</li>
<li>08.14 multi-incident：跨事故優先序判準</li>
<li>08.17 security vs operational：分流影響 severity 計算</li>
</ul>
]]></content:encoded></item><item><title>2.1 高併發下的 Redis 讀寫邊界</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/</guid><description>&lt;p>Redis 在後端服務裡常扮演 cache、session、counter、dedup、presence 或輕量協調層。它通常比 SQL 更適合高併發短操作，但前提是 client、連線池、pipeline 與 key 設計都受控。高併發下的 Redis 仍然會遇到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a>、快取穿透、stampede、過大 pipeline 與不當鎖設計。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解為什麼 Redis client 應該共用&lt;/li>
&lt;li>分辨單鍵操作、pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與 Lua 的邊界&lt;/li>
&lt;li>了解高併發下的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede&lt;/a> 與 hot key 問題&lt;/li>
&lt;li>用 &lt;code>context&lt;/code> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 保護 Redis 呼叫&lt;/li>
&lt;li>把 Redis 用在適合的資料角色，並保留正式狀態來源&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察redis-呼叫大多是短網路-io">【觀察】Redis 呼叫大多是短網路 I/O&lt;/h2>
&lt;p>應用端對 Redis 的操作通常是短小但頻繁的網路請求。這代表真正影響效能的往往是 RTT、連線重用、批次送出與 key 設計。&lt;/p>
&lt;p>所以高併發時，重點是控制 Redis 邊界：&lt;/p>
&lt;ul>
&lt;li>用同一個 client 共用連線池&lt;/li>
&lt;li>對獨立操作使用合理的 pipeline&lt;/li>
&lt;li>熱門資料要避免集中到單一 key&lt;/li>
&lt;/ul>
&lt;h2 id="判讀client-共用比每次建立更重要">【判讀】client 共用比每次建立更重要&lt;/h2>
&lt;p>Redis client 的核心設計通常就是讓應用共用同一個實例。每個 request 都 new client，會把連線管理成本、握手成本與資源回收問題全部放大。&lt;/p>
&lt;p>高併發服務通常會採用：&lt;/p>
&lt;ul>
&lt;li>process 啟動時建立一個 Redis client&lt;/li>
&lt;li>request handler、worker、service layer 共用它&lt;/li>
&lt;li>所有操作都帶 &lt;code>context&lt;/code>&lt;/li>
&lt;li>timeout 與取消由上層傳入&lt;/li>
&lt;/ul>
&lt;h2 id="策略pipeline-用來節省-rtt">【策略】pipeline 用來節省 RTT&lt;/h2>
&lt;p>pipeline 的價值是把多個獨立命令一次送出，減少往返次數。它很適合：&lt;/p>
&lt;ul>
&lt;li>多個彼此獨立的讀取&lt;/li>
&lt;li>批次寫入&lt;/li>
&lt;li>一次更新多個 cache key&lt;/li>
&lt;/ul>
&lt;p>pipeline 的核心限制是批次大小仍要受控。太大的 pipeline 會帶來：&lt;/p>
&lt;ul>
&lt;li>內存壓力&lt;/li>
&lt;li>回應延遲變大&lt;/li>
&lt;li>單次失敗影響更多操作&lt;/li>
&lt;/ul>
&lt;h2 id="判讀原子性需求要分清楚">【判讀】原子性需求要分清楚&lt;/h2>
&lt;p>Redis 的很多操作本身就可以很快，但原子性與一致性需要額外設計。當需求需要多個資料變更形成同一個結果時，才應該考慮：&lt;/p>
&lt;ul>
&lt;li>單鍵原子操作&lt;/li>
&lt;li>transaction&lt;/li>
&lt;li>Lua script&lt;/li>
&lt;li>由上層做去重或補償&lt;/li>
&lt;/ul>
&lt;p>transaction 應服務明確的一致性需求，cache 寫入也應維持輔助狀態定位。Redis 很常是輔助狀態，真正的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 通常還是在 SQL 或 domain store。&lt;/p>
&lt;h2 id="策略cache-stampede-與-hot-key-要先處理">【策略】cache stampede 與 hot key 要先處理&lt;/h2>
&lt;p>高併發快取最常見的兩個問題，是大量 goroutine 同時 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a> 同一筆資料，以及大量流量打到同一個 key。&lt;/p>
&lt;h3 id="cache-stampede">cache stampede&lt;/h3>
&lt;p>當 cache miss 發生時，如果每個 request 都直接回源查 DB，會把後端放大成更大的壓力。常見的處理方式包括：&lt;/p>
&lt;ul>
&lt;li>設定合理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a>&lt;/li>
&lt;li>加 single-flight 類型的去重&lt;/li>
&lt;li>讓部分請求等待同一批重建結果&lt;/li>
&lt;li>對重建失敗設退避或短暫保護&lt;/li>
&lt;/ul>
&lt;h3 id="hot-key">hot key&lt;/h3>
&lt;p>如果某些 key 過度熱門，壓力會集中到 Redis 甚至單一 shard。處理方式通常是：&lt;/p>
&lt;ul>
&lt;li>拆 key 或拆資料粒度&lt;/li>
&lt;li>讓讀取走多層 cache&lt;/li>
&lt;li>降低單點依賴&lt;/li>
&lt;li>在應用端做短暫本地快取或節流&lt;/li>
&lt;/ul>
&lt;h2 id="cache-在規模化服務的角色光譜主寫於-_index">Cache 在規模化服務的角色光譜（主寫於 _index）&lt;/h2>
&lt;p>Cache 在規模化服務的角色從「DB 補救」逐步轉變到「主要服務面」再到「資料平面」、是橫跨整個 02 模組的入門 frame。完整光譜跟判讀條件主寫於 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組入口的「規模化下 cache 的角色光譜」段&lt;/a>；本章從 &lt;em>高併發讀寫&lt;/em> 角度補充：當 cache 已落在「主要服務面」或「資料平面」角色、cache lookup 是 critical path、容量規劃跟 stampede 防護要按本章「Cache 容量規劃跟 DB 不一樣」段執行。&lt;/p></description><content:encoded><![CDATA[<p>Redis 在後端服務裡常扮演 cache、session、counter、dedup、presence 或輕量協調層。它通常比 SQL 更適合高併發短操作，但前提是 client、連線池、pipeline 與 key 設計都受控。高併發下的 Redis 仍然會遇到 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、快取穿透、stampede、過大 pipeline 與不當鎖設計。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解為什麼 Redis client 應該共用</li>
<li>分辨單鍵操作、pipeline、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與 Lua 的邊界</li>
<li>了解高併發下的 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a> 與 hot key 問題</li>
<li>用 <code>context</code> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 保護 Redis 呼叫</li>
<li>把 Redis 用在適合的資料角色，並保留正式狀態來源</li>
</ol>
<hr>
<h2 id="觀察redis-呼叫大多是短網路-io">【觀察】Redis 呼叫大多是短網路 I/O</h2>
<p>應用端對 Redis 的操作通常是短小但頻繁的網路請求。這代表真正影響效能的往往是 RTT、連線重用、批次送出與 key 設計。</p>
<p>所以高併發時，重點是控制 Redis 邊界：</p>
<ul>
<li>用同一個 client 共用連線池</li>
<li>對獨立操作使用合理的 pipeline</li>
<li>熱門資料要避免集中到單一 key</li>
</ul>
<h2 id="判讀client-共用比每次建立更重要">【判讀】client 共用比每次建立更重要</h2>
<p>Redis client 的核心設計通常就是讓應用共用同一個實例。每個 request 都 new client，會把連線管理成本、握手成本與資源回收問題全部放大。</p>
<p>高併發服務通常會採用：</p>
<ul>
<li>process 啟動時建立一個 Redis client</li>
<li>request handler、worker、service layer 共用它</li>
<li>所有操作都帶 <code>context</code></li>
<li>timeout 與取消由上層傳入</li>
</ul>
<h2 id="策略pipeline-用來節省-rtt">【策略】pipeline 用來節省 RTT</h2>
<p>pipeline 的價值是把多個獨立命令一次送出，減少往返次數。它很適合：</p>
<ul>
<li>多個彼此獨立的讀取</li>
<li>批次寫入</li>
<li>一次更新多個 cache key</li>
</ul>
<p>pipeline 的核心限制是批次大小仍要受控。太大的 pipeline 會帶來：</p>
<ul>
<li>內存壓力</li>
<li>回應延遲變大</li>
<li>單次失敗影響更多操作</li>
</ul>
<h2 id="判讀原子性需求要分清楚">【判讀】原子性需求要分清楚</h2>
<p>Redis 的很多操作本身就可以很快，但原子性與一致性需要額外設計。當需求需要多個資料變更形成同一個結果時，才應該考慮：</p>
<ul>
<li>單鍵原子操作</li>
<li>transaction</li>
<li>Lua script</li>
<li>由上層做去重或補償</li>
</ul>
<p>transaction 應服務明確的一致性需求，cache 寫入也應維持輔助狀態定位。Redis 很常是輔助狀態，真正的 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 通常還是在 SQL 或 domain store。</p>
<h2 id="策略cache-stampede-與-hot-key-要先處理">【策略】cache stampede 與 hot key 要先處理</h2>
<p>高併發快取最常見的兩個問題，是大量 goroutine 同時 <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a> 同一筆資料，以及大量流量打到同一個 key。</p>
<h3 id="cache-stampede">cache stampede</h3>
<p>當 cache miss 發生時，如果每個 request 都直接回源查 DB，會把後端放大成更大的壓力。常見的處理方式包括：</p>
<ul>
<li>設定合理 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a></li>
<li>加 single-flight 類型的去重</li>
<li>讓部分請求等待同一批重建結果</li>
<li>對重建失敗設退避或短暫保護</li>
</ul>
<h3 id="hot-key">hot key</h3>
<p>如果某些 key 過度熱門，壓力會集中到 Redis 甚至單一 shard。處理方式通常是：</p>
<ul>
<li>拆 key 或拆資料粒度</li>
<li>讓讀取走多層 cache</li>
<li>降低單點依賴</li>
<li>在應用端做短暫本地快取或節流</li>
</ul>
<h2 id="cache-在規模化服務的角色光譜主寫於-_index">Cache 在規模化服務的角色光譜（主寫於 _index）</h2>
<p>Cache 在規模化服務的角色從「DB 補救」逐步轉變到「主要服務面」再到「資料平面」、是橫跨整個 02 模組的入門 frame。完整光譜跟判讀條件主寫於 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組入口的「規模化下 cache 的角色光譜」段</a>；本章從 <em>高併發讀寫</em> 角度補充：當 cache 已落在「主要服務面」或「資料平面」角色、cache lookup 是 critical path、容量規劃跟 stampede 防護要按本章「Cache 容量規劃跟 DB 不一樣」段執行。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> — 4700 萬 MAU 配對引擎、每次滑動查多個 cache（用戶 profile、距離、偏好過濾、推薦池）、cache lookup 屬 critical path。詳細 cache vs persistent store 取捨見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>。</p>
<h2 id="cache-容量規劃跟-db-不一樣">Cache 容量規劃跟 DB 不一樣</h2>
<p>容量規劃基準在 cache 跟 DB 有本質差異：DB 容量受 <em>total dataset size</em> 影響（要存所有資料）；cache 容量受 <em>working set size</em> 影響（只存熱資料）。兩者的擴容邏輯、成本曲線、評估指標都不同、不能套用相同規劃模板。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> — 47M MAU sustained growth、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo</a> — 當熱資料超過 DRAM 經濟範圍、單層 cache 同時遇到成本跟命中率瓶頸、要分層（DRAM + flash、詳見 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 ttl-eviction 分層快取段</a>）。</p>
<p><strong>Cache 容量規劃的三個維度</strong>：</p>
<ul>
<li><strong>Working set size</strong>：熱資料大小決定 cache 需要多少 RAM。監控指標是 <em>hot key 分布</em> 跟 <em>resident set growth</em>。working set 估算方式因 workload 不同、要靠實測得出。</li>
<li><strong>命中率目標</strong>：命中率目標決定 cache 大小的成長曲線。90% / 95% / 99% 對應不同 cache 大小、每加一個 9 需要的 cache size 通常顯著增加（具體倍數依 access pattern 分布、Zipfian 分布越平倍數越高）。</li>
<li><strong>回源 budget</strong>：cache miss 後 origin（DB / 重算）能承受多少 QPS、決定 cache 命中率下限。命中率掉幾個 percentage point 可能讓 origin QPS 翻數倍、容量規劃要按命中率敏感度反推 origin headroom。</li>
</ul>
<p><strong>判讀重點</strong>：cache 命中率變化是 <em>業務變化訊號</em>、可能是新功能影響 access pattern（推薦演算法改、查詢條件擴大、tenant 結構變化）、應先看業務側、再考慮加 cache capacity。</p>
<h2 id="redis-規模化的單執行緒邊界">Redis 規模化的單執行緒邊界</h2>
<p>Redis command 執行至今仍 single-threaded、單實例 command 吞吐受 CPU 單核限制。6.0+ 起可開啟 I/O thread 提升 I/O 吞吐、但 command 執行仍序列化。規模化服務遇到這個邊界時、四個選項各自適合不同壓力：</p>
<p><strong>1. 拆 cluster（應用層分散 key）</strong>：Redis Cluster 自帶分片、適合 key 數量多、單 key 不熱的場景。每 shard 仍 single-threaded、但總吞吐線性擴展。典型壓力是「KV 種類多、每種 key 不算熱、整體流量大」、跟 Tinder 47M MAU 同類 — 用戶 profile 跨大量 key 分散、每個 key 流量不極端、cluster 切片足夠。</p>
<p><strong>2. Redis 6.0+ I/O thread</strong>：保留 Redis protocol、I/O 處理 multi-threaded、command 執行仍 single-threaded。提升 read-heavy 場景吞吐、實測倍數依 workload 跟 thread 數而定。適合「主要瓶頸在 I/O syscall 不在 command CPU」的場景、是低改動量的階段性升級、不換 broker。</p>
<p><strong>3. <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> / <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">Dragonfly</a>（multi-threaded fork）</strong>：command 執行也 multi-threaded。對應 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB</a> — Snap 採用 KeyDB 在 GCP 上替代原生 Redis、9.C35 判讀段提出「單實例 throughput 提升 5-10x」（屬案例 derived 推論、實測倍數依 workload）。適合「單 key 極熱、cluster 切不開、需要單實例多執行緒撐單 partition」的壓力。代價是 vendor lock-in、fork 治理走向不確定（KeyDB 公司被收購後策略未明）。</p>
<p><strong>4. Memcached（multi-threaded、功能少）</strong>：純 KV 不支援複雜資料結構（hash / sorted set / stream）、適合「資料形狀單純、要 multi-threaded」的 cache-only 場景。如果 application 不需要 Redis 的進階資料結構、Memcached 通常單實例吞吐更高、運維更簡單。</p>
<p><strong>規模化常用組合</strong>：ElastiCache for Redis 7.1 在 r7g.4xlarge 上的 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS 公布上限</a>（單節點百萬級 RPS、單 cluster 5 億 RPS）+ Cluster 模式 + 應用層 connection multiplexing。實際配置依工作量跟成本邊界決定、不是「規模化必然全配滿」。對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> 的設計方向。</p>
<p>判讀順序：先確認瓶頸是不是單實例 command 吞吐（CPU 單核滿載 vs 整體 RAM / network 是否還有 headroom）、再選方案。應用層 key 分布不均（hot key）跟 single-threaded 限制是兩個獨立議題、混在一起會誤選方案。</p>
<h2 id="執行把-redis-用在對的角色">【執行】把 Redis 用在對的角色</h2>
<p>Redis 在高併發場景常見角色有：</p>
<ul>
<li>cache</li>
<li>session store</li>
<li>counter / <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a></li>
<li>presence / online state</li>
<li>dedup / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> key</li>
<li>lightweight <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> / stream</li>
</ul>
<p>每一種角色都有不同容錯方式。counter、presence 和 cache 的失敗語意各自不同，因此需要依資料角色選擇處理策略。</p>
<h2 id="策略分散式-lock-要謹慎使用">【策略】分散式 lock 要謹慎使用</h2>
<p>Redis 常被拿來做 distributed lock，但這類機制要非常清楚 lease、過期、持有者與失效風險。高併發下最怕的是鎖住之後沒有安全釋放，或以為鎖保證了完整業務一致性。</p>
<p>原則上：</p>
<ul>
<li>鎖應該短</li>
<li>鎖持有者要可辨識</li>
<li>鎖過期要可接受</li>
<li>業務上若能不用分散式鎖，通常應優先考慮更簡單的設計</li>
</ul>
<h2 id="延伸語言端仍然要負責限流與取消">【延伸】語言端仍然要負責限流與取消</h2>
<p>Redis 很快，但應用端仍然要設計邊界。語言端應使用 timeout、cancellation、<a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>、rate limit 或 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 把壓力收斂起來；否則排隊等待 Redis 回應的工作會越堆越多。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>Redis 高併發邊界會受語言 runtime 影響。Thread-based runtime 要管理 client pool 與 blocking command；async runtime 要確認 Redis client 不會阻塞 event loop；輕量 task runtime 要限制同時呼叫 Redis 的工作數量。動態語言要特別控制 cache value schema 與序列化格式；強型別語言要避免把內部型別直接當成跨服務 cache <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>高併發 cache 場景重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a></td>
          <td>47M MAU 配對引擎、cache 是主要服務面、sustained growth 成本曲線</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a></td>
          <td>ML inference 之前 feature lookup、p99 &lt; 10ms 是業務 KPI</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB</a></td>
          <td>KeyDB multi-threaded fork、跨 cloud 部署</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>cache 成為資料層能力、社交圖查詢的快取治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>跨區分散式 cache、平台層基礎設施</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>client 散落邏輯收斂到路由層、跨叢集 cache 路由</td>
      </tr>
  </tbody>
</table>
<p>這六個案例可以分成兩群讀。<strong>規模化容量群</strong>（Tinder、Tubi、Snap）的共同訊號是「sustained growth 下 cache 變主要服務面、容量規劃跟單實例邊界要重新設計」、本章「Cache 容量規劃跟 DB 不一樣」跟「Redis 規模化的單執行緒邊界」段直接對應；<strong>跨區資料平面群</strong>（Meta TAO、Netflix EVCache、Meta mcrouter）的共同訊號是「cache 變成跨區資料層、需要路由治理跟一致性窗口」、詳細展開在 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的跨區一致性窗口</a> 跟 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape</a>。兩群讀法切入點不同、本章先處理前者的高併發 / 容量議題、後者跨章節讀。</p>
<h2 id="小結">小結</h2>
<p>高併發服務處理 Redis 的核心原則：client 共用、操作要短、pipeline 要有節制、熱點 key 要設計、cache miss 要防 stampede、鎖要保守使用。</p>
<p><strong>規模化補充</strong>：cache 角色變化（DB 補救 → 主要服務面 → 資料平面）主寫於 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">_index 規模化下 cache 的角色光譜</a>、本章在角色已落「主要服務面」或「資料平面」時提供高併發判讀。Redis 規模化的單執行緒邊界有四個選項（cluster / I/O thread / KeyDB 等 fork / Memcached）、判讀順序是先確認瓶頸再選方案。</p>
]]></content:encoded></item><item><title>模組一：資料庫與持久化</title><link>https://tarrragon.github.io/blog/backend/01-database/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/</guid><description>&lt;p>資料庫模組的核心目標是說明 application 狀態進入持久化層後，如何維持一致性、可演進性與可測性。語言教材會先定義 repository port、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol&lt;/a> 或 interface；本模組負責說明具體資料庫 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter&lt;/a> 如何實作這些邊界。閱讀本模組前，可先建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 的共同語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors&lt;/a> — T1 收錄 PostgreSQL / MySQL / SQLite / MongoDB / DynamoDB / CockroachDB / Aurora，每個服務頁提供定位、適用場景、取捨、容量判準、案例對照與下一步路由。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SQLite&lt;/td>
 &lt;td>embedded &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>、單機服務、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>、測試資料庫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PostgreSQL&lt;/td>
 &lt;td>schema design、index、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>versioned schema、rollback、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract&lt;/a> migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>unit of work、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary&lt;/a>、deadlock、retry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Repository &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">adapter&lt;/a>&lt;/td>
 &lt;td>SQL row mapping、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test、錯誤轉換&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>資料庫選型的核心判斷是資料是否承擔正式狀態與一致性。當資料需要長期保存、支援查詢、被多個流程共同讀寫，並且需要交易保護時，應先評估 relational database 或 document database。&lt;/p>
&lt;p>SQLite 適合單機服務、embedded app、測試資料庫與低操作成本場景；PostgreSQL 適合多使用者後端、複雜查詢、transaction、index 與長期 schema evolution。Migration 工具解決 schema 隨版本演進的問題；transaction boundary 解決多筆資料一起成功或失敗的問題；repository adapter 解決 application port 到具體 SQL 實作的轉換。&lt;/p>
&lt;p>接近真實網路服務的例子包括訂單系統、會員系統、訂閱方案、付款紀錄與權限資料。這些資料都需要明確 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，因此本模組會從資料模型、一致性、migration 與 repository adapter 邊界開始說明。&lt;/p>
&lt;h2 id="與語言教材的分工">與語言教材的分工&lt;/h2>
&lt;p>語言教材處理 repository interface / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol&lt;/a>、取消與逾時、error wrapping、memory fake 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test。Backend database 模組處理 SQL schema、migration tool、transaction isolation、connection pool 與資料庫錯誤語意。&lt;/p></description><content:encoded><![CDATA[<p>資料庫模組的核心目標是說明 application 狀態進入持久化層後，如何維持一致性、可演進性與可測性。語言教材會先定義 repository port、<a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a> 或 interface；本模組負責說明具體資料庫 <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter</a> 如何實作這些邊界。閱讀本模組前，可先建立 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 與 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 的共同語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors</a> — T1 收錄 PostgreSQL / MySQL / SQLite / MongoDB / DynamoDB / CockroachDB / Aurora，每個服務頁提供定位、適用場景、取捨、容量判準、案例對照與下一步路由。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite</td>
          <td>embedded <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、單機服務、<a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>、測試資料庫</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>schema design、index、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a></td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>versioned schema、rollback、<a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> migration</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>unit of work、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、deadlock、retry</td>
      </tr>
      <tr>
          <td>Repository <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">adapter</a></td>
          <td>SQL row mapping、<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test、錯誤轉換</td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>資料庫選型的核心判斷是資料是否承擔正式狀態與一致性。當資料需要長期保存、支援查詢、被多個流程共同讀寫，並且需要交易保護時，應先評估 relational database 或 document database。</p>
<p>SQLite 適合單機服務、embedded app、測試資料庫與低操作成本場景；PostgreSQL 適合多使用者後端、複雜查詢、transaction、index 與長期 schema evolution。Migration 工具解決 schema 隨版本演進的問題；transaction boundary 解決多筆資料一起成功或失敗的問題；repository adapter 解決 application port 到具體 SQL 實作的轉換。</p>
<p>接近真實網路服務的例子包括訂單系統、會員系統、訂閱方案、付款紀錄與權限資料。這些資料都需要明確 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，因此本模組會從資料模型、一致性、migration 與 repository adapter 邊界開始說明。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理 repository interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a>、取消與逾時、error wrapping、memory fake 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test。Backend database 模組處理 SQL schema、migration tool、transaction isolation、connection pool 與資料庫錯誤語意。</p>
<p>跨模組端到端串聯（DB → cache → event → observability）見 <a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a></td>
          <td>高併發下的 SQL 讀寫邊界</td>
          <td>共用 <code>sql.DB</code>、控制連線池、縮小 transaction 範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2</a></td>
          <td>schema design 與資料建模</td>
          <td>規劃 table、index、key 與命名規則</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a></td>
          <td>transaction 與一致性邊界</td>
          <td>判斷何時使用 transaction、retry 與 isolation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4</a></td>
          <td>repository adapter 實作</td>
          <td>把 SQL row mapping 與錯誤轉換封裝成 adapter</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5</a></td>
          <td>攻擊者視角（紅隊）：資料層弱點判讀</td>
          <td>用越權查詢、資料外洩路徑與恢復成本檢查資料層設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
          <td>資料庫轉換實作</td>
          <td>把雙寫、回填、切流與回滾做成可分段驗證流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7</a></td>
          <td>Schema Migration Rollout 證據實作示範</td>
          <td>以訂單付款狀態欄位演進示範 evidence、gate 與 decision log</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8</a></td>
          <td>State Ownership 與 Query Boundary</td>
          <td>分辨正式狀態、派生狀態與不同查詢責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9</a></td>
          <td>Reconciliation 與 Data Repair</td>
          <td>把資料錯誤轉成可驗證、可修復、可稽核流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10</a></td>
          <td>KV / Document DB 容量規劃</td>
          <td>partition key 設計、capacity mode、multi-model 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11</a></td>
          <td>全球分散式 OLTP</td>
          <td>Spanner / Aurora DSQL / Cosmos DB multi-region 跟 <a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12</a></td>
          <td>大規模 DB 遷移實戰</td>
          <td>dual-write / <a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> / cutover / <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13</a></td>
          <td>應用層查詢反模式與 Query 預算</td>
          <td>N+1、select *、缺索引、ORM lazy load、long transaction 與每請求 query 預算</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &#43; PR review 整合 &#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14</a></td>
          <td>Production Slow Log Closed Loop</td>
          <td>採集 / Normalize / PR review 整合 / Regression 偵測 — 把 slow log 從事故工具變成定期審視訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">Vendor 文章撰寫規格</a></td>
          <td>Vendor overview / deep article / migration playbook 分工</td>
          <td>把 PostgreSQL / MySQL batch 經驗整理成後續資料庫服務頁的撰寫規格</td>
      </tr>
  </tbody>
</table>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>資料庫章節下一輪的核心責任是把正式狀態的演進路徑講完整。現有章節已經涵蓋 schema、transaction、repository adapter 與 migration playbook，但還需要補上 state ownership、query boundary、migration safety 與 reconciliation 之間的引用關係，讓讀者知道資料庫變更如何從設計、發布、觀測一路接到事故決策。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>State ownership</td>
          <td>哪些資料是正式狀態，哪些只是 cache、index 或事件副本</td>
          <td><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>Query boundary</td>
          <td>交易查詢、列表查詢、報表查詢與對帳查詢是否混在一起</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Migration safety</td>
          <td>schema 變更是否能分批、驗證、暫停與回退</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Reconciliation</td>
          <td>資料錯誤發生後如何驗證、修復、對帳與留下證據</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
      <tr>
          <td>Data protection</td>
          <td>正式資料在查詢、匯出、修復與刪除時如何保留責任邊界</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4</a>、<a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要寫成資料庫自己的敘事，避免把 04/06/08 的欄位直接搬進來。資料庫關心的是狀態能否正確演進；觀測、驗證與事故流程接收這個演進結果作為下游證據。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>資料庫模組的 knowledge card 缺口集中在「變更如何被驗證」與「資料如何被修復」。已有 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a>、<a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill</a> 與 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a> 可作為第一批錨點。</p>
<p>下一批候選卡片包括 migration validation、read compatibility、<a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>、reconciliation、data repair runbook 與 fail-forward migration。這些卡片要先定義服務責任與使用時機，再讓 1.6 migration playbook 與後續實作文章引用。</p>
<h2 id="vendor-文章規格入口">Vendor 文章規格入口</h2>
<p>資料庫 vendor 文章的下一輪重點是把 PostgreSQL / MySQL batch 經驗變成可重複使用的撰寫規格。後續寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 前，先讀 <a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">資料庫 Vendor 文章撰寫規格</a>；該文分清 vendor overview、deep article 與 migration playbook 的責任，並列出 PG / MySQL 回收出的橫向調整項。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>資料庫的第一條實作路徑已完成： <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據實作示範</a>。這篇以訂單資料表付款狀態欄位演進為例，說明 migration plan、<a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、rollback condition 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用是 1.2 schema design、1.3 transaction boundary、1.6 migration playbook、<a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a> 與 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入 02 cache migration。</p>
<p>資料庫路徑的 artifact 對齊重點是「先證明資料演進正確，再討論是否放行」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並在 query 內容覆蓋 validation query、row count 差異與 replication lag；對 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 expand/contract 分段結果；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 pause / rollback / fail-forward 的判斷與依據。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/deployment-order-database/" data-link-title="部署順序與資料庫上 IaC" data-link-desc="核心服務的依賴圖決定部署順序，資料庫作為第一批上層服務需要最謹慎的 IaC 描述 — 涵蓋 RDS 接線、連線管理、read replica 與端點暴露">infra 模組五：資料庫上 IaC</a>：RDS 的 IaC 描述（subnet group、parameter group、連線管理、read replica）與部署順序</li>
<li>→ <a href="/blog/infra/05-core-services/stateful-protection-dependency/" data-link-title="Stateful 資源保護與跨服務依賴表達" data-link-desc="stateful 資源的保護策略（multi-AZ、備份、刪除保護）、stateful 與 stateless 的操作差異，以及用 output 與 data source 表達服務間依賴">infra 模組五：Stateful 資源保護</a>：multi-AZ、backup retention、deletion protection、PITR 的 IaC 設定</li>
</ul>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>資料庫使用方式會受語言的 connection pool、transaction scope、ORM 行為、錯誤處理與 migration 生態影響。同步 thread-based runtime 要控制 blocking query 與 pool 大小；async runtime 要確認 database client 是否真正非阻塞；輕量並發 runtime 要限制同時查詢數量，避免把大量 task 轉成資料庫連線壓力。強型別語言適合把 row mapping、schema 與錯誤分類型別化；動態語言則需要靠 migration、runtime validation、fixture 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test 保護資料邊界。</p>
]]></content:encoded></item><item><title>GitHub Actions：Environment Protection 與 OIDC Cloud Auth</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/environment-protection-and-oidc-cloud-auth/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/environment-protection-and-oidc-cloud-auth/</guid><description>&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CI pipeline 的可靠性驗證在測試階段結束後，還需要兩道控制面才算完整。第一道是 deploy approval gate — 決定誰可以核准 production deploy、在什麼條件下放行。第二道是 credential 安全 — deploy 需要 cloud credential，但 long-lived secret 存在 CI 環境中會擴大洩漏面。&lt;/p>
&lt;p>GitHub Actions 用 environment protection rules 處理第一道，用 OIDC federation 處理第二道。兩者搭配讓 deploy 流程同時滿足 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 的放行控制與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的 credential 最小暴露原則。&lt;/p>
&lt;h2 id="environment-protection-rules">Environment Protection Rules&lt;/h2>
&lt;p>Environment 是 GitHub Actions 的 deploy 分層單位。每個 environment（staging / canary / production）可以獨立設定 protection rules，讓不同風險等級的 deploy 走不同的放行流程。&lt;/p>
&lt;h3 id="protection-rule-類型">Protection rule 類型&lt;/h3>
&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>Required reviewers&lt;/td>
 &lt;td>指定人員核准後才能 deploy&lt;/td>
 &lt;td>production 需 2 人核准&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Wait timer&lt;/td>
 &lt;td>deploy 前強制等待，讓最後一刻能攔住&lt;/td>
 &lt;td>production 等 15 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deployment branch policy&lt;/td>
 &lt;td>只允許特定 branch deploy 到該 environment&lt;/td>
 &lt;td>production 只接受 main / release/*&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Required reviewers 是 deploy 層的 release gate。當 workflow job 標記 &lt;code>environment: production&lt;/code>，GitHub 會暫停 job 直到指定 reviewer 核准。reviewer 的選擇應對齊服務 ownership — 由該服務的 on-call lead 或 tech lead 核准，避免核准權過於集中或分散。&lt;/p>
&lt;p>Wait timer 提供一個緩衝窗口。deploy 前等待 N 分鐘讓團隊有時間檢查 staging 結果、確認沒有進行中的事故、或在發現問題時取消 deploy。timer 長度跟服務風險等級對齊 — 低風險服務可以 0 分鐘，交易路徑可以 15-30 分鐘。&lt;/p>
&lt;p>Deployment branch policy 限制哪些 branch 可以觸發特定 environment 的 deploy。這防止 feature branch 意外 deploy 到 production。production 通常只接受 main 或 release branch。&lt;/p>
&lt;h3 id="分層建議">分層建議&lt;/h3>
&lt;p>staging 用自動 deploy — push 到 staging branch 直接觸發 workflow，無需 approval，回饋速度最大化。production 用 required reviewer + wait timer — 確保每次 production deploy 都經過人工確認與緩衝。canary 介於兩者之間 — 可以自動 deploy 但加 wait timer，讓觀測指標有時間反映。&lt;/p>
&lt;h2 id="oidc-cloud-auth">OIDC Cloud Auth&lt;/h2>
&lt;h3 id="long-lived-credential-的風險">Long-lived credential 的風險&lt;/h3>
&lt;p>CI deploy 需要 cloud credential（AWS access key / GCP service account key / Azure service principal）。傳統做法是把這些 credential 存在 GitHub repository secret 或 environment secret 中。long-lived credential 的風險在於：洩漏後攻擊者可以長期使用、rotation 需要手動更新 CI 設定、credential scope 常設得比實際需求更大。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題情境">問題情境</h2>
<p>CI pipeline 的可靠性驗證在測試階段結束後，還需要兩道控制面才算完整。第一道是 deploy approval gate — 決定誰可以核准 production deploy、在什麼條件下放行。第二道是 credential 安全 — deploy 需要 cloud credential，但 long-lived secret 存在 CI 環境中會擴大洩漏面。</p>
<p>GitHub Actions 用 environment protection rules 處理第一道，用 OIDC federation 處理第二道。兩者搭配讓 deploy 流程同時滿足 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的放行控制與 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的 credential 最小暴露原則。</p>
<h2 id="environment-protection-rules">Environment Protection Rules</h2>
<p>Environment 是 GitHub Actions 的 deploy 分層單位。每個 environment（staging / canary / production）可以獨立設定 protection rules，讓不同風險等級的 deploy 走不同的放行流程。</p>
<h3 id="protection-rule-類型">Protection rule 類型</h3>
<table>
  <thead>
      <tr>
          <th>規則</th>
          <th>責任</th>
          <th>典型設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Required reviewers</td>
          <td>指定人員核准後才能 deploy</td>
          <td>production 需 2 人核准</td>
      </tr>
      <tr>
          <td>Wait timer</td>
          <td>deploy 前強制等待，讓最後一刻能攔住</td>
          <td>production 等 15 分鐘</td>
      </tr>
      <tr>
          <td>Deployment branch policy</td>
          <td>只允許特定 branch deploy 到該 environment</td>
          <td>production 只接受 main / release/*</td>
      </tr>
  </tbody>
</table>
<p>Required reviewers 是 deploy 層的 release gate。當 workflow job 標記 <code>environment: production</code>，GitHub 會暫停 job 直到指定 reviewer 核准。reviewer 的選擇應對齊服務 ownership — 由該服務的 on-call lead 或 tech lead 核准，避免核准權過於集中或分散。</p>
<p>Wait timer 提供一個緩衝窗口。deploy 前等待 N 分鐘讓團隊有時間檢查 staging 結果、確認沒有進行中的事故、或在發現問題時取消 deploy。timer 長度跟服務風險等級對齊 — 低風險服務可以 0 分鐘，交易路徑可以 15-30 分鐘。</p>
<p>Deployment branch policy 限制哪些 branch 可以觸發特定 environment 的 deploy。這防止 feature branch 意外 deploy 到 production。production 通常只接受 main 或 release branch。</p>
<h3 id="分層建議">分層建議</h3>
<p>staging 用自動 deploy — push 到 staging branch 直接觸發 workflow，無需 approval，回饋速度最大化。production 用 required reviewer + wait timer — 確保每次 production deploy 都經過人工確認與緩衝。canary 介於兩者之間 — 可以自動 deploy 但加 wait timer，讓觀測指標有時間反映。</p>
<h2 id="oidc-cloud-auth">OIDC Cloud Auth</h2>
<h3 id="long-lived-credential-的風險">Long-lived credential 的風險</h3>
<p>CI deploy 需要 cloud credential（AWS access key / GCP service account key / Azure service principal）。傳統做法是把這些 credential 存在 GitHub repository secret 或 environment secret 中。long-lived credential 的風險在於：洩漏後攻擊者可以長期使用、rotation 需要手動更新 CI 設定、credential scope 常設得比實際需求更大。</p>
<h3 id="oidc-federation-的運作方式">OIDC federation 的運作方式</h3>
<p>GitHub Actions 支援作為 OIDC identity provider。workflow 在執行時可以向 GitHub 請求一個 short-lived OIDC token，cloud provider 信任這個 token 後發出 short-lived cloud credential。整個流程不需要在 CI 環境中存放任何 long-lived secret。</p>
<p>流程：workflow 啟動 → 向 GitHub OIDC provider 請求 token → token 帶有 repo / branch / environment 等 claim → cloud provider 的 trust policy 驗證 claim → 發出 short-lived credential（通常 1 小時有效期）。</p>
<h3 id="cloud-provider-配置">Cloud provider 配置</h3>
<p><strong>AWS</strong>：在 IAM 設定 OIDC identity provider（issuer: <code>token.actions.githubusercontent.com</code>）、建立 IAM role 並設定 trust policy 限制 repo + branch + environment。workflow 中用 <code>aws-actions/configure-aws-credentials</code> action 取得 session credential。</p>
<p><strong>GCP</strong>：設定 Workload Identity Federation pool + provider、建立 service account 並綁定 pool。workflow 中用 <code>google-github-actions/auth</code> action 取得 short-lived token。</p>
<p><strong>Azure</strong>：在 Azure AD 設定 federated credential 給 app registration、限制 repo + branch + environment。workflow 中用 <code>azure/login</code> action。</p>
<h3 id="trust-policy-的安全邊界">Trust policy 的安全邊界</h3>
<p>OIDC trust policy 必須限制到特定 repo、branch 與 environment。trust policy 寫成 wildcard（信任整個 GitHub org 的所有 repo）等於讓 org 內任何 repo 的 workflow 都能取得 cloud credential。最小權限原則：production environment 的 trust policy 只信任 <code>repo:org/service:environment:production</code>，不信任其他 environment 或 branch。</p>
<h2 id="實作範例">實作範例</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># .github/workflows/deploy.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">deploy-staging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/staging-deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">deploy-production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">deploy-staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/production-deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh production</span></span></span></code></pre></div><p>staging job 自動觸發。production job 等 staging 完成後暫停，等待 environment protection rules 中設定的 reviewer 核准。兩個 job 各自用不同的 IAM role，scope 分離。</p>
<p>Environment secret 與 repository secret 的差異：environment secret 只在該 environment 的 job 中可用。把 production-only 的設定（如 database connection string）存在 production environment secret 而非 repository secret，避免 staging workflow 意外存取 production 資源。</p>
<h2 id="邊界與陷阱">邊界與陷阱</h2>
<p>Environment protection rules 在 private repo 上需要 GitHub Team 或 Enterprise 方案。Free 方案的 private repo 無法使用 required reviewers 與 wait timer，只有 public repo 或付費方案可用。</p>
<p>OIDC trust policy 的常見錯誤是 subject claim 設定太寬。<code>sub</code> claim 的格式是 <code>repo:{owner}/{repo}:environment:{name}</code>（使用 environment 時）或 <code>repo:{owner}/{repo}:ref:refs/heads/{branch}</code>（不使用 environment 時）。用 wildcard match 或省略 environment 限制會讓非預期的 workflow 取得 credential。</p>
<p>Wait timer 設定要跟服務風險等級對齊。所有服務統一用 30 分鐘 wait timer 會拖慢低風險服務的 deploy velocity。對齊方式：低風險服務 0 分鐘、中風險 5-10 分鐘、高風險（交易路徑）15-30 分鐘。</p>
<p>Required reviewer 數量跟團隊大小對齊。只有 1 個 reviewer 等於沒有四眼原則；需要 5 個 reviewer 會造成 approval 排隊。2-3 個 reviewer 是多數團隊的平衡點。</p>
<h2 id="整合路由">整合路由</h2>
<ul>
<li>上游：<a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>（CI gate 通過後才進入 deploy 階段）</li>
<li>下游：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（environment protection 是 deploy 層的 release gate）</li>
<li>下游：<a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>（deploy 結果作為 release evidence）</li>
<li>平行：<a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a> contexts + approval jobs（同類功能的不同實作）</li>
<li>案例回寫：<a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft 變更分層</a>（變更風險分層對應 environment 分層）、<a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google Error Budget</a>（error budget 消耗時提高 gate 門檻 → 可動態調整 required reviewer 數量）</li>
</ul>
]]></content:encoded></item><item><title>10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀&lt;/a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &amp;#43; 逐步遷移 &amp;#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern&lt;/a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。&lt;/p>
&lt;h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義&lt;/h2>
&lt;p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>舊系統繼續運作&lt;/strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出&lt;/li>
&lt;li>&lt;strong>流量逐步遷移&lt;/strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊&lt;/li>
&lt;li>&lt;strong>驗證 → 擴大&lt;/strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例&lt;/li>
&lt;li>&lt;strong>舊系統最終下架&lt;/strong>：當所有功能都遷出後、monolith 才被退役&lt;/li>
&lt;/ul>
&lt;p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。&lt;/p>
&lt;h2 id="拆分執行階段">拆分執行階段&lt;/h2>
&lt;p>把 Strangler 細化成可操作的四階段：&lt;/p>
&lt;h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出&lt;/h3>
&lt;p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。&lt;/p>
&lt;p>這層動作的責任：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強制 dependency 清楚&lt;/strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase&lt;/li>
&lt;li>&lt;strong>資料邊界明示&lt;/strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access&lt;/li>
&lt;li>&lt;strong>變更頻率冷凍&lt;/strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」&lt;/li>
&lt;/ul>
&lt;p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。&lt;/p>
&lt;h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期&lt;/h3>
&lt;p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。&lt;/p>
&lt;p>雙寫期的設計關鍵：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>寫入順序&lt;/strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務&lt;/li>
&lt;li>&lt;strong>跨服務一致性&lt;/strong>：兩邊寫入用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga&lt;/a> 保證最終一致、不能容忍長期不一致&lt;/li>
&lt;li>&lt;strong>資料對賬機制&lt;/strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率&lt;/li>
&lt;li>&lt;strong>雙寫期長度&lt;/strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3&lt;/li>
&lt;/ul>
&lt;p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。&lt;/p>
&lt;h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）&lt;/h3>
&lt;p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>按 user / tenant ID hash 分流&lt;/strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）&lt;/li>
&lt;li>&lt;strong>按 endpoint 分流&lt;/strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景&lt;/li>
&lt;li>&lt;strong>Dark launch&lt;/strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case&lt;/li>
&lt;/ul>
&lt;p>切流期間的觀測重點：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。<a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern</a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。</p>
<h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義</h2>
<p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：</p>
<ul>
<li><strong>舊系統繼續運作</strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出</li>
<li><strong>流量逐步遷移</strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊</li>
<li><strong>驗證 → 擴大</strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例</li>
<li><strong>舊系統最終下架</strong>：當所有功能都遷出後、monolith 才被退役</li>
</ul>
<p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。</p>
<h2 id="拆分執行階段">拆分執行階段</h2>
<p>把 Strangler 細化成可操作的四階段：</p>
<h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出</h3>
<p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。</p>
<p>這層動作的責任：</p>
<ul>
<li><strong>強制 dependency 清楚</strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase</li>
<li><strong>資料邊界明示</strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access</li>
<li><strong>變更頻率冷凍</strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」</li>
</ul>
<p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。</p>
<h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期</h3>
<p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。</p>
<p>雙寫期的設計關鍵：</p>
<ul>
<li><strong>寫入順序</strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務</li>
<li><strong>跨服務一致性</strong>：兩邊寫入用 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 或 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a> 保證最終一致、不能容忍長期不一致</li>
<li><strong>資料對賬機制</strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率</li>
<li><strong>雙寫期長度</strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3</li>
</ul>
<p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。</p>
<h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）</h3>
<p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：</p>
<ul>
<li><strong>按 user / tenant ID hash 分流</strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）</li>
<li><strong>按 endpoint 分流</strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景</li>
<li><strong>Dark launch</strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case</li>
</ul>
<p>切流期間的觀測重點：</p>
<ul>
<li><strong>錯誤率對比</strong>：新服務 vs monolith 同 endpoint 的 5xx / 4xx 比例</li>
<li><strong>延遲分布對比</strong>：P50 / P95 / P99 latency</li>
<li><strong>業務指標對比</strong>：轉換率、跳出率、訂單成功率 — 確認沒有「技術指標看起來正常、業務指標掉」的隱形 regression</li>
</ul>
<p>任一指標惡化、切回 monolith、不繼續推進。</p>
<h3 id="階段-4寫路徑遷移--monolith-退役">階段 4：寫路徑遷移 + Monolith 退役</h3>
<p>讀路徑 100% 切完、且穩定觀察一段時間後（建議至少 2 週）、寫路徑才從「雙寫」變成「只寫新服務」。</p>
<p>寫路徑切換的步驟：</p>
<ol>
<li><strong>雙寫變成「新服務 + 異步 backfill 到 monolith」</strong>：以新服務為主、monolith 變成 standby</li>
<li><strong>觀察期 1-2 週</strong>：確認新服務寫入路徑穩定、無資料遺失或不一致</li>
<li><strong>停止 backfill</strong>：monolith 不再被寫入、變成 read-only</li>
<li><strong>Monolith 該功能下架</strong>：等確認所有 dependency 都已遷移後（通常還要再 1-4 週觀察）、刪掉 monolith 對應 code 跟 table</li>
</ol>
<p>階段 4 是 point of no return — 過了寫路徑切換、回 monolith 的成本變得很高（要把新服務累積的寫入 backfill 回去）。這個 checkpoint 必須有明確的 go/no-go 決策、不是「順勢推進」。</p>
<h2 id="回退路徑設計">回退路徑設計</h2>
<p>回退條件必須在拆分啟動前就定義、不是事故時臨時決策。常見回退路徑：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>失敗訊號</th>
          <th>回退動作</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Adapter 抽出後 monolith 變慢 / 出錯</td>
          <td>revert PR、重新規劃 adapter 邊界</td>
          <td>低</td>
      </tr>
      <tr>
          <td>2</td>
          <td>雙寫期差異率 &gt; 1% 持續</td>
          <td>停雙寫、回 monolith 單寫、修資料模型對應</td>
          <td>中</td>
      </tr>
      <tr>
          <td>3</td>
          <td>切流期間錯誤率 / 延遲 / 業務指標惡化</td>
          <td>切流比例調回 0%、回 monolith 單讀、雙寫繼續</td>
          <td>中</td>
      </tr>
      <tr>
          <td>4</td>
          <td>寫路徑切換後 1 週內出資料遺失</td>
          <td>觸發 backfill from 新服務 → monolith、切回雙寫期</td>
          <td>高</td>
      </tr>
      <tr>
          <td>4+</td>
          <td>Monolith 已下架、新服務出事</td>
          <td>災難級別、需要從備份重建 + 大規模事件公告</td>
          <td>極高</td>
      </tr>
  </tbody>
</table>
<p>階段 4 之後的回退代價是指數成長的。設計時要把 monolith 下架時點延後到「確信不需要回退」、寧可多保留 monolith 1-2 個月。</p>
<h2 id="拆分執行的判讀訊號">拆分執行的判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Adapter 抽出時發現難以封裝（dependency 散落各處）</td>
          <td>邊界其實沒形成、拆分判斷錯了</td>
          <td>回 10.1 重新評估、考慮先重構 monolith 再拆</td>
      </tr>
      <tr>
          <td>雙寫期差異率不收斂</td>
          <td>資料模型對應有 gap、或業務邏輯有 monolith 隱式依賴</td>
          <td>暫停拆分、做 data audit、找出隱式依賴點</td>
      </tr>
      <tr>
          <td>切流比例增加後業務指標掉</td>
          <td>技術等價但業務行為不等價（例如 latency 微升影響轉換）</td>
          <td>切回 monolith、檢查 latency / 業務指標關聯</td>
      </tr>
      <tr>
          <td>階段 4 出現「monolith 還有人在用」</td>
          <td>dependency 沒清乾淨、有隱藏的呼叫者</td>
          <td>延後 monolith 下架、用 access log audit 找出殘留呼叫者</td>
      </tr>
      <tr>
          <td>拆分過程中 dev velocity 大幅下降</td>
          <td>拆分成本超過短期收益、可能拆錯時機</td>
          <td>評估暫停拆分、回到 modular monolith</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把拆分當成「直接把功能搬出去」、跳過階段 1 adapter 抽出。沒有 adapter 抽出、新服務跟 monolith 的 dependency 邊界不清楚、雙寫期會出現難以排查的隱式依賴問題。</p>
<p>把雙寫期當成「過渡而已、隨便寫」。雙寫期是拆分的 source of truth verification 階段、差異率沒收斂前不能進切流。隨便寫的結果是切流後出資料一致性事故。</p>
<p>把「monolith 下架」當成拆分成功訊號。Monolith 下架太早是常見事故來源 — 即使流量 100% 切完、可能仍有 batch job / report / 內部 tool 在用 monolith。下架前先用 access log audit 確認真實流量為 0。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「Strangler Fig 漸進拆分的執行流程」。當問題進入「該不該拆」的判讀、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>；進入跨服務通訊設計（同步 vs 異步、event-driven）、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a>；進入部署層的切流機制（feature flag、canary、blue/green）、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a>；進入資料庫遷移層的具體技術（dual write、shadow read、cutover），進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 / 05 案例庫中、Strangler 拆分案例不算多（多數案例是已拆完的狀態描述、而非拆分過程紀錄）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — Netflix 的故事是「拆完合回去」、隱含 strangler 反向。對照本章可問：合併過程是否也走了類似四階段、只是方向相反（雙寫期把多 DB 合到 Aurora、再切讀路徑、最後下架原 DB）？</li>
<li><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：EKS 平台整併</a> — 平台層整併。本章在「服務層」、整併在「平台層」、邏輯類似但 surface 不同。</li>
</ul>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分判讀</a> 的交接：10.1 給「該拆」的判讀、本章給「怎麼拆」的執行。</li>
<li>與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue + outbox</a> 的交接：雙寫期跟拆分後跨服務通訊都依賴 outbox / saga 保證一致性。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a> 的交接：階段 3 切流的技術機制（feature flag、canary）跟部署層的 rollout 同源。</li>
<li>與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> 的交接：階段 2 雙寫期跟資料庫遷移的雙寫期是同一套機制、只是 surface 不同。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看拆分判讀（該不該拆）、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>。要看拆分後跨服務通訊設計、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a>。要看部署層的切流技術細節、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout</a>。</p>
]]></content:encoded></item><item><title>Auth0</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/</guid><description>&lt;p>Auth0 是 Customer Identity Cloud 的代表選項。它承擔三段責任：B2C / B2B app 的&lt;em>使用者登入流程&lt;/em>託管、社交與企業 connection 的 token broker、user profile 與 metadata 的 store。當產品把登入交給 Auth0、信任邊界從「我的 app 自管密碼表」變成「tenant 配置 + Action hook 程式碼 + signing key 託管」三件事是否健康。認證在 &lt;a href="https://tarrragon.github.io/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 建&lt;/a> 裡是 commodity 買的典型、Auth0 正是它的 feature SaaS（dev-tool 端）例子；要不要買、外包到多深、見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度&lt;/a> 卡。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Auth0 是 &lt;em>customer identity 的控制面&lt;/em>、不是員工 SSO（員工走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta Workforce&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a>）。雖然 Auth0 於 2021 被 Okta 收購、目前屬「Customer Identity Cloud」產品線、跟 Workforce Okta 是 &lt;em>同公司不同 control plane&lt;/em>：tenant 叢集、事件分布、signing key 託管路徑都分開、Okta Workforce 的事故（2022 Sitel、2023 support system HAR）並未直接打到 Auth0 customer。&lt;/p>
&lt;p>跟自管 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a> 比、Auth0 把 Universal Login UI、social connection 預建、Rules / Action runtime、attack protection 都託管出去 — 代價是 &lt;em>SaaS 計費、token issuance / login attempt 都計量&lt;/em>、流量大的 B2C 場景遇到 credential stuffing 不擋會吃成本。跟 &lt;a href="https://docs.aws.amazon.com/cognito/">AWS Cognito&lt;/a> / &lt;a href="https://firebase.google.com/docs/auth">Firebase Auth&lt;/a> 比、Auth0 的核心優勢是 &lt;em>developer-first tenant 體驗 + 預建 social connection（Google / Facebook / Apple / Microsoft 等數十種）+ Action hook 寫 JS 客製&lt;/em>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Auth0 該承擔哪一段 customer identity 控制（login flow / token broker / profile store / B2B Organizations）、哪一段該回到自己的 app&lt;/li>
&lt;li>Auth0 tenant 的信任邊界與最低稽核需求（admin role、management API token、Action 程式碼、connection 設定）&lt;/li>
&lt;li>Auth0 流量出事或母公司事件時的降級路徑（fallback connection、token rotation、anomaly throttle）&lt;/li>
&lt;li>何時用 Auth0、何時走 Cognito / Firebase Auth / Keycloak 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Auth0 tenant 是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>Auth0 是 Customer Identity Cloud 的代表選項。它承擔三段責任：B2C / B2B app 的<em>使用者登入流程</em>託管、社交與企業 connection 的 token broker、user profile 與 metadata 的 store。當產品把登入交給 Auth0、信任邊界從「我的 app 自管密碼表」變成「tenant 配置 + Action hook 程式碼 + signing key 託管」三件事是否健康。認證在 <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> 裡是 commodity 買的典型、Auth0 正是它的 feature SaaS（dev-tool 端）例子；要不要買、外包到多深、見 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a> 卡。</p>
<h2 id="服務定位">服務定位</h2>
<p>Auth0 是 <em>customer identity 的控制面</em>、不是員工 SSO（員工走 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta Workforce</a> 或 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a>）。雖然 Auth0 於 2021 被 Okta 收購、目前屬「Customer Identity Cloud」產品線、跟 Workforce Okta 是 <em>同公司不同 control plane</em>：tenant 叢集、事件分布、signing key 託管路徑都分開、Okta Workforce 的事故（2022 Sitel、2023 support system HAR）並未直接打到 Auth0 customer。</p>
<p>跟自管 <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a> 比、Auth0 把 Universal Login UI、social connection 預建、Rules / Action runtime、attack protection 都託管出去 — 代價是 <em>SaaS 計費、token issuance / login attempt 都計量</em>、流量大的 B2C 場景遇到 credential stuffing 不擋會吃成本。跟 <a href="https://docs.aws.amazon.com/cognito/">AWS Cognito</a> / <a href="https://firebase.google.com/docs/auth">Firebase Auth</a> 比、Auth0 的核心優勢是 <em>developer-first tenant 體驗 + 預建 social connection（Google / Facebook / Apple / Microsoft 等數十種）+ Action hook 寫 JS 客製</em>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Auth0 該承擔哪一段 customer identity 控制（login flow / token broker / profile store / B2B Organizations）、哪一段該回到自己的 app</li>
<li>Auth0 tenant 的信任邊界與最低稽核需求（admin role、management API token、Action 程式碼、connection 設定）</li>
<li>Auth0 流量出事或母公司事件時的降級路徑（fallback connection、token rotation、anomaly throttle）</li>
<li>何時用 Auth0、何時走 Cognito / Firebase Auth / Keycloak 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Auth0 tenant 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能做什麼</strong>：Dashboard admin、Management API token 的 owner 與 scope、Action 是否走 code review、tenant 之間（dev / staging / prod）是否分離且授權獨立</li>
<li><strong>憑證在哪裡</strong>：Management API token / M2M client 的 scope 與 TTL、社交 connection 的 client secret 存放位置、signing key（per-tenant）的 rotation 節奏、是否啟用 Custom Domain（避免 token issuer 暴露 <code>*.auth0.com</code> 域名）</li>
<li><strong>入口如何暴露</strong>：登入走 Universal Login（託管 UI）還是 Embedded Login（嵌自家 app）、Cross-Origin Authentication 是否打開、Attack Protection（bot detection / brute-force / breached password / suspicious IP throttling）配置強度</li>
<li><strong>證據是否可回查</strong>：Tenant Log 是否同步到 SIEM（Log Stream 推 HTTP / Datadog / Splunk）、登入失敗 / Action 例外 / Management API 變更是否 alert、保留期是否符合合規要求</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">Authentication</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Tenant 與環境分離</strong>：Auth0 的 tenant 是邏輯隔離的多租戶 SaaS、不是物理叢集。每個環境（dev / staging / prod）開獨立 tenant、避免 dev 的 Action bug 打到 prod 流量、避免共用 client secret 跨環境洩漏。tenant 間用 <code>auth0-deploy-cli</code> 同步配置、Action 程式碼進版控。</p>
<p><strong>Connection 設計</strong>：Database Connection（Auth0 託管帳密 store）跟 Social / Enterprise Connection（OIDC / SAML federation 到 Google / Microsoft / Okta）是兩種來源。決策點是 <em>user 是否要進 Auth0 profile store</em> — 純 federation 不存密碼、純 Database Connection 是 Auth0 替 app 管帳密表。混用要清楚 <em>primary identity</em> 與 <em>linked account</em> 的合併規則。</p>
<p><strong>Action / Rule hook 的風險</strong>：Action（新框架）跟 Rule（舊框架）讓 tenant admin 在 login pipeline 注入 JS 程式碼（pre / post login、M2M、send email 等）。這是 Auth0 強大但也是 <em>最大的供應鏈攻擊面</em> — Action 可以 <code>require()</code> npm package、惡意 dependency 會在每個 login flow 執行。應該 pin dependency 版本、code review、用最小權限的 Management API scope、定期掃 dependency CVE（思維對齊 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/" data-link-title="7.R7.2 Supply Chain 類案例" data-link-desc="整理第三方整合、CI/CD、更新鏈、開源與 MSP 供應鏈事故案例">紅隊 supply chain 案例</a>）。</p>
<p><strong>Universal Login vs Embedded Login</strong>：Universal Login 把登入 UI 託管在 Auth0 domain（或 Custom Domain）、user 跳轉到該頁完成登入後 redirect 回 app — 防 phishing / CSRF 的成本由 Auth0 吃。Embedded Login 把登入表單嵌進自己 app 並用 <code>/co/authenticate</code> 端點 — 看似 UX 順、但要自己防 XSS、CSRF、CORS、credential leak、且要打開 Cross-Origin Authentication（暴露額外攻擊面）。預設選 Universal Login、Embedded 只在 UX 強需求且能承擔安全成本時開。</p>
<p><strong>Management API token / M2M client</strong>：Management API 控制整個 tenant（建 user、改 client secret、改 Action 程式碼）。token 不該長期存在程式碼或 CI；改用 M2M Application（client credentials grant）拿短期 token、scope 收到最小（<code>read:users</code> ≠ <code>update:users</code> ≠ <code>update:actions</code>）、走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 取用。</p>
<p><strong>Attack Protection 配置</strong>：B2C 流量大、登入嘗試本身計費也是攻擊面。Brute-force Protection（單 IP 多失敗鎖 user）、Suspicious IP Throttling（單 IP 多失敗鎖 IP）、Breached Password Detection（已洩漏密碼禁用）、Bot Detection（CAPTCHA / risk score）四個機制都該打開、否則 credential stuffing 既吃成本也提高帳號被接管的機率。</p>
<p><strong>Break-glass 與 fallback</strong>：B2C 場景沒有「員工備用 admin」概念、break-glass 是 <em>確保使用者在 Auth0 暫不可用時仍能登入</em>。常見作法：app 端容忍 Auth0 暫時失敗、提供 magic link / email OTP 的替代登入路徑（透過獨立 ESP）、或預先發放長 TTL 的 refresh token 撐過短時故障。tenant 管理面則維持至少 2 個獨立 admin、credential 離線存。</p>
<p><strong>Audit / handoff</strong>：Tenant Log 透過 Log Stream 推 SIEM、alert 三類事件 — Management API 對 Action / Connection / Client 的變更（供應鏈）、登入異常突增（credential stuffing）、support impersonation / Auth0 員工 access tenant 的紀錄（control plane）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Auth0</th>
          <th>AWS Cognito</th>
          <th>Firebase Auth</th>
          <th>自管 Keycloak</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制面責任</td>
          <td>Auth0 託管 issuer / signing / Action runtime</td>
          <td>AWS 託管、限 AWS 帳號信任邊界</td>
          <td>Google 託管、綁 Firebase / GCP</td>
          <td>自己跑 issuer、key、HA、support</td>
      </tr>
      <tr>
          <td>Social connection</td>
          <td>預建數十種、UI / token broker 完整</td>
          <td>主要 OIDC / SAML、social 要自己接</td>
          <td>Google / Apple / Facebook 預建、其他要自接</td>
          <td>OIDC / SAML 通用、specific provider 要自配</td>
      </tr>
      <tr>
          <td>客製化能力</td>
          <td>Action JS hook 強、Universal Login 高度客製</td>
          <td>Lambda Trigger、UI 客製有限</td>
          <td>Cloud Function Trigger、UI 客製中等</td>
          <td>任何 — 自己掌握程式碼</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>月活躍 user（MAU）+ B2B Organizations + 進階功能加價</td>
          <td>MAU 階梯、AWS 內部其他資源費用</td>
          <td>MAU + 簡訊 / phone auth 另計</td>
          <td>自管基礎設施成本</td>
      </tr>
      <tr>
          <td>成本陡升點</td>
          <td>大量 MAU、credential stuffing、Adaptive MFA 加價</td>
          <td>Cognito Identity Pool federation 複雜場景</td>
          <td>通常便宜、但 phone auth 成本明顯</td>
          <td>規模化後運維成本（HA、DR、cert、upgrade）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>B2C / B2B SaaS、要 social login、developer-first</td>
          <td>AWS-heavy 後端、不要求 social 廣度</td>
          <td>mobile-first、Firebase 生態內</td>
          <td>主權 / 自管要求、不接受 SaaS IdP</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中高 — user / password hash 可匯出、Action 要重寫</td>
          <td>中 — Cognito user pool 可匯出、policy 重寫</td>
          <td>中 — Firebase user 可匯出</td>
          <td>低 — 自己掌握</td>
      </tr>
  </tbody>
</table>
<p>選 Auth0 的核心訴求：<em>customer identity + 大量 social / enterprise connection + 要 developer 客製 login flow</em>、且接受 SaaS 計費與第三方控制面風險、能投入 SIEM / Action 程式碼治理 / attack protection 配置。</p>
<p>Microsoft 生態（Entra External ID / 前 Azure AD B2C）是另一個 B2C / B2B 選項、本表沒列入主要競品 — 它在 M365 / Azure 重度組織內是合理選擇、但 social connection 預建廣度跟 developer-centric tenant 體驗仍不及 Auth0。M365 重度 + B2C 需求的組織可同時評估 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID</a> 的 External ID 產品線。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Action / Rule 的供應鏈治理</strong>：Action 程式碼進版控、走 PR review、<code>auth0-deploy-cli</code> 部署。Action 引用的 npm dependency pin 版本、避免 <code>^</code> / <code>~</code>、CI 跑 SCA 掃 CVE。新增 Action 時 default scope 給 read-only、需要寫操作另外升級。Action secret（OAuth credential、API key）走 Action Secret 管理、不寫死在程式碼。</p>
<p><strong>B2B Organizations</strong>：Auth0 Organizations 把同 tenant 內的多客戶（B2B 場景）邏輯隔離 — 每個 organization 有自己的 connection、branding、member。設計點是 <em>user 是 organization member 還是 tenant-wide user</em>、跨 organization 操作的 admin 是否有 organization scope。Organization 之間的隔離是 tenant 內邏輯層、共享底層 control plane、不能等同實體 tenant 隔離。</p>
<p><strong>Adaptive MFA / Step-up Authentication</strong>：Auth0 Adaptive MFA 用 device / location / behavioral signal 動態升級 MFA 要求（impossible travel、新裝置、低信任 IP）。屬付費 add-on、本質是把 risk-based 認證內建。對 B2C 場景比強制全 user MFA 友善、但要把 <em>risk threshold</em> 跟 <em>false positive 容忍度</em> 設清楚、避免合法 user 被連續挑戰流失。</p>
<p><strong>Custom Domain</strong>：預設登入網域是 <code>&lt;tenant&gt;.auth0.com</code>、揭露使用 Auth0 與 tenant 名稱、且 issuer 是 Auth0 子網域。Custom Domain 把 issuer 改成自己網域（如 <code>login.example.com</code>）、user 看到的 URL 一致、降低 phishing 對照成本。屬付費功能、production app 預設應該開。</p>
<p><strong>Cross-Origin Authentication 的攻擊面</strong>：Embedded Login 必須開 Cross-Origin Authentication、讓 app 域名直接呼叫 Auth0 的 <code>/co/authenticate</code>。風險是 XSS 拿到 token、CSRF 偽造登入、third-party cookie 政策變動讓 silent auth 壞掉。Universal Login 不需要這個、所以同樣風險不存在 — 這是 Universal Login 推薦的核心理由。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Management API token 散落 / 過權</strong>：CI / 後端服務各自存 token、scope 都給 <code>update:users</code> / <code>update:actions</code> — 改 M2M Application + 最小 scope、定期 rotate、用 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 集中取用</li>
<li><strong>Action 直接 <code>require</code> 未 pin 的 npm package</strong>：login flow 每次都拉最新版、惡意 dependency 直接執行 — pin 版本、code review、定期掃 CVE</li>
<li><strong>登入嘗試暴增 / 計費突增</strong>：Attack Protection 沒開或門檻太鬆、credential stuffing 吃額度 — 打開 Bot Detection、Brute-force、Suspicious IP Throttling、配合 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Anomaly Detection</a></li>
<li><strong>使用 Embedded Login 又沒控 XSS</strong>：自家 app 一旦 XSS、token 直接被偷 — 改 Universal Login、或補上嚴格 CSP / DOM 防護、定期 pen test</li>
<li><strong>Tenant Log 沒進 SIEM</strong>：事件只在 Dashboard、無法跨系統 correlation — 配 Log Stream 打到 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">SIEM</a>、特定事件接 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
<li><strong>沒 Custom Domain</strong>：phishing 對照成本低、issuer 暴露 vendor — 配 Custom Domain、TLS cert 自管或走 Auth0 託管</li>
<li><strong>B2B Organizations 缺 scope 限制</strong>：admin 工具沒按 organization scope、單一 admin compromise 跨 organization 擴散 — 思維對齊 <a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">Okta Cross-Tenant 2023</a> 的 lesson</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>員工 SSO / Workforce identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
      </tr>
      <tr>
          <td>自管 / 不接受 SaaS IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a></td>
      </tr>
      <tr>
          <td>AWS-only 應用</td>
          <td><a href="https://docs.aws.amazon.com/cognito/">AWS Cognito</a></td>
      </tr>
      <tr>
          <td>Firebase / mobile-first 生態</td>
          <td><a href="https://firebase.google.com/docs/auth">Firebase Authentication</a></td>
      </tr>
      <tr>
          <td>Cloud resource 權限（非人類身份）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>事件偵測（跨系統）</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></td>
      </tr>
      <tr>
          <td>Secret / API key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Auth0 完整 OIDC / OAuth2 規格細節</li>
<li>Action / Rule 完整 API 與 trigger 清單</li>
<li>B2B Organizations 完整 schema 與 SDK 整合教學</li>
<li>Auth0 定價層級的詳細功能對照</li>
<li>各 social connection provider 的 OAuth app 註冊步驟</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Auth0 在 07 沒有直接案例（母公司 Okta 的事件並未直接打到 Auth0 customer），以下案例採對照引用、抽取對 Auth0 customer 的 lesson。要注意的是 <em>缺直接案例不等於 vendor 沒有風險</em> — Auth0 自 2021 被 Okta 收購以來未公開重大 vendor 級事件、但同類 SaaS IdP 的歷史事件（Okta 集團、signing key 託管、credential stuffing）都是 Auth0 customer 的可預期風險面、不該等到第一次出事才補控制：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Auth0 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System Incident 2023</a></td>
          <td>母公司 Workforce 事件、Auth0 customer 未直接受害；lesson：signing key 受託管時 break-glass 與替代登入路徑必要</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Management API token / connection client secret 的 rotation 要分域 — 多 tenant / 多 connection 不能用同一把</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023 Okta Token Follow-Through</a></td>
          <td>上游 IdP 事件後客戶側的 token rotation 節奏；Auth0 customer 應主動 rotate Management API token、不等供應商公告</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>Auth0 Adaptive MFA / step-up 的設計目標 — 高風險動作要求 phishing-resistant factor、避免單純 push fatigue</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/" data-link-title="7.R7.2 Supply Chain 類案例" data-link-desc="整理第三方整合、CI/CD、更新鏈、開源與 MSP 供應鏈事故案例">紅隊 supply chain 案例</a></td>
          <td>Action / Rule 引用 npm dependency 的供應鏈攻擊面、思維同 build pipeline 但發生在 login flow</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（Auth0 認證後的 cloud resource 權限層）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Auth0 異常如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://auth0.com/docs">Auth0 Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>AWS Secrets Manager</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/</guid><description>&lt;p>AWS Secrets Manager 是 AWS 原生的 &lt;em>static secret 集中保管 service&lt;/em>、核心能力是把 secret 用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">KMS&lt;/a> 加密儲存、加上 &lt;em>built-in rotation Lambda&lt;/em>（針對 RDS / Redshift / DocumentDB）跟 &lt;em>Resource Policy + IAM Policy 雙層 grant&lt;/em>、把 secret lifecycle 鎖在 AWS account / IAM 邊界內。設計取捨跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> 不同 — Secrets Manager 不做 dynamic credential、不做 transit encryption、不做內部 PKI、只把 &lt;em>static secret + AWS native DB rotation&lt;/em> 這條路徑做到極致。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Secrets Manager 的定位是 &lt;em>AWS-only workload 的 static secret 控制面&lt;/em>、跟 &lt;a href="https://docs.aws.amazon.com/systems-manager/">SSM Parameter Store&lt;/a> SecureString 在 &lt;em>存 secret&lt;/em> 這層功能重疊、但設計目的不同。Parameter Store 是 &lt;em>parameter 管理&lt;/em>（free tier、advanced parameter 每 10000 個約 $0.05、KMS 加密但無 staging label 與 rotation Lambda）；Secrets Manager 是 &lt;em>secret 管理&lt;/em>（每個 secret per month $0.40 + API call、有 staging label / rotation Lambda / Resource Policy / Cross-Region Replica）。價差 8 倍以上、選擇基準在 &lt;em>是否需要 rotation 跟 cross-account sharing&lt;/em>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> 比、Secrets Manager 是 &lt;em>單一雲、簡單、低運維&lt;/em>、Vault 是 &lt;em>跨雲、dynamic credential、高表達力&lt;/em>。AWS-only 組織用 Vault 等於多扛一個 HA cluster 運維成本只為了拿 KV engine 跟 RDS rotation、ROI 不划算；反向跨雲組織用 Secrets Manager 等於每個雲都自己一套 secret store、治理鏈會斷。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a> 比、設計理念類似（雲廠 managed、KMS 加密、IAM 授權）但 rotation 機制各家不同 — Secrets Manager 用 built-in Lambda 四階段 flow、GSM 用 Pub/Sub event 觸發自寫 Cloud Function、Azure 用 Key Vault rotation policy + Event Grid。&lt;/p></description><content:encoded><![CDATA[<p>AWS Secrets Manager 是 AWS 原生的 <em>static secret 集中保管 service</em>、核心能力是把 secret 用 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">KMS</a> 加密儲存、加上 <em>built-in rotation Lambda</em>（針對 RDS / Redshift / DocumentDB）跟 <em>Resource Policy + IAM Policy 雙層 grant</em>、把 secret lifecycle 鎖在 AWS account / IAM 邊界內。設計取捨跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 不同 — Secrets Manager 不做 dynamic credential、不做 transit encryption、不做內部 PKI、只把 <em>static secret + AWS native DB rotation</em> 這條路徑做到極致。</p>
<h2 id="服務定位">服務定位</h2>
<p>Secrets Manager 的定位是 <em>AWS-only workload 的 static secret 控制面</em>、跟 <a href="https://docs.aws.amazon.com/systems-manager/">SSM Parameter Store</a> SecureString 在 <em>存 secret</em> 這層功能重疊、但設計目的不同。Parameter Store 是 <em>parameter 管理</em>（free tier、advanced parameter 每 10000 個約 $0.05、KMS 加密但無 staging label 與 rotation Lambda）；Secrets Manager 是 <em>secret 管理</em>（每個 secret per month $0.40 + API call、有 staging label / rotation Lambda / Resource Policy / Cross-Region Replica）。價差 8 倍以上、選擇基準在 <em>是否需要 rotation 跟 cross-account sharing</em>。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 比、Secrets Manager 是 <em>單一雲、簡單、低運維</em>、Vault 是 <em>跨雲、dynamic credential、高表達力</em>。AWS-only 組織用 Vault 等於多扛一個 HA cluster 運維成本只為了拿 KV engine 跟 RDS rotation、ROI 不划算；反向跨雲組織用 Secrets Manager 等於每個雲都自己一套 secret store、治理鏈會斷。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 比、設計理念類似（雲廠 managed、KMS 加密、IAM 授權）但 rotation 機制各家不同 — Secrets Manager 用 built-in Lambda 四階段 flow、GSM 用 Pub/Sub event 觸發自寫 Cloud Function、Azure 用 Key Vault rotation policy + Event Grid。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 secret 用 Secrets Manager、哪些可以下放到 Parameter Store、哪些該走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 的 dynamic credential</li>
<li>Secrets Manager 的 <em>雙層 grant 模型</em>（Resource Policy + IAM Policy）跟 KMS encryption key custody 怎麼配</li>
<li>Built-in rotation 跟 Custom Rotation Lambda 的設計邊界、staging label 在 zero-downtime rotation 內的角色</li>
<li>何時 Secrets Manager 已經不夠用、要往 Vault / 跨雲 broker 走</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 Secrets Manager 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 GetSecretValue</strong>：IAM Policy 那邊是不是用 <code>secretsmanager:GetSecretValue</code> 限定到 <em>特定 secret ARN</em>（不是 <code>*</code>）、Resource Policy 是不是只允許特定 principal（不是 <code>Principal: *</code>）、跨帳號 share 有沒有用 ABAC tag 限縮</li>
<li><strong>KMS key custody</strong>：secret 用 <em>AWS-managed key</em>（<code>aws/secretsmanager</code>）還是 <em>customer-managed key</em>（CMK）— production 應該全部 CMK、key policy 限定 only Secrets Manager service principal 可用、KMS key 持有者跟 secret 持有者要分離</li>
<li><strong>Rotation 設定</strong>：rotation 開了沒、rotation interval 多久、Lambda 過去執行 success rate、staging label 在 rotation 過程中是否依序 promote（AWSPENDING → AWSCURRENT → AWSPREVIOUS）</li>
<li><strong>CloudTrail data event</strong>：<code>GetSecretValue</code> 是 <em>Data event</em>、預設不記、要手動開 data event logging — 沒開等於事故時看不到 <em>誰拿了 secret</em>、只看得到 management API（CreateSecret / UpdateSecret）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 跟 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Resource Policy + IAM Policy 雙層 grant</strong>：Secrets Manager 跟 S3 bucket policy 同模型 — IAM Policy 控制 <em>principal 端能做什麼</em>、Resource Policy 控制 <em>secret 端允許誰來</em>、兩者要 <em>都同意</em> 才放行。常見錯配：Resource Policy 寫 <code>Principal: &quot;*&quot;</code> 加 <code>aws:SourceAccount</code> condition 想做跨帳號 share、但 condition 漏寫或寫錯就變成公開可讀。跨帳號 share 一定要明確列 <code>Principal: arn:aws:iam::123456789012:role/AppRole</code>、不要靠 wildcard + condition 拼隔離。</p>
<p><strong>IAM Policy 細粒度授權</strong>：<code>secretsmanager:GetSecretValue</code> 該限定到 <em>specific secret ARN</em>（不是 <code>*</code>）、配合 ABAC tag condition（<code>secretsmanager:ResourceTag/team = payments</code>）限縮 blast radius。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation</a> — CI 出事時要能依 tag 快速列出 <em>CI runner 可拿的所有 secret</em>、沒這套 tag 就只能盲目 rotate 全部。</p>
<p><strong>KMS encryption key 選 CMK 不是 default</strong>：每個 secret 用一把 KMS key 加密、預設用 AWS-managed key <code>aws/secretsmanager</code>、production 應該換 customer-managed key（CMK）。差別在 <em>key policy 是不是自己控</em> — AWS-managed key 的 policy 同 account 任何 service 可呼叫、CMK 的 key policy 可以鎖到 only Secrets Manager service principal 加 only specific role 可 Decrypt。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558</a> 的對照啟示：<em>key 的 blast radius 來自 key policy</em>、用 CMK 把 policy 寫窄是減 blast radius 的關鍵動作。</p>
<p><strong>Built-in Rotation Lambda 只限 AWS native DB</strong>：Secrets Manager 內建 rotation template 涵蓋 RDS（PostgreSQL / MySQL / MariaDB / Oracle / SQL Server）/ Aurora / Redshift / DocumentDB — 拿 AWS 提供的 Lambda template、設定 rotation interval（最短 1 天、最長 365 天）、Secrets Manager 自動排程觸發。其他 DB（self-hosted PostgreSQL、MongoDB Atlas、Snowflake）或 API key 要寫 <em>Custom Rotation Lambda</em>、走 4-step state machine：<code>createSecret</code>（產新 credential 存為 AWSPENDING）、<code>setSecret</code>（把新 credential 寫到 target system）、<code>testSecret</code>（用新 credential 驗證可連）、<code>finishSecret</code>（promote AWSPENDING → AWSCURRENT）。Lambda 任一步失敗 Secrets Manager 會 rollback、舊 credential 不受影響。</p>
<p><strong>Staging Label（AWSCURRENT / AWSPENDING / AWSPREVIOUS）</strong>：staging label 是 <em>指向 version 的 pointer</em>、app 一律用 <code>GetSecretValue</code> 不帶 VersionStage 拿 AWSCURRENT、rotation 過程中 Secrets Manager 先把新 credential 標 AWSPENDING、testSecret 過後 promote 到 AWSCURRENT、舊的降到 AWSPREVIOUS。設計初衷是 <em>zero-downtime rotation</em> — 但 <em>只有 app 端支援 AWSPREVIOUS fallback</em> 期間才有意義：rotation 完成瞬間有些 app instance 還拿著舊 credential，target system 應該同時接受 AWSCURRENT 跟 AWSPREVIOUS（DB rotation template 會在 setSecret 階段保留舊 user 一段時間）。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a>：scope map 沒做、AWSPREVIOUS 窗口期太短、長尾 batch job 拿到舊 credential 就掛。</p>
<p><strong>Cross-Region Replica</strong>：multi-region app 把 secret replicate 到其他 region、replica 在 replica region 有獨立 ARN、KMS key 跟 rotation 都要在 replica region 各自配（不能跨 region 共用 KMS key）。replica 是 <em>讀副本</em>、寫只能在 primary region、rotation 觸發後新 version 自動 sync 到 replica（有秒級延遲）。failover 時 app 直接讀 replica region ARN、不需要 cross-region call。</p>
<p><strong>Cross-Account Sharing</strong>：跨帳號 share secret 走 Resource Policy + 對方帳號 IAM Policy 雙向授權 — Resource Policy 列對方 account 的具體 role ARN、對方 role 的 IAM Policy 加 <code>GetSecretValue</code> 對應 ARN。KMS key 也要跨帳號授權（KMS key policy 加對方 role 的 Decrypt 權限）— 漏了 KMS 授權會出現 <em>GetSecretValue 成功但 Decrypt 失敗</em> 的詭異錯誤。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS Secrets Manager</th>
          <th>SSM Parameter Store SecureString</th>
          <th><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></th>
          <th><a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a></th>
          <th><a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>AWS managed</td>
          <td>AWS managed</td>
          <td>自管 cluster</td>
          <td>GCP managed</td>
          <td>Azure managed</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>弱 — 綁 AWS</td>
          <td>弱 — 綁 AWS</td>
          <td>強</td>
          <td>弱 — 綁 GCP</td>
          <td>弱 — 綁 Azure</td>
      </tr>
      <tr>
          <td>每月每 secret 成本</td>
          <td>~$0.40 + API call</td>
          <td>free / advanced ~$0.05/10k</td>
          <td>self-hosted 成本</td>
          <td>~$0.06 + API call</td>
          <td>~$0.03 + operation</td>
      </tr>
      <tr>
          <td>Built-in rotation</td>
          <td>RDS / Redshift / DocumentDB 內建 Lambda</td>
          <td>無</td>
          <td>dynamic engine 自動發短期 credential</td>
          <td>無 built-in</td>
          <td>Key Vault rotation policy（key 為主）</td>
      </tr>
      <tr>
          <td>Staging label</td>
          <td>AWSCURRENT / AWSPENDING / AWSPREVIOUS</td>
          <td>無、用 version number</td>
          <td>KV v2 用 version</td>
          <td>version 機制</td>
          <td>version 機制</td>
      </tr>
      <tr>
          <td>Cross-account share</td>
          <td>Resource Policy + IAM</td>
          <td>不支援（同 account only）</td>
          <td>Vault namespace + policy</td>
          <td>IAM cross-project</td>
          <td>RBAC cross-tenant</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>無（rotation Lambda 是 static 換 static）</td>
          <td>無</td>
          <td>有（DB / cloud / SSH engine）</td>
          <td>弱（IAM impersonation）</td>
          <td>弱（Managed Identity）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-only + static secret + RDS rotation 為主</td>
          <td>AWS-only + 大量低敏 config + 不需 rotation</td>
          <td>跨雲 + dynamic credential + 內部 PKI</td>
          <td>GCP-only + Workload Identity 已主導</td>
          <td>Azure-only + Managed Identity 已主導</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低</td>
          <td>低</td>
          <td>中</td>
          <td>低</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 Secrets Manager 的核心訴求：<em>AWS-only</em> + <em>大部分 secret 是 static 或 AWS native DB credential</em> + <em>需要 cross-account share 或 rotation Lambda</em> + <em>不想 / 沒量能自管 Vault</em>。如果只是要存 config（feature flag、non-sensitive endpoint）、Parameter Store 8 倍便宜；如果跨雲 + 需要 dynamic credential / transit / PKI、Vault 才能滿足。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Custom Rotation Lambda 設計</strong>：4-step state machine 是 <em>idempotent contract</em> — Lambda 必須能被 Secrets Manager 重試任意步驟而不破壞狀態。常見實作陷阱：createSecret 不檢查 AWSPENDING 是否已存在、重試時又產生一把新的、AWSPENDING 對不上 setSecret 寫進去的；setSecret 沒處理「target system 已經有同名 user」的情況、第二次跑會卡住。Template 提供的 PostgreSQL rotation Lambda 用 <em>cloning approach</em> — 在 DB 內 clone 一份 user、改密碼、保留舊 user 跨 rotation 一個週期、下次 rotation 才 drop。</p>
<p><strong>Resource Policy + ABAC tag 跨帳號</strong>：跨帳號 share 時用 ABAC tag 條件比硬列 role ARN 有彈性 — Resource Policy 寫 <code>Condition: aws:PrincipalTag/team = payments</code>、對方 account 任何帶該 tag 的 role 都可讀。代價是 <em>tag 治理</em> 變成 critical control：對方 account 內誰能 attach tag = 誰能拿 secret、IAM Policy 要鎖 <code>iam:TagRole</code> 跟 <code>iam:UntagRole</code> 權限。</p>
<p><strong>Rotation 失敗的監控訊號</strong>：Lambda 執行失敗會在 CloudWatch 留 invocation error、Secrets Manager 把 rotation 標記為 failed、但 <em>secret 仍可用</em>（AWSCURRENT 保留舊 version）— 容易出現 <em>半年沒 rotate 成功但 app 看起來正常</em> 的盲區。要監控 <code>SecretsManager.RotationFailed</code> event（EventBridge rule）+ <code>LastRotatedDate</code> metric 超過 rotation interval 1.5 倍就 alert。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 整合</strong>：誰可以 <code>GetSecretValue</code> 完全由 IAM 控制、最佳實踐是 <em>workload role</em> 拿 secret（EC2 instance role / ECS task role / Lambda execution role / EKS IRSA）、不要硬把 AWS credential 塞進 secret 再給 application read。Secret 內容應該是 <em>DB password / API token / third-party credential</em>、不應該是 <em>AWS credential</em>（AWS credential 用 IAM role 短期 STS 拿就好）。</p>
<p><strong>CloudTrail data event 的成本權衡</strong>：開 <code>GetSecretValue</code> data event 等於每次 secret 取用都進 CloudTrail、高 QPS application 一天可能跑數百萬筆、CloudTrail 成本（每 100k events 約 $0.10）跟 S3 儲存成本會明顯上升。降本作法：在 EventBridge 用 <em>filtering</em>（只送特定 sensitive secret 的 data event 到 SIEM）、CloudWatch Logs 端設 retention 短一點（7-30 天熱資料、長尾走 S3 + Athena）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>GetSecretValue AccessDenied 但 IAM Policy 看起來對</strong>：檢查 Resource Policy 是否限定 source account / VPC、檢查 KMS key policy 是否允許該 role Decrypt — 兩層 grant + KMS 三點任一缺都會 AccessDenied</li>
<li><strong>跨帳號 secret 拿不到</strong>：Resource Policy 沒列對方 role、或 KMS key policy 沒給對方 Decrypt 權限 — 跨帳號要同步配三處（Resource Policy + 對方 IAM + KMS key policy）</li>
<li><strong>Rotation 一直失敗但沒人發現</strong>：沒設 EventBridge alert on <code>RotationFailed</code>、AWSCURRENT 保持舊 version、app 正常但 secret 過期 — 必設 LastRotatedDate metric alert</li>
<li><strong>App 拿到 stale secret rotation 後爆掉</strong>：app 端用了 SDK cache（如 AWS SDK 的 Secrets Manager Cache）、rotation 完成後 cache 沒 invalidate — cache TTL 要短於 staging label 重疊窗口、或實作 retry-on-auth-fail 觸發 cache refresh</li>
<li><strong>CloudTrail 看不到誰拿 secret</strong>：沒開 data event logging — 在 CloudTrail trail 設定加上 <code>AWS::SecretsManager::Secret</code> 為 data resource</li>
<li><strong>跨 region replica rotation 失效</strong>：rotation Lambda 只在 primary region 配、replica region 沒對應 Lambda — 每個 region 各自配 Lambda、或乾脆只在 primary rotate 讓 replica 自動 sync</li>
<li><strong>AWSPREVIOUS fallback 沒生效 batch job 掛</strong>：rotation Lambda finishSecret 太快 drop 舊 user、batch job 拿到舊 credential 連 DB 失敗 — DB rotation template 預設保留舊 user 一個 rotation 週期、custom Lambda 要自己實作雙軌窗口</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大量低敏 config / feature flag</td>
          <td><a href="https://docs.aws.amazon.com/systems-manager/">SSM Parameter Store</a>（free tier、無 rotation 需求）</td>
      </tr>
      <tr>
          <td>跨雲統一 secret 控制面</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></td>
      </tr>
      <tr>
          <td>Dynamic DB credential（non-AWS DB）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault database engine</a></td>
      </tr>
      <tr>
          <td>Workload 拿 AWS credential</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> role（EC2 instance role / ECS task role / IRSA）— 不要把 AWS credential 塞 secret</td>
      </tr>
      <tr>
          <td>Encryption-as-a-service / envelope encryption</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> Encrypt / Decrypt API、或 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault transit engine</a></td>
      </tr>
      <tr>
          <td>內部 PKI / mTLS workload cert</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> + <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS Private CA</a></td>
      </tr>
      <tr>
          <td>Secret rotation 跨服務 scope 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Secrets Manager 完整 API reference 跟 SDK 用法</li>
<li>每種 RDS engine 的 rotation Lambda template 內部 SQL 細節</li>
<li>AWS pricing 詳細計算（每 region 略有差異）</li>
<li>Terraform / CDK 跟 Secrets Manager 的 IaC 整合</li>
<li>AWS account organization / SCP 怎麼限制 secret 建立</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Secrets Manager 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Secrets Manager 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Secrets Manager rotation 必須有 scope map — 跨服務共用同一把 secret 時、AWSPREVIOUS 窗口期 + 雙軌驗證要對齊長尾 batch job、不能單靠 Lambda 自動 promote</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation (red-team)</a></td>
          <td>CI 出事時 Secrets Manager 內 <em>所有 CI runner role 可拿的 secret</em> 都要 rotate — 必須事先以 ABAC tag 標 blast radius、不然只能盲掃整個 account</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>對照啟示 — Secrets Manager 的 KMS encryption key 必須走 CMK 而非 AWS-managed key、key policy 限定 only Secrets Manager service principal 且 only specific role 可 Decrypt、把 blast radius 鎖在 key policy 內</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>（Secrets Manager 加密 key custodian、CMK 與 key policy 治理）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>（誰可以 GetSecretValue、跨帳號 share 的 principal 來源）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（secret 外洩事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/secretsmanager/">AWS Secrets Manager Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>AWS WAF</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/</guid><description>&lt;p>AWS WAF 是 &lt;em>AWS-internal&lt;/em> 的 Web Application Firewall、掛在 ALB、CloudFront、API Gateway、App Runner、AppSync 與 Cognito User Pool 的前面，攔截 HTTP/HTTPS 攻擊。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &amp;#43; ATO &amp;#43; Bot 一體">Fastly Next-Gen WAF&lt;/a> 的核心差異是 &lt;em>部署位置在 AWS 內部&lt;/em>：流量先經 AWS 邊界進來、再進 Web ACL 過濾、最後抵達 origin；不是在 Cloudflare anycast edge 提早攔。對 AWS-heavy 客戶、AWS WAF 的價值是 &lt;em>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / VPC / &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html">AWS Shield&lt;/a> 同一個控制面&lt;/em>；對 multi-cloud / on-prem origin、AWS WAF 觸不到、要回到 edge WAF。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>AWS WAF 的核心定位是 &lt;em>跟 AWS 服務深度耦合的 L7 防護層&lt;/em>。Web ACL 直接掛 AWS resource、規則用 IAM policy 管理、log 進 Kinesis Firehose / CloudWatch Logs / S3、跟 AWS Shield Standard（內含、L3/L4 DDoS）自動整合。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> 在 &lt;em>origin 之前的 edge&lt;/em> 攔截不同 — AWS WAF 流量 &lt;em>已經進到 AWS 邊界&lt;/em>、不是擋在外部。對 origin 跑在 ALB / CloudFront / API Gateway 後的客戶、AWS WAF 是天然選項；origin 在其他雲或地端、AWS WAF 觸不到。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &amp;#43; ATO &amp;#43; Bot 一體">Fastly Next-Gen WAF&lt;/a> 相比、AWS WAF 走 &lt;em>signature + managed rule group&lt;/em> 偵測模型、不像 Fastly NG-WAF 走語意 / behavioral；AWS WAF 的 Managed Rule Group 來自 AWS Managed 與 AWS Marketplace 第三方（Fortinet、F5、Imperva 等）、客戶端 &lt;em>看不到 rule logic&lt;/em>、debug 時要靠 sampled request 反推。&lt;/p></description><content:encoded><![CDATA[<p>AWS WAF 是 <em>AWS-internal</em> 的 Web Application Firewall、掛在 ALB、CloudFront、API Gateway、App Runner、AppSync 與 Cognito User Pool 的前面，攔截 HTTP/HTTPS 攻擊。它跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a> 的核心差異是 <em>部署位置在 AWS 內部</em>：流量先經 AWS 邊界進來、再進 Web ACL 過濾、最後抵達 origin；不是在 Cloudflare anycast edge 提早攔。對 AWS-heavy 客戶、AWS WAF 的價值是 <em>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / VPC / <a href="https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html">AWS Shield</a> 同一個控制面</em>；對 multi-cloud / on-prem origin、AWS WAF 觸不到、要回到 edge WAF。</p>
<h2 id="服務定位">服務定位</h2>
<p>AWS WAF 的核心定位是 <em>跟 AWS 服務深度耦合的 L7 防護層</em>。Web ACL 直接掛 AWS resource、規則用 IAM policy 管理、log 進 Kinesis Firehose / CloudWatch Logs / S3、跟 AWS Shield Standard（內含、L3/L4 DDoS）自動整合。這跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 在 <em>origin 之前的 edge</em> 攔截不同 — AWS WAF 流量 <em>已經進到 AWS 邊界</em>、不是擋在外部。對 origin 跑在 ALB / CloudFront / API Gateway 後的客戶、AWS WAF 是天然選項；origin 在其他雲或地端、AWS WAF 觸不到。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a> 相比、AWS WAF 走 <em>signature + managed rule group</em> 偵測模型、不像 Fastly NG-WAF 走語意 / behavioral；AWS WAF 的 Managed Rule Group 來自 AWS Managed 與 AWS Marketplace 第三方（Fortinet、F5、Imperva 等）、客戶端 <em>看不到 rule logic</em>、debug 時要靠 sampled request 反推。</p>
<p>計費模型也是關鍵差異：AWS WAF 按 <em>per-Web-ACL + per-rule + per-request</em> 計費（單 ACL $5/月、單 rule $1/月、$0.60 per 1M request），Managed Rule Group 算多 rule、開太多套 ruleset 與流量大時帳單會明顯漲。Cloudflare 是 plan-tier 計費（Pro / Business / Enterprise）、不會因為多開 rule 線性漲價。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>AWS WAF 在 AWS-internal 防護 stack 中承擔哪一段、哪些要靠 <a href="https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html">AWS Shield</a> / VPC / CloudFront 補位</li>
<li>Web ACL scope（Regional vs CloudFront）的選擇與跨 region 部署成本</li>
<li>Managed Rule Group / Custom Rule / Rate-based Rule 的取捨、Bot Control add-on 是否值得開</li>
<li>何時用 AWS WAF、何時走 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly NG-WAF</a> 的判準</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 AWS WAF 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>Web ACL scope 對不對</strong>：CloudFront distribution 必須掛 <em>CloudFront scope</em>（強制在 us-east-1 建立 ACL）、ALB / API Gateway 必須掛 <em>Regional scope</em>（每個 region 各一份）；scope 配錯掛不上去、跨 region 部署是否用 IaC（Terraform / CloudFormation）同步複製 ACL</li>
<li><strong>Managed Rule Group 與 sensitivity</strong>：是否啟用 <em>AWSManagedRulesCommonRuleSet</em>（CRS）、<em>AmazonIpReputationList</em>（已知惡意 IP）、<em>AnonymousIpList</em>（VPN / proxy / Tor）、<em>KnownBadInputsRuleSet</em>（已知 exploit pattern）、Marketplace rule 是否在 Count mode 觀察 1-2 週 FP 再切 Block</li>
<li><strong>Logging 有沒有開</strong>：Web ACL log 預設關閉、必須手動配 Kinesis Firehose / CloudWatch Logs / S3 destination；event 是否進 SIEM（見 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>）、是否能對 sampled request 反推 rule 行為</li>
<li><strong>IAM 邊界</strong>：誰能 update Web ACL（<code>wafv2:UpdateWebACL</code>、<code>wafv2:UpdateRuleGroup</code>）、是否限定 admin role 才能改、CI 是否只有 <code>wafv2:Get*</code> / <code>List*</code> 用來 verify、敏感變更是否走 Change Management / <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">Entry Point Protection</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Web ACL 與 scope</strong>：Web ACL 是 AWS WAF 的 <em>規則容器</em>、必須 attach 到 AWS resource。Scope 兩種：<em>Regional</em>（給 ALB / API Gateway / App Runner / AppSync / Cognito User Pool、每 region 獨立）與 <em>CloudFront</em>（給 CloudFront distribution、必須在 us-east-1 建立、全球生效）。同一個 ACL 不能跨 scope 共用；跨 region 部署同一套規則必須複製 ACL、用 Terraform / CloudFormation 管理避免 drift。</p>
<p><strong>Rule action 五種</strong>：每個 rule 觸發時可以做 <em>Block</em>（直接 403）、<em>Allow</em>（跳過後續 rule、放行）、<em>Count</em>（不擋、只記錄、用於 dry-run 觀察 FP）、<em>CAPTCHA</em>（出題給人類解、bot 過不去）、<em>Challenge</em>（silent JS challenge、無感驗證）。新 rule 上線標準動作是先 <em>Count</em> 1-2 週看 sample、確認 FP 在容忍範圍才切 <em>Block</em>。CAPTCHA / Challenge 是 <a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-bot-control.html">Bot Control add-on</a> 配套、要額外計費。</p>
<p><strong>Managed Rule Group（managed by AWS / Marketplace）</strong>：AWS Managed（免費含在 WAF）涵蓋 <em>Common Rule Set</em>（OWASP top10 對應）、<em>Known Bad Inputs</em>、<em>SQL Database</em>、<em>Linux</em>、<em>Unix</em>、<em>Windows</em>、<em>Anonymous IP List</em>、<em>Amazon IP Reputation List</em>、<em>Account Takeover Prevention (ATP)</em>、<em>Account Creation Fraud Prevention (ACFP)</em>。AWS Marketplace（付費）來自 Fortinet / F5 / Imperva / Cyber Security Cloud 等。Marketplace 規則 <em>不公開 rule logic</em>、攔錯時只能用 sampled request 反推、debug 比 AWS Managed 困難。</p>
<p><strong>Custom Rule（statement + 條件）</strong>：Custom Rule 用 <em>statement</em>（match condition + transformation）組合：IP Set match、Geo match、Regex Pattern Set、Size constraint、SQL injection match、XSS match、String match（含 header / body / URI / query 各部位）。複雜條件用 AND / OR / NOT 組合、上限是每 Web ACL 5,000 Web ACL Capacity Units（WCU）— 規則越複雜 WCU 越高、Marketplace 大型 rule group 可能直接吃掉一半 budget。</p>
<p><strong>IP Set / Regex Pattern Set</strong>：IP Set 存 IPv4 / IPv6 CIDR 清單、Regex Pattern Set 存正則表達式集合。兩者都是 <em>獨立資源</em>、可在多個 Web ACL 引用、單獨更新（不必動 Web ACL 結構）。實務上 threat intel feed 應該 push 到 IP Set、用 Lambda 自動 sync、不用手動加。</p>
<p><strong>Rate-based Rule</strong>：限制 <em>單一 aggregate key</em> 在滾動 5 分鐘窗口內的請求數、超過 threshold 觸發 action。aggregate key 可選 <em>IP</em>、<em>Forwarded-IP</em>（看 X-Forwarded-For）、<em>HTTP method</em>、<em>URI path</em>、<em>Header</em>、<em>Cookie</em> 或組合。關鍵陷阱：<strong>CloudFront 後 origin ALB 必須用 Forwarded-IP</strong>、否則 Rate-based Rule 看到的全是 CloudFront 邊緣節點 IP、所有真實使用者被合併計算、要嘛全擋要嘛全放。</p>
<p><strong>Logging 必須手動開</strong>：Web ACL log 預設關閉、destination 三選一：<em>Kinesis Data Firehose</em>（推到 S3 / Splunk / Datadog）、<em>CloudWatch Logs</em>（簡單但貴）、<em>S3</em>（直寫、需自己處理 partition）。production 通常走 Kinesis Firehose → S3 + Athena query、配合 SIEM 拉 alert。沒開 log 等於 <em>攻擊發生時沒證據</em>、事後無法回查。</p>
<p><strong>跟 AWS Shield 整合</strong>：所有 AWS WAF 客戶自動含 <em>Shield Standard</em>（L3/L4 DDoS、免費、SYN flood / UDP reflection 等基礎防護）。<em>Shield Advanced</em> 是付費 add-on（$3,000/month per organization + per-resource fee + data transfer out fee）、提供 <em>24/7 DRT（DDoS Response Team）</em>、cost protection（DDoS 期間 AWS service scaling fee 補貼）、進階分析。一般客戶 Shield Standard 已足夠；金融 / 政府 / 高知名度品牌需要 Shield Advanced 的 DRT 與 cost protection。</p>
<p><strong>Lambda@Edge / CloudFront Functions 補位</strong>：當 WAF rule statement 表達不出複雜業務邏輯（geofencing + business hour + user tier 組合、JWT claim 解析後判斷 routing）、用 <em>Lambda@Edge</em>（Node.js / Python、跑在 CloudFront 邊緣節點、4 個 phase：viewer-request / origin-request / origin-response / viewer-response）或 <em>CloudFront Functions</em>（純 JS、輕量、低延遲、只在 viewer-request / viewer-response）補位。Lambda@Edge 適合複雜邏輯、CloudFront Functions 適合 header rewrite / 簡單 routing；兩者都不能取代 WAF managed rule、但補位 WAF 表達力上限。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 整合</strong>：誰能改 Web ACL 是 <em>IAM policy</em> 決定（<code>wafv2:CreateWebACL</code>、<code>wafv2:UpdateWebACL</code>、<code>wafv2:AssociateWebACL</code>、<code>wafv2:UpdateRuleGroup</code> 等 action）。production 標準配置：admin role 才能 update、CI / 開發者只有 <code>wafv2:Get*</code> / <code>List*</code> 用來 verify、敏感變更走 Change Management + CloudTrail <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS WAF</th>
          <th>Cloudflare WAF</th>
          <th>Fastly Next-Gen WAF</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署位置</td>
          <td>AWS 內部（ALB / CloudFront / API Gateway 前）</td>
          <td>Cloudflare global edge（300+ POP）</td>
          <td>Fastly global edge / 各 origin agent</td>
      </tr>
      <tr>
          <td>Origin 適配</td>
          <td>強耦合 — origin 必須在 AWS</td>
          <td>強中立 — 任意雲 / on-prem</td>
          <td>強中立 — Fastly CDN / 任何 origin</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>per-ACL + per-rule + per-request</td>
          <td>plan tier（Free / Pro / Business / Enterprise）</td>
          <td>request-based + plan</td>
      </tr>
      <tr>
          <td>Managed Rule</td>
          <td>AWS Managed（免費）+ Marketplace（付費、logic 不透明）</td>
          <td>Cloudflare Managed + OWASP CRS + Exposed Credentials</td>
          <td>Signal-based（語意、低 FP、不靠 regex signature）</td>
      </tr>
      <tr>
          <td>Rate Limiting</td>
          <td>Rate-based Rule（含在 WAF、5 分鐘 window）</td>
          <td>Rate Limiting 獨立 product</td>
          <td>inline rate limit + Signal</td>
      </tr>
      <tr>
          <td>Bot 對應</td>
          <td>AWS WAF Bot Control（add-on、付費）</td>
          <td>Bot Management（Pro+ add-on）</td>
          <td>NG-WAF behavioral bot detection</td>
      </tr>
      <tr>
          <td>DDoS 內建</td>
          <td>Shield Standard 自動含（L3/L4）、Advanced 加價</td>
          <td>同套餐內建</td>
          <td>內建 + Fastly DDoS</td>
      </tr>
      <tr>
          <td>控制面整合</td>
          <td>跟 IAM / CloudTrail / Shield / VPC 同 plane</td>
          <td>Cloudflare 控制面、跟其他 Cloudflare 產品同套</td>
          <td>Fastly 控制面、agent 跑在 origin</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中陡 — Web ACL + WCU + scope + IAM policy 多軌</td>
          <td>中 — UI / Rules language / Terraform 完整</td>
          <td>中 — agent 安裝 + Signal 語意設定</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-heavy、ALB / CloudFront 是主要入口</td>
          <td>Multi-cloud / on-prem origin、要整套 edge security</td>
          <td>高 FP 容忍度低、業務有 schema、想避 regex signature</td>
      </tr>
  </tbody>
</table>
<p>選 AWS WAF 的核心訴求：<em>AWS-internal app</em> + origin 跑在 ALB / CloudFront / API Gateway / App Runner 後 + 想跟 IAM / CloudTrail / Shield 同套 control plane 治理。Origin 不在 AWS、或要 <em>把攻擊擋在抵達雲之前</em>、應該走 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 或 <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly NG-WAF</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>AWS WAF Bot Control（add-on）</strong>：付費 add-on、用 AWS 自家 bot fingerprinting 區分 <em>verified bot</em>（搜尋引擎）/ <em>signal: automated browser</em>（headless Chrome 等）/ <em>signal: known bot</em>（已標記 IoT / scraper），給每個請求 <em>bot category label</em>。Custom Rule 在 label 上做條件、決定 Block / Challenge / CAPTCHA。比 user-agent 過濾準很多、但要額外計費（per-request）。Bot Control 有兩個 inspection level：<em>common</em>（便宜、基礎指紋）與 <em>targeted</em>（貴、含 JavaScript challenge、CAPTCHA、token-based）。</p>
<p><strong>Fraud Control（ATP / ACFP）</strong>：<em>Account Takeover Prevention</em>（ATP）跟 <em>Account Creation Fraud Prevention</em>（ACFP）是 Managed Rule Group 的特殊類別、需付費啟用。ATP 看登入端點的 credential stuffing、ACFP 看註冊端點的 bot signup。兩者都用 AWS 自家 threat intel（被竊憑證 list、行為模型）打 label、客戶側用 Custom Rule 處理。對有 login / signup 端點的 SaaS / 電商有價值、純內部後台不必開。</p>
<p><strong>CAPTCHA / Challenge</strong>：AWS WAF 內建 CAPTCHA puzzle 與 silent JS Challenge、可在 rule action 直接呼叫。Challenge 在客戶端執行 proof-of-work、合法瀏覽器無感、headless 工具卡住；CAPTCHA 是視覺題、人類解、bot 不會。Production 標準做法：Bot Control 給 label → Custom Rule 看 label → likely bot 走 Challenge、known bad 走 Block、人類流量直接 Allow。</p>
<p><strong>ACM Private CA + WAF 對 mTLS</strong>：AWS WAF 本身不做 mTLS 驗證、mTLS 是 ALB / API Gateway / CloudFront 自己的功能（搭配 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> Private CA 簽發 client cert）。WAF 在 mTLS 完成後才看 L7 流量、可以用 <em>HTTP header match</em>（mTLS 後 ALB 注入 client cert 資訊到 header）做進一步 rule。Internal API 用 mTLS + WAF 是常見組合。</p>
<p><strong>Lambda@Edge 補 inline business logic</strong>：複雜判斷（user tier × geo × business hour × A/B test）WAF rule statement 表達不出來、用 Lambda@Edge 在 <em>viewer-request</em> phase 解析 JWT、查 internal risk API、回 response header 給 WAF 後續判斷。代價：Lambda@Edge 部署只能在 us-east-1、code 更新傳播到全球 edge 要幾分鐘、debug 是分散式 CloudWatch Logs。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Web ACL 掛不上 CloudFront</strong>：scope 配成 Regional、CloudFront 拒絕 attach — Web ACL 必須在 us-east-1 + CloudFront scope 才能掛 CloudFront；ALB / API Gateway 反過來只能掛 Regional scope</li>
<li><strong>Rate-based Rule 全擋 / 全放</strong>：CloudFront 後 origin 看到全部都是 CloudFront IP、aggregate key 沒換 Forwarded-IP — 改用 <em>Forwarded-IP</em>（X-Forwarded-For）作 aggregate key，並設 Fallback behavior</li>
<li><strong>Managed Rule Group 誤殺合法請求</strong>：CRS High sensitivity 開後 file upload / rich text editor 端點被 Block — 找 sampled request 看 rule_id、用 <em>Scope-down statement</em> 限定該 rule 在某 path 不執行、或開該 rule 為 Count、不要關整個 group</li>
<li><strong>Marketplace Rule 攔不明流量</strong>：Marketplace rule logic 不公開、sampled request 看到 rule label 但不知為何 — 切該 rule 到 Count mode 觀察、若無 attack 跡象換 AWS Managed 同類 rule</li>
<li><strong>WCU 超限</strong>：Web ACL 上限 5,000 WCU、加 Marketplace + 多個 AWS Managed 就會爆 — 看 <em>Capacity Used</em>、移除重疊 rule、把 Custom Rule 表達式簡化（少用 <em>transformation chain</em>）</li>
<li><strong>Logging 沒設 / 設錯</strong>：事件發生後沒有完整 log 可查、只有 sampled request（保留 3 小時、機率抽樣） — 必開 <em>Logging configuration</em> 到 Kinesis Firehose / S3 / CloudWatch Logs、確認 IAM role 有 firehose:PutRecord 權限</li>
<li><strong>IAM 權限過寬</strong>：CI account 拿到 <code>wafv2:*</code> 整 zone 都能改 — 收斂到 <code>wafv2:Get*</code> / <code>List*</code> 唯讀、敏感寫入限 admin role + MFA + Change Management</li>
<li><strong>跨 region 部署 drift</strong>：手動在 console 改 us-east-1 ACL、其他 region 沒同步 — 用 Terraform / CloudFormation IaC 管理、PR review、CI plan 檢查 drift</li>
<li><strong>Shield Standard 不夠擋大型 L7 DDoS</strong>：Standard 只防 L3/L4、L7 attack 靠 WAF Rate-based Rule + Bot Control — 若反覆遭遇大型 L7 DDoS、評估 Shield Advanced 的 DRT + cost protection 是否值得</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-cloud / on-prem origin</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a></td>
      </tr>
      <tr>
          <td>低 FP 容忍 / 業務有 schema</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></td>
      </tr>
      <tr>
          <td>L3/L4 DDoS 進階防護</td>
          <td>AWS Shield Advanced / Cloudflare Magic Transit</td>
      </tr>
      <tr>
          <td>純內部 mTLS / east-west</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> + service mesh</td>
      </tr>
      <tr>
          <td>Cert lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> / <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>Secrets / API key</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></td>
      </tr>
      <tr>
          <td>複雜業務邏輯 inline 處理</td>
          <td>Lambda@Edge / CloudFront Functions</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS WAF Classic（v1）的遷移細節 — 本頁全以 WAFv2 為準</li>
<li>完整 WCU 計算規則與每個 statement 的 WCU cost reference</li>
<li>Marketplace 第三方 rule group 各家功能矩陣</li>
<li>AWS WAF 在 GovCloud / China region 的差異</li>
<li>Bot Control / ATP / ACFP 完整 label schema reference</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>AWS WAF 在 07 案例庫無直接 vendor-level case、但多個 case 對應 WAF 作為 <em>修補窗口期臨時控制</em> 與 <em>entry point 治理</em> 的角色：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 AWS WAF 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — AWS Managed Rule Group 當時推出 Log4Shell 規則作為 emergency mitigation；但 exploitation 通過 WAF 後在後端執行，不能單靠 WAF 防 supply chain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — WAF 攔不住 edge appliance zero-day、需要「修補 + session 失效 + 異常清查」三同步</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet SSL-VPN CVE 2023-27997</a></td>
          <td>對照啟示 — vendor patch 前的臨時 AWS WAF Custom Rule + Shield Advanced + Origin lockdown 是修補窗口期動作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></td>
          <td>AWS WAF 是 entry point protection 的工具、章節原則對應 WAF rule lifecycle 治理（Count → Block、IaC、IAM 收斂）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>、<a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>（WAF block 不夠時、資料層也要遮罩）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>（誰能改 Web ACL）、<a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a>（mTLS client cert）、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>（rule update 用的 API key）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（WAF block 事件如何 routing 進 IR）</li>
<li>官方：<a href="https://docs.aws.amazon.com/waf/latest/developerguide/">AWS WAF Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Elastic Security</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/</guid><description>&lt;p>Elastic Security 是 Elastic Stack（Elasticsearch + Kibana + Beats / Agent）上的 SIEM + EDR + Cloud Security 套件、OSS 起源、現屬 Elastic 商業版的 Solution。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a> 的差異在 &lt;em>計費模型 + 查詢語言模型 + ecosystem 開放度&lt;/em>、偵測能力本身相近 — Elastic 走 &lt;em>resource-based pricing&lt;/em>（按 cluster size 而非 ingestion volume）、且提供 KQL / EQL / Lucene / ES|QL 四種互補的查詢語言。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Elastic Security 的核心定位是 &lt;em>Elastic Stack 上的 security solution&lt;/em>、底層是 &lt;em>Elasticsearch&lt;/em>（資料層）+ &lt;em>Kibana&lt;/em>（查詢與 UI 層）+ &lt;em>Fleet / Elastic Agent&lt;/em>（採集層）、頂層產品分三條：&lt;em>Elastic SIEM&lt;/em>（log aggregation + detection rule + Case + Timeline）、&lt;em>Elastic Defend&lt;/em>（前 Endgame 收購而來、EDR + endpoint protection、跟 CrowdStrike / SentinelOne 同層）、&lt;em>Elastic Cloud Security&lt;/em>（CSPM + CWP、雲端資源 misconfig 與 workload 防護）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> 比、Elastic 走 &lt;em>OSS-friendly + resource-based pricing&lt;/em> — TB-scale ingestion 不直接漲費用（要 scale node 但邊際成本遠低於 Splunk per-GB 累進）、Sigma rule 社群可直接 import 5000+ 規則；但 Splunk Security Content 跟 SOAR / RBA 等 detection content + SOC tooling 成熟度仍高一個量級。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 比、Elastic 跨 on-prem + 多雲、可自管也可 Elastic Cloud SaaS；Datadog 是 SaaS-only、適合純 cloud-native。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a> 比、Elastic 多查詢語言（KQL / EQL / Lucene / ES|QL）、Google 走 YARA-L 單一統一語言、超大規模 ingestion Google 反而划算。&lt;/p></description><content:encoded><![CDATA[<p>Elastic Security 是 Elastic Stack（Elasticsearch + Kibana + Beats / Agent）上的 SIEM + EDR + Cloud Security 套件、OSS 起源、現屬 Elastic 商業版的 Solution。它跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 的差異在 <em>計費模型 + 查詢語言模型 + ecosystem 開放度</em>、偵測能力本身相近 — Elastic 走 <em>resource-based pricing</em>（按 cluster size 而非 ingestion volume）、且提供 KQL / EQL / Lucene / ES|QL 四種互補的查詢語言。</p>
<h2 id="服務定位">服務定位</h2>
<p>Elastic Security 的核心定位是 <em>Elastic Stack 上的 security solution</em>、底層是 <em>Elasticsearch</em>（資料層）+ <em>Kibana</em>（查詢與 UI 層）+ <em>Fleet / Elastic Agent</em>（採集層）、頂層產品分三條：<em>Elastic SIEM</em>（log aggregation + detection rule + Case + Timeline）、<em>Elastic Defend</em>（前 Endgame 收購而來、EDR + endpoint protection、跟 CrowdStrike / SentinelOne 同層）、<em>Elastic Cloud Security</em>（CSPM + CWP、雲端資源 misconfig 與 workload 防護）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 比、Elastic 走 <em>OSS-friendly + resource-based pricing</em> — TB-scale ingestion 不直接漲費用（要 scale node 但邊際成本遠低於 Splunk per-GB 累進）、Sigma rule 社群可直接 import 5000+ 規則；但 Splunk Security Content 跟 SOAR / RBA 等 detection content + SOC tooling 成熟度仍高一個量級。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 比、Elastic 跨 on-prem + 多雲、可自管也可 Elastic Cloud SaaS；Datadog 是 SaaS-only、適合純 cloud-native。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 比、Elastic 多查詢語言（KQL / EQL / Lucene / ES|QL）、Google 走 YARA-L 單一統一語言、超大規模 ingestion Google 反而划算。</p>
<p>關鍵張力：<em>多查詢語言模型</em> 同時是 Elastic 的優勢跟負擔。EQL 寫 attack chain sequence 比 SPL correlation 更直接、KQL 過濾快、ES|QL 寫 aggregation 像 SQL 直覺、Lucene 處理 full-text；但 SOC team 要決定哪個 rule 用哪個語言、不能讓每個 analyst 各寫各的。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Elastic Security 在 SOC stack 中承擔哪一段（log aggregation / SIEM / EDR / CSPM）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> IdP log、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> secret rotation）</li>
<li>KQL / EQL / Lucene / ES|QL 四種查詢語言的職責分工（誰用在哪種 rule、誰負責教育 SOC）</li>
<li>Resource-based pricing 的治理（cluster sizing、hot-warm-cold tier、Searchable Snapshots、Elastic Cloud Serverless）</li>
<li>何時用 Elastic、何時走 Splunk / Datadog / Google Security Ops 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Elastic Security deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能改 detection rule</strong>：Elastic Security app 的 rule editor 權限、<code>detection-rules</code> repo（Elastic 官方 OSS rule 庫）有沒有 fork 進組織版控、rule change 是否走 PR review + staging space 驗證</li>
<li><strong>採集治理</strong>：Fleet 統一管 Elastic Agent policy / 還是散落 Beats（filebeat / metricbeat / auditbeat / winlogbeat）各自設定、log source 是否分 hot / warm / cold tier、Searchable Snapshots 是否開</li>
<li><strong>Detection content coverage</strong>：Elastic Prebuilt rules + Sigma 社群規則 import 多少 enabled、是否跟 MITRE ATT&amp;CK 對照、EQL sequence 規則覆蓋多少 attack chain pattern</li>
<li><strong>Alert quality / SOC handoff</strong>：alert volume per day、Case 跟 Timeline 是否進入日常 SOC workflow、ML anomaly job 是否在線 + threshold 是否 tuned、跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> 的 routing 是否定義</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Ingestion architecture</strong>：log 進 Elastic 三種主路徑 — <em>Elastic Agent + Fleet</em>（現代部署的預設、單一 agent 收 system / endpoint / cloud / app log、中央 Fleet server 統一管 policy）、<em>Beats</em>（filebeat / metricbeat / auditbeat / winlogbeat 等專用 agent、Fleet 推出前的傳統做法、現在持續支援但建議遷移到 Elastic Agent）、<em>Logstash</em>（pipeline-style ETL、用在 enrich / filter / route 複雜場景）。production 通常 Elastic Agent + Fleet 為主、Logstash 補 ETL 缺口。</p>
<p><strong>KQL / EQL / Lucene / ES|QL 的職責分工</strong>：四種查詢語言各有 first-class 場景。<em>KQL</em>（Kibana Query Language）是 Kibana 預設過濾語法、<code>user.name : &quot;alice&quot; and event.action : &quot;logon-failed&quot;</code>、簡單直觀、適合 dashboard / Discover 過濾。<em>EQL</em>（Event Query Language）做 sequence pattern matching、<code>sequence by user.name [authentication where event.outcome==&quot;failure&quot;] [authentication where event.outcome==&quot;success&quot; and source.geo.country != &quot;TW&quot;]</code>、表達 attack chain 比 SPL correlation 更直接。<em>Lucene</em> 是底層 full-text query、特殊需要時直接寫。<em>ES|QL</em>（Elasticsearch Query Language、2024+）是新版 SQL-like、<code>FROM logs-* | WHERE event.category == &quot;authentication&quot; | STATS count = COUNT(*) BY user.name</code>、寫 aggregation 直覺；屬新語言、production 採用 cadence 還在跟進中。</p>
<p><strong>Detection rule 種類</strong>：Elastic Security 的 rule type 是六種 first-class 概念、不是只有「query rule」一種 — <em>Query rule</em>（KQL / Lucene 觸發）、<em>EQL rule</em>（sequence pattern）、<em>Threshold rule</em>（聚合超過閾值、例如同一 IP 5min 內 login fail &gt; 100）、<em>ML rule</em>（綁 Elastic ML anomaly job、anomaly score 超過閾值觸發）、<em>New term rule</em>（首次出現的 entity、例如某 user 第一次從某國登入）、<em>Indicator match rule</em>（事件 enrich 比對 threat intel feed、IoC hit 觸發）。production rule 經常組合多種 — query rule 做粗篩、EQL rule 抓 sequence、threshold + ML 補 baseline anomaly。</p>
<p><strong>Sigma rule import</strong>：Sigma 是 OSS 通用 detection rule 格式（YAML、跨 SIEM 可移植）、社群維護 5000+ 規則。Elastic 支援直接 import Sigma rule 轉成 Elastic detection rule、是 Elastic 拉開跟商業 SIEM 距離的 OSS 槓桿。實務做法：先 import Sigma baseline + 全部走 staging space 跑 false positive 觀察、再 enable 到 production；不要直接全 enable、Sigma rule 跨 SIEM 通用所以 environment-specific tuning 必須自己做。</p>
<p><strong>Case + Timeline</strong>：Case 是 incident 容器、聚合 alert + comment + assignment + status；Timeline 是 SOC analyst 的 investigation workspace、可以 pin event / annotate / link related alert、產出 investigation narrative。兩者組合是 Elastic 的 SOC workflow first-class、不是外掛 — 對應 Splunk ES 的 Notable Event + Incident Review、但 Elastic 走 OSS 化、Case 可 export markdown 進 ticketing。</p>
<p><strong>Elastic Defend（EDR）</strong>：前 Endgame 收購整合、提供 endpoint detection + prevention（malware block / ransomware protection / behavior detection）、跟 CrowdStrike Falcon / SentinelOne 同層。Elastic Defend 跑在 Elastic Agent 內、policy 從 Fleet 推。實務上多數 SIEM 客戶不會用內建 EDR、而是外接專業 EDR feed 進 Elastic SIEM；但 OSS-friendly + 預算敏感的中型客戶可以直接整合到一個 stack。</p>
<p><strong>Cross-cluster search</strong>：跨多個 Elastic cluster 統一查詢（<code>remote_cluster:index-name</code>）、適合 multi-region / multi-tenant SOC、不需要把所有 log 搬到單一 cluster。對應 Splunk Cloud federated search。實務場景：歐洲 GDPR 資料留在 EU cluster、美國 cluster query 過去做 incident investigation 而不複製資料。</p>
<p><strong>ML jobs（anomaly detection）</strong>：Elastic ML 內建 unsupervised anomaly detection、pre-built ML job library 覆蓋 SOC 常見場景（user behavior baseline、host login pattern、port scan detection、rare process）。ML rule 綁 ML job、anomaly score 超過閾值觸發 detection rule。對應 Splunk UBA、但 Elastic ML 是 stack 內建、不是 add-on app。</p>
<p><strong>Resource-based pricing 治理</strong>：Elastic Cloud 按 <em>cluster size</em>（node count × node size）計費、不按 ingestion volume — 意義是 ingest 多 log 不直接漲費用、但要 scale node 維持查詢效能。實務治理：<em>hot tier</em>（最近 7-30 天、SSD 高效能 node）、<em>warm tier</em>（30-90 天、低 IO node）、<em>cold tier</em> / <em>frozen tier</em>（90 天以上、Searchable Snapshots on S3 / GCS、查詢慢但成本極低）。對應 Splunk SmartStore、但 Elastic frozen tier 把 retention 從幾個月延長到幾年、cost 不線性漲。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Elastic Security</th>
          <th>Splunk</th>
          <th>Datadog Security</th>
          <th>Google Security Operations</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>Resource-based（node / cluster size）</td>
          <td>Ingestion-based（GB/day、累進）</td>
          <td>Per-host + per-event（events/month）</td>
          <td>Fixed price by data tier（PB-scale 划算）</td>
      </tr>
      <tr>
          <td>查詢語言</td>
          <td>KQL / EQL / Lucene / ES|QL 四種互補</td>
          <td>SPL（單一強表達力）</td>
          <td>Datadog Query（沿用 observability 語法）</td>
          <td>YARA-L（統一、結構清楚）</td>
      </tr>
      <tr>
          <td>Sequence 表達</td>
          <td>EQL <code>sequence by</code> 直接表達 attack chain</td>
          <td>SPL transaction / streamstats</td>
          <td>log + metrics + trace 同 plane</td>
          <td>UDM + YARA-L 多事件 rule</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted / Elastic Cloud / Serverless</td>
          <td>Self-hosted (Enterprise) / SaaS (Cloud)</td>
          <td>SaaS only</td>
          <td>SaaS only（Google Cloud）</td>
      </tr>
      <tr>
          <td>Detection content</td>
          <td>Elastic Prebuilt rules + Sigma 社群 5000+</td>
          <td>Splunk Security Content（最豐富、社群活躍）</td>
          <td>Datadog Security Rules（中等）</td>
          <td>Google YARA-L + Google threat intel</td>
      </tr>
      <tr>
          <td>EDR 整合</td>
          <td>Elastic Defend 內建（前 Endgame）</td>
          <td>外接 CrowdStrike / Defender</td>
          <td>Workload Security（容器 focus）</td>
          <td>外接（透過 forwarder）</td>
      </tr>
      <tr>
          <td>SOAR / Response</td>
          <td>Cases + Endpoint response（Elastic Defend）</td>
          <td>Splunk SOAR（前 Phantom、業界先驅）</td>
          <td>Workflow Automation（基本）</td>
          <td>SOAR 內建（前 Siemplify）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>OSS-friendly、中大型、Elastic stack 已用</td>
          <td>Enterprise + 跨 on-prem、預算允許</td>
          <td>Cloud-native + observability 已用 Datadog</td>
          <td>超大規模 ingestion、Google 雲 + 多雲 SOC</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — Sigma / Lucene / EQL 部分可移植</td>
          <td>高 — SPL / detection content / dashboard 量多</td>
          <td>中</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<p>選 Elastic 的核心訴求：<em>OSS-friendly 文化 + resource-based pricing 友善 + Elastic Stack 已作為 observability 在用</em>、團隊有能力跨四種查詢語言（或至少把 EQL 跟 KQL 雙語分工清楚）、能接受 detection content 跟 SOAR 成熟度 trade-off。TB-scale ingestion 時 Elastic 比 Splunk 省 60-80% license cost 是最大誘因、但要算進 cluster sizing 跟 SRE 維運的隱形成本。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>EQL sequence pattern（時序攻擊鏈）</strong>：EQL 的 <code>sequence by</code> 是 Elastic 表達 attack chain 的 first-class 武器、比 SPL correlation 直接。例如 MFA fatigue 寫成 <code>sequence by user.name with maxspan=5m [authentication where event.outcome==&quot;failure&quot;] [authentication where event.outcome==&quot;failure&quot;] [authentication where event.outcome==&quot;success&quot; and source.ip != known_ip]</code>、序列邏輯直接表達。配對 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a> lesson：MFA fail 序列 + 新裝置 success 直接觸發。</p>
<p><strong>Elastic Defend endpoint response</strong>：除偵測外、Defend 支援 host isolation（隔離受感染 endpoint 但保留 SOC 連線）、process kill、file quarantine 等 response action、直接從 Kibana Security app 觸發。對應 CrowdStrike Real Time Response。production 採用前要設 approval gate、避免 SOC analyst 誤觸動 production server。</p>
<p><strong>CSPM / CWP（Elastic Cloud Security）</strong>：CSPM（Cloud Security Posture Management）對 AWS / GCP / Azure 帳號做 misconfig 掃描（S3 bucket public、IAM over-permission、security group 0.0.0.0/0）、對照 CIS Benchmark；CWP（Cloud Workload Protection）對 Kubernetes workload 跑 runtime detection。屬較新的功能、跟 Wiz / Lacework 等專業 CNAPP 比覆蓋還在追趕。</p>
<p><strong>Cross-cluster search 跨環境 federated query</strong>：multi-region SOC 的 first-class 工具 — query 寫 <code>FROM logs-auth-*, eu-cluster:logs-auth-*</code>、Elastic 自動路由跨 cluster。實務注意：跨 cluster query 延遲較高、要設 timeout；資料合規（GDPR）必須留意 query 結果是否包含跨境資料、不是搬資料但 query 結果回傳算不算傳輸要法務確認。</p>
<p><strong>Sigma 規則社群</strong>：Sigma 是 OSS detection rule 通用格式、Elastic 是 Sigma 主力使用者（內建 importer + Elastic 工程師參與 Sigma upstream）。實務做法：fork SigmaHQ repo 進組織版控、CI pipeline 自動轉 Sigma → Elastic detection rule、staging space 跑 false positive curve、promote 到 production；不要每次 manually import。</p>
<p><strong>Elastic Cloud Serverless（2024+）</strong>：新模型、按 <em>workload type</em>（search / observability / security）計費、不再按 cluster size — 減少 sizing 決策、autoscaling 由 Elastic 託管。屬新模型、production 採用 cadence 還在跟進中、適合 greenfield 部署或 PoC、existing cluster 遷移 roadmap 還在演進。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Alert volume 爆炸 / SOC 看不完</strong>：Sigma rule 全 enable 沒 tune、或 threshold rule 閾值太低 — staging space 跑 1 週統計 FP、tune threshold、加 exception list 排除已知合法 source、ML rule 補 user-specific baseline</li>
<li><strong>EQL sequence rule 跑不動 / timeout</strong>：sequence span 太長（24h）或 by field cardinality 太高、查詢成本爆炸 — 縮短 maxspan、限定 index pattern、加 pre-filter 條件</li>
<li><strong>Cluster 查詢慢 / Kibana 卡</strong>：hot tier 塞太多舊資料、沒做 hot-warm-cold tier 分層 — 開 ILM（Index Lifecycle Management）policy 自動 rollover、warm tier 用便宜 node、cold / frozen 走 Searchable Snapshots</li>
<li><strong>Fleet agent enrollment 失敗</strong>：Fleet server 跟 Elasticsearch 之間網路 / 憑證 / token 問題 — 檢查 Fleet server health、確認 enrollment token 未過期、agent log 看 specific 錯誤</li>
<li><strong>Sigma rule import 後大量 FP</strong>：Sigma rule 是 cross-SIEM 通用、沒有 environment-specific exclusion — 不要全 enable、staging tune 後再 promote、加 exception list（known scanner IP / 內部測試帳號）</li>
<li><strong>Resource-based pricing 超預算</strong>：node 過度 scale 或 hot tier 留太多 — 開 hot-warm-cold ILM、把 retention 超過 30 天的 index 推到 frozen tier on S3、Searchable Snapshots 是預設應該開</li>
<li><strong>ML job anomaly score 不準</strong>：training data 包含已 compromise 期間、baseline 被汙染 — 確認 training window 在乾淨期、定期重訓、配 detection rule 用 anomaly_score &gt; 75 而非 &gt; 50</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise + detection content 最豐富</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a></td>
      </tr>
      <tr>
          <td>Cloud-native + observability 已用 Datadog</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></td>
      </tr>
      <tr>
          <td>超大規模 ingestion + Google 雲</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>DLP / sensitive data discovery</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Endpoint detection 為主、不要全 stack</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint / SentinelOne</td>
      </tr>
      <tr>
          <td>CNAPP 為主（雲端 posture + workload）</td>
          <td>Wiz / Lacework / Prisma Cloud（Elastic Cloud Security 較新）</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>KQL / EQL / ES|QL 完整語法 reference、Lucene query DSL 進階用法</li>
<li>Elasticsearch index sharding / replica / ILM tuning 細節（屬 observability / 資料工程範圍）</li>
<li>Elastic Observability（APM / logs / metrics）— 屬 observability 不屬 security</li>
<li>Elastic Cloud Serverless 詳細 sizing 與 pricing 模型（2024+ 新模型、變動中）</li>
<li>Elastic Stack 自管的維運（cluster upgrade、Kibana plugin 開發）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Elastic Security 在 07 案例庫沒有直接 vendor-level 事件、但所有 detection-related case 都是 SIEM 偵測覆蓋率的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Elastic Security 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>Elastic EQL <code>sequence by user.name [auth fail count &gt; 50 in 5min] [auth success from new device]</code> 直接表達 MFA fatigue pattern、Sigma 社群有現成規則可 import 起步</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>跨租戶 token 異常驗證需 Elastic Cross-cluster search 跨 Azure AD log + GCP audit log + 自家 app log 同時 query、不需先搬資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>Elastic Defend 直接看到 desktop app process spawn + 異常網路 callback、不需外接 EDR feed；EQL <code>sequence</code> 抓 process → DNS → C2 行為鏈</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>Elastic rule 走 <code>detection-rules</code> repo（OSS、Elastic 官方維護）+ Sigma fork + staging space + promote 工程化 lifecycle、不是 Kibana UI 直改</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>Elastic 沒有 Splunk RBA 對應、用 ML anomaly rule + threshold rule severity + Case grouping 三層降噪、要設 ML job 重訓 lifecycle</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP signal 進 Elastic SIEM）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP log source）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（secret rotation API）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（WAF log + Sigma rule 對接）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Case → IR routing）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（Elastic Stack 共用 log pipeline）</li>
<li>官方：<a href="https://www.elastic.co/guide/en/security/current/index.html">Elastic Security Documentation</a>、<a href="https://github.com/elastic/detection-rules">detection-rules repo</a></li>
</ul>
]]></content:encoded></item><item><title>Apache JMeter</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/</guid><description>&lt;p>JMeter 的核心責任是把多 protocol 測試與既有企業測試資產轉成可重跑的負載驗證。它適合 GUI 驅動、plugin 生態成熟、HTTP 之外還需要 JDBC、JMS、FTP、mail 或 legacy protocol 的團隊，重點在把測試流程保留成可審查、可交接、可在 non-GUI mode 跑的 artifact。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>JMeter 是 Apache Software Foundation 的 OSS load testing tool、Java 寫、用 XML 描述 thread group / sampler / listener 組成的 test plan（&lt;code>.jmx&lt;/code> 檔）、支援 GUI 與 CLI（non-GUI / headless）雙模式。它是業界最老牌、protocol 覆蓋最廣的壓測工具 — sampler 直接覆蓋 HTTP、JDBC、JMS、SOAP、FTP、SMTP、IMAP、TCP、JUnit、OS process 等。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a> 比、JMeter 走 &lt;em>GUI-driven + protocol 廣&lt;/em>、k6 走 &lt;em>code-first（JavaScript）+ HTTP 為主&lt;/em>；JMeter 適合 QA 團隊維護、k6 適合 dev / SRE 寫進 CI。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust&lt;/a> 比、JMeter 用 XML + plugin、Locust 用純 Python class、custom client 彈性 Locust 強但 protocol 內建支援 JMeter 廣。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling&lt;/a> 比、JMeter 偏 GUI / 多 protocol、Gatling 偏 JVM DSL（Scala / Java / Kotlin）+ async runtime、單機 throughput Gatling 較高但 protocol 廣度與既有資產承接 JMeter 勝。&lt;/p>
&lt;p>關鍵張力：&lt;em>GUI / protocol 廣度&lt;/em> ↔ &lt;em>單機 throughput / CI 友善度&lt;/em> 是選 JMeter 的根本取捨。GUI 適合 QA 團隊與跨角色協作、&lt;code>.jmx&lt;/code> 又有 plugin 生態與十多年累積；代價是 XML diff 難 review、GUI listener 吃記憶體、CI 整合相比 k6 / Gatling 多一層 packaging。&lt;/p>
&lt;p>JMeter 適合測試資產已經存在的組織。當團隊有大量 &lt;code>.jmx&lt;/code> 測試計畫、QA 團隊用 GUI 維護 scenario、或壓測需要跨 HTTP、JDBC、JMS 與其他 plugin protocol，JMeter 的價值在於承接組織流程，而不只是產生 HTTP 負載。這個定位讓 JMeter 接到 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a>。它能支援 production-like test 的多系統 dependency，但 evidence package 要補上測試計畫版本、plugin 版本、runner 配置與結果保存方式。&lt;/p></description><content:encoded><![CDATA[<p>JMeter 的核心責任是把多 protocol 測試與既有企業測試資產轉成可重跑的負載驗證。它適合 GUI 驅動、plugin 生態成熟、HTTP 之外還需要 JDBC、JMS、FTP、mail 或 legacy protocol 的團隊，重點在把測試流程保留成可審查、可交接、可在 non-GUI mode 跑的 artifact。</p>
<h2 id="服務定位">服務定位</h2>
<p>JMeter 是 Apache Software Foundation 的 OSS load testing tool、Java 寫、用 XML 描述 thread group / sampler / listener 組成的 test plan（<code>.jmx</code> 檔）、支援 GUI 與 CLI（non-GUI / headless）雙模式。它是業界最老牌、protocol 覆蓋最廣的壓測工具 — sampler 直接覆蓋 HTTP、JDBC、JMS、SOAP、FTP、SMTP、IMAP、TCP、JUnit、OS process 等。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> 比、JMeter 走 <em>GUI-driven + protocol 廣</em>、k6 走 <em>code-first（JavaScript）+ HTTP 為主</em>；JMeter 適合 QA 團隊維護、k6 適合 dev / SRE 寫進 CI。跟 <a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a> 比、JMeter 用 XML + plugin、Locust 用純 Python class、custom client 彈性 Locust 強但 protocol 內建支援 JMeter 廣。跟 <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a> 比、JMeter 偏 GUI / 多 protocol、Gatling 偏 JVM DSL（Scala / Java / Kotlin）+ async runtime、單機 throughput Gatling 較高但 protocol 廣度與既有資產承接 JMeter 勝。</p>
<p>關鍵張力：<em>GUI / protocol 廣度</em> ↔ <em>單機 throughput / CI 友善度</em> 是選 JMeter 的根本取捨。GUI 適合 QA 團隊與跨角色協作、<code>.jmx</code> 又有 plugin 生態與十多年累積；代價是 XML diff 難 review、GUI listener 吃記憶體、CI 整合相比 k6 / Gatling 多一層 packaging。</p>
<p>JMeter 適合測試資產已經存在的組織。當團隊有大量 <code>.jmx</code> 測試計畫、QA 團隊用 GUI 維護 scenario、或壓測需要跨 HTTP、JDBC、JMS 與其他 plugin protocol，JMeter 的價值在於承接組織流程，而不只是產生 HTTP 負載。這個定位讓 JMeter 接到 <a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a> 與 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a>。它能支援 production-like test 的多系統 dependency，但 evidence package 要補上測試計畫版本、plugin 版本、runner 配置與結果保存方式。</p>
<h2 id="適用場景">適用場景</h2>
<p>多 protocol 壓測是 JMeter 的主要入口。企業服務常同時需要測 HTTP API、JDBC query、JMS queue、FTP 或 mail flow，JMeter 的 sampler 與 plugin 生態能讓同一份測試計畫覆蓋多種 dependency。</p>
<p>GUI 協作適合非純工程團隊。QA、測試中心或受監管環境常需要可視化測試設計、審核與交接，JMeter 的 GUI 能降低跨角色溝通成本。</p>
<p>Legacy 測試資產適合保留 JMeter。既有 <code>.jmx</code> 檔案、listener、plugin 與報表流程如果已經運作多年，重寫到 k6、Gatling 或 Locust 的機會成本要用維護收益抵銷。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 JMeter deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Thread group 設計</strong>：thread count / ramp-up / loop count / duration 是否反映真實流量模型、有沒有用 <em>Stepping Thread Group</em>（plugin）或 <em>Concurrency Thread Group</em> 控制 arrival rate、不是把 thread 當「user」直接綁</li>
<li><strong>Listener 配置</strong>：GUI listener（View Results Tree / Aggregate Report / Graph）只在 design / debug 階段開、正式跑必須改 <em>Simple Data Writer</em> 輸出 JTL、結果分析交給離線 HTML report 或外部 Grafana</li>
<li><strong>Distributed mode 設定</strong>：單機 thread 上限約 3000-5000（受 JVM heap 與 thread context switch 限制）、超過要走 <em>master + slave</em>（remote engine）；slave 機器 plugin / JMeter version / JVM 參數要跟 master 一致、否則結果不可信</li>
<li><strong>GUI vs CLI 模式區分</strong>：GUI 是 design / debug only、production load 一律走 <code>jmeter -n -t plan.jmx -l result.jtl</code>；GUI 跑大規模測試會把 listener 拉爆記憶體、結果反而失真</li>
</ul>
<p>四件事任一缺、就是 <a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a> 邊界的待補項目。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>JMeter 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 protocol</td>
          <td>sampler 與 plugin 覆蓋廣</td>
          <td>plugin 版本治理與測試環境一致性</td>
      </tr>
      <tr>
          <td>GUI 協作</td>
          <td>非工程角色可讀可改</td>
          <td>code review、diff 與版本控制紀律</td>
      </tr>
      <tr>
          <td>既有資產</td>
          <td><code>.jmx</code>、listener、報表可延續</td>
          <td>scenario cleanup 與 artifact 標準化</td>
      </tr>
      <tr>
          <td>分散式執行</td>
          <td>remote engine 可擴負載</td>
          <td>runner sizing、網路瓶頸與結果合併</td>
      </tr>
  </tbody>
</table>
<p>多 protocol 價值來自 dependency coverage。當 workload model 包含 database、queue、file transfer 或 legacy endpoint，JMeter 可以把不同 dependency 的壓力放在同一個測試計畫中觀察。</p>
<p>GUI 協作價值來自跨角色可見性。這個優點會帶來版本控制成本，因為 XML diff 不容易 review；團隊要補上 naming、folder structure、parameterization 與 review checklist。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>JMeter 和 k6 的主要差異是 workflow。JMeter 偏 GUI、plugin 與既有企業流程；k6 偏 code-first、CLI、threshold 與 CI artifact。</p>
<p>JMeter 和 Gatling 的主要差異是 scenario 表達。JMeter 用 test plan、thread group、sampler 與 listener 組裝；Gatling 用 JVM DSL 描述 simulation，較適合工程團隊維護複雜 flow。</p>
<p>JMeter 和 Locust 的主要差異是自訂能力。JMeter 依賴 plugin 與 sampler，Locust 可以直接用 Python library 實作 custom client；如果 protocol 特別特殊，Python 團隊可能更適合 Locust。</p>
<p>JMeter 和 Vegeta 的主要差異是複雜度。Vegeta 適合快速 HTTP saturation probe；JMeter 適合多步驟、多 dependency 與可交接測試計畫。</p>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>JMeter</th>
          <th>k6</th>
          <th>Locust</th>
          <th>Gatling</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>描述語言</td>
          <td>XML（<code>.jmx</code>）+ GUI</td>
          <td>JavaScript</td>
          <td>Python（class-based）</td>
          <td>Scala / Java / Kotlin DSL</td>
      </tr>
      <tr>
          <td>Protocol 覆蓋</td>
          <td>HTTP/JDBC/JMS/SOAP/FTP/SMTP/TCP</td>
          <td>HTTP/WebSocket/gRPC</td>
          <td>HTTP + 任何 Python lib custom</td>
          <td>HTTP/JMS/MQTT</td>
      </tr>
      <tr>
          <td>單機 throughput</td>
          <td>中（thread-per-user）</td>
          <td>高（Go goroutine）</td>
          <td>中（gevent / async）</td>
          <td>高（Akka async）</td>
      </tr>
      <tr>
          <td>Runtime model</td>
          <td>JVM thread</td>
          <td>Go runtime</td>
          <td>Python gevent</td>
          <td>JVM async actor</td>
      </tr>
      <tr>
          <td>CI 友善度</td>
          <td>需 packaging <code>.jmx</code> + plugin</td>
          <td>強 — 單一 JS file + CLI</td>
          <td>強 — pip + Python file</td>
          <td>強 — sbt / Maven + Scala file</td>
      </tr>
      <tr>
          <td>GUI</td>
          <td>完整 GUI（design / debug）</td>
          <td>無（CLI only）</td>
          <td>Web UI（runtime monitoring）</td>
          <td>無（HTML report only）</td>
      </tr>
      <tr>
          <td>Distributed</td>
          <td>Master + Slave（remote engine）</td>
          <td>k6 Cloud / Operator</td>
          <td>Master + Worker</td>
          <td>Gatling Enterprise / FrontLine</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Enterprise QA + 多 protocol</td>
          <td>Dev / SRE + HTTP-heavy + CI</td>
          <td>Python 團隊 + custom protocol</td>
          <td>JVM 團隊 + 複雜 scenario</td>
      </tr>
  </tbody>
</table>
<h2 id="操作成本">操作成本</h2>
<p>JMeter 的主要成本是測試計畫治理。<code>.jmx</code> 檔案可以累積大量 listener、debug sampler、hard-coded variable 與過期 assertion，長期不整理會讓壓測結果失去可追溯性。</p>
<p>Runner 成本來自 JVM 與 listener。GUI listener 適合開發階段觀察，不適合大規模壓測；正式測試要使用 non-GUI mode，把結果輸出成 JTL、HTML report 或外部 metrics。</p>
<p>Plugin 成本來自版本漂移。不同 runner、不同工程師機器或 CI image 的 plugin 版本如果不一致，同一份測試計畫可能產生不同結果，因此要把 plugin 清單、JMeter 版本與 container image 固定下來。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>JMeter 結果應回寫到 evidence package。最小欄位包括 test plan version、JMeter version、plugin list、runner topology、thread group 設定、ramp-up、duration、p95 / p99、error rate、throughput、target saturation metric 與 known gap。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>JMeter 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td><code>.jmx</code>、JTL、HTML report、dashboard link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>test start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / Prometheus / DB / queue 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>test plan version、plugin version</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>runner topology、production similarity</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 protocol、資料偏差、listener overhead</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓結果可審查。JMeter 測試計畫常由多人維護，gate decision 要能追到哪一版 <code>.jmx</code>、哪一組 runner、哪一批測試資料與哪一個目標環境。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>JMeter Plugins 生態</strong>：<a href="https://jmeter-plugins.org/">jmeter-plugins.org</a> 社群維護的 plugin 集合補齊原版 JMeter 的不足 — <em>Custom Thread Groups</em>（Stepping / Ultimate / Concurrency / Arrivals）讓 thread schedule 反映真實 arrival rate、<em>PerfMon</em> 抓 remote server CPU / memory、<em>Throughput Shaping Timer</em> 直接以 RPS 為目標而非 thread count、<em>Dummy Sampler</em> 拿來 mock dependency。Plugin Manager 統一安裝、CI image 要把 plugin 清單固定（<code>PluginsManagerCMD.sh install &lt;plugins&gt;</code>）避免漂移。</p>
<p><strong>BlazeMeter Cloud / Distributed execution</strong>：自建 distributed mode（master + slave 跨多 VM）成本高 — slave 機器要同 JMeter 版本、同 plugin、同 JVM 參數、RMI port 開通、結果回傳網路足夠。<a href="https://www.blazemeter.com/">BlazeMeter</a>（Perforce / 前 CA）是 JMeter SaaS、直接吃 <code>.jmx</code> 跑 cloud-scale 壓測、附 geo-distributed runner、適合短期 spike 測試不想自建 distributed cluster 的團隊。trade-off 是 vendor lock-in 跟 per-test 計費 — 長期高頻測試自建較划算。</p>
<p><strong>Distributed mode 細節</strong>：master 機器發 control plane（thread group 配置、test plan 分發）、slave 跑 thread 並回傳 sample 結果。瓶頸常出在 <em>master 收結果</em>（RMI / 自訂 protocol），不是 slave 跑不動 — 大規模測試應該關掉 GUI listener、用 <em>Backend Listener</em> 把 metric 即時推到外部時序資料庫、master 只收彙整指標而非每個 sample。同步要點：所有 slave 用同一份 <code>.jmx</code> 與 test data CSV，CSV 不能依賴 master local path。</p>
<p><strong>Backend Listener + Grafana 整合</strong>：JMeter 原生 <em>Backend Listener</em> 支援 InfluxDB / Graphite / Elasticsearch、把 active thread / response time / hit / error 即時推出去、Grafana 配 <a href="https://grafana.com/grafana/dashboards/5496-apache-jmeter-dashboard/">official JMeter dashboard</a> 即時看 throughput / latency curve。這個組合取代 GUI listener、是 distributed mode 的標準觀測方式 — listener overhead 從 master 移到外部時序系統、master 不再被 GUI 拉爆。配合 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a> 的時序資料庫已有時、JMeter metric 進同一個 Grafana、跟 application 端的 latency / error 並列、加速 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a> 的對照判讀。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>GUI 模式吃記憶體爆 / OOM</strong>：GUI listener（View Results Tree / Graph）會把所有 sample 留在 heap、跑大規模就 OutOfMemoryError — 設計階段才開 GUI、正式跑切 <code>jmeter -n</code> non-GUI、listener 用 Simple Data Writer 寫 JTL 而非 in-memory aggregate</li>
<li><strong>Listener 拖累 throughput / 結果失真</strong>：太多 listener 同時開、每個 sample 都被多個 listener 處理、JMeter 自身成為瓶頸 — 正式測試只留 Simple Data Writer + Backend Listener、結果分析離線跑 <code>jmeter -g result.jtl -o report/</code> 產 HTML</li>
<li><strong>Thread group 計算錯 / 真實流量對不上</strong>：把 thread 當「user」直接設、忽略 think time + ramp-up、結果壓出來的是 thread 全速跑而非業務流量 — 改用 Concurrency Thread Group 或 Throughput Shaping Timer 直接以 RPS 為目標、配 Constant Timer 模擬 think time</li>
<li><strong>Distributed mode 結果跟單機對不上</strong>：slave 機器 plugin / JMeter version / JVM heap 不一致、或 CSV 路徑只存在 master — 把 slave 環境 container 化（同 Docker image）、CSV 隨 <code>.jmx</code> 一起分發、<code>--remote-start</code> 統一啟動</li>
<li><strong><code>.jmx</code> XML diff 不可 review / merge conflict 多</strong>：多人同時改測試計畫、GUI 改完 XML 結構大變 — 拆 fragment（Test Fragment + Module Controller）、scenario 分檔、parameterization 走外部 CSV / properties、PR review 看截圖 + 跑結果而非 raw XML diff</li>
<li><strong>Plugin 版本漂移 / CI 結果不可重現</strong>：dev 機器 plugin 跟 CI image 不同版 — 固定 plugin manifest、CI image 用 <code>PluginsManagerCMD.sh install-for-jmx plan.jmx</code> 從 plan 自動安裝、版本鎖到 image tag</li>
<li><strong>HTTPS / TLS 連線數爆炸</strong>：JMeter 預設每 thread 一個 TLS handshake、large thread count 把 server TLS 拖垮、結果反而測到 TLS 不是 app — 開 <em>HTTP Cache Manager</em> 跟 <em>KeepAlive</em>、必要時調 <code>httpclient4.idletimeout</code></li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>JMeter 在 09 案例庫中適合作為 enterprise load test 承接點。它可回寫到 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 售票壓測</a> 的 pre-event validation、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow ticketing</a> 的售票流量模型、<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day readiness</a> 的 staged validation、<a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL 1860 萬同時觀看</a> 的全球直播 pre-event rehearsal、以及 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 跨 7 個受監管市場的 Aurora 4000 TPS 容量驗證。</p>
<p>這些案例提供的是複雜業務流程與活動前驗證節奏。JMeter 頁引用案例時，要把 case 轉成 thread group、ramp-up、data set、dependency sampler 與 result artifact，並讓負載數字回到業務流程判讀 — 例如 Hotstar 的「集中地理區 CDN 壓力」要在 JMeter 用 per-region thread group 模擬、不是把全球流量塞進單一 runner。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>、<a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>、<a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>官方：<a href="https://jmeter.apache.org/usermanual/index.html">Apache JMeter documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.2 Schema Design 與資料建模</title><link>https://tarrragon.github.io/blog/backend/01-database/schema-design/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/schema-design/</guid><description>&lt;p>資料綱要設計（schema design）的核心責任是把業務狀態轉成可維護、可查詢、可演進的資料結構。資料建模做得好、交易邊界、查詢效率、migration 成本與事故修復路徑都會更穩定。&lt;/p>
&lt;p>本章是 01 模組的基礎章節之一、結合 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary&lt;/a>（交易範圍）、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence&lt;/a>（演進證據）與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃&lt;/a>（partition key 設計）一起讀。讀完後能回答：table 怎麼切、index 怎麼選、什麼時候 denormalize、partition 怎麼設、命名怎麼治理。&lt;/p>
&lt;h2 id="先定義狀態責任">先定義狀態責任&lt;/h2>
&lt;p>資料模型第一步是定義狀態責任：哪些欄位代表正式狀態、哪些欄位是派生值、哪些欄位只為追蹤與審計。這個分層會直接決定 table 邊界與 relation 方向。&lt;/p>
&lt;p>在訂單服務中、訂單主檔、付款狀態、庫存扣減屬於正式狀態；展示排序欄位、快取摘要屬於派生值；版本號、更新時間與來源欄位屬於可追蹤證據。把三類混在同一模型裡、後續查詢與演進成本會持續上升。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary&lt;/a>。&lt;/p>
&lt;h2 id="table-與-relation">Table 與 Relation&lt;/h2>
&lt;p>table 切分要對齊業務聚合邊界。聚合內需要交易一致性的欄位、放在同一交易可控範圍；跨聚合流程透過事件或引用關係接續。relation 的責任是表達資料約束、不是替代流程編排。&lt;/p>
&lt;p>主鍵策略要先回答「如何穩定識別」與「如何支援查詢」。自然鍵可讀性高但變動風險高；代理鍵穩定且易擴展、常搭配業務唯一鍵一起使用。外鍵策略則要平衡完整性與演進自由度：正式核心域可強約束、跨域整合可由應用層保護並保留遷移彈性。&lt;/p>
&lt;p>&lt;strong>主鍵選擇實務&lt;/strong>：&lt;/p>
&lt;p>ID 設計不只是「選個格式」，而是在五個維度做取捨。先理解取捨、再按場景選型。&lt;/p>
&lt;h3 id="id-設計的五個取捨維度">ID 設計的五個取捨維度&lt;/h3>
&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>&lt;strong>唯一性&lt;/strong>&lt;/td>
 &lt;td>跨機器、跨時間不碰撞&lt;/td>
 &lt;td>分散式系統的核心需求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>有序性&lt;/strong>&lt;/td>
 &lt;td>是否可按生成順序排序&lt;/td>
 &lt;td>B-tree 插入效能、時間軸查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>隱私性&lt;/strong>&lt;/td>
 &lt;td>是否洩漏業務資訊（量級、時間、機器）&lt;/td>
 &lt;td>外部可見的 ID 不應洩漏用戶數量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>儲存成本&lt;/strong>&lt;/td>
 &lt;td>佔多少 byte、index 體積&lt;/td>
 &lt;td>高 TPS 場景每 byte 都乘以百萬筆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>產生效能&lt;/strong>&lt;/td>
 &lt;td>需要鎖？需要 crypto/rand？需要 network call？&lt;/td>
 &lt;td>熱路徑上的 ID 產生 ns 級差異有影響&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="id-類型選型矩陣">ID 類型選型矩陣&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>ID 類型&lt;/th>
 &lt;th>大小&lt;/th>
 &lt;th>唯一性&lt;/th>
 &lt;th>有序性&lt;/th>
 &lt;th>隱私性&lt;/th>
 &lt;th>產生效能&lt;/th>
 &lt;th>適合場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Bigint sequence&lt;/strong>&lt;/td>
 &lt;td>8 byte&lt;/td>
 &lt;td>單機唯一&lt;/td>
 &lt;td>嚴格有序&lt;/td>
 &lt;td>低（可猜量級）&lt;/td>
 &lt;td>最快（DB 自增）&lt;/td>
 &lt;td>單機、內部 ID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>UUID v4&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>無序&lt;/td>
 &lt;td>高（不可預測）&lt;/td>
 &lt;td>中（crypto/rand）&lt;/td>
 &lt;td>外部可見 ID、隱私敏感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>UUID v7&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>中（時間可推）&lt;/td>
 &lt;td>中（timestamp + crypto/rand）&lt;/td>
 &lt;td>內部 ID、事件追蹤、DB 主鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>ULID&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>類 UUID v7（先於 v7 標準化）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Snowflake&lt;/strong>&lt;/td>
 &lt;td>8 byte&lt;/td>
 &lt;td>需要 machine_id 協調&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>低（含 machine_id）&lt;/td>
 &lt;td>快（無 crypto）&lt;/td>
 &lt;td>高 TPS + 分散式 + 空間敏感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>NanoID&lt;/strong>&lt;/td>
 &lt;td>可變（預設 21 字元）&lt;/td>
 &lt;td>依長度&lt;/td>
 &lt;td>無序&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>快（PRNG 即可）&lt;/td>
 &lt;td>URL-safe 短 ID（用於外部可見的短連結、邀請碼）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="選型決策流程">選型決策流程&lt;/h3>





&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">需要跨機器唯一？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> └─ 否 → Bigint sequence（最簡單、效能最好）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └─ 是 → ID 對外部可見？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> └─ 是 → 隱私敏感？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> └─ 是 → UUID v4（不可預測）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> └─ 否 → UUID v7（有序、DB 友好）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> └─ 否 → 空間敏感（8 byte vs 16 byte）？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> └─ 是 → Snowflake（需要 machine_id 協調）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl"> └─ 否 → UUID v7（簡單、標準）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="有序-id-的-db-效能影響">有序 ID 的 DB 效能影響&lt;/h3>
&lt;p>B-tree 索引的插入效能和 key 的分布有直接關係。UUID v4 的隨機分布導致每次插入都可能落在 B-tree 的不同 leaf page，造成大量隨機 I/O（page split、cache miss）。UUID v7 的時間戳前綴讓插入集中在 B-tree 的尾端，接近 sequential insert。&lt;/p></description><content:encoded><![CDATA[<p>資料綱要設計（schema design）的核心責任是把業務狀態轉成可維護、可查詢、可演進的資料結構。資料建模做得好、交易邊界、查詢效率、migration 成本與事故修復路徑都會更穩定。</p>
<p>本章是 01 模組的基礎章節之一、結合 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>（交易範圍）、<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a>（演進證據）與 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a>（partition key 設計）一起讀。讀完後能回答：table 怎麼切、index 怎麼選、什麼時候 denormalize、partition 怎麼設、命名怎麼治理。</p>
<h2 id="先定義狀態責任">先定義狀態責任</h2>
<p>資料模型第一步是定義狀態責任：哪些欄位代表正式狀態、哪些欄位是派生值、哪些欄位只為追蹤與審計。這個分層會直接決定 table 邊界與 relation 方向。</p>
<p>在訂單服務中、訂單主檔、付款狀態、庫存扣減屬於正式狀態；展示排序欄位、快取摘要屬於派生值；版本號、更新時間與來源欄位屬於可追蹤證據。把三類混在同一模型裡、後續查詢與演進成本會持續上升。</p>
<p>詳見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary</a>。</p>
<h2 id="table-與-relation">Table 與 Relation</h2>
<p>table 切分要對齊業務聚合邊界。聚合內需要交易一致性的欄位、放在同一交易可控範圍；跨聚合流程透過事件或引用關係接續。relation 的責任是表達資料約束、不是替代流程編排。</p>
<p>主鍵策略要先回答「如何穩定識別」與「如何支援查詢」。自然鍵可讀性高但變動風險高；代理鍵穩定且易擴展、常搭配業務唯一鍵一起使用。外鍵策略則要平衡完整性與演進自由度：正式核心域可強約束、跨域整合可由應用層保護並保留遷移彈性。</p>
<p><strong>主鍵選擇實務</strong>：</p>
<p>ID 設計不只是「選個格式」，而是在五個維度做取捨。先理解取捨、再按場景選型。</p>
<h3 id="id-設計的五個取捨維度">ID 設計的五個取捨維度</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>說明</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>唯一性</strong></td>
          <td>跨機器、跨時間不碰撞</td>
          <td>分散式系統的核心需求</td>
      </tr>
      <tr>
          <td><strong>有序性</strong></td>
          <td>是否可按生成順序排序</td>
          <td>B-tree 插入效能、時間軸查詢</td>
      </tr>
      <tr>
          <td><strong>隱私性</strong></td>
          <td>是否洩漏業務資訊（量級、時間、機器）</td>
          <td>外部可見的 ID 不應洩漏用戶數量</td>
      </tr>
      <tr>
          <td><strong>儲存成本</strong></td>
          <td>佔多少 byte、index 體積</td>
          <td>高 TPS 場景每 byte 都乘以百萬筆</td>
      </tr>
      <tr>
          <td><strong>產生效能</strong></td>
          <td>需要鎖？需要 crypto/rand？需要 network call？</td>
          <td>熱路徑上的 ID 產生 ns 級差異有影響</td>
      </tr>
  </tbody>
</table>
<h3 id="id-類型選型矩陣">ID 類型選型矩陣</h3>
<table>
  <thead>
      <tr>
          <th>ID 類型</th>
          <th>大小</th>
          <th>唯一性</th>
          <th>有序性</th>
          <th>隱私性</th>
          <th>產生效能</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Bigint sequence</strong></td>
          <td>8 byte</td>
          <td>單機唯一</td>
          <td>嚴格有序</td>
          <td>低（可猜量級）</td>
          <td>最快（DB 自增）</td>
          <td>單機、內部 ID</td>
      </tr>
      <tr>
          <td><strong>UUID v4</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>無序</td>
          <td>高（不可預測）</td>
          <td>中（crypto/rand）</td>
          <td>外部可見 ID、隱私敏感</td>
      </tr>
      <tr>
          <td><strong>UUID v7</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>時間有序</td>
          <td>中（時間可推）</td>
          <td>中（timestamp + crypto/rand）</td>
          <td>內部 ID、事件追蹤、DB 主鍵</td>
      </tr>
      <tr>
          <td><strong>ULID</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>時間有序</td>
          <td>中</td>
          <td>中</td>
          <td>類 UUID v7（先於 v7 標準化）</td>
      </tr>
      <tr>
          <td><strong>Snowflake</strong></td>
          <td>8 byte</td>
          <td>需要 machine_id 協調</td>
          <td>時間有序</td>
          <td>低（含 machine_id）</td>
          <td>快（無 crypto）</td>
          <td>高 TPS + 分散式 + 空間敏感</td>
      </tr>
      <tr>
          <td><strong>NanoID</strong></td>
          <td>可變（預設 21 字元）</td>
          <td>依長度</td>
          <td>無序</td>
          <td>高</td>
          <td>快（PRNG 即可）</td>
          <td>URL-safe 短 ID（用於外部可見的短連結、邀請碼）</td>
      </tr>
  </tbody>
</table>
<h3 id="選型決策流程">選型決策流程</h3>





<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">需要跨機器唯一？
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └─ 否 → Bigint sequence（最簡單、效能最好）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  └─ 是 → ID 對外部可見？
</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">                    └─ 是 → UUID v4（不可預測）
</span></span><span class="line"><span class="ln">6</span><span class="cl">                    └─ 否 → UUID v7（有序、DB 友好）
</span></span><span class="line"><span class="ln">7</span><span class="cl">           └─ 否 → 空間敏感（8 byte vs 16 byte）？
</span></span><span class="line"><span class="ln">8</span><span class="cl">                    └─ 是 → Snowflake（需要 machine_id 協調）
</span></span><span class="line"><span class="ln">9</span><span class="cl">                    └─ 否 → UUID v7（簡單、標準）</span></span></code></pre></div><h3 id="有序-id-的-db-效能影響">有序 ID 的 DB 效能影響</h3>
<p>B-tree 索引的插入效能和 key 的分布有直接關係。UUID v4 的隨機分布導致每次插入都可能落在 B-tree 的不同 leaf page，造成大量隨機 I/O（page split、cache miss）。UUID v7 的時間戳前綴讓插入集中在 B-tree 的尾端，接近 sequential insert。</p>
<table>
  <thead>
      <tr>
          <th>測試場景（PostgreSQL、1000 萬筆）</th>
          <th>UUID v4</th>
          <th>UUID v7</th>
          <th>Bigint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>INSERT 吞吐</td>
          <td>~5,000/sec</td>
          <td>~15,000/sec</td>
          <td>~20,000/sec</td>
      </tr>
      <tr>
          <td>Index 大小</td>
          <td>~400 MB</td>
          <td>~350 MB</td>
          <td>~200 MB</td>
      </tr>
      <tr>
          <td>範圍查詢延遲</td>
          <td>要額外建 timestamp index</td>
          <td>UUID 本身有序</td>
          <td>天然有序</td>
      </tr>
  </tbody>
</table>
<p>上表數字是基於 NVMe SSD 環境的量級估算（源自 UUID v4 的 random page split 成本約為 sequential 的 1/3-1/4 這個 B-tree 特性推導），實際效能依硬體和 workload 而定。核心結論：UUID v7 的插入效能約為 v4 的 3 倍，接近 bigint sequential。</p>
<h3 id="隱私考量v4-vs-v7">隱私考量：v4 vs v7</h3>
<p>UUID v7 的前 48 bit 是 Unix 時間戳（毫秒精度）。攻擊者拿到 UUID v7 可以推算「這個 ID 在幾點幾分產生」。這在不同場景有不同風險：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>v7 洩漏的資訊</th>
          <th>風險等級</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部事件追蹤 ID</td>
          <td>事件產生時間</td>
          <td>無風險（log 本身有 timestamp）</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>DB 主鍵（內部）</td>
          <td>資料建立時間</td>
          <td>低風險</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>Session ID（自用工具）</td>
          <td>Session 開始時間</td>
          <td>低風險</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>Session ID（商業產品、有外部使用者）</td>
          <td>使用者活動時間</td>
          <td>中風險（可交叉比對身份）</td>
          <td>v4</td>
      </tr>
      <tr>
          <td>API key / token</td>
          <td>簽發時間</td>
          <td>高風險（可推斷 key 輪換週期）</td>
          <td>v4 或加密</td>
      </tr>
      <tr>
          <td>訂單 ID（外部可見）</td>
          <td>下單時間 + 量級趨勢</td>
          <td>中風險</td>
          <td>v4 或 NanoID</td>
      </tr>
  </tbody>
</table>
<p>經驗法則：<strong>對外暴露給不可信第三方的 ID 用 v4（不可預測），內部 ID 用 v7（有序、效能好）。</strong></p>
<h3 id="各語言的標準庫支援">各語言的標準庫支援</h3>
<table>
  <thead>
      <tr>
          <th>語言</th>
          <th>UUID v4</th>
          <th>UUID v7</th>
          <th>套件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Python 3.14+</td>
          <td><code>uuid.uuid4()</code></td>
          <td><code>uuid.uuid7()</code></td>
          <td>標準庫</td>
      </tr>
      <tr>
          <td>Python &lt; 3.14</td>
          <td><code>uuid.uuid4()</code></td>
          <td><code>uuid_utils.uuid7()</code></td>
          <td>第三方</td>
      </tr>
      <tr>
          <td>Go</td>
          <td><code>google/uuid</code> v4</td>
          <td><code>google/uuid</code> v7（1.6+）</td>
          <td>事實標準</td>
      </tr>
      <tr>
          <td>TypeScript</td>
          <td><code>crypto.randomUUID()</code></td>
          <td>標準庫無（<code>uuidv7</code> npm）</td>
          <td>第三方</td>
      </tr>
      <tr>
          <td>Dart</td>
          <td><code>uuid</code> package</td>
          <td><code>uuid</code> package v4+（支援 v7）</td>
          <td>pub.dev</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td><code>gen_random_uuid()</code></td>
          <td><code>uuidv7()</code>（pg_uuidv7 extension）</td>
          <td>擴展</td>
      </tr>
  </tbody>
</table>
<p>Go 的 <code>google/uuid</code> v1.6+ 內建 <code>uuid.NewV7()</code>，效能約 350ns/op（含 crypto/rand），和 JSON 解析（5-10μs）、DB 寫入（200μs）相比不是瓶頸。</p>
<p>對應 KV 案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads partition key</a>、<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft composite key</a> 都是主鍵策略的延伸。</p>
<h2 id="index-設計">Index 設計</h2>
<p>index 設計要從查詢路徑反推、不是從欄位列表前推。每個高頻查詢至少要回答三件事：過濾條件是什麼、排序規則是什麼、回傳範圍有多大。這三件事能否由索引覆蓋、決定了 latency 與成本。</p>
<p><strong>Index 類型對照</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Index 類型</th>
          <th>適用 query</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B-tree（預設）</td>
          <td><code>WHERE col = ?</code> / <code>WHERE col &gt; ?</code> / <code>ORDER BY col</code></td>
          <td>多數查詢</td>
      </tr>
      <tr>
          <td>Hash</td>
          <td><code>WHERE col = ?</code>（不支援 range）</td>
          <td>PostgreSQL 限定、少用</td>
      </tr>
      <tr>
          <td>GIN</td>
          <td>JSONB / array / full-text search</td>
          <td><code>WHERE jsonb_data @&gt; ?</code></td>
      </tr>
      <tr>
          <td>GiST</td>
          <td>範圍 / 地理 / 自訂型別</td>
          <td>PostGIS、range type</td>
      </tr>
      <tr>
          <td>BRIN</td>
          <td>大表時序資料、欄位跟物理順序相關</td>
          <td>log table by timestamp</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td><code>WHERE</code> 條件下才建 index</td>
          <td><code>WHERE status = 'pending'</code></td>
      </tr>
      <tr>
          <td>Covering index</td>
          <td>包含所有查詢欄位、避免 heap lookup</td>
          <td><code>INDEX (a) INCLUDE (b, c)</code></td>
      </tr>
      <tr>
          <td>Compound index</td>
          <td>多欄位、順序敏感</td>
          <td><code>INDEX (a, b)</code> 對 <code>WHERE a=? AND b=?</code></td>
      </tr>
  </tbody>
</table>
<p><strong>常見設計原則</strong>：</p>
<ol>
<li>先保護交易關鍵查詢、再處理報表與後台查詢</li>
<li>複合索引依查詢過濾與排序順序排列、避免僅憑欄位熱門度排列</li>
<li>大表變更前先評估索引建立成本與回退方案、避免在高峰時段同步放大風險</li>
<li>定期 review 未用 index（PostgreSQL <code>pg_stat_user_indexes</code>、MySQL <code>sys.schema_unused_indexes</code>）— 寫入吞吐被舊 index 拖垮</li>
<li>partial index 對 <code>boolean</code> / <code>status</code> column 特別有用 — 只 index 「pending」「failed」等小集合</li>
</ol>
<p><strong>Index 反模式</strong>：</p>
<ul>
<li>每個欄位都建 index：寫入吞吐被拖垮</li>
<li>不看 EXPLAIN 就建 index：可能跟 query planner 不對齊</li>
<li>用 OR 條件依賴單一 index：query planner 不一定能用</li>
<li>大表 ALTER INDEX 不分批：lock 整個表</li>
</ul>
<h2 id="denormalization-模式">Denormalization 模式</h2>
<p>normalize 是 SQL 的預設、但 denormalize 有時是更好的工程選擇。</p>
<p><strong>Precomputed aggregate</strong>：</p>
<ul>
<li>把 COUNT / SUM 結果存在 parent row 而非每次 query 算</li>
<li>例：<code>posts.comment_count</code> 存實際值、不每次 SELECT COUNT</li>
<li>風險：consistency（comment 寫入後 count 沒更新）</li>
<li>對策：用 trigger 或應用層 transaction 確保同步、或定期 reconcile</li>
</ul>
<p><strong>Embedded one-to-many</strong>：</p>
<ul>
<li>小量 1-many 關係可以 embed 成 JSONB / nested column</li>
<li>例：<code>order.line_items</code> JSON column、不另建 line_items table</li>
<li>風險：個別 line item 查詢不便</li>
<li>適合：line items 通常一起讀寫（同 transaction boundary）</li>
</ul>
<p><strong>Materialized view</strong>：</p>
<ul>
<li>預計算 query 結果、定期 refresh</li>
<li>適合：複雜 JOIN / aggregation 重複跑</li>
<li>風險：refresh window 內看到舊資料</li>
</ul>
<p><strong>Read model</strong>（CQRS）：</p>
<ul>
<li>寫入路徑跟讀取路徑用不同 schema</li>
<li>寫入 normalize、讀取 denormalize 成不同 read model</li>
<li>詳見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a></li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+ watch list</a> — denormalize 用戶 metadata、跨裝置查詢方便</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — DynamoDB single-table design 是極端 denormalization</li>
</ul>
<h2 id="partition-策略">Partition 策略</h2>
<p>單表 &gt; 1 TB 時、partition 是必要的維運手段。partition 不是「擴 storage」、是「讓 vacuum / index / DROP 可分批跑」。</p>
<p><strong>Partition 類型</strong>：</p>
<ul>
<li><strong>Range partition</strong>：按 timestamp / id 範圍切。<code>orders_2024_q1</code>, <code>orders_2024_q2</code>&hellip;</li>
<li><strong>List partition</strong>：按枚舉值切。<code>orders_us</code>, <code>orders_eu</code>&hellip;</li>
<li><strong>Hash partition</strong>：按 hash 均勻切。適合無自然切分維度的大表</li>
</ul>
<p><strong>Partition 設計要點</strong>：</p>
<ol>
<li>partition key 必須出現在 <em>多數 query 的 WHERE clause</em>（partition pruning 才能生效）</li>
<li>partition 數量 <em>適中</em>（10-100）— 太少 partition 太大、太多 partition metadata 開銷大</li>
<li>老 partition 可以 DROP 或 archive、儲存成本可控</li>
<li><code>cross-partition unique constraint</code> 限制 — 唯一鍵必須含 partition key</li>
</ol>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 個獨立 Aurora cluster 是極端 partition by business</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — DynamoDB 透明 partition、應用層不必管</li>
</ul>
<h2 id="schema-evolution-友好設計">Schema Evolution 友好設計</h2>
<p>schema 從 day 1 就要為演進設計、不能假設「以後不會改」。</p>
<p><strong>避免 breaking changes</strong>：</p>
<ul>
<li><strong>加欄位</strong>：safe（nullable 或 default）</li>
<li><strong>刪欄位</strong>：unsafe（先讓所有 code 不再讀 → 部署 → 再刪）</li>
<li><strong>改欄位類型</strong>：unsafe（先加新欄位、雙寫、backfill、移除舊欄位）</li>
<li><strong>改欄位名</strong>：unsafe（同上）</li>
<li><strong>加 NOT NULL constraint</strong>：unsafe（先 backfill default、再加 constraint）</li>
</ul>
<p><strong>Evolution-friendly schema 原則</strong>：</p>
<ol>
<li><strong>欄位 nullable by default</strong>：除非業務不允許 null、否則先 nullable、之後再 tighten</li>
<li><strong>避免大表 ALTER TABLE</strong>：用 <a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> 模式</li>
<li><strong>predict breaking changes</strong>：訂版本、跟 application code 同步演進</li>
<li><strong>schema version column</strong>：每 row 帶 version、應用層按版本處理</li>
<li><strong>migration 工具版本控</strong>：Flyway / Liquibase / Atlas / golang-migrate 必須有</li>
</ol>
<p>詳見 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h2 id="naming-與一致性">Naming 與一致性</h2>
<p>命名規則的責任是維持跨版本可讀性。table、column、index 的命名若沒有一致語意、migration 與故障排查會持續變慢。穩定做法是把命名和業務語意對齊、並保留可辨識版本與作用域。</p>
<p><strong>Naming 慣例</strong>：</p>
<ul>
<li><strong>Table</strong>：複數名詞、<code>snake_case</code>（<code>orders</code>, <code>payment_methods</code>）</li>
<li><strong>Column</strong>：<code>snake_case</code>、明確語意（<code>created_at</code> 不是 <code>ts</code>）</li>
<li><strong>Foreign key</strong>：<code>{referenced_table}_id</code>（<code>user_id</code> 指 <code>users.id</code>）</li>
<li><strong>Boolean</strong>：<code>is_*</code> / <code>has_*</code> / <code>can_*</code>（<code>is_active</code>, <code>has_subscription</code>）</li>
<li><strong>Timestamp</strong>：<code>*_at</code> for events（<code>created_at</code>, <code>paid_at</code>）、<code>*_on</code> for dates（<code>born_on</code>）</li>
<li><strong>Index</strong>：<code>idx_{table}_{cols}</code>（<code>idx_orders_user_id_created_at</code>）</li>
<li><strong>Unique constraint</strong>：<code>uq_{table}_{cols}</code></li>
<li><strong>Foreign key constraint</strong>：<code>fk_{table}_{ref}</code></li>
</ul>
<p><strong>避免的反模式</strong>：</p>
<ul>
<li>縮寫不一致（<code>u_id</code> vs <code>user_id</code>）</li>
<li>隱性意義（<code>status</code> 是 enum、值在哪裡？）</li>
<li>跨表同義不同名（<code>user.name</code> vs <code>customer.full_name</code>）</li>
<li>反向命名（<code>name_first</code> vs 業界 <code>first_name</code>）</li>
</ul>
<p>schema 演進時、命名與結構要一起考慮。欄位重命名、拆欄位、合併欄位都應配合 <a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> 與 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a> 策略、讓新舊版本在過渡期可共存。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一查詢在資料量成長後延遲快速上升</td>
          <td>索引與查詢模型不對齊</td>
          <td>補複合索引、重寫查詢條件</td>
      </tr>
      <tr>
          <td>migration 後查詢計畫顯著變化</td>
          <td>統計資訊或索引選擇偏移</td>
          <td>重建統計、校正索引與查詢</td>
      </tr>
      <tr>
          <td>交易流程需跨多表同步更新</td>
          <td>table 邊界與業務聚合邊界不一致</td>
          <td>重切聚合邊界、減少跨聚合同步更新</td>
      </tr>
      <tr>
          <td>同義欄位在多表重複存在且語意漂移</td>
          <td>命名與責任邊界失控</td>
          <td>收斂欄位責任、補資料字典與遷移計畫</td>
      </tr>
      <tr>
          <td>修復事故時需要多次手動比對資料</td>
          <td>可追蹤欄位與關聯鍵不足</td>
          <td>補追蹤欄位、設計對帳查詢與修復流程</td>
      </tr>
      <tr>
          <td>單表 &gt; 1 TB 且 vacuum 變慢</td>
          <td>沒 partition、後續維運成本爆</td>
          <td>規劃 partition by range / hash</td>
      </tr>
      <tr>
          <td>大量 unused index</td>
          <td>寫入吞吐被舊 index 拖垮</td>
          <td>review pg_stat_user_indexes、定期 drop</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 schema 設計等同於「先能寫入就好」、會把結構債延後到流量成長與事故時一次爆發。資料模型的工程價值在於可演進性、不在於初版欄位數量最少。</p>
<p>把索引當成效能補丁、忽略查詢模型與資料責任、也會讓後續維護成本持續疊加。索引與查詢要一起設計、才能在演進中保持穩定。</p>
<p>把 normalize 當成 <em>絕對守則</em>、忽略 denormalize 的工程效益。1NF / 2NF / 3NF 是理論起點、不是 <em>production 必須</em>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>Schema 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>DynamoDB single-table design、極端 denormalize</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>Composite partition key、event_id × user_id_hash</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>200 個獨立 cluster、按業務切 partition</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>watch list embedded design、跨裝置同步</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></td>
          <td>Cosmos DB synthetic partition key 強制分散</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>資料建模議題可以用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 做回寫練習。讀這個事件時、先看跨區拓樸切換如何影響資料一致性、再回到本章檢查三件事：聚合邊界是否清晰、交易查詢與對帳查詢是否分層、修復時是否有可追蹤欄位與對帳鍵。</p>
<p>這個案例主要支撐的是「查詢與資料模型邊界」判讀、不直接支撐 transaction retry 或 queue replay 調校；若問題是重試放大、應轉到 1.3 或 3.x 章節處理。</p>
<p>當事件呈現長時間人工比對或查詢語意漂移時、先修正本章的 query boundary 與 naming 一致性、再補 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> 的驗證與回退路徑。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>schema 設計會直接影響後續可靠性與事故處理。</p>
<ol>
<li>與 1.3 的交接：交易一致性邊界落在 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.6 的交接：演進策略落在 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>。</li>
<li>與 1.7 的交接：欄位責任進入 production rollout 時、讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據實作示範</a>。</li>
<li>與 1.8 的交接：state ownership 跟 query boundary 設計落在 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a>。</li>
<li>與 1.10 的交接：KV / Document 的 partition key 設計落在 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">KV / Document 容量規劃</a>。</li>
<li>與 4.20 的交接：查詢與資料驗證證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.11 的交接：高風險 schema 變更進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>。</li>
<li>與 8.19 的交接：資料修復與回退決策記錄進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>、<a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a></li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> / <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a></li>
<li>Vendor：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL index 設計</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL InnoDB clustered index</a>、<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 single-table design</a></li>
<li>DynamoDB schema 深入：<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table design</a> / <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition key 反模式</a> / <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">GSI / LSI 設計</a></li>
<li>MongoDB schema 深入：<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a> / <a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key 選型</a></li>
<li>Cosmos DB schema 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">partition key 設計</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/</guid><description>&lt;p>MySQL 是大型網路服務的常見選擇、簡單 query 效能跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a> 生態（Vitess / PlanetScale）成熟。GitHub、Shopify、Slack、Facebook（YouTube 從 MySQL 起家）等大規模服務的核心 OLTP 多採 MySQL。InnoDB engine 的 row-level lock、clustered index、buffer pool tuning 都被深度驗證。&lt;/p>
&lt;h2 id="教學路線高併發-oltp-與分片生態">教學路線：高併發 OLTP 與分片生態&lt;/h2>
&lt;p>MySQL 服務頁的教學目標是把「簡單 SQL 查詢」推進到高併發 OLTP、replication、online schema change 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding governance&lt;/a>。讀者讀完後要能判斷 MySQL 何時是成熟預設、何時已經進入 Vitess / PlanetScale 或 application sharding 的討論。&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>OLTP 基線&lt;/td>
 &lt;td>MySQL 適合哪種大量簡單查詢與交易路徑&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication&lt;/td>
 &lt;td>replica、failover、lag 與 read scaling 如何影響服務&lt;/td>
 &lt;td>容量特性、容量規劃要點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema change&lt;/td>
 &lt;td>online schema change 與 migration 如何保護高流量服務&lt;/td>
 &lt;td>容量規劃要點、預計實作話題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sharding&lt;/td>
 &lt;td>Vitess、PlanetScale 與 application sharding 何時變成主線&lt;/td>
 &lt;td>跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時轉 PostgreSQL、Aurora、DynamoDB 或 distributed SQL&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位高併發簡單-sql--強分片生態">定位：高併發簡單 SQL + 強分片生態&lt;/h2>
&lt;p>MySQL 跟 PostgreSQL 是 SQL OLTP 兩大主流、但設計取捨明顯不同：&lt;/p>
&lt;ul>
&lt;li>MySQL 偏 &lt;em>簡單 query 效能 + 分片生態&lt;/em> — InnoDB clustered index 對 primary key range query 特別快、Vitess 提供超大規模透明 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a>&lt;/li>
&lt;li>PostgreSQL 偏 &lt;em>特性深度&lt;/em> — 詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>選 MySQL 的核心訴求：需要超大規模分片（&amp;gt; 100 TB、&amp;gt; 100K WPS）、簡單 query 為主、已用 MySQL 生態工具鏈（gh-ost、pt-online-schema-change）。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>&lt;strong>單一 primary 寫吞吐&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>標準 InnoDB：10K-30K WPS（依 row size、commit sync、index 數量）&lt;/li>
&lt;li>高階 instance + 優化 schema：50K-100K WPS&lt;/li>
&lt;li>超過此級別 → &lt;a href="vitess-sharding/">Vitess sharding&lt;/a> 或 PlanetScale&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Connection 上限&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>預設 max_connections = 151、實務常設 1000-5000&lt;/li>
&lt;li>每個 connection thread stack ~3 MB + session buffer 累積、active 高峰時 ~8-10 MB（thread + sort/join buffer）&lt;/li>
&lt;li>仍建議 ProxySQL / connection pool 限制 backend connection 數&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Replication&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>MySQL 是大型網路服務的常見選擇、簡單 query 效能跟 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 生態（Vitess / PlanetScale）成熟。GitHub、Shopify、Slack、Facebook（YouTube 從 MySQL 起家）等大規模服務的核心 OLTP 多採 MySQL。InnoDB engine 的 row-level lock、clustered index、buffer pool tuning 都被深度驗證。</p>
<h2 id="教學路線高併發-oltp-與分片生態">教學路線：高併發 OLTP 與分片生態</h2>
<p>MySQL 服務頁的教學目標是把「簡單 SQL 查詢」推進到高併發 OLTP、replication、online schema change 與 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding governance</a>。讀者讀完後要能判斷 MySQL 何時是成熟預設、何時已經進入 Vitess / PlanetScale 或 application sharding 的討論。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OLTP 基線</td>
          <td>MySQL 適合哪種大量簡單查詢與交易路徑</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>replica、failover、lag 與 read scaling 如何影響服務</td>
          <td>容量特性、容量規劃要點</td>
      </tr>
      <tr>
          <td>Schema change</td>
          <td>online schema change 與 migration 如何保護高流量服務</td>
          <td>容量規劃要點、預計實作話題</td>
      </tr>
      <tr>
          <td>Sharding</td>
          <td>Vitess、PlanetScale 與 application sharding 何時變成主線</td>
          <td>跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時轉 PostgreSQL、Aurora、DynamoDB 或 distributed SQL</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位高併發簡單-sql--強分片生態">定位：高併發簡單 SQL + 強分片生態</h2>
<p>MySQL 跟 PostgreSQL 是 SQL OLTP 兩大主流、但設計取捨明顯不同：</p>
<ul>
<li>MySQL 偏 <em>簡單 query 效能 + 分片生態</em> — InnoDB clustered index 對 primary key range query 特別快、Vitess 提供超大規模透明 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a></li>
<li>PostgreSQL 偏 <em>特性深度</em> — 詳見 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a></li>
</ul>
<p>選 MySQL 的核心訴求：需要超大規模分片（&gt; 100 TB、&gt; 100K WPS）、簡單 query 為主、已用 MySQL 生態工具鏈（gh-ost、pt-online-schema-change）。</p>
<h2 id="容量特性">容量特性</h2>
<p><strong>單一 primary 寫吞吐</strong>：</p>
<ul>
<li>標準 InnoDB：10K-30K WPS（依 row size、commit sync、index 數量）</li>
<li>高階 instance + 優化 schema：50K-100K WPS</li>
<li>超過此級別 → <a href="vitess-sharding/">Vitess sharding</a> 或 PlanetScale</li>
</ul>
<p><strong>Connection 上限</strong>：</p>
<ul>
<li>預設 max_connections = 151、實務常設 1000-5000</li>
<li>每個 connection thread stack ~3 MB + session buffer 累積、active 高峰時 ~8-10 MB（thread + sort/join buffer）</li>
<li>仍建議 ProxySQL / connection pool 限制 backend connection 數</li>
</ul>
<p><strong>Replication</strong>：</p>
<ul>
<li>async / semi-sync / GTID-based</li>
<li>跨 AZ async lag 通常 &lt; 100ms</li>
<li>跨 region 通常用 chain replication 或 binlog 同步</li>
</ul>
<p><strong>Storage 上限</strong>：</p>
<ul>
<li>單一 table 64 TB（InnoDB 設計上限）</li>
<li>實務超過 1 TB 表建議分片</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. 大規模 OLTP + 分片需求</strong>：</p>
<ul>
<li>流量 &gt; 50K WPS、必須進入 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 設計</li>
<li>用 Vitess / PlanetScale 透明 sharding、應用層幾乎不必改</li>
<li>對應產業：超大網路服務（GitHub、Shopify、Slack）</li>
</ul>
<p><strong>2. 簡單 query 為主</strong>：</p>
<ul>
<li>primary key lookup、簡單 range query</li>
<li>不太用 CTE、window function、複雜 JOIN</li>
<li>InnoDB clustered index 對這類 workload 特別快</li>
</ul>
<p><strong>3. 既有 MySQL 生態工具</strong>：</p>
<ul>
<li>gh-ost / pt-online-schema-change（online schema migration）</li>
<li>Orchestrator（HA topology 管理）</li>
<li>ProxySQL（query routing + connection pool）</li>
<li>Maxwell / Debezium MySQL（<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">CDC</a>）</li>
</ul>
<p><strong>4. 強一致 transaction 但容忍部分 SQL 功能缺失</strong>：</p>
<ul>
<li>不需 partial index、不需 JSONB indexing</li>
<li>不需 PostGIS、用 spatial extension 夠</li>
</ul>
<p><strong>5. Aurora MySQL（managed 路徑）</strong>：</p>
<ul>
<li>從自管 MySQL 上 AWS、保留 wire protocol</li>
<li>詳見 <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 page</a></li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 需要 PostgreSQL 等級的 SQL / JSON 特性</strong>：</p>
<ul>
<li>複雜 CTE、recursive query、window function</li>
<li>JSON Schema validation、JSONB GIN indexing</li>
<li>PostGIS 等深度 extension</li>
</ul>
<p><strong>2. 全球 multi-region active-active write</strong>：</p>
<ul>
<li>MySQL 設計是 single primary、跨 region 是 async</li>
<li>替代：Aurora DSQL、Spanner、Vitess multi-cluster</li>
</ul>
<p><strong>3. 大規模 OLAP</strong>：</p>
<ul>
<li>MySQL 定位在 OLTP，analytics workload 交給 OLAP 系統</li>
<li>替代：ClickHouse、BigQuery、Snowflake</li>
</ul>
<p><strong>4. KV 簡單查詢 + sub-10ms p99</strong>：</p>
<ul>
<li>跟 PostgreSQL 一樣有 parsing / planning 開銷</li>
<li>替代：DynamoDB、Redis</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs PostgreSQL</strong>：</p>
<ul>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a> 對比段</li>
<li>摘要：MySQL 適合超大規模分片、PostgreSQL 適合進階 SQL 特性</li>
</ul>
<p><strong>vs Aurora MySQL（同 wire protocol）</strong>：</p>
<ul>
<li>MySQL（自管 / RDS）：可跨雲、彈性高</li>
<li>Aurora MySQL：AWS managed、storage / compute 分離、更多 read replica</li>
<li>選自管 MySQL：跨雲需求、預算敏感</li>
<li>選 Aurora MySQL：AWS 生態深、需要 storage scaling</li>
</ul>
<p><strong>vs PlanetScale（Vitess managed）</strong>：</p>
<ul>
<li>MySQL（自管 + Vitess）：完全控制、可自管分片</li>
<li>PlanetScale：managed Vitess、branch-based schema migration</li>
<li>選 MySQL + Vitess：team 有能力管 Vitess、預算敏感</li>
<li>選 PlanetScale：想 zero ops、branch-based workflow</li>
</ul>
<p><strong>vs TiDB</strong>：</p>
<ul>
<li>MySQL：single-primary、傳統分片靠 Vitess</li>
<li>TiDB：MySQL wire protocol 相容、HTAP（OLTP + OLAP 同庫）、跨 region 強一致</li>
<li>選 MySQL：已有 MySQL 投資、不想換引擎</li>
<li>選 TiDB：需要跨 region 強一致 + OLAP 同庫</li>
</ul>
<p><strong>vs Vitess（self-managed sharding layer）</strong>：</p>
<ul>
<li>Vitess 本質是 MySQL 上層的 sharding layer</li>
<li>由 YouTube 設計、捐贈 CNCF</li>
<li>適合超大規模 MySQL 集群、需要透明 sharding</li>
</ul>
<p><strong>vs DynamoDB（document/KV 替代）</strong>：</p>
<ul>
<li>MySQL：SQL、有 transaction、ad-hoc query、connection-based</li>
<li>DynamoDB：KV、partition 透明、無 connection 限制、5 個 9 SLA</li>
<li>選 MySQL：需要 ad-hoc query、複雜 JOIN、SQL transaction</li>
<li>選 DynamoDB：access pattern 固定、AWS-only、想避免 connection limit 問題</li>
<li>詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 的 connection model 對比</li>
</ul>
<p><strong>vs Spanner / CockroachDB / Aurora DSQL（distributed SQL）</strong>：</p>
<ul>
<li>MySQL + Vitess：自管 sharding、operational 重、跨雲可用</li>
<li>Spanner / CockroachDB / Aurora DSQL：分散式 SQL、跨 region 強一致、transparent sharding</li>
<li>選 MySQL + Vitess：已有 MySQL 投資、有能力管 Vitess、預算敏感</li>
<li>選 distributed SQL：需要 multi-region 強一致、不想自管 sharding</li>
<li>詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
</ul>
<p><strong>vs MongoDB（document 替代）</strong>：</p>
<ul>
<li>MySQL：SQL + JSON column 補充</li>
<li>MongoDB：document 為主、aggregation pipeline 強、schema-flexible</li>
<li>選 MySQL：主要結構化、少量半結構化</li>
<li>選 MongoDB：document 占主要 schema、aggregation 工作負載</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. Sharding 是 MySQL 大規模的核心</strong>：</p>
<ul>
<li>單一 MySQL primary 寫吞吐有上限</li>
<li>Vitess / PlanetScale 用 keyspace + shard 切分</li>
<li>shard key 設計類似 DynamoDB partition key — 必須均勻</li>
<li>大規模案例：Shopify（多 shard 分散）、Slack（per-team sharding）</li>
</ul>
<p><strong>2. Online schema change 是必備</strong>：</p>
<ul>
<li>ALTER TABLE 直接跑會 lock 整個 table</li>
<li>gh-ost（GitHub）/ pt-online-schema-change（Percona）/ Vitess online DDL 用 ghost table 漸進 migrate</li>
<li>大表 schema change 可能跑 hours / days、要排程</li>
</ul>
<p><strong>3. Replication 跟 GTID</strong>：</p>
<ul>
<li>GTID-based replication 比 binlog position 容易管 topology</li>
<li>semi-sync replication 保證至少一個 standby ack 才 commit</li>
<li>async replication 高吞吐但 lag 較大</li>
</ul>
<p><strong>4. Connection management</strong>：</p>
<ul>
<li>ProxySQL 是 MySQL 生態的 connection pool 標準</li>
<li>提供 query routing（讀 → replica、寫 → primary）</li>
<li>對應 <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 case</a> — RDB connection limit 議題對 MySQL 同樣適用</li>
</ul>
<p><strong>5. InnoDB tuning</strong>：</p>
<ul>
<li>innodb_buffer_pool_size：dedicated server 70-75%、shared server 30-50%（詳見 <a href="innodb-tuning/">InnoDB Tuning</a>）</li>
<li>innodb_flush_log_at_trx_commit：1（durable）vs 2（faster）vs 0（fastest, 不安全）</li>
<li>innodb_io_capacity：依 storage 類型調整</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>MySQL 的成熟生態容易讓讀者過早引入重工具。這一段補上 deep article audit 提到的 anti-recommendation 缺口：先說何時維持簡單 MySQL 路徑，再說何時升級到 ProxySQL、Orchestrator、gh-ost、Vitess、PlanetScale 或 distributed SQL。</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replication</td>
          <td>單 primary + 1-2 replica，lag 可被 read routing 容忍</td>
          <td>failover 反覆手動、GTID gap、semi-sync fallback</td>
          <td><a href="replication-topology/">Replication Topology</a>、<a href="orchestrator-failover/">Orchestrator Failover</a></td>
      </tr>
      <tr>
          <td>Online schema change</td>
          <td>小表、maintenance window 足夠、MySQL 8.0 instant DDL 可 cover</td>
          <td>大表 ALTER 需 hours、metadata lock 影響 production</td>
          <td><a href="online-schema-change-tools/">Online Schema Change Tools</a>、<a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a></td>
      </tr>
      <tr>
          <td>ProxySQL</td>
          <td>application pool + primary endpoint 已能控制連線</td>
          <td>read/write routing、lag-aware routing、connection storm</td>
          <td><a href="proxysql-config/">ProxySQL Config</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool</a></td>
      </tr>
      <tr>
          <td>Vitess / sharding</td>
          <td>單 primary 寫入與資料量仍在可維護範圍</td>
          <td>&gt; 50K WPS、&gt; 100 TB、shard key 已明確、跨 shard query 可接受</td>
          <td><a href="vitess-sharding/">Vitess Sharding</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a></td>
      </tr>
      <tr>
          <td>PlanetScale</td>
          <td>團隊已有 DBA / SRE 能力管理 Vitess 或自管 MySQL</td>
          <td>想把 Vitess ops、schema branch workflow 與 failover 交給平台</td>
          <td><a href="migrate-to-planetscale/">→ PlanetScale</a>、<a href="migrate-vitess-to-planetscale/">Vitess → PlanetScale</a></td>
      </tr>
      <tr>
          <td>Distributed SQL</td>
          <td>workload 仍是 single-region OLTP 或 Vitess 可解</td>
          <td>multi-region 強一致、cross-shard transaction 是核心需求</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></td>
      </tr>
  </tbody>
</table>
<p>Replication 的簡單路徑是 GTID + async replica + 明確 read routing。當 failover 仍靠人工判斷、replica re-pointing 反覆出錯、或 semi-sync fallback 沒有被監控時，才需要把 Orchestrator、ProxySQL 與 incident runbook 放進同一條 HA 路徑。</p>
<p>Online schema change 的簡單路徑是先判斷 MySQL 8.0 instant / inplace DDL 能否 cover。只有大表 rewrite、長時間 metadata lock、FK / trigger 複雜互動或 maintenance window 不足時，才讓 gh-ost / pt-online-schema-change 成為主線工具。</p>
<p>Sharding 的簡單路徑是延後到資料形狀穩定後再做。Vitess 能把 MySQL 推到超大規模，但它也引入 VTGate、VTTablet、VReplication、VSchema、resharding workflow 與跨 shard transaction 邊界；<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">shard key</a> 還沒穩定時，應先用 schema、index、read replica、partition 與容量治理延長單 primary 壽命。</p>
<p>Managed sharding 的簡單路徑是先確認團隊想轉移哪一層責任。PlanetScale 解的是 Vitess operation、branch-based schema workflow 與 managed failover；FK、cross-shard query、connection pool 與 cost model 仍要在 migration playbook 中驗證。</p>
<h2 id="deep-article--migration-playbook已完成">Deep article + Migration playbook（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replication topology（async / semi-sync / GTID）配置</td>
          <td><a href="replication-topology/">replication-topology</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>gh-ost / pt-online-schema-change 對比</td>
          <td><a href="online-schema-change-tools/">online-schema-change-tools</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>ProxySQL 配置跟 query routing</td>
          <td><a href="proxysql-config/">proxysql-config</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Orchestrator failover 設計</td>
          <td><a href="orchestrator-failover/">orchestrator-failover</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>InnoDB tuning（buffer pool / log / IO）</td>
          <td><a href="innodb-tuning/">innodb-tuning</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Binary log + Maxwell / Debezium CDC</td>
          <td><a href="binlog-cdc/">binlog-cdc</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Vitess sharding 設計</td>
          <td><a href="vitess-sharding/">vitess-sharding</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>8.0 modern SQL（CTE / window / JSON_TABLE）</td>
          <td><a href="modern-sql-features/">modern-sql-features</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Group Replication / InnoDB Cluster 部署</td>
          <td><a href="group-replication/">group-replication</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Query optimization deep dive</td>
          <td><a href="query-optimization/">query-optimization</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Partitioning（range / list / hash / sub-partition）</td>
          <td><a href="partitioning/">partitioning</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>PITR + Backup strategy</td>
          <td><a href="pitr-backup/">pitr-backup</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Lock contention（gap / next-key / deadlock）</td>
          <td><a href="lock-contention/">lock-contention</a></td>
          <td>Deep article</td>
      </tr>
      <tr>
          <td>Hands-on 操作路線</td>
          <td><a href="hands-on/">hands-on</a></td>
          <td>操作型章節群</td>
      </tr>
      <tr>
          <td>5.7 → 8.0 major version upgrade</td>
          <td><a href="major-version-upgrade/">major-version-upgrade</a></td>
          <td>Migration playbook（Type E）</td>
      </tr>
      <tr>
          <td>從自管 MySQL 遷到 Aurora MySQL</td>
          <td><a href="migrate-to-aurora/">migrate-to-aurora</a></td>
          <td>Migration playbook（Type C）</td>
      </tr>
      <tr>
          <td>從自管 MySQL 遷到 PlanetScale</td>
          <td><a href="migrate-to-planetscale/">migrate-to-planetscale</a></td>
          <td>Migration playbook（Type E）</td>
      </tr>
      <tr>
          <td>自管 Vitess 遷到 PlanetScale</td>
          <td><a href="migrate-vitess-to-planetscale/">migrate-vitess-to-planetscale</a></td>
          <td>Migration playbook（Type C）</td>
      </tr>
      <tr>
          <td>從 MySQL 遷到 PostgreSQL</td>
          <td><a href="migrate-to-postgresql/">migrate-to-postgresql</a></td>
          <td>Migration playbook</td>
      </tr>
  </tbody>
</table>
<h2 id="補充正文路由">補充正文路由</h2>
<p>當前 deep article、migration playbook、補充正文與 hands-on 已 cover ops / schema / failover / tuning / SQL features / sharding / backup / migration / security / audit / document / OLAP / memory / metadata lock 等維度。下列補充正文用來承接 overview 中提到的延伸議題：</p>
<ul>
<li><strong><a href="encryption-tls-key-management/">Encryption at rest + TLS in transit + key management</a></strong>：對應 PG TLS-mTLS 議題</li>
<li><strong><a href="audit-log-siem/">Audit log + SIEM 整合</a></strong>：MySQL Enterprise Audit Plugin 跟 Splunk / Elastic Security 整合</li>
<li><strong><a href="document-store-x-protocol/">MySQL Document Store（X-Protocol）</a></strong>：少用但對特定 use case 有興趣</li>
<li><strong><a href="multi-source-replication/">Multi-source replication topology</a></strong>：1 個 replica 從 N 個 primary 拉、用於 sharded environment 整合</li>
<li><strong><a href="heatwave-olap-addon/">HeatWave（MySQL OLAP add-on）</a></strong>：Oracle 推的 HTAP solution、跟 ClickHouse / Snowflake 對比</li>
<li><strong><a href="cross-buffer-memory-contention/">Cross-buffer memory contention deep dive</a></strong>：buffer pool / connection thread / temp table / sort buffer 之間的 RAM 競爭、跟 OS swap 互動</li>
<li><strong><a href="metadata-lock-deep-dive/">Metadata lock deep dive</a></strong>：DDL / long-running SELECT / FK 互動造成的 stalls</li>
</ul>
<p>上述補充篇已完成正文，並保留既有路由。Encryption / TLS / key management 接 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a> 與 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>；audit log 接 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 07 資安資料保護；Document Store 接 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a> 與 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>；multi-source replication 接 <a href="replication-topology/">Replication Topology</a>；HeatWave 接 OLAP 替代路由；memory contention 接 <a href="innodb-tuning/">InnoDB Tuning</a>；metadata lock 接 <a href="lock-contention/">Lock Contention</a> 與 <a href="online-schema-change-tools/">Online Schema Change Tools</a>。</p>
<h2 id="已知-limitation多輪-audit-結論">已知 limitation（多輪 audit 結論）</h2>
<p>17 篇 batch 跑過 4-reviewer audit（寫作規範 / 跨檔一致性 / 技術準確性 / 結構性質疑）後留下的 limitation：</p>
<ul>
<li><em>Framework bias</em>：5 篇 migration playbook 全落在 Type A / C / E、沒一篇 Type B / D / F。這反映 <em>MySQL 領域 migration 的本質</em>（多數情境是 schema 差 / operational 轉手 / paradigm shift）、也可能反映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 type framework</a> 的覆蓋限制</li>
<li><em>Anti-recommendation 已補 overview 路由</em>：本頁新增「Anti-recommendation 與升級路由」作為總入口；各 deep article 之後仍可逐篇補「何時維持簡單設計」段。</li>
<li><em>Real case anchor 已下沉</em>：本頁「真實案例 anchor」把 Shopify、Slack、GitHub gh-ost、YouTube / Vitess 與既有 09 case 串回 deep article；Shopify CDC、gh-ost workflow、YouTube / Vitess 與 Netflix Aurora consolidation 已補到對應 deep article 的 production case 段。</li>
<li><em>PG 對比 narrative</em>：對比段公允度尚可、但 PG 弱點（vacuum ops 開銷 / connection-per-process model / replication slot 治理）較少在 MySQL 視角展開、單方面對比偶有偏 MySQL 不利</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<p>MySQL 沒有直接的 09 case（大規模 MySQL 多在 engineering blog、不在 vendor case study）、但作為 baseline / 遷移源 在多處出現：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 MySQL 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a></td>
          <td>從多套 RDBMS（含 MySQL）統一到 Aurora MySQL</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB</a></td>
          <td>TiDB（MySQL 相容）→ DynamoDB 對比</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 RDB connection limit</a></td>
          <td>MySQL connection 限制問題（同 PostgreSQL）</td>
      </tr>
  </tbody>
</table>
<h2 id="真實案例-anchor">真實案例 anchor</h2>
<p>MySQL 真實案例的責任是把大規模 OLTP 的機制壓力放回正文。案例不只證明「某公司使用 MySQL」，而是提供 schema change、CDC、sharding、connection、queue 整合或 managed migration 的壓力來源。</p>
<table>
  <thead>
      <tr>
          <th>案例 / 來源</th>
          <th>回收的工程訊號</th>
          <th>對應正文路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">Shopify Debezium CDC over sharded MySQL</a></td>
          <td>100+ shard、~150 Debezium connector、BFCM 100K records/sec、snapshot lock 與 oversized payload</td>
          <td><a href="binlog-cdc/">Binary Log + CDC</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>、<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">Slack Job Queue 演進到 Kafka + Redis</a></td>
          <td>成長期把背景工作拆成多條傳遞路徑，揭露單一資料路徑與 queue 路徑分工</td>
          <td>MySQL 只承擔 OLTP <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>；queue / cache 路徑回 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 Message Queue</a></td>
      </tr>
      <tr>
          <td>gh-ost / GitHub operation workflow</td>
          <td>大表 schema change 需要 throttle、pause / resume、cutover 控制</td>
          <td><a href="online-schema-change-tools/">Online Schema Change Tools</a></td>
      </tr>
      <tr>
          <td>YouTube / Vitess</td>
          <td>MySQL sharding layer 需要 VTGate、VTTablet、VReplication、VSchema</td>
          <td><a href="vitess-sharding/">Vitess Sharding</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>、<a href="migrate-to-planetscale/">→ PlanetScale</a></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 Aurora consolidation</a></td>
          <td>多套 RDBMS 整併到 managed Aurora，揭露 operation transfer driver</td>
          <td><a href="migrate-to-aurora/">→ Aurora</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 vendor</a></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 RDB connection limit</a></td>
          <td>surge 場景 connection limit 讓 RDB 退到 DynamoDB 類 access pattern</td>
          <td><a href="proxysql-config/">ProxySQL Config</a>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></td>
      </tr>
  </tbody>
</table>
<p>案例下沉規則是先放 overview，再進 deep article。當某個案例只支撐服務定位，留在本頁；當案例提供具體操作訊號，例如 Shopify 的 Debezium connector scaling、GitHub 的 gh-ost workflow 或 YouTube 的 Vitess topology，對應 deep article 要保留 production case 段、讓讀者能從機制直接跳到案例。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>直接 ALTER TABLE 大表</strong>：lock 表 hours、production 停擺、必須用 online schema change</li>
<li><strong>不用 GTID</strong>：replication topology 變更困難、recover from failure 容易出錯</li>
<li><strong>buffer pool 太小</strong>：cache miss 高、IOPS 飆升</li>
<li><strong>shard key 選錯</strong>：hot shard 出現、整體吞吐達不到名義</li>
<li><strong>connection 沒 pool</strong>：跟 PostgreSQL 同樣問題、用 ProxySQL</li>
<li><strong>semi-sync 對高吞吐 workload</strong>：每次 commit 等 ack、寫吞吐降一半</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</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 vendor</a>（managed MySQL）</li>
<li>操作：<a href="/blog/backend/01-database/vendors/mysql/hands-on/" data-link-title="MySQL Hands-on 操作路線" data-link-desc="MySQL local lab、ProxySQL routing、online schema change、replication failover、backup restore 與 Vitess sandbox 的操作型章節設計">MySQL Hands-on</a>（local lab、ProxySQL、OSC、replication failover、backup restore、Vitess sandbox）</li>
<li>上游：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（MySQL 不適用時的替代）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> — connection / replication / lock contention 常見 MySQL bottleneck</li>
<li>官方：<a href="https://dev.mysql.com/doc/">MySQL Documentation</a>、<a href="https://vitess.io/">Vitess</a>、<a href="https://planetscale.com/">PlanetScale</a></li>
</ul>
]]></content:encoded></item><item><title>9.2 Workload Modeling</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Workload modeling 的角色是讓壓測結果有意義。如果壓測模型跟 production traffic shape 不一致、壓測通過不代表 production 撐得住。這一層的工作不是「製造大量請求」、而是「製造跟 production 一樣形狀的請求」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&amp;#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論&lt;/a> 的關係：9.1 提供推導工具、9.2 把工具的輸入（流量參數）量化。沒有 workload model、Little&amp;rsquo;s Law 的 λ 跟 W 都是猜。&lt;/p>
&lt;p>本章的核心問題：production traffic 不是「N RPS」這麼簡單。它有時間分布、地理分布、操作分布、cohort 分布、burst pattern。每個維度都會影響系統行為。一個只測「總 RPS」的壓測通過了、production 還是可能因為某個 cohort 集中或某個 burst pattern 出事。&lt;/p>
&lt;h2 id="traffic-shape-的五個維度">Traffic shape 的五個維度&lt;/h2>
&lt;p>Production traffic shape 至少要量五個維度才算 model 完整。&lt;/p>
&lt;p>&lt;strong>平均吞吐 vs 峰值&lt;/strong>：peak/avg ratio 是工程意義最大的單一指標。1.5x 的 peak/avg 代表流量相對平緩、容量規劃可以接近 average peak；3-5x 的 peak/avg 代表 bursty 流量、必須按 peak 規劃、平日大幅 over-provision。對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday 24h 1.67 億 / 峰值 3500 RPS&lt;/a> 峰均比約 1.81x 屬於相對溫和；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">Tixcraft 5 分鐘賣完&lt;/a> 是另一極端。&lt;/p>
&lt;p>&lt;strong>時間分布&lt;/strong>：日內（早晚通勤）、週內（週末活躍）、月內（月初發薪）、季內（節慶）、年內（活動）。不同尺度的週期都要記錄、用於 forecast 跟 pre-scaling 決策。&lt;/p>
&lt;p>&lt;strong>用戶分布&lt;/strong>：geographic（哪個 region 多）、device（mobile vs desktop）、tier（free / paid / VIP）。同樣 RPS、不同分布可能造成完全不同系統行為 — VIP 用戶可能跑更複雜 query、mobile 用戶可能更多 retry、跨 region 用戶可能更多 cross-zone latency。&lt;/p>
&lt;p>&lt;strong>操作分布&lt;/strong>：read vs write 比、不同 endpoint 的 mix。一個系統 90% read 跟 50% read 的容量設計完全不同 — read-heavy 可以 cache、write-heavy 必須關注 storage IOPS。&lt;/p>
&lt;p>&lt;strong>Cohort 與 burst pattern&lt;/strong>：同一秒的請求不一定均勻 — bursty arrival 比 Poisson arrival 對系統更殘酷。突發 burst 來源：promo 推播、KOL 推廣、新片發布、新聞事件。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &amp;#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 賽事高潮 burst&lt;/a> — 賽事「進球瞬間」 burst 比平均流量高 10-50 倍；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&amp;#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&amp;#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布&lt;/a> — 同片瞬間集中、cohort 高度集中。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Workload modeling 的角色是讓壓測結果有意義。如果壓測模型跟 production traffic shape 不一致、壓測通過不代表 production 撐得住。這一層的工作不是「製造大量請求」、而是「製造跟 production 一樣形狀的請求」。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> 的關係：9.1 提供推導工具、9.2 把工具的輸入（流量參數）量化。沒有 workload model、Little&rsquo;s Law 的 λ 跟 W 都是猜。</p>
<p>本章的核心問題：production traffic 不是「N RPS」這麼簡單。它有時間分布、地理分布、操作分布、cohort 分布、burst pattern。每個維度都會影響系統行為。一個只測「總 RPS」的壓測通過了、production 還是可能因為某個 cohort 集中或某個 burst pattern 出事。</p>
<h2 id="traffic-shape-的五個維度">Traffic shape 的五個維度</h2>
<p>Production traffic shape 至少要量五個維度才算 model 完整。</p>
<p><strong>平均吞吐 vs 峰值</strong>：peak/avg ratio 是工程意義最大的單一指標。1.5x 的 peak/avg 代表流量相對平緩、容量規劃可以接近 average peak；3-5x 的 peak/avg 代表 bursty 流量、必須按 peak 規劃、平日大幅 over-provision。對應案例：<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday 24h 1.67 億 / 峰值 3500 RPS</a> 峰均比約 1.81x 屬於相對溫和；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 5 分鐘賣完</a> 是另一極端。</p>
<p><strong>時間分布</strong>：日內（早晚通勤）、週內（週末活躍）、月內（月初發薪）、季內（節慶）、年內（活動）。不同尺度的週期都要記錄、用於 forecast 跟 pre-scaling 決策。</p>
<p><strong>用戶分布</strong>：geographic（哪個 region 多）、device（mobile vs desktop）、tier（free / paid / VIP）。同樣 RPS、不同分布可能造成完全不同系統行為 — VIP 用戶可能跑更複雜 query、mobile 用戶可能更多 retry、跨 region 用戶可能更多 cross-zone latency。</p>
<p><strong>操作分布</strong>：read vs write 比、不同 endpoint 的 mix。一個系統 90% read 跟 50% read 的容量設計完全不同 — read-heavy 可以 cache、write-heavy 必須關注 storage IOPS。</p>
<p><strong>Cohort 與 burst pattern</strong>：同一秒的請求不一定均勻 — bursty arrival 比 Poisson arrival 對系統更殘酷。突發 burst 來源：promo 推播、KOL 推廣、新片發布、新聞事件。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 賽事高潮 burst</a> — 賽事「進球瞬間」 burst 比平均流量高 10-50 倍；<a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布</a> — 同片瞬間集中、cohort 高度集中。</p>
<h2 id="從-production-log-抽-workload-model">從 production log 抽 workload model</h2>
<p>實務上 workload model 不能憑空寫、要從 production data 抽。流程通常分四步：</p>
<p><strong>第一步：data 蒐集</strong>。從 access log、APM trace、metric 系統取得 production traffic 樣本。要 sampling（不是全量）、避免影響 production；要包含 <em>至少一個完整 weekly cycle</em>（含週末、含峰谷）；要按 endpoint / per-tenant 分組。</p>
<p><strong>第二步：分組統計</strong>。對每組（per endpoint、per tier、per region）計算 percentile（p50 / p95 / p99）、arrival pattern（Poisson、bursty、scheduled）、payload size 分布。輸出是「workload profile」 — 比單一數字更接近 reality。</p>
<p><strong>第三步：序列重播</strong>。複製一段 production traffic 的時間序列、保留 inter-arrival timing（不只是 RPS 平均、是 <em>每秒幾個</em>）。這層讓 burst 在壓測重現、不只是「平均壓力均勻分布」。</p>
<p><strong>第四步：脫敏處理</strong>。PII（user_id、phone、address）必須匿名化或替換 — 否則壓測環境變成 PII 洩漏點。常見做法：hash + salt + 確保結果 cardinality 跟 production 一致。</p>
<p>production log 通常缺寫入 payload（log 只記 metadata、不記 request body）、要從 application metric 或 schema sample 補。schema sample 用「distinct value 抽樣」、不是「random」 — 確保壓測涵蓋常見 value pattern。</p>
<h2 id="synthetic-load-vs-production-replay">Synthetic load vs production replay</h2>
<p>兩種主要壓測方式各有取捨。</p>
<p><strong>Synthetic load</strong>：手寫腳本、明確控制每個請求的 shape。優點是好複現、可以針對特定情境設計（例如「測登入失敗 retry」）；缺點是容易脫離 production reality、寫腳本的人會無意識套用自己的偏見。</p>
<p><strong>Production traffic replay</strong>：用 GoReplay、Istio mirror、AWS VPC Traffic Mirroring 等工具把 production traffic 複製到測試環境。優點是 <em>最貼近真實</em>、自動帶上 burst 跟 cohort；缺點是消耗 production 下游資源（要算進容量規劃）、PII / 合規處理複雜、replay 環境的下游 mock 不容易做。</p>
<p><strong>混合模式</strong>：常態壓測用 synthetic（cheap、可控）、release candidate 驗證用 production replay（真實）、debug 特定 incident 用 <em>特定時段</em> 的 replay。三種工具在不同階段用、不是二選一。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel 雙峰需要兩個 workload model 並行</a> — 直播 model（CDN heavy、長 session）跟投注 model（低延遲、burst at goal）必須分開壓測、不能合成一個。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model 卡片</a> 跟 <a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic 卡片</a>。</p>
<h2 id="模型驗證怎麼知道模型像-production">模型驗證：怎麼知道模型像 production</h2>
<p>寫了 workload model 之後、怎麼驗證它真的「像 production」？方法是 <em>跑壓測 同時 對比 production metrics</em>。</p>
<p>驗證指標包含：throughput pattern（總 RPS、各 endpoint mix）、latency 分布（p50 / p95 / p99 對比）、resource utilization（CPU / memory / network 行為）、error rate 與 retry pattern。</p>
<p>兩個可能的偏差結果：</p>
<ul>
<li><strong>模型撐不住但 production 撐得住</strong> → 模型太苛刻、可能高估了流量或操作複雜度。usually fine、調整模型參數即可。</li>
<li><strong>模型撐得住但 production 撐不住</strong> → 模型不足、漏了某個維度。dangerous、需要回到 data 蒐集階段找漏掉的 pattern。</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID surge</a> — 之前的 workload model 完全不能用、必須 reset baseline 重新從 post-COVID 流量抽 model；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — 用實際售票場景重播驗證、不是 synthetic 數字。</p>
<h2 id="模型維護定期-review">模型維護：定期 review</h2>
<p>Workload model 不是一次抽完就永久有效。業務變化會讓模型過時、過時的模型導出的容量規劃會失準。</p>
<p>需要 re-抽 model 的訊號：</p>
<ul>
<li>新功能上線改變 user journey（例如新增 video upload、user 行為變寫多）</li>
<li>新市場進入改變 cohort 分布（例如進入印度市場、mobile share 大幅增加）</li>
<li>行銷活動改變 burst pattern（例如新增 push notification、burst 集中度上升）</li>
<li>用戶習慣轉變（例如 work-from-home 讓週末跟平日流量比變化）</li>
</ul>
<p>維護節奏建議每季 review 一次、重大產品改動立即 re-抽。每次 re-抽要 <em>跟前一版對比</em>、量化變化幅度、決定哪些容量計畫要重新評估。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a></td>
          <td>持續高峰型 workload（峰均比 1.81x）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>flash-sale 形狀（5 分鐘賣完）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a></td>
          <td>100+ 微服務各自 workload model（不能用單一）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>3 億 / 天的峰均比預估</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>雙峰必須兩個 model 並行</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a>（用什麼工具實作 model）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（用 model 跑 ramp-up）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（production log 來源）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve</a></li>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
</ul>
]]></content:encoded></item><item><title>9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/</guid><description>&lt;p>這個案例的核心責任是說明「事件型不可預期峰值」的工程做法。體育博彩流量的形狀跟 Prime Day 不同 — 峰值會在賽事的特定瞬間（進球、最後一分鐘）爆量、單一賽事內可能有多次脈衝、跨賽事的時間點難以提前數月排程。GR8 Tech 在 2022 FIFA World Cup 期間達到零停機營運、是這類負載形狀的有效參考。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>GR8 Tech 從本地基礎設施遷移到 AWS、重建為微服務架構後的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/gr8-tech-case-study/">GR8 Tech case study&lt;/a>）：&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>投注延遲&lt;/td>
 &lt;td>賽事高峰期額外延遲 2-3 秒&lt;/td>
 &lt;td>25 ms p95&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結算吞吐&lt;/td>
 &lt;td>（未公開）&lt;/td>
 &lt;td>每分鐘 100 萬次投注結算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交易吞吐&lt;/td>
 &lt;td>（未公開）&lt;/td>
 &lt;td>54000 TPS @ 25ms p95&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同時在線&lt;/td>
 &lt;td>-&lt;/td>
 &lt;td>200,000+ 同時使用者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>投注吞吐&lt;/td>
 &lt;td>-&lt;/td>
 &lt;td>每分鐘 80,000 次體育投注&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性&lt;/td>
 &lt;td>-&lt;/td>
 &lt;td>99.95% uptime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本彈性&lt;/td>
 &lt;td>固定預配置&lt;/td>
 &lt;td>需求降低時成本下降 25%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Amazon EKS（Kubernetes 容器編排、跨雲端與本地）、Amazon EC2（compute）、Amazon S3 與 Amazon EBS（儲存）、AWS Auto Scaling 結合 &lt;strong>GR8 Tech 自家 AI 預測模型&lt;/strong>、AWS Infrastructure Event Management（重大賽事支援）。&lt;/p>
&lt;p>擴展範圍：「Scaled to 15 markets using AWS」。事件覆蓋：2022 FIFA World Cup 期間零停機。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>GR8 Tech 的工程做法揭露三個事件型峰值的判讀重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>不可預期 ≠ 不可預測&lt;/strong>：賽事「何時開打」是已知的（schedule 提前公告）、「賽事內何時爆量」是未知的（進球、加時、最後一分鐘）。AI 預測模型不是預測「會不會有峰值」、而是預測「峰值在 60 秒內可能多大」、把擴容窗口縮短到反應時間之內。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的「預測時間尺度」軸。&lt;/li>
&lt;li>&lt;strong>延遲是業務指標、不是技術指標&lt;/strong>：「2-3 秒額外延遲」直接造成「投注失敗、客戶流失」。25ms p95 是收入 KPI 而不是 SLO 漂亮數字。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性&lt;/a> 把 latency 翻成業務 metric 的責任。&lt;/li>
&lt;li>&lt;strong>微服務 + 容器編排是擴容粒度的前置&lt;/strong>：遷移前的單體系統「擴容」只能複製整套系統、成本曲線陡峭。EKS 拆解後可以針對熱點服務（投注引擎、結算引擎）獨立擴容、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 的逐層定位直接對齊。&lt;/li>
&lt;/ol>
&lt;p>需要警惕的判讀盲點：54000 TPS @ 25ms 是 &lt;em>公開的成功數字&lt;/em>、不是「永遠都這樣」的承諾。AI 預測模型必然有預測誤差、AWS Infrastructure Event Management 也是事件型服務、不是平台預設。這類案例適合作為「目標可達性」的存在證明、不適合直接套用為自家服務的容量假設。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>把賽事 schedule 灌進 capacity forecast&lt;/strong>：在事件已知的條件下、預先把 baseline 拉高、避免 AI 模型在零起跑時擴容。對應 EC2 Auto Scaling 的 &lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-scheduled-scaling.html">scheduled scaling&lt;/a> + predictive scaling 雙模。&lt;/li>
&lt;li>&lt;strong>AI 模型輸入要包含領域訊號&lt;/strong>：通用 ML autoscaler 用 CPU / latency 預測、領域 autoscaler 還會用 &lt;em>賽事重要性&lt;/em>、&lt;em>投注量歷史曲線&lt;/em>、&lt;em>下注玩家集中度&lt;/em> 等業務訊號。這層讓擴容時機從反應式變成預測式。&lt;/li>
&lt;li>&lt;strong>熱點服務獨立擴容、不是整體擴容&lt;/strong>：投注引擎跟結算引擎的峰值時間不一致（投注集中在賽前 + 比賽中、結算集中在賽後）、單獨擴容比整體擴容省 25%+ 成本。&lt;/li>
&lt;li>&lt;strong>AWS Infrastructure Event Management 等廠商支援服務&lt;/strong>：在年度重大事件可以申請（World Cup、Olympic、Black Friday 等）、提供 pre-scaling 與專屬監控通道。這在 GCP / Azure 也有對等服務（GCP Customer Care Premium、Azure Event Management Support）。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP GKE + Vertical Pod Autoscaler + 自家 ML 預測、Azure AKS + KEDA + Azure ML 預測、自建 Kubernetes + Karpenter + Prometheus 推導模型都可以實作同樣的「預測 + 擴容」模式。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「事件型不可預期峰值」的工程做法。體育博彩流量的形狀跟 Prime Day 不同 — 峰值會在賽事的特定瞬間（進球、最後一分鐘）爆量、單一賽事內可能有多次脈衝、跨賽事的時間點難以提前數月排程。GR8 Tech 在 2022 FIFA World Cup 期間達到零停機營運、是這類負載形狀的有效參考。</p>
<h2 id="觀察">觀察</h2>
<p>GR8 Tech 從本地基礎設施遷移到 AWS、重建為微服務架構後的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/gr8-tech-case-study/">GR8 Tech case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>遷移前狀況</th>
          <th>遷移後峰值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>投注延遲</td>
          <td>賽事高峰期額外延遲 2-3 秒</td>
          <td>25 ms p95</td>
      </tr>
      <tr>
          <td>結算吞吐</td>
          <td>（未公開）</td>
          <td>每分鐘 100 萬次投注結算</td>
      </tr>
      <tr>
          <td>交易吞吐</td>
          <td>（未公開）</td>
          <td>54000 TPS @ 25ms p95</td>
      </tr>
      <tr>
          <td>同時在線</td>
          <td>-</td>
          <td>200,000+ 同時使用者</td>
      </tr>
      <tr>
          <td>投注吞吐</td>
          <td>-</td>
          <td>每分鐘 80,000 次體育投注</td>
      </tr>
      <tr>
          <td>可用性</td>
          <td>-</td>
          <td>99.95% uptime</td>
      </tr>
      <tr>
          <td>成本彈性</td>
          <td>固定預配置</td>
          <td>需求降低時成本下降 25%</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Amazon EKS（Kubernetes 容器編排、跨雲端與本地）、Amazon EC2（compute）、Amazon S3 與 Amazon EBS（儲存）、AWS Auto Scaling 結合 <strong>GR8 Tech 自家 AI 預測模型</strong>、AWS Infrastructure Event Management（重大賽事支援）。</p>
<p>擴展範圍：「Scaled to 15 markets using AWS」。事件覆蓋：2022 FIFA World Cup 期間零停機。</p>
<h2 id="判讀">判讀</h2>
<p>GR8 Tech 的工程做法揭露三個事件型峰值的判讀重點。</p>
<ol>
<li><strong>不可預期 ≠ 不可預測</strong>：賽事「何時開打」是已知的（schedule 提前公告）、「賽事內何時爆量」是未知的（進球、加時、最後一分鐘）。AI 預測模型不是預測「會不會有峰值」、而是預測「峰值在 60 秒內可能多大」、把擴容窗口縮短到反應時間之內。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的「預測時間尺度」軸。</li>
<li><strong>延遲是業務指標、不是技術指標</strong>：「2-3 秒額外延遲」直接造成「投注失敗、客戶流失」。25ms p95 是收入 KPI 而不是 SLO 漂亮數字。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性</a> 把 latency 翻成業務 metric 的責任。</li>
<li><strong>微服務 + 容器編排是擴容粒度的前置</strong>：遷移前的單體系統「擴容」只能複製整套系統、成本曲線陡峭。EKS 拆解後可以針對熱點服務（投注引擎、結算引擎）獨立擴容、跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的逐層定位直接對齊。</li>
</ol>
<p>需要警惕的判讀盲點：54000 TPS @ 25ms 是 <em>公開的成功數字</em>、不是「永遠都這樣」的承諾。AI 預測模型必然有預測誤差、AWS Infrastructure Event Management 也是事件型服務、不是平台預設。這類案例適合作為「目標可達性」的存在證明、不適合直接套用為自家服務的容量假設。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>把賽事 schedule 灌進 capacity forecast</strong>：在事件已知的條件下、預先把 baseline 拉高、避免 AI 模型在零起跑時擴容。對應 EC2 Auto Scaling 的 <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-scheduled-scaling.html">scheduled scaling</a> + predictive scaling 雙模。</li>
<li><strong>AI 模型輸入要包含領域訊號</strong>：通用 ML autoscaler 用 CPU / latency 預測、領域 autoscaler 還會用 <em>賽事重要性</em>、<em>投注量歷史曲線</em>、<em>下注玩家集中度</em> 等業務訊號。這層讓擴容時機從反應式變成預測式。</li>
<li><strong>熱點服務獨立擴容、不是整體擴容</strong>：投注引擎跟結算引擎的峰值時間不一致（投注集中在賽前 + 比賽中、結算集中在賽後）、單獨擴容比整體擴容省 25%+ 成本。</li>
<li><strong>AWS Infrastructure Event Management 等廠商支援服務</strong>：在年度重大事件可以申請（World Cup、Olympic、Black Friday 等）、提供 pre-scaling 與專屬監控通道。這在 GCP / Azure 也有對等服務（GCP Customer Care Premium、Azure Event Management Support）。</li>
</ol>
<p>跨平台等效：GCP GKE + Vertical Pod Autoscaler + 自家 ML 預測、Azure AKS + KEDA + Azure ML 預測、自建 Kubernetes + Karpenter + Prometheus 推導模型都可以實作同樣的「預測 + 擴容」模式。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想做事件型峰值的容量預測 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想用 AI / ML 做預測式擴容 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9 Performance Improvement Loop</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性</a></li>
<li>想拆解微服務以便獨立擴容 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
<li>對照不同形狀的峰值 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a>（可預期極端峰值）/ <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a>（無峰值低延遲）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/gr8-tech-case-study/">GR8 Tech Achieves High Performance and Scalability with Data Center Migration to AWS</a></li>
<li><a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-predictive-scaling.html">Predictive scaling for Amazon EC2 Auto Scaling</a></li>
<li><a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-scheduled-scaling.html">Scheduled scaling for Amazon EC2 Auto Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>2.C2 Meta：mcrouter 與跨區快取路由</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/</guid><description>&lt;p>這個案例的核心責任是說明快取規模變大後，路由層本身會成為選型主題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>mcrouter 被用來統一處理大量 memcached 流量與跨叢集路由，代表快取已從局部優化變成平台層能力。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當快取服務跨區、跨叢集且請求量極高時，應把路由策略、故障切換與運維一致性視為主議題。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把 client 端散落邏輯收斂到路由層。&lt;/li>
&lt;li>把跨區路由與故障策略標準化。&lt;/li>
&lt;li>用可觀測訊號監控路由品質與新鮮度。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發 Redis 邊界&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 service discovery&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2014/09/15/web/introducing-mcrouter-a-memcached-protocol-router-for-scaling-memcached-deployments/">Introducing mcrouter&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取規模變大後，路由層本身會成為選型主題。</p>
<h2 id="觀察">觀察</h2>
<p>mcrouter 被用來統一處理大量 memcached 流量與跨叢集路由，代表快取已從局部優化變成平台層能力。</p>
<h2 id="判讀">判讀</h2>
<p>當快取服務跨區、跨叢集且請求量極高時，應把路由策略、故障切換與運維一致性視為主議題。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把 client 端散落邏輯收斂到路由層。</li>
<li>把跨區路由與故障策略標準化。</li>
<li>用可觀測訊號監控路由品質與新鮮度。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發 Redis 邊界</a> 與 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 service discovery</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2014/09/15/web/introducing-mcrouter-a-memcached-protocol-router-for-scaling-memcached-deployments/">Introducing mcrouter</a></li>
</ul>
]]></content:encoded></item><item><title>3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/</guid><description>&lt;p>這個案例的核心責任是把 broker 遷移拆成平台責任、運維責任與資料責任三層。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>CloudHealth 由自管 Kafka 遷移到 Amazon MSK，過程涵蓋 topic、存取控制、觀測與遷移執行節奏。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這類轉換的實際風險在 ACL、topic policy、client 相容性與 cutover 節奏，服務名稱本身反而是次要問題。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先建立新叢集治理基線（ACL、觀測、部署）。&lt;/li>
&lt;li>分批 topic 遷移並持續監測 lag/錯誤。&lt;/li>
&lt;li>把回退與流量切換條件寫成明確門檻。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/big-data/how-vmware-tanzu-cloudhealth-migrated-from-self-managed-kafka-to-amazon-msk/">VMware CloudHealth Kafka to MSK&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 broker 遷移拆成平台責任、運維責任與資料責任三層。</p>
<h2 id="觀察">觀察</h2>
<p>CloudHealth 由自管 Kafka 遷移到 Amazon MSK，過程涵蓋 topic、存取控制、觀測與遷移執行節奏。</p>
<h2 id="判讀">判讀</h2>
<p>這類轉換的實際風險在 ACL、topic policy、client 相容性與 cutover 節奏，服務名稱本身反而是次要問題。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先建立新叢集治理基線（ACL、觀測、部署）。</li>
<li>分批 topic 遷移並持續監測 lag/錯誤。</li>
<li>把回退與流量切換條件寫成明確門檻。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/big-data/how-vmware-tanzu-cloudhealth-migrated-from-self-managed-kafka-to-amazon-msk/">VMware CloudHealth Kafka to MSK</a></li>
</ul>
]]></content:encoded></item><item><title>5.C2 Condé Nast：EKS 平台整併與標準化</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/</guid><description>&lt;p>這個案例的核心責任是說明平台整併常是組織治理問題，技術選型只是其中一層。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Condé Nast 旗下多個小團隊各自維護獨立的 Kubernetes 環境，各團隊使用不同的 Kubernetes 版本、操作模型、部署流程與存取模式。Self-managed Kubernetes 跑在 EC2 上，每個團隊自行維護 control plane、AMI、安全修補與 IAM credential 管理（使用 kube2iam 等開源工具）。&lt;/p>
&lt;p>整併後成立一個 single global platform team，遷移到 Amazon EKS。技術棧標準化為 Bottlerocket OS、VPC CNI、AWS Load Balancer Controller、IRSA（IAM Roles for Service Accounts）。Multi-tenancy 用 Kubernetes namespace 隔離，搭配 resource quotas 與 limits 防止 noisy neighbor。&lt;/p>
&lt;p>結果面：搭配 CloudFront 與 AWS Global Accelerator 後，end user latency 降低達 50%。團隊可以在 guardrails 內快速建立新叢集，operational overhead 顯著降低。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>平台碎片化的代價分兩層。表面層是重工——每個團隊各自處理安全修補、版本升級、credential 管理，相同工作做了 N 遍。深層是一致性缺失——不同團隊的安全基線不同，某個團隊漏修的 CVE 可能成為整個組織的入口。&lt;/p>
&lt;p>整併的工程價值在於把「每個團隊各自解決平台問題」變成「平台團隊解決一次、所有團隊共用」。這個轉換的前提是平台團隊能提供足夠彈性的 multi-tenancy 模型——resource quotas 防止資源搶占、namespace 隔離防止互相影響、IRSA 讓每個 workload 有獨立的 AWS 權限而非共用 node-level credential。&lt;/p>
&lt;p>kube2iam → IRSA 的切換是這個案例中安全基線提升最顯著的一步。kube2iam 依賴 iptables 攔截 metadata endpoint，在多租戶環境下有 race condition 與 credential leak 風險。IRSA 用 OIDC federation 讓每個 service account 直接取得 scoped IAM role，消除了 node-level 的 credential 共用。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>盤點既有叢集的差異維度&lt;/strong>：Kubernetes 版本、CNI、ingress controller、credential 管理方式、部署流程、監控工具。差異清單是遷移計畫的輸入。&lt;/li>
&lt;li>&lt;strong>定義統一平台基線&lt;/strong>：選定 EKS + Bottlerocket + VPC CNI + IRSA 作為所有叢集的共通配置。基線要涵蓋安全（pod 唯讀 filesystem、禁 root）、資源（quotas、limits）、網路（CNI、LB controller）。&lt;/li>
&lt;li>&lt;strong>用 namespace multi-tenancy 取代獨立叢集&lt;/strong>：每個團隊一個 namespace，resource quotas 限制資源用量。這比一個團隊一個叢集的運維成本低，但需要在 namespace 層級做好隔離（NetworkPolicy、ResourceQuota、RBAC scope）。&lt;/li>
&lt;li>&lt;strong>漸進切換業務流量&lt;/strong>：按 region / 市場分批遷移，每批遷移後驗證 latency 與 error rate。搭配 CloudFront 做 edge 層的流量管理。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%a4%a7%e8%a6%8f%e6%a8%a1-k8s-%e7%9a%84%e8%a8%ad%e8%a8%88%e5%8f%96%e6%8d%a8" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 大規模 K8s 的設計取捨&lt;/a>：single-cluster multi-namespace 的治理單位選擇&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界&lt;/a>：global platform team 的職責重訂&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract&lt;/a>：AWS LB Controller + CloudFront 的流量入口配置&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/containers/how-conde-nast-modernized-its-container-platform-on-amazon-elastic-kubernetes-service/">How Condé Nast modernized its container platform on Amazon EKS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台整併常是組織治理問題，技術選型只是其中一層。</p>
<h2 id="觀察">觀察</h2>
<p>Condé Nast 旗下多個小團隊各自維護獨立的 Kubernetes 環境，各團隊使用不同的 Kubernetes 版本、操作模型、部署流程與存取模式。Self-managed Kubernetes 跑在 EC2 上，每個團隊自行維護 control plane、AMI、安全修補與 IAM credential 管理（使用 kube2iam 等開源工具）。</p>
<p>整併後成立一個 single global platform team，遷移到 Amazon EKS。技術棧標準化為 Bottlerocket OS、VPC CNI、AWS Load Balancer Controller、IRSA（IAM Roles for Service Accounts）。Multi-tenancy 用 Kubernetes namespace 隔離，搭配 resource quotas 與 limits 防止 noisy neighbor。</p>
<p>結果面：搭配 CloudFront 與 AWS Global Accelerator 後，end user latency 降低達 50%。團隊可以在 guardrails 內快速建立新叢集，operational overhead 顯著降低。</p>
<h2 id="判讀">判讀</h2>
<p>平台碎片化的代價分兩層。表面層是重工——每個團隊各自處理安全修補、版本升級、credential 管理，相同工作做了 N 遍。深層是一致性缺失——不同團隊的安全基線不同，某個團隊漏修的 CVE 可能成為整個組織的入口。</p>
<p>整併的工程價值在於把「每個團隊各自解決平台問題」變成「平台團隊解決一次、所有團隊共用」。這個轉換的前提是平台團隊能提供足夠彈性的 multi-tenancy 模型——resource quotas 防止資源搶占、namespace 隔離防止互相影響、IRSA 讓每個 workload 有獨立的 AWS 權限而非共用 node-level credential。</p>
<p>kube2iam → IRSA 的切換是這個案例中安全基線提升最顯著的一步。kube2iam 依賴 iptables 攔截 metadata endpoint，在多租戶環境下有 race condition 與 credential leak 風險。IRSA 用 OIDC federation 讓每個 service account 直接取得 scoped IAM role，消除了 node-level 的 credential 共用。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>盤點既有叢集的差異維度</strong>：Kubernetes 版本、CNI、ingress controller、credential 管理方式、部署流程、監控工具。差異清單是遷移計畫的輸入。</li>
<li><strong>定義統一平台基線</strong>：選定 EKS + Bottlerocket + VPC CNI + IRSA 作為所有叢集的共通配置。基線要涵蓋安全（pod 唯讀 filesystem、禁 root）、資源（quotas、limits）、網路（CNI、LB controller）。</li>
<li><strong>用 namespace multi-tenancy 取代獨立叢集</strong>：每個團隊一個 namespace，resource quotas 限制資源用量。這比一個團隊一個叢集的運維成本低，但需要在 namespace 層級做好隔離（NetworkPolicy、ResourceQuota、RBAC scope）。</li>
<li><strong>漸進切換業務流量</strong>：按 region / 市場分批遷移，每批遷移後驗證 latency 與 error rate。搭配 CloudFront 做 edge 層的流量管理。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%a4%a7%e8%a6%8f%e6%a8%a1-k8s-%e7%9a%84%e8%a8%ad%e8%a8%88%e5%8f%96%e6%8d%a8" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 大規模 K8s 的設計取捨</a>：single-cluster multi-namespace 的治理單位選擇</li>
<li><a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>：global platform team 的職責重訂</li>
<li><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a>：AWS LB Controller + CloudFront 的流量入口配置</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/containers/how-conde-nast-modernized-its-container-platform-on-amazon-elastic-kubernetes-service/">How Condé Nast modernized its container platform on Amazon EKS</a></li>
</ul>
]]></content:encoded></item><item><title>7.C2 Cloudflare：2023 Control-plane Token 事件</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/</guid><description>&lt;p>這個案例的核心責任是把控制面 token 風險落到 secret lifecycle 與權限邊界治理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>控制面 token 事件顯示機器憑證若治理不足，會形成跨服務高權限風險。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這類問題的根因是 token 生命週期、最小權限與審計證據鏈未對齊，單一憑證洩漏只是觸發點。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>用工作負載身份替代長期共享 token。&lt;/li>
&lt;li>強制 token rotation 與細粒度 scope。&lt;/li>
&lt;li>把憑證事件寫入 release gate 與 incident triage。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 secrets and machine credential governance&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 supply chain integrity&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/cloudflare-incident-on-january-24th-2023/">Cloudflare incident on January 24, 2023&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把控制面 token 風險落到 secret lifecycle 與權限邊界治理。</p>
<h2 id="觀察">觀察</h2>
<p>控制面 token 事件顯示機器憑證若治理不足，會形成跨服務高權限風險。</p>
<h2 id="判讀">判讀</h2>
<p>這類問題的根因是 token 生命週期、最小權限與審計證據鏈未對齊，單一憑證洩漏只是觸發點。</p>
<h2 id="策略">策略</h2>
<ol>
<li>用工作負載身份替代長期共享 token。</li>
<li>強制 token rotation 與細粒度 scope。</li>
<li>把憑證事件寫入 release gate 與 incident triage。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 secrets and machine credential governance</a> 與 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 supply chain integrity</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/cloudflare-incident-on-january-24th-2023/">Cloudflare incident on January 24, 2023</a></li>
</ul>
]]></content:encoded></item><item><title>AWS 2021 US-EAST-1 Control Plane Degradation</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/</guid><description>&lt;p>2021 年 AWS us-east-1 事件的核心教訓是：控制面退化不一定來自服務程式錯誤，內部網路壓力也能讓 API 與依賴鏈條同時失真。這類事故要先確認控制面健康，再決定是否進行服務層回退。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>AWS 在 2021-12-07 發生 us-east-1 多服務退化事件。官方資訊指出，內部網路裝置的異常行為導致這個區域的 API 請求與內部服務通訊壅塞，進而造成多個服務管理與控制面能力受影響。部分資料面能力可用，但控制面操作、狀態回報與恢復節奏出現延遲。&lt;/p>
&lt;p>這類事故的難點在於，使用者看到的是「很多服務一起怪」，而工程上真正要先判斷的是：共同依賴是否先失真。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多服務 API 錯誤率同時上升&lt;/td>
 &lt;td>共享控制面或內部網路層可能失真&lt;/td>
 &lt;td>優先調查共用控制平面，不先分散逐服務排障&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>控制操作延遲遠高於資料讀寫&lt;/td>
 &lt;td>控制面與資料面可用性不同步&lt;/td>
 &lt;td>對外通訊要分清 control/data plane 差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>區域集中異常（us-east-1）&lt;/td>
 &lt;td>區域依賴與路由聚集形成單點風險&lt;/td>
 &lt;td>啟動跨區降載或備援策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>狀態更新節奏出現抖動&lt;/td>
 &lt;td>事故資訊供應鏈本身受影響&lt;/td>
 &lt;td>建立固定 cadence 與替代更新通道&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>區域內部網路層出現異常與壅塞。&lt;/li>
&lt;li>控制面 API 與內部依賴通訊受阻。&lt;/li>
&lt;li>多服務管理能力與狀態回報受到影響。&lt;/li>
&lt;li>部分服務資料面仍可運作，但操作與恢復節奏失真。&lt;/li>
&lt;li>團隊逐步收斂網路壓力並恢復控制面可用性。&lt;/li>
&lt;/ol>
&lt;p>這條路徑顯示：真正的擴散點在 shared internal network + control plane，不是某個單一服務程式。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Control/Data plane 分離判讀&lt;/td>
 &lt;td>對外敘述常把兩者混在一起&lt;/td>
 &lt;td>在通訊與 runbook 明確區分控制面與資料面狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>區域依賴治理&lt;/td>
 &lt;td>單區域控制面異常可牽動多服務&lt;/td>
 &lt;td>把跨區備援與降載條件納入 release 與 incident gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared network health 訊號治理&lt;/td>
 &lt;td>內部網路異常訊號未被快速上提&lt;/td>
 &lt;td>補 shared infrastructure 指標到 [4.20 evidence package]&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident communication cadence&lt;/td>
 &lt;td>事故中更新節奏易受狀態不完整影響&lt;/td>
 &lt;td>固定 cadence，並保留「已知 / 未知 / 下一更新時間」欄位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>觀測證據包： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>&lt;/li>
&lt;li>可觀測性 operating model： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model&lt;/a>&lt;/li>
&lt;li>可靠性準備度： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 Reliability Readiness Review&lt;/a>&lt;/li>
&lt;li>止血與回復： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>影響評估： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/message/12721/">Summary of the AWS service event in the Northern Virginia (US-EAST-1) Region&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2021 年 AWS us-east-1 事件的核心教訓是：控制面退化不一定來自服務程式錯誤，內部網路壓力也能讓 API 與依賴鏈條同時失真。這類事故要先確認控制面健康，再決定是否進行服務層回退。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>AWS 在 2021-12-07 發生 us-east-1 多服務退化事件。官方資訊指出，內部網路裝置的異常行為導致這個區域的 API 請求與內部服務通訊壅塞，進而造成多個服務管理與控制面能力受影響。部分資料面能力可用，但控制面操作、狀態回報與恢復節奏出現延遲。</p>
<p>這類事故的難點在於，使用者看到的是「很多服務一起怪」，而工程上真正要先判斷的是：共同依賴是否先失真。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多服務 API 錯誤率同時上升</td>
          <td>共享控制面或內部網路層可能失真</td>
          <td>優先調查共用控制平面，不先分散逐服務排障</td>
      </tr>
      <tr>
          <td>控制操作延遲遠高於資料讀寫</td>
          <td>控制面與資料面可用性不同步</td>
          <td>對外通訊要分清 control/data plane 差異</td>
      </tr>
      <tr>
          <td>區域集中異常（us-east-1）</td>
          <td>區域依賴與路由聚集形成單點風險</td>
          <td>啟動跨區降載或備援策略</td>
      </tr>
      <tr>
          <td>狀態更新節奏出現抖動</td>
          <td>事故資訊供應鏈本身受影響</td>
          <td>建立固定 cadence 與替代更新通道</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>區域內部網路層出現異常與壅塞。</li>
<li>控制面 API 與內部依賴通訊受阻。</li>
<li>多服務管理能力與狀態回報受到影響。</li>
<li>部分服務資料面仍可運作，但操作與恢復節奏失真。</li>
<li>團隊逐步收斂網路壓力並恢復控制面可用性。</li>
</ol>
<p>這條路徑顯示：真正的擴散點在 shared internal network + control plane，不是某個單一服務程式。</p>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Control/Data plane 分離判讀</td>
          <td>對外敘述常把兩者混在一起</td>
          <td>在通訊與 runbook 明確區分控制面與資料面狀態</td>
      </tr>
      <tr>
          <td>區域依賴治理</td>
          <td>單區域控制面異常可牽動多服務</td>
          <td>把跨區備援與降載條件納入 release 與 incident gate</td>
      </tr>
      <tr>
          <td>Shared network health 訊號治理</td>
          <td>內部網路異常訊號未被快速上提</td>
          <td>補 shared infrastructure 指標到 [4.20 evidence package]</td>
      </tr>
      <tr>
          <td>Incident communication cadence</td>
          <td>事故中更新節奏易受狀態不完整影響</td>
          <td>固定 cadence，並保留「已知 / 未知 / 下一更新時間」欄位</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>觀測證據包： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>可觀測性 operating model： <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a></li>
<li>可靠性準備度： <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 Reliability Readiness Review</a></li>
<li>止血與回復： <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 Containment / Recovery Strategy</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>影響評估： <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/message/12721/">Summary of the AWS service event in the Northern Virginia (US-EAST-1) Region</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare 2023 Control Plane Token Incident</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/</guid><description>&lt;p>2023 年 Cloudflare control-plane 事故的核心教訓是：身份與憑證類變更一旦跨產品共用，單點錯誤會變成系統級連鎖故障。這類事故要先切的是信任邊界，不是先做流量微調。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cloudflare 在 2023-01-24 經歷 service token 相關變更問題，造成內外部控制面能力受影響，連帶影響多個產品面向。事件本質是控制面身份機制失效，並透過共用依賴擴散。&lt;/p>
&lt;p>這類事故的危險在於症狀看起來像多個服務同時不穩，但根因其實是同一個共享身份控制點。若沒有先識別 shared dependency，排障會被切成很多局部問題，恢復速度會顯著下降。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多產品同時出現驗證/授權異常&lt;/td>
 &lt;td>共享身份或憑證控制點可能失效&lt;/td>
 &lt;td>優先檢查 token / policy 最新變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗集中在控制面 API&lt;/td>
 &lt;td>問題偏向控制面，不是資料面容量瓶頸&lt;/td>
 &lt;td>啟動控制面優先處理，不先做業務層調參&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>局部回復但整體仍不穩&lt;/td>
 &lt;td>依賴鏈條有殘留錯誤狀態&lt;/td>
 &lt;td>補 dependency-by-dependency 驗證清單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後錯誤快速下降&lt;/td>
 &lt;td>變更與故障關聯度高&lt;/td>
 &lt;td>立即凍結同批身份變更與關聯部署&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故中責任邊界模糊&lt;/td>
 &lt;td>ownership 與交接規則不足&lt;/td>
 &lt;td>指派 single incident owner 與決策記錄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>控制面 token/身份相關變更進入生產環境。&lt;/li>
&lt;li>共享身份依賴開始出現授權或驗證失效。&lt;/li>
&lt;li>多個產品面的控制操作受阻，形成連鎖症狀。&lt;/li>
&lt;li>團隊透過回退與修正策略逐步收斂。&lt;/li>
&lt;li>事件後需回寫身份變更治理與事故交接流程。&lt;/li>
&lt;/ol>
&lt;p>這條路徑顯示：擴散關鍵在 shared identity dependency，不在單一產品流量高低。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>身份變更審核&lt;/td>
 &lt;td>token/policy 變更前缺少跨產品影響分析&lt;/td>
 &lt;td>補 shared dependency impact checklist&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發布策略&lt;/td>
 &lt;td>身份控制面變更缺少逐層 rollout&lt;/td>
 &lt;td>先低風險範圍啟用，再逐步擴大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故啟動條件&lt;/td>
 &lt;td>多產品異常時未即時指向 shared root&lt;/td>
 &lt;td>新增「多產品授權異常」的快速升級條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision log&lt;/td>
 &lt;td>假設、回退條件與責任分工不夠明確&lt;/td>
 &lt;td>事中強制記錄假設、證據、回退門檻與 owner&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence write-back&lt;/td>
 &lt;td>教訓停在事件敘述&lt;/td>
 &lt;td>回寫 &lt;code>07&lt;/code> 身分邊界治理、&lt;code>08&lt;/code> decision log、&lt;code>04&lt;/code> 控制面健康訊號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Handoff protocol&lt;/td>
 &lt;td>長事故交接易遺失上下文&lt;/td>
 &lt;td>使用固定 handoff 模板，包含當前假設、已驗證路徑、未完成風險與下一步責任&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>身分邊界與權限治理： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 Identity Access Boundary&lt;/a>&lt;/li>
&lt;li>規則推送安全閘門： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a>&lt;/li>
&lt;li>事故決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>控制面訊號治理： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/cloudflare-incident-on-january-24th-2023/">Cloudflare incident on January 24, 2023&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2023 年 Cloudflare control-plane 事故的核心教訓是：身份與憑證類變更一旦跨產品共用，單點錯誤會變成系統級連鎖故障。這類事故要先切的是信任邊界，不是先做流量微調。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Cloudflare 在 2023-01-24 經歷 service token 相關變更問題，造成內外部控制面能力受影響，連帶影響多個產品面向。事件本質是控制面身份機制失效，並透過共用依賴擴散。</p>
<p>這類事故的危險在於症狀看起來像多個服務同時不穩，但根因其實是同一個共享身份控制點。若沒有先識別 shared dependency，排障會被切成很多局部問題，恢復速度會顯著下降。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多產品同時出現驗證/授權異常</td>
          <td>共享身份或憑證控制點可能失效</td>
          <td>優先檢查 token / policy 最新變更</td>
      </tr>
      <tr>
          <td>失敗集中在控制面 API</td>
          <td>問題偏向控制面，不是資料面容量瓶頸</td>
          <td>啟動控制面優先處理，不先做業務層調參</td>
      </tr>
      <tr>
          <td>局部回復但整體仍不穩</td>
          <td>依賴鏈條有殘留錯誤狀態</td>
          <td>補 dependency-by-dependency 驗證清單</td>
      </tr>
      <tr>
          <td>回退後錯誤快速下降</td>
          <td>變更與故障關聯度高</td>
          <td>立即凍結同批身份變更與關聯部署</td>
      </tr>
      <tr>
          <td>事故中責任邊界模糊</td>
          <td>ownership 與交接規則不足</td>
          <td>指派 single incident owner 與決策記錄</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>控制面 token/身份相關變更進入生產環境。</li>
<li>共享身份依賴開始出現授權或驗證失效。</li>
<li>多個產品面的控制操作受阻，形成連鎖症狀。</li>
<li>團隊透過回退與修正策略逐步收斂。</li>
<li>事件後需回寫身份變更治理與事故交接流程。</li>
</ol>
<p>這條路徑顯示：擴散關鍵在 shared identity dependency，不在單一產品流量高低。</p>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身份變更審核</td>
          <td>token/policy 變更前缺少跨產品影響分析</td>
          <td>補 shared dependency impact checklist</td>
      </tr>
      <tr>
          <td>發布策略</td>
          <td>身份控制面變更缺少逐層 rollout</td>
          <td>先低風險範圍啟用，再逐步擴大</td>
      </tr>
      <tr>
          <td>事故啟動條件</td>
          <td>多產品異常時未即時指向 shared root</td>
          <td>新增「多產品授權異常」的快速升級條件</td>
      </tr>
      <tr>
          <td>Decision log</td>
          <td>假設、回退條件與責任分工不夠明確</td>
          <td>事中強制記錄假設、證據、回退門檻與 owner</td>
      </tr>
      <tr>
          <td>Evidence write-back</td>
          <td>教訓停在事件敘述</td>
          <td>回寫 <code>07</code> 身分邊界治理、<code>08</code> decision log、<code>04</code> 控制面健康訊號</td>
      </tr>
      <tr>
          <td>Handoff protocol</td>
          <td>長事故交接易遺失上下文</td>
          <td>使用固定 handoff 模板，包含當前假設、已驗證路徑、未完成風險與下一步責任</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身分邊界與權限治理： <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 Identity Access Boundary</a></li>
<li>規則推送安全閘門： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a></li>
<li>事故決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>控制面訊號治理： <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/cloudflare-incident-on-january-24th-2023/">Cloudflare incident on January 24, 2023</a></li>
</ul>
]]></content:encoded></item><item><title>Gaming：高峰流量下的訊號新鮮度與 Cardinality</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/</guid><description>&lt;p>本案例的核心責任是避免高峰流量讓觀測系統本身失真。若訊號延遲與 cardinality 膨脹失控，值班決策會落在過期資料上。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一個線上多人遊戲平台，日活躍使用者約 50 萬人。每逢賽季開跑或限時活動，同時在線人數在 30 分鐘內從平日基線暴增 8-10 倍，matchmaking 服務的 request rate 從 5k/s 衝到 50k/s，遊戲伺服器同時運行的 match instance 從數千增到數萬。&lt;/p>
&lt;p>觀測系統在平日運作良好 — Prometheus 單機 scrape 500 萬 active series、Grafana dashboard 查詢秒級回應、告警在 1 分鐘內觸發。但每次活動開跑時，觀測系統本身開始劣化：dashboard 查詢從秒級變成分鐘級、告警延遲 5 分鐘以上才送到、部分 metric 直接消失。值班工程師在最需要觀測的時刻失去了可信訊號。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="cardinality-爆炸">Cardinality 爆炸&lt;/h3>
&lt;p>平日的 metric label 設計包含 &lt;code>match_id&lt;/code>、&lt;code>player_id&lt;/code> 跟 &lt;code>server_instance&lt;/code>。平日 active series 約 500 萬，活動開跑後 match 跟 player 數量暴增，active series 在 30 分鐘內衝到 2000 萬。Prometheus 的 head block 記憶體從 20 GB 暴增到 80 GB，超過機器 64 GB 上限，觸發 OOM kill。&lt;/p>
&lt;p>OOM 後 Prometheus 重啟需要 replay WAL，這段時間（5-15 分鐘）完全沒有 metric。活動最需要觀測的前 30 分鐘，觀測系統反而停擺。&lt;/p>
&lt;h3 id="scrape-freshness-延遲">Scrape freshness 延遲&lt;/h3>
&lt;p>即使 Prometheus 沒 OOM，大量 target 的 scrape 時間也會拉長。平日每輪 scrape 15 秒完成，活動期間拉長到 60-90 秒。Scrape interval 設定 30 秒時，下一輪 scrape 在上一輪還沒結束時就啟動，造成 sample 丟失跟時間錯位。Dashboard 上看到的數字可能延遲 2-3 分鐘，值班人員基於過期數據做判斷。&lt;/p>
&lt;h3 id="alert-閾值失真">Alert 閾值失真&lt;/h3>
&lt;p>告警規則基於平日 baseline 設定 — 例如 &lt;code>error_rate &amp;gt; 1%&lt;/code> 觸發。活動期間的 error rate 波動更大（matchmaking 短暫排隊造成的 timeout 增加是預期行為），平日閾值在活動期間持續觸發 false positive。值班人員開始 ignore alert，真正的問題（伺服器記憶體洩漏）被淹沒在噪音中。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="cardinality-guardrail">Cardinality guardrail&lt;/h3>
&lt;p>把高 cardinality label 從 real-time metric 移除。&lt;code>match_id&lt;/code> 和 &lt;code>player_id&lt;/code> 不再作為 Prometheus label，改為 log 和 trace 的欄位。Real-time metric 只保留 &lt;code>region&lt;/code>、&lt;code>server_pool&lt;/code>、&lt;code>game_mode&lt;/code> 等低 cardinality 維度。&lt;/p>
&lt;p>需要 per-match 或 per-player 分析時，走 log analytics pipeline（非 real-time，延遲 5-10 分鐘可接受）。這讓 Prometheus 的 active series 在活動期間從 2000 萬降到 800 萬，留在單機可承受範圍。&lt;/p>
&lt;h3 id="pre-aggregation-recording-rules">Pre-aggregation recording rules&lt;/h3>
&lt;p>為活動期間最常查的 pattern（per-region error rate、matchmaking queue depth、server utilization）建立 recording rules。Recording rules 在 Prometheus server 端預先計算，dashboard 查詢直接讀預計算結果，避免 heavy aggregation query 在活動期間拖慢 Prometheus。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是避免高峰流量讓觀測系統本身失真。若訊號延遲與 cardinality 膨脹失控，值班決策會落在過期資料上。</p>
<h2 id="業務背景">業務背景</h2>
<p>一個線上多人遊戲平台，日活躍使用者約 50 萬人。每逢賽季開跑或限時活動，同時在線人數在 30 分鐘內從平日基線暴增 8-10 倍，matchmaking 服務的 request rate 從 5k/s 衝到 50k/s，遊戲伺服器同時運行的 match instance 從數千增到數萬。</p>
<p>觀測系統在平日運作良好 — Prometheus 單機 scrape 500 萬 active series、Grafana dashboard 查詢秒級回應、告警在 1 分鐘內觸發。但每次活動開跑時，觀測系統本身開始劣化：dashboard 查詢從秒級變成分鐘級、告警延遲 5 分鐘以上才送到、部分 metric 直接消失。值班工程師在最需要觀測的時刻失去了可信訊號。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="cardinality-爆炸">Cardinality 爆炸</h3>
<p>平日的 metric label 設計包含 <code>match_id</code>、<code>player_id</code> 跟 <code>server_instance</code>。平日 active series 約 500 萬，活動開跑後 match 跟 player 數量暴增，active series 在 30 分鐘內衝到 2000 萬。Prometheus 的 head block 記憶體從 20 GB 暴增到 80 GB，超過機器 64 GB 上限，觸發 OOM kill。</p>
<p>OOM 後 Prometheus 重啟需要 replay WAL，這段時間（5-15 分鐘）完全沒有 metric。活動最需要觀測的前 30 分鐘，觀測系統反而停擺。</p>
<h3 id="scrape-freshness-延遲">Scrape freshness 延遲</h3>
<p>即使 Prometheus 沒 OOM，大量 target 的 scrape 時間也會拉長。平日每輪 scrape 15 秒完成，活動期間拉長到 60-90 秒。Scrape interval 設定 30 秒時，下一輪 scrape 在上一輪還沒結束時就啟動，造成 sample 丟失跟時間錯位。Dashboard 上看到的數字可能延遲 2-3 分鐘，值班人員基於過期數據做判斷。</p>
<h3 id="alert-閾值失真">Alert 閾值失真</h3>
<p>告警規則基於平日 baseline 設定 — 例如 <code>error_rate &gt; 1%</code> 觸發。活動期間的 error rate 波動更大（matchmaking 短暫排隊造成的 timeout 增加是預期行為），平日閾值在活動期間持續觸發 false positive。值班人員開始 ignore alert，真正的問題（伺服器記憶體洩漏）被淹沒在噪音中。</p>
<h2 id="解法">解法</h2>
<h3 id="cardinality-guardrail">Cardinality guardrail</h3>
<p>把高 cardinality label 從 real-time metric 移除。<code>match_id</code> 和 <code>player_id</code> 不再作為 Prometheus label，改為 log 和 trace 的欄位。Real-time metric 只保留 <code>region</code>、<code>server_pool</code>、<code>game_mode</code> 等低 cardinality 維度。</p>
<p>需要 per-match 或 per-player 分析時，走 log analytics pipeline（非 real-time，延遲 5-10 分鐘可接受）。這讓 Prometheus 的 active series 在活動期間從 2000 萬降到 800 萬，留在單機可承受範圍。</p>
<h3 id="pre-aggregation-recording-rules">Pre-aggregation recording rules</h3>
<p>為活動期間最常查的 pattern（per-region error rate、matchmaking queue depth、server utilization）建立 recording rules。Recording rules 在 Prometheus server 端預先計算，dashboard 查詢直接讀預計算結果，避免 heavy aggregation query 在活動期間拖慢 Prometheus。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># recording rule 示例</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">peak_precompute</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">region:matchmaking_errors:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum(rate(matchmaking_errors_total[5m])) by (region)</span></span></span></code></pre></div><h3 id="signal-tiering">Signal tiering</h3>
<p>把觀測訊號分成兩層：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>訊號類型</th>
          <th>Pipeline</th>
          <th>Freshness</th>
          <th>Cardinality 限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tier 1</td>
          <td>Golden signals（latency、error rate、throughput、saturation）</td>
          <td>Prometheus real-time</td>
          <td>&lt; 30s</td>
          <td>嚴格（低 cardinality label only）</td>
      </tr>
      <tr>
          <td>Tier 2</td>
          <td>Debug signals（per-match、per-player、per-request）</td>
          <td>Log + trace analytics</td>
          <td>5-10 min</td>
          <td>無限制</td>
      </tr>
  </tbody>
</table>
<p>Tier 1 支撐告警跟即時 dashboard，保證活動期間不劣化。Tier 2 支撐事後分析跟 root cause investigation，接受延遲。</p>
<h3 id="dynamic-alert-threshold">Dynamic alert threshold</h3>
<p>活動期間啟用「高峰模式」alert profile — 調高 error rate 閾值（1% → 5%）、加長 <code>for:</code> duration（1m → 5m）、停用已知在活動期間會 false positive 的告警。高峰模式由活動排程系統自動觸發，活動結束後自動切回平日 profile。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>高 cardinality real-time</th>
          <th>分層治理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debug 即時性</td>
          <td>高（per-match real-time）</td>
          <td>低到中（per-match 延遲 5-10 min）</td>
      </tr>
      <tr>
          <td>Prometheus 穩定性</td>
          <td>低（活動期間 OOM 風險）</td>
          <td>高（active series 可控）</td>
      </tr>
      <tr>
          <td>Dashboard 回應速度</td>
          <td>活動期間劣化</td>
          <td>穩定（recording rules 預計算）</td>
      </tr>
      <tr>
          <td>告警可信度</td>
          <td>低（false positive 淹沒真問題）</td>
          <td>中到高（dynamic threshold 降噪）</td>
      </tr>
      <tr>
          <td>維護複雜度</td>
          <td>低（一套 pipeline）</td>
          <td>中（兩套 pipeline + 高峰模式切換）</td>
      </tr>
  </tbody>
</table>
<p>分層治理的核心取捨是犧牲 per-match real-time debug 能力，換取觀測系統在高峰期間的穩定。這個取捨在活動場景成立 — 活動期間最需要的是「整體是否健康」的判斷，per-match debug 在事後分析夠用。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<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 Cost Governance</a>：cardinality guardrail 的設計原則與偵測機制。</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：scrape freshness、sampling bias 與 signal tiering。</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>：real-time vs batch analytics pipeline 的分層設計。</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 Dashboard Alert</a>：dynamic alert threshold 與高峰模式切換。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>流量高峰期間 Prometheus 記憶體使用異常增長或觸發 OOM</li>
<li>Dashboard 在尖峰時段查詢變慢或 timeout，正好是最需要看的時候</li>
<li>Alert 在活動期間大量觸發但多數是 false positive，值班人員開始 ignore</li>
<li><code>prometheus_tsdb_head_series</code> 在特定時段突然暴增，結束後回落</li>
<li>Metric label 中包含高 cardinality identifier（user_id、session_id、request_id）</li>
</ul>
]]></content:encoded></item><item><title>Gaming：高峰流量與隔離邊界選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cases/gaming-peak-traffic-and-isolation/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cases/gaming-peak-traffic-and-isolation/</guid><description>&lt;p>這個案例的核心責任是把活動高峰轉成預先可驗證的容量與隔離決策。Gaming 場景的失效通常來自瞬間峰值與連線風暴疊加。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>peak burst ratio&lt;/td>
 &lt;td>尖峰是否超過模型緩衝&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>matchmaking queue lag&lt;/td>
 &lt;td>非同步鏈路是否壅塞&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reconnect storm indicator&lt;/td>
 &lt;td>回復是否放大負載&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="風險與邊界">風險與邊界&lt;/h2>
&lt;p>只追求低延遲而忽略隔離邊界，會在高峰時把單一熱點擴散成全域事故。選型時需要同時定義分流邏輯與分批恢復策略。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>把容量假設回寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>，並在 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14&lt;/a> 補多事故協調規則。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是把活動高峰轉成預先可驗證的容量與隔離決策。Gaming 場景的失效通常來自瞬間峰值與連線風暴疊加。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>peak burst ratio</td>
          <td>尖峰是否超過模型緩衝</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>matchmaking queue lag</td>
          <td>非同步鏈路是否壅塞</td>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a></td>
      </tr>
      <tr>
          <td>reconnect storm indicator</td>
          <td>回復是否放大負載</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
  </tbody>
</table>
<h2 id="風險與邊界">風險與邊界</h2>
<p>只追求低延遲而忽略隔離邊界，會在高峰時把單一熱點擴散成全域事故。選型時需要同時定義分流邏輯與分批恢復策略。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>把容量假設回寫 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a>，並在 <a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a> 補多事故協調規則。</p>
]]></content:encoded></item><item><title>Apache Kafka</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/</guid><description>&lt;p>Kafka 是 distributed event streaming platform、承擔三個責任：log-based 訊息儲存（partition + replication）、事件流分發（consumer group 各自進度）、跨系統事件總線（schema-aware contract）。設計取捨偏向「寫入即承諾、可長期保留、多 consumer 各自 replay」、broker 級可靠性與 consumer 端 idempotency 拆開、broker 不負責業務正確性。&lt;/p>
&lt;p>對「事件驅動架構、CDC、跨系統事件分發、長期保留 + replay」這條路徑、Kafka 是業界事實標準。本頁先給最短路徑、再展開日常 producer / consumer 操作與 topic 設計、最後進階治理（多租戶、跨區、自動修復）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker-compose 跑起 Kafka + KRaft、驗證 broker 健康&lt;/li>
&lt;li>用 CLI 建 topic、produce / consume 訊息、看 partition 分布&lt;/li>
&lt;li>設計 producer acks / idempotence / consumer commit 策略對齊 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics&lt;/a>&lt;/li>
&lt;li>看懂 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、ISR shrink、rebalance 訊號、定位故障層&lt;/li>
&lt;li>評估 multi-tenant、cross-region、tiered storage、self-healing 等規模化議題&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-kafka-跑起來">最短路徑：5 分鐘把 Kafka 跑起來&lt;/h2>
&lt;p>最短路徑用 KRaft 模式（取代 ZooKeeper、單節點即可跑）、避免初學者卡在 ZK 安裝。&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"># 1. 啟動 Kafka（apache/kafka 內建 KRaft、單一容器即含 broker + controller）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name kafka -p 9092:9092 apache/kafka:latest
&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"># 2. 建 topic（CLI 在容器內 /opt/kafka/bin/）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> kafka /opt/kafka/bin/kafka-topics.sh --create --topic demo --partitions &lt;span class="m">3&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> --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> kafka /opt/kafka/bin/kafka-topics.sh --describe --topic demo &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> --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證 produce / consume&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> kafka bash -c &lt;span class="s2">&amp;#34;echo hello | /opt/kafka/bin/kafka-console-producer.sh \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s2"> --topic demo --bootstrap-server localhost:9092&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> kafka /opt/kafka/bin/kafka-console-consumer.sh --topic demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --from-beginning --max-messages &lt;span class="m">1&lt;/span> --bootstrap-server localhost:9092&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑只驗證「broker 起來、能寫能讀」。實際寫程式用 producer / consumer client、見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>CLI 指令對照表（kafka-topics / kafka-configs / kafka-consumer-groups / kafka-acls）&lt;/li>
&lt;li>Producer client 配置：acks / batch.size / linger.ms / compression / enable.idempotence&lt;/li>
&lt;li>Consumer client 配置：auto.offset.reset / enable.auto.commit / max.poll.records / max.poll.interval.ms&lt;/li>
&lt;li>對應指令範例：&lt;code>kafka-topics.sh --describe&lt;/code>、&lt;code>kafka-consumer-groups.sh --describe --group &amp;lt;id&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="topic-設計">Topic 設計&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic&lt;/a> 承擔事件的邏輯邊界。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Kafka 是 distributed event streaming platform、承擔三個責任：log-based 訊息儲存（partition + replication）、事件流分發（consumer group 各自進度）、跨系統事件總線（schema-aware contract）。設計取捨偏向「寫入即承諾、可長期保留、多 consumer 各自 replay」、broker 級可靠性與 consumer 端 idempotency 拆開、broker 不負責業務正確性。</p>
<p>對「事件驅動架構、CDC、跨系統事件分發、長期保留 + replay」這條路徑、Kafka 是業界事實標準。本頁先給最短路徑、再展開日常 producer / consumer 操作與 topic 設計、最後進階治理（多租戶、跨區、自動修復）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker-compose 跑起 Kafka + KRaft、驗證 broker 健康</li>
<li>用 CLI 建 topic、produce / consume 訊息、看 partition 分布</li>
<li>設計 producer acks / idempotence / consumer commit 策略對齊 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a></li>
<li>看懂 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、ISR shrink、rebalance 訊號、定位故障層</li>
<li>評估 multi-tenant、cross-region、tiered storage、self-healing 等規模化議題</li>
</ol>
<h2 id="最短路徑5-分鐘把-kafka-跑起來">最短路徑：5 分鐘把 Kafka 跑起來</h2>
<p>最短路徑用 KRaft 模式（取代 ZooKeeper、單節點即可跑）、避免初學者卡在 ZK 安裝。</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. 啟動 Kafka（apache/kafka 內建 KRaft、單一容器即含 broker + controller）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name kafka -p 9092:9092 apache/kafka:latest
</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"># 2. 建 topic（CLI 在容器內 /opt/kafka/bin/）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">docker <span class="nb">exec</span> kafka /opt/kafka/bin/kafka-topics.sh --create --topic demo --partitions <span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker <span class="nb">exec</span> kafka /opt/kafka/bin/kafka-topics.sh --describe --topic demo <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</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="c1"># 3. 驗證 produce / consume</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">docker <span class="nb">exec</span> kafka bash -c <span class="s2">&#34;echo hello | /opt/kafka/bin/kafka-console-producer.sh \
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">  --topic demo --bootstrap-server localhost:9092&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">docker <span class="nb">exec</span> kafka /opt/kafka/bin/kafka-console-consumer.sh --topic demo <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --from-beginning --max-messages <span class="m">1</span> --bootstrap-server localhost:9092</span></span></code></pre></div><p>最短路徑只驗證「broker 起來、能寫能讀」。實際寫程式用 producer / consumer client、見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>CLI 指令對照表（kafka-topics / kafka-configs / kafka-consumer-groups / kafka-acls）</li>
<li>Producer client 配置：acks / batch.size / linger.ms / compression / enable.idempotence</li>
<li>Consumer client 配置：auto.offset.reset / enable.auto.commit / max.poll.records / max.poll.interval.ms</li>
<li>對應指令範例：<code>kafka-topics.sh --describe</code>、<code>kafka-consumer-groups.sh --describe --group &lt;id&gt;</code></li>
</ul>
<h3 id="topic-設計">Topic 設計</h3>
<p><a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a> 承擔事件的邏輯邊界。子議題：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition</a> 數規劃（並行度 vs metadata 成本）</li>
<li>Replication factor 與 min.insync.replicas（資料保護等級）</li>
<li>Retention policy（time-based vs size-based、compact vs delete）</li>
<li>Key 策略（ordering 範圍、hot partition 避免）</li>
</ul>
<h3 id="producer-與-consumer-設計">Producer 與 Consumer 設計</h3>
<p>設計決定 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a> 實際達成。子議題：</p>
<ul>
<li>Producer：acks=0/1/all 對應的可靠性取捨、idempotence、transaction 邊界</li>
<li>Consumer：commit 策略（auto vs manual）、commit 時機與 at-least-once / at-most-once 對應</li>
<li><a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a>：rebalance protocol（eager vs cooperative）、static membership</li>
<li>對應指令：producer 配置範例、consumer 配置範例、<code>kafka-consumer-groups.sh --describe</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>本段主題多數已展開為 deep article：<a href="consumer-rebalance-lag-diagnosis/">consumer rebalance 與 lag 診斷</a>、<a href="replication-isr-exactly-once/">replication / ISR / exactly-once</a>、<a href="retention-tiered-storage/">retention 與 tiered storage</a>、<a href="schema-registry-evolution/">Schema Registry 與 schema 演進</a>、<a href="multi-tenant-quota-acl/">multi-tenant quota 與 ACL 治理</a>。下列子議題段保留每個主題的選題判讀入口。</p>
<h3 id="multi-tenant-與配額治理">Multi-tenant 與配額治理</h3>
<p>對應案例 <a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka 事件平台</a>。子議題：</p>
<ul>
<li>Producer / Consumer quota（byte rate、request rate）</li>
<li>ACL 設計（principal、resource、operation）</li>
<li>Topic 命名規範與 ownership</li>
<li>對應指令：<code>kafka-configs.sh --alter --add-config 'producer_byte_rate=...'</code>、<code>kafka-acls.sh --add</code></li>
</ul>
<h3 id="cross-region-與分層叢集">Cross-region 與分層叢集</h3>
<p>對應案例 <a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS</a> 與 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a>。子議題：</p>
<ul>
<li>MirrorMaker 2 配置（active-active vs active-passive）</li>
<li>分層叢集策略（critical / standard / experimental）</li>
<li>跨區 consumer 路徑與 routing freshness</li>
</ul>
<h3 id="topic-生命週期治理">Topic 生命週期治理</h3>
<p>對應案例 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn TopicGC</a>。子議題：</p>
<ul>
<li>Topic 活躍判準（last produce / consume timestamp）</li>
<li>自動回收條件與稽核</li>
<li>Metadata 壓力訊號（controller log、partition 數量上限）</li>
</ul>
<h3 id="replication-與-exactly-once-升級">Replication 與 exactly-once 升級</h3>
<p>對應案例 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配</a>。子議題：</p>
<ul>
<li>acks=all + min.insync.replicas ≥ 2 + producer idempotence</li>
<li>Kafka transaction 與 read_committed 邊界</li>
<li>端到端 exactly-once（Kafka Streams 場景）</li>
</ul>
<h3 id="self-healing-與自動修復">Self-healing 與自動修復</h3>
<p>對應案例 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/" data-link-title="3.C7 LinkedIn：Kafka 自動修復治理" data-link-desc="Kafka 維運從人工處置轉向自動修復的案例。">3.C7 LinkedIn Self-Healing</a>。子議題：</p>
<ul>
<li>可自動修復故障類型（disk full、broker offline、under-replicated partition）</li>
<li>自動修復 vs 人工升級邊界</li>
<li>修復過程的證據鏈納入觀測</li>
</ul>
<h3 id="kraft-與-schema-registry">KRaft 與 Schema Registry</h3>
<p>子議題：</p>
<ul>
<li>KRaft mode 取代 ZooKeeper（運維簡化、metadata 治理）</li>
<li>Schema Registry（Confluent / Apicurio）與 Avro / Protobuf</li>
<li>Schema 演進策略（forward / backward / full compatibility）</li>
</ul>
<h3 id="tiered-storage">Tiered storage</h3>
<p>子議題：</p>
<ul>
<li>冷熱分層（hot tier on local disk、cold tier on S3）</li>
<li>Retention 設計與成本</li>
<li>Read 路徑差異（hot vs cold）</li>
</ul>
<h3 id="kafka-connect-與-cdc">Kafka Connect 與 CDC</h3>
<p>子議題：</p>
<ul>
<li>Source connector / Sink connector 模型</li>
<li>Debezium CDC pipeline 與 outbox 整合</li>
<li>Connect cluster 治理與 schema evolution</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="consumer-lag-暴增">Consumer lag 暴增</h3>
<p>操作原則：先看 lag 是「均勻分布」還是「集中在少數 partition」、再定位 consumer 慢 vs partition 不平衡。</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">kafka-consumer-groups.sh --describe --group &lt;id&gt; --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 輸出含 CURRENT-OFFSET / LOG-END-OFFSET / LAG 逐 partition 列、可看 lag 集中在哪幾個 partition</span></span></span></code></pre></div><p>判讀路徑：consumer 慢（CPU / GC / 下游 I/O）→ producer 突增 → partition 不平衡（key 分布）。</p>
<h3 id="isr-shrink-與-under-replicated-partition">ISR shrink 與 under-replicated partition</h3>
<p>操作原則：ISR 縮小代表 follower 跟不上 leader、看 broker 健康 / 網路 / disk。</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">kafka-topics.sh --describe --under-replicated-partitions --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 輸出為空代表所有 partition 同步正常；列出的 partition 即 ISR 落後者</span></span></span></code></pre></div><h3 id="rebalance-storm">Rebalance storm</h3>
<p>操作原則：consumer 頻繁加入 / 離開觸發 rebalance、看 session.timeout.ms 與 max.poll.interval.ms。</p>
<h3 id="offset-reset-或重複消費">Offset reset 或重複消費</h3>
<p>對應反例 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9</a>。判讀路徑：commit 策略錯誤、broker 端 offset 過期、auto.offset.reset = earliest。</p>
<h3 id="schema-不相容">Schema 不相容</h3>
<p>操作原則：producer 升級 schema、consumer 未升、看 compatibility level。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任務隊列（中等吞吐、複雜 routing）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></td>
      </tr>
      <tr>
          <td>Managed queue（AWS 生態、簡單）</td>
          <td><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></td>
      </tr>
      <tr>
          <td>Managed pub/sub（GCP 生態）</td>
          <td><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>（遷移路徑見 <a href="/blog/backend/03-message-queue/vendors/google-pubsub/migrate-from-kafka/" data-link-title="Kafka → Google Cloud Pub/Sub：從 partition 到 topic-subscription 的模型轉換" data-link-desc="從 Apache Kafka 遷移到 Google Cloud Pub/Sub，處理 partition → topic 模型轉換、ordering 語意差異、consumer group → subscription 對應、offset → ack deadline 切換的階段化流程">Kafka → Pub/Sub</a>）</td>
      </tr>
      <tr>
          <td>輕量 messaging + 微服務通訊</td>
          <td><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
      </tr>
      <tr>
          <td>Redis 生態內 stream</td>
          <td><a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
      </tr>
      <tr>
          <td>Managed Kafka</td>
          <td>AWS MSK / Confluent Cloud（見 <a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2</a>）</td>
      </tr>
      <tr>
          <td>Kafka 相容、單 binary</td>
          <td>Redpanda（T2 候選）</td>
      </tr>
      <tr>
          <td>多租戶 + 分層儲存原生</td>
          <td>Apache Pulsar（T2 候選）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 client API reference（依官方文件）</li>
<li>Kafka Streams / ksqlDB（另開 stream processing 章節）</li>
<li>Confluent 商業功能（Confluent Cloud、Control Center）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="既有通用案例c1-c10">既有通用案例（C1-C10）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS</a></td>
          <td>跨區 queue、tenant 遷移節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware → MSK</a></td>
          <td>自管轉 managed、ACL / cutover</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn TopicGC</a></td>
          <td>Topic 生命週期治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a></td>
          <td>分層叢集策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Kafka+Redis</a></td>
          <td>多 broker 組合拓樸</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka</a></td>
          <td>多租戶 + 平台治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/" data-link-title="3.C7 LinkedIn：Kafka 自動修復治理" data-link-desc="Kafka 維運從人工處置轉向自動修復的案例。">3.C7 LinkedIn Self-Healing</a></td>
          <td>自動修復</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues</a></td>
          <td>全球交付（對比）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配</a></td>
          <td>Replication + idempotence 升級</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>不同規模下的佇列模型</td>
      </tr>
  </tbody>
</table>
<h3 id="kafka-專屬案例c11-c22">Kafka 專屬案例（C11-C22）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">3.C11 Pinterest Tiered Storage</a></td>
          <td>Broker-decoupled tiered storage / S3</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/" data-link-title="3.C12 Pinterest：Shallow Mirror 優化 MirrorMaker" data-link-desc="Pinterest 跨 3 region MirrorMaker、原版解壓&#43;重壓造成 CPU/memory 2-10x spike、改 RecordBatch 層淺迭代。">3.C12 Pinterest Shallow Mirror</a></td>
          <td>MirrorMaker CPU/memory 優化</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">3.C13 Shopify Debezium CDC</a></td>
          <td>Sharded MySQL CDC pipeline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">3.C14 Yelp Schematizer</a></td>
          <td>Schema Registry + 強制 compatibility</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15 Airbnb Spark Streaming</a></td>
          <td>Partition-task 解耦 / data skew</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16 Robinhood Faust</a></td>
          <td>Python stream processing 生態</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17 Walmart MPS</a></td>
          <td>Partition-consumer 1:1 解耦 / K8s 擴張</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-wix-greyhound-troubleshooting/" data-link-title="3.C18 Wix：Greyhound TLLSR 解 consumer 卡住" data-link-desc="Wix 2000&#43; microservice 66B msg/day、自建 Greyhound 抽象、TLLSR 框架解 single-partition lag / poison pill / handler 卡住。">3.C18 Wix Greyhound</a></td>
          <td>TLLSR consumer troubleshooting</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-wix-multi-cluster-migration/" data-link-title="3.C19 Wix：Multi-cluster Kafka zero-downtime 遷移" data-link-desc="Wix metadata 從 5K topic 漲到 20K topic / 200K partition、controller startup 跟 broker stability 受壓垮、分多 cluster 解決。">3.C19 Wix Multi-cluster</a></td>
          <td>Metadata scaling ceiling / 分群</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20 Spotify 遷出 Kafka</a></td>
          <td>（反例）early Kafka 版本可靠性硬限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/" data-link-title="3.C21 Goldman Sachs：MSK 遷移 with MirrorMaker 2" data-link-desc="Goldman Sachs Global Investment Research 從 on-prem Kafka 遷到 MSK、用 MM2 同步 topic/ACL/offset、atomic cutover 7 小時完成。">3.C21 Goldman Sachs MSK</a></td>
          <td>MM2 + LB + timeout 整合 pitfall</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA</a></td>
          <td>Consumer lag 驅動 scale-to-zero</td>
      </tr>
  </tbody>
</table>
<p><strong>KRaft 缺直接 customer case</strong>：目前依官方 KIP-833 / Confluent 公告為準、後續若有 customer 一手案例可補。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a>、<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>下游能力：<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>、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
</ul>
]]></content:encoded></item><item><title>CircleCI</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/circleci/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/circleci/</guid><description>&lt;p>CircleCI 是獨立 CI/CD 平台、承擔三個責任：強進階 cache（layer-aware）+ parallelism（test splitting）、跨 VCS（GitHub / Bitbucket / GitLab）、resource class 彈性（含 macOS / ARM / GPU）。設計取捨偏向「進階 cache + 並行加速 + cross-VCS」、適合需要極致 build speed 跟 macOS runner 的團隊。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 .circleci/config.yml workflow&lt;/li>
&lt;li>設計 cache + workspace 加速 build&lt;/li>
&lt;li>用 parallelism + test splitting&lt;/li>
&lt;li>選 resource class（CPU / memory / macOS / GPU）&lt;/li>
&lt;li>評估 CircleCI vs GitHub Actions 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-circleci-跑起來">最短路徑：5 分鐘把 CircleCI 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># .circleci/config.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">docker&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cimg/node:20}]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">checkout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">workflows&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ci&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">test]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="pipeline--workflow--job-模型">Pipeline / workflow / job 模型&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Pipeline（一次 trigger 的執行）&lt;/li>
&lt;li>Workflow（多 job 編排、DAG）&lt;/li>
&lt;li>Job（一組 step）&lt;/li>
&lt;li>對應指令範例：&lt;code>circleci local execute&lt;/code>（本地測 config）&lt;/li>
&lt;/ul>
&lt;h3 id="orb-重用">Orb 重用&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Orb = package of reusable config（types / commands / jobs / executors）&lt;/li>
&lt;li>Public orb registry（circleci.com/developer/orbs）&lt;/li>
&lt;li>Private orb for company&lt;/li>
&lt;/ul>
&lt;h3 id="cache--workspace">Cache + workspace&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Cache：跨 build 保留（dependency / build artifact）&lt;/li>
&lt;li>Workspace：同 workflow 內 job 之間傳遞&lt;/li>
&lt;li>Cache key 設計（與 GitHub Actions 類似）&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="parallelism--test-splitting">Parallelism + test splitting&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Job parallelism N&lt;/li>
&lt;li>Test splitting by timing / name / class&lt;/li>
&lt;li>對應 test suite 加速&lt;/li>
&lt;/ul>
&lt;h3 id="resource-class">Resource class&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>small / medium / large / xlarge / 2xlarge&lt;/li>
&lt;li>macOS / Arm / GPU classes&lt;/li>
&lt;li>跟 cost 平衡&lt;/li>
&lt;/ul>
&lt;h3 id="self-hosted-runner">Self-hosted runner&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>CircleCI 是獨立 CI/CD 平台、承擔三個責任：強進階 cache（layer-aware）+ parallelism（test splitting）、跨 VCS（GitHub / Bitbucket / GitLab）、resource class 彈性（含 macOS / ARM / GPU）。設計取捨偏向「進階 cache + 並行加速 + cross-VCS」、適合需要極致 build speed 跟 macOS runner 的團隊。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 .circleci/config.yml workflow</li>
<li>設計 cache + workspace 加速 build</li>
<li>用 parallelism + test splitting</li>
<li>選 resource class（CPU / memory / macOS / GPU）</li>
<li>評估 CircleCI vs GitHub Actions 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-circleci-跑起來">最短路徑：5 分鐘把 CircleCI 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># .circleci/config.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">2.1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">docker</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cimg/node:20}]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="l">checkout</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm test</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="nt">workflows</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">ci</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">test]</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="pipeline--workflow--job-模型">Pipeline / workflow / job 模型</h3>
<p>子議題：</p>
<ul>
<li>Pipeline（一次 trigger 的執行）</li>
<li>Workflow（多 job 編排、DAG）</li>
<li>Job（一組 step）</li>
<li>對應指令範例：<code>circleci local execute</code>（本地測 config）</li>
</ul>
<h3 id="orb-重用">Orb 重用</h3>
<p>子議題：</p>
<ul>
<li>Orb = package of reusable config（types / commands / jobs / executors）</li>
<li>Public orb registry（circleci.com/developer/orbs）</li>
<li>Private orb for company</li>
</ul>
<h3 id="cache--workspace">Cache + workspace</h3>
<p>子議題：</p>
<ul>
<li>Cache：跨 build 保留（dependency / build artifact）</li>
<li>Workspace：同 workflow 內 job 之間傳遞</li>
<li>Cache key 設計（與 GitHub Actions 類似）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="parallelism--test-splitting">Parallelism + test splitting</h3>
<p>子議題：</p>
<ul>
<li>Job parallelism N</li>
<li>Test splitting by timing / name / class</li>
<li>對應 test suite 加速</li>
</ul>
<h3 id="resource-class">Resource class</h3>
<p>子議題：</p>
<ul>
<li>small / medium / large / xlarge / 2xlarge</li>
<li>macOS / Arm / GPU classes</li>
<li>跟 cost 平衡</li>
</ul>
<h3 id="self-hosted-runner">Self-hosted runner</h3>
<p>子議題：</p>
<ul>
<li>Runner agent</li>
<li>適合：內網 / 特殊環境</li>
</ul>
<h3 id="oidc-integration">OIDC integration</h3>
<p>子議題：</p>
<ul>
<li>OIDC token → AWS / GCP（無 long-lived secret）</li>
<li>跟 GitHub Actions 同 pattern</li>
</ul>
<h3 id="approval-job">Approval job</h3>
<p>子議題：</p>
<ul>
<li>type: approval job：人工介入</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
</ul>
<h3 id="cross-vcs-support">Cross-VCS support</h3>
<p>子議題：</p>
<ul>
<li>GitHub / Bitbucket / GitLab</li>
<li>跟 GitHub Actions 只 GitHub 對比</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="build-慢">Build 慢</h3>
<p>操作原則：cache miss / test 沒 split / resource class 太小。</p>
<h3 id="cache-不命中">Cache 不命中</h3>
<p>操作原則：cache key 設計問題 / key change。</p>
<h3 id="parallelism-不均勻">Parallelism 不均勻</h3>
<p>操作原則：test split strategy（timing 最好但要 historical data）。</p>
<h3 id="approval-卡住">Approval 卡住</h3>
<p>操作原則：approval job 沒人按 / on-call 不在。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitHub-hosted</td>
          <td><a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a></td>
      </tr>
      <tr>
          <td>Self-hosted enterprise</td>
          <td>Jenkins / Buildkite / Tekton</td>
      </tr>
      <tr>
          <td>GitLab-hosted</td>
          <td>GitLab CI</td>
      </tr>
      <tr>
          <td>複雜 DAG / K8s-native</td>
          <td>Tekton / Argo Workflows</td>
      </tr>
      <tr>
          <td>預算敏感</td>
          <td>GitHub Actions / self-hosted Jenkins</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Orb 細節</li>
<li>CircleCI Server（self-host enterprise）</li>
<li>Pricing 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>canary deploy / approval job 的部署節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>峰值前 CI workflow 跑 capacity test</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft：變更治理與可靠性門檻</a></td>
          <td>approval job 對應變更分層審查</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 CircleCI customer case</strong>：大規模 CircleCI 採用、macOS / iOS CI 加速案例、CircleCI → GitHub Actions 遷移案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/</guid><description>&lt;p>Cloudflare 是 anycast edge 的代表、單一配置 push 即可影響全球流量、是 configuration push 風險 / regex catastrophic backtracking / BGP 信任的教學標竿。Cloudflare 工程部落格公開度極高、post-mortem 細節豐富。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>全球 configuration push 的 blast radius：為何 60 秒內可癱瘓全球流量&lt;/li>
&lt;li>Regex CPU 耗盡：catastrophic backtracking 如何繞過所有 timeout&lt;/li>
&lt;li>BGP 風險：路由洩漏如何把流量吸入錯誤 ASN&lt;/li>
&lt;li>Recovery 設計：為何 configuration rollback 需要 dataplane 層協作&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2019&lt;/td>
 &lt;td>Regex CPU 27 分鐘&lt;/td>
 &lt;td>catastrophic backtracking、WAF rule 部署流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2020&lt;/td>
 &lt;td>BGP route leak&lt;/td>
 &lt;td>跨 ASN 信任、網路層事故止血&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2022&lt;/td>
 &lt;td>配置 push 全球退化&lt;/td>
 &lt;td>變更節奏、staged rollout 的價值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2023&lt;/td>
 &lt;td>Control plane token incident&lt;/td>
 &lt;td>身分控制面與多產品連鎖影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2026&lt;/td>
 &lt;td>BYOIP / BGP withdrawal&lt;/td>
 &lt;td>Addressing API、prefix withdrawal、狀態恢復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">2023 Workers KV Deployment Tool Misconfiguration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">2023 Workers KV Deployment Tool Misconfiguration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Cloudflare 這個案例在講的是 edge 平台如何把一個小錯誤快速放大到全球。讀者先看懂配置推送、runtime 驗證與路由撤銷各自的責任，再把 anycast 與 control plane 當成事故擴散的核心路徑。&lt;/p></description><content:encoded><![CDATA[<p>Cloudflare 是 anycast edge 的代表、單一配置 push 即可影響全球流量、是 configuration push 風險 / regex catastrophic backtracking / BGP 信任的教學標竿。Cloudflare 工程部落格公開度極高、post-mortem 細節豐富。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>全球 configuration push 的 blast radius：為何 60 秒內可癱瘓全球流量</li>
<li>Regex CPU 耗盡：catastrophic backtracking 如何繞過所有 timeout</li>
<li>BGP 風險：路由洩漏如何把流量吸入錯誤 ASN</li>
<li>Recovery 設計：為何 configuration rollback 需要 dataplane 層協作</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2019</td>
          <td>Regex CPU 27 分鐘</td>
          <td>catastrophic backtracking、WAF rule 部署流程</td>
      </tr>
      <tr>
          <td>2020</td>
          <td>BGP route leak</td>
          <td>跨 ASN 信任、網路層事故止血</td>
      </tr>
      <tr>
          <td>2022</td>
          <td>配置 push 全球退化</td>
          <td>變更節奏、staged rollout 的價值</td>
      </tr>
      <tr>
          <td>2023</td>
          <td>Control plane token incident</td>
          <td>身分控制面與多產品連鎖影響</td>
      </tr>
      <tr>
          <td>2026</td>
          <td>BYOIP / BGP withdrawal</td>
          <td>Addressing API、prefix withdrawal、狀態恢復</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">2023 Workers KV Deployment Tool Misconfiguration</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">2023 Workers KV Deployment Tool Misconfiguration</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>Cloudflare 這個案例在講的是 edge 平台如何把一個小錯誤快速放大到全球。讀者先看懂配置推送、runtime 驗證與路由撤銷各自的責任，再把 anycast 與 control plane 當成事故擴散的核心路徑。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 regex、workers 設定或 deployment tool 出現問題時，真正危險的是錯誤被快速推到全網，單一節點故障反而容易收斂。當 BGP 或 BYOIP 參數變動時，回滾與驗證就必須先於擴散，否則影響會直接表現在全球流量上。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否在全網推送前做足夠的配置驗證</li>
<li>能否把 blast radius 限制在局部 edge 群組</li>
<li>能否在 CPU 熱點或路由撤銷前先看見異常</li>
<li>能否把 rollback 動作設計成快速且可驗證</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Cloudflare 和 Fastly 都在講 edge 平台的快速擴散，但 Cloudflare 更常暴露控制面與部署工具的問題。它和 AWS S3、GCP 放在一起看，可以更清楚看到全球網路事故是配置與路由鏈條的連鎖反應，單一節點失效很少是起因。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2019 年 regex CPU outage 是 catastrophic backtracking 直接拖垮 edge runtime 的經典樣本。</li>
<li>2023 年控制面事故與 2026 年 BYOIP / BGP 事故則顯示配置與路由都能成為全球擴散點。</li>
<li>這組樣本也能對照配置推送與回滾速度對 blast radius 的影響。</li>
<li>Cloudflare 的事故史很適合拿來和 Fastly 比較 edge 平台差異。</li>
<li>workers / deployment tool misconfiguration 讓控制面本身成為風險。</li>
<li>anycast edge 讓路由錯誤能在全球尺度迅速顯現。</li>
<li>global propagation 讓回滾時間直接影響用戶體感。</li>
<li>control plane bug 常常比 data plane bug 更難局部化。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019">Details of the Cloudflare outage on July 2, 2019</a>：regex CPU / catastrophic backtracking 事故的官方回顧。</li>
<li><a href="https://blog.cloudflare.com/cloudflare-incident-on-january-24th-2023/">Cloudflare incident on January 24, 2023</a>：service token / control plane 變更導致的多產品連鎖影響。</li>
<li><a href="https://blog.cloudflare.com/cloudflare-incident-on-october-30-2023/">Cloudflare incident on October 30, 2023</a>：Workers KV / deployment tool misconfiguration 的控制面事故。</li>
<li><a href="https://blog.cloudflare.com/cloudflare-outage-february-20-2026/">Cloudflare outage on February 20, 2026</a>：BYOIP / BGP 變更造成的路由撤銷事故。</li>
</ul>
]]></content:encoded></item><item><title>Docker</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/</guid><description>&lt;p>Docker 是最早 popularize container 的工具、承擔三個責任：container image build（Dockerfile / BuildKit）、local container runtime（docker run / Compose）、image distribution（Docker Hub / private registry）。設計取捨偏向「dev experience + image format standard」、production orchestration 多被 Kubernetes + containerd 取代、但 image build / dev workflow / OCI image 仍是事實標準。&lt;/p>
&lt;p>對「Local dev / CI container 工具、image build pipeline、小規模 dev 環境」這條路徑、Docker 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 Dockerfile + 跑 docker build / run&lt;/li>
&lt;li>用 multi-stage build / BuildKit 優化 image&lt;/li>
&lt;li>用 Docker Compose 編排 dev 環境&lt;/li>
&lt;li>配置 image registry + scanning + SBOM&lt;/li>
&lt;li>評估 Docker Desktop license 對團隊的影響、選替代（Podman / Rancher Desktop）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-docker-跑起來">最短路徑：5 分鐘把 Docker 跑起來&lt;/h2>





&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"># 1. 安裝（macOS 擇一）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">brew install --cask docker &lt;span class="c1"># Docker Desktop（商業企業需付費授權）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># brew install podman # 替代方案：Podman（無 daemon、免費）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 跑 container&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">docker run -d -p 8080:80 --name web nginx:stable-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker ps &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> docker logs web
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Build + push image&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">docker build -t myapp:1 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">docker tag myapp:1 ghcr.io/&amp;lt;org&amp;gt;/myapp:1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">docker push ghcr.io/&amp;lt;org&amp;gt;/myapp:1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="dockerfile-設計">Dockerfile 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>FROM / RUN / COPY / WORKDIR / EXPOSE / CMD / ENTRYPOINT&lt;/li>
&lt;li>Multi-stage build（build stage + runtime stage 分離）&lt;/li>
&lt;li>Layer cache 設計（COPY 順序影響 cache hit）&lt;/li>
&lt;li>對應指令：&lt;code>docker build --no-cache&lt;/code>、&lt;code>docker history &amp;lt;image&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="buildkit--buildx">BuildKit / Buildx&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>BuildKit：新 builder、parallel + cache mount + secret + SSH agent&lt;/li>
&lt;li>Buildx：cross-platform build（amd64 / arm64）&lt;/li>
&lt;li>Cache backend（local / registry / S3 / GHA）&lt;/li>
&lt;li>對應指令：&lt;code>docker buildx create --use&lt;/code>、&lt;code>docker buildx build --platform=linux/amd64,linux/arm64&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="docker-compose">Docker Compose&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Docker 是最早 popularize container 的工具、承擔三個責任：container image build（Dockerfile / BuildKit）、local container runtime（docker run / Compose）、image distribution（Docker Hub / private registry）。設計取捨偏向「dev experience + image format standard」、production orchestration 多被 Kubernetes + containerd 取代、但 image build / dev workflow / OCI image 仍是事實標準。</p>
<p>對「Local dev / CI container 工具、image build pipeline、小規模 dev 環境」這條路徑、Docker 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 Dockerfile + 跑 docker build / run</li>
<li>用 multi-stage build / BuildKit 優化 image</li>
<li>用 Docker Compose 編排 dev 環境</li>
<li>配置 image registry + scanning + SBOM</li>
<li>評估 Docker Desktop license 對團隊的影響、選替代（Podman / Rancher Desktop）</li>
</ol>
<h2 id="最短路徑5-分鐘把-docker-跑起來">最短路徑：5 分鐘把 Docker 跑起來</h2>





<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. 安裝（macOS 擇一）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install --cask docker            <span class="c1"># Docker Desktop（商業企業需付費授權）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># brew install podman                 # 替代方案：Podman（無 daemon、免費）</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="c1"># 2. 跑 container</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker run -d -p 8080:80 --name web nginx:stable-alpine
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker ps <span class="o">&amp;&amp;</span> docker logs web
</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="c1"># 3. Build + push image</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">docker build -t myapp:1 .
</span></span><span class="line"><span class="ln">11</span><span class="cl">docker tag myapp:1 ghcr.io/&lt;org&gt;/myapp:1
</span></span><span class="line"><span class="ln">12</span><span class="cl">docker push ghcr.io/&lt;org&gt;/myapp:1</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="dockerfile-設計">Dockerfile 設計</h3>
<p>子議題：</p>
<ul>
<li>FROM / RUN / COPY / WORKDIR / EXPOSE / CMD / ENTRYPOINT</li>
<li>Multi-stage build（build stage + runtime stage 分離）</li>
<li>Layer cache 設計（COPY 順序影響 cache hit）</li>
<li>對應指令：<code>docker build --no-cache</code>、<code>docker history &lt;image&gt;</code></li>
</ul>
<h3 id="buildkit--buildx">BuildKit / Buildx</h3>
<p>子議題：</p>
<ul>
<li>BuildKit：新 builder、parallel + cache mount + secret + SSH agent</li>
<li>Buildx：cross-platform build（amd64 / arm64）</li>
<li>Cache backend（local / registry / S3 / GHA）</li>
<li>對應指令：<code>docker buildx create --use</code>、<code>docker buildx build --platform=linux/amd64,linux/arm64</code></li>
</ul>
<h3 id="docker-compose">Docker Compose</h3>
<p>子議題：</p>
<ul>
<li>docker-compose.yml：service / network / volume 配置</li>
<li>適合：local dev 多 container（DB + cache + app）</li>
<li>不適合：production（用 K8s）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s deployment</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="image-security--scanning--sbom">Image security / scanning / SBOM</h3>
<p>子議題：</p>
<ul>
<li>Trivy / Grype / Snyk image vulnerability scanning</li>
<li>SBOM 產生（syft / Docker scout）</li>
<li>Sign image（cosign / notary v2）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> supply chain</li>
</ul>
<h3 id="image-registry-選擇">Image registry 選擇</h3>
<p>子議題：</p>
<ul>
<li>Docker Hub（public + rate limit issue）</li>
<li>雲端：ECR / GCR / Artifact Registry / ACR</li>
<li>Self-host：Harbor / GitLab Container Registry / Nexus</li>
<li>對應 image pull credentials 管理</li>
</ul>
<h3 id="docker-desktop-license">Docker Desktop license</h3>
<p>子議題：</p>
<ul>
<li>2021 改授權：商業企業（&gt; 250 員工 / &gt; $10M）需付費</li>
<li>替代：Podman Desktop / Rancher Desktop / Colima / Lima</li>
<li>替代品的 daemon / rootless 差異</li>
<li>對應企業 IT 採購決策</li>
</ul>
<h3 id="containerd--cri-o-在-production">Containerd / CRI-O 在 production</h3>
<p>子議題：</p>
<ul>
<li>K8s 1.24+ 移除 dockershim、改用 containerd / CRI-O</li>
<li>Docker image 跟 containerd 相容（OCI standard）</li>
<li>production 不用 Docker、用 containerd</li>
</ul>
<h3 id="image-size-優化">Image size 優化</h3>
<p>子議題：</p>
<ul>
<li>Base image 選擇（distroless / alpine / scratch）</li>
<li>Multi-stage build + layer combine</li>
<li>Build context（.dockerignore）</li>
<li>跟 image scanning 跟 deploy speed 對應</li>
</ul>
<h3 id="rootless--安全強化">Rootless / 安全強化</h3>
<p>子議題：</p>
<ul>
<li>Rootless mode（Docker / Podman 都支援）</li>
<li>User namespace mapping</li>
<li>Seccomp / AppArmor / SELinux profile</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> container security</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="image-build-cache-不命中">Image build cache 不命中</h3>
<p>操作原則：COPY 順序錯、<code>.dockerignore</code> 缺、變動的 layer 在前面。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker build --progress<span class="o">=</span>plain --no-cache -t myapp:debug .   <span class="c1"># 逐層輸出、比對哪層吃時間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker <span class="nb">history</span> myapp:debug                                  <span class="c1"># 看每層大小</span></span></span></code></pre></div><h3 id="image-過大">Image 過大</h3>
<p>操作原則：base image 太重 / 沒 multi-stage / build context 過大。判讀：<code>docker history</code> 看 layer 大小。</p>
<h3 id="container-起不來">Container 起不來</h3>
<p>操作原則：<code>docker logs</code> + <code>docker inspect</code> 看 exit code + state。</p>
<h3 id="network-port-不通">Network port 不通</h3>
<p>操作原則：<code>-p</code> mapping vs <code>EXPOSE</code> 差異、host network vs bridge network、firewall。</p>
<h3 id="volume-權限問題">Volume 權限問題</h3>
<p>操作原則：container UID 跟 host UID 不對齊、rootless mode 特別容易踩。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production orchestration</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
      </tr>
      <tr>
          <td>Rootless / 安全強化</td>
          <td>Podman</td>
      </tr>
      <tr>
          <td>替代 Docker Desktop（cost）</td>
          <td>Rancher Desktop / Colima / Lima</td>
      </tr>
      <tr>
          <td>純單機 service</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
      </tr>
      <tr>
          <td>雲端 managed container</td>
          <td>ECS / Cloud Run / Container Apps</td>
      </tr>
      <tr>
          <td>Build-only（無 daemon）</td>
          <td>Buildah / Kaniko / BuildKit standalone</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Dockerfile 完整 reference</li>
<li>Docker Compose v2 進階配置</li>
<li>Container runtime spec（runc / OCI）</li>
<li>各 registry 完整 API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Docker 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s</a></td>
          <td>Container image 是平台遷移的可攜介面、orchestrator 換但 image 不換</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小規模直接 Docker / Compose、中大型才走 K8s（Docker 退到 build only）</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Docker 案例</strong>：Docker Hub rate limit incident、企業 license 遷移到 Podman 案例、image scanning supply chain 案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>、<a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>（image scanning / SBOM）</li>
</ul>
]]></content:encoded></item><item><title>Netflix</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/</guid><description>&lt;p>Netflix 是 Chaos Engineering 的起源、Chaos Monkey 跟 Simian Army 是領域標準工具的概念來源、FIT（Failure Injection Testing）是大規模 production chaos 的實作範本。教學重點在「故障注入如何作為 first-class 工程實踐」。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Chaos Monkey 起點：在 production 隨機殺實例為何能改進架構&lt;/li>
&lt;li>Simian Army 工具鏈：Latency / Janitor / Conformity 等不同維度的 chaos&lt;/li>
&lt;li>FIT：把 chaos 從 instance 層升級到 request 層、攻擊更精細&lt;/li>
&lt;li>Chaos Maturity Model：團隊採用 chaos 的能力分級&lt;/li>
&lt;li>Steady state hypothesis：chaos 實驗的科學方法基礎&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Chaos Monkey&lt;/td>
 &lt;td>起源、規則、為何在 weekday business hour&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Simian Army&lt;/td>
 &lt;td>多維度故障注入的設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>FIT&lt;/td>
 &lt;td>Request-level fault injection 的工程化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Chaos Engineering Manifesto&lt;/td>
 &lt;td>hypothesis / scope / blast radius 控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production chaos vs Staging&lt;/td>
 &lt;td>為何 production 才有真實價值&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">N1&lt;/a>&lt;/td>
 &lt;td>Steady State、Chaos 與 FIT&lt;/td>
 &lt;td>把故障注入變成可證偽、可停止、可回寫的驗證流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">N2&lt;/a>&lt;/td>
 &lt;td>Business-Hours Guardrails&lt;/td>
 &lt;td>把時段策略、風險邊界與應變能力整合進 chaos 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">N3&lt;/a>&lt;/td>
 &lt;td>FIT 證據交接&lt;/td>
 &lt;td>把故障注入結果轉成 release gate 可用證據&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Netflix 這個案例在講的是故障注入如何從實驗變成工程制度。讀者要先分辨 steady state、hypothesis、blast radius 與回復條件各自扮演的角色，才能理解為什麼 chaos 是驗證服務韌性的方法，演示層面的價值是次要的。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當團隊只在 staging 做演練時，先看測試是否真的碰到生產流量的分布與依賴關係。當問題需要更細的干預時，再往 FIT 這種 request-level fault injection 移動，讓故障落在真正會被客戶碰到的路徑上。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否先寫出 steady state，再設計實驗&lt;/li>
&lt;li>能否說清楚 blast radius 與 rollback 條件&lt;/li>
&lt;li>能否說明為何在 business hour 做 chaos 反而更安全&lt;/li>
&lt;li>能否判斷問題需要 instance-level 還是 request-level 注入&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Netflix 把「先驗證再承擔風險」這件事做成制度，和 AWS S3、Cloudflare 這類事故頁形成對照。前者是在可控條件下主動打破假設，後者是在失敗後回頭整理假設，因此兩者一起讀才能看懂 reliability 與 incident response 的分工。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>Chaos Monkey 直接驗證實例被殺掉後，服務是否仍能維持 steady state。&lt;/li>
&lt;li>FIT 把故障注入從 instance 級推進到 request 級，讓實驗更貼近真實流量路徑。&lt;/li>
&lt;li>Simian Army 讓不同故障類型有各自的注入面。&lt;/li>
&lt;li>business-hour chaos 讓測試更接近真實營運節奏。&lt;/li>
&lt;li>chaos maturity model 讓團隊知道自己在採用故障注入的哪個階段。&lt;/li>
&lt;li>steady state hypothesis 讓實驗成為可證偽的工程判斷，超越單純演示。&lt;/li>
&lt;li>latency monkey 讓延遲問題成為可以主動驗證的故障型態。&lt;/li>
&lt;li>janitor / conformity 類工具把環境清理與架構規則也納入韌性管理。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey&lt;/a>：Chaos Monkey 的現行開源實作。&lt;/li>
&lt;li>&lt;a href="https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey">Netflix/SimianArmy Wiki: Chaos Monkey&lt;/a>：Simian Army 舊版 wiki，說明 business-hours chaos 的基本規則。&lt;/li>
&lt;li>&lt;a href="https://github.com/Netflix/SimianArmy">Netflix/SimianArmy&lt;/a>：Simian Army 套件入口，補齊多種 monkey 的整體脈絡。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Netflix 是 Chaos Engineering 的起源、Chaos Monkey 跟 Simian Army 是領域標準工具的概念來源、FIT（Failure Injection Testing）是大規模 production chaos 的實作範本。教學重點在「故障注入如何作為 first-class 工程實踐」。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Chaos Monkey 起點：在 production 隨機殺實例為何能改進架構</li>
<li>Simian Army 工具鏈：Latency / Janitor / Conformity 等不同維度的 chaos</li>
<li>FIT：把 chaos 從 instance 層升級到 request 層、攻擊更精細</li>
<li>Chaos Maturity Model：團隊採用 chaos 的能力分級</li>
<li>Steady state hypothesis：chaos 實驗的科學方法基礎</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chaos Monkey</td>
          <td>起源、規則、為何在 weekday business hour</td>
      </tr>
      <tr>
          <td>Simian Army</td>
          <td>多維度故障注入的設計</td>
      </tr>
      <tr>
          <td>FIT</td>
          <td>Request-level fault injection 的工程化</td>
      </tr>
      <tr>
          <td>Chaos Engineering Manifesto</td>
          <td>hypothesis / scope / blast radius 控制</td>
      </tr>
      <tr>
          <td>Production chaos vs Staging</td>
          <td>為何 production 才有真實價值</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">N1</a></td>
          <td>Steady State、Chaos 與 FIT</td>
          <td>把故障注入變成可證偽、可停止、可回寫的驗證流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">N2</a></td>
          <td>Business-Hours Guardrails</td>
          <td>把時段策略、風險邊界與應變能力整合進 chaos 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">N3</a></td>
          <td>FIT 證據交接</td>
          <td>把故障注入結果轉成 release gate 可用證據</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Netflix 這個案例在講的是故障注入如何從實驗變成工程制度。讀者要先分辨 steady state、hypothesis、blast radius 與回復條件各自扮演的角色，才能理解為什麼 chaos 是驗證服務韌性的方法，演示層面的價值是次要的。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當團隊只在 staging 做演練時，先看測試是否真的碰到生產流量的分布與依賴關係。當問題需要更細的干預時，再往 FIT 這種 request-level fault injection 移動，讓故障落在真正會被客戶碰到的路徑上。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否先寫出 steady state，再設計實驗</li>
<li>能否說清楚 blast radius 與 rollback 條件</li>
<li>能否說明為何在 business hour 做 chaos 反而更安全</li>
<li>能否判斷問題需要 instance-level 還是 request-level 注入</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Netflix 把「先驗證再承擔風險」這件事做成制度，和 AWS S3、Cloudflare 這類事故頁形成對照。前者是在可控條件下主動打破假設，後者是在失敗後回頭整理假設，因此兩者一起讀才能看懂 reliability 與 incident response 的分工。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>Chaos Monkey 直接驗證實例被殺掉後，服務是否仍能維持 steady state。</li>
<li>FIT 把故障注入從 instance 級推進到 request 級，讓實驗更貼近真實流量路徑。</li>
<li>Simian Army 讓不同故障類型有各自的注入面。</li>
<li>business-hour chaos 讓測試更接近真實營運節奏。</li>
<li>chaos maturity model 讓團隊知道自己在採用故障注入的哪個階段。</li>
<li>steady state hypothesis 讓實驗成為可證偽的工程判斷，超越單純演示。</li>
<li>latency monkey 讓延遲問題成為可以主動驗證的故障型態。</li>
<li>janitor / conformity 類工具把環境清理與架構規則也納入韌性管理。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey</a>：Chaos Monkey 的現行開源實作。</li>
<li><a href="https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey">Netflix/SimianArmy Wiki: Chaos Monkey</a>：Simian Army 舊版 wiki，說明 business-hours chaos 的基本規則。</li>
<li><a href="https://github.com/Netflix/SimianArmy">Netflix/SimianArmy</a>：Simian Army 套件入口，補齊多種 monkey 的整體脈絡。</li>
</ul>
]]></content:encoded></item><item><title>Opsgenie</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/</guid><description>&lt;p>Opsgenie 是 Atlassian 出品的 on-call 平台、承擔三個責任：alert routing + escalation policy、跟 Atlassian 套件（Jira Service Management / Statuspage / Confluence）深度整合、heartbeat monitoring（被動觀察 service 是否還在）。已被併入 Jira Service Management Cloud、原獨立服務逐漸 deprecated。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Opsgenie 的核心定位是 &lt;em>Atlassian 生態內的 on-call 元件&lt;/em>、跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> 比、它的差異在 &lt;em>跟 Jira Service Management / Confluence / Statuspage 的整合深度&lt;/em>、paging 能力本身相近：ticket、runbook、status page、incident 都在同一個身份體系（Atlassian Identity）內、不用跨 SaaS 串 SSO 跟 webhook。Atlassian-heavy enterprise 通常已經買了 JSM / Confluence / Statuspage、再買獨立 PagerDuty 等於多一條供應商線、ROI 不一定划算。&lt;/p>
&lt;p>2025 年 Atlassian 公開宣布 &lt;em>Opsgenie 將在 2027 年 4 月 EOL&lt;/em>、原 Opsgenie standalone 客戶要遷移到 &lt;a href="https://www.atlassian.com/software/jira/service-management">Jira Service Management Premium / Enterprise&lt;/a> 內建的 on-call 能力。這是現有 Opsgenie 客戶在 2025-2027 期間的最大議題、新案不該再選 Opsgenie standalone。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>配置 Opsgenie team / schedule / escalation&lt;/li>
&lt;li>設計 alert routing 與 deduplication&lt;/li>
&lt;li>整合 Jira Service Management / Statuspage / Confluence&lt;/li>
&lt;li>用 Heartbeat monitoring 守護 cron / scheduled job&lt;/li>
&lt;li>評估 Opsgenie → JSM Cloud 遷移路徑&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Opsgenie deployment 是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>誰能 ack alert&lt;/strong>：schedule rotation 是否真的有人在線、override 機制是否被濫用（永久 override 掩蓋人力缺口）、escalation policy 的 final step 是否有 fallback team 而非無限循環&lt;/li>
&lt;li>&lt;strong>跟 JSM migration plan&lt;/strong>：是否已盤點 standalone Opsgenie 跟 JSM on-call 的 feature gap、現有 integration（Datadog / Prometheus webhook、Slack routing、custom API）在 JSM on-call 是否 parity、API token / Terraform config 的轉換路徑&lt;/li>
&lt;li>&lt;strong>Atlassian Identity 整合&lt;/strong>：是否走 Atlassian Access（IdP SSO + SCIM provision + audit log）、還是停留在 Opsgenie 自己的 user store；後者在 migration / offboarding / compliance 都是坑&lt;/li>
&lt;li>&lt;strong>Slack notification routing&lt;/strong>：alert routing 規則是 fan-out 到所有 team channel（吵雜）還是 priority-based（P1 → on-call DM + channel、P3 → channel only）；Slack 是事實上的 incident war room、routing 不對 SOC 就漏接&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness&lt;/a> 邊界的待補項目。&lt;/p></description><content:encoded><![CDATA[<p>Opsgenie 是 Atlassian 出品的 on-call 平台、承擔三個責任：alert routing + escalation policy、跟 Atlassian 套件（Jira Service Management / Statuspage / Confluence）深度整合、heartbeat monitoring（被動觀察 service 是否還在）。已被併入 Jira Service Management Cloud、原獨立服務逐漸 deprecated。</p>
<h2 id="服務定位">服務定位</h2>
<p>Opsgenie 的核心定位是 <em>Atlassian 生態內的 on-call 元件</em>、跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 比、它的差異在 <em>跟 Jira Service Management / Confluence / Statuspage 的整合深度</em>、paging 能力本身相近：ticket、runbook、status page、incident 都在同一個身份體系（Atlassian Identity）內、不用跨 SaaS 串 SSO 跟 webhook。Atlassian-heavy enterprise 通常已經買了 JSM / Confluence / Statuspage、再買獨立 PagerDuty 等於多一條供應商線、ROI 不一定划算。</p>
<p>2025 年 Atlassian 公開宣布 <em>Opsgenie 將在 2027 年 4 月 EOL</em>、原 Opsgenie standalone 客戶要遷移到 <a href="https://www.atlassian.com/software/jira/service-management">Jira Service Management Premium / Enterprise</a> 內建的 on-call 能力。這是現有 Opsgenie 客戶在 2025-2027 期間的最大議題、新案不該再選 Opsgenie standalone。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>配置 Opsgenie team / schedule / escalation</li>
<li>設計 alert routing 與 deduplication</li>
<li>整合 Jira Service Management / Statuspage / Confluence</li>
<li>用 Heartbeat monitoring 守護 cron / scheduled job</li>
<li>評估 Opsgenie → JSM Cloud 遷移路徑</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Opsgenie deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 ack alert</strong>：schedule rotation 是否真的有人在線、override 機制是否被濫用（永久 override 掩蓋人力缺口）、escalation policy 的 final step 是否有 fallback team 而非無限循環</li>
<li><strong>跟 JSM migration plan</strong>：是否已盤點 standalone Opsgenie 跟 JSM on-call 的 feature gap、現有 integration（Datadog / Prometheus webhook、Slack routing、custom API）在 JSM on-call 是否 parity、API token / Terraform config 的轉換路徑</li>
<li><strong>Atlassian Identity 整合</strong>：是否走 Atlassian Access（IdP SSO + SCIM provision + audit log）、還是停留在 Opsgenie 自己的 user store；後者在 migration / offboarding / compliance 都是坑</li>
<li><strong>Slack notification routing</strong>：alert routing 規則是 fan-out 到所有 team channel（吵雜）還是 priority-based（P1 → on-call DM + channel、P3 → channel only）；Slack 是事實上的 incident war room、routing 不對 SOC 就漏接</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a> 邊界的待補項目。</p>
<h2 id="最短路徑">最短路徑</h2>





<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. Atlassian admin 啟用 Opsgenie / JSM</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. 建 team / schedule</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. 配置 integration（Datadog / Prometheus webhook）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. 試 alert + escalation</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="team--schedule--escalation">Team / schedule / escalation</h3>
<p>子議題：</p>
<ul>
<li>Team 對應 service 或 component</li>
<li>Schedule rotation / override</li>
<li>Escalation policy（多 step / responder）</li>
</ul>
<h3 id="alert-routing--atlassian-套件整合">Alert routing + Atlassian 套件整合</h3>
<p>子議題：</p>
<ul>
<li>Routing rule（priority / source）+ deduplication</li>
<li>Jira Service Management（ITSM workflow）</li>
<li>Statuspage（incident → public update）</li>
<li>Confluence runbook</li>
<li>Slack / Teams 通知</li>
</ul>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Opsgenie</th>
          <th>PagerDuty</th>
          <th>incident.io</th>
          <th>Grafana OnCall</th>
          <th>JSM Premium on-call</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>生態錨點</td>
          <td>Atlassian（JSM / Confluence / Statuspage）</td>
          <td>獨立 SaaS、整合廣</td>
          <td>Slack-first、incident workflow</td>
          <td>Grafana stack（OSS-friendly）</td>
          <td>Atlassian 內建</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>按 user / month</td>
          <td>按 user / month + add-on</td>
          <td>按 user / month</td>
          <td>OSS 免費 / Grafana Cloud 付費</td>
          <td>包在 JSM Premium / Enterprise license</td>
      </tr>
      <tr>
          <td>身份整合</td>
          <td>Atlassian Identity / Access SSO</td>
          <td>自家 + SAML / SCIM</td>
          <td>Slack identity + SAML</td>
          <td>Grafana auth + OAuth</td>
          <td>Atlassian Identity（原生）</td>
      </tr>
      <tr>
          <td>Runbook / postmortem</td>
          <td>Confluence runbook + 基本 postmortem</td>
          <td>Runbook Automation + Jeli postmortem</td>
          <td>內建 incident timeline + retrospective</td>
          <td>Grafana dashboard runbook（弱）</td>
          <td>Confluence + JSM workflow</td>
      </tr>
      <tr>
          <td>長期路徑</td>
          <td>2027/4 EOL、移到 JSM on-call</td>
          <td>持續演進、Process Automation 加深</td>
          <td>持續演進、IR workflow 強化</td>
          <td>持續演進、OSS 路線</td>
          <td>跟 JSM 同步演進</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>既有 Opsgenie 客戶 migration 期、無新案</td>
          <td>不在 Atlassian 生態、跨工具堆疊</td>
          <td>Slack-native IR、incident workflow 重</td>
          <td>OSS / 預算敏感、Grafana 已用</td>
          <td>Atlassian-heavy enterprise</td>
      </tr>
  </tbody>
</table>
<p>選 Opsgenie 的核心訴求現在 <em>只有一個</em>：既有客戶在 EOL 前的 migration 緩衝期。新案應該直接走 JSM Premium on-call（已在 Atlassian 生態）、PagerDuty（不在 Atlassian 生態）或 incident.io（Slack-native）。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="heartbeat-monitoring">Heartbeat monitoring</h3>
<p>子議題：主動 ping 監控、schedule heartbeat（cron / batch job 守護）。Heartbeat 是 <em>被動 alert</em> 的補位 — cron 跑完該打 ping、ping 沒到就 alert；常見坑是 network 路徑或 outbound proxy 擋掉 ping、cron 其實正常但 Opsgenie 收不到、變成 false positive 半夜叫人。</p>
<h3 id="atlassian-整合深度">Atlassian 整合深度</h3>
<p>子議題：Issue creation / sync、SLA / OLA tracking、audit log。跟 PagerDuty + Jira webhook 比、Opsgenie 的差異是 <em>同身份體系 + native field mapping</em> — incident 直接綁 JSM ticket、Statuspage component 跟 Opsgenie service 同 schema、Confluence runbook 在 Opsgenie alert 內可直接 inline 預覽。</p>
<h3 id="team-based-routing-跟-service-ownership">Team-based routing 跟 service ownership</h3>
<p>子議題：team 對應 service / component 的 ownership model、global schedule 跟 team-local schedule 的分層、cross-team escalation（DB team alert escalate 到 platform team）。跟 PagerDuty 比 Opsgenie 的 team 是 first-class concept、跟 JSM project / Confluence space 雙向綁、ownership 邊界比 PagerDuty service 更貼近組織結構。</p>
<h3 id="atlassian-identity-sso--audit">Atlassian Identity SSO + audit</h3>
<p>子議題：<a href="https://www.atlassian.com/software/access">Atlassian Access</a> 統一 IdP SSO（Okta / Azure AD / Google Workspace）+ SCIM 自動 provision / deprovision、audit log 集中。沒走 Atlassian Access 的 Opsgenie 是 <em>身份孤島</em> — 離職員工 JSM 已 deprovision 但 Opsgenie schedule 還在、半夜還會被 page。</p>
<h3 id="opsgenie--jsm-cloud--jsm-premium-on-call-過渡">Opsgenie → JSM Cloud / JSM Premium on-call 過渡</h3>
<p>子議題：原 Opsgenie 用戶遷移時程（Atlassian 官方公告 2027/4 EOL）、功能 parity 盤點（migration 前確認 integration / API / Terraform config 都有對應）、API 兼容（Opsgenie REST API 在 JSM 上是否保留 / 改路徑）。migration 不是換工具、是換產品架構 — schedule / escalation / integration / runbook 的 ID 都會變、要規劃 <em>parallel run 期</em> 而非 cutover。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Alert 不觸發</strong>：integration / API key / routing rule</li>
<li><strong>Heartbeat false alarm</strong>：cron 跑了但 ping 沒到 / network</li>
<li><strong>Atlassian 整合斷裂</strong>：JSM permission / project mapping</li>
<li><strong>通知 missed</strong>：mobile app / push / SMS provider</li>
<li><strong>Escalation 跨時區壞掉</strong>：schedule timezone 設錯（team timezone vs user timezone）、override 把全 24hr 都蓋掉、final step 沒 fallback team — 跑 game day 驗證實際 paging 路徑、不只看 config</li>
<li><strong>Stale schedule</strong>：有人離職但 schedule 沒撤、半夜叫到前同事；走 Atlassian Access SCIM auto-deprovision、或定期 schedule audit</li>
<li><strong>Atlassian Cloud authentication trap</strong>：API token 過期 / 換 region / Atlassian Access policy 變更導致 integration 全斷；token 走 secret manager、Atlassian Access policy 變更前先 dry-run integration</li>
<li><strong>JSM migration drift</strong>：migration 期間 standalone Opsgenie 跟 JSM on-call 兩邊 schedule / escalation 不同步、alert 兩邊都觸發或都沒觸發；parallel run 期要有 <em>single source of truth</em> 跟 reconciliation script</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不在 Atlassian 生態</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
      </tr>
      <tr>
          <td>OSS 偏好</td>
          <td><a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a></td>
      </tr>
      <tr>
          <td>Slack-native IR</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>Microsoft Teams + IR</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>新案、Atlassian-heavy</td>
          <td>JSM Premium / Enterprise 內建 on-call（取代 Opsgenie standalone）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Jira Service Management 完整 ITSM workflow / Atlassian Cloud admin / Statuspage 細節</li>
<li>JSM Premium on-call 完整 feature set（屬 Atlassian product roadmap、跟 Opsgenie EOL 公告同期演進）</li>
<li>Atlassian Access 完整 IdP / SCIM 設定（屬 identity 模組）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>Opsgenie 是 Atlassian 自家產品</strong>：Atlassian 內部 incident routing / on-call 走 Opsgenie + Jira Service Management、其多租戶事故的協作流程是 Opsgenie 在大型 IR 場景的代表樣本。Atlassian-heavy enterprise 看這個案例的角度不是「PagerDuty 也能做」、而是「同身份體系 + JSM ticket / Confluence runbook / Statuspage 在 14 天事故內怎麼協作」— 這是 Opsgenie 在生態整合上的代表性場景。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">Atlassian cases</a></td>
          <td>14 天事故的 incident commander 輪值與 paging 節奏</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a>、<a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
</ul>
]]></content:encoded></item><item><title>Prometheus</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/</guid><description>&lt;p>Prometheus 是 CNCF graduated 的 metrics 系統、承擔三個責任：pull-based metrics scraping（service discovery + scrape）、PromQL 查詢與 recording rules、Alertmanager 告警與路由。設計取捨偏向「短中期 metrics + 簡單部署 + cloud-native 整合」、長期儲存交給 Mimir / Thanos / Cortex。是 Kubernetes 生態 metrics 的事實標準。&lt;/p>
&lt;p>對「K8s metrics、service metrics、需要 PromQL 表達能力、自管 metrics 棧」這條路徑、Prometheus 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker 跑起 Prometheus、配置 scrape target&lt;/li>
&lt;li>用 PromQL 查詢 metrics、寫 recording rules / alerting rules&lt;/li>
&lt;li>設計 service discovery（K8s / Consul / file_sd）&lt;/li>
&lt;li>看懂 cardinality 訊號、避免 label explosion&lt;/li>
&lt;li>評估長期儲存（Thanos / Mimir / Cortex）跟 remote write 的選擇&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-prometheus-跑起來">最短路徑：5 分鐘把 Prometheus 跑起來&lt;/h2>
&lt;p>先建最小 config 檔（Prometheus scrape 自己）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c"># prometheus.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">global&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scrape_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">scrape_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;prometheus&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;localhost:9090&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動並驗證：&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"># 1. 啟動 Prometheus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name prom -p 9090:9090 &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> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/prometheus.yml:/etc/prometheus/prometheus.yml&amp;#34;&lt;/span> &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> prom/prometheus
&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"># 2. 確認 target 正常（等 15 秒讓第一次 scrape 完成）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">curl -s http://localhost:9090/api/v1/targets &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.activeTargets[].health&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 查詢驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">curl -s &lt;span class="s1">&amp;#39;http://localhost:9090/api/v1/query?query=up&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.result[].value[1]&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>up&lt;/code> 回傳 &lt;code>&amp;quot;1&amp;quot;&lt;/code> 代表 Prometheus 能 scrape 自己。瀏覽器訪 &lt;code>http://localhost:9090&lt;/code> 可用 PromQL UI 互動查詢。實際 production 要配 retention、alerting rules 與 HA。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="scrape-配置與-service-discovery">Scrape 配置與 service discovery&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Static config：手動列 target、適合小規模&lt;/li>
&lt;li>File SD：動態檔案、適合外部系統推送&lt;/li>
&lt;li>Kubernetes SD：K8s API server 動態發現&lt;/li>
&lt;li>Consul SD：跟 Consul service registry 整合&lt;/li>
&lt;li>對應配置：&lt;code>scrape_configs&lt;/code> 區段&lt;/li>
&lt;/ul>
&lt;h3 id="promql-查詢">PromQL 查詢&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Instant query vs range query&lt;/li>
&lt;li>Aggregation：sum / avg / max / min / count + by / without&lt;/li>
&lt;li>Rate / increase（counter 處理）&lt;/li>
&lt;li>Histogram quantile（histogram_quantile + bucket）&lt;/li>
&lt;li>對應指令：HTTP API &lt;code>/api/v1/query&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="recording-rules--alerting-rules">Recording rules / Alerting rules&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Prometheus 是 CNCF graduated 的 metrics 系統、承擔三個責任：pull-based metrics scraping（service discovery + scrape）、PromQL 查詢與 recording rules、Alertmanager 告警與路由。設計取捨偏向「短中期 metrics + 簡單部署 + cloud-native 整合」、長期儲存交給 Mimir / Thanos / Cortex。是 Kubernetes 生態 metrics 的事實標準。</p>
<p>對「K8s metrics、service metrics、需要 PromQL 表達能力、自管 metrics 棧」這條路徑、Prometheus 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker 跑起 Prometheus、配置 scrape target</li>
<li>用 PromQL 查詢 metrics、寫 recording rules / alerting rules</li>
<li>設計 service discovery（K8s / Consul / file_sd）</li>
<li>看懂 cardinality 訊號、避免 label explosion</li>
<li>評估長期儲存（Thanos / Mimir / Cortex）跟 remote write 的選擇</li>
</ol>
<h2 id="最短路徑5-分鐘把-prometheus-跑起來">最短路徑：5 分鐘把 Prometheus 跑起來</h2>
<p>先建最小 config 檔（Prometheus scrape 自己）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;prometheus&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;localhost:9090&#34;</span><span class="p">]</span></span></span></code></pre></div><p>啟動並驗證：</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. 啟動 Prometheus</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name prom -p 9090:9090 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/prometheus.yml:/etc/prometheus/prometheus.yml&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  prom/prometheus
</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"># 2. 確認 target 正常（等 15 秒讓第一次 scrape 完成）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">curl -s http://localhost:9090/api/v1/targets <span class="p">|</span> jq <span class="s1">&#39;.data.activeTargets[].health&#39;</span>
</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="c1"># 3. 查詢驗證</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">curl -s <span class="s1">&#39;http://localhost:9090/api/v1/query?query=up&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.data.result[].value[1]&#39;</span></span></span></code></pre></div><p><code>up</code> 回傳 <code>&quot;1&quot;</code> 代表 Prometheus 能 scrape 自己。瀏覽器訪 <code>http://localhost:9090</code> 可用 PromQL UI 互動查詢。實際 production 要配 retention、alerting rules 與 HA。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="scrape-配置與-service-discovery">Scrape 配置與 service discovery</h3>
<p>子議題：</p>
<ul>
<li>Static config：手動列 target、適合小規模</li>
<li>File SD：動態檔案、適合外部系統推送</li>
<li>Kubernetes SD：K8s API server 動態發現</li>
<li>Consul SD：跟 Consul service registry 整合</li>
<li>對應配置：<code>scrape_configs</code> 區段</li>
</ul>
<h3 id="promql-查詢">PromQL 查詢</h3>
<p>子議題：</p>
<ul>
<li>Instant query vs range query</li>
<li>Aggregation：sum / avg / max / min / count + by / without</li>
<li>Rate / increase（counter 處理）</li>
<li>Histogram quantile（histogram_quantile + bucket）</li>
<li>對應指令：HTTP API <code>/api/v1/query</code></li>
</ul>
<h3 id="recording-rules--alerting-rules">Recording rules / Alerting rules</h3>
<p>子議題：</p>
<ul>
<li>Recording rules：預先計算昂貴 query、降低 dashboard 查詢成本</li>
<li>Alerting rules：定義 alert condition + for duration + labels / annotations</li>
<li>Alertmanager：去重 / 抑制 / 分組 / routing</li>
<li>對應配置：<code>rule_files</code></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="capacity-failure-modes/">Prometheus 容量規劃與故障模式</a>：單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀</li>
<li><a href="promql-recording-rules/">PromQL 與 Recording Rules 實務</a>：常見 SLI 查詢模式、recording rules 設計慣例、效能陷阱與故障判讀</li>
<li><a href="remote-write-long-term-storage/">Remote Write 與長期儲存整合</a>：remote write 配置、Mimir / Thanos / Cortex 三家比較、故障模式與容量規劃</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="high-availability">High availability</h3>
<p>子議題：</p>
<ul>
<li>Prometheus 沒原生 HA — 跑兩個 instance scrape 同 target、靠下游去重</li>
<li>Thanos：sidecar 模式、跨 Prometheus instance 查詢統一</li>
<li>Mimir：fully replicated metric storage（多 Prometheus → Mimir）</li>
<li>對應案例 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></li>
</ul>
<h3 id="cardinality-管理">Cardinality 管理</h3>
<p>對應案例 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a>。子議題：</p>
<ul>
<li>Cardinality = unique label combinations 數量</li>
<li>High-cardinality label（user_id / request_id / trace_id）會炸 Prometheus</li>
<li>偵測：<code>prometheus_tsdb_head_series</code> metric</li>
<li>修法：drop label / aggregation / 改用 traces backend（Honeycomb）</li>
</ul>
<h3 id="remote-write--read">Remote write / read</h3>
<p>子議題：</p>
<ul>
<li>Remote write：Prometheus → 長期儲存（Mimir / Cortex / Thanos / Datadog / Grafana Cloud）</li>
<li>Remote read：查詢時拉長期儲存資料</li>
<li>用 receiver / agent 模式（無 local TSDB）</li>
<li>對應配置：<code>remote_write</code> / <code>remote_read</code></li>
</ul>
<h3 id="exporters-生態">Exporters 生態</h3>
<p>子議題：</p>
<ul>
<li>Node exporter（host metrics）</li>
<li>Blackbox exporter（HTTP / TCP / ICMP probing）</li>
<li>Database exporters（postgres / mysql / redis）</li>
<li>應用層 metrics：用 client library（prometheus_client）原生暴露</li>
<li>對應 ServiceMonitor / PodMonitor（Prometheus Operator）</li>
</ul>
<h3 id="prometheus-operatork8s">Prometheus Operator（K8s）</h3>
<p>子議題：</p>
<ul>
<li>CRD：Prometheus / ServiceMonitor / PodMonitor / PrometheusRule / Alertmanager</li>
<li>自動發現 ServiceMonitor 物件、不手動改 scrape config</li>
<li>kube-prometheus-stack Helm chart</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a> 對照</li>
</ul>
<h3 id="pull-vs-push-model">Pull vs Push model</h3>
<p>子議題：</p>
<ul>
<li>Pull model（Prometheus default）：service discovery、health check 自然</li>
<li>Push model（Pushgateway）：適合 short-lived job、不建議常駐 service</li>
<li>為何 Pushgateway 不推：cardinality 不易管、scrape semantics 違反</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="scrape-failure">Scrape failure</h3>
<p>操作原則：先看 target 是否健康、再看 network 跟認證。</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">curl -s http://localhost:9090/api/v1/targets <span class="p">|</span> jq <span class="s1">&#39;.data.activeTargets[] | {job: .labels.job, health, lastError}&#39;</span></span></span></code></pre></div><h3 id="cardinality-explosion">Cardinality explosion</h3>
<p>操作原則：series 數量持續增長、可能 OOM。</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">curl -s <span class="s1">&#39;http://localhost:9090/api/v1/query?query=prometheus_tsdb_head_series&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.data.result[].value[1]&#39;</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak</a> 的處理路徑。</p>
<h3 id="query-過慢">Query 過慢</h3>
<p>操作原則：query 過大範圍 / aggregation 過多 → Recording rules 預先聚合。</p>
<h3 id="alert-flapping--noise">Alert flapping / noise</h3>
<p>操作原則：alert 觸發頻繁但無實際問題、調整 <code>for:</code> duration、加 absent() check、用 Alertmanager inhibition。</p>
<h3 id="memory-pressure">Memory pressure</h3>
<p>操作原則：Prometheus retention 跟 cardinality 決定 memory。判讀：cardinality 太大 → remote write 卸載長期儲存。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>長期 retention（年級）</td>
          <td>Thanos / Mimir / Cortex / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Cloud</a></td>
      </tr>
      <tr>
          <td>需要 logs / traces</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> (Loki/Tempo) / <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
      <tr>
          <td>Auto-instrumentation</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> + Prometheus exporter</td>
      </tr>
      <tr>
          <td>SaaS turnkey</td>
          <td><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></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> + Managed Prometheus</td>
      </tr>
      <tr>
          <td>Pure push model</td>
          <td>StatsD / InfluxDB（不在本模組）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>PromQL 完整 syntax reference（prometheus.io/docs/prometheus/latest/querying/）</li>
<li>Exporter 內部實作</li>
<li>Alertmanager routing tree 細節</li>
<li>Operator CRD spec</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>Cardinality 管理 / freshness 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></td>
          <td>AWS Distro + Prometheus 整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></td>
          <td>K8s metrics + Prometheus 規模化</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Prometheus 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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></td>
          <td>從 Prometheus + Datadog 雙軌走向 OTel 對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Prometheus 指標跟新管線的語意對不齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型單 instance / 中型 Operator / 大型 + Mimir</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Mimir）、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>Valkey</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/</guid><description>&lt;p>Valkey 是 2024 年從 Redis 7.2.4 fork 的開源專案、承擔三個責任：維持 Redis API 相容（drop-in 替換）、提供 OSI 認可的開源授權（BSD 3-clause）、由 Linux Foundation 託管避免單一公司控制。設計取捨偏向「相容 Redis 既有 client / 工具 + 開源治理透明 + 多雲廠商共同維護」、不追求功能超越 Redis Inc。&lt;/p>
&lt;p>對「既有 Redis 部署、需要 OSI 認可授權、多雲避免 vendor lock-in、合規敏感」這條路徑、Valkey 是 Redis 的替代首選。AWS / Google / Oracle / Ericsson 等共同支援、AWS ElastiCache 已把 Valkey 設為 default engine。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Valkey、用 redis-cli 驗證 API 相容性&lt;/li>
&lt;li>評估從 Redis 遷移到 Valkey 的相容性風險（module / Stack 功能）&lt;/li>
&lt;li>看懂 Valkey vs Redis Inc 的版本對應跟功能差距&lt;/li>
&lt;li>評估管雲端 managed Valkey（ElastiCache）的選用判斷&lt;/li>
&lt;li>區分 Valkey 跟 Redis 商業版本對你的合規 / 採購 / SLA 影響&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-valkey-跑起來">最短路徑：5 分鐘把 Valkey 跑起來&lt;/h2>





&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"># 1. 啟動 Valkey（Redis API 相容、可直接用 redis-cli）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name valkey -p 6379:6379 valkey/valkey:8
&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"># 2. 驗證讀寫（valkey-cli 與 redis-cli 命令一致）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本：Valkey 同時回報相容的 redis_version 與自身 valkey_version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version|server_name&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 class="c1"># redis_version:7.2.4 ← client library 以此判斷相容性（fork 自 Redis 7.2.4）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># server_name:valkey&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自身版本&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步是相容性的關鍵證據：既有 Redis client library 看到 &lt;code>redis_version:7.2.4&lt;/code> 就以 Redis 7.2.4 的行為運作、無需改 code；&lt;code>valkey_version&lt;/code> 才是 Valkey 自身的演進線。實機驗證於 valkey/valkey:8 image、最後檢查日 2026-06-16。實際遷移路徑見&lt;a href="#%e5%be%9e-redis-%e9%81%b7%e7%a7%bb">進階主題：從 Redis 遷移&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>valkey-cli vs redis-cli：兩個 binary 都可連 Valkey、命令一致&lt;/li>
&lt;li>Client library 配置：所有 Redis client 自動相容（無需 Valkey-specific client）&lt;/li>
&lt;li>對應指令範例：&lt;code>INFO server&lt;/code> 顯示 valkey_version 而非 redis_version&lt;/li>
&lt;/ul>
&lt;h3 id="跟-redis-的相容邊界">跟 Redis 的相容邊界&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Valkey 是 2024 年從 Redis 7.2.4 fork 的開源專案、承擔三個責任：維持 Redis API 相容（drop-in 替換）、提供 OSI 認可的開源授權（BSD 3-clause）、由 Linux Foundation 託管避免單一公司控制。設計取捨偏向「相容 Redis 既有 client / 工具 + 開源治理透明 + 多雲廠商共同維護」、不追求功能超越 Redis Inc。</p>
<p>對「既有 Redis 部署、需要 OSI 認可授權、多雲避免 vendor lock-in、合規敏感」這條路徑、Valkey 是 Redis 的替代首選。AWS / Google / Oracle / Ericsson 等共同支援、AWS ElastiCache 已把 Valkey 設為 default engine。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Valkey、用 redis-cli 驗證 API 相容性</li>
<li>評估從 Redis 遷移到 Valkey 的相容性風險（module / Stack 功能）</li>
<li>看懂 Valkey vs Redis Inc 的版本對應跟功能差距</li>
<li>評估管雲端 managed Valkey（ElastiCache）的選用判斷</li>
<li>區分 Valkey 跟 Redis 商業版本對你的合規 / 採購 / SLA 影響</li>
</ol>
<h2 id="最短路徑5-分鐘把-valkey-跑起來">最短路徑：5 分鐘把 Valkey 跑起來</h2>





<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. 啟動 Valkey（Redis API 相容、可直接用 redis-cli）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name valkey -p 6379:6379 valkey/valkey:8
</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"># 2. 驗證讀寫（valkey-cli 與 redis-cli 命令一致）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli SET foo bar   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli GET foo       <span class="c1"># → bar</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"># 3. 確認版本：Valkey 同時回報相容的 redis_version 與自身 valkey_version</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version|server_name&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 以此判斷相容性（fork 自 Redis 7.2.4）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># server_name:valkey</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自身版本</span></span></span></code></pre></div><p>第三步是相容性的關鍵證據：既有 Redis client library 看到 <code>redis_version:7.2.4</code> 就以 Redis 7.2.4 的行為運作、無需改 code；<code>valkey_version</code> 才是 Valkey 自身的演進線。實機驗證於 valkey/valkey:8 image、最後檢查日 2026-06-16。實際遷移路徑見<a href="#%e5%be%9e-redis-%e9%81%b7%e7%a7%bb">進階主題：從 Redis 遷移</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>valkey-cli vs redis-cli：兩個 binary 都可連 Valkey、命令一致</li>
<li>Client library 配置：所有 Redis client 自動相容（無需 Valkey-specific client）</li>
<li>對應指令範例：<code>INFO server</code> 顯示 valkey_version 而非 redis_version</li>
</ul>
<h3 id="跟-redis-的相容邊界">跟 Redis 的相容邊界</h3>
<p>子議題：</p>
<ul>
<li>Core data types / commands：100% 相容（fork 自 Redis 7.2.4）</li>
<li>Eviction / persistence / cluster：相容</li>
<li>Pub/Sub / Streams：相容</li>
<li><strong>不相容</strong>：Redis 7.4+ 引入的功能、Redis Stack 商業 modules</li>
</ul>
<h3 id="遷移評估">遷移評估</h3>
<p>子議題：</p>
<ul>
<li>AOF / RDB 文件格式相容、可直接拷貝資料目錄</li>
<li>Client library 完全相容、無需改 code</li>
<li>監控工具相容（RedisInsight 雖偏 Redis Inc、但基本命令通用）</li>
<li>需確認 modules 使用狀況（Stack modules 未必有 Valkey fork）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="從-redis-遷移">從 Redis 遷移</h3>
<p>子議題：</p>
<ul>
<li>評估 module 使用：列出當前使用的 Redis modules、確認 Valkey 對應替代</li>
<li>評估 Redis 7.4+ 功能使用（Functions、CLIENT NO-TOUCH 等）</li>
<li>遷移路徑：rolling restart with replica swap / 雙寫 / 直接 cutover</li>
<li>對應雲端 managed：AWS ElastiCache for Valkey 自動遷移工具</li>
</ul>
<h3 id="授權合規評估">授權合規評估</h3>
<p>子議題：</p>
<ul>
<li>為何 Redis 改 RSALv2 / SSPL — OSI 認知（不算 OSI 認可開源）</li>
<li>Valkey BSD 3-clause — 商業使用無限制</li>
<li>對 SaaS 供應商：Redis 限制把 Redis 當成 service 對外提供、Valkey 無此限制</li>
<li>對企業 / 公部門：開源合規政策可能要求 OSI 認可、Valkey 通過、Redis 不過</li>
</ul>
<h3 id="module-生態相容性">Module 生態相容性</h3>
<p>子議題：</p>
<ul>
<li>Valkey 計畫自有 modules（valkey-search / valkey-bloom 等）</li>
<li>Redis Stack modules（RedisJSON / RedisSearch）部分有 fork</li>
<li>評估你用的 modules 是否有 Valkey 替代、否則考慮遷 module-free 設計</li>
</ul>
<h3 id="雲端-managed-valkey">雲端 managed Valkey</h3>
<p>子議題：</p>
<ul>
<li>AWS ElastiCache for Valkey（成本比 Redis 低 ~20%、AWS 推）</li>
<li>GCP Memorystore（規劃 Valkey 支援）</li>
<li>Azure Cache（規劃中）</li>
<li>managed 邊界跟 ElastiCache for Redis 一致</li>
</ul>
<h3 id="跟-redis-8-的功能差距">跟 Redis 8 的功能差距</h3>
<p>子議題：</p>
<ul>
<li>Redis 8 新功能對 Valkey 的影響（功能落後幾個月）</li>
<li>Valkey 自有 roadmap（valkey.io/blog 追蹤）</li>
<li>何時 Redis 新功能值得遷回（罕見、通常 Valkey 跟上）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="client-連不上api-相容問題">Client 連不上（API 相容問題）</h3>
<p>操作原則：先確認 Valkey 回報的相容版本、再對照 client library 支援到 Redis 哪個版本。</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">valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 用這個判斷相容性</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># valkey_version:8.1.8</span></span></span></code></pre></div><p>絕大多數情況直接相容、若失敗多是 client library 太舊（不支援 Redis 7.2 對應版本）。</p>
<h3 id="module-不可用">Module 不可用</h3>
<p>操作原則：Valkey 對 Redis Stack modules 不一定有 fork、看 Valkey modules 清單。</p>
<h3 id="監控工具相容性">監控工具相容性</h3>
<p>操作原則：RedisInsight 連 Valkey 可能 partial 工作（部分 vendor-specific 命令缺）、用通用工具（valkey-cli、Prometheus + redis_exporter）較穩。</p>
<h3 id="performance-regressionvs-redis">Performance regression（vs Redis）</h3>
<p>操作原則：Valkey 跟 Redis 7.2.4 為 baseline、效能應接近、差距 &lt; 5% 屬於正常。明顯回歸要看 Valkey roadmap 是否有 known issue。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>依賴 Redis Stack 商業 modules</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（Redis Inc 商業版）</td>
      </tr>
      <tr>
          <td>純 KV cache 不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>極高 throughput / 多核</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（已 default Valkey）</td>
      </tr>
      <tr>
          <td>Durable Redis-compatible</td>
          <td>AWS MemoryDB</td>
      </tr>
      <tr>
          <td>跨雲 fully-portable</td>
          <td>Valkey self-host（無 vendor lock-in）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 Valkey command reference（valkey.io/commands）</li>
<li>Linux Foundation governance 細節</li>
<li>各語言 client compatibility matrix</li>
<li>Redis Stack module 對應替代清單</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例沿用-redis-同源案例--待補-valkey-specific-case">直接相關案例（沿用 Redis 同源案例 + 待補 Valkey-specific case）</h3>
<p>Valkey 從 Redis 7.2.4 fork、API 與行為 100% 相容、Redis-on-Valkey 同源案例可直接套用。截至本文時 Valkey-specific production case 仍累積中。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Valkey 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移策略 client-side 實作、Valkey 跟 Redis 行為一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 在 Valkey 上跟 Redis 同樣 API、無遷移風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>invalidation / shard move 一致性議題、Valkey Cluster 沿用 Redis Cluster 模型</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Valkey-specific 案例</strong>：Linux Foundation Valkey customer adoption stories、AWS ElastiCache for Valkey 客戶遷移個案、re:Invent 2025+ talks、企業 OSI 合規驅動的遷移路徑公開分享。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Valkey 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>Valkey 跟 Redis 規模化路徑一致（fork 同源）、小型 single / 中型 Sentinel / 大型 Cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter / singleflight 通用、Valkey 行為跟 Redis 一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>Memcached routing 案例、Valkey 對應為 Cluster + client-side routing 或 Envoy Redis proxy</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 為 Memcached based、Valkey 對應為 Global Datastore（ElastiCache for Valkey）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（fork 源頭）、<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a></li>
<li>下游能力：跟 Redis 完全一致、見 Redis vendor 頁的下游連結</li>
</ul>
]]></content:encoded></item><item><title>0.2 狀態與資料儲存選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/</guid><description>&lt;p>狀態與資料儲存選型的核心原則是先判斷資料責任。正式狀態、暫存資料、搜尋索引、事件歷史與大型檔案都屬於資料，但它們需要不同服務能力。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage&lt;/a>&lt;/li>
&lt;li>用資料生命週期判斷儲存服務類型&lt;/li>
&lt;li>看懂資料庫與 Redis、搜尋引擎、event store、object storage 的差異&lt;/li>
&lt;li>把資料選型轉成可檢查的工程判斷&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察資料類型不同儲存責任也不同">【觀察】資料類型不同，儲存責任也不同&lt;/h2>
&lt;p>資料儲存服務的第一個問題是「這份資料扮演什麼責任」。同一份商品資料可以同時出現在 PostgreSQL、Redis、Elasticsearch、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage&lt;/a> 裡，但每個位置的責任不同。&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>正式狀態&lt;/td>
 &lt;td>需要交易、一致性、查詢與長期保存&lt;/td>
 &lt;td>SQL / document &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>暫存讀取&lt;/td>
 &lt;td>來源資料已存在，目標是降低讀取成本&lt;/td>
 &lt;td>Redis / cache&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>搜尋查詢&lt;/td>
 &lt;td>需要&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/full-text-search/" data-link-title="Full-Text Search" data-link-desc="說明全文檢索如何處理關鍵字匹配、語言分析與排序">全文搜尋&lt;/a>、排序、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/facet-query/" data-link-title="Facet Query" data-link-desc="說明分面查詢如何提供分類統計與篩選體驗">facet&lt;/a>、相關性&lt;/td>
 &lt;td>search engine&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件歷史&lt;/td>
 &lt;td>需要追蹤發生過的事、audit、replay&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> / stream&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大型檔案&lt;/td>
 &lt;td>需要保存圖片、影片、報表、備份&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是索引。選型時要看資料是否能重建、是否需要一致性、是否要被使用者查詢、是否承擔稽核責任。&lt;/p>
&lt;h2 id="判讀source-of-truth-承擔正式狀態">【判讀】source of truth 承擔正式狀態&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Source of truth&lt;/a> 的核心責任是保存系統承認的正式狀態。當資料需要被交易保護、被多個流程共同讀寫、支援一致查詢與長期保存時，應先評估資料庫。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>訂單狀態：created、paid、shipped、refunded&lt;/li>
&lt;li>會員帳號：email、password hash、角色、訂閱方案&lt;/li>
&lt;li>付款紀錄：交易 ID、金額、貨幣、狀態、時間&lt;/li>
&lt;/ul>
&lt;p>這類資料的主要風險是寫入一致性。服務要知道誰能改狀態、哪些欄位要一起成功、失敗後如何重試或補償。這些問題通常屬於資料庫與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 邊界。&lt;/p>
&lt;h2 id="判讀cache-承擔可重建的讀取加速">【判讀】cache 承擔可重建的讀取加速&lt;/h2>
&lt;p>cache 的核心責任是降低讀取成本。快取資料應該能從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 或下游服務重建；它的價值在於吸收熱門讀取、降低延遲、保護正式資料來源。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>商品詳情頁快取商品名稱、價格與庫存摘要&lt;/li>
&lt;li>使用者 session 或權限摘要&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> presence 狀態與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 訂閱集合&lt;/li>
&lt;/ul>
&lt;p>這類資料的主要風險是過期與不一致。服務要知道 cache &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a> 怎麼處理、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 如何設定、資料更新時如何失效、熱門 key 如何保護。&lt;/p>
&lt;h2 id="判讀search-index-承擔查詢體驗">【判讀】search index 承擔查詢體驗&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">Search index&lt;/a> 的核心責任是支援搜尋體驗。當使用者需要&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/full-text-search/" data-link-title="Full-Text Search" data-link-desc="說明全文檢索如何處理關鍵字匹配、語言分析與排序">全文搜尋&lt;/a>、排序、filter、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/facet-query/" data-link-title="Facet Query" data-link-desc="說明分面查詢如何提供分類統計與篩選體驗">facet&lt;/a>、autocomplete 或相關性排序，搜尋索引通常比一般資料庫查詢更合適。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>電商商品搜尋與分類篩選&lt;/li>
&lt;li>文件站全文搜尋&lt;/li>
&lt;li>企業知識庫搜尋與權限過濾&lt;/li>
&lt;/ul>
&lt;p>這類資料的主要風險是索引延遲與查詢語意。正式狀態通常仍在資料庫，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index&lt;/a> 是為搜尋體驗建立的讀取模型。服務要知道資料更新後多久進索引、搜尋結果是否允許短暫延遲。&lt;/p>
&lt;h2 id="判讀event-log-承擔歷史與重播">【判讀】event log 承擔歷史與重播&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log&lt;/a> 的核心責任是保存已發生的事。當系統需要 audit、replay、補送、狀態重建或跨服務事件傳遞，事件歷史就需要獨立設計。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>訂單狀態每次改變都要留下 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>&lt;/li>
&lt;li>付款成功事件需要被通知、出貨、分析系統各自消費&lt;/li>
&lt;li>使用者行為事件需要進入分析 pipeline&lt;/li>
&lt;/ul>
&lt;p>這類資料的主要風險是順序、重複與 schema 演進。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log&lt;/a> 要說明事件代表哪個 domain fact、如何去重、如何處理舊版本 payload。&lt;/p></description><content:encoded><![CDATA[<p>狀態與資料儲存選型的核心原則是先判斷資料責任。正式狀態、暫存資料、搜尋索引、事件歷史與大型檔案都屬於資料，但它們需要不同服務能力。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、cache、<a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a>、<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 與 <a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a></li>
<li>用資料生命週期判斷儲存服務類型</li>
<li>看懂資料庫與 Redis、搜尋引擎、event store、object storage 的差異</li>
<li>把資料選型轉成可檢查的工程判斷</li>
</ol>
<hr>
<h2 id="觀察資料類型不同儲存責任也不同">【觀察】資料類型不同，儲存責任也不同</h2>
<p>資料儲存服務的第一個問題是「這份資料扮演什麼責任」。同一份商品資料可以同時出現在 PostgreSQL、Redis、Elasticsearch、<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 與 <a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a> 裡，但每個位置的責任不同。</p>
<table>
  <thead>
      <tr>
          <th>資料責任</th>
          <th>可觀察特徵</th>
          <th>常見服務方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>正式狀態</td>
          <td>需要交易、一致性、查詢與長期保存</td>
          <td>SQL / document <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a></td>
      </tr>
      <tr>
          <td>暫存讀取</td>
          <td>來源資料已存在，目標是降低讀取成本</td>
          <td>Redis / cache</td>
      </tr>
      <tr>
          <td>搜尋查詢</td>
          <td>需要<a href="/blog/backend/knowledge-cards/full-text-search/" data-link-title="Full-Text Search" data-link-desc="說明全文檢索如何處理關鍵字匹配、語言分析與排序">全文搜尋</a>、排序、<a href="/blog/backend/knowledge-cards/facet-query/" data-link-title="Facet Query" data-link-desc="說明分面查詢如何提供分類統計與篩選體驗">facet</a>、相關性</td>
          <td>search engine</td>
      </tr>
      <tr>
          <td>事件歷史</td>
          <td>需要追蹤發生過的事、audit、replay</td>
          <td><a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> / stream</td>
      </tr>
      <tr>
          <td>大型檔案</td>
          <td>需要保存圖片、影片、報表、備份</td>
          <td><a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a></td>
      </tr>
  </tbody>
</table>
<p>這張表是索引。選型時要看資料是否能重建、是否需要一致性、是否要被使用者查詢、是否承擔稽核責任。</p>
<h2 id="判讀source-of-truth-承擔正式狀態">【判讀】source of truth 承擔正式狀態</h2>
<p><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Source of truth</a> 的核心責任是保存系統承認的正式狀態。當資料需要被交易保護、被多個流程共同讀寫、支援一致查詢與長期保存時，應先評估資料庫。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>訂單狀態：created、paid、shipped、refunded</li>
<li>會員帳號：email、password hash、角色、訂閱方案</li>
<li>付款紀錄：交易 ID、金額、貨幣、狀態、時間</li>
</ul>
<p>這類資料的主要風險是寫入一致性。服務要知道誰能改狀態、哪些欄位要一起成功、失敗後如何重試或補償。這些問題通常屬於資料庫與 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 邊界。</p>
<h2 id="判讀cache-承擔可重建的讀取加速">【判讀】cache 承擔可重建的讀取加速</h2>
<p>cache 的核心責任是降低讀取成本。快取資料應該能從 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 或下游服務重建；它的價值在於吸收熱門讀取、降低延遲、保護正式資料來源。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>商品詳情頁快取商品名稱、價格與庫存摘要</li>
<li>使用者 session 或權限摘要</li>
<li><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> presence 狀態與 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 訂閱集合</li>
</ul>
<p>這類資料的主要風險是過期與不一致。服務要知道 cache <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a> 怎麼處理、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 如何設定、資料更新時如何失效、熱門 key 如何保護。</p>
<h2 id="判讀search-index-承擔查詢體驗">【判讀】search index 承擔查詢體驗</h2>
<p><a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">Search index</a> 的核心責任是支援搜尋體驗。當使用者需要<a href="/blog/backend/knowledge-cards/full-text-search/" data-link-title="Full-Text Search" data-link-desc="說明全文檢索如何處理關鍵字匹配、語言分析與排序">全文搜尋</a>、排序、filter、<a href="/blog/backend/knowledge-cards/facet-query/" data-link-title="Facet Query" data-link-desc="說明分面查詢如何提供分類統計與篩選體驗">facet</a>、autocomplete 或相關性排序，搜尋索引通常比一般資料庫查詢更合適。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>電商商品搜尋與分類篩選</li>
<li>文件站全文搜尋</li>
<li>企業知識庫搜尋與權限過濾</li>
</ul>
<p>這類資料的主要風險是索引延遲與查詢語意。正式狀態通常仍在資料庫，<a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a> 是為搜尋體驗建立的讀取模型。服務要知道資料更新後多久進索引、搜尋結果是否允許短暫延遲。</p>
<h2 id="判讀event-log-承擔歷史與重播">【判讀】event log 承擔歷史與重播</h2>
<p><a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log</a> 的核心責任是保存已發生的事。當系統需要 audit、replay、補送、狀態重建或跨服務事件傳遞，事件歷史就需要獨立設計。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>訂單狀態每次改變都要留下 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a></li>
<li>付款成功事件需要被通知、出貨、分析系統各自消費</li>
<li>使用者行為事件需要進入分析 pipeline</li>
</ul>
<p>這類資料的主要風險是順序、重複與 schema 演進。<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log</a> 要說明事件代表哪個 domain fact、如何去重、如何處理舊版本 payload。</p>
<h2 id="判讀object-storage-承擔大型非結構化資料">【判讀】object storage 承擔大型非結構化資料</h2>
<p><a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">Object storage</a> 的核心責任是保存大型 blob。當資料是圖片、影片、PDF、匯出報表、備份檔或模型檔案，儲存服務通常需要 object storage，而正式 metadata 放在資料庫。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>使用者上傳的大頭貼、附件與影片</li>
<li>每日報表匯出的 CSV 或 PDF</li>
<li>系統備份、稽核封存與資料匯出檔</li>
</ul>
<p>這類資料的主要風險是存取權限、生命週期、版本與連結有效性。資料庫保存 object key、owner、狀態與 metadata；<a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a> 保存實際檔案內容。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入資料儲存實作章節：</p>
<ol>
<li>每一類資料的責任是否明確（正式狀態、快取、搜尋、事件、檔案）</li>
<li>每一類資料的真實來源是否明確（source of truth 在哪裡）</li>
<li>每一類資料是否定義一致性與延遲容忍度</li>
<li>每一類資料是否定義保留期限與回復方式</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01-database</a></li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02-cache-redis</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>資料儲存選型要先問資料責任。正式狀態進資料庫，可重建讀取資料進快取，搜尋體驗用 <a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a>，歷史與重播用 <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>，大型檔案用 <a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a>。責任分清楚後，同一份業務資料可以出現在多個服務中，但每個服務的位置都能被解釋。</p>
]]></content:encoded></item><item><title>2.2 cache aside 與失效策略</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/</guid><description>&lt;p>旁路快取（cache aside）的核心責任是把讀取加速與正式狀態分離。資料庫維持 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，快取維持可重建副本；兩者透過失效策略與新鮮度窗口對齊。&lt;/p>
&lt;h2 id="基本流程">基本流程&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside&lt;/a> 的讀路徑是「先讀 cache，miss 後回源，再回填 cache」；寫路徑是「先寫 source of truth，再做 cache invalidation 或版本更新」。這個流程讓正式狀態維持單一責任，同時讓熱門讀取獲得低延遲。&lt;/p>
&lt;p>實務上要先定義 freshness window。每個資料類型可容忍的不新鮮時間不同：商品介紹可接受秒級延遲，價格、庫存、權限與配額則需要更短窗口或即時失效。&lt;/p>
&lt;h2 id="失效策略">失效策略&lt;/h2>
&lt;p>失效策略的責任是控制 cache 和 source of truth 之間的偏差。常見做法有三類：&lt;/p>
&lt;ol>
&lt;li>事件驅動失效：寫入成功後推事件刪 key 或更新版本，適合正確性要求高的資料。&lt;/li>
&lt;li>TTL 失效：以時間上限控制資料壽命，適合可短暫不新鮮的資料。&lt;/li>
&lt;li>混合策略：事件失效為主、TTL 為保底，適合多來源寫入或跨區快取。&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data&lt;/a> 是快取系統的常態成本，視為例外事件會導致設計盲區。設計時要先定義可接受的 stale 形式，再設計對應補償與回退路徑。&lt;/p>
&lt;h3 id="應用層--邊緣層-invalidation-pipeline">應用層 + 邊緣層 Invalidation Pipeline&lt;/h3>
&lt;p>當系統同時用應用層快取（Redis、本機 cache）跟邊緣層快取（&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN&lt;/a>）時、失效策略要把兩層當「一條 pipeline」設計、不能各自獨立 purge。兩層失效的物理特性差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>Purge 控制&lt;/th>
 &lt;th>Purge 延遲&lt;/th>
 &lt;th>失敗代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>應用層 cache&lt;/td>
 &lt;td>自家 cluster 內、application 控制&lt;/td>
 &lt;td>毫秒 - 秒級（cache cluster 內傳播）&lt;/td>
 &lt;td>Cluster 內 stale、用戶感受立即修正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>Vendor API 控制、全球節點同步&lt;/td>
 &lt;td>秒 - 分鐘級（傳統 origin pull）或 150ms 級（push-based）&lt;/td>
 &lt;td>全球節點 stale、回填到應用層污染快取&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>正確順序是「先應用層、再 CDN」：&lt;/p>
&lt;ol>
&lt;li>業務寫入完成、source of truth 更新&lt;/li>
&lt;li>Purge 應用層 cache（毫秒級完成）&lt;/li>
&lt;li>Purge CDN（秒級到分鐘級）&lt;/li>
&lt;li>等 CDN purge 完成的 ack（或設等待窗口）&lt;/li>
&lt;/ol>
&lt;p>順序顛倒會出事 — 若先 purge CDN、CDN 全球節點 miss 後到 origin 拉資料、若 origin 應用層還是舊 cache、CDN 會把舊資料回填到全球節點、stale 被「重新永久化」一個 TTL 週期。&lt;/p>
&lt;p>實務上的權衡是「CDN purge ack 是否要等」。等了會讓 write API latency 升高到秒級、不等則必須接受短暫雙層不一致。價格 / 庫存類資料適合「短 TTL + 等 purge ack」、blog 文章類適合「長 TTL + 不等 ack」。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a> 的 purge 操作模型。&lt;/p>
&lt;h2 id="cache-aside-vs-write-through-的選擇">Cache aside vs write-through 的選擇&lt;/h2>
&lt;p>選 cache 模式由 &lt;em>miss 成本&lt;/em> 跟 &lt;em>寫入頻率&lt;/em> 的取捨決定。Cache aside、write-through、write-behind 三種主流模式各自適合不同業務壓力。&lt;/p>
&lt;p>&lt;strong>Cache aside&lt;/strong>（read-through）：寫入只動 source-of-truth、讀取 miss 時才填 cache。適合寫入頻率低於讀取、cache 可以重建、寫入失敗時 cache 保持不污染的場景。常見於商品詳情、推薦列表、設定值這類 read-heavy 資料、業務代價是 cache miss 時用戶等待回源、可接受。&lt;/p></description><content:encoded><![CDATA[<p>旁路快取（cache aside）的核心責任是把讀取加速與正式狀態分離。資料庫維持 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，快取維持可重建副本；兩者透過失效策略與新鮮度窗口對齊。</p>
<h2 id="基本流程">基本流程</h2>
<p><a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a> 的讀路徑是「先讀 cache，miss 後回源，再回填 cache」；寫路徑是「先寫 source of truth，再做 cache invalidation 或版本更新」。這個流程讓正式狀態維持單一責任，同時讓熱門讀取獲得低延遲。</p>
<p>實務上要先定義 freshness window。每個資料類型可容忍的不新鮮時間不同：商品介紹可接受秒級延遲，價格、庫存、權限與配額則需要更短窗口或即時失效。</p>
<h2 id="失效策略">失效策略</h2>
<p>失效策略的責任是控制 cache 和 source of truth 之間的偏差。常見做法有三類：</p>
<ol>
<li>事件驅動失效：寫入成功後推事件刪 key 或更新版本，適合正確性要求高的資料。</li>
<li>TTL 失效：以時間上限控制資料壽命，適合可短暫不新鮮的資料。</li>
<li>混合策略：事件失效為主、TTL 為保底，適合多來源寫入或跨區快取。</li>
</ol>
<p><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 是快取系統的常態成本，視為例外事件會導致設計盲區。設計時要先定義可接受的 stale 形式，再設計對應補償與回退路徑。</p>
<h3 id="應用層--邊緣層-invalidation-pipeline">應用層 + 邊緣層 Invalidation Pipeline</h3>
<p>當系統同時用應用層快取（Redis、本機 cache）跟邊緣層快取（<a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN</a>）時、失效策略要把兩層當「一條 pipeline」設計、不能各自獨立 purge。兩層失效的物理特性差異：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>Purge 控制</th>
          <th>Purge 延遲</th>
          <th>失敗代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用層 cache</td>
          <td>自家 cluster 內、application 控制</td>
          <td>毫秒 - 秒級（cache cluster 內傳播）</td>
          <td>Cluster 內 stale、用戶感受立即修正</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>Vendor API 控制、全球節點同步</td>
          <td>秒 - 分鐘級（傳統 origin pull）或 150ms 級（push-based）</td>
          <td>全球節點 stale、回填到應用層污染快取</td>
      </tr>
  </tbody>
</table>
<p>正確順序是「先應用層、再 CDN」：</p>
<ol>
<li>業務寫入完成、source of truth 更新</li>
<li>Purge 應用層 cache（毫秒級完成）</li>
<li>Purge CDN（秒級到分鐘級）</li>
<li>等 CDN purge 完成的 ack（或設等待窗口）</li>
</ol>
<p>順序顛倒會出事 — 若先 purge CDN、CDN 全球節點 miss 後到 origin 拉資料、若 origin 應用層還是舊 cache、CDN 會把舊資料回填到全球節點、stale 被「重新永久化」一個 TTL 週期。</p>
<p>實務上的權衡是「CDN purge ack 是否要等」。等了會讓 write API latency 升高到秒級、不等則必須接受短暫雙層不一致。價格 / 庫存類資料適合「短 TTL + 等 purge ack」、blog 文章類適合「長 TTL + 不等 ack」。詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的 purge 操作模型。</p>
<h2 id="cache-aside-vs-write-through-的選擇">Cache aside vs write-through 的選擇</h2>
<p>選 cache 模式由 <em>miss 成本</em> 跟 <em>寫入頻率</em> 的取捨決定。Cache aside、write-through、write-behind 三種主流模式各自適合不同業務壓力。</p>
<p><strong>Cache aside</strong>（read-through）：寫入只動 source-of-truth、讀取 miss 時才填 cache。適合寫入頻率低於讀取、cache 可以重建、寫入失敗時 cache 保持不污染的場景。常見於商品詳情、推薦列表、設定值這類 read-heavy 資料、業務代價是 cache miss 時用戶等待回源、可接受。</p>
<p><strong>Write-through</strong>：寫入同時動 source-of-truth + cache、保證 cache 永遠最新。對應 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify Write-through Cache</a> — Shopify 在 Shop App 後端的 read-heavy 路徑用 write-through 降低 cache miss 風險、改善熱門資料讀取穩定性。適合場景：cache miss 成本很高（回源慢或會壓垮 origin）、寫入流量可控、資料更新時間可預測。典型應用包括熱門商品的庫存 / 價格、用戶 session、需要避免讀路徑抖動的場景。</p>
<p><strong>Write-behind</strong>（async）：寫入只動 cache、async 同步到 source-of-truth。適合寫入頻率極高、source-of-truth 跟不上、可接受 cache crash 丟失少量資料的場景。常見於 counter、rate limit、metrics aggregation 這類 <em>吞吐優先、可接受短暫不持久</em> 的資料。代價是 cache crash 會丟最近 N 秒寫入、要確認業務代價可承受。</p>
<p>判讀順序：先看 read/write 比例（read-heavy 偏 cache aside / write-through、write-extreme 偏 write-behind）、再看 miss 成本（miss 貴選 write-through、miss 便宜選 cache aside）、最後看持久性需求（不可丟選 write-through、可丟選 write-behind）。</p>
<h2 id="cache-模式選擇的判讀順序">Cache 模式選擇的判讀順序</h2>
<p>當「重算成本」「資料一致性」「持久性」三個維度互相衝突、選擇優先序：</p>
<ol>
<li><strong>持久性必須</strong>（不可丟、無法重建）→ 必須選 write-through 或 persistent store + cache、不能選 write-behind 或純 cache aside</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性嚴格</strong>（餘額、權限類）→ write-through 同步更新、確保 cache 不 stale</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性可放寬</strong> + <strong>重算貴</strong> → cache aside + 較長 TTL、減少回源</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性可放寬</strong> + <strong>重算便宜</strong> → cache aside + 短 TTL 或 write-behind</li>
</ol>
<p>例如 ML feature store 場景（<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>）— 持久性可接受失損（feature 可重算）、一致性可放寬（推薦演算法）、重算便宜（feature engineering pipeline 跑得到）— 落在第 4 類、Tubi 把 feature store 從 ScyllaDB 遷到 ElastiCache 是合理取捨。p99 落在 ElastiCache 的 &lt; 10ms 範圍（先前 ScyllaDB-based 架構為 ML inference 路徑的延遲瓶頸、案例未公開 ScyllaDB 端具體延遲數字）。</p>
<p>判讀重點：cache 的本質是用 miss 風險換取 latency；資料若無法重建、需採 persistent store 並接受 latency 成本；資料若可重建但一致性嚴格、可用 cache 但要 write-through 確保即時收斂。詳見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a> 的「Cache vs Persistent Store 取捨」段。</p>
<h2 id="判讀訊號與回源保護">判讀訊號與回源保護</h2>
<p>cache 命中下降時，來源系統會承受瞬間回源壓力。回源保護需要和失效策略一起設計：</p>
<table>
  <thead>
      <tr>
          <th>風險訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>hit ratio 下降且 origin QPS 快速上升</td>
          <td>大量 key 同時過期或失效策略失準</td>
          <td>分散 TTL、分批失效、啟用 <a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a></td>
      </tr>
      <tr>
          <td>熱門 key miss 後延遲與錯誤率同步上升</td>
          <td>單 key 造成 stampede</td>
          <td>啟用 request coalescing、局部預熱、限流回源</td>
      </tr>
      <tr>
          <td>cache 層延遲穩定但業務錯誤增加</td>
          <td>值語意過期或序列化版本漂移</td>
          <td>補 key version 與 schema migration</td>
      </tr>
      <tr>
          <td>eviction rate 升高且 value size 變大</td>
          <td>容量策略與資料形狀不匹配</td>
          <td>重配記憶體策略、調整 value 拆分</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a> 與 <a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 都是回源保護議題；重點是把來源系統視為有限資源，讓 miss 風險可控。</p>
<h2 id="服務情境">服務情境</h2>
<p>商品詳情頁是典型 cache aside 場景。頁面讀取需要組合商品主檔、價格、庫存與行銷標籤。主檔可用較長 TTL 與背景更新，價格與庫存則用事件失效與較短 TTL，讓讀取延遲與正確性維持平衡。</p>
<p>當促銷開始時，大量熱門商品同時被讀取。這時 cache 策略的重點從命中率轉到來源保護與新鮮度控制：是否能限制回源尖峰、是否能快速修正錯誤資料、是否能在事故時降級。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把命中率當作唯一目標，會忽略資料語意與失敗代價。命中率高不代表結果正確，尤其在價格、權限、配額類資料。</p>
<p>把 cache 當成正式資料來源，會讓資料修復與稽核變複雜。快取系統適合承擔讀取加速，不適合承擔正式狀態的最終判定。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>cache aside 的失效風險可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> 做回寫。先看事件中的失效節奏：是大批 key 同時過期、失效順序錯置，還是熱點 key 回源放大，再對照本章的 freshness window、回源保護與容量策略。
這個案例主要支撐的是「失效節奏與回源壓力」判讀，不直接支撐分散式鎖租約或 queue replay；若是互斥控制或重播問題，應轉到 2.4 或 3.x。</p>
<p>命中率看似正常但業務錯誤上升時，先回到本章檢查值語意與 key 版本化，再把量測缺口接到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>cache aside 的設計會直接影響觀測、驗證與事故處理。</p>
<ol>
<li>與 01 的交接：source of truth 與查詢壓力回到 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發讀寫邊界</a>。</li>
<li>與 04 的交接：hit ratio、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 與 eviction 進入 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 06 的交接：回源保護與壓測邊界進入 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 08 的交接：失效策略誤配與 stampede 事故回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a></strong>：應用層快取上面還有 CDN 邊緣層、兩層失效時序要對齊（先 purge 應用層、再 purge 邊緣層、避免邊緣回填到應用層舊資料）。</p>
<p>其他延伸方向：</p>
<ul>
<li>進一步處理 TTL、容量與淘汰策略 → <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a></li>
<li>快取策略在真實事件中的失敗與修復 → <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a></li>
</ul>
]]></content:encoded></item><item><title>3.2 durable queue 與重試策略</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/</guid><description>&lt;p>持久化佇列（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a>）的核心責任是讓非同步工作在 process、節點或網路故障後仍可被恢復處理。它讓業務動作在失敗後仍有可追蹤、可重試、可隔離的路徑。&lt;/p>
&lt;h2 id="durable-與-ephemeral-的差異">durable 與 ephemeral 的差異&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 在語意上可分 durable 與 ephemeral。ephemeral queue 側重低延遲與短暫協調，適合可丟失任務；durable queue 側重故障後可恢復，適合正式狀態相關副作用，例如付款通知、發票產生、庫存同步與合規事件記錄。&lt;/p>
&lt;p>這個選擇本質上是失敗代價選擇。若任務丟失可接受，ephemeral 可降低成本；若任務丟失會造成金流、合約或審計問題，durable 是必要基線。&lt;/p>
&lt;h2 id="重試策略">重試策略&lt;/h2>
&lt;p>重試策略的責任是把暫時性故障和系統性故障分開。durable queue 常見的重試組合是：有限次重試、指數退避、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter&lt;/a> 分散峰值、超過門檻後分流到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>。&lt;/p>
&lt;p>重試上限與間隔要由下游承載能力決定。重試太快會形成故障放大，重試太慢會拖長恢復時間。穩定做法是把重試策略當成服務容量控制的一部分，而不是固定平台預設值。&lt;/p>
&lt;h2 id="dlq-與-requeue-風險">DLQ 與 requeue 風險&lt;/h2>
&lt;p>DLQ 的責任是隔離異常訊息，避免拖垮主消費流程。DLQ 是診斷與修復入口，把它當終點會讓問題沉積。每個進入 DLQ 的訊息，都應能回答：失敗原因是 payload 錯誤、下游不可用、版本不相容，還是消費邏輯缺陷。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/requeue/" data-link-title="Requeue" data-link-desc="說明處理失敗的訊息重新排回 queue 時的風險與控制條件">requeue&lt;/a> 需要明確條件。直接把異常訊息無限 requeue，通常會造成隊列震盪與延遲累積。穩定做法是先隔離、分群、修復，再批次回放。&lt;/p>
&lt;h2 id="ordering-與吞吐取捨">ordering 與吞吐取捨&lt;/h2>
&lt;p>durable queue 在順序與吞吐之間需要明確取捨。全域順序通常成本極高，實務上多採用分區內順序：同一 key 保持順序，不同 key 可並行。這能兼顧一致性需求與處理吞吐。&lt;/p>
&lt;p>順序要求越高，恢復流程越需要明確 checkpoint 與補償策略。否則故障後的重播容易造成亂序副作用，放大修復成本。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>queue depth 持續上升&lt;/td>
 &lt;td>輸入速率高於消費能力&lt;/td>
 &lt;td>擴消費能力、調整重試節奏、分流高成本任務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>retry ratio 升高且成功率下降&lt;/td>
 &lt;td>故障從暫時性轉為系統性&lt;/td>
 &lt;td>降級下游、縮小重試並啟動隔離策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DLQ 量快速增加&lt;/td>
 &lt;td>payload/版本/邏輯異常集中爆發&lt;/td>
 &lt;td>分群診斷、修復邏輯、定向重播&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>requeue 循環導致延遲尖峰&lt;/td>
 &lt;td>缺少隔離邊界與停損機制&lt;/td>
 &lt;td>停止盲目 requeue、先隔離後回放&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>消費恢復後出現大量重複副作用&lt;/td>
 &lt;td>去重與冪等保護不足&lt;/td>
 &lt;td>補 idempotency key 與 side-effect guard&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 durable queue 視為「寫進去就安全」，會忽略消費與恢復責任。持久化只保證訊息可取回，不保證業務結果已正確提交。&lt;/p>
&lt;p>把 DLQ 當成長期倉庫，也會讓問題持續累積。DLQ 的工程價值在於快速定位異常類型並回到修復流程。&lt;/p>
&lt;h2 id="訊息系統的通知-vs-訊息分類">訊息系統的「通知 vs 訊息」分類&lt;/h2>
&lt;p>訊息系統設計區分兩種 SLO 不同的傳遞責任：&lt;em>transactional 通知&lt;/em> 承擔業務副作用的可靠送達、&lt;em>broadcast 訊息&lt;/em> 承擔大量低成本傳播。兩者用不同 storage、不同重試策略、不同投遞保證。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay&lt;/a> — 行動支付每日 3 億訊息、付款通知承擔「確認交易完成」的業務責任、SLO 包含秒級延遲跟高投遞率（用戶付完款後若 30 秒沒收到通知會打客服、產生重複扣款風險）。這層需求嚴於 OTA 推播、需要 durable queue + retry + 重複偵測。&lt;/p>
&lt;p>&lt;strong>分類設計&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Transactional 通知&lt;/strong>（付款收據、訂單狀態變更、配額警告）：承擔業務副作用確認、需 durable + idempotency key 去重、SLO 通常是 &lt;em>秒級延遲 + 99.99% 投遞率&lt;/em>&lt;/li>
&lt;li>&lt;strong>Broadcast 訊息&lt;/strong>（行銷推播、新片發布通知、社群動態）：承擔大量低成本傳播、SLO 是 &lt;em>吞吐量&lt;/em> 跟覆蓋率、允許 best-effort retry&lt;/li>
&lt;/ul>
&lt;p>判讀含義：規模化訊息系統的容量規劃要按類別分開、避免套同一個 broker capacity。3 億訊息 / 天看似一致、但 &lt;em>通知&lt;/em> 跟 &lt;em>訊息&lt;/em> 的工程負擔差數量級。&lt;/p>
&lt;h2 id="下游推送是隱性瓶頸">下游推送是隱性瓶頸&lt;/h2>
&lt;p>訊息系統的真正瓶頸常落在 &lt;em>下游推送通道&lt;/em>（APNs、FCM、SMS gateway、email provider）、不在 broker。下游 quota 是 hard ceiling、超過會被 throttle、訊息積壓回 broker 形成 backlog。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay&lt;/a> — DynamoDB 寫入可以撐 3K msg/sec 平均（PayPay 本身用 DynamoDB 作訊息後端、不是傳統 broker）、但 APNs 推送額度成為事故當下的隱性瓶頸。容量規劃要把下游 quota 算進去、不只看訊息後端吞吐。&lt;/p></description><content:encoded><![CDATA[<p>持久化佇列（<a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>）的核心責任是讓非同步工作在 process、節點或網路故障後仍可被恢復處理。它讓業務動作在失敗後仍有可追蹤、可重試、可隔離的路徑。</p>
<h2 id="durable-與-ephemeral-的差異">durable 與 ephemeral 的差異</h2>
<p><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 在語意上可分 durable 與 ephemeral。ephemeral queue 側重低延遲與短暫協調，適合可丟失任務；durable queue 側重故障後可恢復，適合正式狀態相關副作用，例如付款通知、發票產生、庫存同步與合規事件記錄。</p>
<p>這個選擇本質上是失敗代價選擇。若任務丟失可接受，ephemeral 可降低成本；若任務丟失會造成金流、合約或審計問題，durable 是必要基線。</p>
<h2 id="重試策略">重試策略</h2>
<p>重試策略的責任是把暫時性故障和系統性故障分開。durable queue 常見的重試組合是：有限次重試、指數退避、<a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a> 分散峰值、超過門檻後分流到 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>。</p>
<p>重試上限與間隔要由下游承載能力決定。重試太快會形成故障放大，重試太慢會拖長恢復時間。穩定做法是把重試策略當成服務容量控制的一部分，而不是固定平台預設值。</p>
<h2 id="dlq-與-requeue-風險">DLQ 與 requeue 風險</h2>
<p>DLQ 的責任是隔離異常訊息，避免拖垮主消費流程。DLQ 是診斷與修復入口，把它當終點會讓問題沉積。每個進入 DLQ 的訊息，都應能回答：失敗原因是 payload 錯誤、下游不可用、版本不相容，還是消費邏輯缺陷。</p>
<p><a href="/blog/backend/knowledge-cards/requeue/" data-link-title="Requeue" data-link-desc="說明處理失敗的訊息重新排回 queue 時的風險與控制條件">requeue</a> 需要明確條件。直接把異常訊息無限 requeue，通常會造成隊列震盪與延遲累積。穩定做法是先隔離、分群、修復，再批次回放。</p>
<h2 id="ordering-與吞吐取捨">ordering 與吞吐取捨</h2>
<p>durable queue 在順序與吞吐之間需要明確取捨。全域順序通常成本極高，實務上多採用分區內順序：同一 key 保持順序，不同 key 可並行。這能兼顧一致性需求與處理吞吐。</p>
<p>順序要求越高，恢復流程越需要明確 checkpoint 與補償策略。否則故障後的重播容易造成亂序副作用，放大修復成本。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>queue depth 持續上升</td>
          <td>輸入速率高於消費能力</td>
          <td>擴消費能力、調整重試節奏、分流高成本任務</td>
      </tr>
      <tr>
          <td>retry ratio 升高且成功率下降</td>
          <td>故障從暫時性轉為系統性</td>
          <td>降級下游、縮小重試並啟動隔離策略</td>
      </tr>
      <tr>
          <td>DLQ 量快速增加</td>
          <td>payload/版本/邏輯異常集中爆發</td>
          <td>分群診斷、修復邏輯、定向重播</td>
      </tr>
      <tr>
          <td>requeue 循環導致延遲尖峰</td>
          <td>缺少隔離邊界與停損機制</td>
          <td>停止盲目 requeue、先隔離後回放</td>
      </tr>
      <tr>
          <td>消費恢復後出現大量重複副作用</td>
          <td>去重與冪等保護不足</td>
          <td>補 idempotency key 與 side-effect guard</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 durable queue 視為「寫進去就安全」，會忽略消費與恢復責任。持久化只保證訊息可取回，不保證業務結果已正確提交。</p>
<p>把 DLQ 當成長期倉庫，也會讓問題持續累積。DLQ 的工程價值在於快速定位異常類型並回到修復流程。</p>
<h2 id="訊息系統的通知-vs-訊息分類">訊息系統的「通知 vs 訊息」分類</h2>
<p>訊息系統設計區分兩種 SLO 不同的傳遞責任：<em>transactional 通知</em> 承擔業務副作用的可靠送達、<em>broadcast 訊息</em> 承擔大量低成本傳播。兩者用不同 storage、不同重試策略、不同投遞保證。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a> — 行動支付每日 3 億訊息、付款通知承擔「確認交易完成」的業務責任、SLO 包含秒級延遲跟高投遞率（用戶付完款後若 30 秒沒收到通知會打客服、產生重複扣款風險）。這層需求嚴於 OTA 推播、需要 durable queue + retry + 重複偵測。</p>
<p><strong>分類設計</strong>：</p>
<ul>
<li><strong>Transactional 通知</strong>（付款收據、訂單狀態變更、配額警告）：承擔業務副作用確認、需 durable + idempotency key 去重、SLO 通常是 <em>秒級延遲 + 99.99% 投遞率</em></li>
<li><strong>Broadcast 訊息</strong>（行銷推播、新片發布通知、社群動態）：承擔大量低成本傳播、SLO 是 <em>吞吐量</em> 跟覆蓋率、允許 best-effort retry</li>
</ul>
<p>判讀含義：規模化訊息系統的容量規劃要按類別分開、避免套同一個 broker capacity。3 億訊息 / 天看似一致、但 <em>通知</em> 跟 <em>訊息</em> 的工程負擔差數量級。</p>
<h2 id="下游推送是隱性瓶頸">下游推送是隱性瓶頸</h2>
<p>訊息系統的真正瓶頸常落在 <em>下游推送通道</em>（APNs、FCM、SMS gateway、email provider）、不在 broker。下游 quota 是 hard ceiling、超過會被 throttle、訊息積壓回 broker 形成 backlog。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a> — DynamoDB 寫入可以撐 3K msg/sec 平均（PayPay 本身用 DynamoDB 作訊息後端、不是傳統 broker）、但 APNs 推送額度成為事故當下的隱性瓶頸。容量規劃要把下游 quota 算進去、不只看訊息後端吞吐。</p>
<p><strong>設計含義</strong>：</p>
<ul>
<li><strong>下游 quota 視為容量上限</strong>：APNs / FCM / SMS 的 daily quota 是 hard ceiling、訊息後端規劃要對應</li>
<li><strong>下游通道多元化</strong>：用 APNs / FCM / SMS / in-app notification 多通道分攤 quota 壓力、單通道飽和時其他通道仍可送出（具體降級策略需依各組織業務規則設計）</li>
<li><strong>重試節奏跟下游容量對齊</strong>：consumer 重試節奏依下游剩餘 quota 動態調整、讓重試節奏跟容量同步</li>
</ul>
<p>判讀重點：訊息系統事故當下、先看下游推送通道狀態（APNs status、FCM error rate）、再看訊息後端。下游 throttle 引發 backlog 是規模化訊息系統最常見的瓶頸來源。下游推送 quota 的攻擊面對照見 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 multi-tenant broker 配額耗盡</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>durable queue 的重試與隔離節奏可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 回寫。先看事件中的 backlog、retry、DLQ 變化，再回到本章判讀是重試策略失衡，還是隔離邊界不清楚。
這個案例主要支撐的是「重試隔離與停損門檻」判讀，不直接支撐 outbox 交易切分；若事件核心是資料提交與發布不一致，應轉到 3.3 與 1.3。</p>
<p>當重試量上升且主隊列延遲同步拉高時，先拆分重試通道並收斂 DLQ 分流條件，再把停損門檻接到 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>durable queue 是非同步可靠性的起點，不是終點。</p>
<ol>
<li>與 3.4 的交接：消費與恢復語意落在 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計與去重</a>。</li>
<li>與 3.3 的交接：發布一致性落在 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">outbox pattern</a>。</li>
<li>與 4.20 的交接：queue depth、retry、DLQ 指標進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.12 的交接：重試與重播驗證進入 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">Idempotency 與 Replay 驗證</a>。</li>
<li>與 8.19 的交接：故障隔離與回放決策進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要從投遞語意往消費語意延伸，接著讀 <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>。要看 queue 切換失敗模式，接著讀 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>。</p>
]]></content:encoded></item><item><title>4.2 metrics 與 SLI/SLO</title><link>https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 基本型別&lt;/li>
&lt;li>latency &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>&lt;/li>
&lt;li>error rate / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 是把服務狀態壓縮成可聚合、可比較、可告警的時間序列，責任是讓團隊看見趨勢、容量與服務健康。&lt;/p>
&lt;p>這一頁處理的是 metric 型別與計算語意。counter、gauge 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 各自回答不同問題；選錯型別會讓後面的 SLI、dashboard 與 alert 都建立在錯誤訊號上。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 metrics 時，先看指標型別是否對應問題，再看分母、bucket 與 label 是否穩定。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>latency 是否用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 補足 average 的盲點&lt;/li>
&lt;li>error rate 的分母是否能代表真實請求量&lt;/li>
&lt;li>bucket 是否覆蓋實際尾端延遲&lt;/li>
&lt;li>label 是否能切出必要維度，同時不讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality&lt;/a> 失控&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>用 average 而非 percentile 追 latency、p99 失真&lt;/li>
&lt;li>counter / gauge 混用、計算公式錯&lt;/li>
&lt;li>histogram bucket 沒對齊實際分佈、tail latency 被截斷&lt;/li>
&lt;li>error rate 分母不穩（流量低時誤觸發、高時稀釋）&lt;/li>
&lt;li>商業 SLI 跟 metric 對不上、靠人解釋&lt;/li>
&lt;/ul>
&lt;h2 id="聚合查詢與-recording-rule">聚合查詢與 recording rule&lt;/h2>
&lt;p>Metrics 的讀取面跟寫入面是兩個不同的效能瓶頸。寫入面的壓力來自 series 數量（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>）；讀取面的壓力來自查詢時的聚合計算量。兩者可以獨立失控 — series 數量合理但每次 dashboard 刷新都重算複雜表達式，query engine 一樣會過載。&lt;/p>
&lt;h3 id="query-time-aggregation-的成本">Query-time aggregation 的成本&lt;/h3>
&lt;p>Dashboard panel 或 alert rule 每次觸發時，TSDB 對 raw series 執行聚合表達式（rate、sum、histogram_quantile）。當 raw series 數量大、查詢時間範圍長、dashboard 刷新頻率高，同一個計算會被反覆執行。&lt;/p>
&lt;p>一個典型的 SLO &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> panel 可能涉及：先算 rate、再除以 total、再跟 threshold 比較、最後乘以 window。每次刷新把整條運算鏈走一遍。當這類 panel 有十幾個、每 30 秒刷新一次，query engine 的 CPU 會被 dashboard 佔滿，留給事故即席查詢的餘量不夠。&lt;/p>
&lt;h3 id="recording-rule-把計算推到寫入時">Recording rule 把計算推到寫入時&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule&lt;/a> 是 Prometheus 生態（包括 Thanos、Mimir、VictoriaMetrics）的標準應對方式：在 TSDB 內定期執行聚合表達式，把結果寫成新的 time series。Dashboard 跟 alert rule 讀 recording rule 的輸出而非重算 raw series。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 基本型別</li>
<li>latency <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a></li>
<li>error rate / <a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a></li>
<li><a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a> / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a></li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 是把服務狀態壓縮成可聚合、可比較、可告警的時間序列，責任是讓團隊看見趨勢、容量與服務健康。</p>
<p>這一頁處理的是 metric 型別與計算語意。counter、gauge 與 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 各自回答不同問題；選錯型別會讓後面的 SLI、dashboard 與 alert 都建立在錯誤訊號上。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 metrics 時，先看指標型別是否對應問題，再看分母、bucket 與 label 是否穩定。</p>
<p>重點訊號包括：</p>
<ul>
<li>latency 是否用 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> / <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 補足 average 的盲點</li>
<li>error rate 的分母是否能代表真實請求量</li>
<li>bucket 是否覆蓋實際尾端延遲</li>
<li>label 是否能切出必要維度，同時不讓 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> 失控</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>用 average 而非 percentile 追 latency、p99 失真</li>
<li>counter / gauge 混用、計算公式錯</li>
<li>histogram bucket 沒對齊實際分佈、tail latency 被截斷</li>
<li>error rate 分母不穩（流量低時誤觸發、高時稀釋）</li>
<li>商業 SLI 跟 metric 對不上、靠人解釋</li>
</ul>
<h2 id="聚合查詢與-recording-rule">聚合查詢與 recording rule</h2>
<p>Metrics 的讀取面跟寫入面是兩個不同的效能瓶頸。寫入面的壓力來自 series 數量（<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>）；讀取面的壓力來自查詢時的聚合計算量。兩者可以獨立失控 — series 數量合理但每次 dashboard 刷新都重算複雜表達式，query engine 一樣會過載。</p>
<h3 id="query-time-aggregation-的成本">Query-time aggregation 的成本</h3>
<p>Dashboard panel 或 alert rule 每次觸發時，TSDB 對 raw series 執行聚合表達式（rate、sum、histogram_quantile）。當 raw series 數量大、查詢時間範圍長、dashboard 刷新頻率高，同一個計算會被反覆執行。</p>
<p>一個典型的 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> panel 可能涉及：先算 rate、再除以 total、再跟 threshold 比較、最後乘以 window。每次刷新把整條運算鏈走一遍。當這類 panel 有十幾個、每 30 秒刷新一次，query engine 的 CPU 會被 dashboard 佔滿，留給事故即席查詢的餘量不夠。</p>
<h3 id="recording-rule-把計算推到寫入時">Recording rule 把計算推到寫入時</h3>
<p><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a> 是 Prometheus 生態（包括 Thanos、Mimir、VictoriaMetrics）的標準應對方式：在 TSDB 內定期執行聚合表達式，把結果寫成新的 time series。Dashboard 跟 alert rule 讀 recording rule 的輸出而非重算 raw series。</p>
<p>Recording rule 的設計判準是查詢頻率跟計算成本的乘積。高頻讀取（dashboard auto-refresh、每分鐘 evaluate 的 alert rule）加上高計算成本（多維度 rate + ratio + quantile）的組合最值得做 recording rule。低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。</p>
<p>Recording rule 的命名慣例用 <code>level:metric:operations</code> 格式（如 <code>job:http_requests_total:rate5m</code>），讓讀者從名稱直接判斷來源粒度跟計算方式。沒有命名慣例時，recording rule 增長到數百條後會難以維護跟除錯。</p>
<h3 id="rollup-與-downsampling">Rollup 與 downsampling</h3>
<p><a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">Rollup</a> 解決的是時間維度的讀取成本。原始資料以 15 秒間隔採集，查詢「過去 90 天的 error rate 趨勢」時需要掃描數百萬個資料點；rollup 把舊資料聚合成 5 分鐘或 1 小時粒度，查詢時只讀取聚合後的少量資料點。</p>
<p>Rollup 的聚合函數選擇影響查詢語意。Counter 用 sum 合理、gauge 用 average 合理、histogram 用 average 會失去分布資訊（p99 被壓平）。設計 rollup 時要按 metric type 指定對應的聚合函數，混用會讓長時間範圍的 dashboard 產生誤導性數值。</p>
<p>查詢路由的透明度也是設計重點。使用者把 dashboard 時間範圍從 1 小時拉到 7 天時，系統自動從 raw series 切到 rollup series，精度從 15 秒變成 5 分鐘。如果這個切換對使用者不透明，事故中觀察到的數值變化可能是精度切換的假象而非真實服務變化。</p>
<h3 id="metrics-讀取面的資源隔離">Metrics 讀取面的資源隔離</h3>
<p>Metrics 的 query engine 跟 log 一樣面臨多種查詢模式競爭資源的問題。Dashboard 定期刷新是穩定的背景負載；alert rule evaluation 是系統關鍵的定期負載；事故即席查詢是偶發的突增負載。三者搶同一個 query engine 時，dashboard 跟 alert 的穩定負載會壓縮即席查詢的可用資源。</p>
<p>Prometheus 原生的資源隔離有限，但 Thanos Query Frontend、Mimir Query Frontend、Grafana Cloud 的 query scheduler 都支援 query priority 或 query queue 分離。設計時把 alert evaluation 設為最高優先（告警不能因 query 排隊而延遲），dashboard 次之，即席查詢的延遲容忍最高但不能被完全餓死。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.6 SLI/SLO 訊號設計：把 metric 升級為 user-journey SLI</li>
<li>04.7 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> / cost：label 治理與成本邊界</li>
<li>04.9 continuous profiling：metrics 之外的第四角觀測訊號</li>
<li>04.23 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">觀測查詢設計</a>：跨訊號類型的讀取路徑系統設計</li>
<li><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11 Uber M3</a>：單機 Prometheus 到平台級 metrics 系統的演進</li>
</ul>
]]></content:encoded></item><item><title>5.2 Kubernetes 部署策略</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</guid><description>&lt;p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。&lt;/p>
&lt;h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout&lt;/h2>
&lt;p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。&lt;/p>
&lt;p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。&lt;/p>
&lt;h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a> 要對齊服務生命週期，不同 probe 有不同責任：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe&lt;/a>：確認服務啟動完成，避免慢啟動服務被過早重啟。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> probe：確認服務可安全接流量。&lt;/li>
&lt;li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。&lt;/li>
&lt;/ol>
&lt;p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。&lt;/p>
&lt;h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項&lt;/h3>
&lt;p>startup probe 跟 &lt;code>initialDelaySeconds&lt;/code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。&lt;code>initialDelaySeconds&lt;/code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。&lt;/p>
&lt;p>startup probe 的總容忍時間 = &lt;code>failureThreshold × periodSeconds&lt;/code>。例如 &lt;code>failureThreshold: 30, periodSeconds: 10&lt;/code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。&lt;/p>
&lt;h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇&lt;/h3>
&lt;p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Port check&lt;/strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。&lt;/li>
&lt;li>&lt;strong>Dependency check&lt;/strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 &lt;code>/ready&lt;/code> endpoint 內驗證必要依賴的連線狀態。&lt;/li>
&lt;li>&lt;strong>Deep health&lt;/strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。&lt;/li>
&lt;/ol>
&lt;p>依賴分類（必要 / 可降級 / 觀測）的判讀框架見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Readiness 設計的核心取捨&lt;/a>。&lt;/p>
&lt;h2 id="config-rollout-與版本相容">config rollout 與版本相容&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。&lt;/p>
&lt;p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。&lt;/p>
&lt;h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating&lt;/h3>
&lt;p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。&lt;/p>
&lt;p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。&lt;/p>
&lt;p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-1 相容視為暫時性保護而非永久設計。設定注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p>
&lt;h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling&lt;/a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。</p>
<h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout</h2>
<p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。</p>
<p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。</p>
<h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期</h2>
<p><a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a> 要對齊服務生命週期，不同 probe 有不同責任：</p>
<ol>
<li><a href="/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe</a>：確認服務啟動完成，避免慢啟動服務被過早重啟。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> probe：確認服務可安全接流量。</li>
<li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。</li>
</ol>
<p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。</p>
<h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項</h3>
<p>startup probe 跟 <code>initialDelaySeconds</code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。<code>initialDelaySeconds</code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。</p>
<p>startup probe 的總容忍時間 = <code>failureThreshold × periodSeconds</code>。例如 <code>failureThreshold: 30, periodSeconds: 10</code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。</p>
<h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇</h3>
<p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：</p>
<ol>
<li><strong>Port check</strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。</li>
<li><strong>Dependency check</strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 <code>/ready</code> endpoint 內驗證必要依賴的連線狀態。</li>
<li><strong>Deep health</strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。</li>
</ol>
<p>依賴分類（必要 / 可降級 / 觀測）的判讀框架見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Readiness 設計的核心取捨</a>。</p>
<h2 id="config-rollout-與版本相容">config rollout 與版本相容</h2>
<p><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。</p>
<p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。</p>
<h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating</h3>
<p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。</p>
<p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。</p>
<p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-1 相容視為暫時性保護而非永久設計。設定注入方式與版本追蹤見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨</a>。</p>
<h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同</h2>
<p><a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling</a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。</p>
<p>長連線服務或有大量背景任務的 workload，通常需要比 stateless API 更保守的 rollout 策略，並額外搭配 drain 與 reconnect 設計。</p>
<p>擴縮策略的演進需要版本化跟可回放。對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb K8s 叢集擴縮演進</a>：揭露「擴縮策略版本化跟可回放」「不同 workload 區分擴縮政策」「容量治理跟事故指標綁定」三個方向。以下基於通用工程知識展開。</p>
<p>可重複套用的做法：</p>
<ol>
<li><strong>擴縮策略進 IaC</strong>：HPA / VPA / Karpenter / Cluster Autoscaler 的配置都進 git、變更走 release flow、避免手動調整在事故後被遺忘。IaC + 自動化的 ownership 邊界見 [5.7 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary](/backend/05-deployment-platform/traffic-config-control-plane-boundary/)。</li>
<li><strong>workload 分群擴縮</strong>：stateless API、長連線服務、batch job、background worker 對擴縮的需求不同。把不同 workload 用不同 namespace + 不同 autoscaler policy 隔離，避免一套規則套全部。</li>
<li><strong>擴縮事件接事故指標</strong>：HPA 觸發、scale-up 延遲、scale-down 過快、cluster autoscaler 加 node 失敗，都該在事故 timeline 上可見。回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a> 的擴縮事件 vs 事故區分。</li>
</ol>
<h2 id="分階段平台遷移">分階段平台遷移</h2>
<p>平台遷移的本質是流量跟依賴的分段切換。遷移期內新舊叢集同時存在，rollout 策略要把跨叢集流量切換納入批次節奏、視為連續多批決策。本段聚焦流量 / 依賴切換時序；遷移期的團隊職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s → EKS</a>：揭露「零停機遷移要把切換做成分段策略」「難點通常在跨叢集服務依賴跟流量切換、不在 Kubernetes API 本身」。對應 <a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye workloads 遷移</a>：揭露「分批遷移 workload、保留觀測對照」「明確切換 / 回退條件」「新平台先驗證容量跟恢復節奏」。以下基於通用工程知識展開。</p>
<p>可重複套用的分階段做法：</p>
<ol>
<li><strong>新叢集 + 共通配置基線</strong>：先在新叢集上建立跟舊叢集對等的配置基線（namespace、ResourceQuota、NetworkPolicy、Ingress class、storage class），讓 workload 可以無縫部署。</li>
<li><strong>小流量先導服務</strong>：選擇影響面小、依賴單純的服務作為先導，先在新叢集跑完整 deployment cycle（rollout、drain、rollback 驗證）、累積信心後再擴大。</li>
<li><strong>可控流量分批切換</strong>：用 DNS 加權、service mesh 流量切分或 LB 規則把流量分批從舊叢集導到新叢集。每批切換後驗證 SLI 偏差、再進下一批。</li>
<li><strong>每批保留回退路徑</strong>：舊叢集服務不立即下線，保留作為回退目標。回退條件先驗證（rollback script、流量切回 DNS / LB 規則），再開始下一批切換。</li>
</ol>
<p>延伸 5.C1 揭露的「跨叢集服務依賴是難點」、5.C10 中型組織判讀「服務本身切過去了、但資料面、認證面、觀測面還沒同步」也指向同類問題。跨叢集遷移最容易出的事故是「服務切過去了、依賴沒切過去」。Database、cache、message queue、observability pipeline、auth service 的切換時機要分別規劃，避免應用層在新叢集但仍跨網路打舊叢集的依賴，造成隱性 latency 或單點失效。規模差異下的同類問題見 <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a>。</p>
<h2 id="大規模-k8s-的設計取捨">大規模 K8s 的設計取捨</h2>
<p>K8s 在不同規模下的設計取捨會明顯分歧。小規模叢集追求簡單跟低運維成本，大規模叢集追求隔離跟自動化治理。同一套部署策略放到不同規模會在某個量級開始失效。</p>
<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 萬美金">9.C12 Riot Games：246 個 EKS cluster</a>：揭露架構決策從 multi-tenant cluster 改成 single-tenant per game、Karpenter + Terraform 的 cluster 級自動化、35ms 延遲門檻 + Local Zones / Outposts 區域部署（case 中「35ms 反推 region 部署」屬作者判讀層、本章引用此推論）。對應 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130,000-node GKE cluster</a>：揭露 control plane 極限取決於 storage backend（GCP 用 Spanner 替代 etcd）、AI workload 跟 web workload 容量規劃差異。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業上 K8s 動機是治理一致性（作者判讀）、適合 single-cluster-multi-namespace。</p>
<p>可重複套用的取捨判讀：</p>
<ol>
<li><strong>single-tenant per workload vs single-cluster multi-namespace</strong>：高隔離需求（每個 workload 失效不能影響其他）、高延遲敏感度（需 region cluster）→ 多 cluster；治理一致性訴求（統一 release flow、合規邊界）→ 單一 cluster 多 namespace。</li>
<li><strong>Cluster 容量極限取決於 control plane</strong>：data plane（worker nodes）擴容容易、control plane（API server、etcd / storage）擴容難、瓶頸通常在 control plane。etcd 撐 5K-10K node 後吃力、需要替換 storage backend（Spanner / PostgreSQL / 自家 KV）才能撐萬級節點（見 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34</a>）。control plane 的 ownership 邊界由 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 control plane boundary</a> 處理。</li>
<li><strong>Multi-cluster 治理需要 IaC + 自動化</strong>：Terraform / Crossplane / Cluster API + Karpenter / Cluster Autoscaler 是基本工具。手動管理超過數十個 cluster 不可行。</li>
<li><strong>AI workload 跟 web workload 容量規劃完全不同</strong>：AI workload 短時間爆量創建 Pods（萬級 / 秒）、preempt 頻繁；web workload 節點生命週期長、變動緩。把 web 經驗套到 AI workload 容量規劃會嚴重低估壓力。</li>
</ol>
<p>關鍵判讀是「先決定 cluster 是隔離單位還是治理單位」。Riot Games 把 cluster 當隔離單位（246 個獨立 cluster），Maersk / Bosch 把 cluster 當治理單位（單 cluster 多 namespace）。同一個工具兩種用法、決定整體運維模型。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：EKS 平台整併與標準化</a>：揭露多叢集整併到單一控制面的場景、跟 Maersk-Bosch 同屬「治理一致性」取捨方向（治理單位優先於隔離單位）。Condé Nast 的整併路徑是「盤點既有叢集差異 → 建立統一平台基線 → 藍綠或漸進切換業務流量」、對應前面「分階段平台遷移」段的批次節奏。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 卡在中段且新副本反覆重啟</td>
          <td>probe 與啟動路徑不匹配</td>
          <td>校正 startup/readiness 探針與超時參數</td>
      </tr>
      <tr>
          <td>rollout 完成後延遲與錯誤率短期上升</td>
          <td>批次切換過快或下游未對齊</td>
          <td>降低批次、延長觀察窗口、回退再重試</td>
      </tr>
      <tr>
          <td>config 變更後特定路徑失敗率飆升</td>
          <td>設定與版本相容窗口不足</td>
          <td>啟動回退配置、補雙軌相容</td>
      </tr>
      <tr>
          <td>autoscaling 在部署期間頻繁抖動</td>
          <td>容量閾值與 rollout 節奏衝突</td>
          <td>分離部署窗口與擴縮窗口、調整資源策略</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 與連線生命週期控制不足</td>
          <td>拉長 drain、分批切流、校正 timeout</td>
      </tr>
      <tr>
          <td>跨叢集遷移後特定路徑 latency 升高</td>
          <td>應用切過去但依賴未切、跨網路</td>
          <td>規劃依賴切換時機、分批一致</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 Kubernetes 部署看成 YAML 套版，會忽略服務語意差異。相同 deployment 參數在不同服務上，可能代表完全不同風險。</p>
<p>把 probe 當成健康檢查 URL，會讓服務在邊界條件下過早接流量。probe 的工程價值在於反映服務真實可用條件。</p>
<p>把 cluster scale-up 想成「加 node 就好」也是常見誤判。當 cluster 規模超過 control plane 預設邊界，etcd / API server 會先撐不住，加 node 反而加重 control plane 負擔。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>部署切換語意可用 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 做回寫。先看事件中的失敗是在 rollout 批次、probe 判斷、還是 drain 時序，再對照本章的 rollout 節奏與停止條件。</p>
<p>這個案例主要支撐的是「部署批次與切換時序」判讀，不直接支撐資料庫交易切分或 consumer 冪等；若問題落在提交一致性或重播補償，應轉到 1.3 或 3.4。</p>
<p>若版本已切換但錯誤率延遲上升，先回到 probe 與 config 相容窗口，再把證據欄位接到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>Kubernetes 部署策略要和觀測、驗證、事故流程同時對齊。</p>
<ol>
<li>與 5.6 的交接：startup / readiness / liveness / drain 的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.1 的交接：image、entrypoint、resource limit 的 runtime 層回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">container 與 runtime</a>。</li>
<li>與 5.3 的交接：流量承接與退出落在 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.4 的交接：endpoint 註冊與摘除回到 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">service discovery</a>。</li>
<li>與 5.7 的交接：control plane 跟 data plane 邊界落在 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.20 的交接：版本切換證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：放行與停損條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 8.19 的交接：部署中止與回退判斷進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把部署與流量切換一起治理，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看切換失敗與回退判讀，接著讀 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。要看大規模 K8s 容量設計，接著讀 <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> 跟 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130K-node</a>。</p>
]]></content:encoded></item><item><title>6.2 load test</title><link>https://tarrragon.github.io/blog/backend/06-reliability/load-testing/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/load-testing/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>當系統需要回答「這個流量撐不撐得住」，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test&lt;/a> 把真實 workload model 變成可重播的壓力情境，找出吞吐、延遲與瓶頸轉折點。&lt;/p>
&lt;p>這一頁關心的是實際流量長什麼樣，不是把數字推高而已。模型若不接近 production shape，壓測結果就只是在驗證假場景。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>Load test 的品質先看模型是否貼近流量結構，再看系統在 saturation 前後的行為。曲線在 saturation 前後如何變形才是關鍵，單點 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput&lt;/a> 只是其中一個讀數。&lt;/p>
&lt;p>判讀時的關鍵面向：&lt;/p>
&lt;ul>
&lt;li>workload 是否包含尖峰、長尾與不同 cohort&lt;/li>
&lt;li>latency 是否在接近飽和時快速劣化&lt;/li>
&lt;li>bottleneck 是否能被定位到具體 resource&lt;/li>
&lt;li>load 結果是否能回寫到 capacity planning&lt;/li>
&lt;/ul>
&lt;h2 id="workload-model-設計">Workload model 設計&lt;/h2>
&lt;p>Workload model 的責任是把 production 流量結構轉成可重播的測試情境。模型越接近真實流量的形狀，壓測結果對容量決策的支撐力越高。&lt;/p>
&lt;p>設計 workload model 時先分析三個維度：&lt;/p>
&lt;p>&lt;strong>Traffic shape&lt;/strong>：production 流量很少是均勻的。峰值時段的 request rate 可能是均值的數倍到數十倍，而且峰值持續時間、上升斜率與衰退曲線各有差異。Shopify 的 BFCM 流量結構是短時間爆量加上高寫入比例；若模型只用日均流量推算，會漏掉峰值集中在數小時內的壓力集中度。模型需要把 peak / off-peak / burst 三種時段分開描述。&lt;/p>
&lt;p>&lt;strong>Cohort 拆分&lt;/strong>：讀與寫的資源消耗模式不同，混合比例會改變瓶頸位置。API gateway 層可能由讀主導，但 checkout 或 order-create 路徑的寫入比例明顯偏高。把不同 cohort（讀 / 寫 / 混合 / 背景任務）分開量測，才能判斷瓶頸是在哪個路徑上出現。&lt;/p>
&lt;p>&lt;strong>資料量對齊&lt;/strong>：staging 環境的資料量常與 production 差一到兩個數量級。query plan、index scan、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 飽和與 cache 行為都跟資料量高度相關。模型要盡可能用 production-like 資料量，或至少在結果判讀時標註資料量差異帶來的偏移。&lt;/p>
&lt;p>LinkedIn 的實踐揭露另一個面向：workload model 會隨時間漂移。流量結構、使用者行為與功能上線都會改變真實壓力形狀。當 load-test 模型不再定期校準，壓測結果與 production 壓力之間的差距會持續擴大。定期用 production traffic replay 或 access log 分析重建模型，是維持壓測可信度的必要動作。&lt;/p>
&lt;p>判斷 workload model 是否仍然有效的實務做法：把最近一次 load test 的 latency distribution 與 production 同時段的 latency distribution 對齊。若兩者的 p50 / p95 / p99 比率偏離超過 20%，模型已經需要校準。20% 是通用起點。latency 敏感的服務（交易、即時通訊）應使用更嚴格的門檻（10%），batch 類服務可適度放寬。偏離來源通常是三個之一：流量結構變了（新功能改變 read/write 比例）、資料量成長了（query plan 改變）、依賴行為變了（上游回應時間漂移）。&lt;/p>
&lt;h2 id="saturation-與瓶頸定位">Saturation 與瓶頸定位&lt;/h2>
&lt;p>Saturation 的轉折點決定了系統的實際容量上限 — 在什麼負載下，系統從線性擴展轉為劣化。&lt;/p>
&lt;p>判讀 saturation 先看 latency curve。在低負載時，latency 通常穩定；隨著負載上升，會出現一個 inflection point，之後 latency 開始加速上升。這個轉折點通常比 throughput ceiling 更早出現，是真正的容量邊界。&lt;/p>
&lt;p>在 inflection point 之後，系統行為會進入幾種退化模式。逐漸退化型的 latency 緩慢爬升，通常來自 queue 堆積或 GC 壓力加重；崩落型的 latency 在某個點突然跳升數倍，通常來自 connection pool 耗盡或 thread pool 飽和。兩種退化的應對策略不同：逐漸退化有 load shedding 的緩衝空間，崩落型需要提早在更低負載觸發限流。壓測結果需要標註系統屬於哪種退化模式，這個資訊直接影響 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition&lt;/a> 的門檻設定。&lt;/p>
&lt;p>瓶頸定位需要對齊資源層。常見瓶頸包括 CPU saturation、memory pressure、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 耗盡、queue depth 堆積與 disk I/O。壓測時需要同步觀測這些資源指標，才能把 latency 劣化歸因到具體 resource。歸因的價值在於讓擴容或優化的投資方向可決策：CPU 瓶頸指向 compute scaling、connection pool 瓶頸指向 pool config 或 connection reuse、queue depth 瓶頸可能指向 consumer 吞吐不足。若只看 latency 劣化但不做歸因，團隊容易直覺式擴容，花了成本卻沒打到真正瓶頸。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>當系統需要回答「這個流量撐不撐得住」，<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a> 把真實 workload model 變成可重播的壓力情境，找出吞吐、延遲與瓶頸轉折點。</p>
<p>這一頁關心的是實際流量長什麼樣，不是把數字推高而已。模型若不接近 production shape，壓測結果就只是在驗證假場景。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Load test 的品質先看模型是否貼近流量結構，再看系統在 saturation 前後的行為。曲線在 saturation 前後如何變形才是關鍵，單點 <a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a> 只是其中一個讀數。</p>
<p>判讀時的關鍵面向：</p>
<ul>
<li>workload 是否包含尖峰、長尾與不同 cohort</li>
<li>latency 是否在接近飽和時快速劣化</li>
<li>bottleneck 是否能被定位到具體 resource</li>
<li>load 結果是否能回寫到 capacity planning</li>
</ul>
<h2 id="workload-model-設計">Workload model 設計</h2>
<p>Workload model 的責任是把 production 流量結構轉成可重播的測試情境。模型越接近真實流量的形狀，壓測結果對容量決策的支撐力越高。</p>
<p>設計 workload model 時先分析三個維度：</p>
<p><strong>Traffic shape</strong>：production 流量很少是均勻的。峰值時段的 request rate 可能是均值的數倍到數十倍，而且峰值持續時間、上升斜率與衰退曲線各有差異。Shopify 的 BFCM 流量結構是短時間爆量加上高寫入比例；若模型只用日均流量推算，會漏掉峰值集中在數小時內的壓力集中度。模型需要把 peak / off-peak / burst 三種時段分開描述。</p>
<p><strong>Cohort 拆分</strong>：讀與寫的資源消耗模式不同，混合比例會改變瓶頸位置。API gateway 層可能由讀主導，但 checkout 或 order-create 路徑的寫入比例明顯偏高。把不同 cohort（讀 / 寫 / 混合 / 背景任務）分開量測，才能判斷瓶頸是在哪個路徑上出現。</p>
<p><strong>資料量對齊</strong>：staging 環境的資料量常與 production 差一到兩個數量級。query plan、index scan、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 飽和與 cache 行為都跟資料量高度相關。模型要盡可能用 production-like 資料量，或至少在結果判讀時標註資料量差異帶來的偏移。</p>
<p>LinkedIn 的實踐揭露另一個面向：workload model 會隨時間漂移。流量結構、使用者行為與功能上線都會改變真實壓力形狀。當 load-test 模型不再定期校準，壓測結果與 production 壓力之間的差距會持續擴大。定期用 production traffic replay 或 access log 分析重建模型，是維持壓測可信度的必要動作。</p>
<p>判斷 workload model 是否仍然有效的實務做法：把最近一次 load test 的 latency distribution 與 production 同時段的 latency distribution 對齊。若兩者的 p50 / p95 / p99 比率偏離超過 20%，模型已經需要校準。20% 是通用起點。latency 敏感的服務（交易、即時通訊）應使用更嚴格的門檻（10%），batch 類服務可適度放寬。偏離來源通常是三個之一：流量結構變了（新功能改變 read/write 比例）、資料量成長了（query plan 改變）、依賴行為變了（上游回應時間漂移）。</p>
<h2 id="saturation-與瓶頸定位">Saturation 與瓶頸定位</h2>
<p>Saturation 的轉折點決定了系統的實際容量上限 — 在什麼負載下，系統從線性擴展轉為劣化。</p>
<p>判讀 saturation 先看 latency curve。在低負載時，latency 通常穩定；隨著負載上升，會出現一個 inflection point，之後 latency 開始加速上升。這個轉折點通常比 throughput ceiling 更早出現，是真正的容量邊界。</p>
<p>在 inflection point 之後，系統行為會進入幾種退化模式。逐漸退化型的 latency 緩慢爬升，通常來自 queue 堆積或 GC 壓力加重；崩落型的 latency 在某個點突然跳升數倍，通常來自 connection pool 耗盡或 thread pool 飽和。兩種退化的應對策略不同：逐漸退化有 load shedding 的緩衝空間，崩落型需要提早在更低負載觸發限流。壓測結果需要標註系統屬於哪種退化模式，這個資訊直接影響 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 的門檻設定。</p>
<p>瓶頸定位需要對齊資源層。常見瓶頸包括 CPU saturation、memory pressure、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 耗盡、queue depth 堆積與 disk I/O。壓測時需要同步觀測這些資源指標，才能把 latency 劣化歸因到具體 resource。歸因的價值在於讓擴容或優化的投資方向可決策：CPU 瓶頸指向 compute scaling、connection pool 瓶頸指向 pool config 或 connection reuse、queue depth 瓶頸可能指向 consumer 吞吐不足。若只看 latency 劣化但不做歸因，團隊容易直覺式擴容，花了成本卻沒打到真正瓶頸。</p>
<p>Pinterest 的快取可靠性案例揭露一種不直覺的瓶頸類型：cache 命中率崩落時，瓶頸會從 compute 層移到 storage throughput。回源壓力瞬間上升，資料層的 I/O 成為新瓶頸。這種情境在純 compute 壓測中看不到，需要特別設計包含 cache miss 場景的 workload。實務上，cache miss 場景可以用兩種方式模擬：清空 cache 後立即打流量（cold start），或在壓測過程中讓部分 key 過期（partial eviction）。兩者暴露的瓶頸位置可能不同，cold start 偏向 storage 吞吐、partial eviction 偏向 connection pool 與 retry 放大。</p>
<h2 id="load-test-與容量規劃的接口">Load test 與容量規劃的接口</h2>
<p>Load test 的產出不只是 pass/fail，它是容量規劃的主要輸入。壓測結果要能轉成 headroom 計算與成本預測。</p>
<p><strong>Headroom 計算</strong>：peak load 佔 capacity ceiling 的比率決定安全緩衝。比率超過 70-80% 時，任何流量突增或依賴劣化都可能觸發 saturation。headroom 的安全值跟系統的退化模式綁在一起：崩落型退化的系統需要更大 headroom，因為從健康到故障的過渡窗口很短。LinkedIn 的做法是把 headroom 預算綁到值班分層，當 headroom 低於門檻時自動升級 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 層級，讓容量風險直接轉成團隊行動。</p>
<p><strong>成本曲線</strong>：擴容的邊際成本會在跨越 availability zone、region 或 tier 邊界時跳升。load test 結果要標註「容量到多少時需要跨越哪個擴容邊界」，讓容量規劃能把成本跳升點納入決策。這類資訊在高峰前特別有價值：團隊能提前決定是靠 load shedding 撐過峰值，還是提前擴容跨區，兩者的成本與風險完全不同。</p>
<p><strong>隔離單位的容量量測</strong>：全域容量規劃在多租戶或 cell-based 架構下會失真。Amazon 的做法是按 cell 獨立量測 saturation，每個隔離單位有自己的 headroom，避免一個 cell 的容量需求拖動全域擴容。這種設計讓 load test 的量測粒度從「整個服務」降到「每個隔離單位」，容量決策更精準。</p>
<p>load test 結果的完整路由是：壓測產出 saturation point 與 headroom ratio → 餵給 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界</a> 做容量預算 → 餵給 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a> 做持續守護。</p>
<h2 id="持續性-load-test-與事件性壓測">持續性 load test 與事件性壓測</h2>
<p>Load test 的執行模式依用途分兩類，兩者設計邏輯不同。</p>
<p><strong>持續性 load test</strong> 跑在 CI pipeline 中，用固定 workload 做 baseline regression 偵測。每次變更跑同一套 scenario，比較 latency 與 throughput 是否偏離 baseline。這類測試的 workload 不需要貼近峰值，但需要穩定到能偵測 5-10% 的 regression。連到 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a> 做自動化 gate。</p>
<p><strong>事件性壓測</strong> 針對特定事件（產品上線、促銷、峰值季節）做一次性或年度壓測。workload 設計要貼近該事件的流量形狀與資料量。Shopify 把 game day 做成年度制度化流程：每輪 BFCM 前跑容量驗證，演練結果回寫 resiliency matrix 與 runbook，讓下一輪從更高基準開始。事件性壓測的關鍵是結果留存與回寫，不是跑完就結束。</p>
<p>兩類測試的分工：持續性負責守住 baseline，事件性負責探索邊界。只跑持續性會漏掉峰值場景；只跑事件性會漏掉漸進退化。</p>
<p>判斷要用哪一類時，先問兩個問題。第一，這個服務是否有可預期的流量事件（促銷、賽季、發布日）？有的話，事件性壓測是必要的，因為峰值壓力的形狀跟日常完全不同。第二，這個服務的變更頻率是否超過每週一次？是的話，持續性 load test 是必要的，因為 regression 可能在任何一次 deploy 進入。多數生產系統兩類都需要。</p>
<h2 id="環境與工具考量">環境與工具考量</h2>
<p><strong>Staging vs production</strong>：staging 壓測控制成本低、風險低，但跟 production 的差異（資料量、網路拓撲、依賴行為）會讓結果偏移。Production load test（dark traffic、shadow read、canary traffic）結果更可信，但需要嚴格的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 控制與 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 設計。選擇哪種環境取決於系統成熟度與風險承受能力。</p>
<p><strong>Synthetic traffic 的限制</strong>：synthetic 請求不帶真實 session、auth token 或 cache warm-up 狀態，行為與真實使用者不同。對 cache 敏感的系統，synthetic traffic 可能打出比真實流量更高的 miss rate，產生虛假瓶頸。對 auth 與 session 敏感的系統，synthetic 請求可能繞過 rate limit 或 WAF 路徑，壓測結果會低估 production 的真實負載。判讀時要標註 synthetic 與 real traffic 的行為差異，避免把假瓶頸或假安全當結論。</p>
<p><strong>資料隔離</strong>：production load test 需要確保測試流量不會污染真實資料。常見做法包括 shadow read（讀路徑複製、寫路徑丟棄）、test tenant 隔離（獨立資料空間）、與 feature flag 控制的 dark traffic。每種做法的隔離強度與實作成本不同，選擇時要對齊系統的資料敏感度。</p>
<p>工具選擇路由：CI-first 場景偏向 CLI 工具（<a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a>）、JVM 生態偏向 <a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a>、Python 團隊偏向 <a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a>、既有 .jmx 資產偏向 <a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a>。工具對照見 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">vendors/</a>。</p>
<h2 id="load-test-結果的證據留存">Load test 結果的證據留存</h2>
<p>Load test 結果需要結構化留存，讓下游（容量規劃、release gate、事故決策）可以直接調用，而不是每次都要重跑或找人解釋。</p>
<p>留存的最小欄位：workload model 版本、測試環境、saturation point（latency inflection 的 RPS）、throughput ceiling、主要瓶頸歸因、headroom ratio、退化模式分類、測試日期。這些欄位讓 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a> 可以把 load test 結論直接納入 release 決策，也讓 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界</a> 可以追蹤 saturation point 隨時間的變化趨勢。</p>
<p>若結果只以 dashboard 截圖或口頭摘要留存，下次壓測時團隊無法判斷「是系統變了還是模型變了」，校準失去基準。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify H1</a>：高峰型流量要求 load model 涵蓋短時間爆量與高寫入比例，game day 把事件性壓測制度化。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn L1</a>：headroom 預算綁值班分層，load-test drift 需要定期校準模型。</li>
<li><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest P1</a>：cache 命中率崩落改變瓶頸位置，壓測要涵蓋 cache miss 場景。</li>
<li><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon A1</a>：cell-based architecture 讓容量規劃按隔離單位量測，避免全域擴容失控。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">LinkedIn L2</a>：自動化壓測接入 CI pipeline，用 production traffic replay 定期更新 saturation point，讓容量預測的輸入持續校準。</li>
</ul>
<h2 id="產業情境電商與零售">產業情境：電商與零售</h2>
<p>電商流量的核心特徵是可預期的季節性峰值（雙十一、Black Friday、Prime Day）與不可預期的閃購爆量。兩者對 workload model 的需求不同，混用同一套模型會讓壓測結論對其中一種場景失真。</p>
<p>季節性峰值的 workload model 需要涵蓋三個電商特有維度：流量上升斜率（開賣瞬間的階梯式爆增 vs 活動期間的漸進增長）、讀寫比例變化（瀏覽階段讀為主 → 結帳階段寫入爆增）、庫存查詢的 cache miss 率（熱門商品快取因庫存變動頻繁失效）。<a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify 的 BFCM 容量治理</a>把這類峰值的容量驗證制度化為年度 game day。</p>
<p>閃購型流量的特徵是持續時間極短（分鐘級）但倍率極高（日常的 10-50 倍）。常規壓測用日均流量推算會完全漏掉這種尖峰，需要獨立的 burst scenario 模擬開賣瞬間的並發衝擊。</p>
<p>轉換率是電商特有的穩態指標。load test 的判讀不只看 latency 和 error rate，還要看結帳轉換率是否在壓力下劣化。研究顯示 latency 上升 100ms 可能讓轉換率下降 1-7%，這個商業影響在純技術指標中看不到。壓測結果要同時記錄技術指標與業務指標，容量決策才能對齊商業價值。</p>
<h2 id="操作判讀">操作判讀</h2>
<table>
  <thead>
      <tr>
          <th>觀察到的狀況</th>
          <th>可能原因</th>
          <th>下一步行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>壓測通過但 production peak 仍故障</td>
          <td>workload model 未涵蓋峰值形狀或 cohort 比例</td>
          <td>用 access log 重建 peak 時段模型</td>
      </tr>
      <tr>
          <td>latency 在低負載就開始劣化</td>
          <td>staging 資料量不足、query plan 與 production 不同</td>
          <td>用 production-like 資料量重測</td>
      </tr>
      <tr>
          <td>throughput ceiling 遠高於 production</td>
          <td>synthetic traffic 繞過 auth/cache 路徑</td>
          <td>加入 realistic session 與 cache miss scenario</td>
      </tr>
      <tr>
          <td>壓測結果每月差異大</td>
          <td>workload model drift</td>
          <td>建立定期校準流程、對比 p50/p95 偏移</td>
      </tr>
      <tr>
          <td>瓶頸定位不出來</td>
          <td>缺少資源層同步觀測</td>
          <td>壓測時同步收 CPU / memory / pool / queue 指標</td>
      </tr>
      <tr>
          <td>cache miss 場景未被覆蓋</td>
          <td>workload 只有 warm cache 情境</td>
          <td>參考 <a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest P1</a> 設計 cold start scenario</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>workload 是合成的、跟 production traffic shape 不同</li>
<li>壓測通過但 production peak 失敗、模型未涵蓋實際模式</li>
<li>只測 throughput、不測 saturation 與 cost curve</li>
<li>bottleneck 識別靠經驗、無系統定位流程</li>
<li>capacity 規劃靠一次性 load test 結論、無持續對齊</li>
<li>load-test 模型超過 6 個月未校準、drift 累積</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界</a>：load test 餵給容量規劃輸入</li>
<li><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a>：load baseline 升級為持續 gate</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：production load test 的 blast radius 與 stop condition</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：load test 驗證 saturation 前後的穩態維持</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：load test 結果作為 release 放行的容量證據</li>
<li><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 reliability metrics</a>：把流量與可靠性指標接起來</li>
</ul>
]]></content:encoded></item><item><title>8.2 事故指揮與角色分工</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>事故指揮與角色分工是把臨場混亂轉成可運作結構的核心節點。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 定義路由決策，scribe 負責記錄時間線，liaison 負責對接外部或跨團隊資訊，owner 負責修復，這些角色的責任要先被切清楚，事故才能收斂。&lt;/p>
&lt;p>這個節點先處理角色，再處理協作。只要角色重疊，事故就會在「誰決定、誰回報、誰修復」上卡住；只要角色缺失，事故就會在同步與交接時失真。這一章要建立的是協作路由，而不是英雄式處理。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a>&lt;/li>
&lt;li>role ownership&lt;/li>
&lt;li>decision boundary&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 的責任是把注意力放在最重要的決策上，而不是親自修所有東西。當事故正在擴散時，incident commander 要先知道風險在往哪裡走，再決定是止血、降級還是切換。scribe 的責任是把決策、時間、責任與下一步整理成後續可回放的時間線，做筆記只是最基本的一層。&lt;/p>
&lt;p>role ownership 的責任是讓每個人知道自己在事故中的邊界。若 owner 不清楚，修復會被反覆來回拉扯；若 liaison 不清楚，對外資訊會失真；若 decision boundary 不清楚，討論就會卡在協商而不是行動。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>incident commander / scribe / liaison 角色重疊或缺失&lt;/li>
&lt;li>同一人兼太多角色、決策變 bottleneck&lt;/li>
&lt;li>decision boundary 不清、跨角色協商耗時&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a> 靠口頭交接、無書面 state&lt;/li>
&lt;li>工程師被臨時 page 進事故、不知道角色與職責&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;p>Atlassian 是最適合看角色分工的案例，因為它把 14 天事故中的 incident commander 輪值、跨團隊協作與客戶溝通都完整公開。Slack 可以補通訊面，因為事故工具本身的可用性會直接影響對外節奏。GitHub 則能看出 status update 與內部復原如何維持同一條時間線。&lt;/p>
&lt;p>Datadog 和 Roblox 也很有用，前者讓我們看到監控供應商自己失明時怎麼協作，後者讓我們看到長尾恢復時角色如何跨班次接力。把這些案例一起看，會發現角色分工是讓事故不會因為協作失序而延長的控制面，形式化的分工反而幫助有限。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Incident Commander&lt;/td>
 &lt;td>決策路由、優先序、節奏控制&lt;/td>
 &lt;td>親自修復、過度介入技術細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scribe&lt;/td>
 &lt;td>記錄時間線、決策與待辦&lt;/td>
 &lt;td>只記結果不記上下文&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Liaison&lt;/td>
 &lt;td>對外 / 對跨團隊溝通&lt;/td>
 &lt;td>沒有同步最新狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>實際修復、驗證、回復&lt;/td>
 &lt;td>邊界不清、被多方拉扯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subject Matter Expert&lt;/td>
 &lt;td>提供技術判斷與風險評估&lt;/td>
 &lt;td>直接搶走決策權&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是分工清楚，不是職稱固定。小團隊可以兼任，但責任不能重疊到失去路由。&lt;/p>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>08.12 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a>：長事故跨班次協調&lt;/li>
&lt;li>08.14 multi-incident：meta-&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色與 incident command system pool 協調&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>事故指揮與角色分工是把臨場混亂轉成可運作結構的核心節點。<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 定義路由決策，scribe 負責記錄時間線，liaison 負責對接外部或跨團隊資訊，owner 負責修復，這些角色的責任要先被切清楚，事故才能收斂。</p>
<p>這個節點先處理角色，再處理協作。只要角色重疊，事故就會在「誰決定、誰回報、誰修復」上卡住；只要角色缺失，事故就會在同步與交接時失真。這一章要建立的是協作路由，而不是英雄式處理。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a></li>
<li>role ownership</li>
<li>decision boundary</li>
<li><a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a></li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a></li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 的責任是把注意力放在最重要的決策上，而不是親自修所有東西。當事故正在擴散時，incident commander 要先知道風險在往哪裡走，再決定是止血、降級還是切換。scribe 的責任是把決策、時間、責任與下一步整理成後續可回放的時間線，做筆記只是最基本的一層。</p>
<p>role ownership 的責任是讓每個人知道自己在事故中的邊界。若 owner 不清楚，修復會被反覆來回拉扯；若 liaison 不清楚，對外資訊會失真；若 decision boundary 不清楚，討論就會卡在協商而不是行動。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>incident commander / scribe / liaison 角色重疊或缺失</li>
<li>同一人兼太多角色、決策變 bottleneck</li>
<li>decision boundary 不清、跨角色協商耗時</li>
<li><a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a> 靠口頭交接、無書面 state</li>
<li>工程師被臨時 page 進事故、不知道角色與職責</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<p>Atlassian 是最適合看角色分工的案例，因為它把 14 天事故中的 incident commander 輪值、跨團隊協作與客戶溝通都完整公開。Slack 可以補通訊面，因為事故工具本身的可用性會直接影響對外節奏。GitHub 則能看出 status update 與內部復原如何維持同一條時間線。</p>
<p>Datadog 和 Roblox 也很有用，前者讓我們看到監控供應商自己失明時怎麼協作，後者讓我們看到長尾恢復時角色如何跨班次接力。把這些案例一起看，會發現角色分工是讓事故不會因為協作失序而延長的控制面，形式化的分工反而幫助有限。</p>
<h2 id="角色分工">角色分工</h2>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>主要責任</th>
          <th>常見失誤</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident Commander</td>
          <td>決策路由、優先序、節奏控制</td>
          <td>親自修復、過度介入技術細節</td>
      </tr>
      <tr>
          <td>Scribe</td>
          <td>記錄時間線、決策與待辦</td>
          <td>只記結果不記上下文</td>
      </tr>
      <tr>
          <td>Liaison</td>
          <td>對外 / 對跨團隊溝通</td>
          <td>沒有同步最新狀態</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>實際修復、驗證、回復</td>
          <td>邊界不清、被多方拉扯</td>
      </tr>
      <tr>
          <td>Subject Matter Expert</td>
          <td>提供技術判斷與風險評估</td>
          <td>直接搶走決策權</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是分工清楚，不是職稱固定。小團隊可以兼任，但責任不能重疊到失去路由。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.12 <a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a>：長事故跨班次協調</li>
<li>08.14 multi-incident：meta-<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色與 incident command system pool 協調</li>
</ul>
]]></content:encoded></item><item><title>模組二：快取與 Redis</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/</guid><description>&lt;p>快取模組的核心目標是說明暫存資料如何提升讀取效率，同時保護 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 的正式判斷責任。語言教材會處理 cache port、資料複製邊界與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 的程式邊界；本模組負責 Redis 與快取策略的具體實作。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 Redis / Valkey / Memcached / DragonflyDB / AWS ElastiCache，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &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/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">Cache aside&lt;/a>&lt;/td>
 &lt;td>read-through 思路、cache &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a>、invalidation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a>&lt;/td>
 &lt;td>過期策略、容量控制、熱點資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">Redis data types&lt;/a>&lt;/td>
 &lt;td>string、hash、set、sorted set、stream 的適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">Presence store&lt;/a>&lt;/td>
 &lt;td>即時連線狀態、過期清理、跨節點查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">Distributed lock&lt;/a>&lt;/td>
 &lt;td>lock 語意、租約、失效與風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a>&lt;/td>
 &lt;td>即時通知、跨節點 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>、可靠性限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="快取分層與邊緣層">快取分層與邊緣層&lt;/h2>
&lt;p>本模組討論的是「應用層快取」（Redis、in-memory cache），跟 CDN / edge cache 是不同責任：CDN 解決「請求是否需要進到應用程式」（網路入口層），本模組討論的快取解決「應用程式如何降低資料層讀寫成本」（應用層）。完整三層快取分工（邊緣層 → 應用層 → DB buffer pool）跟 origin protection 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a>。&lt;/p>
&lt;p>兩層快取的失效路徑要協調設計：應用層 purge 在自家 cluster 內可控、CDN purge 要等全球節點同步。寫入路徑變更時，要先 purge 應用層、再 purge 邊緣層，避免短時間內邊緣回填到應用層舊資料。&lt;/p>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>快取選型的核心判斷是資料是否可以重建，以及讀取壓力是否集中。當正式狀態已經存在於資料庫或下游服務，但熱門讀取造成延遲、成本或容量壓力時，快取與 Redis 值得優先評估。&lt;/p>
&lt;p>Cache aside 適合商品詳情、權限摘要、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 這類可重建讀取資料；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 用來控制資料新鮮度與容量；Redis data types 用來表達 set、sorted set、hash、stream 等不同資料形狀；presence store 適合即時連線狀態；distributed lock 適合需要短時間互斥的協調流程；pub/sub 適合即時 fan-out。&lt;/p></description><content:encoded><![CDATA[<p>快取模組的核心目標是說明暫存資料如何提升讀取效率，同時保護 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 的正式判斷責任。語言教材會處理 cache port、資料複製邊界與 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 的程式邊界；本模組負責 Redis 與快取策略的具體實作。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 Redis / Valkey / Memcached / DragonflyDB / AWS ElastiCache，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">Cache aside</a></td>
          <td>read-through 思路、cache <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a>、invalidation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a></td>
          <td>過期策略、容量控制、熱點資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">Redis data types</a></td>
          <td>string、hash、set、sorted set、stream 的適用場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">Presence store</a></td>
          <td>即時連線狀態、過期清理、跨節點查詢</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">Distributed lock</a></td>
          <td>lock 語意、租約、失效與風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a></td>
          <td>即時通知、跨節點 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、可靠性限制</td>
      </tr>
  </tbody>
</table>
<h2 id="快取分層與邊緣層">快取分層與邊緣層</h2>
<p>本模組討論的是「應用層快取」（Redis、in-memory cache），跟 CDN / edge cache 是不同責任：CDN 解決「請求是否需要進到應用程式」（網路入口層），本模組討論的快取解決「應用程式如何降低資料層讀寫成本」（應用層）。完整三層快取分工（邊緣層 → 應用層 → DB buffer pool）跟 origin protection 設計見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a>。</p>
<p>兩層快取的失效路徑要協調設計：應用層 purge 在自家 cluster 內可控、CDN purge 要等全球節點同步。寫入路徑變更時，要先 purge 應用層、再 purge 邊緣層，避免短時間內邊緣回填到應用層舊資料。</p>
<h2 id="選型入口">選型入口</h2>
<p>快取選型的核心判斷是資料是否可以重建，以及讀取壓力是否集中。當正式狀態已經存在於資料庫或下游服務，但熱門讀取造成延遲、成本或容量壓力時，快取與 Redis 值得優先評估。</p>
<p>Cache aside 適合商品詳情、權限摘要、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 這類可重建讀取資料；<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 用來控制資料新鮮度與容量；Redis data types 用來表達 set、sorted set、hash、stream 等不同資料形狀；presence store 適合即時連線狀態；distributed lock 適合需要短時間互斥的協調流程；pub/sub 適合即時 fan-out。</p>
<p>接近真實網路服務的例子包括熱門商品頁、會員 session、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> presence、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> counter 與跨節點通知。這些場景的共同問題是讀取節奏、過期策略與資料一致性，因此本模組會先處理資料形狀、<a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、<a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 與失效邊界。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理 interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a>、並發或非同步保護、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與 cache 呼叫邊界。Backend cache 模組處理 Redis command、資料結構、失效策略、跨節點一致性與操作風險。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>快取案例的核心讀法是先看「一致性問題長什麼樣」，再決定要調策略還是調架構。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta：Cache Consistency 升級</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>把 invalidation 問題前移到訊號治理 + mutation tracing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>把快取路由層納入可用性邊界、跨區一致性窗口設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify：序列化遷移</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>把格式轉換做成雙軌相容與可回退流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta：CacheLib / Kangaroo 分層快取</a></td>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>分層 cache 容量跟成本曲線（DRAM / flash / 持久 KV）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify：Write-through Cache</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>cache aside / write-through / write-behind 選擇條件</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix：EVCache 全域快取層</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>、<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a></td>
          <td>cache 成為跨區資料層、平台層基礎設施</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare：Cache Reserve 分層</a></td>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>edge + persistent reserve 的長尾命中率設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta：TAO 社交圖快取演進</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>cache 變資料層能力、資料模型治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder：ElastiCache 47M MAU</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>cache 是主要服務面、sustained growth 成本曲線</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi：ML feature store</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>ML feature store 三層 cache 設計、cache vs persistent store 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap：KeyDB cross-cloud</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>KeyDB multi-threaded fork、跨 cloud 部署資料引力</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a></td>
          <td>高併發下的 Redis 讀寫邊界</td>
          <td>共用 client、控制 pipeline、避免 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 與 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>cache aside 與失效策略</td>
          <td>寫出讀取優先的 cache 流程與失效方式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>TTL 與 eviction</td>
          <td>規劃過期、淘汰與容量控制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.4</a></td>
          <td>distributed lock 與租約</td>
          <td>分辨鎖語意、租約風險與適用場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5</a></td>
          <td>presence store 與即時狀態</td>
          <td>追蹤線上狀態、跨節點查詢與過期清理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">2.6</a></td>
          <td>快取威脅建模（Threat Modeling）</td>
          <td>用一致性、污染、放大與 side-channel 風險盤點快取設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>Cache Copy Boundary 與 Freshness</td>
          <td>分辨快取副本、正式狀態、新鮮度與回源保護</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a></td>
          <td>Cache Data Shape 與 Access Pattern</td>
          <td>用 key space、value shape 與 access pattern 判讀資料形狀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9</a></td>
          <td>Cache Migration 與 Stampede Rollback 實作示範</td>
          <td>以商品詳情或價格快取示範 evidence、gate 與 rollback trigger</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10</a></td>
          <td>Pub/Sub 與即時 fan-out</td>
          <td>用 at-most-once 邊界判讀即時廣播何時夠用、何時升級到 Streams 或 message queue</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11</a></td>
          <td>Redis data types 實作</td>
          <td>用 sorted set、bitmap、HLL、counter、hash 各自的原子性與記憶體曲線選型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/" data-link-title="模組二案例正文" data-link-desc="快取策略與快取平台演進案例入口。">2.C</a></td>
          <td>轉換案例正文</td>
          <td>把快取策略、路由層與序列化遷移轉成可回寫實作</td>
      </tr>
  </tbody>
</table>
<p>反例與規模對照入口： <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> / <a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，快取案例要優先保留回源壓力、資料新鮮度與熱門 key 行為。</p>
<p>跨模組端到端串聯（DB → cache → event → observability）見 <a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>快取章節下一輪的核心責任是把「暫存副本」和「正式狀態」的界線寫清楚。現有章節已經有 cache aside、TTL、distributed lock、presence store，並補上了 Pub/Sub 即時 fan-out（2.10）與 data types 型別實作（2.11）兩個向度；仍可深化的是資料新鮮度、失效語意、回源保護與快取遷移之間的引用關係，讓讀者知道快取策略何時只是加速，何時已經變成服務正確性風險。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cache copy boundary</td>
          <td>cache value 是否只是可重建副本，還是被誤用成正式狀態</td>
          <td><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a></td>
      </tr>
      <tr>
          <td>Freshness window</td>
          <td>stale data 在產品上可接受多久，誰承擔錯誤後果</td>
          <td><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Invalidation model</td>
          <td>更新、刪除、TTL、event invalidation 是否互相對齊</td>
          <td><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
      </tr>
      <tr>
          <td>Origin protection</td>
          <td>miss、hot key、stampede 是否會把壓力打回資料庫</td>
          <td><a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>Cache migration</td>
          <td>key format、value schema、TTL 策略是否能分批回退</td>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用快取自己的服務壓力展開。商品詳情、價格、權限摘要、presence 與 rate limit 的失敗代價不同，寫作時要分別處理它們的新鮮度與回源壓力。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>快取模組的 knowledge card 缺口集中在「新鮮度」與「回源保護」。已有 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate</a>、<a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a>、<a href="/blog/backend/knowledge-cards/cache-prefetching/" data-link-title="Cache Prefetching" data-link-desc="說明系統如何在資料被需要前預先載入快取">cache prefetching</a> 與 <a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 可以先引用。</p>
<p>下一批候選卡片包括 freshness window、origin protection、request coalescing（single-flight）、negative cache、cache key versioning 與 cache serialization migration。這些卡片要讓讀者能分辨「可短暫不新鮮」和「錯誤會直接影響交易或權限」的差異。2.4 帶入的 fencing token 是跨模組的分散式術語、且是「鎖不是正確性保證」這個核心論點的依據，值得獨立建卡（候選）。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>快取的第一條實作路徑是 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback（實作示範）</a>。這篇以商品詳情或價格快取為例，說明 cache evidence package、origin protection gate、warmup plan 與 rollback trigger 如何一起成立。型別實作層面的具體入口是 <a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11 Redis data types 實作</a>，聚焦 sorted set、bitmap、HLL、counter、hash 各自的操作語意、原子性與容量行為。</p>
<p>這條路徑的前置引用應該是 2.2 cache aside、2.3 TTL / eviction、<a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a> 與 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>快取路徑的 artifact 對齊重點是「先證明回源壓力受控，再擴大快取覆蓋率」。對 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> / <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 hit/miss、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 與 hot key 分布；對 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 warmup 演練與 stampede 停損門檻；對 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a> / <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 key pattern、影響範圍與修復後追蹤信號。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>快取與 Redis 的使用方式會受語言的資料複製模型、client lifecycle、序列化成本與並發模型影響。同步 runtime 要避免每個 request 建立連線；async runtime 要避免 blocking Redis client 卡住 event loop；輕量並發 runtime 要用 timeout、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與 pipeline 邊界保護 Redis。動態語言要特別留意 cache value schema 演進；強型別語言則要避免把內部型別直接當成跨服務快取 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a>。</p>
]]></content:encoded></item><item><title>4.3 tracing 與 context link</title><link>https://tarrragon.github.io/blog/backend/04-observability/tracing-context/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/tracing-context/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 模型&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> propagation&lt;/li>
&lt;li>context 斷鏈的常見邊界與修復&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略的 tracing 面（SSoT 在 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）&lt;/li>
&lt;li>service graph 與依賴發現&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace&lt;/a> 是把一次 request 在多個服務、queue 與背景任務中的路徑串起來的診斷訊號，責任是讓團隊從症狀追到跨服務等待點。&lt;/p>
&lt;p>Log 回答「某個服務發生了什麼」；metric 回答「某個服務的健康趨勢」；trace 回答「一次 request 跨多個服務時，時間花在哪、錯誤發生在哪一段」。三者互補，trace 的獨特價值在於它串起跨服務的因果鏈 — 沒有 trace，事故定位只能靠人工比對不同服務的 log timestamp。&lt;/p>
&lt;p>本章處理的是 context propagation — 怎麼讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 HTTP call、queue 投遞、背景任務啟動等邊界上正確傳遞。Context 斷掉時，trace 從「完整路徑」退化成幾段需要人工拼接的局部紀錄，跨服務診斷的時間成本會從秒級回退到分鐘甚至小時級。&lt;/p>
&lt;h2 id="trace-與-span-的結構">Trace 與 Span 的結構&lt;/h2>
&lt;h3 id="span-是-trace-的基本單位">Span 是 trace 的基本單位&lt;/h3>
&lt;p>一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 代表一段有起止時間的工作。每個 span 記錄：操作名稱（&lt;code>POST /api/orders&lt;/code>）、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception、log message）。&lt;/p>
&lt;p>Span 之間透過 parent-child 關係組成 tree。一個 HTTP request 進入 API gateway 時建立 root span，gateway 呼叫 order service 時建立 child span，order service 查 DB 時建立另一個 child span。整棵 tree 共享同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>，讓所有 span 可以被聚合成一次 request 的完整路徑。&lt;/p>
&lt;h3 id="trace-是-span-tree">Trace 是 span tree&lt;/h3>
&lt;p>一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 是所有共享同一個 trace id 的 span 的集合。在 waterfall view 中，trace 呈現為時間軸上的巢狀條狀圖 — root span 在最上面，child span 依序往下排列，每段的長度代表耗時。&lt;/p>
&lt;p>Waterfall view 的診斷價值是「一眼看到時間花在哪」。如果 checkout API 的 total latency 是 800ms，waterfall 會顯示 payment service 佔了 600ms — 問題定位從「整個 checkout 慢」縮小到「payment service 慢」，後續 debug 只需要看 payment service 的 log 跟 metric。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> / <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 模型</li>
<li><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> propagation</li>
<li>context 斷鏈的常見邊界與修復</li>
<li><a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略的 tracing 面（SSoT 在 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）</li>
<li>service graph 與依賴發現</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace</a> 是把一次 request 在多個服務、queue 與背景任務中的路徑串起來的診斷訊號，責任是讓團隊從症狀追到跨服務等待點。</p>
<p>Log 回答「某個服務發生了什麼」；metric 回答「某個服務的健康趨勢」；trace 回答「一次 request 跨多個服務時，時間花在哪、錯誤發生在哪一段」。三者互補，trace 的獨特價值在於它串起跨服務的因果鏈 — 沒有 trace，事故定位只能靠人工比對不同服務的 log timestamp。</p>
<p>本章處理的是 context propagation — 怎麼讓 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 HTTP call、queue 投遞、背景任務啟動等邊界上正確傳遞。Context 斷掉時，trace 從「完整路徑」退化成幾段需要人工拼接的局部紀錄，跨服務診斷的時間成本會從秒級回退到分鐘甚至小時級。</p>
<h2 id="trace-與-span-的結構">Trace 與 Span 的結構</h2>
<h3 id="span-是-trace-的基本單位">Span 是 trace 的基本單位</h3>
<p>一個 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 代表一段有起止時間的工作。每個 span 記錄：操作名稱（<code>POST /api/orders</code>）、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception、log message）。</p>
<p>Span 之間透過 parent-child 關係組成 tree。一個 HTTP request 進入 API gateway 時建立 root span，gateway 呼叫 order service 時建立 child span，order service 查 DB 時建立另一個 child span。整棵 tree 共享同一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>，讓所有 span 可以被聚合成一次 request 的完整路徑。</p>
<h3 id="trace-是-span-tree">Trace 是 span tree</h3>
<p>一個 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 是所有共享同一個 trace id 的 span 的集合。在 waterfall view 中，trace 呈現為時間軸上的巢狀條狀圖 — root span 在最上面，child span 依序往下排列，每段的長度代表耗時。</p>
<p>Waterfall view 的診斷價值是「一眼看到時間花在哪」。如果 checkout API 的 total latency 是 800ms，waterfall 會顯示 payment service 佔了 600ms — 問題定位從「整個 checkout 慢」縮小到「payment service 慢」，後續 debug 只需要看 payment service 的 log 跟 metric。</p>
<h2 id="context-propagation">Context Propagation</h2>
<h3 id="什麼是-trace-context">什麼是 trace context</h3>
<p><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context</a> 是跨服務傳遞 trace 身份的資料。最小的 trace context 包含 trace id（標識整條 trace）跟 parent span id（標識上游 span）。下游服務收到 trace context 後，建立新的 child span 並繼承 trace id，讓兩端的 span 歸屬同一條 trace。</p>
<p>W3C Trace Context 標準定義了 HTTP header 的傳遞格式：<code>traceparent</code> header 帶 trace id + parent span id + trace flags，<code>tracestate</code> header 帶 vendor-specific 的附加資訊。OpenTelemetry SDK 預設使用 W3C 格式；部分 vendor 有自己的 header 格式（Datadog 用 <code>x-datadog-trace-id</code>、AWS X-Ray 用 <code>X-Amzn-Trace-Id</code>），需要在 collector 或 SDK 層做格式轉換。</p>
<h3 id="propagation-的傳遞機制">Propagation 的傳遞機制</h3>
<p>HTTP call 是最常見的 propagation 路徑 — SDK 的 HTTP client middleware 自動把 trace context 注入 request header，下游 SDK 的 HTTP server middleware 自動從 header 提取 context。大部分 OpenTelemetry SDK 的 auto-instrumentation 會自動處理這一層，開發者不需要手動注入。</p>
<p>gRPC 用 metadata（等同 HTTP header）傳遞，機制類似。</p>
<p>Message queue 的 propagation 需要把 trace context 放進 message 的 header 或 metadata。Kafka 用 record header、RabbitMQ 用 message properties、NATS 用 message header。Producer 端注入、consumer 端提取。Queue 的 propagation 比 HTTP 複雜的原因是 consumer 可能在 producer 之後很久才消費 — context 的時間跨度可能從毫秒擴大到分鐘或小時。</p>
<h3 id="context-斷鏈的常見邊界">Context 斷鏈的常見邊界</h3>
<p>Context propagation 在以下邊界容易斷裂：</p>
<p><strong>Thread / goroutine / task 邊界</strong>：同步 runtime 通常用 thread-local 存放 context，新開 thread 不會自動繼承。Go 用 <code>context.Context</code> 顯式傳遞，相對不容易遺漏；Java 用 ThreadLocal，啟動新 thread 或提交到 thread pool 時 context 需要手動傳遞或用 agent auto-instrumentation。Async runtime（Node.js 的 AsyncLocalStorage、Python 的 contextvars）各有自己的 context 傳播機制。</p>
<p><strong>Queue / event 邊界</strong>：producer 把 trace context 注入 message header，consumer 提取並建立新 span。如果 producer 端的 SDK 沒有自動注入（例如用了原生 Kafka client 而非 instrumented client），context 就斷了。跨 queue 的 trace 在 waterfall view 中會出現時間斷層 — producer span 結束到 consumer span 開始之間可能有秒級到分鐘級的等待。</p>
<p><strong>Background job / cron 邊界</strong>：cron job 或 scheduled task 沒有上游 request，沒有 trace context 可繼承。這類工作需要在啟動時建立 root span，並把 job name、schedule、trigger reason 作為 span 屬性，讓 trace 至少可以追蹤 job 內部的行為。</p>
<p><strong>跨語言 / 跨 vendor 邊界</strong>：不同語言的 SDK 或不同 vendor 的 instrumentation 可能用不同的 header 格式。W3C Trace Context 標準解決了格式問題，但混用 vendor-specific SDK 時（例如一個服務用 Datadog agent、另一個用 OTel SDK），需要在 collector 層做 context format 轉換。</p>
<h3 id="斷鏈的修復策略">斷鏈的修復策略</h3>
<p>修復斷鏈的目標是讓 trace 在邊界處重新接上，不需要人工拼接。</p>
<p><strong>Queue 邊界</strong>：確保 producer 跟 consumer 都使用 instrumented client（OTel SDK 的 messaging instrumentation），而非原生 client。Instrumented client 自動處理 header 注入跟提取。Consumer 端建立的 span 用 <code>CONSUMER</code> kind 標記，waterfall view 會顯示 queue 等待時間。</p>
<p><strong>Thread pool 邊界</strong>：Java 生態用 <code>Context.wrap()</code> 包裝提交到 thread pool 的 Runnable/Callable；Go 生態用 <code>context.Context</code> 作為第一個函數參數傳遞（這是 Go 的慣例，不需要額外處理）。Auto-instrumentation agent 可以自動處理常見 thread pool（Java 的 ExecutorService、Node.js 的 worker_threads）。</p>
<p><strong>跨 vendor 邊界</strong>：在 collector 層（OTel Collector）統一轉換 header 格式。Collector 的 receiver 支援多種格式輸入，exporter 統一輸出 W3C 格式。這層轉換在 <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 中介段處理。</p>
<h2 id="trace-與-log--metric-的關聯">Trace 與 Log / Metric 的關聯</h2>
<h3 id="correlation-id-統一">Correlation id 統一</h3>
<p><a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">Trace id</a> 應該同時出現在 log 的結構化欄位中。當 log 的 <code>trace_id</code> 欄位帶著跟 trace 相同的值，debug 工作流就能從 trace waterfall 跳到某個 span 對應的 log，或從 log 跳到完整的 trace view。</p>
<p>實作方式是在 logger 初始化時，把當前 span 的 trace id 注入 log 的 context fields。OTel SDK 的 log bridge 可以自動做這件事；沒有自動橋接的框架需要手動把 <code>span.SpanContext().TraceID()</code> 寫進 log 的 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> 欄位。</p>
<h3 id="exemplarmetric-到-trace-的跳板">Exemplar：metric 到 trace 的跳板</h3>
<p>Metric 是聚合訊號，本身不帶單一 request 的 trace id。Exemplar 是附加在 metric 資料點上的代表性 trace id — 當某個 histogram bucket 收到一個資料點時，附帶記錄產生這個資料點的 trace id。</p>
<p>Dashboard 上看到 latency p99 升高時，可以從 exemplar 跳到一個具體的高延遲 trace，看 waterfall 定位慢在哪。Exemplar 是 metric 到 trace 的橋樑，讓聚合訊號（metric）跟個別案例（trace）連接起來。</p>
<h2 id="service-graph-與依賴發現">Service Graph 與依賴發現</h2>
<p>Trace 資料聚合後可以自動生成 service graph — 哪些服務在呼叫哪些服務、call 的頻率、延遲分布、錯誤率。這個 graph 跟手動維護的 architecture diagram 不同：它來自實際流量，反映的是「現在真的在發生什麼」而非「設計時預期會發生什麼」。</p>
<p>Service graph 的價值在於依賴發現。新服務加入後，如果有 trace instrumentation，它會自動出現在 graph 上。舊服務之間新增的依賴（例如 A 開始直接呼叫 C、繞過 B）也會被 graph 反映。手動維護的 wiki 通常落後實際狀況數週到數月。</p>
<p>Service graph 的完整性取決於 trace 的覆蓋率。如果某些服務沒有 instrumentation 或 sampling 率太低，graph 上會出現斷點或邊權不準。把 service graph 的完整性（「有多少比例的服務有 trace」）作為觀測覆蓋率的一個指標，能推動 instrumentation 的漸進覆蓋。</p>
<p>詳見 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 tracing 時，先看 propagation 是否完整，再看 sampling 是否保留可除錯樣本。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 是否能和 log、metric 共享 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a></li>
<li>async / queue / background job 是否能保留 parent-child 關係</li>
<li>sampling 是否能在高流量下保留錯誤與高延遲樣本（策略矩陣見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）</li>
<li>service graph 是否能由 trace 聚合而來，並降低 wiki 手動維護成本</li>
<li>trace context 在跨語言 / 跨 vendor 邊界是否用 W3C 標準統一</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Request 跨服務後 trace 斷鏈、靠人重組</li>
<li>Async / queue 邊界 context 沒傳遞</li>
<li>採樣率太低、production debug 找不到對應 trace</li>
<li>Trace id 跟 log / metric 對不上、無共同 correlation key</li>
<li>Service graph 不存在或半年沒人看</li>
<li>多個 vendor SDK 混用、header 格式不一致</li>
<li>Background job / cron 沒有 root span、trace 無法追蹤</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只 instrument HTTP、忽略 queue</td>
          <td>Queue 消費後的 span 都是孤兒</td>
          <td>Producer / consumer 都用 instrumented client</td>
      </tr>
      <tr>
          <td>Thread pool 不傳 context</td>
          <td>平行處理的 span 不歸屬任何 trace</td>
          <td>用 Context.wrap() 或語言慣例傳遞 context</td>
      </tr>
      <tr>
          <td>Trace id 沒寫進 log</td>
          <td>從 log 找不到對應 trace、反向也找不到</td>
          <td>Logger context 注入 trace id</td>
      </tr>
      <tr>
          <td>混用 vendor header 無轉換</td>
          <td>部分服務的 span 串不進同一條 trace</td>
          <td>Collector 層統一轉換成 W3C 格式</td>
      </tr>
      <tr>
          <td>所有 span 都是 root span</td>
          <td>Trace 只有一層、沒有 parent-child 結構</td>
          <td>確認 SDK 的 context extraction 有正確從 header 繼承</td>
      </tr>
      <tr>
          <td>Background job 無 instrumentation</td>
          <td>Job 內的 DB / HTTP call 沒有 trace 可追蹤</td>
          <td>Job 啟動時建立 root span、內部操作作為 child span</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：trace 資料在 dashboard 的呈現跟 alert 設計</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：sampling 策略矩陣（Head / Tail / Adaptive / Exemplar）與保留決策</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>：sampling 在 collector 的集中治理、跨 vendor header 轉換</li>
<li><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>：trace 訊號聚合成依賴圖</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：sampling bias 跟 trace 完整性的資料品質</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：trace 查詢作為即席診斷的一種模式</li>
</ul>
]]></content:encoded></item><item><title>10.3 託管形態遷出：資產線盤點與並行期執行</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的升級自建 tripwire 回答「何時該重新評估」、評估成立後、本章接手回答「按下遷出鍵之後的工程」。讀者情境：產品跑在 Wix / Shopify / Firebase / WordPress 這類託管形態上、tripwire 已命中、目標是自建或半託管。遷出的核心原則是把「搬家」拆成多條資產線各自的受控 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>：資料、身分、流量、整合的可攜性差異極大、斷點位置不同、可以分開 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover&lt;/a> — 把它們綁成同一天的大爆炸切換（big bang cutover）、等於把可攜性最差的那條線的風險強加給其他所有線。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor lock-in&lt;/a> 在遷出日的具體形狀就是這幾條資產線的斷點。0.21 的可遷出保險清單（自有網域、資料定期匯出、客戶聯絡管道自有、金流可攜性、密碼不可攜的預案、業務邏輯文件化）是進場時買的保險、本章是理賠流程 — 保險有買、每條線的斷點都有現成出口；保險沒買、本章每一節都會多一段「先補保險再動手」的前置工。&lt;/p>
&lt;h2 id="資產線盤點">資產線盤點&lt;/h2>
&lt;p>動手前先盤點：這個產品在平台上累積了哪些資產、每項資產走哪條線、可攜性如何。盤點的產出是一張「資產 → 線 → 出口 → 斷點」對照、它決定後面所有階段的順序與凍結窗口長度。&lt;/p>
&lt;h3 id="資料線">資料線&lt;/h3>
&lt;p>資料線問兩個問題：拿得出來嗎、拿出來之後能直接用嗎。多數平台對第一個問題的答案是肯定的 — Shopify 的商品與訂單歷史有官方 CSV / API 匯出、WordPress 的文章與媒體是最成熟的匯出路徑、Firebase 的 Firestore 有官方 export。真正的工程量在第二個問題：匯出格式是平台資料模型的快照、不是自建系統的 schema。&lt;/p>
&lt;p>兩個典型情境。第一、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS&lt;/a> 的反正規化結構：Firestore 的文件沿查詢需求生長、同一份事實散在多個 collection、而目標端的關聯式 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> 要求單一事實單一位置 — 執行順序是先設計目標 schema、再寫轉換管線、而不是把 export 原樣灌進去。第二、半託管 CMS 的外掛私有表：WordPress 官方匯出涵蓋文章與媒體、外掛各自的私有表（會員等級、預約規則、客製欄位）在匯出範圍之外 — 每個外掛要單獨確認資料位置與匯出手段。盤點階段把這兩類「拿得出來但不能直接用」的資產標出來、它們是資料線時程的主要變數。&lt;/p>
&lt;p>歷史資料搬完之後、增量是另一個問題：平台在並行期仍持續產生新訂單、新會員、新內容、需要一條增量同步管道（webhook、API 輪詢、排程匯出）把變更餵進新系統 — 角色等同自建世界的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture&lt;/a>、只是來源是平台 API 而不是資料庫 log。&lt;/p>
&lt;p>資料線還有一類「可放棄、但要快照」的資產：平台內建報表與分析歷史。這類數據多數沒有匯出路徑、平台降級或關站後即消失 — 歷史明細可以放棄、但 cutover 後的健康判讀需要遷移前的基線（自然流量、轉換率、客單價）。盤點階段把基線指標匯出存檔、觀察期的「下滑超過預估」才有對照對象。&lt;/p>
&lt;h3 id="身分線">身分線&lt;/h3>
&lt;p>身分線的可攜性在所有資產線中分布最極端。會員的 email 與基本資料幾乎都可匯出；密碼雜湊多數平台拒絕交出 — Firebase Auth 是少數友善案例、官方工具可匯出密碼雜湊、演算法參數從主控台另行取得、自建認證系統照參數驗證即可無感銜接。多數平台（電商會員、網站會員系統）把雜湊留在自己手上、這條線的執行形態於是變成全體重設密碼。&lt;/p>
&lt;p>重設密碼遷移要當成產品功能設計、而不是遷移日的告示：分批寄送重設邀請、首次登入時引導重設、保留舊 email 驗證鏈路、把重設高峰排開行銷活動。0.21 可遷出保險裡「密碼不可攜的預案」指的就是這套體驗、執行階段它從預案變成排程上的工作項。&lt;/p>
&lt;p>Session 綁定在平台端、cutover 當天全體使用者重新登入是預設行為、要納入切換日的客服與監控預期。第三方登入（Google / Apple 登入）的識別碼可攜性介於兩者之間：識別碼存在 provider 端、但可能綁定在 OAuth client 或開發者帳號的範圍上 — Apple 的 user identifier 以開發者團隊為界、換團隊後同一使用者拿到不同識別碼。遷移前先用測試帳號驗證新舊系統拿到的識別碼一致、再決定第三方登入使用者要走無感銜接還是重新綁定。&lt;/p>
&lt;p>身分線的盤點對象除了終端使用者、還有操作者與機器：員工帳號、角色權限、API key 與第三方服務的 OAuth 授權都要在新系統重新佈建、並納入 cutover 演練 — 切換日客服登不進新後台、是這條線最常見的自傷事故。&lt;/p>
&lt;h3 id="流量線">流量線&lt;/h3>
&lt;p>流量線的前提是自有網域 — 0.21 可遷出保險清單裡的保險項。網域在自己名下、DNS 自己控制、流量切換就是一次 DNS 變更加一套轉址規則；流量活在平台贈送的子網域上、遷出等於換址、SEO 與既有連結歸零、這條線要先補保險（買網域、在平台上綁定、讓搜尋引擎與外部連結先收斂到自有網域）再談切換。&lt;/p>
&lt;p>執行面的關鍵是斷裂面管理。平台的 URL 結構（&lt;code>/products/handle&lt;/code>、&lt;code>/blogs/news/slug&lt;/code>）跟自建系統的路由幾乎必然不同、而離開平台後、舊 URL 的轉址規則沒有地方住 — 平台停用後它連 404 都不會回、是 DNS 直接指向新系統。所以轉址表（舊 URL 樣式 → 新 URL）要建在新系統自己身上：cutover 後由新系統對舊樣式回 301、搜尋引擎與外部連結沿轉址收斂。配套動作：cutover 前把 DNS TTL 調低、cutover 後重交 sitemap、用搜尋主控台觀察索引替換進度。SEO 累積是按月計的資產、轉址表缺漏的代價以自然流量下滑直接體現。&lt;/p>
&lt;h3 id="整合線">整合線&lt;/h3>
&lt;p>整合線收所有由平台出面跟外部世界握手的合約、其中金流是最硬的斷點 — 它在本章盤點順序排最後、執行確認要排最早、答案會改變整場遷移的形狀。一次性收款的遷移成本低 — 換金流串接、新訂單走新管道。訂閱制是另一回事：扣款授權 token 存在金流商的 vault 裡、且常綁定在平台名下的金流帳戶上。遷出時先問金流商「授權能否轉移到商家自有的金流帳戶」— 部分金流商支援處理商之間的卡號資料轉移、談得下來就是一次後台作業；談不下來、全體訂閱者重新授權、流失率直接換算成訂閱營收缺口。執行手段跟重設密碼同構：分批通知、寬限期、必要時用優惠補償授權摩擦。&lt;/p>
&lt;p>金流之外、整合線還包括：平台外掛 / app 生態承擔的業務邏輯（Shopify app 做的折扣規則、WordPress 外掛做的預約流程）要逐個盤點、決定重寫進自建系統、換成獨立 SaaS、或趁機放棄；對外 webhook（ERP、出貨、會計系統）的端點切換要跟對方排時程；行銷 email 的寄送信譽綁在平台的寄件網域上、換到自有寄件網域要重建 SPF / DKIM 並逐步暖機、避免遷移週的通知信全進垃圾箱。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的升級自建 tripwire 回答「何時該重新評估」、評估成立後、本章接手回答「按下遷出鍵之後的工程」。讀者情境：產品跑在 Wix / Shopify / Firebase / WordPress 這類託管形態上、tripwire 已命中、目標是自建或半託管。遷出的核心原則是把「搬家」拆成多條資產線各自的受控 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>：資料、身分、流量、整合的可攜性差異極大、斷點位置不同、可以分開 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover</a> — 把它們綁成同一天的大爆炸切換（big bang cutover）、等於把可攜性最差的那條線的風險強加給其他所有線。</p>
<p><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor lock-in</a> 在遷出日的具體形狀就是這幾條資產線的斷點。0.21 的可遷出保險清單（自有網域、資料定期匯出、客戶聯絡管道自有、金流可攜性、密碼不可攜的預案、業務邏輯文件化）是進場時買的保險、本章是理賠流程 — 保險有買、每條線的斷點都有現成出口；保險沒買、本章每一節都會多一段「先補保險再動手」的前置工。</p>
<h2 id="資產線盤點">資產線盤點</h2>
<p>動手前先盤點：這個產品在平台上累積了哪些資產、每項資產走哪條線、可攜性如何。盤點的產出是一張「資產 → 線 → 出口 → 斷點」對照、它決定後面所有階段的順序與凍結窗口長度。</p>
<h3 id="資料線">資料線</h3>
<p>資料線問兩個問題：拿得出來嗎、拿出來之後能直接用嗎。多數平台對第一個問題的答案是肯定的 — Shopify 的商品與訂單歷史有官方 CSV / API 匯出、WordPress 的文章與媒體是最成熟的匯出路徑、Firebase 的 Firestore 有官方 export。真正的工程量在第二個問題：匯出格式是平台資料模型的快照、不是自建系統的 schema。</p>
<p>兩個典型情境。第一、<a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a> 的反正規化結構：Firestore 的文件沿查詢需求生長、同一份事實散在多個 collection、而目標端的關聯式 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> 要求單一事實單一位置 — 執行順序是先設計目標 schema、再寫轉換管線、而不是把 export 原樣灌進去。第二、半託管 CMS 的外掛私有表：WordPress 官方匯出涵蓋文章與媒體、外掛各自的私有表（會員等級、預約規則、客製欄位）在匯出範圍之外 — 每個外掛要單獨確認資料位置與匯出手段。盤點階段把這兩類「拿得出來但不能直接用」的資產標出來、它們是資料線時程的主要變數。</p>
<p>歷史資料搬完之後、增量是另一個問題：平台在並行期仍持續產生新訂單、新會員、新內容、需要一條增量同步管道（webhook、API 輪詢、排程匯出）把變更餵進新系統 — 角色等同自建世界的 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture</a>、只是來源是平台 API 而不是資料庫 log。</p>
<p>資料線還有一類「可放棄、但要快照」的資產：平台內建報表與分析歷史。這類數據多數沒有匯出路徑、平台降級或關站後即消失 — 歷史明細可以放棄、但 cutover 後的健康判讀需要遷移前的基線（自然流量、轉換率、客單價）。盤點階段把基線指標匯出存檔、觀察期的「下滑超過預估」才有對照對象。</p>
<h3 id="身分線">身分線</h3>
<p>身分線的可攜性在所有資產線中分布最極端。會員的 email 與基本資料幾乎都可匯出；密碼雜湊多數平台拒絕交出 — Firebase Auth 是少數友善案例、官方工具可匯出密碼雜湊、演算法參數從主控台另行取得、自建認證系統照參數驗證即可無感銜接。多數平台（電商會員、網站會員系統）把雜湊留在自己手上、這條線的執行形態於是變成全體重設密碼。</p>
<p>重設密碼遷移要當成產品功能設計、而不是遷移日的告示：分批寄送重設邀請、首次登入時引導重設、保留舊 email 驗證鏈路、把重設高峰排開行銷活動。0.21 可遷出保險裡「密碼不可攜的預案」指的就是這套體驗、執行階段它從預案變成排程上的工作項。</p>
<p>Session 綁定在平台端、cutover 當天全體使用者重新登入是預設行為、要納入切換日的客服與監控預期。第三方登入（Google / Apple 登入）的識別碼可攜性介於兩者之間：識別碼存在 provider 端、但可能綁定在 OAuth client 或開發者帳號的範圍上 — Apple 的 user identifier 以開發者團隊為界、換團隊後同一使用者拿到不同識別碼。遷移前先用測試帳號驗證新舊系統拿到的識別碼一致、再決定第三方登入使用者要走無感銜接還是重新綁定。</p>
<p>身分線的盤點對象除了終端使用者、還有操作者與機器：員工帳號、角色權限、API key 與第三方服務的 OAuth 授權都要在新系統重新佈建、並納入 cutover 演練 — 切換日客服登不進新後台、是這條線最常見的自傷事故。</p>
<h3 id="流量線">流量線</h3>
<p>流量線的前提是自有網域 — 0.21 可遷出保險清單裡的保險項。網域在自己名下、DNS 自己控制、流量切換就是一次 DNS 變更加一套轉址規則；流量活在平台贈送的子網域上、遷出等於換址、SEO 與既有連結歸零、這條線要先補保險（買網域、在平台上綁定、讓搜尋引擎與外部連結先收斂到自有網域）再談切換。</p>
<p>執行面的關鍵是斷裂面管理。平台的 URL 結構（<code>/products/handle</code>、<code>/blogs/news/slug</code>）跟自建系統的路由幾乎必然不同、而離開平台後、舊 URL 的轉址規則沒有地方住 — 平台停用後它連 404 都不會回、是 DNS 直接指向新系統。所以轉址表（舊 URL 樣式 → 新 URL）要建在新系統自己身上：cutover 後由新系統對舊樣式回 301、搜尋引擎與外部連結沿轉址收斂。配套動作：cutover 前把 DNS TTL 調低、cutover 後重交 sitemap、用搜尋主控台觀察索引替換進度。SEO 累積是按月計的資產、轉址表缺漏的代價以自然流量下滑直接體現。</p>
<h3 id="整合線">整合線</h3>
<p>整合線收所有由平台出面跟外部世界握手的合約、其中金流是最硬的斷點 — 它在本章盤點順序排最後、執行確認要排最早、答案會改變整場遷移的形狀。一次性收款的遷移成本低 — 換金流串接、新訂單走新管道。訂閱制是另一回事：扣款授權 token 存在金流商的 vault 裡、且常綁定在平台名下的金流帳戶上。遷出時先問金流商「授權能否轉移到商家自有的金流帳戶」— 部分金流商支援處理商之間的卡號資料轉移、談得下來就是一次後台作業；談不下來、全體訂閱者重新授權、流失率直接換算成訂閱營收缺口。執行手段跟重設密碼同構：分批通知、寬限期、必要時用優惠補償授權摩擦。</p>
<p>金流之外、整合線還包括：平台外掛 / app 生態承擔的業務邏輯（Shopify app 做的折扣規則、WordPress 外掛做的預約流程）要逐個盤點、決定重寫進自建系統、換成獨立 SaaS、或趁機放棄；對外 webhook（ERP、出貨、會計系統）的端點切換要跟對方排時程；行銷 email 的寄送信譽綁在平台的寄件網域上、換到自有寄件網域要重建 SPF / DKIM 並逐步暖機、避免遷移週的通知信全進垃圾箱。</p>
<h2 id="並行期設計">並行期設計</h2>
<p><a href="/blog/backend/knowledge-cards/parallel-run/" data-link-title="並行期（Parallel Run）" data-link-desc="說明舊系統維持 source of truth、新系統以單向同步加唯讀驗證運轉的遷移共存階段、與雙寫的寫入路徑控制權差異">並行期</a>是舊平台與新系統共存、用真實資料驗證新系統的階段 — 前提是目標系統已依 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零的選型順序</a> 建置完成、本章不重複選型推導。它跟 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a> 的雙寫期同源但形狀不同：服務拆分時、寫入路徑在自己的程式碼裡、可以實作 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a>；託管平台的寫入發生在平台內部 — 顧客在 Shopify 結帳、會員在平台註冊 — 自建程式碼插不進那條寫入路徑。所以並行期的形態是「平台維持 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、單向同步、新系統唯讀驗證」：</p>
<ol>
<li>增量同步管道（webhook / API 輪詢 / 排程匯出）持續把平台變更餵進新系統</li>
<li>新系統以唯讀 replica 的角色運轉、對帳 job 定期比對兩邊的訂單數、會員數、金額總和</li>
<li>內部使用者先在新系統上工作（報表、後台查詢）、用真實業務流量驗證資料轉換的正確性</li>
<li>差異率收斂並穩定後、才排 cutover 日</li>
</ol>
<p>Cutover 本身是一段 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>、不是一個按鈕：選低流量時段、短暫凍結平台側變更（電商常用「暫停結帳維護頁」幾十分鐘）、跑最後一輪增量同步、切 DNS、然後密集觀察訂單成功率、登入成功率、金流授權成功率 — 觀察清單來自資產線盤點、每條線各有自己的健康訊號。</p>
<p>回切窗口的設計決定這場遷移的失敗代價。cutover 後保留舊平台訂閱與設定、回切動作是 DNS 切回；代價是新系統在窗口內產生的交易要補回平台 — 平台側通常沒有批次匯入訂單的好路徑、補回多半是手動作業、所以回切窗口內要刻意壓低不可逆變更的累積速度（例如窗口前 48 小時內暫停大型行銷活動）。這跟 10.2 寫路徑切換的 point of no return 是同一個判讀：回退成本隨時間墊高、go/no-go 要當成有明確時點的決策執行、判定條件在進入窗口前排定。</p>
<p>關舊站走降級、而不是直接刪除。觀察期過後、平台帳號先降到最低方案、店面關閉但後台保留 — 退款處理、客服查歷史訂單、會計與稅務稽核都還會用到平台側資料。刪除帳號前的檢查條件：所有歷史資料已完整落地自有儲存並驗證可讀、法規要求的交易紀錄保存年限已由自有系統接手、最後一筆平台側退款 / 爭議單已結案。</p>
<h2 id="部分遷出是常見的中繼形態">部分遷出是常見的中繼形態</h2>
<p>資產線可以獨立 cutover 的另一面、是遷出可以分期：先撤其中幾條線、其餘留在平台。部分遷出是把遷移風險拆期攤還的標準形態、結構上同 <a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig</a>：新系統從旁長出、逐線取代、平台最後才退役。</p>
<p>常見的中繼形態有四種。資料層先撤：增量同步管道建好之後、自有資料庫先成為報表與分析的 source、前台與結帳留在平台 — 0.21 BaaS 段描述的跨集合報表困境、在這個形態下已經解掉、而最高風險的金流與流量線還沒動。前台先撤（headless）：自建前端體驗層、平台降級為後端引擎（結帳 API、內容 API）— 流量線與 SEO 控制權先回手、金流與資料留在平台的成熟路徑上。身分後撤：認證是使用者感知最強的線、Firebase Auth 這類可攜性好的元件常被留到最後 — 資料與流量都搬完、產品穩定後、再做密碼雜湊匯入或重設遷移。金流後撤（或長期留平台）：訂閱授權轉移談不下來時、資料、前台與流量都遷出、訂閱扣款續走平台帳戶 — 它跟前三種不同、可能由中繼轉成長期形態、去留判讀回整合線的金流斷點確認。</p>
<p>中繼形態的判讀標準是「每個階段結束時、撤出的那條線已經完整脫離平台、由新系統持有唯一事實」。模糊狀態（一半訂單在平台、一半在自建、靠人腦記得哪邊查）是部分遷出最常見的事故源 — 每條線在任一時刻都要有唯一的 source of truth。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>盤點時發現業務邏輯只存在平台 UI 設定裡</td>
          <td>0.21 可遷出保險「業務邏輯文件化」缺項</td>
          <td>先文件化再動手、規則重建期計入時程</td>
      </tr>
      <tr>
          <td>並行期對帳差異率不收斂</td>
          <td>資料轉換有 gap、或增量同步管道漏事件</td>
          <td>暫停 cutover 排程、audit 轉換管線與 webhook</td>
      </tr>
      <tr>
          <td>金流商拒絕授權轉移</td>
          <td>訂閱線變成全體重新授權、流失進入營收預估</td>
          <td>重算遷移 ROI、評估訂閱線單獨延後</td>
      </tr>
      <tr>
          <td>Cutover 後自然流量持續下滑超過觀察期預估</td>
          <td>轉址表缺漏、或索引替換異常</td>
          <td>比對搜尋主控台的 404 清單、補轉址規則</td>
      </tr>
      <tr>
          <td>回切窗口內手動補單量超出客服消化能力</td>
          <td>不可逆變更累積速度超過回切設計</td>
          <td>縮短決策週期、提前 go/no-go 判定</td>
      </tr>
      <tr>
          <td>並行期超過原定窗口仍未排 cutover</td>
          <td>並行不是穩態、雙系統維運與平台月費在吃遷移 ROI</td>
          <td>重訂 cutover 條件、或承認部分遷出為長期形態</td>
      </tr>
      <tr>
          <td>新舊系統各管一部分同類資料超過一個階段</td>
          <td>部分遷出停在模糊狀態、source of truth 分裂</td>
          <td>強制收斂該資產線、明確指定唯一 source of truth</td>
      </tr>
  </tbody>
</table>
<p>業務邏輯那一列值得展開：平台設定裡長出來的折扣邏輯、會員等級、運費規則、是盤點階段最容易漏的資產 — 它們沒有檔案形態、不會出現在任何匯出工具裡。0.21 可遷出保險清單把「業務邏輯文件化」列為進場保險、沒買這項保險的遷移、第一個階段是考古：對著平台後台逐頁截圖、把規則寫成文件、再評估哪些重寫、哪些放棄。</p>
<p>金流那一列是整場遷移裡少數「工程努力無法繞過」的斷點 — 授權轉移的決定權在金流商與平台的合約上、不在工程團隊手上。所以它在盤點階段就要最先確認：答案直接改變遷移的營收影響模型、甚至可能讓「訂閱線留在平台、其餘遷出」成為長期形態。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「託管形態 → 自建 / 半託管」的遷出執行。當問題回到「該不該遷、何時該重新評估」、回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的升級自建 tripwire 表；遷移目標的自建選型（資料庫、部署、金流接法）走 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零的選型順序</a>；自建系統之間的資料庫搬遷技術細節（雙寫、shadow read、切換）進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>；服務層的漸進替換紀律進 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要回頭確認遷移時機與保險、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。要看並行期同源的雙寫與切流紀律、見 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a>。遷入自建後的第一站、從 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖</a> 開始走選型順序。</p>
]]></content:encoded></item><item><title>Datadog Security</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/</guid><description>&lt;p>Datadog Security 是 Datadog observability platform 上的 security 套件、跟 Datadog logs / metrics / APM / infrastructure 共用同一個 control plane 與 data plane。它的設計起點不是 SIEM、是 &lt;em>把資安訊號當成 observability 的一個維度&lt;/em>：alert 不只看 log、可以同時 pivot 到 APM trace、infra metrics 與 host context。這個定位決定了它的優勢（cloud-native + 混合 incident 偵測）與限制（SaaS-only + 計費隨 host 量線性漲、不適合 on-prem-heavy 或預算敏感場景）。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Datadog Security 由四個 product 構成、共用 Datadog Agent 與 backend：&lt;em>Cloud SIEM&lt;/em>（log-based detection、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk Enterprise Security&lt;/a> 同類）、&lt;em>Cloud Security Management (CSM)&lt;/em> — 涵蓋 &lt;em>CSPM&lt;/em>（cloud config posture）與 &lt;em>Cloud Workload Security (CWS)&lt;/em>（container / Linux runtime via eBPF）、&lt;em>App and API Protection (AAP、前 ASM)&lt;/em> — RASP-style 在 app runtime 收 attack signal、&lt;em>Sensitive Data Scanner&lt;/em> — scan log 中的 PII / credential 並 redact。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> 比、Datadog 走 &lt;em>observability-first + security 是 view&lt;/em>、Splunk 是 &lt;em>security-first&lt;/em>。Splunk 在 enterprise SOC tooling 深度（SOAR playbook、RBA、CIM data model）與跨 on-prem 部署上更成熟、Datadog SaaS-only 但跟 APM / Infra 同 plane、混合 incident（latency 異常是攻擊還是容量？）的判讀路徑更短。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a> 比、Elastic 可跨 on-prem + OSS、Datadog 只給 SaaS；Elastic 要自己整合 observability 訊號、Datadog 出廠就有。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a> 比、Google 走 &lt;em>fixed-price by data、PB-scale 划算&lt;/em>、Datadog 隨 host 線性漲、中等規模友善但破千 host 後 cost 曲線變陡。&lt;/p></description><content:encoded><![CDATA[<p>Datadog Security 是 Datadog observability platform 上的 security 套件、跟 Datadog logs / metrics / APM / infrastructure 共用同一個 control plane 與 data plane。它的設計起點不是 SIEM、是 <em>把資安訊號當成 observability 的一個維度</em>：alert 不只看 log、可以同時 pivot 到 APM trace、infra metrics 與 host context。這個定位決定了它的優勢（cloud-native + 混合 incident 偵測）與限制（SaaS-only + 計費隨 host 量線性漲、不適合 on-prem-heavy 或預算敏感場景）。</p>
<h2 id="服務定位">服務定位</h2>
<p>Datadog Security 由四個 product 構成、共用 Datadog Agent 與 backend：<em>Cloud SIEM</em>（log-based detection、跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk Enterprise Security</a> 同類）、<em>Cloud Security Management (CSM)</em> — 涵蓋 <em>CSPM</em>（cloud config posture）與 <em>Cloud Workload Security (CWS)</em>（container / Linux runtime via eBPF）、<em>App and API Protection (AAP、前 ASM)</em> — RASP-style 在 app runtime 收 attack signal、<em>Sensitive Data Scanner</em> — scan log 中的 PII / credential 並 redact。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 比、Datadog 走 <em>observability-first + security 是 view</em>、Splunk 是 <em>security-first</em>。Splunk 在 enterprise SOC tooling 深度（SOAR playbook、RBA、CIM data model）與跨 on-prem 部署上更成熟、Datadog SaaS-only 但跟 APM / Infra 同 plane、混合 incident（latency 異常是攻擊還是容量？）的判讀路徑更短。跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> 比、Elastic 可跨 on-prem + OSS、Datadog 只給 SaaS；Elastic 要自己整合 observability 訊號、Datadog 出廠就有。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 比、Google 走 <em>fixed-price by data、PB-scale 划算</em>、Datadog 隨 host 線性漲、中等規模友善但破千 host 後 cost 曲線變陡。</p>
<p>關鍵張力：<em>observability 與 security 同 plane</em> 是 Datadog 最大賣點、也是 cost 風險來源。host count 跟 events/month 同時是 observability 跟 security 的計費基準、security 加上去後 bill 不會獨立 — 預算要從 <em>整個 Datadog 帳單</em> 看、不是 security 單列。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Datadog Security 在 SOC stack 中承擔哪一段（log SIEM / CSPM / 容器 runtime / WAF-runtime / log DLP）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> IdP log、edge WAF）</li>
<li>observability + security 同 plane 的優勢何時成立、何時是 vendor lock-in 風險</li>
<li>Cloud SIEM 計費（events/month + indexed）跟 Standard / Flex Logs retention tier 的成本治理</li>
<li>何時用 Datadog、何時走 Splunk / Elastic / Google Security Ops 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Datadog Security 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>Datadog Agent coverage</strong>：agent 是否裝在所有 host / container / serverless wrapper、log forwarder 是否覆蓋 cloud control plane（AWS CloudTrail / GCP Audit Log / Azure Activity Log）、IdP（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>）audit log 是否進來 — 缺一個就是 detection 盲點</li>
<li><strong>Detection rule ownership</strong>：Cloud SIEM rule 是用內建還是 custom、custom rule 是否走 Git 版控（Terraform <code>datadog_security_monitoring_rule</code>）、staging 環境是否 dry-run 24-48hr 才 promote production</li>
<li><strong>CSPM compliance check 治理</strong>：CIS / NIST / PCI baseline 開哪些、findings 是否進 ticket workflow、misconfig 修復 SLA 有沒有定義（critical 24hr、high 7d、medium 30d）</li>
<li><strong>Events/month + Indexed Log 預算</strong>：Cloud SIEM 按 events/month + indexed event 計費、新加 source 前是否估算 ingestion impact、Standard / Flex Logs retention tier 是否依 log priority 分流</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Datadog Agent 採集</strong>：log / metrics / trace / security event 走同一個 Agent、用 integration（150+）抓 cloud / SaaS / database / queue。security event 跟 observability event 在後端用 <em>attribute tag</em>（<code>env</code>、<code>service</code>、<code>host</code>、<code>trace_id</code>）關聯、查 incident 時可以從 log alert pivot 到同 trace_id 的 APM trace 看 attack 發生的 application context。</p>
<p><strong>Cloud SIEM detection rule</strong>：rule 形式類似 SPL 的 query — <code>source:okta @evt.name:user.authentication.auth_via_mfa @outcome:failure</code> 加 <em>signal aggregation</em>（rolling window count、new value、anomaly detection、impossible travel）。內建 rule 跟 MITRE ATT&amp;CK 對應、跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk Security Content</a> 同類但 rule 數量較少；custom rule 走 Terraform provider 進版控、不在 UI 直改 production。</p>
<p><strong>CSPM compliance check</strong>：scan AWS / GCP / Azure 配置 vs CIS / NIST 800-53 / PCI / SOC 2 baseline、發現 misconfig（public S3 bucket、overly permissive IAM、不安全 SG rule）。跟 Wiz / Prisma Cloud 同類但跟 Datadog Infra 同 dashboard、findings 可以直接看到 affected resource 的 metrics / log。優勢是 <em>資安發現可以直接看業務影響</em>、限制是 graph-based attack path（Wiz 強項）不及專業 CNAPP。</p>
<p><strong>Cloud Workload Security（CWS）</strong>：用 Linux eBPF probe 在 kernel 層觀察 container / process behavior、偵測 cryptominer / privilege escalation / 異常 syscall / file integrity 變動。跟 <a href="https://falco.org/">Falco</a> 同類但跟 Datadog Infra 同 plane、CWS alert 可以直接 pivot 到該 container 的 CPU / memory / trace。Linux eBPF 對 kernel 版本敏感、舊 kernel 部份功能不可用、production 前要確認 fleet kernel matrix。</p>
<p><strong>App and API Protection（AAP）</strong>：RASP-style protection、Datadog APM library 在 application runtime 收 attack signal（SQLi / XSS / SSRF / 異常 traffic pattern）。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> 不同層 — WAF 在 edge / CDN、AAP 在 app runtime 看到的是真實 request handler / DB query。兩者互補不互斥：edge WAF 擋 volumetric attack 跟已知 pattern、AAP 補 app-specific business logic abuse。</p>
<p><strong>Sensitive Data Scanner</strong>：scan ingest 進來的 log、用內建或 custom pattern 偵測 PII / credential / payment card / API key、發現後可以 redact、quarantine 或 alert。是 <em>DLP-lite</em> — 比不上 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 的 sensitive data discovery / classification / lineage 全套、但對 <em>log 中誤洩 secret</em> 的場景夠用、是 detection signal source 也是 DLP 補位。</p>
<p><strong>Notebooks + Workflow Automation</strong>：Notebooks 是 incident investigation 用的 query workbook、混 log query + metric chart + APM trace + 註記、跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk Search</a> 比較像 Jupyter notebook 的 SOC 版。Workflow Automation 是輕量 SOAR、接 PagerDuty / Slack / Jira / Webhook / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> API、playbook 走 visual builder + Python。SOAR 深度不到 Splunk SOAR、但對中等規模 SOC（10-50 人）的常見 response 動作（rotate credential / block IP / open ticket）夠用。</p>
<p><strong>Standard Logs / Flex Logs + retention tier</strong>：log 進 Datadog 後分 <em>Indexed</em>（hot、可全文搜尋、貴）、<em>Flex Logs</em>（warm、retention 長、查詢延遲較高、cost 1/3-1/5）、<em>Archive</em>（cold、丟 S3 / GCS、純儲存）三層。Cloud SIEM detection 跑在 indexed log 上、所以 <em>哪些 log 走 indexed</em> 直接決定 detection coverage 跟 bill。tier 1 source（IdP / cloud control plane / payment）必 indexed、tier 2 source（app log）按 sampling、tier 3（debug）走 Flex 或 Archive。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Datadog Security</th>
          <th>Splunk</th>
          <th>Elastic Security</th>
          <th>Google Security Operations</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計起點</td>
          <td>Observability + security 同 plane</td>
          <td>Security-first、log 統一查詢平台</td>
          <td>Search-first、ELK stack 延伸</td>
          <td>Massive scale ingestion、Google threat intel</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>Per-host + per-event（events/month）</td>
          <td>Ingestion-based（GB/day、累進）</td>
          <td>Resource-based（node / cluster）</td>
          <td>Fixed price by data tier（PB-scale 划算）</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>SaaS only</td>
          <td>Self-hosted / SaaS</td>
          <td>Self-hosted / Cloud / Serverless</td>
          <td>SaaS only（Google Cloud）</td>
      </tr>
      <tr>
          <td>觀測整合</td>
          <td>Native — log + APM + metrics + infra 同 query</td>
          <td>需自接（Splunk Observability 另收）</td>
          <td>需自接（Elastic Observability 另開）</td>
          <td>弱 — 跨產品 federation</td>
      </tr>
      <tr>
          <td>雲端 posture (CSPM)</td>
          <td>內建（CSM）</td>
          <td>第三方 add-on / Cisco 整合</td>
          <td>第三方 / Wazuh</td>
          <td>第三方 / Mandiant 整合</td>
      </tr>
      <tr>
          <td>容器 runtime</td>
          <td>內建 CWS（eBPF）</td>
          <td>需 Falco / 第三方</td>
          <td>Elastic Defend</td>
          <td>需 Falco / 第三方</td>
      </tr>
      <tr>
          <td>App runtime（RASP）</td>
          <td>內建 AAP</td>
          <td>需第三方</td>
          <td>第三方</td>
          <td>第三方</td>
      </tr>
      <tr>
          <td>SOAR / Response</td>
          <td>Workflow Automation（輕量）</td>
          <td>Splunk SOAR（業界先驅）</td>
          <td>Cases + Endpoint response</td>
          <td>SOAR 內建（前 Siemplify）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Cloud-native + 已用 Datadog + 中等規模 SOC</td>
          <td>Enterprise + 跨 on-prem、預算允許</td>
          <td>OSS-friendly、Elastic stack 已用</td>
          <td>超大規模 ingestion、Google 雲</td>
      </tr>
  </tbody>
</table>
<p>選 Datadog 的核心訴求：<em>已經用 Datadog observability、cloud-native 為主、SOC 規模中等（10-50 人）、需要 observability + security 同 plane 的 incident 判讀路徑</em>。on-prem 為主、預算敏感（host 量 1000+）、需要 enterprise SOAR / RBA 深度、走 Splunk；OSS-friendly、跨 on-prem、走 Elastic。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Cross-product correlation（log + APM + metrics 同 trace_id）</strong>：Datadog 最特別的偵測形狀 — security alert 不只 log line、而是綁 trace_id 的 <em>integrated incident view</em>。例如 API endpoint 出現 SQLi 嘗試、Cloud SIEM 開 signal、同時 APM 看到該 request 的 DB query 跟 latency、infra 看到該 host 的 CPU。對「query latency 異常是不是被攻擊」這種混合 incident 偵測有結構性優勢、跟 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a> 的調查路徑直接對應。</p>
<p><strong>CWS Linux eBPF 行為偵測</strong>：eBPF probe 在 kernel 層、不需要 kernel module、不影響 process performance（&lt; 1% overhead）。可以偵測的行為包括 file integrity（<code>/etc/passwd</code> 被改）、process tree（<code>bash → curl → /tmp/payload</code> 異常 chain）、network connection（容器對外連 cryptominer pool）、syscall pattern（<code>ptrace</code> 用於 process injection）。跟 <a href="https://falco.org/">Falco</a> 同樣用 eBPF、差別是 Datadog CWS 不需要單獨部署 + 跟 Datadog 其他 signal 同 plane。</p>
<p><strong>Datadog Threat Intelligence</strong>：內建 threat feed（malicious IP / domain / file hash）、自動標記 log / network event 命中 IoC。可以加自家 STIX/TAXII feed、不過深度比不上 <a href="https://www.mandiant.com/">Mandiant</a> / Recorded Future / 專業 TI platform；中等規模 SOC 夠用、嚴重 APT 對抗場景要外接專業 TI。</p>
<p><strong>跟 Datadog Incident Management 整合</strong>：security signal 可以直接開 Datadog Incident（內建 incident channel + timeline + post-mortem template）、跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 同類但跟 observability 同 plane。對 <em>資安事件升級成全公司 incident</em> 的場景（<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 Operations Impact</a> 那種規模）可以共用 incident commander 視角、不用兩套 timeline 拼起來。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Cloud SIEM 偵測 lag / 沒 alert</strong>：events 沒進 indexed log（走了 Flex）、retention tier 設錯 — 檢查 log pipeline rule 是否把 security-critical source 標 indexed</li>
<li><strong>Events/month 暴衝</strong>：debug log / verbose log 進 Cloud SIEM index、CWS event 量爆 — log pipeline 前置 filter（Datadog Observability Pipeline 或 Cribl）、CWS rule 收斂 noisy 行為</li>
<li><strong>CSPM findings 100+ 沒人修</strong>：findings 沒進 ticket workflow、沒分 priority — 整合 Jira / ServiceNow、severity 對應 SLA、findings 老化超 30 天升級</li>
<li><strong>CWS 在舊 kernel host 沒資料</strong>：eBPF feature 對 kernel 版本敏感（&lt; 4.18 部份功能不支援）— 升級 kernel 或標記該 host 為 CWS-incompatible、補位用 host-based agent</li>
<li><strong>AAP false positive 卡 user</strong>：RASP 在 app runtime 直接 block、誤殺正常 request — AAP 先走 monitor mode 1-2 週收 baseline、tune 後再轉 protect mode</li>
<li><strong>Sensitive Data Scanner miss PII</strong>：custom pattern 沒寫對、log format 嵌套（JSON 內又是 JSON）— 用 sample log 跑 dry-run、scanner 跑在 ingest 階段不是 retroactive</li>
<li><strong>Workflow Automation playbook 黑箱</strong>：自動 rotate credential 結果誤殺 prod service account — playbook high-impact action 走 approval gate、default 走 containment 不走 deletion</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise + 跨 on-prem、預算允許</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a></td>
      </tr>
      <tr>
          <td>OSS-friendly / Elastic stack 已用</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>超大規模 ingestion + Google 雲</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>嚴格 DLP / 資料分類</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Cloud posture graph / attack path</td>
          <td>Wiz / Prisma Cloud / Lacework</td>
      </tr>
      <tr>
          <td>Edge WAF / volumetric attack</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></td>
      </tr>
      <tr>
          <td>Endpoint EDR</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Datadog Agent 完整 configuration reference、custom check 撰寫</li>
<li>Datadog observability（APM / RUM / Synthetics / DBM）細節 — 屬 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a> 模組</li>
<li>Cloud SIEM rule 完整語法 reference</li>
<li>CWS eBPF probe 撰寫（custom rule via Agent Expression Language）細節</li>
<li>Datadog Incident Management workflow（屬 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 IR</a> 模組）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Datadog Security 在 07 案例庫沒有直接 vendor-level 事件、但 observability + security 同 plane 的偵測形狀讓部份案例的調查路徑變短、值得對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Datadog Security 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>Query volume + 連接數 + CPU 負載異常是 Datadog 同 plane 的強項、Cloud SIEM rule + DBM metrics 同 query 不用 SIEM + 監控工具拼接</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 Operations Impact</a></td>
          <td>業務中樞事件的影響評估、APM + Infra 可秒級判斷 latency 異常源自資安 vs 容量、Datadog Incident 共用 IC 視角</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>APM span correlation 可看到單一 operator 短時間跨多 tenant access 的 trace pattern、log-only SIEM 看不到 application-level tenant 切換</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>Cloud SIEM detection rule 配 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> MFA log + APM error rate correlation、不靠單一 log source</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance (section)</a></td>
          <td>Standard / Flex Logs + retention tier 是 detection coverage 治理的工具、tier 1 source 必 indexed、tier 2 / 3 走 Flex / Archive</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>、<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP signal 進 Datadog）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP log source）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Workflow Automation 拉 API）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a>（edge WAF log 進 Cloud SIEM、AAP 在 app 層補位）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（同 Agent / 同 plane）、<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Datadog Incident → IR routing）</li>
<li>官方：<a href="https://docs.datadoghq.com/security/">Datadog Security Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Fastly Next-Gen WAF</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/</guid><description>&lt;p>Fastly Next-Gen WAF（NG-WAF）的核心定位是 &lt;em>用語意分析 + behavioral detection 取代 regex signature&lt;/em> 的 web application firewall。它前身是 2020 年被 Fastly 收購的 Signal Sciences、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &amp;#43; Managed Rule Group &amp;#43; Rate-based Rule、Shield Standard 內含">AWS WAF&lt;/a> 的根本差異不在覆蓋面、在 &lt;em>偵測 mindset&lt;/em> — 不靠 pattern 比對、靠解析請求語意（這段內容像不像 SQL、像不像 shell command）跟跨請求行為模式（同一 token 在多 endpoint 連續觸發異常）下判斷。產出是 &lt;em>低 false positive 的 inline block 模式可以直接上 production&lt;/em>、不需要先養 Log Mode 兩週、不需要 SOC 全職人員跟 rule 戰。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Fastly NG-WAF 設計的第一順位是 &lt;em>production 可直接走 Block 模式&lt;/em>。Signature WAF 的成本不在 rule 本身、在 false positive — 一條 SQLi pattern 可能誤判合法 SQL-like 字串（搜尋查詢、CSV 上傳）、production 開 Block 立刻炸合法流量、所以多數 signature WAF 跑在 &lt;em>Detect / Log Only&lt;/em> 模式、攔不下真正攻擊。Fastly NG-WAF 走 &lt;em>Signal&lt;/em> 模型：每個請求被解析後標記若干 Signal（SQLi、XSS、CMDI、Traversal、Anomaly 等）、再依 &lt;em>threshold-based rule&lt;/em>（N 個 Signal 在 M 秒內聚集）才動作 — false positive 自然降低、Block 模式可開。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> 的對照：Cloudflare 走 signature + managed rule + ML 三層、覆蓋廣但需要 sensitivity tuning；Fastly NG-WAF 預設低 FP 但需要 &lt;em>客戶自己定義業務語意&lt;/em>（哪些 path 是 admin、哪些 header 不該出現、哪些 anomaly 對自家業務代表攻擊）— 用 &lt;em>Tag&lt;/em> + &lt;em>Match Conditions&lt;/em> 表達。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &amp;#43; Managed Rule Group &amp;#43; Rate-based Rule、Shield Standard 內含">AWS WAF&lt;/a> 的對照：AWS WAF 跟 ALB / CloudFront / API Gateway 整合深、跨雲弱；Fastly NG-WAF 部署模型多樣（Edge / Agent / Cloud）、跨 AWS / GCP / on-prem / K8s 一致。&lt;/p></description><content:encoded><![CDATA[<p>Fastly Next-Gen WAF（NG-WAF）的核心定位是 <em>用語意分析 + behavioral detection 取代 regex signature</em> 的 web application firewall。它前身是 2020 年被 Fastly 收購的 Signal Sciences、跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> 的根本差異不在覆蓋面、在 <em>偵測 mindset</em> — 不靠 pattern 比對、靠解析請求語意（這段內容像不像 SQL、像不像 shell command）跟跨請求行為模式（同一 token 在多 endpoint 連續觸發異常）下判斷。產出是 <em>低 false positive 的 inline block 模式可以直接上 production</em>、不需要先養 Log Mode 兩週、不需要 SOC 全職人員跟 rule 戰。</p>
<h2 id="服務定位">服務定位</h2>
<p>Fastly NG-WAF 設計的第一順位是 <em>production 可直接走 Block 模式</em>。Signature WAF 的成本不在 rule 本身、在 false positive — 一條 SQLi pattern 可能誤判合法 SQL-like 字串（搜尋查詢、CSV 上傳）、production 開 Block 立刻炸合法流量、所以多數 signature WAF 跑在 <em>Detect / Log Only</em> 模式、攔不下真正攻擊。Fastly NG-WAF 走 <em>Signal</em> 模型：每個請求被解析後標記若干 Signal（SQLi、XSS、CMDI、Traversal、Anomaly 等）、再依 <em>threshold-based rule</em>（N 個 Signal 在 M 秒內聚集）才動作 — false positive 自然降低、Block 模式可開。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 的對照：Cloudflare 走 signature + managed rule + ML 三層、覆蓋廣但需要 sensitivity tuning；Fastly NG-WAF 預設低 FP 但需要 <em>客戶自己定義業務語意</em>（哪些 path 是 admin、哪些 header 不該出現、哪些 anomaly 對自家業務代表攻擊）— 用 <em>Tag</em> + <em>Match Conditions</em> 表達。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> 的對照：AWS WAF 跟 ALB / CloudFront / API Gateway 整合深、跨雲弱；Fastly NG-WAF 部署模型多樣（Edge / Agent / Cloud）、跨 AWS / GCP / on-prem / K8s 一致。</p>
<p>關鍵張力：低 FP 的 <em>代價</em> 是要花時間理解自家業務語意。Signature WAF 是「裝上就有保護」、Fastly NG-WAF 是「裝上有 baseline、業務 anomaly 要自己標」。沒有人定義 Tag + Power Rules、就只用到產品 30% 能力。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Fastly NG-WAF 的 Signal / Tag / Rule / Mode 四個核心 first-class concept 各承擔什麼責任</li>
<li>Edge / Agent + Module / Cloud Proxy 三種部署模型的選擇條件</li>
<li>Account Takeover Protection、Bot Protection、API discovery 三個進階 module 的適用情境</li>
<li>何時用 Fastly NG-WAF、何時走 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Fastly NG-WAF 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>部署模型對齊架構</strong>：Fastly Edge inline（流量本來就過 Fastly CDN）/ Agent + Module（自管 Nginx / Apache / IIS / Envoy / .NET 加 sigsci-agent local process）/ Cloud Proxy（Fastly 接 origin proxy）三選一或混用、是否覆蓋所有入口（含 admin、internal API、staging）</li>
<li><strong>Signal 與 Tag 設計</strong>：預設 Signal（SQLi / XSS / CMDI / Traversal / Backdoor / Anomaly）是否全開、業務語意 Tag（admin-path、internal-only、payment-flow）是否定義並掛上 Match Conditions、Power Rules 是否組合多 Signal / Tag 走 threshold-based action</li>
<li><strong>Rule mode 與 threshold</strong>：Site-level 跟 Corp-level Rule 是 Block 還是 Off、threshold（連續幾個 Signal / 多久窗口）是否依 endpoint 業務調整、Template Rule（ATO、Bot）是否啟用</li>
<li><strong>Logging 與 sigsci-agent token 治理</strong>：Syslog / HTTP webhook / S3 / SIEM（Splunk / Datadog / Sumo Logic）整合是否 production-grade、sigsci-agent 連回控制面的 token 是否進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、跨環境 token 是否分離</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">Entry Point Protection</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>部署模型選擇</strong>：<em>Fastly Edge inline</em> 是最簡部署、流量已過 Fastly CDN 就 inline 加 NG-WAF、沒有額外 agent 要管；<em>Agent + Module</em> 是 self-managed Nginx / Apache / IIS / Envoy / HAProxy / .NET / Java（Tomcat）等加裝 sigsci-module（process 內 module 攔請求）+ sigsci-agent（本機 daemon、跟 Fastly 控制面 sync rule、collect event）— 適合 origin 不過 Fastly CDN、或 internal API；<em>Cloud Proxy</em> 是 Fastly 提供 reverse proxy 端點、客戶 DNS 指過去、origin 在後面 — 適合不想改 origin、又沒用 Fastly CDN。三種混用常見、大企業 edge 用 Fastly Edge、internal service 用 Agent + Module。</p>
<p><strong>Signal 是已知攻擊指標</strong>：Fastly NG-WAF 預定義 Signal 包含 <em>SQLi / XSS / CMDI（command injection）/ Traversal（路徑穿越）/ Backdoor / RCE / Anomaly</em> 等。Signal 是 <em>語意解析結果</em> — request body 被 parser 拆解（JSON / form / multipart）、每個欄位看「這像不像某類攻擊」、不是 regex 比對。意義是 <em>encoding 變化攔不住</em>（base64 / URL encode / Unicode normalize 都會被解開）、跟 signature WAF 的脆性對比明顯。</p>
<p><strong>Tag 是客戶自定 Signal</strong>：用 <em>Match Conditions</em>（path / method / IP / header / body content / query 參數）定義「什麼樣的請求叫某 tag」、例：<code>Path: /admin/* AND Source IP NOT IN internal_cidr → tag: admin-external-access</code>。Tag 之後可以走 Rule 處理（看到 admin-external-access 就 alert / block）。Tag 是 Fastly NG-WAF 表達 <em>業務語意</em> 的主要工具、不是用來補強 Signal。</p>
<p><strong>Rule 三層</strong>：<em>Site-level Rule</em>（單一 site / property）/ <em>Corp-level Rule</em>（整個 organization 共用、用於 corp-wide block list、跨 BU 統一 policy）/ <em>Template Rule</em>（Fastly 提供的預設複合 rule、如 ATO template、Bot template）。Rule 表達式組合 Signal / Tag / Source IP / Path / Method、走 Block / Off。Power Rules 是進階版 — 支援 <em>threshold</em> + <em>時間窗口</em> + <em>多條件 AND/OR</em>、例：「同 IP 在 60 秒內觸發 5 個 SQLi Signal 就 Block 10 分鐘」。</p>
<p><strong>Mode 兩種</strong>：<em>Block</em>（攔截、回 406 / 自訂 status）/ <em>Off</em>（不動作、純 log）。沒有 Cloudflare 的 Sensitivity 滑桿 — 因為 Signal 本身已是語意判讀結果、不需要敏感度調整、調整在 <em>threshold</em>（多少 Signal 才動作）。</p>
<p><strong>Account Takeover Protection（ATO）</strong>：偵測 credential stuffing pattern — 同 IP 多 login fail、跨 IP 同 account 多 login、impossible travel、unusual UA。Fastly NG-WAF 內建 <em>login endpoint detection</em>（自動 / 手動標記 <code>/login</code>、<code>/auth/signin</code> 等）、配合 ATO Template Rule 直接 inline 處理（rate limit、challenge、block）。對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Boundary</a> 的 ATO 對策、但是在 WAF 層直接攔、不等 IdP 內 ATO 邏輯。</p>
<p><strong>Bot Protection</strong>：跟 Cloudflare Bot Management 同類、走 behavioral + browser fingerprint + JS challenge、區分 verified bot / likely bot / human。比 user-agent 過濾穩、headless browser 攔得住。</p>
<p><strong>API discovery</strong>：Fastly NG-WAF 自動學習 site 的 API endpoint 與 schema、偵測 <em>schema drift</em>（突然出現的多餘欄位、缺欄位、type mismatch）— 比手動維護 OpenAPI schema 輕量、適合內部 API 多但沒寫完整 OpenAPI 的團隊。</p>
<p><strong>Logging 與 sigsci-agent 治理</strong>：所有 event 走 Fastly NG-WAF 控制面 + 客戶端 Syslog / HTTP webhook / S3 / SIEM（Splunk / Datadog / Sumo Logic）。sigsci-agent 連回控制面用 <em>Site API key</em> — 該 key 進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、跨環境 prod / staging 分離、rotation 走標準 secret rotation 流程、不能寫死在 agent 配置檔。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Fastly Next-Gen WAF</th>
          <th>Cloudflare WAF</th>
          <th>AWS WAF</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偵測模型</td>
          <td>Signal / 語意分析 / behavioral（低 FP）</td>
          <td>Signature + Managed Rule + ML</td>
          <td>Signature + Managed Rule + Lambda 自訂</td>
      </tr>
      <tr>
          <td>部署位置</td>
          <td>Fastly Edge / Agent + Module / Cloud Proxy</td>
          <td>Cloudflare global edge</td>
          <td>AWS region 內 ALB / CloudFront / API Gateway 前</td>
      </tr>
      <tr>
          <td>Block 模式可行性</td>
          <td>高 — 預設低 FP、production 可直開</td>
          <td>中 — 需 sensitivity tuning + Log Mode 觀察</td>
          <td>中 — managed rule FP 需排除、custom rule 自管</td>
      </tr>
      <tr>
          <td>業務語意表達</td>
          <td>Tag + Match Conditions + Power Rules（threshold）</td>
          <td>Custom Rule（Rules language）+ Bot Score</td>
          <td>JSON policy + Lambda 自訂</td>
      </tr>
      <tr>
          <td>自管伺服器支援</td>
          <td>強 — sigsci-agent + module 覆蓋 Nginx / Apache / IIS</td>
          <td>弱 — 必須流量過 Cloudflare edge</td>
          <td>弱 — 必須走 AWS service</td>
      </tr>
      <tr>
          <td>ATO 內建</td>
          <td>是 — Template Rule 直接 inline</td>
          <td>Exposed Credentials Check（部分覆蓋）</td>
          <td>AWS WAF Fraud Control（加價）</td>
      </tr>
      <tr>
          <td>Bot Protection</td>
          <td>內建（同層產品）</td>
          <td>加價 add-on（Pro / Business / Enterprise）</td>
          <td>AWS WAF Bot Control（加價）</td>
      </tr>
      <tr>
          <td>API discovery</td>
          <td>內建（auto schema learning）</td>
          <td>API Shield（Enterprise）</td>
          <td>API Gateway request validator</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — Signal / Tag mindset 要轉、agent 安裝要熟</td>
          <td>中 — UI 易上手、Rules language 表達力強</td>
          <td>較陡 — JSON policy + 多 AWS service 整合</td>
      </tr>
      <tr>
          <td>價格</td>
          <td>較高 — Enterprise tier 為主、按請求量計</td>
          <td>分層（Free / Pro / Business / Enterprise）</td>
          <td>按 rule + request 量、起步低</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>低 FP 要求、API 重、自管伺服器多、跨雲 / on-prem</td>
          <td>多雲 / on-prem origin、要整套 edge security suite</td>
          <td>AWS-heavy、ALB / CloudFront / API Gateway 是主入口</td>
      </tr>
  </tbody>
</table>
<p>選 Fastly NG-WAF 的核心訴求：<em>production 直接 Block</em> + <em>API / schema-rich 業務</em> + <em>自管伺服器需要 inline agent</em> + <em>跨雲 / on-prem mix</em>、且有預算支付 Enterprise tier。純 AWS-internal 簡單 web app 用 AWS WAF 整合更直接；要整套 edge security suite 用 Cloudflare。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>VCL + Edge custom rule</strong>：Fastly Edge 部署模式下、NG-WAF 跟 <a href="https://www.fastly.com/">Fastly CDN</a> 的 VCL（Varnish Configuration Language）共存、複雜邏輯可寫 VCL 在 NG-WAF 處理前後攔截 — 例：geo block 在 VCL 做、NG-WAF 處理通過的請求。Compute@Edge（Fastly 的 edge serverless、類 Cloudflare Workers）也可以接 NG-WAF 結果做進一步處理。代價是 VCL / Compute@Edge code 變另一條 ops trace、要有版控與 staging。</p>
<p><strong>ATO 進階 — credential stuffing 場景</strong>：login endpoint 接 ATO Template Rule 後、可進一步整合 <em>已洩漏 credential check</em>（類 Have I Been Pwned 整合）、failed login burst → progressive challenge（先 CAPTCHA、再 block）。對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Boundary</a> 的 IdP ATO 邏輯、Fastly 在 WAF 層攔的好處是 <em>攻擊不會打到 IdP</em>、減少 IdP 端 rate limit 壓力。</p>
<p><strong>Bot Protection 進階</strong>：browser fingerprint + behavioral pattern + JS challenge 三層、可掛 <em>bot score threshold</em> 在 Power Rules 內、配合 ATO 做 <em>high-risk login flow</em>（bot score 高 + login endpoint → 強 challenge）。</p>
<p><strong>Agent + Module 在 K8s / VM</strong>：K8s 場景 sigsci-agent 走 sidecar 或 DaemonSet、sigsci-module 在 ingress controller（Nginx Ingress Controller 加 sigsci-nginx module）；VM 場景 sigsci-agent 走 systemd service、module 隨 web server 啟動。跨環境 token 隔離（prod / staging / dev）走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> dynamic secret 或環境變數注入、不寫死配置檔。</p>
<p><strong>Corp-level Rule 共用</strong>：多 BU / 多產品線在同一 Corp（Fastly NG-WAF 的 organization 概念）下、Corp Rule 跨所有 Site 生效 — 適合表達「全公司禁 IP X」「全公司 ATO Template 都開」、避免每個 Site 重複配置。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Signal 沒觸發、攻擊穿過</strong>：Encoding 異常 / parser 沒解析該 content-type — 確認 Content-Type 正確、body 大小沒超過 sigsci-module 限制（預設 100KB）、Signal scope 是否包含該 endpoint</li>
<li><strong>Tag 沒掛上</strong>：Match Conditions 寫錯（path 大小寫、trailing slash、wildcard 語意）— 在 Fastly NG-WAF console 用 <em>Rule Evaluation</em> 工具測試 request 是否命中</li>
<li><strong>Block 模式誤殺</strong>：Power Rules threshold 太低、單一合法請求觸發多 Signal — 調 threshold 或加 Site Rule exception 排除特定 path / source</li>
<li><strong>sigsci-agent 跟控制面失聯</strong>：Site API key 過期 / firewall block out-bound / agent 版本太舊 — agent log 看 connection status、輪換 token 走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>、保持 agent 在 supported version range</li>
<li><strong>sigsci-module load 失敗</strong>：web server 啟動報 module 載入錯 — 確認 module 版本跟 web server major version 對齊（Nginx 1.20 對 sigsci-nginx 對應版本）</li>
<li><strong>ATO Template 沒攔到</strong>：login endpoint detection 沒標到自家 path — 手動在 console 標記 login endpoint 路徑</li>
<li><strong>Logging gap</strong>：Syslog / webhook 送失敗、SIEM 沒收到 — 確認 destination accept、TLS cert 沒過期、retry policy</li>
<li><strong>跨環境 token 漏氣</strong>：staging token 流到 prod、改 staging 影響 prod rule — Vault 環境分離、token 加標籤、定期 audit token usage</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only + ALB / CloudFront origin</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></td>
      </tr>
      <tr>
          <td>多雲 + 要整套 edge security suite</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a></td>
      </tr>
      <tr>
          <td>純 internal mTLS / east-west</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> + service mesh</td>
      </tr>
      <tr>
          <td>Cert lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>Bot management 為主要訴求、預算敏感</td>
          <td>Cloudflare Bot Management 入門 / AWS WAF Bot Control</td>
      </tr>
      <tr>
          <td>DDoS L3/L4 為主</td>
          <td>Cloudflare Magic Transit / AWS Shield Advanced</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Signal Sciences 收購前的 product line 演進細節</li>
<li>完整 Signal 清單與每個 Signal 的內部解析邏輯</li>
<li>VCL / Compute@Edge 完整語法 reference</li>
<li>Fastly CDN 本身的 caching / TLS / origin shielding 細節</li>
<li>Enterprise 合約細節、各國資料駐留選項</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Fastly NG-WAF 沒有直接 vendor-level 公開事件、案例庫對照引用以「behavioral detection 在 zero-day / supply chain 場景的 inline mitigation 角色」為主：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Fastly NG-WAF 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — Anomaly Signal 對 JNDI pattern 有 immediate inline detection、不需等 vendor signature 更新；但 exploitation 進後端後仍要靠 supply chain 治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — WAF 攔不住 edge appliance zero-day、需要「修補 + session 失效 + 異常清查」三同步、NG-WAF Power Rules 可在窗口期提供臨時 anomaly 偵測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet SSL-VPN CVE 2023-27997</a></td>
          <td>對照啟示 — vendor patch 前用 Power Rules + Tag 快速部署臨時 mitigation、收斂可達來源是修補窗口期的標準動作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></td>
          <td>Fastly NG-WAF 是 entry point protection 的工具、低 FP 設計讓 production Block 模式可行、跟 signature WAF 的部署成本曲線根本不同</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>（WAF block 不夠時、資料層也要遮罩）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（sigsci-agent Site API key 存放）、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（Fastly admin 走 SSO）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（WAF block 事件 routing 進 IR）</li>
<li>官方：<a href="https://docs.fastly.com/products/next-gen-waf">Fastly Next-Gen WAF Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Google Secret Manager</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/</guid><description>&lt;p>Google Secret Manager（GSM）是 GCP 原生的 &lt;em>static secret 集中保管&lt;/em> 服務、設計上刻意保持 &lt;em>簡單&lt;/em>：只負責 secret 儲存、版本管理、IAM 授權、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &amp;#43; Cloud HSM &amp;#43; External Key Manager">Cloud KMS&lt;/a> 整合的 envelope encryption。rotation orchestration、cross-region replication policy、dynamic credential issuing 都不在 GSM 自己做、留給上層用 Cloud Function / Cloud Run 自組。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a> 最大的差異是 &lt;em>沒有 built-in rotation Lambda&lt;/em> — rotation logic 要自己寫、GSM 只提供 &lt;em>Rotation Schedule + Pub/Sub event&lt;/em> 當觸發點。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>GSM 的定位是 &lt;em>GCP-native 的 secret 集中點&lt;/em>、解決三件事：把 secret 從 environment variable / Cloud Build substitution / GitHub secret 收回單一受控位置；用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> 的 &lt;em>role binding on secret resource&lt;/em> 控制誰能讀；走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Workload Identity Federation&lt;/a> 讓 GKE / Cloud Run / 外部 workload（GitHub Actions / AWS / Azure）安全取用、避免長期 service account key 散落。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> 比、GSM 沒有 dynamic credential engine、沒有 transit / PKI engine、沒有跨雲統一介面 — 但運維成本接近於零、跟 GCP IAM / KMS / Cloud Logging 的整合是 first-class。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a> 比、GSM 把 rotation orchestration 推給應用層、自由度高但代價是 &lt;em>rotation 流程要自己設計&lt;/em>；跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a> 比、兩者 mindset 相近（單雲、IAM-driven、CMEK 整合）、各自綁雲。&lt;/p></description><content:encoded><![CDATA[<p>Google Secret Manager（GSM）是 GCP 原生的 <em>static secret 集中保管</em> 服務、設計上刻意保持 <em>簡單</em>：只負責 secret 儲存、版本管理、IAM 授權、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a> 整合的 envelope encryption。rotation orchestration、cross-region replication policy、dynamic credential issuing 都不在 GSM 自己做、留給上層用 Cloud Function / Cloud Run 自組。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> 最大的差異是 <em>沒有 built-in rotation Lambda</em> — rotation logic 要自己寫、GSM 只提供 <em>Rotation Schedule + Pub/Sub event</em> 當觸發點。</p>
<h2 id="服務定位">服務定位</h2>
<p>GSM 的定位是 <em>GCP-native 的 secret 集中點</em>、解決三件事：把 secret 從 environment variable / Cloud Build substitution / GitHub secret 收回單一受控位置；用 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 的 <em>role binding on secret resource</em> 控制誰能讀；走 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Workload Identity Federation</a> 讓 GKE / Cloud Run / 外部 workload（GitHub Actions / AWS / Azure）安全取用、避免長期 service account key 散落。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 比、GSM 沒有 dynamic credential engine、沒有 transit / PKI engine、沒有跨雲統一介面 — 但運維成本接近於零、跟 GCP IAM / KMS / Cloud Logging 的整合是 first-class。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> 比、GSM 把 rotation orchestration 推給應用層、自由度高但代價是 <em>rotation 流程要自己設計</em>；跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 比、兩者 mindset 相近（單雲、IAM-driven、CMEK 整合）、各自綁雲。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 secret 適合 GSM（GCP-only、static、靠 IAM 授權即可）、哪些該走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 或其他雲端 native</li>
<li>GSM 最低安全設定（CMEK、Data Access audit、Workload Identity Federation、IAM Conditions）</li>
<li>自寫 rotation Cloud Function 時必須處理的 <em>版本切換窗口</em> 跟 <em>fallback 邏輯</em></li>
<li>何時 GSM 不夠用、要往 Vault / Berglas / Cloud HSM 走</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判讀一個 GSM deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能讀 secret</strong>：secret resource 上的 IAM binding 是不是用最小單位授權（per-secret、不是 project-level <code>roles/secretmanager.secretAccessor</code>）、有沒有上 IAM Conditions 限定時間 / IP / resource tag</li>
<li><strong>Key custody 分離</strong>：encryption key 是 Google-managed default key、還是 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a> CMEK？CMEK 的 key 持有 admin 跟 secret access admin 是不是分人</li>
<li><strong>取用路徑</strong>：workload 取 secret 是走 <em>service account key</em>（壞模式、長期憑證散落）還是 <em>Workload Identity Federation</em>（GKE WIF / 外部 OIDC token exchange）</li>
<li><strong>證據是否可回查</strong>：Admin Activity audit 預設開、Data Access audit（<code>AccessSecretVersion</code> 誰呼叫）預設 <em>關</em>、production 要手動 enable + 接 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Cloud Logging sink</a> 推到 SIEM</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>IAM Conditions 收 scope</strong>：GSM 的 secretAccessor role 預設綁到 secret resource、但組織常見錯配是給整個 project 上 <code>roles/secretmanager.secretAccessor</code> — 等於整個 project 所有 secret 都能讀。應該用 <em>per-secret binding</em>、再加 IAM Conditions（<code>resource.name.endsWith('prod-db-password')</code>、<code>request.time &lt; timestamp('...')</code>）限縮時間窗口。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta Cloudflare 2023 supply chain</a> 的對照啟示：第三方 token scope 過寬時、上游事件直接傳導下游、IAM Conditions 是收 scope 的工具。</p>
<p><strong>Secret Version + Alias 模型</strong>：每個 secret 有 monotonic version（v1、v2、v3…）、預設 alias <code>latest</code> 指向最新 enabled version。rotation 不是「更新現有 secret」、是 <em>建立新 version + 把舊 version disable</em>。應用端要支援 <em>讀新 version 失敗時 fallback 舊 version</em>、或在 rotation Cloud Function 內實作 <em>雙軌驗證窗口</em>（新版本上線後一段時間舊版還能讀、確認所有 consumer 切過去再 destroy 舊版）。沒這層設計、一次 rotation 就會打掉沒及時更新的 consumer。</p>
<p><strong>CMEK（Customer-Managed Encryption Key）</strong>：GSM 預設用 Google-managed key、production 應該指向 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a> CMEK。意義是 <em>把 key 持有跟 secret 取用分離</em> — 即使 secret admin 被攻破、沒有 CMEK 的 <code>decrypt</code> 權限拿不到明文。代價是 CMEK key region 跟 secret replication 要對齊（key 在 <code>us-central1</code> 但 secret 設 automatic replication = key 進不去其他 region、secret access 會失敗）。</p>
<p><strong>Replication 策略</strong>：automatic 是 GCP 自動跨 region replicate（高可用、不需要管 region 一致性、但 data residency 受 GCP 全球策略支配）；user-managed 是手動指定 region list（精細控制資料駐留、適合有 GDPR / 跨境合規需求的場景、但 region 加減要自己管 + CMEK key 要在每個指定 region 都存在）。一個常見錯配：選 user-managed 但只設一個 region — 等於沒有跨 region 冗餘、該 region 出事 secret 完全讀不到。</p>
<p><strong>Rotation 是自管 schedule</strong>：GSM 提供的不是 rotation logic、是 <em>Rotation Schedule</em>（cron 或固定間隔）、到期會發 <em>Pub/Sub message</em> 到指定 topic、由 <em>自己寫的 Cloud Function / Cloud Run</em> 訂閱該 topic 執行實際 rotation（呼叫上游系統 API 生新 credential、寫成新 secret version、disable 舊 version）。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a>：rotation Cloud Function 必須自己處理 <em>scope map</em>（哪些 consumer 用了同一把 secret）跟 <em>雙軌驗證窗口</em>（confirm 所有 consumer 切到新版本才 disable 舊版）、不像 AWS Secrets Manager 有 built-in 四階段 flow（<code>createSecret</code> → <code>setSecret</code> → <code>testSecret</code> → <code>finishSecret</code>）。</p>
<p><strong>Workload Identity Federation 取用</strong>：external workload（GitHub Actions / AWS workload / Azure workload / on-prem K8s）用 WIF 拿 GSM secret 是現代預設模式 — workload 用自己的 OIDC token（GitHub OIDC、AWS STS）跟 GCP STS 交換 short-lived access token、再用 token 呼叫 GSM。避開了「長期 service account JSON key 散落 CI / 第三方環境」的問題。GKE 內 workload 走 <em>GKE Workload Identity</em>（pod ServiceAccount → GCP service account 綁定）取 secret、也是同 mindset。</p>
<p><strong>Audit log 治理</strong>：GSM 的 audit 分兩層 — Admin Activity（create / delete / IAM 變更、預設開、免費）、Data Access（<code>AccessSecretVersion</code>、預設 <em>關</em>、開啟有 log 量跟 BigQuery export cost）。production 不開 Data Access = 事故時 <em>連 secret 被誰取過都查不到</em>、必須在 project IAM Audit Config 開、Cloud Logging sink 推到 SIEM 或 BigQuery（見 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google Secret Manager</th>
          <th>HashiCorp Vault</th>
          <th>AWS Secrets Manager</th>
          <th>Azure Key Vault</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>GCP managed</td>
          <td>自管 cluster（HA + replication）</td>
          <td>AWS managed</td>
          <td>Azure managed</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>弱 — 綁 GCP</td>
          <td>強 — 同一介面跨 AWS / GCP / Azure / on-prem</td>
          <td>弱 — 綁 AWS</td>
          <td>弱 — 綁 Azure</td>
      </tr>
      <tr>
          <td>Rotation 模型</td>
          <td>自寫 Cloud Function（Pub/Sub trigger）</td>
          <td>dynamic engine 自動 lease</td>
          <td>built-in Lambda 四階段 flow</td>
          <td>自寫 Function App（Event Grid trigger）</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>無（靠 IAM impersonation 替代）</td>
          <td>DB / cloud / SSH engine 完整</td>
          <td>RDS rotation 有、cloud STS 較弱</td>
          <td>較弱（依靠 Managed Identity）</td>
      </tr>
      <tr>
          <td>Encryption key</td>
          <td>Google-managed default / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a> CMEK</td>
          <td>自管 / KMS auto-unseal</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> CMK</td>
          <td>Azure Key Vault key</td>
      </tr>
      <tr>
          <td>External workload</td>
          <td>Workload Identity Federation（成熟）</td>
          <td>AppRole / Kubernetes / OIDC auth</td>
          <td>IAM Roles Anywhere（較新）</td>
          <td>Managed Identity / Workload Identity</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>低</td>
          <td>高 — HA、upgrade、replication 自己顧</td>
          <td>低</td>
          <td>低</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>GCP-heavy + WIF 已主導 + static secret 為主</td>
          <td>跨雲、dynamic credential、內部 PKI</td>
          <td>AWS-heavy + 需要 built-in rotation 收斂</td>
          <td>Azure-heavy + Managed Identity 已主導</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低</td>
          <td>中 — dynamic engine 接線多</td>
          <td>低</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 GSM 的核心訴求：workload 主要跑在 GCP（GKE / Cloud Run / Cloud Build）、已經用 Workload Identity Federation 收 service account key、secret 形態以 static 為主（DB password、third-party API key、private key）、rotation 邏輯願意用 Cloud Function 自寫。要跨雲、要 dynamic credential、要內建 rotation flow、需要 transit encryption — 走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>CMEK + Cloud KMS 雙軌權限分離</strong>：production 應該 <em>至少</em> 把 prod secret 的 CMEK key 跟 secret IAM 分到不同 admin group — secret admin 可以建 / 改 secret 但不能 decrypt（沒 KMS <code>cloudkms.cryptoKeyDecrypter</code>），KMS admin 可以管 key 但不能讀 secret 內容。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 signing key chain</a> 的對照啟示：key 不離 KMS 邊界、跟 HSM-bound 同 mindset；CMEK 是把這個原則內建到 secret 路徑。</p>
<p><strong>Berglas（OSS pattern）</strong>：<a href="https://github.com/GoogleCloudPlatform/berglas">Berglas</a> 是 Google 開源的 GSM client library + CLI、在 Cloud Run / Cloud Function / GKE 啟動時把 <code>sm://...</code> 參考自動 resolve 成實際 secret value、注進環境變數或檔案。比起應用端寫 SDK 取 secret 的好處：<em>secret 不進 container image / build manifest</em>、只有 runtime 取得；缺點是多一層 dependency、且 Berglas 自己有 IAM 需求要管。</p>
<p><strong>GKE Workload Identity 取用</strong>：GKE pod 用 ServiceAccount → IAM service account 綁定（透過 <code>iam.gke.io/gcp-service-account</code> annotation）、pod 內呼叫 GSM API 自動帶 GCP service account 身份、metadata server 簽 token。比起把 service account JSON key mount 進 pod、Workload Identity 沒有長期 credential 在 pod 內、credential rotation 由 GCP metadata 自動處理。</p>
<p><strong>Secret rotation Cloud Function 樣板</strong>：訂閱 secret 的 rotation topic（Pub/Sub）、message 帶 secret name 跟 trigger reason；Function 內呼叫上游系統 API（DB / SaaS）生新 credential、用 <code>secretmanager.AddSecretVersion</code> 寫新 version、等一段時間（雙軌驗證窗口）後 <code>DisableSecretVersion</code> 舊 version、最後 <code>DestroySecretVersion</code> 完成 rotation。<strong>雙軌窗口的長度必須大於 consumer 的最長 cache TTL</strong>、否則沒及時 refresh 的 consumer 會在 disable 後失敗。</p>
<p><strong>Pub/Sub event subscription（new in 2023+）</strong>：除了 rotation schedule 自動發 event、GSM 也支援對 secret 任意變更（new version、IAM change）發 Pub/Sub message、可接 SOAR / SIEM 做 <em>secret 異常變更告警</em>（例：非 CI service account 在週末新增 secret version）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>取 secret 拿到 PERMISSION_DENIED</strong>：通常是 IAM binding 在 project 層但 secret 在某 sub-resource、或 IAM Conditions 把當前 caller 排除 — 用 <code>gcloud secrets get-iam-policy</code> 直接看 binding、確認 condition 表達式</li>
<li><strong>CMEK 設定後突然讀不到 secret</strong>：CMEK key region 跟 secret replication region 不對齊、或 caller 沒有 KMS decrypt 權限 — 確認 key 在所有 replication region 都有版本、secret accessor service account 有 <code>cloudkms.cryptoKeyDecrypter</code></li>
<li><strong>Rotation Cloud Function 跑了但 consumer 認證失敗</strong>：雙軌窗口太短或 consumer 沒實作 <em>latest version 失敗 fallback</em>、舊版 disable 後孤兒 consumer 直接斷 — 把雙軌窗口拉到 cache TTL × 2、補 fallback 邏輯</li>
<li><strong>Data Access audit 沒紀錄</strong>：預設關、要在 project IAM Audit Config 明確開 <code>secretmanager.googleapis.com</code> 的 DATA_READ — 不開等於沒辦法回答「事故當下誰讀了 secret」</li>
<li><strong>External workload 拿不到 secret</strong>：Workload Identity Federation 的 provider attribute mapping 沒對齊（GitHub OIDC token 的 <code>repository</code> claim 沒被 map 到 attribute condition）— 走 <code>gcloud iam workload-identity-pools providers describe</code> 看 mapping、用 token introspection 驗實際 claim</li>
<li><strong>Secret version 累積過多</strong>：rotation 只 disable 不 destroy、版本無限長 — 加 lifecycle policy（手動 / Cloud Function 排程）destroy 超過 N 個版本以前的舊版</li>
<li><strong>GKE pod 用 Workload Identity 但拿不到 secret</strong>：通常是 GKE 沒 enable Workload Identity feature、或 <code>iam.gke.io/gcp-service-account</code> annotation 拼錯、或 GCP service account 沒給 K8s ServiceAccount <code>iam.workloadIdentityUser</code> — 三層都要對才能通</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲 secret 統一介面</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></td>
      </tr>
      <tr>
          <td>需要 dynamic database / cloud credential</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> dynamic engine</td>
      </tr>
      <tr>
          <td>需要 built-in 四階段 rotation flow</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>（若可遷 AWS）</td>
      </tr>
      <tr>
          <td>Encryption-as-a-service / 內部 PKI</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> transit / PKI engine</td>
      </tr>
      <tr>
          <td>FIPS 140-2 Level 3 HSM 需求</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud HSM</a>（KMS 後端可改 HSM）</td>
      </tr>
      <tr>
          <td>公開憑證 PKI</td>
          <td>Google Certificate Authority Service / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>K8s workload cert 自動化</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>Secret rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>GSM 完整 REST API 跟 <code>gcloud secrets</code> 詳盡子命令</li>
<li>Cloud KMS key lifecycle 跟 rotation 細節（看 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a> 章）</li>
<li>Workload Identity Federation 完整設定步驟（attribute mapping、condition expression、provider 設定看 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 章）</li>
<li>Berglas 完整 CLI 用法</li>
<li>Cloud Function / Cloud Run 部署細節</li>
<li>GCP Organization Policy 跟 secret 跨 project 共享的進階場景</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>GSM 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 GSM 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>GSM rotation 是自寫 Cloud Function、scope map 跟雙軌驗證窗口都要自己設計、不像 AWS Secrets Manager 有 built-in 四階段 flow — 設計時就要把 consumer scope 跟 cache TTL 算進 rotation 排程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>對照啟示 — GSM CMEK 把 encryption key 放 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a>、key 不離 KMS 邊界、跟 HSM-bound 同 mindset；secret admin 跟 KMS admin 分人是減 blast radius 的關鍵</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta Cloudflare 2023 Support Supply Chain (red-team)</a></td>
          <td>對照啟示 — GSM 管的第三方 token（GitHub PAT / Slack token / SaaS API key）scope 過寬時、上游事件直接傳導下游、要走 IAM Conditions 收 caller scope 跟過期時間</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>（GSM CMEK 後端、key custody 分離）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（secret IAM binding、Workload Identity Federation 設定）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（GSM 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://cloud.google.com/secret-manager/docs">Secret Manager Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Keycloak</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/</guid><description>&lt;p>Keycloak 是 open source 自管 Identity Provider、Red Hat 主導維護（商業支援版本為 Red Hat build of Keycloak、前身 Red Hat SSO）。它承擔的責任跟 SaaS IdP 相同 — SSO、MFA、federation、user lifecycle — 但 &lt;em>整個控制面留在組織自己手上&lt;/em>：issuer signing key、support tooling、底層 PostgreSQL、HA cluster、CVE patch cadence 全部自管。決定上 Keycloak 不是技術偏好、是組織決定把 SaaS IdP 的「第三方信任成本」換成「自家 SRE 運維成本 + 安全責任」。在 &lt;a href="https://tarrragon.github.io/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 建&lt;/a> 的光譜上、Keycloak 是認證能力「建」側的 canonical 例子 — 把 feature SaaS（Auth0 / Okta）的第三方信任成本、換成自管控制面的運維成本；什麼訊號該翻到這一側、見 0.22 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度&lt;/a> 卡。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Keycloak 是 &lt;em>自管控制面&lt;/em> 的 human identity 與 federation engine、不是 cloud resource permission engine。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0&lt;/a> 的本質差異在於信任邊界落點：SaaS IdP 把 signing key、tenant 隔離、support workflow 都託管出去、客戶承擔「供應商出事我也跟著被打」的風險；Keycloak 把整條控制面收回自家機房或自家 VPC、客戶承擔「signing key 過期 / DB 崩 / Java app CVE 沒跟上」的運維風險。&lt;/p>
&lt;p>跟 cloud-native SSO（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a>）相比、Keycloak 的核心優勢是 &lt;em>不綁雲廠 + 可深度客製 authentication flow + 資料不出境&lt;/em>。適合垂直：金融、政府、醫療某些不接受 SaaS IdP 的場景；以及預算敏感、員工數中等、SRE 量能足以接 24/7 on-call 的組織。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Keycloak 該承擔哪一段 identity 控制（SSO / MFA / federation / brokering）、哪一段該交給雲端 IAM 或下游應用&lt;/li>
&lt;li>自管 IdP 的最低運維基線（HA、DB DR、cert / signing key rotation、CVE cadence、SIEM 接點）&lt;/li>
&lt;li>Realm / Client / User Federation / Identity Broker / Authentication Flow / SPI 各自的決策時機與陷阱&lt;/li>
&lt;li>何時用 Keycloak、何時改走 SaaS（Okta / Auth0）或其他 OSS（Authentik / Zitadel）&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Keycloak 部署是否健康、最少看 SaaS IdP 的四件事加上自管特有的四個維度：&lt;/p></description><content:encoded><![CDATA[<p>Keycloak 是 open source 自管 Identity Provider、Red Hat 主導維護（商業支援版本為 Red Hat build of Keycloak、前身 Red Hat SSO）。它承擔的責任跟 SaaS IdP 相同 — SSO、MFA、federation、user lifecycle — 但 <em>整個控制面留在組織自己手上</em>：issuer signing key、support tooling、底層 PostgreSQL、HA cluster、CVE patch cadence 全部自管。決定上 Keycloak 不是技術偏好、是組織決定把 SaaS IdP 的「第三方信任成本」換成「自家 SRE 運維成本 + 安全責任」。在 <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> 的光譜上、Keycloak 是認證能力「建」側的 canonical 例子 — 把 feature SaaS（Auth0 / Okta）的第三方信任成本、換成自管控制面的運維成本；什麼訊號該翻到這一側、見 0.22 與 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a> 卡。</p>
<h2 id="服務定位">服務定位</h2>
<p>Keycloak 是 <em>自管控制面</em> 的 human identity 與 federation engine、不是 cloud resource permission engine。跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a> 的本質差異在於信任邊界落點：SaaS IdP 把 signing key、tenant 隔離、support workflow 都託管出去、客戶承擔「供應商出事我也跟著被打」的風險；Keycloak 把整條控制面收回自家機房或自家 VPC、客戶承擔「signing key 過期 / DB 崩 / Java app CVE 沒跟上」的運維風險。</p>
<p>跟 cloud-native SSO（<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a>）相比、Keycloak 的核心優勢是 <em>不綁雲廠 + 可深度客製 authentication flow + 資料不出境</em>。適合垂直：金融、政府、醫療某些不接受 SaaS IdP 的場景；以及預算敏感、員工數中等、SRE 量能足以接 24/7 on-call 的組織。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Keycloak 該承擔哪一段 identity 控制（SSO / MFA / federation / brokering）、哪一段該交給雲端 IAM 或下游應用</li>
<li>自管 IdP 的最低運維基線（HA、DB DR、cert / signing key rotation、CVE cadence、SIEM 接點）</li>
<li>Realm / Client / User Federation / Identity Broker / Authentication Flow / SPI 各自的決策時機與陷阱</li>
<li>何時用 Keycloak、何時改走 SaaS（Okta / Auth0）或其他 OSS（Authentik / Zitadel）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Keycloak 部署是否健康、最少看 SaaS IdP 的四件事加上自管特有的四個維度：</p>
<ul>
<li><strong>誰能做什麼</strong>：master realm admin 的人數、是否走 access request workflow、admin console 是否限 IP / device trust、是否強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant 認證</a></li>
<li><strong>憑證在哪裡</strong>：client secret 是否走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a>、realm signing key 的 rotation 排程、admin token 的 TTL</li>
<li><strong>入口如何暴露</strong>：哪些 realm 對外、reverse proxy / Ingress 是否做 rate limit、admin console（/auth/admin）是否限內網或 zero trust</li>
<li><strong>證據是否可回查</strong>：Event Listener SPI 是否接 SIEM、admin event 跟 login event 是否分流、保留期是否符合稽核</li>
<li><strong>DB 健康</strong>：PostgreSQL / MySQL 是否跨 AZ、是否有 PITR、是否做過 restore 演練（不是只有備份成功訊息）</li>
<li><strong>Cert lifecycle</strong>：TLS cert 與 realm signing key 各自的 rotation 排程、是否走 <a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">Website Certificate Lifecycle</a> 自動化</li>
<li><strong>HA topology</strong>：Keycloak cluster 是否多節點、Infinispan cache 是否跨 AZ、單節點重啟是否會踢掉所有 session</li>
<li><strong>Upgrade cadence</strong>：Keycloak 每年 major release、CVE patch 是否能在 SLA 內上、是否有 staging 跑 DB migration</li>
</ul>
<p>八個維度任一缺失、都是自管 IdP 常見事故的入口。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Realm 設計</strong>：Realm 是 Keycloak 的隔離邊界、每個 realm 有獨立的 user store、client、role、signing key。multi-tenancy 走 realm 是正確選擇、但 <em>master realm 能管所有 realm</em>、master realm 的 admin compromise = 全公司 IdP compromise。把 master realm 鎖在內網、operational realm 才對外、是基本姿勢。</p>
<p><strong>Client 註冊與 secret</strong>：每個應用是一個 client、confidential client 有 secret、public client（SPA / mobile）走 PKCE 不存 secret。client secret 不存 source code、走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a> 注入。client 數量爆炸時要設 naming convention 跟 ownership 標記、不然 stale client 會堆積。</p>
<p><strong>User Federation</strong>：把既有 LDAP / Active Directory 接進 Keycloak、user 還是住在原 directory、Keycloak 做 protocol 翻譯（LDAP → OIDC / SAML）。這是 Keycloak 強項之一 — 不需要 user migration、漸進接入。陷阱是 LDAP 連線健康 = IdP 健康、LDAP 慢 = 全公司 login 慢。</p>
<p><strong>Identity Brokering</strong>：把外部 IdP（Google、Microsoft、其他 SAML / OIDC provider）federate 進來、Keycloak 當中介。B2B 合作常見模式 — partner 用自己的 IdP、不在我的 user store 開帳號。決策點是 <em>trust mapping</em>：外部 claim 怎麼對應到內部 role、外部 IdP 的 MFA 狀態怎麼信任。</p>
<p><strong>Authentication Flow</strong>：Keycloak 把 login / registration / reset password 做成可編輯的 flow DAG、可以插入自訂 step。這是 Keycloak 跟 SaaS IdP 最大差異點之一 — 想要 step-up MFA、device fingerprint、risk-based 判斷都可以自己接。雙面刃是 <em>自訂 flow 容易留漏洞</em>：跳過必要步驟、condition 寫錯讓 MFA 變可選、custom Authenticator SPI 沒處理 race condition。</p>
<p><strong>Theme / 客製 UI</strong>：Keycloak 支援 theme override、可以改 login page HTML / CSS / JS。custom JS 在 login page = 自己注入 XSS 風險 — theme 寫進去之後就是 IdP 本體的攻擊面、不是普通網頁。CSP 跟 input sanitization 要當成 IdP 安全規範看待。</p>
<p><strong>Event Listener / Audit</strong>：Keycloak 預設只把 event 寫進 DB、UI 上能查、但 <em>不會自動推到外部 SIEM</em>。生產環境必須接 Event Listener SPI（內建 jboss-logging、或自寫 Kafka / file listener）把 admin event 跟 login event 推進 SIEM。沒接的話 audit trail 只在 IdP 本機、IdP 出事就拿不到 evidence。</p>
<p><strong>Exception / break-glass</strong>：master realm 留至少 2 個 break-glass admin、credential 離線存、走獨立 MFA（hardware key）。Keycloak cluster 整個失聯時、用 break-glass 直連 DB / 直連單一節點救回。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Keycloak（自管 OSS）</th>
          <th>Okta（SaaS）</th>
          <th>Auth0（SaaS / B2C）</th>
          <th>Authentik / Zitadel（其他 OSS）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制面責任</td>
          <td>自己跑 issuer / signing / HA / DB / upgrade</td>
          <td>Okta 託管</td>
          <td>Auth0 託管</td>
          <td>自己跑、但社群規模小於 Keycloak</td>
      </tr>
      <tr>
          <td>客製化深度</td>
          <td>高 — Authenticator SPI / theme / event listener</td>
          <td>中 — Workflows / Hooks、限定範圍</td>
          <td>高 — Actions（JS hook）</td>
          <td>中 — Authentik flow 視覺化、彈性中等</td>
      </tr>
      <tr>
          <td>第三方信任成本</td>
          <td>低 — 自管、自己承擔運維</td>
          <td>高 — 供應商事件直接波及</td>
          <td>高 — 同 Okta（同集團）</td>
          <td>低 — 自管</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>高 — HA、DR、cert、DB、CVE 都自管</td>
          <td>低 — SaaS</td>
          <td>低 — SaaS</td>
          <td>高 — 同 Keycloak、生態系更小</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>資料主權、預算敏感、需深度客製、有 SRE 量能</td>
          <td>多雲、大量 SaaS、lifecycle 自動化</td>
          <td>B2C、消費者 identity、developer-centric</td>
          <td>規模小、Keycloak 太重、想要更現代 UI</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — 自己掌握資料、protocol 標準可遷移</td>
          <td>高 — SAML / SCIM 接線散在數百 app</td>
          <td>高 — Actions / Rules 客製綁定深</td>
          <td>中 — 同 Keycloak</td>
      </tr>
  </tbody>
</table>
<p>選 Keycloak 的核心訴求：<em>資料主權 + 預算控制 + 客製 flow 需求</em>、且有 SRE 團隊能 24/7 on-call、能接受自管的運維重量。團隊小於 50 人沒 SRE 量能、應用主要在 SaaS（pre-built integration 用不上 Keycloak 強項）、需要快速接 7000+ SaaS app — 都該回頭看 Okta / Auth0。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>User Federation 跟 LDAP 整合</strong>：企業環境常見「Active Directory 是 user source of truth、Keycloak 做 protocol 層」。注意 LDAP 同步策略（read-only / writable / import）、LDAP 健康直接影響 IdP 可用性、LDAP timeout 要設嚴格避免 login 卡住整個 cluster。</p>
<p><strong>Identity Brokering 跟外部 IdP</strong>：把 Google / Microsoft / 其他 SAML IdP federate 進來、外部 user 進來時 Keycloak 自動建 link。trust mapping 是關鍵 — 外部 IdP 宣稱「這個 user 已 MFA」、要不要信？外部 group claim 怎麼對應到內部 role？沒有預設答案、要用 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a> 邊界決定。</p>
<p><strong>Fine-Grained Authorization（UMA / Authorization Services）</strong>：Keycloak 內建 policy engine、可以做 resource-level 授權（不只是 role-based）。適合需要中央化 policy decision 的場景、但會把應用的授權邏輯綁進 Keycloak、退場成本變高。多數場景應該把 authorization 留在應用內、Keycloak 只做 authentication + role token 發行。</p>
<p><strong>Custom Authenticator SPI</strong>：用 Java 寫自訂 authenticator、插進 Authentication Flow。能做 step-up MFA、device posture、risk score 判斷。陷阱是 SPI 程式碼就是 IdP 本體的一部分、bug = IdP 漏洞、必須走完整 code review + 安全測試流程、不能當普通 feature 開發。</p>
<p><strong>Realm signing key rotation</strong>：每個 realm 有自己的 RSA / EC signing key、用來簽 ID token / SAML assertion。rotation 必須跟下游 client 協調（key rollover 期間 client 要能接受新舊 key）、否則 rotation 當天全公司 login 失敗。分域分批是必做的、參考 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>DB 是 SPOF</strong>：Keycloak 所有 state 在 PostgreSQL / MySQL、DB 出事 = IdP 停 = 全公司 SSO 停。跨 AZ replication + PITR + 季度 restore 演練、不是 nice-to-have</li>
<li><strong>Cert / signing key 過期</strong>：自管 IdP 最常見事故、TLS cert 過期擋對外 endpoint、realm signing key 過期讓所有 token 變無效。走 <a href="/blog/backend/knowledge-cards/certificate-rotation-renewal/" data-link-title="Certificate Rotation and Renewal" data-link-desc="說明網站憑證如何安全續期與輪替以避免停機">Certificate Rotation</a> 自動化、過期前 30 天 alert</li>
<li><strong>Cluster split-brain</strong>：Infinispan cache 跨節點同步、網路分區時 session 狀態不一致、user 看起來登入但下一個 request 又被踢出。HA topology 設計要考慮 cache mode（distributed vs replicated）、network 健康監控要 alert split-brain</li>
<li><strong>Major upgrade 卡 DB migration</strong>：每年 major release 帶 schema migration、staging 沒跑過就 production 升級 = 數小時 downtime。upgrade plan 包含 rollback DB snapshot + staging full rehearsal</li>
<li><strong>Custom theme / Authenticator 留漏洞</strong>：theme JS 引入 XSS、custom Authenticator 跳過 MFA、SPI 沒處理 race condition。把 IdP 客製當成 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain</a> 看待、走 code review + 安全測試</li>
<li><strong>Event 沒進 SIEM</strong>：預設只在 Keycloak DB、IdP 出事就拿不到 evidence。Event Listener SPI 接 Kafka / file / SIEM、admin event 跟 login event 各自接 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
<li><strong>Master realm admin 過多</strong>：日常工作不該用 master realm admin、應該在 operational realm 開有限權限 admin。master realm 是 single point of compromise</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不想自管、要 SaaS IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a></td>
      </tr>
      <tr>
          <td>AWS-only 員工 SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
      </tr>
      <tr>
          <td>Cloud resource 權限</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>小團隊、Keycloak 太重</td>
          <td>Authentik / Zitadel / Ory Hydra（更輕量 OSS、生態系較小）</td>
      </tr>
      <tr>
          <td>事件偵測（不只 Keycloak event）</td>
          <td>04 SIEM / detection 工具（<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 跟 07 SIEM 章節）</td>
      </tr>
      <tr>
          <td>Secret / signing key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Keycloak 完整 SAML / OIDC 規格細節、SPI Java API 文件</li>
<li>Red Hat build of Keycloak 商業支援的差異與授權細節</li>
<li>Keycloak Operator（Kubernetes deployment）的逐步部署教學</li>
<li>LDAP / Active Directory 各種 schema 對應規格</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Keycloak 沒有直接的廠商級公開事件（OSS 沒有 vendor incident 的對應形態）、自管 IdP 的失效模式以下分兩類整理：跨 vendor 共通的 <em>同構失效</em> 用既有 case 對照、自管 IdP <em>特有</em> 的失效情境補敘事說明、避免案例表變成「同一個 frame 拼四個 case slug」。</p>
<p><strong>對照引用（跨 vendor 同構失效）</strong>：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Keycloak 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>對所有自管 IdP 的啟示：IdP 控制面故障會外溢到下游所有依賴 SSO 的服務、降級策略（local fallback、cached session）必須事先設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Keycloak realm signing key rotation 必須分域分批、一次 rotate 全部 realm = 全公司 login 同時失敗</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>純 push MFA 抗不過 fatigue、Keycloak 自訂 Authentication Flow 應該強制高風險操作走 phishing-resistant factor</td>
      </tr>
  </tbody>
</table>
<p><strong>自管 IdP 特有的失效情境</strong>（沒有對應公開 vendor case、來自自管運維常見事故樣態）：</p>
<ul>
<li><strong>Cert 過期讓全公司 SSO 卡死</strong>：Keycloak signing cert / TLS cert / 後端 DB cert 都自己管、任何一張過期 = login 全停。Okta / Auth0 客戶不會遇到這個失效面（vendor 自己 rotate）— 自管組織必須有 cert lifecycle monitoring（Prometheus exporter + alert）+ 季度 rotate rehearsal、不能等 Let&rsquo;s Encrypt / 公司 PKI 發過期通知才動</li>
<li><strong>Major upgrade 卡 DB migration 變數小時 downtime</strong>：Keycloak 每年 major release 帶 schema migration、若 staging 沒 full rehearsal 就 production 升級、可能遇到 migration 比預期慢 5-10 倍、整個維護視窗炸掉。對照 Okta / Auth0：vendor 自己升、客戶感知是 minutes-level、不是 hours-level</li>
<li><strong>Realm scope 在小規模時用法跟大規模衝突</strong>：<a href="/blog/backend/07-security-data-protection/cases/contrast-identity-governance-by-scale/" data-link-title="7.C10 對照：規模差異下的身份治理" data-link-desc="identity 控制面治理在不同規模服務下的失敗邊界差異。">Contrast: Identity Governance by Scale</a> 揭示不同規模治理模式差異 — 小團隊用單一 realm 順、團隊長大後該拆 realm 卻沒拆、最後 admin compromise blast radius 變整個組織。Keycloak 比 SaaS IdP 更容易踩到、因為 realm 拆分要自己決定時機、沒 vendor 推使用者升級 tier</li>
<li><strong>DB 是 SPOF、自管沒做好 = SSO 跟 DB 一起死</strong>：Keycloak 用 PostgreSQL / MySQL 存 user / session / signing key、DB 出事 = IdP 停。跨 AZ HA + 跨 region DR + 季度 failover 演練是硬性要求、不是 nice-to-have；SaaS IdP 客戶不會遇到這個層次的失效面</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>（Keycloak 之後的 cloud resource permission 層）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（自管 IdP 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://www.keycloak.org/documentation">Keycloak Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Gatling</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/</guid><description>&lt;p>Gatling 的核心責任是把複雜使用者流程寫成可維護的 JVM simulation。它適合 JVM 生態團隊、強型別 DSL、HTTP / WebSocket / JMS / MQTT 等 scenario，以及需要把 injection profile、assertion、report 與 CI pipeline 綁在一起的壓測流程。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Gatling 是 &lt;em>Scala-origin / 現以 Java DSL 為主流&lt;/em> 的 load testing 工具、跑在 JVM、async / non-blocking engine（基於 Akka / Netty）讓單一 injector node 就能驅動高 RPS。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust&lt;/a> 的核心差異在 &lt;em>語言生態 + engine efficiency + scenario 表達力&lt;/em>、壓出負載的能力都具備：&lt;/p>
&lt;ul>
&lt;li>vs k6 — k6 走 Go runtime + JavaScript scripting、CLI / Grafana 生態友善；Gatling 走 JVM + Java/Scala/Kotlin DSL、適合既有 JVM 工具鏈與強型別 review&lt;/li>
&lt;li>vs JMeter — JMeter 走 GUI / XML test plan、適合非工程角色協作；Gatling 走 code-first、適合 PR / build pipeline / refactor 工作流&lt;/li>
&lt;li>vs Locust — Locust 走 Python coroutine、scripting 自由度高；Gatling 走 DSL + injection profile、scenario 結構化程度更高&lt;/li>
&lt;li>engine efficiency — async / non-blocking model 讓 Gatling 在單機可推到數萬 RPS、JMeter thread-per-user 在同等資源下 throughput 較低&lt;/li>
&lt;/ul>
&lt;p>產品線分兩層：&lt;em>Gatling OSS&lt;/em>（開源 simulation runner + HTML report）與 &lt;em>Gatling Enterprise&lt;/em>（前身 FrontLine、加上 distributed injector、cluster orchestration、live monitoring、long-term result storage、role-based access）。OSS 適合單機 baseline / CI smoke、Enterprise 適合 cross-region distributed / 大型活動前壓測 / 結果長期治理。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Gatling 在壓測流程裡是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Scala DSL vs Java DSL 版本&lt;/strong>：Gatling 3.7+（2022）正式加 Java DSL、2024 後新專案多走 Java DSL；舊 Scala simulation 仍可跑、但團隊要決定 &lt;em>維持 Scala 還是漸進改寫 Java&lt;/em>、避免雙語言治理&lt;/li>
&lt;li>&lt;strong>Injection profile 設計&lt;/strong>：simulation 是否明確區分 &lt;em>open model&lt;/em>（&lt;code>rampUsersPerSec&lt;/code> / &lt;code>constantUsersPerSec&lt;/code>、模擬真實 arrival）vs &lt;em>closed model&lt;/em>（&lt;code>atOnceUsers&lt;/code> / &lt;code>rampUsers&lt;/code>、模擬 fixed user pool），對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 的 traffic shape&lt;/li>
&lt;li>&lt;strong>Assertion gate&lt;/strong>：simulation 是否有 &lt;code>assertions { global.responseTime.percentile3.lt(500) }&lt;/code> 這類 hard gate、CI 跑完直接 fail build；沒 assertion 的 simulation 只是壓測、不是 release gate&lt;/li>
&lt;li>&lt;strong>Enterprise vs OSS 邊界&lt;/strong>：是否清楚知道哪些能力只 Enterprise 有（distributed injector / multi-region / long-term result storage / live dashboard）、避免用 OSS 拼湊 Enterprise 級需求&lt;/li>
&lt;/ul>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Gatling 適合 code-first 且 JVM 能力強的團隊。當 workload model 需要多步驟 flow、資料 feeder、條件分支、session state 與明確 injection profile，Gatling 能用 simulation 把這些行為寫成工程 artifact。&lt;/p></description><content:encoded><![CDATA[<p>Gatling 的核心責任是把複雜使用者流程寫成可維護的 JVM simulation。它適合 JVM 生態團隊、強型別 DSL、HTTP / WebSocket / JMS / MQTT 等 scenario，以及需要把 injection profile、assertion、report 與 CI pipeline 綁在一起的壓測流程。</p>
<h2 id="服務定位">服務定位</h2>
<p>Gatling 是 <em>Scala-origin / 現以 Java DSL 為主流</em> 的 load testing 工具、跑在 JVM、async / non-blocking engine（基於 Akka / Netty）讓單一 injector node 就能驅動高 RPS。它跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> / <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a> / <a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a> 的核心差異在 <em>語言生態 + engine efficiency + scenario 表達力</em>、壓出負載的能力都具備：</p>
<ul>
<li>vs k6 — k6 走 Go runtime + JavaScript scripting、CLI / Grafana 生態友善；Gatling 走 JVM + Java/Scala/Kotlin DSL、適合既有 JVM 工具鏈與強型別 review</li>
<li>vs JMeter — JMeter 走 GUI / XML test plan、適合非工程角色協作；Gatling 走 code-first、適合 PR / build pipeline / refactor 工作流</li>
<li>vs Locust — Locust 走 Python coroutine、scripting 自由度高；Gatling 走 DSL + injection profile、scenario 結構化程度更高</li>
<li>engine efficiency — async / non-blocking model 讓 Gatling 在單機可推到數萬 RPS、JMeter thread-per-user 在同等資源下 throughput 較低</li>
</ul>
<p>產品線分兩層：<em>Gatling OSS</em>（開源 simulation runner + HTML report）與 <em>Gatling Enterprise</em>（前身 FrontLine、加上 distributed injector、cluster orchestration、live monitoring、long-term result storage、role-based access）。OSS 適合單機 baseline / CI smoke、Enterprise 適合 cross-region distributed / 大型活動前壓測 / 結果長期治理。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Gatling 在壓測流程裡是否健康、最少看四件事：</p>
<ul>
<li><strong>Scala DSL vs Java DSL 版本</strong>：Gatling 3.7+（2022）正式加 Java DSL、2024 後新專案多走 Java DSL；舊 Scala simulation 仍可跑、但團隊要決定 <em>維持 Scala 還是漸進改寫 Java</em>、避免雙語言治理</li>
<li><strong>Injection profile 設計</strong>：simulation 是否明確區分 <em>open model</em>（<code>rampUsersPerSec</code> / <code>constantUsersPerSec</code>、模擬真實 arrival）vs <em>closed model</em>（<code>atOnceUsers</code> / <code>rampUsers</code>、模擬 fixed user pool），對應 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 的 traffic shape</li>
<li><strong>Assertion gate</strong>：simulation 是否有 <code>assertions { global.responseTime.percentile3.lt(500) }</code> 這類 hard gate、CI 跑完直接 fail build；沒 assertion 的 simulation 只是壓測、不是 release gate</li>
<li><strong>Enterprise vs OSS 邊界</strong>：是否清楚知道哪些能力只 Enterprise 有（distributed injector / multi-region / long-term result storage / live dashboard）、避免用 OSS 拼湊 Enterprise 級需求</li>
</ul>
<h2 id="定位">定位</h2>
<p>Gatling 適合 code-first 且 JVM 能力強的團隊。當 workload model 需要多步驟 flow、資料 feeder、條件分支、session state 與明確 injection profile，Gatling 能用 simulation 把這些行為寫成工程 artifact。</p>
<p>這個定位讓 Gatling 接到 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 與 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>。它的價值在於把 traffic shape 寫進 injection profile，讓 ramp-up、constant users、stress peak 與 soak test 都能被版本化。</p>
<h2 id="適用場景">適用場景</h2>
<p>JVM 團隊適合用 Gatling 承接壓測。Java、Scala 或 Kotlin 團隊能把 simulation 當成一般程式碼 review，並用既有 build、dependency、CI 與 artifact 流程維護。</p>
<p>複雜 scenario 適合用 Gatling 表達。登入、搜尋、加入購物車、checkout、payment mock、order query 這類 multi-step flow 可以用 session 與 feeder 管理資料。</p>
<p>高品質 report 適合 release review。Gatling 的 report 能幫 reviewer 看到 response time distribution、request group、error 與 injection profile，適合在 release gate 中保留可讀證據。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Gatling 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JVM DSL</td>
          <td>simulation 可 code review</td>
          <td>Scala / Java / Kotlin 維護能力</td>
      </tr>
      <tr>
          <td>Injection profile</td>
          <td>負載階段可精準表達</td>
          <td>production traffic shape 校正</td>
      </tr>
      <tr>
          <td>Session / feeder</td>
          <td>多步驟資料與狀態容易管理</td>
          <td>測試資料治理與敏感資料遮罩</td>
      </tr>
      <tr>
          <td>Report</td>
          <td>release review 可讀性高</td>
          <td>長期趨勢儲存與 cross-run comparison</td>
      </tr>
  </tbody>
</table>
<p>JVM DSL 價值來自可維護性。壓測 scenario 如果需要被長期 review、重構、抽 helper 或接 build pipeline，Gatling 的 code-first workflow 會比 GUI test plan 更適合工程團隊。</p>
<p>Injection profile 價值來自負載形狀精準。團隊可以把 steady load、spike、ramp、open model 與 closed model 放到 simulation 中，讓 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的 knee point 判讀更可重現。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Gatling 和 k6 的主要差異是語言與生態。Gatling 適合 JVM 團隊與強型別 simulation；k6 適合 JavaScript-style scripting、CLI workflow 與 Grafana 生態。</p>
<p>Gatling 和 JMeter 的主要差異是維護模式。Gatling 偏 code review、build pipeline 與 simulation abstraction；JMeter 偏 GUI、plugin 與跨角色測試資產。</p>
<p>Gatling 和 Locust 的主要差異是自訂語言。Locust 適合 Python 團隊與任意 Python client；Gatling 適合 JVM 團隊與 report / injection profile 的結構化壓測。</p>
<p>Gatling 和 Vegeta 的主要差異是 scenario 深度。Vegeta 適合快速 HTTP pressure test；Gatling 適合需要 session、feeder、assertion 與多 request group 的長期測試。</p>
<h2 id="操作成本">操作成本</h2>
<p>Gatling 的主要成本是 JVM 團隊能力。非 JVM 團隊要承擔語言、build tool、dependency 與 simulation pattern 的學習成本；這個成本只有在 scenario 複雜度夠高時才划算。</p>
<p>測試資料成本來自 feeder 與 session。多步驟 flow 需要 account、cart、order、token、region 與 tenant 資料，資料過期或分布偏差會讓壓測結果失真。</p>
<p>Enterprise / distributed 成本要提前評估。單機 Gatling 適合中小型 baseline；跨 region、大型活動前驗證或長時間 soak test 需要 runner topology、結果集中與雲端成本治理。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Gatling 結果應回寫到 evidence package。最小欄位包括 simulation version、injection profile、feeder source、target environment、assertion、response time distribution、error rate、throughput、target service saturation metric、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Gatling 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>simulation code、HTML report、dashboard link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>test start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / metrics / logs 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>feeder freshness、scenario coverage</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>production similarity、runner capacity</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 flow、資料偏差、下游 mock 限制</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 simulation 可回放。Reviewer 要能從 report 回到 injection profile、scenario code、feeder 與目標環境，才有辦法判斷一次壓測是容量訊號還是測試設計偏差。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Gatling</th>
          <th>k6</th>
          <th>JMeter</th>
          <th>Locust</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>語言 / DSL</td>
          <td>Java / Kotlin / Scala DSL（JVM）</td>
          <td>JavaScript（Go runtime）</td>
          <td>GUI / XML test plan（JVM）</td>
          <td>Python（coroutine / gevent）</td>
      </tr>
      <tr>
          <td>Engine model</td>
          <td>Async / non-blocking（Akka + Netty）</td>
          <td>Async（Go goroutine）</td>
          <td>Thread-per-user（同步）</td>
          <td>Async coroutine</td>
      </tr>
      <tr>
          <td>單機 RPS 上限</td>
          <td>高（數萬 RPS）</td>
          <td>高（數萬 RPS）</td>
          <td>中（thread overhead）</td>
          <td>中（GIL + coroutine）</td>
      </tr>
      <tr>
          <td>Scenario 表達力</td>
          <td>強（session / feeder / 條件分支內建）</td>
          <td>中（JS function 自寫）</td>
          <td>中（GUI 拖拉 + listener）</td>
          <td>中（Python class + task）</td>
      </tr>
      <tr>
          <td>Report quality</td>
          <td>高（HTML report 內建、distribution / group 詳細）</td>
          <td>中（CLI 摘要 + Grafana 串接）</td>
          <td>中（GUI listener、不適合 headless）</td>
          <td>中（web UI 即時、無 historical）</td>
      </tr>
      <tr>
          <td>CI integration</td>
          <td>強（Maven / Gradle / sbt + assertion gate）</td>
          <td>強（CLI + JSON output）</td>
          <td>中（CLI mode 可、但 GUI-first）</td>
          <td>強（CLI + Python ecosystem）</td>
      </tr>
      <tr>
          <td>Distributed</td>
          <td>OSS 自建 / Enterprise 內建</td>
          <td>k6 Cloud / OSS 自建</td>
          <td>自建（master-slave）</td>
          <td>自建（master-worker）</td>
      </tr>
      <tr>
          <td>商業版本</td>
          <td>Gatling Enterprise（前 FrontLine）</td>
          <td>Grafana Cloud k6</td>
          <td>無（純 OSS）</td>
          <td>無（純 OSS）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>JVM 團隊、複雜 scenario、release gate、高 RPS efficiency</td>
          <td>全棧團隊、CLI workflow、Grafana 生態</td>
          <td>跨角色團隊、legacy test plan、protocol 多樣</td>
          <td>Python 團隊、自訂 client、輕量 setup</td>
      </tr>
  </tbody>
</table>
<p>選 Gatling 的核心訴求：<em>JVM 團隊 + 複雜 scenario（session / feeder / 多 group）+ 高 RPS 單機效率 + HTML report 作為 release gate 證據</em>。Java DSL 在 2024 後降低了 Scala 學習門檻、讓 Java/Kotlin 後端團隊不必再為了壓測導入 Scala。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Gatling Enterprise（前 FrontLine）</strong>：商業版加 <em>distributed injector cluster</em>（跨 region / 跨 cloud 推大型負載）、<em>live monitoring dashboard</em>（real-time RPS / response time 趨勢、不用等 simulation 結束看 HTML）、<em>long-term result storage</em>（cross-run comparison、retention policy）、<em>role-based access</em>（QA / dev / SRE 不同權限）。對只跑單機 baseline 的團隊 OSS 已夠；要跑黑五 / 春晚級活動前壓測或多 region 同時施壓、需要 Enterprise 或自建 distributed topology。</p>
<p><strong>Java DSL 取代 Scala 成主流（2022-2024）</strong>：Gatling 3.7（2022）正式釋出 Java DSL、3.9+ 文件 Java / Kotlin / Scala 三語並列、2024 後新教學多以 Java 為主。對 Java 後端團隊降低 onboarding 成本、但要注意 <em>Gatling 2.x → 3.x</em> 的 Scala syntax 不向後相容（<code>scenario</code> builder、<code>http</code> config、<code>feed</code> 用法都改寫）— 舊 simulation 升級時等於改寫一遍。</p>
<p><strong>Distributed execution（OSS）</strong>：OSS 沒有內建 cluster orchestration、要靠 <em>multiple injector + result aggregation</em>：每台 injector 跑同一份 simulation（按 user count 切割）、結束後把 <code>simulation.log</code> 蒐集到一處用 <code>gatling.sh</code> 重跑 report stage。常見補位是用 Kubernetes Job + 共享 PVC、或直接走 Gatling Enterprise。</p>
<p><strong>HTML report 與 release gate</strong>：simulation 跑完自動產 HTML report、含 <em>response time percentile distribution</em>（mean / p50 / p95 / p99 / max）、<em>per-request-group breakdown</em>、<em>active users over time</em>、<em>error log</em>。release gate 的標準做法是：CI job 跑 simulation → assertion gate fail 直接 break build → HTML report 存成 build artifact 供 reviewer 翻查、配合 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Evidence Package</a> 治理。</p>
<p><strong>CI integration 模式</strong>：Jenkins / GitLab CI / GitHub Actions 都靠 <code>mvn gatling:test</code> / <code>gradle gatlingRun</code> / <code>sbt gatling:test</code> 入口、CI 設定 <em>baseline simulation</em>（每 PR 跑、catch regression）+ <em>release simulation</em>（release branch / nightly 跑、長時間 soak）。staging environment 跑壓測時要隔離噪音來源（其他 QA 流量 / cron job）、否則 RPS 數字會被污染。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Scala learning curve 拖累進度</strong>：團隊沒人會 Scala、被 implicit / case class / pattern match 卡住 — 改用 Java DSL（3.7+）或 Kotlin DSL、保留 Gatling 表達力但去除 Scala 學習成本</li>
<li><strong>Gatling 2.x → 3.x 升級 simulation 全紅</strong>：<code>bootstrap</code> import path / <code>scenario</code> builder API / <code>feed</code> 語法都變了 — 走 <em>新專案直接 3.x、舊專案維持 2.x</em> 雙軌、或安排專門 sprint 改寫、避免邊跑邊踩雷</li>
<li><strong>JVM heap OOM / GC pause 拖慢 RPS</strong>：高 RPS 下 default heap 不夠、Young Gen GC 頻繁 — 調 <code>-Xmx4G -Xms4G</code>、用 G1GC / ZGC、監控 injector 的 GC log 跟 CPU、不是只看 target service</li>
<li><strong>Injection profile 設計錯導致誤判 saturation</strong>：用 <code>atOnceUsers(1000)</code> 壓 closed model 但實際 traffic 是 open arrival、結果 knee point 找錯 — 看 production traffic shape、open model 用 <code>constantUsersPerSec</code> / <code>rampUsersPerSec</code>、closed model 才用 <code>atOnceUsers</code></li>
<li><strong>Single injector node 撞 client-side bottleneck</strong>：injector CPU / network / file descriptor / source port 用滿、看起來 target saturate 其實是 injector saturate — 監控 injector resource、scale out 成 distributed 或走 Enterprise</li>
<li><strong>Feeder data 過期 / 分布偏差</strong>：用同一份 <code>users.csv</code> 反覆壓、cache hit rate 失真、production 看不到的 cache miss 路徑沒被測 — feeder 走 <code>random</code> / <code>shuffle</code>、定期 regenerate、覆蓋 long-tail key</li>
<li><strong>HTML report 看起來綠但 production 出事</strong>：assertion gate 只設 average response time、p99 / error rate 沒設、release 後尖峰時段才爆 — assertion 要明確設 p95 / p99 + error rate threshold、不只看 mean</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Gatling 適合回寫多步驟與多負載模型案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 雙峰 workload</a> 的直播與投注雙模型、<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek waiting room</a> 的 token / admission flow、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow ticketing</a> 的售票流程壓力、<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora 金融帳本</a> 的「比賽期讀爆量 + payout 時寫爆量」雙峰錯位，以及 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> 的「投注 / 結算 / 賠率更新」三類請求 group 的 injection profile。</p>
<p>這些案例的重點是 scenario 與 injection profile。Gatling 頁引用案例時，要把業務流程拆成 request group、session state、feeder、assertion 與 stop condition — 例如 DraftKings 雙峰錯位要寫成兩個 scenario 平行注入、各自有獨立 assertion budget。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>、<a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a>、<a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></li>
<li>官方：<a href="https://docs.gatling.io/">Gatling documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.3 Transaction 與一致性邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</guid><description>&lt;p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。&lt;/p>
&lt;p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。&lt;/p>
&lt;h2 id="邊界先於語法">邊界先於語法&lt;/h2>
&lt;p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。&lt;/p>
&lt;p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。&lt;/p>
&lt;h2 id="isolation-level-五級深度">Isolation Level 五級深度&lt;/h2>
&lt;p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 &lt;em>正確性 vs 性能&lt;/em> 之間做取捨。&lt;/p>
&lt;p>&lt;strong>0. Read Uncommitted（dirty read 可能）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>可讀到別的 transaction 還沒 commit 的資料&lt;/li>
&lt;li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）&lt;/li>
&lt;li>實務不要用&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>1. Read Committed（PostgreSQL / Oracle 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>只讀到 commit 的資料&lt;/li>
&lt;li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）&lt;/li>
&lt;li>適合：read-heavy workload、不要求同 transaction 內 read consistency&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Repeatable Read（MySQL InnoDB 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同 transaction 內 read 一致（snapshot at transaction start）&lt;/li>
&lt;li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了&lt;/li>
&lt;li>適合：報表類 transaction、需要 snapshot 一致性&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Serializable（最強）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>看起來像所有 transaction 序列執行&lt;/li>
&lt;li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）&lt;/li>
&lt;li>衝突時會 serialization failure、應用層必須 retry&lt;/li>
&lt;li>適合：金融交易、ticketing inventory、需要絕對正確&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致&lt;/li>
&lt;li>全球分散式系統的特殊取捨&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> 的 Spanner TrueTime 段&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>選擇原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>90% 業務用 Read Committed 夠&lt;/li>
&lt;li>報表 / 對帳用 Repeatable Read&lt;/li>
&lt;li>金融交易 / inventory 用 Serializable&lt;/li>
&lt;li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統&lt;/li>
&lt;/ul>
&lt;h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a> 的責任是定義交易彼此可見性。&lt;code>Read Committed&lt;/code> 在高併發寫入下可維持一般業務一致性；&lt;code>Repeatable Read&lt;/code> 與 &lt;code>Serializable&lt;/code> 提供更強約束、同時提高鎖競爭與重試頻率。&lt;/p></description><content:encoded><![CDATA[<p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。</p>
<p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。</p>
<h2 id="邊界先於語法">邊界先於語法</h2>
<p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。</p>
<p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。</p>
<h2 id="isolation-level-五級深度">Isolation Level 五級深度</h2>
<p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 <em>正確性 vs 性能</em> 之間做取捨。</p>
<p><strong>0. Read Uncommitted（dirty read 可能）</strong>：</p>
<ul>
<li>可讀到別的 transaction 還沒 commit 的資料</li>
<li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）</li>
<li>實務不要用</li>
</ul>
<p><strong>1. Read Committed（PostgreSQL / Oracle 預設）</strong>：</p>
<ul>
<li>只讀到 commit 的資料</li>
<li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）</li>
<li>適合：read-heavy workload、不要求同 transaction 內 read consistency</li>
</ul>
<p><strong>2. Repeatable Read（MySQL InnoDB 預設）</strong>：</p>
<ul>
<li>同 transaction 內 read 一致（snapshot at transaction start）</li>
<li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了</li>
<li>適合：報表類 transaction、需要 snapshot 一致性</li>
</ul>
<p><strong>3. Serializable（最強）</strong>：</p>
<ul>
<li>看起來像所有 transaction 序列執行</li>
<li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）</li>
<li>衝突時會 serialization failure、應用層必須 retry</li>
<li>適合：金融交易、ticketing inventory、需要絕對正確</li>
</ul>
<p><strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）</strong>：</p>
<ul>
<li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致</li>
<li>全球分散式系統的特殊取捨</li>
<li>詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 Spanner TrueTime 段</li>
<li>詳見 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case</a></li>
</ul>
<p><strong>選擇原則</strong>：</p>
<ul>
<li>90% 業務用 Read Committed 夠</li>
<li>報表 / 對帳用 Repeatable Read</li>
<li>金融交易 / inventory 用 Serializable</li>
<li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統</li>
</ul>
<h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係</h2>
<p><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 的責任是定義交易彼此可見性。<code>Read Committed</code> 在高併發寫入下可維持一般業務一致性；<code>Repeatable Read</code> 與 <code>Serializable</code> 提供更強約束、同時提高鎖競爭與重試頻率。</p>
<p>併發交易的常見結果是 deadlock 或 serialization failure。這些結果代表資料庫在保護一致性、應用層需要把它視為可重試路徑：</p>
<ul>
<li><strong>重試次數有上限</strong>（通常 3-5 次）— 避免 retry storm</li>
<li><strong>重試間隔有抖動</strong>（exponential backoff + jitter）— 避免同步衝突</li>
<li><strong>重試前提是動作可重入</strong>（idempotent）— 不會放大副作用</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a> 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> 卡片。</p>
<h2 id="optimistic-vs-pessimistic-locking">Optimistic vs Pessimistic Locking</h2>
<p>當多個 transaction 同時操作同一筆資料、有兩種防衝突策略：</p>
<p><strong>Pessimistic locking（悲觀鎖）</strong>：</p>
<ul>
<li><code>SELECT ... FOR UPDATE</code>、提前 lock 行</li>
<li>適合：衝突機率高、retry 成本高</li>
<li>缺點：lock 期間其他 transaction 等待、容易 deadlock</li>
</ul>
<p><strong>Optimistic locking（樂觀鎖）</strong>：</p>
<ul>
<li>不 lock、用 version column 或 <code>WHERE old_value = ?</code></li>
<li>commit 時若 version 不對、整個 transaction 失敗、應用層 retry</li>
<li>適合：衝突機率低、性能優先</li>
<li>缺點：高衝突場景 retry 多、整體吞吐反而低</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>衝突 &lt; 5% → optimistic（更高吞吐）</li>
<li>衝突 &gt; 30% → pessimistic（避免 retry waste）</li>
<li>中間區 → 量測再決定</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">hot row contention 處理</a>（<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a>）— 高衝突 hot row 通常該換 KV / cache、不該硬擴 SQL。</p>
<h2 id="服務情境checkout-多層邊界">服務情境：Checkout 多層邊界</h2>
<p>電商 checkout 是典型的 transaction boundary 設計題、可拆成兩層邊界。</p>
<p><strong>第一層：交易層（即時一致）</strong>：</p>
<ul>
<li>建立訂單主表</li>
<li>寫入訂單項目</li>
<li>扣減可售庫存</li>
<li>寫入付款待確認狀態</li>
</ul>
<p><strong>第二層：延伸層（最終可達）</strong>：</p>
<ul>
<li>寄訂單確認 email</li>
<li>同步 CRM 系統</li>
<li>觸發 analytics event</li>
<li>更新推薦模型</li>
</ul>
<p>這種切法讓交易控制面跟非同步控制面各自穩定：</p>
<ul>
<li>交易層關注 <em>鎖、隔離與回退</em></li>
<li>非同步層關注 <em>投遞、重試與補償</em></li>
</ul>
<p>對應案例：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> — 體育博彩 ledger、200 個獨立 cluster 處理 transaction、後續 settlement 跑非同步</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨市場銀行 transaction、各市場獨立、跨市場結算非同步</li>
</ul>
<h2 id="distributed-transaction2pc-vs-saga">Distributed Transaction：2PC vs Saga</h2>
<p>當業務動作跨越 <em>多個服務 / 資料庫</em>、傳統 ACID transaction 不夠用、需要 distributed transaction 模式。</p>
<p><strong>Two-Phase Commit (2PC)</strong>：</p>
<ul>
<li>階段 1：coordinator 詢問所有 participant「你能 commit 嗎？」</li>
<li>階段 2：所有都說 yes → coordinator 廣播 commit；任一說 no → 廣播 abort</li>
<li><strong>優點</strong>：強一致、ACID 保證</li>
<li><strong>缺點</strong>：coordinator failure 會 block 所有 participant、性能差、跨服務複雜</li>
<li>適合：少數高一致性需求的場景（金融交易、跨多 DB 一致性）</li>
</ul>
<p><strong>Saga Pattern</strong>：</p>
<ul>
<li>把長 transaction 拆成多個 local transaction + compensating transaction</li>
<li>每個 step 成功 → 進下個；任一失敗 → 倒回去跑 compensation</li>
<li>例：訂單 step1 扣庫存、step2 收款、step3 送貨。step2 失敗 → 跑 step1 的 compensation（補庫存）</li>
<li><strong>優點</strong>：高可用、性能好、容易擴展</li>
<li><strong>缺點</strong>：不是強一致、中間狀態可見、compensation 必須設計</li>
<li>適合：multi-service 業務流程、可接受 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
</ul>
<p><strong>Choreography vs Orchestration</strong>：</p>
<ul>
<li>Choreography：每個 service 自己決定下一步（event-driven）</li>
<li>Orchestration：中央 orchestrator 控制流程（state machine）</li>
<li>大規模傾向 orchestration（容易追蹤、debug）、小規模 choreography 足夠</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 + 付款分開：DynamoDB 接搶單（local transaction）、legacy server 跑付款（compensation 處理庫存回退）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 投注 → 結算的 saga 流程</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern 卡片</a> 跟 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 Outbox Pattern</a>。</p>
<h2 id="跨-region-transactioncap-取捨">跨 Region Transaction：<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 取捨</h2>
<p>當 transaction 必須跨 region 同時成立、CAP 定理開始作用。</p>
<p><strong>Single-region transaction</strong>（PostgreSQL / MySQL / Aurora）：</p>
<ul>
<li>ACID within region</li>
<li>跨 region 用 async replication、不是 transaction</li>
</ul>
<p><strong>Multi-region eventual consistency</strong>（DynamoDB Global Tables、Cosmos DB session/eventual）：</p>
<ul>
<li>各 region 都能寫</li>
<li>LWW 或 application-level conflict resolution</li>
<li>不是 ACID、是 BASE</li>
</ul>
<p><strong>Multi-region strong consistency</strong>（Spanner、Aurora DSQL、CockroachDB）：</p>
<ul>
<li>跨 region linearizable transaction</li>
<li>代價是 latency（跨洲 100-200ms <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>）</li>
<li>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
</ul>
<p><strong>決策邏輯</strong>：</p>
<ul>
<li>業務不需要跨 region 強一致 → single-region OLTP + eventual replication</li>
<li>需要跨 region 強一致 + 接受 latency → Spanner / Aurora DSQL</li>
<li>需要跨 region 寫但接受最終一致 → Cosmos DB session / DynamoDB Global Tables</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>deadlock rate 升高</td>
          <td>交易範圍過大或鎖順序不一致</td>
          <td>統一更新順序、縮小 transaction 範圍</td>
      </tr>
      <tr>
          <td>transaction duration 在尖峰時段上升</td>
          <td>交易內含慢查詢或外部依賴</td>
          <td>將外部呼叫移出交易、補索引與查詢計畫</td>
      </tr>
      <tr>
          <td>retry 成功率下降</td>
          <td>重試條件與業務冪等假設不一致</td>
          <td>補 idempotency key、調整 retry 邏輯</td>
      </tr>
      <tr>
          <td>rollback 後仍出現業務狀態殘留</td>
          <td>邊界切分和副作用落點未對齊</td>
          <td>將副作用統一移到 outbox / consumer 路徑</td>
      </tr>
      <tr>
          <td>交易內讀寫跨多資料域導致 contention 爆發</td>
          <td>業務聚合邊界與資料模型邊界衝突</td>
          <td>重新切 aggregate 與拆分熱點資料結構</td>
      </tr>
      <tr>
          <td>Serializable retry 率 &gt; 10%</td>
          <td>isolation 太嚴或業務衝突高</td>
          <td>降到 Repeatable Read 或拆 hot row</td>
      </tr>
      <tr>
          <td>跨服務 transaction 用 2PC 卡住</td>
          <td>coordinator failure 阻塞</td>
          <td>改 Saga + compensation</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>交易保護的是一致性、不是吞吐量最大化。把過多步驟包進單一交易、會同時放大鎖競爭與回退成本。把交易切成可驗證的業務單位、能讓高併發下的可預期性更高。</p>
<p>重試保護的是暫時性失敗、不是所有失敗。沒有冪等保護的重試會放大副作用、特別是金流、庫存、配額這類正式狀態。</p>
<p>isolation level 不是「越強越好」。Serializable 比 Read Committed 慢數倍、且 retry rate 上升。只在 <em>必要</em> 場景用最強 isolation、其他場景用最低可接受 isolation。</p>
<p>distributed transaction 不是「跨服務就要 2PC」。多數 multi-service 業務用 Saga 更可靠、2PC 是少數場景的特殊工具。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>Transaction 相關重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>Aurora MySQL ACID transaction、200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>External consistency（linearizable）跨 region transaction、TrueTime</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場 transaction 各市場獨立 cluster、合規限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>搶票 + 付款 saga 模式、DynamoDB queue + legacy SQL</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>交易邊界可用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 做回寫。先看事件中的主從切換與恢復順序、再回到本章判讀三件事：哪些變更必須同交易成功、哪些副作用應拆到 outbox、哪些錯誤屬於可重試而非立即回退。</p>
<p>這個案例主要支撐的是「提交與副作用切分」判讀、不直接支撐 schema naming 或 cache freshness；若問題落在資料命名或快取新鮮度、應回到 1.2 或 2.x。</p>
<p>若事件出現資料已寫入但外部流程落後、或重試後副作用重複、先收斂本章的邊界切分與重試前提、再同步更新 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> 與 <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>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>交易邊界設計會直接影響後續模組的可操作性。</p>
<ol>
<li>與 03 的交接：交易外副作用透過 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 與 consumer 落地。</li>
<li>與 1.7 的交接：付款狀態拆欄位、雙寫與回呼更新要進入 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據</a> 的驗證流程。</li>
<li>與 1.10 / 1.11 的交接：KV 跟全球分散式 OLTP 的 transaction model 不同、選型時要回到本章邊界判讀。</li>
<li>與 04 的交接：交易失敗需要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 的查詢與證據欄位。</li>
<li>與 06 的交接：高風險交易變更納入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a> 與 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>。</li>
<li>與 08 的交接：交易層回退或 fail-forward 判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>（connection pool / hot row）</li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> / <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>跨模組：<a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> / <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>卡片：<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">Transaction Boundary</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> / <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern</a> / <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a></li>
<li>Spanner 一致性深入：<a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">TrueTime API 深入</a> / <a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">Spanner 一致性模型對照</a></li>
<li>CockroachDB retry / 隔離深入：<a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a> / <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
<li>Aurora 寫入語意深入：<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">Aurora 儲存層架構</a>（6 寫 / 4 讀 quorum 對 transaction 的影響）</li>
</ul>
]]></content:encoded></item><item><title>MongoDB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/</guid><description>&lt;p>MongoDB 是 document database 的事實標準。schema flexibility、aggregation pipeline、跨雲 managed（Atlas）讓它成為許多 startup 的 default 選擇。Microsoft 365、Disney+ 早期、Uber 等大規模平台都從 MongoDB 起家，後來依 workload 壓力把部分路徑遷移到 KV / 雲商專屬服務（Cosmos DB、DynamoDB）。&lt;/p>
&lt;h2 id="教學路線document-shape-與-schema-governance">教學路線：Document shape 與 schema governance&lt;/h2>
&lt;p>MongoDB 服務頁的教學目標是把 document model、schema flexibility、index、aggregation pipeline 與 sharding 放回資料形狀治理。讀者讀完後要能判斷資料是否適合 aggregate root，並知道 schema governance 如何影響長期維護成本。&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>Document shape&lt;/td>
 &lt;td>哪些資料適合 aggregate root 與 nested document&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema governance&lt;/td>
 &lt;td>schema flexibility 如何搭配 validation、版本與 migration&lt;/td>
 &lt;td>容量規劃要點、預計實作話題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query / index&lt;/td>
 &lt;td>index、aggregation pipeline、ad-hoc query 如何影響成本&lt;/td>
 &lt;td>容量特性、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sharding&lt;/td>
 &lt;td>shard key、chunk、balancer 如何把資料形狀變容量問題&lt;/td>
 &lt;td>容量規劃要點、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時轉 PostgreSQL、DynamoDB、Cosmos DB 或 search&lt;/td>
 &lt;td>不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位json-document--跨雲彈性">定位：JSON document + 跨雲彈性&lt;/h2>
&lt;p>MongoDB 是以 document model 為主體的 DB。PostgreSQL JSONB 適合「SQL 為主、少量半結構化欄位」；MongoDB 則把 BSON document、aggregation pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a> 與 schema governance 放在核心設計裡。近年版本加入 time series、change streams、queryable encryption、CSFLE 等能力。&lt;/p>
&lt;p>選 MongoDB 的核心訴求：document model 是主要 use case、需要跨雲 managed（Atlas）、想避免 vendor lock-in（也可自管）。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>&lt;strong>單一 instance 吞吐&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一般 m5.4xlarge：5K-15K WPS（依 doc size、index）&lt;/li>
&lt;li>高階 instance + tuning：30K-50K WPS&lt;/li>
&lt;li>超過此級別 → sharding&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Sharding&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>MongoDB 原生支援 sharded cluster&lt;/li>
&lt;li>mongos router + config servers + shard&lt;/li>
&lt;li>MongoDB sharding 要主動設計 shard key，並和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition&lt;/a> 風險一起看&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Replication&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Replica set（primary + secondary、async）&lt;/li>
&lt;li>跨 region 通常 async&lt;/li>
&lt;li>自動 failover &amp;lt; 30 秒（mongod 內建）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Storage&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>單一 collection 沒有官方上限、但 shard key resharding 過去版本是大手術（4.4+ 支援 reshardCollection）&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. Document model 主要 workload&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>MongoDB 是 document database 的事實標準。schema flexibility、aggregation pipeline、跨雲 managed（Atlas）讓它成為許多 startup 的 default 選擇。Microsoft 365、Disney+ 早期、Uber 等大規模平台都從 MongoDB 起家，後來依 workload 壓力把部分路徑遷移到 KV / 雲商專屬服務（Cosmos DB、DynamoDB）。</p>
<h2 id="教學路線document-shape-與-schema-governance">教學路線：Document shape 與 schema governance</h2>
<p>MongoDB 服務頁的教學目標是把 document model、schema flexibility、index、aggregation pipeline 與 sharding 放回資料形狀治理。讀者讀完後要能判斷資料是否適合 aggregate root，並知道 schema governance 如何影響長期維護成本。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Document shape</td>
          <td>哪些資料適合 aggregate root 與 nested document</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Schema governance</td>
          <td>schema flexibility 如何搭配 validation、版本與 migration</td>
          <td>容量規劃要點、預計實作話題</td>
      </tr>
      <tr>
          <td>Query / index</td>
          <td>index、aggregation pipeline、ad-hoc query 如何影響成本</td>
          <td>容量特性、常見陷阱</td>
      </tr>
      <tr>
          <td>Sharding</td>
          <td>shard key、chunk、balancer 如何把資料形狀變容量問題</td>
          <td>容量規劃要點、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a></td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時轉 PostgreSQL、DynamoDB、Cosmos DB 或 search</td>
          <td>不適用場景、跟其他 vendor 的取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="定位json-document--跨雲彈性">定位：JSON document + 跨雲彈性</h2>
<p>MongoDB 是以 document model 為主體的 DB。PostgreSQL JSONB 適合「SQL 為主、少量半結構化欄位」；MongoDB 則把 BSON document、aggregation pipeline、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 與 schema governance 放在核心設計裡。近年版本加入 time series、change streams、queryable encryption、CSFLE 等能力。</p>
<p>選 MongoDB 的核心訴求：document model 是主要 use case、需要跨雲 managed（Atlas）、想避免 vendor lock-in（也可自管）。</p>
<h2 id="容量特性">容量特性</h2>
<p><strong>單一 instance 吞吐</strong>：</p>
<ul>
<li>一般 m5.4xlarge：5K-15K WPS（依 doc size、index）</li>
<li>高階 instance + tuning：30K-50K WPS</li>
<li>超過此級別 → sharding</li>
</ul>
<p><strong>Sharding</strong>：</p>
<ul>
<li>MongoDB 原生支援 sharded cluster</li>
<li>mongos router + config servers + shard</li>
<li>MongoDB sharding 要主動設計 shard key，並和 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a> 風險一起看</li>
</ul>
<p><strong>Replication</strong>：</p>
<ul>
<li>Replica set（primary + secondary、async）</li>
<li>跨 region 通常 async</li>
<li>自動 failover &lt; 30 秒（mongod 內建）</li>
</ul>
<p><strong>Storage</strong>：</p>
<ul>
<li>單一 collection 沒有官方上限、但 shard key resharding 過去版本是大手術（4.4+ 支援 reshardCollection）</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. Document model 主要 workload</strong>：</p>
<ul>
<li>schema 變化頻繁的早期產品</li>
<li>nested document 自然表達領域模型（訂單含多個 item、用戶含多個 preference）</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 遷移到 Cosmos DB MongoDB API、保留 document model</li>
</ul>
<p><strong>2. Aggregation pipeline 重 workload</strong>：</p>
<ul>
<li>複雜的 $group / $match / $project chain</li>
<li>報表、analytics、ETL prep</li>
<li>比 RDBMS 寫複雜 query 更直觀（對某些 team）</li>
</ul>
<p><strong>3. 跨雲 managed（Atlas）</strong>：</p>
<ul>
<li>MongoDB Atlas 跨 AWS / GCP / Azure</li>
<li>跟 DynamoDB（AWS only）、Cosmos DB（Azure only）、Spanner（GCP only）相反</li>
<li>適合多雲策略、避免單一 vendor lock-in</li>
</ul>
<p><strong>4. Time series workload（6.0+）</strong>：</p>
<ul>
<li>time series collection 專屬優化</li>
<li>不過 InfluxDB / TimescaleDB 仍是更專業選擇</li>
</ul>
<p><strong>5. 已有 MongoDB 生態 + 想轉移操作責任</strong>：</p>
<ul>
<li>Atlas 提供 backup、failover、monitoring、auto-scale</li>
<li>想把 MongoDB DBA / SRE 操作責任交給 Atlas</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 強 ACID multi-document transaction</strong>：</p>
<ul>
<li>MongoDB Transaction 支援多 document、但跨 shard 有性能影響</li>
<li>高頻金融交易仍建議 SQL 系統</li>
<li>替代：PostgreSQL、Aurora、Spanner</li>
</ul>
<p><strong>2. 複雜 JOIN</strong>：</p>
<ul>
<li>MongoDB <code>$lookup</code> 適合少量相鄰資料，JOIN-heavy workload 應回 SQL 系統</li>
<li>schema design 階段要把常用讀取路徑 denormalize 成 document shape</li>
<li>替代：SQL 系統做 JOIN-heavy workload</li>
</ul>
<p><strong>3. 純 KV + sub-ms latency</strong>：</p>
<ul>
<li>MongoDB document model 比 KV 多一層 BSON parsing</li>
<li>替代：Redis、DynamoDB、Bigtable</li>
</ul>
<p><strong>4. 大規模 OLAP</strong>：</p>
<ul>
<li>aggregation 對中等資料量還行、TB 級不適合</li>
<li>替代：ClickHouse、BigQuery、Spark on Delta Lake</li>
</ul>
<p><strong>5. 嚴格資料模型 + schema enforcement</strong>：</p>
<ul>
<li>MongoDB schema flexibility 可能導致 production data inconsistency</li>
<li>替代：SQL DB（schema 強制）+ JSONB column 處理半結構化</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs Cosmos DB MongoDB API</strong>：</p>
<ul>
<li>MongoDB Atlas：跨雲、原生 MongoDB 行為</li>
<li>Cosmos DB MongoDB API：Azure-only、global distribution + 5 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>s</li>
<li>選 MongoDB Atlas：跨雲、需要原生 MongoDB features</li>
<li>選 Cosmos DB：Azure 生態、需要更好 global distribution</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 遷到 Cosmos DB MongoDB API，主要保留 document model</li>
</ul>
<p><strong>vs DynamoDB</strong>：</p>
<ul>
<li>MongoDB：document model、aggregation 強、跨雲</li>
<li>DynamoDB：KV / single-table design、AWS 整合、5 個 9 SLA</li>
<li>選 MongoDB：document 為主、跨雲</li>
<li>選 DynamoDB：KV 為主、AWS 生態</li>
<li>詳見 <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 vendor page</a> 對比段</li>
</ul>
<p><strong>vs PostgreSQL JSONB</strong>：</p>
<ul>
<li>MongoDB：document 為主、schema-less</li>
<li>PostgreSQL：SQL 為主、JSONB 補充</li>
<li>選 MongoDB：document 占主要 schema</li>
<li>選 PostgreSQL JSONB：主要結構化、少量半結構化欄位</li>
</ul>
<p><strong>vs Couchbase / Couchdb / Firestore</strong>：</p>
<ul>
<li>Couchbase：MongoDB 替代、有 N1QL（SQL-like）</li>
<li>CouchDB：偏小規模、master-master replication</li>
<li>Firestore：GCP-only、realtime updates</li>
<li>MongoDB 在這群裡是生態最廣的</li>
</ul>
<p><strong>vs Elasticsearch 作為 search 替代</strong>：</p>
<ul>
<li>兩者分屬不同類別：MongoDB 是 OLTP / document、Elasticsearch 是 search + analytics</li>
<li>通常搭配用：MongoDB 主、Elasticsearch 處理 full-text search</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. Shard key 設計是命脈</strong>：</p>
<ul>
<li>跟 DynamoDB partition key 同樣關鍵</li>
<li>不均勻 → hot shard、實際容量達不到名義</li>
<li>4.4+ 可以 reshard、但仍是大手術</li>
</ul>
<p><strong>2. Replica set 是 HA 基礎</strong>：</p>
<ul>
<li>至少 3 個 member（1 primary + 2 secondary）</li>
<li>secondary 可 read（read preference）但要注意 lag</li>
<li>failover 通常 &lt; 30 秒</li>
</ul>
<p><strong>3. Atlas managed 服務</strong>：</p>
<ul>
<li>提供 auto-scaling、auto-backup、跨雲部署</li>
<li>Tier 從 M0（free）到 M700（高階）</li>
<li>Atlas Online Archive 自動把舊資料移到便宜 storage</li>
</ul>
<p><strong>4. Index 限制</strong>：</p>
<ul>
<li>單 collection 最多 64 個 index</li>
<li>compound index 有順序敏感（{a:1, b:1} 跟 {b:1, a:1} 不同）</li>
<li>TTL index 自動 expire 過期 document</li>
</ul>
<p><strong>5. Change streams（CDC）</strong>：</p>
<ul>
<li>4.0+ 提供原生 change streams</li>
<li>對接 Kafka / event bus 做 event sourcing</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>MongoDB 的 schema flexibility 會降低早期建模成本，也會把 schema governance 延後到 production。這一段先說何時維持 document model，再說何時升級 Atlas、sharding、Cosmos DB、DynamoDB 或 SQL。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 replica set</td>
          <td>document size 穩定、working set 可控、primary 寫入足夠</td>
          <td>storage / write / working set 接近上限、failover 演練不足</td>
          <td><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></td>
      </tr>
      <tr>
          <td>Atlas managed</td>
          <td>團隊仍能管理 backup、upgrade、monitoring 與 scaling</td>
          <td>DBA / SRE 責任想轉交平台、跨雲部署與 backup 成為主要壓力</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a></td>
      </tr>
      <tr>
          <td>Sharded cluster</td>
          <td>single replica set 還能承擔容量與維護窗口</td>
          <td>shard key 穩定、tenant / user / region 可分、hot shard 可觀測</td>
          <td><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></td>
      </tr>
      <tr>
          <td>Cosmos DB MongoDB API</td>
          <td>Azure 只是部署選項，原生 MongoDB 行為仍重要</td>
          <td>Azure global distribution、multi-region write 或 RU governance 成主題</td>
          <td><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a></td>
      </tr>
      <tr>
          <td>DynamoDB / KV</td>
          <td>query 仍需要 document traversal 與 aggregation</td>
          <td>access pattern 固定、sub-10ms p99、connection-free scaling 成主題</td>
          <td><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 vendor</a></td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>document 是主要資料形狀</td>
          <td>JOIN-heavy、transaction-heavy、schema 約束是主要價值</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></td>
      </tr>
  </tbody>
</table>
<p>MongoDB 的簡單路徑是先把 document boundary 寫清楚。資料可以彈性演進，但 application 仍要知道哪些欄位是正式契約、哪些欄位只是相容期，並用 validation、migration 與 data quality check 管住版本漂移。</p>
<p>Sharding 的升級路徑要等 shard key 與 query shape 足夠穩定。過早切 shard 會把 aggregation、transaction 與 index 成本提前放大；過晚切 shard 則會讓 resharding、chunk migration 與 balancer 壓力進入 production 高峰期。</p>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本批 6 篇 deep article 已完成、覆蓋 MongoDB 從 schema 設計到 production 跨層架構的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema contract 該放 DB 層 validator 還是 app 層 abstraction</td>
          <td><a href="schema-design-pattern/">schema-design-pattern</a></td>
          <td>Toyota polymorphic governance、Forbes abstraction layer</td>
      </tr>
      <tr>
          <td>Shard key 選型 + 單 cluster vs 多 cluster blast radius</td>
          <td><a href="shard-key-selection/">shard-key-selection</a></td>
          <td>Toyota 20 DB blast radius、跟 DynamoDB 可逆性對比</td>
      </tr>
      <tr>
          <td>Read preference + causal session 跟 cache 層 freshness token</td>
          <td><a href="replica-set-read-preference/">replica-set-read-preference</a></td>
          <td>DB 層 + cache 層讀後一致性兩層合用</td>
      </tr>
      <tr>
          <td>Aggregation pipeline 順序 / index / memory boundary</td>
          <td><a href="aggregation-pipeline-optimization/">aggregation-pipeline-optimization</a></td>
          <td>report dashboard 跑爆 primary 的 anti-pattern 治理</td>
      </tr>
      <tr>
          <td>Change streams resume token + Kafka connector 治理</td>
          <td><a href="change-streams-kafka/">change-streams-kafka</a></td>
          <td>at-least-once 語義 + idempotency + resume token 過期防護</td>
      </tr>
      <tr>
          <td>Driver × deployment × cache × predictive scaling 三層協作</td>
          <td><a href="connection-management-and-cache-layer/">connection-management-and-cache-layer</a></td>
          <td>Coinbase mongobetween + freshness token + ML 預測擴容三件套</td>
      </tr>
  </tbody>
</table>
<p>跨 vendor entry：先看 <a href="../db3-vendor-selection/">DB3 vendor selection</a>（MongoDB / DynamoDB / Cosmos DB 三方選型 + workload shape 前置判讀），再進本 vendor 的 deep article。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>Index 設計跟覆蓋</li>
<li>從自管 MongoDB 遷到 Atlas</li>
<li>從 MongoDB 遷到 Cosmos DB MongoDB API（保留 document model）</li>
<li>從 MongoDB 遷到 DynamoDB（access pattern 需要重設計）</li>
<li>Queryable encryption（CSFLE）</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 MongoDB 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a></td>
          <td>從 MongoDB 遷到 Cosmos DB MongoDB API、planet-scale analytics</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a></td>
          <td>MongoDB 為主資料層、自建 mongobetween 解決 Ruby 連線爆炸、users 服務 1.5M reads/sec</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a></td>
          <td>自管 MongoDB → Atlas on GCP、6 個月遷完、build 25→9 分鐘、120M MAU</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a></td>
          <td>Atlas 撐 900 萬車 telematics、月 180 億 transaction、緊急訊號 3 秒內到 agent</td>
      </tr>
  </tbody>
</table>
<p>MongoDB case 的讀法分三組：</p>
<ul>
<li><strong>作為 production 主角持續演進</strong>（Coinbase、Toyota Connected）：document model 撐住核心 OLTP / IoT、配 connection proxy / cache / event-driven 處理擴展周邊。</li>
<li><strong>自管 → managed 遷移</strong>（Forbes）：同 document model、換託管模式、ROI 集中在 DBA 責任轉移跟跨雲彈性、不是性能改善。</li>
<li><strong>遷出 MongoDB 保留 API</strong>（Microsoft 365）：document model 保留、底層換到 Cosmos DB MongoDB API、換取 Azure global distribution。</li>
</ul>
<p>讀 case 時要區分 MongoDB 在「主角 / 遷入 / 遷出」三種位置的差異，三種位置揭露的工程議題完全不同。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>schema 長期 schema-less</strong>：production 出現 data inconsistency、難 query</li>
<li><strong>shard key 用 _id（自增）</strong>：寫入全集中在最後一個 shard</li>
<li><strong>$lookup 過度使用</strong>：跨 collection JOIN-heavy workload 應在 schema design 時 denormalize 或回 SQL</li>
<li><strong>index 太多</strong>：寫吞吐被拖垮、定期 review 未用 index</li>
<li><strong>secondary read 不檢查 lag</strong>：用戶讀到 stale data</li>
<li><strong>不規劃 Atlas tier upgrade 路徑</strong>：流量上來才發現 tier 跟不上、緊急升級費用高</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a>（MongoDB API replacement）、<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 vendor</a>（KV alternative）</li>
<li>上游：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（MongoDB 遷出範例）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（shard key 跟 hot shard）</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/">MongoDB Manual</a>、<a href="https://www.mongodb.com/atlas">MongoDB Atlas</a></li>
</ul>
]]></content:encoded></item><item><title>9.3 壓測工具選型</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>壓測工具選型的核心不是「哪個工具最強」、是「哪個工具最貼合本團隊的 workload model 表達能力跟 CI 整合需求」。沒有絕對最好的工具、只有最匹配當前場景的工具。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 的關係：9.2 定義 workload 長什麼樣、9.3 找能複製這個樣子的工具。工具選對、壓測結果可信；工具選錯、壓測結果誤導。&lt;/p>
&lt;p>本章不是工具教學、是 &lt;em>選型維度&lt;/em> + 主流工具的 &lt;em>適用情境&lt;/em>。讀者讀完後能回答「我現在這個 workload 該用哪個工具」、而不是「哪個工具最快」。&lt;/p>
&lt;h2 id="六個選型維度">六個選型維度&lt;/h2>
&lt;p>選工具時要按六個維度評估、不能只看「能不能跑 HTTP GET」。&lt;/p>
&lt;p>&lt;strong>腳本表達能力&lt;/strong>：能不能寫複雜 user journey（登入 → 瀏覽 → 加購物車 → 結帳）、不只是單一 HTTP request。複雜系統的壓測通常是 user journey 級別、單一 endpoint 壓測只能找絕對極限、找不到 cross-endpoint contention。&lt;/p>
&lt;p>&lt;strong>協議支援&lt;/strong>：HTTP / WebSocket / gRPC / TCP / 自家二進位協議。WebSocket 跟 gRPC 是現代後端常見、傳統工具（JMeter、wrk）可能要 plugin 補。&lt;/p>
&lt;p>&lt;strong>規模能力&lt;/strong>：單機可以發多少 RPS、能不能分散式擴容。本機 wrk 可發 10K-50K RPS；分散式 Locust 可發 1M+ RPS。決定因素：CPU 效率、async I/O 模型、是否單機 bound。&lt;/p>
&lt;p>&lt;strong>CI 整合&lt;/strong>：能不能在 PR 上跑 lightweight perf check、結果能不能機器可讀（JSON / Prometheus exposition）、能不能跟 baseline diff。沒有 CI 整合的工具只能做「事件型壓測」、無法做 continuous perf governance。&lt;/p>
&lt;p>&lt;strong>結果分析&lt;/strong>：原生 dashboard（k6 Cloud、Gatling Enterprise）/ Prometheus + Grafana 整合 / 純文字輸出。要看結果分發、團隊成員能不能輕鬆查詢歷史。&lt;/p>
&lt;p>&lt;strong>學習曲線&lt;/strong>：腳本語言（JavaScript / Scala / Python / Go）、團隊熟悉度。工具好但團隊不會用、會變成 1-2 個工程師的孤島技能、流失時整套廢掉。&lt;/p>
&lt;h2 id="主流開源工具對照">主流開源工具對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>腳本&lt;/th>
 &lt;th>規模&lt;/th>
 &lt;th>學習曲線&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>k6&lt;/td>
 &lt;td>JS&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低-中&lt;/td>
 &lt;td>複雜 user journey + CI 整合、現代工具首選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JMeter&lt;/td>
 &lt;td>XML/GUI&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;td>企業已有流程、protocol 廣、reluctant 改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gatling&lt;/td>
 &lt;td>Scala&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>報表精美、Scala 學習門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Locust&lt;/td>
 &lt;td>Python&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>複雜邏輯、Python 生態、單機 throughput 受限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vegeta&lt;/td>
 &lt;td>CLI&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>CLI driven、quick HTTP 壓測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>wrk/wrk2&lt;/td>
 &lt;td>C&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>單機極限 RPS、saturation discovery 用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>k6&lt;/strong> 是過去 5 年崛起的綜合首選。JavaScript 腳本（前端工程師也能寫）、原生 dashboard、Prometheus exposition、CI 友善。Grafana 收購後生態加速。缺點：複雜 stateful 場景（DB connection pool 共享）需要繞 workaround。&lt;/p>
&lt;p>&lt;strong>JMeter&lt;/strong> 是企業常見的 incumbent。協議支援廣（含 LDAP、JDBC、JMS）、有 GUI 編輯器。缺點：腳本是 XML、版本控制困難；GUI 主要用來生成腳本、實際跑壓測還是要 headless。已經在用的團隊建議繼續、新團隊不必特意選它。&lt;/p>
&lt;p>&lt;strong>Gatling&lt;/strong> 高 throughput 純 async、性能優秀、報表精美。缺點：Scala / Kotlin DSL 學習曲線陡、新版本（11+）改了 DSL 不向後相容。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>壓測工具選型的核心不是「哪個工具最強」、是「哪個工具最貼合本團隊的 workload model 表達能力跟 CI 整合需求」。沒有絕對最好的工具、只有最匹配當前場景的工具。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 的關係：9.2 定義 workload 長什麼樣、9.3 找能複製這個樣子的工具。工具選對、壓測結果可信；工具選錯、壓測結果誤導。</p>
<p>本章不是工具教學、是 <em>選型維度</em> + 主流工具的 <em>適用情境</em>。讀者讀完後能回答「我現在這個 workload 該用哪個工具」、而不是「哪個工具最快」。</p>
<h2 id="六個選型維度">六個選型維度</h2>
<p>選工具時要按六個維度評估、不能只看「能不能跑 HTTP GET」。</p>
<p><strong>腳本表達能力</strong>：能不能寫複雜 user journey（登入 → 瀏覽 → 加購物車 → 結帳）、不只是單一 HTTP request。複雜系統的壓測通常是 user journey 級別、單一 endpoint 壓測只能找絕對極限、找不到 cross-endpoint contention。</p>
<p><strong>協議支援</strong>：HTTP / WebSocket / gRPC / TCP / 自家二進位協議。WebSocket 跟 gRPC 是現代後端常見、傳統工具（JMeter、wrk）可能要 plugin 補。</p>
<p><strong>規模能力</strong>：單機可以發多少 RPS、能不能分散式擴容。本機 wrk 可發 10K-50K RPS；分散式 Locust 可發 1M+ RPS。決定因素：CPU 效率、async I/O 模型、是否單機 bound。</p>
<p><strong>CI 整合</strong>：能不能在 PR 上跑 lightweight perf check、結果能不能機器可讀（JSON / Prometheus exposition）、能不能跟 baseline diff。沒有 CI 整合的工具只能做「事件型壓測」、無法做 continuous perf governance。</p>
<p><strong>結果分析</strong>：原生 dashboard（k6 Cloud、Gatling Enterprise）/ Prometheus + Grafana 整合 / 純文字輸出。要看結果分發、團隊成員能不能輕鬆查詢歷史。</p>
<p><strong>學習曲線</strong>：腳本語言（JavaScript / Scala / Python / Go）、團隊熟悉度。工具好但團隊不會用、會變成 1-2 個工程師的孤島技能、流失時整套廢掉。</p>
<h2 id="主流開源工具對照">主流開源工具對照</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>腳本</th>
          <th>規模</th>
          <th>學習曲線</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>k6</td>
          <td>JS</td>
          <td>中</td>
          <td>低-中</td>
          <td>複雜 user journey + CI 整合、現代工具首選</td>
      </tr>
      <tr>
          <td>JMeter</td>
          <td>XML/GUI</td>
          <td>中</td>
          <td>中-高</td>
          <td>企業已有流程、protocol 廣、reluctant 改</td>
      </tr>
      <tr>
          <td>Gatling</td>
          <td>Scala</td>
          <td>高</td>
          <td>高</td>
          <td>報表精美、Scala 學習門檻</td>
      </tr>
      <tr>
          <td>Locust</td>
          <td>Python</td>
          <td>高</td>
          <td>中</td>
          <td>複雜邏輯、Python 生態、單機 throughput 受限</td>
      </tr>
      <tr>
          <td>Vegeta</td>
          <td>CLI</td>
          <td>中</td>
          <td>低</td>
          <td>CLI driven、quick HTTP 壓測</td>
      </tr>
      <tr>
          <td>wrk/wrk2</td>
          <td>C</td>
          <td>高</td>
          <td>低</td>
          <td>單機極限 RPS、saturation discovery 用</td>
      </tr>
  </tbody>
</table>
<p><strong>k6</strong> 是過去 5 年崛起的綜合首選。JavaScript 腳本（前端工程師也能寫）、原生 dashboard、Prometheus exposition、CI 友善。Grafana 收購後生態加速。缺點：複雜 stateful 場景（DB connection pool 共享）需要繞 workaround。</p>
<p><strong>JMeter</strong> 是企業常見的 incumbent。協議支援廣（含 LDAP、JDBC、JMS）、有 GUI 編輯器。缺點：腳本是 XML、版本控制困難；GUI 主要用來生成腳本、實際跑壓測還是要 headless。已經在用的團隊建議繼續、新團隊不必特意選它。</p>
<p><strong>Gatling</strong> 高 throughput 純 async、性能優秀、報表精美。缺點：Scala / Kotlin DSL 學習曲線陡、新版本（11+）改了 DSL 不向後相容。</p>
<p><strong>Locust</strong> 是 Python 生態的選擇、特別適合複雜業務邏輯（用 Python 寫 user journey 自然）。分散式部署原生支援。缺點：Python 單線程 throughput 受限、要靠分散式擴容。</p>
<p><strong>Vegeta</strong> 跟 <strong>wrk</strong> 是「quick check」工具、用於單一 endpoint 的極限測試。不適合複雜場景、適合 saturation discovery 第一輪「找這個服務的天花板」。</p>
<h2 id="production-traffic-replay-工具">Production traffic replay 工具</h2>
<p>當需要複製 <em>真實 production traffic</em> 的壓測場景時、需要另一類工具。</p>
<p><strong>GoReplay</strong> 是最常用的開源 traffic replay 工具。在 production server 上 tcpdump-based 捕獲 HTTP traffic、可以 store 到 file 或 stream 到 staging 環境。優點：開源、無 vendor lock-in；缺點：HTTP only、加密流量要拿到 key 才能用。</p>
<p><strong>Service mesh shadow（Istio / Linkerd mirror）</strong>：mesh 層 mirror traffic 到 staging service。優點：mesh 已部署的話 zero infra cost、加密 traffic 也能 mirror。缺點：需要 service mesh 已落地。</p>
<p><strong>AWS VPC Traffic Mirroring</strong>：底層網路層 mirror、application 完全無感。優點：最低 invasion；缺點：AWS only、加密 traffic 要另外處理。</p>
<p><strong>Diffy（Twitter / X 開源、已 deprecated 但概念仍有效）</strong>：dual-write 同時打到舊 / 新版本、比對結果。適合驗證「新版本是否邏輯正確」、不是純壓測。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — 用分散式 EC2 跑 synthetic load 模擬 100K 同時搶票；<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">SeatGeek Virtual Waiting Room</a> — token 配發邏輯通常用 dual-write 驗證新舊版本一致。</p>
<h2 id="雲端-managed-壓測服務">雲端 managed 壓測服務</h2>
<p>當不想養 load test infrastructure、想 ad-hoc 跑大規模壓測時、用 managed service。</p>
<p><strong>AWS Distributed Load Testing</strong>：CloudFormation 起 Fargate cluster 跑 JMeter 或 Taurus、報表寫到 S3。優點：一鍵部署、Fargate 計費；缺點：JMeter-based、不是現代 k6 風格。</p>
<p><strong>Grafana k6 Cloud</strong>：託管 k6、跨地理 distributed 壓測（從多個 region 同時發流量）。優點：地理分散原生、跟 Grafana 整合無縫；缺點：vendor cost。</p>
<p><strong>Azure Load Testing</strong>：Azure 原生、整合 Application Insights。優點：Azure 用戶無縫；缺點：相對較新、生態還在補。</p>
<p><strong>GCP 沒有 first-party managed load testing</strong>：要靠 Marketplace 方案或自管 Locust on GKE。</p>
<h2 id="工具選型決策樹">工具選型決策樹</h2>
<p>落地時的快速決策：</p>
<ul>
<li>想快速驗證單一 API 極限 → wrk / Vegeta</li>
<li>想寫複雜 user journey + CI 整合 + JavaScript 團隊 → <strong>k6</strong>（新項目首選）</li>
<li>企業已有 JMeter 流程、不想換 → JMeter（接受 XML / GUI 複雜度）</li>
<li>大規模分散式 + Python 生態 → Locust</li>
<li>報表給管理層看、Scala 團隊 → Gatling</li>
<li>想複製真實 production traffic → GoReplay 或 service mesh shadow</li>
<li>想 ad-hoc 雲端大規模壓測 → 對應雲商的 managed load test</li>
</ul>
<h2 id="常見反模式">常見反模式</h2>
<ul>
<li><strong>只測單一 API、不測 user journey</strong>：找不到 cross-endpoint contention、找不到 session state 累積</li>
<li><strong>壓測機跟被測機在同一網段</strong>：網路延遲被低估、p99 比 production 樂觀</li>
<li><strong>壓測時 throttle 自己的工具</strong>：結果不是被測系統的極限、是工具自己的極限</li>
<li><strong>結果報表只看平均</strong>：<a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">tail latency</a> 看不到、p99 退化被掩蓋</li>
<li><strong>壓測環境跟 production hardware 不一致</strong>：CPU 型號、network、disk IOPS 差很大、結果不可外推</li>
<li><strong>沒驗證 model</strong>：跑了壓測但沒對比 production metrics、不知道 model 是否貼近 reality</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>10,000 台 t2.micro 跑分散式壓測（$130 / 小時）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>ML p99 &lt; 10ms 壓測必須帶 latency distribution</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（用工具找 knee）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a>（CI 整合）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">06.1 CI Pipeline</a>（壓測在 CI 的位置）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">Load Test</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
</ul>
]]></content:encoded></item><item><title>9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/</guid><description>&lt;p>這個案例的核心責任是揭示「無明顯峰值但延遲就是收入」這類負載的容量設計、跟前兩個案例形成對照。金融交易不靠峰值定義成敗、靠每個交易的延遲穩定性 — 多 1ms 延遲在套利策略下可能直接吃掉整筆交易的利潤。Coinbase International Exchange 為這類負載做了一系列「反主流」的取捨：固定佈署、不啟用自動擴容、強制節點實體靠近。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Coinbase 在 2023-05 推出國際交易所、上線後關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/coinbase-cryptocurrency-exchange-case-study/">Coinbase Case Study&lt;/a>）：&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>吞吐量&lt;/td>
 &lt;td>100,000 messages/sec（擴容後）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲目標&lt;/td>
 &lt;td>sub-millisecond（次毫秒級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>累計交易額&lt;/td>
 &lt;td>上線以來超過 150 億美元&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性&lt;/td>
 &lt;td>24/7、受監管的交易平台&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Amazon EC2 z1d 實例&lt;/strong>：高頻 CPU + NVMe 本地儲存、針對單執行緒效能最佳化&lt;/li>
&lt;li>&lt;strong>EC2 Cluster Placement Groups&lt;/strong>：強制把節點集中到單一機架附近、最小化 node-to-node 網路延遲&lt;/li>
&lt;li>&lt;strong>Amazon Aurora&lt;/strong>：高速 transaction lookup 的關聯式資料庫&lt;/li>
&lt;li>「Built from the ground up, using Cloud Native principles」（沒有複用既有交易所程式碼）&lt;/li>
&lt;li>內部使用 &lt;strong>RAFT consensus&lt;/strong> 維持交易順序&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這個案例最值得讀的地方、是它「沒有做」的事比「做了」的事更有教學價值。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>沒有用 Auto Scaling&lt;/strong>：交易撮合引擎用 RAFT consensus 維持嚴格順序、節點數量是 consensus 一部分、不能臨時增加。容量規劃完全是 &lt;em>pre-provision&lt;/em>、不是 reactive。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 必須區分「可水平擴容服務」跟「不可水平擴容服務」、後者的容量公式只有 headroom × peak、沒有 elastic 補救。&lt;/li>
&lt;li>&lt;strong>沒有用通用 EC2 實例&lt;/strong>：z1d 是 AWS 針對「高頻 CPU + NVMe」設計的特化實例、犧牲了通用性換取單核效能。這層選擇隱含一個容量規劃決策：&lt;em>單機效能上限&lt;/em> 直接決定 &lt;em>系統理論吞吐上限&lt;/em>、橫向擴容不能超過 RAFT 節點數限制、那麼縱向就必須榨乾。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 必須先判斷瓶頸屬「可分散」還是「不可分散」。&lt;/li>
&lt;li>&lt;strong>沒有用多區域分散&lt;/strong>：Cluster Placement Group 把節點壓到同一可用區內、犧牲了 region failover 速度、換取 node-to-node 網路延遲。這跟「高可用性」的常見直覺相反、是「延遲敏感型負載的容量設計優先於可靠性設計」的一個範例。&lt;/li>
&lt;li>&lt;strong>延遲是設計輸入、不是設計結果&lt;/strong>：sub-millisecond 是先訂目標、再反推所有架構選擇的結果、壓測只是驗證手段。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為&lt;/a> 中 Little&amp;rsquo;s Law 的反向應用 — 給定延遲目標 + 吞吐目標、反推 concurrency 上限 + 每個 stage 的 latency budget。&lt;/li>
&lt;/ol>
&lt;p>需要警惕的判讀盲點：「sub-millisecond latency 達成」這類陳述通常指 &lt;em>p50 或 p90&lt;/em>、不一定是 p99 或 p999。長尾延遲在 RAFT 系統下可能比平均高一個數量級（leader election、replication lag）。讀案例時要注意延遲分布 vs 平均值的差別。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>延遲敏感型服務先做 latency budget 反推&lt;/strong>：給每個 stage（網路、CPU、磁碟、序列化、共識）一個 latency 配額、總和等於 SLO 上限。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a>。&lt;/li>
&lt;li>&lt;strong>單機效能榨乾優先於橫向擴容&lt;/strong>：當 consensus / ordered processing 限制了水平擴容時、單機選型（CPU 頻率、NUMA locality、NVMe）變成主要槓桿。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 把 saturation 點推得越遠。&lt;/li>
&lt;li>&lt;strong>拓樸感知的部署策略&lt;/strong>：Cluster Placement Group 是 AWS 名稱、概念是「網路拓樸感知的工作負載放置」。GCP 有 Compact Placement Policy、Azure 有 Proximity Placement Groups、自建 Kubernetes 有 Pod Topology Spread Constraints + Node Affinity。&lt;/li>
&lt;li>&lt;strong>接受「不可彈性」是有意識決策、不是失敗&lt;/strong>：很多服務不該全部都自動擴容。設計時要區分「需要 elastic 的 stateless 邊緣」跟「必須 pre-provision 的有狀態核心」、容量規劃也要兩條腿。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：所有主流雲端都有對應的高頻 CPU 實例（GCP C2 / Azure HBv 系列）、placement policy 與本地 NVMe 儲存。自建環境可以用 SR-IOV + RDMA + NUMA pinning 達成更極致的版本。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是揭示「無明顯峰值但延遲就是收入」這類負載的容量設計、跟前兩個案例形成對照。金融交易不靠峰值定義成敗、靠每個交易的延遲穩定性 — 多 1ms 延遲在套利策略下可能直接吃掉整筆交易的利潤。Coinbase International Exchange 為這類負載做了一系列「反主流」的取捨：固定佈署、不啟用自動擴容、強制節點實體靠近。</p>
<h2 id="觀察">觀察</h2>
<p>Coinbase 在 2023-05 推出國際交易所、上線後關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/coinbase-cryptocurrency-exchange-case-study/">Coinbase Case Study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐量</td>
          <td>100,000 messages/sec（擴容後）</td>
      </tr>
      <tr>
          <td>延遲目標</td>
          <td>sub-millisecond（次毫秒級）</td>
      </tr>
      <tr>
          <td>累計交易額</td>
          <td>上線以來超過 150 億美元</td>
      </tr>
      <tr>
          <td>可用性</td>
          <td>24/7、受監管的交易平台</td>
      </tr>
  </tbody>
</table>
<p>服務組合：</p>
<ul>
<li><strong>Amazon EC2 z1d 實例</strong>：高頻 CPU + NVMe 本地儲存、針對單執行緒效能最佳化</li>
<li><strong>EC2 Cluster Placement Groups</strong>：強制把節點集中到單一機架附近、最小化 node-to-node 網路延遲</li>
<li><strong>Amazon Aurora</strong>：高速 transaction lookup 的關聯式資料庫</li>
<li>「Built from the ground up, using Cloud Native principles」（沒有複用既有交易所程式碼）</li>
<li>內部使用 <strong>RAFT consensus</strong> 維持交易順序</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>這個案例最值得讀的地方、是它「沒有做」的事比「做了」的事更有教學價值。</p>
<ol>
<li><strong>沒有用 Auto Scaling</strong>：交易撮合引擎用 RAFT consensus 維持嚴格順序、節點數量是 consensus 一部分、不能臨時增加。容量規劃完全是 <em>pre-provision</em>、不是 reactive。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 必須區分「可水平擴容服務」跟「不可水平擴容服務」、後者的容量公式只有 headroom × peak、沒有 elastic 補救。</li>
<li><strong>沒有用通用 EC2 實例</strong>：z1d 是 AWS 針對「高頻 CPU + NVMe」設計的特化實例、犧牲了通用性換取單核效能。這層選擇隱含一個容量規劃決策：<em>單機效能上限</em> 直接決定 <em>系統理論吞吐上限</em>、橫向擴容不能超過 RAFT 節點數限制、那麼縱向就必須榨乾。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 必須先判斷瓶頸屬「可分散」還是「不可分散」。</li>
<li><strong>沒有用多區域分散</strong>：Cluster Placement Group 把節點壓到同一可用區內、犧牲了 region failover 速度、換取 node-to-node 網路延遲。這跟「高可用性」的常見直覺相反、是「延遲敏感型負載的容量設計優先於可靠性設計」的一個範例。</li>
<li><strong>延遲是設計輸入、不是設計結果</strong>：sub-millisecond 是先訂目標、再反推所有架構選擇的結果、壓測只是驗證手段。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為</a> 中 Little&rsquo;s Law 的反向應用 — 給定延遲目標 + 吞吐目標、反推 concurrency 上限 + 每個 stage 的 latency budget。</li>
</ol>
<p>需要警惕的判讀盲點：「sub-millisecond latency 達成」這類陳述通常指 <em>p50 或 p90</em>、不一定是 p99 或 p999。長尾延遲在 RAFT 系統下可能比平均高一個數量級（leader election、replication lag）。讀案例時要注意延遲分布 vs 平均值的差別。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>延遲敏感型服務先做 latency budget 反推</strong>：給每個 stage（網路、CPU、磁碟、序列化、共識）一個 latency 配額、總和等於 SLO 上限。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a>。</li>
<li><strong>單機效能榨乾優先於橫向擴容</strong>：當 consensus / ordered processing 限制了水平擴容時、單機選型（CPU 頻率、NUMA locality、NVMe）變成主要槓桿。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 把 saturation 點推得越遠。</li>
<li><strong>拓樸感知的部署策略</strong>：Cluster Placement Group 是 AWS 名稱、概念是「網路拓樸感知的工作負載放置」。GCP 有 Compact Placement Policy、Azure 有 Proximity Placement Groups、自建 Kubernetes 有 Pod Topology Spread Constraints + Node Affinity。</li>
<li><strong>接受「不可彈性」是有意識決策、不是失敗</strong>：很多服務不該全部都自動擴容。設計時要區分「需要 elastic 的 stateless 邊緣」跟「必須 pre-provision 的有狀態核心」、容量規劃也要兩條腿。</li>
</ol>
<p>跨平台等效：所有主流雲端都有對應的高頻 CPU 實例（GCP C2 / Azure HBv 系列）、placement policy 與本地 NVMe 儲存。自建環境可以用 SR-IOV + RDMA + NUMA pinning 達成更極致的版本。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計延遲敏感型服務的容量地圖 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想搞清楚哪些服務該水平擴容、哪些不該 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a></li>
<li>想做 latency budget 反推 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a></li>
<li>對照不同形狀的負載 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a>（可預期極端峰值）/ <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a>（事件型不可預期峰值）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/coinbase-cryptocurrency-exchange-case-study/">Coinbase Launches an Ultra-Low-Latency Cryptocurrency Exchange on AWS</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/coinbase-migration-case-study/">Coinbase Scales 50% Faster, Cuts Costs 62% with AWS</a></li>
<li><a href="https://aws.amazon.com/video/watch/a413043e5cb/">Ultra-Low-Latency Crypto Exchange on AWS（video）</a></li>
</ul>
]]></content:encoded></item><item><title>AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/</guid><description>&lt;p>這篇的核心責任是補齊「控制面事故如何說清楚責任邊界」。和 2017、2021 兩篇相比，這裡重點在事故治理樣式、單一技術細節是次要的：怎麼分辨控制面與資料面、怎麼維持對外更新節奏、怎麼保留決策脈絡。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>當控制面退化時，最容易出現三種混亂：第一，內部把多個症狀拆成獨立事件；第二，對外更新把控制面和資料面混在一起；第三，決策紀錄只留結論，沒有留下假設與回退條件。這三種混亂會直接拉長復原時間。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多服務管理 API 同步抖動&lt;/td>
 &lt;td>shared control plane 可能異常&lt;/td>
 &lt;td>先建立單一 incident thread&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料讀寫可用但控制操作失真&lt;/td>
 &lt;td>control/data plane 分離已發生&lt;/td>
 &lt;td>對外更新分兩條狀態敘述&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>更新頻率不穩、描述反覆修正&lt;/td>
 &lt;td>evidence pipeline 不穩定&lt;/td>
 &lt;td>固定更新 cadence 與欄位結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退有效但後續仍有殘留警訊&lt;/td>
 &lt;td>依賴鏈條尚未收斂&lt;/td>
 &lt;td>增加 dependency-level 驗證步驟&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故治理路徑樣式">事故治理路徑（樣式）&lt;/h2>
&lt;ol>
&lt;li>啟動單一事件線，避免按產品拆散。&lt;/li>
&lt;li>明確標註控制面與資料面狀態，分開追蹤。&lt;/li>
&lt;li>固定對外 cadence（例如每 30 分鐘）更新「已知 / 未知 / 下一步」。&lt;/li>
&lt;li>在 decision log 記錄假設、證據、回退條件與 owner。&lt;/li>
&lt;li>收斂後把通訊節奏與責任邊界回寫 runbook 與 evidence package。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>Incident decision log&lt;/td>
 &lt;td>事中假設與回退條件缺少結構化&lt;/td>
 &lt;td>強制套用 [8.19] 欄位（假設/證據/條件/責任）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Customer impact assessment&lt;/td>
 &lt;td>對外影響描述粒度不一致&lt;/td>
 &lt;td>在 [8.20] 補 control/data plane 影響分欄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Communication cadence&lt;/td>
 &lt;td>更新節奏受資訊不完整影響&lt;/td>
 &lt;td>在 [8.4] 固定 cadence 與狀態模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence package&lt;/td>
 &lt;td>事後很難回推當時判斷基礎&lt;/td>
 &lt;td>在 [4.20] 補控制面健康、依賴鏈與更新記錄欄位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>事故決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>客戶影響評估： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment&lt;/a>&lt;/li>
&lt;li>事故通訊： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication&lt;/a>&lt;/li>
&lt;li>觀測證據包： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://health.aws.amazon.com/health/status">AWS Service Health Dashboard&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/message/">AWS post-event summaries&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這篇的核心責任是補齊「控制面事故如何說清楚責任邊界」。和 2017、2021 兩篇相比，這裡重點在事故治理樣式、單一技術細節是次要的：怎麼分辨控制面與資料面、怎麼維持對外更新節奏、怎麼保留決策脈絡。</p>
<h2 id="問題場景">問題場景</h2>
<p>當控制面退化時，最容易出現三種混亂：第一，內部把多個症狀拆成獨立事件；第二，對外更新把控制面和資料面混在一起；第三，決策紀錄只留結論，沒有留下假設與回退條件。這三種混亂會直接拉長復原時間。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>代表意義</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多服務管理 API 同步抖動</td>
          <td>shared control plane 可能異常</td>
          <td>先建立單一 incident thread</td>
      </tr>
      <tr>
          <td>資料讀寫可用但控制操作失真</td>
          <td>control/data plane 分離已發生</td>
          <td>對外更新分兩條狀態敘述</td>
      </tr>
      <tr>
          <td>更新頻率不穩、描述反覆修正</td>
          <td>evidence pipeline 不穩定</td>
          <td>固定更新 cadence 與欄位結構</td>
      </tr>
      <tr>
          <td>回退有效但後續仍有殘留警訊</td>
          <td>依賴鏈條尚未收斂</td>
          <td>增加 dependency-level 驗證步驟</td>
      </tr>
  </tbody>
</table>
<h2 id="事故治理路徑樣式">事故治理路徑（樣式）</h2>
<ol>
<li>啟動單一事件線，避免按產品拆散。</li>
<li>明確標註控制面與資料面狀態，分開追蹤。</li>
<li>固定對外 cadence（例如每 30 分鐘）更新「已知 / 未知 / 下一步」。</li>
<li>在 decision log 記錄假設、證據、回退條件與 owner。</li>
<li>收斂後把通訊節奏與責任邊界回寫 runbook 與 evidence package。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>暴露缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident decision log</td>
          <td>事中假設與回退條件缺少結構化</td>
          <td>強制套用 [8.19] 欄位（假設/證據/條件/責任）</td>
      </tr>
      <tr>
          <td>Customer impact assessment</td>
          <td>對外影響描述粒度不一致</td>
          <td>在 [8.20] 補 control/data plane 影響分欄</td>
      </tr>
      <tr>
          <td>Communication cadence</td>
          <td>更新節奏受資訊不完整影響</td>
          <td>在 [8.4] 固定 cadence 與狀態模板</td>
      </tr>
      <tr>
          <td>Evidence package</td>
          <td>事後很難回推當時判斷基礎</td>
          <td>在 [4.20] 補控制面健康、依賴鏈與更新記錄欄位</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>事故決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>客戶影響評估： <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment</a></li>
<li>事故通訊： <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 Incident Communication</a></li>
<li>觀測證據包： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://health.aws.amazon.com/health/status">AWS Service Health Dashboard</a></li>
<li><a href="https://aws.amazon.com/message/">AWS post-event summaries</a></li>
</ul>
]]></content:encoded></item><item><title>2.C3 Shopify：快取序列化格式遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/</guid><description>&lt;p>這個案例的核心責任是說明快取轉換常見的格式遷移如何安全落地。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Shopify 在快取編碼轉換過程使用雙軌策略，先允許新舊格式共存，再逐步收斂。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>快取格式轉換本質上是相容性遷移。若一次切換，回退與資料可讀性風險會放大。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>新格式可編碼就先寫新格式。&lt;/li>
&lt;li>編碼失敗回落舊格式，保留服務可用性。&lt;/li>
&lt;li>維持一段雙軌期，觀測命中率與錯誤率再收斂。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/caching-without-marshal-part-two-messagepack">Caching Without Marshal Part 2&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取轉換常見的格式遷移如何安全落地。</p>
<h2 id="觀察">觀察</h2>
<p>Shopify 在快取編碼轉換過程使用雙軌策略，先允許新舊格式共存，再逐步收斂。</p>
<h2 id="判讀">判讀</h2>
<p>快取格式轉換本質上是相容性遷移。若一次切換，回退與資料可讀性風險會放大。</p>
<h2 id="策略">策略</h2>
<ol>
<li>新格式可編碼就先寫新格式。</li>
<li>編碼失敗回落舊格式，保留服務可用性。</li>
<li>維持一段雙軌期，觀測命中率與錯誤率再收斂。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/caching-without-marshal-part-two-messagepack">Caching Without Marshal Part 2</a></li>
</ul>
]]></content:encoded></item><item><title>3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/</guid><description>&lt;p>這個案例的核心責任是說明 queue 系統的轉換也包含 metadata 治理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>LinkedIn 以 TopicGC 清理未使用 topic，降低 Kafka metadata 壓力並改善 produce/consume 效能。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 queue 規模擴大，僅靠容量擴充不夠，topic 生命週期與治理自動化會成為可靠性關鍵。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>定義 topic 活躍判準與回收條件。&lt;/li>
&lt;li>自動化清理流程並保留稽核紀錄。&lt;/li>
&lt;li>監控清理前後的性能與穩定性指標。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer design&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.linkedin.com/content/engineering/en-us/blog/2022/topicgc_how-linkedin-cleans-up-unused-metadata-for-its-kafka-clu">TopicGC at LinkedIn&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 queue 系統的轉換也包含 metadata 治理。</p>
<h2 id="觀察">觀察</h2>
<p>LinkedIn 以 TopicGC 清理未使用 topic，降低 Kafka metadata 壓力並改善 produce/consume 效能。</p>
<h2 id="判讀">判讀</h2>
<p>當 queue 規模擴大，僅靠容量擴充不夠，topic 生命週期與治理自動化會成為可靠性關鍵。</p>
<h2 id="策略">策略</h2>
<ol>
<li>定義 topic 活躍判準與回收條件。</li>
<li>自動化清理流程並保留稽核紀錄。</li>
<li>監控清理前後的性能與穩定性指標。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 design</a> 與 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.linkedin.com/content/engineering/en-us/blog/2022/topicgc_how-linkedin-cleans-up-unused-metadata-for-its-kafka-clu">TopicGC at LinkedIn</a></li>
</ul>
]]></content:encoded></item><item><title>5.C3 Orbitera：遷移到 Managed Kubernetes</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/</guid><description>&lt;p>這個案例的核心責任是說明平台遷移的關鍵在服務連續性與能力重建，單次技術替換只是其中一步。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Orbitera 原本在 AWS 上以 EC2 為基礎運行 monolithic 架構，使用 EC2 + S3 + RDS + RedShift 組合。被 Google Cloud 收購後，在產品持續運作的前提下遷移到 Google Kubernetes Engine（GKE），同時從 monolith 重構為 microservices 架構。&lt;/p>
&lt;p>遷移後的架構運行在 multi-zone 配置下，每個 zone 維持 3 個 replica，確保單一 zone 故障時服務不中斷。整合 Cloud SQL（取代 RDS）、Google 的 load balancer、Stackdriver（觀測）。遷移完成後取得的操作能力包含 on-demand scaling、快速部署到新 region/zone、以及快速 rollback 失敗的 build。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>跨平台遷移本質是能力遷移：部署、觀測、恢復與團隊流程都需要同步重建。Orbitera 的遷移同時改變了兩個維度——平台（AWS → GCP）和架構（monolith → microservices）。雙維度同時改變放大了遷移風險，但也讓團隊避免了「先遷平台再拆架構」的兩階段成本。&lt;/p>
&lt;p>這個案例揭露的隱性工作量在「能力對等重建」。原本在 AWS 上已經建好的觀測（CloudWatch → Stackdriver）、資料庫操作（RDS → Cloud SQL）、load balancing 都要在新平台上重新建立並驗證。這些能力不會隨著 workload 遷移自動出現——需要明確的 checklist 和驗證流程。&lt;/p>
&lt;p>monolith → microservices 的架構重構改變了 runtime 的基本假設。Monolith 的 readiness 是單一進程啟動完成；microservices 的 readiness 涉及多個服務之間的依賴就緒。&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的 readiness 設計取捨在這類重構後需要重新定義——哪些是必要依賴、哪些是可降級依賴，從 monolith 時代的「全部在同一個進程」變成需要顯式判斷。&lt;/p>
&lt;p>Multi-zone HA（3 replicas/zone）是遷移後 managed 平台提供的基線能力。在 self-managed 環境下實現相同程度的跨 zone 冗餘需要大量手動配置（zone-aware scheduling、cross-zone load balancing）；managed 平台把這些收進平台層，團隊精力從「維持 HA 運作」轉向「定義 HA 目標」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>先驗證新平台的最小可行服務&lt;/strong>：選擇一個依賴少、風險低的服務在 GKE 上完成完整 deployment cycle（build → deploy → observe → rollback），驗證 CI/CD pipeline、觀測整合、rollback 路徑都可運作。&lt;/li>
&lt;li>&lt;strong>建立能力對等 checklist&lt;/strong>：列出舊平台已有的操作能力（觀測、告警、backup、secret 管理、log 收集），逐一確認新平台有對應方案且經過驗證。未對等的能力是遷移的 blocking 條件。&lt;/li>
&lt;li>&lt;strong>逐步搬遷核心工作負載&lt;/strong>：按依賴關係排序遷移批次，保留舊平台的回切路徑。每批遷移後在新平台上跑 load test 驗證容量與恢復能力。&lt;/li>
&lt;li>&lt;strong>把平台能力納入日常治理節奏&lt;/strong>：遷移完成不是終點——GKE 版本升級、node pool 更新、Cloud SQL 維護窗口都要進入團隊的日常操作流程，避免遷移後進入「只部署不維護」的狀態。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫的章節段落">可回寫的章節段落&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 Container Runtime — 遷移期的 Runtime 穩定性&lt;/a>：monolith → microservices 改變 image 建置策略與啟動行為&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract — 遷移期的 Lifecycle 重新驗證&lt;/a>：readiness 條件在架構重構後需重新定義&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal&lt;/a>：遷移後的回退路徑驗證&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/gcp/why-we-migrated-orbitera-to-managed-kubernetes-on-google-cloud-platform/">Why we migrated Orbitera to managed Kubernetes on Google Cloud Platform&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台遷移的關鍵在服務連續性與能力重建，單次技術替換只是其中一步。</p>
<h2 id="觀察">觀察</h2>
<p>Orbitera 原本在 AWS 上以 EC2 為基礎運行 monolithic 架構，使用 EC2 + S3 + RDS + RedShift 組合。被 Google Cloud 收購後，在產品持續運作的前提下遷移到 Google Kubernetes Engine（GKE），同時從 monolith 重構為 microservices 架構。</p>
<p>遷移後的架構運行在 multi-zone 配置下，每個 zone 維持 3 個 replica，確保單一 zone 故障時服務不中斷。整合 Cloud SQL（取代 RDS）、Google 的 load balancer、Stackdriver（觀測）。遷移完成後取得的操作能力包含 on-demand scaling、快速部署到新 region/zone、以及快速 rollback 失敗的 build。</p>
<h2 id="判讀">判讀</h2>
<p>跨平台遷移本質是能力遷移：部署、觀測、恢復與團隊流程都需要同步重建。Orbitera 的遷移同時改變了兩個維度——平台（AWS → GCP）和架構（monolith → microservices）。雙維度同時改變放大了遷移風險，但也讓團隊避免了「先遷平台再拆架構」的兩階段成本。</p>
<p>這個案例揭露的隱性工作量在「能力對等重建」。原本在 AWS 上已經建好的觀測（CloudWatch → Stackdriver）、資料庫操作（RDS → Cloud SQL）、load balancing 都要在新平台上重新建立並驗證。這些能力不會隨著 workload 遷移自動出現——需要明確的 checklist 和驗證流程。</p>
<p>monolith → microservices 的架構重構改變了 runtime 的基本假設。Monolith 的 readiness 是單一進程啟動完成；microservices 的 readiness 涉及多個服務之間的依賴就緒。<a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的 readiness 設計取捨在這類重構後需要重新定義——哪些是必要依賴、哪些是可降級依賴，從 monolith 時代的「全部在同一個進程」變成需要顯式判斷。</p>
<p>Multi-zone HA（3 replicas/zone）是遷移後 managed 平台提供的基線能力。在 self-managed 環境下實現相同程度的跨 zone 冗餘需要大量手動配置（zone-aware scheduling、cross-zone load balancing）；managed 平台把這些收進平台層，團隊精力從「維持 HA 運作」轉向「定義 HA 目標」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>先驗證新平台的最小可行服務</strong>：選擇一個依賴少、風險低的服務在 GKE 上完成完整 deployment cycle（build → deploy → observe → rollback），驗證 CI/CD pipeline、觀測整合、rollback 路徑都可運作。</li>
<li><strong>建立能力對等 checklist</strong>：列出舊平台已有的操作能力（觀測、告警、backup、secret 管理、log 收集），逐一確認新平台有對應方案且經過驗證。未對等的能力是遷移的 blocking 條件。</li>
<li><strong>逐步搬遷核心工作負載</strong>：按依賴關係排序遷移批次，保留舊平台的回切路徑。每批遷移後在新平台上跑 load test 驗證容量與恢復能力。</li>
<li><strong>把平台能力納入日常治理節奏</strong>：遷移完成不是終點——GKE 版本升級、node pool 更新、Cloud SQL 維護窗口都要進入團隊的日常操作流程，避免遷移後進入「只部署不維護」的狀態。</li>
</ol>
<h2 id="可回寫的章節段落">可回寫的章節段落</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 Container Runtime — 遷移期的 Runtime 穩定性</a>：monolith → microservices 改變 image 建置策略與啟動行為</li>
<li><a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract — 遷移期的 Lifecycle 重新驗證</a>：readiness 條件在架構重構後需重新定義</li>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal</a>：遷移後的回退路徑驗證</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/gcp/why-we-migrated-orbitera-to-managed-kubernetes-on-google-cloud-platform/">Why we migrated Orbitera to managed Kubernetes on Google Cloud Platform</a></li>
</ul>
]]></content:encoded></item><item><title>7.C3 Azure AD：2021 Identity Control-plane 事件</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/</guid><description>&lt;p>這個案例的核心責任是說明身份服務控制面故障會外溢成大範圍服務故障。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Azure AD 控制面事件導致多個依賴身份驗證的服務受影響，事故處理需要同時兼顧身份恢復與服務降級策略。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當身份系統是共同依賴，問題會跨產品線傳播，必須把身份恢復路徑與業務優先序綁定管理。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>建立身份控制面的降級與隔離策略。&lt;/li>
&lt;li>讓關鍵服務支援有限模式運行。&lt;/li>
&lt;li>在 incident command 中獨立處理 identity workstream。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity and access boundary&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.8 security vs operational incident&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.microsoft.com/en-us/security/blog/2021/03/17/azure-active-directory-resilience-lessons-from-the-march-15-2021-incident/">Azure AD 2021 incident&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明身份服務控制面故障會外溢成大範圍服務故障。</p>
<h2 id="觀察">觀察</h2>
<p>Azure AD 控制面事件導致多個依賴身份驗證的服務受影響，事故處理需要同時兼顧身份恢復與服務降級策略。</p>
<h2 id="判讀">判讀</h2>
<p>當身份系統是共同依賴，問題會跨產品線傳播，必須把身份恢復路徑與業務優先序綁定管理。</p>
<h2 id="策略">策略</h2>
<ol>
<li>建立身份控制面的降級與隔離策略。</li>
<li>讓關鍵服務支援有限模式運行。</li>
<li>在 incident command 中獨立處理 identity workstream。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity and access boundary</a> 與 <a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.8 security vs operational incident</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.microsoft.com/en-us/security/blog/2021/03/17/azure-active-directory-resilience-lessons-from-the-march-15-2021-incident/">Azure AD 2021 incident</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare 2026 BYOIP BGP Withdrawal</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/</guid><description>&lt;p>2026 年 Cloudflare BYOIP / BGP 事故的核心教訓是：控制面資料一旦同時承擔 customer configuration 與 operational state，錯誤清理流程會直接變成全網路由變更。這類事故的第一責任是停止錯誤狀態傳播，再把 desired state 與 actual state 拆開恢復。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cloudflare 在 2026-02-20 17:48 UTC 發生 BYOIP 相關 outage。部分使用 Bring Your Own IP（BYOIP）的客戶，其 IP prefixes 被 Cloudflare 經由 BGP 非預期撤告，導致相關服務從 Internet 無法到達。官方回顧指出，事故總時長為 6 小時 7 分鐘；在 4,306 個 BYOIP prefixes 中，約 1,100 個 prefixes 曾被撤告，約佔 BYOIP prefixes 的 25%。&lt;/p>
&lt;p>事故起因是 Cloudflare 在 Addressing API / BYOIP pipeline 中引入的自動化清理流程，與外部攻擊無關。該流程原本要移除 pending deletion 的 prefixes，但 API query 的 &lt;code>pending_delete&lt;/code> 參數沒有值，server 端將它解讀成一般查詢，回傳所有 BYOIP prefixes。下游流程接著把回傳結果當成待刪除集合，開始撤告 prefixes 與移除相關 service bindings。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>BYOIP prefixes 數量快速下降&lt;/td>
 &lt;td>BGP advertisement 正在被控制面錯誤改寫&lt;/td>
 &lt;td>立即停止最新 Addressing API / cleanup 任務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客戶服務從 Internet 無法連線&lt;/td>
 &lt;td>prefix withdrawal 已影響資料面可達性&lt;/td>
 &lt;td>優先恢復 prefix advertisement，而非只查應用層錯誤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部分客戶可自行 re-advertise&lt;/td>
 &lt;td>部分狀態只被撤告，binding 尚未被刪除&lt;/td>
 &lt;td>對外提供 dashboard workaround，降低待處理影響面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部分客戶無法自助恢復&lt;/td>
 &lt;td>service bindings 或 edge 設定也被移除&lt;/td>
 &lt;td>需要工程團隊做資料恢復與 global configuration rollout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>恢復分成多批完成&lt;/td>
 &lt;td>受影響 prefixes 處於不同損壞狀態&lt;/td>
 &lt;td>decision log 要分別記錄「可自助」「需手動」「需全域 rollout」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>Addressing API 相關程式碼在 2026-02-05 合併，並於 2026-02-20 部署。&lt;/li>
&lt;li>cleanup sub-task 查詢 &lt;code>/v1/prefixes?pending_delete&lt;/code>，但 &lt;code>pending_delete&lt;/code> 沒有值。&lt;/li>
&lt;li>API server 沒有進入 pending deletion 分支，而是回傳所有 BYOIP prefixes。&lt;/li>
&lt;li>cleanup sub-task 將回傳的 prefixes 解讀成待移除集合，開始撤告 prefixes 與刪除 dependent objects。&lt;/li>
&lt;li>Cloudflare 在觀察到 1.1.1.1 相關失敗後回退變更並終止 broken sub-process。&lt;/li>
&lt;li>多數 prefixes 透過 re-advertise 或 restore 流程恢復，剩餘約 300 個 prefixes 需要工程師手動恢復 service bindings 與 edge 設定。&lt;/li>
&lt;/ol>
&lt;p>這條路徑顯示：BGP withdrawal 是結果，真正的事故起點是控制面資料查詢語意不明確，以及 operational workflow 對查詢結果缺少大範圍變更 circuit breaker。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>API schema&lt;/td>
 &lt;td>boolean-like query 參數語意不明確&lt;/td>
 &lt;td>將狀態查詢參數標準化，錯誤或空值直接拒絕，不進入危險預設路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Desired / actual state 分離&lt;/td>
 &lt;td>customer configuration 與 operational action 混在同一資料面&lt;/td>
 &lt;td>引入 snapshot / staged deployment，讓壞資料可快速回到 known-good state&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大範圍 withdrawal circuit breaker&lt;/td>
 &lt;td>cleanup 任務可一次影響大量 prefixes&lt;/td>
 &lt;td>對 prefix withdrawal / deletion 設速率、數量與健康訊號閘門&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Staging 與 mock data&lt;/td>
 &lt;td>測試資料未覆蓋 task-runner 自主操作情境&lt;/td>
 &lt;td>補 production-like state mutation 測試，而不只測 customer journey&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident intake&lt;/td>
 &lt;td>1.1.1.1 異常成為早期觀察點&lt;/td>
 &lt;td>將共享基礎服務異常納入控制面事故快速升級條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence write-back&lt;/td>
 &lt;td>恢復分成 dashboard 自助、資料修復、global rollout 多條路&lt;/td>
 &lt;td>回寫 decision log 與 evidence package，保留每種狀態的恢復判準&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>控制面資料品質： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a>&lt;/li>
&lt;li>規則推送安全閘門： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a>&lt;/li>
&lt;li>變更安全邊界： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>&lt;/li>
&lt;li>驗證證據交接： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff&lt;/a>&lt;/li>
&lt;li>事故決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>證據回寫流程： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/cloudflare-outage-february-20-2026/">Cloudflare outage on February 20, 2026&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>2026 年 Cloudflare BYOIP / BGP 事故的核心教訓是：控制面資料一旦同時承擔 customer configuration 與 operational state，錯誤清理流程會直接變成全網路由變更。這類事故的第一責任是停止錯誤狀態傳播，再把 desired state 與 actual state 拆開恢復。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Cloudflare 在 2026-02-20 17:48 UTC 發生 BYOIP 相關 outage。部分使用 Bring Your Own IP（BYOIP）的客戶，其 IP prefixes 被 Cloudflare 經由 BGP 非預期撤告，導致相關服務從 Internet 無法到達。官方回顧指出，事故總時長為 6 小時 7 分鐘；在 4,306 個 BYOIP prefixes 中，約 1,100 個 prefixes 曾被撤告，約佔 BYOIP prefixes 的 25%。</p>
<p>事故起因是 Cloudflare 在 Addressing API / BYOIP pipeline 中引入的自動化清理流程，與外部攻擊無關。該流程原本要移除 pending deletion 的 prefixes，但 API query 的 <code>pending_delete</code> 參數沒有值，server 端將它解讀成一般查詢，回傳所有 BYOIP prefixes。下游流程接著把回傳結果當成待刪除集合，開始撤告 prefixes 與移除相關 service bindings。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>事故中代表什麼</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BYOIP prefixes 數量快速下降</td>
          <td>BGP advertisement 正在被控制面錯誤改寫</td>
          <td>立即停止最新 Addressing API / cleanup 任務</td>
      </tr>
      <tr>
          <td>客戶服務從 Internet 無法連線</td>
          <td>prefix withdrawal 已影響資料面可達性</td>
          <td>優先恢復 prefix advertisement，而非只查應用層錯誤</td>
      </tr>
      <tr>
          <td>部分客戶可自行 re-advertise</td>
          <td>部分狀態只被撤告，binding 尚未被刪除</td>
          <td>對外提供 dashboard workaround，降低待處理影響面</td>
      </tr>
      <tr>
          <td>部分客戶無法自助恢復</td>
          <td>service bindings 或 edge 設定也被移除</td>
          <td>需要工程團隊做資料恢復與 global configuration rollout</td>
      </tr>
      <tr>
          <td>恢復分成多批完成</td>
          <td>受影響 prefixes 處於不同損壞狀態</td>
          <td>decision log 要分別記錄「可自助」「需手動」「需全域 rollout」</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>Addressing API 相關程式碼在 2026-02-05 合併，並於 2026-02-20 部署。</li>
<li>cleanup sub-task 查詢 <code>/v1/prefixes?pending_delete</code>，但 <code>pending_delete</code> 沒有值。</li>
<li>API server 沒有進入 pending deletion 分支，而是回傳所有 BYOIP prefixes。</li>
<li>cleanup sub-task 將回傳的 prefixes 解讀成待移除集合，開始撤告 prefixes 與刪除 dependent objects。</li>
<li>Cloudflare 在觀察到 1.1.1.1 相關失敗後回退變更並終止 broken sub-process。</li>
<li>多數 prefixes 透過 re-advertise 或 restore 流程恢復，剩餘約 300 個 prefixes 需要工程師手動恢復 service bindings 與 edge 設定。</li>
</ol>
<p>這條路徑顯示：BGP withdrawal 是結果，真正的事故起點是控制面資料查詢語意不明確，以及 operational workflow 對查詢結果缺少大範圍變更 circuit breaker。</p>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>這次事故暴露的缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API schema</td>
          <td>boolean-like query 參數語意不明確</td>
          <td>將狀態查詢參數標準化，錯誤或空值直接拒絕，不進入危險預設路徑</td>
      </tr>
      <tr>
          <td>Desired / actual state 分離</td>
          <td>customer configuration 與 operational action 混在同一資料面</td>
          <td>引入 snapshot / staged deployment，讓壞資料可快速回到 known-good state</td>
      </tr>
      <tr>
          <td>大範圍 withdrawal circuit breaker</td>
          <td>cleanup 任務可一次影響大量 prefixes</td>
          <td>對 prefix withdrawal / deletion 設速率、數量與健康訊號閘門</td>
      </tr>
      <tr>
          <td>Staging 與 mock data</td>
          <td>測試資料未覆蓋 task-runner 自主操作情境</td>
          <td>補 production-like state mutation 測試，而不只測 customer journey</td>
      </tr>
      <tr>
          <td>Incident intake</td>
          <td>1.1.1.1 異常成為早期觀察點</td>
          <td>將共享基礎服務異常納入控制面事故快速升級條件</td>
      </tr>
      <tr>
          <td>Evidence write-back</td>
          <td>恢復分成 dashboard 自助、資料修復、global rollout 多條路</td>
          <td>回寫 decision log 與 evidence package，保留每種狀態的恢復判準</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>控制面資料品質： <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>規則推送安全閘門： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a></li>
<li>變更安全邊界： <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>驗證證據交接： <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li>事故決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>證據回寫流程： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/cloudflare-outage-february-20-2026/">Cloudflare outage on February 20, 2026</a></li>
</ul>
]]></content:encoded></item><item><title>Healthcare：存取可追溯性與保留邊界</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/</guid><description>&lt;p>本案例的核心責任是讓資料主權場景下的觀測仍可追溯。Healthcare 系統常同時面臨最小存取原則、資料留存規範與跨團隊協作需求。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一個遠距醫療平台，服務多家醫療機構（multi-tenant），處理病歷查閱、處方開立、檢驗報告與預約排程。平台受 HIPAA 跟當地個資法規範，稽核單位要求能回答「哪個使用者在什麼時間查看了哪個病患的哪份紀錄」。&lt;/p>
&lt;p>初期系統的存取紀錄散落在各服務的 application log 中 — 病歷服務記了一筆 &lt;code>GET /patient/123/records&lt;/code>，處方服務記了一筆 &lt;code>POST /prescription&lt;/code>，但兩者沒有共同的 correlation key。稽核問「護理師 A 在 3 月 15 日存取了哪些病歷」時，工程師需要在四個服務各自 grep，再用 timestamp 近似對齊，整個流程耗時半天且結果不可靠。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="存取-log-與-application-log-混合">存取 log 與 application log 混合&lt;/h3>
&lt;p>存取紀錄（誰看了什麼）跟 operational log（request timing、error、retry）寫在同一個 pipeline。Application log 的 retention 設定 30 天（除錯夠用），但法規要求存取紀錄保留 6 年。等到稽核來查詢時，超過 30 天的存取紀錄已經被刪。&lt;/p>
&lt;h3 id="跨服務存取鏈斷裂">跨服務存取鏈斷裂&lt;/h3>
&lt;p>一次病歷查閱可能經過 API gateway → auth service → patient service → record service → audit service 五個服務。每個服務各自記 log，但沒有統一的 access event correlation。Auth service 知道「誰」，patient service 知道「看了哪個病患」，record service 知道「看了哪份紀錄」— 三段資訊散落在三個服務的 log 中，無法自動關聯。&lt;/p>
&lt;h3 id="multi-tenant-retention-差異">Multi-tenant retention 差異&lt;/h3>
&lt;p>不同醫療機構受不同法規管轄 — 機構 A 在美國需要 HIPAA 6 年 retention，機構 B 在歐盟需要 GDPR 的「目的限縮」原則（保留期限隨用途而定），機構 C 在台灣需要醫療法規定的 7 年。統一 retention policy 要嘛過度保留（增加成本與 PII 暴露面），要嘛保留不足（法規風險）。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="data-access-audit-log-獨立-pipeline">Data access audit log 獨立 pipeline&lt;/h3>
&lt;p>把存取事件從 application log 分離出來。每當使用者查閱、修改或匯出 PHI（Protected Health Information）時，產生結構化 access event：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;phi_access&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;nurse-a@hospital-x.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;patient_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;P-2048&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;medical_record/lab_result/2026-03-15&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;view&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;trace_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;access_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acc-789&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;tenant&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;hospital-x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;timestamp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-03-15T14:22:05Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Access event 寫入獨立的 immutable storage（append-only log），跟 application log 分開的 pipeline 與 retention。&lt;/p>
&lt;h3 id="cross-service-access-chain">Cross-service access chain&lt;/h3>
&lt;p>在 API gateway 入口產生 &lt;code>access_id&lt;/code>，跟 &lt;code>trace_id&lt;/code> 一起透過 context propagation 傳遞到所有下游服務。每個服務在產生 access event 時帶上這兩個 key。查詢時用 &lt;code>access_id&lt;/code> 就能撈出一次存取操作在所有服務的完整軌跡，不需要手動拼接。&lt;/p>
&lt;p>&lt;code>trace_id&lt;/code> 用於關聯 operational 訊號（latency、error），&lt;code>access_id&lt;/code> 用於關聯合規稽核。兩者可以相同也可以不同 — 關鍵是 access event 要同時帶兩個 key。&lt;/p>
&lt;h3 id="分層-retention-與-tenant-level-policy">分層 retention 與 tenant-level policy&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>儲存&lt;/th>
 &lt;th>Retention&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Hot&lt;/td>
 &lt;td>搜尋引擎（Elasticsearch / Cloud Logging）&lt;/td>
 &lt;td>90 天&lt;/td>
 &lt;td>即時查詢、事故調查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm&lt;/td>
 &lt;td>Object storage（壓縮）&lt;/td>
 &lt;td>2 年&lt;/td>
 &lt;td>定期稽核、合規查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cold&lt;/td>
 &lt;td>Archive storage（冰凍）&lt;/td>
 &lt;td>6-7 年（依 tenant 法規）&lt;/td>
 &lt;td>法規保留、法務調查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 tenant 在平台建立時設定法規要求的 retention 期限。Pipeline 根據 tenant tag 自動把 access event 路由到對應的 retention tier。Tenant A 的紀錄到第 6 年自動歸檔到 cold，tenant B 在 GDPR 目的屆滿時觸發刪除審核。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是讓資料主權場景下的觀測仍可追溯。Healthcare 系統常同時面臨最小存取原則、資料留存規範與跨團隊協作需求。</p>
<h2 id="業務背景">業務背景</h2>
<p>一個遠距醫療平台，服務多家醫療機構（multi-tenant），處理病歷查閱、處方開立、檢驗報告與預約排程。平台受 HIPAA 跟當地個資法規範，稽核單位要求能回答「哪個使用者在什麼時間查看了哪個病患的哪份紀錄」。</p>
<p>初期系統的存取紀錄散落在各服務的 application log 中 — 病歷服務記了一筆 <code>GET /patient/123/records</code>，處方服務記了一筆 <code>POST /prescription</code>，但兩者沒有共同的 correlation key。稽核問「護理師 A 在 3 月 15 日存取了哪些病歷」時，工程師需要在四個服務各自 grep，再用 timestamp 近似對齊，整個流程耗時半天且結果不可靠。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="存取-log-與-application-log-混合">存取 log 與 application log 混合</h3>
<p>存取紀錄（誰看了什麼）跟 operational log（request timing、error、retry）寫在同一個 pipeline。Application log 的 retention 設定 30 天（除錯夠用），但法規要求存取紀錄保留 6 年。等到稽核來查詢時，超過 30 天的存取紀錄已經被刪。</p>
<h3 id="跨服務存取鏈斷裂">跨服務存取鏈斷裂</h3>
<p>一次病歷查閱可能經過 API gateway → auth service → patient service → record service → audit service 五個服務。每個服務各自記 log，但沒有統一的 access event correlation。Auth service 知道「誰」，patient service 知道「看了哪個病患」，record service 知道「看了哪份紀錄」— 三段資訊散落在三個服務的 log 中，無法自動關聯。</p>
<h3 id="multi-tenant-retention-差異">Multi-tenant retention 差異</h3>
<p>不同醫療機構受不同法規管轄 — 機構 A 在美國需要 HIPAA 6 年 retention，機構 B 在歐盟需要 GDPR 的「目的限縮」原則（保留期限隨用途而定），機構 C 在台灣需要醫療法規定的 7 年。統一 retention policy 要嘛過度保留（增加成本與 PII 暴露面），要嘛保留不足（法規風險）。</p>
<h2 id="解法">解法</h2>
<h3 id="data-access-audit-log-獨立-pipeline">Data access audit log 獨立 pipeline</h3>
<p>把存取事件從 application log 分離出來。每當使用者查閱、修改或匯出 PHI（Protected Health Information）時，產生結構化 access event：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;event_type&#34;</span><span class="p">:</span> <span class="s2">&#34;phi_access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;actor&#34;</span><span class="p">:</span> <span class="s2">&#34;nurse-a@hospital-x.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;patient_id&#34;</span><span class="p">:</span> <span class="s2">&#34;P-2048&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;resource&#34;</span><span class="p">:</span> <span class="s2">&#34;medical_record/lab_result/2026-03-15&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;view&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;trace_id&#34;</span><span class="p">:</span> <span class="s2">&#34;abc123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;access_id&#34;</span><span class="p">:</span> <span class="s2">&#34;acc-789&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&#34;tenant&#34;</span><span class="p">:</span> <span class="s2">&#34;hospital-x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nt">&#34;timestamp&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-03-15T14:22:05Z&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Access event 寫入獨立的 immutable storage（append-only log），跟 application log 分開的 pipeline 與 retention。</p>
<h3 id="cross-service-access-chain">Cross-service access chain</h3>
<p>在 API gateway 入口產生 <code>access_id</code>，跟 <code>trace_id</code> 一起透過 context propagation 傳遞到所有下游服務。每個服務在產生 access event 時帶上這兩個 key。查詢時用 <code>access_id</code> 就能撈出一次存取操作在所有服務的完整軌跡，不需要手動拼接。</p>
<p><code>trace_id</code> 用於關聯 operational 訊號（latency、error），<code>access_id</code> 用於關聯合規稽核。兩者可以相同也可以不同 — 關鍵是 access event 要同時帶兩個 key。</p>
<h3 id="分層-retention-與-tenant-level-policy">分層 retention 與 tenant-level policy</h3>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>儲存</th>
          <th>Retention</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>搜尋引擎（Elasticsearch / Cloud Logging）</td>
          <td>90 天</td>
          <td>即時查詢、事故調查</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>Object storage（壓縮）</td>
          <td>2 年</td>
          <td>定期稽核、合規查詢</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>Archive storage（冰凍）</td>
          <td>6-7 年（依 tenant 法規）</td>
          <td>法規保留、法務調查</td>
      </tr>
  </tbody>
</table>
<p>每個 tenant 在平台建立時設定法規要求的 retention 期限。Pipeline 根據 tenant tag 自動把 access event 路由到對應的 retention tier。Tenant A 的紀錄到第 6 年自動歸檔到 cold，tenant B 在 GDPR 目的屆滿時觸發刪除審核。</p>
<h3 id="存取-log-中的-pii-處理">存取 log 中的 PII 處理</h3>
<p>Access event 本身包含 <code>patient_id</code> 跟 <code>actor</code>，這些在存取紀錄中是必要資訊（「誰看了什麼」需要這兩個欄位）。處理方式是存取控制而非遮罩 — access event storage 的讀取權限限縮到 compliance team 跟 audit 角色，engineering team 的一般查詢權限無法看到這些欄位。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>統一 retention</th>
          <th>分層 + tenant-level</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作複雜度</td>
          <td>低</td>
          <td>高（routing 邏輯、多層 storage）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>高（全部留最長）</td>
          <td>可控（各層各自成本）</td>
      </tr>
      <tr>
          <td>合規精確度</td>
          <td>低（過度保留或保留不足）</td>
          <td>高（對齊各 tenant 法規要求）</td>
      </tr>
      <tr>
          <td>刪除能力</td>
          <td>無法按 tenant 刪</td>
          <td>可（GDPR right to erasure）</td>
      </tr>
      <tr>
          <td>查詢效率</td>
          <td>全量搜尋</td>
          <td>Hot tier 秒級、Cold tier 分鐘到小時級</td>
      </tr>
  </tbody>
</table>
<p>分層架構的最大風險是跨層查詢的延遲 — 稽核要求「給我 3 年前的存取紀錄」時，cold tier 的解凍時間可能是小時級。解法是在稽核週期前預先解凍相關 tenant 的 cold archive 到 warm tier。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：audit log 分離與 PII 治理。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：access log pipeline 的 ownership 與 review cadence。</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：timestamp integrity 跟跨服務時序校正。</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>：access_id 跟 trace_id 的 propagation 設計。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>稽核問「使用者 X 在某段時間存取了什麼」，回答需要超過數小時的手動拼接</li>
<li>存取紀錄的 retention 跟法規要求不一致，但沒人確切量化差距</li>
<li>Multi-tenant 環境中所有 tenant 共用同一個 retention policy，無法按法規區分</li>
<li>跨服務的存取事件無法自動關聯，需要靠 timestamp 近似比對</li>
<li>PHI 相關的 log 跟一般 application log 存在同一個 storage，存取控制無法區隔</li>
</ul>
]]></content:encoded></item><item><title>Healthcare：資料主權與回復順序選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cases/healthcare-data-sovereignty-and-recovery/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cases/healthcare-data-sovereignty-and-recovery/</guid><description>&lt;p>這個案例的核心責任是讓資料主權與可用性同時被治理。Healthcare 場景常同時面臨資料區域限制、最小存取原則與緊急回復需求。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>cross-region data movement&lt;/td>
 &lt;td>是否違反主權邊界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>access audit completeness&lt;/td>
 &lt;td>存取證據是否可追溯&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recovery ordering conflict&lt;/td>
 &lt;td>回復步驟是否與合規衝突&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="風險與邊界">風險與邊界&lt;/h2>
&lt;p>將合規需求與 DR 流程分開設計，容易在事故時出現互斥決策。較穩定做法是先定義可恢復資料集合與不可跨境資料集合，再安排回復順序。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先補 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18&lt;/a> 的責任邊界，再在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a> 驗證回復流程。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是讓資料主權與可用性同時被治理。Healthcare 場景常同時面臨資料區域限制、最小存取原則與緊急回復需求。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cross-region data movement</td>
          <td>是否違反主權邊界</td>
          <td><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
      </tr>
      <tr>
          <td>access audit completeness</td>
          <td>存取證據是否可追溯</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>recovery ordering conflict</td>
          <td>回復步驟是否與合規衝突</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
  </tbody>
</table>
<h2 id="風險與邊界">風險與邊界</h2>
<p>將合規需求與 DR 流程分開設計，容易在事故時出現互斥決策。較穩定做法是先定義可恢復資料集合與不可跨境資料集合，再安排回復順序。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先補 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a> 的責任邊界，再在 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a> 驗證回復流程。</p>
]]></content:encoded></item><item><title>Amazon</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/</guid><description>&lt;p>Amazon 是 cell-based architecture 與 shuffle sharding 的代表、AWS Builders&amp;rsquo; Library 是大規模分散式系統的工程實踐 SSoT。教學重點在「如何設計才能讓失效局部化」。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Cell-based Architecture：把服務切成獨立 cell、每個 cell 有完整 stack&lt;/li>
&lt;li>Shuffle Sharding：客戶請求映射到 cell 的隨機切分、讓單一壞客戶無法擊倒所有 cell&lt;/li>
&lt;li>Static Stability：control plane 失效時 data plane 仍能服務&lt;/li>
&lt;li>Constant Work Pattern：avoid scaling traffic in failure modes&lt;/li>
&lt;li>AWS Builders&amp;rsquo; Library：可重用 reliability patterns 的官方文件&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cell-based Architecture&lt;/td>
 &lt;td>DynamoDB / Route 53 / S3 的 cell 劃分原則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shuffle Sharding&lt;/td>
 &lt;td>數學上的 blast radius 量化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Static Stability&lt;/td>
 &lt;td>control / data plane 分離的設計取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workload Isolation&lt;/td>
 &lt;td>tenancy / region / availability zone 的隔離層級&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build with constant work&lt;/td>
 &lt;td>為何 push-based 比 pull-based 在 failure 時更穩定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1&lt;/a>&lt;/td>
 &lt;td>Shuffle Sharding 與 Cell 邊界&lt;/td>
 &lt;td>用局部隔離限制多租戶擴散，讓恢復可以分批收斂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">A2&lt;/a>&lt;/td>
 &lt;td>Static Stability 與 Constant Work Pattern&lt;/td>
 &lt;td>控制面失效時資料面用快取與固定工作量維持服務&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Amazon 這個案例在講的是可靠性如何靠隔離來守住擴散邊界。讀者先看懂 cell-based architecture 與 shuffle sharding 的責任，再把它們當成控制 blast radius 的設計語言，而不是單純的 AWS 名詞。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當多租戶系統出現資源爭用時，cell 邊界先決定故障能擴散到哪裡。當容量壓力開始拉高時，shuffle sharding 讓風險分散到不同子集合，避免單一熱點把整個服務拖進同一個失敗模式。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否指出一個 workload 的 blast radius 邊界&lt;/li>
&lt;li>能否把共享基礎設施切成可獨立恢復的 cell&lt;/li>
&lt;li>能否說明 contention 會落在哪個 shard&lt;/li>
&lt;li>能否把 recovery 設計成分批恢復，而不是一次全開&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Amazon 的重點是把隔離變成架構語言，這和 Meta 的 region failover、Shopify 的 pod 架構、GCP 的控制面邊界都在同一條線上。差別只在於 Amazon 更早把 cell 與 shard 語言標準化，所以特別適合用來反推其他大型平台的設計選擇。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>cell-based architecture 讓一個 cell 壞掉時，其他 cell 仍能維持服務。&lt;/li>
&lt;li>shuffle sharding 將多租戶請求分散到不同子集合，限制單一客戶或單一熱點的擴散範圍。&lt;/li>
&lt;li>static stability 讓 control plane 失效時 data plane 仍可服務。&lt;/li>
&lt;li>constant work pattern 避免失敗模式下的額外放大成本。&lt;/li>
&lt;li>workload isolation 讓 tenancy / region / AZ 的邊界能各自承擔風險。&lt;/li>
&lt;li>failure containment 讓擴散先停在 cell 或 shard 邊界。&lt;/li>
&lt;li>push-based recovery 讓恢復節奏不依賴大規模同步操作。&lt;/li>
&lt;li>fault isolation 讓局部失效不會拖垮整個 fleet。&lt;/li>
&lt;li>constant work 讓 failure mode 不會因為多做一件事而繼續放大。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/about-aws/whats-new/2019/12/introducing-amazon-builders-library/">Introducing The Amazon Builders’ Library&lt;/a>：Builders&amp;rsquo; Library 的官方入口。&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding&lt;/a>：shuffle sharding 與 fault isolation 的官方文章。&lt;/li>
&lt;li>&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/faq.html">FAQ - Reducing the Scope of Impact with Cell-Based Architecture&lt;/a>：cell-based architecture 與 shuffle sharding 的關係說明。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Amazon 是 cell-based architecture 與 shuffle sharding 的代表、AWS Builders&rsquo; Library 是大規模分散式系統的工程實踐 SSoT。教學重點在「如何設計才能讓失效局部化」。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Cell-based Architecture：把服務切成獨立 cell、每個 cell 有完整 stack</li>
<li>Shuffle Sharding：客戶請求映射到 cell 的隨機切分、讓單一壞客戶無法擊倒所有 cell</li>
<li>Static Stability：control plane 失效時 data plane 仍能服務</li>
<li>Constant Work Pattern：avoid scaling traffic in failure modes</li>
<li>AWS Builders&rsquo; Library：可重用 reliability patterns 的官方文件</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cell-based Architecture</td>
          <td>DynamoDB / Route 53 / S3 的 cell 劃分原則</td>
      </tr>
      <tr>
          <td>Shuffle Sharding</td>
          <td>數學上的 blast radius 量化</td>
      </tr>
      <tr>
          <td>Static Stability</td>
          <td>control / data plane 分離的設計取捨</td>
      </tr>
      <tr>
          <td>Workload Isolation</td>
          <td>tenancy / region / availability zone 的隔離層級</td>
      </tr>
      <tr>
          <td>Build with constant work</td>
          <td>為何 push-based 比 pull-based 在 failure 時更穩定</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1</a></td>
          <td>Shuffle Sharding 與 Cell 邊界</td>
          <td>用局部隔離限制多租戶擴散，讓恢復可以分批收斂</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">A2</a></td>
          <td>Static Stability 與 Constant Work Pattern</td>
          <td>控制面失效時資料面用快取與固定工作量維持服務</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Amazon 這個案例在講的是可靠性如何靠隔離來守住擴散邊界。讀者先看懂 cell-based architecture 與 shuffle sharding 的責任，再把它們當成控制 blast radius 的設計語言，而不是單純的 AWS 名詞。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當多租戶系統出現資源爭用時，cell 邊界先決定故障能擴散到哪裡。當容量壓力開始拉高時，shuffle sharding 讓風險分散到不同子集合，避免單一熱點把整個服務拖進同一個失敗模式。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否指出一個 workload 的 blast radius 邊界</li>
<li>能否把共享基礎設施切成可獨立恢復的 cell</li>
<li>能否說明 contention 會落在哪個 shard</li>
<li>能否把 recovery 設計成分批恢復，而不是一次全開</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Amazon 的重點是把隔離變成架構語言，這和 Meta 的 region failover、Shopify 的 pod 架構、GCP 的控制面邊界都在同一條線上。差別只在於 Amazon 更早把 cell 與 shard 語言標準化，所以特別適合用來反推其他大型平台的設計選擇。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>cell-based architecture 讓一個 cell 壞掉時，其他 cell 仍能維持服務。</li>
<li>shuffle sharding 將多租戶請求分散到不同子集合，限制單一客戶或單一熱點的擴散範圍。</li>
<li>static stability 讓 control plane 失效時 data plane 仍可服務。</li>
<li>constant work pattern 避免失敗模式下的額外放大成本。</li>
<li>workload isolation 讓 tenancy / region / AZ 的邊界能各自承擔風險。</li>
<li>failure containment 讓擴散先停在 cell 或 shard 邊界。</li>
<li>push-based recovery 讓恢復節奏不依賴大規模同步操作。</li>
<li>fault isolation 讓局部失效不會拖垮整個 fleet。</li>
<li>constant work 讓 failure mode 不會因為多做一件事而繼續放大。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/about-aws/whats-new/2019/12/introducing-amazon-builders-library/">Introducing The Amazon Builders’ Library</a>：Builders&rsquo; Library 的官方入口。</li>
<li><a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding</a>：shuffle sharding 與 fault isolation 的官方文章。</li>
<li><a href="https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/faq.html">FAQ - Reducing the Scope of Impact with Cell-Based Architecture</a>：cell-based architecture 與 shuffle sharding 的關係說明。</li>
</ul>
]]></content:encoded></item><item><title>GitHub</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/</guid><description>&lt;p>GitHub 是高 traffic、跨區資料庫 + 強一致性需求的代表、MySQL split-brain / Actions 大規模 outage 是跨區資料一致性與 control-plane 失效的教學標竿。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>MySQL 跨區拓撲：master / replica / Orchestrator 自動切換的失敗模式&lt;/li>
&lt;li>Split-brain 復原：為何資料一致性復原比可用性復原更耗時&lt;/li>
&lt;li>Actions / Codespaces 等控制面：使用者面 outage 與 control plane 的關係&lt;/li>
&lt;li>通訊節奏：GitHub status page / blog 的事故揭露文化&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2018-10&lt;/td>
 &lt;td>MySQL split-brain 24 小時&lt;/td>
 &lt;td>Orchestrator 自動 failover 失誤、人工干預延遲&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2020-11&lt;/td>
 &lt;td>Actions outages&lt;/td>
 &lt;td>CI/CD 平台失效的客戶影響量化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2021-11&lt;/td>
 &lt;td>跨區網路 / replication&lt;/td>
 &lt;td>跨區一致性 vs 可用性的取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>GitHub 這個案例在講的是跨區資料一致性如何把事故拉長。讀者先看懂 replication、Orchestrator 與 status communication 的責任，再把 split-brain 與 Actions outage 視為不同層級的 control-plane 失效。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 replication lag 或 schema 變更讓資料庫進入不穩定狀態時，恢復速度會被一致性約束拉慢。當使用者面產品也同時掛掉時，狀態頁與事故報告就成了對外與對內的共同路由，讓時間線保持一致。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否說明哪個節點持有權威寫入&lt;/li>
&lt;li>能否區分自動 failover 與人工切換的責任邊界&lt;/li>
&lt;li>能否把事故時間線寫成對外可理解的 status update&lt;/li>
&lt;li>能否把 Actions 這類控制面事故量化成客戶影響&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>GitHub 和 Atlassian、Microsoft 365 的共通點，是都把「對外說明」與「內部復原」綁在一起。它也能和 Azure AD 對照，因為一旦身份或 replication 的控制面退化，後面所有產品層的恢復都會被拉長。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2018-10 split-brain 事故說明權威寫入與人工切換的邊界。&lt;/li>
&lt;li>2020-11 Actions outage 與 2021-11 replication 問題則展示了控制面失效如何影響客戶體感與恢復時間。&lt;/li>
&lt;li>replication lag、schema migration 與 read replica deadlock 都屬於相近失敗面。&lt;/li>
&lt;li>status report 的寫法本身也是事故管理能力的一部分。&lt;/li>
&lt;li>orchestrator 自動切換失敗讓自動化與人工介入的邊界更明顯。&lt;/li>
&lt;li>control-plane outage 會同時影響 CI/CD 與資料服務的信任感。&lt;/li>
&lt;li>code hosting 與 CI/CD 共享控制面，讓一個事故同時影響多種使用情境。&lt;/li>
&lt;li>read replica deadlock 讓 schema 變更也成為事故起點。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.blog/2018-10-30-oct21-post-incident-analysis/">October 21 post-incident analysis&lt;/a>：GitHub 2018 年資料庫與 replication 事故的深度分析。&lt;/li>
&lt;li>&lt;a href="https://github.blog/2020-12-02-availability-report-november-2020/">GitHub Availability Report: November 2020&lt;/a>：MySQL replication lag 與 Actions 事故的官方報告。&lt;/li>
&lt;li>&lt;a href="https://github.blog/news-insights/company-news/github-availability-report-december-2020/">GitHub Availability Report: December 2020&lt;/a>：November incident 的後續說明。&lt;/li>
&lt;li>&lt;a href="https://github.blog/news-insights/company-news/github-availability-report-november-2021/">GitHub Availability Report: November 2021&lt;/a>：schema migration / MySQL read replica deadlock 的官方報告。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>GitHub 是高 traffic、跨區資料庫 + 強一致性需求的代表、MySQL split-brain / Actions 大規模 outage 是跨區資料一致性與 control-plane 失效的教學標竿。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>MySQL 跨區拓撲：master / replica / Orchestrator 自動切換的失敗模式</li>
<li>Split-brain 復原：為何資料一致性復原比可用性復原更耗時</li>
<li>Actions / Codespaces 等控制面：使用者面 outage 與 control plane 的關係</li>
<li>通訊節奏：GitHub status page / blog 的事故揭露文化</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2018-10</td>
          <td>MySQL split-brain 24 小時</td>
          <td>Orchestrator 自動 failover 失誤、人工干預延遲</td>
      </tr>
      <tr>
          <td>2020-11</td>
          <td>Actions outages</td>
          <td>CI/CD 平台失效的客戶影響量化</td>
      </tr>
      <tr>
          <td>2021-11</td>
          <td>跨區網路 / replication</td>
          <td>跨區一致性 vs 可用性的取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>GitHub 這個案例在講的是跨區資料一致性如何把事故拉長。讀者先看懂 replication、Orchestrator 與 status communication 的責任，再把 split-brain 與 Actions outage 視為不同層級的 control-plane 失效。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 replication lag 或 schema 變更讓資料庫進入不穩定狀態時，恢復速度會被一致性約束拉慢。當使用者面產品也同時掛掉時，狀態頁與事故報告就成了對外與對內的共同路由，讓時間線保持一致。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否說明哪個節點持有權威寫入</li>
<li>能否區分自動 failover 與人工切換的責任邊界</li>
<li>能否把事故時間線寫成對外可理解的 status update</li>
<li>能否把 Actions 這類控制面事故量化成客戶影響</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>GitHub 和 Atlassian、Microsoft 365 的共通點，是都把「對外說明」與「內部復原」綁在一起。它也能和 Azure AD 對照，因為一旦身份或 replication 的控制面退化，後面所有產品層的恢復都會被拉長。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2018-10 split-brain 事故說明權威寫入與人工切換的邊界。</li>
<li>2020-11 Actions outage 與 2021-11 replication 問題則展示了控制面失效如何影響客戶體感與恢復時間。</li>
<li>replication lag、schema migration 與 read replica deadlock 都屬於相近失敗面。</li>
<li>status report 的寫法本身也是事故管理能力的一部分。</li>
<li>orchestrator 自動切換失敗讓自動化與人工介入的邊界更明顯。</li>
<li>control-plane outage 會同時影響 CI/CD 與資料服務的信任感。</li>
<li>code hosting 與 CI/CD 共享控制面，讓一個事故同時影響多種使用情境。</li>
<li>read replica deadlock 讓 schema 變更也成為事故起點。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://github.blog/2018-10-30-oct21-post-incident-analysis/">October 21 post-incident analysis</a>：GitHub 2018 年資料庫與 replication 事故的深度分析。</li>
<li><a href="https://github.blog/2020-12-02-availability-report-november-2020/">GitHub Availability Report: November 2020</a>：MySQL replication lag 與 Actions 事故的官方報告。</li>
<li><a href="https://github.blog/news-insights/company-news/github-availability-report-december-2020/">GitHub Availability Report: December 2020</a>：November incident 的後續說明。</li>
<li><a href="https://github.blog/news-insights/company-news/github-availability-report-november-2021/">GitHub Availability Report: November 2021</a>：schema migration / MySQL read replica deadlock 的官方報告。</li>
</ul>
]]></content:encoded></item><item><title>Grafana OnCall</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/grafana-oncall/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/grafana-oncall/</guid><description>&lt;p>Grafana OnCall 是 Grafana Labs 維護的 &lt;em>OSS-friendly&lt;/em> on-call 平台、源自 2021 年收購的 Amixr.io、以 Apache 2.0 授權釋出。它承擔三段責任：&lt;em>alert routing + schedule + escalation&lt;/em>（PagerDuty 的 OSS 替代）、&lt;em>Grafana 生態 alert 收斂&lt;/em>（Grafana / Alertmanager / Mimir / Loki alert 進統一 routing）、&lt;em>phone / SMS notification&lt;/em> 透過 Twilio 等 provider。2024 年起 Grafana Labs 推出 &lt;em>Grafana IRM (Incident Response Management) bundle&lt;/em>、把 Grafana OnCall + Grafana Incident（前 Grafana Incident Response &amp;amp; Communications）綁成一個 alert-to-resolve workflow、定位明確對標 PagerDuty 跟 incident.io 的整合 IR 路線。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Grafana OnCall 的核心定位是 &lt;em>Grafana 生態內的 on-call layer&lt;/em>、不是獨立 IR platform。底層產品線：&lt;em>Grafana OnCall OSS&lt;/em>（self-hosted、Helm chart、Apache 2.0）、&lt;em>Grafana Cloud OnCall&lt;/em>（SaaS、含在 Grafana Cloud Pro/Advanced）、&lt;em>Grafana IRM bundle&lt;/em>（OnCall + Incident 整合、2024+ 主推路線）。對非 Grafana-heavy 環境也能單獨用、但跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> 比 ecosystem 廣度不及。&lt;/p>
&lt;p>跟 PagerDuty 比、Grafana OnCall 走 &lt;em>OSS-first + 預算敏感&lt;/em>、核心 schedule / escalation / phone-call 功能對齊、但 advanced workflow（global event orchestration、business service mapping、analytics depth）較弱。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie&lt;/a> 比、Grafana OnCall 不綁 Atlassian 生態、適合已用 Grafana stack 的團隊。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> 比、Grafana IRM bundle 在 alert routing 強、但 Slack-native incident channel 體驗 incident.io 仍領先。&lt;/p>
&lt;p>關鍵張力：&lt;em>OSS 路徑的維運成本&lt;/em> ↔ &lt;em>商業 SaaS 的 SLA&lt;/em>。Self-hosted OSS 要自管 PostgreSQL / Redis / Celery worker / Twilio account、出事故時自家 on-call 平台不能掛（chicken-and-egg）；Grafana Cloud OnCall 解這層、但脫離了 OSS 自管的成本優勢。中型團隊通常走 Grafana Cloud、小型 OSS-first 團隊走自管 + Twilio。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p></description><content:encoded><![CDATA[<p>Grafana OnCall 是 Grafana Labs 維護的 <em>OSS-friendly</em> on-call 平台、源自 2021 年收購的 Amixr.io、以 Apache 2.0 授權釋出。它承擔三段責任：<em>alert routing + schedule + escalation</em>（PagerDuty 的 OSS 替代）、<em>Grafana 生態 alert 收斂</em>（Grafana / Alertmanager / Mimir / Loki alert 進統一 routing）、<em>phone / SMS notification</em> 透過 Twilio 等 provider。2024 年起 Grafana Labs 推出 <em>Grafana IRM (Incident Response Management) bundle</em>、把 Grafana OnCall + Grafana Incident（前 Grafana Incident Response &amp; Communications）綁成一個 alert-to-resolve workflow、定位明確對標 PagerDuty 跟 incident.io 的整合 IR 路線。</p>
<h2 id="服務定位">服務定位</h2>
<p>Grafana OnCall 的核心定位是 <em>Grafana 生態內的 on-call layer</em>、不是獨立 IR platform。底層產品線：<em>Grafana OnCall OSS</em>（self-hosted、Helm chart、Apache 2.0）、<em>Grafana Cloud OnCall</em>（SaaS、含在 Grafana Cloud Pro/Advanced）、<em>Grafana IRM bundle</em>（OnCall + Incident 整合、2024+ 主推路線）。對非 Grafana-heavy 環境也能單獨用、但跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 比 ecosystem 廣度不及。</p>
<p>跟 PagerDuty 比、Grafana OnCall 走 <em>OSS-first + 預算敏感</em>、核心 schedule / escalation / phone-call 功能對齊、但 advanced workflow（global event orchestration、business service mapping、analytics depth）較弱。跟 <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> 比、Grafana OnCall 不綁 Atlassian 生態、適合已用 Grafana stack 的團隊。跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> 比、Grafana IRM bundle 在 alert routing 強、但 Slack-native incident channel 體驗 incident.io 仍領先。</p>
<p>關鍵張力：<em>OSS 路徑的維運成本</em> ↔ <em>商業 SaaS 的 SLA</em>。Self-hosted OSS 要自管 PostgreSQL / Redis / Celery worker / Twilio account、出事故時自家 on-call 平台不能掛（chicken-and-egg）；Grafana Cloud OnCall 解這層、但脫離了 OSS 自管的成本優勢。中型團隊通常走 Grafana Cloud、小型 OSS-first 團隊走自管 + Twilio。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>自管 Grafana OnCall（Helm chart）vs Grafana Cloud OnCall vs Grafana IRM bundle 的取捨</li>
<li>配置 schedule / escalation chain / Twilio phone-call 的最短路徑</li>
<li>Grafana / Alertmanager / 自家 webhook 進 OnCall 的 routing 設計</li>
<li>跟 SIEM（<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Elastic）webhook 整合的 alert 收斂模式</li>
<li>評估 Grafana OnCall vs <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> 取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Grafana OnCall deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Slack / Teams integration</strong>：on-call notification 是否進團隊主 chat channel、ack / resolve 是否能直接在 Slack 操作不切換 UI、@here / @channel 跟 phone-call 是否分層（低風險 Slack only、高風險才打電話）</li>
<li><strong>Escalation chain</strong>：N step escalation 是否覆蓋 <em>primary → secondary → manager</em>、每階是否有 timeout（5min / 15min / 30min）、節假日 / 跨時區 schedule 是否走 <em>rotation</em> 而非單人值班、override 機制是否清楚</li>
<li><strong>Webhook integration to SIEM</strong>：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Elastic Notable Event 進 OnCall 的 webhook 是否走 <em>correlation rule 過濾後</em> 才轉發、HMAC / token auth 是否正確、failed delivery 是否有 retry 跟 dead-letter queue</li>
<li><strong>Grafana dashboard alert routing</strong>：Grafana / Alertmanager alert 是否走 <em>severity-based routing</em>（critical / warning / info 分流到不同 escalation chain）、alert grouping / deduplication 是否啟用避免 alert storm、跟 <a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">observability-reliability-incident-loop</a> 的 signal-to-incident 邊界是否定義</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">drills-and-oncall-readiness</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Schedule + escalation chain</strong>：rotation 走 <em>weekly</em> / <em>daily</em> / <em>custom</em>、可掛 calendar import（iCal / Google Calendar）做休假 override。Escalation chain 是 <em>N step + timeout</em> 結構（例：notify primary → 5min no ack → notify secondary → 15min no ack → notify manager + phone-call）。反例是 <em>single-step chain</em> — 一個人 ack 不到整個 incident 卡住、production chain 至少要 3 step + 跨時區 fallback。</p>
<p><strong>Alert grouping + Notification</strong>：alert source 包含 <em>Alertmanager</em>（Prometheus / Mimir）、<em>Grafana alert</em>（unified alerting 推送）、<em>generic webhook</em>（自家 app / SIEM）、<em>Sentry / Datadog 等第三方</em>。Grouping 用 <em>integration template</em> 寫 Jinja2 抽欄位（service / severity / region）做 deduplication。Notification channel 分層：Slack / Teams 走低成本通知、Twilio phone-call / SMS 留給 P0 / P1、Mobile push 走 Grafana IRM mobile app。</p>
<p><strong>Grafana 生態整合</strong>：Grafana Cloud 帳號內 OnCall 直接啟用、不另外 deploy。Grafana unified alerting 推 alert 到 OnCall integration、Loki / Tempo 的 metric-from-log / trace-anomaly alert 一條 pipeline 進 OnCall。對應 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> 的 alert 出口。Grafana SLO（Service Level Objective）違反 burn rate threshold 也可直接路由到 OnCall escalation。</p>
<p><strong>Grafana IRM bundle</strong>（2024+）：Grafana 把 OnCall（alert routing）+ Incident（incident lifecycle / war room / timeline）打包、目標是把 <em>alert paged → IC declared → channel created → timeline auto-recorded → post-incident review</em> 收進一個 console。對 Grafana-heavy 環境的吸引力是 <em>少一個 vendor seam</em>；對 Slack-native 團隊則跟 incident.io / FireHydrant 競爭、要看 Slack 體驗深度。</p>
<p><strong>OnCall webhook 整合 SIEM / 第三方</strong>：generic webhook integration 接 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> Notable Event、Elastic Security alert、Datadog monitor、自家 app exception。Webhook payload 走 <em>integration template</em> 轉成 OnCall alert 欄位、加 routing label 進對應 escalation chain。注意 <em>webhook auth</em> 走 token / HMAC、不要用 anonymous webhook 接外網 — 對應 <a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">incident-workflow-automation-boundary</a> 的入口治理。</p>
<p><strong>Maintenance mode</strong>：planned maintenance window 期間 suppress alert、避免 deploy / DB migration 觸發大量假 alert。設定 <em>integration-level mute</em> 或 <em>route-level mute</em>、附 reason 跟 expiry time、不要無限期 mute（容易遺忘變盲點）。</p>
<p><strong>Mobile app</strong>：Grafana IRM mobile app（iOS / Android）支援 push notification + ack / resolve / 加 note、replace 部分電話需求。但 phone-call 不可完全廢除 — 手機靜音 / 深夜值班 push 不一定醒、P0 仍需 Twilio 多次呼叫升級。</p>
<p><strong>自管部署</strong>：Helm chart 部署、依賴 <em>PostgreSQL</em>（state）+ <em>Redis</em>（cache / Celery broker）+ <em>Celery worker</em>（background job）+ <em>Twilio account</em>（phone / SMS）+ TLS domain。Production checklist：PostgreSQL 走 managed service（RDS / Cloud SQL）避免自管 DB on-call 平台兩層 chicken-and-egg、Redis 走 managed、Helm values 走 GitOps 版控、Twilio account 走獨立 sub-account 避免 quota 跟其他服務搶。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Grafana OnCall</th>
          <th>PagerDuty</th>
          <th>Opsgenie</th>
          <th>incident.io</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>OSS 自管免費 / Cloud 含在 Grafana Cloud 套餐</td>
          <td>Per-user / 月、advanced tier 加價</td>
          <td>Per-user / 月（Atlassian 套餐）</td>
          <td>Per-user / 月、Slack-native focus</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted (Helm) / Grafana Cloud SaaS</td>
          <td>SaaS only</td>
          <td>SaaS only</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>授權</td>
          <td>Apache 2.0 OSS</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
      </tr>
      <tr>
          <td>Advanced workflow</td>
          <td>基本 schedule + escalation、analytics 較弱</td>
          <td>業界最強（global orchestration / RBA）</td>
          <td>中等（Atlassian Jira / Confluence 整合）</td>
          <td>Slack incident channel + post-incident</td>
      </tr>
      <tr>
          <td>Integration ecosystem</td>
          <td>Grafana / Alertmanager 強、第三方靠 webhook</td>
          <td>700+ 原生 integration</td>
          <td>Atlassian 生態深、Jira / Confluence 一線</td>
          <td>Slack-native、深度有限但體驗好</td>
      </tr>
      <tr>
          <td>Phone / SMS</td>
          <td>Twilio（自配 account / OSS 路徑要自管）</td>
          <td>內建、跨地區 carrier 覆蓋廣</td>
          <td>內建、Atlassian 計費</td>
          <td>內建、focus 在 Slack ack 多於電話</td>
      </tr>
      <tr>
          <td>Slack 體驗</td>
          <td>Slack integration 基本（notify / ack）</td>
          <td>Slack integration 完整</td>
          <td>Slack integration 中等</td>
          <td>Slack-native、incident channel 自動建</td>
      </tr>
      <tr>
          <td>跨平台 IR</td>
          <td>Grafana IRM bundle（OnCall + Incident）2024+</td>
          <td>PagerDuty Incident Workflows</td>
          <td>Jira Service Management incident</td>
          <td>incident.io Catalog + workflow</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Grafana-heavy / OSS-first / 預算敏感</td>
          <td>Enterprise / 跨產品線 / 高 SLA</td>
          <td>已用 Atlassian / Jira Service Management</td>
          <td>Slack-first / startup-to-midsize</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — OSS 路徑可帶走 config、Cloud 也有 export</td>
          <td>中-高 — escalation policy / workflow 量多</td>
          <td>中 — Atlassian 套餐綁定</td>
          <td>中 — Slack workflow 客製化深度</td>
      </tr>
  </tbody>
</table>
<p>選 Grafana OnCall 的核心訴求：<em>OSS-friendly / 預算敏感 / Grafana 生態已是觀測平台主力</em>、能接受 advanced workflow 較弱（或預期不需要）、自管路徑能投入 PostgreSQL / Redis / Twilio account 維運。Enterprise + 高 SLA + 跨產品線 ecosystem 廣度需求仍走 PagerDuty。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Grafana IRM bundle 的整合決策</strong>：OnCall（alert routing）+ Incident（incident channel / timeline / post-mortem）打包後、IR workflow 收在一個 console。決策點是 <em>是否已用 Slack 做 incident channel</em>、若團隊 Slack incident workflow 成熟、IRM Incident 的 channel 自動建可能跟現有 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">incident-communication</a> 模式衝突；若還沒成熟、IRM bundle 是最短路徑。</p>
<p><strong>OnCall webhook 整合 SIEM 的 alert 收斂模式</strong>：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> ES Notable Event / Elastic Security alert 不該直接打 OnCall — 噪音太大會造成 <a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">alert-fatigue-and-signal-quality</a> 問題。實務做法：SIEM 端先走 <em>correlation rule + risk-based threshold</em>、只有 high-confidence finding 才 webhook 到 OnCall、低風險走 Slack notification channel 給 SOC analyst triage。</p>
<p><strong>Maintenance mode 跟 deploy 流程的整合</strong>：deploy pipeline 在 production rollout 前 call OnCall API 開 maintenance window（mute 特定 integration / route）、deploy 完成或失敗 rollback 後關閉。避免 deploy 期間 false alert 把 on-call 叫醒、但要設 <em>max maintenance duration</em>（例 1hr 自動 expire）避免長 window 變盲點。</p>
<p><strong>OSS 自管的 chicken-and-egg</strong>：自管 OnCall 部署本身的 monitoring 不能依賴 OnCall — OnCall 掛了 alert 進不來、on-call 不知道 OnCall 掛了。實務做法：OnCall infra 的 monitoring 走另一條 <em>bootstrap alert</em>（直接 Twilio API call + email-to-pager fallback）、或保留小規模 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> free tier 做 backstop。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Webhook 沒觸發 / alert 沒進來</strong>：integration URL 錯（環境變數沒帶 base URL）、token / HMAC auth 設錯、source 端 webhook payload format 不對（沒走 integration template mapping）— 檢查 OnCall integration log + source webhook delivery log 對齊</li>
<li><strong>Slack notification stuck / 不出現</strong>：Slack OAuth token 過期、Slack workspace permission 變更、OnCall Slack bot 沒被 invite 進 channel — 重 OAuth + 確認 bot membership</li>
<li><strong>Twilio quota 用完 / phone-call 失敗</strong>：Twilio account balance 不足 / 沒升級 trial / 地區 carrier 限制 — 看 Twilio dashboard balance + delivery log、A2P 10DLC 註冊跟地區 toll-free 預先設定</li>
<li><strong>Schedule overlap / on-call 漏排班</strong>：rotation override 配錯、calendar import 沒同步、時區誤判（UTC vs local）— 用 OnCall schedule preview 跑 7-day forward 檢查</li>
<li><strong>Notification delay / 來得慢</strong>：provider latency（Twilio / Slack / FCM push）、Celery worker queue backlog（自管路徑）、escalation timeout 設太長 — 自管路徑檢查 Celery queue length + worker count</li>
<li><strong>Self-hosted upgrade gotcha</strong>：Helm chart major upgrade 帶 DB schema migration、跳版升級失敗、PostgreSQL extension 缺 — 走 staging environment 跑 migration + 備 rollback DB snapshot、不直接 production helm upgrade</li>
<li><strong>Maintenance mode 沒到期 / 變盲點</strong>：mute 沒設 expiry / reason、deploy 完成沒清 mute — maintenance window 強制設 max duration、weekly review mute 清單</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>進階 IR workflow / RBA</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
      </tr>
      <tr>
          <td>Atlassian 生態 / Jira</td>
          <td><a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a></td>
      </tr>
      <tr>
          <td>Slack-native incident</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>商業 SLA / Enterprise</td>
          <td>PagerDuty / Opsgenie</td>
      </tr>
      <tr>
          <td>Post-incident learning</td>
          <td><a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a>（PagerDuty 收購）</td>
      </tr>
      <tr>
          <td>Status page (對外溝通)</td>
          <td><a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> / <a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Twilio account 申請 / A2P 10DLC 註冊 / 地區 carrier 設定細節</li>
<li>Helm chart values 完整 reference（看官方 docs）</li>
<li>Grafana Cloud OnCall pricing tier 對照</li>
<li>Grafana unified alerting 規則語法（屬 observability 範圍、見 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>）</li>
<li>Grafana Incident 的 channel / timeline 細節（屬 IRM bundle 另一半、本頁聚焦 OnCall）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Grafana OnCall 在 08 案例庫沒有直接 vendor-level 事件、本案例庫的多數事故主角是 Slack / GitHub / Cloudflare / AWS 等基礎設施。Grafana OnCall 的對照位置在 <em>OSS-first organization / Grafana-heavy 監控環境</em> 的 IR routing 設計、相關 case 的啟示如下：</p>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>跟 Grafana OnCall 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSS-first / Grafana-heavy 觀測環境</td>
          <td>Alertmanager / Mimir / Loki alert 進 OnCall 是最短整合路徑、escalation chain 走 Grafana SLO burn rate trigger</td>
      </tr>
      <tr>
          <td>預算敏感的中型團隊</td>
          <td>Self-hosted OnCall + Twilio account 是 PagerDuty 的 OSS 替代、要算 PostgreSQL / Redis 維運成本是否真的省</td>
      </tr>
      <tr>
          <td>Slack-only IR workflow vs Grafana IRM</td>
          <td>Grafana IRM bundle 把 incident channel 收進 console、跟 incident.io / Slack-native workflow 二選一</td>
      </tr>
      <tr>
          <td>Vendor 依賴出事（<a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">vendor-dependency-incident</a>）</td>
          <td>OnCall 自身是 vendor、自管路徑要設 bootstrap alert、Cloud 路徑要評估 Grafana Labs SLA 跟 backup paging</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a>、<a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">Incident Workflow Automation Boundary</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a>、<a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a>、<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a>、<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a>、<a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></li>
<li>下游：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（alert source）、<a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">Observability ↔ Reliability ↔ Incident Loop</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（SIEM webhook → OnCall）、<a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">Vendor Dependency Incident</a>（OnCall 自身 vendor 風險）</li>
<li>官方：<a href="https://grafana.com/docs/oncall/">Grafana OnCall Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Grafana Stack</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/</guid><description>&lt;p>Grafana Stack 是 Grafana Labs 提供的 OSS observability 全棧、承擔三個責任：跨 data source 統一視覺化（Grafana）、各訊號類型專屬 backend（Loki logs / Tempo traces / Mimir metrics / Pyroscope profiles）、可自管或用 Grafana Cloud（managed）。設計取捨偏向「OSS-first + signal-specific backend + 統一查詢介面」、是 Datadog 的 OSS 替代方案。&lt;/p>
&lt;p>對「需要 OSS / 自管 observability、跨 data source 統一儀表板、不想 vendor lock-in」這條路徑、Grafana Stack 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Grafana + Prometheus + Loki + Tempo 基本棧&lt;/li>
&lt;li>用 LogQL 查詢 Loki、用 TraceQL 查詢 Tempo&lt;/li>
&lt;li>設計 dashboard as code（Jsonnet / Terraform）&lt;/li>
&lt;li>評估 Mimir vs Thanos 的長期 metrics 儲存選擇&lt;/li>
&lt;li>評估 Grafana Cloud（managed）跟自管的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-grafana-stack-跑起來">最短路徑：5 分鐘把 Grafana Stack 跑起來&lt;/h2>





&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"># 1. 用 docker-compose 跑起 Grafana + Prometheus + Loki&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker-compose.yml with grafana / prometheus / loki&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="c1"># 2. 在 Grafana 加 data source&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: Prometheus / Loki 各自的 datasource config&lt;/span>
&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="c1"># 3. 建第一個 dashboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 用 explorer 試 PromQL + LogQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證 Grafana 起來、可訪 metrics + logs。實際 production 要評估 Mimir / Tempo + Grafana Cloud 取捨。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="grafana-視覺化">Grafana 視覺化&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Data source 配置（Prometheus / Loki / Tempo / Postgres / MySQL / Elasticsearch）&lt;/li>
&lt;li>Dashboard 設計：variable + template + panel&lt;/li>
&lt;li>Dashboard as code：Jsonnet (Grafonnet) / Terraform Grafana provider&lt;/li>
&lt;li>對應指令：HTTP API &lt;code>/api/dashboards&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="logqlloki-查詢">LogQL（Loki 查詢）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>LogQL syntax：log stream selector + filter + parser + aggregation&lt;/li>
&lt;li>跟 PromQL 對齊的設計（同樣 label-based）&lt;/li>
&lt;li>範例：&lt;code>{job=&amp;quot;app&amp;quot;} |= &amp;quot;error&amp;quot; | json | line_format &amp;quot;...&amp;quot;&lt;/code>&lt;/li>
&lt;li>對應 metrics-from-logs（unwrap + rate）&lt;/li>
&lt;/ul>
&lt;h3 id="traceqltempo-查詢">TraceQL（Tempo 查詢）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Grafana Stack 是 Grafana Labs 提供的 OSS observability 全棧、承擔三個責任：跨 data source 統一視覺化（Grafana）、各訊號類型專屬 backend（Loki logs / Tempo traces / Mimir metrics / Pyroscope profiles）、可自管或用 Grafana Cloud（managed）。設計取捨偏向「OSS-first + signal-specific backend + 統一查詢介面」、是 Datadog 的 OSS 替代方案。</p>
<p>對「需要 OSS / 自管 observability、跨 data source 統一儀表板、不想 vendor lock-in」這條路徑、Grafana Stack 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Grafana + Prometheus + Loki + Tempo 基本棧</li>
<li>用 LogQL 查詢 Loki、用 TraceQL 查詢 Tempo</li>
<li>設計 dashboard as code（Jsonnet / Terraform）</li>
<li>評估 Mimir vs Thanos 的長期 metrics 儲存選擇</li>
<li>評估 Grafana Cloud（managed）跟自管的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-grafana-stack-跑起來">最短路徑：5 分鐘把 Grafana Stack 跑起來</h2>





<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. 用 docker-compose 跑起 Grafana + Prometheus + Loki</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker-compose.yml with grafana / prometheus / loki</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="c1"># 2. 在 Grafana 加 data source</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: Prometheus / Loki 各自的 datasource config</span>
</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="c1"># 3. 建第一個 dashboard</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: 用 explorer 試 PromQL + LogQL</span></span></span></code></pre></div><p>最短路徑驗證 Grafana 起來、可訪 metrics + logs。實際 production 要評估 Mimir / Tempo + Grafana Cloud 取捨。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="grafana-視覺化">Grafana 視覺化</h3>
<p>子議題：</p>
<ul>
<li>Data source 配置（Prometheus / Loki / Tempo / Postgres / MySQL / Elasticsearch）</li>
<li>Dashboard 設計：variable + template + panel</li>
<li>Dashboard as code：Jsonnet (Grafonnet) / Terraform Grafana provider</li>
<li>對應指令：HTTP API <code>/api/dashboards</code></li>
</ul>
<h3 id="logqlloki-查詢">LogQL（Loki 查詢）</h3>
<p>子議題：</p>
<ul>
<li>LogQL syntax：log stream selector + filter + parser + aggregation</li>
<li>跟 PromQL 對齊的設計（同樣 label-based）</li>
<li>範例：<code>{job=&quot;app&quot;} |= &quot;error&quot; | json | line_format &quot;...&quot;</code></li>
<li>對應 metrics-from-logs（unwrap + rate）</li>
</ul>
<h3 id="traceqltempo-查詢">TraceQL（Tempo 查詢）</h3>
<p>子議題：</p>
<ul>
<li>TraceQL syntax：span selector + attribute + aggregation</li>
<li>範例：<code>{ span.http.status_code = 500 &amp;&amp; duration &gt; 1s }</code></li>
<li>Service graph：跨服務依賴自動分析</li>
<li>對應 trace-to-logs / trace-to-metrics 關聯查詢</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="lgtm-stack-operations/">LGTM Stack 組合運維</a>：四個元件的責任分工、部署模式、常見故障與 dashboard provisioning</li>
<li><a href="loki-design-operational-limits/">Loki 設計與操作限制</a>：label-based index 設計、LogQL 查詢模式、cardinality 治理與 Elasticsearch 差異</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="loki-設計與限制">Loki 設計與限制</h3>
<p>子議題：</p>
<ul>
<li>Storage：S3 / GCS / 本地、按 stream 切 chunks</li>
<li>Label cardinality 跟 Prometheus 一樣敏感（不是 stream content）</li>
<li>LogQL 不適合 high-cardinality content search（用 Elastic）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
</ul>
<h3 id="tempo-trace-採集">Tempo trace 採集</h3>
<p>子議題：</p>
<ul>
<li>接受 OTLP / Jaeger / Zipkin protocol</li>
<li>Storage：S3 / GCS、cheap object storage</li>
<li>Trace ID lookup 為主、no full-text search（用 traces metrics 反向查）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></li>
</ul>
<h3 id="mimir-長期-metrics-儲存">Mimir 長期 metrics 儲存</h3>
<p>子議題：</p>
<ul>
<li>Prometheus remote write 接收 metric</li>
<li>Horizontally scalable（multi-tenant）</li>
<li>跟 Thanos / Cortex 的對照（Mimir 是 Cortex fork + improvements）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></li>
</ul>
<h3 id="pyroscope-continuous-profiling">Pyroscope continuous profiling</h3>
<p>子議題：</p>
<ul>
<li>CPU / memory / mutex / goroutine profiling</li>
<li>Flame graph 視覺化</li>
<li>跟 Tempo trace 關聯（trace-to-profile）</li>
<li>OSS（Grafana 收購）vs Pyroscope OG</li>
</ul>
<h3 id="grafana-cloudmanaged">Grafana Cloud（managed）</h3>
<p>子議題：</p>
<ul>
<li>Free tier 額度 + paid tier</li>
<li>含所有 stack（Metrics / Logs / Traces / Profiles）</li>
<li>Grafana Cloud vs Datadog cost 對照</li>
<li>Hybrid 模式：self-host backend + Grafana Cloud Grafana</li>
</ul>
<h3 id="unified-alerting">Unified Alerting</h3>
<p>子議題：</p>
<ul>
<li>Grafana 9+ 統一 alerting（取代 dashboard alert + Prometheus alertmanager 分裂）</li>
<li>跨 data source 寫 alert rule</li>
<li>Multi-dimensional alert（per-label）</li>
<li>對應 Alertmanager 兼容</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="dashboard-載入慢">Dashboard 載入慢</h3>
<p>操作原則：先看 query 範圍跟 panel 數、用 query inspector 看 query 時間分布。</p>
<h3 id="loki-query-過慢--失敗">Loki query 過慢 / 失敗</h3>
<p>操作原則：Loki query 需要 label filter 先縮範圍、再 content match。</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"># TODO: LogQL: {namespace=&#34;prod&#34;, app=&#34;api&#34;} |= &#34;error&#34;（先 label 後 filter）</span></span></span></code></pre></div><h3 id="tempo-span-gap">Tempo span gap</h3>
<p>操作原則：trace 不完整、看 sampling 設定 + Collector buffer 是否 drop。</p>
<h3 id="mimir-ingestion-失敗">Mimir ingestion 失敗</h3>
<p>操作原則：remote_write rate / size limit 撞到 Mimir quota。判讀：Mimir HTTP 429 / 413。</p>
<h3 id="grafana-跟-prometheus-disconnected">Grafana 跟 Prometheus disconnected</h3>
<p>操作原則：data source 連不上、看 Grafana log + network。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 單獨用</td>
      </tr>
      <tr>
          <td>SaaS turnkey APM</td>
          <td><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></td>
      </tr>
      <tr>
          <td>Log full-text search 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS / GCP native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Ops</a></td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>Profile only</td>
          <td>Pyroscope OSS / Polar Signals</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Grafana plugin 細節</li>
<li>Dashboard 美術 / UX 建議</li>
<li>Grafana / Loki / Tempo / Mimir 各自完整 admin 手冊</li>
<li>Grafana 商業版 (Enterprise) 功能</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>Loki / Mimir 高峰下的 ingestion lag 與標籤治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Loki retention / compliance</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></td>
          <td>Mimir scale / Prometheus 長期儲存</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Grafana Stack 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>從 X-Ray 遷出後 Tempo 是 OSS trace backend 候選</td>
      </tr>
      <tr>
          <td><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></td>
          <td>從 Datadog 遷出可去 Grafana Cloud</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型 single Grafana / 中型加 Loki+Tempo / 大型 Grafana Cloud 或 Mimir</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>k6</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/k6/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/k6/</guid><description>&lt;p>k6 是 Grafana Labs 出品的 load test 工具、承擔三個責任：CLI-first load test（Go 寫成、JS 寫測試 script）、threshold-based CI gate（pass/fail 直接接 CI）、Grafana Cloud k6 / k6 Operator on K8s 分散式。設計取捨偏向「CI-first + JS DX + 整合 Grafana 生態」、是現代 load test 主流選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 k6 test script（VU / iteration / stages）&lt;/li>
&lt;li>設計 threshold + CI gate（pass/fail）&lt;/li>
&lt;li>用 xk6 extension 擴展（gRPC / Kafka / SQL）&lt;/li>
&lt;li>部署 k6 Operator 做 distributed load&lt;/li>
&lt;li>評估 k6 vs Gatling / Locust / JMeter 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-k6-跑起來">最短路徑：5 分鐘把 k6 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: brew install k6 / docker run grafana/k6&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="c1"># 2. 寫 test.js&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: import http from &amp;#39;k6/http&amp;#39;; export default function(){ http.get(...) }&lt;/span>
&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="c1"># 3. 跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: k6 run --vus 10 --duration 30s test.js&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="test-script-結構">Test script 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>export default function（per-VU iteration）&lt;/li>
&lt;li>export const options（VU / duration / stages / thresholds）&lt;/li>
&lt;li>Setup / teardown&lt;/li>
&lt;li>對應指令範例：&lt;code>k6 run --vus 100 --duration 10m&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="threshold--ci-gate">Threshold + CI gate&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>thresholds: &lt;code>http_req_duration: ['p(95)&amp;lt;500']&lt;/code>&lt;/li>
&lt;li>Exit code 非 0 → CI fail&lt;/li>
&lt;li>Custom metric thresholds&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="test-pattern">Test pattern&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Smoke / Load / Stress / Spike / Soak / Breakpoint&lt;/li>
&lt;li>Stages（ramp-up / steady / ramp-down）&lt;/li>
&lt;li>VU vs iteration vs RPS-based&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="xk6-extensions">xk6 extensions&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>k6 是 Grafana Labs 出品的 load test 工具、承擔三個責任：CLI-first load test（Go 寫成、JS 寫測試 script）、threshold-based CI gate（pass/fail 直接接 CI）、Grafana Cloud k6 / k6 Operator on K8s 分散式。設計取捨偏向「CI-first + JS DX + 整合 Grafana 生態」、是現代 load test 主流選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 k6 test script（VU / iteration / stages）</li>
<li>設計 threshold + CI gate（pass/fail）</li>
<li>用 xk6 extension 擴展（gRPC / Kafka / SQL）</li>
<li>部署 k6 Operator 做 distributed load</li>
<li>評估 k6 vs Gatling / Locust / JMeter 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-k6-跑起來">最短路徑：5 分鐘把 k6 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: brew install k6 / docker run grafana/k6</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="c1"># 2. 寫 test.js</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: import http from &#39;k6/http&#39;; export default function(){ http.get(...) }</span>
</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="c1"># 3. 跑</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: k6 run --vus 10 --duration 30s test.js</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="test-script-結構">Test script 結構</h3>
<p>子議題：</p>
<ul>
<li>export default function（per-VU iteration）</li>
<li>export const options（VU / duration / stages / thresholds）</li>
<li>Setup / teardown</li>
<li>對應指令範例：<code>k6 run --vus 100 --duration 10m</code></li>
</ul>
<h3 id="threshold--ci-gate">Threshold + CI gate</h3>
<p>子議題：</p>
<ul>
<li>thresholds: <code>http_req_duration: ['p(95)&lt;500']</code></li>
<li>Exit code 非 0 → CI fail</li>
<li>Custom metric thresholds</li>
<li>對應 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
</ul>
<h3 id="test-pattern">Test pattern</h3>
<p>子議題：</p>
<ul>
<li>Smoke / Load / Stress / Spike / Soak / Breakpoint</li>
<li>Stages（ramp-up / steady / ramp-down）</li>
<li>VU vs iteration vs RPS-based</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="xk6-extensions">xk6 extensions</h3>
<p>子議題：</p>
<ul>
<li>自訂 binary：xk6 build + import extension</li>
<li>內建：HTTP / WebSocket / gRPC</li>
<li>社群：Kafka / SQL / Redis / browser</li>
<li>對應 cross-protocol load test</li>
</ul>
<h3 id="k6-operator-on-k8s">k6 Operator on K8s</h3>
<p>子議題：</p>
<ul>
<li>TestRun CRD</li>
<li>Distributed load（多 pod 模擬高 VU）</li>
<li>Result aggregation</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
<h3 id="grafana-cloud-k6">Grafana Cloud k6</h3>
<p>子議題：</p>
<ul>
<li>Managed runner（多 region load source）</li>
<li>跟 Grafana dashboard 整合</li>
<li>跟 Loki / Tempo trace 關聯（test → APM trace）</li>
</ul>
<h3 id="browser-testing">Browser testing</h3>
<p>子議題：</p>
<ul>
<li>k6 browser：Chromium-based browser testing</li>
<li>跟 Playwright 重疊但更聚焦 load</li>
<li>適合 frontend regression load test</li>
</ul>
<h3 id="ci-integration">CI integration</h3>
<p>子議題：</p>
<ul>
<li>GitHub Actions / GitLab CI / Jenkins 整合</li>
<li>Artifact + report upload</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
</ul>
<h3 id="k6-vs-xk6-vs-cloud">k6 vs xk6 vs Cloud</h3>
<p>子議題：</p>
<ul>
<li>k6 OSS：CLI + local script</li>
<li>xk6：build custom binary with extensions</li>
<li>k6 Cloud / Grafana Cloud k6：managed + UI</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="test-結果差異大">Test 結果差異大</h3>
<p>操作原則：local network / VU saturation / target 處理能力。</p>
<h3 id="threshold-太鬆--太嚴">Threshold 太鬆 / 太嚴</h3>
<p>操作原則：baseline 不準 / production traffic pattern 沒模擬。</p>
<h3 id="distributed-load-不均勻">Distributed load 不均勻</h3>
<p>操作原則：k6 Operator 分配 VU 不均 / pod 規格差異。</p>
<h3 id="browser-testing-慢--不穩">Browser testing 慢 / 不穩</h3>
<p>操作原則：Chromium 啟動成本 / network condition / target 反應時間。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JVM 生態</td>
          <td><a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></td>
      </tr>
      <tr>
          <td>GUI / 老牌</td>
          <td><a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></td>
      </tr>
      <tr>
          <td>Python</td>
          <td><a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a></td>
      </tr>
      <tr>
          <td>純 browser flow</td>
          <td>Playwright / Cypress</td>
      </tr>
      <tr>
          <td>Cloud managed</td>
          <td>Grafana Cloud k6 / BlazeMeter / k6 Cloud</td>
      </tr>
      <tr>
          <td>Capacity planning（非 CI）</td>
          <td><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity 模組</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>JS 語言基礎</li>
<li>k6 完整 API</li>
<li>Grafana Cloud k6 pricing</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>峰值前 load test 對齊 capacity model + CI gate</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity 與 On-call 分層</a></td>
          <td>automated load testing 變成日常流程的工程化做法</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 k6 customer case</strong>：Grafana Labs / k6 customer engineering blog、企業遷移 JMeter → k6 案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a>、<a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a>、<a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></li>
<li>下游能力：<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> load test 模組</li>
</ul>
]]></content:encoded></item><item><title>Memcached</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/</guid><description>&lt;p>Memcached 是純粹的 in-memory key-value cache、承擔三個責任：簡單 string KV cache、多執行緒高吞吐、嚴格的 cache 邊界（無持久化 / 無 data types / 無 lock）。設計取捨偏向「越簡單越好」— 沒有 Redis 的 data types / Streams / Pub/Sub、也沒有持久化 / 複製 / cluster mode。極輕量、運維成本低、適合 strict cache 場景。&lt;/p>
&lt;p>對「純 cache、避免誤用為 source-of-truth、需要多執行緒高 throughput、極簡運維」這條路徑、Memcached 是首選。從 LiveJournal 2003 年開源至今、是業界最久經考驗的 cache。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Memcached、用 telnet 或 memcached-tool 驗證&lt;/li>
&lt;li>用 SET / GET / DELETE / INCR / DECR 操作、區分 Memcached 跟 Redis 的場景界限&lt;/li>
&lt;li>設計 client-side consistent hashing 做 sharding&lt;/li>
&lt;li>看懂 hit rate / slab fragmentation / eviction 訊號&lt;/li>
&lt;li>評估 Memcached vs Redis 的選用判讀（何時純粹勝過豐富）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-memcached-跑起來">最短路徑：5 分鐘把 Memcached 跑起來&lt;/h2>





&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"># 1. 啟動 Memcached（-t 4 開 4 條 worker thread、-m 64 給 64MB）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 memcached -t &lt;span class="m">4&lt;/span> -m &lt;span class="m">64&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="c1"># 2. 用 text protocol 驗證讀寫（沒有 redis-cli 這種專屬 CLI、直接走 TCP）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># set &amp;lt;key&amp;gt; &amp;lt;flags&amp;gt; &amp;lt;ttl&amp;gt; &amp;lt;bytes&amp;gt;，下一行是 value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;set foo 0 60 3\r\nbar\r\nget foo\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># STORED&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># VALUE foo 0 3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># END&lt;/span>
&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 class="c1"># 3. 確認多執行緒與記憶體上限&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;stats settings\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;num_threads|maxbytes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT maxbytes 67108864 ← 64MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT num_threads 4 ← -t 4 生效&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Memcached 起來、能讀寫、多執行緒生效」。Memcached 沒有 redis-cli 這類專屬 CLI、實際 ops 走 client library（python-memcached / pylibmc / go memcache）+ &lt;code>stats&lt;/code> 系列命令。實機驗證於 memcached:1.6（VERSION 1.6.42）、最後檢查日 2026-06-16。&lt;/p></description><content:encoded><![CDATA[<p>Memcached 是純粹的 in-memory key-value cache、承擔三個責任：簡單 string KV cache、多執行緒高吞吐、嚴格的 cache 邊界（無持久化 / 無 data types / 無 lock）。設計取捨偏向「越簡單越好」— 沒有 Redis 的 data types / Streams / Pub/Sub、也沒有持久化 / 複製 / cluster mode。極輕量、運維成本低、適合 strict cache 場景。</p>
<p>對「純 cache、避免誤用為 source-of-truth、需要多執行緒高 throughput、極簡運維」這條路徑、Memcached 是首選。從 LiveJournal 2003 年開源至今、是業界最久經考驗的 cache。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Memcached、用 telnet 或 memcached-tool 驗證</li>
<li>用 SET / GET / DELETE / INCR / DECR 操作、區分 Memcached 跟 Redis 的場景界限</li>
<li>設計 client-side consistent hashing 做 sharding</li>
<li>看懂 hit rate / slab fragmentation / eviction 訊號</li>
<li>評估 Memcached vs Redis 的選用判讀（何時純粹勝過豐富）</li>
</ol>
<h2 id="最短路徑5-分鐘把-memcached-跑起來">最短路徑：5 分鐘把 Memcached 跑起來</h2>





<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. 啟動 Memcached（-t 4 開 4 條 worker thread、-m 64 給 64MB）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 memcached -t <span class="m">4</span> -m <span class="m">64</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="c1"># 2. 用 text protocol 驗證讀寫（沒有 redis-cli 這種專屬 CLI、直接走 TCP）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#    set &lt;key&gt; &lt;flags&gt; &lt;ttl&gt; &lt;bytes&gt;，下一行是 value</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;set foo 0 60 3\r\nbar\r\nget foo\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># STORED</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># VALUE foo 0 3</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># bar</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># END</span>
</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 class="c1"># 3. 確認多執行緒與記憶體上限</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;stats settings\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;num_threads|maxbytes&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># STAT maxbytes 67108864      ← 64MB</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># STAT num_threads 4          ← -t 4 生效</span></span></span></code></pre></div><p>最短路徑驗證「Memcached 起來、能讀寫、多執行緒生效」。Memcached 沒有 redis-cli 這類專屬 CLI、實際 ops 走 client library（python-memcached / pylibmc / go memcache）+ <code>stats</code> 系列命令。實機驗證於 memcached:1.6（VERSION 1.6.42）、最後檢查日 2026-06-16。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="協議與-client-library">協議與 client library</h3>
<p>子議題：</p>
<ul>
<li>ASCII protocol vs binary protocol（兩種都支援、binary 較有效率）</li>
<li>Client library：python-memcached、pylibmc（libmemcached 綁定）、go memcache、Java spymemcached</li>
<li>Connection management：connection pool / persistent connection</li>
</ul>
<h3 id="指令對照">指令對照</h3>
<p>子議題：</p>
<ul>
<li>基本：SET / GET / ADD / REPLACE / DELETE / FLUSH_ALL</li>
<li>Counter：INCR / DECR（不能 &lt; 0）</li>
<li>條件：CAS（compare-and-swap）做 optimistic lock</li>
<li>批次：GETS（批次 + CAS token）</li>
</ul>
<h3 id="client-side-sharding">Client-side sharding</h3>
<p>Memcached server 本身無 cluster mode、靠 client library 做 sharding。子議題：</p>
<ul>
<li>Consistent hashing（ketama）— 加減 node 時 minimum key 移動</li>
<li>Hash 演算法：md5 / SHA1 / ketama</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a></li>
</ul>
<h3 id="memory-modelslab-allocator">Memory model（slab allocator）</h3>
<p>子議題：</p>
<ul>
<li>Memcached 用 slab allocator 預分配記憶體 chunk</li>
<li>不同 size class（slab class）對應不同 chunk size</li>
<li>Fragmentation：當 value size 跟 slab 不對齊、memory 浪費</li>
<li>對應指令：<code>stats slabs</code> / <code>stats items</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="slab-allocator-與-memory-fragmentation">Slab allocator 與 memory fragmentation</h3>
<p>子議題：</p>
<ul>
<li>Slab class 自動分配機制</li>
<li>Slab reassignment（Memcached 1.4.25+）— 把記憶體在 slab class 間搬移</li>
<li>監控 <code>STAT total_malloced</code> vs <code>STAT bytes_read</code></li>
<li>對應指令：<code>stats slabs</code>、<code>slabs reassign &lt;src&gt; &lt;dst&gt;</code></li>
</ul>
<h3 id="multi-threaded-scaling">Multi-threaded scaling</h3>
<p>子議題：</p>
<ul>
<li>Memcached 從早期就 multi-threaded（vs Redis 早期 single-thread）</li>
<li><code>-t</code> 設 thread 數、預設 4、依 CPU core 調</li>
<li>Lock contention：高 thread 數可能 hit per-bucket lock</li>
<li>對比 Redis：Redis 6+ 加 I/O threads、但 main thread 仍單線</li>
</ul>
<h3 id="aws-elasticache-for-memcached">AWS ElastiCache for Memcached</h3>
<p>子議題：</p>
<ul>
<li>ElastiCache 提供 managed Memcached cluster</li>
<li>Auto Discovery：客戶端自動發現 cluster node 變化</li>
<li>ElastiCache config endpoint 取代 client-side sharding 配置</li>
<li>跟 Redis ElastiCache 的成本對照</li>
</ul>
<h3 id="cascompare-and-swap">CAS（compare-and-swap）</h3>
<p>子議題：</p>
<ul>
<li>GETS 拿 value + token、SET 帶 token 做 conditional update</li>
<li>適合做 optimistic lock（vs Redis SETNX + lua）</li>
<li>CAS 失敗時的 retry 策略</li>
</ul>
<h3 id="memcached-vs-redis-的場景區分">Memcached vs Redis 的場景區分</h3>
<p>子議題：</p>
<ul>
<li>純 cache 不需 data types → Memcached 更輕量</li>
<li>Session store / counter / hot key 兩者都行</li>
<li>Leaderboard / sorted set / Streams / Pub/Sub → 只 Redis</li>
<li>Distributed lock → Redis（Memcached CAS 不夠強）</li>
<li>持久化（cache warmup 後不想全失）→ Redis（RDB / AOF）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="hit-rate-下降">Hit rate 下降</h3>
<p>操作原則：先看 eviction 是否提高、再看 key naming 是否變動。</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="nb">printf</span> <span class="s1">&#39;stats\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;get_hits|get_misses|evictions&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># get_hits / get_misses 算 hit rate、evictions 持續增加代表 memory 壓力</span></span></span></code></pre></div><h3 id="eviction-增加memory-pressure">Eviction 增加（memory pressure）</h3>
<p>操作原則：超過 <code>-m</code> 設定的 memory limit、Memcached 用 LRU evict 老 key。看 <code>stats slabs</code> 哪些 slab class 最常 evict、可能要 slab reassign。</p>
<h3 id="slab-fragmentation">Slab fragmentation</h3>
<p>操作原則：value size 跟 slab class 不對齊造成 wasted memory。判讀：<code>stats slabs</code> 看每個 slab class 的 used vs total chunks。</p>
<h3 id="client-side-sharding-不平衡">Client-side sharding 不平衡</h3>
<p>操作原則：node 加減後、ketama 應 minimum 移動、但實際分布可能因 key 集中而偏斜。判讀：每個 node 的 <code>stats</code> 看 key count + memory usage 是否均衡。</p>
<h3 id="connection-耗盡">Connection 耗盡</h3>
<p>操作原則：每個 client 開太多 connection、Memcached 預設 max 1024 connection。看 <code>stats curr_connections</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 data types（hash / list / set）</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要持久化 / 半持久化</td>
          <td>Redis with AOF / RDB</td>
      </tr>
      <tr>
          <td>需要 distributed lock</td>
          <td>Redis（Redlock 或 SETNX）</td>
      </tr>
      <tr>
          <td>需要 Pub/Sub / Streams</td>
          <td>Redis / Kafka / NATS</td>
      </tr>
      <tr>
          <td>多核高 throughput</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache for Memcached</a></td>
      </tr>
      <tr>
          <td>Process-local cache</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> / Guava Cache（JVM 內、無網路）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Memcached client 完整 API</li>
<li>Memcached internal data structure 細節</li>
<li>Custom binary protocol 實作</li>
<li>ASCII vs binary protocol 完整對照</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Memcached 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>mcrouter 是 Memcached 專屬 protocol-aware routing proxy、處理跨叢集 / 跨區流量收斂與失效隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 基於 Memcached、Netflix 加上跨 AZ replication + client-side smart routing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>TAO 底層用 Memcached 作為 graph 資料的快取層、上層加一致性 / 關聯查詢能力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>Meta 大規模 Memcached 部署的 invalidation / shard move 一致性治理</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Memcached 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>通用、Memcached 也需 TTL jitter / lease / probabilistic early expiration</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single instance / 中型 client-side ketama / 大型 mcrouter 路由 + 跨區 pool</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>CacheLib 是 Memcached 之後 Meta 的分層 cache library、處理 DRAM 經濟極限後的議題</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 編碼遷移在 Memcached 上一樣適用、雙軌策略不依賴 vendor</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 模式 Memcached 用 SET + CAS 實作、不像 Redis 有 Lua / transaction 可組合</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a></li>
</ul>
]]></content:encoded></item><item><title>NATS</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/</guid><description>&lt;p>NATS 是 lightweight high-performance messaging system、承擔三個責任：subject-based routing（hierarchical wildcards）、low-latency messaging（Core NATS、fire-and-forget）、選擇性持久化（JetStream、streams + KV + Object Store）。設計取捨偏向「協議極簡、運維輕、必要時才開持久化」、適合微服務通訊跟 edge 場景。&lt;/p>
&lt;p>對「微服務 messaging、IoT/edge、Request/Reply、需要 messaging + KV 一體」這條路徑、NATS 是輕量首選。本頁先給最短路徑、再展開日常 publish / subscribe 與 subject 設計、最後進階治理（JetStream、supercluster、leaf node）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 nats-server 跑起 NATS（含 JetStream）、驗證 broker 健康&lt;/li>
&lt;li>用 nats CLI publish / subscribe、看 subject hierarchy 匹配&lt;/li>
&lt;li>區分 Core NATS（fire-and-forget）vs JetStream（durable）的選用判讀&lt;/li>
&lt;li>看懂 stream 配置、consumer 配置、pending 訊號&lt;/li>
&lt;li>評估 supercluster、leaf node、KV / Object Store 等延伸場景&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-nats-跑起來">最短路徑：5 分鐘把 NATS 跑起來&lt;/h2>





&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"># 1. 啟動 NATS server（-js 開 JetStream、-m 8222 開監控埠）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name nats -p 4222:4222 -p 8222:8222 nats:latest -js -m &lt;span class="m">8222&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="c1"># 2. 用 nats CLI publish / subscribe（CLI 可用 natsio/nats-box 容器）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># docker run --rm --network host natsio/nats-box nats &amp;lt;subcommand&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">nats --server nats://localhost:4222 pub demo.hello &lt;span class="s2">&amp;#34;world&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">nats --server nats://localhost:4222 sub &lt;span class="s2">&amp;#34;demo.&amp;gt;&amp;#34;&lt;/span> &lt;span class="c1"># 另開一個 shell 持續訂閱&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 建 JetStream stream + pull consumer（持久化 + ack）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">nats --server nats://localhost:4222 stream add demo --subjects &lt;span class="s1">&amp;#39;demo.&amp;gt;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --storage file --retention limits --discard old --defaults
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">nats --server nats://localhost:4222 consumer add demo worker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --pull --deliver all --ack explicit --filter &lt;span class="s1">&amp;#39;demo.&amp;gt;&amp;#39;&lt;/span> --defaults&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Core NATS + JetStream 都可用」。實際寫程式用 nats client library、見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>nats CLI 指令對照表（pub / sub / stream / consumer / kv）&lt;/li>
&lt;li>監控 endpoint（&lt;code>/varz&lt;/code> / &lt;code>/connz&lt;/code> / &lt;code>/jsz&lt;/code> HTTP）&lt;/li>
&lt;li>Client library 配置：connection / reconnect / timeout / async / sync subscribe&lt;/li>
&lt;li>對應指令範例：&lt;code>nats stream info &amp;lt;name&amp;gt;&lt;/code>、&lt;code>nats consumer info &amp;lt;stream&amp;gt; &amp;lt;consumer&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="subject-hierarchy-與-wildcard">Subject hierarchy 與 wildcard&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Subject&lt;/a> 是 NATS 路由的核心、層級式設計：&lt;/p></description><content:encoded><![CDATA[<p>NATS 是 lightweight high-performance messaging system、承擔三個責任：subject-based routing（hierarchical wildcards）、low-latency messaging（Core NATS、fire-and-forget）、選擇性持久化（JetStream、streams + KV + Object Store）。設計取捨偏向「協議極簡、運維輕、必要時才開持久化」、適合微服務通訊跟 edge 場景。</p>
<p>對「微服務 messaging、IoT/edge、Request/Reply、需要 messaging + KV 一體」這條路徑、NATS 是輕量首選。本頁先給最短路徑、再展開日常 publish / subscribe 與 subject 設計、最後進階治理（JetStream、supercluster、leaf node）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 nats-server 跑起 NATS（含 JetStream）、驗證 broker 健康</li>
<li>用 nats CLI publish / subscribe、看 subject hierarchy 匹配</li>
<li>區分 Core NATS（fire-and-forget）vs JetStream（durable）的選用判讀</li>
<li>看懂 stream 配置、consumer 配置、pending 訊號</li>
<li>評估 supercluster、leaf node、KV / Object Store 等延伸場景</li>
</ol>
<h2 id="最短路徑5-分鐘把-nats-跑起來">最短路徑：5 分鐘把 NATS 跑起來</h2>





<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. 啟動 NATS server（-js 開 JetStream、-m 8222 開監控埠）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name nats -p 4222:4222 -p 8222:8222 nats:latest -js -m <span class="m">8222</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="c1"># 2. 用 nats CLI publish / subscribe（CLI 可用 natsio/nats-box 容器）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#    docker run --rm --network host natsio/nats-box nats &lt;subcommand&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">nats --server nats://localhost:4222 pub demo.hello <span class="s2">&#34;world&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">nats --server nats://localhost:4222 sub <span class="s2">&#34;demo.&gt;&#34;</span>   <span class="c1"># 另開一個 shell 持續訂閱</span>
</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="c1"># 3. 建 JetStream stream + pull consumer（持久化 + ack）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">nats --server nats://localhost:4222 stream add demo --subjects <span class="s1">&#39;demo.&gt;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --storage file --retention limits --discard old --defaults
</span></span><span class="line"><span class="ln">12</span><span class="cl">nats --server nats://localhost:4222 consumer add demo worker <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --pull --deliver all --ack explicit --filter <span class="s1">&#39;demo.&gt;&#39;</span> --defaults</span></span></code></pre></div><p>最短路徑驗證「Core NATS + JetStream 都可用」。實際寫程式用 nats client library、見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>nats CLI 指令對照表（pub / sub / stream / consumer / kv）</li>
<li>監控 endpoint（<code>/varz</code> / <code>/connz</code> / <code>/jsz</code> HTTP）</li>
<li>Client library 配置：connection / reconnect / timeout / async / sync subscribe</li>
<li>對應指令範例：<code>nats stream info &lt;name&gt;</code>、<code>nats consumer info &lt;stream&gt; &lt;consumer&gt;</code></li>
</ul>
<h3 id="subject-hierarchy-與-wildcard">Subject hierarchy 與 wildcard</h3>
<p><a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Subject</a> 是 NATS 路由的核心、層級式設計：</p>
<ul>
<li>層級用 <code>.</code> 分隔（例：<code>orders.created.us-west</code>）</li>
<li>單層 wildcard <code>*</code>（匹配一層）</li>
<li>多層 wildcard <code>&gt;</code>（匹配剩餘所有層）</li>
<li>Subject 命名規範與 ownership</li>
</ul>
<h3 id="core-nats-vs-jetstream">Core NATS vs JetStream</h3>
<p>子議題：</p>
<ul>
<li>Core NATS：fire-and-forget、無持久化、極低延遲、適合即時通知 / 控制信號</li>
<li>JetStream：append-only stream + durable consumer、適合需要 replay / 持久化的事件流</li>
<li>兩者並存設計（同一 NATS server 同時跑）</li>
</ul>
<h3 id="requestreply-與-queue-groups">Request/Reply 與 Queue groups</h3>
<p>子議題：</p>
<ul>
<li>Request/Reply pattern（RPC over messaging）</li>
<li>Queue groups（load balancing、多 subscriber 分擔同 subject）</li>
<li>Pub/Sub vs Queue groups 的差異</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>JetStream 已展開為兩篇 deep article：<a href="jetstream-durability-consumer/">core 到 JetStream 邊界</a>（採用決策入口）、<a href="jetstream-supercluster-design/">JetStream 設計與 supercluster/leaf node</a>（stream / consumer / 跨區拓樸 / 多租戶完整實作）。下列子議題段保留選題判讀入口。</p>
<h3 id="jetstream-stream-設計">JetStream stream 設計</h3>
<p>子議題：</p>
<ul>
<li>Stream 配置（subjects、retention policy、storage type）</li>
<li>File-based vs Memory-based storage</li>
<li>MaxMsgs / MaxBytes / MaxAge（保留策略）</li>
<li>Replicas（JetStream raft、跨節點一致性）</li>
</ul>
<h3 id="jetstream-consumer-設計">JetStream consumer 設計</h3>
<p>子議題：</p>
<ul>
<li>Durable vs ephemeral consumer</li>
<li>Push vs pull consumer</li>
<li>Ack 策略（explicit ack / all / none）</li>
<li>AckWait + MaxDeliver + DeliverPolicy（重試控制）</li>
</ul>
<h3 id="cluster--supercluster--leaf-node">Cluster / Supercluster / Leaf node</h3>
<p>子議題：</p>
<ul>
<li>Cluster：單一 region 多 broker、JetStream raft 同步</li>
<li>Supercluster：跨 cluster gateway、跨區延展</li>
<li>Leaf node：邊緣節點、subject mapping、適合 IoT / edge 場景</li>
<li>對應 <a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues 全球交付</a> 的對照思路</li>
</ul>
<h3 id="jetstream-kv--object-store">JetStream KV / Object Store</h3>
<p>子議題：</p>
<ul>
<li>KV store（基於 JetStream、簡單 key-value）</li>
<li>Object Store（基於 JetStream、大 blob）</li>
<li>何時用 NATS KV vs 真的 KV 服務（Redis / etcd）</li>
</ul>
<h3 id="subject-based-acl-與多租戶">Subject-based ACL 與多租戶</h3>
<p>子議題：</p>
<ul>
<li>Account 隔離（multi-tenancy 主機制）</li>
<li>Subject-level permission（publish / subscribe）</li>
<li>Cross-account import / export</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="consumer-pending-累積">Consumer pending 累積</h3>
<p>操作原則：先看 pending 是 ack-pending 還是 stream backlog、再定位 consumer 慢 vs stream 寫入過快。</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">nats --server nats://localhost:4222 consumer info &lt;stream&gt; &lt;consumer&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 看 Unprocessed Messages（stream backlog）與 Redelivered / Acknowledgment Pending（ack-pending）區分兩種累積</span></span></span></code></pre></div><h3 id="stream-超-retention-limit">Stream 超 retention limit</h3>
<p>操作原則：超 MaxBytes / MaxMsgs 時 stream 觸發 discard policy、看是 old discard 還是 new discard。</p>
<h3 id="leaf-node-連線不穩">Leaf node 連線不穩</h3>
<p>操作原則：邊緣節點到 hub 的網路品質決定 subject mapping 延遲、看 reconnect 次數與 latency。</p>
<h3 id="subject-路由錯誤">Subject 路由錯誤</h3>
<p>操作原則：wildcard 設計錯導致訂閱不到、或匹配過多。看 subject hierarchy 規範與實際 subject。</p>
<h3 id="jetstream-raft-不一致">JetStream raft 不一致</h3>
<p>操作原則：replica 配置 R3 但只有 2 個健康節點、stream 變 read-only。看 cluster info 與 raft state。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高吞吐事件流（百萬 msg/sec）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></td>
      </tr>
      <tr>
          <td>複雜 routing（exchange model）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></td>
      </tr>
      <tr>
          <td>Managed queue（AWS / GCP）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS</a> / <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">Pub/Sub</a></td>
      </tr>
      <tr>
          <td>Redis 生態已存在</td>
          <td><a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
      </tr>
      <tr>
          <td>大型企業生態整合</td>
          <td>RabbitMQ / Kafka（社群更大）</td>
      </tr>
      <tr>
          <td>Managed NATS</td>
          <td>Synadia Cloud</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 client 完整 API（依官方文件）</li>
<li>NATS 跟 gRPC 的對比（在分散式通訊章節）</li>
<li>Synadia Cloud 商業功能</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="nats-專屬案例c34-c41">NATS 專屬案例（C34-C41）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-netlify-data-plane-fanout/" data-link-title="3.C34 Netlify：NATS 當全球 metrics/logs 統一資料平面" data-link-desc="Netlify 70K&#43; 網站、10 億 PV/月、跨多雲、NATS 當 all-purpose data plane fan-out bus、超 RabbitMQ 評估。">3.C34 Netlify data plane</a></td>
          <td>全球 metrics / logs fan-out</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/" data-link-title="3.C35 Form3：NATS JetStream 多雲低延遲支付" data-link-desc="Form3 服務 Tier-1 銀行、500ms SLA、SNS/SQS 吃 300ms 預算、改 NATS&#43;JetStream 跨雲 6x 延遲改善。">3.C35 Form3 multi-cloud</a></td>
          <td>JetStream Leaf Node 跨雲低延遲支付</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/" data-link-title="3.C36 Intelecy：工業 IoT 即時感測 &#43; 多租戶" data-link-desc="Intelecy 工廠 gateway 接數萬感測器、&lt; 2 秒往返延遲做即時 ML、從 BoltDB 本地快取演進到 JetStream 持久化。">3.C36 Intelecy IoT</a></td>
          <td>工業 IoT / BoltDB → JetStream</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37 MachineMetrics edge</a></td>
          <td>Leaf node + KV + Object Store + 多租戶 Auth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">3.C38 Clarifai ML</a></td>
          <td>NATS Streaming queue group / at-least-once</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-choria-orchestration-fleet/" data-link-title="3.C39 Choria：NATS 管 50 萬 server fleet" data-link-desc="Choria 替代 Puppet MCollective、NATS 單 binary 無 Zookeeper、4GB node 可達 50 萬 server、wildcard &#43; queue group 做 scatter-gather RPC。">3.C39 Choria fleet</a></td>
          <td>Request/Reply + Queue group / 50 萬 server</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-resgate-realtime-api-gateway/" data-link-title="3.C40 Resgate：WebSocket-to-NATS realtime API gateway" data-link-desc="Resgate 把 NATS subject 暴露成 REST &#43; WebSocket、subject 階層當 schema、event 延遲 &lt; 1ms、純 Core NATS。">3.C40 Resgate API gateway</a></td>
          <td>Subject hierarchy 即 schema / Core NATS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">3.C41 i-flow OT/IT</a></td>
          <td>多工廠 leaf node hub-and-spoke</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 NATS 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues</a></td>
          <td>全球交付對照：leaf node + supercluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>小型 messaging / 中型 JetStream / 大型 supercluster</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>、<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></li>
<li>下游能力：<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>、<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a></li>
</ul>
]]></content:encoded></item><item><title>systemd</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/</guid><description>&lt;p>systemd 是 Linux 主流 init system、承擔三個責任：service unit lifecycle（start / stop / restart / reload）、signal + journald + cgroups 整合、socket activation + timer（cron 替代）。設計取捨偏向「OS-level 整合 + 單機資源管理 + dependency graph」、適合 VM / bare metal 上單機服務、不需要 cluster orchestration 的場景。&lt;/p>
&lt;p>對「VM / bare metal 服務管理、邊緣 / appliance、單機 lifecycle + journal + cgroups」這條路徑、systemd 是 Linux 主流選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 service unit file、配置 Type / Restart / ExecStart&lt;/li>
&lt;li>設計 signal handling + graceful shutdown&lt;/li>
&lt;li>用 journald + journalctl 查 logs&lt;/li>
&lt;li>設定 cgroups v2 resource limit&lt;/li>
&lt;li>用 socket activation / timer 替代 inetd / cron&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-systemd-service-跑起來">最短路徑：5 分鐘把 systemd service 跑起來&lt;/h2>





&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"># 1. 建 unit file（需 root 或 sudo）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">cat &amp;gt; /etc/systemd/system/myapp.service &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;UNIT&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">[Unit]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">Description=My Application
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">After=network.target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">[Service]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">Restart=on-failure
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">RestartSec=5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s">[Install]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s">WantedBy=multi-user.target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s">UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 啟用 + 啟動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">systemctl daemon-reload
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">systemctl &lt;span class="nb">enable&lt;/span> --now myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">systemctl status myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">journalctl -u myapp -f&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="unit-file-設計">Unit file 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Unit type：service / socket / timer / target / mount / path&lt;/li>
&lt;li>Service Type：simple / forking / oneshot / notify / dbus&lt;/li>
&lt;li>Restart：no / on-failure / on-abnormal / always&lt;/li>
&lt;li>ExecStart / ExecStop / ExecReload&lt;/li>
&lt;li>對應指令：&lt;code>systemctl cat myapp.service&lt;/code>、&lt;code>systemctl edit&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="systemctl-指令">systemctl 指令&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Lifecycle：start / stop / restart / reload / enable / disable&lt;/li>
&lt;li>Status：status / is-active / is-enabled / list-units&lt;/li>
&lt;li>Reload after edit：daemon-reload&lt;/li>
&lt;li>對應指令範例：&lt;code>systemctl status myapp&lt;/code>、&lt;code>systemctl list-units --failed&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="journald-日誌">journald 日誌&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>systemd 是 Linux 主流 init system、承擔三個責任：service unit lifecycle（start / stop / restart / reload）、signal + journald + cgroups 整合、socket activation + timer（cron 替代）。設計取捨偏向「OS-level 整合 + 單機資源管理 + dependency graph」、適合 VM / bare metal 上單機服務、不需要 cluster orchestration 的場景。</p>
<p>對「VM / bare metal 服務管理、邊緣 / appliance、單機 lifecycle + journal + cgroups」這條路徑、systemd 是 Linux 主流選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 service unit file、配置 Type / Restart / ExecStart</li>
<li>設計 signal handling + graceful shutdown</li>
<li>用 journald + journalctl 查 logs</li>
<li>設定 cgroups v2 resource limit</li>
<li>用 socket activation / timer 替代 inetd / cron</li>
</ol>
<h2 id="最短路徑5-分鐘把-systemd-service-跑起來">最短路徑：5 分鐘把 systemd service 跑起來</h2>





<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. 建 unit file（需 root 或 sudo）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">cat &gt; /etc/systemd/system/myapp.service <span class="s">&lt;&lt;&#39;UNIT&#39;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">[Unit]
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">Description=My Application
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">After=network.target
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">[Service]
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">Restart=on-failure
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">RestartSec=5
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">[Install]
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">WantedBy=multi-user.target
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">UNIT</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 2. 啟用 + 啟動</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="ln">18</span><span class="cl">systemctl <span class="nb">enable</span> --now myapp
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 3. 驗證</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">systemctl status myapp
</span></span><span class="line"><span class="ln">22</span><span class="cl">journalctl -u myapp -f</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="unit-file-設計">Unit file 設計</h3>
<p>子議題：</p>
<ul>
<li>Unit type：service / socket / timer / target / mount / path</li>
<li>Service Type：simple / forking / oneshot / notify / dbus</li>
<li>Restart：no / on-failure / on-abnormal / always</li>
<li>ExecStart / ExecStop / ExecReload</li>
<li>對應指令：<code>systemctl cat myapp.service</code>、<code>systemctl edit</code></li>
</ul>
<h3 id="systemctl-指令">systemctl 指令</h3>
<p>子議題：</p>
<ul>
<li>Lifecycle：start / stop / restart / reload / enable / disable</li>
<li>Status：status / is-active / is-enabled / list-units</li>
<li>Reload after edit：daemon-reload</li>
<li>對應指令範例：<code>systemctl status myapp</code>、<code>systemctl list-units --failed</code></li>
</ul>
<h3 id="journald-日誌">journald 日誌</h3>
<p>子議題：</p>
<ul>
<li>結構化日誌（kv pairs）</li>
<li>journalctl filter（-u / &ndash;since / -p / -f）</li>
<li>對應 logging：persistent vs runtime journal</li>
<li>跟外部 log forwarder（Vector / Fluent Bit）對接</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="signal-handling--graceful-shutdown">Signal handling + graceful shutdown</h3>
<p>子議題：</p>
<ul>
<li>SIGTERM（default stop signal）/ SIGKILL（force kill after timeout）</li>
<li>TimeoutStopSec：grace period</li>
<li>應用程式要 trap SIGTERM 做 cleanup</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform lifecycle contract</a>（concept 通用）</li>
</ul>
<h3 id="cgroups-v2--resource-limit">cgroups v2 + resource limit</h3>
<p>子議題：</p>
<ul>
<li>CPUQuota / MemoryMax / IOWeight / TasksMax</li>
<li>Slice unit（樹狀 resource 限制）</li>
<li>跟 Kubernetes 的 resource limit 對比（K8s 用 cgroups 但抽象更高）</li>
<li>對應指令：<code>systemd-cgls</code>、<code>systemd-cgtop</code></li>
</ul>
<h3 id="socket-activation">Socket activation</h3>
<p>子議題：</p>
<ul>
<li>用 .socket unit 持有 listening socket、service 啟動時繼承</li>
<li>啟動延遲：socket 一直在、service 按需起</li>
<li>替代 inetd</li>
<li>適合 occasional service / low-traffic</li>
</ul>
<h3 id="systemd-timer">systemd timer</h3>
<p>子議題：</p>
<ul>
<li>.timer unit 替代 cron</li>
<li>OnCalendar / OnUnitActiveSec / RandomizedDelaySec</li>
<li>跟對應 .service unit 配對</li>
<li>比 cron 強：journal log / dependency / 失敗 restart</li>
</ul>
<h3 id="portable-services--systemd-run">Portable services + systemd-run</h3>
<p>子議題：</p>
<ul>
<li>systemd-run：ad-hoc 跑 transient unit</li>
<li>Portable services：把 service + image 一起搬</li>
<li>systemd-nspawn 容器（systemd 自家輕量容器）</li>
</ul>
<h3 id="跟-container-整合">跟 container 整合</h3>
<p>子議題：</p>
<ul>
<li>跑 podman container 在 systemd（quadlet / generators）</li>
<li>Docker daemon 由 systemd 管</li>
<li>K8s kubelet 由 systemd 管（cluster node）</li>
<li>對應 single-node container management</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-start-failure">Service start failure</h3>
<p>操作原則：先 <code>systemctl status</code>、再 <code>journalctl -u</code> 看 log。</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">systemctl status myapp                <span class="c1"># 看 Active state + Main PID + 最近 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -u myapp --since<span class="o">=</span>-5m       <span class="c1"># 最近 5 分鐘的完整 log</span></span></span></code></pre></div><h3 id="restart-loop">Restart loop</h3>
<p>操作原則：Restart 配置不當 + StartLimit 觸發。判讀：<code>systemctl status</code> 看 restart count + RateLimit。</p>
<h3 id="journald-disk-full">journald disk full</h3>
<p>操作原則：journal storage 超 SystemMaxUse 設定。判讀：<code>journalctl --disk-usage</code>、<code>/etc/systemd/journald.conf</code> 設限。</p>
<h3 id="cgroup-oom">cgroup OOM</h3>
<p>操作原則：MemoryMax 超過、系統 OOM kill。判讀：<code>journalctl -k</code> 看 kernel oom 訊息。</p>
<h3 id="dependency-不對">Dependency 不對</h3>
<p>操作原則：unit 依賴 network / db 但 After= 沒設。判讀：<code>systemctl list-dependencies myapp</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多實例 cluster</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
      </tr>
      <tr>
          <td>Container workflow 為主</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a> / Podman</td>
      </tr>
      <tr>
          <td>Process supervisor（非 init）</td>
          <td>supervisord / runit</td>
      </tr>
      <tr>
          <td>Cron-only 場景</td>
          <td>純 cron / systemd timer</td>
      </tr>
      <tr>
          <td>Non-Linux（Windows / macOS）</td>
          <td>Windows Service / launchd</td>
      </tr>
      <tr>
          <td>邊緣 K8s</td>
          <td>K3s（systemd 上跑 K3s）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 unit file directive reference</li>
<li>systemd internals（dbus / pid 1）</li>
<li>各 distro systemd 版本差異</li>
<li>systemd-resolved / systemd-networkd 等其他 component</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 systemd 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>systemd 服務切換要靠 ExecStop / TimeoutStopSec / SIGTERM trap 等價 drain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小規模 VM 服務首選 systemd、跨規模升階到 K8s 時要保留 unit-level 回退腳本</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 systemd 案例</strong>：大規模 fleet（HashiCorp Nomad 跟 systemd 整合）、IoT / edge appliance 案例、systemd portable services 落地案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>、<a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability</a>（graceful shutdown）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（journald）</li>
</ul>
]]></content:encoded></item><item><title>0.3 非同步與事件傳遞選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/</guid><description>&lt;p>非同步與事件傳遞選型的核心原則是先判斷工作離開 request 後需要什麼保證。背景工作、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a>、stream、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub&lt;/a> 與 outbox 都能讓流程非同步化，但它們對持久化、重試、順序、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a> 與一致性的承諾不同。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分本地背景工作、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、stream、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub&lt;/a> 與 outbox&lt;/li>
&lt;li>用投遞保證、重試需求與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a> 需求判斷服務類型&lt;/li>
&lt;li>看懂 RabbitMQ、Kafka、NATS、Redis Streams 這類工具的選型入口&lt;/li>
&lt;li>把非同步設計轉成可檢查的工程判斷&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察非同步需求來自-request-邊界外的工作">【觀察】非同步需求來自 request 邊界外的工作&lt;/h2>
&lt;p>非同步處理通常從一個現象開始：某件事適合在 request 結束後繼續做。這可能是因為工作太慢、需要重試、需要多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a>、需要跨服務傳遞，或需要在資料庫交易後補送事件。&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>工作只需要離開 request，但留在同一 process&lt;/td>
 &lt;td>背景處理與生命週期&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">local worker&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工作需要 process 重啟後仍存在&lt;/td>
 &lt;td>持久化與重試&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多個 consumer 要各自追進度&lt;/td>
 &lt;td>replay、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a>&lt;/td>
 &lt;td>stream / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多個訂閱者即時收到訊息&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a> 與即時通知&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料寫入和事件發布要一起可靠&lt;/td>
 &lt;td>交易一致性與補送&lt;/td>
 &lt;td>outbox&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是索引。選型時要看事件是否能遺失、是否會重複、是否要重播、是否要多個服務各自消費。&lt;/p>
&lt;h2 id="判讀local-worker-承擔-process-內背景工作">【判讀】local worker 承擔 process 內背景工作&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Local worker&lt;/a> 的核心責任是把工作從 request 等待時間中拆出來，但仍留在同一個 process 裡。當工作可以接受 process 重啟後消失，或上游可以重新觸發，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">local worker&lt;/a> 通常足夠。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>request 完成後寫一筆非關鍵 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>&lt;/li>
&lt;li>在同一服務內批次刷新短生命週期快取&lt;/li>
&lt;li>定期清理 memory repository 裡的過期資料&lt;/li>
&lt;/ul>
&lt;p>這類設計的主要風險是生命週期。worker 要能停止、記錄錯誤、控制 queue full，並在 shutdown 時有明確策略。語言教材通常會處理這一層，例如 Go 的 &lt;code>Run(ctx)&lt;/code>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a>。&lt;/p>
&lt;h2 id="判讀durable-queue-承擔可重試工作">【判讀】durable queue 承擔可重試工作&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">Durable queue&lt;/a> 的核心責任是讓工作在 process 重啟、暫時失敗或 consumer 下線後仍能被處理。當事件可以延後，但需要可靠送達與重試，應評估 broker queue。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p></description><content:encoded><![CDATA[<p>非同步與事件傳遞選型的核心原則是先判斷工作離開 request 後需要什麼保證。背景工作、<a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>、stream、<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 與 outbox 都能讓流程非同步化，但它們對持久化、重試、順序、<a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 與一致性的承諾不同。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分本地背景工作、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、stream、<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 與 outbox</li>
<li>用投遞保證、重試需求與 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 需求判斷服務類型</li>
<li>看懂 RabbitMQ、Kafka、NATS、Redis Streams 這類工具的選型入口</li>
<li>把非同步設計轉成可檢查的工程判斷</li>
</ol>
<hr>
<h2 id="觀察非同步需求來自-request-邊界外的工作">【觀察】非同步需求來自 request 邊界外的工作</h2>
<p>非同步處理通常從一個現象開始：某件事適合在 request 結束後繼續做。這可能是因為工作太慢、需要重試、需要多個 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a>、需要跨服務傳遞，或需要在資料庫交易後補送事件。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>代表的工程問題</th>
          <th>常見服務方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工作只需要離開 request，但留在同一 process</td>
          <td>背景處理與生命週期</td>
          <td><a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">local worker</a></td>
      </tr>
      <tr>
          <td>工作需要 process 重啟後仍存在</td>
          <td>持久化與重試</td>
          <td><a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a></td>
      </tr>
      <tr>
          <td>多個 consumer 要各自追進度</td>
          <td>replay、<a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a>、<a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a></td>
          <td>stream / <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a></td>
      </tr>
      <tr>
          <td>多個訂閱者即時收到訊息</td>
          <td><a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 與即時通知</td>
          <td><a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a></td>
      </tr>
      <tr>
          <td>資料寫入和事件發布要一起可靠</td>
          <td>交易一致性與補送</td>
          <td>outbox</td>
      </tr>
  </tbody>
</table>
<p>這張表是索引。選型時要看事件是否能遺失、是否會重複、是否要重播、是否要多個服務各自消費。</p>
<h2 id="判讀local-worker-承擔-process-內背景工作">【判讀】local worker 承擔 process 內背景工作</h2>
<p><a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Local worker</a> 的核心責任是把工作從 request 等待時間中拆出來，但仍留在同一個 process 裡。當工作可以接受 process 重啟後消失，或上游可以重新觸發，<a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">local worker</a> 通常足夠。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>request 完成後寫一筆非關鍵 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a></li>
<li>在同一服務內批次刷新短生命週期快取</li>
<li>定期清理 memory repository 裡的過期資料</li>
</ul>
<p>這類設計的主要風險是生命週期。worker 要能停止、記錄錯誤、控制 queue full，並在 shutdown 時有明確策略。語言教材通常會處理這一層，例如 Go 的 <code>Run(ctx)</code>、<a href="/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel</a> 與 <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>。</p>
<h2 id="判讀durable-queue-承擔可重試工作">【判讀】durable queue 承擔可重試工作</h2>
<p><a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">Durable queue</a> 的核心責任是讓工作在 process 重啟、暫時失敗或 consumer 下線後仍能被處理。當事件可以延後，但需要可靠送達與重試，應評估 broker queue。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>付款成功後寄送 email、簡訊與推播</li>
<li>上傳影片後排隊轉檔</li>
<li>訂單成立後建立出貨任務</li>
</ul>
<p>這類設計的主要風險是 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>。服務要決定 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a>、retry、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>、<a href="/blog/backend/knowledge-cards/poison-message/" data-link-title="Poison Message" data-link-desc="說明特定訊息內容如何穩定造成 consumer 失敗">poison message</a> 與 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>。RabbitMQ、NATS JetStream、Redis Streams 都可以承擔部分 durable delivery，但模型不同。</p>
<h2 id="判讀stream-承擔可重播事件序列">【判讀】stream 承擔可重播事件序列</h2>
<p>stream 的核心責任是保存事件序列，讓 consumer 可以依自己的進度讀取。當資料需要 replay、多個 consumer group、offset 或 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> ordering，stream 模型會比單純 queue 更合適。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>使用者行為事件進入分析 pipeline</li>
<li>訂單事件同時給推薦、風控、報表系統消費</li>
<li>IoT sensor readings 需要持續聚合與回放</li>
</ul>
<p>這類設計的主要風險是順序、保留期限與 schema 演進。Kafka、Redis Streams、NATS JetStream 都提供不同程度的 stream 能力；選型時要看 <a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a>、consumer group、保留策略與操作成本。</p>
<h2 id="判讀pubsub-承擔即時-fan-out">【判讀】pub/sub 承擔即時 fan-out</h2>
<p><a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a> 的核心責任是把訊息即時傳給目前訂閱者。當訊息偏向即時通知，且訂閱者離線後可以透過 <a href="/blog/backend/knowledge-cards/offline-catchup/" data-link-title="Offline Catch-up" data-link-desc="說明訂閱者離線後如何補回缺失事件或狀態">offline catch-up</a> 補狀態，<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 通常是好候選。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> server 跨節點廣播 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> update</li>
<li>presence 狀態變更通知在線 client</li>
<li><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 即時刷新目前任務進度</li>
</ul>
<p>這類設計的主要風險是 <a href="/blog/backend/knowledge-cards/reliability-boundary/" data-link-title="Reliability Boundary" data-link-desc="說明系統在哪個邊界內承諾可靠傳遞，邊界外需要哪些補償機制">reliability boundary</a>。pub/sub 適合即時 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>；若訊息需要 <a href="/blog/backend/knowledge-cards/offline-catchup/" data-link-title="Offline Catch-up" data-link-desc="說明訂閱者離線後如何補回缺失事件或狀態">offline catch-up</a>、audit 或 <a href="/blog/backend/knowledge-cards/strong-reliability/" data-link-title="Strong Reliability" data-link-desc="說明高可靠事件路徑需要的保存、重試、去重與回復責任">strong reliability</a>，通常還需要 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>、<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 或資料庫狀態搭配。</p>
<h2 id="判讀outbox-承擔資料寫入與事件補送">【判讀】outbox 承擔資料寫入與事件補送</h2>
<p>outbox 的核心責任是把業務資料寫入和待發事件放進同一個資料庫交易，再由 publisher 補送。當狀態更新成功後必須可靠發布事件，outbox 是常見選型。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>訂單寫入成功後必須發布 <code>order.created</code></li>
<li>付款狀態更新後必須通知出貨與報表系統</li>
<li>帳號停用後必須可靠通知所有安全相關服務</li>
</ul>
<p>這類設計的主要風險是半成功。outbox 讓事件至少會被發現並補送；consumer 仍需要 idempotency，因為補送與重試可能造成重複投遞。</p>
<h2 id="判讀用業務形狀反推-broker-候選">【判讀】用業務形狀反推 broker 候選</h2>
<p>反推的核心責任是把「目前場景需要的吞吐、延遲、保留窗口與操作承擔」轉成 broker 候選、不是從 vendor 規格表挑工具。先決定需求形狀、再對齊量級訊號、最後才挑工具。</p>
<p>接近真實網路服務的反推路徑：</p>
<ul>
<li>感測器一秒上報幾百筆、可接受偶發遺失、後端只需即時聚合 → broker 候選是 MQTT broker / NATS、量級訊號 sub-ms 延遲 + 萬到十萬 msg/sec</li>
<li>訂單事件需要多個下游服務各自 replay、保留 7 天以上 → broker 候選是 Kafka / Pulsar、量級訊號 partition 化吞吐 + retention 天 / 週 / 月可設</li>
<li>寄信、轉檔等可重試任務、不要遺失但允許短暫延遲 → broker 候選是 RabbitMQ / SQS、量級訊號萬級 msg/sec + ack/nack + dead-letter</li>
<li>跨節點即時通知在線 client、訂閱者離線可放棄 → broker 候選是 Redis Pub/Sub / NATS、量級訊號 sub-ms + 即時廣播、不保留</li>
</ul>
<p>反推的目的是把「broker 比較」轉成「需求對齊」、避免從 vendor 規格表開始挑工具。下面四個維度是反推時要對齊的量級訊號。</p>
<h3 id="吞吐量訊號">吞吐量訊號</h3>
<p>吞吐評估的核心問題是「broker 在我的 topology 下能撐多少」、不是「broker 規格上限」。同一個 broker 在不同 partition / queue / consumer / 訊息大小下、實際吞吐可以差一個量級。</p>
<p>實務量級（典型值、視配置與部署）：</p>
<table>
  <thead>
      <tr>
          <th>broker 類型</th>
          <th>單節點典型吞吐</th>
          <th>量級擴張條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MQTT broker</td>
          <td>萬到十萬 msg/sec</td>
          <td>連線數 / topic 樹深度</td>
      </tr>
      <tr>
          <td>RabbitMQ classic queue</td>
          <td>萬級 msg/sec</td>
          <td>quorum queue / stream / cluster scaling</td>
      </tr>
      <tr>
          <td>Redis Streams</td>
          <td>十萬 msg/sec</td>
          <td>shard / consumer group</td>
      </tr>
      <tr>
          <td>NATS JetStream</td>
          <td>十萬到百萬 msg/sec</td>
          <td>subject hierarchy / cluster</td>
      </tr>
      <tr>
          <td>Kafka</td>
          <td>百萬 msg/sec（partition + batch）</td>
          <td>partition 數 + batch.size + linger.ms</td>
      </tr>
      <tr>
          <td>Managed queue（SQS 等）</td>
          <td>視 account quota</td>
          <td>region / 訊息大小</td>
      </tr>
  </tbody>
</table>
<p>對齊的問題是尖峰打進來後 broker 是否仍有 headroom（見 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5 流量與資料量評估</a>）。穩定流量 × 尖峰倍率 × <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 倍率才是真正要對齊的數字。</p>
<h3 id="延遲訊號">延遲訊號</h3>
<p>延遲評估的核心問題是「業務能容忍 P99 多少」、跟 broker 級延遲特性對齊。請求-應答、fire-and-forget、事件流的可容忍延遲是不同量級。</p>
<p>實務量級：</p>
<ul>
<li>sub-ms 到個位數 ms：MQTT broker、NATS、Redis Pub/Sub — 即時通知 / 控制信號 / IoT 上報</li>
<li>個位數 ms：RabbitMQ classic queue、Redis Streams — 任務隊列 / 中等延遲事件</li>
<li>十 ms 到百 ms：Kafka（低 batch）、managed pub/sub — 事件流 / 分析 pipeline</li>
<li>百 ms 以上：Kafka 高 batch、SQS standard — 批次處理 / 容忍延遲的補送</li>
</ul>
<p>陷阱是把「broker 內部延遲」當成「端到端延遲」。實際端到端通常被 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 處理時間 + 下游 I/O 主導、不是 broker 傳遞時間。</p>
<h3 id="保留窗口訊號">保留窗口訊號</h3>
<p>保留窗口的核心問題是「事件需要被未來多久內的 consumer 讀到」。任務隊列吃掉就丟、事件流要可 replay、分析 pipeline 要留週級到月級。</p>
<p>實務量級：</p>
<ul>
<li>不保留 / 短期：Redis Pub/Sub、MQTT QoS 0 — 只給「現在」訂閱者</li>
<li>queue 級（持久但 ack 後刪）：RabbitMQ classic queue、SQS（最長 14 天）</li>
<li>中期（小時到天、受 RAM）：Redis Streams</li>
<li>天到月級（log-based、<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> policy）：Kafka、Pulsar、NATS JetStream</li>
<li>永久 / tiered：Kafka tiered storage、Pulsar tiered storage</li>
</ul>
<p>保留窗口直接影響成本：log-based broker 的儲存成本隨保留期線性增加、queue-based broker 的成本主要由「待處理深度」決定。</p>
<h3 id="操作複雜度訊號">操作複雜度訊號</h3>
<p>複雜度評估的核心問題是「團隊願意承擔哪些日常運維」、不是「broker 安裝多難」。安裝跟運維是不同量級工作。</p>
<p>實務量級：</p>
<ul>
<li>低（managed）：SQS、Google Pub/Sub — quota / IAM / DLQ drain 是主要工作</li>
<li>低到中（self-host 但運維輕）：Redis Streams、NATS — 跟 Redis / NATS 本體運維捆綁</li>
<li>中（broker 級運維）：RabbitMQ — Erlang / clustering / mirrored vs quorum / network partition 處理</li>
<li>高（平台級運維）：Kafka self-host — partition rebalance / <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> / KRaft / topic governance / 跨 cluster 路由</li>
</ul>
<p>複雜度的真正成本不在初期 setup、在「事故時誰能讀懂訊號」。挑 broker 時要問「下次 lag 暴增、團隊能在多久內找到原因」、這比 broker 規格表更接近真實業務考慮。</p>
<h3 id="反推的常見陷阱">反推的常見陷阱</h3>
<p>把「broker 規格上限」當需求對齊基準、會導致過度選型。Kafka 規格上百萬 msg/sec 不代表你需要 — 多數任務隊列場景在 RabbitMQ 萬級吞吐就足夠、Kafka 的 partition / consumer group / retention 治理成本反而是負擔。</p>
<p>把「現在吞吐」當未來基準、會導致欠選型。新 broker 通常要支撐 2-3 年成長、評估時要乘上預期成長倍率再對齊量級訊號。</p>
<p>把「規格表」當「實測值」、會在實際 topology 出問題。Broker 規格數字通常在最佳化測試環境得到、實際 production 受訊息大小 / consumer 速度 / 網路延遲 / replication factor 影響、實測常見差距 30%-60%。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入訊息傳遞實作章節：</p>
<ol>
<li>每種事件的投遞語意是否明確（可遺失、可重試、可重播）</li>
<li>事件失敗後的路徑是否明確（retry、DLQ、replay）</li>
<li>consumer 的去重責任是否明確（idempotency 範圍與語意鍵）</li>
<li>壓力保護條件是否明確（lag、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、降級觸發）</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03-message-queue</a></li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08-incident-response</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>非同步選型要先看工作需要什麼保證。本地工作用 <a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">local worker</a>，可重試工作用 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>，可重播事件序列用 stream，即時 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 用 <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a>，資料寫入與事件發布一致性用 outbox。分類清楚後，RabbitMQ、Kafka、NATS、Redis Streams 等工具比較才有意義。</p>
]]></content:encoded></item><item><title>2.3 TTL 與 eviction</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/</guid><description>&lt;p>存活時間與淘汰策略（TTL and eviction）的核心責任是把快取資源分配成可預期策略。TTL 決定資料可存活多久，eviction 決定容量壓力下誰先被移除；兩者共同定義快取的新鮮度、命中率與回源風險。&lt;/p>
&lt;h2 id="ttl-是新鮮度預算">TTL 是新鮮度預算&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 是資料類型的新鮮度預算，用單一時間常數理解它會漏掉關鍵差異。商品描述、推薦列表、活動文案可容忍較長 TTL；價格、庫存、配額、權限則需要更短 TTL 或事件失效。&lt;/p>
&lt;p>TTL 設計要連到業務代價。可容忍舊資料的欄位可用長 TTL 降回源壓力；不可容忍錯誤結果的欄位要搭配事件失效與版本控制，讓 TTL 只作為保底機制。&lt;/p>
&lt;h2 id="eviction-是容量分流策略">eviction 是容量分流策略&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 的責任是當記憶體不足時，優先保留最有價值資料。常見策略如 LRU、LFU、TTL-based eviction，各自偏好不同存取型態。&lt;/p>
&lt;p>策略選擇重點在流量形狀，演算法名稱是次要的：高重複讀取場景偏向保留高頻資料；大量一次性讀取場景需要避免短期噪音擠掉核心 key。快取層若同時承載多種資料，應分 key space 或分叢集管理，避免策略互相干擾。&lt;/p>
&lt;h2 id="hot--cold-data-的容量節奏">hot / cold data 的容量節奏&lt;/h2>
&lt;p>hot data 與 cold data 的差異不只在存取次數，也在回源成本與業務風險。熱資料 miss 會直接放大來源壓力，冷資料 miss 多半只影響單次延遲。容量規劃要先保護熱資料，再決定冷資料淘汰節奏。&lt;/p>
&lt;p>在促銷或重大活動期間，流量分布常快速改變。TTL 與 eviction 需要具備活動模式：預熱核心 key、分散過期時間、限制單批失效，讓來源系統不被同時回源壓垮。&lt;/p>
&lt;h2 id="分層快取的容量跟成本曲線">分層快取的容量跟成本曲線&lt;/h2>
&lt;p>當熱資料集合超過 DRAM 經濟範圍、單層快取會同時遇到成本跟命中率瓶頸、要把 cache 結構擴展到分層管理。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo&lt;/a> — Meta 把快取結構從 DRAM-only 擴展到 DRAM + flash 分層、改善容量跟成本平衡。當「全部熱資料塞 DRAM」變太貴、把次熱資料推到 flash、保留 DRAM 給最熱的子集。&lt;/p>
&lt;p>&lt;strong>分層快取的相對特性&lt;/strong>（具體 size / latency / cost 視硬體配置跟業務 workload）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>L1 (DRAM)&lt;/strong>：容量最小、延遲最低、單位成本最高、放最熱的子集 — Meta CacheLib 用這層保留熱度最高的 working set&lt;/li>
&lt;li>&lt;strong>L2 (flash / NVMe)&lt;/strong>：容量比 L1 大、延遲比 L1 高、單位成本比 L1 低 — Meta Kangaroo 在這層處理次熱資料&lt;/li>
&lt;li>&lt;strong>L3 (持久 KV)&lt;/strong>：容量最大、延遲最高、單位成本最低、放冷資料跟 fallback&lt;/li>
&lt;/ul>
&lt;p>落層策略要看 &lt;em>資料熱度分布&lt;/em>。Zipfian 分布（80/20 法則）下、L1 放最熱 20% 就能命中大部分；如果分布更平、要把 L1 擴大或接受更低命中率。具體 L1 / L2 大小比例要實測 workload 才能定。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve&lt;/a> — edge cache 跟 persistent reserve 的分層、長尾資料用 reserve 接住、降低 origin 回源。這是 &lt;em>同類設計思維&lt;/em> 在 CDN 場景的應用、但分層語意不同（edge cache 是地理分散的、Meta 分層是垂直記憶體 / flash 層）— 兩者都用「冷熱分離降低總成本」、實作機制差異需依場景區分。&lt;/p>
&lt;p>&lt;strong>Eviction 跟回補延遲要納入共同指標&lt;/strong>：分層 cache 的訊號不只看 L1 命中率、要看 L1 evict 到 L2 的速率、L2 回補到 L1 的延遲、L3 回源到 L2 的尾巴延遲。混合 metric 才能判斷分層策略是否健康。&lt;/p>
&lt;p>判讀重點：分層 cache 屬規模觸發的設計、要從 working set 大小判斷。Working set 在 DRAM 經濟範圍內、單層即可；working set 顯著超過 DRAM 容量、需分層讓 DRAM 集中放最熱子集、其餘走 flash 或更下層。&lt;/p></description><content:encoded><![CDATA[<p>存活時間與淘汰策略（TTL and eviction）的核心責任是把快取資源分配成可預期策略。TTL 決定資料可存活多久，eviction 決定容量壓力下誰先被移除；兩者共同定義快取的新鮮度、命中率與回源風險。</p>
<h2 id="ttl-是新鮮度預算">TTL 是新鮮度預算</h2>
<p><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 是資料類型的新鮮度預算，用單一時間常數理解它會漏掉關鍵差異。商品描述、推薦列表、活動文案可容忍較長 TTL；價格、庫存、配額、權限則需要更短 TTL 或事件失效。</p>
<p>TTL 設計要連到業務代價。可容忍舊資料的欄位可用長 TTL 降回源壓力；不可容忍錯誤結果的欄位要搭配事件失效與版本控制，讓 TTL 只作為保底機制。</p>
<h2 id="eviction-是容量分流策略">eviction 是容量分流策略</h2>
<p><a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 的責任是當記憶體不足時，優先保留最有價值資料。常見策略如 LRU、LFU、TTL-based eviction，各自偏好不同存取型態。</p>
<p>策略選擇重點在流量形狀，演算法名稱是次要的：高重複讀取場景偏向保留高頻資料；大量一次性讀取場景需要避免短期噪音擠掉核心 key。快取層若同時承載多種資料，應分 key space 或分叢集管理，避免策略互相干擾。</p>
<h2 id="hot--cold-data-的容量節奏">hot / cold data 的容量節奏</h2>
<p>hot data 與 cold data 的差異不只在存取次數，也在回源成本與業務風險。熱資料 miss 會直接放大來源壓力，冷資料 miss 多半只影響單次延遲。容量規劃要先保護熱資料，再決定冷資料淘汰節奏。</p>
<p>在促銷或重大活動期間，流量分布常快速改變。TTL 與 eviction 需要具備活動模式：預熱核心 key、分散過期時間、限制單批失效，讓來源系統不被同時回源壓垮。</p>
<h2 id="分層快取的容量跟成本曲線">分層快取的容量跟成本曲線</h2>
<p>當熱資料集合超過 DRAM 經濟範圍、單層快取會同時遇到成本跟命中率瓶頸、要把 cache 結構擴展到分層管理。</p>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo</a> — Meta 把快取結構從 DRAM-only 擴展到 DRAM + flash 分層、改善容量跟成本平衡。當「全部熱資料塞 DRAM」變太貴、把次熱資料推到 flash、保留 DRAM 給最熱的子集。</p>
<p><strong>分層快取的相對特性</strong>（具體 size / latency / cost 視硬體配置跟業務 workload）：</p>
<ul>
<li><strong>L1 (DRAM)</strong>：容量最小、延遲最低、單位成本最高、放最熱的子集 — Meta CacheLib 用這層保留熱度最高的 working set</li>
<li><strong>L2 (flash / NVMe)</strong>：容量比 L1 大、延遲比 L1 高、單位成本比 L1 低 — Meta Kangaroo 在這層處理次熱資料</li>
<li><strong>L3 (持久 KV)</strong>：容量最大、延遲最高、單位成本最低、放冷資料跟 fallback</li>
</ul>
<p>落層策略要看 <em>資料熱度分布</em>。Zipfian 分布（80/20 法則）下、L1 放最熱 20% 就能命中大部分；如果分布更平、要把 L1 擴大或接受更低命中率。具體 L1 / L2 大小比例要實測 workload 才能定。</p>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a> — edge cache 跟 persistent reserve 的分層、長尾資料用 reserve 接住、降低 origin 回源。這是 <em>同類設計思維</em> 在 CDN 場景的應用、但分層語意不同（edge cache 是地理分散的、Meta 分層是垂直記憶體 / flash 層）— 兩者都用「冷熱分離降低總成本」、實作機制差異需依場景區分。</p>
<p><strong>Eviction 跟回補延遲要納入共同指標</strong>：分層 cache 的訊號不只看 L1 命中率、要看 L1 evict 到 L2 的速率、L2 回補到 L1 的延遲、L3 回源到 L2 的尾巴延遲。混合 metric 才能判斷分層策略是否健康。</p>
<p>判讀重點：分層 cache 屬規模觸發的設計、要從 working set 大小判斷。Working set 在 DRAM 經濟範圍內、單層即可；working set 顯著超過 DRAM 容量、需分層讓 DRAM 集中放最熱子集、其餘走 flash 或更下層。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>eviction rate 持續上升</td>
          <td>容量不足或 key/value 體積失控</td>
          <td>調整策略、拆分 key space、補容量</td>
      </tr>
      <tr>
          <td>hit rate 下降且 origin QPS 同步上升</td>
          <td>TTL 設定過短或過期同步爆發</td>
          <td>拉長部分 TTL、加入 jitter、分批更新</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 事件上升</td>
          <td>TTL 過長或失效機制不足</td>
          <td>縮短關鍵欄位 TTL、補事件失效</td>
      </tr>
      <tr>
          <td>熱門 key 在尖峰時段頻繁 miss</td>
          <td>熱資料未被優先保留</td>
          <td>預熱 hot set、調整 eviction 權重</td>
      </tr>
      <tr>
          <td>記憶體穩定但業務錯誤增加</td>
          <td>值語意失真，非容量問題</td>
          <td>檢查序列化版本、補新鮮度監控與驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 TTL 統一設定成同一數值，會掩蓋資料語意差異。快取策略應反映資料的重要性與可容忍延遲，而不是單一預設。</p>
<p>把 eviction 視為平台預設值即可，也常導致壓力失真。策略與流量形狀不對齊時，命中率看似可接受，來源系統仍可能在尖峰被回源壓垮。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>TTL/eviction 的容量節奏可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> 回寫。先看事件中的過期同步與回源尖峰，再回到本章檢查 TTL 分布、淘汰策略與熱資料保護是否同時成立。
這個案例主要支撐的是「容量淘汰與過期波形」判讀，不直接支撐資料庫交易切分或部署切流策略；若事件核心在交易提交或 rollout 批次，應轉到 1.3 或 5.2。</p>
<p>當 eviction 上升但命中率未明顯下降時，先補 value size 與 key 分布監控，再把量測定義回寫到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>TTL 與 eviction 設計會直接影響觀測、驗證與事故處理。</p>
<ol>
<li>與 2.2 的交接：讀寫失效流程落在 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside</a>。</li>
<li>與 4.17 的交接：新鮮度與容量訊號進入 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.20 的交接：尖峰演練與停損邊界進入 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 8.22 的交接：容量失配與快取事故教訓回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 TTL/eviction 放進失效流程，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要看容量與策略失配案例，接著讀 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>。</p>
]]></content:encoded></item><item><title>3.3 outbox pattern 與發佈一致性</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/</guid><description>&lt;p>這一章處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與訊息發佈之間的一致性問題，後續可以再延伸到 polling、relay 與 failure recovery。&lt;/p>
&lt;p>外部發件箱模式（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern&lt;/a>）的核心責任是讓資料提交與事件發布在失敗時保持可恢復一致。它把重複發布轉成可判讀、可去重、可補償的治理問題。&lt;/p>
&lt;h2 id="基本流程">基本流程&lt;/h2>
&lt;p>transaction outbox 的典型流程是：在同一資料庫交易內，同時寫入業務資料與 outbox 記錄；交易提交後，由 relay worker 讀取 outbox 並發布到 broker；發布成功後標記或刪除 outbox 記錄。&lt;/p>
&lt;p>這個流程把一致性問題從「跨系統兩段提交」改成「單系統交易 + 非同步重送」，讓失敗路徑更可控。&lt;/p>
&lt;h2 id="relay-worker">relay worker&lt;/h2>
&lt;p>relay worker 的責任是穩定發布與可恢復進度。worker 需要具備批次拉取、順序控制、重試策略與停損條件。進度管理要明確，避免重啟後漏發或重複失控。&lt;/p>
&lt;p>當流量上升時，relay 吞吐會成為關鍵瓶頸。穩定做法是分 shard 處理、限制批次大小、對重試與正常發布做通道分流。&lt;/p>
&lt;h2 id="發布失敗與補償">發布失敗與補償&lt;/h2>
&lt;p>發布失敗通常分為暫時性與系統性。暫時性故障走有限重試，系統性故障走隔離與告警。關鍵是保留 outbox 記錄與發布狀態，讓恢復時可重播。&lt;/p>
&lt;p>duplicate publish 在 outbox 模式下屬於預期現象。消費端需要配合 idempotency 機制，確保重複事件不會產生重複業務結果。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>outbox backlog 持續堆積&lt;/td>
 &lt;td>relay 吞吐不足或下游故障持續&lt;/td>
 &lt;td>擴充 worker、分流重試、啟動降級流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務資料已更新但下游狀態延遲明顯&lt;/td>
 &lt;td>發布延遲超出可接受窗口&lt;/td>
 &lt;td>提升 relay 優先級、補告警與可視化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>duplicate consume 比例上升&lt;/td>
 &lt;td>重試與重播增加，去重壓力上升&lt;/td>
 &lt;td>強化 consumer idempotency 與去重儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>relay 重啟後出現漏發&lt;/td>
 &lt;td>進度標記與交易邊界設計不穩&lt;/td>
 &lt;td>收斂進度策略、補恢復測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同步交易延遲上升且 outbox 寫入增加&lt;/td>
 &lt;td>outbox 表設計與索引不足&lt;/td>
 &lt;td>調整索引與分表策略、拆分熱路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 outbox 當作「一次解決一致性」的銀彈，會忽略消費端冪等與補償責任。outbox 保證的是發布可恢復，不是端到端結果自動正確。&lt;/p>
&lt;p>把 outbox 表當一般業務表無上限累積，也會放大查詢與維護成本。需要定義保留與清理節奏，並確保稽核需求有對應方案。&lt;/p>
&lt;h2 id="self-managed-vs-managed-broker-的長期-tco">Self-managed vs Managed broker 的長期 TCO&lt;/h2>
&lt;p>Broker 選型本質是 long-term TCO 決策、需評估雲端費用 + 工程稅 + 治理負擔三層成本。Self-managed Kafka 的容量規劃 + broker 數量 + 副本因子 + disk + ZooKeeper / KRaft 治理是長期工程 tax、每次擴容是工程專案。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration&lt;/a> — Spotify 從自管 Kafka 遷到 Google Cloud Pub/Sub、動機是 &lt;em>容量規劃的工程成本&lt;/em> 在 sustained growth 下變得不划算、非 Kafka 效能不足。對 7500 萬用戶的事件交付系統、把 broker 容量規劃跟運維負擔卸給 vendor、釋放工程團隊 capacity。&lt;/p>
&lt;p>&lt;strong>TCO 評估的真實成本項&lt;/strong>（9.C9 case 列前 4 項 + 雲端費用、第 5 項屬跨案例綜合）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Broker 雲端費用&lt;/strong>：明面成本、相對小&lt;/li>
&lt;li>&lt;strong>容量規劃工程&lt;/strong>：每季 partition planning、每年容量擴張專案&lt;/li>
&lt;li>&lt;strong>故障處理人力&lt;/strong>：broker 故障 oncall、ZooKeeper / KRaft 故障診斷&lt;/li>
&lt;li>&lt;strong>升級遷移成本&lt;/strong>：Kafka 每個 major version 升級是專案&lt;/li>
&lt;li>&lt;strong>跨團隊治理&lt;/strong>（從 3.C6 Uber 跨案例補充）：規模化後的 multi-tenant 隔離、quota 管理、observability 建設&lt;/li>
&lt;/ul>
&lt;p>判讀含義：Self-managed Kafka 在中小團隊可能比 Pub/Sub 便宜（雲端費用低）；但規模化後人力成本壓過雲端費用差、managed service 反而划算。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware Tanzu Kafka → MSK&lt;/a> 同樣是「自管 → managed」的決策。&lt;/p></description><content:encoded><![CDATA[<p>這一章處理 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與訊息發佈之間的一致性問題，後續可以再延伸到 polling、relay 與 failure recovery。</p>
<p>外部發件箱模式（<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a>）的核心責任是讓資料提交與事件發布在失敗時保持可恢復一致。它把重複發布轉成可判讀、可去重、可補償的治理問題。</p>
<h2 id="基本流程">基本流程</h2>
<p>transaction outbox 的典型流程是：在同一資料庫交易內，同時寫入業務資料與 outbox 記錄；交易提交後，由 relay worker 讀取 outbox 並發布到 broker；發布成功後標記或刪除 outbox 記錄。</p>
<p>這個流程把一致性問題從「跨系統兩段提交」改成「單系統交易 + 非同步重送」，讓失敗路徑更可控。</p>
<h2 id="relay-worker">relay worker</h2>
<p>relay worker 的責任是穩定發布與可恢復進度。worker 需要具備批次拉取、順序控制、重試策略與停損條件。進度管理要明確，避免重啟後漏發或重複失控。</p>
<p>當流量上升時，relay 吞吐會成為關鍵瓶頸。穩定做法是分 shard 處理、限制批次大小、對重試與正常發布做通道分流。</p>
<h2 id="發布失敗與補償">發布失敗與補償</h2>
<p>發布失敗通常分為暫時性與系統性。暫時性故障走有限重試，系統性故障走隔離與告警。關鍵是保留 outbox 記錄與發布狀態，讓恢復時可重播。</p>
<p>duplicate publish 在 outbox 模式下屬於預期現象。消費端需要配合 idempotency 機制，確保重複事件不會產生重複業務結果。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>outbox backlog 持續堆積</td>
          <td>relay 吞吐不足或下游故障持續</td>
          <td>擴充 worker、分流重試、啟動降級流程</td>
      </tr>
      <tr>
          <td>業務資料已更新但下游狀態延遲明顯</td>
          <td>發布延遲超出可接受窗口</td>
          <td>提升 relay 優先級、補告警與可視化</td>
      </tr>
      <tr>
          <td>duplicate consume 比例上升</td>
          <td>重試與重播增加，去重壓力上升</td>
          <td>強化 consumer idempotency 與去重儲存</td>
      </tr>
      <tr>
          <td>relay 重啟後出現漏發</td>
          <td>進度標記與交易邊界設計不穩</td>
          <td>收斂進度策略、補恢復測試</td>
      </tr>
      <tr>
          <td>同步交易延遲上升且 outbox 寫入增加</td>
          <td>outbox 表設計與索引不足</td>
          <td>調整索引與分表策略、拆分熱路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 outbox 當作「一次解決一致性」的銀彈，會忽略消費端冪等與補償責任。outbox 保證的是發布可恢復，不是端到端結果自動正確。</p>
<p>把 outbox 表當一般業務表無上限累積，也會放大查詢與維護成本。需要定義保留與清理節奏，並確保稽核需求有對應方案。</p>
<h2 id="self-managed-vs-managed-broker-的長期-tco">Self-managed vs Managed broker 的長期 TCO</h2>
<p>Broker 選型本質是 long-term TCO 決策、需評估雲端費用 + 工程稅 + 治理負擔三層成本。Self-managed Kafka 的容量規劃 + broker 數量 + 副本因子 + disk + ZooKeeper / KRaft 治理是長期工程 tax、每次擴容是工程專案。</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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration</a> — Spotify 從自管 Kafka 遷到 Google Cloud Pub/Sub、動機是 <em>容量規劃的工程成本</em> 在 sustained growth 下變得不划算、非 Kafka 效能不足。對 7500 萬用戶的事件交付系統、把 broker 容量規劃跟運維負擔卸給 vendor、釋放工程團隊 capacity。</p>
<p><strong>TCO 評估的真實成本項</strong>（9.C9 case 列前 4 項 + 雲端費用、第 5 項屬跨案例綜合）：</p>
<ul>
<li><strong>Broker 雲端費用</strong>：明面成本、相對小</li>
<li><strong>容量規劃工程</strong>：每季 partition planning、每年容量擴張專案</li>
<li><strong>故障處理人力</strong>：broker 故障 oncall、ZooKeeper / KRaft 故障診斷</li>
<li><strong>升級遷移成本</strong>：Kafka 每個 major version 升級是專案</li>
<li><strong>跨團隊治理</strong>（從 3.C6 Uber 跨案例補充）：規模化後的 multi-tenant 隔離、quota 管理、observability 建設</li>
</ul>
<p>判讀含義：Self-managed Kafka 在中小團隊可能比 Pub/Sub 便宜（雲端費用低）；但規模化後人力成本壓過雲端費用差、managed service 反而划算。對應 <a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware Tanzu Kafka → MSK</a> 同樣是「自管 → managed」的決策。</p>
<p><strong>Managed service 的取捨</strong>：</p>
<ul>
<li>Pub/Sub 自動 scaling、伴隨 vendor lock-in、cost-per-message 累積、message ordering / latency 特性跟 Kafka 差異</li>
<li>業務語意對映（Kafka partition / offset / consumer group 在 Pub/Sub 對映成 subscription / ordering key / message attribute）需重新校準、見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 跨 broker 業務語意對映</a></li>
<li>遷移本身需驗證業務語意 — 對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a> 的同類流程</li>
</ul>
<h2 id="broker-遷移的階段流程">Broker 遷移的階段流程</h2>
<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 的容量規劃成本">9.C9 Spotify</a> — broker 遷移屬高併發容量工程、需維持 producer 連續寫入、保證 message 不丟。Spotify case 列三階段（dual write → shadow → cutover）、本章補第四階段（Decommission）作為清理收尾。replay 模型差異見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Replay 跟 Idempotency 共設計</a>。</p>
<ol>
<li><strong>Dual-write</strong>：producer 同時寫兩個 broker、確保 cutover 前新 broker 有完整資料</li>
<li><strong>Shadow consume</strong>：新 broker 有獨立 consumer group 消費、驗證業務結果跟舊 broker 一致</li>
<li><strong>Cutover</strong>：流量逐步切到新 broker、保留舊 broker 為 fallback</li>
<li><strong>Decommission</strong>（本章補充、case 未明文）：確認新 broker 穩定後關掉舊 broker、清理舊架構</li>
</ol>
<p>遷移期容量規劃含義：</p>
<ul>
<li>Dual-write 期間 broker 雙倍流量（writer side）</li>
<li>Shadow consume 期間 consumer 雙倍負載（reader side）</li>
<li>業務驗證（mismatch tracking）期間有額外的對帳工作量</li>
</ul>
<p>跟 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移</a> 是同類流程、流程細節跟 evidence chain 可互相參考。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>outbox 一致性可用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 的恢復段落回寫。先看資料寫入與下游狀態同步是否脫節，再回到本章檢查 outbox backlog、relay 進度與重播策略。
這個案例主要支撐的是「提交後發布一致性」判讀，不直接支撐 broker 的底層投遞參數；若問題是 ack/partition 策略，應回到 3.1/3.2。</p>
<p>當資料已提交但事件遲到，或重播後副作用重複時，先調整 relay 節流與 consumer 冪等，再把驗證證據對齊 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：交易邊界語意回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 3.2 的交接：發布後重試與隔離回到 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">durable queue 與重試策略</a>。</li>
<li>與 3.4 的交接：消費冪等與重播回到 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計與去重</a>。</li>
<li>與 6.12 的交接：一致性驗證與重播演練回到 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">Idempotency 與 Replay 驗證</a>。</li>
<li>與 8.19 的交接：發布故障決策回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要從 outbox 延伸到消費恢復，接著讀 <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>。要看 queue 切換失敗時的一致性風險，接著讀 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>。</p>
]]></content:encoded></item><item><title>5.3 load balancer 合約</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</guid><description>&lt;p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。&lt;/p>
&lt;h2 id="contract-組成">contract 組成&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract&lt;/a> 可以拆成四個部分：&lt;/p>
&lt;ol>
&lt;li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。&lt;/li>
&lt;li>health contract：哪些訊號代表可接流量，何時摘除節點。&lt;/li>
&lt;li>connection contract：長短連線的 idle timeout、keepalive、重試規則。&lt;/li>
&lt;li>drain contract：版本切換時如何讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request 安全收斂。&lt;/li>
&lt;/ol>
&lt;p>這四個部分共同定義 rollout 的穩定性。服務端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。&lt;/p>
&lt;h2 id="draining-與-shutdown">draining 與 shutdown&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。&lt;/p>
&lt;p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。&lt;/p>
&lt;h2 id="timeout-與-sticky-session">timeout 與 sticky session&lt;/h2>
&lt;p>idle &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。&lt;/p>
&lt;h3 id="timeout-層級串聯">Timeout 層級串聯&lt;/h3>
&lt;p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>典型 timeout 範圍&lt;/th>
 &lt;th>設定位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Client / Browser&lt;/td>
 &lt;td>30-120 秒&lt;/td>
 &lt;td>前端 fetch / axios / SDK 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>CDN vendor 設定（Cloudflare / CloudFront）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load balancer&lt;/td>
 &lt;td>30-60 秒&lt;/td>
 &lt;td>LB idle timeout / request timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>HTTP server read/write timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database / Cache&lt;/td>
 &lt;td>1-5 秒&lt;/td>
 &lt;td>連線池 query timeout / connect timeout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。&lt;/p>
&lt;p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。&lt;/p></description><content:encoded><![CDATA[<p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。</p>
<h2 id="contract-組成">contract 組成</h2>
<p><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a> 可以拆成四個部分：</p>
<ol>
<li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。</li>
<li>health contract：哪些訊號代表可接流量，何時摘除節點。</li>
<li>connection contract：長短連線的 idle timeout、keepalive、重試規則。</li>
<li>drain contract：版本切換時如何讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request 安全收斂。</li>
</ol>
<p>這四個部分共同定義 rollout 的穩定性。服務端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。</p>
<h2 id="draining-與-shutdown">draining 與 shutdown</h2>
<p><a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。</p>
<p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。</p>
<h2 id="timeout-與-sticky-session">timeout 與 sticky session</h2>
<p>idle <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。</p>
<h3 id="timeout-層級串聯">Timeout 層級串聯</h3>
<p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型 timeout 範圍</th>
          <th>設定位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client / Browser</td>
          <td>30-120 秒</td>
          <td>前端 fetch / axios / SDK 設定</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>5-30 秒</td>
          <td>CDN vendor 設定（Cloudflare / CloudFront）</td>
      </tr>
      <tr>
          <td>Load balancer</td>
          <td>30-60 秒</td>
          <td>LB idle timeout / request timeout</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>5-30 秒</td>
          <td>HTTP server read/write timeout</td>
      </tr>
      <tr>
          <td>Database / Cache</td>
          <td>1-5 秒</td>
          <td>連線池 query timeout / connect timeout</td>
      </tr>
  </tbody>
</table>
<p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。</p>
<p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。</p>
<p><a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。</p>
<h3 id="lb--cdn-連線生命週期協調">LB + CDN 連線生命週期協調</h3>
<p>當 LB 上游有 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN</a> 時、兩層的 timeout / retry 行為要對齊、否則會出現「使用者已經 timeout 但 origin 還在處理」這類雙層不一致：</p>
<ul>
<li><strong>CDN edge timeout</strong> 通常比 origin LB timeout 短（5-30 秒）— edge 認定 origin 慢就放棄。若 origin LB timeout 是 60 秒、edge 在 30 秒已放棄回 504、origin 還在處理一個沒人在意的 request。應對齊兩邊的 timeout 上限。</li>
<li><strong>CDN retry policy</strong> 在 edge miss 後若拿不到 origin response、預設不會重試（避免雙倍 origin 流量）— LB 端的 idle timeout 設計要假設「只有一次機會」、不依賴上游重試</li>
<li><strong>長連線（WebSocket、SSE、gRPC）通常繞過 CDN</strong> — 直接連到 origin LB。這些連線的 idle timeout 跟一般 HTTP 不同、要單獨配置</li>
<li><strong>Edge cache HIT 時 LB 完全沒收到 request</strong> — 容量規劃時要把 cache hit ratio 算進 origin RPS、不是用使用者 RPS 直接 size LB</li>
</ul>
<p>詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的 origin protection 段。</p>
<h2 id="切流失敗的回退判讀">切流失敗的回退判讀</h2>
<p>切流失敗的回退判讀第一步是先分辨「平台問題」跟「流量生命週期問題」、再決定回退手法。平台問題用重啟服務恢復、流量生命週期問題用凍結切換並等待震盪收斂。回退手法錯位會把事故推進第二階段。</p>
<p>切流失敗的本質是 connection lifecycle 跟切換時序錯位、平台元件本身往往是健康的。對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：平台切流未先 Draining</a>：揭露切流失敗常因 connection lifecycle 管理錯位、重啟動作會放大震盪。以下基於通用工程知識展開回退節奏。</p>
<p>回退節奏有兩個時序階段、性質不同。</p>
<p><strong>第一階段：先讓震盪不擴大</strong>。發現切流失敗的第一動作是凍結 rollout（不再擴大切換範圍）跟恢復舊入口權重（把 LB 規則 / DNS 加權 / service mesh 流量切回舊版本主導）。新版本不立即關閉、保留作為對照證據。這個階段的目標是穩定當前狀態、為後續分析爭取時間、所有動作要在分鐘級內完成。</p>
<p><strong>第二階段：再讓系統可恢復</strong>。震盪不擴大後、進入「等待 + 修正」狀態。長連線跟 reconnect 風暴需要時間消化、盲目重啟新版本實例會把重連集中在新一輪實例上、造成 thundering herd。觀察連線數、reconnect rate、5xx 趨勢回到 baseline 是進入修正階段的訊號。修正動作聚焦於 drain window、idle timeout、health check、client retry 之間的節奏錯位、找出後修正、重新進入小範圍驗證。這個階段的時間尺度通常是小時級、不能用第一階段的緊急節奏對待。</p>
<p>兩階段時序不能合併。把第一階段（凍結 + 切回）跟第二階段（等待 + 修正）並列執行、會在連線尚未穩定時嘗試修正、造成第二次震盪。</p>
<p>回退時最常見的誤判是「LB 顯示新節點 healthy = 服務可服務」。LB 的健康判斷通常是定期 health check 通過，跟「該節點能承受重連潮」是不同問題。事故中要把這兩個訊號分開看：節點層健康（health check pass）、連線層健康（reconnect rate、長連線錯誤率、tail latency）。</p>
<h2 id="切流告警條件">切流告警條件</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 的「部署專屬告警條件」段：揭露切流期告警的三個核心訊號（批次內 5xx 突增、長連線重連率快速上升、rollback time 超過既定 RTO）。本段在 case 三條基礎上補第 4 條（per-version error rate 偏離）與操作建議。</p>
<p>切流期告警的核心責任是對應切流批次節奏、跟日常閾值分離。日常閾值在切流期會被切換本身的短暫波動觸發、變成 alert noise；切流期需要更嚴格的「批次內偏差」訊號。</p>
<p>可操作的切流期告警條件：</p>
<ul>
<li><strong>批次內 5xx 異常升高</strong>：當前批次相對於前一批的 5xx 升幅超過閾值、停止下一批。</li>
<li><strong>長連線重連率飆升</strong>：reconnect rate 超過 baseline N 倍、暗示 drain / timeout 錯位。</li>
<li><strong>回退時間超過 RTO</strong>：執行回退後恢復時間超過既定 RTO、升級為事故等級。</li>
<li><strong>per-version error rate 偏離</strong>：新舊版本 error rate 差距超過閾值、不收斂（屬本章補強、case 未明示）。</li>
</ul>
<p>這些告警的閾值要在 release plan 中先定義、進事故時直接套用、避免臨時拍定。把切流告警跟一般日常告警分流到不同 channel，避免事故團隊在切流期被日常 noise 淹沒。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間 5xx 上升且集中在舊版本</td>
          <td>drain 順序或窗口不足</td>
          <td>拉長 drain 時間、調整摘流順序</td>
      </tr>
      <tr>
          <td>readiness 通過但首批請求延遲高</td>
          <td>應用啟動完成與可服務條件未對齊</td>
          <td>細化 readiness 指標、補 startup gate</td>
      </tr>
      <tr>
          <td>reconnect storm 出現在切版後</td>
          <td>timeout 與連線生命週期不匹配</td>
          <td>調整 idle timeout、分批切流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 比例低時正常，擴到高比例出現抖動</td>
          <td>LB 權重策略與服務容量曲線不一致</td>
          <td>降低增量批次、補容量保護</td>
      </tr>
      <tr>
          <td>多租戶場景下單租戶延遲飆升</td>
          <td>sticky/routing policy 造成熱點聚集</td>
          <td>分離租戶路由、加入負載重平衡</td>
      </tr>
      <tr>
          <td>回退後 reconnect 風暴持續</td>
          <td>重啟動作放大震盪、未先恢復穩定路徑</td>
          <td>凍結切換、等連線數穩定、再修錯位點</td>
      </tr>
  </tbody>
</table>
<p>「回退後 reconnect 風暴持續」是切流事故中最容易誤判的訊號。判讀順序：先看是否「凍結切換」已執行（rollout 是否真的停了）、再看「舊入口權重」是否回到主導比例（DNS / LB 規則是否切回）、最後看連線數曲線是否進入下降。三項都做完仍見風暴持續、才考慮新版本實例層級的問題（image / config / runtime 漂移）、而非反向重啟新版本。解凍切換的條件是「連線數曲線回到 baseline + reconnect rate 低於閾值連續 N 分鐘」、不是「等夠久了就解凍」的時間導向。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 load balancer 當成「只做轉發」的元件，會忽略它在部署與事故中的決策角色。LB 設定定義了流量切換節奏、回退可行性與故障擴散速度。</p>
<p>Health check 跟 readiness 的混淆會在切換時暴露隱性風險。health contract 要反映服務真實 readiness — 含依賴連線池、必要 config、關鍵背景任務狀態 — 而非停在單一探針成功訊號。</p>
<p>把「LB 顯示節點 healthy」當作「服務可承受流量」的訊號，也是事故中的常見誤判。健康檢查通過跟承受重連潮是不同層級的訊號。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>流量契約可用 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 回寫。先看事件中的摘流量順序、drain 視窗與連線重建節奏，再回到本章判讀 connection contract 與 drain contract 是否對齊。</p>
<p>這個案例主要支撐的是「連線生命週期與摘流量順序」判讀，不直接支撐 container build 可重現性；若根因在映像與 runtime 漂移，應回到 5.1。</p>
<p>當回退後錯誤率仍高或重連風暴延續，通常表示 timeout 與 sticky policy 仍在放大舊連線狀態。先重建連線生命週期時序，再把回退判斷同步到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>load balancer contract 是部署平台與操作控制面的匯流點。</p>
<ol>
<li>與 5.6 的交接：drain 的生命週期定義與 workload 分類回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 04 的交接：版本切換訊號與錯誤率證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 06 的交接：canary 放行與回退條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 07 的交接：入口治理與管理面保護進入 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
<li>與 08 的交接：切換與回退判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發</a> 的交接：CDN 是 origin LB 的上游、edge miss 後流量進 origin LB、timeout / retry 設定要協調。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 LB 合約放進整體部署流程，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a> 與 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。要把部署切換接到事故流程，接著讀 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
]]></content:encoded></item><item><title>6.3 fuzz campaign</title><link>https://tarrragon.github.io/blog/backend/06-reliability/fuzz-campaign/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/fuzz-campaign/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fuzz-test/" data-link-title="Fuzz Test" data-link-desc="說明用隨機與變異輸入驗證解析器與邊界處理健壯性">Fuzz test&lt;/a> 把沒想過的輸入轉成可重播、可修補的失敗案例，補齊人工列舉無法觸及的邊界盲區。&lt;/p>
&lt;p>這一頁處理的是輸入空間的盲區。當 API、parser、codec 或 schema 的邊界不清楚時，fuzz 比人工列案例更能覆蓋非預期路徑。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 fuzz 的品質先看 target 選擇是否對準高風險輸入邊界，再看 corpus 是否持續收斂，最後看 crash 是否能轉成可回歸的修復。&lt;/p>
&lt;p>重點判斷：&lt;/p>
&lt;ul>
&lt;li>fuzz target 是否足夠小，能對準單一責任&lt;/li>
&lt;li>corpus 是否持續收斂，coverage delta 是否仍為正&lt;/li>
&lt;li>crash reproduction 是否可重播到同一條路徑&lt;/li>
&lt;li>修補後是否回寫成 regression test&lt;/li>
&lt;/ul>
&lt;h2 id="fuzz-target-設計">Fuzz target 設計&lt;/h2>
&lt;p>Fuzz target 是 fuzz campaign 的最小驗證單位，責任是把外部輸入導入一個可觀測邊界的函式。&lt;/p>
&lt;p>好的 target 對準單一 parser、codec、serializer 或 validation function，函式簽章接受原始位元組（如 &lt;code>func([]byte)&lt;/code> 或等效形式）。target 選擇的判準有三個：這個函式是否直接處理外部輸入、邊界行為是否不清楚、crash 是否有業務影響。&lt;/p>
&lt;p>target 粒度影響 fuzz 的效率與判讀價值。target 太大（整個 HTTP handler 含 auth / routing / DB 存取）會讓 crash 難以定位到具體邊界，因為 fuzz engine 需要同時探索太多分支，coverage 增長慢且 crash 歸因模糊。target 太小（單一 if 分支）會讓 coverage 增長無意義，因為分支行為已經被 unit test 覆蓋。&lt;/p>
&lt;p>常見的高價值 target 類型：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Target 類型&lt;/th>
 &lt;th>典型邊界風險&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Protocol parser&lt;/td>
 &lt;td>畸形封包、長度溢位、巢狀深度&lt;/td>
 &lt;td>HTTP header parser、gRPC frame decoder&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema deserializer&lt;/td>
 &lt;td>型別不匹配、缺欄位、巢狀物件遞迴&lt;/td>
 &lt;td>JSON/Protobuf/MessagePack deserializer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Image / media codec&lt;/td>
 &lt;td>buffer overflow、memory allocation&lt;/td>
 &lt;td>PNG decoder、PDF parser&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation function&lt;/td>
 &lt;td>邊界值、正則回溯、encoding 混淆&lt;/td>
 &lt;td>email validator、URL parser、SQL escaper&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Config parser&lt;/td>
 &lt;td>非預期組合、環境變數注入&lt;/td>
 &lt;td>YAML/TOML config loader&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="corpus-管理">Corpus 管理&lt;/h2>
&lt;p>Corpus 累積有效的輸入種子，讓 fuzz engine 能從已知邊界往外探索。corpus 品質直接決定 fuzz campaign 的探索效率。&lt;/p>
&lt;p>初始 corpus 從三個來源收集：unit test 的既有 fixture（已知的合法與邊界輸入）、production sample 脫敏後的真實請求（反映實際流量的輸入結構）、schema 範例與文件中的合法樣本。初始 corpus 的重點是涵蓋主要合法路徑，讓 fuzz engine 從合法輸入開始 mutation，更容易觸達邊界。&lt;/p>
&lt;p>持續擴充靠 coverage-guided mutation。fuzz engine 每次產生的 mutated input 若觸發了新的 code path（新分支、新呼叫），這個 input 會自動加入 corpus。隨著 campaign 進行，corpus 會累積越來越多能觸達深層分支的種子。&lt;/p>
&lt;p>corpus 品質的判讀指標是 coverage delta trend — 每個時段新增的 code path 數量。coverage delta 持續為正代表 corpus 仍在有效探索；coverage delta 趨近零代表當前 target 的探索接近飽和，應考慮三個方向：切換到新 target、調整 mutation dictionary（加入 domain-specific token）、或擴充初始 corpus 的多樣性。&lt;/p>
&lt;p>corpus 需要持久化管理。corpus 檔案應納入版本控制或 artifact storage，跨 CI job 保留。每次 fuzz campaign 結束時，新發現的有效種子合併回 corpus；crash input 在修復後轉成 regression fixture，從 fuzz corpus 移到 test fixture。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/fuzz-test/" data-link-title="Fuzz Test" data-link-desc="說明用隨機與變異輸入驗證解析器與邊界處理健壯性">Fuzz test</a> 把沒想過的輸入轉成可重播、可修補的失敗案例，補齊人工列舉無法觸及的邊界盲區。</p>
<p>這一頁處理的是輸入空間的盲區。當 API、parser、codec 或 schema 的邊界不清楚時，fuzz 比人工列案例更能覆蓋非預期路徑。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 fuzz 的品質先看 target 選擇是否對準高風險輸入邊界，再看 corpus 是否持續收斂，最後看 crash 是否能轉成可回歸的修復。</p>
<p>重點判斷：</p>
<ul>
<li>fuzz target 是否足夠小，能對準單一責任</li>
<li>corpus 是否持續收斂，coverage delta 是否仍為正</li>
<li>crash reproduction 是否可重播到同一條路徑</li>
<li>修補後是否回寫成 regression test</li>
</ul>
<h2 id="fuzz-target-設計">Fuzz target 設計</h2>
<p>Fuzz target 是 fuzz campaign 的最小驗證單位，責任是把外部輸入導入一個可觀測邊界的函式。</p>
<p>好的 target 對準單一 parser、codec、serializer 或 validation function，函式簽章接受原始位元組（如 <code>func([]byte)</code> 或等效形式）。target 選擇的判準有三個：這個函式是否直接處理外部輸入、邊界行為是否不清楚、crash 是否有業務影響。</p>
<p>target 粒度影響 fuzz 的效率與判讀價值。target 太大（整個 HTTP handler 含 auth / routing / DB 存取）會讓 crash 難以定位到具體邊界，因為 fuzz engine 需要同時探索太多分支，coverage 增長慢且 crash 歸因模糊。target 太小（單一 if 分支）會讓 coverage 增長無意義，因為分支行為已經被 unit test 覆蓋。</p>
<p>常見的高價值 target 類型：</p>
<table>
  <thead>
      <tr>
          <th>Target 類型</th>
          <th>典型邊界風險</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Protocol parser</td>
          <td>畸形封包、長度溢位、巢狀深度</td>
          <td>HTTP header parser、gRPC frame decoder</td>
      </tr>
      <tr>
          <td>Schema deserializer</td>
          <td>型別不匹配、缺欄位、巢狀物件遞迴</td>
          <td>JSON/Protobuf/MessagePack deserializer</td>
      </tr>
      <tr>
          <td>Image / media codec</td>
          <td>buffer overflow、memory allocation</td>
          <td>PNG decoder、PDF parser</td>
      </tr>
      <tr>
          <td>Validation function</td>
          <td>邊界值、正則回溯、encoding 混淆</td>
          <td>email validator、URL parser、SQL escaper</td>
      </tr>
      <tr>
          <td>Config parser</td>
          <td>非預期組合、環境變數注入</td>
          <td>YAML/TOML config loader</td>
      </tr>
  </tbody>
</table>
<h2 id="corpus-管理">Corpus 管理</h2>
<p>Corpus 累積有效的輸入種子，讓 fuzz engine 能從已知邊界往外探索。corpus 品質直接決定 fuzz campaign 的探索效率。</p>
<p>初始 corpus 從三個來源收集：unit test 的既有 fixture（已知的合法與邊界輸入）、production sample 脫敏後的真實請求（反映實際流量的輸入結構）、schema 範例與文件中的合法樣本。初始 corpus 的重點是涵蓋主要合法路徑，讓 fuzz engine 從合法輸入開始 mutation，更容易觸達邊界。</p>
<p>持續擴充靠 coverage-guided mutation。fuzz engine 每次產生的 mutated input 若觸發了新的 code path（新分支、新呼叫），這個 input 會自動加入 corpus。隨著 campaign 進行，corpus 會累積越來越多能觸達深層分支的種子。</p>
<p>corpus 品質的判讀指標是 coverage delta trend — 每個時段新增的 code path 數量。coverage delta 持續為正代表 corpus 仍在有效探索；coverage delta 趨近零代表當前 target 的探索接近飽和，應考慮三個方向：切換到新 target、調整 mutation dictionary（加入 domain-specific token）、或擴充初始 corpus 的多樣性。</p>
<p>corpus 需要持久化管理。corpus 檔案應納入版本控制或 artifact storage，跨 CI job 保留。每次 fuzz campaign 結束時，新發現的有效種子合併回 corpus；crash input 在修復後轉成 regression fixture，從 fuzz corpus 移到 test fixture。</p>
<h2 id="crash-reproduction-與-minimization">Crash reproduction 與 minimization</h2>
<p>Fuzz 找到 crash 後的處理流程是 reproduce → minimize → fix → 回灌 regression test。</p>
<p><strong>Reproduce</strong>：用 fuzz engine 產出的 crash input 在相同環境重跑，確認 crash 可穩定觸發。不可穩定觸發的 crash 通常來自 race condition 或環境差異，需要額外的 concurrency 或環境控制才能定位。</p>
<p><strong>Minimize</strong>：minimization 把觸發 crash 的輸入縮到最小等效形式，讓 root cause 更容易定位。自動化 minimizer（如 Go 內建的 fuzz minimizer、libFuzzer 的 <code>-minimize_crash=1</code>）會反覆刪減 input 中的位元組，保留能觸發同一 crash 的最小子集。minimized input 通常比原始 input 短一到兩個數量級，讓開發者能直接看出觸發條件。</p>
<p><strong>Fix 與 regression test</strong>：修復 crash 後，用 minimized input 作為 fixture 寫成 regression test。這個 test 確保同類 bug 不再出現，也讓未來的 refactor 不會重新打開已修復的邊界。regression test 歸入 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">CI pipeline</a> 的 fast path，每次 push 都跑。</p>
<h2 id="ci-整合">CI 整合</h2>
<p>Fuzz 在 CI 的執行模式跟 unit test 不同。unit test 有明確的 pass/fail 結束條件，fuzz campaign 是開放式探索，執行時間越長覆蓋越廣。</p>
<p>CI 整合分兩種模式，對齊 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a> 的分層策略：</p>
<p><strong>Fast path regression</strong>（30 秒至 5 分鐘）：用既有 corpus 跑 fuzz，確認已知邊界沒退化。這個模式的目標是 regression 檢查，每次 push 觸發。corpus 裡的種子已經覆蓋了過去發現的邊界，短時間跑完可以確保修復沒被破壞、新變更沒引入已知類型的 crash。</p>
<p><strong>Scheduled exploration</strong>（小時級）：定期（每日或每週）跑長時間 fuzz，讓 engine 有足夠時間做深層 mutation 與路徑探索。新發現的種子合併回 corpus，crash input 產生 issue 或 alert。這個模式的 coverage delta 是判讀 campaign 價值的主要指標。</p>
<p>CI 整合的關鍵是 corpus 持久化。corpus 必須跨 job 保存（cache、artifact storage 或版本控制），每次 job 從上一次的 corpus 繼續探索。若 corpus 每次從零開始，fuzz engine 會重複探索已知路徑，浪費運算資源。</p>
<h2 id="coverage-門檻與收斂判讀">Coverage 門檻與收斂判讀</h2>
<p>Fuzz coverage 跟 unit test coverage 的意義不同。unit test coverage 衡量的是「多少行被執行過」，fuzz coverage 衡量的是「多少邊界被探索過」。同一個函式的 fuzz coverage 可以隨 corpus 擴充持續增長，因為 mutation 會觸發不同的分支組合。</p>
<p>判讀 fuzz campaign 是否仍有價值靠兩個指標：coverage delta trend（每小時新增多少 code path）與 corpus size growth（每小時新增多少有效種子）。兩者同時趨近零代表當前 target 的探索飽和。</p>
<p>飽和訊號指引兩個決策。第一，是否切換 target — 當前 target 的邊界已被充分探索，把 fuzz 資源移到另一個高風險 target 的邊際價值更高。第二，是否調整 mutation dictionary — 加入 domain-specific token（如 SQL keyword、JSON structure token、protocol magic bytes）可以讓 engine 更有效地觸達 domain-aware 的邊界。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">Google</a>：OSS-Fuzz 對大量基礎元件（parser、codec、serializer）做持續 fuzz，corpus 跨版本累積，crash 自動提 issue 並追蹤修復。這個規模的 fuzz campaign 說明 corpus 持久化與自動化 crash 處理是可擴展的前提。</li>
<li><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">Stripe</a>：API 與 serialization 邊界的 fuzz 需要 domain-specific dictionary（支付欄位、currency code、idempotency key 格式），通用 mutation 難以觸達業務語意上的邊界 crash。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：webhook payload 與 schema 邊界的 fuzz 適合用 schema-aware fuzzer，從 OpenAPI / JSON Schema 產生結構化 mutation，覆蓋嵌套物件與型別邊界。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fuzz corpus 從未更新、覆蓋率停滯</td>
          <td>campaign 已失去探索價值 — 檢查是否需要換 target 或調整 mutation strategy</td>
          <td>換 target 或加 mutation dictionary</td>
      </tr>
      <tr>
          <td>crash 復現靠人工 minimization</td>
          <td>minimization 應自動化 — 手動 minimization 耗時且不可重複</td>
          <td>啟用 fuzzer 內建 minimizer 或接 CI 自動化</td>
      </tr>
      <tr>
          <td>fuzz 找到 bug 沒回灌成 regression test</td>
          <td>修復後邊界可能被再次打開 — regression fixture 應歸入 CI fast path</td>
          <td>把 minimized input 加入 CI regression 套件</td>
      </tr>
      <tr>
          <td>input boundary 無 spec、fuzz 範圍模糊</td>
          <td>target 選擇需要對齊 — 先定義哪些函式直接處理外部輸入</td>
          <td>盤點外部輸入函式、建立 target 清單</td>
      </tr>
      <tr>
          <td>production 出 crash 但 fuzz 沒抓到</td>
          <td>fuzz target 未覆蓋該輸入路徑 — 把 production crash input 加入 corpus</td>
          <td>補 target + 把 crash input 加入 seed</td>
      </tr>
      <tr>
          <td>coverage delta 持續為零但仍在跑長時間 fuzz</td>
          <td>資源浪費 — 飽和後應切換 target 或調整 dictionary</td>
          <td>停止當前 campaign、轉移資源到新 target</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：fuzz regression 歸入 fast path、exploration 歸入 scheduled path</li>
<li><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a>：schema fuzz 與契約驗證互補，contract 定義已知邊界、fuzz 探索未知邊界</li>
<li><a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 test data</a>：fuzz 找到的 crash input 沉澱成 seed 與 fixture</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：長時間 fuzz campaign 在 production-like 環境跑時需要資源邊界控制</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：security-relevant fuzz crash 可作為 release 阻擋條件</li>
<li><a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">8.9 事故型態庫</a>：recurrent crash pattern 抽象化成型態</li>
</ul>
]]></content:encoded></item><item><title>8.3 止血、降級與回復策略</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/</guid><description>&lt;p>止血、降級與回復策略的核心責任是讓事故處理有明確節奏：先停止擴散，再維持最小可用，最後回到可驗證穩態。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>止血、降級與回復是事故處理中不同時間尺度的三種策略。止血的責任是先把擴散停住，降級的責任是讓服務在功能變少的情況下仍能活著，回復的責任則是把系統帶回正常狀態。三者如果混在一起，現場就會失去優先序。&lt;/p>
&lt;p>這個節點先處理 containment，再處理完整回復。先問現在應不應該砍功能、切流量、停寫入、關入口，然後再問何時恢復、恢復後怎麼驗證。這樣讀，才會知道事故處理是先讓局勢可控，一下子把所有東西修好的思路反而會失序。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>containment priority&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation&lt;/a> path&lt;/li>
&lt;li>rollback checkpoints&lt;/li>
&lt;li>recovery validation&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>止血優先級跟回復優先級衝突、現場臨時做選擇&lt;/li>
&lt;li>rollback checkpoint 沒測、按下去才知道掛了&lt;/li>
&lt;li>degradation 路徑沒設計、事故時臨時砍功能&lt;/li>
&lt;li>recovery 完成判讀無客觀標準、靠 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 主觀宣告&lt;/li>
&lt;li>containment 後驗證關閉缺步驟、同事故反覆再起&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>止血的責任是把擴散先停住。當事故正在擴大時，最重要的是先讓影響面停止擴張，恢復所有功能是後續階段的事。這可能意味著切流量、停寫入、暫時關閉某些入口，或把高風險功能降級。止血做得越早，後面的回復成本通常越低。&lt;/p>
&lt;p>降級的責任是讓服務保持最小可用狀態。不是所有事故都能立即回復，有些事故需要先讓部分功能退場，再用 degraded mode 撐住核心路徑。回復的責任則是把系統帶回完整狀態，並在回來之後做驗證，確認事故沒有再起。&lt;/p>
&lt;p>判讀止血策略時，先看擴散速度，再看回復可行性。當 error rate、impact scope 或依賴失效還在擴大，優先目標是停止擴散；當擴散停止且穩態訊號開始回線，才進入回復節奏。&lt;/p>
&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>Containment&lt;/td>
 &lt;td>影響面還在擴大嗎&lt;/td>
 &lt;td>error rate 不再上升、impact scope 不再擴張&lt;/td>
 &lt;td>限流、停寫入、隔離 tenant、停入口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Degradation&lt;/td>
 &lt;td>能否保住核心旅程&lt;/td>
 &lt;td>核心成功率維持門檻、次要功能可暫停&lt;/td>
 &lt;td>read-only、fallback、load shedding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>是否可逐步回到完整服務&lt;/td>
 &lt;td>依賴穩定、資料一致性可驗證、回復步驟可重播&lt;/td>
 &lt;td>分批恢復、回放驗證、解除降級&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation&lt;/td>
 &lt;td>是否可宣告恢復與關閉事故&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a> 回線、關鍵指標連續達標&lt;/td>
 &lt;td>宣告恢復、進入 post-incident review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>止血決策的重點不是「修好」，而是「先不要更壞」。回復決策的重點不是「盡快全開」，而是「按可驗證順序回線」。&lt;/p>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;p>AWS S3 和 Cloudflare 很適合看止血，因為這兩類事故最容易出現配置推送後的快速擴散，必須先切開傳播路徑。GitHub 與 Azure AD 適合看回復順序，因為 replication 與 identity 問題都會讓回復比止血慢得多。Slack、Discord 與 Datadog 則適合看降級，因為通訊平台和觀測平台在事故中都可能需要先維持部分能力，再逐步恢復完整服務。&lt;/p>
&lt;p>Atlassian、Roblox 與 Heroku 也能提供不同視角。Atlassian 告訴我們多租戶誤刪後，降級與恢復要和客戶通訊一起走；Roblox 告訴我們 prolonged recovery 需要長尾驗證；Heroku 告訴我們入口路由出問題時，先止血比硬修單一應用更重要。這些案例放在一起，會讓 containment 成為一條具體的操作路線，而不是抽象口號。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>stop the bleed&lt;/td>
 &lt;td>先讓影響面停止擴散&lt;/td>
 &lt;td>流量下降、錯誤率不再上升&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>degrade safely&lt;/td>
 &lt;td>保住核心功能，放掉非必要功能&lt;/td>
 &lt;td>核心路徑可用、次要功能關閉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recover service&lt;/td>
 &lt;td>把服務帶回正常&lt;/td>
 &lt;td>功能恢復、依賴穩定、指標回穩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>validate again&lt;/td>
 &lt;td>確認事故沒有反覆&lt;/td>
 &lt;td>重放失敗情境、觀察是否再起&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些步驟的價值在於順序。事故處理常見的錯誤，是把 recover service 當成第一步，結果在局勢還沒穩定前就把風險重新打開。&lt;/p>
&lt;h2 id="案例回扣">案例回扣&lt;/h2>
&lt;p>Cloudflare 2019 的教訓是規則推送錯誤會在秒級擴散，containment 必須先切傳播路徑，再處理規則內容。AWS S3 2017 的教訓是共享子系統恢復有順序，對外通訊要清楚分開「哪些操作已恢復、哪些仍在回復中」。&lt;/p>
&lt;p>這兩個案例都指向同一件事：回復順序與驗證門檻必須早於「全面恢復」承諾，否則會產生二次失信與反覆事故。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>止血與回復同時全開&lt;/td>
 &lt;td>還在擴散就開始大規模回復&lt;/td>
 &lt;td>先完成 containment，再進 recovery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回復無分批&lt;/td>
 &lt;td>一次全開導致次生異常&lt;/td>
 &lt;td>用 staged recovery + checkpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>宣告恢復靠主觀感覺&lt;/td>
 &lt;td>指標短暫回穩就關閉事故&lt;/td>
 &lt;td>以 6.22 steady state 的連續門檻判斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通訊與狀態不同步&lt;/td>
 &lt;td>對外說已恢復，內部仍在手動修復&lt;/td>
 &lt;td>對外更新必須引用 8.19 decision log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只修功能不修流程&lt;/td>
 &lt;td>下次遇到同型事故仍無路由&lt;/td>
 &lt;td>回寫 8.22 evidence write-back&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR 演練與 Rollback Rehearsal&lt;/a>：演練結果作為事中決策素材&lt;/li>
&lt;li>08.15 vendor 事故：依賴方掛掉時的止血手段&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance&lt;/a>：ops flag（kill switch）作為事中止血手段&lt;/li>
&lt;li>08.17 security vs operational：止血策略差異&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>：把止血邊界轉成演練門檻&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition&lt;/a>：用同一門檻判斷恢復完成&lt;/li>
&lt;li>08.19 incident decision log：記錄每一步的條件與回退門檻&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>止血、降級與回復策略的核心責任是讓事故處理有明確節奏：先停止擴散，再維持最小可用，最後回到可驗證穩態。</p>
<h2 id="概念定位">概念定位</h2>
<p>止血、降級與回復是事故處理中不同時間尺度的三種策略。止血的責任是先把擴散停住，降級的責任是讓服務在功能變少的情況下仍能活著，回復的責任則是把系統帶回正常狀態。三者如果混在一起，現場就會失去優先序。</p>
<p>這個節點先處理 containment，再處理完整回復。先問現在應不應該砍功能、切流量、停寫入、關入口，然後再問何時恢復、恢復後怎麼驗證。這樣讀，才會知道事故處理是先讓局勢可控，一下子把所有東西修好的思路反而會失序。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>containment priority</li>
<li><a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a> path</li>
<li>rollback checkpoints</li>
<li>recovery validation</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>止血優先級跟回復優先級衝突、現場臨時做選擇</li>
<li>rollback checkpoint 沒測、按下去才知道掛了</li>
<li>degradation 路徑沒設計、事故時臨時砍功能</li>
<li>recovery 完成判讀無客觀標準、靠 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 主觀宣告</li>
<li>containment 後驗證關閉缺步驟、同事故反覆再起</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>止血的責任是把擴散先停住。當事故正在擴大時，最重要的是先讓影響面停止擴張，恢復所有功能是後續階段的事。這可能意味著切流量、停寫入、暫時關閉某些入口，或把高風險功能降級。止血做得越早，後面的回復成本通常越低。</p>
<p>降級的責任是讓服務保持最小可用狀態。不是所有事故都能立即回復，有些事故需要先讓部分功能退場，再用 degraded mode 撐住核心路徑。回復的責任則是把系統帶回完整狀態，並在回來之後做驗證，確認事故沒有再起。</p>
<p>判讀止血策略時，先看擴散速度，再看回復可行性。當 error rate、impact scope 或依賴失效還在擴大，優先目標是停止擴散；當擴散停止且穩態訊號開始回線，才進入回復節奏。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>決策問題</th>
          <th>最小門檻</th>
          <th>常見動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Containment</td>
          <td>影響面還在擴大嗎</td>
          <td>error rate 不再上升、impact scope 不再擴張</td>
          <td>限流、停寫入、隔離 tenant、停入口</td>
      </tr>
      <tr>
          <td>Degradation</td>
          <td>能否保住核心旅程</td>
          <td>核心成功率維持門檻、次要功能可暫停</td>
          <td>read-only、fallback、load shedding</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>是否可逐步回到完整服務</td>
          <td>依賴穩定、資料一致性可驗證、回復步驟可重播</td>
          <td>分批恢復、回放驗證、解除降級</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>是否可宣告恢復與關閉事故</td>
          <td><a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 回線、關鍵指標連續達標</td>
          <td>宣告恢復、進入 post-incident review</td>
      </tr>
  </tbody>
</table>
<p>止血決策的重點不是「修好」，而是「先不要更壞」。回復決策的重點不是「盡快全開」，而是「按可驗證順序回線」。</p>
<h2 id="案例對照">案例對照</h2>
<p>AWS S3 和 Cloudflare 很適合看止血，因為這兩類事故最容易出現配置推送後的快速擴散，必須先切開傳播路徑。GitHub 與 Azure AD 適合看回復順序，因為 replication 與 identity 問題都會讓回復比止血慢得多。Slack、Discord 與 Datadog 則適合看降級，因為通訊平台和觀測平台在事故中都可能需要先維持部分能力，再逐步恢復完整服務。</p>
<p>Atlassian、Roblox 與 Heroku 也能提供不同視角。Atlassian 告訴我們多租戶誤刪後，降級與恢復要和客戶通訊一起走；Roblox 告訴我們 prolonged recovery 需要長尾驗證；Heroku 告訴我們入口路由出問題時，先止血比硬修單一應用更重要。這些案例放在一起，會讓 containment 成為一條具體的操作路線，而不是抽象口號。</p>
<h2 id="回復步驟">回復步驟</h2>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>目的</th>
          <th>常見驗證</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>stop the bleed</td>
          <td>先讓影響面停止擴散</td>
          <td>流量下降、錯誤率不再上升</td>
      </tr>
      <tr>
          <td>degrade safely</td>
          <td>保住核心功能，放掉非必要功能</td>
          <td>核心路徑可用、次要功能關閉</td>
      </tr>
      <tr>
          <td>recover service</td>
          <td>把服務帶回正常</td>
          <td>功能恢復、依賴穩定、指標回穩</td>
      </tr>
      <tr>
          <td>validate again</td>
          <td>確認事故沒有反覆</td>
          <td>重放失敗情境、觀察是否再起</td>
      </tr>
  </tbody>
</table>
<p>這些步驟的價值在於順序。事故處理常見的錯誤，是把 recover service 當成第一步，結果在局勢還沒穩定前就把風險重新打開。</p>
<h2 id="案例回扣">案例回扣</h2>
<p>Cloudflare 2019 的教訓是規則推送錯誤會在秒級擴散，containment 必須先切傳播路徑，再處理規則內容。AWS S3 2017 的教訓是共享子系統恢復有順序，對外通訊要清楚分開「哪些操作已恢復、哪些仍在回復中」。</p>
<p>這兩個案例都指向同一件事：回復順序與驗證門檻必須早於「全面恢復」承諾，否則會產生二次失信與反覆事故。</p>
<h2 id="常見反模式">常見反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>止血與回復同時全開</td>
          <td>還在擴散就開始大規模回復</td>
          <td>先完成 containment，再進 recovery</td>
      </tr>
      <tr>
          <td>回復無分批</td>
          <td>一次全開導致次生異常</td>
          <td>用 staged recovery + checkpoint</td>
      </tr>
      <tr>
          <td>宣告恢復靠主觀感覺</td>
          <td>指標短暫回穩就關閉事故</td>
          <td>以 6.22 steady state 的連續門檻判斷</td>
      </tr>
      <tr>
          <td>通訊與狀態不同步</td>
          <td>對外說已恢復，內部仍在手動修復</td>
          <td>對外更新必須引用 8.19 decision log</td>
      </tr>
      <tr>
          <td>只修功能不修流程</td>
          <td>下次遇到同型事故仍無路由</td>
          <td>回寫 8.22 evidence write-back</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR 演練與 Rollback Rehearsal</a>：演練結果作為事中決策素材</li>
<li>08.15 vendor 事故：依賴方掛掉時的止血手段</li>
<li><a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance</a>：ops flag（kill switch）作為事中止血手段</li>
<li>08.17 security vs operational：止血策略差異</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>：把止血邊界轉成演練門檻</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a>：用同一門檻判斷恢復完成</li>
<li>08.19 incident decision log：記錄每一步的條件與回退門檻</li>
</ul>
]]></content:encoded></item><item><title>模組三：訊息佇列與事件傳遞</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/</guid><description>&lt;p>訊息佇列模組的核心目標是說明事件離開單一 process 後，如何處理持久化、重試、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">重複投遞&lt;/a>與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 協調。語言教材會先處理本地 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> abstraction、publisher port、processor 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> interface；本模組負責 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 的具體語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/" data-link-title="訊息佇列 Vendor 清單" data-link-desc="規劃 broker、event streaming 與 managed queue 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 RabbitMQ / Kafka / NATS / Redis Streams / AWS SQS / Google Pub/Sub，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/" data-link-title="訊息佇列 Vendor 清單" data-link-desc="規劃 broker、event streaming 與 managed queue 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>RabbitMQ&lt;/td>
 &lt;td>exchange、queue、routing key、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NATS&lt;/td>
 &lt;td>subject、consumer、JetStream、at-least-once delivery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kafka&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a>、ordering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Redis Streams&lt;/td>
 &lt;td>stream、consumer group、pending entry、claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Outbox&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> outbox、poller、publisher、重試策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Idempotency&lt;/td>
 &lt;td>idempotency key、dedup store、replay safety&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>訊息佇列選型的核心判斷是工作離開 request 或 process 後需要什麼投遞保證。當工作需要排隊、重試、跨服務傳遞、多 consumer 協作或事件補送時，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 與 outbox 值得優先評估。&lt;/p>
&lt;p>RabbitMQ 適合明確 routing、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a> 與工作佇列；NATS 適合 subject-based messaging 與較輕量的服務通訊，搭配 JetStream 可加入持久化；Kafka 適合高吞吐事件流、partition 與長期 replay；Redis Streams 適合 Redis 生態內的 stream 與 consumer group；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox&lt;/a> 解決資料寫入與事件發布的一致性；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 解決重複投遞造成的結果穩定性；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter&lt;/a> 則控制故障期間的重試壓力。&lt;/p></description><content:encoded><![CDATA[<p>訊息佇列模組的核心目標是說明事件離開單一 process 後，如何處理持久化、重試、<a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">重複投遞</a>與 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 協調。語言教材會先處理本地 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> abstraction、publisher port、processor 與 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> interface；本模組負責 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 的具體語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/03-message-queue/vendors/" data-link-title="訊息佇列 Vendor 清單" data-link-desc="規劃 broker、event streaming 與 managed queue 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 RabbitMQ / Kafka / NATS / Redis Streams / AWS SQS / Google Pub/Sub，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/03-message-queue/vendors/" data-link-title="訊息佇列 Vendor 清單" data-link-desc="規劃 broker、event streaming 與 managed queue 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RabbitMQ</td>
          <td>exchange、queue、routing key、<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a></td>
      </tr>
      <tr>
          <td>NATS</td>
          <td>subject、consumer、JetStream、at-least-once delivery</td>
      </tr>
      <tr>
          <td>Kafka</td>
          <td><a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>、<a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a>、<a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a>、ordering</td>
      </tr>
      <tr>
          <td>Redis Streams</td>
          <td>stream、consumer group、pending entry、claim</td>
      </tr>
      <tr>
          <td>Outbox</td>
          <td><a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> outbox、poller、publisher、重試策略</td>
      </tr>
      <tr>
          <td>Idempotency</td>
          <td>idempotency key、dedup store、replay safety</td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>訊息佇列選型的核心判斷是工作離開 request 或 process 後需要什麼投遞保證。當工作需要排隊、重試、跨服務傳遞、多 consumer 協作或事件補送時，<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 與 outbox 值得優先評估。</p>
<p>RabbitMQ 適合明確 routing、<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 與工作佇列；NATS 適合 subject-based messaging 與較輕量的服務通訊，搭配 JetStream 可加入持久化；Kafka 適合高吞吐事件流、partition 與長期 replay；Redis Streams 適合 Redis 生態內的 stream 與 consumer group；<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox</a> 解決資料寫入與事件發布的一致性；<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 解決重複投遞造成的結果穩定性；<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a> 與 <a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a> 則控制故障期間的重試壓力。</p>
<p>接近真實網路服務的例子包括付款後寄信、影片轉檔、訂單事件傳給多個系統、IoT readings pipeline 與跨節點通知。這些場景的共同問題是 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>，因此本模組會先處理 broker 模型、retry、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a>、outbox 與 consumer 設計。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理本地 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>、processor 邊界、port / <a href="/blog/backend/knowledge-cards/message-protocol/" data-link-title="Message Protocol" data-link-desc="說明 queue 或 stream message 如何對齊格式與處理語意">Message Protocol</a> 設計與單一 process 內的去重。Backend message queue 模組處理 broker selection、ack/nack、DLQ、consumer group、outbox 與跨 process 重試。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>佇列案例的核心讀法是先辨識遷移的是「資料路徑」還是「治理路徑」，再決定先做 broker 切換還是治理收斂。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta：FOQS 全域遷移</a></td>
          <td><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1</a>、<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2</a></td>
          <td>把跨區 queue 路由與可用性邊界前置</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware：Kafka -&gt; MSK</a></td>
          <td><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1</a>、<a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a></td>
          <td>把 managed broker 遷移轉成 ACL/lag/回退治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn：TopicGC</a></td>
          <td><a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a></td>
          <td>把 topic 生命週期治理納入可靠性成本模型</td>
      </tr>
  </tbody>
</table>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>訊息佇列使用方式會受語言的 worker model、錯誤處理、序列化、背景任務框架與 idempotency 設計影響。同步 runtime 要控制 consumer thread 數量與 ack <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>；async runtime 要處理 backpressure 與 long-running handler；輕量並發 runtime 要限制同時處理量，避免 consumer 擴張超過下游容量。強型別語言適合建立 event schema 與 command model；動態語言要補足 payload validation、dead-letter 診斷與重播測試。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1</a></td>
          <td>broker 基礎與投遞模型</td>
          <td>看懂 exchange、topic、consumer 與 delivery semantics</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2</a></td>
          <td><a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a> 與重試策略</td>
          <td>規劃持久化、ack/nack、DLQ 與 retry</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3</a></td>
          <td><a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 與發佈一致性</td>
          <td>把交易寫入與事件發佈分離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a></td>
          <td>consumer 設計與去重</td>
          <td>設計 idempotency、<a href="/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint</a> 與 replay safety</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5</a></td>
          <td>攻擊者視角（紅隊）：傳遞層弱點判讀</td>
          <td>用重放、重複、毒訊息與延遲累積檢查非同步傳遞邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6</a></td>
          <td>Processing Semantics 與 Recovery Semantics</td>
          <td>分辨投遞成功、處理成功與恢復成功</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7</a></td>
          <td>Event Contract 與 Replay Boundary</td>
          <td>定義 event schema、idempotency key、replay window 與補償邊界</td>
      </tr>
      <tr>
          <td><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。">3.8</a></td>
          <td>Queue Consumer Retry 與 Replay Handoff 實作示範</td>
          <td>以訂單事件 consumer 示範 evidence、DLQ、replay runbook 與 decision log</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/" data-link-title="模組三案例正文" data-link-desc="訊息佇列與事件傳遞的轉換案例入口、含通用案例與 6 個 vendor 的真實 production case 庫。">3.C</a></td>
          <td>轉換案例正文</td>
          <td>把 queue 架構、broker 遷移與 topic 治理轉成可操作案例</td>
      </tr>
  </tbody>
</table>
<p>反例與規模對照入口： <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> / <a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，queue 案例要優先保留 delivery semantics、lag、DLQ 與 replay 條件。</p>
<p>跨模組端到端串聯（DB → cache → event → observability）見 <a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>訊息佇列章節下一輪的核心責任是把「投遞成功」和「業務結果正確」分開。現有章節已經有 broker、durable queue、outbox 與 consumer design，但還需要補上 delivery semantics、processing semantics 與 recovery semantics 的三層關係，讓讀者知道 queue 失敗同時包括訊息遺失、重複副作用、順序錯亂、重播風險與下游壓力放大。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Delivery semantics</td>
          <td>broker 如何 ack、nack、redelivery、retry、送入 DLQ</td>
          <td><a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>、<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2</a></td>
      </tr>
      <tr>
          <td>Processing semantics</td>
          <td>consumer 的副作用是否能承受重複、亂序與部分失敗</td>
          <td><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a></td>
      </tr>
      <tr>
          <td>Recovery semantics</td>
          <td>replay、checkpoint、offset 與補償是否可重播與驗證</td>
          <td><a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>Outbox boundary</td>
          <td>資料庫交易與事件發布是否有一致性邊界</td>
          <td><a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a></td>
      </tr>
      <tr>
          <td>Poison handling</td>
          <td>壞訊息是否會卡住 consumer 或被無限重試</td>
          <td><a href="/blog/backend/knowledge-cards/poison-message/" data-link-title="Poison Message" data-link-desc="說明特定訊息內容如何穩定造成 consumer 失敗">poison message</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用非同步服務自己的語意展開。寄信、開 invoice、更新 CRM、同步 search index、發 webhook 的副作用不同，retry、DLQ 與 replay 的判準也不同。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>佇列模組的 knowledge card 缺口集中在「處理語意」與「恢復語意」。已有 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a>、<a href="/blog/backend/knowledge-cards/poison-message/" data-link-title="Poison Message" data-link-desc="說明特定訊息內容如何穩定造成 consumer 失敗">poison message</a> 與 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 可以作為第一批錨點。</p>
<p>第二批卡片已補上 <a href="/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">processing semantics</a>、<a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">recovery semantics</a>、<a href="/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window</a>、<a href="/blog/backend/knowledge-cards/consumer-pause/" data-link-title="Consumer Pause" data-link-desc="說明暫停消費作為事故控制手段，止住錯誤副作用擴大">consumer pause</a>、<a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">event schema compatibility</a>、<a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">DLQ drain</a> 與 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison-message quarantine</a>。這些卡片讓讀者能分辨「queue 有持久化」和「consumer 結果可恢復」分屬不同責任。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>佇列的第一條實作路徑是 <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。">3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）</a>。這篇以 <code>order_created</code> consumer 為例，說明 idempotency evidence、DLQ handling、replay runbook 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用應該是 3.2 durable queue、3.3 outbox pattern、3.4 consumer design、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 與 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>佇列路徑的 artifact 對齊重點是「把投遞成功與處理成功拆開記錄」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 consumer lag、retry、DLQ 與 duplicate side-effect；對 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a> / <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 replay 範圍、去重驗證與補償路徑；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 pause consumer、drain DLQ、重播啟停的決策序列。</p>
]]></content:encoded></item><item><title>4.4 dashboard 與 alert 設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a> 設計原則：SLI 導向 vs 指標堆疊&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a> 設計：symptom-based vs cause-based&lt;/li>
&lt;li>Alert noise control 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook&lt;/a> linkage&lt;/li>
&lt;li>Dashboard / alert 的生命週期與 ownership&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 是把觀測訊號轉成操作入口的控制面，責任是讓團隊在正常巡檢與事故響應時看到同一組事實。&lt;/p>
&lt;p>Dashboard 讓人理解狀態，alert 讓人採取行動。兩者的設計問題不同：dashboard 的問題是「資訊太多、焦點不明」；alert 的問題是「通知太多、行動不明」。兩者都需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership&lt;/a>、生命週期管理與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 連結。&lt;/p>
&lt;h2 id="dashboard-設計">Dashboard 設計&lt;/h2>
&lt;h3 id="sli-導向-vs-指標堆疊">SLI 導向 vs 指標堆疊&lt;/h3>
&lt;p>Dashboard 的常見失敗模式是「把所有能拿到的指標都放上去」。二十個 panel、五十條曲線、無法在 3 秒內回答「服務現在健康嗎」。&lt;/p>
&lt;p>SLI 導向的 dashboard 從使用者體驗出發：第一排 panel 回答「使用者感受到的健康狀態」（availability、latency percentile、error ratio），第二排回答「健康狀態的原因」（dependency latency、queue depth、resource utilization），第三排回答「趨勢與容量」（traffic growth、storage usage、capacity headroom）。&lt;/p>
&lt;p>每個 panel 都應該能回答一個具體問題。如果團隊看了某個 panel 後的反應是「所以呢？」，這個 panel 不是放錯位置就是不該存在。&lt;/p>
&lt;h3 id="dashboard-層級">Dashboard 層級&lt;/h3>
&lt;p>不同使用者看不同層級的 dashboard。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。&lt;/p>
&lt;p>&lt;strong>Service overview&lt;/strong>：on-call 工程師的第一個入口。5-8 個 panel，回答「這個服務現在有沒有問題」。SLI 指標（error rate、latency p99、availability）、最近的 alert、dependency 健康。&lt;/p>
&lt;p>&lt;strong>Debug dashboard&lt;/strong>：事故中的深入診斷入口。按 dependency 分組（database panel group、cache panel group、downstream API panel group），每組顯示延遲、錯誤率、連線數。Panel 數量多但按需展開。&lt;/p>
&lt;p>&lt;strong>Capacity dashboard&lt;/strong>：容量規劃用。週到月級的趨勢圖 — traffic growth、storage usage、connection pool saturation、cost trends。刷新頻率低（每小時或每天），panel 讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 資料。&lt;/p>
&lt;p>&lt;strong>Business dashboard&lt;/strong>：給非工程角色看。轉換率、使用者活躍度、營收指標。資料來源可能不只是觀測訊號，還包括 analytics 跟 business metrics。&lt;/p>
&lt;h3 id="dashboard-的查詢效能">Dashboard 的查詢效能&lt;/h3>
&lt;p>Dashboard 是觀測查詢設計中「聚合趨勢」模式的主要消費者（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23&lt;/a>）。每個 panel 每 30 秒刷新一次，十個團隊各自有 dashboard 就是每分鐘數百個背景查詢。&lt;/p>
&lt;p>Panel 設計時要注意查詢成本：時間範圍越長、raw series 越多、聚合越複雜，query-time cost 越高。長時間趨勢 panel 應該讀 recording rule 或 rollup series，而非每次刷新都掃描 raw data。&lt;/p>
&lt;h2 id="alert-設計">Alert 設計&lt;/h2>
&lt;h3 id="symptom-based-vs-cause-based">Symptom-based vs cause-based&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert&lt;/a> 觸發在使用者可感知的症狀上 — error rate 升高、latency p99 超過閾值、availability 下降。Cause-based alert 觸發在內部原因上 — CPU &amp;gt; 90%、disk usage &amp;gt; 85%、connection pool exhausted。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a> 設計原則：SLI 導向 vs 指標堆疊</li>
<li><a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a> 設計：symptom-based vs cause-based</li>
<li>Alert noise control 與 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a></li>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a> linkage</li>
<li>Dashboard / alert 的生命週期與 ownership</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a> 與 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 是把觀測訊號轉成操作入口的控制面，責任是讓團隊在正常巡檢與事故響應時看到同一組事實。</p>
<p>Dashboard 讓人理解狀態，alert 讓人採取行動。兩者的設計問題不同：dashboard 的問題是「資訊太多、焦點不明」；alert 的問題是「通知太多、行動不明」。兩者都需要 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership</a>、生命週期管理與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 連結。</p>
<h2 id="dashboard-設計">Dashboard 設計</h2>
<h3 id="sli-導向-vs-指標堆疊">SLI 導向 vs 指標堆疊</h3>
<p>Dashboard 的常見失敗模式是「把所有能拿到的指標都放上去」。二十個 panel、五十條曲線、無法在 3 秒內回答「服務現在健康嗎」。</p>
<p>SLI 導向的 dashboard 從使用者體驗出發：第一排 panel 回答「使用者感受到的健康狀態」（availability、latency percentile、error ratio），第二排回答「健康狀態的原因」（dependency latency、queue depth、resource utilization），第三排回答「趨勢與容量」（traffic growth、storage usage、capacity headroom）。</p>
<p>每個 panel 都應該能回答一個具體問題。如果團隊看了某個 panel 後的反應是「所以呢？」，這個 panel 不是放錯位置就是不該存在。</p>
<h3 id="dashboard-層級">Dashboard 層級</h3>
<p>不同使用者看不同層級的 dashboard。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。</p>
<p><strong>Service overview</strong>：on-call 工程師的第一個入口。5-8 個 panel，回答「這個服務現在有沒有問題」。SLI 指標（error rate、latency p99、availability）、最近的 alert、dependency 健康。</p>
<p><strong>Debug dashboard</strong>：事故中的深入診斷入口。按 dependency 分組（database panel group、cache panel group、downstream API panel group），每組顯示延遲、錯誤率、連線數。Panel 數量多但按需展開。</p>
<p><strong>Capacity dashboard</strong>：容量規劃用。週到月級的趨勢圖 — traffic growth、storage usage、connection pool saturation、cost trends。刷新頻率低（每小時或每天），panel 讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 資料。</p>
<p><strong>Business dashboard</strong>：給非工程角色看。轉換率、使用者活躍度、營收指標。資料來源可能不只是觀測訊號，還包括 analytics 跟 business metrics。</p>
<h3 id="dashboard-的查詢效能">Dashboard 的查詢效能</h3>
<p>Dashboard 是觀測查詢設計中「聚合趨勢」模式的主要消費者（見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a>）。每個 panel 每 30 秒刷新一次，十個團隊各自有 dashboard 就是每分鐘數百個背景查詢。</p>
<p>Panel 設計時要注意查詢成本：時間範圍越長、raw series 越多、聚合越複雜，query-time cost 越高。長時間趨勢 panel 應該讀 recording rule 或 rollup series，而非每次刷新都掃描 raw data。</p>
<h2 id="alert-設計">Alert 設計</h2>
<h3 id="symptom-based-vs-cause-based">Symptom-based vs cause-based</h3>
<p><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert</a> 觸發在使用者可感知的症狀上 — error rate 升高、latency p99 超過閾值、availability 下降。Cause-based alert 觸發在內部原因上 — CPU &gt; 90%、disk usage &gt; 85%、connection pool exhausted。</p>
<p>Symptom-based 是 alert 設計的起點。原因是：cause-based alert 容易產生大量「系統在忙但使用者沒受影響」的 false alarm。CPU 短暫衝到 95% 然後回落，如果 latency 跟 error rate 都正常，這個 alert 不需要人類介入。</p>
<p>Cause-based alert 的價值是預防性告警 — disk usage 趨勢在兩天後會滿、connection pool 使用率在高峰時逼近上限。這類 alert 不需要立即行動，但需要在工作時間排入 task。把 cause-based alert 設成 warning（不 page）、symptom-based alert 設成 critical（page on-call），能降低 noise。</p>
<h3 id="slo-based-alerting">SLO-based alerting</h3>
<p>SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 取代固定閾值。不是「error rate &gt; 1% 就告警」，而是「error budget 的消耗速度超過預期就告警」。</p>
<p>Burn rate alerting 的好處是自動適應基線。低流量時段的 1% error rate 可能只是幾筆錯誤、不值得 page；高流量時段的 0.5% error rate 可能代表大量使用者受影響。Burn rate 用「相對於 SLO 允許的錯誤量，目前消耗速度有多快」來判斷嚴重性，比固定閾值更能反映使用者影響。</p>
<p>SLO-based alert 的實作通常用 multi-window burn rate — 短視窗（5 分鐘）抓急性問題、長視窗（1 小時）抓慢性問題。兩個視窗都超過 burn rate 閾值時才觸發，減少單一 spike 造成的 false alarm。</p>
<p>SLI/SLO 訊號的詳細設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a>。</p>
<h3 id="alert-的必要欄位">Alert 的必要欄位</h3>
<p>每個 alert rule 應該帶以下 metadata，讓收到 page 的 on-call 工程師在 30 秒內知道下一步：</p>
<ul>
<li><strong>Severity</strong>：critical（立即行動）/ warning（工作時間處理）/ info（記錄但不通知）</li>
<li><strong>Runbook link</strong>：對應的 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> URL，描述診斷步驟跟可能的修復動作</li>
<li><strong>Owner</strong>：負責這個 alert 的團隊或服務</li>
<li><strong>Dashboard link</strong>：點進去直接看相關 panel，不用自己找 dashboard</li>
<li><strong>Summary</strong>：一句話描述發生了什麼（<code>checkout error rate &gt; 2% for 5 minutes</code>），而非只有 alert rule 名稱</li>
</ul>
<p>缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」。On-call 工程師收到不認識的 alert 時，第一反應是 ack 然後繼續觀察 — 這就是 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的起點。</p>
<h2 id="alert-noise-control">Alert Noise Control</h2>
<h3 id="什麼是-noise">什麼是 noise</h3>
<p>Alert noise 是「觸發了但不需要人類行動」的 alert。包括：</p>
<ul>
<li><strong>False positive</strong>：條件觸發但實際沒問題（短暫 spike 觸發固定閾值、maintenance 期間的預期 error）</li>
<li><strong>Redundant alert</strong>：同一個問題觸發多個 alert（database 慢 → query timeout alert + error rate alert + latency alert 同時觸發）</li>
<li><strong>Stale alert</strong>：條件已經不適用（服務改版後舊 alert rule 沒更新、abandoned service 的 alert 還在）</li>
</ul>
<h3 id="noise-rate-量測">Noise rate 量測</h3>
<p>Noise rate = 不需要行動的 alert / 總 alert。追蹤方式是讓 on-call 工程師在 ack alert 時標記「actionable」或「noise」。月度彙整 noise rate，超過 30% 的 alert rule 進入治理流程（業界常用的基線閾值，Google SRE Workbook 建議 actionable rate 維持在 70% 以上；團隊可依自身容忍度調整）。</p>
<h3 id="降噪手段">降噪手段</h3>
<p><strong>Grouping</strong>：把同一個根因觸發的多個 alert 合併成一則通知。Alertmanager 的 <code>group_by</code> 讓同服務、同 alert name 的 alert 只發一次。</p>
<p><strong>Inhibition</strong>：高嚴重性 alert 抑制低嚴重性。Database down 觸發時，所有依賴該 database 的 query timeout alert 被抑制 — 根因已知、不需要每個症狀都通知。</p>
<p><strong>Silence / maintenance window</strong>：已知的維護活動期間暫停特定 alert。Silence 需要有過期時間，避免永久靜默掩蓋真實問題。</p>
<p><strong>Hysteresis</strong>：alert 觸發需要條件持續 N 分鐘（<code>for: 5m</code>），避免瞬間 spike 觸發。恢復也需要條件持續 N 分鐘，避免「反覆觸發 → 恢復」的 flapping。</p>
<h2 id="runbook-設計">Runbook 設計</h2>
<p><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a> 是 alert 的行動指南。每個 critical alert 應該連到一份 runbook，描述「收到這個 alert 時該做什麼」。</p>
<p>Runbook 的有效結構：</p>
<ol>
<li><strong>症狀描述</strong>：這個 alert 代表什麼（「checkout error rate 超過 SLO burn rate」）</li>
<li><strong>影響評估</strong>：誰受影響、嚴重程度（「付款功能受影響、影響所有 checkout 流程」）</li>
<li><strong>診斷步驟</strong>：先看哪個 dashboard、查哪些 log、跑哪些 query</li>
<li><strong>可能的修復動作</strong>：restart service、scale up、rollback deployment、failover to backup</li>
<li><strong>升級路徑</strong>：如果 15 分鐘內無法解決，通知誰</li>
</ol>
<p>Runbook 的維護責任跟 alert 的 owner 一致。Alert rule 改了但 runbook 沒更新是常見的退化 — 把 runbook 的 last-reviewed date 作為 alert 治理的審計項目。</p>
<h2 id="dashboard-與-alert-的生命週期">Dashboard 與 Alert 的生命週期</h2>
<p>Dashboard 跟 alert 都有生命週期。建立時有用，但隨服務演進可能變得過時、冗餘或誤導。沒有生命週期管理的 dashboard / alert 系統會累積 debt — dashboard 數量膨脹但無人看、alert rule 堆疊但多數是 noise。</p>
<h3 id="ownership">Ownership</h3>
<p>每個 dashboard 跟每個 alert rule 都需要明確的 owner。Owner 負責：維護 panel / rule 的正確性、定期審視 noise rate 跟使用率、在服務變更時更新對應的 dashboard / alert。</p>
<p>沒有 owner 的 dashboard 跟 alert 應該有過期機制 — 超過 N 天沒有人訪問的 dashboard 標記為候選淘汰、超過 N 天沒有觸發的 alert rule 審視是否仍有意義。</p>
<h3 id="定期審視">定期審視</h3>
<p>Dashboard 跟 alert 的定期審視是 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance loop</a> 的一部分。每季或每次重大事故後，審視：</p>
<ul>
<li>哪些 alert 的 noise rate 過高、需要調整或刪除</li>
<li>哪些 dashboard 沒人訪問、可以合併或淘汰</li>
<li>事故中是否有缺少的 alert 或 dashboard panel</li>
</ul>
<p>Ownership 矩陣與 metadata 欄位的詳細設計見 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Dashboard 跟 alert 是否有效，最直接的訊號是 alert noise rate 跟 dashboard 訪問頻率 — noise rate 超過 30% 代表通知品質退化，dashboard 長期零訪問代表資訊跟決策脫節。</p>
<p>重點訊號包括：</p>
<ul>
<li>Alert 是否能對應到明確 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、<a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership</a> 與停止條件</li>
<li>Dashboard 是否有固定使用者與更新責任</li>
<li>Threshold 是否對齊 SLO、容量邊界或使用者影響</li>
<li>Noise rate 是否被追蹤並回寫治理流程</li>
<li>Dashboard panel 是否讀 recording rule 而非每次重算 raw data</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 沒連、收到 page 不知道做什麼</li>
<li>Dashboard 數量爆量、無 owner、半年無人訪問</li>
<li>同一訊號多個 alert 重複觸發、無 grouping 或 inhibition</li>
<li>Alert noise rate &gt; 30%、ack 後無實際動作，形成 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a></li>
<li>Alert threshold 用直覺數字、沒對齊 SLO / 商業承諾</li>
<li>Dashboard panel 載入慢、因為直接查 raw series 而非 recording rule</li>
<li>Maintenance window 過後 silence 沒移除、真實問題被掩蓋</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>指標堆疊 dashboard</td>
          <td>50 個 panel、看不出服務是否健康</td>
          <td>SLI 導向重構：第一排回答健康、第二排回答原因</td>
      </tr>
      <tr>
          <td>全部 cause-based alert</td>
          <td>CPU / disk / memory alert 頻繁但服務正常</td>
          <td>區分 symptom（page）跟 cause（warning）</td>
      </tr>
      <tr>
          <td>固定閾值 alert</td>
          <td>低流量時 false alarm、高流量時漏報</td>
          <td>改用 SLO burn rate alerting</td>
      </tr>
      <tr>
          <td>Alert 無 runbook</td>
          <td>On-call 收到 page 後自行摸索、MTTR 高</td>
          <td>每個 critical alert 必附 runbook link</td>
      </tr>
      <tr>
          <td>Alert 無 owner</td>
          <td>沒人維護的 alert rule 累積成 noise 來源</td>
          <td>每個 alert rule 帶 owner metadata、定期審視</td>
      </tr>
      <tr>
          <td>Dashboard 無過期機制</td>
          <td>三年累積 200 個 dashboard、多數沒人看</td>
          <td>訪問頻率追蹤 + 定期淘汰審視</td>
      </tr>
      <tr>
          <td>同一問題觸發 N 個 alert</td>
          <td>On-call 同時收到 5 則通知、不知道看哪個</td>
          <td>Alertmanager grouping + inhibition</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<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</a>：trace waterfall 作為 dashboard 的診斷入口</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>：alert 的訊號源頭、burn rate alerting 的 SLI 依據</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a>：alert / dashboard 的生命週期維運</li>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 client-side / RUM</a>：補 server-side 看不到的 dashboard 維度</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：rule-based alert 之外的統計訊號</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：dashboard / alert 的 ownership 矩陣與 metadata 欄位</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：dashboard 查詢的效能與 recording rule</li>
</ul>
]]></content:encoded></item><item><title>AWS IAM Identity Center</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/</guid><description>&lt;p>AWS IAM Identity Center 是 AWS 原生的 workforce SSO 控制面、前身為 AWS SSO（2022 改名）。它承擔三個責任：人類身份進 AWS 多帳號的 &lt;em>統一入口&lt;/em>（Access Portal）、把使用者映射到各帳號 IAM role 的 &lt;em>Permission Set&lt;/em> 模板、以及對少量已整合 SAML app 的 SSO gateway。它不是 AWS IAM 的替代品、是疊在 AWS IAM 之上的 &lt;em>人類入口層&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>IAM Identity Center 是 &lt;em>人類身份進 AWS 的 portal&lt;/em>、不是 cloud resource permission engine。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> 的分工是兩層：Identity Center 管「人是誰、能登入哪些 account」、AWS IAM 管「進到 account 後對 resource 能做什麼」。實際機制是 Identity Center 透過 Permission Set 在每個目標 account 建一個 &lt;code>AWSReservedSSO_*&lt;/code> 命名的 IAM role、使用者 assume 該 role 拿短期 STS token。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 相比、Identity Center 的核心優勢是 &lt;em>跟 AWS Organizations + Control Tower 原生整合&lt;/em>、Permission Set 可以一次發佈到數百個 account、不必每個 account 各接 SAML。代價是 SaaS app integration 量級遠少於 Okta（Okta 7000+ 預建、Identity Center 僅中等規模）、跨雲 federation（GCP / Azure）也不在原生範圍。&lt;/p>
&lt;p>許多大型組織採三層架構：Okta 是 HRIS 下游的 identity source of truth、SCIM push 進 Identity Center、Identity Center 再 map 到 AWS IAM Permission Set。Okta 管「人是誰」、Identity Center 管「AWS portal 入口」、AWS IAM 管「resource 能做什麼」。中小組織可以省略 Okta、直接用 Identity Center 內建 user store、但就失去跨 SaaS 統一 SSO。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Identity Center 在 &lt;em>人類身份 / AWS portal / resource permission&lt;/em> 三層裡的位置、何時該交回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> 或上游 IdP&lt;/li>
&lt;li>Identity Source 選擇（內建 / Active Directory / 外部 SAML）對 lifecycle 與 lock-in 的長期影響&lt;/li>
&lt;li>Permission Set / Account Assignment / Access Portal 三個核心概念的稽核重點&lt;/li>
&lt;li>何時 Identity Center 夠用、何時要疊 Okta 在前、何時 Identity Center 反而是錯選擇&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Identity Center 配置是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>AWS IAM Identity Center 是 AWS 原生的 workforce SSO 控制面、前身為 AWS SSO（2022 改名）。它承擔三個責任：人類身份進 AWS 多帳號的 <em>統一入口</em>（Access Portal）、把使用者映射到各帳號 IAM role 的 <em>Permission Set</em> 模板、以及對少量已整合 SAML app 的 SSO gateway。它不是 AWS IAM 的替代品、是疊在 AWS IAM 之上的 <em>人類入口層</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>IAM Identity Center 是 <em>人類身份進 AWS 的 portal</em>、不是 cloud resource permission engine。它跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 的分工是兩層：Identity Center 管「人是誰、能登入哪些 account」、AWS IAM 管「進到 account 後對 resource 能做什麼」。實際機制是 Identity Center 透過 Permission Set 在每個目標 account 建一個 <code>AWSReservedSSO_*</code> 命名的 IAM role、使用者 assume 該 role 拿短期 STS token。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 相比、Identity Center 的核心優勢是 <em>跟 AWS Organizations + Control Tower 原生整合</em>、Permission Set 可以一次發佈到數百個 account、不必每個 account 各接 SAML。代價是 SaaS app integration 量級遠少於 Okta（Okta 7000+ 預建、Identity Center 僅中等規模）、跨雲 federation（GCP / Azure）也不在原生範圍。</p>
<p>許多大型組織採三層架構：Okta 是 HRIS 下游的 identity source of truth、SCIM push 進 Identity Center、Identity Center 再 map 到 AWS IAM Permission Set。Okta 管「人是誰」、Identity Center 管「AWS portal 入口」、AWS IAM 管「resource 能做什麼」。中小組織可以省略 Okta、直接用 Identity Center 內建 user store、但就失去跨 SaaS 統一 SSO。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Identity Center 在 <em>人類身份 / AWS portal / resource permission</em> 三層裡的位置、何時該交回 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 或上游 IdP</li>
<li>Identity Source 選擇（內建 / Active Directory / 外部 SAML）對 lifecycle 與 lock-in 的長期影響</li>
<li>Permission Set / Account Assignment / Access Portal 三個核心概念的稽核重點</li>
<li>何時 Identity Center 夠用、何時要疊 Okta 在前、何時 Identity Center 反而是錯選擇</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Identity Center 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 assume 哪個 role</strong>：Permission Set 跟 Account Assignment 是否走最小權限、<code>AdministratorAccess</code> 範圍 Permission Set 是否限定 break-glass、是否強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant 認證</a> 才能 assume 高權限</li>
<li><strong>Permission Set 邊界</strong>：每個 Permission Set 的 session duration（預設 1 hour、可調 12 hour）、inline policy vs Customer Managed Policy reference、是否用 ABAC tag 收斂跨 account 散佈</li>
<li><strong>External IdP federation 狀態</strong>：Identity Source 是內建 / AD / 外部 SAML、若走外部 IdP SCIM push 是否監控 sync 失敗、signing certificate 是否在 rotation 排程內</li>
<li><strong>CloudTrail 是否完整</strong>：Identity Center 事件分布在 management account 跟 member account、是否有 organization trail 收齊、admin 變更 / Permission Set 變更 / failed assume 是否 alert</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Identity Source 是根信任</strong>：Identity Center 支援三種 user/group 來源 — 內建 store、AWS Managed AD / on-prem AD via AD Connector、外部 SAML IdP（Okta / Entra ID 等、SCIM 推進來）。選了之後 user lifecycle 從哪來就鎖死、換 Identity Source 是大工程（要重建所有 Permission Set assignment、舊 user GUID 不通用）。早期決定錯比 Permission Set 設錯難救。</p>
<p><strong>Permission Set 是 cross-account role template</strong>：定義一次、apply 到多 account、實際在每個 account 部署成一個 AWS-Reserved 命名的 IAM role。Permission Set 本身不是 role、是 <em>role 的部署模板</em> — 改 Permission Set 會 push 到所有 account 上對應的 role。Customer Managed Policy reference 比 inline policy 好維護、但要先確保每個 target account 都有同名 policy、否則 assignment 會失敗。</p>
<p><strong>Account Assignment</strong>：把 user/group 綁到 Permission Set + 特定 account 的三元組。這層用 group 而不是個別 user、跟著 Identity Source 的 group 變動自動同步。臨時權限（離職員工延長、incident 應變）走 access request workflow 或 IAM Access Analyzer + Just-in-Time、不要永久 assignment。</p>
<p><strong>Access Portal URL 是 phishing 目標</strong>：custom URL（<code>https://&lt;alias&gt;.awsapps.com/start</code>）設定後變成員工每天用的入口、phishing 攻擊會 mimic。要強制 phishing-resistant MFA（WebAuthn / passkey）、純 push MFA 抗不過 fatigue。CLI 走 <code>aws sso login</code> 自帶 browser-based flow、不要叫員工複製貼 access key。</p>
<p><strong>Application assignment</strong>：Identity Center 也能管 SAML app 的 SSO assignment、但 integration 數量遠少於 Okta。大量 SaaS app 的場景應該疊 Okta 在前、Identity Center 只管 AWS portal。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>IAM Identity Center</th>
          <th>Okta + AWS IAM</th>
          <th>直接用 AWS IAM Users（不推薦）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制面責任</td>
          <td>AWS 託管、限 AWS 帳號 + 中等 SAML app</td>
          <td>Okta 管人類身份、AWS IAM 管 resource、兩層分工</td>
          <td>每個 account 各自管 user、無跨帳號統一</td>
      </tr>
      <tr>
          <td>多帳號統一入口</td>
          <td>原生、Permission Set 一次發到全 Org</td>
          <td>透過 SAML federation 到 IAM role</td>
          <td>不存在 — 每個 account 各自 IAM Users</td>
      </tr>
      <tr>
          <td>SaaS app 範圍</td>
          <td>中等規模 integration</td>
          <td>7000+ 預建 integration</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Lifecycle</td>
          <td>內建 / AD / 外部 SCIM 進來</td>
          <td>Okta 走 HRIS SCIM 同步、Identity Center 接 Okta SCIM</td>
          <td>手動管理、容易 stale</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — AWS 內部換</td>
          <td>高 — Okta + Identity Center 都要拆</td>
          <td>高 — 大量 IAM Users 散佈在 N 個 account</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-heavy、員工數中等、SaaS app 少</td>
          <td>多雲 + 大量 SaaS + AWS 帳號數十個以上</td>
          <td>不存在合理場景（small lab 例外）</td>
      </tr>
  </tbody>
</table>
<p>選 Identity Center 的核心訴求：<em>AWS 是主要工作環境、員工 SaaS app 用量低、要統一多帳號入口而不要再付 Okta 訂閱</em>。員工大量用 SaaS 的場景應該疊 Okta 在前。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>External IdP federation（Okta / Entra ID SCIM 進來）</strong>：Identity Center 接外部 IdP 是 <em>push model</em> — IdP 主動 SCIM push、Identity Center 不 pull。push provisioning 失敗會 silent（IdP 端有 log、Identity Center 端只看到 user 沒出現）、要在 IdP 端設 sync failure alert。SAML signing certificate rotation 兩邊都要排程、過期會整個 federation 斷。</p>
<p><strong>Multi-account Permission Set 設計</strong>：避免每個 environment / team 各自一份 Permission Set — 用 ABAC（tag-based access control）把「<code>Environment=Prod</code> + <code>Team=Payments</code>」的條件寫進一個 Permission Set 的 policy、tag 跟著 user attribute 跑。Permission Set 數量爆炸是 Identity Center 老化最常見訊號。</p>
<p><strong>Customer Managed Policy reference</strong>：Permission Set 可以 reference target account 裡的 customer managed policy（同名同 path）、policy 本身在每個 account 獨立維護。比 inline policy 適合大規模、但要靠 CI / Terraform 確保 policy 在所有 target account 同步存在、否則 assignment 失敗。</p>
<p><strong>Session duration 是攻擊面</strong>：預設 1 hour、可調到 12 hour。長 session 對 dev 體驗友善、但不利於 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">credential rotation</a> — 高權限 Permission Set（<code>AdministratorAccess</code>、production write）應該短 session（1-2 hour）、低風險 read-only 可放 8-12 hour。</p>
<p><strong>IAM Identity Center API 不該當 workforce IdP 用</strong>：API 是給 admin 管 assignment 用、不是給 app 拿 user token。要 workforce app SSO 走 SAML / OIDC federation、不要叫 app 打 Identity Center API 查 user。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Permission Set 數量爆炸</strong>：每個 team / environment 各一份、上百個 Permission Set 沒人敢動 — 改用 ABAC + user attribute 把條件寫進 policy、收斂到十位數</li>
<li><strong>Identity Source 選錯難換</strong>：早期選內建 store、後來公司導入 Okta 要換成外部 SAML — 整個 user GUID 重新映射、Permission Set assignment 重綁、評估比建新 tenant 還久</li>
<li><strong>External SCIM sync 失敗 silent</strong>：Okta 端 push 失敗、Identity Center 沒人 — 要在上游 IdP 設 SCIM provisioning failure alert、不要等使用者反映「我登不進去」</li>
<li><strong>Access Portal URL 被 phishing</strong>：custom URL 員工記憶、phishing 站 mimic、無 phishing-resistant MFA 擋不住 — 強制 WebAuthn / passkey、員工教育只認 bookmark / SSO launcher</li>
<li><strong>CloudTrail 不完整</strong>：只開 management account trail、member account 的 role assumption 看不到 — 開 organization trail 收齊、特別 alert Permission Set 變更與失敗 assume</li>
<li><strong>Break-glass 缺席</strong>：Identity Center 控制面故障時 console 進不去 — 保留每個 account 的 root credential（離線存）跟少數 break-glass IAM User（hardware MFA、與 Identity Center 獨立 audit）、季度驗證</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大量 SaaS app 統一 SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor</a>（疊在 Identity Center 前）</td>
      </tr>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a></td>
      </tr>
      <tr>
          <td>自管 / 不接受 cloud-managed IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a></td>
      </tr>
      <tr>
          <td>AWS resource permission（policy / role / STS）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM vendor</a></td>
      </tr>
      <tr>
          <td>跨雲 federation（GCP / Azure workforce）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>Secret / API key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS IAM 的 policy / role / STS 機制細節（屬 AWS IAM vendor 頁）</li>
<li>Permission Set 的 JSON policy 撰寫教學</li>
<li>AWS Organizations / Control Tower 的完整架構</li>
<li>各 SaaS app SAML 接線教學</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 IAM Identity Center 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>Identity Center 控制面故障會擋住 AWS console portal、降級路徑必須事先設計（emergency root credential、break-glass IAM User）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Permission Set session duration 跟 external IdP signing key rotation 是不同域、要分開排程、不能混為一談</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System Incident 2023</a></td>
          <td>Okta 作為 Identity Center 的 external IdP 時、上游事件會傳導下來、Identity Center 端要看 SCIM sync 異常與 federation token reuse</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023 Okta Token Follow-Through</a></td>
          <td>上游 IdP 出事後、Identity Center 端的 active session 是否要強制 reauth、不能等供應商公告</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor</a>（外部 IdP 疊在前）、<a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0 vendor</a>、<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak vendor</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM vendor</a>（Permission Set 落地的 resource permission 層）、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>（多雲對照）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Identity Center 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/singlesignon/">AWS IAM Identity Center Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>AWS KMS</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/</guid><description>&lt;p>AWS KMS 是 AWS 原生的 key management service、解決 &lt;em>對稱 / 非對稱金鑰生命週期管理&lt;/em> 與 &lt;em>envelope encryption pattern&lt;/em>：service 內部保管 master key（KMS Key）、應用層用 &lt;code>GenerateDataKey&lt;/code> 取得短暫的 data key 對實際資料加密、master key 完全不離 KMS 服務邊界。整合面跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a> / S3 / EBS / RDS 都串好、是 AWS 上幾乎所有靜態資料加密的後端。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>AWS KMS 的核心定位是 &lt;em>AWS-only 的 multi-tenant managed key management&lt;/em>，FIPS 140-2 Level 3 認證、跨服務 envelope encryption 的共同地基。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &amp;#43; 資料主權場景的 key custody">CloudHSM&lt;/a> 比、KMS 是 &lt;em>managed + shared HSM 池&lt;/em>、CloudHSM 是 &lt;em>single-tenant dedicated HSM&lt;/em>；需要更高隔離 / 自管 cluster / FIPS Level 3 single-tenant 時走 CloudHSM、或用 KMS Custom Key Store 把 KMS 後端指向自己的 CloudHSM。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &amp;#43; Cloud HSM &amp;#43; External Key Manager">Google Cloud KMS&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a> 比、設計概念相近、但 KMS 把 secret store 切出去（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager&lt;/a>）、Key Vault 則把兩者合一。&lt;/p></description><content:encoded><![CDATA[<p>AWS KMS 是 AWS 原生的 key management service、解決 <em>對稱 / 非對稱金鑰生命週期管理</em> 與 <em>envelope encryption pattern</em>：service 內部保管 master key（KMS Key）、應用層用 <code>GenerateDataKey</code> 取得短暫的 data key 對實際資料加密、master key 完全不離 KMS 服務邊界。整合面跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / S3 / EBS / RDS 都串好、是 AWS 上幾乎所有靜態資料加密的後端。</p>
<h2 id="服務定位">服務定位</h2>
<p>AWS KMS 的核心定位是 <em>AWS-only 的 multi-tenant managed key management</em>，FIPS 140-2 Level 3 認證、跨服務 envelope encryption 的共同地基。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a> 比、KMS 是 <em>managed + shared HSM 池</em>、CloudHSM 是 <em>single-tenant dedicated HSM</em>；需要更高隔離 / 自管 cluster / FIPS Level 3 single-tenant 時走 CloudHSM、或用 KMS Custom Key Store 把 KMS 後端指向自己的 CloudHSM。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 比、設計概念相近、但 KMS 把 secret store 切出去（<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a>）、Key Vault 則把兩者合一。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault transit engine</a> 比、行為相似（key 不離 service、app 拿 ciphertext）、但治理面完全不同：KMS 綁 AWS 控制面、IAM + Key Policy 雙層授權、CloudTrail 是稽核入口；Vault transit 是跨雲統一介面、token + policy 為主、需要自管 cluster。AWS-heavy 組織首選 KMS、跨雲組織才會把 KMS 當下游、上游用 Vault transit 抽象。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些資料 / 場景該用 Customer Managed KMS Key、哪些 AWS Managed Key 已經夠用、什麼時候直接走 CloudHSM</li>
<li>Key Policy + IAM + Grant 三層授權的分工、production 必開的 CloudTrail Data event 與 monitor 範圍</li>
<li>Multi-Region Key、Custom Key Store、External Key Store、BYOK 等進階形態的取捨</li>
<li>KMS 出事（IAM 過寬、Key Policy 把自己鎖死、Schedule Deletion 誤觸發）時的判讀路徑跟回退選項</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 AWS KMS deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Key Policy 設計</strong>：是否含 <code>root</code> principal（不然 key 變孤兒）、是否走 least privilege（不是 <code>kms:*</code> 給整個 account）、admin / user / monitor 三類 principal 是否分開、policy 變更是否走 PR review</li>
<li><strong>Grant 治理</strong>：哪些 service-to-service 短期授權走 Grant（rotation Lambda / RDS / EBS）、Grant TTL 是否設、廢棄 grant 是否定期 <code>RetireGrant</code></li>
<li><strong>Multi-Region 與 rotation 策略</strong>：是否啟用 annual automatic rotation（適用 symmetric encryption key）、Multi-Region Key 的 replica 是否跟 DR plan 對齊、asymmetric / signing key 的 manual rotation 流程是否有 runbook</li>
<li><strong>CloudTrail Data Event 必開</strong>：management event 預設記、但 <code>Encrypt</code> / <code>Decrypt</code> / <code>GenerateDataKey</code> 是 data event、預設不記 — 沒這層 forensic 沒著力點、Storm-0558 對照下完全無法回答「誰用哪把 key 簽了什麼 token」</li>
</ul>
<p>四件事任一缺失、就回到 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a> 跟 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 的補丁清單。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Key Type 選擇</strong>：symmetric encryption key（AES-256-GCM、最常用、S3 / EBS / RDS / Secrets Manager 都走這個）；asymmetric key pair（RSA / ECC、用於 sign / verify 或 encrypt / decrypt、JWT 簽署、CodeSign、文件簽章）；HMAC key（generate / verify MAC、API request signing）。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558 signing key chain</a> — 自己 host signing key 出事的核心教訓是 <em>key 不該離 HSM service</em>、所以 JWT signing 用 asymmetric KMS key 是 baseline 設計、private key 永遠不離 KMS。</p>
<p><strong>Key Origin（key material 來源）</strong>：<code>AWS_KMS</code>（KMS 內部生成、預設）；<code>EXTERNAL</code>（BYOK、組織自己生成 key material、import 進 KMS、可以隨時 reimport 或刪除）；<code>AWS_CLOUDHSM</code>（Custom Key Store、key material 存在自己的 CloudHSM cluster）；<code>EXTERNAL_KEY_STORE</code>（XKS、AWS 外的 HSM、控制面在 AWS、key material 在 on-prem）。多數場景用 <code>AWS_KMS</code> 就夠、合規 / 主權需求才走 EXTERNAL / Custom Key Store。</p>
<p><strong>Key Policy 跟 IAM 的雙層</strong>：KMS 跟其他 AWS service 最大差異是 <em>Key Policy 是主要授權機制</em>、IAM policy 單獨不夠。Key Policy 必含 <code>arn:aws:iam::ACCOUNT_ID:root</code> 給 root principal（不是 root user、是讓 IAM 能參與授權的開關）— 沒這條 key 變孤兒、即使 IAM 開了 admin 也救不回來。production 通常分三類 statement：admin（Create / Delete / Schedule、走 break-glass）、user（Encrypt / Decrypt / GenerateDataKey、給 app）、monitor（Describe / List、給 SRE）。</p>
<p><strong>Grant 是程式化短期授權</strong>：service-to-service 整合（<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a> rotation Lambda、RDS 自動加密、EBS volume attach）通常走 Grant 而不是改 Key Policy — 每個 grant 有自己的 grant token、可以帶 TTL、可以 <code>RetireGrant</code> / <code>RevokeGrant</code> 收回、不跟 key policy 永久綁定。沒治理時 grant 累積上千個 / 沒人 retire 是常見問題、跟 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 同類 — 沒 scope map 等於沒治理。</p>
<p><strong>Alias 與 Key ID 的解耦</strong>：alias（<code>alias/my-app-prod-key</code>）是 <em>指向 key 的可變指標</em>、key ID / ARN 是 <em>不可變識別</em>。production code 應該用 alias、要換 key 時只需要重綁 alias、不用改 deployment。Cross-account 跨帳號使用必須用 ARN（alias 不跨帳號）。</p>
<p><strong>Key Rotation 的真實語義</strong>：annual automatic rotation（symmetric encryption key 才支援）換的是 <em>KMS 內部的 backing key material</em>、key ARN / Alias / Key ID 都不變、app 完全不需要動。<strong>舊資料仍用舊 backing key 解密、KMS 自動處理</strong>、不是「資料全部重新加密」— 這是常見誤解。asymmetric / HMAC key 不支援 automatic rotation、必須 manual 建新 key + alias 切換 + app 端雙讀容忍窗口（跟 JWT signing key rotation 同套路）。</p>
<p><strong>Multi-Region Key</strong>：跨 region replicate 的 KMS key 共用 <em>key material</em> 跟 <em>Key ID</em>（後綴帶 <code>mrk-</code>）、不是建立新 key — 跨 region 加密的 ciphertext 在另一 region 可以直接 decrypt、不用 cross-region API call。適合 multi-region active-active app + DR scenario。代價是 <em>replica region 跟 primary region 的權限要分別治理</em>、Key Policy 不會自動同步。</p>
<p><strong>Encryption Context 是 <em>authenticated data</em></strong>：encrypt 時帶的 key-value pair（例：<code>{&quot;app&quot;: &quot;billing&quot;, &quot;tenant&quot;: &quot;acme&quot;}</code>）、decrypt 必須提供同一組 context — 否則失敗。用來防 <em>ciphertext 被 replay 到別的 context</em>（攻擊者拿到 billing 的 ciphertext 想當 payroll 的 ciphertext 用）、所有 context 都會進 CloudTrail、是 forensic 上的關鍵欄位。production 一律帶 context、單純加密不帶 context 等於少一層防護。</p>
<p><strong>Customer Managed vs AWS Managed vs AWS Owned</strong>：三層分權 — Customer Managed（CMK、自己控 Key Policy + 自選 rotation）、AWS Managed（<code>aws/secretsmanager</code>、<code>aws/s3</code>、AWS 管 Key Policy、看得到但改不了）、AWS Owned（完全看不見、AWS 自己用、無 CloudTrail）。production 高敏感資料應該用 Customer Managed、才能控 policy + 開 data event + 自選 rotation 週期。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS KMS</th>
          <th>Google Cloud KMS</th>
          <th>Azure Key Vault</th>
          <th>AWS CloudHSM</th>
          <th>Vault transit engine</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>AWS managed multi-tenant、FIPS 140-2 Level 3</td>
          <td>GCP managed multi-tenant、FIPS 140-2 L3</td>
          <td>Azure managed、Standard / Premium tier</td>
          <td>AWS managed single-tenant HSM cluster</td>
          <td>自管 Vault cluster</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>弱 — AWS-only</td>
          <td>弱 — GCP-only</td>
          <td>弱 — Azure-only</td>
          <td>弱 — AWS-only</td>
          <td>強 — 跨雲統一介面</td>
      </tr>
      <tr>
          <td>授權模型</td>
          <td>Key Policy（強制） + IAM + Grant 三層</td>
          <td>IAM 為主、Resource policy 輔</td>
          <td>Access policy + RBAC 雙模式</td>
          <td>CloudHSM user / role + Cluster IAM</td>
          <td>path-based policy + token</td>
      </tr>
      <tr>
          <td>Multi-Region</td>
          <td>Multi-Region Key（共用 key material）</td>
          <td>自動跨 region replication 較易</td>
          <td>Geo-replication 透過 Premium tier</td>
          <td>自管 cross-region replication</td>
          <td>Replication（Enterprise）</td>
      </tr>
      <tr>
          <td>Envelope encryption</td>
          <td>一級 pattern（<code>GenerateDataKey</code>）</td>
          <td>一級 pattern</td>
          <td>一級 pattern</td>
          <td>自己實作</td>
          <td>內建（transit engine）</td>
      </tr>
      <tr>
          <td>Asymmetric signing</td>
          <td>支援（RSA / ECC、JWT / CodeSign 直用）</td>
          <td>支援</td>
          <td>支援</td>
          <td>支援 + 完整 PKCS#11</td>
          <td>支援（部分）</td>
      </tr>
      <tr>
          <td>整合面</td>
          <td>全 AWS service 原生（S3 / EBS / RDS / Lambda）</td>
          <td>全 GCP service 原生</td>
          <td>全 Azure service 原生</td>
          <td>PKCS#11 / JCE / OpenSSL</td>
          <td>應用層 SDK</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-heavy + envelope encryption + JWT signing</td>
          <td>GCP-heavy</td>
          <td>Azure-heavy + 跟 AD 整合</td>
          <td>合規 / FIPS L3 single-tenant / 自管 HSM</td>
          <td>跨雲 + key 不離 service</td>
      </tr>
      <tr>
          <td>不適合場景</td>
          <td>跨雲統一 custody、需 FIPS L4、需自管 HSM cluster</td>
          <td>同左</td>
          <td>同左</td>
          <td>純 envelope encryption 用 KMS 即可</td>
          <td>AWS-only 簡單需求（KMS 更便宜）</td>
      </tr>
  </tbody>
</table>
<p>KMS 是 AWS 上的 <em>預設選擇</em>、CloudHSM 是合規 / 自管要求才上的 <em>昇級</em>、Vault transit 是跨雲統一介面、Google / Azure 對標品在各自雲一樣是預設選擇。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>KMS Custom Key Store + CloudHSM 整合</strong>：Custom Key Store 把 KMS 的 <em>控制面</em>（API、Key Policy、CloudTrail、IAM 整合）保留、但 <em>key material 存在自己的 CloudHSM cluster</em>。組織需要 FIPS 140-2 Level 3 single-tenant 但又不想放棄 KMS 的 service 整合（S3 SSE-KMS / EBS encryption）時用。代價是 CloudHSM cluster 的運維成本（cluster HA、user 管理、backup）。</p>
<p><strong>External Key Store (XKS)</strong>：更激進的形態 — key material 完全在 AWS 之外（on-prem HSM 或第三方 HSM）、AWS 透過 XKS proxy 呼叫外部 HSM 做 cryptographic operation。用於 <em>資料主權</em> 場景（金融 / 政府 / 跨境合規要求 key 不出組織邊界）、代價是 latency 跟 availability 完全綁外部 HSM、AWS service 整合面要算清楚。</p>
<p><strong>Multi-Region Replica Key 跟 DR</strong>：primary region 出事時 replica region 仍能 decrypt 既有 ciphertext、不需要 cross-region API call。但 <em>primary 跟 replica 是各自獨立的 Key Policy</em>、變更不會自動同步 — 跟 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 治理一樣、replica region 也要納入 CloudTrail Data Event 覆蓋範圍。</p>
<p><strong>BYOK（Bring Your Own Key）</strong>：<code>Origin = EXTERNAL</code> 的 KMS Key、key material 由組織自己生成、用 wrapping key 加密後 import 進 KMS。優點是組織保有 <em>master copy</em>（KMS 出事時仍能 re-import 到別處）、缺點是 <em>automatic rotation 不支援</em>（必須手動 import 新 key material）、且必須自己處理 wrapping key 的生命週期。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a> 的整合</strong>：Secrets Manager 的 secret 本身用 KMS key 加密（預設 AWS Managed <code>aws/secretsmanager</code>、production 應該指到 Customer Managed CMK）。rotation Lambda 透過 Grant 取得 Decrypt + Encrypt 能力、跟 Secrets Manager 一起構成 <em>static secret rotation 的證據鏈</em> — 跟 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">credential rotation scoped evidence</a> 對齊。</p>
<p><strong>Asymmetric signing 的 use cases</strong>：JWT signing（KMS <code>Sign</code> API 直接簽 JWT header.payload、private key 不離 KMS、跟 Storm-0558 的設計對照鮮明）；CodeSign / S3 object signing（artifact integrity）；mTLS client cert 的 private key（搭配 <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> AWS issuer）。代價是 <em>latency</em>（每次 sign 一次 KMS API call、~10ms 級別、不適合超高 QPS）跟 <em>cost</em>（asymmetric operation 比 symmetric 貴 ~5x）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Key Policy 沒有 <code>root</code> principal</strong>：Schedule 時忘了寫、key 立刻變孤兒、誰都不能用 — 只能透過 AWS Support 救（流程慢）；建立流程強制 template 含 root principal</li>
<li><strong>IAM admin 改不動 KMS key</strong>：Key Policy 沒授權 IAM 介入、即使 admin policy 有 <code>kms:*</code> 也擋掉 — 加 <code>Enable IAM User Permissions</code> statement 給 root principal、IAM 才能參與授權</li>
<li><strong>Schedule Key Deletion 誤觸發</strong>：min 7 天、max 30 天的等待期、期內可 cancel — production key 必含 alert（CloudWatch Alarm on <code>ScheduleKeyDeletion</code> event）+ 強制 4-eyes approval</li>
<li><strong>CloudTrail Data Event 沒開</strong>：事故後想查「誰 decrypt 了什麼」、發現只有 management event — production 必開 KMS data event、預估 cost（每 100k events ~$0.10）、敏感 key 一律開</li>
<li><strong>Encryption Context 不一致</strong>：encrypt 時帶 context、decrypt 時忘了帶（或帶錯）、<code>InvalidCiphertextException</code> — code review 強制 context schema、用 typed wrapper 避免人手帶錯</li>
<li><strong>Grant 累積 + 沒 retire</strong>：每個 KMS key 有 50,000 grant 上限、rotation Lambda 跑久了 grant 累積 — 定期 <code>ListGrants</code> + <code>RetireGrant</code> 廢棄的、IaC 治理 grant lifecycle</li>
<li><strong>Cross-region decrypt 失敗</strong>：以為 ciphertext 跨 region 通用、結果原本不是 Multi-Region Key — production 跨 region 場景一律建 Multi-Region Key、不要事後補</li>
<li><strong>CMK rotation 後舊 ciphertext 還能 decrypt</strong>：annual rotation 不會 re-encrypt 舊資料、KMS 自動用對應 backing key — 這是設計、不是 bug；真要全量 re-encrypt 要走 application-level migration</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FIPS 140-2 Level 3 single-tenant HSM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a>、或 KMS Custom Key Store 橋接</td>
      </tr>
      <tr>
          <td>GCP-heavy 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a></td>
      </tr>
      <tr>
          <td>Azure-heavy + 跟 AD / Managed Identity 整合</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></td>
      </tr>
      <tr>
          <td>跨雲統一 key custody</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault transit engine</a></td>
      </tr>
      <tr>
          <td>Static secret + rotation orchestration</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>（後端是 KMS）</td>
      </tr>
      <tr>
          <td>K8s workload mTLS cert</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（可用 KMS asymmetric key）</td>
      </tr>
      <tr>
          <td>Public TLS cert</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>數據主權 / on-prem HSM required</td>
          <td>KMS External Key Store (XKS) 或直接 <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>KMS 完整 API reference 跟 SDK 範例</li>
<li>各 AWS service（S3 SSE-KMS、EBS encryption、RDS encryption、DynamoDB encryption）的詳盡設定步驟</li>
<li>跟 AWS Organizations / SCPs 的 cross-account KMS sharing 完整治理流程</li>
<li>CloudHSM cluster 的完整運維（高可用、user 管理、backup）— 看 <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></li>
<li>各種 cryptographic algorithm 的數學原理跟選型細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>KMS 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 KMS 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a></td>
          <td>KMS 設計核心對照 — signing key 必須 HSM-bound + 不可導出、KMS 預設 key 完全不離 service；自己 host private key 是 Storm-0558 級事件的根因</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>三件事必到位：asymmetric KMS Key 做 JWT signing（private key 永遠不離 KMS）、強制 rotation 流程、CloudTrail Data Event 紀錄「誰用 key 簽什麼 token」</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>KMS Alias / Grant 的 rotation 跟 revocation 要分域 — 一次 Schedule Key Deletion 沒 scope map 等於潛在全停、Grant lifecycle 要納入治理</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>（KMS 為 TLS / signing key 的 root custodian）、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>（後端用 KMS）、<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（可用 KMS asymmetric key 當 issuer）</li>
<li>對照：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（transit engine / 跨雲統一介面）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（KMS 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/kms/">AWS KMS Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>GitHub Advanced Security</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/</guid><description>&lt;p>GitHub Advanced Security（GHAS）是 GitHub 內建的 &lt;em>application security platform&lt;/em>、由四大模組組成：&lt;em>Code Scanning&lt;/em>（CodeQL 為預設 SAST、可接受第三方 SARIF）、&lt;em>Secret Scanning&lt;/em>（偵測 leaked credential、含 Push Protection 預防 push）、&lt;em>Dependency Review&lt;/em>（PR 級依賴變更 gate）、&lt;em>Dependabot&lt;/em>（自動化依賴 update + alert、細節見獨立 vendor 頁）。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 等獨立 SCA 工具的核心差異是 &lt;em>跟 GitHub workflow / PR / Security tab 深度整合&lt;/em> — security finding 直接出現在 PR review 跟 organization Security overview、不需另一個 dashboard。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>GHAS 的核心定位是 &lt;em>把 application security 控制面收斂回 GitHub 平台&lt;/em>：SAST、Secret Scanning、Dependency Review、Dependabot 共用 GitHub 的 identity / permission / PR / branch protection / Actions / Security tab，讓 security finding 跟 code review 在同一個 surface 上決策。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 走「跨 SCM、跨雲、自有 dashboard」是相反方向 — Snyk 把 security 抽到平台之上、GHAS 把 security 釘在 GitHub 之內。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 比、定位差更遠。Trivy 主打 &lt;em>container image / IaC / SBOM scan&lt;/em>、open-source 免費、適合塞進任何 CI；GHAS 主打 &lt;em>source code + secret + dependency&lt;/em>、Enterprise 付費、container scan 有但偏弱。兩者通常 &lt;em>並存&lt;/em> — Trivy 跑 container artifact、GHAS 跑 source repo。&lt;/p></description><content:encoded><![CDATA[<p>GitHub Advanced Security（GHAS）是 GitHub 內建的 <em>application security platform</em>、由四大模組組成：<em>Code Scanning</em>（CodeQL 為預設 SAST、可接受第三方 SARIF）、<em>Secret Scanning</em>（偵測 leaked credential、含 Push Protection 預防 push）、<em>Dependency Review</em>（PR 級依賴變更 gate）、<em>Dependabot</em>（自動化依賴 update + alert、細節見獨立 vendor 頁）。它跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 等獨立 SCA 工具的核心差異是 <em>跟 GitHub workflow / PR / Security tab 深度整合</em> — security finding 直接出現在 PR review 跟 organization Security overview、不需另一個 dashboard。</p>
<h2 id="服務定位">服務定位</h2>
<p>GHAS 的核心定位是 <em>把 application security 控制面收斂回 GitHub 平台</em>：SAST、Secret Scanning、Dependency Review、Dependabot 共用 GitHub 的 identity / permission / PR / branch protection / Actions / Security tab，讓 security finding 跟 code review 在同一個 surface 上決策。這跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 走「跨 SCM、跨雲、自有 dashboard」是相反方向 — Snyk 把 security 抽到平台之上、GHAS 把 security 釘在 GitHub 之內。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 比、定位差更遠。Trivy 主打 <em>container image / IaC / SBOM scan</em>、open-source 免費、適合塞進任何 CI；GHAS 主打 <em>source code + secret + dependency</em>、Enterprise 付費、container scan 有但偏弱。兩者通常 <em>並存</em> — Trivy 跑 container artifact、GHAS 跑 source repo。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a> 的關係是 <em>內含</em> — Dependabot 是 GHAS 四模組之一、跟 GHAS 同一個控制平面、跟 PR / Security tab 同一條 evidence chain。本頁聚焦 GHAS 整體 + Code Scanning / Secret Scanning / Dependency Review；Dependabot 的 update PR 政策、ecosystem 覆蓋、alert routing 細節留在該頁。</p>
<p>關鍵張力：GHAS 計費走 <em>per-active-committer + per-repo</em>、2024 後 Secret Scanning 跟 Code Scanning 拆開計費。大型 mono-repo 或 committer 數量膨脹的組織會撞到成本天花板、需要選擇性 enable repo + 拆模組買；同時、Push Protection 這類 <em>預防型</em> 控制只有 enable 後才有效、選擇性 enable 等於默認 risk 接受。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>GHAS 四大模組各自承擔哪段控制責任（SAST / Secret / PR-level dependency gate / 自動 update）、哪些跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 重疊或互補</li>
<li>CodeQL 跟 SARIF 標準的關係、為什麼第三方 SAST 工具的 finding 也能進 GHAS Security tab</li>
<li>Secret Scanning 的 <em>Push Protection</em>（預防 push）跟 <em>Secret Scanning Alert</em>（偵測 leaked）的職責差、partner pattern vs custom pattern 何時用</li>
<li>何時用 GHAS、何時改走 Snyk / Trivy / GitLab Ultimate（GitLab 自家相當品）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 GHAS 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 enable / disable</strong>：Organization owner / Security manager role 配置、enable GHAS 的 audit log 是否同步、誰能改 Code Scanning workflow（branch protection 是否擋住 workflow file 直接 push）</li>
<li><strong>哪些 repo 開啟</strong>：Org Security overview 看 <em>Code Scanning / Secret Scanning / Dependency Review coverage</em>、新建 repo 是否預設啟用（Organization-level default setting）、private / internal / public repo 是否一致開啟</li>
<li><strong>Push Protection 狀態</strong>：Secret Scanning Push Protection 是否 organization-wide enable、bypass 權限給誰（developer 個人 bypass vs 必須走 Security team approval）、bypass 事件是否進 audit</li>
<li><strong>Secret Scanning Coverage</strong>：partner pattern（AWS / GCP / Stripe / Slack 等預配）是否全開、custom pattern 是否涵蓋自家 internal token（service token、internal API key）、historical scan 是否跑過（不只新 commit、舊 commit 也要掃）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 跟 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Code Scanning 走 SARIF 標準</strong>：Code Scanning 不只是 CodeQL 的 UI、是 <em>SAST aggregation layer</em>。所有 SAST 結果（CodeQL 預設、或 Semgrep / Snyk Code / Brakeman / Bandit / SonarCloud / Checkmarx 等第三方）以 SARIF（Static Analysis Results Interchange Format）upload 到 Code Scanning、Security tab 統一展示、PR review 統一標註。意義是 <em>組織可以用多個 SAST 工具但只看一個 dashboard</em> — 不需要每個 vendor 各自登入。多工具 SARIF upload 用 GitHub Actions 的 <code>github/codeql-action/upload-sarif</code> step。</p>
<p><strong>CodeQL 是 first-class query language</strong>：CodeQL 用 Datalog-like 語法寫 <em>自定 query</em>、可以檢測 <em>organization-specific anti-pattern</em>（例：禁用某內部 deprecated function、強制 input validation 在特定 trust boundary）。vendor-provided pack（GitHub 維護的 CodeQL pack）覆蓋 OWASP Top 10 / CWE Top 25、自定 query 補組織 idiomatic check。代價是 <em>CodeQL 學習曲線陡</em> — 不是 regex / AST pattern、是完整的 graph query language。</p>
<p><strong>Secret Scanning 三層職責</strong>：Secret Scanning 分三層。<em>Partner pattern</em> — GitHub 跟 AWS / GCP / Stripe / Slack / npm 等 vendor 預配 token pattern、預設 detection 範圍最大、leaked token 還會通知 vendor revoke。<em>Push Protection</em> — commit push 前 scan、發現 secret 直接 reject push、開發者必須先移除才能 push；這是 <em>預防</em> 不是 <em>偵測</em>、不需要等 leaked 後 rotation。<em>Custom pattern</em> — 組織自己的 internal token（service-to-service API key、legacy auth token）寫 regex pattern、配 validation endpoint 降 FP。</p>
<p><strong>Dependency Review 是 PR-level gate</strong>：每個 PR 跑 <em>新增 / 升級依賴的漏洞檢查 + license check</em>、把 <em>新引入 CVE</em> 列在 PR review、可設 branch protection 強制 PR 過 Dependency Review 才能 merge。這跟 <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a> 是互補關係：Dependabot 是 <em>已 merge 依賴的 update PR</em>（時間軸：merge 後 vuln 出現、自動發 update PR）、Dependency Review 是 <em>PR 加新依賴時的 gate</em>（時間軸：merge 前 vuln 已知、擋 PR）。兩條軸都要開。</p>
<p><strong>Security overview 是 org-level dashboard</strong>：Organization Security tab 看 <em>跨 repo</em> 的 Code Scanning / Secret Scanning / Dependency / Dependabot alert 彙整、用 repo / severity / age filter 排序。對於 <em>security team 不是 repo owner</em> 的組織、Security manager role 給 security team 跨 repo read + triage 權限、不需要 admin。</p>
<p><strong>Security Advisories（CVE 揭露 workflow）</strong>：自家 OSS / 商業 product 出 CVE 時、走 <em>GitHub Security Advisory</em> — 在 private fork 修補、coordinated disclosure 時間到公開 advisory、GitHub 自動向 <a href="https://www.cve.org/">CVE Numbering Authority</a> 申請 CVE ID。這條 workflow 是 <em>維護者視角</em>、不是 <em>使用者視角</em>；使用者收到的是其他人發的 advisory 進 Dependabot alert。</p>
<p><strong>SARIF integration 是 GHAS 的 <em>aggregation</em> 角色關鍵</strong>：GHAS 不強迫只用 CodeQL — Snyk Code / Semgrep / SonarCloud 等 SAST 工具跑完輸出 SARIF、CI 上傳到 GitHub、Security tab 集中展示。意義是 <em>組織用 Snyk 做 SAST、但 finding 走 GHAS UI</em> 是合法配置；GHAS 賣的不只是 CodeQL、是 SAST 統一視圖。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>GHAS</th>
          <th>Snyk</th>
          <th>Trivy</th>
          <th>Dependabot（GHAS 子模組）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要範圍</td>
          <td>Source code + secret + dependency（PR-level）</td>
          <td>SCA + Container + IaC + SAST（跨 SCM）</td>
          <td>Container image + IaC + SBOM scan</td>
          <td>依賴 update + alert（merged code）</td>
      </tr>
      <tr>
          <td>SCM 綁定</td>
          <td>緊綁 GitHub</td>
          <td>跨 GitHub / GitLab / Bitbucket / Azure Repos</td>
          <td>無 SCM 綁定、跑在 CI / artifact registry</td>
          <td>緊綁 GitHub</td>
      </tr>
      <tr>
          <td>SAST 引擎</td>
          <td>CodeQL 預設 + 第三方 SARIF aggregation</td>
          <td>Snyk Code（DeepCode）</td>
          <td>無 SAST</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Secret Scanning</td>
          <td>Partner pattern + Push Protection + custom pattern</td>
          <td>Snyk Secret Scanning（較弱）</td>
          <td>有限（filesystem secret scan）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Container 強度</td>
          <td>中（Code Scanning 可掃 Dockerfile）</td>
          <td>強（Snyk Container 是主打）</td>
          <td>強（Trivy 是 container scan 標準）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>License / SBOM</td>
          <td>有（Dependency Review 含 license）</td>
          <td>強（SBOM 生成、license compliance dashboard）</td>
          <td>強（SBOM 是 first-class）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>PR 整合</td>
          <td>深 — Security tab + PR review 直連</td>
          <td>中 — GitHub Check + 跨 SCM PR comment</td>
          <td>中 — 第三方 Action 整合</td>
          <td>深 — 自動發 PR</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Per-active-committer + per-repo（Enterprise）</td>
          <td>Per-developer + tier</td>
          <td>Open source 免費（Aqua 商業版加值）</td>
          <td>GHAS 一部分</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>GitHub-heavy org、想統一 PR + security UI</td>
          <td>多 SCM / 多雲、SCA + Container 一站、license 強需求</td>
          <td>Container / IaC scan 為主、CI pluggable</td>
          <td>GitHub repo 想要自動依賴 update</td>
      </tr>
      <tr>
          <td>不適合</td>
          <td>GitLab / Bitbucket / 自管 Git 為主</td>
          <td>GitHub-only 又要省成本</td>
          <td>需要 SAST + Secret Scanning</td>
          <td>不想自動產生 PR（噪音）</td>
      </tr>
  </tbody>
</table>
<p>選 GHAS 的核心訴求：<em>GitHub 是 SCM</em> + 想 <em>PR review 跟 security finding 合一</em> + Enterprise 預算可吸收 per-committer cost。GitLab 主要的組織直接走 GitLab Ultimate 的對等功能；多 SCM 或 container 為主走 Snyk + Trivy 組合。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>CodeQL custom query 開發</strong>：寫自定 query 用 CodeQL CLI 本地開發、跑 <code>codeql database analyze</code>、SARIF output 上傳。常見場景：禁用 internal deprecated API、特定 framework 的 misuse pattern、組織 idiomatic security check。Query pack 可以 publish 到 GitHub Container Registry 或 internal registry、跨 repo 復用。代價是 <em>維護成本</em> — CodeQL query language 學習曲線陡、組織需要至少 1-2 個 security engineer 專門養護。</p>
<p><strong>Push Protection bypass workflow</strong>：Push Protection reject push 後、developer 可以 <em>bypass</em>（標記 false positive / test data / 風險已知）。Bypass 權限治理是關鍵 — 開放給 developer 個人 bypass 失去預防意義、強制 Security team approval 又拖慢 dev velocity。常見折中：低風險 pattern（test fixture token）developer 可 bypass、高風險 pattern（production credential）必須 Security team approve；所有 bypass 事件進 audit log。</p>
<p><strong>跟 GitHub Actions 整合</strong>：Code Scanning 走 GitHub Actions workflow 跑 CodeQL — <code>github/codeql-action/init</code> + <code>github/codeql-action/analyze</code>。同 workflow 可以加 <code>upload-sarif</code> step 接第三方 SAST 結果。Actions 用 GitHub-hosted runner 跑 CodeQL 是預設、大型 repo 跑 CodeQL analyze 可能超時、需改 self-hosted runner（大 RAM / 多 CPU）— 但 self-hosted runner 自身是 supply chain 風險、需要 ephemeral runner + 限制 secret access。</p>
<p><strong>SARIF 多工具整合</strong>：第三方 SAST / SCA / Container scan 工具（Snyk / Semgrep / Trivy / Brakeman / Bandit / Gosec）跑完輸出 SARIF、CI 上傳到 GHAS。實務上組織常用 <em>CodeQL + Semgrep</em> 雙軌 — CodeQL 跑深度 graph query、Semgrep 跑快速 pattern 規則；finding 在 Security tab 用 <em>tool</em> filter 分開看。</p>
<p><strong>Secret Scanning partner pattern</strong>：GitHub 維護的 partner pattern list 涵蓋 AWS / GCP / Azure / Stripe / Slack / npm / Docker Hub / GitHub PAT 等。leaked token detect 後、GitHub 自動通知 vendor、vendor 端可選擇 <em>自動 revoke</em> 該 token。意義是 <em>組織不需要做 rotation</em> — vendor 已經把 leaked token 廢掉。custom pattern 則需要組織自己提供 validation endpoint、GHAS 呼叫驗證才確認是真 leak。</p>
<p><strong>GHAS Cloud-hosted vs Self-hosted Runner 治理</strong>：CodeQL 跑在 GitHub-hosted runner 是預設、所有 source code 上傳到 GitHub 運算環境。對 <em>source code 機密度高</em> 的組織（金融 / 國防 / 法規限制 source 出境）、需走 self-hosted runner。Self-hosted runner 的供應鏈風險見 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a> — runner token 是 supply chain entry、OIDC short-lived token 是建議方向。</p>
<p><strong>GHAS Enterprise pricing trap</strong>：Per-active-committer 計費、organization 內所有 <em>過去 90 天有 commit</em> 的 user 都算 active committer、即使只 commit 1 行也計費。大型公司容易超支；2024 後 Secret Scanning 跟 Code Scanning 拆開計費、可只買 Secret Scanning（單價較低）給全 org、Code Scanning 給關鍵 repo。Public repo 上 GHAS 功能多數免費（Code Scanning、Secret Scanning、Dependency Review）；GitHub Enterprise Cloud 的 internal / private repo 才落入 GHAS 計費範圍 — 兩者範圍不同、新組織常踩到把 private repo 全開的成本。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>新建 repo 沒自動開 GHAS</strong>：Organization-level default 沒設、新 repo 預設 disable — 開 Organization Security settings 的 <em>Enable for new repositories</em>、現有 repo 用 bulk enable</li>
<li><strong>Push Protection 大量誤殺</strong>：custom pattern regex 太寬、合法字串被當 secret — 加 validation endpoint 或收緊 regex、bypass 統計看 FP rate</li>
<li><strong>Secret Scanning 沒掃歷史 commit</strong>：只 enable 後新 commit 觸發、舊 commit leaked secret 沒被發現 — 跑 <em>historical scan</em>（enable 後 GitHub 自動掃過去全部 commit）、可能花數小時</li>
<li><strong>Dependency Review 沒擋住 vuln PR</strong>：Branch protection 沒加 <em>Dependency Review</em> required check — 加進 required status check、新 PR 才強制過</li>
<li><strong>Code Scanning workflow 跑很久 / 超時</strong>：repo 太大、GitHub-hosted runner RAM 不足 — 換 larger runner（GitHub Larger Runners）或 self-hosted、或只跑 changed file analysis</li>
<li><strong>Custom CodeQL query FP 多</strong>：query 寫得太寬、commit 都跳 alert — 加 <code>@precision high</code> 標籤、用 <code>Sink-Source</code> 分析降低 reach</li>
<li><strong>第三方 SAST SARIF 沒進 Security tab</strong>：upload-sarif step 沒設對 category 或 permissions — <code>security-events: write</code> permission 必須在 workflow 給；同 repo 多工具用不同 <code>category</code> 區分</li>
<li><strong>Bypass 沒進 audit</strong>：Push Protection bypass 沒同步到 SIEM — Enterprise audit log streaming 開、event filter 加 <code>secret_scanning.bypass</code></li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 SCM（GitHub + GitLab + Bitbucket）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>Container image scan 為主</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 或 Snyk Container</td>
      </tr>
      <tr>
          <td>SBOM 生成 + license compliance</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a>（SBOM-first OSS）/ <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> + <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（SBOM 含在 scan）</td>
      </tr>
      <tr>
          <td>GitLab 為主</td>
          <td>GitLab Ultimate（SAST / Secret Detection / Dependency Scanning 內建）</td>
      </tr>
      <tr>
          <td>Secret scan 但不在 GitHub</td>
          <td>GitGuardian / Gitleaks</td>
      </tr>
      <tr>
          <td>Runtime detection（不只 source code）</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a> 系列工具</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>CodeQL 完整 query language reference</li>
<li>Dependabot 的 update PR 政策、ecosystem 覆蓋、grouped update（見 <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot vendor 頁</a>）</li>
<li>GHAS Enterprise Server（自管 GitHub）跟 Cloud GHAS 的功能差異</li>
<li>各語言 / 框架的 CodeQL pack 完整覆蓋表</li>
<li>GHAS 跟 GitHub Copilot Autofix 整合的 AI-assisted remediation 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>GHAS 在 07 案例庫沒有 <em>直接 GHAS-level vendor 事件</em>。對照引用展示 GHAS 在 supply chain / source-level 控制的能力邊界：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 GHAS 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Dependency Review + Code Scanning 應覆蓋 transitive 依賴、不只 direct import；Security Advisory 是維護者揭露 CVE 的 workflow</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>對照啟示 — GHAS Dependency Review 看 <em>package version</em>、看不到 <em>maintainer takeover</em>；需補 release-tarball vs git tag 差異跟 maintainer trust baseline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>對照啟示 — Code Scanning 是 source-level、看不到 build-time 植入；需配合 artifact provenance（SLSA L2+）+ reproducible build</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain</a></td>
          <td>對照啟示 — GHAS 自身 token / Actions 權限治理是 supply chain risk、Push Protection + OIDC trust（非長期 token）是 mitigation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></td>
          <td>GHAS 是 supply chain 治理工具集、章節原則對應四模組 workflow</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>、<a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a>、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a>（SBOM 走 SARIF 進 GHAS Code Scanning 是常見組合）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Secret Scanning 配 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> rotation）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>（GHAS alert 進 SIEM 的 routing）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（leaked secret / SAST critical finding 進 IR 流程）</li>
<li>官方：<a href="https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security">GitHub Advanced Security Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Google Security Operations</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/</guid><description>&lt;p>Google Security Operations 是 Google 雲端的 SOC 整合平台、2023 年起把前 &lt;em>Chronicle SIEM&lt;/em> + 2022 收購的 &lt;em>Siemplify SOAR&lt;/em> + 2022 收購的 &lt;em>Mandiant threat intel&lt;/em> 三條產品線整合成單一品牌。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 的差異在 &lt;em>資料規模假設 + 計費哲學 + threat intel 內建程度&lt;/em>、偵測能力本身相近 — Google 的設計假設是 &lt;em>PB/day ingestion + Google 級基礎設施 + 固定費率 by data tier&lt;/em>、跟 Splunk per-GB 累進的計費哲學完全相反。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Google Security Operations 的核心定位是 &lt;em>為超大規模 SOC 設計的雲原生 SIEM + SOAR + threat intel 一體機&lt;/em>、底層走 Google 自家 search infrastructure、上層由四個 first-class concept 撐起來：&lt;em>UDM&lt;/em>（Unified Data Model、Google 自定 schema、所有 source 強制 normalize）、&lt;em>YARA-L&lt;/em>（Google 自家 detection rule 語言）、&lt;em>Curated Detection&lt;/em>（Google 維護的 detection rule 訂閱、客戶不需自己拉）、&lt;em>Mandiant Applied Threat Intel&lt;/em>（事件期間自動 enrich + IoC push）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> 比、Google 走 &lt;em>fixed-price by data tier + 強制 schema normalization&lt;/em> — Splunk per-GB ingestion 計費在 PB-scale 會痛、Google 在 multi-PB 通常便宜 3-5 倍、但客戶要接受 UDM 強制 schema 跟 YARA-L 新語法。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a> 比、Google 是 SaaS-only + 大規模優化、Elastic 可自管 + OSS-friendly。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 比、Google 是 &lt;em>純 SOC 專用工具&lt;/em>、Datadog 是 &lt;em>observability 平面上的 security view&lt;/em>；Datadog 適合中等規模 + observability 已用 Datadog、Google 適合大規模 SOC + 不需要 observability 同 plane。&lt;/p></description><content:encoded><![CDATA[<p>Google Security Operations 是 Google 雲端的 SOC 整合平台、2023 年起把前 <em>Chronicle SIEM</em> + 2022 收購的 <em>Siemplify SOAR</em> + 2022 收購的 <em>Mandiant threat intel</em> 三條產品線整合成單一品牌。它跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 的差異在 <em>資料規模假設 + 計費哲學 + threat intel 內建程度</em>、偵測能力本身相近 — Google 的設計假設是 <em>PB/day ingestion + Google 級基礎設施 + 固定費率 by data tier</em>、跟 Splunk per-GB 累進的計費哲學完全相反。</p>
<h2 id="服務定位">服務定位</h2>
<p>Google Security Operations 的核心定位是 <em>為超大規模 SOC 設計的雲原生 SIEM + SOAR + threat intel 一體機</em>、底層走 Google 自家 search infrastructure、上層由四個 first-class concept 撐起來：<em>UDM</em>（Unified Data Model、Google 自定 schema、所有 source 強制 normalize）、<em>YARA-L</em>（Google 自家 detection rule 語言）、<em>Curated Detection</em>（Google 維護的 detection rule 訂閱、客戶不需自己拉）、<em>Mandiant Applied Threat Intel</em>（事件期間自動 enrich + IoC push）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 比、Google 走 <em>fixed-price by data tier + 強制 schema normalization</em> — Splunk per-GB ingestion 計費在 PB-scale 會痛、Google 在 multi-PB 通常便宜 3-5 倍、但客戶要接受 UDM 強制 schema 跟 YARA-L 新語法。跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> 比、Google 是 SaaS-only + 大規模優化、Elastic 可自管 + OSS-friendly。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 比、Google 是 <em>純 SOC 專用工具</em>、Datadog 是 <em>observability 平面上的 security view</em>；Datadog 適合中等規模 + observability 已用 Datadog、Google 適合大規模 SOC + 不需要 observability 同 plane。</p>
<p>關鍵張力：<em>fixed-price tier</em> 在小規模反而不划算、PB-scale 才回本。組織要看清楚自己的 ingestion 量級 — TB/day 以下走 Datadog / Elastic 通常更便宜、TB-PB/day 之間是模糊地帶、PB/day 以上 Google 是少數能撐又便宜的選擇。Mandiant threat intel 跟 Gemini for Security 是 Google-only 的加值、但這兩個是 <em>enhancement</em>、不是選 Google 的主理由。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Google Security Ops 在 SOC stack 承擔哪一段（log aggregation + SIEM + SOAR + threat intel 一體）、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> 怎麼整合</li>
<li>UDM forced normalization 跟 YARA-L 對 detection 設計的影響（schema-first 而非 query-first）</li>
<li>Curated Detection + Mandiant Applied Threat Intel 在偵測 lifecycle 的位置（不是自己拉、是訂閱）</li>
<li>何時選 Google Security Ops、何時走 Splunk / Elastic / Datadog 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Google Security Ops deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Ingestion 邊界</strong>：哪些 source 進來（Forwarder / GCS bucket / Pub/Sub feed / Cloud-native API feed）、UDM normalization 是否覆蓋全部 source、自家 app log 的 parser 是否寫好</li>
<li><strong>Detection 治理</strong>：誰能改 YARA-L rule、Curated Detection 開了哪些、自家 rule 是否走版控（Git → API push）、staging tenant 是否在 production 之前 sanity-check</li>
<li><strong>Threat intel 流向</strong>：Mandiant Applied Threat Intel 是否啟用、Curated Detection 是否跟新 IoC 自動同步、IoC enrichment 是否回 alert 上下文</li>
<li><strong>Response 流向</strong>：Siemplify SOAR 是否接 alert、playbook 是否進版控、跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> 的 routing 是否定義</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Ingestion 路徑</strong>：log 進 Google Security Ops 有三種主路徑 — <em>Chronicle Forwarder</em>（agent-based、on-prem / VM、syslog / file tail）、<em>Cloud Storage feed</em>（log 先進 GCS bucket、Google 拉）、<em>Pub/Sub feed</em>（serverless / GCP 原生 push）、再加 <em>Direct API feed</em>（cloud SaaS 像 Okta / Azure AD / AWS CloudTrail 透過原廠 connector）。SaaS-heavy 環境通常以 Direct API feed 為主、on-prem 才需要 Forwarder。</p>
<p><strong>UDM (Unified Data Model)</strong>：UDM 是 Google 自定的統一 event schema、所有 source（CloudTrail / Azure AD / Okta / endpoint / DNS）在 ingestion 時 <em>強制 normalize</em> 到 UDM 欄位（<code>principal.user</code>、<code>target.resource</code>、<code>security_result.action</code> 等）。跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> CIM 同概念、但 Splunk CIM 是 <em>選擇性 mapping</em>、Google UDM 是 <em>forced normalization</em> — 不寫 parser 就不能 ingest custom source。設計取捨：schema-first 讓跨 source query 一致、但客製 source 的 onboarding 變重。</p>
<p><strong>YARA-L detection rule</strong>：Google 自家 detection rule 語言、跟 SPL / EQL 同類但結構更明示 — <code>events { }</code> 段定義 source pattern、<code>match { }</code> 段定義 join / time window、<code>condition { }</code> 段定義 threshold、<code>outcome { }</code> 段定義 risk score。比 SPL 的 pipe 風格更接近 <em>關聯式宣告</em>、特別適合表達 <em>time-bounded sequence + cross-source join</em>。Uber MFA 那種「5min 內 50 個 MFA fail + 新裝置 + 異常地理」用 YARA-L 直接寫成 sequence pattern 比 SPL 清楚。</p>
<p><strong>Curated Detection</strong>：Google 維護的 detection rule 訂閱集合、跟 Splunk Security Content 同類但 Google 是 <em>built-in subscription</em>、客戶不需要自己拉 / merge — Google 自動跟 Mandiant threat intel 同步、新 IoC 發布後對應 rule 自動 enable。組織通常 <em>先全部啟用 baseline、再選擇性 disable noisy 規則 + 補自家 custom YARA-L</em>。</p>
<p><strong>Applied Threat Intel (Mandiant)</strong>：事件發生時 Google 自動把 alert 裡的 IoC（IP / domain / hash）跟 Mandiant feed 對照、若命中已知 APT 活動就升級 risk score + 附上 Mandiant 報告。跟其他 SIEM 走第三方 threat intel feed 需要自己 maintain enrichment pipeline 不同、Google 走 <em>vertical integration</em> — 收購 Mandiant 後直接內建。</p>
<p><strong>Siemplify SOAR</strong>：2022 收購 Siemplify 後整合進 Google Security Ops、playbook 處理 alert triage + 自動 response — 例如 leaked credential 自動 rotate（拉 <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> API）、suspect user 自動 disable（拉 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Google Workspace API）、suspect IP 自動加 firewall block（拉 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> custom rule）。playbook 進版控、走 approval gate for high-impact action、不能黑箱 fire-and-forget。</p>
<p><strong>Entity Graph</strong>：Google Security Ops 把 user / asset / IP / domain / hash 等實體做 graph、做 <em>correlation + lateral movement detection</em>。Snowflake 2024 那種「同一 credential / IP 跨多個 Snowflake account」的橫向擴散用 Entity Graph 直接視覺化關聯。</p>
<p><strong>Google Cloud 整合</strong>：跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / Workload Identity Federation 整合度高 — GCP audit log 直接內建 connector、IAM policy change 直接 surface 成 alert 候選、跨 GCP project 的 federation 走 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 認證。非 GCP 環境（AWS / Azure / on-prem）一樣支援、但設定路徑比 Splunk add-on 略陡。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google Security Operations</th>
          <th>Splunk</th>
          <th>Elastic Security</th>
          <th>Datadog Security</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>Fixed price by data tier（PB-scale 划算）</td>
          <td>Ingestion-based（GB/day、累進）</td>
          <td>Resource-based（node / cluster size）</td>
          <td>Per-host + per-event（events/month）</td>
      </tr>
      <tr>
          <td>Schema 處理</td>
          <td>UDM forced normalization</td>
          <td>CIM optional mapping</td>
          <td>ECS optional mapping</td>
          <td>Tag-based、彈性高</td>
      </tr>
      <tr>
          <td>Detection 語言</td>
          <td>YARA-L（結構化 events / match / condition）</td>
          <td>SPL（pipe-based、表達力強）</td>
          <td>KQL / EQL</td>
          <td>Datadog query</td>
      </tr>
      <tr>
          <td>Detection content</td>
          <td>Curated Detection 內建訂閱</td>
          <td>Splunk Security Content（OSS、自拉）</td>
          <td>Elastic Prebuilt + Sigma</td>
          <td>Datadog Security Rules</td>
      </tr>
      <tr>
          <td>Threat intel</td>
          <td>Mandiant Applied Threat Intel 內建</td>
          <td>需第三方 feed + 自家 pipeline</td>
          <td>需第三方 feed</td>
          <td>Datadog 內建 + 第三方</td>
      </tr>
      <tr>
          <td>SOAR / Response</td>
          <td>Siemplify SOAR 內建</td>
          <td>Splunk SOAR（前 Phantom、業界先驅）</td>
          <td>Cases + Elastic Defend</td>
          <td>Workflow Automation（基本）</td>
      </tr>
      <tr>
          <td>LLM-assisted</td>
          <td>Gemini for Security 內建（2024+）</td>
          <td>Splunk AI Assistant</td>
          <td>Elastic AI Assistant</td>
          <td>Bits AI</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>SaaS only（Google Cloud）</td>
          <td>Self-hosted / SaaS</td>
          <td>Self-hosted / SaaS / Serverless</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>PB-scale SOC、Google Cloud heavy、要 Mandiant</td>
          <td>Enterprise + 跨 on-prem、預算允許</td>
          <td>OSS-friendly、Elastic stack 已用</td>
          <td>Cloud-native + observability 已用 Datadog</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — YARA-L 跟 UDM 是 Google-specific</td>
          <td>高 — SPL / detection / dashboard 量多</td>
          <td>中 — Sigma / Lucene 較可移植</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<p>選 Google Security Ops 的核心訴求：<em>PB-scale ingestion + fixed-price 計費可預期 + Mandiant threat intel 內建 + Google Cloud 整合度</em>。中等規模 / on-prem 為主 / 預算敏感 / 需要 observability 同 plane 的場景都更適合走 Splunk / Elastic / Datadog。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Risk Score multi-signal aggregation</strong>：Google Security Ops 給每個 entity（user / asset）累積 risk score、跨多 rule 加總、超 threshold 才升級 alert。設計上跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> RBA 同類、但 Google 把 risk decay 跟 attribution 走 Entity Graph、跨 entity 關係的 risk 傳遞比較細。配對 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a> 的 lesson：MFA fail 累積 + 新裝置 login + 異常地理三個 signal 加總、單獨任一個都不該 alert。</p>
<p><strong>Cross-tenant federated search</strong>：MSSP / 大型集團多 BU 可在 Google Security Ops 跨多個 tenant 做 federated search、單一 console 看跨組織 detection。權限走 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> role assignment、跨 tenant admin 是高權限角色、走 break-glass + audit。</p>
<p><strong>Applied Threat Intel + Curated Detection 同步</strong>：Mandiant 揭露新 APT 活動後、Curated Detection 對應 rule 自動 enable + Applied Threat Intel IoC 自動 push、客戶 SOC 不需要手動 onboard。SolarWinds 2020 揭露當下、Mandiant client 是少數能即時 enable 對應 detection 的 SOC。</p>
<p><strong>Siemplify playbook 工程化</strong>：playbook 走 <em>graph-based workflow</em>（不是 linear pipeline）、可以 branching / approval gate / human-in-the-loop。Production rule 走 <em>containment-first</em>（disable session、不 delete account）+ approval gate for irreversible action。</p>
<p><strong>Gemini for Security (2024+)</strong>：LLM-assisted investigation — natural language 問「過去 24hr 哪些 user 有異常 GCP API 行為」直接生成 UDM query、alert 自動 summarize + 提供 next step 建議。不取代 SOC analyst、但縮短 triage time。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Custom source ingest 失敗</strong>：UDM parser 沒寫 / 寫錯、source 進不來或欄位 NULL — 補 parser、staging tenant 跑 sanity check、看 UDM event count by source 確認 normalization 通過</li>
<li><strong>Detection 沒觸發 / 漏報</strong>：YARA-L 的 <code>match { }</code> 段 time window 寫太短、或 <code>condition { }</code> threshold 寫太高 — staging tenant 用歷史資料 backtest、tune window / threshold 後 promote</li>
<li><strong>Alert volume 過多</strong>：Curated Detection 全開沒 tune、env-specific noise 沒 disable — 跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 一樣走 staging 觀察 false positive curve、tune 或 disable 個別規則</li>
<li><strong>Mandiant threat intel 沒命中</strong>：licensing tier 沒包 Mandiant Advantage、或 enrichment pipeline 沒啟用 — 檢查 tier、確認 Applied Threat Intel 開</li>
<li><strong>Siemplify playbook 黑箱 fire-and-forget</strong>：自動 disable 結果誤殺合法 user — playbook 走 approval gate、預設 containment 不 deletion、定期 dry-run</li>
<li><strong>Cross-tenant admin 太多</strong>：日常運維用 cross-tenant admin、blast radius 太大 — 收 admin、改 tenant-scoped role + 特定 capability、跨 tenant 走 break-glass</li>
<li><strong>Cost 比預期高</strong>：data tier 選錯（買了 Enterprise Plus 卻只用 Enterprise feature）、retention 設太長 — 看實際 ingestion + retention 用量、tier 跟 retention 一起 review</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise + 跨 on-prem + detection 成熟</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a></td>
      </tr>
      <tr>
          <td>OSS-friendly / 自管 / 預算敏感</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>Cloud-native + observability 已用 Datadog</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></td>
      </tr>
      <tr>
          <td>DLP / sensitive data discovery</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Endpoint detection 為主</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>YARA-L 完整語法 reference、UDM 全欄位 schema</li>
<li>Chronicle / Siemplify / Mandiant 三條產品線整合前的歷史細節</li>
<li>Mandiant Advantage 平台（threat intel 訂閱、跟 SIEM 整合但獨立產品）</li>
<li>VirusTotal（Google 旗下、跟 Mandiant 互補但獨立服務）</li>
<li>Gemini for Security 的 prompt engineering 細節</li>
<li>Google Workspace security center（屬 Google Workspace、不在 Security Ops 範圍）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Google Security Ops 在 07 案例庫沒有直接 vendor-level 事件、但所有 detection-related case 都是 SIEM 偵測覆蓋率的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Google Security Ops 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>UDM 強制 normalize 跨 Azure AD / GCP / Okta token validation 欄位、YARA-L 跨 source join 直接表達跨租戶 token forging pattern、Entity Graph 視覺化</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>YARA-L sequence pattern 直接表達「MFA fail count + 新裝置 login」、Risk Score 累積到 threshold 觸發 Siemplify playbook 自動 disable session</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>Mandiant 揭露 IoC 後 Applied Threat Intel 自動 push、Curated Detection 對應規則自動 enable、客戶不需要手動 onboard rule</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>YARA-L 表達「query 體積 / 跨 schema scan / 來源 IP baseline」三軸 correlation rule；Entity Graph 聚合 credential / IP / data warehouse account 視覺化異常擴散（公開 UNC5537 跨客戶模式屬案例外延伸）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>Curated Detection + 自家 YARA-L rule 走 propose → staging → promote lifecycle、Google Security Ops 內建 rule versioning + Git → API push</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>Risk Score multi-signal aggregation 是 alert fatigue 的工程化解法、跟 Splunk RBA 同類但 risk 傳遞走 Entity Graph、跨 entity 關係更細</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>、<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP signal 進 Google Security Ops）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（GCP IAM log + Workload Identity Federation）、<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>（SOAR playbook 拉 API）、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP log source）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（WAF log + auto-block）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（alert → IR routing）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（log pipeline 共用判斷）</li>
<li>官方：<a href="https://cloud.google.com/security/products/security-operations">Google Security Operations Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Locust</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/</guid><description>&lt;p>Locust 的核心責任是用 Python 表達高度自訂的使用者行為與 protocol client。它適合 Python 團隊、需要自訂 client、需要 distributed worker、或 scenario 邏輯比工具內建 sampler 更複雜的壓測流程。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Locust 適合把壓測寫成一般 Python 程式。當 workload model 需要呼叫 internal SDK、特殊 protocol、複雜資料準備、狀態機、隨機行為或自訂 client、Locust 可以直接使用 Python 生態來表達。底層架構是 &lt;em>master + worker&lt;/em> 分散式 swarm、worker 之間用 Gevent green-thread（非 OS thread）模擬大量並發 user、master 負責 spawn rate、aggregation 跟 Web UI。&lt;/p>
&lt;p>這個定位讓 Locust 接到 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a>。它能把特殊 client 與下游 dependency 放進同一個 user behavior、但也要求團隊處理 runner、資料與可重現性。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a>（JS / Go runtime）比、Locust 用 Python 換到 &lt;em>自訂能力與生態相容&lt;/em>、但代價是單 worker capacity 低、CPU bound 容易先打到自己。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter&lt;/a>（GUI / XML）比、Locust 偏 &lt;em>code-first 工程團隊&lt;/em>、scenario 直接走 Git review、不靠 GUI plugin 拼裝。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling&lt;/a>（Scala DSL）比、Locust 換到 &lt;em>Python team 友善 + 既有 domain library 重用&lt;/em>、但失去 JVM injection profile 的精細度與報表內建。&lt;/p>
&lt;p>關鍵張力：&lt;em>Python 表達力&lt;/em> ↔ &lt;em>runner 效能上限&lt;/em>。Python team 想 reuse domain library、staging fixture、API client 寫壓測腳本時 Locust 是首選；但要心裡有數 &lt;em>單 worker RPS 上限不高&lt;/em>、超過幾千 RPS 就要靠 worker scale-out、不是調 Locust 本身。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>Python 團隊適合用 Locust 長期維護壓測。既有 domain library、API client、fixture、資料產生器與驗證 helper 都可以被壓測腳本重用。&lt;/p>
&lt;p>自訂 protocol 適合用 Locust。HTTP 之外、如果服務需要 gRPC、WebSocket、binary protocol、message broker client 或自家 SDK、Locust 可以直接接 Python library。&lt;/p>
&lt;p>Distributed load 適合用 Locust worker 擴展。當單機 Python runner 遇到 CPU 或 connection bottleneck、可以用 master / worker 拆開負載產生能力。&lt;/p></description><content:encoded><![CDATA[<p>Locust 的核心責任是用 Python 表達高度自訂的使用者行為與 protocol client。它適合 Python 團隊、需要自訂 client、需要 distributed worker、或 scenario 邏輯比工具內建 sampler 更複雜的壓測流程。</p>
<h2 id="服務定位">服務定位</h2>
<p>Locust 適合把壓測寫成一般 Python 程式。當 workload model 需要呼叫 internal SDK、特殊 protocol、複雜資料準備、狀態機、隨機行為或自訂 client、Locust 可以直接使用 Python 生態來表達。底層架構是 <em>master + worker</em> 分散式 swarm、worker 之間用 Gevent green-thread（非 OS thread）模擬大量並發 user、master 負責 spawn rate、aggregation 跟 Web UI。</p>
<p>這個定位讓 Locust 接到 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。它能把特殊 client 與下游 dependency 放進同一個 user behavior、但也要求團隊處理 runner、資料與可重現性。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>（JS / Go runtime）比、Locust 用 Python 換到 <em>自訂能力與生態相容</em>、但代價是單 worker capacity 低、CPU bound 容易先打到自己。跟 <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a>（GUI / XML）比、Locust 偏 <em>code-first 工程團隊</em>、scenario 直接走 Git review、不靠 GUI plugin 拼裝。跟 <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>（Scala DSL）比、Locust 換到 <em>Python team 友善 + 既有 domain library 重用</em>、但失去 JVM injection profile 的精細度與報表內建。</p>
<p>關鍵張力：<em>Python 表達力</em> ↔ <em>runner 效能上限</em>。Python team 想 reuse domain library、staging fixture、API client 寫壓測腳本時 Locust 是首選；但要心裡有數 <em>單 worker RPS 上限不高</em>、超過幾千 RPS 就要靠 worker scale-out、不是調 Locust 本身。</p>
<h2 id="適用場景">適用場景</h2>
<p>Python 團隊適合用 Locust 長期維護壓測。既有 domain library、API client、fixture、資料產生器與驗證 helper 都可以被壓測腳本重用。</p>
<p>自訂 protocol 適合用 Locust。HTTP 之外、如果服務需要 gRPC、WebSocket、binary protocol、message broker client 或自家 SDK、Locust 可以直接接 Python library。</p>
<p>Distributed load 適合用 Locust worker 擴展。當單機 Python runner 遇到 CPU 或 connection bottleneck、可以用 master / worker 拆開負載產生能力。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Locust 在壓測 stack 中承擔哪一段（user behavior modeling / load generation / distributed swarm）、哪些要外接（<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Prometheus / Grafana</a> 觀測 worker 自身、APM 看目標 saturation）</li>
<li>User class / task weight / on_start lifecycle 的 ownership 設計（誰寫 locustfile、誰 review、誰調 spawn rate）</li>
<li>Distributed master-worker 部署的容量規劃（單 worker user 上限、worker 數量計算、target RPS 對應 worker count）</li>
<li>何時用 Locust、何時走 k6 / JMeter / Gatling 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Locust 壓測是否健康、最少看四件事：</p>
<ul>
<li><strong>User class 設計</strong>：每個 <code>HttpUser</code> / <code>User</code> subclass 是不是一個明確的 <em>persona</em>（mobile user / API client / admin user）、<code>wait_time</code> 是否反映真實使用者間隔（不是 0 拼最大 RPS、是 <code>between(1, 5)</code> 模擬 think time）、user state 是否在 instance 內封閉</li>
<li><strong>Task 比例</strong>：<code>@task(weight)</code> 數字是否對應 production traffic mix（80% read / 15% write / 5% admin、不是每個 endpoint 等比例）、weight 是否走版控 review</li>
<li><strong>on_start lifecycle</strong>：login / token fetch / session bootstrap 是否寫在 <code>on_start</code>（每個 user 一次）、不是寫在 <code>@task</code> 裡（每個 request 都重做）— 寫錯位置會讓 auth endpoint 變成主要 traffic</li>
<li><strong>Distributed master-worker</strong>：worker 數量是否夠（單 worker 跑幾千 user 後 CPU 會先打死、不是目標服務先死）、master 是否獨立機器（master 也跑 user 時 aggregation 跟 Web UI 會卡）、<code>--expect-workers</code> 是否設、worker sync drift 是否觀察</li>
</ul>
<p>四件事任一缺失、就是壓測證據可信度的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>locustfile 結構</strong>：locustfile.py 是 Python module、定義 <code>User</code> / <code>HttpUser</code> subclass、每個 user 有 <code>wait_time</code>、若干 <code>@task(weight)</code> method、<code>on_start</code> / <code>on_stop</code> lifecycle hook。執行用 <code>locust -f locustfile.py --host=https://target</code> 起 Web UI、或 <code>locust --headless -u 1000 -r 100 -t 10m</code> 在 CI 跑無 UI 模式。locustfile 應該走 Git review、不是 GUI 改完就跑。</p>
<p><strong>Task weight / wait_time 設計</strong>：weight 是 <em>相對權重</em>、不是百分比 —<code>@task(8)</code> + <code>@task(2)</code> 等於 80% / 20%。<code>wait_time = between(1, 5)</code> 在每個 task 之間等 1-5 秒、模擬 think time；若要拚最大 RPS 用 <code>constant(0)</code>、但同時要意識到這就不是 user behavior 模型、是 <em>throughput probe</em>。</p>
<p><strong>on_start vs @task 的邊界</strong>：<code>on_start(self)</code> 每個 user instance 啟動時跑一次、適合做 login、token fetch、cache warm、fixture lookup；<code>@task</code> 是 user 行為主迴圈、每次選一個 task 跑。把 login 寫在 <code>@task</code> 是常見錯誤、會讓 IdP 變成主壓力來源、不是目標 API。</p>
<p><strong>Gevent-based concurrency</strong>：Locust 用 <a href="https://www.gevent.org/">gevent</a> 的 green-thread 模擬大量 concurrent user、不是 OS thread。意義是單 worker 可以跑幾千個 <em>user</em>、但 CPU bound 工作（JSON serialization、加密、本地計算）會 <em>blocking</em> 整個 worker 的 event loop。<code>gevent.monkey.patch_all()</code> 要在 import 第一行、否則 socket / time / ssl 不會被 patch、blocking call 會卡死 swarm。</p>
<p><strong>Distributed master-worker</strong>：單機到極限時開 distributed — <code>locust --master</code> 起 master、<code>locust --worker --master-host=master.example.com</code> 起 worker。Master 負責 Web UI、spawn rate 控制、result aggregation、stat 收集；worker 負責跑 user。Master 不該跑 user（會跟 aggregation 搶 CPU、stat 失真）。worker 數量計算：先單 worker 拉到 CPU 80% 看能撐多少 user、目標 user 數除這個值 + 20% buffer。</p>
<p><strong>Custom load shape</strong>：除了固定 <code>-u 1000</code>、Locust 支援 <code>LoadTestShape</code> subclass 寫 <em>時間軸負載曲線</em> — spike test（瞬間 0 → 5000 user）、ramp test（線性爬升）、wave test（週期性高低交替）、step test（階梯式增加）。<code>tick()</code> method 每秒回傳 <code>(user_count, spawn_rate)</code>。用 custom shape 才能模擬 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek waiting room</a> 那種 ticket drop 瞬間衝擊。</p>
<p><strong>Prometheus exporter / 觀測</strong>：Locust 內建 stat 只是 in-memory 的 p50 / p95 / p99 / RPS、結束就消失。長期觀測接 <a href="https://github.com/ContainerSolutions/locust_exporter">locust-prometheus-exporter</a>（或 <code>--csv result.csv</code> 自己抓）、把 metric 推到 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Prometheus</a> + Grafana。<strong>worker 自身的 CPU / memory / network</strong> 一定要同時觀測、不然分不出是目標 saturation 還是 worker 已死。</p>
<p><strong>Locust Cloud（managed SaaS）</strong>：2024 後 Locust 推官方 <a href="https://docs.locust.cloud/">Locust Cloud</a>、託管 master + worker + result storage、付費換 ops 成本。自管 master-worker 對 CI / staging 是合理的；production 等級的 scale test（10k+ concurrent user）跑一次要拉幾十台 worker、用 Cloud 省 infra ops 是合理 trade-off。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Locust</th>
          <th>k6</th>
          <th>JMeter</th>
          <th>Gatling</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>腳本語言</td>
          <td>Python（generic）</td>
          <td>JavaScript (k6 runtime)</td>
          <td>XML / GUI / Groovy</td>
          <td>Scala DSL（也支援 Java / Kotlin）</td>
      </tr>
      <tr>
          <td>Runtime</td>
          <td>Python + Gevent green-thread</td>
          <td>Go-based、單 binary、低 overhead</td>
          <td>JVM、heavy</td>
          <td>JVM、async actor model</td>
      </tr>
      <tr>
          <td>單 worker capacity</td>
          <td>中低（Python overhead、千級 user）</td>
          <td>高（Go runtime、萬級 VU 單機）</td>
          <td>中（JVM tuning 後可用）</td>
          <td>高（Akka actor、效能好）</td>
      </tr>
      <tr>
          <td>Distributed mode</td>
          <td>內建 master-worker</td>
          <td>內建 k6 Cloud / k6 Operator</td>
          <td>內建 master-slave</td>
          <td>Gatling Enterprise（前 FrontLine）</td>
      </tr>
      <tr>
          <td>User behavior 彈性</td>
          <td>高 — 一般 Python、任意 library</td>
          <td>中 — JS 但 k6 runtime 受限</td>
          <td>中 — GUI 拼裝 + plugin</td>
          <td>中高 — Scala DSL 表達 simulation</td>
      </tr>
      <tr>
          <td>Custom protocol</td>
          <td>強 — 接任何 Python library</td>
          <td>強 — 有 gRPC / WS / Kafka extension</td>
          <td>強但繁瑣 — plugin 生態廣</td>
          <td>中 — 主要 HTTP / WS</td>
      </tr>
      <tr>
          <td>CI / headless</td>
          <td><code>--headless</code> 支援</td>
          <td>CI-first design</td>
          <td>non-GUI mode 支援</td>
          <td>內建支援</td>
      </tr>
      <tr>
          <td>Report / UI</td>
          <td>Web UI 即時 + CSV 匯出</td>
          <td>k6 Cloud / Grafana / 簡 stdout</td>
          <td>GUI listener / HTML report</td>
          <td>HTML report 內建、視覺豐富</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>緩（Python team）/ 陡（非 Python）</td>
          <td>中 — JS-style scripting</td>
          <td>緩（GUI）/ 陡（深度 tuning）</td>
          <td>陡 — Scala 語法</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Python team + 自訂 behavior / client</td>
          <td>DevOps + CI / 標準 HTTP / 高 RPS 單機</td>
          <td>非工程角色協作 / legacy enterprise</td>
          <td>JVM team + 精細 injection profile</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — Python 腳本可移植</td>
          <td>中 — k6 runtime 綁定</td>
          <td>中 — XML jmx 不易他移</td>
          <td>中 — Scala DSL 綁定</td>
      </tr>
  </tbody>
</table>
<p>選 Locust 的核心訴求：<em>Python team + custom user behavior + 既有 domain library 重用</em>、且能投入 worker scale-out 預算（單 worker capacity 低、要靠分散式補）+ scenario 走 Git review 不靠 GUI。標準 HTTP 高 RPS 單機壓測直接走 k6 更快、非工程角色協作壓測走 JMeter、JVM team 精細模擬走 Gatling。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Distributed Locust 的 master-worker swarm</strong>：production scale test 通常需要 10-100 個 worker。實作要點：worker 之間 <em>不要</em> 共享 state、shared resource 由 master 統一發（用 <a href="https://zeromq.org/">zeromq</a> message bus）；worker 加入 / 離開時 user 會 redistribute、避免 user index 當 unique key；worker 跨 region 跑時 <em>latency 來自 worker → target 不只是 target 內部</em>、要在 worker 本身的 region 對齊。</p>
<p><strong>Custom load shape（spike / wave / step）</strong>：<code>LoadTestShape.tick(self)</code> return <code>(user_count, spawn_rate)</code> tuple 每秒被叫一次。Spike test：前 60 秒 0 user、第 61 秒瞬間衝 5000、模擬 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek waiting room</a> 的 admission storm。Wave test：sine wave 在 1000-3000 user 之間振盪、測 autoscaling 反應速度。Step test：每 5 分鐘加 1000 user、觀察哪一階開始降級。custom shape 是 Locust 比 k6 強的點之一。</p>
<p><strong>跟 Prometheus exporter 整合</strong>：locust-prometheus-exporter 把 Locust stat 推到 Prometheus / Grafana、做長期 baseline、跨 test 比較、p99 退化偵測。實務上要在 dashboard 同時放 <em>Locust 內部 stat</em> + <em>worker host metric</em> + <em>目標服務 APM</em>、三層 stack 起來才能判讀是 runner 還是目標 saturation。</p>
<p><strong>Locust Cloud（managed SaaS）</strong>：2024+ 官方 SaaS、託管 master + worker + result + dashboard。trade-off：自管適合 CI / staging / 內網壓測（target 跑在內網時 Cloud 連不到）；Cloud 適合大規模一次性 scale test（拉 50 worker 跑 2 小時、跑完即停、不想自己 infra ops）。</p>
<h2 id="操作成本">操作成本</h2>
<p>Locust 的主要成本是 runner overhead 與分散式治理。Python runner 的效能上限要用 worker scale-out 解決；壓測結論要同時檢查目標服務 saturation 與 worker 本身 CPU、connection、network 是否已成瓶頸。</p>
<p>腳本工程成本來自自由度。Python 可以很快寫出複雜行為、也容易把測試資料、randomness、side effect、sleep 與 exception handling 寫散；團隊要維持 scenario structure、fixture、logging 與 artifact 標準。</p>
<p>自訂 client 成本來自校正。使用 SDK 或 custom protocol client 時、要確認 client retry、timeout、connection pool 與 serialization 行為是否接近 production、避免 runner 模擬出不存在的壓力形狀。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Worker CPU 100% 但目標服務閒</strong>：Python runner 先死、不是 target saturation — 加 worker 數量、或檢查 task 裡有沒有 CPU bound 的本地計算（大 JSON parse、加密、本地 fixture 生成）擠掉 event loop</li>
<li><strong>Gevent monkey-patch gotcha</strong>：<code>requests</code> / <code>psycopg2</code> / 自家 SDK 在第三方 library 內部 blocking call、整個 worker 卡住 — <code>gevent.monkey.patch_all()</code> 一定要寫在 import 第一行；無法 patch 的 C extension（如 native MySQL driver）改用 gevent-friendly client</li>
<li><strong>RPS 達不到目標 / 看起來像 target 慢</strong>：實際是 worker connection pool 耗盡、或 worker 本身網卡飽和 — 觀測 worker 本身的 TCP socket 數、netstat ESTABLISHED、network throughput；不要直接 blame target</li>
<li><strong>Distributed sync drift</strong>：worker 之間 user count 不平均、aggregation 顯示 RPS 抖動 — <code>--expect-workers=N</code> 確認 master 等所有 worker join 才開測；worker 跨 region 時 message bus latency 也會影響 sync</li>
<li><strong>on_start 在 @task 裡跑</strong>：壓測啟動瞬間打爆 auth endpoint、看到 IdP latency 飆高以為是 target — 把 login / token fetch 移到 <code>on_start</code>、每個 user 只做一次</li>
<li><strong>wait_time = 0 拼最大 RPS 結果結論奇怪</strong>：這已經不是 user behavior 是 throughput probe、p99 跟 production 對不上 — 改成 <code>between(1, 5)</code> 模擬 think time 或寫 custom shape</li>
<li><strong>Web UI 卡 / master CPU 100%</strong>：master 同時在跑 user + aggregation — <code>locust --master</code> 跟 worker 拆機器、master 不跑 user</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>標準 HTTP / 高 RPS 單機 / CI-first</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a></td>
      </tr>
      <tr>
          <td>非工程角色協作 / GUI 拼裝</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a></td>
      </tr>
      <tr>
          <td>JVM team / 精細 injection profile</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a></td>
      </tr>
      <tr>
          <td>極簡 HTTP probe / 命令列 one-shot</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/vegeta/" data-link-title="Vegeta" data-link-desc="用簡潔 CLI 與固定 rate HTTP attack 快速探測 latency、throughput 與 saturation 的效能工程工具">Vegeta</a></td>
      </tr>
      <tr>
          <td>Production traffic replay / shadow</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a> / <a href="/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring</a></td>
      </tr>
      <tr>
          <td>壓測結果回寫到效能工程 lifecycle</td>
          <td><a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>、<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>locustfile 完整語法 reference、<code>User</code> 跟 <code>HttpUser</code> 的 attribute 細節</li>
<li>Locust Cloud 計費跟 quota 細節（看官方 docs）</li>
<li>gevent 跟 asyncio 的取捨（Locust 選了 gevent、不在本頁討論替代）</li>
<li>壓測證據怎麼歸檔（看 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">9.7 evidence package</a> 通則）</li>
</ul>
<h2 id="evidence-package">Evidence Package</h2>
<p>Locust 結果應回寫到 evidence package。最小欄位包括 locustfile version、user class、task weight、spawn rate、worker count、client library version、target environment、p95 / p99、error rate、throughput、target saturation metric、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Locust 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>locustfile、CSV / JSON result、dashboard link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>test start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / metrics / logs 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>user behavior coverage、fixture freshness</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>worker capacity、client realism</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>worker bottleneck、custom client 偏差、資料偏差</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是區分目標瓶頸與 runner 瓶頸。Locust 分散式測試要同時保存 worker 數量、worker 資源、spawn rate 與 client behavior、讓 reviewer 知道壓力是否真的打到目標服務。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>Locust 適合回寫需要高度自訂 user behavior 的案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 雙峰 workload</a> 的投注行為模型、<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek waiting room</a> 的 admission / token flow、<a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay mobile payment messaging</a> 的外部推送與下游 quota 模擬、<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Niantic Pokémon GO 50x surge</a> 的玩家移動 + 互動混合行為、以及 <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom COVID 30x surge</a> 的會議建立 / 加入 / 離開行為混合。</p>
<p>這些案例的重點是 domain behavior。Locust 頁引用案例時、要把 case 轉成 user class、task weight、custom client、downstream mock 與 worker capacity、再把總 RPS 放回這些行為條件下判讀 — 例如 Pokémon GO 玩家行為跟一般 web user 完全不同（持續 GPS 上報 + 偶發互動）、不能直接用 HTTP RPS 衡量；SeatGeek waiting room 要寫 <code>LoadTestShape</code> 模擬 ticket drop 瞬間衝擊、不是穩態 RPS。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>、<a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a>、<a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>、<a href="/blog/backend/09-performance-capacity/vendors/vegeta/" data-link-title="Vegeta" data-link-desc="用簡潔 CLI 與固定 rate HTTP attack 快速探測 latency、throughput 與 saturation 的效能工程工具">Vegeta</a></li>
<li>跨類：<a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a>（production traffic replay 替代 synthetic load）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 Observability</a>（worker 自身 + 目標 APM 雙觀測）</li>
<li>官方：<a href="https://docs.locust.io/">Locust documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.4 Repository Adapter 實作</title><link>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</guid><description>&lt;p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 &lt;code>domain model&lt;/code> 和 &lt;code>SQL model&lt;/code> 之間的邊界層、不承擔業務流程編排。&lt;/p>
&lt;p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。&lt;/p>
&lt;h2 id="port--adapter-邊界">Port / Adapter 邊界&lt;/h2>
&lt;p>Repository 在 hexagonal architecture（也叫 ports &amp;amp; adapters）中是 &lt;em>outbound port&lt;/em> 的實作。&lt;/p>
&lt;p>&lt;strong>Port（domain layer 定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>抽象 interface / protocol、描述 &lt;em>領域語意&lt;/em>&lt;/li>
&lt;li>不暴露 SQL、不暴露 DB 細節&lt;/li>
&lt;li>例：&lt;code>type OrderRepository interface { Find(id) Order; Save(order); ... }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Adapter（infrastructure layer 實作）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>實作 port、負責跟具體 DB 對話&lt;/li>
&lt;li>翻譯 domain entity ↔ DB row&lt;/li>
&lt;li>翻譯 DB error → domain error&lt;/li>
&lt;li>例：&lt;code>type SQLOrderRepository struct { db *sql.DB }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼這層抽象有價值&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>可替換性&lt;/strong>：DB 換 vendor 時、domain layer 不必改&lt;/li>
&lt;li>&lt;strong>可測試性&lt;/strong>：在 domain layer test 時可注入 memory fake、不必起 DB&lt;/li>
&lt;li>&lt;strong>語意清楚&lt;/strong>：domain 不被 SQL 細節污染、business rule 集中&lt;/li>
&lt;li>&lt;strong>演進可控&lt;/strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式&lt;/li>
&lt;/ol>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片&lt;/a>。&lt;/p>
&lt;h2 id="adapter-三個核心責任">Adapter 三個核心責任&lt;/h2>
&lt;p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。&lt;/p>
&lt;p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。&lt;/p>
&lt;h3 id="1-查詢與命令組裝">1. 查詢與命令組裝&lt;/h3>
&lt;p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Raw SQL&lt;/strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection&lt;/li>
&lt;li>&lt;strong>Query builder&lt;/strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL&lt;/li>
&lt;li>&lt;strong>ORM&lt;/strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1&lt;/li>
&lt;/ul>
&lt;p>詳見下方「ORM vs Query Builder vs Raw SQL」段。&lt;/p>
&lt;h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling&lt;/h3>
&lt;p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。&lt;/p>
&lt;p>&lt;strong>Nullable handling 模式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Optional type&lt;/strong>：Go &lt;code>sql.NullString&lt;/code>、Java &lt;code>Optional&amp;lt;T&amp;gt;&lt;/code>、Rust &lt;code>Option&amp;lt;T&amp;gt;&lt;/code>、Python &lt;code>Optional[T]&lt;/code>&lt;/li>
&lt;li>&lt;strong>Sentinel value&lt;/strong>：用特殊值代表 null（不推薦、易混淆）&lt;/li>
&lt;li>&lt;strong>Default fallback&lt;/strong>：null → 預設值（要明確、不要悄悄轉換）&lt;/li>
&lt;/ul>
&lt;p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 <code>domain model</code> 和 <code>SQL model</code> 之間的邊界層、不承擔業務流程編排。</p>
<p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。</p>
<h2 id="port--adapter-邊界">Port / Adapter 邊界</h2>
<p>Repository 在 hexagonal architecture（也叫 ports &amp; adapters）中是 <em>outbound port</em> 的實作。</p>
<p><strong>Port（domain layer 定義）</strong>：</p>
<ul>
<li>抽象 interface / protocol、描述 <em>領域語意</em></li>
<li>不暴露 SQL、不暴露 DB 細節</li>
<li>例：<code>type OrderRepository interface { Find(id) Order; Save(order); ... }</code></li>
</ul>
<p><strong>Adapter（infrastructure layer 實作）</strong>：</p>
<ul>
<li>實作 port、負責跟具體 DB 對話</li>
<li>翻譯 domain entity ↔ DB row</li>
<li>翻譯 DB error → domain error</li>
<li>例：<code>type SQLOrderRepository struct { db *sql.DB }</code></li>
</ul>
<p><strong>為什麼這層抽象有價值</strong>：</p>
<ol>
<li><strong>可替換性</strong>：DB 換 vendor 時、domain layer 不必改</li>
<li><strong>可測試性</strong>：在 domain layer test 時可注入 memory fake、不必起 DB</li>
<li><strong>語意清楚</strong>：domain 不被 SQL 細節污染、business rule 集中</li>
<li><strong>演進可控</strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式</li>
</ol>
<p>詳見 <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片</a>。</p>
<h2 id="adapter-三個核心責任">Adapter 三個核心責任</h2>
<p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。</p>
<p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。</p>
<h3 id="1-查詢與命令組裝">1. 查詢與命令組裝</h3>
<p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：</p>
<ul>
<li><strong>Raw SQL</strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection</li>
<li><strong>Query builder</strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL</li>
<li><strong>ORM</strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1</li>
</ul>
<p>詳見下方「ORM vs Query Builder vs Raw SQL」段。</p>
<h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling</h3>
<p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。</p>
<p><strong>Nullable handling 模式</strong>：</p>
<ul>
<li><strong>Optional type</strong>：Go <code>sql.NullString</code>、Java <code>Optional&lt;T&gt;</code>、Rust <code>Option&lt;T&gt;</code>、Python <code>Optional[T]</code></li>
<li><strong>Sentinel value</strong>：用特殊值代表 null（不推薦、易混淆）</li>
<li><strong>Default fallback</strong>：null → 預設值（要明確、不要悄悄轉換）</li>
</ul>
<p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h3 id="3-error-translation">3. Error Translation</h3>
<p>error translation 的責任是把底層錯誤分類成應用層可決策訊號。唯一鍵衝突、外鍵限制、交易衝突、連線逾時、都需要翻譯成可行動錯誤類型、而不是將原生錯誤字串直接外漏。</p>
<p><strong>常見錯誤分類</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Domain error</th>
          <th>SQL error 對應</th>
          <th>應用層動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ErrAlreadyExists</code></td>
          <td><code>unique_violation</code>（PostgreSQL 23505）</td>
          <td>409 Conflict / 業務 retry</td>
      </tr>
      <tr>
          <td><code>ErrNotFound</code></td>
          <td>empty result set</td>
          <td>404</td>
      </tr>
      <tr>
          <td><code>ErrConstraintFailed</code></td>
          <td><code>foreign_key_violation</code>（23503）</td>
          <td>400 Bad Request</td>
      </tr>
      <tr>
          <td><code>ErrConflict</code></td>
          <td><code>serialization_failure</code>（40001）</td>
          <td>retry with backoff</td>
      </tr>
      <tr>
          <td><code>ErrTimeout</code></td>
          <td><code>query_canceled</code>（57014）/ context deadline</td>
          <td>retry / circuit break</td>
      </tr>
      <tr>
          <td><code>ErrUnavailable</code></td>
          <td>connection refused / pool exhausted</td>
          <td>circuit break / fallback</td>
      </tr>
  </tbody>
</table>
<p>這層翻譯會直接影響重試、回退與事故判讀。分類越穩定、越能在 06/08 模組形成一致決策語言。</p>
<h2 id="orm-vs-query-builder-vs-raw-sql">ORM vs Query Builder vs Raw SQL</h2>
<p>選 mapping 工具是 repository adapter 的核心取捨。</p>
<h3 id="raw-sql">Raw SQL</h3>
<ul>
<li>優勢：完全控制 query plan、易 tune</li>
<li>優勢：大規模 query 性能最好</li>
<li>限制：易拼錯字、IDE 支援差</li>
<li>風險：一不小心就 SQL injection（用 prepared statement / parameterized query）</li>
<li>適合：性能極限關鍵 / 複雜 query / 已有 SQL 專家團隊</li>
</ul>
<h3 id="query-builder">Query Builder</h3>
<p>主流工具：Knex（Node）、SQLAlchemy Core（Python）、jOOQ（Java）、sqlc（Go）、Diesel（Rust）。</p>
<ul>
<li>優勢：型別安全、IDE 自動完成</li>
<li>優勢：不需要 ORM 的複雜度</li>
<li>優勢：仍可看到生成的 SQL</li>
<li>限制：學 DSL 成本</li>
<li>適合：中等複雜度 + 想要安全性 + 想看 SQL</li>
</ul>
<h3 id="orm">ORM</h3>
<p>主流工具：GORM（Go）、SQLAlchemy ORM（Python）、Active Record（Rails）、JPA / Hibernate（Java）、Entity Framework（.NET）、Prisma（TypeScript）。</p>
<ul>
<li>優勢：CRUD 操作快速、boilerplate 少</li>
<li>優勢：自動 mapping、自動 transaction</li>
<li>優勢：migration 工具通常整合</li>
<li>限制：隱藏 SQL 細節、易產生 N+1 query</li>
<li>限制：複雜 query 反而比 raw SQL 難寫</li>
<li>風險：lazy loading 容易意外性能問題</li>
<li>適合：CRUD 為主的應用、團隊偏業務開發</li>
</ul>
<h3 id="選型決策">選型決策</h3>
<ol>
<li><strong>小團隊 + CRUD-heavy</strong>：ORM（快速 prototype、boilerplate 少）</li>
<li><strong>中型 + 混合需求</strong>：Query Builder（安全 + 仍能寫複雜 query）</li>
<li><strong>大型 + 性能極限</strong>：Raw SQL + Query Builder（複雜 query 用 raw、簡單用 builder）</li>
<li><strong>microservice 私有 store</strong>：通常 Query Builder 為主（見 <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> 模式）</li>
</ol>
<h3 id="orm-反模式">ORM 反模式</h3>
<ul>
<li><code>find()</code> 隨手呼叫導致 N+1 query</li>
<li>lazy loading 在 view 層觸發 query</li>
<li>用 ORM 寫複雜 aggregation（應該 raw SQL）</li>
<li>不 eager load 關聯資料</li>
</ul>
<h2 id="testing-策略">Testing 策略</h2>
<p>repository 是 <em>infrastructure</em> 層、test 策略不同於 domain layer。</p>
<h3 id="memory-fakeunit-test-友善">Memory Fake（unit test 友善）</h3>
<ul>
<li>用 in-memory implementation 滿足 port interface</li>
<li>不必起 DB、快、可隔離</li>
<li>適合：domain layer test、test repository 的 <em>呼叫者</em></li>
<li>反模式：用 memory fake test repository 本身（測不到實際 SQL 行為）</li>
</ul>
<h3 id="integration-test驗證真實-db-行為">Integration Test（驗證真實 DB 行為）</h3>
<ul>
<li>用 testcontainers / Docker 起真實 DB（PostgreSQL / MySQL）</li>
<li>跑真實 SQL、抓真實 error</li>
<li>用 transaction rollback 隔離各 test</li>
<li>適合：test repository adapter 本身</li>
</ul>
<h3 id="contract-test">Contract Test</h3>
<ul>
<li>驗證 adapter 對外語意穩定：同一輸入是否得到一致輸出、同一錯誤是否被穩定分類、同一查詢語意在 schema 演進後是否保持相容</li>
<li>測試重點是邊界語意覆蓋、資料庫產品特性覆蓋是另一件事</li>
<li>例：「unique 衝突必須回 <code>ErrAlreadyExists</code>」這條 contract、不管底層是 PostgreSQL / MySQL / SQLite 都成立</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">Contract 卡片</a> 跟 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing</a>。</p>
<h3 id="sqlite-作為-test-db">SQLite 作為 test DB</h3>
<ul>
<li>起 quick、無 external dependency</li>
<li>但 SQL dialect 跟 PostgreSQL / MySQL 有差異</li>
<li>適合：簡單 query 的 test、不適合 production-fidelity test</li>
<li>對應 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor page</a></li>
</ul>
<h2 id="transaction-傳遞">Transaction 傳遞</h2>
<p>repository 操作通常要支援「我自己起 transaction」跟「在已有 transaction 內操作」兩種模式。</p>
<p><strong>Pattern 1：repository 自己起 transaction</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">OrderRepo</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">BeginTx</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// ... 操作 ...</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>問題：跨多個 repository 時無法共用 transaction。</p>
<p><strong>Pattern 2：unit of work pattern</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">uow</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="nx">Transaction</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">orderRepo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">inventoryRepo</span><span class="p">.</span><span class="nf">Decrease</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Items</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">paymentRepo</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Payment</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>把 transaction 從 repository 抽到 unit-of-work、跨 repository 共用。</p>
<p><strong>Pattern 3：context-based transaction</strong>：</p>
<ul>
<li>把 transaction 塞進 context</li>
<li>repository 從 context 拿 transaction（有 → 用、沒有 → 自己起）</li>
<li>Go 常用 pattern、但有「context 不該裝這種東西」的爭議</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>簡單應用：pattern 1 夠用</li>
<li>跨 repository transaction：pattern 2 或 3</li>
<li>大型 application：pattern 2（最清楚）</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>。</p>
<h2 id="microservice-私有-store-對應">Microservice 私有 Store 對應</h2>
<p>現代 microservice 設計強調「每個 service 私有 DB」、不跟其他 service 共用。</p>
<p><strong>對 repository adapter 的影響</strong>：</p>
<ul>
<li>每個 service 自己的 schema、自己的 adapter</li>
<li>跨 service 不直接 DB query、要透過 API</li>
<li>transaction 不跨 service（用 Saga 或 outbox）</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a>、<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></li>
</ul>
<p><strong>反模式</strong>：</p>
<ul>
<li>共用 DB schema、不同 service 都 query 同一張表 → 強耦合、schema 改一個影響全部</li>
<li>跨 service 用 DB foreign key → 不能 enforce、會壞掉</li>
</ul>
<h2 id="repository-adapter-五個常見變體">Repository Adapter 五個常見變體</h2>
<p>實務上 repository 不止「CRUD」這個樣態：</p>
<ol>
<li><strong>Pure CRUD repository</strong>：Find / Save / Delete、最簡單</li>
<li><strong>Aggregate repository</strong>：操作 aggregate root、含 nested entities</li>
<li><strong>Read model repository</strong>（CQRS）：專門 read、不 write</li>
<li><strong>Event-sourced repository</strong>：存 events、不存 state</li>
<li><strong>Cached repository</strong>：包一層 cache（pass-through、refresh-ahead）</li>
</ol>
<p>實作時要明確選哪種、不要讓一個 repository 跨多種 pattern。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一業務錯誤在不同路徑返回不同型別</td>
          <td>error translation 分類漂移</td>
          <td>收斂錯誤分類介面與 mapping</td>
      </tr>
      <tr>
          <td>schema 變更後應用層出現大量 null 問題</td>
          <td>nullable handling 規則不足</td>
          <td>補顯式轉換與 fallback 規則</td>
      </tr>
      <tr>
          <td>SQL 細節在 service 層大量出現</td>
          <td>adapter 邊界被繞過</td>
          <td>收斂資料操作入口到 repository</td>
      </tr>
      <tr>
          <td>同一查詢在不同環境結果不一致</td>
          <td>contract test 覆蓋不足</td>
          <td>補跨環境合約測試與 fixture</td>
      </tr>
      <tr>
          <td>事故排查時難以判斷重試與回退條件</td>
          <td>錯誤分類無法對應決策</td>
          <td>建立錯誤分類到 gate/incident 的映射表</td>
      </tr>
      <tr>
          <td>N+1 query 在 ORM 環境下出現</td>
          <td>lazy loading 反模式</td>
          <td>改 eager loading 或換 query builder</td>
      </tr>
      <tr>
          <td>跨 repository 的 transaction 不一致</td>
          <td>transaction 沒共用機制</td>
          <td>引入 unit-of-work pattern</td>
      </tr>
      <tr>
          <td>Test 跑很慢、需要起 DB</td>
          <td>test 沒分層</td>
          <td>unit test 用 memory fake、integration 才用 DB</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 repository adapter 寫成「直接包 SQL 的工具函式」、容易讓業務規則與資料邏輯混雜。邊界失焦後、schema 演進與事故修復都會擴大影響面。</p>
<p>把資料庫錯誤原樣往上拋、也會讓上層決策不穩定。錯誤翻譯是可靠性控制面的必要前置。</p>
<p>把 ORM 當銀彈、忘了 SQL 還在背後。N+1 query、lazy loading 災難、複雜 aggregation 反而難寫 — 這些都是「過度信任 ORM 抽象」的後果。</p>
<p>把 memory fake 拿來 test repository 本身、不會抓到實際 DB bug。memory fake 是給 <em>呼叫者</em> test 用的、不是給 repository test 用的。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>repository / adapter 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a></td>
          <td>microservice 私有 store、每個 service 自己 repository</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></td>
          <td>微服務私有 DB、跨 service 不直接 DB query</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>TiDB → DynamoDB、repository adapter 是換 DB 的關鍵抽象</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>adapter 邊界可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 的資料一致性段落回寫。若事件中出現同一錯誤在不同路徑被不同方式處理、通常代表 adapter 的錯誤翻譯與契約分層不足。</p>
<p>這個案例主要支撐的是「錯誤分類與契約映射」判讀、不直接支撐 broker delivery 參數調整；若根因在 ack/retry 節奏、應回到 3.1/3.2。</p>
<p>回寫步驟是先盤點錯誤分類、再對齊重試與回退決策、最後把分類結果映射到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a> 的驗證欄位、讓發版前可先發現漂移。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design 與資料建模</a>。</li>
<li>與 1.3 的交接：交易錯誤與重試語意回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 1.12 的交接：cross-DB migration 時、repository 是 <em>關鍵抽象</em> — 詳見 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">大規模 DB 遷移實戰</a>。</li>
<li>與 6.10 的交接：跨服務契約一致性回到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Contract Testing 與 Schema 演進</a>。</li>
<li>與 8.19 的交接：資料層錯誤判斷與回退決策回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>跨 vendor adapter 深入：<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a>（document KV adapter 邊界）、<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">MongoDB schema design pattern</a>（document adapter 的 ODM 取捨）、<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>（multi-API adapter 取捨）</li>
</ul>
]]></content:encoded></item><item><title>CockroachDB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/</guid><description>&lt;p>CockroachDB 是分散式 SQL、PostgreSQL wire protocol 相容、跨 region 強一致。設計理念接近 Spanner（線性化、跨 region &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum&lt;/a>），但採 HLC + Raft 而非 TrueTime hardware，是 open source + 跨雲可用的全球 OLTP 選擇。&lt;/p>
&lt;h2 id="教學路線distributed-sql-與跨雲一致性">教學路線：Distributed SQL 與跨雲一致性&lt;/h2>
&lt;p>CockroachDB 服務頁的教學目標是把 PostgreSQL-like 介面背後的 range sharding、Raft replication、serializable transaction、leaseholder 與 region placement 說清楚。讀者讀完後要能判斷 distributed SQL 何時能取代自管 sharding，何時會把 latency 與 retry 壓力推回應用層。&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>Distributed SQL&lt;/td>
 &lt;td>SQL 介面如何藏住 range sharding 與 Raft replication&lt;/td>
 &lt;td>定位、容量特性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serializable default&lt;/td>
 &lt;td>transaction retry、contention、latency 如何影響應用設計&lt;/td>
 &lt;td>容量規劃要點、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Region placement&lt;/td>
 &lt;td>multi-region table、leaseholder、survival goal 如何服務產品需求&lt;/td>
 &lt;td>適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration pressure&lt;/td>
 &lt;td>從 PostgreSQL / MySQL 或自管 sharding 過來時要檢查哪些差異&lt;/td>
 &lt;td>預計實作話題、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時留 PostgreSQL、用 Spanner、Aurora DSQL 或 application sharding&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位spanner-的開源--跨雲替代">定位：Spanner 的開源 / 跨雲替代&lt;/h2>
&lt;p>CockroachDB 跟 Spanner 解決同一個問題（跨 region 強一致 SQL）、但定位不同：&lt;/p>
&lt;ul>
&lt;li>Spanner：GCP managed service、用 TrueTime hardware&lt;/li>
&lt;li>CockroachDB：開源（雙授權）、可自管 + Cockroach Cloud、跨 AWS / GCP / Azure / on-prem、用 HLC + Raft&lt;/li>
&lt;/ul>
&lt;p>選 CockroachDB 的核心訴求：需要跨 region 強一致 SQL + 想避免雲商 lock-in、想自管或跨雲部署。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> 的 CockroachDB 段。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>&lt;strong>節點即容量單位&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>跟 Spanner 同樣設計、節點數量決定容量&lt;/li>
&lt;li>每節點承擔 query + storage + replication&lt;/li>
&lt;li>線性擴展（理論）、實際依 query pattern&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>跨 region 配置&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>multi-region survival goal（zone-level / region-level）&lt;/li>
&lt;li>跨 region quorum 必要、決定 latency&lt;/li>
&lt;li>跟 Spanner 同樣的物理限制（跨洲 100ms+）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Replication&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Raft consensus per range&lt;/li>
&lt;li>預設 3-replica&lt;/li>
&lt;li>可配置每個 region 不同 replica count（Survival Goals）&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. 需要跨 region 強一致 SQL + 跨雲&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>CockroachDB 是分散式 SQL、PostgreSQL wire protocol 相容、跨 region 強一致。設計理念接近 Spanner（線性化、跨 region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>），但採 HLC + Raft 而非 TrueTime hardware，是 open source + 跨雲可用的全球 OLTP 選擇。</p>
<h2 id="教學路線distributed-sql-與跨雲一致性">教學路線：Distributed SQL 與跨雲一致性</h2>
<p>CockroachDB 服務頁的教學目標是把 PostgreSQL-like 介面背後的 range sharding、Raft replication、serializable transaction、leaseholder 與 region placement 說清楚。讀者讀完後要能判斷 distributed SQL 何時能取代自管 sharding，何時會把 latency 與 retry 壓力推回應用層。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Distributed SQL</td>
          <td>SQL 介面如何藏住 range sharding 與 Raft replication</td>
          <td>定位、容量特性</td>
      </tr>
      <tr>
          <td>Serializable default</td>
          <td>transaction retry、contention、latency 如何影響應用設計</td>
          <td>容量規劃要點、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level</a></td>
      </tr>
      <tr>
          <td>Region placement</td>
          <td>multi-region table、leaseholder、survival goal 如何服務產品需求</td>
          <td>適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>Migration pressure</td>
          <td>從 PostgreSQL / MySQL 或自管 sharding 過來時要檢查哪些差異</td>
          <td>預計實作話題、案例對照</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時留 PostgreSQL、用 Spanner、Aurora DSQL 或 application sharding</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位spanner-的開源--跨雲替代">定位：Spanner 的開源 / 跨雲替代</h2>
<p>CockroachDB 跟 Spanner 解決同一個問題（跨 region 強一致 SQL）、但定位不同：</p>
<ul>
<li>Spanner：GCP managed service、用 TrueTime hardware</li>
<li>CockroachDB：開源（雙授權）、可自管 + Cockroach Cloud、跨 AWS / GCP / Azure / on-prem、用 HLC + Raft</li>
</ul>
<p>選 CockroachDB 的核心訴求：需要跨 region 強一致 SQL + 想避免雲商 lock-in、想自管或跨雲部署。</p>
<p>詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 CockroachDB 段。</p>
<h2 id="容量特性">容量特性</h2>
<p><strong>節點即容量單位</strong>：</p>
<ul>
<li>跟 Spanner 同樣設計、節點數量決定容量</li>
<li>每節點承擔 query + storage + replication</li>
<li>線性擴展（理論）、實際依 query pattern</li>
</ul>
<p><strong>跨 region 配置</strong>：</p>
<ul>
<li>multi-region survival goal（zone-level / region-level）</li>
<li>跨 region quorum 必要、決定 latency</li>
<li>跟 Spanner 同樣的物理限制（跨洲 100ms+）</li>
</ul>
<p><strong>Replication</strong>：</p>
<ul>
<li>Raft consensus per range</li>
<li>預設 3-replica</li>
<li>可配置每個 region 不同 replica count（Survival Goals）</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. 需要跨 region 強一致 SQL + 跨雲</strong>：</p>
<ul>
<li>multi-region active-active write</li>
<li>GCP-only（Spanner）或 AWS-only（Aurora DSQL）和部署策略不合</li>
<li>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的選型決策</li>
</ul>
<p><strong>2. PostgreSQL wire protocol 相容路徑</strong>：</p>
<ul>
<li>既有 PostgreSQL 應用想升級到分散式</li>
<li>應用層改動小（保留 PostgreSQL driver / ORM）</li>
<li>注意：PostgreSQL 相容要以實際 query、extension 與 migration test 驗證</li>
</ul>
<p><strong>3. 自管 on-prem / hybrid</strong>：</p>
<ul>
<li>金融 / 受監管產業需要 on-prem</li>
<li>Spanner / Aurora DSQL 以 cloud service 為主</li>
<li>CockroachDB 可自管</li>
</ul>
<p><strong>4. 想避免單一 vendor 全球分散式 lock-in</strong>：</p>
<ul>
<li>開源 + 跨雲、可遷移性高</li>
<li>但企業版功能要付費（CockroachDB Cloud 或 Enterprise license）</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. single-region OLTP 夠用</strong>：</p>
<ul>
<li>90% 場景 PostgreSQL / Aurora 已夠</li>
<li>CockroachDB 有分散式 overhead（每個寫經 Raft）</li>
<li>替代：PostgreSQL、Aurora、MySQL</li>
</ul>
<p><strong>2. 極端高吞吐 single-query</strong>：</p>
<ul>
<li>CockroachDB 寫入有 Raft 開銷、單機吞吐 &lt; PostgreSQL</li>
<li>整體吞吐靠 scale-out 達成、單一 query latency 較高</li>
</ul>
<p><strong>3. 跨洲低延遲（&lt; 50ms）</strong>：</p>
<ul>
<li>跟 Spanner 同樣物理限制</li>
<li>跨洲 quorum 100ms+ 是物理成本</li>
</ul>
<p><strong>4. 預算極敏感的小 workload</strong>：</p>
<ul>
<li>CockroachDB 至少 3 個節點（Raft quorum）</li>
<li>跟 single-instance PostgreSQL 比較貴</li>
</ul>
<p><strong>5. 需要 PostgreSQL 進階特性</strong>：</p>
<ul>
<li>部分 PostgreSQL extension 或行為需要替代方案</li>
<li>partial index、exclusion constraint 等可能缺</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs Spanner（GCP）</strong>：</p>
<ul>
<li>CockroachDB：開源、跨雲、可自管</li>
<li>Spanner：GCP-only、TrueTime hardware、Google 規模驗證</li>
<li>選 CockroachDB：跨雲 / on-prem 需求</li>
<li>選 Spanner：GCP 生態 + managed operation + Google 規模驗證的成熟度</li>
</ul>
<p><strong>vs Aurora DSQL（AWS 2024）</strong>：</p>
<ul>
<li>CockroachDB：跨雲、生產驗證較久</li>
<li>Aurora DSQL：AWS-only、serverless、新（2024）</li>
<li>選 CockroachDB：跨雲、想避免 AWS lock-in</li>
<li>選 Aurora DSQL：AWS 生態 + 已用 PostgreSQL + serverless 訴求</li>
</ul>
<p><strong>vs TiDB</strong>：</p>
<ul>
<li>CockroachDB：PostgreSQL wire、英語 / 歐美生態深</li>
<li>TiDB：MySQL wire、亞洲生態深、HTAP（OLTP + OLAP 同庫）</li>
<li>選 CockroachDB：PostgreSQL 應用、跨雲</li>
<li>選 TiDB：MySQL 應用、需要 OLAP 整合、亞洲市場</li>
</ul>
<p><strong>vs PostgreSQL（傳統）</strong>：</p>
<ul>
<li>CockroachDB：分散式、跨 region 強一致</li>
<li>PostgreSQL：single-primary、跨 region 是 async replication</li>
<li>選 CockroachDB：需要跨 region 強一致</li>
<li>選 PostgreSQL：single-region 夠用（90% 場景）</li>
</ul>
<p><strong>vs Aurora（single-region scaling）</strong>：</p>
<ul>
<li>CockroachDB：multi-region 強一致</li>
<li>Aurora：single-region scaling、跨 region 是 async Global Database</li>
<li>選 CockroachDB：需要 multi-region write</li>
<li>選 Aurora：single-region scaling + AWS 生態</li>
</ul>
<p><strong>vs MySQL + Vitess（self-managed distributed MySQL）</strong>：</p>
<ul>
<li>CockroachDB：PostgreSQL wire、transparent sharding（range-based）、跨 region 強一致內建</li>
<li>MySQL + Vitess：MySQL wire、application 層配 keyspace + shard key、跨 region 靠 application + async replication</li>
<li>選 CockroachDB：PostgreSQL 應用 + transparent multi-region + 想避開 Vitess operation burden</li>
<li>選 MySQL + Vitess：MySQL 應用 + 有 DBA 養 Vitess + 已是 YouTube / Slack 規模</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. Node count + zone / region 配置</strong>：</p>
<ul>
<li>至少 3 個節點（Raft quorum）</li>
<li>multi-region 通常 9+ 節點（3 region × 3 replica）</li>
<li>Survival Goals 配置決定每 region 復原能力</li>
</ul>
<p><strong>2. Range（CockroachDB 的 partition）</strong>：</p>
<ul>
<li>跟 DynamoDB partition、Spanner split 同類</li>
<li>CockroachDB 自動 split 大 range</li>
<li>application 主要管理 query locality、transaction retry 與 region placement</li>
</ul>
<p><strong>3. Locality 配置</strong>：</p>
<ul>
<li>跟 Spanner 一樣可以指定 voting region</li>
<li>寫入 locality 影響跨 region latency</li>
</ul>
<p><strong>4. Backup / restore</strong>：</p>
<ul>
<li>CockroachDB 原生 backup 支援 cluster-level snapshot</li>
<li>增量 backup 支援</li>
<li>注意：incremental backup chain 可能很長、定期 full backup</li>
</ul>
<p><strong>5. Self-managed vs Cockroach Cloud</strong>：</p>
<ul>
<li>Self-managed：需要 ops team、可跨雲 / on-prem</li>
<li>Cockroach Cloud：managed、跨 cloud（AWS / GCP / Azure）、可考慮 serverless tier</li>
</ul>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本批 deep article 覆蓋 CockroachDB 從 consensus 機制、multi-region 配置到 managed 形態選型的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HLC + per-range Raft、leaseholder、寫入 latency 結構</td>
          <td><a href="hlc-raft-consensus/">hlc-raft-consensus</a></td>
          <td>DoorDash Aurora 撞牆訊號（1.636 M QPS）、Netflix 380+ artery of small DBs 容量規劃顆粒</td>
      </tr>
      <tr>
          <td>SURVIVE ZONE / REGION FAILURE 倒推、業務 SLO 決定副本拓樸</td>
          <td><a href="survival-goals/">survival-goals</a></td>
          <td>Hard Rock RPO=0 倒推、Netflix Gaming 48-node 跨 4 region「為求 survival 而非 latency」反直覺</td>
      </tr>
      <tr>
          <td>Serializable default、application 必須包 retry loop、SAVEPOINT 語法</td>
          <td><a href="transaction-retry-pattern/">transaction-retry-pattern</a></td>
          <td>PG → CockroachDB application contract 重塑、5 種 retry failure mode（跨 case 合成 frame）</td>
      </tr>
      <tr>
          <td>REGIONAL BY ROW / TABLE / GLOBAL、跨州合規 + 邏輯一個 cluster</td>
          <td><a href="locality-aware-schema/">locality-aware-schema</a></td>
          <td>Hard Rock 跨 8 州 sportsbook + AWS Outposts、Outposts 是合規工具不是 latency 工具反直覺判讀</td>
      </tr>
      <tr>
          <td>三種 table locality 的選擇與 latency / 一致性取捨、選錯重配代價</td>
          <td><a href="multi-region-table-config/">multi-region-table-config</a></td>
          <td>Netflix multi-region 動機為 survival 非 latency、Hard Rock row-level 歸屬 + 單一邏輯 cluster</td>
      </tr>
      <tr>
          <td>Cockroach Cloud serverless vs dedicated、RU 計費、冷啟動 / scale</td>
          <td><a href="cloud-serverless/">cloud-serverless</a></td>
          <td>Netflix 需 Platform Team 反向 = managed 入口、Hard Rock 可預測賽季擴縮 vs serverless 突發甜蜜區</td>
      </tr>
      <tr>
          <td>Distributed SQL 三選一決策樹：撞牆訊號分型 + 七問題</td>
          <td><a href="aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a></td>
          <td>DB4 cross-vendor entry：DoorDash / Netflix / Hard Rock driver path 識別 + sizing barrier</td>
      </tr>
  </tbody>
</table>
<p>DB4 cross-vendor entry：先看 <a href="aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a> 識別 driver path、再進個別 vendor 深度。</p>
<p>multi-region-table-config 與 locality-aware-schema 切分：前者主寫「三種 table locality 怎麼選 + 選錯重配代價」、後者主寫「schema 怎麼配合 locality 設計（合規 boundary、跨州業務邏輯、Outposts 拓樸）」、兩者互補、survival goal 機制以 survival-goals 為 SSoT。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>PostgreSQL 相容性 audit（partial index / extension / SQL 行為 gap 清單）</li>
<li>Backup / restore 與 PITR 操作（incremental chain 管理、restore 演練）</li>
<li>Changefeed / CDC 配置（CockroachDB 原生 CDC 到 Kafka / sink）</li>
</ul>
<blockquote>
<p>「從 PostgreSQL 遷到 CockroachDB（playbook）」已由 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB migration</a> 涵蓋、不再列為待補。</p></blockquote>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>CockroachDB 的 PostgreSQL-like 介面會降低導入門檻，但 distributed SQL 的成本會出現在 transaction retry、range lease、multi-region latency 與操作拓樸。這一段先說何時維持 PostgreSQL / Aurora，再說何時升級 CockroachDB、Cockroach Cloud、Spanner、Aurora DSQL 或 Vitess。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PostgreSQL / Aurora</td>
          <td>single-region primary、async DR、read replica 已滿足需求</td>
          <td>multi-region write、region failure survival、跨雲部署是硬需求</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</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 vendor</a></td>
      </tr>
      <tr>
          <td>CockroachDB single-region</td>
          <td>需要水平擴容或 future multi-region，但目前在單區運作</td>
          <td>Raft overhead 讓成本高於 PostgreSQL，且沒有 region requirement</td>
          <td><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">Distributed SQL</a></td>
      </tr>
      <tr>
          <td>CockroachDB multi-region</td>
          <td>跨雲 / on-prem、PostgreSQL wire、strong consistency 是主需求</td>
          <td>跨洲 p99 目標過低、transaction retry 影響 user flow</td>
          <td><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">Quorum</a>、<a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></td>
      </tr>
      <tr>
          <td>Cockroach Cloud</td>
          <td>團隊仍能自管 Raft、backup、upgrade、node failure</td>
          <td>想把 operation transfer 給 vendor</td>
          <td><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>跨雲或自管是硬需求</td>
          <td>GCP managed、TrueTime 成熟度、Google scale evidence 是主訴求</td>
          <td><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a></td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>跨雲 / on-prem 是硬需求</td>
          <td>AWS-only、serverless、PostgreSQL 相容與 AWS operation model 是主訴求</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL Migration</a></td>
      </tr>
      <tr>
          <td>MySQL + Vitess</td>
          <td>PostgreSQL-like SQL 與 strong consistency 是主需求</td>
          <td>MySQL ecosystem、application sharding 與 Vitess ops 已成熟</td>
          <td><a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess Sharding</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a></td>
      </tr>
  </tbody>
</table>
<p>CockroachDB 的簡單路徑是先證明 distributed SQL 的價值大於 retry 與 latency 成本。若 workload 仍是 single-region OLTP，PostgreSQL / Aurora 通常提供更低成本；若跨 region 寫入與一致性是產品承諾，CockroachDB 才成為主要候選。</p>
<p>Transaction retry 的升級路徑要進入 application contract。Serializable default 能保護一致性，但 retry 會把 idempotency、timeout、user-visible latency 與 workflow compensation 帶回應用層；這些條件要在 migration playbook 前先盤點。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>CockroachDB overview 目前完成 distributed SQL 判斷。下一輪 deep article / playbook 應補 HLC + Raft、range / leaseholder、multi-region table locality、transaction retry pattern、PostgreSQL compatibility audit、Cockroach Cloud operation 與 PostgreSQL → CockroachDB migration。</p>
<h2 id="案例對照">案例對照</h2>
<p>CockroachDB 在 09 案例庫已有三條直接 case 軸線（OLTP 寫入擴展、polyglot 補位、合規邊界），另外兩條對比參考軸線（Spanner 設計理念、受監管金融）一併保留。</p>
<h3 id="direct-casecockroachdb-為主角">Direct case（CockroachDB 為主角）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主要工程議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a></td>
          <td>Aurora Postgres single-primary 1.6 M QPS 撞牆 → multi-primary 解寫入</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a></td>
          <td>380+ cluster 艦隊、Cassandra 不夠用的 transactional workload 補位</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a></td>
          <td>AWS Outposts + 跨州單一邏輯 DB、Wire Act 合規 + 賽季型擴縮容</td>
      </tr>
  </tbody>
</table>
<h3 id="對比參考案例">對比參考案例</h3>
<table>
  <thead>
      <tr>
          <th>案例（對比參考）</th>
          <th>跟 CockroachDB 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>設計理念對標、CockroachDB 是開源版本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>受監管金融、CockroachDB 可作為 on-prem 替代候選</td>
      </tr>
  </tbody>
</table>
<p>CockroachDB direct case 的讀法是「寫入擴展（DoorDash）→ polyglot 補位（Netflix）→ 合規邊界（Hard Rock Digital）」三條軸線；對比案例則提醒讀者：Spanner 提供 global consistency 的成熟對照，受監管金融類案例提醒部署位置、合規邊界與自管能力常和一致性需求同時決定 vendor。</p>
<h2 id="反向-sibling-路由">反向 sibling 路由</h2>
<p>CockroachDB 的反向 sibling 路由用來把 PostgreSQL 相容性和 distributed SQL 責任拆開。若讀者從 PostgreSQL 章節過來，先讀 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB migration</a>；若只是要 managed SQL 與 storage autoscale，先回 <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>；若要 Google Cloud 原生 external consistency 與 fully managed control plane，再對照 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a>。</p>
<p>這條路由的判準是「應用是否能承擔 distributed transaction 的語意差異」。SQL dialect 相近只降低 migration entry cost，真正的交付風險在 transaction retry、hot range、survival goal、backup restore 與 locality design。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>single-region 用 CockroachDB</strong>：浪費分散式開銷、PostgreSQL 便宜很多</li>
<li><strong>跨洲 active-active 期待低延遲</strong>：物理限制、跨洲 quorum 100ms+</li>
<li><strong>PostgreSQL extension 假設</strong>：部分 extension 或 SQL 行為需要替代方案，應用要驗證</li>
<li><strong>不規劃 Survival Goals</strong>：default 配置可能不符合 RTO / RPO 需求</li>
<li><strong>backup chain 過長</strong>：incremental 不 full、recovery time 變長</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</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 vendor</a>、<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></li>
<li>上游：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> — 完整選型對比</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
<li>Last reviewed：2026-05-22（PostgreSQL compatibility / survival goal / managed offering 屬時間敏感 claim）</li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/">CockroachDB Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.4 Saturation Discovery</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Saturation discovery 的責任是把「系統能撐多少」這個問題變成可量化答案。沒有 saturation 量測時、容量規劃只能猜；有 saturation 量測之後、能說「在當前配置下、p99 &amp;lt; 100ms 的條件下、能撐 X RPS、headroom Y%」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&amp;#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論&lt;/a> 的關係：9.1 預測 saturation curve 的形狀（linear → knee → cliff）、9.4 用實測找出 &lt;em>本服務&lt;/em> 的曲線具體位置。理論告訴我們 knee 存在、實測告訴我們它在哪裡。&lt;/p>
&lt;p>本章不深入工具操作（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3&lt;/a> 處理工具）、聚焦在 &lt;em>方法論&lt;/em> — 怎麼設計 ramp-up、怎麼判斷 knee、怎麼把結果文件化讓後續決策可用。&lt;/p>
&lt;h2 id="saturation-的精確定義">Saturation 的精確定義&lt;/h2>
&lt;p>容量規劃裡 saturation 不是「系統當機」、是「系統 &lt;em>進入 latency 指數成長區&lt;/em>」。這個區分很重要 — 系統 &lt;em>看起來&lt;/em> 還在跑、其實已經不可預測。&lt;/p>
&lt;p>技術上 saturation 對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">queueing theory 的 knee point&lt;/a>：utilization 超過某個臨界（M/M/c 通常 70-80%）、平均 queue length 從線性轉成指數成長。latency 是 queue length 的線性函數、所以也跟著指數成長。&lt;/p>
&lt;p>實務上把 saturation 分三段：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>linear region&lt;/strong>（utilization &amp;lt; 50%）：latency 平穩、加流量幾乎不影響&lt;/li>
&lt;li>&lt;strong>knee region&lt;/strong>（utilization 50-80%）：latency 開始上升、但還可接受&lt;/li>
&lt;li>&lt;strong>cliff region&lt;/strong>（utilization &amp;gt; 80%）：latency 不可預測、可能 timeout / cascade failure&lt;/li>
&lt;/ul>
&lt;p>健康系統運轉在 linear 後半段或 knee 前段（utilization 50-70%）、留出 headroom 應付 burst。autoscaler 的 target metric 通常訂在 60-70%、是這條曲線推導出的安全位置。&lt;/p>
&lt;h2 id="ramp-up-測試方法">Ramp-up 測試方法&lt;/h2>
&lt;p>要找出 saturation 點、必須跑 &lt;em>ramp-up 測試&lt;/em> — 不能固定一個壓力值。&lt;/p>
&lt;p>&lt;strong>單點壓測的問題&lt;/strong>：跑「2000 RPS 連續 10 分鐘」、看 latency 100ms、結論「能撐 2000 RPS」 — 但不知道 1500 跟 2500 RPS 是什麼樣。可能 1500 也是 100ms（離 knee 還很遠）、可能 2500 直接崩（已經在 cliff）。&lt;/p>
&lt;p>&lt;strong>Ramp-up 流程&lt;/strong>：從基線開始、按倍數加壓（1x / 2x / 4x / 8x &amp;hellip;）。每個壓力 level 維持 5-10 分鐘、觀察 latency / throughput / resource utilization 的穩態（不是 transient）。紀錄每個 level 的 percentile 分布。&lt;/p>
&lt;p>&lt;strong>Knee 出現的訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>throughput 從線性成長轉成 sub-linear（加壓但 throughput 不再等比成長）&lt;/li>
&lt;li>latency p50 還算穩、但 p99 / p999 開始飆&lt;/li>
&lt;li>resource saturation queue 開始堆積（不只 utilization 上升）&lt;/li>
&lt;li>error rate 仍接近 0（cliff 才會 error 飆）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Cliff 出現的訊號&lt;/strong>：throughput 開始下降（加壓反而越來越慢）、latency p99 變成 timeout、error rate 飆升、retry storm 出現。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Saturation discovery 的責任是把「系統能撐多少」這個問題變成可量化答案。沒有 saturation 量測時、容量規劃只能猜；有 saturation 量測之後、能說「在當前配置下、p99 &lt; 100ms 的條件下、能撐 X RPS、headroom Y%」。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> 的關係：9.1 預測 saturation curve 的形狀（linear → knee → cliff）、9.4 用實測找出 <em>本服務</em> 的曲線具體位置。理論告訴我們 knee 存在、實測告訴我們它在哪裡。</p>
<p>本章不深入工具操作（<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3</a> 處理工具）、聚焦在 <em>方法論</em> — 怎麼設計 ramp-up、怎麼判斷 knee、怎麼把結果文件化讓後續決策可用。</p>
<h2 id="saturation-的精確定義">Saturation 的精確定義</h2>
<p>容量規劃裡 saturation 不是「系統當機」、是「系統 <em>進入 latency 指數成長區</em>」。這個區分很重要 — 系統 <em>看起來</em> 還在跑、其實已經不可預測。</p>
<p>技術上 saturation 對應 <a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">queueing theory 的 knee point</a>：utilization 超過某個臨界（M/M/c 通常 70-80%）、平均 queue length 從線性轉成指數成長。latency 是 queue length 的線性函數、所以也跟著指數成長。</p>
<p>實務上把 saturation 分三段：</p>
<ul>
<li><strong>linear region</strong>（utilization &lt; 50%）：latency 平穩、加流量幾乎不影響</li>
<li><strong>knee region</strong>（utilization 50-80%）：latency 開始上升、但還可接受</li>
<li><strong>cliff region</strong>（utilization &gt; 80%）：latency 不可預測、可能 timeout / cascade failure</li>
</ul>
<p>健康系統運轉在 linear 後半段或 knee 前段（utilization 50-70%）、留出 headroom 應付 burst。autoscaler 的 target metric 通常訂在 60-70%、是這條曲線推導出的安全位置。</p>
<h2 id="ramp-up-測試方法">Ramp-up 測試方法</h2>
<p>要找出 saturation 點、必須跑 <em>ramp-up 測試</em> — 不能固定一個壓力值。</p>
<p><strong>單點壓測的問題</strong>：跑「2000 RPS 連續 10 分鐘」、看 latency 100ms、結論「能撐 2000 RPS」 — 但不知道 1500 跟 2500 RPS 是什麼樣。可能 1500 也是 100ms（離 knee 還很遠）、可能 2500 直接崩（已經在 cliff）。</p>
<p><strong>Ramp-up 流程</strong>：從基線開始、按倍數加壓（1x / 2x / 4x / 8x &hellip;）。每個壓力 level 維持 5-10 分鐘、觀察 latency / throughput / resource utilization 的穩態（不是 transient）。紀錄每個 level 的 percentile 分布。</p>
<p><strong>Knee 出現的訊號</strong>：</p>
<ul>
<li>throughput 從線性成長轉成 sub-linear（加壓但 throughput 不再等比成長）</li>
<li>latency p50 還算穩、但 p99 / p999 開始飆</li>
<li>resource saturation queue 開始堆積（不只 utilization 上升）</li>
<li>error rate 仍接近 0（cliff 才會 error 飆）</li>
</ul>
<p><strong>Cliff 出現的訊號</strong>：throughput 開始下降（加壓反而越來越慢）、latency p99 變成 timeout、error rate 飆升、retry storm 出現。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 用 10K t2.micro 壓測</a> 找 DynamoDB 從 20 IOPS 到 135K 的擴展曲線、知道 knee 在哪。</p>
<h2 id="resource-saturation-的六個維度">Resource saturation 的六個維度</h2>
<p>每次 ramp-up 都要同時觀察六個維度的 resource saturation、找出哪個 <em>先 saturate</em>。</p>
<p><strong>CPU</strong>：utilization 100% <em>不一定</em> 等於 saturation。要看 load average 跟 run queue。utilization 80% 但 run queue 不斷增長 → 已 saturate；utilization 100% 但 run queue 空 → 還能撐（單純 CPU bound）。</p>
<p><strong>Memory</strong>：not OOM 即可？不夠。GC pause（Java、Go）、swap（Linux）、cache eviction 都是隱性 saturation。記憶體不直接 OOM 但 GC 飆 → 已影響 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">tail latency</a>。</p>
<p><strong>Disk I/O</strong>：要看三個維度：throughput（MB/s）、IOPS（operations/sec）、queue depth。雲端 SSD 通常先 IOPS bound、不是 throughput；本機 NVMe 可能先 throughput bound。</p>
<p><strong>Network</strong>：bandwidth（Gbps）、packets per second、connection count。雲端 instance 通常有 PPS limit、超過會 silent drop、不是顯式錯誤。</p>
<p><strong>Connection pool</strong>：DB / cache / external API 的連線數。這是 <em>最常見的隱性 bottleneck</em>。pool size 訂 100、實際在用 95 → utilization 看似還好、其實已經 saturate（剩下的 request 在等 connection）。</p>
<p><strong>External API quota</strong>：第三方 rate limit（Stripe、Twilio、Slack API）。這個維度的 saturation 看不到 <em>本系統</em> 的訊號、要看 <em>對方 API 的 429 error rate</em>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino RDB connection limit</a> — connection 是 RDB 的 saturation 點、CPU 跟 RAM 都還沒到。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片</a>。</p>
<h2 id="hot-partition-的隱性-saturation">Hot partition 的隱性 saturation</h2>
<p>對分散式 KV / OLTP（DynamoDB、Cosmos DB、Bigtable、Cassandra）、saturation 還有另一個維度：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>。</p>
<p>名義容量 = 每 partition 上限 × partition 數量。partition key 分布不均 → 名義容量達不到。整體 utilization 看起來 20% → 系統還能撐？不一定。最熱 partition 已經 100%、其他 partition 0%、整體平均才 20%、但加流量會打在最熱 partition、立即 throttle。</p>
<p><strong>識別 hot partition 的訊號</strong>：</p>
<ul>
<li>throughput 上不去、但 average resource utilization 低</li>
<li>某些 key 的 request latency 飆、其他 key 正常</li>
<li>DynamoDB throttling event 出現（即使 capacity 還沒滿）</li>
</ul>
<p><strong>處理方法</strong>：</p>
<ul>
<li>composite key（event_id + user_id_hash）</li>
<li>write sharding（event_id + random_suffix）</li>
<li>time-bucket（event_id + minute）</li>
<li>用 cache 吸收 hot key（DAX、ElastiCache）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9000 萬 RPS</a> — partition 設計均勻時可以撐 sustained 高吞吐；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 售票</a> — 同一場演唱會（event_id）天然容易 hot、必須用 composite key 分散。</p>
<h2 id="long-tail-latency-的-saturation">Long-tail latency 的 saturation</h2>
<p>p50 / p95 / p99 / p999 在 saturate 時表現可能完全不同。</p>
<p>p50（中位數）對 GC pause、retry storm、tail latency 不敏感 — 大部分 request 沒事、p50 看不到。
p99（百分之 1）對 connection contention 開始敏感、能早期看到 saturation。
p999（千分之 1）對 GC stop-the-world、leader election、retry storm 敏感、是長尾的最強訊號。</p>
<p>純看 average / p50 會誤判 saturation 還沒到。SLO 通常訂 p99（讓 99% 用戶體驗良好）、internal critical 系統可訂 p99.9（5 個 9 的可用性對應 5 個 9 的 latency 期待）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML 系統的 user-perceived latency 是 <em>最後完成的 inference</em>、p50 快沒用；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — RAFT 系統的 p999 通常比 p99 高一個量級。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency 卡片</a>。</p>
<h2 id="saturation-文件化容量地圖">Saturation 文件化：容量地圖</h2>
<p>Saturation discovery 跑完之後、產出 <em>容量地圖</em> — 不是一個數字、是一張表。</p>
<p>容量地圖至少要回答：</p>
<ul>
<li>在 X 配置下（instance count、type、network）</li>
<li>SLO 條件 Y 下（p99 &lt; N ms、error rate &lt; M%）</li>
<li>能撐 Z RPS（含分解到不同 endpoint）</li>
<li>knee 在哪（什麼條件下進入 cliff）</li>
<li>第一個 saturate 的 resource 是什麼</li>
</ul>
<p>紀錄 <em>測試時間</em> 跟 <em>軟硬體版本</em>：硬體 / 軟體版本變動後、saturation 點可能位移、舊地圖不能套用。</p>
<p>加入 release gate：每次重大改動後 re-test、確認 knee 沒往不好的方向移。這層自動化跟 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 對接。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>DynamoDB IOPS 20 → 135K 的擴展曲線量測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>partition 均勻時的線性擴展</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>connection limit 是 RDB 的 saturation 點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>p99 &lt; 10ms saturation 條件比平均嚴格</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> / <a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>（找到 knee 之後、定位是哪個 resource）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>（用 knee 算 headroom）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（量測訊號）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
</ul>
]]></content:encoded></item><item><title>9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/</guid><description>&lt;p>這個案例的核心責任是說明「transactional 金融系統」如何在不可預期峰值下維持低延遲。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &amp;#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech&lt;/a> 對比 — GR8 Tech 走「微服務 + AI 預測擴容」、DraftKings 走「Aurora 單一資料庫服務支撐多 DB cluster」、兩條路徑都解決同類業務問題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>DraftKings 帳本系統的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/draftkings-aurora-case-study/">DraftKings case study&lt;/a>）：&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>客戶數&lt;/td>
 &lt;td>310 萬 unique customers / month (Q2 2024)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>峰值操作&lt;/td>
 &lt;td>100 萬 ops / 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀延遲&lt;/td>
 &lt;td>&amp;lt; 1 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫延遲&lt;/td>
 &lt;td>6 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication lag&lt;/td>
 &lt;td>從 30 秒降到 10-30 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database 數量&lt;/td>
 &lt;td>200 個 individual databases&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Super Bowl 流量&lt;/td>
 &lt;td>比賽季開幕高 +50%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Amazon Aurora MySQL-Compatible、Aurora Replicas（讀寫分流）、Aurora I/O-Optimized（2023-05 推出）、Aurora Database Cloning（測試環境）、跨三個 AZ 儲存複製。&lt;/p>
&lt;p>關鍵負載形狀：「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時是讀爆量、payout event 時是寫爆量、雙峰錯位。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>DraftKings 的工程選擇揭露三個 OLTP 容量設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>200 個獨立資料庫 = sharding 預先做好&lt;/strong>：按業務切 200 個 cluster、用巨型 cluster 撐全部在這個規模行不通。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 把「單機極限」改成「shard 極限」、每個 shard 的容量規劃變成獨立問題。&lt;/li>
&lt;li>&lt;strong>Replication lag 30 秒 → 10-30 ms&lt;/strong>：這個改善不只是「快」、而是讓 read-after-write 變得可預測。Aurora 的 storage layer 多 AZ 複製是這個 lag 改善的主因。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 replication lag 影響 transaction boundary 設計。&lt;/li>
&lt;li>&lt;strong>Super Bowl +50% 「no sweat」&lt;/strong>：這句話的工程意義是 &lt;em>提前做好容量規劃&lt;/em>、不是「Aurora 神奇」。寫 workload 預期可能 + 50%、整個 system headroom 預留至少 50%、加上 read replica 動態加減、才能讓 50% 增幅變成「不流汗」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的 headroom budget 與 event-driven scheduled scaling。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：100 萬 ops / 分鐘 = ~17K ops / 秒、跨 200 個 databases 平均下來每個 DB 約 80 ops / 秒。這不是「單一 DB 撐 100 萬 ops」、而是「200 shard 加總 100 萬」。讀案例時要看「峰值是分散到多少 shard」、不只看總數。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「transactional 金融系統」如何在不可預期峰值下維持低延遲。跟 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> 對比 — GR8 Tech 走「微服務 + AI 預測擴容」、DraftKings 走「Aurora 單一資料庫服務支撐多 DB cluster」、兩條路徑都解決同類業務問題。</p>
<h2 id="觀察">觀察</h2>
<p>DraftKings 帳本系統的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/draftkings-aurora-case-study/">DraftKings case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶數</td>
          <td>310 萬 unique customers / month (Q2 2024)</td>
      </tr>
      <tr>
          <td>峰值操作</td>
          <td>100 萬 ops / 分鐘</td>
      </tr>
      <tr>
          <td>讀延遲</td>
          <td>&lt; 1 ms</td>
      </tr>
      <tr>
          <td>寫延遲</td>
          <td>6 ms</td>
      </tr>
      <tr>
          <td>Replication lag</td>
          <td>從 30 秒降到 10-30 ms</td>
      </tr>
      <tr>
          <td>Database 數量</td>
          <td>200 個 individual databases</td>
      </tr>
      <tr>
          <td>Super Bowl 流量</td>
          <td>比賽季開幕高 +50%</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Amazon Aurora MySQL-Compatible、Aurora Replicas（讀寫分流）、Aurora I/O-Optimized（2023-05 推出）、Aurora Database Cloning（測試環境）、跨三個 AZ 儲存複製。</p>
<p>關鍵負載形狀：「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時是讀爆量、payout event 時是寫爆量、雙峰錯位。</p>
<h2 id="判讀">判讀</h2>
<p>DraftKings 的工程選擇揭露三個 OLTP 容量設計重點。</p>
<ol>
<li><strong>200 個獨立資料庫 = sharding 預先做好</strong>：按業務切 200 個 cluster、用巨型 cluster 撐全部在這個規模行不通。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 把「單機極限」改成「shard 極限」、每個 shard 的容量規劃變成獨立問題。</li>
<li><strong>Replication lag 30 秒 → 10-30 ms</strong>：這個改善不只是「快」、而是讓 read-after-write 變得可預測。Aurora 的 storage layer 多 AZ 複製是這個 lag 改善的主因。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 replication lag 影響 transaction boundary 設計。</li>
<li><strong>Super Bowl +50% 「no sweat」</strong>：這句話的工程意義是 <em>提前做好容量規劃</em>、不是「Aurora 神奇」。寫 workload 預期可能 + 50%、整個 system headroom 預留至少 50%、加上 read replica 動態加減、才能讓 50% 增幅變成「不流汗」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的 headroom budget 與 event-driven scheduled scaling。</li>
</ol>
<p>需要警惕：100 萬 ops / 分鐘 = ~17K ops / 秒、跨 200 個 databases 平均下來每個 DB 約 80 ops / 秒。這不是「單一 DB 撐 100 萬 ops」、而是「200 shard 加總 100 萬」。讀案例時要看「峰值是分散到多少 shard」、不只看總數。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>按業務切 OLTP cluster、不要一個 DB 撐全部</strong>：DraftKings 200 個 databases 顯示「業務切片」是 OLTP 擴容的前置。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema design 與 partition 決策。</li>
<li><strong>讀寫分流是 OLTP 容量規劃的基線</strong>：6ms 寫 vs &lt;1ms 讀的差距、加上 read replica、是 OLTP 擴容最基本的兩個槓桿。</li>
<li><strong>事件型峰值預測寫進 baseline</strong>：Super Bowl 是已知事件、+50% 是歷史經驗、所以可以提前 pre-scale。事件未知（突發新聞、KOL 推廣）的情況才需要 AI 預測（對照 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a>）。</li>
</ol>
<p>跨平台等效：GCP Cloud SQL + read replica / Spanner、Azure Database for PostgreSQL + read replica、自建 PostgreSQL + Patroni + pgbouncer 都可以實作對等架構。Aurora 的差異是 storage layer 對 replica 的 lag 改善。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 OLTP 高峰容量 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想搞清楚事件型 vs 突發型峰值 → <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> 對照</li>
<li>想做 read replica 容量設計 → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
<li>想理解 replication lag 對 transaction boundary 的影響 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a></li>
<li>想理解 6 寫 / 4 讀 quorum 跟 200 cluster fleet 治理 → <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">Aurora 儲存層架構</a></li>
<li>想規劃 read replica scaling 與 reader endpoint 路由 → <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 / 合規）">Aurora read replica scaling</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/draftkings-aurora-case-study/">DraftKings Scales Its Financial Ledger with Amazon Aurora</a></li>
<li><a href="https://aws.amazon.com/blogs/database/amazon-aurora-i-o-optimized-database-storage-configuration/">Aurora I/O-Optimized announcement</a></li>
</ul>
]]></content:encoded></item><item><title>2.C4 Meta：CacheLib / Kangaroo 分層快取</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/</guid><description>&lt;p>這個案例的核心責任是說明快取容量壓力升高後，策略會從單層記憶體轉向分層管理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta 透過 CacheLib 與 Kangaroo 把快取結構擴展到記憶體與快閃分層，改善容量與成本平衡。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當熱門資料集合超過 DRAM 經濟範圍時，單層快取會同時遇到成本與命中率瓶頸。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>定義不同資料熱度的落層策略。&lt;/li>
&lt;li>把 eviction 與回補延遲納入共同指標。&lt;/li>
&lt;li>驗證分層後 tail latency 與成本曲線。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity/cost&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2021/04/09/core-data/cachelib/">CacheLib and Kangaroo&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取容量壓力升高後，策略會從單層記憶體轉向分層管理。</p>
<h2 id="觀察">觀察</h2>
<p>Meta 透過 CacheLib 與 Kangaroo 把快取結構擴展到記憶體與快閃分層，改善容量與成本平衡。</p>
<h2 id="判讀">判讀</h2>
<p>當熱門資料集合超過 DRAM 經濟範圍時，單層快取會同時遇到成本與命中率瓶頸。</p>
<h2 id="策略">策略</h2>
<ol>
<li>定義不同資料熱度的落層策略。</li>
<li>把 eviction 與回補延遲納入共同指標。</li>
<li>驗證分層後 tail latency 與成本曲線。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction</a> 與 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity/cost</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2021/04/09/core-data/cachelib/">CacheLib and Kangaroo</a></li>
</ul>
]]></content:encoded></item><item><title>3.C4 LinkedIn：Kafka 分層叢集治理</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/</guid><description>&lt;p>LinkedIn 的 Kafka 分層叢集案例呈現了 Kafka 在規模化之後，瓶頸從「broker 容量」轉移到「workload 互相干擾」。分層的核心判斷是按業務風險隔離，把叢集當成資源治理單位。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>LinkedIn 是 Kafka 的誕生地，內部 Kafka 叢集承載的工作負載涵蓋即時推薦、搜尋索引更新、analytics pipeline、audit log 跟 monitoring。早期所有 workload 共用少數幾個大叢集，隨流量成長，叢集內不同 workload 的資源競爭開始互相影響。&lt;/p>
&lt;p>LinkedIn 的 Kafka 規模是全球最大的之一 — 數千個 broker、每秒數百萬筆訊息、PB 級資料保留。在這個規模下，單一叢集的容量限制是 broker 數量跟 ZooKeeper 的 metadata 管理上限，但更早觸及的限制是 workload 之間的干擾。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="noisy-neighbor">Noisy neighbor&lt;/h3>
&lt;p>即時推薦系統需要低延遲的 consumer（P99 &amp;lt; 50ms），analytics pipeline 是大量 batch consumer（高吞吐但延遲容忍到秒級）。兩者共用同一組 broker 時，batch consumer 的大範圍 sequential read 佔滿 disk I/O，擠壓即時推薦的 random read latency。&lt;/p>
&lt;p>一個 analytics job 的重跑（backfill 歷史資料）可以讓推薦系統的 consumer lag 從毫秒跳到秒級。在共享叢集中，這種干擾難以預防 — 只能在事後發現、人工協調。&lt;/p>
&lt;h3 id="broker-故障的影響面">Broker 故障的影響面&lt;/h3>
&lt;p>單一叢集中 broker 故障會觸發 partition reassignment，reassignment 的資料搬移佔用 disk I/O 跟網路頻寬。在混合 workload 的叢集中，reassignment 同時影響所有 workload 的效能 — 包括跟故障 broker 無直接關係的 topic。&lt;/p>
&lt;p>叢集越大、topic 越多、reassignment 的影響面越廣。&lt;/p>
&lt;h3 id="容量規劃的模糊邊界">容量規劃的模糊邊界&lt;/h3>
&lt;p>共享叢集的容量規劃沒有清楚的 owner — analytics 團隊說「我們需要更多 retention」、推薦團隊說「我們需要更低 latency」、audit 團隊說「我們的資料不能丟」。三種需求各自合理，但共享叢集無法同時最佳化。&lt;/p>
&lt;h2 id="解法分層叢集">解法：分層叢集&lt;/h2>
&lt;p>LinkedIn 按業務風險跟效能需求把 workload 分配到不同叢集：&lt;/p>
&lt;p>&lt;strong>Tier 1 — 即時關鍵路徑&lt;/strong>：即時推薦、搜尋索引更新、使用者通知。Broker 配置偏向低延遲（SSD、高 IOPS）、replication factor 3、retention 短（保留足夠的 consumer catchup 時間）。&lt;/p>
&lt;p>&lt;strong>Tier 2 — 可靠性要求高但延遲容忍&lt;/strong>：audit log、合規事件、支付事件。配置偏向持久性（replication factor 3、min.insync.replicas 2、acks=all）、retention 長。&lt;/p>
&lt;p>&lt;strong>Tier 3 — 高吞吐分析&lt;/strong>：analytics pipeline、ETL、batch processing。配置偏向吞吐（大 batch size、長 linger.ms、HDD）、retention 最長、容忍偶發 consumer lag。&lt;/p>
&lt;h3 id="分層的判準">分層的判準&lt;/h3>
&lt;p>分層的判準是「這個 workload 故障時，業務影響有多大、多快」：&lt;/p>
&lt;ul>
&lt;li>即時影響使用者體驗 → Tier 1&lt;/li>
&lt;li>影響合規或財務但可容忍分鐘級延遲 → Tier 2&lt;/li>
&lt;li>影響分析準確性但可容忍小時級延遲 → Tier 3&lt;/li>
&lt;/ul>
&lt;h2 id="取捨">取捨&lt;/h2>
&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>資源利用率&lt;/td>
 &lt;td>高（所有 workload 共用資源池）&lt;/td>
 &lt;td>低到中（每層有獨立的保留容量）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>隔離性&lt;/td>
 &lt;td>低（noisy neighbor 互相干擾）&lt;/td>
 &lt;td>高（故障跟效能退化限制在同層）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>運維複雜度&lt;/td>
 &lt;td>低（一組 broker 統一管理）&lt;/td>
 &lt;td>高（多組 broker、各自的監控跟維護）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量規劃清晰度&lt;/td>
 &lt;td>模糊（多種需求混合、難以歸因）&lt;/td>
 &lt;td>清楚（每層的需求跟 owner 明確）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障影響面&lt;/td>
 &lt;td>廣（reassignment 影響所有 topic）&lt;/td>
 &lt;td>有限（reassignment 只影響同層）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分層的成本是資源利用率下降 — 每層都需要保留一定的 headroom 應對高峰，加總起來比共享叢集多。LinkedIn 的判斷是隔離性的價值大於利用率的損失 — 推薦系統一次 P99 退化的業務損失遠大於多幾台 broker 的成本。&lt;/p></description><content:encoded><![CDATA[<p>LinkedIn 的 Kafka 分層叢集案例呈現了 Kafka 在規模化之後，瓶頸從「broker 容量」轉移到「workload 互相干擾」。分層的核心判斷是按業務風險隔離，把叢集當成資源治理單位。</p>
<h2 id="業務背景">業務背景</h2>
<p>LinkedIn 是 Kafka 的誕生地，內部 Kafka 叢集承載的工作負載涵蓋即時推薦、搜尋索引更新、analytics pipeline、audit log 跟 monitoring。早期所有 workload 共用少數幾個大叢集，隨流量成長，叢集內不同 workload 的資源競爭開始互相影響。</p>
<p>LinkedIn 的 Kafka 規模是全球最大的之一 — 數千個 broker、每秒數百萬筆訊息、PB 級資料保留。在這個規模下，單一叢集的容量限制是 broker 數量跟 ZooKeeper 的 metadata 管理上限，但更早觸及的限制是 workload 之間的干擾。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="noisy-neighbor">Noisy neighbor</h3>
<p>即時推薦系統需要低延遲的 consumer（P99 &lt; 50ms），analytics pipeline 是大量 batch consumer（高吞吐但延遲容忍到秒級）。兩者共用同一組 broker 時，batch consumer 的大範圍 sequential read 佔滿 disk I/O，擠壓即時推薦的 random read latency。</p>
<p>一個 analytics job 的重跑（backfill 歷史資料）可以讓推薦系統的 consumer lag 從毫秒跳到秒級。在共享叢集中，這種干擾難以預防 — 只能在事後發現、人工協調。</p>
<h3 id="broker-故障的影響面">Broker 故障的影響面</h3>
<p>單一叢集中 broker 故障會觸發 partition reassignment，reassignment 的資料搬移佔用 disk I/O 跟網路頻寬。在混合 workload 的叢集中，reassignment 同時影響所有 workload 的效能 — 包括跟故障 broker 無直接關係的 topic。</p>
<p>叢集越大、topic 越多、reassignment 的影響面越廣。</p>
<h3 id="容量規劃的模糊邊界">容量規劃的模糊邊界</h3>
<p>共享叢集的容量規劃沒有清楚的 owner — analytics 團隊說「我們需要更多 retention」、推薦團隊說「我們需要更低 latency」、audit 團隊說「我們的資料不能丟」。三種需求各自合理，但共享叢集無法同時最佳化。</p>
<h2 id="解法分層叢集">解法：分層叢集</h2>
<p>LinkedIn 按業務風險跟效能需求把 workload 分配到不同叢集：</p>
<p><strong>Tier 1 — 即時關鍵路徑</strong>：即時推薦、搜尋索引更新、使用者通知。Broker 配置偏向低延遲（SSD、高 IOPS）、replication factor 3、retention 短（保留足夠的 consumer catchup 時間）。</p>
<p><strong>Tier 2 — 可靠性要求高但延遲容忍</strong>：audit log、合規事件、支付事件。配置偏向持久性（replication factor 3、min.insync.replicas 2、acks=all）、retention 長。</p>
<p><strong>Tier 3 — 高吞吐分析</strong>：analytics pipeline、ETL、batch processing。配置偏向吞吐（大 batch size、長 linger.ms、HDD）、retention 最長、容忍偶發 consumer lag。</p>
<h3 id="分層的判準">分層的判準</h3>
<p>分層的判準是「這個 workload 故障時，業務影響有多大、多快」：</p>
<ul>
<li>即時影響使用者體驗 → Tier 1</li>
<li>影響合規或財務但可容忍分鐘級延遲 → Tier 2</li>
<li>影響分析準確性但可容忍小時級延遲 → Tier 3</li>
</ul>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>共享叢集</th>
          <th>分層叢集</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資源利用率</td>
          <td>高（所有 workload 共用資源池）</td>
          <td>低到中（每層有獨立的保留容量）</td>
      </tr>
      <tr>
          <td>隔離性</td>
          <td>低（noisy neighbor 互相干擾）</td>
          <td>高（故障跟效能退化限制在同層）</td>
      </tr>
      <tr>
          <td>運維複雜度</td>
          <td>低（一組 broker 統一管理）</td>
          <td>高（多組 broker、各自的監控跟維護）</td>
      </tr>
      <tr>
          <td>容量規劃清晰度</td>
          <td>模糊（多種需求混合、難以歸因）</td>
          <td>清楚（每層的需求跟 owner 明確）</td>
      </tr>
      <tr>
          <td>故障影響面</td>
          <td>廣（reassignment 影響所有 topic）</td>
          <td>有限（reassignment 只影響同層）</td>
      </tr>
  </tbody>
</table>
<p>分層的成本是資源利用率下降 — 每層都需要保留一定的 headroom 應對高峰，加總起來比共享叢集多。LinkedIn 的判斷是隔離性的價值大於利用率的損失 — 推薦系統一次 P99 退化的業務損失遠大於多幾台 broker 的成本。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 配置怎麼影響延遲 vs 吞吐 vs 持久性的取捨。</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>：不同 tier 的 Kafka 叢集各自有不同的 reliability budget。</li>
<li><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 design</a>：batch consumer 跟 real-time consumer 的資源消耗差異。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>即時消費者的 consumer lag 因為同叢集的 batch job 而上升</li>
<li>Broker 故障後的 partition reassignment 影響到跟故障無關的 topic</li>
<li>容量規劃會議中不同團隊的需求互相矛盾、無法在同一組配置中滿足</li>
<li>Kafka 叢集的 topic 數量超過 500 個、workload 類型超過三種</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.linkedin.com/kafka/running-kafka-scale">Running Kafka at Scale at LinkedIn</a></li>
</ul>
]]></content:encoded></item><item><title>4.C4 AWS：X-Ray 到 OpenTelemetry 轉換</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/</guid><description>&lt;p>這個案例的核心責任是把觀測遷移從工具替換，提升為標準化策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS 已明確提出 X-Ray SDK/Daemon 的維護時程，並提供遷移到 OpenTelemetry 的路徑。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 observability agent 與 SDK 受限於單一供應商，轉向 OTel 可以降低未來轉移成本，但需要治理採集、匯出與語意對齊。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先盤點現有 instrumentation 與依賴 SDK。&lt;/li>
&lt;li>先換 collector/agent，再逐步改應用端 instrumentation。&lt;/li>
&lt;li>把 trace/metric 的等價驗證納入 release gate。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-migration.html">X-Ray to OpenTelemetry migration guide&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把觀測遷移從工具替換，提升為標準化策略。</p>
<h2 id="觀察">觀察</h2>
<p>AWS 已明確提出 X-Ray SDK/Daemon 的維護時程，並提供遷移到 OpenTelemetry 的路徑。</p>
<h2 id="判讀">判讀</h2>
<p>當 observability agent 與 SDK 受限於單一供應商，轉向 OTel 可以降低未來轉移成本，但需要治理採集、匯出與語意對齊。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先盤點現有 instrumentation 與依賴 SDK。</li>
<li>先換 collector/agent，再逐步改應用端 instrumentation。</li>
<li>把 trace/metric 的等價驗證納入 release gate。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<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> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-migration.html">X-Ray to OpenTelemetry migration guide</a></li>
</ul>
]]></content:encoded></item><item><title>5.C4 Mobileye：Workloads 遷移到 EKS</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/</guid><description>&lt;p>這個案例的核心責任是把 workload 遷移從基礎設施作業改成服務可用性作業。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Mobileye 將大規模工作負載遷移到 EKS。遷移動機集中在運維一致性與可用性治理——原有環境中不同團隊各自維護部署流程，升級節奏、監控覆蓋、容量規劃的標準不統一。遷移目標是用 managed 平台統一這些操作基線，讓各團隊可以專注在 workload 本身。&lt;/p>
&lt;p>遷移範圍涵蓋多種 workload 類型：API 服務、資料處理 pipeline、ML 推論服務。這些 workload 的啟動時間、資源需求、drain 條件差異顯著，同一套遷移策略無法直接套用。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>工作負載遷移若缺乏分段驗證，容易在切流時放大依賴與資源風險。這個判讀的具體含義是：workload 從舊平台搬到新平台時，表面上看 pod 跑起來了、health check 通過了，但依賴路徑（資料庫連線、cache endpoint、queue consumer 註冊）可能還指向舊環境。這類錯位在小流量時不明顯，放大流量後才暴露延遲升高或認證失敗。&lt;/p>
&lt;p>另一個判讀是容量假設需要重新驗證。舊平台的 resource request/limit、HPA 設定是在舊環境的 node type、網路拓樸下校準的。新平台的 node 規格、storage driver、CNI 可能不同，原本的容量假設可能過鬆或過緊。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>分批遷移 workload、保留觀測對照&lt;/strong>：先遷移影響面小、依賴單純的 workload（如內部工具、非關鍵 API）。新舊平台同時跑相同 workload 時，比較 error rate、latency、資源使用率。觀測對照是驗證的基礎——沒有對照就無法判斷新平台行為是否符合預期。&lt;/li>
&lt;li>&lt;strong>明確定義每批次切換與回退條件&lt;/strong>：每批遷移前寫下「什麼條件算成功」和「什麼條件觸發回退」。成功條件用 SLI 偏差衡量（error rate 不超過基線 + N%、p99 latency 不超過基線 + M ms）。回退條件要可操作——回退腳本事先驗證、DNS/LB 規則切回路徑事先測試。&lt;/li>
&lt;li>&lt;strong>新平台先驗證容量與恢復節奏&lt;/strong>：在新平台上跑容量測試，確認 HPA 觸發、node scale-up、pod scheduling 的時間符合預期。恢復節奏驗證包含模擬 node 失效後 pod 重新調度的時間、模擬 deployment rollback 的完成時間。&lt;/li>
&lt;li>&lt;strong>workload 類型分群遷移&lt;/strong>：API 服務、batch job、ML 推論的遷移順序與驗證條件不同。API 服務看延遲與錯誤率；batch job 看完成時間與資料正確性；ML 推論看推論延遲與 GPU 資源分配。混在一批遷移會讓驗證條件模糊。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>這類遷移的回退判讀重點是「回退到舊平台時，舊平台是否仍在可服務狀態」。遷移進行中若舊平台的資源已被縮減（node 數降低、monitoring 設定已移除），回退路徑就失效。穩定做法是在該批 workload 的新平台觀測窗口結束前，舊平台維持原規模不動。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看分階段平台遷移的流量切換節奏。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract&lt;/a> 看不同 workload 類型的 lifecycle 差異。回 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review&lt;/a> 看遷移前的可靠性評估。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/mobileye-amazon-eks/">Mobileye migration to Amazon EKS&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 workload 遷移從基礎設施作業改成服務可用性作業。</p>
<h2 id="觀察">觀察</h2>
<p>Mobileye 將大規模工作負載遷移到 EKS。遷移動機集中在運維一致性與可用性治理——原有環境中不同團隊各自維護部署流程，升級節奏、監控覆蓋、容量規劃的標準不統一。遷移目標是用 managed 平台統一這些操作基線，讓各團隊可以專注在 workload 本身。</p>
<p>遷移範圍涵蓋多種 workload 類型：API 服務、資料處理 pipeline、ML 推論服務。這些 workload 的啟動時間、資源需求、drain 條件差異顯著，同一套遷移策略無法直接套用。</p>
<h2 id="判讀">判讀</h2>
<p>工作負載遷移若缺乏分段驗證，容易在切流時放大依賴與資源風險。這個判讀的具體含義是：workload 從舊平台搬到新平台時，表面上看 pod 跑起來了、health check 通過了，但依賴路徑（資料庫連線、cache endpoint、queue consumer 註冊）可能還指向舊環境。這類錯位在小流量時不明顯，放大流量後才暴露延遲升高或認證失敗。</p>
<p>另一個判讀是容量假設需要重新驗證。舊平台的 resource request/limit、HPA 設定是在舊環境的 node type、網路拓樸下校準的。新平台的 node 規格、storage driver、CNI 可能不同，原本的容量假設可能過鬆或過緊。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>分批遷移 workload、保留觀測對照</strong>：先遷移影響面小、依賴單純的 workload（如內部工具、非關鍵 API）。新舊平台同時跑相同 workload 時，比較 error rate、latency、資源使用率。觀測對照是驗證的基礎——沒有對照就無法判斷新平台行為是否符合預期。</li>
<li><strong>明確定義每批次切換與回退條件</strong>：每批遷移前寫下「什麼條件算成功」和「什麼條件觸發回退」。成功條件用 SLI 偏差衡量（error rate 不超過基線 + N%、p99 latency 不超過基線 + M ms）。回退條件要可操作——回退腳本事先驗證、DNS/LB 規則切回路徑事先測試。</li>
<li><strong>新平台先驗證容量與恢復節奏</strong>：在新平台上跑容量測試，確認 HPA 觸發、node scale-up、pod scheduling 的時間符合預期。恢復節奏驗證包含模擬 node 失效後 pod 重新調度的時間、模擬 deployment rollback 的完成時間。</li>
<li><strong>workload 類型分群遷移</strong>：API 服務、batch job、ML 推論的遷移順序與驗證條件不同。API 服務看延遲與錯誤率；batch job 看完成時間與資料正確性；ML 推論看推論延遲與 GPU 資源分配。混在一批遷移會讓驗證條件模糊。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>這類遷移的回退判讀重點是「回退到舊平台時，舊平台是否仍在可服務狀態」。遷移進行中若舊平台的資源已被縮減（node 數降低、monitoring 設定已移除），回退路徑就失效。穩定做法是在該批 workload 的新平台觀測窗口結束前，舊平台維持原規模不動。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看分階段平台遷移的流量切換節奏。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract</a> 看不同 workload 類型的 lifecycle 差異。回 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a> 看遷移前的可靠性評估。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/mobileye-amazon-eks/">Mobileye migration to Amazon EKS</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>7.C4 Microsoft：Storm-0558 簽章金鑰事件</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/</guid><description>&lt;p>這個案例的核心責任是把身份簽章事件轉成長期信任治理問題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Storm-0558 事件揭露簽章金鑰與驗證流程一旦失守，會跨租戶影響身份驗證信任。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>此類事件的重點不只在修補漏洞，而在重建 key lifecycle、issuer 驗證與審計可見性。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>重新定義 key issuance 與 rotation 流程。&lt;/li>
&lt;li>強化 token 驗證路徑與異常檢測。&lt;/li>
&lt;li>讓身份證據鏈可被 incident 與稽核共用。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 secrets/credentials&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit/accountability&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.microsoft.com/en-us/security/blog/2023/09/06/analysis-of-storm-0558-technique-and-microsofts-response/">Microsoft analysis of Storm-0558&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把身份簽章事件轉成長期信任治理問題。</p>
<h2 id="觀察">觀察</h2>
<p>Storm-0558 事件揭露簽章金鑰與驗證流程一旦失守，會跨租戶影響身份驗證信任。</p>
<h2 id="判讀">判讀</h2>
<p>此類事件的重點不只在修補漏洞，而在重建 key lifecycle、issuer 驗證與審計可見性。</p>
<h2 id="策略">策略</h2>
<ol>
<li>重新定義 key issuance 與 rotation 流程。</li>
<li>強化 token 驗證路徑與異常檢測。</li>
<li>讓身份證據鏈可被 incident 與稽核共用。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 secrets/credentials</a> 與 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit/accountability</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.microsoft.com/en-us/security/blog/2023/09/06/analysis-of-storm-0558-technique-and-microsofts-response/">Microsoft analysis of Storm-0558</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare 2023 Workers KV Deployment Tool Misconfiguration</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/</guid><description>&lt;p>這起事件的核心責任判讀是：控制面工具設定錯誤會跨越產品邊界擴散，事故第一步要先切斷擴散路徑，再做功能修復。若先把症狀拆成多個產品問題，恢復速度會被 shared dependency 拖慢。&lt;/p>
&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cloudflare 在 2023-10-30 發生控制面相關事故，根因涉及 deployment tool 的設定錯誤，影響 Workers KV 與相關服務操作路徑。表面症狀可出現在多個產品面向，但本質是共享控制面變更帶來的連鎖失效。&lt;/p>
&lt;p>這類事故和單點 runtime bug 不同。關鍵不是「哪個服務先報錯」，而是「哪個共用控制點先失真」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多產品控制操作同時不穩&lt;/td>
 &lt;td>shared control dependency 可能失效&lt;/td>
 &lt;td>先盤點同批變更與共用工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>功能異常分布不均&lt;/td>
 &lt;td>擴散沿著控制面依賴鏈條走&lt;/td>
 &lt;td>用 dependency map 排定恢復優先順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後錯誤率快速下降&lt;/td>
 &lt;td>變更關聯度高&lt;/td>
 &lt;td>凍結同類變更、啟動增量復原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故中角色交接反覆切換&lt;/td>
 &lt;td>ownership 與指揮節奏不足&lt;/td>
 &lt;td>固定 single incident commander 與節點交接&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="事故路徑">事故路徑&lt;/h2>
&lt;ol>
&lt;li>控制面 deployment tool 變更進入生產。&lt;/li>
&lt;li>設定錯誤導致共享控制路徑失真。&lt;/li>
&lt;li>Workers KV 與關聯產品出現控制操作異常。&lt;/li>
&lt;li>團隊透過回退與修正逐步收斂錯誤。&lt;/li>
&lt;li>事故後回寫 deployment guardrail、decision log 與 evidence 管線。&lt;/li>
&lt;/ol>
&lt;h2 id="可回寫控制面">可回寫控制面&lt;/h2>
&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>變更範圍治理&lt;/td>
 &lt;td>控制面變更可快速全域擴散&lt;/td>
 &lt;td>強制 staged rollout + canary gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>決策紀錄&lt;/td>
 &lt;td>假設與回退條件在事中容易遺失&lt;/td>
 &lt;td>強制使用 [8.19] 決策欄位模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證據回寫&lt;/td>
 &lt;td>教訓停留在事件敘事&lt;/td>
 &lt;td>連到 [8.22]，把證據回寫到 observability/reliability 控制面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則推送安全閘門&lt;/td>
 &lt;td>變更工具缺少風險分級&lt;/td>
 &lt;td>回寫 [6.24] 的 rule rollout gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>事故決策紀錄： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>&lt;/li>
&lt;li>事故證據回寫： &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>&lt;/li>
&lt;li>規則推送安全閘門： &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a>&lt;/li>
&lt;li>觀測治理模型： &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/cloudflare-incident-on-october-30-2023/">Cloudflare incident on October 30, 2023&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這起事件的核心責任判讀是：控制面工具設定錯誤會跨越產品邊界擴散，事故第一步要先切斷擴散路徑，再做功能修復。若先把症狀拆成多個產品問題，恢復速度會被 shared dependency 拖慢。</p>
<h2 id="事故摘要">事故摘要</h2>
<p>Cloudflare 在 2023-10-30 發生控制面相關事故，根因涉及 deployment tool 的設定錯誤，影響 Workers KV 與相關服務操作路徑。表面症狀可出現在多個產品面向，但本質是共享控制面變更帶來的連鎖失效。</p>
<p>這類事故和單點 runtime bug 不同。關鍵不是「哪個服務先報錯」，而是「哪個共用控制點先失真」。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>代表意義</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多產品控制操作同時不穩</td>
          <td>shared control dependency 可能失效</td>
          <td>先盤點同批變更與共用工具</td>
      </tr>
      <tr>
          <td>功能異常分布不均</td>
          <td>擴散沿著控制面依賴鏈條走</td>
          <td>用 dependency map 排定恢復優先順序</td>
      </tr>
      <tr>
          <td>回退後錯誤率快速下降</td>
          <td>變更關聯度高</td>
          <td>凍結同類變更、啟動增量復原</td>
      </tr>
      <tr>
          <td>事故中角色交接反覆切換</td>
          <td>ownership 與指揮節奏不足</td>
          <td>固定 single incident commander 與節點交接</td>
      </tr>
  </tbody>
</table>
<h2 id="事故路徑">事故路徑</h2>
<ol>
<li>控制面 deployment tool 變更進入生產。</li>
<li>設定錯誤導致共享控制路徑失真。</li>
<li>Workers KV 與關聯產品出現控制操作異常。</li>
<li>團隊透過回退與修正逐步收斂錯誤。</li>
<li>事故後回寫 deployment guardrail、decision log 與 evidence 管線。</li>
</ol>
<h2 id="可回寫控制面">可回寫控制面</h2>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>暴露缺口</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>變更範圍治理</td>
          <td>控制面變更可快速全域擴散</td>
          <td>強制 staged rollout + canary gate</td>
      </tr>
      <tr>
          <td>決策紀錄</td>
          <td>假設與回退條件在事中容易遺失</td>
          <td>強制使用 [8.19] 決策欄位模板</td>
      </tr>
      <tr>
          <td>證據回寫</td>
          <td>教訓停留在事件敘事</td>
          <td>連到 [8.22]，把證據回寫到 observability/reliability 控制面</td>
      </tr>
      <tr>
          <td>規則推送安全閘門</td>
          <td>變更工具缺少風險分級</td>
          <td>回寫 [6.24] 的 rule rollout gate</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>事故決策紀錄： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>事故證據回寫： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>規則推送安全閘門： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a></li>
<li>觀測治理模型： <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/cloudflare-incident-on-october-30-2023/">Cloudflare incident on October 30, 2023</a></li>
</ul>
]]></content:encoded></item><item><title>營運後技術轉換：語言、工具與架構何時該換</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/</guid><description>&lt;p>這個案例的核心責任是把「營運後轉換」變成可判讀決策，而不是技術潮流追逐。服務在成長期常會遇到早期選型與現況負載不再匹配，此時轉換的重點是風險收斂與效率改善，而不是語言偏好。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Slack：PHP 逐步遷移到 Hack&lt;/td>
 &lt;td>語言/型別系統&lt;/td>
 &lt;td>以漸進式靜態型別提升重構安全與開發效率，降低 runtime 才暴露型別錯誤的成本。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Discord：Read States 服務 Go 重寫為 Rust&lt;/td>
 &lt;td>語言/執行模型&lt;/td>
 &lt;td>Go 服務在特定負載下出現 GC 造成的週期性延遲尖峰，Rust 以無 GC 記憶體模型降低延遲抖動。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dropbox：Python 2 轉 Python 3&lt;/td>
 &lt;td>語言/runtime 生命週期&lt;/td>
 &lt;td>Python 2 EOL 與型別工具鏈演進壓力，驅動全面升級並降低長期維護風險。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dropbox：內部 RPC 轉向 gRPC（Courier）&lt;/td>
 &lt;td>工具/協定標準化&lt;/td>
 &lt;td>多語言服務擴張後，需要統一傳輸契約、提高跨團隊可維護性與可觀測性。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitLab：單一資料庫拆成 Main/CI 資料庫&lt;/td>
 &lt;td>資料層架構&lt;/td>
 &lt;td>單庫承載產品與 CI 工作負載，容量與干擾風險上升，需以職責拆分換取穩定性。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Notion：Postgres 單庫轉分片&lt;/td>
 &lt;td>資料層架構&lt;/td>
 &lt;td>寫入與資料量成長造成熱點與容量壓力，以分片提升可擴展性與故障隔離。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify：Rails 後端引入 Vitess 水平擴充&lt;/td>
 &lt;td>資料層工具&lt;/td>
 &lt;td>MySQL 垂直擴充成本上升，需在不中斷服務前提下取得分片與路由能力。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify：Ruby 導入 Sorbet 靜態型別&lt;/td>
 &lt;td>工具/語言治理&lt;/td>
 &lt;td>大型程式碼庫重構與跨團隊協作風險高，需要型別訊號降低變更不確定性。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Figma：服務遷移至 Kubernetes&lt;/td>
 &lt;td>平台/部署工具&lt;/td>
 &lt;td>手工或半自動部署流程難以支撐規模成長，需要統一調度、回滾與資源治理能力。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare：邊緣系統由 C/NGINX 模組逐步改寫 Rust&lt;/td>
 &lt;td>語言/安全性&lt;/td>
 &lt;td>記憶體安全與可維護性需求提升，在高效能路徑引入 Rust 降低記憶體錯誤風險。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Slack：關鍵服務從單體拓撲遷移到 Cell-based 架構&lt;/td>
 &lt;td>架構/隔離策略&lt;/td>
 &lt;td>以降低爆炸半徑與提高冗餘為目標，將重大故障影響限制在局部 cell。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Uber：大規模微服務治理轉向 Domain-oriented 邊界重整&lt;/td>
 &lt;td>架構/組織對齊&lt;/td>
 &lt;td>服務數量擴張後依賴複雜度暴增，需要把技術邊界與業務邊界對齊以降低協作與故障傳染成本。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta：MySQL 大規模場景導入 MyRocks&lt;/td>
 &lt;td>儲存引擎/成本優化&lt;/td>
 &lt;td>寫入放大與儲存成本壓力上升，透過新儲存引擎換取空間效率與寫入效能。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例分組判讀">案例分組判讀&lt;/h2>
&lt;h3 id="語言與型別系統轉換">語言與型別系統轉換&lt;/h3>
&lt;p>語言轉換常見於「延遲抖動不可接受」或「重構風險不可接受」兩類壓力。前者多是 runtime/記憶體模型問題，後者多是大型程式碼庫可維護性問題。&lt;/p>
&lt;ul>
&lt;li>代表案例：Slack PHP -&amp;gt; Hack、Discord Go -&amp;gt; Rust、Dropbox Python 2 -&amp;gt; Python 3、Cloudflare C/NGINX -&amp;gt; Rust&lt;/li>
&lt;li>主要動機：降低 tail latency、提升記憶體安全、對抗 runtime EOL、引入更強型別訊號&lt;/li>
&lt;/ul>
&lt;h3 id="資料層與儲存架構轉換">資料層與儲存架構轉換&lt;/h3>
&lt;p>資料層轉換通常源自單體資料庫在容量、隔離與可恢復性上出現結構性瓶頸，追新技術本身很少是真正驅動力。&lt;/p>
&lt;ul>
&lt;li>代表案例：GitLab Main/CI split、Notion Postgres sharding、Shopify Vitess、Meta MyRocks&lt;/li>
&lt;li>主要動機：解耦不同負載、降低熱點、取得水平擴充、降低儲存成本&lt;/li>
&lt;/ul>
&lt;h3 id="平台與部署工具轉換">平台與部署工具轉換&lt;/h3>
&lt;p>平台轉換通常發生在部署頻率提升後，原本的人工作業或弱自動化無法承擔發布風險。&lt;/p>
&lt;ul>
&lt;li>代表案例：Figma 遷移 Kubernetes、Dropbox RPC 標準化到 gRPC&lt;/li>
&lt;li>主要動機：統一部署控制面、縮短發布/回滾時間、提升跨語言協作效率&lt;/li>
&lt;/ul>
&lt;h3 id="架構邊界重整">架構邊界重整&lt;/h3>
&lt;p>架構重整通常是「故障會跨邊界放大」或「團隊邊界與系統邊界失配」時的修正動作。&lt;/p>
&lt;ul>
&lt;li>代表案例：Slack cellular architecture、Uber domain-oriented microservice governance&lt;/li>
&lt;li>主要動機：縮小 blast radius、讓服務責任與組織責任對齊、降低跨團隊耦合&lt;/li>
&lt;/ul>
&lt;h2 id="三倍擴充案例池42">三倍擴充案例池（42）&lt;/h2>
&lt;p>這份案例池的核心責任是提供「可直接回寫實作」的案例母體，而不是只做公司清單。下面分成兩層：外部官方遷移案例（偏選型與轉換動機）與站內已整理案例（偏實作、驗證、事故教訓）。&lt;/p>
&lt;h3 id="a-外部官方遷移案例20">A. 外部官方遷移案例（20）&lt;/h3>
&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>Slack PHP -&amp;gt; Hack&lt;/td>
 &lt;td>漸進型別化與大型重構安全&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Discord Go -&amp;gt; Rust&lt;/td>
 &lt;td>延遲長尾與 GC 抖動治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dropbox Python 2 -&amp;gt; 3&lt;/td>
 &lt;td>runtime EOL 與生態升級&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dropbox RPC -&amp;gt; gRPC&lt;/td>
 &lt;td>協定標準化與跨語言維運&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitLab Main/CI DB split&lt;/td>
 &lt;td>單庫拆分與負載隔離&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Notion Postgres sharding&lt;/td>
 &lt;td>熱點與容量壓力分片&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify MySQL -&amp;gt; Vitess&lt;/td>
 &lt;td>水平擴充與線上遷移&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify Ruby + Sorbet&lt;/td>
 &lt;td>動態語言型別治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Figma -&amp;gt; Kubernetes&lt;/td>
 &lt;td>部署控制面平台化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare C/NGINX -&amp;gt; Rust&lt;/td>
 &lt;td>記憶體安全與效能路徑重寫&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Slack monolith topology -&amp;gt; cellular&lt;/td>
 &lt;td>blast radius 局部化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Uber domain-oriented microservices&lt;/td>
 &lt;td>服務邊界與組織對齊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta MySQL -&amp;gt; MyRocks&lt;/td>
 &lt;td>儲存成本與寫入效率&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pinterest HBase -&amp;gt; TiDB&lt;/td>
 &lt;td>零停機儲存遷移&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pinterest 新 wide-column DB（RocksDB）&lt;/td>
 &lt;td>資料層能力換血&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta MySQL Raft deploy&lt;/td>
 &lt;td>failover 工具化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify MySQL upgrade program&lt;/td>
 &lt;td>大規模升級治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitLab major PostgreSQL upgrade&lt;/td>
 &lt;td>主版本升級與回退窗&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS shuffle sharding adoption&lt;/td>
 &lt;td>多租戶隔離重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare observability stack內建化&lt;/td>
 &lt;td>觀測平台內生化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="b-站內可回寫實作案例池22">B. 站內可回寫實作案例池（22）&lt;/h3>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移&lt;/a>&lt;/td>
 &lt;td>交易安全 + migration 並行&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest：快取可靠性與容量驚奇治理&lt;/a>&lt;/td>
 &lt;td>快取策略與容量重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon：Shuffle Sharding 與 Cell 邊界&lt;/a>&lt;/td>
 &lt;td>cell/shard 重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">Meta：Region Failover 與可靠性邊界&lt;/a>&lt;/td>
 &lt;td>區域切換能力演進&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day&lt;/a>&lt;/td>
 &lt;td>高峰前治理轉換&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 發布門檻&lt;/a>&lt;/td>
 &lt;td>從速度導向轉為預算導向&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft：變更治理與可靠性門檻&lt;/a>&lt;/td>
 &lt;td>變更流程平台化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">Spotify：平台工程與可靠性契約&lt;/a>&lt;/td>
 &lt;td>團隊自助平台化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity Headroom 與 On-call 分層&lt;/a>&lt;/td>
 &lt;td>容量與值班模型重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT&lt;/a>&lt;/td>
 &lt;td>驗證方法轉換&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動操作&lt;/a>&lt;/td>
 &lt;td>告警治理轉換&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 MySQL Topology Incident&lt;/a>&lt;/td>
 &lt;td>跨區 DB 拓撲決策轉換&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">Reddit 2023 Kubernetes 升級事故&lt;/a>&lt;/td>
 &lt;td>平台升級失敗模式&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">Discord 2022 Gateway 容量事件&lt;/a>&lt;/td>
 &lt;td>容量與連線模型調整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage&lt;/a>&lt;/td>
 &lt;td>規則系統推送模型調整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">8.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">Cloudflare 2023 Control Plane Token Incident&lt;/a>&lt;/td>
 &lt;td>控制面信任邊界重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-control-handoff-to-delivery-and-incident/" data-link-title="7.18 資安控制面如何交接到部署與事故流程" data-link-desc="建立資安控制面交接到部署、可靠性與事故流程的大綱">7.12&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">Fastly 2021 全域 Edge 配置事故&lt;/a>&lt;/td>
 &lt;td>配置發布流程轉換&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">AWS S3 2017 US-EAST-1 事件&lt;/a>&lt;/td>
 &lt;td>控制面操作模型重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">Atlassian 2022 多租戶刪除事故&lt;/a>&lt;/td>
 &lt;td>tenant 安全邊界重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">Azure AD 2021 身分控制面事件&lt;/a>&lt;/td>
 &lt;td>身分服務依賴治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 多服務網路擁塞事件&lt;/a>&lt;/td>
 &lt;td>區域網路依賴重整&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">Heroku 2021 Routing 控制事件&lt;/a>&lt;/td>
 &lt;td>路由控制面恢復策略&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這兩層合計 42 個案例。使用方式是先在 A 層找轉換動機，再到 B 層找可操作證據與失敗模式，最後回寫到 &lt;code>01/04/06/08&lt;/code> 的正文。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是把「營運後轉換」變成可判讀決策，而不是技術潮流追逐。服務在成長期常會遇到早期選型與現況負載不再匹配，此時轉換的重點是風險收斂與效率改善，而不是語言偏好。</p>
<h2 id="大量真實案例與轉換原因">大量真實案例與轉換原因</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>轉換類型</th>
          <th>為什麼轉換</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack：PHP 逐步遷移到 Hack</td>
          <td>語言/型別系統</td>
          <td>以漸進式靜態型別提升重構安全與開發效率，降低 runtime 才暴露型別錯誤的成本。</td>
      </tr>
      <tr>
          <td>Discord：Read States 服務 Go 重寫為 Rust</td>
          <td>語言/執行模型</td>
          <td>Go 服務在特定負載下出現 GC 造成的週期性延遲尖峰，Rust 以無 GC 記憶體模型降低延遲抖動。</td>
      </tr>
      <tr>
          <td>Dropbox：Python 2 轉 Python 3</td>
          <td>語言/runtime 生命週期</td>
          <td>Python 2 EOL 與型別工具鏈演進壓力，驅動全面升級並降低長期維護風險。</td>
      </tr>
      <tr>
          <td>Dropbox：內部 RPC 轉向 gRPC（Courier）</td>
          <td>工具/協定標準化</td>
          <td>多語言服務擴張後，需要統一傳輸契約、提高跨團隊可維護性與可觀測性。</td>
      </tr>
      <tr>
          <td>GitLab：單一資料庫拆成 Main/CI 資料庫</td>
          <td>資料層架構</td>
          <td>單庫承載產品與 CI 工作負載，容量與干擾風險上升，需以職責拆分換取穩定性。</td>
      </tr>
      <tr>
          <td>Notion：Postgres 單庫轉分片</td>
          <td>資料層架構</td>
          <td>寫入與資料量成長造成熱點與容量壓力，以分片提升可擴展性與故障隔離。</td>
      </tr>
      <tr>
          <td>Shopify：Rails 後端引入 Vitess 水平擴充</td>
          <td>資料層工具</td>
          <td>MySQL 垂直擴充成本上升，需在不中斷服務前提下取得分片與路由能力。</td>
      </tr>
      <tr>
          <td>Shopify：Ruby 導入 Sorbet 靜態型別</td>
          <td>工具/語言治理</td>
          <td>大型程式碼庫重構與跨團隊協作風險高，需要型別訊號降低變更不確定性。</td>
      </tr>
      <tr>
          <td>Figma：服務遷移至 Kubernetes</td>
          <td>平台/部署工具</td>
          <td>手工或半自動部署流程難以支撐規模成長，需要統一調度、回滾與資源治理能力。</td>
      </tr>
      <tr>
          <td>Cloudflare：邊緣系統由 C/NGINX 模組逐步改寫 Rust</td>
          <td>語言/安全性</td>
          <td>記憶體安全與可維護性需求提升，在高效能路徑引入 Rust 降低記憶體錯誤風險。</td>
      </tr>
      <tr>
          <td>Slack：關鍵服務從單體拓撲遷移到 Cell-based 架構</td>
          <td>架構/隔離策略</td>
          <td>以降低爆炸半徑與提高冗餘為目標，將重大故障影響限制在局部 cell。</td>
      </tr>
      <tr>
          <td>Uber：大規模微服務治理轉向 Domain-oriented 邊界重整</td>
          <td>架構/組織對齊</td>
          <td>服務數量擴張後依賴複雜度暴增，需要把技術邊界與業務邊界對齊以降低協作與故障傳染成本。</td>
      </tr>
      <tr>
          <td>Meta：MySQL 大規模場景導入 MyRocks</td>
          <td>儲存引擎/成本優化</td>
          <td>寫入放大與儲存成本壓力上升，透過新儲存引擎換取空間效率與寫入效能。</td>
      </tr>
  </tbody>
</table>
<h2 id="案例分組判讀">案例分組判讀</h2>
<h3 id="語言與型別系統轉換">語言與型別系統轉換</h3>
<p>語言轉換常見於「延遲抖動不可接受」或「重構風險不可接受」兩類壓力。前者多是 runtime/記憶體模型問題，後者多是大型程式碼庫可維護性問題。</p>
<ul>
<li>代表案例：Slack PHP -&gt; Hack、Discord Go -&gt; Rust、Dropbox Python 2 -&gt; Python 3、Cloudflare C/NGINX -&gt; Rust</li>
<li>主要動機：降低 tail latency、提升記憶體安全、對抗 runtime EOL、引入更強型別訊號</li>
</ul>
<h3 id="資料層與儲存架構轉換">資料層與儲存架構轉換</h3>
<p>資料層轉換通常源自單體資料庫在容量、隔離與可恢復性上出現結構性瓶頸，追新技術本身很少是真正驅動力。</p>
<ul>
<li>代表案例：GitLab Main/CI split、Notion Postgres sharding、Shopify Vitess、Meta MyRocks</li>
<li>主要動機：解耦不同負載、降低熱點、取得水平擴充、降低儲存成本</li>
</ul>
<h3 id="平台與部署工具轉換">平台與部署工具轉換</h3>
<p>平台轉換通常發生在部署頻率提升後，原本的人工作業或弱自動化無法承擔發布風險。</p>
<ul>
<li>代表案例：Figma 遷移 Kubernetes、Dropbox RPC 標準化到 gRPC</li>
<li>主要動機：統一部署控制面、縮短發布/回滾時間、提升跨語言協作效率</li>
</ul>
<h3 id="架構邊界重整">架構邊界重整</h3>
<p>架構重整通常是「故障會跨邊界放大」或「團隊邊界與系統邊界失配」時的修正動作。</p>
<ul>
<li>代表案例：Slack cellular architecture、Uber domain-oriented microservice governance</li>
<li>主要動機：縮小 blast radius、讓服務責任與組織責任對齊、降低跨團隊耦合</li>
</ul>
<h2 id="三倍擴充案例池42">三倍擴充案例池（42）</h2>
<p>這份案例池的核心責任是提供「可直接回寫實作」的案例母體，而不是只做公司清單。下面分成兩層：外部官方遷移案例（偏選型與轉換動機）與站內已整理案例（偏實作、驗證、事故教訓）。</p>
<h3 id="a-外部官方遷移案例20">A. 外部官方遷移案例（20）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>轉換主題</th>
          <th>實作討論入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack PHP -&gt; Hack</td>
          <td>漸進型別化與大型重構安全</td>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
      </tr>
      <tr>
          <td>Discord Go -&gt; Rust</td>
          <td>延遲長尾與 GC 抖動治理</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td>Dropbox Python 2 -&gt; 3</td>
          <td>runtime EOL 與生態升級</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Dropbox RPC -&gt; gRPC</td>
          <td>協定標準化與跨語言維運</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a></td>
      </tr>
      <tr>
          <td>GitLab Main/CI DB split</td>
          <td>單庫拆分與負載隔離</td>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
      </tr>
      <tr>
          <td>Notion Postgres sharding</td>
          <td>熱點與容量壓力分片</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>Shopify MySQL -&gt; Vitess</td>
          <td>水平擴充與線上遷移</td>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
      </tr>
      <tr>
          <td>Shopify Ruby + Sorbet</td>
          <td>動態語言型別治理</td>
          <td><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10</a></td>
      </tr>
      <tr>
          <td>Figma -&gt; Kubernetes</td>
          <td>部署控制面平台化</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a></td>
      </tr>
      <tr>
          <td>Cloudflare C/NGINX -&gt; Rust</td>
          <td>記憶體安全與效能路徑重寫</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>Slack monolith topology -&gt; cellular</td>
          <td>blast radius 局部化</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
      <tr>
          <td>Uber domain-oriented microservices</td>
          <td>服務邊界與組織對齊</td>
          <td><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a></td>
      </tr>
      <tr>
          <td>Meta MySQL -&gt; MyRocks</td>
          <td>儲存成本與寫入效率</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>Pinterest HBase -&gt; TiDB</td>
          <td>零停機儲存遷移</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td>Pinterest 新 wide-column DB（RocksDB）</td>
          <td>資料層能力換血</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>Meta MySQL Raft deploy</td>
          <td>failover 工具化</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a></td>
      </tr>
      <tr>
          <td>Shopify MySQL upgrade program</td>
          <td>大規模升級治理</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>GitLab major PostgreSQL upgrade</td>
          <td>主版本升級與回退窗</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td>AWS shuffle sharding adoption</td>
          <td>多租戶隔離重整</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>Cloudflare observability stack內建化</td>
          <td>觀測平台內生化</td>
          <td><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
  </tbody>
</table>
<h3 id="b-站內可回寫實作案例池22">B. 站內可回寫實作案例池（22）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>轉換主題</th>
          <th>實作討論入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>交易安全 + migration 並行</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest：快取可靠性與容量驚奇治理</a></td>
          <td>快取策略與容量重整</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon：Shuffle Sharding 與 Cell 邊界</a></td>
          <td>cell/shard 重整</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">Meta：Region Failover 與可靠性邊界</a></td>
          <td>區域切換能力演進</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>高峰前治理轉換</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.6</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 發布門檻</a></td>
          <td>從速度導向轉為預算導向</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.2</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft：變更治理與可靠性門檻</a></td>
          <td>變更流程平台化</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">Spotify：平台工程與可靠性契約</a></td>
          <td>團隊自助平台化</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity Headroom 與 On-call 分層</a></td>
          <td>容量與值班模型重整</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT</a></td>
          <td>驗證方法轉換</td>
          <td><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.5</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動操作</a></td>
          <td>告警治理轉換</td>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.13</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 MySQL Topology Incident</a></td>
          <td>跨區 DB 拓撲決策轉換</td>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">Reddit 2023 Kubernetes 升級事故</a></td>
          <td>平台升級失敗模式</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">Discord 2022 Gateway 容量事件</a></td>
          <td>容量與連線模型調整</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage</a></td>
          <td>規則系統推送模型調整</td>
          <td><a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">8.13</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">Cloudflare 2023 Control Plane Token Incident</a></td>
          <td>控制面信任邊界重整</td>
          <td><a href="/blog/backend/07-security-data-protection/security-control-handoff-to-delivery-and-incident/" data-link-title="7.18 資安控制面如何交接到部署與事故流程" data-link-desc="建立資安控制面交接到部署、可靠性與事故流程的大綱">7.12</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">Fastly 2021 全域 Edge 配置事故</a></td>
          <td>配置發布流程轉換</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">AWS S3 2017 US-EAST-1 事件</a></td>
          <td>控制面操作模型重整</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">Atlassian 2022 多租戶刪除事故</a></td>
          <td>tenant 安全邊界重整</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">Azure AD 2021 身分控制面事件</a></td>
          <td>身分服務依賴治理</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 多服務網路擁塞事件</a></td>
          <td>區域網路依賴重整</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">Heroku 2021 Routing 控制事件</a></td>
          <td>路由控制面恢復策略</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<p>這兩層合計 42 個案例。使用方式是先在 A 層找轉換動機，再到 B 層找可操作證據與失敗模式，最後回寫到 <code>01/04/06/08</code> 的正文。</p>
<h2 id="跨分類覆蓋與缺口">跨分類覆蓋與缺口</h2>
<p>這一段的核心責任是避免案例池被資料庫議題主導。選型與轉換在實務上會同時涉及快取、訊息傳遞、觀測、部署、安全與事故治理，因此案例覆蓋要跨分類配置。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>目前案例密度</th>
          <th>代表案例入口</th>
          <th>目前缺口與補查方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>01 Database / Storage</td>
          <td>高</td>
          <td><a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a></td>
          <td>已有遷移流程與 rollout evidence；下一步補更多 vendor 轉換對照</td>
      </tr>
      <tr>
          <td>02 Cache / Redis</td>
          <td>中低</td>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest：快取可靠性與容量驚奇治理</a></td>
          <td>補「快取策略轉換」案例（cache-aside -&gt; write-through、multi-layer cache）</td>
      </tr>
      <tr>
          <td>03 Message Queue</td>
          <td>中低</td>
          <td><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon：Shuffle Sharding 與 Cell 邊界</a></td>
          <td>補「自管 broker -&gt; managed queue」與「語義轉換（at-least-once / exactly-once）」</td>
      </tr>
      <tr>
          <td>04 Observability</td>
          <td>中</td>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動操作</a></td>
          <td>補「監控平台遷移」與「OpenTelemetry 導入遷移」案例</td>
      </tr>
      <tr>
          <td>05 Deployment Platform</td>
          <td>中</td>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">Reddit：2023 Kubernetes 升級事故</a></td>
          <td>補「自建部署 -&gt; Kubernetes/GitOps」轉換案例</td>
      </tr>
      <tr>
          <td>06 Reliability</td>
          <td>高</td>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>持續補不同產業的 rollout/rollback 對照</td>
      </tr>
      <tr>
          <td>07 Security / Data Protection</td>
          <td>中低</td>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">Cloudflare 2023 Control Plane Token Incident</a></td>
          <td>補「憑證、金鑰、身分邊界治理轉換」案例</td>
      </tr>
      <tr>
          <td>08 Incident Response</td>
          <td>高</td>
          <td><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 MySQL Topology Incident</a></td>
          <td>補「轉換期間事故」專題，建立遷移失敗模式索引</td>
      </tr>
  </tbody>
</table>
<h2 id="覆蓋門檻與缺口追蹤">覆蓋門檻與缺口追蹤</h2>
<p>這份追蹤表的核心責任是把「案例夠不夠」變成可量化判斷，而不是主觀感覺。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>最低門檻（篇）</th>
          <th>目前已收錄（篇）</th>
          <th>缺口（篇）</th>
          <th>狀態</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>01 Database / Storage</td>
          <td>12</td>
          <td>12</td>
          <td>0</td>
          <td>達標</td>
          <td>補 vendor 轉換對照深度</td>
      </tr>
      <tr>
          <td>02 Cache / Redis</td>
          <td>10</td>
          <td>10</td>
          <td>0</td>
          <td>達標</td>
          <td>進入案例深度擴寫與反例補充</td>
      </tr>
      <tr>
          <td>03 Message Queue</td>
          <td>10</td>
          <td>10</td>
          <td>0</td>
          <td>達標</td>
          <td>進入案例深度擴寫與反例補充</td>
      </tr>
      <tr>
          <td>04 Observability</td>
          <td>10</td>
          <td>10</td>
          <td>0</td>
          <td>達標</td>
          <td>進入案例深度擴寫與反例補充</td>
      </tr>
      <tr>
          <td>05 Deployment Platform</td>
          <td>10</td>
          <td>10</td>
          <td>0</td>
          <td>達標</td>
          <td>進入案例深度擴寫與反例補充</td>
      </tr>
      <tr>
          <td>06 Reliability</td>
          <td>10</td>
          <td>12</td>
          <td>0</td>
          <td>達標</td>
          <td>補產業多樣性與 rollback 成本對照</td>
      </tr>
      <tr>
          <td>07 Security / Data Protection</td>
          <td>10</td>
          <td>10</td>
          <td>0</td>
          <td>達標</td>
          <td>進入案例深度擴寫與反例補充</td>
      </tr>
      <tr>
          <td>08 Incident Response</td>
          <td>10</td>
          <td>12</td>
          <td>0</td>
          <td>達標</td>
          <td>補「轉換期間事故」專題索引</td>
      </tr>
  </tbody>
</table>
<h2 id="下一輪優先順序">下一輪優先順序</h2>
<p>門檻已達標，下一輪優先順序改為：</p>
<ol>
<li>每分類補「失敗反例」與「轉換失敗回退案例」</li>
<li>每分類補「同議題不同規模企業」對照</li>
<li>把案例回寫到章節正文中的判讀訊號與 tripwire 欄位</li>
</ol>
<h2 id="回退失敗專題索引">回退失敗專題索引</h2>
<p>這個索引的核心責任是讓讀者在「已經出錯」時，能快速找到對應回退失敗模式，而不是從頭重讀選型章節。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>回退失敗專題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>02 Cache / Redis</td>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：快取切換失敗</a></td>
      </tr>
      <tr>
          <td>03 Message Queue</td>
          <td><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義切換失敗</a></td>
      </tr>
      <tr>
          <td>04 Observability</td>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 反例：OTel 訊號漂移</a></td>
      </tr>
      <tr>
          <td>05 Deployment Platform</td>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：切流未先 drain</a></td>
      </tr>
      <tr>
          <td>07 Security / Data Protection</td>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">7.C9 反例：憑證輪替失敗</a></td>
      </tr>
  </tbody>
</table>
<h2 id="回退判讀寫法">回退判讀寫法</h2>
<p>回退判讀的核心責任是把失敗條件寫回該分類自己的業務語境。快取看的是回源壓力與資料新鮮度；queue 看的是語義、lag 與重播；observability 看的是訊號語意漂移；deployment 看的是切流、draining 與連線生命週期；security 看的是身份、憑證作用域與控制面擴散。</p>
<p>這些判讀不能抽成同一份模板。每次寫案例時，先回答該分類自己的問題：哪個業務路徑受影響、哪個訊號最早失真、哪個回退動作會降低傷害、哪份證據能證明回退有效。</p>
<h2 id="下一輪補查清單非-db-優先">下一輪補查清單（非 DB 優先）</h2>
<p>下一輪補查會優先補目前中低密度分類，目標是讓每一類至少有 8 到 12 個可回寫案例。</p>
<ol>
<li>Cache：快取策略遷移與失效治理（multi-layer、eviction、warmup）</li>
<li>Queue：broker/語義轉換與 replay 風險控制</li>
<li>Observability：監控平台遷移與資料品質治理</li>
<li>Deployment：部署平台轉換與灰度/回滾策略</li>
<li>Security：控制面信任邊界與憑證機制轉換</li>
</ol>
<h2 id="第二批外部案例補充非-db-類">第二批外部案例補充（非 DB 類）</h2>
<p>這一批的核心責任是把中低密度分類補到可用水位，讓 <code>02/03/04/05/07</code> 都有可引用的真實轉換案例，而不是只有資料庫案例可用。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>案例</th>
          <th>轉換焦點</th>
          <th>回寫入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cache</td>
          <td>Meta：Cache made consistent</td>
          <td>cache invalidation 一致性治理升級</td>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.1</a></td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>Meta：mcrouter at scale</td>
          <td>單機快取轉成跨區路由層</td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.4</a></td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>Meta：CacheLib + Kangaroo</td>
          <td>DRAM-only 快取轉向 flash-friendly 架構</td>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.5</a></td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>Shopify：Marshal -&gt; MessagePack cache migration</td>
          <td>快取序列化格式遷移與雙軌相容</td>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.1</a></td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>Shopify：Shop App write-through cache</td>
          <td>read-heavy 路徑轉 write-through</td>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.1</a></td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>Meta：FOQS disaster-ready migration</td>
          <td>區域佇列轉全域架構且零停機</td>
          <td><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.3</a></td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>LinkedIn：Running Kafka at Scale</td>
          <td>單叢集使用模式轉 tiered cluster</td>
          <td><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1</a></td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>LinkedIn：TopicGC</td>
          <td>Kafka topic 治理從手動轉自動回收</td>
          <td><a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.2</a></td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>VMware Tanzu CloudHealth：Kafka -&gt; Amazon MSK</td>
          <td>自管 broker 轉 managed streaming</td>
          <td><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1</a></td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>Slack：Scaling job queue</td>
          <td>背景工作通道轉 Kafka + Redis 組合</td>
          <td><a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.4</a></td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>AWS：X-Ray SDK/Daemon -&gt; OpenTelemetry migration</td>
          <td>vendor SDK 轉 OTel 標準化</td>
          <td><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.21</a></td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>Google Cloud：OTLP support in Cloud Trace (2025)</td>
          <td>專有 ingest 轉 OTLP 標準入口</td>
          <td><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.21</a></td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>AWS：ADOT 建立集中觀測平台</td>
          <td>多代理轉單一 OTel pipeline</td>
          <td><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>AWS：EKS + ADOT + X-Ray/CloudWatch</td>
          <td>既有監控拆散轉標準化管線</td>
          <td><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.7</a></td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>Honeycomb：Burn rate operations</td>
          <td>告警規則轉 error budget 驅動治理</td>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.13</a></td>
      </tr>
      <tr>
          <td>Deployment</td>
          <td>Tradeshift：self-hosted K8s -&gt; EKS (zero downtime)</td>
          <td>自管控制面轉 managed control plane</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td>Deployment</td>
          <td>Condé Nast：K8s platform modernization on EKS</td>
          <td>多團隊異質集群轉統一平台</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td>Deployment</td>
          <td>Orbitera：AWS -&gt; GKE migration</td>
          <td>基礎平台重置與容器編排轉換</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td>Deployment</td>
          <td>Mobileye：workloads -&gt; EKS</td>
          <td>資源調度模式轉 managed K8s</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td>Deployment</td>
          <td>Miro：microservices/K8s -&gt; EKS managed</td>
          <td>自維運平台轉 managed service 組合</td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
      </tr>
      <tr>
          <td>Security/Control Plane</td>
          <td>Cloudflare：2026 route leak incident</td>
          <td>路由政策自動化治理重整</td>
          <td><a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.16</a></td>
      </tr>
      <tr>
          <td>Security/Control Plane</td>
          <td>Cloudflare：2026 BYOIP BGP withdrawal</td>
          <td>控制面變更保護與回退策略</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>Security/Control Plane</td>
          <td>Cloudflare：2023 control-plane token incident</td>
          <td>token 管理邊界與供應鏈信任調整</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.11</a></td>
      </tr>
      <tr>
          <td>Security/Control Plane</td>
          <td>Azure AD：2021 identity control-plane disruption</td>
          <td>身分控制面故障隔離與恢復路由</td>
          <td><a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.8</a></td>
      </tr>
      <tr>
          <td>Security/Control Plane</td>
          <td>Microsoft 365：2023 suite-wide authentication incident</td>
          <td>身分服務相依邊界重整</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
  </tbody>
</table>
<h2 id="第二批補查來源">第二批補查來源</h2>
<ul>
<li>Meta：Cache consistency / mcrouter / CacheLib / Kangaroo / FOQS / MyRocks migration</li>
<li>LinkedIn Engineering：Kafka at scale / TopicGC</li>
<li>AWS：CloudHealth Kafka -&gt; MSK、X-Ray -&gt; OTel migration、ADOT/EKS 實務、EKS 遷移案例</li>
<li>Google Cloud：OTLP in Cloud Trace、Orbitera -&gt; GKE</li>
<li>Shopify Engineering：cache serialization migration、write-through cache</li>
<li>Cloudflare Post-mortem：2023/2026 control-plane 與路由事件</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>延遲分布長尾惡化</td>
          <td>是平均值問題還是尖峰問題</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>重構風險持續升高</td>
          <td>型別/契約是否不足以支撐變更</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>故障常跨服務放大</td>
          <td>架構邊界是否缺乏隔離能力</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
      <tr>
          <td>發布節奏被品質問題拖慢</td>
          <td>問題在語言、工具鏈或架構層</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a></td>
      </tr>
  </tbody>
</table>
<h2 id="轉換決策資料要求">轉換決策資料要求</h2>
<table>
  <thead>
      <tr>
          <th>資料面向</th>
          <th>最低需要的證據</th>
          <th>若缺失會發生什麼事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>成本面</td>
          <td>現況維運成本與轉換成本（人力、基礎設施、機會成本）</td>
          <td>轉換中途停擺或 ROI 判斷失真</td>
      </tr>
      <tr>
          <td>風險面</td>
          <td>故障型態、爆炸半徑、回退時間</td>
          <td>上線後故障放大但無法快速止血</td>
      </tr>
      <tr>
          <td>性能面</td>
          <td>P50/P95/P99、吞吐、尖峰流量下的行為</td>
          <td>只優化平均值，長尾問題仍存在</td>
      </tr>
      <tr>
          <td>組織面</td>
          <td>團隊技能分布、訓練成本、維運責任邊界</td>
          <td>工具換了但組織無法承接</td>
      </tr>
      <tr>
          <td>生命週期面</td>
          <td>依賴版本 EOL、供應商策略、平台相容性</td>
          <td>被動升級，且在最差時機被迫遷移</td>
      </tr>
      <tr>
          <td>遷移可行性面</td>
          <td>雙寫/雙跑策略、灰度範圍、指標切換門檻、回滾條件</td>
          <td>遷移無法分段驗證，風險一次性爆發</td>
      </tr>
  </tbody>
</table>
<h2 id="轉換前要先回答的三個問題">轉換前要先回答的三個問題</h2>
<ol>
<li>現有問題是「局部優化可解」還是「結構性不匹配」？</li>
<li>轉換後的收益是性能、可靠性、開發效率哪一項，如何量化？</li>
<li>遷移期間如何維持雙軌可運行與回退能力？</li>
</ol>
<p>如果三個問題答不清楚，通常代表先做局部治理比全面轉換更穩定。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把「技術新舊」當成轉換理由，容易忽略遷移期成本。可靠做法是先界定症狀與邊界，再決定要換語言、換工具，或只換架構切分方式。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>若問題在執行時特性（延遲抖動、記憶體模型），先回 <a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a> 與 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a>。若是資料庫轉換已進入執行階段，直接進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>；需要把 production migration 寫成 evidence、gate 與 decision log，接 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>；需要放行與回滾治理時，接 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a>；若要看事故層教訓，接 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://slack.engineering/hacklang-at-slack-a-better-php/">Hacklang at Slack: A Better PHP</a>：Slack 說明 PHP 到 Hack 的遷移動機與型別收益。</li>
<li><a href="https://slack.engineering/how-big-technical-changes-happen-at-slack/">How Big Technical Changes Happen at Slack</a>：Slack 逐步遷移與組織推進方式。</li>
<li><a href="https://discord.com/blog/why-discord-is-switching-from-go-to-rust">Why Discord is switching from Go to Rust</a>：Discord 說明 Go→Rust 的延遲與 GC 觀察。</li>
<li><a href="https://slack.engineering/slacks-migration-to-a-cellular-architecture/">Slack’s Migration to a Cellular Architecture</a>：Slack 從單體拓撲轉到 cell 架構的原因。</li>
<li><a href="https://dropbox.tech/application/the-long-awaited-python-3-upgrade-at-dropbox">The Long-Awaited Python 3 Upgrade at Dropbox</a>：Dropbox 的 Python 2 -&gt; 3 遷移動機與推進方式。</li>
<li><a href="https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine">Rewriting the heart of our sync engine</a>：Dropbox 在核心效能路徑重寫的轉換決策脈絡。</li>
<li><a href="https://dropbox.tech/infrastructure/courier-driving-the-first-years-of-grpc">Courier: Driving the first years of gRPC</a>：Dropbox 內部 RPC 到 gRPC 的演進背景。</li>
<li><a href="https://about.gitlab.com/blog/2022/06/02/splitting-database-into-main-and-ci/">Splitting database into Main and CI</a>：GitLab 的資料庫職責拆分案例。</li>
<li><a href="https://www.notion.com/blog/sharding-postgres-at-notion">Sharding Postgres at Notion</a>：Notion 分片遷移與容量壓力背景。</li>
<li><a href="https://shopify.engineering/blogs/engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">Horizontally scaling the Rails backend of Shop App with Vitess</a>：Shopify 導入 Vitess 的原因與方式。</li>
<li><a href="https://shopify.engineering/adopting-sorbet">How Shopify Is Adopting Sorbet</a>：Shopify 在大型 Ruby 程式碼庫導入型別系統。</li>
<li><a href="https://www.figma.com/blog/migrating-figma-to-kubernetes/">Migrating Figma to Kubernetes</a>：Figma 的平台遷移原因與收益。</li>
<li><a href="https://blog.cloudflare.com/rust-nginx-module/">A Rust regex engine in NGINX</a>：Cloudflare 在高效能路徑導入 Rust 的案例。</li>
<li><a href="https://www.uber.com/en-GB/blog/microservice-architecture/">Domain-Oriented Microservice Architecture</a>：Uber 在規模化後重整服務邊界。</li>
<li><a href="https://engineering.fb.com/2016/08/31/core-infra/myrocks-a-space-and-write-optimized-mysql-database/">MyRocks: A space- and write-optimized MySQL database</a>：Meta 導入 MyRocks 的成本與效能動機。</li>
</ul>
]]></content:encoded></item><item><title>Datadog</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/</guid><description>&lt;p>Datadog 是 all-in-one SaaS observability 平台、承擔三個責任：覆蓋 APM / logs / metrics / RUM / synthetics / security / CI visibility 全訊號類型、auto-instrumentation 廣度業界第一、跟 600+ integrations 即插即用。設計取捨偏向「turnkey + 廣度 + integration」、成本是主要取捨點。&lt;/p>
&lt;p>對「想要 turnkey 體驗、不想自管 observability、多訊號類型統一平台、團隊規模可承擔成本」這條路徑、Datadog 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>安裝 Datadog Agent、配置 APM auto-instrumentation&lt;/li>
&lt;li>用 Datadog Logs / Metrics / APM 三大查詢介面&lt;/li>
&lt;li>控制 cost（log indexing / metric cardinality / APM trace sampling）&lt;/li>
&lt;li>寫 Monitor as code（Terraform）&lt;/li>
&lt;li>評估 OTLP ingestion 跟 Datadog SDK 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-datadog-跑起來">最短路徑：5 分鐘把 Datadog 跑起來&lt;/h2>





&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"># 1. 安裝 Agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: DD_API_KEY=&amp;lt;key&amp;gt; DD_SITE=&amp;#34;datadoghq.com&amp;#34; bash -c &amp;#34;$(curl -L ...)&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="c1"># 2. 啟用 APM&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 在 Agent config 加 apm_config.enabled: true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 應用程式加 ddtrace-run / dd-trace-py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證 Agent + APM 上線&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 在 Datadog UI 看 Host map + APM Service List&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="agent-安裝與配置">Agent 安裝與配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>安裝方式：package（apt/yum）/ container / K8s DaemonSet / Lambda extension&lt;/li>
&lt;li>Agent config：core / APM / Logs / NetFlow / SNMP 各 sub-config&lt;/li>
&lt;li>DogStatsD：應用層 custom metrics 入口&lt;/li>
&lt;li>對應指令：&lt;code>datadog-agent status&lt;/code>、&lt;code>/etc/datadog-agent/datadog.yaml&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="apm-自動-instrumentation">APM 自動 instrumentation&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>各語言 tracer：dd-trace-java / dd-trace-py / dd-trace-js / dd-trace-go&lt;/li>
&lt;li>Auto-instrumentation 廣度（業界最廣）&lt;/li>
&lt;li>Service / Resource / Operation 三層 trace 結構&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="logs-配置">Logs 配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>採集方式：Agent 採集 / Fluent Bit / Vector → Datadog&lt;/li>
&lt;li>Indexing vs Archives：indexing 費錢但可查、archives 便宜但只能 rehydrate&lt;/li>
&lt;li>Log Pipeline：parsing / enrichment / sensitive data scrubbing&lt;/li>
&lt;li>對應 cost 控制：indexing rate / retention&lt;/li>
&lt;/ul>
&lt;h3 id="metrics">Metrics&lt;/h3>
&lt;p>子議題:&lt;/p></description><content:encoded><![CDATA[<p>Datadog 是 all-in-one SaaS observability 平台、承擔三個責任：覆蓋 APM / logs / metrics / RUM / synthetics / security / CI visibility 全訊號類型、auto-instrumentation 廣度業界第一、跟 600+ integrations 即插即用。設計取捨偏向「turnkey + 廣度 + integration」、成本是主要取捨點。</p>
<p>對「想要 turnkey 體驗、不想自管 observability、多訊號類型統一平台、團隊規模可承擔成本」這條路徑、Datadog 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>安裝 Datadog Agent、配置 APM auto-instrumentation</li>
<li>用 Datadog Logs / Metrics / APM 三大查詢介面</li>
<li>控制 cost（log indexing / metric cardinality / APM trace sampling）</li>
<li>寫 Monitor as code（Terraform）</li>
<li>評估 OTLP ingestion 跟 Datadog SDK 的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-datadog-跑起來">最短路徑：5 分鐘把 Datadog 跑起來</h2>





<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. 安裝 Agent</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: DD_API_KEY=&lt;key&gt; DD_SITE=&#34;datadoghq.com&#34; bash -c &#34;$(curl -L ...)&#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="c1"># 2. 啟用 APM</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: 在 Agent config 加 apm_config.enabled: true</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: 應用程式加 ddtrace-run / dd-trace-py</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"># 3. 驗證 Agent + APM 上線</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: 在 Datadog UI 看 Host map + APM Service List</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="agent-安裝與配置">Agent 安裝與配置</h3>
<p>子議題：</p>
<ul>
<li>安裝方式：package（apt/yum）/ container / K8s DaemonSet / Lambda extension</li>
<li>Agent config：core / APM / Logs / NetFlow / SNMP 各 sub-config</li>
<li>DogStatsD：應用層 custom metrics 入口</li>
<li>對應指令：<code>datadog-agent status</code>、<code>/etc/datadog-agent/datadog.yaml</code></li>
</ul>
<h3 id="apm-自動-instrumentation">APM 自動 instrumentation</h3>
<p>子議題：</p>
<ul>
<li>各語言 tracer：dd-trace-java / dd-trace-py / dd-trace-js / dd-trace-go</li>
<li>Auto-instrumentation 廣度（業界最廣）</li>
<li>Service / Resource / Operation 三層 trace 結構</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></li>
</ul>
<h3 id="logs-配置">Logs 配置</h3>
<p>子議題：</p>
<ul>
<li>採集方式：Agent 採集 / Fluent Bit / Vector → Datadog</li>
<li>Indexing vs Archives：indexing 費錢但可查、archives 便宜但只能 rehydrate</li>
<li>Log Pipeline：parsing / enrichment / sensitive data scrubbing</li>
<li>對應 cost 控制：indexing rate / retention</li>
</ul>
<h3 id="metrics">Metrics</h3>
<p>子議題:</p>
<ul>
<li>Custom metrics（DogStatsD / Agent / API）</li>
<li>Metric Type：count / gauge / histogram / distribution</li>
<li>Cardinality 控制：每 metric 收 tags 數限制</li>
<li>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming cardinality</a></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="cost-governance-agent-config/">Datadog 成本治理與 Agent 配置</a>：計價模型、custom metrics 成本控制、Agent 部署配置與常見故障</li>
<li><a href="otlp-ingestion-otel-integration/">OTLP Ingestion 與 OTel 整合</a>：Agent OTLP receiver 配置、OTel SDK feature parity、resource mapping 與故障判讀</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="成本治理">成本治理</h3>
<p>子議題：</p>
<ul>
<li>Hosts pricing（vs APM / Logs / Custom Metrics 各自獨立）</li>
<li>Log indexing rate 控制（Exclusion Filters）</li>
<li>Custom metrics 計費（per metric per host）</li>
<li>APM trace sampling</li>
<li>對應 Datadog Usage Attribution</li>
</ul>
<h3 id="otlp-ingestion">OTLP ingestion</h3>
<p>子議題：</p>
<ul>
<li>Datadog Agent 接受 OTLP（gRPC + HTTP）</li>
<li>對 OTel SDK 用戶的優勢（avoid Datadog SDK lock-in）</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></li>
<li>Datadog 自家 SDK vs OTel：feature parity 取捨</li>
</ul>
<h3 id="monitor-as-code">Monitor as code</h3>
<p>子議題：</p>
<ul>
<li>Terraform Datadog provider：dashboard / monitor / SLO / synthetic</li>
<li>跟 IaC pipeline 整合</li>
<li>多環境（dev / staging / prod）配置</li>
</ul>
<h3 id="apm-trace-sampling">APM Trace Sampling</h3>
<p>子議題：</p>
<ul>
<li>Head-based sampling（rate-based）</li>
<li>Tail-based（Datadog 新功能、需 Agent 支援）</li>
<li>Ingestion vs Indexing sampling 兩層</li>
<li>對應 cost 控制</li>
</ul>
<h3 id="rum--synthetics">RUM / Synthetics</h3>
<p>子議題：</p>
<ul>
<li>RUM（Real User Monitoring）：前端用戶體驗</li>
<li>Synthetics：browser test / API test 主動探測</li>
<li>Session Replay</li>
<li>跟 APM 關聯：frontend trace → backend trace</li>
</ul>
<h3 id="security-monitoring">Security Monitoring</h3>
<p>子議題：</p>
<ul>
<li>Cloud SIEM</li>
<li>ASM（Application Security Management、wAF/RASP）</li>
<li>Cloud Security Posture Management</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a> 對照</li>
</ul>
<h2 id="跟-monitoring-模組的分工">跟 Monitoring 模組的分工</h2>
<p>本頁從 server-side APM 平台角度說明 Datadog — agent 部署、cost governance、OTel 遷移、跟 Grafana Stack 的對照。Client-side 的 RUM 體驗（RUM SDK 四種事件、session replay、全棧追蹤的 client 端視角）見 <a href="/blog/monitoring/06-commercial-comparison/datadog-rum/" data-link-title="Datadog RUM" data-link-desc="全棧 APM 的 client-side 觀點 — client action 到 server trace 的完整鏈路追蹤">Monitoring 模組 Datadog RUM</a>。</p>
<p>兩者的交叉點是 trace context — RUM SDK 注入的 trace header 讓 client action 跟 server span 串在同一個 trace。沒有 server-side APM 的團隊用 RUM 也有價值（client-side error + performance），但全棧追蹤需要兩邊都部署。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="agent-連不上-datadog">Agent 連不上 Datadog</h3>
<p>操作原則：先 <code>datadog-agent status</code> 看 connectivity、再看 API key + region。</p>
<h3 id="apm-trace-缺失">APM trace 缺失</h3>
<p>操作原則：trace context propagation 在跨 service / 跨 thread 邊界丟失。</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"># TODO: dd-trace-py debug mode / `DD_TRACE_DEBUG=true`</span></span></span></code></pre></div><h3 id="log-indexing-cost-爆">Log indexing cost 爆</h3>
<p>操作原則：indexed log 量超預期、用 Exclusion Filter 過濾不必要 log。判讀：Datadog Usage page 看每 day indexed log。</p>
<h3 id="custom-metrics-爆預算">Custom metrics 爆預算</h3>
<p>操作原則：每 host 每 metric 計費、cardinality 高（per-user / per-request label）會爆。判讀：Metrics Summary 看 metric volume。</p>
<h3 id="monitor-noise">Monitor noise</h3>
<p>操作原則：alert 太多、低品質、用 Composite Monitor + Recovery / No data threshold。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預算敏感</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（OSS）/ Cloud（cheaper）</td>
      </tr>
      <tr>
          <td>需要 OSS / self-host</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> + <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug 深度</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-only + 成本</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a></td>
      </tr>
      <tr>
          <td>純 error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>多 vendor 標準化</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> + 任一 backend</td>
      </tr>
      <tr>
          <td>Logs full-text 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 dd-trace SDK 完整 API</li>
<li>Datadog UI 操作詳細</li>
<li>Pricing 詳細計算（用 Datadog Usage page）</li>
<li>600+ integrations 各自設定</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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></td>
          <td>OTLP ingestion + SDK 移轉</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Datadog 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Datadog Logs Indexing / Archives 作為審計證據面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming cardinality</a></td>
          <td>Custom metrics cardinality 治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Datadog SDK ↔ OTLP 雙軌語意漂移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>中大型常選 Datadog turnkey</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Datadog 案例</strong>：客戶 cost optimization stories、large scale 部署（Shopify / Coinbase / Zoom 等）engineering blog。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>DragonflyDB</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/</guid><description>&lt;p>DragonflyDB 是 C++ 重寫的 in-memory store、承擔三個責任：Redis / Memcached protocol 相容（drop-in 替換）、shared-nothing 多核架構（充分利用 CPU）、高 memory efficiency。設計取捨偏向「協議相容但效能大幅提升」、宣稱比 Redis 高 25 倍 throughput。授權從 Apache 2.0 改 BSL（Business Source License）、商業使用有限制。&lt;/p>
&lt;p>對「需要極高 single-instance throughput、多核機器希望充分利用 CPU、Redis drop-in 但要 scale up 而非 out」這條路徑、DragonflyDB 是值得評估的替代。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 DragonflyDB、用 redis-cli 驗證 protocol 相容&lt;/li>
&lt;li>評估從 Redis 遷移的相容性風險（unsupported commands）&lt;/li>
&lt;li>看懂 shared-nothing 多核架構跟 Redis I/O thread 的差異&lt;/li>
&lt;li>評估 BSL 授權對你的商業使用影響&lt;/li>
&lt;li>區分 DragonflyDB 跟 Redis Cluster / Garnet / KeyDB 的選用判讀&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-dragonflydb-跑起來">最短路徑：5 分鐘把 DragonflyDB 跑起來&lt;/h2>





&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"># 1. 啟動 DragonflyDB（thread 數預設 = CPU 核數、自動多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name dragonfly -p 6379:6379 &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> docker.dragonflydb.io/dragonflydb/dragonfly
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 redis-cli 驗證（wire-protocol 相容、直接用 redis-cli）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">redis-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">redis-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本與多核：DragonflyDB 回報相容的 redis_version + 自身版本 + thread 數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|dragonfly_version|thread_count|multiplexing_api&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.4.0 ← client library 以此判斷相容性&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># dragonfly_version:df-v1.39.0 ← DragonflyDB 自身版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># thread_count:8 ← 自動對齊 CPU 核數（shared-nothing 多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># multiplexing_api:epoll&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步是 DragonflyDB 跟 Redis 的核心差異證據：&lt;code>thread_count&lt;/code> 自動對齊 CPU 核數、每個 thread 管自己的 partition（shared-nothing），這是它高吞吐的來源；&lt;code>redis_version:7.4.0&lt;/code> 讓既有 Redis client 直接相容、無需改 code。實機驗證於 dragonfly df-v1.39.0、最後檢查日 2026-06-16。實際遷移評估見 &lt;a href="#redis-%e7%9b%b8%e5%ae%b9%e9%82%8a%e7%95%8c">Redis 相容邊界&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>直接用 redis-cli（DragonflyDB 100% wire-protocol 相容）&lt;/li>
&lt;li>所有 Redis client library 自動相容&lt;/li>
&lt;li>沒有 dragonfly-cli、用 INFO 命令確認 server type&lt;/li>
&lt;/ul>
&lt;h3 id="redis-相容邊界">Redis 相容邊界&lt;/h3>
&lt;p>DragonflyDB 相容大多數 Redis commands、但部分行為差異。子議題：&lt;/p></description><content:encoded><![CDATA[<p>DragonflyDB 是 C++ 重寫的 in-memory store、承擔三個責任：Redis / Memcached protocol 相容（drop-in 替換）、shared-nothing 多核架構（充分利用 CPU）、高 memory efficiency。設計取捨偏向「協議相容但效能大幅提升」、宣稱比 Redis 高 25 倍 throughput。授權從 Apache 2.0 改 BSL（Business Source License）、商業使用有限制。</p>
<p>對「需要極高 single-instance throughput、多核機器希望充分利用 CPU、Redis drop-in 但要 scale up 而非 out」這條路徑、DragonflyDB 是值得評估的替代。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 DragonflyDB、用 redis-cli 驗證 protocol 相容</li>
<li>評估從 Redis 遷移的相容性風險（unsupported commands）</li>
<li>看懂 shared-nothing 多核架構跟 Redis I/O thread 的差異</li>
<li>評估 BSL 授權對你的商業使用影響</li>
<li>區分 DragonflyDB 跟 Redis Cluster / Garnet / KeyDB 的選用判讀</li>
</ol>
<h2 id="最短路徑5-分鐘把-dragonflydb-跑起來">最短路徑：5 分鐘把 DragonflyDB 跑起來</h2>





<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. 啟動 DragonflyDB（thread 數預設 = CPU 核數、自動多核）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name dragonfly -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly
</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="c1"># 2. 用 redis-cli 驗證（wire-protocol 相容、直接用 redis-cli）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli SET foo bar    <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli GET foo        <span class="c1"># → bar</span>
</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="c1"># 3. 確認版本與多核：DragonflyDB 回報相容的 redis_version + 自身版本 + thread 數</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|dragonfly_version|thread_count|multiplexing_api&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># redis_version:7.4.0          ← client library 以此判斷相容性</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0 ← DragonflyDB 自身版本</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># thread_count:8               ← 自動對齊 CPU 核數（shared-nothing 多核）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># multiplexing_api:epoll</span></span></span></code></pre></div><p>第三步是 DragonflyDB 跟 Redis 的核心差異證據：<code>thread_count</code> 自動對齊 CPU 核數、每個 thread 管自己的 partition（shared-nothing），這是它高吞吐的來源；<code>redis_version:7.4.0</code> 讓既有 Redis client 直接相容、無需改 code。實機驗證於 dragonfly df-v1.39.0、最後檢查日 2026-06-16。實際遷移評估見 <a href="#redis-%e7%9b%b8%e5%ae%b9%e9%82%8a%e7%95%8c">Redis 相容邊界</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>直接用 redis-cli（DragonflyDB 100% wire-protocol 相容）</li>
<li>所有 Redis client library 自動相容</li>
<li>沒有 dragonfly-cli、用 INFO 命令確認 server type</li>
</ul>
<h3 id="redis-相容邊界">Redis 相容邊界</h3>
<p>DragonflyDB 相容大多數 Redis commands、但部分行為差異。子議題：</p>
<ul>
<li>支援：Core data types / commands / persistence / pub-sub / transactions</li>
<li>注意：部分 Module 不支援（RedisJSON 有自家版、RedisSearch 沒有）</li>
<li>注意：Lua scripting 支援但效能取捨不同</li>
<li>限制：Cluster mode 採 single-instance scale-up、沒有 Redis Cluster mode（單 instance 已能處理 Redis Cluster 規模）</li>
</ul>
<p>對應指令：<code>INFO server</code> 確認 dragonfly version + 配置。</p>
<h3 id="配置與調優">配置與調優</h3>
<p>子議題：</p>
<ul>
<li><code>--threads</code>：thread 數量、預設 CPU core 數</li>
<li><code>--maxmemory</code>：memory limit、行為跟 Redis 類似</li>
<li><code>--cache_mode</code>：傳統 cache 模式 vs DragonflyDB 預設模式</li>
<li><code>--snapshot_cron</code>：snapshot 策略</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="shared-nothing-多核架構">Shared-nothing 多核架構</h3>
<p>子議題：</p>
<ul>
<li>每個 thread 管自己的 partition、no shared state</li>
<li>VLL（Very Lightweight Lock）取代 Redis 的 single-thread model</li>
<li>Hash 分到不同 thread、靠 epoll 跟 io_uring 做 I/O</li>
<li>跟 Redis I/O threads 的對比：Redis 仍 single main thread、只 I/O 多線；DragonflyDB 完全多線</li>
</ul>
<h3 id="memory-efficiency">Memory efficiency</h3>
<p>子議題：</p>
<ul>
<li>用 dashtable（DragonflyDB 自製 hash table）取代 Redis dict</li>
<li>Snapshot 用 fork-less 機制、避免大記憶體 fork 開銷</li>
<li>同樣 dataset 通常比 Redis 省 20-40% memory（依資料形狀）</li>
</ul>
<h3 id="bsl-授權影響">BSL 授權影響</h3>
<p>子議題：</p>
<ul>
<li>BSL（Business Source License）：商業使用受限、4 年後轉 Apache 2.0</li>
<li>限制：不可作為 managed DragonflyDB service 對外提供</li>
<li>內部使用無限制（多數企業場景）</li>
<li>對 SaaS 供應商：要審慎評估</li>
</ul>
<h3 id="跟-keydb--garnet-的對比">跟 KeyDB / Garnet 的對比</h3>
<p>子議題：</p>
<ul>
<li><strong>KeyDB</strong>：Redis fork、multi-threaded、Snap 收購後相對停滯</li>
<li><strong>Garnet</strong>（Microsoft）：研究用、極高 throughput、生態淺</li>
<li><strong>DragonflyDB</strong>：商業化最積極、生態最活躍</li>
</ul>
<h3 id="scale-up-vs-scale-out">Scale-up vs Scale-out</h3>
<p>子議題：</p>
<ul>
<li>DragonflyDB 哲學：single instance 撐到很大規模（廠商宣稱 1TB+ memory / 6.4M QPS）</li>
<li>Redis 哲學：single instance 有上限、靠 Cluster sharding</li>
<li>何時 scale-up 不夠：跨 region / 跨 AZ HA 需求 → 仍需 replica / sentinel</li>
</ul>
<h3 id="從-redis-遷移">從 Redis 遷移</h3>
<p>子議題：</p>
<ul>
<li>評估 module 使用：列出當前 modules、確認 DragonflyDB 對應</li>
<li>評估 Cluster mode 使用：DragonflyDB 不支援 Cluster mode、要評估能否回到 single instance</li>
<li>遷移路徑：replica 模式雙寫 / 直接 cutover</li>
<li>對應 BSL 授權影響評估</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="performance-不如預期">Performance 不如預期</h3>
<p>操作原則：先確認 thread 數對齊 CPU core、再看 memory pressure。</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">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;dragonfly_version|thread_count&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># thread_count:8                ← 對齊 CPU 核數才能發揮多核</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli INFO memory <span class="p">|</span> grep -E <span class="s2">&#34;used_memory:|maxmemory:&#34;</span></span></span></code></pre></div><p>判讀：thread &lt; core → 沒充分利用 CPU；memory &gt; 50% maxmemory → 影響 throughput。</p>
<h3 id="command-不支援">Command 不支援</h3>
<p>操作原則：DragonflyDB 不支援全部 Redis commands、看 dragonflydb.io/docs/api/redis 確認。</p>
<p>判讀路徑：client error「unknown command」→ 確認 DragonflyDB 對應實作狀態。</p>
<h3 id="cluster-mode-client-連不上">Cluster mode client 連不上</h3>
<p>操作原則：DragonflyDB 不支援 Redis Cluster mode、若 client 配置 cluster mode 會連不上。判讀：改回 standalone client config。</p>
<h3 id="module-不可用">Module 不可用</h3>
<p>對應 KeyDB / Garnet 的對照思路：DragonflyDB 自家 modules 偏少、Redis Stack modules 大多沒有 fork。</p>
<h3 id="bsl-授權商業使用問題">BSL 授權商業使用問題</h3>
<p>操作原則：商業使用前審 license terms、若是 managed service 對外提供、需聯絡 DragonflyDB 取得商業 license。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 Redis Cluster mode</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要 OSI 認可開源授權</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要 Redis Stack 完整 modules</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></td>
      </tr>
      <tr>
          <td>純 KV 不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（無 Dragonfly managed）</td>
      </tr>
      <tr>
          <td>Multi-threaded Redis fork</td>
          <td>KeyDB（停滯中）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>DragonflyDB internal 架構細節（dashtable、VLL 等）</li>
<li>BSL 授權法律解讀（請諮詢律師）</li>
<li>各語言 client 完整對應表</li>
<li>詳細 benchmark methodology</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例沿用-redis-compatible-同源案例--待補-dragonflydb-specific-case">直接相關案例（沿用 Redis-compatible 同源案例 + 待補 DragonflyDB-specific case）</h3>
<p>DragonflyDB 2022 年開源、wire-protocol 與 Redis 相容、Redis 上的 cache pattern 案例可作為框架參考。Production case 仍累積中。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 DragonflyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 模式在 DragonflyDB 上行為一致、單 instance 多核可承接更大 throughput</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移 client-side 實作、DragonflyDB 跟 Redis 共用 API、遷移路徑相同</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 DragonflyDB-specific 案例</strong>：早期採用者 benchmark 報告、從 Redis Cluster 收回 single-instance 的遷移案例、BSL 授權實際商業使用評估、multi-core 加速效果的 production 實測。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 DragonflyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>DragonflyDB 擅長 scale-up、中大型 single instance 取代 Redis Cluster 是核心賣點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter 通用、DragonflyDB 行為跟 Redis 一致、多核擴展不會消除 stampede 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>分層 cache 議題對照、DragonflyDB 強調 memory efficiency 取代 flash tier 的部分需求</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>一致性治理框架通用、但 DragonflyDB 無 Cluster mode、shard move 議題不同（單 instance scope）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
<li>回退路徑：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/" data-link-title="DragonflyDB → Redis / Valkey：回退到標準生態的遷移路徑" data-link-desc="從 DragonflyDB 遷回 Redis 或 Valkey，處理 snapshotting → RDB/AOF 差異、HA 架構切換與 Cluster mode 重建的階段化流程">DragonflyDB → Redis/Valkey</a></li>
</ul>
]]></content:encoded></item><item><title>Gatling</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/gatling/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/gatling/</guid><description>&lt;p>Gatling 是 JVM 生態的 load test 工具、承擔三個責任：code-first 強型別 scenario DSL（Scala / Java / Kotlin、編譯期就抓 script bug）、async / non-blocking 引擎（單機高 VU 不靠 thread-per-VU）、Gatling Enterprise 分散式負載與企業 dashboard。設計取捨偏向「強型別 + 高單機 throughput + JVM 既有資產」、跟 k6（JS DX）跟 JMeter（GUI + plugins）的取捨在 dev workflow 跟團隊既有技能。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 Scala / Java / Kotlin DSL 寫 simulation（scenario + injection profile）&lt;/li>
&lt;li>設計 assertion + threshold 接 CI&lt;/li>
&lt;li>用 HAR-driven recording 從瀏覽器抓真實 user flow 起 script&lt;/li>
&lt;li>評估 Gatling Enterprise 分散式 vs OSS 單機高 VU 的取捨&lt;/li>
&lt;li>評估 Gatling vs k6 / JMeter / Locust 的選用條件&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-gatling-跑起來">最短路徑：5 分鐘把 Gatling 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: brew install gatling / 下載 bundle / Maven / sbt plugin&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="c1"># 2. 寫 simulation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: class MySim extends Simulation {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># val httpProtocol = http.baseUrl(&amp;#34;...&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="c1"># val scn = scenario(&amp;#34;...&amp;#34;).exec(http(&amp;#34;get&amp;#34;).get(&amp;#34;/&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="c1"># setUp(scn.inject(rampUsersPerSec(1).to(50).during(60))).protocols(httpProtocol)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># }&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="c1"># 3. 跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: gatling.sh -s MySim / mvn gatling:test / sbt Gatling/test&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="simulation-結構">Simulation 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Simulation&lt;/code> class（一個檔一個 simulation、整個 test 的根）&lt;/li>
&lt;li>&lt;code>scenario(...).exec(...)&lt;/code>（一條 user journey 的步驟序列）&lt;/li>
&lt;li>&lt;code>httpProtocol&lt;/code>（baseUrl / header / acceptedContent / proxy 共用配置）&lt;/li>
&lt;li>&lt;code>feeder&lt;/code>（CSV / JSON / JDBC 餵 data、配合 &lt;code>randomFeeder&lt;/code> / &lt;code>circular&lt;/code>）&lt;/li>
&lt;/ul>
&lt;h3 id="injection-profilevu-注入節奏">Injection profile（VU 注入節奏）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>atOnceUsers(n)&lt;/code>、&lt;code>rampUsers(n).during(t)&lt;/code>、&lt;code>constantUsersPerSec(rate).during(t)&lt;/code>、&lt;code>rampUsersPerSec(a).to(b).during(t)&lt;/code>、&lt;code>heavisideUsers(n).during(t)&lt;/code>&lt;/li>
&lt;li>跟 k6 stages 對照：Gatling 用 injection step composition、k6 用 stages array — 概念近、語法不同&lt;/li>
&lt;li>Closed model（固定 VU）vs Open model（固定 rate）— Gatling 兩者都支援、production 流量多半 open model 更貼近&lt;/li>
&lt;/ul>
&lt;h3 id="assertion--threshold--ci">Assertion + threshold + CI&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Gatling 是 JVM 生態的 load test 工具、承擔三個責任：code-first 強型別 scenario DSL（Scala / Java / Kotlin、編譯期就抓 script bug）、async / non-blocking 引擎（單機高 VU 不靠 thread-per-VU）、Gatling Enterprise 分散式負載與企業 dashboard。設計取捨偏向「強型別 + 高單機 throughput + JVM 既有資產」、跟 k6（JS DX）跟 JMeter（GUI + plugins）的取捨在 dev workflow 跟團隊既有技能。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 Scala / Java / Kotlin DSL 寫 simulation（scenario + injection profile）</li>
<li>設計 assertion + threshold 接 CI</li>
<li>用 HAR-driven recording 從瀏覽器抓真實 user flow 起 script</li>
<li>評估 Gatling Enterprise 分散式 vs OSS 單機高 VU 的取捨</li>
<li>評估 Gatling vs k6 / JMeter / Locust 的選用條件</li>
</ol>
<h2 id="最短路徑5-分鐘把-gatling-跑起來">最短路徑：5 分鐘把 Gatling 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># TODO: brew install gatling / 下載 bundle / Maven / sbt plugin</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="c1"># 2. 寫 simulation</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># TODO: class MySim extends Simulation {</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#         val httpProtocol = http.baseUrl(&#34;...&#34;)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#         val scn = scenario(&#34;...&#34;).exec(http(&#34;get&#34;).get(&#34;/&#34;))</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">#         setUp(scn.inject(rampUsersPerSec(1).to(50).during(60))).protocols(httpProtocol)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">#       }</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="c1"># 3. 跑</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># TODO: gatling.sh -s MySim / mvn gatling:test / sbt Gatling/test</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="simulation-結構">Simulation 結構</h3>
<p>子議題：</p>
<ul>
<li><code>Simulation</code> class（一個檔一個 simulation、整個 test 的根）</li>
<li><code>scenario(...).exec(...)</code>（一條 user journey 的步驟序列）</li>
<li><code>httpProtocol</code>（baseUrl / header / acceptedContent / proxy 共用配置）</li>
<li><code>feeder</code>（CSV / JSON / JDBC 餵 data、配合 <code>randomFeeder</code> / <code>circular</code>）</li>
</ul>
<h3 id="injection-profilevu-注入節奏">Injection profile（VU 注入節奏）</h3>
<p>子議題：</p>
<ul>
<li><code>atOnceUsers(n)</code>、<code>rampUsers(n).during(t)</code>、<code>constantUsersPerSec(rate).during(t)</code>、<code>rampUsersPerSec(a).to(b).during(t)</code>、<code>heavisideUsers(n).during(t)</code></li>
<li>跟 k6 stages 對照：Gatling 用 injection step composition、k6 用 stages array — 概念近、語法不同</li>
<li>Closed model（固定 VU）vs Open model（固定 rate）— Gatling 兩者都支援、production 流量多半 open model 更貼近</li>
</ul>
<h3 id="assertion--threshold--ci">Assertion + threshold + CI</h3>
<p>子議題：</p>
<ul>
<li><code>setUp(...).assertions(global.responseTime.percentile3.lt(500), global.successfulRequests.percent.gt(95))</code></li>
<li>Assertion 失敗時 process exit code 非 0、直接接 CI pass/fail gate</li>
<li>對應 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="har-driven-recording">HAR-driven recording</h3>
<p>子議題：</p>
<ul>
<li>Chrome DevTools 匯出 HAR、<code>gatling-recorder</code> 從 HAR 產 simulation skeleton</li>
<li>適合：複雜 user flow（multi-step checkout / form / login redirect）懶得手寫 script</li>
<li>邊界：recording 出來是 baseline、需手動補 dynamic correlation（CSRF token / session id / form state）</li>
</ul>
<h3 id="gatling-enterprise前-frontline">Gatling Enterprise（前 FrontLine）</h3>
<p>子議題：</p>
<ul>
<li>分散式 load（多 injector node 模擬 100k+ VU）、跨 region traffic source</li>
<li>Web UI 跑 test、看 dashboard、開 trend analysis</li>
<li>接 Git repo 自動 build simulation、跟 CI / Jenkins / GitLab 整合</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a> 的 on-K8s 部署</li>
</ul>
<h3 id="async-engine-跟單機高-vu">Async engine 跟單機高 VU</h3>
<p>子議題：</p>
<ul>
<li>引擎基於 Akka / Netty、non-blocking IO、單 thread 可驅動上千 VU</li>
<li>對比 JMeter thread-per-VU 模型、Gatling 單機 VU 上限可高 10x 起跳</li>
<li>邊界：target service 才是瓶頸時、單機更高 VU 也壓不出更多訊號、要走分散式</li>
</ul>
<h3 id="jvm-tuning">JVM tuning</h3>
<p>子議題：</p>
<ul>
<li>Heap size（<code>-Xms / -Xmx</code>）跟 GC 策略（G1 / ZGC）影響高 VU 穩定性</li>
<li>Connection pool / file descriptor ulimit 是常見卡關點</li>
<li>Container 跑 Gatling 要注意 CPU / memory request 給足</li>
</ul>
<h3 id="從-jmeter-遷移">從 JMeter 遷移</h3>
<p>子議題：</p>
<ul>
<li>JMeter <code>.jmx</code> 沒官方 converter、要人工 port</li>
<li>適合切點：新 simulation 寫 Gatling、舊 <code>.jmx</code> 維護收斂後再評估</li>
<li>對應 <a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a> 「既有 .jmx 資產治理」段</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="單機-vu-上不去">單機 VU 上不去</h3>
<p>操作原則：JVM heap / ulimit / connection pool 三層先排、再看是不是 target service 已是瓶頸（latency 漲、VU 卻沒滿）。</p>
<h3 id="response-time-p99-不穩">Response time p99 不穩</h3>
<p>操作原則：GC pause（看 GC log）/ network jitter / target service warmup 沒做完。Steady-state 量測前要先 ramp-up + soak 5-10 分鐘。</p>
<h3 id="assertion-偶發-fail">Assertion 偶發 fail</h3>
<p>操作原則：threshold 設在 noise level 附近、把 baseline 重跑 3 次抓 p95 區間、再設 threshold 留 buffer。</p>
<h3 id="recording-出來的-script-跑不通">Recording 出來的 script 跑不通</h3>
<p>操作原則：HAR 沒抓到 dynamic value（CSRF / session）、要手動加 <code>check(regex(...).saveAs(...))</code> 把 response 抓出來餵後續 request。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>非 JVM 團隊 / JS DX</td>
          <td><a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a></td>
      </tr>
      <tr>
          <td>Python + 動態 user behavior</td>
          <td><a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a></td>
      </tr>
      <tr>
          <td>GUI 設計 / 既有資產</td>
          <td><a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></td>
      </tr>
      <tr>
          <td>Browser flow load</td>
          <td>k6 browser / Playwright + 自製 load harness</td>
      </tr>
      <tr>
          <td>Cloud managed</td>
          <td>Gatling Enterprise / BlazeMeter / k6 Cloud</td>
      </tr>
      <tr>
          <td>Capacity planning（非 CI）</td>
          <td><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Scala / Kotlin 語言基礎</li>
<li>Gatling DSL 完整 API reference</li>
<li>Gatling Enterprise pricing 跟 deployment model 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity 與 On-call 分層</a></td>
          <td>JVM 服務的 capacity headroom 與 automated load test</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>峰值準備期 scenario-driven load test 的對照組</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Gatling customer case</strong>：金融 / e-commerce 重度 JVM 生態採用 Gatling Enterprise、HAR-driven scenario recording 在 multi-step checkout flow 的實踐。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a>、<a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a>、<a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></li>
<li>下游能力：<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> load test 模組</li>
</ul>
]]></content:encoded></item><item><title>Google Cloud Platform</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/</guid><description>&lt;p>GCP 是全球 anycast + 強控制面整合的代表、Load Balancer / IAM 失效是全球控制面事故的教學標竿。Google 公開的 post-mortem 包含詳細時間線與技術細節、適合作為事故敘事範本。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>全球控制面失效：IAM / Load Balancer 失效如何擴散到所有地區&lt;/li>
&lt;li>配置變更的 blast radius：staged rollout 為何在 L7 LB 變更上難以實施&lt;/li>
&lt;li>Postmortem 結構：Google PIR 的 timeline / impact / root cause / action items 格式&lt;/li>
&lt;li>跨服務依賴：Cloud SQL / GKE / Cloud Build 之間的隱性耦合&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>事件&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Incident #20003&lt;/td>
 &lt;td>Cloud IAM 造成多個 GCP 服務受影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident #20001&lt;/td>
 &lt;td>Cloud IAM 區域性事故與連鎖影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>External ALB incident&lt;/td>
 &lt;td>控制面變更 staged rollout 的限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>下游服務退化案例&lt;/td>
 &lt;td>跨產品的 dependency 暴露&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>GCP 這個案例在講的是全球控制面如何把單一變更擴成跨產品事故。讀者先看懂 LB、IAM 與 identity 依賴的責任，再把 status event 當成 postmortem 與容災設計的入口。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 Load Balancer 或 IAM 出現問題時，故障不會只停在單一產品，而會沿著共享控制面擴散到 YouTube、Drive 或其他下游。當變更需要 staged rollout 時，重點不只是慢，而是能否在全球邊界上保留足夠的驗證空間。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否指出事故是發生在 control plane 還是 data plane&lt;/li>
&lt;li>能否把一個 LB 變更的影響範圍說清楚&lt;/li>
&lt;li>能否在 status page 上對應到具體恢復階段&lt;/li>
&lt;li>能否把 identity 依賴視為跨產品風險&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>GCP 這頁和 Azure AD、AWS S3 是同一組「共享控制面」案例，只是 GCP 更強調全球服務整合。讀者若把這頁和 Cloudflare 一起讀，會更容易看出 staged rollout、identity 依賴與全球路由之間的互相牽制。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>Incident #20003 與 #20001 是 Cloud IAM 影響多服務的直接樣本。&lt;/li>
&lt;li>External ALB incident 顯示全球控制面變更為何需要保留驗證空間。&lt;/li>
&lt;li>LB、IAM 與 identity 依賴是同一條控制面鏈上的不同節點。&lt;/li>
&lt;li>這類樣本適合和 Cloudflare / AWS S3 一起看。&lt;/li>
&lt;li>staged rollout 限制讓 global LB 變更不能只靠局部驗證。&lt;/li>
&lt;li>identity 控制面失效會把下游產品一起拉進事故。&lt;/li>
&lt;li>service health page 的粒度決定客戶能不能快速定位影響範圍。&lt;/li>
&lt;li>global load balancing 讓一個配置錯誤具有跨區同步效應。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://status.cloud.google.com/incident/zall/20003">Google Cloud Status Dashboard: Incident #20003&lt;/a>：Cloud IAM 造成多個 GCP 服務受影響的官方事件摘要。&lt;/li>
&lt;li>&lt;a href="https://status.cloud.google.com/incident/cloud-iam/20001">Google Cloud Status Dashboard: Incident #20001&lt;/a>：Cloud IAM 區域性事故與連鎖影響。&lt;/li>
&lt;li>&lt;a href="https://cloud.google.com/architecture/disaster-recovery">Architecting disaster recovery for cloud infrastructure outages&lt;/a>：Google Cloud 的 LB / IAM / IAP / Identity Platform 容災說明。&lt;/li>
&lt;li>&lt;a href="https://status.cloud.google.com/incidents/4jGVd9eWeezcNwH8cFhU">Google Cloud Service Health: External Application Load Balancer incident&lt;/a>：Cloud Load Balancing 的全球影響案例。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>GCP 是全球 anycast + 強控制面整合的代表、Load Balancer / IAM 失效是全球控制面事故的教學標竿。Google 公開的 post-mortem 包含詳細時間線與技術細節、適合作為事故敘事範本。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>全球控制面失效：IAM / Load Balancer 失效如何擴散到所有地區</li>
<li>配置變更的 blast radius：staged rollout 為何在 L7 LB 變更上難以實施</li>
<li>Postmortem 結構：Google PIR 的 timeline / impact / root cause / action items 格式</li>
<li>跨服務依賴：Cloud SQL / GKE / Cloud Build 之間的隱性耦合</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>事件</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident #20003</td>
          <td>Cloud IAM 造成多個 GCP 服務受影響</td>
      </tr>
      <tr>
          <td>Incident #20001</td>
          <td>Cloud IAM 區域性事故與連鎖影響</td>
      </tr>
      <tr>
          <td>External ALB incident</td>
          <td>控制面變更 staged rollout 的限制</td>
      </tr>
      <tr>
          <td>下游服務退化案例</td>
          <td>跨產品的 dependency 暴露</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>GCP 這個案例在講的是全球控制面如何把單一變更擴成跨產品事故。讀者先看懂 LB、IAM 與 identity 依賴的責任，再把 status event 當成 postmortem 與容災設計的入口。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 Load Balancer 或 IAM 出現問題時，故障不會只停在單一產品，而會沿著共享控制面擴散到 YouTube、Drive 或其他下游。當變更需要 staged rollout 時，重點不只是慢，而是能否在全球邊界上保留足夠的驗證空間。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否指出事故是發生在 control plane 還是 data plane</li>
<li>能否把一個 LB 變更的影響範圍說清楚</li>
<li>能否在 status page 上對應到具體恢復階段</li>
<li>能否把 identity 依賴視為跨產品風險</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>GCP 這頁和 Azure AD、AWS S3 是同一組「共享控制面」案例，只是 GCP 更強調全球服務整合。讀者若把這頁和 Cloudflare 一起讀，會更容易看出 staged rollout、identity 依賴與全球路由之間的互相牽制。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>Incident #20003 與 #20001 是 Cloud IAM 影響多服務的直接樣本。</li>
<li>External ALB incident 顯示全球控制面變更為何需要保留驗證空間。</li>
<li>LB、IAM 與 identity 依賴是同一條控制面鏈上的不同節點。</li>
<li>這類樣本適合和 Cloudflare / AWS S3 一起看。</li>
<li>staged rollout 限制讓 global LB 變更不能只靠局部驗證。</li>
<li>identity 控制面失效會把下游產品一起拉進事故。</li>
<li>service health page 的粒度決定客戶能不能快速定位影響範圍。</li>
<li>global load balancing 讓一個配置錯誤具有跨區同步效應。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://status.cloud.google.com/incident/zall/20003">Google Cloud Status Dashboard: Incident #20003</a>：Cloud IAM 造成多個 GCP 服務受影響的官方事件摘要。</li>
<li><a href="https://status.cloud.google.com/incident/cloud-iam/20001">Google Cloud Status Dashboard: Incident #20001</a>：Cloud IAM 區域性事故與連鎖影響。</li>
<li><a href="https://cloud.google.com/architecture/disaster-recovery">Architecting disaster recovery for cloud infrastructure outages</a>：Google Cloud 的 LB / IAM / IAP / Identity Platform 容災說明。</li>
<li><a href="https://status.cloud.google.com/incidents/4jGVd9eWeezcNwH8cFhU">Google Cloud Service Health: External Application Load Balancer incident</a>：Cloud Load Balancing 的全球影響案例。</li>
</ul>
]]></content:encoded></item><item><title>incident.io</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/</guid><description>&lt;p>incident.io 是 Slack-native IR 平台、承擔三個責任：把 incident lifecycle 整合在 Slack 內（declare / respond / update / close / postmortem）、自動 timeline + action item tracking、後加 on-call 模組整合 paging。設計取捨偏向「Slack-first + lifecycle automation + 一站式」。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>incident.io 設計上把 &lt;em>Slack 當成 IR 工作台&lt;/em>、不需要在事故中切換 dashboard：宣告、角色指派、status update、stakeholder comms、timeline、action item、postmortem 全部在 Slack channel 完成、PM / leadership / customer-facing team 看 Slack 就能跟上節奏。2023 年起加上 incident.io On-call（取代 PagerDuty 的 alerting / schedule / escalation layer），從純 &lt;em>response orchestration&lt;/em> 變成完整 &lt;em>IR + on-call 平台&lt;/em>、減少 PagerDuty + Slack bot 雙系統的 state drift。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> 比、incident.io 是 &lt;em>response-first&lt;/em>、PagerDuty 是 &lt;em>paging-first&lt;/em>；組合使用時 PagerDuty 觸發 → incident.io 開 channel 跑 response、現在 On-call 模組讓 incident.io 也能獨立扛 paging layer。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant&lt;/a> 比、兩者定位接近、差別在 incident.io 偏 &lt;em>opinionated workflow&lt;/em>（流程預設嚴謹、custom 餘地小）、FireHydrant 偏 &lt;em>customizable + Microsoft Teams 友善&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &amp;#43; AI investigation、Slack-native &amp;#43; 200&amp;#43; integration">Rootly&lt;/a> 比、Rootly 強調 no-code workflow builder 跟 AI 補助、incident.io 強調 &lt;em>catalog-driven service ownership&lt;/em> 跟 learning review 結構化。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>整合 incident.io 到 Slack workspace&lt;/li>
&lt;li>配置 incident severity / role / status workflow&lt;/li>
&lt;li>設計 catalog（service / team metadata）&lt;/li>
&lt;li>用 post-incident flow 自動產 postmortem template&lt;/li>
&lt;li>評估 incident.io vs FireHydrant / Rootly、判斷是否要走 On-call 模組合併 PagerDuty&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 incident.io deployment 是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Slack workflow 完整度&lt;/strong>：&lt;code>/incident&lt;/code> declare 後是否自動開 channel、role bot prompt 是否觸發、status update reminder 是否進 Slack（不靠人記憶 cadence）、stakeholder 是否能在不進 incident channel 的前提下追進度（broadcast channel / status page mirror）&lt;/li>
&lt;li>&lt;strong>Incident type 設計&lt;/strong>：severity（SEV1-4）+ incident type（infra / security / customer-facing）+ role 三者是否清楚、severity 定義有沒有歧義（這條是大型 org 最常翻車的地方）&lt;/li>
&lt;li>&lt;strong>Role assignment 跟交接&lt;/strong>：commander / scribe / comms / SME 的角色定義、handoff 時 bot 是否 prompt、長 incident（&amp;gt;4hr）的 commander rotation 是否有 fallback&lt;/li>
&lt;li>&lt;strong>Post-incident learning&lt;/strong>：close 後是否自動產 postmortem skeleton、action item 是否 sync 到 Jira / Linear 並追完成率、learning review 是否在 N 天內走完（不是寫完 postmortem 就結案）&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness&lt;/a> 的待補項目。&lt;/p></description><content:encoded><![CDATA[<p>incident.io 是 Slack-native IR 平台、承擔三個責任：把 incident lifecycle 整合在 Slack 內（declare / respond / update / close / postmortem）、自動 timeline + action item tracking、後加 on-call 模組整合 paging。設計取捨偏向「Slack-first + lifecycle automation + 一站式」。</p>
<h2 id="服務定位">服務定位</h2>
<p>incident.io 設計上把 <em>Slack 當成 IR 工作台</em>、不需要在事故中切換 dashboard：宣告、角色指派、status update、stakeholder comms、timeline、action item、postmortem 全部在 Slack channel 完成、PM / leadership / customer-facing team 看 Slack 就能跟上節奏。2023 年起加上 incident.io On-call（取代 PagerDuty 的 alerting / schedule / escalation layer），從純 <em>response orchestration</em> 變成完整 <em>IR + on-call 平台</em>、減少 PagerDuty + Slack bot 雙系統的 state drift。</p>
<p>跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 比、incident.io 是 <em>response-first</em>、PagerDuty 是 <em>paging-first</em>；組合使用時 PagerDuty 觸發 → incident.io 開 channel 跑 response、現在 On-call 模組讓 incident.io 也能獨立扛 paging layer。跟 <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> 比、兩者定位接近、差別在 incident.io 偏 <em>opinionated workflow</em>（流程預設嚴謹、custom 餘地小）、FireHydrant 偏 <em>customizable + Microsoft Teams 友善</em>。跟 <a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a> 比、Rootly 強調 no-code workflow builder 跟 AI 補助、incident.io 強調 <em>catalog-driven service ownership</em> 跟 learning review 結構化。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>整合 incident.io 到 Slack workspace</li>
<li>配置 incident severity / role / status workflow</li>
<li>設計 catalog（service / team metadata）</li>
<li>用 post-incident flow 自動產 postmortem template</li>
<li>評估 incident.io vs FireHydrant / Rootly、判斷是否要走 On-call 模組合併 PagerDuty</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 incident.io deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Slack workflow 完整度</strong>：<code>/incident</code> declare 後是否自動開 channel、role bot prompt 是否觸發、status update reminder 是否進 Slack（不靠人記憶 cadence）、stakeholder 是否能在不進 incident channel 的前提下追進度（broadcast channel / status page mirror）</li>
<li><strong>Incident type 設計</strong>：severity（SEV1-4）+ incident type（infra / security / customer-facing）+ role 三者是否清楚、severity 定義有沒有歧義（這條是大型 org 最常翻車的地方）</li>
<li><strong>Role assignment 跟交接</strong>：commander / scribe / comms / SME 的角色定義、handoff 時 bot 是否 prompt、長 incident（&gt;4hr）的 commander rotation 是否有 fallback</li>
<li><strong>Post-incident learning</strong>：close 後是否自動產 postmortem skeleton、action item 是否 sync 到 Jira / Linear 並追完成率、learning review 是否在 N 天內走完（不是寫完 postmortem 就結案）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a> 的待補項目。</p>
<h2 id="最短路徑">最短路徑</h2>





<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. Slack install incident.io app</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. /incident declare 建第一個 incident</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. 配置 severity / role</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. close + retrospective</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="slack-workflow">Slack workflow</h3>
<p>子議題：</p>
<ul>
<li><code>/incident</code> slash command</li>
<li>Auto-created channel（#inc-&hellip;）</li>
<li>Role assignment（commander / scribe / comms）</li>
<li>Bot prompts</li>
</ul>
<h3 id="catalog--post-incident-flow">Catalog + Post-incident flow</h3>
<p>子議題：</p>
<ul>
<li>Service / team / customer metadata</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment service ownership</a> 對齊</li>
<li>Auto timeline from Slack</li>
<li>Action item sync 到 Jira / Linear</li>
<li>Postmortem template + learning review</li>
</ul>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>incident.io</th>
          <th>PagerDuty</th>
          <th>FireHydrant</th>
          <th>Rootly</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要 surface</td>
          <td>Slack-native</td>
          <td>Web / mobile app + 通知</td>
          <td>Slack + Microsoft Teams</td>
          <td>Slack 為主</td>
      </tr>
      <tr>
          <td>設計取向</td>
          <td>Opinionated workflow、流程預設嚴謹</td>
          <td>Paging-first、response 較淺</td>
          <td>Customizable workflow、Teams 友善</td>
          <td>No-code workflow builder + AI 補助</td>
      </tr>
      <tr>
          <td>Paging layer</td>
          <td>自家 On-call 模組（2023+）</td>
          <td>業界 paging 標準</td>
          <td>整合 PagerDuty / Opsgenie</td>
          <td>整合 PagerDuty / Opsgenie</td>
      </tr>
      <tr>
          <td>Catalog</td>
          <td>First-class、service ownership 強</td>
          <td>Service directory 較淺</td>
          <td>Functionality + service catalog</td>
          <td>Service catalog 中等</td>
      </tr>
      <tr>
          <td>Learning review</td>
          <td>Structured（內建 review cadence）</td>
          <td>Postmortems by PagerDuty（需另外 enable）</td>
          <td>Retrospectives 工作流</td>
          <td>Retrospectives + AI summary</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Slack-heavy 中型 SaaS、流程要嚴謹</td>
          <td>大型 enterprise、paging-critical</td>
          <td>多 surface（Slack + Teams）、需要 custom 流程</td>
          <td>Slack-heavy、想用 AI 加速 retro / comms 撰寫</td>
      </tr>
  </tbody>
</table>
<p>選 incident.io 的核心訴求：<em>團隊已 Slack-heavy</em>、想要一套 <em>opinionated workflow</em> 把 IR 從「靠經驗」變成「靠流程」、且願意接受 catalog 維護成本換取 ownership clarity。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="workflowscustom-automation">Workflows（custom automation）</h3>
<p>子議題：trigger → condition → action 的低代碼自動化、severity-based auto-page、approval gate、跟外部 API 串接（呼叫 Jira / Linear / Statuspage）。重點是 workflow 進 Git 版控、change review 走 PR、不在 console 直改。</p>
<h3 id="catalogueservice-ownership--dependency">Catalogue（service ownership + dependency）</h3>
<p>子議題：incident.io Catalog 把 service / team / customer / region 等實體建模、incident 宣告時自動帶出 owner team + on-call 名單 + dependent service。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment service ownership</a> 的 service catalog 概念；catalog stale 是常見 anti-pattern、要設 sync source（Backstage / Terraform / IdP group）+ stale alert。</p>
<h3 id="on-call-layer-integration2023">On-call layer integration（2023+）</h3>
<p>子議題：incident.io On-call 取代 PagerDuty 的 schedule + escalation + paging。優勢是 <em>single source of truth</em>（不需要 PagerDuty incident ↔ Slack channel state sync）、缺點是 paging reliability 還在追 PagerDuty 的 multi-region failover 成熟度。遷移時走 <em>parallel run</em>（兩邊都 page）2-4 週再切。</p>
<h3 id="status-page-integration">Status Page integration</h3>
<p>子議題：跟 <a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> / <a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a> 整合、auto-sync incident status 到 public page、避免 SRE 手動雙寫造成 stakeholder 看到的狀態跟內部不一致。</p>
<h3 id="ai-investigation-features2024">AI investigation features（2024+）</h3>
<p>子議題：AI summarizer（自動產 incident summary 給 leadership）、suggested actions、postmortem draft。要當 <em>first draft</em> 不是 <em>source of truth</em>、commander 仍負責最終敘事。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Slack outage 時 fallback</strong>：incident.io 重度依賴 Slack、Slack 自身 outage 時 IR 工作台會跟著掛 — 要預先準備 <em>out-of-band channel</em>（Zoom war room / Google Meet / 手機群組）、commander handoff 流程要寫進 runbook、不能假設 Slack 永遠在</li>
<li><strong>Slack app 沒回應</strong>：bot offline / permission scope 不足 / workspace admin 改了 app 權限 — 檢查 incident.io admin console 的 health status</li>
<li><strong>Incident type 設計過細</strong>：SEV 1-5 + 10 種 type + 20 個 role 結果沒人記得選哪個、宣告時 friction 太高反而延遲 declare — 收斂到 3-4 種 type、severity 限 4 級、role 預設帶入</li>
<li><strong>Incident type 設計過粗</strong>：所有事故都 SEV2、escalation criteria 不明 — 要寫 <em>severity definition doc</em>、附判讀範例（customer-facing impact / data loss risk / blast radius）</li>
<li><strong>Severity 沒對齊</strong>：team severity definition 不一致、設 catalog default + 在 Slack 宣告時 bot 自動 quote 定義</li>
<li><strong>Catalog stale</strong>：service owner 離職沒更新、dependency 改了沒同步 — 要從 IdP group / Terraform / Backstage sync、設 <em>stale threshold</em>（&gt;90 天沒更新就 alert owner team）</li>
<li><strong>Action item drift</strong>：sync to Jira 失敗 / ownership 不明 — 在 close incident 前 bot 強制要求每個 action item 都有 owner + due date + Jira ticket</li>
<li><strong>Postmortem 沒做</strong>：close 後 prompt 沒觸發 / template 太複雜 — 把 template 縮到 5 個必填欄位、其餘 optional、用 AI draft 降低 friction</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Microsoft Teams</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>No-code workflow / AI</td>
          <td><a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></td>
      </tr>
      <tr>
          <td>Paging-first</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
      </tr>
      <tr>
          <td>自建 Slack workflow</td>
          <td>Slack workflow + GitHub Issues / Linear</td>
      </tr>
      <tr>
          <td>Learning-focused</td>
          <td><a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a>（PagerDuty 整合）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Slack app 完整 spec / Custom workflow 細節 / Pricing</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>incident.io 主打 Slack-native IR</strong>：本案例庫尚無直接揭露 incident.io 使用細節的事故；可參照的閱讀脈絡是「以 Slack 為主要協作通道、事故 channel + 公開 status 同步運作」的服務、典型客戶側 profile 是 <em>Slack-heavy 中型 SaaS organization</em>、IR 流程強調 collaboration 跟 learning 而非單純 paging。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack cases</a></td>
          <td>通訊平台失效時 IR channel 的退路設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">Discord cases</a></td>
          <td>即時通訊產品事故的多通道協作節奏（對照素材）</td>
      </tr>
  </tbody>
</table>
<p>待補 candidate：Lightspeed / Linear / Etsy 等 incident.io 公開 customer story。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a>、<a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
]]></content:encoded></item><item><title>nginx</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/</guid><description>&lt;p>nginx 是 HTTP server / reverse proxy / load balancer 的事實標準之一、承擔三個責任：HTTP 7 層處理（reverse proxy / TLS termination / static content）、L4 / L7 load balancing、Kubernetes ingress controller（ingress-nginx）。設計取捨偏向「配置簡單 + 效能穩定 + reload 機制成熟」、跟 envoy 比是靜態 config-driven（無 dynamic xDS）。F5 收購後 nginx Plus 是商業版、社群 fork 有 Freenginx / angie。&lt;/p>
&lt;p>對「HTTP reverse proxy / LB、TLS termination、K8s ingress、API gateway 入門」這條路徑、nginx 是穩定首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 nginx config（server / location / upstream）&lt;/li>
&lt;li>配置 TLS / mTLS + SNI&lt;/li>
&lt;li>設計 rate limiting + connection limit&lt;/li>
&lt;li>部署 ingress-nginx 到 Kubernetes&lt;/li>
&lt;li>評估 nginx vs nginx Plus / OSS fork（Freenginx / angie）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-nginx-跑起來">最短路徑：5 分鐘把 nginx 跑起來&lt;/h2>





&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"># 1. 啟動 nginx（docker）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name nginx-demo -p 80:80 &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> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/nginx.conf:/etc/nginx/nginx.conf:ro&amp;#34;&lt;/span> nginx:stable-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 寫 reverse proxy config（nginx.conf 範例）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;CONF&amp;#39; &amp;gt; nginx.conf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">events { worker_connections 1024; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">http {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> upstream backend {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s"> server app:8080;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> server {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> listen 80;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> location / {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_pass http://backend;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_set_header Host $host;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> proxy_set_header X-Real-IP $remote_addr;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">CONF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. reload + 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">nginx -t &lt;span class="c1"># test config syntax&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">nginx -s reload &lt;span class="c1"># reload without restart（zero-downtime config update）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="nginx-config-設計">nginx config 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>階層：events / http / server / location / upstream&lt;/li>
&lt;li>變數：$host / $remote_addr / $http_&lt;name>&lt;/li>
&lt;li>Include 拆分大 config&lt;/li>
&lt;li>對應指令：&lt;code>nginx -T&lt;/code>（dump full config）、&lt;code>nginx -t&lt;/code>（test）、&lt;code>nginx -s reload&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="reverse-proxy-配置">Reverse proxy 配置&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>nginx 是 HTTP server / reverse proxy / load balancer 的事實標準之一、承擔三個責任：HTTP 7 層處理（reverse proxy / TLS termination / static content）、L4 / L7 load balancing、Kubernetes ingress controller（ingress-nginx）。設計取捨偏向「配置簡單 + 效能穩定 + reload 機制成熟」、跟 envoy 比是靜態 config-driven（無 dynamic xDS）。F5 收購後 nginx Plus 是商業版、社群 fork 有 Freenginx / angie。</p>
<p>對「HTTP reverse proxy / LB、TLS termination、K8s ingress、API gateway 入門」這條路徑、nginx 是穩定首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 nginx config（server / location / upstream）</li>
<li>配置 TLS / mTLS + SNI</li>
<li>設計 rate limiting + connection limit</li>
<li>部署 ingress-nginx 到 Kubernetes</li>
<li>評估 nginx vs nginx Plus / OSS fork（Freenginx / angie）</li>
</ol>
<h2 id="最短路徑5-分鐘把-nginx-跑起來">最短路徑：5 分鐘把 nginx 跑起來</h2>





<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. 啟動 nginx（docker）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name nginx-demo -p 80:80 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/nginx.conf:/etc/nginx/nginx.conf:ro&#34;</span> nginx:stable-alpine
</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="c1"># 2. 寫 reverse proxy config（nginx.conf 範例）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">cat <span class="s">&lt;&lt;&#39;CONF&#39; &gt; nginx.conf
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">events { worker_connections 1024; }
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">http {
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  upstream backend {
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">    server app:8080;
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  server {
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">    listen 80;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">    location / {
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">      proxy_pass http://backend;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">      proxy_set_header Host $host;
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">      proxy_set_header X-Real-IP $remote_addr;
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">CONF</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 3. reload + 驗證</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">nginx -t            <span class="c1"># test config syntax</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">nginx -s reload     <span class="c1"># reload without restart（zero-downtime config update）</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="nginx-config-設計">nginx config 設計</h3>
<p>子議題：</p>
<ul>
<li>階層：events / http / server / location / upstream</li>
<li>變數：$host / $remote_addr / $http_<name></li>
<li>Include 拆分大 config</li>
<li>對應指令：<code>nginx -T</code>（dump full config）、<code>nginx -t</code>（test）、<code>nginx -s reload</code></li>
</ul>
<h3 id="reverse-proxy-配置">Reverse proxy 配置</h3>
<p>子議題：</p>
<ul>
<li>proxy_pass / proxy_set_header / proxy_http_version</li>
<li>proxy_buffering / proxy_request_buffering</li>
<li>upstream load balancing（round_robin / least_conn / ip_hash）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB contract</a></li>
</ul>
<h3 id="tls-termination">TLS termination</h3>
<p>子議題：</p>
<ul>
<li>ssl_certificate / ssl_certificate_key / ssl_protocols</li>
<li>SNI（server_name + listen 443 ssl）</li>
<li>mTLS：ssl_client_certificate + ssl_verify_client</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> TLS 章</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="rate-limiting--connection-limit">Rate limiting / connection limit</h3>
<p>子議題：</p>
<ul>
<li>limit_req_zone + limit_req（leaky bucket）</li>
<li>limit_conn_zone + limit_conn</li>
<li>跟 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">knowledge cards rate-limit</a> 對照</li>
<li>對應威脅建模: <a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">2.6 快取威脅建模</a></li>
</ul>
<h3 id="ingress-nginx-for-kubernetes">ingress-nginx for Kubernetes</h3>
<p>子議題：</p>
<ul>
<li>Helm chart 部署</li>
<li>Ingress resource + Annotations 配置</li>
<li>ConfigMap + Snippets（power users）</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a> / Gateway API 對比</li>
</ul>
<h3 id="openresty--lua-extension">OpenResty / Lua extension</h3>
<p>子議題：</p>
<ul>
<li>OpenResty：nginx + LuaJIT、可寫 Lua handler</li>
<li>ngx_lua: access / content / log phase handler</li>
<li>適合：自訂 auth / dynamic routing</li>
<li>對應 envoy WASM extension 對比</li>
</ul>
<h3 id="nginx-vs-nginx-plus--freenginx--angie">nginx vs nginx Plus / Freenginx / angie</h3>
<p>子議題：</p>
<ul>
<li>nginx OSS（F5 維護）：basic feature</li>
<li>nginx Plus（商業）：active health check / dynamic config API / DNS upstream</li>
<li>Freenginx：2024 社群 fork（不滿 F5 治理）</li>
<li>angie：另一個 fork、多 commercial extension</li>
<li>選擇判讀：dynamic config 重要 → 看 Envoy / Plus；OSS 純社群 → Freenginx / angie</li>
</ul>
<h3 id="performance-tuning">Performance tuning</h3>
<p>子議題：</p>
<ul>
<li>worker_processes / worker_connections</li>
<li>keepalive_timeout / keepalive_requests</li>
<li>sendfile / tcp_nopush / tcp_nodelay</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a> 對照</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="502-bad-gateway">502 Bad Gateway</h3>
<p>操作原則：upstream 不可達 / 回應錯。判讀：<code>error.log</code> + upstream health。</p>
<h3 id="504-gateway-timeout">504 Gateway Timeout</h3>
<p>操作原則：proxy_read_timeout / proxy_send_timeout 超過。判讀：upstream 處理時間 vs timeout 配置。</p>
<h3 id="connection-limit--502-under-load">Connection limit / 502 under load</h3>
<p>操作原則：worker_connections 不夠、ephemeral port 耗盡、upstream keepalive 不對。判讀：<code>netstat</code> + nginx stub_status。</p>
<h3 id="ssl-handshake-failure">SSL handshake failure</h3>
<p>操作原則：cipher / protocol mismatch、cert chain incomplete、SNI 不對。判讀：<code>openssl s_client -connect host:443 -servername host</code>。</p>
<h3 id="reload-不生效">Reload 不生效</h3>
<p>操作原則：<code>nginx -t</code> 先 test、新 worker 起來舊 worker drain。若行為怪、檢查是否拿到舊 listening socket。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dynamic config / xDS</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a>（ALB / NLB）</td>
      </tr>
      <tr>
          <td>L4 為主 / 高吞吐</td>
          <td>HAProxy / NLB</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>API Gateway 進階</td>
          <td>Kong / Tyk / Apigee</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 nginx directive reference</li>
<li>ngx_lua / OpenResty 完整教學</li>
<li>各 distro nginx 版本差異</li>
<li>nginx internal architecture</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 nginx 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>切流時 nginx upstream / ingress-nginx 沒做 graceful drain、長連線跟 5xx 一起放大</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型直接 nginx reverse proxy、中型走 ingress-nginx、大型才考慮 envoy 或 service mesh</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 nginx 案例</strong>：Cloudflare 為何 fork（freenginx）、大規模 ingress-nginx 客戶案例、OpenResty 在 production 的擴展案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a>、<a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a>、<a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a>（TLS / WAF）、<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Streams</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/</guid><description>&lt;p>Redis Streams 是 Redis 5.0 引入的 append-only log data type、承擔三個責任：輕量 event stream（XADD / XREAD）、consumer group 與 pending entries list（XREADGROUP / XACK）、Redis 生態內整合（避免額外引入 Kafka）。設計取捨偏向「跟 Redis 本體生命週期綁定、低延遲 + 記憶體成本、適合中等規模」。Redis vendor 細節見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">02 redis&lt;/a>。&lt;/p>
&lt;p>對「已用 Redis、需要輕量 stream、不想引入額外基礎設施」這條路徑、Redis Streams 是務實選擇。本頁先給最短路徑、再展開日常 XADD/XREAD 操作與 consumer group 設計、最後進階治理（PEL、retention、Cluster 影響）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 redis-cli XADD / XREAD 操作 stream&lt;/li>
&lt;li>設計 consumer group + XCLAIM 處理 consumer 失敗的訊息接管&lt;/li>
&lt;li>看懂 pending entries list（PEL）累積訊號、定位 consumer 健康&lt;/li>
&lt;li>設計 MAXLEN / MINID retention 對齊記憶體預算&lt;/li>
&lt;li>評估 Redis Cluster 對 Streams 的影響與限制&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-redis-streams-跑起來">最短路徑：5 分鐘把 Redis Streams 跑起來&lt;/h2>





&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"># 1. 啟動 Redis（已有 Redis 跳過）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name redis -p 6379:6379 redis:7
&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"># 2. XADD 寫入 stream（&amp;#39;*&amp;#39; 由 Redis 產生遞增 entry ID）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> redis redis-cli XADD mystream &lt;span class="s1">&amp;#39;*&amp;#39;&lt;/span> field1 value1
&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="c1"># 3. XREAD 讀取（從 0 起讀、最多 10 筆）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> redis redis-cli XREAD COUNT &lt;span class="m">10&lt;/span> STREAMS mystream &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 4. 建 consumer group 後用 group 模式讀（&amp;#39;&amp;gt;&amp;#39; 取未投遞訊息、進 PEL 等 ack）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> redis redis-cli XGROUP CREATE mystream mygroup &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> redis redis-cli XREADGROUP GROUP mygroup consumer1 COUNT &lt;span class="m">10&lt;/span> STREAMS mystream &lt;span class="s1">&amp;#39;&amp;gt;&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Redis 起來、stream 能寫能讀」。實際用 consumer group 場景見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="xadd--xread--xreadgroup">XADD / XREAD / XREADGROUP&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>XADD：寫入 entry、&lt;code>*&lt;/code> 自動 ID vs 手動 ID&lt;/li>
&lt;li>XREAD：簡單讀取（無 consumer group、適合單 consumer）&lt;/li>
&lt;li>XREADGROUP：consumer group 模式、配合 ACK&lt;/li>
&lt;li>對應指令範例：&lt;code>XADD&lt;/code>、&lt;code>XREAD&lt;/code>、&lt;code>XREADGROUP&lt;/code>、&lt;code>XACK&lt;/code>、&lt;code>XPENDING&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="consumer-group-與-pel">Consumer group 與 PEL&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group&lt;/a> 是 Streams 的核心抽象、配合 Pending Entries List（PEL）追蹤未 ack 訊息。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Redis Streams 是 Redis 5.0 引入的 append-only log data type、承擔三個責任：輕量 event stream（XADD / XREAD）、consumer group 與 pending entries list（XREADGROUP / XACK）、Redis 生態內整合（避免額外引入 Kafka）。設計取捨偏向「跟 Redis 本體生命週期綁定、低延遲 + 記憶體成本、適合中等規模」。Redis vendor 細節見 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">02 redis</a>。</p>
<p>對「已用 Redis、需要輕量 stream、不想引入額外基礎設施」這條路徑、Redis Streams 是務實選擇。本頁先給最短路徑、再展開日常 XADD/XREAD 操作與 consumer group 設計、最後進階治理（PEL、retention、Cluster 影響）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 redis-cli XADD / XREAD 操作 stream</li>
<li>設計 consumer group + XCLAIM 處理 consumer 失敗的訊息接管</li>
<li>看懂 pending entries list（PEL）累積訊號、定位 consumer 健康</li>
<li>設計 MAXLEN / MINID retention 對齊記憶體預算</li>
<li>評估 Redis Cluster 對 Streams 的影響與限制</li>
</ol>
<h2 id="最短路徑5-分鐘把-redis-streams-跑起來">最短路徑：5 分鐘把 Redis Streams 跑起來</h2>





<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. 啟動 Redis（已有 Redis 跳過）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name redis -p 6379:6379 redis:7
</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"># 2. XADD 寫入 stream（&#39;*&#39; 由 Redis 產生遞增 entry ID）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">docker <span class="nb">exec</span> redis redis-cli XADD mystream <span class="s1">&#39;*&#39;</span> field1 value1
</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="c1"># 3. XREAD 讀取（從 0 起讀、最多 10 筆）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">docker <span class="nb">exec</span> redis redis-cli XREAD COUNT <span class="m">10</span> STREAMS mystream <span class="m">0</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="c1"># 4. 建 consumer group 後用 group 模式讀（&#39;&gt;&#39; 取未投遞訊息、進 PEL 等 ack）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">docker <span class="nb">exec</span> redis redis-cli XGROUP CREATE mystream mygroup <span class="m">0</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">docker <span class="nb">exec</span> redis redis-cli XREADGROUP GROUP mygroup consumer1 COUNT <span class="m">10</span> STREAMS mystream <span class="s1">&#39;&gt;&#39;</span></span></span></code></pre></div><p>最短路徑驗證「Redis 起來、stream 能寫能讀」。實際用 consumer group 場景見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="xadd--xread--xreadgroup">XADD / XREAD / XREADGROUP</h3>
<p>子議題：</p>
<ul>
<li>XADD：寫入 entry、<code>*</code> 自動 ID vs 手動 ID</li>
<li>XREAD：簡單讀取（無 consumer group、適合單 consumer）</li>
<li>XREADGROUP：consumer group 模式、配合 ACK</li>
<li>對應指令範例：<code>XADD</code>、<code>XREAD</code>、<code>XREADGROUP</code>、<code>XACK</code>、<code>XPENDING</code></li>
</ul>
<h3 id="consumer-group-與-pel">Consumer group 與 PEL</h3>
<p><a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a> 是 Streams 的核心抽象、配合 Pending Entries List（PEL）追蹤未 ack 訊息。子議題：</p>
<ul>
<li>XGROUP CREATE / SETID / DESTROY</li>
<li>XACK：明確 ack</li>
<li>XPENDING：查 PEL 狀態</li>
<li>XCLAIM / XAUTOCLAIM：consumer 失敗時接管訊息</li>
</ul>
<h3 id="retentionmaxlen--minid">Retention：MAXLEN / MINID</h3>
<p>子議題：</p>
<ul>
<li>MAXLEN：保留最近 N 個 entry（近似或精確）</li>
<li>MINID：保留 ID 大於某值的 entry</li>
<li>XADD 寫入時帶 MAXLEN（最常用）</li>
<li>XTRIM 手動修剪</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>PEL 失敗接管、retention 與 cluster 影響已展開為 deep article：<a href="xclaim-pel-recovery/">XCLAIM/PEL 失敗接管與 cluster 影響</a>。下列子議題段保留選題判讀入口。</p>
<h3 id="xclaim-與-consumer-失敗接管">XCLAIM 與 consumer 失敗接管</h3>
<p>子議題：</p>
<ul>
<li>Idle time 判讀（min-idle-time 參數）</li>
<li>XAUTOCLAIM（Redis 6.2+、自動接管）</li>
<li>接管後的去重責任（仍需 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>）</li>
</ul>
<h3 id="memory-與-retention-取捨">Memory 與 retention 取捨</h3>
<p>子議題：</p>
<ul>
<li>Stream 佔用 Redis 記憶體、MAXLEN 是主要旋鈕</li>
<li>近似修剪（<code>~</code> 標記）vs 精確修剪的性能差異</li>
<li>配合 <code>maxmemory-policy</code> 與 eviction（注意 stream 不會被 eviction）</li>
</ul>
<h3 id="redis-cluster-對-streams-的影響">Redis Cluster 對 Streams 的影響</h3>
<p>子議題：</p>
<ul>
<li>Stream key 只在單一 shard（無 partition 概念）</li>
<li>多 stream 跨 shard 的設計（用 hash tag 控制分布）</li>
<li>Cluster failover 對 PEL 一致性的影響</li>
</ul>
<h3 id="stream--functionsredis-7">Stream + Functions（Redis 7+）</h3>
<p>子議題：</p>
<ul>
<li>Redis Functions（取代 Lua scripting）</li>
<li>Stream 處理寫成 Redis-side function</li>
<li>適用 / 不適用場景</li>
</ul>
<h3 id="redis-sentinel--cluster-對可靠性的影響">Redis Sentinel / Cluster 對可靠性的影響</h3>
<p>子議題：</p>
<ul>
<li>Replication lag 對 Streams 一致性的影響</li>
<li>AOF 與 RDB 對 Stream 持久化的差異</li>
<li>Failover 期間 PEL 是否完整</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="pel-累積xpending-數字持續增長">PEL 累積（XPENDING 數字持續增長）</h3>
<p>操作原則：先看是單一 consumer 還是整 group 都累積、再定位 consumer 失敗 vs ACK 漏寫。</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">redis-cli XPENDING mystream mygroup
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 回傳 PEL 總數 + 每個 consumer 的待 ack 數、定位累積集中在哪個 consumer</span></span></span></code></pre></div><p>判讀路徑：consumer crash 沒 ACK → consumer 慢 → ACK 程式碼漏寫。</p>
<h3 id="memory-pressurestream-佔用過大">Memory pressure（stream 佔用過大）</h3>
<p>操作原則：MAXLEN 沒設或設太大、stream 持續增長。判讀：用 <code>MEMORY USAGE</code> 看 stream 佔用、調整 MAXLEN。</p>
<h3 id="跨-shard-stream-限制">跨 shard stream 限制</h3>
<p>操作原則：Streams 不支援 partition、單 stream 受單 shard 容量限制。設計：用 hash tag 強制分散到多 stream。</p>
<h3 id="consumer-重平衡無原生機制">Consumer 重平衡（無原生機制）</h3>
<p>操作原則：consumer group 沒有自動 rebalance、要手動 XCLAIM 接管。看 idle time 與 XPENDING 判斷該接管哪些。</p>
<h3 id="failover-後-pel-不一致">Failover 後 PEL 不一致</h3>
<p>操作原則：Sentinel / Cluster failover 後、replica 升 primary、PEL 可能不完整。對應 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 語義誤配</a> 的思路。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高吞吐 / 長期 retention</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></td>
      </tr>
      <tr>
          <td>複雜 routing</td>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></td>
      </tr>
      <tr>
          <td>跨節點 stream（partition + replication）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / Pulsar</td>
      </tr>
      <tr>
          <td>輕量 messaging（不需 Redis）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
      </tr>
      <tr>
          <td>Managed queue</td>
          <td><a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS</a> / <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">Pub/Sub</a></td>
      </tr>
      <tr>
          <td>Redis Pub/Sub（fire-and-forget）</td>
          <td>Redis Pub/Sub（同 Redis、不持久化）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Redis 本體運維（見 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">02 cache 模組 redis vendor</a>）</li>
<li>各語言 Redis client 完整 API</li>
<li>Redis Pub/Sub 細節（不是 Streams、語意不同）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="redis-streams-專屬案例c42-c47">Redis Streams 專屬案例（C42-C47）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">3.C42 Bitso Reliable Streams</a></td>
          <td>自建抽象層 + DLQ + idempotency</td>
      </tr>
      <tr>
          <td><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 取代 Kafka</a></td>
          <td>Janitor 自寫 retention / 6 位數 → $1k</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">3.C44 Harness event-driven</a></td>
          <td>XAUTOCLAIM head-of-line / 監控缺口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/" data-link-title="3.C45 Klaxit：Rust &#43; Redis Streams 處理 Heroku Logplex" data-link-desc="Klaxit carpool 用 Redis Streams 處理 Heroku Logplex 匯流、自動偵測修復平台 perf 問題、6 個月 production Rust。">3.C45 Klaxit Rust + Logplex</a></td>
          <td>High-throughput log ingestion / consumer group</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">3.C46 Learning.com 退場</a></td>
          <td>（反例）長期事件儲存因成本與延遲退場</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/" data-link-title="3.C47 PHP 微服務：Redis Streams &#43; S3 hybrid storage" data-link-desc="PHP 雙微服務通訊、Kafka 在 PHP 生態工具薄弱、用 Redis Streams &#43; payload compression &#43; S3 hybrid 處理大訊息。">3.C47 PHP + S3 hybrid</a></td>
          <td>Payload 大小限制 / hybrid storage</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Redis Streams 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Kafka+Redis</a></td>
          <td>多 broker 組合：Kafka 處理量、Redis 處理即時性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>中等規模 / Redis 生態內 / 不跨 shard</td>
      </tr>
  </tbody>
</table>
<p><strong>Stream + Functions / Redis Cluster on Streams 缺直接 customer case</strong>：公開資料多在 single-instance / Sentinel 規模、Cluster 跟 Functions 案例稀薄、撰寫該段時要明示。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>Redis 本體：<a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 cache 模組</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>、<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>下游能力：<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></li>
</ul>
]]></content:encoded></item><item><title>Stripe</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/</guid><description>&lt;p>Stripe 是金流場景的可靠性教學標竿、deploy strategy 與 idempotency 設計是 API platform 的工程典範。教學重點在「金流不可重複扣款 / 不可漏扣款」如何透過工程實踐保證。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Deploy strategy：canary / staged rollout 的實作節奏&lt;/li>
&lt;li>Game Day：Stripe 公開的 game day 設計與運作&lt;/li>
&lt;li>Idempotency Key：API 設計層面的 retry safety&lt;/li>
&lt;li>Increasing reliability：從 99% 到 99.999% 的逐階段工程投資&lt;/li>
&lt;li>Capture the flag：內部紅藍演練（這是 Stripe 自有的、不是套 07 的紅藍）&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Idempotency Key&lt;/td>
 &lt;td>API 重試安全的工程實作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Game Day&lt;/td>
 &lt;td>演練設計、scope、後續 action items&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Canary Deploy&lt;/td>
 &lt;td>rollout 節奏、自動 rollback 條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database online migration&lt;/td>
 &lt;td>高頻交易場景的 schema 變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Monitoring &amp;amp; Alerting&lt;/td>
 &lt;td>金流場景的訊號設計&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1&lt;/a>&lt;/td>
 &lt;td>Idempotency 與零停機遷移&lt;/td>
 &lt;td>把交易重試與資料遷移放在同一套一致性安全模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">S2&lt;/a>&lt;/td>
 &lt;td>Canary Deploy 與 Progressive Rollout&lt;/td>
 &lt;td>用交易指標驅動放行節奏，延遲確認與自動回退&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Stripe 這個案例在講的是交易系統如何把重試、遷移與部署都設計成可回復的操作。讀者先抓 idempotency 與 zero-downtime migration 這兩個原語，再看它們怎麼保護支付流程不被重試與變更放大。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當客戶端會重送請求時，idempotency key 讓 server 能把重試視為同一筆交易。當資料結構需要調整時，零停機遷移則把高風險變更拆成可驗證的小步驟，避免一次把整個 payment path 推到不可回復的狀態。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否讓同一筆請求重送後仍得到同一個結果&lt;/li>
&lt;li>能否把 migration 拆成可觀察、可回滾的小階段&lt;/li>
&lt;li>能否區分 client retry 與 server duplicate processing&lt;/li>
&lt;li>能否把 deploy strategy 和交易一致性放在同一個判準下&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Stripe 的可靠性核心是把交易語義寫進系統邊界，這和 GitHub 的 replication、一樣都在處理「重複動作不能造成雙重結果」的問題。差別在於 Stripe 面對的是金流，容錯成本更高，所以 idempotency 與 zero-downtime migration 會比一般平台更早變成硬要求。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>idempotency key 讓同一筆請求重送後，系統仍能回到相同交易結果。&lt;/li>
&lt;li>zero-downtime migration 把高風險資料變更拆成可驗證的小階段。&lt;/li>
&lt;li>canary deploy 讓交易流量先經過小範圍驗證。&lt;/li>
&lt;li>game day 讓支付與資料遷移的失效路徑先被演練。&lt;/li>
&lt;li>retry semantics 讓 client 重送不會變成雙重扣款。&lt;/li>
&lt;li>monitoring &amp;amp; alerting 讓支付路徑的異常先在訊號層浮出來。&lt;/li>
&lt;li>operational simplicity 讓流程越少分支，越容易守住交易正確性。&lt;/li>
&lt;li>safe deploy strategy 讓變更節奏和風險控制綁在一起。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://stripe.com/blog/idempotency">Designing robust and predictable APIs with idempotency&lt;/a>：idempotency key 與重試安全的官方文章。&lt;/li>
&lt;li>&lt;a href="https://stripe.com/blog/how-stripes-document-databases-supported-99.999-uptime-with-zero-downtime-data-migrations">How Stripe’s document databases supported 99.999% uptime with zero-downtime data migrations&lt;/a>：零停機資料遷移與可靠性投資的官方案例。&lt;/li>
&lt;li>&lt;a href="https://stripe.com/blog/engineering">Stripe Engineering&lt;/a>：Stripe Engineering 內容總入口，補 deploy / CI / reliability 的延伸脈絡。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Stripe 是金流場景的可靠性教學標竿、deploy strategy 與 idempotency 設計是 API platform 的工程典範。教學重點在「金流不可重複扣款 / 不可漏扣款」如何透過工程實踐保證。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Deploy strategy：canary / staged rollout 的實作節奏</li>
<li>Game Day：Stripe 公開的 game day 設計與運作</li>
<li>Idempotency Key：API 設計層面的 retry safety</li>
<li>Increasing reliability：從 99% 到 99.999% 的逐階段工程投資</li>
<li>Capture the flag：內部紅藍演練（這是 Stripe 自有的、不是套 07 的紅藍）</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Idempotency Key</td>
          <td>API 重試安全的工程實作</td>
      </tr>
      <tr>
          <td>Game Day</td>
          <td>演練設計、scope、後續 action items</td>
      </tr>
      <tr>
          <td>Canary Deploy</td>
          <td>rollout 節奏、自動 rollback 條件</td>
      </tr>
      <tr>
          <td>Database online migration</td>
          <td>高頻交易場景的 schema 變更</td>
      </tr>
      <tr>
          <td>Monitoring &amp; Alerting</td>
          <td>金流場景的訊號設計</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1</a></td>
          <td>Idempotency 與零停機遷移</td>
          <td>把交易重試與資料遷移放在同一套一致性安全模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">S2</a></td>
          <td>Canary Deploy 與 Progressive Rollout</td>
          <td>用交易指標驅動放行節奏，延遲確認與自動回退</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Stripe 這個案例在講的是交易系統如何把重試、遷移與部署都設計成可回復的操作。讀者先抓 idempotency 與 zero-downtime migration 這兩個原語，再看它們怎麼保護支付流程不被重試與變更放大。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當客戶端會重送請求時，idempotency key 讓 server 能把重試視為同一筆交易。當資料結構需要調整時，零停機遷移則把高風險變更拆成可驗證的小步驟，避免一次把整個 payment path 推到不可回復的狀態。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否讓同一筆請求重送後仍得到同一個結果</li>
<li>能否把 migration 拆成可觀察、可回滾的小階段</li>
<li>能否區分 client retry 與 server duplicate processing</li>
<li>能否把 deploy strategy 和交易一致性放在同一個判準下</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Stripe 的可靠性核心是把交易語義寫進系統邊界，這和 GitHub 的 replication、一樣都在處理「重複動作不能造成雙重結果」的問題。差別在於 Stripe 面對的是金流，容錯成本更高，所以 idempotency 與 zero-downtime migration 會比一般平台更早變成硬要求。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>idempotency key 讓同一筆請求重送後，系統仍能回到相同交易結果。</li>
<li>zero-downtime migration 把高風險資料變更拆成可驗證的小階段。</li>
<li>canary deploy 讓交易流量先經過小範圍驗證。</li>
<li>game day 讓支付與資料遷移的失效路徑先被演練。</li>
<li>retry semantics 讓 client 重送不會變成雙重扣款。</li>
<li>monitoring &amp; alerting 讓支付路徑的異常先在訊號層浮出來。</li>
<li>operational simplicity 讓流程越少分支，越容易守住交易正確性。</li>
<li>safe deploy strategy 讓變更節奏和風險控制綁在一起。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://stripe.com/blog/idempotency">Designing robust and predictable APIs with idempotency</a>：idempotency key 與重試安全的官方文章。</li>
<li><a href="https://stripe.com/blog/how-stripes-document-databases-supported-99.999-uptime-with-zero-downtime-data-migrations">How Stripe’s document databases supported 99.999% uptime with zero-downtime data migrations</a>：零停機資料遷移與可靠性投資的官方案例。</li>
<li><a href="https://stripe.com/blog/engineering">Stripe Engineering</a>：Stripe Engineering 內容總入口，補 deploy / CI / reliability 的延伸脈絡。</li>
</ul>
]]></content:encoded></item><item><title>0.4 操作平台選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/</guid><description>&lt;p>操作平台選型的核心原則是先判斷系統需要哪一種操作能力。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、deployment platform 與 reliability pipeline 都服務於系統運行，但它們回答的問題不同。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分 log、metric、trace、dashboard 與 alert 的用途&lt;/li>
&lt;li>判斷部署平台與可靠性驗證流程解決的問題&lt;/li>
&lt;li>用事故症狀和操作需求判斷應先補哪種平台能力&lt;/li>
&lt;li>把操作平台選型轉成可檢查的工程判斷&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察操作問題會表現成診斷或交付困難">【觀察】操作問題會表現成診斷或交付困難&lt;/h2>
&lt;p>操作平台需求通常來自事故、擴容、發版或維護壓力。當服務在本機可用，但到生產環境後很難診斷、告警、部署或驗證，問題就已經超出語言本身。&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>只知道錯了，看不到上下文&lt;/td>
 &lt;td>操作事件與錯誤脈絡&lt;/td>
 &lt;td>log aggregation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想看趨勢、容量、錯誤率&lt;/td>
 &lt;td>數值訊號與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨服務 request path 不清楚&lt;/td>
 &lt;td>呼叫鏈與延遲拆解&lt;/td>
 &lt;td>tracing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>團隊需要共同看服務健康&lt;/td>
 &lt;td>視覺化與操作入口&lt;/td>
 &lt;td>dashboard&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>問題發生時需要主動通知&lt;/td>
 &lt;td>告警與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>&lt;/td>
 &lt;td>alerting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發版與擴容不穩&lt;/td>
 &lt;td>平台合約與流量入口&lt;/td>
 &lt;td>deployment platform&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想驗證系統能承受壓力與失敗&lt;/td>
 &lt;td>可靠性驗證&lt;/td>
 &lt;td>reliability pipeline&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是索引。每種能力都可以採用不同產品與平台，但第一步是判斷你缺的是哪一種操作能力。&lt;/p>
&lt;h2 id="判讀log-aggregation-承擔事件脈絡">【判讀】log aggregation 承擔事件脈絡&lt;/h2>
&lt;p>log aggregation 的核心責任是收集可搜尋的操作事件。當工程師需要知道某個 request、event、worker 或 client 發生了什麼，log 是最直接的診斷入口。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>查某筆訂單 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> 為什麼被拒絕&lt;/li>
&lt;li>查某個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> message 重試了幾次&lt;/li>
&lt;li>查某個 client 連線何時建立、何時斷線&lt;/li>
&lt;/ul>
&lt;p>這類平台的主要風險是欄位不穩定與敏感資料外洩。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 要像 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract&lt;/a> 一樣維持欄位名稱，並在服務輸出前控制 token、payload 與個資。&lt;/p>
&lt;h2 id="判讀metrics-承擔趨勢與容量判斷">【判讀】metrics 承擔趨勢與容量判斷&lt;/h2>
&lt;p>metrics 的核心責任是把服務狀態轉成可聚合的數值。當團隊需要看錯誤率、延遲、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput&lt;/a>、queue lag、goroutine count 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate&lt;/a>，metrics 是主要工具。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>API p95 latency 是否持續上升&lt;/li>
&lt;li>queue lag 是否超過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 處理能力&lt;/li>
&lt;li>Redis &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 是否造成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 增加&lt;/li>
&lt;/ul>
&lt;p>這類平台的主要風險是 cardinality。label 設計要能聚合趨勢，同時避免把 user id、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 這類高基數欄位放進 metric。&lt;/p>
&lt;h2 id="判讀tracing-承擔跨服務路徑">【判讀】tracing 承擔跨服務路徑&lt;/h2>
&lt;p>tracing 的核心責任是把一次 request 或事件處理串成跨服務路徑。當一個操作會經過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-gateway/" data-link-title="API Gateway" data-link-desc="說明外部流量如何先收斂到一層可集中控制的入口">API Gateway&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-routing/" data-link-title="Request Routing" data-link-desc="說明入口流量如何依規則被導向不同服務或處理路徑">Request Routing&lt;/a>、service、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>、queue、worker 和外部 API，trace 可以拆解每一段延遲與錯誤位置。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>checkout request 經過 cart、payment、inventory、shipping 多個服務&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> 進入後觸發 queue，再由 worker 呼叫外部 API&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/bff/" data-link-title="BFF" data-link-desc="說明 Backend for Frontend 如何聚合下游服務並服務特定客戶端">BFF&lt;/a> API 聚合多個下游服務造成延遲不穩&lt;/li>
&lt;/ul>
&lt;p>這類平台的主要風險是 context propagation。服務之間要傳遞 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> context 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a>，否則 trace 會在邊界斷掉。&lt;/p></description><content:encoded><![CDATA[<p>操作平台選型的核心原則是先判斷系統需要哪一種操作能力。<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、deployment platform 與 reliability pipeline 都服務於系統運行，但它們回答的問題不同。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分 log、metric、trace、dashboard 與 alert 的用途</li>
<li>判斷部署平台與可靠性驗證流程解決的問題</li>
<li>用事故症狀和操作需求判斷應先補哪種平台能力</li>
<li>把操作平台選型轉成可檢查的工程判斷</li>
</ol>
<hr>
<h2 id="觀察操作問題會表現成診斷或交付困難">【觀察】操作問題會表現成診斷或交付困難</h2>
<p>操作平台需求通常來自事故、擴容、發版或維護壓力。當服務在本機可用，但到生產環境後很難診斷、告警、部署或驗證，問題就已經超出語言本身。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>代表的工程問題</th>
          <th>優先評估方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只知道錯了，看不到上下文</td>
          <td>操作事件與錯誤脈絡</td>
          <td>log aggregation</td>
      </tr>
      <tr>
          <td>想看趨勢、容量、錯誤率</td>
          <td>數值訊號與 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a></td>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a></td>
      </tr>
      <tr>
          <td>跨服務 request path 不清楚</td>
          <td>呼叫鏈與延遲拆解</td>
          <td>tracing</td>
      </tr>
      <tr>
          <td>團隊需要共同看服務健康</td>
          <td>視覺化與操作入口</td>
          <td>dashboard</td>
      </tr>
      <tr>
          <td>問題發生時需要主動通知</td>
          <td>告警與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a></td>
          <td>alerting</td>
      </tr>
      <tr>
          <td>發版與擴容不穩</td>
          <td>平台合約與流量入口</td>
          <td>deployment platform</td>
      </tr>
      <tr>
          <td>想驗證系統能承受壓力與失敗</td>
          <td>可靠性驗證</td>
          <td>reliability pipeline</td>
      </tr>
  </tbody>
</table>
<p>這張表是索引。每種能力都可以採用不同產品與平台，但第一步是判斷你缺的是哪一種操作能力。</p>
<h2 id="判讀log-aggregation-承擔事件脈絡">【判讀】log aggregation 承擔事件脈絡</h2>
<p>log aggregation 的核心責任是收集可搜尋的操作事件。當工程師需要知道某個 request、event、worker 或 client 發生了什麼，log 是最直接的診斷入口。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>查某筆訂單 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> 為什麼被拒絕</li>
<li>查某個 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> message 重試了幾次</li>
<li>查某個 client 連線何時建立、何時斷線</li>
</ul>
<p>這類平台的主要風險是欄位不穩定與敏感資料外洩。<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 要像 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a> 一樣維持欄位名稱，並在服務輸出前控制 token、payload 與個資。</p>
<h2 id="判讀metrics-承擔趨勢與容量判斷">【判讀】metrics 承擔趨勢與容量判斷</h2>
<p>metrics 的核心責任是把服務狀態轉成可聚合的數值。當團隊需要看錯誤率、延遲、<a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a>、queue lag、goroutine count 或 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate</a>，metrics 是主要工具。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>API p95 latency 是否持續上升</li>
<li>queue lag 是否超過 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 處理能力</li>
<li>Redis <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 是否造成 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 增加</li>
</ul>
<p>這類平台的主要風險是 cardinality。label 設計要能聚合趨勢，同時避免把 user id、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 這類高基數欄位放進 metric。</p>
<h2 id="判讀tracing-承擔跨服務路徑">【判讀】tracing 承擔跨服務路徑</h2>
<p>tracing 的核心責任是把一次 request 或事件處理串成跨服務路徑。當一個操作會經過 <a href="/blog/backend/knowledge-cards/api-gateway/" data-link-title="API Gateway" data-link-desc="說明外部流量如何先收斂到一層可集中控制的入口">API Gateway</a>、<a href="/blog/backend/knowledge-cards/request-routing/" data-link-title="Request Routing" data-link-desc="說明入口流量如何依規則被導向不同服務或處理路徑">Request Routing</a>、service、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、queue、worker 和外部 API，trace 可以拆解每一段延遲與錯誤位置。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>checkout request 經過 cart、payment、inventory、shipping 多個服務</li>
<li><a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> 進入後觸發 queue，再由 worker 呼叫外部 API</li>
<li><a href="/blog/backend/knowledge-cards/bff/" data-link-title="BFF" data-link-desc="說明 Backend for Frontend 如何聚合下游服務並服務特定客戶端">BFF</a> API 聚合多個下游服務造成延遲不穩</li>
</ul>
<p>這類平台的主要風險是 context propagation。服務之間要傳遞 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> context 與 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a>，否則 trace 會在邊界斷掉。</p>
<h2 id="判讀dashboard-與-alert-承擔操作決策">【判讀】dashboard 與 alert 承擔操作決策</h2>
<p>dashboard 的核心責任是讓團隊看見服務健康；alert 的核心責任是把需要動作的異常主動送到負責者面前。兩者應該連到同一套 SLI、SLO 與 runbook。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>API error rate 超過 SLO 時通知 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a></li>
<li>queue lag 超過可接受時間時提示擴容 consumer</li>
<li><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> disconnect rate 在特定地區突然升高</li>
</ul>
<p>這類平台的主要風險是噪音。alert 應對應可執行動作；dashboard 應服務排障與容量判斷，圖表呈現則要服務這些操作目標。</p>
<h2 id="判讀deployment-platform-承擔服務交付">【判讀】deployment platform 承擔服務交付</h2>
<p>deployment platform 的核心責任是讓服務穩定啟動、更新、接流量、擴容與停止。當問題集中在發版、健康檢查、資源限制、流量入口或服務發現，應先評估部署平台能力。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a> 時新版本還沒 ready 就接到流量</li>
<li>pod 被停止時還有 worker 和長連線尚未清理</li>
<li>多個 service instance 需要透過 <a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 與 <a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 協作</li>
</ul>
<p>這類平台的主要風險是程式與平台合約不一致。服務要提供 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">liveness</a>、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 與 resource usage 訊號；平台要根據這些訊號調度流量。</p>
<h2 id="判讀reliability-pipeline-承擔失敗前驗證">【判讀】reliability pipeline 承擔失敗前驗證</h2>
<p>reliability pipeline 的核心責任是在事故前驗證系統承受能力。<a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a>、<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a>、<a href="/blog/backend/knowledge-cards/fuzz-test/" data-link-title="Fuzz Test" data-link-desc="說明用隨機與變異輸入驗證解析器與邊界處理健壯性">fuzz test</a>、<a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test</a> 都屬於可靠性驗證，但它們觀察的風險不同。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>發版前確認 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a> 和 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> 能一起通過</li>
<li>高流量活動前用 <a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a> 驗證容量</li>
<li>對 parser、<a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a> 或 <a href="/blog/backend/knowledge-cards/input-validation/" data-link-title="Input Validation" data-link-desc="說明進入系統的資料如何先被檢查格式、範圍與語意">input validation</a> 做 fuzz campaign</li>
<li>在預備環境演練 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、database、network failure</li>
</ul>
<p>這類流程的主要風險是測試和真實系統脫節。可靠性驗證要對準實際 failure mode，並產出可行的修正或容量決策。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入操作平台實作章節：</p>
<ol>
<li>每種觀測訊號的責任是否明確（log、metric、trace、alert）</li>
<li>告警是否對應可執行動作與 runbook</li>
<li>部署平台與服務合約是否明確（readiness、shutdown、資源限制）</li>
<li>可靠性驗證是否有固定入口（CI、load、chaos）</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04-observability</a></li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05-deployment-platform</a></li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06-reliability</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>操作平台選型要先看團隊缺的是哪種運行能力。需要事件脈絡看 log，需要趨勢看 metrics，需要跨服務路徑看 tracing，需要共同操作入口看 dashboard，需要主動通知看 alert，需要穩定交付看 deployment platform，需要事故前驗證看 reliability pipeline。分類清楚後，產品與工具比較才會有明確目標。</p>
]]></content:encoded></item><item><title>2.4 distributed lock 與租約</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">分散式鎖（distributed lock）&lt;/a>的核心責任是協調跨節點互斥，避免同一資源被重複處理。它解的是協調一致性問題；正式狀態一致性仍由交易邊界或版本控制承擔。&lt;/p>
&lt;h2 id="鎖與租約">鎖與租約&lt;/h2>
&lt;p>分散式鎖通常採租約語意：持鎖者在租約有效期內擁有操作權，租約到期後鎖自動釋放、需重新競爭。租約的存在是為了處理「持鎖者掛掉但沒釋放鎖」這個分散式系統無法避免的情況——沒有租約，一個 crash 的節點會讓鎖永遠卡住。代價是引入時鐘漂移、網路延遲與續租失敗這幾個新風險。&lt;/p>
&lt;p>在 Redis 上，取鎖是一個原子命令：&lt;code>SET lock:order:42 &amp;lt;token&amp;gt; NX PX 30000&lt;/code>。&lt;code>NX&lt;/code> 保證只有 key 不存在時才寫入，這讓「檢查鎖是否被持有」與「取得鎖」變成單一原子操作，避免兩個節點同時判斷「沒人持鎖」後都寫入。&lt;code>PX 30000&lt;/code> 設定 30 秒租約，持鎖者 crash 時鎖會在租約到期後自動消失。&lt;code>&amp;lt;token&amp;gt;&lt;/code> 是每個持鎖者產生的唯一隨機值，它的作用在釋放階段才顯現。&lt;/p>
&lt;p>釋放鎖不能用單純的 &lt;code>DEL lock:order:42&lt;/code>，因為這會誤刪別人的鎖。考慮這個時序：節點 A 取得鎖、處理超過 30 秒、租約到期自動釋放、節點 B 取得同一把鎖；此時 A 終於處理完、執行 &lt;code>DEL&lt;/code>，刪掉的是 B 的鎖。正確的釋放是「比對 token 相同才刪」，而這個 check-and-delete 必須原子，用 Lua script 達成：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">if&lt;/span> &lt;span class="n">redis.call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">KEYS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">ARGV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="kr">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">return&lt;/span> &lt;span class="n">redis.call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;DEL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">KEYS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kr">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kr">return&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ARGV[1]&lt;/code> 帶入持鎖者自己的 token，只有 token 吻合才刪。這把「釋放鎖」從一個盲目的刪除，變成「確認我仍是持鎖者後才釋放」的條件操作。&lt;/p>
&lt;p>租約長度要對著任務耗時分布校準，而非拍一個固定值：租約要明顯長於正常任務的 P99 耗時，避免工作還沒做完租約就過期、引發雙持鎖；但也不能長到讓 crash 的持鎖者把鎖卡住太久。兩個方向夾出一個區間，長尾工作再用 watchdog 補足。&lt;/p>
&lt;p>續租策略要明確：何時續租、續租失敗如何降級。長時間工作會用 watchdog 在租約過半（約 T/2）時用 &lt;code>PEXPIRE&lt;/code> 延長租約，讓鎖跟著工作存活；但 watchdog 也意味著鎖可能被無限延長，需要設一個絕對上限（例如業務超時的數倍）避免一個卡住的工作永久佔用鎖。若只依賴「拿到鎖就安全」的假設、不處理續租失敗，異常時容易產生重複副作用。&lt;/p>
&lt;h2 id="split-brain-與-fencing">split brain 與 fencing&lt;/h2>
&lt;p>split brain 常見於網路分割或 process 暫停（GC stop-the-world、容器被搶占）後恢復。核心問題是租約：節點 A 取得鎖後發生一次長 GC 暫停，暫停期間租約到期、鎖被節點 B 取得，A 從暫停中醒來時仍「以為」自己持有鎖，於是 A 與 B 同時對下游寫入，互斥語意失效。這是基於租約的鎖在自身層無法消除的時序窗口，解法要往下游推——讓擁有正式狀態的那一層成為最終仲裁者，而非期望鎖本身堵住這個窗口。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fencing-token/" data-link-title="Fencing Token" data-link-desc="說明用單調遞增的 token 讓下游拒絕過期持鎖者的寫入，把互斥正確性下沉到資料層">fencing token&lt;/a> 的責任是把這個問題推到下游解決：每次取鎖時發一個單調遞增的 token，持鎖者對下游的每個寫入都帶上這個 token，下游記住「見過的最大 token」並拒絕比它小的寫入。回到上面的時序，A 帶 token 33、B 帶 token 34，當 A 醒來用 token 33 寫入時，下游已經接受過 34，於是拒絕 33。token 的單調遞增可以用 Redis 原子計數器（&lt;code>INCR fence:order:42&lt;/code>）或鎖服務自己維護的自增序號實作，關鍵是取鎖動作本身要保證拿到的序號嚴格遞增。fencing token 讓下游成為仲裁者，鎖只負責減少競爭、不再是唯一的正確性保證。&lt;/p>
&lt;p>若下游無法驗證 fencing token（例如下游是不支援條件寫入的第三方 API），distributed lock 的保護能力會明顯下降——它只能降低衝突機率，無法消除雙寫。這時更穩定的做法是改成資料版本控制或條件更新（&lt;code>WATCH&lt;/code>/&lt;code>MULTI&lt;/code> 的樂觀鎖、資料庫的 &lt;code>UPDATE ... WHERE version = ?&lt;/code>），把互斥下沉到擁有正式狀態的那一層。&lt;/p>
&lt;h2 id="redlock-與單節點的取捨">Redlock 與單節點的取捨&lt;/h2>
&lt;p>單節點 Redis 鎖有一個可用性缺口：持鎖期間 Redis 主節點故障、failover 到還沒同步該鎖的副本時，新主節點上這把鎖不存在，另一個節點能立刻取得，造成雙持鎖。Redis 作者提出的 Redlock 演算法用多個獨立 master（通常 5 個）解這個問題：向所有節點取鎖，取得多數（3/5）且總耗時在租約內才算成功，藉冗餘避免單點 failover 造成的鎖遺失。&lt;/p>
&lt;p>Redlock 是否真的更安全有公開爭論。Martin Kleppmann 的批評指出，Redlock 依賴各節點時鐘不發生大幅跳動，而 GC 暫停與時鐘校正這類事件仍會讓持鎖者醒來時鎖已失效；更進一步，若 NTP 時鐘跳躍發生在取鎖過程中，各節點對租約是否有效的判斷本身就可能出錯，Redlock 賴以成立的多數決計數因此無法可靠排除雙持鎖。也就是說，Redlock 提升了「鎖不會因單節點故障而遺失」的可用性，但沒有解決「持鎖者暫停導致的 split brain」，後者仍需 fencing token。判讀因此落在：鎖只是效率優化（偶爾雙跑代價可接受）時，單節點 Redis 鎖足夠且運維簡單；鎖牽涉正確性（雙跑會造成金錢或資料損壞）時，無論單節點還是 Redlock 都不足以單獨成立，必須有 fencing token 或下游條件寫入兜底。&lt;/p>
&lt;h2 id="何時使用何時轉向">何時使用、何時轉向&lt;/h2>
&lt;p>distributed lock 在「偶爾失效的代價可控」的場景是一個效率優化工具，降低重複工作的機率而非保證互斥。符合這個特徵的場景包括排程任務避免重複執行、單資源批次工作協調、短期臨界區互斥。以 cron job 為例，偶爾被兩個節點同時觸發時，若任務本身 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotent&lt;/a>，重複執行只是浪費資源而非產生錯誤結果，鎖把這類浪費的機率壓低就足夠。&lt;/p>
&lt;p>讀取路徑上避免 cache miss 風暴的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight&lt;/a> 互斥也屬這一類，但租約特性不同：熱門 key 失效時用一把短鎖讓單一請求回源重建快取、其餘請求等結果，偶爾多跑一次回源的代價可控。它的鎖租約通常很短（一次回源的時間），競爭集中在少數熱門 key，與批次任務的長租約、低競爭剖面相反，校準時要分開看。&lt;/p>
&lt;p>高價值交易資料更新則相反，優先使用資料庫交易與唯一約束，將鎖作為輔助而非核心一致性機制。扣款、出貨、配額扣減這類操作，正確性不能依賴「鎖沒失效」這個無法保證的前提，而要靠資料層的唯一約束或版本檢查讓重複操作在最後一刻被擋下。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">分散式鎖（distributed lock）</a>的核心責任是協調跨節點互斥，避免同一資源被重複處理。它解的是協調一致性問題；正式狀態一致性仍由交易邊界或版本控制承擔。</p>
<h2 id="鎖與租約">鎖與租約</h2>
<p>分散式鎖通常採租約語意：持鎖者在租約有效期內擁有操作權，租約到期後鎖自動釋放、需重新競爭。租約的存在是為了處理「持鎖者掛掉但沒釋放鎖」這個分散式系統無法避免的情況——沒有租約，一個 crash 的節點會讓鎖永遠卡住。代價是引入時鐘漂移、網路延遲與續租失敗這幾個新風險。</p>
<p>在 Redis 上，取鎖是一個原子命令：<code>SET lock:order:42 &lt;token&gt; NX PX 30000</code>。<code>NX</code> 保證只有 key 不存在時才寫入，這讓「檢查鎖是否被持有」與「取得鎖」變成單一原子操作，避免兩個節點同時判斷「沒人持鎖」後都寫入。<code>PX 30000</code> 設定 30 秒租約，持鎖者 crash 時鎖會在租約到期後自動消失。<code>&lt;token&gt;</code> 是每個持鎖者產生的唯一隨機值，它的作用在釋放階段才顯現。</p>
<p>釋放鎖不能用單純的 <code>DEL lock:order:42</code>，因為這會誤刪別人的鎖。考慮這個時序：節點 A 取得鎖、處理超過 30 秒、租約到期自動釋放、節點 B 取得同一把鎖；此時 A 終於處理完、執行 <code>DEL</code>，刪掉的是 B 的鎖。正確的釋放是「比對 token 相同才刪」，而這個 check-and-delete 必須原子，用 Lua script 達成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">if</span> <span class="n">redis.call</span><span class="p">(</span><span class="s2">&#34;GET&#34;</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">==</span> <span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">return</span> <span class="n">redis.call</span><span class="p">(</span><span class="s2">&#34;DEL&#34;</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">else</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kr">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">end</span></span></span></code></pre></div><p><code>ARGV[1]</code> 帶入持鎖者自己的 token，只有 token 吻合才刪。這把「釋放鎖」從一個盲目的刪除，變成「確認我仍是持鎖者後才釋放」的條件操作。</p>
<p>租約長度要對著任務耗時分布校準，而非拍一個固定值：租約要明顯長於正常任務的 P99 耗時，避免工作還沒做完租約就過期、引發雙持鎖；但也不能長到讓 crash 的持鎖者把鎖卡住太久。兩個方向夾出一個區間，長尾工作再用 watchdog 補足。</p>
<p>續租策略要明確：何時續租、續租失敗如何降級。長時間工作會用 watchdog 在租約過半（約 T/2）時用 <code>PEXPIRE</code> 延長租約，讓鎖跟著工作存活；但 watchdog 也意味著鎖可能被無限延長，需要設一個絕對上限（例如業務超時的數倍）避免一個卡住的工作永久佔用鎖。若只依賴「拿到鎖就安全」的假設、不處理續租失敗，異常時容易產生重複副作用。</p>
<h2 id="split-brain-與-fencing">split brain 與 fencing</h2>
<p>split brain 常見於網路分割或 process 暫停（GC stop-the-world、容器被搶占）後恢復。核心問題是租約：節點 A 取得鎖後發生一次長 GC 暫停，暫停期間租約到期、鎖被節點 B 取得，A 從暫停中醒來時仍「以為」自己持有鎖，於是 A 與 B 同時對下游寫入，互斥語意失效。這是基於租約的鎖在自身層無法消除的時序窗口，解法要往下游推——讓擁有正式狀態的那一層成為最終仲裁者，而非期望鎖本身堵住這個窗口。</p>
<p><a href="/blog/backend/knowledge-cards/fencing-token/" data-link-title="Fencing Token" data-link-desc="說明用單調遞增的 token 讓下游拒絕過期持鎖者的寫入，把互斥正確性下沉到資料層">fencing token</a> 的責任是把這個問題推到下游解決：每次取鎖時發一個單調遞增的 token，持鎖者對下游的每個寫入都帶上這個 token，下游記住「見過的最大 token」並拒絕比它小的寫入。回到上面的時序，A 帶 token 33、B 帶 token 34，當 A 醒來用 token 33 寫入時，下游已經接受過 34，於是拒絕 33。token 的單調遞增可以用 Redis 原子計數器（<code>INCR fence:order:42</code>）或鎖服務自己維護的自增序號實作，關鍵是取鎖動作本身要保證拿到的序號嚴格遞增。fencing token 讓下游成為仲裁者，鎖只負責減少競爭、不再是唯一的正確性保證。</p>
<p>若下游無法驗證 fencing token（例如下游是不支援條件寫入的第三方 API），distributed lock 的保護能力會明顯下降——它只能降低衝突機率，無法消除雙寫。這時更穩定的做法是改成資料版本控制或條件更新（<code>WATCH</code>/<code>MULTI</code> 的樂觀鎖、資料庫的 <code>UPDATE ... WHERE version = ?</code>），把互斥下沉到擁有正式狀態的那一層。</p>
<h2 id="redlock-與單節點的取捨">Redlock 與單節點的取捨</h2>
<p>單節點 Redis 鎖有一個可用性缺口：持鎖期間 Redis 主節點故障、failover 到還沒同步該鎖的副本時，新主節點上這把鎖不存在，另一個節點能立刻取得，造成雙持鎖。Redis 作者提出的 Redlock 演算法用多個獨立 master（通常 5 個）解這個問題：向所有節點取鎖，取得多數（3/5）且總耗時在租約內才算成功，藉冗餘避免單點 failover 造成的鎖遺失。</p>
<p>Redlock 是否真的更安全有公開爭論。Martin Kleppmann 的批評指出，Redlock 依賴各節點時鐘不發生大幅跳動，而 GC 暫停與時鐘校正這類事件仍會讓持鎖者醒來時鎖已失效；更進一步，若 NTP 時鐘跳躍發生在取鎖過程中，各節點對租約是否有效的判斷本身就可能出錯，Redlock 賴以成立的多數決計數因此無法可靠排除雙持鎖。也就是說，Redlock 提升了「鎖不會因單節點故障而遺失」的可用性，但沒有解決「持鎖者暫停導致的 split brain」，後者仍需 fencing token。判讀因此落在：鎖只是效率優化（偶爾雙跑代價可接受）時，單節點 Redis 鎖足夠且運維簡單；鎖牽涉正確性（雙跑會造成金錢或資料損壞）時，無論單節點還是 Redlock 都不足以單獨成立，必須有 fencing token 或下游條件寫入兜底。</p>
<h2 id="何時使用何時轉向">何時使用、何時轉向</h2>
<p>distributed lock 在「偶爾失效的代價可控」的場景是一個效率優化工具，降低重複工作的機率而非保證互斥。符合這個特徵的場景包括排程任務避免重複執行、單資源批次工作協調、短期臨界區互斥。以 cron job 為例，偶爾被兩個節點同時觸發時，若任務本身 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotent</a>，重複執行只是浪費資源而非產生錯誤結果，鎖把這類浪費的機率壓低就足夠。</p>
<p>讀取路徑上避免 cache miss 風暴的 <a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight</a> 互斥也屬這一類，但租約特性不同：熱門 key 失效時用一把短鎖讓單一請求回源重建快取、其餘請求等結果，偶爾多跑一次回源的代價可控。它的鎖租約通常很短（一次回源的時間），競爭集中在少數熱門 key，與批次任務的長租約、低競爭剖面相反，校準時要分開看。</p>
<p>高價值交易資料更新則相反，優先使用資料庫交易與唯一約束，將鎖作為輔助而非核心一致性機制。扣款、出貨、配額扣減這類操作，正確性不能依賴「鎖沒失效」這個無法保證的前提，而要靠資料層的唯一約束或版本檢查讓重複操作在最後一刻被擋下。</p>
<p>當鎖競爭成為常態、租約續租頻繁失敗、鎖持有時間與業務耗時高度耦合時，代表模型需要轉向分片、隊列化或版本檢查。鎖競爭高通常是粒度設計問題：把單一全域鎖換成依資源分片的細粒度鎖（<code>lock:order:42</code> 而非 <code>lock:orders</code>），讓不相關的資源互不阻塞。若工作本身就是序列化處理一批項目，改用 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a> 的單一 consumer 語意，比用鎖模擬序列更穩定。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鎖等待時間持續拉長</td>
          <td>臨界區過大或熱點資源集中</td>
          <td>縮小臨界區、拆分資源粒度</td>
      </tr>
      <tr>
          <td>續租失敗與重入衝突同時上升</td>
          <td>租約時間與工作耗時不匹配</td>
          <td>重設租約、加入 fencing token</td>
      </tr>
      <tr>
          <td>相同任務重複執行率上升</td>
          <td>鎖語意失效或持鎖者判定漂移</td>
          <td>檢查時鐘與網路、補下游去重</td>
      </tr>
      <tr>
          <td>網路抖動時 split brain 事件增加</td>
          <td>鎖系統與下游防護未對位</td>
          <td>補下游版本檢查、限制高風險操作</td>
      </tr>
      <tr>
          <td>鎖系統穩定但業務仍不一致</td>
          <td>問題層級在資料一致性而非協調層</td>
          <td>回到 transaction/constraint 設計</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把分散式鎖當作通用一致性解法，會讓錯誤責任落在錯誤層級。最常見的具體形狀是「用鎖保護寫入、但讀取路徑不過鎖」：寫入互斥成立了，讀取卻仍可能讀到未提交或 stale 的值，不一致沒有被鎖擋住。鎖負責互斥協調，資料正確性要由資料模型與交易邊界保護，讀寫兩端要納入同一套一致性設計、而非只鎖寫端。</p>
<p>用單純的 <code>DEL</code> 釋放鎖，是最容易在程式碼裡漏掉的一個錯誤。租約到期後鎖可能已被別人取得，盲目 <code>DEL</code> 會誤刪他人的鎖、讓互斥瓦解。釋放一律要走 token 比對的條件刪除。</p>
<p>把 Redlock 或多節點鎖當成正確性保證，是第二個誤區。多節點冗餘提升的是「鎖不會因單點故障遺失」的可用性，不是「持鎖者暫停不會造成雙寫」的正確性。需要正確性時，fencing token 或下游條件寫入才是真正的防線，鎖只是減少競爭。</p>
<p>把租約時間固定為常數，也會在流量波動下放大風險。租約太短會在正常工作未完成時就過期、引發雙持鎖；太長則讓 crash 的持鎖者長時間卡住鎖。租約策略需要和任務耗時分布與錯誤模型一起校準，長尾工作要靠 watchdog 續租而非把租約一律設大。</p>
<h2 id="情境回寫">情境回寫</h2>
<p>分散式鎖失效回寫到真實服務時，最常見的形狀是排程任務的重複執行。一個跨多節點部署的對帳 job 用 distributed lock 確保同一批次只有一個節點處理；當持鎖節點發生長暫停、租約到期被另一節點接手，而暫停節點醒來後仍繼續寫入時，同一批對帳被執行兩次。回寫時先判讀鎖失效來自時序漂移、網路分割還是續租策略，再決定防線往哪裡補。</p>
<p>這個形狀支撐的是「互斥語意在異常下失效」的判讀。若任務本身 idempotent，重複執行只是浪費資源；若會產生重複副作用（重複出帳、重複通知），正確性不能靠鎖，要靠下游的 fencing token 或唯一約束。高風險路徑可接到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a> 做故障演練。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.2 的交接：鎖搭配失效策略回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 1.3 的交接：高價值資料一致性回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 6.20 的交接：鎖失效演練與停損條件回到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 8.19 的交接：鎖衝突與回退判斷回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
<li>與 2.6 的交接：split brain 與鎖失效的弱點可從威脅建模角度重新盤點，回到 <a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">快取威脅建模</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取層一致性與容量壓力，接著讀 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>。要看鎖語意在事故裡的擴散方式，接著讀 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>。</p>
]]></content:encoded></item><item><title>3.4 consumer 設計與去重</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/</guid><description>&lt;p>消費者設計（consumer design）的核心責任是把訊息投遞結果轉成可恢復的業務結果。queue 層提供 delivery 保證，consumer 層提供 processing 與 recovery 保證；三者對齊後，非同步流程才具備可預期性。&lt;/p>
&lt;h2 id="三層語意">三層語意&lt;/h2>
&lt;p>consumer 端需要同時處理三層語意：&lt;/p>
&lt;ol>
&lt;li>delivery semantics：訊息是否被成功投遞與確認，包含 ack/nack、retry、DLQ。&lt;/li>
&lt;li>processing semantics：業務副作用是否可承受重複、亂序與部分失敗。&lt;/li>
&lt;li>recovery semantics：故障後是否能重播、補償與回復到一致狀態。&lt;/li>
&lt;/ol>
&lt;p>這三層拆開後，才能看清問題落在哪一層。訊息送達不代表副作用完成；副作用完成不代表系統可恢復。&lt;/p>
&lt;h2 id="consumer-grouppartition-與順序責任">consumer group、partition 與順序責任&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 定義了並行與順序邊界。順序要求高的流程要把同一鍵值固定在同一 partition；吞吐優先的流程可提高 partition 數並分散處理。&lt;/p>
&lt;p>分區策略會直接影響恢復成本。分區鍵混亂時，重播與補償很難限定範圍，事故期間容易擴大影響面。&lt;/p>
&lt;h2 id="checkpointoffset-與-idempotency">checkpoint、offset 與 idempotency&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 的責任是標記「處理到哪裡」，不是「業務一定完成」。寫 checkpoint 的時機要晚於副作用提交，避免進度前移導致資料遺漏。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> key 的責任是讓重試與重播可重入。付款、發票、通知、庫存變更都需要明確冪等鍵與去重儲存策略，讓「至少一次投遞」不會變成「多次業務結果」。&lt;/p>
&lt;h2 id="replay-safety">replay safety&lt;/h2>
&lt;p>replay safety 的核心是先定義可重播範圍，再定義副作用控制。常見做法包含：&lt;/p>
&lt;ol>
&lt;li>限定 replay window，避免一次重播跨越多個版本邊界。&lt;/li>
&lt;li>將副作用拆成可比對與可補償動作，保留對帳路徑。&lt;/li>
&lt;li>對 replay 期間的下游壓力設置節流與停損條件。&lt;/li>
&lt;/ol>
&lt;p>poison message 要獨立隔離。持續重試同一壞訊息會壓垮整體吞吐，穩定做法是送入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>，再走診斷與修復流程。&lt;/p>
&lt;h2 id="queue-語意誤配是-broker-遷移最常見的失敗模式">Queue 語意誤配是 broker 遷移最常見的失敗模式&lt;/h2>
&lt;p>Broker 遷移失敗的根因通常是 &lt;em>consumer 對舊 broker 行為的隱式依賴&lt;/em>、不是 broker 本身效能。表面上訊息仍被送達、但業務資料開始出現重複扣款、重複寄信、狀態漏更新。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：Queue Semantics Mismatch Cutover&lt;/a> — case 揭露切換後語意誤配三個方向：consumer 依賴特定 offset 行為、依賴特定重試節奏、依賴特定 idempotency 行為。失敗重播時、新系統即使提供相近 delivery semantics、結果可能不同。語意誤配會沿著下游資料寫入擴散、難以靠 queue depth 判斷。&lt;/p>
&lt;p>&lt;strong>典型誤配場景&lt;/strong>（基於通用 broker 行為知識展開、非 3.C9 case 原文具體列舉）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>At-least-once 假設變成 exactly-once 依賴&lt;/strong>：consumer 假設 broker 僅送一次、靠記憶單次處理；新 broker 重送同一 message、consumer 處理兩次&lt;/li>
&lt;li>&lt;strong>Offset 跳號處理差異&lt;/strong>：舊系統重啟後 offset 從特定位置開始、新系統可能從 latest / earliest 不同位置開始&lt;/li>
&lt;li>&lt;strong>Consumer group rebalance 行為差異&lt;/strong>：rebalance 期間舊系統會 pause 處理、新系統可能繼續處理、產生並發寫入衝突&lt;/li>
&lt;li>&lt;strong>DLQ retry 節奏差異&lt;/strong>：舊系統 DLQ message 預設不重試、新系統可能自動重試、製造重複副作用&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>回退判讀&lt;/strong>：回退前要先確認哪一段資料已經被新語意處理過。直接切回舊 broker 可能讓同一批事件再次被處理。穩定做法是先凍結新 consumer、保留 offset 對照與 replay 範圍、再決定補償或重播。&lt;/p>
&lt;p>詳細處理 / 恢復語意分層見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics&lt;/a>。規模差異判讀（小 / 中 / 大型服務的 job queue 治理重點）見 &lt;a href="https://tarrragon.github.io/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。">3.8 queue-consumer-retry-replay-handoff&lt;/a> — 中型服務常見問題是 lag/DLQ 長期累積、需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力。&lt;/p></description><content:encoded><![CDATA[<p>消費者設計（consumer design）的核心責任是把訊息投遞結果轉成可恢復的業務結果。queue 層提供 delivery 保證，consumer 層提供 processing 與 recovery 保證；三者對齊後，非同步流程才具備可預期性。</p>
<h2 id="三層語意">三層語意</h2>
<p>consumer 端需要同時處理三層語意：</p>
<ol>
<li>delivery semantics：訊息是否被成功投遞與確認，包含 ack/nack、retry、DLQ。</li>
<li>processing semantics：業務副作用是否可承受重複、亂序與部分失敗。</li>
<li>recovery semantics：故障後是否能重播、補償與回復到一致狀態。</li>
</ol>
<p>這三層拆開後，才能看清問題落在哪一層。訊息送達不代表副作用完成；副作用完成不代表系統可恢復。</p>
<h2 id="consumer-grouppartition-與順序責任">consumer group、partition 與順序責任</h2>
<p><a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 與 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 定義了並行與順序邊界。順序要求高的流程要把同一鍵值固定在同一 partition；吞吐優先的流程可提高 partition 數並分散處理。</p>
<p>分區策略會直接影響恢復成本。分區鍵混亂時，重播與補償很難限定範圍，事故期間容易擴大影響面。</p>
<h2 id="checkpointoffset-與-idempotency">checkpoint、offset 與 idempotency</h2>
<p><a href="/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint</a> 與 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 的責任是標記「處理到哪裡」，不是「業務一定完成」。寫 checkpoint 的時機要晚於副作用提交，避免進度前移導致資料遺漏。</p>
<p><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> key 的責任是讓重試與重播可重入。付款、發票、通知、庫存變更都需要明確冪等鍵與去重儲存策略，讓「至少一次投遞」不會變成「多次業務結果」。</p>
<h2 id="replay-safety">replay safety</h2>
<p>replay safety 的核心是先定義可重播範圍，再定義副作用控制。常見做法包含：</p>
<ol>
<li>限定 replay window，避免一次重播跨越多個版本邊界。</li>
<li>將副作用拆成可比對與可補償動作，保留對帳路徑。</li>
<li>對 replay 期間的下游壓力設置節流與停損條件。</li>
</ol>
<p>poison message 要獨立隔離。持續重試同一壞訊息會壓垮整體吞吐，穩定做法是送入 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>，再走診斷與修復流程。</p>
<h2 id="queue-語意誤配是-broker-遷移最常見的失敗模式">Queue 語意誤配是 broker 遷移最常見的失敗模式</h2>
<p>Broker 遷移失敗的根因通常是 <em>consumer 對舊 broker 行為的隱式依賴</em>、不是 broker 本身效能。表面上訊息仍被送達、但業務資料開始出現重複扣款、重複寄信、狀態漏更新。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：Queue Semantics Mismatch Cutover</a> — case 揭露切換後語意誤配三個方向：consumer 依賴特定 offset 行為、依賴特定重試節奏、依賴特定 idempotency 行為。失敗重播時、新系統即使提供相近 delivery semantics、結果可能不同。語意誤配會沿著下游資料寫入擴散、難以靠 queue depth 判斷。</p>
<p><strong>典型誤配場景</strong>（基於通用 broker 行為知識展開、非 3.C9 case 原文具體列舉）：</p>
<ul>
<li><strong>At-least-once 假設變成 exactly-once 依賴</strong>：consumer 假設 broker 僅送一次、靠記憶單次處理；新 broker 重送同一 message、consumer 處理兩次</li>
<li><strong>Offset 跳號處理差異</strong>：舊系統重啟後 offset 從特定位置開始、新系統可能從 latest / earliest 不同位置開始</li>
<li><strong>Consumer group rebalance 行為差異</strong>：rebalance 期間舊系統會 pause 處理、新系統可能繼續處理、產生並發寫入衝突</li>
<li><strong>DLQ retry 節奏差異</strong>：舊系統 DLQ message 預設不重試、新系統可能自動重試、製造重複副作用</li>
</ul>
<p><strong>回退判讀</strong>：回退前要先確認哪一段資料已經被新語意處理過。直接切回舊 broker 可能讓同一批事件再次被處理。穩定做法是先凍結新 consumer、保留 offset 對照與 replay 範圍、再決定補償或重播。</p>
<p>詳細處理 / 恢復語意分層見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>。規模差異判讀（小 / 中 / 大型服務的 job queue 治理重點）見 <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。">3.8 queue-consumer-retry-replay-handoff</a> — 中型服務常見問題是 lag/DLQ 長期累積、需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力。</p>
<h2 id="三個工程議題要一起設計">三個工程議題要一起設計</h2>
<p><code>Consumer idempotency</code> + <code>重播流程</code> + <code>下游承載能力</code> 三件事是 consumer design 的鐵三角、需同步落地。缺一個會在規模化時暴露成事故：</p>
<ul>
<li><strong>Consumer idempotency 不完整</strong>：DLQ replay 後產生重複副作用、即使 broker 切換成功、業務帳本仍然錯亂</li>
<li><strong>重播流程不完整</strong>：事故當下需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力</li>
<li><strong>下游承載能力不足</strong>：consumer 跟 broker 都健康、但下游 DB / API 撐不住 replay 速率、形成新事故</li>
</ul>
<p>Job queue 的拓樸分工是另一個獨立議題、跟鐵三角互補但不重疊 — 詳見 <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。">3.8 Job queue 拓樸分工</a>、主寫 Slack Kafka + Redis 案例。consumer 內部三件事要做好之外、不同類工作（高吞吐 / 即時 / 持久）也應專注單一目標、其他目標拆到對應路徑。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 持續上升</td>
          <td>consumer 吞吐低於輸入速率</td>
          <td>提升併發、拆分 partition、檢查下游瓶頸</td>
      </tr>
      <tr>
          <td>retry count 上升且成功率下降</td>
          <td>錯誤已從暫時性轉為系統性</td>
          <td>啟動降級、切換路由、保留重播窗口</td>
      </tr>
      <tr>
          <td>duplicate side effect 增加</td>
          <td>冪等鍵或去重流程失效</td>
          <td>修正 idempotency store、暫停高風險副作用</td>
      </tr>
      <tr>
          <td>DLQ 量快速增加</td>
          <td>payload 或版本相容性問題集中爆發</td>
          <td>分批隔離、加 schema 檢查、修復後定向重播</td>
      </tr>
      <tr>
          <td>replay 期間下游 timeout 同步上升</td>
          <td>重播速率超出依賴容量</td>
          <td>節流 replay、分段回放、加 backpressure 控制</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 consumer 設計等同於「把 handler 寫完」，會漏掉恢復責任。consumer 的工程價值在於故障後仍可追蹤、可補償、可重播。</p>
<p>把 DLQ 當成終點，會讓問題在下次事件再出現。DLQ 的責任是隔離與診斷入口，最終要回到 schema、邏輯或依賴治理。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>consumer 恢復語意可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 與 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn：TopicGC</a> 對照回寫。先判讀問題是 idempotency 失效、checkpoint 前移，還是 replay 邊界失控，再對應本章的 processing/recovery 段落。
這組案例主要支撐的是「處理恢復語意」判讀，不直接支撐 deployment drain 或 cache eviction；若根因在切流順序或快取容量，應轉到 5.3 或 2.3。</p>
<p>若重播成功但業務狀態仍不一致，先補副作用補償與對帳路徑，並把決策證據同步到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>consumer 設計是 01/03/04/06/08 的交界點。</p>
<ol>
<li>與 03 內部的交接：processing/recovery 語意完整定義在 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>；event contract 跟 replay boundary 在 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7</a>；規模差異判讀跟 job queue 拓樸分工在 <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。">3.8</a>。</li>
<li>與 01 的交接：交易與發布一致性回到 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> 與 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>。</li>
<li>與 04 的交接：lag、retry、DLQ、duplicate 指標進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 06 的交接：重試與重播驗證進入 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a>。</li>
<li>與 08 的交接：pause consumer、replay 決策與補償判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看 processing / recovery 三層語意完整定義、接著讀 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>。要建立 broker 層投遞模型，接著讀 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker 基礎與投遞模型</a> 與 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。要看錯誤切換案例，接著讀 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>。</p>
]]></content:encoded></item><item><title>5.4 service discovery</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</guid><description>&lt;p>服務發現（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery&lt;/a>）的核心責任是讓服務在變動環境中仍能找到正確目標實例。它處理的是定位與可用集合，不處理業務設定判斷；這個邊界清楚後，部署切換與故障回退才可預期。&lt;/p>
&lt;h2 id="dns-與-registry">DNS 與 registry&lt;/h2>
&lt;p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。&lt;/p>
&lt;p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。&lt;/p>
&lt;h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制&lt;/h3>
&lt;p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（&lt;code>clusterIP: None&lt;/code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。&lt;/p>
&lt;p>DNS-based discovery 的限制來自 DNS 本身的語意：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTL 與快取&lt;/strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM &lt;code>networkaddress.cache.ttl&lt;/code> 預設 30 秒、有些版本預設 -1 代表永不過期）。&lt;/li>
&lt;li>&lt;strong>無健康資訊&lt;/strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。&lt;/li>
&lt;li>&lt;strong>無權重 / 元資料&lt;/strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。&lt;/li>
&lt;/ol>
&lt;p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。&lt;/p>
&lt;h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式&lt;/h3>
&lt;p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。&lt;/p>
&lt;p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。&lt;/p>
&lt;p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。&lt;/p>
&lt;h3 id="選擇判讀框架">選擇判讀框架&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>需求&lt;/th>
 &lt;th>DNS-based&lt;/th>
 &lt;th>Registry-based&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>instance 變動頻率低、路由簡單&lt;/td>
 &lt;td>適合：低維護、零侵入&lt;/td>
 &lt;td>過度設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要權重路由或版本切流&lt;/td>
 &lt;td>不適合：DNS 不帶權重&lt;/td>
 &lt;td>適合：metadata + 路由規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要跨叢集 / 跨區域 discovery&lt;/td>
 &lt;td>需要外部 DNS 配合（困難）&lt;/td>
 &lt;td>適合：registry federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務用多語言實作&lt;/td>
 &lt;td>適合：任何語言都能解 DNS&lt;/td>
 &lt;td>需要每個語言的 SDK 或 sidecar&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要即時健康反映&lt;/td>
 &lt;td>受 TTL 限制、有延遲窗口&lt;/td>
 &lt;td>適合：health check 即時更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="endpoint-discovery">endpoint discovery&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> discovery 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。&lt;/p></description><content:encoded><![CDATA[<p>服務發現（<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>）的核心責任是讓服務在變動環境中仍能找到正確目標實例。它處理的是定位與可用集合，不處理業務設定判斷；這個邊界清楚後，部署切換與故障回退才可預期。</p>
<h2 id="dns-與-registry">DNS 與 registry</h2>
<p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。</p>
<p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。</p>
<h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制</h3>
<p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（<code>clusterIP: None</code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。</p>
<p>DNS-based discovery 的限制來自 DNS 本身的語意：</p>
<ol>
<li><strong>TTL 與快取</strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM <code>networkaddress.cache.ttl</code> 預設 30 秒、有些版本預設 -1 代表永不過期）。</li>
<li><strong>無健康資訊</strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。</li>
<li><strong>無權重 / 元資料</strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。</li>
</ol>
<p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。</p>
<h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式</h3>
<p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。</p>
<p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。</p>
<p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。</p>
<h3 id="選擇判讀框架">選擇判讀框架</h3>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>DNS-based</th>
          <th>Registry-based</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>instance 變動頻率低、路由簡單</td>
          <td>適合：低維護、零侵入</td>
          <td>過度設計</td>
      </tr>
      <tr>
          <td>需要權重路由或版本切流</td>
          <td>不適合：DNS 不帶權重</td>
          <td>適合：metadata + 路由規則</td>
      </tr>
      <tr>
          <td>需要跨叢集 / 跨區域 discovery</td>
          <td>需要外部 DNS 配合（困難）</td>
          <td>適合：registry federation</td>
      </tr>
      <tr>
          <td>服務用多語言實作</td>
          <td>適合：任何語言都能解 DNS</td>
          <td>需要每個語言的 SDK 或 sidecar</td>
      </tr>
      <tr>
          <td>需要即時健康反映</td>
          <td>受 TTL 限制、有延遲窗口</td>
          <td>適合：health check 即時更新</td>
      </tr>
  </tbody>
</table>
<h2 id="endpoint-discovery">endpoint discovery</h2>
<p><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。</p>
<p>endpoint 變更需要可追溯訊號，讓事故期間能快速判讀是路由失真、註冊延遲，還是下游本身不可用。</p>
<h3 id="註冊時序與-readiness-對齊">註冊時序與 Readiness 對齊</h3>
<p>endpoint 的註冊時機是 discovery 穩定性的關鍵變數。註冊太早（服務尚未 ready 就被加入可用集合）會導致客戶端打到未就緒實例；註冊太晚（服務已 ready 但尚未被 discovery 看到）會導致容量不足。</p>
<p>Kubernetes 的做法是把 endpoint 跟 readinessProbe 綁定：readiness pass 才把 pod IP 加入 Endpoints 物件。這個設計讓 readiness 定義直接決定 discovery 行為。但 readiness probe 的判斷到 Endpoints 更新之間仍有延遲（endpoint controller sync 週期 + kube-proxy rules 更新），這個延遲窗口內的行為要理解：</p>
<ul>
<li>Pod 剛從 not-ready 變 ready：endpoint controller 需要同步周期把 pod IP 加入 Endpoints → kube-proxy 更新 iptables / IPVS → 流量才會到。期間該 pod 不接流量但已可服務。</li>
<li>Pod 從 ready 變 not-ready：同樣有延遲。期間客戶端仍可能打到已 not-ready 的 pod。drain 設計要覆蓋這段窗口。</li>
</ul>
<h3 id="摘除節奏與-drain-的配合">摘除節奏與 Drain 的配合</h3>
<p>endpoint 摘除不是瞬時的。從 pod 標記 not-ready 到所有 client 停止向它送流量，中間經過多個同步步驟。這段時間內，被摘除的 pod 仍會收到流量。</p>
<p>穩定做法是在 preStop hook 加入短暫等待（通常 5-15 秒），讓 endpoint 更新有時間傳播到所有 kube-proxy / envoy，然後再開始 graceful shutdown。這段 preStop 等待是 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 中 drain 總窗口（短 API 通常 5-30 秒）的 endpoint 傳播子區間，drain 總窗口還要覆蓋 preStop 之後的在途請求收斂時間。</p>
<h3 id="跨叢集-discovery-的挑戰">跨叢集 Discovery 的挑戰</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed K8s → EKS</a>：揭露「遷移難點通常在跨叢集服務依賴與流量切換、不在 Kubernetes API 本身」。跨叢集 discovery 是遷移期的核心難題——服務 A 在新叢集、服務 B 在舊叢集，A 要能找到 B。</p>
<p>跨叢集 discovery 的常見做法：</p>
<ol>
<li><strong>外部 DNS + 加權路由</strong>：兩個叢集的 service 都註冊到外部 DNS（Route 53、Cloud DNS），用權重控制流量比例。簡單但粒度粗，只能整體切、不能 per-service 切。</li>
<li><strong>Service mesh federation</strong>：Istio multi-cluster、Linkerd multi-cluster 把跨叢集 endpoint 統一管理。粒度細、可以 per-service 切流量，但引入 mesh 的複雜度。</li>
<li><strong>Application-level routing</strong>：應用自己管理多叢集 endpoint（通常透過 config 或 feature flag），切換時改 config。最靈活但最手動，適合遷移期的過渡方案。</li>
</ol>
<p>遷移期最危險的狀態是「服務切過去了、discovery 沒切過去」——新叢集的服務 A 仍透過舊 discovery 找舊叢集的 B，跨網路延遲從微秒級跳到毫秒級，或在網路分區時完全斷開。discovery 切換要跟服務切換同批規劃。</p>
<h2 id="failure-fallback">failure fallback</h2>
<p><a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 在 discovery 層的責任是縮小定位失敗影響。常見策略包含本地快取最後可用集合、區域優先回退、受控重試與短暫降級。</p>
<p>fallback 設計要明確停止條件。長期依賴過期 endpoint 快取會造成隱性錯誤累積，事故期反而更難收斂。</p>
<h3 id="fallback-的三層防線">Fallback 的三層防線</h3>
<p>discovery 故障的 fallback 可分三層，每層有不同的代價與風險：</p>
<p><strong>第一層：本地 endpoint 快取</strong>。Client 維持最後一次成功查詢的 endpoint 清單。discovery 服務不可用時，繼續用快取 endpoint。風險是快取中的 endpoint 可能已經下線或不健康。有效期要設上限——超過 N 分鐘的快取視為不可信，進入第二層。</p>
<p><strong>第二層：區域降級</strong>。本區域的 endpoint 全部不可用時，降級到其他區域的 endpoint。代價是跨區延遲增加。風險是其他區域也可能因為同源故障而不可用。降級時要觀測跨區延遲是否在 SLO 內，超出則進第三層。</p>
<p><strong>第三層：服務降級</strong>。discovery 完全失效時，服務本身降級——返回快取回應、靜態頁面、或明確的錯誤訊息。這一層的設計責任落在應用的 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 策略，discovery 只負責提供「目前無可用 endpoint」的訊號。</p>
<p>三層防線的共同原則是每一層都有明確的進入條件和退出條件。進入 fallback 不是終點——要持續嘗試恢復正常路徑，fallback 狀態持續時間要被觀測和告警。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務延遲上升且下游錯誤分布不均</td>
          <td>路由到不可用或高負載實例</td>
          <td>檢查註冊健康、刷新 endpoint 集合</td>
      </tr>
      <tr>
          <td>節點重啟後短時間大量 5xx</td>
          <td>註冊與 readiness 時序不對齊</td>
          <td>延後註冊時機、收斂就緒條件</td>
      </tr>
      <tr>
          <td>跨區呼叫比例異常升高</td>
          <td>區域內可用集合失真或容量不足</td>
          <td>檢查區域路由策略、恢復本地優先</td>
      </tr>
      <tr>
          <td>discovery 查詢成功但連線失敗率升高</td>
          <td>endpoint 新鮮度不足或 DNS 快取漂移</td>
          <td>縮短 TTL、加入主動刷新</td>
      </tr>
      <tr>
          <td>fallback 命中率長期偏高</td>
          <td>主路徑失效被掩蓋</td>
          <td>啟動故障調查、限制 fallback 存活時間</td>
      </tr>
      <tr>
          <td>擴容後新 pod 遲遲不接流量</td>
          <td>endpoint 註冊延遲或 kube-proxy 同步慢</td>
          <td>檢查 endpoint controller 延遲</td>
      </tr>
      <tr>
          <td>遷移期跨叢集延遲突增</td>
          <td>discovery 沒切過去、跨網路打舊叢集</td>
          <td>規劃 discovery 切換與服務切換同批</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Service discovery 跟 DNS 設定的混淆，會讓註冊時序、健康判斷與摘除節奏的缺口在平時被忽略。這類缺口在平時不明顯，通常在切版、擴縮容或區域異常時集中爆發。</p>
<p>把 fallback 命中率視為穩定指標也容易誤判。fallback 長期偏高代表主路徑問題被遮蔽，應回頭檢查 endpoint 新鮮度與註冊健康，而不是只放寬重試。</p>
<p>把 DNS TTL 設成 0 試圖取得即時一致性，會大幅增加 DNS 查詢量。DNS 的設計前提是快取——TTL 0 在高流量服務下會讓 DNS server 成為瓶頸。穩定做法是設合理 TTL（5-30 秒）搭配 client-side 主動刷新。</p>
<p>把 JVM 的 DNS cache 當成 OS 的 DNS TTL——JVM <code>networkaddress.cache.ttl</code> 的預設值在不同版本不同（有些版本是 30 秒、有些是永不過期）。容器化部署時要顯式設定，避免 pod IP 變了但 JVM 還在打舊 IP。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>service discovery 專注「找到可用實例」。當問題進入設定分發、版本切換、策略開關，責任轉到 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 與部署策略章節。邊界分明能避免故障排查時把不同控制面混為一談。</p>
<p>discovery 跟 load balancing 的邊界：discovery 回答「有哪些 endpoint 可用」，load balancing 回答「在可用 endpoint 中選哪一個」。DNS round-robin 把兩者混在一起，registry-based 方案通常把兩者分開，讓 LB 策略（round-robin、least-connection、consistent hash）在 discovery 結果之上獨立運作。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>發現與定位鏈路可用 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera：managed K8s migration</a> 回寫。先看遷移期間實例註冊、摘除與 DNS/registry 同步節奏，再對照本章判讀 endpoint 新鮮度與 fallback 壽命是否合理。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed K8s → EKS</a> 從跨叢集角度支撐：揭露遷移期的 discovery 挑戰——「難點在跨叢集服務依賴與流量切換」。遷移期 discovery 要處理新舊叢集的 endpoint 共存、切換時序、回退路徑。</p>
<p>這些案例主要支撐「定位集合新鮮度」與「跨叢集 discovery 同步」判讀。不直接支撐 LB 連線 timeout 或 runtime 建置一致性；若問題在連線生命週期或映像漂移，應轉到 5.3 或 5.1。</p>
<p>遇到「查詢成功但連線失敗率高」時，應拆成註冊時序、TTL 與快取刷新三條線同步驗證，避免把定位問題誤判成下游異常，再把證據分流到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake &amp; Evidence Triage</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：實例註冊與可用判定回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：路由目標與流量合約回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.6 的交接：endpoint 註冊時序與 readiness 的對齊回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.7 的交接：discovery 與 control plane boundary 的分責回到 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.13 的交接：依賴拓樸與發現信號回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">Service Topology 與 Dependency Map</a>。</li>
<li>與 8.18 的交接：定位故障的證據分流回到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">Incident Intake &amp; Evidence Triage</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把發現機制放進流量契約，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看部署切換如何影響可用集合，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看 discovery 在 control plane 邊界中的定位，接著讀 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Traffic、Config 與 Control Plane Boundary</a>。</p>
]]></content:encoded></item><item><title>6.4 chaos testing</title><link>https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test&lt;/a> 是在可控條件下主動注入故障，驗證系統是否能在真實依賴失效時維持 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a> 與可接受的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>。&lt;/p>
&lt;p>這一頁關心的是失效時系統怎麼退化。chaos 的價值在於判讀系統收到故障後的退化行為是否符合預期。沒有先定義 steady state，chaos 只會變成故障展示，不會變成判讀工具。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 chaos 的重點是對控制面、資料面與依賴鏈的回復能力做驗證，而不是單純證明服務死過一次。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>是否先定義 steady state 與成功條件&lt;/li>
&lt;li>故障是否真的落在常見依賴與控制點&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 是否可量測、可縮限&lt;/li>
&lt;li>recovery path 是否能在演練後被重播&lt;/li>
&lt;/ul>
&lt;h2 id="故障注入的設計流程">故障注入的設計流程&lt;/h2>
&lt;p>一輪有效的 chaos 驗證從穩態定義開始。先知道系統正常時應維持什麼行為，再設計注入去測試這個行為是否可持續。&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>定義穩態&lt;/td>
 &lt;td>服務正常時應維持什麼行為&lt;/td>
 &lt;td>穩態指標與門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設計假設&lt;/td>
 &lt;td>失效發生後系統仍應維持什麼&lt;/td>
 &lt;td>可證偽假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>限制 blast radius&lt;/td>
 &lt;td>實驗範圍怎麼控制&lt;/td>
 &lt;td>服務 / 區域 / 流量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定停止條件&lt;/td>
 &lt;td>何時立即停止實驗&lt;/td>
 &lt;td>abort trigger&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>穩態定義是整個流程的錨點。Netflix 的 chaos 實踐把 steady state 放在驗證循環的第一步 — 先定義穩態指標（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI&lt;/a>、business KPI、queue lag），再用故障注入去測試這些指標是否能在壓力下維持。沒有穩態定義的故障注入只能產出「系統被打壞了」的結論，無法回答「系統是否按預期退化」。&lt;/p>
&lt;p>假設設計決定實驗能學到什麼。好的假設會說明「當 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 節點離線時，訊息消費延遲應在 30 秒內回線，checkout 成功率應維持在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">SLO&lt;/a> 門檻內」，而不只是「關掉 broker 看看會怎樣」。假設越具體，實驗結果的判讀價值越高。&lt;/p>
&lt;p>Blast radius 需要同時包含技術範圍與客戶範圍。技術範圍是 service、region、cluster、dependency；客戶範圍是 tenant、plan、traffic percentage 或 internal-only cohort。從最小範圍開始，逐步放大，每一步都要確認停止條件仍可執行。&lt;/p>
&lt;p>停止條件讓實驗可控。當 SLO &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 超門檻、customer impact 出現或 cost 異常上升時，實驗應立即終止。停止條件要連到可觀測訊號，不能靠臨場討論決定是否繼續。&lt;/p>
&lt;h2 id="注入類型與層次">注入類型與層次&lt;/h2>
&lt;p>故障注入按依賴類型分層。不同依賴的失效模式不同，預期退化也不同，實驗設計需要對應調整。&lt;/p>
&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>Broker outage&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 節點或 partition&lt;/td>
 &lt;td>消費延遲上升、DLQ 累積&lt;/td>
 &lt;td>流量接近 production pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DB latency&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> 連線或查詢&lt;/td>
 &lt;td>請求排隊、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 觸發&lt;/td>
 &lt;td>connection pool 配置與 production 一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Node restart&lt;/td>
 &lt;td>應用節點&lt;/td>
 &lt;td>短暫不可用、load balancer 切流&lt;/td>
 &lt;td>readiness probe 與 graceful shutdown 配置一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network jitter&lt;/td>
 &lt;td>跨服務通訊&lt;/td>
 &lt;td>latency 抖動、retry 上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter&lt;/a> 模式接近真實 ISP / cloud&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Broker outage 驗證的是非同步依賴的容錯能力。當 broker 節點或 partition 不可用時，生產端應有 retry 與 fallback，消費端應能在恢復後 drain backlog 而不是 replay storm。測試時需要確認 DLQ 設定正確、消費 lag 有監控、恢復後的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 不會壓垮下游。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test</a> 是在可控條件下主動注入故障，驗證系統是否能在真實依賴失效時維持 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 與可接受的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>。</p>
<p>這一頁關心的是失效時系統怎麼退化。chaos 的價值在於判讀系統收到故障後的退化行為是否符合預期。沒有先定義 steady state，chaos 只會變成故障展示，不會變成判讀工具。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 chaos 的重點是對控制面、資料面與依賴鏈的回復能力做驗證，而不是單純證明服務死過一次。</p>
<p>重點訊號包括：</p>
<ul>
<li>是否先定義 steady state 與成功條件</li>
<li>故障是否真的落在常見依賴與控制點</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 是否可量測、可縮限</li>
<li>recovery path 是否能在演練後被重播</li>
</ul>
<h2 id="故障注入的設計流程">故障注入的設計流程</h2>
<p>一輪有效的 chaos 驗證從穩態定義開始。先知道系統正常時應維持什麼行為，再設計注入去測試這個行為是否可持續。</p>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>核心問題</th>
          <th>產出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>定義穩態</td>
          <td>服務正常時應維持什麼行為</td>
          <td>穩態指標與門檻</td>
      </tr>
      <tr>
          <td>設計假設</td>
          <td>失效發生後系統仍應維持什麼</td>
          <td>可證偽假設</td>
      </tr>
      <tr>
          <td>限制 blast radius</td>
          <td>實驗範圍怎麼控制</td>
          <td>服務 / 區域 / 流量</td>
      </tr>
      <tr>
          <td>設定停止條件</td>
          <td>何時立即停止實驗</td>
          <td>abort trigger</td>
      </tr>
  </tbody>
</table>
<p>穩態定義是整個流程的錨點。Netflix 的 chaos 實踐把 steady state 放在驗證循環的第一步 — 先定義穩態指標（<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI</a>、business KPI、queue lag），再用故障注入去測試這些指標是否能在壓力下維持。沒有穩態定義的故障注入只能產出「系統被打壞了」的結論，無法回答「系統是否按預期退化」。</p>
<p>假設設計決定實驗能學到什麼。好的假設會說明「當 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 節點離線時，訊息消費延遲應在 30 秒內回線，checkout 成功率應維持在 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">SLO</a> 門檻內」，而不只是「關掉 broker 看看會怎樣」。假設越具體，實驗結果的判讀價值越高。</p>
<p>Blast radius 需要同時包含技術範圍與客戶範圍。技術範圍是 service、region、cluster、dependency；客戶範圍是 tenant、plan、traffic percentage 或 internal-only cohort。從最小範圍開始，逐步放大，每一步都要確認停止條件仍可執行。</p>
<p>停止條件讓實驗可控。當 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 超門檻、customer impact 出現或 cost 異常上升時，實驗應立即終止。停止條件要連到可觀測訊號，不能靠臨場討論決定是否繼續。</p>
<h2 id="注入類型與層次">注入類型與層次</h2>
<p>故障注入按依賴類型分層。不同依賴的失效模式不同，預期退化也不同，實驗設計需要對應調整。</p>
<table>
  <thead>
      <tr>
          <th>注入類型</th>
          <th>打到的依賴</th>
          <th>預期退化</th>
          <th>結果可信條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Broker outage</td>
          <td><a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 節點或 partition</td>
          <td>消費延遲上升、DLQ 累積</td>
          <td>流量接近 production pattern</td>
      </tr>
      <tr>
          <td>DB latency</td>
          <td><a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> 連線或查詢</td>
          <td>請求排隊、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 觸發</td>
          <td>connection pool 配置與 production 一致</td>
      </tr>
      <tr>
          <td>Node restart</td>
          <td>應用節點</td>
          <td>短暫不可用、load balancer 切流</td>
          <td>readiness probe 與 graceful shutdown 配置一致</td>
      </tr>
      <tr>
          <td>Network jitter</td>
          <td>跨服務通訊</td>
          <td>latency 抖動、retry 上升</td>
          <td><a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a> 模式接近真實 ISP / cloud</td>
      </tr>
  </tbody>
</table>
<p>Broker outage 驗證的是非同步依賴的容錯能力。當 broker 節點或 partition 不可用時，生產端應有 retry 與 fallback，消費端應能在恢復後 drain backlog 而不是 replay storm。測試時需要確認 DLQ 設定正確、消費 lag 有監控、恢復後的 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 不會壓垮下游。</p>
<p>DB latency 驗證的是同步依賴在退化時的行為。延遲注入比完全斷線更接近真實故障 — production 常見的是 slow query、connection pool exhaustion 或 replica lag，而不是 database 完全離線。測試時需要確認 timeout 是否會級聯：一個慢查詢拖住連線，其他請求開始排隊，最終 thread pool 或 goroutine 耗盡。</p>
<p>Node restart 驗證的是服務在節點層級的恢復能力。graceful shutdown 是否正確 drain 連線、readiness probe 是否能阻止 load balancer 過早送流量、cold start 是否會因 cache miss 或 JIT warmup 造成短暫效能劣化。</p>
<p>Network jitter 驗證的是跨服務通訊的韌性。jitter 注入需要模擬真實的 latency distribution（長尾、間歇性），而不是固定延遲。測試時需要關注 retry 行為：固定 retry 在 jitter 環境下可能放大流量，需要搭配 <a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a> 控制。</p>
<h3 id="注入粒度instance-level-vs-request-path">注入粒度：instance-level vs request-path</h3>
<p>故障注入有兩個主要粒度，適用場景不同。</p>
<p>Instance-level injection（如 Chaos Monkey）在節點層注入故障 — 關閉 instance、斷開網路、暫停程序。這個粒度驗證的是基礎設施韌性：load balancer 能否切流、auto-scaling 能否補位、graceful shutdown 能否完成。優點是簡單、接近真實硬體故障；缺點是粒度粗，無法精準驗證特定依賴路徑。</p>
<p>Request-path injection（如 FIT）在請求路徑層注入故障 — 對特定 API call、dependency request 或 service-to-service 通訊植入 timeout、error 或延遲。這個粒度驗證的是應用韌性：fallback 是否生效、circuit breaker 是否觸發、retry 是否安全。優點是精準、blast radius 小；缺點是需要更深的 instrumentation，建置成本較高。</p>
<p>兩者不互斥。instance-level injection 適合驗證基礎設施層的回復能力，request-path injection 適合驗證應用層的容錯邏輯。團隊可以從 instance-level 開始建立 chaos 習慣，再逐步引入 request-path injection 提升驗證精度。第三種粒度是 infrastructure-level injection（AZ failure / region failure），由 cloud provider 的 chaos 工具（如 AWS FIS、Azure Chaos Studio）支援，驗證的是跨 AZ 冗餘與 failover 路由。</p>
<h2 id="執行時段與環境">執行時段與環境</h2>
<p>故障注入的執行時段與環境直接影響驗證價值。</p>
<h3 id="business-hours-vs-off-peak">Business hours vs off-peak</h3>
<p>在 business hours 執行 chaos 能同時驗證系統韌性與團隊應變能力。人員在線可即時觀測、依賴流量接近真實、通訊鏈條（值班升級、跨團隊協作、內外部狀態更新）被完整測到。off-peak 雖然短期風險低，但測到的多是「工具可執行」，不是「服務在真實壓力下可承受」。</p>
<p>選擇 business hours 執行的前提是 guardrails 到位：時段限制在可支援的工作時間、blast radius 從小範圍開始、abort trigger 連到明確門檻、事後回寫進工程控制面。風險來自 guardrails 的缺失。</p>
<h3 id="staging-vs-production">Staging vs production</h3>
<p>Staging 適合驗證工具整合與基礎假設：注入能否生效、dashboard 能否呈現訊號、stop condition 能否觸發。但 staging 與 production 之間通常存在環境漂移 — traffic pattern 不同、dependency 配置不同、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 大小不同、cache warmup 狀態不同。在 staging 通過的實驗，不能直接等同於 production 可承受。</p>
<p>Production chaos 的價值在於驗證真實依賴路徑。它需要從最小 cohort 開始（internal traffic、canary region、特定 tenant），搭配完整 stop condition 與 rollback path。Production chaos 需要 stop condition 作為安全網。團隊可以從簡單的 stop condition（如 error rate 超門檻就停止）起步，隨經驗累積逐步精細化。</p>
<h2 id="證據結構與回寫">證據結構與回寫</h2>
<p>Chaos 實驗的產出是可決策的證據。當實驗結果能直接回答「這個依賴的容錯能力是否足夠」，chaos 才從測試活動升級為可靠性控制面。</p>
<table>
  <thead>
      <tr>
          <th>證據欄位</th>
          <th>核心問題</th>
          <th>決策用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Steady-state impact</td>
          <td>注入後穩態指標是否維持</td>
          <td>判斷容錯能力是否符合預期</td>
      </tr>
      <tr>
          <td>Abort trigger record</td>
          <td>停止條件是否被觸發、何時觸發</td>
          <td>判斷是否需要凍結或回退</td>
      </tr>
      <tr>
          <td>Fallback result</td>
          <td>降級路徑是否可用、恢復是否收斂</td>
          <td>判斷事故時能否安全止血</td>
      </tr>
      <tr>
          <td>Dependency drift</td>
          <td>受影響依賴是否落在預期範圍</td>
          <td>判斷 blast radius 是否可接受</td>
      </tr>
  </tbody>
</table>
<p>Steady-state impact 是最核心的證據欄位。它回答的問題是「系統在故障期間是否維持了服務承諾」。若 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI</a> 維持在 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">SLO</a> 門檻內，代表容錯機制有效；若偏離，需要記錄偏離幅度、持續時間與影響範圍。</p>
<p>Abort trigger record 讓團隊知道 stop condition 是否可執行。若停止條件被觸發但執行延遲，代表觀測或通訊鏈條有缺口；若停止條件沒被觸發但影響已擴大，代表門檻設定需要校準。</p>
<p>這四個欄位接到 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a> 後，可直接成為 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的放行輸入。release decision 從「主觀討論」轉成「政策驅動」：有證據支持容錯能力 → 放行；abort 被觸發 → 凍結並修復；fallback 失敗 → 補 action item 再重驗。</p>
<h2 id="規模差異">規模差異</h2>
<p>Chaos 的設計在不同規模下差異顯著。單服務 chaos 與跨區 chaos 打到的系統層不同，blast radius 控制方式也不同。</p>
<h3 id="單服務-chaos">單服務 chaos</h3>
<p>單服務 chaos 驗證的是一個服務對其直接依賴的容錯能力。blast radius 限在該服務的 instance、replica 或 traffic cohort 內。適合驗證 circuit breaker、fallback、timeout、retry 與 graceful degradation。</p>
<h3 id="跨區-chaos-與-failure-localization">跨區 chaos 與 failure localization</h3>
<p>跨區 chaos 驗證的是故障在區域或依賴鏈上的擴散行為。Amazon 的 cell-based architecture 把多租戶服務的故障域限制在 cell 內 — 一個 cell 的異常不會擴散到其他 cell，恢復策略從全域搶救轉為分批收斂。Meta 的 region failover 實踐則關注控制面故障的跨區擴散 — 當核心網路或 BGP 配置異常跨越區域邊界，恢復動作本身可能成為新的放大器。</p>
<p>兩者共同的判讀重點是：故障是否被限制在預期邊界內。單服務 chaos 的邊界是 instance 與 dependency；跨區 chaos 的邊界是 region、cell 與 shared dependency。blast radius 越大，stop condition 與 rollback path 的設計要求越高。</p>
<h2 id="產業情境串流與媒體服務">產業情境：串流與媒體服務</h2>
<p>串流服務的故障注入需要考慮觀眾正在觀看的即時性。CDN 節點失效、origin server 延遲或 transcoding pipeline 中斷都會直接造成 buffering 或畫質降級，使用者的容忍窗口以秒計。</p>
<p>串流的 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 指標跟一般 web service 不同：buffering ratio（觀眾看到轉圈的時間比例）、bitrate stability（畫質是否頻繁跳動）、video start time（按下播放到第一幀的延遲）。這些指標直接反映觀看體驗，chaos 實驗的假設必須用這些指標定義穩態，而非只用 HTTP success rate。</p>
<p>CDN 有多層快取（edge / mid-tier / origin），某一層失效時流量會 fallback 到下一層。chaos 要驗證的是 fallback 路徑能否承受突增的回源流量，以及 adaptive bitrate 策略是否能平滑過渡到較低畫質，而非直接中斷播放。回源流量的放大倍數取決於該層的快取命中率 — 命中率越高的層失效，回源放大越劇烈。</p>
<p>直播事件的 chaos 約束更嚴格。VOD 內容可重試、可重播，直播沒有第二次機會。直播前的 chaos 演練需要模擬「直播進行中 CDN 節點失效」的場景，驗證備援路徑的切換速度是否在觀眾可感知門檻（通常 2-5 秒）內。Netflix 的 chaos 實踐原始動機即是保護串流觀看體驗，其 <a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">steady state hypothesis</a> 的設計直接適用於串流場景。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT</a>：把故障注入變成科學化驗證循環，四元素（steady state / hypothesis / blast radius / abort condition）提供 chaos 設計的結構。FIT 把注入粒度推進到 request path，讓測試更接近真實依賴路徑。</li>
<li><a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">Netflix：Business-Hours Chaos Guardrails</a>：business hours 執行的前提是 guardrails 到位（時段限制、範圍限制、abort trigger、事後回寫），驗證的不只是系統韌性，也包含團隊應變能力。</li>
<li><a href="/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">Netflix：FIT 證據交接</a>：把 FIT 輸出結構化成四個決策欄位，讓實驗結果直接驅動 release gate。</li>
<li><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon：Shuffle Sharding 與 Cell 邊界</a>：cell-based architecture 讓恢復策略從全域搶救轉為分批收斂，是跨區 chaos 設計的前提。</li>
<li><a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">Meta：Region Failover 邊界治理</a>：跨區依賴與控制面故障的回復順序，說明 blast radius 在大規模系統中的擴散治理。</li>
<li><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a>：game day 把演練、壓測與隔離單位連成一條線，適合補充高峰型場景的 chaos 設計。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<p>判讀 chaos 的品質不只看實驗是否通過，要看實驗設計是否能產出可信結論。</p>
<ul>
<li><strong>chaos experiment 只測 happy path 的故障</strong>：只關掉不重要的服務、只在低流量時段跑，通過了也無法證明高價值路徑的容錯能力。判讀條件：注入目標是否對應服務的關鍵依賴路徑。行動：把注入目標對齊服務的 top-3 關鍵依賴。</li>
<li><strong>broker / DB / network 故障無自動演練、靠真事故學</strong>：沒有定期 chaos 的團隊只能從真實事故中學習，學習成本高且機會不可控。判讀條件：chaos 是否有固定節奏，而非只在事故後才啟動。行動：排入季度 chaos sprint、從最小 blast radius 開始。</li>
<li><strong>chaos 暴露問題沒修、紀錄堆積</strong>：實驗發現缺口但 action item 沒有 owner、沒有 deadline，同類問題反覆出現。判讀條件：action item 是否進入 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a> 並被追蹤。行動：每次 chaos 結束後 action item 指定 owner + deadline。</li>
<li><strong>production chaos 只在低流量時段跑、訊號失真</strong>：低流量時段的依賴行為、流量模式與團隊狀態都跟 production peak 不同，通過了不代表高峰時可承受。判讀條件：是否有 business-hours 或接近 peak 的驗證補充。行動：至少每季補一次 business-hours chaos 驗證。</li>
<li><strong>故障注入工具跟 production 不同 stack、結果不可信</strong>：staging 用不同的 broker、database 或 network 配置做 chaos，結果無法外推到 production。判讀條件：實驗環境與 production 的差異是否被記錄並納入結論限制。行動：在結論中標註環境差異、逐步推進 production chaos。</li>
<li><strong>chaos 結果沒進 runbook</strong>：值班人員不知道特定依賴失效後的預期退化行為，事故時仍靠臨場推理。判讀條件：chaos 結論是否已回寫到對應服務的 on-call runbook。行動：每次 chaos 完成後回寫 runbook 的「依賴失效預期行為」段。</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR / rollback rehearsal</a>：chaos 暴露的回復路徑問題進入 DR 演練</li>
<li><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a>：注入重複訊息驗證冪等能力</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a>：對依賴注入故障驗證 reliability budget</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：chaos 的 blast radius、stop condition 與權限約束</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：chaos 開始前的穩態定義</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>：chaos 證據接到 release gate</li>
<li><a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6 drills / on-call readiness</a>：chaos 結果回饋到值班訓練</li>
</ul>
]]></content:encoded></item><item><title>8.4 事故通訊與狀態更新</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/</guid><description>&lt;p>事故通訊與狀態更新的核心責任是維持單一事實敘事，讓內外部在同一時間窗理解同一件事，並在主要通道故障時仍能持續發布。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Incident communication channel 是事故期間的通訊控制面，責任是固定主通道、備援通道與更新節奏，避免訊息流量比事故本身更快失控。&lt;/p>
&lt;p>這一頁處理的是通訊路由與節奏，不是公關措辭。當主通道、備援通道與發言權限沒有先定義，現場就會出現多版本敘事、更新延遲與錯誤承諾。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>通訊控制面的責任：維持內外部單一敘事&lt;/li>
&lt;li>通訊拓樸：內部主通道、外部主通道、備援通道&lt;/li>
&lt;li>更新節奏：固定 cadence、變更觸發、緊急補播&lt;/li>
&lt;li>欄位模型：時間窗、影響範圍、已知限制、下一次更新時間&lt;/li>
&lt;li>主要通道失效處理：status page 依賴檢查與切換門檻&lt;/li>
&lt;li>與 decision log 的關係：所有對外敘事變更都需可回放&lt;/li>
&lt;li>反模式：多通道平行宣布、主通道故障但不切換、只報「仍在調查」&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>對外 update cadence 不規律，客戶不清楚下一次更新時間&lt;/li>
&lt;li>內部多 channel 並存，決策與通訊內容分裂&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stakeholder-mapping/" data-link-title="Stakeholder Mapping" data-link-desc="說明事故期間如何把通報對象分層與對應 owner">stakeholder mapping&lt;/a> 過期，漏通知關鍵角色&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 入口依賴受影響系統，更新卡住&lt;/li>
&lt;li>對外聲明沒有標示已知限制，後續反覆修正文案&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀通訊控制面時，先看主通道是否明確，再看備援通道是否可在門檻內切換。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>是否有單一對內主通道與單一對外發布節點&lt;/li>
&lt;li>對外更新是否固定包含「下次更新時間」&lt;/li>
&lt;li>主通道失效時是否能切到備援通道&lt;/li>
&lt;li>對外敘事是否連到同一條 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stakeholder-mapping/" data-link-title="Stakeholder Mapping" data-link-desc="說明事故期間如何把通報對象分層與對應 owner">stakeholder mapping&lt;/a> 是否覆蓋支援、客服、法務與管理層&lt;/li>
&lt;/ul>
&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>主通道&lt;/td>
 &lt;td>內外部各一個主通道&lt;/td>
 &lt;td>多組人各自對外更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>備援通道&lt;/td>
 &lt;td>有切換門檻與啟動責任人&lt;/td>
 &lt;td>主通道卡住後仍等待&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節奏&lt;/td>
 &lt;td>固定 cadence + 事件觸發補播&lt;/td>
 &lt;td>更新間隔不可預期&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>欄位&lt;/td>
 &lt;td>時間窗、影響範圍、限制、下一步齊備&lt;/td>
 &lt;td>對外只有「調查中」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對位&lt;/td>
 &lt;td>通訊內容對齊 decision log&lt;/td>
 &lt;td>內外部敘事彼此衝突&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="通訊拓樸">通訊拓樸&lt;/h2>
&lt;p>通訊拓樸要先定義，再進入事故。拓樸的責任是讓每個角色知道資訊要去哪裡收斂、從哪裡發布。&lt;/p>
&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>IC、scribe、service owner&lt;/td>
 &lt;td>incident room / war-room&lt;/td>
 &lt;td>收斂事實、同步決策、更新時間線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部主通道&lt;/td>
 &lt;td>comms lead&lt;/td>
 &lt;td>status page&lt;/td>
 &lt;td>對外發布已確認事實與下一次更新時間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部備援&lt;/td>
 &lt;td>comms lead&lt;/td>
 &lt;td>vendor status page、社群帳號、客服入口&lt;/td>
 &lt;td>主通道失效時維持公告能力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>內部主通道要偏向決策，外部主通道要偏向已確認事實。兩者共用同一條決策與證據基線，但敘述粒度不同。&lt;/p>
&lt;p>外部備援不是選配項。若 status page 管理面與受影響服務同依賴，主通道可能同時失效；備援通道要能在數分鐘內接手公告。&lt;/p>
&lt;h2 id="更新欄位與節奏">更新欄位與節奏&lt;/h2>
&lt;p>更新內容要固定欄位，避免每次都重寫格式。欄位固定後，對外訊息才可比較、可審核、可回放。&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>Timestamp&lt;/td>
 &lt;td>說明本次更新時間&lt;/td>
 &lt;td>2026-05-07T16:30Z&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope&lt;/td>
 &lt;td>說明受影響區域 / 功能 / 客戶群&lt;/td>
 &lt;td>us-east-1 PUT API / 部分租戶&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Known facts&lt;/td>
 &lt;td>說明已確認事實&lt;/td>
 &lt;td>index subsystem 重啟中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Known limitation&lt;/td>
 &lt;td>說明未確認或資料限制&lt;/td>
 &lt;td>目前僅掌握 API 指標，客戶端待補證據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mitigation&lt;/td>
 &lt;td>說明已執行止血或降級&lt;/td>
 &lt;td>限流 + read-only fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Next update&lt;/td>
 &lt;td>承諾下一次更新時間&lt;/td>
 &lt;td>20 分鐘後或重大進展立即更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>更新節奏需要雙軌：固定 cadence + 重大事件補播。固定 cadence 提供可預期性，重大事件補播提供時效性。&lt;/p>
&lt;h2 id="主通道失效切換">主通道失效切換&lt;/h2>
&lt;p>主通道失效切換的責任是確保事故中仍有可信對外入口。切換條件要事前定義，避免現場臨時爭論。&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>status page 入口不可用超過門檻&lt;/td>
 &lt;td>啟動備援通道&lt;/td>
 &lt;td>記錄觸發時間、責任人、備援 URL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主通道更新延遲超過既定 cadence&lt;/td>
 &lt;td>由 comms lead 直接補播&lt;/td>
 &lt;td>記錄延遲原因與修正措施&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部依賴造成訊息發布阻塞&lt;/td>
 &lt;td>切換到不共依賴的公告入口&lt;/td>
 &lt;td>記錄依賴關係與下次演練需修正的拓樸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內外部敘事版本不一致&lt;/td>
 &lt;td>凍結對外新增敘事、先對齊事實版本&lt;/td>
 &lt;td>記錄哪個欄位衝突與由誰核定最終版本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個控制面直接對應 AWS S3 2017 的教訓：狀態頁更新入口如果受同一事故影響，團隊必須先維持對外可見性，再補全細節。&lt;/p>
&lt;h2 id="與-decision-log-的關係">與 Decision Log 的關係&lt;/h2>
&lt;p>每一次對外敘事變更都應在 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log&lt;/a> 留下原因與證據。通訊不是附屬工作，它本身就是事故決策的一部分。&lt;/p></description><content:encoded><![CDATA[<p>事故通訊與狀態更新的核心責任是維持單一事實敘事，讓內外部在同一時間窗理解同一件事，並在主要通道故障時仍能持續發布。</p>
<h2 id="概念定位">概念定位</h2>
<p>Incident communication channel 是事故期間的通訊控制面，責任是固定主通道、備援通道與更新節奏，避免訊息流量比事故本身更快失控。</p>
<p>這一頁處理的是通訊路由與節奏，不是公關措辭。當主通道、備援通道與發言權限沒有先定義，現場就會出現多版本敘事、更新延遲與錯誤承諾。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>通訊控制面的責任：維持內外部單一敘事</li>
<li>通訊拓樸：內部主通道、外部主通道、備援通道</li>
<li>更新節奏：固定 cadence、變更觸發、緊急補播</li>
<li>欄位模型：時間窗、影響範圍、已知限制、下一次更新時間</li>
<li>主要通道失效處理：status page 依賴檢查與切換門檻</li>
<li>與 decision log 的關係：所有對外敘事變更都需可回放</li>
<li>反模式：多通道平行宣布、主通道故障但不切換、只報「仍在調查」</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>對外 update cadence 不規律，客戶不清楚下一次更新時間</li>
<li>內部多 channel 並存，決策與通訊內容分裂</li>
<li><a href="/blog/backend/knowledge-cards/stakeholder-mapping/" data-link-title="Stakeholder Mapping" data-link-desc="說明事故期間如何把通報對象分層與對應 owner">stakeholder mapping</a> 過期，漏通知關鍵角色</li>
<li><a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 入口依賴受影響系統，更新卡住</li>
<li>對外聲明沒有標示已知限制，後續反覆修正文案</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀通訊控制面時，先看主通道是否明確，再看備援通道是否可在門檻內切換。</p>
<p>重點訊號包括：</p>
<ul>
<li>是否有單一對內主通道與單一對外發布節點</li>
<li>對外更新是否固定包含「下次更新時間」</li>
<li>主通道失效時是否能切到備援通道</li>
<li>對外敘事是否連到同一條 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a></li>
<li><a href="/blog/backend/knowledge-cards/stakeholder-mapping/" data-link-title="Stakeholder Mapping" data-link-desc="說明事故期間如何把通報對象分層與對應 owner">stakeholder mapping</a> 是否覆蓋支援、客服、法務與管理層</li>
</ul>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>最小可用判準</th>
          <th>失效訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主通道</td>
          <td>內外部各一個主通道</td>
          <td>多組人各自對外更新</td>
      </tr>
      <tr>
          <td>備援通道</td>
          <td>有切換門檻與啟動責任人</td>
          <td>主通道卡住後仍等待</td>
      </tr>
      <tr>
          <td>節奏</td>
          <td>固定 cadence + 事件觸發補播</td>
          <td>更新間隔不可預期</td>
      </tr>
      <tr>
          <td>欄位</td>
          <td>時間窗、影響範圍、限制、下一步齊備</td>
          <td>對外只有「調查中」</td>
      </tr>
      <tr>
          <td>對位</td>
          <td>通訊內容對齊 decision log</td>
          <td>內外部敘事彼此衝突</td>
      </tr>
  </tbody>
</table>
<h2 id="通訊拓樸">通訊拓樸</h2>
<p>通訊拓樸要先定義，再進入事故。拓樸的責任是讓每個角色知道資訊要去哪裡收斂、從哪裡發布。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>角色</th>
          <th>典型通道</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部主通道</td>
          <td>IC、scribe、service owner</td>
          <td>incident room / war-room</td>
          <td>收斂事實、同步決策、更新時間線</td>
      </tr>
      <tr>
          <td>外部主通道</td>
          <td>comms lead</td>
          <td>status page</td>
          <td>對外發布已確認事實與下一次更新時間</td>
      </tr>
      <tr>
          <td>外部備援</td>
          <td>comms lead</td>
          <td>vendor status page、社群帳號、客服入口</td>
          <td>主通道失效時維持公告能力</td>
      </tr>
  </tbody>
</table>
<p>內部主通道要偏向決策，外部主通道要偏向已確認事實。兩者共用同一條決策與證據基線，但敘述粒度不同。</p>
<p>外部備援不是選配項。若 status page 管理面與受影響服務同依賴，主通道可能同時失效；備援通道要能在數分鐘內接手公告。</p>
<h2 id="更新欄位與節奏">更新欄位與節奏</h2>
<p>更新內容要固定欄位，避免每次都重寫格式。欄位固定後，對外訊息才可比較、可審核、可回放。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Timestamp</td>
          <td>說明本次更新時間</td>
          <td>2026-05-07T16:30Z</td>
      </tr>
      <tr>
          <td>Scope</td>
          <td>說明受影響區域 / 功能 / 客戶群</td>
          <td>us-east-1 PUT API / 部分租戶</td>
      </tr>
      <tr>
          <td>Known facts</td>
          <td>說明已確認事實</td>
          <td>index subsystem 重啟中</td>
      </tr>
      <tr>
          <td>Known limitation</td>
          <td>說明未確認或資料限制</td>
          <td>目前僅掌握 API 指標，客戶端待補證據</td>
      </tr>
      <tr>
          <td>Mitigation</td>
          <td>說明已執行止血或降級</td>
          <td>限流 + read-only fallback</td>
      </tr>
      <tr>
          <td>Next update</td>
          <td>承諾下一次更新時間</td>
          <td>20 分鐘後或重大進展立即更新</td>
      </tr>
  </tbody>
</table>
<p>更新節奏需要雙軌：固定 cadence + 重大事件補播。固定 cadence 提供可預期性，重大事件補播提供時效性。</p>
<h2 id="主通道失效切換">主通道失效切換</h2>
<p>主通道失效切換的責任是確保事故中仍有可信對外入口。切換條件要事前定義，避免現場臨時爭論。</p>
<table>
  <thead>
      <tr>
          <th>切換觸發條件</th>
          <th>切換動作</th>
          <th>決策紀錄要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>status page 入口不可用超過門檻</td>
          <td>啟動備援通道</td>
          <td>記錄觸發時間、責任人、備援 URL</td>
      </tr>
      <tr>
          <td>主通道更新延遲超過既定 cadence</td>
          <td>由 comms lead 直接補播</td>
          <td>記錄延遲原因與修正措施</td>
      </tr>
      <tr>
          <td>外部依賴造成訊息發布阻塞</td>
          <td>切換到不共依賴的公告入口</td>
          <td>記錄依賴關係與下次演練需修正的拓樸</td>
      </tr>
      <tr>
          <td>內外部敘事版本不一致</td>
          <td>凍結對外新增敘事、先對齊事實版本</td>
          <td>記錄哪個欄位衝突與由誰核定最終版本</td>
      </tr>
  </tbody>
</table>
<p>這個控制面直接對應 AWS S3 2017 的教訓：狀態頁更新入口如果受同一事故影響，團隊必須先維持對外可見性，再補全細節。</p>
<h2 id="與-decision-log-的關係">與 Decision Log 的關係</h2>
<p>每一次對外敘事變更都應在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a> 留下原因與證據。通訊不是附屬工作，它本身就是事故決策的一部分。</p>
<p>最小紀錄包括：本次對外訊息的變更原因、支撐 evidence、風險限制與下次更新條件。這能避免復盤時只看到文案，卻看不到為何當時這樣表述。</p>
<h2 id="常見反模式">常見反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多通道平行對外</td>
          <td>客戶收到互相衝突版本</td>
          <td>固定單一外部主通道</td>
      </tr>
      <tr>
          <td>主通道故障不切換</td>
          <td>status page 卡住卻持續等待</td>
          <td>定義切換門檻與備援通道</td>
      </tr>
      <tr>
          <td>只報「仍在調查」</td>
          <td>缺少時間窗與下一步承諾</td>
          <td>固定更新欄位，至少包含 next update</td>
      </tr>
      <tr>
          <td>通訊與決策脫鉤</td>
          <td>對外說法與內部決策不一致</td>
          <td>所有敘事變更回寫 8.19 decision log</td>
      </tr>
      <tr>
          <td>事故後不回寫通訊缺口</td>
          <td>下次事故重演同樣混亂</td>
          <td>把缺口回寫 8.22 evidence write-back</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.10 stakeholder / 外部狀態頁：對外承諾與補償政策</li>
<li>08.12 <a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a>：跨班次對外節奏不可斷</li>
<li>08.19 incident decision log：保留敘事變更的證據鏈</li>
<li>08.22 incident evidence write-back：回寫主通道失效與備援切換缺口</li>
</ul>
]]></content:encoded></item><item><title>模組四：可觀測性平台</title><link>https://tarrragon.github.io/blog/backend/04-observability/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/</guid><description>&lt;p>可觀測性模組的核心目標是說明服務如何把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 轉成可操作的診斷系統。語言教材會處理標準 logger、執行環境訊號、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 邊界；本模組負責平台、資料流與操作規則。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 OpenTelemetry / Prometheus / Grafana Stack / Datadog / Elastic Stack / Honeycomb / AWS CloudWatch / GCP Cloud Operations / Sentry，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。Error tracking 是獨立子維度（Sentry），跟 metrics / logs / traces 三角互補。&lt;/p>
&lt;p>進入 vendor 比較前，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型&lt;/a> 判斷目前缺的是訊號層、驗證層、響應層還是閉環層。可觀測性 vendor 選型只處理訊號層與部分告警入口；可靠性驗證與事故協作要交給可靠性與事故流程。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &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/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log&lt;/a> aggregation&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、索引、查詢、保留策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics&lt;/a>&lt;/td>
 &lt;td>counter、gauge、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality&lt;/a>、Prometheus&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tracing&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a>、OpenTelemetry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a>&lt;/td>
 &lt;td>SLI、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a>、容量趨勢、服務健康&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a>&lt;/td>
 &lt;td>alert rule、noise control、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> workflow&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>可觀測性選型的核心判斷是團隊缺少哪一種操作訊號。當工程師需要還原事件脈絡時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>；需要趨勢與容量判斷時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>；需要跨服務路徑時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>；需要共同操作入口時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>；需要主動通知時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性模組的核心目標是說明服務如何把 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 與 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 轉成可操作的診斷系統。語言教材會處理標準 logger、執行環境訊號、<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint</a> 與 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 邊界；本模組負責平台、資料流與操作規則。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 OpenTelemetry / Prometheus / Grafana Stack / Datadog / Elastic Stack / Honeycomb / AWS CloudWatch / GCP Cloud Operations / Sentry，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。Error tracking 是獨立子維度（Sentry），跟 metrics / logs / traces 三角互補。</p>
<p>進入 vendor 比較前，先回到 <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型</a> 判斷目前缺的是訊號層、驗證層、響應層還是閉環層。可觀測性 vendor 選型只處理訊號層與部分告警入口；可靠性驗證與事故協作要交給可靠性與事故流程。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log</a> aggregation</td>
          <td><a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、索引、查詢、保留策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics</a></td>
          <td>counter、gauge、<a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>、<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a>、Prometheus</td>
      </tr>
      <tr>
          <td>Tracing</td>
          <td><a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、OpenTelemetry</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a></td>
          <td>SLI、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、容量趨勢、服務健康</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a></td>
          <td>alert rule、noise control、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> workflow</td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>可觀測性選型的核心判斷是團隊缺少哪一種操作訊號。當工程師需要還原事件脈絡時先看 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>；需要趨勢與容量判斷時先看 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>；需要跨服務路徑時先看 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>；需要共同操作入口時先看 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>；需要主動通知時先看 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>。</p>
<p>Log aggregation 適合查單一事件與錯誤脈絡；metrics 適合觀察 error rate、latency、<a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a> 與 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> lag；tracing 適合拆解跨服務 request path；dashboard 適合整合 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a> 與容量趨勢；alert 適合把需要動作的異常送到負責者面前，並連到 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>。</p>
<p>接近真實網路服務的例子包括 checkout 變慢、queue lag 上升、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 斷線增加、Redis <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 增加與下游 API 錯誤率上升。這些場景的共同問題是從症狀回到原因，因此本模組會先處理欄位、關聯、<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a>、查詢、視覺化與告警規則。</p>
<h2 id="訊號情境庫">訊號情境庫</h2>
<p>本模組收的是可重複套用的訊號情境，不收服務級案例庫。服務的長期時間線與事故史，留給可靠性驗證與事故處理兩個模組；可觀測性平台只保留能反覆套用在不同服務上的觀測判讀樣式，讓讀者先知道「該看哪種訊號、如何辨識失真、下一步交給誰」。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>先看訊號</th>
          <th>判讀重點</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>checkout 變慢</td>
          <td>latency <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、downstream error rate</td>
          <td>先分辨是 app latency、DB wait、cache miss 還是外部依賴慢</td>
          <td>需要驗證回歸時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>queue lag 上升</td>
          <td><a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> count</td>
          <td>先判斷是 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 不足、downstream 變慢，還是 <a href="/blog/backend/knowledge-cards/redelivery/" data-link-title="Redelivery" data-link-desc="說明 broker 重新投遞訊息時 consumer 需要承擔的重入責任">redelivery</a></td>
          <td>需要壓力驗證與回放時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>metric cardinality 爆掉</td>
          <td>label explosion、cardinality growth、query latency</td>
          <td>先看是否為維度設計失控、tenant label 過細，或聚合點過多</td>
          <td>需要訊號治理與告警修正時回到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
      </tr>
      <tr>
          <td>trace 斷鏈</td>
          <td>missing <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> propagation error、sample gap</td>
          <td>先看 context 是否跨 thread / task / process 正確傳遞</td>
          <td>需要補 instrumentation 時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>alert 太吵但真正事件沒被抓到</td>
          <td>alert volume、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a> mismatch</td>
          <td>先判斷是閾值太低、維度太窄，還是只盯症狀而沒盯服務健康指標</td>
          <td>需要事故演練與回寫時回到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
      </tr>
  </tbody>
</table>
<p>這種情境庫的責任是定位訊號，服務史由可靠性驗證與事故處理承接。當讀者需要的是平台能力與判讀路由，可觀測性模組的範圍就夠了；當需要的是某個服務怎麼一路演進、怎麼歷次驗證與恢復，那是可靠性與事故模組的工作。</p>
<h2 id="跟可靠性與事故模組的串接">跟可靠性與事故模組的串接</h2>
<p>可觀測性是「觀測 → 驗證 → 事故」閉環的起點，但閉環是雙向的：</p>
<ul>
<li><strong>觀測 → 事故</strong>：訊號（log spike、SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、error rate）觸發告警、進入事故響應流程。判讀邊界由可觀測性定義、響應節奏由事故處理定義。</li>
<li><strong>觀測 → 驗證</strong>：SLO / SLI 量測由可觀測性提供、是 SLO 政策與 chaos hypothesis 的 baseline。沒有可信訊號就沒有可信驗證。</li>
<li><strong>驗證 → 觀測</strong>：驗證需求驅動訊號設計 — chaos experiment 需要新 metric、load test 需要新 dashboard、SLO 政策需要新 alert rule。</li>
<li><strong>事故 → 觀測</strong>：每次事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 揭露偵測缺口（symptom-based alert 缺、訊號太晚、cardinality 不足），回寫到訊號治理。</li>
<li><strong>資安 → 觀測</strong>：資安偵測、稽核證據與資料外洩風險會形成新的 log schema、audit log、alert 與 evidence chain 需求。尤其 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">偵測覆蓋率與訊號治理</a> 會回寫到訊號治理閉環。</li>
<li><strong>觀測 → 資安</strong>：log、trace、audit log 與 service topology 提供資安 triage 的事實基礎，讓 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">稽核追蹤與責任邊界</a> 能把責任鏈落到可查證資料。</li>
<li><strong>詳細閉環說明</strong>：見 <a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">Observability / Reliability / Incident Response 閉環</a>。</li>
</ul>
<h2 id="跟-monitoring-模組的串接">跟 Monitoring 模組的串接</h2>
<p><a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 模組</a> 聚焦非 server 端 runtime — mobile app、web 頁面、本機腳本的行為蒐集、錯誤回報與 SDK 設計。本模組聚焦 server-side observability。兩者的交叉點是 trace context propagation 和 event transport format。</p>
<ul>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a>：概念定位、RUM 與 synthetic 的 server-side 整合</li>
<li><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24 Client-to-Server 觀測串接</a>：從 browser click 到 server span 的完整 trace 鏈路</li>
<li><a href="/blog/monitoring/telemetry-data-dual-use/" data-link-title="監控資料的雙重用途：行為分析與訊號治理" data-link-desc="同一份 event data 如何同時服務行為分析（funnel / cohort / attribution）和訊號治理（cardinality / cost / signal governance）— 格式交叉、治理衝突與分流架構">監控資料的雙重用途</a>：同一份 event data 如何同時服務行為分析（monitoring/08）與訊號治理（04）</li>
<li><a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>：從 DB write 到 observability evidence 的四層端到端串聯</li>
</ul>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理如何產生穩定欄位與執行環境訊號。Backend observability 模組處理收集、儲存、查詢、視覺化、告警與跨服務關聯。</p>
<h2 id="企業案例補充">企業案例補充</h2>
<p>可觀測性的案例補充重點是「訊號平台為什麼這樣設計」，不是工具比較表。閱讀時先抓資料規模、查詢延遲、保留策略與多租戶治理，再對照本模組章節。</p>
<table>
  <thead>
      <tr>
          <th>企業案例</th>
          <th>主要觀測選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.uber.com/en-GB/blog/m3/">M3: Uber’s Open Source, Large-scale Metrics Platform for Prometheus</a></td>
          <td>單機 Prometheus 不足時如何擴成平台層</td>
          <td><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a>、<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</a></td>
      </tr>
      <tr>
          <td><a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Building Cloudflare on Cloudflare</a></td>
          <td>大規模系統內部如何同時做 logs/metrics/traces</td>
          <td><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1</a>、<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</a></td>
      </tr>
      <tr>
          <td><a href="https://blog.cloudflare.com/vision-for-observability/">Cloudflare Observability</a></td>
          <td>監控、分析、鑑識三層能力如何組合</td>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td><a href="https://discord.com/blog/how-discord-stores-trillions-of-messages">How Discord Stores Trillions of Messages</a></td>
          <td>成長後如何從儲存問題回推觀測缺口</td>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>、<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
  </tbody>
</table>
<p>若要擴充企業案例，先到 <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a> 依「企業型態 × 規模階段」挑樣本，再把觀測面教訓回寫到 4.16-4.21。這樣案例擴充會先補齊覆蓋度，再補單點技巧。</p>
<p>第一批缺口回填建議先做三條觀測題目：FinTech 補 audit log completeness 與 evidence traceability（回寫 4.12、4.20）；Gaming 補高峰時段 signal freshness 與 cardinality guardrail（回寫 4.7、4.17）；Healthcare 補資料主權相關的 access evidence 與留存邊界（回寫 4.12、4.18）。</p>
<table>
  <thead>
      <tr>
          <th>產業案例類型</th>
          <th>觀測回寫重點</th>
          <th>章節路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td>金流與帳務事件的 evidence chain、審計 log 完整性</td>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td>高峰流量下的訊號新鮮度、cardinality 膨脹與警示品質</td>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td>存取軌跡可追溯性、資料留存邊界與跨團隊 ownership</td>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a>、<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
  </tbody>
</table>
<p>第一批案例正文入口見 <a href="/blog/backend/04-observability/cases/" data-link-title="可觀測性案例正文" data-link-desc="模組四案例正文入口，將企業案例補充轉成可回寫的訊號判讀文章。">可觀測性案例正文</a>，可直接對應 <code>4.12 / 4.17 / 4.18 / 4.20</code> 的回寫欄位。</p>
<p>第二批觀測遷移案例已補： <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray 到 OTel 轉換</a> 與 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP 導入</a>。兩者可直接回寫到 <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>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>。</p>
<p>反例與規模對照入口： <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 反例</a> / <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，觀測案例要優先保留訊號語意、採樣策略、告警偏差與 SLO 判讀差異。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>可觀測性使用方式會受語言的 logger 生態、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、exception/error model、執行環境 metrics 與 instrumentation SDK 影響。同步 runtime 要保留 request context 與 thread-local 邊界；async runtime 要確認 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 能跨 task 傳遞；輕量並發 runtime 要觀察 task/goroutine 數量、queue lag 與下游等待。動態語言要特別管理 log schema 穩定性；強型別語言則要避免過度包裝導致 trace 與 error chain 斷裂。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1</a></td>
          <td>log schema 與搜尋規劃</td>
          <td>設計欄位、索引與查詢方式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a></td>
          <td>metrics 與 SLI/SLO</td>
          <td>用 counter、gauge、histogram 描述服務健康</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>tracing 與 context link</td>
          <td>追蹤跨服務 request path</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a></td>
          <td>dashboard 與 alert 設計</td>
          <td>讓告警能對應 runbook 與容量趨勢</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5</a></td>
          <td>可觀測性威脅建模（Threat Modeling）</td>
          <td>用盲區、告警失真與資料暴露風險盤點觀測系統</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a></td>
          <td>SLI 量測與 SLO 訊號設計</td>
          <td>把可靠性目標轉成可量測訊號、餵給 6.6 SLO 政策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a></td>
          <td>Cardinality 治理與成本邊界</td>
          <td>把 cardinality 與保留階梯作為平台一級治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8</a></td>
          <td>訊號治理閉環</td>
          <td>把 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 偵測缺口回寫成新訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9</a></td>
          <td>Continuous Profiling</td>
          <td>把 CPU / heap / lock profile 升級為持續訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10</a></td>
          <td>Client-side / Synthetic / RUM</td>
          <td>補 server-side 看不到的 user perceived 訊號</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>Telemetry Pipeline 架構</td>
          <td>把採集到查詢分層治理、定位 pipeline 失敗</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a></td>
          <td>Audit Log 邊界與 PII 治理</td>
          <td>把稽核訊號從 operational log 拆出、按法規治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a></td>
          <td>Service Topology 與 Dependency Map</td>
          <td>把跨服務依賴變成自動發現的觀測訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14</a></td>
          <td>Anomaly Detection</td>
          <td>ML / statistical baseline alert 跟 rule-based 整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15</a></td>
          <td>Cost Attribution / Chargeback</td>
          <td>把 observability 成本拆到團隊 / 服務維度</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16</a></td>
          <td>Observability Readiness Review</td>
          <td>在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
          <td>Telemetry Data Quality</td>
          <td>把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
          <td>Observability Operating Model</td>
          <td>定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19</a></td>
          <td>Debuggability by Design</td>
          <td>把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
          <td>Observability Evidence Package</td>
          <td>把 log、metric、trace、audit 與資料品質限制包成可交接證據</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21</a></td>
          <td>Rule-level CPU Signal Governance</td>
          <td>把規則執行成本變成可觀測訊號，避免小變更在全域 rollout 後形成 CPU 熱點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22</a></td>
          <td>Checkout API Evidence Package 實作示範</td>
          <td>以 checkout 路徑示範 evidence package 如何交接到 gate 與 incident</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a></td>
          <td>觀測查詢設計</td>
          <td>把讀取路徑當系統設計問題：三種查詢模式、storage tiering、pre-aggregation 與資源治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24</a></td>
          <td>Client-to-Server 端到端觀測串接</td>
          <td>用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>註：4.1-4.24 已完成概念層、實作示範與端到端串接正文，案例庫可支援 06 與 08 的路由引用。後續工作重點為案例深挖與跨模組回寫密度提升，而非章節補齊。</p></blockquote>
<h2 id="個案前拓展空間">個案前拓展空間</h2>
<p>個案前拓展的責任是補足讀案例時需要的判讀框架。04 適合補「訊號是否足以支援判讀」這類跨服務能力，不適合展開單一服務的事故史。</p>
<table>
  <thead>
      <tr>
          <th>拓展方向</th>
          <th>補充理由</th>
          <th>先放位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability Readiness Review</td>
          <td>服務上線前需要先知道訊號是否支援事故分級與驗證</td>
          <td>4.16</td>
      </tr>
      <tr>
          <td>Telemetry Data Quality</td>
          <td>觀測資料本身也會缺漏、漂移、偏誤與時間錯位</td>
          <td>4.17</td>
      </tr>
      <tr>
          <td>Observability Operating Model</td>
          <td>dashboard、alert、成本與淘汰需要明確 owner</td>
          <td>4.18</td>
      </tr>
      <tr>
          <td>Debuggability by Design</td>
          <td>診斷能力需要進入 API / async / dependency 設計</td>
          <td>4.19</td>
      </tr>
  </tbody>
</table>
<p>本輪先完成這四個前置控制面，讓後續 06 與 08 文章有穩定的訊號前提可引用。若服務案例暴露的是訊號分類問題，回寫 4.16；若暴露的是資料品質問題，回寫 4.17；若暴露的是 owner 與治理問題，回寫 4.18；若暴露的是架構本身難以診斷，回寫 4.19。</p>
<h2 id="後續深化方向">後續深化方向</h2>
<p>04 後續深化以「案例反例補強、跨模組回寫、證據欄位對齊」為主。可觀測性是 06 與 08 的輸入層，重點在提高 evidence package、data quality 與 incident write-back 的銜接精度。</p>
<table>
  <thead>
      <tr>
          <th>深化方向</th>
          <th>主要責任</th>
          <th>回寫路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>案例反例補強</td>
          <td>補齊遷移失敗與訊號失真案例</td>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td>跨模組對位</td>
          <td>把觀測欄位對齊 release/incident 決策欄位</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>成本與治理</td>
          <td>把採樣、cardinality、chargeback 連到 owner 決策</td>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>、<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15</a></td>
      </tr>
  </tbody>
</table>
<h2 id="實作探討入口">實作探討入口</h2>
<p>進入實作層時，04 建議先從一條最小切片開始：同一個 user journey 建立 <code>SLI + dashboard + alert + evidence query</code> 四件組，再把欄位直接接到 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<p>首篇示範已完成： <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package 實作示範</a>。</p>
<p>完成條件是每篇都能回答四件事：判讀訊號、風險代價、控制面邊界與下一步路由。這樣 06 的 SLO / readiness / experiment safety 與 08 的 intake / decision log / impact assessment 才能引用 04，而不需要在各自章節重寫觀測前提。</p>
<h2 id="跟-infra-可觀測性的分界">跟 Infra 可觀測性的分界</h2>
<p><a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">Infra 模組六：可觀測性與 log</a> 處理基礎設施層的訊號 — log group、CloudWatch metric、alarm 跟資源同生命週期的 IaC 管理。本模組處理應用層的訊號 — 服務的延遲、錯誤率、trace、業務指標。分界的判讀是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」——前者進 infra 的 IaC，後者進本模組的應用程式碼。事故排查時兩層合流：infra alarm 告訴你哪個資源異常，本模組的 trace 告訴你哪個請求路徑受影響。</p>
]]></content:encoded></item><item><title>4.5 可觀測性威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/04-observability/attacker-view-observability-risks/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/attacker-view-observability-risks/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>觀測系統為什麼需要威脅建模&lt;/li>
&lt;li>三類弱點：觀測盲區、告警失真、資料暴露&lt;/li>
&lt;li>每類弱點的判讀流程與修復方向&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的分工&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>可觀測性威脅建模的判讀目標是「觀測系統本身有哪些弱點會讓事故更難處理、更慢收斂、或擴大成資安事件」。觀測系統是事故處理的核心工具 — 工具失靈時，事故的 MTTD（偵測時間）跟 MTTR（修復時間）都會被拉長。&lt;/p>
&lt;p>本章用三類弱點盤點觀測系統：觀測盲區（看不到問題）、告警失真（看到錯的東西）、資料暴露（觀測資料本身變成風險）。每類弱點有各自的判讀流程跟修復方向。&lt;/p>
&lt;p>跟傳統資安威脅建模的差異：資安威脅建模聚焦「攻擊者怎麼入侵系統」；觀測威脅建模聚焦「觀測系統的設計缺陷怎麼讓事故更難處理」。兩者的交叉點在資料暴露 — 觀測資料含 secret 或 PII 時，觀測弱點直接成為資安弱點。&lt;/p>
&lt;h2 id="哪些服務要先做觀測弱點盤點">哪些服務要先做觀測弱點盤點&lt;/h2>
&lt;p>下列情境同時出現時，觀測弱點會快速放大：&lt;/p>
&lt;ul>
&lt;li>服務數量增加，跨服務呼叫變深 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 斷鏈的影響面擴大&lt;/li>
&lt;li>值班依賴告警，但告警常常失真或過量 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 讓真正的問題被淹沒&lt;/li>
&lt;li>調查事故高度依賴人工搜尋 log — 缺少結構化查詢入口&lt;/li>
&lt;li>支援工具與觀測平台可接觸敏感資料 — 觀測資料的存取控制不足&lt;/li>
&lt;/ul>
&lt;h2 id="弱點一觀測盲區">弱點一：觀測盲區&lt;/h2>
&lt;p>觀測盲區是「問題存在但觀測系統看不到」的狀態。盲區的危險在於它讓團隊對系統狀態的判斷建立在不完整的資訊上 — 看起來一切正常，但其實有路徑沒被觀測到。&lt;/p>
&lt;h3 id="常見盲區">常見盲區&lt;/h3>
&lt;p>&lt;strong>Sampling 導致的盲區&lt;/strong>：head sampling 按固定比例丟棄 trace，低流量服務的錯誤樣本可能全部被丟。事故時查 trace 查不到，因為 sampling 把剛好那些 request 的 trace 丟了。修復方向是 tail sampling 或 minimum sample floor（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略&lt;/a>）。&lt;/p>
&lt;p>&lt;strong>Uninstrumented 路徑&lt;/strong>：新上線的服務沒加 instrumentation、async worker 沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a>、third-party SDK 的 HTTP call 沒被攔截。這些路徑在 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service graph&lt;/a> 上不存在，事故時團隊甚至不知道有這條依賴。修復方向是把 instrumentation coverage 作為 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review&lt;/a> 的檢查項。&lt;/p>
&lt;p>&lt;strong>Context 斷鏈形成的局部盲區&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 queue、thread pool、background job 邊界斷掉後，下游的 span 成為孤兒。團隊可以看到下游服務有問題，但看不到跟上游 request 的因果關係。修復策略見 &lt;a href="https://tarrragon.github.io/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&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Log schema 漂移&lt;/strong>：不同服務的 log 用不同欄位名稱記錄同一個概念（&lt;code>request_id&lt;/code> vs &lt;code>req_id&lt;/code> vs &lt;code>requestId&lt;/code>）。查詢時用 &lt;code>request_id&lt;/code> 搜尋會漏掉用其他名稱的服務。修復方向是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema&lt;/a> 的跨服務統一。&lt;/p>
&lt;h3 id="盲區的判讀方式">盲區的判讀方式&lt;/h3>
&lt;ul>
&lt;li>列出所有服務，標記哪些有 trace instrumentation、哪些沒有&lt;/li>
&lt;li>檢查 service graph 跟已知 architecture diagram 的差異 — 差異就是盲區&lt;/li>
&lt;li>用已知的跨服務 request 做 end-to-end trace 驗證，看有沒有斷點&lt;/li>
&lt;li>檢查 sampling policy，確認低流量服務跟 error sample 的保留率&lt;/li>
&lt;/ul>
&lt;h2 id="弱點二告警失真">弱點二：告警失真&lt;/h2>
&lt;p>告警失真是「觀測系統看到了、但告訴你的是錯的或沒用的」。失真比盲區更危險 — 盲區至少讓團隊知道「這裡沒資料、要用其他方式查」；失真讓團隊基於錯誤訊號做判斷。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>觀測系統為什麼需要威脅建模</li>
<li>三類弱點：觀測盲區、告警失真、資料暴露</li>
<li>每類弱點的判讀流程與修復方向</li>
<li>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的分工</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>可觀測性威脅建模的判讀目標是「觀測系統本身有哪些弱點會讓事故更難處理、更慢收斂、或擴大成資安事件」。觀測系統是事故處理的核心工具 — 工具失靈時，事故的 MTTD（偵測時間）跟 MTTR（修復時間）都會被拉長。</p>
<p>本章用三類弱點盤點觀測系統：觀測盲區（看不到問題）、告警失真（看到錯的東西）、資料暴露（觀測資料本身變成風險）。每類弱點有各自的判讀流程跟修復方向。</p>
<p>跟傳統資安威脅建模的差異：資安威脅建模聚焦「攻擊者怎麼入侵系統」；觀測威脅建模聚焦「觀測系統的設計缺陷怎麼讓事故更難處理」。兩者的交叉點在資料暴露 — 觀測資料含 secret 或 PII 時，觀測弱點直接成為資安弱點。</p>
<h2 id="哪些服務要先做觀測弱點盤點">哪些服務要先做觀測弱點盤點</h2>
<p>下列情境同時出現時，觀測弱點會快速放大：</p>
<ul>
<li>服務數量增加，跨服務呼叫變深 — <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 斷鏈的影響面擴大</li>
<li>值班依賴告警，但告警常常失真或過量 — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 讓真正的問題被淹沒</li>
<li>調查事故高度依賴人工搜尋 log — 缺少結構化查詢入口</li>
<li>支援工具與觀測平台可接觸敏感資料 — 觀測資料的存取控制不足</li>
</ul>
<h2 id="弱點一觀測盲區">弱點一：觀測盲區</h2>
<p>觀測盲區是「問題存在但觀測系統看不到」的狀態。盲區的危險在於它讓團隊對系統狀態的判斷建立在不完整的資訊上 — 看起來一切正常，但其實有路徑沒被觀測到。</p>
<h3 id="常見盲區">常見盲區</h3>
<p><strong>Sampling 導致的盲區</strong>：head sampling 按固定比例丟棄 trace，低流量服務的錯誤樣本可能全部被丟。事故時查 trace 查不到，因為 sampling 把剛好那些 request 的 trace 丟了。修復方向是 tail sampling 或 minimum sample floor（見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略</a>）。</p>
<p><strong>Uninstrumented 路徑</strong>：新上線的服務沒加 instrumentation、async worker 沒有 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、third-party SDK 的 HTTP call 沒被攔截。這些路徑在 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service graph</a> 上不存在，事故時團隊甚至不知道有這條依賴。修復方向是把 instrumentation coverage 作為 <a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review</a> 的檢查項。</p>
<p><strong>Context 斷鏈形成的局部盲區</strong>：<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 queue、thread pool、background job 邊界斷掉後，下游的 span 成為孤兒。團隊可以看到下游服務有問題，但看不到跟上游 request 的因果關係。修復策略見 <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</a>。</p>
<p><strong>Log schema 漂移</strong>：不同服務的 log 用不同欄位名稱記錄同一個概念（<code>request_id</code> vs <code>req_id</code> vs <code>requestId</code>）。查詢時用 <code>request_id</code> 搜尋會漏掉用其他名稱的服務。修復方向是 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema</a> 的跨服務統一。</p>
<h3 id="盲區的判讀方式">盲區的判讀方式</h3>
<ul>
<li>列出所有服務，標記哪些有 trace instrumentation、哪些沒有</li>
<li>檢查 service graph 跟已知 architecture diagram 的差異 — 差異就是盲區</li>
<li>用已知的跨服務 request 做 end-to-end trace 驗證，看有沒有斷點</li>
<li>檢查 sampling policy，確認低流量服務跟 error sample 的保留率</li>
</ul>
<h2 id="弱點二告警失真">弱點二：告警失真</h2>
<p>告警失真是「觀測系統看到了、但告訴你的是錯的或沒用的」。失真比盲區更危險 — 盲區至少讓團隊知道「這裡沒資料、要用其他方式查」；失真讓團隊基於錯誤訊號做判斷。</p>
<h3 id="常見失真模式">常見失真模式</h3>
<p><strong>Threshold drift</strong>：alert 的閾值在設定時是合理的（error rate &gt; 1%），但服務改版後基線變了（正常 error rate 從 0.1% 變成 0.5%），閾值沒跟著調。結果是 alert 頻繁觸發但團隊知道是 false alarm — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 開始累積。</p>
<p><strong>Aggregation 掩蓋</strong>：用 average latency 做 alert，tail latency 被掩蓋。Average 200ms 但 p99 是 5 秒 — 1% 的使用者體驗極差但 alert 沒觸發。修復方向是 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 跟 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>。</p>
<p><strong>Alert storm</strong>：單一根因觸發大量 alert（database 慢 → 所有依賴該 database 的服務都觸發 latency alert + error alert + timeout alert）。On-call 收到 20 則通知，分不清哪個是因、哪個是果。修復方向是 alert grouping 跟 inhibition（見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>）。</p>
<p><strong>Stale dashboard</strong>：Dashboard 的 panel 引用的 metric name 已改名、panel 的 query 因 label 變更而回空值。Dashboard 看起來正常（曲線是平的），但其實是 no data 被渲染成 zero。修復方向是 dashboard 的 no-data alert 跟定期審視。</p>
<h3 id="失真的判讀方式">失真的判讀方式</h3>
<ul>
<li>追蹤 alert noise rate（每月有多少 alert 是 actionable 的）</li>
<li>檢查 alert rule 的 threshold 跟當前 baseline 是否對齊</li>
<li>確認 SLI 用 percentile 而非 average</li>
<li>事故復盤時問「這次的事故，alert 有沒有在對的時間告訴我們對的事」</li>
</ul>
<h2 id="弱點三資料暴露">弱點三：資料暴露</h2>
<p>觀測資料本身是風險資產。Log 可能含 secret（API key、token、password）、trace 可能含 PII（使用者 email、電話號碼在 span attribute 中）、dashboard 可能對所有人開放且顯示敏感業務指標。</p>
<h3 id="常見暴露路徑">常見暴露路徑</h3>
<p><strong>Log 含 secret</strong>：SDK 或框架在 error 發生時把完整 request body 寫進 log，body 中的 API key、token、password 跟著進入 log storage。Log storage 的存取控制通常比 secret manager 寬鬆 — 有 log 讀取權限的人都能看到 secret。</p>
<p><strong>Trace attribute 含 PII</strong>：<code>http.url</code> attribute 帶完整 URL（含 query parameter 裡的 email 或 token）、<code>db.statement</code> attribute 帶完整 SQL（含 WHERE 子句的使用者 ID）。Trace storage 的保留期可能比業務資料庫長，PII 在 trace 裡存活的時間超過必要範圍。</p>
<p><strong>Dashboard 權限過寬</strong>：所有工程師都能看所有服務的 dashboard，包含財務相關的 metric（營收、訂單金額分布）。Dashboard 的存取控制粒度通常是「整個 Grafana instance」而非「per-dashboard」。</p>
<p><strong>Collector / pipeline 有管理員權限</strong>：OTel Collector 或 log aggregator 以 admin 權限部署，可以讀寫 secret、修改配置、存取所有資料。Collector 被入侵時，攻擊者可以把 redaction 規則關掉、讓後續的 log 全量暴露。</p>
<h3 id="暴露的修復方向">暴露的修復方向</h3>
<ul>
<li>SDK 端做 redaction（在送出前掃描已知 secret pattern 並替換成 <code>[REDACTED]</code>）</li>
<li>Collector 端做 attribute 過濾（在 pipeline 中移除敏感 attribute）</li>
<li>Log / trace storage 做存取控制（RBAC、per-team 隔離）</li>
<li>Dashboard 做權限分層（業務 dashboard 需要額外授權）</li>
<li>定期掃描 log storage 檢查是否有未 redact 的 secret pattern</li>
</ul>
<p>詳見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安與資料保護</a> 跟 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a>。</p>
<h2 id="設計取捨訊號完整度與成本控制">設計取捨：訊號完整度與成本控制</h2>
<p>觀測覆蓋越完整，盲區越少、事故定位越快。同時儲存、查詢與維護成本也會上升。穩定做法是先定義核心訊號與最低欄位（<a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema</a> 的 correlation fields、<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI</a> 的 availability + latency），再按高風險路徑逐步加深觀測。</p>
<p>「全收」的成本問題見 <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>；「選擇性收」的品質問題見 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀觀測弱點時，按三類依序盤點：</p>
<ol>
<li><strong>盲區</strong>：哪些服務或路徑沒有被觀測到？Sampling 是否丟掉高價值樣本？</li>
<li><strong>失真</strong>：Alert noise rate 有多高？Threshold 跟 baseline 是否對齊？SLI 用的是 average 還是 percentile？</li>
<li><strong>暴露</strong>：Log / trace 是否含 secret 或 PII？Dashboard 權限是否過寬？Collector 的存取權限是否最小化？</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時查 trace 查不到（sampling 丟掉）</li>
<li>Service graph 跟 architecture diagram 有明顯差異（uninstrumented 服務）</li>
<li>Alert noise rate &gt; 30%（threshold drift 或 aggregation 掩蓋）</li>
<li>同一事故觸發 10+ 個 alert（alert storm、缺 grouping / inhibition）</li>
<li>Log grep 到 API key 或 token（redaction 缺失）</li>
<li>Dashboard 對所有人開放且顯示營收指標（權限過寬）</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<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</a>：context 斷鏈的修復策略</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：alert noise control、grouping、inhibition</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>：sampling 策略與保留決策</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance</a>：alert / dashboard 的定期審視</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log</a>：觀測資料的存取控制與稽核</li>
<li><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16 readiness review</a>：instrumentation coverage 的上線前檢查</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：sampling bias 跟 schema drift 的品質問題</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a>：secret management、data masking、存取控制</li>
</ul>
]]></content:encoded></item><item><title>AWS IAM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/</guid><description>&lt;p>AWS IAM 是 AWS 的 cloud resource permission engine — 它回答的問題是「這個身份能對哪一個 AWS resource 做哪一個 API call」。它不是 workforce IdP、也不負責「這個人類是誰」的判定。所有 AWS API 流量（無論來自 console 操作、CI pipeline、Lambda、EC2、跨帳號 partner）最終都要經過 IAM 的 policy 評估、IAM 是 AWS 安全模型的根。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>AWS IAM 是 &lt;em>cloud resource permission engine&lt;/em>、人類 workforce 的 SSO 與 lifecycle 應該走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a> 或外部 IdP（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a>）。Identity Center 把人類映射到 &lt;em>Permission Set&lt;/em>、Permission Set 在每個目標帳號裡實際上是 AWS-Reserved IAM Role — 也就是說：人類登入走 Identity Center、實際的 API 授權判斷一定回到 IAM。兩層責任分清楚、policy 才不會錯放在「誰是誰」的地方。&lt;/p>
&lt;p>AWS IAM 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a> 在 policy model 上設計差異很大。AWS 的表達力最強 — identity-based policy、resource-based policy、Service Control Policy（SCP）、Permission Boundary、Session Policy 是五個獨立的層、最終結果由 &lt;em>Explicit Deny &amp;gt; Org SCP &amp;gt; Resource-based &amp;gt; Identity-based &amp;gt; Permission Boundary &amp;gt; Session Policy&lt;/em> 的評估順序決定。表達力換來的代價是 &lt;em>最容易設定錯&lt;/em>：S3 bucket policy 設錯 = public、KMS key policy 漏一個 condition = 跨帳號可以解密、Trust Policy 沒設 ExternalID = confused deputy 攻擊面。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>哪些 IAM first-class concept（User / Group / Role / Policy / STS）對應到自己的場景、哪些要避免（例如：給人類發 IAM User access key）&lt;/li>
&lt;li>跨帳號信任、CI / 第三方 SaaS 連進 AWS、service-to-service 認證該走 Role assumption / OIDC trust 還是 Roles Anywhere&lt;/li>
&lt;li>SCP、Permission Boundary、resource-based policy 三層上限的疊加方式、何時用哪一層&lt;/li>
&lt;li>CloudTrail + Access Analyzer 的稽核 baseline、出事時的最短取證路徑&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷一個 AWS 帳號的 IAM 配置是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>AWS IAM 是 AWS 的 cloud resource permission engine — 它回答的問題是「這個身份能對哪一個 AWS resource 做哪一個 API call」。它不是 workforce IdP、也不負責「這個人類是誰」的判定。所有 AWS API 流量（無論來自 console 操作、CI pipeline、Lambda、EC2、跨帳號 partner）最終都要經過 IAM 的 policy 評估、IAM 是 AWS 安全模型的根。</p>
<h2 id="服務定位">服務定位</h2>
<p>AWS IAM 是 <em>cloud resource permission engine</em>、人類 workforce 的 SSO 與 lifecycle 應該走 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a> 或外部 IdP（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a>）。Identity Center 把人類映射到 <em>Permission Set</em>、Permission Set 在每個目標帳號裡實際上是 AWS-Reserved IAM Role — 也就是說：人類登入走 Identity Center、實際的 API 授權判斷一定回到 IAM。兩層責任分清楚、policy 才不會錯放在「誰是誰」的地方。</p>
<p>AWS IAM 跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a> 在 policy model 上設計差異很大。AWS 的表達力最強 — identity-based policy、resource-based policy、Service Control Policy（SCP）、Permission Boundary、Session Policy 是五個獨立的層、最終結果由 <em>Explicit Deny &gt; Org SCP &gt; Resource-based &gt; Identity-based &gt; Permission Boundary &gt; Session Policy</em> 的評估順序決定。表達力換來的代價是 <em>最容易設定錯</em>：S3 bucket policy 設錯 = public、KMS key policy 漏一個 condition = 跨帳號可以解密、Trust Policy 沒設 ExternalID = confused deputy 攻擊面。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 IAM first-class concept（User / Group / Role / Policy / STS）對應到自己的場景、哪些要避免（例如：給人類發 IAM User access key）</li>
<li>跨帳號信任、CI / 第三方 SaaS 連進 AWS、service-to-service 認證該走 Role assumption / OIDC trust 還是 Roles Anywhere</li>
<li>SCP、Permission Boundary、resource-based policy 三層上限的疊加方式、何時用哪一層</li>
<li>CloudTrail + Access Analyzer 的稽核 baseline、出事時的最短取證路徑</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 AWS 帳號的 IAM 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 assume 哪個 Role</strong>：所有 Role 的 Trust Policy（誰能呼叫 <code>sts:AssumeRole</code>）、有沒有跨帳號 trust、跨帳號 trust 是否帶 ExternalID、有沒有 <code>*</code> 在 Principal 裡</li>
<li><strong>Resource-based policy 暴露面</strong>：S3 bucket policy、KMS key policy、Lambda function policy、SNS / SQS policy 是否有 <code>Principal: *</code> 或來自非預期帳號；用 <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html">IAM Access Analyzer</a> 找 <em>unintended external access</em></li>
<li><strong>Permission Boundary 與 SCP 是否生效</strong>：開發者建的 Role 是否 attach Permission Boundary（防止 admin 自己給自己升權）、Organization 是否 attach SCP 做整個 OU 的上限</li>
<li><strong>CloudTrail 是否完整、是否進 SIEM</strong>：management event 跟 data event 都開、跨 region、跨帳號、保留期符合稽核要求、特定事件（<code>AssumeRole</code> 失敗、root login、<code>CreateAccessKey</code>）接 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 與 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Role 設計（cross-account / service / OIDC trust）</strong>：所有 <em>持續性</em> 的身份都應該是 Role、不是 IAM User。Service Role（給 EC2 / Lambda / ECS task）是 AWS 內部 service-to-service；Cross-account Role 給 partner 帳號或自家其他帳號用 <code>sts:AssumeRole</code> 進來；OIDC trust 是現代 CI 必備路徑（GitHub Actions / GitLab / 自管 K8s 用短期 OIDC token 換 AWS STS 短期憑證、不在 secret store 存 long-lived access key）。</p>
<p><strong>Policy 種類分工</strong>：identity-based policy attach 在 User / Group / Role 上、回答「這個身份能做什麼」。Resource-based policy attach 在 resource 上（S3 bucket、KMS key、SNS topic、Lambda function）、回答「誰能對這個 resource 做什麼」— 同帳號內 identity-based 跟 resource-based 任一個 allow 就通過、跨帳號 <em>兩邊都要 allow</em>。SCP 是 Organization 層級的上限、不是 grant — SCP allow 不會給任何權限、SCP deny 會擋掉整個 OU 的所有 identity。Permission Boundary 是 <em>user 角度的上限</em>、給 admin 用來限制「我把 admin 權限委派給 developer 後、developer 自己建的 role 不能超過這條線」。</p>
<p><strong>STS 與臨時憑證</strong>：所有 cross-account、service-to-service、人類 console federation 都應該走 STS — <code>sts:AssumeRole</code>（跨帳號 / 跨 role）、<code>sts:AssumeRoleWithSAML</code>（SAML IdP）、<code>sts:AssumeRoleWithWebIdentity</code>（OIDC）、<code>sts:GetFederationToken</code>（外部 broker）。Session 預設 1 小時、最長可設 12 小時（依 Role 設定）。Debug 起手式：<code>aws sts get-caller-identity</code> 確認當前 caller 是誰、是 User、Role 還是 federated session。</p>
<p><strong>Access Key 治理</strong>：IAM User 的 long-lived access key 是 <em>最後手段</em>、用於 break-glass 或無法跑 IMDS / Roles Anywhere 的 legacy。所有 access key 走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、定期 rotation、IAM Access Analyzer 的 unused access finding 找閒置 key。</p>
<p><strong>CloudTrail / Access Analyzer baseline</strong>：CloudTrail organization trail 開到所有帳號、management event 必開、data event（S3 object level、Lambda invoke）依資料敏感度開。Access Analyzer 至少跑 <em>external access</em>（找 resource-based policy 把資源暴露給外部帳號）跟 <em>unused access</em>（找閒置 Role、user、permission）。</p>
<p><strong>Trust Policy / ExternalID</strong>：第三方 SaaS（監控、CSPM、備份服務）要進你的 AWS 帳號時、其 Trust Policy 必須要求 ExternalID — 否則攻擊者只要知道 Role ARN 就能假冒第三方 SaaS 的呼叫端、走 confused deputy 攻擊面（<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html">AWS confused deputy 官方說明</a>）。自家跨帳號 trust 不一定要 ExternalID、第三方一定要。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS IAM</th>
          <th>Google Cloud IAM</th>
          <th>Azure RBAC</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>基本單位</td>
          <td>Policy（attach 到 identity 或 resource）</td>
          <td>Role Binding（principal + role + resource）</td>
          <td>Role Assignment（scope + principal + role）</td>
      </tr>
      <tr>
          <td>隔離邊界</td>
          <td>Account（root）+ Organization SCP</td>
          <td>Project / Folder / Org（階層 inherit）</td>
          <td>Subscription / Management Group（階層 inherit）</td>
      </tr>
      <tr>
          <td>Policy 表達力</td>
          <td>高 — identity / resource / SCP / boundary / session 五層</td>
          <td>中 — Conditional IAM + Organization Policy</td>
          <td>中 — RBAC + Azure Policy 兩層</td>
      </tr>
      <tr>
          <td>Resource-based</td>
          <td>多 service 支援（S3 / KMS / SNS / SQS / Lambda&hellip;）</td>
          <td>較少（GCS / Pub/Sub / KMS 等）</td>
          <td>較少、多走 RBAC 統一</td>
      </tr>
      <tr>
          <td>設定錯誤代價</td>
          <td>高 — bucket / key policy 設錯就 public</td>
          <td>中 — 較統一但精細度也較低</td>
          <td>中 — 階層 inherit 容易誤放</td>
      </tr>
  </tbody>
</table>
<p>AWS IAM 是 <em>表達力最強、最容易設定錯</em> 的雲端 IAM。Google Cloud IAM 設計較統一、policy model 易讀但精細度有限。Azure RBAC 走 inheritance + scope、靠 Management Group 結構治理。三家都不能直接互換、跨雲環境需要在每家自己的 IAM 模型裡建等價的 least-privilege baseline。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Service Control Policy（SCP）</strong>：Organization 層級的上限、用來宣告「整個 OU 永遠不能做什麼」 — 例如禁止 root user 操作、禁止關閉 CloudTrail、禁止在非允許 region 建 resource。SCP 是 <em>deny-list 防護網</em>、不是日常授權；日常授權交給 identity-based policy。SCP 過嚴會擋住合法操作、過鬆等於沒設、設計時要對齊 organization 的安全政策骨幹。</p>
<p><strong>Permission Boundary</strong>：用在 <em>委派 admin</em> 場景 — 公司想讓 platform team 自己建 IAM Role 給應用、但又不想讓他們建出 admin role。Admin 給 platform team 一個 Permission Boundary policy、platform team 建的所有 Role 都會被這個 boundary 限制 <em>上限</em>、就算 attach 了 <code>AdministratorAccess</code> 也只能在 boundary 範圍內生效。</p>
<p><strong>ABAC（attribute-based / tag-based access control）</strong>：大規模 multi-account 環境、每個 service 一個 Role 會 Role 爆炸。ABAC 用 <em>tag</em>（principal tag、resource tag、request tag）做 policy condition — 例如「Role 上有 <code>team=payments</code> tag 的人能操作 <code>team=payments</code> tag 的 resource」。設計成立的前提是 tag 來源可信、不能讓使用者自己改 principal tag。</p>
<p><strong>IAM Roles Anywhere</strong>：給 AWS 之外的 workload（地端 K8s、其他雲、邊緣設備）用 X.509 憑證換 STS 短期憑證。前提是有一個可信的 PKI（自管 CA 或公開 CA）跟 trust anchor。比起把 IAM User access key 放在地端 secret store、Roles Anywhere 是更安全的設計。</p>
<p><strong>OIDC trust（GitHub Actions / GitLab CI / 第三方 CI）</strong>：CI / CD 連 AWS 的標準做法。在 AWS 建一個 OIDC identity provider 指向 CI 的 OIDC issuer、Role 的 Trust Policy condition 限制 <code>repo:org/repo:ref:refs/heads/main</code>、CI workflow 直接 <code>aws sts assume-role-with-web-identity</code>。完全不需要在 CI secret store 存 long-lived AWS access key、token TTL 隨 job 結束自動失效。</p>
<p><strong>Resource-based policy 跨帳號設計</strong>：S3 bucket policy、KMS key policy、SNS / SQS / Lambda policy 都支援跨帳號授權。設計時兩件事必查：Principal 是否包含預期的帳號 / Role ARN、condition 是否限制來源（<code>aws:SourceAccount</code>、<code>aws:SourceArn</code>、<code>aws:PrincipalOrgID</code>）。漏了 condition、就可能讓任何拿到「假裝是某個 service」身份的人都能呼叫 — Capital One 2019 事件本質就是 SSRF 取得 EC2 IMDS 的 Role credential、再用該 Role 的權限去 S3 列舉跟讀取資料、揭示 <em>resource-based policy + identity-based policy 沒有最小化、就會在事故時最大化</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong><code>AccessDenied</code> 但 policy 看起來 allow</strong>：先用 <a href="https://policysim.aws.amazon.com/">IAM Policy Simulator</a> 或 <code>aws iam simulate-principal-policy</code> 重算、確認是 SCP 擋、Permission Boundary 擋、resource-based policy 沒 allow、還是 condition key 不匹配。Explicit Deny 永遠贏。</li>
<li><strong>跨帳號 <code>sts:AssumeRole</code> 失敗</strong>：兩邊都要設 — caller 帳號的 identity-based policy 要 allow <code>sts:AssumeRole</code> 到目標 Role ARN、目標 Role 的 Trust Policy 要 allow caller 的 Principal。漏其一就失敗。</li>
<li><strong>S3 bucket 不小心 public</strong>：用 Access Analyzer 的 external access finding 找、用 <em>Block Public Access</em> 帳號級別開關擋掉（即使 bucket policy 寫了 public、Block Public Access 也會擋）。常見根因：bucket policy 寫 <code>Principal: *</code> 沒加 condition、或 ACL 殘留歷史設定。</li>
<li><strong>Role / access key 殘留</strong>：用 Access Analyzer 的 unused access finding、或 IAM credential report 找超過 90 天沒用的 user / role、配 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的分域分批 rotation 流程清理</li>
<li><strong>第三方 SaaS Role 缺 ExternalID</strong>：稽核第三方 vendor 的 onboarding 文件、若沒要求 ExternalID 是 vendor 自己安全模型有破口、自己這邊也要拒絕這種 onboarding</li>
<li><strong>CloudTrail 落地不全</strong>：Organization trail 沒覆蓋新建帳號、data event 沒開、log 沒進 SIEM、保留期不足 — 這四件事都會讓事故發生時拿不到證據</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類員工 SSO 進 AWS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
      </tr>
      <tr>
          <td>多雲 / SaaS app 統一 SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a></td>
      </tr>
      <tr>
          <td>Google Cloud resource 權限</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>Azure resource 權限</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>Secret / API key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>Key lifecycle / envelope encryption</td>
          <td>AWS KMS vendor 頁（S2 批次撰寫中）+ <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>事件偵測（CloudTrail 以外）</td>
          <td>04 SIEM / detection 工具與 07 SIEM 章節</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>IAM policy JSON 語法完整 reference 與所有 condition key 清單</li>
<li>每個 AWS service 的細部 IAM 動作對照</li>
<li>AWS Organization、Control Tower、Landing Zone 完整建置流程</li>
<li>KMS / Secrets Manager / Certificate Manager 的內部細節（見對應 vendor 頁）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 AWS IAM 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a></td>
          <td>雖是 Microsoft Entra / Exchange Online 事件、對 AWS <em>cross-account role assumption signing chain</em> 提供對照：ExternalID 設計、HSM-bound key、跨帳號 token 驗證一致性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>IAM User access key、STS session、Role trust 的 rotation 必須分域分批、不能單一指令打全部</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>對 IAM Roles Anywhere / OIDC trust 的 signing material 治理啟示：trust anchor、key custody、跨環境驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（AWS KMS vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（CloudTrail / Access Analyzer 訊號如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/">AWS IAM User Guide</a>、<a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/">AWS IAM Identity Center User Guide</a></li>
</ul>
]]></content:encoded></item><item><title>Google Cloud KMS</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/</guid><description>&lt;p>Google Cloud KMS 是 GCP 原生的 key management service、把 envelope encryption、asymmetric signing 與 MAC 等密碼運算集中在受控的 key custodian 內、key material 不離保護邊界。應用端只持 &lt;em>KMS resource name + IAM 權限&lt;/em>、用 &lt;code>Encrypt&lt;/code> / &lt;code>Decrypt&lt;/code> / &lt;code>AsymmetricSign&lt;/code> API 把 plaintext 或 hash 送進 Cloud KMS、key 永遠在 Google 管理的 software 模組或 HSM 內運算完才把結果送回。整個 GCP 的 CMEK（Customer Managed Encryption Key）生態都以 Cloud KMS 為錨點 — GCS bucket、BigQuery dataset、Persistent Disk、Cloud SQL、GKE etcd 都可指定一把 Cloud KMS key 做加密、跟 cloud-native 預設加密（GCP 自管 key、客戶看不到）拉出邊界。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Cloud KMS 的核心定位是 &lt;em>GCP-native envelope encryption + signing 控制面&lt;/em>、用 KeyRing 作為 organizational + locational grouping、CryptoKey + CryptoKeyVersion 作為 key material 的版本軸。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">AWS KMS&lt;/a> 相比、最大差異是 &lt;em>沒有獨立的 Key Policy&lt;/em>：權限完全走 GCP IAM（Role Binding 綁到 KeyRing 或 CryptoKey resource）、好處是跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> 統一治理（同一份 IAM audit、同一套 conditional binding）、代價是少了 AWS KMS Key Policy 那種 &lt;em>key-level 的獨立 deny override&lt;/em>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a> 相比、Cloud KMS 拆得更細：Azure 把 secret + key + certificate 合在同一個 Key Vault service、Google 拆成 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a>（secret）+ Cloud KMS（key）+ Certificate Authority Service（PKI），各 service IAM、quota、audit 獨立。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &amp;#43; 資料主權場景的 key custody">CloudHSM&lt;/a> 相比、Cloud KMS Protection Level=HSM 是 &lt;em>managed HSM&lt;/em>（FIPS 140-2 Level 3、Google 顧 cluster）、CloudHSM 是 &lt;em>single-tenant 專屬 HSM&lt;/em>（客戶顧 cluster、合規隔離更強）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault transit&lt;/a> 相比、Cloud KMS 綁 GCP、Vault transit 可跨雲；但 Vault 自己常用 Cloud KMS 當 auto-unseal master key custodian。&lt;/p></description><content:encoded><![CDATA[<p>Google Cloud KMS 是 GCP 原生的 key management service、把 envelope encryption、asymmetric signing 與 MAC 等密碼運算集中在受控的 key custodian 內、key material 不離保護邊界。應用端只持 <em>KMS resource name + IAM 權限</em>、用 <code>Encrypt</code> / <code>Decrypt</code> / <code>AsymmetricSign</code> API 把 plaintext 或 hash 送進 Cloud KMS、key 永遠在 Google 管理的 software 模組或 HSM 內運算完才把結果送回。整個 GCP 的 CMEK（Customer Managed Encryption Key）生態都以 Cloud KMS 為錨點 — GCS bucket、BigQuery dataset、Persistent Disk、Cloud SQL、GKE etcd 都可指定一把 Cloud KMS key 做加密、跟 cloud-native 預設加密（GCP 自管 key、客戶看不到）拉出邊界。</p>
<h2 id="服務定位">服務定位</h2>
<p>Cloud KMS 的核心定位是 <em>GCP-native envelope encryption + signing 控制面</em>、用 KeyRing 作為 organizational + locational grouping、CryptoKey + CryptoKeyVersion 作為 key material 的版本軸。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> 相比、最大差異是 <em>沒有獨立的 Key Policy</em>：權限完全走 GCP IAM（Role Binding 綁到 KeyRing 或 CryptoKey resource）、好處是跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 統一治理（同一份 IAM audit、同一套 conditional binding）、代價是少了 AWS KMS Key Policy 那種 <em>key-level 的獨立 deny override</em>。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> 相比、Cloud KMS 拆得更細：Azure 把 secret + key + certificate 合在同一個 Key Vault service、Google 拆成 <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>（secret）+ Cloud KMS（key）+ Certificate Authority Service（PKI），各 service IAM、quota、audit 獨立。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a> 相比、Cloud KMS Protection Level=HSM 是 <em>managed HSM</em>（FIPS 140-2 Level 3、Google 顧 cluster）、CloudHSM 是 <em>single-tenant 專屬 HSM</em>（客戶顧 cluster、合規隔離更強）。跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault transit</a> 相比、Cloud KMS 綁 GCP、Vault transit 可跨雲；但 Vault 自己常用 Cloud KMS 當 auto-unseal master key custodian。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>KeyRing 該放哪個 location（global / regional / dual-regional / multi-regional）、為何一旦決定無法搬遷</li>
<li>CryptoKey Version + Primary 版本軸怎麼支撐 rotation、何時該 disable / destroy 舊 version</li>
<li>Protection Level（SOFTWARE / HSM / EXTERNAL）跟 Cloud HSM、External Key Manager 的取捨</li>
<li>CMEK 整合 GCS / BigQuery / Persistent Disk 跟 cloud-native default encryption 的邊界差異</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一份 Cloud KMS 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>KeyRing location 對不對</strong>：production sensitive key 用 region / multi-region、避免不必要的 <code>global</code> KeyRing；location 一旦設定 <em>不能改</em>、key 也搬不出原 KeyRing — 設錯只能建新 KeyRing + 重新加密所有 ciphertext</li>
<li><strong>IAM Conditions 跟 least privilege</strong>：<code>roles/cloudkms.cryptoKeyEncrypterDecrypter</code> 不該綁到 KeyRing level（會放大爆炸半徑）、應綁到具體 CryptoKey；admin 跟 use 角色分離（<code>roles/cloudkms.admin</code> ≠ <code>roles/cloudkms.signer</code>）；敏感 key 加 IAM Condition（時間窗、resource attribute）</li>
<li><strong>Cloud Audit Logs 開到對的層級</strong>：Admin Activity（建 key、改 IAM、destroy version）預設開、Data Access（每次 Encrypt / Decrypt / Sign）<em>預設關</em> — production sensitive key 必須在 IAM audit config 把 Data Access 開、否則「誰用 key 做了什麼」查不到</li>
<li><strong>Protection Level 對齊合規</strong>：production 跟 PII / 金融 / 醫療資料的 key 應走 HSM 或 EXTERNAL、SOFTWARE 只給 dev / 低敏感場景；EKM 對應 <em>資料主權</em>（key 物理上不在 GCP）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 KMS 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>KeyRing 設計</strong>：KeyRing 是 <em>組織單位 + 位置鎖</em>。建議切法：依 <em>環境 + 用途</em> 拆（<code>prod-data-encryption-asia-east1</code>、<code>prod-signing-global</code>、<code>dev-data-encryption-asia-east1</code>），不要全公司一個 KeyRing。Location 選擇：跟資料 colocate（GCS bucket 在 <code>asia-east1</code> 的 key 也放 <code>asia-east1</code> KeyRing、避免跨區延遲與資料主權問題）；signing key 多半放 <code>global</code> 或 multi-region 提高可用性；CMEK 給 BigQuery 時 KeyRing location 必須跟 dataset location 一致、否則綁不上。一個原則：<em>KeyRing location 是一次性決策</em>、上線前確認跟 cloud resource location + 法規要求對齊。</p>
<p><strong>CryptoKey Version 與 Primary</strong>：CryptoKey 有多個 version（<code>projects/.../cryptoKeys/k/cryptoKeyVersions/1</code>、<code>v2</code>、<code>v3</code>）、其中一個是 Primary — 所有 <code>Encrypt</code> API 預設用 Primary version 加密、<code>Decrypt</code> 自動依 ciphertext 內嵌的 version ID 找對應 version 解。Rotation 不是「換 key」、是 <em>建立新 version 並 promote 為 Primary</em>；舊 version 仍可 decrypt 既有 ciphertext（除非手動 disable / destroy）。Destroy 是 24 小時延遲（可在期內 restore）、destroy 之後 ciphertext 永久不可解 — 排程 destroy 前必須確認沒有遺留 ciphertext 還在用該 version。</p>
<p><strong>Auto Rotation</strong>：CryptoKey 可設 <code>rotationPeriod</code>（最短 1 天、預設 90 天）、KMS 在到期時自動建立新 version + promote 為 Primary、app 不需要改 code。Auto rotation 只對 <em>symmetric encryption key</em> 有效；asymmetric key（signing / decryption）不支援 auto rotation、需要手動建 version + 通知 consumer 更新 public key。注意 auto rotation 是 <em>key version 換</em>、不會 re-encrypt 既有資料 — 真正的 <em>資料 re-encryption</em> 是另一條工作流（讀回 ciphertext + 用新 Primary 重加密寫回）、要依 CMEK-integrated resource 各自規劃。</p>
<p><strong>Protection Level</strong>：SOFTWARE（軟體運算、最便宜、FIPS 140-2 Level 1）/ HSM（Cloud HSM 後端、FIPS 140-2 Level 3、key 物理上在 Google 管理的 HSM cluster）/ EXTERNAL（External Key Manager、key 在客戶自管的外部 HSM、Cloud KMS 把運算委派出去）。Production sensitive key 應走 HSM、SOFTWARE 給 dev / 低敏感場景。Protection Level 是 <em>CryptoKey 建立時決定</em>、不能改 — 要升等只能建新 CryptoKey + 遷移 ciphertext。</p>
<p><strong>CMEK 整合</strong>：CMEK 把 Cloud KMS key 綁到 GCS bucket / BigQuery dataset / Persistent Disk / Cloud SQL / GKE etcd / Pub/Sub topic / Dataflow job 等 resource。設定方式：cloud service 的 service account（如 <code>service-PROJECT_NUMBER@gs-project-accounts.iam.gserviceaccount.com</code>）取得該 CryptoKey 的 <code>cryptoKeyEncrypterDecrypter</code> 權限、resource 在加密時自動呼叫 KMS。跟 cloud-native default encryption（GCP 自己管 key）的差異：CMEK 下 <em>客戶可隨時 disable key 讓整個 bucket / dataset 立刻無法解</em>（compliance kill switch）、default encryption 沒這個能力。代價是 KMS 故障 = CMEK-integrated resource 全部讀寫卡住、所以 production KMS 自身 SLA 跟 monitoring 是 cluster-level dependency。</p>
<p><strong>External Key Manager (EKM)</strong>：GCP 把 encryption / decryption operation <em>委派</em> 給客戶自管的外部 HSM（Thales、Equinix SmartKey、Fortanix 等）、key 物理上不在 GCP、Cloud KMS 只是個 proxy。適合 <em>資料主權</em> 嚴格的場景（歐盟金融、政府機密、跨境法規）— 客戶撤銷外部 HSM 的存取、GCP 立刻無法解密、達成「Google 看不到資料」的合規承諾。代價：每次 Encrypt / Decrypt 都打外部 HSM、延遲跟可用性受外部 HSM 影響、運維複雜度大幅上升。</p>
<p><strong>IAM 整合</strong>：用 Role Binding 控制存取（綁在 KeyRing 或 CryptoKey resource）— <code>roles/cloudkms.cryptoKeyEncrypterDecrypter</code>（Encrypt + Decrypt）/ <code>roles/cloudkms.signer</code>（AsymmetricSign）/ <code>roles/cloudkms.signerVerifier</code>（含 public key 取得）/ <code>roles/cloudkms.admin</code>（建 key、改 IAM）。對應 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 的 conditional binding、可加時間窗、resource attribute、access level 條件。跟 AWS KMS 的關鍵差異：<em>沒有 Key Policy</em> — 所有授權都在 IAM、好處是統一治理、代價是少了 key-level 的獨立 deny override（AWS KMS Key Policy 可寫「即使 IAM 給了 admin、仍 deny destroy」、Cloud KMS 要用 Organization Policy 或 IAM Deny 達成類似效果）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google Cloud KMS</th>
          <th>AWS KMS</th>
          <th>Azure Key Vault</th>
          <th>Vault transit</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>GCP managed</td>
          <td>AWS managed</td>
          <td>Azure managed</td>
          <td>self-hosted 或 HCP</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>弱 — 綁 GCP</td>
          <td>弱 — 綁 AWS</td>
          <td>弱 — 綁 Azure</td>
          <td>強 — 同介面跨雲</td>
      </tr>
      <tr>
          <td>Multi-region key</td>
          <td>用 multi-region KeyRing（key material 在多 region 鏡像）</td>
          <td>Multi-Region Key 較直接（單一 key ID、跨 region 自動同步）</td>
          <td>支援 geo-replication</td>
          <td>跨雲、需自行設計 replication</td>
      </tr>
      <tr>
          <td>Key 權限模型</td>
          <td>純 IAM Role Binding、無 Key Policy</td>
          <td>IAM + 獨立 Key Policy（雙層授權）</td>
          <td>RBAC + Access Policy 雙模式</td>
          <td>Vault policy（path-based）</td>
      </tr>
      <tr>
          <td>HSM 選項</td>
          <td>Protection Level=HSM（managed、FIPS 140-2 L3）</td>
          <td>AWS KMS HSM-backed（預設）+ CloudHSM（專屬）</td>
          <td>Premium tier + Managed HSM</td>
          <td>依賴後端 KMS / HSM</td>
      </tr>
      <tr>
          <td>外部 key 託管</td>
          <td>External Key Manager (EKM)</td>
          <td>XKS (External Key Store)</td>
          <td>BYOK + Managed HSM</td>
          <td>自管 HSM unseal</td>
      </tr>
      <tr>
          <td>Audit</td>
          <td>Cloud Audit Logs（Data Access 需手動開）</td>
          <td>CloudTrail（KMS event 自動進）</td>
          <td>Azure Monitor / Activity Log</td>
          <td>Vault audit device</td>
      </tr>
      <tr>
          <td>CMEK 整合廣度</td>
          <td>GCS / BQ / PD / Cloud SQL / GKE etcd / Pub/Sub / Dataflow</td>
          <td>S3 / EBS / RDS / DynamoDB / Lambda env</td>
          <td>Storage / SQL / Cosmos / Disk</td>
          <td>不適用（app-level）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>GCP-heavy、需 CMEK 整合、Workload Identity Federation 已主導</td>
          <td>AWS-heavy、需 Multi-Region Key + Key Policy 精細控制</td>
          <td>Azure-heavy、需要 secret + key 統一治理</td>
          <td>跨雲、需要 app-level encryption-as-a-service</td>
      </tr>
  </tbody>
</table>
<p>選 Cloud KMS 的核心訴求：<em>GCP 是主力雲</em> + 需要 CMEK 把 GCS / BigQuery / PD / Cloud SQL 的加密 key custody 拉回客戶手上 + 接受 IAM-only 授權模型。需要 <em>跨雲統一 key custody</em> 走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault transit</a> 或 EKM；需要 <em>單一專屬 HSM 隔離</em> 走 <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a> 或 EKM 接 on-prem HSM。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>External Key Manager (EKM) 與資料主權</strong>：EKM 讓 key 物理上不在 GCP、Cloud KMS 變成 proxy 把 cryptographic operation 委派給客戶自管 HSM。常見部署：金融 / 政府用 <em>EKM via VPC</em>（外部 HSM 在客戶 VPC 內、Cloud KMS 走 PSC 連線、延遲較低）、跨境合規用 <em>EKM via Internet</em>（HSM 在第三方 KMS provider、延遲較高但治理邊界更乾淨）。代價：每次 Encrypt / Decrypt = 一次外部呼叫、CMEK-integrated resource 的讀寫吞吐量受外部 HSM 限制、外部 HSM 故障 = 整個 GCP 端讀寫卡住。</p>
<p><strong>Cloud HSM（Protection Level=HSM）</strong>：把 CryptoKey 物理上鎖在 Google 託管的 FIPS 140-2 Level 3 HSM cluster 內、key 不可 export、所有 cryptographic operation 在 HSM 邊界內完成。對應 <a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a> 的對照啟示：signing key 一旦能被 export 或從 memory crash dump 撈出、整個信任鏈崩 — HSM-bound key 從設計上斷掉這條路徑。代價：HSM 後端比 SOFTWARE 貴、operation 延遲略高（典型多 &lt; 10ms）、quota 也獨立計算。</p>
<p><strong>Asymmetric Key 做 JWT signing</strong>：CryptoKey purpose=<code>ASYMMETRIC_SIGN</code> 配 algorithm（RSA / EC）、app 透過 <code>AsymmetricSign</code> API 把 JWT header+payload 的 hash 送進 KMS、KMS 回 signature。Public key 走 <code>GetPublicKey</code> API 取得、給 JWKS endpoint 對外發布。優勢：private key 不離 KMS、即使 app server compromise 也無法搬走 signing key；劣勢：每次簽名都 round-trip 一次 KMS、高 QPS 場景要算 quota 跟延遲（典型 ~10-30ms / sign）。</p>
<p><strong>跟 Google Secret Manager 的 CMEK 整合</strong>：<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> 預設用 GCP 管的 key 加密 secret、若要 <em>客戶管 key</em>、可設 CMEK 把 GSM 的 secret 用客戶 Cloud KMS key 加密。意義：disable Cloud KMS key 立刻讓 GSM secret 不可讀（compliance kill switch）— 但代價是 KMS 故障 = GSM 也卡住、是強耦合 dependency。</p>
<p><strong>Multi-region key</strong>：Cloud KMS 的 multi-region KeyRing（如 <code>us</code>、<code>europe</code>、<code>asia</code>）讓 key material 在多 region 鏡像、提高可用性但加密 / 解密延遲較高。AWS KMS 的 Multi-Region Key 設計不同（單一 key ID 跨 region 同步、有獨立的 primary / replica 角色）— 跨雲遷移 / 多雲 active-active 設計時要留意這個差異、Cloud KMS multi-region 比較像 <em>單一邏輯 key 多 region 可用</em>、不是 <em>多 region 各自獨立可寫</em>。</p>
<p><strong>Import 自有 key material（BYOK）</strong>：Cloud KMS 可 import 客戶自產的 key material（透過 wrapping key 包覆後上傳）、適合需要 <em>客戶端 key generation 證據鏈</em> 的合規場景。代價：import 的 key 不能 auto rotate（rotation 必須客戶端重新產 key 再 import），且 SOFTWARE / HSM Protection Level 都支援、EXTERNAL 不適用（EXTERNAL 本來就在外部 HSM、不走 import 路徑）。</p>
<p><strong>Organization Policy 與防護欄</strong>：跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 整合的 Org Policy 可在 organization-level 強制 <em>只允許 HSM / EXTERNAL key</em>（<code>constraints/gcp.restrictNonCmekServices</code>）、防止工程師建出 SOFTWARE key 處理敏感資料。這層防護欄比依賴 reviewer 紀律有效、屬於 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 同類「規約靠系統而非紀律」的設計。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>KeyRing location 設錯</strong>：KeyRing 建在 <code>global</code>、要綁 <code>asia-east1</code> 的 BigQuery dataset CMEK — 綁不上、location 不能改、只能建新 KeyRing + 重新加密 — 上線前 review KeyRing location 跟 resource location 對齊</li>
<li><strong>Data Access audit 沒開</strong>：production 用 Cloud KMS 做 signing、事故時要查 <em>誰用 key 簽了什麼</em>、發現只有 Admin Activity log、沒有 Decrypt / Sign 記錄 — IAM audit config 加 <code>dataAccess</code> log type、留意 audit log 自己會增加成本與 quota</li>
<li><strong>CMEK key disable 後 resource 全卡</strong>：disable CryptoKey 想做 compliance 演練、整個 GCS bucket 讀寫立刻 503 — disable 是 <em>全或無</em>、要演練得排維護窗、有 rollback 計畫（re-enable 後恢復）</li>
<li><strong>Auto rotation 設定 + asymmetric key</strong>：以為 asymmetric signing key 也會 auto rotate、上線數月後發現 version 1 還在用 — asymmetric key 不支援 auto rotation、要手動建 version + 通知 JWKS consumer</li>
<li><strong>IAM Role 過寬</strong>：給整個 KeyRing <code>cryptoKeyEncrypterDecrypter</code>、單一 service account 可以解所有 key — 改綁到具體 CryptoKey、加 IAM Condition</li>
<li><strong>EKM 外部 HSM 故障</strong>：外部 HSM 連線中斷、Cloud KMS 端 Encrypt / Decrypt 全 fail、所有 CMEK-integrated resource 讀寫卡住 — EKM 需要 dual HSM redundancy + Cloud KMS 端 monitoring alert</li>
<li><strong>Destroy 後資料不可解</strong>：CryptoKeyVersion destroy 後 24 小時 grace period 過了、發現某個 backup 還是用該 version 加密 — destroy 前必須跑 inventory 確認沒有 ciphertext 還掛在該 version</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only 加密 + 需 Key Policy 精細控制</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a></td>
      </tr>
      <tr>
          <td>Azure-only 加密 + 需 secret + key 同治理</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></td>
      </tr>
      <tr>
          <td>跨雲統一 encryption-as-a-service</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> transit engine</td>
      </tr>
      <tr>
          <td>單一專屬 HSM 隔離 / 跨雲合規</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></td>
      </tr>
      <tr>
          <td>GCP secret 管理（非 key）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a></td>
      </tr>
      <tr>
          <td>GCP IAM 治理基底</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>公開憑證 / PKI</td>
          <td>Certificate Authority Service（GCP）或 <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>Secret rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Cloud KMS 完整 API reference 跟 <code>gcloud kms</code> CLI 詳盡用法</li>
<li>Cloud HSM partition 內部架構、FIPS 140-2 Level 3 驗證細節</li>
<li>EKM 各 partner（Thales / Fortanix / Equinix）的整合步驟與 API 對照</li>
<li>BigQuery / GCS / Cloud SQL 各自 CMEK 設定的完整教學</li>
<li>Cloud KMS pricing 詳盡計算（key version 數、operation 次數、HSM 加成）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Cloud KMS 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Cloud KMS 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a></td>
          <td>Cloud KMS Protection Level=HSM 把 signing key 鎖在硬體、不可 export、跟 HSM-bound mindset 同源 — signing key 一旦能 export 整條信任鏈崩</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>Asymmetric Key + Cloud Audit Data Access 是 <em>誰用 key 簽什麼</em> 的稽核基礎、預設關閉的 Data Access log 在 production 必須開、否則事故時無證據</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Auto Rotation 是 vendor-controlled、但 CMEK 整合的 GCS bucket / BQ dataset 的 <em>re-encryption schedule</em> 還是要自己管、否則 rotation 只換 key version、舊資料還是用舊 version</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>（KMS 為 TLS / signing key 的 root custodian）、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></li>
<li>平行（secret）：<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>上游（IAM）：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（Cloud KMS 權限完全走 IAM Role Binding）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（KMS 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://cloud.google.com/kms/docs">Cloud KMS Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Google DLP</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/</guid><description>&lt;p>Google DLP（Data Loss Prevention、2023 重新命名為 &lt;em>Sensitive Data Protection / SDP&lt;/em>）是 GCP 原生的敏感資料 &lt;em>discovery + classification + transformation&lt;/em> 服務。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a> / AWS Macie / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy&lt;/a> 的差異不在「能不能發現 PII」、而在 &lt;em>發現之後能做多少事&lt;/em> — Google DLP 的核心優勢是 transformation 層（masking / Format-Preserving Encryption / tokenization / k-anonymity / differential privacy），不只是 detection。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Google DLP 的核心定位是 &lt;em>infrastructure-level 敏感資料治理&lt;/em>、跨 GCS / BigQuery / Cloud SQL / 任意 Inspect API input 的 PII 發現與去識別化。三層能力堆疊：&lt;em>Discovery&lt;/em>（背景 scan GCS bucket / BigQuery table / Cloud SQL instance 找 PII / payment / credential）、&lt;em>Classification&lt;/em>（150+ 預定義 infoType + custom infoType 組合）、&lt;em>Transformation&lt;/em>（redact / mask / replace / pseudonymize / Format-Preserving Encryption / k-anonymity / differential privacy）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a> 比、Purview 走 &lt;em>information protection&lt;/em>（sensitivity label + Office docs + Microsoft 365）+ DLP、Google DLP 走 &lt;em>infrastructure-level data scan + transformation&lt;/em>；兩者解不同層、企業若 Office docs / SharePoint 為主走 Purview、cloud data warehouse / object storage 為主走 Google DLP。跟 AWS Macie 比、Macie 限 S3 + EBS / RDS snapshot、Google DLP 跨 GCS + BigQuery + Cloud SQL + 任意 Inspect API content（含 streaming / on-prem 透過 API call）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy&lt;/a> 比、Google DLP 是 &lt;em>detection + transformation&lt;/em>、Cloud-native policy 是 &lt;em>access control&lt;/em>；production 常組合使用 — DLP 發現敏感欄位 → policy 限制誰能 access → 必要時 DLP transformation 在 query time 自動 redact。&lt;/p></description><content:encoded><![CDATA[<p>Google DLP（Data Loss Prevention、2023 重新命名為 <em>Sensitive Data Protection / SDP</em>）是 GCP 原生的敏感資料 <em>discovery + classification + transformation</em> 服務。它跟 <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> / AWS Macie / <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> 的差異不在「能不能發現 PII」、而在 <em>發現之後能做多少事</em> — Google DLP 的核心優勢是 transformation 層（masking / Format-Preserving Encryption / tokenization / k-anonymity / differential privacy），不只是 detection。</p>
<h2 id="服務定位">服務定位</h2>
<p>Google DLP 的核心定位是 <em>infrastructure-level 敏感資料治理</em>、跨 GCS / BigQuery / Cloud SQL / 任意 Inspect API input 的 PII 發現與去識別化。三層能力堆疊：<em>Discovery</em>（背景 scan GCS bucket / BigQuery table / Cloud SQL instance 找 PII / payment / credential）、<em>Classification</em>（150+ 預定義 infoType + custom infoType 組合）、<em>Transformation</em>（redact / mask / replace / pseudonymize / Format-Preserving Encryption / k-anonymity / differential privacy）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 比、Purview 走 <em>information protection</em>（sensitivity label + Office docs + Microsoft 365）+ DLP、Google DLP 走 <em>infrastructure-level data scan + transformation</em>；兩者解不同層、企業若 Office docs / SharePoint 為主走 Purview、cloud data warehouse / object storage 為主走 Google DLP。跟 AWS Macie 比、Macie 限 S3 + EBS / RDS snapshot、Google DLP 跨 GCS + BigQuery + Cloud SQL + 任意 Inspect API content（含 streaming / on-prem 透過 API call）。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> 比、Google DLP 是 <em>detection + transformation</em>、Cloud-native policy 是 <em>access control</em>；production 常組合使用 — DLP 發現敏感欄位 → policy 限制誰能 access → 必要時 DLP transformation 在 query time 自動 redact。</p>
<p>關鍵張力：<em>content scanned 計費</em> ↔ <em>偵測覆蓋率</em>。DLP API 按 scanned bytes 計費、整 BigQuery dataset full scan 在 PB-scale 跟 SIEM ingestion 同類痛點。實務應該分 <em>sample scan</em>（每 dataset 抽 1% 找 infoType 分布）+ <em>full scan</em>（高敏感 dataset 才完整 scan）+ <em>streaming scan</em>（write path 即時擋）三層。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Google DLP 在 GCP 資料保護 stack 中承擔哪一段（discovery / classification / transformation）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 管 DLP service account、BigQuery column-level security 補 access control）</li>
<li>infoType / Inspection Job / transformation 種類的選用判準（什麼場景 mask、什麼場景 FPE、什麼場景 k-anonymity）</li>
<li>計費 trap 的應對（sample scan + full scan 分層、Pub/Sub trigger 避免重複 scan）</li>
<li>何時用 Google DLP、何時走 Purview / Macie / Cloud-native policy 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Google DLP deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰跑 Inspection Job</strong>：DLP service account 的 IAM role（<code>roles/dlp.user</code> / <code>roles/dlp.jobsEditor</code>）、能 scan 哪些 project / bucket / dataset、findings 寫進哪個 BigQuery table、誰能讀 findings</li>
<li><strong>infoType coverage</strong>：是否覆蓋 organization-specific PII（員工 ID / 客戶 ID 用 custom infoType + dictionary）、預定義 infoType 是否 enable 對應業務的（PCI 場景需 CREDIT_CARD_NUMBER + Luhn check、HIPAA 場景需 healthcare infoType）</li>
<li><strong>Transformation lifecycle</strong>：發現 PII 後做什麼（自動 quarantine bucket / 自動 redact view / Pub/Sub trigger Cloud Function）、transformation 是 <em>one-way</em>（mask / redact）還是 <em>reversible</em>（FPE / tokenization 需 key management 走 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a>）</li>
<li><strong>Cost 治理</strong>：scan 頻率 vs scan scope 的策略、是否分 sample / full / streaming 三層、findings retention policy（findings table 本身也是敏感資料、不該無限保留）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>使用模式：Inspect API vs Inspection Job</strong>：DLP 有兩種呼叫模式 — <em>Inspect API</em> 走同步單次 scan（小 payload、即時 mask、API 寫入前的 streaming gate）、<em>Inspection Job</em> 走非同步批次 scan（大 dataset、結果存 BigQuery findings table、Pub/Sub trigger 後續 workflow）。production 通常混用：write path（Cloud Function / API gateway）走 Inspect API 即時擋住敏感資料寫進儲存、背景 Inspection Job 對既有 dataset 跑覆盤。</p>
<p><strong>infoType 是 first-class concept</strong>：infoType 不是 regex、是 <em>PII 分類單位</em>。預定義 150+ 種（CREDIT_CARD_NUMBER / EMAIL_ADDRESS / US_SOCIAL_SECURITY_NUMBER / IP_ADDRESS / GENERIC_ID / PERSON_NAME 等）、各帶內建驗證邏輯（CREDIT_CARD_NUMBER 內建 Luhn check 比純 regex 精準、減少 FP）。Custom infoType 三種：<em>regex pattern</em>（自訂 regex）、<em>dictionary</em>（明確 token list、例員工 ID 全集）、<em>hotword rule</em>（context-aware、附近出現特定字才認、例「身分證」附近的數字才認 ID）。FP rate 直接由 infoType 精度決定、production rule 應該優先用預定義 infoType + hotword 限縮。</p>
<p><strong>Transformation 種類遠不只 mask</strong>：DLP 的 transformation 是它跟其他 discovery-only 工具的核心差異。<em>Redact</em> 完全刪除（query result 看不到欄位）；<em>Mask</em> 保留長度替換字元（<code>****1234</code>）；<em>Replace</em> 替換成固定字串（<code>[REDACTED]</code>）；<em>Pseudonymize / Tokenization</em> 一致性 token（同樣 input 給同樣 output、可做 join 但不可逆）；<em>Format-Preserving Encryption (FPE)</em> 保留長度 / format 的可逆加密（key 在 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a>、analyst 查 anonymized data + 必要時授權 reverse）；<em>k-anonymity / l-diversity</em> aggregate 到至少 k 個 record 才公開（防止 quasi-identifier re-identification）；<em>Differential privacy</em> 加 noise 保證 statistical privacy（aggregated analytics 用）。後三項是 production analytics 場景的關鍵 — 不是「藏起來」而是「可用但保護」。</p>
<p><strong>跟 BigQuery 深度整合</strong>：DLP 可 inline scan BigQuery column、findings 自動寫回 metadata。配合 BigQuery <em>column-level security</em>（policy tag）+ <em>authorized view</em> 做「敏感 column 只給特定 role + 自動 redact 給其他 role」。Production 模式：DLP Inspection Job 跑完後、自動 apply policy tag 到含 PII 的 column、無 tag access 的 query 自動失敗或 mask。</p>
<p><strong>跟 Cloud Storage 整合</strong>：可 schedule 掃 bucket 整批檔案、發現後可自動 <em>quarantine</em>（移到隔離 bucket、不同 IAM、警告 owner）。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a> 的對照：backup bucket 應該獨立 DLP scan、含 credential 的 backup 走獨立 quarantine bucket + 不同 IAM 邊界、不是放在跟 dev backup 同一個 bucket。</p>
<p><strong>Pub/Sub trigger workflow</strong>：Inspection Job 完成後可 publish 到 Pub/Sub topic、Cloud Function 訂閱後執行 — 自動 quarantine / 自動通知 owner / 自動寫進 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">SIEM</a> findings index / 觸發 BigQuery policy tag update。這是 detection → response 自動化的 first-class pattern、不是後加的 webhook。</p>
<p><strong>IAM 邊界</strong>：DLP service account 需要讀 source data（<code>roles/storage.objectViewer</code> / <code>roles/bigquery.dataViewer</code>）+ 寫 findings（<code>roles/bigquery.dataEditor</code> to findings dataset）+ 呼叫 DLP API（<code>roles/dlp.user</code>）。service account 本身是高敏感 — 它能讀整個 organization 的 PII、應該走 short-lived credential（<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Workload Identity Federation</a>）+ 嚴格 audit。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google DLP</th>
          <th>Microsoft Purview</th>
          <th>AWS Macie</th>
          <th>Cloud-native data policy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心能力</td>
          <td>Discovery + classification + <strong>transformation</strong></td>
          <td>Sensitivity label + DLP + Office docs</td>
          <td>Discovery + classification（無 transform）</td>
          <td>Access control + column-level security</td>
      </tr>
      <tr>
          <td>Data source 範圍</td>
          <td>GCS + BigQuery + Cloud SQL + 任意 Inspect API</td>
          <td>Microsoft 365 + SharePoint + Azure data</td>
          <td>S3 + EBS / RDS snapshot 限定</td>
          <td>BigQuery / S3 / Snowflake 各自 native</td>
      </tr>
      <tr>
          <td>Transformation</td>
          <td>mask / FPE / tokenize / k-anonymity / DP（全套）</td>
          <td>redact + Office sensitivity label</td>
          <td>無 — 只 detection</td>
          <td>無 — 只 access control</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>按 content scanned（GB）</td>
          <td>按 user / asset / 流量</td>
          <td>按 storage scanned（GB） + bucket count</td>
          <td>多半含在 cloud platform、policy 規模相關</td>
      </tr>
      <tr>
          <td>Custom 分類能力</td>
          <td>infoType (regex + dictionary + hotword)</td>
          <td>sensitive info type + classifier (ML)</td>
          <td>managed data identifier + custom</td>
          <td>tag-based / column-level、無 content scan</td>
      </tr>
      <tr>
          <td>Healthcare / PHI</td>
          <td>Cloud DLP for Healthcare（FHIR / DICOM）</td>
          <td>Purview Healthcare data + Microsoft 365 PHI</td>
          <td>有限</td>
          <td>無原生 PHI 認知</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>GCP-first + BigQuery / GCS 為 PII 儲存層</td>
          <td>Microsoft 365 / Office docs / SharePoint 為主</td>
          <td>AWS-only + S3 為 PII 儲存層</td>
          <td>已知敏感 column、想做 access control 不做 mask</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — transformation 邏輯耦合 DLP API</td>
          <td>高 — sensitivity label 跟 Microsoft 365 深綁</td>
          <td>低 — 只是 finding 跟 alert</td>
          <td>低 — policy 是 metadata</td>
      </tr>
  </tbody>
</table>
<p>選 Google DLP 的核心訴求：<em>GCP 為主資料平台 + BigQuery / GCS 有大量 PII + 需要 transformation（不只 detection）+ 合規（GDPR / HIPAA / PCI）需要 column-level redaction / tokenization</em>。on-prem 為主或 Office docs 為主走 Purview、AWS-only 走 Macie + S3 policy。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Custom infoType 三層組合</strong>：production 自家業務的 PII（員工 ID / 客戶 ID / 內部 case ID）需要 custom infoType。三種組合：<em>regex</em> 抓 pattern（員工 ID 格式 <code>EMP-\d{6}</code>）、<em>dictionary</em> 抓明確 token list（內部 case ID 全集、月更新）、<em>hotword</em> 限縮 context（附近出現「員工」「ID」才認、避免一般 6 位數字誤判）。三者組合的 FP rate 比單獨 regex 低一個量級。</p>
<p><strong>Format-Preserving Encryption (FPE) vs Tokenization</strong>：兩者都產生「外觀像原值但不是原值」的替換。<em>FPE</em> 是可逆加密、key 在 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a>、analyst 在 anonymized data 工作 + 必要時走授權流程 reverse（例：客服需要看完整信用卡號處理退款）。<em>Tokenization</em> 是 deterministic mapping、同樣 input 給同樣 output、可做 join 分析但 token table 不存（理論上不可逆、實務上看 implementation）。選擇判準：<em>需要分析 join 同一 user 跨 dataset</em> 用 tokenization、<em>需要授權 reverse</em> 用 FPE、<em>只要遮蔽不需要還原</em> 用 mask / redact。</p>
<p><strong>k-anonymity / l-diversity / differential privacy</strong>：解決 <em>quasi-identifier re-identification</em> 問題 — 即使欄位不是直接 PII（如 ZIP + 性別 + 年齡）、組合起來能反推個人。<em>k-anonymity</em> 保證每個 record 在 quasi-identifier 上至少跟 k-1 個其他 record 一樣（典型 k=5）。<em>l-diversity</em> 進一步保證 sensitive attribute 在每組內至少 l 個不同值（防止 homogeneity attack）。<em>Differential privacy</em> 加 calibrated noise 到 aggregate query 結果、保證個別 record 加入或刪除對結果影響有 bound。Risk Analysis API 可估算 dataset 的 k-anonymity / l-diversity 風險、不需要先 transform 才知道風險。</p>
<p><strong>跟 Cloud DLP for Healthcare 整合</strong>：FHIR / DICOM 格式的 PHI 有專屬 transformation pipeline。FHIR resource 的特定欄位（patient name / MRN / birth date）按 HIPAA Safe Harbor 自動遮罩、DICOM image 的 metadata 跟 burned-in text 都可 redact。Healthcare 場景的 PHI 治理跟一般 PII 不同 — 不能直接 mask 全部、要保留 clinical utility（年齡轉年齡段、ZIP 保留前三碼）。</p>
<p><strong>跟 BigQuery column-level encryption</strong>：BigQuery 原生支援 AEAD encryption function、可用 KMS-managed key 對 column 做 cell-level encryption。DLP 可在 ingestion 階段先 tokenize、BigQuery query 階段配合 column-level security 做 access-time decryption。是「detection（DLP）+ classification（policy tag）+ encryption（AEAD）+ access control（column-level security）」的完整 stack。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>DLP scan 找不到明顯 PII</strong>：infoType 沒 enable / 預定義 infoType 對 organization-specific 格式不認 — 加 custom infoType + hotword、跑 sample scan 驗證 coverage</li>
<li><strong>FP rate 太高 / findings 淹沒</strong>：infoType 太寬 / hotword 沒設 — 加 likelihood threshold（VERY_LIKELY / LIKELY）、custom infoType 加 hotword 限縮 context</li>
<li><strong>Scan cost 暴衝</strong>：每次都 full scan 整個 dataset / 沒分層 — 改 sample scan（每 dataset 1%）+ 高敏感 dataset 才 full scan + streaming scan 守 write path</li>
<li><strong>Inspection Job 跑超久 / timeout</strong>：dataset 過大 / 沒 partition — 切 partition by date、Job concurrency 提高、避免單 Job 跨整個 organization</li>
<li><strong>Transformation 後 analyst 無法工作</strong>：mask / redact 全部、保留不下 utility — 改 FPE / tokenization 保留 join 能力、k-anonymity 保留 statistical utility</li>
<li><strong>Findings table 自己變成 PII 洩漏面</strong>：findings 含 sample value（預設 quotable）、findings table 無獨立 IAM — 設定 <code>includeQuote: false</code>、findings table 走獨立 dataset + 嚴格 IAM</li>
<li><strong>DLP service account 權限太大 / 沒 audit</strong>：service account 能讀全 organization PII、用 long-lived key — 改 Workload Identity Federation + short-lived credential + Cloud Audit Log 監控 DLP API call</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Microsoft 365 / Office docs 為主</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>AWS-only + S3 為 PII 儲存層</td>
          <td>AWS Macie</td>
      </tr>
      <tr>
          <td>只要 access control 不要 transformation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a></td>
      </tr>
      <tr>
          <td>Secret / credential scanning（非 PII）</td>
          <td>GitGuardian / Gitleaks</td>
      </tr>
      <tr>
          <td>Data lineage / catalog</td>
          <td>Dataplex / Atlan / Collibra</td>
      </tr>
      <tr>
          <td>KMS / key management for FPE</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a></td>
      </tr>
      <tr>
          <td>SIEM ingestion of DLP findings</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Chronicle</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>預定義 infoType 完整 list 跟各自 detection 邏輯（150+ 種、見官方 <a href="https://cloud.google.com/dlp/docs/infotypes-reference">InfoType reference</a>）</li>
<li>Cloud DLP for Healthcare 的 FHIR / DICOM 完整 pipeline 細節</li>
<li>BigQuery column-level security / policy tag 的 policy 設計（屬 Data Governance 章節）</li>
<li>GDPR / HIPAA / PCI 合規逐條對應（屬 <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.8 資料駐留與刪除證據鏈</a> 跟 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a> 章節）</li>
<li>Differential privacy 的數學定義跟 epsilon budget 設計</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Google DLP 在 07 案例庫沒有直接 vendor-level 事件、但所有資料外洩 / 敏感資料治理 case 都是 DLP 控制覆蓋率的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Google DLP 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>資料平台 export 流程應該有 DLP scan gate — query result 含批量 PII / 整 table dump 直接 alert 或自動 redact、不是事後審 audit log</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>客服工具的客戶資料 export 應走 DLP Inspect API、單次 export 超過 N 筆 PII 或含 credential 直接擋住 + 觸發 alert、不靠 rate limit 一招</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a></td>
          <td>Backup bucket 應該獨立 DLP scan、含 credential / token 的 backup 自動 quarantine 到獨立 bucket + 不同 IAM、不是跟 dev backup 同 bucket 同 IAM</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking Governance (section)</a></td>
          <td>Google DLP 是 transformation 工具的代表、章節原則對應 mask / FPE / tokenization / k-anonymity 的選用判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">Data Residency Deletion and Evidence Chain (section)</a></td>
          <td>DLP findings 是 deletion 證據鏈的一部分 — 哪些 PII 在哪些 dataset、deletion 後是否 re-scan verified、findings history 是 GDPR right-to-erasure 的稽核證據</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>、<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11 資料駐留、刪除與證據鏈</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a></li>
<li>上下游 IAM：<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（DLP service account 治理）、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>（FPE / tokenization key）</li>
<li>SIEM 路由：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（DLP findings 進 SIEM correlation）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（DLP alert → IR handoff）</li>
<li>官方：<a href="https://cloud.google.com/sensitive-data-protection/docs">Google Cloud Sensitive Data Protection Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Snyk</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/</guid><description>&lt;p>Snyk 是 &lt;em>developer-first&lt;/em> 的 &lt;em>跨 SCM 多模組 application security platform&lt;/em>、把 SCA、SAST、Container scan、IaC scan、CSPM 整合到一個 dashboard、五大模組共用同一套 Project / Issue / Fix 模型。流量打到 GitHub / GitLab / Bitbucket / Azure Repos 任一 SCM、Snyk 拉取 repo、按 manifest 建 Project、發現 Issue 後送 PR 修補。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security&lt;/a> 比、Snyk &lt;em>跨 SCM&lt;/em> 跟 &lt;em>跨技術棧&lt;/em>；跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 比、Snyk 是商業 SaaS、覆蓋面更廣、但年費按 Project 計價。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Snyk 的核心定位是 &lt;em>用一個工具一個 dashboard 同時管 SCA + SAST + IaC + Container + Cloud&lt;/em>。五大模組 — &lt;em>Snyk Open Source&lt;/em>（SCA、依賴漏洞）、&lt;em>Snyk Code&lt;/em>（SAST）、&lt;em>Snyk Container&lt;/em>（image scan）、&lt;em>Snyk IaC&lt;/em>（Terraform / CloudFormation / K8s manifest 安全）、&lt;em>Snyk Cloud&lt;/em>（CSPM、雲端配置 drift）— 共用 Project / Target / Organization / Issue 模型、Issue 跨模組可一起 prioritize。對 &lt;em>多 SCM + 多技術棧&lt;/em> 的組織、Snyk 比拼裝 GHAS + Trivy + Dependabot 更整合。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security&lt;/a> 的核心差異是 &lt;em>部署模型跟 SCM 範圍&lt;/em>：GHAS 綁 GitHub、走 GitHub Actions、PR 整合更深（Code Scanning alert 直接顯示在 PR review）；Snyk 走 SaaS、SCM 中立、但需要 OAuth 連到每個 repo。組織用 GitLab / Bitbucket / Azure Repos 或同時用多種 SCM、Snyk 是天然選擇。&lt;/p></description><content:encoded><![CDATA[<p>Snyk 是 <em>developer-first</em> 的 <em>跨 SCM 多模組 application security platform</em>、把 SCA、SAST、Container scan、IaC scan、CSPM 整合到一個 dashboard、五大模組共用同一套 Project / Issue / Fix 模型。流量打到 GitHub / GitLab / Bitbucket / Azure Repos 任一 SCM、Snyk 拉取 repo、按 manifest 建 Project、發現 Issue 後送 PR 修補。跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> 比、Snyk <em>跨 SCM</em> 跟 <em>跨技術棧</em>；跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 比、Snyk 是商業 SaaS、覆蓋面更廣、但年費按 Project 計價。</p>
<h2 id="服務定位">服務定位</h2>
<p>Snyk 的核心定位是 <em>用一個工具一個 dashboard 同時管 SCA + SAST + IaC + Container + Cloud</em>。五大模組 — <em>Snyk Open Source</em>（SCA、依賴漏洞）、<em>Snyk Code</em>（SAST）、<em>Snyk Container</em>（image scan）、<em>Snyk IaC</em>（Terraform / CloudFormation / K8s manifest 安全）、<em>Snyk Cloud</em>（CSPM、雲端配置 drift）— 共用 Project / Target / Organization / Issue 模型、Issue 跨模組可一起 prioritize。對 <em>多 SCM + 多技術棧</em> 的組織、Snyk 比拼裝 GHAS + Trivy + Dependabot 更整合。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> 的核心差異是 <em>部署模型跟 SCM 範圍</em>：GHAS 綁 GitHub、走 GitHub Actions、PR 整合更深（Code Scanning alert 直接顯示在 PR review）；Snyk 走 SaaS、SCM 中立、但需要 OAuth 連到每個 repo。組織用 GitLab / Bitbucket / Azure Repos 或同時用多種 SCM、Snyk 是天然選擇。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 比、Trivy 是 OSS、主 container + IaC、適合 CI 內 self-hosted；Snyk 商業 SaaS、覆蓋更廣（含 SAST 跟 Reachability）、適合 <em>組織級 governance + 跨團隊統一 dashboard</em>。Trivy 是 <em>跑工具</em>、Snyk 是 <em>買治理</em>。</p>
<p>關鍵張力：Snyk 的 <em>Project 是計費單位</em>。每個 manifest 算一個 Project（一個 repo 有 package.json + requirements.txt + Dockerfile = 3 Project）。大 monorepo 容易暴量、需要 <em>project filter / archive</em> 治理、否則年費失控。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Snyk 五大模組在 application security stack 承擔哪一段、哪些靠其他工具</li>
<li>Project 計費模型、monorepo 跟 multi-manifest repo 的 Project 暴量風險跟治理路徑</li>
<li>Reachability analysis 的價值跟限制、何時減 noise、何時被誤判</li>
<li>何時用 Snyk、何時走 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Snyk 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 enable Snyk</strong>：Organization 的 admin / collaborator role 配置、Service Account token scope（不要用 personal API token 跑 CI、用 Service Account + scoped token）、Audit Log 是否同步到 SIEM</li>
<li><strong>Project import 治理</strong>：每個 SCM target 自動 import 哪些 manifest、是否有 <em>project filter</em> 排除 test fixture / vendored dependency、archived project 是否真的不計費、monorepo 是否走 <em>.snyk policy file</em> 控制</li>
<li><strong>Reachability analysis 是否啟用</strong>：Snyk Code + Open Source 整合、call graph 分析「我的 code 真的呼叫到 vulnerable 函式嗎」— 大幅減少 <em>transitive dep 但 unreachable</em> 的 noise、production 應該啟用</li>
<li><strong>SBOM export 是否走 release pipeline</strong>：CycloneDX / SPDX 格式是否定期匯出、是否進 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain integrity</a> 流程、合規要求（EO 14028 / NIS2）是否覆蓋</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 supply chain 治理邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Project / Target / Organization 模型</strong>：<em>Organization</em> 是計費跟 RBAC 邊界、對應一個團隊或一個 BU。<em>Target</em> 是一個 SCM 來源（一個 GitHub repo / 一個 container registry image / 一個 Terraform stack）。<em>Project</em> 是 Target 內的單一掃描單位（一個 manifest 或一個 image tag）。Issue 是發現的漏洞 / license / misconfig、有 severity（Critical / High / Medium / Low）、CVSS、exploit maturity、fix availability。Project 暴量的根因通常是 monorepo 內 nested manifest 全被 auto-import、用 <code>.snyk</code> 或 import filter 排除。</p>
<p><strong>五大模組分工</strong>：<em>Snyk Open Source</em>（SCA）掃 package manifest（npm、pip、Maven、Go modules、Composer、NuGet 等 20+ 生態）對 Snyk Vulnerability DB（自家維護、補強 NVD 延遲）。<em>Snyk Code</em>（SAST）掃源碼、symbolic execution + ML、覆蓋 OWASP Top 10 跟 CWE。<em>Snyk Container</em> 掃 image base layer + installed package、支援 Docker / OCI / ECR / GCR / Harbor。<em>Snyk IaC</em> 掃 Terraform / CloudFormation / K8s YAML / Helm chart 對 CIS Benchmark + custom policy。<em>Snyk Cloud</em>（2023 收購 Fugue 後加入）是 CSPM、scan AWS / Azure / GCP runtime 配置 + IaC drift detection（cloud 實際狀態 vs Terraform 狀態的差異）。</p>
<p><strong>Snyk Code (SAST) vs GHAS CodeQL</strong>：Snyk Code 走 <em>快速 inline scan</em>（秒級回饋、走 cloud inference）、適合 dev loop；CodeQL 走 <em>深度 dataflow query</em>（分鐘級、執行更慢但表達力更強）、適合 release gate。同時用兩者並不矛盾 — Snyk Code 在 IDE / PR 給快速訊號、CodeQL 在 release 前跑深度檢查。</p>
<p><strong>Reachability analysis</strong>：跟 <em>純 dependency list 比對 CVE</em> 不同、Snyk 結合 Snyk Code (SAST) 跟 Snyk Open Source (SCA)、做 <em>call graph 分析</em>、判斷「我的 code 是否真的呼叫到 vulnerable 函式」。實務影響：多數 transitive dependency 的 CVE 在你的 app 內 <em>不 reachable</em>（你引入的 lib 沒呼叫到那條 path）— Reachability 過濾後、可以從 <em>幾百個 Critical / High</em> 降到 <em>幾個真的 exploitable</em>。限制：只支援部分語言（Java / JS / Python / Go 較完整）、且 dynamic dispatch / reflection / runtime plugin load 會被當成 reachable（false positive）或 unreachable（false negative）— 不可全信、是 <em>prioritization signal</em> 不是 <em>binary verdict</em>。</p>
<p><strong>Fix advice / Auto PR</strong>：發現 vuln 後、Snyk 自動發 PR 升級到 <em>最小 fix version</em>（包含 transitive dep 的 root cause upgrade）。跟 <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a> 功能重疊、差異是 Snyk 跨 SCM（不只 GitHub）、且 fix advice 含 Reachability 標註（reachable vuln 的 PR 優先級高）。重複用兩者要關掉其一、否則 PR 量翻倍。</p>
<p><strong>跟 CI 整合</strong>：<code>snyk</code> CLI（<code>snyk test</code> / <code>snyk monitor</code> / <code>snyk container test</code> / <code>snyk iac test</code>）走 SNYK_TOKEN 環境變數、可在任何 CI 跑。官方 Snyk Action（GitHub Actions）跟 Jenkins / GitLab CI / CircleCI plugin 是 wrapper。release gate 推薦在 build 後跑 <code>snyk test --severity-threshold=high --fail-on=upgradable</code>、只擋 <em>可升級</em> 的 high+ vuln（無 fix 的 vuln 阻塞 release 沒意義、走 <em>.snyk policy</em> 暫時 ignore + alert）。</p>
<p><strong>SBOM export</strong>：<code>snyk sbom --format=cyclonedx1.4+json</code> / <code>--format=spdx2.3+json</code> 產 SBOM、支援 Snyk attestation（signed SBOM）。近年 supply chain compliance（US EO 14028、EU NIS2 / CRA）要求 SBOM、Snyk 是自動產線之一。SBOM 應該在 <em>release artifact 旁</em> 一起發布、走 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain integrity</a> 流程。</p>
<p><strong>License compliance</strong>：除了漏洞、Snyk 也掃 dependency license（GPL / AGPL / LGPL / proprietary / unknown）、可設 <em>license policy</em>（allow / disallow / require-review）、PR 引入違規 license 直接 fail check。對需要避開 copyleft license 的商業產品、license scan 跟 vulnerability scan 一樣關鍵。</p>
<p><strong>API token 治理</strong>：CI / 第三方 integration 用 <em>Service Account + scoped token</em>（限 Organization、限 permission）、不要用個人 personal token（離職就失效）。Token 進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>、定期 rotate。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Snyk</th>
          <th>GitHub Advanced Security</th>
          <th>Trivy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>商業 SaaS</td>
          <td>GitHub 整合 SaaS</td>
          <td>OSS、self-hosted CLI</td>
      </tr>
      <tr>
          <td>SCM 範圍</td>
          <td>跨 SCM（GitHub / GitLab / Bitbucket / Azure Repos）</td>
          <td>GitHub only</td>
          <td>SCM 無關（CI / local 跑）</td>
      </tr>
      <tr>
          <td>SCA</td>
          <td>Snyk Open Source（含 Reachability）</td>
          <td>Dependabot（純 manifest 比對）</td>
          <td>是、限 OS package + language package</td>
      </tr>
      <tr>
          <td>SAST</td>
          <td>Snyk Code（fast inline）</td>
          <td>CodeQL（dataflow query）</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Container scan</td>
          <td>Snyk Container</td>
          <td>透過 Dependabot + 第三方</td>
          <td>Trivy Container（主打）</td>
      </tr>
      <tr>
          <td>IaC scan</td>
          <td>Snyk IaC</td>
          <td>透過 Code Scanning + KICS / Checkov</td>
          <td>Trivy Config（主打）</td>
      </tr>
      <tr>
          <td>CSPM</td>
          <td>Snyk Cloud</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Reachability</td>
          <td>有（限部分語言）</td>
          <td>部分 CodeQL query 有</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Auto-fix PR</td>
          <td>Snyk PR + fix advice</td>
          <td>Dependabot PR</td>
          <td>無</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>按 Project（manifest）數</td>
          <td>GitHub seat-based</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — UI 友善、CLI 直觀</td>
          <td>低 — 跟 GitHub 一體</td>
          <td>低 — 單一 binary、CLI 為主</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>多 SCM + 多 stack + 想統一 dashboard</td>
          <td>純 GitHub + 想跟 PR 深整合</td>
          <td>純 container / IaC + 想 OSS + 預算敏感</td>
      </tr>
  </tbody>
</table>
<p>選 Snyk 的核心訴求：<em>組織用多個 SCM 或多技術棧（後端 + 前端 + container + Terraform + cloud）</em> + 需要 <em>統一 dashboard + 跨團隊 prioritization</em> + 接受按 Project 計費的成本。純 GitHub 組織用 GHAS 更整合、純 container CI 用 Trivy 免費、極大型 monorepo 用 Snyk 容易爆 Project 數要小心。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Snyk Cloud (CSPM) 跟 IaC drift detection</strong>：Snyk Cloud 連 AWS / Azure / GCP read-only role、掃 runtime 配置（S3 bucket public、IAM over-permission、security group 0.0.0.0/0）對 CIS Benchmark + custom policy。跟 <em>Snyk IaC</em> 結合做 <em>drift detection</em> — Terraform 內定義是 private bucket、但 cloud 實際是 public（有人 console 手改）、Snyk 報 drift。對標 <a href="https://www.wiz.io/">Wiz</a> / Prisma Cloud / Lacework、Snyk Cloud 是 <em>跟 Snyk IaC 同源治理</em> 的優勢（同個 dashboard 看 IaC + runtime）。</p>
<p><strong>Custom Rule（Snyk IaC custom policy）</strong>：Snyk IaC 預設規則庫覆蓋 CIS Benchmark + AWS / GCP / Azure 最佳實踐、可寫 <em>custom policy</em>（Rego-like / SnykIQL）擴展。例：禁止 RDS 沒開 encryption-at-rest、禁止 S3 沒 versioning、禁止 K8s pod 跑 hostNetwork。Custom policy 走版控（git）跟 PR review、避免在 console 直接改。</p>
<p><strong>Reachability vs 純 static SCA</strong>：純 SCA（如 Dependabot / Trivy）只看 <em>manifest 中聲明的版本是否有 CVE</em>、不分 reachable / unreachable。結果是 Critical / High alert 大量、開發者 <em>alert fatigue</em> 後直接 ignore。Snyk Reachability 用 SAST + SCA 整合做 call graph、過濾掉 <em>vulnerable lib 載入了但 vulnerable 函式從未被呼叫</em> 的案例。限制：dynamic dispatch / reflection / 動態載入 plugin / native binding 都會讓 reachability 判斷失準、不可當成 binary truth。</p>
<p><strong>Snyk Insights（風險優先級 prioritization）</strong>：除了 CVSS、Snyk 加入 <em>exploit maturity</em>（exploit in-the-wild / PoC / no known exploit）、<em>fix availability</em>（有無 fix version）、<em>social trend</em>（CVE 被討論度）、<em>Reachability</em> 綜合算 <em>Priority Score</em>。production 用 Priority Score 排 backlog、而非單純 CVSS — 一個 <em>Critical 但 unreachable + no fix</em> 的 vuln 不該擋 release。</p>
<p><strong>SBOM 流程整合</strong>：把 <code>snyk sbom</code> 接到 CI release step、SBOM artifact 跟 release binary 一起進 registry / object store、走 <a href="https://in-toto.io/">in-toto attestation</a> 或 <a href="https://slsa.dev/">SLSA</a> provenance 流程、合規時可回溯。跟 <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a> 流程的差異：Syft + Grype 是 OSS local-first + Unix philosophy、Snyk 是 SaaS、SBOM 含 Snyk Issue ID 跟 fix advice link。</p>
<p><strong>License policy enforcement</strong>：除了 vulnerability、license 違規（GPL / AGPL 引入到 proprietary product、unknown license dep）走同套 policy / PR fail-check 機制、production 應該把 license policy 跟 vulnerability policy 並列當 release gate。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Project 暴量計費</strong>：monorepo 自動 import 把 test fixture / node_modules-vendored 全當 Project — 用 <em>.snyk</em> 跟 import filter 排除、archived project 確認真的不計費</li>
<li><strong>Reachability 漏判 / 誤判</strong>：dynamic dispatch / reflection / plugin load 讓 call graph 失準、Critical vuln 被標 unreachable 但實際 reachable — 對 framework-heavy code（Spring / Django middleware / Rails initializer）保守處理、不全信 Reachability</li>
<li><strong>PR noise</strong>：Snyk + Dependabot 同時開、依賴升級 PR 翻倍 — 二選一、或讓 Snyk 處理 vuln-driven upgrade、Dependabot 處理 routine version bump</li>
<li><strong>CI fail-on 設不對</strong>：<code>--severity-threshold=low</code> 把 release 整個擋死 / <code>--severity-threshold=critical</code> 漏 high — production 通常 <code>--severity-threshold=high --fail-on=upgradable</code>、再用 <code>.snyk</code> policy file 例外管理</li>
<li><strong>License check 誤殺</strong>：transitive dep 引入 LGPL 被當 GPL 阻擋 — 細分 license policy（allow LGPL-with-dynamic-linking、disallow GPL）、走 review workflow 而非 fail-fast</li>
<li><strong>API token over-scoped</strong>：CI 拿到 admin-level Service Account token、整 org Project 都能改 — 改 scoped token、限 Organization + 限 permission、進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></li>
<li><strong>SBOM 沒進 release pipeline</strong>：SBOM 只在 Snyk dashboard、release artifact 沒附 — 把 <code>snyk sbom</code> 加進 CI release step、SBOM 跟 binary 一起發</li>
<li><strong>Snyk Cloud drift 沒人看</strong>：CSPM alert 進 dashboard 但沒 routing 到 on-call — 接 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">SIEM</a> / Slack / PagerDuty、高 severity drift 觸發 ticket</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 GitHub + 想跟 PR / Action 深整合</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a></td>
      </tr>
      <tr>
          <td>純 container / IaC + OSS + 預算敏感</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></td>
      </tr>
      <tr>
          <td>純 dependency 升級（routine version bump）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a></td>
      </tr>
      <tr>
          <td>Secret scanning（leaked API key in repo）</td>
          <td>GitGuardian / Gitleaks（Snyk 不主打）</td>
      </tr>
      <tr>
          <td>Runtime container threat detection</td>
          <td>Falco / Cilium Tetragon</td>
      </tr>
      <tr>
          <td>深度 SAST（dataflow query / taint analysis）</td>
          <td>CodeQL / Semgrep（Snyk Code 偏 fast inline、深度查走 CodeQL）</td>
      </tr>
      <tr>
          <td>CSPM 跨 multi-cloud + asset inventory</td>
          <td>Wiz / Prisma Cloud / Lacework（Snyk Cloud 較新、功能仍在追）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Snyk 完整 pricing tier（Team / Business / Enterprise）跟 Project 計費細節</li>
<li>Snyk Vulnerability DB 跟 NVD / GHSA 的覆蓋差異對照</li>
<li>Snyk Code SAST 規則完整 reference</li>
<li>Snyk IaC 內建 policy 完整列表 + CIS Benchmark 對照</li>
<li>Snyk Cloud 多雲 onboarding 步驟（AWS / Azure / GCP read-only role 設置）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Snyk 在 07 案例庫沒有直接 vendor-level 事件、但多個 supply chain 案例展示 Snyk 工具能力的 <em>範圍跟邊界</em>：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Snyk 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — Reachability analysis 能快速回答「我的 service 是否真用到 vulnerable JndiLookup」、減少 emergency triage 的 noise</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024 Open Source Supply Chain</a></td>
          <td>對照啟示 — Snyk 看 package version + CVE、看不到 maintainer takeover；需補 release-tarball 比對 + maintainer trust signal</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>對照啟示 — Snyk Container 看 image 內 package CVE、看不到 update channel 被植入；需配合 artifact provenance / SLSA</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></td>
          <td>章節對應 — Snyk SBOM + License policy 是 supply chain governance 的工具、合規門檻（EO 14028 / NIS2）的標準產線之一</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>、<a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>（vuln 阻擋不完全時、資料層也要遮罩）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>（Snyk API token 存放）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Critical CVE 揭露時的 emergency triage routing）</li>
<li>官方：<a href="https://docs.snyk.io/">Snyk Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Vegeta</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vegeta/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vegeta/</guid><description>&lt;p>Vegeta 的核心責任是用簡潔 CLI 對 HTTP endpoint 產生固定 rate 負載，快速探測 latency、throughput、error rate 與 saturation。它適合單一 endpoint、少量 header / body 變化、快速 baseline、incident 後驗證與工程師本機或 CI 中的輕量壓測。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Vegeta 是 Go 寫的 HTTP load testing CLI，核心模型是 &lt;em>constant rate attack&lt;/em>：指定「每秒 N 個 request」就持續打 N rps、不會因 server 變慢就降速，跟「fire-and-wait」型工具（hey / wrk 預設 closed-loop）行為差異很大。constant rate 是 &lt;em>open-loop&lt;/em> 模型 — 模擬真實流量「不會因服務慢而減少」的行為、所以 saturation 點才會明確浮現。&lt;/p>
&lt;p>Vegeta 是 Unix philosophy CLI：targets 從 stdin 讀（可以 pipe 進複雜 generator）、binary report 從 stdout 出（可以 pipe 進 &lt;code>vegeta report&lt;/code> / &lt;code>vegeta plot&lt;/code> / &lt;code>vegeta encode&lt;/code>）。這個設計讓 Vegeta 容易跟 shell pipeline / CI script 接合、但同時也決定它不適合表達多步驟 session。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a> 比、Vegeta 走 &lt;em>CLI-first + open-loop constant rate&lt;/em>、k6 走 &lt;em>JS scenario + threshold + CI artifact&lt;/em>。Vegeta 適合「我要對這個 URL 打 200 rps 60 秒」的一次性壓測、k6 適合「我有 3 種 user journey、各占 40/30/30%、跑 ramp-up profile」的可維護 scenario。跟 hey 比、Vegeta 的 constant rate 是真的 open-loop、hey 的 &lt;code>-q&lt;/code> 是 per-worker rate（worker 變慢整體就降速）— 探測 saturation 時 Vegeta 比較誠實。跟 wrk / wrk2 比、Vegeta 沒有 LuaJIT 那麼極致的單機壓測效能、但 binary report + &lt;code>vegeta plot&lt;/code> + targets pipe 對日常工程師工作流更友善。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>何時用 Vegeta、何時走 k6 / hey / wrk / Gatling / Locust 的取捨&lt;/li>
&lt;li>constant rate attack 的設計意涵（open-loop vs closed-loop、為什麼這對 saturation discovery 重要）&lt;/li>
&lt;li>target file / rate / duration / report 四件套的 baseline workflow 跟 evidence package 對應&lt;/li>
&lt;li>排錯時的常見陷阱：runner 端 TCP socket exhaust、open file limit、constant rate 跟 target server 限速 disconnect&lt;/li>
&lt;/ol>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Vegeta 適合快速回答「這個 endpoint 在某個 rate 下表現如何」。當團隊需要先找出大概 knee point、驗證一個修補是否降低 latency、或在 CI 裡跑小型 performance smoke test，Vegeta 的 CLI workflow 很直接。&lt;/p></description><content:encoded><![CDATA[<p>Vegeta 的核心責任是用簡潔 CLI 對 HTTP endpoint 產生固定 rate 負載，快速探測 latency、throughput、error rate 與 saturation。它適合單一 endpoint、少量 header / body 變化、快速 baseline、incident 後驗證與工程師本機或 CI 中的輕量壓測。</p>
<h2 id="服務定位">服務定位</h2>
<p>Vegeta 是 Go 寫的 HTTP load testing CLI，核心模型是 <em>constant rate attack</em>：指定「每秒 N 個 request」就持續打 N rps、不會因 server 變慢就降速，跟「fire-and-wait」型工具（hey / wrk 預設 closed-loop）行為差異很大。constant rate 是 <em>open-loop</em> 模型 — 模擬真實流量「不會因服務慢而減少」的行為、所以 saturation 點才會明確浮現。</p>
<p>Vegeta 是 Unix philosophy CLI：targets 從 stdin 讀（可以 pipe 進複雜 generator）、binary report 從 stdout 出（可以 pipe 進 <code>vegeta report</code> / <code>vegeta plot</code> / <code>vegeta encode</code>）。這個設計讓 Vegeta 容易跟 shell pipeline / CI script 接合、但同時也決定它不適合表達多步驟 session。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> 比、Vegeta 走 <em>CLI-first + open-loop constant rate</em>、k6 走 <em>JS scenario + threshold + CI artifact</em>。Vegeta 適合「我要對這個 URL 打 200 rps 60 秒」的一次性壓測、k6 適合「我有 3 種 user journey、各占 40/30/30%、跑 ramp-up profile」的可維護 scenario。跟 hey 比、Vegeta 的 constant rate 是真的 open-loop、hey 的 <code>-q</code> 是 per-worker rate（worker 變慢整體就降速）— 探測 saturation 時 Vegeta 比較誠實。跟 wrk / wrk2 比、Vegeta 沒有 LuaJIT 那麼極致的單機壓測效能、但 binary report + <code>vegeta plot</code> + targets pipe 對日常工程師工作流更友善。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>何時用 Vegeta、何時走 k6 / hey / wrk / Gatling / Locust 的取捨</li>
<li>constant rate attack 的設計意涵（open-loop vs closed-loop、為什麼這對 saturation discovery 重要）</li>
<li>target file / rate / duration / report 四件套的 baseline workflow 跟 evidence package 對應</li>
<li>排錯時的常見陷阱：runner 端 TCP socket exhaust、open file limit、constant rate 跟 target server 限速 disconnect</li>
</ol>
<h2 id="定位">定位</h2>
<p>Vegeta 適合快速回答「這個 endpoint 在某個 rate 下表現如何」。當團隊需要先找出大概 knee point、驗證一個修補是否降低 latency、或在 CI 裡跑小型 performance smoke test，Vegeta 的 CLI workflow 很直接。</p>
<p>這個定位讓 Vegeta 接到 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。它提供的是快速壓力探針，後續若要表達複雜 workload model，通常要轉向 k6、Gatling、Locust 或 JMeter。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一次 Vegeta 壓測是否有效、最少看四件事：</p>
<ul>
<li><strong>Target 描述完整性</strong>：targets file 是否包含 method / URL / headers / body、是否反映真實 request shape（含 auth header、content-type、representative payload size），缺一就會讓壓測結果偏離正式環境</li>
<li><strong>Rate model 設計</strong>：選的是 constant rate（<code>-rate=200/s</code>）還是 ramp（用多段 attack pipe），constant rate 適合 saturation probe、ramp-up 要 wrap script 自己 stage、Vegeta 沒有原生 ramp profile</li>
<li><strong>Report 解讀</strong>：<code>vegeta report</code> 給 mean / p50 / p95 / p99 / max latency + success rate + throughput，重點看 <em>p99 跟 max 的距離</em> 與 <em>requested rate vs actual throughput</em> 是否 disconnect — disconnect 表示 server / runner 端有人在限速</li>
<li><strong>Duration vs warm-up</strong>：短 duration（&lt; 30s）容易吃到 JIT / cache / connection pool warm-up 噪音，baseline 壓測 duration 至少 60s、且第一段 result 要 discard，否則 p99 會被前 5s 拉高</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p>單 endpoint saturation probe 是 Vegeta 的主要入口。工程師可以對 login、search、read API、feature flag endpoint 或 internal health-like endpoint 施加固定 rate，觀察 p95 / p99 與 error rate 何時開始上升。</p>
<p>Regression smoke test 適合用 Vegeta。CI 或 pre-release 可以用短時間固定 rate 測試，確認 hot path 沒有明顯退化，再把更完整的 scenario 交給 k6、Gatling 或 Locust。</p>
<p>Incident 後修補驗證適合用 Vegeta。當事故根因是某個 endpoint 的 query、cache miss、lock contention 或 timeout，修補後可以用相同 request set 重跑，快速比較 latency distribution。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Vegeta 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CLI 簡潔</td>
          <td>本機、CI、shell workflow 容易接</td>
          <td>長期報表與 artifact 標準化</td>
      </tr>
      <tr>
          <td>固定 rate</td>
          <td>探測 rate / latency 關係清楚</td>
          <td>複雜使用者行為與 arrival pattern</td>
      </tr>
      <tr>
          <td>HTTP 導向</td>
          <td>API hot path 快速驗證</td>
          <td>非 HTTP protocol 與 multi-step flow</td>
      </tr>
      <tr>
          <td>快速 probe</td>
          <td>適合 smoke test 與修補驗證</td>
          <td>完整 workload model 與資料治理</td>
      </tr>
  </tbody>
</table>
<p>CLI 簡潔價值來自低摩擦。當問題還在定位階段，工程師可以很快產生可重跑 command 與 target file，先取得 baseline，再決定是否需要完整壓測平台。</p>
<p>固定 rate 價值來自可比較。用相同 request set、rate、duration 與 target environment 重跑，可以讓修補前後的 latency distribution 有清楚對照。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Vegeta 和 k6 的主要差異是 scenario 深度。Vegeta 適合固定 rate HTTP probe；k6 適合多步驟 scenario、threshold、CI artifact 與 browser-style flow。</p>
<p>Vegeta 和 JMeter 的主要差異是工具重量。Vegeta 適合快速 CLI；JMeter 適合 GUI、多 protocol、plugin 與企業測試資產。</p>
<p>Vegeta 和 Gatling 的主要差異是長期維護模式。Vegeta 用 command / target file 保持簡單；Gatling 用 simulation 維護複雜 flow 與 injection profile。</p>
<p>Vegeta 和 Locust 的主要差異是自訂能力。Locust 適合 Python user behavior 與 custom client；Vegeta 適合 HTTP endpoint 的直接壓力測量。</p>
<h2 id="操作成本">操作成本</h2>
<p>Vegeta 的主要成本是 workload coverage 有限。它能快速測 endpoint，但多步驟 session、資料依賴、payment mock、queue side effect 與 realistic user journey 需要額外工具或腳本補上。</p>
<p>Artifact 成本來自命令可追溯性。每次測試要保存 rate、duration、targets、headers、body、環境、版本與結果檔；否則快速 probe 很容易變成不可比較的一次性觀察。</p>
<p>Runner 成本通常較低，但仍要檢查本機瓶頸。高 rate 測試時，產生負載的機器也可能先被 CPU、network、file descriptor 或 connection limit 卡住。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Vegeta 結果應回寫到 evidence package。最小欄位包括 command、target file hash、rate、duration、workers、target environment、p95 / p99、max latency、error rate、throughput、target saturation metric、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Vegeta 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>command、targets file、binary result、report</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>test start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / metrics / logs 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>target set freshness、header / body correctness</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>runner capacity、endpoint representativeness</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋多步驟 flow、資料偏差、runner limit</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓快速測試可以比較。Vegeta 的結果通常很短，反而更需要保存 command 與 target set，讓下一次修補驗證能跑同一組條件。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Vegeta</th>
          <th>k6</th>
          <th>hey</th>
          <th>wrk / wrk2</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>負載模型</td>
          <td>Open-loop constant rate（rps 不隨 latency 降）</td>
          <td>Open-loop（k6 default）/ closed-loop（VU mode）</td>
          <td>Per-worker rate（closed-loop 傾向）</td>
          <td>wrk closed-loop / wrk2 open-loop</td>
      </tr>
      <tr>
          <td>Scenario 深度</td>
          <td>單 endpoint pipe target、多 endpoint 需 script</td>
          <td>JS script、多步驟、staging / threshold / SLO 內建</td>
          <td>單一 URL CLI flag</td>
          <td>Lua script 可寫複雜邏輯但 idiom 較陡</td>
      </tr>
      <tr>
          <td>輸出形式</td>
          <td>Binary stream + <code>vegeta report/plot/encode</code></td>
          <td>stdout summary + JSON + 內建 dashboard</td>
          <td>stdout 文字 summary</td>
          <td>stdout 文字 summary、HdrHistogram</td>
      </tr>
      <tr>
          <td>CI 整合</td>
          <td>用 shell 包、自寫 threshold gate</td>
          <td>內建 threshold / exit code、CI artifact 標準化</td>
          <td>簡單 smoke、無 threshold</td>
          <td>需自寫 wrapper</td>
      </tr>
      <tr>
          <td>學習成本</td>
          <td>低 — 幾個 flag 就上手</td>
          <td>中 — 要寫 JS scenario</td>
          <td>極低 — 一行 CLI</td>
          <td>中 — Lua 加 HdrHistogram 概念</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>修補驗證、CI smoke、saturation probe</td>
          <td>完整壓測平台、SLO gate、多 scenario</td>
          <td>一次性 ad-hoc 探測</td>
          <td>極致單機壓測效能、低 overhead 量測</td>
      </tr>
  </tbody>
</table>
<p>選 Vegeta 的核心訴求：<em>工程師本機 / CI smoke / 修補驗證 / saturation probe</em> 都要快速可重跑、且結果要可以保存比較；不需要完整 scenario 模型也不需要 GUI 報表。若團隊需要完整 user journey、threshold / SLO gate、長期 trend dashboard，直接走 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> 或 <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Reporting 多輸出 format</strong>：<code>vegeta report</code> 預設 text summary、加 <code>-type=hist[0,10ms,50ms,100ms,500ms]</code> 給 latency bucket histogram、<code>-type=json</code> 給機器可讀 result、<code>vegeta plot</code> 出 HTML latency chart、<code>vegeta encode -to=csv</code> 轉成可進 spreadsheet / dashboard 的 CSV。binary result 檔可重複 decode 成不同 format，不用重跑壓測。修補驗證的標準作法是保留 <code>results.bin</code>、之後可隨時 re-render report。</p>
<p><strong>Pipe attack workflow</strong>：Vegeta 的 stdin/stdout 都是 stream — 可以用 shell pipe 串接 <code>jq</code> 動態產 targets（<code>jq -r '.urls[] | &quot;GET &quot; + .'</code>）、用 <code>vegeta attack | tee results.bin | vegeta report</code> 同時寫檔跟即時看 summary、用 <code>cat results-old.bin results-new.bin | vegeta report</code> 比較兩次結果。這個設計讓 Vegeta 跟 incident drill / chaos test script 容易接合 — 修補 deploy 完跑一次 attack、result 直接 commit 進 git 當 evidence。</p>
<p><strong>CI integration pattern</strong>：CI 裡 Vegeta 沒有 k6 那種內建 threshold，要自寫 gate — <code>vegeta report -type=json results.bin | jq '.latencies.p99'</code> 出 p99、bash 比較 budget、超標 exit 非零。把 <code>targets.txt</code> + <code>attack.sh</code> + <code>expected-budget.json</code> commit 進 repo、CI artifact 上傳 <code>results.bin</code> + <code>plot.html</code>，下次 regression 時可以 diff。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Requested rate 跟 actual throughput disconnect（要 200rps 實際只跑 80rps）</strong>：runner 端先飽和、不是 server 飽和 — 看 <code>vegeta attack</code> stderr 是否報 <code>socket: too many open files</code>、檢查 <code>ulimit -n</code>（生產壓測 runner 至少設 65535）；或 server 端有限速 / rate limit / connection cap 把 request reject 在 TCP 層、Vegeta 看不到完整 response 就被卡</li>
<li><strong>TCP socket exhaust（runner 端）</strong>：constant rate 模型下、若 server 回應慢、connection 會堆積、<code>TIME_WAIT</code> socket 爆 ephemeral port range — 用 <code>-keepalive=true</code>（預設）並調 <code>net.ipv4.tcp_tw_reuse=1</code>、或加 <code>-connections=N</code> 限制 connection pool 上限避免無限堆 socket</li>
<li><strong>p99 / max latency 異常高、但 server-side metrics 看不到</strong>：runner 端 GC pause / CPU steal / network jitter 把 latency 量測污染 — 把 runner 移到跟 target 同 placement group / same AZ、確認 runner CPU 沒被其他 process 搶、duration 拉長到 5min 讓 outlier 變稀釋</li>
<li><strong>Success rate 100% 但 server 已經爆</strong>：targets 沒帶 auth header / 打到 LB 而非 backend、所有 request 在前面就 200 / cache hit、server 根本沒收到壓力 — 檢查 target server access log 的 request count 跟 Vegeta requested rate 是否對得上</li>
<li><strong>短時間壓測結果不穩定（同 command 跑兩次差很多）</strong>：duration 太短（&lt; 30s）、warm-up 噪音占比太高 — 至少 60s、第一段 5-10s discard、若 endpoint 有 lazy initialization（cache / connection pool / JIT compile）先跑一段 warm-up attack 再正式量</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Vegeta 適合回寫單 endpoint hot path 與修補驗證案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase ultra-low latency</a> 的 sub-millisecond latency distribution 判讀、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a> 的 p99 &lt; 10ms lookup 驗證、<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 connection limit</a> 的 RDB bottleneck 探測、<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 的次毫秒 cache lookup 驗證，以及 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a> 的 hot partition 探測。</p>
<p>這些案例的重點是快速定位與比較。Vegeta 頁引用案例時，要把 case 轉成 endpoint、rate、duration、latency budget、target saturation metric 與 runner limit — 例如 Coinbase 的 sub-ms 目標要求 Vegeta runner 必須跟 target 同 placement group、否則 runner 自身的網路 jitter 會吃掉觀測精度。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>、<a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>、<a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>官方：<a href="https://github.com/tsenart/vegeta">Vegeta documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.5 攻擊者視角（紅隊）：資料層弱點判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/red-team-data-layer/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/red-team-data-layer/</guid><description>&lt;p>資料層紅隊判讀的核心目標是確認「誰能讀到什麼資料、資料會從哪裡流出、錯誤狀態如何回復」。這裡的紅隊指攻擊者視角的風險檢查：從可被濫用的路徑反向檢查資料邊界。database 一旦承擔 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、弱點就同時影響正確性、隱私與可恢復性。&lt;/p>
&lt;p>本章聚焦在 &lt;em>資料層&lt;/em>（DB 自身）的攻擊面、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 資安與資料保護模組&lt;/a> 的網路 / 身份 / 加密層形成互補。讀完後讀者能盤點：DB 上有哪些 &lt;em>攻擊路徑&lt;/em>、哪些 &lt;em>外洩管道&lt;/em>、哪些 &lt;em>偵測訊號&lt;/em>。&lt;/p>
&lt;h2 id="資料層弱點的主要軸線">資料層弱點的主要軸線&lt;/h2>
&lt;p>資料層弱點可分成三條軸線：存取邊界、狀態邊界、資料流邊界。&lt;/p>
&lt;p>&lt;strong>存取邊界&lt;/strong>：看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a>。哪些 user / role / tenant 可以 read / write 哪些資料。
&lt;strong>狀態邊界&lt;/strong>：看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a>。同時讀寫時的 race condition、TOCTOU。
&lt;strong>資料流邊界&lt;/strong>：看查詢結果、匯出、備份、觀測與支援工具的資料暴露路徑。&lt;/p>
&lt;p>三條軸線各有典型攻擊模式、要分別檢查。&lt;/p>
&lt;h2 id="db-攻擊面的外圍層次">DB 攻擊面的外圍層次&lt;/h2>
&lt;p>DB 攻擊面分三層、每層有典型攻擊向量跟防禦邊界、紅隊盤點要逐層檢查。傳統做法常把 90% 精力放在最內層 DB、外圍兩層的失守會讓內層防禦變成無效投資。&lt;/p>
&lt;p>&lt;strong>Layer 1：DB 本身&lt;/strong>（最直接、防禦最成熟）— SQL injection、authentication、authorization、RLS 都在這層。&lt;/p>
&lt;p>&lt;strong>Layer 2：DB 周邊產品&lt;/strong>（最常被忽略）— file transfer service（MFT）、API gateway、search proxy、admin console 都「接 DB」、且通常 perimeter 設定比 DB 鬆。對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a> — MOVEit Transfer 是 file transfer 產品、漏洞讓攻擊者直接存取後端資料、屬於 edge-exposure 類別的批量利用事件。判讀重點：任何「接 DB」的產品都屬於 DB 攻擊面、要盤 &lt;em>所有上游 caller 產品&lt;/em>。類似結構還有 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Layer 3：認證信任根&lt;/strong>（最致命、最少人想到）— signing key、token issuer、IAM &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> 都決定「誰能宣稱是哪個 user」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558&lt;/a> — 簽章金鑰外洩後、攻擊者偽造可被驗證的身分權杖、application 層的 BOLA / BOPLA / RLS 都會在底層 trust 失守時被繞過。判讀重點：DB authorization 接受上游認證結果、上游 trust 失守時、DB 層的精緻設計就被旁路掉。&lt;/p>
&lt;p>&lt;strong>設計含義&lt;/strong>：紅隊盤點順序是由外向內。先盤「誰能通過認證」（trust root）、再盤「通過認證後能打到哪些產品」（caller surface）、最後盤「打到 DB 後能做什麼」（DB authorization）。三層任一失守、後續層的防禦投資都會被旁路。&lt;/p>
&lt;h2 id="攻擊模式-1注入類">攻擊模式 1：注入類&lt;/h2>
&lt;p>&lt;strong>SQL Injection&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>經典攻擊、把 user input 拼進 SQL 字串&lt;/li>
&lt;li>防禦：parameterized query / prepared statement、絕不字串拼接&lt;/li>
&lt;li>二階注入：input 已存進 DB、後續 query 時才觸發 — 比一階更難偵測&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>NoSQL Injection&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>MongoDB / DynamoDB 也可能被注入（不同形式）&lt;/li>
&lt;li>MongoDB：&lt;code>{$where: ...}&lt;/code> operator injection、&lt;code>{$ne: null}&lt;/code> 跳過 auth&lt;/li>
&lt;li>DynamoDB：FilterExpression 注入（少見、需要特定 application 結構）&lt;/li>
&lt;li>防禦：白名單 user input、不直接組 query operator&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>ORM Injection&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>資料層紅隊判讀的核心目標是確認「誰能讀到什麼資料、資料會從哪裡流出、錯誤狀態如何回復」。這裡的紅隊指攻擊者視角的風險檢查：從可被濫用的路徑反向檢查資料邊界。database 一旦承擔 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、弱點就同時影響正確性、隱私與可恢復性。</p>
<p>本章聚焦在 <em>資料層</em>（DB 自身）的攻擊面、跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 資安與資料保護模組</a> 的網路 / 身份 / 加密層形成互補。讀完後讀者能盤點：DB 上有哪些 <em>攻擊路徑</em>、哪些 <em>外洩管道</em>、哪些 <em>偵測訊號</em>。</p>
<h2 id="資料層弱點的主要軸線">資料層弱點的主要軸線</h2>
<p>資料層弱點可分成三條軸線：存取邊界、狀態邊界、資料流邊界。</p>
<p><strong>存取邊界</strong>：看 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a> 與 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a>。哪些 user / role / tenant 可以 read / write 哪些資料。
<strong>狀態邊界</strong>：看 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與 <a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>。同時讀寫時的 race condition、TOCTOU。
<strong>資料流邊界</strong>：看查詢結果、匯出、備份、觀測與支援工具的資料暴露路徑。</p>
<p>三條軸線各有典型攻擊模式、要分別檢查。</p>
<h2 id="db-攻擊面的外圍層次">DB 攻擊面的外圍層次</h2>
<p>DB 攻擊面分三層、每層有典型攻擊向量跟防禦邊界、紅隊盤點要逐層檢查。傳統做法常把 90% 精力放在最內層 DB、外圍兩層的失守會讓內層防禦變成無效投資。</p>
<p><strong>Layer 1：DB 本身</strong>（最直接、防禦最成熟）— SQL injection、authentication、authorization、RLS 都在這層。</p>
<p><strong>Layer 2：DB 周邊產品</strong>（最常被忽略）— file transfer service（MFT）、API gateway、search proxy、admin console 都「接 DB」、且通常 perimeter 設定比 DB 鬆。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a> — MOVEit Transfer 是 file transfer 產品、漏洞讓攻擊者直接存取後端資料、屬於 edge-exposure 類別的批量利用事件。判讀重點：任何「接 DB」的產品都屬於 DB 攻擊面、要盤 <em>所有上游 caller 產品</em>。類似結構還有 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023</a>。</p>
<p><strong>Layer 3：認證信任根</strong>（最致命、最少人想到）— signing key、token issuer、IAM <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> 都決定「誰能宣稱是哪個 user」。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558</a> — 簽章金鑰外洩後、攻擊者偽造可被驗證的身分權杖、application 層的 BOLA / BOPLA / RLS 都會在底層 trust 失守時被繞過。判讀重點：DB authorization 接受上游認證結果、上游 trust 失守時、DB 層的精緻設計就被旁路掉。</p>
<p><strong>設計含義</strong>：紅隊盤點順序是由外向內。先盤「誰能通過認證」（trust root）、再盤「通過認證後能打到哪些產品」（caller surface）、最後盤「打到 DB 後能做什麼」（DB authorization）。三層任一失守、後續層的防禦投資都會被旁路。</p>
<h2 id="攻擊模式-1注入類">攻擊模式 1：注入類</h2>
<p><strong>SQL Injection</strong>：</p>
<ul>
<li>經典攻擊、把 user input 拼進 SQL 字串</li>
<li>防禦：parameterized query / prepared statement、絕不字串拼接</li>
<li>二階注入：input 已存進 DB、後續 query 時才觸發 — 比一階更難偵測</li>
</ul>
<p><strong>NoSQL Injection</strong>：</p>
<ul>
<li>MongoDB / DynamoDB 也可能被注入（不同形式）</li>
<li>MongoDB：<code>{$where: ...}</code> operator injection、<code>{$ne: null}</code> 跳過 auth</li>
<li>DynamoDB：FilterExpression 注入（少見、需要特定 application 結構）</li>
<li>防禦：白名單 user input、不直接組 query operator</li>
</ul>
<p><strong>ORM Injection</strong>：</p>
<ul>
<li>即使用 ORM、<code>Raw()</code> / <code>Exec()</code> 等 escape hatch 仍能注入</li>
<li>用 <code>where</code> clause 接 user input 不過濾、ORM 不會自動防</li>
<li>防禦：永遠 parameterized、<code>Raw()</code> 必須 review</li>
</ul>
<p><strong>Second-order Injection</strong>：</p>
<ul>
<li>第一次寫入時看起來安全、第二次讀出來時觸發</li>
<li>例：username 帶 SQL fragment、寫入時 escape、後續 admin 查詢時不 escape</li>
<li>防禦：<em>所有</em> DB output 都當 untrusted、不能依賴「寫入時的 escape」</li>
</ul>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023 mass exfiltration</a> 是 SQL injection 升級成 mass data exfil 的代表性事件。Progress Software 的 MOVEit Transfer 是 file transfer 產品、漏洞讓未認證攻擊者直接打到後端 DB、跨上百家客戶持續外洩。判讀重點：file transfer 這類「次要產品」也接 DB、且因為通常 perimeter 設定鬆、變成最先被打的點。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">Attack Surface 卡片</a> 跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 entrypoint security</a>。</p>
<h2 id="攻擊模式-2授權繞過類">攻擊模式 2：授權繞過類</h2>
<p><strong>BOLA</strong>（Broken Object Level Authorization）：</p>
<ul>
<li>用戶 A 改 user_id 為 B 的請求、後端不檢查就回 B 的資料</li>
<li>最常見的 web app 漏洞（OWASP API Top 10 第 1 名）</li>
<li>防禦：每個 DB query 都帶 <code>WHERE owner_id = current_user_id</code>、不只信 URL parameter</li>
<li>對應 <a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR 卡片</a></li>
</ul>
<p><strong>BOPLA</strong>（Broken Object Property Level Authorization）：</p>
<ul>
<li>物件級檢查過了、但物件內 <em>某些屬性</em> 不該被存取 / 修改</li>
<li>例：用戶能更新自己 profile、但不該改 <code>is_admin</code> flag</li>
<li>防禦：應用層 <em>allowlist</em> 屬性、不是 deny-list</li>
<li>對應 <a href="/blog/backend/knowledge-cards/bopla/" data-link-title="BOPLA" data-link-desc="說明屬性層授權缺失如何讓使用者讀寫不該暴露的欄位">BOPLA 卡片</a></li>
</ul>
<p><strong>Mass Assignment</strong>：</p>
<ul>
<li>應用層直接把 request body bind 到 DB row、含未檢查欄位</li>
<li>例：<code>Order.fromJSON(request.body)</code> 自動 set <code>is_admin_override</code> 為 true</li>
<li>防禦：明確 allowlist 哪些 field 可從 request 來</li>
<li>對應 <a href="/blog/backend/knowledge-cards/mass-assignment/" data-link-title="Mass Assignment" data-link-desc="說明自動綁定 request 欄位如何造成未授權欄位被修改">Mass Assignment 卡片</a></li>
</ul>
<p><strong>Multi-tenant Boundary Leak</strong>：</p>
<ul>
<li>multi-tenant SaaS：tenant A 的 query 不該看到 tenant B 的資料</li>
<li>常見錯誤：忘了 <code>WHERE tenant_id = ?</code>、用 application 層而非 DB 層強制</li>
<li>進階防禦：Row-Level Security（PostgreSQL RLS）、由 DB 強制 tenant boundary</li>
</ul>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 credential abuse</a> 揭露 <em>資料平台帳號沒強制 MFA</em> 的代價、攻擊者拿到外洩 credential 後直接 query 多家客戶的 Snowflake account、大量外送資料。判讀重點：DB 認證 = 資料邊界、但雲端資料平台預設未必開 MFA、要主動 enforce。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 紅隊版</a> — signing key 洩漏後攻擊者直接以任意 user 身份查任意 mailbox、application 層 BOLA / BOPLA 全部失效、因為攻擊者通過了底層 trust boundary。</p>
<h2 id="攻擊模式-3資料外洩類">攻擊模式 3：資料外洩類</h2>
<p><strong>Excessive Data Exposure</strong>：</p>
<ul>
<li>API 回應比需要的多（內部欄位、PII、信用卡末四碼）</li>
<li>「前端會 filter」是反模式 — 攻擊者直接看 raw response</li>
<li>防禦：DTO / response schema 明確列哪些欄位可回、不要 <code>SELECT *</code></li>
<li>對應 <a href="/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">Excessive Data Exposure 卡片</a></li>
</ul>
<p><strong>Log / Trace 洩漏</strong>：</p>
<ul>
<li>把 query 含 PII 直接寫進 log、log 進 SIEM、SIEM 給多人看</li>
<li>distributed tracing 把 query 跟 user_id 都記下來</li>
<li>防禦：log 前 redact、敏感欄位 mask、distributed tracing 的 attribute allowlist</li>
</ul>
<p><strong>Backup / Export 洩漏</strong>：</p>
<ul>
<li>DB backup 沒加密、放公開 S3 bucket</li>
<li>客服 / BI 工具導出 CSV、檔案被搬到不該的地方</li>
<li>防禦：backup encryption、export audit、emit-once endpoint</li>
<li><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 backup chain</a> — 開發環境被入侵後、攻擊者沿著 <em>備份路徑</em> 拿到 production vault backup、雖然 vault 內容是加密的、但 master password 弱的客戶可被離線爆破。判讀重點：備份檔案的 <em>存放位置</em> 跟 <em>加密狀態</em> 是攻擊面、不只 production DB。</li>
</ul>
<p><strong>Support Tool Path</strong>：</p>
<ul>
<li>客服 admin 工具可以 query 任何用戶資料</li>
<li>內部工具沒有 audit log、不知道誰看了什麼</li>
<li>防禦：客服 tool 必須 audit log、敏感欄位 mask、access 按 ticket 限制</li>
<li><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System 事件</a> — 攻擊者拿到 Okta support 系統存取後、能看到客戶上傳的 HAR 檔（含 session token）、再用 token 進客戶 tenant。Support tool 的 <em>查詢能力</em> 跟 <em>資料分級</em> 不對等就會放大事故面。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data protection and masking</a> 跟 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit trail</a>。</p>
<h2 id="攻擊模式-4競態--toctou-類">攻擊模式 4：競態 / TOCTOU 類</h2>
<p><strong>TOCTOU</strong>（Time of Check Time of Use）：</p>
<ul>
<li>檢查時是 A 狀態、用的時候是 B 狀態</li>
<li>例：先 SELECT 確認 user 有 100 credit、再 UPDATE 扣 100、中間有別的 transaction 改了 credit</li>
<li>防禦：用 <code>SELECT ... FOR UPDATE</code> 鎖、或用 atomic operation（<code>UPDATE ... WHERE credit &gt;= 100</code>）</li>
</ul>
<p><strong>Double-spend 攻擊</strong>：</p>
<ul>
<li>多個 request 同時花同一筆錢</li>
<li>防禦：optimistic locking with version、unique constraint、或交易層 serializable</li>
<li>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的 isolation level 段</li>
</ul>
<p><strong>Race condition in business logic</strong>：</p>
<ul>
<li>註冊：兩個 request 同時用同一個 email、可能都成功</li>
<li>防禦：unique constraint 在 DB 層、不只 application 層 check</li>
</ul>
<h2 id="攻擊模式-5dos--資源耗盡類">攻擊模式 5：DoS / 資源耗盡類</h2>
<p><strong>Unrestricted Resource Consumption</strong>：</p>
<ul>
<li>沒分頁的 <code>SELECT *</code>、用戶傳 <code>?limit=999999</code></li>
<li>沒 timeout 的長 query</li>
<li>防禦：query timeout、pagination 強制上限、rate limit</li>
</ul>
<p><strong>Connection 耗盡</strong>：</p>
<ul>
<li>攻擊者開大量 connection、佔光 DB connection pool</li>
<li>防禦：connection pool 限制、application 層 connection limit、PgBouncer 共享</li>
</ul>
<p><strong>Storage 灌爆</strong>：</p>
<ul>
<li>API 允許大量 insert、storage 被填滿</li>
<li>防禦：rate limit、quota per tenant、auto-archive</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/unrestricted-resource-consumption/" data-link-title="Unrestricted Resource Consumption" data-link-desc="說明缺少資源限制如何讓 API 被濫用或拖垮">Unrestricted Resource Consumption 卡片</a>。</p>
<h2 id="何時要提高紅隊檢查優先級">何時要提高紅隊檢查優先級</h2>
<p>下列訊號出現時、資料層弱點通常會放大成系統風險：</p>
<ul>
<li>角色與租戶模型快速增加、且查詢條件跨多個權限層</li>
<li>migration 頻率提高、且 schema 與讀寫流程同時變更</li>
<li>匯出、對帳、客服查詢與搜尋索引共用同一批敏感欄位</li>
<li>事故修復高度依賴人工 SQL 與臨時腳本</li>
<li>新引入的 ORM / query builder / cache layer 改變了 query 路徑</li>
</ul>
<h2 id="失敗代價">失敗代價</h2>
<p>資料層弱點會把單點錯誤轉成長尾影響。</p>
<ul>
<li><strong>越權查詢</strong>：直接資料洩漏 → 通知監管 + 客戶 + 媒體</li>
<li><strong>交易邊界混亂</strong>：部分寫入與狀態偏移 → 對帳成本 + 退款處理</li>
<li><strong>資料外洩進 log / backup</strong>：拉長處理週期 → 跨 team 清理</li>
<li><strong>support tool 濫用</strong>：無 audit log → 無法追究、信任成本上升</li>
<li><strong>業務全面中斷</strong>：資料事件升級成 availability 事件、整條業務鏈停擺</li>
</ul>
<p>這些問題的共同代價是：修復路徑長、稽核負擔高、信任成本上升。</p>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 ops impact</a> 是「資料事件變成業務連續性事件」的代表。攻擊者進入 DB 後、不只外洩資料、還破壞處理能力、讓整個美國醫療支付網路停擺數週。判讀重點：DB 失守不只代表 <em>資料外洩</em> 一種損失、還可能直接停掉 <em>上游業務流程</em>、評估代價時要把這層算進去。<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 identity lateral impact</a> 是另一個對照：vishing 拿到 identity 後橫向到核心系統、酒店訂房 / 自助 check-in / 老虎機全停。資料層的攻擊代價要跨業務流量去評估、不只看 DB 本身。</p>
<h2 id="incident-三角db-事故的同步處置">Incident 三角：DB 事故的同步處置</h2>
<p>DB 事故的處置三角是 <em>同步</em> 執行三件事、共同消除攻擊者在處置間隙繼續入侵的時間窗：</p>
<ol>
<li><strong>漏洞修補</strong>：補上被利用的具體漏洞或 misconfiguration</li>
<li><strong>Session / 憑證失效</strong>：撤銷所有可能被攻擊者拿到的 session、token、credential</li>
<li><strong>異常痕跡清查</strong>：盤點攻擊者已經做了什麼、哪些資料動過、哪些 backdoor 留下</li>
</ol>
<p>同步執行的理由是 <em>攻擊者擁有平行能力</em>：用已拿到的 credential 在 patch 完成前重新進入、或用清查前還沒被發現的 backdoor 繞過修補。線性執行「先修漏洞、再失效憑證、再清查」會留下兩個時間窗、攻擊代價被放大。</p>
<p><strong>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a></strong> — 公告漏洞到攻擊者大規模利用之間只有數小時、單純等 vendor 修補來不及。實務做法是：</p>
<ul>
<li><strong>發布前</strong>：對外服務建立 <em>即時隔離開關</em>、不等 vendor patch</li>
<li><strong>事故中</strong>：先把入口下線（DNS 切走 / WAF rule 全擋）、同步進行 patch + token revoke + audit log review</li>
<li><strong>前提</strong>：事先有 inventory（知道哪些產品接 DB）+ 自動化失效能力（不是手動逐個 revoke）</li>
</ul>
<p>這個三角是 <em>能力前提</em>、不是 <em>當下決策</em>。事故當下發現缺哪一角、就只能線性執行、攻擊代價會被放大。</p>
<h2 id="偵測與審計">偵測與審計</h2>
<p>紅隊檢查不只「找漏洞」、也要設計 <em>持續偵測</em>：</p>
<h3 id="1-query-audit">1. Query audit</h3>
<ul>
<li>DB query 寫進 audit log（誰、什麼時候、查了什麼）</li>
<li>不只 admin tool、application 也要 audit</li>
<li>對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log 卡片</a></li>
</ul>
<h3 id="2-anomaly-detection">2. Anomaly detection</h3>
<ul>
<li>異常 query pattern（突然 SELECT 全表、跨 tenant 範圍）</li>
<li>異常 export volume</li>
<li>Cross-tenant token 異常（同一 issuer 出現本不應跨域的軌跡）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection coverage</a></li>
</ul>
<p>Cross-tenant token 偵測是觀測單一 issuer 發出的 token 在不應跨域的 tenant 出現的能力。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558</a> — 偽造 token <em>形式上完全合法</em>、單看 token validation 找不到異常、要看 <em>軌跡</em>（哪個 issuer 的 token 跨了哪些 tenant、跟歷史 baseline 比對）。這層偵測需要 application 跟 DB layer 都記下「token 來源 → tenant 目的」的對應、才能事後比對。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 揭露的異常查詢偵測維度：</p>
<ul>
<li>query 體積異常（單一 user 短時間內查詢量遠超日常）</li>
<li>來源 IP 異常（從合法網段突然變成未知 endpoint）</li>
<li>跨 schema scan 模式（單一 user 突然查多個 tenant 的表）</li>
<li>匯出頻率異常（單位時間匯出次數遠超基線）</li>
</ul>
<p>這些維度都需要足夠歷史 telemetry 建立基線、新部署的 DB 在累積基線前處於偵測盲區、要靠 <em>絕對閾值</em> 補（例如「任何 user 單次查詢 &gt; 1GB 都告警」、不等基線）。</p>
<h3 id="3-db-level-monitoring">3. DB-level monitoring</h3>
<ul>
<li>slow query log（可能是 attacker 在 enumerate）</li>
<li>failed login（DB 層 connection attempt）</li>
<li>privilege escalation event</li>
</ul>
<h3 id="4-periodic-review">4. Periodic review</h3>
<ul>
<li>每季 review role / permission</li>
<li>每年 audit support tool access pattern</li>
<li>migration 後重新檢查 access boundary</li>
</ul>
<h2 id="認證--網路雙重防護">認證 + 網路雙重防護</h2>
<p>DB 認證 = 資料邊界、但雲端資料平台（Snowflake、BigQuery、Cosmos DB）預設未必開 MFA、且 <em>網路層通常 open</em>（任何 IP 都能嘗試連線）。任一層失守、攻擊者就進來。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> — 外洩 credential + 未強制 MFA + 沒設 network policy → 攻擊者直接從任意 IP 用 leaked credential 登入、查多家 tenant 的資料。</p>
<p><strong>雙重防護設計</strong>：</p>
<ul>
<li><strong>網路層</strong>：network rule allowlist（只允許公司 IP / VPN / 雲端 NAT 連線）— leaked credential 即使有效、也碰不到 DB</li>
<li><strong>認證層</strong>：強制 MFA + 條件式存取（context-aware：時間 / 地點 / 裝置）— 即使網路層失守、credential 還要過 MFA</li>
<li><strong>應用層</strong>：API key / service account 跟 user credential 分開、各有 lifecycle</li>
</ul>
<p>兩層獨立、單層失守仍能阻擋資料外送。資料平台預設應強制 MFA + network policy、把「credential 外洩 = 資料外送」這條捷徑切斷。</p>
<h2 id="批量憑證撤銷的工程能力">批量憑證撤銷的工程能力</h2>
<p>批量憑證撤銷能力是事故當下「攔停攻擊者」的核心動作、要 <em>快速、大量、選擇性</em> 執行可疑憑證撤銷。這個能力屬於 <em>事先準備</em>、事故當下臨時建來不及。</p>
<p><strong>最小能力清單</strong>：</p>
<ul>
<li><strong>Credential inventory</strong>：列出所有 active credential（user password、API key、service account token、session）。事故當下若靠工程師記憶查、會漏掉長期沒人動的 service account 或 OAuth integration、變成攻擊者 persist 的後門。Inventory 要 <em>自動產生</em>、不是人工維護的 spreadsheet。</li>
<li><strong>分批撤銷 API</strong>：能按 user group / service / scope 批次撤銷、不是逐個 revoke。批次需要 idempotency key、避免重複撤銷產生競爭。受影響範圍大時、逐個撤銷可能需要數小時、攻擊者持續外送資料。</li>
<li><strong>撤銷後 audit</strong>：撤銷紀錄要存（誰被撤、什麼時間、什麼原因、誰執行）、避免事後爭議。</li>
<li><strong>重新發放流程</strong>：撤銷後使用者要重新登入、SSO + MFA 流程在事故當下要能撐住瞬間湧入的重新驗證請求。若流程卡住、會在「沒攻擊但用戶進不來」狀態下被迫降回安全等級較低的應急 fallback、形成新攻擊面。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 的事故處置 — 平台級事故影響數百家客戶、撤銷必須跨 tenant 同步進行、單一客戶手動撤銷來不及。</p>
<h2 id="長期可重複匯出工件">長期可重複匯出工件</h2>
<p>Long-lived repeatable export artifact 是事故後仍能持續產出資料的工件、屬於跨事故時間軸的 attack surface。攻擊者拿到一次、就能長期外送、不需要每次重新進入系統。常見類型：</p>
<ul>
<li><strong>預先生成的報表 URL</strong>（內部 BI tool 給 download link、URL 通常長期有效）</li>
<li><strong>API key 綁定的 export endpoint</strong>（key 沒過期、endpoint 一直能匯出最新資料）</li>
<li><strong>資料平台的 scheduled / saved query</strong>（以合法 user 身份定期執行匯出）</li>
<li><strong>Database backup 的 share link</strong>（雲端儲存的 signed URL、有效期可達數年）</li>
</ul>
<p><strong>防禦設計</strong>：</p>
<ul>
<li><strong>預設短 TTL</strong>：所有匯出 URL / signed link 預設 1-24 小時失效</li>
<li><strong>單次性匯出</strong>：sensitive export 限定 emit-once、用過就失效</li>
<li><strong>匯出記錄審計</strong>：每次匯出寫進 audit log、定期審查哪些 endpoint 異常高頻使用</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 連結的紅隊 problem-card「Long-lived repeatable export artifact」— 這類工件的核心風險是 <em>憑證撤銷後仍可運作</em>、修復不只要撤 credential、還要盤所有由該 credential 建立的長效工件。</p>
<h2 id="備份-vs-正式環境的權限獨立性">備份 vs 正式環境的權限獨立性</h2>
<p>備份系統是 <em>獨立</em> 的攻擊面、跟正式環境要 <em>不同權限域</em>。常見錯誤是「備份用同一組 IAM principal 跟同一把 KMS key」、結果正式環境被打、攻擊者沿著 <em>備份路徑</em> 拿到所有歷史資料。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 backup chain</a> — 開發環境被入侵後、攻擊者沿著備份路徑拿到雲端備份的加密保管庫資料、形成長尾資料保護壓力。判讀重點：備份的 <em>存放位置</em>、<em>金鑰管理</em>、<em>存取權限</em> 都是攻擊面、不只 production DB；備份檔加密本身不足以擋下取走後的離線分析。</p>
<p><strong>權限獨立性設計</strong>：</p>
<ul>
<li><strong>不同 IAM principal</strong>：production 跟 backup 用不同 service account、production 帳號沒有 backup 讀權限</li>
<li><strong>不同 KMS key audience</strong>：production 用 production key、backup 用 backup key、兩者 lifecycle 分離</li>
<li><strong>不同 audit log</strong>：production read / write 跟 backup read 在 <em>不同</em> audit stream、後續調查能區分「正常運作」vs「備份被讀」</li>
<li><strong>不同 access pattern review</strong>：定期審查哪些 principal 在哪些時段讀 backup（正常情況很少有人讀 backup、頻繁讀取是異常訊號）</li>
</ul>
<p>「正式環境的接管不直接通到備份」是設計準則、不是 best practice 加分項。對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation</a> 的備份 / PITR 段討論。</p>
<h2 id="最低控制面">最低控制面</h2>
<p>資料層在討論具體服務前、先定義四個控制面最穩定：</p>
<ol>
<li><strong>權限模型</strong>：資料存取與角色、租戶、操作情境的對應關係</li>
<li><strong>交易與一致性模型</strong>：哪些操作必須同成敗、哪些可以延遲一致</li>
<li><strong>資料分級與遮罩模型</strong>：哪些欄位可回傳、可觀測、可匯出</li>
<li><strong>恢復模型</strong>：錯誤資料如何比對、回復、追蹤與稽核</li>
</ol>
<h2 id="案例對照">案例對照</h2>
<h3 id="07-主案例產品--平台事故">07 主案例（產品 / 平台事故）</h3>
<table>
  <thead>
      <tr>
          <th>07 案例</th>
          <th>跟資料層的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">7.C1 Cloudflare Route Leak</a></td>
          <td>控制面變更可能影響資料層存取</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2 Cloudflare Token 事件</a></td>
          <td>Token 洩漏 → DB 存取被濫用</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">7.C3 Azure AD 2021</a></td>
          <td>identity failure → 應用 fallback、可能讓 DB 存取錯誤路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">7.C4 Microsoft Storm-0558</a></td>
          <td>signing key 洩漏 → 任意 user 身份、可 query 任何資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">7.C5 Okta Support System</a></td>
          <td>support tool 洩漏 → 客戶資料被存取</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">7.C6 Okta Cross-Tenant</a></td>
          <td>tenant boundary 失守 → DB-level RLS 也擋不住</td>
      </tr>
  </tbody>
</table>
<h3 id="07-紅隊案例攻擊鏈--入侵路徑">07 紅隊案例（攻擊鏈 / 入侵路徑）</h3>
<table>
  <thead>
      <tr>
          <th>紅隊案例</th>
          <th>攻擊鏈到資料層的路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 憑證濫用</a></td>
          <td>外洩 credential + 未強制 MFA → 直接 query 多家 tenant 資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 備份鏈</a></td>
          <td>開發環境 → production backup 路徑 → 客戶加密 vault 外送</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023 mass exfiltration</a></td>
          <td>file transfer 產品零時差 → 後端資料批量外送</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 ops impact</a></td>
          <td>DB 入侵 → 醫療支付網路全面停擺、資料事件升級成業務中斷</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 signing key chain</a></td>
          <td>signing key 洩漏 → 任意身份 token forge → application BOLA / BOPLA 全部失效</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 identity lateral impact</a></td>
          <td>社交工程 → identity lateral → 業務系統全停、資料層攻擊代價跨業務流量</td>
      </tr>
  </tbody>
</table>
<p>紅隊案例庫的完整入口看 <a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">紅隊案例參考地圖</a> — 那邊有按攻擊階段（exposure / exfiltration / identity / supply-chain）的完整索引。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：race condition / TOCTOU 用 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 的 isolation level 處理</li>
<li>與 1.4 的交接：repository adapter 應用 allowlist / parameterized query — <a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">repository adapter</a></li>
<li>與 1.8 的交接：state ownership 決定哪些資料需要嚴格存取控制 — <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a></li>
<li>與 7.2 的交接：identity / authorization 邊界 — <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity &amp; Access Boundary</a></li>
<li>與 7.4 的交接：資料保護與遮罩 — <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking</a></li>
<li>與 7.7 的交接：audit trail — <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a></li>
<li>與 7.13 的交接：detection coverage — <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>與 8.19 的交接：事故時的資料層判讀 — <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
<li>合規驅動的多 region 部署選型：<a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">Aurora global database 多 region</a>、<a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">Aurora 跨 AZ failover RTO</a>、<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency 知識卡</a></li>
</ol>
<h2 id="關聯卡片">關聯卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">Attack Surface</a></li>
<li><a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">Trust Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">Excessive Data Exposure</a></li>
<li><a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR</a></li>
<li><a href="/blog/backend/knowledge-cards/bopla/" data-link-title="BOPLA" data-link-desc="說明屬性層授權缺失如何讓使用者讀寫不該暴露的欄位">BOPLA</a></li>
<li><a href="/blog/backend/knowledge-cards/mass-assignment/" data-link-title="Mass Assignment" data-link-desc="說明自動綁定 request 欄位如何造成未授權欄位被修改">Mass Assignment</a></li>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
<li><a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">Data Reconciliation</a></li>
<li><a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">Tenant Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/unrestricted-resource-consumption/" data-link-title="Unrestricted Resource Consumption" data-link-desc="說明缺少資源限制如何讓 API 被濫用或拖垮">Unrestricted Resource Consumption</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/</guid><description>&lt;p>DynamoDB 是 AWS managed key-value store、用 partition-based scaling 提供 &lt;em>可預測 P99 latency&lt;/em> 跟 &lt;em>elastic capacity&lt;/em>。Amazon 自家 Ads（9000 萬 RPS）、Disney+、Zoom（COVID 30x surge）、Capcom（billions of requests / single-digit ms）都用 DynamoDB 撐核心 workload — 它是目前公開 case 最多、最被驗證的 managed KV 服務。&lt;/p>
&lt;h2 id="教學路線access-pattern-與-partition-capacity">教學路線：Access pattern 與 partition capacity&lt;/h2>
&lt;p>DynamoDB 服務頁的教學目標是把 access pattern 轉成 partition key、sort key、GSI、capacity mode 與 global tables 的設計判斷。讀者讀完後要能從查詢路徑反推資料模型，並估算 hot partition、成本與 consistency trade-off。&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>Access pattern&lt;/td>
 &lt;td>查詢形狀如何先於資料表設計&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Partition key&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition&lt;/a>、single-digit latency、GSI 如何成為設計核心&lt;/td>
 &lt;td>容量規劃要點、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity mode&lt;/td>
 &lt;td>on-demand、provisioned、auto scaling 如何對應高峰與成本&lt;/td>
 &lt;td>容量特性、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Global tables&lt;/td>
 &lt;td>multi-region availability 與 consistency 會付出哪些代價&lt;/td>
 &lt;td>適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時回 SQL、MongoDB、Cosmos DB 或 cache / queue&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位partition-based-kv-scale">定位：partition-based KV scale&lt;/h2>
&lt;p>DynamoDB 的核心設計是「partition 透明、capacity 抽象化」。不像 MongoDB 要主動 shard、不像 Cassandra 要管 ring topology、不像 PostgreSQL 要選 instance type — DynamoDB 把所有底層 scaling 隱藏在 RCU / WCU 抽象層後。&lt;/p>
&lt;p>&lt;strong>容量單位&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>1 RCU（Read Capacity Unit）= 1 strongly consistent read of 4KB / sec、2 eventually consistent reads&lt;/li>
&lt;li>1 WCU（Write Capacity Unit）= 1 write of 1KB / sec&lt;/li>
&lt;li>每個 partition 上限：3000 RCU / 1000 WCU&lt;/li>
&lt;li>總容量 = partition 數量 × 每 partition 上限（partition 數量透明、vendor 自動管理）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>延遲特性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>single-digit millisecond p99 latency（read / write）&lt;/li>
&lt;li>同 region 跨 AZ replication 內建、預設 eventually consistent reads&lt;/li>
&lt;li>strongly consistent reads 依 region 內 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum&lt;/a> 成立，跨 region 讀寫要看 Global Tables 語意&lt;/li>
&lt;/ul>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的 partition 設計章節。&lt;/p></description><content:encoded><![CDATA[<p>DynamoDB 是 AWS managed key-value store、用 partition-based scaling 提供 <em>可預測 P99 latency</em> 跟 <em>elastic capacity</em>。Amazon 自家 Ads（9000 萬 RPS）、Disney+、Zoom（COVID 30x surge）、Capcom（billions of requests / single-digit ms）都用 DynamoDB 撐核心 workload — 它是目前公開 case 最多、最被驗證的 managed KV 服務。</p>
<h2 id="教學路線access-pattern-與-partition-capacity">教學路線：Access pattern 與 partition capacity</h2>
<p>DynamoDB 服務頁的教學目標是把 access pattern 轉成 partition key、sort key、GSI、capacity mode 與 global tables 的設計判斷。讀者讀完後要能從查詢路徑反推資料模型，並估算 hot partition、成本與 consistency trade-off。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Access pattern</td>
          <td>查詢形狀如何先於資料表設計</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Partition key</td>
          <td><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、single-digit latency、GSI 如何成為設計核心</td>
          <td>容量規劃要點、常見陷阱</td>
      </tr>
      <tr>
          <td>Capacity mode</td>
          <td>on-demand、provisioned、auto scaling 如何對應高峰與成本</td>
          <td>容量特性、案例對照</td>
      </tr>
      <tr>
          <td>Global tables</td>
          <td>multi-region availability 與 consistency 會付出哪些代價</td>
          <td>適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時回 SQL、MongoDB、Cosmos DB 或 cache / queue</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位partition-based-kv-scale">定位：partition-based KV scale</h2>
<p>DynamoDB 的核心設計是「partition 透明、capacity 抽象化」。不像 MongoDB 要主動 shard、不像 Cassandra 要管 ring topology、不像 PostgreSQL 要選 instance type — DynamoDB 把所有底層 scaling 隱藏在 RCU / WCU 抽象層後。</p>
<p><strong>容量單位</strong>：</p>
<ul>
<li>1 RCU（Read Capacity Unit）= 1 strongly consistent read of 4KB / sec、2 eventually consistent reads</li>
<li>1 WCU（Write Capacity Unit）= 1 write of 1KB / sec</li>
<li>每個 partition 上限：3000 RCU / 1000 WCU</li>
<li>總容量 = partition 數量 × 每 partition 上限（partition 數量透明、vendor 自動管理）</li>
</ul>
<p><strong>延遲特性</strong>：</p>
<ul>
<li>single-digit millisecond p99 latency（read / write）</li>
<li>同 region 跨 AZ replication 內建、預設 eventually consistent reads</li>
<li>strongly consistent reads 依 region 內 <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> 成立，跨 region 讀寫要看 Global Tables 語意</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的 partition 設計章節。</p>
<h2 id="適用場景">適用場景</h2>
<p>按公開 case 提煉的典型適用場景：</p>
<p><strong>1. KV / single-table design 為主的查詢</strong>：</p>
<ul>
<li>用 partition key + sort key 設計、單筆 / 範圍查詢</li>
<li>查詢路徑固定，JOIN / ad-hoc query 需求低</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 9000 萬 reads/sec + 500 萬 writes/sec、99.999% 可用</li>
</ul>
<p><strong>2. 可預測 sub-10ms p99 latency 需求</strong>：</p>
<ul>
<li>遊戲後端（玩家狀態、戰績）</li>
<li>內容平台 metadata（watchlist、播放進度）</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom</a>（billions of requests / single-digit ms）、<a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a>（每日數十億 actions）</li>
</ul>
<p><strong>3. 流量 spiky 或 surge 場景</strong>：</p>
<ul>
<li>on-demand capacity 自動吸收 burst</li>
<li>不需 connection pool（HTTP API、無 stateful connection）</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a>（COVID 1000 萬 → 3 億 DAU）、<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（IOPS 20 → 135K、售票搶購）、<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>（RDB connection limit → 改 DynamoDB）</li>
</ul>
<p><strong>4. 大規模通知 / 訊息系統</strong>：</p>
<ul>
<li>TTL 自動清理過期 records</li>
<li>partition key 用 user_id / message_id 天然均勻</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a>（行動支付每日 3 億訊息）</li>
</ul>
<p><strong>5. 5 個 9 可用性 B2B SaaS</strong>：</p>
<ul>
<li>multi-region Global Tables active-active</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a>（99.999% 跨 15 region）</li>
</ul>
<p><strong>6. 高吞吐 budget 敏感</strong>：</p>
<ul>
<li>on-demand 適合突發、provisioned 適合 sustained</li>
<li>對應案例：<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> — TiDB over-provision 壓力轉成 DynamoDB on-demand pay-per-use，成本下降 50%</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 複雜 ad-hoc query / JOIN</strong>：</p>
<ul>
<li>DynamoDB query 以 partition key + sort key 為主，JOIN-heavy workload 交給 SQL 系統</li>
<li>PartiQL 提供 SQL-like 語法但底層還是 KV、複雜 query 會 scan 全表</li>
<li>替代：用 Aurora / PostgreSQL / Spanner</li>
</ul>
<p><strong>2. 強一致 multi-row transaction</strong>：</p>
<ul>
<li>DynamoDB Transaction 支援 25 個 item 的 ACID</li>
<li>超過 25 個 item 或跨 region 的 transaction 要改用 workflow / SQL / distributed SQL 設計</li>
<li>替代：Spanner / Aurora DSQL / CockroachDB</li>
</ul>
<p><strong>3. 跨雲需求</strong>：</p>
<ul>
<li>DynamoDB only on AWS、vendor lock-in</li>
<li>替代：Cosmos DB（Azure global NoSQL）、自管 ScyllaDB</li>
</ul>
<p><strong>4. 大物件 / 文件儲存</strong>：</p>
<ul>
<li>單一 item 最大 400KB</li>
<li>大物件用 S3、metadata 用 DynamoDB</li>
</ul>
<p><strong>5. 預算極度敏感 + 流量穩定</strong>：</p>
<ul>
<li>流量高度 predictable 的 sustained workload，自管 PostgreSQL / MySQL 可能更便宜</li>
<li>DynamoDB 的 managed 跟 elastic 是有溢價的</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs MongoDB（自管或 Atlas）</strong>：</p>
<ul>
<li>DynamoDB：managed、partition 透明、application 主要管理 partition key，有 5 個 9 SLA</li>
<li>MongoDB：彈性高、可自管、aggregation pipeline 強、跨雲可用</li>
<li>選 DynamoDB：AWS-only、想轉移 operation、partition 設計簡單可預測</li>
<li>選 MongoDB：跨雲、複雜 query、ad-hoc analysis</li>
</ul>
<p><strong>vs Aurora（同 AWS）</strong>：</p>
<ul>
<li>DynamoDB：KV、partition 擴展、無 connection pool 限制</li>
<li>Aurora：SQL（PostgreSQL / MySQL）、有 transaction、ad-hoc query</li>
<li>詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 跟 <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 case</a> — connection limit 是 RDB vs DynamoDB 的關鍵差異</li>
</ul>
<p><strong>vs Redis（含 ElastiCache）作為 KV 替代</strong>：</p>
<ul>
<li>DynamoDB：持久化、單 item 持久查得到、有 TTL 但物件不會自動失蹤</li>
<li>Redis：純記憶體、預設不持久（MemoryDB 例外）、快但易失</li>
<li>選 DynamoDB：data 是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，需要持久保存</li>
<li>選 Redis：data 是 cache、丟了能 recompute</li>
</ul>
<p><strong>vs Cosmos DB（cross-cloud）</strong>：</p>
<ul>
<li>DynamoDB：AWS-only、KV 為主、無 multi-model</li>
<li>Cosmos DB：Azure-only、multi-model（SQL / Mongo / Cassandra / Gremlin / Table）、5 個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>s</li>
<li>選 DynamoDB：AWS 生態、KV 純粹</li>
<li>選 Cosmos DB：Azure 生態、需要 multi-model、需要 multi-region active-active write</li>
</ul>
<p><strong>vs Cassandra / ScyllaDB（self-managed）</strong>：</p>
<ul>
<li>DynamoDB：managed、5 個 9 SLA、無 ops 負擔</li>
<li>Cassandra / ScyllaDB：可自管、更深 tuning、跨雲可用</li>
<li>選 DynamoDB：團隊想把 DBA / SRE 操作責任交給 AWS</li>
<li>選 Cassandra / ScyllaDB：有 DBA、想 lock-in 風險低、需要極限 throughput tuning</li>
</ul>
<p><strong>vs PostgreSQL（SQL baseline）</strong>：</p>
<ul>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a> 取捨段、跟 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 的 connection model 對比</li>
<li>摘要：DynamoDB 是 <em>access pattern 固定 + 需要避免 connection-bound</em> 的選項；ad-hoc query / 複雜 transaction 留 PostgreSQL</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>從 09 案例庫提煉的 DynamoDB 容量規劃實踐：</p>
<p><strong>1. partition key 設計是命脈</strong>：</p>
<ul>
<li>partition key 不均 → hot partition → 名義容量達不到</li>
<li>composite key（event_id + user_id_hash）強制分散</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 9000 萬 RPS 靠 partition 均勻、<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 用 composite key 分散售票流量</li>
<li>詳見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a></li>
</ul>
<p><strong>2. on-demand vs provisioned 選型</strong>：</p>
<ul>
<li>流量 peak/avg &gt; 5x → on-demand</li>
<li>sustained predictable → provisioned + auto-scaling</li>
<li>知名大事件（Black Friday）→ provisioned baseline + scheduled scale-up</li>
<li>對應 <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> — on-demand 解放 over-provisioning</li>
</ul>
<p><strong>3. Global Tables（multi-region active-active）</strong>：</p>
<ul>
<li>每個 region 都能寫、conflict resolution 用 LWW</li>
<li>容量在每個 region 獨立配置，全球總和要按 region 分別估算</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 15 region 達 5 個 9 可用</li>
</ul>
<p><strong>4. DAX（DynamoDB Accelerator）</strong>：</p>
<ul>
<li>DynamoDB 前置 in-memory cache</li>
<li>從 single-digit ms 降到 microsecond</li>
<li>適合超高 read 重複的 workload（同樣 key 大量讀）</li>
<li>對應 <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> 用 DAX 加速</li>
</ul>
<p><strong>5. Streams + Lambda</strong>：</p>
<ul>
<li>DynamoDB 寫入 → Stream event → Lambda 處理</li>
<li>適合 CDC、event-driven 工作流</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 用 Stream 把 DynamoDB 當 durable queue 給 legacy server 消費</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>DynamoDB 的 managed elasticity 會讓團隊忽略 access pattern 的前置成本。這一段先說何時維持單純 table / index，再說何時升級到 Global Tables、DAX、Streams、或改回 SQL / document DB。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 table / 少量 GSI</td>
          <td>access pattern 穩定、partition key 均勻、query 成本可預測</td>
          <td>新查詢路徑大量增加、GSI 成本壓過主表、hot partition 出現</td>
          <td><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>、<a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a></td>
      </tr>
      <tr>
          <td>On-demand capacity</td>
          <td>peak/avg 差距大、流量有事件性 surge</td>
          <td>sustained traffic 穩定、成本曲線可預測</td>
          <td><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></td>
      </tr>
      <tr>
          <td>Provisioned + autoscaling</td>
          <td>baseline 穩定、團隊能預測高峰</td>
          <td>黑五、售票、直播等已知大事件需要預先升配</td>
          <td><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></td>
      </tr>
      <tr>
          <td>DAX</td>
          <td>read 重複率低、single-digit ms 已足夠</td>
          <td>同 key 超高讀取、需要 microsecond read</td>
          <td><a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">Cache Aside</a>、<a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data</a></td>
      </tr>
      <tr>
          <td>Global Tables</td>
          <td>single-region availability 已足夠</td>
          <td>RTO/RPO、region residency 或 active-active write 是產品需求</td>
          <td><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">Consistency Level</a></td>
      </tr>
      <tr>
          <td>SQL / document DB</td>
          <td>access pattern 可提前列舉</td>
          <td>ad-hoc query、JOIN、multi-row transaction 或 document traversal 成主題</td>
          <td><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>、<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a></td>
      </tr>
  </tbody>
</table>
<p>DynamoDB 的簡單路徑是先把每個 query path 寫成契約。table、partition key、sort key、GSI 與 TTL 都應從 access pattern 反推；如果需求仍在探索期，PostgreSQL 或 MongoDB 可能提供更低的變更成本。</p>
<p>Global Tables 的升級路徑要先處理 conflict 與讀寫語意。它提供 multi-region availability，但 LWW conflict resolution、region-local capacity 與跨 region reconciliation 仍要由 application contract 承擔。</p>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本 vendor 現有 deep article 覆蓋 DynamoDB 從 access pattern 反推到寫一致性、讀加速、事件驅動與資料生命週期的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>適用度 4 軸前置判讀 + access pattern 反推 PK/SK + durable queue</td>
          <td><a href="single-table-design-pattern/">single-table-design-pattern</a></td>
          <td>適用度判讀 + control plane vs data plane + 9.C15 Tixcraft Stream durable queue</td>
      </tr>
      <tr>
          <td>1000 WCU partition 上限 + composite key / calculated shard 修法</td>
          <td><a href="partition-key-antipatterns/">partition-key-antipatterns</a></td>
          <td>9.C15 Tixcraft 6750x 擴展、mode × partition 在 provisioned / on-demand 表現</td>
      </tr>
      <tr>
          <td>GSI / LSI projection 三型、sparse、DAX 補位</td>
          <td><a href="gsi-lsi-design/">gsi-lsi-design</a></td>
          <td>GSI 自己會 hot partition、Capcom derive vs Lemino case fact 分層</td>
      </tr>
      <tr>
          <td>6 軸 capacity mode 決策 + auto-scaling 邊界 + cost crossover</td>
          <td><a href="on-demand-vs-provisioned/">on-demand-vs-provisioned</a></td>
          <td>Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload</td>
      </tr>
      <tr>
          <td>Multi-region active-active + LWW conflict + cross-device sync</td>
          <td><a href="global-tables-conflict/">global-tables-conflict</a></td>
          <td>Genesys 99.999% / 15 region、Disney+ 跨裝置同步</td>
      </tr>
      <tr>
          <td>Strongly / eventually consistent read 取捨</td>
          <td><a href="consistency-model-optimization/">consistency-model-optimization</a></td>
          <td>read consistency 成本選擇</td>
      </tr>
      <tr>
          <td>跨 item 原子性 + conditional write + optimistic lock + idempotency</td>
          <td><a href="transactions-conditional-writes/">transactions-conditional-writes</a></td>
          <td>雙寫不一致、超賣 race、transaction 2x 成本邊界</td>
      </tr>
      <tr>
          <td>DAX cluster + item/query cache + write-through + invalidation 邊界</td>
          <td><a href="dax-caching-strategy/">dax-caching-strategy</a></td>
          <td>讀峰值 p99 尖刺、query cache 只靠 TTL 失效、strong read 繞過 cache</td>
      </tr>
      <tr>
          <td>Streams CDC + shard 順序 + Lambda 消費 + 失敗處理</td>
          <td><a href="streams-lambda-event-driven/">streams-lambda-event-driven</a></td>
          <td>下游即時反應、at-least-once 冪等、毒丸 record 隔離</td>
      </tr>
      <tr>
          <td>TTL 自動過期 + 48h 刪除延遲 + 過期仍可讀 + storage 成本</td>
          <td><a href="ttl-data-lifecycle/">ttl-data-lifecycle</a></td>
          <td>9.C26 PayPay 每日上億訊息 storage 清理、過期未刪 item 讀取陷阱</td>
      </tr>
  </tbody>
</table>
<p>Migration playbook：<a href="migrate-rds-mongodb-to-dynamodb/">從 RDS / MongoDB 遷移到 DynamoDB</a>（Type E paradigm shift、access-pattern-first 重建模 + 混合架構 + Zomato cost crossover）。</p>
<p>跨 vendor entry：先看 <a href="../db3-vendor-selection/">DB3 vendor selection</a>（MongoDB / DynamoDB / Cosmos DB 三方選型 + workload shape 前置判讀），再進本 vendor 的 deep article。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>DynamoDB Streams 進階 lab：Kinesis Data Streams for DynamoDB 多消費者 fan-out 與長 retention 重播（Lambda vs Kinesis 比較層已在 <a href="streams-lambda-event-driven/">streams-lambda-event-driven</a> 覆蓋、此處指可操作的深度 hands-on lab）</li>
<li>Export to S3 / point-in-time export 做離線分析</li>
<li>DynamoDB → SQL / search / analytics split（遷出方向 playbook）</li>
<li>Backup / PITR restore drill（hands-on lab）</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>規模</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>9000 萬 RPS + 500 萬 WPS</td>
          <td>partition 均勻設計典範</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>IOPS 20 → 135K（6750x 擴展）</td>
          <td>flash-sale 緩衝模式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>30x DAU surge（1000 萬 → 3 億）</td>
          <td>SaaS surge baseline 重新校準</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>billions of requests / single-digit ms</td>
          <td>遊戲後端 KV、跨遊戲共用平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>4x 吞吐、90% latency 降、50% 成本降</td>
          <td>TiDB → DynamoDB cross-DB 遷移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a></td>
          <td>99.999% / 15 region / 8000+ orgs</td>
          <td>B2B SaaS 5 個 9 可用性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>3 億 訊息 / 天</td>
          <td>行動支付通知系統、TTL 自動清理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>每日數十億 actions</td>
          <td>串流 metadata 層 + cross-device 同步</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>tens of thousands req/sec、5M MAU / 3 月</td>
          <td>RDB connection limit → DynamoDB</td>
      </tr>
  </tbody>
</table>
<p>DynamoDB case 的讀法是先分類 access pattern，再看容量模式。Amazon Ads / Capcom / Disney+ 說明高吞吐 KV，Zoom / Tixcraft / Lemino 說明 surge 與 connection-free scaling，Zomato 則說明 on-demand cost model 如何改變 over-provision 壓力。</p>
<h2 id="反向-sibling-路由">反向 sibling 路由</h2>
<p>DynamoDB 的反向 sibling 路由用來把 RDBMS 退場條件寫清楚。若讀者從 PostgreSQL / MySQL 的 connection bottleneck 過來，先讀 <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 case</a> 與 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>；若需求仍需要 ad hoc SQL、join 與 transaction report，回 <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> 或 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a>；若需求是 global document model 與 Azure 生態，再對照 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a>。</p>
<p>這條路由的判準是 access pattern 是否穩定到可以先設計 key。DynamoDB 擅長固定 lookup、寫入尖峰、connection-free scaling 與 TTL 類生命週期；資料探索、報表 join 與多條件查詢仍應留在 SQL / search / analytics service。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<p>從公開 incident 跟 case 提煉：</p>
<ul>
<li><strong>partition key 集中</strong>：event_id 一個演唱會、bot user 大量同 user_id 寫入 → 用 composite key 或 write sharding</li>
<li><strong>單一 partition 達 3000 RCU / 1000 WCU 上限</strong>：throttling event 出現、即使整體 capacity 還沒滿</li>
<li><strong>Scan 全表</strong>：scan 會吃光 capacity，正式讀取路徑應回到 query / index design</li>
<li><strong>DAX 跟 DynamoDB 直連混用</strong>：寫入直連 DynamoDB、讀經過 DAX → cache 一致性問題</li>
<li><strong>Global Tables conflict</strong>：跨 region 同 key 同時被寫、LWW 可能丟失寫入、要設計 idempotency</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<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 page</a>（SQL 對比）</li>
<li>上游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（從 RDBMS 遷 DynamoDB 案例）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>、<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>Last reviewed：2026-05-22（capacity mode / Global Tables / best practices 屬時間敏感 claim）</li>
<li>官方：<a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a>、<a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html">DynamoDB 設計 best practices</a></li>
</ul>
]]></content:encoded></item><item><title>9.5 瓶頸定位流程</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>瓶頸定位的責任是回答「為什麼擴 app 沒用」這類問題。當 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 找到 knee point 之後、下一步是知道 &lt;em>哪個 resource&lt;/em> 先 saturate。沒有定位、容量規劃只能 &lt;em>全部翻倍&lt;/em>；有定位、可以 &lt;em>精準加在瓶頸層&lt;/em>。&lt;/p>
&lt;p>跟其他章節的關係：跟 9.4 是姊妹章（9.4 找出 knee、9.5 定位 knee 的成因）、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性&lt;/a> 互補（9.8 訊號治理、9.5 用訊號做定位）。&lt;/p>
&lt;p>本章不深入工具操作、聚焦在 &lt;em>方法論&lt;/em> — 怎麼按層次定位、怎麼避免常見誤判、怎麼區分可分散 vs 不可分散瓶頸。&lt;/p>
&lt;h2 id="use-methodresource-oriented-觀察">USE method：resource-oriented 觀察&lt;/h2>
&lt;p>Brendan Gregg 的 USE method 提供逐層定位的最小框架：對每個資源、量三個維度。&lt;/p>
&lt;p>&lt;strong>Utilization&lt;/strong>：資源使用率 0-100%。CPU 70%、memory 60%、disk 40% 這類數字。
&lt;strong>Saturation&lt;/strong>：資源排隊量（queue depth）。CPU run queue length、memory swap rate、disk I/O wait queue、connection pool wait count。
&lt;strong>Errors&lt;/strong>：資源層錯誤。CPU page fault、memory OOM、disk I/O error、network packet drop、connection refused。&lt;/p>
&lt;p>對每個資源（CPU / RAM / disk / network / DB connection / cache connection / file descriptor）逐一檢查。&lt;em>第一個出現 saturation 上升的資源是 bottleneck&lt;/em>、不是 utilization 最高的那個。&lt;/p>
&lt;p>USE 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method&lt;/a>（rate / errors / duration）互補：USE 看「哪個資源頂不住」、RED 看「哪個 endpoint 表現變差」。容量規劃通常先用 USE 找瓶頸、再用 RED 看影響面。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片&lt;/a>。&lt;/p>
&lt;h2 id="逐層定位流程">逐層定位流程&lt;/h2>
&lt;p>從 application 層往下查、按依賴鏈逐層檢查。多數 bottleneck 在 application 跟 DB 兩層、但不能跳過其他層 — 偶爾真的在意外位置。&lt;/p>
&lt;p>&lt;strong>1. 應用層（application）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>thread / coroutine pool 使用率：是否已飽和&lt;/li>
&lt;li>event loop lag（Node.js、async runtime）：&amp;gt; 50ms 是警訊&lt;/li>
&lt;li>GC pause 頻率與時長：影響 p99 / p999&lt;/li>
&lt;li>request queue（accept queue、application internal queue）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. DB 層&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>connection pool 使用率（最常見隱性 bottleneck）&lt;/li>
&lt;li>slow query frequency&lt;/li>
&lt;li>replication lag&lt;/li>
&lt;li>lock contention（row lock、table lock）&lt;/li>
&lt;li>transaction queue depth&lt;/li>
&lt;/ul>
&lt;p>定位到 DB 層瓶頸時、優先檢查 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式&lt;/a> 清單 — 多數 DB 層瓶頸的根因是「應用程式發給 DB 的 query 寫法」、不是 DB 規格不夠。N+1 query 放大 connection 占用、long-running transaction 放大 lock contention、缺索引讓 slow query frequency 升高、&lt;code>SELECT *&lt;/code> 放大 transaction queue。這層判讀走完、再考慮 DB 規格升級或加 replica。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>瓶頸定位的責任是回答「為什麼擴 app 沒用」這類問題。當 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 找到 knee point 之後、下一步是知道 <em>哪個 resource</em> 先 saturate。沒有定位、容量規劃只能 <em>全部翻倍</em>；有定位、可以 <em>精準加在瓶頸層</em>。</p>
<p>跟其他章節的關係：跟 9.4 是姊妹章（9.4 找出 knee、9.5 定位 knee 的成因）、跟 <a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a> 互補（9.8 訊號治理、9.5 用訊號做定位）。</p>
<p>本章不深入工具操作、聚焦在 <em>方法論</em> — 怎麼按層次定位、怎麼避免常見誤判、怎麼區分可分散 vs 不可分散瓶頸。</p>
<h2 id="use-methodresource-oriented-觀察">USE method：resource-oriented 觀察</h2>
<p>Brendan Gregg 的 USE method 提供逐層定位的最小框架：對每個資源、量三個維度。</p>
<p><strong>Utilization</strong>：資源使用率 0-100%。CPU 70%、memory 60%、disk 40% 這類數字。
<strong>Saturation</strong>：資源排隊量（queue depth）。CPU run queue length、memory swap rate、disk I/O wait queue、connection pool wait count。
<strong>Errors</strong>：資源層錯誤。CPU page fault、memory OOM、disk I/O error、network packet drop、connection refused。</p>
<p>對每個資源（CPU / RAM / disk / network / DB connection / cache connection / file descriptor）逐一檢查。<em>第一個出現 saturation 上升的資源是 bottleneck</em>、不是 utilization 最高的那個。</p>
<p>USE 跟 <a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a>（rate / errors / duration）互補：USE 看「哪個資源頂不住」、RED 看「哪個 endpoint 表現變差」。容量規劃通常先用 USE 找瓶頸、再用 RED 看影響面。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片</a>。</p>
<h2 id="逐層定位流程">逐層定位流程</h2>
<p>從 application 層往下查、按依賴鏈逐層檢查。多數 bottleneck 在 application 跟 DB 兩層、但不能跳過其他層 — 偶爾真的在意外位置。</p>
<p><strong>1. 應用層（application）</strong>：</p>
<ul>
<li>thread / coroutine pool 使用率：是否已飽和</li>
<li>event loop lag（Node.js、async runtime）：&gt; 50ms 是警訊</li>
<li>GC pause 頻率與時長：影響 p99 / p999</li>
<li>request queue（accept queue、application internal queue）</li>
</ul>
<p><strong>2. DB 層</strong>：</p>
<ul>
<li>connection pool 使用率（最常見隱性 bottleneck）</li>
<li>slow query frequency</li>
<li>replication lag</li>
<li>lock contention（row lock、table lock）</li>
<li>transaction queue depth</li>
</ul>
<p>定位到 DB 層瓶頸時、優先檢查 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 清單 — 多數 DB 層瓶頸的根因是「應用程式發給 DB 的 query 寫法」、不是 DB 規格不夠。N+1 query 放大 connection 占用、long-running transaction 放大 lock contention、缺索引讓 slow query frequency 升高、<code>SELECT *</code> 放大 transaction queue。這層判讀走完、再考慮 DB 規格升級或加 replica。</p>
<p><strong>3. Cache 層</strong>：</p>
<ul>
<li>hit rate（突然下降是訊號）</li>
<li>eviction rate</li>
<li>connection 飽和（cache pool 也會耗盡）</li>
<li>memory utilization</li>
</ul>
<p><strong>4. Broker / queue 層</strong>：</p>
<ul>
<li>consumer lag（最重要的單一指標）</li>
<li>queue depth</li>
<li>dead-letter rate</li>
<li>broker connection count</li>
</ul>
<p><strong>5. 外部 API / 第三方 quota</strong>：</p>
<ul>
<li>rate limit 觸發頻率</li>
<li>retry storm（自家 retry 把對方 quota 打爆）</li>
<li>circuit breaker trip</li>
<li>timeout rate</li>
</ul>
<p><strong>6. 網路層</strong>：</p>
<ul>
<li>bandwidth utilization</li>
<li>packets per second（PPS limit）</li>
<li>socket count（file descriptor limit）</li>
<li>跨 region / 跨 AZ latency</li>
</ul>
<p><strong>7. DNS / load balancer</strong>：</p>
<ul>
<li>DNS resolution latency</li>
<li>LB connection establishment time</li>
<li>TLS handshake duration</li>
<li>backend health check failure</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino</a> RDB connection limit 是隱性 bottleneck、CPU / RAM 都還行；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 付款層獨立</a> — 把高頻搶票流量跟低頻付款流量分離、避免一層拖累另一層。</p>
<h2 id="profile-工具鏈">Profile 工具鏈</h2>
<p>USE 找出哪一層 saturate 之後、profile 工具找出 <em>該層的哪段 code</em> 拖累。</p>
<p><strong>Continuous profiling</strong>：Datadog Continuous Profiler、Pyroscope（開源 + Grafana 整合）、Parca（CNCF）、GCP Cloud Profiler、Azure Application Insights Profiler、AWS CodeGuru Profiler。production 持續取樣 CPU / heap / lock、overhead 通常 &lt; 1%。</p>
<p><strong>Distributed tracing</strong>：OpenTelemetry、Jaeger、Tempo、AWS X-Ray、GCP Cloud Trace、Azure Application Insights。記錄 request 在每個 service / 每個 stage 花了多少時間、找跨服務的 latency 累積。</p>
<p><strong>Flame graph</strong>：profile 結果視覺化的標準。從寬度可以看到「哪段 code 佔 CPU 最多」。學會看 flame graph 是 SRE 的基本功。</p>
<p><strong>Profile diff</strong>：壓測 baseline 跟 release candidate 比 stack 差異。看 <em>相對變化</em> 而非絕對值。詳見 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff 卡片</a>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora storage / compute 分離</a> — DB 統一後 application profile 變單純、退化來源更容易識別。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling 卡片</a>。</p>
<h2 id="跨層依賴鏈">跨層依賴鏈</h2>
<p>瓶頸不一定在 <em>本服務</em>、可能在 <em>下游服務</em>。這層判斷常被忽略。</p>
<p><strong>第三方 API quota</strong> 是常見隱性瓶頸。Twilio SMS、Stripe API、Slack webhook、Sendgrid email、Google Maps API 都有 rate limit。自家服務看起來健康、實際是 <em>對方 throttle</em>、自家 retry 再讓對方更慢。</p>
<p><strong>跨 region / 跨 zone 網路延遲</strong> 是累積的。一個 user request 經過 5 個 service、每個 service 跨 AZ 一次、累積 10-20ms cross-AZ latency。看起來每個 service 都很快、但 end-to-end 慢。</p>
<p><strong>Downstream cache</strong> 也是依賴。app 看起來健康、但其實是 cache 在擋；cache 突然 cold start（restart、eviction storm）、application 直接被打爆。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 行動支付</a> — DynamoDB 寫入可以撐 3K msg/sec、但 APNs / FCM 一天的 quota 有限、推送下游才是瓶頸。</p>
<h2 id="可分散-vs-不可分散瓶頸">可分散 vs 不可分散瓶頸</h2>
<p>定位完瓶頸後、要判斷它 <em>可不可以橫向擴</em>。這個判斷決定能不能用「加機器」解決。</p>
<p><strong>可分散瓶頸</strong>：</p>
<ul>
<li>stateless app server → 加機器有用</li>
<li>partitioned KV / OLTP（partition key 均勻時）→ 加 partition 有用</li>
<li>read replica（read-heavy workload）→ 加 replica 有用</li>
<li>worker pool → 加 worker 有用</li>
</ul>
<p><strong>不可分散瓶頸</strong>：</p>
<ul>
<li>consensus DB（RAFT / Paxos）→ 加節點不一定快（<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> overhead）</li>
<li>single leader DB（master 寫）→ 必須垂直擴</li>
<li>中央 coordinator → 必須拆解或垂直擴</li>
<li>共享 cache（hot key）→ 必須改 partition key 或加 local cache</li>
</ul>
<p>判斷不可分散的關鍵是「協調成本」。一個操作必須 <em>跟所有 / 多數節點協調</em> 才能完成、就不可水平擴。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase RAFT consensus</a> — consensus 不可水平擴、所以 <em>選擇不擴</em>、改用單機極致；<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner TrueTime</a> — TrueTime 把協調成本 amortize 到 hardware（GPS + 原子鐘）、讓 OLTP 可水平擴。</p>
<h2 id="常見定位陷阱">常見定位陷阱</h2>
<p><strong>看單一指標就下結論</strong>：CPU 100% 不一定是 bottleneck（可能 saturation queue 空）；CPU 50% 不一定健康（可能 saturation queue 已滿）。always 看 USE 三個維度。</p>
<p><strong>平均看 OK、p99 看不出來</strong>：average latency 50ms 看起來健康、p99 500ms 已經出事。用 percentile、不用 average。</p>
<p><strong>Observer effect</strong>：profile / tracing 本身有 overhead、量測會輕微影響系統。critical path 上的 instrumentation 要 sampled 不要 100%。</p>
<p><strong>跨 release 比較 baseline 沒對齊</strong>：上週的 baseline 對應 v1.2、這週的 candidate 對應 v1.3、但 v1.2 跟 v1.3 之間還有 schema migration / hardware 變化、baseline 已經漂移。重新建 baseline 再 diff。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>connection limit 是 RDB 隱性 bottleneck</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 付款層獨立</a></td>
          <td>關鍵路徑切分避免 cross contamination</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase RAFT consensus</a></td>
          <td>不可分散 bottleneck</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>下游 APNs / FCM quota 瓶頸</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>（針對 bottleneck 規劃）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a>（用 profile diff 改進）</li>
<li>下游：<a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a>（DB 層 bottleneck 多半在 query 寫法）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> / <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
</ul>
]]></content:encoded></item><item><title>9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/</guid><description>&lt;p>這個案例的核心責任是提供「key-value 持續高吞吐」的極限參考點。廣告事件量測屬 &lt;em>write-heavy + read-heavy 同時存在&lt;/em> 的負載 — 每個曝光都要寫進度、每個曝光也都要查 metadata。這類負載沒有明顯峰谷、是長期 sustained growth、跟事件型峰值的容量設計邏輯不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Amazon Ads 在 DynamoDB 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers&lt;/a>）：&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>讀吞吐&lt;/td>
 &lt;td>9000 萬 reads / 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫吞吐&lt;/td>
 &lt;td>500 萬 writes / 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性&lt;/td>
 &lt;td>99.999%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用途&lt;/td>
 &lt;td>廣告事件量測&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀寫比約 18:1。這個比例反映「曝光發生 1 次、後續查詢可能發生 18 次」的廣告計費邏輯。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這個案例最重要的不是「DynamoDB 能撐多少」、而是「為什麼可以這樣設計」。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>單表分散到上千個 partition&lt;/strong>：DynamoDB 把每個 table 拆成多個 partition、每個 partition 內部還可以再分散。9000 萬 reads / 秒 是上千個 partition 加總的結果、單一節點達不到這個量級。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 的 sharding 邊界、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 partition 設計。&lt;/li>
&lt;li>&lt;strong>partition key 選擇直接決定容量上限&lt;/strong>：DynamoDB 的容量是「每 partition 上限 × partition 數量」。partition key 不均勻會出現 hot partition、實際容量遠低於名義容量。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 saturation 不一定是整體 saturation、而是 &lt;em>最熱的 partition&lt;/em> saturation。&lt;/li>
&lt;li>&lt;strong>99.999% availability ≈ 5 分鐘 / 年的容錯&lt;/strong>：廣告計費 1 分鐘斷線可能損失幾百萬美金廣告收入。這個 SLO 不是行銷數字、是真實的營收邊界。對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「9000 萬 reads / 秒」這種敘述通常是 &lt;em>年度峰值的最高一秒&lt;/em>、不是平均值。容量規劃要區分「最大瞬時」、「99 百分位平均」、「常態流量」三個不同口徑。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>partition key 設計是 KV 容量的第一決策&lt;/strong>：均勻分散、避免 hot partition、必要時加 random suffix 強制分散。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 schema design 章節。&lt;/li>
&lt;li>&lt;strong>read-heavy 跟 write-heavy 比例變化是容量警訊&lt;/strong>：當業務邏輯改變（例如新增即時報表）、讀寫比可能跳一個量級、原本的容量規劃會失效。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性&lt;/a> 持續監控比例變化。&lt;/li>
&lt;li>&lt;strong>on-demand vs provisioned 是成本 vs 反應速度的取捨&lt;/strong>：on-demand 自動擴容但成本高、provisioned 便宜但需要預測。Amazon Ads 這種 sustained workload 通常用 provisioned + auto scaling、不用 on-demand。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP Cloud Bigtable + 良好 row key 設計、Azure Cosmos DB partition key 設計都是對等概念。差異是 DynamoDB 的 partition 透明度（你看不到 partition 數量）vs Bigtable 的明確 tablet 模型。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「key-value 持續高吞吐」的極限參考點。廣告事件量測屬 <em>write-heavy + read-heavy 同時存在</em> 的負載 — 每個曝光都要寫進度、每個曝光也都要查 metadata。這類負載沒有明顯峰谷、是長期 sustained growth、跟事件型峰值的容量設計邏輯不同。</p>
<h2 id="觀察">觀察</h2>
<p>Amazon Ads 在 DynamoDB 的關鍵數字（引自 <a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀吞吐</td>
          <td>9000 萬 reads / 秒</td>
      </tr>
      <tr>
          <td>寫吞吐</td>
          <td>500 萬 writes / 秒</td>
      </tr>
      <tr>
          <td>可用性</td>
          <td>99.999%</td>
      </tr>
      <tr>
          <td>用途</td>
          <td>廣告事件量測</td>
      </tr>
  </tbody>
</table>
<p>讀寫比約 18:1。這個比例反映「曝光發生 1 次、後續查詢可能發生 18 次」的廣告計費邏輯。</p>
<h2 id="判讀">判讀</h2>
<p>這個案例最重要的不是「DynamoDB 能撐多少」、而是「為什麼可以這樣設計」。</p>
<ol>
<li><strong>單表分散到上千個 partition</strong>：DynamoDB 把每個 table 拆成多個 partition、每個 partition 內部還可以再分散。9000 萬 reads / 秒 是上千個 partition 加總的結果、單一節點達不到這個量級。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的 sharding 邊界、跟 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 partition 設計。</li>
<li><strong>partition key 選擇直接決定容量上限</strong>：DynamoDB 的容量是「每 partition 上限 × partition 數量」。partition key 不均勻會出現 hot partition、實際容量遠低於名義容量。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 saturation 不一定是整體 saturation、而是 <em>最熱的 partition</em> saturation。</li>
<li><strong>99.999% availability ≈ 5 分鐘 / 年的容錯</strong>：廣告計費 1 分鐘斷線可能損失幾百萬美金廣告收入。這個 SLO 不是行銷數字、是真實的營收邊界。對應 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a> 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a>。</li>
</ol>
<p>需要警惕：「9000 萬 reads / 秒」這種敘述通常是 <em>年度峰值的最高一秒</em>、不是平均值。容量規劃要區分「最大瞬時」、「99 百分位平均」、「常態流量」三個不同口徑。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>partition key 設計是 KV 容量的第一決策</strong>：均勻分散、避免 hot partition、必要時加 random suffix 強制分散。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema design 章節。</li>
<li><strong>read-heavy 跟 write-heavy 比例變化是容量警訊</strong>：當業務邏輯改變（例如新增即時報表）、讀寫比可能跳一個量級、原本的容量規劃會失效。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性</a> 持續監控比例變化。</li>
<li><strong>on-demand vs provisioned 是成本 vs 反應速度的取捨</strong>：on-demand 自動擴容但成本高、provisioned 便宜但需要預測。Amazon Ads 這種 sustained workload 通常用 provisioned + auto scaling、不用 on-demand。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
</ol>
<p>跨平台等效：GCP Cloud Bigtable + 良好 row key 設計、Azure Cosmos DB partition key 設計都是對等概念。差異是 DynamoDB 的 partition 透明度（你看不到 partition 數量）vs Bigtable 的明確 tablet 模型。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 KV 高吞吐架構 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想避免 hot partition → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a></li>
<li>想對照其他 KV 案例 → <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth Cosmos DB</a>（Azure 全球分散）</li>
<li>想深入 DynamoDB hot partition 反模式 → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a></li>
<li>想拆 access pattern 對應的 single-table design → <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a></li>
<li>想評估 on-demand vs provisioned 切換時機 → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/handle-traffic-spikes-with-amazon-dynamodb-provisioned-capacity/">Handle traffic spikes with Amazon DynamoDB provisioned capacity</a></li>
<li><a href="https://aws.amazon.com/blogs/database/demystifying-amazon-dynamodb-on-demand-capacity-mode/">Demystifying Amazon DynamoDB on-demand capacity mode</a></li>
</ul>
]]></content:encoded></item><item><title>2.C5 Shopify：Write-through Cache 在高讀流量的實作</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/</guid><description>&lt;p>這個案例的核心責任是把快取從被動補貨模式，轉成資料寫入時即同步更新的模式。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Shopify 在高讀取路徑以 write-through 策略降低 miss 風險，改善熱門資料讀取穩定性。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 cache miss 成本過高且資料更新可控時，write-through 能降低讀路徑抖動。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把寫入流程與快取更新綁定。&lt;/li>
&lt;li>對失敗寫入設計補償與重試。&lt;/li>
&lt;li>用 hit rate 與 stale rate 檢驗策略收益。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">How Shop App uses write-through caching&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把快取從被動補貨模式，轉成資料寫入時即同步更新的模式。</p>
<h2 id="觀察">觀察</h2>
<p>Shopify 在高讀取路徑以 write-through 策略降低 miss 風險，改善熱門資料讀取穩定性。</p>
<h2 id="判讀">判讀</h2>
<p>當 cache miss 成本過高且資料更新可控時，write-through 能降低讀路徑抖動。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把寫入流程與快取更新綁定。</li>
<li>對失敗寫入設計補償與重試。</li>
<li>用 hit rate 與 stale rate 檢驗策略收益。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">How Shop App uses write-through caching</a></li>
</ul>
]]></content:encoded></item><item><title>3.C5 Slack：Job Queue 演進到 Kafka + Redis</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/</guid><description>&lt;p>這個案例的核心責任是說明工作佇列轉換常是拓樸重整，而不是單點替換。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Slack 在 job queue 擴展中使用 Kafka 與 Redis 分工，處理吞吐與即時性需求。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當背景工作同時要高吞吐與快速反應，單一通道模型通常會變成瓶頸。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把不同工作類型切到不同傳遞路徑。&lt;/li>
&lt;li>分別治理持久性與即時性目標。&lt;/li>
&lt;li>以 lag、重試與失敗重播驗證穩定性。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://slack.engineering/scaling-slacks-job-queue/">Scaling Slack&amp;rsquo;s Job Queue&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明工作佇列轉換常是拓樸重整，而不是單點替換。</p>
<h2 id="觀察">觀察</h2>
<p>Slack 在 job queue 擴展中使用 Kafka 與 Redis 分工，處理吞吐與即時性需求。</p>
<h2 id="判讀">判讀</h2>
<p>當背景工作同時要高吞吐與快速反應，單一通道模型通常會變成瓶頸。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把不同工作類型切到不同傳遞路徑。</li>
<li>分別治理持久性與即時性目標。</li>
<li>以 lag、重試與失敗重播驗證穩定性。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a> 與 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://slack.engineering/scaling-slacks-job-queue/">Scaling Slack&rsquo;s Job Queue</a></li>
</ul>
]]></content:encoded></item><item><title>4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/</guid><description>&lt;p>這個案例的核心責任是說明 observability 平台轉換常來自資料通道標準化需求。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Google Cloud 在 Cloud Trace 提供 OTLP 支援，降低應用程式對特定傳輸介面的綁定。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當團隊要跨多環境與多工具，標準化傳輸協定能減少重複 instrumentation 與遷移摩擦。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>將 collector 與 in-process exporter 對齊 OTLP。&lt;/li>
&lt;li>把 trace schema 與 sampling 規則集中治理。&lt;/li>
&lt;li>在遷移期保留舊通道與新通道比對。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/management-tools/opentelemetry-now-in-google-cloud-observability">OTLP in Google Cloud Observability&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 observability 平台轉換常來自資料通道標準化需求。</p>
<h2 id="觀察">觀察</h2>
<p>Google Cloud 在 Cloud Trace 提供 OTLP 支援，降低應用程式對特定傳輸介面的綁定。</p>
<h2 id="判讀">判讀</h2>
<p>當團隊要跨多環境與多工具，標準化傳輸協定能減少重複 instrumentation 與遷移摩擦。</p>
<h2 id="策略">策略</h2>
<ol>
<li>將 collector 與 in-process exporter 對齊 OTLP。</li>
<li>把 trace schema 與 sampling 規則集中治理。</li>
<li>在遷移期保留舊通道與新通道比對。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<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> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/management-tools/opentelemetry-now-in-google-cloud-observability">OTLP in Google Cloud Observability</a></li>
</ul>
]]></content:encoded></item><item><title>5.C5 Miro：Managed EKS 遷移</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/</guid><description>&lt;p>這個案例的核心責任是說明平台遷移也會改變團隊職責分工。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Miro 從自維運 Kubernetes 遷移到 managed EKS。遷移前的狀態是平台團隊大部分精力花在叢集本身的運維——control plane 升級、node AMI 維護、etcd 備份、安全修補。這些工作是必要的，但它們跟「讓開發者更快交付功能」沒有直接關聯。&lt;/p>
&lt;p>遷移後 managed EKS 接管了 control plane 運維。平台團隊的工作重心從「維持叢集跑起來」轉向「定義 release flow、observability convention、developer experience」。這個轉變是 managed 平台的組織層面價值，技術層面的價值（省維運、自動升級）反而是次要的。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略。這個判讀成立的前提是組織主動重新定義職責邊界——managed 平台不會自動帶來組織轉型，它只是移除了一類維運負擔。如果平台團隊在遷移後沒有重新定義職責，很容易繼續用舊模式工作（只是工作量少了），錯失把省下的精力轉到更高價值工作的機會。&lt;/p>
&lt;p>另一個判讀是 managed 平台引入新的 grey zone。control plane 由供應商管理，但 cluster-internal 元件（CNI、ingress controller、service mesh、cluster DNS）的 ownership 需要顯式界定。Miro 的經驗顯示這些 grey zone 若不在 day-1 處理，後續會在事故時暴露——「以為供應商在管」跟「供應商認為客戶在管」的認知差距，會讓故障排查繞圈。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>先定義遷移後的平台責任邊界&lt;/strong>：列出四層責任矩陣——cluster 層（供應商管）、cluster-internal 層（platform team 管）、application 層（service team 管）、跨層議題（協作）。每層有明確 owner，避免 grey zone。責任矩陣的詳細結構見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界&lt;/a>。&lt;/li>
&lt;li>&lt;strong>以自動化流程取代手動平台操作&lt;/strong>：遷移前的手動操作（node 升級、cert rotation、backup restore）在 managed 平台上由供應商或 IaC 接管。剩餘的手動操作（namespace provisioning、resource quota 設定、network policy review）也要自動化或流程化，避免依賴個人經驗。&lt;/li>
&lt;li>&lt;strong>將 incident 與 release policy 接回平台治理&lt;/strong>：managed 平台的 incident 跟 self-managed 不同——control plane 故障由供應商處理，但供應商的 incident 訊號要進入自家的 incident timeline。release policy（升級節奏、canary 比例、rollback 條件）在 managed 平台上仍是 platform team 的責任。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>從 managed 回退到 self-managed 的成本極高（要重建 control plane 運維能力），因此這類遷移的回退策略通常是「在 managed 平台內回退」而非「回到 self-managed」。具體做法是保留舊叢集一段時間作為 fallback，但同時接受「回到 self-managed 不是選項」的設計假設。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime&lt;/a> 看遷移後 runtime 層的變化驗證。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 managed 平台與職責邊界&lt;/a> 看職責矩陣的完整結構。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模&lt;/a> 看遷移期攻擊面變動。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/miro-amazon-eks/">Miro on AWS containers and EKS&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明平台遷移也會改變團隊職責分工。</p>
<h2 id="觀察">觀察</h2>
<p>Miro 從自維運 Kubernetes 遷移到 managed EKS。遷移前的狀態是平台團隊大部分精力花在叢集本身的運維——control plane 升級、node AMI 維護、etcd 備份、安全修補。這些工作是必要的，但它們跟「讓開發者更快交付功能」沒有直接關聯。</p>
<p>遷移後 managed EKS 接管了 control plane 運維。平台團隊的工作重心從「維持叢集跑起來」轉向「定義 release flow、observability convention、developer experience」。這個轉變是 managed 平台的組織層面價值，技術層面的價值（省維運、自動升級）反而是次要的。</p>
<h2 id="判讀">判讀</h2>
<p>平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略。這個判讀成立的前提是組織主動重新定義職責邊界——managed 平台不會自動帶來組織轉型，它只是移除了一類維運負擔。如果平台團隊在遷移後沒有重新定義職責，很容易繼續用舊模式工作（只是工作量少了），錯失把省下的精力轉到更高價值工作的機會。</p>
<p>另一個判讀是 managed 平台引入新的 grey zone。control plane 由供應商管理，但 cluster-internal 元件（CNI、ingress controller、service mesh、cluster DNS）的 ownership 需要顯式界定。Miro 的經驗顯示這些 grey zone 若不在 day-1 處理，後續會在事故時暴露——「以為供應商在管」跟「供應商認為客戶在管」的認知差距，會讓故障排查繞圈。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>先定義遷移後的平台責任邊界</strong>：列出四層責任矩陣——cluster 層（供應商管）、cluster-internal 層（platform team 管）、application 層（service team 管）、跨層議題（協作）。每層有明確 owner，避免 grey zone。責任矩陣的詳細結構見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>。</li>
<li><strong>以自動化流程取代手動平台操作</strong>：遷移前的手動操作（node 升級、cert rotation、backup restore）在 managed 平台上由供應商或 IaC 接管。剩餘的手動操作（namespace provisioning、resource quota 設定、network policy review）也要自動化或流程化，避免依賴個人經驗。</li>
<li><strong>將 incident 與 release policy 接回平台治理</strong>：managed 平台的 incident 跟 self-managed 不同——control plane 故障由供應商處理，但供應商的 incident 訊號要進入自家的 incident timeline。release policy（升級節奏、canary 比例、rollback 條件）在 managed 平台上仍是 platform team 的責任。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>從 managed 回退到 self-managed 的成本極高（要重建 control plane 運維能力），因此這類遷移的回退策略通常是「在 managed 平台內回退」而非「回到 self-managed」。具體做法是保留舊叢集一段時間作為 fallback，但同時接受「回到 self-managed 不是選項」的設計假設。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a> 看遷移後 runtime 層的變化驗證。回 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 managed 平台與職責邊界</a> 看職責矩陣的完整結構。回 <a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模</a> 看遷移期攻擊面變動。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/miro-amazon-eks/">Miro on AWS containers and EKS</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>7.C5 Okta：2023 Support System 事件</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/</guid><description>&lt;p>這個案例的核心責任是提醒控制面不只在正式生產系統，也在支援工具鏈。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Okta 2023 事件顯示支援系統若涉及高權限資料與工作流程，會成為跨租戶風險放大點。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>身份與授權治理若只覆蓋產品面，忽略支援流程，仍會留下高影響面缺口。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把 support tooling 納入同等級身份治理。&lt;/li>
&lt;li>補強 session、token 與操作留痕控制。&lt;/li>
&lt;li>將異常支援活動接入告警與 incident 路由。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity/access boundary&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection coverage&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sec.okta.com/harfiles">Okta support system case update&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是提醒控制面不只在正式生產系統，也在支援工具鏈。</p>
<h2 id="觀察">觀察</h2>
<p>Okta 2023 事件顯示支援系統若涉及高權限資料與工作流程，會成為跨租戶風險放大點。</p>
<h2 id="判讀">判讀</h2>
<p>身份與授權治理若只覆蓋產品面，忽略支援流程，仍會留下高影響面缺口。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把 support tooling 納入同等級身份治理。</li>
<li>補強 session、token 與操作留痕控制。</li>
<li>將異常支援活動接入告警與 incident 路由。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity/access boundary</a> 與 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection coverage</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sec.okta.com/harfiles">Okta support system case update</a></li>
</ul>
]]></content:encoded></item><item><title>Apache JMeter</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/</guid><description>&lt;p>JMeter 是 Apache 出品的老牌 load test 工具、承擔三個責任：GUI-driven test plan 設計、多 protocol sampler（HTTP / JDBC / JMS / FTP / mail）、plugins 生態廣 + 企業環境普及。設計取捨偏向「GUI 易上手 + 既有測試資產治理 + 多 protocol」、跟 code-first（k6 / Gatling）的取捨在 dev workflow 跟 version control 友善度。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 GUI 設計 test plan（thread group / sampler / listener / assertion）&lt;/li>
&lt;li>跑 non-GUI mode 給 CI&lt;/li>
&lt;li>用 Distributed mode（master / slave）擴張 VU&lt;/li>
&lt;li>用 JMeter Plugins Manager 加擴展&lt;/li>
&lt;li>評估 JMeter vs 現代 CLI-first（k6 / Gatling / Locust）的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-jmeter-跑起來">最短路徑：5 分鐘把 JMeter 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: brew install jmeter / 下載 zip&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="c1"># 2. GUI 設計 .jmx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 開 jmeter GUI、加 Thread Group / HTTP Sampler / Listener&lt;/span>
&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="c1"># 3. CI 跑 non-GUI mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: jmeter -n -t test.jmx -l result.jtl -e -o report/&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="test-plan-結構">Test plan 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Thread Group（VU + ramp-up + loop count）&lt;/li>
&lt;li>Sampler（HTTP / JDBC / JMS / FTP / Java Request）&lt;/li>
&lt;li>Listener（aggregate report / view tree / graph）&lt;/li>
&lt;li>Assertion（response / duration / size）&lt;/li>
&lt;/ul>
&lt;h3 id="non-gui-mode-for-ci">Non-GUI mode for CI&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>-n&lt;/code> non-GUI&lt;/li>
&lt;li>&lt;code>-t&lt;/code> test file / &lt;code>-l&lt;/code> log file&lt;/li>
&lt;li>&lt;code>-e -o&lt;/code> 產生 HTML dashboard&lt;/li>
&lt;li>Exit code 0 / 1（搭配 backend listener / assertion）&lt;/li>
&lt;/ul>
&lt;h3 id="distributed-testing">Distributed testing&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Master / slave 配置&lt;/li>
&lt;li>RMI port 設定&lt;/li>
&lt;li>Result aggregation 在 master&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="plugins-manager">Plugins Manager&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>JMeter 是 Apache 出品的老牌 load test 工具、承擔三個責任：GUI-driven test plan 設計、多 protocol sampler（HTTP / JDBC / JMS / FTP / mail）、plugins 生態廣 + 企業環境普及。設計取捨偏向「GUI 易上手 + 既有測試資產治理 + 多 protocol」、跟 code-first（k6 / Gatling）的取捨在 dev workflow 跟 version control 友善度。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 GUI 設計 test plan（thread group / sampler / listener / assertion）</li>
<li>跑 non-GUI mode 給 CI</li>
<li>用 Distributed mode（master / slave）擴張 VU</li>
<li>用 JMeter Plugins Manager 加擴展</li>
<li>評估 JMeter vs 現代 CLI-first（k6 / Gatling / Locust）的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-jmeter-跑起來">最短路徑：5 分鐘把 JMeter 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: brew install jmeter / 下載 zip</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="c1"># 2. GUI 設計 .jmx</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: 開 jmeter GUI、加 Thread Group / HTTP Sampler / Listener</span>
</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="c1"># 3. CI 跑 non-GUI mode</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: jmeter -n -t test.jmx -l result.jtl -e -o report/</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="test-plan-結構">Test plan 結構</h3>
<p>子議題：</p>
<ul>
<li>Thread Group（VU + ramp-up + loop count）</li>
<li>Sampler（HTTP / JDBC / JMS / FTP / Java Request）</li>
<li>Listener（aggregate report / view tree / graph）</li>
<li>Assertion（response / duration / size）</li>
</ul>
<h3 id="non-gui-mode-for-ci">Non-GUI mode for CI</h3>
<p>子議題：</p>
<ul>
<li><code>-n</code> non-GUI</li>
<li><code>-t</code> test file / <code>-l</code> log file</li>
<li><code>-e -o</code> 產生 HTML dashboard</li>
<li>Exit code 0 / 1（搭配 backend listener / assertion）</li>
</ul>
<h3 id="distributed-testing">Distributed testing</h3>
<p>子議題：</p>
<ul>
<li>Master / slave 配置</li>
<li>RMI port 設定</li>
<li>Result aggregation 在 master</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="plugins-manager">Plugins Manager</h3>
<p>子議題：</p>
<ul>
<li>jmeter-plugins.org plugins</li>
<li>常用：PerfMon / Dummy Sampler / Custom Thread Groups / WebSocket</li>
<li>安裝管理：Plugins Manager 安裝後可 UI 管</li>
</ul>
<h3 id="recording-controller">Recording controller</h3>
<p>子議題：</p>
<ul>
<li>HTTP(S) Test Script Recorder</li>
<li>Browser proxy 設定</li>
<li>適合：快速錄製 user flow</li>
</ul>
<h3 id="csv-data-set--parameterization">CSV data set / parameterization</h3>
<p>子議題：</p>
<ul>
<li>CSV Data Set Config</li>
<li>各 thread 取不同資料</li>
<li>適合 data-driven test</li>
</ul>
<h3 id="ci--jenkins-integration">CI / Jenkins integration</h3>
<p>子議題：</p>
<ul>
<li>Jenkins JMeter plugin</li>
<li>Performance plugin（trend analysis）</li>
<li>對應 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
</ul>
<h3 id="既有-jmx-資產治理">既有 .jmx 資產治理</h3>
<p>子議題：</p>
<ul>
<li>XML 不友善 git diff</li>
<li>大 test plan 可讀性差</li>
<li>改用 module 拆 + Test Fragment</li>
<li>對應企業遷移到 k6 / Gatling 評估</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="high-vu-起不來">High VU 起不來</h3>
<p>操作原則：JVM heap 不夠 / GUI 模式有限制（永遠 non-GUI for production load）。</p>
<h3 id="listener-拖慢">Listener 拖慢</h3>
<p>操作原則：View Results Tree 記錄太多 → 改 simple data writer / disable detail。</p>
<h3 id="distributed-rmi-連不上">Distributed RMI 連不上</h3>
<p>操作原則：firewall + RMI port 不對。</p>
<h3 id="assertion-noise">Assertion noise</h3>
<p>操作原則：assertion failed 多但實際 OK → response time / size 設過嚴。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Code-first / CI-first</td>
          <td><a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a> / <a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></td>
      </tr>
      <tr>
          <td>Python</td>
          <td><a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a></td>
      </tr>
      <tr>
          <td>Cloud managed</td>
          <td>BlazeMeter / Octoperf / Tricentis NeoLoad</td>
      </tr>
      <tr>
          <td>Browser flow</td>
          <td>Playwright / Cypress / k6 browser</td>
      </tr>
      <tr>
          <td>Capacity planning</td>
          <td><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 plugins 列表</li>
<li>BeanShell / Groovy scripting</li>
<li>JMeter internal architecture</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity 與 On-call 分層</a></td>
          <td>企業內部 load test pipeline + headroom 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>峰值前 load test scenario 與 capacity baseline 的對照組</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 JMeter customer case</strong>：企業內部 JMeter 大規模採用案例、JMeter → k6 遷移案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a>、<a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></li>
<li>下游能力：<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a></li>
</ul>
]]></content:encoded></item><item><title>Atlassian</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/</guid><description>&lt;p>Atlassian 2022 的 14 天事故是多租戶誤刪 + 跨團隊協作的教學標竿。事故 post-mortem 公開度極高、揭露 IR 內部運作細節（incident commander 輪值、跨團隊溝通、客戶補償政策），是少數能完整看到大型事故 IR 流程的公開素材。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>多租戶資料模型：跨產品 tenant ID 的 cascading delete 風險&lt;/li>
&lt;li>Recovery 順序：885 個 tenants 為何不能平行恢復、需要排序&lt;/li>
&lt;li>跨團隊協作：incident commander 輪值、24x7 支援、客戶溝通分軌&lt;/li>
&lt;li>Stakeholder 通訊：customer impact 量化、補償政策、合約衝擊&lt;/li>
&lt;li>Postmortem 文化：Atlassian Incident Management Handbook 公開內容&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2022&lt;/td>
 &lt;td>14 天多租戶誤刪&lt;/td>
 &lt;td>大規模 IR 協作、長尾 recovery、客戶溝通&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2023&lt;/td>
 &lt;td>較小規模事故&lt;/td>
 &lt;td>對比 14 天事故的 IR 流程演化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Atlassian 這個案例在講的是多租戶 SaaS 在發生誤刪後，復原與對外通訊如何一起構成事故本體。讀者先看懂 PIR、status update 與 restore path 的責任，再把 2022 事件當成跨團隊協作與復原節奏的範例。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當事故牽涉到客戶資料或多個內部系統時，復原速度取決於能否把依賴關係一層一層還原。當事故持續時間拉長時，對外更新的節奏也要固定，讓客戶能知道哪些功能先恢復、哪些風險仍在。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否把誤刪後的復原步驟寫成明確順序&lt;/li>
&lt;li>能否把 status update 與內部復原節奏對齊&lt;/li>
&lt;li>能否說明哪些服務先恢復、哪些依賴後恢復&lt;/li>
&lt;li>能否在 PIR 中把流程缺口轉成可追蹤的改善項&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Atlassian 和 Microsoft 365 都在講企業 SaaS 的客戶通訊問題，但 Atlassian 更像是把復原流程完整攤在桌上。它也適合和 GitHub 一起看，因為兩者都能說明長時間事故裡，時間線、責任與客戶影響如何一起被管理。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2022 年 14 天 outage 代表多租戶誤刪後的長尾復原。&lt;/li>
&lt;li>PIR 與對外 update 的節奏，讓客戶能知道哪些服務先回來。&lt;/li>
&lt;li>incident commander 輪值與跨團隊協作是這類事故的核心樣本。&lt;/li>
&lt;li>補償政策與客戶溝通會直接影響事故收斂速度。&lt;/li>
&lt;li>885 個 tenants 的排序恢復讓復原順序本身成為事故管理的一部分。&lt;/li>
&lt;li>customer impact quantification 讓補償與優先恢復有可執行依據。&lt;/li>
&lt;li>multi-tenant data model 讓單一誤刪能直接跨產品擴散。&lt;/li>
&lt;li>stakeholder communication 會和技術復原一起構成事故處理流程。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/post-incident-review-april-2022-outage">Post-Incident Review on the Atlassian April 2022 outage&lt;/a>：Atlassian 2022 年大規模誤刪事件的完整 PIR。&lt;/li>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/april-2022-outage-update">Update on the Atlassian outage affecting some customers&lt;/a>：對外更新版本，適合對照復原節奏。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Atlassian 2022 的 14 天事故是多租戶誤刪 + 跨團隊協作的教學標竿。事故 post-mortem 公開度極高、揭露 IR 內部運作細節（incident commander 輪值、跨團隊溝通、客戶補償政策），是少數能完整看到大型事故 IR 流程的公開素材。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>多租戶資料模型：跨產品 tenant ID 的 cascading delete 風險</li>
<li>Recovery 順序：885 個 tenants 為何不能平行恢復、需要排序</li>
<li>跨團隊協作：incident commander 輪值、24x7 支援、客戶溝通分軌</li>
<li>Stakeholder 通訊：customer impact 量化、補償政策、合約衝擊</li>
<li>Postmortem 文化：Atlassian Incident Management Handbook 公開內容</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2022</td>
          <td>14 天多租戶誤刪</td>
          <td>大規模 IR 協作、長尾 recovery、客戶溝通</td>
      </tr>
      <tr>
          <td>2023</td>
          <td>較小規模事故</td>
          <td>對比 14 天事故的 IR 流程演化</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>Atlassian 這個案例在講的是多租戶 SaaS 在發生誤刪後，復原與對外通訊如何一起構成事故本體。讀者先看懂 PIR、status update 與 restore path 的責任，再把 2022 事件當成跨團隊協作與復原節奏的範例。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當事故牽涉到客戶資料或多個內部系統時，復原速度取決於能否把依賴關係一層一層還原。當事故持續時間拉長時，對外更新的節奏也要固定，讓客戶能知道哪些功能先恢復、哪些風險仍在。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否把誤刪後的復原步驟寫成明確順序</li>
<li>能否把 status update 與內部復原節奏對齊</li>
<li>能否說明哪些服務先恢復、哪些依賴後恢復</li>
<li>能否在 PIR 中把流程缺口轉成可追蹤的改善項</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Atlassian 和 Microsoft 365 都在講企業 SaaS 的客戶通訊問題，但 Atlassian 更像是把復原流程完整攤在桌上。它也適合和 GitHub 一起看，因為兩者都能說明長時間事故裡，時間線、責任與客戶影響如何一起被管理。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2022 年 14 天 outage 代表多租戶誤刪後的長尾復原。</li>
<li>PIR 與對外 update 的節奏，讓客戶能知道哪些服務先回來。</li>
<li>incident commander 輪值與跨團隊協作是這類事故的核心樣本。</li>
<li>補償政策與客戶溝通會直接影響事故收斂速度。</li>
<li>885 個 tenants 的排序恢復讓復原順序本身成為事故管理的一部分。</li>
<li>customer impact quantification 讓補償與優先恢復有可執行依據。</li>
<li>multi-tenant data model 讓單一誤刪能直接跨產品擴散。</li>
<li>stakeholder communication 會和技術復原一起構成事故處理流程。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/post-incident-review-april-2022-outage">Post-Incident Review on the Atlassian April 2022 outage</a>：Atlassian 2022 年大規模誤刪事件的完整 PIR。</li>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/april-2022-outage-update">Update on the Atlassian outage affecting some customers</a>：對外更新版本，適合對照復原節奏。</li>
</ul>
]]></content:encoded></item><item><title>AWS ElastiCache</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/</guid><description>&lt;p>AWS ElastiCache 是 AWS managed cache 服務、承擔三個責任：託管 Redis / Valkey / Memcached engine（無需自管 broker）、自動 failover + 跨 AZ 複製、AWS 生態原生整合（IAM / VPC / CloudWatch / KMS）。設計取捨偏向「把運維責任轉給 AWS、付 managed premium 換可預測 SLA」、AWS 生態下的 cache 預設選擇。2024 起 default engine 從 Redis 改為 Valkey（成本約低 20%）。&lt;/p>
&lt;p>對「AWS 生態服務需要 cache、不想自管 Redis cluster、跨 AZ 高可用」這條路徑、ElastiCache 是首選。本頁先給最短路徑、再展開日常 cluster 管理跟 engine 選擇、最後進階治理（Serverless、MemoryDB 對照）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 AWS CLI 建立 ElastiCache cluster、選擇 engine（Redis / Valkey / Memcached）&lt;/li>
&lt;li>區分 Cluster mode enabled vs disabled 的選用條件&lt;/li>
&lt;li>配置 auto failover、cross-AZ replication、snapshot backup&lt;/li>
&lt;li>評估 ElastiCache Serverless vs node-based 的成本取捨&lt;/li>
&lt;li>區分 ElastiCache 跟 MemoryDB（durable）跟自管 Redis 的定位&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-elasticache-跑起來">最短路徑：5 分鐘把 ElastiCache 跑起來&lt;/h2>





&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"># 1. 建立 Valkey replication group（cluster mode disabled、單 primary + 1 replica、Multi-AZ）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">aws elasticache create-replication-group &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> --replication-group-id 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> --replication-group-description &lt;span class="s2">&amp;#34;demo cache&amp;#34;&lt;/span> &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> --engine valkey &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> --cache-node-type cache.t4g.micro &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> --num-cache-clusters &lt;span class="m">2&lt;/span> &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> --automatic-failover-enabled &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> --multi-az-enabled
&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="c1"># 2. 取得 primary endpoint（建立需數分鐘、status 變 available 才有 endpoint）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">aws elasticache describe-replication-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --replication-group-id demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s2">&amp;#34;ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address&amp;#34;&lt;/span> --output text
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 從 VPC 內（EC2 / Lambda）用 redis-cli 連線（ElastiCache 只在 VPC 內可達）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">redis-cli -h &amp;lt;primary-endpoint&amp;gt; -p &lt;span class="m">6379&lt;/span> PING &lt;span class="c1"># → PONG&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>指令依 &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/elasticache/">AWS ElastiCache CLI 官方文件&lt;/a>、最後檢查日 2026-06-16（managed 服務需 AWS 帳號與 VPC、本機無法 docker 驗證、引數以官方為準）。ElastiCache 端點只在 VPC 內可達、不對公網開放。實際 production 需要評估 cluster mode、節點大小、replica 數、AZ 分布。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="aws-cli-與-console">AWS CLI 與 console&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>AWS ElastiCache 是 AWS managed cache 服務、承擔三個責任：託管 Redis / Valkey / Memcached engine（無需自管 broker）、自動 failover + 跨 AZ 複製、AWS 生態原生整合（IAM / VPC / CloudWatch / KMS）。設計取捨偏向「把運維責任轉給 AWS、付 managed premium 換可預測 SLA」、AWS 生態下的 cache 預設選擇。2024 起 default engine 從 Redis 改為 Valkey（成本約低 20%）。</p>
<p>對「AWS 生態服務需要 cache、不想自管 Redis cluster、跨 AZ 高可用」這條路徑、ElastiCache 是首選。本頁先給最短路徑、再展開日常 cluster 管理跟 engine 選擇、最後進階治理（Serverless、MemoryDB 對照）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 AWS CLI 建立 ElastiCache cluster、選擇 engine（Redis / Valkey / Memcached）</li>
<li>區分 Cluster mode enabled vs disabled 的選用條件</li>
<li>配置 auto failover、cross-AZ replication、snapshot backup</li>
<li>評估 ElastiCache Serverless vs node-based 的成本取捨</li>
<li>區分 ElastiCache 跟 MemoryDB（durable）跟自管 Redis 的定位</li>
</ol>
<h2 id="最短路徑5-分鐘把-elasticache-跑起來">最短路徑：5 分鐘把 ElastiCache 跑起來</h2>





<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. 建立 Valkey replication group（cluster mode disabled、單 primary + 1 replica、Multi-AZ）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elasticache create-replication-group <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --replication-group-id demo <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --replication-group-description <span class="s2">&#34;demo cache&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine valkey <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --cache-node-type cache.t4g.micro <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --num-cache-clusters <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --automatic-failover-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --multi-az-enabled
</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="c1"># 2. 取得 primary endpoint（建立需數分鐘、status 變 available 才有 endpoint）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">aws elasticache describe-replication-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --replication-group-id demo <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address&#34;</span> --output text
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 3. 從 VPC 內（EC2 / Lambda）用 redis-cli 連線（ElastiCache 只在 VPC 內可達）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">redis-cli -h &lt;primary-endpoint&gt; -p <span class="m">6379</span> PING   <span class="c1"># → PONG</span></span></span></code></pre></div><p>指令依 <a href="https://docs.aws.amazon.com/cli/latest/reference/elasticache/">AWS ElastiCache CLI 官方文件</a>、最後檢查日 2026-06-16（managed 服務需 AWS 帳號與 VPC、本機無法 docker 驗證、引數以官方為準）。ElastiCache 端點只在 VPC 內可達、不對公網開放。實際 production 需要評估 cluster mode、節點大小、replica 數、AZ 分布。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="aws-cli-與-console">AWS CLI 與 console</h3>
<p>子議題：</p>
<ul>
<li>CLI 指令對照表（create-cache-cluster / create-replication-group / describe-* / modify-* / delete-*）</li>
<li>Console 操作流程（VPC subnet group / security group / parameter group）</li>
<li>Terraform / CloudFormation 範例</li>
<li>對應指令範例：<code>aws elasticache describe-replication-groups --replication-group-id &lt;id&gt;</code></li>
</ul>
<h3 id="engine-選擇">Engine 選擇</h3>
<p>子議題：</p>
<ul>
<li><strong>Valkey</strong>（2024+ default）：成本低 20%、OSI 開源、Redis 7.2.4 fork</li>
<li><strong>Redis OSS</strong>（legacy support）：仍可選、但 AWS 不推</li>
<li><strong>Memcached</strong>：純 cache 場景、無 cluster mode 概念（client-side sharding）</li>
<li>選擇判讀：新部署 → Valkey；既有 Redis 遷移 → Valkey（API 相容）；純 cache → Memcached</li>
</ul>
<h3 id="cluster-mode-enabled-vs-disabled">Cluster mode enabled vs disabled</h3>
<p>子議題：</p>
<ul>
<li><strong>Disabled</strong>：1 primary + N replica（最多 5）、單 shard、上限 ~340GB</li>
<li><strong>Enabled</strong>：多 shard（最多 500）、自動 sharding、橫向擴展</li>
<li>客戶端要求：Cluster mode enabled 需要 cluster-aware client</li>
<li>選擇判讀：&lt; 300GB + 簡單 → disabled；&gt; 300GB 或要 sharding → enabled</li>
</ul>
<h3 id="snapshot-與-backup">Snapshot 與 backup</h3>
<p>子議題：</p>
<ul>
<li>Automatic snapshot（保留 1-35 天）</li>
<li>Manual snapshot（保留永久、可跨 region 複製）</li>
<li>Restore：從 snapshot 建新 cluster</li>
<li>對應指令：<code>aws elasticache create-snapshot</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="auto-failover-機制">Auto failover 機制</h3>
<p>子議題：</p>
<ul>
<li>Multi-AZ 部署：primary 失敗、replica 自動晉升</li>
<li>Failover 時間：~30 秒到幾分鐘（依 client 重連)</li>
<li>Client 影響：DNS 切到新 primary、client 要 reconnect</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a> 跨 AZ 對照</li>
</ul>
<h3 id="elasticache-serverless">ElastiCache Serverless</h3>
<p>子議題：</p>
<ul>
<li>On-demand 模式：不選 node type、按 ECPU + storage 計費</li>
<li>自動 scale：流量增加自動擴</li>
<li>適合：流量不可預測、不想規劃容量</li>
<li>不適合：成本敏感（serverless premium）、極大 dataset</li>
</ul>
<h3 id="跨-region-replicationglobal-datastore">跨 region replication（Global Datastore）</h3>
<p>子議題：</p>
<ul>
<li>Global Datastore：1 primary region + 多個 secondary region read replica</li>
<li>跨 region replication lag &lt; 1 second（業界宣稱）</li>
<li>適合 active-passive DR</li>
<li>不支援 active-active multi-master</li>
</ul>
<h3 id="memorydb-對照">MemoryDB 對照</h3>
<p>子議題：</p>
<ul>
<li>ElastiCache：cache、Multi-AZ replica 但仍是 cache 語意（資料可重建）</li>
<li>MemoryDB：Redis-compatible durable database、multi-AZ transaction log</li>
<li>MemoryDB cost 2-3x ElastiCache、但提供 source-of-truth 語意</li>
<li>選擇判讀：要 source-of-truth Redis API → MemoryDB；cache 用途 → ElastiCache</li>
</ul>
<h3 id="parameter-group-與配置">Parameter group 與配置</h3>
<p>子議題：</p>
<ul>
<li>Parameter group：custom maxmemory-policy、timeout、client-output-buffer-limit</li>
<li>Cluster vs parameter group 的應用範圍</li>
<li>對應指令：<code>aws elasticache modify-cache-parameter-group</code></li>
</ul>
<h3 id="iam-authenticationredis-7">IAM authentication（Redis 7+）</h3>
<p>子議題：</p>
<ul>
<li>從 Redis AUTH password 升級到 IAM-based authentication</li>
<li>IAM role / user 連 ElastiCache、無需傳 password</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a></li>
</ul>
<h3 id="cost-模型">Cost 模型</h3>
<p>子議題：</p>
<ul>
<li>Node type 成本（t4g.micro 到 r7g.16xlarge）</li>
<li>Reserved Instance（1/3 年承諾、折扣 30-60%）</li>
<li>Data transfer cost（同 AZ 免費、跨 AZ 收費）</li>
<li>Snapshot storage cost</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="endpoint-連不上">Endpoint 連不上</h3>
<p>操作原則：先確認 VPC + security group + subnet group 配置正確。</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 elasticache describe-replication-groups --replication-group-id &lt;id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;ReplicationGroups[0].Status&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 從 VPC 內 EC2 測試連通性</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli -h &lt;primary-endpoint&gt; -p <span class="m">6379</span> PING</span></span></code></pre></div><p>判讀路徑：security group 沒開 6379 → VPC peering 不通 → DNS 解析失敗。</p>
<h3 id="failover-過程中-client-持續-error">Failover 過程中 client 持續 error</h3>
<p>操作原則：failover 期間 client 重連需要時間、確認 client 有 reconnect 邏輯。</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 elasticache describe-events --source-identifier &lt;id&gt; --source-type replication-group
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 看 failover 開始 / 完成事件、對照 client 重連時間軸</span></span></span></code></pre></div><h3 id="replication-lag-高">Replication lag 高</h3>
<p>操作原則：cross-AZ replication 通常 ms 級、若 &gt; 1 sec 看 CloudWatch ReplicationLag metric。原因可能是 write throughput 過高、replica node 規格不足。</p>
<h3 id="memory-pressure--eviction">Memory pressure / eviction</h3>
<p>操作原則：看 CloudWatch DatabaseMemoryUsagePercentage、超 80% 考慮 scale up node type 或調 maxmemory-policy。</p>
<h3 id="snapshot-失敗">Snapshot 失敗</h3>
<p>操作原則：snapshot 過程暫時 fork（Redis）會佔用記憶體、若 memory 已緊張可能失敗。看 CloudWatch BytesUsedForCache。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 source-of-truth Redis API</td>
          <td>AWS MemoryDB（durable Redis-compatible）</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>自管 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>極端 throughput single instance</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> self-host</td>
      </tr>
      <tr>
          <td>Edge / HTTP cache</td>
          <td>CloudFront / Cloudflare Cache（T4 候選）</td>
      </tr>
      <tr>
          <td>不在 AWS 生態</td>
          <td>GCP Memorystore / Azure Cache for Redis</td>
      </tr>
      <tr>
          <td>完全 serverless 計費</td>
          <td>ElastiCache Serverless（同模組內）/ <a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS IAM / VPC / Security Group 完整配置（見 security 模組）</li>
<li>CloudFormation / Terraform 完整模板</li>
<li>AWS pricing 詳細計算</li>
<li>ElastiCache vs Memorystore vs Azure Cache 完整對照</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 ElastiCache 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 為 Netflix 自管 Memcached based 全域 cache、對應 ElastiCache for Memcached + Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 在 managed cache 的實作、ElastiCache 提供同樣 Redis/Valkey API、無 self-host 維運負擔</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移 client-side 實作、ElastiCache 對應為 engine version upgrade + parameter group 滾動</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 ElastiCache-specific 案例</strong>：Airbnb / Lyft / Pinterest 等公開的 ElastiCache 規模化案例、re:Invent talks（如 ElastiCache for Valkey 遷移、Serverless 採用、Global Datastore active-passive DR 實作）。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 ElastiCache 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>Managed 也會 stampede、AWS 不會幫你做 client-side jitter / singleflight、需自行設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single primary / 中型 Multi-AZ replica / 大型 Cluster mode enabled + Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>ElastiCache 對應為 Cluster mode + Configuration Endpoint（client-side discovery）、無原生 protocol proxy</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>Failover / replica promotion 期間 ElastiCache 也會出現一致性議題、CloudWatch ReplicationLag 是主要訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a></td>
          <td>分層儲存對照、AWS 對應為 ElastiCache（hot）+ S3 / DynamoDB（cold）的應用層分層設計</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
</ul>
]]></content:encoded></item><item><title>AWS SQS</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/</guid><description>&lt;p>AWS SQS 是 AWS managed queue 服務、承擔三個責任：訊息排隊與重試（visibility timeout + DLQ）、解耦 producer / consumer（無 broker 運維）、AWS 生態原生整合（Lambda / EventBridge / Step Functions）。設計取捨偏向「極簡 API + managed 運維、用 visibility timeout 取代 broker ACK、無原生 ordering（standard queue）」。&lt;/p>
&lt;p>對「AWS 生態 task queue、不想自管 broker、配合 Lambda 事件處理」這條路徑、SQS 是首選。本頁先給最短路徑、再展開日常 SendMessage / ReceiveMessage 操作與 visibility timeout 設計、最後進階治理（FIFO、DLQ、IAM、VPC endpoint）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 AWS CLI 建 standard / FIFO queue、發送與接收訊息&lt;/li>
&lt;li>設計 visibility timeout 對齊 consumer 處理時間&lt;/li>
&lt;li>配置 DLQ（dead-letter queue）與 maxReceiveCount&lt;/li>
&lt;li>區分 long polling vs short polling、配合 Lambda event source mapping&lt;/li>
&lt;li>評估 IAM policy、VPC endpoint、cross-account 訪問等治理場景&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-sqs-跑起來">最短路徑：5 分鐘把 SQS 跑起來&lt;/h2>





&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"># 1. 建 queue（回傳 QueueUrl、後續操作都用它）&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-queue
&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"># 2. 發送訊息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">aws sqs send-message --queue-url &amp;lt;url&amp;gt; --message-body &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>
&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="c1"># 3. 接收訊息（long polling、最多等 20 秒）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">aws sqs receive-message --queue-url &amp;lt;url&amp;gt; --wait-time-seconds &lt;span class="m">20&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「queue 建得起來、能發能收」。實際應用配合 SDK / Lambda、見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。指令對真實 AWS 需設定 credentials 與 region；本機要先驗證可加 &lt;code>--endpoint-url&lt;/code> 指向 SQS-相容的 local 模擬器跑同一組指令。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="aws-cli-與-sdk">AWS CLI 與 SDK&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>AWS CLI 指令對照表（create-queue / send-message / receive-message / delete-message / set-queue-attributes）&lt;/li>
&lt;li>SDK 配置：region / credentials / retry policy / timeout&lt;/li>
&lt;li>Batch operation（SendMessageBatch、DeleteMessageBatch、最多 10 條）&lt;/li>
&lt;li>對應指令範例：&lt;code>aws sqs get-queue-attributes --queue-url &amp;lt;url&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="standard-vs-fifo-queue">Standard vs FIFO queue&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Standard：高吞吐、at-least-once、無 ordering、適合多數 task queue&lt;/li>
&lt;li>FIFO：exactly-once-ish（去重 5 分鐘窗口）、ordering（per MessageGroupId）、吞吐受限（3000 msg/sec with batching）&lt;/li>
&lt;li>選擇判讀（ordering 需求 vs 吞吐）&lt;/li>
&lt;/ul>
&lt;h3 id="visibility-timeout-與-in-flight">Visibility timeout 與 in-flight&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">Visibility timeout&lt;/a> 是 SQS 的 delivery 控制機制、取代 broker ACK：&lt;/p></description><content:encoded><![CDATA[<p>AWS SQS 是 AWS managed queue 服務、承擔三個責任：訊息排隊與重試（visibility timeout + DLQ）、解耦 producer / consumer（無 broker 運維）、AWS 生態原生整合（Lambda / EventBridge / Step Functions）。設計取捨偏向「極簡 API + managed 運維、用 visibility timeout 取代 broker ACK、無原生 ordering（standard queue）」。</p>
<p>對「AWS 生態 task queue、不想自管 broker、配合 Lambda 事件處理」這條路徑、SQS 是首選。本頁先給最短路徑、再展開日常 SendMessage / ReceiveMessage 操作與 visibility timeout 設計、最後進階治理（FIFO、DLQ、IAM、VPC endpoint）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 AWS CLI 建 standard / FIFO queue、發送與接收訊息</li>
<li>設計 visibility timeout 對齊 consumer 處理時間</li>
<li>配置 DLQ（dead-letter queue）與 maxReceiveCount</li>
<li>區分 long polling vs short polling、配合 Lambda event source mapping</li>
<li>評估 IAM policy、VPC endpoint、cross-account 訪問等治理場景</li>
</ol>
<h2 id="最短路徑5-分鐘把-sqs-跑起來">最短路徑：5 分鐘把 SQS 跑起來</h2>





<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. 建 queue（回傳 QueueUrl、後續操作都用它）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs create-queue --queue-name demo-queue
</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"># 2. 發送訊息</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws sqs send-message --queue-url &lt;url&gt; --message-body <span class="s2">&#34;hello&#34;</span>
</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="c1"># 3. 接收訊息（long polling、最多等 20 秒）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">aws sqs receive-message --queue-url &lt;url&gt; --wait-time-seconds <span class="m">20</span></span></span></code></pre></div><p>最短路徑驗證「queue 建得起來、能發能收」。實際應用配合 SDK / Lambda、見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。指令對真實 AWS 需設定 credentials 與 region；本機要先驗證可加 <code>--endpoint-url</code> 指向 SQS-相容的 local 模擬器跑同一組指令。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="aws-cli-與-sdk">AWS CLI 與 SDK</h3>
<p>子議題：</p>
<ul>
<li>AWS CLI 指令對照表（create-queue / send-message / receive-message / delete-message / set-queue-attributes）</li>
<li>SDK 配置：region / credentials / retry policy / timeout</li>
<li>Batch operation（SendMessageBatch、DeleteMessageBatch、最多 10 條）</li>
<li>對應指令範例：<code>aws sqs get-queue-attributes --queue-url &lt;url&gt;</code></li>
</ul>
<h3 id="standard-vs-fifo-queue">Standard vs FIFO queue</h3>
<p>子議題：</p>
<ul>
<li>Standard：高吞吐、at-least-once、無 ordering、適合多數 task queue</li>
<li>FIFO：exactly-once-ish（去重 5 分鐘窗口）、ordering（per MessageGroupId）、吞吐受限（3000 msg/sec with batching）</li>
<li>選擇判讀（ordering 需求 vs 吞吐）</li>
</ul>
<h3 id="visibility-timeout-與-in-flight">Visibility timeout 與 in-flight</h3>
<p><a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">Visibility timeout</a> 是 SQS 的 delivery 控制機制、取代 broker ACK：</p>
<ul>
<li>訊息被接收後變 in-flight、其他 consumer 看不到</li>
<li>Consumer 處理完呼叫 DeleteMessage、否則 timeout 後回到 queue</li>
<li>ChangeMessageVisibility 動態延長（長任務）</li>
<li>預設 30 秒、上限 12 小時</li>
</ul>
<h3 id="dlq-設計dead-letter-queue">DLQ 設計（dead-letter queue）</h3>
<p>子議題：</p>
<ul>
<li>maxReceiveCount：訊息被接收 N 次後送 DLQ</li>
<li>DLQ 監控與 alarm（CloudWatch metric）</li>
<li>Redrive policy（從 DLQ 重新放回 main queue）</li>
<li>對應 <a href="/blog/backend/knowledge-cards/poison-message/" data-link-title="Poison Message" data-link-desc="說明特定訊息內容如何穩定造成 consumer 失敗">poison message</a> 處理思路</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>visibility timeout、polling、Lambda event source 與 cost 已展開為 deep article：<a href="visibility-polling-lambda-cost/">visibility timeout / long polling / Lambda + cost</a>。下列子議題段保留選題判讀入口。</p>
<h3 id="long-polling-vs-short-polling">Long polling vs Short polling</h3>
<p>子議題：</p>
<ul>
<li>Short polling（預設）：立即回應、可能空回（高 cost）</li>
<li>Long polling（WaitTimeSeconds 1-20）：等到有訊息或超時</li>
<li>對 cost 與 latency 的取捨</li>
</ul>
<h3 id="sqs--lambda-event-source-mapping">SQS + Lambda event source mapping</h3>
<p>子議題：</p>
<ul>
<li>Lambda 自動 poll SQS（managed event source）</li>
<li>Batch size / batch window 配置</li>
<li>Partial batch failure（ReportBatchItemFailures）</li>
<li>對應 <a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues</a> 的全球交付對照</li>
</ul>
<h3 id="iam--cross-account-訪問">IAM / Cross-account 訪問</h3>
<p>子議題：</p>
<ul>
<li>Queue policy（resource-based）vs IAM policy（identity-based）</li>
<li>Cross-account producer / consumer 設定</li>
<li>Encryption（SSE-SQS / SSE-KMS）</li>
</ul>
<h3 id="vpc-endpoint私網訪問">VPC endpoint（私網訪問）</h3>
<p>子議題：</p>
<ul>
<li>Interface endpoint（PrivateLink）</li>
<li>適合不想經 public internet 的場景</li>
<li>跟 NAT Gateway 的 cost 對照</li>
</ul>
<h3 id="cloudwatch-metric-與-alarm">CloudWatch metric 與 alarm</h3>
<p>子議題：</p>
<ul>
<li>ApproximateNumberOfMessagesVisible（queue depth）</li>
<li>ApproximateAgeOfOldestMessage（lag 訊號）</li>
<li>NumberOfMessagesSent / Received / Deleted</li>
<li>Alarm 設計（depth 暴增、age 超 SLO）</li>
</ul>
<h3 id="cost-模型">Cost 模型</h3>
<p>子議題：</p>
<ul>
<li>Request cost（每百萬 request）</li>
<li>Data transfer cost（跨 region 才有）</li>
<li>FIFO 比 standard 貴的判讀</li>
<li>對應 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="message-反覆-redelivery看到同訊息多次">Message 反覆 redelivery（看到同訊息多次）</h3>
<p>操作原則：visibility timeout 設定 &lt; consumer 處理時間、訊息回 queue 又被另一 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">aws sqs get-queue-attributes --queue-url &lt;url&gt; --attribute-names VisibilityTimeout
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 新建 queue 預設 VisibilityTimeout 為 30 秒、處理時間長於此值就會看到 redelivery</span></span></span></code></pre></div><p>調整：延長 VisibilityTimeout 或 consumer 主動 ChangeMessageVisibility。</p>
<h3 id="dlq-累積">DLQ 累積</h3>
<p>操作原則：先看 DLQ 訊息內容、判斷 poison message vs 下游卡。</p>
<p>判讀路徑：訊息格式錯（永遠失敗）→ 下游服務 down（暫時失敗、可 redrive）→ consumer bug。</p>
<h3 id="throttlingaccount-quota">Throttling（account quota）</h3>
<p>操作原則：超過 account-level SendMessage / ReceiveMessage TPS、看 CloudWatch ThrottledRequests。處理：requeue exchange、quota 申請。</p>
<h3 id="iam-權限錯">IAM 權限錯</h3>
<p>操作原則：access denied 大多是 queue policy 跟 IAM policy 互動。判讀：用 IAM Policy Simulator 或 CloudTrail 看 deny 原因。</p>
<h3 id="lambda-event-source-失敗">Lambda event source 失敗</h3>
<p>操作原則：Lambda 失敗會自動 retry、超過 retry 進 DLQ。看 Lambda 的 DLQ 跟 SQS 的 DLQ 分工。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 streaming / replay</td>
          <td>AWS Kinesis / <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / MSK</td>
      </tr>
      <tr>
          <td>需要 pub/sub fan-out</td>
          <td>AWS SNS（搭配 SQS 做 fan-out）/ EventBridge</td>
      </tr>
      <tr>
          <td>需要複雜 routing</td>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> on EC2</td>
      </tr>
      <tr>
          <td>跨雲 / 跨平台</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
      </tr>
      <tr>
          <td>嚴格低延遲（&lt; 100ms）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> / Redis</td>
      </tr>
      <tr>
          <td>Workflow + durable execution</td>
          <td>AWS Step Functions / Temporal</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>SNS / EventBridge 細節（另開 cloud event routing 章節）</li>
<li>Step Functions / Lambda 完整功能</li>
<li>AWS SDK 各語言完整 API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="sqs-專屬案例c48-c59">SQS 專屬案例（C48-C59）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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></td>
          <td>分散式延遲任務 / at-least-once + DLQ</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/" data-link-title="3.C49 Airbnb Inspekt：Visibility timeout 當 retry budget" data-link-desc="Airbnb Inspekt 隱私掃描器、scanner pull message、visibility timeout 自然觸發重現、用重現次數當 retry budget。">3.C49 Airbnb Inspekt</a></td>
          <td>Visibility timeout 當隱式 retry</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Visibility timeout / Lambda event source</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51 Atlassian JiRT</a></td>
          <td>Kinesis + per-consumer SQS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/" data-link-title="3.C52 Nielsen：Spark on EKS 雙 SQS 工作流" data-link-desc="Nielsen 每日 25TB / 30B event、work queue &#43; completion queue 雙 SQS、queue depth autoscale EKS pod。">3.C52 Nielsen Spark on EKS</a></td>
          <td>雙 SQS / queue depth autoscale</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/" data-link-title="3.C53 FINRA：S3 → SQS notification 大檔上傳" data-link-desc="FINRA 金融監管、broker 上傳大檔、S3 → SQS notification → LFS、KMS &#43; bucket policy &#43; queue policy 三層稽核。">3.C53 FINRA Large File</a></td>
          <td>S3 → SQS 合規 / IAM 多層稽核</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54 Twitch EventSub</a></td>
          <td>SNS-SQS fan-out + Dispatcher</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/" data-link-title="3.C55 SmugMug：SQS 驅動可重放搜尋管線" data-link-desc="SmugMug 用 SQS 兩種模式：DynamoDB scan-segment 平行 backfill &#43; production query 鏡像 replay 到 replica。">3.C55 SmugMug search</a></td>
          <td>Workload generator / 平行 scan + replay</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/" data-link-title="3.C56 PostNL EBE：完整 DLQ &#43; retention &#43; redrive 設計" data-link-desc="PostNL 物流每天 1000 萬訊息、每 producer/consumer 隔離 stack、24h 內 100 次 retry、final DLQ 可 consumer redrive。">3.C56 PostNL EBE</a></td>
          <td>完整 DLQ + redrive + 隔離 stack</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/" data-link-title="3.C57 Lob：自家 fork @lob/sqs-consumer 修 FIFO bug" data-link-desc="Lob 原用 bbc/sqs-consumer 鎖 SDK v2、fork 出 @lob/sqs-consumer 支援 SDK v3 &#43; TypeScript &#43; 修 FIFO bug。">3.C57 Lob sqs-consumer</a></td>
          <td>Client library / SDK v3 / FIFO bug</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook</a></td>
          <td>Webhook → SQS buffer / FIFO 300 TPS</td>
      </tr>
      <tr>
          <td><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 scale</a></td>
          <td>100 億 msg/day 規模參考點</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 SQS 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware → MSK</a></td>
          <td>反面對照：何時 managed queue 不夠用、要升 streaming</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues</a></td>
          <td>全球交付對照（SQS 是 region-scoped）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>小型直接用 SQS / 中型補 idempotency / 大型補 streaming</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>平行 vendor：<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>、<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></li>
<li>下游能力：<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>、<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></li>
</ul>
]]></content:encoded></item><item><title>Elastic Stack</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/</guid><description>&lt;p>Elastic Stack（前 ELK）是 logs-heavy observability 棧、承擔三個責任：Elasticsearch 搜尋與分析（full-text + structured query）、Beats / Logstash 採集 pipeline、Kibana 視覺化 + Elastic APM（traces）。設計取捨偏向「搜尋為核心 + 統一搜尋介面 + Elastic Security SIEM 整合」。AWS 因 2021 license 變動 fork OpenSearch、提供 Apache 2.0 替代。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Elasticsearch + Kibana + Beats 基本棧&lt;/li>
&lt;li>用 KQL / Lucene 查詢 logs、用 ES DSL 寫進階搜尋&lt;/li>
&lt;li>設計 index lifecycle（hot / warm / cold / frozen）&lt;/li>
&lt;li>評估 Beats / Logstash / Fluent Bit / Vector 的採集選擇&lt;/li>
&lt;li>評估 Elastic License vs OpenSearch fork 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-elastic-stack-跑起來">最短路徑：5 分鐘把 Elastic Stack 跑起來&lt;/h2>





&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"># 1. 用 docker-compose 跑 ES + Kibana&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker-compose.yml with elasticsearch + kibana&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="c1"># 2. 用 Filebeat 採集 host logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: filebeat.yml with inputs + output.elasticsearch&lt;/span>
&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="c1"># 3. 在 Kibana 查詢驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: KQL: `@timestamp &amp;gt;= now-15m AND log.level: &amp;#34;error&amp;#34;`&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="採集-pipeline">採集 pipeline&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Beats（Filebeat / Metricbeat / Packetbeat / Heartbeat / Auditbeat）：輕量、各自專屬&lt;/li>
&lt;li>Logstash：重型 ETL（grok parsing / enrichment / 多 output）&lt;/li>
&lt;li>Fluent Bit / Vector：替代採集 agent（更輕量、OSS）&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS&lt;/a> 對照&lt;/li>
&lt;/ul>
&lt;h3 id="查詢語法">查詢語法&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>KQL（Kibana Query Language）：直覺、適合日常查詢&lt;/li>
&lt;li>Lucene query string：複雜搜尋、boolean operators&lt;/li>
&lt;li>ES DSL（JSON）：API 級進階查詢&lt;/li>
&lt;li>ES|QL（Elastic Query Language、ES 8.11+）：類 SQL pipeline 語法&lt;/li>
&lt;/ul>
&lt;h3 id="index-設計">Index 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Index template（mapping / settings）&lt;/li>
&lt;li>Data streams（time-series log / metrics）&lt;/li>
&lt;li>Field types：keyword / text / date / numeric / object / nested&lt;/li>
&lt;li>Dynamic mapping 風險：unbounded field 爆 index&lt;/li>
&lt;/ul>
&lt;h3 id="index-lifecycle-managementilm">Index Lifecycle Management（ILM）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Elastic Stack（前 ELK）是 logs-heavy observability 棧、承擔三個責任：Elasticsearch 搜尋與分析（full-text + structured query）、Beats / Logstash 採集 pipeline、Kibana 視覺化 + Elastic APM（traces）。設計取捨偏向「搜尋為核心 + 統一搜尋介面 + Elastic Security SIEM 整合」。AWS 因 2021 license 變動 fork OpenSearch、提供 Apache 2.0 替代。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Elasticsearch + Kibana + Beats 基本棧</li>
<li>用 KQL / Lucene 查詢 logs、用 ES DSL 寫進階搜尋</li>
<li>設計 index lifecycle（hot / warm / cold / frozen）</li>
<li>評估 Beats / Logstash / Fluent Bit / Vector 的採集選擇</li>
<li>評估 Elastic License vs OpenSearch fork 的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-elastic-stack-跑起來">最短路徑：5 分鐘把 Elastic Stack 跑起來</h2>





<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. 用 docker-compose 跑 ES + Kibana</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker-compose.yml with elasticsearch + kibana</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="c1"># 2. 用 Filebeat 採集 host logs</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: filebeat.yml with inputs + output.elasticsearch</span>
</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="c1"># 3. 在 Kibana 查詢驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: KQL: `@timestamp &gt;= now-15m AND log.level: &#34;error&#34;`</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="採集-pipeline">採集 pipeline</h3>
<p>子議題：</p>
<ul>
<li>Beats（Filebeat / Metricbeat / Packetbeat / Heartbeat / Auditbeat）：輕量、各自專屬</li>
<li>Logstash：重型 ETL（grok parsing / enrichment / 多 output）</li>
<li>Fluent Bit / Vector：替代採集 agent（更輕量、OSS）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a> 對照</li>
</ul>
<h3 id="查詢語法">查詢語法</h3>
<p>子議題：</p>
<ul>
<li>KQL（Kibana Query Language）：直覺、適合日常查詢</li>
<li>Lucene query string：複雜搜尋、boolean operators</li>
<li>ES DSL（JSON）：API 級進階查詢</li>
<li>ES|QL（Elastic Query Language、ES 8.11+）：類 SQL pipeline 語法</li>
</ul>
<h3 id="index-設計">Index 設計</h3>
<p>子議題：</p>
<ul>
<li>Index template（mapping / settings）</li>
<li>Data streams（time-series log / metrics）</li>
<li>Field types：keyword / text / date / numeric / object / nested</li>
<li>Dynamic mapping 風險：unbounded field 爆 index</li>
</ul>
<h3 id="index-lifecycle-managementilm">Index Lifecycle Management（ILM）</h3>
<p>子議題：</p>
<ul>
<li>Hot phase：active write</li>
<li>Warm phase：read-only、查詢頻率低</li>
<li>Cold phase：searchable snapshot（S3 / object storage）</li>
<li>Frozen phase（ES 7.12+）：searchable snapshot + minimal cluster resource</li>
<li>Delete phase</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="ilm-log-pipeline/">Index Lifecycle Management 與 Log Pipeline</a>：ILM policy 設計、data stream / rollover、Beats vs Elastic Agent 採集選擇、ingest pipeline 與 shard sizing、cost governance</li>
</ul>
<h2 id="migration-playbook">Migration Playbook</h2>
<ul>
<li><a href="migrate-to-elastic-cloud/">Elastic Cloud 遷移</a>：自管 Elastic Stack 遷移到 Elastic Cloud</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="elastic-apm">Elastic APM</h3>
<p>子議題：</p>
<ul>
<li>APM Server 接收 trace data</li>
<li>各語言 APM agent（Java / Python / Node / .NET / Go / Ruby / PHP）</li>
<li>接受 OTLP（ES 7.16+）</li>
<li>Service map / dependency 視覺化</li>
</ul>
<h3 id="elastic-securitysiem">Elastic Security（SIEM）</h3>
<p>子議題：</p>
<ul>
<li>SIEM dashboard / detection rule</li>
<li>ECS（Elastic Common Schema）跨資料統一 field naming</li>
<li>Sigma rule import</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a> 對照</li>
</ul>
<h3 id="cluster-scaling">Cluster scaling</h3>
<p>子議題：</p>
<ul>
<li>Node roles：master / data / ingest / coordinating / ML / transform</li>
<li>Hot-warm-cold architecture</li>
<li>Shard sizing（推薦 20-40GB per shard）</li>
<li>Cross-cluster search / replication</li>
</ul>
<h3 id="elastic-license-vs-opensearch-fork">Elastic License vs OpenSearch fork</h3>
<p>子議題：</p>
<ul>
<li>2021 Elastic 改 ELv2 / SSPL（非 OSI 認可）— AWS 不能提供「Elasticsearch as a Service」</li>
<li>AWS fork OpenSearch（Apache 2.0、基於 ES 7.10）</li>
<li>OpenSearch 持續演進、跟 ES 功能逐漸分歧</li>
<li>選擇判讀：合規 → OpenSearch；要最新 ES feature → Elastic</li>
</ul>
<h3 id="searchable-snapshots">Searchable Snapshots</h3>
<p>子議題：</p>
<ul>
<li>把 cold/frozen index 存 S3 / GCS / Azure Blob</li>
<li>查詢時動態 hydrate、成本降 80%+</li>
<li>適合 logs retention 長但查詢頻率低</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
</ul>
<h3 id="vector--fluent-bit-採集替代">Vector / Fluent Bit 採集替代</h3>
<p>子議題：</p>
<ul>
<li>為何用 Vector / Fluent Bit：更輕、resource 用量低</li>
<li>Beats 在 K8s 跑起來資源耗較大</li>
<li>對應 cost 跟 maintainability 取捨</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="index-mapping-explosion">Index mapping explosion</h3>
<p>操作原則：dynamic mapping 對未知 field 自動建 index、大量 field 爆 ES。</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"># TODO: GET /_cat/indices?v 看 field count</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: PUT index/_mapping 鎖定 fields</span></span></span></code></pre></div><h3 id="cluster-yellow--red">Cluster yellow / red</h3>
<p>操作原則：cluster status 影響 query。</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"># TODO: GET /_cluster/health</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: GET /_cat/shards?v 看 unassigned shards</span></span></span></code></pre></div><h3 id="query-過慢">Query 過慢</h3>
<p>操作原則：query 結果 &gt; 10K → 用 search_after / scroll；text field 上做 aggregation → 改 keyword field。</p>
<h3 id="disk-pressure">Disk pressure</h3>
<p>操作原則：cluster disk &gt; 85% → ES 進 read-only 模式。判讀：cluster.routing.allocation.disk.watermark。</p>
<h3 id="logstash-backpressure">Logstash backpressure</h3>
<p>操作原則：Logstash queue full → upstream Beats 累積 backpressure。判讀：Logstash monitoring page。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / Mimir</td>
      </tr>
      <tr>
          <td>純 logs 但 less search</td>
          <td>Loki（<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>）— 更便宜</td>
      </tr>
      <tr>
          <td>SaaS turnkey APM</td>
          <td><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></td>
      </tr>
      <tr>
          <td>AWS-managed Elastic</td>
          <td>OpenSearch on AWS（Apache 2.0）</td>
      </tr>
      <tr>
          <td>Cloud-native logs</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch Logs</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Logging</a></td>
      </tr>
      <tr>
          <td>多 tier observability</td>
          <td><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> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></td>
      </tr>
      <tr>
          <td>Enterprise SIEM</td>
          <td>Splunk / Microsoft Sentinel</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ES query DSL 完整 reference</li>
<li>Lucene scoring 演算法</li>
<li>Kibana dashboard 美術</li>
<li>Elastic ML / Anomaly Detection 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Logs 作為 audit evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Index Lifecycle / retention</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Elastic Stack 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>Beats / Logstash ↔ OTel Collector 採集 pipeline 對照</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型 single-node / 中型 hot-warm / 大型 hot-warm-cold-frozen</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Loki 對照）、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>Envoy</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/</guid><description>&lt;p>Envoy 是 CNCF graduated 的 service proxy、承擔三個責任：cloud-native L7 + L4 proxy（HTTP/1.1 + HTTP/2 + HTTP/3 + gRPC）、xDS dynamic config（不需 reload）、observability 內建（access log / stats / tracing）。設計取捨偏向「dynamic config + advanced traffic management + filter chain extensibility」、是 Istio / Linkerd2-proxy / AWS App Mesh / Envoy Gateway 的底層實作。&lt;/p>
&lt;p>對「service mesh data plane、API Gateway、advanced traffic management、gRPC / HTTP/2 / HTTP/3」這條路徑、Envoy 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Envoy + 基本 reverse proxy config&lt;/li>
&lt;li>用 xDS API 動態更新 config（不 reload）&lt;/li>
&lt;li>配置 listener / route / cluster / filter chain&lt;/li>
&lt;li>看懂 Envoy access log + stats + admin endpoint&lt;/li>
&lt;li>評估 Envoy 直接用 vs 用 Istio / Envoy Gateway 抽象&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-envoy-跑起來">最短路徑：5 分鐘把 Envoy 跑起來&lt;/h2>





&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"># 1. 啟動 Envoy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d --name envoy-demo &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> -p 9901:9901 -p 10000:10000 &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> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/envoy.yaml:/etc/envoy/envoy.yaml:ro&amp;#34;&lt;/span> &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> envoyproxy/envoy:v1.31-latest&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Static config 範例（&lt;code>envoy.yaml&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">static_resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">listener_0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: 0.0.0.0, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filter_chains&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">envoy.filters.network.http_connection_manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typed_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;@type&amp;#34;: &lt;/span>&lt;span class="l">type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stat_prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ingress_http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">virtual_hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">domains&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http_filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">envoy.filters.http.router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">typed_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#34;@type&amp;#34;: &lt;/span>&lt;span class="l">type.googleapis.com/envoy.extensions.filters.http.router.v3.Router&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clusters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">connect_timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">STRICT_DNS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">load_assignment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cluster_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">lb_endpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: app, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">admin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">socket_address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">address: 0.0.0.0, port_value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">9901&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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"># 3. 驗證 + admin endpoint&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">curl http://localhost:10000 &lt;span class="c1"># proxy 路徑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">curl http://localhost:9901/stats &lt;span class="c1"># metrics&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">curl http://localhost:9901/clusters &lt;span class="c1"># upstream health&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">curl http://localhost:9901/config_dump &lt;span class="c1"># running config&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="envoy-config-結構">Envoy config 結構&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Envoy 是 CNCF graduated 的 service proxy、承擔三個責任：cloud-native L7 + L4 proxy（HTTP/1.1 + HTTP/2 + HTTP/3 + gRPC）、xDS dynamic config（不需 reload）、observability 內建（access log / stats / tracing）。設計取捨偏向「dynamic config + advanced traffic management + filter chain extensibility」、是 Istio / Linkerd2-proxy / AWS App Mesh / Envoy Gateway 的底層實作。</p>
<p>對「service mesh data plane、API Gateway、advanced traffic management、gRPC / HTTP/2 / HTTP/3」這條路徑、Envoy 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Envoy + 基本 reverse proxy config</li>
<li>用 xDS API 動態更新 config（不 reload）</li>
<li>配置 listener / route / cluster / filter chain</li>
<li>看懂 Envoy access log + stats + admin endpoint</li>
<li>評估 Envoy 直接用 vs 用 Istio / Envoy Gateway 抽象</li>
</ol>
<h2 id="最短路徑5-分鐘把-envoy-跑起來">最短路徑：5 分鐘把 Envoy 跑起來</h2>





<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. 啟動 Envoy</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name envoy-demo <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -p 9901:9901 -p 10000:10000 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/envoy.yaml:/etc/envoy/envoy.yaml:ro&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  envoyproxy/envoy:v1.31-latest</span></span></code></pre></div><p>Static config 範例（<code>envoy.yaml</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">static_resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">listener_0</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: 0.0.0.0, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w"> </span>}<span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">filter_chains</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">envoy.filters.network.http_connection_manager</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">typed_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">&#34;@type&#34;: </span><span class="l">type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">stat_prefix</span><span class="p">:</span><span class="w"> </span><span class="l">ingress_http</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">          </span><span class="nt">route_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">            </span><span class="nt">virtual_hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">              </span><span class="nt">domains</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;*&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">              </span><span class="nt">routes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span>- <span class="nt">match</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/&#34;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">                </span><span class="nt">route</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend }</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">http_filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">envoy.filters.http.router</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span><span class="nt">typed_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span><span class="nt">&#34;@type&#34;: </span><span class="l">type.googleapis.com/envoy.extensions.filters.http.router.v3.Router</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">clusters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nt">connect_timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">STRICT_DNS</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">    </span><span class="nt">load_assignment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">      </span><span class="nt">cluster_name</span><span class="p">:</span><span class="w"> </span><span class="l">service_backend</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span>- <span class="nt">lb_endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span>- <span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: app, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w"> </span>}<span class="w"> </span>}<span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w"></span><span class="nt">admin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">socket_address</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">address: 0.0.0.0, port_value</span><span class="p">:</span><span class="w"> </span><span class="m">9901</span><span class="w"> </span>}<span class="w"> </span>}</span></span></code></pre></div>




<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"># 3. 驗證 + admin endpoint</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl http://localhost:10000                    <span class="c1"># proxy 路徑</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">curl http://localhost:9901/stats               <span class="c1"># metrics</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">curl http://localhost:9901/clusters            <span class="c1"># upstream health</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">curl http://localhost:9901/config_dump         <span class="c1"># running config</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="envoy-config-結構">Envoy config 結構</h3>
<p>子議題：</p>
<ul>
<li>Listener：listen address + filter chain</li>
<li>Route：path matching + cluster routing</li>
<li>Cluster：upstream endpoint discovery + load balancing</li>
<li>Endpoint：實際 backend</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
</ul>
<h3 id="static-vs-dynamic-config">Static vs Dynamic config</h3>
<p>子議題：</p>
<ul>
<li>Static：YAML 寫死、適合 dev / debug</li>
<li>Dynamic（xDS）：control plane push config</li>
<li>xDS protocol：LDS / RDS / CDS / EDS / SDS</li>
<li>對應 control plane：Istio / Gloo / 自寫</li>
</ul>
<h3 id="admin-endpoint">Admin endpoint</h3>
<p>子議題：</p>
<ul>
<li>/stats / /clusters / /config_dump / /listeners / /server_info</li>
<li>runtime config（/runtime_modify）</li>
<li>對應 observability 跟 debug</li>
<li>對應指令：<code>curl admin:9901/clusters</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="xds-api-細節">xDS API 細節</h3>
<p>子議題：</p>
<ul>
<li>LDS / RDS / CDS / EDS / SDS / RTDS / ECDS</li>
<li>ADS（Aggregated Discovery Service）統一通道</li>
<li>Delta xDS（incremental）vs SOTW（State of the World）</li>
<li>對應案例 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></li>
</ul>
<h3 id="filter-chainhttp--network-filter">Filter chain（HTTP / network filter）</h3>
<p>子議題：</p>
<ul>
<li>HTTP filters：router / cors / fault / rate_limit / ext_authz / jwt_authn</li>
<li>Network filters：tcp_proxy / mongo_proxy / redis_proxy</li>
<li>自訂 filter（C++ / WebAssembly）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a>（ext_authz）</li>
</ul>
<h3 id="observability-內建">Observability 內建</h3>
<p>子議題：</p>
<ul>
<li>Access log（structured / configurable format）</li>
<li>Stats（envoy 內建 metrics）</li>
<li>Distributed tracing（Jaeger / Zipkin / Datadog / OpenTelemetry）</li>
<li>對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a></li>
</ul>
<h3 id="envoy-gateway--emissary--gloo">Envoy Gateway / Emissary / Gloo</h3>
<p>子議題：</p>
<ul>
<li>Envoy Gateway：Gateway API native（CNCF project）</li>
<li>Emissary（前 Ambassador）：K8s ingress + API Gateway</li>
<li>Gloo：Solo.io 商業 Envoy 整合</li>
<li>選型判讀：純 K8s ingress → Envoy Gateway；商業支援 → Gloo / Emissary</li>
</ul>
<h3 id="service-mesh-data-plane">Service mesh data plane</h3>
<p>子議題：</p>
<ul>
<li>Istio：control plane + Envoy sidecar</li>
<li>Linkerd2：自家 Rust proxy（不是 Envoy）— Linkerd2-proxy</li>
<li>Cilium Service Mesh：eBPF + Envoy</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio governance</a></li>
</ul>
<h3 id="webassembly-extension">WebAssembly extension</h3>
<p>子議題：</p>
<ul>
<li>WASM filter：跨語言寫 Envoy extension（Rust / AssemblyScript / Go）</li>
<li>跟 Lua（OpenResty 模式）對比</li>
<li>適合：custom auth / rate limit / metric collection</li>
</ul>
<h3 id="advanced-traffic-management">Advanced traffic management</h3>
<p>子議題：</p>
<ul>
<li>Retry / Circuit breaker / Outlier detection</li>
<li>Timeout（connect / request / idle）</li>
<li>Traffic split（canary / blue-green / mirror）</li>
<li>Rate limit（local + global）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="config-sync-失敗">Config sync 失敗</h3>
<p>操作原則：xDS control plane 連不上 / config 格式錯。判讀：admin /stats 看 update_failure、/config_dump 看當前 config。</p>
<h3 id="listener-config-error">Listener config error</h3>
<p>操作原則：YAML 格式錯、port 衝突、bind address 錯。判讀：startup log + admin /listeners。</p>
<h3 id="cluster-endpoint-全-unhealthy">Cluster endpoint 全 unhealthy</h3>
<p>操作原則：health check 失敗、SDS 沒提供 cert、network 不通。判讀：admin /clusters 看 endpoint state。</p>
<h3 id="circuit-breaker-trip">Circuit breaker trip</h3>
<p>操作原則：upstream 失敗率 &gt; threshold、Envoy 主動切。判讀：admin /stats 看 cb 相關 metric。</p>
<h3 id="tracing-missing-spans">Tracing missing spans</h3>
<p>操作原則：tracer config + sampler rate 設錯、context propagation 不對。對應 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">04 observability OTel</a>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置簡單 / 小場景</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
      </tr>
      <tr>
          <td>K8s ingress only</td>
          <td>Ingress-nginx / Envoy Gateway / Gateway API</td>
      </tr>
      <tr>
          <td>Service mesh control plane</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>Edge proxy / CDN</td>
          <td>Cloudflare / Fastly / CloudFront</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 Envoy YAML schema reference</li>
<li>xDS protocol binary format</li>
<li>各 Istio / Gloo / Emissary 細節（見各自 docs）</li>
<li>Envoy C++ filter 開發</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio governance</a></td>
          <td>Envoy-based service mesh 在大規模叢集的分批升級與可重播流程</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Envoy 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>Tradeshift 選 Linkerd（非 Envoy）做切流、對照 Envoy/Istio 的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>Envoy outlier detection / circuit breaker / draining listener 是回退面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>大規模 / 複雜 traffic / 多 DC → Envoy mesh 才能撐住協同節奏</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Envoy 案例</strong>：Lyft 自家 Envoy production 案例、Stripe / Reddit 用 Envoy 邊緣案例、Envoy Gateway 早期 adopter。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">04 observability OTel</a>、<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a></li>
</ul>
]]></content:encoded></item><item><title>FireHydrant</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/</guid><description>&lt;p>FireHydrant 是 IR 平台、承擔三個責任：incident response lifecycle（declare / respond / update）、retrospective workflow + runbook automation、cross-platform integration（Slack + Microsoft Teams 雙支援）。內建 status page、後加 on-call 模組。設計取捨偏向「完整 IR + retrospective + Teams 支援」、跟 incident.io 的差異是 Teams 友善。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>FireHydrant 的核心定位是 &lt;em>service catalog 驅動的 IR platform&lt;/em> — 強調 &lt;em>service ownership + runbook automation + retrospective workflow&lt;/em> 三角支撐、而不是只把 Slack 當 chat surface。底層是 &lt;em>service catalog&lt;/em>（service / team / dependency / owner metadata）、incident 一宣告就自動關聯 affected service 跟 on-call team；上層是 &lt;em>runbook engine&lt;/em>（trigger + action DAG）跟 &lt;em>retrospective workflow&lt;/em>（template + facilitator + action item tracking）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> 同層、差異在 &lt;em>Teams-native&lt;/em> 而非 Slack-only — Microsoft 365 + Salesforce-heavy enterprise 是 FireHydrant 主場。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> 比是 &lt;em>IR + retrospective platform&lt;/em> vs &lt;em>paging platform&lt;/em>、覆蓋 lifecycle 更廣但 on-call 模組相對年輕。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &amp;#43; AI investigation、Slack-native &amp;#43; 200&amp;#43; integration">Rootly&lt;/a> 比走 &lt;em>catalog-first&lt;/em> 而非 &lt;em>AI / no-code first&lt;/em>。&lt;/p>
&lt;p>關鍵張力：&lt;em>service catalog 完整度&lt;/em> ↔ &lt;em>runbook automation 黑箱&lt;/em> 是 FireHydrant 客戶最大的 trade-off。catalog 沒維護好、runbook 自動 page 錯 team、retrospective owner 找不到；catalog 維護成本又會被視為 platform team 負擔。要看清楚自己 &lt;em>願意投多少 catalog 治理換多少 IR 自動化&lt;/em>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>整合 FireHydrant 到 Slack / Teams&lt;/li>
&lt;li>配置 incident lifecycle + severity matrix&lt;/li>
&lt;li>用 Runbook automation 自動化 standard response&lt;/li>
&lt;li>用 Retrospective facilitator 跑復盤&lt;/li>
&lt;li>評估 FireHydrant vs incident.io / Rootly&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 FireHydrant deployment 是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>FireHydrant 是 IR 平台、承擔三個責任：incident response lifecycle（declare / respond / update）、retrospective workflow + runbook automation、cross-platform integration（Slack + Microsoft Teams 雙支援）。內建 status page、後加 on-call 模組。設計取捨偏向「完整 IR + retrospective + Teams 支援」、跟 incident.io 的差異是 Teams 友善。</p>
<h2 id="服務定位">服務定位</h2>
<p>FireHydrant 的核心定位是 <em>service catalog 驅動的 IR platform</em> — 強調 <em>service ownership + runbook automation + retrospective workflow</em> 三角支撐、而不是只把 Slack 當 chat surface。底層是 <em>service catalog</em>（service / team / dependency / owner metadata）、incident 一宣告就自動關聯 affected service 跟 on-call team；上層是 <em>runbook engine</em>（trigger + action DAG）跟 <em>retrospective workflow</em>（template + facilitator + action item tracking）。跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> 同層、差異在 <em>Teams-native</em> 而非 Slack-only — Microsoft 365 + Salesforce-heavy enterprise 是 FireHydrant 主場。跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 比是 <em>IR + retrospective platform</em> vs <em>paging platform</em>、覆蓋 lifecycle 更廣但 on-call 模組相對年輕。跟 <a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a> 比走 <em>catalog-first</em> 而非 <em>AI / no-code first</em>。</p>
<p>關鍵張力：<em>service catalog 完整度</em> ↔ <em>runbook automation 黑箱</em> 是 FireHydrant 客戶最大的 trade-off。catalog 沒維護好、runbook 自動 page 錯 team、retrospective owner 找不到；catalog 維護成本又會被視為 platform team 負擔。要看清楚自己 <em>願意投多少 catalog 治理換多少 IR 自動化</em>。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>整合 FireHydrant 到 Slack / Teams</li>
<li>配置 incident lifecycle + severity matrix</li>
<li>用 Runbook automation 自動化 standard response</li>
<li>用 Retrospective facilitator 跑復盤</li>
<li>評估 FireHydrant vs incident.io / Rootly</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 FireHydrant deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Runbook automation 範圍</strong>：runbook 是否走版控（API / Terraform Provider）、trigger 條件是否有 staging dry-run、high-impact action（自動 page exec / 自動發 customer notification）是否走 <em>approval gate</em> 而非 fire-and-forget</li>
<li><strong>Service catalog 完整度</strong>：service / team / dependency / owner 是否齊全、stale entry 是否有 review cadence、incident declare 時 affected service dropdown 是否能立即定位、catalog 是否跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">ServiceNow CMDB</a> / Backstage / Salesforce 同步</li>
<li><strong>Retrospective workflow</strong>：incident close 後是否自動觸發 retrospective、facilitator 是否指定、action item 是否寫回 Jira / Linear 並 track close-rate、template 是否區分 sev1 / sev2 不同深度</li>
<li><strong>SSO + audit</strong>：SCIM provisioning 是否跟 IdP 同步、admin / responder / viewer 三層角色是否區分、audit log 是否 export 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 或 SIEM</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a> 邊界的待補項目。</p>
<h2 id="最短路徑">最短路徑</h2>





<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. 註冊 + install Slack / Teams app</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. 配置 severity matrix / roles</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. Declare test incident</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. 跑 retrospective workflow</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="incident-lifecycle">Incident lifecycle</h3>
<p>子議題：</p>
<ul>
<li>Severity matrix（impact × urgency）</li>
<li>Status workflow（detected → investigating → identified → monitoring → resolved）</li>
<li>Role：commander / scribe / SME</li>
</ul>
<h3 id="runbook-automation--retrospective">Runbook automation + Retrospective</h3>
<p>子議題：</p>
<ul>
<li>預定 runbook（auto page / 建 Jira / open Zoom）</li>
<li>Trigger condition</li>
<li>Retrospective template + facilitator role + action items</li>
</ul>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>FireHydrant</th>
          <th>incident.io</th>
          <th>PagerDuty</th>
          <th>Rootly</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chat 主場</td>
          <td>Slack + Teams 雙支援</td>
          <td>Slack-first（Teams 後加）</td>
          <td>Slack / Teams（chat 非核心）</td>
          <td>Slack-first</td>
      </tr>
      <tr>
          <td>核心抽象</td>
          <td>Service catalog + runbook</td>
          <td>Incident workflow + AI assist</td>
          <td>On-call schedule + paging</td>
          <td>No-code workflow + AI</td>
      </tr>
      <tr>
          <td>Retrospective</td>
          <td>內建 facilitator + template + action 追蹤</td>
          <td>內建、AI assist 草稿</td>
          <td>弱、靠 integration</td>
          <td>內建、AI summary</td>
      </tr>
      <tr>
          <td>Catalog</td>
          <td>一級概念、service / team / dependency</td>
          <td>有 catalog、深度較淺</td>
          <td>Service 概念存在、不強調 ownership</td>
          <td>有 catalog、強調 no-code 編輯</td>
      </tr>
      <tr>
          <td>On-call</td>
          <td>後加模組、相對年輕</td>
          <td>內建、跟 incident workflow 整合</td>
          <td>業界最成熟</td>
          <td>內建</td>
      </tr>
      <tr>
          <td>整合主場</td>
          <td>ServiceNow / Salesforce / Microsoft</td>
          <td>Linear / Notion / GitHub</td>
          <td>廣泛、paging-centric</td>
          <td>Jira / Slack</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Enterprise + Teams + service ownership-heavy</td>
          <td>Slack-native + 高速 startup</td>
          <td>Paging-first + 已有 IR tooling</td>
          <td>No-code / AI-forward + 中型團隊</td>
      </tr>
  </tbody>
</table>
<p>選 FireHydrant 的核心訴求：<em>service ownership 是組織一級概念</em>（platform team / SRE 已維護 catalog）、<em>Microsoft 365 / Teams 是預設辦公 surface</em>、<em>retrospective + action item 追蹤要 first-class</em>。Slack-only + startup 速度優先走 incident.io；paging 是核心走 PagerDuty。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="status-page-內建">Status page 內建</h3>
<p>子議題：不需另接 Statuspage / Instatus、Component / incident sync、Subscriber notification</p>
<h3 id="cross-platformslack--teams">Cross-platform（Slack + Teams）</h3>
<p>子議題：同帳號跨兩平台、Microsoft Teams enterprise 需求</p>
<h3 id="on-call-模組--service-catalog">On-call 模組 + Service catalog</h3>
<p>子議題：後加 module、service / team / dependency metadata 跟 incident 自動關聯</p>
<h3 id="runbook-automationtrigger--action-dag">Runbook automation（trigger + action DAG）</h3>
<p>Runbook 是 trigger（severity 升級 / service 標籤 / 時間 elapsed）+ action（page team / 建 Zoom / 建 Jira / 發 customer notification / 更新 status page）的 DAG。production 設計要回答：<em>哪些 action 可以 fire-and-forget</em>（建 Zoom / 建 ticket）、<em>哪些要 approval gate</em>（發 customer notification / 自動 page exec）、<em>失敗回退是什麼</em>（action 失敗時 commander 是否會收到通知、還是默默 skip）。Runbook 走 API / Terraform Provider 版控、不在 console 直改 production。</p>
<h3 id="service-catalog--dependency">Service catalog + dependency</h3>
<p>Catalog 一級欄位：service / owning team / on-call rotation / upstream dependency / downstream consumer / tier（critical / standard / experimental）。意義是 incident declare 時 <em>affected service</em> 一選、systems team + on-call + 通報範圍自動推導。catalog stale 是最大失敗模式 — team 重組沒同步、deprecated service 沒下架、ownership 落在離職員工身上。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9 IT asset 模組</a> 的 CMDB / inventory 治理原則。</p>
<h3 id="servicenow--salesforce-整合">ServiceNow / Salesforce 整合</h3>
<p>FireHydrant 的 Microsoft / Salesforce 生態整合是 differentiator：incident 自動建 ServiceNow ticket（CMDB CI 關聯）、Salesforce case escalate 自動 declare incident、Customer Success 在 Salesforce 看到 affected account list。enterprise customer 常見部署模式。</p>
<h3 id="signalsalerting-layer">Signals（alerting layer）</h3>
<p>FireHydrant Signals 是 alerting / paging layer、跟 PagerDuty 直接對打 — alert source（<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog</a> / Prometheus / <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Sentry</a> etc）→ Signals → on-call rotation。意義是 <em>paging 不再需要外接 PagerDuty</em>、FireHydrant 一站涵蓋 alert → incident → retrospective。但成熟度仍年輕、PagerDuty paging 細節（escalation policy / override / global event routing）仍有差距。</p>
<h3 id="ai-features">AI features</h3>
<p>FireHydrant 後加 AI assist：incident summary 草稿、retrospective draft、similar incident suggestion。定位是 <em>assist</em>、不取代 commander / facilitator 判斷。production 用法限制在 <em>草稿 + human review</em>、不自動 publish 對外 communication。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Severity matrix 不一致</strong>：跨 team 定義不同、用 catalog default + onboarding</li>
<li><strong>Runbook 沒觸發</strong>：trigger 不滿足 / integration token 失效</li>
<li><strong>Status page 不同步</strong>：自動 / 手動 sync 配置錯</li>
<li><strong>Retrospective 沒人做</strong>：close 後沒 prompt / facilitator 沒指派</li>
<li><strong>Service catalog stale</strong>：team 重組沒同步、ownership 落在離職員工身上 — 設 quarterly review cadence、catalog 走 PR + owner attestation、跟 IdP / HR system join 偵測 orphan ownership</li>
<li><strong>Runbook action 黑箱 fire-and-forget</strong>：自動發 customer notification 結果發錯客群、自動 page exec 結果半夜誤叫 — high-impact action 走 approval gate、failure path 要顯式通知 commander、不能默默 skip</li>
<li><strong>SSO sync drift</strong>：SCIM 沒同步離職 user、admin 角色沒回收 — SCIM provisioning 必開、admin 角色走 break-glass、audit log export 到 SIEM 對賬</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack-first</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>No-code / AI</td>
          <td><a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></td>
      </tr>
      <tr>
          <td>Paging-first</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
      </tr>
      <tr>
          <td>Atlassian 套件</td>
          <td><a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> + JSM</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 integration 完整 setup / Pricing / Teams workflow 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>FireHydrant 偏向 Microsoft Teams + Jira 生態的 IR 平台</strong>：本案例庫尚無直接揭露 FireHydrant 使用細節的事故；可參照的閱讀脈絡是「企業套件 + 跨產品 IR」與「service ownership-heavy enterprise 跨產品依賴」的事故。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365 cases</a></td>
          <td>Teams + 套件級事故的 IR 協作對照、ServiceNow ticket join 場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">Azure AD cases</a></td>
          <td>身份控制面事故的跨產品依賴對照、SSO drift 跟 service catalog ownership 失準對應</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">Atlassian cases</a></td>
          <td>Jira / Confluence 生態事故、retrospective action item 寫回流程的失敗模式</td>
      </tr>
  </tbody>
</table>
<p>待補 candidate：Snyk / Vercel / 大型 Microsoft 生態 customer 公開 story。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a>、<a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
]]></content:encoded></item><item><title>Shopify</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/</guid><description>&lt;p>Shopify 是 BFCM（Black Friday / Cyber Monday）流量峰值的可靠性教學標竿、pod-based architecture 是 multi-tenant SaaS 的隔離典範。教學重點在「年度可預期峰值如何透過架構與演練準備」。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Pod-based Architecture：多租戶切分、商家隔離設計&lt;/li>
&lt;li>BFCM 準備：年度峰值的 capacity planning 流程&lt;/li>
&lt;li>Resiliency Matrix：列舉服務與失效模式的對照表&lt;/li>
&lt;li>Toxiproxy / Resiliency tooling：Shopify 開源的 chaos 工具&lt;/li>
&lt;li>Database sharding：MySQL 分片策略與 online resharding&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>BFCM Capacity Planning&lt;/td>
 &lt;td>容量預測、load test 設計、實際峰值對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pod Architecture&lt;/td>
 &lt;td>多租戶切分、failure isolation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resiliency Matrix&lt;/td>
 &lt;td>失效模式對照表的維護方法&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Toxiproxy&lt;/td>
 &lt;td>TCP-level 故障注入的工程實作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database resharding&lt;/td>
 &lt;td>線上 schema 與 sharding 變更&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1&lt;/a>&lt;/td>
 &lt;td>BFCM 容量治理與 Game Day&lt;/td>
 &lt;td>把季節性峰值壓力轉成可預演、可回寫的年度可靠性節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">H2&lt;/a>&lt;/td>
 &lt;td>Pod Architecture 與 Resiliency Matrix&lt;/td>
 &lt;td>多租戶隔離與系統化失敗模式盤點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Shopify 這個案例在講的是峰值流量如何被提前吸收，而不是在事故當下硬扛。讀者先抓 capacity planning、performance testing 與 pods architecture 的分工，再看它們怎麼把 BFCM 這種季節性壓力轉成可管理的工程節奏。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當流量會在短時間內暴增時，先做容量模型與壓測，再確認 pods 邊界能否切住故障擴散。當資料平台也在同一波壓力下成長時，重點不只在擴容，而在是否能保住查詢、寫入與回放的穩定節奏。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否在 peak 之前說出容量上限與安全緩衝&lt;/li>
&lt;li>能否把壓測結果對應到真實流量模型&lt;/li>
&lt;li>能否讓 pods 邊界成為故障隔離單位&lt;/li>
&lt;li>能否在高峰前完成演練與當日指揮節奏對齊&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Shopify 的價值在於它把峰值準備寫成年度節奏，這和 LinkedIn 的 capacity planning、AWS S3 的區域風險、Discord 的流量驚奇都能互相對照。讀這頁時要抓的是「先把峰值變成可預測問題」，而不是等事故來了才補救。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>BFCM 前的 capacity planning 讓峰值壓力先被模型吸收，而不是直接落在事故當下。&lt;/li>
&lt;li>pods architecture 把多租戶流量切成較小隔離單位，限制故障擴散。&lt;/li>
&lt;li>performance testing 讓真實峰值在演練階段就可見。&lt;/li>
&lt;li>resiliency tooling 讓團隊能在高峰前驗證失效模式。&lt;/li>
&lt;li>database resharding 讓高峰下的 stateful 系統仍能持續擴容。&lt;/li>
&lt;li>incident rehearsal 讓當日指揮與復原節奏先對齊。&lt;/li>
&lt;li>resiliency matrix 讓每個服務與失效模式都有明確對照。&lt;/li>
&lt;li>Toxiproxy 讓 TCP 層故障注入成為可重用工具。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/capacity-planning-shopify">Capacity Planning at Scale&lt;/a>：BFCM 前的容量規劃與驗證方法。&lt;/li>
&lt;li>&lt;a href="https://shopify.engineering/scale-performance-testing">Performance Testing At Scale—for BFCM and Beyond&lt;/a>：BFCM scale testing 與壓測節奏。&lt;/li>
&lt;li>&lt;a href="https://shopify.engineering/a-pods-architecture-to-allow-shopify-to-scale">A Pods Architecture To Allow Shopify To Scale&lt;/a>：pods 架構與隔離設計。&lt;/li>
&lt;li>&lt;a href="https://shopify.engineering/blogs/engineering/reliably-scale-data-platform">How to Reliably Scale Your Data Platform for High Volumes&lt;/a>：資料平台在高流量下的可靠性方法。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Shopify 是 BFCM（Black Friday / Cyber Monday）流量峰值的可靠性教學標竿、pod-based architecture 是 multi-tenant SaaS 的隔離典範。教學重點在「年度可預期峰值如何透過架構與演練準備」。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Pod-based Architecture：多租戶切分、商家隔離設計</li>
<li>BFCM 準備：年度峰值的 capacity planning 流程</li>
<li>Resiliency Matrix：列舉服務與失效模式的對照表</li>
<li>Toxiproxy / Resiliency tooling：Shopify 開源的 chaos 工具</li>
<li>Database sharding：MySQL 分片策略與 online resharding</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BFCM Capacity Planning</td>
          <td>容量預測、load test 設計、實際峰值對照</td>
      </tr>
      <tr>
          <td>Pod Architecture</td>
          <td>多租戶切分、failure isolation</td>
      </tr>
      <tr>
          <td>Resiliency Matrix</td>
          <td>失效模式對照表的維護方法</td>
      </tr>
      <tr>
          <td>Toxiproxy</td>
          <td>TCP-level 故障注入的工程實作</td>
      </tr>
      <tr>
          <td>Database resharding</td>
          <td>線上 schema 與 sharding 變更</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1</a></td>
          <td>BFCM 容量治理與 Game Day</td>
          <td>把季節性峰值壓力轉成可預演、可回寫的年度可靠性節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">H2</a></td>
          <td>Pod Architecture 與 Resiliency Matrix</td>
          <td>多租戶隔離與系統化失敗模式盤點</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Shopify 這個案例在講的是峰值流量如何被提前吸收，而不是在事故當下硬扛。讀者先抓 capacity planning、performance testing 與 pods architecture 的分工，再看它們怎麼把 BFCM 這種季節性壓力轉成可管理的工程節奏。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當流量會在短時間內暴增時，先做容量模型與壓測，再確認 pods 邊界能否切住故障擴散。當資料平台也在同一波壓力下成長時，重點不只在擴容，而在是否能保住查詢、寫入與回放的穩定節奏。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否在 peak 之前說出容量上限與安全緩衝</li>
<li>能否把壓測結果對應到真實流量模型</li>
<li>能否讓 pods 邊界成為故障隔離單位</li>
<li>能否在高峰前完成演練與當日指揮節奏對齊</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Shopify 的價值在於它把峰值準備寫成年度節奏，這和 LinkedIn 的 capacity planning、AWS S3 的區域風險、Discord 的流量驚奇都能互相對照。讀這頁時要抓的是「先把峰值變成可預測問題」，而不是等事故來了才補救。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>BFCM 前的 capacity planning 讓峰值壓力先被模型吸收，而不是直接落在事故當下。</li>
<li>pods architecture 把多租戶流量切成較小隔離單位，限制故障擴散。</li>
<li>performance testing 讓真實峰值在演練階段就可見。</li>
<li>resiliency tooling 讓團隊能在高峰前驗證失效模式。</li>
<li>database resharding 讓高峰下的 stateful 系統仍能持續擴容。</li>
<li>incident rehearsal 讓當日指揮與復原節奏先對齊。</li>
<li>resiliency matrix 讓每個服務與失效模式都有明確對照。</li>
<li>Toxiproxy 讓 TCP 層故障注入成為可重用工具。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/capacity-planning-shopify">Capacity Planning at Scale</a>：BFCM 前的容量規劃與驗證方法。</li>
<li><a href="https://shopify.engineering/scale-performance-testing">Performance Testing At Scale—for BFCM and Beyond</a>：BFCM scale testing 與壓測節奏。</li>
<li><a href="https://shopify.engineering/a-pods-architecture-to-allow-shopify-to-scale">A Pods Architecture To Allow Shopify To Scale</a>：pods 架構與隔離設計。</li>
<li><a href="https://shopify.engineering/blogs/engineering/reliably-scale-data-platform">How to Reliably Scale Your Data Platform for High Volumes</a>：資料平台在高流量下的可靠性方法。</li>
</ul>
]]></content:encoded></item><item><title>3.5 攻擊者視角（紅隊）：傳遞層弱點判讀</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/</guid><description>&lt;p>傳遞層紅隊判讀的核心目標是確認「訊息如何被重送、重放、放大與耗盡資源」。這裡的紅隊指攻擊者視角的風險檢查：先找可被放大的傳遞路徑，再回推控制面。只要系統採用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 或 stream，弱點就會同時落在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics&lt;/a>、consumer 容量與回復流程。&lt;/p>
&lt;h2 id="判讀傳遞層弱點的主要軸線">【判讀】傳遞層弱點的主要軸線&lt;/h2>
&lt;p>傳遞層弱點可分成三條軸線：投遞語意、處理語意、回復語意。投遞語意看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a> 與重送條件；處理語意看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與 side effect；回復語意看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號何時要提高紅隊檢查優先級">【可觀察訊號】何時要提高紅隊檢查優先級&lt;/h2>
&lt;p>下列訊號出現時，傳遞層通常需要先做弱點盤點：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 持續增加，且重試量同步升高&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 累積速度高於排空速度&lt;/li>
&lt;li>同一事件會被多路 consumer 讀取並觸發多個下游 side effect&lt;/li>
&lt;li>回放流程缺少操作邊界與審核節點&lt;/li>
&lt;/ul>
&lt;h2 id="失敗代價傳遞層弱點的代價型態">【失敗代價】傳遞層弱點的代價型態&lt;/h2>
&lt;p>傳遞層弱點會把局部錯誤放大成系統性壓力。重複投遞會造成重複扣款、重複通知或重複建單；毒訊息會阻塞分區與 worker；重放策略缺少邊界會把歷史事件再次推進生產流程。這些問題的共同代價是資料偏移、事故窗口延長與操作風險上升。&lt;/p>
&lt;h2 id="最低控制面進入服務實體前要先定義">【最低控制面】進入服務實體前要先定義&lt;/h2>
&lt;p>傳遞層在討論具體服務前，先定義四個控制面最穩定：&lt;/p>
&lt;ol>
&lt;li>投遞保證模型：哪些流程接受 at-least-once、哪些流程需要更嚴格保證。&lt;/li>
&lt;li>去重與副作用模型：哪些操作必須具備 idempotency，如何界定重複。&lt;/li>
&lt;li>重試與降載模型：重試節奏、上限、退避與壓力保護機制。&lt;/li>
&lt;li>回復與重放模型：DLQ 分流、回放準入條件與結果校正流程。&lt;/li>
&lt;/ol>
&lt;h2 id="多租戶-broker-的隔離邊界">多租戶 broker 的隔離邊界&lt;/h2>
&lt;p>Multi-tenant broker 的隔離邊界承擔「單租戶故障不放大到其他租戶」的責任。Multi-tenant broker 的紅隊重點是跨租戶邊界能否擋住攻擊放大跟資源耗盡。3.1 已建立規模化分層討論、本段聚焦攻擊面跟控制面。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka Infrastructure Evolution&lt;/a> — case 提出方向：定義租戶隔離、配額規則、標準化 topic 治理、平台指標治理。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters&lt;/a> — 規模化分層 cluster、高優先 workload 跟低優先 workload 各自獨立、降低 noisy neighbor 風險。以下攻擊面 taxonomy 基於通用 multi-tenant broker 知識展開、非 case 原文列舉。&lt;/p>
&lt;p>&lt;strong>Multi-tenant broker 的攻擊面&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>配額耗盡&lt;/strong>：單一 tenant 大量 publish 占光 broker bandwidth / storage、其他 tenant 投遞延遲拉長。對應控制是 &lt;em>per-tenant quota&lt;/em> + &lt;em>rate limit&lt;/em>。下游推送 quota 作為硬上限見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 下游推送是隱性瓶頸&lt;/a>&lt;/li>
&lt;li>&lt;strong>Topic 命名衝突 / 越權&lt;/strong>：tenant A 透過命名衝突或缺失 ACL 取得 tenant B topic 存取權限。對應控制是 &lt;em>namespace 強制隔離&lt;/em> + &lt;em>IAM topic-level ACL&lt;/em>&lt;/li>
&lt;li>&lt;strong>DLQ 跨租戶污染&lt;/strong>：tenant A 的 poison message 進共用 DLQ、影響 tenant B 的 DLQ 處理流程。對應控制是 &lt;em>per-tenant DLQ&lt;/em> + &lt;em>獨立排空策略&lt;/em>&lt;/li>
&lt;li>&lt;strong>Consumer group 命名衝突&lt;/strong>：意外或惡意註冊跟其他 tenant 同名的 consumer group、搶 partition 分配。對應控制是 &lt;em>consumer group naming convention&lt;/em> + &lt;em>prefix-based ACL&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>判讀重點：multi-tenant broker 的紅隊不只看 broker 容量是否充足、還要看單一 tenant 出事時其他 tenant 是否受影響。單一租戶事件擴散到其他租戶屬隔離失敗、非 broker 效能問題。&lt;/p></description><content:encoded><![CDATA[<p>傳遞層紅隊判讀的核心目標是確認「訊息如何被重送、重放、放大與耗盡資源」。這裡的紅隊指攻擊者視角的風險檢查：先找可被放大的傳遞路徑，再回推控制面。只要系統採用 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 或 stream，弱點就會同時落在 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>、consumer 容量與回復流程。</p>
<h2 id="判讀傳遞層弱點的主要軸線">【判讀】傳遞層弱點的主要軸線</h2>
<p>傳遞層弱點可分成三條軸線：投遞語意、處理語意、回復語意。投遞語意看 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 與重送條件；處理語意看 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與 side effect；回復語意看 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a> 與 <a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation</a>。</p>
<h2 id="可觀察訊號何時要提高紅隊檢查優先級">【可觀察訊號】何時要提高紅隊檢查優先級</h2>
<p>下列訊號出現時，傳遞層通常需要先做弱點盤點：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 持續增加，且重試量同步升高</li>
<li><a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 累積速度高於排空速度</li>
<li>同一事件會被多路 consumer 讀取並觸發多個下游 side effect</li>
<li>回放流程缺少操作邊界與審核節點</li>
</ul>
<h2 id="失敗代價傳遞層弱點的代價型態">【失敗代價】傳遞層弱點的代價型態</h2>
<p>傳遞層弱點會把局部錯誤放大成系統性壓力。重複投遞會造成重複扣款、重複通知或重複建單；毒訊息會阻塞分區與 worker；重放策略缺少邊界會把歷史事件再次推進生產流程。這些問題的共同代價是資料偏移、事故窗口延長與操作風險上升。</p>
<h2 id="最低控制面進入服務實體前要先定義">【最低控制面】進入服務實體前要先定義</h2>
<p>傳遞層在討論具體服務前，先定義四個控制面最穩定：</p>
<ol>
<li>投遞保證模型：哪些流程接受 at-least-once、哪些流程需要更嚴格保證。</li>
<li>去重與副作用模型：哪些操作必須具備 idempotency，如何界定重複。</li>
<li>重試與降載模型：重試節奏、上限、退避與壓力保護機制。</li>
<li>回復與重放模型：DLQ 分流、回放準入條件與結果校正流程。</li>
</ol>
<h2 id="多租戶-broker-的隔離邊界">多租戶 broker 的隔離邊界</h2>
<p>Multi-tenant broker 的隔離邊界承擔「單租戶故障不放大到其他租戶」的責任。Multi-tenant broker 的紅隊重點是跨租戶邊界能否擋住攻擊放大跟資源耗盡。3.1 已建立規模化分層討論、本段聚焦攻擊面跟控制面。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka Infrastructure Evolution</a> — case 提出方向：定義租戶隔離、配額規則、標準化 topic 治理、平台指標治理。對應 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a> — 規模化分層 cluster、高優先 workload 跟低優先 workload 各自獨立、降低 noisy neighbor 風險。以下攻擊面 taxonomy 基於通用 multi-tenant broker 知識展開、非 case 原文列舉。</p>
<p><strong>Multi-tenant broker 的攻擊面</strong>：</p>
<ul>
<li><strong>配額耗盡</strong>：單一 tenant 大量 publish 占光 broker bandwidth / storage、其他 tenant 投遞延遲拉長。對應控制是 <em>per-tenant quota</em> + <em>rate limit</em>。下游推送 quota 作為硬上限見 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 下游推送是隱性瓶頸</a></li>
<li><strong>Topic 命名衝突 / 越權</strong>：tenant A 透過命名衝突或缺失 ACL 取得 tenant B topic 存取權限。對應控制是 <em>namespace 強制隔離</em> + <em>IAM topic-level ACL</em></li>
<li><strong>DLQ 跨租戶污染</strong>：tenant A 的 poison message 進共用 DLQ、影響 tenant B 的 DLQ 處理流程。對應控制是 <em>per-tenant DLQ</em> + <em>獨立排空策略</em></li>
<li><strong>Consumer group 命名衝突</strong>：意外或惡意註冊跟其他 tenant 同名的 consumer group、搶 partition 分配。對應控制是 <em>consumer group naming convention</em> + <em>prefix-based ACL</em></li>
</ul>
<p>判讀重點：multi-tenant broker 的紅隊不只看 broker 容量是否充足、還要看單一 tenant 出事時其他 tenant 是否受影響。單一租戶事件擴散到其他租戶屬隔離失敗、非 broker 效能問題。</p>
<h2 id="replay-攻擊跟-dlq-濫用">Replay 攻擊跟 DLQ 濫用</h2>
<p>Replay 機制是事故恢復工具、也是攻擊面。攻擊者可能濫用 replay 重複觸發副作用（重複退款、重複送通知、重複下單）、或讓 DLQ 變成 backdoor 通道。以下 3 個攻擊向量基於通用紅隊知識展開、非 case 原文列舉。</p>
<p><strong>Replay 攻擊向量</strong>：</p>
<ul>
<li><strong>未授權 replay 觸發</strong>：攻擊者拿到 replay 控制權、replay 舊事件造成重複副作用。對應控制是 <em>replay 授權需獨立審核</em> + <em>audit trail 記錄誰 replay 什麼</em></li>
<li><strong>Replay window 越界</strong>：replay 跨越 idempotency 紀錄到期、舊事件被當新事件處理。對應控制是 <em>replay window 上限 = idempotency 保留期</em>、見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics 的 replay 跟 idempotency 共設計</a></li>
<li><strong>DLQ message 注入</strong>：攻擊者把惡意 message 直接寫進 DLQ、繞過主通道驗證、等 replay 時觸發副作用。對應控制是 <em>DLQ 寫入權限獨立於主通道</em> + <em>replay 前 schema 重新驗證</em></li>
</ul>
<p>判讀重點：replay 屬 production 操作、跟 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation 修復權限管理</a> 同層級、要 audit trail + 審核流程。合規 replay 路徑應具備 audit trail + window 上限 + DLQ 寫入隔離三層控制、把 replay 從事故工具升級為可稽核的 production 操作。</p>
<h2 id="案例對照">【案例對照】</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>紅隊視角重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka Infrastructure</a></td>
          <td>治理視角、反推 multi-tenant 隔離攻擊面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a></td>
          <td>治理視角、反推分層 cluster 跟 workload 隔離防護</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例 Queue Semantics Mismatch</a></td>
          <td>切換語意誤配引發重複副作用、replay 跟 idempotency 失準</td>
      </tr>
  </tbody>
</table>
<p>以上 3.C6 / 3.C4 屬治理視角案例、紅隊章節做反推使用（從控制面反推攻擊面）。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 03 內部：規模化分層治理回 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker-basics</a>；下游推送 quota 攻擊面跟 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable-queue 下游推送是隱性瓶頸</a> 互補；replay 跟 idempotency 共設計回 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6</a></li>
<li>與 01 的交接：replay / 補償權限管理回 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation 修復權限管理</a></li>
<li>與 04 的交接：紅隊偵測訊號（DLQ 速率、retry storm、duplicate）進 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>與 06 的交接：rule rollout 安全閘門進 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 rule-rollout-safety-gate</a></li>
<li>與 08 的交接：事故當下決策進 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
</ol>
<h2 id="關聯卡片">【關聯卡片】</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/poison-message/" data-link-title="Poison Message" data-link-desc="說明特定訊息內容如何穩定造成 consumer 失敗">Poison Message</a></li>
<li><a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">Duplicate Delivery</a></li>
<li><a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">Retry Storm</a></li>
<li><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure</a></li>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a></li>
</ul>
]]></content:encoded></item><item><title>5.5 平台與入口威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</guid><description>&lt;p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。&lt;/p>
&lt;h2 id="服務環節問題地圖">服務環節問題地圖&lt;/h2>
&lt;p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。&lt;/p>
&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>入口分級與實際可達範圍不一致&lt;/td>
 &lt;td>入口清單與責任鏈要先對齊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生命週期訊號&lt;/td>
 &lt;td>readiness、draining、shutdown 節奏不一致&lt;/td>
 &lt;td>平台合約要先定義再驗證&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定與密鑰下發&lt;/td>
 &lt;td>設定漂移與權限擴張同時發生&lt;/td>
 &lt;td>高風險設定要進 release gate，並分離 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付切換節奏&lt;/td>
 &lt;td>回滾與切換條件不清晰&lt;/td>
 &lt;td>先定停損條件再定交付速度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="入口暴露面">入口暴露面&lt;/h3>
&lt;p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a>。&lt;/p>
&lt;p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>設計意圖內的入口&lt;/strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。&lt;/li>
&lt;li>&lt;strong>隱性入口&lt;/strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。&lt;/li>
&lt;li>&lt;strong>暫態入口&lt;/strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。&lt;/li>
&lt;/ol>
&lt;p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口&lt;/a> 的認證疊法段。&lt;/p>
&lt;h3 id="生命週期訊號">生命週期訊號&lt;/h3>
&lt;p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。&lt;/p>
&lt;p>脆弱窗口的判讀要跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的生命週期狀態對齊：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>startup → readiness 窗口&lt;/strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。&lt;/li>
&lt;li>&lt;strong>readiness → drain 窗口&lt;/strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。&lt;/li>
&lt;li>&lt;strong>drain → shutdown 窗口&lt;/strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。&lt;/li>
&lt;/ul>
&lt;h3 id="設定與密鑰下發">設定與密鑰下發&lt;/h3>
&lt;p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。&lt;/p></description><content:encoded><![CDATA[<p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。</p>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。</p>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>主要問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>入口暴露面</td>
          <td>入口分級與實際可達範圍不一致</td>
          <td>入口清單與責任鏈要先對齊</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a></td>
      </tr>
      <tr>
          <td>生命週期訊號</td>
          <td>readiness、draining、shutdown 節奏不一致</td>
          <td>平台合約要先定義再驗證</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a></td>
      </tr>
      <tr>
          <td>設定與密鑰下發</td>
          <td>設定漂移與權限擴張同時發生</td>
          <td>高風險設定要進 release gate，並分離 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a></td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023</a></td>
      </tr>
      <tr>
          <td>交付切換節奏</td>
          <td>回滾與切換條件不清晰</td>
          <td>先定停損條件再定交付速度</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></td>
      </tr>
  </tbody>
</table>
<h3 id="入口暴露面">入口暴露面</h3>
<p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a>。</p>
<p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：</p>
<ol>
<li><strong>設計意圖內的入口</strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。</li>
<li><strong>隱性入口</strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。</li>
<li><strong>暫態入口</strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。</li>
</ol>
<p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a> 的認證疊法段。</p>
<h3 id="生命週期訊號">生命週期訊號</h3>
<p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。</p>
<p>脆弱窗口的判讀要跟 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的生命週期狀態對齊：</p>
<ul>
<li><strong>startup → readiness 窗口</strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。</li>
<li><strong>readiness → drain 窗口</strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。</li>
<li><strong>drain → shutdown 窗口</strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。</li>
</ul>
<h3 id="設定與密鑰下發">設定與密鑰下發</h3>
<p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。</p>
<p>設定變更的弱點盤點要分兩個方向：</p>
<p><strong>顯式設定變更</strong>（ConfigMap、Secret、feature flag 更新）：變更本身是可追蹤的，弱點在於 review 機制是否涵蓋高風險設定。payment endpoint、auth provider URL、rate limit 閾值、CORS 允許來源——這些設定的變更影響跟程式碼變更等量，應走同等 review 流程。設定變更的 review 與 rollout 策略見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Config Boundary</a>。</p>
<p><strong>隱式設定漂移</strong>（RBAC 逐步放寬、network policy 例外累積、service account 權限擴張）：這類變更是多次小修改累積的結果，單次變更看起來合理但累積後超出安全邊界。盤點方式是定期用 policy-as-code（OPA/Gatekeeper、Kyverno）掃描 cluster 內的 RBAC binding、network policy、pod security 設定，跟 baseline 比對偏移程度。</p>
<h3 id="交付切換節奏">交付切換節奏</h3>
<p>交付切換節奏的弱點判讀是「在不穩定窗口期、系統是否還有防禦能力」。Canary / rollout / rollback 期間 5xx 升高、connection 重建、auth 短暫失敗，會掩蓋同期間的攻擊訊號。沒有先定停損條件就推交付速度、是把切換期變成攻擊期的常見做法。</p>
<p>交付窗口期的防禦能力退化有兩個機制：</p>
<p><strong>訊號淹沒</strong>：rollout 本身產生的短暫錯誤（5xx spike、reconnect、auth retry）跟攻擊訊號長得一樣。事故團隊在切流期把所有異常歸因於部署變更，攻擊者剛好利用這個注意力盲區。對策是把切流期 alert 跟安全 alert 分流到不同 channel，安全訊號走獨立通道、由 security on-call 獨立判讀。</p>
<p><strong>防禦元件版本不一致</strong>：<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 期間新舊版本同時在線，WAF 規則、rate limit 設定、auth middleware 版本可能不同。攻擊者可以針對舊版本的已知弱點送流量，利用 canary 期間的路由特性讓流量到達舊版本。對策是把安全元件的更新跟應用版本解耦——WAF 規則、rate limit 是平台層設定，應在所有版本一致生效。</p>
<h2 id="案例對照表情境--判讀--注意事項--路由章節">案例對照表（情境 → 判讀 → 注意事項 → 路由章節）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>判讀</th>
          <th>注意事項</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外網可達入口在發版後增加</td>
          <td>入口分級與交付節奏存在脫鉤</td>
          <td>入口盤點要成為交付前條件</td>
          <td><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a></td>
      </tr>
      <tr>
          <td>readiness 通過但實際流量錯誤率上升</td>
          <td>生命週期合約與流量模型不一致</td>
          <td>探針、draining、shutdown 要同批驗證</td>
          <td><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判</a></td>
      </tr>
      <tr>
          <td>設定異動與異常事件同時出現</td>
          <td>設定漂移可能已跨越安全邊界</td>
          <td>設定審查與責任追蹤要同步維護</td>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 復盤與改進追蹤</a></td>
      </tr>
      <tr>
          <td>切流期間入侵告警被淹沒</td>
          <td>rollout 噪音掩蓋攻擊訊號</td>
          <td>切流期 alert 分流、攻擊訊號獨立通道</td>
          <td><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a></td>
      </tr>
  </tbody>
</table>
<p>「外網可達入口在發版後增加」是平台變更弱點盤點的頭號議題。Ingress class 換、Service type 改、LB 規則重組都可能讓原本內部服務獲得外部 IP。把入口盤點放進 release pre-check、能讓這類變更在合併前被擋下。</p>
<p>「readiness 通過但實際流量錯誤率上升」揭露 readiness probe 設計失誤的弱點面向。Probe 只回 200 OK 不代表服務可承受真實流量、攻擊者剛好可以在這個窗口送高頻 request 看是否壓垮服務。Readiness 反映依賴就緒條件而非單一探針成功、能縮短這個窗口。</p>
<p>「設定異動與異常事件同時出現」是 config rollout 的弱點風險。Config 變更後出現異常事件、可能是設定本身的問題、也可能是攻擊者剛好利用了設定窗口。Config 審查跟責任追蹤同步維護、能讓事後復盤分辨兩者。</p>
<p>「切流期間入侵告警被淹沒」是新加入的議題。切流產生大量短暫 5xx、reconnect、auth retry、可能淹沒真正的攻擊訊號。把切流期 alert 跟一般 alert 分流、攻擊訊號走獨立通道、能避免攻擊在切流窗口下被忽略。</p>
<h2 id="平台遷移期的攻擊面變動">平台遷移期的攻擊面變動</h2>
<p>對應 5.C1 / 5.C4 / 5.C5 揭露的遷移分段切換流程、本段從弱點盤點角度補充其攻擊面變動風險（case 庫未直接揭露此角度、屬通用工程經驗）。遷移期的職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>、弱點盤點跟治理視角合用才完整。</p>
<p>平台遷移（self-managed → managed、單 cluster → 多 cluster、舊版本 → 新版本）會短期擴大攻擊面、然後逐步收斂。遷移期顯式管理攻擊面變化、避免雙軌期變成攻擊面雙倍期。</p>
<p>可重複套用的弱點判讀：</p>
<ol>
<li><strong>盤點雙軌期入口</strong>：舊平台跟新平台的入口清單分別列出、確認新平台不繼承舊平台已知漏洞、舊平台的廢棄入口確實關閉。</li>
<li><strong>identity / credential 重新對位</strong>：service account、API token、TLS cert 在新平台是否走新的 rotation flow、舊平台的 credential 是否在切換完成後撤除。</li>
<li><strong>observability 對應更新</strong>：新平台的 audit log、access log、security event 是否進入同一個 SIEM / 告警通道、避免遷移期內攻擊訊號掉到觀測缺口。</li>
<li><strong>回退路徑的攻擊面評估</strong>：回退到舊平台時、舊平台是否仍處於最新 patch 狀態、回退本身會不會把已修補的漏洞重新引入。</li>
</ol>
<p>遷移計畫要把資安 review 列為 gate 之一、讓遷移期攻擊面變動進入可見治理流程。沒有這道 gate、遷移期容易被當成純技術項目處理、漏掉攻擊面的隱性擴大。</p>
<h2 id="到實作前的最後一層">到實作前的最後一層</h2>
<p>弱點盤點在概念層回答的是平台風險判讀與交接節奏。當討論進入 Kubernetes 欄位、LB 規則、系統服務參數或腳本配置時，就代表已進入實作層。</p>
<p>實作層的防護驗證跟概念層分工：實作層看具體 YAML / config / rule 是否符合 hardening baseline、概念層看交付路徑跟責任鏈是否完整。兩者都做才能讓平台變更的攻擊面在 release 前可見。</p>
<p>進實作層後接 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護模組</a> 的具體 hardening 章節、跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 對齊入口分級。</p>
]]></content:encoded></item><item><title>6.5 失敗模式預判（Pre-mortem 與 FMEA）</title><link>https://tarrragon.github.io/blog/backend/06-reliability/failure-mode-pre-mortem/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/failure-mode-pre-mortem/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>失敗模式預判是在變更上線前，主動尋找驗證覆蓋的缺口。責任是把「我們漏掉了什麼」從事後驚訝變成事前盤點。&lt;/p>
&lt;p>這一頁處理的是驗證邊界。當某個環節一旦失效就會放大事故，pre-mortem 與 FMEA 的工作是提前把那個環節標出來，讓團隊能在上線前決定是補驗證、收窄範圍還是延後變更。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>驗證缺口的核心問題是變更是否被差異化控制、回復路徑是否經過驗證。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>高風險變更是否有獨立 gate&lt;/li>
&lt;li>負載模型是否包含失敗流量特徵&lt;/li>
&lt;li>故障演練是否覆蓋 partial failure 與連鎖失效&lt;/li>
&lt;li>rollback 與 runbook 是否有時限驗證&lt;/li>
&lt;/ul>
&lt;h2 id="pre-mortem-流程">Pre-mortem 流程&lt;/h2>
&lt;p>Pre-mortem 的核心假設是「這個變更已經在 production 造成事故」，然後反向推導可能的失敗路徑。這個方法的價值在於成本極低（只需要一次結構化討論）但能暴露驗證盲區。&lt;/p>
&lt;p>流程分四步：&lt;/p>
&lt;p>&lt;strong>列出依賴與資料路徑&lt;/strong>：把變更涉及的服務依賴、資料寫入路徑與外部呼叫畫出來。重點是找出「變更直接或間接觸及的系統邊界」，包括 schema、config、依賴服務版本與流量路由。&lt;/p>
&lt;p>&lt;strong>對每條路徑問失敗影響&lt;/strong>：對每條路徑假設失敗，判斷影響範圍。問的是「如果這條路徑斷了 / 慢了 / 回傳錯誤，影響會擴散到哪裡」。影響範圍包含直接依賴方、上游呼叫者、使用者可見行為與資料一致性。&lt;/p>
&lt;p>&lt;strong>判斷現有驗證覆蓋&lt;/strong>：對每條失敗路徑，檢查現有 CI、load test、chaos experiment、contract test 是否能攔住這個失敗。重點是找出「我們認為有覆蓋但實際沒覆蓋」的路徑 — 例如 CI 有 unit test 但沒有 integration test 覆蓋跨服務呼叫，或 load test 有 throughput 驗證但沒有 retry storm 場景。&lt;/p>
&lt;p>&lt;strong>識別驗證缺口並路由&lt;/strong>：未覆蓋的失敗路徑進入兩條路由。上線前能補的缺口回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review&lt;/a>，作為上線前檢查項目。上線前補不了的缺口回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog&lt;/a>，作為可排序的改善項目。&lt;/p>
&lt;p>Pre-mortem 的常見失效是流程走了但結論沒路由。當缺口被列出但沒有 owner、沒有 deadline、沒有連到 readiness review 或 debt backlog，pre-mortem 就只是會議紀錄。&lt;/p>
&lt;h2 id="fmea-分類軸">FMEA 分類軸&lt;/h2>
&lt;p>Failure Mode and Effects Analysis 按失效模式分類驗證缺口。按模式分類的好處是讓團隊能判斷「缺口屬於哪一類」，然後沿對應章節的路由去補。&lt;/p>
&lt;h3 id="gate-failure">Gate failure&lt;/h3>
&lt;p>Release gate 缺少高風險變更的差異化控制。當所有變更走同一條 CI pipeline、同一套 gate 門檻，高風險變更（schema migration、payment path、config rollout）的驗證強度跟日常小改動相同，gate 實質上對高風險變更無效。&lt;/p>
&lt;p>判讀條件：高風險變更是否有獨立的 gate 流程；gate 門檻是否隨變更風險等級調整。&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft 的變更治理實踐&lt;/a>把變更按風險分層，高風險變更需要更嚴的放行條件與更完整的驗證路徑。回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 補差異化門檻。&lt;/p>
&lt;h3 id="load-failure">Load failure&lt;/h3>
&lt;p>Workload model 沒覆蓋失敗流量特徵。壓測模型通常反映正常流量，但事故時的流量形狀完全不同：retry storm 放大請求量、cascade &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 佔住連線、queue backlog 堆積改變消費節奏。當壓測模型只包含正常流量，通過壓測不代表系統能承受失敗流量。&lt;/p>
&lt;p>判讀條件：workload model 是否包含 retry 放大、timeout cascade 與 queue 堆積場景。回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test&lt;/a> 補失敗流量模型。&lt;/p>
&lt;h3 id="recovery-failure">Recovery failure&lt;/h3>
&lt;p>Rollback 或 DR 路徑在事故前沒被驗證過。團隊假設 rollback 可用，但 schema 已經不向下相容；團隊假設 failover 可用，但 failover config 跟 production 已經漂移。recovery failure 的特徵是「有計畫但沒跑過」。&lt;/p>
&lt;p>判讀條件：rollback 是否在過去 90 天被 rehearsal 驗證過；DR failover config 是否跟 production 同步。回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR / rollback rehearsal&lt;/a> 建立定期驗證節奏。&lt;/p>
&lt;h3 id="detection-failure">Detection failure&lt;/h3>
&lt;p>告警延遲或缺失，問題被使用者先發現。當 SLO alert 覆蓋不足、dashboard 缺少關鍵路徑的訊號、或告警門檻設定過寬，團隊的 MTTD（mean time to detect）會拉長到使用者回報之後。detection failure 讓所有下游反應（止血、升級、溝通）都延遲。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>失敗模式預判是在變更上線前，主動尋找驗證覆蓋的缺口。責任是把「我們漏掉了什麼」從事後驚訝變成事前盤點。</p>
<p>這一頁處理的是驗證邊界。當某個環節一旦失效就會放大事故，pre-mortem 與 FMEA 的工作是提前把那個環節標出來，讓團隊能在上線前決定是補驗證、收窄範圍還是延後變更。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>驗證缺口的核心問題是變更是否被差異化控制、回復路徑是否經過驗證。</p>
<p>重點訊號包括：</p>
<ul>
<li>高風險變更是否有獨立 gate</li>
<li>負載模型是否包含失敗流量特徵</li>
<li>故障演練是否覆蓋 partial failure 與連鎖失效</li>
<li>rollback 與 runbook 是否有時限驗證</li>
</ul>
<h2 id="pre-mortem-流程">Pre-mortem 流程</h2>
<p>Pre-mortem 的核心假設是「這個變更已經在 production 造成事故」，然後反向推導可能的失敗路徑。這個方法的價值在於成本極低（只需要一次結構化討論）但能暴露驗證盲區。</p>
<p>流程分四步：</p>
<p><strong>列出依賴與資料路徑</strong>：把變更涉及的服務依賴、資料寫入路徑與外部呼叫畫出來。重點是找出「變更直接或間接觸及的系統邊界」，包括 schema、config、依賴服務版本與流量路由。</p>
<p><strong>對每條路徑問失敗影響</strong>：對每條路徑假設失敗，判斷影響範圍。問的是「如果這條路徑斷了 / 慢了 / 回傳錯誤，影響會擴散到哪裡」。影響範圍包含直接依賴方、上游呼叫者、使用者可見行為與資料一致性。</p>
<p><strong>判斷現有驗證覆蓋</strong>：對每條失敗路徑，檢查現有 CI、load test、chaos experiment、contract test 是否能攔住這個失敗。重點是找出「我們認為有覆蓋但實際沒覆蓋」的路徑 — 例如 CI 有 unit test 但沒有 integration test 覆蓋跨服務呼叫，或 load test 有 throughput 驗證但沒有 retry storm 場景。</p>
<p><strong>識別驗證缺口並路由</strong>：未覆蓋的失敗路徑進入兩條路由。上線前能補的缺口回寫到 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a>，作為上線前檢查項目。上線前補不了的缺口回寫到 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>，作為可排序的改善項目。</p>
<p>Pre-mortem 的常見失效是流程走了但結論沒路由。當缺口被列出但沒有 owner、沒有 deadline、沒有連到 readiness review 或 debt backlog，pre-mortem 就只是會議紀錄。</p>
<h2 id="fmea-分類軸">FMEA 分類軸</h2>
<p>Failure Mode and Effects Analysis 按失效模式分類驗證缺口。按模式分類的好處是讓團隊能判斷「缺口屬於哪一類」，然後沿對應章節的路由去補。</p>
<h3 id="gate-failure">Gate failure</h3>
<p>Release gate 缺少高風險變更的差異化控制。當所有變更走同一條 CI pipeline、同一套 gate 門檻，高風險變更（schema migration、payment path、config rollout）的驗證強度跟日常小改動相同，gate 實質上對高風險變更無效。</p>
<p>判讀條件：高風險變更是否有獨立的 gate 流程；gate 門檻是否隨變更風險等級調整。<a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft 的變更治理實踐</a>把變更按風險分層，高風險變更需要更嚴的放行條件與更完整的驗證路徑。回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 補差異化門檻。</p>
<h3 id="load-failure">Load failure</h3>
<p>Workload model 沒覆蓋失敗流量特徵。壓測模型通常反映正常流量，但事故時的流量形狀完全不同：retry storm 放大請求量、cascade <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 佔住連線、queue backlog 堆積改變消費節奏。當壓測模型只包含正常流量，通過壓測不代表系統能承受失敗流量。</p>
<p>判讀條件：workload model 是否包含 retry 放大、timeout cascade 與 queue 堆積場景。回到 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a> 補失敗流量模型。</p>
<h3 id="recovery-failure">Recovery failure</h3>
<p>Rollback 或 DR 路徑在事故前沒被驗證過。團隊假設 rollback 可用，但 schema 已經不向下相容；團隊假設 failover 可用，但 failover config 跟 production 已經漂移。recovery failure 的特徵是「有計畫但沒跑過」。</p>
<p>判讀條件：rollback 是否在過去 90 天被 rehearsal 驗證過；DR failover config 是否跟 production 同步。回到 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR / rollback rehearsal</a> 建立定期驗證節奏。</p>
<h3 id="detection-failure">Detection failure</h3>
<p>告警延遲或缺失，問題被使用者先發現。當 SLO alert 覆蓋不足、dashboard 缺少關鍵路徑的訊號、或告警門檻設定過寬，團隊的 MTTD（mean time to detect）會拉長到使用者回報之後。detection failure 讓所有下游反應（止血、升級、溝通）都延遲。</p>
<p>判讀條件：關鍵路徑的 MTTD 是否在可接受範圍；SLO alert 是否覆蓋使用者可見的服務承諾。<a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix 的 chaos 實踐</a>把 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 定義放在驗證的第一步 — 沒有穩態定義，告警就無法判斷系統是否偏離正常，detection 變成盲目。回到 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a> 補訊號覆蓋。</p>
<h2 id="失敗模式嚴重度評估">失敗模式嚴重度評估</h2>
<p>FMEA 傳統用 severity × probability × detectability 三軸評估風險優先序。在可靠性驗證的語境中，這三軸可以簡化為可操作判讀：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>判讀問題</th>
          <th>量測方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Severity</td>
          <td>失效的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 有多大</td>
          <td>單服務 / 跨服務 / 跨區 / 跨租戶</td>
      </tr>
      <tr>
          <td>Probability</td>
          <td>這個失效路徑多常被觸及</td>
          <td>變更頻率、歷史事故率、依賴穩定度</td>
      </tr>
      <tr>
          <td>Detectability</td>
          <td>問題被發現需要多久</td>
          <td>MTTD、alert 覆蓋率、synthetic probe 頻率</td>
      </tr>
  </tbody>
</table>
<p>三軸的交叉決定驗證投資順序：high severity + high probability + low detectability 的缺口最先處理。反過來，low severity + low probability 的缺口可以先記錄在 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt</a>，不需要立即補驗證。</p>
<p>嚴重度評估的陷阱是把評分當目標。三軸的責任是排序驗證投資，讓團隊在有限時間內先補最危險的缺口。當評分本身變成需要維護的文件，評估的維護成本會超過它帶來的判讀價值。</p>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>失效分類</th>
          <th>主要問題</th>
          <th>案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Release Gate</td>
          <td>Gate</td>
          <td>高風險變更缺少差異化 gate</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/" data-link-title="7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險" data-link-desc="CI 平台入口被利用後，如何沿著建置與發佈流程擴散供應鏈風險">TeamCity 2023</a></td>
      </tr>
      <tr>
          <td>負載驗證模型</td>
          <td>Load</td>
          <td>測試流量與實際失敗節奏脫鉤</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023</a></td>
      </tr>
      <tr>
          <td>失敗模式演練</td>
          <td>Recovery</td>
          <td>partial failure 與連鎖失效覆蓋不足</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></td>
      </tr>
      <tr>
          <td>回復路徑驗證</td>
          <td>Recovery</td>
          <td>rollback 與 runbook 缺少時限驗證</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023</a></td>
      </tr>
  </tbody>
</table>
<p>TeamCity 案例暴露的是 gate failure：CI 入口本身被繞過時，後續所有 gate 都失效。判讀條件是 CI pipeline 的存取控制是否被納入驗證範圍，而不只是 pipeline 內容。</p>
<p>Change Healthcare 案例暴露的是 recovery failure：事故影響擴散到營運層面時，技術回復完成不代表服務恢復。判讀條件是 DR plan 是否涵蓋跨系統依賴的恢復順序，而不只是單一服務的 rollback。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>失效分類</th>
          <th>判讀</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI 綠燈但線上回滾率上升</td>
          <td>Gate</td>
          <td>gate 覆蓋與實際風險未對齊</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a></td>
      </tr>
      <tr>
          <td>壓測通過但事故時連鎖降速</td>
          <td>Load</td>
          <td>負載模型缺少失敗流量特徵</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a></td>
      </tr>
      <tr>
          <td>演練記錄完整但回復時間偏長</td>
          <td>Recovery</td>
          <td>演練內容與實戰決策節奏不一致</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal</a></td>
      </tr>
      <tr>
          <td>使用者先於告警發現問題</td>
          <td>Detection</td>
          <td>訊號覆蓋不足或門檻過寬</td>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a></td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google 的 error budget 政策</a>把 gate 門檻跟 budget 消耗綁在一起：budget 健康時走正常 gate，budget 快速消耗時提高門檻。這種做法讓 gate failure 的偵測從「事後觀察回滾率」轉成「事前看 budget 消耗趨勢」。</p>
<p><a href="/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">Shopify 的 resiliency matrix</a> 是 FMEA 的制度化形式：service × failure mode 的矩陣，每格填入防護狀態（covered / gap / in-progress），gap 欄直接成為 game day 的演練題目。這種做法讓 FMEA 從一次性盤點變成持續維護的驗證清單。</p>
<h2 id="跟其他章節的整合">跟其他章節的整合</h2>
<p>Pre-mortem 與 FMEA 的產出需要路由到三個下游：</p>
<ul>
<li><a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a>：上線前能補的缺口進入 readiness checklist</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：需要驗證的失敗假設轉成 chaos / load test 的實驗設計</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：上線前補不了的缺口進入可排序的改善 backlog</li>
</ul>
<p>路由清晰度決定 pre-mortem 的實際價值。當缺口被識別但沒有路由到具體章節的具體動作，pre-mortem 就只是風險清單。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高風險變更走一般 gate、無差異化控制</td>
          <td>gate failure — 回到 6.8 確認是否有風險分層</td>
      </tr>
      <tr>
          <td>壓測通過但 production 事故來自 retry/queue</td>
          <td>load failure — workload model 是否涵蓋失敗流量</td>
      </tr>
      <tr>
          <td>rollback 路徑上次驗證超過 90 天</td>
          <td>recovery failure — 回到 6.7 確認 rehearsal 節奏</td>
      </tr>
      <tr>
          <td>事故 MTTD 超過 SLO window</td>
          <td>detection failure — 回到 04 確認 alert 覆蓋與門檻</td>
      </tr>
      <tr>
          <td>pre-mortem 有做但缺口無 owner</td>
          <td>流程失效 — 結論沒路由到 6.19 或 6.21</td>
      </tr>
      <tr>
          <td>FMEA 評分定期更新但驗證沒跟著動</td>
          <td>評估與行動脫鉤 — 評分的責任是排序投資，改完要回寫驗證狀態</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a>：補失敗流量模型（retry / timeout / queue）</li>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR / rollback rehearsal</a>：補回復路徑驗證</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：補高風險變更的差異化 gate</li>
<li><a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a>：pre-mortem 缺口轉成上線前檢查</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：失敗假設轉成實驗設計</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：未修缺口進入可排序 backlog</li>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a>：detection failure 回到訊號覆蓋</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>：FMEA 結論作為 readiness 證據</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理</a>：pre-mortem 假設在事故中被驗證時回寫</li>
</ul>
]]></content:encoded></item><item><title>0.5 流量與資料量評估</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/</guid><description>&lt;p>流量與資料量評估的核心原則是先描述規模形狀，再討論服務能力。平均 QPS、尖峰倍率、資料成長速度、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a>、保留期限與讀寫比例，會直接影響資料庫、快取、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、觀測與部署平台的選型方向。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分平均流量、尖峰流量、burst 與批次流量&lt;/li>
&lt;li>用讀寫比例、hot key 與資料成長辨識瓶頸形狀&lt;/li>
&lt;li>評估資料保留期限與查詢範圍對服務能力的影響&lt;/li>
&lt;li>避免用單一數字描述所有容量問題&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察容量問題通常來自形狀差異">【觀察】容量問題通常來自形狀差異&lt;/h2>
&lt;p>容量評估的第一個問題是「壓力如何出現」。同樣是一千個 request，每秒穩定進來、五秒內全部湧入、集中打同一個商品、或每次都查不同使用者，對系統的壓力完全不同。&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>平均流量&lt;/td>
 &lt;td>平常每秒有多少 request 或 message&lt;/td>
 &lt;td>基礎容量與成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>尖峰倍率&lt;/td>
 &lt;td>尖峰是平均的幾倍，持續多久&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀寫比例&lt;/td>
 &lt;td>讀多、寫多，還是混合交易&lt;/td>
 &lt;td>cache、index、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hot key&lt;/td>
 &lt;td>壓力是否集中在少數 key&lt;/td>
 &lt;td>cache、sharding、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料成長&lt;/td>
 &lt;td>每天新增多少 row、event 或 object&lt;/td>
 &lt;td>storage、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢範圍&lt;/td>
 &lt;td>查最近資料、全量資料，還是任意條件&lt;/td>
 &lt;td>index、search、archive&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保留期限&lt;/td>
 &lt;td>資料要留多久，是否需要 audit&lt;/td>
 &lt;td>cost、lifecycle、compliance&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是評估索引。真正的容量討論要把數字放回產品情境，才能知道需要擴充哪種能力。&lt;/p>
&lt;h2 id="判讀平均流量決定基礎容量">【判讀】平均流量決定基礎容量&lt;/h2>
&lt;p>平均流量的核心用途是估算日常成本與基本容量。穩定 API、背景 worker、資料同步與觀測資料，都需要知道平常每秒會產生多少 request、message、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric 或資料寫入。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>一個 B2B &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS&lt;/a> 白天每秒 50 個 API request，晚上降到每秒 5 個。&lt;/li>
&lt;li>一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> 平均每秒 20 筆事件，但每筆事件會觸發三個下游工作。&lt;/li>
&lt;li>一個即時 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 平均每秒收到 200 筆狀態更新。&lt;/li>
&lt;/ul>
&lt;p>這類評估的陷阱是只看平均值。平均值能估算基礎成本，但它無法說明尖峰、集中 key、批次匯入或下游失敗時的堆積風險。&lt;/p>
&lt;h2 id="判讀尖峰流量決定緩衝與降級策略">【判讀】尖峰流量決定緩衝與降級策略&lt;/h2>
&lt;p>尖峰流量的核心用途是估算系統如何吸收短時間壓力。活動開賣、推播通知、直播開始、月底結帳、第三方批次同步，都可能讓流量在短時間內暴增。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>活動開始後前三分鐘的商品頁瀏覽量是平常的 30 倍。&lt;/li>
&lt;li>推播送出後，大量 client 同時回到 App 查通知列表。&lt;/li>
&lt;li>每天凌晨外部系統一次送入大量資料檔。&lt;/li>
&lt;/ul>
&lt;p>這類評估的陷阱是把尖峰當成一般擴容問題。尖峰可能需要 queue、backpressure、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup&lt;/a>、rate limit、預先產生 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 或降級策略；單純加機器未必能保護資料庫、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 或外部 API。&lt;/p>
&lt;h2 id="判讀讀寫比例決定資料路徑設計">【判讀】讀寫比例決定資料路徑設計&lt;/h2>
&lt;p>讀寫比例的核心用途是判斷主要壓力在讀取、寫入還是交易一致性。讀多系統常需要 cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 或搜尋索引；寫多系統則更關心 transaction、batching、queue、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與資料成長。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>商品頁是讀多寫少，資料可短暫快取。&lt;/li>
&lt;li>訂單建立是寫入與交易一致性重點，狀態轉移要受保護。&lt;/li>
&lt;li>行為分析事件是寫多讀少，讀取通常集中在離線報表或聚合結果。&lt;/li>
&lt;/ul>
&lt;p>這類評估的陷阱是只問資料量。十億筆冷資料和一萬筆每秒被反覆讀寫的熱資料，壓力來源完全不同。讀寫比例要和查詢模式、更新頻率與一致性需求一起看。&lt;/p>
&lt;h2 id="判讀hot-key-會讓平均流量失真">【判讀】hot key 會讓平均流量失真&lt;/h2>
&lt;p>hot key 的核心訊號是壓力集中在少數資料上。即使整體 QPS 看起來正常，單一商品、單一直播間、單一聊天室、單一熱門文章或單一 tenant 也可能打爆特定資料路徑。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>一個熱門商品承接大部分查詢與庫存扣減。&lt;/li>
&lt;li>一個大型直播間同時有大量觀眾接收訊息。&lt;/li>
&lt;li>一個企業 tenant 的使用量遠高於其他 tenant。&lt;/li>
&lt;/ul>
&lt;p>這類評估的陷阱是只做整體水平擴展。hot key 可能需要資料拆分、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 分層、快取策略、讀寫分離、限流或產品層降級；具體做法要等需求形狀確認後再進入服務細節。&lt;/p>
&lt;h2 id="判讀資料成長與保留期限決定長期成本">【判讀】資料成長與保留期限決定長期成本&lt;/h2>
&lt;p>資料成長評估的核心問題是「今天可用的設計，三個月後是否仍可用」。row、event、log、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、object、index 都會成長；不同資料還有不同查詢頻率與保留需求。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>每天新增一百萬筆行為事件，但只查最近七天即時聚合。&lt;/li>
&lt;li>每天新增十萬筆付款紀錄，法規要求保存多年。&lt;/li>
&lt;li>每天產生大量 debug log，但事故排查主要看最近兩週。&lt;/li>
&lt;/ul>
&lt;p>這類評估的陷阱是把所有資料都放進同一個保存策略。正式狀態、audit、分析事件、debug log、trace、使用者上傳檔案需要不同保留期限、查詢方式與封存策略。&lt;/p></description><content:encoded><![CDATA[<p>流量與資料量評估的核心原則是先描述規模形狀，再討論服務能力。平均 QPS、尖峰倍率、資料成長速度、<a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、保留期限與讀寫比例，會直接影響資料庫、快取、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、觀測與部署平台的選型方向。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分平均流量、尖峰流量、burst 與批次流量</li>
<li>用讀寫比例、hot key 與資料成長辨識瓶頸形狀</li>
<li>評估資料保留期限與查詢範圍對服務能力的影響</li>
<li>避免用單一數字描述所有容量問題</li>
</ol>
<hr>
<h2 id="觀察容量問題通常來自形狀差異">【觀察】容量問題通常來自形狀差異</h2>
<p>容量評估的第一個問題是「壓力如何出現」。同樣是一千個 request，每秒穩定進來、五秒內全部湧入、集中打同一個商品、或每次都查不同使用者，對系統的壓力完全不同。</p>
<table>
  <thead>
      <tr>
          <th>評估面向</th>
          <th>需要回答的問題</th>
          <th>常見影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平均流量</td>
          <td>平常每秒有多少 request 或 message</td>
          <td>基礎容量與成本</td>
      </tr>
      <tr>
          <td>尖峰倍率</td>
          <td>尖峰是平均的幾倍，持續多久</td>
          <td><a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>、<a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling</a>、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></td>
      </tr>
      <tr>
          <td>讀寫比例</td>
          <td>讀多、寫多，還是混合交易</td>
          <td>cache、index、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 設計</td>
      </tr>
      <tr>
          <td>hot key</td>
          <td>壓力是否集中在少數 key</td>
          <td>cache、sharding、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a></td>
      </tr>
      <tr>
          <td>資料成長</td>
          <td>每天新增多少 row、event 或 object</td>
          <td>storage、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>、<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a></td>
      </tr>
      <tr>
          <td>查詢範圍</td>
          <td>查最近資料、全量資料，還是任意條件</td>
          <td>index、search、archive</td>
      </tr>
      <tr>
          <td>保留期限</td>
          <td>資料要留多久，是否需要 audit</td>
          <td>cost、lifecycle、compliance</td>
      </tr>
  </tbody>
</table>
<p>這張表是評估索引。真正的容量討論要把數字放回產品情境，才能知道需要擴充哪種能力。</p>
<h2 id="判讀平均流量決定基礎容量">【判讀】平均流量決定基礎容量</h2>
<p>平均流量的核心用途是估算日常成本與基本容量。穩定 API、背景 worker、資料同步與觀測資料，都需要知道平常每秒會產生多少 request、message、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric 或資料寫入。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>一個 B2B <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS</a> 白天每秒 50 個 API request，晚上降到每秒 5 個。</li>
<li>一個 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> 平均每秒 20 筆事件，但每筆事件會觸發三個下游工作。</li>
<li>一個即時 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 平均每秒收到 200 筆狀態更新。</li>
</ul>
<p>這類評估的陷阱是只看平均值。平均值能估算基礎成本，但它無法說明尖峰、集中 key、批次匯入或下游失敗時的堆積風險。</p>
<h2 id="判讀尖峰流量決定緩衝與降級策略">【判讀】尖峰流量決定緩衝與降級策略</h2>
<p>尖峰流量的核心用途是估算系統如何吸收短時間壓力。活動開賣、推播通知、直播開始、月底結帳、第三方批次同步，都可能讓流量在短時間內暴增。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>活動開始後前三分鐘的商品頁瀏覽量是平常的 30 倍。</li>
<li>推播送出後，大量 client 同時回到 App 查通知列表。</li>
<li>每天凌晨外部系統一次送入大量資料檔。</li>
</ul>
<p>這類評估的陷阱是把尖峰當成一般擴容問題。尖峰可能需要 queue、backpressure、<a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a>、rate limit、預先產生 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 或降級策略；單純加機器未必能保護資料庫、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 或外部 API。</p>
<h2 id="判讀讀寫比例決定資料路徑設計">【判讀】讀寫比例決定資料路徑設計</h2>
<p>讀寫比例的核心用途是判斷主要壓力在讀取、寫入還是交易一致性。讀多系統常需要 cache、<a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 或搜尋索引；寫多系統則更關心 transaction、batching、queue、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與資料成長。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>商品頁是讀多寫少，資料可短暫快取。</li>
<li>訂單建立是寫入與交易一致性重點，狀態轉移要受保護。</li>
<li>行為分析事件是寫多讀少，讀取通常集中在離線報表或聚合結果。</li>
</ul>
<p>這類評估的陷阱是只問資料量。十億筆冷資料和一萬筆每秒被反覆讀寫的熱資料，壓力來源完全不同。讀寫比例要和查詢模式、更新頻率與一致性需求一起看。</p>
<h2 id="判讀hot-key-會讓平均流量失真">【判讀】hot key 會讓平均流量失真</h2>
<p>hot key 的核心訊號是壓力集中在少數資料上。即使整體 QPS 看起來正常，單一商品、單一直播間、單一聊天室、單一熱門文章或單一 tenant 也可能打爆特定資料路徑。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>一個熱門商品承接大部分查詢與庫存扣減。</li>
<li>一個大型直播間同時有大量觀眾接收訊息。</li>
<li>一個企業 tenant 的使用量遠高於其他 tenant。</li>
</ul>
<p>這類評估的陷阱是只做整體水平擴展。hot key 可能需要資料拆分、<a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 分層、快取策略、讀寫分離、限流或產品層降級；具體做法要等需求形狀確認後再進入服務細節。</p>
<h2 id="判讀資料成長與保留期限決定長期成本">【判讀】資料成長與保留期限決定長期成本</h2>
<p>資料成長評估的核心問題是「今天可用的設計，三個月後是否仍可用」。row、event、log、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、object、index 都會成長；不同資料還有不同查詢頻率與保留需求。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>每天新增一百萬筆行為事件，但只查最近七天即時聚合。</li>
<li>每天新增十萬筆付款紀錄，法規要求保存多年。</li>
<li>每天產生大量 debug log，但事故排查主要看最近兩週。</li>
</ul>
<p>這類評估的陷阱是把所有資料都放進同一個保存策略。正式狀態、audit、分析事件、debug log、trace、使用者上傳檔案需要不同保留期限、查詢方式與封存策略。</p>
<h2 id="判讀查詢範圍決定索引與讀取模型">【判讀】查詢範圍決定索引與讀取模型</h2>
<p>查詢範圍的核心問題是「使用者或系統實際會怎麼找資料」。查最近十筆、查單一 ID、查某個 tenant、查全文、查任意時間範圍與查聚合報表，需要不同資料模型。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>後台訂單頁主要查單一訂單與最近訂單。</li>
<li>客服系統需要依 email、電話、交易 ID 找到使用者。</li>
<li>分析頁需要依時間、地區、產品線聚合趨勢。</li>
</ul>
<p>這類評估的陷阱是把所有查詢都塞進正式資料庫的單一模型。當查詢體驗、聚合方式或資料保留策略不同時，可能需要 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>、<a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a>、analytics pipeline 或 archive，但這些都應來自明確查詢需求。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入容量與成本實作章節：</p>
<ol>
<li>流量形狀是否明確（平均、尖峰、burst、批次）</li>
<li>主要壓力來源是否明確（讀寫比例、hot key、查詢範圍）</li>
<li>成長假設是否明確（資料新增速度、保留期限、查詢頻率）</li>
<li>容量保護策略是否明確（backpressure、rate limit、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>）</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a>（流量壓力出來後、選擴展軸）</li>
<li><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a>（讀峰值的第一層緩衝）</li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02-cache-redis</a></li>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03-message-queue</a></li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06-reliability</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>流量與資料量評估要描述壓力形狀。平均流量估算基礎容量，尖峰流量決定緩衝與降級，讀寫比例影響資料路徑，hot key 會讓平均值失真，資料成長與保留期限決定長期成本，查詢範圍決定索引與讀取模型。這些資訊補齊後，服務選型才會有可靠依據。</p>
]]></content:encoded></item><item><title>2.5 presence store 與即時狀態</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/</guid><description>&lt;p>在線狀態儲存（presence store）的核心責任是維持短生命週期狀態的可查詢性，例如線上狀態、連線節點、最後活動時間。它屬於即時協調層，與正式帳務資料分層治理。&lt;/p>
&lt;h2 id="狀態模型">狀態模型&lt;/h2>
&lt;p>presence 模型通常包含 &lt;code>subject&lt;/code>、&lt;code>node&lt;/code>、&lt;code>last_seen_at&lt;/code>、&lt;code>ttl&lt;/code>。主體可能是使用者、裝置、連線或工作者。模型設計重點是查詢責任：需要查單一主體是否在線、查群組在線清單，還是查節點負載分布。&lt;/p>
&lt;p>presence 資料具備高變動、短保留特性。設計時應避免把正式業務欄位混入 presence store，讓它保持可快速更新與快速過期。&lt;/p>
&lt;h2 id="heartbeat-與-expiration">heartbeat 與 expiration&lt;/h2>
&lt;p>heartbeat 的責任是維持活性訊號，expiration 的責任是清理失效狀態。heartbeat 間隔太長會放大誤判離線，太短會增加寫入壓力。expiration 視窗要和網路抖動容忍度一起設計。&lt;/p>
&lt;p>穩定做法是定義「可接受延遲在線」窗口，而不是追求絕對即時。presence 判讀通常是近即時近似，不是強一致保證。&lt;/p>
&lt;h2 id="cross-node-query">cross-node query&lt;/h2>
&lt;p>跨節點查詢要先明確一致性需求。聊天室在線名單可容忍短暫不一致；調度系統節點可用性則需要更保守窗口與校驗策略。查詢層應同時提供快取讀取與回源校正路徑，避免單一路徑失真。&lt;/p>
&lt;p>在多區部署中，presence 常採區域內優先、跨區聚合延遲同步。這樣能降低廣域寫入成本，同時保留可接受的全域可見性。&lt;/p>
&lt;h2 id="cleanup-策略">cleanup 策略&lt;/h2>
&lt;p>cleanup 的責任是避免殭屍狀態堆積。定期掃描、lazy cleanup、事件驅動清理可混合使用。清理策略要與業務容忍度對齊：社交場景可容忍秒級延遲清除，調度場景則需更快收斂。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>在線數異常下降但流量未下降&lt;/td>
 &lt;td>heartbeat 發送或寫入路徑中斷&lt;/td>
 &lt;td>檢查 producer 路徑、降級為回源校驗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>離線判斷延遲明顯增加&lt;/td>
 &lt;td>expiration 視窗過長或清理積壓&lt;/td>
 &lt;td>調整 TTL、提高 cleanup 頻率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨節點查詢結果波動大&lt;/td>
 &lt;td>多節點寫入競態與聚合窗口不一致&lt;/td>
 &lt;td>收斂聚合邏輯、加入版本時間戳&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點重啟後出現大量殭屍在線&lt;/td>
 &lt;td>清理與重建流程未對齊&lt;/td>
 &lt;td>啟動全量重整、補啟動時同步清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高峰時段 presence 查詢延遲拉高&lt;/td>
 &lt;td>熱 key 集中與查詢模式不匹配&lt;/td>
 &lt;td>分散 key、按群組分片、加查詢快取&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把在線狀態儲存當正式狀態來源，會讓一致性與修復成本快速上升。presence 模型適合即時協調，最終業務判定仍由正式資料層承擔。&lt;/p>
&lt;p>把 heartbeat 當固定頻率任務，也會造成高峰寫入抖動。頻率應該與線上人數與連線型態一起調整。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>presence 模型可用 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由&lt;/a> 回寫。先看跨區路由如何影響在線可見性，再回到本章檢查 heartbeat 視窗、跨節點聚合與清理節奏是否一致。
這個案例主要支撐的是「跨區可見性與狀態新鮮度」判讀，不直接支撐 lock 租約或 queue 語意；若問題是互斥衝突或重播邊界，應轉到 2.4 或 3.x。&lt;/p>
&lt;p>若區域內在線正常、跨區可見性延遲偏大，先調整跨區同步策略與 fallback 壽命，再把影響評估接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment&lt;/a>。&lt;/p>
&lt;h2 id="跨模組路由">跨模組路由&lt;/h2>
&lt;ol>
&lt;li>與 2.3 的交接：保留與清理策略回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction&lt;/a>。&lt;/li>
&lt;li>與 4.17 的交接：presence 資料品質與延遲偏差回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a>。&lt;/li>
&lt;li>與 6.22 的交接：穩態定義與高峰演練回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">Steady State Definition&lt;/a>。&lt;/li>
&lt;li>與 8.20 的交接：即時狀態誤判造成客戶影響回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">Customer Impact Assessment&lt;/a>。&lt;/li>
&lt;li>與 2.10 的交接：presence 狀態變更如何即時廣播給其他節點回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">Pub/Sub 與即時 fan-out&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要看快取層一致性與失效策略，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略&lt;/a>。要看 presence 狀態變更如何即時扇出給其他節點，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out&lt;/a>。要看跨規模 presence 路由案例，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>在線狀態儲存（presence store）的核心責任是維持短生命週期狀態的可查詢性，例如線上狀態、連線節點、最後活動時間。它屬於即時協調層，與正式帳務資料分層治理。</p>
<h2 id="狀態模型">狀態模型</h2>
<p>presence 模型通常包含 <code>subject</code>、<code>node</code>、<code>last_seen_at</code>、<code>ttl</code>。主體可能是使用者、裝置、連線或工作者。模型設計重點是查詢責任：需要查單一主體是否在線、查群組在線清單，還是查節點負載分布。</p>
<p>presence 資料具備高變動、短保留特性。設計時應避免把正式業務欄位混入 presence store，讓它保持可快速更新與快速過期。</p>
<h2 id="heartbeat-與-expiration">heartbeat 與 expiration</h2>
<p>heartbeat 的責任是維持活性訊號，expiration 的責任是清理失效狀態。heartbeat 間隔太長會放大誤判離線，太短會增加寫入壓力。expiration 視窗要和網路抖動容忍度一起設計。</p>
<p>穩定做法是定義「可接受延遲在線」窗口，而不是追求絕對即時。presence 判讀通常是近即時近似，不是強一致保證。</p>
<h2 id="cross-node-query">cross-node query</h2>
<p>跨節點查詢要先明確一致性需求。聊天室在線名單可容忍短暫不一致；調度系統節點可用性則需要更保守窗口與校驗策略。查詢層應同時提供快取讀取與回源校正路徑，避免單一路徑失真。</p>
<p>在多區部署中，presence 常採區域內優先、跨區聚合延遲同步。這樣能降低廣域寫入成本，同時保留可接受的全域可見性。</p>
<h2 id="cleanup-策略">cleanup 策略</h2>
<p>cleanup 的責任是避免殭屍狀態堆積。定期掃描、lazy cleanup、事件驅動清理可混合使用。清理策略要與業務容忍度對齊：社交場景可容忍秒級延遲清除，調度場景則需更快收斂。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>在線數異常下降但流量未下降</td>
          <td>heartbeat 發送或寫入路徑中斷</td>
          <td>檢查 producer 路徑、降級為回源校驗</td>
      </tr>
      <tr>
          <td>離線判斷延遲明顯增加</td>
          <td>expiration 視窗過長或清理積壓</td>
          <td>調整 TTL、提高 cleanup 頻率</td>
      </tr>
      <tr>
          <td>跨節點查詢結果波動大</td>
          <td>多節點寫入競態與聚合窗口不一致</td>
          <td>收斂聚合邏輯、加入版本時間戳</td>
      </tr>
      <tr>
          <td>節點重啟後出現大量殭屍在線</td>
          <td>清理與重建流程未對齊</td>
          <td>啟動全量重整、補啟動時同步清理</td>
      </tr>
      <tr>
          <td>高峰時段 presence 查詢延遲拉高</td>
          <td>熱 key 集中與查詢模式不匹配</td>
          <td>分散 key、按群組分片、加查詢快取</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把在線狀態儲存當正式狀態來源，會讓一致性與修復成本快速上升。presence 模型適合即時協調，最終業務判定仍由正式資料層承擔。</p>
<p>把 heartbeat 當固定頻率任務，也會造成高峰寫入抖動。頻率應該與線上人數與連線型態一起調整。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>presence 模型可用 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a> 回寫。先看跨區路由如何影響在線可見性，再回到本章檢查 heartbeat 視窗、跨節點聚合與清理節奏是否一致。
這個案例主要支撐的是「跨區可見性與狀態新鮮度」判讀，不直接支撐 lock 租約或 queue 語意；若問題是互斥衝突或重播邊界，應轉到 2.4 或 3.x。</p>
<p>若區域內在線正常、跨區可見性延遲偏大，先調整跨區同步策略與 fallback 壽命，再把影響評估接到 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.3 的交接：保留與清理策略回到 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction</a>。</li>
<li>與 4.17 的交接：presence 資料品質與延遲偏差回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.22 的交接：穩態定義與高峰演練回到 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">Steady State Definition</a>。</li>
<li>與 8.20 的交接：即時狀態誤判造成客戶影響回到 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">Customer Impact Assessment</a>。</li>
<li>與 2.10 的交接：presence 狀態變更如何即時廣播給其他節點回到 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">Pub/Sub 與即時 fan-out</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取層一致性與失效策略，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要看 presence 狀態變更如何即時扇出給其他節點，接著讀 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out</a>。要看跨規模 presence 路由案例，接著讀 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a>。</p>
]]></content:encoded></item><item><title>8.5 復盤與改進追蹤</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/post-incident-review/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/post-incident-review/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>timeline reconstruction&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">rca&lt;/a> method&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a>&lt;/li>
&lt;li>closure criteria&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>timeline 還原靠記憶、不是 log / chat 紀錄&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 停在症狀層、不挖系統性根因&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a> 不清、action items 寫了沒人追、永遠 open&lt;/li>
&lt;li>closure criteria 不清、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 變形式檢查&lt;/li>
&lt;li>同類事故反覆發生、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 學習未跨團隊擴散&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>復盤要包含影響摘要、時間線、根因、有效措施、無效措施、行動項與驗證期限。行動項需要指定 owner、完成標準與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a> 條件，避免停在會議紀錄。&lt;/p>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>04.8 訊號治理閉環：偵測缺口回寫成新訊號&lt;/li>
&lt;li>08.9 事故型態庫：抽象出 pattern&lt;/li>
&lt;li>08.13 repeated / toil：跨事故 pattern 的工程化處理&lt;/li>
&lt;li>08.16 runbook lifecycle：事故後 runbook 修訂&lt;/li>
&lt;li>06.18 reliability metrics：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a> 計算的事件來源&lt;/li>
&lt;li>08.17 security vs operational：證據保全與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 範圍&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog&lt;/a>：復盤 action item 回寫成 reliability debt&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 Chaos Testing&lt;/a>：復盤教訓轉成下一輪 chaos 演練題目&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>timeline reconstruction</li>
<li><a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">rca</a> method</li>
<li><a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a></li>
<li>closure criteria</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>timeline 還原靠記憶、不是 log / chat 紀錄</li>
<li><a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 停在症狀層、不挖系統性根因</li>
<li><a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a> 不清、action items 寫了沒人追、永遠 open</li>
<li>closure criteria 不清、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 變形式檢查</li>
<li>同類事故反覆發生、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 學習未跨團隊擴散</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>復盤要包含影響摘要、時間線、根因、有效措施、無效措施、行動項與驗證期限。行動項需要指定 owner、完成標準與 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a> 條件，避免停在會議紀錄。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.8 訊號治理閉環：偵測缺口回寫成新訊號</li>
<li>08.9 事故型態庫：抽象出 pattern</li>
<li>08.13 repeated / toil：跨事故 pattern 的工程化處理</li>
<li>08.16 runbook lifecycle：事故後 runbook 修訂</li>
<li>06.18 reliability metrics：<a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a> 計算的事件來源</li>
<li>08.17 security vs operational：證據保全與 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 範圍</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a>：復盤 action item 回寫成 reliability debt</li>
<li><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 Chaos Testing</a>：復盤教訓轉成下一輪 chaos 演練題目</li>
</ul>
]]></content:encoded></item><item><title>模組五：部署平台與網路入口</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/</guid><description>&lt;p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>、health / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 檢查與 signal handling；本模組負責平台設定與操作語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &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/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container&lt;/a>&lt;/td>
 &lt;td>image build、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kubernetes&lt;/td>
 &lt;td>deployment、pod lifecycle、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>systemd&lt;/td>
 &lt;td>service unit、restart policy、signal、journal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry&lt;/a>&lt;/td>
 &lt;td>實例如何註冊、更新與摘除&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> discovery、DNS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout&lt;/a>&lt;/td>
 &lt;td>設定如何安全下發到正在運作的服務實例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>&lt;/td>
 &lt;td>environment variable、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN 與邊緣分發&lt;/td>
 &lt;td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;p>章節編號是主題分類，不是閱讀順序。建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。&lt;/p></description><content:encoded><![CDATA[<p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>、health / <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 檢查與 signal handling；本模組負責平台設定與操作語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container</a></td>
          <td>image build、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a>、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a></td>
      </tr>
      <tr>
          <td>Kubernetes</td>
          <td>deployment、pod lifecycle、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a></td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>service unit、restart policy、signal、journal</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer</a></td>
          <td><a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>、<a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry</a></td>
          <td>實例如何註冊、更新與摘除</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery</a></td>
          <td><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery、DNS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout</a></td>
          <td>設定如何安全下發到正在運作的服務實例</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a></td>
          <td>environment variable、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a></td>
      </tr>
      <tr>
          <td>CDN 與邊緣分發</td>
          <td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate</td>
      </tr>
  </tbody>
</table>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<p>章節編號是主題分類，不是閱讀順序。建議先讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。</p>
<h2 id="選型入口">選型入口</h2>
<p>部署平台選型的核心判斷是服務如何被啟動、更新、接流量、擴容與停止。當問題集中在 container image、rolling update、health check、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 或 <a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 時，應先評估部署平台能力。</p>
<p>Container 解決服務包裝與 runtime 依賴；Kubernetes 解決多 instance 調度、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update 與 <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a>；systemd 適合單機或 VM 上的 service lifecycle；<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 解決流量入口、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>；<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a> 解決實例狀態維護；<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 解決服務彼此如何找到 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a>；<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 解決環境差異、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 與 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a>。</p>
<p>接近真實網路服務的例子包括發版時 request 失敗、pod 尚未 ready 就接流量、長連線 shutdown 清理不完整、服務擴容後 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> 更新延遲。這些場景的共同問題是程式與平台合約，因此本模組會先處理生命週期、流量入口與平台訊號。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理程式內的生命週期與訊號。Backend deployment 模組處理 Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 與 <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 平台如何觸發、解讀與限制這些訊號。</p>
<h2 id="與資安概念層的交接">與資安概念層的交接</h2>
<p>本模組承接 07 模組的概念判讀，並在服務實體層落地。交接基線如下：</p>
<ul>
<li>來自 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>：承接入口分級、管理平面分離、修補窗口節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>：承接 TLS/mTLS 與憑證佈署節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>：承接 runtime secret 與機器憑證交付模型。</li>
</ul>
<p>這個交接讓部署模組聚焦實體配置與平台語意，同時保持與資安判讀一致。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>部署平台案例的核心讀法是先確認切換單位（服務、流量、叢集），再定義可回退邊界。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a>、<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3</a></td>
          <td>把零停機遷移拆成分批切流策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：平台整併</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>把多叢集治理收斂成單一控制面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera：managed K8s migration</a></td>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a>、<a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td>把平台重置與服務連續性目標綁定</td>
      </tr>
  </tbody>
</table>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>部署平台使用方式會受語言的啟動時間、process model、signal handling、thread/task lifecycle、runtime memory behavior 與 liveness 支援影響。啟動慢的 runtime 要調整 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與 rollout 節奏；長連線或背景 worker 要支援 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>；使用 GC 的 runtime 要觀察 memory limit 與 pause 行為；多 process 模型要確認 signal、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 如何聚合。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a></td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 與 runtime</td>
          <td>規劃 image、資源限制與啟動行為</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>Kubernetes 部署策略</td>
          <td>了解 deployment、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3</a></td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a></td>
          <td>處理 <a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a></td>
          <td>讓服務能穩定註冊與發現彼此</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5</a></td>
          <td>平台與入口威脅建模（Threat Modeling）</td>
          <td>用隱藏入口、設定漂移與切換風險盤點交付平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6</a></td>
          <td>Platform Lifecycle Contract</td>
          <td>分辨 startup、readiness、liveness、shutdown 與 drain 的責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7</a></td>
          <td>Traffic、Config 與 Control Plane Boundary</td>
          <td>拆分流量、設定、secret、service discovery 與管理面邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8</a></td>
          <td>Deployment Rollout with Drain and Rollback 實作示範</td>
          <td>以 checkout service 示範 canary evidence、drain signal 與 rollback decision</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9</a></td>
          <td>邊緣分發與靜態資源（CDN / Origin Protection）</td>
          <td>把 CDN 視為網路入口層，理解三層快取分工、origin protection、purge 操作模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10</a></td>
          <td>Outbound Tunnel 入口與生命週期（cloudflared / Tailscale）</td>
          <td>把反向隧道視為一種入口形態、理解就緒對齊、network 層故障與認證疊法</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/" data-link-title="模組五案例正文" data-link-desc="部署平台轉換案例入口。">5.C</a></td>
          <td>轉換案例正文</td>
          <td>把平台遷移、整併與流量切換做成可回寫案例</td>
      </tr>
  </tbody>
</table>
<p>反例與規模對照入口： <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> / <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，部署案例要優先保留切流批次、draining、連線生命週期與回退時間。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>部署平台章節下一輪的核心責任是把平台能力寫成服務契約。現有章節已經有 container、Kubernetes、load balancer 與 service discovery，但還需要補上 runtime contract、lifecycle contract、traffic contract、rollout contract 與 control-plane contract 的關係，讓讀者知道部署是一組流量、連線、設定、資源與回退條件的連續切換。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Runtime contract</td>
          <td>image、entrypoint、runtime config 與 resource limit 是否可預期</td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a>、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">runtime config</a></td>
      </tr>
      <tr>
          <td>Lifecycle contract</td>
          <td>startup、readiness、liveness、shutdown 與 drain 是否對齊</td>
          <td><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a></td>
      </tr>
      <tr>
          <td>Traffic contract</td>
          <td>load balancer、timeout、sticky session 與 routing 是否有明確邊界</td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a>、<a href="/blog/backend/knowledge-cards/request-routing/" data-link-title="Request Routing" data-link-desc="說明入口流量如何依規則被導向不同服務或處理路徑">request routing</a></td>
      </tr>
      <tr>
          <td>Rollout contract</td>
          <td>canary、rolling update、config rollout 與 rollback 是否可分批</td>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Control-plane contract</td>
          <td>service discovery、secret delivery 與管理面是否被保護</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>、<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用部署平台自己的服務壓力展開。短 request API、長連線服務、背景 worker、<a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> config push 與多租戶平台的生命週期不同，寫作時要分別處理它們的 rollout 與 drain 條件。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>部署模組的 knowledge card 缺口集中在「平台契約」與「切換完成訊號」。已有 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 與 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 可以作為第一批錨點。</p>
<p>下一批候選卡片包括 startup probe、drain completion、rollout batch、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、config freeze、environment protection 與 deployment contract。這些卡片要讓讀者能分辨「服務已啟動」和「服務可安全接流量」分屬不同責任。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>部署平台的第一條實作路徑是 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback（實作示範）</a>。這篇以 checkout service rollout 為例，說明 rollout plan、canary evidence、drain signal、rollback condition 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用應該是 5.2 Kubernetes deployment、5.3 load balancer contract、<a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>部署路徑的 artifact 對齊重點是「每一批切換都能被觀測、被放行、被回退」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 per-version error rate、latency、drain completion 與 reconnect 訊號；對 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 canary 批次與停損規則；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 freeze、回退與重啟切流的決策條件與時間序列。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">infra 模組五：核心服務上 IaC</a>：ECS / EKS 的 IaC 描述（subnet 接線、IAM task role、映像版本解耦）是部署平台的地基層</li>
</ul>
]]></content:encoded></item><item><title>4.6 SLI 量測與 SLO 訊號設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>SLI 設計起點：user-journey 而非 system metric&lt;/li>
&lt;li>量測點選擇：edge / gateway / service / dependency 各自代表什麼&lt;/li>
&lt;li>Ratio metric vs latency &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a>：何時用哪種&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate&lt;/a> 訊號：multi-window multi-burn-rate alert&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget&lt;/a> 計算所需的 metric 結構&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics&lt;/a> 的分工：4.2 是 counter/gauge/histogram 基礎、4.6 是 SLI 化的設計&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 的分工：4.4 是 alert 規則治理、4.6 是 alert 的訊號源頭&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>SLI 訊號設計是把可靠性目標轉成可量測資料的步驟，責任是讓 SLO 政策建立在使用者旅程與服務結果上。&lt;/p>
&lt;p>CPU、memory、queue depth 可以提供系統背景，但 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 需要回答的是使用者層面的問題：request 是否成功、回應是否夠快、結果是否正確。SLI 量測的位置跟算式決定了 SLO 反映的是「使用者體驗」還是「基礎設施健康」— 兩者的判讀意義不同。&lt;/p>
&lt;p>本章處理的是 metric 到 SLI 的轉換。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2&lt;/a> 定義 counter / gauge / histogram 的基礎型別；本章定義怎麼用這些型別組出代表使用者體驗的 SLI，並設計 burn rate alert 的訊號結構。SLO 政策本身（error budget freeze、release gate 決策）由 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策&lt;/a> 處理。&lt;/p>
&lt;h2 id="sli-設計起點user-journey">SLI 設計起點：User Journey&lt;/h2>
&lt;h3 id="從使用者操作推導-sli">從使用者操作推導 SLI&lt;/h3>
&lt;p>SLI 的設計起點是「使用者在做什麼、期待什麼結果」，不是「系統有什麼 metric 可以用」。&lt;/p>
&lt;p>一個 checkout 流程的使用者期待：request 成功（不會看到 error page）、回應夠快（不會等超過 3 秒）、結果正確（扣款金額正確）。對應三種 SLI：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Availability SLI&lt;/strong>：成功 request 的比例（&lt;code>successful_requests / total_requests&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Latency SLI&lt;/strong>：回應時間在閾值內的比例（&lt;code>requests_under_3s / total_requests&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Correctness SLI&lt;/strong>：結果正確的比例（需要業務邏輯判定，通常用特定 error code 或 reconciliation 結果）&lt;/li>
&lt;/ul>
&lt;p>每個 user journey 不需要三種 SLI 都有。Checkout 的 availability 跟 latency 是核心；correctness 靠事後對帳驗證。搜尋頁面的 latency 比 availability 更關鍵 — 使用者容忍偶發的「搜不到結果」但不容忍 5 秒的載入。&lt;/p>
&lt;h3 id="system-metric-跟-sli-的差異">System metric 跟 SLI 的差異&lt;/h3>
&lt;p>CPU &amp;gt; 90% 不是 SLI — 它是 cause signal。CPU 高但 latency 正常，使用者沒受影響。Disk usage &amp;gt; 85% 也不是 SLI — 它是 capacity signal，需要處理但不代表當下使用者體驗退化。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>SLI 設計起點：user-journey 而非 system metric</li>
<li>量測點選擇：edge / gateway / service / dependency 各自代表什麼</li>
<li>Ratio metric vs latency <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a>：何時用哪種</li>
<li><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 訊號：multi-window multi-burn-rate alert</li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget</a> 計算所需的 metric 結構</li>
<li>跟 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a> 的分工：4.2 是 counter/gauge/histogram 基礎、4.6 是 SLI 化的設計</li>
<li>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 的分工：4.4 是 alert 規則治理、4.6 是 alert 的訊號源頭</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>SLI 訊號設計是把可靠性目標轉成可量測資料的步驟，責任是讓 SLO 政策建立在使用者旅程與服務結果上。</p>
<p>CPU、memory、queue depth 可以提供系統背景，但 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 需要回答的是使用者層面的問題：request 是否成功、回應是否夠快、結果是否正確。SLI 量測的位置跟算式決定了 SLO 反映的是「使用者體驗」還是「基礎設施健康」— 兩者的判讀意義不同。</p>
<p>本章處理的是 metric 到 SLI 的轉換。<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a> 定義 counter / gauge / histogram 的基礎型別；本章定義怎麼用這些型別組出代表使用者體驗的 SLI，並設計 burn rate alert 的訊號結構。SLO 政策本身（error budget freeze、release gate 決策）由 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策</a> 處理。</p>
<h2 id="sli-設計起點user-journey">SLI 設計起點：User Journey</h2>
<h3 id="從使用者操作推導-sli">從使用者操作推導 SLI</h3>
<p>SLI 的設計起點是「使用者在做什麼、期待什麼結果」，不是「系統有什麼 metric 可以用」。</p>
<p>一個 checkout 流程的使用者期待：request 成功（不會看到 error page）、回應夠快（不會等超過 3 秒）、結果正確（扣款金額正確）。對應三種 SLI：</p>
<ul>
<li><strong>Availability SLI</strong>：成功 request 的比例（<code>successful_requests / total_requests</code>）</li>
<li><strong>Latency SLI</strong>：回應時間在閾值內的比例（<code>requests_under_3s / total_requests</code>）</li>
<li><strong>Correctness SLI</strong>：結果正確的比例（需要業務邏輯判定，通常用特定 error code 或 reconciliation 結果）</li>
</ul>
<p>每個 user journey 不需要三種 SLI 都有。Checkout 的 availability 跟 latency 是核心；correctness 靠事後對帳驗證。搜尋頁面的 latency 比 availability 更關鍵 — 使用者容忍偶發的「搜不到結果」但不容忍 5 秒的載入。</p>
<h3 id="system-metric-跟-sli-的差異">System metric 跟 SLI 的差異</h3>
<p>CPU &gt; 90% 不是 SLI — 它是 cause signal。CPU 高但 latency 正常，使用者沒受影響。Disk usage &gt; 85% 也不是 SLI — 它是 capacity signal，需要處理但不代表當下使用者體驗退化。</p>
<p>System metric 的價值在 root cause analysis，不在 SLI。事故中先看 SLI 判斷「使用者是否受影響」，確認受影響後再看 system metric 判斷「原因是什麼」。把 system metric 當 SLI 會讓 SLO 反映基礎設施噪音而非使用者體驗。</p>
<h2 id="量測點選擇">量測點選擇</h2>
<p>SLI 的量測點影響「看到的是誰的觀點」。同一個 request 在不同位置量測會得到不同的 latency 跟 success rate。</p>
<h3 id="edge--load-balancer">Edge / Load Balancer</h3>
<p>最貼近使用者的量測點。量到的 latency 包含 network round-trip + TLS handshake + 所有 backend 處理時間。Availability 反映的是使用者實際看到的 success rate（包含 load balancer 自身的 502/503）。</p>
<p>優點是最能代表使用者體驗。缺點是 load balancer 的 metric 粒度有限 — 通常只有 status code 跟 latency，不帶 service-level 的維度切分。</p>
<h3 id="api-gateway">API Gateway</h3>
<p>比 edge 更有應用層上下文。可以按 route / method / tenant 切分 SLI。量到的 latency 不含 network round-trip（已經進入服務網路），但包含 authentication、rate limiting 跟所有下游處理。</p>
<p>API gateway 是多數團隊的 SLI 量測起點 — 粒度足夠、位置夠近使用者、通常已有 instrumentation。</p>
<h3 id="service-level">Service level</h3>
<p>每個服務的 handler-level metric。可以看到單一服務的 latency 跟 error rate，但不含上下游的影響。適合做 service-level SLO（「order service 的 p99 latency &lt; 200ms」），但不直接代表 user-journey SLO。</p>
<p>Service-level SLI 的價值在於 SLO 階層化 — user-journey SLO 拆分成每個服務的 SLO，事故時能快速定位是哪個服務的 SLO 被打破。</p>
<h3 id="dependency-level">Dependency level</h3>
<p>量測外部依賴（database、cache、third-party API）的回應時間跟 error rate。Dependency metric 的角色是 SLI 退化時的歸因訊號，用來追溯因果鏈而非直接代表使用者體驗。Database latency 上升 → service latency 上升 → user-journey latency SLO 被打破 — dependency metric 幫助追溯因果鏈。</p>
<h2 id="sli-的-metric-結構">SLI 的 Metric 結構</h2>
<h3 id="ratio-metricavailability-跟-correctness">Ratio metric：availability 跟 correctness</h3>
<p>Availability SLI 的 metric 結構需要兩個 counter：total requests 跟 successful requests（或 failed requests）。SLI = good / total。</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"># Availability SLI
</span></span><span class="line"><span class="ln">2</span><span class="cl">http_requests_total{service=&#34;checkout&#34;, status=&#34;2xx&#34;} / http_requests_total{service=&#34;checkout&#34;}</span></span></code></pre></div><p>定義「good」的邊界需要明確。5xx 算 bad，4xx 呢？Client error（400）通常不算服務失敗；authentication failure（401/403）也不算。但 429（rate limit）可能代表服務容量不足，視情境可能算 bad。這個邊界要在 SLI 定義時明確寫下來。</p>
<h3 id="latency-metricthreshold-based-ratio">Latency metric：threshold-based ratio</h3>
<p>Latency SLI 用 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 量測，SLI 值是「在閾值內的 request 比例」。</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"># Latency SLI：p99 &lt; 500ms 的比例
</span></span><span class="line"><span class="ln">2</span><span class="cl">histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=&#34;checkout&#34;}[5m])) &lt; 0.5
</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"># 或用 ratio 形式
</span></span><span class="line"><span class="ln">5</span><span class="cl">sum(rate(http_request_duration_seconds_bucket{le=&#34;0.5&#34;,service=&#34;checkout&#34;}[5m]))
</span></span><span class="line"><span class="ln">6</span><span class="cl">/ sum(rate(http_request_duration_seconds_count{service=&#34;checkout&#34;}[5m]))</span></span></code></pre></div><p>Latency 閾值的選擇要對齊使用者期待而非系統能力。使用者期待 checkout 在 3 秒內完成 — 這是閾值的來源，不是「系統平均 latency 是 200ms 所以閾值設 500ms」。</p>
<h3 id="label-設計">Label 設計</h3>
<p>SLI metric 的 label 需要足夠的切分能力（by service、by endpoint、by tenant），但受 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> 預算約束。</p>
<p>最小 label set：service name + method（GET/POST）+ status class（2xx/4xx/5xx）。這組 label 支撐 service-level SLO 計算。</p>
<p>擴展 label：endpoint path（normalize 後，例如 <code>/api/orders/{id}</code> → <code>/api/orders/:id</code>）、tenant（多租戶場景）。每增加一個 label 維度，series 數量乘法增長 — 在 <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> 的 label 白名單中管理。</p>
<h2 id="burn-rate-與-multi-window-alert">Burn Rate 與 Multi-window Alert</h2>
<h3 id="burn-rate-的概念">Burn rate 的概念</h3>
<p><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 是「error budget 被消耗的速度」。Burn rate = 1 代表按 SLO 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。</p>
<p>用 burn rate alert 取代固定閾值 alert 的好處：burn rate 自動適應流量。低流量時段的幾筆 error 可能 burn rate 很低（因為 total 也少、對 error budget 影響小）；高流量時段的相同 error rate 可能 burn rate 很高（因為 total 多、影響的使用者量大）。</p>
<h3 id="multi-window-multi-burn-rate">Multi-window multi-burn-rate</h3>
<p>單一時間窗口的 burn rate alert 會太吵（短窗口）或太晚（長窗口）。Multi-window 策略組合兩者：</p>
<table>
  <thead>
      <tr>
          <th>視窗組合</th>
          <th>Burn rate 閾值</th>
          <th>偵測速度</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5min + 1hr</td>
          <td>14.4x</td>
          <td>快</td>
          <td>急性問題、page</td>
      </tr>
      <tr>
          <td>30min + 6hr</td>
          <td>6x</td>
          <td>中</td>
          <td>持續退化</td>
      </tr>
      <tr>
          <td>2hr + 3day</td>
          <td>1x</td>
          <td>慢</td>
          <td>慢性消耗</td>
      </tr>
  </tbody>
</table>
<p>14.4x 的來源：若 SLO 週期是 30 天、要在 1 小時內偵測到會耗盡 2% error budget 的問題，burn rate = (30 × 24) / 1 × 0.02 ≈ 14.4。6x 跟 1x 依此邏輯調整消耗比例跟偵測窗口。</p>
<p>短窗口（5min）抓急性：error rate 突然飆高、burn rate 衝到 14.4x。長窗口（1hr）做確認：退化確實持續、排除瞬間 spike。兩個窗口都超過閾值才觸發 alert，減少單一 spike 的 false alarm。</p>
<h3 id="recording-rule-支撐-burn-rate-計算">Recording rule 支撐 burn rate 計算</h3>
<p>Burn rate 的計算涉及多個時間窗口的 ratio metric。每次 alert evaluate 都重算會給 TSDB 帶來查詢壓力。用 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 把每個窗口的 error ratio 預計算，alert rule 讀 recording rule 的輸出：</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"># Recording rule：5 分鐘窗口的 error ratio
</span></span><span class="line"><span class="ln">2</span><span class="cl">- record: slo:checkout:error_ratio:rate5m
</span></span><span class="line"><span class="ln">3</span><span class="cl">  expr: sum(rate(http_requests_total{service=&#34;checkout&#34;,status=~&#34;5..&#34;}[5m]))
</span></span><span class="line"><span class="ln">4</span><span class="cl">      / sum(rate(http_requests_total{service=&#34;checkout&#34;}[5m]))</span></span></code></pre></div><p>Alert rule 讀 recording rule 比每次重算 raw series 高效，也讓 burn rate 的計算邏輯集中管理。</p>
<h2 id="error-budget-的-metric-結構">Error Budget 的 Metric 結構</h2>
<p><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget</a> 是 SLO 週期內允許的錯誤量。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗。Error budget = total requests × 0.001。</p>
<p>Error budget 的 metric 結構需要：</p>
<ul>
<li><strong>Total requests（rolling window）</strong>：過去 30 天的 total request count</li>
<li><strong>Failed requests（rolling window）</strong>：過去 30 天的 failed request count</li>
<li><strong>Budget consumed</strong>：failed / (total × (1 - SLO target))</li>
<li><strong>Budget remaining</strong>：1 - budget consumed</li>
</ul>
<p>Budget remaining 作為 dashboard panel 跟 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 的輸入 — 餘額低於閾值時 freeze deployment。這個計算的 rolling window 用 recording rule 維護，避免每次查詢掃描 30 天的 raw data。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 SLI 設計時，先看量測點是否貼近使用者，再看算式是否能穩定支援 error budget。</p>
<p>重點訊號包括：</p>
<ul>
<li>Edge / gateway / service / dependency 的量測點是否各自有清楚責任</li>
<li>Latency percentile 與 ratio metric 是否對應不同使用者體驗</li>
<li><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 是否使用多時間窗，避免太吵或太晚</li>
<li>SLI label 是否有足夠切分能力，同時受 cardinality 預算約束</li>
<li>Error budget 的 rolling window 是否用 recording rule 維護</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 用 system metric（CPU / memory）而非 user-facing 訊號</li>
<li>Burn rate 只有單窗、噪音多或偵測太晚</li>
<li>SLI 計算用平均、不用 percentile</li>
<li>Error budget 算式分母不穩（流量低時誤觸發、高時稀釋）</li>
<li>SLI 量測點離使用者太遠（內部 service 而非 edge/gateway）</li>
<li>SLI 沒有定義「什麼算 good request」的邊界（4xx 算不算 bad）</li>
<li>Burn rate 計算每次重算 raw series、沒有 recording rule</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>System metric 當 SLI</td>
          <td>CPU/memory alert 頻繁但使用者沒受影響</td>
          <td>改用 user-facing ratio / latency SLI</td>
      </tr>
      <tr>
          <td>Burn rate 單窗</td>
          <td>短窗太吵或長窗太晚、alert 價值低</td>
          <td>組合 5min+1hr / 30min+6hr 多窗策略</td>
      </tr>
      <tr>
          <td>SLI 用 average latency</td>
          <td>Tail latency 被掩蓋、p99 使用者體驗失真</td>
          <td>改用 histogram percentile</td>
      </tr>
      <tr>
          <td>Good request 邊界不明</td>
          <td>4xx 算不算 bad、SLI 值忽高忽低</td>
          <td>明確定義 good/bad 分類、寫進 SLI spec</td>
      </tr>
      <tr>
          <td>Error budget 無 rolling</td>
          <td>月初 budget 就耗盡、剩下 20 天沒有保護機制</td>
          <td>用 rolling window 持續計算、預警消耗速度</td>
      </tr>
      <tr>
          <td>SLI label 無界</td>
          <td>每個 URL path 都是獨立 SLI、series 爆炸</td>
          <td>Normalize path、label 白名單、cardinality 預算</td>
      </tr>
      <tr>
          <td>SLO 無 owner</td>
          <td>沒人維護 SLI 定義跟閾值、退化時無人負責</td>
          <td>每個 SLO 帶 owner、定期審視</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：counter / gauge / histogram 基礎型別</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：burn rate alert 的 noise control 跟 runbook</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 / cost</a>：SLI metric 的 cardinality 預算</li>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 client-side / RUM</a>：user-journey-centric SLI 的前端訊號來源</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rule 支撐 burn rate 計算</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策</a>：error budget 餘額作為 freeze 條件</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.8 release gate</a>：burn rate 觸發 freeze</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.1 incident severity</a>：burn rate 對應 severity 門檻</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：跟 SLO threshold 的訊號分工</li>
</ul>
]]></content:encoded></item><item><title>KeyDB</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/</guid><description>&lt;p>KeyDB 是 Redis 的 multi-threaded fork、承擔三個責任：把 Redis 的命令執行從單執行緒改成多執行緒（不只 I/O、連命令處理都多核）、提供 active-active 多主複製（兩個 master 互相同步、都可寫）、維持 Redis protocol 相容（drop-in 替換）。設計取捨偏向「沿用 Redis 生態 + 單實例榨多核 + 多主寫入」、是 Redis 單執行緒撞牆但又不想重寫 client 的中間選項。&lt;/p>
&lt;p>對「單 key 極熱、Redis Cluster 切不開、需要單實例多執行緒撐單 partition」這條路徑、KeyDB 是值得評估的 fork。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 GCP 上用 KeyDB&lt;/a> 是這條路線最大的公開採用者——但要注意該案例的主因是 multi-cloud 架構下的 cross-cloud latency 治理（把 cache 跟 application 放同一個 cloud），KeyDB 的 multi-threaded 單實例吞吐是附帶優勢、不是 Snap 採用的主要驅動。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 KeyDB、用 redis-cli 驗證 protocol 相容&lt;/li>
&lt;li>評估 multi-threaded 命令執行跟 Redis I/O threads 的差異&lt;/li>
&lt;li>判斷 active-active 多主複製適用與衝突風險&lt;/li>
&lt;li>評估 KeyDB on FLASH 對大 dataset 的成本意義&lt;/li>
&lt;li>區分 KeyDB 跟 DragonflyDB / Redis Cluster 的選用判讀，並評估 Snap 收購後的治理風險&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-keydb-跑起來">最短路徑：5 分鐘把 KeyDB 跑起來&lt;/h2>





&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"># 1. 啟動 KeyDB（--server-threads 開多執行緒、命令執行也多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name keydb -p 6379:6379 &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> eqalpha/keydb keydb-server --server-threads &lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 redis-cli 驗證（KeyDB 100% Redis protocol 相容）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">redis-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">redis-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本（KeyDB 回報 redis_version、client 以此判斷相容性）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|redis_mode&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:6.3.4 ← KeyDB 的版本方案、client library 以此協商相容&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_mode:standalone&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；&lt;code>--server-threads&lt;/code> 是啟動參數（不在 &lt;code>CONFIG GET&lt;/code> 內、改值要重啟）。多主複製見&lt;a href="#active-active-%e5%a4%9a%e4%b8%bb%e8%a4%87%e8%a3%bd">進階主題&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>直接用 redis-cli / 所有 Redis client library（KeyDB 維持 Redis protocol）&lt;/li>
&lt;li>&lt;code>--server-threads N&lt;/code> 設命令執行的執行緒數、對齊 CPU 核數&lt;/li>
&lt;li>&lt;code>INFO server&lt;/code> 確認 redis_version（KeyDB 的版本對應 Redis 哪個 base）&lt;/li>
&lt;/ul>
&lt;h3 id="multi-threaded-命令執行">Multi-threaded 命令執行&lt;/h3>
&lt;p>KeyDB 跟 Redis I/O threads 的差異是核心賣點。子議題：&lt;/p></description><content:encoded><![CDATA[<p>KeyDB 是 Redis 的 multi-threaded fork、承擔三個責任：把 Redis 的命令執行從單執行緒改成多執行緒（不只 I/O、連命令處理都多核）、提供 active-active 多主複製（兩個 master 互相同步、都可寫）、維持 Redis protocol 相容（drop-in 替換）。設計取捨偏向「沿用 Redis 生態 + 單實例榨多核 + 多主寫入」、是 Redis 單執行緒撞牆但又不想重寫 client 的中間選項。</p>
<p>對「單 key 極熱、Redis Cluster 切不開、需要單實例多執行緒撐單 partition」這條路徑、KeyDB 是值得評估的 fork。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 GCP 上用 KeyDB</a> 是這條路線最大的公開採用者——但要注意該案例的主因是 multi-cloud 架構下的 cross-cloud latency 治理（把 cache 跟 application 放同一個 cloud），KeyDB 的 multi-threaded 單實例吞吐是附帶優勢、不是 Snap 採用的主要驅動。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 KeyDB、用 redis-cli 驗證 protocol 相容</li>
<li>評估 multi-threaded 命令執行跟 Redis I/O threads 的差異</li>
<li>判斷 active-active 多主複製適用與衝突風險</li>
<li>評估 KeyDB on FLASH 對大 dataset 的成本意義</li>
<li>區分 KeyDB 跟 DragonflyDB / Redis Cluster 的選用判讀，並評估 Snap 收購後的治理風險</li>
</ol>
<h2 id="最短路徑5-分鐘把-keydb-跑起來">最短路徑：5 分鐘把 KeyDB 跑起來</h2>





<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. 啟動 KeyDB（--server-threads 開多執行緒、命令執行也多核）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name keydb -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --server-threads <span class="m">4</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="c1"># 2. 用 redis-cli 驗證（KeyDB 100% Redis protocol 相容）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli SET foo bar    <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli GET foo        <span class="c1"># → bar</span>
</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="c1"># 3. 確認版本（KeyDB 回報 redis_version、client 以此判斷相容性）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|redis_mode&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># redis_version:6.3.4    ← KeyDB 的版本方案、client library 以此協商相容</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># redis_mode:standalone</span></span></span></code></pre></div><p>實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；<code>--server-threads</code> 是啟動參數（不在 <code>CONFIG GET</code> 內、改值要重啟）。多主複製見<a href="#active-active-%e5%a4%9a%e4%b8%bb%e8%a4%87%e8%a3%bd">進階主題</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>直接用 redis-cli / 所有 Redis client library（KeyDB 維持 Redis protocol）</li>
<li><code>--server-threads N</code> 設命令執行的執行緒數、對齊 CPU 核數</li>
<li><code>INFO server</code> 確認 redis_version（KeyDB 的版本對應 Redis 哪個 base）</li>
</ul>
<h3 id="multi-threaded-命令執行">Multi-threaded 命令執行</h3>
<p>KeyDB 跟 Redis I/O threads 的差異是核心賣點。子議題：</p>
<ul>
<li>Redis 6+ 的 I/O threads 只分擔 socket 讀寫、命令仍在 main thread；KeyDB 連命令執行都多執行緒</li>
<li><code>--server-threads</code> 對齊核數、單實例吞吐隨核數擴展</li>
<li>多執行緒下單 key 的並發保護由 KeyDB 內部處理、application 端語意不變</li>
</ul>
<h3 id="active-active-多主複製">Active-active 多主複製</h3>
<p>子議題：</p>
<ul>
<li>兩個（含以上）KeyDB master 互相複製、都可接受寫入</li>
<li>衝突解決用 last-write-wins（依時間戳）、不是強一致</li>
<li>適合跨 AZ / 跨 region 的讀寫就近、但要接受最終一致與衝突覆蓋風險</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="active-active-多主複製-1">Active-active 多主複製</h3>
<p>子議題：</p>
<ul>
<li><code>replicaof</code> + <code>active-replica yes</code> 開雙向複製</li>
<li>衝突語意：同 key 並發寫入、last-write-wins、可能丟其中一側的寫入</li>
<li>適用：跨區讀寫就近、可容忍最終一致的 cache；不適用：需要強一致的 counter / lock</li>
</ul>
<h3 id="keydb-on-flash">KeyDB on FLASH</h3>
<p>子議題：</p>
<ul>
<li>把冷資料放 SSD、熱資料留記憶體、降低大 dataset 的記憶體成本</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">Meta CacheLib + Kangaroo</a> 的 DRAM + flash 分層思路</li>
<li>代價：FLASH 路徑延遲高於純記憶體、適合冷熱分明的 workload</li>
</ul>
<h3 id="跟-dragonflydb--garnet-的對比">跟 DragonflyDB / Garnet 的對比</h3>
<p>子議題：</p>
<ul>
<li>KeyDB：Redis fork（沿用 Redis code base、相容度高、base 版本較舊）</li>
<li>DragonflyDB：C++ 從零重寫（架構更激進、shared-nothing、相容核心但非 fork）</li>
<li>Garnet（Microsoft）：研究型高吞吐 store、生態淺</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/" data-link-title="DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster" data-link-desc="Redis 要靠 Cluster 分片才能用滿一台多核機器，DragonflyDB 賭的是相反方向——單一進程 thread-per-core、shared-nothing、把單機推到 Redis 要好幾個 shard 才達到的規模。本文展開 thread-per-core 與 dashtable 的架構、fork-less snapshot、5 個把架構假設寫成 production 事故的踩坑，以及 scale-up 撞牆該回 Cluster 的邊界">DragonflyDB 多核架構 deep article</a> 的 fork vs 重寫光譜</li>
</ul>
<h3 id="治理風險snap-收購後">治理風險（Snap 收購後）</h3>
<p>子議題：</p>
<ul>
<li>KeyDB 公司 2022 年被 Snap 收購、開源版本的後續投入與 roadmap 不確定</li>
<li>評估採用前確認專案活躍度（commit 頻率、release cadence）</li>
<li>對長期依賴敏感的場景、Redis fork 光譜上的 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Linux Foundation 治理）治理更穩</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="多執行緒下吞吐沒提升">多執行緒下吞吐沒提升</h3>
<p>操作原則：先確認 <code>--server-threads</code> 對齊 CPU 核數、再看是否 CPU 密集 workload。判讀：thread &lt; core → 沒用滿多核；單 key 極熱 → 仍受單 partition 限制。</p>
<h3 id="active-active-衝突丟資料">Active-active 衝突丟資料</h3>
<p>操作原則：last-write-wins 下並發寫同 key 會覆蓋。判讀：跨區同 key 高頻寫入要改設計（key 分區到不同 master）、或改用強一致儲存。</p>
<h3 id="protocol-相容問題">Protocol 相容問題</h3>
<p>操作原則：KeyDB base 版本較舊（redis_version 6.x），用到 Redis 7+ 新命令會不支援。判讀：<code>INFO server</code> 確認 base 版本、對照 application 用到的命令。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>要最新 Redis 功能 / 治理穩定</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Linux Foundation、跟上 Redis）</td>
      </tr>
      <tr>
          <td>更激進的多核 / 記憶體效率</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（重寫、shared-nothing）</td>
      </tr>
      <tr>
          <td>需要 Redis Cluster sharding</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / Valkey Cluster</td>
      </tr>
      <tr>
          <td>純 KV、極簡運維</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（無 managed KeyDB）</td>
      </tr>
      <tr>
          <td>需要強一致 + durability</td>
          <td>AWS MemoryDB</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>KeyDB 完整 command reference（沿用 Redis、查 redis.io/commands）</li>
<li>各語言 client API（用 Redis client 即可）</li>
<li>KeyDB on FLASH 詳細調參</li>
<li>Active-replication 內部複製協定細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 KeyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB cross-cloud</a></td>
          <td>Snap 在 GCP 部署 KeyDB cluster、主因是 multi-cloud 的 cross-cloud latency 治理（cache 與 application 共置同 cloud）；9.C35 另記 KeyDB multi-threaded「單實例 throughput 提升 5-10x」（通則、依 workload）</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 KeyDB-specific 案例</strong>：Snap 收購後的公開技術分享、KeyDB on FLASH 的 production 成本案例、active-active 多主複製的跨區衝突治理實例。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 KeyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>KeyDB on FLASH 對應 DRAM + flash 分層的成本決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter / singleflight 通用、KeyDB 多執行緒不消除 stampede 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>KeyDB 是「單實例多核撐大」的選項、介於 Redis Cluster 與 DragonflyDB 之間</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>deep article：<a href="/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/" data-link-title="KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入" data-link-desc="KeyDB 的 active-active 讓兩個 master 都能寫、互相同步，聽起來解決了跨區寫入的所有問題——直到兩邊同時寫同一個 key，last-write-wins 默默丟掉其中一筆。本文展開 active-active 的複製機制與衝突語意、實機驗證雙向同步、5 個把多主複製寫成資料遺失與迴圈的 production 踩坑，以及哪些資料能放 active-active、哪些不能的邊界">KeyDB active-active 多主複製</a>（last-write-wins 衝突與跨區寫入）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>（單執行緒邊界的四個選項）</li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>（跨區資料引力）</li>
<li>回退路徑：<a href="/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/" data-link-title="KeyDB → Redis / Valkey：從多線程 fork 回歸主線的遷移路徑" data-link-desc="從 KeyDB 遷回 Redis 或 Valkey，處理 active-active replication 拆除、多線程 → 單線程效能差異、FLASH storage 移除與 Sentinel/Cluster 對齊">KeyDB → Redis/Valkey</a></li>
</ul>
]]></content:encoded></item><item><title>Azure Key Vault</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/</guid><description>&lt;p>Azure Key Vault 是 Azure 平台把 &lt;em>secret&lt;/em>、&lt;em>cryptographic key&lt;/em>、&lt;em>X.509 certificate&lt;/em> 三類資產 &lt;em>合進同一個 service&lt;/em> 的設計。Vault instance 本身是 first-class ARM resource、有 FQDN endpoint（&lt;code>https://&amp;lt;vault-name&amp;gt;.vault.azure.net&lt;/code>）、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID&lt;/a> Managed Identity 深度整合 — 每個 Vault 自己一個邊界、區別於 region-wide service 的模型。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Azure Key Vault 的核心定位是 &lt;em>三合一 secret + key + cert service 加 Azure-native secret-less 取用&lt;/em>。AWS 是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">KMS&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &amp;#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">ACM&lt;/a> 三個獨立 service、職責邊界清楚但要管三套權限；GCP 是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &amp;#43; Cloud HSM &amp;#43; External Key Manager">Cloud KMS&lt;/a> + Certificate Authority Service 三個獨立；Azure 把這三件事合在 Key Vault — 同一 RBAC role 可同時管 secret / key / cert、減少 IAM 維護成本、但治理上需要在 Vault 內用 &lt;em>naming convention + 多 Vault instance&lt;/em> 自己劃分敏感度邊界（例：production secret / cert 分開不同 Vault、admin access 分人）。&lt;/p></description><content:encoded><![CDATA[<p>Azure Key Vault 是 Azure 平台把 <em>secret</em>、<em>cryptographic key</em>、<em>X.509 certificate</em> 三類資產 <em>合進同一個 service</em> 的設計。Vault instance 本身是 first-class ARM resource、有 FQDN endpoint（<code>https://&lt;vault-name&gt;.vault.azure.net</code>）、跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a> 跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID</a> Managed Identity 深度整合 — 每個 Vault 自己一個邊界、區別於 region-wide service 的模型。</p>
<h2 id="服務定位">服務定位</h2>
<p>Azure Key Vault 的核心定位是 <em>三合一 secret + key + cert service 加 Azure-native secret-less 取用</em>。AWS 是 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">Secrets Manager</a> + <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">KMS</a> + <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">ACM</a> 三個獨立 service、職責邊界清楚但要管三套權限；GCP 是 <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> + <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Cloud KMS</a> + Certificate Authority Service 三個獨立；Azure 把這三件事合在 Key Vault — 同一 RBAC role 可同時管 secret / key / cert、減少 IAM 維護成本、但治理上需要在 Vault 內用 <em>naming convention + 多 Vault instance</em> 自己劃分敏感度邊界（例：production secret / cert 分開不同 Vault、admin access 分人）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> 相比、Azure Key Vault 是 Azure-only 的 <em>static-focused</em> 服務 — 沒有 dynamic credential engine、沒有 transit encryption-as-a-service、沒有跨雲統一介面。優勢是 <em>零運維</em> + <em>Managed Identity 取用免 client secret</em> + <em>Premium tier 直接 HSM-backed</em>。Azure-heavy + 一站式 secret/key/cert + secret-less workload 取用是 Key Vault 的甜蜜點。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 secret / key / cert 適合放 Key Vault、哪些該走 <a href="https://learn.microsoft.com/azure/key-vault/managed-hsm/overview">Managed HSM</a>（FIPS 140-2 Level 3 需求）</li>
<li>Access Policy 跟 Azure RBAC 兩種授權模型的差異與 migration 路徑</li>
<li>Soft Delete + Purge Protection 的 <em>防誤刪</em> 與 <em>防勒索</em> 邊界</li>
<li>何時用 Key Vault、何時改走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（跨雲 + dynamic credential）的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Azure Key Vault deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 access</strong>：Vault 用 Access Policy 還是 Azure RBAC、是否還有 legacy Access Policy 沒清掉、Managed Identity 的 role assignment 是否最小化（Key Vault Secrets User 而非 Key Vault Administrator）</li>
<li><strong>RBAC vs Access Policy 模型</strong>：production 應該全走 Azure RBAC（跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC vendor</a> 同套）、舊 Access Policy 是 migration backlog、不可長期兩軌並存</li>
<li><strong>Soft Delete + Purge Protection</strong>：兩個都應開、Soft Delete 90 天 retention、Purge Protection 開了之後連 owner 都不能立即 purge — 防誤刪 + 防 ransomware 一次性刪光</li>
<li><strong>Diagnostic Logs</strong>：Key Vault <em>預設不記操作 log</em>、必須手動配 Diagnostic Setting 推 Log Analytics / Event Hub / Storage — 沒這層 <code>KeyVaultGet</code> / <code>SecretGet</code> 都沒 audit trail</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Vault Standard vs Premium</strong>：Standard 用 software protection（key 存在 Microsoft-managed software boundary）、Premium 用 FIPS 140-2 Level 2 HSM-backed key、key material 在 HSM 內、不可 export。Premium 適合 <em>signing key / wrapping key 等高敏 key</em>、Standard 適合 <em>application secret + 常規 envelope encryption key</em>。要 FIPS 140-2 Level 3、Standard 跟 Premium 都不夠、必須用 Managed HSM。</p>
<p><strong>Access Policy vs Azure RBAC（兩種授權）</strong>：Access Policy 是 Key Vault legacy 模型 — 在 Vault 物件上掛一張 capability 表（Get / List / Set / Delete / Encrypt / Sign 等細粒度權限）、跟 Azure RBAC 體系獨立。Azure RBAC 模型是新版 — 用 Azure built-in role（Key Vault Secrets User / Key Vault Crypto User / Key Vault Administrator）走 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID</a> 統一身份治理。production 全走 RBAC、舊 Vault 的 Access Policy 是 migration backlog — 兩軌並存會出現 <em>RBAC 拒絕但 Access Policy 允許</em> 的權限漏洞。</p>
<p><strong>Managed Identity 取用（secret-less）</strong>：Azure VM / Function / App Service / AKS pod 走 <em>Managed Identity</em> 直接呼叫 Key Vault API — 不需要存 client secret 或 cert。Workload 拿 IMDS token、token 帶 Entra ID identity、Key Vault 端用 RBAC role assignment 驗證 — 這是 Azure-native 的 secret-less 取用模式、跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM Role for Service Account</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">GCP Workload Identity</a> 同類設計。production 應該 <em>只允許</em> Managed Identity 取用、禁用 service principal + client secret。</p>
<p><strong>Secret rotation（手動 / event-driven）</strong>：Key Vault Secret <em>沒有像 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> 內建的 rotation Lambda</em>。Rotation 走兩條路：手動更新 secret version（app 端拉新版）、或 Event Grid 通知 secret 過期 + Azure Function 觸發 rotation。後者需要自己寫 rotation logic、Key Vault 只提供 <em>版本管理</em> 跟 <em>過期通知</em>、不負責執行 rotation。</p>
<p><strong>Key Rotation Policy</strong>：Key（不是 Secret）有 native Rotation Policy — Vault 在 key 到期前自動生成新版、舊版保留可解密但不再 encrypt。policy 設 <code>rotationPeriod</code> + <code>notifyBeforeExpiry</code>、Key Vault 自動跑、不需要外部觸發。Secret 沒這功能、Key 才有。</p>
<p><strong>Certificate auto-renewal</strong>：Certificate object 可整合 <em>Issuer</em>（DigiCert / GlobalSign / 自簽）做 auto-issue + auto-renew — Key Vault 在到期前自動跑 CSR、向 Issuer 申請新 cert、寫回同一個 Certificate object（保留歷史版本）。比起手動跑 OpenSSL + 寫進 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a>、Certificate object 的優勢是 <em>Issuer 在 Vault 端統一治理</em> — 不過只支援整合過的 public CA。</p>
<p><strong>Soft Delete + Purge Protection</strong>：Soft Delete 預設開（2020 後新 Vault 強制開）、delete 後 90 天 retention、Recover 可救回。Purge Protection 是 <em>額外</em> 開關 — 開了之後 retention 內任何人（包含 subscription owner）都不能 <code>purge</code> 立即清除、必須等 90 天到期才會物理刪除。這是 <em>防勒索</em> 的關鍵 — 沒 Purge Protection、attacker 拿到 owner role 可以 delete + purge 一次性清光。</p>
<p><strong>Private Endpoint</strong>：Key Vault 預設是 public endpoint（FQDN 走 internet）。Private Endpoint 把 Vault 拉進 VNet、只走內網存取 — 高敏 Vault 應該關 public access、強制走 Private Endpoint + Firewall rule（IP 白名單）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Azure Key Vault</th>
          <th>AWS（拆三個）</th>
          <th>GCP（拆三個）</th>
          <th>HashiCorp Vault</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>Azure managed、三合一</td>
          <td>AWS managed、Secrets Manager + KMS + ACM 各獨立</td>
          <td>GCP managed、GSM + Cloud KMS + CAS 各獨立</td>
          <td>自管或 HCP managed</td>
      </tr>
      <tr>
          <td>服務邊界</td>
          <td>一個 Vault 內 secret/key/cert 共用 ACL</td>
          <td>三個 service 各自 IAM policy、邊界清楚</td>
          <td>三個 service 各自 IAM policy</td>
          <td>一個 cluster 內 path-based policy</td>
      </tr>
      <tr>
          <td>Secret-less 取用</td>
          <td>Managed Identity 原生</td>
          <td>IAM Role for Service Account / IRSA</td>
          <td>Workload Identity Federation</td>
          <td>AppRole / K8s / cloud IAM auth</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>無 — 純 static</td>
          <td>部分（RDS rotation Lambda）</td>
          <td>較弱（依靠 IAM impersonation）</td>
          <td>強 — database / cloud / SSH engine</td>
      </tr>
      <tr>
          <td>HSM 等級</td>
          <td>Standard 軟體 / Premium FIPS 140-2 Level 2 / Managed HSM Level 3</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">KMS</a> Level 3 / <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a> Level 3</td>
          <td>Cloud KMS HSM Level 3 / Cloud HSM Level 3</td>
          <td>走後端 KMS（AWS / GCP / Azure）</td>
      </tr>
      <tr>
          <td>Certificate auto-renew</td>
          <td>內建（整合 DigiCert / GlobalSign）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">ACM</a> auto-renew、限 AWS-issued</td>
          <td>CAS + Public CA 整合</td>
          <td>PKI engine 自簽 + <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>弱 — Azure-only</td>
          <td>弱 — AWS-only</td>
          <td>弱 — GCP-only</td>
          <td>強 — 跨雲統一介面</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Azure-heavy + 三合一一站式 + Managed Identity</td>
          <td>AWS-heavy + 職責拆分 + RDS 自動 rotation</td>
          <td>GCP-heavy + Workload Identity Federation</td>
          <td>跨雲 + dynamic credential + 內部 PKI</td>
      </tr>
  </tbody>
</table>
<p>選 Azure Key Vault 的核心訴求：<em>Azure-only</em>、需要 <em>secret + key + cert</em> 一站式、workload 走 <em>Managed Identity</em> secret-less 取用、可接受 <em>無 dynamic credential</em>。需要跨雲統一 secret 控制面、或要 dynamic database credential、走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Managed HSM（dedicated）</strong>：Managed HSM 是 <em>dedicated single-tenant HSM cluster</em>、FIPS 140-2 Level 3、跟 multi-tenant 的 Key Vault Premium 是不同 service。Managed HSM 適合 <em>主權合規</em>（key material 完全自有控制權、Microsoft 也不可存取）、<em>金融 / 醫療 / 政府場景</em>。代價是 <em>貴</em> 跟 <em>初始化要走 ceremony</em>（多人持有 activation key、Microsoft 不可單方面操作）— 不是 Premium 的簡單升級、是另一條 product line。</p>
<p><strong>Premium tier HSM-backed Key</strong>：Premium tier 的 key 有 <code>HSM-protected</code> 屬性、key material 在 multi-tenant HSM 內、API call 還是走標準 Key Vault endpoint、但 cryptographic operation 在 HSM 跑。比 Standard 慢一點、價格高、適合 <em>signing key / wrapping key / root encryption key</em> — 一般 application secret 還是 Standard 即可。</p>
<p><strong>Certificate Issuer 整合</strong>：Vault 內可註冊 Issuer（DigiCert / GlobalSign / Entrust）、提供 API credential、Vault 在 Certificate 到期前自動跑 CSR、向 Issuer 申請、Issuer 簽完寫回 Vault。Self-signed / Unknown Issuer 也支援、後者表示 <em>Vault 產 CSR、人或 pipeline 拿去外部 CA 簽完再 import 回 Vault</em>。</p>
<p><strong>Cross-tenant key access（federated identity）</strong>：Key Vault 可允許跨 Entra ID tenant 的 service principal 取用 — 透過 Federated Identity Credential（Workload Identity Federation）、外部 tenant 的 identity（甚至 GitHub Actions OIDC、AWS workload）拿 token 來 Key Vault 驗證。這是 cross-cloud workload 拉 Azure secret 的方式、不需要存 Azure service principal credential。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID</a> Conditional Access 整合</strong>：Key Vault 用 Azure RBAC 模型時、可走 Conditional Access policy — <em>特定 IP</em>、<em>已 enrolled 裝置</em>、<em>MFA 已驗證</em> 才能取用 secret / key。production 高敏 Vault 應該疊 Conditional Access、避免單純 RBAC 在 token leak 時就直接被存取。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Diagnostic Setting 沒開</strong>：production Vault 啟用後忘了配 Diagnostic Setting 推 log、事故發生時無 <code>SecretGet</code> / <code>KeyDecrypt</code> 紀錄 — 啟動 checklist 必含「Diagnostic Setting → Log Analytics」、Azure Policy 強制全 subscription Vault 都配</li>
<li><strong>Access Policy 跟 RBAC 兩軌並存</strong>：migration 過程中 RBAC 已切換但舊 Access Policy 沒清、出現 <em>RBAC 拒絕但 Access Policy 允許</em> — migration 一次切斷、跑 <code>az keyvault update --enable-rbac-authorization true</code> 後清空所有 Access Policy</li>
<li><strong>Soft Delete 沒開 / Purge Protection 沒開</strong>：誤刪 secret 救不回、或 attacker 拿到 owner role 一次 purge 清光 — 新 Vault 兩個都強制開、Azure Policy 阻擋 <code>enablePurgeProtection: false</code> 的 Vault 建立</li>
<li><strong>Managed Identity role 過寬</strong>：給 workload identity <code>Key Vault Administrator</code> 而非 <code>Key Vault Secrets User</code> — workload 拿到 admin role 等於可改 ACL — role assignment 走 least privilege built-in role</li>
<li><strong>Premium key 跑非 HSM operation</strong>：Premium key 配錯 attribute、key 變成 software-protected 而非 HSM-protected — 建 key 時明示 <code>--protection hsm</code>、CI 驗證 key attribute</li>
<li><strong>Certificate auto-renew Issuer credential 過期</strong>：Vault 內 DigiCert API credential 過期、auto-renew 默默失敗、cert 到期前才發現 — Issuer credential 也要 rotation + monitor</li>
<li><strong>Public access 開著</strong>：Vault 沒關 public endpoint、secret 暴露在 internet（雖然有 RBAC、但 attack surface 多一層）— 高敏 Vault 強制 Private Endpoint + Firewall rule</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲統一 secret 控制面</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></td>
      </tr>
      <tr>
          <td>Dynamic database / cloud credential</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（database / cloud secret engine）</td>
      </tr>
      <tr>
          <td>FIPS 140-2 Level 3 HSM</td>
          <td><a href="https://learn.microsoft.com/azure/key-vault/managed-hsm/overview">Managed HSM</a> / <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></td>
      </tr>
      <tr>
          <td>內部 PKI workload mTLS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> + Vault PKI / SPIRE</td>
      </tr>
      <tr>
          <td>公開 web cert 自動更新（非 Azure-issued）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> + cert-manager</td>
      </tr>
      <tr>
          <td>Entra ID 身份治理 / Conditional Access</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>Secret rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Key Vault REST API / Azure CLI 完整 reference</li>
<li>Managed HSM activation ceremony 完整步驟</li>
<li>Bicep / Terraform 配置 Key Vault 的完整 IaC 範例</li>
<li>Certificate Issuer（DigiCert / GlobalSign）的合約與計價細節</li>
<li>每個 Entra ID role 的細粒度 permission map</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Azure Key Vault 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>Key Vault 是身份控制面下游、Entra ID 出事時 Managed Identity 取 Vault 也失敗 — 需要 fallback access plan（emergency Access Policy + separate identity 走 break-glass）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a></td>
          <td>Key Vault Premium / Managed HSM 把 signing key 鎖硬體、key 不離保護邊界、跟 HSM-bound 同 mindset — signing key 必上 Premium 或 Managed HSM、不放 Standard</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>Asymmetric Key + Diagnostic Logs 是「誰用 key」的稽核基礎 — production Vault 必開 Diagnostic Setting 推 SIEM、不然 key 被誰用過完全沒紀錄</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Key Vault Secret 跨 service 共用時 rotation 要分域 — Vault 端用 Event Grid 通知 + app 端訂閱 rotation event、不能一次 push 全域更新</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>（Key Vault Certificate + Managed HSM 為 TLS / signing key 的 root custodian）、<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li>平行（secret store）：<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>平行（KMS-class）：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a>（Key Vault 是跨類 vendor、同時是 secret store 跟 key management）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>（Managed Identity + RBAC 取用模型）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（K8s workload cert 自動化、可整合 Key Vault Certificate）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Key Vault 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://learn.microsoft.com/azure/key-vault/">Azure Key Vault Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Dependabot</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/dependabot/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/dependabot/</guid><description>&lt;p>Dependabot 是 GitHub 內建的 &lt;em>依賴更新自動化&lt;/em> 工具、原為 Dependabot Inc.、2019 年被 GitHub 收購後改為 GitHub native feature、目前 public repo 免費、private repo 部分功能 (Alerts / Security Update) 也免費、Version Update 跟進階治理納入 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security&lt;/a> 套餐。它做三件事：&lt;em>Dependabot version updates&lt;/em>（定期 PR 升級依賴到最新 compatible 版本）、&lt;em>Dependabot security updates&lt;/em>（CVE 觸發的緊急 PR 升級到 fix version）、&lt;em>Dependabot alerts&lt;/em>（看到漏洞列在 Security tab、不一定自動 PR）。它的設計目標 &lt;em>狹窄而深&lt;/em> — 只做 GitHub repo 的依賴 PR 自動化、不做容器掃描、不做 IaC 掃描、不跨 SCM。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Dependabot 的核心定位是 &lt;em>把依賴升級從人工 ritual 變成 PR review 工作流&lt;/em>。它把「找新版」「跑 manifest update」「開 PR」「附 release note」自動化、剩下的 &lt;em>是否合併&lt;/em> 留給人類 / CI 判斷。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 看似重疊 — 兩者都會自動發升級 PR — 但 Snyk 是 &lt;em>跨 SCM + 多 stack&lt;/em>（GitHub / GitLab / Bitbucket、SCA + 容器 + IaC + Code）、Dependabot 是 &lt;em>GitHub-only + 純依賴&lt;/em>。多數組織選一個、混用兩者會在同一個 manifest 上各自開 PR、造成 noise。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GHAS&lt;/a> 的關係比較細：Dependabot Alerts 跟 Security Updates 本身是 GHAS &lt;em>Dependabot&lt;/em> 子模組的核心、但功能上 &lt;em>Alerts 對所有 repo 免費&lt;/em>、Security Update 也免費自動發 PR、Version Update 也免費；GHAS 提供的是 &lt;em>Dependency Review&lt;/em>（PR-time gate、阻擋 PR 引入新漏洞依賴）、&lt;em>Security Overview&lt;/em>（org-wide dashboard）跟 enterprise-level 控制。Dependabot 是 &lt;em>background PR 工廠&lt;/em>、GHAS Dependency Review 是 &lt;em>PR-time blocker&lt;/em>、兩者互補不重疊。&lt;/p></description><content:encoded><![CDATA[<p>Dependabot 是 GitHub 內建的 <em>依賴更新自動化</em> 工具、原為 Dependabot Inc.、2019 年被 GitHub 收購後改為 GitHub native feature、目前 public repo 免費、private repo 部分功能 (Alerts / Security Update) 也免費、Version Update 跟進階治理納入 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> 套餐。它做三件事：<em>Dependabot version updates</em>（定期 PR 升級依賴到最新 compatible 版本）、<em>Dependabot security updates</em>（CVE 觸發的緊急 PR 升級到 fix version）、<em>Dependabot alerts</em>（看到漏洞列在 Security tab、不一定自動 PR）。它的設計目標 <em>狹窄而深</em> — 只做 GitHub repo 的依賴 PR 自動化、不做容器掃描、不做 IaC 掃描、不跨 SCM。</p>
<h2 id="服務定位">服務定位</h2>
<p>Dependabot 的核心定位是 <em>把依賴升級從人工 ritual 變成 PR review 工作流</em>。它把「找新版」「跑 manifest update」「開 PR」「附 release note」自動化、剩下的 <em>是否合併</em> 留給人類 / CI 判斷。這跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 看似重疊 — 兩者都會自動發升級 PR — 但 Snyk 是 <em>跨 SCM + 多 stack</em>（GitHub / GitLab / Bitbucket、SCA + 容器 + IaC + Code）、Dependabot 是 <em>GitHub-only + 純依賴</em>。多數組織選一個、混用兩者會在同一個 manifest 上各自開 PR、造成 noise。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS</a> 的關係比較細：Dependabot Alerts 跟 Security Updates 本身是 GHAS <em>Dependabot</em> 子模組的核心、但功能上 <em>Alerts 對所有 repo 免費</em>、Security Update 也免費自動發 PR、Version Update 也免費；GHAS 提供的是 <em>Dependency Review</em>（PR-time gate、阻擋 PR 引入新漏洞依賴）、<em>Security Overview</em>（org-wide dashboard）跟 enterprise-level 控制。Dependabot 是 <em>background PR 工廠</em>、GHAS Dependency Review 是 <em>PR-time blocker</em>、兩者互補不重疊。</p>
<p>跟 <a href="https://docs.renovatebot.com/">Renovate</a>（Mend 維護的 OSS）的差異：Renovate 配置更彈性、跨 SCM、支援 ecosystem 數量多（含 Helm chart、Docker tag、ArgoCD 等）、Grouped Updates 規則更細；Dependabot 整合 GitHub 原生 UI（Security tab、Dependency graph、PR diff）更深、設定簡單。需要 <em>跨 SCM</em> 或 <em>Helm / ArgoCD / 自訂 ecosystem</em> 走 Renovate；單純 GitHub-only 加 npm / Maven / pip 等主流 ecosystem、Dependabot 配置成本更低。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Dependabot 在 supply chain 防護裡承擔哪一段（背景 PR 升級）、哪些不在它責任內（容器掃描、IaC 掃描、PR-time gate）</li>
<li><code>dependabot.yml</code> 的關鍵配置面：ecosystem、schedule、open-pull-requests-limit、groups、reviewers</li>
<li>Version Update vs Security Update vs Alerts 三個功能何時開、PR noise 怎麼控制</li>
<li>Auto-merge 政策的邊界：哪種更新可以全自動、哪種要保留 human approval</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 repo 的 Dependabot 配置是否健康、最少看四件事：</p>
<ul>
<li><strong><code>dependabot.yml</code> 配置</strong>：repo 是否有 <code>.github/dependabot.yml</code>、ecosystem 是否覆蓋所有 manifest（npm / Maven / pip / Docker / GitHub Actions / Terraform）、<code>directory</code> 路徑對不對（monorepo 各 sub-package 是否獨立配置）</li>
<li><strong>Update Schedule</strong>：<code>schedule.interval</code> 是 daily / weekly / monthly、<code>open-pull-requests-limit</code> 是否合理（預設 5、太低會卡住 backlog、太高會 PR noise）、Grouped Updates 是否啟用（減少 minor / patch PR 數量）</li>
<li><strong>Auto-merge 政策</strong>：branch protection 是否設「CI green + required reviewer」、auto-merge 是否限定 <em>patch + minor</em> 自動、<em>major</em> 強制 human review、production 跟 staging branch 是否有差異化規則</li>
<li><strong>Token 治理</strong>：repo secrets 是否被 Dependabot PR 誤用、Dependabot secrets（私有 registry credential）是否獨立配置、PR 觸發的 Actions 是否假設 read-only token</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong><code>dependabot.yml</code> 是版控的配置檔</strong>：放在 <code>.github/dependabot.yml</code>、跟 manifest 同 repo、所有變更走 PR review。不在 GitHub UI 直接改 — UI 只能 <em>啟用 / 停用</em> Dependabot 本身、細節必須 commit 進 repo。Monorepo 結構（例：<code>/services/api</code>、<code>/services/web</code> 各自 <code>package.json</code>）每個 sub-package 寫一個 entry、<code>directory</code> 指到 sub-package 根目錄、<code>package-ecosystem</code> 標 manifest 類型。<code>schedule.interval</code> 一般 weekly 開始、daily 適合高活躍度團隊但 PR noise 高、monthly 適合穩定 lib 但 CVE 延遲風險高。</p>
<p><strong>Version Update vs Security Update 分開</strong>：Version Update 是 <em>定期掃 manifest 看有沒有 newer compatible 版本</em>、不分 CVE、是 hygiene 工作；Security Update 是 <em>Dependabot 偵測到 CVE 且 manifest 指到 vulnerable 範圍時自動發 PR 升級到 fix version</em>、是 incident 工作。多數組織開 Security Update 全 repo + 選擇性開 Version Update（核心服務開、archived repo 不開）— 避免 PR noise 淹沒緊急 PR。Security Update 預設啟用、Version Update 要 explicit 在 <code>dependabot.yml</code> 寫 entry 才會跑。</p>
<p><strong>Grouped Updates</strong>：2023 推出、單一 PR 含多個 minor / patch 升級（例：一個 PR 升 10 個 npm package）、PR 數量從 10 個降到 1 個。配置在 <code>dependabot.yml</code> 的 <code>groups</code> 區、可以按 dependency name pattern（例：<code>@types/*</code> 一組、<code>eslint*</code> 一組）或 update-type（<code>patch</code> / <code>minor</code> 分組）。Major version 仍分開 PR — 因 breaking change 風險、需要單獨 review。Grouped Updates 配 auto-merge 是 <em>minor / patch 全自動</em> 的標準配置。</p>
<p><strong>Auto-merge 是 PR 級、不是 commit 級</strong>：Dependabot 發 PR、搭配 GitHub branch protection 設「CI green + 1 approver」就 auto-merge — GitHub <code>gh pr merge --auto</code> 或 Actions workflow（<code>peter-evans/enable-pull-request-automerge</code>）都行。production 環境應該保留 human approval（至少對 major version）、staging / dev 可以全自動。常見模式：staging branch 全自動合（patch + minor）+ 自動 deploy；production branch 走 staging → cherry-pick / promote 流程、human approve。</p>
<p><strong>Reviewer / Assignee / Label 自動標記</strong>：<code>dependabot.yml</code> 的 <code>reviewers</code> / <code>assignees</code> / <code>labels</code> 欄位讓 Dependabot 開 PR 時自動標 reviewer 跟 label。實務上配 <code>labels: [&quot;dependencies&quot;]</code> 讓 Dependabot PR 在 PR list 跟一般 feature PR 分開、CI workflow 可以針對 <code>dependencies</code> label 跑特化 lint（例：跑完整 e2e、不只 unit test）。</p>
<p><strong>Token 治理</strong>：Dependabot PR 跑 GitHub Actions 時、<code>secrets.GITHUB_TOKEN</code> 是 <em>read-only</em>（GitHub 設計上限制、防 PR 觸發 supply chain attack）— 這代表 Dependabot PR 不能跑需要 write permission 的 job（推 image / 改 status / comment）。需要的話用 <code>pull_request_target</code> event（用 base branch 的 workflow + 完整 secrets）、但這也是 supply chain attack 高風險面、必須 <em>最少 permission</em>。私有 registry credential（npm private registry token、Maven private repo password）用 <em>Dependabot secrets</em>（org / repo level）配置、跟 GitHub Actions secrets 是 <em>不同 namespace</em>、不會互相讀到。</p>
<p><strong>跟 GHAS Dependency Review 搭配</strong>：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Dependency Review</a> 在 PR-time 看 manifest diff 阻擋 <em>引入新漏洞依賴</em>、Dependabot Security Update 在 background <em>升級舊有漏洞依賴</em>、兩個方向互補。production repo 標準配置：GHAS Dependency Review 設 high severity block + Dependabot Security Update 全開 + Dependabot Version Update 選擇性開。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Dependabot</th>
          <th>Snyk</th>
          <th>Renovate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SCM 範圍</td>
          <td>GitHub only</td>
          <td>GitHub / GitLab / Bitbucket / Azure DevOps</td>
          <td>GitHub / GitLab / Bitbucket / Azure DevOps / Gitea</td>
      </tr>
      <tr>
          <td>涵蓋面</td>
          <td>純依賴（SCA）</td>
          <td>SCA + 容器 + IaC + Code</td>
          <td>純依賴（SCA）+ Docker tag / Helm / 自訂</td>
      </tr>
      <tr>
          <td>Ecosystem 數量</td>
          <td>主流（npm / Maven / pip / Docker / Actions / Terraform 等 20+）</td>
          <td>主流相近 + 商業資料庫優先</td>
          <td>多（含 Helm / ArgoCD / preCommit / 自訂 regex）</td>
      </tr>
      <tr>
          <td>Grouped Updates</td>
          <td>有（2023+、按 pattern / update-type）</td>
          <td>有（按 type）</td>
          <td>有（規則最細、按 manager / depType / pattern）</td>
      </tr>
      <tr>
          <td>Auto-merge</td>
          <td>走 GitHub branch protection + auto-merge</td>
          <td>Snyk 自家 PR + 走 SCM auto-merge</td>
          <td>內建 <code>automerge</code> 配置、規則細</td>
      </tr>
      <tr>
          <td>漏洞資料庫</td>
          <td>GitHub Advisory Database（公開 + 私有）</td>
          <td>Snyk Intel（商業、揭露快、加入專屬 advisory）</td>
          <td>OSV / NVD / GitHub Advisory（聚合）</td>
      </tr>
      <tr>
          <td>PR 整合深度</td>
          <td>GitHub Security tab / Dependency graph 原生</td>
          <td>Snyk UI 為主、SCM PR 是延伸</td>
          <td>SCM PR 原生、Renovate dashboard issue 集中管理</td>
      </tr>
      <tr>
          <td>設定方式</td>
          <td><code>dependabot.yml</code>（簡單）</td>
          <td>UI + <code>.snyk</code> policy file（漏洞例外）</td>
          <td><code>renovate.json</code>（極彈性、配置複雜）</td>
      </tr>
      <tr>
          <td>商業成本</td>
          <td>GitHub 免費（Version Update / Security Update / Alerts 都免費）</td>
          <td>商業授權（含免費 tier、規模上來付費）</td>
          <td>OSS 免費、Mend 商業版加分析 dashboard</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>GitHub-only + 純依賴 + 設定要簡單</td>
          <td>跨 SCM、要容器 / IaC、商業 advisory 加值</td>
          <td>跨 SCM 或要 Helm / ArgoCD / 自訂 ecosystem</td>
      </tr>
  </tbody>
</table>
<p>選 Dependabot 的核心訴求：<em>GitHub-only</em> + 只要依賴 PR 自動化、不要容器 / IaC scan、配置成本要低、整合 GitHub Security tab。要跨 SCM 或多 stack 走 Snyk、要彈性 ecosystem / Helm chart / ArgoCD 走 Renovate。混用 Dependabot + Snyk 對同一 manifest 自動 PR 會 noise、二選一。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Multi-ecosystem repo</strong>：一個 repo 同時有 npm + Docker + Terraform + GitHub Actions、<code>dependabot.yml</code> 寫四個 entry、各自 schedule。實務常見配置：application 依賴（npm / pip）weekly、base image（Docker）weekly、IaC（Terraform provider）monthly、GitHub Actions（CI workflow）weekly。Actions ecosystem 要特別注意 — Dependabot 升級 <code>uses:</code> 指向的 action version、可以同時 pin commit hash（防 tag re-publish 攻擊）、但 pin hash 後 release note 看不到 — 取捨 <em>安全 vs 可讀性</em>。</p>
<p><strong>Private registry support</strong>：私有 npm registry（GitHub Packages / Artifactory / Nexus）、私有 Maven repo、私有 PyPI mirror、私有 container registry 都要在 <code>dependabot.yml</code> 配置 <code>registries</code> 區、credential 走 Dependabot secrets。Dependabot 從私有 registry 抓 package metadata 跟 release info、否則只能看 public registry、會誤判 internal lib 沒新版。Org-level Dependabot secrets 適合共用 credential、repo-level 適合特殊 credential 隔離。</p>
<p><strong>Self-hosted runner 隔離</strong>：Dependabot PR 觸發的 Actions 預設跑在 GitHub-hosted runner、跟 Dependabot 本身的 sandbox 不同。如果 CI 跑在 self-hosted runner（內網資源 / 大 build cache）、Dependabot PR 也會跑在 self-hosted runner — 要確認 runner 不會被 PR 注入的惡意 manifest 攻擊（npm install 跑 postinstall script 是經典攻擊路徑）。Mitigation：Dependabot PR 用 ephemeral runner（每次新 VM）、隔離 build cache、不掛 sensitive volume。</p>
<p><strong>Auto-merge 風險</strong>：auto-merge 加速合併、但也放寬 <em>攻擊者升級 dep 攻擊我</em> 的窗口。<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a> 的攻擊路徑就是攻擊者花兩年取得 upstream maintainer 信任、發 release 帶 backdoor — 如果下游 auto-merge 升級、攻擊就直達 production。Mitigation：major version 永不 auto-merge、critical infra dep（auth / crypto / network 函式庫）pin commit hash + 手動 review、auto-merge 範圍縮到 patch + minor + low-criticality dep。</p>
<p><strong>GitHub Actions 跟 Dependabot 互動</strong>：Dependabot PR 觸發的 workflow 預設 <code>GITHUB_TOKEN</code> 是 <em>read-only</em>、<code>secrets.*</code> 是 <em>empty</em>（Dependabot context）— 防止 PR 注入腳本竊取 secret。需要在 Dependabot PR 跑帶 secret 的 job、用 <code>pull_request_target</code> event（workflow 從 base branch 取、有完整 secret）— 但這會 <em>讀 PR 的 code 跑 workflow</em>、必須先 <code>checkout</code> base 然後最小化 PR code 的執行（不跑 PR 的 install script、只跑既有 lint）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>PR noise 淹沒緊急 PR</strong>：Version Update 全開 + 沒 Grouped Updates、一週 30+ PR — 啟用 <code>groups</code> 按 pattern 分組（<code>@types/*</code> / <code>eslint*</code> / <code>dev-dependencies</code>）、<code>open-pull-requests-limit</code> 設 5、archived repo 關 Version Update</li>
<li><strong>Security Update 沒發 PR</strong>：CVE 公告了但 Dependabot 沒動 — 確認 manifest 真的指到 vulnerable 範圍、<code>dependabot.yml</code> 沒 <code>ignore</code> 該 dependency、Security Updates 在 repo settings 是啟用、Dependency graph 有抓到該 manifest</li>
<li><strong>私有 registry 抓不到</strong>：Dependabot 在私有 npm / Maven repo 失敗 — <code>dependabot.yml</code> 配 <code>registries</code> 區、credential 進 Dependabot secrets（不是 Actions secrets）、URL 跟 token 範圍對齊</li>
<li><strong>Auto-merge 不觸發</strong>：PR 開了 CI 也綠了但沒合 — 確認 branch protection required check 跟 CI workflow 名稱對齊、<code>gh pr merge --auto</code> 在 PR comment / workflow 有觸發、reviewer count 達標</li>
<li><strong>Dependabot PR 跑 Actions 失敗</strong>：PR 的 workflow 報 permission denied — <code>GITHUB_TOKEN</code> 在 Dependabot context read-only、改用 <code>pull_request_target</code> 或拆 job（push secret 的部分跑在 merge 後 main branch event）</li>
<li><strong>Major version 被 auto-merge</strong>：規則沒寫對、major 也自動合進 production — <code>dependabot.yml</code> 的 <code>ignore</code> 加 <code>update-types: [&quot;version-update:semver-major&quot;]</code> 或 auto-merge 條件改 <code>${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' }}</code></li>
<li><strong>Monorepo 漏掃</strong>：<code>/services/api/package.json</code> 沒掃 — <code>dependabot.yml</code> 每個 sub-package 寫一個 entry、<code>directory</code> 指到正確路徑、不是只在 root 一個 entry</li>
<li><strong>GitHub Actions ecosystem 升級拿掉 commit hash pin</strong>：原本 <code>uses: actions/checkout@a12b3c4</code> 被升成 <code>uses: actions/checkout@v5</code> — Dependabot 會 follow 既有 reference 風格、想要 hash pin 設 <code>dependabot.yml</code> 的 ecosystem-level config 但目前限制較多、實務常另用 <a href="https://github.com/suzuki-shunsuke/pinact">pinact</a> 或 Renovate 處理 Actions hash pinning</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨 SCM（GitLab / Bitbucket）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="https://docs.renovatebot.com/">Renovate</a></td>
      </tr>
      <tr>
          <td>容器 / IaC scan</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></td>
      </tr>
      <tr>
          <td>Helm / ArgoCD / 自訂 ecosystem</td>
          <td><a href="https://docs.renovatebot.com/">Renovate</a></td>
      </tr>
      <tr>
          <td>PR-time block 引入新漏洞</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Dependency Review</a></td>
      </tr>
      <tr>
          <td>SAST / Code scanning</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Code Scanning</a> / <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk Code</a></td>
      </tr>
      <tr>
          <td>SBOM 生成 / 簽章</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a>（含 Sigstore cosign 整合段落）</td>
      </tr>
      <tr>
          <td>Secret scanning</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> / GitGuardian</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li><code>dependabot.yml</code> 完整欄位 reference（看 <a href="https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file">GitHub 官方文件</a>）</li>
<li>GitHub Advisory Database 詳細運作（CVE 來源、curation 流程）</li>
<li>GHAS 其他模組（Code Scanning / Secret Scanning / Dependency Review）細節 — 看 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS 頁</a></li>
<li>Renovate / Snyk 完整配置 — 看各自 vendor 頁</li>
<li>Container base image 升級的 multi-stage Dockerfile 處理</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Dependabot 沒有自身 vendor-level case、但在 supply chain case 中是 <em>標準 mitigation</em> 或 <em>風險面</em>：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Dependabot 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — Dependabot Security Update 在 Log4Shell 期間自動發 log4j-core 升級 PR、auto-merge 必須有 functional + security 雙重 CI verify、不能單看 build pass</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain</a></td>
          <td>對照啟示 — Dependabot 自己用 GitHub token、需確認 Dependabot PR 不能讀 production secrets（GitHub 設計上已 read-only / empty secrets）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation</a></td>
          <td>對照啟示 — CI 出事時 Dependabot secrets（私有 registry credential）也要 rotate、不是只 rotate Actions secrets</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>對照啟示 — Dependabot auto-merge 隱含 maintainer trust、攻擊者控制 upstream 後升級 = 自動進 production；major 不 auto-merge + 重要 dep pin commit hash</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（容器 scan）、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a>（SBOM）</li>
<li>跨類：artifact 簽章（Sigstore cosign）見 <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype 頁的 SBOM attestation 段</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 可靠性驗證流程</a>（Dependabot PR 進 release flow 的 gate 設計）、<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></li>
<li>官方：<a href="https://docs.github.com/en/code-security/dependabot">Dependabot Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Google Cloud IAM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/</guid><description>&lt;p>Google Cloud IAM 是 GCP 的 cloud resource permission engine、把 &lt;em>誰能對哪個 resource 做什麼&lt;/em> 統一成一個模型：Principal + Role + Resource scope 三件事拼成一個 &lt;em>role binding&lt;/em>。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 等 IdP 是兩層責任 — Okta 回答「這個人是誰」、Google IAM 回答「這個身份能對 GCP resource 做什麼」。設計上比 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> 統一、沒有 resource-based policy vs identity-based policy 雙軌、也沒有 SCP / Permission Boundary 多層覆蓋、policy 評估路徑短而可預測。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Google Cloud IAM 的核心抽象是 &lt;em>role binding on a resource scope&lt;/em>：把 role grant 給 principal、生效範圍是某個 Organization / Folder / Project / 個別 resource、沿 resource hierarchy 向下繼承。同一個 principal 在不同 scope 可以有不同 role、有效權限是所有 binding 的 union。這跟 AWS IAM 的「identity policy + resource policy + SCP + boundary 多層 intersect / union」相比、推理成本低、但也意味著 &lt;em>guardrail 必須走 Organization Policy 這另一個系統&lt;/em> — 不是 IAM grant 的一部分。&lt;/p>
&lt;p>跟 Azure RBAC 相比、兩者都是 scope-based、都靠 hierarchy 繼承。差異在 &lt;em>Service Account 是 GCP 的 first-class identity&lt;/em>：有自己的 email、可被 impersonate、可以 grant role 給它也可以 grant &lt;code>iam.serviceAccountUser&lt;/code> 讓人類 act-as 它。Azure 的對應是 Managed Identity、語義接近但 impersonation chain 的表達更隱晦。選 GCP（= 用 Google Cloud IAM）的核心訴求通常是：BigQuery / Vertex AI / GKE workload、想用 Workload Identity Federation 取代 long-lived key、團隊偏好較統一的 policy 模型。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Google Cloud IAM 該承擔哪一段權限（resource access、service-to-service、cross-cloud federation）、哪一段該交給 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / IdP&lt;/li>
&lt;li>Role 的選擇順序（Predefined &amp;gt; Custom &amp;gt; Basic）與 IAM Conditions 何時補上&lt;/li>
&lt;li>Service Account / Workload Identity Federation 的信任邊界、何時不該再發 service account key&lt;/li>
&lt;li>何時改走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a> / Organization Policy / VPC Service Controls&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷一個 GCP project 的 IAM 配置是否健康、最少看五件事：&lt;/p></description><content:encoded><![CDATA[<p>Google Cloud IAM 是 GCP 的 cloud resource permission engine、把 <em>誰能對哪個 resource 做什麼</em> 統一成一個模型：Principal + Role + Resource scope 三件事拼成一個 <em>role binding</em>。它跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 等 IdP 是兩層責任 — Okta 回答「這個人是誰」、Google IAM 回答「這個身份能對 GCP resource 做什麼」。設計上比 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 統一、沒有 resource-based policy vs identity-based policy 雙軌、也沒有 SCP / Permission Boundary 多層覆蓋、policy 評估路徑短而可預測。</p>
<h2 id="服務定位">服務定位</h2>
<p>Google Cloud IAM 的核心抽象是 <em>role binding on a resource scope</em>：把 role grant 給 principal、生效範圍是某個 Organization / Folder / Project / 個別 resource、沿 resource hierarchy 向下繼承。同一個 principal 在不同 scope 可以有不同 role、有效權限是所有 binding 的 union。這跟 AWS IAM 的「identity policy + resource policy + SCP + boundary 多層 intersect / union」相比、推理成本低、但也意味著 <em>guardrail 必須走 Organization Policy 這另一個系統</em> — 不是 IAM grant 的一部分。</p>
<p>跟 Azure RBAC 相比、兩者都是 scope-based、都靠 hierarchy 繼承。差異在 <em>Service Account 是 GCP 的 first-class identity</em>：有自己的 email、可被 impersonate、可以 grant role 給它也可以 grant <code>iam.serviceAccountUser</code> 讓人類 act-as 它。Azure 的對應是 Managed Identity、語義接近但 impersonation chain 的表達更隱晦。選 GCP（= 用 Google Cloud IAM）的核心訴求通常是：BigQuery / Vertex AI / GKE workload、想用 Workload Identity Federation 取代 long-lived key、團隊偏好較統一的 policy 模型。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Google Cloud IAM 該承擔哪一段權限（resource access、service-to-service、cross-cloud federation）、哪一段該交給 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / IdP</li>
<li>Role 的選擇順序（Predefined &gt; Custom &gt; Basic）與 IAM Conditions 何時補上</li>
<li>Service Account / Workload Identity Federation 的信任邊界、何時不該再發 service account key</li>
<li>何時改走 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a> / Organization Policy / VPC Service Controls</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 GCP project 的 IAM 配置是否健康、最少看五件事：</p>
<ul>
<li><strong>Principal 級別</strong>：誰是 Owner / Editor / Viewer（Basic Role 應該幾乎為空）、Service Account 是否獨立列管、有沒有 user 直接 grant 沒走 group</li>
<li><strong>Role 種類</strong>：Predefined Role 是 baseline、Custom Role 收斂 least privilege、Basic Role 視為待修；user-managed Service Account key 是否存在（理想是 0）</li>
<li><strong>Impersonation chain 展平稽核</strong>：誰有 <code>iam.serviceAccountTokenCreator</code> / <code>iam.serviceAccountUser</code> 對哪個 SA、間接 chain（A → B → C）展平後 <em>誰最終能 act as 高權限 SA</em>。這是 GCP IAM 最容易漏稽核的一條 — 直接 binding 看 Role、但 lateral movement 走 impersonation chain</li>
<li><strong>IAM Conditions</strong>：高敏 resource（prod bucket、KMS key、BigQuery dataset）是否用 condition expression 補 attribute-level 限制（resource name prefix、request time、IP）</li>
<li><strong>Audit Logs</strong>：Admin Activity 預設開、Data Access logs 在 sensitive resource 是否手動開、System Log 是否同步到 SIEM 並 alert role 變更與 service account key 建立</li>
</ul>
<p>五件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Role 選擇順序</strong>：Predefined Role 是 baseline、覆蓋 80% 場景；Custom Role 用於收斂 least privilege（例如只給 <code>bigquery.dataViewer</code> 的特定子集）；Basic Role（Owner / Editor / Viewer）幾乎不該再用 — Editor 預設帶寫權限到幾乎所有資源類型、Owner 還能改 IAM policy 本身、粒度過粗。Project 建立預設給的 Owner role 是 <em>人類自己 grant 自己</em>、不是無法避免的 baseline。</p>
<p><strong>Principal type</strong>：人類用 Google Workspace user / external user，群組走 Google Group（grant 給 group 比 grant 給 user 更穩、離職 lifecycle 由 IdP / HRIS 推 group 變更即可）。Service Account 是 <em>第一級身份</em>、跟 user 同等、有自己的 email（<code>name@project.iam.gserviceaccount.com</code>）、可被 grant role 也可被 impersonate。Workload identity（K8s SA、外部 OIDC subject）是 federation 層、不在 IAM 內直接列管、但 <em>最後仍 impersonate 一個 Service Account 來拿 GCP 權限</em>。</p>
<p><strong>IAM Conditions</strong>：在 role binding 上加 attribute-based 條件、補純 RBAC 不足。常見 expression：<code>resource.name.startsWith(&quot;projects/_/buckets/prod-&quot;)</code>、<code>request.time &lt; timestamp(&quot;2026-12-31T00:00:00Z&quot;)</code>、<code>resource.type == &quot;storage.googleapis.com/Bucket&quot;</code>。適合 <em>temporary access</em>、<em>resource name 範圍限定</em>、<em>環境隔離</em>；不適合複雜 ABAC 規則（會難以稽核、且 condition 只能用在支援的 resource type 上）。</p>
<p><strong>Service Account impersonation</strong>：人類或另一個 Service Account 透過 <code>iam.serviceAccountTokenCreator</code> role 借用目標 SA 的權限、不需要 SA key。impersonation chain 可以串（A 可 impersonate B、B 可 impersonate C）— 這條鏈是 lateral movement 風險、稽核時要展平看 <em>誰最終能 act as 高權限 SA</em>。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的教訓：rotation 沒分域時、單點 SA compromise 會跨環境擴散。</p>
<p><strong>Workload Identity Federation（WIF）</strong>：GCP 接受外部 OIDC / SAML issuer（GitHub Actions、AWS、Azure、自管 K8s OIDC、CircleCI 等）發的 token、在 Workload Identity Pool 設 attribute mapping 後、外部 token 換成 short-lived GCP credential、最後 impersonate 指定 Service Account。是 <em>取代 SA JSON key 的 modern best practice</em>、CI / 跨雲 / 邊緣 workload 都該優先用。Trust 條件要鎖 <em>issuer + audience + subject</em>（例：<code>assertion.repository == &quot;myorg/myrepo&quot;</code>）— 缺一個就可能被同 issuer 下其他 subject 借用，這是 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a> 對 external OIDC 信任的提醒：發 token 的 issuer 一旦被攻破、所有信任它的 audience 都跟著受害。</p>
<p><strong>Service Account key（避免）</strong>：user-managed JSON key 是 long-lived credential、無 TTL、無 IP 限制、外洩偵測難。應該以 Workload Identity Federation 或 Service Account Impersonation 取代；若必須用、走 Organization Policy <code>iam.disableServiceAccountKeyCreation</code> 預設禁用、例外申請走 ticket、key 進 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、季度盤點未使用 key 刪除。</p>
<p><strong>Organization Policy（guardrail）</strong>：跟 IAM 完全不同層 — 不是 grant、是 <em>限制可以做什麼設定</em>。常用 constraint：<code>iam.disableServiceAccountKeyCreation</code>、<code>iam.allowedPolicyMemberDomains</code>（限制只能 grant 給特定 domain 的 principal）、<code>compute.vmExternalIpAccess</code>（限制 VM external IP）、<code>storage.publicAccessPrevention</code>。Org Policy 在 Organization / Folder / Project 層設定、IAM 即使想 grant 也擋得住。</p>
<p><strong>Audit / handoff</strong>：Admin Activity Log 預設開、不能關、保留 400 天免費；Data Access Log 預設關、開了會大量 log（也大量計費）— 對 sensitive resource（KMS key access、BigQuery dataset read、Secret Manager access）應該手動開；System Event Log 補基礎設施事件。三類都接 Cloud Logging sink 推到 SIEM、特別 alert 三件事 — IAM policy 變更、Service Account key 建立 / 上傳、Workload Identity Pool / Provider 變更。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google Cloud IAM</th>
          <th>AWS IAM</th>
          <th>Azure RBAC</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Policy 模型</td>
          <td>Role binding on resource scope、單軌</td>
          <td>Identity policy + resource policy + SCP + boundary</td>
          <td>Scope-based、Management Group 階層</td>
      </tr>
      <tr>
          <td>表達力</td>
          <td>中等、IAM Conditions 補 attribute</td>
          <td>最高、policy language 表達 ABAC / 條件 / 否決</td>
          <td>中等、Azure Policy 補 ABAC</td>
      </tr>
      <tr>
          <td>Guardrail 機制</td>
          <td>Organization Policy（獨立系統、constraint）</td>
          <td>SCP（policy 同語法、separate plane）</td>
          <td>Azure Policy（獨立系統、constraint）</td>
      </tr>
      <tr>
          <td>Machine identity</td>
          <td>Service Account first-class + WIF</td>
          <td>IAM Role + STS AssumeRole + OIDC trust</td>
          <td>Managed Identity + Workload Identity Federation</td>
      </tr>
      <tr>
          <td>Cross-cloud federation</td>
          <td>WIF 接外部 OIDC 是 modern best practice</td>
          <td>OIDC trust on IAM Role、表達力強</td>
          <td>Federated credentials、近年補齊</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>較緩、模型統一</td>
          <td>陡、policy 評估順序複雜</td>
          <td>中等、scope inheritance 直覺</td>
      </tr>
      <tr>
          <td>推理 / 稽核成本</td>
          <td>低 — binding union、Org Policy 獨立看</td>
          <td>高 — 多層 intersect / union、需 policy simulator</td>
          <td>中 — scope 繼承明確、policy 分散</td>
      </tr>
  </tbody>
</table>
<p>選 Google Cloud IAM 的核心訴求：<em>已在 GCP 上、或想用 BigQuery / Vertex AI / GKE</em>、團隊偏好較統一的 policy 模型、跨雲場景靠 WIF 對外發 trust 而不維護多套 key。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Workload Identity Federation 的深層應用</strong>：除了 GitHub Actions、AWS、Azure 這類常見 issuer、WIF 也支援自管 K8s OIDC issuer（OSS K8s cluster 跑 GKE workload identity 等價物）、SaaS（Snowflake、Terraform Cloud）發的 OIDC token。trust 設定要鎖 issuer URL、audience、subject pattern 三件事 — 任何一個太寬都是同 issuer 下別人借用你 SA 的入口。</p>
<p><strong>Organization Policy 的 dry-run / 例外</strong>：constraint 可以先設 <code>dryRun</code> 觀察會擋掉哪些操作再 enforce；例外用 <em>exception folder</em>（特定 folder 不繼承上層 constraint）或 <em>condition</em>（特定 resource pattern 不擋）。直接全 org 一次 enforce 通常會打掉既有 workload、要分階段。</p>
<p><strong>IAM Conditions 的有限性</strong>：condition 只能用在支援的 resource type 上、不是全 GCP 通用；複雜 expression 難稽核（CEL 語法、不易讀）；condition 不能否決 — 只能限制 binding 的生效範圍、不能像 AWS policy 那樣寫 <code>Deny</code>。複雜 ABAC 場景該走 Organization Policy + 應用層授權邊界、不是把所有規則塞進 IAM Conditions。</p>
<p><strong>Service Account Impersonation chain 的稽核</strong>：列出 <em>有 <code>serviceAccountTokenCreator</code> 的 principal</em> 是基本；展平 chain（A → B → C）需要 graph walk 工具或 Policy Analyzer；高權限 SA（owner-equivalent custom role、跨 project 寫權限）的 impersonation 來源應該是 <em>寫死的少數 admin SA + break-glass</em>、不該開放給 CI / 一般 service。</p>
<p><strong>VPC Service Controls（資料邊界、跟 IAM 互補）</strong>：在 IAM 之外加 <em>資料 perimeter</em> — 即使 principal 有 IAM 權限、如果請求不是來自 perimeter 內（VPC、特定 IP、特定 service account），仍然會被擋。適合 BigQuery / GCS / Secret Manager 這類存資料的 service、防 <em>合法 credential 從外部 exfiltrate 資料</em>（<a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a> 場景的下游補位：identity 控制面失守時、資料層仍有獨立 perimeter）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Basic Role 還在用</strong>：Project Owner / Editor 散落、新人 onboard 直接 Editor — 改 group + Predefined Role、Basic Role 改成 break-glass 限定</li>
<li><strong>Service Account key 散落</strong>：CI 用 JSON key、key 進 git 或環境變數、無 rotation — 改 WIF（GitHub Actions / GitLab CI 都支援）、Org Policy 禁用 SA key 建立</li>
<li><strong>WIF trust 太寬</strong>：只鎖 issuer 沒鎖 subject、同 GitHub org 任何 repo 都能借用 SA — trust 要含 <code>assertion.repository</code>、<code>assertion.ref</code>（main branch only）等 condition</li>
<li><strong>IAM Conditions 越寫越多</strong>：condition expression 過度複雜、稽核時沒人讀得懂 — 簡化條件、把複雜規則上移到應用層或 Org Policy</li>
<li><strong>Data Access Logs 沒開</strong>：sensitive resource 出事時只有 Admin Activity、看不到 <em>誰讀了什麼</em> — KMS key、Secret Manager、BigQuery 高敏 dataset 必開 Data Access Log</li>
<li><strong>Impersonation chain 失控</strong>：太多人有 <code>serviceAccountTokenCreator</code> 到高權限 SA — 用 Policy Analyzer 展平、收斂到必要 admin + break-glass</li>
<li><strong>Org Policy 沒設</strong>：root org 沒有 baseline constraint、新建 project 預設可建 SA key / public IP / public bucket — 至少設 <code>disableServiceAccountKeyCreation</code> + <code>publicAccessPrevention</code> + <code>allowedPolicyMemberDomains</code></li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類身份的 SSO / MFA / lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / IdP</td>
      </tr>
      <tr>
          <td>AWS resource permission</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a></td>
      </tr>
      <tr>
          <td>Azure resource permission</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>跨雲 unified IAM</td>
          <td>沒有單一答案 — 各雲 IAM + Workload Identity Federation 對接、或外部 PAM（Teleport / Boundary）</td>
      </tr>
      <tr>
          <td>Secret / Service Account key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>資料分類 / DLP / 匯出控制</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></td>
      </tr>
      <tr>
          <td>Workload runtime detection（容器、syscall）</td>
          <td>04 + Falco / Cilium Tetragon 類工具</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Predefined Role 的完整權限清單與細部 permission 差異</li>
<li>IAM Conditions CEL 語法的完整 spec</li>
<li>Workload Identity Federation 跟特定 issuer（GitHub / AWS / Azure）的逐步設定教學</li>
<li>BigQuery / GCS / KMS 等服務的 service-specific IAM 行為細節</li>
<li>GCP 計費 / SKU 對 Audit Log 開關的影響</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Google Cloud IAM 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>Identity 控制面故障不直接打到 Google IAM、但設計啟示是 IAM evaluation 路徑必須 HA、且 VPC Service Controls 等資料 perimeter 是 identity 失守時的下游補位</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Service Account key、WIF provider 的 rotation 必須分域 — 跨 project / 跨環境的 SA 共用是 blast radius 放大器</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>對 WIF 的提醒 — 信任 external OIDC issuer 時、issuer 自己被攻破會打到所有 audience；trust condition 必須鎖 issuer + audience + subject 三件事</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Google Secret Manager / Google Cloud KMS 個別 vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（GCP IAM 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://cloud.google.com/iam/docs">Google Cloud IAM Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Microsoft Purview</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/</guid><description>&lt;p>Microsoft Purview 是 Microsoft 在 2022 年把原 Microsoft Information Protection (MIP)、Azure Purview data catalog、Microsoft 365 Compliance Center 合併後的統合品牌、定位是 &lt;em>跨 M365 / Azure / endpoint / 跨平台&lt;/em> 的 data governance + information protection + DLP + audit + insider risk 平台。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a> 的本質差異在 &lt;em>控制層級&lt;/em>、功能列表反而看起來相似 — Purview 走 &lt;em>information protection&lt;/em>（document / email / collaboration tool 的 sensitivity label + endpoint inline 攔截）、Google DLP 走 &lt;em>infrastructure-level discovery + transformation&lt;/em>（GCS / BigQuery 的 content scan + de-identification）— 兩者層級不同、典型大型 Microsoft + GCP 混合環境會並存而非互斥。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Purview 的核心 first-class concept 是 &lt;em>sensitivity label&lt;/em> — 一個 label 帶動 encryption、access restriction、watermarking、DLP policy 多個控制、可由 user 手動標也可由 trainable classifier 自動標、跨 Office docs / SharePoint / Teams / Power BI / endpoint 繼承。其上的模組包含：&lt;em>Data Loss Prevention (DLP)&lt;/em> — 跨 Exchange / SharePoint / Teams / Endpoint / Microsoft Defender for Cloud Apps (MDA) 的 policy 引擎；&lt;em>Data Map / Data Catalog&lt;/em> — Azure / 多雲資料源 discovery + lineage；&lt;em>Unified Audit Log&lt;/em> — M365 + Azure AD + Defender 統一 audit；&lt;em>Insider Risk Management&lt;/em> — 行為 risk score 偵測內部威脅；&lt;em>Communication Compliance&lt;/em> — Teams / email 內容 review。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a> 比、Purview 走 &lt;em>information protection 層 + label-driven + endpoint inline&lt;/em>、Google DLP 走 &lt;em>infrastructure 層 + content-based + transformation pipeline&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> 比、Purview 不是 SIEM — Unified Audit Log 是 &lt;em>event source&lt;/em>、Splunk 或 Microsoft Sentinel 才是 aggregation 平面；Purview audit 進 SIEM 是常見組合。跟&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">雲端原生 data policy&lt;/a>（BigQuery Column-Level Security / S3 Block Public Access）比、Purview 跨平台 + label 統一、雲端原生只覆蓋單一雲、不同責任邊界。&lt;/p></description><content:encoded><![CDATA[<p>Microsoft Purview 是 Microsoft 在 2022 年把原 Microsoft Information Protection (MIP)、Azure Purview data catalog、Microsoft 365 Compliance Center 合併後的統合品牌、定位是 <em>跨 M365 / Azure / endpoint / 跨平台</em> 的 data governance + information protection + DLP + audit + insider risk 平台。它跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> 的本質差異在 <em>控制層級</em>、功能列表反而看起來相似 — Purview 走 <em>information protection</em>（document / email / collaboration tool 的 sensitivity label + endpoint inline 攔截）、Google DLP 走 <em>infrastructure-level discovery + transformation</em>（GCS / BigQuery 的 content scan + de-identification）— 兩者層級不同、典型大型 Microsoft + GCP 混合環境會並存而非互斥。</p>
<h2 id="服務定位">服務定位</h2>
<p>Purview 的核心 first-class concept 是 <em>sensitivity label</em> — 一個 label 帶動 encryption、access restriction、watermarking、DLP policy 多個控制、可由 user 手動標也可由 trainable classifier 自動標、跨 Office docs / SharePoint / Teams / Power BI / endpoint 繼承。其上的模組包含：<em>Data Loss Prevention (DLP)</em> — 跨 Exchange / SharePoint / Teams / Endpoint / Microsoft Defender for Cloud Apps (MDA) 的 policy 引擎；<em>Data Map / Data Catalog</em> — Azure / 多雲資料源 discovery + lineage；<em>Unified Audit Log</em> — M365 + Azure AD + Defender 統一 audit；<em>Insider Risk Management</em> — 行為 risk score 偵測內部威脅；<em>Communication Compliance</em> — Teams / email 內容 review。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> 比、Purview 走 <em>information protection 層 + label-driven + endpoint inline</em>、Google DLP 走 <em>infrastructure 層 + content-based + transformation pipeline</em>。跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 比、Purview 不是 SIEM — Unified Audit Log 是 <em>event source</em>、Splunk 或 Microsoft Sentinel 才是 aggregation 平面；Purview audit 進 SIEM 是常見組合。跟<a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">雲端原生 data policy</a>（BigQuery Column-Level Security / S3 Block Public Access）比、Purview 跨平台 + label 統一、雲端原生只覆蓋單一雲、不同責任邊界。</p>
<p>關鍵張力：<em>label 設計簡單度</em> ↔ <em>自動分類精準度</em> ↔ <em>使用者教育成本</em> 是 Purview 導入時最常踩的三角。label 太細（10+ 層 hierarchical）使用者選不出來、label 太粗（只有 Public / Internal / Confidential）DLP policy 觸發精度不夠。Trainable classifier + auto-labeling 是補救、但要投入訓練樣本維運。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Purview 在 information protection stack 中承擔哪一段（label / DLP / audit / insider risk）、跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC + Entra ID</a> / SIEM / cloud-native policy 怎麼分工</li>
<li>Sensitivity label 的層級設計（粗細、auto-label 條件、跨 Office / endpoint / Power BI 一致性）</li>
<li>DLP policy 的 location + condition + action 三軸如何配置、跟 endpoint DLP / MDA 怎麼覆蓋 SaaS shadow IT</li>
<li>Purview 計費分 SKU 的 trap、E3 + add-on vs E5 license 的決策</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Purview deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Label 層級設計</strong>：sensitivity label 幾層、是否 hierarchical（parent / sublabel）、是否定義 auto-labeling 條件（含某 SIT、來自某 SharePoint site、某 user group 建立）、跨 Office / endpoint / Power BI / Teams 是否一致繼承</li>
<li><strong>DLP policy coverage</strong>：location 是否涵蓋 Exchange + SharePoint + Teams + Endpoint + MDA、condition 是否用 SIT + label 雙軸（而非只看 SIT）、action 是否依風險分層（block / warn / encrypt / audit-only）</li>
<li><strong>Audit + Insider Risk 證據鏈</strong>：Unified Audit Log retention 是否足夠（預設 180 天、E5 可到 1 年、長期要 archive）、Insider Risk policy 是否定義「離職前 30 天 mass download」「異常時段 access」等 organization-specific pattern、是否 export 進 SIEM</li>
<li><strong>License 跟模組對應</strong>：Information Protection / DLP / Insider Risk / Communication Compliance 屬不同 SKU、是否買到所需模組、E3 + add-on 還是 E5、避免「policy 寫好但 license 沒解鎖功能」</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Sensitivity label 是 first-class control</strong>：label 不只是 metadata、而是 <em>單一 identifier 帶動多個控制</em> — 標到 document 後同時觸發 AES encryption（透過 Azure Rights Management）、access restriction（誰能開 / 列印 / 轉寄）、watermarking、DLP policy condition、Power BI dataset 繼承。Hierarchical label（Confidential → Confidential\Finance、Confidential\Legal）讓子部門客製、但層級超過 3 層使用者選擇困難。Label 設計要先決定 <em>跨 BU 共用 base set + 每 BU 自家 sublabel</em> 的拓撲、不是一次列 20 個。</p>
<p><strong>Trainable classifier 補 SIT 不足</strong>：預定義 SIT（Sensitive Information Type、如 credit card / SSN / passport）涵蓋通用 PII / PCI、但 organization-specific 敏感資料（內部 product spec、合約模板、未公開財報草稿）SIT 抓不到。Trainable classifier 用 ML 訓練 — 提供 50-500 個正例 + 反例、Purview 訓 classifier、跑 staging 驗證 precision / recall 達標再 promote。維運成本是樣本要定期 refresh、business 變動時 classifier 會 drift。</p>
<p><strong>DLP policy = location + condition + action</strong>：location（Exchange email / SharePoint site / Teams chat / OneDrive / Endpoint / MDA-managed SaaS）決定 <em>在哪攔</em>、condition（含某 SIT N 次 / 標 Confidential / 來自外部 user / 含某 trainable classifier 命中）決定 <em>何時觸發</em>、action（block + notify / encrypt / quarantine / audit-only / require justification）決定 <em>怎麼處理</em>。production 不該一上來就 block — 先 audit-only 跑 2 週收集 baseline、tune false positive、再 promote 到 warn、最後選擇性 block 高風險 condition。</p>
<p><strong>Endpoint DLP（Windows / macOS）</strong>：透過 Microsoft Defender for Endpoint agent 在端點 inline 攔截 — copy to USB / upload to non-corp cloud（Dropbox / Google Drive personal）/ print / paste to browser、針對標 Confidential 的 document 自動 block 或 warn。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 的 Sensitive Data Scanner 不同層 — 後者 scan log / APM payload 事後發現、Endpoint DLP 事前在 user action 攔截。Endpoint DLP 要 Defender for Endpoint license + Purview Endpoint DLP add-on 雙重 license、容易踩計費 trap。</p>
<p><strong>Microsoft Defender for Cloud Apps (MDA) 整合</strong>：MDA 是 Microsoft 的 CASB（Cloud Access Security Broker）、把 Purview DLP policy 延伸到非 Microsoft 的 SaaS（Salesforce / Box / Slack / Google Workspace）。MDA 透過 API connector 或 reverse proxy 攔截 SaaS 上的 sensitive document、套 Purview label / DLP action。覆蓋 shadow IT 跟 third-party SaaS 是 MDA 的價值、但每個 connector 都要單獨配置 + 維運。</p>
<p><strong>Data Map / Data Catalog discovery + lineage</strong>：Purview Data Map 自動掃描 Azure Storage / Synapse / SQL DB / Power BI / 部分 AWS / GCP 資料源、產 metadata + classification + lineage。跟 information protection 模組是不同 surface — Data Map 偏 <em>data governance</em>（誰擁有什麼資料、資料流向哪）、information protection 偏 <em>control</em>（誰能存取、能否 export）。中大型組織通常分開 onboard、不要一次全推。</p>
<p><strong>Unified Audit Log 是 SIEM source</strong>：M365 + Azure AD + Defender + Purview 自身的 audit event 統一進 Unified Audit Log、可透過 Compliance Center search、或 Office 365 Management Activity API export 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Sentinel / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>。Purview 自己不做 correlation / alerting、要做跨來源 detection 必須接 SIEM。Retention 預設 180 天、E5 license 1 年、長期合規要走 Audit Premium 或 archive 到 long-term storage。</p>
<p><strong>Insider Risk Management 跟 SIEM 互補</strong>：SIEM 主軸是 <em>external threat + cross-source correlation</em>、Insider Risk 主軸是 <em>single-user 行為 risk score over time</em> — 離職前 30 天 mass download、異常時段存取 sensitive folder、跨 sensitivity tier 大量 access。Risk score 累積到 threshold 觸發 case、進 Compliance officer review queue。預定義 policy template（departing employee、disgruntled employee、data leak）可快速 onboard、organization-specific pattern 要自己定。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC + Entra ID</a> 整合</strong>：Purview policy 的 user / group 引用直接吃 Entra ID identity、sensitivity label 的 access restriction 也走 Entra ID group。Compliance / Information Protection admin 是 Entra ID role、應該收緊到少數人 + 走 PIM (Privileged Identity Management) just-in-time elevation。Break-glass account 要單獨設計、不能跟日常運維混。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Microsoft Purview</th>
          <th>Google DLP</th>
          <th>Splunk</th>
          <th>雲端原生 data policy（BigQuery / S3）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制層級</td>
          <td>Information protection（document / label）</td>
          <td>Infrastructure（content scan + transform）</td>
          <td>Detection / aggregation</td>
          <td>Resource policy（column / object 級別）</td>
      </tr>
      <tr>
          <td>核心抽象</td>
          <td>Sensitivity label + DLP policy</td>
          <td>InfoType + de-identification</td>
          <td>SPL + correlation rule</td>
          <td>IAM policy + column tag</td>
      </tr>
      <tr>
          <td>覆蓋面</td>
          <td>M365 + Endpoint + MDA-managed SaaS + Azure</td>
          <td>GCS / BigQuery / Pub/Sub / 任意 API content</td>
          <td>任意 log source</td>
          <td>單一雲服務內</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>Per-user license（E3 + add-on / E5、模組分 SKU）</td>
          <td>Per-GB scan + per-API call</td>
          <td>Per-GB ingestion</td>
          <td>多半免費 / 服務內計費</td>
      </tr>
      <tr>
          <td>自動分類</td>
          <td>Trainable classifier + 預定義 SIT</td>
          <td>InfoType detector（150+ 預定義 + custom）</td>
          <td>不做分類</td>
          <td>Column tag 手動 / catalog 工具自動</td>
      </tr>
      <tr>
          <td>Endpoint inline</td>
          <td>強 — Endpoint DLP（Win/macOS）</td>
          <td>無（基礎設施層）</td>
          <td>無（觀測層）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Shadow IT 覆蓋</td>
          <td>強 — 透過 MDA CASB</td>
          <td>弱 — 限 GCP / API 整合</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — label 嵌入 document、跨 M365 黏著</td>
          <td>中 — InfoType pattern 可移植</td>
          <td>高 — SPL / detection content</td>
          <td>低 — IAM policy 較通用</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>M365 / Office / collaboration 為主、insider risk</td>
          <td>Infrastructure data + multi-cloud + GCP</td>
          <td>SIEM / SOC</td>
          <td>單一雲服務內 fine-grained access</td>
      </tr>
  </tbody>
</table>
<p>選 Purview 的核心訴求：<em>M365 / Office / collaboration 為主、需要 label 統一控制跨 document / email / Teams / endpoint、insider risk 是主要威脅、且能買到 E5 或對應 add-on</em>。Non-Microsoft 環境或 infrastructure data 為主（BigQuery / S3）走 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / cloud-native policy 更直接、不要硬塞 Purview。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Trainable classifier 的 lifecycle</strong>：classifier 不是 train 一次永久用、business context 變化（產品線改、合約模板更新、合規詞彙變）會讓 precision / recall 下降。Production 應定期 review classifier hit / miss、補新樣本 retrain、跟 SIT 互補不是替代 — 通用 PII 走 SIT 穩定、organization-specific 走 trainable classifier。Staging 跑 2 週驗證 false positive &lt; threshold 才 promote。</p>
<p><strong>Endpoint DLP 跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> Sensitive Data Scanner 的不同層</strong>：Endpoint DLP 在 user action 當下攔截（copy / upload / print）、Datadog Sensitive Data Scanner 在 log / APM ingestion 時 scrub。兩者不互斥 — Endpoint DLP 防 <em>資料離開端點</em>、Datadog Scanner 防 <em>PII 寫進觀測 log</em>、典型 Microsoft + Datadog 環境會並存。</p>
<p><strong>Data Loss Prevention for Power BI</strong>：Power BI dataset / report 可繼承 Purview sensitivity label、export to Excel / PDF 時 label 跟著走、DLP policy 可條件 <em>標 Highly Confidential 的 dataset 不能 export</em>。是 Microsoft analytics stack 比 Tableau / Looker 在 information protection 上的關鍵優勢。</p>
<p><strong>Information Barriers（內部 walled garden）</strong>：合規場景（投行 research vs trading desk、law firm 對手客戶）需 organization 內部某 group 不能 Teams 對話 / 不能 share 檔案、Purview Information Barriers 設定 segment + policy 阻擋。是 compliance-specific feature、非合規環境用不到、但金融 / 法律 / 顧問業是 must-have。</p>
<p><strong>E3 + add-on vs E5 的計費決策</strong>：Purview 完整功能（trainable classifier、Endpoint DLP、Insider Risk、Communication Compliance、Audit Premium）要 E5 license、單價約 E3 的 1.5 倍。中小組織從 E3 + 個別 add-on（Information Protection and Governance E5、Insider Risk Management E5）起步、避免一次 E5 全推；大組織直接 E5 反而簡化計費跟 license 管理。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>DLP policy 寫好但沒觸發</strong>：condition 或 location 設錯（policy 只覆蓋 Exchange 沒包 SharePoint）、或 license 沒解鎖該模組（Endpoint DLP 要額外 add-on）— 在 Compliance Center 看 policy match 統計、確認 license 對應</li>
<li><strong>使用者抱怨 label 選不出來 / 選錯</strong>：label 層級太細 + 沒有預設 label、user 不知該選哪個 — 簡化到 3-5 個 base label、用 auto-labeling 補自動分類、加 label tooltip</li>
<li><strong>Trainable classifier false positive 多</strong>：訓練樣本不足 / 正反例失衡 — 補樣本到 50+ per class、retrain、staging 跑 2 週驗證再 promote</li>
<li><strong>Audit log retention 不夠 / 合規查不到</strong>：預設 180 天、合規要 1 年以上 — 升 E5 或 Audit Premium、或 export 到 SIEM / long-term storage</li>
<li><strong>Insider Risk policy 太敏感 / 太多 case</strong>：預設 template 沒 tune organization baseline — 跑 audit-only 模式 30 天統計、調 threshold、加 user group 排除（VIP / legitimate bulk download role）</li>
<li><strong>Endpoint DLP 攔到合法業務操作</strong>：policy 沒區分 corp managed device vs BYOD、或沒給 user override + justification — 加 device compliance condition、設 warn + justification 而非直接 block</li>
<li><strong>MDA connector 落後 SaaS 新功能</strong>：API connector 有 lag、新功能未涵蓋 — 對高風險 SaaS 補 reverse proxy 模式、或在 SaaS 側設原生 DLP</li>
<li><strong>License 模組混亂</strong>：policy 寫好但功能沒解鎖、admin 不知道哪些要 E5 — 維護 license-to-feature 對照表、Compliance Center 警示「需要 license」要直接修</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure data（GCS / BigQuery）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a></td>
      </tr>
      <tr>
          <td>SIEM / cross-source correlation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Microsoft Sentinel</td>
      </tr>
      <tr>
          <td>Observability log PII scrubbing</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></td>
      </tr>
      <tr>
          <td>單一雲 column / object 級別權限</td>
          <td>BigQuery Column-Level Security / S3 Block Public Access</td>
      </tr>
      <tr>
          <td>AWS-centric data protection</td>
          <td>AWS Macie / <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a></td>
      </tr>
      <tr>
          <td>Endpoint detection 為主（不只 DLP）</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Microsoft 365 / Azure AD 完整管理（屬 <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC + Entra ID</a>）</li>
<li>eDiscovery 跟法律 hold 流程細節</li>
<li>Microsoft Sentinel SIEM 完整配置（屬 SIEM 群、跟 Purview 是互補不是同一頁）</li>
<li>Purview Data Map 對非 Azure 資料源（AWS / GCP / on-prem）的完整 connector 矩陣</li>
<li>Compliance Manager 的法規對照與 scoring 細節</li>
<li>Azure Information Protection (AIP) 舊版 client 的 migration 流程</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Purview 在 07 案例庫沒有直接 vendor-level 事件、但 information protection + insider risk 角度跟多個案例對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Purview 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>客服系統客戶資料應標「Customer Confidential」label、DLP policy 自動阻擋大量匯出、Insider Risk Management 偵測異常 operator 行為</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>Endpoint DLP 在 Microsoft 端點攔截從 Snowflake 下載到 USB / personal cloud 的大量資料；對照啟示是「資料平台外洩仍可在 endpoint 端補位攔截」、不是依賴 Snowflake 自身控制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System 2023</a></td>
          <td>Unified Audit Log 紀錄 support tool 高風險操作、Insider Risk 偵測異常 pattern、跟 SIEM 串接做 cross-source correlation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking Governance (section)</a></td>
          <td>Sensitivity label + DLP policy 是 information protection 的工具、跟 Google DLP transformation 不同層、可並存</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary (section)</a></td>
          <td>Unified Audit Log 是 accountability evidence chain、retention 跟 export 設計是合規證據可用性的關鍵</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>、<a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.8 稽核軌跡與責任邊界</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a>（infrastructure 層 DLP、跟 Purview 並存）、<a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native Data Policy (BigQuery + S3)</a>（resource-bound access control、跟 Purview label-driven 互補）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（Unified Audit Log export 進 SIEM）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC + Entra ID</a>（identity 基底）、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>（log PII scrubbing、不同層互補）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Insider Risk case → IR routing）</li>
<li>官方：<a href="https://learn.microsoft.com/en-us/purview/">Microsoft Purview Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.6 資料庫轉換實作：雙寫、回填、切流與回滾</title><link>https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/</guid><description>&lt;p>資料庫轉換實作的核心責任是讓 schema、資料與流量切換都可分段驗證、並在任一階段可安全回退。這一頁不討論要不要轉換、專注回答「決定要換之後怎麼做」。&lt;/p>
&lt;p>本章跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰&lt;/a> 分工：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>1.6 同 DB 內&lt;/strong>：schema 演進、資料變更、新舊欄位共存、雙寫驗證、切流。例：加欄位、改欄位、拆表、合表、加 partition。&lt;/li>
&lt;li>&lt;strong>1.12 跨 DB 引擎&lt;/strong>：換 vendor（PostgreSQL → Aurora、MongoDB → Cosmos DB、TiDB → DynamoDB）。例：&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%">9.C20 Zomato&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>。&lt;/li>
&lt;/ul>
&lt;p>兩者用同樣的工程方法論（dual-write、shadow、cutover、rollback）、但 &lt;em>stakes&lt;/em> 跟 &lt;em>跨越的邊界&lt;/em> 不同。本章先處理 1.6 的同 DB schema 轉換、1.12 處理更大規模的 cross-engine。若來源是託管平台（Shopify / Firebase / WordPress）的匯出而非自建資料庫、整場遷出的資產線盤點與並行期設計見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出&lt;/a>；資料落地自建後的 schema 演進回到本章、跨引擎搬遷走 1.12。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. 邊界定義&lt;/td>
 &lt;td>定義 source of truth、切換範圍、不可中斷路徑&lt;/td>
 &lt;td>migration scope 與 rollback 邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. Expand&lt;/td>
 &lt;td>新欄位 / 新表先上線、應用可同時讀舊寫新或雙寫&lt;/td>
 &lt;td>新舊版本相容窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Backfill&lt;/td>
 &lt;td>批次回填歷史資料、保留節流與 checkpoint&lt;/td>
 &lt;td>可追蹤的回填進度與失敗重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. 驗證&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read&lt;/a>、checksum、業務指標對帳&lt;/td>
 &lt;td>一致性證據包&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5. Cutover&lt;/td>
 &lt;td>逐步切讀、再切寫、保留快速回切策略&lt;/td>
 &lt;td>切流完成且可回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6. Contract&lt;/td>
 &lt;td>移除舊欄位與舊路徑、收斂技術債&lt;/td>
 &lt;td>單一資料語意落地&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="expand-contract-模式">Expand-Contract 模式&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract&lt;/a>（也叫 parallel change）是同 DB schema 演進的核心模式。&lt;/p>
&lt;p>&lt;strong>為什麼需要這個模式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>應用 deploy 跟 DB migration 不能 &lt;em>原子&lt;/em> 完成&lt;/li>
&lt;li>在 deploy window 內、有些 instance 跑舊 code、有些跑新 code&lt;/li>
&lt;li>DB 必須同時容納舊 code 跟新 code 的 schema&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Expand 階段&lt;/strong>（加新欄位、不刪舊）：&lt;/p>
&lt;ul>
&lt;li>加 &lt;code>new_column&lt;/code>、允許 nullable&lt;/li>
&lt;li>應用層 dual-write：同時寫 &lt;code>old_column&lt;/code> 跟 &lt;code>new_column&lt;/code>&lt;/li>
&lt;li>應用層 read 仍走 &lt;code>old_column&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Backfill 階段&lt;/strong>（資料同步）：&lt;/p>
&lt;ul>
&lt;li>把歷史 row 的 &lt;code>new_column&lt;/code> 補上值（從 &lt;code>old_column&lt;/code> 算出來）&lt;/li>
&lt;li>分批跑、用 checkpoint 追進度、避開 peak&lt;/li>
&lt;li>監控：rate、error、progress、unaffected rows count&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Migrate Reads 階段&lt;/strong>（切讀）：&lt;/p></description><content:encoded><![CDATA[<p>資料庫轉換實作的核心責任是讓 schema、資料與流量切換都可分段驗證、並在任一階段可安全回退。這一頁不討論要不要轉換、專注回答「決定要換之後怎麼做」。</p>
<p>本章跟 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 分工：</p>
<ul>
<li><strong>1.6 同 DB 內</strong>：schema 演進、資料變更、新舊欄位共存、雙寫驗證、切流。例：加欄位、改欄位、拆表、合表、加 partition。</li>
<li><strong>1.12 跨 DB 引擎</strong>：換 vendor（PostgreSQL → Aurora、MongoDB → Cosmos DB、TiDB → DynamoDB）。例：<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>、<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>。</li>
</ul>
<p>兩者用同樣的工程方法論（dual-write、shadow、cutover、rollback）、但 <em>stakes</em> 跟 <em>跨越的邊界</em> 不同。本章先處理 1.6 的同 DB schema 轉換、1.12 處理更大規模的 cross-engine。若來源是託管平台（Shopify / Firebase / WordPress）的匯出而非自建資料庫、整場遷出的資產線盤點與並行期設計見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>；資料落地自建後的 schema 演進回到本章、跨引擎搬遷走 1.12。</p>
<h2 id="實作流程">實作流程</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>核心動作</th>
          <th>交付成果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 邊界定義</td>
          <td>定義 source of truth、切換範圍、不可中斷路徑</td>
          <td>migration scope 與 rollback 邊界</td>
      </tr>
      <tr>
          <td>2. Expand</td>
          <td>新欄位 / 新表先上線、應用可同時讀舊寫新或雙寫</td>
          <td>新舊版本相容窗口</td>
      </tr>
      <tr>
          <td>3. Backfill</td>
          <td>批次回填歷史資料、保留節流與 checkpoint</td>
          <td>可追蹤的回填進度與失敗重試</td>
      </tr>
      <tr>
          <td>4. 驗證</td>
          <td><a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a>、checksum、業務指標對帳</td>
          <td>一致性證據包</td>
      </tr>
      <tr>
          <td>5. Cutover</td>
          <td>逐步切讀、再切寫、保留快速回切策略</td>
          <td>切流完成且可回退</td>
      </tr>
      <tr>
          <td>6. Contract</td>
          <td>移除舊欄位與舊路徑、收斂技術債</td>
          <td>單一資料語意落地</td>
      </tr>
  </tbody>
</table>
<h2 id="expand-contract-模式">Expand-Contract 模式</h2>
<p><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a>（也叫 parallel change）是同 DB schema 演進的核心模式。</p>
<p><strong>為什麼需要這個模式</strong>：</p>
<ul>
<li>應用 deploy 跟 DB migration 不能 <em>原子</em> 完成</li>
<li>在 deploy window 內、有些 instance 跑舊 code、有些跑新 code</li>
<li>DB 必須同時容納舊 code 跟新 code 的 schema</li>
</ul>
<p><strong>Expand 階段</strong>（加新欄位、不刪舊）：</p>
<ul>
<li>加 <code>new_column</code>、允許 nullable</li>
<li>應用層 dual-write：同時寫 <code>old_column</code> 跟 <code>new_column</code></li>
<li>應用層 read 仍走 <code>old_column</code></li>
</ul>
<p><strong>Backfill 階段</strong>（資料同步）：</p>
<ul>
<li>把歷史 row 的 <code>new_column</code> 補上值（從 <code>old_column</code> 算出來）</li>
<li>分批跑、用 checkpoint 追進度、避開 peak</li>
<li>監控：rate、error、progress、unaffected rows count</li>
</ul>
<p><strong>Migrate Reads 階段</strong>（切讀）：</p>
<ul>
<li>應用層 read 改走 <code>new_column</code></li>
<li>仍 dual-write、可以快速 fallback 回 <code>old_column</code></li>
<li>持續 shadow read 驗證一致性</li>
</ul>
<p><strong>Contract 階段</strong>（刪舊）：</p>
<ul>
<li>確認所有 application instance 都跑新 code 後</li>
<li>刪 <code>old_column</code>、停止 dual-write</li>
<li>移除應用層的 fallback 邏輯</li>
</ul>
<p>每個階段都是 <em>可獨立 rollback</em> 的、不像 big-bang 一次切完。</p>
<h2 id="同-db-內常見-migration-類型">同 DB 內常見 migration 類型</h2>
<h3 id="type-a加欄位最簡單">Type A：加欄位（最簡單）</h3>
<ul>
<li>直接 <code>ALTER TABLE ADD COLUMN</code>（nullable 或 default）</li>
<li>應用層後續加寫入、讀取</li>
<li>風險：低</li>
<li>注意：大表 ADD COLUMN with DEFAULT 在 PostgreSQL 11+ 是 instant、之前要 rewrite</li>
</ul>
<h3 id="type-b刪欄位">Type B：刪欄位</h3>
<ul>
<li>先讓所有 application 不再讀寫該欄位</li>
<li>部署完成、確認後再 DROP COLUMN</li>
<li>風險：中</li>
<li>注意：DROP COLUMN 是 instant、但無法 rollback、必須 backup</li>
</ul>
<h3 id="type-c改欄位型別">Type C：改欄位型別</h3>
<ul>
<li>用 expand-contract：加新欄位、dual-write、backfill、切讀、刪舊</li>
<li>風險：高（特別是大表）</li>
<li>注意：直接 <code>ALTER COLUMN TYPE</code> 可能 rewrite 整表、lock 時間長</li>
</ul>
<h3 id="type-d改欄位名--表名">Type D：改欄位名 / 表名</h3>
<ul>
<li>同型別改名：用 expand-contract、加新名 + dual-write、切讀、刪舊</li>
<li>DB 端 native rename 是 instant 但 application 需要同步 update — 不適合大規模 deploy</li>
</ul>
<h3 id="type-e拆表--合表">Type E：拆表 / 合表</h3>
<ul>
<li>拆：先 dual-write 到新舊表、backfill、切讀、刪舊</li>
<li>合：先 dual-write 到新表、backfill、切讀、刪舊</li>
<li>風險：高 — 影響面廣</li>
</ul>
<h3 id="type-f加-index">Type F：加 index</h3>
<ul>
<li>PostgreSQL：<code>CREATE INDEX CONCURRENTLY</code>（不 lock 表、可能 slow）</li>
<li>MySQL：<code>gh-ost</code> / <code>pt-online-schema-change</code>（ghost table）</li>
<li>風險：低-中（看 index 大小）</li>
</ul>
<h3 id="type-g加-not-null-constraint">Type G：加 NOT NULL constraint</h3>
<ul>
<li>先確保 application 所有 instance 都不寫 null</li>
<li>backfill null 為 default</li>
<li>加 NOT NULL constraint</li>
<li>風險：中</li>
</ul>
<h3 id="type-h加-partition">Type H：加 partition</h3>
<ul>
<li>先把現有表變成 partition 0</li>
<li>加新 partition 接新資料</li>
<li>漸進把舊資料 move 到對應 partition</li>
<li>風險：高（schema 大變）</li>
</ul>
<h2 id="online-schema-change-工具">Online Schema Change 工具</h2>
<p>大表 ALTER TABLE 直接跑會 lock。生產級 migration 用 online schema change 工具：</p>
<p><strong>PostgreSQL</strong>：</p>
<ul>
<li><code>CREATE INDEX CONCURRENTLY</code>（內建）</li>
<li><code>pg_repack</code>（vacuum + reindex without lock）</li>
<li><code>pgroll</code>（zero-downtime migration）</li>
<li>Atlas（schema-as-code）</li>
</ul>
<p><strong>MySQL</strong>：</p>
<ul>
<li><code>gh-ost</code>（GitHub 開源、無觸發器、推薦）</li>
<li><code>pt-online-schema-change</code>（Percona、用觸發器）</li>
<li>Vitess online DDL（managed via Vitess）</li>
</ul>
<p><strong>機制概要</strong>：</p>
<ul>
<li>建 ghost table（新 schema）</li>
<li>copy 資料到 ghost table（漸進、avoid peak）</li>
<li>用 trigger 或 binlog 同步 ongoing changes</li>
<li>切換：原 table → ghost table（atomic rename）</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor page</a> 跟 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a> 的相關段落。</p>
<h2 id="validation-query-設計">Validation Query 設計</h2>
<p>migration 過程中必須有 <em>validation query</em> 確認資料一致性。</p>
<p><strong>Checksum 對比</strong>：</p>
<ul>
<li>跑 <code>MD5(new_column) = MD5(derived_from_old)</code></li>
<li>抽樣 10% 跑、不打全表</li>
<li>不一致 → 修轉換函式、不直接修資料</li>
</ul>
<p><strong>Row count 對比</strong>：</p>
<ul>
<li>新欄位 NULL count 跟預期 backfill 進度比對</li>
<li>過慢 → 增加 backfill worker</li>
<li>不一致 → 找出 backfill 漏跑的 batch</li>
</ul>
<p><strong>業務指標對比</strong>：</p>
<ul>
<li>跟業務 metric 對齊（訂單金額總和、用戶數）</li>
<li>比 row-level checksum 更貼近 business correctness</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">Validation Query 卡片</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h2 id="backfill-設計">Backfill 設計</h2>
<p>backfill 是 migration 中最 <em>容易出錯</em> 的環節 — 大量寫、影響 production。</p>
<p><strong>設計要點</strong>：</p>
<ol>
<li><strong>節流（throttle）</strong>：每秒寫入限制、跟 production peak 錯開</li>
<li><strong>Checkpoint</strong>：紀錄進度、可 resume</li>
<li><strong>錯誤分類</strong>：可 retry 的錯誤 vs 必須人工處理</li>
<li><strong>dry-run mode</strong>：先看會修改多少、不實際寫</li>
<li><strong>monitoring</strong>：rate、error、progress、replica lag</li>
</ol>
<p><strong>backfill 反模式</strong>：</p>
<ul>
<li>一個大 transaction 跑全表 → lock 太久、可能 OOM</li>
<li>沒 checkpoint → 中途失敗從頭開始</li>
<li>沒 throttle → 影響 production read</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill 卡片</a>。</p>
<h2 id="各階段監控訊號">各階段監控訊號</h2>
<p>每階段都要監控、不只是「最後驗證」：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>主要訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Expand</td>
          <td>DDL 執行時間、replication lag</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>rate、error rate、checkpoint progress、production load 影響</td>
      </tr>
      <tr>
          <td>驗證</td>
          <td>shadow read 不一致率、checksum 結果、業務 metric 差異</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>error rate、p99 latency、rollback trigger 是否就緒</td>
      </tr>
      <tr>
          <td>Contract</td>
          <td>DDL 執行時間、無 application 還在用舊 column 的證據</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>回填速度不穩、延遲飆高</td>
          <td>可能與線上流量競爭 IOPS</td>
          <td>降低批次大小、加節流、避開 peak</td>
      </tr>
      <tr>
          <td>雙寫成功率高但 shadow read 漂移</td>
          <td>業務語意映射不一致</td>
          <td>先修轉換函式、再重跑對帳</td>
      </tr>
      <tr>
          <td>切流後 error rate 升高</td>
          <td>新庫讀寫路徑與索引未對齊</td>
          <td>回切舊讀路徑、補索引後再灰度</td>
      </tr>
      <tr>
          <td>rollback 時間超出 RTO</td>
          <td>回退流程過度人工</td>
          <td>把回退腳本化並演練</td>
      </tr>
      <tr>
          <td>大表 ALTER TABLE 卡住</td>
          <td>online 工具沒用對 / lock</td>
          <td>用 gh-ost / pgroll、或分批執行</td>
      </tr>
      <tr>
          <td>Backfill 後 NULL count 不歸零</td>
          <td>有漏跑的 batch、或新寫入沒走 dual-write</td>
          <td>補檢查 dual-write 邏輯、re-run backfill</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把資料庫轉換當成單次 DDL 任務、會讓風險集中在 cutover 當下。穩定做法是把每一階段都做成可驗證、可回退的獨立里程碑。</p>
<p>把 dual-write 當成最終保障也常出錯。雙寫只能保證「兩邊都有寫」、不保證「語意一致」、仍要配 shadow read 與業務對帳。</p>
<p>把 online schema change 工具當「萬能」也是錯。gh-ost / pgroll 仍有 <em>限制</em>（例如 trigger 限制、IO 影響）、要按工具規格操作。</p>
<h2 id="案例回寫">案例回寫</h2>
<ul>
<li>選型層案例： <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a></li>
<li>可靠性治理： <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a></li>
<li>事故反饋： <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a></li>
<li>大規模跨 DB 遷移： <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（<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</a>、<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</a>、<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">Microsoft 365</a> 等 case）</li>
</ul>
<p>這組案例主要支撐的是「分段切換與可回退驗證」判讀、不直接支撐快取 TTL 或 broker delivery 參數；若問題核心在快取新鮮度或投遞語意、應轉到 2.x 或 3.x。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位演進與命名語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a>。</li>
<li>與 1.3 的交接：交易邊界與副作用切分回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.7 的交接：production rollout 證據實作 — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a>。</li>
<li>與 1.12 的交接：跨 DB 引擎遷移 — <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">大規模 DB 遷移實戰</a>。</li>
<li>與 4.20 的交接：validation query 與一致性證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.11 / 6.8 的交接：放行與停損條件進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 8.19 的交接：pause、rollback、fail-forward 決策記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>若你還在判斷是否該轉換、先回 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4</a> 看決策訊號。若你要把這套流程寫成 production rollout evidence、接著讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據實作示範</a>。若你在設計放行與演練、接著看 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a>。若你在事故回溯、接著看 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.23 Post-incident Review</a>。若你要做 <em>跨 DB 引擎遷移</em>、看 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12</a>。</p>
]]></content:encoded></item><item><title>SQLite</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/</guid><description>&lt;p>SQLite 是世界上部署最多的 DB（手機、瀏覽器、car、IoT 都有）。傳統定位是 embedded、單檔案與低操作成本資料庫；multi-tenant 網路服務通常會先看 PostgreSQL、MySQL 或 managed SQL。但近年因 Cloudflare D1（serverless SQLite）、Turso（distributed SQLite）、Litestream（SQLite replication）等服務興起，出現「SQLite as production DB」的新場景。&lt;/p>
&lt;h2 id="教學路線單檔正式狀態與-local-first">教學路線：單檔正式狀態與 local-first&lt;/h2>
&lt;p>SQLite 服務頁的教學目標是把單機、單檔案、edge、desktop、test fixture 的正式狀態責任說清楚。讀者讀完後要能判斷 SQLite 何時是 production state，何時要轉向 server database、edge KV 或分散式 SQLite 變體。&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>Embedded state&lt;/td>
 &lt;td>單檔案資料庫如何成為 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local-first&lt;/td>
 &lt;td>device、edge、desktop、test fixture 的責任形狀&lt;/td>
 &lt;td>適用場景、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Writer boundary&lt;/td>
 &lt;td>single writer、file lock、WAL 如何決定服務上限&lt;/td>
 &lt;td>容量特性、容量規劃要點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distributed variants&lt;/td>
 &lt;td>Turso、LiteFS、rqlite、D1 解決哪類同步或 edge 問題&lt;/td>
 &lt;td>跟其他 vendor 的取捨、章節群結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時升級 PostgreSQL、MySQL、DynamoDB 或 edge KV&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位單檔案-embedded--新興分散式-sqlite-生態">定位：單檔案 embedded + 新興分散式 SQLite 生態&lt;/h2>
&lt;p>SQLite 跟 PostgreSQL / MySQL 承擔不同層級的資料責任：&lt;/p>
&lt;ul>
&lt;li>以 function-call API 使用，省掉 server process&lt;/li>
&lt;li>單一檔案（含 schema、data、index、metadata）&lt;/li>
&lt;li>無 user / role / connection 概念&lt;/li>
&lt;li>同 process 同時 read / write 受 file lock 限制&lt;/li>
&lt;/ul>
&lt;p>傳統定位：test fixture、CLI tool data store、mobile app（iOS / Android 內建）、edge device。&lt;/p>
&lt;p>新興定位：edge serverless（Cloudflare D1）、distributed SQLite（Turso、rqlite）、replicated SQLite（Litestream）。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>&lt;strong>單檔案上限&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>DB 最大 281 TB（理論）&lt;/li>
&lt;li>實務上單表 &amp;gt; 100 GB 開始有 vacuum / index 問題&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>並發寫&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>WAL mode：可同時多 reader + 1 writer&lt;/li>
&lt;li>寫入仍由 single writer boundary 控制&lt;/li>
&lt;li>寫吞吐受 disk fsync 限制（通常 &amp;lt; 1K WPS）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>並發讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>WAL mode 多 reader 可同時跑&lt;/li>
&lt;li>read-only workload 可以撐高吞吐&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Cross-process / cross-instance&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>多個 process / instance 同時寫同一檔案會破壞 single writer boundary&lt;/li>
&lt;li>需要分散時用 Litestream（replication）或 Turso（distributed）&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. Test fixture / CI 用 DB&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>整合測試需要的 fixed DB&lt;/li>
&lt;li>比 spin up PostgreSQL container 快&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter&lt;/a> 的 contract test 模式&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. CLI tool / desktop app 內建 store&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>SQLite 是世界上部署最多的 DB（手機、瀏覽器、car、IoT 都有）。傳統定位是 embedded、單檔案與低操作成本資料庫；multi-tenant 網路服務通常會先看 PostgreSQL、MySQL 或 managed SQL。但近年因 Cloudflare D1（serverless SQLite）、Turso（distributed SQLite）、Litestream（SQLite replication）等服務興起，出現「SQLite as production DB」的新場景。</p>
<h2 id="教學路線單檔正式狀態與-local-first">教學路線：單檔正式狀態與 local-first</h2>
<p>SQLite 服務頁的教學目標是把單機、單檔案、edge、desktop、test fixture 的正式狀態責任說清楚。讀者讀完後要能判斷 SQLite 何時是 production state，何時要轉向 server database、edge KV 或分散式 SQLite 變體。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Embedded state</td>
          <td>單檔案資料庫如何成為 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a></td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Local-first</td>
          <td>device、edge、desktop、test fixture 的責任形狀</td>
          <td>適用場景、案例對照</td>
      </tr>
      <tr>
          <td>Writer boundary</td>
          <td>single writer、file lock、WAL 如何決定服務上限</td>
          <td>容量特性、容量規劃要點</td>
      </tr>
      <tr>
          <td>Distributed variants</td>
          <td>Turso、LiteFS、rqlite、D1 解決哪類同步或 edge 問題</td>
          <td>跟其他 vendor 的取捨、章節群結構</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時升級 PostgreSQL、MySQL、DynamoDB 或 edge KV</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位單檔案-embedded--新興分散式-sqlite-生態">定位：單檔案 embedded + 新興分散式 SQLite 生態</h2>
<p>SQLite 跟 PostgreSQL / MySQL 承擔不同層級的資料責任：</p>
<ul>
<li>以 function-call API 使用，省掉 server process</li>
<li>單一檔案（含 schema、data、index、metadata）</li>
<li>無 user / role / connection 概念</li>
<li>同 process 同時 read / write 受 file lock 限制</li>
</ul>
<p>傳統定位：test fixture、CLI tool data store、mobile app（iOS / Android 內建）、edge device。</p>
<p>新興定位：edge serverless（Cloudflare D1）、distributed SQLite（Turso、rqlite）、replicated SQLite（Litestream）。</p>
<h2 id="容量特性">容量特性</h2>
<p><strong>單檔案上限</strong>：</p>
<ul>
<li>DB 最大 281 TB（理論）</li>
<li>實務上單表 &gt; 100 GB 開始有 vacuum / index 問題</li>
</ul>
<p><strong>並發寫</strong>：</p>
<ul>
<li>WAL mode：可同時多 reader + 1 writer</li>
<li>寫入仍由 single writer boundary 控制</li>
<li>寫吞吐受 disk fsync 限制（通常 &lt; 1K WPS）</li>
</ul>
<p><strong>並發讀</strong>：</p>
<ul>
<li>WAL mode 多 reader 可同時跑</li>
<li>read-only workload 可以撐高吞吐</li>
</ul>
<p><strong>Cross-process / cross-instance</strong>：</p>
<ul>
<li>多個 process / instance 同時寫同一檔案會破壞 single writer boundary</li>
<li>需要分散時用 Litestream（replication）或 Turso（distributed）</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. Test fixture / CI 用 DB</strong>：</p>
<ul>
<li>整合測試需要的 fixed DB</li>
<li>比 spin up PostgreSQL container 快</li>
<li>對應 <a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter</a> 的 contract test 模式</li>
</ul>
<p><strong>2. CLI tool / desktop app 內建 store</strong>：</p>
<ul>
<li>Chrome / Firefox（cookies、history、bookmark）、Fossil SCM、iOS app</li>
<li>省掉 server、單檔案攜帶</li>
</ul>
<p><strong>3. Mobile app（iOS / Android）</strong>：</p>
<ul>
<li>iOS Core Data 底層用 SQLite</li>
<li>Android 自帶 SQLite API</li>
<li>offline-first app 的標準</li>
</ul>
<p><strong>4. Single-instance backend（特殊場景）</strong>：</p>
<ul>
<li>流量小 + HA 由備份 / restore / redeploy 流程承擔</li>
<li>例：Sidekick / 個人 SaaS / family-scale app</li>
<li>配合 Litestream 做 backup / DR</li>
</ul>
<p><strong>5. Edge / serverless（新興）</strong>：</p>
<ul>
<li>Cloudflare D1：edge SQLite、跟 Workers 整合</li>
<li>Turso：distributed SQLite、跨 region replication</li>
<li>跟傳統 SQLite 不同等級、是 <em>新的 product</em></li>
</ul>
<p><strong>6. Embedded device / IoT</strong>：</p>
<ul>
<li>沒網路或要降低 server 依賴</li>
<li>SQLite 內建、無 external dependency</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 多 instance / 多 region web service</strong>：</p>
<ul>
<li>SQLite 的單檔模型以單 instance writer 為主要邊界</li>
<li>替代：PostgreSQL、Aurora、Spanner、CockroachDB</li>
</ul>
<p><strong>2. 高寫入吞吐（&gt; 1K WPS）</strong>：</p>
<ul>
<li>fsync 限制</li>
<li>替代：任何 server-based RDBMS</li>
</ul>
<p><strong>3. Multi-user 權限管理</strong>：</p>
<ul>
<li>無 user / role 概念</li>
<li>替代：PostgreSQL / MySQL</li>
</ul>
<p><strong>4. 跨機器 transaction</strong>：</p>
<ul>
<li>SQLite 是 single-machine</li>
<li>替代：分散式 SQL</li>
</ul>
<p><strong>5. 大規模 production OLTP</strong>：</p>
<ul>
<li>大規模 production OLTP 需要 server database 的 HA、replica、權限與操作邊界</li>
<li>替代：MySQL / PostgreSQL / Aurora</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs PostgreSQL（作為 test DB）</strong>：</p>
<ul>
<li>SQLite：快 spin up、SQL dialect 接近但有差異</li>
<li>PostgreSQL：跟 production 一致、發現的 bug 真實</li>
<li>選 SQLite：speed of iteration、簡單 query</li>
<li>選 PostgreSQL：catch production-like bug、PostgreSQL-specific 特性測試</li>
</ul>
<p><strong>vs Cloudflare D1</strong>：</p>
<ul>
<li>SQLite（local）：單機、自管</li>
<li>D1：edge serverless、跟 Workers 整合</li>
<li>選 SQLite：embedded / CLI / app 場景</li>
<li>選 D1：edge web service、跟 Cloudflare 生態整合</li>
</ul>
<p><strong>vs Turso（distributed SQLite）</strong>：</p>
<ul>
<li>SQLite：單機、單檔案</li>
<li>Turso：distributed、跨 region replication、SQLite-compatible</li>
<li>選 SQLite：simple use case</li>
<li>選 Turso：需要 SQLite simplicity + 全球分散</li>
</ul>
<p><strong>vs Litestream（replicated SQLite）</strong>：</p>
<ul>
<li>SQLite：單檔案</li>
<li>Litestream：把 SQLite 變成 streaming replicated 到 S3</li>
<li>選 Litestream：想要 SQLite simplicity + DR</li>
</ul>
<p><strong>vs Firebase / Firestore（mobile app）</strong>：</p>
<ul>
<li>SQLite：embedded、offline-first、無 sync</li>
<li>Firestore：realtime、自動 sync、雲端 store</li>
<li>選 SQLite：offline-first、單機</li>
<li>選 Firestore：multi-device sync、realtime</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. WAL mode 是 production baseline</strong>：</p>
<ul>
<li>default journal mode 是 rollback journal（每寫都 lock）</li>
<li>WAL（Write-Ahead Log）讓多 reader 可同時跑</li>
<li><code>PRAGMA journal_mode = WAL</code></li>
</ul>
<p><strong>2. fsync 配置</strong>：</p>
<ul>
<li><code>PRAGMA synchronous = FULL</code>（durable、慢）</li>
<li><code>PRAGMA synchronous = NORMAL</code>（faster、少數情況可能掉資料）</li>
<li><code>PRAGMA synchronous = OFF</code>（最快、不安全）</li>
</ul>
<p><strong>3. mmap 加速 read</strong>：</p>
<ul>
<li><code>PRAGMA mmap_size = 268435456</code>（256 MB）</li>
<li>把 DB 部分內容 mmap 進 RAM、加速 read</li>
</ul>
<p><strong>4. Cache size</strong>：</p>
<ul>
<li><code>PRAGMA cache_size = -64000</code>（64 MB cache）</li>
<li>大 cache 對 read-heavy workload 有幫助</li>
</ul>
<p><strong>5. Auto-vacuum</strong>：</p>
<ul>
<li>預設 off、delete 後檔案不縮小</li>
<li><code>PRAGMA auto_vacuum = INCREMENTAL</code> + 定期 <code>PRAGMA incremental_vacuum</code></li>
</ul>
<h2 id="章節群結構">章節群結構</h2>
<p>SQLite 章節群的責任是把單檔正式狀態、embedded process、writer boundary、backup / restore、test fixture、local-first 與 edge SQLite 變體拆成可教學路線。完整結構見 <a href="teaching-structure/">SQLite Teaching Structure</a>；下表列出目前已建立的 deep article、hands-on 與 migration route。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>文件</th>
          <th>狀態</th>
          <th>教學責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>結構總覽</td>
          <td><a href="teaching-structure/">Teaching Structure</a></td>
          <td>已有正文</td>
          <td>對齊 PG / MySQL 與 LLM 架構，固定 SQLite 後續讀法</td>
      </tr>
      <tr>
          <td>Core deep</td>
          <td><a href="file-lifecycle-backup-boundary/">File lifecycle / backup boundary</a></td>
          <td>已有正文</td>
          <td>WAL sidecar、backup API、restore drill、corruption route</td>
      </tr>
      <tr>
          <td>Hands-on</td>
          <td><a href="hands-on/">Hands-on 操作路線</a></td>
          <td>已有正文</td>
          <td>local file、backup restore、WAL busy、migration fixture</td>
      </tr>
      <tr>
          <td>Concurrency</td>
          <td><a href="wal-concurrency-locking/">WAL concurrency / locking</a></td>
          <td>已有正文</td>
          <td>single writer、file lock、<code>SQLITE_BUSY</code>、checkpoint</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td><a href="pragma-tuning-performance/">PRAGMA tuning / performance</a></td>
          <td>已有正文</td>
          <td>journal、sync、cache、mmap、vacuum 的取捨</td>
      </tr>
      <tr>
          <td>Migration</td>
          <td><a href="schema-migration-versioning/">Schema migration / versioning</a></td>
          <td>已有正文</td>
          <td>app release、schema version、rollback、migration evidence</td>
      </tr>
      <tr>
          <td>Testing</td>
          <td><a href="test-fixture-best-practice/">Test fixture best practice</a></td>
          <td>已有正文</td>
          <td>SQLite 測試便利性與 production dialect gap</td>
      </tr>
      <tr>
          <td>Embedded app</td>
          <td><a href="mobile-desktop-embedded-store/">Mobile / desktop embedded store</a></td>
          <td>已有正文</td>
          <td>device local state、privacy、backup、app version</td>
      </tr>
      <tr>
          <td>Sync</td>
          <td><a href="local-first-sync-boundary/">Local-first sync boundary</a></td>
          <td>已有正文</td>
          <td>多裝置同步、conflict、server authority</td>
      </tr>
      <tr>
          <td>Edge variant</td>
          <td><a href="d1-turso-libsql-comparison/">D1 / Turso / libSQL comparison</a></td>
          <td>已有正文</td>
          <td>edge SQLite 產品與 local SQLite 的責任差異</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td><a href="litestream-litefs-replication/">Litestream / LiteFS replication</a></td>
          <td>已有正文</td>
          <td>continuous backup、read replica、failover boundary</td>
      </tr>
      <tr>
          <td>SQL compatibility</td>
          <td><a href="sql-dialect-index-limits/">SQL dialect and index limits</a></td>
          <td>已有正文</td>
          <td>type affinity、index、constraint、PostgreSQL / MySQL gap</td>
      </tr>
      <tr>
          <td>Operations</td>
          <td><a href="observability-runbook/">Observability / runbook</a></td>
          <td>已有正文</td>
          <td>busy errors、WAL growth、backup evidence、incident route</td>
      </tr>
      <tr>
          <td>Migration route</td>
          <td><a href="migrate-to-postgresql/">SQLite to PostgreSQL</a></td>
          <td>已有正文</td>
          <td>多 tenant、權限、HA、audit 出現時的升級路線</td>
      </tr>
      <tr>
          <td>Migration route</td>
          <td><a href="migrate-to-d1-turso/">SQLite to D1 / Turso</a></td>
          <td>已有正文</td>
          <td>edge / serverless 化路線</td>
      </tr>
      <tr>
          <td>Migration route</td>
          <td><a href="migrate-from-postgresql-simplification/">PostgreSQL to SQLite simplification</a></td>
          <td>已有正文</td>
          <td>single-user / embedded 工具的反向簡化路線</td>
      </tr>
  </tbody>
</table>
<p>章節群的讀法是先讀 file lifecycle，再按壓力選 deep article。若問題是 write contention，讀 WAL locking；若問題是測試，讀 test fixture；若問題是 edge / serverless，讀 D1 / Turso comparison；若問題是服務長大，讀 SQLite to PostgreSQL migration。</p>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>SQLite 的低操作成本容易讓團隊忽略它的 writer boundary。這一段先說何時維持 SQLite，再說何時升級到 server SQL、edge SQLite 變體或 managed KV。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local SQLite</td>
          <td>單 process、單 writer、資料可用檔案備份保護</td>
          <td>多 instance 寫入、需要 HA、需要資料層權限</td>
          <td><a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">Database</a>、<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Source of Truth</a></td>
      </tr>
      <tr>
          <td>WAL + file backup</td>
          <td>read-heavy、寫入量低、RPO 可接受定期 snapshot</td>
          <td>restore 演練失敗、WAL growth 失控、RPO / RTO 變嚴格</td>
          <td><a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a></td>
      </tr>
      <tr>
          <td>Litestream / LiteFS</td>
          <td>單 primary 寫入清楚、主要需求是 backup 或 read replica</td>
          <td>需要多地 active write、跨 region transaction</td>
          <td><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a></td>
      </tr>
      <tr>
          <td>Cloudflare D1 / Turso</td>
          <td>edge / serverless 生態已是主平台</td>
          <td>SQL 特性、migration、observability 或 vendor 限制卡住</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></td>
      </tr>
      <tr>
          <td>PostgreSQL / MySQL</td>
          <td>application 已進入多服務、多 tenant、權限與備份治理需求</td>
          <td>schema migration、connection、audit 與 failover 成主題</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor</a></td>
      </tr>
  </tbody>
</table>
<p>SQLite 的簡單路徑是讓檔案生命週期成為正式操作流程。只要單一 writer、備份、restore、migration 與 file ownership 都能被 runbook 控制，SQLite 可以是正式狀態，而非臨時 cache。</p>
<p>升級到 server SQL 的訊號是操作責任超過檔案邊界。當團隊需要資料庫帳號、權限分層、read replica、線上 schema migration、集中 audit 或跨 instance failover 時，PostgreSQL / MySQL / Aurora 會比繼續包裝 SQLite 更清楚。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>SQLite overview 目前已完成服務判斷與章節群正文路由。File lifecycle、WAL locking、PRAGMA tuning、schema migration、test fixture、local-first sync、edge product 差異、observability、hands-on 與 migration route 都已有對應正文；下一輪審查可集中在案例補強、引用精度與跨章重複整理。</p>
<h2 id="案例對照">案例對照</h2>
<p>SQLite 不在 09 case 庫的「規模化 vendor」類別、但作為 <em>embedded 跟 test</em> 廣泛使用：</p>
<ul>
<li>iOS Core Data：所有 iOS app 的 default DB</li>
<li>Chrome / Firefox：cookie、history、bookmark</li>
<li>Fossil SCM：repository metadata 與 application-file use case</li>
<li>Cloudflare D1：edge serverless（新興 production 場景）</li>
<li>Turso：distributed SQLite（新興 production 場景）</li>
</ul>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>default journal mode 不改 WAL</strong>：read 跟 write 互相 block、performance 差</li>
<li><strong>多 process / instance 同時寫同檔</strong>：corruption</li>
<li><strong>delete 後檔案沒縮小</strong>：忘了 vacuum</li>
<li><strong>synchronous=OFF 給 production</strong>：power loss 可能掉資料</li>
<li><strong>SQLite 跟 PostgreSQL 行為差異測試不足</strong>：SQLite test 過、PostgreSQL production 出 bug（特別是 date / time、NULL 處理、type coercion）</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a> / <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor</a>（production server-based RDBMS）</li>
<li>上游：<a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter</a>（test fixture 模式）</li>
<li>結構：<a href="/blog/backend/01-database/vendors/sqlite/teaching-structure/" data-link-title="SQLite Teaching Structure" data-link-desc="SQLite 服務章節群的大綱：從 embedded formal state、WAL、backup、test fixture、local-first、edge SQLite 到遷移路由">SQLite Teaching Structure</a>（完整章節群與寫作順序）</li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a>（local file、backup restore、WAL busy reproduction、migration fixture、D1 / Turso preview）</li>
<li>深入：<a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">SQLite file lifecycle 與 backup boundary</a>（WAL、backup、restore、file ownership）</li>
<li>官方：<a href="https://sqlite.org/docs.html">SQLite Documentation</a>、<a href="https://litestream.io/">Litestream</a>、<a href="https://turso.tech/">Turso</a>、<a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a></li>
</ul>
]]></content:encoded></item><item><title>9.6 容量規劃模型</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>容量規劃的責任是把「未來 N 個月可能多大」翻成「現在該訂多少 capacity」。這層工作不純靠歷史外推、要結合業務 forecast、事件型成長、頂部風險 buffer。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的關係：9.4 提供「當前配置能撐多少」、9.6 用這個數字加上 forecast 推「該規劃多少」。沒有 9.4 的 baseline、9.6 只是猜；沒有 9.6 的 forecast、9.4 的 baseline 只是 snapshot。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸&lt;/a> 的關係：9.13 先決定「沿哪條軸擴」（垂直 / 水平 / Y 軸拆服務 / Z 軸 partition），9.6 才能算出「該擴多少」。同樣是「處理 10 倍流量」、選垂直擴展要算單機規格上限、選水平擴展要算協調成本跟連線池放大、選 Y 軸拆服務要算跨服務 latency budget — 三條軸的容量公式參數完全不同。沒先做 9.13、9.6 的數字會落到錯誤的擴展軸上。&lt;/p>
&lt;p>本章是「規劃決策」的章節、不是執行手冊。讀完後讀者能回答：peak 怎麼預測、headroom 訂多少、autoscaler 怎麼配、不可水平擴的服務怎麼處理。&lt;/p>
&lt;h2 id="容量公式三項">容量公式三項&lt;/h2>
&lt;p>容量規劃的核心公式可以濃縮成三項相乘：&lt;code>容量 = 預期峰值 × (1 + headroom) / 可擴容速度&lt;/code>。每一項都需要獨立分析：&lt;/p>
&lt;p>&lt;strong>預期峰值（peak forecast）&lt;/strong>：歷史 baseline × 預期成長 × 事件因子。三項中最影響整體準度。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast 卡片&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Headroom budget&lt;/strong>：通常 30-50%、為了應付異常 burst + AZ 故障 + forecast 誤差。不同工作負載 headroom 不同。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &amp;#43; AZ 故障 &amp;#43; forecast 誤差的安全餘量">Headroom Budget 卡片&lt;/a>。&lt;/p>
&lt;p>&lt;strong>可擴容速度（reactive vs predictive）&lt;/strong>：autoscaler 反應時間 vs 流量上升速度。如果流量上升比 autoscaler 快、必須 &lt;em>提前&lt;/em> pre-scale、不能等 reactive 反應。&lt;/p>
&lt;p>這個公式的另一個寫法是「容量 = peak × 安全係數」、安全係數 = (1 + headroom) / 可擴容速度。預測準 + 擴容快 → 安全係數小、容量緊湊；預測差 + 擴容慢 → 安全係數大、成本高。&lt;/p>
&lt;h2 id="peak-forecast-方法">Peak forecast 方法&lt;/h2>
&lt;p>Forecast 方法分三層、按業務型態選用。&lt;/p>
&lt;p>&lt;strong>歷史線性外推&lt;/strong>：拿過去 N 個月的趨勢、按斜率外推到下 N 個月。適合 sustained growth（B2B SaaS 月增 X%）；不適合 event peak（年度活動）跟 surge（產品爆紅）。&lt;/p>
&lt;p>&lt;strong>季節性分解（STL：Seasonal-Trend decomposition using Loess）&lt;/strong>：把長期趨勢、週期成分、殘差分開預測。適合電商（雙 11 / Black Friday）、串流（IPL / Super Bowl）、零售（聖誕節）。需要 &lt;em>至少兩個完整 cycle&lt;/em> 的歷史資料。&lt;/p>
&lt;p>&lt;strong>業務 ML 模型&lt;/strong>：結合 marketing pipeline（廣告投入）、新用戶獲取（acquisition rate）、留存率、產品變化等多 feature。最精準但成本高、需要 ML team。&lt;/p>
&lt;p>&lt;strong>最常見錯誤是「拿去年同期 × (1 + 預期成長 %)」&lt;/strong>：忽略產品改動 + 行銷投入變化 + 外部事件。Prime Day 2025 vs 2024 不只是 +30% — 是 AI shopping assistant 上線、是 ad spend 變化、是新國家上線。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>容量規劃的責任是把「未來 N 個月可能多大」翻成「現在該訂多少 capacity」。這層工作不純靠歷史外推、要結合業務 forecast、事件型成長、頂部風險 buffer。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的關係：9.4 提供「當前配置能撐多少」、9.6 用這個數字加上 forecast 推「該規劃多少」。沒有 9.4 的 baseline、9.6 只是猜；沒有 9.6 的 forecast、9.4 的 baseline 只是 snapshot。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的關係：9.13 先決定「沿哪條軸擴」（垂直 / 水平 / Y 軸拆服務 / Z 軸 partition），9.6 才能算出「該擴多少」。同樣是「處理 10 倍流量」、選垂直擴展要算單機規格上限、選水平擴展要算協調成本跟連線池放大、選 Y 軸拆服務要算跨服務 latency budget — 三條軸的容量公式參數完全不同。沒先做 9.13、9.6 的數字會落到錯誤的擴展軸上。</p>
<p>本章是「規劃決策」的章節、不是執行手冊。讀完後讀者能回答：peak 怎麼預測、headroom 訂多少、autoscaler 怎麼配、不可水平擴的服務怎麼處理。</p>
<h2 id="容量公式三項">容量公式三項</h2>
<p>容量規劃的核心公式可以濃縮成三項相乘：<code>容量 = 預期峰值 × (1 + headroom) / 可擴容速度</code>。每一項都需要獨立分析：</p>
<p><strong>預期峰值（peak forecast）</strong>：歷史 baseline × 預期成長 × 事件因子。三項中最影響整體準度。詳見 <a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast 卡片</a>。</p>
<p><strong>Headroom budget</strong>：通常 30-50%、為了應付異常 burst + AZ 故障 + forecast 誤差。不同工作負載 headroom 不同。詳見 <a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget 卡片</a>。</p>
<p><strong>可擴容速度（reactive vs predictive）</strong>：autoscaler 反應時間 vs 流量上升速度。如果流量上升比 autoscaler 快、必須 <em>提前</em> pre-scale、不能等 reactive 反應。</p>
<p>這個公式的另一個寫法是「容量 = peak × 安全係數」、安全係數 = (1 + headroom) / 可擴容速度。預測準 + 擴容快 → 安全係數小、容量緊湊；預測差 + 擴容慢 → 安全係數大、成本高。</p>
<h2 id="peak-forecast-方法">Peak forecast 方法</h2>
<p>Forecast 方法分三層、按業務型態選用。</p>
<p><strong>歷史線性外推</strong>：拿過去 N 個月的趨勢、按斜率外推到下 N 個月。適合 sustained growth（B2B SaaS 月增 X%）；不適合 event peak（年度活動）跟 surge（產品爆紅）。</p>
<p><strong>季節性分解（STL：Seasonal-Trend decomposition using Loess）</strong>：把長期趨勢、週期成分、殘差分開預測。適合電商（雙 11 / Black Friday）、串流（IPL / Super Bowl）、零售（聖誕節）。需要 <em>至少兩個完整 cycle</em> 的歷史資料。</p>
<p><strong>業務 ML 模型</strong>：結合 marketing pipeline（廣告投入）、新用戶獲取（acquisition rate）、留存率、產品變化等多 feature。最精準但成本高、需要 ML team。</p>
<p><strong>最常見錯誤是「拿去年同期 × (1 + 預期成長 %)」</strong>：忽略產品改動 + 行銷投入變化 + 外部事件。Prime Day 2025 vs 2024 不只是 +30% — 是 AI shopping assistant 上線、是 ad spend 變化、是新國家上線。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day 年增率 +30% ~ +77%</a> — 連 Amazon 自家每年成長都不能線性外推；<a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布</a> — 事件型 forecast、按過去新片 metric 預估。</p>
<p>Forecast 必須有 <em>誤差範圍</em>、不能單一數字。給上下界（最壞 / 預期 / 最好）、容量規劃才能用 worst-case 訂 baseline。</p>
<h2 id="headroom-budget-設計">Headroom budget 設計</h2>
<p>Headroom 不是 over-provisioning 浪費、是容量規劃的安全邊界。常見比例 30-50%、按 saturation 行為跟工作負載敏感度調整。</p>
<p><strong>為什麼是 30-50% 而不是 10%</strong>：</p>
<ul>
<li>forecast 誤差：預測準度通常 ±20-30%</li>
<li>burst pattern：瞬間 spike 超過 average peak、需要短時間吸收</li>
<li>AZ / region failover：一個 AZ 掛、剩下兩個要承擔全部（多 33% 容量）</li>
<li>系統老化 / drift：軟硬體升級後 saturation 點可能位移</li>
</ul>
<p><strong>不同工作負載不同 headroom</strong>：</p>
<ul>
<li>stateless service：30%（autoscaler 反應快、headroom 可以薄）</li>
<li>DB：50%（不易擴容、要備援足夠空間）</li>
<li>broker / queue：60%（consumer 落後恢復時要瞬間吃下積壓）</li>
<li>consensus DB：80%+（完全不能 reactive 擴）</li>
</ul>
<p><strong>headroom 太低 → 出事</strong>：peak 期間進 cliff、用戶體驗變差。
<strong>headroom 太高 → 浪費錢</strong>：平日成本拉高、CFO 質疑。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech AI 預測</a> — 預測準了可以降 headroom 比例；預測不準必須拉高 headroom 補回安全邊界。</p>
<h2 id="growth-curve-形狀分類">Growth curve 形狀分類</h2>
<p>不同 growth curve 形狀對應不同 forecast 方法跟 review 節奏。</p>
<p><strong>Linear growth</strong>：用戶月增 X%。B2B SaaS 最常見。forecast 線性外推、每季 review、headroom 可以薄（成長可預測）。</p>
<p><strong>Step growth</strong>：每次行銷 / 活動跳一階、之間 plateau。需要 event tier 規劃、每個事件單獨 forecast、headroom 跟 event 強度連動。</p>
<p><strong>Exponential growth</strong>：早期初創、病毒擴散。forecast 容易低估、傳統線性外推會大幅低估；headroom 必須拉到 100%+、不能省。</p>
<p><strong>S-curve growth</strong>：成熟產品、會 saturate。Forecast 初期像 exponential、中期 plateau、晚期 mature。需要識別 inflection point、過了就調 forecast 方法。</p>
<p><strong>Cyclical</strong>：電商季節性。每年 Black Friday / Cyber Monday / Christmas / Chinese New Year 都重複、forecast 用 STL 季節性分解。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID</a> — step growth、外部衝擊讓 baseline 永久上移；<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO 50x surge</a> — exponential（早期）+ 之後 S-curve；<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday</a> — cyclical。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve 卡片</a>。</p>
<h2 id="autoscaling-sizing">Autoscaling sizing</h2>
<p>訂好 capacity 之後、要設計 autoscaler 把這個容量 <em>動態使用</em>。</p>
<p><strong>min / max / target metric 三個參數</strong>：</p>
<ul>
<li>min 太低 → cold start 風險（流量上來時還在 boot）</li>
<li>min 太高 → 平日浪費</li>
<li>max 太低 → 限流（peak 時 autoscaler 不能再擴）</li>
<li>max 太高 → 月底炸帳單（autoscaler 不受控、過 peak 不會主動降）</li>
<li>target 太高 → autoscale 啟動太晚、進 knee 才反應</li>
<li>target 太低 → autoscale 太敏感、頻繁 scale up / down 浪費</li>
</ul>
<p><strong>Predictive vs reactive</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>：根據歷史 pattern 或 ML 模型提前擴</li>
<li>reactive scaling：根據當下指標擴</li>
<li>兩者組合最穩：predictive 處理已知 pattern、reactive 處理 unexpected burst</li>
</ul>
<p><strong>Scheduled vs metric-based</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a>：時段觸發（年度活動、daily peak）</li>
<li>metric-based：根據 utilization / queue depth 觸發</li>
<li>三層組合（scheduled + predictive + reactive）最穩</li>
</ul>
<p><strong>不同層的 autoscaler 各自設計</strong>：</p>
<ul>
<li>EC2 Auto Scaling Group：infrastructure 層</li>
<li>Kubernetes HPA / VPA：pod 層</li>
<li>Karpenter：node 層</li>
<li>DynamoDB auto-scaling：DB capacity 層</li>
<li>CloudFront：CDN 層</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 30 分鐘擴 130 倍</a> — 6 台 → 800 台靠 ASG + AMI prebuild + ELB warmup；<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day predictive</a> — pre-scaling 30-77% 年增率提前算進容量。</p>
<h2 id="不可水平擴容服務的容量規劃">不可水平擴容服務的容量規劃</h2>
<p>部分服務不能用「加機器」解決容量問題。這類服務的容量規劃有獨立邏輯。</p>
<p><strong>典型不可水平擴</strong>：</p>
<ul>
<li>consensus DB（RAFT / Paxos）：節點數量是 consensus 一部分、不能臨時增減</li>
<li>single leader DB（PostgreSQL primary、MySQL master）：寫只有一個 leader</li>
<li>中央 coordinator：必須拆解才可擴</li>
</ul>
<p><strong>容量公式變成</strong>：單機極限 × headroom、沒有 elastic 救援。
<strong>設計重點</strong>：</p>
<ul>
<li>預先 provision 到能撐 peak、不依賴 reactive 擴</li>
<li>垂直擴容（更大 instance）為主、不是橫向</li>
<li>留更高 headroom（80%+）、出事沒有第二招</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase pre-provision</a> — RAFT 限制下完全 pre-provision、不 autoscale；<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner 節點即容量單位</a> — 雖然全球可擴、但每個 region 內節點數要預先規劃。</p>
<h2 id="跨地理--跨-region-容量規劃">跨地理 / 跨 region 容量規劃</h2>
<p>跨 region 服務不能用 <em>全球總量</em> 平攤、每個 region 獨立規劃。</p>
<p><strong>為什麼不能聚合</strong>：</p>
<ul>
<li>用戶在哪、流量就在哪、不會自動 spread</li>
<li>跨 region 切流量有延遲（DNS TTL、用戶習慣）、不能即時 rebalance</li>
<li>資料駐留合規可能強制各 region 獨立</li>
</ul>
<p><strong>規劃方法</strong>：</p>
<ul>
<li>每個 region 抽各自的 workload model</li>
<li>各自跑 saturation discovery</li>
<li>各自訂 headroom（區域峰值 + 區域 AZ failover）</li>
<li>跨 region failover plan：哪個 region 掛了、流量去哪、目標 region 要留多少 headroom 接</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">Standard Chartered 7 個受監管市場</a> — 跨市場獨立容量規劃；<a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 15 region</a> — 15 主 region + 5 衛星 region 各自規劃；<a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">Mercado Libre 18 國</a> — 每國獨立 cycle。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a></td>
          <td>可預期峰值的 forecast + pre-scaling</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a></td>
          <td>AI 預測式擴容、縮短反應時間</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>30x surge 後 baseline 永久上移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場獨立容量規劃</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>不可水平擴的 pre-provision</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> / <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a>（先選軸再算數量、不可水平擴容服務的判讀基底）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a>（容量翻成成本）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>跨模組：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> autoscaler 實作</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve</a></li>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/</guid><description>&lt;p>這個案例的核心責任是說明「cache layer 在持續成長服務」的角色 — 不是峰值問題、是延遲 SLA 與成本曲線同時拉緊的長期工程議題。Tinder 的配對引擎需要在每次滑動都查多個快取（用戶 profile、距離、偏好過濾、推薦池），單次互動的延遲就是 UX 本身。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tinder 在 ElastiCache for Valkey 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/elasticache/customers/">ElastiCache customers&lt;/a>）：&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>月活用戶&lt;/td>
 &lt;td>約 4700 萬 MAU (2025)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配對累計&lt;/td>
 &lt;td>超過 10 億次配對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>地理覆蓋&lt;/td>
 &lt;td>190 個國家&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務年數&lt;/td>
 &lt;td>自 2012 年起&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲特性&lt;/td>
 &lt;td>sub-millisecond latency&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ElastiCache for Redis 7.1 在 r7g.4xlarge 上可達單節點 100 萬 RPS、單 cluster 5 億 RPS（引自 &lt;a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog&lt;/a>）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Tinder 案例值得讀的是「快取在 long-running 服務的角色變化」。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>快取不是 DB 的補救、是主要服務面&lt;/strong>：配對引擎每次互動讀 cache 不讀 DB、cache miss 是 &lt;em>邊緣案例&lt;/em>。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache-as-source-of-truth 與 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary&lt;/a> 設計。&lt;/li>
&lt;li>&lt;strong>次毫秒延遲是業務 KPI、不只是技術指標&lt;/strong>：手指滑動之後 250ms 內必須給結果、否則「卡頓」。中間整個 chain（網路、cache、序列化）的 latency budget 必須緊。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 反推。&lt;/li>
&lt;li>&lt;strong>長期 sustained growth 的容量曲線是成本曲線&lt;/strong>：47M MAU 沒有明顯峰谷、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的長期成本工程。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：Tinder 的「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁。一個 schema 變更可能讓既有 cache 全部 invalid、引發 cache stampede。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback&lt;/a>。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>cache layer 容量規劃跟 DB 容量規劃要分開&lt;/strong>：cache 容量受 working set size 影響、DB 容量受 total dataset 影響、兩者擴容邏輯不一樣。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache sizing。&lt;/li>
&lt;li>&lt;strong>cache 命中率變化是業務變化的訊號&lt;/strong>：突然命中率掉、可能是新功能影響 access pattern、不一定是 cache 容量問題。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性&lt;/a> 的訊號治理。&lt;/li>
&lt;li>&lt;strong>Valkey vs Redis OSS vs MemoryDB 是不同 trade-off&lt;/strong>：Valkey（社群分支、AWS 主推）、Redis OSS（受授權變化影響）、MemoryDB（持久化）三者選擇影響長期 vendor lock-in。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP Memorystore for Redis / Valkey、Azure Cache for Redis、自建 Redis Cluster + Sentinel 都可以實作對等架構。差異是 vendor 的 patch cadence 與容量擴張流程。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「cache layer 在持續成長服務」的角色 — 不是峰值問題、是延遲 SLA 與成本曲線同時拉緊的長期工程議題。Tinder 的配對引擎需要在每次滑動都查多個快取（用戶 profile、距離、偏好過濾、推薦池），單次互動的延遲就是 UX 本身。</p>
<h2 id="觀察">觀察</h2>
<p>Tinder 在 ElastiCache for Valkey 的關鍵數字（引自 <a href="https://aws.amazon.com/elasticache/customers/">ElastiCache customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月活用戶</td>
          <td>約 4700 萬 MAU (2025)</td>
      </tr>
      <tr>
          <td>配對累計</td>
          <td>超過 10 億次配對</td>
      </tr>
      <tr>
          <td>地理覆蓋</td>
          <td>190 個國家</td>
      </tr>
      <tr>
          <td>服務年數</td>
          <td>自 2012 年起</td>
      </tr>
      <tr>
          <td>延遲特性</td>
          <td>sub-millisecond latency</td>
      </tr>
  </tbody>
</table>
<p>ElastiCache for Redis 7.1 在 r7g.4xlarge 上可達單節點 100 萬 RPS、單 cluster 5 億 RPS（引自 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog</a>）。</p>
<h2 id="判讀">判讀</h2>
<p>Tinder 案例值得讀的是「快取在 long-running 服務的角色變化」。</p>
<ol>
<li><strong>快取不是 DB 的補救、是主要服務面</strong>：配對引擎每次互動讀 cache 不讀 DB、cache miss 是 <em>邊緣案例</em>。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache-as-source-of-truth 與 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a> 設計。</li>
<li><strong>次毫秒延遲是業務 KPI、不只是技術指標</strong>：手指滑動之後 250ms 內必須給結果、否則「卡頓」。中間整個 chain（網路、cache、序列化）的 latency budget 必須緊。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency budget 反推。</li>
<li><strong>長期 sustained growth 的容量曲線是成本曲線</strong>：47M MAU 沒有明顯峰谷、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的長期成本工程。</li>
</ol>
<p>需要警惕：Tinder 的「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁。一個 schema 變更可能讓既有 cache 全部 invalid、引發 cache stampede。對應 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback</a>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>cache layer 容量規劃跟 DB 容量規劃要分開</strong>：cache 容量受 working set size 影響、DB 容量受 total dataset 影響、兩者擴容邏輯不一樣。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache sizing。</li>
<li><strong>cache 命中率變化是業務變化的訊號</strong>：突然命中率掉、可能是新功能影響 access pattern、不一定是 cache 容量問題。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性</a> 的訊號治理。</li>
<li><strong>Valkey vs Redis OSS vs MemoryDB 是不同 trade-off</strong>：Valkey（社群分支、AWS 主推）、Redis OSS（受授權變化影響）、MemoryDB（持久化）三者選擇影響長期 vendor lock-in。</li>
</ol>
<p>跨平台等效：GCP Memorystore for Redis / Valkey、Azure Cache for Redis、自建 Redis Cluster + Sentinel 都可以實作對等架構。差異是 vendor 的 patch cadence 與容量擴張流程。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 cache layer 容量 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
<li>想做 latency budget 反推 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為</a></li>
<li>想理解 cache stampede 風險 → <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback</a></li>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a>（KV 高吞吐）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/elasticache/customers/">Amazon ElastiCache Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">Achieve over 500 million requests per second per cluster with ElastiCache for Redis 7.1</a></li>
<li><a href="https://aws.amazon.com/blogs/database/optimize-redis-client-performance-for-amazon-elasticache/">Optimize Redis Client Performance for ElastiCache and MemoryDB</a></li>
</ul>
]]></content:encoded></item><item><title>3.6 Processing Semantics 與 Recovery Semantics</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/processing-recovery-semantics/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/processing-recovery-semantics/</guid><description>&lt;p>Processing semantics 與 recovery semantics 的核心責任是把訊息送達、業務副作用完成、故障後可恢復三件事分開判斷。進入 Kafka、RabbitMQ、SQS、NATS 或 Redis Streams 前，讀者需要先知道 broker 保證主要落在傳遞語意的一部分。&lt;/p>
&lt;h2 id="delivery--processing--recovery">Delivery / Processing / Recovery&lt;/h2>
&lt;p>三層語意的責任不同：&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>Delivery semantics&lt;/td>
 &lt;td>訊息是否被 broker 投遞、確認、重送或隔離&lt;/td>
 &lt;td>ack、nack、redelivery、DLQ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Processing semantics&lt;/td>
 &lt;td>consumer 副作用是否能承受重複、亂序與部分失敗&lt;/td>
 &lt;td>idempotency、side effect、ordering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery semantics&lt;/td>
 &lt;td>故障後是否能重播、補償與恢復一致&lt;/td>
 &lt;td>replay、checkpoint、reconciliation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics&lt;/a> 成立不代表 processing 成立。訊息被 ack 也不代表發票、email、search index 或 webhook 都已完成。&lt;/p>
&lt;p>Delivery 層的判讀重點是 broker 是否還能掌握訊息位置。Processing 層的判讀重點是 consumer 是否已經完成業務副作用。Recovery 層的判讀重點是事故後能否用 replay、checkpoint 與 reconciliation 回到一致狀態。這三層拆開後，隊列工具選型才會對到真正問題。&lt;/p>
&lt;h2 id="processing-semantics">Processing Semantics&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">Processing semantics&lt;/a> 的責任是讓 consumer 副作用在重複投遞與部分失敗下仍可控。常見副作用包含寫資料庫、呼叫外部 API、寄信、建立發票、更新 search index。&lt;/p>
&lt;p>每個副作用都要先回答：&lt;/p>
&lt;ol>
&lt;li>idempotency key 是什麼。&lt;/li>
&lt;li>副作用完成後如何記錄。&lt;/li>
&lt;li>重複執行時結果是否穩定。&lt;/li>
&lt;li>部分成功時如何補償。&lt;/li>
&lt;/ol>
&lt;p>缺少這些答案時，at-least-once delivery 會轉成多次業務結果。&lt;/p>
&lt;h2 id="recovery-semantics">Recovery Semantics&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">Recovery semantics&lt;/a> 的責任是讓系統在 consumer crash、DLQ 爆量、下游故障或資料修復後能恢復一致。它依賴 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window&lt;/a>、checkpoint、offset、去重紀錄與對帳查詢。&lt;/p>
&lt;p>恢復流程要先分範圍。按時間、tenant、partition、schema version 或 event type 分段，能降低 replay 造成的下游壓力與重複副作用。&lt;/p>
&lt;h2 id="checkpoint-與-side-effect">Checkpoint 與 Side Effect&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint&lt;/a> 的責任是標記處理進度，業務完成則要由副作用紀錄與對帳證據證明。若 checkpoint 早於副作用提交，consumer crash 後可能漏做副作用；若 checkpoint 太晚，重啟後會造成重複處理。&lt;/p>
&lt;p>穩定設計通常讓副作用具備 idempotency，再把 checkpoint 放在可恢復的位置。checkpoint 與 idempotency 是一組設計，需要一起審查。&lt;/p>
&lt;h2 id="poison-message-的處理層次">Poison Message 的處理層次&lt;/h2>
&lt;p>Poison message 屬於觸發 consumer 持續失敗、需要被隔離處理的訊息類型。處理流程從 &lt;em>偵測 / 隔離 / 診斷 / 修復&lt;/em> 四個層次設計、屬於 DLQ 之後的延伸責任。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：Queue Semantics Mismatch&lt;/a> — case 提供切換後 DLQ 激增的觀察方向、是 broker 遷移時 consumer 沒對齊 processing/recovery 語意的訊號、poison message 是其下游表徵之一。&lt;/p>
&lt;p>&lt;strong>四個處理層次&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>偵測&lt;/strong>：retry count 超過組織自定閾值後識別為 poison candidate。早期偵測訊號是 retry rate 升高但 success rate 沒同步上升、單一 consumer 反覆失敗&lt;/li>
&lt;li>&lt;strong>隔離&lt;/strong>：把 poison message 移出主通道、進 DLQ 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">quarantine queue&lt;/a>。隔離要即時、避免持續占用主通道吞吐&lt;/li>
&lt;li>&lt;strong>診斷&lt;/strong>：DLQ 內 poison message 要分群分析、找出共同 failure pattern（payload schema 不符、外部 API 永久失敗、邏輯 bug）&lt;/li>
&lt;li>&lt;strong>修復&lt;/strong>：依據 root cause 修 consumer / contract / 邏輯後、再&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">定向回放 DLQ&lt;/a> 內 poison message、避免 zombie cycle（同一 message 反覆進 DLQ）&lt;/li>
&lt;/ul>
&lt;p>判讀重點：DLQ size 持續增加但沒有對應修復 commit、表示處理流程斷在「隔離」這層、要回到「診斷 / 修復」。release gate 加「DLQ 排空速率 &amp;gt;= 流入速率」的條件、讓 DLQ 維持診斷入口的角色。未授權 replay 跟 window 越界攻擊面見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章 Replay 攻擊&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Processing semantics 與 recovery semantics 的核心責任是把訊息送達、業務副作用完成、故障後可恢復三件事分開判斷。進入 Kafka、RabbitMQ、SQS、NATS 或 Redis Streams 前，讀者需要先知道 broker 保證主要落在傳遞語意的一部分。</p>
<h2 id="delivery--processing--recovery">Delivery / Processing / Recovery</h2>
<p>三層語意的責任不同：</p>
<table>
  <thead>
      <tr>
          <th>語意層</th>
          <th>負責問題</th>
          <th>主要訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Delivery semantics</td>
          <td>訊息是否被 broker 投遞、確認、重送或隔離</td>
          <td>ack、nack、redelivery、DLQ</td>
      </tr>
      <tr>
          <td>Processing semantics</td>
          <td>consumer 副作用是否能承受重複、亂序與部分失敗</td>
          <td>idempotency、side effect、ordering</td>
      </tr>
      <tr>
          <td>Recovery semantics</td>
          <td>故障後是否能重播、補償與恢復一致</td>
          <td>replay、checkpoint、reconciliation</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a> 成立不代表 processing 成立。訊息被 ack 也不代表發票、email、search index 或 webhook 都已完成。</p>
<p>Delivery 層的判讀重點是 broker 是否還能掌握訊息位置。Processing 層的判讀重點是 consumer 是否已經完成業務副作用。Recovery 層的判讀重點是事故後能否用 replay、checkpoint 與 reconciliation 回到一致狀態。這三層拆開後，隊列工具選型才會對到真正問題。</p>
<h2 id="processing-semantics">Processing Semantics</h2>
<p><a href="/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">Processing semantics</a> 的責任是讓 consumer 副作用在重複投遞與部分失敗下仍可控。常見副作用包含寫資料庫、呼叫外部 API、寄信、建立發票、更新 search index。</p>
<p>每個副作用都要先回答：</p>
<ol>
<li>idempotency key 是什麼。</li>
<li>副作用完成後如何記錄。</li>
<li>重複執行時結果是否穩定。</li>
<li>部分成功時如何補償。</li>
</ol>
<p>缺少這些答案時，at-least-once delivery 會轉成多次業務結果。</p>
<h2 id="recovery-semantics">Recovery Semantics</h2>
<p><a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">Recovery semantics</a> 的責任是讓系統在 consumer crash、DLQ 爆量、下游故障或資料修復後能恢復一致。它依賴 <a href="/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window</a>、checkpoint、offset、去重紀錄與對帳查詢。</p>
<p>恢復流程要先分範圍。按時間、tenant、partition、schema version 或 event type 分段，能降低 replay 造成的下游壓力與重複副作用。</p>
<h2 id="checkpoint-與-side-effect">Checkpoint 與 Side Effect</h2>
<p><a href="/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint</a> 的責任是標記處理進度，業務完成則要由副作用紀錄與對帳證據證明。若 checkpoint 早於副作用提交，consumer crash 後可能漏做副作用；若 checkpoint 太晚，重啟後會造成重複處理。</p>
<p>穩定設計通常讓副作用具備 idempotency，再把 checkpoint 放在可恢復的位置。checkpoint 與 idempotency 是一組設計，需要一起審查。</p>
<h2 id="poison-message-的處理層次">Poison Message 的處理層次</h2>
<p>Poison message 屬於觸發 consumer 持續失敗、需要被隔離處理的訊息類型。處理流程從 <em>偵測 / 隔離 / 診斷 / 修復</em> 四個層次設計、屬於 DLQ 之後的延伸責任。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：Queue Semantics Mismatch</a> — case 提供切換後 DLQ 激增的觀察方向、是 broker 遷移時 consumer 沒對齊 processing/recovery 語意的訊號、poison message 是其下游表徵之一。</p>
<p><strong>四個處理層次</strong>：</p>
<ul>
<li><strong>偵測</strong>：retry count 超過組織自定閾值後識別為 poison candidate。早期偵測訊號是 retry rate 升高但 success rate 沒同步上升、單一 consumer 反覆失敗</li>
<li><strong>隔離</strong>：把 poison message 移出主通道、進 DLQ 或 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">quarantine queue</a>。隔離要即時、避免持續占用主通道吞吐</li>
<li><strong>診斷</strong>：DLQ 內 poison message 要分群分析、找出共同 failure pattern（payload schema 不符、外部 API 永久失敗、邏輯 bug）</li>
<li><strong>修復</strong>：依據 root cause 修 consumer / contract / 邏輯後、再<a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">定向回放 DLQ</a> 內 poison message、避免 zombie cycle（同一 message 反覆進 DLQ）</li>
</ul>
<p>判讀重點：DLQ size 持續增加但沒有對應修復 commit、表示處理流程斷在「隔離」這層、要回到「診斷 / 修復」。release gate 加「DLQ 排空速率 &gt;= 流入速率」的條件、讓 DLQ 維持診斷入口的角色。未授權 replay 跟 window 越界攻擊面見 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章 Replay 攻擊</a>。</p>
<h2 id="replay-跟-idempotency-的共設計">Replay 跟 Idempotency 的共設計</h2>
<p>Replay safety 跟 idempotency 屬於同一個設計階段、需共設計並落地後才能上線。replay window 設多大、idempotency key 怎麼定、checkpoint 何時提交、三者互相影響、任一改動都會破壞其他。</p>
<p><strong>共設計的判讀順序</strong>：</p>
<ol>
<li><strong>先定 idempotency key</strong>：什麼欄位組合能唯一標記副作用（event_id、entity_id + version、business operation id）</li>
<li><strong>再定 idempotency 儲存策略</strong>：去重紀錄存多久（決定 replay window 上限）、儲存在 cache / DB / 應用層 memory</li>
<li><strong>依儲存策略反推 replay window</strong>：去重紀錄保留 7 天、replay window 上限就是 7 天、超過會出現重複副作用</li>
<li><strong>再依 replay window 反推 checkpoint 策略</strong>：checkpoint 落地時機要保證 crash 後 replay window 內可恢復</li>
</ol>
<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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub</a> — broker 遷移要驗證業務語意跟新 broker 兼容、replay 模型在 Kafka（offset）跟 Pub/Sub（snapshot + seek）不同、idempotency 策略要重新校準。</p>
<p>判讀重點：replay window 由 idempotency 儲存策略反推、不是 broker 設定值。先看 idempotency key 跟去重儲存、再決定 replay window 安全範圍。順序顛倒會踩到「replay 跨越去重紀錄到期」的事故、表現是 replay 後出現本來該被去重的重複副作用。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>Queue 選型前要先回答：</p>
<ol>
<li>需要保證的是投遞、處理還是恢復。</li>
<li>哪些副作用必須 idempotent。</li>
<li>哪些事件需要順序，順序邊界是全域、tenant、entity 還是 partition。</li>
<li>Replay 時下游能承受多少吞吐。</li>
<li>DLQ 是診斷入口還是已經變成長期倉庫。</li>
</ol>
<p>這些答案會決定後續比較 Kafka、RabbitMQ、SQS、NATS 或 Redis Streams 時該看哪些能力。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體 queue/broker 文章要承接本篇的 processing 與 recovery semantics。Kafka、RabbitMQ、SQS、NATS 或 Redis Streams 的比較，應先問服務需要什麼投遞、處理與恢復責任，再比較 topic、queue、partition、consumer group、DLQ 或 retention。</p>
<p>若主問題是高吞吐事件流，後續文章要比較 partition、retention、consumer lag 與 replay 能力。若主問題是工作派發，後續文章要比較 ack/nack、routing、DLQ 與 retry。若主問題是受管服務操作成本，後續文章要比較可觀測性、IAM、區域能力與 failure mode。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 03 內部：consumer 端去重跟 ack timing 詳見 <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-design</a>；event payload 跟 replay 邊界寫入事件契約見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7</a>；規模差異判讀跟 job queue 拓樸分工見 <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。">3.8</a></li>
<li>與 04 的交接：lag、retry、DLQ、duplicate 訊號進 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>與 06 的交接：idempotency 跟 replay 驗證進 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 event payload 跟 replay 邊界寫進事件契約、接著讀 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 Event Contract 與 Replay Boundary</a>。要建立 broker 投遞模型，接著讀 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker 基礎與投遞模型</a>。要把三層語意放進完整服務路徑，接著讀 <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。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。</p>
]]></content:encoded></item><item><title>5.6 Platform Lifecycle Contract</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</guid><description>&lt;p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。&lt;/p>
&lt;h2 id="lifecycle-contract">Lifecycle Contract&lt;/h2>
&lt;p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。&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>runtime&lt;/td>
 &lt;td>固定 image、entrypoint、config 與 resource&lt;/td>
 &lt;td>提供可預期執行環境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>startup&lt;/td>
 &lt;td>初始化依賴與內部狀態&lt;/td>
 &lt;td>避免過早重啟慢啟動服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>readiness&lt;/td>
 &lt;td>宣告可安全接流量&lt;/td>
 &lt;td>只把流量導向 ready instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>liveness&lt;/td>
 &lt;td>宣告基本運作能力&lt;/td>
 &lt;td>在不可恢復時重建 instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>shutdown&lt;/td>
 &lt;td>停接新工作並釋放資源&lt;/td>
 &lt;td>給予 termination window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>drain&lt;/td>
 &lt;td>完成在途請求或連線退場&lt;/td>
 &lt;td>從路由集合摘除 instance&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。&lt;/p>
&lt;p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。&lt;/p>
&lt;h2 id="startup-與-readiness">Startup 與 Readiness&lt;/h2>
&lt;p>startup 的責任是確認服務初始化完成。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。&lt;/p>
&lt;p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。&lt;/p>
&lt;h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮&lt;/h3>
&lt;p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>runtime 初始化&lt;/strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。&lt;/li>
&lt;li>&lt;strong>依賴建立&lt;/strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。&lt;/li>
&lt;li>&lt;strong>資料預載&lt;/strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。&lt;/li>
&lt;li>&lt;strong>就緒驗證&lt;/strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。&lt;/li>
&lt;/ol>
&lt;p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。&lt;/p>
&lt;h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨&lt;/h3>
&lt;p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。&lt;/p>
&lt;p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>必要依賴&lt;/strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。&lt;/li>
&lt;li>&lt;strong>可降級依賴&lt;/strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 處理。&lt;/li>
&lt;li>&lt;strong>觀測依賴&lt;/strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。&lt;/li>
&lt;/ul>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。&lt;/p>
&lt;h2 id="liveness-與-restart">Liveness 與 Restart&lt;/h2>
&lt;p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。&lt;/p>
&lt;p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。&lt;/p>
&lt;h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式&lt;/h3>
&lt;p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。&lt;/p></description><content:encoded><![CDATA[<p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。</p>
<h2 id="lifecycle-contract">Lifecycle Contract</h2>
<p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>服務責任</th>
          <th>平台責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>runtime</td>
          <td>固定 image、entrypoint、config 與 resource</td>
          <td>提供可預期執行環境</td>
      </tr>
      <tr>
          <td>startup</td>
          <td>初始化依賴與內部狀態</td>
          <td>避免過早重啟慢啟動服務</td>
      </tr>
      <tr>
          <td>readiness</td>
          <td>宣告可安全接流量</td>
          <td>只把流量導向 ready instance</td>
      </tr>
      <tr>
          <td>liveness</td>
          <td>宣告基本運作能力</td>
          <td>在不可恢復時重建 instance</td>
      </tr>
      <tr>
          <td>shutdown</td>
          <td>停接新工作並釋放資源</td>
          <td>給予 termination window</td>
      </tr>
      <tr>
          <td>drain</td>
          <td>完成在途請求或連線退場</td>
          <td>從路由集合摘除 instance</td>
      </tr>
  </tbody>
</table>
<p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。</p>
<p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。</p>
<h2 id="startup-與-readiness">Startup 與 Readiness</h2>
<p>startup 的責任是確認服務初始化完成。<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。</p>
<p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。</p>
<h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮</h3>
<p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：</p>
<ol>
<li><strong>runtime 初始化</strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。</li>
<li><strong>依賴建立</strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。</li>
<li><strong>資料預載</strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。</li>
<li><strong>就緒驗證</strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。</li>
</ol>
<p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。</p>
<h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨</h3>
<p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。</p>
<p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：</p>
<ul>
<li><strong>必要依賴</strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。</li>
<li><strong>可降級依賴</strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 <a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker</a> 或 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 處理。</li>
<li><strong>觀測依賴</strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。</li>
</ul>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。</p>
<h2 id="liveness-與-restart">Liveness 與 Restart</h2>
<p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。</p>
<p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。</p>
<h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式</h3>
<p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。</p>
<p>適合 liveness 偵測的狀態：</p>
<ul>
<li><strong>deadlock</strong>：所有 worker thread 被卡住，無法處理新請求也無法回傳錯誤。liveness endpoint 設在獨立 goroutine / thread 上，如果 worker pool 卡住但 liveness goroutine 能回應，問題在業務邏輯而非 deadlock。</li>
<li><strong>memory leak 導致的 OOM 前兆</strong>：記憶體使用率持續上升不回落，GC 已無法回收。此時主動回報 unhealthy 讓平台在 OOM kill 前重建，比被動等 OOM 更可控——OOM kill 不走 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>，在途請求直接中斷。</li>
<li><strong>essential background task 永久停止</strong>：必要的定期任務（如 license renewal、session cleanup）超過預期間隔仍未執行。這類失敗靜默發生，只有 liveness 主動偵測能發現。</li>
</ul>
<p>不適合 liveness 偵測的狀態：下游資料庫短暫不可用、外部 API timeout、cache miss 率升高。這些由 readiness 或 circuit breaker 處理——用 liveness 重建不會修好下游，只會用重啟放大問題。</p>
<h3 id="restart-的代價量化">Restart 的代價量化</h3>
<p>每次 liveness 觸發的重啟會產生四類代價：</p>
<ol>
<li><strong>在途請求中斷</strong>：被重啟的實例正在處理的請求直接失敗。</li>
<li><strong>連線重建成本</strong>：資料庫連線池、cache 連線、queue consumer 重新建立。</li>
<li><strong>啟動期間的容量缺口</strong>：重啟到 readiness 通過之間，整體服務容量降低。</li>
<li><strong>thundering herd 風險</strong>：多實例同時被 liveness 判定失敗並重啟時，同時重建連線、同時搶資源、下游壓力瞬間放大。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a>：揭露「基礎平台元件升級若缺乏分批治理、會形成全域風險放大器」。以下基於通用工程知識展開：Istio 等 service mesh 升級期間的 sidecar 重啟可觸發大量服務的 liveness 暫時失敗，若 liveness 太敏感會放大成全域 restart storm。升級期的 liveness 閾值應比穩態更寬鬆，或在升級批次中暫時加大 liveness failure threshold。</p>
<h2 id="shutdown-與-drain">Shutdown 與 Drain</h2>
<p>shutdown 的責任是讓服務停止接新工作並完成資源釋放。<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓平台在移除實例前，讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、長連線或背景工作有時間收束。</p>
<p>短 request API、長連線服務與 background worker 的 drain 條件不同。短 API 主要看在途請求歸零；長連線看 reconnect 節奏；worker 看已領取工作能否完成或重新排隊。tunnel 入口的 startup / readiness / drain 對齊見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>。</p>
<h3 id="三種-workload-的-drain-差異">三種 Workload 的 Drain 差異</h3>
<p>不同 workload 類型的 drain 完成條件與時間尺度完全不同，用同一套 drain 設定覆蓋所有 workload 會在至少一類服務上出事。</p>
<p><strong>短 request API</strong>（HTTP REST、gRPC unary）：drain 窗口通常在 5-30 秒。核心條件是在途請求數歸零。風險點是 load balancer 的 deregistration delay——LB 可能在服務已標記 not-ready 後仍送幾秒流量（取決於 health check interval 與 deregistration delay），所以服務端 drain 窗口要覆蓋這段延遲。endpoint 摘除的傳播窗口與 preStop 等待策略見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 摘除節奏與 Drain 的配合</a>。</p>
<p><strong>長連線服務</strong>（WebSocket、gRPC streaming、SSE）：drain 窗口通常在 30 秒到數分鐘。核心條件是現有連線收斂且 reconnect 波形穩定。風險點是客戶端 reconnect 策略——服務端 drain 完成不代表客戶端已連上新實例。若客戶端沒有 backoff 或 reconnect 目標選擇邏輯，會形成 reconnect storm。drain 設計要跟客戶端 reconnect 策略一起規劃。</p>
<p><strong>Background worker</strong>（queue consumer、定時任務、batch job）：drain 窗口取決於單一工作的最長執行時間。核心條件是已領取的工作完成處理或安全重新排隊。風險點是不可中斷工作——某些 job 做到一半無法重試（例如外部 API 呼叫已發出但回應尚未確認），drain 時序要覆蓋這類 job 的最長完成時間，否則 job 被中斷後產生不一致狀態。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：平台切流未先 Draining</a>：揭露「切流失敗常在 connection lifecycle 管理」「drain / idle timeout / health check / client retry 沒有同一節奏」。反例中的事故擴大機制正是不同 workload 類型的 drain 條件被忽略——短 API 的 drain 完成了，長連線的 reconnect 仍在震盪，worker 的 job 被中斷重試造成重複處理。</p>
<h3 id="shutdown-信號的傳遞路徑">Shutdown 信號的傳遞路徑</h3>
<p>platform 到 application 的 shutdown 信號傳遞有多個可能斷點。信號從平台送到容器 PID 1、PID 1 轉發到應用進程——PID 1 的信號處理語意與常見陷阱見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 PID 1 與信號處理</a>。本段聚焦 lifecycle 層的時序問題：</p>
<ul>
<li><strong>preStop hook 與 SIGTERM 時序</strong>：Kubernetes 先執行 preStop hook、再送 SIGTERM。preStop hook 可用來等 LB 摘流量（sleep 幾秒讓 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">endpoint 從可用集合移除</a>），讓 SIGTERM 到達時在途流量已經減少。</li>
<li><strong>terminationGracePeriodSeconds</strong>：平台等待的最長時間。超過後 SIGKILL 強制結束，不走 graceful shutdown。這個值要覆蓋 preStop + drain + 資源釋放的總時間。</li>
</ul>
<p>shutdown 信號傳遞的驗證方式是在 staging 環境觸發 pod delete，觀察應用 log 中是否出現 shutdown handler 的紀錄。沒看到 shutdown log 代表信號沒傳到、要先修傳遞路徑再談 drain 設計。</p>
<h2 id="不同-workload-的-lifecycle-特性對照">不同 Workload 的 Lifecycle 特性對照</h2>
<p>生命週期合約的參數設定要依 workload 類型調整。以下是三類常見 workload 的特性差異。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>短 request API</th>
          <th>長連線服務</th>
          <th>Background worker</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>startup 關注點</td>
          <td>依賴連線池建立</td>
          <td>依賴連線池 + 監聽埠就緒</td>
          <td>queue consumer 註冊完成</td>
      </tr>
      <tr>
          <td>readiness 條件</td>
          <td>必要依賴可達 + 連線池滿</td>
          <td>必要依賴可達 + 可接受新連線</td>
          <td>consumer 已註冊 + 可拉取新工作</td>
      </tr>
      <tr>
          <td>liveness 偵測</td>
          <td>deadlock、OOM 前兆</td>
          <td>連線管理 thread 存活</td>
          <td>worker loop 存活、queue 輪詢正常</td>
      </tr>
      <tr>
          <td>drain 完成條件</td>
          <td>在途請求數歸零</td>
          <td>現有連線收斂、reconnect 穩</td>
          <td>已領取工作完成或重新排隊</td>
      </tr>
      <tr>
          <td>drain 窗口</td>
          <td>5-30 秒</td>
          <td>30 秒 - 數分鐘</td>
          <td>取決於最長 job 執行時間</td>
      </tr>
      <tr>
          <td>shutdown 風險</td>
          <td>LB 延遲仍送流量</td>
          <td>reconnect storm</td>
          <td>不可中斷 job 被強制結束</td>
      </tr>
      <tr>
          <td>rollout 節奏建議</td>
          <td>可激進（秒級觀察窗）</td>
          <td>保守（分鐘級、等 reconnect）</td>
          <td>依 job 粒度（完成當前批次再切）</td>
      </tr>
  </tbody>
</table>
<p>這張表是選型前判準的操作化：先確認服務屬於哪類 workload，再套用對應的 lifecycle 參數基線。混合 workload（例如同時提供 HTTP API 和 WebSocket）要取各層的嚴格值——drain 窗口取最長的、readiness 取最嚴格的。</p>
<h2 id="平台如何表達-lifecycle-差異">平台如何表達 Lifecycle 差異</h2>
<p>不同部署平台表達生命週期合約的能力不同。選型時要問的是「這個平台能不能分別設定 startup、readiness、liveness 與 drain」。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>startup gate</th>
          <th>readiness 與 liveness 分離</th>
          <th>drain 能力</th>
          <th>termination 窗口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kubernetes</td>
          <td>startupProbe</td>
          <td>readinessProbe / livenessProbe 獨立</td>
          <td>preStop hook + endpoint 摘除</td>
          <td>terminationGracePeriodSeconds</td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>無原生 startup probe</td>
          <td>靠 sd_notify(READY=1)</td>
          <td>ExecStop + KillSignal</td>
          <td>TimeoutStopSec</td>
      </tr>
      <tr>
          <td>Docker</td>
          <td>HEALTHCHECK（不分離）</td>
          <td>單一 HEALTHCHECK</td>
          <td>stop_grace_period</td>
          <td>stop_grace_period</td>
      </tr>
      <tr>
          <td>ECS</td>
          <td>startupHealthCheck</td>
          <td>health check（不分離）</td>
          <td>deregistration delay</td>
          <td>stopTimeout</td>
      </tr>
  </tbody>
</table>
<p>Kubernetes 在 lifecycle 表達力上最完整，但參數最多也最容易配錯。systemd 靠 sd_notify 協議明確宣告 readiness，在單機部署場景下反而比 K8s 的 probe 直接。Docker 和 ECS 不分離 readiness 與 liveness，需要在應用層自行實作降級邏輯。</p>
<p>選平台不只看功能清單，要看它表達 lifecycle 差異的粒度是否覆蓋服務需求。若服務需要分離 startup 和 readiness 但平台只有一個 health check，這個差距要在應用層補——代價是複雜度從平台設定轉移到程式碼。</p>
<h2 id="遷移期的-lifecycle-重新驗證">遷移期的 Lifecycle 重新驗證</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb Kubernetes 叢集擴縮演進</a>：揭露「擴縮策略版本化與可回放」「不同 workload 區分擴縮政策」。以下基於通用工程知識展開：叢集演進過程中，lifecycle 參數的假設會改變——workload 從穩態變成高波動、從單一類型變成混合類型、從小規模變成大規模。lifecycle contract 的參數不是設一次就好，要隨叢集演進重新驗證。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照：規模差異下的平台遷移</a>：揭露「小型組織最容易漏掉回退腳本化」「中型組織依賴錯位、服務切過去但資料面 / 認證面 / 觀測面沒同步」。lifecycle contract 在遷移後的完整性驗證不只看 probe 設定——secret 注入時序、資料庫連線池的 endpoint 是否切到新叢集、observability pipeline 的 readiness 是否對齊，都是 lifecycle 合約的一部分。</p>
<p>遷移後的 lifecycle 驗證清單：</p>
<ol>
<li><strong>startup 時序重測</strong>：新平台的 image pull 時間、secret mount 時間、DNS 解析路徑可能不同，原本的 startup timeout 可能不夠。</li>
<li><strong>readiness 依賴路徑檢查</strong>：readiness 檢查的依賴是否仍可達（新叢集到舊資料庫的 latency 是否增加、跨叢集 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">service discovery</a> 是否對齊、DNS TTL 與快取行為是否改變）。</li>
<li><strong>drain 行為驗證</strong>：在新平台觸發 pod delete、觀察 drain 完成時間與在途請求處理是否符合預期。</li>
<li><strong>信號傳遞驗證</strong>：在新平台觸發 shutdown、確認 SIGTERM 到達應用進程並觸發 graceful shutdown handler。</li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>部署平台選型前要先回答：</p>
<ol>
<li>服務啟動需要多久，哪些依賴是 readiness 條件。</li>
<li>服務失敗時應由自己恢復，還是由平台重建。</li>
<li>服務停止時有哪些 in-flight request、connection 或 job。</li>
<li>平台是否能表達 startup、readiness、liveness 與 drain 的差異。</li>
</ol>
<p>這些問題決定後續要比較 Kubernetes probe、systemd restart policy、load balancer health check 或 service mesh drain 能力。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間新版本反覆重啟</td>
          <td>startup timeout 小於實際啟動時間</td>
          <td>拆分啟動四段分析瓶頸、調整 startup gate</td>
      </tr>
      <tr>
          <td>新版本 readiness 通過但首批請求錯誤率高</td>
          <td>readiness 條件太鬆、依賴未就緒就接流量</td>
          <td>加入必要依賴檢查、分離可降級依賴</td>
      </tr>
      <tr>
          <td>下游故障時大量實例被 liveness 重啟</td>
          <td>liveness 檢查了不該檢查的下游依賴</td>
          <td>把下游可達性移到 readiness、liveness 只看自身</td>
      </tr>
      <tr>
          <td>shutdown 後仍有請求中斷</td>
          <td>SIGTERM 未正確傳達或 drain 窗口不足</td>
          <td>驗證信號傳遞路徑、調整 terminationGracePeriod</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 設計未考慮客戶端 reconnect 策略</td>
          <td>拉長 drain、分批切流、搭配 reconnect backoff</td>
      </tr>
      <tr>
          <td>worker 切版後出現重複處理</td>
          <td>job 被中斷後重試、但前次已產生副作用</td>
          <td>drain 窗口覆蓋最長 job、或 job 支援冪等</td>
      </tr>
      <tr>
          <td>遷移新平台後啟動時間變長</td>
          <td>新平台 image pull / secret mount 路徑不同</td>
          <td>重測啟動四段、調整新平台的 startup timeout</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把所有 probe 設成同一個 <code>/health</code> endpoint，會讓 startup、readiness 與 liveness 的語意混在一起。三種 probe 回答不同問題：startup 問「初始化完了嗎」、readiness 問「可以接流量嗎」、liveness 問「還活著嗎」。同一個 endpoint 無法同時回答三個問題，因為初始化完成不代表依賴就緒，依賴暫時不可達不代表服務本身壞了。</p>
<p>把 drain 窗口設成固定值不分 workload 類型，會在某一類服務上出事。5 秒對短 API 足夠、對長連線不夠、對 batch job 遠遠不夠。drain 窗口要依服務實際 workload 設定，不是用平台預設值。</p>
<p>把 liveness 失敗當成「服務壞了」而不問代價，會忽略重啟本身的連鎖效應。每次重啟都有在途請求中斷、連線重建、容量缺口的代價——特別是多實例同時被判定 liveness 失敗時，代價會被放大。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>lifecycle contract 的完整性可用多個案例交叉驗證。<a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a> 揭露遷移後 readiness 依賴路徑改變的風險。<a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 揭露不同 workload 的 drain 條件被忽略造成的事故擴大。<a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a> 揭露基礎平台元件升級缺乏分批治理會形成全域風險放大器。<a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a> 揭露不同規模下 lifecycle 驗證的缺口模式。</p>
<p>這些案例共同支撐的判讀是「lifecycle contract 的每個狀態都有不同的失敗模式，混在一起處理會在事故時無法定位」。流量切換或連線生命週期問題路由到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。runtime 產物穩定性問題路由到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container 與 runtime</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>lifecycle contract 是部署模組的概念基底，後續章節都會引用本篇的狀態分類。</p>
<ol>
<li>與 5.1 的交接：runtime 與 entrypoint 定義 startup 行為回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">container 與 runtime</a>。</li>
<li>與 5.2 的交接：probe 設定與 rollout 節奏回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：drain 與流量退場回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.10 的交接：tunnel 入口的 readiness 與 drain 對齊回到 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">Outbound Tunnel 入口</a>。</li>
<li>與 4.20 的交接：lifecycle 事件的證據收集回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：lifecycle 狀態作為 release gate 判定條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看 Kubernetes 如何承接這組生命週期，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看流量退場如何和 LB 對齊，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看不同平台的 lifecycle 表達力比較，接著讀 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a>。</p>
]]></content:encoded></item><item><title>2.C6 Netflix：EVCache 全域快取層</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/</guid><description>&lt;p>這個案例的核心責任是說明快取在全球服務下會變成平台能力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Netflix 用 EVCache 支撐大規模低延遲讀取，把快取從單服務實作提升為共用基礎設施。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當讀取延遲目標很嚴格且區域分布廣，快取需要跨區一致性與故障容忍設計。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>平台化快取客戶端與治理規則。&lt;/li>
&lt;li>把失效策略與區域容錯納入同一模型。&lt;/li>
&lt;li>以可觀測指標評估命中率與恢復能力。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://netflixtechblog.com/caching-for-a-global-netflix-7bcc457012f1">EVCache&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取在全球服務下會變成平台能力。</p>
<h2 id="觀察">觀察</h2>
<p>Netflix 用 EVCache 支撐大規模低延遲讀取，把快取從單服務實作提升為共用基礎設施。</p>
<h2 id="判讀">判讀</h2>
<p>當讀取延遲目標很嚴格且區域分布廣，快取需要跨區一致性與故障容忍設計。</p>
<h2 id="策略">策略</h2>
<ol>
<li>平台化快取客戶端與治理規則。</li>
<li>把失效策略與區域容錯納入同一模型。</li>
<li>以可觀測指標評估命中率與恢復能力。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a> 與 <a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://netflixtechblog.com/caching-for-a-global-netflix-7bcc457012f1">EVCache</a></li>
</ul>
]]></content:encoded></item><item><title>3.C6 Uber：Kafka 事件平台演進</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/</guid><description>&lt;p>Uber 的 Kafka 演進案例揭露了 MQ 從「幾個團隊自管的 broker」到「全公司共享的事件平台」的治理轉折點。轉折的核心判斷是：規模化之後，broker 容量擴展的成本小於 workload 治理缺失的成本。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Uber 的事件流涵蓋行程追蹤、司機定位、計費事件、推播通知、即時定價、ETA 計算跟 analytics。早期各團隊各自架設 Kafka 叢集，隨著 Kafka 在 Uber 內部的採用率上升，叢集數量跟 topic 數量快速增長，但沒有統一的治理。&lt;/p>
&lt;p>Uber 的 Kafka 規模峰值達到每秒數百萬筆訊息、數十個叢集、數千個 topic。在這個規模下，管理壓力從「單一叢集的 broker 夠不夠」轉到「誰在用、用多少、怎麼收費、故障時誰負責」。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="團隊自管的碎片化">團隊自管的碎片化&lt;/h3>
&lt;p>各團隊各自架設 Kafka 時，每個叢集的版本、配置、監控、備份策略都不同。運維知識散落在各團隊，沒有共享的 runbook 或值班流程。某個團隊的 Kafka 出問題時，其他團隊幫不上忙；知識在人員流動時遺失。&lt;/p>
&lt;p>碎片化的另一個後果是資源浪費。每個團隊各自預留的容量加總起來遠大於集中管理所需。低流量團隊的叢集常年使用率低於 10%，但因為自管模式下沒有共享容量的機制，資源無法調配。&lt;/p>
&lt;h3 id="topic-爆炸與無主-topic">Topic 爆炸與無主 topic&lt;/h3>
&lt;p>沒有 topic 建立的治理流程時，任何人都可以建 topic。Topic 的命名不一致、retention 設定不一致、owner 不明。離職的工程師建立的 topic 仍在接收資料、佔用 broker 資源，但沒人知道這些 topic 服務什麼業務。&lt;/p>
&lt;p>LinkedIn 後來也遇到同樣的問題並開發了 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">TopicGC&lt;/a> 做 topic 生命週期管理。Uber 的解法路線類似 — 把 topic 建立變成需要 owner、retention policy 跟業務標籤的審核流程。&lt;/p>
&lt;h3 id="故障排查的責任不清">故障排查的責任不清&lt;/h3>
&lt;p>叢集內的故障（broker OOM、partition leader 不均衡、consumer lag spike）需要 Kafka 專業知識排查。團隊自管模式下，每個團隊都需要一定程度的 Kafka 運維能力，但多數團隊的核心能力是業務邏輯而非 MQ 運維。&lt;/p>
&lt;p>故障排查的慣性是「先問 Kafka 團隊有沒有人可以幫忙」— 但沒有正式的 Kafka 團隊，所以問的是「上次修過 Kafka 的那個人」。&lt;/p>
&lt;h2 id="解法平台化">解法：平台化&lt;/h2>
&lt;p>Uber 的解法是把 Kafka 從分散自管收斂到集中平台 — 一個專責的 Kafka platform team 統一管理所有叢集、提供標準化的使用介面。&lt;/p>
&lt;h3 id="多租戶治理">多租戶治理&lt;/h3>
&lt;p>平台化的核心是多租戶模型 — 每個業務團隊是一個 tenant，tenant 有 quota（ingestion rate、partition 數量上限、retention 上限）跟 cost attribution。&lt;/p>
&lt;p>Quota 的目的是防止單一 tenant 的爆量拖累整個平台。Cost attribution 的目的是讓 tenant 看到自己的用量跟成本，驅動合理使用。&lt;/p>
&lt;h3 id="標準化-topic-管理">標準化 topic 管理&lt;/h3>
&lt;p>Topic 的建立走 self-service portal — 團隊填寫 owner、業務用途、預估流量、retention 需求，portal 自動配置 topic 並建立監控。沒有 owner 的 topic 不允許建立；owner 離職時 topic 需要交接或標記為候選淘汰。&lt;/p>
&lt;h3 id="統一監控與值班">統一監控與值班&lt;/h3>
&lt;p>Platform team 統一監控所有叢集的 broker 健康（replication lag、under-replicated partitions、disk usage、CPU），提供共用的 dashboard 跟 alert。值班由 platform team 負責 broker 層面的問題，業務層面的問題（consumer 設計錯誤、message 格式不對）由各 tenant team 自行處理。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>自主性&lt;/td>
 &lt;td>高（團隊想怎麼配就怎麼配）&lt;/td>
 &lt;td>低到中（受 quota 跟 policy 約束）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>運維負擔分配&lt;/td>
 &lt;td>分散（每個團隊各自負擔）&lt;/td>
 &lt;td>集中（platform team 吸收 broker 層）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資源利用率&lt;/td>
 &lt;td>低（各自預留、無法共用）&lt;/td>
 &lt;td>高（共享容量、動態分配）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>治理一致性&lt;/td>
 &lt;td>低（版本、配置、命名各自為政）&lt;/td>
 &lt;td>高（統一版本、統一配置標準）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障影響面&lt;/td>
 &lt;td>小（自管叢集只影響自己的團隊）&lt;/td>
 &lt;td>大（共享平台故障影響所有 tenant）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>專業知識需求&lt;/td>
 &lt;td>每個團隊都要一些 Kafka 運維知識&lt;/td>
 &lt;td>集中在 platform team&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>平台化的最大風險是共享平台成為單點 — broker 故障影響所有 tenant。Uber 用跟 LinkedIn 類似的分層叢集策略（critical vs best-effort）降低共享風險，但這也讓平台的運維複雜度上升。&lt;/p></description><content:encoded><![CDATA[<p>Uber 的 Kafka 演進案例揭露了 MQ 從「幾個團隊自管的 broker」到「全公司共享的事件平台」的治理轉折點。轉折的核心判斷是：規模化之後，broker 容量擴展的成本小於 workload 治理缺失的成本。</p>
<h2 id="業務背景">業務背景</h2>
<p>Uber 的事件流涵蓋行程追蹤、司機定位、計費事件、推播通知、即時定價、ETA 計算跟 analytics。早期各團隊各自架設 Kafka 叢集，隨著 Kafka 在 Uber 內部的採用率上升，叢集數量跟 topic 數量快速增長，但沒有統一的治理。</p>
<p>Uber 的 Kafka 規模峰值達到每秒數百萬筆訊息、數十個叢集、數千個 topic。在這個規模下，管理壓力從「單一叢集的 broker 夠不夠」轉到「誰在用、用多少、怎麼收費、故障時誰負責」。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="團隊自管的碎片化">團隊自管的碎片化</h3>
<p>各團隊各自架設 Kafka 時，每個叢集的版本、配置、監控、備份策略都不同。運維知識散落在各團隊，沒有共享的 runbook 或值班流程。某個團隊的 Kafka 出問題時，其他團隊幫不上忙；知識在人員流動時遺失。</p>
<p>碎片化的另一個後果是資源浪費。每個團隊各自預留的容量加總起來遠大於集中管理所需。低流量團隊的叢集常年使用率低於 10%，但因為自管模式下沒有共享容量的機制，資源無法調配。</p>
<h3 id="topic-爆炸與無主-topic">Topic 爆炸與無主 topic</h3>
<p>沒有 topic 建立的治理流程時，任何人都可以建 topic。Topic 的命名不一致、retention 設定不一致、owner 不明。離職的工程師建立的 topic 仍在接收資料、佔用 broker 資源，但沒人知道這些 topic 服務什麼業務。</p>
<p>LinkedIn 後來也遇到同樣的問題並開發了 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">TopicGC</a> 做 topic 生命週期管理。Uber 的解法路線類似 — 把 topic 建立變成需要 owner、retention policy 跟業務標籤的審核流程。</p>
<h3 id="故障排查的責任不清">故障排查的責任不清</h3>
<p>叢集內的故障（broker OOM、partition leader 不均衡、consumer lag spike）需要 Kafka 專業知識排查。團隊自管模式下，每個團隊都需要一定程度的 Kafka 運維能力，但多數團隊的核心能力是業務邏輯而非 MQ 運維。</p>
<p>故障排查的慣性是「先問 Kafka 團隊有沒有人可以幫忙」— 但沒有正式的 Kafka 團隊，所以問的是「上次修過 Kafka 的那個人」。</p>
<h2 id="解法平台化">解法：平台化</h2>
<p>Uber 的解法是把 Kafka 從分散自管收斂到集中平台 — 一個專責的 Kafka platform team 統一管理所有叢集、提供標準化的使用介面。</p>
<h3 id="多租戶治理">多租戶治理</h3>
<p>平台化的核心是多租戶模型 — 每個業務團隊是一個 tenant，tenant 有 quota（ingestion rate、partition 數量上限、retention 上限）跟 cost attribution。</p>
<p>Quota 的目的是防止單一 tenant 的爆量拖累整個平台。Cost attribution 的目的是讓 tenant 看到自己的用量跟成本，驅動合理使用。</p>
<h3 id="標準化-topic-管理">標準化 topic 管理</h3>
<p>Topic 的建立走 self-service portal — 團隊填寫 owner、業務用途、預估流量、retention 需求，portal 自動配置 topic 並建立監控。沒有 owner 的 topic 不允許建立；owner 離職時 topic 需要交接或標記為候選淘汰。</p>
<h3 id="統一監控與值班">統一監控與值班</h3>
<p>Platform team 統一監控所有叢集的 broker 健康（replication lag、under-replicated partitions、disk usage、CPU），提供共用的 dashboard 跟 alert。值班由 platform team 負責 broker 層面的問題，業務層面的問題（consumer 設計錯誤、message 格式不對）由各 tenant team 自行處理。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>團隊自管</th>
          <th>平台化</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自主性</td>
          <td>高（團隊想怎麼配就怎麼配）</td>
          <td>低到中（受 quota 跟 policy 約束）</td>
      </tr>
      <tr>
          <td>運維負擔分配</td>
          <td>分散（每個團隊各自負擔）</td>
          <td>集中（platform team 吸收 broker 層）</td>
      </tr>
      <tr>
          <td>資源利用率</td>
          <td>低（各自預留、無法共用）</td>
          <td>高（共享容量、動態分配）</td>
      </tr>
      <tr>
          <td>治理一致性</td>
          <td>低（版本、配置、命名各自為政）</td>
          <td>高（統一版本、統一配置標準）</td>
      </tr>
      <tr>
          <td>故障影響面</td>
          <td>小（自管叢集只影響自己的團隊）</td>
          <td>大（共享平台故障影響所有 tenant）</td>
      </tr>
      <tr>
          <td>專業知識需求</td>
          <td>每個團隊都要一些 Kafka 運維知識</td>
          <td>集中在 platform team</td>
      </tr>
  </tbody>
</table>
<p>平台化的最大風險是共享平台成為單點 — broker 故障影響所有 tenant。Uber 用跟 LinkedIn 類似的分層叢集策略（critical vs best-effort）降低共享風險，但這也讓平台的運維複雜度上升。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 容量規劃跟 topic 管理的基礎。</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>：共享 Kafka 平台作為 dependency，tenant team 的 reliability budget 怎麼計算。</li>
<li><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 design</a>：平台化後 consumer 設計的規範跟限制。</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>：平台成本歸因到 tenant 的做法。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>組織內有 3 個以上團隊各自架設 Kafka、版本跟配置不統一</li>
<li>Topic 數量持續增長但沒人能說清楚哪些 topic 還在用</li>
<li>故障排查依賴特定個人而非共用的 runbook</li>
<li>叢集資源利用率低但各團隊仍要求擴容</li>
<li>管理層問「Kafka 總共花多少錢、誰在用」但沒人能回答</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.uber.com/en-TW/blog/kafka/">Building Uber&rsquo;s Kafka Infrastructure</a></li>
</ul>
]]></content:encoded></item><item><title>4.C6 AWS：ADOT on EKS 管線遷移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/</guid><description>&lt;p>這個案例的核心責任是把 observability 遷移做成管線治理，而不是單點 agent 替換。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS ADOT on EKS 的實務把 metrics、traces 採集策略整合到可管理的 collector pipeline。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>多代理混用雖然能運作，但在規模化時會放大配置漂移與維運成本。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先統一 collector 部署模式。&lt;/li>
&lt;li>將 exporter 與 sampling 規則集中管理。&lt;/li>
&lt;li>以資料品質指標驗證遷移成效。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws-otel.github.io/docs/getting-started/adot-eks-add-on/">AWS Distro for OpenTelemetry on EKS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 observability 遷移做成管線治理，而不是單點 agent 替換。</p>
<h2 id="觀察">觀察</h2>
<p>AWS ADOT on EKS 的實務把 metrics、traces 採集策略整合到可管理的 collector pipeline。</p>
<h2 id="判讀">判讀</h2>
<p>多代理混用雖然能運作，但在規模化時會放大配置漂移與維運成本。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先統一 collector 部署模式。</li>
<li>將 exporter 與 sampling 規則集中管理。</li>
<li>以資料品質指標驗證遷移成效。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<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> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws-otel.github.io/docs/getting-started/adot-eks-add-on/">AWS Distro for OpenTelemetry on EKS</a></li>
</ul>
]]></content:encoded></item><item><title>5.C6 Airbnb：Kubernetes 叢集擴縮演進</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/</guid><description>&lt;p>這個案例的核心責任是說明部署平台演進常來自容量治理需求。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 的叢集擴縮經歷了多個演進階段。早期是手動調整 node 數量——工程師根據流量預測或事故壓力臨時加 node、事後忘記縮回。中期引入 Cluster Autoscaler，讓 node 數量跟 pending pod 連動。後期隨工作負載類型分化（stateless API、長連線服務、batch job、ML 訓練），單一 autoscaler policy 無法覆蓋所有場景，開始分群治理。&lt;/p>
&lt;p>這個演進路徑的共同主題是「每當流量型態或 workload 組成改變，原本的擴縮策略就會在某個量級開始失效」。擴縮策略的有效期跟服務演進速度成反比。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>叢集擴縮若停留在人工流程，面對高波動流量會放大成本與可用性風險。人工擴縮的問題有兩面：反應太慢（流量已衝高但 node 還沒加上來）和撤退太慢（流量已回落但多餘 node 繼續燒錢）。自動化解決反應速度，但引入新的判讀問題——autoscaler 的參數設定本身需要治理。&lt;/p>
&lt;p>HPA 觸發閾值設太低會造成 pod 數量頻繁抖動；Cluster Autoscaler 的 scale-down delay 設太短會在流量波動時反覆 add/remove node，增加 pod eviction 頻率。這些參數的調校要依 workload 類型分群——API 服務的擴縮節奏跟 batch job 完全不同。&lt;/p>
&lt;p>另一個判讀是擴縮策略跟事故指標要綁定。autoscaler 的動作（scale-up trigger、scale-down execution、node provision latency）如果不在事故 timeline 上可見，事故團隊無法分辨「是 autoscaler 來不及」還是「是應用本身有問題」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>擴縮策略版本化與可回放&lt;/strong>：HPA / VPA / Cluster Autoscaler / Karpenter 的配置進 git，變更走 release flow。每次調參都有 commit 紀錄，事故後可以追溯「這次 scale-down 過快是因為哪次參數變更」。版本化的另一個價值是可回放——新的擴縮配置在 staging 環境用歷史流量 replay 驗證後，再推到 production。&lt;/li>
&lt;li>&lt;strong>workload 分群擴縮&lt;/strong>：stateless API 用 CPU / RPS-based HPA、batch job 用 queue depth-based HPA、長連線服務用 connection count-based 自訂 metric。不同 workload 類型放在不同 namespace，各自有獨立的 autoscaler policy。避免一套 HPA 規則套全部 workload。&lt;/li>
&lt;li>&lt;strong>容量治理與事故指標綁定&lt;/strong>：HPA 觸發事件、Cluster Autoscaler 的 scale-up / scale-down 事件、node provision latency 都送進事故 timeline（可用 Kubernetes event exporter 或 custom metric）。事故 timeline 上看到「HPA 觸發後 3 分鐘 node 才 ready」就能直接判斷「容量補充太慢」而非「應用有 bug」。&lt;/li>
&lt;/ol>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>擴縮策略變更的回退比應用版本回退簡單——改 HPA / autoscaler 的 config 就好。風險在於回退後的舊策略可能已經跟當前 workload 型態不匹配（workload 成長了、流量特性變了）。穩定做法是回退後立刻進入觀察窗口，確認舊策略在當前流量下仍然有效。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看 autoscaling 與部署策略協同。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract&lt;/a> 看不同 workload 的 lifecycle 差異如何影響擴縮設計。回 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity &amp;amp; cost&lt;/a> 看容量規劃的完整框架。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb&lt;/a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明部署平台演進常來自容量治理需求。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 的叢集擴縮經歷了多個演進階段。早期是手動調整 node 數量——工程師根據流量預測或事故壓力臨時加 node、事後忘記縮回。中期引入 Cluster Autoscaler，讓 node 數量跟 pending pod 連動。後期隨工作負載類型分化（stateless API、長連線服務、batch job、ML 訓練），單一 autoscaler policy 無法覆蓋所有場景，開始分群治理。</p>
<p>這個演進路徑的共同主題是「每當流量型態或 workload 組成改變，原本的擴縮策略就會在某個量級開始失效」。擴縮策略的有效期跟服務演進速度成反比。</p>
<h2 id="判讀">判讀</h2>
<p>叢集擴縮若停留在人工流程，面對高波動流量會放大成本與可用性風險。人工擴縮的問題有兩面：反應太慢（流量已衝高但 node 還沒加上來）和撤退太慢（流量已回落但多餘 node 繼續燒錢）。自動化解決反應速度，但引入新的判讀問題——autoscaler 的參數設定本身需要治理。</p>
<p>HPA 觸發閾值設太低會造成 pod 數量頻繁抖動；Cluster Autoscaler 的 scale-down delay 設太短會在流量波動時反覆 add/remove node，增加 pod eviction 頻率。這些參數的調校要依 workload 類型分群——API 服務的擴縮節奏跟 batch job 完全不同。</p>
<p>另一個判讀是擴縮策略跟事故指標要綁定。autoscaler 的動作（scale-up trigger、scale-down execution、node provision latency）如果不在事故 timeline 上可見，事故團隊無法分辨「是 autoscaler 來不及」還是「是應用本身有問題」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>擴縮策略版本化與可回放</strong>：HPA / VPA / Cluster Autoscaler / Karpenter 的配置進 git，變更走 release flow。每次調參都有 commit 紀錄，事故後可以追溯「這次 scale-down 過快是因為哪次參數變更」。版本化的另一個價值是可回放——新的擴縮配置在 staging 環境用歷史流量 replay 驗證後，再推到 production。</li>
<li><strong>workload 分群擴縮</strong>：stateless API 用 CPU / RPS-based HPA、batch job 用 queue depth-based HPA、長連線服務用 connection count-based 自訂 metric。不同 workload 類型放在不同 namespace，各自有獨立的 autoscaler policy。避免一套 HPA 規則套全部 workload。</li>
<li><strong>容量治理與事故指標綁定</strong>：HPA 觸發事件、Cluster Autoscaler 的 scale-up / scale-down 事件、node provision latency 都送進事故 timeline（可用 Kubernetes event exporter 或 custom metric）。事故 timeline 上看到「HPA 觸發後 3 分鐘 node 才 ready」就能直接判斷「容量補充太慢」而非「應用有 bug」。</li>
</ol>
<h2 id="回退判讀">回退判讀</h2>
<p>擴縮策略變更的回退比應用版本回退簡單——改 HPA / autoscaler 的 config 就好。風險在於回退後的舊策略可能已經跟當前 workload 型態不匹配（workload 成長了、流量特性變了）。穩定做法是回退後立刻進入觀察窗口，確認舊策略在當前流量下仍然有效。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看 autoscaling 與部署策略協同。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 platform lifecycle contract</a> 看不同 workload 的 lifecycle 差異如何影響擴縮設計。回 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity &amp; cost</a> 看容量規劃的完整框架。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb</a>（原始 URL 已失效，內容基於骨架與通用工程知識擴充）</li>
</ul>
]]></content:encoded></item><item><title>7.C6 Okta：Cross-tenant Impersonation 防禦回寫</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/</guid><description>&lt;p>這個案例的核心責任是把跨租戶身份濫用轉成可檢測、可回退的控制流程。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Okta 公開 cross-tenant impersonation 預防與偵測建議，揭示管理員流程與身份策略是關鍵風險點。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>若高權限管理流程與租戶隔離規則未收斂，會形成跨租戶攻擊面。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>收斂高權限管理員權限與適用範圍。&lt;/li>
&lt;li>建立 impersonation 相關事件偵測規則。&lt;/li>
&lt;li>將可疑活動納入 incident triage 快速路由。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection/">Cross-Tenant Impersonation: Prevention and Detection&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把跨租戶身份濫用轉成可檢測、可回退的控制流程。</p>
<h2 id="觀察">觀察</h2>
<p>Okta 公開 cross-tenant impersonation 預防與偵測建議，揭示管理員流程與身份策略是關鍵風險點。</p>
<h2 id="判讀">判讀</h2>
<p>若高權限管理流程與租戶隔離規則未收斂，會形成跨租戶攻擊面。</p>
<h2 id="策略">策略</h2>
<ol>
<li>收斂高權限管理員權限與適用範圍。</li>
<li>建立 impersonation 相關事件偵測規則。</li>
<li>將可疑活動納入 incident triage 快速路由。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2</a> 與 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection/">Cross-Tenant Impersonation: Prevention and Detection</a></li>
</ul>
]]></content:encoded></item><item><title>6.6 SLO 與 Error Budget 政策</title><link>https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>SLO 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 是把可靠性從口號變成政策的工具。SLO 定義的是服務要對哪個使用者旅程負責，error budget 定義的是這個責任在一段時間內可以承受多少退化。當這兩個條件被寫清楚，可靠性就能從「感覺上應該穩」變成「超過哪個門檻就要暫停、降風險或修復」。&lt;/p>
&lt;p>這個節點先處理目標，再處理門檻。先問服務要守住什麼體驗，再問這個體驗要用哪些訊號衡量，最後才決定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 到多少時要 freeze。這樣寫的好處是，讀者會先理解政策責任，再理解數字本身。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>SLI 選型：user-journey-centric vs system-metric&lt;/li>
&lt;li>SLO 目標訂定：可達性、商業意義、頻率窗&lt;/li>
&lt;li>error budget：burn rate、policy、freeze 條件&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 觀測&lt;/a> 的訊號交接&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 的凍結觸發&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級&lt;/a> 的門檻對齊&lt;/li>
&lt;li>反模式：cargo-cult 99.99%、SLO 無人擁有、burn rate 無 alert&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>SLO 的責任是讓團隊知道自己到底在保護什麼。當讀者看到一個 SLO 時，第一個問題是這個數字是否對應使用者行為、商業風險與回復成本；數字高低要放在這個脈絡中判讀。&lt;/p>
&lt;p>error budget 的責任是把風險傳導成決策。當 burn rate 開始上升時，團隊先確認 budget 還剩多少、目前的變更是否會放大風險、freeze 條件是否已經被觸發。這裡的重點是路由清楚，數字只是路由的輸入。&lt;/p>
&lt;h2 id="sli-選型">SLI 選型&lt;/h2>
&lt;p>SLI 選型的責任是把使用者旅程轉成可量測訊號。好的 SLI 先描述使用者能否完成重要任務，再選擇最能代表該任務的 log、metric、trace 或 client-side signal。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>SLI 類型&lt;/th>
 &lt;th>適用旅程&lt;/th>
 &lt;th>常見訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Availability&lt;/td>
 &lt;td>request、checkout、login 是否成功&lt;/td>
 &lt;td>success rate、valid response&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency&lt;/td>
 &lt;td>使用者等待是否在可接受範圍&lt;/td>
 &lt;td>latency histogram、p95 / p99&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>資料是否足夠新&lt;/td>
 &lt;td>replication lag、index delay&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Correctness&lt;/td>
 &lt;td>回應是否符合業務語意&lt;/td>
 &lt;td>reconciliation error、mismatch&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Durability&lt;/td>
 &lt;td>寫入是否可保留與回復&lt;/td>
 &lt;td>write success、replay validation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Availability 適合描述同步 API 與 user-facing request。它需要清楚定義分母與分子，例如只計算有效請求、排除客戶端取消，或把 timeout、5xx 與 business failure 分開。&lt;/p>
&lt;p>Latency 適合描述體驗壓力。平均值容易掩蓋長尾，可靠性政策通常需要 percentile 或 histogram，並且要對應使用者旅程，再用單一 process 的 handler time 作為診斷輔助。&lt;/p>
&lt;p>Freshness 適合描述資料管線、search index、cache projection 與 read model。這類服務即使 API 回應成功，資料過舊仍會破壞使用者體驗。&lt;/p>
&lt;p>Correctness 適合描述金流、帳務、庫存、資料同步與 migration。這類可靠性目標需要資料校驗與 reconciliation，而不只看 request 成功率。&lt;/p>
&lt;p>Durability 適合描述 queue、event log、object storage 與資料寫入。它關心寫入後能否找回、重播、備份與回復，常和 RPO / RTO 一起定義。&lt;/p>
&lt;h2 id="slo-政策">SLO 政策&lt;/h2>
&lt;p>SLO 政策的責任是把可靠性目標轉成團隊行為。數字本身只是門檻，政策要說明目標的 owner、時間窗、例外條件、檢視頻率與觸發後動作。&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>User journey&lt;/td>
 &lt;td>定義受保護體驗&lt;/td>
 &lt;td>避免 SLO 停在系統資源層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLI formula&lt;/td>
 &lt;td>定義分母、分子與資料來源&lt;/td>
 &lt;td>保護 SLO 可重算與可解釋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Objective&lt;/td>
 &lt;td>定義目標值與時間窗&lt;/td>
 &lt;td>連接可靠性承諾與風險預算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>指定維護與決策責任&lt;/td>
 &lt;td>讓 policy 能被檢視與調整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Burn alert&lt;/td>
 &lt;td>定義消耗速度與通知條件&lt;/td>
 &lt;td>讓風險在 budget 耗盡前被看見&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freeze action&lt;/td>
 &lt;td>定義暫停發布或限制變更的條件&lt;/td>
 &lt;td>把可靠性風險接到 release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review cadence&lt;/td>
 &lt;td>定義檢視頻率與調整機制&lt;/td>
 &lt;td>避免目標跟服務現況脫節&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User journey 是 SLO 的錨點。checkout、login、message delivery、search freshness、invoice generation 都比 CPU 或 memory 更適合承載可靠性承諾，因為它們能直接對應使用者結果。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>SLO 與 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 是把可靠性從口號變成政策的工具。SLO 定義的是服務要對哪個使用者旅程負責，error budget 定義的是這個責任在一段時間內可以承受多少退化。當這兩個條件被寫清楚，可靠性就能從「感覺上應該穩」變成「超過哪個門檻就要暫停、降風險或修復」。</p>
<p>這個節點先處理目標，再處理門檻。先問服務要守住什麼體驗，再問這個體驗要用哪些訊號衡量，最後才決定 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 到多少時要 freeze。這樣寫的好處是，讀者會先理解政策責任，再理解數字本身。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>SLI 選型：user-journey-centric vs system-metric</li>
<li>SLO 目標訂定：可達性、商業意義、頻率窗</li>
<li>error budget：burn rate、policy、freeze 條件</li>
<li>跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 觀測</a> 的訊號交接</li>
<li>跟 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的凍結觸發</li>
<li>跟 <a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級</a> 的門檻對齊</li>
<li>反模式：cargo-cult 99.99%、SLO 無人擁有、burn rate 無 alert</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>SLO 的責任是讓團隊知道自己到底在保護什麼。當讀者看到一個 SLO 時，第一個問題是這個數字是否對應使用者行為、商業風險與回復成本；數字高低要放在這個脈絡中判讀。</p>
<p>error budget 的責任是把風險傳導成決策。當 burn rate 開始上升時，團隊先確認 budget 還剩多少、目前的變更是否會放大風險、freeze 條件是否已經被觸發。這裡的重點是路由清楚，數字只是路由的輸入。</p>
<h2 id="sli-選型">SLI 選型</h2>
<p>SLI 選型的責任是把使用者旅程轉成可量測訊號。好的 SLI 先描述使用者能否完成重要任務，再選擇最能代表該任務的 log、metric、trace 或 client-side signal。</p>
<table>
  <thead>
      <tr>
          <th>SLI 類型</th>
          <th>適用旅程</th>
          <th>常見訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Availability</td>
          <td>request、checkout、login 是否成功</td>
          <td>success rate、valid response</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>使用者等待是否在可接受範圍</td>
          <td>latency histogram、p95 / p99</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>資料是否足夠新</td>
          <td>replication lag、index delay</td>
      </tr>
      <tr>
          <td>Correctness</td>
          <td>回應是否符合業務語意</td>
          <td>reconciliation error、mismatch</td>
      </tr>
      <tr>
          <td>Durability</td>
          <td>寫入是否可保留與回復</td>
          <td>write success、replay validation</td>
      </tr>
  </tbody>
</table>
<p>Availability 適合描述同步 API 與 user-facing request。它需要清楚定義分母與分子，例如只計算有效請求、排除客戶端取消，或把 timeout、5xx 與 business failure 分開。</p>
<p>Latency 適合描述體驗壓力。平均值容易掩蓋長尾，可靠性政策通常需要 percentile 或 histogram，並且要對應使用者旅程，再用單一 process 的 handler time 作為診斷輔助。</p>
<p>Freshness 適合描述資料管線、search index、cache projection 與 read model。這類服務即使 API 回應成功，資料過舊仍會破壞使用者體驗。</p>
<p>Correctness 適合描述金流、帳務、庫存、資料同步與 migration。這類可靠性目標需要資料校驗與 reconciliation，而不只看 request 成功率。</p>
<p>Durability 適合描述 queue、event log、object storage 與資料寫入。它關心寫入後能否找回、重播、備份與回復，常和 RPO / RTO 一起定義。</p>
<h2 id="slo-政策">SLO 政策</h2>
<p>SLO 政策的責任是把可靠性目標轉成團隊行為。數字本身只是門檻，政策要說明目標的 owner、時間窗、例外條件、檢視頻率與觸發後動作。</p>
<table>
  <thead>
      <tr>
          <th>政策欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>User journey</td>
          <td>定義受保護體驗</td>
          <td>避免 SLO 停在系統資源層</td>
      </tr>
      <tr>
          <td>SLI formula</td>
          <td>定義分母、分子與資料來源</td>
          <td>保護 SLO 可重算與可解釋</td>
      </tr>
      <tr>
          <td>Objective</td>
          <td>定義目標值與時間窗</td>
          <td>連接可靠性承諾與風險預算</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定維護與決策責任</td>
          <td>讓 policy 能被檢視與調整</td>
      </tr>
      <tr>
          <td>Burn alert</td>
          <td>定義消耗速度與通知條件</td>
          <td>讓風險在 budget 耗盡前被看見</td>
      </tr>
      <tr>
          <td>Freeze action</td>
          <td>定義暫停發布或限制變更的條件</td>
          <td>把可靠性風險接到 release gate</td>
      </tr>
      <tr>
          <td>Review cadence</td>
          <td>定義檢視頻率與調整機制</td>
          <td>避免目標跟服務現況脫節</td>
      </tr>
  </tbody>
</table>
<p>User journey 是 SLO 的錨點。checkout、login、message delivery、search freshness、invoice generation 都比 CPU 或 memory 更適合承載可靠性承諾，因為它們能直接對應使用者結果。</p>
<p>SLI formula 需要可重算。分母包含哪些 request、分子如何判定成功、資料來源來自 server-side 還是 client-side、sampling 有哪些限制，都需要寫進政策。</p>
<p>Objective 需要結合商業風險與回復成本。99.9% 與 99.99% 的差異不只是小數點，而是代表可接受 downtime、工程投資、成本與變更節奏的差異。</p>
<p>Freeze action 讓 error budget 進入工程決策。當 budget 消耗過快時，團隊需要知道哪些變更暫停、哪些修復可繼續、哪些例外需要 owner 核准。</p>
<h2 id="error-budget-與-burn-rate">Error Budget 與 Burn Rate</h2>
<p>Error budget 的責任是把可靠性退化轉成可管理的風險餘額。它讓團隊在「追求穩定」與「持續變更」之間有共同語言。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>判讀訊號</th>
          <th>常見動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Budget healthy</td>
          <td>burn rate 低於門檻</td>
          <td>維持正常發布節奏</td>
      </tr>
      <tr>
          <td>Budget warning</td>
          <td>短窗 burn rate 上升</td>
          <td>檢查近期變更與高風險發布</td>
      </tr>
      <tr>
          <td>Budget critical</td>
          <td>多窗口 burn rate 同時超門檻</td>
          <td>暫停高風險變更，優先修復可靠性</td>
      </tr>
      <tr>
          <td>Budget exhausted</td>
          <td>error budget 用盡或接近用盡</td>
          <td>啟動 freeze、復盤與可靠性改善</td>
      </tr>
      <tr>
          <td>Policy mismatch</td>
          <td>SLO 長期過鬆或過緊</td>
          <td>調整 SLI、objective 或時間窗</td>
      </tr>
  </tbody>
</table>
<p>Burn rate 要看短窗與長窗。短窗能捕捉快速事故，長窗能避免一次性尖峰造成過度反應；兩者一起使用，才適合觸發 page、ticket 或 release freeze。</p>
<p>Budget warning 適合做風險整理。團隊可以檢查近期 deploy、feature flag、migration、capacity、dependency 與 incident review action item，判斷是否需要降低變更速度。</p>
<p>Budget critical 適合觸發 release gate。此時可靠性風險已經從觀測層進入決策層，團隊需要把發布、rollback、capacity 與 incident readiness 放在同一張表中判讀。</p>
<p>Budget exhausted 適合觸發可靠性改善。改善內容可能是修 bug、補 capacity、降低 alert noise、補 runbook、重設 SLO 或清理 reliability debt。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>SLO 數字無 owner、過半年沒檢視</li>
<li>burn rate 無 alert、只有 monthly review</li>
<li>error budget 耗盡但 deployment 節奏不變</li>
<li>SLI 用 system metric（CPU / memory）、不對應 user journey</li>
<li>目標數字是抄來的（99.9 / 99.99）、無商業 anchor</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<p>Google 提供的是制度原點，因為它把 SLO、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 與 toil budget 串成可管理的可靠性文化。Honeycomb 提供的是訊號層的延伸，因為 high-cardinality 與 burn rate alert 讓 SLO 可以在真實流量下被看見。Stripe 則把 SLO 風格的決策壓到交易語義上，讓 idempotency 與 migration 不會因為重試而失真。</p>
<p>當讀者把這三個案例放在一起，就會看見 SLO 不只是「填一個百分比」，而是把不同層級的風險接到同一條路由：制度、訊號與交易正確性。這也是本節章節要建立的核心能力。</p>
<h2 id="error-budget-三對齊跟-release-gating">Error Budget 三對齊跟 Release Gating</h2>
<p>Error budget 三對齊是把「SLI 範圍」「SLO 目標」「Budget gate 觸發點」分別跟「使用者價值 / 可接受承諾 / 交付節奏」綁定的設計練習。任一條未對齊、policy 就會跟團隊行為脫鉤 — SLI 不對齊使用者價值、policy 就保護錯的東西；SLO 不對齊承諾、團隊就追錯目標；Gate 不對齊交付節奏、政策就無人遵循。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">G1 Google Error Budget Policy</a>：揭露 SLO policy 設計的三個對齊 — 使用者行為對齊（哪些 journey 直接反映服務價值 → SLI 範圍）、可靠性承諾對齊（什麼水準算服務仍可接受 → SLO 目標）、交付節奏對齊（可靠性消耗到哪裡要改變發布策略 → Budget gate）。</p>
<p>三對齊完成後、release gate 可從「主觀風險判斷」轉成「政策驅動」：</p>
<ul>
<li>budget 健康：正常發版</li>
<li>budget 快速消耗：啟用變更限速、提高驗證門檻</li>
<li>budget 透支：凍結非必要變更、先修復與回補訊號</li>
</ul>
<p>把 budget gate 跟 <a href="/blog/backend/06-reliability/release-gate/#%e8%ae%8a%e6%9b%b4%e5%88%86%e5%b1%a4%e8%b7%9f-gate-%e6%94%bf%e7%ad%96" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release-gate 變更分層段</a> 綁定、讓「budget 三階段」對應「release gate 三層放行決策」。</p>
<p>Error budget 是「可靠性 vs 交付節奏」的平衡工具、不是被追求的固定分數。當 budget 被 KPI 化、SLI 範圍會被縮小、告警會被延後、例外條件會被擴張 — 三者都降低 budget 的判讀可信度。</p>
<h2 id="burn-rate-雙窗監控">Burn Rate 雙窗監控</h2>
<p>Burn rate 雙窗監控是把「budget 消耗速率」拆成短窗（急性事故）跟長窗（慢性退化）兩個 channel、各自觸發不同回應的設計。比固定閾值告警更接近使用者體感、且能區分「需立即頁」跟「需排修復節奏」。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">HC1 Honeycomb Burn Rate 驅動可靠性操作</a>：揭露 fast burn / slow burn 雙窗監控的價值 — 固定閾值告警在高變化流量下容易失真、burn rate 提供比固定閾值更接近使用者體感的判讀方式。</p>
<p>雙窗監控的設計：</p>
<ul>
<li><strong>Fast burn</strong>（短窗、高消耗率）：捕捉急性事故、觸發 page 立即響應</li>
<li><strong>Slow burn</strong>（長窗、低消耗率持續累積）：捕捉慢性退化、觸發 ticket 排入修復節奏</li>
</ul>
<p>兩窗一起用、避免單一閾值在不同流量型態下失真。Honeycomb 自家平台展示 burn rate 訊號可以跟 trace outlier path 對接 — 看到 burn rate 上升、能直接跳到具體退化 trace（這是 Honeycomb 的產品特色、tracing-first 對 burn rate 的補強）。vendor-neutral 的同類概念見 <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> 跟 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 sli-slo-signal</a> 的訊號設計。</p>
<h2 id="控制面">控制面</h2>
<p>SLO 與 error budget 的控制面是把可靠性訊號接到發布、事故與改善流程。SLO 只有在能改變團隊行為時，才會成為政策。</p>
<ol>
<li>SLI 設計回到 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI 量測與 SLO 訊號設計</a>。</li>
<li>資料品質限制回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>。</li>
<li>Budget warning 進入 release risk review。</li>
<li>Budget critical 進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</li>
<li>事故觸發與復盤回寫進入 <a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級</a> 與 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 復盤</a>。</li>
</ol>
<p>SLO policy 需要定期校準。服務規模、使用者旅程、依賴型態與商業風險變化後，原本的 SLI、objective 與 freeze 條件也要重新檢視。</p>
<p>SLO policy 也需要例外流程。重大資安修補、合規變更、資料修復或客戶承諾可能需要在 budget 緊張時繼續推進；例外應記錄 owner、理由、風險與回退條件。</p>
<h2 id="產業情境金融科技">產業情境：金融科技</h2>
<p>金融服務的 error budget 治理需要把合規週期納入凍結條件。交易關鍵路徑（payment / settlement / reconciliation）的 SLO 破壞可能直接觸發監管通報義務，budget 消耗到門檻時的升級路徑必須包含合規人員。</p>
<p>交易路徑的 SLI 選型需要涵蓋 correctness（reconciliation error rate），availability 和 latency 通過但對帳失敗仍然是 SLO 破壞。correctness SLI 的量測來源通常是日終或即時的 reconciliation pipeline，跟 availability SLI 的即時 request-level 量測有不同的時間粒度。</p>
<p>Budget 凍結的觸發條件除了 burn rate，還要對齊監管報告週期。若 budget 在季末報告前已消耗過多，凍結應提前啟動，因為報告期間內的可靠性退化會被放大審視。這個提前量取決於報告週期長度與修復節奏 — 月報制的提前量比季報制短。</p>
<p>Error budget 政策的升級路徑需要跟 compliance team 對齊。budget warning 階段通知工程 owner；budget critical 階段同時通知合規人員；budget exhausted 階段啟動合規審查流程。這個分層讓合規介入的時機跟工程介入同步，避免事後才發現可靠性退化已觸發通報義務。</p>
<p>金融場景的 budget 恢復比一般 SaaS 慢。恢復期間需要額外的 reconciliation 驗證（確認退化期間無交易錯漏）才能宣告 budget 回補。若 reconciliation 發現差異，budget 恢復會被延後直到差異被解決。這個約束讓金融服務的 freeze 持續時間通常比一般服務長。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>SLO 反模式通常來自把目標數字當成可靠性制度本身。數字需要對應旅程、資料、owner 與決策，才有工程意義。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cargo-cult 99.99%</td>
          <td>目標抄自外部範例</td>
          <td>從 user journey 與商業風險回推</td>
      </tr>
      <tr>
          <td>System metric SLO</td>
          <td>SLO 看 CPU / memory</td>
          <td>改用成功率、延遲、freshness</td>
      </tr>
      <tr>
          <td>SLO 無 owner</td>
          <td>目標存在但無人調整</td>
          <td>指定 policy owner 與 review</td>
      </tr>
      <tr>
          <td>Burn rate 無 alert</td>
          <td>budget 耗盡後才開會</td>
          <td>建立短窗 / 長窗 burn alert</td>
      </tr>
      <tr>
          <td>Freeze 無路由</td>
          <td>可靠性風險不影響發布</td>
          <td>接到 release gate 與例外流程</td>
      </tr>
  </tbody>
</table>
<p>Cargo-cult 99.99% 的問題在於缺少服務脈絡。高可用目標會增加架構、成本、演練與值班負擔；低可用目標則會增加使用者與商業風險。合理目標要從服務承諾回推。</p>
<p>System metric SLO 會讓可靠性偏向基礎設施視角。CPU 健康不代表 checkout 成功，pod running 不代表資料新鮮；系統指標適合支援 diagnosis，user journey 指標適合承載 SLO。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04 訊號治理：SLI / burn rate metric 設計</li>
<li>06.8 release gate：error budget 耗盡觸發 freeze</li>
<li>06.9 capacity / cost：容量不足傳導為 SLO 風險</li>
<li>06.14 dependency budget：依賴可靠性納入 SLO 算式</li>
<li>08 事故閉環：burn rate alert 啟動條件</li>
<li>08.13 repeated / toil：error budget 撥用 toil reduction</li>
<li>06.18 reliability metrics：SLO 跟 DORA / SPACE 的指標分層</li>
</ul>
]]></content:encoded></item><item><title>AWS ELB（ALB / NLB / CLB）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/</guid><description>&lt;p>AWS ELB 是 AWS managed load balancer 系列、承擔三個責任：流量入口（HTTP/HTTPS for ALB、TCP/UDP for NLB）、health check + draining、跟 AWS 生態整合（ACM TLS / Target Group / WAF / Lambda）。包含 ALB（L7、HTTP/HTTPS）、NLB（L4、極低延遲）、CLB（legacy、不要選）。設計取捨偏向「managed + AWS-native + integrate with ECS/EKS/Lambda」、跨雲 / 進階 traffic management 是限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>建立 ALB / NLB、配置 listener + target group&lt;/li>
&lt;li>設計 health check + connection draining&lt;/li>
&lt;li>用 ACM 自動憑證 + SNI&lt;/li>
&lt;li>用 ALB Ingress Controller / AWS Load Balancer Controller for K8s&lt;/li>
&lt;li>評估 ALB vs NLB vs CloudFront vs API Gateway&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-aws-elb-跑起來">最短路徑：5 分鐘把 AWS ELB 跑起來&lt;/h2>





&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"># 1. 建 ALB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">aws elbv2 create-load-balancer &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> --name demo-alb &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> --subnets subnet-aaa subnet-bbb &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> --security-groups sg-xxx &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> --scheme internet-facing &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> --type application
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 建 target group + register targets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">aws elbv2 create-target-group &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --name demo-tg &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --protocol HTTP --port &lt;span class="m">8080&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --vpc-id vpc-xxx &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --target-type instance &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --health-check-path /health &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --health-check-interval-seconds &lt;span class="m">15&lt;/span>
&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">aws elbv2 register-targets &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/... &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --targets &lt;span class="nv">Id&lt;/span>&lt;span class="o">=&lt;/span>i-0abc123 &lt;span class="nv">Id&lt;/span>&lt;span class="o">=&lt;/span>i-0def456
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 建 listener + 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">aws elbv2 create-listener &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/demo-alb/... &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --protocol HTTP --port &lt;span class="m">80&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --default-actions &lt;span class="nv">Type&lt;/span>&lt;span class="o">=&lt;/span>forward,TargetGroupArn&lt;span class="o">=&lt;/span>arn:aws:...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="nv">ALB_DNS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>aws elbv2 describe-load-balancers --names demo-alb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;LoadBalancers[0].DNSName&amp;#39;&lt;/span> --output text&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">curl &lt;span class="s2">&amp;#34;http://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ALB_DNS&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="alb-vs-nlb-vs-clb">ALB vs NLB vs CLB&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>ALB：L7、path/host routing、WebSocket、gRPC、Lambda target&lt;/li>
&lt;li>NLB：L4、static IP、preserve client IP、極低延遲、TCP/UDP&lt;/li>
&lt;li>CLB：legacy、不要新用&lt;/li>
&lt;li>選擇判讀：HTTP/HTTPS → ALB；TCP/UDP / 高吞吐 → NLB&lt;/li>
&lt;/ul>
&lt;h3 id="target-group--listener-rule">Target group / listener rule&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>AWS ELB 是 AWS managed load balancer 系列、承擔三個責任：流量入口（HTTP/HTTPS for ALB、TCP/UDP for NLB）、health check + draining、跟 AWS 生態整合（ACM TLS / Target Group / WAF / Lambda）。包含 ALB（L7、HTTP/HTTPS）、NLB（L4、極低延遲）、CLB（legacy、不要選）。設計取捨偏向「managed + AWS-native + integrate with ECS/EKS/Lambda」、跨雲 / 進階 traffic management 是限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>建立 ALB / NLB、配置 listener + target group</li>
<li>設計 health check + connection draining</li>
<li>用 ACM 自動憑證 + SNI</li>
<li>用 ALB Ingress Controller / AWS Load Balancer Controller for K8s</li>
<li>評估 ALB vs NLB vs CloudFront vs API Gateway</li>
</ol>
<h2 id="最短路徑5-分鐘把-aws-elb-跑起來">最短路徑：5 分鐘把 AWS ELB 跑起來</h2>





<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. 建 ALB</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elbv2 create-load-balancer <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --name demo-alb <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --subnets subnet-aaa subnet-bbb <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --security-groups sg-xxx <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --scheme internet-facing <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --type application
</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="c1"># 2. 建 target group + register targets</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">aws elbv2 create-target-group <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --name demo-tg <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --protocol HTTP --port <span class="m">8080</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --vpc-id vpc-xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --target-type instance <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --health-check-path /health <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --health-check-interval-seconds <span class="m">15</span>
</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">aws elbv2 register-targets <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>  --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/... <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --targets <span class="nv">Id</span><span class="o">=</span>i-0abc123 <span class="nv">Id</span><span class="o">=</span>i-0def456
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 3. 建 listener + 驗證</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">aws elbv2 create-listener <span class="se">\
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="se"></span>  --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/demo-alb/... <span class="se">\
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="se"></span>  --protocol HTTP --port <span class="m">80</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="se"></span>  --default-actions <span class="nv">Type</span><span class="o">=</span>forward,TargetGroupArn<span class="o">=</span>arn:aws:...
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nv">ALB_DNS</span><span class="o">=</span><span class="k">$(</span>aws elbv2 describe-load-balancers --names demo-alb <span class="se">\
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;LoadBalancers[0].DNSName&#39;</span> --output text<span class="k">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">curl <span class="s2">&#34;http://</span><span class="si">${</span><span class="nv">ALB_DNS</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="alb-vs-nlb-vs-clb">ALB vs NLB vs CLB</h3>
<p>子議題：</p>
<ul>
<li>ALB：L7、path/host routing、WebSocket、gRPC、Lambda target</li>
<li>NLB：L4、static IP、preserve client IP、極低延遲、TCP/UDP</li>
<li>CLB：legacy、不要新用</li>
<li>選擇判讀：HTTP/HTTPS → ALB；TCP/UDP / 高吞吐 → NLB</li>
</ul>
<h3 id="target-group--listener-rule">Target group / listener rule</h3>
<p>子議題：</p>
<ul>
<li>Target type：instance / IP / Lambda</li>
<li>Listener rule：path-based / host-based / header-based routing</li>
<li>Priority 排序</li>
<li>對應指令：<code>aws elbv2 modify-rule</code></li>
</ul>
<h3 id="health-check-與-draining">Health check 與 draining</h3>
<p>子議題：</p>
<ul>
<li>Health check：HTTP path / interval / threshold</li>
<li>Connection draining（deregistration delay）：deregister 後等到 in-flight requests 完成</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例 cutover without drain</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="tls-termination--sni">TLS termination + SNI</h3>
<p>子議題：</p>
<ul>
<li>ACM 自動憑證 + 續期</li>
<li>SNI：單 ALB 多 domain（最多 25 certificates）</li>
<li>TLS policy（min TLS version）</li>
<li>Mutual TLS（ALB 2023+）</li>
</ul>
<h3 id="alb-ingress-controller--aws-load-balancer-controller">ALB Ingress Controller / AWS Load Balancer Controller</h3>
<p>子議題：</p>
<ul>
<li>在 EKS 內配置 ALB / NLB（Ingress / Service of type LoadBalancer）</li>
<li>IngressClass / annotations</li>
<li>Pod readiness gate（pod 到 ALB target group healthy 才接流量）</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
<h3 id="cross-zone-load-balancing">Cross-zone load balancing</h3>
<p>子議題：</p>
<ul>
<li>ALB default enabled、NLB default disabled</li>
<li>Cross-zone 跨 AZ data transfer cost</li>
<li>跟 AZ failover 對應</li>
</ul>
<h3 id="waf-integration">WAF integration</h3>
<p>子議題：</p>
<ul>
<li>AWS WAF on ALB</li>
<li>Rate-based rule / managed rule group</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security WAF</a></li>
</ul>
<h3 id="idle-timeout">Idle timeout</h3>
<p>子議題：</p>
<ul>
<li>ALB default 60s、可調 1-4000s</li>
<li>跟 keep-alive / WebSocket 長連線對應</li>
<li>跟 backend（K8s pod / EC2）的 timeout 對齊</li>
</ul>
<h3 id="cost-模型">Cost 模型</h3>
<p>子議題：</p>
<ul>
<li>LB-hour（per ALB / NLB）</li>
<li>LCU（Load Balancer Capacity Unit）— 多維度計算</li>
<li>Data processing charge</li>
<li>跨 AZ data transfer</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="target-unhealthy">Target unhealthy</h3>
<p>操作原則：health check path 不對 / security group 沒開 / backend 反應慢。</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 elbv2 describe-target-health <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/demo-tg/...
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># HealthState: unhealthy → 查 Reason（Target.Timeout / Elb.InternalError / Target.ResponseCodeMismatch）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 常見根因：security group 沒開 health check port、health check path 回 404、backend 回應超過 timeout</span></span></span></code></pre></div><h3 id="504-gateway-timeout">504 Gateway Timeout</h3>
<p>操作原則：backend 超 ALB idle timeout / 60s。判讀：backend log + ALB access log。</p>
<h3 id="cross-zone-imbalance">Cross-zone imbalance</h3>
<p>操作原則：cross-zone disabled、流量集中單 AZ。修法：enable cross-zone（注意 cost）。</p>
<h3 id="draining-卡住">Draining 卡住</h3>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>。判讀：deregistration delay 太短 / connection 未結束就被斷。</p>
<h3 id="acm-cert-renew-失敗">ACM cert renew 失敗</h3>
<p>操作原則：DNS validation 失敗 / domain ownership 變動。判讀：ACM console 看 cert state。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲 / 自管</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> / <a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a> + Istio</td>
      </tr>
      <tr>
          <td>Cloud-native auto-discovery</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
      </tr>
      <tr>
          <td>CDN / edge</td>
          <td>CloudFront / Cloudflare / Fastly</td>
      </tr>
      <tr>
          <td>API Gateway</td>
          <td>AWS API Gateway / Kong</td>
      </tr>
      <tr>
          <td>極低成本</td>
          <td>自管 <a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a> on EC2</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS WAF rule 完整 reference</li>
<li>Network Firewall 配置</li>
<li>各 AWS region 限制差異</li>
<li>ELB classic（CLB）細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>遷 EKS 時 ALB / NLB 是入口、切流批次跟 target group 權重連動</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多集群整併 EKS、AWS Load Balancer Controller 統一 ingress 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye EKS</a></td>
          <td>大規模 workload 遷 EKS、ALB target group health check 是切流驗證點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed EKS 後 ALB / NLB 治理回到平台團隊</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 AWS ELB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>ALB deregistration delay / NLB connection draining 是切流的關鍵回退面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>AWS 生態小型 ALB + EC2 / 中型 ALB + EKS / 大型 NLB + 多 region + WAF</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 AWS ELB 案例</strong>：大規模 AWS Load Balancer Controller 客戶案例、NLB static IP 場景、AWS WAF + ALB 安全整合。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security WAF</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability release gate</a></li>
</ul>
]]></content:encoded></item><item><title>Google Cloud Pub/Sub</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/</guid><description>&lt;p>Google Cloud Pub/Sub 是 GCP managed pub/sub 服務、承擔三個責任：全球 topic 路由（無 region 概念）、彈性 delivery（push 跟 pull 並存）、GCP 生態整合（BigQuery / Dataflow / Cloud Run）。設計取捨偏向「topic 是 first-class、subscription 各自進度、ack deadline 控制重試」、跟 Kafka 的 partition / consumer group 思路不同。&lt;/p>
&lt;p>對「GCP 生態事件分發、跨 region 全球路由、push HTTP endpoint 接收事件、Dataflow streaming」這條路徑、Pub/Sub 是首選。本頁先給最短路徑、再展開日常 topic / subscription 操作與 ack deadline 設計、最後進階治理（ordering、DLT、push endpoint、IAM）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 gcloud CLI 建 topic / subscription、publish / pull 訊息&lt;/li>
&lt;li>區分 push vs pull subscription、選擇對應的 delivery 模型&lt;/li>
&lt;li>設計 ack deadline 與 ackExtension、處理長任務&lt;/li>
&lt;li>配置 dead-letter topic 與 retry policy&lt;/li>
&lt;li>評估 ordering key、Pub/Sub Lite、BigQuery subscription 等延伸場景&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-pubsub-跑起來">最短路徑：5 分鐘把 Pub/Sub 跑起來&lt;/h2>





&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"># 1. 建 topic&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">gcloud pubsub topics create demo-topic
&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"># 2. 建 subscription（pull 模式、綁定 topic）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">gcloud pubsub subscriptions create demo-sub --topic&lt;span class="o">=&lt;/span>demo-topic
&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="c1"># 3. publish + pull 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">gcloud pubsub topics publish demo-topic --message&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">gcloud pubsub subscriptions pull demo-sub --auto-ack&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「topic / subscription 建得起來、能發能收」。實際應用見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。指令對真實 GCP 需設定 project 與認證；本機要先驗證可啟動 Pub/Sub emulator、用 &lt;code>gcloud config set api_endpoint_overrides/pubsub&lt;/code> 把同一組 CLI 指向 emulator 跑通。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="gcloud-cli-與-client-library">gcloud CLI 與 client library&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>gcloud CLI 指令對照表（topics / subscriptions / publish / pull / ack）&lt;/li>
&lt;li>Client library 配置：credentials / flow control / async vs sync&lt;/li>
&lt;li>Batch publish（提高吞吐、增加延遲的取捨）&lt;/li>
&lt;li>對應指令範例：&lt;code>gcloud pubsub subscriptions describe &amp;lt;sub&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="topic--subscription-設計">Topic / Subscription 設計&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic&lt;/a> 是 first-class entity、跟 Kafka 不同的是 subscription 才是 consumer 抽象：&lt;/p></description><content:encoded><![CDATA[<p>Google Cloud Pub/Sub 是 GCP managed pub/sub 服務、承擔三個責任：全球 topic 路由（無 region 概念）、彈性 delivery（push 跟 pull 並存）、GCP 生態整合（BigQuery / Dataflow / Cloud Run）。設計取捨偏向「topic 是 first-class、subscription 各自進度、ack deadline 控制重試」、跟 Kafka 的 partition / consumer group 思路不同。</p>
<p>對「GCP 生態事件分發、跨 region 全球路由、push HTTP endpoint 接收事件、Dataflow streaming」這條路徑、Pub/Sub 是首選。本頁先給最短路徑、再展開日常 topic / subscription 操作與 ack deadline 設計、最後進階治理（ordering、DLT、push endpoint、IAM）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 gcloud CLI 建 topic / subscription、publish / pull 訊息</li>
<li>區分 push vs pull subscription、選擇對應的 delivery 模型</li>
<li>設計 ack deadline 與 ackExtension、處理長任務</li>
<li>配置 dead-letter topic 與 retry policy</li>
<li>評估 ordering key、Pub/Sub Lite、BigQuery subscription 等延伸場景</li>
</ol>
<h2 id="最短路徑5-分鐘把-pubsub-跑起來">最短路徑：5 分鐘把 Pub/Sub 跑起來</h2>





<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. 建 topic</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud pubsub topics create demo-topic
</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"># 2. 建 subscription（pull 模式、綁定 topic）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud pubsub subscriptions create demo-sub --topic<span class="o">=</span>demo-topic
</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="c1"># 3. publish + pull 驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">gcloud pubsub topics publish demo-topic --message<span class="o">=</span><span class="s2">&#34;hello&#34;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">gcloud pubsub subscriptions pull demo-sub --auto-ack</span></span></code></pre></div><p>最短路徑驗證「topic / subscription 建得起來、能發能收」。實際應用見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。指令對真實 GCP 需設定 project 與認證；本機要先驗證可啟動 Pub/Sub emulator、用 <code>gcloud config set api_endpoint_overrides/pubsub</code> 把同一組 CLI 指向 emulator 跑通。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="gcloud-cli-與-client-library">gcloud CLI 與 client library</h3>
<p>子議題：</p>
<ul>
<li>gcloud CLI 指令對照表（topics / subscriptions / publish / pull / ack）</li>
<li>Client library 配置：credentials / flow control / async vs sync</li>
<li>Batch publish（提高吞吐、增加延遲的取捨）</li>
<li>對應指令範例：<code>gcloud pubsub subscriptions describe &lt;sub&gt;</code></li>
</ul>
<h3 id="topic--subscription-設計">Topic / Subscription 設計</h3>
<p><a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a> 是 first-class entity、跟 Kafka 不同的是 subscription 才是 consumer 抽象：</p>
<ul>
<li>1 topic ↔ N subscription（fan-out 內建）</li>
<li>Subscription 各自進度（無 consumer group 概念）</li>
<li>Subscription expiration policy（閒置 N 天自動刪）</li>
</ul>
<h3 id="push-vs-pull-subscription">Push vs Pull subscription</h3>
<p>子議題：</p>
<ul>
<li>Push：Pub/Sub 主動 POST 到 HTTP endpoint、適合無狀態 worker / Cloud Run</li>
<li>Pull：consumer 主動拉取、適合長 worker / 需要 flow control</li>
<li>Push endpoint 要求（HTTPS、認證）</li>
<li>兩者的可靠性 / latency / cost 對照</li>
</ul>
<h3 id="ack-deadline-與-ack-extension">Ack deadline 與 ack extension</h3>
<p>子議題：</p>
<ul>
<li>Ack deadline：subscription 等待 ack 的時間（預設 10 秒、上限 600 秒）</li>
<li>Modify ack deadline（長任務動態延長）</li>
<li>Client library 的自動 ack extension</li>
<li>跟 SQS visibility timeout 的對照（語意類似、機制不同）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>ordering key、dead-letter topic 與 schema enforcement 已展開為 deep article：<a href="ordering-dlt-schema/">ordering key / DLT / schema enforcement</a>、<a href="push-pull-ack-flow-control/">push / pull / ack flow control</a>。下列子議題段保留選題判讀入口。</p>
<h3 id="ordering-key">Ordering key</h3>
<p>子議題：</p>
<ul>
<li>啟用 ordering 的限制（subscription 設定 enableMessageOrdering）</li>
<li>Ordering 在 push 跟 pull 的差異</li>
<li>跟 Kafka partition + key 的對照</li>
<li>性能影響（throughput 受限）</li>
</ul>
<h3 id="dead-letter-topic">Dead-letter topic</h3>
<p>子議題：</p>
<ul>
<li>設定 max delivery attempt、超過送到 DLT</li>
<li>DLT 是另一個 topic、可以再訂閱重處理</li>
<li>跟 SQS DLQ 的差異（DLT 是 topic、不是 queue）</li>
</ul>
<h3 id="pubsub-lite">Pub/Sub Lite</h3>
<p>子議題：</p>
<ul>
<li>Pub/Sub Lite vs Pub/Sub（partition-based、zonal、cost 低）</li>
<li>何時用 Lite（高吞吐、確定 region）</li>
<li>何時用 standard（global routing 內建）</li>
</ul>
<h3 id="bigquery-subscription--cloud-storage-subscription">BigQuery subscription / Cloud Storage subscription</h3>
<p>子議題：</p>
<ul>
<li>BigQuery subscription：訊息直接寫入 BQ table（無需 Dataflow）</li>
<li>Cloud Storage subscription：訊息批次寫入 GCS object</li>
<li>適合 streaming analytics / data lake 場景</li>
</ul>
<h3 id="schema-enforcement">Schema enforcement</h3>
<p>子議題：</p>
<ul>
<li>Topic 綁定 schema（Avro / Protobuf）</li>
<li>Schema evolution</li>
<li>跟 Kafka Schema Registry 的對照</li>
</ul>
<h3 id="iam--service-account">IAM / Service Account</h3>
<p>子議題：</p>
<ul>
<li>Pub/Sub IAM role（publisher / subscriber / viewer）</li>
<li>Service Account 認證（push endpoint 用）</li>
<li>VPC Service Controls</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="subscriber-backlogunacked-messages-累積">Subscriber backlog（unacked messages 累積）</h3>
<p>操作原則：先看是 push 還是 pull、再定位 endpoint 失敗 vs flow control 限制。</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">gcloud pubsub subscriptions describe &lt;sub&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 看 ackDeadlineSeconds（預設 10s）與 messageRetentionDuration（預設 604800s / 7 天）是否符合處理時間與 replay 需求</span></span></span></code></pre></div><p>判讀：Cloud Monitoring metric 的 <code>num_undelivered_messages</code> 與 <code>oldest_unacked_message_age</code>。</p>
<h3 id="push-endpoint-500retry-storm">Push endpoint 500（retry storm）</h3>
<p>操作原則：push endpoint 持續 500、Pub/Sub 會 backoff retry、看 retry policy 設定。判讀：endpoint 健康 vs 訊息毒性。</p>
<h3 id="ordering-key-限制誤用">Ordering key 限制誤用</h3>
<p>操作原則：啟用 ordering 後 throughput 變低、單一 ordering key 是順序的。判讀：throughput 是否被 ordering 限制、可拆 ordering key。</p>
<h3 id="iam-權限錯">IAM 權限錯</h3>
<p>操作原則：publish / pull / ack 各自需要不同 IAM role。判讀：用 Cloud Logging 看 deny 原因。</p>
<h3 id="subscription-expired">Subscription expired</h3>
<p>操作原則：閒置太久 subscription 被 GC。判讀：subscription expiration policy 設定 + 監控 lastReceiveTime。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 streaming + replay long window</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / Confluent Cloud</td>
      </tr>
      <tr>
          <td>需要 partition + consumer group</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / Pub/Sub Lite</td>
      </tr>
      <tr>
          <td>需要複雜 routing</td>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> on GKE</td>
      </tr>
      <tr>
          <td>跨雲 / 跨平台</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
      </tr>
      <tr>
          <td>AWS 生態</td>
          <td><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> / SNS</td>
      </tr>
      <tr>
          <td>Workflow + durable execution</td>
          <td>Google Workflows / Temporal</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Dataflow / BigQuery 完整功能（另開 streaming analytics 章節）</li>
<li>Cloud Run / Functions 整合細節</li>
<li>各語言 client 完整 API</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="pubsub-專屬案例c60-c69">Pub/Sub 專屬案例（C60-C69）</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-event-delivery-platform/" data-link-title="3.C60 Spotify：Event Delivery 從 Kafka 遷到 Pub/Sub" data-link-desc="Spotify 全球 event delivery 從 Kafka 遷到 Pub/Sub、~2500 VM、Q1 2019 8M events/s、350TB/day raw、自建 dedup。">3.C60 Spotify Event Delivery</a></td>
          <td>從 Kafka 遷入 / 自建 dedup</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61 Spotify autoscaling</a></td>
          <td>Backlog ≠ healthy / autoscale 反效果</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-cloud-storage-export/" data-link-title="3.C62 Spotify：Pub/Sub → GCS reliable export" data-link-desc="Spotify 用 Oldest Unacknowledged Message metric 判斷 hourly bucket 何時可安全關閉、ack 綁定下游 commit。">3.C62 Spotify GCS export</a></td>
          <td>Ack = end-to-end commit</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/" data-link-title="3.C63 Mercari Actionable History：ack deadline 是 batch-level" data-link-desc="Merpay 支付流水帳用 Pub/Sub、ack deadline 是整批 batch 而非單訊息、acked 訊息會跟同批 expired 一起 redeliver。">3.C63 Mercari Actionable History</a></td>
          <td>Ack deadline 是 batch-level（陷阱）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64 Mercari Item Feed DLT</a></td>
          <td>DLT 防 poison message 阻塞</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65 Mercari LINE flow control</a></td>
          <td>Pull subscription 對齊外部 RPS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-b2c-grpc-pusher/" data-link-title="3.C66 Mercari B2C：自建 PubSub gRPC Pusher" data-link-desc="Mercari 全球商品同步、原生 HTTP push 在「長 job &#43; 高吞吐 &#43; 動態 RPS」場景受限、自建 gRPC 版 push。">3.C66 Mercari B2C gRPC pusher</a></td>
          <td>自建 push / 長 job + 動態 RPS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/" data-link-title="3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest" data-link-desc="Pokémon GO frontend publish 玩家事件、~1M TPS、Pub/Sub elastic buffer、下游 BigQuery streaming。">3.C67 Niantic Pokémon GO</a></td>
          <td>Elastic buffer / BQ streaming</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/" data-link-title="3.C68 Wix：Pub/Sub decouple &#43; Dataflow &#43; BQ archive" data-link-desc="Wix App Engine 收 clickstream 進 Pub/Sub、Dataflow 進 Datastore &lt; 100ms、BigQuery 並行存 raw recovery。">3.C68 Wix clickstream</a></td>
          <td>Pub/Sub + Dataflow + BQ 教科書組合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-twitter-ad-engagement/" data-link-title="3.C69 Twitter Ad Engagement：把 stream 切成多 topic 做 partition" data-link-desc="Twitter 把 80K msg/s stream 切成 6 個 topic 做 partition、Avro schema、Beam/Dataflow → Bigtable/BQ。">3.C69 Twitter Ad Engagement</a></td>
          <td>多 topic 切分取代 partition</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Pub/Sub 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8 Cloudflare Queues</a></td>
          <td>全球交付對照：Pub/Sub global routing 內建</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 規模對照</a></td>
          <td>中小型直接用 / 大型考慮 Pub/Sub Lite / 超大跨雲走 Kafka</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20 Spotify 遷出 Kafka</a></td>
          <td>Pub/Sub 遷入的源頭（為何遷出 Kafka）</td>
      </tr>
  </tbody>
</table>
<p><strong>IAM + Service Account 缺直接 customer engineering case</strong>：customer engineering blog 著墨少、建議撰寫該段時依 GCP 官方 IAM 文件 + 通用安全原則。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a>、<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a></li>
<li>平行 vendor：<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>、<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>下游能力：<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>、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
</ul>
]]></content:encoded></item><item><title>Honeycomb</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/</guid><description>&lt;p>Honeycomb 是 high-cardinality observability SaaS、承擔三個責任：events-based 資料模型（不是 metrics aggregation）、unknown-unknowns 偵錯能力（BubbleUp / Heatmap）、observability-driven SRE 文化代表平台。設計取捨偏向「深度優於廣度」、不追求 Datadog 的 integration 廣度、專注於 high-cardinality + distributed system debugging。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 Honeycomb SDK 或 OTel 送 events 到 Honeycomb&lt;/li>
&lt;li>用 BubbleUp 找 outlier 模式（unknown-unknowns）&lt;/li>
&lt;li>設計 SLO + burn rate alert&lt;/li>
&lt;li>配置 Refinery（tail-based sampling）&lt;/li>
&lt;li>評估 Honeycomb vs Datadog 的選用判讀&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-honeycomb-跑起來">最短路徑：5 分鐘把 Honeycomb 跑起來&lt;/h2>





&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"># 1. 應用程式加 instrumentation（Honeycomb SDK 或 OTel SDK）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: HONEYCOMB_API_KEY + dataset 設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 用 Beeline SDK 或 OTel + OTLP exporter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 送 sample events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 觀察 trace 出現在 Honeycomb UI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 用 query 介面查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: SELECT count + visualize by service.name&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="events-vs-metrics-心智模型">Events vs metrics 心智模型&lt;/h3>
&lt;p>Honeycomb 跟 metrics-aggregation 平台不同。子議題：&lt;/p>
&lt;ul>
&lt;li>Event = 一個 trace span（包含 dozens of attributes）&lt;/li>
&lt;li>不預先 aggregate、查詢時 group by 任意 attribute&lt;/li>
&lt;li>High-cardinality 不是問題、是設計目標&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="instrumentation">Instrumentation&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Honeycomb SDK&lt;/strong>（Beeline）：簡單、Honeycomb-specific、auto-instrumentation 部分&lt;/li>
&lt;li>&lt;strong>OTel SDK + OTLP&lt;/strong>：標準、vendor-neutral、推薦新部署用&lt;/li>
&lt;li>Manual attribute：對 business / domain context attribute 不省略&lt;/li>
&lt;li>Refinery：tail-based sampling proxy&lt;/li>
&lt;/ul>
&lt;h3 id="query-介面">Query 介面&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Visualize：count / count_distinct / heatmap / p50 / p95 / p99&lt;/li>
&lt;li>Group by：任意 attribute（user_id / region / version 等）&lt;/li>
&lt;li>Filter：WHERE clause&lt;/li>
&lt;li>對應 SLO query：&lt;code>heatmap(duration_ms) GROUP BY service.name WHERE http.status_code = 500&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="deep-article">Deep Article&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="high-cardinality-query-bubbleup/">High-Cardinality Query Model 與 BubbleUp&lt;/a>：event-based 資料模型、high-cardinality 查詢設計、BubbleUp 異常偵測、SLO / burn rate、derived columns、dataset 設計與 OTLP ingestion&lt;/li>
&lt;/ul>
&lt;h2 id="migration-playbook">Migration Playbook&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="migrate-from-sentry/">Sentry 遷移到 Honeycomb&lt;/a>：error tracking 轉 event-based observability&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="bubbleup-分析">BubbleUp 分析&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Honeycomb 是 high-cardinality observability SaaS、承擔三個責任：events-based 資料模型（不是 metrics aggregation）、unknown-unknowns 偵錯能力（BubbleUp / Heatmap）、observability-driven SRE 文化代表平台。設計取捨偏向「深度優於廣度」、不追求 Datadog 的 integration 廣度、專注於 high-cardinality + distributed system debugging。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 Honeycomb SDK 或 OTel 送 events 到 Honeycomb</li>
<li>用 BubbleUp 找 outlier 模式（unknown-unknowns）</li>
<li>設計 SLO + burn rate alert</li>
<li>配置 Refinery（tail-based sampling）</li>
<li>評估 Honeycomb vs Datadog 的選用判讀</li>
</ol>
<h2 id="最短路徑5-分鐘把-honeycomb-跑起來">最短路徑：5 分鐘把 Honeycomb 跑起來</h2>





<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. 應用程式加 instrumentation（Honeycomb SDK 或 OTel SDK）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: HONEYCOMB_API_KEY + dataset 設定</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># TODO: 用 Beeline SDK 或 OTel + OTLP exporter</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="c1"># 2. 送 sample events</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: 觀察 trace 出現在 Honeycomb UI</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"># 3. 用 query 介面查詢</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: SELECT count + visualize by service.name</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="events-vs-metrics-心智模型">Events vs metrics 心智模型</h3>
<p>Honeycomb 跟 metrics-aggregation 平台不同。子議題：</p>
<ul>
<li>Event = 一個 trace span（包含 dozens of attributes）</li>
<li>不預先 aggregate、查詢時 group by 任意 attribute</li>
<li>High-cardinality 不是問題、是設計目標</li>
<li>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></li>
</ul>
<h3 id="instrumentation">Instrumentation</h3>
<p>子議題：</p>
<ul>
<li><strong>Honeycomb SDK</strong>（Beeline）：簡單、Honeycomb-specific、auto-instrumentation 部分</li>
<li><strong>OTel SDK + OTLP</strong>：標準、vendor-neutral、推薦新部署用</li>
<li>Manual attribute：對 business / domain context attribute 不省略</li>
<li>Refinery：tail-based sampling proxy</li>
</ul>
<h3 id="query-介面">Query 介面</h3>
<p>子議題：</p>
<ul>
<li>Visualize：count / count_distinct / heatmap / p50 / p95 / p99</li>
<li>Group by：任意 attribute（user_id / region / version 等）</li>
<li>Filter：WHERE clause</li>
<li>對應 SLO query：<code>heatmap(duration_ms) GROUP BY service.name WHERE http.status_code = 500</code></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="high-cardinality-query-bubbleup/">High-Cardinality Query Model 與 BubbleUp</a>：event-based 資料模型、high-cardinality 查詢設計、BubbleUp 異常偵測、SLO / burn rate、derived columns、dataset 設計與 OTLP ingestion</li>
</ul>
<h2 id="migration-playbook">Migration Playbook</h2>
<ul>
<li><a href="migrate-from-sentry/">Sentry 遷移到 Honeycomb</a>：error tracking 轉 event-based observability</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="bubbleup-分析">BubbleUp 分析</h3>
<p>子議題：</p>
<ul>
<li>給定 heatmap 異常區、自動找區隔 outlier 跟 baseline 的 attribute</li>
<li>適合「我看到 latency spike、但不知道哪個維度造成」</li>
<li>Unknown-unknowns 偵錯模式</li>
<li>跟 Datadog APM 的 service map 對照</li>
</ul>
<h3 id="slo-與-burn-rate-alert">SLO 與 burn rate alert</h3>
<p>子議題：</p>
<ul>
<li>SLO 配置（service + indicator + objective + window）</li>
<li>Burn rate calculation：multi-window multi-burn-rate alert</li>
<li>跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a> 對照</li>
<li>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></li>
</ul>
<h3 id="refinerytail-based-sampling">Refinery（tail-based sampling）</h3>
<p>子議題：</p>
<ul>
<li>為什麼需要 tail-based：保留有錯 / 高延遲 trace、丟正常 trace</li>
<li>Refinery 部署模式（gateway in front of Honeycomb）</li>
<li>Sampling rule：error / latency / per-service / dynamic</li>
<li>對應成本：100% ingestion 太貴、tail-based 平衡</li>
</ul>
<h3 id="otlp-integration">OTLP integration</h3>
<p>子議題：</p>
<ul>
<li>Honeycomb 接受 OTLP（gRPC / HTTP）</li>
<li>應用層用 OTel SDK、傳給 Honeycomb 不用改 SDK</li>
<li>Multi-backend 支援：同一份 OTel data 送 Honeycomb + 其他</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></li>
</ul>
<h3 id="結構化-events-設計">結構化 events 設計</h3>
<p>子議題：</p>
<ul>
<li>哪些 attribute 應加（user_id / request_id / business 維度）</li>
<li>哪些 attribute 不該加（PII / secrets）</li>
<li>Wide events 哲學：一個 event 帶 dozens of attributes、不分散到多 metric</li>
<li>對應 PII redaction strategy</li>
</ul>
<h3 id="observability-driven-development">Observability-driven development</h3>
<p>子議題：</p>
<ul>
<li>Charity Majors 提的 SDLC 模式：production debug 是常態</li>
<li>TDD + observability：寫 code 同時思考可觀測性</li>
<li>跟 SRE 文化整合</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="events-沒到-honeycomb">Events 沒到 Honeycomb</h3>
<p>操作原則：先看 SDK 配置（API key + dataset）、再看 network、最後看 Honeycomb status page。</p>
<h3 id="query-timeout">Query timeout</h3>
<p>操作原則：query window 過大或 attribute cardinality 過高造成 backend slow。判讀：縮 time window、簡化 group by。</p>
<h3 id="sampling-過頭-vs-不足">Sampling 過頭 vs 不足</h3>
<p>操作原則：debug 時找不到 trace（sampling 過頭）vs cost 爆（sampling 不足）。Refinery 提供 dynamic sampling 解決靜態 rate 的不足。</p>
<h3 id="burn-rate-alert-noise">Burn rate alert noise</h3>
<p>操作原則：multi-window 設計避免「短暫 spike 觸發 alert」、低 burn rate window 給長期趨勢。</p>
<h3 id="跟其他-backend-dual-ship-不一致">跟其他 backend dual ship 不一致</h3>
<p>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a>。判讀：兩個 backend 數據不對齊、看 SDK 是否 dual export、attribute mapping 是否一致。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>廣度大、要 600+ integrations</td>
          <td><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></td>
      </tr>
      <tr>
          <td>預算敏感</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（OSS）</td>
      </tr>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
      </tr>
      <tr>
          <td>Logs full-text</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
      </tr>
      <tr>
          <td>Error tracking 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>Cloud-native (AWS / GCP)</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Ops</a></td>
      </tr>
      <tr>
          <td>Self-hosted</td>
          <td>OSS observability（Honeycomb 是 SaaS only）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Honeycomb SDK 完整 API</li>
<li>BubbleUp 內部演算法</li>
<li>Refinery 詳細配置</li>
<li>Honeycomb pricing 詳細</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>High-cardinality debug pattern</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel signal drift</a></td>
          <td>（反例）Refinery / dual ship 對齊驗證</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Honeycomb 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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></td>
          <td>從 Datadog APM 遷出時 Honeycomb 是 events 替代</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></td>
          <td>動態叢集下 wide events 補 metrics 維度不足</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>Honeycomb 適合中大型 + observability-driven team</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Honeycomb 案例</strong>：Charity Majors 的 production talks、Honeycomb customer engineering blog、Refinery scale-up case。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<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></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability 模組</a>（SLO / burn rate）、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>Locust</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/locust/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/locust/</guid><description>&lt;p>Locust 是 Python-based load test 工具、承擔三個責任：Python class-based test 設計（user behavior 表達力強）、distributed mode（master / worker 內建）、Web UI 即時觀察。設計取捨偏向「Python DX + 高度自訂邏輯 + 任何 Python lib 都可用」、適合 Python 團隊與需要極高自訂邏輯的場景。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 Locust user class + task&lt;/li>
&lt;li>跑 standalone + distributed mode&lt;/li>
&lt;li>自訂 client（非 HTTP、如 gRPC / WebSocket）&lt;/li>
&lt;li>設計 task weight + on_start / on_stop hook&lt;/li>
&lt;li>評估 Locust vs k6 / Gatling 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-locust-跑起來">最短路徑：5 分鐘把 Locust 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: pip install locust&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="c1"># 2. 寫 locustfile.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: class User(HttpUser): wait_time = ..., @task def hello(self): ...&lt;/span>
&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="c1"># 3. 跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: locust -f locustfile.py --host=http://target&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 瀏覽器 http://localhost:8089 操作&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="user-class--task">User class + task&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>HttpUser / FastHttpUser（FastHttpUser 用 geventhttpclient、效能高）&lt;/li>
&lt;li>@task decorator + weight&lt;/li>
&lt;li>on_start / on_stop（per-VU setup / teardown）&lt;/li>
&lt;li>對應 Python class inheritance&lt;/li>
&lt;/ul>
&lt;h3 id="distributed-mode">Distributed mode&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>master：協調 + 收集 metric&lt;/li>
&lt;li>worker：實際發送 request&lt;/li>
&lt;li>&lt;code>locust --master&lt;/code> / &lt;code>locust --worker --master-host=...&lt;/code>&lt;/li>
&lt;li>多 worker 突破 Python GIL 限制&lt;/li>
&lt;/ul>
&lt;h3 id="web-ui-vs-headless">Web UI vs headless&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Web UI（dev / interactive）&lt;/li>
&lt;li>Headless（&lt;code>--headless --users N --spawn-rate N --run-time T&lt;/code>）&lt;/li>
&lt;li>對應 CI 整合：CSV report&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="自訂-client非-http">自訂 client（非 HTTP）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>任何 Python lib 都可包成 user&lt;/li>
&lt;li>gRPC / WebSocket / database / queue 都行&lt;/li>
&lt;li>request event 手動 fire&lt;/li>
&lt;/ul>
&lt;h3 id="custom-request">Custom request&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>self.client.get/post（HTTP）&lt;/li>
&lt;li>自訂 event emission&lt;/li>
&lt;li>Custom statistics&lt;/li>
&lt;/ul>
&lt;h3 id="locust-plugins-生態">locust-plugins 生態&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Locust 是 Python-based load test 工具、承擔三個責任：Python class-based test 設計（user behavior 表達力強）、distributed mode（master / worker 內建）、Web UI 即時觀察。設計取捨偏向「Python DX + 高度自訂邏輯 + 任何 Python lib 都可用」、適合 Python 團隊與需要極高自訂邏輯的場景。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 Locust user class + task</li>
<li>跑 standalone + distributed mode</li>
<li>自訂 client（非 HTTP、如 gRPC / WebSocket）</li>
<li>設計 task weight + on_start / on_stop hook</li>
<li>評估 Locust vs k6 / Gatling 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-locust-跑起來">最短路徑：5 分鐘把 Locust 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: pip install locust</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="c1"># 2. 寫 locustfile.py</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: class User(HttpUser): wait_time = ..., @task def hello(self): ...</span>
</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="c1"># 3. 跑</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: locust -f locustfile.py --host=http://target</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: 瀏覽器 http://localhost:8089 操作</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="user-class--task">User class + task</h3>
<p>子議題：</p>
<ul>
<li>HttpUser / FastHttpUser（FastHttpUser 用 geventhttpclient、效能高）</li>
<li>@task decorator + weight</li>
<li>on_start / on_stop（per-VU setup / teardown）</li>
<li>對應 Python class inheritance</li>
</ul>
<h3 id="distributed-mode">Distributed mode</h3>
<p>子議題：</p>
<ul>
<li>master：協調 + 收集 metric</li>
<li>worker：實際發送 request</li>
<li><code>locust --master</code> / <code>locust --worker --master-host=...</code></li>
<li>多 worker 突破 Python GIL 限制</li>
</ul>
<h3 id="web-ui-vs-headless">Web UI vs headless</h3>
<p>子議題：</p>
<ul>
<li>Web UI（dev / interactive）</li>
<li>Headless（<code>--headless --users N --spawn-rate N --run-time T</code>）</li>
<li>對應 CI 整合：CSV report</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="自訂-client非-http">自訂 client（非 HTTP）</h3>
<p>子議題：</p>
<ul>
<li>任何 Python lib 都可包成 user</li>
<li>gRPC / WebSocket / database / queue 都行</li>
<li>request event 手動 fire</li>
</ul>
<h3 id="custom-request">Custom request</h3>
<p>子議題：</p>
<ul>
<li>self.client.get/post（HTTP）</li>
<li>自訂 event emission</li>
<li>Custom statistics</li>
</ul>
<h3 id="locust-plugins-生態">locust-plugins 生態</h3>
<p>子議題：</p>
<ul>
<li>locust-plugins：第三方 plugin（CSV report enhanced / Postgres / Kafka / etc）</li>
<li>Custom shape（dynamic load profile）</li>
<li>TaskSet / SequentialTaskSet</li>
</ul>
<h3 id="ci-integration">CI integration</h3>
<p>子議題：</p>
<ul>
<li>Headless mode + exit code</li>
<li>CSV / JSON report</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
</ul>
<h3 id="distributed-scaling">Distributed scaling</h3>
<p>子議題：</p>
<ul>
<li>Kubernetes 部署</li>
<li>多 region load source</li>
<li>Result aggregation</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="high-vu-跑不上去">High VU 跑不上去</h3>
<p>操作原則：Python GIL + 單 worker 限制、用 distributed mode。判讀：CPU / network bottleneck？</p>
<h3 id="worker-disconnect">Worker disconnect</h3>
<p>操作原則：master / worker network 不通、heartbeat timeout。判讀：log + master UI。</p>
<h3 id="custom-protocol-報告不正確">Custom protocol 報告不正確</h3>
<p>操作原則：手動 event fire 缺 / metric name 不對。</p>
<h3 id="memory-leak">Memory leak</h3>
<p>操作原則：long run test、user state accumulate。判讀：on_stop cleanup。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>編譯後分發 / 高 VU 單機</td>
          <td><a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a></td>
      </tr>
      <tr>
          <td>JVM 生態</td>
          <td><a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></td>
      </tr>
      <tr>
          <td>GUI / 老牌</td>
          <td><a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></td>
      </tr>
      <tr>
          <td>Cloud managed</td>
          <td>k6 Cloud / BlazeMeter / Locust 自管 K8s</td>
      </tr>
      <tr>
          <td>Capacity planning</td>
          <td><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity 模組</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Python 語言基礎</li>
<li>gevent / asyncio 內部</li>
<li>locust-plugins 完整列表</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn：Capacity 與 On-call 分層</a></td>
          <td>automated load testing 對齊 headroom 預測（Python 場景）</td>
      </tr>
  </tbody>
</table>
<p><strong>Case 庫稀薄</strong>：本 cases/ 目錄目前沒有以 Locust 為主軸的案例。可參考候選方向：</p>
<ul>
<li><strong>待補 Locust customer case</strong>：Python-heavy 團隊 load test 採用案例、distributed Locust 大規模部署案例</li>
<li><strong>候選 case</strong>：Pinterest（ML serving / 推薦系統壓測場景）、Spotify（squad-based 各團隊自管壓測）— 若未來收錄需先在 cases/ 補正文，本欄再寫實際 link</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a>、<a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></li>
<li>下游能力：<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 performance capacity</a></li>
</ul>
]]></content:encoded></item><item><title>Roblox</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/</guid><description>&lt;p>Roblox 2021 的 73 小時事故是 Consul 流量模式 + long-tail recovery 的教學標竿。事故 post-mortem 詳細揭露根因發現過程、適合作為「為何根因難找」「為何 recovery 比預期慢」的敘事範本。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Consul 流量模式：streaming + 大量 watch 的非預期行為&lt;/li>
&lt;li>根因發現延遲：72 小時內為何無法定位 streaming 是兇手&lt;/li>
&lt;li>Long-tail recovery：服務恢復後為何效能未恢復、cache cold start 影響&lt;/li>
&lt;li>廠商協作：HashiCorp 介入時機、第三方協助的 IR 流程&lt;/li>
&lt;li>Postmortem 公開度：Roblox 罕見的詳細工程敘事&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2021&lt;/td>
 &lt;td>73 小時 outage&lt;/td>
 &lt;td>根因難尋、long-tail recovery、廠商協作&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Roblox 這個案例在講的是長時間事故如何把基礎設施依賴顯性化。讀者先看懂控制面、配置與服務恢復的順序，再把 73 小時這類事件當成 prolonged recovery 的範例。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當核心依賴出現問題時，恢復不只是在某台機器上按下重啟，而是要讓整個服務依賴鏈按順序回來。當事件持續多天時，修復與驗證的節奏要穩定，否則使用者面恢復會反覆抖動。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否說明哪個基礎設施層先恢復&lt;/li>
&lt;li>能否把長尾恢復拆成可驗證的階段&lt;/li>
&lt;li>能否在控制面回穩前避免過早開流量&lt;/li>
&lt;li>能否把 prolonged recovery 的每一步對外說清楚&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Roblox 和 Discord、Heroku 一起讀時，最能看出長連線與多租戶基礎設施的恢復難度。它也能對照 AWS S3，因為兩者都在說明基礎層恢復順序一旦錯了，後面的使用者體感就會反覆抖動。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>73 小時 outage 是長尾恢復與根因難尋的代表案例。&lt;/li>
&lt;li>Return to Service 文件則提供了從事故到結構性改善的完整敘事。&lt;/li>
&lt;li>Consul 的流量模式揭露了意外的 session 壓力。&lt;/li>
&lt;li>廠商協作是 prolonged recovery 的重要組件。&lt;/li>
&lt;li>streaming / watch traffic 讓非預期的控制面壓力浮出來。&lt;/li>
&lt;li>infrastructure efficiency 改善是事故之後的結構性回應。&lt;/li>
&lt;li>streaming / watch traffic 讓非預期的控制面壓力浮出來。&lt;/li>
&lt;li>infrastructure efficiency 改善是事故之後的結構性回應。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://corp.roblox.com/newsroom/2021/10/update-recent-service-outage/">An Update on Our Outage&lt;/a>：Roblox 73 小時 outage 的初始對外說明。&lt;/li>
&lt;li>&lt;a href="https://corp.roblox.com/fr/salledepresse/2022/01/roblox-return-to-service-10-28-10-31-2021">Roblox Return to Service&lt;/a>：完整 return-to-service 與技術復盤。&lt;/li>
&lt;li>&lt;a href="https://corp.roblox.com/de/newsroom/2023/12/making-robloxs-infrastructure-efficient-resilient">How We’re Making Roblox’s Infrastructure More Efficient and Resilient&lt;/a>：後續的結構性改善與 cell 化方向。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Roblox 2021 的 73 小時事故是 Consul 流量模式 + long-tail recovery 的教學標竿。事故 post-mortem 詳細揭露根因發現過程、適合作為「為何根因難找」「為何 recovery 比預期慢」的敘事範本。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Consul 流量模式：streaming + 大量 watch 的非預期行為</li>
<li>根因發現延遲：72 小時內為何無法定位 streaming 是兇手</li>
<li>Long-tail recovery：服務恢復後為何效能未恢復、cache cold start 影響</li>
<li>廠商協作：HashiCorp 介入時機、第三方協助的 IR 流程</li>
<li>Postmortem 公開度：Roblox 罕見的詳細工程敘事</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2021</td>
          <td>73 小時 outage</td>
          <td>根因難尋、long-tail recovery、廠商協作</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>Roblox 這個案例在講的是長時間事故如何把基礎設施依賴顯性化。讀者先看懂控制面、配置與服務恢復的順序，再把 73 小時這類事件當成 prolonged recovery 的範例。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當核心依賴出現問題時，恢復不只是在某台機器上按下重啟，而是要讓整個服務依賴鏈按順序回來。當事件持續多天時，修復與驗證的節奏要穩定，否則使用者面恢復會反覆抖動。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否說明哪個基礎設施層先恢復</li>
<li>能否把長尾恢復拆成可驗證的階段</li>
<li>能否在控制面回穩前避免過早開流量</li>
<li>能否把 prolonged recovery 的每一步對外說清楚</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Roblox 和 Discord、Heroku 一起讀時，最能看出長連線與多租戶基礎設施的恢復難度。它也能對照 AWS S3，因為兩者都在說明基礎層恢復順序一旦錯了，後面的使用者體感就會反覆抖動。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>73 小時 outage 是長尾恢復與根因難尋的代表案例。</li>
<li>Return to Service 文件則提供了從事故到結構性改善的完整敘事。</li>
<li>Consul 的流量模式揭露了意外的 session 壓力。</li>
<li>廠商協作是 prolonged recovery 的重要組件。</li>
<li>streaming / watch traffic 讓非預期的控制面壓力浮出來。</li>
<li>infrastructure efficiency 改善是事故之後的結構性回應。</li>
<li>streaming / watch traffic 讓非預期的控制面壓力浮出來。</li>
<li>infrastructure efficiency 改善是事故之後的結構性回應。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://corp.roblox.com/newsroom/2021/10/update-recent-service-outage/">An Update on Our Outage</a>：Roblox 73 小時 outage 的初始對外說明。</li>
<li><a href="https://corp.roblox.com/fr/salledepresse/2022/01/roblox-return-to-service-10-28-10-31-2021">Roblox Return to Service</a>：完整 return-to-service 與技術復盤。</li>
<li><a href="https://corp.roblox.com/de/newsroom/2023/12/making-robloxs-infrastructure-efficient-resilient">How We’re Making Roblox’s Infrastructure More Efficient and Resilient</a>：後續的結構性改善與 cell 化方向。</li>
</ul>
]]></content:encoded></item><item><title>Rootly</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/rootly/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/rootly/</guid><description>&lt;p>Rootly 是 IR 平台、承擔三個責任：no-code workflow builder（拖拉式自動化）、AI 輔助 retrospective + timeline 整理、Slack / Teams 雙平台整合 + integration 數量最廣（200+）。產品迭代快、跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant&lt;/a> 三家構成 modern IR 平台主要選項。2023+ 加入 Rootly AI 模組做 incident enrichment 與 retrospective auto-draft、把 IR 平台從 &lt;em>workflow 自動化&lt;/em> 推到 &lt;em>AI-assisted investigation&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Rootly 的核心定位是 &lt;em>Slack-native IR platform + no-code automation engine&lt;/em>、目標客戶是「想最大化降低 incident response toil」的 AI-first / engineering-led 組織。產品主軸：&lt;em>no-code workflow builder&lt;/em>（IFTTT-style condition / action 鏈、不需工程 deploy）+ &lt;em>Rootly AI&lt;/em>（incident summarization / enrichment / retrospective auto-draft）+ &lt;em>Slack / Teams 雙平台對等支援&lt;/em>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> 比、PagerDuty 是 alerting-first（on-call schedule + escalation 為核心）、Rootly 是 IR-process-first（incident workflow + retro 為核心）、兩家常一起用（PagerDuty 負責 page、Rootly 接 declare 後的 process）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> 比、incident.io 走 &lt;em>opinionated minimal&lt;/em>（流程固定、學習快）、Rootly 走 &lt;em>configurable maximal&lt;/em>（workflow 可深度客製、學習曲線稍陡）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant&lt;/a> 比、FireHydrant 在 service catalog / runbook 結構更剛、Rootly 在 AI + integration 廣度更領先。&lt;/p>
&lt;p>關鍵張力：&lt;em>no-code 客製深度&lt;/em> ↔ &lt;em>配置複雜度&lt;/em> 是 Rootly 客戶最大的 trade-off — workflow 可以做得很深，但配多了會出現 &lt;em>workflow loop / 通知爆量 / AI summary 失準&lt;/em>，需要有人定期 review workflow inventory。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>用 no-code builder 設計 incident workflow（trigger / condition / action）&lt;/li>
&lt;li>配置 severity matrix + role assignment&lt;/li>
&lt;li>用 Rootly AI 輔助 timeline + retrospective、了解 AI 失準的邊界&lt;/li>
&lt;li>整合 200+ tool（觀測 / cloud / collaboration / ticket / paging）&lt;/li>
&lt;li>評估 Rootly vs incident.io / FireHydrant / PagerDuty 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Rootly deployment 是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>Rootly 是 IR 平台、承擔三個責任：no-code workflow builder（拖拉式自動化）、AI 輔助 retrospective + timeline 整理、Slack / Teams 雙平台整合 + integration 數量最廣（200+）。產品迭代快、跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> / <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> 三家構成 modern IR 平台主要選項。2023+ 加入 Rootly AI 模組做 incident enrichment 與 retrospective auto-draft、把 IR 平台從 <em>workflow 自動化</em> 推到 <em>AI-assisted investigation</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>Rootly 的核心定位是 <em>Slack-native IR platform + no-code automation engine</em>、目標客戶是「想最大化降低 incident response toil」的 AI-first / engineering-led 組織。產品主軸：<em>no-code workflow builder</em>（IFTTT-style condition / action 鏈、不需工程 deploy）+ <em>Rootly AI</em>（incident summarization / enrichment / retrospective auto-draft）+ <em>Slack / Teams 雙平台對等支援</em>。</p>
<p>跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> 比、PagerDuty 是 alerting-first（on-call schedule + escalation 為核心）、Rootly 是 IR-process-first（incident workflow + retro 為核心）、兩家常一起用（PagerDuty 負責 page、Rootly 接 declare 後的 process）。跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> 比、incident.io 走 <em>opinionated minimal</em>（流程固定、學習快）、Rootly 走 <em>configurable maximal</em>（workflow 可深度客製、學習曲線稍陡）。跟 <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> 比、FireHydrant 在 service catalog / runbook 結構更剛、Rootly 在 AI + integration 廣度更領先。</p>
<p>關鍵張力：<em>no-code 客製深度</em> ↔ <em>配置複雜度</em> 是 Rootly 客戶最大的 trade-off — workflow 可以做得很深，但配多了會出現 <em>workflow loop / 通知爆量 / AI summary 失準</em>，需要有人定期 review workflow inventory。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>用 no-code builder 設計 incident workflow（trigger / condition / action）</li>
<li>配置 severity matrix + role assignment</li>
<li>用 Rootly AI 輔助 timeline + retrospective、了解 AI 失準的邊界</li>
<li>整合 200+ tool（觀測 / cloud / collaboration / ticket / paging）</li>
<li>評估 Rootly vs incident.io / FireHydrant / PagerDuty 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Rootly deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Slack workflow 入口統一</strong>：<code>/rootly declare</code> 是否唯一 declare 入口、severity / service / role 是否在 declare 時就 bind、Slack channel naming convention（<code>inc-YYYY-MM-DD-slug</code>）跟 retention 是否設定</li>
<li><strong>No-code automation 治理</strong>：workflow 數量 / owner / 上次 review 時間是否有 inventory、有沒有 staging tenant 跑新 workflow、production workflow change 是否走 PR-like review</li>
<li><strong>AI integration 邊界</strong>：Rootly AI 用在哪些環節（incident summary / timeline enrichment / retrospective draft）、AI 輸出是否標記為 draft 而非 finalized、AI hallucination 的 human review gate 是否定義</li>
<li><strong>SSO + audit + integration health</strong>：SSO（Okta / Azure AD）+ audit log（誰改 workflow / 誰 close incident）是否開、Integration token 是否定期 rotate、Jira / Linear / GitHub PR / PagerDuty / Opsgenie 對接是否雙向同步</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a> 邊界的待補項目。</p>
<h2 id="最短路徑">最短路徑</h2>





<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. Slack / Teams install Rootly app</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. /rootly declare 建 test incident</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. 拖拉 workflow（severity → action）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. Close + AI retrospective</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="no-code-workflow-builder">No-code workflow builder</h3>
<p>子議題：</p>
<ul>
<li>Trigger（severity / status / time）→ Action（page / message / ticket）</li>
<li>Branch / condition / parallel</li>
<li>Custom field bind</li>
</ul>
<p><strong>IFTTT-style 邏輯</strong>：workflow 是 <em>trigger → condition → action</em> 的 DAG、可以 branch / parallel / loop（loop 要小心、見排錯）。典型 production workflow：「severity SEV1 declared → page on-call via PagerDuty + create Jira ticket + post status page draft + invite security lead to Slack channel」。複雜度上限是「能 express 在 UI 拖拉上」、超過這個複雜度應該寫 webhook 接外部 orchestrator。</p>
<h3 id="ai-retrospective--slackteams-workflow">AI retrospective + Slack/Teams workflow</h3>
<p>子議題：</p>
<ul>
<li>自動 timeline from Slack messages</li>
<li>AI summary（what happened / contributing factor）</li>
<li>同 incident.io / FireHydrant Slack workflow</li>
<li>Teams 平等支援</li>
<li>Mobile app</li>
</ul>
<p><strong>Rootly AI 的能力邊界</strong>：AI 從 Slack channel 訊息抽 timeline、產生 <em>contributing factor</em> draft、列 <em>action item</em> candidate。產出是 <em>draft、不是 finalized retrospective</em> — IR lead 應該逐項驗證再 publish、AI hallucination 在 contributing factor / blame attribution 段最常出現（見排錯段）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Rootly</th>
          <th>incident.io</th>
          <th>FireHydrant</th>
          <th>PagerDuty</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心定位</td>
          <td>No-code workflow + AI investigation</td>
          <td>Opinionated Slack-native IR</td>
          <td>Service catalog + runbook 結構</td>
          <td>Alerting + on-call schedule</td>
      </tr>
      <tr>
          <td>客製化深度</td>
          <td>高 — workflow builder + custom field</td>
          <td>中 — 流程相對固定</td>
          <td>中高 — runbook + catalog 模型清晰</td>
          <td>中 — escalation 配置強、流程較輕</td>
      </tr>
      <tr>
          <td>AI 能力</td>
          <td>Rootly AI（summary / enrich / retro）</td>
          <td>AI 摘要（較新、範圍較窄）</td>
          <td>較少強調 AI</td>
          <td>AIOps（alert grouping）</td>
      </tr>
      <tr>
          <td>平台支援</td>
          <td>Slack + Teams 對等</td>
          <td>Slack-first（Teams 較弱）</td>
          <td>Slack + Teams</td>
          <td>Slack / Teams / Mobile / Email</td>
      </tr>
      <tr>
          <td>Integration 廣度</td>
          <td>200+（業界最廣）</td>
          <td>中（Slack ecosystem 為主）</td>
          <td>中高</td>
          <td>最廣（paging ecosystem）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中陡 — 配置選項多</td>
          <td>緩 — 流程少</td>
          <td>中 — service model 要先想清楚</td>
          <td>中 — escalation policy 要先設計</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AI-first / 想自動化 toil / Slack-heavy</td>
          <td>小到中型、想快上手 + 流程一致</td>
          <td>中大型、service ownership 清楚</td>
          <td>任何需要強 paging 的團隊</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — workflow / custom field 量會綁</td>
          <td>低 — 流程相對標準</td>
          <td>中 — service catalog 綁定深</td>
          <td>高 — schedule + integration 量大</td>
      </tr>
  </tbody>
</table>
<p>選 Rootly 的核心訴求：<em>Slack-native IR + 想用 no-code + AI 把 incident process toil 自動化最大化</em>、且能投入時間維護 workflow inventory（避免 workflow sprawl）。需要重 paging 的團隊通常 Rootly + PagerDuty 並用（Rootly 不取代 PagerDuty 的 schedule + escalation）。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="rootly-ai-深入">Rootly AI 深入</h3>
<p>子議題：incident summary（給 stakeholder broadcast 用）、enrichment（自動補 service owner / recent deploy / related incident）、retrospective auto-draft（timeline + contributing factor + action item）。AI 輸出是 <em>draft</em>、需要 human review gate 才 publish。對 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a> 的影響是「快、但要驗」、不能把 AI draft 直接當成 source of truth。</p>
<h3 id="no-code-workflow-進階">No-code workflow 進階</h3>
<p>子議題：condition expression（field / value / operator）、parallel branch、wait / delay、custom webhook action 接外部 orchestrator。複雜 workflow 應該 <em>先在 staging tenant 跑</em>、production workflow change 走 review。Workflow loop（A workflow 觸發 B、B 觸發 A）會在 misconfig 時出現、見排錯段。</p>
<h3 id="ticket--pr--paging-integration">Ticket / PR / paging integration</h3>
<p>子議題：Jira / Linear 雙向同步（incident close 同步 ticket、ticket update 帶回 Slack）、GitHub PR 自動連 incident（commit message 含 incident ID）、PagerDuty / Opsgenie alerting layer 對接（page 從 PagerDuty 來、process 在 Rootly 跑）。Integration token 失效是常見 silent failure、需要 monitoring。</p>
<h3 id="integration-廣度">Integration 廣度</h3>
<p>子議題：觀測（Datadog / Grafana / New Relic / Honeycomb）/ Cloud（AWS / GCP / Azure）/ Collaboration（Slack / Teams / Zoom）/ Ticket（Jira / Linear / GitHub）/ Status page</p>
<h3 id="service-catalog--custom-field">Service catalog + Custom field</h3>
<p>子議題：service / team / customer metadata、custom field 帶業務 context、workflow trigger by field</p>
<h3 id="on-call-模組">On-call 模組</h3>
<p>子議題：Rootly OnCall（schedule + escalation）、跟 IR workflow 同 app</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Workflow 行為不符</strong>：trigger / condition 邏輯錯、看 workflow run log</li>
<li><strong>AI summary / retrospective 失準</strong>：Slack noise 多、AI 對 contributing factor / blame attribution hallucinate — 手動補 timeline、AI 輸出標記為 draft、由 IR lead 逐項驗證才 publish</li>
<li><strong>Workflow loop / 通知爆量</strong>：A workflow 觸發 B、B 又觸發 A、Slack 訊息或 ticket 暴衝 — 在 staging tenant pre-test、production workflow change 走 review、加 rate limit / loop detection</li>
<li><strong>Slack notification overload</strong>：每個 severity 都 broadcast 全公司 channel — 設 severity threshold、SEV3 以下走 team channel、SEV1/2 才 broadcast</li>
<li><strong>Integration token 失效</strong>：rotate / OAuth re-auth、加 integration health monitoring（token expiry alert）</li>
<li><strong>Slack channel 亂</strong>：naming convention（<code>inc-YYYY-MM-DD-slug</code>）/ retention 沒設、舊 incident channel 累積成千</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack-only / 簡潔</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>Microsoft Teams</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>Paging-first</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
      </tr>
      <tr>
          <td>Learning-focused</td>
          <td><a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a></td>
      </tr>
      <tr>
          <td>自建 Slack workflow</td>
          <td>Slack + GitHub Issues / Linear</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AI model / training detail / Pricing / 200+ integration 個別 setup</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>Rootly 主打 Slack-native + AI-assisted IR</strong>：本案例庫尚無直接揭露 Rootly 使用細節的事故；可參照的閱讀脈絡是「Slack-centric 協作 + 自動化 retro + AI-first 組織想 minimize IR toil」的服務事故。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack cases</a></td>
          <td>Slack-native IR 平台在通訊平台自身事故下的回退</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/" data-link-title="Reddit" data-link-desc="Reddit Pi Day 2023 k8s 升級事故">Reddit cases</a></td>
          <td>mid-size 平台升級事故的 retro 結構（對照素材）</td>
      </tr>
  </tbody>
</table>
<p>待補 candidate：NVIDIA / Figma / Canva 等 Rootly 公開 customer story。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a>、<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a>、<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
]]></content:encoded></item><item><title>模組六：可靠性驗證流程</title><link>https://tarrragon.github.io/blog/backend/06-reliability/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/</guid><description>&lt;p>可靠性驗證模組的核心目標是說明測試如何從單一函式擴展到整個後端系統。語言教材會處理 unit test、table-driven / parameterized test、race / async test 與 integration test；本模組負責 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline&lt;/a>、壓力測試、fuzz campaign、chaos testing、SLO 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate&lt;/a>。&lt;/p>
&lt;p>本輪規劃採問題驅動方法、用 SRE 領域 first-class 詞彙（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">SLO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget&lt;/a> / Failure Mode / Chaos Hypothesis），把驗證議題拆成問題節點，蒐集公開 SRE 實踐作為服務級案例庫，再把控制面交接到可觀測性、部署平台與事故處理模組落地。&lt;/p>
&lt;h2 id="驗證角色">驗證角色&lt;/h2>
&lt;p>可靠性驗證的角色是把「系統會不會在真實壓力下失敗」變成可預演的工程問題。這一層不負責寫測試語法，也不負責定義服務功能，而是負責定義哪些失效值得被主動打破、哪一種訊號可以證明風險存在、哪一種門檻可以阻止變更往下流。&lt;/p>
&lt;p>當讀者把驗證看成流程，就會自然分出三個層次。第一層是訊號，先知道要看什麼。第二層是演練，先知道要怎麼打。第三層是放行，先知道什麼情況需要暫停或退回。這三層分別對應可觀測性、可靠性驗證與交付平台的責任。&lt;/p>
&lt;h2 id="問題節點">問題節點&lt;/h2>
&lt;p>問題節點先描述失效風險，再描述驗證手段。這樣寫的好處是，讀者能先理解「為什麼要驗證」，再看到「怎麼驗證」，讓工具名回到解題手段的位置。&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>CI pipeline&lt;/td>
 &lt;td>測試是否真的攔住回歸、artifact 是否可重播&lt;/td>
 &lt;td>flaky rate、test duration、build queue&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load test&lt;/td>
 &lt;td>真實負載是否被模型覆蓋、瓶頸是否被提早暴露&lt;/td>
 &lt;td>latency curve、throughput ceiling、error rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fuzz campaign&lt;/td>
 &lt;td>邊界輸入是否能觸發 crash、corpus 是否持續擴充&lt;/td>
 &lt;td>crash reproduction、coverage delta&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Chaos testing&lt;/td>
 &lt;td>依賴失效後系統是否仍能維持服務、回復路徑是否可執行&lt;/td>
 &lt;td>steady state drift、rollback success rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLO / Error Budget&lt;/td>
 &lt;td>可靠性是否已經被消耗、變更是否還能繼續推進&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> remaining&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的責任是提供路由。每一列都要回到服務案例庫，從公開實踐找出真實世界的樣本，把問題節點和失效模式綁在一起。&lt;/p>
&lt;h2 id="案例庫讀法">案例庫讀法&lt;/h2>
&lt;p>案例庫的責任是提供幾種反覆出現的失效與驗證模式。Google、Netflix、Amazon、Stripe 與 Shopify 這五個 T1 案例，分別對應量化門檻、主動故障注入、隔離邊界、交易正確性與峰值準備。&lt;/p>
&lt;p>當讀者遇到某個驗證節點卡住時，可以先問三個問題。第一，現在缺的是訊號還是門檻。第二，失敗是在單一服務內還是在依賴鏈上。第三，這種風險更像回歸、容量、變更還是恢復問題。這三個問題會把讀者導向不同案例頁，也會把讀者導回可觀測性、部署平台或事故處理的交接節點。&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>Google&lt;/td>
 &lt;td>把可靠性制度化&lt;/td>
 &lt;td>SLO、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Netflix&lt;/td>
 &lt;td>把故障注入制度化&lt;/td>
 &lt;td>chaos、steady state、FIT&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon&lt;/td>
 &lt;td>把隔離與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 制度化&lt;/td>
 &lt;td>cell、shard、static stability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stripe&lt;/td>
 &lt;td>把交易正確性制度化&lt;/td>
 &lt;td>idempotency、canary、migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify&lt;/td>
 &lt;td>把峰值準備與演練制度化&lt;/td>
 &lt;td>capacity planning、resiliency matrix&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作工具見 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 CI（GitHub Actions / CircleCI）、Load test（k6 / Gatling / JMeter / Locust）、Chaos（Chaos Mesh / LitmusChaos / Gremlin / Toxiproxy）、SLO（Nobl9 / Sloth）共 12 個 vendor 骨架。跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">cases/&lt;/a> 是不同維度（cases 是教學案例來源、vendors 是實作工具）。&lt;/p></description><content:encoded><![CDATA[<p>可靠性驗證模組的核心目標是說明測試如何從單一函式擴展到整個後端系統。語言教材會處理 unit test、table-driven / parameterized test、race / async test 與 integration test；本模組負責 <a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a>、壓力測試、fuzz campaign、chaos testing、SLO 與 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a>。</p>
<p>本輪規劃採問題驅動方法、用 SRE 領域 first-class 詞彙（<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI</a> / <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">SLO</a> / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget</a> / Failure Mode / Chaos Hypothesis），把驗證議題拆成問題節點，蒐集公開 SRE 實踐作為服務級案例庫，再把控制面交接到可觀測性、部署平台與事故處理模組落地。</p>
<h2 id="驗證角色">驗證角色</h2>
<p>可靠性驗證的角色是把「系統會不會在真實壓力下失敗」變成可預演的工程問題。這一層不負責寫測試語法，也不負責定義服務功能，而是負責定義哪些失效值得被主動打破、哪一種訊號可以證明風險存在、哪一種門檻可以阻止變更往下流。</p>
<p>當讀者把驗證看成流程，就會自然分出三個層次。第一層是訊號，先知道要看什麼。第二層是演練，先知道要怎麼打。第三層是放行，先知道什麼情況需要暫停或退回。這三層分別對應可觀測性、可靠性驗證與交付平台的責任。</p>
<h2 id="問題節點">問題節點</h2>
<p>問題節點先描述失效風險，再描述驗證手段。這樣寫的好處是，讀者能先理解「為什麼要驗證」，再看到「怎麼驗證」，讓工具名回到解題手段的位置。</p>
<table>
  <thead>
      <tr>
          <th>節點</th>
          <th>驗證問題</th>
          <th>常見訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI pipeline</td>
          <td>測試是否真的攔住回歸、artifact 是否可重播</td>
          <td>flaky rate、test duration、build queue</td>
      </tr>
      <tr>
          <td>Load test</td>
          <td>真實負載是否被模型覆蓋、瓶頸是否被提早暴露</td>
          <td>latency curve、throughput ceiling、error rate</td>
      </tr>
      <tr>
          <td>Fuzz campaign</td>
          <td>邊界輸入是否能觸發 crash、corpus 是否持續擴充</td>
          <td>crash reproduction、coverage delta</td>
      </tr>
      <tr>
          <td>Chaos testing</td>
          <td>依賴失效後系統是否仍能維持服務、回復路徑是否可執行</td>
          <td>steady state drift、rollback success rate</td>
      </tr>
      <tr>
          <td>SLO / Error Budget</td>
          <td>可靠性是否已經被消耗、變更是否還能繼續推進</td>
          <td><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> remaining</td>
      </tr>
  </tbody>
</table>
<p>這張表的責任是提供路由。每一列都要回到服務案例庫，從公開實踐找出真實世界的樣本，把問題節點和失效模式綁在一起。</p>
<h2 id="案例庫讀法">案例庫讀法</h2>
<p>案例庫的責任是提供幾種反覆出現的失效與驗證模式。Google、Netflix、Amazon、Stripe 與 Shopify 這五個 T1 案例，分別對應量化門檻、主動故障注入、隔離邊界、交易正確性與峰值準備。</p>
<p>當讀者遇到某個驗證節點卡住時，可以先問三個問題。第一，現在缺的是訊號還是門檻。第二，失敗是在單一服務內還是在依賴鏈上。第三，這種風險更像回歸、容量、變更還是恢復問題。這三個問題會把讀者導向不同案例頁，也會把讀者導回可觀測性、部署平台或事故處理的交接節點。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主要用途</th>
          <th>常見回扣節點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Google</td>
          <td>把可靠性制度化</td>
          <td>SLO、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>、<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a></td>
      </tr>
      <tr>
          <td>Netflix</td>
          <td>把故障注入制度化</td>
          <td>chaos、steady state、FIT</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td>把隔離與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 制度化</td>
          <td>cell、shard、static stability</td>
      </tr>
      <tr>
          <td>Stripe</td>
          <td>把交易正確性制度化</td>
          <td>idempotency、canary、migration</td>
      </tr>
      <tr>
          <td>Shopify</td>
          <td>把峰值準備與演練制度化</td>
          <td>capacity planning、resiliency matrix</td>
      </tr>
  </tbody>
</table>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作工具見 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">vendors</a> — T1 收錄 CI（GitHub Actions / CircleCI）、Load test（k6 / Gatling / JMeter / Locust）、Chaos（Chaos Mesh / LitmusChaos / Gremlin / Toxiproxy）、SLO（Nobl9 / Sloth）共 12 個 vendor 骨架。跟 <a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">cases/</a> 是不同維度（cases 是教學案例來源、vendors 是實作工具）。</p>
<p>進入工具比較前，先回到 <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型</a> 判斷目前缺的是驗證層能力，還是缺少可觀測性的訊號 baseline 或事故處理的接手流程。可靠性工具選型要以「能否安全驗證失敗」為主軸，CI、load、chaos 或 SLO 工具名稱只是落地選項。</p>
<p>Deep article（工具自身的配置、故障、容量）跟 migration playbook（跨工具遷移流程）的撰寫進度見 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="規劃方向">規劃方向</h2>
<p>本輪規劃的核心是把模組從「驗證手段列表」升級成「失敗風險節點 + 服務級案例庫」兩層結構：</p>
<ol>
<li><strong>問題節點先行</strong>：6.1-6.5 主章已建立、補 6.6（SLO/Error Budget）/ 6.7（DR &amp; Rollback Rehearsal）/ 6.8（Release Gate &amp; Change Cadence）/ 6.9（Capacity &amp; Cost）等節點，不綁特定框架。</li>
<li><strong>服務級案例庫</strong>：以公開 SRE 實踐（Google / Netflix / Amazon / Stripe / Shopify 等）作 cases，每個服務一個資料夾、累積架構脈絡與多次驗證案例。</li>
<li><strong>資安驗證是其中一類</strong>：跟 07 的交接點維持，但 07 的紅藍隊框架不外推到本模組 — SRE 自有 Failure Mode / Pre-mortem / FMEA / Chaos Hypothesis 等 first-class 詞彙、不需要藉攻防隱喻表達。</li>
</ol>
<p>不經實作即可推進的理由：可靠性的價值在「失敗模式預判與驗證設計」，這層跟具體框架解耦，SRE 公開素材成熟，符合先建概念層的條件。</p>
<h2 id="模組方法">模組方法</h2>
<p>問題驅動方法的核心是讓案例退到證據角色，讓知識網以失敗風險為主體。</p>
<ol>
<li>先定義驗證環節問題與失敗風險邊界。</li>
<li>再定義判讀訊號（容量門檻、退化曲線、依賴失效模式）與門檻條件。</li>
<li>接著定義交接路由與前置控制面。</li>
<li>最後在問題觸發時引用對應服務的 SRE 案例。</li>
</ol>
<h2 id="模組分工定位">模組分工定位</h2>
<p>本模組提供觀念、判讀與路由。實作細節由對應模組承接，確保概念層與實作層分工清晰。</p>
<ul>
<li><code>backend/04-observability</code>：可觀測性模組，負責訊號定義、SLO 量測與 alert 治理實作。</li>
<li><code>backend/05-deployment-platform</code>：rollout、rollback、流量切換與環境管理實作。</li>
<li><code>backend/07-security-data-protection</code>：權限、稽核與高風險演練約束實作。</li>
<li><code>backend/08-incident-response</code>：事故處理模組，負責事故指揮、分級與復盤的事中事後流程。</li>
</ul>
<h2 id="從章節到實作的-chain">從章節到實作的 chain</h2>
<p>各章節交付三樣：問題節點清單、判讀訊號、控制面 link。判讀完成後沿兩條 chain 進入 implementation：</p>
<ol>
<li><strong>Mechanism chain</strong>：點問題節點表的 <code>[control-name]</code> link 進 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge-cards</a>、那層展開機制 / 邊界 / context-dependence。例：<code>[circuit-breaker]</code> 的 knowledge-card 是該 control 的 mechanism SSoT。</li>
<li><strong>Delivery chain</strong>：章節「交接路由」欄位指向下游模組，包括可觀測性（訊號 / SLO）、部署平台（rollout / rollback）、資安與資料保護（權限約束）與事故處理（事故閉環）。</li>
</ol>
<p>兩條 chain 走完，控制面交付完整。Implementation 強度取決於兩條 chain 的完成度，章節閱讀本身完成 routing 階段。</p>
<h2 id="跟既有模組的串接">跟既有模組的串接</h2>
<p>本模組是「觀測 → 驗證 → 事故」閉環的中段、承接資安概念判讀、同時餵給事故處理閉環。資安驗證僅是驗證的一個子集、其他多數驗證是容量 / 變更 / 依賴類。</p>
<p><strong>觀測、驗證與事故閉環交接基線</strong>：</p>
<ul>
<li><strong>來自 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></strong>：SLO / SLI 量測 baseline、production 訊號是 chaos hypothesis 與 SLO 政策的依據。沒有可信訊號就沒有可信驗證。</li>
<li><strong>餵給 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></strong>：驗證需求驅動訊號設計 — chaos experiment 需要新 metric、load test 需要新 dashboard、SLO 政策需要新 alert rule。</li>
<li><strong>餵給 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></strong>：把事前演練結果作為事中決策素材、game day 暴露的 runbook 缺口直接補進值班與演練能力建設。</li>
<li><strong>來自 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></strong>：事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> action items 回寫成新 chaos / DR 演練題目。</li>
<li><strong>詳細閉環說明</strong>：見 <a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">Observability / Reliability / Incident Response 閉環</a>。</li>
</ul>
<p><strong>07 資安交接基線</strong>：</p>
<ul>
<li>來自 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>：承接資料外送與回復排序的驗證場景。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a>：承接事件證據完整性與回查演練。</li>
<li>來自紅隊 <a href="/blog/backend/07-security-data-protection/red-team/resource-abuse/" data-link-title="7.R4 資源濫用與可用性破壞" data-link-desc="說明攻擊者如何把合法操作放大成容量壓力或服務退化">7.R4 資源濫用與可用性破壞</a>：承接壓力放大路徑與降級回復驗證。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/security-and-reliability-shared-controls/" data-link-title="7.23 資安與可靠性的共同控制面" data-link-desc="建立資安與可靠性共同控制面的交集，整合 rollback、containment、degradation 與 evidence">7.23 資安與可靠性的共同控制面</a>：承接 rollback、containment、degradation 共用語意。</li>
</ul>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理測試程式如何寫得可讀、可重現、可定位。Backend reliability 模組處理測試如何在 CI、環境、資料庫、broker、網路與部署流程中被執行。</p>
<h2 id="企業案例補充">企業案例補充</h2>
<p>可靠性案例補充的重點是「驗證機制如何被制度化」。閱讀時先抓它在保護哪一種風險，再對照本模組的驗證節點與放行門檻。</p>
<table>
  <thead>
      <tr>
          <th>企業案例</th>
          <th>主要可靠性選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://shopify.engineering/four-steps-creating-effective-game-day-tests">Four Steps to Creating Effective Game Day Tests</a></td>
          <td>Game Day 如何從想法變成可執行驗證流程</td>
          <td><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4</a>、<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td><a href="https://shopify.engineering/resiliency-planning-for-high-traffic-events">Resiliency Planning for High-Traffic Events</a></td>
          <td>高流量活動前如何做風險建模與演練</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td><a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding</a></td>
          <td>多租戶系統如何把故障影響限制在局部</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a>、<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td><a href="https://sre.google/workbook/error-budget-policy/">Google SRE Workbook: Example Error Budget Policy</a></td>
          <td>Error budget 如何直接影響 release 節奏</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
  </tbody>
</table>
<p>若要延續案例擴充，先從 <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a> 找到對應規模與產業，再回到本模組決定要補哪一類驗證節點（6.6、6.19、6.20、6.22、6.24）。案例頁與主章的關係是「案例提供壓力樣本，主章提供放行規則」。</p>
<p>產業情境回寫覆蓋 7 個產業，每個產業回寫到 2 個最相關的章節。不同產業的約束類型不同（監管 / 即時互動 / 合規 / 季節峰值 / 多租戶 / 即時交付 / 邊緣可靠性），通用章節覆蓋不到的差異由產業情境段補齊。</p>
<table>
  <thead>
      <tr>
          <th>產業案例類型</th>
          <th>約束類型</th>
          <th>驗證回寫重點</th>
          <th>章節路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td>監管 + 交易正確性</td>
          <td>error budget 觸發凍結、交易關鍵路徑的 release gate</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td>即時互動 + 使用者體驗</td>
          <td>高峰事件前穩態定義、規則推送回退與停止條件</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a>、<a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td>合規 + 臨床安全</td>
          <td>DR rehearsal 節奏、合規約束下的恢復驗證與 readiness</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a>、<a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19</a></td>
      </tr>
      <tr>
          <td>電商 / 零售</td>
          <td>季節峰值 + 轉換率</td>
          <td>峰值 workload model、容量成本與降級邊界</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2</a>、<a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>SaaS / B2B</td>
          <td>多租戶 SLA + 隔離</td>
          <td>依賴 budget 按 SLA 分配、租戶級穩態定義</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>串流 / 媒體</td>
          <td>即時交付 + 直播事件</td>
          <td>CDN chaos 與媒體品質 regression</td>
          <td><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4</a>、<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13</a></td>
      </tr>
      <tr>
          <td>IoT / 製造</td>
          <td>邊緣可靠性 + OTA 安全</td>
          <td>firmware rollback 與裝置碎片化 release gate</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
  </tbody>
</table>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>可靠性驗證使用方式會受語言的測試框架、fixture 生態、並發測試能力、型別系統、fuzz 支援與容器化工具影響。同步 runtime 要測 thread pool、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>；async runtime 要測 event loop blocking、task cancellation 與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>；動態語言要用 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test 與 runtime validation 補足 schema 風險；強型別語言要把型別安全延伸到外部 payload 與 migration 相容性。</p>
<h2 id="主章規劃">主章規劃</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a></td>
          <td>CI Pipeline</td>
          <td>分層測試、快慢測試與 artifact 管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 Load test</a></td>
          <td>Load Test</td>
          <td>定義 workload、吞吐與延遲基準</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/fuzz-campaign/" data-link-title="6.3 fuzz campaign" data-link-desc="用自動化輸入探索覆蓋未知邊界：target 設計、corpus 管理、crash reproduction 與 CI 整合">6.3 Fuzz campaign</a></td>
          <td>Fuzz Campaign</td>
          <td>建立輸入邊界、corpus 與 crash reproduction</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 Chaos testing</a></td>
          <td>Chaos Testing</td>
          <td>模擬 broker、DB、network 與節點故障</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判（Pre-mortem 與 FMEA）</a></td>
          <td>Failure Mode Pre-mortem</td>
          <td>用驗證盲區、演練缺口與門檻失真檢查 release 風險用 SRE first-class 詞彙定義失敗模式預判</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 與 Error Budget 政策</a></td>
          <td>SLO &amp; Error Budget</td>
          <td>把可靠性目標轉成可驗證量測與凍結條件</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR 演練與 Rollback Rehearsal</a></td>
          <td>DR &amp; Rollback Rehearsal</td>
          <td>把回復路徑變成定期可重播流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a></td>
          <td>Release Gate</td>
          <td>把驗證、migration、相容性納入放行判準</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界</a></td>
          <td>Capacity &amp; Cost</td>
          <td>把容量規劃跟成本約束變成驗證輸入</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a></td>
          <td>Contract Testing</td>
          <td>把跨服務 / API / event schema 契約變成可驗證 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety 與 DB Rollout</a></td>
          <td>Migration Safety</td>
          <td>把 schema migration 變成可逆、可漸進的 rollout 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a></td>
          <td>Idempotency &amp; Replay</td>
          <td>把重試 / 重播 / 冪等從口頭約定變成可驗證屬性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a></td>
          <td>Perf Regression Gate</td>
          <td>把效能 baseline 從一次性壓測變成持續 release gate</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 Dependency Reliability Budget</a></td>
          <td>Dependency Budget</td>
          <td>把內外依賴可靠性納入 SLO 計算與設計約束</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 Environment Parity 與漂移控制</a></td>
          <td>Environment Parity</td>
          <td>把 staging / preprod / prod 差異作為一級風險治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 Test Data Management</a></td>
          <td>Test Data Management</td>
          <td>把 fixture / seed / production-like data 作為跨模組共用 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance</a></td>
          <td>Feature Flag Governance</td>
          <td>把 feature flag 從上線工具升級為有 lifecycle / debt 治理的 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 Reliability Metrics Governance</a></td>
          <td>Reliability Metrics</td>
          <td>DORA / SPACE / CFR 等可靠性指標的選用、量測與治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 Reliability Readiness Review</a></td>
          <td>Reliability Readiness Review</td>
          <td>把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></td>
          <td>Experiment Safety Boundary</td>
          <td>定義 chaos、load test、DR drill 的 blast radius、停止條件與權限約束</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a></td>
          <td>Reliability Debt Backlog</td>
          <td>把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 Steady State Definition</a></td>
          <td>Steady State Definition</td>
          <td>在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></td>
          <td>Verification Evidence Handoff</td>
          <td>把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門</a></td>
          <td>Rule Rollout Safety Gate</td>
          <td>把規則、策略與控制面配置推送變更納入高擴散風險 gate</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate</a></td>
          <td>Provider Dependency Release Gate 實作示範</td>
          <td>以 payment provider 變更示範 gate、stop condition 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 的實作交接</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>註：6.1-6.25 已完成概念層與第一篇實作示範正文，案例庫可支援 SLO、readiness、experiment boundary、evidence handoff 與 release gate 實作路由。後續工作重點是案例深挖與主章回寫密度，不是章節補齊。</p></blockquote>
<h2 id="個案前拓展空間">個案前拓展空間</h2>
<p>個案前拓展的責任是先建立驗證判準，再讓服務案例成為證據。可靠性驗證適合補「怎麼安全地驗證失敗」這類跨服務流程，不適合先把 Google / Netflix / Amazon 的故事直接展開。</p>
<table>
  <thead>
      <tr>
          <th>拓展方向</th>
          <th>補充理由</th>
          <th>先放位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reliability Readiness Review</td>
          <td>服務進入 production 前需要有可檢查的可靠性門檻</td>
          <td>6.19</td>
      </tr>
      <tr>
          <td>Experiment Safety Boundary</td>
          <td>故障注入與壓測需要明確 blast radius 與停止條件</td>
          <td>6.20</td>
      </tr>
      <tr>
          <td>Reliability Debt Backlog</td>
          <td>復盤與演練缺口需要形成可排序的改善 backlog</td>
          <td>6.21</td>
      </tr>
      <tr>
          <td>Steady State Definition</td>
          <td>chaos 與 DR drill 需要先知道什麼狀態算穩定</td>
          <td>6.22</td>
      </tr>
  </tbody>
</table>
<p>本輪先完成其中三個前置章節：Reliability Readiness Review、Experiment Safety Boundary 與 Steady State Definition，並補強 6.6 SLO / Error Budget 政策。服務案例完成後，若教訓是「上線前準備不足」，回寫 Reliability Readiness Review；若是「實驗本身造成過大影響」，回寫 Experiment Safety Boundary；若是「反覆事故沒有被工程化」，回寫 Reliability Debt Backlog；若是「chaos 沒有穩態定義」，回寫 Steady State Definition。</p>
<h2 id="後續深化方向">後續深化方向</h2>
<p>06 後續深化以「多事件案例鏈、驗證證據欄位統一、事故路由回寫」為主。可靠性驗證承接 04 的訊號可信度，並把結果穩定交給 08 的 incident 決策流程。</p>
<table>
  <thead>
      <tr>
          <th>深化方向</th>
          <th>主要責任</th>
          <th>回寫路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多事件案例鏈</td>
          <td>同服務補第二、第三事件，提升 longitudinal 判讀</td>
          <td><a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">cases/</a></td>
      </tr>
      <tr>
          <td>證據欄位統一</td>
          <td>把 SLO / chaos / rollout 證據變成同一決策格式</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>風險回寫治理</td>
          <td>把 repeated incident 與手動補救回寫 backlog</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
  </tbody>
</table>
<h2 id="實作探討入口">實作探討入口</h2>
<p>進入實作層時，06 建議先做一條最小 release gate：同一個變更同時具備 <code>SLO 狀態、readiness 結論、experiment 證據、rollback 條件</code> 四欄，並寫入 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 供 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 直接調用。</p>
<p>首篇示範已完成： <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a>。</p>
<p>完成條件是每篇都能回答四件事：可靠性目標、驗證訊號、停止或凍結條件、事故或發布路由。這樣可靠性章節才會成為「觀測 → 驗證 → 事故」閉環的中段，而不是測試工具清單。</p>
<h2 id="服務案例庫規劃">服務案例庫規劃</h2>
<p>服務作為案例單位、累積架構脈絡與多次驗證實踐。每個服務一個資料夾、收錄該服務的 SRE 實踐、failure mode 與 chaos / DR 案例。資料夾位置：<code>content/backend/06-reliability/cases/{vendor-service}/</code>。</p>
<h3 id="t1必寫sre-教學標竿">T1（必寫、SRE 教學標竿）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">google</a></td>
          <td>SRE Book 原典 / SLI-SLO / <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> culture / error budget</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/" data-link-title="Netflix" data-link-desc="Netflix Chaos Engineering 起源：Simian Army / FIT / 規模化故障注入">netflix</a></td>
          <td>Chaos Monkey / Simian Army / FIT 故障注入工具鏈</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/amazon/" data-link-title="Amazon" data-link-desc="Amazon Cell-based Architecture / Shuffle Sharding / Blast Radius 設計">amazon</a></td>
          <td>Cell-based architecture / shuffle sharding / blast radius</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe</a></td>
          <td>Deploy strategy / Game day / canary 與 idempotency</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">shopify</a></td>
          <td>BFCM scaling / pod-based isolation / capacity planning</td>
      </tr>
  </tbody>
</table>
<h3 id="t2補不同視角">T2（補不同視角）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin</a></td>
          <td>Capacity planning / <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> structure</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/" data-link-title="Honeycomb" data-link-desc="Honeycomb Observability-driven SRE 與 SLO 實作">honeycomb</a></td>
          <td>Observability-driven SRE / SLO 實作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare</a></td>
          <td>Edge reliability engineering / 公開實踐（住於 08）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/" data-link-title="Microsoft / Azure SRE" data-link-desc="Microsoft Azure SRE Practices 與 Resilience Patterns">microsoft</a></td>
          <td>Azure SRE / Resilience patterns</td>
      </tr>
  </tbody>
</table>
<h3 id="t3補完">T3（補完）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/" data-link-title="Spotify" data-link-desc="Spotify Chaos Engineering 與 Squad-based SRE">spotify</a></td>
          <td>Squad-based SRE / Backstage</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/" data-link-title="Pinterest" data-link-desc="Pinterest Capacity Planning 與儲存架構可靠性">pinterest</a></td>
          <td>Storage capacity / cache reliability</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/meta/" data-link-title="Meta / Facebook" data-link-desc="Meta Reliability Engineering 與超大規模事故學習">meta</a></td>
          <td>2021-10 BGP / Region failover / cell arch</td>
      </tr>
  </tbody>
</table>
<h2 id="模組完成狀態">模組完成狀態</h2>
<p>主章 6.1-6.25 已完成首輪正文，服務案例庫第一批正文已補齊（T1：Google / Netflix / Amazon / Stripe / Shopify；T2/T3：LinkedIn / Honeycomb / Microsoft / Spotify / Pinterest / Meta）。目前重點從「補章節骨架」轉為「補案例深度與跨章節回寫」。</p>
<p>案例正文入口見 <a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">可靠性案例庫</a>。每篇案例至少要能回寫一個章節判準（例如 6.6、6.19、6.20、6.22、6.23、6.24），避免案例只停留在事件敘事。</p>
<p>第二批案例深挖已補 Google 與 Netflix 的第二篇正文： <a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">Google Postmortem Closure 治理</a> 與 <a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">Netflix Business-Hours Chaos Guardrails</a>。兩者分別對應 <code>6.21 / 8.5 / 8.22</code> 與 <code>6.19 / 6.20 / 6.22 / 8.6</code> 的制度化回寫。</p>
<p>深挖批次 B 已補 Google 第三篇制度案例： <a href="/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">Google Toil Budget 與 Automation 投資政策</a>。這篇把 toil ratio 直接接到 <code>6.8 / 6.21 / 8.22</code>，補齊「值班壓力 → 工程投資 → release gate」的決策鏈。</p>
<p>第三批案例補強已補 <code>Netflix</code> 第三篇： <a href="/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">FIT 證據交接與 Release Gate 回寫</a>。這篇把故障注入結果直接接到 <code>6.23 / 6.24 / 8.19 / 8.22</code>，補齊「實驗結果 → 放行決策 → 事故調用」的鏈路。</p>
<h2 id="case-first-第四批8-章-stage-2-擴充">Case-First 第四批：8 章 stage 2 擴充</h2>
<p>依 <a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-First + Agent Team Review 流程</a> 完成 8 個章節的 case-driven 擴充（commit 3c33ea9 / 41c0101）、覆蓋全部 15 個 content case：</p>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>擴充內容</th>
          <th>Case 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release-gate</a></td>
          <td>變更分層 + Release Gate 政策、交易類變更的 gate 設計</td>
          <td>MS1 / G2 / S1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency-reliability-budget</a></td>
          <td>失效局部化、跨區故障與回復順序、跨團隊 reliability 契約</td>
          <td>A1 / M1 / SP1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability-debt-backlog</a></td>
          <td>Action Item 分級跟 Release Gate 綁定、Toil Budget 預算治理</td>
          <td>G2 / G3</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity-cost</a></td>
          <td>高峰型容量治理、容量值班分層協同、快取容量特殊性</td>
          <td>H1 / L1 / P1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration-safety</a></td>
          <td>交易類 migration 的特殊性</td>
          <td>S1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency-replay</a></td>
          <td>支付類 Idempotency 的設計約束</td>
          <td>S1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 slo-error-budget</a></td>
          <td>Error Budget 三對齊跟 Release Gating、Burn Rate 雙窗監控</td>
          <td>G1 / HC1</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment-safety-boundary</a></td>
          <td>案例對照：Chaos / FIT 的安全邊界設計</td>
          <td>N1 / N2 / N3</td>
      </tr>
  </tbody>
</table>
<p>擴充紀律對應 <a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-First Module Workflow</a> 的五階段流程、用 agent team review 三維度（寫作規範 / 案例引用 / 跨章一致性）驗證、case fidelity 達 88%。</p>
<h2 id="下一輪推演大綱">下一輪推演大綱</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>產出</th>
          <th>責任</th>
          <th>回寫位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>案例深挖批次 A</td>
          <td>針對 T1 案例補第二篇以上正文，強化同一服務的多次驗證脈絡</td>
          <td><code>cases/google/</code>、<code>cases/netflix/</code></td>
      </tr>
      <tr>
          <td>2</td>
          <td>案例深挖批次 B</td>
          <td>針對 T2/T3 案例補跨規模對照，避免只描述單一事件</td>
          <td><code>cases/{service}/</code></td>
      </tr>
      <tr>
          <td>3</td>
          <td>章節回寫補強</td>
          <td>把案例中的 policy、gate、readiness 與 evidence 直接回寫主章段落</td>
          <td><code>6.6</code>、<code>6.19</code>、<code>6.20</code>、<code>6.22</code>、<code>6.23</code></td>
      </tr>
      <tr>
          <td>4</td>
          <td>跨模組路由校正</td>
          <td>補齊 04/05/07/08 的交接連結，讓讀者可從案例直接跳到對應控制面</td>
          <td>各章節「交接路由」段</td>
      </tr>
  </tbody>
</table>
<p>推演資產化的完成條件是讓讀者能從一個失敗風險出發，找到驗證節點、服務 case 與回寫章節。完成後可靠性模組才進入穩定維護狀態。</p>
<h3 id="本輪全面推進2026-06-23">本輪全面推進（2026-06-23）</h3>
<p>主章 6.1-6.25 全部從骨架擴充到完整內容（最小 75 行、中位 113 行、最大 176 行），覆蓋概念定位、判讀訊號、案例回寫與交接路由。案例庫補齊 T1/T2/T3 第二批正文共 9 篇（Amazon A2 / Stripe S2 / Shopify H2 / LinkedIn L2 / Meta M2 / Honeycomb HC2 / Microsoft MS2 / Spotify SP2 / Pinterest P2），11 個 vendor 各有 2+ 篇案例。Vendor deep article 新增 4 篇（k6 / Chaos Mesh / Sloth / GitHub Actions）。產業情境回寫 3 組（FinTech → 6.6+6.8 / Gaming → 6.22+6.24 / Healthcare → 6.7+6.19）。經過三輪多輪審查（寫作規範 / cadence 同質化 / steelman reality test）修法。</p>
<p>目前模組處於穩定維護狀態。剩餘 backlog：8 個 vendor 的 deep article（CircleCI / Gatling / JMeter / Locust / LitmusChaos / Gremlin / Toxiproxy / Nobl9）、08 模組的 06 反向引用補齊、判讀訊號表格補行動建議欄。</p>
<h2 id="tripwire">Tripwire</h2>
<ul>
<li>寫 T1 服務第 3 個時、若 case 之間無共通分類軸 → 改用單服務獨立檔，不開資料夾。</li>
<li>寫到第 9 主章發現章節覆蓋 60%+ → 軸線過於相似、合併或重切。</li>
<li>進服務實作模組時 routing chain 走不通 → 回頭補對應主章。</li>
</ul>
<h2 id="既有可引用卡片">既有可引用卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a></li>
<li><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test</a></li>
<li><a href="/blog/backend/knowledge-cards/fuzz-test/" data-link-title="Fuzz Test" data-link-desc="說明用隨機與變異輸入驗證解析器與邊界處理健壯性">fuzz test</a></li>
<li><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></li>
<li><a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a></li>
</ul>
]]></content:encoded></item><item><title>2.6 快取威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/attacker-view-cache-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/attacker-view-cache-risks/</guid><description>&lt;p>快取層威脅建模的判讀目標是確認「資料是否可被污染、可被放大、可被錯用」。快取的存在是為了用副本換取讀取效率，盤點要問的就是這個副本能不能被攻擊者操弄：寫進錯誤內容、被放大成回源洪水、或被當成正式狀態誤信。只看效能設計快取，常把一致性與安全邊界留到事故後才補。&lt;/p>
&lt;h2 id="哪些快取場景要先做弱點盤點">哪些快取場景要先做弱點盤點&lt;/h2>
&lt;p>快取弱點會在特定條件下快速放大，這些條件出現時值得在設計階段就做一次盤點，而不是等流量打上來才發現。&lt;/p>
&lt;p>熱門資料高度集中是第一個訊號。當少數 key 承載大部分讀取，形成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 時，這些 key 的失效或污染影響面被流量放大：一個被污染的熱門商品價格，會在 TTL 週期內被數百萬次請求讀到。攻擊者只要能影響一個熱門 key，就能用快取的扇出能力把單點錯誤放大成大規模事故。&lt;/p>
&lt;p>同一份資料被多個系統共用是第二個訊號。當一份快取資料同時被 API、搜尋與報表讀取，污染這份資料的後果會跨系統擴散，而各系統對資料正確性的假設不同——API 可能容忍短暫陳舊，計費報表不能。共用快取讓「誰負責驗證這份資料」變得模糊。&lt;/p>
&lt;p>失效策略依賴多服務協作是第三個訊號。當快取失效需要多個服務按順序執行才成立時，任何一個環節被延遲或繞過，都會留下一個 stale 窗口。攻擊者可以針對這個協作鏈的最弱環節，製造可預測的不一致窗口。&lt;/p>
&lt;p>匯出、權限摘要或價格資料大量走快取是第四個訊號。這幾類資料的錯誤直接對應到金錢、越權或合規後果，一旦快取成為它們的讀取路徑，快取的一致性強度就決定了這些高風險判斷的正確性。&lt;/p>
&lt;h2 id="快取弱點的檢查順序">快取弱點的檢查順序&lt;/h2>
&lt;p>弱點盤點依「資料責任 → 失效面 → 放大面 → 污染面」的順序展開，每一層對應一個攻擊者會利用的弱點。&lt;/p>
&lt;p>第一層看資料責任，先區分 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 與快取副本。攻擊者最想找的是「被當成正式狀態的快取副本」——如果某個權限判斷直接信任快取值而不回源驗證，污染這個快取值就等於提權。檢查的方法是追每個快取讀取點，確認它讀到的是可重建的副本，還是被誤用成不該被快取的正式判斷依據。&lt;/p>
&lt;p>第二層看失效面，檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 規則是否一致。失效面的弱點是「stale 窗口可被預測或延長」：若失效只靠廣播通知而沒有 TTL 兜底，攻擊者讓廣播漏送就能讓某節點長期持有舊值；若 TTL 設得過長，污染或過期資料的影響期就被拉長。&lt;/p>
&lt;p>第三層看放大面，檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd&lt;/a> 與回源壓力保護。放大面的攻擊是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-penetration/" data-link-title="Cache Penetration" data-link-desc="說明查詢必定不存在的 key 繞過快取直接打向 origin 的弱點與防護">cache penetration&lt;/a>：攻擊者枚舉大量必定不存在的 key（不連續的 id、構造的非法 slug），這些查詢全部 miss 並穿透到資料庫，把快取的保護作用繞過、直接打垮 origin。防線是對不存在的 key 也做短期 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache&lt;/a>（把「查無此 key」這個結果也快取一小段時間，擋掉重複穿透），以及對回源路徑加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 與單飛（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight&lt;/a>，讓同一 key 的並發回源只有一個真正打到資料庫、其餘等結果）保護。negative cache 自身有代價：真實資料建立後要等 negative 項過期才會被命中，TTL 要夠短，避免新上架資料被「查無」結果短暫遮擋。&lt;/p>
&lt;p>第四層看污染面，檢查 key 設計、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">租戶隔離&lt;/a>與欄位遮罩是否防止快取污染與越權讀取。污染面最常見的弱點是 key 命名沒有把租戶或權限維度編進去：若兩個租戶的資料共用同一個快取 key，一個租戶就能讀到另一個租戶的快取值。key 設計要讓隔離維度成為 key 的一部分，而非依賴應用層在讀取後才過濾。&lt;/p>
&lt;p>第五層看推斷面，檢查 cache 命中與否是否會透過回應時間洩漏資訊。cache hit 與 miss 的延遲差異本身是一個 side-channel：攻擊者用一批查詢的回應時間分布，可以推斷某個帳號、商品或 slug 是否存在，即使回應內容本身有做存在性遮罩。對帳號存在性、未上架商品這類敏感判斷，要讓 hit 與 miss 的可觀察延遲一致（例如 miss 也走一段固定延遲），或把存在性判斷的快取與對外查詢路徑分離。這層在敏感資料才需要盤點，一般可重建副本不受此威脅。&lt;/p>
&lt;h2 id="快取弱點先表現為資料錯誤而非停機">快取弱點先表現為資料錯誤而非停機&lt;/h2>
&lt;p>快取事故的判讀重點是它的早期症狀先表現為資料錯誤，而停機往往是後續才浮現的次級症狀。價格、庫存、權限摘要若短時間錯誤，系統照常回應請求、監控的可用性指標一切正常，但回應的內容是錯的，直接造成客訴與營運損失。這種「服務還活著但說錯話」的故障比停機更難被即時發現，因為它不觸發可用性告警。&lt;/p>
&lt;p>回源壓力缺少保護時，快取問題還會反向擴散。原本快取是用來保護資料庫的，但當 stampede 或 penetration 讓大量請求同時穿透，快取從保護層變成放大層，把一個快取層的問題擴散成資料庫與下游服務的連鎖過載。弱點盤點因此要把「快取失效時，壓力會打到哪裡」當成必答問題。&lt;/p>
&lt;h2 id="低延遲與一致性強度的取捨">低延遲與一致性強度的取捨&lt;/h2>
&lt;p>快取命中率越高，延遲與成本越好，同時一致性風險也越高。按資料重要性分層是平衡這個張力最穩定的做法，比對所有資料套同一套快取策略更能讓高風險資料拿到該有的一致性強度。&lt;/p>
&lt;p>高風險資料採較短生命週期與強驗證。價格、權限、餘額這類錯誤代價高的資料，用較短 TTL（典型在數秒到數分鐘量級）縮小污染與陳舊的影響窗口，並在關鍵判斷點保留回源驗證，讓快取只承擔加速、不承擔最終正確性。低風險資料採較寬鬆策略以保留效能收益。商品描述、頭像、靜態文案這類偶爾陳舊無實質後果的資料，用較長 TTL（數十分鐘到數小時）換取更高命中率。具體秒數沒有通用值，依該資料的陳舊容忍度決定；分層的判準是「這份資料錯誤幾分鐘的代價是什麼」，代價高的往一致性傾斜，代價低的往效能傾斜。&lt;/p>
&lt;h2 id="進入實作前要先定義的最低控制面">進入實作前要先定義的最低控制面&lt;/h2>
&lt;p>弱點盤點的產出是一組進入實作前必須先定義清楚的控制面，缺少其中任何一項，後續的快取設計都是在未定義的安全假設上往前蓋。每一項控制面同時是一個診斷工具：可以用「若沒有它會看到什麼現象」反過來判斷現有系統是否缺這道防線。&lt;/p>
&lt;p>快取資料分級與可接受陳舊窗口要先定義，這決定每類資料的 TTL 與是否需要回源驗證。沒有分級，所有資料會被同一套策略對待，高風險資料的陳舊窗口被低風險策略放寬。未定義的早期訊號是「所有 key 用同一個預設 TTL」「說不清哪些資料錯了會出事」。&lt;/p>
&lt;p>失效策略與回源保護規則要先定義，這決定 stale 窗口的上界與回源洪水的防線。失效要明確是廣播、TTL 還是事件驅動，並確認廣播類失效一定有 TTL 兜底；回源要明確 negative cache、single-flight 與 rate limit 的配置。未定義的早期訊號是「失效靠廣播但沒有 TTL 兜底」「miss 尖峰會直接打到資料庫」。&lt;/p>
&lt;p>key 命名、租戶隔離與敏感欄位限制要先定義，這決定污染與越權讀取的防線。隔離維度要編進 key、敏感欄位要在寫入快取前就遮罩，而非依賴讀取後過濾。未定義的早期訊號是「快取 key 不含租戶 id」「敏感欄位整包序列化進 cache」。&lt;/p>
&lt;p>快取異常時的降級與回復流程要先定義，這決定事故發生時系統往哪個方向退。降級要明確是「快取失效時回源並接受延遲上升」還是「直接拒絕並保護資料庫」，並預先演練回復路徑，避免事故當下才設計。未定義的早期訊號是「沒人能回答快取掛掉時系統會怎樣」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>大量查詢不存在的 key、回源 QPS 飆&lt;/td>
 &lt;td>cache penetration 繞過快取保護&lt;/td>
 &lt;td>對不存在的 key 加 negative cache、回源加 rate limit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩租戶讀到彼此的快取資料&lt;/td>
 &lt;td>key 未把租戶維度編入、隔離失效&lt;/td>
 &lt;td>把隔離維度納入 key、敏感欄位寫入前遮罩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性指標正常但客訴資料錯誤&lt;/td>
 &lt;td>快取污染或陳舊，故障不觸發可用性告警&lt;/td>
 &lt;td>補資料正確性監控、關鍵判斷點回源驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>單一熱門 key 失效造成回源尖峰&lt;/td>
 &lt;td>hot key 失效面被流量放大&lt;/td>
 &lt;td>對熱門 key 加 single-flight、錯開 TTL&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>可用性正常但資料錯誤之所以難察覺，是因為可用性監控設計上只看系統有沒有正常回應，不掃回應內容對不對，污染或陳舊因此可以在告警全程靜默的情況下持續數小時。這類問題要靠資料正確性監控而非可用性監控才能發現，弱點盤點要確認高風險資料有對應的正確性檢查，而不是只看 latency 與 error rate。&lt;/p></description><content:encoded><![CDATA[<p>快取層威脅建模的判讀目標是確認「資料是否可被污染、可被放大、可被錯用」。快取的存在是為了用副本換取讀取效率，盤點要問的就是這個副本能不能被攻擊者操弄：寫進錯誤內容、被放大成回源洪水、或被當成正式狀態誤信。只看效能設計快取，常把一致性與安全邊界留到事故後才補。</p>
<h2 id="哪些快取場景要先做弱點盤點">哪些快取場景要先做弱點盤點</h2>
<p>快取弱點會在特定條件下快速放大，這些條件出現時值得在設計階段就做一次盤點，而不是等流量打上來才發現。</p>
<p>熱門資料高度集中是第一個訊號。當少數 key 承載大部分讀取，形成 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 時，這些 key 的失效或污染影響面被流量放大：一個被污染的熱門商品價格，會在 TTL 週期內被數百萬次請求讀到。攻擊者只要能影響一個熱門 key，就能用快取的扇出能力把單點錯誤放大成大規模事故。</p>
<p>同一份資料被多個系統共用是第二個訊號。當一份快取資料同時被 API、搜尋與報表讀取，污染這份資料的後果會跨系統擴散，而各系統對資料正確性的假設不同——API 可能容忍短暫陳舊，計費報表不能。共用快取讓「誰負責驗證這份資料」變得模糊。</p>
<p>失效策略依賴多服務協作是第三個訊號。當快取失效需要多個服務按順序執行才成立時，任何一個環節被延遲或繞過，都會留下一個 stale 窗口。攻擊者可以針對這個協作鏈的最弱環節，製造可預測的不一致窗口。</p>
<p>匯出、權限摘要或價格資料大量走快取是第四個訊號。這幾類資料的錯誤直接對應到金錢、越權或合規後果，一旦快取成為它們的讀取路徑，快取的一致性強度就決定了這些高風險判斷的正確性。</p>
<h2 id="快取弱點的檢查順序">快取弱點的檢查順序</h2>
<p>弱點盤點依「資料責任 → 失效面 → 放大面 → 污染面」的順序展開，每一層對應一個攻擊者會利用的弱點。</p>
<p>第一層看資料責任，先區分 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 與快取副本。攻擊者最想找的是「被當成正式狀態的快取副本」——如果某個權限判斷直接信任快取值而不回源驗證，污染這個快取值就等於提權。檢查的方法是追每個快取讀取點，確認它讀到的是可重建的副本，還是被誤用成不該被快取的正式判斷依據。</p>
<p>第二層看失效面，檢查 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 規則是否一致。失效面的弱點是「stale 窗口可被預測或延長」：若失效只靠廣播通知而沒有 TTL 兜底，攻擊者讓廣播漏送就能讓某節點長期持有舊值；若 TTL 設得過長，污染或過期資料的影響期就被拉長。</p>
<p>第三層看放大面，檢查 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 與回源壓力保護。放大面的攻擊是 <a href="/blog/backend/knowledge-cards/cache-penetration/" data-link-title="Cache Penetration" data-link-desc="說明查詢必定不存在的 key 繞過快取直接打向 origin 的弱點與防護">cache penetration</a>：攻擊者枚舉大量必定不存在的 key（不連續的 id、構造的非法 slug），這些查詢全部 miss 並穿透到資料庫，把快取的保護作用繞過、直接打垮 origin。防線是對不存在的 key 也做短期 <a href="/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache</a>（把「查無此 key」這個結果也快取一小段時間，擋掉重複穿透），以及對回源路徑加 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與單飛（<a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight</a>，讓同一 key 的並發回源只有一個真正打到資料庫、其餘等結果）保護。negative cache 自身有代價：真實資料建立後要等 negative 項過期才會被命中，TTL 要夠短，避免新上架資料被「查無」結果短暫遮擋。</p>
<p>第四層看污染面，檢查 key 設計、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">租戶隔離</a>與欄位遮罩是否防止快取污染與越權讀取。污染面最常見的弱點是 key 命名沒有把租戶或權限維度編進去：若兩個租戶的資料共用同一個快取 key，一個租戶就能讀到另一個租戶的快取值。key 設計要讓隔離維度成為 key 的一部分，而非依賴應用層在讀取後才過濾。</p>
<p>第五層看推斷面，檢查 cache 命中與否是否會透過回應時間洩漏資訊。cache hit 與 miss 的延遲差異本身是一個 side-channel：攻擊者用一批查詢的回應時間分布，可以推斷某個帳號、商品或 slug 是否存在，即使回應內容本身有做存在性遮罩。對帳號存在性、未上架商品這類敏感判斷，要讓 hit 與 miss 的可觀察延遲一致（例如 miss 也走一段固定延遲），或把存在性判斷的快取與對外查詢路徑分離。這層在敏感資料才需要盤點，一般可重建副本不受此威脅。</p>
<h2 id="快取弱點先表現為資料錯誤而非停機">快取弱點先表現為資料錯誤而非停機</h2>
<p>快取事故的判讀重點是它的早期症狀先表現為資料錯誤，而停機往往是後續才浮現的次級症狀。價格、庫存、權限摘要若短時間錯誤，系統照常回應請求、監控的可用性指標一切正常，但回應的內容是錯的，直接造成客訴與營運損失。這種「服務還活著但說錯話」的故障比停機更難被即時發現，因為它不觸發可用性告警。</p>
<p>回源壓力缺少保護時，快取問題還會反向擴散。原本快取是用來保護資料庫的，但當 stampede 或 penetration 讓大量請求同時穿透，快取從保護層變成放大層，把一個快取層的問題擴散成資料庫與下游服務的連鎖過載。弱點盤點因此要把「快取失效時，壓力會打到哪裡」當成必答問題。</p>
<h2 id="低延遲與一致性強度的取捨">低延遲與一致性強度的取捨</h2>
<p>快取命中率越高，延遲與成本越好，同時一致性風險也越高。按資料重要性分層是平衡這個張力最穩定的做法，比對所有資料套同一套快取策略更能讓高風險資料拿到該有的一致性強度。</p>
<p>高風險資料採較短生命週期與強驗證。價格、權限、餘額這類錯誤代價高的資料，用較短 TTL（典型在數秒到數分鐘量級）縮小污染與陳舊的影響窗口，並在關鍵判斷點保留回源驗證，讓快取只承擔加速、不承擔最終正確性。低風險資料採較寬鬆策略以保留效能收益。商品描述、頭像、靜態文案這類偶爾陳舊無實質後果的資料，用較長 TTL（數十分鐘到數小時）換取更高命中率。具體秒數沒有通用值，依該資料的陳舊容忍度決定；分層的判準是「這份資料錯誤幾分鐘的代價是什麼」，代價高的往一致性傾斜，代價低的往效能傾斜。</p>
<h2 id="進入實作前要先定義的最低控制面">進入實作前要先定義的最低控制面</h2>
<p>弱點盤點的產出是一組進入實作前必須先定義清楚的控制面，缺少其中任何一項，後續的快取設計都是在未定義的安全假設上往前蓋。每一項控制面同時是一個診斷工具：可以用「若沒有它會看到什麼現象」反過來判斷現有系統是否缺這道防線。</p>
<p>快取資料分級與可接受陳舊窗口要先定義，這決定每類資料的 TTL 與是否需要回源驗證。沒有分級，所有資料會被同一套策略對待，高風險資料的陳舊窗口被低風險策略放寬。未定義的早期訊號是「所有 key 用同一個預設 TTL」「說不清哪些資料錯了會出事」。</p>
<p>失效策略與回源保護規則要先定義，這決定 stale 窗口的上界與回源洪水的防線。失效要明確是廣播、TTL 還是事件驅動，並確認廣播類失效一定有 TTL 兜底；回源要明確 negative cache、single-flight 與 rate limit 的配置。未定義的早期訊號是「失效靠廣播但沒有 TTL 兜底」「miss 尖峰會直接打到資料庫」。</p>
<p>key 命名、租戶隔離與敏感欄位限制要先定義，這決定污染與越權讀取的防線。隔離維度要編進 key、敏感欄位要在寫入快取前就遮罩，而非依賴讀取後過濾。未定義的早期訊號是「快取 key 不含租戶 id」「敏感欄位整包序列化進 cache」。</p>
<p>快取異常時的降級與回復流程要先定義，這決定事故發生時系統往哪個方向退。降級要明確是「快取失效時回源並接受延遲上升」還是「直接拒絕並保護資料庫」，並預先演練回復路徑，避免事故當下才設計。未定義的早期訊號是「沒人能回答快取掛掉時系統會怎樣」。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大量查詢不存在的 key、回源 QPS 飆</td>
          <td>cache penetration 繞過快取保護</td>
          <td>對不存在的 key 加 negative cache、回源加 rate limit</td>
      </tr>
      <tr>
          <td>兩租戶讀到彼此的快取資料</td>
          <td>key 未把租戶維度編入、隔離失效</td>
          <td>把隔離維度納入 key、敏感欄位寫入前遮罩</td>
      </tr>
      <tr>
          <td>可用性指標正常但客訴資料錯誤</td>
          <td>快取污染或陳舊，故障不觸發可用性告警</td>
          <td>補資料正確性監控、關鍵判斷點回源驗證</td>
      </tr>
      <tr>
          <td>單一熱門 key 失效造成回源尖峰</td>
          <td>hot key 失效面被流量放大</td>
          <td>對熱門 key 加 single-flight、錯開 TTL</td>
      </tr>
      <tr>
          <td>權限或價格判斷直接信任快取值</td>
          <td>快取副本被誤用成正式狀態</td>
          <td>關鍵判斷回源驗證，快取只承擔加速</td>
      </tr>
  </tbody>
</table>
<p>可用性正常但資料錯誤之所以難察覺，是因為可用性監控設計上只看系統有沒有正常回應，不掃回應內容對不對，污染或陳舊因此可以在告警全程靜默的情況下持續數小時。這類問題要靠資料正確性監控而非可用性監控才能發現，弱點盤點要確認高風險資料有對應的正確性檢查，而不是只看 latency 與 error rate。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把快取副本當成正式狀態信任，是最危險的誤區。權限、餘額、配額這類判斷若直接信任快取值而不回源驗證，污染快取就等於繞過業務規則。快取承擔加速，正式判斷的正確性要由 source of truth 保證。</p>
<p>只用廣播做失效而不設 TTL 兜底，是第二個誤區。廣播是 at-most-once，總有漏送可能，缺 TTL 時一次漏送就讓某節點長期持有污染或陳舊資料。TTL 是讓失效失敗的影響有上界的保險。</p>
<p>把租戶隔離放在讀取後過濾，是第三個誤區。若快取 key 不含租戶維度、靠應用層讀出後再過濾，任何一個漏掉過濾的讀取路徑都會洩漏跨租戶資料。隔離要編進 key，讓不同租戶在儲存層就不共用快取項。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>快取放大面的弱點盤點可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：stampede rollout regression</a> 回寫。該案例的回源洪水來自部署回歸而非惡意攻擊，放大後果相似——大量請求同時 miss、穿透到 origin、把快取從保護層變成放大層——但觸發源不同：2.C9 是意外的部署回歸，攻擊場景則是刻意查詢大量不存在的 key。後果相似讓防護有共通部分（single-flight、回源 rate limit），觸發源不同則讓防線各有重點：部署回歸重在發布保護與 warmup，惡意穿透重在 negative cache 與請求來源限制。回寫時要保留「失效時壓力打到哪裡、單飛與 rate limit 是否就位」的判讀，把它從事後復盤前移到設計階段的弱點盤點。</p>
<p>這個案例主要支撐放大面與回源保護的判讀，不直接支撐污染面或租戶隔離；若根因是跨租戶資料洩漏或快取被當正式狀態，應回到 key 設計與 source of truth 邊界，而非回源保護。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.2 的交接：失效面的策略與 TTL 兜底回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 2.7 的交接：快取副本與正式狀態的邊界回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">Cache Copy Boundary 與 Freshness</a>。</li>
<li>與 2.1 的交接：hot key 與 stampede 的放大面保護回到 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">高併發下的 Redis 讀寫邊界</a>。</li>
<li>與 6.20 的交接：弱點盤點後的故障演練與停損條件回到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取副本與正式狀態的界線如何劃分，接著讀 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary 與 Freshness</a>。要看放大面的回源保護如何在實作中成立，接著讀 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback 實作示範</a>。</p>
]]></content:encoded></item><item><title>0.6 成本、風險與選型取捨</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/</guid><description>&lt;p>成本與風險取捨的核心原則是把選型看成長期承諾。每加入一種後端服務能力，都會帶來雲端費用、人力維護、操作流程、事故風險與學習成本；它也可能降低延遲、失敗代價、開發摩擦與未來重構成本。&lt;/p>
&lt;p>這一章的內容是所有 Backend 服務實體章節的共同段落要求。後續討論 PostgreSQL、Redis、RabbitMQ、Kafka、Prometheus、Kubernetes、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a> 或任何具體服務時，都要回到同一組問題：資安限制會增加什麼成本，流量與穩定性會造成什麼壓力，伺服器與雲端費用如何成長，團隊要承擔多少操作成本，選擇這個方案會放棄哪些替代路線。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分建置成本、使用成本、操作成本與失敗代價&lt;/li>
&lt;li>用產品後果評估資料遺失、重複、延遲與停機風險&lt;/li>
&lt;li>判斷何時先用簡單設計，何時需要提前補能力&lt;/li>
&lt;li>把成本討論轉成可比較的選型問題&lt;/li>
&lt;li>在每個服務實體章節保留固定的成本與機會成本討論&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察後端選型同時改變成本與風險">【觀察】後端選型同時改變成本與風險&lt;/h2>
&lt;p>選型取捨的第一個問題是「這個能力降低哪種風險，又增加哪種成本」。資料庫、快取、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、觀測平台、部署平台與可靠性流程都能提升能力，但它們也會增加操作面積。&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>建置成本&lt;/td>
 &lt;td>開發與導入要花多少時間&lt;/td>
 &lt;td>schema、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter&lt;/a>、pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用成本&lt;/td>
 &lt;td>流量與資料量帶來多少費用&lt;/td>
 &lt;td>storage、egress、request、compute&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作成本&lt;/td>
 &lt;td>誰負責維護、升級、排障&lt;/td>
 &lt;td>backup、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、權限、容量規劃&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗代價&lt;/td>
 &lt;td>延遲、遺失、重複、停機造成什麼後果&lt;/td>
 &lt;td>付款錯誤、通知延遲、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">資料不一致&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>機會成本&lt;/td>
 &lt;td>導入這項能力會延後哪些產品工作&lt;/td>
 &lt;td>平台建設、功能交付、技術債&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資安成本&lt;/td>
 &lt;td>權限、遮罩、加密、稽核與防護帶來多少額外責任&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是成本索引。討論選型時，應把「技術是否強大」轉成「它是否值得目前承擔」。&lt;/p>
&lt;h2 id="判讀資安限制會改變成本模型">【判讀】資安限制會改變成本模型&lt;/h2>
&lt;p>資安成本的核心問題是「安全要求會讓原本的服務選型增加哪些責任」。同一個資料庫、cache、queue 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage&lt;/a>，在沒有敏感資料與有個資、金流、企業權限、稽核要求時，成本模型完全不同。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>匯出報表若包含個資，系統需要欄位遮罩、核准流程、下載期限、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a> 與存取權限。&lt;/li>
&lt;li>內部 service-to-service 呼叫若傳遞付款資料，可能需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">mTLS&lt;/a>、signed request、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential&lt;/a> rotation 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 關聯。&lt;/li>
&lt;li>客服查詢後台若能看到敏感資料，權限分級、操作稽核與資料最小揭露會成為必要成本。&lt;/li>
&lt;/ul>
&lt;p>這類取捨的核心風險是低估安全需求對操作面的影響。資安限制會增加設計、測試、稽核、教育訓練與事故處理成本；它也會降低資料外洩、權限誤用與合規事故的風險。服務章節討論選型時，必須把這兩邊一起列出。&lt;/p>
&lt;h2 id="判讀建置成本要和需求成熟度一起看">【判讀】建置成本要和需求成熟度一起看&lt;/h2>
&lt;p>建置成本的核心問題是「需求是否穩定到值得建立能力」。需求仍在探索時，過度完整的平台能力會讓修改變慢；需求已經穩定且失敗代價高時，缺少能力會讓事故與重工成本上升。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>新功能只有少量 beta 使用者，先用簡單資料模型與明確 interface 保留替換空間。&lt;/li>
&lt;li>付款流程已是正式收入來源，狀態一致性、audit、告警與回歸測試需要提前補上。&lt;/li>
&lt;li>內部報表先用每日批次匯出即可，等查詢需求穩定後再討論更完整分析平台。&lt;/li>
&lt;/ul>
&lt;p>這類取捨的陷阱是把「未來可能需要」當成現在必須導入。比較穩定的做法是先定義 interface、資料責任與測試合約，等需求成熟或風險升高，再引入具體服務能力。&lt;/p>
&lt;h2 id="判讀使用成本要看成長曲線">【判讀】使用成本要看成長曲線&lt;/h2>
&lt;p>本章的成本取捨發生在自建世界內；更早一層的成本交叉 — 託管平台月費加抽成、對上自建的工程薪資加雲端帳單 — 屬於交付形態的判斷、見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a>。&lt;/p>
&lt;p>使用成本的核心問題是「流量與資料量成長後，費用如何變化」。儲存、查詢、訊息傳遞、log、trace、egress、compute 都可能隨使用量成長。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>debug log 在小流量時成本很低，流量變大後集中式 log 費用快速增加。&lt;/li>
&lt;li>trace 全量採樣對低流量服務很方便，高流量後需要採樣與欄位控制。&lt;/li>
&lt;li>長期保存大量事件可以支援 audit，但保留期限會直接影響 storage 與查詢成本。&lt;/li>
&lt;/ul>
&lt;p>這類取捨的陷阱是只看當月帳單。成本評估要看資料保留期限、查詢頻率、尖峰流量、跨區傳輸與成長速度，並把成本上限轉成明確策略。&lt;/p>
&lt;h2 id="判讀操作成本要看團隊能否承擔">【判讀】操作成本要看團隊能否承擔&lt;/h2>
&lt;p>操作成本的核心問題是「導入後誰能維護」。一項服務能力上線後，需要監控、備份、升級、權限、容量規劃、事故處理與文件。團隊若缺少操作能力，技術本身再合適也會變成風險。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>團隊導入多種 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 後，需要同步建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a>。&lt;/li>
&lt;li>服務開始使用多個快取層後，需要同步建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">失效策略&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">資料不一致&lt;/a> 的排查方式。&lt;/li>
&lt;li>部署平台支援自動擴容後，application 需要提供 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 合約。&lt;/li>
&lt;/ul>
&lt;p>這類取捨的陷阱是只計算開發時間。操作成本常在上線後才出現，因此選型時要把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、告警、權限、備份、回復與測試環境列入範圍。&lt;/p></description><content:encoded><![CDATA[<p>成本與風險取捨的核心原則是把選型看成長期承諾。每加入一種後端服務能力，都會帶來雲端費用、人力維護、操作流程、事故風險與學習成本；它也可能降低延遲、失敗代價、開發摩擦與未來重構成本。</p>
<p>這一章的內容是所有 Backend 服務實體章節的共同段落要求。後續討論 PostgreSQL、Redis、RabbitMQ、Kafka、Prometheus、Kubernetes、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 或任何具體服務時，都要回到同一組問題：資安限制會增加什麼成本，流量與穩定性會造成什麼壓力，伺服器與雲端費用如何成長，團隊要承擔多少操作成本，選擇這個方案會放棄哪些替代路線。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分建置成本、使用成本、操作成本與失敗代價</li>
<li>用產品後果評估資料遺失、重複、延遲與停機風險</li>
<li>判斷何時先用簡單設計，何時需要提前補能力</li>
<li>把成本討論轉成可比較的選型問題</li>
<li>在每個服務實體章節保留固定的成本與機會成本討論</li>
</ol>
<hr>
<h2 id="觀察後端選型同時改變成本與風險">【觀察】後端選型同時改變成本與風險</h2>
<p>選型取捨的第一個問題是「這個能力降低哪種風險，又增加哪種成本」。資料庫、快取、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、觀測平台、部署平台與可靠性流程都能提升能力，但它們也會增加操作面積。</p>
<table>
  <thead>
      <tr>
          <th>取捨面向</th>
          <th>要回答的問題</th>
          <th>常見例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建置成本</td>
          <td>開發與導入要花多少時間</td>
          <td>schema、<a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter</a>、pipeline、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a></td>
      </tr>
      <tr>
          <td>使用成本</td>
          <td>流量與資料量帶來多少費用</td>
          <td>storage、egress、request、compute</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>誰負責維護、升級、排障</td>
          <td>backup、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、權限、容量規劃</td>
      </tr>
      <tr>
          <td>失敗代價</td>
          <td>延遲、遺失、重複、停機造成什麼後果</td>
          <td>付款錯誤、通知延遲、<a href="/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">資料不一致</a></td>
      </tr>
      <tr>
          <td>機會成本</td>
          <td>導入這項能力會延後哪些產品工作</td>
          <td>平台建設、功能交付、技術債</td>
      </tr>
      <tr>
          <td>資安成本</td>
          <td>權限、遮罩、加密、稽核與防護帶來多少額外責任</td>
          <td><a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM</a>、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a></td>
      </tr>
  </tbody>
</table>
<p>這張表是成本索引。討論選型時，應把「技術是否強大」轉成「它是否值得目前承擔」。</p>
<h2 id="判讀資安限制會改變成本模型">【判讀】資安限制會改變成本模型</h2>
<p>資安成本的核心問題是「安全要求會讓原本的服務選型增加哪些責任」。同一個資料庫、cache、queue 或 <a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a>，在沒有敏感資料與有個資、金流、企業權限、稽核要求時，成本模型完全不同。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>匯出報表若包含個資，系統需要欄位遮罩、核准流程、下載期限、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 與存取權限。</li>
<li>內部 service-to-service 呼叫若傳遞付款資料，可能需要 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">mTLS</a>、signed request、<a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a> rotation 與 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 關聯。</li>
<li>客服查詢後台若能看到敏感資料，權限分級、操作稽核與資料最小揭露會成為必要成本。</li>
</ul>
<p>這類取捨的核心風險是低估安全需求對操作面的影響。資安限制會增加設計、測試、稽核、教育訓練與事故處理成本；它也會降低資料外洩、權限誤用與合規事故的風險。服務章節討論選型時，必須把這兩邊一起列出。</p>
<h2 id="判讀建置成本要和需求成熟度一起看">【判讀】建置成本要和需求成熟度一起看</h2>
<p>建置成本的核心問題是「需求是否穩定到值得建立能力」。需求仍在探索時，過度完整的平台能力會讓修改變慢；需求已經穩定且失敗代價高時，缺少能力會讓事故與重工成本上升。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>新功能只有少量 beta 使用者，先用簡單資料模型與明確 interface 保留替換空間。</li>
<li>付款流程已是正式收入來源，狀態一致性、audit、告警與回歸測試需要提前補上。</li>
<li>內部報表先用每日批次匯出即可，等查詢需求穩定後再討論更完整分析平台。</li>
</ul>
<p>這類取捨的陷阱是把「未來可能需要」當成現在必須導入。比較穩定的做法是先定義 interface、資料責任與測試合約，等需求成熟或風險升高，再引入具體服務能力。</p>
<h2 id="判讀使用成本要看成長曲線">【判讀】使用成本要看成長曲線</h2>
<p>本章的成本取捨發生在自建世界內；更早一層的成本交叉 — 託管平台月費加抽成、對上自建的工程薪資加雲端帳單 — 屬於交付形態的判斷、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
<p>使用成本的核心問題是「流量與資料量成長後，費用如何變化」。儲存、查詢、訊息傳遞、log、trace、egress、compute 都可能隨使用量成長。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>debug log 在小流量時成本很低，流量變大後集中式 log 費用快速增加。</li>
<li>trace 全量採樣對低流量服務很方便，高流量後需要採樣與欄位控制。</li>
<li>長期保存大量事件可以支援 audit，但保留期限會直接影響 storage 與查詢成本。</li>
</ul>
<p>這類取捨的陷阱是只看當月帳單。成本評估要看資料保留期限、查詢頻率、尖峰流量、跨區傳輸與成長速度，並把成本上限轉成明確策略。</p>
<h2 id="判讀操作成本要看團隊能否承擔">【判讀】操作成本要看團隊能否承擔</h2>
<p>操作成本的核心問題是「導入後誰能維護」。一項服務能力上線後，需要監控、備份、升級、權限、容量規劃、事故處理與文件。團隊若缺少操作能力，技術本身再合適也會變成風險。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>團隊導入多種 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 後，需要同步建立 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter</a> 與 <a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a>。</li>
<li>服務開始使用多個快取層後，需要同步建立 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">失效策略</a> 與 <a href="/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">資料不一致</a> 的排查方式。</li>
<li>部署平台支援自動擴容後，application 需要提供 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 合約。</li>
</ul>
<p>這類取捨的陷阱是只計算開發時間。操作成本常在上線後才出現，因此選型時要把 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、告警、權限、備份、回復與測試環境列入範圍。</p>
<h2 id="判讀失敗代價決定保證等級">【判讀】失敗代價決定保證等級</h2>
<p>失敗代價的核心問題是「錯誤發生時產品後果是什麼」。資料遺失、<a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">重複投遞</a>、短暫不一致、延遲、<a href="/blog/backend/knowledge-cards/partial-failure/" data-link-title="Partial Failure" data-link-desc="說明分散式系統中部分依賴失效時如何保留整體可用性">partial failure</a>、<a href="/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">cascading failure</a>、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>與<a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機</a>的代價不同，對應的保證等級也不同。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>付款事件重複可能造成重複出貨或重複通知，因此 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 需要 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>。</li>
<li>聊天 typing indicator 遺失通常可接受，正式訊息遺失則需要保存與補送。</li>
<li>商品價格短暫不一致可能造成客訴，庫存短暫不一致可能造成超賣。</li>
</ul>
<p>這類取捨的陷阱是追求所有資料都最高保證。高保證通常帶來更高延遲、成本與操作複雜度；合理設計會依資料語意分級，而非把所有訊息都放進同一種可靠性模型。</p>
<h2 id="判讀機會成本決定投入順序">【判讀】機會成本決定投入順序</h2>
<p>機會成本的核心問題是「做這件事會延後什麼」。後端能力建設很容易變成長期平台工程；它可能值得，也可能讓產品驗證變慢。投入順序要跟風險、成長與團隊能力對齊。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>產品仍在找市場定位時，先用清楚邊界保留替換空間，比導入完整事件平台更實際。</li>
<li>服務已經有穩定收入且事故頻繁時，補 observability、<a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract</a> 與 reliability pipeline 會直接降低業務風險。</li>
<li>流量即將進入大型活動前，先做 <a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a>、容量預估與降級策略，比重構所有資料層更有時效。</li>
</ul>
<p>這類取捨的陷阱是把架構完整度當成目標。選型應回答目前最需要降低哪個風險，並設計能回頭修正的邊界。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入具體服務取捨與落地章節：</p>
<ol>
<li>成本維度是否完整（建置、使用、操作、資安、機會成本）</li>
<li>失敗代價是否分級（遺失、重複、延遲、<a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機</a>）</li>
<li>團隊可承擔的操作責任是否明確（runbook、告警、備份、回復）</li>
<li>何時重評選型的條件是否明確（流量、法規、事故頻率）</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01-database</a></li>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03-message-queue</a></li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07-security-data-protection</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>後端服務選型要同時看成本與風險。建置成本要看需求成熟度，使用成本要看成長曲線，操作成本要看團隊能否承擔，失敗代價決定保證等級，機會成本決定投入順序。這些取捨清楚後，後續討論具體服務才會有共同標準。</p>
]]></content:encoded></item><item><title>8.6 演練與值班能力建設</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day&lt;/a> design&lt;/li>
&lt;li>scenario library&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> training&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>演練與值班能力建設是把事故反應從個人經驗變成團隊能力的流程，責任是讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 在真事故來臨前先看過類似情境。&lt;/p>
&lt;p>這一頁處理的是反應能力，不是單次知識傳遞。沒有演練，交接會停在「知道有這件事」，不會變成「知道怎麼做」。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 時，先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day&lt;/a> 是否接近真實情境，再看升級路徑是否可執行。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>drills 是否涵蓋常見事故型態&lt;/li>
&lt;li>shadowing 是否讓新人接觸真實決策節奏&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> tree 是否有可達性與最新 owner&lt;/li>
&lt;li>演練結果是否回寫成改善項&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">Google&lt;/a>：可靠性文化常先從演練習慣建立。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/" data-link-title="Netflix" data-link-desc="Netflix Chaos Engineering 起源：Simian Army / FIT / 規模化故障注入">Netflix&lt;/a>：大規模系統需要把故障反應變成肌肉記憶。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：訊息平台的 oncall 需要熟悉高壓通訊節奏。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>08.2 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> / role分工：演練時的責任分派&lt;/li>
&lt;li>08.4 通訊與狀態：演練時 update cadence&lt;/li>
&lt;li>08.12 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a>：長事故接班節奏&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day&lt;/a> 一年一次、無常態演練節奏&lt;/li>
&lt;li>新值班無 onboarding、靠生事故學&lt;/li>
&lt;li>scenario library 過期、跟現況架構脫鉤&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> metric 不存在、值班品質靠主觀評斷&lt;/li>
&lt;li>drill 結束後無 action items、學習未沉澱回 runbook&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>06.7 DR / rollback rehearsal：DR 演練回饋值班訓練&lt;/li>
&lt;li>08.12 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a>：handoff 演練&lt;/li>
&lt;li>08.16 runbook lifecycle：演練是 runbook 有效性證明&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a> design</li>
<li>scenario library</li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> training</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a></li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>演練與值班能力建設是把事故反應從個人經驗變成團隊能力的流程，責任是讓 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 在真事故來臨前先看過類似情境。</p>
<p>這一頁處理的是反應能力，不是單次知識傳遞。沒有演練，交接會停在「知道有這件事」，不會變成「知道怎麼做」。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 時，先看 <a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a> 是否接近真實情境，再看升級路徑是否可執行。</p>
<p>重點訊號包括：</p>
<ul>
<li>drills 是否涵蓋常見事故型態</li>
<li>shadowing 是否讓新人接觸真實決策節奏</li>
<li><a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> tree 是否有可達性與最新 owner</li>
<li>演練結果是否回寫成改善項</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">Google</a>：可靠性文化常先從演練習慣建立。</li>
<li><a href="/blog/backend/06-reliability/cases/netflix/" data-link-title="Netflix" data-link-desc="Netflix Chaos Engineering 起源：Simian Army / FIT / 規模化故障注入">Netflix</a>：大規模系統需要把故障反應變成肌肉記憶。</li>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：訊息平台的 oncall 需要熟悉高壓通訊節奏。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>08.2 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> / role分工：演練時的責任分派</li>
<li>08.4 通訊與狀態：演練時 update cadence</li>
<li>08.12 <a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a>：長事故接班節奏</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a> 一年一次、無常態演練節奏</li>
<li>新值班無 onboarding、靠生事故學</li>
<li>scenario library 過期、跟現況架構脫鉤</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> metric 不存在、值班品質靠主觀評斷</li>
<li>drill 結束後無 action items、學習未沉澱回 runbook</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>06.7 DR / rollback rehearsal：DR 演練回饋值班訓練</li>
<li>08.12 <a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a>：handoff 演練</li>
<li>08.16 runbook lifecycle：演練是 runbook 有效性證明</li>
</ul>
]]></content:encoded></item><item><title>Momento</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/momento/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/momento/</guid><description>&lt;p>Momento 是 serverless cache 服務、承擔三個責任：把 cache 變成一個按用量計費的 API（沒有 node、沒有 cluster、不規劃容量）、自動隨流量 scale（尖峰自動擴、閒置不付固定費）、提供原生 SDK 與 Redis / Memcached 相容介面（既有 client 可遷）。設計取捨偏向「把 cache 的容量規劃與維運完全消除、用計費換掉 sizing」、是不想養 cache 叢集又要彈性的選項。&lt;/p>
&lt;p>對「流量不可預測、不想規劃容量與 sizing、團隊沒有 cache 運維資源」這條路徑、Momento 是 serverless 方向的代表。它跟自管 Redis、managed cache 的上層取捨（自管 vs managed vs serverless vs BaaS bundle）見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;blockquote>
&lt;p>本頁的計費、limit 與功能宣稱以 &lt;a href="https://docs.momentohq.com/">Momento 官方文件&lt;/a> 與 &lt;a href="https://www.gomomento.com/pricing/">Momento 定價&lt;/a> 為準、最後檢查日 2026-06-16。Momento 是 SaaS、需帳號與 API key、無法本機 docker 驗證、指令為依官方文件的範例。&lt;/p>&lt;/blockquote>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>理解 serverless cache 跟 node-based / managed cache 的計費與維運差異&lt;/li>
&lt;li>評估按用量計費（per request + data transfer）對你的流量形狀划不划算&lt;/li>
&lt;li>判斷 Momento 原生 SDK vs Redis 相容介面的遷移路徑&lt;/li>
&lt;li>區分 Momento 跟 ElastiCache Serverless 的定位差異&lt;/li>
&lt;li>判斷哪些 cache 場景適合 serverless、哪些該回 node-based&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑用-sdk-連-momento">最短路徑：用 SDK 連 Momento&lt;/h2>





&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"># 1. 在 Momento Console 建 cache + 取得 API key（無 node / cluster 配置）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"># 2. 用語言 SDK（以 pseudo-code 示意、實際 API 以官方 SDK 文件為準）
&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">client = CacheClient(api_key, default_ttl=60s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">client.set(&amp;#34;my-cache&amp;#34;, &amp;#34;foo&amp;#34;, &amp;#34;bar&amp;#34;) # 寫入、TTL 內有效
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">client.get(&amp;#34;my-cache&amp;#34;, &amp;#34;foo&amp;#34;) # → &amp;#34;bar&amp;#34;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑的重點是「沒有 endpoint / node / sizing 要配」——建 cache 是一個 API 動作、不是 provision 一台機器。實際 SDK 介面以 &lt;a href="https://docs.momentohq.com/">Momento SDK 文件&lt;/a> 為準。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="sdk-與相容介面">SDK 與相容介面&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>原生 SDK（多語言）：gRPC-based、Momento 自有 API&lt;/li>
&lt;li>Redis / Memcached 相容介面：既有 Redis / Memcached client 可遷（相容範圍以官方為準、要驗證）&lt;/li>
&lt;li>沒有 redis-cli 等價的 server 操作（serverless 無 server 可登入）&lt;/li>
&lt;/ul>
&lt;h3 id="計費模型核心決策">計費模型（核心決策）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Momento 是 serverless cache 服務、承擔三個責任：把 cache 變成一個按用量計費的 API（沒有 node、沒有 cluster、不規劃容量）、自動隨流量 scale（尖峰自動擴、閒置不付固定費）、提供原生 SDK 與 Redis / Memcached 相容介面（既有 client 可遷）。設計取捨偏向「把 cache 的容量規劃與維運完全消除、用計費換掉 sizing」、是不想養 cache 叢集又要彈性的選項。</p>
<p>對「流量不可預測、不想規劃容量與 sizing、團隊沒有 cache 運維資源」這條路徑、Momento 是 serverless 方向的代表。它跟自管 Redis、managed cache 的上層取捨（自管 vs managed vs serverless vs BaaS bundle）見 <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>。</p>
<blockquote>
<p>本頁的計費、limit 與功能宣稱以 <a href="https://docs.momentohq.com/">Momento 官方文件</a> 與 <a href="https://www.gomomento.com/pricing/">Momento 定價</a> 為準、最後檢查日 2026-06-16。Momento 是 SaaS、需帳號與 API key、無法本機 docker 驗證、指令為依官方文件的範例。</p></blockquote>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>理解 serverless cache 跟 node-based / managed cache 的計費與維運差異</li>
<li>評估按用量計費（per request + data transfer）對你的流量形狀划不划算</li>
<li>判斷 Momento 原生 SDK vs Redis 相容介面的遷移路徑</li>
<li>區分 Momento 跟 ElastiCache Serverless 的定位差異</li>
<li>判斷哪些 cache 場景適合 serverless、哪些該回 node-based</li>
</ol>
<h2 id="最短路徑用-sdk-連-momento">最短路徑：用 SDK 連 Momento</h2>





<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. 在 Momento Console 建 cache + 取得 API key（無 node / cluster 配置）
</span></span><span class="line"><span class="ln">2</span><span class="cl"># 2. 用語言 SDK（以 pseudo-code 示意、實際 API 以官方 SDK 文件為準）
</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">client = CacheClient(api_key, default_ttl=60s)
</span></span><span class="line"><span class="ln">5</span><span class="cl">client.set(&#34;my-cache&#34;, &#34;foo&#34;, &#34;bar&#34;)     # 寫入、TTL 內有效
</span></span><span class="line"><span class="ln">6</span><span class="cl">client.get(&#34;my-cache&#34;, &#34;foo&#34;)            # → &#34;bar&#34;</span></span></code></pre></div><p>最短路徑的重點是「沒有 endpoint / node / sizing 要配」——建 cache 是一個 API 動作、不是 provision 一台機器。實際 SDK 介面以 <a href="https://docs.momentohq.com/">Momento SDK 文件</a> 為準。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="sdk-與相容介面">SDK 與相容介面</h3>
<p>子議題：</p>
<ul>
<li>原生 SDK（多語言）：gRPC-based、Momento 自有 API</li>
<li>Redis / Memcached 相容介面：既有 Redis / Memcached client 可遷（相容範圍以官方為準、要驗證）</li>
<li>沒有 redis-cli 等價的 server 操作（serverless 無 server 可登入）</li>
</ul>
<h3 id="計費模型核心決策">計費模型（核心決策）</h3>
<p>子議題：</p>
<ul>
<li>按用量計費：data transfer（傳輸量）+ 可能的 request / storage 維度（以官方定價為準）</li>
<li>無固定 node 費用：閒置時段不付 idle node 的錢</li>
<li>流量尖峰自動 scale：不需預留容量、但尖峰量直接反映在帳單</li>
</ul>
<h3 id="沒有容量規劃">沒有容量規劃</h3>
<p>子議題：</p>
<ul>
<li>不選 node type、不設 maxmemory、不規劃 shard</li>
<li>scaling 由 Momento 處理、application 端不感知</li>
<li>代價：失去對底層的控制（無法調 eviction policy 等 server 參數）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="serverless-計費的甜蜜點與陷阱">Serverless 計費的甜蜜點與陷阱</h3>
<p>子議題：</p>
<ul>
<li>甜蜜點：流量不可預測、有大量閒置時段、不想為峰值預留容量</li>
<li>陷阱：穩態高流量下、按用量可能比 node-based + Reserved Instance 貴</li>
<li>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache Serverless 的計費踩坑</a> 同類議題、access pattern 低效會推高帳單</li>
</ul>
<h3 id="momento-vs-elasticache-serverless">Momento vs ElastiCache Serverless</h3>
<p>子議題：</p>
<ul>
<li>Momento：cache-as-API、完全 serverless、跨雲（不綁單一 cloud）</li>
<li>ElastiCache Serverless：AWS 生態內的 node 抽象、仍是 ElastiCache engine、綁 AWS</li>
<li>選擇：要完全擺脫容量規劃 + 跨雲 → Momento；已在 AWS 生態 + 要 engine 控制 → ElastiCache</li>
</ul>
<h3 id="遷移與相容性驗證">遷移與相容性驗證</h3>
<p>子議題：</p>
<ul>
<li>從 Redis / Memcached 遷 Momento：用相容介面或改用原生 SDK</li>
<li>相容範圍要逐項驗證（serverless 不支援 server-side 操作如 SCAN 全庫、Lua 等、以官方為準）</li>
<li>失去的能力：server 參數調校、自管 persistence、module</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="帳單超出預期">帳單超出預期</h3>
<p>操作原則：serverless 帳單反映實際用量、先看 data transfer 與 request 量。判讀：access pattern 低效（大量小請求、大 value）會推高、用批次 / 合併降量；穩態高流量重新評估 node-based。</p>
<h3 id="延遲比自管高">延遲比自管高</h3>
<p>操作原則：serverless cache 多一層 API gateway / 跨網路、延遲可能高於同 VPC 的自管 Redis。判讀：latency-sensitive 且穩態高流量的場景、評估自管或 managed node-based。</p>
<h3 id="相容介面行為差異">相容介面行為差異</h3>
<p>操作原則：Redis 相容介面不等於 100% Redis、server-side 操作可能不支援。判讀：對照官方相容清單、用到的命令逐一驗證。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>穩態高流量、成本敏感</td>
          <td>node-based <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> + Reserved Instance</td>
      </tr>
      <tr>
          <td>需要 server 參數 / eviction 控制</td>
          <td>自管 Redis / <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a></td>
      </tr>
      <tr>
          <td>已在 AWS 生態</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache Serverless</a>（同生態）</td>
      </tr>
      <tr>
          <td>需要 Redis data types / module</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（完整 data types）</td>
      </tr>
      <tr>
          <td>process-local 極低延遲</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a>（JVM 內、無網路）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Momento 完整 SDK API（各語言、以官方文件為準）</li>
<li>詳細計費計算（以官方定價為準）</li>
<li>Redis / Memcached 相容介面的完整相容矩陣</li>
<li>Momento Topics（pub/sub）等 cache 以外的產品線</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照本模組-case-庫暫無-momento-specific-case">跨 vendor 對照（本模組 case 庫暫無 Momento-specific case）</h3>
<p>Momento 是較新的 serverless cache、本 blog 的 cache case 庫（Meta / Shopify / Netflix / Cloudflare / Tinder / Tubi / Snap）暫無 Momento production case。以下用 serverless 的角度對照既有 case 提供判讀。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Momento 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>serverless 也會 stampede、client-side jitter / singleflight 仍要自己做</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a></td>
          <td>「feature 可重算才選 cache」的判斷對 serverless 一樣適用、不可重建走 durable</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>serverless 適合早期 / 不可預測流量、規模穩定後評估 node-based 成本</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Momento-specific 案例</strong>：serverless cache 的成本與彈性 production 個案、從 ElastiCache 遷 Momento 的成本對照、不可預測流量場景的採用分享。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游能力：<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>（自管 vs managed vs serverless）、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（Serverless 選項）、<a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a>（另一端：process-local）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
</ul>
]]></content:encoded></item><item><title>AWS CloudHSM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudhsm/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudhsm/</guid><description>&lt;p>AWS CloudHSM 是 &lt;em>single-tenant dedicated HSM&lt;/em> 服務（FIPS 140-2 Level 3）、客戶獨享一個 HSM cluster、AWS 提供 &lt;em>硬體 + network + provisioning&lt;/em>、客戶自己管 &lt;em>crypto user / partition / key custody / backup&lt;/em>。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">AWS KMS&lt;/a> 是 &lt;em>不同信任模型&lt;/em> — KMS 是 multi-tenant managed、AWS 持有 key custody 與 API plane；CloudHSM 上 &lt;em>AWS 看不到 key、也不能 reset Crypto User password&lt;/em>、客戶丟了 credential 等於 key 永久遺失。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>CloudHSM 的核心定位是 &lt;em>把 cryptographic root of trust 放回客戶手上&lt;/em> — 適合金融、政府、醫療這類有資料主權、FIPS 140-2 Level 3、PCI HSM、HIPAA 合規壓力的場景。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">AWS KMS&lt;/a> 比、KMS 也滿足 FIPS 140-2 Level 3、但 &lt;em>HSM cluster 是 AWS 多租戶共用&lt;/em>、key material 由 AWS-controlled HSM 持有、控制面 API 也是 AWS。CloudHSM 把 HSM cluster 物理隔離給單一客戶、PKCS#11 / JCE / OpenSSL Dynamic Engine 直接打 HSM、AWS 在資料平面 &lt;em>沒有讀 key 的能力&lt;/em>。&lt;/p>
&lt;p>跟 &lt;em>自管 on-prem HSM&lt;/em>（SafeNet / Thales 自架）比、CloudHSM 把硬體採購、機房、network、firmware patch 交還 AWS、客戶只管 key custody 跟 Crypto User policy；代價是不能完全脫離 AWS region。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault auto-unseal&lt;/a> 整合場景中、CloudHSM 是 &lt;em>Vault master key 的 root custodian&lt;/em> — Vault unseal key 用 CloudHSM 加密、CloudHSM 出事整個 Vault cluster 沒法 unseal、所以可用性設計（cross-AZ cluster、cross-region backup）很關鍵。多數一般 web app / SaaS 用 KMS 即可、不需要 CloudHSM 的物理隔離。&lt;/p></description><content:encoded><![CDATA[<p>AWS CloudHSM 是 <em>single-tenant dedicated HSM</em> 服務（FIPS 140-2 Level 3）、客戶獨享一個 HSM cluster、AWS 提供 <em>硬體 + network + provisioning</em>、客戶自己管 <em>crypto user / partition / key custody / backup</em>。它跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> 是 <em>不同信任模型</em> — KMS 是 multi-tenant managed、AWS 持有 key custody 與 API plane；CloudHSM 上 <em>AWS 看不到 key、也不能 reset Crypto User password</em>、客戶丟了 credential 等於 key 永久遺失。</p>
<h2 id="服務定位">服務定位</h2>
<p>CloudHSM 的核心定位是 <em>把 cryptographic root of trust 放回客戶手上</em> — 適合金融、政府、醫療這類有資料主權、FIPS 140-2 Level 3、PCI HSM、HIPAA 合規壓力的場景。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> 比、KMS 也滿足 FIPS 140-2 Level 3、但 <em>HSM cluster 是 AWS 多租戶共用</em>、key material 由 AWS-controlled HSM 持有、控制面 API 也是 AWS。CloudHSM 把 HSM cluster 物理隔離給單一客戶、PKCS#11 / JCE / OpenSSL Dynamic Engine 直接打 HSM、AWS 在資料平面 <em>沒有讀 key 的能力</em>。</p>
<p>跟 <em>自管 on-prem HSM</em>（SafeNet / Thales 自架）比、CloudHSM 把硬體採購、機房、network、firmware patch 交還 AWS、客戶只管 key custody 跟 Crypto User policy；代價是不能完全脫離 AWS region。跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault auto-unseal</a> 整合場景中、CloudHSM 是 <em>Vault master key 的 root custodian</em> — Vault unseal key 用 CloudHSM 加密、CloudHSM 出事整個 Vault cluster 沒法 unseal、所以可用性設計（cross-AZ cluster、cross-region backup）很關鍵。多數一般 web app / SaaS 用 KMS 即可、不需要 CloudHSM 的物理隔離。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>何時需要 CloudHSM 的 dedicated 模型、何時 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> 已足夠</li>
<li>CloudHSM cluster 的最低安全 / 可用性需求（cross-AZ、Crypto Officer 分離、Quorum、backup）</li>
<li>Crypto User credential 出事的降級路徑（AWS 不能幫忙、靠 backup + Quorum）</li>
<li>跟 <a href="https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html">KMS Custom Key Store</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault auto-unseal</a> 整合的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 CloudHSM deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Cluster 拓樸</strong>：production cluster 是否至少 2 個 HSM instance 跨 AZ、cluster 內自動 replicate、單一 AZ 故障時 key 是否仍可用</li>
<li><strong>Crypto User 管理</strong>：Crypto Officer（CO）跟 Crypto User（CU）是否分離、CO password 是否走 break-glass 保管、CU credential 是否走 short-lived 取得 + audit</li>
<li><strong>Quorum-based policy</strong>：高敏 operation（建 CU、改 policy、key export wrapped）是否設 M-of-N approval、避免單一 admin compromise 後 silent abuse</li>
<li><strong>Backup 治理</strong>：automatic 24h backup 跟 manual backup 是否都開、cross-region backup 是否走 explicit copy、restore 流程是否定期演練</li>
</ul>
<p>四件事任一缺失、就是 CloudHSM deployment 待補項目 — 跟 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a> 的 evidence 邊界同類。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Cluster + HSM Instance 拓樸</strong>：CloudHSM 的部署單位是 <em>cluster</em>、cluster 內可以有 1-N 個 <em>HSM instance</em>。production 場景至少 2 個 HSM instance 跨 AZ、cluster 自動把 key material replicate 在所有 instance 上、單一 AZ 失效不影響 cryptographic operation。跨 region 不自動 replicate — 跨 region DR 要靠 backup copy。</p>
<p><strong>Crypto Officer (CO) vs Crypto User (CU)</strong>：CO 是 cluster 管理員、能建 / 刪 CU、設 policy、做 backup；CU 是真的做 cryptographic operation 的 identity（encrypt / decrypt / sign / verify）。production 必須分離 — CO credential 走 break-glass 保管、CU credential 給 application 使用、application compromise 只影響 CU 邊界、不能改 CO policy。</p>
<p><strong>Quorum-based policy（M-of-N approval）</strong>：CloudHSM 支援把高敏操作（建 CU、改 policy、key export wrapped）綁定 <em>M-of-N CO approval</em>。例如 3-of-5 quorum、單一 CO 即使 credential 外洩也不能單獨建後門 CU、必須拿到另外 2 個 CO 的 signed token。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558 signing key chain</a> 啟示：高價值 key custodian 的 admin operation 不該是 <em>單人單 token</em>、必須有第二人簽核才能改變信任根。</p>
<p><strong>Backup 治理</strong>：CloudHSM 每 24 小時自動 backup 整個 cluster state（含 key material）、backup 是 AWS-managed encrypted blob、AWS 自己也不能解密、restore 必須在 CloudHSM cluster context 內進行。可手動 backup、可 copy 到其他 region 做 DR。Backup retention 預設 90 天、可延長。Backup 不是 <em>export</em> — 不能把 key material 從 HSM 拿出來看 plaintext。</p>
<p><strong>Key Replication 跨 region</strong>：CloudHSM cluster 綁定單一 AWS region、跨 region 走 <em>backup → copy → restore</em> 流程、不是 active replication。設計 DR 時要算 RTO：restore 一個 cluster 從 backup 大約小時級、不適合 hot failover、應該 <em>primary region 跑、DR region 備好空 cluster + backup copy</em>。</p>
<p><strong>PKCS#11 / JCE / OpenSSL Dynamic Engine 整合</strong>：application 不用 AWS SDK 講 CloudHSM、而是透過 <em>標準 cryptographic API library</em>（PKCS#11 for C/C++、JCE Provider for Java、OpenSSL Dynamic Engine 走 TLS termination）。好處是 <em>application code 用業界標準介面</em>、未來換 HSM 廠也只需要換 library。代價是 client SDK 要裝在 application host、CU credential 要 deploy 到 host、host security baseline 變成 cryptographic boundary 的一部分。</p>
<p><strong>跟 KMS Custom Key Store 整合</strong>：<a href="https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html">KMS Custom Key Store</a> 把 KMS Key 的 <em>backing material 放在 CloudHSM</em>、API 仍透過 KMS（<code>kms:Encrypt</code> / <code>kms:Decrypt</code>）、application code 不需要改。這是 <em>KMS 易用 + HSM dedicated 雙重</em>：保留 KMS 的 IAM policy / key rotation / audit log（CloudTrail）、又得到 single-tenant HSM 的合規屬性。代價是 CloudHSM 失效時、Custom Key Store backing 的 KMS Key 全部不可用、需要監控 cluster health。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS CloudHSM</th>
          <th>AWS KMS</th>
          <th>Azure Managed HSM</th>
          <th>Google Cloud HSM</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>Single-tenant dedicated cluster</td>
          <td>Multi-tenant managed</td>
          <td>Single-tenant pool</td>
          <td>HSM-backed Cloud KMS（Protection Level=HSM）</td>
      </tr>
      <tr>
          <td>FIPS 140-2</td>
          <td>Level 3（dedicated）</td>
          <td>Level 3（shared cluster）</td>
          <td>Level 3</td>
          <td>Level 3</td>
      </tr>
      <tr>
          <td>AWS / 雲廠持 key？</td>
          <td>不持（CU credential 客戶獨有）</td>
          <td>持（managed key custody）</td>
          <td>不持（HSM admin 客戶獨有）</td>
          <td>不持 plaintext key material</td>
      </tr>
      <tr>
          <td>整合介面</td>
          <td>PKCS#11 / JCE / OpenSSL</td>
          <td>AWS SDK / CLI / KMS API</td>
          <td>Key Vault SDK / REST</td>
          <td>Cloud KMS API</td>
      </tr>
      <tr>
          <td>Quorum 多人簽核</td>
          <td>內建（M-of-N）</td>
          <td>透過 IAM policy + organization SCP</td>
          <td>RBAC + Privileged Identity Management</td>
          <td>IAM Condition + organization policy</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>高 — 自管 CU credential / patch / topology</td>
          <td>低</td>
          <td>中</td>
          <td>低</td>
      </tr>
      <tr>
          <td>合規憑證</td>
          <td>FIPS 140-2 L3 + PCI HSM + Common Criteria</td>
          <td>FIPS 140-2 L3 + PCI DSS</td>
          <td>FIPS 140-2 L3 + Common Criteria</td>
          <td>FIPS 140-2 L3</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>金融 / 政府 / 醫療、需要物理隔離 + AWS 不持 key</td>
          <td>一般 AWS-heavy workload、需要 IAM 整合</td>
          <td>Azure-heavy + 合規壓力</td>
          <td>GCP-heavy + 合規壓力</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — backup 跨廠不可移植、key 不能 export</td>
          <td>中</td>
          <td>中</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<p>選 CloudHSM 的核心訴求：<em>合規明文要求 dedicated HSM</em>（PCI HSM、某些國家資料主權法規）、或 <em>trust model 上不接受 AWS 持 key</em>。多數 AWS-heavy workload 用 KMS 即可、加 CloudHSM 反而引入 <em>Crypto User credential 的單點失誤</em>（丟了 = key 永久遺失）。需要 KMS API 但又要 dedicated HSM、走 <a href="https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html">Custom Key Store</a> 是折衷路徑。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Quorum Auth 設計</strong>：production 把 Quorum threshold 設為 <em>3-of-5</em> 或 <em>2-of-3</em>、五位 CO 由不同部門 / 不同地理位置持有、避免單一辦公室 / 單一網路同時被攻陷。Quorum token 有 TTL、單次 operation 用完就失效、防止 replay。建議 quarterly 演練：模擬一個 CO 不在、用剩餘 quorum 完成 emergency operation、驗證流程在事故時跑得通。</p>
<p><strong>KMS Custom Key Store 整合決策</strong>：用 Custom Key Store 的關鍵問題是 <em>availability blast radius</em> — KMS Key 出事影響範圍是 <em>使用該 Key 的 AWS service</em>（S3、EBS、RDS encryption）、Custom Key Store backing 失效會讓這些 service 同步斷。設計時做 <em>分層 key strategy</em>：mass volume 的 S3 / EBS 用 AWS-managed KMS Key、高合規敏感的 database / secret 才用 Custom Key Store backing 的 KMS Key、降低單一 cluster 失效的影響面。</p>
<p><strong>Cross-Region Backup</strong>：DR 要把 backup copy 到第二個 region、走 <code>CopyBackupToRegion</code> API、restore 時建空 cluster + 套 backup。整個 RTO 通常數小時、不適合熱備、設計上是 <em>容忍小時級 outage 換到 BCDR 環境</em>、不是 <em>秒級 failover</em>。對應 <a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a> 對照啟示：身份 / 加密控制面的單點 outage 影響整個 platform、availability 的 topology 設計跟 confidentiality 同等重要。</p>
<p><strong>跟 Vault auto-unseal 整合</strong>：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> auto-unseal 可用 CloudHSM 作 master key custodian、走 PKCS#11 plugin、Vault unseal 時呼叫 CloudHSM <code>Unwrap</code> master key。比起 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS auto-unseal</a> 多一層 dedicated HSM 保證、適合監管特別嚴的場景。代價是 CloudHSM cluster 失效 → Vault 不能 unseal → 下游所有 secret 拿不到、要設計 break-glass 流程。</p>
<p><strong>合規憑證</strong>：CloudHSM 同時持有 FIPS 140-2 Level 3、PCI HSM、Common Criteria EAL4+ 多個認證、可作金融 PIN block 處理、payment 業者的 HSM 上鏈、政府機敏資料加密的 <em>直接合規承諾</em>、不需要客戶端再做 HSM 認證 audit。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Crypto User credential 丟失</strong>：CU password 全公司只有一份、保管人離職 → AWS <em>不能 reset</em>、key material 永久不可用 — CU credential 要走 password manager + 多人持有、CO 有能力 revoke 舊 CU 建新 CU</li>
<li><strong>Cluster 只有單一 HSM instance</strong>：成本省了、單一 instance 故障 cluster 整個失效 — production 強制至少 2 個 instance、跨 AZ</li>
<li><strong>Backup 沒測過 restore</strong>：每天 automatic backup 跑、從未 restore 演練、DR 真要用時發現流程不通 — quarterly 演練 restore 到測試 cluster、驗證 key material 可用</li>
<li><strong>Custom Key Store 沒監控 CloudHSM health</strong>：CloudHSM cluster degraded 時、KMS Custom Key Store 跟著失效、application 看到 KMS 5xx — CloudWatch metric 監 <code>HsmsActive</code> / <code>HsmTemperature</code>、cluster health degrade 立即 alert</li>
<li><strong>PKCS#11 library 版本漂移</strong>：application host 的 client SDK 版本跟 cluster firmware 不相容、cryptographic operation 失敗 — version compatibility matrix 進 deployment pipeline、firmware upgrade 前先測 staging</li>
<li><strong>Quorum CO 全部同地點</strong>：5 個 CO 全在同一個辦公室、辦公室斷網 = quorum 不能組 — CO 跨 region / 跨組織分散</li>
<li><strong>Audit log 沒接 SIEM</strong>：CloudHSM activity 透過 CloudTrail + cluster audit log、沒接 SIEM 就無 forensic — CloudTrail 跟 cluster audit 都 push 到 SIEM（見 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>）</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般 AWS workload 加密、無 dedicated 合規</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a></td>
      </tr>
      <tr>
          <td>Azure-heavy + dedicated HSM 合規需求</td>
          <td>Azure Managed HSM（見上方對照表）</td>
      </tr>
      <tr>
          <td>GCP-heavy + dedicated HSM 合規需求</td>
          <td>Google Cloud HSM（Cloud KMS Protection Level=HSM）</td>
      </tr>
      <tr>
          <td>Secret storage + dynamic credential</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a></td>
      </tr>
      <tr>
          <td>Certificate / PKI（不是 key custody）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> / <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>跨雲 unified key custody</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> transit engine（雲廠中立）</td>
      </tr>
      <tr>
          <td>Key rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>CloudHSM 完整 PKCS#11 / JCE API reference</li>
<li>CloudHSM Classic（舊版、已 EOL）的差異</li>
<li>每種合規法規（PCI HSM、HIPAA、FedRAMP）的逐條對應</li>
<li>CloudHSM CLI 跟 <code>cloudhsm_mgmt_util</code> 詳細指令</li>
<li>應用層使用 HSM-bound key 做 TLS termination 的 nginx / Apache 配置細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>CloudHSM 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 CloudHSM 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>核心對照 — CloudHSM 設計 <em>AWS 不持 key + key 不能 export</em> 是 Storm-0558 反設計、攻擊者進 cluster 也搬不走 key material、Quorum policy 阻單一 admin compromise</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>CloudHSM key rotation 需要應用層配合 key alias 切換、不像 KMS 自動 rotation；scope map 跟雙軌驗證窗口更明顯、PKCS#11 client 散落 host 群時 rotation 要分批</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>對照啟示 — HSM cluster 是 single point of compromise、cross-AZ topology + cross-region backup 是 <em>availability</em> 的設計依據、不是 confidentiality</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>（HSM 為 CA / signing key 的 FIPS-grade root custodian）、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>整合：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（CloudHSM 作為 Vault auto-unseal master key custodian）</li>
<li>整合：<a href="https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html">KMS Custom Key Store</a>（KMS API + CloudHSM backing 雙重）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（HSM 失效如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/cloudhsm/">AWS CloudHSM Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Azure RBAC + Entra ID</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/</guid><description>&lt;p>Azure 的身份與權限體系是 &lt;em>雙層&lt;/em> — Entra ID（前 Azure AD）是 IdP，承擔人類與 workload 的身份來源、SSO、MFA 與 Conditional Access；Azure RBAC 是 cloud resource 的 permission engine，把 role 指派到 scope（Management Group / Subscription / Resource Group / Resource）上的 principal。兩層責任不同、設定介面不同、出事故時的徵兆也不同 — 把兩者寫成同一件事是 Azure 治理最常見的混淆來源。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Entra ID 是 &lt;em>Microsoft 自有的 workforce IdP&lt;/em>、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 是直接競爭者。M365 / Azure-heavy 的組織通常直接用 Entra ID 當主 IdP；Okta-first 的組織可以把 Entra ID 當下游 SP（federation）、也可以雙 IdP 並存、但雙 IdP 的 break-glass 跟 lifecycle 路徑要重新設計。Entra ID 同時承擔 &lt;em>consumer-side 跟 partner-side 的 multi-tenant app&lt;/em> 信任、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0&lt;/a> 在 B2C 場景有交集。&lt;/p>
&lt;p>Azure RBAC 是 cloud resource permission engine、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> 同層 — 都在解「身份對 cloud resource 能做什麼」。差異在 &lt;em>scope hierarchy&lt;/em> — Azure 用 Management Group → Subscription → Resource Group → Resource 四層繼承、AWS 用 account + organization、Google 用 organization → folder → project。Azure RBAC 預期 &lt;em>role assignment 沿 scope 向下繼承&lt;/em>、這跟 AWS 在每個 account 重新指派的習慣不一樣、跨雲團隊轉過來常踩到。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>哪一段控制屬於 Entra ID（身份）、哪一段屬於 Azure RBAC（resource permission）、不要把兩層當同一件事&lt;/li>
&lt;li>Entra ID tenant 的最低稽核需求（Global Admin、App Registration、Conditional Access、Managed Identity）&lt;/li>
&lt;li>Azure RBAC 的 scope 設計、Custom Role 跟 PIM 何時必要&lt;/li>
&lt;li>Entra ID 控制面事故的降級路徑、跟 Azure RBAC 出事的徵兆差異&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Azure 雙層體系是否健康、要分兩層各看兩件事、跟「日常操作與決策形狀」段的兩層結構對齊。&lt;/p></description><content:encoded><![CDATA[<p>Azure 的身份與權限體系是 <em>雙層</em> — Entra ID（前 Azure AD）是 IdP，承擔人類與 workload 的身份來源、SSO、MFA 與 Conditional Access；Azure RBAC 是 cloud resource 的 permission engine，把 role 指派到 scope（Management Group / Subscription / Resource Group / Resource）上的 principal。兩層責任不同、設定介面不同、出事故時的徵兆也不同 — 把兩者寫成同一件事是 Azure 治理最常見的混淆來源。</p>
<h2 id="服務定位">服務定位</h2>
<p>Entra ID 是 <em>Microsoft 自有的 workforce IdP</em>、跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 是直接競爭者。M365 / Azure-heavy 的組織通常直接用 Entra ID 當主 IdP；Okta-first 的組織可以把 Entra ID 當下游 SP（federation）、也可以雙 IdP 並存、但雙 IdP 的 break-glass 跟 lifecycle 路徑要重新設計。Entra ID 同時承擔 <em>consumer-side 跟 partner-side 的 multi-tenant app</em> 信任、跟 <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a> 在 B2C 場景有交集。</p>
<p>Azure RBAC 是 cloud resource permission engine、跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 同層 — 都在解「身份對 cloud resource 能做什麼」。差異在 <em>scope hierarchy</em> — Azure 用 Management Group → Subscription → Resource Group → Resource 四層繼承、AWS 用 account + organization、Google 用 organization → folder → project。Azure RBAC 預期 <em>role assignment 沿 scope 向下繼承</em>、這跟 AWS 在每個 account 重新指派的習慣不一樣、跨雲團隊轉過來常踩到。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪一段控制屬於 Entra ID（身份）、哪一段屬於 Azure RBAC（resource permission）、不要把兩層當同一件事</li>
<li>Entra ID tenant 的最低稽核需求（Global Admin、App Registration、Conditional Access、Managed Identity）</li>
<li>Azure RBAC 的 scope 設計、Custom Role 跟 PIM 何時必要</li>
<li>Entra ID 控制面事故的降級路徑、跟 Azure RBAC 出事的徵兆差異</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Azure 雙層體系是否健康、要分兩層各看兩件事、跟「日常操作與決策形狀」段的兩層結構對齊。</p>
<p><strong>Entra ID 層</strong>（身份控制面）：</p>
<ul>
<li><strong>誰能做什麼</strong>：Global Admin / Privileged Role Administrator 的人數、是否走 <a href="#%e9%80%b2%e9%9a%8e%e4%b8%bb%e9%a1%8c">PIM</a> just-in-time、Conditional Access 是否強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant 認證</a>、break-glass 帳號是否 <em>exclude</em> 自所有 CA policy 又單獨監控</li>
<li><strong>入口如何暴露</strong>：App Registration 是否限定 single-tenant、multi-tenant app 的 admin consent 流程是否經審查、Managed Identity 是否取代 service principal client secret</li>
</ul>
<p><strong>Azure RBAC 層</strong>（resource permission）：</p>
<ul>
<li><strong>誰能對 resource 做什麼</strong>：Owner / Contributor 在哪個 scope（Management Group 還是 Subscription）、production 環境是否用 Custom Role 收緊權限、有沒有 standing assignment 該改 PIM</li>
<li><strong>證據是否可回查</strong>：Entra ID Sign-in Log / Audit Log 是否同步到 SIEM、Azure Activity Log 是否設保留與 alert、admin consent / role assignment 變更是否觸發 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
</ul>
<p>兩層任一邊任一條缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="entra-id-層">Entra ID 層</h3>
<p><strong>User / Group / lifecycle</strong>：HRIS 推 SCIM 進 Entra ID、Entra ID 同步到下游 SaaS 跟 Azure RBAC group。決策點是 <em>source of truth</em> — 多數組織把 HRIS 設為人員來源、Entra ID 當分發層、避免雙寫造成 stale account。</p>
<p><strong>Conditional Access 是 MFA <em>主要強制機制</em></strong>：MFA 不是設在 user 屬性上、是 Conditional Access policy 在登入時判斷 user / device / location / app / risk 後觸發。常見設定錯誤包含 <em>exclude legacy auth 沒做、break-glass 規則太寬、emergency access 帳號沒獨立監控</em>。Conditional Access 規則設計錯、就是高權限 bypass 的入口。</p>
<p><strong>App Registration vs Enterprise Application</strong>：開發者註冊 multi-tenant app 走 <em>App Registration</em>（app 的定義）、組織 admin 為某 app 設定 SAML SSO / admin consent 走 <em>Enterprise Application</em>（該 tenant 對 app 的信任）。兩者常被混講、但安全意義不同 — App Registration 是「我們做了一個 app」、Enterprise Application 是「我們信任這個 app 用我們的身份」。Consent phishing 攻擊就是針對後者。</p>
<p><strong>Managed Identity</strong>：Azure resource（VM、Function、AKS pod）自帶身份、不需要 service principal client secret、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Workload Identity Federation</a> 同概念但 Azure-internal。System-assigned 跟 resource 生命週期綁定、resource 刪掉 identity 跟著刪；User-assigned 獨立、可跨 resource 共用。production 環境的服務存取 Key Vault / Storage 應走 Managed Identity、不該用 client secret。</p>
<p><strong>Workload Identity Federation</strong>：Entra ID 可以 <em>trust 外部 OIDC issuer</em>（GitHub Actions、AWS、Google）、讓外部 workload 直接拿 Entra ID token、不用儲存 client secret。CI/CD 的 OIDC 整合是這層的主用例、比把 client secret 塞進 CI variable 安全很多。</p>
<p><strong>Signing key 是 control plane 託管</strong>：Entra ID 不暴露 signing key、客戶沒有 rotate 它的能力。這層信任邊界一旦失守、客戶側 <em>直接修不了</em>、要等供應商發 patch 或公告 — Storm-0558 揭示了這條依賴的代價。客戶側能做的補強是 <em>下游檢查</em> 而非 <em>上游修復</em>：</p>
<ul>
<li>訂閱 Microsoft Security Advisory（MSRC）+ tenant-specific notification、讓事件公告第一時間進 IR pipeline、不要靠新聞才知道</li>
<li>SIEM alert <em>anomalous token issuance pattern</em>（跨租戶 token 在 Exchange / Graph API 出現異常存取序列）、不能只信 token signature valid</li>
<li>高敏 app 的 token validation 不只看 Entra ID 標準驗證、加 <em>issuer + tenant + audience + nonce</em> 多層比對、攻擊者偽造跨租戶 token 時可能漏掉某層</li>
<li>Conditional Access 配 <em>token protection</em>（token binding to device）、降低 stolen token replay 的命中率</li>
<li>IR playbook 預設 <em>signing key 事件</em> 一條 — 一旦供應商公告、強制 sign-out 高權限 user、token TTL 收短、回頭看 90 天 sign-in log 找異常</li>
</ul>
<h3 id="azure-rbac-層">Azure RBAC 層</h3>
<p><strong>Scope 設計</strong>：role assignment 沿 Management Group → Subscription → Resource Group → Resource 向下繼承。在 Management Group 給 Contributor、底下所有 subscription / RG / resource 都繼承 — 這既是優點（統一治理）也是風險（誤指派擴散範圍大）。設計原則是 <em>指派盡量低、不要對全 Management Group 給 Contributor</em>。</p>
<p><strong>Built-in role vs Custom Role</strong>：Owner（含 user access admin）/ Contributor（不含權限管理）/ Reader 是 built-in、通常太粗。production 環境需要 Custom Role 把 <code>Microsoft.Storage/storageAccounts/listKeys/action</code> 之類的高風險 action 收掉、只留 read。Custom Role 是 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">least privilege</a> 在 Azure 的落實工具、不做就是用 Contributor 當預設、權限過寬。</p>
<p><strong>Privileged Identity Management（PIM）</strong>：高權限角色（Global Admin、Subscription Owner、User Access Administrator）應走 just-in-time activation、需要 MFA 跟 approval、不該 permanent assignment。沒上 PIM 的組織通常會發現 <em>standing Global Admin 超過 10 個</em>、那是 phishing / token theft 的高價值靶。</p>
<p><strong>Service principal vs Managed Identity</strong>：service principal 是 app 在 Entra ID 的代表、可以用 client secret 或 certificate 認證；Managed Identity 是 service principal 的特殊形式、由 Azure 自動管 credential。能用 Managed Identity 就不用 service principal client secret — 後者要自己 rotate、要存 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a>、容易 stale。</p>
<p><strong>Azure Policy 是 RBAC 的補位</strong>：RBAC 管 <em>principal 能不能對 resource 做這個 action</em>、Azure Policy 管 <em>允不允許這樣設定 resource</em>（例如 storage account 強制加密、VM 只能用認可的 image）。RBAC 給 Contributor 的人可以建 storage account、但 Azure Policy 可以拒絕未加密的 storage account 建立 — 兩層互補、缺一不可。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<p>Azure 雙層體系的取捨要分開看 — 一張表回答 <em>cloud resource permission 該選哪家</em>（Azure RBAC vs AWS IAM vs Google IAM）、一張表回答 <em>workforce IdP 該選哪家</em>（Entra ID vs Okta）。兩個決策獨立、可以混搭（例如：Okta 當 workforce IdP + federate 到 Entra ID + 走 Azure RBAC 管 Azure resource）。</p>
<h3 id="azure-rbac-vs-aws-iam-vs-google-cloud-iam">Azure RBAC vs AWS IAM vs Google Cloud IAM</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Azure RBAC</th>
          <th>AWS IAM</th>
          <th>Google Cloud IAM</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope</td>
          <td>Management Group → Subscription → RG → Resource</td>
          <td>Account + Organization、policy attach</td>
          <td>Organization → Folder → Project</td>
      </tr>
      <tr>
          <td>繼承模型</td>
          <td>scope 向下繼承</td>
          <td>account boundary 強、跨 account 用 assume role</td>
          <td>scope 向下繼承、condition 強</td>
      </tr>
      <tr>
          <td>自訂角色</td>
          <td>Custom Role（JSON）</td>
          <td>Custom managed policy（JSON）</td>
          <td>Custom Role（YAML / API）</td>
      </tr>
      <tr>
          <td>JIT 機制</td>
          <td>Privileged Identity Management（PIM）內建</td>
          <td>無原生 JIT、要靠 IAM Identity Center / 第三方</td>
          <td>無原生 JIT、要靠 third-party / 自建</td>
      </tr>
      <tr>
          <td>Workload</td>
          <td>Managed Identity（內部）+ Workload Identity Fed</td>
          <td>IAM role + OIDC trust</td>
          <td>Workload Identity Federation</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Azure-heavy、M365 整合</td>
          <td>AWS-heavy、account isolation 模型成熟</td>
          <td>GCP-heavy、resource hierarchy 治理</td>
      </tr>
  </tbody>
</table>
<h3 id="entra-id-vs-oktaworkforce-idp">Entra ID vs Okta（workforce IdP）</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Entra ID</th>
          <th>Okta</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主場</td>
          <td>M365 / Azure 原生、跟 RBAC 共生</td>
          <td>多雲 + SaaS、跨平台 SSO</td>
      </tr>
      <tr>
          <td>MFA 機制</td>
          <td>Conditional Access 觸發、Authenticator app / FIDO2</td>
          <td>Sign-On / Authentication Policy、多 factor 選擇</td>
      </tr>
      <tr>
          <td>Lifecycle</td>
          <td>SCIM + cross-tenant sync</td>
          <td>SCIM + Lifecycle Management、整合更廣</td>
      </tr>
      <tr>
          <td>Workload</td>
          <td>Managed Identity / Workload Identity Federation</td>
          <td>較弱、CI 通常 federate 到雲 IAM</td>
      </tr>
      <tr>
          <td>整合廣度</td>
          <td>M365 / Azure / Office app 深、外部 SaaS 比 Okta 少</td>
          <td>7000+ SaaS app 預建</td>
      </tr>
      <tr>
          <td>第三方風險</td>
          <td>Microsoft 控制面（Storm-0558、Midnight Blizzard）</td>
          <td>Okta 控制面（2022 / 2023 多起）</td>
      </tr>
  </tbody>
</table>
<p>選 Entra ID 的核心訴求：<em>M365 / Azure 重度使用、要跟 RBAC + Managed Identity 直接整合、能接受 Microsoft 控制面風險</em>；選 Okta 的核心訴求看 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor 頁</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Conditional Access 進階規則</strong>：除了 user / device / location 基本條件、進階場景包含 <em>risk-based</em>（Identity Protection 給的 user risk / sign-in risk）、<em>token protection</em>（token binding 到 device、防止 token replay）、<em>authentication strength</em>（強制 phishing-resistant factor）。production tenant 至少要有「Global Admin 必須走 phishing-resistant + compliant device」這條規則。</p>
<p><strong>Privileged Identity Management（PIM）的設計細節</strong>：activation 要求 MFA、approval（高權限角色）、justification、時限（預設 8 小時、最長 24）。Access Review 是 PIM 的配套 — 季度檢視 standing assignment 是否還需要、不需要的撤掉。沒做 Access Review 的 PIM 等於只把問題從 standing 推到 <em>誰申請就給</em> — 不是 least privilege。</p>
<p><strong>Workload Identity Federation 跨雲</strong>：Entra ID 可以 trust GitHub Actions / GitLab / AWS / Google 的 OIDC issuer、讓 CI 直接拿 Azure token。同向也成立 — Azure workload 可以拿 Google ID token federate 進 GCP。多雲 CI 不該存任何 client secret、走 federation 比較安全。</p>
<p><strong>Custom Role 設計實務</strong>：用 <code>Microsoft.Authorization/roleDefinitions</code> API 或 portal 定義、<code>actions</code> / <code>notActions</code> / <code>dataActions</code> 各自獨立 — <code>actions</code> 是 control plane、<code>dataActions</code> 是 data plane（讀寫 blob、key vault secret 內容）。常見錯誤是只收 <code>actions</code> 沒收 <code>dataActions</code>、結果 storage account 設定改不了但 blob 內容隨便讀。</p>
<p><strong>Azure Policy 跟 Initiative</strong>：Policy 是單一規則、Initiative 是 policy 的集合（用來組 baseline、例如 CIS、ISO 27001）。Policy effect 有 audit / deny / deployIfNotExists、後者可以自動補洞（例如自動加 diagnostic setting）。RBAC + Policy 一起設計才是完整的 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Global Admin 過多</strong>：standing Global Admin 超過 5 個就要警惕 — 上 PIM、把日常運維改用 Privileged Role Administrator + 特定 admin role group</li>
<li><strong>Conditional Access 規則漏 legacy auth</strong>：規則只 cover modern auth、IMAP / POP / SMTP 等 legacy protocol 不走 CA — 用「Block legacy authentication」baseline policy 補</li>
<li><strong>App Registration / Enterprise Application admin consent 沒審查</strong>：使用者自己 consent 把 mail.read 給三方 app、變 consent phishing 入口 — 關閉 user consent、改 admin consent workflow</li>
<li><strong>Service principal client secret 散落</strong>：CI / 服務裡有大量 client secret、rotate 沒節奏 — 改 Managed Identity（內部）或 Workload Identity Federation（跨雲 CI）</li>
<li><strong>Subscription Owner 太多</strong>：subscription 級 Owner 是高風險、應該收到 Management Group 級 Reader + 必要時 PIM activate Owner</li>
<li><strong>Azure Activity Log 沒進 SIEM</strong>：role assignment 變更、Key Vault access policy 變更只在 Azure portal 看得到、沒 alert — 用 Diagnostic Setting 推 Event Hub / Log Analytics、再進 SIEM</li>
<li><strong>Break-glass 帳號 exclude 自所有 CA policy、但沒監控</strong>：emergency access 帳號不能被 CA 鎖、但 <em>任何登入都該 alert</em> — 配對 Sign-in Log alert + 季度驗證可用</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a></td>
      </tr>
      <tr>
          <td>GCP-only 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>多雲 + 大量 SaaS、IdP 中心化</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></td>
      </tr>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a></td>
      </tr>
      <tr>
          <td>自管 IdP / 不接受 SaaS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>Secret / Key 管理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Azure Key Vault vendor 頁 S2 批次撰寫中）</td>
      </tr>
      <tr>
          <td>偵測訊號（不只 Entra ID 內部）</td>
          <td>07 SIEM 章節、04 observability</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Entra ID 完整 SAML / OIDC / SCIM 規格細節</li>
<li>Azure RBAC built-in role 完整清單與 action 對照</li>
<li>Conditional Access policy template 細節</li>
<li>Azure Policy 內建 initiative 完整清單</li>
<li>Microsoft 365 / Defender for Identity 等周邊產品</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Entra ID / Azure RBAC 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD Identity Control Plane 2021</a></td>
          <td>Entra ID 控制面故障外溢到 Teams / SharePoint / Exchange、業務必須有降級與切換策略、不能完全依賴單一 IdP 可用性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558 Signing Key 2023</a></td>
          <td>signing key 治理失效會跨租戶影響 token 驗證信任、客戶側只能等供應商修復（MSRC / CSRB 公開報告補充了 crash dump / Exchange Online 等具體外洩路徑、屬 case 檔之外的歷史 reference）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>HSM-bound key 是 control plane 必要前提、跨租戶 token 異常要立即升級、不能等供應商先公告</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Entra ID app secret 跟 Managed Identity 的 rotation 分域、不該把 service principal client secret 跟 user password 混在同一個 rotation policy</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Entra ID / Managed Identity 之後的 secret / key 層、Azure Key Vendor 個別 vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Entra ID / Azure 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://learn.microsoft.com/entra/">Microsoft Entra Documentation</a>、<a href="https://learn.microsoft.com/azure/role-based-access-control/">Azure RBAC Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Cloud-native Data Policy (BigQuery + S3)</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/</guid><description>&lt;p>Cloud-native data policy 的核心責任是把資料層的 access 控制綁在 &lt;em>storage resource 本身&lt;/em>、用該雲既有的 IAM 體系做 enforcement、不依賴額外的 data security platform。本頁同時涵蓋 &lt;em>BigQuery policy tooling&lt;/em>（Authorized View / Column-level security / Row-level security / Dynamic Data Masking）跟 &lt;em>AWS S3 policy tooling&lt;/em>（Bucket policy / Access Points / Object Lambda / Macie / Block Public Access）— 兩條 sister stack 是各自雲端代表性的 data access control 設計、合一頁是為了讓讀者看清楚 &lt;em>GCP 走 SQL-native 細粒度&lt;/em> 跟 &lt;em>AWS 走 storage-resource-bound&lt;/em> 的取捨差異、不是把它們當同類混寫。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Cloud-native data policy 是 &lt;em>resource-bound&lt;/em> access control — 控制邏輯掛在 BigQuery dataset / column / row 或 S3 bucket / object 上、用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> 的 principal 體系做 evaluation。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a> 比、DLP 是 &lt;em>content-based discovery + transformation&lt;/em>（掃 PII、做 de-id）、本頁工具是 &lt;em>access boundary&lt;/em>；典型組合是 &lt;em>DLP 發現 sensitive column → BigQuery policy tag 控制誰能讀 → S3 Object Lambda redact at read time&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a> 比、Purview 走 &lt;em>label-driven + 跨 platform&lt;/em>（同一個 sensitivity label 跨 SharePoint / Fabric / Azure SQL）、雲端原生 policy 走 &lt;em>resource-bound + 限該雲&lt;/em>；雲端原生更貼近 storage、跨雲統一靠商業 platform。跟通用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Cloud IAM&lt;/a> 比、IAM 是 &lt;em>resource-level read/write 二分&lt;/em>、本頁是 &lt;em>column / row / object-level 細粒度&lt;/em>、補 IAM 解不掉的「同一張表只能看自家行」場景。&lt;/p></description><content:encoded><![CDATA[<p>Cloud-native data policy 的核心責任是把資料層的 access 控制綁在 <em>storage resource 本身</em>、用該雲既有的 IAM 體系做 enforcement、不依賴額外的 data security platform。本頁同時涵蓋 <em>BigQuery policy tooling</em>（Authorized View / Column-level security / Row-level security / Dynamic Data Masking）跟 <em>AWS S3 policy tooling</em>（Bucket policy / Access Points / Object Lambda / Macie / Block Public Access）— 兩條 sister stack 是各自雲端代表性的 data access control 設計、合一頁是為了讓讀者看清楚 <em>GCP 走 SQL-native 細粒度</em> 跟 <em>AWS 走 storage-resource-bound</em> 的取捨差異、不是把它們當同類混寫。</p>
<h2 id="服務定位">服務定位</h2>
<p>Cloud-native data policy 是 <em>resource-bound</em> access control — 控制邏輯掛在 BigQuery dataset / column / row 或 S3 bucket / object 上、用 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 的 principal 體系做 evaluation。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> 比、DLP 是 <em>content-based discovery + transformation</em>（掃 PII、做 de-id）、本頁工具是 <em>access boundary</em>；典型組合是 <em>DLP 發現 sensitive column → BigQuery policy tag 控制誰能讀 → S3 Object Lambda redact at read time</em>。跟 <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 比、Purview 走 <em>label-driven + 跨 platform</em>（同一個 sensitivity label 跨 SharePoint / Fabric / Azure SQL）、雲端原生 policy 走 <em>resource-bound + 限該雲</em>；雲端原生更貼近 storage、跨雲統一靠商業 platform。跟通用 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Cloud IAM</a> 比、IAM 是 <em>resource-level read/write 二分</em>、本頁是 <em>column / row / object-level 細粒度</em>、補 IAM 解不掉的「同一張表只能看自家行」場景。</p>
<p>關鍵張力：<em>資料細粒度</em> ↔ <em>跨雲 portability</em>。BigQuery RLS 跟 S3 Access Points 的 policy 語法都是該雲專屬、換雲要重寫；換來的是 free（無額外授權）+ 平台原生效能（不過代理）。多雲 enterprise 若要統一 policy DSL、走 Immuta / Privacera / Snowflake Horizon。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>BigQuery 跟 S3 policy 各自能做到什麼層級的細粒度（column / row / object / cross-region）、不能做到什麼</li>
<li>Cloud-native policy 跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 的責任分界、何時要組合使用</li>
<li>Multi-tenant SaaS 在共用 dataset / bucket 場景的 access boundary 設計（BigQuery RLS / S3 Access Points）</li>
<li>何時用雲端原生 policy、何時改走 Immuta / Privacera / Snowflake 跨雲 data security platform</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 cloud-native data policy 是否健康、最少看四件事：</p>
<ul>
<li><strong>BigQuery 側 — RLS / column policy coverage</strong>：multi-tenant dataset 是否有 <code>CREATE ROW ACCESS POLICY</code>、sensitive column 是否綁 policy tag、policy tag 上的 IAM 是否走 group 而非 individual user、view-only access 是否走 <a href="https://cloud.google.com/bigquery/docs/authorized-views">Authorized View</a> 而非 dataset grant</li>
<li><strong>S3 側 — bucket policy 結構</strong>：Block Public Access 是否 account-level 開啟、ACL 是否 disabled（Object Ownership = BucketOwnerEnforced）、共用 bucket 是否走 Access Points 分租戶、跨帳號是否經 AP policy + bucket policy 雙重驗證</li>
<li><strong>Sensitive data discovery 接口</strong>：BigQuery 是否接 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> inspection job、Dataplex 是否跑 data classification、S3 是否開 Macie scan、findings 是否進 EventBridge / Security Hub 而非僅 console 看</li>
<li><strong>Audit trail completeness</strong>：BigQuery audit log（dataAccess）是否進 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Cloud Logging</a> + 進 SIEM、S3 是否開 server access logging + CloudTrail data event（GetObject / PutObject）、跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage</a> 對齊</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">Data Residency, Deletion and Evidence Chain</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="bigquery-側">BigQuery 側</h3>
<p><strong>Authorized View / Authorized Routine</strong>：view 的 SQL definition 可以讀 source dataset、grantee 只要被 grant view 自身就能查、<em>不需要 grant source dataset access</em>。經典「給 analyst 看 aggregate 數據但不給原始 PII row」模式 — analyst 看 <code>SELECT region, count(*) FROM customer</code> 沒問題、但 underlying <code>customer</code> table 從不出現在 analyst IAM。Authorized Routine 是同邏輯延伸到 stored procedure / UDF、適合 logic 比 SELECT 複雜的轉換場景。</p>
<p><strong>Column-level security（policy tag）</strong>：在 <a href="https://cloud.google.com/data-catalog">Data Catalog</a> 建 taxonomy + policy tag、把 BigQuery column schema 綁 tag、policy tag 上設 <em>fine-grained reader</em> role。沒這個 role 的 user 即使有 dataset access、<code>SELECT *</code> 時該 column 會 <em>raise error</em> 或 <em>被 omit</em>。HIPAA / PCI-DSS 對「即使 DBA 也不能 default 看到 PHI / cardholder data」的硬要求、走 policy tag 是技術性 enforcement、不是 procedural control。</p>
<p><strong>Row-level security (RLS)</strong>：<code>CREATE ROW ACCESS POLICY tenant_filter ON dataset.table GRANT TO ('group:analysts@org.com') FILTER USING (tenant_id = SESSION_USER())</code>。每個 query 自動 append filter、user 看到的 row 由 policy expression 決定。Multi-tenant SaaS（共用 dataset、每行帶 <code>tenant_id</code>）必用 — 否則 query 必須在 application layer 帶 WHERE、漏一處就是跨 tenant data leak。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a> 的對照啟示。</p>
<p><strong>Dynamic Data Masking</strong>：column 上設 masking rule（hash / nullify / partial mask / regex replace）、不同 IAM 角色看不同 mask 程度 — <code>email_address</code> 在 admin 看到原值、在 analyst 看到 <code>***@example.com</code>、在 external partner 看到 NULL。補 RLS 不足之處：RLS 過濾 <em>哪些 row 看得到</em>、Masking 過濾 <em>看到的 row 內容怎麼呈現</em>；兩者組合解大多數 multi-tenant + multi-role 場景。</p>
<p><strong>Dataplex Data Classification + DLP 整合</strong>：Dataplex 走 lake-wide 治理（dataset metadata + lineage + quality）、自動觸發 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> inspection、發現 sensitive column 自動建議 / 套用 policy tag。是 GCP 內部把 <em>discovery → access control</em> 自動化的標準路徑。</p>
<h3 id="s3-側">S3 側</h3>
<p><strong>Block Public Access account-level</strong>：2018 推出、2023 起新建 bucket 預設開啟。account-level setting 強制 override 所有 bucket policy / ACL — 即使有 bucket policy 寫 <code>&quot;Principal&quot;: &quot;*&quot;</code>、Block Public Access 開啟時也禁止對外暴露。Production AWS 帳號必須 account-level 開、bucket-level 額外加固。是 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a> 類事故的 last-line defense。</p>
<p><strong>Bucket policy / IAM policy / ACL（legacy）</strong>：三層 evaluation — bucket policy（resource-based、寫在 bucket 上）、IAM policy（identity-based、寫在 principal 上）、ACL（legacy object-level、新建 bucket 應禁用）。AWS 2023 起推 <em>Object Ownership = BucketOwnerEnforced</em>、強制 ACL disabled、所有 access 經 bucket policy + IAM 決定。舊 bucket 應走 ACL → bucket policy migration。</p>
<p><strong>S3 Access Points</strong>：每個 bucket 可開多個 Access Point、各有獨立 name + policy + VPC restriction。Multi-tenant 場景（一個 bucket 服務多個 tenant）走「每個 tenant 一個 AP + AP policy 限定 prefix + 限定 VPC」、取代過去「shared bucket + prefix-based IAM」的脆弱模式。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a> 的對照啟示 — 共用入口需 <em>per-tenant policy boundary</em>、不是 application-layer filtering。</p>
<p><strong>Multi-Region Access Points (MRAP)</strong>：跨 region replicated bucket 的單一 global endpoint、自動 route 到最近 region。資料駐留要求高的場景（GDPR / 中國資料法）反而要慎用、因為 read 來源不可預測；對 latency-sensitive 全球分發是 first-class 解法。</p>
<p><strong>Object Lambda Access Points</strong>：在 GetObject response path 插 Lambda、做 <em>read-time transformation</em>（redact PII / format conversion / image resize / decrypt + re-encrypt）。同一份 raw object、不同 caller 透過不同 Object Lambda AP 看到不同版本 — 等同 BigQuery Dynamic Data Masking 在 S3 的對應物。但 Lambda 有 cold start + 6MB response limit、不是所有場景都合適。</p>
<p><strong>Macie sensitive data discovery</strong>：S3 專屬、scan bucket 找 PII / credential / payment data、findings 進 EventBridge + AWS Security Hub。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> 同層但限 S3、不能掃 RDS / DynamoDB。findings 應自動 route 到 SIEM、不是只在 Macie console 等人看。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023 File Service Breach</a> 的對照 — 對外檔案服務必有 audit + 異常量 baseline + Macie sensitive content scan。</p>
<p><strong>S3 Object Ownership / ACL disabled</strong>：2023+ 預設 ACL disabled、所有新 bucket 應 keep this default、舊 bucket 走 audit + migration（先掃 ACL grant、確認沒人靠 ACL 拿 access、再切換）。混用 ACL + bucket policy 的 bucket 是 access control 漂移最常見的源頭。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>BigQuery policy tooling</th>
          <th>S3 policy tooling</th>
          <th>Immuta / Privacera</th>
          <th>Snowflake Horizon</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>細粒度層級</td>
          <td>Column / Row / cell-level（policy tag + RLS + DDM）</td>
          <td>Object-level（prefix-based）+ Object Lambda 內容轉換</td>
          <td>Column / Row / cell + 跨平台統一 DSL</td>
          <td>Column / Row + Snowflake 平台限定</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Free（included in BigQuery）</td>
          <td>Free（bucket policy）+ Macie / Object Lambda 用量計費</td>
          <td>商業授權、per-user 或 per-data-source</td>
          <td>Snowflake 平台費內含</td>
      </tr>
      <tr>
          <td>跨雲 portable</td>
          <td>GCP only</td>
          <td>AWS only</td>
          <td>跨 BigQuery / Snowflake / Databricks / S3</td>
          <td>Snowflake only</td>
      </tr>
      <tr>
          <td>Policy DSL</td>
          <td>SQL-native（CREATE ROW ACCESS POLICY、masking SQL）</td>
          <td>JSON policy + Lambda 程式碼</td>
          <td>統一 attribute-based DSL</td>
          <td>SQL-native</td>
      </tr>
      <tr>
          <td>Sensitive discovery</td>
          <td>DLP / Dataplex 自動整合</td>
          <td>Macie（限 S3）</td>
          <td>內建 + 跨平台 scan</td>
          <td>跨 schema metadata + classification</td>
      </tr>
      <tr>
          <td>Audit</td>
          <td>Cloud Audit Log dataAccess 細到 column</td>
          <td>CloudTrail data event + server access log</td>
          <td>跨平台統一 audit trail</td>
          <td>Snowflake QUERY_HISTORY</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>GCP-first、BigQuery 為主 data warehouse</td>
          <td>AWS-first、S3 為 data lake / 檔案分發</td>
          <td>多雲 enterprise、跨平台統一 policy</td>
          <td>Snowflake-centric data platform</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — RLS / policy tag 重寫到目標平台</td>
          <td>中 — bucket policy / AP 重寫</td>
          <td>低 — DSL 抽象可遷移</td>
          <td>中 — 限 Snowflake</td>
      </tr>
  </tbody>
</table>
<p>選雲端原生 policy 的核心訴求：<em>單一雲 + 預算敏感 + 不想引入新 vendor</em>。多雲 enterprise + 統一治理需求高、走 Immuta / Privacera 才能避免兩套 policy 漂移。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>BigQuery Authorized View vs RLS 取捨</strong>：Authorized View 適合 <em>shape-based filtering</em>（grantee 只能看 aggregate / 特定 column subset）、RLS 適合 <em>value-based filtering</em>（grantee 只能看 tenant_id = self 的行）。實務常常組合 — view 限 column、view 上再加 RLS 限 row。view 的問題是維護成本（schema 改要同步改 view）、RLS 的問題是 policy expression 寫錯整批 user 看不到資料、staging tenant 跑過再 promote。</p>
<p><strong>S3 Access Points + VPC-only restriction</strong>：AP policy 可加 <code>&quot;Condition&quot;: {&quot;StringEquals&quot;: {&quot;aws:SourceVpc&quot;: &quot;vpc-xxx&quot;}}</code>、強制只能從特定 VPC access — 跨帳號場景（partner 帳號 access 自家 bucket）必加、避免 partner credential 外洩後可從任意網路位置存取。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a> 對照、backup bucket 不該跟 prod bucket 共用 IAM role + 不該允許 internet-wide access。</p>
<p><strong>Object Lambda redact PII at read time</strong>：適合 <em>raw data 已寫入、但不同 consumer 需要不同 view</em> 的場景 — 例如客服查 user record 看到 mask 過的 SSN、合規 audit 帳號看到完整 SSN。Lambda 內部呼叫 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> deid template / Comprehend PII detection / 自家 regex；要注意 cold start 對 latency 的影響、不適合 high-throughput 場景。</p>
<p><strong>Macie automated discovery → SIEM</strong>：Macie findings 走 EventBridge rule → Security Hub → 推 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">Splunk / Elastic Security / Datadog Security</a> — 不該只在 Macie console 看 findings。發現 unencrypted S3 bucket 有 cardholder data 必須觸發 incident response runbook、進 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 事故處理</a>。</p>
<p><strong>跨 region 跟 data residency</strong>：BigQuery dataset region + S3 bucket region 是 <em>資料駐留 enforcement</em> 的硬邊界、policy tooling 不能 override。GDPR / 中國資料法場景必須 <em>region pinning</em> + 禁止 Multi-Region replication、policy tag / RLS 無法解決資料離境問題。對應 <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">Data Residency Deletion and Evidence Chain</a> 章節原則。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>BigQuery RLS 設了但 user 還是看到全部 row</strong>：policy <code>GRANT TO</code> 沒包該 user 的 group、或 user 有 <code>bigquery.dataOwner</code> role（owner override RLS）— check group membership + 降權到 dataViewer</li>
<li><strong>Column policy tag 沒生效</strong>：column 沒 attach tag、或 tag taxonomy 沒在該 project / region — check Data Catalog taxonomy location 跟 dataset region 對齊</li>
<li><strong>S3 bucket 意外 public</strong>：Block Public Access account-level 沒開 + bucket policy 寫 <code>&quot;Principal&quot;: &quot;*&quot;</code>、或 ACL 殘留 AllUsers grant — 立即開 BPA + audit ACL（aws s3api get-bucket-acl）</li>
<li><strong>Access Point policy 跟 bucket policy 衝突</strong>：AP 允許 但 bucket policy 拒絕、最後是拒絕（explicit deny 永遠勝）— 兩層都要明確 allow、bucket policy 加 <code>&quot;Principal&quot;: {&quot;AWS&quot;: &quot;*&quot;}</code> + condition 限定 AP ARN</li>
<li><strong>Macie scan 跑很久 / cost 暴衝</strong>：scan 整個 bucket、含 archive prefix、沒設 sampling — 用 <em>sensitive data discovery job</em> with prefix filter + sampling rate、不要 default 全 bucket scan</li>
<li><strong>Authorized View grantee 看不到資料</strong>：view definition 走的 source dataset 沒 authorize 該 view、或 view 自身改了但沒重新 authorize — <code>bq update --view_authorization</code> 重設</li>
<li><strong>Object Lambda 慢 / timeout</strong>：Lambda cold start + 6MB response limit、大檔案不該走 Object Lambda — 改在寫入時 transform、或用 pre-signed URL 繞過 Object Lambda</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲統一 data policy DSL</td>
          <td>Immuta / Privacera</td>
      </tr>
      <tr>
          <td>Content-based discovery + de-id</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Label-driven + Microsoft 365 跨 platform</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Application-layer access control</td>
          <td>應用層 RBAC / ABAC（Casbin / OPA / Cerbos）</td>
      </tr>
      <tr>
          <td>Snowflake-centric data platform</td>
          <td>Snowflake Horizon（row access policy / masking policy 平台內建）</td>
      </tr>
      <tr>
          <td>通用 cloud resource permission</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>SIEM / detection</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">Splunk / Elastic Security / Datadog Security</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>BigQuery / S3 自身的完整 admin guide（pricing / region / quota）</li>
<li>Encryption-at-rest 細節（KMS 整合走 <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a> 頁）</li>
<li>Azure Data Lake / Azure SQL policy（屬 Azure stack、本頁不涵蓋）</li>
<li>應用層 RBAC framework（Casbin / Cerbos / OPA Rego）</li>
<li>資料庫層 RLS（PostgreSQL RLS / SQL Server Row-Level Security）— 跟雲端原生 storage policy 是不同層</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Cloud-native data policy 在 07 案例庫沒有直接 vendor-level 事件、所有 data exfiltration case 都是 access boundary 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 cloud-native data policy 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>Multi-tenant SaaS 共用 dataset / schema 必須有 BigQuery RLS / Snowflake row access policy 等技術邊界、即使 credential 外洩攻擊者也只能看授權 row、不能只靠 application-layer WHERE</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a></td>
          <td>S3 backup bucket 跟 prod bucket 必須獨立 Access Point + 獨立 IAM role + VPC restriction、同帳號 prefix-based 區隔不夠、Block Public Access 是 last-line</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023 File Service Breach</a></td>
          <td>對外檔案服務必須有 S3 server access log + CloudTrail data event + Macie sensitive content scan、批量下載靠 GetObject 速率 baseline alert、不是事後檢視</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>共用 bucket 服務多 tenant 必走 S3 Access Points 拆 per-tenant policy、取代 prefix-based ACL 跟 application-layer filtering 的脆弱模式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">Data Residency Deletion and Evidence Chain (section)</a></td>
          <td>Cloud-native policy 是 deletion + residency 治理的技術 enforcement 層、region pinning + 禁止 Multi-Region replication + audit log retention 對應章節原則</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.7 資料駐留刪除與證據鏈</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a>（discovery + de-id 互補）、<a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（label-driven 對照）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">Splunk / Elastic Security / Datadog Security</a>（audit log + Macie findings → SIEM）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（principal 體系基底）、<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>（encryption-at-rest）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（data exfiltration incident routing）、<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">1 資料庫模組</a>（database-layer RLS / column policy 對照）</li>
<li>官方：<a href="https://cloud.google.com/bigquery/docs/column-level-security">BigQuery column-level security</a>、<a href="https://cloud.google.com/bigquery/docs/row-level-security-intro">BigQuery row-level security</a>、<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html">Amazon S3 Access Points</a>、<a href="https://docs.aws.amazon.com/macie/">Amazon Macie</a></li>
</ul>
]]></content:encoded></item><item><title>Trivy</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/</guid><description>&lt;p>Trivy 是 Aqua Security 維護的 &lt;em>open-source all-in-one security scanner&lt;/em>、Apache 2.0、單一 CLI 涵蓋 container image / filesystem / git repo / Kubernetes / IaC 五種 scan target、額外做 secret / license / SBOM scan。設計目標跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 不同 — Snyk 是 SaaS-first、用 server-side dashboard 跨 SCM / 跨 repo 聚合；Trivy 是 CLI-first、零 server、CI runner 自己就能完成所有工作、air-gapped 環境也能跑。商業版 Aqua Platform 加 dashboard / RBAC / policy / runtime defense、但 Trivy 本身免費覆蓋大部分團隊需求。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Trivy 的核心定位是 &lt;em>把 supply chain scan 收斂成一個 CLI&lt;/em>。同一個 binary 處理 container image、source tree、K8s cluster live state、Terraform / Dockerfile / CloudFormation 配置、secret / license / SBOM — 不需要拼裝多個工具、不需要 SaaS account、不需要 server。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 商業 SaaS 的差異是 &lt;em>資料治理權&lt;/em> 在自己這邊（scan 結果不上 vendor cloud）、代價是 &lt;em>跨 repo 集中報表&lt;/em> 需要自己拼（用 Trivy Operator 或 Aqua Platform）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &amp;#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &amp;#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype&lt;/a> 的差異是 &lt;em>工具邊界劃法&lt;/em>。Anchore Syft 專做 SBOM 生成、Grype 專做 vuln scan、兩個工具靠 SBOM 標準（CycloneDX / SPDX）串接；Trivy 一個 CLI 全包、SBOM 也同樣輸出標準格式。多 vendor 並存環境（例：build pipeline 用 Syft 生 SBOM、release gate 用 Grype scan、跟 SBOM repository 互通）Syft+Grype 模組化較適合；單一團隊單一 pipeline 想 &lt;em>一次裝完&lt;/em> 用 Trivy 更直接。&lt;/p></description><content:encoded><![CDATA[<p>Trivy 是 Aqua Security 維護的 <em>open-source all-in-one security scanner</em>、Apache 2.0、單一 CLI 涵蓋 container image / filesystem / git repo / Kubernetes / IaC 五種 scan target、額外做 secret / license / SBOM scan。設計目標跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 不同 — Snyk 是 SaaS-first、用 server-side dashboard 跨 SCM / 跨 repo 聚合；Trivy 是 CLI-first、零 server、CI runner 自己就能完成所有工作、air-gapped 環境也能跑。商業版 Aqua Platform 加 dashboard / RBAC / policy / runtime defense、但 Trivy 本身免費覆蓋大部分團隊需求。</p>
<h2 id="服務定位">服務定位</h2>
<p>Trivy 的核心定位是 <em>把 supply chain scan 收斂成一個 CLI</em>。同一個 binary 處理 container image、source tree、K8s cluster live state、Terraform / Dockerfile / CloudFormation 配置、secret / license / SBOM — 不需要拼裝多個工具、不需要 SaaS account、不需要 server。跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 商業 SaaS 的差異是 <em>資料治理權</em> 在自己這邊（scan 結果不上 vendor cloud）、代價是 <em>跨 repo 集中報表</em> 需要自己拼（用 Trivy Operator 或 Aqua Platform）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a> 的差異是 <em>工具邊界劃法</em>。Anchore Syft 專做 SBOM 生成、Grype 專做 vuln scan、兩個工具靠 SBOM 標準（CycloneDX / SPDX）串接；Trivy 一個 CLI 全包、SBOM 也同樣輸出標準格式。多 vendor 並存環境（例：build pipeline 用 Syft 生 SBOM、release gate 用 Grype scan、跟 SBOM repository 互通）Syft+Grype 模組化較適合；單一團隊單一 pipeline 想 <em>一次裝完</em> 用 Trivy 更直接。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> 的差異是 <em>偵測類型 + 部署面</em>。GHAS 綁 GitHub、SAST（CodeQL）覆蓋深、但容器掃跟 IaC scan 較弱；Trivy 跨 SCM、容器跟 IaC 掃強、但沒 SAST 深度。跟 Clair（RedHat / Quay 內建）或 Anchore Enterprise 比、Trivy 用戶基數大（CNCF Sandbox）、社群更新快、整合面廣（GitLab CI / GitHub Actions / Jenkins / CircleCI 都有官方 step）。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Trivy 的五種 scan target（image / fs / repo / k8s / config）各承擔哪段 supply chain 責任、什麼時候用哪個</li>
<li>Trivy DB 的更新模型（OCI artifact、6 小時 cadence、air-gapped mirror）跟 CI runner 信任邊界</li>
<li><code>.trivyignore</code> 跟 severity gate 在 CI 怎麼接、exception 治理要設哪些 tripwire</li>
<li>何時用 Trivy、何時改走 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a> / <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Trivy 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>scan target 覆蓋面</strong>：是否 image / fs / config / secret 四類都跑（不是只 scan image）、CI 是否把 dev container / base image / runtime image 全納入 — 漏掉 base image 等於信任 upstream registry</li>
<li><strong>Trivy DB 更新 cadence</strong>：CI runner 是否每次都 pull 最新 DB（OCI artifact、預設 6 小時 TTL）、air-gapped 環境是否有內部 mirror（<code>--db-repository</code> 指到內部 registry）、<code>trivy --skip-db-update</code> 是否被誤用</li>
<li><strong>severity gate 是否真的 fail build</strong>：Trivy 預設 scan 完 exit 0、CI 不會 fail；需要 <code>--exit-code 1 --severity HIGH,CRITICAL</code> 才會把 PR build 擋下來、否則 scan 結果只在 log、沒人看</li>
<li><strong><code>.trivyignore</code> 治理</strong>：ignore 的 CVE 有 reason + expiration 嗎、quarterly review 流程在嗎、<code>.trivyignore.yaml</code> 有用嗎 — 沒治理的 ignore list 會無限膨脹、最後等於沒 scan</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain integrity</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>CLI 五種 scan target</strong>：<code>trivy image &lt;ref&gt;</code> 掃 container image 的 OS package + language dependency；<code>trivy fs &lt;dir&gt;</code> 掃 source tree（含 lockfile + Dockerfile + IaC manifest + secret）；<code>trivy repo &lt;url&gt;</code> 不 clone 直接掃 git repo；<code>trivy k8s --report summary cluster</code> 掃 K8s cluster 內所有 workload（image + manifest 配置）；<code>trivy config &lt;dir&gt;</code> 專掃 IaC 配置（Terraform / CloudFormation / K8s YAML / Dockerfile / Helm）。本地 dev 最常用 <code>trivy fs .</code>、CI 最常用 <code>trivy image $IMAGE</code>、K8s 場景用 Trivy Operator 跑 in-cluster scan。</p>
<p><strong>Trivy DB（OCI artifact）</strong>：Trivy 自己維護 vulnerability DB、以 OCI artifact 形式存在 <code>ghcr.io/aquasecurity/trivy-db</code>、每 6 小時更新一次。CI runner 第一次 scan 自動 pull、後續用 cache。air-gapped 環境（金融 / 政府 / 工控）需要把 DB mirror 到內部 OCI registry、<code>--db-repository internal.registry/trivy-db</code> 指過去。DB 內容是 aggregated source — NVD、GHSA、各 Linux distro security advisory、language ecosystem advisory（npm / PyPI / Maven / RubyGems / crates.io / Go / etc.）合在一起、所以單一查詢就能跨多生態。</p>
<p><strong><code>.trivyignore</code> 跟 <code>.trivyignore.yaml</code></strong>：scan 發現的 CVE 若已評估無風險（無 reachable code path、已有 mitigation、upstream 尚未 patch 但業務不受影響）寫進 <code>.trivyignore</code>（純 CVE-ID list）或 <code>.trivyignore.yaml</code>（含 <code>expired_at</code> + <code>comment</code> + <code>paths</code>、更適合治理）。後者強制每筆 ignore 有 expiration（建議 quarterly）跟 reason、過期自動失效、避免 ignore list 變成「忘了清的死帳」。CI 應該每季跑 <code>trivy --ignorefile .trivyignore.yaml</code> 同時 alert 即將過期的條目。</p>
<p><strong>Severity gate 是 CI 必設</strong>：Trivy 預設 scan 完 print 結果但 exit 0、CI build 不會 fail。要在 CI 真正擋下高風險 PR、必須 <code>trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE</code>。Severity 級別（UNKNOWN / LOW / MEDIUM / HIGH / CRITICAL）對應 CVSS score、團隊需要決定 <em>什麼 severity 算 release blocker</em>。常見 baseline：CRITICAL fail PR build、HIGH fail nightly build（給 24 小時修補窗口）、MEDIUM 進 backlog ticket。</p>
<p><strong>SBOM 生成與 scan</strong>：<code>trivy image --format cyclonedx --output sbom.json $IMAGE</code> 生 CycloneDX 格式 SBOM、<code>--format spdx-json</code> 生 SPDX。也可以反向 — 拿別人生的 SBOM 餵給 Trivy：<code>trivy sbom sbom.json</code> 跑 vuln scan、不重新解析 image。這個 workflow 跟 <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a> 重疊（Syft 生 SBOM + Grype scan SBOM）、差別是 Trivy 一站完成、Syft+Grype 拆兩階段更模組化。SBOM artifact 進 OCI registry（用 cosign attach）或 SBOM repository（如 Dependency-Track）做長期追蹤。</p>
<p><strong>Misconfig + Secret + License 一起 scan</strong>：<code>trivy fs .</code> 預設啟用四類 scanner — vuln（package CVE）、misconfig（IaC 配置錯誤）、secret（hardcoded credential）、license（license compliance）。Misconfig 內建 hundreds of built-in policy（Rego 寫的）涵蓋 K8s / Terraform / Docker / CloudFormation 常見錯誤（privileged container / open S3 bucket / 0.0.0.0/0 ingress）。Secret scanner 用 regex pattern 找 AWS access key / GCP service account / Stripe key 等常見格式、不是萬能、但 dev pre-commit 攔截已洩漏 secret 很實用。</p>
<p><strong>Trivy Operator（K8s in-cluster scanner）</strong>：K8s 場景的標準配置。Operator 在 cluster 跑、定期 scan 所有 namespace 的 workload、產 CRD reports：<code>VulnerabilityReport</code>（image CVE）、<code>ConfigAuditReport</code>（manifest 配置）、<code>SbomReport</code>、<code>ClusterComplianceReport</code>（CIS Kubernetes Benchmark / NSA Kubernetes Hardening Guide）。Operator 可選配 ValidatingAdmissionWebhook、admission 階段拒絕高風險 image（CVE severity 超門檻）。Reports 是 CRD、可以走 <code>kubectl get vulnerabilityreport</code> 看、也可以 prometheus exporter 出 metric 進 Grafana。</p>
<p><strong>Aqua Platform 整合</strong>：Trivy CLI / Operator 結果可以推到 Aqua Platform（商業版）做集中 dashboard、跨 cluster RBAC、policy engine、compliance report、runtime defense（runtime container 監控）。純 CLI 用戶不需要、但企業有多 cluster + 跨團隊 governance 需求時、Aqua Platform 補 server-side aggregation 那塊（對應 Snyk dashboard 的功能）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Trivy</th>
          <th>Snyk</th>
          <th>Syft + Grype</th>
          <th>GitHub Advanced Security</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>CLI-only、零 server</td>
          <td>SaaS-first、需要 Snyk account</td>
          <td>CLI-only、兩個 binary</td>
          <td>綁 GitHub、整合在 PR / Code Scanning</td>
      </tr>
      <tr>
          <td>授權</td>
          <td>Apache 2.0、完全免費</td>
          <td>商業 SaaS（Free tier + 付費 plan）</td>
          <td>Apache 2.0、完全免費</td>
          <td>GitHub Enterprise add-on</td>
      </tr>
      <tr>
          <td>Scan target</td>
          <td>image / fs / repo / k8s / config</td>
          <td>image / SCA / IaC / Code (SAST) / Container</td>
          <td>image / fs（SBOM-first）</td>
          <td>SAST (CodeQL) + Dependabot + Secret scanning</td>
      </tr>
      <tr>
          <td>Vulnerability DB</td>
          <td>Trivy DB（OCI artifact、6h cadence、可 mirror）</td>
          <td>Snyk Intel（私有、含 reachability data）</td>
          <td>Grype DB（GitHub-hosted、可 mirror）</td>
          <td>GitHub Advisory DB</td>
      </tr>
      <tr>
          <td>Reachability</td>
          <td>無</td>
          <td>有（Snyk Code reachability）</td>
          <td>無</td>
          <td>部分（CodeQL data flow）</td>
      </tr>
      <tr>
          <td>SBOM 支援</td>
          <td>生 + scan（CycloneDX / SPDX）</td>
          <td>生（Snyk SBOM）</td>
          <td>Syft 生、Grype scan、最完整 SBOM workflow</td>
          <td>部分（Dependency Graph）</td>
      </tr>
      <tr>
          <td>K8s in-cluster</td>
          <td>Trivy Operator（CRD reports + admission）</td>
          <td>Snyk Kubernetes（agent-based）</td>
          <td>無原生、靠外部 wrapper</td>
          <td>無</td>
      </tr>
      <tr>
          <td>跨 repo 報表</td>
          <td>Trivy 本身無、Aqua Platform 補</td>
          <td>Snyk dashboard（強項）</td>
          <td>無原生、靠外部</td>
          <td>GitHub Security tab（綁 GitHub）</td>
      </tr>
      <tr>
          <td>Air-gapped 支援</td>
          <td>強 — DB 可 mirror 到內部 registry</td>
          <td>弱 — 需要 Snyk SaaS（Snyk On-Prem 商業版另算）</td>
          <td>強 — DB 可 mirror</td>
          <td>弱 — 綁 GitHub.com</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>低 — 一個 CLI + 通用 flag</td>
          <td>低 — UI 友善、CLI 也順</td>
          <td>中 — 兩個工具拼、SBOM 概念要懂</td>
          <td>中 — CodeQL query 寫 / 調有門檻</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>CI image scan、K8s scan、air-gapped、OSS-only 預算</td>
          <td>跨 SCM 跨 repo 集中治理、SaaS 預算 OK、需 reachability</td>
          <td>SBOM 為主軸的 supply chain、多 vendor 互通</td>
          <td>GitHub-only + 需要 SAST 深度</td>
      </tr>
  </tbody>
</table>
<p>選 Trivy 的核心訴求：<em>零 server / OSS-only 預算 / air-gapped 友善 / 一個 CLI 涵蓋 container + IaC + secret</em>。需要跨 SCM 集中 dashboard 跟 reachability 走 Snyk；純 SBOM workflow + 多工具互通走 Syft+Grype；GitHub-only + 重 SAST 走 GHAS。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Trivy Operator + admission control</strong>：Operator 跑 ValidatingAdmissionWebhook、admission 階段對 Pod spec 的 image 跑 vuln check、超門檻就拒絕創建。對應 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply chain integrity</a> 的 <em>artifact gate at deploy time</em>。組態要小心 — webhook timeout / Trivy DB 不可用 / Operator 自己 down 都會擋住 deploy、production 通常 fail-open（DB 不可用時放行 + alert）而非 fail-close。</p>
<p><strong>Custom check（Rego policy）</strong>：Trivy misconfig scanner 用 Rego 寫 policy、可以自己加 custom check（例：禁止特定 namespace 用 hostPath volume、禁止特定 IAM action）。policy 走 <code>--policy ./custom-policies/</code> 載入、跟內建 policy 一起跑。比 OPA Gatekeeper 簡單（不需要部署 admission webhook、scan-time 就執行）、但 runtime enforcement 還是要靠 Gatekeeper / Kyverno。</p>
<p><strong>Air-gapped DB sync</strong>：金融 / 政府 / 工控環境 CI runner 不能連外網。流程是：有對外網的 staging machine 跑 <code>trivy --download-db-only</code> 把 OCI artifact 拉下來、用 <code>skopeo copy</code> 推到內部 OCI registry、CI runner 用 <code>--db-repository internal.registry/trivy-db --skip-db-update</code>（或排程從內部 mirror pull）。DB 更新節奏要排程化（每天 / 每 6 小時）、否則 air-gapped DB 落後幾天會 miss 掉新公布 CVE。</p>
<p><strong>Cosign + SLSA + Trivy 三件事</strong>：Trivy 看的是 <em>known CVE</em>、看不到 <em>build-time backdoor</em>。配套需要 Sigstore cosign 做 image signature verify（確認 image 真的是自家 CI 出的）+ SLSA provenance（build pipeline 不可篡改紀錄）+ Trivy scan（known CVE）三件事一起、才是完整 supply chain trust chain。對應 <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">Cert-manager</a> 在 TLS 的角色、Trivy 在 supply chain 的角色是 <em>已知漏洞檢測</em>、不是 <em>trust establishment</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>CI 顯示 scan 完但 build 沒 fail</strong>：忘了 <code>--exit-code 1 --severity HIGH,CRITICAL</code>、scan 結果只在 log、PR 一直 merge 進高風險 image — 補 severity gate flag、設 baseline</li>
<li><strong>Trivy DB 拉不下來 / 過期</strong>：CI runner 沒對外網 / GitHub Container Registry 被擋 / DB cache 太舊 — 設內部 OCI mirror、CI runner <code>--db-repository</code> 指過去、排程 update</li>
<li><strong><code>.trivyignore</code> 無限膨脹</strong>：用純 list 沒 expiration、團隊找不到誰加的 / 為什麼加 — 改 <code>.trivyignore.yaml</code> 強制 reason + expiration、quarterly review 排進 sprint</li>
<li><strong>false positive 多到 alert fatigue</strong>：base image 自帶大量未修補 OS package、scan 出 50+ HIGH — 換 distroless / Chainguard / Wolfi 等 <em>minimal base image</em>、或 multi-stage build 只保留必要 binary、不是調高門檻當沒看到</li>
<li><strong>secret scanner 漏報</strong>：hardcoded credential 是非標準格式（內部 token、特殊 vendor key）— 加 custom secret pattern、或配合 dedicated tool（Gitleaks / GitGuardian）做第二道</li>
<li><strong>Trivy Operator 報表沒人看</strong>：reports 是 CRD、<code>kubectl get</code> 才看到、PR / Slack 沒通知 — 接 prometheus exporter + Grafana alert、或 webhook 推 Slack</li>
<li><strong>K8s admission webhook fail 擋住 deploy</strong>：Operator down / DB 不可用、所有 Pod 創建被拒 — webhook 配 <code>failurePolicy: Ignore</code>、production 通常 fail-open + alert、不是 fail-close</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需 reachability / 跨 SCM dashboard</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>SBOM-first / 多工具互通</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a></td>
      </tr>
      <tr>
          <td>SAST 深度 / GitHub-only</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>（CodeQL）</td>
      </tr>
      <tr>
          <td>純依賴升級自動化</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a></td>
      </tr>
      <tr>
          <td>Runtime container monitoring</td>
          <td>Falco / Cilium Tetragon / Aqua Runtime（商業版）</td>
      </tr>
      <tr>
          <td>TLS / mTLS cert lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>Image signing / provenance</td>
          <td>Sigstore cosign + SLSA framework</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Trivy CLI 所有 flag 跟 output format 完整 reference</li>
<li>Rego policy language 完整語法（OPA / Rego 自有體系）</li>
<li>Aqua Platform 商業版完整功能矩陣（dashboard / RBAC / runtime defense）</li>
<li>各 PCI DSS / SOC 2 / FedRAMP 合規 mapping</li>
<li>跟其他 scanner（Clair / Anchore Enterprise / Twistlock）的逐項比較</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Trivy 在 07 案例庫沒有 <em>直接 vendor-level 事件</em>（Trivy 本身 OSS、無 vendor-side 控制面風險）、但 supply chain 案例都對應 Trivy 的能力與邊界：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Trivy 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — CVE 公開後 Trivy DB 幾小時內更新、scan container image 找受影響 service 是緊急 response 主軸；air-gapped 環境 DB mirror 更新節奏直接決定窗口期長度</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>對照啟示 — Trivy scan known CVE、看不到 build-time backdoor 植入；必須配合 image signing（cosign）+ SLSA provenance 才完整</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>對照啟示 — container scan 看 image layer 內 known CVE、看不到 runtime callback / dynamic load；需配合 runtime monitoring（Falco / Tetragon）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>對照啟示 — Trivy 比對 package name + version 對應 CVE、看不到 maintainer takeover；mitigation 走 SBOM provenance + maintainer trust baseline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></td>
          <td>章節原則 — Trivy 是 <em>known CVE 檢測</em>、SBOM + signing + provenance 三件事一起才形成完整 trust chain</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft + Grype</a>、<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>（image 漏洞最終影響的是 origin server 風險面）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（TLS lifecycle）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（secret rotation 對應 Trivy secret scan 找到的 hardcoded credential）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（CVE 緊急 response 流程 / 高風險 image rollback）</li>
<li>官方：<a href="https://aquasecurity.github.io/trivy/">Trivy Documentation</a>、<a href="https://aquasecurity.github.io/trivy-operator/">Trivy Operator</a></li>
</ul>
]]></content:encoded></item><item><title>AWS Aurora</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/</guid><description>&lt;p>Aurora 是 AWS managed PostgreSQL / MySQL、把 storage layer 重寫成跨 AZ 分散式 log service、保留 wire protocol 相容。Netflix 把多套 RDBMS 統一到 Aurora（+75% 效能、-28% 成本）、DraftKings 撐每分鐘 100 萬 ops 體育博彩、Standard Chartered 跨 7 個受監管市場、FanDuel 處理 Super Bowl 5-10 倍峰值 — 是 SQL OLTP managed 服務的代表。&lt;/p>
&lt;h2 id="教學路線managed-sql-與平台責任轉移">教學路線：Managed SQL 與平台責任轉移&lt;/h2>
&lt;p>Aurora 服務頁的教學目標是把 PostgreSQL / MySQL 語意延伸到 AWS managed storage / compute 分離模型。讀者讀完後要能判斷哪些責任交給 Aurora，哪些責任仍留在 schema、query、maintenance window、region 與成本治理。&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>Managed SQL&lt;/td>
 &lt;td>Aurora 如何保留 PostgreSQL / MySQL 語意並改變操作責任&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage / compute&lt;/td>
 &lt;td>分離 storage layer 如何影響 replica、failover、backup&lt;/td>
 &lt;td>容量規劃要點、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS operation model&lt;/td>
 &lt;td>parameter group、maintenance、region、cost 如何成為平台責任&lt;/td>
 &lt;td>跟其他 vendor 的取捨、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Peak workload&lt;/td>
 &lt;td>金融、串流、Super Bowl、banking case 如何提供容量判準&lt;/td>
 &lt;td>適用場景、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時留 RDS、自管 PostgreSQL / MySQL、轉 Spanner 或 DynamoDB&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位storage--compute-分離的-sql">定位：storage / compute 分離的 SQL&lt;/h2>
&lt;p>Aurora 跟傳統 PostgreSQL / MySQL primary 最大差異是 &lt;em>storage layer 重寫&lt;/em>。傳統 SQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication lag 受 compute 影響。Aurora 把 storage 拉到分散式 log service、跨 6 個 storage node（3 AZ × 2 node）、storage 跟 compute 獨立擴。&lt;/p>
&lt;p>&lt;strong>容量特性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>單一 cluster 最高 storage：128 TB&lt;/li>
&lt;li>最多 15 個 read replica（單 region 內）&lt;/li>
&lt;li>read replica replication lag：10-30ms（vs 傳統 PostgreSQL 跨 AZ 可能秒級）&lt;/li>
&lt;li>跨 AZ failover：&amp;lt; 30 秒（promote read replica）&lt;/li>
&lt;li>Aurora Global Database 跨 region replication：&amp;lt; 1 秒典型 lag&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼這個分離很重要&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>傳統 PostgreSQL primary 上的 read replica 都靠 logical replication、會跟著 primary write load 走慢&lt;/li>
&lt;li>Aurora storage 直接複製到 6 個 storage node、read replica 從 storage 讀、不靠 primary&lt;/li>
&lt;li>→ read replica 大幅減少 lag、可以撐更多 OLTP read traffic&lt;/li>
&lt;li>對應 &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&lt;/a> +75% 效能改善的關鍵原因&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>按公開 case 提煉的典型適用場景：&lt;/p></description><content:encoded><![CDATA[<p>Aurora 是 AWS managed PostgreSQL / MySQL、把 storage layer 重寫成跨 AZ 分散式 log service、保留 wire protocol 相容。Netflix 把多套 RDBMS 統一到 Aurora（+75% 效能、-28% 成本）、DraftKings 撐每分鐘 100 萬 ops 體育博彩、Standard Chartered 跨 7 個受監管市場、FanDuel 處理 Super Bowl 5-10 倍峰值 — 是 SQL OLTP managed 服務的代表。</p>
<h2 id="教學路線managed-sql-與平台責任轉移">教學路線：Managed SQL 與平台責任轉移</h2>
<p>Aurora 服務頁的教學目標是把 PostgreSQL / MySQL 語意延伸到 AWS managed storage / compute 分離模型。讀者讀完後要能判斷哪些責任交給 Aurora，哪些責任仍留在 schema、query、maintenance window、region 與成本治理。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Managed SQL</td>
          <td>Aurora 如何保留 PostgreSQL / MySQL 語意並改變操作責任</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Storage / compute</td>
          <td>分離 storage layer 如何影響 replica、failover、backup</td>
          <td>容量規劃要點、案例對照</td>
      </tr>
      <tr>
          <td>AWS operation model</td>
          <td>parameter group、maintenance、region、cost 如何成為平台責任</td>
          <td>跟其他 vendor 的取捨、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></td>
      </tr>
      <tr>
          <td>Peak workload</td>
          <td>金融、串流、Super Bowl、banking case 如何提供容量判準</td>
          <td>適用場景、案例對照</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時留 RDS、自管 PostgreSQL / MySQL、轉 Spanner 或 DynamoDB</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位storage--compute-分離的-sql">定位：storage / compute 分離的 SQL</h2>
<p>Aurora 跟傳統 PostgreSQL / MySQL primary 最大差異是 <em>storage layer 重寫</em>。傳統 SQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication lag 受 compute 影響。Aurora 把 storage 拉到分散式 log service、跨 6 個 storage node（3 AZ × 2 node）、storage 跟 compute 獨立擴。</p>
<p><strong>容量特性</strong>：</p>
<ul>
<li>單一 cluster 最高 storage：128 TB</li>
<li>最多 15 個 read replica（單 region 內）</li>
<li>read replica replication lag：10-30ms（vs 傳統 PostgreSQL 跨 AZ 可能秒級）</li>
<li>跨 AZ failover：&lt; 30 秒（promote read replica）</li>
<li>Aurora Global Database 跨 region replication：&lt; 1 秒典型 lag</li>
</ul>
<p><strong>為什麼這個分離很重要</strong>：</p>
<ul>
<li>傳統 PostgreSQL primary 上的 read replica 都靠 logical replication、會跟著 primary write load 走慢</li>
<li>Aurora storage 直接複製到 6 個 storage node、read replica 從 storage 讀、不靠 primary</li>
<li>→ read replica 大幅減少 lag、可以撐更多 OLTP read traffic</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> +75% 效能改善的關鍵原因</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p>按公開 case 提煉的典型適用場景：</p>
<p><strong>1. 既有 PostgreSQL / MySQL 應用想要 managed</strong>：</p>
<ul>
<li>wire protocol 相容，應用層改動通常集中在連線、參數與操作流程</li>
<li>ORM / driver / SQL 多數可保留，但 migration plan 仍要驗證 dialect 與 extension</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — 多套 RDBMS（PostgreSQL、MySQL、Oracle）統一到 Aurora、+75% 效能、-28% 成本</li>
</ul>
<p><strong>2. 金融交易 / 體育博彩 OLTP</strong>：</p>
<ul>
<li>強 ACID transaction</li>
<li>多 read replica 處理 query traffic、不影響寫</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 每分鐘 100 萬 ops、200 個獨立資料庫、Super Bowl 流量 +50% 無影響</li>
</ul>
<p><strong>3. 受監管產業跨市場部署</strong>：</p>
<ul>
<li>每個市場一個獨立 cluster、合規分割</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 7 個受監管市場、各自獨立 Aurora、總吞吐 4000 TPS、10x 提升</li>
</ul>
<p><strong>4. 高峰流量 + 多 read replica 擴容</strong>：</p>
<ul>
<li>read 高峰用 read replica 接、write 走 primary</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 5-10x Super Bowl 峰值、直播 + 投注雙工作負載</li>
</ul>
<p><strong>5. Aurora Serverless v2 適用場景</strong>：</p>
<ul>
<li>流量 unpredictable + sustained workload</li>
<li>自動 scale CPU / RAM，降低 instance class 管理負擔</li>
<li>適合：dev / test 環境、流量稀疏的多 tenant SaaS</li>
</ul>
<p><strong>6. Aurora Global Database</strong>：</p>
<ul>
<li>跨 region async replication（&lt; 1 秒 typical）</li>
<li>DR + 跨地理 read（write 在 primary region、read 可從 secondary region）</li>
<li>Global Database 是跨 region DR / read route，multi-region active-active write 要改看 Aurora DSQL</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 跨雲需求</strong>：</p>
<ul>
<li>Aurora 是 AWS-only、wire protocol 相容但 storage 是 AWS 專屬</li>
<li>替代：自管 PostgreSQL / MySQL on Kubernetes</li>
</ul>
<p><strong>2. 需要最新 upstream PostgreSQL / MySQL 特性</strong>：</p>
<ul>
<li>Aurora 通常落後 upstream 1-2 個 major version</li>
<li>替代：RDS PostgreSQL（更接近 upstream）</li>
</ul>
<p><strong>3. 極端寫入吞吐</strong>：</p>
<ul>
<li>單一 primary 寫入受 storage 設計限制（雖然比 PostgreSQL 快）</li>
<li>
<blockquote>
<p>100K WPS 級別、考慮 sharding、CockroachDB、或 DynamoDB</p></blockquote>
</li>
<li>對應 <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> — RDB connection limit 是 bottleneck、改 DynamoDB</li>
</ul>
<p><strong>4. 全球 multi-region active-active write</strong>：</p>
<ul>
<li>Aurora Global Database 是 async、有 lag，write 仍集中在 primary region</li>
<li>替代：Aurora DSQL（2024 推出）、Spanner、Cosmos DB</li>
</ul>
<p><strong>5. 預算敏感的小 workload</strong>：</p>
<ul>
<li>Aurora 比 self-managed PostgreSQL 貴 20-30%</li>
<li>小流量場景、自管 PostgreSQL on EC2 或 RDS 更便宜</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs RDS PostgreSQL / MySQL（同 AWS）</strong>：</p>
<ul>
<li>Aurora：storage / compute 分離、更多 read replica、更快 failover、跨 AZ 自動 replication</li>
<li>RDS：純 managed PostgreSQL / MySQL、不重寫 storage、更接近 upstream</li>
<li>選 Aurora：需要 scale read replica 或 cross-AZ failover &lt; 30 秒</li>
<li>選 RDS：需要最新 upstream 特性、預算更敏感</li>
</ul>
<p><strong>vs 自管 PostgreSQL / MySQL</strong>：</p>
<ul>
<li>Aurora：託管、自動 backup / failover，降低日常 database operation</li>
<li>自管：彈性高、可自己 tuning、跨雲可用、預算可控</li>
<li>選 Aurora：團隊想把 DBA / SRE 操作責任轉交 AWS、AWS 生態深</li>
<li>選自管：跨雲需求、需要客製化、預算極敏感</li>
</ul>
<p><strong>vs CockroachDB</strong>：</p>
<ul>
<li>Aurora：single-region scaling（一個 region 內擴）、AWS-only</li>
<li>CockroachDB：multi-region 強一致、跨雲可用、PostgreSQL wire protocol</li>
<li>選 Aurora：AWS-only + single-region OLTP</li>
<li>選 CockroachDB：需要 multi-region 強一致 + 跨雲 / on-prem 彈性</li>
</ul>
<p><strong>vs Aurora DSQL（2024-12 preview / 2025-05 GA）</strong>：</p>
<ul>
<li>Aurora：single-region scaling、傳統 OLTP</li>
<li>Aurora DSQL：multi-region active-active write、serverless、強一致</li>
<li>選 Aurora：流量集中在一個 region</li>
<li>選 Aurora DSQL：需要全球 active-active</li>
<li>從 PG / Aurora PG 遷 DSQL 的完整 playbook 見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL Migration</a></li>
</ul>
<p><strong>vs DynamoDB</strong>：</p>
<ul>
<li>詳見 <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 vendor page</a> 對比段。Aurora 是 SQL、DynamoDB 是 KV、適用場景不同。</li>
</ul>
<p><strong>vs Azure SQL Hyperscale</strong>：</p>
<ul>
<li>設計理念類似（storage / compute 分離）</li>
<li>Aurora 在 AWS、Hyperscale 在 Azure</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent</a> — Azure 生態的同類設計、5 億 payment txn / 年</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>從 09 案例庫提煉的 Aurora 容量規劃實踐：</p>
<p><strong>1. read replica 是擴 read traffic 的主要工具</strong>：</p>
<ul>
<li>最多 15 個 read replica、replication lag 10-30ms</li>
<li>read replica autoscaler 按 CPU / connection 自動加減</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 用多個 read replica 處理「比賽期間用戶查 balance」流量</li>
</ul>
<p><strong>2. 200 個獨立 cluster 模式</strong>：</p>
<ul>
<li>Aurora 的實務設計通常用多個 bounded cluster 控制 blast radius</li>
<li>按業務切多個小 cluster（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 200 個）、降低 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a></li>
<li>對應 microservice 私有 store（<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> 同樣思維）</li>
</ul>
<p><strong>3. Aurora I/O-Optimized</strong>：</p>
<ul>
<li>2023-05 推出的 storage 配置</li>
<li>適合 I/O-heavy workload（write 多、scan 多）</li>
<li>比 standard storage 貴、但少 I/O 收費</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 用 I/O-Optimized 加速</li>
</ul>
<p><strong>4. Aurora Serverless v2</strong>：</p>
<ul>
<li>ACU（Aurora Capacity Unit）為單位、自動 scale 0.5-128 ACU</li>
<li>適合 dev / test、稀疏 workload、unpredictable burst</li>
<li>不適合：sustained predictable high workload（provisioned 便宜）</li>
</ul>
<p><strong>5. Cross-region Global Database</strong>：</p>
<ul>
<li>&lt; 1 秒 typical replication lag、但是 async</li>
<li>secondary region 可 read，write 仍回 primary region</li>
<li>DR 切換通常 1-2 分鐘</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨市場各自獨立 Aurora，合規邊界優先於 Global Database</li>
</ul>
<p><strong>6. Connection pool 仍是隱性限制</strong>：</p>
<ul>
<li>Aurora 跟傳統 PostgreSQL 一樣有 connection pool 上限</li>
<li>應用層 + Aurora 之間建議用 RDS Proxy 做 pool 共享</li>
<li>對應 <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> — RDB connection limit 是 surge 場景的 bottleneck；Lemino 案例發生在 RDS，但 connection-bound 機制同樣適用 Aurora</li>
</ul>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本 vendor 現有 deep article 覆蓋 Aurora 從 storage architecture、fleet 治理到容量彈性、連線管理與 distributed 升級門檻的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>quorum-based 分散式 log、韌性即性能、6-way replication</td>
          <td><a href="storage-architecture/">storage-architecture</a></td>
          <td>4-of-6 write / 3-of-6 read、DraftKings 6ms 寫 / &lt;1ms 讀 production reference</td>
      </tr>
      <tr>
          <td>Cross-AZ failover lifecycle、&lt; 30 秒 RTO、endpoint routing</td>
          <td><a href="cross-az-failover-rto/">cross-az-failover-rto</a></td>
          <td>application DNS cache + connection pool 對齊、Standard Chartered 受監管獨立 cluster 而非 Global Database failover</td>
      </tr>
      <tr>
          <td>15 replica 上限、lag profile、headroom 預留、fleet 治理 3 條 driver</td>
          <td><a href="read-replica-scaling/">read-replica-scaling</a></td>
          <td>Aurora fleet 治理 SSoT、DraftKings headroom 預留、FanDuel 雙 SLO 並行</td>
      </tr>
      <tr>
          <td>跨 region async replication、&lt; 1 秒 lag、合規 anti-recommendation</td>
          <td><a href="global-database-multi-region/">global-database-multi-region</a></td>
          <td>planned vs unplanned failover RTO、Standard Chartered 合規禁止跨境複製反指標</td>
      </tr>
      <tr>
          <td>從自管 PostgreSQL / MySQL 遷到 Aurora（Type C operational redesign）</td>
          <td><a href="migrate-from-self-managed-pg-mysql/">migrate-from-self-managed-pg-mysql</a></td>
          <td>Standard Chartered 合規 lead time、Netflix 非 all-purpose store 邊界</td>
      </tr>
      <tr>
          <td>ACU 自動擴縮、min/max 設定、混合 cluster、成本 crossover</td>
          <td><a href="serverless-v2-scaling/">serverless-v2-scaling</a></td>
          <td>離峰浪費 vs 尖峰不足、穩定高負載 serverless 反而更貴</td>
      </tr>
      <tr>
          <td>多 cluster 業務切分、blast radius 隔離、fleet 治理</td>
          <td><a href="multi-cluster-business-split/">multi-cluster-business-split</a></td>
          <td>Netflix 微服務私有 store + DB 種類 consolidation 雙重成立</td>
      </tr>
      <tr>
          <td>RDS Proxy connection multiplexing、pinning 陷阱、failover 加速</td>
          <td><a href="rds-proxy-connection-pooling/">rds-proxy-connection-pooling</a></td>
          <td>Lambda 連線風暴、pinning 讓 multiplexing 失效</td>
      </tr>
      <tr>
          <td>standard Aurora vs Aurora DSQL 升級門檻取捨</td>
          <td><a href="aurora-vs-dsql-tradeoff/">aurora-vs-dsql-tradeoff</a></td>
          <td>single-writer 上限 vs active-active distributed、何時跨 paradigm</td>
      </tr>
  </tbody>
</table>
<p>I/O-Optimized vs Standard 成本對比由 <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 PostgreSQL I/O-Optimized Cost</a> 主寫（storage I/O 成本模型 SSoT），本 vendor 各篇提到 storage 成本時 cross-link 它、不重複展開。</p>
<p>跨 vendor entry：先看 <a href="../cockroachdb/aurora-dsql-spanner-decision-tree/">CockroachDB vs Aurora DSQL vs Spanner 決策樹</a>（distributed SQL 三選一 + 撞牆訊號分型），再決定是否進 Aurora overview。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>Aurora Global Database write forwarding 深入</li>
<li>Babelfish（SQL Server 相容層）適用判斷</li>
<li>Blue/Green deployment 做 major version 升級</li>
<li>Backup / PITR restore drill（hands-on lab）</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>Aurora 的 managed SQL 能把大量操作責任交給 AWS，但它仍保留 single-primary SQL 的資料模型與交易邊界。這一段先說何時維持 RDS / Aurora，再說何時升級 Global Database、Serverless v2、RDS Proxy、Aurora DSQL 或 DynamoDB。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RDS PostgreSQL / MySQL</td>
          <td>upstream 相容、成本、版本節奏比 storage 分離更重要</td>
          <td>read replica lag、backup / failover、storage growth 成主題</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor</a></td>
      </tr>
      <tr>
          <td>Aurora provisioned</td>
          <td>workload sustained、容量可預測、團隊能管理 instance class</td>
          <td>read replica、fast failover、storage autoscale 是主要需求</td>
          <td><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag</a>、<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover</a></td>
      </tr>
      <tr>
          <td>Aurora Serverless v2</td>
          <td>sustained workload 已穩定且 provisioned 成本較低</td>
          <td>稀疏 tenant、dev/test、不可預測 burst</td>
          <td><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>、<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></td>
      </tr>
      <tr>
          <td>RDS Proxy</td>
          <td>application pool 已能控制 backend connection</td>
          <td>Lambda / surge / connection storm 造成 pool 壓力</td>
          <td><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool</a></td>
      </tr>
      <tr>
          <td>Global Database</td>
          <td>single-region DR 已符合 RTO/RPO</td>
          <td>跨 region read、regional DR、低 RPO 是產品需求</td>
          <td><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a></td>
      </tr>
      <tr>
          <td>Aurora DSQL / Spanner / CockroachDB</td>
          <td>single-primary write 仍足夠</td>
          <td>multi-region active-active write、global strong consistency</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>SQL query 與 transaction 仍是主要價值</td>
          <td>access pattern 固定、connection-free surge、KV latency 成主題</td>
          <td><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 vendor</a></td>
      </tr>
  </tbody>
</table>
<p>Aurora 的簡單路徑是先把 operation transfer 寫清楚。Backup、minor upgrade、storage growth、failover 與 read replica lag 交給平台後，schema design、query shape、transaction boundary、connection pool 與 cost guardrail 仍由 application / SRE 共同承擔。</p>
<p>Global Database 的升級路徑要先定義讀寫方向。它適合 DR 與跨地理 read，若業務需要多 region 同時寫入並保持強一致，應直接進入 Aurora DSQL、Spanner 或 CockroachDB 的 distributed SQL 比較。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>Aurora overview 目前完成 managed SQL 判斷。下一輪 deep article / playbook 應補 storage architecture、RDS Proxy、Global Database、Serverless v2、I/O-Optimized cost、PostgreSQL / MySQL → Aurora migration 與 Aurora → Aurora DSQL 的分歧路徑。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>規模</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>1M ops/min、&lt;1ms reads、6ms writes、200 個 DB</td>
          <td>體育博彩金融帳本、按業務切 cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>4000 TPS、7 個受監管市場、10x 提升</td>
          <td>受監管金融跨市場部署</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>+75% 效能、-28% 成本</td>
          <td>多套 RDBMS 統一到 Aurora</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>Super Bowl 5-10x peak</td>
          <td>直播 + 投注雙工作負載</td>
      </tr>
  </tbody>
</table>
<p>Aurora case 的讀法是看 operation transfer 如何變成容量與成本結果。DraftKings 與 FanDuel 提供 peak OLTP 訊號，Standard Chartered 提供合規分區訊號，Netflix 則提供多套 RDBMS 整併到 managed SQL 的組織與成本訊號。</p>
<h2 id="反向-sibling-路由">反向 sibling 路由</h2>
<p>Aurora 的反向 sibling 路由用來避免把 managed SQL 誤讀成唯一升級方向。若讀者從 PostgreSQL / MySQL 章節過來，先對照 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 與 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">MySQL → Aurora</a>；若核心需求是 connection surge，補讀 <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 vendor</a> 與 <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 case</a>；若核心需求是 multi-region active-active write，轉到 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 或 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a>。</p>
<p>這條路由的判準是先問「保留 SQL + 轉移 operation」是否足夠。答案成立時，Aurora 是 RDS / 自管 MySQL / 自管 PostgreSQL 的 managed endpoint；答案需要改成 global quorum、partition-key access pattern 或 document API 時，Aurora 應退到對照組，而非成為最後選項。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>誤以為 Aurora 等於無限擴</strong>：寫吞吐仍受 primary 限制，容量曲線和 distributed SQL 不同</li>
<li><strong>忽略 read replica</strong>：把所有 query 打 primary，會浪費 read replica scaling 能力</li>
<li><strong>跨 region 強一致誤解</strong>：Global Database 是 <em>async</em> 複製，multi-region active-active 要看 Aurora DSQL / Spanner / CockroachDB</li>
<li><strong>connection pool 忽略</strong>：Aurora 仍是 PostgreSQL / MySQL、connection 上限有效</li>
<li><strong>單一巨大 cluster</strong>：把所有業務塞進一個 cluster 會放大 blast radius，通常要按業務切</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<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 vendor page</a>（NoSQL 對比）</li>
<li>上游：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（從 RDS / 自管遷到 Aurora）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>、<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>Last reviewed：2026-05-22（Aurora storage / Serverless / Global Database / I/O-Optimized 屬時間敏感 claim）</li>
<li>官方：<a href="https://aws.amazon.com/rds/aurora/">Amazon Aurora</a>、<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html">Aurora storage architecture</a></li>
</ul>
]]></content:encoded></item><item><title>9.7 成本邊界與 efficiency</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 &lt;em>在每一個容量決策點&lt;/em> 把「多保險」跟「多省錢」一起評估。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。&lt;/p>
&lt;p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。&lt;/p>
&lt;h2 id="cost-per-request-模型">Cost per request 模型&lt;/h2>
&lt;p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。&lt;/p>
&lt;p>&lt;strong>基本公式&lt;/strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
&lt;strong>分 stage 拆解&lt;/strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
&lt;strong>分 endpoint 拆解&lt;/strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。&lt;/p>
&lt;p>&lt;strong>對齊業務 metric&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>cost per active user：總成本 / MAU&lt;/li>
&lt;li>cost per transaction：總成本 / 完成的訂單數&lt;/li>
&lt;li>cost per ML inference：總成本 / inference 次數&lt;/li>
&lt;/ul>
&lt;p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降&lt;/a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降&lt;/a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片&lt;/a>。&lt;/p>
&lt;h2 id="cost-curve-形狀">Cost curve 形狀&lt;/h2>
&lt;p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。&lt;/p>
&lt;p>&lt;strong>On-demand（pay-per-use）&lt;/strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
&lt;strong>Reserved instances（RI）/ Savings Plans&lt;/strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
&lt;strong>Spot instances&lt;/strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 <em>在每一個容量決策點</em> 把「多保險」跟「多省錢」一起評估。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。</p>
<p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。</p>
<h2 id="cost-per-request-模型">Cost per request 模型</h2>
<p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。</p>
<p><strong>基本公式</strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
<strong>分 stage 拆解</strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
<strong>分 endpoint 拆解</strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。</p>
<p><strong>對齊業務 metric</strong>：</p>
<ul>
<li>cost per active user：總成本 / MAU</li>
<li>cost per transaction：總成本 / 完成的訂單數</li>
<li>cost per ML inference：總成本 / inference 次數</li>
</ul>
<p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降</a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降</a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片</a>。</p>
<h2 id="cost-curve-形狀">Cost curve 形狀</h2>
<p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。</p>
<p><strong>On-demand（pay-per-use）</strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
<strong>Reserved instances（RI）/ Savings Plans</strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
<strong>Spot instances</strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。</p>
<p><strong>最佳組合通常是「Reserved baseline + On-demand spike + Spot batch」</strong>：</p>
<ul>
<li>Reserved 覆蓋 baseline 容量（永遠用得到）</li>
<li>On-demand 處理 peak 跟 unpredicted burst</li>
<li>Spot 跑 batch 工作（不在 critical path、可被中斷）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">Riot Games 年省 1000 萬</a> — 從自管 Mesos 遷到 EKS、降的不只是 instance cost、是 cluster 管理人力 + ops 簡化；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 30% 成本下降</a> — DynamoDB + EKS 取代自管、釋放 DBA 人力。</p>
<h2 id="over-provisioning-vs-under-provisioning-取捨">Over-provisioning vs under-provisioning 取捨</h2>
<p>容量決策的核心經濟學問題：訂多大容量才是最划算？</p>
<p><strong>Over-provisioning 成本</strong>：每月多付 $X 雲端費。這個數字直接看帳單。
<strong>Under-provisioning 成本</strong>：sigma 機率 × downtime × revenue per minute。這個數字更難算 — 需要 historical incident rate + downtime impact analysis。</p>
<p><strong>兩個成本平衡點 = 經濟最佳 headroom</strong>。但實務上 under-provisioning 成本不容易量化、保守做法是把 sigma 機率拉高（用 worst-case 估）、headroom 訂寬一點。</p>
<p><strong>Critical workload</strong>（金融、醫療、付款）：under-provisioning 成本極高（合約違約 + 客戶流失 + 法規）、寧可 over-provisioning 30-50%。
<strong>Non-critical workload</strong>（內部工具、分析、batch）：under-provisioning 成本低、可以更貼近 minimum capacity。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato TiDB 必須 over-provision</a> — 為了應付 spike、TiDB 必須長期 over-provision；DynamoDB on-demand 不必、pay-per-use 自然處理。</p>
<h2 id="降級的成本邊界">降級的成本邊界</h2>
<p>「降級 vs 擴容」是常見容量決策、但常被當成「技術問題」而非「成本問題」。</p>
<p><strong>降級不是免費</strong>：</p>
<ul>
<li>流失轉換：UI 顯示「系統忙碌」、用戶可能放棄</li>
<li>客訴成本：客服處理客訴的 OpEx</li>
<li>品牌損失：社群媒體負面評論、口碑下降</li>
<li>合約違約：B2B 客戶可能基於 SLA 求償</li>
</ul>
<p><strong>算「降級 vs 擴容」哪個成本低</strong>：</p>
<ul>
<li>擴容成本：peak 時段多付的 cloud 費用</li>
<li>降級成本：上述四項合計</li>
<li>哪邊低就選哪邊</li>
</ul>
<p><strong>降級觸發條件</strong>通常按負載門檻 / 成本門檻 / SLA 觸發：</p>
<ul>
<li>負載門檻：utilization &gt; 85% → 啟動降級</li>
<li>成本門檻：本月雲端費已超預算 X% → 啟動降級</li>
<li>SLA 觸發：<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 快用完 → 啟動降級保 SLA</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO 50x surge</a> — surge 期間無法等比擴容、必須降級保住核心遊戲機制、犧牲附加功能。</p>
<h2 id="人力成本工程化">人力成本工程化</h2>
<p>雲端帳單是顯性成本、但 <em>人力成本</em> 是常被忽略的隱性容量成本。</p>
<p><strong>自建 vs managed 的人力成本對比</strong>：</p>
<ul>
<li>自建 Kafka / PostgreSQL / Redis：需要 DBA / SRE 持續維護 + 升級 + 故障處理</li>
<li>Managed 服務（MSK、Aurora、ElastiCache）：vendor 負責 patch、backup、failover</li>
<li>差距通常 <em>3-10 倍</em> 人力成本</li>
</ul>
<p><strong>DBA / SRE / network engineer 都是隱性容量成本</strong>：</p>
<ul>
<li>一個資深 DBA 在美國年薪 $200K+、台灣 NTD 200-400 萬</li>
<li>工程師時間是有上限的、自管系統佔的時間就是 <em>無法投入產品開發</em> 的機會成本</li>
</ul>
<p><strong>「90% 工程工時下降」是管理 ROI 的關鍵</strong>：重點是把工程資源從 <em>維持</em> 轉移到 <em>建構</em>、不是拿來吹噓技術。這條自建 vs managed 的人力成本對比、是 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a> 裡「計費隨規模成長、自建 TCO 出現交叉點」那條 tripwire 的算法側 — 選型方向在 0.22 判、成本量化在這裡做。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">Spotify Kafka → Pub/Sub</a> — 不是因為 Pub/Sub 便宜、是因為 Spotify 規模下自管 Kafka 的人力成本不划算；<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 90% 工程工時降</a> — managed 路線讓電信商級新串流服務只用 5-10 個工程師 launch；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom DBA 釋放</a> — 把 DBA 時間從 patching 轉到遊戲品質。</p>
<h2 id="finops-跟容量規劃的整合">FinOps 跟容量規劃的整合</h2>
<p>FinOps 是 <em>財務跟工程的協作框架</em>、把成本決策從事後對帳變成事前規劃。</p>
<p><strong>Showback / chargeback</strong>：把雲端成本攤到團隊 / 服務 / feature。每個團隊看得到自己的成本、自然開始 optimize。chargeback（實際扣預算）比 showback（純展示）更有效但組織複雜度高。</p>
<p><strong>每月 cost review 變成容量 review 的一部分</strong>：</p>
<ul>
<li>對比預算 vs 實際</li>
<li>找出 top 5 cost driver</li>
<li>對比上月趨勢、看是否有 anomaly</li>
<li>跟 capacity team 一起討論 right-sizing</li>
</ul>
<p><strong>Spot diversification</strong>：spot 中斷風險可以靠 <em>多 instance type 跟多 AZ</em> 分散。例如：spot pool 同時包含 m5.large + m5a.large + m5n.large、各 AZ 都有、單一 type pool 撤回時其他 type 還在。</p>
<p><strong>Right-sizing</strong>：定期 review instance type 是否最適。常見浪費：訂太大 instance（CPU / RAM 用 30%）、過時 instance generation（用 c5 沒升到 c7）、reserved 過剩。</p>
<h2 id="反模式">反模式</h2>
<p>容量成本的常見錯誤模式：</p>
<p><strong>Autoscaling max 設無限大</strong>：流量爆衝時 autoscaler 跟著爆衝、月底帳單炸裂。max 必須訂、是 financial circuit breaker。</p>
<p><strong>全部用 on-demand、沒談 reserved / savings plan</strong>：cloud spending &gt; $10K/月 已經值得跟雲商 talk discount、savings plan 通常 30-60% off。</p>
<p><strong>沒成本 monitoring、直到帳單來才知道</strong>：要建 daily cost dashboard、anomaly 即時 alert、不要等月帳單。</p>
<p><strong>降級用人工觸發、出事時來不及</strong>：降級邏輯要 <em>自動化</em>、按 metric 觸發、不是 oncall 工程師看到 dashboard 才下指令。</p>
<p><strong>忘了人力成本</strong>：算 build vs buy 只算 cloud 費、忘了 SRE / DBA 時間、結果發現「省的 cloud 費 &lt; 多花的人力」。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>50% 成本下降（從 over-provision 解放）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a></td>
          <td>年省 1000 萬（EKS 替代 Mesos）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>28% 成本下降（DB consolidation）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>90% 工程工時降（managed 路線）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom</a></td>
          <td>30% 成本下降（DBA 釋放到遊戲品質）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a>（cost attribution）</li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.C7 Lyft：100+ 微服務在 8 倍峰值下的 Auto Scaling</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/</guid><description>&lt;p>這個案例的核心責任是說明「微服務架構在事件型峰值下的容量治理」。共乘服務的負載形狀獨特 — 平日早晚通勤雙峰、週末晚間爆量、特殊事件（演唱會、球賽結束、機場）瞬間爆量、每個城市跟每個時段都不同。100+ 個微服務各自有不同的峰值時段、需要獨立擴容策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Lyft 在 AWS 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/lyft/">Lyft case study&lt;/a>）：&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>峰值倍數&lt;/td>
 &lt;td>8x 平日基線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>微服務數&lt;/td>
 &lt;td>100+ 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>月均搭乘&lt;/td>
 &lt;td>1400 萬 / 月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務城市&lt;/td>
 &lt;td>200+&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Amazon DynamoDB（搭乘追蹤、GPS 座標）、Amazon Redshift（客戶洞察）、Amazon Kinesis（即時事件串流）、AWS Auto Scaling、Amazon EC2 Container Registry。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Lyft 的工程做法揭露三個微服務容量治理重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>微服務不是「全部 8x」、是「特定服務 8x」&lt;/strong>：8x 是 &lt;em>某些核心服務&lt;/em> 在週末爆量時刻的擴容比、不是 100 個服務全部 8x。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 必須先做「哪個服務是熱點」的層次定位。&lt;/li>
&lt;li>&lt;strong>微服務粒度 = 擴容粒度&lt;/strong>：把 ride matching、payment、driver tracking、notification 切成獨立服務、每個服務的 autoscaling policy 可以獨立設計。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的服務邊界。&lt;/li>
&lt;li>&lt;strong>GPS 座標寫入 DynamoDB 是高頻 sustained workload&lt;/strong>：每個 driver 每秒寫 1-2 次位置、200+ 城市 × 每個城市數萬司機 = 巨量持續寫入、跟峰值無關。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 的 KV 高吞吐設計同類。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「8x 峰值」是 &lt;em>峰值倍數&lt;/em>、不是 &lt;em>尖峰持續時間&lt;/em>。週末晚間的尖峰可能持續 3-4 小時、機場特殊事件可能持續 30 分鐘、演唱會結束可能只有 10 分鐘瞬間。容量策略要按持續時間區分。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>微服務粒度切到「同性質擴容單位」&lt;/strong>：同步 vs async、stateful vs stateless、CPU-bound vs I/O-bound 不該混在同一服務、否則擴容邏輯互相衝突。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 service decomposition。&lt;/li>
&lt;li>&lt;strong>預測式 + 反應式擴容混用&lt;/strong>：可預測（早晚通勤）用 scheduled scaling、不可預測（演唱會散場）用 reactive autoscaling、兩者組合。&lt;/li>
&lt;li>&lt;strong>GPS 類持續寫入適合 KV / time-series store&lt;/strong>：不適合放 OLTP DB、會佔用 transaction 資源。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 storage choice。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP GKE + HPA / VPA / Karpenter、Azure AKS + KEDA、自建 Kubernetes + Cluster Autoscaler 都可以實作對等架構。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想做微服務容量治理 → &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a>&lt;/li>
&lt;li>想規劃事件型峰值 → &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &amp;#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech&lt;/a>&lt;/li>
&lt;li>想設計高頻 sustained workload → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/lyft/">Lyft Case Study&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「微服務架構在事件型峰值下的容量治理」。共乘服務的負載形狀獨特 — 平日早晚通勤雙峰、週末晚間爆量、特殊事件（演唱會、球賽結束、機場）瞬間爆量、每個城市跟每個時段都不同。100+ 個微服務各自有不同的峰值時段、需要獨立擴容策略。</p>
<h2 id="觀察">觀察</h2>
<p>Lyft 在 AWS 的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/lyft/">Lyft case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>峰值倍數</td>
          <td>8x 平日基線</td>
      </tr>
      <tr>
          <td>微服務數</td>
          <td>100+ 個</td>
      </tr>
      <tr>
          <td>月均搭乘</td>
          <td>1400 萬 / 月</td>
      </tr>
      <tr>
          <td>服務城市</td>
          <td>200+</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Amazon DynamoDB（搭乘追蹤、GPS 座標）、Amazon Redshift（客戶洞察）、Amazon Kinesis（即時事件串流）、AWS Auto Scaling、Amazon EC2 Container Registry。</p>
<h2 id="判讀">判讀</h2>
<p>Lyft 的工程做法揭露三個微服務容量治理重點。</p>
<ol>
<li><strong>微服務不是「全部 8x」、是「特定服務 8x」</strong>：8x 是 <em>某些核心服務</em> 在週末爆量時刻的擴容比、不是 100 個服務全部 8x。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 必須先做「哪個服務是熱點」的層次定位。</li>
<li><strong>微服務粒度 = 擴容粒度</strong>：把 ride matching、payment、driver tracking、notification 切成獨立服務、每個服務的 autoscaling policy 可以獨立設計。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 跟 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的服務邊界。</li>
<li><strong>GPS 座標寫入 DynamoDB 是高頻 sustained workload</strong>：每個 driver 每秒寫 1-2 次位置、200+ 城市 × 每個城市數萬司機 = 巨量持續寫入、跟峰值無關。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 的 KV 高吞吐設計同類。</li>
</ol>
<p>需要警惕：「8x 峰值」是 <em>峰值倍數</em>、不是 <em>尖峰持續時間</em>。週末晚間的尖峰可能持續 3-4 小時、機場特殊事件可能持續 30 分鐘、演唱會結束可能只有 10 分鐘瞬間。容量策略要按持續時間區分。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>微服務粒度切到「同性質擴容單位」</strong>：同步 vs async、stateful vs stateless、CPU-bound vs I/O-bound 不該混在同一服務、否則擴容邏輯互相衝突。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 service decomposition。</li>
<li><strong>預測式 + 反應式擴容混用</strong>：可預測（早晚通勤）用 scheduled scaling、不可預測（演唱會散場）用 reactive autoscaling、兩者組合。</li>
<li><strong>GPS 類持續寫入適合 KV / time-series store</strong>：不適合放 OLTP DB、會佔用 transaction 資源。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 storage choice。</li>
</ol>
<p>跨平台等效：GCP GKE + HPA / VPA / Karpenter、Azure AKS + KEDA、自建 Kubernetes + Cluster Autoscaler 都可以實作對等架構。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想做微服務容量治理 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想規劃事件型峰值 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a></li>
<li>想設計高頻 sustained workload → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/lyft/">Lyft Case Study</a></li>
<li><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers</a></li>
</ul>
]]></content:encoded></item><item><title>1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範</title><link>https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/</guid><description>&lt;p>Schema migration rollout 證據（Schema Migration Rollout Evidence）的核心責任是把正式狀態的演進拆成可觀測、可放行、可停止與可回寫的服務路徑。這篇以訂單資料表的付款狀態欄位演進為例，示範資料庫變更如何從 schema design、backfill、cutover 交接到 evidence package、release gate 與 incident decision log。&lt;/p>
&lt;h2 id="服務路徑與狀態責任">服務路徑與狀態責任&lt;/h2>
&lt;p>這條服務路徑是 &lt;code>checkout-api -&amp;gt; order-db -&amp;gt; payment-callback -&amp;gt; reconciliation-job&lt;/code>。Checkout 建立訂單時先寫入訂單主檔與付款待確認狀態；payment callback 會更新付款結果；客服後台與對帳 job 會讀取同一筆訂單狀態來判斷是否需要補償、退款或人工處理。&lt;/p>
&lt;p>本篇示範的變更是把原本單一 &lt;code>status&lt;/code> 欄位中的付款語意拆到 &lt;code>payment_state&lt;/code>。這個欄位屬於正式狀態，會影響使用者看到的訂單結果、付款回呼的冪等更新、客服查詢與對帳流程，因此 rollout 的核心是讓新舊狀態語意在過渡期同時成立；DDL 只是其中一個執行動作。&lt;/p>
&lt;p>這條路徑的前置概念來自 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作&lt;/a>。1.2 定義欄位責任，1.3 定義哪些更新要在同一個交易邊界內成立，1.6 定義 expand、backfill、cutover 與 contract 的執行節奏。&lt;/p>
&lt;h2 id="rollout-階段">Rollout 階段&lt;/h2>
&lt;p>Migration rollout 的責任是把一次高風險資料變更切成多個可驗證階段。每個階段都要有輸入條件、完成訊號與停止條件，讓團隊能在資料漂移擴大前停下來。&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>Expand&lt;/td>
 &lt;td>新欄位與新程式碼能和舊版本共存&lt;/td>
 &lt;td>新舊程式可同時讀寫，舊欄位仍可支撐服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backfill&lt;/td>
 &lt;td>歷史訂單補齊 &lt;code>payment_state&lt;/code>&lt;/td>
 &lt;td>checkpoint 穩定前進，mismatch 維持在門檻內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cutover&lt;/td>
 &lt;td>讀取路徑改以新欄位為主&lt;/td>
 &lt;td>新欄位讀取成功率與對帳結果達到放行條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Contract&lt;/td>
 &lt;td>移除舊語意與舊寫入路徑&lt;/td>
 &lt;td>舊欄位已無服務依賴，回寫與監控已更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是責任轉移。Expand 保護相容性，backfill 保護歷史資料，cutover 保護線上讀取，contract 保護長期維護成本；四者對應不同 evidence，也需要不同 release gate 判讀。&lt;/p>
&lt;h2 id="實作基準先寫出狀態契約">實作基準：先寫出狀態契約&lt;/h2>
&lt;p>狀態契約的責任是讓 migration 先有可驗證的語意邊界。這篇的範例把 &lt;code>orders.status&lt;/code> 裡混合的訂單生命週期與付款語意拆開：訂單仍用 &lt;code>status&lt;/code> 表示 &lt;code>created&lt;/code>、&lt;code>fulfilled&lt;/code>、&lt;code>cancelled&lt;/code> 這類流程狀態，付款結果則交給 &lt;code>payment_state&lt;/code> 表示 &lt;code>pending&lt;/code>、&lt;code>authorized&lt;/code>、&lt;code>captured&lt;/code>、&lt;code>failed&lt;/code> 與 &lt;code>refunded&lt;/code>。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>舊狀態&lt;/th>
 &lt;th>新欄位 &lt;code>payment_state&lt;/code>&lt;/th>
 &lt;th>判讀理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>pending_payment&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>訂單已建立，付款結果仍未確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>paid&lt;/code>&lt;/td>
 &lt;td>&lt;code>captured&lt;/code>&lt;/td>
 &lt;td>付款已完成，可進入出貨或履約流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>payment_failed&lt;/code>&lt;/td>
 &lt;td>&lt;code>failed&lt;/code>&lt;/td>
 &lt;td>付款失敗，需要重試或取消路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>refunded&lt;/code>&lt;/td>
 &lt;td>&lt;code>refunded&lt;/code>&lt;/td>
 &lt;td>付款已逆向處理，客服與對帳要可查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cancelled_before_pay&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>沒有付款成功事實，只保留流程取消&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>manual_review_required&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>付款狀態未完成，等待人工判讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mapping-table/" data-link-title="Mapping Table" data-link-desc="說明遷移或轉換期間如何把舊語意明確對應到新語意">mapping table&lt;/a> 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a>、backfill job 與 incident decision log 的共同語意來源。Mapping table 留在工程師腦中時，後續 mismatch 會變成「資料看起來怪」；mapping table 進入 artifact 後，gate 就能判斷錯誤集中在哪個付款語意，而不是停在總筆數。&lt;/p>
&lt;h2 id="expand先建立相容窗口">Expand：先建立相容窗口&lt;/h2>
&lt;p>Expand phase 的核心責任是讓新資料結構先進入 production，同時保留舊程式的可運作性。以 &lt;code>payment_state&lt;/code> 為例，常見起點是新增 nullable 欄位、補上必要索引，並讓寫入路徑可以在新欄位缺值時仍使用舊 &lt;code>status&lt;/code> 判讀付款狀態。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CONCURRENTLY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_orders_payment_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 SQL 的用途是示範 artifact 形狀。Nullable 欄位保留舊資料的相容窗口；partial index 讓新讀取路徑能先被驗證，同時避免把尚未 backfill 的歷史資料全部推進新查詢模型。不同資料庫會有不同線上 DDL 能力，release gate 要把 lock 行為、index build 進度與 replication lag 納入 checks。&lt;/p></description><content:encoded><![CDATA[<p>Schema migration rollout 證據（Schema Migration Rollout Evidence）的核心責任是把正式狀態的演進拆成可觀測、可放行、可停止與可回寫的服務路徑。這篇以訂單資料表的付款狀態欄位演進為例，示範資料庫變更如何從 schema design、backfill、cutover 交接到 evidence package、release gate 與 incident decision log。</p>
<h2 id="服務路徑與狀態責任">服務路徑與狀態責任</h2>
<p>這條服務路徑是 <code>checkout-api -&gt; order-db -&gt; payment-callback -&gt; reconciliation-job</code>。Checkout 建立訂單時先寫入訂單主檔與付款待確認狀態；payment callback 會更新付款結果；客服後台與對帳 job 會讀取同一筆訂單狀態來判斷是否需要補償、退款或人工處理。</p>
<p>本篇示範的變更是把原本單一 <code>status</code> 欄位中的付款語意拆到 <code>payment_state</code>。這個欄位屬於正式狀態，會影響使用者看到的訂單結果、付款回呼的冪等更新、客服查詢與對帳流程，因此 rollout 的核心是讓新舊狀態語意在過渡期同時成立；DDL 只是其中一個執行動作。</p>
<p>這條路徑的前置概念來自 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a> 與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>。1.2 定義欄位責任，1.3 定義哪些更新要在同一個交易邊界內成立，1.6 定義 expand、backfill、cutover 與 contract 的執行節奏。</p>
<h2 id="rollout-階段">Rollout 階段</h2>
<p>Migration rollout 的責任是把一次高風險資料變更切成多個可驗證階段。每個階段都要有輸入條件、完成訊號與停止條件，讓團隊能在資料漂移擴大前停下來。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>服務責任</th>
          <th>完成訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Expand</td>
          <td>新欄位與新程式碼能和舊版本共存</td>
          <td>新舊程式可同時讀寫，舊欄位仍可支撐服務</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>歷史訂單補齊 <code>payment_state</code></td>
          <td>checkpoint 穩定前進，mismatch 維持在門檻內</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>讀取路徑改以新欄位為主</td>
          <td>新欄位讀取成功率與對帳結果達到放行條件</td>
      </tr>
      <tr>
          <td>Contract</td>
          <td>移除舊語意與舊寫入路徑</td>
          <td>舊欄位已無服務依賴，回寫與監控已更新</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是責任轉移。Expand 保護相容性，backfill 保護歷史資料，cutover 保護線上讀取，contract 保護長期維護成本；四者對應不同 evidence，也需要不同 release gate 判讀。</p>
<h2 id="實作基準先寫出狀態契約">實作基準：先寫出狀態契約</h2>
<p>狀態契約的責任是讓 migration 先有可驗證的語意邊界。這篇的範例把 <code>orders.status</code> 裡混合的訂單生命週期與付款語意拆開：訂單仍用 <code>status</code> 表示 <code>created</code>、<code>fulfilled</code>、<code>cancelled</code> 這類流程狀態，付款結果則交給 <code>payment_state</code> 表示 <code>pending</code>、<code>authorized</code>、<code>captured</code>、<code>failed</code> 與 <code>refunded</code>。</p>
<table>
  <thead>
      <tr>
          <th>舊狀態</th>
          <th>新欄位 <code>payment_state</code></th>
          <th>判讀理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pending_payment</code></td>
          <td><code>pending</code></td>
          <td>訂單已建立，付款結果仍未確認</td>
      </tr>
      <tr>
          <td><code>paid</code></td>
          <td><code>captured</code></td>
          <td>付款已完成，可進入出貨或履約流程</td>
      </tr>
      <tr>
          <td><code>payment_failed</code></td>
          <td><code>failed</code></td>
          <td>付款失敗，需要重試或取消路由</td>
      </tr>
      <tr>
          <td><code>refunded</code></td>
          <td><code>refunded</code></td>
          <td>付款已逆向處理，客服與對帳要可查</td>
      </tr>
      <tr>
          <td><code>cancelled_before_pay</code></td>
          <td><code>pending</code></td>
          <td>沒有付款成功事實，只保留流程取消</td>
      </tr>
      <tr>
          <td><code>manual_review_required</code></td>
          <td><code>pending</code></td>
          <td>付款狀態未完成，等待人工判讀</td>
      </tr>
  </tbody>
</table>
<p>這張 <a href="/blog/backend/knowledge-cards/mapping-table/" data-link-title="Mapping Table" data-link-desc="說明遷移或轉換期間如何把舊語意明確對應到新語意">mapping table</a> 是 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、backfill job 與 incident decision log 的共同語意來源。Mapping table 留在工程師腦中時，後續 mismatch 會變成「資料看起來怪」；mapping table 進入 artifact 後，gate 就能判斷錯誤集中在哪個付款語意，而不是停在總筆數。</p>
<h2 id="expand先建立相容窗口">Expand：先建立相容窗口</h2>
<p>Expand phase 的核心責任是讓新資料結構先進入 production，同時保留舊程式的可運作性。以 <code>payment_state</code> 為例，常見起點是新增 nullable 欄位、補上必要索引，並讓寫入路徑可以在新欄位缺值時仍使用舊 <code>status</code> 判讀付款狀態。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">idx_orders_payment_state</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">payment_state</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span></span></span></code></pre></div><p>這段 SQL 的用途是示範 artifact 形狀。Nullable 欄位保留舊資料的相容窗口；partial index 讓新讀取路徑能先被驗證，同時避免把尚未 backfill 的歷史資料全部推進新查詢模型。不同資料庫會有不同線上 DDL 能力，release gate 要把 lock 行為、index build 進度與 replication lag 納入 checks。</p>
<p>應用程式在 expand 階段要支援 <a href="/blog/backend/knowledge-cards/read-compatibility/" data-link-title="Read Compatibility" data-link-desc="說明資料或服務演進期間讀取路徑如何同時支援新舊語意">read compatibility</a>。相容性較高的寫法是讀取時優先使用 <code>payment_state</code>，缺值時 fallback 到舊 <code>status</code> 的付款語意；寫入時則依交易邊界同步更新舊欄位與新欄位，直到 cutover 前都保留一致性檢查。</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">readPaymentState(order):
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  if order.payment_state is not null:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    return order.payment_state
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  return mapLegacyStatusToPaymentState(order.status)
</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">applyPaymentCallback(order, callback):
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  nextPaymentState = mapCallbackToPaymentState(callback)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  update orders
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    set status = mapPaymentStateToLegacyStatus(nextPaymentState),
</span></span><span class="line"><span class="ln">10</span><span class="cl">        payment_state = nextPaymentState
</span></span><span class="line"><span class="ln">11</span><span class="cl">    where id = order.id</span></span></code></pre></div><p>這段相容讀寫的重點是「同一個 callback 只產生一個付款判讀」。舊欄位與新欄位可以同時存在，但它們要由同一份 mapping function 產生，否則 payment callback、客服修復與 reconciliation job 會各自形成一套隱性規則。</p>
<p>這裡要特別看 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a> 的風險。雙寫只表示兩個欄位都有被寫入，仍要用 validation query 驗證兩者語意是否一致。若付款回呼、手動退款與對帳修復走不同程式路徑，雙寫函式也要被這些路徑共同使用。</p>
<h3 id="dual-write-divergence-schema">Dual-write divergence schema</h3>
<p>Dual-write 的責任不只是「兩邊都寫」、是「兩邊寫的結果一致」。要證明這件事、需要明確的 divergence schema、否則事故當下無法區分 mapping bug 跟 race condition。</p>
<p>最小 divergence 紀錄欄位：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>order_id</code></td>
          <td>哪一筆訂單</td>
      </tr>
      <tr>
          <td><code>legacy_value</code></td>
          <td>舊欄位寫入後的值</td>
      </tr>
      <tr>
          <td><code>new_value</code></td>
          <td>新欄位寫入後的值</td>
      </tr>
      <tr>
          <td><code>expected_new</code></td>
          <td>用 mapping function 從 <code>legacy_value</code> 推算的預期新值</td>
      </tr>
      <tr>
          <td><code>divergence_type</code></td>
          <td><code>mapping-mismatch</code> / <code>race-condition</code> / <code>manual-override</code></td>
      </tr>
      <tr>
          <td><code>write_path</code></td>
          <td>哪個程式路徑寫的（callback / refund / manual / reconciliation）</td>
      </tr>
      <tr>
          <td><code>detected_at</code></td>
          <td>偵測時間</td>
      </tr>
  </tbody>
</table>
<p><code>expected_new</code> 跟 <code>new_value</code> 對不上、表示 mapping function 在某些 path 沒被使用、是 mapping bug。<code>legacy_value</code> 跟 <code>new_value</code> 對不上、且 <code>expected_new == legacy_value</code> 對得上、是 dual-write 本身少寫一筆、可能是 race condition 或部分失敗。兩種情況的修法完全不同、不分類會在事故當下亂修。</p>
<p>Dual-write 失敗回退策略：寫舊欄位成功、寫新欄位失敗時、不能直接 retry 新欄位（會跟主寫入競爭）。實務做法是把 divergence 寫進 outbox / repair queue、由 backfill 同類流程補。對應 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a> 的 outbox-style 設計。</p>
<h3 id="線上-ddl-的-vendor-差異">線上 DDL 的 vendor 差異</h3>
<p>Expand 階段加欄位 / 加索引、不同資料庫的 <em>阻塞行為</em> 差異極大、選錯時機會直接讓 production 鎖表。</p>
<ul>
<li><strong>PostgreSQL</strong>：<code>ALTER TABLE ADD COLUMN ... NULL</code> 是 metadata-only、不重寫 table。<code>ADD COLUMN ... NOT NULL DEFAULT ...</code> 在 PG 11+ 才是 metadata-only。<code>CREATE INDEX CONCURRENTLY</code> 不阻塞寫入、但更慢、且 transaction 中不能用。<code>ALTER TABLE ALTER COLUMN TYPE</code> 通常會重寫整張表、要先評估規模。</li>
<li><strong>MySQL / Aurora MySQL</strong>：<code>ALTER TABLE ... ALGORITHM=INSTANT</code> 是 8.0+ 的 metadata-only、5.7 則靠 <code>ALGORITHM=INPLACE</code> / <code>LOCK=NONE</code>。Aurora MySQL 還有 fast DDL（部分變更秒級完成、不重寫）。判讀重點是 <em>explicitly 指定 ALGORITHM</em>、不要讓 MySQL 自己選（可能掉回 COPY 算法、整張表複製）。</li>
<li><strong>Spanner</strong>：schema change 預設非阻塞、後端 async 補欄位。新欄位 read 在 schema change 完成前可能讀不到、應用層要容忍。</li>
<li><strong>DynamoDB</strong>：表本身沒 schema、但 <em>GSI（Global Secondary Index）創建是 async</em>、可能跑數小時、且新 GSI 在 backfill 完成前查不到完整資料。判讀重點：cutover 不能假設新 GSI 立即可用、要等 <code>IndexStatus = ACTIVE</code>。</li>
<li><strong>Cosmos DB</strong>：document 級別無 schema、新 indexed path 加進 indexing policy 後、後端 <em>re-index</em> 整個 partition、期間 RU consumption 飆升。</li>
</ul>
<p>各 vendor 的線上 DDL evidence 都要包含：操作開始時間、預估完成時間、是否阻塞讀寫、實際 lock duration。expand gate 通過條件不能只看 DDL 跑完、要看 <em>所有副效應收斂</em>（index status active、re-indexing 完成、replica 同步）。</p>
<p>對應 vendor pages：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</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>、<a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner</a>、<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>、<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> 的線上 DDL 段。</p>
<h2 id="backfill把歷史資料變成可驗證進度">Backfill：把歷史資料變成可驗證進度</h2>
<p>Backfill phase 的核心責任是把歷史資料補齊成可追蹤、可暫停、可重試的進度。訂單表通常會同時承擔交易查詢、客服查詢與對帳查詢；backfill 若只追求速度，容易和線上流量競爭 I/O、放大 replication lag 或改變查詢計畫。</p>
<p>Backfill job 應以 checkpoint 管理進度。每批選取固定範圍的訂單，轉換 <code>status</code> 到 <code>payment_state</code>，寫入後立刻產生該批 validation query 結果。批次大小要能依延遲、鎖等待、replication lag 與線上錯誤率調整。</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">checkpoint:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  migration_id: orders-payment-state-2026-05
</span></span><span class="line"><span class="ln">3</span><span class="cl">  last_order_id: 18420000
</span></span><span class="line"><span class="ln">4</span><span class="cl">  batch_size: 5000
</span></span><span class="line"><span class="ln">5</span><span class="cl">  started_at: 2026-05-11T02:10:00Z
</span></span><span class="line"><span class="ln">6</span><span class="cl">  completed_at: 2026-05-11T02:12:40Z
</span></span><span class="line"><span class="ln">7</span><span class="cl">  rows_scanned: 5000
</span></span><span class="line"><span class="ln">8</span><span class="cl">  rows_updated: 4921
</span></span><span class="line"><span class="ln">9</span><span class="cl">  mismatch_count: 3</span></span></code></pre></div><p>Checkpoint 的角色是把 backfill 變成可恢復流程。<code>last_order_id</code> 告訴下一批從哪裡繼續，<code>rows_updated</code> 與 <code>mismatch_count</code> 告訴 gate 這批是否可以被納入放行證據，時間欄位則讓 replication lag、slow query 與錯誤率能回到同一個觀察窗口。</p>
<p><a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">Validation query</a> 的責任是證明語意一致。最小集合包含總筆數、已補筆數、缺值筆數、新舊語意不一致樣本、每批耗時、慢查詢與 replication lag。這些查詢要保留 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a>，後續才能進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total_rows</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">missing_payment_state</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="k">AND</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">map_legacy_status_to_payment_state</span><span class="p">(</span><span class="n">status</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">mismatch_rows</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="mi">18415001</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="mi">18420000</span><span class="p">;</span></span></span></code></pre></div><p>Validation query 要和 mapping table 共用同一個語意。資料庫端缺少同一份 mapping function 時，查詢至少要把 mapping 規則展開成明確 CASE expression，並把 query version 保存在 evidence package；這樣事後才能知道 mismatch 是資料錯誤、mapping 規則改變，還是查詢本身落後。</p>
<h2 id="cutover先切讀取再收斂寫入">Cutover：先切讀取，再收斂寫入</h2>
<p>Cutover phase 的核心責任是把服務判讀權交給新欄位，同時保留可回退窗口。對訂單付款狀態來說，切換順序通常先從低風險讀取路徑開始，例如客服後台與內部對帳，再進入 checkout 查詢與使用者可見狀態；每一批切換都要有自己的 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>。</p>
<p>讀取 cutover 的 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 要比寫入 cutover 更早觸發。新欄位讀取後出現 mismatch、客服查詢結果漂移、對帳 job 補償量異常時，先回到 <a href="/blog/backend/knowledge-cards/fallback-read/" data-link-title="Fallback Read" data-link-desc="說明讀取路徑切換失敗時如何暫時回到舊資料語意或舊讀取來源">fallback read</a>，讓錯誤限制在判讀層，再重新驗證寫入收斂條件。</p>
<p>寫入 cutover 要確認所有更新來源都已對齊。付款回呼、手動修復、退款、訂單取消與 reconciliation job 都可能更新付款狀態；只切主 checkout 寫入路徑會留下長尾漂移。完成 cutover 前，要用 audit query 確認仍在寫舊欄位的程式路徑已經歸零或被納入例外清單。</p>
<h3 id="shadow-read-patterncutover-前的讀取驗證">Shadow read pattern：cutover 前的讀取驗證</h3>
<p>Shadow read 的責任是讓新讀取路徑在 <em>真實流量</em> 下被驗證、但 <em>不影響使用者結果</em>。這跟 dual-write 是對偶機制：dual-write 證寫入收斂、<a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> 證讀取分歧。</p>
<p>實作模式：</p>
<ol>
<li>每一筆讀取請求、同時用 <em>舊邏輯</em> 跟 <em>新邏輯</em> 查一次。</li>
<li>回給用戶的仍是舊邏輯結果（用戶體驗不變）。</li>
<li>在背景把兩個結果差異寫進 divergence log。</li>
<li>收集足夠樣本後、再決定切換 cutover。</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">readPaymentStateWithShadow(order):
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  legacy = mapLegacyStatusToPaymentState(order.status)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  new_result = order.payment_state ?? legacy
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  if legacy != new_result:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    asyncLogDivergence({
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      order_id: order.id,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      legacy: legacy,
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      new: new_result,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      sample_at: now(),
</span></span><span class="line"><span class="ln">10</span><span class="cl">      caller: requestContext.caller,
</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">  return legacy  // 用戶仍拿舊邏輯結果</span></span></code></pre></div><p>Shadow read 的判讀重點：</p>
<ul>
<li><strong>抽樣率</strong>：1% / 10% / 100% — 高流量場景全量 shadow 會雙倍 DB 讀取、要先評估容量。Cosmos DB / DynamoDB 的 RU 成本要乘 2。</li>
<li><strong>分歧分類</strong>：跟 dual-write 一樣、divergence 要分類（mapping bug / race condition / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>）、不分類無法定位修法。</li>
<li><strong>覆蓋條件</strong>：要驗證所有 caller path（checkout / support / reconciliation / external API）都跑過 shadow、否則 cutover 後可能踩到沒測試過的 path。</li>
<li><strong>退場條件</strong>：shadow read 不該長期跑、會增加負載。設明確 sunset deadline、cutover 完成後一週內移除。</li>
</ul>
<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%">9.C20 Zomato TiDB → DynamoDB migration</a> — migration 期間用 shadow read 持續驗證 mapping 規則、抓到 mapping drift。</p>
<p>Dual-write 跟 shadow read 的選擇不是互斥、是依風險組合：</p>
<table>
  <thead>
      <tr>
          <th>風險場景</th>
          <th>建議組合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新邏輯只影響讀取（cache、index）</td>
          <td>shadow read 即可、不需要 dual-write</td>
      </tr>
      <tr>
          <td>新欄位是 source of truth</td>
          <td>dual-write 必要、cutover 前加 shadow read 驗證</td>
      </tr>
      <tr>
          <td>跨 service 共用欄位</td>
          <td>dual-write + shadow read + cross-service contract test</td>
      </tr>
      <tr>
          <td>跨 region migration</td>
          <td>dual-write + shadow read + 跨 region replication evidence</td>
      </tr>
  </tbody>
</table>
<h2 id="multi-region-與跨服務協調">Multi-region 與跨服務協調</h2>
<p>Migration 跨越 region 或多個 service 時、rollout 順序錯誤是最常見的失敗模式。Service A 切到新欄位、service B 還在讀舊欄位、結果整條業務流量看到不一致。</p>
<h3 id="multi-region-rollout-順序">Multi-region rollout 順序</h3>
<p>跨 region 的 schema migration 要從 <em>最後寫入點</em> 開始 expand、從 <em>最後讀取點</em> 開始 cutover。先 expand 寫端、再 expand 讀端；先 cutover 讀端、再 cutover 寫端。順序反了會在過渡期讀到沒被寫的新欄位、或寫了沒被讀的新欄位。</p>
<p>實務步驟：</p>
<ol>
<li><strong>Schema expand</strong>：所有 region 同步加新欄位（先寫端再讀端、不能跳）。確認跨 region replication lag 在新欄位上收斂、再進下一步。</li>
<li><strong>Backfill</strong>：可以平行跑、但每 region 各自 checkpoint、不共用。某 region backfill stuck 不應該卡住其他 region。</li>
<li><strong>Cutover read</strong>：region by region 切讀、用 canary region 先試 24-48 小時、再擴散。</li>
<li><strong>Cutover write</strong>：所有 region 都切完讀、再統一切寫。寫端切換比讀端更敏感、跨 region 寫差異會放大成跨 region inconsistency。</li>
</ol>
<p>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的跨 region consistency 段。</p>
<h3 id="cross-service-migration-協調">Cross-service migration 協調</h3>
<p>當 schema 變更影響多個 service 時、API contract 是 <em>鬆耦合</em> 介面、不該讓所有 service 同步切換。</p>
<p>協調機制：</p>
<ul>
<li><strong>新欄位先在 API 是 optional</strong>：API contract 加新欄位、預設 nullable / optional。下游 service 可選擇何時讀。</li>
<li><strong>舊欄位保留至少一個版本週期</strong>：API 不能跟 DB schema 同步 contract、否則下游沒時間切。實務上保留 1-2 季、給下游充足 cutover 窗口。</li>
<li><strong>owner-by-owner cutover roster</strong>：明確列出每個下游 service 的 owner、預計 cutover 時間、目前狀態。常用工具是共享 dashboard、不是散落的 ticket。</li>
<li><strong>Contract test</strong>：每個下游 service 對新欄位都要有 contract test、在 CI gate 跑過。避免上游 cutover 後下游才發現沒讀對。</li>
</ul>
<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%">9.C20 Zomato TiDB → DynamoDB</a> — 跨多個 service 的 access pattern 變更、必須每個 service 各自驗證、不能假設「DB 切了就好」。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>資料庫 migration 的 evidence package 負責證明資料演進是否可判讀。這份 package 要把 validation query、時間窗、資料限制與 owner 包成後續放行與事故判斷可引用的證據，dashboard 只作為摘要入口。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>訂單欄位演進中的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>validation query、DB metric、migration job log、audit log</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>expand、backfill、cutover 各階段的查詢窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>row count、mismatch sample、replication lag、slow query</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>database owner、checkout owner、reconciliation owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>query 延遲、replica freshness、sample completeness</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>未覆蓋的手動修復路徑、低流量 tenant、延遲回呼</td>
      </tr>
  </tbody>
</table>
<p>Source 欄位要保留資料來源的能力邊界。Validation query 能證明欄位語意一致，DB metric 能看出 latency 與 lag，job log 能追進度，audit log 能判斷是否有高權限修復行為。把這些來源混在一起會讓下游誤判證據的用途。</p>
<p><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a> 欄位要直接寫出限制。若查詢只跑 primary、replica lag 還在回復、某些 tenant 因資料遮罩未被抽樣，這些限制要跟 evidence 一起交給 release gate，讓 gate 能以證據完整度決定是否放行。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">evidence_package</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">orders-payment-state-cutover-batch-37</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span>- <span class="nt">validation_query</span><span class="p">:</span><span class="w"> </span><span class="l">q_orders_payment_state_batch_37</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span>- <span class="nt">db_metric</span><span class="p">:</span><span class="w"> </span><span class="l">replication_lag_orders_primary</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">job_log</span><span class="p">:</span><span class="w"> </span><span class="l">backfill_orders_payment_state_2026_05</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">time_range</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T02:10:00Z</span><span class="l">/2026-05-11T02:20:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">data-platform-oncall</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-oncall</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">reconciliation</span><span class="p">:</span><span class="w"> </span><span class="l">finance-ops-owner</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">data_quality</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">replica_freshness</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;primary only; replica lag still recovering&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">sample_completeness</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;tenant tier enterprise covered; sandbox tenants excluded&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">confidence</span><span class="p">:</span><span class="w"> </span><span class="l">suspected</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">known_gap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;manual refund repair path not yet sampled&#34;</span></span></span></code></pre></div><p>這份 package 故意把 <code>confidence</code> 標成 <code>suspected</code>。原因是 evidence 已能支持 backfill 繼續前進，但還不足以支持使用者可見讀取 cutover；這種中間狀態要被明確寫出，gate 才能做分階段決策。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Schema migration 的 release gate 負責判斷下一階段是否可以放行。它接收 evidence package，但決策語言要回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>：<code>Gate decision</code>、<code>Checks</code>、<code>Stop condition</code>、<code>Rollback window</code>、<code>Owner</code>。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>這條路徑的最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批 backfill、暫停 cutover、回到 fallback read 或 fail-forward</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>compatibility result、mismatch rate、replication lag、slow query</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>mismatch 超門檻、交易錯誤率上升、lag 超窗口、客服查詢漂移</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>讀取 fallback 可用時間、舊欄位可支撐多久、contract 前最後回退點</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>migration owner、service owner、on-call owner</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a> 要用服務語言書寫。<code>migration pass</code> 這種結論對下游不夠具體；<code>放行 10% 訂單 backfill</code>、<code>暫停使用者可見讀取 cutover</code>、<code>維持 fallback read 24 小時</code> 才能讓執行團隊知道下一步。</p>
<p><a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback window</a> 是資料庫 migration 的關鍵欄位。Expand 與 backfill 階段通常能回到舊讀取；cutover 後仍可 fallback；contract 後舊語意被移除，回退會變成資料修復或 <a href="/blog/backend/knowledge-cards/fail-forward/" data-link-title="Fail-forward" data-link-desc="說明無法回到舊狀態時如何用受控前進完成修復">fail-forward</a>。gate 要在每階段說清楚目前還剩哪種退路。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">release_gate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">gate_decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;allow next 10% backfill; block customer-visible read cutover&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">checks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">mismatch_rate</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.04%, below 0.1% batch threshold&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">replication_lag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;p95 12s, below 30s stop condition&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">slow_query</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;no new support-admin slow query above 500ms&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">stop_condition</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;mismatch_rate &gt;= 0.1% for two consecutive batches&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;replication_lag &gt;= 30s for 10 minutes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;support-admin query drift confirmed by reconciliation owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">rollback_window</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;fallback read available until contract phase starts&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-oncall</span></span></span></code></pre></div><p>這份 gate record 把「繼續 backfill」和「暫緩讀取 cutover」拆成兩個決策。資料庫 migration 常見的判讀問題是 evidence 只支撐下一批資料修補，還支撐不了使用者可見行為切換。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>Migration 進入 production 後，pause、rollback 與 fail-forward 都是事故決策。這些決策要同步寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>，讓事中交班與事後復盤能回放當時的證據與限制。</p>
<p>常見決策包括暫停 backfill、降低 batch size、回到舊讀取、停止 contract、手動修補 mismatch、選擇 fail-forward。每筆都要保留 <code>Timestamp</code>、<code>Decision</code>、<code>Context</code>、<code>Evidence</code>、<code>Owner</code>、<code>Expected effect</code> 與 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<p>例如 cutover 後發現客服查詢 mismatch 升高，decision log 可以寫成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T03:05:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rollback support-admin read path to legacy status fallback&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;support-admin mismatch increased after internal read cutover&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">q_orders_payment_state_support_mismatch</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">window</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T02:35:00Z</span><span class="l">/2026-05-11T03:05:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="nt">interpretation</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;suspected callback mapping drift&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;support ticket misclassification returns to baseline&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;mismatch remains above threshold after 15 minutes&#34;</span></span></span></code></pre></div><p>這種記錄能避免事後只剩「當時有回退」的模糊敘事。後續 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back 實作示範</a> 可承接同一組決策紀錄，把缺少 validation、owner 或 runbook 的地方回寫成改善項。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>判讀訊號的責任是讓讀者知道何時該繼續、何時該停、何時該改路線。Migration 訊號要同時看資料正確性、線上健康度與回退窗口。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>mismatch rate 持續低於門檻</td>
          <td>新舊欄位語意大致一致</td>
          <td>放行下一批 backfill 或低風險讀取 cutover</td>
      </tr>
      <tr>
          <td>mismatch 樣本集中在特定 callback</td>
          <td>轉換函式或特定付款路徑語意不一致</td>
          <td>暫停 cutover，修 mapping 後重跑該批</td>
      </tr>
      <tr>
          <td>dual-write divergence 分布偏向 mapping</td>
          <td>mapping function 在某 path 沒被使用</td>
          <td>找出該 path、強制走共用 mapping function</td>
      </tr>
      <tr>
          <td>dual-write divergence 偏向 race</td>
          <td>部分寫入失敗、寫順序問題</td>
          <td>切到 outbox-based dual-write、別直連</td>
      </tr>
      <tr>
          <td>shadow read 抽樣 RU 飆升</td>
          <td>shadow 讀取沒設抽樣率、雙倍負載</td>
          <td>降低抽樣率、或改成 off-peak shadow</td>
      </tr>
      <tr>
          <td>replication lag 在 backfill 升高</td>
          <td>migration 與線上查詢競爭資源</td>
          <td>降低 batch size，避開 peak，延長觀察窗口</td>
      </tr>
      <tr>
          <td>slow query 出現在客服查詢</td>
          <td>新欄位索引或查詢模型未對齊</td>
          <td>回到 fallback read，補 index 或改查詢條件</td>
      </tr>
      <tr>
          <td>DynamoDB GSI 仍在 building</td>
          <td>cutover 前依賴未 ACTIVE 的 GSI</td>
          <td>等 GSI ACTIVE 再切讀、別假設立即可用</td>
      </tr>
      <tr>
          <td>跨 region replica lag 在新欄位上漂移</td>
          <td>expand 階段沒等所有 region 收斂</td>
          <td>暫停 backfill、等 region 同步</td>
      </tr>
      <tr>
          <td>某下游 service 沒 cutover</td>
          <td>cross-service 協調沒做 contract test</td>
          <td>補 contract test、推遲 contract 階段</td>
      </tr>
      <tr>
          <td>contract 前仍有舊欄位寫入</td>
          <td>更新來源尚未完全收斂</td>
          <td>延後 contract，盤點寫入來源與 owner</td>
      </tr>
  </tbody>
</table>
<p>這些訊號要放回服務路徑判讀。Mismatch 要看集中在哪個業務入口；若 mismatch 只出現在延遲付款 callback，它代表外部 provider 回呼語意未對齊。Replication lag 要看是否和 backfill 批次對位；若它只在 backfill 批次出現，gate 應調整 migration 節奏，再判斷 schema 設計是否需要修正。</p>
<p>Dual-write 跟 shadow read 的 divergence 要分開看 — 兩者偵測不同層的問題。Dual-write divergence 偏向 mapping bug 或 race condition；shadow read divergence 偏向讀取邏輯漂移或 stale read。混在同一個 dashboard 會讓 reviewer 看不出問題真正在哪一層。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 schema migration 寫成 DDL 任務，會讓風險集中在切換當下。穩定做法是先建立相容窗口，再用 evidence 證明資料語意已經跟上，最後才收斂舊路徑。</p>
<p>把 validation query 當成事後對帳，也會削弱 rollout 控制。Validation query 適合在 expand、backfill、cutover 每一階段都產生證據，讓 release gate 能在風險擴大前停下來。</p>
<p>把 rollback 寫成單一動作容易誤導團隊。資料庫 migration 的 rollback 會隨階段改變：expand 可回退 schema 使用，backfill 可暫停與重跑，cutover 可回到 fallback read，contract 後多半只能做資料修復或 fail-forward。</p>
<p>把 dual-write 跟 shadow read 當成同一個工具。兩者偵測不同層、結合使用可以互補、互相替代會留下盲點。Dual-write 不跑 shadow read、cutover 後可能踩到沒驗過的讀取 path；shadow read 不跑 dual-write、新欄位可能在某些寫路徑根本沒被寫進去。</p>
<p>把線上 DDL 當「一個 SQL 跑完就好」。各 vendor 的 DDL 語意差異大、PostgreSQL 的 <code>ADD COLUMN NOT NULL DEFAULT</code> 在 PG 10 重寫整張表、PG 11+ 是 metadata-only；MySQL 不指定 <code>ALGORITHM=INSTANT</code> 可能掉回 COPY。Expand evidence 要包含 <em>實際 lock duration</em>、不是只看 DDL 是否回傳成功。</p>
<p>只在主寫入路徑切 cutover、忘記補償流程跟 reconciliation job 也會寫舊欄位。這些長尾寫入會在 contract 階段才暴露、那時候已經沒有 fallback 可走。Cutover 前要 audit 所有寫舊欄位的程式路徑、不只看主流程。</p>
<h2 id="案例回寫">案例回寫</h2>
<p><a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a> 可以回寫這篇的決策層。當服務營運後需要拆欄位、拆庫、分片或升級儲存引擎，先用 0.C4 判斷「為什麼要換」，再用本篇判斷「進入 production 後如何證明每一步成立」。</p>
<p><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 可以回寫這篇的事故層。該事件顯示資料一致性優先時，團隊需要可回放的 fail-forward / fail-back 判準；本篇則把這個需求落到 migration rollout 的 evidence、gate 與 decision log。</p>
<p>這兩個案例共同支撐的是「資料狀態演進需要證據閉環」。0.C4 提供轉換動機與選型壓力，GitHub 事故提供資料一致性與恢復決策的代價；兩者都不直接替代 validation query、release gate 與 decision log 的實作細節。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位責任、命名與查詢模型回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a>。</li>
<li>與 1.3 的交接：付款回呼、手動修復與對帳更新的交易邊界回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.6 的交接：expand、backfill、cutover 與 contract 的執行流程回到 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>。</li>
<li>與 4.20 / 4.22 的交接：validation query、row count、lag 與 slow query 進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">Checkout API Evidence Package</a>。</li>
<li>與 6.11 / 6.8 / 6.25 的交接：migration 可逆性與放行條件進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a> 與 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">Provider Dependency Release Gate</a>。</li>
<li>與 8.19 / 8.23 的交接：pause、rollback、fail-forward 與 write-back 進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a> 與 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">Control Plane Decision Log and Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把資料庫 migration 的 evidence 交給 release gate，接著讀 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a>，並把 provider 依賴示範中的 gate 欄位改寫成 migration gate 欄位。要看下一條分類服務路徑，接著進 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 Cache / Redis 模組</a> 的 <code>Cache migration and stampede rollback</code> 服務路徑。</p>
<p>跨 vendor schema migration 深入：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/" data-link-title="Spanner Schema Migration Without Downtime &#43; Interleaved Tables" data-link-desc="Spanner DDL 是 long-running operation、用 TrueTime 給每次 schema change 分配 version timestamp、所有 read / write 對應自己 transaction timestamp 看到對應 schema。Interleaved table 是 storage-level parent-child 物理交錯、不是 logical FK。本文走 schema change lifecycle、interleaved layout 機制、backfill capacity 影響、5 production 踩雷、跟 PostgreSQL online schema change 對照">Spanner interleaved table 的 schema migration</a> — 全球分散式表結構變更的 evidence shape</li>
<li><a href="/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/" data-link-title="從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook" data-link-desc="PostgreSQL / MySQL → Aurora 的 Type C operational redesign hybrid playbook、6 規格面（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、Standard Chartered 合規 lead time 模型、Netflix 非 all-purpose store 邊界">Aurora 從自管 PostgreSQL / MySQL 遷入</a> — schema 比對與 dual-write 證據鏈</li>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a> — multi-API document 在 rollout 階段的相容性 evidence</li>
</ul>
]]></content:encoded></item><item><title>2.7 Cache Copy Boundary 與 Freshness</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/</guid><description>&lt;p>Cache copy boundary 與 freshness 的核心責任是定義快取副本能承擔什麼判斷，以及它可以不新鮮多久。進入 Redis、Valkey、Memcached 或其他快取服務前，讀者需要先理解快取同時是加速層，也是 source of truth 與讀取壓力之間的風險邊界。&lt;/p>
&lt;h2 id="cache-copy-boundary">Cache Copy Boundary&lt;/h2>
&lt;p>Cache copy boundary 的責任是把可重建副本和正式狀態分開。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 承擔最終判斷，cache 承擔低延遲讀取與來源保護。&lt;/p>
&lt;p>商品描述、公開設定、推薦摘要通常是可重建副本。價格、庫存、權限、配額與付款狀態雖然可以被快取，但它們的錯誤會直接影響交易或安全判斷，因此 freshness 與 invalidation 要更嚴格。&lt;/p>
&lt;h2 id="freshness">Freshness&lt;/h2>
&lt;p>Freshness 的責任是定義資料可接受的 stale window。不同欄位需要不同 window，TTL 策略要跟欄位風險分層對齊。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料類型&lt;/th>
 &lt;th>可接受 stale&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>秒到分鐘級&lt;/td>
 &lt;td>主要影響排序與轉換率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>價格&lt;/td>
 &lt;td>秒級或事件失效&lt;/td>
 &lt;td>影響交易正確性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>庫存&lt;/td>
 &lt;td>秒級或即時查詢&lt;/td>
 &lt;td>影響超賣與履約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>權限&lt;/td>
 &lt;td>極短或強制失效&lt;/td>
 &lt;td>影響資料外洩與越權&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配額&lt;/td>
 &lt;td>極短或原子更新&lt;/td>
 &lt;td>影響濫用與計費&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data&lt;/a> 本身是快取常態成本，定義 stale 代價能讓團隊選擇對應保護。可接受 stale 的資料可用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 管理，高代價 stale 的資料需要事件失效、版本化 key 或回源確認。&lt;/p>
&lt;p>商品描述與推薦清單偏向體驗資料，短暫 stale 的主要代價是使用者看到較舊內容。價格與庫存偏向交易資料，stale 會改變付款、履約或客服判斷。權限與配額偏向控制資料，stale 會放大越權、濫用或計費風險。這些差異決定快取策略要分欄位設計，並以服務層邊界統一交接。&lt;/p>
&lt;h2 id="invalidation">Invalidation&lt;/h2>
&lt;p>Invalidation 的責任是讓快取副本在正式狀態變更後收斂。常見模型包含刪除 key、更新 key、版本化 key、事件驅動失效與 TTL 保底。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a> 要和資料責任對齊。價格類資料適合事件驅動失效加短 TTL；商品描述可以長 TTL 加背景刷新；權限類資料要能在撤權後快速失效。&lt;/p>
&lt;h3 id="cache-不一致的主要來源點">Cache 不一致的主要來源點&lt;/h3>
&lt;p>規模化 cache 的不一致主要由 &lt;em>topology 變動事件&lt;/em> 觸發、不是 TTL 設定。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency Upgrade&lt;/a> — 案例指出 promotion、shard move、故障恢復是三類主要事件來源、傳統 invalidation 在大規模系統難以維持穩定。&lt;/p>
&lt;p>&lt;strong>三類事件的典型機制&lt;/strong>（具體實作依 cluster 設計而異）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Promotion / failover&lt;/strong>：primary 切到 replica 過程中、寫入順序可能跨節點不一致、replica 變 primary 後可能讀到舊資料&lt;/li>
&lt;li>&lt;strong>Shard move / rebalance&lt;/strong>：cluster topology 變更時、部分 key 在搬遷窗口內可能讀到舊 shard 的副本&lt;/li>
&lt;li>&lt;strong>故障恢復&lt;/strong>：節點重啟後、cache 從 backing store 重建、跟 application 寫入的新值可能交錯&lt;/li>
&lt;/ul>
&lt;p>在這些事件中、cache 拓樸隨著事件改變、需要追蹤 mutation 收斂、不只清 key。Meta 的解法是把 &lt;em>mutation tracing&lt;/em> 制度化、追蹤每次資料變動是否在所有 cache 副本都收斂。&lt;/p>
&lt;h3 id="mutation-tracing-跟一致性指標">Mutation tracing 跟一致性指標&lt;/h3>
&lt;p>Mutation tracing 是 &lt;em>資料變動到所有 cache 副本收斂的時間軸&lt;/em> 追蹤、跟一般 cache hit rate 屬不同維度。常見的工程實踐指標（屬 case-derived 推論、非 Meta case 直接揭露具體 SLO）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Inconsistency window&lt;/strong>：從 source-of-truth 寫入到所有 cache 副本反映的耗時（平均 / p99）&lt;/li>
&lt;li>&lt;strong>Inconsistency rate&lt;/strong>：query 取到 stale 副本的比例&lt;/li>
&lt;li>&lt;strong>Inconsistency duration distribution&lt;/strong>：stale 持續時間的分布（看長尾才能識別事故風險、平均值會掩蓋）&lt;/li>
&lt;/ul>
&lt;p>這些指標要接到告警跟回退條件、用法接近一般 SLO（例：inconsistency window p99 超過 &lt;em>服務可接受 stale window&lt;/em> 觸發保護動作）。具體門檻依業務型態定 — 付款 / 庫存 / 權限類資料的容忍可能在秒級、商品描述可能在分鐘級。&lt;/p></description><content:encoded><![CDATA[<p>Cache copy boundary 與 freshness 的核心責任是定義快取副本能承擔什麼判斷，以及它可以不新鮮多久。進入 Redis、Valkey、Memcached 或其他快取服務前，讀者需要先理解快取同時是加速層，也是 source of truth 與讀取壓力之間的風險邊界。</p>
<h2 id="cache-copy-boundary">Cache Copy Boundary</h2>
<p>Cache copy boundary 的責任是把可重建副本和正式狀態分開。<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 承擔最終判斷，cache 承擔低延遲讀取與來源保護。</p>
<p>商品描述、公開設定、推薦摘要通常是可重建副本。價格、庫存、權限、配額與付款狀態雖然可以被快取，但它們的錯誤會直接影響交易或安全判斷，因此 freshness 與 invalidation 要更嚴格。</p>
<h2 id="freshness">Freshness</h2>
<p>Freshness 的責任是定義資料可接受的 stale window。不同欄位需要不同 window，TTL 策略要跟欄位風險分層對齊。</p>
<table>
  <thead>
      <tr>
          <th>資料類型</th>
          <th>可接受 stale</th>
          <th>判斷重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商品描述</td>
          <td>秒到分鐘級</td>
          <td>主要影響體驗</td>
      </tr>
      <tr>
          <td>推薦清單</td>
          <td>秒到分鐘級</td>
          <td>主要影響排序與轉換率</td>
      </tr>
      <tr>
          <td>價格</td>
          <td>秒級或事件失效</td>
          <td>影響交易正確性</td>
      </tr>
      <tr>
          <td>庫存</td>
          <td>秒級或即時查詢</td>
          <td>影響超賣與履約</td>
      </tr>
      <tr>
          <td>權限</td>
          <td>極短或強制失效</td>
          <td>影響資料外洩與越權</td>
      </tr>
      <tr>
          <td>配額</td>
          <td>極短或原子更新</td>
          <td>影響濫用與計費</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 本身是快取常態成本，定義 stale 代價能讓團隊選擇對應保護。可接受 stale 的資料可用 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 管理，高代價 stale 的資料需要事件失效、版本化 key 或回源確認。</p>
<p>商品描述與推薦清單偏向體驗資料，短暫 stale 的主要代價是使用者看到較舊內容。價格與庫存偏向交易資料，stale 會改變付款、履約或客服判斷。權限與配額偏向控制資料，stale 會放大越權、濫用或計費風險。這些差異決定快取策略要分欄位設計，並以服務層邊界統一交接。</p>
<h2 id="invalidation">Invalidation</h2>
<p>Invalidation 的責任是讓快取副本在正式狀態變更後收斂。常見模型包含刪除 key、更新 key、版本化 key、事件驅動失效與 TTL 保底。</p>
<p><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a> 要和資料責任對齊。價格類資料適合事件驅動失效加短 TTL；商品描述可以長 TTL 加背景刷新；權限類資料要能在撤權後快速失效。</p>
<h3 id="cache-不一致的主要來源點">Cache 不一致的主要來源點</h3>
<p>規模化 cache 的不一致主要由 <em>topology 變動事件</em> 觸發、不是 TTL 設定。對應 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency Upgrade</a> — 案例指出 promotion、shard move、故障恢復是三類主要事件來源、傳統 invalidation 在大規模系統難以維持穩定。</p>
<p><strong>三類事件的典型機制</strong>（具體實作依 cluster 設計而異）：</p>
<ul>
<li><strong>Promotion / failover</strong>：primary 切到 replica 過程中、寫入順序可能跨節點不一致、replica 變 primary 後可能讀到舊資料</li>
<li><strong>Shard move / rebalance</strong>：cluster topology 變更時、部分 key 在搬遷窗口內可能讀到舊 shard 的副本</li>
<li><strong>故障恢復</strong>：節點重啟後、cache 從 backing store 重建、跟 application 寫入的新值可能交錯</li>
</ul>
<p>在這些事件中、cache 拓樸隨著事件改變、需要追蹤 mutation 收斂、不只清 key。Meta 的解法是把 <em>mutation tracing</em> 制度化、追蹤每次資料變動是否在所有 cache 副本都收斂。</p>
<h3 id="mutation-tracing-跟一致性指標">Mutation tracing 跟一致性指標</h3>
<p>Mutation tracing 是 <em>資料變動到所有 cache 副本收斂的時間軸</em> 追蹤、跟一般 cache hit rate 屬不同維度。常見的工程實踐指標（屬 case-derived 推論、非 Meta case 直接揭露具體 SLO）：</p>
<ul>
<li><strong>Inconsistency window</strong>：從 source-of-truth 寫入到所有 cache 副本反映的耗時（平均 / p99）</li>
<li><strong>Inconsistency rate</strong>：query 取到 stale 副本的比例</li>
<li><strong>Inconsistency duration distribution</strong>：stale 持續時間的分布（看長尾才能識別事故風險、平均值會掩蓋）</li>
</ul>
<p>這些指標要接到告警跟回退條件、用法接近一般 SLO（例：inconsistency window p99 超過 <em>服務可接受 stale window</em> 觸發保護動作）。具體門檻依業務型態定 — 付款 / 庫存 / 權限類資料的容忍可能在秒級、商品描述可能在分鐘級。</p>
<p>當 inconsistency window 突然拉長、可能是 invalidation pipeline 卡住或 cache topology 變更中、應觸發保護動作（停止寫入、降級到回源、或回退近期變更）。</p>
<p>對應 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> — cache 一致性指標屬於 <em>資料品質指標</em>、要進 evidence chain、跟效能指標分開追蹤。</p>
<h2 id="origin-protection">Origin Protection</h2>
<p>Origin protection 的責任是避免 cache miss 把壓力集中打回資料庫或下游服務。快取越接近高流量路徑，越要把 miss 視為需要治理的事件。</p>
<p>保護策略包含：</p>
<ol>
<li><a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a> 先建立熱門資料覆蓋。</li>
<li><a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight</a> 或 request coalescing 合併同 key 回源。</li>
<li>對回源設 rate limit、timeout 與 fallback。</li>
<li>對短暫找不到的結果使用短 TTL negative cache。</li>
</ol>
<p>這些策略的共同目標是優先保護正式狀態來源，再提升命中率與延遲表現。</p>
<h2 id="跨區一致性窗口">跨區一致性窗口</h2>
<p>當 cache 跨多 region 部署、一致性問題從「副本 vs source-of-truth」變成「副本 vs 副本」。同一個用戶在不同 region 看到 cache 內容差異、可能影響業務邏輯（庫存超賣、配額超用、權限延遲）。規模化的 cache 把跨區一致性窗口跟區域容錯設計納入同一模型、不是分開治理（對應 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a> 跟 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a>）。</p>
<p><strong>Strong sync</strong> 採每次寫入同步到所有 region、延遲高、可靠性高。適合付款 / 庫存 / 權限類資料 — 庫存超賣的代價是業務直接損失（賣出實際沒有的商品）、權限不一致的代價是越權或拒服務、付款延遲一致的代價是重複扣款。這些代價高到值得付跨 region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> 的 latency 成本。失敗代價路徑：跨 region quorum 不可達時 → 寫入失敗 → 用戶看到操作失敗、業務不繼續寫錯資料。</p>
<p><strong>Async with bounded staleness</strong> 寫入主 region、其他 region 在 N 秒內收斂、多數場景夠用、要明確 stale window。適合 B2B SaaS、社群動態、推薦資料 — 用戶可以接受短暫看到舊版內容、但長時間 stale 會影響體驗。失敗代價路徑：跨 region 同步 lag 增長 → 用戶看到不同版本內容 → 累積到 stale window 上限時觸發 alert 跟保護。</p>
<p><strong>Per-region cache</strong> 每 region 各自獨立、不跨區同步、靠 backing store 收斂。適合本地用戶為主的資料（區域電商、本地內容平台） — 同一用戶極少跨 region、跨區一致性需求低、為了少數情境付跨區同步成本不划算。失敗代價路徑：跨 region 操作的用戶看到 region 之間不一致 → 業務側手動補償或要求用戶重試。</p>
<p>判讀重點：選哪種跨區一致性跟「同一用戶會不會跨 region 操作」直接相關。全球漫遊用戶（旅遊、跨國商務）要更強的同步；本地用戶為主的服務可以 per-region。</p>
<h3 id="跨-cloud-部署的資料引力">跨 cloud 部署的資料引力</h3>
<p>當 application 跟 cache 不在同一 cloud / region、每次 cache lookup 吃跨網路 latency（視 region pair 而定、9.C35 觀察值為 5-30ms）。對「每次互動查多個 cache」的服務、5ms × 10 lookup = 50ms 額外延遲、用戶感受明顯。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB cross-cloud</a> — Snap 把 KeyDB cache 放在 GCP 上、減少跨 cloud cache lookup latency。資料引力原則：data 在哪、cache 跟著去、跨 cloud 走 batch sync 降頻、應用與 cache 共置主資料 cloud。</p>
<p><strong>Multi-cloud cache 部署原則</strong>：</p>
<ul>
<li><strong>同 cloud 內</strong>：cache + application + DB 都在同一 cloud、cache lookup 在 ms 級內</li>
<li><strong>跨 cloud 採 batch sync 降頻</strong>：低頻、高延遲容忍的資料同步（每小時 / 每天）、應用本地讀 cache</li>
<li><strong>應用與 cache 共置主資料 cloud</strong>：高頻、低延遲容忍的路徑跟主資料同 cloud、避免跨 cloud RTT</li>
</ul>
<p>判讀重點：multi-cloud 架構的 cache 設計要先確定 data 主要在哪個 cloud、其他 cloud 的 application 要靠 batch sync 拿資料。Snap 從 zero-day 就在 GCP、近年走 multi-cloud 時、把 KeyDB 留在 GCP（data 一直在的地方）、避免反向部署引發的隱性 latency。違反這原則會踩到用戶層難以 debug 的延遲瓶頸。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>快取服務選型前要先回答四個問題：</p>
<ol>
<li>快取值是可重建副本，還是被拿來做正式判斷。</li>
<li>每種值的 freshness window 是多久。</li>
<li>miss 時來源系統能承受多少回源 QPS。</li>
<li>錯誤資料要如何失效、降級與回寫事故證據。</li>
</ol>
<p>這些問題先回答後，才進入 Redis data structure、Memcached 設計、Valkey 相容性或 managed cache 的討論。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體快取服務文章要承接本篇的 copy boundary 與 freshness。Redis、Valkey、Memcached、DragonflyDB 或 managed cache 的比較，應先問它們如何支援 key 失效、TTL、eviction、warmup、回源保護與觀測訊號，再進入 command 或部署細節。</p>
<p>若服務需要嚴格 freshness，後續文章要比較事件失效、版本化 key、原子更新與 fallback 能力。若服務主要面對高讀取壓力，後續文章要比較連線模型、hot key 保護、memory policy 與 cluster/sharding 行為。若服務需要事故回退，後續文章要比較 key migration、dual read、metrics 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理讀寫流程，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要把 freshness 放進 rollout 與停損，接著讀 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a>。</p>
]]></content:encoded></item><item><title>3.7 Event Contract 與 Replay Boundary</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</guid><description>&lt;p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。&lt;/p>
&lt;h2 id="event-contract">Event Contract&lt;/h2>
&lt;p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。&lt;/p>
&lt;p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。&lt;/p>
&lt;p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 或 partition 設計才有可依附的語意。&lt;/p>
&lt;h2 id="schema-compatibility">Schema Compatibility&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility&lt;/a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。&lt;/p>
&lt;p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。&lt;/p>
&lt;h2 id="replay-boundary">Replay Boundary&lt;/h2>
&lt;p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。&lt;/p>
&lt;p>replay window 要和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 對齊，讓事後能回放當時重播的是哪一批事件。&lt;/p>
&lt;h2 id="compensation">Compensation&lt;/h2>
&lt;p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。&lt;/p>
&lt;p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log&lt;/a>。&lt;/p>
&lt;h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映&lt;/h2>
&lt;p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration&lt;/a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。&lt;/p>
&lt;p>&lt;strong>典型概念對映差異&lt;/strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：&lt;/p></description><content:encoded><![CDATA[<p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。</p>
<h2 id="event-contract">Event Contract</h2>
<p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。</p>
<p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。</p>
<p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 或 partition 設計才有可依附的語意。</p>
<h2 id="schema-compatibility">Schema Compatibility</h2>
<p><a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility</a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。</p>
<p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。</p>
<h2 id="replay-boundary">Replay Boundary</h2>
<p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。</p>
<p>replay window 要和 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 與 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 對齊，讓事後能回放當時重播的是哪一批事件。</p>
<h2 id="compensation">Compensation</h2>
<p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。</p>
<p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映</h2>
<p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。</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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration</a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。</p>
<p><strong>典型概念對映差異</strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：</p>
<ul>
<li><strong>Partition (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka partition 是物理分片 + 順序邊界；Pub/Sub subscription 是邏輯 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、無物理分片概念。靠 Kafka partition 保證 per-key 順序的 consumer、遷到 Pub/Sub 改用 ordering key</li>
<li><strong>Offset (Kafka) 對映成 message attribute (Pub/Sub)</strong>：9.C9 case 原文對映方向；replay 模型差異上、Kafka offset 是位置指標、可任意回放到某個 offset；Pub/Sub 用 Snapshot + Seek API 達成類似 replay 能力、模型不同</li>
<li><strong>Consumer Group (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka consumer group 內部 rebalance 自動分 partition；Pub/Sub subscription 自動分 message、語意接近但 rebalance 細節差異會影響 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> message 處理順序</li>
</ul>
<p><strong>遷移評估要驗證的業務語意</strong>：</p>
<ul>
<li>順序保證：原系統靠 partition / consumer group 保證什麼順序、新系統能否複製</li>
<li>Replay 模型：原系統 replay 方式、新系統的 replay 工具能否達成同範圍。replay window 上限由 idempotency 保留期反推、見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Replay 跟 Idempotency 共設計</a></li>
<li>失敗模式：consumer 故障時、原系統的 rebalance / redelivery 行為、新系統會不會差異</li>
</ul>
<p>判讀重點：broker migration 屬語意對映工程、吞吐能力比較是次要驗證項。對應 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 的「Broker 遷移階段流程」</a>、實作面用 dual-write + shadow consume + cutover、驗證面靠 event contract 跟 replay 邊界做對帳。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 03 內部：replay window 跟 idempotency 共設計回到 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>；broker 遷移階段流程回到 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a></li>
<li>與 04 的交接：event contract 演進 + replay 邊界進 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>與 06 的交接：event contract 跟 replay 驗證進 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 跟 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li>與 07 的交接：event payload 的 PII / audit 邊界進 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data protection and masking</a></li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>Broker 選型前要先回答：</p>
<ol>
<li>event contract 是否能支援版本相容。</li>
<li>consumer 是否能用 dedup key 判斷重複。</li>
<li>replay window 是否能用查詢與指標證明。</li>
<li>不可逆副作用是否有補償流程。</li>
<li>event payload 是否包含 PII 或 audit-sensitive 欄位。</li>
</ol>
<p>這些問題決定後續要比較 broker retention、schema registry、DLQ、partition 與 replay 工具，並把吞吐放回服務語意下判讀。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體 broker 文章要承接本篇的 event contract 與 replay boundary。Kafka 的長期 retention、RabbitMQ 的 routing 與 DLQ、SQS 的 <a href="/blog/backend/knowledge-cards/visibility-timeout/" data-link-title="Visibility Timeout" data-link-desc="說明訊息被取走後對其他 consumer 暫時不可見的時間窗，timeout 後重新投遞">visibility timeout</a>、NATS JetStream 的 stream/consumer 模型，都要放回事件契約與重播邊界下判讀。</p>
<p>若事件需要長期 replay，後續文章要比較 retention、offset、partition 與 schema evolution。若事件主要是工作任務，後續文章要比較 visibility、ack、DLQ 與重試治理。若事件包含 PII 或高風險副作用，後續文章要比較 audit、encryption、access control 與補償流程。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 outbox 與事件發布一致性，接著讀 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 與發佈一致性</a>。要處理 consumer 端去重與重播，接著讀 <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>。</p>
]]></content:encoded></item><item><title>5.7 Traffic、Config 與 Control Plane Boundary</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</guid><description>&lt;p>Traffic、config 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。&lt;/p>
&lt;h2 id="traffic-boundary">Traffic Boundary&lt;/h2>
&lt;p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。&lt;/p>
&lt;p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a> 才能從比例設定變成可回退策略。&lt;/p>
&lt;p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 越容易被驗證。&lt;/p>
&lt;h3 id="切換單位的選擇">切換單位的選擇&lt;/h3>
&lt;p>切換單位決定故障的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與回退的精準度。常見切換單位各有不同操作特性：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>切換單位&lt;/th>
 &lt;th>blast radius&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>按流量比例&lt;/td>
 &lt;td>粗（全域）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>通用 canary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>區域 / AZ&lt;/td>
 &lt;td>限定地理範圍&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跨區部署的服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>租戶 / 組織&lt;/td>
 &lt;td>限定特定客戶&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>多租戶 SaaS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路由規則&lt;/td>
 &lt;td>限定特定路徑&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>API 版本切換、功能漸進上線&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。&lt;/p>
&lt;h2 id="config-boundary">Config Boundary&lt;/h2>
&lt;p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout&lt;/a> 和應用版本不一定同步，因此要保留相容窗口。&lt;/p>
&lt;p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。&lt;/p>
&lt;h3 id="config-變更的風險分級">Config 變更的風險分級&lt;/h3>
&lt;p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>風險等級&lt;/th>
 &lt;th>設定類型&lt;/th>
 &lt;th>review 與 rollout 要求&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>payment endpoint、auth provider URL、encryption key&lt;/td>
 &lt;td>等同 code review + staged rollout + rollback 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>rate limit、timeout、feature flag、CORS 設定&lt;/td>
 &lt;td>變更 review + 觀測窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>log level、debug flag、非關鍵 UI 文案&lt;/td>
 &lt;td>變更紀錄即可&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Traffic、config 與 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。</p>
<h2 id="traffic-boundary">Traffic Boundary</h2>
<p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。</p>
<p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 才能從比例設定變成可回退策略。</p>
<p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 越容易被驗證。</p>
<h3 id="切換單位的選擇">切換單位的選擇</h3>
<p>切換單位決定故障的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與回退的精準度。常見切換單位各有不同操作特性：</p>
<table>
  <thead>
      <tr>
          <th>切換單位</th>
          <th>blast radius</th>
          <th>回退精準度</th>
          <th>操作複雜度</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>比例（%）</td>
          <td>按流量比例</td>
          <td>粗（全域）</td>
          <td>低</td>
          <td>通用 canary</td>
      </tr>
      <tr>
          <td>區域 / AZ</td>
          <td>限定地理範圍</td>
          <td>中</td>
          <td>中</td>
          <td>跨區部署的服務</td>
      </tr>
      <tr>
          <td>租戶 / 組織</td>
          <td>限定特定客戶</td>
          <td>高</td>
          <td>高</td>
          <td>多租戶 SaaS</td>
      </tr>
      <tr>
          <td>路由規則</td>
          <td>限定特定路徑</td>
          <td>高</td>
          <td>高</td>
          <td>API 版本切換、功能漸進上線</td>
      </tr>
  </tbody>
</table>
<p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。</p>
<h2 id="config-boundary">Config Boundary</h2>
<p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 和應用版本不一定同步，因此要保留相容窗口。</p>
<p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。</p>
<h3 id="config-變更的風險分級">Config 變更的風險分級</h3>
<p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。</p>
<table>
  <thead>
      <tr>
          <th>風險等級</th>
          <th>設定類型</th>
          <th>review 與 rollout 要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>payment endpoint、auth provider URL、encryption key</td>
          <td>等同 code review + staged rollout + rollback 驗證</td>
      </tr>
      <tr>
          <td>中</td>
          <td>rate limit、timeout、feature flag、CORS 設定</td>
          <td>變更 review + 觀測窗口</td>
      </tr>
      <tr>
          <td>低</td>
          <td>log level、debug flag、非關鍵 UI 文案</td>
          <td>變更紀錄即可</td>
      </tr>
  </tbody>
</table>
<p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨</a>。</p>
<h2 id="secret-boundary">Secret Boundary</h2>
<p>Credential、token、certificate 與 machine identity 需要可輪替、可稽核、可回退——Secret boundary 管理這組生命週期。Secret 變更同時影響平台、應用與外部依賴，應使用比普通 config 更嚴格的 evidence 與 rollback window。</p>
<p>Secret rollout 要回答版本相容、雙軌驗證、舊 secret 撤除時間與失敗回退。這裡要接到 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence</a>。</p>
<h3 id="secret-rollout-的雙軌驗證">Secret Rollout 的雙軌驗證</h3>
<p>Secret 輪替跟應用版本部署有本質差異：rollback secret 不是「換回舊版本」那麼單純——舊 secret 可能已經被撤銷、過期、或在外部系統中標記為失效。Secret rollout 的安全做法是雙軌驗證：</p>
<ol>
<li><strong>新 secret 先加入、舊 secret 暫不移除</strong>：應用先驗證能用新 secret 正常運作。</li>
<li><strong>觀測窗口確認新 secret 穩定</strong>：auth 成功率、API 呼叫成功率、certificate handshake 成功率都在 baseline 內。</li>
<li><strong>確認後移除舊 secret</strong>：舊 secret 的撤除要有明確時間點，而且要在撤除前確認沒有服務還在用舊 secret。</li>
</ol>
<p>這個流程的風險點是第 3 步：撤除舊 secret 後發現某個遺漏的服務或 job 還在用、導致該服務認證失敗。盤點覆蓋率的做法是在觀測窗口內搜尋 audit log，確認所有 secret 使用都已切到新版本。</p>
<h2 id="service-discovery-boundary">Service Discovery Boundary</h2>
<p>Service discovery 的責任是維持可用 endpoint 集合。它回答服務應該連到哪些實例；業務設定與版本正確性則分別交給 config boundary 與 rollout gate。Discovery 的 DNS / registry 運作模式與註冊時序見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 Service Discovery</a>。</p>
<p>Discovery 失準常見於 rollout、擴縮容與區域故障。判讀時要拆成註冊時序、健康判斷、DNS/registry 新鮮度與 fallback 存活時間。</p>
<h2 id="control-plane-boundary">Control Plane Boundary</h2>
<p>設定、策略、部署與路由規則的管理落在 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>。Control plane 變更會影響大量服務，因此需要更嚴格的 evidence、gate 與 decision log。</p>
<p>Control plane 事故常見於規則推送、routing 誤配、secret 下發失敗與 registry 異常。這類事故要先保留 decision timeline，避免事後只看到資料面錯誤率。</p>
<h3 id="control-plane-變更的-blast-radius-控制">Control Plane 變更的 Blast Radius 控制</h3>
<p>Control plane 變更的 blast radius 跟 data plane 變更不同——一條 routing rule 推送錯誤可能同時影響所有服務的流量。控制 blast radius 的做法：</p>
<ol>
<li><strong>分批推送</strong>：規則變更先推到 staging / canary namespace、驗證後再推到 production。推送結果的觀測應包含受影響服務的 error rate 與 latency。</li>
<li><strong>approval gate</strong>：高影響變更（network policy、admission webhook、RBAC binding）需要多人 review。變更的 blast radius 估算（影響多少 namespace / service）應在 review 時可見。</li>
<li><strong>decision log</strong>：所有 control plane 變更記入 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>，包含時間、操作者、受影響範圍、預期效果與回退條件。事故時對照 decision log 跟 data plane 症狀的時間序列，可以快速判斷因果。</li>
</ol>
<h2 id="平台元件升級的可重播流程">平台元件升級的可重播流程</h2>
<p>平台基礎元件升級是 control plane 風險最高的場景。Service mesh、ingress controller、CNI、API server 這類元件影響面廣、單次升級可能形成全域風險放大器。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a>：揭露 1 個判讀（基礎平台元件升級缺乏分批治理會形成全域風險放大器）+ 3 條策略（分批升級 + 回退窗口、升級驗證標準固定化、升級事件接入 incident command 節奏）。以下基於通用工程知識展開、「升級事件進 timeline」是從 case「接入 incident command」策略進一步推到具體操作。</p>
<p>可重複套用的升級流程：</p>
<ol>
<li><strong>分批升級單位</strong>：先在開發 / staging 叢集驗證、再選低流量 production 叢集 / namespace 作為先導、之後分批擴大。分批單位可以是叢集、namespace、region、tenant，依風險面選擇。</li>
<li><strong>回退窗口跟驗證標準同時設</strong>：每批升級前定義「驗證通過」的具體訊號（SLI 維持、特定 metric 不偏移、無新告警），跟「回退窗口」（多久內可以回退）。沒有驗證標準的分批等於連續高風險動作。</li>
<li><strong>升級流程紀錄到 incident-style 文件</strong>：升級期間的決策、觀察、停止點都用 incident decision log 格式紀錄。下次升級可重播、不依賴執行者個人經驗。</li>
<li><strong>升級事件進 timeline</strong>：升級本身產生的短暫錯誤、reconnect、配置同步延遲，要在事故 timeline 上可見、避免被誤判成事故。</li>
</ol>
<p>平台元件升級的核心治理價值是把「一次性高風險作業」變成「可重複的低風險作業」。第一次升級用流程，第二次升級用同樣流程，第三次升級流程已經穩定到可以委派、不再需要資深工程師親自執行。</p>
<h2 id="managed-平台跟團隊職責邊界">Managed 平台跟團隊職責邊界</h2>
<p>平台託管化（self-managed → managed）改變維運責任跟團隊精力的分配。本段聚焦團隊職責邊界；流量跟依賴的分段切換流程見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>、紅隊視角的攻擊面變動見 <a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/#%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb%e6%9c%9f%e7%9a%84%e6%94%bb%e6%93%8a%e9%9d%a2%e8%ae%8a%e5%8b%95" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台遷移期的攻擊面變動</a>、三者組合才完整。</p>
<p>Platform team 從「維持 Kubernetes 跑起來」轉向「定義 release flow、observability convention、cost governance」。managed 平台採用後第一個治理動作是顯式重新定義職責邊界、讓 platform team 從 cluster ops 轉到 release flow / observability convention / cost governance。重新定義缺位、組織轉型紅利容易被誤判為純技術升級。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro Managed EKS 遷移</a>：揭露 1 個判讀（平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略）+ 3 條策略（先定義遷移後的平台責任邊界、自動化流程取代手動平台操作、incident 跟 release policy 接回平台治理）。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch Azure AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業 K8s 動機是治理一致性 + 釋放工程資源到業務功能（後者屬作者判讀）。以下基於通用工程知識展開。</p>
<p>managed 平台採用後的職責邊界重訂可以分四層：</p>
<ol>
<li><strong>Cluster 層</strong>：control plane 上游接管（API server、etcd、scheduler、controller-manager）、platform team 從 cluster ops 退到 cluster policy。CIS benchmark、network policy、admission controller 配置仍是 platform 責任。</li>
<li><strong>Cluster-internal 層</strong>：CNI、ingress controller、service mesh、cluster DNS、storage CSI 通常仍由 platform team own。這層是 managed 服務沒覆蓋的 grey zone、需要明確 ownership。</li>
<li><strong>Application 層</strong>：deployment、service、HPA、PDB 由 service team own、platform 提供 convention 跟 review process。</li>
<li><strong>跨層議題</strong>：cost governance、observability convention、release flow、incident response 是 platform / service / SRE / finance 跨層協作、需要 operating model 明確化。</li>
</ol>
<p>managed 採用後 day-1 治理項目有兩件事：明確界定 grey zone ownership（避免「以為 managed 服務什麼都管了」的心智模型）、把 platform team 心力從 cluster ops 轉到組織轉型紅利（release flow、observability convention、cost governance）。把重新定義職責當 day-2 議題、會錯失組織轉型紅利。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>平台選型前要先回答：</p>
<ol>
<li>哪些變更屬於 traffic，哪些屬於 config，哪些屬於 secret。</li>
<li>每種變更是否能分批、暫停與回退。</li>
<li>Discovery 失準時是否有可控 fallback。</li>
<li>Control plane 變更是否有 audit、owner 與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 限制。</li>
<li>基礎元件升級是否有可重播流程跟回退窗口。</li>
<li>Managed 平台採用後團隊職責邊界是否重新定義。</li>
</ol>
<p>這些答案決定後續要比較 load balancer、service mesh、secret manager、service registry 或 deployment controller 的能力。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體平台文章要承接本篇的 traffic、config 與 control plane boundary。ELB、nginx、Envoy、service mesh、Consul、Kubernetes controller、secret manager 或 Terraform 的比較，要先分清它們是在資料面接流量、在控制面改規則，還是在設定面下發狀態。</p>
<p>若主問題是流量切換，後續文章要比較 routing rule、weight、health check、drain 與 rollback。若主問題是設定與 secret，後續文章要比較 rollout、audit、rotation 與相容窗口。若主問題是 control plane 風險，後續文章要比較 blast radius、approval、observability 與 incident decision log。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要把流量邊界接到實際 LB 合約，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要把 control plane 決策寫入事故流程，接著讀 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back</a>。</p>
]]></content:encoded></item><item><title>2.C7 Cloudflare：Cache Reserve 分層儲存快取</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/</guid><description>&lt;p>這個案例的核心責任是把快取從短期命中策略擴展到長期容量策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Cloudflare Cache Reserve 透過分層儲存延長快取可用性，降低 origin 回源成本。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當熱門資料長尾明顯，僅靠 edge cache 會有命中率上限，需引入分層儲存。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>定義 edge 與 reserve 的資料分層規則。&lt;/li>
&lt;li>把回源成本納入快取策略評估。&lt;/li>
&lt;li>監控命中率、延遲與儲存成本三者平衡。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/introducing-cache-reserve/">Cloudflare Cache Reserve&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把快取從短期命中策略擴展到長期容量策略。</p>
<h2 id="觀察">觀察</h2>
<p>Cloudflare Cache Reserve 透過分層儲存延長快取可用性，降低 origin 回源成本。</p>
<h2 id="判讀">判讀</h2>
<p>當熱門資料長尾明顯，僅靠 edge cache 會有命中率上限，需引入分層儲存。</p>
<h2 id="策略">策略</h2>
<ol>
<li>定義 edge 與 reserve 的資料分層規則。</li>
<li>把回源成本納入快取策略評估。</li>
<li>監控命中率、延遲與儲存成本三者平衡。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a> 與 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/introducing-cache-reserve/">Cloudflare Cache Reserve</a></li>
</ul>
]]></content:encoded></item><item><title>3.C7 LinkedIn：Kafka 自動修復治理</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/</guid><description>&lt;p>這個案例的核心責任是把 queue 可靠性從人力值班轉成自動化機制。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>LinkedIn 在 Kafka 維運中導入自動化治理，降低人工介入與恢復時間波動。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當叢集規模超過人力可及範圍，自動修復與治理工具會成為必要能力。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>明確定義可自動修復的故障類型。&lt;/li>
&lt;li>將自動修復與人工升級條件分離。&lt;/li>
&lt;li>把修復過程納入可觀測證據鏈。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/runbook-lifecycle/" data-link-title="8.16 Runbook Lifecycle 管理" data-link-desc="把 runbook 從一次性文件變成有版本、有演練、會過期的 artifact">8.16&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.linkedin.com/blog">Automating Kafka Self-Healing at LinkedIn&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 queue 可靠性從人力值班轉成自動化機制。</p>
<h2 id="觀察">觀察</h2>
<p>LinkedIn 在 Kafka 維運中導入自動化治理，降低人工介入與恢復時間波動。</p>
<h2 id="判讀">判讀</h2>
<p>當叢集規模超過人力可及範圍，自動修復與治理工具會成為必要能力。</p>
<h2 id="策略">策略</h2>
<ol>
<li>明確定義可自動修復的故障類型。</li>
<li>將自動修復與人工升級條件分離。</li>
<li>把修復過程納入可觀測證據鏈。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2</a> 與 <a href="/blog/backend/08-incident-response/runbook-lifecycle/" data-link-title="8.16 Runbook Lifecycle 管理" data-link-desc="把 runbook 從一次性文件變成有版本、有演練、會過期的 artifact">8.16</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.linkedin.com/blog">Automating Kafka Self-Healing at LinkedIn</a></li>
</ul>
]]></content:encoded></item><item><title>4.C7 Datadog：OTel 相容遷移實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/</guid><description>&lt;p>這個案例的核心責任是把 observability 遷移做成可逐步替換的技術路線。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Datadog 與 OTel 生態整合的做法，顯示團隊可在不一次重寫下逐步切換採集管線。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>觀測遷移的主要風險是資料語意漂移與管線雙軌期成本，而非單一 agent 安裝。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先建立雙軌採集的對照驗證。&lt;/li>
&lt;li>把 schema 與 sampling 政策版本化。&lt;/li>
&lt;li>用品質指標決定何時關閉舊管線。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.datadoghq.com/blog/instrument-python-apps-with-datadog-and-opentelemetry/">Datadog and OpenTelemetry&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 observability 遷移做成可逐步替換的技術路線。</p>
<h2 id="觀察">觀察</h2>
<p>Datadog 與 OTel 生態整合的做法，顯示團隊可在不一次重寫下逐步切換採集管線。</p>
<h2 id="判讀">判讀</h2>
<p>觀測遷移的主要風險是資料語意漂移與管線雙軌期成本，而非單一 agent 安裝。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先建立雙軌採集的對照驗證。</li>
<li>把 schema 與 sampling 政策版本化。</li>
<li>用品質指標決定何時關閉舊管線。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<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</a> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.datadoghq.com/blog/instrument-python-apps-with-datadog-and-opentelemetry/">Datadog and OpenTelemetry</a></li>
</ul>
]]></content:encoded></item><item><title>5.C7 Airbnb：Istio 升級治理</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/</guid><description>&lt;p>這個案例的核心責任是把平台元件升級從一次性作業轉成可重播流程。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 在數十個 Kubernetes 叢集、數萬個 pod、數千個 VM 的規模下持續升級 Istio service mesh，峰值流量達數千萬 QPS。團隊累計完成 14 次成功的 Istio 升級。&lt;/p>
&lt;p>升級的核心挑戰是規模帶來的協同成本：無法逐一通知每個 workload team 進行升級配合，也無法同時監控所有 workload 的升級狀態。升級策略必須對 workload team 透明——workload 不需要改程式碼或調配置就能完成 proxy 版本切換。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>基礎平台元件升級若缺乏分批治理，會形成全域風險放大器。Istio 升級的影響面覆蓋所有跑 sidecar 的服務——一次壞的升級可以讓整個叢集的服務間通訊中斷。這個風險決定了升級策略必須是 canary 模式（小比例先行），而且 canary 的粒度要夠細（namespace 或 workload 級別），才能在問題擴大前攔截。&lt;/p>
&lt;p>另一個判讀是升級流程本身要版本化。第一次升級靠資深工程師手動操作可以成功，但這個知識留在個人經驗裡。第二次升級換了人就可能踩到不同的坑。把升級流程固定成可重播的 spec（升級計畫 → 執行 → 驗證 → 確認/回退），讓升級從「英雄行為」變成「例行操作」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Canary upgrade model（兩版本並存）&lt;/strong>：採用 Istio 的 canary upgrade 機制，同時跑兩個版本的 Istiod。新版本的 sidecar proxy 跟對應版本的 control plane 配置一起原子部署，避免跨版本相容性問題。透過 revision label 決定每個 namespace 使用哪個版本的 Istiod。&lt;/li>
&lt;li>&lt;strong>自建工具解耦基礎設施更新與 workload 部署&lt;/strong>：團隊開發了 Krispr（mutation framework），在 CI 階段注入 Istio revision label，並在 admission 階段對超過兩週未部署的 pod 重新注入最新 label。這讓 workload 在正常部署流程中自動完成 proxy 升級，不需要額外操作。&lt;/li>
&lt;li>&lt;strong>rollouts.yml 定義升級批次與比例&lt;/strong>：用 spec 檔定義每個環境（staging / production）、每個 namespace pattern 的版本分佈（例如 staging 75% 舊版 / 25% 新版）。比例可以逐步調整——先 5% → 25% → 50% → 100%。每個批次有明確的觀測窗口與停損條件。&lt;/li>
&lt;li>&lt;strong>VM 升級用 mxrc controller&lt;/strong>：Kubernetes 外的 VM workload 用 mxrc controller 根據 rollouts.yml 更新 tag，遵守健康狀態檢查與可用性門檻。VM 的升級通常在兩週內透過自然輪替完成。&lt;/li>
&lt;li>&lt;strong>升級事件進 incident timeline&lt;/strong>：升級期間的短暫錯誤（proxy 重連、配置同步延遲）在事故 timeline 上標記為升級事件，避免被誤判成獨立事故。升級的決策紀錄用 incident decision log 格式，讓下次升級可以回溯上次的判斷依據。&lt;/li>
&lt;/ol>
&lt;h2 id="升級節奏的收斂">升級節奏的收斂&lt;/h2>
&lt;p>14 次升級的經驗讓升級流程逐步收斂。多數 workload 在正常 deployment 時自動完成 proxy 升級（因為 Krispr 在 admission 階段注入最新 revision）。沒有 regular deployment 的 workload 在四週內透過自然 pod cycling（node 維護、HPA 調整）完成升級。這個四週窗口是可接受的——超過四週未部署的 workload 通常也是低變動、低風險的。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>Istio 升級的回退是把 revision label 切回舊版本、讓 pod 在下次 restart 時重新注入舊版 sidecar。回退的風險在於回退期間新舊 proxy 混跑，traffic policy 可能不完全一致。穩定做法是先在小範圍驗證回退行為（一個 namespace），確認 traffic policy 一致性後再擴大回退範圍。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment&lt;/a> 看 rollout 節奏與 probe 設計。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a> 看通用升級框架。回 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.6 IC handoff&lt;/a> 看升級期事故的指揮交接。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台元件升級從一次性作業轉成可重播流程。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 在數十個 Kubernetes 叢集、數萬個 pod、數千個 VM 的規模下持續升級 Istio service mesh，峰值流量達數千萬 QPS。團隊累計完成 14 次成功的 Istio 升級。</p>
<p>升級的核心挑戰是規模帶來的協同成本：無法逐一通知每個 workload team 進行升級配合，也無法同時監控所有 workload 的升級狀態。升級策略必須對 workload team 透明——workload 不需要改程式碼或調配置就能完成 proxy 版本切換。</p>
<h2 id="判讀">判讀</h2>
<p>基礎平台元件升級若缺乏分批治理，會形成全域風險放大器。Istio 升級的影響面覆蓋所有跑 sidecar 的服務——一次壞的升級可以讓整個叢集的服務間通訊中斷。這個風險決定了升級策略必須是 canary 模式（小比例先行），而且 canary 的粒度要夠細（namespace 或 workload 級別），才能在問題擴大前攔截。</p>
<p>另一個判讀是升級流程本身要版本化。第一次升級靠資深工程師手動操作可以成功，但這個知識留在個人經驗裡。第二次升級換了人就可能踩到不同的坑。把升級流程固定成可重播的 spec（升級計畫 → 執行 → 驗證 → 確認/回退），讓升級從「英雄行為」變成「例行操作」。</p>
<h2 id="策略">策略</h2>
<ol>
<li><strong>Canary upgrade model（兩版本並存）</strong>：採用 Istio 的 canary upgrade 機制，同時跑兩個版本的 Istiod。新版本的 sidecar proxy 跟對應版本的 control plane 配置一起原子部署，避免跨版本相容性問題。透過 revision label 決定每個 namespace 使用哪個版本的 Istiod。</li>
<li><strong>自建工具解耦基礎設施更新與 workload 部署</strong>：團隊開發了 Krispr（mutation framework），在 CI 階段注入 Istio revision label，並在 admission 階段對超過兩週未部署的 pod 重新注入最新 label。這讓 workload 在正常部署流程中自動完成 proxy 升級，不需要額外操作。</li>
<li><strong>rollouts.yml 定義升級批次與比例</strong>：用 spec 檔定義每個環境（staging / production）、每個 namespace pattern 的版本分佈（例如 staging 75% 舊版 / 25% 新版）。比例可以逐步調整——先 5% → 25% → 50% → 100%。每個批次有明確的觀測窗口與停損條件。</li>
<li><strong>VM 升級用 mxrc controller</strong>：Kubernetes 外的 VM workload 用 mxrc controller 根據 rollouts.yml 更新 tag，遵守健康狀態檢查與可用性門檻。VM 的升級通常在兩週內透過自然輪替完成。</li>
<li><strong>升級事件進 incident timeline</strong>：升級期間的短暫錯誤（proxy 重連、配置同步延遲）在事故 timeline 上標記為升級事件，避免被誤判成獨立事故。升級的決策紀錄用 incident decision log 格式，讓下次升級可以回溯上次的判斷依據。</li>
</ol>
<h2 id="升級節奏的收斂">升級節奏的收斂</h2>
<p>14 次升級的經驗讓升級流程逐步收斂。多數 workload 在正常 deployment 時自動完成 proxy 升級（因為 Krispr 在 admission 階段注入最新 revision）。沒有 regular deployment 的 workload 在四週內透過自然 pod cycling（node 維護、HPA 調整）完成升級。這個四週窗口是可接受的——超過四週未部署的 workload 通常也是低變動、低風險的。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>Istio 升級的回退是把 revision label 切回舊版本、讓 pod 在下次 restart 時重新注入舊版 sidecar。回退的風險在於回退期間新舊 proxy 混跑，traffic policy 可能不完全一致。穩定做法是先在小範圍驗證回退行為（一個 namespace），確認 traffic policy 一致性後再擴大回退範圍。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 kubernetes deployment</a> 看 rollout 節奏與 probe 設計。回 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a> 看通用升級框架。回 <a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.6 IC handoff</a> 看升級期事故的指揮交接。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/seamless-istio-upgrades-at-scale/">Seamless Istio Upgrades at Scale</a></li>
</ul>
]]></content:encoded></item><item><title>7.C7 Okta：BYO Telephony 的身份安全責任轉換</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/</guid><description>&lt;p>這個案例的核心責任是說明身份安全控制也會出現供應鏈責任重分配。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Okta 推動 BYO telephony，將 SMS/voice MFA 的供應商控制責任轉給客戶側治理。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這類轉換是信任邊界與責任邊界變更，需要同步更新風險模型，單純當功能變更處理會漏掉安全面。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>明確定義 telephony provider 的安全要求。&lt;/li>
&lt;li>把供應商變更納入身份風險評估節奏。&lt;/li>
&lt;li>建立跨供應商故障與濫用應變流程。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sec.okta.com/articles/2023/08/byo-telephony-and-future-sms-okta/">BYO Telephony and the future of SMS at Okta&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明身份安全控制也會出現供應鏈責任重分配。</p>
<h2 id="觀察">觀察</h2>
<p>Okta 推動 BYO telephony，將 SMS/voice MFA 的供應商控制責任轉給客戶側治理。</p>
<h2 id="判讀">判讀</h2>
<p>這類轉換是信任邊界與責任邊界變更，需要同步更新風險模型，單純當功能變更處理會漏掉安全面。</p>
<h2 id="策略">策略</h2>
<ol>
<li>明確定義 telephony provider 的安全要求。</li>
<li>把供應商變更納入身份風險評估節奏。</li>
<li>建立跨供應商故障與濫用應變流程。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10</a> 與 <a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sec.okta.com/articles/2023/08/byo-telephony-and-future-sms-okta/">BYO Telephony and the future of SMS at Okta</a></li>
</ul>
]]></content:encoded></item><item><title>4.7 Cardinality 治理與成本邊界</title><link>https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>cardinality 為何爆：unbounded label（user_id / request_id / url path）&lt;/li>
&lt;li>metrics 的 cardinality 影響：時序資料庫 series 爆炸、查詢退化&lt;/li>
&lt;li>log 的 cardinality 影響：索引膨脹、保留成本&lt;/li>
&lt;li>trace 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略：head sampling vs tail sampling、tradeoff&lt;/li>
&lt;li>cost-aware observability：成本作為治理輸入而非事後賬單&lt;/li>
&lt;li>governance 控制面：label 白名單、ingestion quota、保留階梯&lt;/li>
&lt;li>高峰場景：流量尖峰時 cardinality slope 是 leading indicator&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a> 的分工：4.1 設計欄位、4.7 設邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics&lt;/a> 的分工：4.2 是 metric 種類、4.7 是 label 治理&lt;/li>
&lt;li>反模式：所有事件都打高 cardinality label、預算耗盡才砍訊號、保留策略無階梯&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Cardinality 治理是把觀測維度當成有限資源管理的流程，責任是讓訊號足夠可切分，同時不讓儲存、查詢與告警成本失控。&lt;/p>
&lt;p>這一頁處理的是成本邊界。可觀測性需要有選擇地收集訊號；它把高價值維度留在可查詢路徑，把低價值或無界維度放到更合適的資料層。&lt;/p>
&lt;p>Cardinality 跟成本的關係是非線性的。Label 數目每增加一倍，metric series 數目可能呈乘法增長；查詢延遲、儲存大小、索引重建時間都會跟著放大。把 cardinality 視為一級治理項目，能避免「收得越多越好」的直覺推著成本上升。&lt;/p>
&lt;h2 id="cardinality-在不同訊號的失分模式">Cardinality 在不同訊號的失分模式&lt;/h2>
&lt;p>Cardinality 在 metric、log、trace 三類訊號的影響機制不同，失分模式也不同。把三者用同一套治理規則處理，會在某類訊號上過度限制、在另一類上失控。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號類型&lt;/th>
 &lt;th>主要失分機制&lt;/th>
 &lt;th>控制手段&lt;/th>
 &lt;th>典型 trigger&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Metric&lt;/td>
 &lt;td>TSDB series 爆炸、查詢退化&lt;/td>
 &lt;td>label 白名單、bucketize、aggregation&lt;/td>
 &lt;td>user_id / request_id 進 label&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log&lt;/td>
 &lt;td>索引膨脹、保留成本暴增&lt;/td>
 &lt;td>索引欄位限制、結構化分層、分流&lt;/td>
 &lt;td>完整 URL / payload 進索引欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trace&lt;/td>
 &lt;td>sampling 後遺失高價值樣本&lt;/td>
 &lt;td>tail sampling、minimum sample floor、 exemplar&lt;/td>
 &lt;td>head sampling 比例固定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Metric cardinality 是最敏感的維度。Prometheus 等 pull-based TSDB 在 series 數超過數百萬時查詢退化、aggregation 失準、recording rule 跑不完。Cloud 託管型 TSDB 雖然容量更大，但每個 active series 的單價非常具體，cardinality 直接對應 vendor 月帳單。&lt;/p>
&lt;p>Log cardinality 的失分比較緩慢。Log 的 unique 值多本身不會立即崩潰，但全文索引 + 結構化欄位索引會持續膨脹，到某個臨界點查詢從毫秒退化到秒、再到分鐘。一般診斷不易察覺，要靠 query latency 跟 index size 的長期趨勢才能發現。&lt;/p>
&lt;p>Trace cardinality 的問題是另一種：sampling 過於粗暴會丟失高價值樣本。低流量服務、錯誤樣本、長尾延遲樣本若被 head sampling 平均稀釋，事故時無 trace 可看。Trace 的治理重點是 sampling 策略而非單純限制 cardinality。&lt;/p>
&lt;h2 id="高-cardinality-的常見來源">高 cardinality 的常見來源&lt;/h2>
&lt;p>無界維度進入可查詢路徑是 cardinality 失控的最大來源。常見的「無意中變成 label」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>User / tenant identifier&lt;/strong>：把 user_id 當 label 時，每個用戶都產生一條 series。10 萬用戶 = 10 萬條 series 乘以其他 label 的笛卡爾積。&lt;/li>
&lt;li>&lt;strong>Request / session identifier&lt;/strong>：request_id、session_id、trace_id 本質是無界的，進入 metric label 後 series 無限增長。&lt;/li>
&lt;li>&lt;strong>完整 URL / path parameters&lt;/strong>：&lt;code>/users/123/orders/456&lt;/code> 這類 path 進入 label，每個 unique URL 都是新 series。&lt;/li>
&lt;li>&lt;strong>錯誤訊息 / stack trace&lt;/strong>：把 raw error message 當 label 時，每次新錯誤 = 新 series。&lt;/li>
&lt;li>&lt;strong>時間戳跟亂數&lt;/strong>：偶發出現的 bug，把 timestamp、uuid 寫進 label。&lt;/li>
&lt;/ul>
&lt;p>這些都應該進 &lt;em>log&lt;/em> 或 &lt;em>trace&lt;/em> 的欄位，不該進 &lt;em>metric&lt;/em> 的 label。Metric 的 label 應該是有界的維度：service name、environment、region、status code、http method、error class。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>cardinality 為何爆：unbounded label（user_id / request_id / url path）</li>
<li>metrics 的 cardinality 影響：時序資料庫 series 爆炸、查詢退化</li>
<li>log 的 cardinality 影響：索引膨脹、保留成本</li>
<li>trace 的 <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略：head sampling vs tail sampling、tradeoff</li>
<li>cost-aware observability：成本作為治理輸入而非事後賬單</li>
<li>governance 控制面：label 白名單、ingestion quota、保留階梯</li>
<li>高峰場景：流量尖峰時 cardinality slope 是 leading indicator</li>
<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> 的分工：4.1 設計欄位、4.7 設邊界</li>
<li>跟 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a> 的分工：4.2 是 metric 種類、4.7 是 label 治理</li>
<li>反模式：所有事件都打高 cardinality label、預算耗盡才砍訊號、保留策略無階梯</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Cardinality 治理是把觀測維度當成有限資源管理的流程，責任是讓訊號足夠可切分，同時不讓儲存、查詢與告警成本失控。</p>
<p>這一頁處理的是成本邊界。可觀測性需要有選擇地收集訊號；它把高價值維度留在可查詢路徑，把低價值或無界維度放到更合適的資料層。</p>
<p>Cardinality 跟成本的關係是非線性的。Label 數目每增加一倍，metric series 數目可能呈乘法增長；查詢延遲、儲存大小、索引重建時間都會跟著放大。把 cardinality 視為一級治理項目，能避免「收得越多越好」的直覺推著成本上升。</p>
<h2 id="cardinality-在不同訊號的失分模式">Cardinality 在不同訊號的失分模式</h2>
<p>Cardinality 在 metric、log、trace 三類訊號的影響機制不同，失分模式也不同。把三者用同一套治理規則處理，會在某類訊號上過度限制、在另一類上失控。</p>
<table>
  <thead>
      <tr>
          <th>訊號類型</th>
          <th>主要失分機制</th>
          <th>控制手段</th>
          <th>典型 trigger</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>TSDB series 爆炸、查詢退化</td>
          <td>label 白名單、bucketize、aggregation</td>
          <td>user_id / request_id 進 label</td>
      </tr>
      <tr>
          <td>Log</td>
          <td>索引膨脹、保留成本暴增</td>
          <td>索引欄位限制、結構化分層、分流</td>
          <td>完整 URL / payload 進索引欄位</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>sampling 後遺失高價值樣本</td>
          <td>tail sampling、minimum sample floor、 exemplar</td>
          <td>head sampling 比例固定</td>
      </tr>
  </tbody>
</table>
<p>Metric cardinality 是最敏感的維度。Prometheus 等 pull-based TSDB 在 series 數超過數百萬時查詢退化、aggregation 失準、recording rule 跑不完。Cloud 託管型 TSDB 雖然容量更大，但每個 active series 的單價非常具體，cardinality 直接對應 vendor 月帳單。</p>
<p>Log cardinality 的失分比較緩慢。Log 的 unique 值多本身不會立即崩潰，但全文索引 + 結構化欄位索引會持續膨脹，到某個臨界點查詢從毫秒退化到秒、再到分鐘。一般診斷不易察覺，要靠 query latency 跟 index size 的長期趨勢才能發現。</p>
<p>Trace cardinality 的問題是另一種：sampling 過於粗暴會丟失高價值樣本。低流量服務、錯誤樣本、長尾延遲樣本若被 head sampling 平均稀釋，事故時無 trace 可看。Trace 的治理重點是 sampling 策略而非單純限制 cardinality。</p>
<h2 id="高-cardinality-的常見來源">高 cardinality 的常見來源</h2>
<p>無界維度進入可查詢路徑是 cardinality 失控的最大來源。常見的「無意中變成 label」：</p>
<ul>
<li><strong>User / tenant identifier</strong>：把 user_id 當 label 時，每個用戶都產生一條 series。10 萬用戶 = 10 萬條 series 乘以其他 label 的笛卡爾積。</li>
<li><strong>Request / session identifier</strong>：request_id、session_id、trace_id 本質是無界的，進入 metric label 後 series 無限增長。</li>
<li><strong>完整 URL / path parameters</strong>：<code>/users/123/orders/456</code> 這類 path 進入 label，每個 unique URL 都是新 series。</li>
<li><strong>錯誤訊息 / stack trace</strong>：把 raw error message 當 label 時，每次新錯誤 = 新 series。</li>
<li><strong>時間戳跟亂數</strong>：偶發出現的 bug，把 timestamp、uuid 寫進 label。</li>
</ul>
<p>這些都應該進 <em>log</em> 或 <em>trace</em> 的欄位，不該進 <em>metric</em> 的 label。Metric 的 label 應該是有界的維度：service name、environment、region、status code、http method、error class。</p>
<h2 id="高峰場景的-cardinality-失控">高峰場景的 cardinality 失控</h2>
<p>高峰場景的 cardinality 治理責任是讓「平時可控的 series 上限」在尖峰時仍能維持決策可用。平時 cardinality 看似穩定，高峰時可能突然出現新 tenant、新 endpoint、新 error class 的湧入，把 series 推到平台極限；治理重點是把「成長斜率」「容量緩衝」「dry-run」「freshness gap」變成預先設計的訊號、而非高峰中即興救火。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming 高峰流量下的訊號新鮮度與 Cardinality</a>：揭露「ingestion lag、cardinality growth slope、alert freshness gap」是高峰場景的核心治理項目（三個訊號名稱屬 case 直接列出）；以下做法基於通用工程知識展開。</p>
<p>高峰場景的可操作做法：</p>
<ol>
<li><strong>把 cardinality growth slope 視為 leading indicator</strong>：series 數目的成長斜率比絕對值更早反映異常。突然出現的快速上升通常意味著新 label 值湧入或既有 label 失控。</li>
<li><strong>預設容量 buffer</strong>：日常使用容量設在平台上限的 50-60%，留高峰時 cardinality 突發空間。把容量推到 90% 才追加治理會在高峰時來不及。</li>
<li><strong>高峰前的 dry-run</strong>：把預期高峰流量的 cardinality 估算進 capacity model，找出可能的 unbounded label。對應 <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><strong>Alert freshness gap 也要監控</strong>：高峰時 ingestion lag 上升、告警延遲、值班決策落在過期資料上的風險。把 alert freshness（資料時間 vs 當前時間）變成 dashboard 訊號。</li>
</ol>
<p>高峰結束後做 retrospective：哪些 label 在高峰時超出預期、哪些 alert 因延遲沒及時觸發、哪些 series 應該下次提前 bucketize。這個 retrospective 是治理閉環的一部分，由 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal-governance-loop</a> 處理長期回寫。</p>
<h2 id="sampling-策略">Sampling 策略</h2>
<p>本章是 04 模組的 sampling 策略 SSoT — Head / Tail / Adaptive / Exemplar 四類策略集中在此；sampling 對資料品質的失真風險（low-traffic bias、error sample loss、tail latency loss）由 <a href="/blog/backend/04-observability/telemetry-data-quality/#sampling-%e8%88%87%e4%bb%a3%e8%a1%a8%e6%80%a7" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Sampling 與代表性</a> 處理；trace context 層的 sampling 配置由 <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> 處理。</p>
<p>Sampling 策略的核心責任是控制觀測成本、同時保留足以判讀的高價值樣本。固定比例 head sampling 是最常見、也是最容易丟失高價值樣本的策略。</p>
<table>
  <thead>
      <tr>
          <th>策略類型</th>
          <th>機制</th>
          <th>適用場景</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Head sampling</td>
          <td>在 trace 開始時決定是否採樣</td>
          <td>簡單、低延遲、collector 端低資源</td>
          <td>不知道 trace 結果就決定、可能丟錯誤</td>
      </tr>
      <tr>
          <td>Tail sampling</td>
          <td>等 trace 結束後再決定（看是否錯誤、長延遲）</td>
          <td>保留錯誤、保留 outlier</td>
          <td>collector 要 buffer 整條 trace、資源高</td>
      </tr>
      <tr>
          <td>Adaptive sampling</td>
          <td>按服務、tenant、流量動態調整比例</td>
          <td>多租戶、流量差異大</td>
          <td>規則複雜、需要監控 sampling rate</td>
      </tr>
      <tr>
          <td>Exemplar attachment</td>
          <td>metric 帶代表性 trace id 樣本</td>
          <td>從 metric 跳到 trace</td>
          <td>不解決 sampling 本身、是補充</td>
      </tr>
  </tbody>
</table>
<p>實務上常用組合：低流量服務用接近 100% 採樣（minimum sample floor）、高流量服務用 tail sampling 保留錯誤跟長尾、metric 帶 exemplar 讓從 dashboard 跳到 trace。</p>
<p>四類策略各自的適用情境：</p>
<ul>
<li><strong>Head sampling</strong> 適合單體應用、延遲敏感、collector 端資源吃緊的場景。代價是 trace 開始時無法判斷是否錯誤、會等比例丟掉錯誤樣本。</li>
<li><strong>Tail sampling</strong> 適合微服務、需保留錯誤跟長尾的場景。代價是 collector 要 buffer 整條 trace、記憶體跟 CPU 用量明顯增加、對 cluster gateway 容量規劃壓力大。</li>
<li><strong>Adaptive sampling</strong> 適合多租戶、流量差異大的場景。風險是規則複雜化會造成 sampling rate 漂移、必須持續監控每個 service / tenant 的實際保留比例、否則治理會失控。</li>
<li><strong>Exemplar attachment</strong> 補強 metric → trace 跳轉、不解決 sampling 本身。在已有 head/tail sampling 的場景上加 exemplar 是低成本高價值的做法。</li>
</ul>
<p>關鍵是 sampling policy 本身要可被服務團隊理解跟調整。把 sampling 規則寫在 collector 配置裡、版本化、跟著 release 一起管理；把當前 sampling rate 跟保留分布暴露在 dashboard 上。當服務團隊發現某段時間 trace 殘缺、要能直接查到 sampling policy 的當下值跟變更紀錄。</p>
<h2 id="控制面與保留階梯">控制面與保留階梯</h2>
<p>可操作的 cardinality / 成本治理控制面有四層，從預防到事後審計都要覆蓋。</p>
<ol>
<li><strong>設計時 label 白名單</strong>：服務團隊新增 metric 時要 review label 是否在白名單內。白名單列出有界維度（service、env、region、status_code、error_class、http_method），明確排除 user_id、request_id、完整 URL。</li>
<li><strong>Ingestion 層 quota 與 cardinality limit</strong>：collector 或 vendor 端設定每服務、每 tenant 的 series 上限。超過上限時觸發告警，並啟動 graceful 降級（保留高優先 series、其他暫停）。</li>
<li><strong>保留階梯</strong>：依資料熱度跟法規責任分層保留。熱資料（最近 7 天）full granularity、溫資料（7-30 天）aggregated、冷資料（30+ 天）長期歸檔。階梯設計要結合 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a> 的法規保留期。</li>
<li><strong>成本歸屬到 owner</strong>：把 ingestion、storage、query 成本拆到服務或團隊維度。沒有歸屬的成本會被視為平台問題，治理動力不會傳到產生成本的團隊。詳見 <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>
</ol>
<p>保留階梯的另一個價值是事故時的容量保護。當熱資料儲存接近滿載、可以加速冷化、主動釋放容量給當下事件、避免被動等保留期到再恢復。</p>
<h2 id="storage-tiering-對查詢能力的影響"><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 對查詢能力的影響</h2>
<p>保留階梯不只是成本工具，它直接決定不同時間範圍的查詢能力。每一層的儲存介質、索引密度、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 精度決定了該層能回答什麼問題、不能回答什麼問題。</p>
<h3 id="每一層能回答什麼">每一層能回答什麼</h3>
<p>Hot tier 保留完整精度與完整索引，能支援即席診斷的所有維度切片（by service、by tenant、by error code、by request id）。當資料從 hot 移到 warm，部分索引可能被移除、精度可能被 rollup 降低，能做的查詢從「特定 request id 的完整事件鏈」退化為「某服務過去兩週的 error rate 趨勢」。到 cold tier，通常只剩 timestamp + 少數結構化欄位的最小索引，細節查詢需要先 rehydrate 回 warm 或 hot 層。</p>
<p>這個退化是設計選擇，但需要被使用者感知。事故復盤時，如果團隊想查兩週前的特定 request 但資料已在 warm tier 且 request id 索引被移除，他們需要知道「不是沒有資料，而是需要 rehydrate 才能查」。</p>
<h3 id="跨層查詢的延遲跳變">跨層查詢的延遲跳變</h3>
<p>Dashboard 的時間範圍選擇直接觸發跨層查詢。使用者從「最近 1 小時」（全部在 hot tier）拉到「最近 7 天」（hot + warm tier），查詢延遲從毫秒跳到秒級。再拉到「最近 90 天」（hot + warm + cold tier），延遲可能跳到十秒甚至分鐘級。</p>
<p>這種延遲跳變在事故中的影響是：incident commander 想看長期趨勢來判斷異常是突發還是漸進時，dashboard 卡在載入。應對方式是在 dashboard 設計時就把「長時間趨勢」panel 指向 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 rollup series，讓它讀取預聚合資料而非跨層掃描 raw data。</p>
<h3 id="tier-邊界依訊號類型差異化">Tier 邊界依訊號類型差異化</h3>
<p>不同訊號類型的 tier 邊界應該不同。Error log 跟 trace 的事故診斷價值比 debug log 高，hot tier 保留期應該更長。<a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">Audit log</a> 因合規要求可能需要長期可查詢而非純歸檔。SLO-critical 的 metric series 可能需要 hot tier 保留 30 天來支援 monthly burn rate 計算，而 debug-level 的 metric 只需要 7 天 hot tier。</p>
<p>把所有訊號用同一個 tier 邊界管理（「全部 7 天 hot、30 天 warm、1 年 cold」）會讓高價值訊號過早退化、低價值訊號佔用過多 hot tier 容量。依訊號優先級設定差異化的 tier 邊界是保留階梯設計的進階步驟。</p>
<p>詳細的跨訊號查詢設計見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 cardinality 時，先看維度是否有決策價值，再看它是否有上界。</p>
<p>重點訊號包括：</p>
<ul>
<li>user id、request id、完整 URL 是否進入不該承受的 metric label</li>
<li>log index 是否只索引常用查詢欄位</li>
<li>trace <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 是否能優先保留高價值樣本</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否依資料熱度與法規責任分層</li>
<li>cardinality growth slope 是否被監控為 leading indicator</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>metric series 數量曲線陡升、TSDB 查詢退化</li>
<li>log ingestion 成本月對月雙位數成長</li>
<li>label 含 user_id / request_id / 完整 URL 直接送到 metric</li>
<li>ingestion quota 觸發時靠砍訊號救火、無 graceful 降階</li>
<li>保留策略全平、無冷熱分層、舊資料拖累查詢</li>
<li>高峰時 alert freshness gap 擴大、值班用過期資料</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>無界 label 進 metric</td>
          <td>user_id / request_id 在 label 中</td>
          <td>label 白名單、把細粒度放到 log / trace</td>
      </tr>
      <tr>
          <td>預算耗盡才砍訊號</td>
          <td>quota 觸發後緊急砍 series</td>
          <td>平時設成長告警、緩衝容量 50-60%</td>
      </tr>
      <tr>
          <td>保留策略全平</td>
          <td>所有 log / metric 都留 30 天</td>
          <td>依熱度跟法規分階、結合 audit retention</td>
      </tr>
      <tr>
          <td>Sampling 比例固定</td>
          <td>head sampling 10% 套全部服務</td>
          <td>低流量 100%、錯誤強制保留、tail sampling</td>
      </tr>
      <tr>
          <td>成本無歸屬</td>
          <td>平台付帳、團隊無動力治理</td>
          <td>歸屬到 service owner、進 cost attribution</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：SLI metric 的 cardinality 上限</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal-governance-loop</a>：高峰 retrospective 回寫治理</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 層 quota 執行</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a>：audit 保留期銜接</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/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：storage tiering 對查詢能力的完整設計</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量成本</a>：observability 成本作為容量規劃輸入</li>
<li><a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors</a>：各平台的 ingestion / query quota 模型</li>
</ul>
]]></content:encoded></item><item><title>6.7 DR 演練與 Rollback Rehearsal</title><link>https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>DR 演練與 rollback rehearsal 是把回復能力從「有計畫」變成「經過驗證」的工具。DR 關心的是系統在災難後能不能回來，rollback rehearsal 關心的是變更失敗時能不能退回安全狀態。兩者的責任是把回復路徑變成可驗證流程。&lt;/p>
&lt;p>這個節點先處理路徑，再處理速度。先確認資料能不能回來、服務能不能切回來、回復後會不會再掉回去，然後才談 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>。這樣讀，會比直接背指標更接近真實系統的恢復成本。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>DR 的責任是證明回復路徑存在，而且可實際走通。只要 backup 還沒被 restore 驗證過，它就只是備份，不是復原能力。只要 failover config 沒跟 production 對齊，它就只是文件，不是操作路由。&lt;/p>
&lt;p>rollback rehearsal 的責任是把失敗變更的退路先跑過。當 deployment 出現問題時，團隊需要知道自己是能回退、必須 roll forward，還是必須先止血再處理資料。這個判斷來自平常 rehearsal 的累積，臨場才不會陷入猜測。&lt;/p>
&lt;h2 id="rollback-vs-roll-forward-的判斷條件">Rollback vs Roll-forward 的判斷條件&lt;/h2>
&lt;p>變更失敗時的第一個決策是退回還是往前修。這個判斷取決於變更是否可逆，以及新資料是否已經依賴新版結構。&lt;/p>
&lt;p>rollback 的前提是變更可逆：schema 仍向下相容、feature flag 可關閉、routing 可切回前一版。當這些條件成立時，rollback 通常比 roll-forward 更快收斂，因為退回的行為已經被驗證過（它就是前一版的 production 狀態）。&lt;/p>
&lt;p>roll-forward 的前提是修復比退版快且安全。當新版已經寫入不可回退的資料（新欄位被使用、新格式被下游消費、交易已在新路徑完成），退版會造成資料遺失或不一致，此時 roll-forward 是被迫的選擇，不是偏好。&lt;/p>
&lt;p>兩者之間存在灰色地帶：schema migration 已執行但流量尚未切換、feature flag 已開啟但影響範圍有限。這類情境需要事前在 rehearsal 中定義判斷條件，而不是事中討論。第三種常見路徑是先 rollback 止血（降低 customer impact），確認穩定後再推出修復版 roll-forward。這個 hybrid 策略的前提是 rollback 安全且修復方案已知。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe 的 expand/contract migration 模型&lt;/a> 說明交易系統的 rollback 需要同時處理 schema 相容與冪等重播。當 idempotency key 與業務操作邊界一致時，rollback 後的重試才能產生正確結果。這個案例揭露的判讀條件是：rollback 安全性不只看部署層，還要看資料語義層。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>條件&lt;/th>
 &lt;th>傾向 rollback&lt;/th>
 &lt;th>傾向 roll-forward&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema 相容性&lt;/td>
 &lt;td>舊版可讀新版資料、無破壞性變更&lt;/td>
 &lt;td>新欄位已被寫入、舊版無法解析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料狀態&lt;/td>
 &lt;td>新版尚未產生不可回退的資料&lt;/td>
 &lt;td>交易、訂單或事件已在新路徑完成&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復時間&lt;/td>
 &lt;td>問題根因不明、修復時間不可預測&lt;/td>
 &lt;td>根因明確、修復可在分鐘內完成&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Feature flag&lt;/td>
 &lt;td>flag 可關閉且影響範圍已知&lt;/td>
 &lt;td>flag 關閉會觸發另一組問題&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;h2 id="restore-驗證">Restore 驗證&lt;/h2>
&lt;p>備份的價值在還原時才能被證明。restore drill 的責任是證明備份能在需要時變成可用的服務狀態。&lt;/p>
&lt;p>restore 驗證分三個層次，每一層回答不同的問題。&lt;/p>
&lt;p>&lt;strong>資料完整性&lt;/strong>：還原後的資料是否完整。驗證手段包含 row count 比對、checksum 校驗、reconciliation query。這一層的失敗模式通常是 backup 時段選擇不當（跨越 batch job 執行期）或 incremental backup 鏈條斷裂。&lt;/p>
&lt;p>&lt;strong>服務可用性&lt;/strong>：還原後的系統是否能正常回應。資料完整不代表服務可用 — config、secret、schema version、connection pool 設定都可能在 restore 後失效。這一層需要在 restore 完成後跑 smoke test 與 health check，確認服務能處理請求。&lt;/p>
&lt;p>&lt;strong>恢復時間量測&lt;/strong>：實際 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> 是否符合承諾。如果承諾 4 小時 RTO 但 restore 本身需要 6 小時，這個承諾就是空的。量測要包含從決策啟動到服務恢復的完整時間，不只是資料還原時間。Roblox 2021 的 73 小時 outage 說明 recovery 不是切回流量就結束 — 資料一致性重建、快取預熱與依賴服務的啟動順序都會拉長實際恢復時間。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>tabletop&lt;/td>
 &lt;td>檢查決策路由與角色分工&lt;/td>
 &lt;td>角色清單、決策順序、通訊模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>partial failover&lt;/td>
 &lt;td>驗證局部區域或子系統能否切換&lt;/td>
 &lt;td>切換結果、回復時間、手動步驟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>full region failover&lt;/td>
 &lt;td>驗證整個區域是否能從災難中回來&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>、資料一致性檢查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>data restore drill&lt;/td>
 &lt;td>驗證備份是否能真的還原資料&lt;/td>
 &lt;td>restore log、校驗結果、缺口清單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些演練的共同點是：演練本身要留下證據。沒有輸出，就沒有辦法判斷回復能力到底有沒有被建立。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>DR 演練與 rollback rehearsal 是把回復能力從「有計畫」變成「經過驗證」的工具。DR 關心的是系統在災難後能不能回來，rollback rehearsal 關心的是變更失敗時能不能退回安全狀態。兩者的責任是把回復路徑變成可驗證流程。</p>
<p>這個節點先處理路徑，再處理速度。先確認資料能不能回來、服務能不能切回來、回復後會不會再掉回去，然後才談 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>。這樣讀，會比直接背指標更接近真實系統的恢復成本。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>DR 的責任是證明回復路徑存在，而且可實際走通。只要 backup 還沒被 restore 驗證過，它就只是備份，不是復原能力。只要 failover config 沒跟 production 對齊，它就只是文件，不是操作路由。</p>
<p>rollback rehearsal 的責任是把失敗變更的退路先跑過。當 deployment 出現問題時，團隊需要知道自己是能回退、必須 roll forward，還是必須先止血再處理資料。這個判斷來自平常 rehearsal 的累積，臨場才不會陷入猜測。</p>
<h2 id="rollback-vs-roll-forward-的判斷條件">Rollback vs Roll-forward 的判斷條件</h2>
<p>變更失敗時的第一個決策是退回還是往前修。這個判斷取決於變更是否可逆，以及新資料是否已經依賴新版結構。</p>
<p>rollback 的前提是變更可逆：schema 仍向下相容、feature flag 可關閉、routing 可切回前一版。當這些條件成立時，rollback 通常比 roll-forward 更快收斂，因為退回的行為已經被驗證過（它就是前一版的 production 狀態）。</p>
<p>roll-forward 的前提是修復比退版快且安全。當新版已經寫入不可回退的資料（新欄位被使用、新格式被下游消費、交易已在新路徑完成），退版會造成資料遺失或不一致，此時 roll-forward 是被迫的選擇，不是偏好。</p>
<p>兩者之間存在灰色地帶：schema migration 已執行但流量尚未切換、feature flag 已開啟但影響範圍有限。這類情境需要事前在 rehearsal 中定義判斷條件，而不是事中討論。第三種常見路徑是先 rollback 止血（降低 customer impact），確認穩定後再推出修復版 roll-forward。這個 hybrid 策略的前提是 rollback 安全且修復方案已知。</p>
<p><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe 的 expand/contract migration 模型</a> 說明交易系統的 rollback 需要同時處理 schema 相容與冪等重播。當 idempotency key 與業務操作邊界一致時，rollback 後的重試才能產生正確結果。這個案例揭露的判讀條件是：rollback 安全性不只看部署層，還要看資料語義層。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>傾向 rollback</th>
          <th>傾向 roll-forward</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 相容性</td>
          <td>舊版可讀新版資料、無破壞性變更</td>
          <td>新欄位已被寫入、舊版無法解析</td>
      </tr>
      <tr>
          <td>資料狀態</td>
          <td>新版尚未產生不可回退的資料</td>
          <td>交易、訂單或事件已在新路徑完成</td>
      </tr>
      <tr>
          <td>修復時間</td>
          <td>問題根因不明、修復時間不可預測</td>
          <td>根因明確、修復可在分鐘內完成</td>
      </tr>
      <tr>
          <td>Feature flag</td>
          <td>flag 可關閉且影響範圍已知</td>
          <td>flag 關閉會觸發另一組問題</td>
      </tr>
      <tr>
          <td>下游依賴</td>
          <td>下游未消費新版輸出</td>
          <td>下游已開始處理新格式資料</td>
      </tr>
  </tbody>
</table>
<h2 id="restore-驗證">Restore 驗證</h2>
<p>備份的價值在還原時才能被證明。restore drill 的責任是證明備份能在需要時變成可用的服務狀態。</p>
<p>restore 驗證分三個層次，每一層回答不同的問題。</p>
<p><strong>資料完整性</strong>：還原後的資料是否完整。驗證手段包含 row count 比對、checksum 校驗、reconciliation query。這一層的失敗模式通常是 backup 時段選擇不當（跨越 batch job 執行期）或 incremental backup 鏈條斷裂。</p>
<p><strong>服務可用性</strong>：還原後的系統是否能正常回應。資料完整不代表服務可用 — config、secret、schema version、connection pool 設定都可能在 restore 後失效。這一層需要在 restore 完成後跑 smoke test 與 health check，確認服務能處理請求。</p>
<p><strong>恢復時間量測</strong>：實際 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> 是否符合承諾。如果承諾 4 小時 RTO 但 restore 本身需要 6 小時，這個承諾就是空的。量測要包含從決策啟動到服務恢復的完整時間，不只是資料還原時間。Roblox 2021 的 73 小時 outage 說明 recovery 不是切回流量就結束 — 資料一致性重建、快取預熱與依賴服務的啟動順序都會拉長實際恢復時間。</p>
<h2 id="演練類型">演練類型</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>目的</th>
          <th>典型輸出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tabletop</td>
          <td>檢查決策路由與角色分工</td>
          <td>角色清單、決策順序、通訊模板</td>
      </tr>
      <tr>
          <td>partial failover</td>
          <td>驗證局部區域或子系統能否切換</td>
          <td>切換結果、回復時間、手動步驟</td>
      </tr>
      <tr>
          <td>full region failover</td>
          <td>驗證整個區域是否能從災難中回來</td>
          <td><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、資料一致性檢查</td>
      </tr>
      <tr>
          <td>data restore drill</td>
          <td>驗證備份是否能真的還原資料</td>
          <td>restore log、校驗結果、缺口清單</td>
      </tr>
  </tbody>
</table>
<p>這些演練的共同點是：演練本身要留下證據。沒有輸出，就沒有辦法判斷回復能力到底有沒有被建立。</p>
<p><strong>Tabletop</strong> 的重點是決策路由清晰度。參與者在紙上走一遍事故情境，回答「誰負責決定切換」「什麼條件觸發升級」「通訊延遲多長可接受」。這個類型成本最低、頻率應最高，適合用來發現流程漏洞與角色模糊。</p>
<p><strong>Partial failover</strong> 的重點是切換腳本與監控覆蓋。選擇一個子系統或單一 availability zone 做真實切換，驗證自動化腳本是否可執行、監控是否能在切換過程中保持可見性。這個階段常暴露的問題是：腳本假設的前提條件在 production 不成立，或監控在切換過程中產生大量 false positive。</p>
<p><strong>Full region failover</strong> 的重點是資料一致性與恢復順序。<a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">Meta 的 2021 年事故</a>顯示，跨區 failover 的最大風險在恢復順序 — 控制面與資料面共用路徑時，先恢復哪條路徑會直接決定整體恢復時間。當恢復動作本身依賴尚未恢復的控制面服務，恢復會陷入循環等待。</p>
<h2 id="演練節奏與升級">演練節奏與升級</h2>
<p>演練是按風險層級安排的循環流程。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>建議節奏</th>
          <th>升級條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tabletop</td>
          <td>季度</td>
          <td>新增關鍵依賴、組織結構變更、重大事故後</td>
      </tr>
      <tr>
          <td>partial failover</td>
          <td>半年</td>
          <td>tabletop 暴露切換路徑疑慮</td>
      </tr>
      <tr>
          <td>full region failover</td>
          <td>年度</td>
          <td>partial 驗證通過、業務需求（合規、審計）</td>
      </tr>
      <tr>
          <td>data restore drill</td>
          <td>季度</td>
          <td>備份策略變更、資料量跳升、新增資料源</td>
      </tr>
  </tbody>
</table>
<p>每輪演練產出的缺口應回寫到 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>，成為下一輪演練的驗證目標。<a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">Google 的 postmortem action item closure 治理</a>說明把事故教訓轉成有 owner 與完成條件的改進項，這個機制同樣適用於演練缺口：P0 缺口應在下個 release 週期前修復，P1 缺口應排入固定追蹤。</p>
<p><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify 的 BFCM 準備流程</a>把年度高峰前的 game day 當作 DR 演練的自然觸發點。容量模型、隔離邊界與 failover 路徑在 game day 中一起驗證，每輪暴露的缺口回寫成下一輪的準備 checklist。這種做法讓演練節奏跟業務節奏對齊，不是額外負擔。</p>
<h2 id="dr-與-chaos-的邊界">DR 與 chaos 的邊界</h2>
<p>DR 演練與 <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">chaos testing</a> 都涉及故障情境，但驗證目標不同。</p>
<p>Chaos 驗證的是系統在故障持續期間能否維持服務。它的成功條件是 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 不被破壞，停止條件是 steady state breach。chaos 實驗結束後，系統應該仍在運作。</p>
<p>DR 驗證的是系統在災難發生後能否回來。它的成功條件是恢復路徑可執行且符合 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 承諾，停止條件是恢復時間超過 RTO 或資料遺失超過 RPO。DR 演練結束時，系統經歷了一次完整的失效與恢復循環。</p>
<p>兩者的交集是 failover drill：chaos 關心切換期間的服務退化程度，DR 關心切換完成後的恢復品質。在實務上，成熟團隊會把 chaos experiment 的結果作為 DR 演練的輸入 — chaos 發現的弱點變成 DR 演練的測試案例。<a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon 的 cell boundary 與 static stability 設計</a>讓恢復可分批執行，同時服務 chaos 驗證（局部故障不擴散）與 DR 驗證（分批恢復可預測）。</p>
<h2 id="產業情境醫療系統">產業情境：醫療系統</h2>
<p>醫療系統的 DR 演練受合規（HIPAA / GDPR health data）和臨床連續性的雙重約束。演練設計需要同時滿足技術恢復目標與臨床安全要求。</p>
<p>演練排程需要跟臨床作業週期對齊。手術高峰、急診高峰與夜班交接時段都應避免做 failover 演練，因為演練造成的短暫服務中斷可能直接影響臨床決策。可執行窗口通常是週末凌晨或排定的維護時段。</p>
<p>恢復順序由臨床風險決定。EMR（電子病歷）系統優先於醫囑系統、PACS（影像系統）與行政系統。這個順序跟技術依賴不完全重疊 — 技術上 PACS 可能先恢復更快，但臨床上 EMR 的中斷風險更高。恢復順序的設計需要臨床代表參與，技術團隊單獨決定會漏掉臨床優先級。</p>
<p>Restore 驗證需要額外的 audit trail 完整性檢查。HIPAA 要求能追蹤誰在什麼時間存取了哪些病患資料，恢復後的資料若 audit trail 斷裂，即使資料本身完整也不符合合規要求。restore drill 的校驗清單需要把 audit trail 連續性納入必檢項。</p>
<p>醫療紀錄的 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 通常比一般 SaaS 更嚴格，接近零資料遺失。遺失的醫療紀錄可能直接影響用藥決策或手術判斷，RPO 設定需要對齊臨床風險而非技術方便性。</p>
<p>演練證據本身也需要合規留存。DR 演練紀錄、恢復時間量測、缺口清單與改善追蹤都是合規審計的輸入。沒有留存的演練在審計視角等同未演練。</p>
<h2 id="產業情境iot-與製造系統">產業情境：IoT 與製造系統</h2>
<p>IoT 裝置的 rollback 成本遠高於雲端服務。雲端服務的 rollback 是 deploy 前一版 container image，秒級生效；IoT 裝置的 rollback 需要 OTA（Over-the-Air）推送，受限於裝置連線狀態、頻寬、電量與儲存空間。部分裝置可能在 rollback 過程中斷線，進入新舊版本混合的不一致狀態。</p>
<p>DR 演練需要包含「裝置不在線」場景。工業場景的裝置可能在偏遠地點、離線數天到數週。DR 計畫需要回答「離線裝置重新上線後，如何安全地同步到正確版本」，以及混合版本期間的相容性處理。</p>
<p>安全關鍵系統（製造產線控制、醫療設備、車載系統）的回退約束比一般軟體更嚴格。firmware 缺陷可能造成物理傷害，rollback 後需要跑功能安全測試（IEC 61508 等級的驗證），確認回退版本在目標硬體上的行為符合安全規格。</p>
<p>A/B firmware partition 是 IoT 的 DR 基礎設計。裝置保留兩個 firmware slot（active / inactive），更新寫入 inactive slot，驗證通過後切換到新 slot。失敗時切回原 active slot，整個過程在裝置本地完成，不需要額外 OTA 推送。這個設計讓裝置的 rollback 路徑跟 <a href="/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">Amazon A2 的 static stability</a> 概念對齊 — 即使控制面（OTA server）不可用，裝置仍能用本地 slot 切換完成回退。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>DR 視角的教訓</th>
          <th>回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Meta M1</td>
          <td>控制面與資料面共用路徑時，恢復順序決定整體恢復時間</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a>、<a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a></td>
      </tr>
      <tr>
          <td>Amazon A1</td>
          <td>cell boundary 讓恢復可分批，不需要全域同步恢復</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>Stripe S1</td>
          <td>交易系統 rollback 需要同時驗證 schema 相容與冪等重播</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a>、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a></td>
      </tr>
      <tr>
          <td>Shopify H1</td>
          <td>年度高峰前的 game day 是 DR 演練的自然觸發點</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>Google G2</td>
          <td>postmortem action item 轉成下一輪 DR 演練題目</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a>、<a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5</a></td>
      </tr>
      <tr>
          <td>Netflix N1</td>
          <td><a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 定義同時作為 DR recovery complete 的判準</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>Amazon A2</td>
          <td>static stability 讓資料面在控制面失效時仍能服務，恢復路徑不依賴已故障的控制面</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>Meta M2</td>
          <td>回復工具依賴已故障的系統（BGP / DNS / 遠端存取），恢復陷入循環等待</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a>、<a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a></td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DR plan 寫在 wiki、過去 12 個月未演練</td>
          <td>回復能力不可信 — plan 與 production 可能已漂移</td>
          <td>排入下季 tabletop + partial failover 演練</td>
      </tr>
      <tr>
          <td>backup 有排程、restore 從未跑過</td>
          <td>備份完整性未知 — restore 是唯一能證明備份可用的手段</td>
          <td>安排 restore drill、量測實際 RTO</td>
      </tr>
      <tr>
          <td>failover 配置與 production 漂移</td>
          <td>failover 路徑不可靠 — 任何 infra 變更都可能讓 failover 腳本失效</td>
          <td>建 failover config diff 定期掃描</td>
      </tr>
      <tr>
          <td>RTO / RPO 是估值、不是量值</td>
          <td>恢復承諾不可信 — 未被演練量測過的數字只是猜測</td>
          <td>用 restore drill 量測實際值、更新承諾</td>
      </tr>
      <tr>
          <td>rollback 需要手動 SQL 或脫離部署流程</td>
          <td>rollback 路徑高風險 — 手動操作在壓力下容易出錯</td>
          <td>把 rollback 步驟自動化進 deploy pipeline</td>
      </tr>
      <tr>
          <td>演練缺口未回寫到 backlog</td>
          <td>演練價值流失 — 發現問題但不追蹤等同未發現</td>
          <td>每次演練產出寫入 6.21 reliability debt + owner</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a>：blue-green / region failover 實作</li>
<li><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos testing</a>：chaos 暴露的弱點變成 DR 演練題目</li>
<li><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>：migration rollback 演練</li>
<li><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a>：replay 是 DR 回復的前提</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：演練缺口回寫</li>
<li><a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 provider dependency release gate</a>：provider 變更的 rollback 實作示範</li>
<li><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血回復</a>：演練結果作為事中決策素材</li>
<li><a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6 演練與值班</a>：DR 結果回饋到團隊技能建設</li>
<li><a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15 vendor 事故</a>：多 vendor / 多區 failover 路徑</li>
</ul>
]]></content:encoded></item><item><title>Atlassian Statuspage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/</guid><description>&lt;p>Statuspage 是 Atlassian 收購整合的公開狀態頁 SaaS、承擔三個責任：對外公開服務狀態揭露（component / incident / maintenance）、subscriber notification（email / SMS / Slack / Microsoft Teams / webhook / RSS）、自有 domain + branding。是公開狀態頁的事實標準、跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie&lt;/a> 同屬 Atlassian 事故處理生態（搭配 Jira Service Management、Confluence post-mortem template）、也跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a> 等第三方 IR 平台廣泛整合。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Statuspage 的定位是 &lt;em>對外狀態頁領導品牌&lt;/em>、責任邊界是 &lt;em>把內部 incident state 翻譯成對外可讀的公告&lt;/em>、不是 IR workflow 本身。功能涵蓋 component status（operational / degraded / partial outage / major outage / under maintenance）、incident update（lifecycle + template）、scheduled maintenance（pre-announce + auto-publish + auto-resolve）、metrics chart（uptime / latency 公開圖表、來源 Datadog / Pingdom / New Relic / Library）、audience targeting（public / private / partner / per-customer 分軌）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie&lt;/a> / Confluence / Jira Service Management 是同生態 — Statuspage 接 Opsgenie alert 自動 create incident draft、incident resolve 自動 publish post-mortem 到 Confluence、JSM ticket 連結 Statuspage incident URL。enterprise polish（custom CSS / 自有 domain / multi-language / SSO admin）是賣點、defaults 也夠用、是大型 SaaS public-facing 的主流選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>建 Statuspage + 設 component / group&lt;/li>
&lt;li>寫第一個 incident update（template-driven）&lt;/li>
&lt;li>配置 subscriber notification channels&lt;/li>
&lt;li>API 自動化（從 IR 平台 push update）&lt;/li>
&lt;li>設定 custom domain + 品牌一致 UI&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑">最短路徑&lt;/h2>





&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"># 1. 註冊 Statuspage、選 plan&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 建 component（按服務拆）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 寫 test incident&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 4. 訂閱者 self-service subscribe&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Statuspage deployment 是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>Statuspage 是 Atlassian 收購整合的公開狀態頁 SaaS、承擔三個責任：對外公開服務狀態揭露（component / incident / maintenance）、subscriber notification（email / SMS / Slack / Microsoft Teams / webhook / RSS）、自有 domain + branding。是公開狀態頁的事實標準、跟 <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> 同屬 Atlassian 事故處理生態（搭配 Jira Service Management、Confluence post-mortem template）、也跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> 等第三方 IR 平台廣泛整合。</p>
<h2 id="服務定位">服務定位</h2>
<p>Statuspage 的定位是 <em>對外狀態頁領導品牌</em>、責任邊界是 <em>把內部 incident state 翻譯成對外可讀的公告</em>、不是 IR workflow 本身。功能涵蓋 component status（operational / degraded / partial outage / major outage / under maintenance）、incident update（lifecycle + template）、scheduled maintenance（pre-announce + auto-publish + auto-resolve）、metrics chart（uptime / latency 公開圖表、來源 Datadog / Pingdom / New Relic / Library）、audience targeting（public / private / partner / per-customer 分軌）。</p>
<p>跟 <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> / Confluence / Jira Service Management 是同生態 — Statuspage 接 Opsgenie alert 自動 create incident draft、incident resolve 自動 publish post-mortem 到 Confluence、JSM ticket 連結 Statuspage incident URL。enterprise polish（custom CSS / 自有 domain / multi-language / SSO admin）是賣點、defaults 也夠用、是大型 SaaS public-facing 的主流選擇。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>建 Statuspage + 設 component / group</li>
<li>寫第一個 incident update（template-driven）</li>
<li>配置 subscriber notification channels</li>
<li>API 自動化（從 IR 平台 push update）</li>
<li>設定 custom domain + 品牌一致 UI</li>
</ol>
<h2 id="最短路徑">最短路徑</h2>





<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. 註冊 Statuspage、選 plan</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. 建 component（按服務拆）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. 寫 test incident</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. 訂閱者 self-service subscribe</span></span></span></code></pre></div><h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Statuspage deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 publish update</strong>：admin / page admin / incident manager 的權限分層、incident publish 是否走 template + reviewer、API token 是否分 <em>human ops</em> 跟 <em>machine push</em> 兩條</li>
<li><strong>Component dependency 設計</strong>：component 是否對應 <em>使用者可感知的服務面</em>（不是內部 microservice）、group 是否拆得太細導致 status update 散落、dependency map 是否誇大內部架構讓對外公告失焦</li>
<li><strong>Metrics integration</strong>：uptime / latency chart 來源是否跟內部 SLO 對齊（Datadog / Pingdom / 自家 API push）、metrics 是否跟 incident state 同步（incident 開了 metrics 還綠燈 = 對外公信力下降）</li>
<li><strong>Audience targeting</strong>：public / private / partner page 是否清楚分軌、subscriber list 是否定期清理（離職者 / 失效 email / SMS bounce）、per-customer audience 是否走 SSO 控管</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">Incident Communication</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="component--group-設計">Component / group 設計</h3>
<p>子議題：</p>
<ul>
<li>Component 對應服務 / API endpoint（粒度跟使用者可感知一致、不是內部服務拓樸）</li>
<li>Group 組織多 component（按產品線 / 區域 / 客戶層）</li>
<li>Status：operational / degraded / partial outage / major outage / under maintenance</li>
<li>Component dependency：parent component 自動匯總 child status（過細會造成內部架構洩漏）</li>
</ul>
<h3 id="incident-lifecycle--subscriber">Incident lifecycle + Subscriber</h3>
<p>子議題：</p>
<ul>
<li>Investigating → Identified → Monitoring → Resolved 四段、每段都該推 update</li>
<li>Template（標準措辭、降低 incident commander 寫稿壓力、避免揭露過多內部細節）</li>
<li>Email / SMS / Slack / Microsoft Teams / webhook / RSS subscriber</li>
<li>Subscribe by component（部分訂閱、避免 noise）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="audience-specific-page">Audience-specific page</h3>
<p>子議題：public（所有人）/ private（authenticated、內部員工 / 特定客戶）/ partner（B2B 獨立 view）、per-customer / per-region status（大型 SaaS 用、避免單一 region 事故影響全球公信力）</p>
<h3 id="scheduled-maintenance">Scheduled maintenance</h3>
<p>子議題：提前公告 maintenance window、auto-publish + auto-resolve、跟 change management 流程串接、recurring maintenance 用 template</p>
<h3 id="subscription-management">Subscription management</h3>
<p>子議題：email / SMS / Slack / Microsoft Teams / webhook 多通道、bounce 清理、SMS provider 限額（高峰 incident 可能塞車）、subscriber list growth 變廣告管理目標時需 GDPR / CAN-SPAM 治理</p>
<h3 id="templates">Templates</h3>
<p>子議題：incident template（standard outage / degraded performance / scheduled maintenance）、避免每次 incident commander 重新寫稿、降低措辭風險</p>
<h3 id="ir-平台整合">IR 平台整合</h3>
<p>子議題：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> Status Pages integration、<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> Statuspage sync、<a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> incident-to-Statuspage workflow、<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> auto-publish</p>
<h3 id="api-automation">API automation</h3>
<p>子議題：從 IR 平台 push update、跟 Opsgenie alert sync、custom field、API token 分軌（human ops vs machine push）、retry / idempotency</p>
<h3 id="custom-domain--branding">Custom domain + branding</h3>
<p>子議題：status.example.com vs example.statuspage.io、custom CSS / logo、多語言、SSO trap（admin SSO 設錯導致 lock-out）</p>
<h3 id="metrics-公開">Metrics 公開</h3>
<p>子議題：uptime / response time 圖表、來源（Datadog / Pingdom / New Relic / 自家 API push）、metrics 跟 incident state 同步、避免 metrics 綠燈但 incident open</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Incident update 沒發</strong>：API token 失效 / IR 沒 trigger / template variable 漏帶</li>
<li><strong>Stale status（incident 過了還掛 active）</strong>：auto-resolve 規則沒設 / IR 平台 close 沒 sync / oncall 手動忘記 resolve</li>
<li><strong>Subscriber 沒收到</strong>：email bounce / SMS provider 限額 / Slack workspace token expired</li>
<li><strong>Component dependency map 過細</strong>：把內部 microservice 都拉成 component、對外公告失焦、攻擊面間接洩漏架構</li>
<li><strong>Subscriber list growth 變廣告管理</strong>：上萬 subscriber 後接近 marketing list、需 GDPR / CAN-SPAM 治理、定期清離職 + bounce</li>
<li><strong>Component status 跟實際不符</strong>：自動 sync 規則錯 / 手動沒更新 / metrics 來源延遲</li>
<li><strong>Custom domain 失效</strong>：DNS / SSL cert 過期、Statuspage cert auto-renew 沒 enable</li>
<li><strong>SSO trap</strong>：admin SSO 切過去後 IdP 出事、Statuspage admin 進不去、break-glass token 沒留</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預算敏感 / 小型團隊</td>
          <td><a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a> / Better Stack</td>
      </tr>
      <tr>
          <td>OSS / 自管 / 完全 control</td>
          <td>Cachet</td>
      </tr>
      <tr>
          <td>IR 平台內建 status</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>IR workflow + Status 一體</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
      </tr>
      <tr>
          <td>內部 only</td>
          <td>內部 dashboard（Grafana / Datadog）</td>
      </tr>
  </tbody>
</table>
<p>選 Statuspage 的核心訴求：<em>enterprise polish + Atlassian 生態整合（Opsgenie / JSM / Confluence）+ subscriber scale（百萬級 email/SMS）+ audience targeting 需求（partner / per-customer page）</em>。中小團隊 / 預算敏感走 Instatus / Better Stack 更划算；IR workflow + status 想一體化走 incident.io。</p>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 API reference / Custom CSS / Statuspage Connect</li>
<li>Atlassian SSO 設定細節（屬 IdP 範疇）</li>
<li>SLA 計算 / SLO dashboard（屬 observability、不屬對外狀態頁）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>Statuspage 廣泛使用</strong>：GitHub / Cloudflare / Atlassian / Slack / Discord / Datadog / Fastly / Heroku / Reddit / Roblox 等大型 SaaS 的 public-facing status communication 多為 Statuspage 託管、是 <em>對外揭露節奏跟措辭</em> 的事實標準。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub cases</a></td>
          <td>Statuspage update 與長尾事故時序</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare cases</a></td>
          <td>控制面事故的公開揭露節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">Atlassian cases</a></td>
          <td>自家 Statuspage、14 天長尾事故對外通訊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack cases</a></td>
          <td>通訊平台失效時的 status 訊息分軌</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">Discord cases</a></td>
          <td>Gateway 事故的 component 拆分</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog cases</a></td>
          <td>觀測平台失效時的 status 自我宣告</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/fastly/" data-link-title="Fastly" data-link-desc="Fastly 全球配置 push 事故時間線">Fastly cases</a></td>
          <td>全球邊緣事故的單頁公開時程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">Heroku cases</a></td>
          <td>平台型 Routing 事故的 incident 分層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/" data-link-title="Reddit" data-link-desc="Reddit Pi Day 2023 k8s 升級事故">Reddit cases</a></td>
          <td>Kubernetes 升級事故的對外揭露策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">Roblox cases</a></td>
          <td>長時間核心基礎設施事故的 incident lifecycle</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a>、<a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a>、<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
]]></content:encoded></item><item><title>AWS CloudWatch</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/</guid><description>&lt;p>CloudWatch 是 AWS 原生 observability 服務、承擔三個責任：AWS 服務內建 metrics / logs / alarms（無需配置）、跨 AWS 服務統一觀測平面、X-Ray + Container Insights + Lambda Insights 等專用擴展。設計取捨偏向「AWS 生態深度整合 + 不用第三方 vendor + 預設 turnkey」、跨雲跟成本是主要限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 AWS CLI / Console 查 CloudWatch metrics / logs / alarms&lt;/li>
&lt;li>用 CloudWatch Logs Insights 查詢結構化 logs&lt;/li>
&lt;li>配置 alarm + composite alarm + EventBridge integration&lt;/li>
&lt;li>用 X-Ray 追蹤 distributed tracing&lt;/li>
&lt;li>控制 CloudWatch cost（log ingestion / metric / API call）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-cloudwatch-跑起來">最短路徑：5 分鐘把 CloudWatch 跑起來&lt;/h2>





&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"># 1. 用 CloudWatch Agent 採集 EC2 metrics + logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: aws-cli + cloudwatch-agent.json config&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="c1"># 2. 查詢 metric&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization&lt;/span>
&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="c1"># 3. 用 Logs Insights 查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="metrics--logs--alarms-整合">Metrics / Logs / Alarms 整合&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Namespace + Dimension + Metric 三層&lt;/li>
&lt;li>Custom metric（CLI / SDK / Agent）&lt;/li>
&lt;li>Logs group + Log stream + Log event&lt;/li>
&lt;li>Alarm + Composite alarm + EventBridge rule&lt;/li>
&lt;/ul>
&lt;h3 id="logs-insights-query">Logs Insights query&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Query syntax：fields / filter / parse / stats / sort&lt;/li>
&lt;li>跟 KQL / LogQL 對照（CloudWatch 自家 syntax）&lt;/li>
&lt;li>對應指令：&lt;code>aws logs start-query&lt;/code>、&lt;code>aws logs get-query-results&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="metrics-math">Metrics Math&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>跨 metric 算術運算（rate / sum / avg）&lt;/li>
&lt;li>適合 dashboard / alarm 不直接 metric 表達的計算&lt;/li>
&lt;li>對比 PromQL：CloudWatch Math 較弱、無 label join 能力&lt;/li>
&lt;/ul>
&lt;h3 id="x-ray-tracing">X-Ray tracing&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>CloudWatch 是 AWS 原生 observability 服務、承擔三個責任：AWS 服務內建 metrics / logs / alarms（無需配置）、跨 AWS 服務統一觀測平面、X-Ray + Container Insights + Lambda Insights 等專用擴展。設計取捨偏向「AWS 生態深度整合 + 不用第三方 vendor + 預設 turnkey」、跨雲跟成本是主要限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 AWS CLI / Console 查 CloudWatch metrics / logs / alarms</li>
<li>用 CloudWatch Logs Insights 查詢結構化 logs</li>
<li>配置 alarm + composite alarm + EventBridge integration</li>
<li>用 X-Ray 追蹤 distributed tracing</li>
<li>控制 CloudWatch cost（log ingestion / metric / API call）</li>
</ol>
<h2 id="最短路徑5-分鐘把-cloudwatch-跑起來">最短路徑：5 分鐘把 CloudWatch 跑起來</h2>





<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. 用 CloudWatch Agent 採集 EC2 metrics + logs</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: aws-cli + cloudwatch-agent.json config</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="c1"># 2. 查詢 metric</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization</span>
</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="c1"># 3. 用 Logs Insights 查詢</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="metrics--logs--alarms-整合">Metrics / Logs / Alarms 整合</h3>
<p>子議題：</p>
<ul>
<li>Namespace + Dimension + Metric 三層</li>
<li>Custom metric（CLI / SDK / Agent）</li>
<li>Logs group + Log stream + Log event</li>
<li>Alarm + Composite alarm + EventBridge rule</li>
</ul>
<h3 id="logs-insights-query">Logs Insights query</h3>
<p>子議題：</p>
<ul>
<li>Query syntax：fields / filter / parse / stats / sort</li>
<li>跟 KQL / LogQL 對照（CloudWatch 自家 syntax）</li>
<li>對應指令：<code>aws logs start-query</code>、<code>aws logs get-query-results</code></li>
</ul>
<h3 id="metrics-math">Metrics Math</h3>
<p>子議題：</p>
<ul>
<li>跨 metric 算術運算（rate / sum / avg）</li>
<li>適合 dashboard / alarm 不直接 metric 表達的計算</li>
<li>對比 PromQL：CloudWatch Math 較弱、無 label join 能力</li>
</ul>
<h3 id="x-ray-tracing">X-Ray tracing</h3>
<p>子議題：</p>
<ul>
<li>各語言 X-Ray SDK</li>
<li>Sampling rule（rate-based / reservoir）</li>
<li>Service map 自動 build</li>
<li>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OpenTelemetry</a> 遷移案例</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="logs-insights-governance/">Logs Insights 查詢與日誌治理</a>：log group 設計、query syntax、retention policy、cross-account aggregation、subscription filter 與 cost governance</li>
<li><a href="alarms-composite-operations/">Alarms 與 Composite Alarms 操作實務</a>：Metric Alarm、Anomaly Detection、Composite Alarm 設計、alarm actions、missing data 處理與 cost</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="container-insights--lambda-insights">Container Insights / Lambda Insights</h3>
<p>子議題：</p>
<ul>
<li>Container Insights：EKS / ECS metrics + logs 自動採集</li>
<li>Lambda Insights：Lambda runtime metrics + cold start visibility</li>
<li>跟 Prometheus + Grafana 的 K8s 模式對照</li>
</ul>
<h3 id="cloudwatch-synthetics--rum">CloudWatch Synthetics / RUM</h3>
<p>子議題：</p>
<ul>
<li>Synthetics：canary script 定期 probe</li>
<li>RUM：前端用戶體驗</li>
<li>跟 Datadog Synthetics / RUM 對照</li>
</ul>
<h3 id="logs-lifecycle">Logs lifecycle</h3>
<p>子議題：</p>
<ul>
<li>Retention（1 day to never expire）</li>
<li>Subscription filter：把 logs 送到 Lambda / Kinesis / S3</li>
<li>Logs to S3 archive</li>
<li>對應 cost 控制</li>
</ul>
<h3 id="cost-控制">Cost 控制</h3>
<p>子議題：</p>
<ul>
<li>Logs ingestion charge（per GB）</li>
<li>Metrics storage charge（custom metrics + high-resolution）</li>
<li>API call charge（GetMetricData / Logs Insights query）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></li>
</ul>
<h3 id="cloudwatch-managed-prometheusamp">CloudWatch Managed Prometheus（AMP）</h3>
<p>子議題：</p>
<ul>
<li>AMP：AWS managed Prometheus、scrape EKS / ECS</li>
<li>跟 CloudWatch 互補（CloudWatch 是 AWS-native、AMP 是 OSS standard）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></li>
</ul>
<h3 id="aws-distro-for-opentelemetryadot">AWS Distro for OpenTelemetry（ADOT）</h3>
<p>子議題：</p>
<ul>
<li>AWS-supported OTel distribution</li>
<li>跟 X-Ray / AMP / CloudWatch 都整合</li>
<li>推薦的 OTel adoption 路徑</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="logs-insights-query-過慢">Logs Insights query 過慢</h3>
<p>操作原則：query 範圍 + 結果集大時、用 sample 縮範圍。</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"># TODO: fields @timestamp, @message | limit 100（先測 logic）</span></span></span></code></pre></div><h3 id="metric-not-found">Metric not found</h3>
<p>操作原則：metric namespace / dimension 對應錯。判讀：用 <code>aws cloudwatch list-metrics --namespace ...</code> 確認。</p>
<h3 id="alarm-沒觸發">Alarm 沒觸發</h3>
<p>操作原則：alarm period / evaluation period / datapoints 配置造成延遲或忽略。</p>
<h3 id="x-ray-trace-incomplete">X-Ray trace incomplete</h3>
<p>操作原則：sampling rule 過頭、subseg context propagation 失敗。判讀：X-Ray console 看 trace timeline。</p>
<h3 id="cost-爆">Cost 爆</h3>
<p>操作原則：log ingestion 多、custom metric 多、Logs Insights query 量大都會貢獻。判讀：Cost Explorer 看 CloudWatch service breakdown。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多雲 / 跨雲統一</td>
          <td><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> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a></td>
      </tr>
      <tr>
          <td>進階 APM 體驗</td>
          <td><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> / <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>高頻 query / 大量 log</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Loki）/ <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
      <tr>
          <td>OTel standard</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a> + ADOT / AMP</td>
      </tr>
      <tr>
          <td>GCP / Azure 生態</td>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a> / Azure Monitor</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 AWS 服務的 CloudWatch metric 名稱列表</li>
<li>CloudWatch Synthetics canary script 語法</li>
<li>Logs Insights 完整 query syntax reference</li>
<li>AWS IAM 跟 CloudWatch 的細部權限</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>X-Ray 遷出到 OTel</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>AWS Distro + EKS 觀測</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 CloudWatch 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>CloudWatch Logs / S3 archive 作為 audit evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Logs lifecycle / retention 對應資料主權限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>AWS-only 場景優先 CloudWatch</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>Chaos Mesh</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/chaos-mesh/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/chaos-mesh/</guid><description>&lt;p>Chaos Mesh 是 PingCAP 開源、CNCF incubating 的 Kubernetes-native chaos engineering 平台、承擔三個責任：CRD-driven fault injection（PodChaos / NetworkChaos / IOChaos / StressChaos）、Chaos Workflow（多步驟編排）、Chaos Dashboard 視覺化 + experiment scope 控制。設計取捨偏向「K8s-native + GitOps-friendly + multi-fault types」、適合 K8s 為主的 chaos engineering。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Chaos Mesh 到 K8s cluster&lt;/li>
&lt;li>設計 PodChaos / NetworkChaos / IOChaos experiment&lt;/li>
&lt;li>用 Chaos Workflow 編排多步驟實驗 + steady state probe&lt;/li>
&lt;li>控制 blast radius（namespace / labelSelector / mode）&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a> 對齊 chaos 實驗審批&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-chaos-mesh-跑起來">最短路徑：5 分鐘把 Chaos Mesh 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: curl -sSL https://mirrors.chaos-mesh.org/v2.7.0/install.sh | bash&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="c1"># 2. 跑第一個 PodChaos&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 寫 podchaos.yaml、kubectl apply&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: action: pod-kill / selector / mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Dashboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: kubectl port-forward svc/chaos-dashboard 2333:2333&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="crd-設計">CRD 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>PodChaos：pod-kill / pod-failure / container-kill&lt;/li>
&lt;li>NetworkChaos：delay / loss / duplicate / corrupt / partition&lt;/li>
&lt;li>IOChaos：delay / errno / mistake / attrOverride&lt;/li>
&lt;li>StressChaos：CPU / memory pressure&lt;/li>
&lt;li>對應 GitOps：Helm / Kustomize 管 experiment&lt;/li>
&lt;/ul>
&lt;h3 id="chaos-workflow">Chaos Workflow&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>多步驟 chaos 編排（serial / parallel）&lt;/li>
&lt;li>Suspend / resume 控制&lt;/li>
&lt;li>Probe（steady state validation）&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="chaos-dashboard">Chaos Dashboard&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>視覺化 experiment timeline&lt;/li>
&lt;li>Experiment archive&lt;/li>
&lt;li>Event log&lt;/li>
&lt;li>RBAC&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="blast-radius-控制">Blast radius 控制&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Chaos Mesh 是 PingCAP 開源、CNCF incubating 的 Kubernetes-native chaos engineering 平台、承擔三個責任：CRD-driven fault injection（PodChaos / NetworkChaos / IOChaos / StressChaos）、Chaos Workflow（多步驟編排）、Chaos Dashboard 視覺化 + experiment scope 控制。設計取捨偏向「K8s-native + GitOps-friendly + multi-fault types」、適合 K8s 為主的 chaos engineering。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Chaos Mesh 到 K8s cluster</li>
<li>設計 PodChaos / NetworkChaos / IOChaos experiment</li>
<li>用 Chaos Workflow 編排多步驟實驗 + steady state probe</li>
<li>控制 blast radius（namespace / labelSelector / mode）</li>
<li>跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a> 對齊 chaos 實驗審批</li>
</ol>
<h2 id="最短路徑5-分鐘把-chaos-mesh-跑起來">最短路徑：5 分鐘把 Chaos Mesh 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: curl -sSL https://mirrors.chaos-mesh.org/v2.7.0/install.sh | bash</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="c1"># 2. 跑第一個 PodChaos</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: 寫 podchaos.yaml、kubectl apply</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: action: pod-kill / selector / mode</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"># 3. Dashboard</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: kubectl port-forward svc/chaos-dashboard 2333:2333</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="crd-設計">CRD 設計</h3>
<p>子議題：</p>
<ul>
<li>PodChaos：pod-kill / pod-failure / container-kill</li>
<li>NetworkChaos：delay / loss / duplicate / corrupt / partition</li>
<li>IOChaos：delay / errno / mistake / attrOverride</li>
<li>StressChaos：CPU / memory pressure</li>
<li>對應 GitOps：Helm / Kustomize 管 experiment</li>
</ul>
<h3 id="chaos-workflow">Chaos Workflow</h3>
<p>子議題：</p>
<ul>
<li>多步驟 chaos 編排（serial / parallel）</li>
<li>Suspend / resume 控制</li>
<li>Probe（steady state validation）</li>
<li>對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
</ul>
<h3 id="chaos-dashboard">Chaos Dashboard</h3>
<p>子議題：</p>
<ul>
<li>視覺化 experiment timeline</li>
<li>Experiment archive</li>
<li>Event log</li>
<li>RBAC</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="blast-radius-控制">Blast radius 控制</h3>
<p>子議題：</p>
<ul>
<li>namespace 限制</li>
<li>labelSelector / value mode（one / all / fixed / fixed-percent / random-max-percent）</li>
<li>annotationSelector</li>
<li>Pause / resume 緊急中止</li>
</ul>
<h3 id="schedule-與-gitops">Schedule 與 GitOps</h3>
<p>子議題：</p>
<ul>
<li>Schedule CRD 定期 chaos</li>
<li>ArgoCD / Flux 整合</li>
<li>Experiment as code review</li>
</ul>
<h3 id="跟-litmuschaos--gremlin-對比">跟 LitmusChaos / Gremlin 對比</h3>
<p>子議題：</p>
<ul>
<li>Chaos Mesh：CRD-driven、PingCAP 主導</li>
<li>LitmusChaos：ChaosHub experiment / CNCF graduated</li>
<li>Gremlin：商業 SaaS、跨平台</li>
<li>選擇判讀：K8s OSS first → Chaos Mesh / Litmus；商業跨平台 → Gremlin</li>
</ul>
<h3 id="steady-state-驗證">Steady state 驗證</h3>
<p>子議題：</p>
<ul>
<li>HTTP / TCP / Pod / podHTTPChaos</li>
<li>Probe success threshold</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.13 SLO</a> 對應 burn rate</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="experiment-沒生效">Experiment 沒生效</h3>
<p>操作原則：先 <code>kubectl describe podchaos</code> 看 status、再看 webhook + RBAC。</p>
<h3 id="blast-radius-過大">Blast radius 過大</h3>
<p>操作原則：mode 設 all 或 percent 設太高、影響超出預期。預防：先 dry-run / staging 測試。</p>
<h3 id="pause-不及時">Pause 不及時</h3>
<p>操作原則：experiment running 中要 pause、不是 delete（delete 不會 cleanup state）。</p>
<h3 id="dashboard-連不上">Dashboard 連不上</h3>
<p>操作原則：service 沒暴露、RBAC 不對。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>非 K8s 環境</td>
          <td><a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a> / <a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></td>
      </tr>
      <tr>
          <td>AWS-native chaos</td>
          <td>AWS Fault Injection Service</td>
      </tr>
      <tr>
          <td>K8s + ChaosHub experiment</td>
          <td><a href="/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos</a></td>
      </tr>
      <tr>
          <td>Integration test 模擬故障</td>
          <td><a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></td>
      </tr>
      <tr>
          <td>商業 + GameDay 設計</td>
          <td><a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 CRD spec</li>
<li>Chaos Mesh internal architecture</li>
<li>各 fault type 詳細 parameter</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT</a></td>
          <td>steady state hypothesis 對應 Chaos Workflow Probe</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">Netflix：Business-Hours Guardrails</a></td>
          <td>blast radius / pause / mode 控制對應時段策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest：快取可靠性與容量驚奇</a></td>
          <td>NetworkChaos / StressChaos 模擬熱點與 cache failure mode</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 與 Release Gating</a></td>
          <td>chaos finding 對應 SLO burn rate 的回寫</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Chaos Mesh customer case</strong>：PingCAP / TiDB 客戶 Chaos Mesh 案例、CNCF Chaos Mesh adopters。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos</a>、<a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></li>
<li>下游能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a>（chaos finding 進 IR 流程）</li>
</ul>
]]></content:encoded></item><item><title>Fastly</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/</guid><description>&lt;p>Fastly 2021-06 的全球分鐘級配置 push 事故是 edge platform 的客戶配置觸發供應商 bug 的教學標竿。事件揭露了「客戶觸發供應商 bug」這類 IR 議題的特殊性、跟 Cloudflare 配置事故有對照價值。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>客戶配置觸發供應商 bug：誰負責、誰補償、誰公開&lt;/li>
&lt;li>全球 edge 分鐘級擴散：為何 edge platform 出事規模特別大&lt;/li>
&lt;li>Recovery 機制：客戶配置回退 vs 供應商 hotfix 的取捨&lt;/li>
&lt;li>通訊責任：上下游服務（Reddit、Amazon、政府網站）受影響時的 status 揭露&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2021-06&lt;/td>
 &lt;td>全球分鐘級配置 push 失效&lt;/td>
 &lt;td>客戶配置觸發、edge platform blast radius&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例清單">案例清單&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage&lt;/a>&lt;/li>
&lt;/ol>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Fastly 這個案例在講的是一個小型配置錯誤如何透過 edge 網路快速放大。讀者先看懂配置驗證、全球推送與回滾的責任，再把這類事故視為 control-plane 失誤，而不是單點節點故障。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當壞配置進入全球推送鏈時，真正關鍵的步驟是能否快速阻斷傳播，事後修補只能限縮損失範圍。當回復開始時，還要同時確認快取、路由與客戶流量是否已回到預期狀態。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否在推送前把配置驗證到足夠高的信心&lt;/li>
&lt;li>能否即時看見錯誤配置的擴散跡象&lt;/li>
&lt;li>能否把 rollback 做成高優先序動作&lt;/li>
&lt;li>能否把 global propagation 與客戶影響對齊&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Fastly 和 Cloudflare 是最接近的一組對照頁，兩者都在講 edge 網路上的配置擴散。Fastly 更適合用來看「客戶配置觸發供應商 bug」這個特殊模式，和 AWS S3 的區域控制面事故放在一起時，會更容易分辨不同層級的 blast radius。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2021-06 全球分鐘級配置 push 失效是最典型的 edge propagation 樣本。&lt;/li>
&lt;li>這類事故強調回滾速度與配置驗證必須先於全球擴散。&lt;/li>
&lt;li>客戶配置觸發供應商 bug 是 edge 平台最難處理的模式之一。&lt;/li>
&lt;li>Fastly 的樣本能和 Cloudflare、AWS S3 一起看 blast radius。&lt;/li>
&lt;li>CDN 邊緣層的壓力會把一個小錯誤迅速推成全球事件。&lt;/li>
&lt;li>rollback 與 status 通訊必須同步，否則客戶只會看到更長的黑箱。&lt;/li>
&lt;li>deploy tool misconfiguration 讓工具本身變成事故起點。&lt;/li>
&lt;li>edge runtime 的錯誤驗證不充分時，影響會直接落到全球流量。&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.fastly.com/blog/summary-of-june-8-outage">Summary of June 8 outage&lt;/a>：Fastly 2021-06 全球 outage 的官方回顧。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Fastly 2021-06 的全球分鐘級配置 push 事故是 edge platform 的客戶配置觸發供應商 bug 的教學標竿。事件揭露了「客戶觸發供應商 bug」這類 IR 議題的特殊性、跟 Cloudflare 配置事故有對照價值。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>客戶配置觸發供應商 bug：誰負責、誰補償、誰公開</li>
<li>全球 edge 分鐘級擴散：為何 edge platform 出事規模特別大</li>
<li>Recovery 機制：客戶配置回退 vs 供應商 hotfix 的取捨</li>
<li>通訊責任：上下游服務（Reddit、Amazon、政府網站）受影響時的 status 揭露</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2021-06</td>
          <td>全球分鐘級配置 push 失效</td>
          <td>客戶配置觸發、edge platform blast radius</td>
      </tr>
  </tbody>
</table>
<h2 id="案例清單">案例清單</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage</a></li>
</ul>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<ol>
<li><a href="/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage</a></li>
</ol>
<h2 id="案例定位">案例定位</h2>
<p>Fastly 這個案例在講的是一個小型配置錯誤如何透過 edge 網路快速放大。讀者先看懂配置驗證、全球推送與回滾的責任，再把這類事故視為 control-plane 失誤，而不是單點節點故障。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當壞配置進入全球推送鏈時，真正關鍵的步驟是能否快速阻斷傳播，事後修補只能限縮損失範圍。當回復開始時，還要同時確認快取、路由與客戶流量是否已回到預期狀態。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否在推送前把配置驗證到足夠高的信心</li>
<li>能否即時看見錯誤配置的擴散跡象</li>
<li>能否把 rollback 做成高優先序動作</li>
<li>能否把 global propagation 與客戶影響對齊</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Fastly 和 Cloudflare 是最接近的一組對照頁，兩者都在講 edge 網路上的配置擴散。Fastly 更適合用來看「客戶配置觸發供應商 bug」這個特殊模式，和 AWS S3 的區域控制面事故放在一起時，會更容易分辨不同層級的 blast radius。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2021-06 全球分鐘級配置 push 失效是最典型的 edge propagation 樣本。</li>
<li>這類事故強調回滾速度與配置驗證必須先於全球擴散。</li>
<li>客戶配置觸發供應商 bug 是 edge 平台最難處理的模式之一。</li>
<li>Fastly 的樣本能和 Cloudflare、AWS S3 一起看 blast radius。</li>
<li>CDN 邊緣層的壓力會把一個小錯誤迅速推成全球事件。</li>
<li>rollback 與 status 通訊必須同步，否則客戶只會看到更長的黑箱。</li>
<li>deploy tool misconfiguration 讓工具本身變成事故起點。</li>
<li>edge runtime 的錯誤驗證不充分時，影響會直接落到全球流量。</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.fastly.com/blog/summary-of-june-8-outage">Summary of June 8 outage</a>：Fastly 2021-06 全球 outage 的官方回顧。</li>
</ul>
]]></content:encoded></item><item><title>Terraform / OpenTofu</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/</guid><description>&lt;p>Terraform 是 HashiCorp 出品的 IaC 工具、承擔三個責任：declarative infrastructure 配置（HCL）、state-based reconciliation（plan → apply）、跨 provider 抽象（AWS / GCP / Azure / K8s / SaaS）。設計取捨偏向「state-driven + declarative + multi-cloud」、provider 生態最廣。2023 改 BSL 授權、社群 fork OpenTofu（Linux Foundation 託管、MPL 2.0）。&lt;/p>
&lt;p>對「跨雲基礎設施管理、團隊協作 IaC、需要 state + plan workflow」這條路徑、Terraform / OpenTofu 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 HCL config（resource / variable / output / module）&lt;/li>
&lt;li>設定 remote state（S3 + DynamoDB lock / Terraform Cloud）&lt;/li>
&lt;li>設計 module + workspace 結構&lt;/li>
&lt;li>跑 plan / apply / destroy 工作流 + GitOps&lt;/li>
&lt;li>評估 Terraform vs OpenTofu vs Pulumi vs Crossplane&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-terraform-跑起來">最短路徑：5 分鐘把 Terraform 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install hashicorp/tap/terraform &lt;span class="c1"># 或 brew install opentofu&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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="c1"># 2. 寫 main.tf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">required_providers&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"> aws&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> { source&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;hashicorp/aws&amp;#34;, version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;~&amp;gt; 5.0&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="n">provider &amp;#34;aws&amp;#34; { region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;us-east-1&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">resource &amp;#34;aws_s3_bucket&amp;#34; &amp;#34;demo&amp;#34; { bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my-tf-demo-bucket&amp;#34;&lt;/span> }&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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"># 3. init + plan + apply&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">terraform plan -out&lt;span class="o">=&lt;/span>plan.tfplan
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">terraform apply plan.tfplan&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="hcl-config-結構">HCL config 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>provider / resource / data source / variable / output / locals&lt;/li>
&lt;li>terraform block（required_version / required_providers / backend）&lt;/li>
&lt;li>Module（reusable group of resources）&lt;/li>
&lt;li>對應指令：&lt;code>terraform fmt&lt;/code>、&lt;code>terraform validate&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="state-管理">State 管理&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Local state（terraform.tfstate）：dev / 學習用&lt;/li>
&lt;li>Remote state（S3 + DynamoDB lock / GCS / Terraform Cloud / Spacelift）&lt;/li>
&lt;li>State migration（terraform state mv / rm / import）&lt;/li>
&lt;li>State sensitive data 不入 git&lt;/li>
&lt;/ul>
&lt;h3 id="plan--apply-workflow">Plan / apply workflow&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Terraform 是 HashiCorp 出品的 IaC 工具、承擔三個責任：declarative infrastructure 配置（HCL）、state-based reconciliation（plan → apply）、跨 provider 抽象（AWS / GCP / Azure / K8s / SaaS）。設計取捨偏向「state-driven + declarative + multi-cloud」、provider 生態最廣。2023 改 BSL 授權、社群 fork OpenTofu（Linux Foundation 託管、MPL 2.0）。</p>
<p>對「跨雲基礎設施管理、團隊協作 IaC、需要 state + plan workflow」這條路徑、Terraform / OpenTofu 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 HCL config（resource / variable / output / module）</li>
<li>設定 remote state（S3 + DynamoDB lock / Terraform Cloud）</li>
<li>設計 module + workspace 結構</li>
<li>跑 plan / apply / destroy 工作流 + GitOps</li>
<li>評估 Terraform vs OpenTofu vs Pulumi vs Crossplane</li>
</ol>
<h2 id="最短路徑5-分鐘把-terraform-跑起來">最短路徑：5 分鐘把 Terraform 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install hashicorp/tap/terraform   <span class="c1"># 或 brew install opentofu</span></span></span></code></pre></div>




<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="c1"># 2. 寫 main.tf
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    aws</span> <span class="o">=</span><span class="n"> { source</span> <span class="o">=</span><span class="n"> &#34;hashicorp/aws&#34;, version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 5.0&#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="n">provider &#34;aws&#34; { region</span> <span class="o">=</span> <span class="s2">&#34;us-east-1&#34;</span> }
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">resource &#34;aws_s3_bucket&#34; &#34;demo&#34; { bucket</span> <span class="o">=</span> <span class="s2">&#34;my-tf-demo-bucket&#34;</span> }</span></span></code></pre></div>




<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"># 3. init + plan + apply</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform init
</span></span><span class="line"><span class="ln">3</span><span class="cl">terraform plan -out<span class="o">=</span>plan.tfplan
</span></span><span class="line"><span class="ln">4</span><span class="cl">terraform apply plan.tfplan</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="hcl-config-結構">HCL config 結構</h3>
<p>子議題：</p>
<ul>
<li>provider / resource / data source / variable / output / locals</li>
<li>terraform block（required_version / required_providers / backend）</li>
<li>Module（reusable group of resources）</li>
<li>對應指令：<code>terraform fmt</code>、<code>terraform validate</code></li>
</ul>
<h3 id="state-管理">State 管理</h3>
<p>子議題：</p>
<ul>
<li>Local state（terraform.tfstate）：dev / 學習用</li>
<li>Remote state（S3 + DynamoDB lock / GCS / Terraform Cloud / Spacelift）</li>
<li>State migration（terraform state mv / rm / import）</li>
<li>State sensitive data 不入 git</li>
</ul>
<h3 id="plan--apply-workflow">Plan / apply workflow</h3>
<p>子議題：</p>
<ul>
<li>terraform plan -out=plan.tfplan（凍結結果）</li>
<li>terraform apply plan.tfplan</li>
<li>Auto-approve（CI / CD）vs manual approve（critical）</li>
<li>對應 GitOps：Atlantis / Terraform Cloud / Spacelift</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="module-設計">Module 設計</h3>
<p>子議題：</p>
<ul>
<li>Module input / output</li>
<li>Module composition（root module → child module）</li>
<li>Public module registry（Terraform Registry / OpenTofu Registry）</li>
<li>Version pinning</li>
<li>對應 Terraform best practice</li>
</ul>
<h3 id="workspaces-vs-directory-layout">Workspaces vs directory layout</h3>
<p>子議題：</p>
<ul>
<li>Workspaces：同 module 多 instance（dev / staging / prod）</li>
<li>Directory：每 env 一個 directory</li>
<li>Workspaces 的局限（state 同 backend、env 共享 config）</li>
<li>選擇判讀：強隔離 → directory；快切換 → workspace</li>
</ul>
<h3 id="drift-detection">Drift detection</h3>
<p>子議題：</p>
<ul>
<li>Drift = 實際 infra ≠ Terraform state</li>
<li>偵測：<code>terraform plan</code> 跑出來有 diff</li>
<li>修法：Manual import / state pull / 修改 cloud directly + plan refresh</li>
<li>對應 自動化 drift detection（Atlantis / Driftctl）</li>
</ul>
<h3 id="terraform-vs-opentofu">Terraform vs OpenTofu</h3>
<p>子議題：</p>
<ul>
<li>2023 Terraform 改 BSL：Linux Foundation fork OpenTofu</li>
<li>OpenTofu 跟 Terraform 1.5 API 相容</li>
<li>之後分歧：OpenTofu 加 state encryption、provider iteration</li>
<li>遷移路徑：替換 binary、import 既有 state</li>
</ul>
<h3 id="provider-生態">Provider 生態</h3>
<p>子議題：</p>
<ul>
<li>AWS / Azure / GCP（cloud provider）</li>
<li>Kubernetes / Helm（K8s provider）</li>
<li>SaaS：Datadog / Pagerduty / Cloudflare / GitHub</li>
<li>Community provider vs official provider 品質差距</li>
</ul>
<h3 id="跟-crossplane--pulumi-對比">跟 Crossplane / Pulumi 對比</h3>
<p>子議題：</p>
<ul>
<li>Crossplane：K8s-native IaC（用 K8s CRD 管 cloud resource）</li>
<li>Pulumi：用通用語言（TS / Python / Go / C#）寫 IaC</li>
<li>選擇判讀：純 cloud infra → Terraform / OpenTofu；K8s-heavy → Crossplane；developer-first → Pulumi</li>
</ul>
<h3 id="terraform-cloud--spacelift--atlantis">Terraform Cloud / Spacelift / Atlantis</h3>
<p>子議題：</p>
<ul>
<li>Terraform Cloud（HashiCorp managed）：remote state + run + policy</li>
<li>Spacelift / env0：商業替代</li>
<li>Atlantis：OSS Pull Request automation</li>
<li>對應 GitOps for IaC</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="state-lock-stuck">State lock stuck</h3>
<p>操作原則：DynamoDB lock 沒釋放（process killed）。判讀 + 修法：<code>terraform force-unlock &lt;lock-id&gt;</code>（小心）。</p>
<h3 id="plan-diff-過大">Plan diff 過大</h3>
<p>操作原則：drift 累積 / provider 升級 / config 改太多。判讀：先看 plan output、再決定要不要 apply。</p>
<h3 id="provider-auth-fail">Provider auth fail</h3>
<p>操作原則：AWS / GCP credentials 沒設、過期、權限不夠。判讀：<code>AWS_PROFILE</code> / IAM role / GCP ADC 配置。</p>
<h3 id="module-version-衝突">Module version 衝突</h3>
<p>操作原則：root module 跟 child module 用不同 provider version。判讀：<code>terraform providers</code> 看 version constraint。</p>
<h3 id="apply-partial-failure">Apply partial failure</h3>
<p>操作原則：apply 中某 resource 失敗、state 一致性問題。判讀：state pull 看當前、可能要 import / state rm 修。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSI-licensed Terraform</td>
          <td>OpenTofu（同模組）</td>
      </tr>
      <tr>
          <td>Imperative API</td>
          <td>Pulumi</td>
      </tr>
      <tr>
          <td>Cloud-specific（單一 cloud）</td>
          <td>CloudFormation / Azure Bicep / GCP Deployment Manager</td>
      </tr>
      <tr>
          <td>K8s-native IaC</td>
          <td>Crossplane</td>
      </tr>
      <tr>
          <td>Application config（不是 infra）</td>
          <td>Helm / Kustomize / cdk8s</td>
      </tr>
      <tr>
          <td>極小場景</td>
          <td>CLI / Cloud Shell（不用 IaC）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 HCL syntax reference</li>
<li>各 provider 完整 resource list</li>
<li>Terraform Cloud / Spacelift 商業 feature</li>
<li>Drift detection 工具細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Terraform 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>平台遷移期間舊 / 新叢集共通配置基線靠 IaC 表達、批次切流時 module 版本要凍結</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast EKS</a></td>
          <td>多團隊異質集群盤點後、用 module + workspace 把平台基線變成統一可審計的 IaC</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro EKS</a></td>
          <td>Managed EKS 後平台團隊把手動操作改成 IaC + GitOps、自動化取代手動操作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>小型 CLI / 中型單 workspace / 大型 multi-workspace + Atlantis / Spacelift 治理</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Terraform 案例</strong>：HashiCorp Cloud 大客戶案例、OpenTofu fork 後企業遷移案例、Drift detection 治理案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment platform</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>（K8s provider）</li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability</a>（IaC GitOps + release gate）</li>
</ul>
]]></content:encoded></item><item><title>8.7 失敗模式審查（Failure Mode Audit）</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/attacker-view-incident-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/attacker-view-incident-risks/</guid><description>&lt;p>本章的責任是把事故弱點判讀維持在概念上限。核心輸出是事故問題地圖、案例對照與交接條件，讓事故流程在進入 playbook 細節前先完成決策對齊。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>事故弱點盤點，是從反向壓力看事故流程是否會在分級、指揮、回復與交接上被擊穿，責任是先找出流程設計的脆弱點。&lt;/p>
&lt;p>這一頁處理的是事故主幹，不是單一 playbook。只要某個節點會讓事故擴散、延長或失去證據，弱點盤點就要先把它標出來。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀事故弱點時，先看啟動是否太慢，再看指揮與交接是否能維持同一條推進線。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>分級門檻是否晚於實際擴散節奏&lt;/li>
&lt;li>指揮鏈與責任鏈是否可回查&lt;/li>
&lt;li>containment、回復與驗證是否形成閉環&lt;/li>
&lt;li>技術時序與通報時序是否一致&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3&lt;/a>：control-plane 類事故會直接考驗回復與驗證。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台級事故常暴露指揮與交接節奏。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare&lt;/a>：edge 型事故容易放大 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與通訊壓力。&lt;/li>
&lt;/ul>
&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>事件啟動節奏晚於擴散節奏&lt;/td>
 &lt;td>分級門檻要對齊服務影響邊界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>指揮與責任&lt;/td>
 &lt;td>角色定義存在但決策鏈延遲&lt;/td>
 &lt;td>指揮鏈與責任鏈要同時可回查&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/" data-link-title="7.R7.3.11 ServiceNow 2024：企業平台入口風險" data-link-desc="企業核心平台漏洞出現時，服務流程與資料流程都需要同步收斂">ServiceNow 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>止血與回復&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a> 完成後仍缺驗證關閉&lt;/td>
 &lt;td>止血、回復、驗證要形成閉環&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/" data-link-title="7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸" data-link-desc="同一波邊界事件在後續通報階段，重點轉為會話與憑證收斂">Citrix ADC 後續事件&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交接與通訊&lt;/td>
 &lt;td>技術時序與通報時序偏移&lt;/td>
 &lt;td>交接格式要先標準化再演練&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例對照表情境---判讀---注意事項---路由章節">案例對照表（情境 -&amp;gt; 判讀 -&amp;gt; 注意事項 -&amp;gt; 路由章節）&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>分級門檻與實際衝擊脫鉤&lt;/td>
 &lt;td>先對齊啟動條件與升級條件&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級與啟動條件&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>決策會議重複但處置進度緩慢&lt;/td>
 &lt;td>指揮責任鏈可能分散&lt;/td>
 &lt;td>角色責任與交接格式要固定&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 事故指揮與角色分工&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>止血後再次出現同類事件&lt;/td>
 &lt;td>驗證關閉條件尚未完成&lt;/td>
 &lt;td>回復與驗證要同批次追蹤&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 復盤與改進追蹤&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="到實作前的最後一層">到實作前的最後一層&lt;/h2>
&lt;p>本章在概念層回答的是事故節奏、責任邊界與交接條件。當討論進入值班排班、playbook 指令、通訊模板與工具操作細節時，就代表已進入實作層。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把事故弱點判讀維持在概念上限。核心輸出是事故問題地圖、案例對照與交接條件，讓事故流程在進入 playbook 細節前先完成決策對齊。</p>
<h2 id="概念定位">概念定位</h2>
<p>事故弱點盤點，是從反向壓力看事故流程是否會在分級、指揮、回復與交接上被擊穿，責任是先找出流程設計的脆弱點。</p>
<p>這一頁處理的是事故主幹，不是單一 playbook。只要某個節點會讓事故擴散、延長或失去證據，弱點盤點就要先把它標出來。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀事故弱點時，先看啟動是否太慢，再看指揮與交接是否能維持同一條推進線。</p>
<p>重點訊號包括：</p>
<ul>
<li>分級門檻是否晚於實際擴散節奏</li>
<li>指揮鏈與責任鏈是否可回查</li>
<li>containment、回復與驗證是否形成閉環</li>
<li>技術時序與通報時序是否一致</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3</a>：control-plane 類事故會直接考驗回復與驗證。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台級事故常暴露指揮與交接節奏。</li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare</a>：edge 型事故容易放大 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與通訊壓力。</li>
</ul>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>主要問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>啟動與分級</td>
          <td>事件啟動節奏晚於擴散節奏</td>
          <td>分級門檻要對齊服務影響邊界</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></td>
      </tr>
      <tr>
          <td>指揮與責任</td>
          <td>角色定義存在但決策鏈延遲</td>
          <td>指揮鏈與責任鏈要同時可回查</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/" data-link-title="7.R7.3.11 ServiceNow 2024：企業平台入口風險" data-link-desc="企業核心平台漏洞出現時，服務流程與資料流程都需要同步收斂">ServiceNow 2024</a></td>
      </tr>
      <tr>
          <td>止血與回復</td>
          <td><a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a> 完成後仍缺驗證關閉</td>
          <td>止血、回復、驗證要形成閉環</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/" data-link-title="7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸" data-link-desc="同一波邊界事件在後續通報階段，重點轉為會話與憑證收斂">Citrix ADC 後續事件</a></td>
      </tr>
      <tr>
          <td>交接與通訊</td>
          <td>技術時序與通報時序偏移</td>
          <td>交接格式要先標準化再演練</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></td>
      </tr>
  </tbody>
</table>
<h2 id="案例對照表情境---判讀---注意事項---路由章節">案例對照表（情境 -&gt; 判讀 -&gt; 注意事項 -&gt; 路由章節）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>判讀</th>
          <th>注意事項</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事件升級頻繁但啟動延遲</td>
          <td>分級門檻與實際衝擊脫鉤</td>
          <td>先對齊啟動條件與升級條件</td>
          <td><a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級與啟動條件</a></td>
      </tr>
      <tr>
          <td>決策會議重複但處置進度緩慢</td>
          <td>指揮責任鏈可能分散</td>
          <td>角色責任與交接格式要固定</td>
          <td><a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 事故指揮與角色分工</a></td>
      </tr>
      <tr>
          <td>止血後再次出現同類事件</td>
          <td>驗證關閉條件尚未完成</td>
          <td>回復與驗證要同批次追蹤</td>
          <td><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 復盤與改進追蹤</a></td>
      </tr>
  </tbody>
</table>
<h2 id="到實作前的最後一層">到實作前的最後一層</h2>
<p>本章在概念層回答的是事故節奏、責任邊界與交接條件。當討論進入值班排班、playbook 指令、通訊模板與工具操作細節時，就代表已進入實作層。</p>
]]></content:encoded></item><item><title>0.7 錯誤定位、觀測訊號與備援切換設計</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/</guid><description>&lt;p>服務可維護性的核心原則是把失敗設計成可分類、可定位、可降級、可恢復的狀態。穩定性表示服務在正常情況下能持續運行；可觀測性與備援設計則決定失敗發生時，團隊能否快速知道發生什麼、影響誰、如何降低傷害，以及如何切換到可用路徑。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>從需求面定義錯誤分類與定位線索&lt;/li>
&lt;li>判斷哪些錯誤需要對外回應、對內記錄、對平台告警&lt;/li>
&lt;li>設計可降級、可重試、可切換的服務行為&lt;/li>
&lt;li>把錯誤定位與備援需求連到 observability、deployment 與 reliability 模組&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察錯誤設計是服務合約的一部分">【觀察】錯誤設計是服務合約的一部分&lt;/h2>
&lt;p>錯誤設計的核心問題是「失敗時系統要留下什麼線索，並給誰什麼動作」。API response、domain error、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、retry、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover&lt;/a> 都是錯誤合約的一部分。&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>錯誤分類&lt;/td>
 &lt;td>這是輸入錯誤、權限錯誤、狀態衝突、下游失敗，還是系統故障&lt;/td>
 &lt;td>error code、status、reason&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定位線索&lt;/td>
 &lt;td>工程師如何找到 request、使用者、資源、下游與版本&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、subject id、dependency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外回應&lt;/td>
 &lt;td>呼叫者能否理解下一步動作&lt;/td>
 &lt;td>stable error response、retry hint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作訊號&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 如何知道影響範圍與嚴重度&lt;/td>
 &lt;td>log、metric、alert、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>降級策略&lt;/td>
 &lt;td>主要路徑失敗時能否提供較低能力服務&lt;/td>
 &lt;td>fallback、cache、read-only、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> later&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切換策略&lt;/td>
 &lt;td>依賴或節點失效時能否轉到其他路徑&lt;/td>
 &lt;td>failover、traffic shift、draining&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是設計索引。錯誤定位與備援切換應在服務設計時討論，而非等事故後才補欄位。&lt;/p>
&lt;h2 id="判讀錯誤分類要服務呼叫者與維護者">【判讀】錯誤分類要服務呼叫者與維護者&lt;/h2>
&lt;p>錯誤分類的核心責任是讓不同角色知道下一步。呼叫者需要知道是否能修正輸入、稍後重試或停止操作；維護者需要知道錯誤來自程式規則、資料狀態、外部依賴、容量瓶頸或平台問題。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>使用者建立訂單時庫存不足，對外 response 要表達「目前狀態不允許」，對內 log 要能定位商品與庫存版本。&lt;/li>
&lt;li>付款 API &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>，對外 response 要避免承諾付款結果，對內訊號要標出 payment provider、timeout duration 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy&lt;/a>。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">Webhook&lt;/a> payload 格式錯誤，對外要回穩定錯誤碼，對內要記錄 schema version 與來源系統。&lt;/li>
&lt;/ul>
&lt;p>這類設計的陷阱是只留下自由文字錯誤。自由文字適合人快速閱讀，但分類、查詢、告警與統計需要穩定欄位。錯誤分類要同時支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、metric label 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>。&lt;/p>
&lt;h2 id="判讀定位線索要沿著-request-與事件流傳遞">【判讀】定位線索要沿著 request 與事件流傳遞&lt;/h2>
&lt;p>定位線索的核心責任是讓工程師能把一個症狀追回完整路徑。當 request 跨過 API、資料庫、cache、queue、worker、外部服務與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> 推送時，線索需要跟著邊界傳遞。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>checkout 變慢時，需要知道同一個 trace 經過 cart、payment、inventory 與 shipping 的哪一段。&lt;/li>
&lt;li>queue message 重試時，需要知道原始 request、event id、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a>、attempt count 與最後錯誤。&lt;/li>
&lt;li>即時通知漏送時，需要知道 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a>、client id、connection id、server instance 與 publish path。&lt;/li>
&lt;/ul>
&lt;p>這類設計的陷阱是每個元件各自產生無關 ID。request id、trace id、event id、subject id 與 dependency name 要有清楚用途，並在跨服務、跨 queue、跨 worker 時保留關聯。&lt;/p></description><content:encoded><![CDATA[<p>服務可維護性的核心原則是把失敗設計成可分類、可定位、可降級、可恢復的狀態。穩定性表示服務在正常情況下能持續運行；可觀測性與備援設計則決定失敗發生時，團隊能否快速知道發生什麼、影響誰、如何降低傷害，以及如何切換到可用路徑。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>從需求面定義錯誤分類與定位線索</li>
<li>判斷哪些錯誤需要對外回應、對內記錄、對平台告警</li>
<li>設計可降級、可重試、可切換的服務行為</li>
<li>把錯誤定位與備援需求連到 observability、deployment 與 reliability 模組</li>
</ol>
<hr>
<h2 id="觀察錯誤設計是服務合約的一部分">【觀察】錯誤設計是服務合約的一部分</h2>
<p>錯誤設計的核心問題是「失敗時系統要留下什麼線索，並給誰什麼動作」。API response、domain error、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、retry、<a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 與 <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a> 都是錯誤合約的一部分。</p>
<table>
  <thead>
      <tr>
          <th>設計面向</th>
          <th>要回答的問題</th>
          <th>常見產出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>錯誤分類</td>
          <td>這是輸入錯誤、權限錯誤、狀態衝突、下游失敗，還是系統故障</td>
          <td>error code、status、reason</td>
      </tr>
      <tr>
          <td>定位線索</td>
          <td>工程師如何找到 request、使用者、資源、下游與版本</td>
          <td><a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、subject id、dependency</td>
      </tr>
      <tr>
          <td>對外回應</td>
          <td>呼叫者能否理解下一步動作</td>
          <td>stable error response、retry hint</td>
      </tr>
      <tr>
          <td>操作訊號</td>
          <td><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 如何知道影響範圍與嚴重度</td>
          <td>log、metric、alert、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a></td>
      </tr>
      <tr>
          <td>降級策略</td>
          <td>主要路徑失敗時能否提供較低能力服務</td>
          <td>fallback、cache、read-only、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> later</td>
      </tr>
      <tr>
          <td>切換策略</td>
          <td>依賴或節點失效時能否轉到其他路徑</td>
          <td>failover、traffic shift、draining</td>
      </tr>
  </tbody>
</table>
<p>這張表是設計索引。錯誤定位與備援切換應在服務設計時討論，而非等事故後才補欄位。</p>
<h2 id="判讀錯誤分類要服務呼叫者與維護者">【判讀】錯誤分類要服務呼叫者與維護者</h2>
<p>錯誤分類的核心責任是讓不同角色知道下一步。呼叫者需要知道是否能修正輸入、稍後重試或停止操作；維護者需要知道錯誤來自程式規則、資料狀態、外部依賴、容量瓶頸或平台問題。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>使用者建立訂單時庫存不足，對外 response 要表達「目前狀態不允許」，對內 log 要能定位商品與庫存版本。</li>
<li>付款 API <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>，對外 response 要避免承諾付款結果，對內訊號要標出 payment provider、timeout duration 與 <a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a>。</li>
<li><a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">Webhook</a> payload 格式錯誤，對外要回穩定錯誤碼，對內要記錄 schema version 與來源系統。</li>
</ul>
<p>這類設計的陷阱是只留下自由文字錯誤。自由文字適合人快速閱讀，但分類、查詢、告警與統計需要穩定欄位。錯誤分類要同時支援 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a>、<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、metric label 與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型</a> 與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</p>
<h2 id="判讀定位線索要沿著-request-與事件流傳遞">【判讀】定位線索要沿著 request 與事件流傳遞</h2>
<p>定位線索的核心責任是讓工程師能把一個症狀追回完整路徑。當 request 跨過 API、資料庫、cache、queue、worker、外部服務與 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 推送時，線索需要跟著邊界傳遞。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>checkout 變慢時，需要知道同一個 trace 經過 cart、payment、inventory 與 shipping 的哪一段。</li>
<li>queue message 重試時，需要知道原始 request、event id、<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a>、attempt count 與最後錯誤。</li>
<li>即時通知漏送時，需要知道 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a>、client id、connection id、server instance 與 publish path。</li>
</ul>
<p>這類設計的陷阱是每個元件各自產生無關 ID。request id、trace id、event id、subject id 與 dependency name 要有清楚用途，並在跨服務、跨 queue、跨 worker 時保留關聯。</p>
<p>下一步可讀：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</p>
<h2 id="判讀對外錯誤要穩定對內錯誤要可診斷">【判讀】對外錯誤要穩定，對內錯誤要可診斷</h2>
<p>對外錯誤的核心責任是讓呼叫者知道可採取的動作；對內錯誤的核心責任是讓工程師定位原因。兩者可以關聯，但承擔不同責任。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>對外回 <code>payment_pending</code>，讓 client 顯示等待確認；對內保留 provider timeout、request payload hash、attempt count。</li>
<li>對外回 <code>rate_limited</code>，讓 client 根據 retry hint 延後；對內記錄 tenant、limit rule、current usage。</li>
<li>對外回 <code>resource_conflict</code>，讓使用者刷新狀態；對內記錄 expected version 與 actual version。</li>
</ul>
<p>這類設計的陷阱是把內部錯誤直接暴露給 client，或把對外訊息當成唯一診斷資料。對外錯誤要穩定、安全、可被產品處理；對內錯誤要保留足夠脈絡、可查詢、可關聯。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型</a>。</p>
<h2 id="判讀降級策略要依資料語意分級">【判讀】降級策略要依資料語意分級</h2>
<p>降級策略的核心問題是「主要能力失效時，哪些功能仍可提供」。降級可以是回舊資料、只讀模式、排隊稍後處理、停用非核心功能、限制流量或切換較慢但可靠的路徑。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>推薦服務失效時，首頁可以回熱門商品或預先產生的榜單。</li>
<li>Email provider 暫時失敗時，通知工作可以進 queue 稍後重試。</li>
<li>搜尋服務延遲升高時，後台可以先提供精確 ID 查詢，暫停全文搜尋。</li>
</ul>
<p>這類設計的陷阱是所有功能共用同一種失敗行為。付款、訊息、搜尋、推薦、通知與報表的失敗代價不同；降級策略要依資料是否可丟、是否可延遲、是否可重建、是否涉及金流或稽核分級。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">成本、風險與選型取捨</a>。</p>
<h2 id="判讀備援切換要先定義切換條件">【判讀】備援切換要先定義切換條件</h2>
<p>備援切換的核心責任是讓系統在依賴、節點或區域失效時轉到可用路徑。切換可以發生在 client、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>、<a href="/blog/backend/knowledge-cards/adapter/" data-link-title="Integration Adapter" data-link-desc="說明外部系統接入層如何轉換介面與隔離差異">Integration Adapter</a>、queue consumer 或資料層；每一層都需要明確條件。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>外部付款 provider 連續 timeout 後，系統暫停建立新付款並保留待確認狀態。</li>
<li>某個 service instance <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 失敗後，<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 停止送新流量並進入 draining。</li>
<li>主要搜尋 cluster 延遲過高時，後台切到只讀快照或簡化查詢。</li>
</ul>
<p>這類設計的陷阱是把 failover 想成自動且無代價。切換可能造成重複請求、順序改變、資料短暫不一致、成本上升或排障複雜度增加。切換條件、回切條件、資料一致性與告警都要一起設計。</p>
<p>下一步可讀：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a> 與 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>。</p>
<h2 id="判讀備援設計需要驗證流程">【判讀】備援設計需要驗證流程</h2>
<p>備援設計的核心完成標準是能被演練。文件中宣稱可以重試、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>、切換或回復，只代表設計意圖；可靠性驗證要證明這些路徑在接近真實條件下能運作。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>在預備環境讓 payment <a href="/blog/backend/knowledge-cards/provider-adapter/" data-link-title="Provider Adapter" data-link-desc="說明第三方服務如何被包裝成內部穩定介面">Provider Adapter</a> 回 timeout，驗證訂單狀態是否停在待確認。</li>
<li>在 <a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a> 中提高 queue lag，驗證 dashboard、alert 與 consumer 擴容決策。</li>
<li>在 <a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test</a> 中讓 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 暫時中斷，驗證 outbox、retry 與 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>。</li>
</ul>
<p>這類設計的陷阱是只測成功路徑。錯誤分類、定位線索、降級策略與 failover 都應有對應測試、演練或 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a>，否則事故發生時才會知道設計缺口。</p>
<p>下一步可讀：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入觀測與事故治理實作章節：</p>
<ol>
<li>錯誤分類是否可被查詢與統計（對外碼、對內欄位）</li>
<li>定位線索是否可跨邊界串接（request、trace、event）</li>
<li>降級與切換條件是否明確（觸發條件、回切條件）</li>
<li>演練與驗證入口是否明確（load、chaos、事故演練）</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04-observability</a></li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06-reliability</a></li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08-incident-response</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>可觀測性與備援設計要從服務需求開始。錯誤分類讓呼叫者與維護者知道下一步，定位線索讓症狀能追回路徑，對外與對內錯誤承擔不同責任，降級策略依資料語意分級，備援切換需要明確條件，可靠性驗證則確認這些設計能在失敗時運作。</p>
]]></content:encoded></item><item><title>4.8 訊號治理閉環</title><link>https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何訊號需要治理閉環：alert / metric / dashboard 是會老化的資產&lt;/li>
&lt;li>偵測缺口的來源：post-incident review、chaos test、日常 noise&lt;/li>
&lt;li>訊號生命週期：新增 → 調整 → 淘汰&lt;/li>
&lt;li>Alert 健康度量測&lt;/li>
&lt;li>Dashboard 健康度量測&lt;/li>
&lt;li>治理節奏與 ownership&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>訊號治理閉環是把事故、演練與日常使用經驗回寫到觀測系統的流程，責任是讓 alert、metric 與 dashboard 隨服務變化而更新。&lt;/p>
&lt;p>觀測資產會老化：服務拓撲會變、流量型態會變、告警接收者會離職或轉組。設定一次就不再動的 alert rule 會在數月後變成 noise 來源；建立一次就不再看的 dashboard 會累積成系統負擔。訊號治理把觀測系統當成需要持續維護的產品，而非建好就完成的基礎設施。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 的分工：4.4 處理設計（怎麼設計好的 dashboard 跟 alert），4.8 處理維運與淘汰（設計好之後怎麼讓它們持續有效）。&lt;/p>
&lt;h2 id="偵測缺口的來源">偵測缺口的來源&lt;/h2>
&lt;h3 id="post-incident-review">Post-incident review&lt;/h3>
&lt;p>每次事故的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 都可能揭露偵測缺口 — 事故發生到被偵測到的時間太長、alert 觸發了但指向錯誤的方向、或根本沒有 alert 觸發。&lt;/p>
&lt;p>偵測缺口的分類：&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>訊號缺失&lt;/td>
 &lt;td>問題存在但沒有對應的 metric 或 trace&lt;/td>
 &lt;td>新增 metric / span&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert 太晚&lt;/td>
 &lt;td>Alert 在使用者投訴後才觸發&lt;/td>
 &lt;td>調整閾值或加短窗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert 指向錯誤&lt;/td>
 &lt;td>Alert 觸發了但指向不相關的服務&lt;/td>
 &lt;td>修正 alert rule&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard 沒有對應視圖&lt;/td>
 &lt;td>事故中需要看某個維度但現有 dashboard 沒有&lt;/td>
 &lt;td>新增 panel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>關聯性斷裂&lt;/td>
 &lt;td>Log / trace / metric 無法用同一個 ID 串連&lt;/td>
 &lt;td>補 correlation field&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Post-incident review 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action items&lt;/a> 中標記為「detection gap」的項目，應該指派給觀測系統的 owner，帶明確的 metric / alert / dashboard 變更規格。&lt;/p>
&lt;h3 id="chaos-test-與演練">Chaos test 與演練&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test&lt;/a> 跟災難恢復演練會在受控條件下暴露觀測盲區。注入 dependency failure 後，觀測系統是否在預期時間內觸發 alert？Alert 是否指向正確的方向？Dashboard 是否有足夠的 panel 支援診斷？&lt;/p>
&lt;p>演練揭露的盲區跟事故揭露的盲區性質相同，但成本更低 — 在受控環境發現的缺口不會拉長真實事故的 MTTR。&lt;/p>
&lt;h3 id="日常-noise-累積">日常 noise 累積&lt;/h3>
&lt;p>Alert noise 的日常累積是漸進式的退化 — 每個月新增幾個 alert rule 但沒有淘汰舊的，noise rate 從 10% 慢慢升到 30% 再到 50%。退化的訊號是 on-call 工程師開始忽略某些 alert（先 ack 再看、或直接 resolve 不看）。&lt;/p>
&lt;h2 id="訊號生命週期">訊號生命週期&lt;/h2>
&lt;h3 id="新增">新增&lt;/h3>
&lt;p>新訊號的來源：新服務上線時的 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review&lt;/a> 檢查、post-incident review 的 detection gap、chaos test 暴露的盲區、新功能上線時的 SLI 定義。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何訊號需要治理閉環：alert / metric / dashboard 是會老化的資產</li>
<li>偵測缺口的來源：post-incident review、chaos test、日常 noise</li>
<li>訊號生命週期：新增 → 調整 → 淘汰</li>
<li>Alert 健康度量測</li>
<li>Dashboard 健康度量測</li>
<li>治理節奏與 ownership</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>訊號治理閉環是把事故、演練與日常使用經驗回寫到觀測系統的流程，責任是讓 alert、metric 與 dashboard 隨服務變化而更新。</p>
<p>觀測資產會老化：服務拓撲會變、流量型態會變、告警接收者會離職或轉組。設定一次就不再動的 alert rule 會在數月後變成 noise 來源；建立一次就不再看的 dashboard 會累積成系統負擔。訊號治理把觀測系統當成需要持續維護的產品，而非建好就完成的基礎設施。</p>
<p>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 的分工：4.4 處理設計（怎麼設計好的 dashboard 跟 alert），4.8 處理維運與淘汰（設計好之後怎麼讓它們持續有效）。</p>
<h2 id="偵測缺口的來源">偵測缺口的來源</h2>
<h3 id="post-incident-review">Post-incident review</h3>
<p>每次事故的 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 都可能揭露偵測缺口 — 事故發生到被偵測到的時間太長、alert 觸發了但指向錯誤的方向、或根本沒有 alert 觸發。</p>
<p>偵測缺口的分類：</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>典型表現</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號缺失</td>
          <td>問題存在但沒有對應的 metric 或 trace</td>
          <td>新增 metric / span</td>
      </tr>
      <tr>
          <td>Alert 太晚</td>
          <td>Alert 在使用者投訴後才觸發</td>
          <td>調整閾值或加短窗</td>
      </tr>
      <tr>
          <td>Alert 指向錯誤</td>
          <td>Alert 觸發了但指向不相關的服務</td>
          <td>修正 alert rule</td>
      </tr>
      <tr>
          <td>Dashboard 沒有對應視圖</td>
          <td>事故中需要看某個維度但現有 dashboard 沒有</td>
          <td>新增 panel</td>
      </tr>
      <tr>
          <td>關聯性斷裂</td>
          <td>Log / trace / metric 無法用同一個 ID 串連</td>
          <td>補 correlation field</td>
      </tr>
  </tbody>
</table>
<p>Post-incident review 的 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action items</a> 中標記為「detection gap」的項目，應該指派給觀測系統的 owner，帶明確的 metric / alert / dashboard 變更規格。</p>
<h3 id="chaos-test-與演練">Chaos test 與演練</h3>
<p><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test</a> 跟災難恢復演練會在受控條件下暴露觀測盲區。注入 dependency failure 後，觀測系統是否在預期時間內觸發 alert？Alert 是否指向正確的方向？Dashboard 是否有足夠的 panel 支援診斷？</p>
<p>演練揭露的盲區跟事故揭露的盲區性質相同，但成本更低 — 在受控環境發現的缺口不會拉長真實事故的 MTTR。</p>
<h3 id="日常-noise-累積">日常 noise 累積</h3>
<p>Alert noise 的日常累積是漸進式的退化 — 每個月新增幾個 alert rule 但沒有淘汰舊的，noise rate 從 10% 慢慢升到 30% 再到 50%。退化的訊號是 on-call 工程師開始忽略某些 alert（先 ack 再看、或直接 resolve 不看）。</p>
<h2 id="訊號生命週期">訊號生命週期</h2>
<h3 id="新增">新增</h3>
<p>新訊號的來源：新服務上線時的 <a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review</a> 檢查、post-incident review 的 detection gap、chaos test 暴露的盲區、新功能上線時的 SLI 定義。</p>
<p>新增訊號時要同時定義：metric / alert 的 owner、預期的 noise rate baseline、review 週期、淘汰條件。沒有 owner 跟 review 週期的訊號會在累積後變成治理負擔。</p>
<h3 id="調整">調整</h3>
<p>調整的觸發條件：alert threshold 跟當前 baseline 偏差過大、dashboard panel 的資料來源（metric name、label）已改變、alert 的 runbook link 過期、noise rate 超過團隊可接受的上限。</p>
<p>調整是訊號治理的主要日常工作。多數訊號不需要刪除，但需要隨服務演進跟著更新。</p>
<h3 id="淘汰">淘汰</h3>
<p>淘汰的觸發條件：alert rule 超過 N 天（例如 180 天）沒有觸發、dashboard 超過 N 天沒有人訪問、metric 被 recording rule 取代後原始查詢不再使用、服務已下線但 alert / dashboard 還在。</p>
<p>淘汰需要 owner 確認。自動淘汰（超過 180 天不觸發就自動刪除）風險太高 — 有些 alert 本來就是極低頻但極高價值（年度高峰才觸發的 capacity alert）。安全做法是自動標記候選淘汰，由 owner 在定期審視中決定保留或刪除。</p>
<h2 id="alert-健康度量測">Alert 健康度量測</h2>
<p>Alert 的健康度用四個指標追蹤：</p>
<p><strong>Noise rate</strong>：不需要行動的 alert / 總 alert。On-call 在 ack 時標記 actionable / noise。月度彙整。目標：&lt; 30%。</p>
<p><strong>MTTD（Mean Time to Detect）</strong>：事故開始到 alert 觸發的時間。從 incident timeline 回溯。目標：跟 SLO burn rate window 對齊（急性問題 &lt; 5 分鐘）。</p>
<p><strong>False positive rate</strong>：alert 觸發但事後確認沒有問題 / 總 alert。跟 noise rate 不同 — noise 包含 redundant alert（有問題但重複），false positive 是真的沒問題。</p>
<p><strong>Coverage</strong>：有 alert 覆蓋的 user journey / 總 user journey。未覆蓋的 user journey 代表潛在的偵測盲區。</p>
<h2 id="dashboard-健康度量測">Dashboard 健康度量測</h2>
<p>Dashboard 的健康度用三個指標追蹤：</p>
<p><strong>訪問頻率</strong>：每個 dashboard 的每週 / 每月訪問次數。Grafana 的 usage analytics 或 access log 可以提供。長期零訪問的 dashboard 是候選淘汰。</p>
<p><strong>Data freshness</strong>：Dashboard panel 是否顯示有效資料。Panel 因 metric name 改變或 label 漂移而回空值時，曲線看起來是平的零線 — 容易被誤讀成「一切正常」。定期掃描所有 panel 的 no-data 狀態。</p>
<p><strong>Owner coverage</strong>：有 owner 的 dashboard / 總 dashboard。沒有 owner 的 dashboard 沒人負責更新，退化只是時間問題。</p>
<h2 id="治理節奏">治理節奏</h2>
<p>訊號治理需要固定節奏，避免「只在事故後才補訊號、平時不管」的反應式治理。</p>
<p><strong>事故驅動（每次事故後）</strong>：Post-incident review 的 detection gap action items 在兩週內 close — 新增 / 調整的 metric、alert、dashboard 已部署並驗證。</p>
<p><strong>定期審視（每季）</strong>：</p>
<ul>
<li>Alert noise rate 報告：noise rate &gt; 30% 的 alert rule 進入調整或淘汰流程</li>
<li>Dashboard 訪問頻率報告：零訪問 dashboard 進入淘汰審視</li>
<li>Orphan alert / dashboard（owner 離職或轉組、未交接）指派新 owner</li>
</ul>
<p><strong>年度回顧</strong>：</p>
<ul>
<li>觀測覆蓋率（有 instrumentation 的服務 / 總服務）</li>
<li>SLI / SLO 的量測點跟閾值是否需要調整（業務變化、流量變化）</li>
<li>觀測成本 vs 事故成本的 ROI 評估</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀訊號治理時，先看缺口是否有來源，再看改善項是否真的關閉。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">Post-incident review</a> 是否把偵測缺口轉成具體 metric / alert / dashboard 變更</li>
<li><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test</a> 或 DR 演練是否暴露新的觀測盲區</li>
<li>Alert noise、ack time、false positive 是否有趨勢追蹤</li>
<li>Orphan dashboard 與過期 alert 是否有定期清理節奏</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 數量只增不減、無淘汰流程</li>
<li>Alert noise rate &gt; 30%、ack 後無實際動作</li>
<li>Dashboard 半年無人訪問、仍存在於主目錄</li>
<li>Post-incident review action items 大半 open &gt; 90 天</li>
<li>同類事故重複發生、觀測系統無更新</li>
<li>Alert owner 離職後無人接手、alert 成為孤兒</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert 只增不減</td>
          <td>數百個 alert rule、多數是 noise</td>
          <td>定期審視 + 自動標記候選淘汰</td>
      </tr>
      <tr>
          <td>Dashboard 全是裝飾</td>
          <td>事故時沒人打開、只有 demo 時展示</td>
          <td>追蹤訪問頻率、零訪問的淘汰</td>
      </tr>
      <tr>
          <td>Post-incident action 永遠 open</td>
          <td>Detection gap 被記錄但半年沒 close</td>
          <td>兩週 close 期限、逾期自動升級</td>
      </tr>
      <tr>
          <td>治理只在事故後才啟動</td>
          <td>平時不管、出事才補</td>
          <td>建立每季定期審視節奏</td>
      </tr>
      <tr>
          <td>Orphan alert 無人負責</td>
          <td>Owner 離職後 alert 持續觸發但沒人處理</td>
          <td>交接流程 + orphan 掃描</td>
      </tr>
      <tr>
          <td>Chaos test 不看觀測面</td>
          <td>只看服務恢復、不看 alert 跟 dashboard 表現</td>
          <td>Chaos hypothesis 包含觀測預期</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：alert / dashboard 的設計原則</li>
<li><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5 威脅建模</a>：告警失真作為觀測弱點</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>：新訊號的成本邊界</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：anomaly false positive 的淘汰</li>
<li><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16 readiness review</a>：上線前的觀測覆蓋檢查</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：ownership 矩陣</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：action items 回寫機制</li>
<li><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">8.11 閉環</a>：跨模組視角的閉環</li>
</ul>
]]></content:encoded></item><item><title>Caffeine</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/</guid><description>&lt;p>Caffeine 是 JVM 上的 high-performance process-local cache library、承擔三個責任：在 application 進程內（on-heap）提供奈秒到微秒級的 cache（沒有網路往返）、用 Window TinyLFU 淘汰演算法逼近最佳命中率（優於傳統 LRU）、提供 expire / refresh / size-based eviction 等完整 cache 語意。設計取捨偏向「最低延遲 + 最高命中率 + 嵌進 application」、是 Redis 之外的另一層 cache，不是 Redis 的替代。&lt;/p>
&lt;p>對「每個請求重複讀同一份小資料、Redis 的網路往返都嫌慢、資料可在每個實例各存一份」這條路徑、Caffeine 是 process-local 層的標準選擇。它常跟 Redis 組成兩層 cache（Caffeine L1 + Redis L2）、不是二選一。Caffeine 是 &lt;a href="https://github.com/google/guava">Guava Cache&lt;/a> 的後繼、由同作者重寫、Spring Boot 等框架的預設 local cache。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 Maven / Gradle 引入 Caffeine、寫出基本 cache&lt;/li>
&lt;li>理解 Window TinyLFU 為何命中率優於 LRU&lt;/li>
&lt;li>設計 expire-after-write / refresh-after-write / 容量上限&lt;/li>
&lt;li>判斷 process-local cache 跟 Redis 的兩層 cache 分工&lt;/li>
&lt;li>評估跨實例 invalidation 的限制與 GC 壓力&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑引入-caffeine-寫一個-cache">最短路徑：引入 Caffeine 寫一個 cache&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Maven 依賴（version 為範例、實際以 Maven Central 最新為準、最後檢查日 2026-06-16）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>com.github.ben-manes.caffeine&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>caffeine&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>3.2.4&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 基本 cache：容量上限 10000、寫入後 5 分鐘過期&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Cache&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Caffeine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">newBuilder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">maximumSize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">10_000&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">expireAfterWrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Duration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ofMinutes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">5&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getIfPresent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// loading cache：miss 時自動回源（取代手寫 cache-aside）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">LoadingCache&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">loading&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Caffeine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">newBuilder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">maximumSize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">10_000&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">refreshAfterWrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Duration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ofMinutes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 背景非同步 refresh、不阻塞讀&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// miss / refresh 時呼叫&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">loading&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Caffeine 是 library 不是 server、跑在 application JVM 內、無法 docker 獨立驗證；上面是依官方 API 的範例（API 以 &lt;a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki&lt;/a> 為準）。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="淘汰與過期策略">淘汰與過期策略&lt;/h3>
&lt;p>Caffeine 把 cache 行為拆成幾個正交的旋鈕。子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>maximumSize&lt;/code> / &lt;code>maximumWeight&lt;/code>：容量上限（筆數或加權大小）、超過用 W-TinyLFU 淘汰&lt;/li>
&lt;li>&lt;code>expireAfterWrite&lt;/code>：寫入後固定時間過期（資料新鮮度上限）&lt;/li>
&lt;li>&lt;code>expireAfterAccess&lt;/code>：最後存取後過期（淘汰冷資料）&lt;/li>
&lt;li>&lt;code>refreshAfterWrite&lt;/code>：到期後背景 refresh、舊值先服務、不阻塞（跟 expire 不同）&lt;/li>
&lt;/ul>
&lt;h3 id="window-tinylfu-淘汰">Window TinyLFU 淘汰&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Caffeine 是 JVM 上的 high-performance process-local cache library、承擔三個責任：在 application 進程內（on-heap）提供奈秒到微秒級的 cache（沒有網路往返）、用 Window TinyLFU 淘汰演算法逼近最佳命中率（優於傳統 LRU）、提供 expire / refresh / size-based eviction 等完整 cache 語意。設計取捨偏向「最低延遲 + 最高命中率 + 嵌進 application」、是 Redis 之外的另一層 cache，不是 Redis 的替代。</p>
<p>對「每個請求重複讀同一份小資料、Redis 的網路往返都嫌慢、資料可在每個實例各存一份」這條路徑、Caffeine 是 process-local 層的標準選擇。它常跟 Redis 組成兩層 cache（Caffeine L1 + Redis L2）、不是二選一。Caffeine 是 <a href="https://github.com/google/guava">Guava Cache</a> 的後繼、由同作者重寫、Spring Boot 等框架的預設 local cache。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 Maven / Gradle 引入 Caffeine、寫出基本 cache</li>
<li>理解 Window TinyLFU 為何命中率優於 LRU</li>
<li>設計 expire-after-write / refresh-after-write / 容量上限</li>
<li>判斷 process-local cache 跟 Redis 的兩層 cache 分工</li>
<li>評估跨實例 invalidation 的限制與 GC 壓力</li>
</ol>
<h2 id="最短路徑引入-caffeine-寫一個-cache">最短路徑：引入 Caffeine 寫一個 cache</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- Maven 依賴（version 為範例、實際以 Maven Central 最新為準、最後檢查日 2026-06-16）--&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">&lt;dependency&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&lt;groupId&gt;</span>com.github.ben-manes.caffeine<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&lt;artifactId&gt;</span>caffeine<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nt">&lt;version&gt;</span>3.2.4<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 基本 cache：容量上限 10000、寫入後 5 分鐘過期</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">Cache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">cache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">expireAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofMinutes</span><span class="p">(</span><span class="n">5</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="n">cache</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">cache</span><span class="p">.</span><span class="na">getIfPresent</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">// loading cache：miss 時自動回源（取代手寫 cache-aside）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="n">LoadingCache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">refreshAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofMinutes</span><span class="p">(</span><span class="n">1</span><span class="p">))</span><span class="w">   </span><span class="c1">// 背景非同步 refresh、不阻塞讀</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">(</span><span class="n">key</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">userRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">key</span><span class="p">));</span><span class="w"> </span><span class="c1">// miss / refresh 時呼叫</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="n">User</span><span class="w"> </span><span class="n">u2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">loading</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">);</span></span></span></code></pre></div><p>Caffeine 是 library 不是 server、跑在 application JVM 內、無法 docker 獨立驗證；上面是依官方 API 的範例（API 以 <a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki</a> 為準）。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="淘汰與過期策略">淘汰與過期策略</h3>
<p>Caffeine 把 cache 行為拆成幾個正交的旋鈕。子議題：</p>
<ul>
<li><code>maximumSize</code> / <code>maximumWeight</code>：容量上限（筆數或加權大小）、超過用 W-TinyLFU 淘汰</li>
<li><code>expireAfterWrite</code>：寫入後固定時間過期（資料新鮮度上限）</li>
<li><code>expireAfterAccess</code>：最後存取後過期（淘汰冷資料）</li>
<li><code>refreshAfterWrite</code>：到期後背景 refresh、舊值先服務、不阻塞（跟 expire 不同）</li>
</ul>
<h3 id="window-tinylfu-淘汰">Window TinyLFU 淘汰</h3>
<p>子議題：</p>
<ul>
<li>W-TinyLFU 結合 recency（window）+ frequency（TinyLFU sketch）、命中率逼近最佳</li>
<li>比 LRU 更抗一次性掃描污染（scan resistance）、跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis LFU</a> 的動機類似但演算法更先進</li>
<li>frequency 用 count-min sketch 近似、記憶體開銷小</li>
</ul>
<h3 id="兩層-cachel1-caffeine--l2-redis">兩層 cache（L1 Caffeine + L2 Redis）</h3>
<p>子議題：</p>
<ul>
<li>L1 Caffeine（process-local、奈秒級、每實例一份）擋掉大部分讀</li>
<li>L2 Redis（共享、毫秒級、跨實例一致）擋掉 L1 miss</li>
<li>對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency 的 hot key 兩層解法</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="跨實例-invalidation-的根本限制">跨實例 invalidation 的根本限制</h3>
<p>子議題：</p>
<ul>
<li>每個 JVM 實例有自己的 Caffeine 副本、一個實例更新不會通知其他實例</li>
<li>解法：短 TTL 容忍 stale、或用 Redis pub/sub 廣播 invalidation 訊息給各實例</li>
<li>這是 process-local cache 的固有取捨：最低延遲換來最弱的跨實例一致性</li>
</ul>
<h3 id="gc-壓力與-on-heap-vs-off-heap">GC 壓力與 on-heap vs off-heap</h3>
<p>子議題：</p>
<ul>
<li>Caffeine 預設 on-heap、大 cache 會增加 JVM heap 與 GC 壓力</li>
<li>容量上限要對齊 heap 預算、避免 cache 把 heap 撐爆觸發 full GC</li>
<li>極大 local cache 考慮 off-heap 方案（如 Ehcache 的 off-heap tier），但 Caffeine 本身專注 on-heap</li>
</ul>
<h3 id="async-與-refresh-語意">async 與 refresh 語意</h3>
<p>子議題：</p>
<ul>
<li><code>AsyncCache</code> / <code>AsyncLoadingCache</code>：回傳 CompletableFuture、不阻塞 caller</li>
<li><code>refreshAfterWrite</code>：到期後第一個讀觸發背景 refresh、舊值立即回、避免 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">stampede</a></li>
<li>refresh vs expire 的差異是「舊值能不能先服務」</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="跨實例讀到舊值">跨實例讀到舊值</h3>
<p>操作原則：process-local cache 各實例獨立、更新不傳播。判讀：縮短 TTL 容忍 stale、或加 Redis pub/sub 廣播 invalidation；強一致需求不該用 process-local cache。</p>
<h3 id="命中率低--cache-沒效果">命中率低 / cache 沒效果</h3>
<p>操作原則：先看 <code>maximumSize</code> 是否太小（working set 放不下）、再看 TTL 是否太短。判讀：用 <code>recordStats()</code> 看 hit rate / eviction count、對齊 working set。</p>
<h3 id="full-gc-頻繁">Full GC 頻繁</h3>
<p>操作原則：on-heap cache 太大撐爆 heap。判讀：降 <code>maximumSize</code> 或用 <code>maximumWeight</code> 控制實際記憶體、對齊 JVM heap 預算。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要跨實例共享 / 一致</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a>（共享 cache 層）</td>
      </tr>
      <tr>
          <td>非 JVM 語言</td>
          <td>該語言的 process-local cache（Go ristretto、Python cachetools 等）</td>
      </tr>
      <tr>
          <td>需要持久化 / durable</td>
          <td>Redis with AOF / AWS MemoryDB</td>
      </tr>
      <tr>
          <td>極大 cache 超過 heap</td>
          <td>off-heap cache（Ehcache off-heap）或外部 cache（Redis）</td>
      </tr>
      <tr>
          <td>不想管容量 / serverless</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a>（serverless、但有網路延遲）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Caffeine 完整 API（以官方 wiki 為準）</li>
<li>各 JVM 框架（Spring Cache abstraction）的整合細節</li>
<li>Guava Cache 到 Caffeine 的完整 API 對照</li>
<li>off-heap cache 方案比較</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照本模組-case-庫暫無-caffeine-specific-case">跨 vendor 對照（本模組 case 庫暫無 Caffeine-specific case）</h3>
<p>Caffeine 是 library 層元件、本 blog cache case 庫（Meta / Shopify / Netflix / Cloudflare / Tinder / Tubi / Snap）暫無 Caffeine-specific case。以下用 process-local 的角度對照。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Caffeine 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>CacheLib 是 C++ 的 process-local + flash 分層 library、Caffeine 是 JVM 的 on-heap 對應</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>TAO 有 application-tier local cache、process-local 擋掉大部分讀的思路一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a></td>
          <td>每次互動查多個 cache、process-local L1 可擋掉重複讀、降低 L2（Redis）的 RTT 壓力</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Caffeine-specific 案例</strong>：L1 Caffeine + L2 Redis 兩層 cache 的 production 命中率分層數據、跨實例 invalidation 的 Redis pub/sub 廣播實作、W-TinyLFU vs LRU 的實測命中率對照。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>deep article：<a href="/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/" data-link-title="Caffeine &#43; Redis 兩層 cache：搭起來很容易，跨實例失效才是全部的問題" data-link-desc="L1 Caffeine（process-local）&#43; L2 Redis（共享）的兩層 cache 程式碼三十行就寫完，但每個 JVM 實例有自己的 L1 副本、一個實例更新不會通知其他實例——跨實例 invalidation 才是這個架構的全部難度。本文展開兩層讀寫路徑、用 Redis pub/sub 廣播失效、5 個把 L1 stale 與 GC 寫成事故的 production 踩坑，以及哪些資料適合放 L1">Caffeine + Redis 兩層 cache 與跨實例失效</a>（L1+L2 + pub/sub 廣播失效）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>（hot key 兩層解法）、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（兩層 cache 的 L2）、<a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a>（另一端：serverless）</li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>（跨實例一致性窗口）</li>
</ul>
]]></content:encoded></item><item><title>cert-manager</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/</guid><description>&lt;p>cert-manager 是 K8s 原生的 &lt;em>certificate lifecycle automation&lt;/em> — 把「拿 cert、放 cert、定期 renew」這條從以前需要 cron + certbot + 手動 reload 的鏈、轉成 &lt;em>declarative + controller pattern&lt;/em>。使用者在 cluster 內 apply 一個 &lt;code>Certificate&lt;/code> resource、cert-manager controller 自動跟 issuer 對話、把 cert 存進 Secret、在 lifetime 2/3 點觸發 renew。它把 cert 這件事接進 K8s 控制循環、跟 Pod / Service / Ingress 同等地位的 first-class resource、層級高於 certbot 的 K8s 移植。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>cert-manager 的核心責任是 &lt;em>K8s cluster 內所有 cert 的生命週期治理&lt;/em>。從 Ingress / Gateway 對外 TLS、internal service mTLS、到 workload-level 短期 cert、都用同一套 declarative model 表達。Issuer 抽象讓底層 cert 來源可換 — 公開 cert 走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&amp;#39;s Encrypt" data-link-desc="免費 &amp;#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&amp;rsquo;s Encrypt&lt;/a> ACME、內部 cert 走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault PKI engine&lt;/a> 或 self-signed CA、企業環境走 Venafi 或 AWS PCA — 上層 &lt;code>Certificate&lt;/code> spec 不變。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &amp;#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM&lt;/a> 的差異是 &lt;em>cert 的部署面&lt;/em>：ACM 是 AWS-managed cert、只能掛在 AWS service（ELB / CloudFront / API Gateway）、私鑰永不離 AWS；cert-manager 是 K8s-native client、cert 放在 cluster 內的 Secret、可以掛任何 ingress controller 或 workload mTLS。跟 Let&amp;rsquo;s Encrypt 的關係是 &lt;em>client vs issuer&lt;/em> — cert-manager 是 ACME client、Let&amp;rsquo;s Encrypt 是 ACME server、不是替代關係。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &amp;#43; Trust Bundle、跨組織 federation">SPIRE&lt;/a> 的差異是 &lt;em>身份模型&lt;/em> — cert-manager 給 &lt;em>DNS-named cert&lt;/em>（CN / SAN 是 hostname）、SPIRE 給 &lt;em>SPIFFE ID-based workload identity&lt;/em>（&lt;code>spiffe://trust-domain/workload&lt;/code>）、兩者互補不衝突。&lt;/p></description><content:encoded><![CDATA[<p>cert-manager 是 K8s 原生的 <em>certificate lifecycle automation</em> — 把「拿 cert、放 cert、定期 renew」這條從以前需要 cron + certbot + 手動 reload 的鏈、轉成 <em>declarative + controller pattern</em>。使用者在 cluster 內 apply 一個 <code>Certificate</code> resource、cert-manager controller 自動跟 issuer 對話、把 cert 存進 Secret、在 lifetime 2/3 點觸發 renew。它把 cert 這件事接進 K8s 控制循環、跟 Pod / Service / Ingress 同等地位的 first-class resource、層級高於 certbot 的 K8s 移植。</p>
<h2 id="服務定位">服務定位</h2>
<p>cert-manager 的核心責任是 <em>K8s cluster 內所有 cert 的生命週期治理</em>。從 Ingress / Gateway 對外 TLS、internal service mTLS、到 workload-level 短期 cert、都用同一套 declarative model 表達。Issuer 抽象讓底層 cert 來源可換 — 公開 cert 走 <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> ACME、內部 cert 走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault PKI engine</a> 或 self-signed CA、企業環境走 Venafi 或 AWS PCA — 上層 <code>Certificate</code> spec 不變。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> 的差異是 <em>cert 的部署面</em>：ACM 是 AWS-managed cert、只能掛在 AWS service（ELB / CloudFront / API Gateway）、私鑰永不離 AWS；cert-manager 是 K8s-native client、cert 放在 cluster 內的 Secret、可以掛任何 ingress controller 或 workload mTLS。跟 Let&rsquo;s Encrypt 的關係是 <em>client vs issuer</em> — cert-manager 是 ACME client、Let&rsquo;s Encrypt 是 ACME server、不是替代關係。跟 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> 的差異是 <em>身份模型</em> — cert-manager 給 <em>DNS-named cert</em>（CN / SAN 是 hostname）、SPIRE 給 <em>SPIFFE ID-based workload identity</em>（<code>spiffe://trust-domain/workload</code>）、兩者互補不衝突。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>cert-manager 用 Issuer / ClusterIssuer 哪個、配什麼 issuer backend（Let&rsquo;s Encrypt / Vault PKI / self-signed / 公司 CA）</li>
<li>Challenge solver 選 HTTP01 還是 DNS01、為什麼 wildcard cert 必須用 DNF01</li>
<li>Auto-renewal 觸發點、renew 失敗的 alert 時機、跟 Ingress / Gateway API 整合的 annotation</li>
<li>何時用 cert-manager、何時改走 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">ACM</a>（雲端原生 service）或 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 cert-manager 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>Issuer 配置</strong>：是 <code>ClusterIssuer</code>（cluster-wide）還是 <code>Issuer</code>（namespace-scoped）、backend 是哪一種（acme / vault / ca / venafi）、credential（ACME private key、Vault token、CA cert）放哪、RBAC 限制誰能參考這個 issuer</li>
<li><strong>Certificate spec</strong>：<code>dnsNames</code> / <code>ipAddresses</code> 跟實際 service 一致、<code>duration</code> 跟 <code>renewBefore</code> 比例合理（renewBefore &gt;= duration / 3）、<code>secretName</code> 指向的 Secret 是不是 ingress 真的會讀的那個</li>
<li><strong>Renewal 觸發</strong>：controller log 有沒有按時觸發 renew、<code>kubectl describe certificate</code> 的 <code>Renewal Time</code> 接近沒、Challenge resource 沒有卡在 pending</li>
<li><strong>Challenge solver</strong>：HTTP01 的 ingress / Gateway 80 port 真的能被 Let&rsquo;s Encrypt 從 Internet 打到、DNS01 用的 cloud provider credential 還有效、wildcard cert 沒誤用 HTTP01</li>
</ul>
<p>四件事任一缺失、cert 就會在不知不覺中過期、production 看到 <code>x509: certificate has expired</code> 才驚覺、是 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle</a> 的典型缺口。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Issuer vs ClusterIssuer 的選擇</strong>：<code>Issuer</code> 是 namespace-scoped、只能 issue 該 namespace 的 cert、適合 <em>單 team 自管 issuer credential</em> 的場景；<code>ClusterIssuer</code> 是 cluster-wide、所有 namespace 都可以參考、適合 <em>平台 team 統一管理 issuer</em>。production 通常用 ClusterIssuer 配特定 issuer backend + RBAC 收 <code>Certificate</code> 建立權（讓 application team 只能在自己 namespace 建 Certificate、不能改 ClusterIssuer）。</p>
<p><strong>Certificate spec 設計</strong>：<code>dnsNames</code> 列出該 cert 涵蓋的 hostname（支援 wildcard <code>*.example.com</code>）、<code>ipAddresses</code> 加 IP SAN（mTLS 跨 service 常用）、<code>duration</code> 是 cert 有效期、<code>renewBefore</code> 是提前多久 renew（預設 duration 的 1/3）。短期 cert（hours-level、Vault PKI 常用）配 <code>renewBefore</code> 短、長期 cert（90 天、Let&rsquo;s Encrypt）配 <code>renewBefore</code> 30 天。<code>secretName</code> 指向 cert-manager 會寫入的 Secret、Ingress 跟 workload 從這個 Secret 讀。</p>
<p><strong>Challenge solver 的選擇</strong>：ACME issuer（Let&rsquo;s Encrypt）需要證明 <em>你控制這個 domain</em>、有兩個方法：HTTP01（在 <code>http://yourdomain/.well-known/acme-challenge/&lt;token&gt;</code> 放檔案、Let&rsquo;s Encrypt 從 Internet 來抓）跟 DNS01（在 DNS zone 加 <code>_acme-challenge.yourdomain TXT &lt;token&gt;</code> record、Let&rsquo;s Encrypt 查 DNS）。<strong>wildcard cert（<code>*.example.com</code>）必須用 DNS01</strong>、HTTP01 不支援 wildcard 因為 Let&rsquo;s Encrypt 不知道要打哪個 subdomain。HTTP01 要求 ingress controller 80 port 對 Internet 開放、DNS01 要求 cluster 有 cloud DNS API credential。</p>
<p><strong>Auto-renewal 機制</strong>：cert-manager 在 cert lifetime 達到 <code>(duration - renewBefore)</code> 時間時觸發 renew、預設約 lifetime 2/3 點。Let&rsquo;s Encrypt cert 90 天 = 60 天時開始嘗試 renew、留 30 天緩衝給 renew 失敗的重試。renew 失敗會持續重試（exponential backoff、最長 8 小時間隔）、剩下 ~7 天時 controller log 開始 ERROR 級別 alert — 監控要 hook 進這個 log 訊號、否則 cert 真的過期才知道就太晚。</p>
<p><strong>跟 Ingress 整合</strong>：Ingress resource 加 annotation <code>cert-manager.io/cluster-issuer: letsencrypt-prod</code>（或 <code>cert-manager.io/issuer:</code>）、cert-manager 看到 Ingress 的 <code>tls.hosts</code> 自動建立對應 Certificate、issue 完寫進 <code>tls.secretName</code> 指定的 Secret、ingress controller 自動 reload 用新 cert。Gateway API 的整合機制類似、用 <code>cert-manager.io/issuer</code> annotation 在 <code>Gateway</code> resource。</p>
<p><strong>CertificateRequest Approval Policy（v1.4+）</strong>：每個 Certificate 建立會產生 CertificateRequest、由 Approver 決定要不要送給 issuer。預設 cert-manager 內建 approver 自動 approve、但可以加 admission policy（Kyverno / OPA / 自寫 webhook）限制「誰能在哪個 namespace 建什麼 SAN 的 cert」— 防 internal compromise 任意 issue cert 對外冒名。production 環境通常會在 platform-level 鎖 wildcard cert、防 application team 誤建涵蓋整個 zone 的 cert。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>cert-manager</th>
          <th>AWS ACM</th>
          <th>手動 certbot / OpenSSL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>K8s controller、declarative <code>Certificate</code> resource</td>
          <td>AWS managed、Console / API request</td>
          <td>手動跑 CLI、cron 跑 renew</td>
      </tr>
      <tr>
          <td>Cert 部署面</td>
          <td>K8s Secret、任何 ingress controller / workload</td>
          <td>只能掛 ELB / CloudFront / API Gateway</td>
          <td>任何地方、但 deploy 要自己做</td>
      </tr>
      <tr>
          <td>Issuer 彈性</td>
          <td>多 issuer（ACME / Vault / Venafi / CA / AWS PCA）</td>
          <td>只能 Amazon CA</td>
          <td>任何 ACME provider、但要手寫 hook</td>
      </tr>
      <tr>
          <td>Auto-renewal</td>
          <td>內建 controller、預設 2/3 lifetime 點 renew</td>
          <td>AWS 自動 renew（DNS-validated only）</td>
          <td>自己寫 cron + reload script</td>
      </tr>
      <tr>
          <td>Wildcard 支援</td>
          <td>走 DNS01 challenge</td>
          <td>支援、需 DNS 驗證</td>
          <td>走 DNS01 hook</td>
      </tr>
      <tr>
          <td>私鑰位置</td>
          <td>K8s Secret（cluster 內、需 RBAC + etcd encryption）</td>
          <td>AWS 內、不可 export</td>
          <td>Local filesystem、要自己管</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>K8s cluster 內所有 cert、跨 issuer、internal mTLS</td>
          <td>AWS-only serving cert（ELB / CDN）</td>
          <td>非 K8s 的 server、舊系統</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — 改其他 ACME client 或回手動</td>
          <td>高 — 私鑰拿不出來、要重新 issue</td>
          <td>低 — 完全自管</td>
      </tr>
  </tbody>
</table>
<p>選 cert-manager 的核心訴求：<em>cluster 內 cert 跨 issuer 統一管理 + 自動 renew + 跟 Ingress / Gateway declarative 整合</em>。如果 cert 完全給 AWS service 用、不進 K8s workload、ACM 更簡單（不用裝 controller、AWS 自動處理）。如果是非 K8s 環境（VM、bare-metal Nginx）、certbot + cron 仍是合理選擇、不需要為了 cert 跑 K8s controller。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>DNS01 challenge 跟 cloud DNS 整合</strong>：cert-manager 支援多家 cloud DNS provider 作為 DNS01 solver — Route53、Cloud DNS（GCP）、Azure DNS、Cloudflare、ACMEDNS（自管 DNS proxy）。每個 provider 需要 <em>DNS zone 寫入 credential</em>（IAM role、service account key、API token）— 這份 credential 等於 <em>任意改該 zone DNS record 的權力</em>、blast radius 大、要走 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">least privilege</a> 限定到 specific zone + 只給 TXT record write、不要全 zone 全 record type。</p>
<p><strong>跟 Vault PKI engine 整合</strong>：cert-manager 可用 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault PKI engine</a> 作為 issuer backend — 在 cluster 內建 <code>Issuer</code> / <code>ClusterIssuer</code> type 為 <code>vault</code>、指向 Vault address + PKI mount path + auth method（Kubernetes auth / AppRole）。每張 cert 的 issue / revoke 都進 Vault audit log、跟 secret rotation 用同一套 evidence chain（呼應 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">Credential Rotation Scoped Evidence</a>）。typical 用法：short-lived workload mTLS cert（hours-level duration、minutes-level renewBefore）、靠 Vault PKI 短期 cert + cert-manager 自動換。</p>
<p><strong>跟 SPIRE 的互補</strong>：cert-manager 自動更新 cert、但 <em>cert 是給人讀的 DNS name</em>；SPIRE 自動建立 workload identity、<em>identity 是 SPIFFE ID</em>。兩者解不同問題 — cert-manager 解「Ingress / external API 的 TLS」、SPIRE 解「service A 要怎麼證明自己是 A 給 service B 看」。production 環境常 <em>並存</em>：edge cert 跟 user-facing TLS 用 cert-manager + Let&rsquo;s Encrypt、internal service mesh 用 SPIRE + SPIFFE。</p>
<p><strong>Trust bundle 管理（trust-manager）</strong>：trust-manager 是 cert-manager 姐妹專案、解決 <em>trust anchor（root CA bundle）跨 namespace 同步</em> 問題。傳統做法是每個 pod ConfigMap 各自塞 CA bundle、更新時要逐個改；trust-manager 提供 <code>Bundle</code> resource 一處定義、自動 distribute 到指定 namespace 的 ConfigMap。對應 <em>cert rotation</em> 跟 <em>CA rotation</em> 是兩條獨立 chain、後者是 trust-manager 的領域。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Challenge 卡在 pending</strong>：HTTP01 卡 = ingress 80 port 沒對 Internet、firewall / NLB 沒開、redirect 80→443 把 challenge 也轉了；DNS01 卡 = DNS provider credential 過期、IAM 沒 zone write 權、<code>_acme-challenge</code> record 沒寫進去 — <code>kubectl describe challenge</code> 看 reason</li>
<li><strong>Wildcard cert 用 HTTP01</strong>：申請失敗 + log 寫 &ldquo;wildcard not supported with HTTP-01&rdquo; — 改 DNS01 solver</li>
<li><strong>renewBefore 太短</strong>：renew 失敗只剩幾天才 alert、實際過期前來不及處理 — <code>renewBefore</code> 至少 duration / 3、production cert 給 30 天</li>
<li><strong>Secret 沒被 ingress 讀到</strong>：Certificate 已 Ready 但 ingress 還用舊 cert — ingress <code>tls.secretName</code> 拼錯、ingress controller 沒 reload、TLS handshake 用的 SNI 沒匹配</li>
<li><strong>ACME rate limit 撞牆</strong>：<a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt rate limit</a> 每週同 domain 50 cert / 同 account 300 pending — 反覆建錯 Certificate 重 issue 會撞、staging environment 用 <code>letsencrypt-staging</code> issuer 測過再上 prod</li>
<li><strong>ClusterIssuer 被 application team 誤改</strong>：沒設 RBAC、任何 namespace 都能 patch ClusterIssuer — 用 admission policy 鎖 ClusterIssuer 變更權給 platform team</li>
<li><strong>Approval Policy 缺失</strong>：任何 namespace 能建 wildcard cert、internal compromise 拿到 K8s API token 就能 issue 假冒 cert — 上 CertificateRequest Approval Policy + Kyverno / OPA rule</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only serving cert（ELB / CloudFront）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a></td>
      </tr>
      <tr>
          <td>非 K8s 環境（VM、bare-metal）的 ACME cert</td>
          <td>certbot / acme.sh / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> 直接用</td>
      </tr>
      <tr>
          <td>Workload identity（不是 DNS-named cert）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（SPIFFE-based）</td>
      </tr>
      <tr>
          <td>大量短期 internal cert + 完整 PKI 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault PKI engine</a>（可配 cert-manager 為 client）</td>
      </tr>
      <tr>
          <td>公司既有 enterprise CA（Venafi / DigiCert）</td>
          <td>cert-manager + Venafi issuer / 商用 issuer plugin</td>
      </tr>
      <tr>
          <td>全公司 cert rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>cert-manager Helm chart 的所有 value 細節跟版本相容性矩陣</li>
<li>每個 issuer backend 的完整 schema（acme / vault / venafi / ca / selfSigned）</li>
<li>Gateway API 跟 Ingress API 的 cert-manager annotation 完整對照</li>
<li>ACME RFC 8555 protocol 細節（HTTP01 / DNS01 / TLS-ALPN-01 challenge mechanism）</li>
<li>trust-manager 的 Bundle source 種類（inMemory / secret / configMap / defaultPackage）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>cert-manager 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 cert-manager 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle (section)</a></td>
          <td>cert-manager 是 cert lifecycle automation 的具體實作 — auto-renewal + Challenge solver + Approval Policy 是 lifecycle 治理三層機制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">Credential Rotation Scoped Evidence (section)</a></td>
          <td>cert-manager 的 renewal 自動但 <em>revocation 流程不自動</em> — 舊 cert 失效後 fleet 層級 trust bundle update 是另一條 chain、走 trust-manager</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — cert 更新後 session 仍可能延續、cert-manager 只管 cert lifecycle、session invalidation 是另一層責任、不要把 cert rotation 當 session 失效手段</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>、<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a>（ACME issuer）、<a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a>（AWS-managed cert）、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（PKI engine 作為 issuer backend）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（cert 過期 / mis-issue 事件如何 routing）</li>
<li>官方：<a href="https://cert-manager.io/docs/">cert-manager Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Syft + Grype</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/syft-grype/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/syft-grype/</guid><description>&lt;p>Syft 跟 Grype 是 Anchore 開源的 &lt;em>姐妹工具&lt;/em>（Apache 2.0、免費）、各做一件事、用 pipe 串接成 &lt;em>SBOM-first&lt;/em> 的 supply chain scan 鏈：&lt;strong>Syft&lt;/strong> 掃 container image / 檔案系統 / 目錄、產出標準 SBOM（CycloneDX 1.5+ / SPDX 2.3 / SyftJSON）；&lt;strong>Grype&lt;/strong> 吃 SBOM 或直接 scan target、比對 Grype-DB 回報 CVE。設計哲學是 Unix philosophy — &lt;code>syft image:tag -o cyclonedx-json | grype&lt;/code> 等價於 &lt;code>grype image:tag&lt;/code>、但中間的 SBOM 是 &lt;em>正式 artifact&lt;/em>、可以單獨簽章、單獨保存、單獨給下游消費。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 全包式設計不同、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 商業 SaaS 路線也不同。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Syft + Grype 的核心定位是 &lt;em>SBOM-first 的 OSS supply chain scan tool chain&lt;/em>。SBOM 不是中間產物、是 &lt;em>正式可簽章 artifact&lt;/em>：Syft 產 SBOM 後通常用 &lt;a href="https://docs.sigstore.dev/">Sigstore cosign&lt;/a> &lt;code>attest --predicate sbom.cdx.json&lt;/code> 把 SBOM 簽進 image OCI metadata、跟 image 一起發布；下游團隊 / 客戶 / scan pipeline 拿 &lt;em>trusted SBOM&lt;/em> 跑 Grype、不需要重新 scan image。對 &lt;em>air-gapped 環境&lt;/em>、&lt;em>multi-team handoff&lt;/em>、&lt;em>合規場景&lt;/em>（EO 14028 / FedRAMP 要求交付 CycloneDX 或 SPDX）特別合適。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 的差異是 &lt;em>分工 vs 全包&lt;/em>：Trivy 一個 binary 把 SBOM 生成 + vuln scan + IaC + secret + license 都做了；Syft + Grype 拆兩個工具、SBOM 互通流程適合、團隊偏好 Unix philosophy 選這條。功能覆蓋面 Trivy 略廣（含 IaC / secret scan）、Syft 的 SBOM 格式互通性是 OSS reference implementation。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 的差異更直接：Snyk 商業 SaaS、覆蓋廣（SAST / IaC / CSPM / Reachability）、有 dashboard 跟 fix PR；Syft + Grype 純 CLI、OSS 免費、聚焦 SBOM + vuln 兩件事、沒 server / 沒 dashboard、要 dashboard 走商業 Anchore Enterprise 或自接 JSON 到 Elasticsearch / Grafana。&lt;/p></description><content:encoded><![CDATA[<p>Syft 跟 Grype 是 Anchore 開源的 <em>姐妹工具</em>（Apache 2.0、免費）、各做一件事、用 pipe 串接成 <em>SBOM-first</em> 的 supply chain scan 鏈：<strong>Syft</strong> 掃 container image / 檔案系統 / 目錄、產出標準 SBOM（CycloneDX 1.5+ / SPDX 2.3 / SyftJSON）；<strong>Grype</strong> 吃 SBOM 或直接 scan target、比對 Grype-DB 回報 CVE。設計哲學是 Unix philosophy — <code>syft image:tag -o cyclonedx-json | grype</code> 等價於 <code>grype image:tag</code>、但中間的 SBOM 是 <em>正式 artifact</em>、可以單獨簽章、單獨保存、單獨給下游消費。跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 全包式設計不同、跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 商業 SaaS 路線也不同。</p>
<h2 id="服務定位">服務定位</h2>
<p>Syft + Grype 的核心定位是 <em>SBOM-first 的 OSS supply chain scan tool chain</em>。SBOM 不是中間產物、是 <em>正式可簽章 artifact</em>：Syft 產 SBOM 後通常用 <a href="https://docs.sigstore.dev/">Sigstore cosign</a> <code>attest --predicate sbom.cdx.json</code> 把 SBOM 簽進 image OCI metadata、跟 image 一起發布；下游團隊 / 客戶 / scan pipeline 拿 <em>trusted SBOM</em> 跑 Grype、不需要重新 scan image。對 <em>air-gapped 環境</em>、<em>multi-team handoff</em>、<em>合規場景</em>（EO 14028 / FedRAMP 要求交付 CycloneDX 或 SPDX）特別合適。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 的差異是 <em>分工 vs 全包</em>：Trivy 一個 binary 把 SBOM 生成 + vuln scan + IaC + secret + license 都做了；Syft + Grype 拆兩個工具、SBOM 互通流程適合、團隊偏好 Unix philosophy 選這條。功能覆蓋面 Trivy 略廣（含 IaC / secret scan）、Syft 的 SBOM 格式互通性是 OSS reference implementation。跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 的差異更直接：Snyk 商業 SaaS、覆蓋廣（SAST / IaC / CSPM / Reachability）、有 dashboard 跟 fix PR；Syft + Grype 純 CLI、OSS 免費、聚焦 SBOM + vuln 兩件事、沒 server / 沒 dashboard、要 dashboard 走商業 Anchore Enterprise 或自接 JSON 到 Elasticsearch / Grafana。</p>
<p>關鍵 first-class concept：<strong>Source</strong>（OCI image / OCI archive / Docker daemon / dir / file / 既有 SBOM）、<strong>Catalog</strong>（Syft 內部 package inventory 結構）、<strong>Package</strong>、<strong>Vulnerability</strong>、<strong>Match</strong>（Grype 的 package ↔ CVE 配對）、<strong>Match Configuration</strong>（<code>grype.yaml</code> 設 severity gate / 比對策略）、<strong>Vulnerability DB</strong>（Grype-DB、Anchore 聚合 NVD + GHSA + 各 distro secdb）、<strong>Ignore Rule</strong>（CVE 例外、強制帶 expiration）。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Syft 跟 Grype 各自的責任邊界、為什麼拆兩個工具比合一個工具好（SBOM 互通、attestation、air-gapped）</li>
<li>SBOM 格式（CycloneDX / SPDX / SyftJSON）的選擇、跟合規要求對應</li>
<li>Grype Match Configuration 跟 Ignore Rule 怎麼設、CI fail 條件怎麼定</li>
<li>何時改走 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 全包式、何時走 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 商業 SaaS</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Syft + Grype 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>SBOM 格式跟保存</strong>：產出格式是否符合合規（多數 EO 14028 / FedRAMP 場景要 CycloneDX 或 SPDX、不是 SyftJSON）、SBOM 是否簽章（cosign attest）、是否集中保存（OCI registry 旁邊 / artifact store）、是否有 <em>baseline diff</em>（image 升級前後依賴變化）</li>
<li><strong>Grype DB 更新</strong>：DB 是否每日同步、air-gapped 場景是否 mirror 到內部 registry（Grype DB 是 OCI artifact、可 <code>oras pull</code> 鏡像）、DB version 是否進 SBOM scan record（重現性）</li>
<li><strong>Match Configuration</strong>：<code>grype.yaml</code> 的 severity gate（CI fail 條件、通常 high / critical fail）、<code>only-fixed: true</code> 是否開（只報有 patch 的 CVE）、<code>add-cpes-if-none: true</code> 對 binary-only package 行為</li>
<li><strong>Ignore Rule 治理</strong>：例外清單是否帶 <em>expiration</em>、<code>reason</code> 欄位是否填 ticket / decision 連結、quarterly review 機制、過期自動回到 fail 狀態</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Syft 用法跟 Source 種類</strong>：<code>syft &lt;source&gt; -o &lt;format&gt;</code> 是核心 — source 可以是 OCI image（<code>registry/image:tag</code>）、OCI archive（<code>oci-archive:image.tar</code>）、Docker daemon（<code>docker:image:tag</code>）、目錄（<code>dir:./</code>）、單一檔案、甚至既有 SBOM（<code>sbom:./prev.cdx.json</code>、用來 <em>轉格式</em>）。format 包括 <code>cyclonedx-json</code> / <code>cyclonedx-xml</code> / <code>spdx-json</code> / <code>spdx-tag-value</code> / <code>syft-json</code> / <code>table</code>。production 通常產 <em>cyclonedx-json</em>（合規要求最常見）+ 保留 <em>syft-json</em>（Syft 自家最完整、未來 round-trip 用）。</p>
<p><strong>Package detector 廣度</strong>：Syft 自動偵測 OS package（apk / dpkg / rpm）+ 語言 dependency（npm / pip / gem / go module / cargo / maven / gradle / nuget / composer / hex / conan / swift / dart 等）+ binary analysis（Go binary 內 embedded module、Rust binary metadata、Java jar / war / ear nested）。對 <em>static binary</em> / <em>FAT image</em> 的支援是 Syft 的強項、比多數 SBOM tool 廣。但 <em>runtime-only dependency</em>（dlopen / dynamic load）SBOM 看不到、要靠 runtime workload protection（Falco / Cilium Tetragon 類工具、見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">7 後續候選 vendor 清單</a>）補。</p>
<p><strong>Grype 用法</strong>：<code>grype &lt;source&gt;</code> 或 <code>grype sbom:./image.cdx.json</code>。輸出 <code>table</code> / <code>json</code> / <code>cyclonedx-json</code>（CycloneDX VEX 格式）/ <code>sarif</code>（GitHub code scanning）/ <code>template</code>（Go template 自訂）。production CI 通常 <code>--output sarif</code> 上傳 GitHub code scanning + <code>--output json</code> 進內部 SIEM。<code>grype sbom:./prev.cdx.json</code> 模式是 <em>SBOM-only scan</em>、不碰 image — 適合 <em>下游團隊拿 SBOM 持續 monitor</em>、原始 image 已經 frozen 或不可達。</p>
<p><strong>Match Configuration（<code>grype.yaml</code>）</strong>：核心欄位包括 <code>fail-on-severity: high</code>（CI gate）、<code>only-fixed: true</code>（只回報有 fix 可用的 CVE、避免 noise）、<code>ignore</code> list（個別 CVE 例外）、<code>match</code> strategy（如何把 package CPE / PURL 對應到 CVE、預設策略對 90% 場景夠用、特殊 binary 場景才調）。所有設定走版控、<code>grype.yaml</code> 跟程式碼一起 review、避免 console 改。</p>
<p><strong>Ignore Rule 治理</strong>：<code>grype.yaml</code> 的 <code>ignore</code> entry 結構：<code>vulnerability</code> + <code>reason</code> + <code>expiration</code>（YYYY-MM-DD）+ optional <code>package.name</code> / <code>fix-state</code>。Anchore 設計 <em>沒有「永久 ignore」</em>、必須帶 expiration — 強制 quarterly review、避免「五年前 ignore 的 CVE 早被 fix 了還在清單裡」。reason 欄位填 ticket 編號或 ADR link、給未來的人 context。</p>
<p><strong>Cosign attest SBOM</strong>：<code>syft image:tag -o cyclonedx-json &gt; sbom.cdx.json &amp;&amp; cosign attest --predicate sbom.cdx.json --type cyclonedx --key cosign.key image:tag</code> — SBOM 被簽進 image 的 OCI signature manifest、下游 <code>cosign verify-attestation --type cyclonedx ...</code> 拿到 <em>cryptographically signed SBOM</em>。這把 SBOM 從「可被竄改的 JSON 檔」升級到 <em>trusted artifact</em>、是 <a href="https://slsa.dev/">SLSA L3+</a> provenance 的基礎。</p>
<p><strong>SLSA / SPDX 流程整合</strong>：Syft SBOM 是 build 階段產物、跟 SLSA provenance（誰 build 的、用什麼 builder、source commit 是什麼）併存、不互斥 — SBOM 答「裡面有什麼」、provenance 答「怎麼 build 的」。完整 supply chain trust 需要兩者 + cosign signature。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Syft + Grype</th>
          <th>Trivy</th>
          <th>Snyk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工具拆分</td>
          <td>兩個（Unix philosophy）</td>
          <td>一個（all-in-one binary）</td>
          <td>SaaS + CLI（多模組）</td>
      </tr>
      <tr>
          <td>授權</td>
          <td>OSS Apache 2.0</td>
          <td>OSS Apache 2.0</td>
          <td>商業（freemium、付費才解鎖完整）</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>CLI、無 server</td>
          <td>CLI、無 server</td>
          <td>SaaS dashboard + CLI</td>
      </tr>
      <tr>
          <td>SBOM 格式</td>
          <td>CycloneDX 1.5+ / SPDX 2.3 / SyftJSON（reference 實作）</td>
          <td>CycloneDX / SPDX</td>
          <td>CycloneDX / SPDX（次要、scan 為主）</td>
      </tr>
      <tr>
          <td>Vuln 資料源</td>
          <td>Grype-DB（NVD + GHSA + 各 distro secdb 聚合）</td>
          <td>Trivy-DB（類似來源 + Aqua 加值）</td>
          <td>Snyk Intel（自家 research、含 reachability）</td>
      </tr>
      <tr>
          <td>額外掃描</td>
          <td>無（聚焦 SBOM + vuln）</td>
          <td>IaC / secret / license / k8s misconfig</td>
          <td>SAST / IaC / container / IaC / Open Source / Code</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>無（Anchore Enterprise 商業才有）</td>
          <td>無（Aqua 商業才有）</td>
          <td>內建 SaaS dashboard</td>
      </tr>
      <tr>
          <td>Air-gapped</td>
          <td>強 — Grype DB 是 OCI artifact、可 mirror</td>
          <td>強 — Trivy DB OCI artifact</td>
          <td>弱 — SaaS-only 為主（自管 server 是 Enterprise）</td>
      </tr>
      <tr>
          <td>Reachability</td>
          <td>無</td>
          <td>無</td>
          <td>有（Java / JS）</td>
      </tr>
      <tr>
          <td>Fix PR 自動化</td>
          <td>無</td>
          <td>無</td>
          <td>有（auto PR、Renovate-like）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>OSS 偏好、SBOM 互通流程、air-gapped、Unix tool chain</td>
          <td>OSS 偏好、單一工具想包多事、k8s misconfig 也要</td>
          <td>商業 SaaS、需 dashboard / fix workflow / reachability</td>
      </tr>
  </tbody>
</table>
<p>選 Syft + Grype 的核心訴求：<em>要正式 SBOM 作為交付 artifact</em>（合規 / 多 team handoff）+ <em>偏好 OSS Unix philosophy</em>（兩個工具各做一件事、容易整合自家 pipeline）+ 不需要 SaaS dashboard（自家 SIEM / Grafana 已經有）。需要 IaC scan 一起做、看一下 Trivy 是不是更省整合成本；需要 fix workflow 跟 reachability、商業預算足、走 Snyk。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>SBOM attestation 完整鏈</strong>：build pipeline 順序通常是 — build image → <code>syft image -o cyclonedx-json &gt; sbom.cdx.json</code> → <code>cosign sign image</code> → <code>cosign attest --predicate sbom.cdx.json --type cyclonedx image</code> → push。下游 admission controller（Kyverno / Gatekeeper / Sigstore policy-controller）<code>verify-attestation</code> 拿 trusted SBOM、再 Grype scan、policy 決定是否允許 deploy。這條鏈把 SBOM 從 <em>文件</em> 升級成 <em>deploy gate</em>。</p>
<p><strong>Grype DB air-gapped sync</strong>：Grype DB 是 OCI artifact（<code>ghcr.io/anchore/grype/listing.json</code> + <code>db.tar.gz</code>）、<code>oras pull</code> 或 <code>grype db update</code> 取得。air-gapped 場景：DMZ 跑 <code>grype db update --skip-listing-content-check</code>、把 <code>~/.cache/grype/db/</code> 整個 sync 到內部 mirror registry、內部 grype 透過 <code>GRYPE_DB_UPDATE_URL</code> 指到內部 listing。DB 版本進 scan record、確保 <em>相同 SBOM + 相同 DB = 相同結果</em>（可重現）。</p>
<p><strong>Custom matcher / Ignore Rule 細部</strong>：Grype 預設 matcher 對 90% 場景夠、但 <em>Go binary</em>、<em>static-linked binary</em>、<em>custom C++ build</em> 可能需要 <code>add-cpes-if-none: true</code> 強制配對 CPE。Ignore Rule 支援 <code>vex-status</code> 欄位（accepted / under-investigation / fixed / not-affected）對齊 CycloneDX VEX 標準、輸出 VEX-enriched SBOM 給下游 / 客戶。</p>
<p><strong>Anchore Enterprise 商業整合</strong>：OSS Syft + Grype 不夠時、Anchore Enterprise 加：policy engine（GraphQL 寫複雜 policy）、dashboard、RBAC、SLA-backed support、跟 Kubernetes admission integration、跟 Jira / ServiceNow ticket 自動建單。OSS 是 90% 場景的起點、Enterprise 解的是 <em>policy + workflow</em> 而非 <em>scan ability</em>。</p>
<p><strong>SBOM diff（baseline 比對）</strong>：<code>syft</code> 自己沒內建 diff、但 <code>cyclonedx-cli diff</code> 或自家 script 可以比對 <em>image v1 SBOM</em> vs <em>image v2 SBOM</em>、找出新增 / 移除 / 升級的 package。用途：<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ backdoor</a> 之類「相同 version 但被植入後門」事件、單靠 SBOM 看不出來、但 <em>baseline + behavior anomaly</em> 雙軌可以提早警示。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Syft scan 找不到 package</strong>：image 是 <code>FROM scratch</code> 或 distroless、Syft 偵測不到 OS package metadata — 改 scan source 為 build 階段的 <code>dir:./</code> 或保留 builder image 的 SBOM</li>
<li><strong>Grype 報一堆 unfixed CVE</strong>：base image 老、有 CVE 但 upstream 還沒 patch — 設 <code>only-fixed: true</code> 過濾 noise、focus 在 actionable item；同時排程 base image 升級</li>
<li><strong>CI 突然 fail 變多</strong>：Grype DB 更新後新 CVE 揭露 — 看 DB version diff、評估是 <em>真新風險</em> 還是 <em>舊 package 被重新分類</em>、必要時用 Ignore Rule + expiration 過渡</li>
<li><strong>SBOM 格式下游不認</strong>：合規要求 SPDX、產的是 SyftJSON — 用 <code>syft convert syft-json:./sbom.json -o spdx-json</code> 轉格式（Syft 本身就是 SBOM 互轉工具）</li>
<li><strong>Air-gapped 環境 Grype 跑不動</strong>：DB 沒同步、scan 直接報 0 vulnerability（假陰性）— <code>grype db status</code> 看 DB age、mirror sync 機制檢查、加 staleness alarm</li>
<li><strong>Ignore Rule 過期回到 fail</strong>：CI 突然 fail、查 expiration 已過 — 預期行為、強制 quarterly review；補 rotation 機制（cronjob 提前一週 alert owner）</li>
<li><strong>Binary 偵測不到 module</strong>：Go binary stripped、<code>-trimpath</code> 後 module path 沒了 — build 改加 <code>-buildvcs=true</code> 保留 VCS info、或 build 階段 SBOM scan source code、不是 binary</li>
<li><strong>cosign verify-attestation 失敗</strong>：image 被 re-tag / re-push 後 attestation manifest 不對 — 用 image digest（<code>@sha256:...</code>）而非 tag 做 attest、tag 不可信</li>
<li><strong>Grype 不抓某個 ecosystem</strong>：例如新冒出的 package manager — Syft 沒實作 detector、Grype 也看不到；submit issue 或自己寫 catalogger 貢獻</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個工具想包 IaC / secret / k8s misconfig</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></td>
      </tr>
      <tr>
          <td>需要 SAST / Reachability / Fix PR workflow</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>綁 GitHub 的 SAST + Dependabot</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a></td>
      </tr>
      <tr>
          <td>Container runtime detection</td>
          <td>Falco / Cilium Tetragon（見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">7 後續候選 vendor 清單</a>）</td>
      </tr>
      <tr>
          <td>Image signing / attestation</td>
          <td><a href="https://docs.sigstore.dev/">Sigstore cosign</a></td>
      </tr>
      <tr>
          <td>Policy at admission</td>
          <td>Kyverno / OPA Gatekeeper（見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">7 後續候選 vendor 清單</a>）</td>
      </tr>
      <tr>
          <td>SBOM dashboard / enterprise policy / RBAC</td>
          <td>Anchore Enterprise（商業）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>CycloneDX / SPDX 完整 schema 規格逐欄位解讀</li>
<li>Sigstore cosign / Rekor / Fulcio 完整架構（attest 鏈的 OIDC / transparency log）</li>
<li>SLSA framework 各 level 對應的 builder 要求</li>
<li>Anchore Enterprise policy DSL 完整語法</li>
<li>VEX（Vulnerability Exploitability eXchange）跟 CSAF 標準對照細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>07 案例庫沒有直接 Syft / Grype-level 事件、但供應鏈案例都是 SBOM-first 思維的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Syft + Grype 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>對照啟示 — 預先用 Syft 產 SBOM 集中保存後、Log4Shell 公開時拿歷史 SBOM 跑 Grype 在分鐘級回答「我們哪些服務有用、含 transitive」</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>對照啟示 — Syft 看 package layer、看不到 build-time backdoor 注入；需配 cosign attest + SLSA provenance 才完整</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>對照啟示 — 相同 version 被植入後 SBOM 一樣、純比對 SBOM 看不出來；mitigation 是 SBOM diff 對 baseline + release tarball verify</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya VSA 2021</a></td>
          <td>對照啟示 — 多服務 SBOM 集中 inventory（哪 service 用哪 component）、緊急時可 <em>affected-services-by-package</em> 反查、不是逐 image scan</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></td>
          <td>Syft 是 SBOM reference implementation、章節原則對應 SBOM + signing + provenance 的 trust chain</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（一站式替代）、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>（商業 SaaS）、<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>（GitHub 內建）</li>
<li>下游：<a href="https://docs.sigstore.dev/">Sigstore cosign</a>（SBOM attestation）、admission policy（Kyverno / OPA Gatekeeper、見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">7 後續候選 vendor 清單</a>）</li>
<li>跨類：runtime workload protection（Falco / Cilium Tetragon、見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">7 後續候選 vendor 清單</a>）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（cosign signing key 保存）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（新 CVE 揭露時的 SBOM-based fan-out 查詢）</li>
<li>官方：<a href="https://github.com/anchore/syft">Syft Documentation</a> / <a href="https://github.com/anchore/grype">Grype Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.8 State Ownership 與 Query Boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</guid><description>&lt;p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。&lt;/p>
&lt;p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。&lt;/p>
&lt;h2 id="state-ownership">State Ownership&lt;/h2>
&lt;p>State ownership 的責任是判斷哪些資料是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。&lt;/p>
&lt;p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。&lt;/p>
&lt;h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Canonical state&lt;/th>
 &lt;th>Derived state&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>角色&lt;/td>
 &lt;td>source of truth&lt;/td>
 &lt;td>從 canonical 計算 / 同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入&lt;/td>
 &lt;td>用戶 / 業務操作&lt;/td>
 &lt;td>從 canonical 推&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性&lt;/td>
 &lt;td>strong / serializable&lt;/td>
 &lt;td>eventual 通常夠用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>必須能精確修復&lt;/td>
 &lt;td>可以「砍掉重建」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範例&lt;/td>
 &lt;td>訂單、付款、餘額&lt;/td>
 &lt;td>搜尋 index、recommendation、daily summary&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Canonical state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>業務決策依據（付款、權限）&lt;/li>
&lt;li>不能從其他地方重建（一旦丟、無法找回）&lt;/li>
&lt;li>需要 audit log、point-in-time recovery、backup&lt;/li>
&lt;li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Derived state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從 canonical 推算出來&lt;/li>
&lt;li>可以「rebuild」（lazy 或 eager）&lt;/li>
&lt;li>失效可接受（用戶可能看到舊的）&lt;/li>
&lt;li>通常在 cache / search / analytics store&lt;/li>
&lt;li>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache&lt;/a> 配對快取、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store&lt;/a> feature&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>設計原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同一資料 &lt;em>不能&lt;/em> 同時是兩個地方的 canonical → 衝突時不知道信誰&lt;/li>
&lt;li>寫入永遠先寫 canonical、再 propagate 到 derived&lt;/li>
&lt;li>derived 出錯只能 rebuild、不能拿來「修正 canonical」&lt;/li>
&lt;/ul>
&lt;h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。&lt;/p>
&lt;p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 是解決形狀不對稱的方式。&lt;/p></description><content:encoded><![CDATA[<p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。</p>
<p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。</p>
<h2 id="state-ownership">State Ownership</h2>
<p>State ownership 的責任是判斷哪些資料是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。</p>
<p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。</p>
<h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Canonical state</th>
          <th>Derived state</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>角色</td>
          <td>source of truth</td>
          <td>從 canonical 計算 / 同步</td>
      </tr>
      <tr>
          <td>寫入</td>
          <td>用戶 / 業務操作</td>
          <td>從 canonical 推</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>strong / serializable</td>
          <td>eventual 通常夠用</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>必須能精確修復</td>
          <td>可以「砍掉重建」</td>
      </tr>
      <tr>
          <td>範例</td>
          <td>訂單、付款、餘額</td>
          <td>搜尋 index、recommendation、daily summary</td>
      </tr>
  </tbody>
</table>
<p><strong>Canonical state 的特徵</strong>：</p>
<ul>
<li>業務決策依據（付款、權限）</li>
<li>不能從其他地方重建（一旦丟、無法找回）</li>
<li>需要 audit log、point-in-time recovery、backup</li>
<li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）</li>
</ul>
<p><strong>Derived state 的特徵</strong>：</p>
<ul>
<li>從 canonical 推算出來</li>
<li>可以「rebuild」（lazy 或 eager）</li>
<li>失效可接受（用戶可能看到舊的）</li>
<li>通常在 cache / search / analytics store</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 配對快取、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a> feature</li>
</ul>
<p><strong>設計原則</strong>：</p>
<ul>
<li>同一資料 <em>不能</em> 同時是兩個地方的 canonical → 衝突時不知道信誰</li>
<li>寫入永遠先寫 canonical、再 propagate 到 derived</li>
<li>derived 出錯只能 rebuild、不能拿來「修正 canonical」</li>
</ul>
<h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用</h2>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。</p>
<p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 是解決形狀不對稱的方式。</p>
<p>資料庫情境的 CQRS 有不同的實作強度：</p>
<p><strong>最輕量 — 同 DB 不同 query path</strong>：寫入走 canonical table，讀取走 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 或反正規化 view。同一個 PostgreSQL 裡用 materialized view 就能實現最基本的讀寫分離，不需要兩個 DB、不需要事件同步。適合讀寫形狀不同但流量規模還不需要獨立擴展的階段。</p>
<p><strong>中度 — 同 DB 加 read replica</strong>：寫入走 primary，列表跟報表走 read replica。Replica lag 決定哪些 query 能走 replica（見下方 Replica Lag 段）。適合讀取流量開始壓迫寫入的階段。</p>
<p><strong>完整 — 獨立 read store</strong>：寫入走 OLTP DB，讀取走獨立的 analytics store（BigQuery、Athena）或搜尋引擎（Elasticsearch）。透過 CDC 或事件同步維護 read store。適合讀取形狀、流量、SLA 都跟寫入完全不同的階段。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層（OLTP）跟資料層（BigQuery / Athena）分開。<a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a> — on-prem OLTP + GCP BigQuery analytics。</p>
<h2 id="event-sourcing-與-state-ownership">Event Sourcing 與 State Ownership</h2>
<p><a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">Event sourcing</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：event sourcing 怎麼改變 state ownership 跟 query boundary。</p>
<p>Event sourcing 把 state ownership 的正式紀錄從 mutable row 改成 append-only <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>。這個改變影響本章的每一個面向：</p>
<p><strong>對 canonical / derived 分類的影響</strong>：採用 event sourcing 後，event log 是 canonical state，current state 變成 derived state。這跟傳統 CRUD 架構相反 — 傳統架構中 current state（mutable row）是 canonical，歷史紀錄（audit log）是 derived。</p>
<p><strong>對 query boundary 的影響</strong>：event log 不適合直接服務交易查詢跟列表查詢（每次 replay 整條事件流太慢）。Event sourcing 幾乎必然搭配 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 維護 read model — projection 持續消費事件流、更新反正規化的查詢 view。交易查詢讀 projection 的輸出而非直接讀 event log。</p>
<p><strong>對修復流程的影響</strong>：傳統架構的資料修復是「直接改 row」；event sourcing 的修復是「發一筆補償事件（compensating event）」。修復本身也是事件、會被記錄在 event log 裡、提供完整的修復 audit trail。</p>
<p>Event sourcing 的設計門檻在於 projection 的維護跟 event schema evolution。Projection 數量增長後，每次 event schema 改版都需要同步更新所有 projection；projection 的 replay 跟 reconciliation 是長期運維的主要成本。這些代價決定了 event sourcing 適合「需要完整變更歷史」的業務場景（金融帳務、訂單流程、法規合規），而非所有資料存取場景。</p>
<h2 id="materialized-view-在資料庫的應用">Materialized View 在資料庫的應用</h2>
<p><a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">Materialized view</a> 的概念定義見知識卡。本段聚焦在 OLTP 資料庫裡 materialized view 作為最輕量 read model 的具體實作。</p>
<p>Materialized view 是「同 DB 內最簡單的讀寫分離」。不需要事件同步、不需要獨立 read store、不需要 projection consumer — 資料庫自己定期執行查詢、存放結果。</p>
<p><strong>跟 regular view 的差別</strong>：regular view 是 SQL 別名，每次 query 重跑底層查詢；materialized view 有實體儲存，query 時直接讀預計算結果。差別在 query-time cost — 複雜 JOIN / aggregation 重複跑時，materialized view 把計算推到 refresh 時、query 時接近零成本。</p>
<p><strong>Refresh 策略</strong>：</p>
<ul>
<li><strong>全量 refresh</strong>：PostgreSQL 的 <code>REFRESH MATERIALIZED VIEW</code>，refresh 期間 view 預設 unavailable。</li>
<li><strong>Concurrent refresh</strong>：PostgreSQL 的 <code>CONCURRENTLY</code> 模式，refresh 期間 view 仍可讀但資料可能 stale。</li>
<li><strong>增量 refresh</strong>：PostgreSQL 的 <code>pg_ivm</code>、Oracle 的 fast refresh — 只更新變更的部分，成本低但配置複雜。</li>
<li><strong>Trigger-based</strong>：特定 event 觸發 refresh，適合低頻變更的資料。</li>
</ul>
<p><strong>在 state ownership 的定位</strong>：materialized view 是 derived state，修復方式是 refresh（重建）而非直接修改。大量 materialized view 會拖累寫入吞吐 — 每次 base table 變更都可能觸發 refresh 計算。設計時要平衡 refresh 頻率跟 query freshness 需求。</p>
<p><strong>跟觀測領域的對照</strong>：觀測領域的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 在概念上等同於 TSDB 層的 materialized view — 定期執行 query expression、把結果寫成新 series。兩者面對同樣的設計問題：refresh 頻率、freshness lag、維護成本與儲存增長。觀測領域的 CQRS 特化應用見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="query-boundary-四種">Query Boundary 四種</h2>
<p>Query boundary 的責任是讓不同查詢路徑承擔不同服務問題。交易查詢、列表查詢、報表查詢與對帳查詢都可能讀同一張表、但它們的正確性、延遲與資料新鮮度要求不同。</p>
<table>
  <thead>
      <tr>
          <th>查詢類型</th>
          <th>服務責任</th>
          <th>典型 latency</th>
          <th>容忍 stale</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易查詢</td>
          <td>支援使用者當下動作、例如付款、下單、授權</td>
          <td>&lt; 100ms</td>
          <td>不容忍</td>
          <td>延遲或錯誤會直接影響交易結果</td>
      </tr>
      <tr>
          <td>列表查詢</td>
          <td>支援使用者瀏覽與管理、例如訂單列表、會員清單</td>
          <td>&lt; 500ms</td>
          <td>可容忍秒級</td>
          <td>可能放大 index、pagination 與排序成本</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>支援營運分析、財務統計與趨勢判讀</td>
          <td>秒到分鐘級</td>
          <td>可容忍 hour 級</td>
          <td>容易壓迫線上資料庫與混淆資料時效</td>
      </tr>
      <tr>
          <td>對帳查詢</td>
          <td>驗證正式狀態與外部事實是否一致</td>
          <td>分鐘到小時級</td>
          <td>視業務</td>
          <td>查詢定義錯誤會造成錯修或漏修</td>
      </tr>
  </tbody>
</table>
<p>這四種查詢混在一起時、資料庫會同時承擔低延遲交易與高成本分析、最後讓任何一種資料庫選型都變得模糊。</p>
<h3 id="交易路徑的邊界">交易路徑的邊界</h3>
<p>交易路徑的責任是維持使用者動作的即時正確性。它需要短查詢、明確 index、可控 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 與清楚 timeout。</p>
<p>交易路徑的設計要把報表聚合或長時間掃描移到其他查詢路徑。若下單 API 同時查歷史報表、計算大範圍統計或同步重建派生狀態、交易延遲會被非交易責任拖慢。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 個獨立 Aurora cluster 把不同業務 transaction 分開、避免互相影響。</p>
<h3 id="列表與報表的邊界">列表與報表的邊界</h3>
<p>列表查詢的責任是支援產品體驗中的瀏覽與定位。列表查詢需要穩定排序、分頁策略、篩選條件與查詢成本界線；它應建立自己的讀取模型或索引策略、避免直接借用交易查詢的資料模型造成 slow query、排序漂移與 pagination 重複。</p>
<p>報表查詢的責任是支援分析與決策。報表通常可以接受資料延遲、因此更適合使用 read replica、materialized view、ETL 或 analytics store。把報表直接壓在線上 primary 上、會讓交易服務承擔不必要的容量風險。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair hybrid burst</a>、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層跟資料層分開部署。</p>
<h3 id="對帳查詢的邊界">對帳查詢的邊界</h3>
<p>對帳查詢的責任是驗證正式狀態是否與外部事實一致。付款、發票、庫存與訂閱方案都需要對帳查詢、但對帳查詢要保留時間窗、資料來源、差異定義與人工修復入口。</p>
<p>對帳查詢承擔比報表更直接的修復責任。報表回答「現在看起來如何」、對帳回答「哪一筆正式狀態需要修復」。因此對帳查詢結果要能進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<p>詳見 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>。</p>
<h2 id="replica-lag-對-query-boundary-的影響">Replica Lag 對 Query Boundary 的影響</h2>
<p>當應用使用 read replica 擴 read traffic 時、replica lag 會直接影響 query boundary 設計。</p>
<p><strong>典型 lag</strong>：</p>
<ul>
<li>PostgreSQL streaming：&lt; 100ms（同 AZ）</li>
<li>Aurora：10-30ms（同 region）</li>
<li>跨 region replica：秒級到分鐘級</li>
</ul>
<p><strong>不同 query 對 lag 的容忍</strong>：</p>
<ul>
<li>交易查詢：不可容忍 lag、必須走 primary</li>
<li>read-after-write（剛寫完查自己）：必須 primary、或 session sticky</li>
<li>列表查詢：通常容忍 lag &lt; 1 秒</li>
<li>報表查詢：lag 分鐘級可接受</li>
<li>對帳查詢：通常用 batch、lag 不關鍵</li>
</ul>
<p><strong>Stale read 容忍策略</strong>：</p>
<ul>
<li>「能容忍秒級 stale」的 read → replica（用戶 profile、報表）</li>
<li>「不能 stale」的 read → primary（剛寫入後的查詢、餘額確認）</li>
<li>read-after-write：用 session token 標記「剛寫過」、N 秒內讀走 primary</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的「Read Replica Scaling」段。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>資料庫選型前要先回答四個問題：</p>
<ol>
<li>哪些資料是正式狀態、哪些是派生狀態</li>
<li>哪些查詢屬於交易路徑、哪些可以延遲或離線化</li>
<li>哪些查詢結果會觸發修復、退款、補償或人工決策</li>
<li>哪些資料需要 audit、masking、retention 或刪除責任</li>
</ol>
<p>這些問題決定後續該比較 relational database、document database、search index、analytics store 還是 cache。工具差異要放在責任邊界之後討論。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 state ownership 與 query boundary。PostgreSQL、MySQL、MSSQL 或其他 relational database 的比較、應先問它們如何支援正式狀態、交易查詢、列表查詢、報表查詢與對帳查詢、再進入索引、隔離層級、replica 或工具語法。</p>
<p>若主問題是正式狀態與交易一致性、後續文章要優先比較 transaction、isolation、index 與 migration 能力。若主問題是報表與搜尋、後續文章要評估 read replica、materialized view、search index 或 analytics store。若主問題是對帳與修復、後續文章要比較 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、audit log、backup/restore 與資料修復流程。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>state / query 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a></td>
          <td>OLTP 交易層 + BigQuery / Athena 分析層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a></td>
          <td>on-prem OLTP + GCP BigQuery 分析、典型 CQRS 配置</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>feature store（derived state）、跟 source 分離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>watch list（user state）跟 content metadata 分層</td>
      </tr>
  </tbody>
</table>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a></li>
<li>與 1.3 的交接：transaction boundary 設計影響哪些 query 走 primary、哪些可走 replica</li>
<li>與 1.7 的交接：正式狀態變更要進入 production rollout — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a></li>
<li>與 1.9 的交接：對帳查詢的下游修復 — <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">Reconciliation and Data Repair</a></li>
<li>與 2 的交接：cache layer 是 derived state 最常見的形式 — <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
<li>與 4.20 的交接：query evidence 跟 reconciliation evidence — <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理 schema 與資料模型、接著讀 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a>。要處理 schema 演進與正式狀態變更、接著讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。要處理對帳跟資料修復、接著讀 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a>。要設計 KV / Document 的 state ownership、接著讀 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a>。</p>
]]></content:encoded></item><item><title>Google Cloud Spanner</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/</guid><description>&lt;p>Cloud Spanner 是 Google 內部 2007 年起跑、2017 年開放為 GCP 服務的 &lt;em>全球分散式 SQL OLTP&lt;/em>。內部撐 Google Ads / Play / Search 計費、外部支援 Blockchain.com、Sharechat、ZEE5 等。它的公開案例重點是每秒 10 億請求等級、線性擴展、強一致與 global distribution 可以同時成為 OLTP 設計目標。&lt;/p>
&lt;h2 id="教學路線全球強一致與-truetime-成本">教學路線：全球強一致與 TrueTime 成本&lt;/h2>
&lt;p>Spanner 服務頁的教學目標是把 global strong consistency、TrueTime、Paxos、region layout 與 processing unit 連成一條產品決策線。讀者讀完後要能判斷何時需要全球一致 SQL，並理解這種能力的 latency、成本與雲平台邊界。&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>Global consistency&lt;/td>
 &lt;td>強一致 SQL 為什麼需要時間邊界與 consensus&lt;/td>
 &lt;td>定位、適用場景、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">Linearizability&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Region layout&lt;/td>
 &lt;td>instance config、leader region、replica 如何影響 latency&lt;/td>
 &lt;td>容量規劃要點、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity unit&lt;/td>
 &lt;td>node / processing unit 如何取代傳統 shard 心智模型&lt;/td>
 &lt;td>容量特性、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Use-case pressure&lt;/td>
 &lt;td>billing、subscription、ticketing、金融交易何時需要 Spanner&lt;/td>
 &lt;td>適用場景、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時用 PostgreSQL、CockroachDB、Aurora DSQL、DynamoDB&lt;/td>
 &lt;td>不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位truetime--paxos-的全球線性-sql">定位：TrueTime + Paxos 的全球線性 SQL&lt;/h2>
&lt;p>Spanner 解決的是跨地理位置同時追求 strong consistency、linear scalability 與 global availability 的 OLTP 問題。&lt;/p>
&lt;p>&lt;strong>關鍵設計&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TrueTime API&lt;/strong>：用 GPS + 原子鐘提供「全球 unambiguous 時間戳」、誤差 &amp;lt; 7ms&lt;/li>
&lt;li>&lt;strong>External consistency&lt;/strong>（線性化）：跨節點交易順序跟 wall clock 一致&lt;/li>
&lt;li>&lt;strong>Paxos-based replication&lt;/strong>：跨 zone / region &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum&lt;/a>&lt;/li>
&lt;li>&lt;strong>線性擴展&lt;/strong>：2 nodes → 45K reads/sec、4 nodes → 90K reads/sec、依此類推&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>容量特性&lt;/strong>（引自 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner 案例&lt;/a>）：&lt;/p>
&lt;ul>
&lt;li>內部峰值：&amp;gt; 10 億 requests / sec&lt;/li>
&lt;li>線性擴展（不像 USL 系統會在某點 plateau）&lt;/li>
&lt;li>跨 region quorum 延遲：50-200ms（視 region 距離）&lt;/li>
&lt;li>最小容量單位：100 processing units（PU）≈ 1/10 node、適合小負載&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. 金融交易、ticketing inventory、payment ledger&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>需要強一致，避免 double-spend、oversell 或帳務順序錯亂&lt;/li>
&lt;li>全球用戶但需要原子性&lt;/li>
&lt;li>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner&lt;/a> — Google Ads 計費與 Google Play 訂閱都需要把每次計費事件放進可驗證順序&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. 全球用戶的 OLTP（不只 read replica）&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>Cloud Spanner 是 Google 內部 2007 年起跑、2017 年開放為 GCP 服務的 <em>全球分散式 SQL OLTP</em>。內部撐 Google Ads / Play / Search 計費、外部支援 Blockchain.com、Sharechat、ZEE5 等。它的公開案例重點是每秒 10 億請求等級、線性擴展、強一致與 global distribution 可以同時成為 OLTP 設計目標。</p>
<h2 id="教學路線全球強一致與-truetime-成本">教學路線：全球強一致與 TrueTime 成本</h2>
<p>Spanner 服務頁的教學目標是把 global strong consistency、TrueTime、Paxos、region layout 與 processing unit 連成一條產品決策線。讀者讀完後要能判斷何時需要全球一致 SQL，並理解這種能力的 latency、成本與雲平台邊界。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Global consistency</td>
          <td>強一致 SQL 為什麼需要時間邊界與 consensus</td>
          <td>定位、適用場景、<a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">Linearizability</a></td>
      </tr>
      <tr>
          <td>Region layout</td>
          <td>instance config、leader region、replica 如何影響 latency</td>
          <td>容量規劃要點、常見陷阱</td>
      </tr>
      <tr>
          <td>Capacity unit</td>
          <td>node / processing unit 如何取代傳統 shard 心智模型</td>
          <td>容量特性、案例對照</td>
      </tr>
      <tr>
          <td>Use-case pressure</td>
          <td>billing、subscription、ticketing、金融交易何時需要 Spanner</td>
          <td>適用場景、案例對照</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時用 PostgreSQL、CockroachDB、Aurora DSQL、DynamoDB</td>
          <td>不適用場景、跟其他 vendor 的取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="定位truetime--paxos-的全球線性-sql">定位：TrueTime + Paxos 的全球線性 SQL</h2>
<p>Spanner 解決的是跨地理位置同時追求 strong consistency、linear scalability 與 global availability 的 OLTP 問題。</p>
<p><strong>關鍵設計</strong>：</p>
<ul>
<li><strong>TrueTime API</strong>：用 GPS + 原子鐘提供「全球 unambiguous 時間戳」、誤差 &lt; 7ms</li>
<li><strong>External consistency</strong>（線性化）：跨節點交易順序跟 wall clock 一致</li>
<li><strong>Paxos-based replication</strong>：跨 zone / region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a></li>
<li><strong>線性擴展</strong>：2 nodes → 45K reads/sec、4 nodes → 90K reads/sec、依此類推</li>
</ul>
<p><strong>容量特性</strong>（引自 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner 案例</a>）：</p>
<ul>
<li>內部峰值：&gt; 10 億 requests / sec</li>
<li>線性擴展（不像 USL 系統會在某點 plateau）</li>
<li>跨 region quorum 延遲：50-200ms（視 region 距離）</li>
<li>最小容量單位：100 processing units（PU）≈ 1/10 node、適合小負載</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. 金融交易、ticketing inventory、payment ledger</strong>：</p>
<ul>
<li>需要強一致，避免 double-spend、oversell 或帳務順序錯亂</li>
<li>全球用戶但需要原子性</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — Google Ads 計費與 Google Play 訂閱都需要把每次計費事件放進可驗證順序</li>
</ul>
<p><strong>2. 全球用戶的 OLTP（不只 read replica）</strong>：</p>
<ul>
<li>跨 region 寫入、各地用戶寫入本地 region 仍維持全球強一致</li>
<li>它承擔的是 multi-region write path，而非 single primary + 跨 region read replica</li>
<li>對應案例：Blockchain.com（高頻 crypto 交易、強一致）</li>
</ul>
<p><strong>3. 想擺脫 sharding 複雜度</strong>：</p>
<ul>
<li>傳統大規模 SQL 常走應用層 sharding（管 shard key、跨 shard query、resharding）</li>
<li>Spanner 自動 partition，application 主要管理 schema、query shape 與 region layout</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner 案例</a> — 「節點數量是容量單位」，shard placement 由 Spanner 管理</li>
</ul>
<p><strong>4. PostgreSQL 相容路徑</strong>：</p>
<ul>
<li>2024 後 Spanner 提供 PostgreSQL dialect interface</li>
<li>從 PostgreSQL 應用遷入 Spanner 變得容易</li>
<li>跟 CockroachDB / Aurora DSQL 類似的策略</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 跨洲低延遲（&lt; 50ms）需求</strong>：</p>
<ul>
<li>跨洲 quorum 物理上 100ms+ 不可壓縮</li>
<li>替代：single-region OLTP（Aurora、Cloud SQL）+ <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a> 跨 region 同步</li>
</ul>
<p><strong>2. 高 throughput 但容忍 eventual consistency</strong>：</p>
<ul>
<li>Spanner 強一致有溢價，eventual consistency workload 通常有更低成本選項</li>
<li>替代：Bigtable（wide-column、eventual）、DynamoDB Global Tables（KV、eventual）</li>
</ul>
<p><strong>3. 小規模 OLTP</strong>：</p>
<ul>
<li>100 PU 起跳、月費約 $65 起、比 Cloud SQL 貴</li>
<li>流量 &lt; 1000 RPS 的場景、Cloud SQL 更划算</li>
<li>Spanner 主要對 <em>中大規模 + 全球</em> workload</li>
</ul>
<p><strong>4. 跨雲需求</strong>：</p>
<ul>
<li>Spanner 是 GCP managed service，cross-cloud / on-prem 需求要看 CockroachDB、TiDB 或其他自管路線</li>
<li>替代：CockroachDB、TiDB（自管、可跨雲）</li>
</ul>
<p><strong>5. 需要 OLAP 分析能力</strong>：</p>
<ul>
<li>Spanner 定位在 OLTP，analytics workload 交給 BigQuery 或其他 OLAP 系統</li>
<li>替代：跟 BigQuery 整合做 ETL、或用 Spanner Graph（2024 推出）</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs Aurora DSQL（AWS 2024 推出、概念對標 Spanner）</strong>：</p>
<ul>
<li>Spanner：用 TrueTime hardware、生產驗證 17 年（Google 內部）+ 7 年（公開）</li>
<li>Aurora DSQL：新（2024）、PostgreSQL 相容、serverless</li>
<li>選 Spanner：GCP 生態、需要極致成熟度</li>
<li>選 Aurora DSQL：AWS 生態、需要 PostgreSQL ORM 相容</li>
</ul>
<p><strong>vs CockroachDB</strong>：</p>
<ul>
<li>Spanner：managed、TrueTime hardware、GCP 限定</li>
<li>CockroachDB：自管、HLC + Raft（不靠 TrueTime）、跨雲</li>
<li>選 Spanner：想把 operation 交給 GCP managed service，並需要 Google 規模驗證</li>
<li>選 CockroachDB：跨雲 / on-prem、PostgreSQL 相容、自管彈性</li>
</ul>
<p><strong>vs TiDB</strong>：</p>
<ul>
<li>Spanner：GCP-only、PostgreSQL-like</li>
<li>TiDB：可自管 + Cloud、MySQL 相容、中國 / 亞洲生態深</li>
<li>選 Spanner：英語 / 歐美生態</li>
<li>選 TiDB：MySQL 應用、亞洲市場</li>
</ul>
<p><strong>vs Aurora（traditional single-region scaling）</strong>：</p>
<ul>
<li>Spanner：全球分散式</li>
<li>Aurora：single-region scaling</li>
<li>選 Spanner：流量明確跨 region + 需要強一致</li>
<li>選 Aurora：流量集中一個 region（多數情況）</li>
</ul>
<p><strong>vs Cosmos DB（multi-region write）</strong>：</p>
<ul>
<li>Spanner：strong consistency 跨 region</li>
<li>Cosmos DB：5 個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>s、AP 系統（含 strong 但語義不同）</li>
<li>選 Spanner：需要 linearizable（金融、ticketing）</li>
<li>選 Cosmos DB：可接受 session / eventual、Azure 生態、需要 multi-model</li>
</ul>
<p><strong>vs Bigtable</strong>：</p>
<ul>
<li>Spanner：SQL、強一致、OLTP</li>
<li>Bigtable：wide-column、eventual replication、時序 / IoT / 大資料</li>
<li>兩者互補：Bigtable 承擔大資料 / wide-column，Spanner 承擔強一致 OLTP</li>
</ul>
<p><strong>vs PostgreSQL（baseline）</strong>：</p>
<ul>
<li>PostgreSQL：single-primary、跨 region async replication、90% 場景夠用</li>
<li>Spanner：全球線性化、強一致跨 region、需要 GCP + 接受 latency / 成本</li>
<li>從 PostgreSQL 升級 Spanner 的判準：流量明確跨 region，且跨 region 一致性是 product requirement</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a> 取捨段 + <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>從 09 案例庫 + Spanner 文件提煉：</p>
<p><strong>1. 節點數量 = 容量單位</strong>：</p>
<ul>
<li>節點配置通常用較長週期 review，並在事件高峰前預先調整</li>
<li>線性擴展讓 forecast 簡單（2x 流量 → 2x 節點）</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的「不可水平擴容服務」反向 — Spanner 是 <em>可水平擴容</em> 但需要 <em>提前 provision</em></li>
</ul>
<p><strong>2. 跨 region quorum 配置</strong>：</p>
<ul>
<li>multi-region instance 可選擇哪些 region 是 voting member</li>
<li>voting region 數量決定 failure domain</li>
<li>跨大洲 voting 延遲高、跨大陸內可接受</li>
</ul>
<p><strong>3. 100 PU 起跳的 granular sizing</strong>：</p>
<ul>
<li>早期 Spanner 最小單位 1 node（約 $1000+/month）、中小負載難用</li>
<li>後來推出 100 PU（1/10 node、約 $65/month）、讓小負載也能 evaluate</li>
</ul>
<p><strong>4. 跨環境與新產品能力要查官方文件</strong>：</p>
<ul>
<li>Spanner 的跨環境、graph、PostgreSQL dialect 與 change streams 能力持續演進</li>
<li>實作前要用官方文件確認可用 region、版本、限制與 pricing</li>
</ul>
<p><strong>5. TrueTime 是 Spanner 價值之一</strong>：</p>
<ul>
<li>Spanner 還有 schema migration without downtime、change streams、interleaved tables</li>
<li>評估 Spanner 要同時看跨 region 強一致與整體 SQL 工程能力</li>
</ul>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本批 4 篇 deep article 已完成、覆蓋 Spanner 從 TrueTime 到 Cloud SQL 遷移的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TrueTime 是手段、line-rate scaling 才是設計目的、commit wait 數學</td>
          <td><a href="truetime-api-depth/">truetime-api-depth</a></td>
          <td>9.C10 Google internal dogfood 線性擴展模式、ε 暴衝失敗模式、cross-region voting latency 影響</td>
      </tr>
      <tr>
          <td>external consistency / serializability / linearizability 精確定義差異</td>
          <td><a href="consistency-models-comparison/">consistency-models-comparison</a></td>
          <td>PG SSI / CockroachDB / Spanner / Aurora DSQL line-rate scaling 對照、9.C10 cross-region quorum 100-200ms</td>
      </tr>
      <tr>
          <td>Schema migration without downtime + interleaved tables 物理 layout</td>
          <td><a href="schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a></td>
          <td>TrueTime version timestamp、5 production 踩雷、跟 PostgreSQL online schema change 對照</td>
      </tr>
      <tr>
          <td>Cloud SQL for PostgreSQL → Spanner（Type E paradigm shift）playbook</td>
          <td><a href="migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a></td>
          <td>sizing barrier（100 pu 起跳）+ &lt; 50ms write latency no-go、cost crossover 報告、9.C10 dogfood 邊界</td>
      </tr>
      <tr>
          <td>Change Streams (CDC)：data change record、watch partition、下游整合</td>
          <td><a href="change-streams-cdc/">change-streams-cdc</a></td>
          <td>OLTP 變更餵搜尋 / 快取 / 分析、child partition 接力、retention 失敗、跟 DynamoDB Streams 對照</td>
      </tr>
      <tr>
          <td>PostgreSQL dialect vs GoogleSQL、相容子集邊界、dialect 不可逆</td>
          <td><a href="postgresql-dialect/">postgresql-dialect</a></td>
          <td>PostgreSQL 生態遷入、相容性 audit、dialect 鎖定的高代價回退、何時選 PG dialect</td>
      </tr>
      <tr>
          <td>Spanner Graph (2024)：property graph、跟 relational 共存、GQL</td>
          <td><a href="spanner-graph/">spanner-graph</a></td>
          <td>多跳關係查詢、edge table layout 不可逆設計代價、super node 扇出、何時用專用 graph DB</td>
      </tr>
      <tr>
          <td>Spanner ↔ BigQuery federation：OLTP/OLAP 分工、Data Boost</td>
          <td><a href="bigquery-federation/">bigquery-federation</a></td>
          <td>分析查詢拖垮 OLTP、Data Boost workload 隔離、federation vs change-stream 落地、何時分出去</td>
      </tr>
  </tbody>
</table>
<p>DB4 cross-vendor entry：先看 <a href="../cockroachdb/aurora-dsql-spanner-decision-tree/">CockroachDB / Aurora DSQL / Spanner 決策樹</a> 識別 driver path、再進本 vendor 深度。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>Spanner Graph 進階查詢 lab（GQL pattern、super node 處理、遍歷效能調校）</li>
<li>Data Boost 容量規劃與成本模型 deep dive</li>
<li>Change Streams → Dataflow hands-on lab（建 stream、部署 pipeline、驗證 end-to-end）</li>
<li>Spanner regional → multi-region topology 升級 playbook</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>Spanner 的 global strong consistency 是高價值能力，也會把 latency、region layout 與 GCP lock-in 帶進核心架構。這一段先說何時維持 Cloud SQL / Aurora，再說何時升級 Spanner、CockroachDB、Aurora DSQL 或 Bigtable / DynamoDB。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloud SQL / Aurora</td>
          <td>single-region primary 足夠、跨 region 只需 async DR / read</td>
          <td>跨 region 寫入順序是產品契約、double-spend / oversell 代價高</td>
          <td><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>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></td>
      </tr>
      <tr>
          <td>Spanner regional</td>
          <td>單 region 強一致與水平擴容已足夠</td>
          <td>需要 multi-region availability、regional failure survival</td>
          <td><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">Quorum</a>、<a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">External Consistency</a></td>
      </tr>
      <tr>
          <td>Spanner multi-region</td>
          <td>GCP 生態、SQL workload、global consistency 是核心需求</td>
          <td>跨洲 p99 目標過低、成本或 GCP lock-in 成為主要風險</td>
          <td><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a>、<a href="/blog/backend/knowledge-cards/global-oltp/" data-link-title="Global OLTP" data-link-desc="跨地理區域仍維持交易一致性的 OLTP 設計責任與代價">Global OLTP</a></td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>GCP-only managed 服務可接受</td>
          <td>跨雲、on-prem、自管或 PostgreSQL wire 相容是硬需求</td>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>團隊已在 GCP 或需要 Spanner 成熟度</td>
          <td>AWS 生態、serverless distributed SQL、PostgreSQL 相容是主訴求</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL Migration</a></td>
      </tr>
      <tr>
          <td>Bigtable / DynamoDB</td>
          <td>workload 可接受 eventual consistency 或 KV / wide-column</td>
          <td>強一致 SQL 的協調成本高於產品收益</td>
          <td><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 vendor</a></td>
      </tr>
  </tbody>
</table>
<p>Spanner 的簡單路徑是先證明跨 region 一致性是產品需求。若只是想要全球 read latency，read replica、cache、edge KV 或 eventual consistency pipeline 可能更划算；Spanner 適合把「全球寫入順序正確」視為產品承諾的資料。</p>
<p>Region layout 的升級路徑要先定義 leader、voting replica 與使用者地理分布。跨洲 quorum 會把物理延遲放進 transaction path，因此 latency budget、降級策略與 read staleness policy 要一起寫進設計。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>Spanner overview 目前完成 global SQL 判斷。下一輪 deep article / playbook 應補 TrueTime、external consistency、PostgreSQL dialect、interleaved tables、change streams、Cloud SQL / PostgreSQL → Spanner migration 與 Spanner / BigQuery federation。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>規模</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner</a></td>
          <td>&gt; 10 億 req/sec、線性擴展</td>
          <td>全球強一致 OLTP 標竿</td>
      </tr>
  </tbody>
</table>
<p>Spanner case 的讀法是先看一致性需求，再看容量數字。10 億 req/sec 證明它能水平擴展，但讀者真正要回收的是「計費、訂閱、庫存、交易順序」這類需要 global external consistency 的產品壓力。</p>
<h2 id="反向-sibling-路由">反向 sibling 路由</h2>
<p>Spanner 的反向 sibling 路由用來把 global strong consistency 和雲端代管責任一起判讀。若讀者從 PostgreSQL / MySQL 過來，先確認是否具產品契約等級的 external consistency 需求；若只是 managed SQL 與 replica scaling，回 <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>；若要 PostgreSQL-like distributed SQL 且需要自管或多雲彈性，對照 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a>；若 access pattern 是固定 KV / document，先看 <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 vendor</a> 或 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a>。</p>
<p>這條路由的判準是交易順序是否跨 region 影響產品正確性。Spanner 的價值在 external consistency、schema 與 SQL 能力、全球 deployment 與 Google Cloud operation model 的組合；若產品只需要 eventual / session consistency，較輕的 NoSQL 或 managed SQL 常有更低成本。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>誤以為跨 region 強一致沒有延遲代價</strong>：跨洲 quorum 100-200ms 是物理成本</li>
<li><strong>設計 schema 像傳統 PostgreSQL</strong>：Spanner 有 interleaved tables、適當用能加速查詢</li>
<li><strong>所有讀取都用強一致</strong>：read-only transaction 可選 bounded staleness，reporting 類路徑常能用 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 換較低成本</li>
<li><strong>單 region 用 Spanner</strong>：浪費、Cloud SQL / Aurora 更便宜</li>
<li><strong>不評估 100 PU 起跳</strong>：早年 1 node minimum、現在 100 PU 起、small workload 也可以 POC</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<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>、<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 vendor</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></li>
<li>上游：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> — 全球 OLTP 的容量規劃特殊性</li>
<li>Last reviewed：2026-05-22（processing units / PostgreSQL interface / TrueTime 文件屬時間敏感 claim）</li>
<li>官方：<a href="https://cloud.google.com/spanner">Cloud Spanner</a>、<a href="https://cloud.google.com/spanner/docs/true-time-external-consistency">TrueTime: Time Distributed in Spanner</a></li>
</ul>
]]></content:encoded></item><item><title>9.8 效能可觀測性</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 是 sibling — 04 處理通用觀測、9.8 處理 &lt;em>容量規劃用&lt;/em> 的觀測。&lt;/p>
&lt;p>本章不重複 04 的訊號治理基礎、聚焦在 &lt;em>容量 / 效能 / 成本三條觀測線怎麼整合&lt;/em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。&lt;/p>
&lt;h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method&lt;/a> 不只是壓測時用、production 也要持續監控。&lt;/p>
&lt;p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Utilization&lt;/strong>（使用率 0-100%）：直觀但會誤判&lt;/li>
&lt;li>&lt;strong>Saturation&lt;/strong>（queue depth）：早期警訊&lt;/li>
&lt;li>&lt;strong>Errors&lt;/strong>（資源層錯誤）：已經出事的訊號&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼不能只看 utilization&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）&lt;/li>
&lt;li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Saturation metric 是 capacity warning 的最早訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>queue depth（每個 queue / pool）&lt;/li>
&lt;li>connection pool 使用率（最常見隱性 bottleneck）&lt;/li>
&lt;li>thread pool / coroutine count&lt;/li>
&lt;li>event loop lag（Node.js、async runtime）&lt;/li>
&lt;li>GC pause time / frequency&lt;/li>
&lt;li>cache hit rate / eviction rate&lt;/li>
&lt;li>replication lag&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Dashboard 設計&lt;/strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 &lt;em>saturation 起飛&lt;/em> 時觸發、不是 utilization 滿。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">Lemino connection limit&lt;/a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%&lt;/a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 是 sibling — 04 處理通用觀測、9.8 處理 <em>容量規劃用</em> 的觀測。</p>
<p>本章不重複 04 的訊號治理基礎、聚焦在 <em>容量 / 效能 / 成本三條觀測線怎麼整合</em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。</p>
<h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控</h2>
<p><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method</a> 不只是壓測時用、production 也要持續監控。</p>
<p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：</p>
<ul>
<li><strong>Utilization</strong>（使用率 0-100%）：直觀但會誤判</li>
<li><strong>Saturation</strong>（queue depth）：早期警訊</li>
<li><strong>Errors</strong>（資源層錯誤）：已經出事的訊號</li>
</ul>
<p><strong>為什麼不能只看 utilization</strong>：</p>
<ul>
<li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）</li>
<li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）</li>
</ul>
<p><strong>Saturation metric 是 capacity warning 的最早訊號</strong>：</p>
<ul>
<li>queue depth（每個 queue / pool）</li>
<li>connection pool 使用率（最常見隱性 bottleneck）</li>
<li>thread pool / coroutine count</li>
<li>event loop lag（Node.js、async runtime）</li>
<li>GC pause time / frequency</li>
<li>cache hit rate / eviction rate</li>
<li>replication lag</li>
</ul>
<p><strong>Dashboard 設計</strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 <em>saturation 起飛</em> 時觸發、不是 utilization 滿。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino connection limit</a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%</a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。</p>
<h2 id="red-method請求層的容量訊號">RED method：請求層的容量訊號</h2>
<p><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a> 跟 USE 互補、從請求層看容量。</p>
<ul>
<li><strong>Rate</strong>：requests per second（每個 service / endpoint）</li>
<li><strong>Errors</strong>：error rate</li>
<li><strong>Duration</strong>：latency distribution（histogram、不是單一 percentile）</li>
</ul>
<p><strong>Duration 比 Errors 早</strong>：duration p99 飆通常先於 error rate 上升、是 saturation 的早期警訊。</p>
<p><strong>每個 endpoint 都要有 RED</strong>：不能只看全站 average、要分 endpoint。登入 endpoint 跟結帳 endpoint 的 saturation 行為不同、混在一起看不到 issue。</p>
<p><strong>Histogram 是必須、不是 nice-to-have</strong>：</p>
<ul>
<li>只記 p99 → 看不到 p999、看不到 distribution shape</li>
<li>記 histogram → 可以隨時算任何 percentile、可以做 long-tail 分析</li>
<li>Prometheus histogram、OpenMetrics histogram 是現代標準</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 25ms p95</a> — p95 是業務 KPI、不是技術指標、每個 endpoint 都有獨立 SLO。</p>
<h2 id="p50--p95--p99--p999-的取捨">p50 / p95 / p99 / p999 的取捨</h2>
<p>不同 percentile 反映不同問題、選錯 percentile 會錯失 issue。</p>
<ul>
<li><strong>p50（中位數）</strong>：整體狀況、感覺正常的指標、對長尾不敏感</li>
<li><strong>p95</strong>：日常 user-perceived experience、大多數用戶感受到的延遲</li>
<li><strong>p99</strong>：minority but critical 用戶體驗、SLO 常訂在這</li>
<li><strong>p999</strong>：極端長尾、受 GC pause / leader election / retry storm 影響、internal critical 系統訂在這</li>
</ul>
<p><strong>業務 SLO 通常訂 p99</strong>：「99% 用戶 request &lt; 500ms」是常見承諾、合約 SLA 也通常基於 p99。
<strong>Internal critical 系統訂 p99.9</strong>：金融交易、即時配對、客服 SaaS（5 個 9 可用性對應 5 個 9 latency 期待）。</p>
<p><strong>紀錄分布、不只紀錄 percentile</strong>：</p>
<ul>
<li>gauge p99 → 看不到 distribution shape、看不到 multimodal 分布</li>
<li>histogram → 可以重新計算任何 percentile、可以對比 distribution、可以找 anomaly</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML inference 在 p99 才能控制用戶體驗、p50 沒意義；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — 必須關注 p999、RAFT 系統長尾顯著。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency 卡片</a>。</p>
<h2 id="cost-dashboard">Cost dashboard</h2>
<p>成本訊號跟容量訊號要 <em>並列顯示</em>、不要分開看。</p>
<p><strong>Per-service / per-endpoint cost attribution</strong>：</p>
<ul>
<li>每個 service 自己的雲端成本</li>
<li>拆到每個 endpoint</li>
<li>跟 RPS / latency 並列、看「成本上升是因為流量還是低效」</li>
</ul>
<p><strong>Cost per request 的時序變化</strong>：</p>
<ul>
<li>突然上升通常是 <em>退化</em> 訊號（新版本沒效率）</li>
<li>緩慢上升通常是 <em>規模</em> 訊號（用戶增加但 efficiency 沒變）</li>
</ul>
<p><strong>成本異常告警（vs 容量異常告警）</strong>：</p>
<ul>
<li>容量告警：utilization &gt; X% → 擴容</li>
<li>成本告警：cost spike &gt; X% → review</li>
<li>兩者可能同時觸發（autoscaler 擴容也擴 cost）、要區分</li>
</ul>
<p><strong>跟業務 metric 對齊</strong>：cost per active user、cost per transaction、cost per ML inference。業務 metric 級別的 cost 才能 review unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">Lyft 100+ 微服務各自 cost</a> — 微服務粒度的 cost attribution、找出哪個 service 過貴；對應 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a>。</p>
<h2 id="continuous-profiling">Continuous profiling</h2>
<p><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous profiling</a> 是現代效能 observability 的關鍵環節 — production 持續取 profile（CPU / heap / lock）、隨時可以做 diff 跟 root cause。</p>
<p><strong>工具生態</strong>：</p>
<ul>
<li>Datadog Continuous Profiler、Pyroscope（開源 + Grafana 整合）、Parca（CNCF）</li>
<li>GCP Cloud Profiler、Azure Application Insights Profiler、AWS CodeGuru Profiler</li>
<li>Overhead 通常 &lt; 1% CPU、放心開在 production</li>
</ul>
<p><strong>跟 distributed tracing 整合</strong>：trace → span → profile。一個 slow request 點下去、能看到對應 span、再下去看 profile。</p>
<p><strong>Profile diff 是 release gate 的核心訊號</strong>：每次 deploy 後自動對比 baseline、退化幅度過門檻 trigger alert。詳見 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 跟 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff 卡片</a>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix 多 DB 統一後 profile 變單純</a> — DB 統一 → application 層 profile 噪音降低 → 退化定位更快。</p>
<h2 id="cardinality-cost-governance">Cardinality cost governance</h2>
<p>效能 observability 的成本經常爆炸、源頭通常是 high cardinality metric。</p>
<p><strong>高 cardinality 來源</strong>：</p>
<ul>
<li>per-user metric（user_id label）</li>
<li>per-request metric（request_id label）</li>
<li>per-trace metric（trace_id label）</li>
</ul>
<p><strong>為什麼會爆</strong>：Prometheus 等 metric system 為每個 label 組合存獨立 time series、cardinality = 所有 label value 的笛卡爾積。100 萬 user × 100 endpoint × 10 region = 10 億 time series、儲存爆炸。</p>
<p><strong>對策</strong>：</p>
<ul>
<li>high cardinality 資訊放 log / trace、不放 metric</li>
<li>metric label 限制在 low-cardinality 維度（service、endpoint、region、status）</li>
<li>真的需要 high-cardinality 分析、用 sampled trace + log query</li>
</ul>
<p>對應 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">04.10 cardinality cost governance</a>、跟 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">Metric Cardinality 卡片</a>。</p>
<h2 id="訊號跟-slo-對接">訊號跟 SLO 對接</h2>
<p>最後一層整合：每個 saturation metric 都要對應一個 SLO threshold、訊號驅動行動。</p>
<p><strong>訊號 → 行動鏈</strong>：</p>
<ul>
<li>saturation metric 超 threshold → trigger alert</li>
<li>alert 觸發 → trigger autoscaler / runbook / oncall</li>
<li>持續超 threshold → trigger <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> burn alert</li>
<li>error budget 用完 → trigger release freeze</li>
</ul>
<p><strong>Alert 不要太敏感</strong>：</p>
<ul>
<li>false positive 浪費 oncall、長期會 alert fatigue（<a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">Alert Fatigue 卡片</a>）</li>
<li>用 multi-window multi-burn-rate alert（Google SRE 推薦）</li>
<li>用 symptom-based alert（業務影響）而非 cause-based alert（單一資源）</li>
</ul>
<p>跟 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a> 直接對接。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads 99.999%</a></td>
          <td>SLO 5 個 9 的訊號治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 12 個月 99.999%</a></td>
          <td>滾動 SLO 觀測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi p99 分解</a></td>
          <td>ML inference 多 stage latency budget</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech p95 是業務 KPI</a></td>
          <td>latency 不只是技術指標</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（基礎訊號）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
</ul>
]]></content:encoded></item><item><title>9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/</guid><description>&lt;p>這個案例的核心責任是說明「surge load」（突發遠超預期）跟 event-peak（事件型可預測峰值）的差異。Pokémon GO 在 2016-07 上線時、實際流量達到原始容量規劃目標的 50 倍 — 根因是 &lt;em>根本沒人能預測這個產品會這麼紅&lt;/em>、峰值規劃方法論本身沒有失敗。這類負載對容量設計的要求跟其他案例本質不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Niantic Pokémon GO 在 GCP 上的關鍵敘述（引自 &lt;a href="https://cloud.google.com/blog/products/gcp/bringing-pokemon-go-to-life-on-google-cloud">Bringing Pokémon GO to life on Google Cloud&lt;/a>）：&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>實際流量&lt;/td>
 &lt;td>達到原始 target 的 50 倍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層&lt;/td>
 &lt;td>Google Container Engine (GKE)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容器編排&lt;/td>
 &lt;td>Kubernetes（planetary-scale 設計）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量支援&lt;/td>
 &lt;td>Google CRE 即時擴容&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵敘述：「Niantic chose GKE for its ability to orchestrate container clusters at planetary-scale」「Google CRE seamlessly provisioned extra capacity on behalf of Niantic to stay ahead of their record-setting growth」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這個案例最重要的判讀是「surge load 跟可預測峰值是不同問題」。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>50x surge 沒辦法事前規劃&lt;/strong>：任何合理的 capacity planning 都不會預留 50x headroom — 那會讓平日成本爆炸。surge 的工程做法不是「事前撐住」、是「事中快速補上」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理模組&lt;/a> 的事件管理。&lt;/li>
&lt;li>&lt;strong>CRE 不是技術、是 vendor 關係&lt;/strong>：Google Customer Reliability Engineering 是 GCP 提供給戰略客戶的 24/7 工程支援團隊。能即時為 Niantic 補容量靠的是 &lt;em>人 + 流程 + 工具&lt;/em> 的組合、不是純技術。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">00.6 操作控制服務選型&lt;/a> 的廠商支援能力評估。&lt;/li>
&lt;li>&lt;strong>Kubernetes 是 surge 的前置條件&lt;/strong>：如果 Niantic 用 VM-based 架構、即使 CRE 想補容量也來不及 boot up。Container orchestrator 把 provisioning 時間從分鐘級降到秒級、才讓 surge 反應變得可能。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 platform 選型。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「Google CRE 即時補容量」這種敘述對中小客戶不適用。一般客戶在 surge 下能依賴的是 &lt;em>自己的 autoscaler&lt;/em>、不是 vendor 工程師。設計 surge 對應策略時要假設「沒有 vendor 救援」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>接受 surge 不可避免、設計快速 onboard 流程&lt;/strong>：核心問題不是「會不會 surge」、是「surge 之後 24 小時內能不能撐住」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">08.8 incident communication&lt;/a>。&lt;/li>
&lt;li>&lt;strong>降級機制作為 surge 救命稻草&lt;/strong>：當容量不足時、優先保住核心功能、暫時關閉非核心。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02.3 cache stampede&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 high concurrency access&lt;/a> 的降級設計。&lt;/li>
&lt;li>&lt;strong>預先談好 vendor 緊急支援條款&lt;/strong>：戰略服務在簽約時就要談好 surge 期間的容量配額、限流豁免、CRE / TAM 支援、不要等出事才談。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的 vendor relationship 設計。&lt;/li>
&lt;li>&lt;strong>container-first 是 surge 反應的前置&lt;/strong>：VM-based 架構在 surge 下擴容速度比 container 慢一個量級、會直接成為 bottleneck。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS Enterprise Support + TAM、Azure Premier Support + CSAM 都有對等服務、但能即時動用工程師補容量的程度跟客戶等級綁定。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「surge load」（突發遠超預期）跟 event-peak（事件型可預測峰值）的差異。Pokémon GO 在 2016-07 上線時、實際流量達到原始容量規劃目標的 50 倍 — 根因是 <em>根本沒人能預測這個產品會這麼紅</em>、峰值規劃方法論本身沒有失敗。這類負載對容量設計的要求跟其他案例本質不同。</p>
<h2 id="觀察">觀察</h2>
<p>Niantic Pokémon GO 在 GCP 上的關鍵敘述（引自 <a href="https://cloud.google.com/blog/products/gcp/bringing-pokemon-go-to-life-on-google-cloud">Bringing Pokémon GO to life on Google Cloud</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實際流量</td>
          <td>達到原始 target 的 50 倍</td>
      </tr>
      <tr>
          <td>應用層</td>
          <td>Google Container Engine (GKE)</td>
      </tr>
      <tr>
          <td>容器編排</td>
          <td>Kubernetes（planetary-scale 設計）</td>
      </tr>
      <tr>
          <td>容量支援</td>
          <td>Google CRE 即時擴容</td>
      </tr>
  </tbody>
</table>
<p>關鍵敘述：「Niantic chose GKE for its ability to orchestrate container clusters at planetary-scale」「Google CRE seamlessly provisioned extra capacity on behalf of Niantic to stay ahead of their record-setting growth」。</p>
<h2 id="判讀">判讀</h2>
<p>這個案例最重要的判讀是「surge load 跟可預測峰值是不同問題」。</p>
<ol>
<li><strong>50x surge 沒辦法事前規劃</strong>：任何合理的 capacity planning 都不會預留 50x headroom — 那會讓平日成本爆炸。surge 的工程做法不是「事前撐住」、是「事中快速補上」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理模組</a> 的事件管理。</li>
<li><strong>CRE 不是技術、是 vendor 關係</strong>：Google Customer Reliability Engineering 是 GCP 提供給戰略客戶的 24/7 工程支援團隊。能即時為 Niantic 補容量靠的是 <em>人 + 流程 + 工具</em> 的組合、不是純技術。對應 <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">00.6 操作控制服務選型</a> 的廠商支援能力評估。</li>
<li><strong>Kubernetes 是 surge 的前置條件</strong>：如果 Niantic 用 VM-based 架構、即使 CRE 想補容量也來不及 boot up。Container orchestrator 把 provisioning 時間從分鐘級降到秒級、才讓 surge 反應變得可能。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 platform 選型。</li>
</ol>
<p>需要警惕：「Google CRE 即時補容量」這種敘述對中小客戶不適用。一般客戶在 surge 下能依賴的是 <em>自己的 autoscaler</em>、不是 vendor 工程師。設計 surge 對應策略時要假設「沒有 vendor 救援」。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>接受 surge 不可避免、設計快速 onboard 流程</strong>：核心問題不是「會不會 surge」、是「surge 之後 24 小時內能不能撐住」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 跟 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">08.8 incident communication</a>。</li>
<li><strong>降級機制作為 surge 救命稻草</strong>：當容量不足時、優先保住核心功能、暫時關閉非核心。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02.3 cache stampede</a> 跟 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 high concurrency access</a> 的降級設計。</li>
<li><strong>預先談好 vendor 緊急支援條款</strong>：戰略服務在簽約時就要談好 surge 期間的容量配額、限流豁免、CRE / TAM 支援、不要等出事才談。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 vendor relationship 設計。</li>
<li><strong>container-first 是 surge 反應的前置</strong>：VM-based 架構在 surge 下擴容速度比 container 慢一個量級、會直接成為 bottleneck。</li>
</ol>
<p>跨平台等效：AWS Enterprise Support + TAM、Azure Premier Support + CSAM 都有對等服務、但能即時動用工程師補容量的程度跟客戶等級綁定。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想對應 surge load → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">08.6 incident severity trigger</a></li>
<li>想設計降級策略 → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 high concurrency access</a> + <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
<li>想評估 vendor 支援 → <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">00.6 operations control service selection</a></li>
<li>對照可預測峰值案例 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/gcp/bringing-pokemon-go-to-life-on-google-cloud">Bringing Pokémon GO to life on Google Cloud</a></li>
<li><a href="https://cloud.google.com/customer-reliability-engineering">Google Customer Reliability Engineering</a></li>
</ul>
]]></content:encoded></item><item><title>2.8 Cache Data Shape 與 Access Pattern</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/</guid><description>&lt;p>Cache data shape 與 access pattern 的核心責任是讓快取資料結構反映服務語意。進入 Redis command 或特定快取服務前，讀者需要先知道 key、value、hash、set、sorted set、stream 與多層 cache 各自適合承擔哪種讀取責任。&lt;/p>
&lt;h2 id="key-space">Key Space&lt;/h2>
&lt;p>Key space 的責任是定義快取資料如何被定位、分組、失效與遷移。key 命名要包含資料責任、版本、租戶或區域等必要維度，讓失效與回退可控。&lt;/p>
&lt;p>常見 key 維度包含：&lt;/p>
&lt;ol>
&lt;li>資料類型，例如 &lt;code>product&lt;/code>、&lt;code>user-permission&lt;/code>、&lt;code>quota&lt;/code>。&lt;/li>
&lt;li>版本，例如 &lt;code>v1&lt;/code>、&lt;code>v2&lt;/code>。&lt;/li>
&lt;li>租戶或區域，例如 tenant、region、locale。&lt;/li>
&lt;li>實體識別，例如 product id、user id。&lt;/li>
&lt;/ol>
&lt;p>key 缺少版本時，cache migration 會變成破壞性替換。key 缺少租戶或區域時，失效範圍會被放大。&lt;/p>
&lt;h2 id="value-shape">Value Shape&lt;/h2>
&lt;p>Value shape 的責任是定義快取值的語意與演進方式。完整 JSON blob 適合一次讀取完整資料，但欄位更新與版本相容成本高；hash 適合欄位局部更新，但需要明確欄位責任；set 與 sorted set 適合集合與排名；counter 適合限流或計數。&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>string / blob&lt;/td>
 &lt;td>商品詳情、設定快照&lt;/td>
 &lt;td>schema 變更容易破壞相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hash&lt;/td>
 &lt;td>使用者摘要、商品局部欄位&lt;/td>
 &lt;td>欄位責任不清會變成半正式狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>set&lt;/td>
 &lt;td>membership、權限集合&lt;/td>
 &lt;td>stale membership 可能造成越權&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>sorted set&lt;/td>
 &lt;td>排名、時間排序、優先級&lt;/td>
 &lt;td>score 語意錯誤會造成排序漂移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>counter&lt;/td>
 &lt;td>rate limit、配額&lt;/td>
 &lt;td>原子性與過期窗口要對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>stream&lt;/td>
 &lt;td>輕量事件流&lt;/td>
 &lt;td>容易和正式 message queue 責任混淆&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>資料形狀的本質是服務責任選擇，Redis 語法是落地方式。&lt;/p>
&lt;p>&lt;code>string / blob&lt;/code> 的判讀重點是整包資料是否需要一起讀取與一起失效。&lt;code>hash&lt;/code> 的判讀重點是欄位是否真的能獨立更新。&lt;code>set&lt;/code> 與 &lt;code>sorted set&lt;/code> 的判讀重點是 membership 或排序錯誤會造成什麼後果。&lt;code>counter&lt;/code> 的判讀重點是原子性與過期窗口。&lt;code>stream&lt;/code> 的判讀重點是這條路徑是否已經接近 message queue 責任。&lt;/p>
&lt;h2 id="access-pattern">Access Pattern&lt;/h2>
&lt;p>Access pattern 的責任是定義快取面對的讀寫節奏。高讀低寫、熱點讀取、短期活動尖峰、租戶隔離與跨區讀取，都會影響 key 設計與容量策略。&lt;/p>
&lt;p>高讀低寫適合長 TTL 與背景刷新；熱點讀取需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 保護；短期尖峰需要 warmup 與分散過期；多租戶場景需要避免單租戶 key 壓垮共享 cache。&lt;/p>
&lt;h2 id="multi-layer-cache">Multi-layer Cache&lt;/h2>
&lt;p>多層快取的責任是分散延遲與來源壓力。常見層次包含 process local cache、distributed cache、CDN 或 search/read model。每一層都需要定義 freshness、失效來源與 fallback。&lt;/p>
&lt;p>多層 cache 的主要風險是 stale 疊加。local cache stale、distributed cache stale 與 CDN stale 缺少共同失效策略時，讀者看到的錯誤會很難追。&lt;/p>
&lt;h3 id="ml-feature-store-的多層-cache-設計模式">ML feature store 的多層 cache 設計模式&lt;/h3>
&lt;p>ML inference 場景的 feature lookup 是多層 cache 的典型應用。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store&lt;/a> 的策略段提出 &lt;em>可重用做法&lt;/em>：用 L1 in-process cache + L2 distributed cache + L3 持久 store 三層。Tubi 實做的是把 feature store 從 ScyllaDB 遷到 ElastiCache（屬於 L2 層的選擇）、p99 &amp;lt; 10ms；三層架構是策略段推導出的通用設計、不一定 Tubi 完整實做。&lt;/p></description><content:encoded><![CDATA[<p>Cache data shape 與 access pattern 的核心責任是讓快取資料結構反映服務語意。進入 Redis command 或特定快取服務前，讀者需要先知道 key、value、hash、set、sorted set、stream 與多層 cache 各自適合承擔哪種讀取責任。</p>
<h2 id="key-space">Key Space</h2>
<p>Key space 的責任是定義快取資料如何被定位、分組、失效與遷移。key 命名要包含資料責任、版本、租戶或區域等必要維度，讓失效與回退可控。</p>
<p>常見 key 維度包含：</p>
<ol>
<li>資料類型，例如 <code>product</code>、<code>user-permission</code>、<code>quota</code>。</li>
<li>版本，例如 <code>v1</code>、<code>v2</code>。</li>
<li>租戶或區域，例如 tenant、region、locale。</li>
<li>實體識別，例如 product id、user id。</li>
</ol>
<p>key 缺少版本時，cache migration 會變成破壞性替換。key 缺少租戶或區域時，失效範圍會被放大。</p>
<h2 id="value-shape">Value Shape</h2>
<p>Value shape 的責任是定義快取值的語意與演進方式。完整 JSON blob 適合一次讀取完整資料，但欄位更新與版本相容成本高；hash 適合欄位局部更新，但需要明確欄位責任；set 與 sorted set 適合集合與排名；counter 適合限流或計數。</p>
<table>
  <thead>
      <tr>
          <th>資料形狀</th>
          <th>適合場景</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>string / blob</td>
          <td>商品詳情、設定快照</td>
          <td>schema 變更容易破壞相容</td>
      </tr>
      <tr>
          <td>hash</td>
          <td>使用者摘要、商品局部欄位</td>
          <td>欄位責任不清會變成半正式狀態</td>
      </tr>
      <tr>
          <td>set</td>
          <td>membership、權限集合</td>
          <td>stale membership 可能造成越權</td>
      </tr>
      <tr>
          <td>sorted set</td>
          <td>排名、時間排序、優先級</td>
          <td>score 語意錯誤會造成排序漂移</td>
      </tr>
      <tr>
          <td>counter</td>
          <td>rate limit、配額</td>
          <td>原子性與過期窗口要對齊</td>
      </tr>
      <tr>
          <td>stream</td>
          <td>輕量事件流</td>
          <td>容易和正式 message queue 責任混淆</td>
      </tr>
  </tbody>
</table>
<p>資料形狀的本質是服務責任選擇，Redis 語法是落地方式。</p>
<p><code>string / blob</code> 的判讀重點是整包資料是否需要一起讀取與一起失效。<code>hash</code> 的判讀重點是欄位是否真的能獨立更新。<code>set</code> 與 <code>sorted set</code> 的判讀重點是 membership 或排序錯誤會造成什麼後果。<code>counter</code> 的判讀重點是原子性與過期窗口。<code>stream</code> 的判讀重點是這條路徑是否已經接近 message queue 責任。</p>
<h2 id="access-pattern">Access Pattern</h2>
<p>Access pattern 的責任是定義快取面對的讀寫節奏。高讀低寫、熱點讀取、短期活動尖峰、租戶隔離與跨區讀取，都會影響 key 設計與容量策略。</p>
<p>高讀低寫適合長 TTL 與背景刷新；熱點讀取需要 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 保護；短期尖峰需要 warmup 與分散過期；多租戶場景需要避免單租戶 key 壓垮共享 cache。</p>
<h2 id="multi-layer-cache">Multi-layer Cache</h2>
<p>多層快取的責任是分散延遲與來源壓力。常見層次包含 process local cache、distributed cache、CDN 或 search/read model。每一層都需要定義 freshness、失效來源與 fallback。</p>
<p>多層 cache 的主要風險是 stale 疊加。local cache stale、distributed cache stale 與 CDN stale 缺少共同失效策略時，讀者看到的錯誤會很難追。</p>
<h3 id="ml-feature-store-的多層-cache-設計模式">ML feature store 的多層 cache 設計模式</h3>
<p>ML inference 場景的 feature lookup 是多層 cache 的典型應用。<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a> 的策略段提出 <em>可重用做法</em>：用 L1 in-process cache + L2 distributed cache + L3 持久 store 三層。Tubi 實做的是把 feature store 從 ScyllaDB 遷到 ElastiCache（屬於 L2 層的選擇）、p99 &lt; 10ms；三層架構是策略段推導出的通用設計、不一定 Tubi 完整實做。</p>
<p><strong>通用三層模式</strong>（推導自 9.C25 策略段、實際分層深度視 workload）：</p>
<ul>
<li><strong>L1：in-process cache</strong>：跟 application 同一 process、避免 network hop、適合最熱的少量 features</li>
<li><strong>L2：distributed cache</strong>（ElastiCache / Memcached）：跨 application instance 共享、能擴容、Tubi 在這層用 ElastiCache 達 p99 &lt; 10ms</li>
<li><strong>L3：持久 store</strong>（ScyllaDB / DynamoDB / S3 + Parquet）：全量資料、cache miss 時的 fallback</li>
</ul>
<p>判讀重點：每層的 latency budget 跟 stale window 都應依 workload 跟業務容忍度設定。相對序列是 L1 stale window 最嚴、L2 中等、L3 為 source-of-truth 或可重算來源。三層 stale 若無共同失效策略、業務代價會落到 <em>推薦結果不穩定</em>、用戶看到不同 session 推不同內容。</p>
<h3 id="跨-cloud-部署的資料引力路由見-27">跨 cloud 部署的資料引力（路由：見 2.7）</h3>
<p>跨 cloud cache 部署的 <em>資料引力</em> 原則跟 <em>跨區一致性</em> 議題密切相關、主寫場域是 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的跨區一致性窗口</a>。本章從 <em>data shape / access pattern</em> 角度補充：當 cache value 包含跨 region 共享的業務資料時、access pattern 自然偏向 <em>同 cloud read</em> + <em>跨 cloud batch sync</em>、不適合即時跨 cloud lookup。詳見 9.C35 Snap KeyDB 案例。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>快取資料形狀選型前要先回答：</p>
<ol>
<li>讀取是單 key、批次 key、集合、排序還是計數。</li>
<li>寫入是整體替換、局部更新、追加還是原子遞增。</li>
<li>失效是單 key、群組、版本、租戶還是全域。</li>
<li>資料結構是否會讓快取承擔正式狀態責任。</li>
</ol>
<p>這些問題決定後續要比較 Redis data type、Memcached blob、CDN cache 或應用端 local cache。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體快取服務文章要承接本篇的 data shape 與 access pattern。Redis/Valkey 的 hash、set、sorted set、stream 能表達多種資料形狀；Memcached 偏向簡單 key/value blob；CDN 與 local cache 則承擔不同層次的讀取加速。比較服務時要先問 access pattern，再問語法。</p>
<p>若讀取是單 key 或 blob，後續文章要比較 serialization、value size、TTL 與 eviction。若讀取是集合、排名或計數，後續文章要比較資料結構、原子性與容量行為。若讀取跨多層 cache，後續文章要比較失效傳播、stale 疊加與 observability。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 TTL 與容量策略，接著讀 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>。要看選定形狀後各型別的操作語意、原子性與記憶體曲線，接著讀 <a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11 Redis data types 實作</a>。要處理 presence 類即時狀態，接著讀 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store 與即時狀態</a>。</p>
]]></content:encoded></item><item><title>3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/</guid><description>&lt;p>Queue consumer retry 與 replay handoff 的核心責任是把 request 外副作用做成可重試、可去重、可隔離、可重播的服務流程。這篇以 &lt;code>order_created&lt;/code> consumer 為例，示範 delivery、processing、recovery 三層語意如何交接到 evidence package、release gate 與 incident decision log。&lt;/p>
&lt;h2 id="服務路徑與語意分層">服務路徑與語意分層&lt;/h2>
&lt;p>這條路徑是 &lt;code>order-service -&amp;gt; broker -&amp;gt; order-created-consumer -&amp;gt; invoice/email/search/webhook&lt;/code>。Producer 把事件交給 broker 後，真正的業務完成要看 consumer 是否正確提交副作用。&lt;/p>
&lt;p>這篇先固定三層語意：&lt;/p>
&lt;ol>
&lt;li>Delivery semantics：訊息是否投遞與確認。&lt;/li>
&lt;li>Processing semantics：副作用是否可承受重複與部分失敗。&lt;/li>
&lt;li>Recovery semantics：故障後是否可重播並恢復一致。&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a> 成功只代表 delivery 進度，不代表發票與通知已完成。&lt;/p>
&lt;h2 id="event-contract-與相容邊界">Event Contract 與相容邊界&lt;/h2>
&lt;p>Event contract 的責任是讓 producer 與 consumer 在版本演進時仍可互通，且可被觀測與回放。&lt;/p>
&lt;p>&lt;code>order_created&lt;/code> 最小欄位：&lt;/p>
&lt;ol>
&lt;li>&lt;code>event_id&lt;/code>：全域唯一識別。&lt;/li>
&lt;li>&lt;code>schema_version&lt;/code>：事件版本。&lt;/li>
&lt;li>&lt;code>occurred_at&lt;/code>：事件發生時間。&lt;/li>
&lt;li>&lt;code>order_id&lt;/code>、&lt;code>tenant_id&lt;/code>：業務定位。&lt;/li>
&lt;li>&lt;code>idempotency_key&lt;/code>：副作用去重鍵。&lt;/li>
&lt;li>&lt;code>pii_scope&lt;/code>：敏感欄位範圍。&lt;/li>
&lt;/ol>
&lt;p>版本演進採向後相容優先：新增欄位可選、舊欄位保留窗口。schema 演進前要先確認 consumer 端 fallback 解析邏輯存在，避免切版後整批進 DLQ。&lt;/p>
&lt;h2 id="retry--dlq--quarantine">Retry / DLQ / Quarantine&lt;/h2>
&lt;p>Retry 的責任是吸收暫時性故障，不把短暫抖動升級成事故。這條路徑使用有限重試 + backoff + jitter：&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>即時重試&lt;/td>
 &lt;td>下游短暫 timeout 或限流&lt;/td>
 &lt;td>在主通道重試少量次數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲重試&lt;/td>
 &lt;td>故障持續但可恢復&lt;/td>
 &lt;td>延長 backoff，避免重試風暴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DLQ 隔離&lt;/td>
 &lt;td>payload 或版本異常、長時故障&lt;/td>
 &lt;td>轉入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Quarantine&lt;/td>
 &lt;td>同型 poison message 連續爆發&lt;/td>
 &lt;td>停主通道回放，先分群診斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>DLQ 的責任是隔離與診斷，不是永久儲存。重點是把異常訊息分群後對應修法，修完再定向回放。&lt;/p>
&lt;h2 id="idempotency-與-ack-timing">Idempotency 與 Ack Timing&lt;/h2>
&lt;p>Idempotency 的責任是把 at-least-once 交付轉成可接受業務結果。副作用如發票、email、webhook 都要以 &lt;code>idempotency_key&lt;/code> 做去重。&lt;/p>
&lt;p>Ack timing 的原則是「核心副作用提交後再 ack」：&lt;/p>
&lt;ol>
&lt;li>先執行副作用或落地可追蹤結果。&lt;/li>
&lt;li>成功後寫去重紀錄或 checkpoint。&lt;/li>
&lt;li>最後 ack broker。&lt;/li>
&lt;/ol>
&lt;p>先 ack 再副作用會造成資料遺失；副作用成功但去重紀錄失敗，則要由 recovery 層補償。&lt;/p>
&lt;h2 id="replay-runbook">Replay Runbook&lt;/h2>
&lt;p>Replay 的責任是故障後在可控範圍內恢復，不把修復變成第二次事故。&lt;/p>
&lt;p>這條路徑的 replay runbook：&lt;/p>
&lt;ol>
&lt;li>選定 replay window：依 &lt;code>occurred_at&lt;/code> 與 &lt;code>schema_version&lt;/code> 分段。&lt;/li>
&lt;li>Dry run：先在影子通道跑去重與下游容量驗證。&lt;/li>
&lt;li>限速回放：按 tenant 或 partition 分批，監控下游錯誤率。&lt;/li>
&lt;li>Reconciliation：對帳發票、通知、索引結果。&lt;/li>
&lt;li>Stop condition：duplicate side-effect、downstream timeout、DLQ 再爆發即停。&lt;/li>
&lt;/ol>
&lt;p>replay window 要能被明確描述與回放，不可用「重播昨天全部」這種不可驗證句子。&lt;/p>
&lt;h2 id="job-queue-的拓樸分工">Job queue 的拓樸分工&lt;/h2>
&lt;p>當背景工作同時要 &lt;em>高吞吐&lt;/em> 跟 &lt;em>快速反應&lt;/em>、單一通道模型會變成瓶頸。job queue 的擴展通常是 &lt;em>拓樸重整&lt;/em>、把不同工作類型切到不同傳遞路徑、而非單點替換。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &amp;#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Job Queue 演進到 Kafka + Redis&lt;/a> — Slack 在 job queue 擴展時把工作切到不同傳遞路徑、Kafka 跟 Redis 分別承擔持久性跟即時性目標、分開治理 lag、重試跟失敗重播。&lt;/p></description><content:encoded><![CDATA[<p>Queue consumer retry 與 replay handoff 的核心責任是把 request 外副作用做成可重試、可去重、可隔離、可重播的服務流程。這篇以 <code>order_created</code> consumer 為例，示範 delivery、processing、recovery 三層語意如何交接到 evidence package、release gate 與 incident decision log。</p>
<h2 id="服務路徑與語意分層">服務路徑與語意分層</h2>
<p>這條路徑是 <code>order-service -&gt; broker -&gt; order-created-consumer -&gt; invoice/email/search/webhook</code>。Producer 把事件交給 broker 後，真正的業務完成要看 consumer 是否正確提交副作用。</p>
<p>這篇先固定三層語意：</p>
<ol>
<li>Delivery semantics：訊息是否投遞與確認。</li>
<li>Processing semantics：副作用是否可承受重複與部分失敗。</li>
<li>Recovery semantics：故障後是否可重播並恢復一致。</li>
</ol>
<p><a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 成功只代表 delivery 進度，不代表發票與通知已完成。</p>
<h2 id="event-contract-與相容邊界">Event Contract 與相容邊界</h2>
<p>Event contract 的責任是讓 producer 與 consumer 在版本演進時仍可互通，且可被觀測與回放。</p>
<p><code>order_created</code> 最小欄位：</p>
<ol>
<li><code>event_id</code>：全域唯一識別。</li>
<li><code>schema_version</code>：事件版本。</li>
<li><code>occurred_at</code>：事件發生時間。</li>
<li><code>order_id</code>、<code>tenant_id</code>：業務定位。</li>
<li><code>idempotency_key</code>：副作用去重鍵。</li>
<li><code>pii_scope</code>：敏感欄位範圍。</li>
</ol>
<p>版本演進採向後相容優先：新增欄位可選、舊欄位保留窗口。schema 演進前要先確認 consumer 端 fallback 解析邏輯存在，避免切版後整批進 DLQ。</p>
<h2 id="retry--dlq--quarantine">Retry / DLQ / Quarantine</h2>
<p>Retry 的責任是吸收暫時性故障，不把短暫抖動升級成事故。這條路徑使用有限重試 + backoff + jitter：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>判讀重點</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時重試</td>
          <td>下游短暫 timeout 或限流</td>
          <td>在主通道重試少量次數</td>
      </tr>
      <tr>
          <td>延遲重試</td>
          <td>故障持續但可恢復</td>
          <td>延長 backoff，避免重試風暴</td>
      </tr>
      <tr>
          <td>DLQ 隔離</td>
          <td>payload 或版本異常、長時故障</td>
          <td>轉入 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a></td>
      </tr>
      <tr>
          <td>Quarantine</td>
          <td>同型 poison message 連續爆發</td>
          <td>停主通道回放，先分群診斷</td>
      </tr>
  </tbody>
</table>
<p>DLQ 的責任是隔離與診斷，不是永久儲存。重點是把異常訊息分群後對應修法，修完再定向回放。</p>
<h2 id="idempotency-與-ack-timing">Idempotency 與 Ack Timing</h2>
<p>Idempotency 的責任是把 at-least-once 交付轉成可接受業務結果。副作用如發票、email、webhook 都要以 <code>idempotency_key</code> 做去重。</p>
<p>Ack timing 的原則是「核心副作用提交後再 ack」：</p>
<ol>
<li>先執行副作用或落地可追蹤結果。</li>
<li>成功後寫去重紀錄或 checkpoint。</li>
<li>最後 ack broker。</li>
</ol>
<p>先 ack 再副作用會造成資料遺失；副作用成功但去重紀錄失敗，則要由 recovery 層補償。</p>
<h2 id="replay-runbook">Replay Runbook</h2>
<p>Replay 的責任是故障後在可控範圍內恢復，不把修復變成第二次事故。</p>
<p>這條路徑的 replay runbook：</p>
<ol>
<li>選定 replay window：依 <code>occurred_at</code> 與 <code>schema_version</code> 分段。</li>
<li>Dry run：先在影子通道跑去重與下游容量驗證。</li>
<li>限速回放：按 tenant 或 partition 分批，監控下游錯誤率。</li>
<li>Reconciliation：對帳發票、通知、索引結果。</li>
<li>Stop condition：duplicate side-effect、downstream timeout、DLQ 再爆發即停。</li>
</ol>
<p>replay window 要能被明確描述與回放，不可用「重播昨天全部」這種不可驗證句子。</p>
<h2 id="job-queue-的拓樸分工">Job queue 的拓樸分工</h2>
<p>當背景工作同時要 <em>高吞吐</em> 跟 <em>快速反應</em>、單一通道模型會變成瓶頸。job queue 的擴展通常是 <em>拓樸重整</em>、把不同工作類型切到不同傳遞路徑、而非單點替換。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Job Queue 演進到 Kafka + Redis</a> — Slack 在 job queue 擴展時把工作切到不同傳遞路徑、Kafka 跟 Redis 分別承擔持久性跟即時性目標、分開治理 lag、重試跟失敗重播。</p>
<p><strong>拓樸分工的判讀</strong>（基於 Slack case 揭露的雙通道分工方向）：</p>
<ul>
<li><strong>持久性主導的 job</strong>（發票、付款通知、合規記錄）→ Kafka / 持久 queue、保證 at-least-once</li>
<li><strong>即時性主導的 job</strong>（線上提醒、playback control、UI 更新）→ Redis / 輕量 queue、low latency</li>
</ul>
<p>設計含義：同一 consumer 應專注單一目標（高吞吐 / 即時 / 持久擇一）、其他目標拆到對應路徑。對應 <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-design 三個工程議題鐵三角</a> — idempotency / 重播流程 / 下游承載能力是 consumer 內部設計、拓樸分工是 <em>跨 consumer</em> 的責任拆分、兩者互補。</p>
<h2 id="job-queue-規模差異的治理重點">Job queue 規模差異的治理重點</h2>
<p>不同規模服務的 job queue 治理問題差異大、SSoT 在本章。對應 <a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 對照：規模差異下的佇列模型</a>：</p>
<ul>
<li><strong>小型服務</strong>：優先用 managed queue（SQS / Pub/Sub）、運維成本最低。最容易忽略的是語意邊界（重試次數、死信規則、重播責任）、規模一上來會出現資料重複與漏處理。<strong>升級訊號</strong>：team 數超 3-5 個、各自寫 consumer 開始出現 idempotency 不一致、進中型階段</li>
<li><strong>中型服務</strong>：常見問題是 lag 與 DLQ 長期累積。原因是 consumer idempotency + 重播流程 + 下游承載能力沒一起設計。對應前段 Job queue 拓樸分工。<strong>升級訊號</strong>：DLQ 累積速度高於排空速度連續 7 天、單一 tenant 流量尖峰拖垮其他 tenant、進大型階段</li>
<li><strong>大型服務</strong>：需要處理跨租戶跟跨區壓力。單叢集思維會讓任何一類流量尖峰拖垮整體。對應 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a> 跟 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker-basics 分層治理平台</a>、重點從「怎麼送訊息」轉成「怎麼隔離失敗」</li>
</ul>
<p>判讀重點：當前服務規模決定要處理的 <em>主要</em> 問題。規模尚小的服務硬上 multi-tenant 隔離治理屬過度設計、規模化服務應同時考慮 broker 容量是否充足跟隔離邊界是否完整。判斷自己在哪個階段、看 <em>升級訊號</em> 對應的指標。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Queue evidence 的責任是證明「投遞可達」與「處理可恢復」兩者同時成立。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>broker metric、consumer metric、DLQ log、reconciliation query</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>retry/replay 批次窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>lag、retry count、DLQ count、duplicate side-effect、throughput</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>queue owner、consumer owner、downstream owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、抽樣缺口、對帳覆蓋率</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>尚未驗證之下游 webhook 供應商、低流量 tenant replay</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Queue release gate 的責任是決定是否擴大回放或恢復主通道，而不是只看單一 lag 指標。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批 replay、維持觀察、暫停 consumer</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>idempotency proof、DLQ drain 結果、下游容量、duplicate 比例</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>retry storm、DLQ 再爆發、下游錯誤率超門檻</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>replay 可中止窗口、主通道可回切時間</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>queue on-call、business owner</td>
      </tr>
  </tbody>
</table>
<p>這組欄位對齊 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>pause consumer、drain DLQ、啟動 replay、停止 replay、執行補償都屬事故決策，需寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T13:18:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pause invoice consumer and start scoped replay for tenant A&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;duplicate invoices increased after consumer version rollout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">duplicate_invoice_ratio_tenant_a</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">dlq_events_by_schema_version</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">queue-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;stop duplicate side effects and restore invoice consistency&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;duplicate ratio does not decrease within two replay batches&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫對齊 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>，重點是切換時語意分層混淆導致 delivery 成功但業務結果失真。</p>
<p>這篇不處理同步 API latency、cache TTL 或 deployment drain。若風險在同步交易壓力、快取失效或流量切換，路由到 <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback</a>。</p>
]]></content:encoded></item><item><title>5.8 Deployment Rollout with Drain and Rollback（實作示範）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</guid><description>&lt;p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a>、drain 到事故回退都保留一致證據。&lt;/p>
&lt;p>本篇以 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約&lt;/a> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a>。&lt;/p>
&lt;h2 id="服務路徑與切換責任">服務路徑與切換責任&lt;/h2>
&lt;p>這條路徑是 &lt;code>client -&amp;gt; load balancer -&amp;gt; checkout-api -&amp;gt; payment provider/order db/order event&lt;/code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。&lt;/p>
&lt;p>切換責任分三層：&lt;/p>
&lt;ol>
&lt;li>版本可啟動：container/runtime/config 可用。&lt;/li>
&lt;li>版本可接流量：readiness 與依賴狀態對齊。&lt;/li>
&lt;li>版本可退場：drain 與在途請求可收束。&lt;/li>
&lt;/ol>
&lt;h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線&lt;/h2>
&lt;p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：&lt;/p>
&lt;ol>
&lt;li>image 與 runtime config 版本對齊。&lt;/li>
&lt;li>secret 已注入且權限正確。&lt;/li>
&lt;li>startup/readiness probe 能反映真實依賴狀態。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract&lt;/a> 參數與服務期望一致。&lt;/li>
&lt;li>service discovery 註冊與摘除路徑可用。&lt;/li>
&lt;/ol>
&lt;p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。&lt;/p>
&lt;h3 id="preflight-自動化">Preflight 自動化&lt;/h3>
&lt;p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>image 與 config 版本對齊檢查&lt;/strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 &lt;code>deploy/compat-matrix.yaml&lt;/code>），CI 自動比對。&lt;/li>
&lt;li>&lt;strong>infra drift detection&lt;/strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。&lt;/li>
&lt;li>&lt;strong>probe 語意驗證&lt;/strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。&lt;/li>
&lt;li>&lt;strong>rollback 可行性驗證&lt;/strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。&lt;/li>
&lt;/ol>
&lt;p>Preflight 自動化的產出是一份 go/no-go 報告，進入 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。&lt;/p>
&lt;h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition&lt;/h2>
&lt;p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。&lt;/p></description><content:encoded><![CDATA[<p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a>、drain 到事故回退都保留一致證據。</p>
<p>本篇以 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a> 與 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<h2 id="服務路徑與切換責任">服務路徑與切換責任</h2>
<p>這條路徑是 <code>client -&gt; load balancer -&gt; checkout-api -&gt; payment provider/order db/order event</code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。</p>
<p>切換責任分三層：</p>
<ol>
<li>版本可啟動：container/runtime/config 可用。</li>
<li>版本可接流量：readiness 與依賴狀態對齊。</li>
<li>版本可退場：drain 與在途請求可收束。</li>
</ol>
<h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線</h2>
<p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：</p>
<ol>
<li>image 與 runtime config 版本對齊。</li>
<li>secret 已注入且權限正確。</li>
<li>startup/readiness probe 能反映真實依賴狀態。</li>
<li><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a> 參數與服務期望一致。</li>
<li>service discovery 註冊與摘除路徑可用。</li>
</ol>
<p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。</p>
<h3 id="preflight-自動化">Preflight 自動化</h3>
<p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：</p>
<ol>
<li><strong>image 與 config 版本對齊檢查</strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 <code>deploy/compat-matrix.yaml</code>），CI 自動比對。</li>
<li><strong>infra drift detection</strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。</li>
<li><strong>probe 語意驗證</strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。</li>
<li><strong>rollback 可行性驗證</strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。</li>
</ol>
<p>Preflight 自動化的產出是一份 go/no-go 報告，進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。</p>
<h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition</h2>
<p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。</p>
<table>
  <thead>
      <tr>
          <th>批次階段</th>
          <th>判讀重點</th>
          <th>停損條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-5%</td>
          <td>per-version error rate、p95/p99 latency</td>
          <td>錯誤率高於基線、延遲持續惡化</td>
      </tr>
      <tr>
          <td>10-25%</td>
          <td>payment dependency timeout、fallback 比例</td>
          <td>依賴 timeout 連續超門檻</td>
      </tr>
      <tr>
          <td>50%</td>
          <td>drain 成功率、reconnect 波形、下游事件完整性</td>
          <td>drain 未完成或 reconnect storm</td>
      </tr>
      <tr>
          <td>100% 前</td>
          <td>新舊版本差異是否收斂、rollback 可行性</td>
          <td>仍需依賴舊版本特殊路徑</td>
      </tr>
  </tbody>
</table>
<p>canary 判讀要維持 per-version 視角。只看整體服務平均值會掩蓋新版本局部退化。</p>
<h2 id="traffic--drain把退場變成可驗證流程">Traffic / Drain：把退場變成可驗證流程</h2>
<p>Drain 的責任是讓舊版本在下線前完成在途請求，不讓 rollout 把短暫切換放大成用戶錯誤。</p>
<p>退場順序：</p>
<ol>
<li>舊實例 readiness 先轉 <code>not-ready</code> 停接新流量。</li>
<li>保留 drain 窗口完成 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request。</li>
<li>確認連線數下降到門檻後再終止進程。</li>
<li>驗證無異常 reconnect 尖峰再進下一批。</li>
</ol>
<p>Drain 條件的完整 workload 分類回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>，本段以 checkout service 為例：短 API 的 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 窗口可短，長輪詢與 webhook callback 要更保守。</p>
<h2 id="rollback-compatibility">Rollback Compatibility</h2>
<p>舊版本回來時仍可運作，是 rollback 能成立的前提——回退如果變成第二次故障，就失去了回退的工程價值。</p>
<p>要先驗證四個相容面：</p>
<ol>
<li>config 相容：新設定不會讓舊版啟動失敗。</li>
<li>schema 相容：資料結構仍可被舊版讀取。</li>
<li>cache key 相容：舊版可讀新快取或有 fallback。</li>
<li>event schema 相容：舊版 consumer 不會因新事件欄位崩潰。</li>
</ol>
<p>若這四項未完成，所謂 rollback 只會停在「版本回切」，無法恢復服務正確性。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>每一批切換要可被判讀、可被追責、可被回放——部署 evidence 支撐這三個條件。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>deployment logs、LB metrics、service metrics、dependency logs</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>每批 rollout/drain 觀察窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>per-version error、latency、5xx、timeout、drain completion</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>platform owner、checkout owner、SRE on-call</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、分區覆蓋、log 掉點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>尚未覆蓋長連線場景、低流量區域樣本不足</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的責任是決定下一批切換與是否凍結 rollout，不是報告「目前看起來正常」。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批、維持 canary、freeze rollout、rollback version</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>per-version SLI、dependency timeout、drain completion</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>error <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、reconnect storm、drain 逾時</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>可回切時間、舊版可服務窗口、config 回退窗口</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>release owner、platform on-call</td>
      </tr>
  </tbody>
</table>
<p>這組欄位要對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>freeze rollout、rollback version、隔離 region、延長 drain 都屬事故決策，需寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。涉及流量規則 / <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> 設定推送的決策、見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7</a> 跟 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T15:06:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;freeze rollout at 25% and rollback one region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;new version timeout to payment provider increased in ap-northeast&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">checkout_error_rate_by_version_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">payment_timeout_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">release-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contain customer impact and restore baseline success rate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout ratio does not recover after rollback batch completes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫對齊 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>、<a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift</a> 與 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera</a>：前者看切換失序，後兩者看遷移路徑與回退策略。preflight / canary / drain 各階段的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>這篇不處理 schema migration 本身、cache stampede 或 queue replay。若核心風險在資料正式狀態、快取回源或事件恢復，路由到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <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。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。</p>
]]></content:encoded></item><item><title>2.C8 Meta：TAO 社交圖快取演進</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/</guid><description>&lt;p>這個案例的核心責任是說明快取在高關聯查詢場景會接近資料庫層角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta TAO 用於社交圖讀取，演進重點在一致性、可擴展性與資料關聯查詢效率。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當查詢負載是高度關聯圖資料，快取策略需從 key-value 轉向資料模型治理。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把資料關聯模型納入快取鍵設計。&lt;/li>
&lt;li>以一致性窗口設計更新策略。&lt;/li>
&lt;li>定期驗證讀取正確性與延遲目標。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/">TAO: Facebook&amp;rsquo;s Distributed Data Store&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取在高關聯查詢場景會接近資料庫層角色。</p>
<h2 id="觀察">觀察</h2>
<p>Meta TAO 用於社交圖讀取，演進重點在一致性、可擴展性與資料關聯查詢效率。</p>
<h2 id="判讀">判讀</h2>
<p>當查詢負載是高度關聯圖資料，快取策略需從 key-value 轉向資料模型治理。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把資料關聯模型納入快取鍵設計。</li>
<li>以一致性窗口設計更新策略。</li>
<li>定期驗證讀取正確性與延遲目標。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a> 與 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/">TAO: Facebook&rsquo;s Distributed Data Store</a></li>
</ul>
]]></content:encoded></item><item><title>3.C8 Cloudflare：Queues 全球交付模型</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/</guid><description>&lt;p>這個案例的核心責任是把 queue 選型從單區域傳遞提升為全球交付治理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Cloudflare Queues 以邊緣網路為背景，提供事件傳遞與 consumer 處理能力。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>全球部署下，queue 模型要同時考慮延遲、重試語義與跨區運維一致性。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>明確設定 delivery semantics 與重試策略。&lt;/li>
&lt;li>把 consumer 行為與死信處理流程標準化。&lt;/li>
&lt;li>將 queue lag 與失敗率接入平台觀測。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4&lt;/a> 與 &lt;a href="https://tarrragon.github.io/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&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/introducing-cloudflare-queues/">Introducing Cloudflare Queues&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 queue 選型從單區域傳遞提升為全球交付治理。</p>
<h2 id="觀察">觀察</h2>
<p>Cloudflare Queues 以邊緣網路為背景，提供事件傳遞與 consumer 處理能力。</p>
<h2 id="判讀">判讀</h2>
<p>全球部署下，queue 模型要同時考慮延遲、重試語義與跨區運維一致性。</p>
<h2 id="策略">策略</h2>
<ol>
<li>明確設定 delivery semantics 與重試策略。</li>
<li>把 consumer 行為與死信處理流程標準化。</li>
<li>將 queue lag 與失敗率接入平台觀測。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a> 與 <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</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/introducing-cloudflare-queues/">Introducing Cloudflare Queues</a></li>
</ul>
]]></content:encoded></item><item><title>4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/</guid><description>&lt;p>這個案例的核心責任是把平台擴縮行為轉成可觀測治理問題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 在 Kubernetes 規模化過程強調動態擴縮，代表觀測系統需要追上容量與拓撲變化。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>若訊號模型無法反映動態叢集，告警與容量判讀容易失真。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>將叢集層指標與服務層指標分開治理。&lt;/li>
&lt;li>在擴縮流程中保留關鍵健康訊號。&lt;/li>
&lt;li>用回溯報表驗證擴縮與事故關聯。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台擴縮行為轉成可觀測治理問題。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 在 Kubernetes 規模化過程強調動態擴縮，代表觀測系統需要追上容量與拓撲變化。</p>
<h2 id="判讀">判讀</h2>
<p>若訊號模型無法反映動態叢集，告警與容量判讀容易失真。</p>
<h2 id="策略">策略</h2>
<ol>
<li>將叢集層指標與服務層指標分開治理。</li>
<li>在擴縮流程中保留關鍵健康訊號。</li>
<li>用回溯報表驗證擴縮與事故關聯。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb</a></li>
</ul>
]]></content:encoded></item><item><title>6.8 Release Gate 與變更節奏</title><link>https://tarrragon.github.io/blog/backend/06-reliability/release-gate/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/release-gate/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release gate&lt;/a> 是把放行決策從「看起來可以」變成「條件已經達成」的控制面。它的責任是把哪些變更可以進、哪些變更要等、哪些變更必須先補證據說清楚，擋住所有變更從來不是目標。當 gate 被寫成政策，團隊就能用同一套條件判斷 CI、SLO、migration、相容性與高風險時段。&lt;/p>
&lt;p>這個節點先處理節奏，再處理工具。先問變更是否應該放行，再問這次放行需要哪些訊號與檢查。當 gate 被看成節奏控制，讀者就會明白為什麼 freeze 是可靠性政策的一部分，視為例外會弱化整套節奏控制。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>release gate 的核心責任：把放行決策從個人判斷變成可驗證條件&lt;/li>
&lt;li>gate 類別：CI 通過、SLO 健康、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 餘額、migration 可逆、相容性檢查&lt;/li>
&lt;li>變更節奏：deploy frequency、batch size、change failure rate（DORA 四指標）&lt;/li>
&lt;li>freeze 條件：error budget 耗盡、事故進行中、高風險時段&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO&lt;/a> 的耦合：error budget 是 gate 的一個條件&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署&lt;/a> 的交接：gate 通過後 rollout 策略接手&lt;/li>
&lt;li>反模式：gate 流於形式、freeze 無 owner、緊急修復繞過 gate 變常態&lt;/li>
&lt;/ul>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>gate 的責任是把放行條件具體化。CI green 只代表測試通過，不代表服務可以安全進 production；SLO 健康只代表目前風險可接受，不代表任何變更都能繼續推；migration 可逆只代表退路存在，不代表已經證明回退完全無害。這些條件要一起看，才知道 gate 有沒有真的在做事。&lt;/p>
&lt;p>資料庫 migration 的 gate 要把 evidence 放回 rollout 階段判讀。Expand、backfill、cutover 與 contract 需要不同 checks：compatibility result、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a>、mismatch rate、replication lag、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 與 owner。完整欄位形狀可接到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據&lt;/a>。&lt;/p>
&lt;p>freeze 的責任是把風險攔住。當 error budget 耗盡、事故正在進行、或高風險時段已到時，freeze 不應該被視為拖延，而應該被視為維持可靠性的一種放行決策。這樣的政策，會比只看 CI 更接近真實的部署世界。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>gate 只看 CI green、不看 SLO / error budget / migration 可逆性&lt;/li>
&lt;li>emergency bypass 從例外變週常&lt;/li>
&lt;li>freeze 條件無 owner、沒人知道誰能解凍&lt;/li>
&lt;li>change failure rate 沒量、無法評估 gate 是否有效&lt;/li>
&lt;li>migration 沒做向後相容檢查、rollback 後資料不一致&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;p>Google 很適合用來看 gate 需要什麼政策語言，因為它把 SLO、error budget 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 連成一套治理系統。Stripe 則適合用來看交易場景下的 gate，因為 idempotency、canary 與 migration safety 會把放行和交易正確性綁在一起。Shopify 可以補峰值節奏，因為 BFCM 前的 gate 不只是測試通過，而是要確定高峰時仍能守住容量與隔離。&lt;/p>
&lt;p>Amazon 和 Meta 則提供更偏架構層的 gate 視角。前者告訴我們隔離邊界與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 會直接影響哪些變更可以放行，後者則顯示 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a> 變更如果沒有足夠的 gate，可能直接把整個區域或整個公司拖進事故。把這些案例一起看，gate 就不再只是 CI 的最後一步，而是整個變更節奏的控制面。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release gate</a> 是把放行決策從「看起來可以」變成「條件已經達成」的控制面。它的責任是把哪些變更可以進、哪些變更要等、哪些變更必須先補證據說清楚，擋住所有變更從來不是目標。當 gate 被寫成政策，團隊就能用同一套條件判斷 CI、SLO、migration、相容性與高風險時段。</p>
<p>這個節點先處理節奏，再處理工具。先問變更是否應該放行，再問這次放行需要哪些訊號與檢查。當 gate 被看成節奏控制，讀者就會明白為什麼 freeze 是可靠性政策的一部分，視為例外會弱化整套節奏控制。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>release gate 的核心責任：把放行決策從個人判斷變成可驗證條件</li>
<li>gate 類別：CI 通過、SLO 健康、<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 餘額、migration 可逆、相容性檢查</li>
<li>變更節奏：deploy frequency、batch size、change failure rate（DORA 四指標）</li>
<li>freeze 條件：error budget 耗盡、事故進行中、高風險時段</li>
<li>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO</a> 的耦合：error budget 是 gate 的一個條件</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a> 的交接：gate 通過後 rollout 策略接手</li>
<li>反模式：gate 流於形式、freeze 無 owner、緊急修復繞過 gate 變常態</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>gate 的責任是把放行條件具體化。CI green 只代表測試通過，不代表服務可以安全進 production；SLO 健康只代表目前風險可接受，不代表任何變更都能繼續推；migration 可逆只代表退路存在，不代表已經證明回退完全無害。這些條件要一起看，才知道 gate 有沒有真的在做事。</p>
<p>資料庫 migration 的 gate 要把 evidence 放回 rollout 階段判讀。Expand、backfill、cutover 與 contract 需要不同 checks：compatibility result、<a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、mismatch rate、replication lag、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 與 owner。完整欄位形狀可接到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
<p>freeze 的責任是把風險攔住。當 error budget 耗盡、事故正在進行、或高風險時段已到時，freeze 不應該被視為拖延，而應該被視為維持可靠性的一種放行決策。這樣的政策，會比只看 CI 更接近真實的部署世界。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>gate 只看 CI green、不看 SLO / error budget / migration 可逆性</li>
<li>emergency bypass 從例外變週常</li>
<li>freeze 條件無 owner、沒人知道誰能解凍</li>
<li>change failure rate 沒量、無法評估 gate 是否有效</li>
<li>migration 沒做向後相容檢查、rollback 後資料不一致</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<p>Google 很適合用來看 gate 需要什麼政策語言，因為它把 SLO、error budget 與 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 連成一套治理系統。Stripe 則適合用來看交易場景下的 gate，因為 idempotency、canary 與 migration safety 會把放行和交易正確性綁在一起。Shopify 可以補峰值節奏，因為 BFCM 前的 gate 不只是測試通過，而是要確定高峰時仍能守住容量與隔離。</p>
<p>Amazon 和 Meta 則提供更偏架構層的 gate 視角。前者告訴我們隔離邊界與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 會直接影響哪些變更可以放行，後者則顯示 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> 變更如果沒有足夠的 gate，可能直接把整個區域或整個公司拖進事故。把這些案例一起看，gate 就不再只是 CI 的最後一步，而是整個變更節奏的控制面。</p>
<p><a href="/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">Stripe 的 canary deploy 實踐</a>把金流場景的 progressive rollout 跟交易指標綁在一起：每一批放量用 checkout success rate、duplicate charge、退款率判斷是否安全。金流的 feedback loop 比一般功能長（結帳 → 確認 → 對帳 → 退款），觀察窗必須對齊這個延遲。</p>
<h2 id="gate-類別">gate 類別</h2>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>作用</th>
          <th>常見例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI 通過</td>
          <td>確認基礎測試與 artifact 可重播</td>
          <td>unit / integration / lint</td>
      </tr>
      <tr>
          <td>SLO 健康</td>
          <td>確認服務健康仍在可接受區間</td>
          <td><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、error budget</td>
      </tr>
      <tr>
          <td>Migration 可逆</td>
          <td>確認 schema / data 變更有退路</td>
          <td>forward / backward compatibility</td>
      </tr>
      <tr>
          <td>相容性檢查</td>
          <td>確認上下游協議與資料不會互相打架</td>
          <td>contract / schema checks</td>
      </tr>
      <tr>
          <td>高風險時段凍結</td>
          <td>確認人在、窗在、風險可控</td>
          <td>freeze window、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> presence</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是每一類都要對應 owner 與回退條件，分類只是組織方式。沒有回退條件的 gate，只是心理安慰。</p>
<h2 id="變更分層跟-gate-政策">變更分層跟 gate 政策</h2>
<p>變更分層是把變更依失敗代價跟回退成本切成不同 gate 政策的控制面。讓高風險變更承受高 gate 成本、低風險變更不被高成本拖累、是分層治理的核心責任。可重複套用的做法是先做變更分層、再對應分層 gate 政策。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">MS1 Microsoft 變更治理與可靠性門檻</a>：揭露「變更分層 + 漸進發布 + 復盤回寫」三個機制、適用大型 SaaS 高頻變更累積回歸的場景。對應 <a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2 Google Postmortem AI Closure</a>：揭露 P0/P1 action item 必須綁定 release gate、未完成不得放行關聯變更（這層綁定讓 gate 從 release 工具升級為事故治理工具）。詳見 <a href="/blog/backend/06-reliability/reliability-debt-backlog/#action-item-%e5%88%86%e7%b4%9a%e8%b7%9f-release-gate-%e7%b6%81%e5%ae%9a" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Action Item 分級跟 Release Gate 綁定</a>。</p>
<p>可操作的分層方法：</p>
<ul>
<li>低風險變更（配置微調、文案、UI 細節）：CI green + SLO 健康 即可放行</li>
<li>中風險變更（新 feature、依賴升級）：加 canary + per-version SLI 偏差檢查</li>
<li>高風險變更（schema migration、payment / auth 路徑、跨 region rollout）：加 evidence package + 高風險時段 freeze + P0 action item closure 檢查</li>
</ul>
<p>高風險層的三類變更要拆開治理、彼此 gate 機制不同：schema migration 的 gate 重點是 expand/contract 階段對齊跟 rollback 路徑（詳見 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration-safety</a>）；跨 region rollout 的 gate 重點是 ordered failover 跟 blast radius 限制（詳見 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency-reliability-budget</a>）；payment / auth 路徑的 gate 重點在交易一致性跟 idempotency（詳見後段「交易類變更的 gate 設計」跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency-replay</a>）。三者皆屬高風險、但失敗模式跟回退路徑完全不同。</p>
<p>分層後高風險變更得到匹配的 gate 強度、低風險變更不被拖累、整體交付節奏跟可靠性同步提升。</p>
<h2 id="交易類變更的-gate-設計">交易類變更的 gate 設計</h2>
<p>交易類變更的 gate 同時承擔可用性跟正確性兩條軸。除了服務健康（一般 gate 已覆蓋）、還要守住交易結果一致性；回退條件也要多看一層：rollback 是否會觸發資料不一致。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Stripe Idempotency 與零停機遷移</a>：揭露 idempotency key + expand/contract migration + canary + rollback gate + transaction observability 四機制組合、適用支付類「可用性 + 正確性同時守住」的場景。</p>
<p>交易類變更的 gate 跟一般 release gate 差別在：</p>
<ul>
<li>一般 release gate 看「服務是否健康」、交易類 gate 還要看「交易結果是否一致」</li>
<li>一般 release gate 看「回退是否可行」、交易類 gate 要看「回退是否會引發資料不一致」</li>
<li>一般 release gate 看「per-version SLI 偏差」、交易類 gate 要看「duplicate request collapse ratio」「migration phase error drift」「canary transaction anomaly」這類交易專屬訊號</li>
</ul>
<p>把交易類變更的 gate 從一般 release gate 分出來、寫進獨立 checklist、由 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency-replay</a> 跟 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration-safety</a> 提供具體欄位。</p>
<h2 id="產業情境金融科技">產業情境：金融科技</h2>
<p>金融服務的 release gate 需要把交易正確性放在跟可用性同等的位置。一般 SaaS 的 gate 主要看 error rate 和 latency；金融服務的 gate 需要加上 duplicate detection、settlement 一致性與 compliance audit trail。</p>
<p>變更風險分層跟交易路徑綁定。碰到 payment path 的變更（provider 切換、timeout 調整、retry 策略、settlement 流程）自動升級到高風險 gate，不論變更看起來多小。payment path 的變更即使只改一個 timeout 值，也可能影響交易成功率、重試行為與對帳結果。</p>
<p>Gate 通過條件需要包含交易專屬欄位。<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 驗證確認重試不會產生重複扣款；reconciliation 通過確認結算數字一致；audit trail 完整確認每個決策都可追溯。這三項跟一般的 CI green / SLO healthy 是不同維度的檢查，需要獨立 checklist。</p>
<p>高風險變更的 canary 觀察窗需要涵蓋結算週期。一般 feature rollout 的觀察窗是分鐘到小時級；金融變更的觀察窗需要涵蓋 T+1（隔日結算）甚至 T+2，因為交易確認延遲、退款申請與對帳差異可能在數小時到數天後才暴露。觀察窗太短會讓問題在全量放行後才被發現。</p>
<p>Rollback 決策需要考慮已完成交易的一致性。當新版已處理交易且交易已進入結算流程，rollback 可能比繼續 roll-forward 更危險 — 退回舊版的 schema / 邏輯可能無法正確處理新版產生的交易紀錄。這個判斷跟 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 rollback vs roll-forward 的判斷條件</a> 對齊，但金融場景的資料不可逆性更高。</p>
<h2 id="產業情境iot-與製造系統">產業情境：IoT 與製造系統</h2>
<p>IoT 的 release gate 需要處理「一旦推出就難以全面回收」的不可逆壓力。雲端服務的 deploy 可以秒級 rollback；IoT firmware 一旦推送到裝置，回收需要每台裝置個別 OTA，受限於連線狀態與頻寬。</p>
<p>裝置碎片化要求 gate 按硬體版本分群驗證。同一產品線可能有多個硬體版本（rev A / B / C），每個版本的 firmware 相容性不同。release gate 需要按硬體版本群組各自跑 checks，通過的群組才放行推送，不能全域一次放行。</p>
<p>IoT 的 canary 是按裝置群組分批推送，而非按流量百分比分流。推送順序通常是：內部測試裝置 → beta 用戶 → 特定區域 → 全域。每批的觀察窗需要比雲端更長（天到週），因為裝置的 failure mode 可能在特定環境條件下才觸發 — 溫度、濕度、網路品質、電力穩定度都是變數。</p>
<p>OTA 推送一旦開始，中途停止意味著部分裝置已更新、部分未更新。stop condition 需要同時監控「已更新裝置的健康度」和「混合版本之間的相容性」。若新舊版本的通訊協議不相容，部分更新的裝置群可能會觸發新的 failure mode。</p>
<p>安全關鍵系統（車載、醫療設備、工業控制）的 gate 需要額外的功能安全驗證（IEC 61508 / ISO 26262 等），通過合規驗證是放行的前置條件。這類 gate 的 owner 通常跨越工程與合規兩個團隊。</p>
<p><a href="/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">Amazon A2 的 static stability</a> 跟 IoT 的離線運作需求對齊 — 裝置在控制面（OTA server）不可用時，用本地快取的配置繼續運作，回復路徑不依賴已故障的控制面。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：CI evidence 是 release gate 的主要輸入</li>
<li>05 部署：canary / progressive delivery 實作</li>
<li>06.6 SLO：error budget 餘額查詢</li>
<li>06.10 contract testing：契約通過作為放行條件</li>
<li>06.11 migration safety：可逆性檢查</li>
<li>01.7 Schema Migration Rollout 證據：把 migration evidence 轉成 <a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision</a>、checks、<a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a></li>
<li>06.13 perf regression gate：退化作為 freeze 條件</li>
<li>07 資安：高風險變更的權限約束</li>
<li>08 事故閉環：事故進行中 freeze 觸發</li>
<li>06.17 feature flag：rollout 的細粒度控制層</li>
<li>06.18 reliability metrics：CFR 是 gate 健康度</li>
</ul>
]]></content:encoded></item><item><title>GCP Cloud Operations</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/</guid><description>&lt;p>GCP Cloud Operations（前 Stackdriver）是 GCP 原生 observability 套件、承擔三個責任：GCP 服務內建 Cloud Logging / Monitoring / Trace（無需配置）、跟 GCP 資源 model 深度整合（project / folder / org）、BigQuery 匯出長期 logs 跟分析。設計取捨偏向「GCP 生態 turnkey + BigQuery 整合 + Cloud Profiler 持續 profiling」、跨雲跟進階 distributed tracing 是限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 gcloud / Console 查 Cloud Logging / Monitoring&lt;/li>
&lt;li>設計 structured logging + log-based metrics&lt;/li>
&lt;li>用 Cloud Monitoring uptime checks + SLO + alerting policy&lt;/li>
&lt;li>用 Cloud Trace + Cloud Profiler 做 application performance&lt;/li>
&lt;li>配置 BigQuery 匯出長期 logs 跟分析&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-cloud-operations-跑起來">最短路徑：5 分鐘把 Cloud Operations 跑起來&lt;/h2>





&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"># 1. GCP 預設啟用 Cloud Logging / Monitoring（free tier 額度）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: GKE / Cloud Run / Cloud Functions 自動 log + metric&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="c1"># 2. 查詢 logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: gcloud logging read &amp;#39;resource.type=&amp;#34;gae_app&amp;#34; AND severity&amp;gt;=ERROR&amp;#39;&lt;/span>
&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="c1"># 3. 用 Logs Explorer 視覺化查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: Console → Logging → Logs Explorer&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cloud-logging-結構化-logs">Cloud Logging 結構化 logs&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>jsonPayload：結構化 log（推薦）&lt;/li>
&lt;li>Severity 7 級（DEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT）&lt;/li>
&lt;li>Resource type / Resource labels：自動帶入&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="log-based-metrics">Log-based metrics&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Counter metric：log 出現次數&lt;/li>
&lt;li>Distribution metric：log field 數值分布&lt;/li>
&lt;li>適合：把 application log 轉成 metric trigger alert&lt;/li>
&lt;li>對應指令：&lt;code>gcloud logging metrics create&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="cloud-monitoring-uptime-checks--slo">Cloud Monitoring uptime checks / SLO&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>GCP Cloud Operations（前 Stackdriver）是 GCP 原生 observability 套件、承擔三個責任：GCP 服務內建 Cloud Logging / Monitoring / Trace（無需配置）、跟 GCP 資源 model 深度整合（project / folder / org）、BigQuery 匯出長期 logs 跟分析。設計取捨偏向「GCP 生態 turnkey + BigQuery 整合 + Cloud Profiler 持續 profiling」、跨雲跟進階 distributed tracing 是限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 gcloud / Console 查 Cloud Logging / Monitoring</li>
<li>設計 structured logging + log-based metrics</li>
<li>用 Cloud Monitoring uptime checks + SLO + alerting policy</li>
<li>用 Cloud Trace + Cloud Profiler 做 application performance</li>
<li>配置 BigQuery 匯出長期 logs 跟分析</li>
</ol>
<h2 id="最短路徑5-分鐘把-cloud-operations-跑起來">最短路徑：5 分鐘把 Cloud Operations 跑起來</h2>





<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. GCP 預設啟用 Cloud Logging / Monitoring（free tier 額度）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: GKE / Cloud Run / Cloud Functions 自動 log + metric</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="c1"># 2. 查詢 logs</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: gcloud logging read &#39;resource.type=&#34;gae_app&#34; AND severity&gt;=ERROR&#39;</span>
</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="c1"># 3. 用 Logs Explorer 視覺化查詢</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: Console → Logging → Logs Explorer</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cloud-logging-結構化-logs">Cloud Logging 結構化 logs</h3>
<p>子議題：</p>
<ul>
<li>jsonPayload：結構化 log（推薦）</li>
<li>Severity 7 級（DEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT）</li>
<li>Resource type / Resource labels：自動帶入</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></li>
</ul>
<h3 id="log-based-metrics">Log-based metrics</h3>
<p>子議題：</p>
<ul>
<li>Counter metric：log 出現次數</li>
<li>Distribution metric：log field 數值分布</li>
<li>適合：把 application log 轉成 metric trigger alert</li>
<li>對應指令：<code>gcloud logging metrics create</code></li>
</ul>
<h3 id="cloud-monitoring-uptime-checks--slo">Cloud Monitoring uptime checks / SLO</h3>
<p>子議題：</p>
<ul>
<li>Uptime check：HTTP / HTTPS / TCP / ICMP 多地點 probe</li>
<li>SLO：service indicator + objective + window + burn rate alert</li>
<li>Multi-window SLO alert（類 Honeycomb burn rate）</li>
<li>對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a></li>
</ul>
<h3 id="cloud-trace">Cloud Trace</h3>
<p>子議題：</p>
<ul>
<li>接受 OTLP（Cloud Trace 2.0+）</li>
<li>自動採集 GCP service（Cloud Run / GKE / App Engine）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP adoption</a></li>
<li>跟 X-Ray 比、distributed tracing 較基礎</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="cloud-monitoring-mql/">Cloud Monitoring Metrics Model 與 MQL</a>：GCP metrics model、MQL vs PromQL、custom metrics 設計、alerting policy 與 Managed Prometheus 整合</li>
<li><a href="cloud-logging-export-compliance/">Cloud Logging 查詢、匯出與合規</a>：查詢語言、log router / sink 匯出、retention 設計、organization-level 聚合、audit log 與 PII / CMEK 合規治理</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="cloud-profiler">Cloud Profiler</h3>
<p>子議題：</p>
<ul>
<li>持續 profiling（CPU / Heap / Wall time / Mutex）</li>
<li>支援 Go / Java / Python / Node</li>
<li>Flame graph 視覺化</li>
<li>跟 Pyroscope / Datadog Profiler 對照</li>
</ul>
<h3 id="bigquery-匯出長期儲存">BigQuery 匯出長期儲存</h3>
<p>子議題：</p>
<ul>
<li>Log Router：定義 sink 把 logs 匯出 BigQuery / GCS / Pub/Sub</li>
<li>BigQuery 適合長期 + 分析查詢（SQL）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
<li>Cost：BigQuery storage 比 Cloud Logging cheaper</li>
</ul>
<h3 id="error-reporting">Error Reporting</h3>
<p>子議題：</p>
<ul>
<li>自動聚合 application error</li>
<li>各語言 client library（Python / Java / Node / Go）</li>
<li>跟 Sentry 對照（Sentry 更深 / 更廣）</li>
</ul>
<h3 id="cloud-monitoring-agent">Cloud Monitoring agent</h3>
<p>子議題：</p>
<ul>
<li>Ops Agent（取代 Stackdriver agent）：統一 logs + metrics 採集</li>
<li>支援 GCE / Bare metal / AWS / on-prem</li>
<li>配置：YAML config + receivers / processors / exporters（類 OTel Collector）</li>
</ul>
<h3 id="multi-project--multi-region-治理">Multi-project / Multi-region 治理</h3>
<p>子議題：</p>
<ul>
<li>Aggregated logging sink：跨 project 集中 logs</li>
<li>Cross-project SLO</li>
<li>Workspace（前 Stackdriver workspace）已 deprecated、改用 Metrics Scope</li>
</ul>
<h3 id="otlp-integration">OTLP integration</h3>
<p>子議題：</p>
<ul>
<li>Cloud Trace 接受 OTLP（2024 GA）</li>
<li>Cloud Monitoring 接受 OTel metrics（via OTel Collector + GCP exporter）</li>
<li>Logs in OTel 跟 Cloud Logging 整合（成熟中）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="logs-沒出現">Logs 沒出現</h3>
<p>操作原則：先看 resource type / project 是否對、再看 IAM 權限。</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"># TODO: gcloud logging read --project=&lt;id&gt; --resource-type=...</span></span></span></code></pre></div><h3 id="monitoring-查不到-metric">Monitoring 查不到 metric</h3>
<p>操作原則：metric name + project + filter 是否對。對應 Metrics Explorer 確認 metric 存在。</p>
<h3 id="slo-alert-noise">SLO alert noise</h3>
<p>操作原則：multi-window burn rate 設計避免噪音。</p>
<h3 id="cloud-trace-太空">Cloud Trace 太空</h3>
<p>操作原則：sampling 不足或 SDK 沒配置。判讀：Cloud Trace 看 span count + 確認 SDK Cloud Trace exporter 設定。</p>
<h3 id="bigquery-匯出-cost-爆">BigQuery 匯出 cost 爆</h3>
<p>操作原則：sink filter 沒收斂、所有 logs 都匯。判讀：Cloud Logging usage 看 export volume。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多雲統一觀測</td>
          <td><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> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a></td>
      </tr>
      <tr>
          <td>進階 APM 廣度</td>
          <td><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></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>Logs full-text 進階</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a> / Loki</td>
      </tr>
      <tr>
          <td>AWS / Azure 生態</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / Azure Monitor</td>
      </tr>
      <tr>
          <td>Error tracking 進階</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>gcloud / Cloud Console UI 操作詳細</li>
<li>各 GCP 服務的內建 metric 完整列表</li>
<li>Cloud Trace span structure 細節</li>
<li>BigQuery SQL syntax</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></td>
          <td>OTLP 在 GCP 的採用路徑</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Cloud Operations 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Cloud Logging + BigQuery 作為審計證據與長期分析</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>BigQuery 匯出長期 retention</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Cloud Trace ↔ OTLP 雙軌語意對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>GCP-only 場景優先 Cloud Operations</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>Instatus</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/instatus/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/instatus/</guid><description>&lt;p>Instatus 是輕量 status page SaaS、承擔三個責任：簡潔現代 UI 的 status page、component + incident management、跟 IR 工具整合（incident.io / Rootly / FireHydrant）。設計取捨偏向「價格親民 + UI 現代 + 中小團隊適用」、是 Atlassian Statuspage 的 budget-friendly 替代。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Instatus 主打 &lt;em>fast + cheap + custom domain&lt;/em>、產品形狀直接對標 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &amp;#43; Atlassian 生態整合、subscriber notification &amp;#43; component dependency 是核心責任">Atlassian Statuspage&lt;/a> 的核心功能（component / incident / subscriber / custom domain），但價格約 1/3-1/5、free tier 就包含 custom domain SSL。typical 客戶是中小 SaaS、indie hacker / 個人 project、不需要 enterprise SLA 但要對外呈現專業感的團隊；不適合需要 audit log、SAML SSO、複雜 access role、SLA 報表的大企業 — 那是 Statuspage / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant&lt;/a> status 模組的場域。&lt;/p>
&lt;p>Instatus 的取捨設計：UI 走 &lt;em>modern + minimal&lt;/em>、頁面 load 快（自稱 ~50ms）、subscriber notification provider 多元（Email / SMS / Slack / Discord / Teams / Telegram / RSS / Webhook），用 &lt;em>generous free tier&lt;/em> 拉初期用戶、進階功能（更多 component、更多 subscriber、white-label、SLA report）走分層 pricing。&lt;/p>
&lt;p>關鍵張力：&lt;em>cheap + custom domain from free tier&lt;/em> ↔ &lt;em>enterprise governance（SAML / audit / role）&lt;/em>。Instatus 故意把 enterprise governance 砍掉以壓 pricing、所以團隊規模成長到需要區分多角色 / 留 audit trail 時、會撞到產品天花板、要評估遷移。提早估算 &lt;em>什麼時候撞到天花板&lt;/em> 比事故當下才發現省事很多。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>建 Instatus + 設 component&lt;/li>
&lt;li>寫 incident template + update&lt;/li>
&lt;li>配置 subscriber notification&lt;/li>
&lt;li>API 從 IR 平台 push&lt;/li>
&lt;li>評估 Instatus vs Statuspage / Cachet&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Instatus 是否健康承載對外狀態揭露、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>誰能 publish update&lt;/strong>：team member 角色設計（admin / member / read-only）、incident update 是否走 PR / approval、誤發 update 的回收路徑（edit / delete + email correction）&lt;/li>
&lt;li>&lt;strong>Component 數量 vs pricing tier&lt;/strong>：current tier 的 component limit、現有 / 規劃中的 component 數、跨 tier 切換的成本影響（升 tier 還是合併 component）&lt;/li>
&lt;li>&lt;strong>Custom domain SSL&lt;/strong>：&lt;code>status.example.com&lt;/code> 的 CNAME 是否生效、SSL cert 自動 renew 是否健康（Instatus 用 Let&amp;rsquo;s Encrypt 自動簽發、需在 DNS 加 CAA record 授權）、未來 domain 變更的遷移流程&lt;/li>
&lt;li>&lt;strong>Subscriber notification 健康度&lt;/strong>：subscriber 數量是否逼近 tier 限制、Email / SMS provider quota / bounce rate、Slack / Discord webhook 是否還有效&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是事故揭露通道有風險、應該優先補完。&lt;/p></description><content:encoded><![CDATA[<p>Instatus 是輕量 status page SaaS、承擔三個責任：簡潔現代 UI 的 status page、component + incident management、跟 IR 工具整合（incident.io / Rootly / FireHydrant）。設計取捨偏向「價格親民 + UI 現代 + 中小團隊適用」、是 Atlassian Statuspage 的 budget-friendly 替代。</p>
<h2 id="服務定位">服務定位</h2>
<p>Instatus 主打 <em>fast + cheap + custom domain</em>、產品形狀直接對標 <a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> 的核心功能（component / incident / subscriber / custom domain），但價格約 1/3-1/5、free tier 就包含 custom domain SSL。typical 客戶是中小 SaaS、indie hacker / 個人 project、不需要 enterprise SLA 但要對外呈現專業感的團隊；不適合需要 audit log、SAML SSO、複雜 access role、SLA 報表的大企業 — 那是 Statuspage / <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> status 模組的場域。</p>
<p>Instatus 的取捨設計：UI 走 <em>modern + minimal</em>、頁面 load 快（自稱 ~50ms）、subscriber notification provider 多元（Email / SMS / Slack / Discord / Teams / Telegram / RSS / Webhook），用 <em>generous free tier</em> 拉初期用戶、進階功能（更多 component、更多 subscriber、white-label、SLA report）走分層 pricing。</p>
<p>關鍵張力：<em>cheap + custom domain from free tier</em> ↔ <em>enterprise governance（SAML / audit / role）</em>。Instatus 故意把 enterprise governance 砍掉以壓 pricing、所以團隊規模成長到需要區分多角色 / 留 audit trail 時、會撞到產品天花板、要評估遷移。提早估算 <em>什麼時候撞到天花板</em> 比事故當下才發現省事很多。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>建 Instatus + 設 component</li>
<li>寫 incident template + update</li>
<li>配置 subscriber notification</li>
<li>API 從 IR 平台 push</li>
<li>評估 Instatus vs Statuspage / Cachet</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Instatus 是否健康承載對外狀態揭露、最少看四件事：</p>
<ul>
<li><strong>誰能 publish update</strong>：team member 角色設計（admin / member / read-only）、incident update 是否走 PR / approval、誤發 update 的回收路徑（edit / delete + email correction）</li>
<li><strong>Component 數量 vs pricing tier</strong>：current tier 的 component limit、現有 / 規劃中的 component 數、跨 tier 切換的成本影響（升 tier 還是合併 component）</li>
<li><strong>Custom domain SSL</strong>：<code>status.example.com</code> 的 CNAME 是否生效、SSL cert 自動 renew 是否健康（Instatus 用 Let&rsquo;s Encrypt 自動簽發、需在 DNS 加 CAA record 授權）、未來 domain 變更的遷移流程</li>
<li><strong>Subscriber notification 健康度</strong>：subscriber 數量是否逼近 tier 限制、Email / SMS provider quota / bounce rate、Slack / Discord webhook 是否還有效</li>
</ul>
<p>四件事任一缺失、就是事故揭露通道有風險、應該優先補完。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="component--incident--subscriber">Component / incident + Subscriber</h3>
<p>Component 是對外揭露單位、status（operational / degraded / partial outage / major outage / maintenance）的抽象顆粒度影響事故揭露的 <em>精準度</em> — 拆太細用戶看不懂、太粗反而失真。實務上跟內部 service map 對齊但 <em>外部可理解語言</em>、例如「Web App」「API」「Login」「Webhooks」、而不是內部 microservice 名稱。</p>
<p>子議題：</p>
<ul>
<li>Component status（跟 Statuspage 相似、操作 surface 簡潔）</li>
<li>Incident template + maintenance window（pre-defined template 讓事故 update 走標準格式、避免臨場寫錯）</li>
<li>Email / SMS / Slack / RSS / Discord / Teams / Telegram / Webhook subscriber、各 channel 的 quota / 失敗模式不同</li>
</ul>
<h3 id="api--ir-整合">API + IR 整合</h3>
<p>REST API 用 token 認證、可程式化 create incident / update / resolve / 改 component status。典型整合：incident.io / Rootly / <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> 觸發事故後同步推 Instatus、避免 SOC / on-call 還要手動雙寫。webhook 也支援反向通知、Instatus 上的 incident 變更通知到 IR 平台。</p>
<p>token 是高權限資源（任何持有 token 的 caller 可對外發布 incident）、應該存在 secrets manager、不放程式碼 / 環境變數明文、定期 rotate；CI / IR 平台用獨立 token、出事可單獨 revoke 不影響其他整合。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Instatus</th>
          <th>Atlassian Statuspage</th>
          <th>Better Stack Status</th>
          <th>Cachet (OSS)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>分層 SaaS、free tier 含 custom domain</td>
          <td>分層 SaaS、custom domain 需付費 tier</td>
          <td>分層 SaaS、跟 monitoring 綁</td>
          <td>OSS 自管、零 license 成本</td>
      </tr>
      <tr>
          <td>UI / 速度</td>
          <td>現代 + 快（~50ms load）</td>
          <td>成熟但偏重</td>
          <td>現代、跟 monitoring 整合</td>
          <td>基本、視自管 stack</td>
      </tr>
      <tr>
          <td>Custom domain</td>
          <td>free tier 即支援、auto SSL</td>
          <td>付費 tier、auto SSL</td>
          <td>付費 tier</td>
          <td>自架 + 自管 cert</td>
      </tr>
      <tr>
          <td>Subscriber</td>
          <td>Email / SMS / Slack / Discord / Teams / Telegram / RSS / Webhook</td>
          <td>同類但部分需高 tier</td>
          <td>Email / Slack 為主</td>
          <td>自實作</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>中小 SaaS / indie hacker / 個人 project</td>
          <td>Enterprise + 跨團隊治理</td>
          <td>已用 Better Stack monitoring</td>
          <td>嚴格資料自管、零外部 SaaS</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — 標準 component / incident 結構</td>
          <td>中</td>
          <td>中</td>
          <td>高 — 自管 ops</td>
      </tr>
  </tbody>
</table>
<p>選 Instatus 的核心訴求：<em>cheap + fast UI + custom domain 從 free tier 就有</em>、且不需要 enterprise SLA / SAML / audit 報表。組織成長到要 SAML SSO / multi-team approval / SLA report 時、再評估遷移到 Statuspage 或 IR 平台內建 status。</p>
<p>遷移成本：標準 component / incident 結構讓 Instatus → Statuspage 的搬遷相對單純（資料模型一致、subscriber 列表可匯出）、但 <em>subscriber 重新確認 opt-in</em> 通常是最大痛點 — 切換 domain / provider 時、許多 email subscriber 不會自動轉移、要走再次訂閱流程。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="custom-css--branding--multi-language">Custom CSS + branding + Multi-language</h3>
<p><code>status.example.com</code> 走 CNAME 指到 Instatus 配發的 host、SSL 由 Instatus 透過 Let&rsquo;s Encrypt 自動簽發 + renew、不用自己管 cert。custom CSS / logo 在中高 tier 開放、可改色票 / 字型 / layout、適合需要跟主站視覺一致的 SaaS；不要為了美觀過度客製、status page 第一順位是 <em>清楚揭露事故</em>、視覺只是輔助。</p>
<p>multi-language 支援同一 incident 用多語 update、適合對外服務跨地區用戶。注意 <em>誰負責翻譯</em> — 事故當下沒人有空一條條翻、實務上 incident update 寫英文 + 主要語言、其餘語言用 fallback 或事後補。</p>
<h3 id="ir-平台-auto-create-incident">IR 平台 auto-create incident</h3>
<p>Instatus 提供 REST API + webhook、典型整合是 IR 平台偵測事故後 <em>自動 create + update</em> status page incident、收尾時 <em>自動 resolve</em>。常見 pattern：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> 觸發 high-severity alert → webhook → Instatus API create incident → resolve 時同步收尾。</p>
<p>要點是 <em>誰是 SSoT</em>：incident timeline 由 IR 平台維護、Instatus 是對外揭露 view、不能讓 status page 變第二份 timeline 否則兩邊會漂移。實務上對外揭露的 update 是 IR timeline 的 <em>過濾子集</em>（去掉內部 root cause / 人名 / 攻擊細節）、不是原文同步。</p>
<h3 id="metrics-公開">Metrics 公開</h3>
<p>子議題：uptime / response time、從 monitor source（如外部 uptime monitor、或自家 metrics）拉資料、決定哪些 metric 對外揭露。揭露太細（例：每個 endpoint p99）會讓潛在攻擊者 reverse-engineer attack surface 跟容量上限；只揭露用戶感受得到的 SLI（前台 availability / API success rate）通常足夠、敏感內部指標留在內部 dashboard。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<ul>
<li><strong>Subscriber 沒收到</strong>：跟 Statuspage 類似、provider quota / bounce / spam filter；SMS 在某些地區需要區號白名單；事故當下若大量 subscriber 同時收到 alert、Email provider 可能短時間 throttle、要留 buffer</li>
<li><strong>Custom domain 失效</strong>：DNS CNAME 設定錯 / Let&rsquo;s Encrypt 簽發失敗（CAA record 衝突、需在 DNS 加 <code>letsencrypt.org</code> 授權）/ SSL renew 卡住 — 事故發生時才發現 cert 過期是最常見的二次事故</li>
<li><strong>API 失敗</strong>：rate limit / token 失效 / webhook signature 驗證錯誤；高 severity 事故時 IR 平台可能短時間發大量 update、要確認 rate limit 不會把 update 卡住</li>
<li><strong>Pricing tier 切換成本</strong>：升 tier 取得更多 component / subscriber、但降 tier 可能要先刪 component 或 subscriber 才生效、規劃要先估好成長曲線</li>
<li><strong>Subscriber list 上限</strong>：tier 有 subscriber 上限、逼近時要嘛升 tier、要嘛清理 inactive subscriber（長期 bounce / unsubscribe）；不要等到滿了才處理、新 subscriber 註冊失敗會直接傷品牌信任</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise SLA / SAML SSO / audit</td>
          <td><a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a></td>
      </tr>
      <tr>
          <td>OSS 自管 / 嚴格資料留在自家環境</td>
          <td>Cachet</td>
      </tr>
      <tr>
          <td>IR 平台內建 status</td>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
      </tr>
      <tr>
          <td>Alert / on-call SSoT</td>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 API reference / Pricing 細節 / Custom CSS 範本</li>
<li>SLA report 設計（Instatus 提供基本 uptime 計算、複雜 SLA 報表走 Statuspage 或 IR 平台）</li>
<li>Status page 對外揭露的法務 / 合約義務（合約 SLA、credit 計算）— 屬法務 / 商務、不在本頁</li>
<li>IR timeline 設計本身（誰寫、誰簽 — 屬 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a> 的範圍）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>Instatus 主打輕量、低成本公開狀態頁</strong>：本案例庫的案例多為大型平台、以 Atlassian Statuspage 揭露事故；Instatus 缺乏直接 vendor-level case、可參照的閱讀脈絡是「事故對外揭露的最小可行樣式」、特別適合中小 SaaS 跟 indie 開發者拿來對照自家 status page 的最低門檻。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對應主題</th>
          <th>對 Instatus 用戶的啟示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">Heroku cases</a></td>
          <td>平台型服務的 component 拆分與訂閱範例</td>
          <td>component 拆分顆粒度可借鏡（Web / API / Build / Dyno）、中小 SaaS 不需要拆到 region 等級、但要分前後台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">Discord cases</a></td>
          <td>事件導向產品的最小事故時序揭露對照</td>
          <td>incident update 節奏 — 第一則確認、後續更新、resolve 收尾、indie 級服務也至少跑這三段、不能 silent recovery</td>
      </tr>
  </tbody>
</table>
<p>待補 candidate：從 Statuspage 遷移至 Instatus 的中小型 SaaS cost-saving story、indie hacker 個人 project 從零搭 status page 的最小配置（含 custom domain + 一個 component + 一個 incident template）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>（決定哪些 timeline event 該對外揭露）</li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a>、<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a>、<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a>、<a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a></li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>（事故結束後對外揭露的 timeline / post-mortem 整理）</li>
<li>跨類：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（一次看完 IR / status / on-call vendor map）</li>
</ul>
]]></content:encoded></item><item><title>LitmusChaos</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/litmuschaos/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/litmuschaos/</guid><description>&lt;p>LitmusChaos 是 CNCF graduated 的 Kubernetes chaos engineering 平台、承擔三個責任：ChaosHub experiment marketplace（現成 experiment 直接用）、ChaosWorkflow 編排多步驟實驗、Probe-based steady state validation。設計取捨偏向「現成 experiment 庫 + workflow-centric + CNCF graduated 治理」、是 Chaos Mesh 的近競品、Harness 提供商業版（ChaosNative）。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Litmus 到 K8s&lt;/li>
&lt;li>從 ChaosHub 引用現成 experiment&lt;/li>
&lt;li>寫 ChaosWorkflow（多步驟 + probe）&lt;/li>
&lt;li>設計 Probe（HTTP / Cmd / K8s / Prometheus）做 steady state&lt;/li>
&lt;li>評估 Litmus vs Chaos Mesh vs Gremlin 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-litmus-跑起來">最短路徑：5 分鐘把 Litmus 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: helm install litmus litmus/litmus -n litmus --create-namespace&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="c1"># 2. 從 ChaosHub 引用 experiment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: kubectl apply -f https://hub.litmuschaos.io/...&lt;/span>
&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="c1"># 3. 跑 experiment + 看 ChaosResult&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: kubectl apply -f chaosengine.yaml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: kubectl describe chaosresult &amp;lt;name&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="crd-設計">CRD 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>ChaosExperiment：experiment 定義&lt;/li>
&lt;li>ChaosEngine：bind experiment 到 target&lt;/li>
&lt;li>ChaosResult：執行結果&lt;/li>
&lt;/ul>
&lt;h3 id="chaoshub-experiment">ChaosHub experiment&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>現成 experiment marketplace&lt;/li>
&lt;li>Generic / Kafka / Cassandra / GCP / AWS / VMware experiments&lt;/li>
&lt;li>自訂 experiment 上傳 Hub&lt;/li>
&lt;/ul>
&lt;h3 id="chaosworkflow">ChaosWorkflow&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Argo Workflow-based&lt;/li>
&lt;li>多步驟 chaos 編排&lt;/li>
&lt;li>Schedule trigger&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="probe-based-steady-state">Probe-based steady state&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>HTTP probe / Cmd probe / K8s probe / Prometheus probe&lt;/li>
&lt;li>跟 chaos 同步 / 序列執行&lt;/li>
&lt;li>Success threshold 設計&lt;/li>
&lt;/ul>
&lt;h3 id="chaoscentercontrol-plane">ChaosCenter（control plane）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>跨 cluster chaos 管理&lt;/li>
&lt;li>ChaosResult dashboard&lt;/li>
&lt;li>RBAC 控制&lt;/li>
&lt;/ul>
&lt;h3 id="harness-chaosnative商業">Harness ChaosNative（商業）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>商業支援版本&lt;/li>
&lt;li>跟 Harness CD 整合&lt;/li>
&lt;li>Enterprise governance&lt;/li>
&lt;/ul>
&lt;h3 id="跟-chaos-mesh-對照">跟 Chaos Mesh 對照&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>LitmusChaos 是 CNCF graduated 的 Kubernetes chaos engineering 平台、承擔三個責任：ChaosHub experiment marketplace（現成 experiment 直接用）、ChaosWorkflow 編排多步驟實驗、Probe-based steady state validation。設計取捨偏向「現成 experiment 庫 + workflow-centric + CNCF graduated 治理」、是 Chaos Mesh 的近競品、Harness 提供商業版（ChaosNative）。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Litmus 到 K8s</li>
<li>從 ChaosHub 引用現成 experiment</li>
<li>寫 ChaosWorkflow（多步驟 + probe）</li>
<li>設計 Probe（HTTP / Cmd / K8s / Prometheus）做 steady state</li>
<li>評估 Litmus vs Chaos Mesh vs Gremlin 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-litmus-跑起來">最短路徑：5 分鐘把 Litmus 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: helm install litmus litmus/litmus -n litmus --create-namespace</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="c1"># 2. 從 ChaosHub 引用 experiment</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: kubectl apply -f https://hub.litmuschaos.io/...</span>
</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="c1"># 3. 跑 experiment + 看 ChaosResult</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: kubectl apply -f chaosengine.yaml</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: kubectl describe chaosresult &lt;name&gt;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="crd-設計">CRD 設計</h3>
<p>子議題：</p>
<ul>
<li>ChaosExperiment：experiment 定義</li>
<li>ChaosEngine：bind experiment 到 target</li>
<li>ChaosResult：執行結果</li>
</ul>
<h3 id="chaoshub-experiment">ChaosHub experiment</h3>
<p>子議題：</p>
<ul>
<li>現成 experiment marketplace</li>
<li>Generic / Kafka / Cassandra / GCP / AWS / VMware experiments</li>
<li>自訂 experiment 上傳 Hub</li>
</ul>
<h3 id="chaosworkflow">ChaosWorkflow</h3>
<p>子議題：</p>
<ul>
<li>Argo Workflow-based</li>
<li>多步驟 chaos 編排</li>
<li>Schedule trigger</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="probe-based-steady-state">Probe-based steady state</h3>
<p>子議題：</p>
<ul>
<li>HTTP probe / Cmd probe / K8s probe / Prometheus probe</li>
<li>跟 chaos 同步 / 序列執行</li>
<li>Success threshold 設計</li>
</ul>
<h3 id="chaoscentercontrol-plane">ChaosCenter（control plane）</h3>
<p>子議題：</p>
<ul>
<li>跨 cluster chaos 管理</li>
<li>ChaosResult dashboard</li>
<li>RBAC 控制</li>
</ul>
<h3 id="harness-chaosnative商業">Harness ChaosNative（商業）</h3>
<p>子議題：</p>
<ul>
<li>商業支援版本</li>
<li>跟 Harness CD 整合</li>
<li>Enterprise governance</li>
</ul>
<h3 id="跟-chaos-mesh-對照">跟 Chaos Mesh 對照</h3>
<p>子議題：</p>
<ul>
<li>Litmus：workflow-centric、ChaosHub</li>
<li>Chaos Mesh：CRD-driven、Dashboard 友善</li>
<li>選擇判讀：現成 experiment 庫 → Litmus；fault types 多樣 → Chaos Mesh</li>
</ul>
<h3 id="chaos-as-code">Chaos as Code</h3>
<p>子議題：</p>
<ul>
<li>ChaosWorkflow YAML version control</li>
<li>GitOps integration</li>
<li>PR-based chaos review</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="experiment-fail-to-start">Experiment fail to start</h3>
<p>操作原則：ServiceAccount + RBAC 不對、experiment image pull 失敗。判讀：<code>kubectl describe chaosengine</code>。</p>
<h3 id="probe-失敗">Probe 失敗</h3>
<p>操作原則：probe 條件設錯 / target 沒準備好。判讀：ChaosResult 看 probe verdict。</p>
<h3 id="hub-experiment-引用版本不對">Hub experiment 引用版本不對</h3>
<p>操作原則：experiment.yaml 跟 Litmus version 不對齊。判讀：Litmus version + experiment compatibility。</p>
<h3 id="workflow-卡住">Workflow 卡住</h3>
<p>操作原則：Argo Workflow 卡 → 看 Argo pod log。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 fault types / Dashboard</td>
          <td><a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a></td>
      </tr>
      <tr>
          <td>非 K8s / 商業</td>
          <td><a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></td>
      </tr>
      <tr>
          <td>Integration test</td>
          <td><a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td>AWS Fault Injection Service</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ChaosHub 各 experiment 詳細 parameter</li>
<li>Argo Workflow 內部</li>
<li>Litmus 商業版本 detail</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT</a></td>
          <td>hypothesis-driven experiment 對應 ChaosHub workflow</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">Spotify：平台工程與可靠性契約</a></td>
          <td>squad-based 採用 chaos 的平台化路徑</td>
      </tr>
  </tbody>
</table>
<p><strong>Case 庫稀薄</strong>：本 cases/ 目錄目前沒有以 LitmusChaos 為主軸的案例。</p>
<ul>
<li><strong>待補 LitmusChaos customer case</strong>：CNCF graduated 後客戶採用案例、Harness ChaosNative 客戶</li>
<li><strong>候選 case</strong>：Meta（K8s-native region failover chaos）、Microsoft（Chaos Studio 對照組）— 若未來收錄需先在 cases/ 補正文</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a>、<a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></li>
<li>下游能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a></li>
</ul>
]]></content:encoded></item><item><title>Traefik</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/</guid><description>&lt;p>Traefik 是 cloud-native reverse proxy / ingress、承擔三個責任：auto-discovery（從 Docker / K8s / Consul / file 自動發現 backend）、dynamic config（不 reload、即時更新）、ACME 自動 TLS（Let&amp;rsquo;s Encrypt 整合）。設計取捨偏向「cloud-native 簡潔 + auto-discovery 為核心 + middleware chain extensibility」、適合 Docker / K8s 中小規模、大規模 / 複雜 traffic management 跟 nginx / envoy 比相對弱。&lt;/p>
&lt;p>對「Docker / K8s ingress、需要 auto-discovery、ACME 自動 TLS、配置簡潔」這條路徑、Traefik 是 cloud-native first 選擇。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Traefik 到 Docker / K8s&lt;/li>
&lt;li>配置 dynamic provider（labels / annotations / CRD / file）&lt;/li>
&lt;li>配置 ACME 自動 TLS&lt;/li>
&lt;li>設計 middleware chain（auth / rate limit / circuit breaker）&lt;/li>
&lt;li>評估 Traefik vs nginx vs Envoy 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-traefik-跑起來">最短路徑：5 分鐘把 Traefik 跑起來&lt;/h2>





&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"># 1. Docker 跑 Traefik + dashboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d -p 80:80 -p 8080:8080 &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> -v /var/run/docker.sock:/var/run/docker.sock &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> traefik:v3 --api.insecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> --providers.docker
&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"># 2. 用 docker label 配置 routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">docker run -d --label &lt;span class="s2">&amp;#34;traefik.http.routers.demo.rule=Host(\`demo.local\`)&amp;#34;&lt;/span> nginx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 訪 dashboard 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">curl -s http://localhost:8080/api/http/routers &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.[].rule&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="provider-auto-discovery">Provider auto-discovery&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Docker provider：從 container labels 讀 config&lt;/li>
&lt;li>Kubernetes Ingress provider：從 Ingress resource&lt;/li>
&lt;li>Kubernetes CRD provider：Traefik IngressRoute CRD&lt;/li>
&lt;li>Consul / Etcd provider：從 KV store&lt;/li>
&lt;li>File provider：YAML / TOML 靜態 file&lt;/li>
&lt;/ul>
&lt;h3 id="ingressroutek8s-crd">IngressRoute（K8s CRD）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Traefik CRD：IngressRoute / Middleware / TLSOption / ServersTransport&lt;/li>
&lt;li>比 Ingress 表達力強（middleware chain / TLS option / multi-protocol）&lt;/li>
&lt;li>跟 Gateway API 對比&lt;/li>
&lt;/ul>
&lt;h3 id="middleware-chain">Middleware chain&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Traefik 是 cloud-native reverse proxy / ingress、承擔三個責任：auto-discovery（從 Docker / K8s / Consul / file 自動發現 backend）、dynamic config（不 reload、即時更新）、ACME 自動 TLS（Let&rsquo;s Encrypt 整合）。設計取捨偏向「cloud-native 簡潔 + auto-discovery 為核心 + middleware chain extensibility」、適合 Docker / K8s 中小規模、大規模 / 複雜 traffic management 跟 nginx / envoy 比相對弱。</p>
<p>對「Docker / K8s ingress、需要 auto-discovery、ACME 自動 TLS、配置簡潔」這條路徑、Traefik 是 cloud-native first 選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Traefik 到 Docker / K8s</li>
<li>配置 dynamic provider（labels / annotations / CRD / file）</li>
<li>配置 ACME 自動 TLS</li>
<li>設計 middleware chain（auth / rate limit / circuit breaker）</li>
<li>評估 Traefik vs nginx vs Envoy 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-traefik-跑起來">最短路徑：5 分鐘把 Traefik 跑起來</h2>





<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. Docker 跑 Traefik + dashboard</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d -p 80:80 -p 8080:8080 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  traefik:v3 --api.insecure<span class="o">=</span><span class="nb">true</span> --providers.docker
</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"># 2. 用 docker label 配置 routing</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">docker run -d --label <span class="s2">&#34;traefik.http.routers.demo.rule=Host(\`demo.local\`)&#34;</span> nginx
</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="c1"># 3. 訪 dashboard 驗證</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">curl -s http://localhost:8080/api/http/routers <span class="p">|</span> jq <span class="s1">&#39;.[].rule&#39;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="provider-auto-discovery">Provider auto-discovery</h3>
<p>子議題：</p>
<ul>
<li>Docker provider：從 container labels 讀 config</li>
<li>Kubernetes Ingress provider：從 Ingress resource</li>
<li>Kubernetes CRD provider：Traefik IngressRoute CRD</li>
<li>Consul / Etcd provider：從 KV store</li>
<li>File provider：YAML / TOML 靜態 file</li>
</ul>
<h3 id="ingressroutek8s-crd">IngressRoute（K8s CRD）</h3>
<p>子議題：</p>
<ul>
<li>Traefik CRD：IngressRoute / Middleware / TLSOption / ServersTransport</li>
<li>比 Ingress 表達力強（middleware chain / TLS option / multi-protocol）</li>
<li>跟 Gateway API 對比</li>
</ul>
<h3 id="middleware-chain">Middleware chain</h3>
<p>子議題：</p>
<ul>
<li>內建 middleware：headers / rate limit / basic auth / forward auth / retry / circuit breaker / compress / IP whitelist</li>
<li>自訂 middleware：plugin（Yaegi-based）</li>
<li>順序：定義 middleware → 在 router 引用</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="acme-自動-tls">ACME 自動 TLS</h3>
<p>子議題：</p>
<ul>
<li>Let&rsquo;s Encrypt 整合（自動憑證 + 續期）</li>
<li>DNS challenge（適合 wildcard）vs HTTP challenge（適合單 domain）</li>
<li>多 resolver 配置（staging / production / 不同 CA）</li>
<li>對應 ACME storage（local / KV / Traefik Hub）</li>
</ul>
<h3 id="provider-weight--priority">Provider weight / priority</h3>
<p>子議題：</p>
<ul>
<li>多 provider 同時跑、config 來源衝突處理</li>
<li>Provider 優先順序</li>
<li>對應 dynamic config debug</li>
</ul>
<h3 id="traefik-hubmanaged">Traefik Hub（managed）</h3>
<p>子議題：</p>
<ul>
<li>Traefik Hub：商業 managed control plane</li>
<li>適合：跨 cluster 統一管理 / API Gateway portal</li>
<li>跟 self-host Traefik 對比</li>
</ul>
<h3 id="跟-nginx--envoy-對比">跟 nginx / Envoy 對比</h3>
<p>子議題：</p>
<ul>
<li>Traefik 強：cloud-native auto-discovery、配置簡潔</li>
<li>nginx 強：穩定 + 配置控制力 + 大量 community recipe</li>
<li>Envoy 強：xDS dynamic config、advanced traffic management</li>
<li>選型判讀：Docker / K8s 小中規模 → Traefik；複雜 traffic → Envoy；標準 HTTP → nginx</li>
</ul>
<h3 id="plugin-機制yaegi">Plugin 機制（Yaegi）</h3>
<p>子議題：</p>
<ul>
<li>Traefik plugins 用 Yaegi（Go interpreter）跑、不需 recompile</li>
<li>Plugin catalog（社群 + 官方）</li>
<li>適合：客戶 auth / metric / transformation 小邏輯</li>
<li>對應 Envoy WASM extension 對比</li>
</ul>
<h3 id="multi-protocol">Multi-protocol</h3>
<p>子議題：</p>
<ul>
<li>HTTP / HTTPS / TCP / UDP</li>
<li>gRPC（HTTP/2）原生支援</li>
<li>WebSocket sticky session</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-沒被發現">Service 沒被發現</h3>
<p>操作原則：先看 provider 是否啟用、再看 label / annotation / CRD 配置。</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">curl -s http://localhost:8080/api/http/services <span class="p">|</span> jq <span class="s1">&#39;.[].name&#39;</span></span></span></code></pre></div><h3 id="route-衝突">Route 衝突</h3>
<p>操作原則：兩個 router 同 rule，看 priority 排序。判讀：dashboard 看 router list。</p>
<h3 id="acme-rate-limit">ACME rate limit</h3>
<p>操作原則：Let&rsquo;s Encrypt 有 rate limit、staging environment 先測再切 production。</p>
<h3 id="middleware-chain-順序錯">Middleware chain 順序錯</h3>
<p>操作原則：middleware 順序影響行為（auth before rate limit vs after）。判讀：dashboard 看 middleware order。</p>
<h3 id="dashboard-連不上">Dashboard 連不上</h3>
<p>操作原則：dashboard 預設 8080、需要 entrypoint 配置。判讀：traefik.yml + entrypoints 設定。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置控制力 / 大量 community 模板</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
      </tr>
      <tr>
          <td>Advanced traffic / xDS</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio / Linkerd / Consul Connect</td>
      </tr>
      <tr>
          <td>Gateway API standard</td>
          <td>Envoy Gateway / Contour</td>
      </tr>
      <tr>
          <td>純 dev / local</td>
          <td>Docker Compose + direct port mapping</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Traefik plugin 開發</li>
<li>Yaegi Go interpreter 細節</li>
<li>Traefik Hub 商業細節</li>
<li>各 cloud provider 整合差異</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Traefik 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 cutover without drain</a></td>
          <td>Traefik auto-discovery 在 service 下線時、要靠 health check + grace period 等價 drain</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>Docker / K8s 中小規模選 Traefik 簡潔、大規模通常升階到 Envoy / ingress-nginx 或 mesh</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Traefik 案例</strong>：Traefik Labs customer story、IngressRoute CRD 大規模採用、Traefik Hub 早期 adopter。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 LB Contract</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a>、<a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></li>
<li>下游能力：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
]]></content:encoded></item><item><title>模組八：事故處理與復盤</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/</guid><description>&lt;p>事故處理模組的核心目標是把「事故發生時的臨場反應」轉成可演練、可交接、可復用的團隊流程。本模組採問題驅動方法、用 IR 領域 first-class 詞彙（ICS / Severity / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> / Game Day），把事故議題拆成問題節點，蒐集公開事故報告作為案例庫，再把控制面交接到可觀測性、部署平台、可靠性驗證與資安約束落地。&lt;/p>
&lt;h2 id="事故角色">事故角色&lt;/h2>
&lt;p>事故處理的角色是把「出了問題之後怎麼做」變成可預期的協作節奏。這一層不負責追究誰做錯，也不負責寫修復程式，而是負責把啟動、分工、止血、通訊、復原與復盤串成同一條路徑。&lt;/p>
&lt;p>當一個事故被定義成流程，讀者才會看懂 severity 是路由，ICS 是角色分工，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 是下一次演練與改進的輸入。這些詞彙的責任，是讓事故從臨場反應變成可交接的制度。&lt;/p>
&lt;h2 id="問題節點">問題節點&lt;/h2>
&lt;p>問題節點先描述事故環節，再描述決策責任。這樣做可以讓讀者先知道哪裡出現風險，再知道應該把判讀輸給哪個角色或流程。&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>Severity &amp;amp; Trigger&lt;/td>
 &lt;td>事故是否已經跨過啟動門檻、是否需要升級處理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a>、user pain、business risk&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Command Model&lt;/td>
 &lt;td>誰在指揮、誰在記錄、誰在修復、誰在對外通訊&lt;/td>
 &lt;td>role assignment、handoff latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Containment&lt;/td>
 &lt;td>現在應該先止血、降級還是回復&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>、degradation success rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Communication&lt;/td>
 &lt;td>內外部要怎麼更新、多久更新一次、哪些細節先說&lt;/td>
 &lt;td>status cadence、customer confusion&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review &amp;amp; Workflow&lt;/td>
 &lt;td>事故後要補什麼流程、哪些 runbook 要重寫、哪個演練要重跑&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a>、repeat incident rate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的目的是讓事故先變成路由。當路由成立後，服務案例庫才有意義，因為案例可以直接提供真實時間線、對外更新與復原節奏。&lt;/p>
&lt;h2 id="案例庫讀法">案例庫讀法&lt;/h2>
&lt;p>案例庫的責任是保留不同型態的事故節奏。AWS S3、Cloudflare、GitHub、GCP、Atlassian、Roblox 與 Fastly 這些 T1 案例，各自代表控制面、路由、資料一致性、多租戶復原與 edge 擴散的不同樣本。&lt;/p>
&lt;p>讀這些案例時，先看它是哪一種事故，再看它如何收斂。第一步是判斷事故屬於控制面還是資料面。第二步是看影響面是否還在擴大。第三步是看對外通訊與內部復原是否同步。這三步會把讀者導向不同的案例頁，也會把讀者導回可觀測性、部署平台、可靠性驗證或資安約束的交接節點。&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>AWS S3&lt;/td>
 &lt;td>控制面失效如何擴散到整個區域&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>、recover order&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare&lt;/td>
 &lt;td>edge 配置與路由如何全球擴散&lt;/td>
 &lt;td>configuration push、rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitHub&lt;/td>
 &lt;td>replication 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a>&lt;/td>
 &lt;td>status update、failover boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GCP&lt;/td>
 &lt;td>全球控制面與 identity 依賴&lt;/td>
 &lt;td>staged rollout、service health&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atlassian&lt;/td>
 &lt;td>多租戶誤刪與長尾復原&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a>、customer comms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Roblox&lt;/td>
 &lt;td>prolonged recovery 與廠商協作&lt;/td>
 &lt;td>root cause discovery、return to service&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fastly&lt;/td>
 &lt;td>客戶配置觸發供應商 bug&lt;/td>
 &lt;td>propagation speed、rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="第一輪案例驅動路由">第一輪案例驅動路由&lt;/h2>
&lt;p>第一輪 T1 案例已補到「每個服務至少一篇可引用事故頁」。這些案例的用途是把 04 的觀測證據、06 的驗證邊界、08 的指揮與通訊串成同一條教學路徑，堆疊事件本身沒有教學價值。&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage&lt;/a>&lt;/td>
 &lt;td>規則推送如何秒級擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">AWS S3 2017 US-EAST-1&lt;/a>&lt;/td>
 &lt;td>共享子系統恢復順序與通訊入口依賴&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21&lt;/a>&lt;/td>
 &lt;td>一致性優先下的 fail-forward 決策&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident&lt;/a>&lt;/td>
 &lt;td>區域網路壅塞如何跨產品擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">Atlassian 2022 Multi-tenant Outage&lt;/a>&lt;/td>
 &lt;td>長事故的分批恢復與客戶通訊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">Roblox 2021 Prolonged Outage&lt;/a>&lt;/td>
 &lt;td>根因定位延遲與長尾恢復治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">Fastly 2021 Global Edge Outage&lt;/a>&lt;/td>
 &lt;td>有效配置觸發潛藏 bug 的全球擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>若要繼續擴案例，不要只沿同一家公司加事件；先回到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜&lt;/a> 補「企業型態 × 規模階段」覆蓋，再把新增事故映射到本章的問題節點（8.1-8.5、8.18-8.22），才能同時強化案例多樣性與教學路由。&lt;/p></description><content:encoded><![CDATA[<p>事故處理模組的核心目標是把「事故發生時的臨場反應」轉成可演練、可交接、可復用的團隊流程。本模組採問題驅動方法、用 IR 領域 first-class 詞彙（ICS / Severity / <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> / Game Day），把事故議題拆成問題節點，蒐集公開事故報告作為案例庫，再把控制面交接到可觀測性、部署平台、可靠性驗證與資安約束落地。</p>
<h2 id="事故角色">事故角色</h2>
<p>事故處理的角色是把「出了問題之後怎麼做」變成可預期的協作節奏。這一層不負責追究誰做錯，也不負責寫修復程式，而是負責把啟動、分工、止血、通訊、復原與復盤串成同一條路徑。</p>
<p>當一個事故被定義成流程，讀者才會看懂 severity 是路由，ICS 是角色分工，<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 是下一次演練與改進的輸入。這些詞彙的責任，是讓事故從臨場反應變成可交接的制度。</p>
<h2 id="問題節點">問題節點</h2>
<p>問題節點先描述事故環節，再描述決策責任。這樣做可以讓讀者先知道哪裡出現風險，再知道應該把判讀輸給哪個角色或流程。</p>
<table>
  <thead>
      <tr>
          <th>節點</th>
          <th>事故問題</th>
          <th>常見訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Severity &amp; Trigger</td>
          <td>事故是否已經跨過啟動門檻、是否需要升級處理</td>
          <td><a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a>、user pain、business risk</td>
      </tr>
      <tr>
          <td>Command Model</td>
          <td>誰在指揮、誰在記錄、誰在修復、誰在對外通訊</td>
          <td>role assignment、handoff latency</td>
      </tr>
      <tr>
          <td>Containment</td>
          <td>現在應該先止血、降級還是回復</td>
          <td><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、degradation success rate</td>
      </tr>
      <tr>
          <td>Communication</td>
          <td>內外部要怎麼更新、多久更新一次、哪些細節先說</td>
          <td>status cadence、customer confusion</td>
      </tr>
      <tr>
          <td>Review &amp; Workflow</td>
          <td>事故後要補什麼流程、哪些 runbook 要重寫、哪個演練要重跑</td>
          <td><a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a>、repeat incident rate</td>
      </tr>
  </tbody>
</table>
<p>這張表的目的是讓事故先變成路由。當路由成立後，服務案例庫才有意義，因為案例可以直接提供真實時間線、對外更新與復原節奏。</p>
<h2 id="案例庫讀法">案例庫讀法</h2>
<p>案例庫的責任是保留不同型態的事故節奏。AWS S3、Cloudflare、GitHub、GCP、Atlassian、Roblox 與 Fastly 這些 T1 案例，各自代表控制面、路由、資料一致性、多租戶復原與 edge 擴散的不同樣本。</p>
<p>讀這些案例時，先看它是哪一種事故，再看它如何收斂。第一步是判斷事故屬於控制面還是資料面。第二步是看影響面是否還在擴大。第三步是看對外通訊與內部復原是否同步。這三步會把讀者導向不同的案例頁，也會把讀者導回可觀測性、部署平台、可靠性驗證或資安約束的交接節點。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主要用途</th>
          <th>常見回扣節點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS S3</td>
          <td>控制面失效如何擴散到整個區域</td>
          <td><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、recover order</td>
      </tr>
      <tr>
          <td>Cloudflare</td>
          <td>edge 配置與路由如何全球擴散</td>
          <td>configuration push、rollback</td>
      </tr>
      <tr>
          <td>GitHub</td>
          <td>replication 與 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a></td>
          <td>status update、failover boundary</td>
      </tr>
      <tr>
          <td>GCP</td>
          <td>全球控制面與 identity 依賴</td>
          <td>staged rollout、service health</td>
      </tr>
      <tr>
          <td>Atlassian</td>
          <td>多租戶誤刪與長尾復原</td>
          <td><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a>、customer comms</td>
      </tr>
      <tr>
          <td>Roblox</td>
          <td>prolonged recovery 與廠商協作</td>
          <td>root cause discovery、return to service</td>
      </tr>
      <tr>
          <td>Fastly</td>
          <td>客戶配置觸發供應商 bug</td>
          <td>propagation speed、rollback</td>
      </tr>
  </tbody>
</table>
<h2 id="第一輪案例驅動路由">第一輪案例驅動路由</h2>
<p>第一輪 T1 案例已補到「每個服務至少一篇可引用事故頁」。這些案例的用途是把 04 的觀測證據、06 的驗證邊界、08 的指揮與通訊串成同一條教學路徑，堆疊事件本身沒有教學價值。</p>
<table>
  <thead>
      <tr>
          <th>事故案例</th>
          <th>主要判讀問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage</a></td>
          <td>規則推送如何秒級擴散</td>
          <td><a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21</a>、<a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">AWS S3 2017 US-EAST-1</a></td>
          <td>共享子系統恢復順序與通訊入口依賴</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a>、<a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21</a></td>
          <td>一致性優先下的 fail-forward 決策</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident</a></td>
          <td>區域網路壅塞如何跨產品擴散</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">Atlassian 2022 Multi-tenant Outage</a></td>
          <td>長事故的分批恢復與客戶通訊</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a>、<a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">Roblox 2021 Prolonged Outage</a></td>
          <td>根因定位延遲與長尾恢復治理</td>
          <td><a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12</a>、<a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">Fastly 2021 Global Edge Outage</a></td>
          <td>有效配置觸發潛藏 bug 的全球擴散</td>
          <td><a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a>、<a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a></td>
      </tr>
  </tbody>
</table>
<p>若要繼續擴案例，不要只沿同一家公司加事件；先回到 <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a> 補「企業型態 × 規模階段」覆蓋，再把新增事故映射到本章的問題節點（8.1-8.5、8.18-8.22），才能同時強化案例多樣性與教學路由。</p>
<p>第一批缺口回填建議先做三條事故題目：FinTech 補交易中斷時的 impact 分級與對外通訊節奏（回寫 8.1、8.10、8.20）；Gaming 補高峰活動期間的 multi-incident 協調與長事故交接（回寫 8.12、8.14）；Healthcare 補資料與服務雙重事件的 evidence triage 與責任分流（回寫 8.17、8.18、8.19）。</p>
<table>
  <thead>
      <tr>
          <th>產業案例類型</th>
          <th>事故回寫重點</th>
          <th>章節路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td>交易中斷分級、對外更新節奏、客戶影響量化</td>
          <td><a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1</a>、<a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10</a>、<a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td>活動高峰多事故協調、跨時區接班與復原節奏</td>
          <td><a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12</a>、<a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td>資料與服務雙軌事件分流、證據分級與決策紀錄</td>
          <td><a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.17</a>、<a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
  </tbody>
</table>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作工具見 <a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">vendors</a> — T1 收錄 On-call（PagerDuty / Opsgenie / Grafana OnCall）、IR 平台（incident.io / FireHydrant / Rootly）、Status page（Atlassian Statuspage / Instatus）、Postmortem（Jeli）共 9 個 vendor 骨架。跟 <a href="/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/</a> 是不同維度（cases 是公開事故案例來源、vendors 是實作工具）。</p>
<p>進入工具比較前，先回到 <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型</a> 判斷目前缺的是響應層能力，還是缺少可觀測性的證據來源或可靠性驗證的事前演練。事故工具選型要以「事故能否被接住、分工、通訊與回寫」為主軸，on-call 或 IR 平台功能清單只是落地選項。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="規劃方向">規劃方向</h2>
<p>本輪規劃的核心是把模組從「章節列表」升級成「問題節點 + 服務級案例庫」兩層結構：</p>
<ol>
<li><strong>問題節點先行</strong>：8.1-8.10 主章定義事故環節的問題、判讀訊號與責任邊界，不綁特定 stack。</li>
<li><strong>服務級案例庫</strong>：以公開事故報告（AWS / Cloudflare / GitHub / GCP / Atlassian / Roblox / Fastly 等）作 cases，每個服務一個資料夾、累積架構脈絡與多次事故的 longitudinal pattern。</li>
<li><strong>資安事故是其中一類</strong>：跟 07 的交接點維持，但 07 的紅藍隊框架不外推到本模組 — IR 自有 Severity / ICS / <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 等 first-class 詞彙、不需要藉攻防隱喻表達。</li>
</ol>
<p>不經實作即可推進的理由：事故處理的價值在「協作節奏與決策模型」，這層跟具體服務技術解耦，公開 post-mortem 案例豐富，符合先建概念層的條件。</p>
<h2 id="模組方法">模組方法</h2>
<p>問題驅動方法的核心是讓案例退到證據角色，讓知識網以事故環節問題為主體。</p>
<ol>
<li>先定義事故環節問題與責任邊界。</li>
<li>再定義判讀訊號（影響面、擴散速率、降級空間）與升級條件。</li>
<li>接著定義交接路由與前置控制面。</li>
<li>最後在問題觸發時引用對應服務的事故案例。</li>
</ol>
<h2 id="模組分工定位">模組分工定位</h2>
<p>本模組提供觀念、判讀與路由。實作細節由對應模組承接，確保概念層與實作層分工清晰。</p>
<ul>
<li><code>backend/04-observability</code>：可觀測性模組，負責訊號偵測、判讀與告警治理實作。</li>
<li><code>backend/05-deployment-platform</code>：切換、回滾、流量控制與隔離實作。</li>
<li><code>backend/06-reliability</code>：可靠性驗證模組，負責事故前驗證、演練與回復排練實作。</li>
<li><code>backend/07-security-data-protection</code>：權限、稽核與高風險操作約束實作。</li>
</ul>
<h2 id="從章節到實作的-chain">從章節到實作的 chain</h2>
<p>各章節交付三樣：問題節點清單、判讀訊號、控制面 link。判讀完成後沿兩條 chain 進入 implementation：</p>
<ol>
<li><strong>Mechanism chain</strong>：點問題節點表的 <code>[control-name]</code> link 進 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge-cards</a>，那層展開機制 / 邊界 / context-dependence。例：<code>[incident-command-system]</code> 的 knowledge-card 是該 control 的 mechanism SSoT。</li>
<li><strong>Delivery chain</strong>：章節「交接路由」欄位指向下游模組，包括可觀測性（訊號）、部署平台（切換 / 回滾）、可靠性驗證（演練 / 回復排練）與資安資料保護（權限 / 稽核）。</li>
</ol>
<p>兩條 chain 走完，控制面交付完整。Implementation 強度取決於兩條 chain 的完成度，章節閱讀本身完成 routing 階段。</p>
<h2 id="跟既有模組的串接">跟既有模組的串接</h2>
<p>本模組是「觀測 → 驗證 → 事故」閉環的收口、承接資安概念判讀、把問題地圖轉成可執行事故節奏。資安事故僅是事故的一個子集、其他多數事故是可用性 / 容量 / 變更類。</p>
<p><strong>觀測、驗證與事故閉環交接基線</strong>：</p>
<ul>
<li><strong>來自 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></strong>：訊號（SLO burn / error rate / latency spike）是事故啟動條件、判讀脈絡的主要來源。</li>
<li><strong>餵給 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></strong>：<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 揭露的偵測缺口（訊號太晚、cardinality 不足、symptom-based alert 缺）回寫到訊號治理。</li>
<li><strong>來自 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></strong>：事前演練（game day / DR rehearsal / chaos experiment）作為事中決策的肌肉記憶與 runbook 來源。</li>
<li><strong>餵給 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></strong>：<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> action items 回寫成新 chaos / DR 演練題目、事故型態變成 chaos 與 DR 演練的場景輸入。</li>
<li><strong>詳細閉環說明</strong>：見 <a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">Observability / Reliability / Incident Response 閉環</a>。</li>
</ul>
<p><strong>07 資安交接基線</strong>：</p>
<ul>
<li>來自 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>：承接身分事件分級與收斂順序。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>：承接入口事件止血、隔離與驗證節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>：承接外送事件通報與影響盤點節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a>：承接證據結構與復盤責任閉環。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.16 從公開事故到工程 Workflow</a>：承接事故案例如何回寫控制面。</li>
</ul>
<h2 id="主章規劃">主章規劃</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1 事故分級與啟動條件</a></td>
          <td>Severity &amp; Trigger</td>
          <td>建立統一分級與啟動門檻</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 事故指揮與角色分工</a></td>
          <td>Command Model</td>
          <td>定義 commander、owner、scribe、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 協作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血、降級與回復策略</a></td>
          <td>Containment &amp; Recovery</td>
          <td>把短期止血與正式回復拆成可執行步驟</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 事故通訊與狀態更新</a></td>
          <td>Incident Communication</td>
          <td>建立內外部通訊節奏與格式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 復盤與改進追蹤</a></td>
          <td>Post-Incident Review</td>
          <td>把 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 與 action items 變成可驗證閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6 演練與值班能力建設</a></td>
          <td>Drills &amp; Readiness</td>
          <td>用 game day 與值班訓練提升反應品質</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/attacker-view-incident-risks/" data-link-title="8.7 失敗模式審查（Failure Mode Audit）" data-link-desc="以概念層判讀事故流程弱點，聚焦分級、指揮、回復與交接節奏">8.7 失敗模式審查（Failure Mode Audit）</a></td>
          <td>Failure Mode Audit</td>
          <td>用擴散路徑、回復瓶頸與交接斷點檢查事故設計（原「攻擊者視角」改名為領域 first-class 詞彙）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow</a></td>
          <td>Case to Workflow</td>
          <td>把事故故事轉成可執行、可驗證、可演練的流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">8.9 事故型態庫入口</a></td>
          <td>Incident Pattern</td>
          <td>把跨服務的共通事故型態（cascading / split-brain / control-plane failure）抽成型態卡</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 Stakeholder 通訊與外部狀態頁</a></td>
          <td>Stakeholder Comms</td>
          <td>把 <a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a>、<a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a>、補償政策串成節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">8.11 觀測、驗證與事故閉環</a></td>
          <td>Cross-Module Loop</td>
          <td>把可觀測性、可靠性驗證與事故處理的雙向反饋串成可判讀循環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12 IC Handoff 與長事故協調</a></td>
          <td>Handover</td>
          <td>把 24h+ / 跨 timezone 事故的接班節奏變成可重複流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">8.13 Repeated Incident 與 Toil 治理</a></td>
          <td>Repeated &amp; Toil</td>
          <td>把同型反覆事故與重複手動修復變成工程化治理對象</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14 Multi-incident Coordination</a></td>
          <td>Multi-incident</td>
          <td>把同時多事故的優先序、資源分配與 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> pool 協調變成可執行流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15 Vendor / 第三方依賴事故處理</a></td>
          <td>Vendor Incident</td>
          <td>依賴方掛掉、自己無 control 時的決策模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/runbook-lifecycle/" data-link-title="8.16 Runbook Lifecycle 管理" data-link-desc="把 runbook 從一次性文件變成有版本、有演練、會過期的 artifact">8.16 Runbook Lifecycle 管理</a></td>
          <td>Runbook Lifecycle</td>
          <td>把 runbook 變成有版本、有演練、會過期的 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.17 Security vs Operational Incident 分流</a></td>
          <td>Security vs Ops IR</td>
          <td>把資安事故跟可用性事故的 IR 流程分支點明確化</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18</a></td>
          <td>Incident Intake &amp; Evidence Triage</td>
          <td>把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
          <td>Incident Decision Log</td>
          <td>把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
          <td>Customer Impact Assessment</td>
          <td>把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">8.21</a></td>
          <td>Incident Workflow Automation Boundary</td>
          <td>定義哪些事故流程適合自動化，哪些決策需要保留人工確認</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
          <td>Incident Evidence Write-back</td>
          <td>把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23</a></td>
          <td>Control Plane Decision Log and Write-back 實作示範</td>
          <td>以 rule/config rollout 事故示範 decision log 與 write-back 的完整閉環</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>註：8.1-8.23 已完成概念層與第一篇實作示範正文，案例庫可支援 intake、decision、impact、write-back 的完整路由。後續重點為多事件對照與跨模組回寫精度提升。</p></blockquote>
<h2 id="個案前拓展空間">個案前拓展空間</h2>
<p>個案前拓展的責任是先建立事故案例的閱讀欄位。事故處理模組適合補 intake、evidence、decision、impact 與 automation boundary 這類跨事故骨架，不適合直接把公開事故故事當正文主軸。</p>
<table>
  <thead>
      <tr>
          <th>拓展方向</th>
          <th>補充理由</th>
          <th>先放位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident Intake &amp; Evidence Triage</td>
          <td>事故來源可能是告警、客訴、支援或第三方狀態</td>
          <td>8.18</td>
      </tr>
      <tr>
          <td>Incident Decision Log</td>
          <td>事中決策需要保留假設、證據、條件與責任人</td>
          <td>8.19</td>
      </tr>
      <tr>
          <td>Customer Impact Assessment</td>
          <td>對外通訊與補償需要更精準的影響評估模型</td>
          <td>8.20</td>
      </tr>
      <tr>
          <td>Incident Workflow Automation Boundary</td>
          <td>自動化適合處理通知與欄位，決策仍需清楚邊界</td>
          <td>8.21</td>
      </tr>
  </tbody>
</table>
<p>本輪先完成這四個個案前拓展章，讓公開事故案例可以被拆成可重用素材。若案例重點是「事故從哪裡被發現」，回寫 Incident Intake &amp; Evidence Triage；若重點是「事中決策如何形成」，回寫 Incident Decision Log；若重點是「客戶影響如何量化」，回寫 Customer Impact Assessment；若重點是「流程工具是否幫上忙」，回寫 Incident Workflow Automation Boundary。</p>
<h2 id="後續深化方向">後續深化方向</h2>
<p>08 後續深化以「同服務多事件對照、decision/evidence 欄位標準化、跨模組閉環回寫」為主。事故處理承接 04 的觀測證據與 06 的驗證結果，並持續回寫上游控制面。</p>
<table>
  <thead>
      <tr>
          <th>深化方向</th>
          <th>主要責任</th>
          <th>回寫路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多事件對照</td>
          <td>同服務建立第二、第三事件對照，提煉失效模式</td>
          <td><a href="/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/</a></td>
      </tr>
      <tr>
          <td>欄位標準化</td>
          <td>intake / decision / impact / write-back 用同一欄位語言</td>
          <td><a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>跨模組閉環回寫</td>
          <td>把事故教訓回寫到觀測與驗證控制面</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a>、<a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
  </tbody>
</table>
<h2 id="實作探討入口">實作探討入口</h2>
<p>進入實作層時，08 建議先建最小 incident artifact 套組：<code>intake sheet + decision log + customer impact note + write-back record</code>，並固定連到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 與 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a>。</p>
<p>首篇示範已完成： <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back 實作示範</a>。</p>
<p>完成條件是每篇都能回答四件事：輸入來源、判讀欄位、決策責任、回寫路由。這樣 08 才能把事故從臨場反應整理成可演練、可復盤、可交接的流程。</p>
<h2 id="服務案例庫規劃">服務案例庫規劃</h2>
<p>服務作為案例單位、累積架構脈絡與多次事故的 longitudinal pattern。每個服務一個資料夾、收錄該服務的事故時間線、共通失敗模式與引用源。資料夾位置：<code>content/backend/08-incident-response/cases/{vendor-service}/</code>。</p>
<h3 id="t1必寫公開素材豐富教學價值高">T1（必寫、公開素材豐富、教學價值高）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">aws-s3</a></td>
          <td>2017 typo / 2021 us-east-1 / blast radius、區域依賴擴散</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare</a></td>
          <td>2019 regex CPU / 2020 BGP / 2023 R2 / configuration push 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">github</a></td>
          <td>2018-10 MySQL split-brain / Actions outages、跨區資料一致性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/gcp/" data-link-title="Google Cloud Platform" data-link-desc="GCP 重大事故時間線與架構脈絡">gcp</a></td>
          <td>Load Balancer / IAM 全球控制面失效</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">atlassian</a></td>
          <td>2022 多租戶誤刪 14 天、IR 公開度極高、跨團隊協作教科書</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">roblox</a></td>
          <td>2021 73 小時、Consul + 流量模式根因、long-tail recovery</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/fastly/" data-link-title="Fastly" data-link-desc="Fastly 全球配置 push 事故時間線">fastly</a></td>
          <td>2021-06 全球分鐘級配置 push 事故</td>
      </tr>
  </tbody>
</table>
<h3 id="t2補不同型態">T2（補不同型態）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">slack</a></td>
          <td>通訊節奏、外部狀態頁設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">datadog</a></td>
          <td>2023 multi-region、監控供應商自己掛、客戶觀測落差</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe</a></td>
          <td>金流影響量化、idempotency 與 API 兼容（住於 06）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">discord</a></td>
          <td>Gateway scale-out 事故、capacity surprise</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">azure-ad</a></td>
          <td>Identity 控制面失效、藍圖式 cascading</td>
      </tr>
  </tbody>
</table>
<h3 id="t3補完視時間">T3（補完，視時間）</h3>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">heroku</a></td>
          <td>Router 層失效、PaaS multi-tenant 路由</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin</a></td>
          <td>Capacity 與 on-call structure（住於 06）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/" data-link-title="Reddit" data-link-desc="Reddit Pi Day 2023 k8s 升級事故">reddit</a></td>
          <td>Pi Day 2023 k8s 升級事故</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">microsoft-365</a></td>
          <td>企業 SaaS 套件事故、PIR 格式</td>
      </tr>
  </tbody>
</table>
<h2 id="既有可引用卡片">既有可引用卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a></li>
<li><a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
<li><a href="/blog/backend/knowledge-cards/runbook-link/" data-link-title="Runbook Link" data-link-desc="說明告警與 dashboard 如何直接連到處理流程">runbook link</a></li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a></li>
<li><a href="/blog/backend/knowledge-cards/playbook/" data-link-title="Playbook" data-link-desc="說明場景化處置腳本如何降低事故處理不確定性">playbook</a></li>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a></li>
<li><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a></li>
<li><a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a></li>
<li><a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">downtime</a></li>
<li><a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a></li>
<li><a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a></li>
<li><a href="/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">fallback plan</a></li>
<li><a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a></li>
<li><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a></li>
<li><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a></li>
<li><a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a></li>
<li><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a></li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a></li>
<li><a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a></li>
<li><a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a></li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a></li>
<li><a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a></li>
<li><a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a></li>
<li><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a></li>
<li><a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></li>
<li><a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a></li>
<li><a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a></li>
<li><a href="/blog/backend/knowledge-cards/stakeholder-mapping/" data-link-title="Stakeholder Mapping" data-link-desc="說明事故期間如何把通報對象分層與對應 owner">stakeholder mapping</a></li>
<li><a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a></li>
</ul>
<h2 id="模組完成狀態">模組完成狀態</h2>
<p>主章 8.1-8.23 已完成首輪正文，服務案例庫第一批正文已補齊（Cloudflare / AWS S3 / GitHub / GCP / Atlassian / Roblox / Fastly，以及 Slack / Datadog / Discord / Azure AD / Heroku / Reddit / Microsoft 365）。目前重點從「補案例檔案」轉為「補多事件對照與決策路徑精度」。</p>
<p>案例正文入口見 <a href="/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">事故案例庫</a>。每篇案例至少要能回寫一個事故控制面章節（例如 8.18、8.19、8.20、8.21、8.22），避免只停在事故時間線描述。</p>
<p>第二批案例深挖已補 <code>AWS</code> 第二事件： <a href="/blog/backend/08-incident-response/cases/aws-s3/2021-us-east-1-control-plane-degradation/" data-link-title="AWS 2021 US-EAST-1 Control Plane Degradation" data-link-desc="2021-12-07 AWS us-east-1 控制面退化案例：內部網路壅塞、API 錯誤率升高、跨服務依賴連鎖與通訊節奏調整。">2021 US-EAST-1 Control Plane Degradation</a>。這篇重點回寫 <code>8.3 / 8.4 / 8.20</code> 與 <code>4.18 / 4.20</code>，補齊 control plane 退化與通訊節奏的判讀。</p>
<p>深挖批次 B 已補 <code>Cloudflare</code> 第三事件： <a href="/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">2023 Workers KV Deployment Tool Misconfiguration</a>。這篇重點回寫 <code>8.19 / 8.22 / 6.24</code>，把控制面變更擴散與 decision log 的治理責任接回主章。</p>
<p>第三批案例補強已補 <code>AWS</code> 第三篇： <a href="/blog/backend/08-incident-response/cases/aws-s3/2023-control-plane-accountability-and-communication-pattern/" data-link-title="AWS：Control Plane 事故的責任邊界與通訊節奏樣式（2023）" data-link-desc="以 AWS 2023 年公開事件樣式為主，整理 control plane 退化時如何建立責任邊界、決策紀錄與對外更新節奏。">2023 Control Plane Accountability and Communication Pattern</a>。這篇重點回寫 <code>8.19 / 8.20 / 8.4 / 4.20</code>，補齊控制面事故的責任邊界與對外節奏樣式。</p>
<h2 id="後續推演大綱">後續推演大綱</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>產出</th>
          <th>責任</th>
          <th>回寫位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>案例深挖批次 A</td>
          <td>針對 T1 案例補第二事件或後續事件，強化同服務的決策演進對照</td>
          <td><code>cases/cloudflare/</code>、<code>cases/aws-s3/</code></td>
      </tr>
      <tr>
          <td>2</td>
          <td>案例深挖批次 B</td>
          <td>針對 T2/T3 案例補不同事故型態，避免只集中在單一故障類型</td>
          <td><code>cases/{service}/</code></td>
      </tr>
      <tr>
          <td>3</td>
          <td>章節回寫補強</td>
          <td>把案例中的 intake、decision、impact、automation 教訓回寫主章</td>
          <td><code>8.18</code>、<code>8.19</code>、<code>8.20</code>、<code>8.21</code>、<code>8.22</code></td>
      </tr>
      <tr>
          <td>4</td>
          <td>跨模組路由校正</td>
          <td>補齊 04/05/06/07 的交接連結，讓讀者可從事故案例直接跳到上游控制面</td>
          <td>各章節「交接路由」段</td>
      </tr>
  </tbody>
</table>
<p>推演資產化的完成條件是讓讀者能從一個事故壓力出發，找到對應問題節點、服務 case 與回寫章節。完成後事故模組才進入穩定維護狀態。</p>
<h2 id="tripwire">Tripwire</h2>
<ul>
<li>寫 T1 服務第 3 個時、若 case 之間無共通分類軸 → 改用單服務獨立檔，不開資料夾。</li>
<li>寫到第 9 主章發現章節覆蓋 60%+ → 軸線過於相似、合併或重切。</li>
<li>進服務實作模組時 routing chain 走不通 → 回頭補對應主章。</li>
</ul>
]]></content:encoded></item><item><title>0.8 資安與資料保護需求</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/</guid><description>&lt;p>資安需求分析的核心原則是先定義安全邊界，再選擇安全工具。權限分級、伺服器防護、資料匯出遮罩、傳輸加密、稽核紀錄與密鑰管理都服務同一個目標：讓資料與操作只在被授權、可追蹤、可控的路徑中流動。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用資料分級與角色分級描述安全需求&lt;/li>
&lt;li>判斷服務入口、內部通訊與資料匯出需要哪些保護&lt;/li>
&lt;li>區分權限控制、資料遮罩、傳輸保護、伺服器防護與稽核需求&lt;/li>
&lt;li>把資安需求連到後續安全與資料保護模組&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察資安需求來自資料角色與路徑">【觀察】資安需求來自資料、角色與路徑&lt;/h2>
&lt;p>資安設計的第一個問題是「誰在什麼路徑上接觸什麼資料」。同一個系統可能同時有使用者、客服、營運、工程師、背景 worker、外部合作方與管理員；每個角色需要的資料、操作與稽核等級都不同。&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>權限分級&lt;/td>
 &lt;td>誰能看、改、匯出、審核或管理資料&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>伺服器防護&lt;/td>
 &lt;td>哪些入口要限制來源、速率與攻擊面&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint&lt;/a>、upload、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料遮罩&lt;/td>
 &lt;td>匯出、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、客服畫面要顯示多少敏感資訊&lt;/td>
 &lt;td>email、電話、身分證、付款資訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>傳輸保護&lt;/td>
 &lt;td>資料在 client、service、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、storage 之間如何被保護&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS&lt;/a>、signed request、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/certificate-chain-trust/" data-link-title="Certificate Chain and Trust Root" data-link-desc="說明網站憑證鏈與信任根如何影響連線可用性與驗證結果">certificate chain and trust root&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>密鑰與秘密&lt;/td>
 &lt;td>token、API key、憑證如何保存、輪替與撤銷&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">Website Certificate Lifecycle&lt;/a>、key rotation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>稽核追蹤&lt;/td>
 &lt;td>高風險操作是否能被追蹤與事後審查&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>、approval、admin action&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是需求索引。資安討論要先定義資料與操作的保護等級，再決定具體平台、服務或產品。&lt;/p>
&lt;h2 id="判讀權限分級要從角色與資料責任開始">【判讀】權限分級要從角色與資料責任開始&lt;/h2>
&lt;p>權限分級的核心責任是控制角色能執行哪些操作。常見模型包括依角色授權、依屬性授權、依 tenant 隔離與依資源 owner 判斷；選型前要先定義資料責任與操作風險。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>客服可以查看訂單狀態與配送資訊，但付款敏感欄位只顯示遮罩版本。&lt;/li>
&lt;li>營運可以調整活動商品，但價格變更需要主管審核。&lt;/li>
&lt;li>企業 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS&lt;/a> 中，workspace admin 可以管理成員，普通 member 只能操作自己有權限的 project。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是只用「是否登入」表示授權。登入代表身份已被確認；授權要回答這個身份能否操作特定資源、特定欄位與特定動作。權限規則也要能被測試、稽核與解釋。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>。&lt;/p>
&lt;h2 id="判讀伺服器防護要先找暴露入口">【判讀】伺服器防護要先找暴露入口&lt;/h2>
&lt;p>伺服器防護的核心責任是降低服務入口的攻擊面。Public API、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a>、file upload、public asset、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> 都有不同暴露程度。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> 需要驗證來源簽章、限制重放時間窗，並記錄來源系統。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint&lt;/a> 需要更高權限、來源限制與操作稽核。&lt;/li>
&lt;li>file upload 需要限制大小、型別、掃描結果與後續存取權限。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是把所有 HTTP 入口視為同一種入口。公開 API、內部 API、診斷 API、管理 API 與第三方 callback 的風險不同；防護策略要依入口用途分級。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口&lt;/a>。&lt;/p>
&lt;h2 id="判讀資料遮罩要依使用情境分級">【判讀】資料遮罩要依使用情境分級&lt;/h2>
&lt;p>資料遮罩的核心責任是讓使用者完成工作，同時降低敏感資料暴露。遮罩可能發生在客服畫面、匯出報表、log、debug payload、analytics dataset、測試資料與外部分享檔案。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>客服查會員資料時，只顯示電話末三碼與 email 部分字元。&lt;/li>
&lt;li>匯出訂單報表時，付款識別碼保留交易對帳所需欄位，個資欄位轉為遮罩值。&lt;/li>
&lt;li>開發環境使用脫敏資料集，保留資料形狀與關聯，但移除真實身份資訊。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是把遮罩視為顯示層問題。資料可能流入匯出、log、queue、搜尋索引、分析資料集與備份；遮罩策略要定義在資料流路徑上，而非只套在單一頁面。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>資安需求分析的核心原則是先定義安全邊界，再選擇安全工具。權限分級、伺服器防護、資料匯出遮罩、傳輸加密、稽核紀錄與密鑰管理都服務同一個目標：讓資料與操作只在被授權、可追蹤、可控的路徑中流動。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用資料分級與角色分級描述安全需求</li>
<li>判斷服務入口、內部通訊與資料匯出需要哪些保護</li>
<li>區分權限控制、資料遮罩、傳輸保護、伺服器防護與稽核需求</li>
<li>把資安需求連到後續安全與資料保護模組</li>
</ol>
<hr>
<h2 id="觀察資安需求來自資料角色與路徑">【觀察】資安需求來自資料、角色與路徑</h2>
<p>資安設計的第一個問題是「誰在什麼路徑上接觸什麼資料」。同一個系統可能同時有使用者、客服、營運、工程師、背景 worker、外部合作方與管理員；每個角色需要的資料、操作與稽核等級都不同。</p>
<table>
  <thead>
      <tr>
          <th>需求類型</th>
          <th>核心問題</th>
          <th>常見情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>權限分級</td>
          <td>誰能看、改、匯出、審核或管理資料</td>
          <td><a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a></td>
      </tr>
      <tr>
          <td>伺服器防護</td>
          <td>哪些入口要限制來源、速率與攻擊面</td>
          <td><a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint</a>、upload、<a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a>、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a></td>
      </tr>
      <tr>
          <td>資料遮罩</td>
          <td>匯出、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、客服畫面要顯示多少敏感資訊</td>
          <td>email、電話、身分證、付款資訊</td>
      </tr>
      <tr>
          <td>傳輸保護</td>
          <td>資料在 client、service、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、storage 之間如何被保護</td>
          <td><a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、signed request、<a href="/blog/backend/knowledge-cards/certificate-chain-trust/" data-link-title="Certificate Chain and Trust Root" data-link-desc="說明網站憑證鏈與信任根如何影響連線可用性與驗證結果">certificate chain and trust root</a></td>
      </tr>
      <tr>
          <td>密鑰與秘密</td>
          <td>token、API key、憑證如何保存、輪替與撤銷</td>
          <td><a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">Website Certificate Lifecycle</a>、key rotation</td>
      </tr>
      <tr>
          <td>稽核追蹤</td>
          <td>高風險操作是否能被追蹤與事後審查</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>、approval、admin action</td>
      </tr>
  </tbody>
</table>
<p>這張表是需求索引。資安討論要先定義資料與操作的保護等級，再決定具體平台、服務或產品。</p>
<h2 id="判讀權限分級要從角色與資料責任開始">【判讀】權限分級要從角色與資料責任開始</h2>
<p>權限分級的核心責任是控制角色能執行哪些操作。常見模型包括依角色授權、依屬性授權、依 tenant 隔離與依資源 owner 判斷；選型前要先定義資料責任與操作風險。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>客服可以查看訂單狀態與配送資訊，但付款敏感欄位只顯示遮罩版本。</li>
<li>營運可以調整活動商品，但價格變更需要主管審核。</li>
<li>企業 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">SaaS</a> 中，workspace admin 可以管理成員，普通 member 只能操作自己有權限的 project。</li>
</ul>
<p>這類需求的陷阱是只用「是否登入」表示授權。登入代表身份已被確認；授權要回答這個身份能否操作特定資源、特定欄位與特定動作。權限規則也要能被測試、稽核與解釋。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>。</p>
<h2 id="判讀伺服器防護要先找暴露入口">【判讀】伺服器防護要先找暴露入口</h2>
<p>伺服器防護的核心責任是降低服務入口的攻擊面。Public API、<a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint</a>、<a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a>、file upload、public asset、<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint</a> 與 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> 都有不同暴露程度。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> 需要驗證來源簽章、限制重放時間窗，並記錄來源系統。</li>
<li><a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint</a> 需要更高權限、來源限制與操作稽核。</li>
<li>file upload 需要限制大小、型別、掃描結果與後續存取權限。</li>
</ul>
<p>這類需求的陷阱是把所有 HTTP 入口視為同一種入口。公開 API、內部 API、診斷 API、管理 API 與第三方 callback 的風險不同；防護策略要依入口用途分級。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 與 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a>。</p>
<h2 id="判讀資料遮罩要依使用情境分級">【判讀】資料遮罩要依使用情境分級</h2>
<p>資料遮罩的核心責任是讓使用者完成工作，同時降低敏感資料暴露。遮罩可能發生在客服畫面、匯出報表、log、debug payload、analytics dataset、測試資料與外部分享檔案。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>客服查會員資料時，只顯示電話末三碼與 email 部分字元。</li>
<li>匯出訂單報表時，付款識別碼保留交易對帳所需欄位，個資欄位轉為遮罩值。</li>
<li>開發環境使用脫敏資料集，保留資料形狀與關聯，但移除真實身份資訊。</li>
</ul>
<p>這類需求的陷阱是把遮罩視為顯示層問題。資料可能流入匯出、log、queue、搜尋索引、分析資料集與備份；遮罩策略要定義在資料流路徑上，而非只套在單一頁面。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a> 與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</p>
<h2 id="判讀傳輸保護要覆蓋跨邊界流動">【判讀】傳輸保護要覆蓋跨邊界流動</h2>
<p>傳輸保護的核心責任是保護資料跨越邊界時的機密性、完整性與來源可信度。邊界可能是 client 到 API、service 到 service、worker 到 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、service 到 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、系統到第三方。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>client 到 API 使用 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS</a>，避免帳號資料在網路中被竊聽。</li>
<li>service 到 service 使用 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">mTLS</a> 或 signed request，確認呼叫來源與訊息完整性。</li>
<li><a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> callback 驗證簽章與 timestamp，降低偽造與重放風險。</li>
</ul>
<p>這類需求的陷阱是只保護公開入口。內部網路、queue message、<a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a> link、backup transfer 與第三方 callback 都是資料流動路徑；傳輸保護要依邊界與資料等級設定。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a> 與 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a>。</p>
<h2 id="判讀密鑰與秘密管理要設計生命週期">【判讀】密鑰與秘密管理要設計生命週期</h2>
<p>密鑰與秘密管理的核心責任是控制 token、API key、private key、database <a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">Credential</a>、session secret 與加密 key 的產生、保存、使用、輪替與撤銷，並把網站憑證納入 <a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">Website Certificate Lifecycle</a>。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>第三方 API key 需要分環境保存，並能在外洩時快速撤銷。</li>
<li>database <a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a> 需要依服務分離，避免單一 <a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a> 擁有過大權限。</li>
<li>簽章密鑰需要支援輪替期，讓新舊 key 在過渡期間都能驗證。</li>
<li>公網站點憑證需要有 <a href="/blog/backend/knowledge-cards/acme-automation/" data-link-title="ACME Automation" data-link-desc="說明網站憑證如何透過 ACME 自動簽發與續期">ACME automation</a> 或明確續期流程，並具備 <a href="/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">certificate revocation</a> 設計。</li>
</ul>
<p>這類需求的陷阱是把秘密寫進設定檔、log、測試資料或部署指令。秘密管理要同時包含保存位置、存取權限、輪替流程、撤銷流程、憑證續期流程與稽核紀錄。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>。</p>
<h2 id="判讀稽核追蹤要服務事後責任判斷">【判讀】稽核追蹤要服務事後責任判斷</h2>
<p>稽核追蹤的核心責任是回答「誰在何時對哪個資源做了什麼，理由與結果是什麼」。高風險操作、管理員操作、資料匯出、權限變更、金流狀態修改都需要清楚 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>管理員修改使用者角色時，記錄操作者、目標使用者、舊角色、新角色與工單 ID。</li>
<li>客服匯出訂單資料時，記錄查詢條件、匯出欄位、資料量與核准者。</li>
<li>系統輪替 API key 時，記錄 key id、使用服務、輪替時間與生效狀態。</li>
</ul>
<p>這類需求的陷阱是把 audit log 和 debug log 混在一起。debug log 服務排障，audit log 服務責任判斷；audit log 需要更穩定的 schema、保存策略、存取權限與完整性保護。</p>
<p>下一步可讀：<a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a> 與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</p>
<h2 id="檢查進入實作前的概念邊界清單">【檢查】進入實作前的概念邊界清單</h2>
<p>當以下問題都能回答時，代表本章的概念層已完成，可以進入資安與資料保護實作章節：</p>
<ol>
<li>資料分級與角色責任是否明確（誰可讀、可改、可匯出）</li>
<li>資料流路徑是否明確（client、service、queue、storage）</li>
<li>秘密與憑證生命週期是否明確（保存、輪替、撤銷、續期）</li>
<li>稽核與事故追蹤要求是否明確（audit 欄位、保存、查核流程）</li>
</ol>
<p>下一步建議路由（按本章六議題對應）：</p>
<ul>
<li>權限分級 → <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li>伺服器防護 → <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>資料遮罩 → <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></li>
<li>傳輸保護 → <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a></li>
<li>密鑰與秘密 → <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>稽核追蹤 → <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
<li>事件處置 → <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08-incident-response</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>資安與資料保護要從資料、角色與路徑開始。權限分級控制誰能操作什麼，伺服器防護降低暴露入口風險，資料遮罩降低敏感資訊外流，傳輸保護保障跨邊界流動，密鑰管理控制秘密生命週期，稽核追蹤支援事後責任判斷。這些需求清楚後，後續才進入具體安全服務與平台能力。</p>
]]></content:encoded></item><item><title>4.9 Continuous Profiling</title><link>https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Continuous profiling 的定位：metrics / logs / traces 之外的第四角&lt;/li>
&lt;li>Profile 維度：CPU、heap、allocations、lock contention、goroutine / async task&lt;/li>
&lt;li>Always-on vs on-demand：何時用哪種&lt;/li>
&lt;li>Flame graph 與版本差異比較&lt;/li>
&lt;li>Overhead 控制&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Continuous profiling 是把 CPU、memory、allocation 與 lock contention 變成長期可比較的 production 訊號，責任是補上 metrics、logs、traces 看不到的 callstack 成本。&lt;/p>
&lt;p>Metrics 會告訴你「CPU usage 上升了」，trace 會告訴你「這條 request 的 latency 從 200ms 變成 800ms」，profile 會告訴你「增加的 600ms 花在哪幾個 function call、哪幾行程式碼」。Profile 是唯一能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;p>「Continuous」的關鍵差異是：傳統 profiling 是事故時才手動開啟，continuous profiling 是 production 常駐的低開銷採樣。事故時不需要重現問題 — baseline profile 已經在那裡，直接跟事故期間的 profile 做 diff。&lt;/p>
&lt;h2 id="profile-維度">Profile 維度&lt;/h2>
&lt;p>不同的 profile 維度回答不同的效能問題。服務的退化模式決定需要哪些維度。&lt;/p>
&lt;h3 id="cpu-profile">CPU profile&lt;/h3>
&lt;p>回答「CPU 時間花在哪些 function」。最常用的 profile 維度。適合診斷 latency 退化（某個 function 開始佔更多 CPU 時間）跟 CPU 利用率異常（某段程式碼意外進入 hot path）。&lt;/p>
&lt;p>CPU profile 用 sampling 方式採集 — 定期（例如每秒 100 次）記錄當前的 callstack。統計意義上，出現在 sample 中的次數跟實際 CPU 消耗成正比。Sampling 頻率越高精度越好，但 overhead 也越高。&lt;/p>
&lt;h3 id="heap--memory-profile">Heap / memory profile&lt;/h3>
&lt;p>回答「memory 被哪些 function 持有」。適合診斷 memory leak（allocation 持續增長、GC 回收不了）跟 GC pressure（大量短命物件導致 GC 頻繁）。&lt;/p>
&lt;p>Heap profile 記錄的是某個時間點的 live object 分布。Allocation profile 記錄的是一段時間內誰做了多少 allocation — 兩者互補。Memory leak 用 heap profile 的時間趨勢看；GC pressure 用 allocation profile 看。&lt;/p>
&lt;h3 id="lock-contention-profile">Lock contention profile&lt;/h3>
&lt;p>回答「哪些 lock 的等待時間最長」。適合診斷 mutex contention（多個 thread / goroutine 搶同一把 lock、等待時間累積成 latency）。&lt;/p>
&lt;p>Lock profile 在高並發服務的診斷中特別有用。Metrics 只能看到整體 latency 上升；trace 能看到某個 span 變慢；lock profile 能精確定位是哪把 lock 在哪個 callstack 被等待。&lt;/p>
&lt;h3 id="goroutine--async-task-profile">Goroutine / async task profile&lt;/h3>
&lt;p>Go 的 goroutine profile 回答「有多少 goroutine、它們在做什麼（running / waiting / blocked）」。Goroutine leak（goroutine 數量持續增長、都在等待某個 channel 或 lock）是 Go 服務常見的退化模式。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Continuous profiling 的定位：metrics / logs / traces 之外的第四角</li>
<li>Profile 維度：CPU、heap、allocations、lock contention、goroutine / async task</li>
<li>Always-on vs on-demand：何時用哪種</li>
<li>Flame graph 與版本差異比較</li>
<li>Overhead 控制</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Continuous profiling 是把 CPU、memory、allocation 與 lock contention 變成長期可比較的 production 訊號，責任是補上 metrics、logs、traces 看不到的 callstack 成本。</p>
<p>Metrics 會告訴你「CPU usage 上升了」，trace 會告訴你「這條 request 的 latency 從 200ms 變成 800ms」，profile 會告訴你「增加的 600ms 花在哪幾個 function call、哪幾行程式碼」。Profile 是唯一能精確到 callstack level 的觀測訊號。</p>
<p>「Continuous」的關鍵差異是：傳統 profiling 是事故時才手動開啟，continuous profiling 是 production 常駐的低開銷採樣。事故時不需要重現問題 — baseline profile 已經在那裡，直接跟事故期間的 profile 做 diff。</p>
<h2 id="profile-維度">Profile 維度</h2>
<p>不同的 profile 維度回答不同的效能問題。服務的退化模式決定需要哪些維度。</p>
<h3 id="cpu-profile">CPU profile</h3>
<p>回答「CPU 時間花在哪些 function」。最常用的 profile 維度。適合診斷 latency 退化（某個 function 開始佔更多 CPU 時間）跟 CPU 利用率異常（某段程式碼意外進入 hot path）。</p>
<p>CPU profile 用 sampling 方式採集 — 定期（例如每秒 100 次）記錄當前的 callstack。統計意義上，出現在 sample 中的次數跟實際 CPU 消耗成正比。Sampling 頻率越高精度越好，但 overhead 也越高。</p>
<h3 id="heap--memory-profile">Heap / memory profile</h3>
<p>回答「memory 被哪些 function 持有」。適合診斷 memory leak（allocation 持續增長、GC 回收不了）跟 GC pressure（大量短命物件導致 GC 頻繁）。</p>
<p>Heap profile 記錄的是某個時間點的 live object 分布。Allocation profile 記錄的是一段時間內誰做了多少 allocation — 兩者互補。Memory leak 用 heap profile 的時間趨勢看；GC pressure 用 allocation profile 看。</p>
<h3 id="lock-contention-profile">Lock contention profile</h3>
<p>回答「哪些 lock 的等待時間最長」。適合診斷 mutex contention（多個 thread / goroutine 搶同一把 lock、等待時間累積成 latency）。</p>
<p>Lock profile 在高並發服務的診斷中特別有用。Metrics 只能看到整體 latency 上升；trace 能看到某個 span 變慢；lock profile 能精確定位是哪把 lock 在哪個 callstack 被等待。</p>
<h3 id="goroutine--async-task-profile">Goroutine / async task profile</h3>
<p>Go 的 goroutine profile 回答「有多少 goroutine、它們在做什麼（running / waiting / blocked）」。Goroutine leak（goroutine 數量持續增長、都在等待某個 channel 或 lock）是 Go 服務常見的退化模式。</p>
<p>其他語言有對應的概念：Java 的 thread dump、Node.js 的 async resource tracking、Python 的 asyncio task inspection。</p>
<h2 id="always-on-vs-on-demand">Always-on vs On-demand</h2>
<h3 id="always-oncontinuous">Always-on（continuous）</h3>
<p>Production 常駐的低開銷 profiling。CPU sampling 頻率降低（每秒 19 或 100 次，避免跟系統 timer 共振），heap sampling 用語言 runtime 內建機制（Go 的 <code>runtime/pprof</code>、Java 的 JFR）。</p>
<p>Always-on 的核心價值是 baseline — 平時就有 profile 資料，事故時可以跟 baseline 做 diff，看「哪些 function 的 CPU 消耗跟平時不同」。沒有 baseline 的 profiling 只能看「現在的 profile 長什麼樣」，無法判斷哪些是異常的。</p>
<h3 id="on-demand">On-demand</h3>
<p>事故中或效能調查時手動開啟的高精度 profiling。Sampling 頻率更高、涵蓋更多維度、但 overhead 也更高（可能影響 production 服務的 latency）。</p>
<p>On-demand profiling 適合在 always-on profile 定位到可疑 function 後，做更細粒度的 callstack 分析。兩者搭配使用 — always-on 做日常監控跟 baseline，on-demand 做事故深挖。</p>
<h3 id="overhead-控制">Overhead 控制</h3>
<p>Continuous profiling 的可行性取決於 overhead 是否夠低。目標是 CPU overhead &lt; 1%、memory overhead &lt; 10MB。</p>
<p>影響 overhead 的因素：</p>
<ul>
<li><strong>Sampling 頻率</strong>：CPU profile 每秒 100 次 vs 1000 次，overhead 差一個數量級</li>
<li><strong>採集機制</strong>：eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集，overhead 比 language-level profiler 低；language runtime 內建機制（Go pprof、Java JFR）overhead 居中；instrumentation-based profiler overhead 最高</li>
<li><strong>資料傳輸</strong>：profile 資料定期傳到 backend 的網路跟序列化成本</li>
</ul>
<p>Production 部署前要用 benchmark 驗證 overhead。在 load test 環境開啟 profiling、比較開啟前後的 latency p99 跟 CPU usage — 差異超過 1% 要調整 sampling 頻率或換更輕量的 profiler。</p>
<h2 id="flame-graph-與版本差異比較">Flame Graph 與版本差異比較</h2>
<h3 id="flame-graph">Flame graph</h3>
<p>Flame graph 是 profile 資料的標準視覺化。X 軸是 callstack 的寬度（代表 sample 佔比 = 資源消耗佔比），Y 軸是 callstack 深度（底部是 root function、頂部是 leaf function）。寬的矩形代表消耗多、窄的代表消耗少。</p>
<p>讀 flame graph 的方式是「從寬的開始看」— 最寬的矩形是當前最大的資源消耗者。如果某個 function 佔整個 flame graph 的 40%，它就是最值得最佳化的候選。</p>
<h3 id="diff-flame-graph">Diff flame graph</h3>
<p>Diff flame graph 是兩個 profile 的差異視覺化。紅色代表新版本消耗增加、綠色代表減少。適合用在：</p>
<ul>
<li><strong>版本間比較</strong>：v1.2.3 vs v1.2.4 的 CPU profile diff，看新版本哪些 function 變慢</li>
<li><strong>Canary 對照</strong>：canary instance vs baseline instance 的即時 diff</li>
<li><strong>事故 vs baseline</strong>：事故期間的 profile vs 平時的 profile</li>
</ul>
<p>Diff flame graph 需要 profile 帶 version / deploy label。Profile 跟版本標記失聯時，跨版本比較只能靠手動對照時間範圍 — 精確度跟效率都會下降。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>採集機制</th>
          <th>語言支援</th>
          <th>定位</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pyroscope</td>
          <td>SDK + eBPF</td>
          <td>Go, Java, Python, Ruby</td>
          <td>開源自架，Grafana 生態整合</td>
      </tr>
      <tr>
          <td>Parca</td>
          <td>eBPF</td>
          <td>語言無關（kernel 級）</td>
          <td>開源自架，零 instrumentation</td>
      </tr>
      <tr>
          <td>Datadog Profiler</td>
          <td>Agent + SDK</td>
          <td>Go, Java, Python, .NET</td>
          <td>託管，跟 APM trace 整合</td>
      </tr>
      <tr>
          <td>Polar Signals</td>
          <td>eBPF（Parca Cloud）</td>
          <td>語言無關</td>
          <td>託管 Parca</td>
      </tr>
  </tbody>
</table>
<p>選擇要點：如果已有 Grafana 生態（Prometheus + Loki + Tempo），Pyroscope 整合最自然。如果不想改 application code（零 instrumentation），eBPF-based 的 Parca 是選項。如果已用 Datadog APM，Datadog Profiler 跟 trace 的整合（從 trace span 跳到對應的 profile）是獨有優勢。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Continuous profiling 的持續價值取決於兩件事：profile 能否按版本做 diff（沒有 baseline 就無法判斷哪些 callstack 是異常的），以及 overhead 能否低到 production 常駐（overhead 過高等於回到「事故時才開」的模式）。</p>
<p>重點訊號包括：</p>
<ul>
<li>Profile 是否帶有 service、version、environment 與 deploy label</li>
<li>Flame graph diff 是否能對照 canary / baseline</li>
<li>CPU、heap、lock、allocation 是否覆蓋主要退化模式</li>
<li>Production sampling 是否足夠低成本且常駐穩定</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一段熱點程式碼反覆出現在事故 RCA 中、無 baseline profile</li>
<li>CPU / memory 異常時靠重現除錯、無 production profile 可對照</li>
<li>版本升級後 latency 退化、定位具體 callstack 需要重現環境</li>
<li>Profile 跟 commit / version label 失聯、跨版本 diff 需要人工對照</li>
<li>Profiling overhead 過高、production 環境常駐成本過高</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Profiling 只在事故時才開</td>
          <td>事故時開 profiler 需要時間、問題可能已消失</td>
          <td>Always-on continuous profiling</td>
      </tr>
      <tr>
          <td>Production sampling rate = 0</td>
          <td>Profile 只存在於 staging、production 沒資料</td>
          <td>調低 sampling 頻率到 overhead &lt; 1%</td>
      </tr>
      <tr>
          <td>Profile 跟 version 失聯</td>
          <td>Diff 只能靠時間範圍猜、無法精確比較</td>
          <td>Profile metadata 帶 version / commit hash label</td>
      </tr>
      <tr>
          <td>只看 CPU profile</td>
          <td>Memory leak 跟 lock contention 被忽略</td>
          <td>按服務退化模式選擇 profile 維度</td>
      </tr>
      <tr>
          <td>Profile 資料沒有保留策略</td>
          <td>儲存持續成長、舊 profile 佔空間但沒被查</td>
          <td>依版本保留（每版本保留 N 天）</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：metrics 是聚合訊號、profile 是 callstack 級別</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</a>：trace 是 request 維度、profile 是 process 維度</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 / cost</a>：profile 儲存量與保留策略</li>
<li><a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 rule-level CPU signal</a>：規則執行成本的 CPU 訊號治理</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：RCA 引用 profile flame graph</li>
</ul>
]]></content:encoded></item><item><title>5.9 邊緣分發與靜態資源（CDN / Origin Protection）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</guid><description>&lt;p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取&lt;/a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。&lt;/p>
&lt;h2 id="三層快取的責任分工">三層快取的責任分工&lt;/h2>
&lt;p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。&lt;/p>
&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>CDN edge node、browser cache&lt;/td>
 &lt;td>降低跨網延遲、保護 origin 流量&lt;/td>
 &lt;td>全球節點 purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層&lt;/td>
 &lt;td>Redis、in-memory cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside&lt;/a>&lt;/td>
 &lt;td>降低資料層查詢成本&lt;/td>
 &lt;td>區域 cluster purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料層快取&lt;/td>
 &lt;td>DB buffer pool、query cache&lt;/td>
 &lt;td>降低硬碟 I/O&lt;/td>
 &lt;td>內部自動管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。&lt;/p>
&lt;h2 id="origin-protection-的設計責任">Origin Protection 的設計責任&lt;/h2>
&lt;p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。&lt;/p>
&lt;p>origin protection 的核心策略包含三個方向：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>cache hit ratio 優化&lt;/strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。&lt;/li>
&lt;li>&lt;strong>回源行為控制&lt;/strong>：edge 沒命中時用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede&lt;/a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。&lt;/li>
&lt;li>&lt;strong>failure fallback&lt;/strong>：origin 不健康時、edge 可以回傳舊版本（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error&lt;/a>）、避免使用者直接看到 5xx。代價是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data&lt;/a> 風險暫時提高、需要在 freshness budget 內。&lt;/li>
&lt;/ol>
&lt;p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。&lt;/p>
&lt;p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。&lt;/p>
&lt;h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀&lt;/h2>
&lt;p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資源類型&lt;/th>
 &lt;th>適合放 CDN？&lt;/th>
 &lt;th>判讀理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>靜態 asset（JS/CSS）&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>內容與使用者無關，hash 命名後可長期快取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>圖片、影片&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>公開資源，跨使用者共用，命中率高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品頁、活動頁&lt;/td>
 &lt;td>條件適合&lt;/td>
 &lt;td>對未登入者一致；對登入者需要分版本或退到應用層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訂單頁、會員中心&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>跟特定使用者綁定，邊緣層無法共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>個人化推薦&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>每個請求結果不同，命中率近於零&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入 API&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>邊緣層不該攔截狀態改變&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。&lt;/p></description><content:encoded><![CDATA[<p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取</a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。</p>
<h2 id="三層快取的責任分工">三層快取的責任分工</h2>
<p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>主要載體</th>
          <th>主要責任</th>
          <th>失效成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邊緣層</td>
          <td>CDN edge node、browser cache</td>
          <td>降低跨網延遲、保護 origin 流量</td>
          <td>全球節點 purge</td>
      </tr>
      <tr>
          <td>應用層</td>
          <td>Redis、in-memory cache、<a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a></td>
          <td>降低資料層查詢成本</td>
          <td>區域 cluster purge</td>
      </tr>
      <tr>
          <td>資料層快取</td>
          <td>DB buffer pool、query cache</td>
          <td>降低硬碟 I/O</td>
          <td>內部自動管理</td>
      </tr>
  </tbody>
</table>
<p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。</p>
<h2 id="origin-protection-的設計責任">Origin Protection 的設計責任</h2>
<p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。</p>
<p>origin protection 的核心策略包含三個方向：</p>
<ol>
<li><strong>cache hit ratio 優化</strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。</li>
<li><strong>回源行為控制</strong>：edge 沒命中時用 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede</a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。</li>
<li><strong>failure fallback</strong>：origin 不健康時、edge 可以回傳舊版本（<a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate</a> / <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error</a>）、避免使用者直接看到 5xx。代價是 <a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data</a> 風險暫時提高、需要在 freshness budget 內。</li>
</ol>
<p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。</p>
<p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。</p>
<h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀</h2>
<p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。</p>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>適合放 CDN？</th>
          <th>判讀理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態 asset（JS/CSS）</td>
          <td>適合</td>
          <td>內容與使用者無關，hash 命名後可長期快取</td>
      </tr>
      <tr>
          <td>圖片、影片</td>
          <td>適合</td>
          <td>公開資源，跨使用者共用，命中率高</td>
      </tr>
      <tr>
          <td>商品頁、活動頁</td>
          <td>條件適合</td>
          <td>對未登入者一致；對登入者需要分版本或退到應用層</td>
      </tr>
      <tr>
          <td>訂單頁、會員中心</td>
          <td>不適合</td>
          <td>跟特定使用者綁定，邊緣層無法共用</td>
      </tr>
      <tr>
          <td>個人化推薦</td>
          <td>不適合</td>
          <td>每個請求結果不同，命中率近於零</td>
      </tr>
      <tr>
          <td>寫入 API</td>
          <td>不適合</td>
          <td>邊緣層不該攔截狀態改變</td>
      </tr>
  </tbody>
</table>
<p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。</p>
<p>判讀後仍要再對齊 freshness：商品價格在限時活動期間每 5 分鐘改一次，10 分鐘 TTL 就會出現超賣或顯示差價。這類情境要把價格放應用層快取、頁面結構放 CDN，整頁邊緣化會超出 freshness budget。</p>
<h2 id="purge-與-invalidation-的操作模型">Purge 與 Invalidation 的操作模型</h2>
<p>CDN 的 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">Cache Invalidation</a> 跟應用層的失效路徑不一樣：應用層 purge 在自家 cluster 內可控，CDN purge 要等全球節點同步。傳統 origin-pull CDN 的全球 purge 需要數秒到數十秒；現代 push-based CDN（Cloudflare、Fastly 等）的 instant purge 在 150ms 級別、語意接近同步、但這條能力依 vendor 而異、要事前驗證。</p>
<p>操作上的三種策略各有適用場景：</p>
<ul>
<li><strong>TTL 自然過期</strong>：適合內容變動慢、不需要立即生效的資源。優點是不依賴 purge API，缺點是無法應對緊急下架。搭配 stale-while-revalidate 後可以兼顧低 origin 壓力與最終新鮮度、是現代 default 而非「弱版本」。</li>
<li><strong>顯式 purge</strong>：適合內容變動時要立刻生效的場景（價格更新、文章下架、合規移除）。要把 purge 列入發布流程，事故期能在分鐘內收回錯誤內容。</li>
<li><strong>版本化路徑</strong>：適合 JS/CSS 等可永久快取的資源。檔名含 hash（<code>app.a3f1b2.js</code>），新版本上線時直接換路徑、舊版本自然失效。這是命中率最高的策略，因為可以設定 <code>max-age=31536000, immutable</code>。</li>
</ul>
<p>這三種策略以 origin pull 模型為主、是基底但不窮盡。現代 CDN 還有兩種重要策略需要展開。</p>
<h3 id="tag-based-purge-的操作模型">Tag-based Purge 的操作模型</h3>
<p><a href="/blog/backend/knowledge-cards/cache-tag-purge/" data-link-title="Cache Tag Purge" data-link-desc="CDN / cache 用 tag / surrogate key 批量失效多個關聯資源">Tag-based / surrogate-key purge</a>（Fastly surrogate key、Cloudflare cache tag、Akamai cache tag）是大型內容系統的事實標準。它解決的核心問題是「一個業務事件需要同時失效多個 URL」——商品下架要同時 purge 商品頁、商品圖、搜尋結果頁中含該商品的快取。</p>
<p>操作流程分三步：</p>
<ol>
<li><strong>打 tag</strong>：origin 在 response header 中標記 tag（如 <code>Surrogate-Key: product-123 category-electronics</code>）。CDN 存快取時同時建立 tag → URL 的反向索引。</li>
<li><strong>按 tag purge</strong>：業務系統發出 <code>PURGE tag=product-123</code> API 呼叫，CDN 用反向索引找出所有帶這個 tag 的快取項目並失效。一次 API 呼叫可能失效數百個 URL。</li>
<li><strong>回源補快取</strong>：被 purge 的 URL 下一次被請求時回源、重新快取。搭配 stale-while-revalidate 可以讓第一個回源請求不阻塞使用者。</li>
</ol>
<p>Tag-based purge 跟顯式 purge（按 URL purge）的本質差異在於「失效單位是業務實體、不是 URL」。按 URL purge 要在業務端維護「一個商品對應哪些 URL」的映射，tag purge 把這個映射交給 CDN 的反向索引。代價是 tag 設計要跟業務模型對齊——tag 太粗（一個 tag 覆蓋太多資源）會過度 purge，tag 太細會退化成按 URL purge。</p>
<p><strong>Push-based instant purge</strong>（Cloudflare、Fastly 規格 &lt;150ms 全球同步）讓全球 purge 從「分鐘級」變成「準同步」。選擇策略時要按 vendor 能力跟資源更新模式組合。</p>
<p>選錯策略的代價會在事故時放大。把限時優惠的價格用「TTL 自然過期」策略佈在 CDN、活動結束後仍有客人看到舊價格繼續下單、客服與退款成本會壓回業務端。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>origin 流量隨使用者線性成長</td>
          <td>cache hit ratio 偏低，邊緣層沒發揮 origin protection</td>
          <td>檢查 cache-control header、命中率分布、coalescing 設定</td>
      </tr>
      <tr>
          <td>edge 命中率忽然下降</td>
          <td>purge 設定誤觸全網、或 cache key 設計過細</td>
          <td>檢查近期 purge 操作、vary 與 query string 設計</td>
      </tr>
      <tr>
          <td>purge 後仍看到舊內容</td>
          <td>全球節點同步延遲、或 CDN 與應用層快取沒對齊</td>
          <td>確認 CDN purge 完成訊號、再追應用層快取狀態</td>
      </tr>
      <tr>
          <td>高峰時 origin 出現 5xx 尖峰</td>
          <td>edge 沒做 stale-if-error，origin 過載直接打回使用者</td>
          <td>啟用 stale-while-revalidate、檢查 origin shield 設定</td>
      </tr>
      <tr>
          <td>部分區域延遲偏高</td>
          <td>區域節點覆蓋不足、或回源走錯區域</td>
          <td>檢查路由策略、加開 edge POP、考慮多 CDN 策略</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>CDN 跟「加速工具」的混淆，會讓 origin protection 跟一致性責任被忽略。多數團隊上線後第一次撞牆，是 KOL 引流或活動高峰把 origin 直接打掛，事後才發現 CDN 只覆蓋了靜態 asset、HTML 與 API 都直接打回 origin。</p>
<p>把 purge 當成同步操作也容易出事。緊急下架觸發 purge 後立刻通知公關「已下線」，但全球節點還沒收斂，仍有區域看到原內容。這類風險要把「purge 已完成」當成可觀測訊號處理，不是 API 回 200 就視為完成。</p>
<p>把 CDN 當成應用層快取替代品則是另一個極端。商品價格、會員等級這類「跟使用者狀態相關」的資料放邊緣層，會在用戶切帳號、優惠變更時暴露其他人的資料或舊狀態，是 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a> 的擴大版。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>CDN 專注「靜態與半靜態內容的網路層分發」。當問題進入動態 API 的延遲、跨服務一致性、寫入路徑保護，責任分別交給 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 模組。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">07 入口治理</a> 的交接：CDN 同時是公網入口，需要承接 WAF、bot mitigation、TLS termination 等資安責任。邊緣層的安全設定不可遺漏，否則 origin 被繞過直接攻擊。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>邊緣分發策略可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar：1800 萬同時觀眾的 IPL 直播</a> — 極端峰值靠多 CDN + origin shield 把 origin 流量壓在容量範圍內。Hotstar 的具體做法是把 hot content（live stream segment）跟 warm content（VOD）分配到不同 CDN provider、利用「edge cache miss 時不是同時打 origin」這條 cache stampede 防禦機制讓 origin 流量曲線跟使用者請求曲線解耦。對照本章「origin protection」段三大策略落地。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — 30 倍突發中，登入頁、會議連結頁這類靜態資源由邊緣層吸收絕大部分讀取流量，API 叢集只面對真實的會議建立 / 結束請求。對照本章「Cacheable vs Non-Cacheable 判讀」段：登入頁屬未登入者一致、適合邊緣化；會議內互動屬寫入 API、保持在 origin。</li>
<li><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve 與 Tiered Storage</a> — Cloudflare 在 CDN 內部再分一層 Cache Reserve（持久層）、把 warm 內容從 origin 卸下、避免 edge LRU 淘汰後又回到 origin。對照本章「三層快取」段：邊緣層內部本身也能有 hot / warm 分層、是同一概念的遞迴應用。</li>
</ul>
<p>三個案例依規模從外向內展開：Hotstar 是極端峰值下 origin protection 防禦的天花板測試、Zoom 是把非交易流量（登入 / 連結頁）分流降低 API 叢集壓力的標準應用、Cloudflare Cache Reserve 則展示 CDN vendor 自身把 hot / warm 內容再分層的內部架構。讀者可串著讀理解規模光譜、也可以挑一條深入。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 的交接：應用層快取與邊緣層的失效路徑要對齊，避免兩層 stale 同時發生。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 的交接：edge miss 後流量進到 origin LB，超時與重試設定要協調。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理</a> 的交接：CDN 是公網入口，WAF、TLS 與 bot mitigation 在邊緣層落地。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：cache hit ratio 是 origin 容量規劃的核心輸入，命中率假設失準會直接撞牆。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a></strong>：邊緣層擋住讀流量後、寫流量與事務鏈的下一塊是非同步化。</p>
<p>其他延伸方向：</p>
<ul>
<li>邊緣失效跟應用層失效串成 invalidation pipeline → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a></li>
<li>高峰活動把 CDN 跟排隊機制組合成保護網 → <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>Origin 端的入口流量合約 → <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a></li>
</ul>
]]></content:encoded></item><item><title>AWS ACM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/</guid><description>&lt;p>AWS Certificate Manager (ACM) 是 AWS-managed 的 &lt;em>certificate provisioning 服務&lt;/em>、解決兩件事：&lt;em>public TLS cert 全自動化&lt;/em>（Amazon Trust Services 簽發、DNS validation 通過後 60 天前自動 renew）跟 &lt;em>AWS-managed service 的 cert 整合&lt;/em>（&lt;a href="https://docs.aws.amazon.com/acm/latest/userguide/acm-services.html">ELB / CloudFront / API Gateway / App Runner&lt;/a> 直接 attach、不需要客戶持有私鑰）。內部 mTLS / 自管 endpoint 的 private cert 走另一個產品 ACM Private CA（PCA）— ACM 是 &lt;em>frontend&lt;/em>、PCA 是 &lt;em>自管 CA hierarchy backend&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>ACM 的核心定位是 &lt;em>AWS 平台內 cert 的全託管 lifecycle&lt;/em>。客戶不持私鑰、不跑 ACME client、不手動 renew — 但代價是 ACM public cert &lt;em>只能 attach 到 AWS-managed service&lt;/em>（ELB / CloudFront / API Gateway / App Runner / Nitro Enclaves）、不能 export 給自管 Nginx / EC2 應用。Private cert 必須有 ACM Private CA (PCA) 後端、ACM 自己不是 CA。&lt;/p>
&lt;p>跟其他 cert 工具的場景重疊度低、定位是分工互補：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&amp;#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &amp;#43; Challenge solver">cert-manager&lt;/a> 走 cluster 內 K8s workload cert（Ingress / service mesh）、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&amp;#39;s Encrypt" data-link-desc="免費 &amp;#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&amp;rsquo;s Encrypt&lt;/a> 走跨平台公共 ACME cert（可 export 任何地方使用）、ACM Private CA 走自管 CA hierarchy（root + intermediate、客戶控制 policy）。常見組合：AWS-native endpoint 用 ACM、K8s workload + 自管伺服器走 cert-manager + Let&amp;rsquo;s Encrypt、內部 mTLS root 走 PCA。詳細差異見「核心取捨表」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>ACM public cert vs private cert vs imported cert 各自的使用邊界（能 attach 哪些 service、能不能 export）&lt;/li>
&lt;li>DNS validation vs Email validation 的差異、跟 auto-renewal 條件的關聯&lt;/li>
&lt;li>跨 region 跟 CloudFront 的 us-east-1 限制如何處理&lt;/li>
&lt;li>何時 ACM 不夠用、要改走 cert-manager / Let&amp;rsquo;s Encrypt / ACM Private CA&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 ACM cert 部署是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>AWS Certificate Manager (ACM) 是 AWS-managed 的 <em>certificate provisioning 服務</em>、解決兩件事：<em>public TLS cert 全自動化</em>（Amazon Trust Services 簽發、DNS validation 通過後 60 天前自動 renew）跟 <em>AWS-managed service 的 cert 整合</em>（<a href="https://docs.aws.amazon.com/acm/latest/userguide/acm-services.html">ELB / CloudFront / API Gateway / App Runner</a> 直接 attach、不需要客戶持有私鑰）。內部 mTLS / 自管 endpoint 的 private cert 走另一個產品 ACM Private CA（PCA）— ACM 是 <em>frontend</em>、PCA 是 <em>自管 CA hierarchy backend</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>ACM 的核心定位是 <em>AWS 平台內 cert 的全託管 lifecycle</em>。客戶不持私鑰、不跑 ACME client、不手動 renew — 但代價是 ACM public cert <em>只能 attach 到 AWS-managed service</em>（ELB / CloudFront / API Gateway / App Runner / Nitro Enclaves）、不能 export 給自管 Nginx / EC2 應用。Private cert 必須有 ACM Private CA (PCA) 後端、ACM 自己不是 CA。</p>
<p>跟其他 cert 工具的場景重疊度低、定位是分工互補：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> 走 cluster 內 K8s workload cert（Ingress / service mesh）、<a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> 走跨平台公共 ACME cert（可 export 任何地方使用）、ACM Private CA 走自管 CA hierarchy（root + intermediate、客戶控制 policy）。常見組合：AWS-native endpoint 用 ACM、K8s workload + 自管伺服器走 cert-manager + Let&rsquo;s Encrypt、內部 mTLS root 走 PCA。詳細差異見「核心取捨表」。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>ACM public cert vs private cert vs imported cert 各自的使用邊界（能 attach 哪些 service、能不能 export）</li>
<li>DNS validation vs Email validation 的差異、跟 auto-renewal 條件的關聯</li>
<li>跨 region 跟 CloudFront 的 us-east-1 限制如何處理</li>
<li>何時 ACM 不夠用、要改走 cert-manager / Let&rsquo;s Encrypt / ACM Private CA</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 ACM cert 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>Cert 跟 service 整合</strong>：cert ARN 是否真的 attach 到 ELB / CloudFront / API Gateway listener、<code>DescribeCertificate</code> 的 <code>InUseBy</code> 有沒有資源、有 cert 但沒 attach 等於 issue 失敗</li>
<li><strong>DNS validation 設定</strong>：cert 是 DNS 還是 Email validation、DNS 的 CNAME record 是否還留在 DNS（auto-renewal 需要這條 record 持續存在）、Route53 vs 外部 DNS 的責任分界</li>
<li><strong>Renewal status</strong>：<code>DescribeCertificate</code> 的 <code>RenewalSummary.RenewalStatus</code> 是 <code>SUCCESS</code> / <code>PENDING_AUTO_RENEWAL</code> / <code>FAILED</code>、失敗時 <code>RenewalStatusReason</code> 是什麼（多半是 DNS record 被刪、CNAME 不再回應）</li>
<li><strong>CloudTrail 證據</strong>：<code>RequestCertificate</code> / <code>ImportCertificate</code> / <code>DeleteCertificate</code> 的 caller identity、是否有非預期的 cert 建立或刪除（防誤刪 / 惡意刪）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle</a> 的覆蓋缺口。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Request public cert</strong>：對 internet-facing endpoint（網站、API）issue public cert、走 <code>RequestCertificate</code> API、選 DNS validation。ACM 給一組 CNAME record、放進 DNS（Route53 可一鍵 create）、ACM 自動驗證 + issue。Cert 生效後 attach 到 ELB / CloudFront / API Gateway listener。Issuer 是 Amazon Trust Services、所有主流瀏覽器 / OS trust store 都認。</p>
<p><strong>Request private cert（需 PCA 後端）</strong>：內部 service mTLS root、走 <code>RequestCertificate</code> 但指定 PCA ARN。ACM 透過 PCA 簽 cert、cert chain 是組織內部 CA hierarchy。Trust store 必須在各 workload 手動建立（不像 public cert 自動 trust）。</p>
<p><strong>DNS validation vs Email validation</strong>：DNS validation 是預設 + 推薦 — CNAME record 放進 DNS 後、ACM 持續驗證 domain ownership、auto-renewal 全自動。Email validation 是 legacy、ACM 寄信到 domain 的 WHOIS / 預設 admin email、人工點連結驗證；auto-renewal 不會自動完成、cert 到期前必須手動 re-validate。Production 一律用 DNS validation。</p>
<p><strong>Auto-renewal 條件</strong>：ACM 在 cert lifetime 60 天前嘗試 renew、條件嚴格：(1) cert 是 ACM-issued（不是 imported）(2) DNS validation 走 CNAME record 仍存在且可回應 (3) cert 至少 attach 到一個 AWS service。三個條件任一不滿足、renewal 不自動觸發、cert 會 expire。Imported cert <em>完全不自動 renew</em>、必須在 expiry 前手動 re-import。</p>
<p><strong>跟 ELB / CloudFront / API Gateway 整合</strong>：ELB / API Gateway 用所在 region 的 ACM cert、CloudFront 例外 — <em>只認 us-east-1 region 的 ACM cert</em>（CloudFront edge 是 global、cert metadata 統一從 us-east-1 拉）。Multi-region app 要在每個 region 各 request 一份 cert、CloudFront 那份固定放 us-east-1。</p>
<p><strong>Imported certificate</strong>：自管 cert（外部 CA 簽的、舊系統遷移過來的）可以 import 進 ACM、拿到 ARN 後一樣 attach 到 AWS service。代價是 <em>ACM 不會 renew</em>、expiry 前必須手動 re-import 新版。常見事故源：imported cert 過期、AWS service 突然 serve expired cert、Browser 顯示警告。建議 imported cert 都設 CloudWatch alarm 監 <code>DaysToExpiry</code>。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 整合</strong>：誰能 issue / delete cert 走 IAM policy 控制 — <code>acm:RequestCertificate</code> / <code>acm:DeleteCertificate</code> / <code>acm:ImportCertificate</code>。Tag-based access control 可以限定「只有帶 <code>team=platform</code> tag 的 cert 才能被 platform team IAM role 改」、防誤刪 production cert。Cert 是 region-scoped resource、IAM policy 可指定 <code>Resource</code> ARN 限定 region / cert ID。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>ACM (public)</th>
          <th>ACM Private CA (PCA)</th>
          <th>cert-manager + Let&rsquo;s Encrypt</th>
          <th>手動 OpenSSL CA</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>AWS managed</td>
          <td>AWS managed CA hierarchy</td>
          <td>K8s cluster 內 self-hosted controller</td>
          <td>手動腳本</td>
      </tr>
      <tr>
          <td>私鑰持有</td>
          <td>AWS 持有、客戶不能 export</td>
          <td>AWS 持有 CA key、subordinate 可 export</td>
          <td>cluster 內 Secret、可 export</td>
          <td>自己持有</td>
      </tr>
      <tr>
          <td>Issuer</td>
          <td>Amazon Trust Services（public trust store）</td>
          <td>客戶自管 CA（內部 trust）</td>
          <td>Let&rsquo;s Encrypt / 任何 ACME CA</td>
          <td>自簽</td>
      </tr>
      <tr>
          <td>適用 endpoint</td>
          <td>AWS-managed service（ELB / CloudFront / API GW）</td>
          <td>內部 mTLS、AWS service 也可用</td>
          <td>K8s workload、Ingress、任何持有 PEM 的服務</td>
          <td>實驗 / 內部小規模</td>
      </tr>
      <tr>
          <td>Auto-renewal</td>
          <td>DNS validation 全自動</td>
          <td>透過 ACM 自動</td>
          <td>cert-manager 自動</td>
          <td>自己寫 cron</td>
      </tr>
      <tr>
          <td>跨雲 / 跨平台</td>
          <td>弱 — AWS 內</td>
          <td>弱 — AWS 內</td>
          <td>強 — K8s 在哪都可</td>
          <td>強</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>public cert 免費</td>
          <td>per CA + per cert（PCA 較貴）</td>
          <td>免費（Let&rsquo;s Encrypt）</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-heavy + edge endpoint</td>
          <td>內部 mTLS root + AWS 整合</td>
          <td>K8s workload + 跨雲</td>
          <td>實驗、極小規模</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — cert 重 issue 但 service 配置要改</td>
          <td>高 — CA hierarchy 遷移痛苦</td>
          <td>低 — PEM 在手、換 issuer 容易</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 ACM 的核心訴求：cert 主要 attach 到 AWS-managed service、希望 cert 完全 hands-off、不需要 export 私鑰、能接受 AWS lock-in。需要 export PEM 或跨雲 / 自管 endpoint、改走 cert-manager + Let&rsquo;s Encrypt。需要內部 mTLS root + CA hierarchy 控制、走 ACM Private CA。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>ACM Private CA hierarchy</strong>：PCA 支援 root CA + 多層 intermediate CA、生產建議 root CA 離線（CA 簽完 intermediate 後 disable）、日常簽發走 subordinate CA。Subordinate CA compromise 時 revoke 該層、root 不受影響。Cert policy（path length、key usage、name constraint）在 CA 建立時設定、之後無法改、設計時要算對。</p>
<p><strong>Cross-region cert（CloudFront 的 us-east-1 限制）</strong>：CloudFront 是 global service、但 attach 的 ACM cert <em>必須在 us-east-1</em>。Multi-region 部署：每個 region 各 issue 一份 cert 給該 region 的 ELB / API Gateway、CloudFront 的那份單獨在 us-east-1 issue。Terraform / CloudFormation 要顯式宣告 provider region。</p>
<p><strong>Imported cert 跟 auto-renewal 邊界</strong>：imported cert（外部 CA 簽的）ACM 知道存在、可以 attach、但 <em>不 renew</em>。常見事故：團隊 import cert 後忘了；幾個月後 cert 到期；CloudFront / ELB serve expired cert；客戶看到 browser 警告。對策：所有 imported cert 設 CloudWatch alarm <code>DaysToExpiry &lt; 30</code>、<code>AlmostExpired</code> event 推 EventBridge → PagerDuty。長期策略是把 imported cert 都遷移成 ACM-issued cert（如果 domain ownership 可驗證）。</p>
<p><strong>Tag-based access control</strong>：cert 加 tag（<code>team=platform</code>、<code>env=prod</code>）後、IAM policy 用 <code>Condition</code> 限定：只有同 tag 的 role 才能 update / delete。防誤刪 production cert（dev IAM role 跑 cleanup script 不會誤刪 prod）。配合 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 的 ABAC 模型運作。</p>
<p><strong>Wildcard cert 跟 SAN cert</strong>：ACM 支援 wildcard（<code>*.example.com</code> 涵蓋一層 subdomain）跟 SAN（一張 cert 多個 domain，最多 100 個）。Wildcard 簡化部署但 blast radius 大 — 一張 cert compromise 等於整個 subdomain tree 出事；SAN cert 細粒度但管理成本高。Production 建議按服務邊界拆 — 每個 service 一張 cert、不共用 wildcard，除非確實有大量短 lifecycle subdomain。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Cert PENDING_VALIDATION 一直卡住</strong>：DNS validation CNAME record 沒放對、或 DNS provider 緩存太久 — 用 <code>dig</code> 直接查 CNAME 是否生效、Route53 + ACM 整合通常幾分鐘、外部 DNS 可能 30 分鐘以上</li>
<li><strong>Cert renewal FAILED</strong>：<code>RenewalStatusReason</code> 多半是 <code>DOMAIN_VALIDATION_DENIED</code>（CNAME record 被刪了）或 cert 沒 attach 到任何 service — 補回 CNAME record、或把 cert attach 到至少一個 resource</li>
<li><strong>CloudFront 找不到 cert</strong>：cert 在 us-east-1 以外的 region issue — 在 us-east-1 重 issue、或用 Terraform 顯式跨 provider 設定</li>
<li><strong>Imported cert expired</strong>：忘了 manual renewal、AWS service serve expired cert — CloudWatch alarm + EventBridge 推 alert、長期遷成 ACM-issued</li>
<li><strong>ACM cert 無法用在 EC2 自管 Nginx</strong>：public cert 私鑰不能 export 是設計限制 — 改用 ACM Private CA 或 Let&rsquo;s Encrypt + cert-manager</li>
<li><strong>誤刪 production cert</strong>：沒設 tag-based protection、admin script bug — 開 deletion protection（暫時無內建、用 IAM Condition 限定 delete operation + 24h cooldown via Lambda）+ CloudTrail alert 上 <code>acm:DeleteCertificate</code></li>
<li><strong>Cross-account cert 共用</strong>：ACM cert 不支援 RAM 共用 — 跨 account 要在每個 account 各 issue（或用 PCA + RAM 共用 PCA、各 account 從 PCA issue）</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s workload mTLS / Ingress TLS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> + Let&rsquo;s Encrypt / 內部 issuer</td>
      </tr>
      <tr>
          <td>自管 Nginx / EC2 / 跨雲 endpoint</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> + 自管 ACME client</td>
      </tr>
      <tr>
          <td>內部 mTLS root + CA hierarchy 控制</td>
          <td>ACM Private CA（PCA）或 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> PKI engine</td>
      </tr>
      <tr>
          <td>Workload identity（SPIFFE）跨平台</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
      </tr>
      <tr>
          <td>Cert renewal 證據鏈（rotation evidence）</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
      <tr>
          <td>Cert + session invalidation 邊界</td>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理</a>、cert renew 跟 session token 是兩條獨立 lifecycle</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ACM Private CA 完整 hierarchy 設計（root CA 離線儲存、HSM-backed CA key、CRL / OCSP responder 部署）</li>
<li>ACM API 完整 CLI reference 跟 Terraform resource 詳盡欄位</li>
<li>TLS protocol 本身（TLS 1.2 vs 1.3、cipher suite、handshake 流程）</li>
<li>Certificate Transparency log 跟 SCT embedding 內部機制</li>
<li>各 browser / OS trust store 的更新週期</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>ACM 在 07 案例庫沒有直接 vendor-level 事件、以下採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 ACM 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle (section)</a></td>
          <td>ACM 是 AWS 平台 cert lifecycle 自動化的具體落地 — DNS validation + auto-renewal 是 <em>自動化覆蓋率</em> 的指標、imported cert 是覆蓋缺口、要單獨設 alarm 兜底</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — cert 自動 renew 不等於 session 自動 invalidate、舊 session token 在新 cert 下仍可重放、session lifecycle 是另一層責任、不在 ACM 範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">Credential Rotation Scoped Evidence (section)</a></td>
          <td>ACM renewal 自動、但 <em>Certificate Transparency log 比對</em> + <em>fleet-wide trust bundle update</em> 是另一條 evidence chain、要跟 SBOM / CMDB 對齊</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.4 傳輸信任與憑證生命週期</a>、<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a>、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>（誰能 issue / delete cert）、<a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>（PCA CA key 後端）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（cert expiry / mis-issuance 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/acm/">AWS Certificate Manager Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.9 Reconciliation 與 Data Repair</title><link>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</guid><description>&lt;p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。&lt;/p>
&lt;p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。&lt;/p>
&lt;h2 id="reconciliation">Reconciliation&lt;/h2>
&lt;p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。&lt;/p>
&lt;p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。&lt;/p>
&lt;h3 id="對帳系統的設計欄位">對帳系統的設計欄位&lt;/h3>
&lt;p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。&lt;/p>
&lt;p>&lt;strong>來源 A 與來源 B&lt;/strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。&lt;/p>
&lt;p>&lt;strong>比對鍵（comparison key）&lt;/strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。&lt;/p>
&lt;p>&lt;strong>時間窗（time window）&lt;/strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。&lt;/p>
&lt;p>&lt;strong>差異分類規則&lt;/strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。&lt;/p>
&lt;p>&lt;strong>Output schema&lt;/strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。&lt;/p>
&lt;h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異&lt;/h3>
&lt;p>兩件事都是「找資料異常」、但本質不同、不能互相替代。&lt;/p>
&lt;p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。&lt;/p>
&lt;p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。&lt;/p>
&lt;p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。&lt;/p>
&lt;h2 id="不一致的三種分類">不一致的三種分類&lt;/h2>
&lt;p>不是所有「資料不一致」都一樣。按 &lt;em>成因&lt;/em> 分三類、各有不同處理策略。&lt;/p>
&lt;h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：replication lag、async event delivery、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency&lt;/a>&lt;/li>
&lt;li>特徵：兩邊都是「對的」、只是 &lt;em>時間點&lt;/em> 不同&lt;/li>
&lt;li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步&lt;/li>
&lt;li>處理：等待收斂或主動觸發 sync、不必修資料&lt;/li>
&lt;li>持續時間：通常 &amp;lt; 1 秒到分鐘級&lt;/li>
&lt;/ul>
&lt;h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：schema migration 期間、dual-write 失敗、partial write&lt;/li>
&lt;li>特徵：兩邊應該一致但實際不一致、其中一邊是 &lt;em>錯的&lt;/em>&lt;/li>
&lt;li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row&lt;/li>
&lt;li>處理：必須修復、不能等&lt;/li>
&lt;li>持續時間：永久（直到修復）&lt;/li>
&lt;/ul>
&lt;h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：業務邏輯 bug、應用層 race condition、人工誤操作&lt;/li>
&lt;li>特徵：資料結構 OK、但 &lt;em>業務語意&lt;/em> 錯&lt;/li>
&lt;li>例：訂單付款狀態是 &lt;code>paid&lt;/code> 但金流端是 &lt;code>refunded&lt;/code>、帳戶餘額跟交易紀錄 sum 不符&lt;/li>
&lt;li>處理：複雜、需要業務判斷哪邊是 source of truth&lt;/li>
&lt;li>持續時間：永久（且容易擴大）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>處理優先序&lt;/strong>：Semantic &amp;gt; Structural &amp;gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。&lt;/p></description><content:encoded><![CDATA[<p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。</p>
<p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。</p>
<h2 id="reconciliation">Reconciliation</h2>
<p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。</p>
<p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。</p>
<h3 id="對帳系統的設計欄位">對帳系統的設計欄位</h3>
<p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。</p>
<p><strong>來源 A 與來源 B</strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。</p>
<p><strong>比對鍵（comparison key）</strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。</p>
<p><strong>時間窗（time window）</strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。</p>
<p><strong>差異分類規則</strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。</p>
<p><strong>Output schema</strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。</p>
<h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異</h3>
<p>兩件事都是「找資料異常」、但本質不同、不能互相替代。</p>
<p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。</p>
<p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。</p>
<p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。</p>
<h2 id="不一致的三種分類">不一致的三種分類</h2>
<p>不是所有「資料不一致」都一樣。按 <em>成因</em> 分三類、各有不同處理策略。</p>
<h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）</h3>
<ul>
<li>來源：replication lag、async event delivery、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
<li>特徵：兩邊都是「對的」、只是 <em>時間點</em> 不同</li>
<li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步</li>
<li>處理：等待收斂或主動觸發 sync、不必修資料</li>
<li>持續時間：通常 &lt; 1 秒到分鐘級</li>
</ul>
<h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）</h3>
<ul>
<li>來源：schema migration 期間、dual-write 失敗、partial write</li>
<li>特徵：兩邊應該一致但實際不一致、其中一邊是 <em>錯的</em></li>
<li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row</li>
<li>處理：必須修復、不能等</li>
<li>持續時間：永久（直到修復）</li>
</ul>
<h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）</h3>
<ul>
<li>來源：業務邏輯 bug、應用層 race condition、人工誤操作</li>
<li>特徵：資料結構 OK、但 <em>業務語意</em> 錯</li>
<li>例：訂單付款狀態是 <code>paid</code> 但金流端是 <code>refunded</code>、帳戶餘額跟交易紀錄 sum 不符</li>
<li>處理：複雜、需要業務判斷哪邊是 source of truth</li>
<li>持續時間：永久（且容易擴大）</li>
</ul>
<p><strong>處理優先序</strong>：Semantic &gt; Structural &gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。</p>
<h2 id="偵測模式">偵測模式</h2>
<p>不同類型的不一致需要不同偵測模式。</p>
<h3 id="continuous-detection持續偵測">Continuous Detection（持續偵測）</h3>
<ul>
<li>每筆寫入跑 sanity check（trigger、constraint）</li>
<li>應用層 invariant check</li>
<li>適合：structural inconsistency（讓 DB 自己擋）</li>
<li>成本：每筆寫入有 overhead</li>
</ul>
<h3 id="scheduled-detection定期對帳">Scheduled Detection（定期對帳）</h3>
<ul>
<li>每 N 分鐘 / 每天跑對帳 query</li>
<li>跟外部 provider 比對</li>
<li>適合：semantic inconsistency（業務級對齊）</li>
<li>成本：對帳 query 本身耗資源</li>
</ul>
<h3 id="sampling-detection抽樣偵測">Sampling Detection（抽樣偵測）</h3>
<ul>
<li>不跑全表、抽樣 10% / 1% 跑 checksum</li>
<li>適合：大表（全表對帳成本高）</li>
<li>成本：可能漏掉低頻 inconsistency</li>
</ul>
<h3 id="reactive-detection反應式偵測">Reactive Detection（反應式偵測）</h3>
<ul>
<li>用戶 / 客服回報後才查</li>
<li>適合：尾長 inconsistency（找不到通用 pattern）</li>
<li>成本：用戶體驗已受影響</li>
</ul>
<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%">9.C20 Zomato</a> — migration 期間 <a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> 持續對帳、抓 mapping 規則漂移。</p>
<h2 id="data-repair">Data Repair</h2>
<p>Data repair 的責任是把已確認的資料差異修回正式狀態、並保留修復原因、範圍、證據與回退條件。修復可以是 SQL update、補事件、補發 webhook、重建 projection 或人工客服流程、但每種修復都要有範圍控制。</p>
<p>資料修復要先分成三種：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>說明</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>欄位修復</td>
          <td>修正單筆或小批正式欄位</td>
          <td>mapping 規則錯誤會造成二次污染</td>
      </tr>
      <tr>
          <td>派生狀態重建</td>
          <td>重建 index、cache、read model</td>
          <td>可能掩蓋正式狀態尚未修復</td>
      </tr>
      <tr>
          <td>補償動作</td>
          <td>補退款、補發票、補通知</td>
          <td>可能產生重複副作用</td>
      </tr>
  </tbody>
</table>
<p>修復前要先確認問題落在哪一層。正式欄位錯誤要修 source of truth；派生狀態錯誤要重建副本；外部副作用漏做要走補償流程。</p>
<p>欄位修復的判讀重點是 mapping 規則是否正確、因為錯誤規則會把單點差異擴成批次污染。派生狀態重建的判讀重點是 source of truth 是否已經正確、否則重建會複製錯誤。補償動作的判讀重點是副作用是否可逆、因為退款、通知或外部 webhook 可能已經被使用者或第三方看見。</p>
<h2 id="repair-原則">Repair 原則</h2>
<p>不管哪種修復、都遵守三個原則：</p>
<h3 id="1-idempotency冪等">1. Idempotency（冪等）</h3>
<ul>
<li>同樣的修復跑兩次、結果跟跑一次一樣</li>
<li>用 <code>WHERE current_value != target_value</code> 而不是無條件 update</li>
<li>補通知 / webhook 帶 idempotency key、第三方可去重</li>
<li>對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency 卡片</a></li>
</ul>
<h3 id="2-auditable可稽核">2. Auditable（可稽核）</h3>
<ul>
<li>每次修復都有 record：誰、什麼時候、改了什麼、為什麼</li>
<li>修復前 + 修復後的 snapshot 都要存</li>
<li>對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log 卡片</a>、<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team</a> 的 audit 段</li>
</ul>
<h3 id="3-reversible可逆">3. Reversible（可逆）</h3>
<ul>
<li>萬一修復是錯的、能回退到 before state</li>
<li>不可逆操作（DELETE）必須有 dry-run、必須備份</li>
<li>對應 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window 卡片</a></li>
</ul>
<h2 id="修復前的-dry-run-與-impact-assessment">修復前的 dry-run 與 impact assessment</h2>
<p>修復前要先回答「這次修復會碰多少筆、影響多少業務、最壞情況是什麼」、才能進入執行。直接跑 update 是 production-grade 流程的反例、即使在 incident 壓力下也不能跳過這步。</p>
<p><strong>Dry-run 的責任</strong>：把 update 改成 select、用同樣的 WHERE 條件、產出將被修改的資料樣本。Dry-run 結果要包含：影響筆數總計、影響金額或業務值（如果有）、affected tenant / user list 的抽樣、未涵蓋的邊界 case。Dry-run 跟正式修復必須共用 mapping 規則、否則 dry-run 結果無法當審核依據。</p>
<p><strong>規模分級的執行策略</strong>：影響筆數會決定執行方式。</p>
<ul>
<li><strong>單筆到十筆</strong>：客服等級的修復、一名工程師執行 + 一名同儕審核 + audit log 即可。</li>
<li><strong>百筆到千筆</strong>：要在低流量時段執行、分批跑、每批跑完比對 invariant、發現意外停下。</li>
<li><strong>萬筆以上</strong>：當成 production deploy 處理、要有 deploy review、staged rollout（先 1% tenant、再 10%、再全量）、跟 oncall 同步。</li>
<li><strong>跨表 / 跨 service</strong>：必須先做跨團隊 review、確認下游依賴（cache、search index、外部 webhook）的處理計畫、不能單一團隊獨自決定。</li>
</ul>
<p><strong>Impact assessment 的必看欄位</strong>：除了筆數、還要看 <em>連帶影響</em>。修復 orders 表會不會觸發 audit trigger 把每筆寫進 audit log 表？會不會觸發 outbox event 把每筆當成新事件對外發布？會不會讓某 tenant 的 metric 一次性異常、誤觸 alert？這些 second-order effect 在 dry-run 階段就要識別、否則修復本身會變成新事故。</p>
<p><strong>Sandbox / staging 驗證</strong>：不可逆或大規模修復、先在 staging 跑一次、確認 query plan、執行時間、lock 行為。Production 規模沒辦法在 staging 重現的話、至少要在 production 的某個低風險 tenant / region 先試跑、再擴大。</p>
<p><strong>Approval gate（4-eyes process）</strong>：超出單筆規模或修復金錢、權限、個資的場合、必須 <em>兩位以上人員</em> 各自看過 dry-run 結果再簽核。常見實作是：執行者提 PR / ticket 帶 dry-run output、reviewer 簽核後才能執行、執行後產出 audit log 帶兩人簽核紀錄。Reviewer 的責任不是橡皮圖章、是獨立驗證 dry-run 結果跟 incident 描述一致。</p>
<h2 id="repair-patterns">Repair Patterns</h2>
<p>實務上常見的 repair pattern：</p>
<h3 id="pattern-1條件式-update">Pattern 1：條件式 UPDATE</h3>
<p>最簡單也最安全的修復。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;paid&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">12345</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">payment_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;abc&#39;</span><span class="p">;</span></span></span></code></pre></div><p><code>AND</code> 條件確保只在 <em>當前狀態符合預期</em> 時才改、避免 race condition。</p>
<h3 id="pattern-2批次修復--節流">Pattern 2：批次修復 + 節流</h3>
<p>大量資料修復、必須節流避免影響 production。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 每批 100 筆、間隔 1 秒
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;fixed&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill 卡片</a> — backfill 跟 batch repair 是同類技術。</p>
<h3 id="pattern-3補事件--補-webhook">Pattern 3：補事件 / 補 webhook</h3>
<p>外部副作用漏做時、補發事件。</p>
<ul>
<li>必須帶 idempotency key（third-party 才能去重）</li>
<li>紀錄補發原因（incident report 連結）</li>
<li>注意：補發前確認 third-party 是否真的沒收到</li>
</ul>
<h3 id="pattern-4重建-derived-state">Pattern 4：重建 derived state</h3>
<p>cache 跟 search index 是 derived state、出錯通常 <em>砍掉重建</em>。</p>
<ul>
<li>不是直接修 cache value、是 invalidate 讓下次 read 重算</li>
<li>大規模重建用 batch job 跑、避免 thundering herd</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a> feature store 重建模式</li>
</ul>
<h3 id="pattern-5point-in-time-recovery">Pattern 5：Point-in-time Recovery</h3>
<p>當資料 <em>損毀且無法重建</em> 時、靠 backup recovery。</p>
<ul>
<li>PostgreSQL：WAL + base backup → PITR</li>
<li>MySQL：binlog + snapshot → PITR</li>
<li>Aurora：cluster snapshot + continuous backup</li>
<li>注意：recovery 期間可能要 <em>整個 DB restore</em>、影響範圍大</li>
</ul>
<h2 id="repair-runbook">Repair Runbook</h2>
<p>Repair runbook 的責任是讓資料修復可重複執行、並降低對當下工程師記憶的依賴。最小 runbook 需要包含：</p>
<ol>
<li>差異查詢與 query link</li>
<li>影響範圍與 tenant / region / time range</li>
<li>修復方式與 dry-run 結果</li>
<li>審核 owner 與執行 owner</li>
<li>rollback condition 與後續 validation query</li>
</ol>
<p>runbook 要和 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 共用語意。若查詢與修復程式用不同 mapping 規則、修復結果就難以被同一份 evidence 驗證。</p>
<h2 id="audit-與權限邊界">Audit 與權限邊界</h2>
<p>Data repair 常常需要高權限、因此必須接到 audit 與資料保護邊界。修復個資、付款、權限或方案資料時、要保留操作者、審核者、查詢範圍、寫入範圍與修復前後樣本。</p>
<p><strong>Audit log 必要欄位</strong>：</p>
<ul>
<li>timestamp（操作時間）</li>
<li>actor（誰執行）</li>
<li>reviewer（誰審核、如果是 4-eyes process）</li>
<li>query（執行了什麼 SQL / API call）</li>
<li>before / after snapshot（值的變化）</li>
<li>reason（為什麼做這次修復、incident ID）</li>
<li>rollback path（如何回退）</li>
</ul>
<p>這裡要接到 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 Audit Trail 與 Accountability Boundary</a>。資料修復同時是可靠性、資安與合規問題。</p>
<h3 id="權限分離與憑證時效">權限分離與憑證時效</h3>
<p>修復權限不該是常駐權限。日常開發 / SRE 帳號只該有 read-only、修復需要時才透過 break-glass 流程申請臨時 write 權限。</p>
<p>常見實作：</p>
<ul>
<li><strong>角色分離</strong>：reviewer 跟 executor 是不同帳號、reviewer 不能執行、executor 不能 self-approve。系統強制檢查兩個帳號不同、避免一人偽造另一身分。</li>
<li><strong>時效性憑證</strong>：申請 write 權限時帶 expiry（30 分鐘 / 2 小時）、過期自動回收。不是「給了就一直有」、避免遺留高權限帳號變成攻擊面。</li>
<li><strong>範圍限定</strong>：申請時要指定哪張表、哪個 tenant / region。粒度不細的話、一次申請就拿到全 production write、超出實際需求。</li>
<li><strong>同步 alert</strong>：高權限被啟用要同步發 alert 到 security channel、給 security team reviewer 看見。事後若 audit log 跟 alert 對不上、表示權限被繞過。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Access Boundary</a> 跟 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets and Machine Credential Governance</a>。修復權限管理跟 incident-time 緊急存取是同一套機制、不該各做各的。</p>
<h2 id="跨服務--跨組織的對帳責任">跨服務 / 跨組織的對帳責任</h2>
<p>當對帳跨團隊、跨子系統、跨外部 provider 時、責任不清是首要失敗模式。對帳結果在組織邊界 <em>穿越</em> 時、要明確標記每段的 owner、否則 mismatch 出現後、所有相關方都會說「不是我們的問題」。</p>
<p><strong>跨服務對帳的責任切分</strong>：</p>
<ul>
<li><strong>資料 owner</strong>：誰擁有那張表 / 那組欄位、誰負責解釋為何資料長那樣。資料 owner 通常是寫入該表的服務團隊。</li>
<li><strong>對帳作業 owner</strong>：誰負責定義 reconciliation query、跑、看結果。可能跟資料 owner 是不同人（例如平台團隊跑對帳、業務團隊擁有資料）。</li>
<li><strong>差異處理 owner</strong>：mismatch 出現後、誰負責決定修復策略。通常跟資料 owner 一致、但跨團隊 mismatch 要先約定誰主導。</li>
<li><strong>修復執行 owner</strong>：實際下 SQL / call API 的人。可能跟差異處理 owner 不同（後者決策、前者執行）。</li>
</ul>
<p>四個 owner 在簡單場景可以是同一人、在複雜跨團隊場景必須清楚分派。AGENTS.md 規範優先序段的「明確 owner」原則在這裡指的是 <em>對每一段流程</em> 都有人能簽收、不是只指對帳這件事整體有 owner。</p>
<p><strong>跨組織對帳的特殊問題</strong>：跟外部 provider（金流、物流、SaaS supplier）對帳時、對方不見得會接受你的對帳結果、也不見得會給差異列表。常見處理：</p>
<ul>
<li>自己跑兩份對帳：A vs provider report（每天）、A vs provider API（即時抽樣）、兩份結果不同代表 provider report 本身有問題。</li>
<li>約定差異仲裁流程：簽 SLA 時就寫清楚、mismatch 出現後雙方各保留多久的資料、誰先給對方檢視。</li>
<li>不能依賴 provider 修：金流 provider 通常只負責對帳、不負責修你的 DB。修復永遠是你方責任。</li>
</ul>
<h2 id="跟-backup--pitr-整合">跟 Backup / PITR 整合</h2>
<p>備份的 <em>權限獨立性</em> 跟 <em>attack surface</em> 屬於 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team 備份段</a> — 本段聚焦 <em>recovery</em> 角度的資料修復責任。兩者互補：1.5 解決「備份本身怎麼防被攻擊」、本段解決「事故後怎麼用備份回復」。</p>
<p>當修復必須跨越「point in time」時、需要 backup 配合。</p>
<h3 id="snapshot-based-recovery">Snapshot-based recovery</h3>
<ul>
<li>整個 cluster 從 N 小時前的 snapshot 還原</li>
<li>影響：所有 <em>其他</em> 資料也回到那個時間點</li>
<li>適合：catastrophic data corruption</li>
</ul>
<h3 id="pitrpoint-in-time-recovery">PITR（Point-in-Time Recovery）</h3>
<ul>
<li>snapshot + WAL / binlog replay 到指定時間</li>
<li>影響：只在指定時間點 stop replay</li>
<li>適合：「3 小時前 admin 誤刪一張表」這類精準回放</li>
</ul>
<h3 id="logical-backupmysqldump--pg_dump">Logical backup（mysqldump / pg_dump）</h3>
<ul>
<li>整個 schema + data 的 SQL script</li>
<li>適合：跨環境遷移、特定表回復、小規模修復</li>
</ul>
<h3 id="continuous-archive">Continuous archive</h3>
<ul>
<li>WAL / binlog 持續備份到 S3 / GCS</li>
<li>一直可以回放到 <em>任何時間點</em></li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a> — 高可用需要快速 PITR</li>
</ul>
<h3 id="recovery-時的對抗壓力">Recovery 時的對抗壓力</h3>
<p>PITR / snapshot recovery 不是純技術問題、會在事故當下面對「為了快、要不要跳檢查」的取捨。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023 ransomware recovery pressure</a> — 虛擬化平台勒索後、團隊在 <em>營運壓力</em> 跟 <em>資料可信度</em> 之間擺盪：snapshot 是否乾淨、回復後資料是否被污染、跳過 integrity check 換 RTO 是否可接受。判讀重點：recovery 流程要事前 <em>演練</em> 過、否則事故當下不知道要 verify 什麼、容易在壓力下接受被污染的 backup。對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a>、事故當下的取捨要寫進 decision log。</p>
<h3 id="rtorpo-跟業務可接受中斷的對照表">RTO/RPO 跟業務可接受中斷的對照表</h3>
<p>業務可接受中斷時間是 RTO/RPO 的判讀對照基準。RTO（Recovery Time Objective、多久能恢復）跟 RPO（Recovery Point Objective、最多丟多少資料）是技術指標、要對照業務側的可接受上限才能判斷夠不夠。常見錯誤是把 RTO/RPO 訂在「技術上能做到的最佳值」、忽略業務實際的容忍範圍。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「定義核心流程的 RTO / RPO、讓資料修復時間跟業務可接受中斷時間明示對照、不藏在直覺」。事故當下發現「DB 能 2 小時恢復、但業務只能容忍 30 分鐘中斷」、來不及補救。</p>
<p><strong>對照表設計</strong>：</p>
<table>
  <thead>
      <tr>
          <th>業務流程</th>
          <th>RTO（技術）</th>
          <th>業務可接受中斷</th>
          <th>落差處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶登入</td>
          <td>30 分鐘</td>
          <td>5 分鐘</td>
          <td>加 standby region failover</td>
      </tr>
      <tr>
          <td>訂單寫入</td>
          <td>1 小時</td>
          <td>30 分鐘</td>
          <td>加 outbox + replay</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>4 小時</td>
          <td>1 天</td>
          <td>RTO 充裕、不需投資</td>
      </tr>
      <tr>
          <td>對帳 batch</td>
          <td>8 小時</td>
          <td>3 天</td>
          <td>RTO 充裕</td>
      </tr>
      <tr>
          <td>付款</td>
          <td>1 小時</td>
          <td>0（不能停）</td>
          <td>必須 active-active</td>
      </tr>
  </tbody>
</table>
<p><strong>關鍵情境延伸</strong>：</p>
<ul>
<li><strong>付款（必須 active-active）</strong>：業務可接受中斷為 0、單一 region failover 都不能用（failover 期間用戶看到失敗）、必須多 region 同時寫入、靠 Aurora DSQL / Spanner / Cosmos DB multi-region write 撐。設計權衡是 <em>跨 region 寫入延遲</em> 跟 <em>對帳一致性的特殊處理</em>（同一筆款項可能在兩個 region 各被處理一次、要靠 idempotency key 去重）。詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。</li>
<li><strong>訂單寫入（outbox + replay）</strong>：30 分鐘容忍區間夠用 outbox pattern — 訂單寫進 DB 同步寫進 outbox table、async worker 把 outbox event 推下游。即使下游中斷、訂單本身已落地、event 可在恢復後 replay。設計權衡是 outbox table 的儲存成本跟 replay 邏輯的冪等性、跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 outbox pattern 整合。</li>
<li><strong>用戶登入（standby region failover）</strong>：5 分鐘容忍意味 <em>自動 failover</em> 必須在這時間內完成、人類介入做不到、要靠 DNS health check + Route 53 / Cloudflare 自動切流。權衡是 standby region 平時付閒置成本、跟 active-active 比、便宜但 failover 時有 1-3 分鐘延遲跟 cache miss。</li>
</ul>
<p>落差是 <em>投資訊號</em>、不是「忽略它」。RTO &gt; 業務容忍時、要嘛降 RTO（加 HA / DR 投資）、要嘛跟業務協商提高容忍（通常不接受）。</p>
<p>判讀重點：對照表要每年 review。業務模式變了（例如從 B2C 變 B2B 客服 SaaS）、容忍時間會大幅縮短、RTO 必須跟著降。</p>
<h2 id="事故角色預定義">事故角色預定義</h2>
<p>DB 事故當下、<em>資安處置</em> 跟 <em>業務連續性處置</em> 要 <em>分軌並行</em>、不是線性執行。這要求事先有 dual-track IC（Incident Command）角色、不是事故當下臨時拉人。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「技術處置與業務處置分軌並行的前提是事先有 dual-track IC 角色」。沒事先定義、事故當下會出現「資安 team 在隔離系統、business team 在喊客戶等不及」、兩條軌道互相干擾。</p>
<p><strong>Dual-track IC 角色定義</strong>（以下為通用 IC 模型、非案例直接揭露；具體角色細分視組織規模調整）：</p>
<table>
  <thead>
      <tr>
          <th>軌道</th>
          <th>角色</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>技術軌道</td>
          <td>Tech IC</td>
          <td>漏洞修補、系統恢復、技術決策（rollback / restart 等）</td>
      </tr>
      <tr>
          <td>業務軌道</td>
          <td>Business IC</td>
          <td>客戶溝通、降級流程啟動、合規通報、業務 fallback</td>
      </tr>
      <tr>
          <td>協調軌道</td>
          <td>Overall IC</td>
          <td>兩條軌道協調、跨軌道決策、對外發言</td>
      </tr>
      <tr>
          <td>資料軌道</td>
          <td>Data IC</td>
          <td>資料完整性驗證、修復決策、audit chain</td>
      </tr>
      <tr>
          <td>Comms 軌道</td>
          <td>Communications Lead</td>
          <td>內部通報、外部公告、media 應對</td>
      </tr>
  </tbody>
</table>
<p><strong>Overall IC 跟一般技術 IC 的差異</strong>：一般 IC 主要在技術軌道內決策（要不要 rollback、要不要重啟）；Overall IC 額外承擔 <em>跨軌道仲裁</em> 責任 — 當 Tech IC 想停服務止血、Business IC 想保服務維持收入、兩者衝突時、由 Overall IC 拍板。這個角色需要對技術跟業務都有足夠理解、不能只懂一邊；通常由高階工程主管或 CTO/VP Eng 兼任、不是輪值的 oncall。</p>
<p><strong>Data IC 的特殊角色</strong>：跟其他軌道相比、Data IC 的決策時間軸最長 — 技術修復可能 1 小時完成、但 <em>資料是否被污染、要不要 PITR、PITR 到哪個時間點</em> 可能要 24-72 小時驗證。Data IC 不能被 Tech IC 跟 Business IC 的「快快上線」壓力推動、必須有獨立判斷權。實務上常見的失誤是讓 Tech IC 兼任 Data IC、結果為了 RTO 跳過 integrity check、事後發現資料污染擴大。</p>
<p><strong>事先準備</strong>：</p>
<ul>
<li><strong>Primary + backup 雙人配置</strong>：每個角色都要有 primary + backup、避免單人不可用（休假、生病、被另一事故占住）讓事故當下卡住。實務上要有 <em>指定流程</em> 而非「臨時找誰」、避免事故當下浪費 30 分鐘喬人。</li>
<li><strong>責任寫進 runbook</strong>：runbook 要列出每個角色該做什麼決策、不該做什麼決策（避免越權）。事故當下查職位、會在最壓力大的時候做組織決策、出錯機會高。</li>
<li><strong>定期 tabletop 演練</strong>：演練的重點不是「技術修復對不對」、是「角色交接是否流暢」。Overall IC 跟 Tech IC 之間的權限邊界、Data IC 何時介入、Comms Lead 何時對外發言、都要在演練中試出來。</li>
<li><strong>跨時區 follow-the-sun 輪值</strong>：B2B SaaS 跟全球業務、事故不分時區、要有 24/7 覆蓋。單一時區團隊在事故發生在凌晨時、人力不足或反應慢、會放大事故代價。</li>
</ul>
<p>判讀重點：DB 事故不只是技術事件、會成為 <em>跨多軌道</em> 的事件。角色預定義是組織能力、不是技術能力、但缺它會放大技術事故的代價。</p>
<p>對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a> 跟 <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 Security Routing</a> — 角色預定義是這些跨模組工作的前置。</p>
<h2 id="evidence-handoff">Evidence Handoff</h2>
<p>資料修復的 evidence handoff 要能支援 release gate 與 incident review。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>reconciliation query、provider report、audit log</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>差異發生窗口與修復窗口</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>mismatch sample、修復前後驗證</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>data owner、service owner、reviewer</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>抽樣覆蓋率、延遲、未覆蓋資料</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>尚未確認的 provider callback、低流量 tenant</td>
      </tr>
  </tbody>
</table>
<p>這份 handoff 要進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對帳差異率持續上升</td>
          <td>上游邏輯有 bug、或時間窗對齊問題</td>
          <td>修上游 + 確認對帳時間窗</td>
      </tr>
      <tr>
          <td>同筆資料對帳 run-to-run 結果不同</td>
          <td>對帳 query 沒處理 in-flight 資料邊界</td>
          <td>排除最近 N 分鐘、或允許收斂多跑幾次</td>
      </tr>
      <tr>
          <td>修復後不一致再次出現</td>
          <td>沒修根因、只修了 symptom</td>
          <td>找根因、增加 invariant check</td>
      </tr>
      <tr>
          <td>修復影響超出預期範圍</td>
          <td>mapping 規則錯誤、二次污染</td>
          <td>立即停止修復、回退</td>
      </tr>
      <tr>
          <td>修復沒 dry-run 直接執行</td>
          <td>流程違規、事後無法佐證影響範圍</td>
          <td>事後 audit、把 dry-run 列入 gate</td>
      </tr>
      <tr>
          <td>Recovery 後 derived state 仍錯</td>
          <td>重建 derived 時 source 還沒修</td>
          <td>先修 source、再重建 derived</td>
      </tr>
      <tr>
          <td>Audit log 缺欄位</td>
          <td>事故時無法追究、難 rollback</td>
          <td>補 audit schema、加 reviewer 欄位</td>
      </tr>
      <tr>
          <td>高權限帳號在非 incident 時段啟用</td>
          <td>可能誤用或攻擊面、break-glass 沒回收</td>
          <td>立刻檢查 audit log、回收憑證</td>
      </tr>
      <tr>
          <td>跨服務 mismatch、各方都推卸</td>
          <td>對帳 owner 沒分派、責任空白</td>
          <td>補資料 owner / 對帳 owner / 執行 owner</td>
      </tr>
      <tr>
          <td>anomaly alert 跟對帳 mismatch 混報</td>
          <td>兩種訊號性質不同、reviewer 無法判讀</td>
          <td>拆 dashboard、deterministic 跟 statistical 分開</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把對帳當成「定期 batch job」、不關心 <em>當下不一致</em>。實時對帳跟 batch 對帳是 <em>不同工具</em>、不能互相替代。</p>
<p>把資料修復當成「一個工程師動手改」、沒 audit、沒 review、沒 rollback。資料修復本質是 production 操作、跟 deploy 同等嚴格。</p>
<p>把 PITR 當成 <em>常規修復工具</em>。PITR 影響大、適合 catastrophic event、不適合單筆資料修復。</p>
<p>把 derived state 不一致跟 canonical state 不一致 <em>混在一起</em> 處理。derived 是 <em>再生</em> 的、canonical 是 <em>永久</em> 的、處理流程完全不同。</p>
<p>把對帳結果跟 anomaly detection 結果放同一份報告。前者是 deterministic、後者是 statistical、混報會讓 incident reviewer 無法判斷必修跟可疑。對帳 mismatch 要有獨立追蹤面板、anomaly 走另一條路徑。</p>
<p>跳過 dry-run、直接 update。即使單筆修復、也要先 select 看到當前 row、確認 WHERE 條件命中預期。incident 壓力下尤其容易跳、結果反而把單點問題擴成批次污染。</p>
<p>把修復權限當常駐權限發放。長期 write 權限放在工程師帳號上、會在事故無關時段被誤用、且事後無法區分「正常工作」跟「非法修復」。修復權限要時效化、申請即用即收。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>reconciliation 重點</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>migration 期間用 shadow read 持續對帳</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>體育博彩 ledger、結算後對帳</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場銀行、每市場獨立對帳</td>
      </tr>
  </tbody>
</table>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 reconciliation 與 data repair 責任。PostgreSQL、MySQL、MSSQL 或其他資料庫的差異、應放在它們如何產生 validation query、保留 audit trail、支援 point-in-time recovery、處理 replica lag 與控制修復權限。</p>
<p>若服務需要高頻對帳、後續文章要比較查詢成本、索引策略與 replica 讀取延遲。若服務需要高風險資料修復、後續文章要比較 transaction log、backup/restore、row-level audit 與權限分離。若服務需要跨系統補償、後續文章要把資料庫能力接到 queue replay 與 incident decision log。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：transaction boundary 決定哪些不一致可避免 — <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">Transaction Boundary</a></li>
<li>與 1.5 的交接：audit 跟 access control — <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">Red Team Data Layer</a></li>
<li>與 1.7 的交接：migration 後驗證 — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a></li>
<li>與 1.8 的交接：canonical vs derived 是修復的前置 — <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a></li>
<li>與 3.8 的交接：消息重放與補事件 — <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</a></li>
<li>與 4.20 的交接：evidence handoff — <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
<li>與 7.7 的交接：audit trail — <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a></li>
<li>與 8.22 的交接：incident evidence write-back — <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 migration 造成的資料差異、接著讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。要處理事件漏發造成的副作用修復、接著讀 <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。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。要設計跨服務 reconciliation 跟 saga compensation、接著讀 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的 Saga 段。</p>
]]></content:encoded></item><item><title>Azure Cosmos DB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/</guid><description>&lt;p>Azure Cosmos DB 是 Microsoft 全球分散式 multi-model database、提供 SQL / MongoDB / Cassandra / Gremlin / Table 五種 API、五個 consistency levels、自動 multi-region write。Microsoft 自家 Microsoft 365 用它做 analytics、ASOS 在 Black Friday 撐 1.67 億請求 24 小時、Minecraft Earth 測試 1M RU/s — 是 Azure 上 NoSQL / Document 工作負載的旗艦。&lt;/p>
&lt;h2 id="教學路線multi-model-api-與全球寫入">教學路線：Multi-model API 與全球寫入&lt;/h2>
&lt;p>Cosmos DB 服務頁的教學目標是把 API model、consistency level、RU/s、logical partition 與 multi-region write 放在同一個 Azure 服務決策中。讀者讀完後要能判斷 Cosmos DB 是遷移相容層、全球 NoSQL 平台，還是特定 Azure workload 的容量抽象。&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>API model&lt;/td>
 &lt;td>SQL API、MongoDB API、Cassandra API 各自服務哪種遷移或資料形狀&lt;/td>
 &lt;td>定位、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency level&lt;/td>
 &lt;td>session、bounded staleness、strong consistency 如何改變產品語意&lt;/td>
 &lt;td>容量規劃要點、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">Consistency Level&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RU/s capacity&lt;/td>
 &lt;td>request unit 如何把 query、index、payload 轉成成本與節流&lt;/td>
 &lt;td>容量特性、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Global write&lt;/td>
 &lt;td>multi-region write 何時值得承擔衝突與一致性成本&lt;/td>
 &lt;td>適用場景、案例對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時用 MongoDB、DynamoDB、Spanner、PostgreSQL 或 analytics&lt;/td>
 &lt;td>不適用場景、下一步路由&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位multi-model--multi-region-write">定位：multi-model + multi-region write&lt;/h2>
&lt;p>Cosmos DB 跟其他 DB 最大差異是 &lt;em>multi-model&lt;/em>。一個服務同時支援 5 種 API、每個 API 對應不同資料模型。應用層選擇用哪個 API、底層是同一個分散式 KV store。&lt;/p>
&lt;p>&lt;strong>5 個 API&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SQL API&lt;/strong>：document（JSON）+ SQL-like query、Cosmos DB native&lt;/li>
&lt;li>&lt;strong>MongoDB API&lt;/strong>：wire-protocol 相容 MongoDB&lt;/li>
&lt;li>&lt;strong>Cassandra API&lt;/strong>：wire-protocol 相容 Cassandra&lt;/li>
&lt;li>&lt;strong>Gremlin API&lt;/strong>：graph database&lt;/li>
&lt;li>&lt;strong>Table API&lt;/strong>：簡單 KV（Azure Table Storage 升級版）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>5 個 consistency levels&lt;/strong>（從強到弱）：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Strong&lt;/strong>：在支援的 account / region 配置內提供最強一致性，通常帶來最高 latency&lt;/li>
&lt;li>&lt;strong>Bounded staleness&lt;/strong>：訂版本 / 時間差異上限&lt;/li>
&lt;li>&lt;strong>Session&lt;/strong>：同 session 內強一致（最常用）&lt;/li>
&lt;li>&lt;strong>Consistent prefix&lt;/strong>：保證寫入順序&lt;/li>
&lt;li>&lt;strong>Eventual&lt;/strong>：最便宜、最終一致&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>容量特性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>容量單位：RU/s（Request Unit per second）— 把 read / write / query 統一抽象&lt;/li>
&lt;li>1 RU = strongly consistent read of 1KB document&lt;/li>
&lt;li>配置擴容延遲：99 百分位 5 秒內生效&lt;/li>
&lt;li>每個 logical partition 上限：10,000 RU/s&lt;/li>
&lt;li>測試最高：1,000,000 RU/s（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">Minecraft Earth 案例&lt;/a>）&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. Azure 生態的 multi-model 需求&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>Azure Cosmos DB 是 Microsoft 全球分散式 multi-model database、提供 SQL / MongoDB / Cassandra / Gremlin / Table 五種 API、五個 consistency levels、自動 multi-region write。Microsoft 自家 Microsoft 365 用它做 analytics、ASOS 在 Black Friday 撐 1.67 億請求 24 小時、Minecraft Earth 測試 1M RU/s — 是 Azure 上 NoSQL / Document 工作負載的旗艦。</p>
<h2 id="教學路線multi-model-api-與全球寫入">教學路線：Multi-model API 與全球寫入</h2>
<p>Cosmos DB 服務頁的教學目標是把 API model、consistency level、RU/s、logical partition 與 multi-region write 放在同一個 Azure 服務決策中。讀者讀完後要能判斷 Cosmos DB 是遷移相容層、全球 NoSQL 平台，還是特定 Azure workload 的容量抽象。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API model</td>
          <td>SQL API、MongoDB API、Cassandra API 各自服務哪種遷移或資料形狀</td>
          <td>定位、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>Consistency level</td>
          <td>session、bounded staleness、strong consistency 如何改變產品語意</td>
          <td>容量規劃要點、<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">Consistency Level</a></td>
      </tr>
      <tr>
          <td>RU/s capacity</td>
          <td>request unit 如何把 query、index、payload 轉成成本與節流</td>
          <td>容量特性、案例對照</td>
      </tr>
      <tr>
          <td>Global write</td>
          <td>multi-region write 何時值得承擔衝突與一致性成本</td>
          <td>適用場景、案例對照</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時用 MongoDB、DynamoDB、Spanner、PostgreSQL 或 analytics</td>
          <td>不適用場景、下一步路由</td>
      </tr>
  </tbody>
</table>
<h2 id="定位multi-model--multi-region-write">定位：multi-model + multi-region write</h2>
<p>Cosmos DB 跟其他 DB 最大差異是 <em>multi-model</em>。一個服務同時支援 5 種 API、每個 API 對應不同資料模型。應用層選擇用哪個 API、底層是同一個分散式 KV store。</p>
<p><strong>5 個 API</strong>：</p>
<ul>
<li><strong>SQL API</strong>：document（JSON）+ SQL-like query、Cosmos DB native</li>
<li><strong>MongoDB API</strong>：wire-protocol 相容 MongoDB</li>
<li><strong>Cassandra API</strong>：wire-protocol 相容 Cassandra</li>
<li><strong>Gremlin API</strong>：graph database</li>
<li><strong>Table API</strong>：簡單 KV（Azure Table Storage 升級版）</li>
</ul>
<p><strong>5 個 consistency levels</strong>（從強到弱）：</p>
<ol>
<li><strong>Strong</strong>：在支援的 account / region 配置內提供最強一致性，通常帶來最高 latency</li>
<li><strong>Bounded staleness</strong>：訂版本 / 時間差異上限</li>
<li><strong>Session</strong>：同 session 內強一致（最常用）</li>
<li><strong>Consistent prefix</strong>：保證寫入順序</li>
<li><strong>Eventual</strong>：最便宜、最終一致</li>
</ol>
<p><strong>容量特性</strong>：</p>
<ul>
<li>容量單位：RU/s（Request Unit per second）— 把 read / write / query 統一抽象</li>
<li>1 RU = strongly consistent read of 1KB document</li>
<li>配置擴容延遲：99 百分位 5 秒內生效</li>
<li>每個 logical partition 上限：10,000 RU/s</li>
<li>測試最高：1,000,000 RU/s（<a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">Minecraft Earth 案例</a>）</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. Azure 生態的 multi-model 需求</strong>：</p>
<ul>
<li>同一服務多種 use case（document、graph、KV 共存）</li>
<li>想把多個 NoSQL 資料模型集中在 Azure 服務邊界內治理</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — Microsoft 自家用 Cosmos DB 撐分析平台</li>
</ul>
<p><strong>2. 全球零售 + 季節性高峰</strong>：</p>
<ul>
<li>multi-region write 讓全球用戶寫入本地 region</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a> — Black Friday 24 小時 1.67 億請求、3500 RPS 峰值、48ms 平均延遲</li>
</ul>
<p><strong>3. 全球分散式遊戲後端</strong>：</p>
<ul>
<li>AR / 即時遊戲跨地區同步</li>
<li>session consistency 對遊戲足夠、不需 strong</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — AR 遊戲玩家位置、跨 region 寫入</li>
</ul>
<p><strong>4. MongoDB 應用想要 <em>managed + 全球分散</em></strong>：</p>
<ul>
<li>Cosmos DB MongoDB API wire protocol compatible</li>
<li>應用層主要驗證相容差異，底層改成分散式架構</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB → Cosmos DB MongoDB API、planet-scale 分析</li>
</ul>
<p><strong>5. 想用 multi-region active-active write</strong>：</p>
<ul>
<li>不像 Spanner / Aurora DSQL 是 PC 系統、Cosmos DB 是 AP 系統</li>
<li>用 LWW（Last-Writer-Wins）或 stored procedure 處理 conflict</li>
<li>適合可接受 eventual / session consistency 的 multi-region write workload；需要 global SQL linearizability 時轉 Spanner / Aurora DSQL</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 跨雲需求</strong>：</p>
<ul>
<li>Cosmos DB only on Azure</li>
<li>替代：MongoDB Atlas（cross-cloud）、CockroachDB（自管）</li>
</ul>
<p><strong>2. Linearizable 全球 OLTP</strong>：</p>
<ul>
<li>Cosmos DB Strong consistency 的適用範圍要按 account / region 配置判讀；全球 linearizable SQL 需求通常轉 Spanner / Aurora DSQL</li>
<li>替代：Spanner / Aurora DSQL（真正全球 linearizable）</li>
</ul>
<p><strong>3. 預算極敏感的小 workload</strong>：</p>
<ul>
<li>最低 400 RU/s（約 $25/month）</li>
<li>小流量場景、Azure SQL Database 更便宜</li>
</ul>
<p><strong>4. 純 OLAP 分析</strong>：</p>
<ul>
<li>Cosmos DB 定位在 OLTP / document，analytics workload 交給 Synapse、BigQuery 或 Snowflake</li>
<li>替代：Azure Synapse、BigQuery、Snowflake</li>
</ul>
<p><strong>5. 嚴格 ACID 跨 partition transaction</strong>：</p>
<ul>
<li>Cosmos DB Transaction 限 same logical partition</li>
<li>跨 partition 的 multi-row transaction 要改用 workflow、stored procedure 邊界或 distributed SQL</li>
<li>替代：Spanner / Aurora DSQL</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs DynamoDB（AWS）</strong>：</p>
<ul>
<li>Cosmos DB：multi-model（5 API）、5 consistency levels、multi-region write</li>
<li>DynamoDB：KV 為主、strong / eventual consistency、Global Tables 以 LWW 處理 multi-region conflict</li>
<li>選 Cosmos DB：Azure 生態、需要 multi-model、需要 consistency 細粒度控制</li>
<li>選 DynamoDB：AWS 生態、純 KV、AWS-native 整合（Lambda、Streams）</li>
</ul>
<p><strong>vs Spanner（GCP）</strong>：</p>
<ul>
<li>Cosmos DB：AP 系統、5 consistency levels、multi-model</li>
<li>Spanner：CP 系統、external consistency、SQL only</li>
<li>選 Cosmos DB：可接受 eventual / session、需要 multi-model</li>
<li>選 Spanner：需要 <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> 與 SQL workload</li>
</ul>
<p><strong>vs MongoDB Atlas</strong>：</p>
<ul>
<li>Cosmos DB MongoDB API：Azure-only、managed、global 強</li>
<li>MongoDB Atlas：跨雲（AWS / GCP / Azure）、原生 MongoDB 行為</li>
<li>選 Cosmos DB：已在 Azure、想要更好 global distribution</li>
<li>選 MongoDB Atlas：跨雲、需要 MongoDB 完整功能（aggregation pipeline 等 native 行為）</li>
</ul>
<p><strong>vs Cassandra / ScyllaDB</strong>：</p>
<ul>
<li>Cosmos DB Cassandra API：managed Azure</li>
<li>Cassandra / ScyllaDB：自管、跨雲</li>
<li>選 Cosmos DB：Azure 生態、想把 operation 交給 managed service</li>
<li>選 Cassandra：跨雲、自管、極限 throughput tuning</li>
</ul>
<p><strong>vs Azure SQL Hyperscale</strong>：</p>
<ul>
<li>Cosmos DB：NoSQL / document、global 分散</li>
<li>Azure SQL Hyperscale：傳統 SQL OLTP、storage / compute 分離、AWS Aurora 對應</li>
<li>選 Cosmos DB：document model、global 分散</li>
<li>選 Azure SQL：SQL workload、應用已用 SQL Server</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a> — SQL 工作負載選 Hyperscale，document / NoSQL workload 才進 Cosmos DB</li>
</ul>
<p><strong>vs PostgreSQL（SQL baseline）</strong>：</p>
<ul>
<li>PostgreSQL：SQL、強一致、single-primary、跨雲可用</li>
<li>Cosmos DB：NoSQL / multi-model、AP 系統、Azure-only、global 分散</li>
<li>選 PostgreSQL：SQL workload、跨雲、需要進階 SQL 特性</li>
<li>選 Cosmos DB：Azure 生態、document / KV / multi-model、需要 global distribution</li>
</ul>
<p><strong>vs Aurora（AWS managed SQL）</strong>：</p>
<ul>
<li>Aurora：AWS、SQL（PostgreSQL / MySQL）、single-region scaling</li>
<li>Cosmos DB：Azure、NoSQL / multi-model、global write</li>
<li>兩者分別站在 cloud provider 與 data model 兩個維度；同需求下通常先看既有雲平台（AWS → Aurora、Azure → Cosmos / Azure SQL）</li>
</ul>
<p><strong>vs CockroachDB（cross-cloud distributed SQL）</strong>：</p>
<ul>
<li>CockroachDB：跨雲、PostgreSQL wire、distributed SQL、強一致</li>
<li>Cosmos DB：Azure-only、multi-model、5 consistency levels、AP 系統</li>
<li>選 CockroachDB：要 SQL + 跨雲 + 強一致</li>
<li>選 Cosmos DB：要 NoSQL + Azure 生態 + 細粒度 consistency 選擇</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. RU/s 抽象化把 read / write / query 統一</strong>：</p>
<ul>
<li>不像 DynamoDB 拆 RCU / WCU、Cosmos DB 用單一 RU</li>
<li>簡化容量規劃、但要算「不同操作各吃多少 RU」</li>
<li>1 RU = 1 KB strong read、寫 ~5 RU、複雜 query 數百 RU</li>
</ul>
<p><strong>2. partition key 設計跟 DynamoDB 一樣關鍵</strong>：</p>
<ul>
<li>每個 logical partition 上限 10,000 RU/s</li>
<li>partition key 不均 → hot partition</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — synthetic partition key 強制分散</li>
<li>詳見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a></li>
</ul>
<p><strong>3. multi-region 配置</strong>：</p>
<ul>
<li>開啟跨 region 後、容量在每個 region 都 mirror、成本乘以 region 數</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 跟 DynamoDB Global Tables 同類思維、各 region 獨立容量</li>
</ul>
<p><strong>4. Consistency level 影響成本</strong>：</p>
<ul>
<li>Strong consistency：跨 region quorum、單個 read 約 2x RU</li>
<li>Session：cost 跟 eventual 接近、但提供同 session 一致</li>
<li>Eventual：最便宜</li>
</ul>
<p><strong>5. Autoscale provisioned throughput</strong>：</p>
<ul>
<li>訂 max RU/s、實際用多少算多少（10% min）</li>
<li>適合：流量 unpredictable、想降低 on-demand 成本治理負擔</li>
</ul>
<p><strong>6. Serverless mode</strong>：</p>
<ul>
<li>按 request 計費，適合稀疏與小流量 workload</li>
<li>適合：dev / test、小流量、稀疏 workload</li>
</ul>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本批 5 篇 deep article 已完成、覆蓋 Cosmos DB 從 consistency level 選擇到 multi-region write conflict 的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略</td>
          <td><a href="consistency-levels-engineering/">consistency-levels-engineering</a></td>
          <td>Session 為何是 production 預設、per-request override、Strong + multi-region 互斥 cross-link</td>
      </tr>
      <tr>
          <td>Synthetic / composite / hierarchical partition key + 不可逆性硬約束</td>
          <td><a href="partition-key-design/">partition-key-design</a></td>
          <td>10000 RU/s 上限、不可改、跟 DynamoDB / MongoDB 可逆性對比</td>
      </tr>
      <tr>
          <td>RU/s 思維、payload、index、provisioned vs autoscale vs serverless</td>
          <td><a href="ru-cost-model-sizing/">ru-cost-model-sizing</a></td>
          <td>ASOS Black Friday + Minecraft Earth 1M RU/s 壓測、autoscale reactive 限制</td>
      </tr>
      <tr>
          <td>MongoDB API vs SQL API：三型遷移、dogfood、multi-model、跨雲 hedging</td>
          <td><a href="mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a></td>
          <td>Microsoft 365 dogfood 邊界、document model 遷移三型 SSoT</td>
      </tr>
      <tr>
          <td>Multi-region active-active + LWW / custom merge / Strong 互斥</td>
          <td><a href="multi-region-write-conflict/">multi-region-write-conflict</a></td>
          <td>Strong + multi-region 互斥的 AP 取捨 SSoT、廣告 SLA vs 實測可用性鏈路</td>
      </tr>
  </tbody>
</table>
<p>第二批 deep article 把 Cosmos DB 從核心容量 / 一致性議題推進到 server-side 邏輯、CDC、不同產品釐清與 OLTP / OLAP federation：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Change Feed (CDC)：persistent change log、Azure Functions trigger</td>
          <td><a href="change-feed-cdc/">change-feed-cdc</a></td>
          <td>latest-version vs all-versions-and-deletes、lease container、DynamoDB Streams 對照</td>
      </tr>
      <tr>
          <td>Stored procedure / trigger（JavaScript）：partition-scoped 交易</td>
          <td><a href="stored-procedure-trigger/">stored-procedure-trigger</a></td>
          <td>single-partition atomicity、bounded execution、多數邏輯應在 application 層</td>
      </tr>
      <tr>
          <td>Cosmos DB for PostgreSQL（Citus-based 分散式 PG、不同產品）</td>
          <td><a href="cosmos-for-postgresql/">cosmos-for-postgresql</a></td>
          <td>定位釐清、distribution column、何時選它而非核心 Cosmos / single-node PG</td>
      </tr>
      <tr>
          <td>Cosmos DB ↔ Azure Synapse Link：OLTP / OLAP <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a></td>
          <td><a href="synapse-link-federation/">synapse-link-federation</a></td>
          <td>analytical store、HTAP、RU 隔離、何時 federate 到專用 OLAP</td>
      </tr>
  </tbody>
</table>
<p>Migration playbook：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應遷移議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>從 MongoDB / Cassandra 遷入 Cosmos DB</td>
          <td><a href="migrate-from-mongodb-cassandra/">migrate-from-mongodb-cassandra</a></td>
          <td>protocol-compat API drop-in（Type B）vs native API paradigm shift（Type E）、相容性邊界、dual-write cutover</td>
      </tr>
  </tbody>
</table>
<p>跨 vendor entry：先看 <a href="../db3-vendor-selection/">DB3 vendor selection</a>（MongoDB / DynamoDB / Cosmos DB 三方選型 + workload shape 前置判讀），再進本 vendor 的 deep article。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>Hierarchical partition key 與 partition split / merge 運維</li>
<li>Autoscale vs serverless 的成本切換決策樹</li>
<li>Hands-on lab 入口（對齊 PostgreSQL / MySQL / SQLite hands-on 形態）</li>
<li>Backup / PITR 與 continuous backup tier 選擇</li>
<li>Gremlin / Table API 的適用邊界與遷入</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>Cosmos DB 的 multi-model 能把遷移阻力降到很低，也會讓 API compatibility、RU/s、partition key 與 consistency level 同時變成設計責任。這一段先說何時維持單一 API model，再說何時升級 multi-region write、Synapse Link、MongoDB Atlas、Spanner 或 Azure SQL。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 API model</td>
          <td>document / MongoDB / Cassandra / Table 語意清楚分工</td>
          <td>多 API 共用同一資料語意、相容層行為差異開始影響 production</td>
          <td><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a>、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">Database</a></td>
      </tr>
      <tr>
          <td>Session consistency</td>
          <td>user session 內讀寫一致已滿足產品需求</td>
          <td>金融 / 庫存 / 票務需要更強順序承諾</td>
          <td><a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">Consistency Level</a>、<a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">Linearizability</a></td>
      </tr>
      <tr>
          <td>Provisioned RU/s</td>
          <td>流量可預測、partition key 均勻</td>
          <td>Black Friday、遊戲上線、全球事件帶來突發尖峰</td>
          <td><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>、<a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></td>
      </tr>
      <tr>
          <td>Multi-region write</td>
          <td>single-region write + global read 已足夠</td>
          <td>regional write latency、region residency、active-active 是產品需求</td>
          <td><a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a></td>
      </tr>
      <tr>
          <td>MongoDB Atlas</td>
          <td>Azure global distribution 是主訴求</td>
          <td>跨雲、原生 MongoDB 行為、Atlas ecosystem 是主訴求</td>
          <td><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a></td>
      </tr>
      <tr>
          <td>Spanner / CockroachDB</td>
          <td>session / eventual consistency 可接受</td>
          <td>global SQL、strong transaction、cross-partition ACID 是核心需求</td>
          <td><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></td>
      </tr>
      <tr>
          <td>Azure SQL Hyperscale</td>
          <td>document / NoSQL 是主要資料形狀</td>
          <td>JOIN-heavy、transaction-heavy、SQL Server 生態是主需求</td>
          <td><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></td>
      </tr>
  </tbody>
</table>
<p>Cosmos DB 的簡單路徑是先固定 API model 與 consistency level。每個 API 的相容範圍、index 行為與 query cost 都不同；單純因為「同一服務支援多模型」而混用 API，後續 migration、debug 與容量估算會變複雜。</p>
<p>RU/s 的升級路徑要把 partition key 與 query shape 放在同一張圖。單純提高 RU/s 只能提高名義容量；logical partition 熱點、跨 partition query、index policy 與 payload size 仍會決定真實成本。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>Cosmos DB overview 目前完成 Azure global NoSQL 判斷。下一輪 deep article / playbook 應補 consistency level 選擇、RU/s cost model、partition key design、multi-region conflict、Change Feed、MongoDB API migration、Cassandra API migration 與 Synapse Link。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>規模</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></td>
          <td>1M RU/s 測試、turnkey global distribution</td>
          <td>AR 遊戲全球分散</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a></td>
          <td>1.67 億 req / 24h、48ms p99</td>
          <td>全球零售 Black Friday</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a></td>
          <td>planet-scale analytics</td>
          <td>MongoDB → Cosmos DB API-compatible 遷移、Microsoft 自家 dogfood</td>
      </tr>
  </tbody>
</table>
<p>Cosmos DB case 的讀法是分開看三種壓力：Minecraft Earth 提供 global partition 與 RU/s 訊號，ASOS 提供季節性零售尖峰訊號，Microsoft 365 提供 MongoDB API 相容遷移與 Azure dogfood 訊號。</p>
<h2 id="反向-sibling-路由">反向 sibling 路由</h2>
<p>Cosmos DB 的反向 sibling 路由用來把 Azure global NoSQL、DynamoDB 與 document migration 分開。若讀者從 DynamoDB 過來，先比較 RU/s、partition key、multi-region conflict 與 API model；若讀者從 MongoDB 過來，先把 API compatibility 當 migration hypothesis，再用 aggregation、index、change stream / Change Feed 行為驗證；若需求其實是 SQL strong consistency，轉到 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 或 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a>。</p>
<p>這條路由的判準是 API model 是否已固定。Cosmos DB 的 multi-model 是產品入口，不代表同一套資料可以在多個 API 之間自由切換；partition key、index policy、RU/s 與 consistency level 一旦進 production，就會成為 migration 與成本邊界。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>Strong consistency 用太多</strong>：多數互動式業務用 session consistency 就能滿足讀寫體驗</li>
<li><strong>partition key 只用 user_id</strong>：某些業務 user 集中（VIP、bot）會 hot</li>
<li><strong>忽略 Change Feed</strong>：寫入後通知、投影與同步流程適合先評估 Change Feed</li>
<li><strong>MongoDB API behavior 假設</strong>：API compat 仍要驗證 aggregation pipeline / index 行為</li>
<li><strong>忽略 multi-region 成本乘數</strong>：開 3 region active-active = 3 倍 RU 成本</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<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 vendor</a>、<a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a>、<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a></li>
<li>上游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（MongoDB → Cosmos 範例）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>Last reviewed：2026-05-22（API compatibility / consistency / RU model 屬時間敏感 claim）</li>
<li>官方：<a href="https://azure.microsoft.com/products/cosmos-db/">Azure Cosmos DB</a>、<a href="https://learn.microsoft.com/azure/cosmos-db/consistency-levels">Cosmos DB consistency levels</a></li>
</ul>
]]></content:encoded></item><item><title>9.9 Performance Improvement Loop</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Improvement loop 的責任是把效能優化從「事件型 hotfix」變成「持續改進的工程流程」。沒有 loop 時、效能問題靠 oncall 觸發、改了又改、改完又退化；有 loop 之後、每次 release 都通過 perf gate、退化在發布前就攔住。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate&lt;/a> 的關係：06.13 是 release gate 的一個環節、9.9 是這個 gate 背後的完整工程閉環。06.13 處理「進 gate 後怎麼判斷」、9.9 處理「進 gate 前怎麼產生比較資料」。&lt;/p>
&lt;p>本章聚焦在 &lt;em>閉環設計&lt;/em> — 怎麼建 baseline、怎麼跑 re-test、怎麼用 profile diff、怎麼整合 CI。讀完後讀者能設計一個 perf improvement workflow、不是只有 ad-hoc 壓測。&lt;/p>
&lt;h2 id="loop-五個階段">Loop 五個階段&lt;/h2>
&lt;p>完整的 improvement loop 包含五個階段、缺一不可：&lt;/p>
&lt;p>&lt;strong>1. Baseline 建立&lt;/strong>：壓測 + profile 取得「當前正常」snapshot。
&lt;strong>2. 變更 + re-test&lt;/strong>：每次 release candidate 跑壓測、跟 baseline diff。
&lt;strong>3. Profile diff&lt;/strong>：用 flame graph diff 定位退化原因。
&lt;strong>4. Fix&lt;/strong>：rollback 或修正 code path。
&lt;strong>5. Update baseline&lt;/strong>：通過後更新 baseline、進下個 cycle。&lt;/p>
&lt;p>少了 baseline → re-test 沒有比較對象、看絕對數字會錯判。
少了 profile diff → 退化定位靠猜、修錯方向。
少了 update baseline → 永遠跟 old baseline 比、退化累積看不出來。
少了 fix → 退化通過 gate、production 出事。&lt;/p>
&lt;h2 id="baseline-設計">Baseline 設計&lt;/h2>
&lt;p>Baseline 不是「歷史最佳」、是「最低可接受效能」。&lt;/p>
&lt;p>&lt;strong>設計原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不只一個 baseline、按 workload model 訂多個（不同 endpoint、不同 user tier 各自 baseline）&lt;/li>
&lt;li>baseline 必須可重複：固定 seed、固定資料集、固定環境、固定壓測參數&lt;/li>
&lt;li>定期 review：硬體 / 軟體升級會讓 baseline 該往好的方向走、不更新就是裝盲&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>儲存策略&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>baseline as artifact：存進 release artifact、隨 release 帶走&lt;/li>
&lt;li>baseline as code：用 Pulumi / Terraform / dedicated config 管理、可 version control&lt;/li>
&lt;li>baseline as service：dedicated service 管 baseline、提供 query API&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Drift 監控&lt;/strong>：baseline 每月對比上月、看趨勢是否往好方向。drift 超門檻 → re-baseline 並 review 原因。&lt;/p>
&lt;h2 id="profile-diff">Profile diff&lt;/h2>
&lt;p>退化定位的關鍵工具是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">profile diff&lt;/a> — 對比兩次 profile 找 hottest 變化。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Improvement loop 的責任是把效能優化從「事件型 hotfix」變成「持續改進的工程流程」。沒有 loop 時、效能問題靠 oncall 觸發、改了又改、改完又退化；有 loop 之後、每次 release 都通過 perf gate、退化在發布前就攔住。</p>
<p>跟 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> 的關係：06.13 是 release gate 的一個環節、9.9 是這個 gate 背後的完整工程閉環。06.13 處理「進 gate 後怎麼判斷」、9.9 處理「進 gate 前怎麼產生比較資料」。</p>
<p>本章聚焦在 <em>閉環設計</em> — 怎麼建 baseline、怎麼跑 re-test、怎麼用 profile diff、怎麼整合 CI。讀完後讀者能設計一個 perf improvement workflow、不是只有 ad-hoc 壓測。</p>
<h2 id="loop-五個階段">Loop 五個階段</h2>
<p>完整的 improvement loop 包含五個階段、缺一不可：</p>
<p><strong>1. Baseline 建立</strong>：壓測 + profile 取得「當前正常」snapshot。
<strong>2. 變更 + re-test</strong>：每次 release candidate 跑壓測、跟 baseline diff。
<strong>3. Profile diff</strong>：用 flame graph diff 定位退化原因。
<strong>4. Fix</strong>：rollback 或修正 code path。
<strong>5. Update baseline</strong>：通過後更新 baseline、進下個 cycle。</p>
<p>少了 baseline → re-test 沒有比較對象、看絕對數字會錯判。
少了 profile diff → 退化定位靠猜、修錯方向。
少了 update baseline → 永遠跟 old baseline 比、退化累積看不出來。
少了 fix → 退化通過 gate、production 出事。</p>
<h2 id="baseline-設計">Baseline 設計</h2>
<p>Baseline 不是「歷史最佳」、是「最低可接受效能」。</p>
<p><strong>設計原則</strong>：</p>
<ul>
<li>不只一個 baseline、按 workload model 訂多個（不同 endpoint、不同 user tier 各自 baseline）</li>
<li>baseline 必須可重複：固定 seed、固定資料集、固定環境、固定壓測參數</li>
<li>定期 review：硬體 / 軟體升級會讓 baseline 該往好的方向走、不更新就是裝盲</li>
</ul>
<p><strong>儲存策略</strong>：</p>
<ul>
<li>baseline as artifact：存進 release artifact、隨 release 帶走</li>
<li>baseline as code：用 Pulumi / Terraform / dedicated config 管理、可 version control</li>
<li>baseline as service：dedicated service 管 baseline、提供 query API</li>
</ul>
<p><strong>Drift 監控</strong>：baseline 每月對比上月、看趨勢是否往好方向。drift 超門檻 → re-baseline 並 review 原因。</p>
<h2 id="profile-diff">Profile diff</h2>
<p>退化定位的關鍵工具是 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">profile diff</a> — 對比兩次 profile 找 hottest 變化。</p>
<p><strong>工具實作</strong>：</p>
<ul>
<li>Brendan Gregg 的 differential flame graph：開源、需要手動 generate</li>
<li>Pyroscope diff：UI 直接對比兩個時間段</li>
<li>Datadog Continuous Profiler diff：跟 deployment marker 整合</li>
<li>Parca compare：CNCF 標準</li>
<li>AWS CodeGuru Profiler：自動偵測 CPU / memory anti-pattern</li>
</ul>
<p><strong>正確使用方法</strong>：</p>
<ul>
<li>在 <em>相同負載 + 相同硬體 + 相同 sampling rate</em> 下取兩次 profile</li>
<li>比較 <em>相對變化</em>、不是絕對 CPU%</li>
<li>看 wider stack（不只看 leaf function）找 systemic regression</li>
</ul>
<p><strong>Profile diff 結果通常需要工程師判讀</strong>：「多花 20% CPU 但 throughput 多 50%」可能是好變化、不能純自動化判斷退化是否可接受。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 統一</a> — DB 層統一後 profile diff 噪音降低、退化來源更容易識別。</p>
<h2 id="regression-gate-整合-ci">Regression gate 整合 CI</h2>
<p>效能改進閉環必須整合到 CI、不能只在 release 前一次性跑。</p>
<p><strong>Multi-tier 壓測策略</strong>：</p>
<ul>
<li>每個 PR：跑 lightweight perf test（單 endpoint、5 分鐘）、合併前比 baseline</li>
<li>主分支 nightly：跑 medium perf test（多 endpoint、30 分鐘）</li>
<li>Release candidate：跑 complete perf test（完整 workload model、數小時）</li>
</ul>
<p><strong>Gate 觸發條件</strong>：</p>
<ul>
<li>p99 退化 &gt; X%（例如 10%）</li>
<li>吞吐降 &gt; Y%（例如 5%）</li>
<li>error rate 升 &gt; Z%</li>
<li>cost per request 升 &gt; W%</li>
</ul>
<p><strong>Gate 通過 / 不通過的後果</strong>：</p>
<ul>
<li>通過：自動 promote 到下個 stage（staging / canary / production）</li>
<li>不通過：block release、自動 notify owner、附 profile diff link</li>
</ul>
<p><strong>Gate 太敏感的反模式</strong>：</p>
<ul>
<li>每天 false positive、最後沒人看（alert fatigue）</li>
<li>false positive 來源：壓測環境噪音、baseline drift 未更新、業務變化</li>
<li>對策：multi-window detection（變化必須持續 N 個 sample）、配合 manual override（資深工程師判斷異常正常）</li>
</ul>
<p>對應案例：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> 的實作建議。</p>
<h2 id="canary-perf-check">Canary perf check</h2>
<p><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary perf check</a> 是 release 階段的另一道 perf gate。跟 regression gate（pre-release）對應、是 <em>production</em> 階段的監控。</p>
<p><strong>Canary 階段除了看 error rate、也看</strong>：</p>
<ul>
<li>latency p99 / p999（最先看到的 regression 訊號）</li>
<li>throughput（是否處理變慢）</li>
<li>resource utilization（CPU / RAM / connection 變化）</li>
<li>cost per request（是否更貴）</li>
</ul>
<p><strong>Canary 流量 vs control 流量比較</strong>：</p>
<ul>
<li>同樣流量同樣時段、不同版本的差才有意義</li>
<li>不能拿 canary 跟 historical baseline 比（外部變數太多）</li>
<li>abort condition：canary p99 比 control 退化 &gt; X%</li>
</ul>
<p><strong>漸進放大策略</strong>：1% → 5% → 25% → 50% → 100%、每階段觀察足夠時間（至少 15 分鐘看 long-tail）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day FIS 8x chaos</a> — canary 模式跟 chaos test 並行、確保新版本在故障場景也撐得住。</p>
<h2 id="pre-release-改進迴圈頻率">Pre-release 改進迴圈頻率</h2>
<p>不同層級的 review 在不同節奏：</p>
<ul>
<li><strong>每日 PR 級 perf check</strong>：lightweight、單 endpoint、5 分鐘</li>
<li><strong>每週 release candidate 完整壓測</strong>：完整 workload model、數小時</li>
<li><strong>每月 baseline review + drift 評估</strong>：對比歷史趨勢、決定是否 re-baseline</li>
<li><strong>每季容量地圖 review</strong>：跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 連動</li>
</ul>
<p>頻率不夠 → 退化累積看不到；頻率太高 → 工程資源吃緊。按團隊規模跟 release 節奏調整。</p>
<h2 id="退化的常見來源">退化的常見來源</h2>
<p>知道退化怎麼來、才能設計對應的 detection：</p>
<ul>
<li><strong>新功能引入 N+1 query</strong>：ORM lazy loading、loop 內 query。看 DB call count 變化</li>
<li><strong>ORM 沒下 index、cache miss 飆升</strong>：看 slow query 跟 cache hit rate</li>
<li><strong>第三方 library upgrade 帶來 overhead</strong>：新版本可能多了 telemetry / validation。看 profile diff</li>
<li><strong>GC tuning 變動</strong>：JVM / Go GC config 調整造成 pause time 變化。看 p999</li>
<li><strong>container resource limit 變動</strong>：Kubernetes limit 改、限制更嚴造成 throttling。看 CPU throttling event</li>
</ul>
<h2 id="反模式">反模式</h2>
<ul>
<li><strong>只在 release 前一次性壓測</strong>：退化已累積數月、找不出原因</li>
<li><strong>baseline 不更新</strong>：永遠跟舊版本比、低估目前狀態</li>
<li><strong>改了又改、改完忘記更新 baseline</strong>：下次 release 又跟過時 baseline 比、迴圈失效</li>
<li><strong>缺 profile diff、退化原因靠猜</strong>：修錯方向、退化還在</li>
<li><strong>gate 訊號跟業務無關</strong>：技術指標退化但業務 metric 沒事、被當 false positive</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>統一 DB 後 profile 變單純</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>遷移後重新做 baseline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day FIS 8x</a></td>
          <td>持續改進的混沌 + 壓測迴圈</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a></li>
</ul>
]]></content:encoded></item><item><title>9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/</guid><description>&lt;p>這個案例的核心責任是說明「事件交付系統的容量規劃，靠 managed service 卸載 vs 自管 broker」的長期成本對照。Spotify 從 Kafka 遷到 Pub/Sub 的驅動力是 &lt;em>容量規劃的工程成本&lt;/em> 在 sustained growth 下變得不划算、Kafka 能力本身不是瓶頸。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Spotify 在 Google Cloud 的遷移敘述（引自 &lt;a href="https://cloud.google.com/blog/products/gcp/spotifys-journey-to-cloud-why-spotify-migrated-its-event-delivery-system-from-kafka-to-google-cloud-pubsub">Spotify&amp;rsquo;s journey to cloud&lt;/a>）：&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>用戶規模&lt;/td>
 &lt;td>7500 萬 + 用戶（遷移時期）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移系統&lt;/td>
 &lt;td>Event Delivery System（事件交付）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷出技術&lt;/td>
 &lt;td>自管 Apache Kafka&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷入技術&lt;/td>
 &lt;td>Google Cloud Pub/Sub&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大數據生態&lt;/td>
 &lt;td>BigQuery / Dataflow / Dataproc / Pub/Sub&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵動機：「moving event delivery to a managed service」— 卸下 Kafka broker 的容量規劃與運維負擔。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Spotify 遷移揭露三個 broker 容量規劃的長期工程問題。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>自管 broker 的容量規劃是長期 tax&lt;/strong>：Kafka cluster 需要 partition planning、broker 數量、副本因子、disk capacity、network bandwidth、ZooKeeper / KRaft 治理 — 每個維度都要持續規劃、每次擴容都是工程專案。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> 的 broker basics 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的人力成本評估。&lt;/li>
&lt;li>&lt;strong>managed service 的容量是 trade-off、不是免費午餐&lt;/strong>：Pub/Sub 自動 scaling、但 vendor lock-in、cost-per-message 累積、message ordering / latency 特性跟 Kafka 不同。遷移本身要驗證 &lt;em>業務語意&lt;/em> 跟 Pub/Sub 兼容。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">03.4 broker basics&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遷移本身是容量規劃題目&lt;/strong>：把 7500 萬用戶的事件交付從 A 平台搬到 B 平台、不能停機、不能丟 message。這個遷移過程本身就是高併發容量工程。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence&lt;/a> 的同類流程。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：Spotify 這個決定不是「Kafka 不好」、是「Spotify 規模下、自管 Kafka 的工程投入不划算」。對中小團隊、自管 Kafka 可能是更便宜的選項。讀案例時要看 &lt;em>規模門檻&lt;/em> 跟 &lt;em>團隊能力&lt;/em>。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>broker 自管 vs managed 是長期 TCO 評估&lt;/strong>：算「平日運維 + 容量擴容 + 故障處理 + 升級遷移」的人力成本、不只算「broker 雲端費用」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遷移分階段：dual write → shadow → cutover&lt;/strong>：先寫兩邊、驗證一致性、再切流量。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence&lt;/a> 的同類流程。&lt;/li>
&lt;li>&lt;strong>業務語意對映是遷移關鍵&lt;/strong>：Kafka 的 partition / offset / consumer group 在 Pub/Sub 對映成不同概念（subscription / ordering key / message attribute）、不是 1:1。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS SNS / SQS / Kinesis、Amazon MSK（managed Kafka）、Azure Service Bus / Event Hubs / Event Grid 都是對等候選。差異是 message ordering 保證、delivery guarantee、cost model。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「事件交付系統的容量規劃，靠 managed service 卸載 vs 自管 broker」的長期成本對照。Spotify 從 Kafka 遷到 Pub/Sub 的驅動力是 <em>容量規劃的工程成本</em> 在 sustained growth 下變得不划算、Kafka 能力本身不是瓶頸。</p>
<h2 id="觀察">觀察</h2>
<p>Spotify 在 Google Cloud 的遷移敘述（引自 <a href="https://cloud.google.com/blog/products/gcp/spotifys-journey-to-cloud-why-spotify-migrated-its-event-delivery-system-from-kafka-to-google-cloud-pubsub">Spotify&rsquo;s journey to cloud</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶規模</td>
          <td>7500 萬 + 用戶（遷移時期）</td>
      </tr>
      <tr>
          <td>遷移系統</td>
          <td>Event Delivery System（事件交付）</td>
      </tr>
      <tr>
          <td>遷出技術</td>
          <td>自管 Apache Kafka</td>
      </tr>
      <tr>
          <td>遷入技術</td>
          <td>Google Cloud Pub/Sub</td>
      </tr>
      <tr>
          <td>大數據生態</td>
          <td>BigQuery / Dataflow / Dataproc / Pub/Sub</td>
      </tr>
  </tbody>
</table>
<p>關鍵動機：「moving event delivery to a managed service」— 卸下 Kafka broker 的容量規劃與運維負擔。</p>
<h2 id="判讀">判讀</h2>
<p>Spotify 遷移揭露三個 broker 容量規劃的長期工程問題。</p>
<ol>
<li><strong>自管 broker 的容量規劃是長期 tax</strong>：Kafka cluster 需要 partition planning、broker 數量、副本因子、disk capacity、network bandwidth、ZooKeeper / KRaft 治理 — 每個維度都要持續規劃、每次擴容都是工程專案。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 broker basics 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的人力成本評估。</li>
<li><strong>managed service 的容量是 trade-off、不是免費午餐</strong>：Pub/Sub 自動 scaling、但 vendor lock-in、cost-per-message 累積、message ordering / latency 特性跟 Kafka 不同。遷移本身要驗證 <em>業務語意</em> 跟 Pub/Sub 兼容。對應 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">03.4 broker basics</a>。</li>
<li><strong>遷移本身是容量規劃題目</strong>：把 7500 萬用戶的事件交付從 A 平台搬到 B 平台、不能停機、不能丟 message。這個遷移過程本身就是高併發容量工程。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence</a> 的同類流程。</li>
</ol>
<p>需要警惕：Spotify 這個決定不是「Kafka 不好」、是「Spotify 規模下、自管 Kafka 的工程投入不划算」。對中小團隊、自管 Kafka 可能是更便宜的選項。讀案例時要看 <em>規模門檻</em> 跟 <em>團隊能力</em>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>broker 自管 vs managed 是長期 TCO 評估</strong>：算「平日運維 + 容量擴容 + 故障處理 + 升級遷移」的人力成本、不只算「broker 雲端費用」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
<li><strong>遷移分階段：dual write → shadow → cutover</strong>：先寫兩邊、驗證一致性、再切流量。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence</a> 的同類流程。</li>
<li><strong>業務語意對映是遷移關鍵</strong>：Kafka 的 partition / offset / consumer group 在 Pub/Sub 對映成不同概念（subscription / ordering key / message attribute）、不是 1:1。</li>
</ol>
<p>跨平台等效：AWS SNS / SQS / Kinesis、Amazon MSK（managed Kafka）、Azure Service Bus / Event Hubs / Event Grid 都是對等候選。差異是 message ordering 保證、delivery guarantee、cost model。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想評估 broker 自管 vs managed → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
<li>想做大規模 message 系統遷移 → <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence</a> 的對等流程</li>
<li>想理解 broker 容量規劃 → <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">03.4 broker basics</a></li>
<li>對照其他事件型負載 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/gcp/spotifys-journey-to-cloud-why-spotify-migrated-its-event-delivery-system-from-kafka-to-google-cloud-pubsub">Spotify&rsquo;s journey to cloud: why Spotify migrated its event delivery system from Kafka to Google Cloud Pub/Sub</a></li>
<li><a href="https://cloud.google.com/blog/products/gcp/spotify-chooses-google-cloud-platform-to-power-data-infrastructure/">Spotify chooses Google Cloud Platform</a></li>
<li><a href="https://cloud.google.com/blog/products/gcp/spotifys-experiments-with-stream-processing-on-google-cloud-dataflow">Spotify&rsquo;s experiments with stream processing on Google Cloud Dataflow</a></li>
</ul>
]]></content:encoded></item><item><title>模組九：效能工程與容量規劃</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/</guid><description>&lt;p>效能工程與容量規劃模組的核心目標是回答兩個工程問題：目前的服務配置能承載多少負載，以及面對預期或意外的流量增長時要加多少資源。語言教材會處理 algorithm、hot path 與 memory profile 等程式層效能；本模組負責 workload modeling、壓測工具選型、saturation discovery、瓶頸定位、容量規劃、成本邊界、效能可觀測性與改進閉環。&lt;/p>
&lt;p>本模組跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程&lt;/a> 是 sibling 工程紀律。06 看「失敗模式如何被驗證」，走 SLO、Error Budget、Failure Mode、Chaos Hypothesis 的詞彙；09 看「正常負載如何被量化與規劃」，走 Workload、Saturation、Capacity、Cost、Throughput、Latency 的詞彙。兩個模組共用案例庫但讀法不同：06 從案例讀「失敗模式驗證」、09 從案例讀「容量量化實踐」。&lt;/p>
&lt;h2 id="教材定位">教材定位&lt;/h2>
&lt;p>效能工程的角色是把「我不知道目前配置能撐多少」這個常見焦慮，變成可量測、可重播、可改進的工程流程。&lt;/p>
&lt;p>多數後端服務不會每天遇到高併發，真正的工程問題是平常運作時的容量地圖。平常運作正常時，目前的配置距離 saturation 還有多遠；當意外流量出現時，現有配置能撐到 autoscaling 介入嗎；要加機器時，怎麼算出該加多少、加在哪一層；加了機器之後，怎麼確認瓶頸真的被移除了。&lt;/p>
&lt;p>這四個問題不需要假設高併發場景，而是要求系統在任何配置下都能回答「現在的容量地圖長什麼樣」。沒有這張地圖，加機器是猜測、不加機器是賭運氣、改架構是恐慌。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>程式層效能&lt;/td>
 &lt;td>algorithm、data structure、hot path、memory profile、micro benchmark&lt;/td>
 &lt;td>workload model、production traffic replay、end-to-end load test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>並發模型&lt;/td>
 &lt;td>goroutine、event loop、thread pool、connection pool 的程式邊界&lt;/td>
 &lt;td>並發設計如何決定 saturation 與 connection pressure 邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Profiling&lt;/td>
 &lt;td>runtime profiler、flame graph、heap dump 解讀&lt;/td>
 &lt;td>continuous profiling 接入、profile diff 作為 regression 定位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量量測&lt;/td>
 &lt;td>resource metric API、process memory、GC pause 訊號&lt;/td>
 &lt;td>saturation metric、USE method、RED method、cost dashboard&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量規劃&lt;/td>
 &lt;td>（不負責）&lt;/td>
 &lt;td>peak forecast、headroom model、growth curve、autoscaling sizing、cost ceiling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>壓測工具&lt;/td>
 &lt;td>（不負責）&lt;/td>
 &lt;td>k6、JMeter、Gatling、Locust、Vegeta、production traffic replay 工具的選型與整合&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="問題節點">問題節點&lt;/h2>
&lt;p>問題節點先描述「不知道答案會發生什麼」，再描述「怎麼建立答案」。讀者能先理解這個問題為什麼重要，再看到怎麼處理。&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>Workload Modeling&lt;/td>
 &lt;td>壓測模型是否貼近 production traffic shape&lt;/td>
 &lt;td>percentile distribution、cohort mix、burst pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load Test Tooling&lt;/td>
 &lt;td>該用哪種工具、怎麼整合 CI 跟 staging&lt;/td>
 &lt;td>tool capability vs workload shape、CI 整合成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Saturation Discovery&lt;/td>
 &lt;td>配置距離飽和還有多少 headroom&lt;/td>
 &lt;td>throughput plateau、latency knee、resource saturation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bottleneck Localization&lt;/td>
 &lt;td>瓶頸在哪一層、是 app / DB / cache / broker&lt;/td>
 &lt;td>resource utilization、queue depth、connection exhaustion&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity Planning&lt;/td>
 &lt;td>要加多少機器、加在哪一層&lt;/td>
 &lt;td>peak forecast、headroom budget、growth curve&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost Engineering&lt;/td>
 &lt;td>容量擴張的成本曲線、降級的成本邊界&lt;/td>
 &lt;td>cost per request、autoscaling cost ceiling、over-provision waste&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Performance Observability&lt;/td>
 &lt;td>容量訊號怎麼看、跟 SLO 怎麼接&lt;/td>
 &lt;td>saturation metric、cost attribution、SLO budget&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Improvement Loop&lt;/td>
 &lt;td>從壓測到 release 怎麼閉環&lt;/td>
 &lt;td>profile diff、regression gate、canary perf signal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production Validation&lt;/td>
 &lt;td>怎麼在 production 安全驗證新配置&lt;/td>
 &lt;td>shadow traffic、dark launch、canary perf check&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Peak Event Readiness&lt;/td>
 &lt;td>預知的流量事件怎麼準備&lt;/td>
 &lt;td>event capacity forecast、pre-warm checklist、rollback path&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的責任是路由。當讀者卡住時，先問三個問題：是模型還是訊號的問題、是量測還是規劃的問題、是技術瓶頸還是成本邊界的問題。這三個問題會把讀者導向不同主章。&lt;/p></description><content:encoded><![CDATA[<p>效能工程與容量規劃模組的核心目標是回答兩個工程問題：目前的服務配置能承載多少負載，以及面對預期或意外的流量增長時要加多少資源。語言教材會處理 algorithm、hot path 與 memory profile 等程式層效能；本模組負責 workload modeling、壓測工具選型、saturation discovery、瓶頸定位、容量規劃、成本邊界、效能可觀測性與改進閉環。</p>
<p>本模組跟 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a> 是 sibling 工程紀律。06 看「失敗模式如何被驗證」，走 SLO、Error Budget、Failure Mode、Chaos Hypothesis 的詞彙；09 看「正常負載如何被量化與規劃」，走 Workload、Saturation、Capacity、Cost、Throughput、Latency 的詞彙。兩個模組共用案例庫但讀法不同：06 從案例讀「失敗模式驗證」、09 從案例讀「容量量化實踐」。</p>
<h2 id="教材定位">教材定位</h2>
<p>效能工程的角色是把「我不知道目前配置能撐多少」這個常見焦慮，變成可量測、可重播、可改進的工程流程。</p>
<p>多數後端服務不會每天遇到高併發，真正的工程問題是平常運作時的容量地圖。平常運作正常時，目前的配置距離 saturation 還有多遠；當意外流量出現時，現有配置能撐到 autoscaling 介入嗎；要加機器時，怎麼算出該加多少、加在哪一層；加了機器之後，怎麼確認瓶頸真的被移除了。</p>
<p>這四個問題不需要假設高併發場景，而是要求系統在任何配置下都能回答「現在的容量地圖長什麼樣」。沒有這張地圖，加機器是猜測、不加機器是賭運氣、改架構是恐慌。</p>
<h2 id="教材邊界">教材邊界</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>放在語言教材</th>
          <th>放在本模組</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>程式層效能</td>
          <td>algorithm、data structure、hot path、memory profile、micro benchmark</td>
          <td>workload model、production traffic replay、end-to-end load test</td>
      </tr>
      <tr>
          <td>並發模型</td>
          <td>goroutine、event loop、thread pool、connection pool 的程式邊界</td>
          <td>並發設計如何決定 saturation 與 connection pressure 邊界</td>
      </tr>
      <tr>
          <td>Profiling</td>
          <td>runtime profiler、flame graph、heap dump 解讀</td>
          <td>continuous profiling 接入、profile diff 作為 regression 定位</td>
      </tr>
      <tr>
          <td>容量量測</td>
          <td>resource metric API、process memory、GC pause 訊號</td>
          <td>saturation metric、USE method、RED method、cost dashboard</td>
      </tr>
      <tr>
          <td>容量規劃</td>
          <td>（不負責）</td>
          <td>peak forecast、headroom model、growth curve、autoscaling sizing、cost ceiling</td>
      </tr>
      <tr>
          <td>壓測工具</td>
          <td>（不負責）</td>
          <td>k6、JMeter、Gatling、Locust、Vegeta、production traffic replay 工具的選型與整合</td>
      </tr>
  </tbody>
</table>
<h2 id="問題節點">問題節點</h2>
<p>問題節點先描述「不知道答案會發生什麼」，再描述「怎麼建立答案」。讀者能先理解這個問題為什麼重要，再看到怎麼處理。</p>
<table>
  <thead>
      <tr>
          <th>節點</th>
          <th>工程問題</th>
          <th>觀察訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workload Modeling</td>
          <td>壓測模型是否貼近 production traffic shape</td>
          <td>percentile distribution、cohort mix、burst pattern</td>
      </tr>
      <tr>
          <td>Load Test Tooling</td>
          <td>該用哪種工具、怎麼整合 CI 跟 staging</td>
          <td>tool capability vs workload shape、CI 整合成本</td>
      </tr>
      <tr>
          <td>Saturation Discovery</td>
          <td>配置距離飽和還有多少 headroom</td>
          <td>throughput plateau、latency knee、resource saturation</td>
      </tr>
      <tr>
          <td>Bottleneck Localization</td>
          <td>瓶頸在哪一層、是 app / DB / cache / broker</td>
          <td>resource utilization、queue depth、connection exhaustion</td>
      </tr>
      <tr>
          <td>Capacity Planning</td>
          <td>要加多少機器、加在哪一層</td>
          <td>peak forecast、headroom budget、growth curve</td>
      </tr>
      <tr>
          <td>Cost Engineering</td>
          <td>容量擴張的成本曲線、降級的成本邊界</td>
          <td>cost per request、autoscaling cost ceiling、over-provision waste</td>
      </tr>
      <tr>
          <td>Performance Observability</td>
          <td>容量訊號怎麼看、跟 SLO 怎麼接</td>
          <td>saturation metric、cost attribution、SLO budget</td>
      </tr>
      <tr>
          <td>Improvement Loop</td>
          <td>從壓測到 release 怎麼閉環</td>
          <td>profile diff、regression gate、canary perf signal</td>
      </tr>
      <tr>
          <td>Production Validation</td>
          <td>怎麼在 production 安全驗證新配置</td>
          <td>shadow traffic、dark launch、canary perf check</td>
      </tr>
      <tr>
          <td>Peak Event Readiness</td>
          <td>預知的流量事件怎麼準備</td>
          <td>event capacity forecast、pre-warm checklist、rollback path</td>
      </tr>
  </tbody>
</table>
<p>這張表的責任是路由。當讀者卡住時，先問三個問題：是模型還是訊號的問題、是量測還是規劃的問題、是技術瓶頸還是成本邊界的問題。這三個問題會把讀者導向不同主章。</p>
<h2 id="跟既有模組的分工">跟既有模組的分工</h2>
<table>
  <thead>
      <tr>
          <th>既有模組</th>
          <th>09 與其分工</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型</a></td>
          <td>00 提供需求量化輸入（traffic / data / failure cost），09 把這些輸入翻成壓測模型與容量計畫</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a></td>
          <td>04 提供 metric / dashboard / SLO baseline，09 定義 saturation metric、USE / RED 訊號、cost attribution 需求</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a></td>
          <td>05 處理 autoscaling、HPA、load balancer 的平台實作，09 提供 capacity 規劃輸入（要 scale 到多少、什麼條件觸發）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性驗證</a></td>
          <td>06 看失敗模式（chaos / error budget / SLO），09 看正常負載（workload / saturation / capacity），共享 6.2 / 6.9 / 6.13 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理</a></td>
          <td>08 處理 capacity-related incident 的事中事後，09 提供事前演練與容量門檻</td>
      </tr>
  </tbody>
</table>
<p>跟 06 的邊界要特別清楚。06.2 load-testing、6.9 capacity-cost、6.13 perf regression gate 留下「在驗證流程中的角色」入口；09 負責「壓測理論、模型、工具、瓶頸定位、容量規劃、成本邊界」的深化。當讀者問「load test 在 release gate 的判讀條件」屬 06；問「load test 的 workload model 怎麼設計、工具怎麼選、瓶頸怎麼定位」屬 09。</p>
<h2 id="從章節到實作的-chain">從章節到實作的 chain</h2>
<p>各章節交付三樣：問題節點、判讀訊號、控制面 link。判讀完成後沿兩條 chain 進入 implementation。</p>
<ol>
<li><strong>Mechanism chain</strong>：點問題節點表的 <code>[control-name]</code> link 進 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge-cards</a>，那層展開機制、邊界、context-dependence。例：<code>[saturation point]</code> 的 knowledge-card 是該 control 的 mechanism SSoT。</li>
<li><strong>Delivery chain</strong>：章節「交接路由」欄位指向下游模組，包括可觀測性（saturation metric / cost dashboard）、部署平台（autoscaling policy / HPA sizing）、可靠性（perf regression gate / SLO budget）與事故處理（capacity incident playbook）。</li>
</ol>
<p>兩條 chain 走完，控制面交付完整。Implementation 強度取決於兩條 chain 的完成度，章節閱讀本身完成 routing 階段。</p>
<h2 id="主章規劃">主章規劃</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論與系統行為</a></td>
          <td>Performance Theory</td>
          <td>Little&rsquo;s Law、queueing theory、USL、saturation curve 的工程意義</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></td>
          <td>Workload Modeling</td>
          <td>把 production traffic shape 翻成可重播的壓測模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></td>
          <td>Load Test Tooling</td>
          <td>k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的選型判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></td>
          <td>Saturation Discovery</td>
          <td>找出 throughput plateau 與 latency knee 的方法</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></td>
          <td>Bottleneck Localization</td>
          <td>從 app 到 DB、cache、broker、第三方 quota 的逐層定位</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Capacity Planning</td>
          <td>peak forecast、headroom、growth curve、autoscaling sizing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a></td>
          <td>Cost Engineering</td>
          <td>cost per request、cost curve、降級成本、over-provisioning trade-off</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Performance Observability</td>
          <td>saturation metric、USE / RED method、cost dashboard</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a></td>
          <td>Improvement Loop</td>
          <td>壓測 → profile → fix → re-test → release gate 的閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></td>
          <td>Production Validation</td>
          <td>shadow traffic、dark launch、canary、production-like load test</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></td>
          <td>Peak Event Readiness</td>
          <td>活動、季節性流量、推廣事件的 capacity readiness 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></td>
          <td>SLO Coupling</td>
          <td>performance budget 跟 SLO / error budget 的對接</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a></td>
          <td>Scaling Axes</td>
          <td>垂直 / 水平擴展取捨、stateless 前提、auto scaling 操作模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 連線池放大解法</a></td>
          <td>Connection Pool Amplification</td>
          <td>PgBouncer / RDS Proxy / ProxySQL 對比、解 9.13 提出的連線池放大隱性成本</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>14 個主章已完成首輪正文。後續工作是補 <code>vendors/</code> 工具入口、提升案例回寫密度，並校正各章與 06 reliability 的分工。</p></blockquote>
<p>主章撰寫順序：9.1 → 9.2 → 9.4 → 9.5 → 9.6 → 9.3 → 9.8 → 9.9 → 9.7 → 9.10 → 9.11 → 9.12。理論與模型先行，工具落地放在 saturation 與 bottleneck 概念成熟之後，最後處理成本與 production 驗證的進階主題。</p>
<h2 id="案例庫規劃">案例庫規劃</h2>
<p>案例庫主軸採「AWS Customer Success Stories」公開案例。這層案例提供具體流量、實例、延遲、成本數字，比一般 engineering blog 更接近實戰判讀。完整索引、讀法與規劃中案例見 <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C 案例正文</a>。</p>
<h3 id="已發佈案例">已發佈案例</h3>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>負載形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1</a></td>
          <td>AWS Prime Day 2025 dogfood</td>
          <td>可預期極端峰值（SQS 1.66 億 msg/sec）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2</a></td>
          <td>GR8 Tech 體育博彩 AI 預測式擴容</td>
          <td>事件型不可預期峰值（54K TPS @ 25ms p95）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3</a></td>
          <td>Coinbase 超低延遲交易</td>
          <td>無峰值低延遲（100K msg/sec、sub-ms）</td>
      </tr>
  </tbody>
</table>
<p>三篇對應三種負載形狀，讀完可以開始把自己的服務歸類，再回到對應主章規劃容量地圖。</p>
<h3 id="規劃中案例補不同視角與規模">規劃中案例（補不同視角與規模）</h3>
<table>
  <thead>
      <tr>
          <th>候選來源</th>
          <th>預期教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lyft / Slack</td>
          <td>微服務 + Auto Scaling、事件型流量的擴容粒度治理</td>
      </tr>
      <tr>
          <td>Riot Games</td>
          <td>EKS 多集群（246 cluster）治理、跨地區延遲與成本平衡</td>
      </tr>
      <tr>
          <td>FanDuel</td>
          <td>直播流量 + 投注峰值的雙重峰值對齊</td>
      </tr>
      <tr>
          <td>Hotstar</td>
          <td>即時 live streaming 全球峰值（1860 萬同時觀看）</td>
      </tr>
      <tr>
          <td>Zoom</td>
          <td>COVID 期間 30 倍成長（1000 萬 → 3 億 DAU）</td>
      </tr>
  </tbody>
</table>
<h3 id="engineering-blog-補充候選">Engineering Blog 補充候選</h3>
<p>當 AWS 案例缺乏某些工程紀律的深度（例如 chaos hypothesis、cell-based architecture 細節），補引 engineering blog 作為交叉驗證。候選來源：Shopify BFCM、Netflix Tech Blog、Amazon Builders&rsquo; Library、Google SRE Book、LinkedIn Engineering、Stripe Engineering、Cloudflare Blog、Discord Engineering、Uber Engineering、Pinterest Engineering 等。這層不另開資料夾，補在主章「案例對照」段。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>效能工程使用方式會受語言的並發模型、runtime overhead、profiler 工具鏈與 client library 成熟度影響。</p>
<ol>
<li>同步 thread-based runtime（Java、C#、傳統 Python / Ruby）：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 是首要瓶頸、blocking I/O 會把 thread 鎖住、壓測時要量 thread saturation 跟 pool exhaustion。</li>
<li>async / event-loop runtime（Node.js、Python asyncio、Tokio）：要量 event loop lag、避免 CPU-bound work 阻塞 loop、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 失控時 throughput 跟 latency 會同時崩。</li>
<li>Goroutine 或 lightweight task runtime（Go、Erlang）：goroutine 廉價但下游連線、檔案 handle、broker channel 仍是昂貴資源、要量「廉價並發 → 昂貴資源」的轉換點。</li>
<li>JIT 語言（JVM、.NET）：warmup 期 latency 高、壓測要區分 cold 與 warm 階段、profile diff 要排除 GC noise。</li>
<li>AOT 語言（Go、Rust、C++）：<a href="/blog/backend/knowledge-cards/cold-start/" data-link-title="Cold Start" data-link-desc="說明服務或快取剛啟動時尚未累積狀態造成的延遲與壓力">cold start</a> 較快、但 GC（Go）或 allocator 行為仍影響長時間 latency。</li>
<li>動態語言（Python、Ruby、PHP）：interpreter overhead 是基線、要先排除 framework 預設配置的隱性成本（worker model、GIL、autoload）。</li>
</ol>
<h2 id="服務分類規範">服務分類規範</h2>
<p>每個討論具體壓測工具或容量服務的章節（k6、JMeter、Gatling、Locust、Vegeta、Grafana k6 Cloud、AWS Distributed Load Testing、Datadog Synthetics、Akamas），都必須包含「成本權衡與機會成本」段落，至少回答：</p>
<ol>
<li>這個工具降低哪一種風險（容量未知、缺少持續驗證、缺少瓶頸定位）。</li>
<li>工具本身的維運成本：runner、artifact、結果儲存、CI 整合成本。</li>
<li>在大規模壓測下會增加哪些雲端成本（流量費、跨區、目標服務的容量壓力）。</li>
<li>團隊需要承擔哪些前置成本：workload model 設計、結果判讀、baseline 維護。</li>
<li>若選擇更簡單方案（人工 ad-hoc 壓測），會承擔哪些風險。</li>
<li>什麼條件出現時，原本的工具選擇應該被重新評估。</li>
</ol>
<h2 id="vendor-清單">Vendor 清單</h2>
<p>實作工具見 <a href="/blog/backend/09-performance-capacity/vendors/" data-link-title="效能與容量工具清單" data-link-desc="整理效能工程、容量規劃、壓測、production replay 與 profiling 工具的服務責任與選型路由">vendors</a> — 已建立 k6 / JMeter / Gatling / Locust / Vegeta 五個壓測工具頁、GoReplay / Service Mesh Mirroring / AWS VPC Traffic Mirroring 三個 production traffic replay 頁，Datadog Continuous Profiler / Pyroscope / Parca 三個 continuous profiling 頁，以及 Akamas / Vantage / CloudHealth / AWS Cost Explorer 四個 capacity / cost analysis 頁。跟 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">06 vendors</a> 的差異：06 收錄壓測工具是為了「驗證流程的工具鏈」、09 收錄是為了「效能工程的工具鏈」、選型角度不同。</p>
<p>Deep article（工具自身的配置、故障、容量）跟 migration playbook（跨工具遷移流程）的撰寫進度見 <a href="/blog/backend/09-performance-capacity/vendors/" data-link-title="效能與容量工具清單" data-link-desc="整理效能工程、容量規劃、壓測、production replay 與 profiling 工具的服務責任與選型路由">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="09-模組專屬知識卡片">09 模組專屬知識卡片</h2>
<p>09 模組已建立 22 張效能工程與容量規劃專屬卡片、覆蓋理論基礎、量測方法、規劃決策、production 驗證與 SLO 治理四個面向。</p>
<p><strong>理論基礎（5 張）</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/little-law/" data-link-title="Little&#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&rsquo;s Law</a> — 並發、到達率、逗留時間的數學關係</li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a> — 擴容到某點後 throughput 反向下降的數學模型</li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a> — linear / knee / cliff 三段曲線的臨界點</li>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a> — 資源層 Utilization / Saturation / Errors</li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a> — 請求層 Rate / Errors / Duration</li>
</ul>
<p><strong>Workload 與容量規劃（8 張）</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a> — production traffic shape 量化模型</li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a> — p99 / p999 長尾為何比平均更能反映 saturation</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a> — 分散式 KV 的隱性 saturation</li>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a> — 預期峰值的預測方法</li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a> — 容量規劃的安全餘量</li>
<li><a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve</a> — 五種典型成長形狀</li>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling</a> — 預測式擴容</li>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a> — 已知時間表預先擴容</li>
</ul>
<p><strong>Production 驗證（5 張）</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a> — production traffic 複製驗證</li>
<li><a href="/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark Launch</a> — UI 入口暫不開放的發布模式</li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a> — canary 階段的 latency 退化檢查</li>
<li><a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff</a> — 兩次 profile 對比找退化原因</li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a> — production 持續低 overhead profile</li>
</ul>
<p><strong>成本與 SLO（4 張）</strong>：</p>
<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> — 雲端成本 unit economics</li>
<li><a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">Performance Budget</a> — 跟 error budget 並列的效能退化額度</li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a> — end-to-end latency 拆到每 stage 配額</li>
<li><a href="/blog/backend/knowledge-cards/slo-baseline-drift/" data-link-title="SLO Baseline Drift" data-link-desc="SLO baseline 因業務變化 / surge / 架構改動而需要重新校準的現象">SLO Baseline Drift</a> — SLO 需要重新校準的現象</li>
</ul>
<h2 id="既有可引用卡片">既有可引用卡片</h2>
<p>從其他模組沿用的卡片：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">Load Test</a></li>
<li><a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">Throughput</a></li>
<li><a href="/blog/backend/knowledge-cards/consumer-capacity/" data-link-title="Consumer Capacity" data-link-desc="說明 consumer 群組每秒能穩定處理多少工作">Consumer Capacity</a></li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool</a></li>
<li><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure</a></li>
<li><a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">Load Shedding</a></li>
<li><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a></li>
<li><a href="/blog/backend/knowledge-cards/cold-start/" data-link-title="Cold Start" data-link-desc="說明服務或快取剛啟動時尚未累積狀態造成的延遲與壓力">Cold Start</a></li>
<li><a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">Thundering Herd</a></li>
<li><a href="/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">Bulkhead</a></li>
<li><a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a></li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">Metric Cardinality</a></li>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day</a></li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">Blast Radius</a></li>
<li><a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">Autoscaling</a></li>
</ul>
<h2 id="模組方法">模組方法</h2>
<p>問題驅動方法的核心是讓案例退到證據角色，讓知識網以「容量量化問題」為主體。</p>
<ol>
<li>先定義效能或容量問題的責任邊界。</li>
<li>再定義判讀訊號（saturation curve、cost curve、percentile distribution）與門檻條件。</li>
<li>接著定義交接路由與前置控制面。</li>
<li>最後在問題觸發時引用對應服務案例。</li>
</ol>
<h2 id="規劃方向">規劃方向</h2>
<p>本模組的核心是把模組架構為「容量量化問題 + 服務級實踐案例」兩層結構。</p>
<ol>
<li><strong>問題節點先行</strong>：9.1-9.12 主章已建立理論、模型、工具、saturation、瓶頸、容量、成本、可觀測性、改進閉環、production 驗證、高峰準備與 SLO 對接的基礎。</li>
<li><strong>服務級案例庫</strong>：以公開效能與容量實踐（Shopify BFCM / Netflix scale / Amazon cost / Google performance budget / LinkedIn capacity planning）作 cases，每個服務累積容量規劃脈絡。</li>
<li><strong>跟 06 共用案例但不同讀法</strong>：服務 case 同一批、但 06 讀「失敗模式驗證」、09 讀「容量量化實踐」、避免重複案例蒐集成本。</li>
</ol>
<p>不經實作即可推進的理由：效能工程的價值在「容量地圖建立與成本邊界判讀」，這層跟具體框架解耦，performance engineering 公開素材成熟，符合先建概念層的條件。</p>
<h2 id="tripwire">Tripwire</h2>
<ul>
<li>寫到第 6 章發現持續繞回 06 已有章節 → 軸線過於相似、合併回 06 或重切。</li>
<li>案例庫跟 06 cases/ 重疊度 &gt; 70% → 改共用 06 案例、不另起一份。</li>
<li>工具章節寫起來像 vendor 比較表、缺判讀邏輯 → 改寫成「workload model → 工具選型」的決策章節。</li>
<li>9.6 capacity planning 跟 9.7 cost engineering 變成兩篇都在講同一個 trade-off → 合併。</li>
<li>9.10 production validation 跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 內容開始重疊 → 明確分工：9.10 走「正常負載驗證」、6.20 走「故障注入安全邊界」。</li>
<li>寫 T1 服務第 3 個時、若 case 之間無共通分類軸 → 改用單服務獨立檔，不開資料夾。</li>
</ul>
<h2 id="模組完成狀態">模組完成狀態</h2>
<p>模組主章與案例庫已完成首輪正文，<code>vendors/</code> 已建立壓測工具、production traffic replay 與 continuous profiling 第一批工具頁。後續工作排序：先補 capacity / cost analysis 工具頁，再提高 9.7-9.12 對案例的回寫密度，最後整理跟 06 reliability 共用案例的分工。</p>
<hr>
<p><em>文件版本：v0.1.0</em>
<em>最後更新：2026-05-12</em>
<em>系列狀態：主章首輪完成，進入工具入口與案例回寫補強</em></p>
]]></content:encoded></item><item><title>2.9 Cache Migration 與 Stampede Rollback（實作示範）</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/</guid><description>&lt;p>Cache migration 與 stampede rollback 的核心責任是讓快取副本在格式、鍵名與覆蓋範圍演進時，仍能保護 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 不被回源流量打穿。這篇以商品詳情與價格快取為例，示範如何把 key schema 演進、freshness 控制、warmup、放行與停損交給可交接 artifact。&lt;/p>
&lt;h2 id="服務路徑與失敗代價">服務路徑與失敗代價&lt;/h2>
&lt;p>這條路徑是 &lt;code>product-page -&amp;gt; cache -&amp;gt; product-db/pricing-service&lt;/code>。商品頁會同時讀取描述、價格、庫存與促銷標籤，快取需要在低延遲與正確性間平衡。&lt;/p>
&lt;p>這篇示範的變更是把舊 key &lt;code>product:{id}&lt;/code> 演進到版本化 key &lt;code>product:v2:{region}:{id}&lt;/code>。演進動機是支援區域價格與促銷欄位拆分，避免舊序列化格式在多區域路徑下持續膨脹。&lt;/p>
&lt;p>失敗代價分三層：描述欄位 stale 主要影響體驗，價格 stale 直接影響交易正確性，回源尖峰會擠壓正式狀態查詢容量。這三層要分別設 freshness、gate 與 rollback 條件。&lt;/p>
&lt;h2 id="key-schema-與相容窗口">Key Schema 與相容窗口&lt;/h2>
&lt;p>Key schema 的責任是讓新舊值可共存，不讓切換變成一次性替換。這條路徑採 &lt;code>dual-read&lt;/code> 再 &lt;code>dual-write&lt;/code> 再 &lt;code>single-read-v2&lt;/code>：&lt;/p>
&lt;ol>
&lt;li>讀取先查 &lt;code>v2&lt;/code>，miss 再查舊 key，最後才回源。&lt;/li>
&lt;li>回填期間新舊 key 同時寫入，保留可回退窗口。&lt;/li>
&lt;li>&lt;code>v2&lt;/code> 命中穩定後，關閉舊 key 寫入，保留舊 key 讀 fallback 一段時間。&lt;/li>
&lt;/ol>
&lt;p>相容窗口的重點是讀語意一致。舊 key 與新 key 的值結構不同時，要先有轉換層，避免同一商品在不同 API path 回傳不同語意。&lt;/p>
&lt;h2 id="freshness-window-與資料分級">Freshness Window 與資料分級&lt;/h2>
&lt;p>Freshness window 的責任是把 stale 代價寫成可執行規則，而不是只寫全域 TTL。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料欄位&lt;/th>
 &lt;th>freshness window&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>商品描述&lt;/td>
 &lt;td>5-15 分鐘&lt;/td>
 &lt;td>體驗導向，短時間 stale 可接受&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>促銷標籤&lt;/td>
 &lt;td>1-3 分鐘&lt;/td>
 &lt;td>促銷切換頻繁，錯誤會影響轉換率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>庫存可售狀態&lt;/td>
 &lt;td>10-30 秒&lt;/td>
 &lt;td>超賣風險高，需接近即時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>價格與幣別&lt;/td>
 &lt;td>5-15 秒&lt;/td>
 &lt;td>交易正確性高風險，需短 TTL 並搭配事件失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗回源保護值&lt;/td>
 &lt;td>3-10 秒&lt;/td>
 &lt;td>下游暫時異常時保護來源，避免反覆 miss 放大回源壓力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與事件失效要同時存在。TTL 控上限，事件失效控即時性；只用其一都會造成隱性風險。&lt;/p>
&lt;h2 id="warmup-與回源保護">Warmup 與回源保護&lt;/h2>
&lt;p>Warmup 的責任是先建立新 key 的可服務覆蓋率，再擴大流量。這條路徑採分批 warmup：&lt;code>region -&amp;gt; category -&amp;gt; hot key list -&amp;gt; 全量&lt;/code>。&lt;/p>
&lt;p>Warmup completion 的判讀訊號：&lt;/p>
&lt;ol>
&lt;li>&lt;code>v2&lt;/code> 命中率在目標區間連續穩定。&lt;/li>
&lt;li>origin QPS 未突破上限。&lt;/li>
&lt;li>熱門 key 的 miss 尖峰已被抹平。&lt;/li>
&lt;/ol>
&lt;p>回源保護策略：&lt;/p>
&lt;ol>
&lt;li>以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight&lt;/a> 合併同 key 同時 miss。&lt;/li>
&lt;li>對回源查詢設 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 與超時。&lt;/li>
&lt;li>回源失敗時寫入短 TTL 降級值，避免瞬時重試風暴。&lt;/li>
&lt;li>針對熱門 key 在切換前做預熱與分散過期。&lt;/li>
&lt;/ol>
&lt;h3 id="cache-切換引發-stampede-的真實事故結構">Cache 切換引發 stampede 的真實事故結構&lt;/h3>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：Cache Stampede Rollout Regression&lt;/a> — 看似低風險的 cache key 或 TTL 切換、若回源保護不足、會讓熱門資料同時 miss。事故結構屬「讀取路徑同時失去緩衝」的系統性失敗、不只是單一 key 問題。&lt;/p>
&lt;p>切換引發 stampede 的三個放大機制會 &lt;em>疊加&lt;/em>、不是獨立失效。在 read-heavy 規模化服務（如 Tinder 47M MAU、Tubi feature store）這類場景、典型疊加順序：重試放大先觸發 → 下游放大跟進 → 應用層放大終結：&lt;/p></description><content:encoded><![CDATA[<p>Cache migration 與 stampede rollback 的核心責任是讓快取副本在格式、鍵名與覆蓋範圍演進時，仍能保護 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 不被回源流量打穿。這篇以商品詳情與價格快取為例，示範如何把 key schema 演進、freshness 控制、warmup、放行與停損交給可交接 artifact。</p>
<h2 id="服務路徑與失敗代價">服務路徑與失敗代價</h2>
<p>這條路徑是 <code>product-page -&gt; cache -&gt; product-db/pricing-service</code>。商品頁會同時讀取描述、價格、庫存與促銷標籤，快取需要在低延遲與正確性間平衡。</p>
<p>這篇示範的變更是把舊 key <code>product:{id}</code> 演進到版本化 key <code>product:v2:{region}:{id}</code>。演進動機是支援區域價格與促銷欄位拆分，避免舊序列化格式在多區域路徑下持續膨脹。</p>
<p>失敗代價分三層：描述欄位 stale 主要影響體驗，價格 stale 直接影響交易正確性，回源尖峰會擠壓正式狀態查詢容量。這三層要分別設 freshness、gate 與 rollback 條件。</p>
<h2 id="key-schema-與相容窗口">Key Schema 與相容窗口</h2>
<p>Key schema 的責任是讓新舊值可共存，不讓切換變成一次性替換。這條路徑採 <code>dual-read</code> 再 <code>dual-write</code> 再 <code>single-read-v2</code>：</p>
<ol>
<li>讀取先查 <code>v2</code>，miss 再查舊 key，最後才回源。</li>
<li>回填期間新舊 key 同時寫入，保留可回退窗口。</li>
<li><code>v2</code> 命中穩定後，關閉舊 key 寫入，保留舊 key 讀 fallback 一段時間。</li>
</ol>
<p>相容窗口的重點是讀語意一致。舊 key 與新 key 的值結構不同時，要先有轉換層，避免同一商品在不同 API path 回傳不同語意。</p>
<h2 id="freshness-window-與資料分級">Freshness Window 與資料分級</h2>
<p>Freshness window 的責任是把 stale 代價寫成可執行規則，而不是只寫全域 TTL。</p>
<table>
  <thead>
      <tr>
          <th>資料欄位</th>
          <th>freshness window</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商品描述</td>
          <td>5-15 分鐘</td>
          <td>體驗導向，短時間 stale 可接受</td>
      </tr>
      <tr>
          <td>促銷標籤</td>
          <td>1-3 分鐘</td>
          <td>促銷切換頻繁，錯誤會影響轉換率</td>
      </tr>
      <tr>
          <td>庫存可售狀態</td>
          <td>10-30 秒</td>
          <td>超賣風險高，需接近即時</td>
      </tr>
      <tr>
          <td>價格與幣別</td>
          <td>5-15 秒</td>
          <td>交易正確性高風險，需短 TTL 並搭配事件失效</td>
      </tr>
      <tr>
          <td>失敗回源保護值</td>
          <td>3-10 秒</td>
          <td>下游暫時異常時保護來源，避免反覆 miss 放大回源壓力</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與事件失效要同時存在。TTL 控上限，事件失效控即時性；只用其一都會造成隱性風險。</p>
<h2 id="warmup-與回源保護">Warmup 與回源保護</h2>
<p>Warmup 的責任是先建立新 key 的可服務覆蓋率，再擴大流量。這條路徑採分批 warmup：<code>region -&gt; category -&gt; hot key list -&gt; 全量</code>。</p>
<p>Warmup completion 的判讀訊號：</p>
<ol>
<li><code>v2</code> 命中率在目標區間連續穩定。</li>
<li>origin QPS 未突破上限。</li>
<li>熱門 key 的 miss 尖峰已被抹平。</li>
</ol>
<p>回源保護策略：</p>
<ol>
<li>以 <a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight</a> 合併同 key 同時 miss。</li>
<li>對回源查詢設 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與超時。</li>
<li>回源失敗時寫入短 TTL 降級值，避免瞬時重試風暴。</li>
<li>針對熱門 key 在切換前做預熱與分散過期。</li>
</ol>
<h3 id="cache-切換引發-stampede-的真實事故結構">Cache 切換引發 stampede 的真實事故結構</h3>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：Cache Stampede Rollout Regression</a> — 看似低風險的 cache key 或 TTL 切換、若回源保護不足、會讓熱門資料同時 miss。事故結構屬「讀取路徑同時失去緩衝」的系統性失敗、不只是單一 key 問題。</p>
<p>切換引發 stampede 的三個放大機制會 <em>疊加</em>、不是獨立失效。在 read-heavy 規模化服務（如 Tinder 47M MAU、Tubi feature store）這類場景、典型疊加順序：重試放大先觸發 → 下游放大跟進 → 應用層放大終結：</p>
<ul>
<li><strong>重試放大</strong>：用戶請求 miss、應用層或 client SDK 內建重試、每次重試又 miss、單一用戶請求變多次 origin QPS</li>
<li><strong>下游放大</strong>：cache miss 同時打到 DB、DB 變慢、應用對 cache 設的 timeout 又觸發新 miss、回到 DB 更慢、形成正向循環</li>
<li><strong>應用層放大</strong>：等待 cache 的 request 堆積、application thread / connection pool 滿、新請求被拒、被拒的請求觸發更多重試</li>
</ul>
<p>判讀重點：stampede 的早期訊號通常出現在下游 origin（DB QPS 突然超 baseline 數倍）跟 application（latency p99 拉高、request queue length 增加）、不一定先在 cache 層看到。cache hit rate 顯示異常時、事故通常已在中後段。</p>
<h3 id="切換順序決定-stampede-風險">切換順序決定 stampede 風險</h3>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 對照：規模差異下的快取策略</a> — 切換順序（先改 key 結構 vs 先改 TTL）會決定是否出現 stampede 連鎖反應、特別在中型服務同時承受活動流量跟版本切換時。</p>
<p><strong>安全切換順序</strong>（dual-read 模式、每步停損點不同）：</p>
<ol>
<li><strong>新 key 寫入啟用</strong>：應用層同時寫舊 key + 新 key、讀路徑不變。停損點是「寫入失敗率」、若雙寫失敗率超基線、回退停止啟用。</li>
<li><strong>新 key 命中觀察</strong>：讀路徑加入 v2 first / fallback to v1 邏輯、v2 命中率隨自然回填爬升。停損點是「v2 hit rate 爬升曲線」、若曲線停滯、表示 warmup 沒擴散到熱資料、要先 manual warmup。</li>
<li><strong>舊 key 命中率穩定下降</strong>：表示新 key 自然 warmup 完成、可進入下一階段。停損點是「舊 key hit rate 是否真的降到目標」、不能只看 v2 hit rate。</li>
<li><strong>舊 key 寫入停止</strong>：只寫 v2、舊 key 自然 TTL 過期。停損點是「v2 唯一寫入是否穩定」、若出現 v2 寫入失敗、回退到雙寫。</li>
<li><strong>舊 key 讀 fallback 移除</strong>：完全切到 v2 only。停損點是「v2 hit rate 是否已達切換前舊 key 水位」、否則 fallback 移除後直接回源。</li>
</ol>
<p><strong>應該注意的反模式</strong>（會引發 stampede）：</p>
<ul>
<li>應先 warmup 新 key 再刪除舊 key、避免所有讀立即 miss</li>
<li>應拆維度切換（key OR TTL OR 序列化各自獨立）、避免多變化疊加讓 debug 困難</li>
<li>應先在低流量 region 試跑、再擴大到全量、避免事故時無回退時間</li>
</ul>
<p>判讀順序：每次切換只動 <em>一個維度</em>（key OR TTL OR 序列化）、先在低流量 region / tenant 試跑、命中率穩定後再擴大。在 Shopify 序列化遷移（<a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a>）類場景、停損 KPI 是「新格式編碼成功率」+「舊格式 fallback 觸發率」；在 Tinder 類 schema 變化頻繁場景、停損 KPI 是「v2 cache hit rate 是否在預估 warmup 時間內達標」。對應 <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> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> 的同類 expand-contract 思維。</p>
<h3 id="schema-變更引發的隱性-cache-invalidation路由見-27">Schema 變更引發的隱性 cache invalidation（路由：見 2.7）</h3>
<p>Cache invalidation <em>模型</em> 主寫於 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的 Invalidation 段</a>；本章從 migration <em>實作步驟</em> 角度補充：schema migration 是 cache stampede 的隱藏觸發點。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> 案例的警惕段提出 <em>風險推測</em>：「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁、一個 schema 變更可能引發 cache invalidation 風險。</p>
<p>Schema 變化讓 cache 失效的三種模式（屬工程實踐推導、非案例直接揭露）：</p>
<ul>
<li><strong>欄位重命名 / 刪除</strong>：舊 cache value 反序列化失敗、application 視為 miss、全部回源</li>
<li><strong>type 變更</strong>（int → string、enum 增 case）：反序列化可能成功但語意錯、業務邏輯踩錯</li>
<li><strong>序列化格式換</strong>（Marshal → MessagePack）：舊格式無法用新 decoder 讀、對應 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify</a> 的雙軌策略</li>
</ul>
<p><strong>Migration 實作步驟</strong>（按優先序）：</p>
<ol>
<li><strong>Schema migration 前盤點 cache key</strong>（最先）：哪些 cache 包含這個 schema 的資料、估算 invalid 範圍。沒這步無法估算 warmup 計畫規模。</li>
<li><strong>大規模 schema migration 配 cache warmup 計畫</strong>：預先 warmup、避免用戶觸發 cache miss。warmup 計畫主寫於本章的「Warmup 與回源保護」段。</li>
<li><strong>新欄位用 versioned key</strong>（同步進行）：<code>product:v2:{id}</code> 跟 <code>product:v1:{id}</code> 並存、避免雙寫干擾。對應 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify 雙軌策略</a>。</li>
<li><strong>降級 fallback</strong>（最後保險）：cache miss 後 origin 也準備好被打、避免假設「cache hit rate 永遠維持高水位」。對應本章「回源保護策略」段。</li>
</ol>
<p>判讀重點：四步應同步落地、缺一個就會在 migration 期間踩 stampede。一致性 invalidation 模型回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>。</p>
<h2 id="rollout--cutover--rollback">Rollout / Cutover / Rollback</h2>
<p>Rollout 的責任是把快取切換拆成可停損批次，不把風險一次放大。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>判讀重點</th>
          <th>停損動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dual read</td>
          <td><code>v2</code> miss 是否快速收斂</td>
          <td>維持舊 key 讀 fallback，暫停擴批</td>
      </tr>
      <tr>
          <td>Dual write</td>
          <td>新舊值語意是否一致</td>
          <td>停新格式寫入，保留舊格式</td>
      </tr>
      <tr>
          <td>Single read on <code>v2</code></td>
          <td>origin QPS 是否受控、價格 stale 是否達門檻</td>
          <td>回退到 dual read，恢復舊 key 讀路徑</td>
      </tr>
      <tr>
          <td>Contract old key</td>
          <td>舊 key 是否仍被依賴</td>
          <td>停 contract，延長相容窗口</td>
      </tr>
  </tbody>
</table>
<p>Rollback 不是只「切回舊 key」。若新格式已經被下游依賴，回退時要同時保留新舊讀寫相容，避免第二次不一致。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>快取 migration evidence 的責任是證明「效能提升」沒有交換成「來源壓力失控」或「交易資料錯誤」。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>cache metrics、origin metrics、query logs、warmup job logs</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>每個 rollout batch 的觀察窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>hit/miss、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>、eviction、latency 分布</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>cache owner、product owner、pricing owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、抽樣覆蓋率、分區漏報</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>未涵蓋低流量區域、尚未演練的促銷尖峰窗口</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的責任是決定是否放行下一批切換，而不是只報告觀測結果。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批、維持當前批、回退到 dual read</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td><code>v2</code> 命中率、origin QPS ceiling、stale price ratio</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>回源尖峰、價格 stale 超門檻、熱門 key miss 反彈</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>舊 key fallback 可維持時間、舊格式寫入可恢復時間</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>cache on-call、pricing on-call</td>
      </tr>
  </tbody>
</table>
<p>這組欄位要對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 與 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>切換過程中的停用新 key、延長 TTL、凍結 invalidation、回退讀路徑都屬於事故決策。每筆決策都要留在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T11:42:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rollback to dual-read and freeze v2-only rollout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;origin QPS exceeded ceiling and stale price ratio increased in TW region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">cache_v2_origin_qps_region_tw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">stale_price_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">cache-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;reduce origin pressure and restore price freshness baseline&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;origin qps or stale ratio does not recover within 15 minutes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫重點對齊 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify：Cache Serialization Migration</a> 與 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>：前者看格式演進與相容窗口，後者看回源尖峰與停損節奏。</p>
<p>這篇不處理分散式鎖正確性、queue replay 或資料庫正式狀態切換。若核心風險在互斥語意、事件重播或資料 schema，路由到 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.4 distributed lock</a>、<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> 或 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
]]></content:encoded></item><item><title>2.C9 反例：快取切換引發 Stampede 回歸</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/</guid><description>&lt;p>這個反例的核心責任是說明快取轉換最常失敗在回源保護不足。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>一次看似低風險的 cache key 或 TTL 切換，會讓熱門資料同時 miss。使用者看到的是 API 變慢與錯誤率上升，資料庫看到的是原本被快取吸收的流量突然全部回源。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>快取切換如果沒有 warmup、singleflight、節流與降級保護，miss 會引發重試，重試又會增加 origin 壓力。影響面是讀取路徑同時失去緩衝，單一 key 層級的思考抓不到全貌。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>回退不應只把程式版本切回去。若新舊快取 key、TTL 或序列化格式已經混在一起，回退還要處理資料可讀性與回源壓力。實務上要先降載或恢復舊 key 讀取，再逐步清理新策略留下的快取狀態。&lt;/p>
&lt;h2 id="快取專屬告警條件">快取專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>熱門 key miss 同步上升，且 origin QPS 快速超過平日基線&lt;/li>
&lt;li>response time 拉長並伴隨重試流量增加&lt;/li>
&lt;li>stale read 與 cache miss 同時惡化&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明快取轉換最常失敗在回源保護不足。</p>
<h2 id="事故長相">事故長相</h2>
<p>一次看似低風險的 cache key 或 TTL 切換，會讓熱門資料同時 miss。使用者看到的是 API 變慢與錯誤率上升，資料庫看到的是原本被快取吸收的流量突然全部回源。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>快取切換如果沒有 warmup、singleflight、節流與降級保護，miss 會引發重試，重試又會增加 origin 壓力。影響面是讀取路徑同時失去緩衝，單一 key 層級的思考抓不到全貌。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>回退不應只把程式版本切回去。若新舊快取 key、TTL 或序列化格式已經混在一起，回退還要處理資料可讀性與回源壓力。實務上要先降載或恢復舊 key 讀取，再逐步清理新策略留下的快取狀態。</p>
<h2 id="快取專屬告警條件">快取專屬告警條件</h2>
<ul>
<li>熱門 key miss 同步上升，且 origin QPS 快速超過平日基線</li>
<li>response time 拉長並伴隨重試流量增加</li>
<li>stale read 與 cache miss 同時惡化</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a> 與 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a>。</p>
]]></content:encoded></item><item><title>3.C9 反例：Queue 語義切換誤配</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/</guid><description>&lt;p>這個反例的核心責任是說明 broker 遷移失敗常發生在語義假設錯置。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>切換 broker 或 consumer group 後，表面上訊息仍然被送達，但業務資料開始出現重複扣款、重複寄信、狀態漏更新這類問題。這種事故很難只靠 queue depth 判斷，因為錯誤發生在「處理語義」而不是「是否有訊息」。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>舊系統若依賴特定 offset 行為、重試節奏或 consumer idempotency，新系統即使名稱上提供相近 delivery semantics，也可能在失敗重播時產生不同結果。語義誤配會沿著下游資料寫入擴散。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>回退前要先確認哪一段資料已經被新語義處理過。若直接切回舊 broker，可能讓同一批事件再次被處理。更穩定的做法是先凍結新 consumer，保留 offset 對照與 replay 範圍，再決定補償或重播。&lt;/p>
&lt;h2 id="queue-專屬告警條件">Queue 專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>下游 reconciliation 同時出現重複與遺漏&lt;/li>
&lt;li>DLQ 激增且重播後仍回到相同錯誤&lt;/li>
&lt;li>consumer lag 下降但業務結果沒有收斂&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 broker 遷移失敗常發生在語義假設錯置。</p>
<h2 id="事故長相">事故長相</h2>
<p>切換 broker 或 consumer group 後，表面上訊息仍然被送達，但業務資料開始出現重複扣款、重複寄信、狀態漏更新這類問題。這種事故很難只靠 queue depth 判斷，因為錯誤發生在「處理語義」而不是「是否有訊息」。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>舊系統若依賴特定 offset 行為、重試節奏或 consumer idempotency，新系統即使名稱上提供相近 delivery semantics，也可能在失敗重播時產生不同結果。語義誤配會沿著下游資料寫入擴散。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>回退前要先確認哪一段資料已經被新語義處理過。若直接切回舊 broker，可能讓同一批事件再次被處理。更穩定的做法是先凍結新 consumer，保留 offset 對照與 replay 範圍，再決定補償或重播。</p>
<h2 id="queue-專屬告警條件">Queue 專屬告警條件</h2>
<ul>
<li>下游 reconciliation 同時出現重複與遺漏</li>
<li>DLQ 激增且重播後仍回到相同錯誤</li>
<li>consumer lag 下降但業務結果沒有收斂</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a> 與 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10</a>。</p>
]]></content:encoded></item><item><title>4.C9 反例：OTel 遷移後訊號漂移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/</guid><description>&lt;p>這個反例的核心責任是說明 observability 遷移失敗常以語意漂移形式出現，資料丟失反而少見。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>OTel 切換後，儀表板看起來都有資料，但 on-call 開始收到不同告警，SLO burn rate 與舊系統長期對不上。同一個事故在新舊管線裡被歸到不同 service、不同 label 或不同 latency bucket。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>觀測資料是事故判讀的入口。若 metric 名稱、label、sampling、aggregation 不一致，團隊會對同一個現象做出不同判斷，甚至在錯誤訊號上回退服務。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>觀測遷移的回退不一定是回到舊 agent。更重要的是保留新舊訊號對照，先停止讓新管線主導告警與 SLO 判定，再修正語意對齊。若直接關掉新管線，反而會失去分析漂移原因的證據。&lt;/p>
&lt;h2 id="觀測專屬告警條件">觀測專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>新舊管線對同一服務的 error rate 長期偏離&lt;/li>
&lt;li>missing span 或 missing metric 比例持續上升&lt;/li>
&lt;li>alert 噪音增加，但事故量沒有對應增加&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17&lt;/a> 與 &lt;a href="https://tarrragon.github.io/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&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 observability 遷移失敗常以語意漂移形式出現，資料丟失反而少見。</p>
<h2 id="事故長相">事故長相</h2>
<p>OTel 切換後，儀表板看起來都有資料，但 on-call 開始收到不同告警，SLO burn rate 與舊系統長期對不上。同一個事故在新舊管線裡被歸到不同 service、不同 label 或不同 latency bucket。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>觀測資料是事故判讀的入口。若 metric 名稱、label、sampling、aggregation 不一致，團隊會對同一個現象做出不同判斷，甚至在錯誤訊號上回退服務。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>觀測遷移的回退不一定是回到舊 agent。更重要的是保留新舊訊號對照，先停止讓新管線主導告警與 SLO 判定，再修正語意對齊。若直接關掉新管線，反而會失去分析漂移原因的證據。</p>
<h2 id="觀測專屬告警條件">觀測專屬告警條件</h2>
<ul>
<li>新舊管線對同一服務的 error rate 長期偏離</li>
<li>missing span 或 missing metric 比例持續上升</li>
<li>alert 噪音增加，但事故量沒有對應增加</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> 與 <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</a>。</p>
]]></content:encoded></item><item><title>5.C9 反例：平台切流未先 Draining</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/</guid><description>&lt;p>這個反例的核心責任是說明部署平台切換失敗常在 connection lifecycle 管理——平台元件本身健康，事故來源是切換時序錯位。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>平台切流一開始看似成功，新的 instance 也通過 readiness，但長連線、背景工作與 load balancer 仍把流量送到即將下線的節點。使用者看到的是短時間大量 5xx、重連風暴與 timeout。&lt;/p>
&lt;p>典型 timeline：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>T+0&lt;/strong>：開始切流，新版本 pod readiness 通過，LB 開始導入流量。&lt;/li>
&lt;li>&lt;strong>T+30s&lt;/strong>：5xx spike 出現。舊 pod 的 endpoint 尚未從所有 kube-proxy / envoy 移除，部分客戶端仍打到舊 pod。舊 pod 同時收到 SIGTERM 開始 shutdown，在途請求被中斷。&lt;/li>
&lt;li>&lt;strong>T+2m&lt;/strong>：長連線客戶端偵測到斷線，觸發 reconnect。大量客戶端同時重連到新 pod，形成 reconnect storm。新 pod 的連線數瞬間飆高，部分 pod 因連線數超出預期開始 timeout。&lt;/li>
&lt;li>&lt;strong>T+5m&lt;/strong>：on-call 判斷切流失敗，決定回退。但回退操作需要時間——DNS 權重切回、LB 規則恢復、舊 pod 重新啟動。&lt;/li>
&lt;li>&lt;strong>T+15m&lt;/strong>：回退完成，舊版本重新接流量。但 reconnect storm 尚未收斂，連線數曲線仍高於 baseline，客戶端在新舊入口之間震盪。&lt;/li>
&lt;li>&lt;strong>T+30m&lt;/strong>：連線數逐漸回落，錯誤率回到 baseline。事故實際影響時間遠超切流本身。&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>事故擴大的根因是 drain、idle timeout、health check、client retry 四者節奏錯位。每一對的不同步都會放大問題：&lt;/p>
&lt;p>&lt;strong>drain 與 endpoint 摘除不同步&lt;/strong>：pod 收到 SIGTERM 開始 shutdown，但 endpoint 還在 LB 的可用集合中（endpoint controller 同步有延遲）。這段窗口內新請求仍被導到即將關閉的 pod，產生 5xx。解法是 preStop hook 先等 endpoint 傳播（5-15 秒），再開始 graceful shutdown。&lt;/p>
&lt;p>&lt;strong>idle timeout 與 drain window 不同步&lt;/strong>：LB 的 idle timeout 設 60 秒，但 drain window 只有 30 秒。drain 結束後 pod 被強制終止，LB 側認為連線還活著（60 秒內不算 idle），繼續送流量到已不存在的 pod。結果是 LB 拿到 connection reset，觸發重試或回 502。&lt;/p>
&lt;p>&lt;strong>health check 與 readiness 語意不同步&lt;/strong>：LB health check 每 10 秒打一次，連續 3 次失敗才摘除。pod 已經 not-ready 但 LB 要 30 秒後才反映。這 30 秒窗口跟 drain window 疊加，讓舊 pod 在 shutdown 狀態下持續收到流量。&lt;/p>
&lt;p>&lt;strong>client retry 與 reconnect 策略不同步&lt;/strong>：客戶端偵測到連線中斷後立即重試（無 backoff），大量客戶端同時重連。如果客戶端沒有 jitter，重連請求會集中在同一毫秒到達，形成 thundering herd。&lt;/p>
&lt;p>這四組錯位在穩態下不會出現——穩態時 drain / timeout / health check 各自運作不衝突。只有在切流時四者同時被觸發，錯位才會互相放大。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>回退分兩個階段，性質不同、節奏不同、不能合併執行。&lt;/p>
&lt;p>&lt;strong>第一階段：凍結 + 恢復穩定路徑（分鐘級）&lt;/strong>。發現切流失敗的第一動作是停止下一批切流（freeze rollout），然後恢復舊入口權重（DNS 加權切回 / LB 規則回復）。新版本 pod 不立即關閉——保留作為對照證據，也避免關閉動作觸發第二波 reconnect。這個階段的目標是「讓震盪不擴大」，所有動作要在 5 分鐘內完成。&lt;/p>
&lt;p>&lt;strong>第二階段：等待收斂 + 修正錯位（小時級）&lt;/strong>。凍結後進入觀察狀態。reconnect storm 需要時間消化——客戶端逐漸穩定到舊入口、連線數曲線下降、5xx 回到 baseline。觀察指標：連線數曲線、reconnect rate、per-version error rate。三項都回到 baseline 且持續 N 分鐘（通常 10-15 分鐘），才算穩定。穩定後開始修正：找出 drain / timeout / health check / retry 的具體錯位點，修正後重新進入小範圍驗證。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明部署平台切換失敗常在 connection lifecycle 管理——平台元件本身健康，事故來源是切換時序錯位。</p>
<h2 id="事故長相">事故長相</h2>
<p>平台切流一開始看似成功，新的 instance 也通過 readiness，但長連線、背景工作與 load balancer 仍把流量送到即將下線的節點。使用者看到的是短時間大量 5xx、重連風暴與 timeout。</p>
<p>典型 timeline：</p>
<ul>
<li><strong>T+0</strong>：開始切流，新版本 pod readiness 通過，LB 開始導入流量。</li>
<li><strong>T+30s</strong>：5xx spike 出現。舊 pod 的 endpoint 尚未從所有 kube-proxy / envoy 移除，部分客戶端仍打到舊 pod。舊 pod 同時收到 SIGTERM 開始 shutdown，在途請求被中斷。</li>
<li><strong>T+2m</strong>：長連線客戶端偵測到斷線，觸發 reconnect。大量客戶端同時重連到新 pod，形成 reconnect storm。新 pod 的連線數瞬間飆高，部分 pod 因連線數超出預期開始 timeout。</li>
<li><strong>T+5m</strong>：on-call 判斷切流失敗，決定回退。但回退操作需要時間——DNS 權重切回、LB 規則恢復、舊 pod 重新啟動。</li>
<li><strong>T+15m</strong>：回退完成，舊版本重新接流量。但 reconnect storm 尚未收斂，連線數曲線仍高於 baseline，客戶端在新舊入口之間震盪。</li>
<li><strong>T+30m</strong>：連線數逐漸回落，錯誤率回到 baseline。事故實際影響時間遠超切流本身。</li>
</ul>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>事故擴大的根因是 drain、idle timeout、health check、client retry 四者節奏錯位。每一對的不同步都會放大問題：</p>
<p><strong>drain 與 endpoint 摘除不同步</strong>：pod 收到 SIGTERM 開始 shutdown，但 endpoint 還在 LB 的可用集合中（endpoint controller 同步有延遲）。這段窗口內新請求仍被導到即將關閉的 pod，產生 5xx。解法是 preStop hook 先等 endpoint 傳播（5-15 秒），再開始 graceful shutdown。</p>
<p><strong>idle timeout 與 drain window 不同步</strong>：LB 的 idle timeout 設 60 秒，但 drain window 只有 30 秒。drain 結束後 pod 被強制終止，LB 側認為連線還活著（60 秒內不算 idle），繼續送流量到已不存在的 pod。結果是 LB 拿到 connection reset，觸發重試或回 502。</p>
<p><strong>health check 與 readiness 語意不同步</strong>：LB health check 每 10 秒打一次，連續 3 次失敗才摘除。pod 已經 not-ready 但 LB 要 30 秒後才反映。這 30 秒窗口跟 drain window 疊加，讓舊 pod 在 shutdown 狀態下持續收到流量。</p>
<p><strong>client retry 與 reconnect 策略不同步</strong>：客戶端偵測到連線中斷後立即重試（無 backoff），大量客戶端同時重連。如果客戶端沒有 jitter，重連請求會集中在同一毫秒到達，形成 thundering herd。</p>
<p>這四組錯位在穩態下不會出現——穩態時 drain / timeout / health check 各自運作不衝突。只有在切流時四者同時被觸發，錯位才會互相放大。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>回退分兩個階段，性質不同、節奏不同、不能合併執行。</p>
<p><strong>第一階段：凍結 + 恢復穩定路徑（分鐘級）</strong>。發現切流失敗的第一動作是停止下一批切流（freeze rollout），然後恢復舊入口權重（DNS 加權切回 / LB 規則回復）。新版本 pod 不立即關閉——保留作為對照證據，也避免關閉動作觸發第二波 reconnect。這個階段的目標是「讓震盪不擴大」，所有動作要在 5 分鐘內完成。</p>
<p><strong>第二階段：等待收斂 + 修正錯位（小時級）</strong>。凍結後進入觀察狀態。reconnect storm 需要時間消化——客戶端逐漸穩定到舊入口、連線數曲線下降、5xx 回到 baseline。觀察指標：連線數曲線、reconnect rate、per-version error rate。三項都回到 baseline 且持續 N 分鐘（通常 10-15 分鐘），才算穩定。穩定後開始修正：找出 drain / timeout / health check / retry 的具體錯位點，修正後重新進入小範圍驗證。</p>
<p>第一階段的陷阱是「回退了但沒凍結」——回退流量的同時繼續推下一批切流，兩個動作互相衝突。第二階段的陷阱是「時間到了就解凍」——用時間而非指標判斷穩定，可能在連線數仍高時重新切流。</p>
<h2 id="這個事故教給後續章節什麼">這個事故教給後續章節什麼</h2>
<ul>
<li><strong>5.3 load balancer 合約</strong>的「切流告警條件」段：四條告警（批次 5xx、reconnect rate、RTO 超時、per-version error rate 偏離）直接來自這類事故的觀測需求。</li>
<li><strong>5.6 Platform Lifecycle Contract</strong>的「三種 Workload 的 Drain 差異」段：短 API、長連線、worker 的 drain 條件不同——這個事故揭露混用單一 drain window 的後果。</li>
<li><strong>5.8 Rollout/Drain/Rollback</strong>的「Traffic / Drain」段退場順序：readiness 先轉 not-ready → 保留 drain 窗口 → 確認連線數下降 → 終止進程，是從這類事故的 timeline 反推出來的。</li>
</ul>
<h2 id="部署專屬告警條件">部署專屬告警條件</h2>
<ul>
<li>切流批次內 5xx 突增（相對於前一批的升幅超過閾值）</li>
<li>長連線重連率快速上升（reconnect rate 超過 baseline N 倍）</li>
<li>rollback time 超過既定 RTO（執行回退後恢復時間超標）</li>
<li>per-version error rate 偏離（新舊版本 error rate 差距持續不收斂）</li>
</ul>
<p>這些告警的閾值要在 release plan 中先定義。切流期告警跟日常告警分流到不同 channel，避免日常 noise 淹沒切流期的關鍵訊號。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a> 看流量契約與回退框架。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 看 drain 的 workload 分類。回 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR/Rollback Rehearsal</a> 看回退演練如何預防這類事故。</p>
]]></content:encoded></item><item><title>7.C9 反例：憑證輪替未分 Scope</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/</guid><description>&lt;p>這個反例的核心責任是說明 credential rotation 的失敗通常是治理節奏錯誤。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>憑證輪替完成後，多個服務同時開始認證失敗。問題不一定是新憑證錯，而是共用憑證牽涉太多服務，且各服務支援新舊憑證的時間窗口不同。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>secret、token、key 若沒有按作用域分開，輪替會變成一次性控制面變更。當一個系統先切新憑證、另一個系統還只認舊憑證，故障會沿著服務依賴快速擴散。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>憑證事故不能只把舊憑證放回去。若舊憑證已被視為風險來源，直接回放可能重新打開安全缺口。更穩定的做法是先分域隔離受影響服務，恢復雙憑證窗口，再逐批收斂。&lt;/p>
&lt;h2 id="資安專屬告警條件">資安專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>認證失敗同時跨多個 service boundary&lt;/li>
&lt;li>輪替失敗率上升並伴隨權限例外增加&lt;/li>
&lt;li>incident log 顯示 owner 與憑證作用域不清&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 credential rotation 的失敗通常是治理節奏錯誤。</p>
<h2 id="事故長相">事故長相</h2>
<p>憑證輪替完成後，多個服務同時開始認證失敗。問題不一定是新憑證錯，而是共用憑證牽涉太多服務，且各服務支援新舊憑證的時間窗口不同。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>secret、token、key 若沒有按作用域分開，輪替會變成一次性控制面變更。當一個系統先切新憑證、另一個系統還只認舊憑證，故障會沿著服務依賴快速擴散。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>憑證事故不能只把舊憑證放回去。若舊憑證已被視為風險來源，直接回放可能重新打開安全缺口。更穩定的做法是先分域隔離受影響服務，恢復雙憑證窗口，再逐批收斂。</p>
<h2 id="資安專屬告警條件">資安專屬告警條件</h2>
<ul>
<li>認證失敗同時跨多個 service boundary</li>
<li>輪替失敗率上升並伴隨權限例外增加</li>
<li>incident log 顯示 owner 與憑證作用域不清</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6</a> 與 <a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14</a>。</p>
]]></content:encoded></item><item><title>6.9 容量與成本邊界</title><link>https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>容量規劃的核心：peak demand × headroom × growth curve&lt;/li>
&lt;li>headroom 訂定：成本 vs 突發承載 tradeoff&lt;/li>
&lt;li>capacity test 跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test&lt;/a> 的差異：load 看 throughput、capacity 看 saturation 與 cost curve&lt;/li>
&lt;li>成本作為驗證輸入：autoscaling 上限、預算告警、queue lag 跟成本的關係&lt;/li>
&lt;li>跨層容量：DB connection、queue、cache、CDN、第三方 API rate limit&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO&lt;/a> 的耦合：SLO 達成的容量代價&lt;/li>
&lt;li>反模式：容量規劃只看 CPU、autoscaling 無上限、成本失控用降級掩蓋&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Capacity 與成本邊界是把容量規劃跟成本約束一起看，責任是讓系統能承載預期負載，同時不把成本曲線推到不可接受區域。&lt;/p>
&lt;p>這一頁處理的是規模化之後的 trade-off。容量不是越高越好，真正的目標是找到能維持 SLO、又不浪費資源的區間。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 capacity 時，先看 saturation 點，再看成本曲線是不是隨之失控。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>autoscaling 是否有清楚上限與成本門檻&lt;/li>
&lt;li>依賴層是否先於應用層成為瓶頸&lt;/li>
&lt;li>peak forecast 是否涵蓋活動、季節性與推廣事件&lt;/li>
&lt;li>降級是否被當成例外策略，而不是常態容量替代&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">Shopify&lt;/a>：高峰型流量把容量與成本的邊界推得很清楚。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">LinkedIn&lt;/a>：互動型服務常先在某個依賴層出現瓶頸。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/" data-link-title="Amazon" data-link-desc="Amazon Cell-based Architecture / Shuffle Sharding / Blast Radius 設計">Amazon&lt;/a>：大規模系統常把成本與可靠性一起做優化。&lt;/li>
&lt;/ul>
&lt;h2 id="高峰型容量治理game-day--capacity-planning">高峰型容量治理：Game Day + Capacity Planning&lt;/h2>
&lt;p>高峰型容量治理是把「可預期的非典型流量」當獨立治理面操作的能力。涵蓋 baseline 預估、邊界隔離、game day 驗證跟 resiliency matrix 對齊四個面向。日常擴容靠 autoscaling、高峰需要的是預先驗證跟邊界控制 — 峰值期間擴容延遲跟依賴抖動會疊加放大成事故。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1 Shopify BFCM 容量治理與 Game Day&lt;/a>：揭露四個機制對應上述四個面向 — capacity planning baseline（高峰前可承受上限是多少）、pod/isolation boundary（故障影響如何限制在局部）、game day（高峰前如何驗證假設）、resiliency matrix（服務與失效模式如何對齊）。&lt;/p>
&lt;p>可重複套用的做法：&lt;/p>
&lt;p>三個治理面性質不同、不是同一個時間軸的三步驟：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Capacity planning&lt;/strong>（forecast + headroom 模型）：高峰前 N 週開始、整合 forecast、headroom、依賴 quota — 不只看單一 CPU 數字、要看整條依賴鏈的瓶頸層&lt;/li>
&lt;li>&lt;strong>Game day&lt;/strong>（production-like 假設驗證）：高峰前 N 天執行、把 runbook、matrix、驗證腳本、放行門檻當固定資產輸出、不是「跑完就好」&lt;/li>
&lt;li>&lt;strong>Isolation boundary&lt;/strong>（runtime 故障擴散控制）：高峰當下持續運作、cell 邊界跟 graceful degradation 把故障限制在最小可影響範圍、補強 autoscaling 來不及的延遲段&lt;/li>
&lt;/ul>
&lt;p>把每輪活動輸出的缺口回寫成固定資產（不只是「一次性專案」），下一輪準備就能從更高基準開始。&lt;/p>
&lt;h2 id="容量跟值班分層的協同">容量跟值班分層的協同&lt;/h2>
&lt;p>容量跟值班分層的綁定責任是讓「容量門檻」跟「升級路徑」在同一個 trigger 觸發：接近 headroom 限制時值班自動分層、避免事故發生才升級。這個綁定需要三件事配合：runtime 訊號（headroom 預算）、接手機制（三層值班）、模型校準（壓測驗證）。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1 LinkedIn Capacity Headroom 與 On-call 分層&lt;/a>：揭露三個機制對應上述三件事 — headroom 預算（何時進入風險區）、primary/secondary/SME 三層值班（何時由誰接手）、自動化壓測（模型是否貼近現況）。前兩個是 runtime 治理、後者是 model 校準、屬於不同邏輯位階。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>容量規劃的核心：peak demand × headroom × growth curve</li>
<li>headroom 訂定：成本 vs 突發承載 tradeoff</li>
<li>capacity test 跟 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a> 的差異：load 看 throughput、capacity 看 saturation 與 cost curve</li>
<li>成本作為驗證輸入：autoscaling 上限、預算告警、queue lag 跟成本的關係</li>
<li>跨層容量：DB connection、queue、cache、CDN、第三方 API rate limit</li>
<li>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO</a> 的耦合：SLO 達成的容量代價</li>
<li>反模式：容量規劃只看 CPU、autoscaling 無上限、成本失控用降級掩蓋</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Capacity 與成本邊界是把容量規劃跟成本約束一起看，責任是讓系統能承載預期負載，同時不把成本曲線推到不可接受區域。</p>
<p>這一頁處理的是規模化之後的 trade-off。容量不是越高越好，真正的目標是找到能維持 SLO、又不浪費資源的區間。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 capacity 時，先看 saturation 點，再看成本曲線是不是隨之失控。</p>
<p>重點訊號包括：</p>
<ul>
<li>autoscaling 是否有清楚上限與成本門檻</li>
<li>依賴層是否先於應用層成為瓶頸</li>
<li>peak forecast 是否涵蓋活動、季節性與推廣事件</li>
<li>降級是否被當成例外策略，而不是常態容量替代</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">Shopify</a>：高峰型流量把容量與成本的邊界推得很清楚。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">LinkedIn</a>：互動型服務常先在某個依賴層出現瓶頸。</li>
<li><a href="/blog/backend/06-reliability/cases/amazon/" data-link-title="Amazon" data-link-desc="Amazon Cell-based Architecture / Shuffle Sharding / Blast Radius 設計">Amazon</a>：大規模系統常把成本與可靠性一起做優化。</li>
</ul>
<h2 id="高峰型容量治理game-day--capacity-planning">高峰型容量治理：Game Day + Capacity Planning</h2>
<p>高峰型容量治理是把「可預期的非典型流量」當獨立治理面操作的能力。涵蓋 baseline 預估、邊界隔離、game day 驗證跟 resiliency matrix 對齊四個面向。日常擴容靠 autoscaling、高峰需要的是預先驗證跟邊界控制 — 峰值期間擴容延遲跟依賴抖動會疊加放大成事故。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1 Shopify BFCM 容量治理與 Game Day</a>：揭露四個機制對應上述四個面向 — capacity planning baseline（高峰前可承受上限是多少）、pod/isolation boundary（故障影響如何限制在局部）、game day（高峰前如何驗證假設）、resiliency matrix（服務與失效模式如何對齊）。</p>
<p>可重複套用的做法：</p>
<p>三個治理面性質不同、不是同一個時間軸的三步驟：</p>
<ul>
<li><strong>Capacity planning</strong>（forecast + headroom 模型）：高峰前 N 週開始、整合 forecast、headroom、依賴 quota — 不只看單一 CPU 數字、要看整條依賴鏈的瓶頸層</li>
<li><strong>Game day</strong>（production-like 假設驗證）：高峰前 N 天執行、把 runbook、matrix、驗證腳本、放行門檻當固定資產輸出、不是「跑完就好」</li>
<li><strong>Isolation boundary</strong>（runtime 故障擴散控制）：高峰當下持續運作、cell 邊界跟 graceful degradation 把故障限制在最小可影響範圍、補強 autoscaling 來不及的延遲段</li>
</ul>
<p>把每輪活動輸出的缺口回寫成固定資產（不只是「一次性專案」），下一輪準備就能從更高基準開始。</p>
<h2 id="容量跟值班分層的協同">容量跟值班分層的協同</h2>
<p>容量跟值班分層的綁定責任是讓「容量門檻」跟「升級路徑」在同一個 trigger 觸發：接近 headroom 限制時值班自動分層、避免事故發生才升級。這個綁定需要三件事配合：runtime 訊號（headroom 預算）、接手機制（三層值班）、模型校準（壓測驗證）。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1 LinkedIn Capacity Headroom 與 On-call 分層</a>：揭露三個機制對應上述三件事 — headroom 預算（何時進入風險區）、primary/secondary/SME 三層值班（何時由誰接手）、自動化壓測（模型是否貼近現況）。前兩個是 runtime 治理、後者是 model 校準、屬於不同邏輯位階。</p>
<p>容量規劃要回答「擴容門檻是多少」、值班分層要回答「接近門檻時誰接手」。兩者綁定後、高峰期值班分層自動觸發、不需等事故發生才升級。詳見 <a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12 IC handoff for long incident</a>。</p>
<h2 id="快取容量的特殊性">快取容量的特殊性</h2>
<p>快取容量治理的核心責任是失溫時資料層仍可承受。headroom 不是看快取 QPS、是看命中率下滑後的回源放大係數 — 快取本身可能耐 10x 流量、資料層可能撐不到 1.5x。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">P1 Pinterest 快取可靠性與容量驚奇治理</a>：揭露三個機制 — cache headroom（命中率下滑能承受多久）、graceful degradation（快取失效時如何降級）、rewarm strategy（熱資料如何有序回填）。</p>
<p>快取容量規劃的核心問題是失溫時資料層能承受的回源放大係數。命中率從 95% 掉到 80% 意味資料層流量 4x、能否承受決定快取退化會不會升級為事故。預先設計 graceful degradation 路徑跟 rewarm 節奏、能避免快取失溫變成連鎖退化。詳見 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 cache stampede rollback</a>。</p>
<h2 id="產業情境電商與零售">產業情境：電商與零售</h2>
<p>電商的容量規劃受峰值倍率與峰值持續時間的雙重約束。為年度一次的峰值預留全年容量成本過高，但峰值容量不足會直接損失營收 — 容量不足在電商是可量化的商業損失（每分鐘宕機對應可估算的 GMV 損失），技術事故與營收衝擊直接掛鉤。</p>
<p>峰值容量策略有三種模式，各自的成本與風險形狀不同。全年預留是最安全但成本最高的做法，適合峰值與日常倍率差距小（&lt; 3x）的服務。彈性擴容依賴 auto-scaling 在峰值到來時及時反應，但擴容延遲（分鐘級）加上依賴層的 warm-up 時間可能讓尖峰初期無法承接。峰值前臨時擴容需要提前 provision 並用 game day 驗證擴容路徑，是中等成本但需要較高工程投入的選項。多數大型電商混用三者：核心路徑全年預留、彈性層 auto-scale、輔助服務臨時擴容。</p>
<p>降級策略在電商有明確的不可降級邊界。推薦引擎、搜尋排序、個人化功能可以在壓力下退回簡化版或靜態結果，但結帳路徑（購物車 → 付款 → 訂單確認）不能降級 — 結帳流程中斷等於訂單流失，使用者不會等系統恢復後重新結帳。降級策略的設計需要把服務按「可降級 / 不可降級」分層，壓力下優先保護不可降級路徑的資源配額。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>06.2 load testing：capacity 輸入來自 workload model</li>
<li>06.9 reliability metrics：容量與成本要有量測口徑</li>
<li>06.13 perf regression gate：效能退化通常伴隨成本上升</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>autoscaling max 設無限大、或長期未觸碰</li>
<li>容量規劃只看 CPU、忽略 connection pool / queue / 第三方 quota</li>
<li>peak 流量 forecast 是直線外推、未考慮 promo / seasonal / 行銷事件</li>
<li>成本告警觸發後才回頭討論容量</li>
<li>降級邏輯被當成常態容量緩衝、而非例外保護</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04 觀測：saturation metric、cost dashboard</li>
<li>05 部署：HPA / autoscaling policy</li>
<li>06.6 SLO：容量不足導致 SLO 風險</li>
<li>04.15 cost attribution：observability 成本作為總體成本一部分</li>
</ul>
]]></content:encoded></item><item><title>8.9 事故型態庫入口</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-pattern-library/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-pattern-library/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何要有事故型態庫：個案易忘、型態可遷移&lt;/li>
&lt;li>型態跟 case 的差異：case 是時間線、型態是跨案例的共通結構&lt;/li>
&lt;li>核心型態（暫定）：
&lt;ul>
&lt;li>cascading failure（依賴鏈崩塌）&lt;/li>
&lt;li>split-brain（一致性 vs 可用性裂解）&lt;/li>
&lt;li>control-plane failure（管理面失效、data plane 連帶）&lt;/li>
&lt;li>thundering herd（重啟 / 快取冷啟動 / retry storm）&lt;/li>
&lt;li>configuration push 風險（全域配置同步發布）&lt;/li>
&lt;li>capacity surprise（流量模式變化超出規劃）&lt;/li>
&lt;li>long-tail recovery（短時間故障、長時間 recover）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 失控（單點影響全租戶 / 全區域）&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>每個型態的卡片結構：機制、徵兆、放大因子、控制面、典型 case&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/&lt;/a> 的關係：cases 是證據來源、型態是抽象索引&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge-cards&lt;/a> 的差異：型態卡是事故脈絡、知識卡是控制面 mechanism&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>事故型態庫是把跨服務的共通事故結構抽成型態卡，責任是讓新事故能先對照既有 pattern，而不是從零開始命名。&lt;/p>
&lt;p>這一頁處理的是跨案例抽象。case 提供證據，型態庫提供搜尋入口，兩者一起讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 不只停在個案。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀型態卡時，先看它是否有足夠的機制描述，再看能否對應到多個真實 case。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>型態是否有明確機制、徵兆與放大因子&lt;/li>
&lt;li>型態是否能跨團隊遷移，而不是只對單一事故有用&lt;/li>
&lt;li>新事故是否能快速被歸入某個型態&lt;/li>
&lt;li>型態庫是否會隨新 case 持續擴充&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3&lt;/a>：control-plane / dependency 類型常能對應多個事故。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare&lt;/a>：edge / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 類型容易成為共通 pattern。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：大規模平台常同時出現 control-plane 與 coordination 型事故。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>08.5 復盤：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 揭露新型態時補卡&lt;/li>
&lt;li>08.13 repeated / toil：repeated pattern 抽象成型態卡&lt;/li>
&lt;li>08.8 事故報告轉 workflow：型態卡回寫到日常流程&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>新事故發生時、團隊無共通詞彙描述「這像之前哪一類」&lt;/li>
&lt;li>每篇 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 從零開始寫、無 type 標籤&lt;/li>
&lt;li>跨團隊事故 retrospective 缺共享參考型態&lt;/li>
&lt;li>chaos / pre-mortem 場景靠人臨時想、無型態 checklist&lt;/li>
&lt;li>同類型事故反覆發生、但學習未跨團隊傳遞&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>04.13 service topology：cascading failure 型態的拓撲依據&lt;/li>
&lt;li>06.4 chaos：型態作為 chaos 場景輸入&lt;/li>
&lt;li>06.5 failure mode pre-mortem：型態作為 pre-mortem checklist&lt;/li>
&lt;li>08.5 復盤：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 揭露新型態時補卡&lt;/li>
&lt;li>08.13 repeated / toil：repeated pattern 抽象成型態卡&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何要有事故型態庫：個案易忘、型態可遷移</li>
<li>型態跟 case 的差異：case 是時間線、型態是跨案例的共通結構</li>
<li>核心型態（暫定）：
<ul>
<li>cascading failure（依賴鏈崩塌）</li>
<li>split-brain（一致性 vs 可用性裂解）</li>
<li>control-plane failure（管理面失效、data plane 連帶）</li>
<li>thundering herd（重啟 / 快取冷啟動 / retry storm）</li>
<li>configuration push 風險（全域配置同步發布）</li>
<li>capacity surprise（流量模式變化超出規劃）</li>
<li>long-tail recovery（短時間故障、長時間 recover）</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 失控（單點影響全租戶 / 全區域）</li>
</ul>
</li>
<li>每個型態的卡片結構：機制、徵兆、放大因子、控制面、典型 case</li>
<li>跟 <a href="/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/</a> 的關係：cases 是證據來源、型態是抽象索引</li>
<li>跟 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">knowledge-cards</a> 的差異：型態卡是事故脈絡、知識卡是控制面 mechanism</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>事故型態庫是把跨服務的共通事故結構抽成型態卡，責任是讓新事故能先對照既有 pattern，而不是從零開始命名。</p>
<p>這一頁處理的是跨案例抽象。case 提供證據，型態庫提供搜尋入口，兩者一起讓 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 不只停在個案。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀型態卡時，先看它是否有足夠的機制描述，再看能否對應到多個真實 case。</p>
<p>重點訊號包括：</p>
<ul>
<li>型態是否有明確機制、徵兆與放大因子</li>
<li>型態是否能跨團隊遷移，而不是只對單一事故有用</li>
<li>新事故是否能快速被歸入某個型態</li>
<li>型態庫是否會隨新 case 持續擴充</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3</a>：control-plane / dependency 類型常能對應多個事故。</li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare</a>：edge / <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 類型容易成為共通 pattern。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：大規模平台常同時出現 control-plane 與 coordination 型事故。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>08.5 復盤：<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 揭露新型態時補卡</li>
<li>08.13 repeated / toil：repeated pattern 抽象成型態卡</li>
<li>08.8 事故報告轉 workflow：型態卡回寫到日常流程</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>新事故發生時、團隊無共通詞彙描述「這像之前哪一類」</li>
<li>每篇 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 從零開始寫、無 type 標籤</li>
<li>跨團隊事故 retrospective 缺共享參考型態</li>
<li>chaos / pre-mortem 場景靠人臨時想、無型態 checklist</li>
<li>同類型事故反覆發生、但學習未跨團隊傳遞</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.13 service topology：cascading failure 型態的拓撲依據</li>
<li>06.4 chaos：型態作為 chaos 場景輸入</li>
<li>06.5 failure mode pre-mortem：型態作為 pre-mortem checklist</li>
<li>08.5 復盤：<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 揭露新型態時補卡</li>
<li>08.13 repeated / toil：repeated pattern 抽象成型態卡</li>
</ul>
]]></content:encoded></item><item><title>Consul</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/</guid><description>&lt;p>Consul 是 HashiCorp 出品的 service networking 平台、承擔三個責任：service registry + discovery + health check（跨 VM / container / bare metal）、KV store + watch（dynamic config）、service mesh（Consul Connect、mTLS sidecar）。設計取捨偏向「跨平台統一 registry + multi-datacenter 一級公民 + DNS interface」、適合非 K8s-only 環境。BSL 授權變動同 Terraform。&lt;/p>
&lt;p>對「非 K8s 環境 service discovery、跨平台統一 registry、KV store + watch、跨 datacenter mesh」這條路徑、Consul 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Consul cluster（Server + Agent）&lt;/li>
&lt;li>註冊 service + 配置 health check&lt;/li>
&lt;li>用 KV store + watch 做 dynamic config&lt;/li>
&lt;li>部署 Consul Connect（mTLS service mesh）&lt;/li>
&lt;li>評估 BSL 授權影響跟 alternative（etcd / ZooKeeper）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-consul-跑起來">最短路徑：5 分鐘把 Consul 跑起來&lt;/h2>





&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"># 1. 啟動 dev mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">consul agent -dev -client&lt;span class="o">=&lt;/span>0.0.0.0
&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"># 2. 註冊 service（用 JSON 定義）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">cat &amp;gt; web.json &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SVC&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">{&amp;#34;service&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;web&amp;#34;, &amp;#34;port&amp;#34;: 8080,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;check&amp;#34;: {&amp;#34;http&amp;#34;: &amp;#34;http://localhost:8080/health&amp;#34;, &amp;#34;interval&amp;#34;: &amp;#34;10s&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="s">SVC&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">consul services register web.json
&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="c1"># 3. 查詢（DNS + HTTP API）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">dig @127.0.0.1 -p &lt;span class="m">8600&lt;/span> web.service.consul SRV
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">curl -s http://localhost:8500/v1/catalog/service/web &lt;span class="p">|&lt;/span> jq .&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="agent--server-拓樸">Agent / Server 拓樸&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Server：Raft consensus、quorum（3 / 5 node）&lt;/li>
&lt;li>Agent：每 host 一個、forward 到 server&lt;/li>
&lt;li>Client mode（不參 Raft、純 forward）&lt;/li>
&lt;li>對應 K8s 內 sidecar mode&lt;/li>
&lt;/ul>
&lt;h3 id="service-registration">Service registration&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>API / CLI / config file 註冊&lt;/li>
&lt;li>Health check：HTTP / TCP / Script / TTL&lt;/li>
&lt;li>Tags / metadata&lt;/li>
&lt;li>對應指令：&lt;code>consul services register&lt;/code>、&lt;code>consul catalog services&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="kv-store--watch">KV store + watch&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Consul 是 HashiCorp 出品的 service networking 平台、承擔三個責任：service registry + discovery + health check（跨 VM / container / bare metal）、KV store + watch（dynamic config）、service mesh（Consul Connect、mTLS sidecar）。設計取捨偏向「跨平台統一 registry + multi-datacenter 一級公民 + DNS interface」、適合非 K8s-only 環境。BSL 授權變動同 Terraform。</p>
<p>對「非 K8s 環境 service discovery、跨平台統一 registry、KV store + watch、跨 datacenter mesh」這條路徑、Consul 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Consul cluster（Server + Agent）</li>
<li>註冊 service + 配置 health check</li>
<li>用 KV store + watch 做 dynamic config</li>
<li>部署 Consul Connect（mTLS service mesh）</li>
<li>評估 BSL 授權影響跟 alternative（etcd / ZooKeeper）</li>
</ol>
<h2 id="最短路徑5-分鐘把-consul-跑起來">最短路徑：5 分鐘把 Consul 跑起來</h2>





<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. 啟動 dev mode</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">consul agent -dev -client<span class="o">=</span>0.0.0.0
</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"># 2. 註冊 service（用 JSON 定義）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">cat &gt; web.json <span class="s">&lt;&lt;&#39;SVC&#39;
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">{&#34;service&#34;: {&#34;name&#34;: &#34;web&#34;, &#34;port&#34;: 8080,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  &#34;check&#34;: {&#34;http&#34;: &#34;http://localhost:8080/health&#34;, &#34;interval&#34;: &#34;10s&#34;}}}
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">SVC</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">consul services register web.json
</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="c1"># 3. 查詢（DNS + HTTP API）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">dig @127.0.0.1 -p <span class="m">8600</span> web.service.consul SRV
</span></span><span class="line"><span class="ln">13</span><span class="cl">curl -s http://localhost:8500/v1/catalog/service/web <span class="p">|</span> jq .</span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="agent--server-拓樸">Agent / Server 拓樸</h3>
<p>子議題：</p>
<ul>
<li>Server：Raft consensus、quorum（3 / 5 node）</li>
<li>Agent：每 host 一個、forward 到 server</li>
<li>Client mode（不參 Raft、純 forward）</li>
<li>對應 K8s 內 sidecar mode</li>
</ul>
<h3 id="service-registration">Service registration</h3>
<p>子議題：</p>
<ul>
<li>API / CLI / config file 註冊</li>
<li>Health check：HTTP / TCP / Script / TTL</li>
<li>Tags / metadata</li>
<li>對應指令：<code>consul services register</code>、<code>consul catalog services</code></li>
</ul>
<h3 id="kv-store--watch">KV store + watch</h3>
<p>子議題：</p>
<ul>
<li>HTTP API：PUT / GET / DELETE</li>
<li>Watch：long polling / blocking query</li>
<li>適合：dynamic config / feature flag / leader election</li>
<li>對應 consul-template 用 KV 模板生 config</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="consul-connectmtls-service-mesh">Consul Connect（mTLS service mesh）</h3>
<p>子議題：</p>
<ul>
<li>Sidecar proxy（Envoy-based）</li>
<li>Service intentions（誰可訪誰）</li>
<li>mTLS 自動憑證</li>
<li>跟 Istio / Linkerd 對比</li>
</ul>
<h3 id="dns-interface">DNS interface</h3>
<p>子議題：</p>
<ul>
<li>Consul DNS port 8600（dig 可訪）</li>
<li>跟 system resolver 整合（unbound / dnsmasq forward to Consul）</li>
<li>SRV record / A record</li>
<li>對應 service discovery 替代 client-side library</li>
</ul>
<h3 id="multi-datacenter">Multi-datacenter</h3>
<p>子議題：</p>
<ul>
<li>Consul 一級公民跨 DC 設計</li>
<li>WAN federation</li>
<li>Network areas</li>
<li>跟 etcd（單 DC focused）對比</li>
</ul>
<h3 id="acl-system">ACL system</h3>
<p>子議題：</p>
<ul>
<li>Token-based ACL</li>
<li>Policy / Role</li>
<li>Bootstrap token / agent token / management token</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security</a> IAM</li>
</ul>
<h3 id="bsl-授權影響">BSL 授權影響</h3>
<p>子議題：</p>
<ul>
<li>2023 改 BSL（同 Terraform）</li>
<li>不能 host Consul-as-a-Service 對外</li>
<li>對 internal 用沒影響</li>
<li>Fork：HashFork / no major fork yet（vs OpenTofu 對 Terraform）</li>
</ul>
<h3 id="跟-etcd--zookeeper-對比">跟 etcd / ZooKeeper 對比</h3>
<p>子議題：</p>
<ul>
<li>etcd：K8s control plane 後端、API minimal</li>
<li>ZooKeeper：老牌、Java-heavy、Kafka 跟 HBase 用</li>
<li>Consul：service discovery first、DNS / health check 內建</li>
<li>選擇判讀：K8s 內 → etcd（就在那）；non-K8s 多 DC → Consul</li>
</ul>
<h3 id="consul--nomad--vault-integration">Consul + Nomad / Vault integration</h3>
<p>子議題：</p>
<ul>
<li>跟 HashiCorp Nomad（替代 K8s）整合</li>
<li>跟 Vault（secrets）整合</li>
<li>三件套：Consul + Nomad + Vault</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="service-不出現在-catalog">Service 不出現在 catalog</h3>
<p>操作原則：先確認 registration API 成功、再看 health check state。</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">consul catalog services
</span></span><span class="line"><span class="ln">2</span><span class="cl">consul members
</span></span><span class="line"><span class="ln">3</span><span class="cl">consul catalog nodes -service<span class="o">=</span>web</span></span></code></pre></div><h3 id="health-check-flapping">Health check flapping</h3>
<p>操作原則：check interval / timeout 設定 + 應用本身不穩定。判讀：UI 看 check history。</p>
<h3 id="split-brainraft">Split brain（Raft）</h3>
<p>操作原則：Server 數量 &lt; quorum（&lt; 半數）會 split brain。修法：recover snapshot / 加 server。</p>
<h3 id="kv-race-condition">KV race condition</h3>
<p>操作原則：多 client 同時改、要用 CAS（compare-and-swap）。判讀：API ModifyIndex。</p>
<h3 id="consul-connect-sidecar-連不上">Consul Connect sidecar 連不上</h3>
<p>操作原則：proxy config 錯 / intention 沒設 / cert 過期。判讀：Envoy admin endpoint（sidecar 後面）。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s 內 service discovery</td>
          <td>K8s 內建 Service / DNS</td>
      </tr>
      <tr>
          <td>K8s service mesh</td>
          <td>Istio / Linkerd / Cilium</td>
      </tr>
      <tr>
          <td>純 K8s control plane backend</td>
          <td>etcd</td>
      </tr>
      <tr>
          <td>純 Java 生態</td>
          <td>ZooKeeper / Eureka</td>
      </tr>
      <tr>
          <td>BSL 敏感</td>
          <td>etcd（OSI）/ ZooKeeper（OSI）</td>
      </tr>
      <tr>
          <td>Cloud-native（AWS）</td>
          <td>Service Connect for ECS / Cloud Map</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Consul API 完整 reference</li>
<li>Vault / Nomad 細節（各自獨立工具）</li>
<li>Raft protocol 內部</li>
<li>BSL 法律細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Consul 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift self-managed → EKS</a></td>
          <td>Tradeshift 用 Linkerd 做切流、對照 Consul Connect 做跨叢集 mTLS 的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio</a></td>
          <td>大規模 mesh 升級節奏的對照、Consul Connect 在類似治理上要設計分批與回退窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 規模對照</a></td>
          <td>非 K8s 多 DC 場景 Consul 首選、K8s-only 場景則退到 K8s 內建 service discovery</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Consul 案例</strong>：HashiCorp customer story、Bloomberg / Cloudflare / Stripe 等大規模 Consul 案例、Consul → K8s service mesh 遷移案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 deployment platform</a></li>
<li>平行 vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>（K8s 內建 service discovery）</li>
<li>下游能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security IAM</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability</a></li>
</ul>
]]></content:encoded></item><item><title>Gremlin</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/gremlin/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/gremlin/</guid><description>&lt;p>Gremlin 是商業 chaos engineering SaaS、承擔三個責任：跨平台 chaos（VM / container / K8s / cloud 都有 agent）、GameDay 設計 + 報告功能、enterprise-grade audit + blast radius guardrail。設計取捨偏向「商業支援 + 跨平台 + 企業安全 + Halt button 緊急中止」、適合非純 K8s 環境 + 需要商業 SLA 的團隊。Founder 來自 Netflix Chaos team。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Gremlin agent 到 VM / container / K8s&lt;/li>
&lt;li>設計 attack（resource / state / network）+ blast radius&lt;/li>
&lt;li>跑 Scenario / GameDay + 報告交付&lt;/li>
&lt;li>用 Halt button 緊急中止&lt;/li>
&lt;li>評估 Gremlin vs Chaos Mesh / LitmusChaos 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-gremlin-跑起來">最短路徑：5 分鐘把 Gremlin 跑起來&lt;/h2>





&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"># 1. 註冊 + 取得 team API key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: gremlin install or container agent&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="c1"># 2. 第一個 attack&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: gremlin attack-container --target ... --type cpu&lt;/span>
&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="c1"># 3. Dashboard 看 attack timeline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: app.gremlin.com&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="attack-types">Attack types&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Resource：CPU / memory / disk / IO&lt;/li>
&lt;li>State：shutdown / process kill / time travel&lt;/li>
&lt;li>Network：blackhole / DNS / latency / packet loss&lt;/li>
&lt;li>Application：custom error inject&lt;/li>
&lt;/ul>
&lt;h3 id="blast-radius--magnitude">Blast radius + magnitude&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Target selection（host / container / K8s pod）&lt;/li>
&lt;li>Magnitude（影響度、CPU %、latency ms）&lt;/li>
&lt;li>Duration（短到分鐘 / 長到小時）&lt;/li>
&lt;li>Halt button：emergency stop&lt;/li>
&lt;/ul>
&lt;h3 id="scenario--gameday-設計">Scenario / GameDay 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Multi-step attack scenario&lt;/li>
&lt;li>GameDay 跨 team 演練設計&lt;/li>
&lt;li>Report 自動產生&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="cross-platform-agent">Cross-platform agent&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>VM agent（Linux / Windows）&lt;/li>
&lt;li>Container agent（Docker / Kubernetes DaemonSet）&lt;/li>
&lt;li>Cloud agent（AWS / GCP / Azure）&lt;/li>
&lt;li>Agent-less mode（限制較多）&lt;/li>
&lt;/ul>
&lt;h3 id="enterprise-audit--rbac">Enterprise audit + RBAC&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Gremlin 是商業 chaos engineering SaaS、承擔三個責任：跨平台 chaos（VM / container / K8s / cloud 都有 agent）、GameDay 設計 + 報告功能、enterprise-grade audit + blast radius guardrail。設計取捨偏向「商業支援 + 跨平台 + 企業安全 + Halt button 緊急中止」、適合非純 K8s 環境 + 需要商業 SLA 的團隊。Founder 來自 Netflix Chaos team。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Gremlin agent 到 VM / container / K8s</li>
<li>設計 attack（resource / state / network）+ blast radius</li>
<li>跑 Scenario / GameDay + 報告交付</li>
<li>用 Halt button 緊急中止</li>
<li>評估 Gremlin vs Chaos Mesh / LitmusChaos 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-gremlin-跑起來">最短路徑：5 分鐘把 Gremlin 跑起來</h2>





<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. 註冊 + 取得 team API key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: gremlin install or container agent</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="c1"># 2. 第一個 attack</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: gremlin attack-container --target ... --type cpu</span>
</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="c1"># 3. Dashboard 看 attack timeline</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: app.gremlin.com</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="attack-types">Attack types</h3>
<p>子議題：</p>
<ul>
<li>Resource：CPU / memory / disk / IO</li>
<li>State：shutdown / process kill / time travel</li>
<li>Network：blackhole / DNS / latency / packet loss</li>
<li>Application：custom error inject</li>
</ul>
<h3 id="blast-radius--magnitude">Blast radius + magnitude</h3>
<p>子議題：</p>
<ul>
<li>Target selection（host / container / K8s pod）</li>
<li>Magnitude（影響度、CPU %、latency ms）</li>
<li>Duration（短到分鐘 / 長到小時）</li>
<li>Halt button：emergency stop</li>
</ul>
<h3 id="scenario--gameday-設計">Scenario / GameDay 設計</h3>
<p>子議題：</p>
<ul>
<li>Multi-step attack scenario</li>
<li>GameDay 跨 team 演練設計</li>
<li>Report 自動產生</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="cross-platform-agent">Cross-platform agent</h3>
<p>子議題：</p>
<ul>
<li>VM agent（Linux / Windows）</li>
<li>Container agent（Docker / Kubernetes DaemonSet）</li>
<li>Cloud agent（AWS / GCP / Azure）</li>
<li>Agent-less mode（限制較多）</li>
</ul>
<h3 id="enterprise-audit--rbac">Enterprise audit + RBAC</h3>
<p>子議題：</p>
<ul>
<li>Team / Project / Role 設計</li>
<li>Attack approval workflow</li>
<li>Audit log</li>
<li>SSO / SAML</li>
</ul>
<h3 id="跟-oss-chaos-對比">跟 OSS chaos 對比</h3>
<p>子議題：</p>
<ul>
<li>Gremlin：商業 / 跨平台 / GameDay / 報告</li>
<li>OSS（Chaos Mesh / Litmus）：成本低 / K8s-only / 自管</li>
<li>選型判讀：企業合規 + 跨平台 → Gremlin；K8s-only + 預算敏感 → OSS</li>
</ul>
<h3 id="halt-button">Halt button</h3>
<p>子議題：</p>
<ul>
<li>緊急 stop 所有 active attack</li>
<li>對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>跟 incident response 連動</li>
</ul>
<h3 id="application-level-fault">Application-level fault</h3>
<p>子議題：</p>
<ul>
<li>Gremlin ALFI（Application-Level Fault Injection）</li>
<li>SDK integration</li>
<li>Custom exception inject</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="agent-連不上-gremlin">Agent 連不上 Gremlin</h3>
<p>操作原則：API key / network 不通、proxy 配置錯。</p>
<h3 id="attack-沒生效">Attack 沒生效</h3>
<p>操作原則：target selection 沒匹配 / agent 沒安裝。</p>
<h3 id="halt-不及時">Halt 不及時</h3>
<p>操作原則：halt button 全 active attack 立即停、但已造成影響不會回滾。</p>
<h3 id="blast-radius-過大">Blast radius 過大</h3>
<p>操作原則：magnitude / duration 設過大、影響超預期。修法：staging 先測 / 分階段放大。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s OSS</td>
          <td><a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a> / <a href="/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos</a></td>
      </tr>
      <tr>
          <td>Integration test 模擬</td>
          <td><a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></td>
      </tr>
      <tr>
          <td>AWS-only</td>
          <td>AWS Fault Injection Service</td>
      </tr>
      <tr>
          <td>Azure-only</td>
          <td>Azure Chaos Studio</td>
      </tr>
      <tr>
          <td>預算極敏感</td>
          <td>OSS chaos 工具</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Gremlin pricing</li>
<li>各 attack parameter detail</li>
<li>Agent internal</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">Netflix：Steady State、Chaos 與 FIT</a></td>
          <td>chaos 文化的對照組、商業 vs 自建工具的選擇</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">Netflix：Business-Hours Guardrails</a></td>
          <td>attack scope / halt 條件對應時段與 blast radius 控制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>Game Day 設計 + 商業 chaos SaaS 的演練節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>峰值前 Game Day 演練的攻擊類型清單</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">Spotify：平台工程與可靠性契約</a></td>
          <td>squad-based 採用 chaos 的商業工具落地</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Gremlin customer case</strong>：Stripe / Shopify / Slack 直接公開的 Gremlin GameDay engineering blog（目前以 cases/ 內的可靠性脈絡引用為主）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a>、<a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></li>
<li>下游能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a></li>
</ul>
]]></content:encoded></item><item><title>Jeli</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/jeli/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/jeli/</guid><description>&lt;p>Jeli 是 &lt;em>post-incident learning platform&lt;/em>、2023 &lt;a href="https://www.pagerduty.com/blog/welcome-jeli/">被 PagerDuty 收購整合&lt;/a>、定位跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io retro&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant retrospective&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty 既有 Postmortem&lt;/a> 的差異在 &lt;em>human-in-the-loop interview workflow + narrative reconstruction + cross-incident pattern detection&lt;/em>、retro template 本身相近。源自 Etsy / Honeycomb 等 SRE-mature org 的 learning-from-incident 流派、創辦人 Nora Jones 推 Production Excellence 文化。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Jeli 的核心定位是 &lt;em>post-incident learning 的方法論工具&lt;/em>、不是 paging / orchestration / on-call。底層三個責任：&lt;em>incident import + 自動 narrative draft&lt;/em>（從 PagerDuty / Slack / Zoom transcript 拉資料、生 timeline + 故事框架）、&lt;em>structured interview workflow&lt;/em>（OPM-style 訪談 facilitator → operator → contributor、question template 走 context / decision / surprise / pattern 四軸）、&lt;em>cross-incident analysis&lt;/em>（多事故 longitudinal scan 找 systemic issue、非單事故 root cause）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io retrospective&lt;/a> 比、incident.io 走 &lt;em>Slack-native + lightweight template&lt;/em>、Jeli 走 &lt;em>interview-heavy + narrative-first&lt;/em>；incident.io 適合 weekly retro 量大、Jeli 適合 sev1 / sev2 深度復盤。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant retrospective&lt;/a> 比、FireHydrant 走 &lt;em>timeline + action item 結構化&lt;/em>、Jeli 走 &lt;em>contributing factors + surprising behavior 敘事化&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty Postmortem&lt;/a>（收購前的舊模組）比、PagerDuty 走 &lt;em>report template 填空&lt;/em>、Jeli 走 &lt;em>interview transcript → analyst-drafted narrative&lt;/em>；收購後 Jeli 是 PD 推薦的 deep-retro layer。&lt;/p></description><content:encoded><![CDATA[<p>Jeli 是 <em>post-incident learning platform</em>、2023 <a href="https://www.pagerduty.com/blog/welcome-jeli/">被 PagerDuty 收購整合</a>、定位跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io retro</a> / <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant retrospective</a> / <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty 既有 Postmortem</a> 的差異在 <em>human-in-the-loop interview workflow + narrative reconstruction + cross-incident pattern detection</em>、retro template 本身相近。源自 Etsy / Honeycomb 等 SRE-mature org 的 learning-from-incident 流派、創辦人 Nora Jones 推 Production Excellence 文化。</p>
<h2 id="服務定位">服務定位</h2>
<p>Jeli 的核心定位是 <em>post-incident learning 的方法論工具</em>、不是 paging / orchestration / on-call。底層三個責任：<em>incident import + 自動 narrative draft</em>（從 PagerDuty / Slack / Zoom transcript 拉資料、生 timeline + 故事框架）、<em>structured interview workflow</em>（OPM-style 訪談 facilitator → operator → contributor、question template 走 context / decision / surprise / pattern 四軸）、<em>cross-incident analysis</em>（多事故 longitudinal scan 找 systemic issue、非單事故 root cause）。</p>
<p>跟 <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io retrospective</a> 比、incident.io 走 <em>Slack-native + lightweight template</em>、Jeli 走 <em>interview-heavy + narrative-first</em>；incident.io 適合 weekly retro 量大、Jeli 適合 sev1 / sev2 深度復盤。跟 <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant retrospective</a> 比、FireHydrant 走 <em>timeline + action item 結構化</em>、Jeli 走 <em>contributing factors + surprising behavior 敘事化</em>。跟 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty Postmortem</a>（收購前的舊模組）比、PagerDuty 走 <em>report template 填空</em>、Jeli 走 <em>interview transcript → analyst-drafted narrative</em>；收購後 Jeli 是 PD 推薦的 deep-retro layer。</p>
<p>關鍵張力：<em>interview workflow 的人力成本</em> ↔ <em>narrative 品質</em>。Jeli 不能取代 facilitator、它放大有經驗的 incident analyst — 沒人投入 interview / coding / pattern review、narrative 流於 timeline 重寫、cross-incident analysis 空轉。組織要看清自己 <em>願意投入多少 incident analyst 時間換多深的 systemic learning</em>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Jeli 在 IR stack 中承擔哪一段（post-incident learning、不是 paging / orchestration）、為何要外接 <a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> on-call + <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">Slack / Zoom</a> 為 transcript source</li>
<li>Interview workflow 的 ownership 設計（誰當 facilitator、誰 code transcript、誰寫 narrative draft、誰 sign-off）</li>
<li>Cross-incident pattern detection 的最小條件（多少事故樣本、tag 怎麼一致、theme 怎麼歸納）</li>
<li>何時用 Jeli、何時走 incident.io / FireHydrant / PagerDuty Postmortem 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Jeli deployment 是否真的在學習、最少看四件事：</p>
<ul>
<li><strong>Incident import workflow</strong>：從 PagerDuty incident / Slack channel / Zoom transcript 自動 import 是否設好、新事故進來幾分鐘內是否有 draft、source coverage 是否包含主 IR 通訊管道</li>
<li><strong>Interview prep</strong>：sev1 / sev2 是否預設排 interview、facilitator 是否非當事人、question template 是否走 context / decision / surprise / pattern 四軸而非自由 freestyle</li>
<li><strong>Narrative draft 品質</strong>：draft 是否寫成 <em>story</em>（contributing factors / latent conditions / surprising behavior）、不是 timeline 重寫；analyst sign-off 前是否走過 transcript citation 驗證</li>
<li><strong>Cross-incident pattern</strong>：多事故 tag taxonomy 是否一致、是否有人定期跑 6-12 個月 pattern scan、output 是否回到 <a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library</a> 或 process / tooling 改善</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">post-incident review</a> 邊界的待補項目。</p>
<h2 id="最短路徑">最短路徑</h2>





<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. PagerDuty 用戶 enable Jeli module（2024+ 整合）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 2. 從 PagerDuty incident / Slack channel / Zoom transcript 自動 import</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 3. analyst 驗 timeline citation、補 contributing factors + latent conditions</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. Schedule interview（facilitator 非當事人）、走 context / decision / surprise / pattern 四軸</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 5. Sign-off narrative、tag 進固定 taxonomy、進 cross-incident 池</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Incident import + 自動 draft</strong>：Jeli 從 PagerDuty incident metadata、Slack incident channel transcript、Zoom recording transcript 三路 import、自動產 timeline + 參與人列表 + 初步 narrative skeleton。意義是 <em>把人力從「翻聊天紀錄拼 timeline」釋放出來、聚焦在 narrative + interview</em>。但 auto-draft 是骨架不是結論、analyst 必須驗每筆 citation 是否準。</p>
<p><strong>Interview workflow（OPM-style）</strong>：Jeli 推的 <em>Operating Procedures Manual</em> style 訪談 — facilitator 不是 incident commander、不是當事人；question template 走 <em>context</em>（這個系統平常怎麼運作）→ <em>decision</em>（事故當下你想到什麼選項、為何選這個）→ <em>surprise</em>（什麼跟你預期不一樣）→ <em>pattern</em>（你是否在別的事故看過類似形狀）。錄音 + transcription + structured coding（標 contributing factor / latent condition / how-near-miss）是這層的工程化。</p>
<p><strong>Narrative reconstruction</strong>：narrative 不是 chronological event list、是 <em>story</em>。三個必寫元素：<em>contributing factors</em>（多重原因疊加、不是 root cause）、<em>latent conditions</em>（事故前已存在但沒人 trip 的條件、像系統 default config / 文檔誤導）、<em>surprising / unexpected behavior</em>（responder 當下覺得「這不對」的點）。對照 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">post-incident review</a> 的章節原則。</p>
<p><strong>Cross-incident pattern detection</strong>：跨 6-12 個月事故跑 longitudinal analysis、找 <em>recurring component</em>（同一個服務反覆 trip）、<em>recurring handoff</em>（某 team 之間 incident 傳遞失敗）、<em>recurring process gap</em>（同類 runbook 缺漏）。Output 是 org-level intervention 建議（process / tooling / training）、不是個案 action item。需要 tag taxonomy 跨事故一致、否則 pattern detection 抓不出 signal。</p>
<p><strong>PagerDuty 整合（2023+）</strong>：收購後 Jeli 從 PD incident 自動 import、整合進 PD Process Automation 的 post-incident workflow、roadmap 朝 PD 主產品 deep integration。對已是 PagerDuty 客戶的 org 是 ecosystem 一致性增加；對非 PD 環境（用 <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> / <a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a>）整合曲線變陡、長期可能要遷 paging stack。</p>
<p><strong>Causal Analysis based on System Theory (CAST)</strong>：Jeli methodology 受 Nancy Leveson 的 CAST / STAMP 影響、把事故看成 <em>control structure failure</em> 而非 <em>component failure</em>。意義是分析重心從「哪台機器壞」轉到「哪個 control loop（人 + tool + process）失效」。實作上反映在 interview question 的 <em>decision</em> 軸（你當下手上有什麼 control）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Jeli (PagerDuty)</th>
          <th>PagerDuty Postmortem 舊模組</th>
          <th>incident.io retrospective</th>
          <th>FireHydrant retrospective</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要產出</td>
          <td>Narrative + contributing factors</td>
          <td>Report template 填空</td>
          <td>Slack-native retro doc</td>
          <td>Timeline + action item 結構</td>
      </tr>
      <tr>
          <td>訪談支援</td>
          <td>Interview workflow + transcript coding</td>
          <td>無</td>
          <td>無（手動）</td>
          <td>無（手動）</td>
      </tr>
      <tr>
          <td>跨事故 pattern</td>
          <td>Longitudinal analysis 內建</td>
          <td>無</td>
          <td>限於 tag filter</td>
          <td>限於 tag filter</td>
      </tr>
      <tr>
          <td>適用 incident sev</td>
          <td>sev1 / sev2 深度復盤</td>
          <td>一般事故報告</td>
          <td>weekly retro 量大</td>
          <td>weekly retro + action tracking</td>
      </tr>
      <tr>
          <td>人力成本</td>
          <td>高（需 incident analyst）</td>
          <td>低</td>
          <td>低</td>
          <td>低</td>
      </tr>
      <tr>
          <td>平台耦合</td>
          <td>PagerDuty ecosystem</td>
          <td>PagerDuty</td>
          <td>incident.io</td>
          <td>FireHydrant</td>
      </tr>
      <tr>
          <td>文化前提</td>
          <td>Production Excellence、blame-aware</td>
          <td>無前提</td>
          <td>Slack-first IR</td>
          <td>結構化 action tracking</td>
      </tr>
  </tbody>
</table>
<p>選 Jeli 的核心訴求：<em>SRE-mature org + 願投入 incident analyst 時間 + 已是 PagerDuty 生態 + 想做 systemic learning 而非單事故 root cause</em>。中等成熟度組織單事故 retro 量大、走 incident.io / FireHydrant 的輕量模板就夠。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Production Excellence 文化前提</strong>：Nora Jones / Charity Majors 推的 <em>blame-aware</em>（不是 blameless — blameless 太絕對、實務上人會自我審查；blame-aware 是承認情緒存在但不把責任貼個人）學習文化、跟 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a> Production Excellence 對齊。Jeli 工具只在這個文化前提下有用、強行 deploy 到 blame-heavy org 會被當成「找戰犯的另一個工具」。</p>
<p><strong>Interview methodology 深層原則</strong>：question template 不是 checklist、是 <em>讓 responder 重建當下心智模型</em> 的工具。常見反例是 facilitator 問「為什麼你沒看 dashboard」— 這是 <em>hindsight bias</em>；正確問法是「你當下看了哪些 signal、它們告訴你什麼」。facilitator 訓練是 Jeli 流程的隱性投資、不只是工具熟悉度。</p>
<p><strong>Cross-incident tag taxonomy</strong>：pattern detection 的前提是 tag 一致。常見治理失敗：每個 incident 用 free-form tag、半年後同類事故掛不同 tag、longitudinal scan 抓不到 signal。實務治理走 <em>固定 tag dictionary</em>（component / failure mode / contributing factor type）+ 季度 retag review、犧牲一些彈性換 pattern detection 可用性。</p>
<p><strong>Multi-incident analysis 的樣本門檻</strong>：跨事故 pattern 要可信、最少 20-30 個同類事故樣本、跨 6-12 個月時間窗。樣本不足時 <em>pattern</em> 可能只是巧合 — 解法是先把單事故 retro 做扎實、樣本累積到門檻再啟動 longitudinal scan、不要為了「跑 cross-incident」而提前下結論。Output 形狀是 <em>org-level intervention 建議書</em>（哪個 process / tooling / training 該改）、回寫 <a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library</a>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Interview transcript 沒寫好</strong>：facilitator 用 leading question / hindsight bias 問法、responder 答案被引導 — 走 question template review、facilitator 訓練、不讓當事人當 facilitator</li>
<li><strong>Narrative drafting AI hallucination</strong>：auto-draft 把 timeline 缺漏處用 plausible 但無 citation 的描述補上、analyst sign-off 沒驗 citation — 強制每段 narrative claim 必須回指 transcript / Slack / metric 來源、AI draft 是骨架不是結論</li>
<li><strong>Narrative 流於表面 timeline 重寫</strong>：interview 沒問 <em>surprising / unexpected</em> 角度、只重述 chronology — 強化 question template 第三軸、analyst review 拒收沒 contributing factors 段落的 draft</li>
<li><strong>Pattern detection 太空 / 抓不到 signal</strong>：多事故 tag 不一致 / 樣本數不足（&lt; 20 incident）/ 沒人定期跑 scan — 補 tag taxonomy + 季度 pattern review 排程、不到樣本數先當單事故 retro</li>
<li><strong>Interview 排不出來</strong>：sev1 後 facilitator 沒指派 / 當事人 schedule 衝突拖 2 週 — sev1 / sev2 預設 IC handoff 時即指派 facilitator、interview 14 天內必排（記憶衰減 window）</li>
<li><strong>Action item 黑洞</strong>：retro 完成但 action item 沒人 own、3 個月後同類事故重發 — Jeli 不是 action tracking 工具、必須外接 Jira / Linear、retro 完成 == action item 有 owner + due date</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>輕量 weekly retro template</td>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a> / <a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a> retro 模組</td>
      </tr>
      <tr>
          <td>不在 PagerDuty 生態</td>
          <td>Blameless / Howie / 自建 Confluence template</td>
      </tr>
      <tr>
          <td>Action item tracking 為主</td>
          <td>Jira / Linear（Jeli 不擅長）</td>
      </tr>
      <tr>
          <td>沒 incident analyst 人力</td>
          <td>PagerDuty Postmortem 舊模組 / Confluence template + Jira action item</td>
      </tr>
      <tr>
          <td>Blame-heavy 文化未準備</td>
          <td>先補 Production Excellence 文化、再上 Jeli</td>
      </tr>
      <tr>
          <td>Pattern library 治理</td>
          <td><a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library</a>（章節層、不是工具）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Production Excellence 完整理論（Nora Jones / Charity Majors 公開資料）</li>
<li>PagerDuty Process Automation 跟 Jeli 的整合細節 roadmap</li>
<li>CAST / STAMP 完整方法論（Nancy Leveson MIT 公開教材）</li>
<li>Interview facilitator 訓練課程</li>
<li>Tag taxonomy 設計細節（屬 <a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library</a>）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Jeli 流程本身的客戶多為 SRE-mature org（Slack / Honeycomb / Netflix 等公開 talk 引用）、本案例庫沒有直接揭露 Jeli 流程的事故、但所有跨事故 systemic learning 的 case 都是 Jeli 方法論的對照閱讀：</p>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>跟 Jeli 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack cases</a></td>
          <td>Slack 內部事故 retro 結構（外部視角）、Production Excellence 文化內生的 learning 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare cases</a></td>
          <td>多次 control plane / data plane 事故的跨事故 pattern、systemic learning 的具體形狀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub cases</a></td>
          <td>大型平台連續事故的 contributing factor 累積、cross-incident pattern detection 的典型 input</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog cases</a></td>
          <td>觀測平台事故的 surprising / unexpected behavior 紀錄、interview workflow 該抓的 narrative 軸</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library (section)</a></td>
          <td>Jeli cross-incident analysis output 該回寫的 collection、tag taxonomy 治理的章節層原則</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">Post-Incident Review (section)</a></td>
          <td>Narrative reconstruction + contributing factors + interview workflow 的章節層原則、Jeli 是其工具實作</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>、<a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">Post-Incident Review</a></li>
<li>平行：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a>（已整合 paging 來源）、<a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a>、<a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a>（輕量 retro 對照）</li>
<li>下游：<a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">Incident Pattern Library</a>（cross-incident output）、<a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a>（observability + Production Excellence 文化）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（事故當下 signal 來源 → Jeli narrative source）</li>
<li>官方：<a href="https://www.pagerduty.com/blog/welcome-jeli/">Welcome Jeli (PagerDuty blog, 2023)</a></li>
</ul>
]]></content:encoded></item><item><title>Sentry</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/</guid><description>&lt;p>Sentry 是 error tracking 的事實標準、承擔三個責任：跨 frontend / backend / mobile 的 unhandled exception 自動聚合（issue grouping）、release-aware error tracking（regressed errors / source map）、延伸功能（APM / Continuous Profiling / Session Replay / Cron Monitoring）。設計取捨偏向「錯誤生命週期管理 + UX 強 + OSS self-host 雙軌」、不追求 metrics / logs 全面平台。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>整合 Sentry SDK（auto-instrumentation）到 frontend / backend / mobile&lt;/li>
&lt;li>配置 release + source map、追蹤 regressed errors&lt;/li>
&lt;li>設計 issue grouping / fingerprint 避免 noise&lt;/li>
&lt;li>用 Sentry Performance / Session Replay / Cron Monitoring&lt;/li>
&lt;li>評估 self-hosted vs SaaS、跟 IR 平台整合&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-sentry-跑起來">最短路徑：5 分鐘把 Sentry 跑起來&lt;/h2>





&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"># 1. 註冊 Sentry / self-host、拿 DSN&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 從 Console 拿 project DSN&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="c1"># 2. 整合 SDK（範例：Python）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: import sentry_sdk; sentry_sdk.init(dsn=..., traces_sample_rate=1.0)&lt;/span>
&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="c1"># 3. 觸發 test exception 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: try: 1/0 / except: sentry_sdk.capture_exception()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="sdk-整合auto-instrumentation">SDK 整合（auto-instrumentation）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>各語言 SDK：Python / Node / Java / Go / Ruby / PHP / .NET / iOS / Android&lt;/li>
&lt;li>自動 framework instrumentation（Django / FastAPI / Express / Rails 等）&lt;/li>
&lt;li>Manual capture：&lt;code>capture_exception&lt;/code> / &lt;code>capture_message&lt;/code>&lt;/li>
&lt;li>對應 OTel integration（Sentry 接受 OTel context）&lt;/li>
&lt;/ul>
&lt;h3 id="release--source-map">Release / source map&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Release 標記每次部署（git SHA / version）&lt;/li>
&lt;li>Source map 上傳：minified frontend code → readable stack trace&lt;/li>
&lt;li>Regressed errors：之前 resolved 在新 release 又出現&lt;/li>
&lt;li>對應 release health metric&lt;/li>
&lt;/ul>
&lt;h3 id="issue-grouping--fingerprint">Issue grouping / fingerprint&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Auto grouping：based on stack trace + exception type&lt;/li>
&lt;li>自訂 fingerprint：把不同 errors 聚成同 issue&lt;/li>
&lt;li>拆 issue：相同 stack 但需分開追蹤&lt;/li>
&lt;li>對應 noise 控制&lt;/li>
&lt;/ul>
&lt;h3 id="performance-monitoring">Performance monitoring&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Sentry 是 error tracking 的事實標準、承擔三個責任：跨 frontend / backend / mobile 的 unhandled exception 自動聚合（issue grouping）、release-aware error tracking（regressed errors / source map）、延伸功能（APM / Continuous Profiling / Session Replay / Cron Monitoring）。設計取捨偏向「錯誤生命週期管理 + UX 強 + OSS self-host 雙軌」、不追求 metrics / logs 全面平台。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>整合 Sentry SDK（auto-instrumentation）到 frontend / backend / mobile</li>
<li>配置 release + source map、追蹤 regressed errors</li>
<li>設計 issue grouping / fingerprint 避免 noise</li>
<li>用 Sentry Performance / Session Replay / Cron Monitoring</li>
<li>評估 self-hosted vs SaaS、跟 IR 平台整合</li>
</ol>
<h2 id="最短路徑5-分鐘把-sentry-跑起來">最短路徑：5 分鐘把 Sentry 跑起來</h2>





<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. 註冊 Sentry / self-host、拿 DSN</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: 從 Console 拿 project DSN</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="c1"># 2. 整合 SDK（範例：Python）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: import sentry_sdk; sentry_sdk.init(dsn=..., traces_sample_rate=1.0)</span>
</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="c1"># 3. 觸發 test exception 驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: try: 1/0 / except: sentry_sdk.capture_exception()</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="sdk-整合auto-instrumentation">SDK 整合（auto-instrumentation）</h3>
<p>子議題：</p>
<ul>
<li>各語言 SDK：Python / Node / Java / Go / Ruby / PHP / .NET / iOS / Android</li>
<li>自動 framework instrumentation（Django / FastAPI / Express / Rails 等）</li>
<li>Manual capture：<code>capture_exception</code> / <code>capture_message</code></li>
<li>對應 OTel integration（Sentry 接受 OTel context）</li>
</ul>
<h3 id="release--source-map">Release / source map</h3>
<p>子議題：</p>
<ul>
<li>Release 標記每次部署（git SHA / version）</li>
<li>Source map 上傳：minified frontend code → readable stack trace</li>
<li>Regressed errors：之前 resolved 在新 release 又出現</li>
<li>對應 release health metric</li>
</ul>
<h3 id="issue-grouping--fingerprint">Issue grouping / fingerprint</h3>
<p>子議題：</p>
<ul>
<li>Auto grouping：based on stack trace + exception type</li>
<li>自訂 fingerprint：把不同 errors 聚成同 issue</li>
<li>拆 issue：相同 stack 但需分開追蹤</li>
<li>對應 noise 控制</li>
</ul>
<h3 id="performance-monitoring">Performance monitoring</h3>
<p>子議題：</p>
<ul>
<li>Traces sampling rate</li>
<li>Transaction / span 結構（類 APM）</li>
<li>Web Vitals（前端 LCP / FID / CLS）</li>
<li>跟 OTel trace 互操作</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting 策略</a>：預設 grouping 演算法、自訂 fingerprint rules、merge/unmerge、grouping 不準的判讀與大量 unique errors 的治理</li>
<li><a href="release-tracking-session-replay/">Release Tracking 與 Session Replay</a>：release health、deploy tracking、session replay 隱私設定、performance monitoring 與 OTel 整合、self-hosted vs SaaS</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="session-replay">Session Replay</h3>
<p>子議題：</p>
<ul>
<li>前端用戶體驗錄影（含 error 前後操作）</li>
<li>隱私設定：mask PII / block element</li>
<li>Sample rate 控制</li>
<li>跟 LogRocket / FullStory 對照</li>
</ul>
<h3 id="cron-monitoringsentry-crons">Cron Monitoring（Sentry Crons）</h3>
<p>子議題：</p>
<ul>
<li>監控 scheduled job 是否準時跑 + 是否成功</li>
<li>Schedule 配置（crontab / interval）</li>
<li>Heartbeat ping / 自動 alert</li>
<li>對應 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a></li>
</ul>
<h3 id="continuous-profiling">Continuous Profiling</h3>
<p>子議題：</p>
<ul>
<li>各語言 profiler（Python / Node / Go）</li>
<li>CPU / memory flame graph</li>
<li>跟 Pyroscope / Datadog Profiler 對照</li>
</ul>
<h3 id="self-hosted-vs-saas">Self-hosted vs SaaS</h3>
<p>子議題：</p>
<ul>
<li>Self-hosted：Sentry OSS（docker-compose + 數十 service）</li>
<li>SaaS：sentry.io、5 levels（developer / team / business / enterprise）</li>
<li>規模化通常用 SaaS（self-host 維運成本高）</li>
<li>Privacy / compliance 場景：self-host</li>
</ul>
<h3 id="跟-ir-平台整合">跟 IR 平台整合</h3>
<p>子議題：</p>
<ul>
<li>跟 PagerDuty / Opsgenie / incident.io 整合</li>
<li>Alert routing：嚴重 issue → on-call</li>
<li>Issue 跟 incident ticket 關聯</li>
<li>對應 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response 模組</a></li>
</ul>
<h3 id="otel-integration">OTel integration</h3>
<p>子議題：</p>
<ul>
<li>Sentry SDK 接受 OTel context（trace_id / span_id）</li>
<li>跟其他 OTel backend dual ship</li>
<li>Sentry 自家 SDK feature 較深（vs 純 OTel）</li>
</ul>
<h2 id="跟-monitoring-模組的分工">跟 Monitoring 模組的分工</h2>
<p>本頁從 server-side 觀測平台角度說明 Sentry — error grouping 的告警整合、performance monitoring 的 SLI 指標設計、self-hosted vs SaaS 成本、跟 OTel 的 context 整合。Client-side 的使用體驗（SDK 自動攔截設計、error grouping 的 client 端行為、session replay 的操作重播、跟自架 monitor 的比較）見 <a href="/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &#43; performance monitoring &#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Monitoring 模組 Sentry 深入</a>。</p>
<p>兩者的交叉點是 error event 的格式和 trace context propagation — client SDK 捕獲的 error 帶 trace context，server-side 的 Sentry 用同一個 trace 串接完整路徑。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="issue-不出現">Issue 不出現</h3>
<p>操作原則：先確認 SDK 配置（DSN + initialization）、再看 sampling rate、最後看 ad blocker 等網路問題。</p>
<h3 id="issue-noise太多-issue">Issue noise（太多 issue）</h3>
<p>操作原則：用 fingerprint / inbound filter / rate limit 控制。判讀：Issue list 看哪些是噪音。</p>
<h3 id="release-沒對應">Release 沒對應</h3>
<p>操作原則：release tag 沒正確傳 SDK、或 source map 沒上傳。判讀：issue 沒有 release 資訊。</p>
<h3 id="performance-traces-缺失">Performance traces 缺失</h3>
<p>操作原則：sampling rate 過低或 SDK 沒啟用 performance。</p>
<h3 id="session-replay-不出現">Session Replay 不出現</h3>
<p>操作原則：sample rate 設定 + 隱私 setting 是否 block 過頭。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完整 metrics / logs 平台</td>
          <td><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> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / ELK</td>
      </tr>
      <tr>
          <td>High-cardinality 分析</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>純 backend 已有 APM</td>
          <td>跟 <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> APM 重疊、選一即可</td>
      </tr>
      <tr>
          <td>替代 error tracking</td>
          <td>Bugsnag / Rollbar / Raygun（T2 候選）</td>
      </tr>
      <tr>
          <td>Pure logs / metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a> / Cloud-native</td>
      </tr>
      <tr>
          <td>OTel-only 標準</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a> + 任一 backend</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Sentry SDK 完整 API</li>
<li>Sentry self-host 部署細節</li>
<li>各 framework integration 細節</li>
<li>Sentry pricing 詳細</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例待補-frontend-sentry-case">直接相關案例（待補 frontend Sentry case）</h3>
<p>Sentry 是 04 observability 模組第二大 SaaS（次 Datadog）、但 04 cases 庫主要聚焦 OTel / Prometheus / Grafana / ELK 等後端 telemetry pipeline 場景、Sentry 直接案例（frontend error / release health）待補。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Sentry 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Issue 跟 audit evidence 串聯、release 對應監管要求</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak</a></td>
          <td>高峰下 issue noise / rate limit / inbound filter</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>Sentry SDK ↔ OTel context propagation 雙軌驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>Frontend / mobile-heavy team 通常選 Sentry</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 frontend Sentry case</strong>：大規模前端團隊（Shopify / Slack / GitHub frontend）error tracking 案例、release health 落地、跟 incident.io / PagerDuty 整合案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<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>、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>0.9 知識網：訊息與事件決策路徑</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-message-flow/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-message-flow/</guid><description>&lt;p>非同步決策的核心原則是先定義投遞語意，再選擇傳遞工具。&lt;code>queue&lt;/code>、&lt;code>stream&lt;/code>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">&lt;code>pub/sub&lt;/code>&lt;/a>、&lt;code>outbox&lt;/code>、&lt;code>retry&lt;/code>、&lt;code>dead-letter&lt;/code>、&lt;code>replay&lt;/code> 與 &lt;code>idempotency&lt;/code> 是同一條決策鏈，不是獨立名詞清單。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用事件生命週期描述非同步需求&lt;/li>
&lt;li>區分「可延遲」、「可重試」、「可重播」與「可去重」的責任邊界&lt;/li>
&lt;li>把訊息系統術語串成可檢查的決策流程&lt;/li>
&lt;li>判斷目前停在概念層，還是已經進入實作層&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="判讀事件生命週期先於產品選型">【判讀】事件生命週期先於產品選型&lt;/h2>
&lt;p>事件設計的核心問題是「事件在系統裡如何出生、傳遞、處理、失敗、重試與回放」。先回答生命週期，才有辦法判斷是否要用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 或 stream。&lt;/p>
&lt;p>一條最小生命週期通常包含：&lt;/p>
&lt;ol>
&lt;li>產生：&lt;code>producer&lt;/code> 何時發布事件&lt;br>
參考：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">Producer&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern&lt;/a>&lt;/li>
&lt;li>傳遞：事件放在哪種通道&lt;br>
參考：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">Queue&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">Broker&lt;/a>&lt;/li>
&lt;li>消費：&lt;code>consumer&lt;/code> 如何確認處理結果&lt;br>
參考：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">Consumer&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack/Nack&lt;/a>&lt;/li>
&lt;li>失敗：重試與隔離如何發生&lt;br>
參考：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">Retry Policy&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-Letter Queue&lt;/a>&lt;/li>
&lt;li>回復：資料如何補送與重播&lt;br>
參考：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">Replay Runbook&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">Offset&lt;/a>&lt;/li>
&lt;/ol>
&lt;p>這條鏈路完整後，才進入 RabbitMQ、Kafka、Redis Streams 或雲端託管服務比較。&lt;/p>
&lt;h2 id="判讀投遞語意決定設計強度">【判讀】投遞語意決定設計強度&lt;/h2>
&lt;p>投遞語意的核心問題是「失敗後，系統接受哪種結果」。&lt;code>at-most-once&lt;/code>、&lt;code>at-least-once&lt;/code> 與順序需求會直接決定重試、去重與補送成本。&lt;/p>
&lt;p>接近真實網路服務的判斷方式包括：&lt;/p>
&lt;ul>
&lt;li>通知類訊息可接受少量遺失：重點在低延遲與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>。&lt;/li>
&lt;li>金流或庫存狀態不可遺失：重點在持久化、重試與補償，並定義 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/strong-reliability/" data-link-title="Strong Reliability" data-link-desc="說明高可靠事件路徑需要的保存、重試、去重與回復責任">strong reliability&lt;/a> 路徑。&lt;/li>
&lt;li>分析事件可接受短暫延遲：重點在可重播與批次處理。&lt;/li>
&lt;/ul>
&lt;p>對應卡片：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">Duplicate Delivery&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/message-persistence/" data-link-title="Message Persistence" data-link-desc="說明訊息是否落盤保存，以及 broker 重啟後能否恢復">Message Persistence&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-mode/" data-link-title="Delivery Mode" data-link-desc="說明訊息投遞模式如何影響可靠性、延遲與成本">Delivery Mode&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="判讀壅塞與延遲要用同一組語言處理">【判讀】壅塞與延遲要用同一組語言處理&lt;/h2>
&lt;p>非同步壓力的核心問題是「輸入速度高於處理速度」。這會同時反映在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 與重試風暴。&lt;/p>
&lt;p>對應卡片關係：&lt;/p>
&lt;ul>
&lt;li>壓力來源：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">Queue Depth&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">Consumer Lag&lt;/a>&lt;/li>
&lt;li>保護策略：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">Load Shedding&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">Circuit Breaker&lt;/a>&lt;/li>
&lt;li>失敗擴散：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">Retry Storm&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">Cascading Failure&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>這一層討論完成前，不需要先決定 broker 產品或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 數量。&lt;/p></description><content:encoded><![CDATA[<p>非同步決策的核心原則是先定義投遞語意，再選擇傳遞工具。<code>queue</code>、<code>stream</code>、<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者"><code>pub/sub</code></a>、<code>outbox</code>、<code>retry</code>、<code>dead-letter</code>、<code>replay</code> 與 <code>idempotency</code> 是同一條決策鏈，不是獨立名詞清單。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用事件生命週期描述非同步需求</li>
<li>區分「可延遲」、「可重試」、「可重播」與「可去重」的責任邊界</li>
<li>把訊息系統術語串成可檢查的決策流程</li>
<li>判斷目前停在概念層，還是已經進入實作層</li>
</ol>
<hr>
<h2 id="判讀事件生命週期先於產品選型">【判讀】事件生命週期先於產品選型</h2>
<p>事件設計的核心問題是「事件在系統裡如何出生、傳遞、處理、失敗、重試與回放」。先回答生命週期，才有辦法判斷是否要用 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 或 stream。</p>
<p>一條最小生命週期通常包含：</p>
<ol>
<li>產生：<code>producer</code> 何時發布事件<br>
參考：<a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">Producer</a> / <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern</a></li>
<li>傳遞：事件放在哪種通道<br>
參考：<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">Queue</a> / <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a> / <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">Broker</a></li>
<li>消費：<code>consumer</code> 如何確認處理結果<br>
參考：<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">Consumer</a> / <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack/Nack</a></li>
<li>失敗：重試與隔離如何發生<br>
參考：<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">Retry Policy</a> / <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-Letter Queue</a></li>
<li>回復：資料如何補送與重播<br>
參考：<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">Replay Runbook</a> / <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">Offset</a></li>
</ol>
<p>這條鏈路完整後，才進入 RabbitMQ、Kafka、Redis Streams 或雲端託管服務比較。</p>
<h2 id="判讀投遞語意決定設計強度">【判讀】投遞語意決定設計強度</h2>
<p>投遞語意的核心問題是「失敗後，系統接受哪種結果」。<code>at-most-once</code>、<code>at-least-once</code> 與順序需求會直接決定重試、去重與補送成本。</p>
<p>接近真實網路服務的判斷方式包括：</p>
<ul>
<li>通知類訊息可接受少量遺失：重點在低延遲與 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>。</li>
<li>金流或庫存狀態不可遺失：重點在持久化、重試與補償，並定義 <a href="/blog/backend/knowledge-cards/strong-reliability/" data-link-title="Strong Reliability" data-link-desc="說明高可靠事件路徑需要的保存、重試、去重與回復責任">strong reliability</a> 路徑。</li>
<li>分析事件可接受短暫延遲：重點在可重播與批次處理。</li>
</ul>
<p>對應卡片：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">Duplicate Delivery</a></li>
<li><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a></li>
<li><a href="/blog/backend/knowledge-cards/message-persistence/" data-link-title="Message Persistence" data-link-desc="說明訊息是否落盤保存，以及 broker 重啟後能否恢復">Message Persistence</a></li>
<li><a href="/blog/backend/knowledge-cards/delivery-mode/" data-link-title="Delivery Mode" data-link-desc="說明訊息投遞模式如何影響可靠性、延遲與成本">Delivery Mode</a></li>
</ul>
<h2 id="判讀壅塞與延遲要用同一組語言處理">【判讀】壅塞與延遲要用同一組語言處理</h2>
<p>非同步壓力的核心問題是「輸入速度高於處理速度」。這會同時反映在 <a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與重試風暴。</p>
<p>對應卡片關係：</p>
<ul>
<li>壓力來源：<br>
<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure</a> / <a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">Queue Depth</a> / <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">Consumer Lag</a></li>
<li>保護策略：<br>
<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a> / <a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">Load Shedding</a> / <a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">Circuit Breaker</a></li>
<li>失敗擴散：<br>
<a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">Retry Storm</a> / <a href="/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">Cascading Failure</a></li>
</ul>
<p>這一層討論完成前，不需要先決定 broker 產品或 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 數量。</p>
<h2 id="判讀回復流程是可靠性設計的一部分">【判讀】回復流程是可靠性設計的一部分</h2>
<p>回復設計的核心問題是「錯誤發生後如何回到正確狀態」。<code>DLQ</code>、<code>replay</code>、<a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">Data Reconciliation</a> 與 <code>runbook</code> 應該一起定義。</p>
<p>對應卡片：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-Letter Queue</a></li>
<li><a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">Replay Runbook</a></li>
<li><a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">Data Reconciliation</a></li>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a></li>
</ul>
<p>若這些概念只有名詞而沒有決策順序，系統上線後會把排障責任推給個人經驗。</p>
<h2 id="邊界何時從概念章節進入實作章節">【邊界】何時從概念章節進入實作章節</h2>
<p>當以下問題都能回答時，代表概念層已完成，可以進入實作模組：</p>
<ol>
<li>哪些事件可遺失，哪些事件不可遺失</li>
<li>哪些 consumer 需要去重，語意鍵是什麼</li>
<li>何時重試、何時進 DLQ、何時啟動 replay</li>
<li>哪些指標觸發擴容或降級</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li>進入訊息系統能力比較：<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03-message-queue</a></li>
<li>進入可觀測與事故流程：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04-observability</a> / <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08-incident-response</a></li>
</ul>
]]></content:encoded></item><item><title>4.10 Client-side / Synthetic / RUM</title><link>https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Server-side 觀測的盲區&lt;/li>
&lt;li>RUM（Real User Monitoring）：真實用戶端訊號&lt;/li>
&lt;li>Synthetic monitoring：主動探測&lt;/li>
&lt;li>Core Web Vitals 與 backend SLI 的整合&lt;/li>
&lt;li>Client trace 跟 server trace 的串接&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Client-side、Synthetic 與 RUM 訊號是把使用者實際感知納入觀測系統的資料來源，責任是補上 server-side 指標看不到的網路、瀏覽器、地區與裝置差異。&lt;/p>
&lt;p>服務端 200 率正常只代表 backend 有回應。使用者是否真的能完成操作，還要看 DNS 解析、CDN 快取、ISP 路由、瀏覽器渲染與 client-side JavaScript 執行。這些環節每一個都可能讓使用者的體驗跟 server-side dashboard 顯示的完全不同。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 模組&lt;/a> 的分工：monitoring 模組聚焦「非 server 端 runtime 的監控體系」（SDK 設計、collector 架構、rule engine）；本章聚焦「backend 觀測系統如何整合 client-side 訊號」。交叉點是事件格式跟 transport。&lt;/p>
&lt;h2 id="server-side-觀測的盲區">Server-side 觀測的盲區&lt;/h2>
&lt;p>Server-side 觀測能看到「request 到達 server 之後發生了什麼」，看不到「request 到達 server 之前」跟「response 離開 server 之後」的環節。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>環節&lt;/th>
 &lt;th>Server 能看到嗎&lt;/th>
 &lt;th>影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>DNS 解析&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>DNS 異常讓使用者完全到不了 server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN / edge 故障&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>CDN 返回 stale 或 error、server 無感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ISP 路由異常&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>特定地區使用者延遲暴增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TLS handshake&lt;/td>
 &lt;td>部分看得到&lt;/td>
 &lt;td>Certificate 問題讓部分 client 連不上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser rendering&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>TTFB 正常但 LCP / CLS 很差&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client-side JS error&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>功能壞了但 API call 正常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>弱網 / offline&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>Request timeout 或完全沒發出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些盲區意味著 server-side 的「一切正常」跟使用者的「用不了」可以同時存在。&lt;/p>
&lt;h2 id="rumreal-user-monitoring">RUM（Real User Monitoring）&lt;/h2>
&lt;p>RUM 在使用者的瀏覽器或 app 中嵌入監控 SDK，收集真實使用者的效能跟錯誤資料。跟 synthetic monitoring 的差異是 RUM 看的是真實流量，能反映真實的地理分布、裝置差異跟網路條件。&lt;/p>
&lt;h3 id="核心指標">核心指標&lt;/h3>
&lt;p>&lt;strong>頁面效能&lt;/strong>：First Contentful Paint（FCP）、Largest Contentful Paint（LCP）、Cumulative Layout Shift（CLS）、Interaction to Next Paint（INP）。這四個指標（Core Web Vitals 系列）是 Google 定義的使用者體驗量化標準。&lt;/p>
&lt;p>&lt;strong>JS error&lt;/strong>：未捕獲的 exception、promise rejection、resource loading failure。RUM SDK 自動攔截（&lt;code>window.onerror&lt;/code>、&lt;code>unhandledrejection&lt;/code>），帶 stack trace、browser info、page URL。&lt;/p>
&lt;p>&lt;strong>API call 效能&lt;/strong>：從 client 端量測的 API latency（包含 DNS + TCP + TLS + server processing + response download）。跟 server-side 量測的差異就是網路延遲跟 client 處理時間。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Server-side 觀測的盲區</li>
<li>RUM（Real User Monitoring）：真實用戶端訊號</li>
<li>Synthetic monitoring：主動探測</li>
<li>Core Web Vitals 與 backend SLI 的整合</li>
<li>Client trace 跟 server trace 的串接</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Client-side、Synthetic 與 RUM 訊號是把使用者實際感知納入觀測系統的資料來源，責任是補上 server-side 指標看不到的網路、瀏覽器、地區與裝置差異。</p>
<p>服務端 200 率正常只代表 backend 有回應。使用者是否真的能完成操作，還要看 DNS 解析、CDN 快取、ISP 路由、瀏覽器渲染與 client-side JavaScript 執行。這些環節每一個都可能讓使用者的體驗跟 server-side dashboard 顯示的完全不同。</p>
<p>跟 <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 模組</a> 的分工：monitoring 模組聚焦「非 server 端 runtime 的監控體系」（SDK 設計、collector 架構、rule engine）；本章聚焦「backend 觀測系統如何整合 client-side 訊號」。交叉點是事件格式跟 transport。</p>
<h2 id="server-side-觀測的盲區">Server-side 觀測的盲區</h2>
<p>Server-side 觀測能看到「request 到達 server 之後發生了什麼」，看不到「request 到達 server 之前」跟「response 離開 server 之後」的環節。</p>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>Server 能看到嗎</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DNS 解析</td>
          <td>看不到</td>
          <td>DNS 異常讓使用者完全到不了 server</td>
      </tr>
      <tr>
          <td>CDN / edge 故障</td>
          <td>看不到</td>
          <td>CDN 返回 stale 或 error、server 無感</td>
      </tr>
      <tr>
          <td>ISP 路由異常</td>
          <td>看不到</td>
          <td>特定地區使用者延遲暴增</td>
      </tr>
      <tr>
          <td>TLS handshake</td>
          <td>部分看得到</td>
          <td>Certificate 問題讓部分 client 連不上</td>
      </tr>
      <tr>
          <td>Browser rendering</td>
          <td>看不到</td>
          <td>TTFB 正常但 LCP / CLS 很差</td>
      </tr>
      <tr>
          <td>Client-side JS error</td>
          <td>看不到</td>
          <td>功能壞了但 API call 正常</td>
      </tr>
      <tr>
          <td>弱網 / offline</td>
          <td>看不到</td>
          <td>Request timeout 或完全沒發出</td>
      </tr>
  </tbody>
</table>
<p>這些盲區意味著 server-side 的「一切正常」跟使用者的「用不了」可以同時存在。</p>
<h2 id="rumreal-user-monitoring">RUM（Real User Monitoring）</h2>
<p>RUM 在使用者的瀏覽器或 app 中嵌入監控 SDK，收集真實使用者的效能跟錯誤資料。跟 synthetic monitoring 的差異是 RUM 看的是真實流量，能反映真實的地理分布、裝置差異跟網路條件。</p>
<h3 id="核心指標">核心指標</h3>
<p><strong>頁面效能</strong>：First Contentful Paint（FCP）、Largest Contentful Paint（LCP）、Cumulative Layout Shift（CLS）、Interaction to Next Paint（INP）。這四個指標（Core Web Vitals 系列）是 Google 定義的使用者體驗量化標準。</p>
<p><strong>JS error</strong>：未捕獲的 exception、promise rejection、resource loading failure。RUM SDK 自動攔截（<code>window.onerror</code>、<code>unhandledrejection</code>），帶 stack trace、browser info、page URL。</p>
<p><strong>API call 效能</strong>：從 client 端量測的 API latency（包含 DNS + TCP + TLS + server processing + response download）。跟 server-side 量測的差異就是網路延遲跟 client 處理時間。</p>
<h3 id="切分維度">切分維度</h3>
<p>RUM 資料的價值在於可以按維度切分：地區（哪個國家 / 城市慢）、裝置（mobile vs desktop、iOS vs Android）、網路型態（4G vs wifi vs 3G）、瀏覽器（Chrome vs Safari vs Firefox）。</p>
<p>切分後的資料能回答 server-side 回答不了的問題：「為什麼巴西的使用者比美國慢 3 倍？」（CDN 沒覆蓋巴西）、「為什麼 Safari 的 error rate 比 Chrome 高？」（某個 JS API 在 Safari 的行為不同）。</p>
<h3 id="取樣與成本">取樣與成本</h3>
<p>RUM 的事件量跟使用者流量成正比。高流量網站的 RUM 資料量可能很大（每秒數千筆 page view + error + resource timing），成本隨之上升。</p>
<p>RUM 的取樣策略跟 server-side trace sampling 類似：可以全收（低流量網站）、按比例取樣（高流量）、或按條件取樣（error 全收、正常 page view 取樣）。取樣後的資料仍能看到趨勢跟 percentile，但個別 session 的完整 replay 需要該 session 被取樣到。</p>
<h2 id="synthetic-monitoring">Synthetic Monitoring</h2>
<p>Synthetic monitoring 用自動化的 <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a> 從外部網路定期發起請求，測量 availability 跟 latency。跟 RUM 的差異是 synthetic 是主動探測（沒有真實使用者也能跑），能 24/7 持續監控。</p>
<h3 id="適用場景">適用場景</h3>
<p><strong>Availability 探測</strong>：每分鐘從多個地區對關鍵頁面或 API endpoint 發 request，確認可達性。DNS 異常、CDN 故障、TLS 過期 — 這些 server-side 看不到的問題，synthetic probe 能第一時間抓到。</p>
<p><strong>SLO probe</strong>：用 synthetic probe 量測關鍵 user journey 的端到端 latency（login → homepage → checkout），作為 SLO 的 client-side 量測點。</p>
<p><strong>Third-party 依賴監控</strong>：探測 payment gateway、SSO provider、CDN 的可用性。這些外部依賴故障時 server-side 只能看到 timeout 或 error code，synthetic probe 能從使用者的角度看到完整影響。</p>
<h3 id="常見陷阱">常見陷阱</h3>
<p>Synthetic probe 的探測路徑必須跟真實使用者一致。Probe 從 datacenter 內部發 request、走內部 DNS、不經過 CDN — 這種 probe 量到的 latency 跟 availability 不代表真實使用者的體驗。</p>
<p>Probe 應該從外部網路、經過公開 DNS、經過 CDN / edge、用真實 browser（headless Chrome）渲染頁面。Catchpoint、Pingdom、Datadog Synthetic 都提供從多個公開地理位置發 probe 的能力。</p>
<h2 id="core-web-vitals-與-backend-sli-的整合">Core Web Vitals 與 Backend SLI 的整合</h2>
<p>Core Web Vitals（LCP、CLS、INP）是 client-side 的使用者體驗指標。Backend SLI（availability、latency p99）是 server-side 的服務健康指標。兩者各自反映不同層面、需要整合看才能得到完整圖像。</p>
<p>整合方式是在 dashboard 上並排顯示：backend SLI panel 旁邊放 RUM 的 LCP / INP panel。當 backend latency 正常但 LCP 退化，問題在 frontend rendering 或 CDN；當 backend latency 升高且 LCP 同步退化，問題在 backend。</p>
<p><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 設計</a> 的 user-journey-centric SLI 應該同時考慮 server-side 跟 client-side 的量測點。只看 server-side 的 SLI 會低估使用者實際感知的延遲。</p>
<h2 id="client-trace-跟-server-trace-的串接">Client Trace 跟 Server Trace 的串接</h2>
<p>RUM SDK 跟 backend 的 trace 串接讓一個 user action 的完整路徑可追蹤 — 從 button click 到 browser 發 API request 到 backend 處理到 response rendering。</p>
<p>串接方式是 RUM SDK 在發起 API request 時注入 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> header（W3C <code>traceparent</code>）。Backend 的 trace instrumentation 提取 header、建立 child span。完整的 trace waterfall 從 browser span 開始、經過 backend span、到 database span。</p>
<p>串接的條件是 RUM SDK 跟 backend SDK 使用相同的 trace context format。OTel 生態（browser SDK + backend SDK）天然支援；混用 vendor 時需要確認 header format 一致。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>RUM</th>
          <th>Synthetic</th>
          <th>特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM</td>
          <td>有</td>
          <td>有</td>
          <td>跟 APM trace 整合、session replay</td>
      </tr>
      <tr>
          <td>Sentry</td>
          <td>有</td>
          <td>無</td>
          <td>Error tracking 為主、效能次之</td>
      </tr>
      <tr>
          <td>New Relic Browser</td>
          <td>有</td>
          <td>有</td>
          <td>全棧觀測整合</td>
      </tr>
      <tr>
          <td>Catchpoint</td>
          <td>無</td>
          <td>有</td>
          <td>Synthetic 專精、全球 probe 網路</td>
      </tr>
      <tr>
          <td>Pingdom</td>
          <td>無</td>
          <td>有</td>
          <td>簡單 availability probe</td>
      </tr>
      <tr>
          <td>Grafana Faro</td>
          <td>有</td>
          <td>無</td>
          <td>開源、Grafana 生態整合</td>
      </tr>
  </tbody>
</table>
<p>選擇要點：已有 APM vendor 的團隊優先用同 vendor 的 RUM（trace 串接最自然）。只需要 availability probe 的用 Pingdom 或 Synthetic 功能。需要 session replay（重現使用者操作序列）的選 Datadog RUM 或 Sentry。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 client-side monitoring 時，先看訊號是否代表真實使用者，再看 synthetic probe 是否覆蓋關鍵旅程。</p>
<p>重點訊號包括：</p>
<ul>
<li>RUM 是否能按地區、裝置、網路型態與瀏覽器切分</li>
<li>Synthetic probe 是否從外部網路與真實入口進入</li>
<li>Core Web Vitals 是否能和 backend SLI 並排比較</li>
<li>Client trace / session 是否能和 server trace 串接</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>使用者回報慢但 server-side latency 正常</li>
<li>CDN / edge 故障時內部 dashboard 全綠</li>
<li>行動弱網場景無 visibility、僅有 wifi 桌面端訊號</li>
<li>Synthetic probe 從 datacenter 內部跑、路徑跟真實使用者不同</li>
<li>客戶投訴定位耗時長、無 client 端 trace / RUM session</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO 只看 server 200 率</td>
          <td>CDN / DNS 故障時 SLO 一切正常</td>
          <td>加 synthetic probe 跟 RUM 作為 SLI 來源</td>
      </tr>
      <tr>
          <td>Synthetic probe 走內部網路</td>
          <td>Probe latency 跟真實使用者差距大</td>
          <td>Probe 從外部公開網路、經 DNS / CDN 路徑</td>
      </tr>
      <tr>
          <td>RUM 無取樣策略</td>
          <td>高流量時 RUM 成本失控</td>
          <td>按條件取樣（error 全收、正常取樣）</td>
      </tr>
      <tr>
          <td>Client trace 跟 server 斷裂</td>
          <td>看不到 browser → server 的完整路徑</td>
          <td>RUM SDK 注入 W3C trace context header</td>
      </tr>
      <tr>
          <td>只看 overall LCP</td>
          <td>全球平均看起來好但特定地區體驗極差</td>
          <td>按地區 / 裝置 / 網路切分 RUM 資料</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：user-journey-centric SLI 需要 client-side 量測點</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</a>：client trace 跟 server trace 的 context 串接</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：CDN / edge 配置變更影響 RUM 訊號</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a>：客戶感知影響量化</li>
<li><a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 模組</a>：非 server 端的監控體系設計</li>
<li><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24 Client-to-Server 觀測串接</a>：從 browser click 到 server span 的完整 trace 鏈路實作</li>
</ul>
]]></content:encoded></item><item><title>Cloud Monitoring Metrics Model 與 MQL</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-monitoring-mql/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-monitoring-mql/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a> 的 vendor deep article，深化 overview「Cloud Monitoring uptime checks / SLO」跟「OTLP integration」段。初次接觸 GCP 觀測的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>GCP 服務預設把 metrics 寫到 Cloud Monitoring，工程師打開 Metrics Explorer 就能看到 CPU、記憶體、request count。問題通常出在三個地方：GCP 內建 metrics 的 resource model 跟應用層的 business metrics 用不同語言描述同一件事，PromQL 使用者要重新學 MQL 語法，alerting policy 的 condition type 跟 notification channel 配置比預期複雜。理解 Cloud Monitoring 的 metrics model 才能避免 custom metrics 爆量、alert noise、跟 Prometheus 生態的銜接摩擦。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="monitored-resource-與-metric-descriptor">Monitored resource 與 metric descriptor&lt;/h3>
&lt;p>Cloud Monitoring 的資料模型有兩個軸：&lt;strong>monitored resource&lt;/strong> 描述「誰產生了這個 metric」，&lt;strong>metric descriptor&lt;/strong> 描述「這個 metric 量什麼」。&lt;/p>
&lt;p>Monitored resource 是 GCP 自動帶入的標籤集合。GKE pod 的 monitored resource type 是 &lt;code>k8s_pod&lt;/code>，帶 &lt;code>project_id&lt;/code>、&lt;code>location&lt;/code>、&lt;code>cluster_name&lt;/code>、&lt;code>namespace_name&lt;/code>、&lt;code>pod_name&lt;/code>。Cloud Run revision 是 &lt;code>cloud_run_revision&lt;/code>，帶 &lt;code>service_name&lt;/code>、&lt;code>revision_name&lt;/code>、&lt;code>location&lt;/code>。這層標籤不需要工程師手動設定，GCP agent 或 SDK 自動填入。&lt;/p>
&lt;p>Metric descriptor 定義 metric 的名稱、型別（GAUGE / DELTA / CUMULATIVE）、value type（INT64 / DOUBLE / DISTRIBUTION）與自訂 label。GCP 內建 metrics 用 &lt;code>compute.googleapis.com/instance/cpu/utilization&lt;/code> 這樣的命名空間格式；custom metrics 用 &lt;code>custom.googleapis.com/&amp;lt;your-name&amp;gt;&lt;/code> 或 &lt;code>workload.googleapis.com/&amp;lt;your-name&amp;gt;&lt;/code>（後者透過 OTel Collector 或 Managed Prometheus 寫入時使用）。&lt;/p>
&lt;p>兩個軸相乘就是 time series 的數量。Cardinality 管理在 GCP 上等同於控制 monitored resource × metric label 的組合數。GCP 對 custom metrics 有每個 project 的 time series 配額（預設 500 per metric descriptor、可申請提高），超過時寫入會被拒。&lt;/p>
&lt;h3 id="mql-vs-promql">MQL vs PromQL&lt;/h3>
&lt;p>Cloud Monitoring 有兩種查詢語言。MQL（Monitoring Query Language）是 GCP 自家設計的 pipeline 語法：&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">fetch k8s_container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">| metric &amp;#39;kubernetes.io/container/cpu/core_usage_time&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| align rate(1m)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">| every 1m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| group_by [resource.cluster_name, resource.namespace_name],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> [value_cpu_usage: aggregate(value.core_usage_time)]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PromQL 在 Cloud Monitoring 上也可用（透過 Managed Service for Prometheus）。兩者的核心差異：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a> 的 vendor deep article，深化 overview「Cloud Monitoring uptime checks / SLO」跟「OTLP integration」段。初次接觸 GCP 觀測的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>GCP 服務預設把 metrics 寫到 Cloud Monitoring，工程師打開 Metrics Explorer 就能看到 CPU、記憶體、request count。問題通常出在三個地方：GCP 內建 metrics 的 resource model 跟應用層的 business metrics 用不同語言描述同一件事，PromQL 使用者要重新學 MQL 語法，alerting policy 的 condition type 跟 notification channel 配置比預期複雜。理解 Cloud Monitoring 的 metrics model 才能避免 custom metrics 爆量、alert noise、跟 Prometheus 生態的銜接摩擦。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="monitored-resource-與-metric-descriptor">Monitored resource 與 metric descriptor</h3>
<p>Cloud Monitoring 的資料模型有兩個軸：<strong>monitored resource</strong> 描述「誰產生了這個 metric」，<strong>metric descriptor</strong> 描述「這個 metric 量什麼」。</p>
<p>Monitored resource 是 GCP 自動帶入的標籤集合。GKE pod 的 monitored resource type 是 <code>k8s_pod</code>，帶 <code>project_id</code>、<code>location</code>、<code>cluster_name</code>、<code>namespace_name</code>、<code>pod_name</code>。Cloud Run revision 是 <code>cloud_run_revision</code>，帶 <code>service_name</code>、<code>revision_name</code>、<code>location</code>。這層標籤不需要工程師手動設定，GCP agent 或 SDK 自動填入。</p>
<p>Metric descriptor 定義 metric 的名稱、型別（GAUGE / DELTA / CUMULATIVE）、value type（INT64 / DOUBLE / DISTRIBUTION）與自訂 label。GCP 內建 metrics 用 <code>compute.googleapis.com/instance/cpu/utilization</code> 這樣的命名空間格式；custom metrics 用 <code>custom.googleapis.com/&lt;your-name&gt;</code> 或 <code>workload.googleapis.com/&lt;your-name&gt;</code>（後者透過 OTel Collector 或 Managed Prometheus 寫入時使用）。</p>
<p>兩個軸相乘就是 time series 的數量。Cardinality 管理在 GCP 上等同於控制 monitored resource × metric label 的組合數。GCP 對 custom metrics 有每個 project 的 time series 配額（預設 500 per metric descriptor、可申請提高），超過時寫入會被拒。</p>
<h3 id="mql-vs-promql">MQL vs PromQL</h3>
<p>Cloud Monitoring 有兩種查詢語言。MQL（Monitoring Query Language）是 GCP 自家設計的 pipeline 語法：</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">fetch k8s_container
</span></span><span class="line"><span class="ln">2</span><span class="cl">| metric &#39;kubernetes.io/container/cpu/core_usage_time&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">| align rate(1m)
</span></span><span class="line"><span class="ln">4</span><span class="cl">| every 1m
</span></span><span class="line"><span class="ln">5</span><span class="cl">| group_by [resource.cluster_name, resource.namespace_name],
</span></span><span class="line"><span class="ln">6</span><span class="cl">    [value_cpu_usage: aggregate(value.core_usage_time)]</span></span></code></pre></div><p>PromQL 在 Cloud Monitoring 上也可用（透過 Managed Service for Prometheus）。兩者的核心差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>MQL</th>
          <th>PromQL（via Managed Prometheus）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料來源</td>
          <td>所有 Cloud Monitoring metrics</td>
          <td>透過 Managed Prometheus 寫入的 metrics</td>
      </tr>
      <tr>
          <td>查詢介面</td>
          <td>Metrics Explorer / alerting condition</td>
          <td>Grafana / Prometheus UI / API</td>
      </tr>
      <tr>
          <td>Aggregation 語法</td>
          <td>pipe-style <code>group_by</code></td>
          <td>函式風格 <code>sum by (label)</code></td>
      </tr>
      <tr>
          <td>跨 GCP 與 custom</td>
          <td>原生支援 GCP 內建 metrics</td>
          <td>需要轉成 Prometheus 格式</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>GCP-specific、不可搬到其他平台</td>
          <td>跨平台標準、可搬到 Mimir / Thanos</td>
      </tr>
  </tbody>
</table>
<p>選擇判讀：純 GCP 環境且團隊沒有 Prometheus 經驗 → MQL 起步快。已有 Prometheus / Grafana 生態 → 用 Managed Prometheus + PromQL、把 GCP 內建 metrics 透過 Prometheus-compatible exporter 導入。混合環境 → 兩者並存、GCP 原生 metrics 用 MQL 做 alerting、application metrics 用 PromQL 查詢。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="custom-metrics-設計與寫入">Custom metrics 設計與寫入</h3>
<p>Custom metrics 的常見路徑有三條：</p>
<p><strong>路徑一：Cloud Monitoring API 直接寫入</strong>。應用程式用 Cloud Monitoring client library 建立 metric descriptor 並寫入 time series。適合 GCP-native 應用，不需要額外 agent。</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">metric type: custom.googleapis.com/checkout/latency_ms
</span></span><span class="line"><span class="ln">2</span><span class="cl">kind: GAUGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">value type: DISTRIBUTION
</span></span><span class="line"><span class="ln">4</span><span class="cl">labels: [service, region, status_code]</span></span></code></pre></div><p><strong>路徑二：OTel Collector + GCP exporter</strong>。應用程式用 OTel SDK 產生 metrics，OTel Collector 透過 <code>googlecloud</code> exporter 寫到 Cloud Monitoring。Metrics 命名空間是 <code>workload.googleapis.com/</code>。適合已有 OTel instrumentation 的服務。</p>
<p><strong>路徑三：Managed Service for Prometheus</strong>。部署 GCP 的 Managed Prometheus collector（或自管 Prometheus + remote write），metrics 存在 GCP 託管的 Monarch backend。查詢用 PromQL。適合 Kubernetes 環境且團隊熟悉 Prometheus 生態。</p>
<p>三條路徑可以共存。選擇判讀：先看團隊的 metrics 生態是 GCP-native 還是 Prometheus-native，再看 multi-cloud 需求。Managed Prometheus 的優勢是 PromQL 可搬、劣勢是 GCP 內建 metrics 需要額外整合。</p>
<h3 id="alerting-policy-配置">Alerting policy 配置</h3>
<p>Cloud Monitoring alerting policy 由三部分組成：condition、notification channel、documentation。</p>
<p>Condition types：</p>
<ul>
<li><strong>Metric threshold</strong>：metric 超過閾值 N 分鐘。適合「error rate &gt; 1% 持續 5 分鐘」。</li>
<li><strong>Metric absence</strong>：metric 消失。適合偵測 scrape 斷裂或服務停擺。</li>
<li><strong>Forecasting</strong>：預測 metric 在 N 小時後超過閾值。適合 disk 滿、quota 耗盡。</li>
<li><strong>Process health</strong>：GCE instance 的 process 是否存活。</li>
<li><strong>Log-based</strong>：Cloud Logging 出現特定 pattern 時觸發。適合把 error log 轉成 alert。</li>
<li><strong>SLO burn rate</strong>：SLO 設定後、burn rate 超過閾值。對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn-rate</a> 概念。</li>
</ul>
<p>Notification channels：Email / PagerDuty / Slack / Pub/Sub / Webhook / SMS。Pub/Sub channel 適合接自定義 automation（收到 alert → trigger Cloud Function）。</p>
<p>Snooze 與 maintenance window：暫時抑制特定 alerting policy。部署期間或已知維護時使用。</p>
<h3 id="managed-prometheus-整合">Managed Prometheus 整合</h3>
<p>GCP Managed Service for Prometheus 的部署模式：</p>
<ul>
<li><strong>GKE 模式</strong>：啟用 GKE monitoring、Managed Prometheus collector 自動部署。不需要自管 Prometheus server。</li>
<li><strong>Remote write 模式</strong>：自管 Prometheus server + <code>remote_write</code> 到 GCP Monarch endpoint。保留本地查詢能力，同時長期儲存在 GCP。</li>
<li><strong>OTel Collector 模式</strong>：OTel Collector 用 <code>googlemanagedprometheus</code> exporter 寫到 Monarch。</li>
</ul>
<p>查詢端：用 GCP Console 的 PromQL UI、或部署 Grafana + GMP datasource。PromQL 功能子集支援良好（rate / histogram_quantile / aggregation），少數進階功能（subquery）有限制。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="custom-metric-配額用盡">Custom metric 配額用盡</h3>
<p><strong>觸發條件</strong>：custom metric descriptor 數量超過 project 配額（預設 500），或單一 metric descriptor 的 time series 數量超過配額。</p>
<p><strong>表現</strong>：API 回傳 429 或 quota exceeded error。新 time series 寫不進去，既有的不受影響。</p>
<p><strong>修復</strong>：清理不再使用的 metric descriptor（describe → delete）、合併語意重疊的 metrics、減少 label cardinality。GCP Console → IAM → Quotas 可以申請提高配額，但先確認是設計問題而非真的需要那麼多 series。</p>
<h3 id="alerting-policy-觸發延遲">Alerting policy 觸發延遲</h3>
<p><strong>觸發條件</strong>：alerting policy 使用的 metrics 的 alignment period 或 duration 設定過長。</p>
<p><strong>表現</strong>：異常已經發生 10 分鐘，alert 才觸發。原因是 Cloud Monitoring 的 evaluation cycle 跟 metrics ingestion delay 相加。GCP 內建 metrics 的 ingestion delay 約 1-3 分鐘；custom metrics 透過 API 寫入的 delay 約 10-30 秒。</p>
<p><strong>修復</strong>：把 condition 的 alignment period 設短（1 分鐘）、duration 設短（但太短會造成 flapping）。Log-based alerting condition 的 delay 通常比 metric-based 短（秒級 vs 分鐘級），緊急異常考慮用 log-based condition。</p>
<h3 id="managed-prometheus-查詢與自管-prometheus-結果不一致">Managed Prometheus 查詢與自管 Prometheus 結果不一致</h3>
<p><strong>觸發條件</strong>：同一個 PromQL query 在本地 Prometheus 跟 GMP 的結果不同。</p>
<p><strong>表現</strong>：dashboard 數字對不上、alert 觸發行為不一致。</p>
<p><strong>修復</strong>：先確認 remote write 是否有 sample drop（看 <code>prometheus_remote_storage_samples_failed_total</code>）。再確認 GMP 的 PromQL 子集限制（部分 subquery 語法不支援）。最後確認 metric naming：local Prometheus 的 metric name 跟 GMP 儲存後的 naming convention 可能有差異（加了 <code>__name__</code> prefix 或 resource label）。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Cloud Monitoring 的計費模型基於 <strong>ingested metrics volume</strong>（per million data points）。GCP 內建 metrics（agent metrics 除外）免費。Custom metrics 的前 150 MB per billing account 免費，超過後按 volume 計費。</p>
<p>成本治理的判讀：</p>
<ul>
<li>最大成本來源通常是高頻率的 custom metrics 或高 cardinality label</li>
<li>用 <code>monitoring.googleapis.com/billing/bytes_ingested</code> metric 追蹤 ingestion 量</li>
<li>減少 scrape interval（15s → 30s 或 60s）可以直接降低 ingestion 量</li>
<li>Managed Prometheus 的計費跟 custom metrics 分開計算（per samples ingested）</li>
</ul>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</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/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a>：SLO burn rate alert 的訊號設計</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>：Managed Prometheus 的上游概念</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：OTel Collector + GCP exporter 整合</li>
<li><a href="../cloud-logging-export-compliance/">Cloud Logging 查詢、匯出與合規</a>：同 vendor 的 logs 面</li>
</ul>
]]></content:encoded></item><item><title>CloudWatch Logs Insights 查詢與日誌治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/logs-insights-governance/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/logs-insights-governance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a> 的 vendor deep article，深化 overview「Logs Insights query」跟「Logs lifecycle」段。初次接觸 CloudWatch 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CloudWatch Logs 的成本模型跟 self-hosted log stack 不同 — ingestion、storage 跟 query 分開計費，每一層都有明確的 cost lever。理解 log group 設計、retention 設定與 subscription filter 的組合，才能在 AWS-native 環境下控制日誌成本而不犧牲事故判讀能力。&lt;/p>
&lt;h2 id="log-group-設計">Log group 設計&lt;/h2>
&lt;h3 id="拆分粒度">拆分粒度&lt;/h3>
&lt;p>Log group 是 CloudWatch Logs 的計費與 retention 邊界。同一個 log group 內的所有 log stream 共用 retention policy 和 access control（IAM resource policy）。&lt;/p>
&lt;p>合理的拆分粒度是 &lt;strong>一個服務一個 log group&lt;/strong>，而非一個帳號一個或一個 container 一個。服務級拆分讓 retention、查詢範圍與 IAM 權限自然對齊服務 ownership。&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>一個服務一個 log group&lt;/td>
 &lt;td>多數 production 服務&lt;/td>
 &lt;td>log group 數量增長需要 naming convention&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個環境一個 log group&lt;/td>
 &lt;td>非常小的團隊、staging/dev 環境&lt;/td>
 &lt;td>混合多個服務的日誌，查詢時需要額外 filter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個 Lambda function 一個 log group&lt;/td>
 &lt;td>Lambda 預設行為&lt;/td>
 &lt;td>Lambda 數量多時 log group 爆量，管理成本高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Lambda 的預設行為是每個 function 自動建一個 log group（&lt;code>/aws/lambda/&amp;lt;function-name&amp;gt;&lt;/code>）。function 數量超過數十個後，需要用 naming convention 加 tag 控制，否則 retention policy 難以統一套用。&lt;/p>
&lt;h3 id="naming-convention">Naming convention&lt;/h3>
&lt;p>推薦格式：&lt;code>/&amp;lt;environment&amp;gt;/&amp;lt;service&amp;gt;/&amp;lt;component&amp;gt;&lt;/code>，例如 &lt;code>/prod/checkout-api/app&lt;/code>、&lt;code>/prod/checkout-api/access-log&lt;/code>。統一前綴讓 Logs Insights 的 multi-log-group query 用 prefix matching 篩選。&lt;/p>
&lt;h2 id="logs-insights-查詢語法">Logs Insights 查詢語法&lt;/h2>
&lt;h3 id="核心語法">核心語法&lt;/h3>
&lt;p>Logs Insights 的查詢結構是 pipe-based：每行用 &lt;code>|&lt;/code> 分隔，依序處理。&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">fields @timestamp, @message, @logStream
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">| filter @message like /ERROR/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| parse @message &amp;#34;order_id=* status=*&amp;#34; as order_id, status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">| stats count(*) as error_count by status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| sort error_count desc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| limit 20&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常用 command 對照：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Command&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>注意事項&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>fields&lt;/code>&lt;/td>
 &lt;td>選擇要顯示的欄位&lt;/td>
 &lt;td>&lt;code>@timestamp&lt;/code>、&lt;code>@message&lt;/code> 是內建欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>filter&lt;/code>&lt;/td>
 &lt;td>條件篩選&lt;/td>
 &lt;td>支援 &lt;code>like /regex/&lt;/code>、&lt;code>=&lt;/code>、&lt;code>&amp;gt;&lt;/code>、&lt;code>in []&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>parse&lt;/code>&lt;/td>
 &lt;td>從非結構化 log 擷取欄位&lt;/td>
 &lt;td>glob pattern 用 &lt;code>*&lt;/code>、regex 用 &lt;code>/pattern/&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>stats&lt;/code>&lt;/td>
 &lt;td>聚合計算&lt;/td>
 &lt;td>&lt;code>count&lt;/code>、&lt;code>avg&lt;/code>、&lt;code>sum&lt;/code>、&lt;code>min&lt;/code>、&lt;code>max&lt;/code>、&lt;code>pct&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>sort&lt;/code>&lt;/td>
 &lt;td>排序&lt;/td>
 &lt;td>預設 &lt;code>@timestamp desc&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>display&lt;/code>&lt;/td>
 &lt;td>只顯示指定欄位（跟 &lt;code>fields&lt;/code> 互補）&lt;/td>
 &lt;td>用在 &lt;code>stats&lt;/code> 後只要看聚合結果&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="json-自動解析">JSON 自動解析&lt;/h3>
&lt;p>CloudWatch Logs 會自動辨識 JSON 格式的 log event。JSON 欄位用 dot notation 存取：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a> 的 vendor deep article，深化 overview「Logs Insights query」跟「Logs lifecycle」段。初次接觸 CloudWatch 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>CloudWatch Logs 的成本模型跟 self-hosted log stack 不同 — ingestion、storage 跟 query 分開計費，每一層都有明確的 cost lever。理解 log group 設計、retention 設定與 subscription filter 的組合，才能在 AWS-native 環境下控制日誌成本而不犧牲事故判讀能力。</p>
<h2 id="log-group-設計">Log group 設計</h2>
<h3 id="拆分粒度">拆分粒度</h3>
<p>Log group 是 CloudWatch Logs 的計費與 retention 邊界。同一個 log group 內的所有 log stream 共用 retention policy 和 access control（IAM resource policy）。</p>
<p>合理的拆分粒度是 <strong>一個服務一個 log group</strong>，而非一個帳號一個或一個 container 一個。服務級拆分讓 retention、查詢範圍與 IAM 權限自然對齊服務 ownership。</p>
<table>
  <thead>
      <tr>
          <th>拆分策略</th>
          <th>適合場景</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個服務一個 log group</td>
          <td>多數 production 服務</td>
          <td>log group 數量增長需要 naming convention</td>
      </tr>
      <tr>
          <td>一個環境一個 log group</td>
          <td>非常小的團隊、staging/dev 環境</td>
          <td>混合多個服務的日誌，查詢時需要額外 filter</td>
      </tr>
      <tr>
          <td>一個 Lambda function 一個 log group</td>
          <td>Lambda 預設行為</td>
          <td>Lambda 數量多時 log group 爆量，管理成本高</td>
      </tr>
  </tbody>
</table>
<p>Lambda 的預設行為是每個 function 自動建一個 log group（<code>/aws/lambda/&lt;function-name&gt;</code>）。function 數量超過數十個後，需要用 naming convention 加 tag 控制，否則 retention policy 難以統一套用。</p>
<h3 id="naming-convention">Naming convention</h3>
<p>推薦格式：<code>/&lt;environment&gt;/&lt;service&gt;/&lt;component&gt;</code>，例如 <code>/prod/checkout-api/app</code>、<code>/prod/checkout-api/access-log</code>。統一前綴讓 Logs Insights 的 multi-log-group query 用 prefix matching 篩選。</p>
<h2 id="logs-insights-查詢語法">Logs Insights 查詢語法</h2>
<h3 id="核心語法">核心語法</h3>
<p>Logs Insights 的查詢結構是 pipe-based：每行用 <code>|</code> 分隔，依序處理。</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">fields @timestamp, @message, @logStream
</span></span><span class="line"><span class="ln">2</span><span class="cl">| filter @message like /ERROR/
</span></span><span class="line"><span class="ln">3</span><span class="cl">| parse @message &#34;order_id=* status=*&#34; as order_id, status
</span></span><span class="line"><span class="ln">4</span><span class="cl">| stats count(*) as error_count by status
</span></span><span class="line"><span class="ln">5</span><span class="cl">| sort error_count desc
</span></span><span class="line"><span class="ln">6</span><span class="cl">| limit 20</span></span></code></pre></div><p>常用 command 對照：</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>用途</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fields</code></td>
          <td>選擇要顯示的欄位</td>
          <td><code>@timestamp</code>、<code>@message</code> 是內建欄位</td>
      </tr>
      <tr>
          <td><code>filter</code></td>
          <td>條件篩選</td>
          <td>支援 <code>like /regex/</code>、<code>=</code>、<code>&gt;</code>、<code>in []</code></td>
      </tr>
      <tr>
          <td><code>parse</code></td>
          <td>從非結構化 log 擷取欄位</td>
          <td>glob pattern 用 <code>*</code>、regex 用 <code>/pattern/</code></td>
      </tr>
      <tr>
          <td><code>stats</code></td>
          <td>聚合計算</td>
          <td><code>count</code>、<code>avg</code>、<code>sum</code>、<code>min</code>、<code>max</code>、<code>pct</code></td>
      </tr>
      <tr>
          <td><code>sort</code></td>
          <td>排序</td>
          <td>預設 <code>@timestamp desc</code></td>
      </tr>
      <tr>
          <td><code>display</code></td>
          <td>只顯示指定欄位（跟 <code>fields</code> 互補）</td>
          <td>用在 <code>stats</code> 後只要看聚合結果</td>
      </tr>
  </tbody>
</table>
<h3 id="json-自動解析">JSON 自動解析</h3>
<p>CloudWatch Logs 會自動辨識 JSON 格式的 log event。JSON 欄位用 dot notation 存取：</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">fields @timestamp, requestId, level, message
</span></span><span class="line"><span class="ln">2</span><span class="cl">| filter level = &#34;ERROR&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">| stats count(*) by bin(5m)</span></span></code></pre></div><p>如果 log 是 JSON 格式，<code>parse</code> 通常不需要 — 直接用欄位名稱。混合格式（部分 JSON、部分 plain text）時，需要用 <code>isPresent()</code> 判斷欄位是否存在。</p>
<h3 id="效能考量">效能考量</h3>
<p>Logs Insights 的查詢成本按掃描的 data 量計費（每 GB scanned），不按結果數。減少掃描量的方式：</p>
<ul>
<li>縮短時間範圍：事故判讀先查最近 30 分鐘，確認 pattern 後再擴大</li>
<li>指定 log group：避免對所有 log group 做全域查詢</li>
<li>用 <code>limit</code> 限制結果集大小（不影響掃描量，但減少資料傳輸）</li>
</ul>
<p>跨 log group 查詢最多同時查 50 個 log group。超過時需要拆成多次查詢或用 subscription filter 把資料匯到集中儲存。</p>
<h2 id="retention-policy">Retention policy</h2>
<h3 id="設定方式">設定方式</h3>
<p>Retention policy 在 log group 級別設定。每個 log group 可以獨立選擇 1 天到 10 年、或永不過期。</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 logs put-retention-policy <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --log-group-name /prod/checkout-api/app <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --retention-in-days <span class="m">30</span></span></span></code></pre></div><p>常見 retention 策略按服務性質分：</p>
<table>
  <thead>
      <tr>
          <th>服務類型</th>
          <th>建議 retention</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心交易路徑（checkout、payment）</td>
          <td>90-365 天</td>
          <td>事故回溯、合規稽核</td>
      </tr>
      <tr>
          <td>一般 API 服務</td>
          <td>30-90 天</td>
          <td>事故回溯足夠，cost 可控</td>
      </tr>
      <tr>
          <td>Background job / worker</td>
          <td>14-30 天</td>
          <td>失敗時看最近數天即可</td>
      </tr>
      <tr>
          <td>Lambda / short-lived function</td>
          <td>7-14 天</td>
          <td>高量低價值，過期快速清理</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>365 天以上或永不過期</td>
          <td>法規要求，見 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a></td>
      </tr>
  </tbody>
</table>
<p>未設定 retention 的 log group 預設永不過期 — 這是 CloudWatch 日誌成本超支的常見原因。新 log group 建立後應立即設定 retention。</p>
<h3 id="fintech-合規場景的-log-group-分離">FinTech 合規場景的 log group 分離</h3>
<p><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">FinTech 審計證據案例</a>揭露一個常見問題：audit log 跟 operational log 混在同一個 log group，retention 只能統一設定。結果要嘛 operational log 為了合規被迫留太久（成本浪費）、要嘛 audit log 跟著 operational log 的短 retention 被刪掉（合規風險）。</p>
<p>CloudWatch 的 log group 設計天然支援這種分離 — audit log 跟 operational log 用不同 log group、各自設定 retention：</p>
<table>
  <thead>
      <tr>
          <th>Log 類型</th>
          <th>Log group 命名</th>
          <th>Retention</th>
          <th>Log class</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易 audit log</td>
          <td><code>/prod/checkout-api/audit</code></td>
          <td>2555 天（7 年）</td>
          <td>Infrequent Access</td>
      </tr>
      <tr>
          <td>Application operational log</td>
          <td><code>/prod/checkout-api/app</code></td>
          <td>30 天</td>
          <td>Standard</td>
      </tr>
      <tr>
          <td>Access log（ALB / API Gateway）</td>
          <td><code>/prod/checkout-api/access</code></td>
          <td>90 天</td>
          <td>Standard</td>
      </tr>
  </tbody>
</table>
<p>Audit log group 的額外治理：</p>
<ul>
<li><strong>IAM 權限分離</strong>：audit log group 的讀取權限（<code>logs:GetLogEvents</code>）限縮到 compliance team 跟 security team，application developer 只能讀 operational log group。避免 audit log 被隨意查詢或汙染</li>
<li><strong>Immutability</strong>：CloudWatch Logs 本身不支援 WORM（write once read many），合規要求 immutable 存檔時用 subscription filter 把 audit log 同步送到 S3 + Object Lock</li>
<li><strong>Cross-account 集中</strong>：audit log 的 cross-account aggregation（見下方段落）的 IAM 權限要比 operational log 嚴格 — aggregated sink 的 destination 只能由 security team 控制</li>
</ul>
<h3 id="infrequent-access-log-class">Infrequent Access log class</h3>
<p>CloudWatch Logs 提供兩種 log class：<strong>Standard</strong>（完整查詢、即時 subscription filter、metric filter）跟 <strong>Infrequent Access</strong>（僅支援 Logs Insights 查詢、不支援即時 subscription filter 跟 metric filter、ingestion 成本約降 50%）。</p>
<p>Audit log 的存取模式通常是「寫入頻繁、查詢極少（只在稽核或事故時才查）」— 正好符合 Infrequent Access 的定位。把 7 年 retention 的 audit log group 設成 Infrequent Access，ingestion 成本直接砍半。</p>
<p>注意 Infrequent Access 的限制：不能用 subscription filter 即時轉發到 Lambda 或 Kinesis，不能用 metric filter 從 log 產生 CloudWatch metric。如果 audit log 需要即時異常偵測（例如偵測大量失敗交易），要用 Standard class + subscription filter 做即時處理、再用 Lambda 寫到長期 audit log group（Infrequent Access）。</p>
<h3 id="自動化套用">自動化套用</h3>
<p>用 AWS Config rule 或 CloudFormation / CDK 的 log group 定義統一設定 retention。Lambda function 自動建立的 log group 不會自動套用 retention，需要額外自動化（Lambda post-hook 或 EventBridge rule + Lambda 設定 retention）。</p>
<h2 id="cross-account-log-aggregation">Cross-account log aggregation</h2>
<h3 id="架構模式">架構模式</h3>
<p>多帳號環境下，常見做法是設立一個「觀測帳號」（observability account），把其他帳號的 logs 匯入。</p>
<p>兩種匯入方式：</p>
<p><strong>Subscription filter + Kinesis Data Firehose</strong>：每個 source 帳號的 log group 設 subscription filter，把 log event 送到 observability 帳號的 Kinesis Data Firehose，再寫到 S3 或 OpenSearch。適合需要長期存檔或進階查詢的場景。</p>
<p><strong>CloudWatch cross-account observability</strong>：AWS 原生功能，在 monitoring account 直接查詢 source accounts 的 CloudWatch 資料（metrics、logs、traces）。設定較簡單，但查詢延遲較高，且 Logs Insights 的 cross-account 查詢有 region 限制。</p>
<table>
  <thead>
      <tr>
          <th>匯入方式</th>
          <th>適合場景</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Subscription filter + Firehose</td>
          <td>需要 S3 archive、OpenSearch 全文搜尋、離線分析</td>
          <td>每個 log group 最多 2 個 subscription filter</td>
      </tr>
      <tr>
          <td>Cross-account observability</td>
          <td>只需要 CloudWatch console 統一查詢</td>
          <td>同 region 限制、查詢延遲較高</td>
      </tr>
  </tbody>
</table>
<h3 id="subscription-filter-實務">Subscription filter 實務</h3>
<p>Subscription filter 可以把 log event 送到 Lambda（即時處理）、Kinesis Data Stream（緩衝）、Kinesis Data Firehose（直接寫 S3/OpenSearch）或另一個 log group。</p>
<p>每個 log group 最多 2 個 subscription filter — 這是硬限制。如果同一個 log group 需要同時送 S3 archive 跟即時 alerting，要用 Kinesis Data Stream 做 fan-out，讓 stream 下游各自消費。</p>
<p>filter pattern 語法支援 JSON 欄位匹配：</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">{ $.level = &#34;ERROR&#34; }</span></span></code></pre></div><p>只把 ERROR 級別的 log 送到 alerting pipeline，可以大幅降低下游處理量跟成本。</p>
<h2 id="cost-governance">Cost governance</h2>
<h3 id="計費結構">計費結構</h3>
<p>CloudWatch Logs 的成本由三個維度組成：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>計費方式</th>
          <th>常見比例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion</td>
          <td>每 GB ingested</td>
          <td>通常佔 50-70%</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>每 GB-month stored</td>
          <td>通常佔 20-40%</td>
      </tr>
      <tr>
          <td>Query（Logs Insights）</td>
          <td>每 GB scanned</td>
          <td>通常佔 5-15%</td>
      </tr>
  </tbody>
</table>
<p>Ingestion 是最大成本。降低 ingestion 的手段：</p>
<ul>
<li><strong>調整 log level</strong>：production 只保留 INFO 以上，DEBUG 只在問題排查時短暫開啟</li>
<li><strong>去除重複資訊</strong>：access log 跟 application log 不要記錄相同欄位</li>
<li><strong>用 metric filter 替代 log query</strong>：高頻計數（error count、request count）用 CloudWatch Metric Filter 從 log 產生 metric，查詢成本從 log scan 轉成 metric query</li>
</ul>
<h3 id="成本觀測">成本觀測</h3>
<p>用 CloudWatch 自己的 metric 觀測 log 成本：</p>
<ul>
<li><code>IncomingBytes</code>（per log group）：監控哪個 log group ingestion 最大</li>
<li><code>IncomingLogEvents</code>（per log group）：監控 event 數量</li>
<li>AWS Cost Explorer 按 CloudWatch 拆分：看 log ingestion vs storage vs API call 的比例</li>
</ul>
<h3 id="降本決策樹">降本決策樹</h3>
<p>判斷成本是否合理的順序：</p>
<ol>
<li>最大 ingestion 的 log group 是哪個？是否合理（核心服務的 access log 量大是正常的）</li>
<li>Retention 是否都有設定？未設定的 log group 會持續累積 storage 成本</li>
<li>是否有 DEBUG 級別 log 在 production 長期開啟？</li>
<li>是否有 subscription filter 把全量 log 送到外部？能否加 filter pattern 只送需要的部分</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>觀測管線整合：CloudWatch Logs → Subscription Filter → Kinesis Firehose → S3 / OpenSearch，見 <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></li>
<li>Audit log 治理：合規場景的 log retention 跟 access control，見 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a></li>
<li>Evidence package：把 Logs Insights query link 跟時間窗放進 evidence，見 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>OTel 整合：ADOT 可以把 log 送到 CloudWatch Logs 或其他 backend，見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</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>High-Cardinality Query Model 與 BubbleUp</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/high-cardinality-query-bubbleup/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/high-cardinality-query-bubbleup/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a> 的 vendor deep article，深化 overview「BubbleUp 分析」跟「Events vs metrics 心智模型」段。初次接觸 Honeycomb 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Metrics-based 觀測系統有一個結構性限制：metric 在寫入前就做了 aggregation，之後只能沿著預先定義的 label 維度查詢。當事故需要按 user_id、request_id、feature_flag_variant 或 deployment_version 定位時，metrics 系統要嘛沒有這些維度（label cardinality 會爆），要嘛需要事先知道要看哪個維度（但事故通常是 unknown-unknowns）。&lt;/p>
&lt;p>Honeycomb 用 event-based 模型解決這個問題 — 每一筆 event（通常是一個 trace span）帶幾十個 attribute，查詢時才決定 group by 哪些維度。BubbleUp 進一步自動找出區隔 outlier 跟 baseline 的 attribute，讓工程師不需要事先猜測問題維度。&lt;/p>
&lt;p>理解 Honeycomb 的資料模型、查詢設計跟 BubbleUp 的工作方式，才能判斷什麼場景下 Honeycomb 比 metrics-first 系統更有效、什麼場景下 metrics-first 仍然是對的選擇。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="event-based-資料模型">Event-based 資料模型&lt;/h3>
&lt;p>Honeycomb 的儲存引擎是 column store — 每一筆 event 是一列、每一個 attribute 是一欄。寫入時不做 aggregation，查詢時才 group by / filter / aggregate。&lt;/p>
&lt;p>跟 metrics-first 系統的根本差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Metrics-first（Prometheus）&lt;/th>
 &lt;th>Event-based（Honeycomb）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫入時&lt;/td>
 &lt;td>按 label 組合 aggregate 成 time series&lt;/td>
 &lt;td>存原始 event、帶所有 attribute&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢時&lt;/td>
 &lt;td>只能沿既有 label 維度查詢&lt;/td>
 &lt;td>任意 attribute 組合 group by&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cardinality&lt;/td>
 &lt;td>label 組合數 = time series 數、有上限&lt;/td>
 &lt;td>Attribute 組合數不影響儲存結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本模型&lt;/td>
 &lt;td>按 time series 數計費&lt;/td>
 &lt;td>按 events volume 計費&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合&lt;/td>
 &lt;td>已知維度的趨勢監控&lt;/td>
 &lt;td>unknown-unknowns 的事故偵錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>一筆 checkout event 在 Honeycomb 可能帶 30+ 個 attribute：service.name、http.method、http.status_code、http.url、user_id、tenant_id、region、deployment_version、feature_flag.variant、db.duration_ms、cache.hit、payment.provider、error.message 等。在 Prometheus 上，user_id 跟 tenant_id 是不能當 label 的（cardinality 爆）；在 Honeycomb 上，它們只是多一欄。&lt;/p>
&lt;h3 id="bubbleup-的工作方式">BubbleUp 的工作方式&lt;/h3>
&lt;p>BubbleUp 是 Honeycomb 的自動異常歸因功能。操作流程：&lt;/p>
&lt;ol>
&lt;li>在 heatmap 上框選異常區域（例如 latency spike 的時間段跟數值範圍）&lt;/li>
&lt;li>BubbleUp 把框選區域的 events（outlier set）跟框外 events（baseline set）做統計比較&lt;/li>
&lt;li>對每一個 attribute，計算兩組 events 的分布差異（Honeycomb 使用 distribution divergence 量度）&lt;/li>
&lt;li>排序差異最大的 attribute 顯示在面板上&lt;/li>
&lt;/ol>
&lt;p>BubbleUp 的價值在於它跳過了「猜測哪個維度有問題」的步驟。傳統 metrics dashboarding 需要工程師先想到「可能是某個 region 的問題」→ 加 region filter → 確認。BubbleUp 直接告訴你「outlier set 跟 baseline set 在 region、deployment_version、payment.provider 三個維度上分布最不同」。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a> 的 vendor deep article，深化 overview「BubbleUp 分析」跟「Events vs metrics 心智模型」段。初次接觸 Honeycomb 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Metrics-based 觀測系統有一個結構性限制：metric 在寫入前就做了 aggregation，之後只能沿著預先定義的 label 維度查詢。當事故需要按 user_id、request_id、feature_flag_variant 或 deployment_version 定位時，metrics 系統要嘛沒有這些維度（label cardinality 會爆），要嘛需要事先知道要看哪個維度（但事故通常是 unknown-unknowns）。</p>
<p>Honeycomb 用 event-based 模型解決這個問題 — 每一筆 event（通常是一個 trace span）帶幾十個 attribute，查詢時才決定 group by 哪些維度。BubbleUp 進一步自動找出區隔 outlier 跟 baseline 的 attribute，讓工程師不需要事先猜測問題維度。</p>
<p>理解 Honeycomb 的資料模型、查詢設計跟 BubbleUp 的工作方式，才能判斷什麼場景下 Honeycomb 比 metrics-first 系統更有效、什麼場景下 metrics-first 仍然是對的選擇。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="event-based-資料模型">Event-based 資料模型</h3>
<p>Honeycomb 的儲存引擎是 column store — 每一筆 event 是一列、每一個 attribute 是一欄。寫入時不做 aggregation，查詢時才 group by / filter / aggregate。</p>
<p>跟 metrics-first 系統的根本差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Metrics-first（Prometheus）</th>
          <th>Event-based（Honeycomb）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入時</td>
          <td>按 label 組合 aggregate 成 time series</td>
          <td>存原始 event、帶所有 attribute</td>
      </tr>
      <tr>
          <td>查詢時</td>
          <td>只能沿既有 label 維度查詢</td>
          <td>任意 attribute 組合 group by</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>label 組合數 = time series 數、有上限</td>
          <td>Attribute 組合數不影響儲存結構</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>按 time series 數計費</td>
          <td>按 events volume 計費</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>已知維度的趨勢監控</td>
          <td>unknown-unknowns 的事故偵錯</td>
      </tr>
  </tbody>
</table>
<p>一筆 checkout event 在 Honeycomb 可能帶 30+ 個 attribute：service.name、http.method、http.status_code、http.url、user_id、tenant_id、region、deployment_version、feature_flag.variant、db.duration_ms、cache.hit、payment.provider、error.message 等。在 Prometheus 上，user_id 跟 tenant_id 是不能當 label 的（cardinality 爆）；在 Honeycomb 上，它們只是多一欄。</p>
<h3 id="bubbleup-的工作方式">BubbleUp 的工作方式</h3>
<p>BubbleUp 是 Honeycomb 的自動異常歸因功能。操作流程：</p>
<ol>
<li>在 heatmap 上框選異常區域（例如 latency spike 的時間段跟數值範圍）</li>
<li>BubbleUp 把框選區域的 events（outlier set）跟框外 events（baseline set）做統計比較</li>
<li>對每一個 attribute，計算兩組 events 的分布差異（Honeycomb 使用 distribution divergence 量度）</li>
<li>排序差異最大的 attribute 顯示在面板上</li>
</ol>
<p>BubbleUp 的價值在於它跳過了「猜測哪個維度有問題」的步驟。傳統 metrics dashboarding 需要工程師先想到「可能是某個 region 的問題」→ 加 region filter → 確認。BubbleUp 直接告訴你「outlier set 跟 baseline set 在 region、deployment_version、payment.provider 三個維度上分布最不同」。</p>
<p>BubbleUp 的限制：它需要足夠的 event 量才能做統計比較。低 QPS 服務（&lt; 1 event/sec）在短時間窗內可能沒有足夠的 outlier events。它也不處理因果關係 — 分布差異最大的 attribute 不一定是 root cause，可能是 correlated symptom。</p>
<h3 id="slo-與-burn-rate-alert">SLO 與 Burn Rate Alert</h3>
<p>Honeycomb 的 SLO 功能把 service-level indicator 定義成一個 query、目標成功率定義成 SLO threshold、窗口跟 burn rate 用來觸發 alert。</p>
<p>SLO 設定要素：</p>
<ul>
<li><strong>SLI query</strong>：定義「成功」的條件。例如 <code>WHERE duration_ms &lt; 500 AND http.status_code &lt; 500</code>。</li>
<li><strong>SLO target</strong>：例如 99.9%。</li>
<li><strong>Window</strong>：通常 30 天 rolling window。</li>
<li><strong>Burn rate alert</strong>：multi-window multi-burn-rate。1 小時窗口看快速 burn（14.4x burn rate）、6 小時窗口看中速 burn（6x）、3 天窗口看慢速 burn（1x）。</li>
</ul>
<p>跟 Prometheus-based SLO 的差異：Prometheus SLO 通常用 recording rule 預先計算 error budget remaining，alert 基於 recording rule 結果。Honeycomb SLO 直接在 event 上做即時計算，不需要 recording rule。代價是 Honeycomb 的 SLO 計算跟平台綁定、不可搬。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn-rate</a> 概念跟 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a> 的訊號設計。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="derived-columns">Derived Columns</h3>
<p>Derived columns 是在 Honeycomb 查詢層建立的計算欄位，不改變原始 event。</p>
<p>常用場景：</p>
<ul>
<li><strong>Duration bucket</strong>：<code>IF(LTE($duration_ms, 100), &quot;fast&quot;, IF(LTE($duration_ms, 500), &quot;normal&quot;, &quot;slow&quot;))</code> — 把連續數值轉成 category、方便 group by</li>
<li><strong>Error classification</strong>：<code>IF(GTE($http.status_code, 500), &quot;server_error&quot;, IF(GTE($http.status_code, 400), &quot;client_error&quot;, &quot;ok&quot;))</code> — 對 status code 做語意分類</li>
<li><strong>Feature flag analysis</strong>：<code>CONCAT($service.name, &quot;-&quot;, $feature_flag.variant)</code> — 組合 attribute 做 A/B 比較</li>
</ul>
<p>Derived columns 的效能影響：它們在查詢時計算，不佔 ingestion 或 storage。但複雜的 derived column expression 會增加查詢 latency。</p>
<h3 id="dataset-設計">Dataset 設計</h3>
<p>Honeycomb 的 dataset 是資料隔離的單位。設計決策：</p>
<p><strong>Option A：per-environment dataset</strong>（production / staging / dev 各自獨立）。優點是查詢預設在單一環境、不需要每次加 environment filter。缺點是跨環境比較需要切換 dataset。</p>
<p><strong>Option B：per-service dataset</strong>（checkout-api / payment-adapter / notification-service 各自獨立）。優點是單一服務的查詢效能好（資料量小）。缺點是跨服務 trace 需要用 trace view 跨 dataset 查。</p>
<p><strong>Option C：single dataset per environment</strong>（production 一個大 dataset、所有服務混在一起）。優點是跨服務查詢不需切換、BubbleUp 能跨服務比較。缺點是資料量大、查詢稍慢、不同服務的 attribute 不一致可能造成混淆。</p>
<p>Honeycomb 推薦 Option C — 把同一環境的所有服務放同一個 dataset。理由是 BubbleUp 跟 trace view 的跨服務能力是 Honeycomb 的核心價值，拆太細會削弱這個優勢。用 <code>service.name</code> attribute 做 per-service filter。</p>
<h3 id="otlp-ingestion">OTLP Ingestion</h3>
<p>Honeycomb 原生接受 OTLP（gRPC 跟 HTTP）。應用程式用 OTel SDK 產生 traces / logs、設定 OTLP endpoint 為 <code>api.honeycomb.io:443</code>、帶 API key header。</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"># OTel Collector config example
</span></span><span class="line"><span class="ln">2</span><span class="cl">exporters:
</span></span><span class="line"><span class="ln">3</span><span class="cl">  otlp:
</span></span><span class="line"><span class="ln">4</span><span class="cl">    endpoint: &#34;api.honeycomb.io:443&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">    headers:
</span></span><span class="line"><span class="ln">6</span><span class="cl">      &#34;x-honeycomb-team&#34;: &#34;${HONEYCOMB_API_KEY}&#34;
</span></span><span class="line"><span class="ln">7</span><span class="cl">      &#34;x-honeycomb-dataset&#34;: &#34;production&#34;</span></span></code></pre></div><p>OTel SDK 跟 Honeycomb Beeline SDK 的選擇：新部署一律用 OTel SDK — vendor neutral、可搬。Beeline SDK 是 Honeycomb-specific，已進入維護模式。既有 Beeline 部署可以逐步遷移到 OTel SDK。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="sampling-不足導致成本失控">Sampling 不足導致成本失控</h3>
<p><strong>觸發條件</strong>：高 QPS 服務（&gt; 10K req/sec）不做 sampling、全量送 Honeycomb。</p>
<p><strong>表現</strong>：月帳單高於預期。Honeycomb 按 events volume 計費、高 QPS 服務全量 ingestion 的成本可能是 Prometheus 的數倍。</p>
<p><strong>修復</strong>：部署 Refinery（Honeycomb 的 tail-based sampling proxy）。Refinery 在 trace 完成後決定是否保留 — 保留所有 error trace、保留所有高 latency trace、對正常 trace 做 sampling（例如保留 10%）。Dynamic sampling 根據 traffic pattern 自動調整 sampling rate。</p>
<p>成本與可見度的取捨：1% sampling 意味著 99% 的正常 event 看不到。如果需要回答「過去一小時有多少 successful request」這種 count 問題，sampling 會引入統計誤差。Honeycomb 支援 sample rate annotation — query 結果會用 sample rate 做加權還原。</p>
<h3 id="bubbleup-結果不可行動">BubbleUp 結果不可行動</h3>
<p><strong>觸發條件</strong>：BubbleUp 顯示差異最大的 attribute 是「timestamp」或「trace_id」— 這些 attribute 天然在 outlier set 跟 baseline set 之間分布不同，不提供歸因資訊。</p>
<p><strong>修復</strong>：在 BubbleUp 設定中排除 high-entropy attribute（trace_id、span_id、timestamp）。Honeycomb 允許設定 BubbleUp 的 ignore list。另外確保 event 帶足夠的 business-context attribute — 如果 event 只有 infra-level attribute（CPU、memory），BubbleUp 能找到的 insight 有限。</p>
<h3 id="gaming-高峰的-cardinality-情境">Gaming 高峰的 cardinality 情境</h3>
<p><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">Gaming 案例</a>揭露了 metrics-first 跟 event-first 系統在高峰期的根本差異。線上遊戲的賽季開跑或限時活動會讓流量在 30 分鐘內暴增 10 倍，同時 per-player、per-match-id 的 label 組合讓 Prometheus 的 active series 從 50 萬爆到 500 萬。</p>
<p>Prometheus 在這個場景的痛點不只是容量 — 而是 cardinality 爆炸改變了系統行為：scrape 變慢導致 metric freshness 從 15 秒退化到數分鐘、recording rule evaluation 跟不上 interval、alert 基於過期數據判斷。修法是 drop per-player label 或做 pre-aggregation、但 drop 掉之後事故時就查不到「哪個玩家的 session 異常」。</p>
<p>Honeycomb 的 event model 在這個場景天然有優勢 — per-player、per-match 是 event 上的 attribute，不產生 series、不影響 ingestion 效能。活動開跑時 event volume 暴增，但 Honeycomb 的 column store 只是行數增加、查詢的 IO 成本線性增長而非指數。BubbleUp 可以在高峰期直接找出「哪些 player_region × match_type 的組合延遲最高」。</p>
<p>代價是成本 — 10 倍的流量意味著 10 倍的 events volume、10 倍的計費。Gaming 場景通常需要搭配動態 sampling：正常 gameplay event 做 1:100 sampling、error 跟 high-latency event 全量保留。Refinery 的 tail-based sampling 在這裡是必備元件。</p>
<h3 id="honeycomb-vs-prometheus-的共存">Honeycomb vs Prometheus 的共存</h3>
<p>Honeycomb 不取代 Prometheus — 兩者解決不同問題。Prometheus 適合已知維度的趨勢監控（error rate dashboard、capacity trending、SLO burn rate），Honeycomb 適合 unknown-unknowns 的事故偵錯。</p>
<p>共存模式：application 用 OTel SDK 同時產生 metrics（→ Prometheus）跟 traces（→ Honeycomb）。Alerting 在 Prometheus 側（因為 metrics aggregation 穩定且成本低），深度偵錯在 Honeycomb 側。</p>
<h3 id="雙工具成本治理模式">雙工具成本治理模式</h3>
<p><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">觀測成本治理案例</a>提出一個在中大型團隊反覆驗證的分工：Prometheus 負責 golden signals（低 cardinality、固定 recording rules、成本可預測），Honeycomb 負責 high-cardinality debug（按需查詢、pay per event）。</p>
<p>這個分工的成本結構：Prometheus 的成本隨 active series 數量增長（cardinality-driven）、Honeycomb 的成本隨 event volume 增長（traffic-driven）。兩者的成本 driver 不同、scaling curve 不同 — Prometheus 在 series 爆炸時成本失控、Honeycomb 在 QPS 暴增時成本失控。把兩者放在一起、用各自的成本 sweet spot 互補、比只買一家更能控制總成本。</p>
<p>判讀自己是否需要雙工具的訊號：Prometheus dashboard 已經穩定、但事故時仍需要 20+ 分鐘才能定位到具體 user / request / deployment_version — 這 20 分鐘就是 Honeycomb 的價值。如果事故定位都能在 5 分鐘內靠 Prometheus label 完成，不需要加 Honeycomb。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Honeycomb 的計費基於 <strong>events volume</strong>（per million events ingested per month）。Event 的大小（attribute 數量）不直接影響計費（目前模型按 event 筆數、不按 payload size）。</p>
<p>成本治理手段：</p>
<ul>
<li><strong>Sampling</strong>：最直接。10% sampling = 成本降 90%。用 Refinery 做 tail-based sampling 保留重要 trace。</li>
<li><strong>Attribute 精簡</strong>：減少不需要的 attribute 不直接降成本（按筆數計費），但能加快查詢。</li>
<li><strong>Dataset 合併</strong>：多個小 dataset 合併成一個不影響成本，但能改善 BubbleUp 的統計品質。</li>
<li><strong>Team plan vs Enterprise</strong>：不同 plan 的 retention 跟 query 配額不同。</li>
</ul>
<p>跟 Prometheus 的成本比較：Prometheus 按 time series 數量計（self-host 的話是 infra 成本），Honeycomb 按 event 數量計。高 QPS + 低 cardinality 場景、Prometheus 成本優勢明顯。高 cardinality + 需要深度偵錯場景、Honeycomb 的 event cost 換到的是 BubbleUp 跟 arbitrary group by 的能力。</p>
<h3 id="不同規模的成本形態">不同規模的成本形態</h3>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>月 event 量</th>
          <th>預估月成本範圍</th>
          <th>成本治理重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>小型（1-5 服務、&lt; 1K QPS）</td>
          <td>&lt; 50M events</td>
          <td>Free tier 或低帳單</td>
          <td>不需特別治理</td>
      </tr>
      <tr>
          <td>中型（10-30 服務、1-10K QPS）</td>
          <td>50M-500M events</td>
          <td>中等（依 plan）</td>
          <td>Refinery sampling 開始有 ROI</td>
      </tr>
      <tr>
          <td>大型（50+ 服務、10K+ QPS）</td>
          <td>1B+ events</td>
          <td>高（需要 Enterprise plan）</td>
          <td>Refinery + 動態 sampling 必備、跟 Prometheus 分工控制總成本</td>
      </tr>
  </tbody>
</table>
<p>大型場景的成本治理核心是 sampling 策略 — 全量 ingestion 的成本通常不可接受。Refinery 的 tail-based sampling 讓 error trace 跟 high-latency trace 全量保留、normal trace 做 1:10 到 1:100 sampling。Sampling rate 的選擇取決於「事故時需要多少正常 trace 做 baseline 比對」— BubbleUp 需要足夠的 baseline events 才能計算分布差異，sampling 太激進會讓 BubbleUp 的統計品質下降。</p>
<p>經驗值：保留至少 5-10% 的正常 trace、同時全量保留所有 error / slow trace。在 Gaming 案例的高峰期，正常 trace 的 sampling 可以暫時降到 1%（高峰流量 10 倍、1% sampling 仍有大量 baseline events），高峰結束後恢復到 10%。動態 sampling 根據當前 QPS 自動調整 — Refinery 的 <code>DynamicSampler</code> 會根據 key field（service.name + http.status_code）的分布自動決定 sample rate。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁</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 在 metrics-first 跟 event-first 系統的不同治理策略</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a>：SLO / burn rate 的訊號設計</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：OTLP ingestion 的上游標準</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>：共存模式中的 metrics 面</li>
<li><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a>：high-cardinality 場景的案例回寫</li>
</ul>
]]></content:encoded></item><item><title>Index Lifecycle Management 與 Log Pipeline</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a> 的 vendor deep article，深化 overview「Index Lifecycle Management」跟「採集 pipeline」段。初次接觸 Elastic 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Elastic Stack 部署後，工程師通常能快速搜尋到 log。問題出在規模成長後：index 數量膨脹導致 cluster 效能退化、disk 滿了才發現沒有 lifecycle policy、shard 太小或太大造成查詢效能不均、採集 agent 的選擇在 Beats / Logstash / Elastic Agent / Fluent Bit 之間搖擺不定。ILM 跟 log pipeline 設計是 Elastic Stack 從「能用」到「可治理」的關鍵步驟。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="data-stream-vs-index-alias">Data Stream vs Index Alias&lt;/h3>
&lt;p>Elasticsearch 7.9+ 引入 data stream，取代傳統 index alias + rollover 模式。兩者的核心差異：&lt;/p>
&lt;p>&lt;strong>Data stream&lt;/strong> 是 append-only 的 time-series 資料結構。每個 data stream 下有多個 backing index，由 ILM 自動管理 rollover。寫入只能 append（沒有 update / delete single document），適合 log、metrics、traces。&lt;/p>
&lt;p>&lt;strong>Index alias&lt;/strong> 是傳統模式 — 手動建立 write alias 指向 current index，配合 ILM rollover action 觸發新 index 建立。支援 update / delete，適合需要修改文件的場景（例如 enrichment pipeline 的 lookup index）。&lt;/p>
&lt;p>選擇判讀：time-series 資料（log / metrics / APM trace）一律用 data stream。需要文件修改的 reference data、lookup table 用 index alias。新部署預設用 data stream，除非有明確理由。&lt;/p>
&lt;h3 id="ilm-policy-設計">ILM Policy 設計&lt;/h3>
&lt;p>ILM（Index Lifecycle Management）把 index 的生命週期分成五個 phase：&lt;/p>
&lt;p>&lt;strong>Hot phase&lt;/strong>：active write + 高頻查詢。Index 在 hot data node 上，用 SSD。Rollover 條件觸發後，current index 變 read-only，新 index 繼續寫入。&lt;/p>
&lt;p>&lt;strong>Warm phase&lt;/strong>：read-only + 中頻查詢。Index 搬到 warm data node（可以是 HDD 或較便宜的 SSD）。通常在 rollover 後 1-7 天觸發。可以執行 force merge（減少 segment 數量、提升查詢效能）跟 shrink（減少 shard 數量）。&lt;/p>
&lt;p>&lt;strong>Cold phase&lt;/strong>：searchable snapshot + 低頻查詢。Index 轉成 partial searchable snapshot，資料存在 object storage（S3 / GCS / Azure Blob），本地只保留 cache。查詢可用但較慢。適合 30 天到 1 年的保留。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> 的 vendor deep article，深化 overview「Index Lifecycle Management」跟「採集 pipeline」段。初次接觸 Elastic 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Elastic Stack 部署後，工程師通常能快速搜尋到 log。問題出在規模成長後：index 數量膨脹導致 cluster 效能退化、disk 滿了才發現沒有 lifecycle policy、shard 太小或太大造成查詢效能不均、採集 agent 的選擇在 Beats / Logstash / Elastic Agent / Fluent Bit 之間搖擺不定。ILM 跟 log pipeline 設計是 Elastic Stack 從「能用」到「可治理」的關鍵步驟。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="data-stream-vs-index-alias">Data Stream vs Index Alias</h3>
<p>Elasticsearch 7.9+ 引入 data stream，取代傳統 index alias + rollover 模式。兩者的核心差異：</p>
<p><strong>Data stream</strong> 是 append-only 的 time-series 資料結構。每個 data stream 下有多個 backing index，由 ILM 自動管理 rollover。寫入只能 append（沒有 update / delete single document），適合 log、metrics、traces。</p>
<p><strong>Index alias</strong> 是傳統模式 — 手動建立 write alias 指向 current index，配合 ILM rollover action 觸發新 index 建立。支援 update / delete，適合需要修改文件的場景（例如 enrichment pipeline 的 lookup index）。</p>
<p>選擇判讀：time-series 資料（log / metrics / APM trace）一律用 data stream。需要文件修改的 reference data、lookup table 用 index alias。新部署預設用 data stream，除非有明確理由。</p>
<h3 id="ilm-policy-設計">ILM Policy 設計</h3>
<p>ILM（Index Lifecycle Management）把 index 的生命週期分成五個 phase：</p>
<p><strong>Hot phase</strong>：active write + 高頻查詢。Index 在 hot data node 上，用 SSD。Rollover 條件觸發後，current index 變 read-only，新 index 繼續寫入。</p>
<p><strong>Warm phase</strong>：read-only + 中頻查詢。Index 搬到 warm data node（可以是 HDD 或較便宜的 SSD）。通常在 rollover 後 1-7 天觸發。可以執行 force merge（減少 segment 數量、提升查詢效能）跟 shrink（減少 shard 數量）。</p>
<p><strong>Cold phase</strong>：searchable snapshot + 低頻查詢。Index 轉成 partial searchable snapshot，資料存在 object storage（S3 / GCS / Azure Blob），本地只保留 cache。查詢可用但較慢。適合 30 天到 1 年的保留。</p>
<p><strong>Frozen phase</strong>：fully mounted searchable snapshot + 極低頻查詢。資料完全在 object storage，本地無 cache。查詢最慢但成本最低。適合 1 年以上的合規保留。</p>
<p><strong>Delete phase</strong>：刪除 index。保留期到期後自動清理。</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">PUT _ilm/policy/application-log-policy
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">{
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  &#34;policy&#34;: {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    &#34;phases&#34;: {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      &#34;hot&#34;: {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">          &#34;rollover&#34;: {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            &#34;max_primary_shard_size&#34;: &#34;30gb&#34;,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            &#34;max_age&#34;: &#34;1d&#34;
</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><span class="line"><span class="ln">13</span><span class="cl">      &#34;warm&#34;: {
</span></span><span class="line"><span class="ln">14</span><span class="cl">        &#34;min_age&#34;: &#34;3d&#34;,
</span></span><span class="line"><span class="ln">15</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln">16</span><span class="cl">          &#34;forcemerge&#34;: {&#34;max_num_segments&#34;: 1},
</span></span><span class="line"><span class="ln">17</span><span class="cl">          &#34;shrink&#34;: {&#34;number_of_shards&#34;: 1}
</span></span><span class="line"><span class="ln">18</span><span class="cl">        }
</span></span><span class="line"><span class="ln">19</span><span class="cl">      },
</span></span><span class="line"><span class="ln">20</span><span class="cl">      &#34;cold&#34;: {
</span></span><span class="line"><span class="ln">21</span><span class="cl">        &#34;min_age&#34;: &#34;30d&#34;,
</span></span><span class="line"><span class="ln">22</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln">23</span><span class="cl">          &#34;searchable_snapshot&#34;: {
</span></span><span class="line"><span class="ln">24</span><span class="cl">            &#34;snapshot_repository&#34;: &#34;s3-repo&#34;
</span></span><span class="line"><span class="ln">25</span><span class="cl">          }
</span></span><span class="line"><span class="ln">26</span><span class="cl">        }
</span></span><span class="line"><span class="ln">27</span><span class="cl">      },
</span></span><span class="line"><span class="ln">28</span><span class="cl">      &#34;delete&#34;: {
</span></span><span class="line"><span class="ln">29</span><span class="cl">        &#34;min_age&#34;: &#34;365d&#34;,
</span></span><span class="line"><span class="ln">30</span><span class="cl">        &#34;actions&#34;: {&#34;delete&#34;: {}}
</span></span><span class="line"><span class="ln">31</span><span class="cl">      }
</span></span><span class="line"><span class="ln">32</span><span class="cl">    }
</span></span><span class="line"><span class="ln">33</span><span class="cl">  }
</span></span><span class="line"><span class="ln">34</span><span class="cl">}</span></span></code></pre></div><p>Rollover 條件的選擇：<code>max_primary_shard_size</code> 比 <code>max_size</code> 更精確（直接控制單一 primary shard 大小）。目標是每個 primary shard 在 20-50 GB 之間。太小（&lt; 5 GB）造成 shard 過多、cluster state 膨脹；太大（&gt; 50 GB）造成 recovery 慢、query 效能下降。</p>
<h3 id="儲存成長回推-lifecycle-設計">儲存成長回推 lifecycle 設計</h3>
<p><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">Discord 儲存成長案例</a>揭露一個在快速成長服務反覆出現的模式：資料量倍增後才發現 ILM 的 hot → warm → cold 邊界不對、hot tier 佔比過高是最常見的成本問題。</p>
<p>問題的根源是 ILM policy 在服務初期設計、之後沒有隨資料量調整。一個服務從 10 GB/day 成長到 100 GB/day 時：</p>
<ul>
<li><strong>Hot tier 膨脹</strong>：原本 hot phase 設 7 天、10 GB/day × 7 天 = 70 GB。成長到 100 GB/day 後、hot tier 變成 700 GB、SSD 成本是原來的 10 倍</li>
<li><strong>Warm tier 延遲啟動</strong>：如果 warm phase 的 <code>min_age</code> 仍然是 7 天、資料在最貴的 tier 停留太久</li>
<li><strong>Cold/frozen phase 未啟用</strong>：初期資料量小時 cold phase 看不到成本效益、成長後才發現 30 天以上的資料全在 warm tier SSD 上</li>
</ul>
<p>修法是把 ILM review 放進服務的 capacity review cadence（季度或半年）。Review 時看三個指標：<code>hot_data_size / total_data_size</code>（hot tier 佔比超過 30% 就該重新評估）、<code>warm_tier_age_distribution</code>（warm tier 是否堆了太多舊資料）、<code>monthly_storage_cost_trend</code>（成本是否跟資料量同比例增長）。</p>
<p>Searchable snapshot（cold/frozen phase）是成本降幅最大的一步 — 資料從 local SSD 搬到 object storage，儲存成本降 70-90%。但搬遷後查詢延遲從 ms 退化到秒級。判讀「什麼資料該移」的訊號是該 index 在過去 30 天的查詢頻率 — 沒被查過的 index 留在 warm tier 是浪費。</p>
<h3 id="採集-pipelinebeats-vs-elastic-agent-vs-第三方">採集 Pipeline：Beats vs Elastic Agent vs 第三方</h3>
<table>
  <thead>
      <tr>
          <th>採集工具</th>
          <th>定位</th>
          <th>適用場景</th>
          <th>管理模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filebeat</td>
          <td>單用途 log 採集</td>
          <td>成熟穩定、資源消耗低、K8s 環境輕量</td>
          <td>手動 config / ConfigMap</td>
      </tr>
      <tr>
          <td>Metricbeat</td>
          <td>單用途 metrics 採集</td>
          <td>host / container / service metrics</td>
          <td>手動 config</td>
      </tr>
      <tr>
          <td>Elastic Agent</td>
          <td>統一採集 agent</td>
          <td>logs + metrics + security + APM、Fleet 集中管理</td>
          <td>Fleet Server 集中</td>
      </tr>
      <tr>
          <td>Logstash</td>
          <td>重型 ETL pipeline</td>
          <td>複雜 parsing / enrichment / 多 output</td>
          <td>手動 config</td>
      </tr>
      <tr>
          <td>Fluent Bit / Vector</td>
          <td>第三方輕量 agent</td>
          <td>多 destination、低 resource、OTel 整合</td>
          <td>手動 config</td>
      </tr>
  </tbody>
</table>
<p>選擇判讀：</p>
<ul>
<li><strong>新部署、想要集中管理</strong>：Elastic Agent + Fleet。Fleet Server 提供 policy 集中推送、版本升級、health monitoring。代價是 Fleet Server 自身需要維運。</li>
<li><strong>既有 Beats 部署、穩定運行</strong>：不急著遷移。Elastic Agent 的 Beats integration 內部仍用 Beats 引擎。</li>
<li><strong>K8s 環境、resource 敏感</strong>：Filebeat DaemonSet。資源消耗 ~50-100 MB per node，比 Elastic Agent 低。</li>
<li><strong>多 destination（ES + S3 + Kafka）</strong>：Logstash 或 Vector。Beats 的 output 只能寫一個 destination（除非用 output plugin hack）。</li>
<li><strong>已有 OTel Collector</strong>：OTel Collector 可以直接把 log 送到 Elasticsearch（OTLP exporter 或 Elasticsearch exporter），不需要額外 Beats。</li>
</ul>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="ingest-pipeline-設計">Ingest Pipeline 設計</h3>
<p>Ingest pipeline 在 Elasticsearch 層做 log 的 parsing 跟 enrichment，在 index 前處理。</p>
<p>常用 processor：</p>
<ul>
<li><strong>grok</strong>：regex pattern 解析非結構化 log。適合 nginx access log、syslog 等固定格式。</li>
<li><strong>dissect</strong>：delimiter-based parsing。比 grok 快 5-10 倍，但只能處理固定 delimiter 格式。</li>
<li><strong>date</strong>：把 log 中的 timestamp string 解析成 <code>@timestamp</code>。</li>
<li><strong>geoip</strong>：IP 地址轉地理位置。</li>
<li><strong>script</strong>：Painless script 做自訂轉換。效能代價高，只在其他 processor 做不到時使用。</li>
<li><strong>set / rename / remove</strong>：field 操作。</li>
</ul>
<p>Pipeline 設計原則：先用 dissect（快）、dissect 做不到才用 grok（慢）。Pipeline 中的 processor 數量跟複雜度直接影響 ingest 吞吐。高 volume 場景（&gt; 10K events/sec per node）要做 ingest pipeline benchmark。</p>
<h3 id="mapping-template-與-dynamic-mapping-治理">Mapping Template 與 Dynamic Mapping 治理</h3>
<p>Mapping template 定義 index 的 field type。Dynamic mapping 對未知 field 自動建立 mapping — 這是 Elastic 的便利功能，也是最常見的治理問題。</p>
<p><strong>Dynamic mapping 風險</strong>：application log 帶 arbitrary JSON payload，dynamic mapping 對每個 key 建立 field mapping。一個 log 帶 100 個 unique key → 100 個 field mapping。大量 unique key 會導致 mapping explosion（field 數量爆、cluster state 膨脹、query routing 變慢）。</p>
<p><strong>治理策略</strong>：</p>
<ul>
<li>用 <code>dynamic: strict</code> 或 <code>dynamic: false</code>（strict = 拒絕未定義 field、false = 接受但不 index）</li>
<li>在 mapping template 明確定義已知 field，用 <code>dynamic_templates</code> 控制未知 field 的行為</li>
<li>對 arbitrary JSON payload 用 <code>flattened</code> field type（ES 7.3+）— 整個 JSON 存為 keyword，可查但不逐 key index</li>
</ul>





<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">PUT _index_template/app-logs
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">{
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  &#34;index_patterns&#34;: [&#34;app-logs-*&#34;],
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  &#34;template&#34;: {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    &#34;mappings&#34;: {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      &#34;dynamic&#34;: &#34;strict&#34;,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      &#34;properties&#34;: {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        &#34;@timestamp&#34;: {&#34;type&#34;: &#34;date&#34;},
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        &#34;message&#34;: {&#34;type&#34;: &#34;text&#34;},
</span></span><span class="line"><span class="ln">10</span><span class="cl">        &#34;log.level&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">11</span><span class="cl">        &#34;service.name&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">12</span><span class="cl">        &#34;trace.id&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">13</span><span class="cl">        &#34;metadata&#34;: {&#34;type&#34;: &#34;flattened&#34;}
</span></span><span class="line"><span class="ln">14</span><span class="cl">      }
</span></span><span class="line"><span class="ln">15</span><span class="cl">    }
</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></code></pre></div><h3 id="shard-sizing">Shard Sizing</h3>
<p>Shard sizing 是 Elastic Stack 效能的核心變數。</p>
<p><strong>目標</strong>：每個 primary shard 20-50 GB（Elastic 官方建議）。每個 data node 管理的 shard 數量上限約 20 per GB heap（預設 heap 一般設 30 GB → ~600 shard per node）。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>日 ingest 量</th>
          <th>primary shard 數</th>
          <th>rollover 頻率</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>小型（&lt; 10 GB/day）</td>
          <td>5 GB</td>
          <td>1</td>
          <td>每天或 max_size 30 GB</td>
          <td>簡單 ILM 即可</td>
      </tr>
      <tr>
          <td>中型（10-100 GB/day）</td>
          <td>50 GB</td>
          <td>2-3</td>
          <td>每天</td>
          <td>warm + cold ILM</td>
      </tr>
      <tr>
          <td>大型（100+ GB/day）</td>
          <td>500 GB</td>
          <td>10-15</td>
          <td>每小時或 max_size 30 GB</td>
          <td>hot-warm-cold-frozen 全用</td>
      </tr>
  </tbody>
</table>
<p>Shard 過多的症狀：cluster state 過大（<code>_cluster/stats</code> 的 <code>indices.shards.total</code> 數千或數萬）、master node CPU 高（維護 cluster state）、recovery 慢。</p>
<p>Shard 過大的症狀：single shard query 慢（&gt; 500ms for simple filter）、segment merge 時間長、recovery 時單一 shard 復原需要數分鐘。</p>
<h3 id="shard-count-治理">Shard count 治理</h3>
<p>大量 index 場景（微服務架構下每個服務每天產生一個 data stream backing index）容易累積過多 shard。一個 50 服務的組織、每個服務每天 rollover 一次、primary + 1 replica = 100 shard/day。30 天後 hot + warm tier 有 3000 個 shard。</p>
<p>Elasticsearch 的經驗法則是每個 data node 管理的 shard 數量上限約 20 per GB heap。30 GB heap 的 node 約能管 600 個 shard。3000 個 shard 需要至少 5 個 data node 才不觸發效能退化。</p>
<p>降低 shard 數量的手段：</p>
<ul>
<li><strong>ILM shrink action</strong>：warm phase 把 primary shard 數量縮減（例如 3 → 1）。適合查詢頻率下降的舊 index</li>
<li><strong>延長 rollover 週期</strong>：如果單個服務的日資料量只有 1-2 GB，每天 rollover 產生的 shard 太小。調整 rollover 條件為 <code>max_primary_shard_size: 30gb</code>（讓系統自動決定 rollover 時機）而非固定 <code>max_age: 1d</code></li>
<li><strong>合併小服務</strong>：QPS 很低的服務共用同一個 data stream（用 <code>service.name</code> field 區分），減少 data stream 數量</li>
</ul>
<p>監控指標：<code>_cat/health</code> 的 <code>active_shards</code> 持續觀察趨勢。設 alert 在 shard count 超過 <code>data_node_count × 500</code> 時通知（留 buffer 給 recovery 跟 rebalance）。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="ilm-rollover-沒觸發">ILM rollover 沒觸發</h3>
<p><strong>觸發條件</strong>：ILM policy 已設定但 rollover action 沒有執行。常見原因：index 沒有正確關聯到 ILM policy、或 ILM 被暫停（<code>_ilm/stop</code>）。</p>
<p><strong>判讀</strong>：用 <code>GET &lt;index&gt;/_ilm/explain</code> 看 ILM 狀態。<code>managed: false</code> 代表 index 不受 ILM 管理。<code>step: ERROR</code> 代表 ILM 卡在某個 action。</p>
<p><strong>修復</strong>：確認 index template 的 <code>index.lifecycle.name</code> 指向正確的 ILM policy。如果 ILM step error，用 <code>POST &lt;index&gt;/_ilm/retry</code> 重試。</p>
<h3 id="searchable-snapshot-查詢延遲高">Searchable snapshot 查詢延遲高</h3>
<p><strong>觸發條件</strong>：cold / frozen phase 的 searchable snapshot index 被高頻查詢。</p>
<p><strong>表現</strong>：query latency 從 ms 級退化到秒級。原因是每次查詢需要從 object storage（S3 / GCS）拉資料。</p>
<p><strong>修復</strong>：cold phase 有 local cache、查重複 query 較快；frozen phase 無 cache、每次都拉。如果查詢頻率高到需要 sub-second 回應，這些 index 不應該在 cold/frozen phase — 調整 ILM policy 的 <code>min_age</code> 讓它們留在 warm phase 更久。</p>
<h3 id="cross-cluster-search-vs-replication">Cross-cluster search vs replication</h3>
<p><strong>Cross-cluster search（CCS）</strong>：查詢時 fan-out 到遠端 cluster。適合偶爾跨 cluster 查詢、不需要常駐複製。代價是查詢 latency 包含跨 cluster 的網路延遲。</p>
<p><strong>Cross-cluster replication（CCR）</strong>：把 index 從 leader cluster 持續複製到 follower cluster。適合 DR、地理就近讀取。代價是複製的 storage 跟網路頻寬成本。</p>
<p>選擇判讀：「偶爾查」→ CCS。「需要低延遲讀 + DR」→ CCR。兩者可以並存。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Elastic Stack 的成本由三個維度決定：</p>
<p><strong>License tier</strong>：Basic（免費、含 ILM / data streams）→ Gold（ML / alerting）→ Platinum（SIEM / endpoint）→ Enterprise。Elastic Cloud 的計費另加 infrastructure cost。</p>
<p><strong>Data tier storage</strong>：hot tier 用 SSD（最貴）、warm tier 用 HDD 或便宜 SSD、cold/frozen tier 用 object storage（最便宜）。ILM 的 phase 設計直接影響 storage cost。</p>
<p><strong>Node 數量</strong>：每增加 data node 增加 compute 成本。Shard sizing 跟 ILM 設計決定需要多少 node。</p>
<p>成本最佳化優先序：</p>
<ol>
<li><strong>ILM + searchable snapshot</strong>：30 天後移到 cold/frozen，storage 成本降 70-90%</li>
<li><strong>Shard sizing</strong>：避免 shard 過多造成的 cluster overhead</li>
<li><strong>Ingest pipeline</strong>：在 ingest 層 drop 不需要的 field，減少 index size</li>
<li><strong>Mapping 治理</strong>：避免 mapping explosion 造成的 cluster state overhead</li>
<li><strong>Retention policy</strong>：明確設定 delete phase，不讓過期資料佔空間</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack 服務頁</a>：overview 與日常操作</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 在觀測架構中的定位</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：mapping drift 跟 field missing 的資料品質面</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a>：ILM + searchable snapshot 在合規場景的應用</li>
<li><a href="../migrate-to-elastic-cloud/">Elastic Cloud migration</a>：從自管 Elastic 遷移到 Elastic Cloud</li>
</ul>
]]></content:encoded></item><item><title>LGTM Stack 組合運維：Loki + Grafana + Tempo + Mimir</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/lgtm-stack-operations/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/lgtm-stack-operations/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a> 的 vendor deep article，深化 overview 的元件組合段。初次接觸 Grafana Stack 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Grafana Stack（LGTM = Loki + Grafana + Tempo + Mimir）是自架觀測平台的完整選項，四個元件各自承擔一類訊號的儲存跟查詢。理解每個元件的責任邊界、部署模式跟故障特性，才能避免「裝了四個元件但不知道哪個壞了」的黑盒問題。&lt;/p>
&lt;h2 id="四元件的責任分工">四元件的責任分工&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元件&lt;/th>
 &lt;th>訊號類型&lt;/th>
 &lt;th>查詢語言&lt;/th>
 &lt;th>儲存後端&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Loki&lt;/td>
 &lt;td>Log&lt;/td>
 &lt;td>LogQL&lt;/td>
 &lt;td>Object storage + BoltDB&lt;/td>
 &lt;td>Log aggregation、grep 替代品&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mimir&lt;/td>
 &lt;td>Metric&lt;/td>
 &lt;td>PromQL&lt;/td>
 &lt;td>Object storage&lt;/td>
 &lt;td>Prometheus 的可擴展長期儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tempo&lt;/td>
 &lt;td>Trace&lt;/td>
 &lt;td>TraceQL&lt;/td>
 &lt;td>Object storage&lt;/td>
 &lt;td>Trace 儲存、span 搜尋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Grafana&lt;/td>
 &lt;td>視覺化&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>Dashboard、alert、data source&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Grafana 是查詢 / 視覺化層，Loki / Mimir / Tempo 是儲存 / 查詢層。Grafana 本身不存觀測資料，它連接 data source（Loki / Mimir / Tempo / Prometheus / Elasticsearch）做查詢跟渲染。&lt;/p>
&lt;p>四個元件獨立部署、獨立擴展、各自有健康指標。一個元件故障不影響其他元件 — Loki 掛了時 Grafana 的 metric dashboard 跟 trace 查詢仍然正常，只有 log panel 會報錯。&lt;/p>
&lt;h2 id="部署模式">部署模式&lt;/h2>
&lt;h3 id="monolithic-mode">Monolithic mode&lt;/h3>
&lt;p>四個元件（或其中幾個）跑在同一個 process / container。適合小規模（每天數 GB log、數十萬 metric series、少量 trace）。部署最簡單 — 一個 docker-compose 或 Helm chart 起全套。&lt;/p>
&lt;p>限制是沒辦法獨立擴展 — log 量大但 metric 量小時，monolithic mode 不能只加 Loki 的資源。&lt;/p>
&lt;h3 id="microservices-mode">Microservices mode&lt;/h3>
&lt;p>每個元件拆成獨立的 deployment、各自 autoscaling。Loki 拆成 distributor / ingester / querier / compactor；Mimir 拆成類似的元件；Tempo 也有對應的分層。&lt;/p>
&lt;p>適合中到大規模。部署跟維運複雜度顯著上升 — 每個元件的每個子服務都需要獨立的 health check、autoscaling 設定、persistent volume。&lt;/p>
&lt;h3 id="選擇判準">選擇判準&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>條件&lt;/th>
 &lt;th>建議模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>團隊 &amp;lt; 5 人、日 log &amp;lt; 10 GB&lt;/td>
 &lt;td>Monolithic&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要獨立擴展某一類訊號&lt;/td>
 &lt;td>Microservices&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不想自管、預算足夠&lt;/td>
 &lt;td>Grafana Cloud&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已有 Prometheus、只需要加 log / trace&lt;/td>
 &lt;td>漸進式加 Loki + Tempo&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見故障模式">常見故障模式&lt;/h2>
&lt;h3 id="lokiingester-oom">Loki：ingester OOM&lt;/h3>
&lt;p>Loki ingester 把 log chunks 保存在記憶體，高流量時容易 OOM。觸發條件是突然的 log 量爆增（部署後 error storm、某服務開了 debug log level）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> 的 vendor deep article，深化 overview 的元件組合段。初次接觸 Grafana Stack 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Grafana Stack（LGTM = Loki + Grafana + Tempo + Mimir）是自架觀測平台的完整選項，四個元件各自承擔一類訊號的儲存跟查詢。理解每個元件的責任邊界、部署模式跟故障特性，才能避免「裝了四個元件但不知道哪個壞了」的黑盒問題。</p>
<h2 id="四元件的責任分工">四元件的責任分工</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>訊號類型</th>
          <th>查詢語言</th>
          <th>儲存後端</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loki</td>
          <td>Log</td>
          <td>LogQL</td>
          <td>Object storage + BoltDB</td>
          <td>Log aggregation、grep 替代品</td>
      </tr>
      <tr>
          <td>Mimir</td>
          <td>Metric</td>
          <td>PromQL</td>
          <td>Object storage</td>
          <td>Prometheus 的可擴展長期儲存</td>
      </tr>
      <tr>
          <td>Tempo</td>
          <td>Trace</td>
          <td>TraceQL</td>
          <td>Object storage</td>
          <td>Trace 儲存、span 搜尋</td>
      </tr>
      <tr>
          <td>Grafana</td>
          <td>視覺化</td>
          <td>—</td>
          <td>—</td>
          <td>Dashboard、alert、data source</td>
      </tr>
  </tbody>
</table>
<p>Grafana 是查詢 / 視覺化層，Loki / Mimir / Tempo 是儲存 / 查詢層。Grafana 本身不存觀測資料，它連接 data source（Loki / Mimir / Tempo / Prometheus / Elasticsearch）做查詢跟渲染。</p>
<p>四個元件獨立部署、獨立擴展、各自有健康指標。一個元件故障不影響其他元件 — Loki 掛了時 Grafana 的 metric dashboard 跟 trace 查詢仍然正常，只有 log panel 會報錯。</p>
<h2 id="部署模式">部署模式</h2>
<h3 id="monolithic-mode">Monolithic mode</h3>
<p>四個元件（或其中幾個）跑在同一個 process / container。適合小規模（每天數 GB log、數十萬 metric series、少量 trace）。部署最簡單 — 一個 docker-compose 或 Helm chart 起全套。</p>
<p>限制是沒辦法獨立擴展 — log 量大但 metric 量小時，monolithic mode 不能只加 Loki 的資源。</p>
<h3 id="microservices-mode">Microservices mode</h3>
<p>每個元件拆成獨立的 deployment、各自 autoscaling。Loki 拆成 distributor / ingester / querier / compactor；Mimir 拆成類似的元件；Tempo 也有對應的分層。</p>
<p>適合中到大規模。部署跟維運複雜度顯著上升 — 每個元件的每個子服務都需要獨立的 health check、autoscaling 設定、persistent volume。</p>
<h3 id="選擇判準">選擇判準</h3>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>建議模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>團隊 &lt; 5 人、日 log &lt; 10 GB</td>
          <td>Monolithic</td>
      </tr>
      <tr>
          <td>需要獨立擴展某一類訊號</td>
          <td>Microservices</td>
      </tr>
      <tr>
          <td>不想自管、預算足夠</td>
          <td>Grafana Cloud</td>
      </tr>
      <tr>
          <td>已有 Prometheus、只需要加 log / trace</td>
          <td>漸進式加 Loki + Tempo</td>
      </tr>
  </tbody>
</table>
<h2 id="常見故障模式">常見故障模式</h2>
<h3 id="lokiingester-oom">Loki：ingester OOM</h3>
<p>Loki ingester 把 log chunks 保存在記憶體，高流量時容易 OOM。觸發條件是突然的 log 量爆增（部署後 error storm、某服務開了 debug log level）。</p>
<p>判讀指標：<code>loki_ingester_memory_chunks</code>、<code>process_resident_memory_bytes</code>。修復方向：調整 chunk flush interval（更頻繁寫入 object storage、降低記憶體壓力）、加 ingester replica、或在 pipeline 層（OTel Collector）做 log volume rate limit。</p>
<h3 id="mimircompactor-卡住">Mimir：compactor 卡住</h3>
<p>Mimir compactor 負責合併 ingester 寫入的 block。Compactor 卡住時，block 數量持續增長、query 需要掃描更多 block、延遲上升。</p>
<p>判讀指標：<code>cortex_compactor_runs_completed_total</code> 停滯、<code>cortex_bucket_blocks_count</code> 持續增長。修復方向：檢查 object storage 的寫入權限跟延遲、增加 compactor 資源（CPU / memory）、或暫時停止 ingestion 讓 compactor 追上。</p>
<h3 id="tempotrace-not-found">Tempo：trace not found</h3>
<p>使用者用 trace ID 查詢時回 &ldquo;trace not found&rdquo;，但 trace 確實存在。常見原因是 Tempo 的 bloom filter / compacted block index 還沒包含該 trace（ingestion 到可查詢有延遲），或 trace 被 retention policy 刪除。</p>
<p>判讀方式：查 trace 的 timestamp 是否在 retention 範圍內、查 <code>tempo_ingester_traces_created_total</code> 確認 ingestion 正常、查 compactor 是否正常運行。</p>
<h3 id="grafanadashboard-provisioning-漂移">Grafana：dashboard provisioning 漂移</h3>
<p>用 provisioning（YAML / JSON 檔案）管理 dashboard 時，手動在 UI 修改的 dashboard 會在下次 provisioning 同步時被覆蓋。團隊成員在 UI 調整了 panel、下次重啟 Grafana 後修改消失。</p>
<p>修復方向：dashboard 修改統一透過 git → provisioning pipeline（GitOps），UI 只用於臨時調整跟探索。把 provisioning 的 <code>allowUiUpdates</code> 設為 false、強制所有變更走 git。</p>
<h2 id="dashboard-provisioning">Dashboard Provisioning</h2>
<p>Dashboard 的管理方式影響長期維護成本。手動在 UI 建立 dashboard 的起步最快，但隨 dashboard 數量增長會出現版本不一致、無法 rollback、owner 不明的問題。</p>
<h3 id="infrastructure-as-code">Infrastructure as Code</h3>
<p>Dashboard JSON 存在 git repo、透過 provisioning 同步到 Grafana。變更走 PR review、有版本歷史、可以 rollback。</p>
<p>Grafana 的 provisioning 機制讀 YAML config，指定 dashboard JSON 的來源（local file / HTTP / API）。Helm chart 部署時把 dashboard JSON 放在 ConfigMap 或 persistent volume。</p>
<h3 id="grafonnet--jsonnet">Grafonnet / Jsonnet</h3>
<p>用 Jsonnet（Grafana 的 dashboard-as-code library）產生 dashboard JSON。適合大量相似 dashboard 的場景 — 每個服務一個 dashboard，結構相同但 data source 跟 label 不同。</p>
<p>Grafonnet 的學習曲線比直接寫 JSON 高，但在 dashboard 數量 &gt; 20 個時開始有維護效率的回報。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：Mimir 的上游 metric 來源</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式</a>：LGTM 的 ingestion 入口</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 各層的治理</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：dashboard / alert 的 ownership</li>
</ul>
]]></content:encoded></item><item><title>Prometheus 容量規劃與故障模式</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。&lt;/p>
&lt;h2 id="資源消耗模型">資源消耗模型&lt;/h2>
&lt;h3 id="memory由-active-series-決定">Memory：由 active series 決定&lt;/h3>
&lt;p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Active series&lt;/th>
 &lt;th>預估 memory（head block）&lt;/th>
 &lt;th>適合的機器規格&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬&lt;/td>
 &lt;td>~400 MB&lt;/td>
 &lt;td>任何 VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100 萬&lt;/td>
 &lt;td>~4 GB&lt;/td>
 &lt;td>8 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>500 萬&lt;/td>
 &lt;td>~20 GB&lt;/td>
 &lt;td>32 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1000 萬&lt;/td>
 &lt;td>~40 GB&lt;/td>
 &lt;td>64 GB VM&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。&lt;/p>
&lt;p>判讀指標：&lt;code>prometheus_tsdb_head_series&lt;/code> 代表當前 active series 數量，&lt;code>process_resident_memory_bytes&lt;/code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。&lt;/p>
&lt;h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定&lt;/h3>
&lt;p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Ingestion rate&lt;/th>
 &lt;th>Retention&lt;/th>
 &lt;th>預估 disk&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~130 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>30 天&lt;/td>
 &lt;td>~260 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>50 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~650 GB&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。</p>
<h2 id="資源消耗模型">資源消耗模型</h2>
<h3 id="memory由-active-series-決定">Memory：由 active series 決定</h3>
<p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。</p>
<table>
  <thead>
      <tr>
          <th>Active series</th>
          <th>預估 memory（head block）</th>
          <th>適合的機器規格</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬</td>
          <td>~400 MB</td>
          <td>任何 VM</td>
      </tr>
      <tr>
          <td>100 萬</td>
          <td>~4 GB</td>
          <td>8 GB VM</td>
      </tr>
      <tr>
          <td>500 萬</td>
          <td>~20 GB</td>
          <td>32 GB VM</td>
      </tr>
      <tr>
          <td>1000 萬</td>
          <td>~40 GB</td>
          <td>64 GB VM</td>
      </tr>
  </tbody>
</table>
<p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。</p>
<p>判讀指標：<code>prometheus_tsdb_head_series</code> 代表當前 active series 數量，<code>process_resident_memory_bytes</code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。</p>
<h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定</h3>
<p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>Retention</th>
          <th>預估 disk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>15 天</td>
          <td>~130 GB</td>
      </tr>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>30 天</td>
          <td>~260 GB</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>15 天</td>
          <td>~650 GB</td>
      </tr>
  </tbody>
</table>
<p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。</p>
<h3 id="cpu由-scrape-數量與-query-負載決定">CPU：由 scrape 數量與 query 負載決定</h3>
<p>Scrape 本身的 CPU 消耗不高（HTTP GET + parse），但 scrape 數量 × scrape 間隔決定了基本的 CPU 基線。1000 個 target × 15 秒間隔 = 每秒 ~67 次 scrape，單核可以處理。</p>
<p>Query 是 CPU 的主要消耗者。Recording rule evaluation、alert rule evaluation、dashboard panel 查詢各自佔 CPU。Recording rule 數量增長到數百條時，evaluation 的 CPU 消耗可能成為瓶頸。</p>
<p>判讀指標：<code>prometheus_rule_evaluation_duration_seconds</code> 的 p99 超過 evaluation interval 時，rule 跑不完、alert 會延遲。</p>
<h2 id="cardinality-失控的判讀">Cardinality 失控的判讀</h2>
<p>Cardinality 是 Prometheus 最常見的容量問題。一個意外的高 cardinality label（user_id、request_id、完整 URL）可以在分鐘內把 series 數從 10 萬推到 100 萬、消耗數 GB 記憶體。</p>
<h3 id="判讀訊號">判讀訊號</h3>
<ul>
<li><code>prometheus_tsdb_head_series</code> 持續成長、斜率陡峭</li>
<li><code>prometheus_tsdb_head_active_appenders</code> 成長（新 series 的寫入速率）</li>
<li>Prometheus 的 memory 持續上升、最終 OOM kill</li>
<li>Query 延遲增加（更多 series 要掃描）</li>
<li>Compaction 時間變長</li>
</ul>
<h3 id="定位方式">定位方式</h3>





<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"># 找出哪個 metric name 的 series 最多
</span></span><span class="line"><span class="ln">2</span><span class="cl">topk(10, count by (__name__)({__name__=~&#34;.+&#34;}))
</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"># 找出哪個 job（scrape target）的 series 最多
</span></span><span class="line"><span class="ln">5</span><span class="cl">topk(10, count by (job)({__name__=~&#34;.+&#34;}))
</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"># 找出某個 metric 的哪個 label 組合在爆
</span></span><span class="line"><span class="ln">8</span><span class="cl">count by (method, status) (http_requests_total)</span></span></code></pre></div><h3 id="修復方向">修復方向</h3>
<ul>
<li><strong>Label 白名單</strong>：在 scrape config 或 relabeling rule 中 drop 高 cardinality label</li>
<li><strong>Metric relabeling</strong>：<code>metric_relabel_configs</code> 在 scrape 後、寫入前移除特定 label</li>
<li><strong>Recording rule 替代</strong>：把高 cardinality metric 聚合成低 cardinality 的 recording rule，下游只讀 recording rule</li>
<li><strong>移到 traces</strong>：user_id / request_id 這類維度放在 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 的 span attribute 而非 metric label</li>
</ul>
<h2 id="常見故障模式">常見故障模式</h2>
<h3 id="oom-kill">OOM Kill</h3>
<p><strong>觸發條件</strong>：active series 超過記憶體容量、或 heavy query 消耗大量暫時記憶體。</p>
<p><strong>表現</strong>：Prometheus process 被 kernel OOM killer 終止。重啟後 WAL replay 可能需要分鐘到十分鐘（取決於 WAL 大小），期間 scrape 跟 query 都不可用。</p>
<p><strong>預防</strong>：設定 memory limit alert（process_resident_memory_bytes / machine memory &gt; 70%）、tracking cardinality growth slope、query timeout 限制。</p>
<h3 id="scrape-timeout-連鎖">Scrape timeout 連鎖</h3>
<p><strong>觸發條件</strong>：target 的 metrics endpoint 回應慢（&gt; scrape_timeout）、或 target 數量超過 Prometheus 的並行 scrape 能力。</p>
<p><strong>表現</strong>：<code>up</code> metric 為 0、<code>scrape_duration_seconds</code> 升高、dashboard 出現資料斷層（missing data points）。大量 target 同時 timeout 時，Prometheus 的 scrape goroutine pool 被佔滿，影響其他健康 target 的 scrape。</p>
<p><strong>修復</strong>：調整 <code>scrape_timeout</code>（預設 10s，太短會造成 false timeout）、把慢 target 移到獨立的 scrape pool、或把 metrics endpoint 的回應最佳化（減少 expose 的 metric 數量）。</p>
<h3 id="wal-corruption">WAL corruption</h3>
<p><strong>觸發條件</strong>：Prometheus process 非正常終止（OOM kill、機器斷電）時，WAL 可能損壞。</p>
<p><strong>表現</strong>：重啟後 WAL replay 失敗、Prometheus 無法啟動。Error log 顯示 <code>WAL corrupted</code> 或 <code>invalid segment</code>。</p>
<p><strong>修復</strong>：刪除損壞的 WAL segment（丟失對應時間段的資料），重啟 Prometheus。嚴重時刪除整個 data 目錄重新開始（丟失所有歷史資料）。WAL 的持久性保證不如資料庫 — Prometheus 設計上允許短暫資料丟失，長期儲存靠 remote write 到 Mimir / Thanos。</p>
<h3 id="recording-rule-evaluation-lag">Recording rule evaluation lag</h3>
<p><strong>觸發條件</strong>：recording rule 數量多且表達式複雜、evaluation 時間超過 evaluation interval。</p>
<p><strong>表現</strong>：<code>prometheus_rule_group_last_duration_seconds</code> 超過 <code>prometheus_rule_group_interval_seconds</code>。Dashboard 讀 recording rule 的 panel 看到的資料落後當前時間。Alert rule 也在同一個 evaluation pipeline 裡，evaluation lag 會讓 alert 延遲觸發。</p>
<p><strong>修復</strong>：把重的 recording rule 拆到獨立的 rule group（各自 evaluation interval）、最佳化 PromQL expression（減少 aggregation 層數、縮小 time range）、或把 recording rule 卸載到 Mimir（ruler component 獨立擴展）。</p>
<h2 id="何時該從單機-prometheus-遷出">何時該從單機 Prometheus 遷出</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Active series &gt; 500 萬、memory 吃緊（32 GB VM 上 head block ~20 GB + query overhead 接近上限）</td>
          <td>Remote write 到 Mimir / Thanos 做長期儲存</td>
      </tr>
      <tr>
          <td>需要跨 region / cluster 查詢</td>
          <td>Thanos query 或 Mimir multi-tenant</td>
      </tr>
      <tr>
          <td>Recording rule evaluation lag 持續</td>
          <td>把 rule evaluation 卸載到 Mimir ruler</td>
      </tr>
      <tr>
          <td>需要 HA（single Prometheus = SPOF）</td>
          <td>兩個 instance + Thanos dedup</td>
      </tr>
      <tr>
          <td>Retention 要 &gt; 90 天但 disk 不夠</td>
          <td>Remote write + 短 local retention</td>
      </tr>
  </tbody>
</table>
<p>遷出的第一步通常是加 remote write — Prometheus 繼續本地 scrape 跟短期查詢，長期資料寫到遠端。這是最低風險的演進路徑，不需要改 scrape config 或 PromQL。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</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/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>：recording rule 跟 rollup 的查詢面設計</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為 Prometheus 的長期儲存後端</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rule 在查詢設計中的定位</li>
</ul>
]]></content:encoded></item><item><title>Sentry Error Grouping 與 Fingerprinting 策略</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/error-grouping-fingerprinting/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/error-grouping-fingerprinting/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 的 vendor deep article，深化 overview「Issue grouping / fingerprint」段。初次接觸 Sentry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Error grouping 決定 Sentry 的使用體驗。Grouping 太粗（不同 bug 被合併成同一個 issue），團隊會漏掉新問題；grouping 太細（同一個 bug 被拆成數百個 issue），issue list 變成 noise。理解 Sentry 的 grouping 演算法跟自訂 fingerprint 機制，才能讓 issue list 反映真實的 bug 數量而非 error event 數量。&lt;/p>
&lt;h2 id="預設-grouping-演算法">預設 Grouping 演算法&lt;/h2>
&lt;h3 id="stack-trace-為主">Stack trace 為主&lt;/h3>
&lt;p>Sentry 的預設 grouping 策略以 exception type + stack trace 為核心。兩個 error event 會被歸到同一個 issue，如果它們的 exception type 相同、且 stack trace 的「相關 frame」相同。&lt;/p>
&lt;p>「相關 frame」是 Sentry 的判定結果 — 它會過濾掉標準函式庫、框架內部 frame 跟已知 noise frame，只留下 application code frame。這個過濾邏輯叫 stack trace rules，由 Sentry 的 grouping 引擎自動決定。&lt;/p>
&lt;h3 id="grouping-版本">Grouping 版本&lt;/h3>
&lt;p>Sentry 的 grouping 演算法有多個版本（稱為 grouping config）。新建的 project 自動用最新版（截至 2024 年是 &lt;code>newstyle:2023-01-11&lt;/code>），舊 project 可能還在用舊版。升級 grouping config 會改變 issue 的歸屬 — 之前合併的 event 可能被拆開，之前分開的可能合併。&lt;/p>
&lt;p>確認目前的 grouping config：Project Settings → General Settings → Event Grouping。升級前先用 Sentry 的 grouping preview 功能測試影響範圍。&lt;/p>
&lt;h3 id="非-exception-事件">非 exception 事件&lt;/h3>
&lt;p>沒有 stack trace 的事件（&lt;code>capture_message&lt;/code>、breadcrumb-only event、CSP violation）用 message 內容做 grouping。相同 message template 的事件歸到同一個 issue。&lt;/p>
&lt;p>message 中如果包含動態值（user ID、request ID、timestamp），Sentry 會嘗試辨識並忽略動態部分。但辨識不完美 — 如果 message 格式不一致，同一種錯誤可能被拆成多個 issue。&lt;/p>
&lt;h2 id="自訂-fingerprint">自訂 Fingerprint&lt;/h2>
&lt;h3 id="何時需要自訂">何時需要自訂&lt;/h3>
&lt;p>預設 grouping 不夠用的常見場景：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>Fingerprint 解法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>外部 API timeout&lt;/td>
 &lt;td>不同 caller 的 stack trace 不同，但根因相同&lt;/td>
 &lt;td>用 &lt;code>{{ default }}&lt;/code> + error type 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database connection error&lt;/td>
 &lt;td>每個 query 的 stack trace 不同&lt;/td>
 &lt;td>用 error message pattern 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>前端 minified code&lt;/td>
 &lt;td>source map 缺失導致 frame 不穩定&lt;/td>
 &lt;td>先修 source map 上傳，而非硬 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rate limit / 429 error&lt;/td>
 &lt;td>大量 429 拆成數百個 issue&lt;/td>
 &lt;td>用 HTTP status code 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="server-side-fingerprint-rules">Server-side fingerprint rules&lt;/h3>
&lt;p>在 Project Settings → Issue Grouping → Fingerprint Rules 設定。語法：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 的 vendor deep article，深化 overview「Issue grouping / fingerprint」段。初次接觸 Sentry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Error grouping 決定 Sentry 的使用體驗。Grouping 太粗（不同 bug 被合併成同一個 issue），團隊會漏掉新問題；grouping 太細（同一個 bug 被拆成數百個 issue），issue list 變成 noise。理解 Sentry 的 grouping 演算法跟自訂 fingerprint 機制，才能讓 issue list 反映真實的 bug 數量而非 error event 數量。</p>
<h2 id="預設-grouping-演算法">預設 Grouping 演算法</h2>
<h3 id="stack-trace-為主">Stack trace 為主</h3>
<p>Sentry 的預設 grouping 策略以 exception type + stack trace 為核心。兩個 error event 會被歸到同一個 issue，如果它們的 exception type 相同、且 stack trace 的「相關 frame」相同。</p>
<p>「相關 frame」是 Sentry 的判定結果 — 它會過濾掉標準函式庫、框架內部 frame 跟已知 noise frame，只留下 application code frame。這個過濾邏輯叫 stack trace rules，由 Sentry 的 grouping 引擎自動決定。</p>
<h3 id="grouping-版本">Grouping 版本</h3>
<p>Sentry 的 grouping 演算法有多個版本（稱為 grouping config）。新建的 project 自動用最新版（截至 2024 年是 <code>newstyle:2023-01-11</code>），舊 project 可能還在用舊版。升級 grouping config 會改變 issue 的歸屬 — 之前合併的 event 可能被拆開，之前分開的可能合併。</p>
<p>確認目前的 grouping config：Project Settings → General Settings → Event Grouping。升級前先用 Sentry 的 grouping preview 功能測試影響範圍。</p>
<h3 id="非-exception-事件">非 exception 事件</h3>
<p>沒有 stack trace 的事件（<code>capture_message</code>、breadcrumb-only event、CSP violation）用 message 內容做 grouping。相同 message template 的事件歸到同一個 issue。</p>
<p>message 中如果包含動態值（user ID、request ID、timestamp），Sentry 會嘗試辨識並忽略動態部分。但辨識不完美 — 如果 message 格式不一致，同一種錯誤可能被拆成多個 issue。</p>
<h2 id="自訂-fingerprint">自訂 Fingerprint</h2>
<h3 id="何時需要自訂">何時需要自訂</h3>
<p>預設 grouping 不夠用的常見場景：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>問題</th>
          <th>Fingerprint 解法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外部 API timeout</td>
          <td>不同 caller 的 stack trace 不同，但根因相同</td>
          <td>用 <code>{{ default }}</code> + error type 做 fingerprint</td>
      </tr>
      <tr>
          <td>Database connection error</td>
          <td>每個 query 的 stack trace 不同</td>
          <td>用 error message pattern 做 fingerprint</td>
      </tr>
      <tr>
          <td>前端 minified code</td>
          <td>source map 缺失導致 frame 不穩定</td>
          <td>先修 source map 上傳，而非硬 fingerprint</td>
      </tr>
      <tr>
          <td>Rate limit / 429 error</td>
          <td>大量 429 拆成數百個 issue</td>
          <td>用 HTTP status code 做 fingerprint</td>
      </tr>
  </tbody>
</table>
<h3 id="server-side-fingerprint-rules">Server-side fingerprint rules</h3>
<p>在 Project Settings → Issue Grouping → Fingerprint Rules 設定。語法：</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"># 所有 ConnectionError 歸成一個 issue
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">error.type:ConnectionError -&gt; connection-error
</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"># 特定 message pattern 歸成一個 issue
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">message:&#34;Rate limit exceeded*&#34; -&gt; rate-limit
</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"># 特定 module 的所有 error 歸成一組
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">module:payment.gateway.* -&gt; payment-gateway-error
</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><span class="line"><span class="ln">11</span><span class="cl">error.type:TimeoutError module:external.api.* -&gt; external-api-timeout</span></span></code></pre></div><p>Server-side rules 的優先順序：越後面的 rule 優先順序越高。如果一個 event 匹配多條 rule，用最後一條。</p>
<h3 id="sdk-side-fingerprint">SDK-side fingerprint</h3>
<p>在 SDK 的 <code>before_send</code> callback 中設定 <code>event.fingerprint</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">before_send</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">hint</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="s2">&#34;ConnectionError&#34;</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">hint</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;exc_info&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">event</span><span class="p">[</span><span class="s2">&#34;fingerprint&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;connection-error&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">event</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="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;...&#34;</span><span class="p">,</span> <span class="n">before_send</span><span class="o">=</span><span class="n">before_send</span><span class="p">)</span></span></span></code></pre></div><p>SDK-side 跟 server-side 的差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Server-side rules</th>
          <th>SDK-side fingerprint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設定位置</td>
          <td>Sentry Web UI</td>
          <td>程式碼</td>
      </tr>
      <tr>
          <td>部署速度</td>
          <td>即時生效</td>
          <td>需要 deploy</td>
      </tr>
      <tr>
          <td>可見性</td>
          <td>團隊都能看到跟修改</td>
          <td>散在程式碼裡</td>
      </tr>
      <tr>
          <td>複雜邏輯</td>
          <td>只支援 pattern matching</td>
          <td>可用任意程式邏輯</td>
      </tr>
  </tbody>
</table>
<p>優先用 server-side rules — 集中管理、即時生效。SDK-side 用在 server-side rules 表達不了的複雜邏輯。</p>
<h3 id="-default--組合"><code>{{ default }}</code> 組合</h3>
<p>Fingerprint 中的 <code>{{ default }}</code> 代表 Sentry 預設的 grouping 結果。跟自訂值組合使用：</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"># 用預設 grouping + environment 維度拆分
</span></span><span class="line"><span class="ln">2</span><span class="cl">fingerprint: [&#34;{{ default }}&#34;, &#34;{{ environment }}&#34;]</span></span></code></pre></div><p>這樣同一個 bug 在 staging 跟 production 會分成兩個 issue，方便分別追蹤。</p>
<h2 id="merge-與-unmerge">Merge 與 Unmerge</h2>
<h3 id="事後修正">事後修正</h3>
<p>當 grouping 不準時，Sentry 提供事後修正：</p>
<p><strong>Merge</strong>：選擇多個 issue，合併成一個。合併後的 issue 保留所有 event，但只保留一個 issue ID。適合預設 grouping 太細（同一 bug 被拆成多個 issue）的情況。</p>
<p><strong>Unmerge</strong>（拆分）：從一個 issue 中選擇部分 event，拆出成新 issue。適合預設 grouping 太粗（不同 bug 被合在同一個 issue）的情況。</p>
<h3 id="mergeunmerge-的限制">Merge/Unmerge 的限制</h3>
<p>Merge 跟 Unmerge 都是「貼 OK 繃」— 只影響現有 event，新進的 event 仍然用原來的 grouping 邏輯。如果根因是 grouping 太粗或太細，應該修 fingerprint rule，而非持續 merge/unmerge。</p>
<p>判讀順序：</p>
<ol>
<li>發現 grouping 不準</li>
<li>先用 merge/unmerge 處理現有 issue（止血）</li>
<li>分析 root cause — 是 stack trace 不穩定、message 有動態值、還是缺 fingerprint rule</li>
<li>加 fingerprint rule 永久修正</li>
<li>驗證新進 event 的 grouping 是否正確</li>
</ol>
<h2 id="grouping-不準的判讀">Grouping 不準的判讀</h2>
<h3 id="太細的訊號">太細的訊號</h3>
<ul>
<li>Issue list 中出現大量「相似標題但不同 ID」的 issue</li>
<li>單一事件只有 1-2 個 occurrence 的 issue 大量出現</li>
<li>同一個使用者操作觸發的 error 被分散到多個 issue</li>
</ul>
<p>常見原因：message 中包含動態值（user ID、timestamp、request path）、source map 缺失（前端）、stack trace 包含 generated code frame。</p>
<h3 id="太粗的訊號">太粗的訊號</h3>
<ul>
<li>一個 issue 的 event 數量持續增長，但 event detail 看起來是不同問題</li>
<li>Issue 的 status 被 resolve 後馬上 regress，但新 event 跟原因不同</li>
<li>團隊 ignore 了一個「雜 issue」但裡面混著真正需要處理的 bug</li>
</ul>
<p>常見原因：exception type 太通用（<code>RuntimeError</code>、<code>Exception</code>）、fingerprint rule 太粗（把整個 module 的 error 合成一個 issue）。</p>
<h2 id="大量-unique-errors-的治理">大量 Unique Errors 的治理</h2>
<h3 id="問題issue-爆量">問題：Issue 爆量</h3>
<p>project 的 issue 數量超過數千時，issue list 失去可操作性。on-call 打開 Sentry 看到 2000 個 unresolved issue，等於沒有 triage。</p>
<h3 id="治理策略">治理策略</h3>
<p><strong>Inbound filter</strong>：在 Project Settings → Inbound Filters 設定，丟棄已知的 noise event（browser extension error、crawler error、legacy browser error）。丟棄在 ingestion 層，不消耗 quota。</p>
<p><strong>Rate limit</strong>：project 或 key 級別的 rate limit。超過限額的 event 被丟棄。適合防止單一 bug 的暴增 event 耗盡 quota，但不解決 issue 數量問題。</p>
<p><strong>Alert rule 搭配 ownership</strong>：用 Sentry alert rule 把特定 tag（service、team、module）的新 issue 通知對應 team。不是所有 issue 都要同一個人看。</p>
<p><strong>定期 triage cadence</strong>：每週或每兩週的 triage session，把 issue 分成 fix / ignore / merge 三類。Sentry 的 <code>For Review</code> tab 自動列出需要初次 triage 的 issue。</p>
<p><strong>Auto-resolve</strong>：設定 auto-resolve policy — 超過 N 天沒有新 event 的 issue 自動 resolve。避免舊 issue 永遠佔據 unresolved list。</p>
<h3 id="治理後的穩態">治理後的穩態</h3>
<p>合理的穩態是：unresolved issue 數量穩定在數十到數百，每週新增 issue 跟 resolve issue 數量大致平衡。如果 unresolved 持續增長，先檢查是否有 noise event 沒被 filter，或 fingerprint 太細。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>Error tracking 跟 observability 的邊界：Sentry 處理 error lifecycle、metrics/logs/traces 處理系統行為，見 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>OTel context 整合：Sentry SDK 接受 OTel trace_id / span_id，讓 error 跟 trace 關聯，見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</a></li>
<li>Release tracking 跟 session replay：見 <a href="../release-tracking-session-replay/">Release Tracking 與 Session Replay</a></li>
<li>事故響應整合：嚴重 issue → alert → on-call，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>2.10 Pub/Sub 與即時 fan-out</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/</guid><description>&lt;p>Redis &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a> 的核心責任是把一則訊息即時推送給當下所有訂閱者，讓跨節點的狀態變更可以在同一瞬間擴散。它承擔的是「現在發生的事，立刻讓所有人知道」，正式的可靠投遞與重播責任由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams&lt;/a> 承擔。把這條邊界放在最前面，是因為 Pub/Sub 的多數事故都來自把它當成可靠訊息系統使用。&lt;/p>
&lt;h2 id="at-most-once訊息只送給此刻在線的訂閱者">at-most-once：訊息只送給此刻在線的訂閱者&lt;/h2>
&lt;p>訊息&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">投遞語意&lt;/a>有三種：at-most-once（最多送一次、可能漏）、at-least-once（至少送一次、可能重複）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/exactly-once/" data-link-title="Exactly-Once" data-link-desc="說明訊息剛好被處理一次的語意承諾、它的代價，以及多數時候該用的替代路">exactly-once&lt;/a>（剛好一次、最難實作）。Pub/Sub 採 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">at-most-once&lt;/a>，用「可能漏」換取低延遲與無狀態，後兩種語意由 Streams 或 message queue 承擔。具體來說：&lt;code>PUBLISH&lt;/code> 把訊息送給發布當下已經 &lt;code>SUBSCRIBE&lt;/code> 該 channel 的連線，沒有訂閱者就直接丟棄，訊息不寫入任何持久結構。訂閱者離線、重連、或處理速度跟不上時，那段時間的訊息不會補送。&lt;/p>
&lt;p>這個語意決定了 Pub/Sub 適合承擔什麼。可以接受「偶爾漏一則、下一則狀態會蓋過來」的場景，Pub/Sub 的低延遲與簡單模型是優勢；要求「每一則都不能掉」的場景，例如訂單事件、扣款通知、稽核軌跡，這些責任屬於 durable queue，不該放在 Pub/Sub。&lt;/p>
&lt;p>判讀的關鍵問題是：漏掉一則訊息的代價是什麼。presence 狀態廣播漏一則，下次 heartbeat 會修正；cache invalidation 廣播漏一則，該節點會保留 stale 副本直到 TTL 到期，代價是短暫不一致；扣款事件漏一則，代價是金額錯誤且無法自動修復。前兩者落在 Pub/Sub 的能力範圍，第三者越界。&lt;/p>
&lt;h2 id="適用場景狀態變更的即時扇出">適用場景：狀態變更的即時扇出&lt;/h2>
&lt;p>Pub/Sub 的典型用途是把一個節點上發生的狀態變更，即時扇出給其他節點。這類場景的共同特徵是「最終狀態會自我修正」，所以單則訊息可丟。&lt;/p>
&lt;p>fan-out 有兩種語意要先分清，因為它們決定能不能用 Pub/Sub。一種是全量 fan-out：每個訂閱者都收到同一則訊息的完整副本，適合「所有節點都要知道這件事」的廣播（presence、cache invalidation、config reload）。另一種是分攤 fan-out：同一則訊息只交給一個 consumer 處理、多個 consumer 之間分攤負載，適合「這件工作只要有一個人做」的任務分派。Pub/Sub 只提供全量 fan-out——&lt;code>PUBLISH&lt;/code> 把訊息送給所有訂閱者，沒有「只給其中一個」的語意。需要分攤 fan-out 時要轉 Redis Streams 的 consumer group（&lt;code>XREADGROUP&lt;/code> 讓一則訊息只有一個 consumer 拿到），這條邊界在本章末的升級段展開。&lt;/p>
&lt;p>presence 變更廣播是最直接的應用。&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store&lt;/a> 的 cross-node query 回答「現在誰在線」，但當某個使用者上線或離線時，其他節點需要被即時通知才能推播給好友列表。presence key 寫入時同步 &lt;code>PUBLISH&lt;/code> 一則 &lt;code>user:online&lt;/code> 訊息，訂閱該 channel 的節點立刻更新本地視圖。漏一則的代價是某個好友的線上狀態延遲幾秒，下次狀態同步會補正，落在可接受範圍。&lt;/p>
&lt;p>cache invalidation 扇出是第二類應用。當一個節點更新了 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 並失效了自己的本地 cache，其他持有同一份 process-local cache 的節點需要被通知一起失效。&lt;code>PUBLISH cache:invalidate product:123&lt;/code> 讓所有節點丟棄該 key 的本地副本。這條路徑要跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 的失效策略對齊：Pub/Sub 負責「通知」，實際失效仍由各節點執行，且因為 at-most-once，必須有 TTL 作為兜底，避免廣播漏送讓某節點永久持有 stale 副本。&lt;/p>
&lt;p>即時配置熱刷新是第三類。feature flag、限流閾值、路由表這類低頻變更的配置，更新時 &lt;code>PUBLISH config:reload&lt;/code>，各節點收到後重新拉取最新配置。低頻特性讓 at-most-once 風險很低，而即時性比輪詢配置中心更省資源。&lt;/p>
&lt;h2 id="subscribe-的連線模型">SUBSCRIBE 的連線模型&lt;/h2>
&lt;p>訂閱會把連線切換進專用模式：一旦 &lt;code>SUBSCRIBE&lt;/code>，該連線只能再執行 &lt;code>SUBSCRIBE&lt;/code>、&lt;code>UNSUBSCRIBE&lt;/code>、&lt;code>PING&lt;/code> 與訂閱相關命令，不能在同一條連線上跑 &lt;code>GET&lt;/code>、&lt;code>SET&lt;/code> 等一般命令。原因是訂閱連線進入了等待推送的狀態，伺服器隨時可能把訊息推過來，與請求應答式命令的時序會衝突。&lt;/p>
&lt;p>這個模型的工程含義是：訂閱要用獨立的連線，不能跟一般讀寫共用同一個 client。共用連線池的應用要為 Pub/Sub 保留專門的訂閱連線，避免訂閱模式污染了拿來做 cache 讀寫的連線。這條限制跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發讀寫邊界&lt;/a> 的連線管理直接相關：訂閱連線是長連線、數量應該受控，與短命的請求應答連線分開計量。&lt;/p>
&lt;p>訂閱連線斷線重連時，要重新 &lt;code>SUBSCRIBE&lt;/code> 所有 channel，且要意識到斷線期間的訊息已經永久丟失。可靠性敏感的設計會在重連後主動拉一次全量狀態，用一次 reconciliation 補上廣播漏掉的窗口。&lt;/p>
&lt;h2 id="cluster-下的-fan-out-與-sharded-pubsub">cluster 下的 fan-out 與 sharded Pub/Sub&lt;/h2>
&lt;p>在單節點與傳統 cluster 中，&lt;code>PUBLISH&lt;/code> 的訊息會傳播到 cluster 內所有節點，確保任何節點上的訂閱者都能收到。這個全傳播模型保證了廣播的完整性，但代價是每則訊息都要在節點間擴散，高頻發布時會佔用 cluster 內部頻寬。&lt;/p></description><content:encoded><![CDATA[<p>Redis <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a> 的核心責任是把一則訊息即時推送給當下所有訂閱者，讓跨節點的狀態變更可以在同一瞬間擴散。它承擔的是「現在發生的事，立刻讓所有人知道」，正式的可靠投遞與重播責任由 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams</a> 承擔。把這條邊界放在最前面，是因為 Pub/Sub 的多數事故都來自把它當成可靠訊息系統使用。</p>
<h2 id="at-most-once訊息只送給此刻在線的訂閱者">at-most-once：訊息只送給此刻在線的訂閱者</h2>
<p>訊息<a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">投遞語意</a>有三種：at-most-once（最多送一次、可能漏）、at-least-once（至少送一次、可能重複）、<a href="/blog/backend/knowledge-cards/exactly-once/" data-link-title="Exactly-Once" data-link-desc="說明訊息剛好被處理一次的語意承諾、它的代價，以及多數時候該用的替代路">exactly-once</a>（剛好一次、最難實作）。Pub/Sub 採 <a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">at-most-once</a>，用「可能漏」換取低延遲與無狀態，後兩種語意由 Streams 或 message queue 承擔。具體來說：<code>PUBLISH</code> 把訊息送給發布當下已經 <code>SUBSCRIBE</code> 該 channel 的連線，沒有訂閱者就直接丟棄，訊息不寫入任何持久結構。訂閱者離線、重連、或處理速度跟不上時，那段時間的訊息不會補送。</p>
<p>這個語意決定了 Pub/Sub 適合承擔什麼。可以接受「偶爾漏一則、下一則狀態會蓋過來」的場景，Pub/Sub 的低延遲與簡單模型是優勢；要求「每一則都不能掉」的場景，例如訂單事件、扣款通知、稽核軌跡，這些責任屬於 durable queue，不該放在 Pub/Sub。</p>
<p>判讀的關鍵問題是：漏掉一則訊息的代價是什麼。presence 狀態廣播漏一則，下次 heartbeat 會修正；cache invalidation 廣播漏一則，該節點會保留 stale 副本直到 TTL 到期，代價是短暫不一致；扣款事件漏一則，代價是金額錯誤且無法自動修復。前兩者落在 Pub/Sub 的能力範圍，第三者越界。</p>
<h2 id="適用場景狀態變更的即時扇出">適用場景：狀態變更的即時扇出</h2>
<p>Pub/Sub 的典型用途是把一個節點上發生的狀態變更，即時扇出給其他節點。這類場景的共同特徵是「最終狀態會自我修正」，所以單則訊息可丟。</p>
<p>fan-out 有兩種語意要先分清，因為它們決定能不能用 Pub/Sub。一種是全量 fan-out：每個訂閱者都收到同一則訊息的完整副本，適合「所有節點都要知道這件事」的廣播（presence、cache invalidation、config reload）。另一種是分攤 fan-out：同一則訊息只交給一個 consumer 處理、多個 consumer 之間分攤負載，適合「這件工作只要有一個人做」的任務分派。Pub/Sub 只提供全量 fan-out——<code>PUBLISH</code> 把訊息送給所有訂閱者，沒有「只給其中一個」的語意。需要分攤 fan-out 時要轉 Redis Streams 的 consumer group（<code>XREADGROUP</code> 讓一則訊息只有一個 consumer 拿到），這條邊界在本章末的升級段展開。</p>
<p>presence 變更廣播是最直接的應用。<a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store</a> 的 cross-node query 回答「現在誰在線」，但當某個使用者上線或離線時，其他節點需要被即時通知才能推播給好友列表。presence key 寫入時同步 <code>PUBLISH</code> 一則 <code>user:online</code> 訊息，訂閱該 channel 的節點立刻更新本地視圖。漏一則的代價是某個好友的線上狀態延遲幾秒，下次狀態同步會補正，落在可接受範圍。</p>
<p>cache invalidation 扇出是第二類應用。當一個節點更新了 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 並失效了自己的本地 cache，其他持有同一份 process-local cache 的節點需要被通知一起失效。<code>PUBLISH cache:invalidate product:123</code> 讓所有節點丟棄該 key 的本地副本。這條路徑要跟 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 的失效策略對齊：Pub/Sub 負責「通知」，實際失效仍由各節點執行，且因為 at-most-once，必須有 TTL 作為兜底，避免廣播漏送讓某節點永久持有 stale 副本。</p>
<p>即時配置熱刷新是第三類。feature flag、限流閾值、路由表這類低頻變更的配置，更新時 <code>PUBLISH config:reload</code>，各節點收到後重新拉取最新配置。低頻特性讓 at-most-once 風險很低，而即時性比輪詢配置中心更省資源。</p>
<h2 id="subscribe-的連線模型">SUBSCRIBE 的連線模型</h2>
<p>訂閱會把連線切換進專用模式：一旦 <code>SUBSCRIBE</code>，該連線只能再執行 <code>SUBSCRIBE</code>、<code>UNSUBSCRIBE</code>、<code>PING</code> 與訂閱相關命令，不能在同一條連線上跑 <code>GET</code>、<code>SET</code> 等一般命令。原因是訂閱連線進入了等待推送的狀態，伺服器隨時可能把訊息推過來，與請求應答式命令的時序會衝突。</p>
<p>這個模型的工程含義是：訂閱要用獨立的連線，不能跟一般讀寫共用同一個 client。共用連線池的應用要為 Pub/Sub 保留專門的訂閱連線，避免訂閱模式污染了拿來做 cache 讀寫的連線。這條限制跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發讀寫邊界</a> 的連線管理直接相關：訂閱連線是長連線、數量應該受控，與短命的請求應答連線分開計量。</p>
<p>訂閱連線斷線重連時，要重新 <code>SUBSCRIBE</code> 所有 channel，且要意識到斷線期間的訊息已經永久丟失。可靠性敏感的設計會在重連後主動拉一次全量狀態，用一次 reconciliation 補上廣播漏掉的窗口。</p>
<h2 id="cluster-下的-fan-out-與-sharded-pubsub">cluster 下的 fan-out 與 sharded Pub/Sub</h2>
<p>在單節點與傳統 cluster 中，<code>PUBLISH</code> 的訊息會傳播到 cluster 內所有節點，確保任何節點上的訂閱者都能收到。這個全傳播模型保證了廣播的完整性，但代價是每則訊息都要在節點間擴散，高頻發布時會佔用 cluster 內部頻寬。</p>
<p>sharded Pub/Sub（<code>SPUBLISH</code> / <code>SSUBSCRIBE</code>）把這個成本收斂：sharded channel 的訊息只在負責該 channel slot 的分片內傳播，不擴散到整個 cluster。代價是訂閱者必須連到正確的分片才能收到。判讀條件是發布頻率與 cluster 規模：低頻廣播用一般 Pub/Sub 換取部署簡單；高頻發布且 cluster 節點多時，sharded Pub/Sub 避免內部頻寬被廣播流量吃掉。<code>PUBSUB SHARDNUMSUB</code> 可以查某 shard channel 的訂閱者數，用來判讀扇出是否落在預期分片。</p>
<h2 id="keyspace-notifications把-key-事件變成廣播源">keyspace notifications：把 key 事件變成廣播源</h2>
<p>keyspace notifications 讓 Redis 在 key 發生變更（寫入、刪除、過期）時自動 <code>PUBLISH</code> 一則事件，訂閱者不必輪詢就能知道某個 key 變了。開啟後，<code>SET</code>、<code>DEL</code>、TTL 過期都會發出對應 channel 的訊息。</p>
<p>這個能力把 presence cleanup 變得更即時。<a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store</a> 的 cleanup 策略依賴 TTL 過期讓離線狀態消失，但「過期了」這件事本身可以透過 <code>__keyevent@0__:expired</code> 事件廣播出去，讓其他節點即時得知某連線下線，而不必等到下次查詢才發現。</p>
<p>keyspace notifications 同樣採 at-most-once 語意，且過期事件的觸發時機與 Redis 的惰性過期機制有關：key 在被存取或背景掃描到時才真正過期並發出事件。延遲量級取決於 key 下次被存取的時機與背景掃描週期（active expiry 預設每秒約執行 10 輪、每輪抽樣部分過期 key），最差情況下事件可能延遲數秒到數分鐘。需要精確過期時序的設計，仍要保留主動查詢路徑作為依據。</p>
<h2 id="何時從-pubsub-升級">何時從 Pub/Sub 升級</h2>
<p>Pub/Sub 的邊界訊號出現時，責任應該往 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams</a> 或正式 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue</a> 移動。判準是 durable 與 replayable 這兩個 Pub/Sub 不提供的能力。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>Pub/Sub 的限制</th>
          <th>該轉向的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訂閱者離線期間的訊息不能丟</td>
          <td>at-most-once、不持久化</td>
          <td>Redis Streams 的 <a href="/blog/backend/knowledge-cards/message-persistence/" data-link-title="Message Persistence" data-link-desc="說明訊息是否落盤保存，以及 broker 重啟後能否恢復">persistence</a> 與 consumer group</td>
      </tr>
      <tr>
          <td>需要重播歷史訊息</td>
          <td>訊息發布後即丟棄、無法回放</td>
          <td>Streams 的 ID 範圍讀取、message queue 的 replay</td>
      </tr>
      <tr>
          <td>需要確認訊息已被處理</td>
          <td>沒有 ack 機制</td>
          <td>Streams 的 <code>XACK</code>、queue 的 acknowledgement</td>
      </tr>
      <tr>
          <td>消費者失效時訊息要被接手</td>
          <td>訊息隨連線丟失</td>
          <td>Streams consumer group 的 pending list 與 claiming</td>
      </tr>
      <tr>
          <td>需要消費者群組分攤負載</td>
          <td>每個訂閱者都收到全部訊息</td>
          <td>Streams <code>XREADGROUP</code> 的單一 owner 語意</td>
      </tr>
  </tbody>
</table>
<p>Redis Streams 是介於 Pub/Sub 與重量級 broker 之間的選項：它持久化訊息、支援 consumer group 與 ack，又仍在 Redis 內，遷移成本低於引入 Kafka 或 RabbitMQ。Streams 與正式 message queue 的選型、consumer 設計、replay 邊界屬於 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 的責任，本章只負責標出「何時該離開 Pub/Sub」這條邊界。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訂閱者抱怨偶爾漏訊息</td>
          <td>at-most-once 在重連窗口丟訊息</td>
          <td>重連後補一次全量 reconciliation，或轉 Streams</td>
      </tr>
      <tr>
          <td>cluster 內部頻寬被廣播流量吃掉</td>
          <td>一般 Pub/Sub 全節點傳播成本過高</td>
          <td>改 sharded Pub/Sub、收斂傳播範圍</td>
      </tr>
      <tr>
          <td>訂閱連線數量隨流量無上限成長</td>
          <td>訂閱連線與一般讀寫連線混用</td>
          <td>分離訂閱連線池、獨立計量</td>
      </tr>
      <tr>
          <td>廣播漏送導致某節點長期 stale</td>
          <td>只靠 Pub/Sub 通知失效、缺 TTL 兜底</td>
          <td>補 TTL 作為失效兜底，廣播只當加速</td>
      </tr>
      <tr>
          <td>訂閱者跟不上發布、訊息靜默丟棄</td>
          <td>Pub/Sub 無 backpressure、發布方看不到消費積壓</td>
          <td>改 Streams（pending list 可量積壓）或限發布速率</td>
      </tr>
      <tr>
          <td>開始需要「這則處理了沒」的確認</td>
          <td>Pub/Sub 無 ack、責任已越界</td>
          <td>轉 Redis Streams 或正式 message queue</td>
      </tr>
  </tbody>
</table>
<p>訂閱者抱怨漏訊息時，先確認這是不是 at-most-once 的預期行為而非 bug。Pub/Sub 在訂閱者重連窗口丟訊息是設計而非故障，正確的修法是判斷這個場景能不能接受丟；能接受就保留 Pub/Sub 並補 reconciliation，不能接受就轉向 durable 方案。</p>
<p>廣播漏送導致長期 stale 之所以難防，是因為 cache invalidation 廣播在多數時候成功，讓人把失效當成可靠，直到某次漏送讓一個節點持有錯誤價格或權限數小時而沒有任何報錯。TTL 兜底的意義就是把「廣播失敗」的最壞影響限制在一個 TTL 週期內，把 Pub/Sub 定位成「加速失效」而非「保證失效」。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 Pub/Sub 當成可靠訊息系統，是最常見也代價最大的誤區。Pub/Sub 沒有持久化、沒有 ack、沒有重播，這些是它換取低延遲與簡單模型的設計取捨。需要這些能力時，正確做法是換工具，而不是在 Pub/Sub 外圍補一層補丁去模擬可靠投遞。</p>
<p>把訂閱連線跟一般讀寫連線共用，是第二個誤區。訂閱會讓連線進入專用模式，混用會讓 cache 讀寫命令在該連線上失敗或行為異常。訂閱連線要獨立管理。</p>
<p>只靠 Pub/Sub 廣播做 cache invalidation 而沒有 TTL 兜底，是第三個誤區。廣播的 at-most-once 特性意味著總有漏送的可能，TTL 是讓漏送影響有上界的保險。</p>
<h2 id="情境回寫">情境回寫</h2>
<p>Pub/Sub 的即時扇出語意，回寫到真實服務時最常見的形狀是多節點即時狀態同步。一個多區域部署的即時通訊服務，使用者上線狀態由所在區域的節點寫入，其他區域的節點需要即時得知才能更新好友列表的線上指示。這條路徑用 Pub/Sub 廣播狀態變更，回寫時要保留「跨區傳播有延遲窗口、單則訊息可丟、靠後續 heartbeat 收斂」的判讀，而非把它當成可靠投遞。</p>
<p>這個形狀支撐的是「即時廣播 + 最終狀態收斂」的判讀。若根因是訊息不能丟（狀態變更會觸發扣款、稽核或計費），應回到 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 的 durable 方案；模組三的 fan-out 案例（如 Twitch EventSub 用 SNS + SQS 扇出給第三方）記錄了 durable 扇出的設計，可在需要持久化與重播時對照。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.5 的交接：presence 狀態變更的廣播回到 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">presence store 與即時狀態</a>。</li>
<li>與 2.2 的交接：cache invalidation 扇出與 TTL 兜底回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 2.1 的交接：訂閱連線管理與一般讀寫連線分離回到 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">高併發下的 Redis 讀寫邊界</a>。</li>
<li>與模組三的交接：需要持久化、ack 與重播時轉向 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a> 與 Redis Streams。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看即時狀態本身如何建模與清理，回到 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store 與即時狀態</a>。要看廣播訊息升級成 durable 投遞後的 consumer 設計與重播邊界，接著讀 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a>。</p>
]]></content:encoded></item><item><title>5.10 Outbound Tunnel 入口與生命週期</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</guid><description>&lt;p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。&lt;/p>
&lt;h2 id="適用判斷">適用判斷&lt;/h2>
&lt;p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。&lt;/p>
&lt;p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。&lt;/p>
&lt;h2 id="tunnel-contract-組成">tunnel contract 組成&lt;/h2>
&lt;p>tunnel 入口合約跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer contract&lt;/a> 對照、差異集中在連線方向與就緒語意：&lt;/p>
&lt;ol>
&lt;li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。&lt;/li>
&lt;li>readiness contract：對外可達 = 隧道已建立 &lt;strong>且&lt;/strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。&lt;/li>
&lt;li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。&lt;/li>
&lt;li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。&lt;/li>
&lt;/ol>
&lt;h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊&lt;/h2>
&lt;p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。&lt;/p>
&lt;p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>;這層責任跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 的 startup / readiness / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。&lt;/p>
&lt;h2 id="穩態維持與重連策略">穩態維持與重連策略&lt;/h2>
&lt;p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。&lt;/p>
&lt;p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。&lt;/p>
&lt;h3 id="隧道多連線與冗餘">隧道多連線與冗餘&lt;/h3>
&lt;p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。&lt;/p>
&lt;p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。&lt;/p></description><content:encoded><![CDATA[<p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。</p>
<h2 id="適用判斷">適用判斷</h2>
<p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。</p>
<p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。</p>
<h2 id="tunnel-contract-組成">tunnel contract 組成</h2>
<p>tunnel 入口合約跟 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer contract</a> 對照、差異集中在連線方向與就緒語意：</p>
<ol>
<li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。</li>
<li>readiness contract：對外可達 = 隧道已建立 <strong>且</strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。</li>
<li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。</li>
<li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。</li>
</ol>
<h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊</h2>
<p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。</p>
<p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>;這層責任跟 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的 startup / readiness / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。</p>
<h2 id="穩態維持與重連策略">穩態維持與重連策略</h2>
<p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。</p>
<p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。</p>
<h3 id="隧道多連線與冗餘">隧道多連線與冗餘</h3>
<p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。</p>
<p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。</p>
<p>這個差異在穩定性預期上很重要：cloudflared 的可達性依賴 Cloudflare 邊緣網路的多點冗餘，Tailscale 的可達性依賴直連品質與 DERP 中繼。選擇時要問「我的網路環境是否穩定到不需要多連線冗餘」。</p>
<h2 id="故障模式network-層與-application-層的分離">故障模式：network 層與 application 層的分離</h2>
<p>tunnel 斷線跟 LB health check 失敗是不同層的故障。LB health check 失敗多半是 application 層（後端掛了、依賴不通）；tunnel 斷線常是 network 層（邊緣連線中斷、本機外連受阻、供應商側問題）、而後端服務本身完全健康。事故判讀要先分清這兩層：後端 log 一切正常、但外部全部連不進來、第一個要看的是 tunnel 進程的連線狀態、不是後端。</p>
<p>這也改變監控訊號的設計。LB 場景看後端 5xx 與 latency 就能覆蓋多數入口問題；tunnel 場景要額外監控隧道本身的連線狀態與重連次數——隧道靜默斷掉時、後端指標一片祥和、唯一的訊號在 tunnel 進程那邊。</p>
<h3 id="故障分類與判讀順序">故障分類與判讀順序</h3>
<p>tunnel 環境下的故障可按層級分類，判讀順序從外到內：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>症狀</th>
          <th>判讀第一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>供應商邊緣</td>
          <td>所有 tunnel 用戶同時受影響</td>
          <td>查供應商 status page</td>
      </tr>
      <tr>
          <td>本機外連</td>
          <td>單一 tunnel 斷線、其他外連也有問題</td>
          <td>查本機網路、NAT、防火牆</td>
      </tr>
      <tr>
          <td>tunnel 進程</td>
          <td>tunnel 進程 crash 或 hang</td>
          <td>查 tunnel 進程 log 與 restart 狀態</td>
      </tr>
      <tr>
          <td>後端服務</td>
          <td>tunnel 正常但外部拿到 502</td>
          <td>查後端服務 readiness</td>
      </tr>
      <tr>
          <td>認證閘道</td>
          <td>tunnel + 後端正常但外部拿到 403</td>
          <td>查認證設定（token / ACL 過期）</td>
      </tr>
  </tbody>
</table>
<p>判讀順序的重點是「先確認 tunnel 層是否正常、再往內看」。如果跳過 tunnel 層直接排查後端，會在後端 log 一切正常的情況下浪費時間。</p>
<h2 id="認證必須疊在-tunnel-之後">認證必須疊在 tunnel 之後</h2>
<p>tunnel 把後端的可達性開到了外部、但它不認證。隧道網址可能從瀏覽器紀錄、分享連結、Referer 外洩、不該被當成安全機制。所以 tunnel 之後必須疊認證閘道、且預設拒絕 — 未通過認證的流量不該觸及後端。</p>
<p>常見的疊法是邊緣與本機各一層：邊緣層（cloudflared 配 Cloudflare Access service token、Tailscale 配 ACL）讓未授權流量在邊緣就被擋、根本到不了本機;本機層（反向代理驗共享密鑰 / basic auth）作為邊緣萬一失效的縱深。入口威脅建模見 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>;單人自用工具的裝置綁定認證見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/#%e5%96%ae%e4%ba%ba%e8%a3%9d%e7%bd%ae%e8%aa%8d%e8%ad%89%e6%a8%a1%e5%9e%8b" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 單人裝置認證模型</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外部全部連不進來、後端 log 正常</td>
          <td>故障在 network 層、隧道斷線</td>
          <td>先查 tunnel 進程連線狀態、不是後端</td>
      </tr>
      <tr>
          <td>啟動後短時間外部拿到一批 502</td>
          <td>tunnel 比後端早 ready、導流量進空服務</td>
          <td>把 tunnel ready gate 在後端 readiness 後</td>
      </tr>
      <tr>
          <td>隧道頻繁重連、外部間歇中斷</td>
          <td>本機外連不穩或邊緣側抖動</td>
          <td>查 cloudflared / tailscaled 的重連 log、確認 backoff 間隔是否正常拉長</td>
      </tr>
      <tr>
          <td>拿到網址的人直接連到後端</td>
          <td>認證沒疊在 tunnel 之後、網址被當密碼</td>
          <td>補邊緣 / 本機認證閘道、預設拒絕</td>
      </tr>
      <tr>
          <td>部署切換隧道時對外中斷拉長</td>
          <td>關閉順序錯位、tunnel 未先收斂</td>
          <td>先停 tunnel 帶入新連線、再退後端</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 tunnel 網址當密碼、是最常見也最危險的誤判。網址不好猜不代表是祕密、它會從各種地方外洩、認證要靠 tunnel 之後的閘道、不是靠網址難猜。</p>
<p>把「後端健康」當成「外部可達」、忽略隧道本身是獨立的失效點。tunnel 場景的可達性是後端健康與隧道連線的交集、監控要覆蓋兩者。</p>
<p>把 tunnel 當「永久掛著」的常駐入口、放大暴露窗。自用場景常更適合用時起、用完關 — 暴露窗壓到最小;要常駐時、認證閘道與監控的投資等級要隨之上調。</p>
<p>把 tunnel 供應商視為零停機、不設本機降級預案。tunnel 依賴外部供應商的邊緣網路與協調伺服器，供應商事故期間本機服務完全健康但外部無法觸及。有降級需求的場景要準備替代入口路徑（如臨時開 port + 反向代理），或接受供應商 SLA 決定自身可用性。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 的交接：tunnel 的 startup / readiness / drain 對齊生命週期合約、只是 drain 對象換成隧道收斂。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 的交接：tunnel 作為對外入口的威脅建模與認證疊法。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a> 的交接：tunnel 憑證與認證閘道密鑰的保管與輪替。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 觀測</a> 的交接：隧道連線狀態與重連次數要進監控、否則 network 層故障無訊號。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 tunnel 入口放進整體生命週期、接著讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。要把 tunnel 之後的認證做紮實、接著讀 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 與 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>。判斷服務是否屬於個人自架工具形態、回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
]]></content:encoded></item><item><title>Firestore</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/</guid><description>&lt;p>Firestore 是 Google 的 serverless document database、承擔 mobile app 與 SPA 的正式狀態與多裝置即時同步責任。它的資料形狀是 collection 下的 document、存取模型是 client 端用 SDK 直連、授權靠 Security Rules，而不是經過自己寫的後端服務。Firestore 同時是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">Firebase&lt;/a> bundle 的資料層、也能在 Google Cloud 上單獨使用；本頁從&lt;strong>資料層 vendor 視角&lt;/strong>說明它承擔什麼狀態責任、為哪種查詢付成本、何時撞牆該遷往自建。要不要採用 BaaS 這種交付形態本身、是更上層的決策，見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 與 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;p>官方文件路由：&lt;a href="https://firebase.google.com/docs/firestore">Firestore documentation&lt;/a>、&lt;a href="https://firebase.google.com/docs/firestore/data-model">Firestore data model&lt;/a>、&lt;a href="https://firebase.google.com/docs/firestore/pricing">Firestore pricing&lt;/a>；本頁時間敏感的計費與限制 claim 以官方為準、最後檢查日 2026-06-16。&lt;/p>
&lt;h2 id="教學路線client-直連的-document-正式狀態">教學路線：client 直連的 document 正式狀態&lt;/h2>
&lt;p>Firestore 服務頁的教學目標是把「前端直接讀寫資料庫」這個存取模型的責任說清楚。讀者讀完後要能判斷 Firestore 何時是合適的正式狀態，何時因為查詢形狀、成本曲線或授權複雜度該轉向自建後端配 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> 或留在 document model 換 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a>。&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>Client-direct state&lt;/td>
 &lt;td>前端用 SDK 直連、授權下沉到 Security Rules 後責任邊界在哪&lt;/td>
 &lt;td>定位、存取模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Document shape&lt;/td>
 &lt;td>collection / document / subcollection 如何決定查詢能力&lt;/td>
 &lt;td>資料形狀、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query boundary&lt;/td>
 &lt;td>為什麼跨 collection 報表查不出來、index 與查詢限制如何約束建模&lt;/td>
 &lt;td>不適用場景、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Realtime / offline&lt;/td>
 &lt;td>snapshot listener 與 offline persistence 解哪類多裝置同步問題&lt;/td>
 &lt;td>適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>撞到報表、成本或授權牆時、遷往自建 relational 或換 document vendor&lt;/td>
 &lt;td>下一步路由、遷移 playbook&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位serverless-document-store--baas-資料層">定位：serverless document store + BaaS 資料層&lt;/h2>
&lt;p>Firestore 跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> 同屬 NoSQL document / KV 家族，但承擔的責任層級不同：&lt;/p>
&lt;ul>
&lt;li>資料組織成 collection 下的 document，document 可巢狀 subcollection，單 document 上限 1 MiB&lt;/li>
&lt;li>沒有 server 端 JOIN，跨 collection 的關聯要靠 application 多次查詢自己組、或在寫入時反正規化&lt;/li>
&lt;li>存取模型以 client SDK 直連為主，授權寫在 Security Rules（一套規則 DSL），而不是後端 API 的權限中介層&lt;/li>
&lt;li>兩種營運模式：Firestore Native mode（行動 / web、含 realtime 與 offline）與 Datastore mode（server 端、相容舊 Datastore）&lt;/li>
&lt;/ul>
&lt;p>傳統定位：Firebase 行動 app 與 SPA 的後端資料層、MVP 快速驗證期、多裝置即時同步的產品。&lt;/p></description><content:encoded><![CDATA[<p>Firestore 是 Google 的 serverless document database、承擔 mobile app 與 SPA 的正式狀態與多裝置即時同步責任。它的資料形狀是 collection 下的 document、存取模型是 client 端用 SDK 直連、授權靠 Security Rules，而不是經過自己寫的後端服務。Firestore 同時是 <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">Firebase</a> bundle 的資料層、也能在 Google Cloud 上單獨使用；本頁從<strong>資料層 vendor 視角</strong>說明它承擔什麼狀態責任、為哪種查詢付成本、何時撞牆該遷往自建。要不要採用 BaaS 這種交付形態本身、是更上層的決策，見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 與 <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>。</p>
<p>官方文件路由：<a href="https://firebase.google.com/docs/firestore">Firestore documentation</a>、<a href="https://firebase.google.com/docs/firestore/data-model">Firestore data model</a>、<a href="https://firebase.google.com/docs/firestore/pricing">Firestore pricing</a>；本頁時間敏感的計費與限制 claim 以官方為準、最後檢查日 2026-06-16。</p>
<h2 id="教學路線client-直連的-document-正式狀態">教學路線：client 直連的 document 正式狀態</h2>
<p>Firestore 服務頁的教學目標是把「前端直接讀寫資料庫」這個存取模型的責任說清楚。讀者讀完後要能判斷 Firestore 何時是合適的正式狀態，何時因為查詢形狀、成本曲線或授權複雜度該轉向自建後端配 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> 或留在 document model 換 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a>。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client-direct state</td>
          <td>前端用 SDK 直連、授權下沉到 Security Rules 後責任邊界在哪</td>
          <td>定位、存取模型</td>
      </tr>
      <tr>
          <td>Document shape</td>
          <td>collection / document / subcollection 如何決定查詢能力</td>
          <td>資料形狀、適用場景</td>
      </tr>
      <tr>
          <td>Query boundary</td>
          <td>為什麼跨 collection 報表查不出來、index 與查詢限制如何約束建模</td>
          <td>不適用場景、常見陷阱</td>
      </tr>
      <tr>
          <td>Realtime / offline</td>
          <td>snapshot listener 與 offline persistence 解哪類多裝置同步問題</td>
          <td>適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>撞到報表、成本或授權牆時、遷往自建 relational 或換 document vendor</td>
          <td>下一步路由、遷移 playbook</td>
      </tr>
  </tbody>
</table>
<h2 id="定位serverless-document-store--baas-資料層">定位：serverless document store + BaaS 資料層</h2>
<p>Firestore 跟 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a>、<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> 同屬 NoSQL document / KV 家族，但承擔的責任層級不同：</p>
<ul>
<li>資料組織成 collection 下的 document，document 可巢狀 subcollection，單 document 上限 1 MiB</li>
<li>沒有 server 端 JOIN，跨 collection 的關聯要靠 application 多次查詢自己組、或在寫入時反正規化</li>
<li>存取模型以 client SDK 直連為主，授權寫在 Security Rules（一套規則 DSL），而不是後端 API 的權限中介層</li>
<li>兩種營運模式：Firestore Native mode（行動 / web、含 realtime 與 offline）與 Datastore mode（server 端、相容舊 Datastore）</li>
</ul>
<p>傳統定位：Firebase 行動 app 與 SPA 的後端資料層、MVP 快速驗證期、多裝置即時同步的產品。</p>
<p>資料層視角的定位：一塊 <em>managed serverless document store</em>，把 capacity、replication、failover、scaling 全部交給平台，代價是查詢能力與資料模型沿平台特性生長。</p>
<h2 id="資料形狀與查詢邊界">資料形狀與查詢邊界</h2>
<p>Firestore 為「已知路徑的 document 讀寫」付成本，不為「任意欄位的 ad-hoc 查詢」付成本。這個取向決定了它的甜蜜區與牆：</p>
<ul>
<li>單 document 與單 collection 內的 key-based / 條件查詢高效，且每筆查詢都要有對應 index（單欄 index 自動建立、複合查詢要建 composite index）</li>
<li>查詢結果集的計費與大小跟「讀了幾筆 document」成正比，不是跟「掃了多少」— 一次回 10,000 筆就計 10,000 次 read</li>
<li>缺少 server 端 aggregation pipeline 與 JOIN；跨集合報表（例如「本月各地區訂單金額」）在 Firestore 上要嘛預先把彙總寫成一份 document、要嘛把資料複製到分析系統</li>
<li>沒有原生全文搜尋，全文需求要接專門的 <a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a>（Algolia、Elasticsearch / OpenSearch）</li>
</ul>
<p>這條查詢邊界是 Firestore 最容易被低估的設計約束。它不是「功能還沒做」，而是 client 直連 + serverless 計費模型的必然結果：把任意 ad-hoc 查詢開放給前端，等於把不可預測的成本與掃描壓力暴露在公網。建模時要先窮舉 access pattern、再決定 document 結構，跟 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a> 的 access-pattern-first 思路同源。</p>
<h2 id="一致性realtime-與容量特性">一致性、realtime 與容量特性</h2>
<p><strong>一致性</strong>：</p>
<ul>
<li>單 document 讀寫與「查詢結果在同一 region 內」提供 strong consistency</li>
<li>多 region 部署靠平台複製、跨 region 讀取可能有延遲；一致性語意由平台決定、不可調到自管資料庫那種 isolation level 顆粒</li>
</ul>
<p><strong>Realtime 與 offline</strong>：</p>
<ul>
<li>snapshot listener 讓 client 訂閱 query 結果、資料變更即時推送，是多裝置同步的核心能力</li>
<li>行動 / web SDK 內建 offline persistence，斷線時讀寫本地快取、回線後同步，這是自建 REST API 要額外工程才有的能力</li>
</ul>
<p><strong>容量與寫入熱點</strong>：</p>
<ul>
<li>serverless 自動擴縮，無 connection 概念，前端裝置數不直接轉成資料庫連線壓力</li>
<li>單一 document 的高頻寫入會撞到 contention（官方建議單 document 的持續寫入維持在每秒個位數量級、高頻計數器要用 distributed counter 分片）</li>
<li>寫入吞吐與索引維護成本綁在一起：每多一個 index、寫入就多一份維護成本</li>
</ul>
<p>容量特性的時間敏感數字（每秒寫入軟上限、單 document contention 門檻）以 <a href="https://firebase.google.com/docs/firestore/best-practices">官方 best practices</a> 為準，設計高頻寫入前先查當前限制。</p>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. 行動 app / SPA 的 MVP 後端</strong>：</p>
<ul>
<li>認證接 Firebase Auth、資料存 Firestore、推播接 Cloud Messaging，整個 MVP 沒有自己的後端服務</li>
<li>對應 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a> BaaS 段的「把後端工程師這個角色延後」</li>
</ul>
<p><strong>2. 多裝置即時同步</strong>：</p>
<ul>
<li>協作筆記、聊天、即時看板這類「一處改、多處即時更新」的產品</li>
<li>snapshot listener + offline persistence 是這類需求的天然形狀</li>
</ul>
<p><strong>3. access pattern 穩定的 document 工作負載</strong>：</p>
<ul>
<li>user profile、設定、feed item、活動紀錄這類讀多寫少、查詢路徑固定的資料</li>
<li>跟 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 對齊：Firestore 可以是這些資料的正式狀態</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 跨實體報表與分析查詢</strong>：</p>
<ul>
<li>跨 collection JOIN、ad-hoc 篩選、彙總統計在 Firestore 上要靠資料複製工程</li>
<li>替代：自建 relational（<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>）或把資料同步進分析系統</li>
</ul>
<p><strong>2. 成本對流量敏感的高讀取場景</strong>：</p>
<ul>
<li>計費隨 document read / write / delete 線性成長，高流量下可能超過自建</li>
<li>替代：自管資料庫 + 應用層 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">cache</a>，把熱讀取的單位成本壓下來</li>
</ul>
<p><strong>3. 複雜授權需要可測試的控制面</strong>：</p>
<ul>
<li>client 直連模型把授權全塞進 Security Rules，規則長到難以 review / 測試時，控制面風險升高</li>
<li>替代：把授權拉回後端 API 中介層（自建後端 + 任意資料庫）</li>
</ul>
<p><strong>4. 強一致的多實體交易</strong>：</p>
<ul>
<li>Firestore 有 transaction 與 batch write，但跨大量 document 的複雜交易不是它的主場</li>
<li>替代：relational database 的多表交易</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs MongoDB（document 對 document）</strong>：</p>
<ul>
<li>Firestore：serverless、client 直連、realtime listener、GCP / Firebase 綁定、查詢能力受限</li>
<li>MongoDB：查詢與 aggregation 彈性高、跨雲、要自管或用 Atlas managed、走後端中介存取</li>
<li>選 Firestore：行動 / 即時同步 / 想省整層後端</li>
<li>選 MongoDB：document model 但要彈性查詢、aggregation、跨雲可攜，見 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">db3 vendor selection</a></li>
</ul>
<p><strong>vs DynamoDB（serverless NoSQL 對 serverless NoSQL）</strong>：</p>
<ul>
<li>Firestore：GCP / Firebase 生態、內建 realtime 與 offline、client 直連為主</li>
<li>DynamoDB：AWS 生態、access-pattern-first KV、通常走後端整合、streams 接事件驅動</li>
<li>兩者的 access-pattern-first 建模思路相近，差別在生態與 client 直連的有無</li>
</ul>
<p><strong>vs SQLite（行動端的反向選擇）</strong>：</p>
<ul>
<li>Firestore：雲端 store、自動多裝置 sync、realtime</li>
<li>SQLite：embedded、offline-first、無 sync（見 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor</a>）</li>
<li>選 Firestore：需要跨裝置同步與即時更新</li>
<li>選 SQLite：純單機 / offline、不需要雲端同步</li>
</ul>
<p><strong>vs Supabase（BaaS bundle 的另一條路）</strong>：</p>
<ul>
<li>Firestore：document model、Google 的 BaaS bundle 資料層</li>
<li>Supabase：底層是 PostgreSQL（relational）、開源 BaaS bundle，遷出時資料是標準 SQL</li>
<li>兩者都是 client 直連 + 規則授權的 BaaS 形狀，差別在資料模型（document vs relational）與遷出時的資料可攜性；Supabase 的資料層判讀見 <a href="/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/" data-link-title="Managed PostgreSQL Comparison" data-link-desc="RDS PostgreSQL、Aurora PostgreSQL、Cloud SQL、Azure Database for PostgreSQL、Neon、Supabase、Crunchy Bridge 的責任邊界比較">Managed PostgreSQL 比較</a>，選型層錨點見 <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</a></li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. access pattern 先於 document 結構</strong>：</p>
<ul>
<li>列出 application 對資料的所有讀寫路徑、再設計 collection / document 形狀</li>
<li>access pattern 沒想清楚就建模，後面報表查不出來要重做</li>
</ul>
<p><strong>2. 反正規化換查詢效率</strong>：</p>
<ul>
<li>為了避免跨 collection 多次查詢，常把關聯資料冗餘寫進同一 document</li>
<li>代價是寫入時要維護多份副本的一致性，對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a></li>
</ul>
<p><strong>3. index 與寫入成本綁定</strong>：</p>
<ul>
<li>複合查詢要先建 composite index、否則查詢直接失敗</li>
<li>每個 index 增加寫入維護成本，移除用不到的 index 是容量優化的一環</li>
</ul>
<p><strong>4. 高頻寫入用 distributed counter</strong>：</p>
<ul>
<li>單一 document 撞到 contention 上限時，把計數拆成多個 shard document 再彙總</li>
</ul>
<p><strong>5. 成本以 document 數計，不以掃描量計</strong>：</p>
<ul>
<li>容量估算要算「每個畫面 / API 觸發幾次 read」、乘上日活與頻率</li>
<li>把熱讀取移到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">應用層快取</a> 是壓低 read 計費的主要手段</li>
</ul>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>把 Firestore 當關聯式用</strong>：規劃了一堆需要 JOIN 的 collection、上線後跨集合查詢全靠 client 自己組、latency 與 read 成本爆炸</li>
<li><strong>報表需求到了才發現查不出來</strong>：老闆要月報、Firestore 沒有 aggregation pipeline、被迫臨時搭資料複製管線</li>
<li><strong>Security Rules 長到沒人敢改</strong>：授權全寫在規則 DSL、沒有版本控制與測試、變更時靠人工推敲</li>
<li><strong>單 document 當高頻計數器</strong>：直播按讚 / 即時計數寫爆單一 document 的 contention 上限</li>
<li><strong>忽略 read 計費規模</strong>：list 畫面一次回上千筆、每次重整都計上千次 read、帳單月底才浮現</li>
</ul>
<h2 id="deep-article-章節群">Deep article 章節群</h2>
<p>Firestore overview 負責第一輪服務判斷；vendor 特有機制的設定、踩坑與容量規劃拆成 deep article。下表是目前已建立的實作層教材，讀法是先讀 overview 判斷服務適配，再按撞到的壓力選 deep article。</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>文件</th>
          <th>教學責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>授權控制面</td>
          <td><a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模與可測試化</a></td>
          <td>規則求值模型、可組合 function、emulator 單元測試、把規則當程式碼治理</td>
      </tr>
      <tr>
          <td>高頻寫入</td>
          <td><a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">高頻寫入與 distributed counter</a></td>
          <td>單 document contention 邊界、分片計數、shard 數與讀寫成本取捨</td>
      </tr>
      <tr>
          <td>資料建模</td>
          <td><a href="/blog/backend/01-database/vendors/firestore/denormalization-fanout-consistency/" data-link-title="Firestore document 反正規化與一致性維護：fan-out write、副本同步與資料修復" data-link-desc="Firestore 沒有 JOIN，查詢能力逼著把關聯資料反正規化複製多份；本文展開反正規化的建模決策、fan-out write 維護副本一致、batch 與 transaction 的選擇、五個副本不一致的 production 踩坑，以及反正規化複雜到該回關聯式的邊界">document 反正規化與一致性維護</a></td>
          <td>反正規化決策、fan-out write、副本同步、不一致修復</td>
      </tr>
      <tr>
          <td>即時同步</td>
          <td><a href="/blog/backend/01-database/vendors/firestore/realtime-listener-fanout-cost/" data-link-title="Firestore realtime listener 扇出與成本：snapshot 訂閱、re-read 計費與連線規模" data-link-desc="Firestore 的 snapshot listener 提供即時同步、但訂閱的扇出、查詢結果變動的 re-read 計費與連線數會在規模下變成成本與效能瓶頸；本文展開 listener 的推送模型、訂閱範圍設計、五個 realtime 成本踩坑，以及即時需求超過 listener 該換推送架構的邊界">realtime listener 扇出與成本</a></td>
          <td>snapshot 推送模型、訂閱範圍設計、re-read 計費、連線規模</td>
      </tr>
  </tbody>
</table>
<p>讀法路由：撞到資料外洩 / 越權，讀 Security Rules；撞到熱門事件寫爆計數，讀 distributed counter；改一筆要連動改一千筆，讀反正規化；即時功能帳單失控，讀 realtime listener。撞到報表 / 成本 / 授權整體性的牆，走 <a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">遷往自建 relational</a>。</p>
<h2 id="hands-on-操作演練">Hands-on 操作演練</h2>
<p>deep article 講機制判讀，<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Hands-on 操作路線</a> 把機制轉成可在本地 <a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator</a> 跑的演練——零雲端成本、可重跑、產出可驗證 artifact。三個 lab：<a href="/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">emulator quickstart</a>（建立共用環境）、<a href="/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab</a>（規則自動化測試 + 接 release gate）、<a href="/blog/backend/01-database/vendors/firestore/hands-on/distributed-counter-lab/" data-link-title="Firestore Distributed Counter Lab" data-link-desc="在 emulator 上實作 distributed counter：建立 N 個 shard、隨機分片寫入、觀察 shard 分佈是否均勻、讀取彙總驗證總和正確，並說明 contention 本身是 emulator 不模擬的 production 特性">distributed counter lab</a>（分片計數機制驗證）。lab 全程標明 emulator 驗得了什麼（功能行為、規則求值）、驗不了什麼（計費、寫入軟上限要回雲端）。</p>
<h2 id="已知-limitation-與後續路由">已知 limitation 與後續路由</h2>
<p>Firestore overview 完成服務判斷、資料形狀、查詢邊界與替代路由；deep article 章節群覆蓋授權、高頻寫入、反正規化與即時同步四個機制；hands-on 章節群提供 emulator 演練。後續可補的方向：offline persistence 的衝突解決深入、realtime listener 在雲端的成本量測 lab（emulator 不計費、要在雲端 staging 跑）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>同類對比：<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a>（彈性查詢 document）/ <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 vendor</a>（access-pattern-first KV）/ <a href="/blog/backend/01-database/vendors/db3-vendor-selection/" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">db3 vendor selection</a>（document / KV / multi-model 三方選型）</li>
<li>遷出方向：<a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a>（撞到報表 / 成本 / 授權牆後的 Type E 重建模 playbook）</li>
<li>操作演練：<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on</a>（emulator quickstart、Security Rules 測試、distributed counter lab）</li>
<li>容量背景：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>選型上層：<a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> / <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> / <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS 知識卡</a></li>
<li>從託管平台遷出的資產線盤點：<a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a></li>
<li>官方：<a href="https://firebase.google.com/docs/firestore">Firestore documentation</a>、<a href="https://firebase.google.com/docs/firestore/best-practices">Firestore best practices</a>、<a href="https://firebase.google.com/docs/firestore/pricing">Firestore pricing</a></li>
</ul>
]]></content:encoded></item><item><title>OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry&lt;/a> 的 vendor deep article，深化 overview「Collector 部署模式」段。初次接觸 OpenTelemetry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁&lt;/a>，再回到本文。指令於 2026-06-16 用 &lt;code>otel/opentelemetry-collector-contrib:0.154.0&lt;/code> 在 docker 實機驗證。&lt;/p>&lt;/blockquote>
&lt;p>應用程式產生的 telemetry 跟最終存放的 backend 之間需要一個中介層 — OTel Collector 就是這個中介。應用只負責用 OTLP 把資料吐給 collector，collector 負責接收、處理、轉發，兩邊解耦。部署這個 collector 的第一個決策是它擺在哪裡（同 host、集中 gateway、還是 pod sidecar），而非配置細節。位置決定了 buffer 能力、enrichment 時機與失效影響面。&lt;/p>
&lt;h2 id="問題情境telemetry-直送-backend-的三個代價">問題情境：telemetry 直送 backend 的三個代價&lt;/h2>
&lt;p>應用程式直接用 vendor SDK 把 telemetry 送到後端，會在規模變大時撞到三個問題。第一是耦合：每個服務都寫死了某個 backend 的 endpoint 與認證，換 backend 要改所有服務重新部署。第二是缺乏 buffer：backend 短暫不可用時，telemetry 直接丟失，因為應用程式不會為了觀測資料保留重試佇列。第三是 enrichment 分散：每個服務各自加 resource attribute、各自做 sampling，標準難統一。&lt;/p>
&lt;p>Collector 把這三件事收斂到一個中介層。應用只認 collector 的 OTLP endpoint，換 backend 只改 collector 配置；collector 有 queue 與重試；enrichment 與 sampling 在 collector 統一做。但這個中介層擺在哪裡，決定了它各自解掉多少。&lt;/p>
&lt;p>服務數少、backend 單一且穩定時，應用直送 backend 是合理起點 — 上述三個代價在小規模下可控。Collector 是規模化後的升級：當 backend 要換、服務數成長到 enrichment 要統一、或 sampling 需求出現時，再引入 collector 補這一層。&lt;/p>
&lt;h2 id="核心概念三種部署位置的責任分工">核心概念：三種部署位置的責任分工&lt;/h2>
&lt;p>Collector 的部署位置分三種，差別在「離應用多近」與「聚合多少來源」。&lt;/p>
&lt;p>Agent 模式把 collector 跟應用程式放在同一個 host 或同一個 K8s node（DaemonSet）。它的責任是做 local buffer 與 host 層 enrichment：應用透過 localhost 把 telemetry 吐給同機的 collector，延遲極低、不跨網路；collector 補上 host name、container id 這類只有在本機才知道的 resource attribute。agent 的價值是「離應用最近」，應用送出 telemetry 後就不必管後續，buffer 與重試由同機 collector 承擔。&lt;/p>
&lt;p>Agent 解了「離應用近、不丟資料」的問題，但它只看得到本機 — 需要全域視野的處理放不進去。Gateway 模式補這一塊：把 collector 集中部署成一個獨立的服務叢集，跨多個 agent 或多個應用接收 telemetry，負責需要全域視野的處理：tail-based sampling（要看完整 trace 才決定採不採）、跨來源的 routing（不同 telemetry 送不同 backend）、集中的 rate limit 與成本控制。gateway 的價值是「集中決策」，把只有匯流後才做得到的處理放在這一層。&lt;/p>
&lt;p>Sidecar 模式在 K8s 把 collector 當成跟應用 pod 同生命週期的 sidecar container。它的責任跟 agent 相似（local buffer、pod 層 enrichment），差別在隔離粒度是 pod 而非 node：比 DaemonSet agent 更貼近單一 pod（共享 pod 網路、隨 pod 起停），適合需要 pod 級獨立配置或強隔離的場景，代價是每個 pod 都多一份 collector 的資源開銷。&lt;/p>
&lt;p>常見部署是兩層組合：agent（DaemonSet）做 local buffer + host enrichment，再把資料送到 gateway 叢集做 tail sampling 與 routing。agent 解掉「離應用近、不丟資料」，gateway 解掉「需要全域視野的處理」，兩層各司其職。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> 的 vendor deep article，深化 overview「Collector 部署模式」段。初次接觸 OpenTelemetry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁</a>，再回到本文。指令於 2026-06-16 用 <code>otel/opentelemetry-collector-contrib:0.154.0</code> 在 docker 實機驗證。</p></blockquote>
<p>應用程式產生的 telemetry 跟最終存放的 backend 之間需要一個中介層 — OTel Collector 就是這個中介。應用只負責用 OTLP 把資料吐給 collector，collector 負責接收、處理、轉發，兩邊解耦。部署這個 collector 的第一個決策是它擺在哪裡（同 host、集中 gateway、還是 pod sidecar），而非配置細節。位置決定了 buffer 能力、enrichment 時機與失效影響面。</p>
<h2 id="問題情境telemetry-直送-backend-的三個代價">問題情境：telemetry 直送 backend 的三個代價</h2>
<p>應用程式直接用 vendor SDK 把 telemetry 送到後端，會在規模變大時撞到三個問題。第一是耦合：每個服務都寫死了某個 backend 的 endpoint 與認證，換 backend 要改所有服務重新部署。第二是缺乏 buffer：backend 短暫不可用時，telemetry 直接丟失，因為應用程式不會為了觀測資料保留重試佇列。第三是 enrichment 分散：每個服務各自加 resource attribute、各自做 sampling，標準難統一。</p>
<p>Collector 把這三件事收斂到一個中介層。應用只認 collector 的 OTLP endpoint，換 backend 只改 collector 配置；collector 有 queue 與重試；enrichment 與 sampling 在 collector 統一做。但這個中介層擺在哪裡，決定了它各自解掉多少。</p>
<p>服務數少、backend 單一且穩定時，應用直送 backend 是合理起點 — 上述三個代價在小規模下可控。Collector 是規模化後的升級：當 backend 要換、服務數成長到 enrichment 要統一、或 sampling 需求出現時，再引入 collector 補這一層。</p>
<h2 id="核心概念三種部署位置的責任分工">核心概念：三種部署位置的責任分工</h2>
<p>Collector 的部署位置分三種，差別在「離應用多近」與「聚合多少來源」。</p>
<p>Agent 模式把 collector 跟應用程式放在同一個 host 或同一個 K8s node（DaemonSet）。它的責任是做 local buffer 與 host 層 enrichment：應用透過 localhost 把 telemetry 吐給同機的 collector，延遲極低、不跨網路；collector 補上 host name、container id 這類只有在本機才知道的 resource attribute。agent 的價值是「離應用最近」，應用送出 telemetry 後就不必管後續，buffer 與重試由同機 collector 承擔。</p>
<p>Agent 解了「離應用近、不丟資料」的問題，但它只看得到本機 — 需要全域視野的處理放不進去。Gateway 模式補這一塊：把 collector 集中部署成一個獨立的服務叢集，跨多個 agent 或多個應用接收 telemetry，負責需要全域視野的處理：tail-based sampling（要看完整 trace 才決定採不採）、跨來源的 routing（不同 telemetry 送不同 backend）、集中的 rate limit 與成本控制。gateway 的價值是「集中決策」，把只有匯流後才做得到的處理放在這一層。</p>
<p>Sidecar 模式在 K8s 把 collector 當成跟應用 pod 同生命週期的 sidecar container。它的責任跟 agent 相似（local buffer、pod 層 enrichment），差別在隔離粒度是 pod 而非 node：比 DaemonSet agent 更貼近單一 pod（共享 pod 網路、隨 pod 起停），適合需要 pod 級獨立配置或強隔離的場景，代價是每個 pod 都多一份 collector 的資源開銷。</p>
<p>常見部署是兩層組合：agent（DaemonSet）做 local buffer + host enrichment，再把資料送到 gateway 叢集做 tail sampling 與 routing。agent 解掉「離應用近、不丟資料」，gateway 解掉「需要全域視野的處理」，兩層各司其職。</p>
<h2 id="pipeline-模型receivers--processors--exporters">pipeline 模型：receivers / processors / exporters</h2>
<p>不論擺在哪個位置，collector 的內部都是同一個 pipeline 模型：telemetry 從 receivers 進來、經過 processors 加工、由 exporters 送出。三者用 <code>service.pipelines</code> 依訊號類型（traces / metrics / logs）串接。以下是最小可驗證配置，三個區塊（receivers / processors / exporters）對應 pipeline 的三個階段，各自職責在後面逐段說明。這份配置在 docker 驗證過可正常啟動並端到端流通（<code>validate --config</code> 回傳 0、送 5 條 trace 後 debug exporter 完整輸出 spans）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">receivers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">otlp</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">protocols</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span><span class="nt">grpc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4317</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">processors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">memory_limiter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">check_interval</span><span class="p">:</span><span class="w"> </span><span class="l">1s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">limit_mib</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">spike_limit_mib</span><span class="p">:</span><span class="w"> </span><span class="m">64</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">batch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">send_batch_size</span><span class="p">:</span><span class="w"> </span><span class="m">1024</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="nt">exporters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">debug</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">verbosity</span><span class="p">:</span><span class="w"> </span><span class="l">detailed</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span><span class="nt">pipelines</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">traces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">receivers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">otlp]</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">processors</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">memory_limiter, batch]</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span><span class="nt">exporters</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">debug]</span></span></span></code></pre></div><p>receivers 定義「資料怎麼進來」，OTLP（gRPC 4317 / HTTP 4318）是標準入口。processors 定義「資料怎麼加工」，順序有意義：<code>memory_limiter</code> 放最前面，先擋住記憶體爆掉；<code>batch</code> 放後面，把零散 span 攢成批次再送，降低下游請求數。此處 256 / 64 MiB 是 demo 用量，production 應依 container memory limit 按比例設定（常見做法是 limit_mib 設為 container memory 的 80%、spike 設為 limit 的 20-25%）。exporters 定義「資料送到哪」，正式環境會是 OTLP 到 backend 或某 vendor exporter，這裡用 <code>debug</code> 驗證流通。service.pipelines 才是真正生效的接線：只有被掛進某個 pipeline 的元件才會運作，定義了卻沒掛進 pipeline 的元件不生效。</p>
<p>processor 順序是常見踩雷點。<code>memory_limiter</code> 要排在第一個，讓它在資料進入後續 processor 前就有機會審查與拒收；<code>batch</code> 排在它之後，因為如果 batch 先跑，telemetry 會先在 batch processor 累積成大批，等觸發記憶體限制時壓力已經更高、拒收效果下降。需要 sampling 時，head sampling 可以放 agent 層的 pipeline，tail sampling 必須放 gateway 層（它要匯流完整 trace），且同一 trace 的所有 span 要路由到同一個 gateway 實例（用 trace-id 維度的 load balancing exporter），否則各 gateway 節點各看片段、tail 決策仍不完整。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<p>Collector 失效的影響面取決於部署模式，這是選位置時要先想清楚的。agent 模式下，單一 node 的 collector 掛掉只影響該 node 的應用，且應用送往 localhost 失敗可以 fail-fast；gateway 模式下，gateway 叢集掛掉會影響所有上游 agent，因此 gateway 必須多副本 + 負載均衡，不能單點；sidecar 模式下，失效影響面比 agent 更窄（只影響同 pod 的應用），但每個 pod 各自是獨立失效點，pod 數多時同時出狀況的機率也高。演練時要分別注入「單 agent 掛」與「gateway 叢集不可用」，確認前者影響被局限、後者有 agent 層 buffer 兜著。</p>
<p>記憶體壓力是 collector 最常見的故障。telemetry 流入速度超過 exporter 送出速度時，資料在 collector 內累積、記憶體上升，沒有保護會 OOM 被 kill、整段 telemetry 全丟。<code>memory_limiter</code> processor 是這道防線，它定期（<code>check_interval</code>）檢查記憶體並用兩個閾值分級反應：記憶體超過軟上限（<code>limit_mib</code> 減去 <code>spike_limit_mib</code>）時強制觸發 GC 並開始拒收，給回收一個緩衝區間；超過硬上限（<code>limit_mib</code>）時全面拒收新資料。只設 <code>limit_mib</code>、不設 <code>spike_limit_mib</code> 是不完整的配置，等於沒有軟性緩衝、直接撞硬牆。演練時用高於 exporter 吞吐的速率灌資料，確認 memory_limiter 在軟上限就介入、collector 存活，而不是 OOM。</p>
<p>Backpressure 的傳遞要驗證到底。當 backend 變慢、exporter queue 滿，collector 的 OTLP receiver 會回壓給上游（gRPC 層用 resource-exhausted 拒收）。在 agent 模式這個回壓會傳到應用的 OTLP exporter，應用 SDK 的 queue 也會滿——此時 SDK 的反應取決於 exporter 配置，要確認 queue-full 策略設為 drop 而非 block，讓 telemetry 被丟棄而非阻塞業務執行緒（各語言 SDK 預設不同，不能假設一定是 drop）。演練要確認「backend 慢 → collector 回壓 → 應用丟 telemetry 但業務不受影響」這條鏈成立，避免觀測系統的壓力反噬主流程。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>判讀</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>collector 容器頻繁 OOM restart</td>
          <td>memory_limiter 閾值過高或未啟用</td>
          <td>調低 limit_mib、確認 spike_limit_mib 有設</td>
      </tr>
      <tr>
          <td>exporter queue depth 持續飽和</td>
          <td>下游 backend 回應慢或不可用</td>
          <td>查 backend 狀態、確認 exporter retry 與 timeout 設定</td>
      </tr>
      <tr>
          <td>receiver refused spans 計數上升</td>
          <td>memory_limiter 啟動拒收、collector 處於壓力狀態</td>
          <td>查上游流量是否異常、考慮擴容 gateway 或調降 sampling</td>
      </tr>
      <tr>
          <td>gateway 全部不可用、agent buffer 開始丟棄</td>
          <td>全域 telemetry 中斷</td>
          <td>確認 gateway 多副本與負載均衡、agent 的 queue 與 drop 策略</td>
      </tr>
      <tr>
          <td>telemetry 到 backend 有延遲但不丟失</td>
          <td>batch processor 正常攢批</td>
          <td>正常行為、確認 batch timeout 符合預期</td>
      </tr>
  </tbody>
</table>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>agent 與 gateway 的成本曲線不同，選型要對著規模看。agent（DaemonSet）的成本是「每個 node 一份 collector」的固定開銷：node 多時總開銷隨 node 數線性成長，但每份 collector 只處理本機流量、單份負載可控。gateway 的成本是「集中叢集」：份數少但每份要扛匯流後的總流量，要按總 telemetry 吞吐量做容量規劃與水平擴展。</p>
<p>兩層架構的成本判讀是：agent 層用最小配置（夠做 buffer + enrichment 即可，<code>limit_mib</code> 設小），把重處理（tail sampling、大量 routing）集中到 gateway，讓 gateway 的擴展跟總流量綁定、agent 的開銷跟 node 數綁定。把 tail sampling 誤放在 agent 層是常見的成本錯誤——agent 看不到完整 trace、做不了正確的 tail sampling，還白白吃掉每個 node 的記憶體。</p>
<p>gateway 層的 processor 是攔截高 cardinality attribute 的有效位置：在 telemetry 流入 backend 前用 <code>attributes</code> / <code>transform</code> processor 把高 cardinality label（user id、request id 當 metric label）移除或降維，比讓它流到 backend 後才治理便宜。高 cardinality 的 attribute 會在下游 backend 炸開成本，是另一條要在 collector 攔截的成本線。這條跟 <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> 對齊。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Collector 部署模式是 OTel 落地的第一個決策，它的下游是 sampling 策略與 backend 選型。決定了 agent + gateway 兩層後，tail sampling 的設計接到 gateway 層的 pipeline；exporter 指向哪個 backend 則回到 <a href="/blog/backend/04-observability/vendors/opentelemetry/#%e4%bd%95%e6%99%82%e6%94%b9%e8%b5%b0%e5%85%b6%e4%bb%96%e6%9c%8d%e5%8b%99" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">何時改走其他服務</a> 的 vendor portability 判讀。</p>
<p>pipeline 的訊號治理與資料品質回到 <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> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>；cardinality 攔截回到 <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>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁</a></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></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></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 link</a></li>
</ul>
]]></content:encoded></item><item><title>模組十：系統演進與遷移</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/</guid><description>&lt;p>系統演進與遷移的核心目標是把高風險的執行變更從設計時的選型判斷分離出來、用獨立的紀律處理。模組零回答「該選哪個服務」、模組十回答「決定要改之後、怎麼安全動手」。兩者的失敗模式不同 — 選型錯了重來成本是「再評估一次」、遷移錯了重來成本可能是「事件、資料損失、回退耗時數週」。&lt;/p>
&lt;h2 id="跟模組零的責任分工">跟模組零的責任分工&lt;/h2>
&lt;p>模組零（服務選型）處理設計階段：辨識需求、比較能力、決定要不要引入某類服務。模組十（系統演進）處理執行階段：拆服務、跨服務重構、schema 大型變更、雲端切換、capacity ramp 的劇本與回退條件。兩者銜接點是「決策完成、執行待動」 — 模組零的結論「應該拆某個服務」進到模組十、變成「怎麼拆、用什麼 pattern、何時切流、回退條件是什麼」。&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>起點訊號&lt;/td>
 &lt;td>需求分類、流量形狀、團隊能力&lt;/td>
 &lt;td>已決定要動、評估完成&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要產物&lt;/td>
 &lt;td>選型表、能力對照、取捨判讀&lt;/td>
 &lt;td>執行劇本、切流策略、回退條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗代價&lt;/td>
 &lt;td>選錯方向、回頭再評估&lt;/td>
 &lt;td>切流失敗、資料損失、事件影響使用者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工具語言&lt;/td>
 &lt;td>mental model、taxonomy、capability&lt;/td>
 &lt;td>runbook、cutover、rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跟其他模組的邊界">跟其他模組的邊界&lt;/h2>
&lt;p>模組十收的是「跨服務、跨模組、跨環境的演進劇本」、不是「該技術的小範圍變更」。常見的容易誤判邊界：&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>schema migration 語法、index 設計、rollout&lt;/td>
 &lt;td>01 留&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>schema 跨多 release 的 zero-downtime 切換&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（未來、Strangler Fig 跨服務替換）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache aside / TTL / eviction&lt;/td>
 &lt;td>02 留&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache 大型 vendor 切換（自建 → 雲服務）&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（未來）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>service 拆分判讀&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（10.1）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>service 拆分執行 runbook&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（10.2）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>雲端能力對照（AWS / GCP / Azure）&lt;/td>
 &lt;td>00 留（0.19）&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨雲遷移執行劇本&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（未來）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>9.x 擴展軸、容量規劃&lt;/td>
 &lt;td>09 留&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付形態該不該遷、升級 tripwire 判讀&lt;/td>
 &lt;td>00 留（0.21）&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>託管形態遷出的執行劇本&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（10.3）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>拆分後造成的容量重平衡 runbook&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>模組十收（未來）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題是「這個變更失敗時、回退範圍跨幾個服務 / 模組？」。跨多模組的演進劇本進模組十、單模組內的小範圍變更留原模組。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1&lt;/a>&lt;/td>
 &lt;td>服務拆分與邊界判讀&lt;/td>
 &lt;td>整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2&lt;/a>&lt;/td>
 &lt;td>服務拆分執行 Runbook&lt;/td>
 &lt;td>10.1 決定該拆之後、實際怎麼動手 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &amp;#43; 逐步遷移 &amp;#43; 最終下架』取代 big bang 重寫">Strangler Fig&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">雙寫期&lt;/a> 管理、切流策略、回退條件設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3&lt;/a>&lt;/td>
 &lt;td>託管形態遷出&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21&lt;/a> 升級自建 tripwire 觸發之後、從託管平台 / BaaS 遷往自建的執行 — 資料 / 身分 / 流量 / 整合的資產線盤點、並行期與回切窗口、部分遷出中繼形態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="後續擴充方向">後續擴充方向&lt;/h2>
&lt;p>本模組目前收服務拆分與託管形態遷出議題。未來會擴充的演進類議題：&lt;/p></description><content:encoded><![CDATA[<p>系統演進與遷移的核心目標是把高風險的執行變更從設計時的選型判斷分離出來、用獨立的紀律處理。模組零回答「該選哪個服務」、模組十回答「決定要改之後、怎麼安全動手」。兩者的失敗模式不同 — 選型錯了重來成本是「再評估一次」、遷移錯了重來成本可能是「事件、資料損失、回退耗時數週」。</p>
<h2 id="跟模組零的責任分工">跟模組零的責任分工</h2>
<p>模組零（服務選型）處理設計階段：辨識需求、比較能力、決定要不要引入某類服務。模組十（系統演進）處理執行階段：拆服務、跨服務重構、schema 大型變更、雲端切換、capacity ramp 的劇本與回退條件。兩者銜接點是「決策完成、執行待動」 — 模組零的結論「應該拆某個服務」進到模組十、變成「怎麼拆、用什麼 pattern、何時切流、回退條件是什麼」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>模組零承擔</th>
          <th>模組十承擔</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>起點訊號</td>
          <td>需求分類、流量形狀、團隊能力</td>
          <td>已決定要動、評估完成</td>
      </tr>
      <tr>
          <td>主要產物</td>
          <td>選型表、能力對照、取捨判讀</td>
          <td>執行劇本、切流策略、回退條件</td>
      </tr>
      <tr>
          <td>失敗代價</td>
          <td>選錯方向、回頭再評估</td>
          <td>切流失敗、資料損失、事件影響使用者</td>
      </tr>
      <tr>
          <td>工具語言</td>
          <td>mental model、taxonomy、capability</td>
          <td>runbook、cutover、rollback</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組的邊界">跟其他模組的邊界</h2>
<p>模組十收的是「跨服務、跨模組、跨環境的演進劇本」、不是「該技術的小範圍變更」。常見的容易誤判邊界：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>留原模組</th>
          <th>進模組十</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>schema migration 語法、index 設計、rollout</td>
          <td>01 留</td>
          <td>—</td>
      </tr>
      <tr>
          <td>schema 跨多 release 的 zero-downtime 切換</td>
          <td>—</td>
          <td>模組十收（未來、Strangler Fig 跨服務替換）</td>
      </tr>
      <tr>
          <td>cache aside / TTL / eviction</td>
          <td>02 留</td>
          <td>—</td>
      </tr>
      <tr>
          <td>cache 大型 vendor 切換（自建 → 雲服務）</td>
          <td>—</td>
          <td>模組十收（未來）</td>
      </tr>
      <tr>
          <td>service 拆分判讀</td>
          <td>—</td>
          <td>模組十收（10.1）</td>
      </tr>
      <tr>
          <td>service 拆分執行 runbook</td>
          <td>—</td>
          <td>模組十收（10.2）</td>
      </tr>
      <tr>
          <td>雲端能力對照（AWS / GCP / Azure）</td>
          <td>00 留（0.19）</td>
          <td>—</td>
      </tr>
      <tr>
          <td>跨雲遷移執行劇本</td>
          <td>—</td>
          <td>模組十收（未來）</td>
      </tr>
      <tr>
          <td>9.x 擴展軸、容量規劃</td>
          <td>09 留</td>
          <td>—</td>
      </tr>
      <tr>
          <td>交付形態該不該遷、升級 tripwire 判讀</td>
          <td>00 留（0.21）</td>
          <td>—</td>
      </tr>
      <tr>
          <td>託管形態遷出的執行劇本</td>
          <td>—</td>
          <td>模組十收（10.3）</td>
      </tr>
      <tr>
          <td>拆分後造成的容量重平衡 runbook</td>
          <td>—</td>
          <td>模組十收（未來）</td>
      </tr>
  </tbody>
</table>
<p>判別問題是「這個變更失敗時、回退範圍跨幾個服務 / 模組？」。跨多模組的演進劇本進模組十、單模組內的小範圍變更留原模組。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1</a></td>
          <td>服務拆分與邊界判讀</td>
          <td>整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2</a></td>
          <td>服務拆分執行 Runbook</td>
          <td>10.1 決定該拆之後、實際怎麼動手 — <a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig</a>、<a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">雙寫期</a> 管理、切流策略、回退條件設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3</a></td>
          <td>託管形態遷出</td>
          <td><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a> 升級自建 tripwire 觸發之後、從託管平台 / BaaS 遷往自建的執行 — 資料 / 身分 / 流量 / 整合的資產線盤點、並行期與回切窗口、部分遷出中繼形態</td>
      </tr>
  </tbody>
</table>
<h2 id="後續擴充方向">後續擴充方向</h2>
<p>本模組目前收服務拆分與託管形態遷出議題。未來會擴充的演進類議題：</p>
<ul>
<li><strong>跨服務 schema 演進</strong>：API contract migration、event schema versioning、跨服務的 backfill 策略</li>
<li><strong>大型雲端遷移</strong>：on-prem → cloud、跨雲遷移的 cutover 劇本、流量切換策略</li>
<li><strong>基礎設施替換</strong>：資料庫引擎切換（如 MySQL → Postgres、自建 → managed）、cache vendor 切換、queue broker 切換的執行紀律</li>
<li><strong>容量重平衡</strong>：拆分後的服務間流量分佈、shard 重分佈、tenant 隔離調整</li>
</ul>
<p>這些議題的共同特徵：跨多個技術模組、失敗代價遠超「該技術的小範圍變更」、需要獨立的執行劇本跟回退條件。</p>
]]></content:encoded></item><item><title>Cloudflare Page Shield：用 CSP + SRI + script monitoring 防 client-side supply chain</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Cloudflare WAF 在入口治理譜系的定位、本文聚焦 &lt;em>Page Shield&lt;/em> 這個 client-side（browser）supply chain attack 防禦工具 — 跟 WAF 攔 server-side request 是不同層。&lt;/p>&lt;/blockquote>
&lt;h2 id="attack-pattern--defense-mechanism-對照">Attack pattern × Defense mechanism 對照&lt;/h2>
&lt;p>Client-side supply chain attack 不會被 WAF 看到 — 攻擊發生在 browser 渲染 page 時、不在 origin server 跟 client 之間的網路層。Page Shield 是 &lt;em>browser-side script execution&lt;/em> 的監測 + 防禦層、跟 WAF 處理 &lt;em>server-side request inspection&lt;/em> 互補不重疊。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Attack pattern&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>Page Shield 對應防禦&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Magecart 信用卡 skimmer&lt;/td>
 &lt;td>第三方 JS 被注入惡意 form listener、信用卡資訊送外部 endpoint&lt;/td>
 &lt;td>CSP &lt;code>connect-src&lt;/code> + script alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方 SDK 被 compromise&lt;/td>
 &lt;td>廠商 CDN 被攻擊、SDK 改版內含 malicious payload&lt;/td>
 &lt;td>SRI hash mismatch + script alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Formjacking&lt;/td>
 &lt;td>結帳頁 form action 被改、submit 送外部 server&lt;/td>
 &lt;td>CSP &lt;code>form-action&lt;/code> directive&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline script injection&lt;/td>
 &lt;td>XSS / DOM-based injection 插入 &lt;code>&amp;lt;script&amp;gt;&lt;/code> 跑外部 source&lt;/td>
 &lt;td>CSP &lt;code>script-src&lt;/code> + nonce&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage abuse&lt;/td>
 &lt;td>malicious JS 讀 localStorage / cookies 送外部&lt;/td>
 &lt;td>CSP &lt;code>connect-src&lt;/code> + CSP report&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三層防禦對應不同 attack 階段：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>CSP（Content Security Policy）&lt;/strong>：browser-enforced policy、preventive、阻止違反 policy 的 script load / network request&lt;/li>
&lt;li>&lt;strong>SRI（Subresource Integrity）&lt;/strong>：load 階段 hash 驗證、detective + preventive、廠商 CDN 上 script 被改就 browser 拒載&lt;/li>
&lt;li>&lt;strong>Script monitoring&lt;/strong>：runtime 觀測、detective only、記錄頁面 load 哪些 third-party script、變動時 alert&lt;/li>
&lt;/ol>
&lt;p>三層各有 ceiling — &lt;em>CSP 擋 inline / unauthorized source 但擋不到 allowed source 被 compromise&lt;/em>；&lt;em>SRI 擋已知 vendor 改 hash 但擋不到動態 loader&lt;/em>；&lt;em>monitoring 看得到但攔不到&lt;/em>。Production 三層疊用、不要單一 layer。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> overview 的 implementation-layer deep article。Overview 已說明 Cloudflare WAF 在入口治理譜系的定位、本文聚焦 <em>Page Shield</em> 這個 client-side（browser）supply chain attack 防禦工具 — 跟 WAF 攔 server-side request 是不同層。</p></blockquote>
<h2 id="attack-pattern--defense-mechanism-對照">Attack pattern × Defense mechanism 對照</h2>
<p>Client-side supply chain attack 不會被 WAF 看到 — 攻擊發生在 browser 渲染 page 時、不在 origin server 跟 client 之間的網路層。Page Shield 是 <em>browser-side script execution</em> 的監測 + 防禦層、跟 WAF 處理 <em>server-side request inspection</em> 互補不重疊。</p>
<table>
  <thead>
      <tr>
          <th>Attack pattern</th>
          <th>表現</th>
          <th>Page Shield 對應防禦</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Magecart 信用卡 skimmer</td>
          <td>第三方 JS 被注入惡意 form listener、信用卡資訊送外部 endpoint</td>
          <td>CSP <code>connect-src</code> + script alert</td>
      </tr>
      <tr>
          <td>第三方 SDK 被 compromise</td>
          <td>廠商 CDN 被攻擊、SDK 改版內含 malicious payload</td>
          <td>SRI hash mismatch + script alert</td>
      </tr>
      <tr>
          <td>Formjacking</td>
          <td>結帳頁 form action 被改、submit 送外部 server</td>
          <td>CSP <code>form-action</code> directive</td>
      </tr>
      <tr>
          <td>Inline script injection</td>
          <td>XSS / DOM-based injection 插入 <code>&lt;script&gt;</code> 跑外部 source</td>
          <td>CSP <code>script-src</code> + nonce</td>
      </tr>
      <tr>
          <td>Storage abuse</td>
          <td>malicious JS 讀 localStorage / cookies 送外部</td>
          <td>CSP <code>connect-src</code> + CSP report</td>
      </tr>
  </tbody>
</table>
<p>三層防禦對應不同 attack 階段：</p>
<ol>
<li><strong>CSP（Content Security Policy）</strong>：browser-enforced policy、preventive、阻止違反 policy 的 script load / network request</li>
<li><strong>SRI（Subresource Integrity）</strong>：load 階段 hash 驗證、detective + preventive、廠商 CDN 上 script 被改就 browser 拒載</li>
<li><strong>Script monitoring</strong>：runtime 觀測、detective only、記錄頁面 load 哪些 third-party script、變動時 alert</li>
</ol>
<p>三層各有 ceiling — <em>CSP 擋 inline / unauthorized source 但擋不到 allowed source 被 compromise</em>；<em>SRI 擋已知 vendor 改 hash 但擋不到動態 loader</em>；<em>monitoring 看得到但攔不到</em>。Production 三層疊用、不要單一 layer。</p>
<h2 id="csp-配置-step-by-step">CSP 配置 step-by-step</h2>
<h3 id="從-cloudflare-dashboard-啟用--寫-policy">從 Cloudflare dashboard 啟用 + 寫 policy</h3>





<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"># Dashboard: Security → Page Shield → CSP
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"># 模式: Report-only（第一週）→ Enforced（驗證後）
</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"># 範例 policy
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">default-src &#39;self&#39;;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">script-src &#39;self&#39; &#39;nonce-{NONCE}&#39; https://cdn.trusted.com https://www.googletagmanager.com;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">style-src &#39;self&#39; &#39;unsafe-inline&#39; https://fonts.googleapis.com;
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">img-src &#39;self&#39; data: https:;
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">connect-src &#39;self&#39; https://api.myapp.com https://*.sentry.io;
</span></span><span class="line"><span class="ln">10</span><span class="cl">form-action &#39;self&#39;;
</span></span><span class="line"><span class="ln">11</span><span class="cl">frame-ancestors &#39;none&#39;;
</span></span><span class="line"><span class="ln">12</span><span class="cl">report-uri https://csp-report.cloudflare.com/cdn-cgi/script_monitor/report;
</span></span><span class="line"><span class="ln">13</span><span class="cl">report-to default;</span></span></code></pre></div><p>關鍵直覺：</p>
<ul>
<li><strong><code>'nonce-{NONCE}'</code></strong>：origin server 每 request 生成 random nonce、注入 <code>&lt;script nonce=&quot;...&quot;&gt;</code> 跟 CSP header；script tag 沒對應 nonce 就被 browser 拒跑、擋 XSS</li>
<li><strong><code>connect-src</code> 精準寫</strong>：第三方 API endpoint 全列出；不寫 <code>*</code> 或 <code>https:</code> 是擋 exfiltration 的關鍵（Magecart 把信用卡送外部 endpoint 就是用 <code>connect-src</code> 攔）</li>
<li><strong><code>form-action</code></strong>：擋 form 被改 action attribute 送外部、formjacking 第一道防線</li>
<li><strong><code>report-uri</code> + <code>report-to</code></strong>：违反 policy 的 event 送 Cloudflare、Page Shield dashboard 看 violation report</li>
</ul>
<h3 id="report-only-mode-第一週">Report-only mode 第一週</h3>





<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">Content-Security-Policy-Report-Only: &lt;policy&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">Content-Security-Policy:             default-src &#39;self&#39;;   # 鬆 policy 仍 enforce</span></span></code></pre></div><p>Report-only 期間 browser <em>report 違反但不擋</em>、production traffic 不受影響；SOC 看 report 找：</p>
<ul>
<li>漏列的 legitimate third-party（marketing / analytics SDK 沒寫進 policy）</li>
<li>意外 inline script（dev 留下的 debug snippet）</li>
<li>跨 domain 的合法 connect（CRM / chat widget）</li>
</ul>
<p>第一週後 dashboard 看 violation 數量趨穩 + 主要違規都已 whitelist、切 Enforced。</p>
<h3 id="enforced-mode-切換--canary">Enforced mode 切換 + canary</h3>
<p>不要直接全站 enforced — 用 Cloudflare Page Rule 對 10% traffic enforced、90% report-only：</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">URL pattern: example.com/*
</span></span><span class="line"><span class="ln">2</span><span class="cl">Page Rule: Add CSP header (enforced)
</span></span><span class="line"><span class="ln">3</span><span class="cl">Bypass: 90% by Cookie / IP hash</span></span></code></pre></div><p>10% traffic 跑 24-48h、確認 zero legitimate violation、再擴大到 50% → 100%。canary 期間 monitor <code>error-rate</code> metric、不只是 violation report。</p>
<h2 id="sri-配置">SRI 配置</h2>
<p>Subresource Integrity 用 hash 驗證 CDN-hosted script 沒被改：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://cdn.example.com/widget.v1.2.3.js&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">        <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="na">crossorigin</span><span class="o">=</span><span class="s">&#34;anonymous&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div><p>Browser load 時算 hash、跟 <code>integrity</code> 不符就拒跑。關鍵：</p>
<ul>
<li><strong>Hash 一定要 version-pinned</strong>：用 <code>widget.v1.2.3.js</code>、不能用 <code>widget.latest.js</code>；廠商更新 latest 時 hash 變 → SRI 拒載 → 服務中斷</li>
<li><strong>多 hash</strong>：寫 <code>integrity=&quot;sha384-... sha512-...&quot;</code> 至少一個 match 就過、可在 vendor rotate hash 時平滑遷移</li>
<li><strong><code>crossorigin=&quot;anonymous&quot;</code></strong> 必加：跨 origin script 預設 browser 不暴露 hash 失敗細節、<code>anonymous</code> 才允許 CORS-based hash check</li>
</ul>
<h3 id="page-shield-自動產-sri-提示">Page Shield 自動產 SRI 提示</h3>
<p>Dashboard → Page Shield → Scripts 列出所有偵測到的 script、含 <em>建議 SRI hash</em>；可以 export 整合進 build pipeline、自動把所有 vendor script 加 SRI。</p>
<h2 id="故障演練">故障演練</h2>
<h3 id="case-1csp-report-floodsoc-noise">Case 1：CSP report flood，SOC noise</h3>
<p><strong>徵兆</strong>：切 Enforced 後、CSP violation report 從每天 ~500 漲到每分鐘 ~50K、Page Shield dashboard 變紅、SOC 收 alert 收到 silent。</p>
<p><strong>根因</strong>：browser extension（廣告攔截 / spell checker / password manager）注入 inline script 跟 connect、被 CSP block 同時觸發 report；不是真實 attack、是 user 端 extension 行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>CSP <code>report-sample</code> directive 限 sampling（只 report 10%）— spec 部分支援、不是所有 browser 都認</li>
<li>Page Shield 規則：filter out extension protocol（<code>chrome-extension://</code>、<code>moz-extension://</code>、<code>safari-extension://</code>）後再 alert</li>
<li>Report endpoint 自管 + aggregation：不直接接 SIEM、先 batch + dedupe、再送 SIEM</li>
<li>接受 report flood 是 normal、focus 監測 <em>unique violation pattern</em> 不是 <em>total volume</em></li>
</ol>
<h3 id="case-2inline-script-漏舊頁面突然壞">Case 2：Inline script 漏，舊頁面突然壞</h3>
<p><strong>徵兆</strong>：切 Enforced 後 X 個舊頁面壞、user feedback 提交 form 失敗、debugger 看到 console <code>Refused to execute inline script because it violates...</code>。</p>
<p><strong>根因</strong>：legacy page 有 inline <code>&lt;script&gt;</code> 沒 nonce、CSP enforced 後 browser 拒跑；報表/管理後台/舊 admin page 常見。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Audit 所有 inline <code>&lt;script&gt;</code>、加 nonce attribute（server-side render 時注入）</li>
<li>短期：對舊頁面用 <code>unsafe-inline</code> 寫進 CSP（接受降級）、page-specific CSP override</li>
<li>長期：legacy page 改 build-time bundle、消除 inline script</li>
</ol>
<h3 id="case-3dynamic-script-loader-繞過-sri">Case 3：Dynamic script loader 繞過 SRI</h3>
<p><strong>徵兆</strong>：vendor script load 成功、但 Page Shield monitoring 看到該 vendor script <em>load 後又動態 load 多個額外 script</em>；額外 script 沒 SRI 保護、廠商側 compromise 直接過。</p>
<p><strong>根因</strong>：第三方 SDK 用 <code>document.createElement('script')</code> + <code>script.src = '...'</code> runtime 動態 load；CSP <code>script-src</code> 可能允許這個來源、但 SRI 沒法在 runtime 注入。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>CSP <code>script-src</code> 精準到 <em>只允許特定 path</em>、不是整個 domain（例：<code>https://cdn.vendor.com/sdk/v3/</code> 而不是 <code>https://cdn.vendor.com</code>）</li>
<li>評估 vendor 是否有 <em>static-only</em> 替代（多數 marketing / analytics SDK 不需要 dynamic loader、是 legacy 設計）</li>
<li>不能消除 dynamic loader 時、Page Shield monitoring 設 <em>new script alert</em>、廠商加 sub-script 即刻通知</li>
</ol>
<h3 id="case-4sri-hash-mismatchvendor-偷偷更新">Case 4：SRI hash mismatch，vendor 偷偷更新</h3>
<p><strong>徵兆</strong>：第三方 widget 突然不顯示、Page Shield 顯示 SRI mismatch、廠商 status page 沒事故公告。</p>
<p><strong>根因</strong>：廠商在 same URL（不是 versioned）下偷偷 push minor patch、hash 變了 → SRI 拒載；不是 attack、是 vendor 不遵守 immutable URL 慣例。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>強制要求廠商提供 versioned URL（<code>widget.v1.2.3.js</code>）、不收 <code>widget.latest.js</code></li>
<li>廠商不配合時、build pipeline 加 <em>daily hash check</em>、廠商偷改 SRI hash 自動更新 + Slack alert</li>
<li>評估換 vendor — 不遵守 immutable URL 的廠商 supply chain integrity 信用低</li>
</ol>
<h2 id="容量--cost">容量 + cost</h2>
<p>Page Shield 是 <em>Enterprise plan + Page Shield add-on</em>、cost 維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CSP report 量</td>
          <td>Cloudflare 端聚合、不另外計費；report endpoint 自管要 sizing</td>
      </tr>
      <tr>
          <td>Script monitoring</td>
          <td>不影響 page load latency（async detection）</td>
      </tr>
      <tr>
          <td>Per-zone pricing</td>
          <td>跨子域 + apex domain 多 zone 各算一份</td>
      </tr>
      <tr>
          <td>SOC operation</td>
          <td>第一週 report 量大、需要 1-2 analyst FTE 跑 tuning；穩定後低人力</td>
      </tr>
  </tbody>
</table>
<p>Page load 影響：</p>
<ul>
<li>CSP header ~1-2KB（policy 寫越精準越長、不是越短越好）</li>
<li>SRI 比對 ~5-10ms / script、現代 browser cache decoded hash、不重複算</li>
<li>Script monitoring beacon ~100 byte / script load、async 不阻塞 page render</li>
</ul>
<p>實務 default：</p>
<ul>
<li>Critical e-commerce / fintech：CSP enforced + SRI 全 vendor + monitoring all、SOC review weekly</li>
<li>一般 SaaS：CSP report-only ongoing + SRI critical vendor only + monitoring 主域</li>
<li>Marketing / blog：CSP <code>default-src 'self'</code> minimum + monitoring only</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-dev-workflow-整合">跟 dev workflow 整合</h3>
<p>CSP 寫進 <em>deploy pipeline</em>、不是 dashboard 手動配：</p>
<ol>
<li>Repo 內 <code>csp-policy.yml</code>、跟 code 同 lifecycle</li>
<li>CI 跑 <em>CSP linter</em>（如 <code>csp-evaluator</code>）、檢查 policy 弱點</li>
<li>Deploy 時 push 到 Cloudflare API、自動 versioning + rollback</li>
</ol>
<h3 id="跟-waf-互補">跟 WAF 互補</h3>
<p>Page Shield 跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 不重疊但互補：</p>
<ul>
<li>WAF 攔 <em>server-side</em> request injection（SQL / command / path traversal）</li>
<li>Page Shield 攔 <em>client-side</em> script execution（XSS / supply chain）</li>
<li>共同 dashboard + alert routing、不要分開 SOC team 看</li>
</ul>
<h3 id="跟-supply-chain-sbom">跟 supply chain SBOM</h3>
<p>Page Shield 偵測的 <em>client-side dependency</em> 可進 SBOM、跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a> 的 server-side SBOM 合併、得到完整 dependency graph。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Trusted Types</strong>：browser-side template injection 的下一代防禦、Chrome 已支援、Firefox / Safari 進度不一</li>
<li><strong>CSP Level 3 + strict-dynamic</strong>：減少 maintenance burden、用 nonce 動態信任 nested script</li>
<li><strong>Reporting API v1</strong>：standard report endpoint + <code>Reporting-Endpoints</code> header 取代 <code>report-uri</code></li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a></li>
<li>上游 chapter：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>、<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a></li>
<li>對照案例：British Airways 2018 Magecart / Macy&rsquo;s 2019 skimmer（公開 supply chain 案例）</li>
<li>平行 vendor：<a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></li>
<li>平行 deep article：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a> / <a href="/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &#43; case management 整合">Splunk RBA</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Vault 在 secrets / credentials 治理譜系的定位（跟 cloud-native secrets manager / cert-manager 的取捨）、本文聚焦 &lt;em>dynamic credential engine&lt;/em> 的實作層：怎麼配 database engine、application 怎麼 renew lease、production 踩過哪些坑、跟 cloud-native vault 跟 vault-agent injector 怎麼整合。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Long-lived database credential 寫進 application config 是 production 環境最常見的 secret hygiene 失敗：credential 一旦外洩、輪替成本是 &lt;em>跨團隊協調 + 多服務同步重啟&lt;/em>、實務上半年才換一次、credential 在 git history / log / dump file 留下軌跡。動態憑證（dynamic credential）的核心承諾是 &lt;em>credential 生命週期跟 application session 對齊&lt;/em>、用完就 revoke、外洩窗口從幾個月縮到幾分鐘。&lt;/p>
&lt;p>但 dynamic credential 不是「換個 SDK 就好」、它把 &lt;em>credential 治理&lt;/em> 從 secret rotation 問題轉成 &lt;em>lease lifecycle&lt;/em> 問題。lease TTL 設多久、renewal 怎麼跑、DB 端 user 創建會不會撞 max_connections、Vault sealed 時 application 怎麼降級 — 每個都是 production-grade 議題、無法靠 vendor doc 預設值直接上線。&lt;/p>
&lt;h2 id="核心概念lease-lifecycle-跟-secrets-engine-模型">核心概念：lease lifecycle 跟 secrets engine 模型&lt;/h2>
&lt;p>Vault dynamic credential 由三個元件協作：&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>&lt;strong>Secrets engine&lt;/strong>&lt;/td>
 &lt;td>後端執行 credential 創建跟 revoke、每個 engine 對應一個 datastore（database / aws / ssh）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Role&lt;/strong>&lt;/td>
 &lt;td>創建 credential 的範本：DB 連線 + creation SQL + default / max TTL + allowed_roles&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Lease&lt;/strong>&lt;/td>
 &lt;td>每次 credential 發放都對應一個 lease ID、由 Vault 管 TTL / renew / revoke&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟 static secret（K/V store）對照、dynamic credential 的關鍵差異是 &lt;em>credential 在 read 時才產生&lt;/em>、且 Vault 追蹤每個 outstanding lease；application 必須 &lt;em>主動 renew&lt;/em> 或接受 credential 失效。&lt;/p>
&lt;p>Lease 的兩個 TTL：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>default_ttl&lt;/strong>：credential 初始有效期、application 不 renew 就到期&lt;/li>
&lt;li>&lt;strong>max_ttl&lt;/strong>：credential 最長有效期、不管 renew 幾次都不能超過&lt;/li>
&lt;/ul>
&lt;p>實務 default 配置：&lt;code>default_ttl: 1h&lt;/code> + &lt;code>max_ttl: 24h&lt;/code>、application 每 30-45 分鐘 renew 一次、credential 最多活 24 小時必換新的。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> overview 的 implementation-layer deep article。Overview 已說明 Vault 在 secrets / credentials 治理譜系的定位（跟 cloud-native secrets manager / cert-manager 的取捨）、本文聚焦 <em>dynamic credential engine</em> 的實作層：怎麼配 database engine、application 怎麼 renew lease、production 踩過哪些坑、跟 cloud-native vault 跟 vault-agent injector 怎麼整合。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Long-lived database credential 寫進 application config 是 production 環境最常見的 secret hygiene 失敗：credential 一旦外洩、輪替成本是 <em>跨團隊協調 + 多服務同步重啟</em>、實務上半年才換一次、credential 在 git history / log / dump file 留下軌跡。動態憑證（dynamic credential）的核心承諾是 <em>credential 生命週期跟 application session 對齊</em>、用完就 revoke、外洩窗口從幾個月縮到幾分鐘。</p>
<p>但 dynamic credential 不是「換個 SDK 就好」、它把 <em>credential 治理</em> 從 secret rotation 問題轉成 <em>lease lifecycle</em> 問題。lease TTL 設多久、renewal 怎麼跑、DB 端 user 創建會不會撞 max_connections、Vault sealed 時 application 怎麼降級 — 每個都是 production-grade 議題、無法靠 vendor doc 預設值直接上線。</p>
<h2 id="核心概念lease-lifecycle-跟-secrets-engine-模型">核心概念：lease lifecycle 跟 secrets engine 模型</h2>
<p>Vault dynamic credential 由三個元件協作：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Secrets engine</strong></td>
          <td>後端執行 credential 創建跟 revoke、每個 engine 對應一個 datastore（database / aws / ssh）</td>
      </tr>
      <tr>
          <td><strong>Role</strong></td>
          <td>創建 credential 的範本：DB 連線 + creation SQL + default / max TTL + allowed_roles</td>
      </tr>
      <tr>
          <td><strong>Lease</strong></td>
          <td>每次 credential 發放都對應一個 lease ID、由 Vault 管 TTL / renew / revoke</td>
      </tr>
  </tbody>
</table>
<p>跟 static secret（K/V store）對照、dynamic credential 的關鍵差異是 <em>credential 在 read 時才產生</em>、且 Vault 追蹤每個 outstanding lease；application 必須 <em>主動 renew</em> 或接受 credential 失效。</p>
<p>Lease 的兩個 TTL：</p>
<ul>
<li><strong>default_ttl</strong>：credential 初始有效期、application 不 renew 就到期</li>
<li><strong>max_ttl</strong>：credential 最長有效期、不管 renew 幾次都不能超過</li>
</ul>
<p>實務 default 配置：<code>default_ttl: 1h</code> + <code>max_ttl: 24h</code>、application 每 30-45 分鐘 renew 一次、credential 最多活 24 小時必換新的。</p>
<h2 id="step-by-step-配置">Step-by-step 配置</h2>
<h3 id="vault-server-啟用-database-secrets-engine">Vault server 啟用 database secrets engine</h3>





<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. enable secrets engine</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vault secrets <span class="nb">enable</span> -path<span class="o">=</span>database database
</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"># 2. 配置 PostgreSQL connection</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">vault write database/config/myapp-prod <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  <span class="nv">plugin_name</span><span class="o">=</span>postgresql-database-plugin <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  <span class="nv">allowed_roles</span><span class="o">=</span><span class="s2">&#34;myapp-reader,myapp-writer&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  <span class="nv">connection_url</span><span class="o">=</span><span class="s2">&#34;postgresql://{{username}}:{{password}}@db.internal:5432/myapp?sslmode=require&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  <span class="nv">username</span><span class="o">=</span><span class="s2">&#34;vault_root&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  <span class="nv">password</span><span class="o">=</span><span class="s2">&#34;&lt;vault_root_pw&gt;&#34;</span>
</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 class="c1"># 3. 創建 role</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">vault write database/roles/myapp-reader <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  <span class="nv">db_name</span><span class="o">=</span>myapp-prod <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  <span class="nv">creation_statements</span><span class="o">=</span><span class="s2">&#34;CREATE ROLE \&#34;{{name}}\&#34; WITH LOGIN PASSWORD &#39;{{password}}&#39; VALID UNTIL &#39;{{expiration}}&#39;; \
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">                       GRANT SELECT ON ALL TABLES IN SCHEMA public TO \&#34;{{name}}\&#34;;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  <span class="nv">default_ttl</span><span class="o">=</span><span class="s2">&#34;1h&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  <span class="nv">max_ttl</span><span class="o">=</span><span class="s2">&#34;24h&#34;</span></span></span></code></pre></div><p>關鍵：<code>vault_root</code> 是 Vault 用來創建其他 user 的 <em>bootstrapping account</em>、權限要含 <code>CREATEROLE</code>、但不需要 SUPERUSER；creation_statements 必須含 <code>VALID UNTIL '{{expiration}}'</code>、否則 DB 端 user 不會自動過期、Vault revoke 失敗時會留 zombie account。</p>
<h3 id="application-取得-credential">Application 取得 credential</h3>





<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"># Read 動態 credential（每次 read 都產生新 user）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault <span class="nb">read</span> database/creds/myapp-reader
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Key                Value</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># lease_id           database/creds/myapp-reader/abc123</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># lease_duration     1h</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># username           v-myapp-reader-x7y8z9-1747512345</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># password           A1b2C3d4E5f6...</span></span></span></code></pre></div><p>Application 從 response 拿三個值：<code>lease_id</code>（用來 renew / revoke）、<code>username</code> + <code>password</code>（DB 連線）、<code>lease_duration</code>（決定何時 renew）。</p>
<h3 id="renew-lease">Renew lease</h3>





<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"># 在 lease 到期前 renew（推薦在 50-70% TTL 跑）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault lease renew database/creds/myapp-reader/abc123
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Key                Value</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># lease_id           database/creds/myapp-reader/abc123</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># lease_duration     1h    # renew 後重置回 default_ttl</span></span></span></code></pre></div><p><code>lease_duration</code> 在 renew 後 <em>重置回 default_ttl</em>、但 <em>不會超過 max_ttl</em>。例：default 1h / max 24h、application 連 renew 23 小時後、第 24 次 renew Vault 拒絕、application 必須拿新 credential。</p>
<h3 id="revoke-leaseapplication-shutdown-時">Revoke lease（application shutdown 時）</h3>





<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"># Graceful shutdown 時主動 revoke</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault lease revoke database/creds/myapp-reader/abc123</span></span></code></pre></div><p>Application 結束時 revoke 是 <em>credential hygiene 的最後一道閘門</em> — 即使 lease 還有時間、主動 revoke 讓 DB 端 user 立刻消失、避免 credential 在 application crash dump / log 內被翻出時還能用。</p>
<h2 id="故障演練--邊界-case">故障演練 / 邊界 case</h2>
<h3 id="case-1lease-renewal-racecredential-中途失效">Case 1：Lease renewal race，credential 中途失效</h3>
<p><strong>徵兆</strong>：application log 突然出現 <code>FATAL: role &quot;v-myapp-reader-x7y8z9-...&quot; does not exist</code>、且時間點接近某個整點 / 半點。</p>
<p><strong>根因</strong>：application 用 lease_duration 推算 renew 時機、但用了 <em>系統時間</em> 而非 <em>lease 簽發時間</em>；application 啟動晚於 lease 簽發 30 秒、renew 跑在 lease 過期後 5 秒、Vault 已 revoke credential、DB 端 user 已刪除。</p>
<p><strong>修法</strong>：用 <em>server 回傳的 lease_duration</em> 反推 renew 時機、留 <em>20-30% buffer</em>。例：lease_duration 3600 秒、application 在 2400-2520 秒（66-70%）開始 renew、不要拖到 3500 秒。Vault SDK 多數有 LifetimeWatcher（Go SDK）或 Renewer（Python hvac）這類 helper、優先用 SDK 不要自管 ticker。</p>
<h3 id="case-2db-max_connections-撞牆">Case 2：DB max_connections 撞牆</h3>
<p><strong>徵兆</strong>：application 在流量高峰開始大量 <code>FATAL: too many connections for role</code>、Vault audit log 顯示新 credential 還在發、PostgreSQL <code>pg_stat_activity</code> 看到上百個 <code>v-myapp-...</code> user 同時連著。</p>
<p><strong>根因</strong>：每個 application instance / pod 在啟動時 read 一次 credential、credential lease 1h、但 <em>application 跑 30 分鐘就重啟</em>（K8s rolling update / OOM）；舊 user 還在 PostgreSQL 端連著（connection pool 沒釋放）、新 user 又被創建、累積到 max_connections。</p>
<p><strong>修法</strong>：兩層</p>
<ol>
<li>Application graceful shutdown 時 <code>vault lease revoke</code> + connection pool drain</li>
<li>PostgreSQL connection pool 加 <code>pool_lifetime_max</code> 跟 application instance lifetime 對齊、避免 connection leak 到 lease 失效後仍 holding</li>
</ol>
<h3 id="case-3vault-sealed-中existing-lease-仍可用但新-lease-拿不到">Case 3：Vault sealed 中、existing lease 仍可用但新 lease 拿不到</h3>
<p><strong>徵兆</strong>：deploy 新 version 時、新 pod 起不來、<code>vault read database/creds/...</code> 卡住或回 <code>Vault is sealed</code>；但 <em>舊 pod 持續運作正常</em>（因為已持有 lease）。</p>
<p><strong>根因</strong>：Vault sealed（master key 被 wrap、需要 unseal key 解封）時、existing lease 因為 <em>credential 已在 DB 端創建</em>、application 連線不需要 Vault 介入；但 <em>新 lease 創建需要 Vault</em> / <em>renew 也需要 Vault</em>。Sealed 期間 application 還能用、但無法擴容、無法 renew。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Vault HA cluster + auto-unseal（KMS / HSM auto-unseal）避免人工 unseal 鏈</li>
<li>Application 加 retry-with-backoff、Vault 短暫 unavailable 時不要立刻 crash</li>
<li>Lease 設長一點（default 4h、max 48h）給 unseal 流程留時間</li>
</ol>
<h3 id="case-4application-vault-token-expirelease-orphan">Case 4：Application Vault token expire、lease orphan</h3>
<p><strong>徵兆</strong>：application 在連續跑 1-2 週後突然開始 <code>Permission denied</code> on <code>vault lease renew</code>、credential 在 max_ttl 後失效但 application 不知道。</p>
<p><strong>根因</strong>：application 的 Vault token（不是 DB credential 的 lease）也有 TTL；token 過期後 application 無法 renew lease、但 application 可能還沒到 <em>自己拿新 token</em> 的循環。Lease 變 orphan（沒人能 renew）、TTL 到就被 revoke。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Application 用 vault-agent injector / sidecar pattern、由 sidecar 維護 token + lease；application 只讀 file</li>
<li>不用 sidecar 時、application token 用 <em>renewable token</em> + 跟 lease 同 lifecycle 管</li>
<li>AppRole auth method 的 secret_id 跟 token TTL 都要納入 application reload 流程</li>
</ol>
<h3 id="case-5circleci-2023-incident-對照--secret_id-scope-過寬">Case 5：<a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">CircleCI 2023 incident</a> 對照 — secret_id scope 過寬</h3>
<p><strong>徵兆</strong>：CircleCI 2023 1 月事件、攻擊者拿到開發者 endpoint session token、進而拿到 Vault AppRole 的 secret_id；secret_id 對應的 policy 含 <em>跨環境跨資料庫 read</em>、攻擊者用 secret_id 拿到大量動態 credential。</p>
<p><strong>根因</strong>：AppRole secret_id 的 policy scope 設成 <em>single AppRole 服務所有環境</em>、而不是 <em>per-environment AppRole</em>；secret_id 外洩等於拿到全公司 dynamic credential 發放權。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Per-environment AppRole：dev / staging / prod 各有獨立 AppRole + secret_id、policy 只允許該環境的 database engine path</li>
<li>Secret_id TTL 短化（&lt; 24h）、用 <em>response wrapping</em> 傳遞、拿到後立刻 unwrap、減少 secret_id 在 build pipeline log 留軌跡</li>
<li>Vault audit log 接 SIEM、<code>approle/login</code> 異常 location / IP 即刻 alert</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<p>Dynamic credential 的容量設計圍繞 <em>lease churn rate</em> — 每秒多少新 lease 創建、多少 renew、多少 revoke。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算方式</th>
          <th>警戒值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 lease / s</td>
          <td><code>應用 instance 數 × (1 / lease_duration)</code></td>
          <td>單 Vault node ~50/s、HA cluster ~200/s</td>
      </tr>
      <tr>
          <td>Renew / s</td>
          <td><code>outstanding lease × renew_freq</code></td>
          <td>renew 跟 read 同 cost</td>
      </tr>
      <tr>
          <td>DB 端 user 數</td>
          <td><code>peak outstanding lease</code></td>
          <td>不能超過 DB max_roles 限制</td>
      </tr>
      <tr>
          <td>DB connection 數</td>
          <td><code>peak outstanding lease × avg connection per credential</code></td>
          <td>不能超過 DB max_connections</td>
      </tr>
      <tr>
          <td>Vault audit log size</td>
          <td>每 lease 操作 ~500 byte、<code>(新+renew+revoke) × 500B</code></td>
          <td>100 lease/s → 50MB/s audit、SIEM 端要 sizing</td>
      </tr>
  </tbody>
</table>
<p>實務 sizing 範例：100 個 application pod、lease_duration 1h、renew at 50% TTL：</p>
<ul>
<li>新 lease：100 / 3600 ≈ 0.03/s（pod 重啟才有）</li>
<li>Renew：100 / 1800 ≈ 0.06/s</li>
<li>Outstanding lease：~100 個（每 pod 一個）</li>
<li>DB user 數：~100 個（peak ~150 含 grace period）</li>
<li>DB connection：100 × 5（pool size）= 500、需要 PostgreSQL <code>max_connections &gt;= 600</code></li>
</ul>
<p>超出單 Vault node 容量（~50 ops/s）時、走 Vault HA cluster + auto-unseal、或拆 namespace。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="vault-agent-injectork8s-環境推薦">vault-agent injector（K8s 環境推薦）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># pod annotation</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">vault.hashicorp.com/agent-inject</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">vault.hashicorp.com/role</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;myapp-reader&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">vault.hashicorp.com/agent-inject-secret-db-creds</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;database/creds/myapp-reader&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">vault.hashicorp.com/agent-inject-template-db-creds</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">      {{- with secret &#34;database/creds/myapp-reader&#34; -}}
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">      DB_USER={{ .Data.username }}
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">      DB_PASSWORD={{ .Data.password }}
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">      {{- end }}</span></span></span></code></pre></div><p>Sidecar 自動 renew lease、credential 寫進 pod shared volume、application 讀 file。Application code 不需要 Vault SDK、降低 dependency。</p>
<h3 id="sdk-pattern非-k8s-環境">SDK pattern（非 K8s 環境）</h3>
<p>Go：<code>hashicorp/vault/api</code> + <code>LifetimeWatcher</code>、Java：spring-cloud-vault、Python：hvac + Renewer。SDK 已處理 renew timing / retry / token rotation、不要自寫 ticker。</p>
<h3 id="跟-cloud-native-secret-manager-的混搭">跟 cloud-native secret manager 的混搭</h3>
<p><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> 也有 dynamic credential rotation（每 30 天輪替）、但 <em>cadence 是按時間</em>、不是 <em>按 application session</em>。混搭 pattern：</p>
<ul>
<li>Cloud-native：infrastructure-level credential（RDS master / k8s service account）、long TTL（30-90 天）</li>
<li>Vault dynamic：application-level credential、short TTL（1-24 小時）</li>
<li>Vault root credential 存 cloud-native secret manager、Vault auto-unseal 也用 cloud KMS</li>
</ul>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Database snapshot 跟 dynamic credential 衝突</strong>：PostgreSQL <code>pg_dump</code> 用 long-lived credential、不適用 dynamic；snapshot user 用 static + scoped policy、跟 application user 分離</li>
<li><strong>Connection pool 端的 dynamic credential 支援</strong>：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> 不支援 per-connection credential rotation、需要 connection 整個 lifecycle 跟 lease 對齊</li>
<li><strong>多 region Vault replication</strong>：performance replication 跟 disaster recovery replication 對 lease 的處理不同、跨 region application 要 sticky 同一 region 的 Vault primary</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>對照案例：<a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></li>
<li>對照案例：<a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">CircleCI 2023 AppRole 事件</a> — Cross-vendor mapping</li>
<li>上游 chapter：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 K8s 在 deployment platform 譜系的定位、本文聚焦 &lt;em>pod termination&lt;/em> 這個 production 最常踩、被誤解最深的議題：序列、配置、五個 case、跟 service mesh 整合。&lt;/p>&lt;/blockquote>
&lt;h2 id="graceful-shutdown-沒做對500-期間每次-deploy-都吃-502">Graceful shutdown 沒做對、500 期間每次 deploy 都吃 502&lt;/h2>
&lt;p>最常見的觸發場景：deploy 新 image、prometheus alert 在 5 分鐘內收到一波 502 / 503、SRE 翻 application log 看到「正在處理 request」「connection closed」交替出現。Application 本身沒 bug、但 K8s 在 pod terminate 時跟 traffic 來源 &lt;em>沒對齊步調&lt;/em>、舊 pod 還在處理請求時就被 SIGKILL、新 request 還在打到準備關閉的 pod 上。&lt;/p>
&lt;p>很多團隊修法是 &lt;em>把 terminationGracePeriodSeconds 從 30 拉到 120&lt;/em>、暫時掩蓋問題；但症狀會在下次 rolling update / HPA scale-down / node drain 時換個形式回來。根因在 &lt;em>termination 序列&lt;/em> — pod 不是收到 SIGTERM 就 graceful、序列裡每一步出錯都有不同 fail mode。&lt;/p>
&lt;h2 id="termination-序列五步每步都能爆">Termination 序列：五步、每步都能爆&lt;/h2>
&lt;p>K8s 收到 delete pod 請求後、發生的事 &lt;em>按時間&lt;/em> 是：&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>t=0&lt;/td>
 &lt;td>API server 標 pod 為 Terminating&lt;/td>
 &lt;td>kubelet 收到 delete&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=0&lt;/td>
 &lt;td>Pod 從 Service Endpoints 移除（&lt;strong>async&lt;/strong>）&lt;/td>
 &lt;td>endpoint controller&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=0&lt;/td>
 &lt;td>kubelet 跑 preStop hook（若有定義）&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=preStop 結束&lt;/td>
 &lt;td>container 收到 SIGTERM&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>t=SIGTERM + terminationGracePeriodSeconds&lt;/td>
 &lt;td>container 收到 SIGKILL&lt;/td>
 &lt;td>container runtime&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵誤解：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>「pod 從 Service 移除」跟「container 收到 SIGTERM」是 &lt;em>平行&lt;/em>、不是序列&lt;/strong>。Endpoint controller 更新 Endpoints object → kube-proxy 重新寫 iptables → 各 node 的 traffic 才真正停 — 這條鏈通常需要 &lt;em>1-5 秒&lt;/em>；同時間 SIGTERM 已經發給 application。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>preStop hook 是「container 還在跑、SIGTERM 還沒發」期間執行&lt;/strong>。pre-Stop 設 &lt;code>sleep 10&lt;/code> 是 production 標準作法 — 用 sleep 讓 endpoint controller 有時間把 pod 從 Service 移除、避免 SIGTERM 期間還有新 request 進來。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a> overview 的 implementation-layer deep article。Overview 已說明 K8s 在 deployment platform 譜系的定位、本文聚焦 <em>pod termination</em> 這個 production 最常踩、被誤解最深的議題：序列、配置、五個 case、跟 service mesh 整合。</p></blockquote>
<h2 id="graceful-shutdown-沒做對500-期間每次-deploy-都吃-502">Graceful shutdown 沒做對、500 期間每次 deploy 都吃 502</h2>
<p>最常見的觸發場景：deploy 新 image、prometheus alert 在 5 分鐘內收到一波 502 / 503、SRE 翻 application log 看到「正在處理 request」「connection closed」交替出現。Application 本身沒 bug、但 K8s 在 pod terminate 時跟 traffic 來源 <em>沒對齊步調</em>、舊 pod 還在處理請求時就被 SIGKILL、新 request 還在打到準備關閉的 pod 上。</p>
<p>很多團隊修法是 <em>把 terminationGracePeriodSeconds 從 30 拉到 120</em>、暫時掩蓋問題；但症狀會在下次 rolling update / HPA scale-down / node drain 時換個形式回來。根因在 <em>termination 序列</em> — pod 不是收到 SIGTERM 就 graceful、序列裡每一步出錯都有不同 fail mode。</p>
<h2 id="termination-序列五步每步都能爆">Termination 序列：五步、每步都能爆</h2>
<p>K8s 收到 delete pod 請求後、發生的事 <em>按時間</em> 是：</p>
<table>
  <thead>
      <tr>
          <th>時序</th>
          <th>事件</th>
          <th>動作來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>t=0</td>
          <td>API server 標 pod 為 Terminating</td>
          <td>kubelet 收到 delete</td>
      </tr>
      <tr>
          <td>t=0</td>
          <td>Pod 從 Service Endpoints 移除（<strong>async</strong>）</td>
          <td>endpoint controller</td>
      </tr>
      <tr>
          <td>t=0</td>
          <td>kubelet 跑 preStop hook（若有定義）</td>
          <td>container runtime</td>
      </tr>
      <tr>
          <td>t=preStop 結束</td>
          <td>container 收到 SIGTERM</td>
          <td>container runtime</td>
      </tr>
      <tr>
          <td>t=SIGTERM + terminationGracePeriodSeconds</td>
          <td>container 收到 SIGKILL</td>
          <td>container runtime</td>
      </tr>
  </tbody>
</table>
<p>關鍵誤解：</p>
<ol>
<li>
<p><strong>「pod 從 Service 移除」跟「container 收到 SIGTERM」是 <em>平行</em>、不是序列</strong>。Endpoint controller 更新 Endpoints object → kube-proxy 重新寫 iptables → 各 node 的 traffic 才真正停 — 這條鏈通常需要 <em>1-5 秒</em>；同時間 SIGTERM 已經發給 application。</p>
</li>
<li>
<p><strong>preStop hook 是「container 還在跑、SIGTERM 還沒發」期間執行</strong>。pre-Stop 設 <code>sleep 10</code> 是 production 標準作法 — 用 sleep 讓 endpoint controller 有時間把 pod 從 Service 移除、避免 SIGTERM 期間還有新 request 進來。</p>
</li>
<li>
<p><strong>terminationGracePeriodSeconds 是 <em>從 preStop 開始</em> 計時、不是從 SIGTERM</strong>。preStop sleep 10s + application 30s graceful = 至少要設 40s。</p>
</li>
<li>
<p><strong>graceful 不是 framework 自動的</strong>。Application 必須 <em>主動處理 SIGTERM</em>：拒絕新 request、等 in-flight 完成、close DB connection、flush log。沒處理 SIGTERM、container 會在 grace period 後被強殺。</p>
</li>
<li>
<p><strong>readiness probe 在 Terminating 期間 <em>仍會被執行</em>、但結果不影響 traffic</strong>（已經從 Endpoints 移除）。但若 application 沒主動讓 readiness fail、service mesh / external LB 可能仍在送 request（依不同 mesh 行為）。</p>
</li>
</ol>
<h2 id="配置全圖">配置全圖</h2>
<h3 id="deployment-spec">Deployment spec</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">          </span><span class="c"># SIGTERM 後 60s 才 SIGKILL</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">lifecycle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">            </span><span class="nt">preStop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">              </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">                </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/bin/sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;sleep 10&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz/ready</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">2</span></span></span></code></pre></div><p>時序：t=0 preStop 開始 sleep 10s → t=10s container SIGTERM → t=70s SIGKILL（不是 t=60s、是 60s after SIGTERM）。</p>
<h3 id="application-處理-sigtermgo-範例">Application 處理 SIGTERM（Go 範例）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">sigs</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">os</span><span class="p">.</span><span class="nx">Signal</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">signal</span><span class="p">.</span><span class="nf">Notify</span><span class="p">(</span><span class="nx">sigs</span><span class="p">,</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">)</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="nx">server</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="p">{</span><span class="nx">Addr</span><span class="p">:</span> <span class="s">&#34;:8080&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">go</span> <span class="nx">server</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">()</span>
</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="o">&lt;-</span><span class="nx">sigs</span>                                              <span class="c1">// 等 SIGTERM</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;SIGTERM received, draining...&#34;</span><span class="p">)</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="c1">// 1. readiness fail（讓 mesh-aware 流量停）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nx">ready</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">false</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="c1">// 2. wait 5s 讓 readiness probe failureThreshold 觸發</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">// 3. graceful shutdown server（拒新請求、等 in-flight）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">45</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nf">Shutdown</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">// 4. close DB / cache / message consumer</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nx">consumer</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</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 class="c1">// 5. flush log + exit</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Sync</span><span class="p">()</span></span></span></code></pre></div><p>關鍵：<code>server.Shutdown(ctx)</code> 是 <em>拒新請求、等 in-flight</em>、ctx timeout 設 <em>grace period 減去 preStop sleep 跟 readiness fail 等待時間</em>（60s - 10s - 5s = 45s）。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1rolling-update-期間-502--503">Case 1：Rolling update 期間 502 / 503</h3>
<p><strong>徵兆</strong>：每次 deploy 後 5 分鐘內 LB / ingress log 一波 502 / 503、application log 顯示「context canceled」「connection closed by peer」、新 pod 已 ready 但舊 pod 在 grace period 內仍收 request。</p>
<p><strong>根因</strong>：沒設 preStop sleep、container 收到 SIGTERM 後立刻 <code>server.Shutdown()</code>、但 kube-proxy 還沒把舊 pod 從 iptables 移除、新 request 持續送到舊 pod、舊 pod 已拒收。</p>
<p><strong>修法</strong>：preStop <code>sleep 10</code>、讓 endpoint propagation 完成再進入 SIGTERM 流程。</p>
<h3 id="case-2connection-drain-racelong-running-request-被中斷">Case 2：Connection drain race，long-running request 被中斷</h3>
<p><strong>徵兆</strong>：deploy 後 application log 有大量 <code>context canceled</code> 對應到 long-running endpoint（例：報表生成、檔案上傳）、user 端看到 transaction 失敗、但短 request 沒事。</p>
<p><strong>根因</strong>：long-running endpoint 處理時間 &gt; terminationGracePeriodSeconds、<code>server.Shutdown(ctx)</code> ctx timeout 設太短、in-flight 強制中斷。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>把 long-running endpoint 改 async（背景 job + status endpoint）、HTTP request 立刻 return job ID</li>
<li>短期：terminationGracePeriodSeconds 拉到 long-running 99 percentile + buffer</li>
<li>application 側 ctx timeout = grace period - preStop - readiness fail wait</li>
</ol>
<h3 id="case-3init-container-在-grace-period-期間重啟sigterm-沒到-main">Case 3：Init container 在 grace period 期間重啟、SIGTERM 沒到 main</h3>
<p><strong>徵兆</strong>：pod 顯示 Terminating 但 phase 一直在 Running、main container restart count + 1、application log 沒看到「SIGTERM received」。</p>
<p><strong>根因</strong>：init container 用 <code>restartPolicy: Always</code>（K8s 1.28+ sidecar 模式）、或 main container 在 SIGTERM 前先 crash 觸發 restart、kubelet 在 restart 後 <em>不重發 SIGTERM</em>、main container 跑到 grace period 結束直接 SIGKILL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Sidecar container（restartPolicy: Always）的 preStop 也要設 <code>sleep</code>、跟 main 同 lifecycle</li>
<li>main container readinessProbe 失敗時 <em>別自動 restart</em>（restartPolicy: OnFailure + crashLoopBackOff 觀察）</li>
<li>觀察 <code>kubectl describe pod</code> 的 events、SIGTERM 沒發出來會有 <code>Killing container</code> event 缺失</li>
</ol>
<h3 id="case-4statefulset-串行終止總時間--pod-數--grace-period">Case 4：StatefulSet 串行終止、總時間 = pod 數 × grace period</h3>
<p><strong>徵兆</strong>：StatefulSet rolling update / scale-down 比 Deployment 慢 N 倍（N = replica 數）、deploy 一個 5 replica 的 statefulset 要 5 分鐘以上。</p>
<p><strong>根因</strong>：StatefulSet 預設 <code>podManagementPolicy: OrderedReady</code> — pod 串行終止 + 串行創建、每個 pod 至少要 grace period 完成才動下一個。Deployment 用 <code>RollingUpdate</code> 預設 maxUnavailable=25% 平行終止。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>StatefulSet 改 <code>podManagementPolicy: Parallel</code>（若 application 不要求嚴格順序）</li>
<li>嚴格順序情境（Cassandra / Kafka / etcd）保留 OrderedReady、但 grace period 設 <em>單 pod 必要時間</em>、不要設 <em>總時間能承受</em></li>
<li>接受序列化代價、把 deploy 排在低流量時段</li>
</ol>
<h3 id="case-5job--cronjob-不-gracefulsigterm-直接-sigkill">Case 5：Job / CronJob 不 graceful、SIGTERM 直接 SIGKILL</h3>
<p><strong>徵兆</strong>：CronJob 在 Job timeout / pod eviction 時不 graceful、寫一半的 file 留在 PVC、下次跑時 corrupt；application log 沒「SIGTERM received」、直接斷。</p>
<p><strong>根因</strong>：Job 的 <code>activeDeadlineSeconds</code> 到期 / node eviction 觸發時、K8s 對 Job pod <em>仍會發 SIGTERM</em>、但 <em>很多 batch framework（Spring Batch / Argo Workflow worker）沒處理 SIGTERM</em>、application 沒主動 checkpoint。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Batch application 處理 SIGTERM、checkpoint 進度寫 storage、下次跑時 resume</li>
<li>不適合 checkpoint 的 batch、保證 <em>idempotent re-run</em>、SIGKILL 後重跑不會 corrupt</li>
<li>Job spec 加 <code>terminationGracePeriodSeconds</code>（預設 30、batch 通常要 60-300）</li>
</ol>
<h2 id="規模影響">規模影響</h2>
<p>Graceful shutdown 的成本主要在 <em>deploy 時間</em> 跟 <em>capacity buffer</em>：</p>
<table>
  <thead>
      <tr>
          <th>規模因素</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>terminationGracePeriod 60s</td>
          <td>單 pod deploy ~70-80s（含 preStop + grace + new pod startup）</td>
      </tr>
      <tr>
          <td>Deployment 100 replica + maxSurge 25%</td>
          <td>全 deploy ~5-10 分鐘、需要 <em>25% extra capacity</em>（25 replica buffer）</td>
      </tr>
      <tr>
          <td>StatefulSet 串行 + 60s grace</td>
          <td>10 replica 約 10-12 分鐘、deploy window 要在低流量時段</td>
      </tr>
      <tr>
          <td>HPA scale-down 跟 graceful 一起跑</td>
          <td>scale-down 觸發 → preStop + grace + new metric → 下次 scale 判斷、avg 反應週期 ≈ 3-5 分鐘</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>Web service：<code>terminationGracePeriodSeconds: 60</code>、preStop sleep 10、application graceful 45s</li>
<li>Backend worker（消費 queue）：<code>terminationGracePeriodSeconds: 120</code>、preStop 不 sleep（用 readiness 控）、application 處理當前 message + commit offset</li>
<li>Batch job：<code>terminationGracePeriodSeconds: 300</code>、checkpoint pattern</li>
<li>StatefulSet（DB / queue）：grace period 對齊 vendor 建議（Kafka 90s、PostgreSQL 60s）</li>
</ul>
<h2 id="跟其他元件整合">跟其他元件整合</h2>
<h3 id="service-meshistio--linkerd">Service mesh（Istio / Linkerd）</h3>
<p>Service mesh sidecar（envoy / linkerd-proxy）也有自己的 termination — 通常比 main container 晚一點關。配置原則：</p>
<ol>
<li>mesh sidecar 設 <code>terminationGracePeriodSeconds</code> 比 main 多 5-10s、main 處理完才換 sidecar</li>
<li>Istio 1.12+ 的 <code>proxy.istio.io/config.holdApplicationUntilProxyStarts</code> 控啟動順序、shutdown 也要對應</li>
<li>mTLS 環境 graceful 多一道：在 SIGTERM 後等 mesh 主動 close cert rotation、不要硬斷</li>
</ol>
<h3 id="readiness-probe-跟-mesh-aware-traffic">Readiness probe 跟 mesh-aware traffic</h3>
<p>純 K8s Service（kube-proxy iptables）：endpoint 移除後 <em>已建立 connection 仍會跑完</em>、新 connection 不來。Mesh-aware traffic（service mesh / external LB with health check）：要 readiness fail 才會停送。</p>
<p>修法：application graceful 第一步是 <code>ready.Store(false)</code> + 等 readiness probe 至少 fail 一次（5-10s）、才開始 server.Shutdown。</p>
<h3 id="跟-pod-disruption-budgetpdb的衝突">跟 Pod Disruption Budget（PDB）的衝突</h3>
<p>Node drain 時 PDB 限制可同時 unavailable 的 pod 數、graceful shutdown 拖長會讓 drain 卡住。對策：</p>
<ol>
<li>緊急 drain（node 硬體故障）：<code>kubectl drain --grace-period=30 --force</code>、接受短時間 502</li>
<li>正常 drain（升級 / 維運）：PDB 設 <code>minAvailable: &lt;replicas-1&gt;</code>、容許單 pod 慢慢 graceful</li>
<li>不要設 <code>maxUnavailable: 0</code>、會讓 drain 卡死</li>
</ol>
<h2 id="下一步">下一步</h2>
<ul>
<li><strong>Application graceful 寫法</strong>：<a href="https://12factor.net/disposability">12-factor app</a> disposability 章節給 framework-agnostic 模板、各語言 SDK 寫法見對應 framework</li>
<li><strong>Queue consumer 的 graceful</strong>：訊息 ack / offset commit 必須在 SIGTERM 內完成、否則 duplicate message — 對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 模組的 consumer-design 段</li>
<li><strong>跨 region / 多 cluster 的 graceful</strong>：multi-cluster service mesh（Istio multicluster / Linkerd multicluster）的 traffic shift 期間 graceful 行為跟單 cluster 不同、需要對齊 mesh 配置</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></li>
<li>上游 chapter：<a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.X deployment-rollout-drain-rollback</a></li>
<li>對照案例：rolling update 期間 502 多見於 stage-3 mesh adoption case 庫</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Let's Encrypt</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/letsencrypt/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/letsencrypt/</guid><description>&lt;p>Let&amp;rsquo;s Encrypt 是免費 + 自動化的公共 ACME CA（Certificate Authority）、由 Internet Security Research Group (ISRG) 營運、簽發 DV（Domain Validation）等級的 public TLS cert。它的核心設計選擇是 &lt;em>只發 90 天 TTL 的 cert + 完全自動化的 ACME protocol&lt;/em>、把人工管理選項從工程實務中拿掉、強迫 cert lifecycle 走機器化路線。今天大多數 public-facing web service 的 TLS cert 都直接或間接從 Let&amp;rsquo;s Encrypt 來、是現代 Web 的事實基礎設施之一。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Let&amp;rsquo;s Encrypt 的角色是 &lt;em>跨雲、跨平台、跨組織規模&lt;/em> 的公共 DV cert 來源。對於需要 public TLS cert 又不被特定雲廠綁定的場景（on-prem、edge node、跨雲 service、自架 CDN origin、開源專案）、Let&amp;rsquo;s Encrypt 是預設選項。它解決的問題不是「能不能拿到 cert」、而是「能不能 &lt;em>無人值守&lt;/em> 持續拿到 cert」— ACME protocol 把申請、驗證、issue、renew、revoke 全部標準化、ACME client（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&amp;#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &amp;#43; Challenge solver">cert-manager&lt;/a> / certbot / acme.sh / Caddy / Traefik）負責 client 端執行。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &amp;#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM&lt;/a> 比、Let&amp;rsquo;s Encrypt 跨雲跨平台、ACM 限 AWS-managed service（ALB / CloudFront / API Gateway）內使用、export 出去要另談；ACM Private CA 又是另一個產品。跟商業 CA（DigiCert / Sectigo / Entrust）比、商業 CA 提供 OV（Organization Validation）/ EV（Extended Validation）cert、cert 內含經過驗證的組織資訊、金融網站或法遵需求會用；Let&amp;rsquo;s Encrypt 只發 DV cert、不驗證組織身份。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault PKI&lt;/a> 比、Vault PKI 是 &lt;em>internal CA&lt;/em>（不被公共瀏覽器信任、適合 internal mTLS / workload identity）、Let&amp;rsquo;s Encrypt 是 &lt;em>public CA&lt;/em>（瀏覽器信任、適合 public-facing service）— 兩個是互補關係、不是替代。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>哪些 cert 需求適合 Let&amp;rsquo;s Encrypt（public-facing、DV、跨平台）、哪些該走 ACM / 商業 CA / Vault PKI&lt;/li>
&lt;li>ACME protocol 的四個 first-class concept（Account / Order / Authorization / Challenge）跟自己選的 ACME client 怎麼對應&lt;/li>
&lt;li>Rate limit 是 &lt;em>硬限制&lt;/em>、SaaS 多 tenant 場景如何規劃（wildcard / SAN / rate limit exemption）&lt;/li>
&lt;li>90 天 TTL + CT log 公開 + revocation 弱化 在 production 設計上的影響&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Let&amp;rsquo;s Encrypt 使用是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>Let&rsquo;s Encrypt 是免費 + 自動化的公共 ACME CA（Certificate Authority）、由 Internet Security Research Group (ISRG) 營運、簽發 DV（Domain Validation）等級的 public TLS cert。它的核心設計選擇是 <em>只發 90 天 TTL 的 cert + 完全自動化的 ACME protocol</em>、把人工管理選項從工程實務中拿掉、強迫 cert lifecycle 走機器化路線。今天大多數 public-facing web service 的 TLS cert 都直接或間接從 Let&rsquo;s Encrypt 來、是現代 Web 的事實基礎設施之一。</p>
<h2 id="服務定位">服務定位</h2>
<p>Let&rsquo;s Encrypt 的角色是 <em>跨雲、跨平台、跨組織規模</em> 的公共 DV cert 來源。對於需要 public TLS cert 又不被特定雲廠綁定的場景（on-prem、edge node、跨雲 service、自架 CDN origin、開源專案）、Let&rsquo;s Encrypt 是預設選項。它解決的問題不是「能不能拿到 cert」、而是「能不能 <em>無人值守</em> 持續拿到 cert」— ACME protocol 把申請、驗證、issue、renew、revoke 全部標準化、ACME client（<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> / certbot / acme.sh / Caddy / Traefik）負責 client 端執行。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> 比、Let&rsquo;s Encrypt 跨雲跨平台、ACM 限 AWS-managed service（ALB / CloudFront / API Gateway）內使用、export 出去要另談；ACM Private CA 又是另一個產品。跟商業 CA（DigiCert / Sectigo / Entrust）比、商業 CA 提供 OV（Organization Validation）/ EV（Extended Validation）cert、cert 內含經過驗證的組織資訊、金融網站或法遵需求會用；Let&rsquo;s Encrypt 只發 DV cert、不驗證組織身份。跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault PKI</a> 比、Vault PKI 是 <em>internal CA</em>（不被公共瀏覽器信任、適合 internal mTLS / workload identity）、Let&rsquo;s Encrypt 是 <em>public CA</em>（瀏覽器信任、適合 public-facing service）— 兩個是互補關係、不是替代。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 cert 需求適合 Let&rsquo;s Encrypt（public-facing、DV、跨平台）、哪些該走 ACM / 商業 CA / Vault PKI</li>
<li>ACME protocol 的四個 first-class concept（Account / Order / Authorization / Challenge）跟自己選的 ACME client 怎麼對應</li>
<li>Rate limit 是 <em>硬限制</em>、SaaS 多 tenant 場景如何規劃（wildcard / SAN / rate limit exemption）</li>
<li>90 天 TTL + CT log 公開 + revocation 弱化 在 production 設計上的影響</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Let&rsquo;s Encrypt 使用是否健康、最少看四件事：</p>
<ul>
<li><strong>Account 管理</strong>：ACME account 是 <em>cross-domain</em> 的身份、同一個 account 可以申請組織所有 domain 的 cert — account key 外洩等於 attacker 可以對所有 domain 發 cert；account key 是否離線備份、是否跟 ACME client 用獨立 key（不重用 server key）</li>
<li><strong>Challenge 選擇</strong>：HTTP-01 需要 port 80 reachable、適合單機 + 直接 internet 暴露；DNS-01 需要 DNS API access、適合 wildcard + 私有環境；TLS-ALPN-01 走 443、適合 port 80 不可用的場景 — Challenge 選錯會卡在 validation 階段</li>
<li><strong>Rate limit 規劃</strong>：50 cert/week per registered domain、5 duplicate cert/week — 大型 SaaS 服務多 customer subdomain 容易撞牆、要先估 cert 量、再決定 wildcard / SAN / rate limit 申請</li>
<li><strong>Revocation 流程</strong>：cert 被洩漏怎麼辦 — revoke 不是 fleet-wide invalidation、real-world 失效靠 <em>rotate + 短 TTL</em>；revocation 程序是否寫入 runbook、舊 cert 是否在所有 endpoint 確實 retire</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle</a> 跟 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">Credential Rotation Scoped Evidence</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>ACME client 選擇</strong>：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> 適合 K8s 環境、Ingress / Gateway / Certificate CRD 自動化；certbot 適合單機 / VM、官方參考實作；acme.sh 是 pure shell、嵌入既有 deployment script 容易；Caddy / Traefik 把 ACME 內建進 reverse proxy、零設定拿 cert。client 端的選擇決定 <em>cert 怎麼存、怎麼 deploy 到 termination point</em>、Let&rsquo;s Encrypt 自己不管這層。</p>
<p><strong>ACME Account（cross-domain identity）</strong>：Account 是 ACME server 認可的身份、用一把 account key（不同於 cert private key）簽 ACME request。同一個 account 可以申請 <em>組織所有 domain</em> 的 cert — 安全意義是 account key 外洩 = attacker 對所有 domain 都能 issue cert。Production 場景把 account key 視為跟 root signing key 同等級的 secret、離線備份、跟日常 ACME client 用獨立 key。</p>
<p><strong>Challenge 選擇 — HTTP-01 / DNS-01 / TLS-ALPN-01</strong>：HTTP-01 在 <code>/.well-known/acme-challenge/&lt;token&gt;</code> 放 response、Let&rsquo;s Encrypt 從 port 80 拉、適合單機 + 直接 internet 暴露；DNS-01 在 <code>_acme-challenge.&lt;domain&gt;</code> 放 TXT record、適合 wildcard cert（<code>*.example.com</code> 必須 DNS-01、HTTP-01 不行）跟私有環境（不需要 port 80 開放）；TLS-ALPN-01 走 port 443、用 special ALPN extension 回 challenge、適合 port 80 被擋的場景。Wildcard cert 強制 DNS-01 是 Let&rsquo;s Encrypt 政策、不能用 HTTP-01 繞過。</p>
<p><strong>Rate limit 是硬限制</strong>：50 cert/week per registered domain（包含 SAN 在內）、5 duplicate cert/week（同樣 SAN 組合）、300 new orders/3 hours per account、5 failed validation/hour。大型 SaaS 對 N 個 customer subdomain 發 cert 容易撞牆 — 解法有三：用 wildcard cert 把多 subdomain 合一張（單張 cert 服務無限 subdomain）、用 SAN cert 把多個 subdomain 寫進同一張 cert、申請 rate limit 上限提高（<a href="https://isrg.formstack.com/forms/rate_limit_adjustment_request">官方表單</a>）。撞 rate limit 後該 domain 整個 week 不能發新 cert、是 production outage 等級。</p>
<p><strong>Staging environment 必用於測試</strong>：<code>acme-staging-v02.api.letsencrypt.org</code> 是 Let&rsquo;s Encrypt 的測試 endpoint、cert 不被瀏覽器信任、但 <em>rate limit 寬鬆很多</em>（30000 cert/week / 60 duplicate cert/week）。debug ACME client 設定、新 deploy pipeline、CI 跑 cert renewal test 都應該先指 staging、確認 OK 再切 production endpoint。直接在 production 試錯撞 rate limit 是常見事故。</p>
<p><strong>90 天 TTL + 60 天 renew cadence</strong>：Let&rsquo;s Encrypt cert 固定 90 天 TTL、ACME client convention 是 <em>過 60 天就開始 renew</em>、留 30 天 buffer 給 retry。90 天是 <em>設計選擇</em>、不是技術限制 — 短 TTL 強迫自動化、把「過期前手動處理」這個失敗模式從設計中拿掉。如果你的 cert renewal 還需要人介入、表示 ACME client / deployment pipeline / monitoring 哪邊沒做好、要在 60 天 buffer 內修。</p>
<p><strong>CT log 公開可查</strong>：Let&rsquo;s Encrypt cert 都會進 Certificate Transparency log（CT log）、可以用 <a href="https://crt.sh">crt.sh</a> 查任何 domain 的歷史 cert。對 production 意義有兩面：blue team 可以監控自家 domain 的 unexpected cert（attacker 用相似 domain 釣魚會留痕）；red team 可以查 target 公司新出現的 internal hostname（cert 上的 SAN 等於公開的 service inventory）。對 <em>internal-only</em> hostname、不要用 Let&rsquo;s Encrypt cert、否則 SAN 變成 recon 資料源 — 內部服務走 Vault PKI / 私有 CA。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Let&rsquo;s Encrypt</th>
          <th>AWS ACM</th>
          <th>商業 CA（DigiCert / Sectigo）</th>
          <th>Vault PKI（internal CA）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>信任範圍</td>
          <td>Public（公共瀏覽器信任）</td>
          <td>Public（公共瀏覽器信任）</td>
          <td>Public（公共瀏覽器信任）</td>
          <td>Internal（需要客戶端裝 CA cert）</td>
      </tr>
      <tr>
          <td>部署範圍</td>
          <td>跨雲、跨平台、on-prem</td>
          <td>限 AWS-managed service（ALB / CF / APIGW）</td>
          <td>跨雲、跨平台</td>
          <td>自管、跨雲皆可</td>
      </tr>
      <tr>
          <td>Cert 等級</td>
          <td>DV（Domain Validation）</td>
          <td>DV（ACM）/ Private CA 任意</td>
          <td>DV / OV / EV</td>
          <td>自定義（內部信任）</td>
      </tr>
      <tr>
          <td>費用</td>
          <td>免費</td>
          <td>免費（ACM public）/ Private CA 收費</td>
          <td>收費（DV / OV / EV 各價位）</td>
          <td>自管成本</td>
      </tr>
      <tr>
          <td>自動化</td>
          <td>ACME protocol 標準化</td>
          <td>ACM 自動 renew（限 AWS-managed service）</td>
          <td>多數需手動 / API 申請、自動化弱</td>
          <td>自管 + ACME server 可選</td>
      </tr>
      <tr>
          <td>TTL</td>
          <td>90 天（硬性）</td>
          <td>13 個月（AWS rotate）</td>
          <td>1-2 年</td>
          <td>自訂</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>public-facing、跨雲、open source、SaaS</td>
          <td>AWS-only + ALB/CloudFront 內</td>
          <td>金融、政府、需要 EV 顯示組織</td>
          <td>internal mTLS、workload identity、企業內部 service</td>
      </tr>
      <tr>
          <td>不適合場景</td>
          <td>internal mTLS、EV cert、cert 內需含組織</td>
          <td>跨雲、export 出 AWS</td>
          <td>需要快速自動化、預算敏感</td>
          <td>public-facing、不能要求客戶端裝 CA</td>
      </tr>
  </tbody>
</table>
<p>選 Let&rsquo;s Encrypt 的核心訴求：<em>public-facing + DV 等級夠用 + 跨平台 + 需要自動化</em>。需要 EV cert 走商業 CA、需要 internal mTLS 走 Vault PKI、AWS-only + 留在 ALB / CloudFront 內走 ACM 更省事。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Rate limit 規劃跟 SaaS 多 tenant</strong>：N 個 customer subdomain 場景下、單 domain 50 cert/week 很容易撞牆。設計選項：(1) wildcard cert（<code>*.app.example.com</code>）一張覆蓋無限 subdomain、但 wildcard cert 不能保護 nested subdomain（<code>*.app.example.com</code> 不蓋 <code>foo.bar.app.example.com</code>）；(2) SAN cert 把多個 subdomain 寫進同一張 cert（單張最多 100 個 SAN）、適合 customer 數固定、新增不頻繁的場景；(3) 申請 rate limit 上限提高、production scale SaaS 走這條；(4) cert reuse — 同樣 SAN 組合在 5 duplicate cert/week 內可 reuse、不重發。</p>
<p><strong>跟 cert-manager + DNS-01 整合</strong>：production K8s 環境最常見組合是 cert-manager + Let&rsquo;s Encrypt + DNS-01、DNS provider 走 Route53 / Cloud DNS / Cloudflare。cert-manager 用 ClusterIssuer 設定 Let&rsquo;s Encrypt account + DNS solver、Certificate CRD 宣告需要的 cert、cert-manager 自動完成 ACME flow。優勢是 <em>wildcard cert 可用</em>（DNS-01 不受 HTTP-01 的 port 80 限制）、跨 cluster 可標準化、cert renewal 進 K8s event stream 容易監控。</p>
<p><strong>ACME profiles（client-specific behavior）</strong>：Let&rsquo;s Encrypt 2024 開始提供 ACME profile 機制、允許 client 選擇 cert 屬性（如 short-lived 6 天 cert vs standard 90 天）。short-lived cert 適合機器 workload、進一步壓縮 revocation 缺陷的影響窗口；普通 web service 用 standard profile 即可。Profile 是 opt-in、ACME client 要支援。</p>
<p><strong>跨 ACME CA fallback</strong>：Let&rsquo;s Encrypt 不是唯一 ACME CA — ZeroSSL、Buypass、Google Trust Services 都提供 ACME endpoint。production 建議 ACME client 設兩個 issuer（Let&rsquo;s Encrypt primary + ZeroSSL / Buypass secondary）、Let&rsquo;s Encrypt 出事（rate limit 撞牆、AWS outage 影響 challenge 驗證、ISRG 服務中斷）時可以 fallback、不會 cert 全停。cert-manager 用兩個 ClusterIssuer 即可、application 端零感知。</p>
<p><strong>Revocation 的弱化現實</strong>：cert 可以 revoke、但實際失效路徑薄弱 — CRL（Certificate Revocation List）跟 OCSP（Online Certificate Status Protocol）更新有延遲、且大多數 client（瀏覽器、API client）不會主動檢查 revocation 狀態（soft-fail：查不到就放行）。real-world 的 cert 失效機制其實是 <em>短 TTL + rotate</em>、不是 revocation API。設計時不要寄望 revoke 後 attacker 拿到的 cert 就無效 — rotate 出新 cert + 在所有 endpoint deploy 新 cert + 觀察舊 cert traffic 歸零、才算真正失效。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>ACME challenge 失敗</strong>：HTTP-01 拉不到 <code>/.well-known/acme-challenge/&lt;token&gt;</code>、檢查 port 80 reachability、firewall、CDN 是否擋；DNS-01 TXT record 沒生效、檢查 DNS provider API permission、TXT TTL 是否設太長</li>
<li><strong>撞 rate limit</strong>：50 cert/week per registered domain 撞牆、整個 week 不能發新 cert — production 必須先 <em>staging 測完</em> 再切 production、cert reuse 機制要開（同 SAN 組合不重發）、長期解走 wildcard / SAN consolidation / rate limit exemption</li>
<li><strong>Renewal 沒在 60 天前開始</strong>：cert 過期前才 renew、撞到 ACME server 暫時不可用會直接過期 — ACME client 設 60 天 renew threshold、cert expiry 30 天前 alert 給 oncall</li>
<li><strong>Account key 沒備份</strong>：account key 弄丟、可以重新註冊但 <em>舊 cert 的 revocation 權限沒了</em>（除非用 cert 私鑰 revoke）— account key 跟 root signing key 同等級保護、離線備份</li>
<li><strong>CT log 暴露 internal hostname</strong>：Let&rsquo;s Encrypt cert 進 CT log、internal-only hostname 的 SAN 變 recon 資料源 — internal service 不用 Let&rsquo;s Encrypt、改 Vault PKI / 私有 CA</li>
<li><strong>Wildcard cert 用 HTTP-01</strong>：<code>*.example.com</code> 申請失敗、Let&rsquo;s Encrypt 政策強制 wildcard 走 DNS-01 — 切到 DNS-01 solver、設定 DNS provider API access</li>
<li><strong>Cert 出事 revoke 後 attacker 還能用</strong>：revocation 不是 fleet-wide invalidation、CRL/OCSP 多數 client 不檢查 — 真正失效靠 rotate + 觀察舊 cert traffic 歸零、不是 revoke API</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only + 留在 ALB / CloudFront 內</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a></td>
      </tr>
      <tr>
          <td>需要 OV / EV cert（cert 含組織資訊）</td>
          <td>商業 CA（DigiCert / Sectigo / Entrust）</td>
      </tr>
      <tr>
          <td>Internal mTLS / workload identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault PKI</a> / <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
      </tr>
      <tr>
          <td>K8s workload cert 自動化（用 LE 當源）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a></td>
      </tr>
      <tr>
          <td>Cert lifecycle 治理（跨 vendor 通則）</td>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.4 Transport Trust and Certificate Lifecycle</a></td>
      </tr>
      <tr>
          <td>Cert rotation 證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ACME protocol RFC 8555 完整規格逐條解讀</li>
<li>每個 ACME client（certbot / cert-manager / acme.sh / Caddy / Traefik）的完整設定教學</li>
<li>Let&rsquo;s Encrypt 內部 CA infrastructure 跟 ISRG governance 細節</li>
<li>CT log 內部結構跟 SCT（Signed Certificate Timestamp）驗證流程</li>
<li>DNS provider 的 API 認證設定（Route53 IAM / Cloud DNS service account / Cloudflare API token）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Let&rsquo;s Encrypt 在 07 案例庫沒有直接 vendor-level 事件、以下案例採對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Let&rsquo;s Encrypt 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">Transport Trust and Certificate Lifecycle (section)</a></td>
          <td>Let&rsquo;s Encrypt 90 天 TTL + 強制 ACME 自動化、把人工依賴從 cert lifecycle 設計中拿掉、是 <em>forcing function 級別</em> 的治理選擇</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">Credential Rotation Scoped Evidence (section)</a></td>
          <td>Let&rsquo;s Encrypt 沒提供 fleet-wide revocation API、cert 出事後客戶側自己負責 fleet update + session invalidation、是 scope map 必要的典型情境</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>對照啟示 — cert rotation 跟 session invalidation 是兩件事、Let&rsquo;s Encrypt cert renew 不會 invalidate 既有 TLS session 跟 application-layer session、要分別處理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Let&rsquo;s Encrypt rate limit（50 cert/week per domain）是 scope-driven 設計的硬約束、單一 domain 不能無限 rotation、wildcard / SAN consolidation 必須納入 rotation 策略</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.4 Transport Trust and Certificate Lifecycle</a>、<a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.5 Credential Rotation Scoped Evidence</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a>、<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Vault PKI 處理 internal CA、跟 Let&rsquo;s Encrypt public CA 互補）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（cert 出事 / private key 外洩如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://letsencrypt.org/docs/">Let&rsquo;s Encrypt Documentation</a>、<a href="https://datatracker.ietf.org/doc/html/rfc8555">ACME RFC 8555</a>、<a href="https://crt.sh">crt.sh CT log search</a></li>
</ul>
]]></content:encoded></item><item><title>Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Splunk Enterprise Security 在 SIEM / Detection 譜系的定位、本文聚焦 &lt;em>Risk-Based Alerting (RBA)&lt;/em> 的實作層 — 從「per-rule alert」轉到「score 累積 + threshold 觸發 notable」的方法論轉變、跟 tuning / scaling / 整合的具體做法。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼-rbaalert-fatigue-是-detection-engineering-的天花板">為什麼 RBA：alert fatigue 是 detection engineering 的天花板&lt;/h2>
&lt;p>Detection engineering 的成熟度上限不是「能寫多少 correlation rule」、是「SOC analyst 能處理多少 alert / day 而不會麻木」。多數 SOC 在 200-500 alert/day 區間就到處理上限、再加 rule 只會推升 false positive、analyst 開始 silent ignore 中低嚴重度 alert。&lt;/p>
&lt;p>RBA 的核心轉折是 &lt;em>把 alert 邏輯從「rule 觸發」拆成「score 累積」&lt;/em>：每個 detection rule 不直接產 alert、而是給 &lt;em>user / asset / process&lt;/em> 加 risk score；多個低嚴重訊號累積到 threshold 才產 notable（高優先 case）。SOC 看的不是「rule X 觸發了」、是「user Y 今天累積 70 分、上週 12 分」。&lt;/p>
&lt;p>RBA 不是 &lt;em>寫 detection rule 的替代&lt;/em>、是 &lt;em>aggregation 跟 prioritization 的新層&lt;/em>。原本 100 條 rule 各自產 alert 變成 100 條 rule 共同貢獻 score、score → notable 是新的 alert 邊界。&lt;/p>
&lt;h2 id="rba-三層-modelmodifierscorenotable">RBA 三層 model：modifier、score、notable&lt;/h2>
&lt;p>Risk 流程的三個 first-class object：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Object&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Risk modifier&lt;/strong>&lt;/td>
 &lt;td>一條 detection rule 產出、提供「給誰加多少分、為什麼、什麼類別」&lt;/td>
 &lt;td>user &lt;code>alice@corp&lt;/code> +25 分、reason &lt;code>unusual_login_geo&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Risk index&lt;/strong>&lt;/td>
 &lt;td>累積所有 modifier、依時間衰減；query 出「user / asset 當前 score」&lt;/td>
 &lt;td>&lt;code>index=risk earliest=-7d&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Risk notable&lt;/strong>&lt;/td>
 &lt;td>當 score 累積超過 threshold 觸發、進 SOC case management&lt;/td>
 &lt;td>user 累積 50 分 → 開 incident&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵設計選擇都在 modifier 層：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>加分維度&lt;/strong>：per user / per asset / per process tree / per IP — 維度越細粒度、score 越能對應「個體」、但 query 成本越高&lt;/li>
&lt;li>&lt;strong>加分 weight&lt;/strong>：簡單做法 severity 直接對應（low=5 / med=15 / high=30 / critical=60）；細做要考慮 &lt;em>signal precision&lt;/em>（rule 的歷史 FP rate）&lt;/li>
&lt;li>&lt;strong>MITRE ATT&amp;amp;CK 對應&lt;/strong>：每個 modifier 標 tactic / technique、跟 ATT&amp;amp;CK 對應、用來判斷 &lt;em>kill chain 階段&lt;/em> 是否完整（reconnaissance → exfiltration 全套出現 vs 單一 tactic 重複）&lt;/li>
&lt;/ul>
&lt;h2 id="es-配置-step-by-step">ES 配置 step-by-step&lt;/h2>
&lt;h3 id="risk-modifier-從-correlation-search-產出">Risk modifier 從 correlation search 產出&lt;/h3>





&lt;pre tabindex="0">&lt;code class="language-spl" data-lang="spl">| search index=auth user=* unusual_geo=true
| stats count by user, src_ip, _time
| eval risk_score=25
| eval risk_object_type=&amp;#34;user&amp;#34;
| eval risk_object=user
| eval risk_message=&amp;#34;Unusual login geography&amp;#34;
| eval threat_object=src_ip
| eval threat_object_type=&amp;#34;ip_address&amp;#34;
| eval mitre_technique=&amp;#34;T1078&amp;#34;
| collect index=risk&lt;/code>&lt;/pre>&lt;p>關鍵欄位：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> overview 的 implementation-layer deep article。Overview 已說明 Splunk Enterprise Security 在 SIEM / Detection 譜系的定位、本文聚焦 <em>Risk-Based Alerting (RBA)</em> 的實作層 — 從「per-rule alert」轉到「score 累積 + threshold 觸發 notable」的方法論轉變、跟 tuning / scaling / 整合的具體做法。</p></blockquote>
<h2 id="為什麼-rbaalert-fatigue-是-detection-engineering-的天花板">為什麼 RBA：alert fatigue 是 detection engineering 的天花板</h2>
<p>Detection engineering 的成熟度上限不是「能寫多少 correlation rule」、是「SOC analyst 能處理多少 alert / day 而不會麻木」。多數 SOC 在 200-500 alert/day 區間就到處理上限、再加 rule 只會推升 false positive、analyst 開始 silent ignore 中低嚴重度 alert。</p>
<p>RBA 的核心轉折是 <em>把 alert 邏輯從「rule 觸發」拆成「score 累積」</em>：每個 detection rule 不直接產 alert、而是給 <em>user / asset / process</em> 加 risk score；多個低嚴重訊號累積到 threshold 才產 notable（高優先 case）。SOC 看的不是「rule X 觸發了」、是「user Y 今天累積 70 分、上週 12 分」。</p>
<p>RBA 不是 <em>寫 detection rule 的替代</em>、是 <em>aggregation 跟 prioritization 的新層</em>。原本 100 條 rule 各自產 alert 變成 100 條 rule 共同貢獻 score、score → notable 是新的 alert 邊界。</p>
<h2 id="rba-三層-modelmodifierscorenotable">RBA 三層 model：modifier、score、notable</h2>
<p>Risk 流程的三個 first-class object：</p>
<table>
  <thead>
      <tr>
          <th>Object</th>
          <th>責任</th>
          <th>例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Risk modifier</strong></td>
          <td>一條 detection rule 產出、提供「給誰加多少分、為什麼、什麼類別」</td>
          <td>user <code>alice@corp</code> +25 分、reason <code>unusual_login_geo</code></td>
      </tr>
      <tr>
          <td><strong>Risk index</strong></td>
          <td>累積所有 modifier、依時間衰減；query 出「user / asset 當前 score」</td>
          <td><code>index=risk earliest=-7d</code></td>
      </tr>
      <tr>
          <td><strong>Risk notable</strong></td>
          <td>當 score 累積超過 threshold 觸發、進 SOC case management</td>
          <td>user 累積 50 分 → 開 incident</td>
      </tr>
  </tbody>
</table>
<p>關鍵設計選擇都在 modifier 層：</p>
<ul>
<li><strong>加分維度</strong>：per user / per asset / per process tree / per IP — 維度越細粒度、score 越能對應「個體」、但 query 成本越高</li>
<li><strong>加分 weight</strong>：簡單做法 severity 直接對應（low=5 / med=15 / high=30 / critical=60）；細做要考慮 <em>signal precision</em>（rule 的歷史 FP rate）</li>
<li><strong>MITRE ATT&amp;CK 對應</strong>：每個 modifier 標 tactic / technique、跟 ATT&amp;CK 對應、用來判斷 <em>kill chain 階段</em> 是否完整（reconnaissance → exfiltration 全套出現 vs 單一 tactic 重複）</li>
</ul>
<h2 id="es-配置-step-by-step">ES 配置 step-by-step</h2>
<h3 id="risk-modifier-從-correlation-search-產出">Risk modifier 從 correlation search 產出</h3>





<pre tabindex="0"><code class="language-spl" data-lang="spl">| search index=auth user=* unusual_geo=true
| stats count by user, src_ip, _time
| eval risk_score=25
| eval risk_object_type=&#34;user&#34;
| eval risk_object=user
| eval risk_message=&#34;Unusual login geography&#34;
| eval threat_object=src_ip
| eval threat_object_type=&#34;ip_address&#34;
| eval mitre_technique=&#34;T1078&#34;
| collect index=risk</code></pre><p>關鍵欄位：</p>
<ul>
<li><code>risk_object</code> + <code>risk_object_type</code>：誰被加分、預設 user / system / other</li>
<li><code>risk_score</code>：加多少分、考量 signal precision</li>
<li><code>threat_object</code>：對應的 attacker artifact（IP / hash / domain）、用來跨 modifier 關聯</li>
<li><code>mitre_technique</code>：對應 ATT&amp;CK ID、用於 kill chain analysis</li>
</ul>
<p><em>Tuning 提醒</em>：第一次部署別直接 <code>collect index=risk</code>、先 <code>| table</code> 看 output、估算每天會產多少 modifier；超出 indexer 容量規劃前先做 sampling（<code>| where random()/2147483647&lt;0.1</code> 取 10%）。</p>
<h3 id="risk-notablethreshold-aggregation">Risk notable：threshold aggregation</h3>





<pre tabindex="0"><code class="language-spl" data-lang="spl">| tstats summariesonly=t count, sum(All_Risk.calculated_risk_score) as total_risk
  from datamodel=Risk.All_Risk
  where earliest=-24h
  by All_Risk.risk_object, All_Risk.risk_object_type
| where total_risk &gt; 80
| `risk_score_format`</code></pre><p><code>total_risk &gt; 80</code> 是觸發 notable 的 threshold。Tuning 重點：</p>
<ul>
<li><strong>Time window</strong>：-24h 是預設、但要看 <em>attack pattern average duration</em> 調整；APT 用 7-14 day window、commodity attack 用 4-12h</li>
<li><strong>Threshold value</strong>：80 是 <em>當量</em> 不是普世值、依 modifier weight 分佈調整；ES 7.0+ 預設建議 100、實務多在 60-150 區間</li>
<li><strong>Aggregation 維度</strong>：by user 是 default、但 lateral movement scenario 要 by asset、credential abuse 要 by service account</li>
</ul>
<p><em>Tuning 提醒</em>：第一週跑 <em>shadow mode</em> — 觸發 notable 但不 page、SOC 後續 review、調整 threshold 跟 weight；shadow 跑 1-2 週後再啟 production page。</p>
<h3 id="notable-enrichment人類能看的-case">Notable enrichment：人類能看的 case</h3>





<pre tabindex="0"><code class="language-spl" data-lang="spl">| eval description=&#34;User &#34;.risk_object.&#34; accumulated &#34;.total_risk.&#34; risk over 24h&#34;
| eval mitre_techniques=mvjoin(mitre_technique, &#34;, &#34;)
| eval contributing_rules=mvjoin(search_name, &#34;, &#34;)
| sendalert notable</code></pre><p>Notable 進入 ES Incident Review、SOC analyst 看到的不只 score、還有 <em>組成這 80 分的 N 條 rule + ATT&amp;CK 覆蓋的 tactic</em>；這是 RBA 比 per-rule alert 強的核心 — analyst 直接看完整 narrative、不用拼湊。</p>
<h2 id="tuning-playbook四類常見-drift">Tuning playbook：四類常見 drift</h2>
<h3 id="playbook-afalse-positive-累積">Playbook A：False positive 累積</h3>
<p><strong>徵兆</strong>：某 user 連續 N 天觸發 notable、SOC 每次 review 後 close 為 FP；但 modifier 仍持續加分。</p>
<p><strong>根因</strong>：modifier 加分邏輯沒考慮 baseline — 例：DBA 每天用 <code>psql</code> 連 prod 是正常、<code>unusual_command</code> rule 把它當異常加 15 分、累積到 threshold。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Modifier 端加 <code>whitelist_lookup</code>：DBA / SRE / approved service account 跳過 specific modifier</li>
<li>進階：modifier 加 <code>signal_precision</code> weight、historical FP rate &gt; 30% 的 rule weight 降到 5 分以下</li>
<li>不能輕易加 <code>NOT user IN (...)</code> exclusion、long whitelist 是反模式 — 用 <em>role-based exclusion</em>（query AD group）</li>
</ol>
<h3 id="playbook-bscore-inflation">Playbook B：Score inflation</h3>
<p><strong>徵兆</strong>：threshold 設 80、SOC 收到的 notable 每 day 從 5 個漲到 25 個、但「實際攻擊」沒對應增加。</p>
<p><strong>根因</strong>：新加的 detection rule 沒對齊既有 weight 分佈、新 rule 都給 +30 / +40、global average 抬升、threshold 變相降低。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>每加新 rule 時跑「+1 rule 對 daily notable 數的影響」shadow simulation</li>
<li>重新 calibrate threshold — 不是固定值、是 <em>p95 daily total_risk 的 1.5 倍</em></li>
<li>季度 review：跑 <code>index=risk | stats sum(risk_score) by source</code> 看 modifier 來源分佈、score 集中在少數 rule 是 inflation 訊號</li>
</ol>
<p><em>Tuning 提醒</em>：score inflation 跟 alert fatigue 是同樣症狀的不同根因；前者改 threshold + rule weight calibration、後者改 modifier 維度跟 whitelist。</p>
<h3 id="playbook-cthreshold-drift">Playbook C：Threshold drift</h3>
<p><strong>徵兆</strong>：threshold 設定半年沒動、但 attack landscape / business 行為都變了；要嘛 notable 太多（threshold 低於 baseline）、要嘛 missed detection（threshold 高於實際攻擊累積）。</p>
<p><strong>根因</strong>：threshold 是 <em>static value、但 baseline 是 dynamic</em>；business 流程變動（雲端遷移 / 新部門 / WFH 比例變化）影響 modifier 觸發頻率。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Quarterly tuning cadence：每季跑 <code>tstats sum(All_Risk.calculated_risk_score) by user | stats p50, p95, p99</code> 看分佈</li>
<li>Adaptive threshold：用 <code>p95 × 1.3</code> 動態計算、寫 macro 自動 update</li>
<li>不要把 threshold drift 當「rule 不準」、是 <em>基準漂移</em>、不是 rule 錯</li>
</ol>
<h3 id="playbook-ddecay-設計">Playbook D：Decay 設計</h3>
<p><strong>徵兆</strong>：user 7 天前的低分異常持續累積在 score 內、threshold 觸發 notable 但實際是 <em>7 天分散事件</em>、不是 <em>當前攻擊 episode</em>。</p>
<p><strong>根因</strong>：default RBA 在 <code>-24h</code> window 內 sum、沒考慮 <em>時間衰減</em>；7 天前的低分跟今天的低分權重一樣。</p>
<p><strong>修法</strong>：加 decay function、modifier weight 隨時間衰減：</p>





<pre tabindex="0"><code class="language-spl" data-lang="spl">| eval age_hours=(now() - _time)/3600
| eval decayed_score = calculated_risk_score * exp(-age_hours / 48)
| stats sum(decayed_score) as total_risk by risk_object</code></pre><p><code>exp(-age/48)</code> 是 48 小時半衰期、24h 前的事件權重剩 60%、48h 剩 37%、7 天前剩 &lt; 3%。half-life 依 attack pattern 調整：commodity attack 12-24h、APT 5-14 day。</p>
<h2 id="capacity-規劃">Capacity 規劃</h2>
<p>RBA 的 capacity 三個面向：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算方式</th>
          <th>警戒值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Risk index event/day</td>
          <td><code>總 detection rule × 平均 trigger 次數/day</code></td>
          <td>中型 SOC ~100K-500K / day</td>
      </tr>
      <tr>
          <td>Risk datamodel size</td>
          <td><code>event/day × 365 day × 1KB avg</code></td>
          <td>100K/day × 365 × 1KB ≈ 36GB / year</td>
      </tr>
      <tr>
          <td>Search head load</td>
          <td>RBA tstats 比 raw search 便宜 ~10x、但 by-user aggregation 在 1M+ user 仍重</td>
          <td>跑 hourly notable trigger search、不是 streaming</td>
      </tr>
      <tr>
          <td>Indexer ingest</td>
          <td>RBA 不大增 ingest（已 ingest 的 log 處理出 modifier）、但 datamodel acceleration 要 CPU</td>
          <td>每 indexer 預留 10-15% CPU 給 datamodel accel</td>
      </tr>
  </tbody>
</table>
<p>實務 sizing：500K modifier/day、用戶 5K、tstats hourly trigger search、需要 <em>3 indexer + 1 search head</em>（含 RBA 之外的工作）。</p>
<blockquote>
<p>注意 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">SC4S / Splunk Cloud</a> ingest pricing — RBA 不增 ingest GB / day、但 datamodel acceleration 算 CPU 工作量、Splunk Cloud 是另外計費的 vCPU；on-prem 自管 indexer 沒這個 cost。</p></blockquote>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-soar--case-management">跟 SOAR / case management</h3>
<p>Notable 觸發後接 SOAR：</p>
<ul>
<li><strong>enrichment</strong>：自動 query AD / asset DB / threat intel、把 user role / asset criticality / known IoC 補進 case</li>
<li><strong>decision tree</strong>：根據 risk score 區間決定 SOC tier（&lt; 100 tier 1 / 100-200 tier 2 / 200+ tier 3 + page）</li>
<li><strong>playbook automation</strong>：disable user / isolate endpoint / rotate credential 走 SOAR pipeline、不要 SOC analyst 手動 click</li>
</ul>
<h3 id="跟-elastic-security--sentinel-對照">跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Sentinel</a> 對照</h3>
<p>各家對 RBA 的實作命名不同：Splunk 叫 RBA、Elastic 叫 Risk Engine、Microsoft Sentinel 叫 Fusion + UEBA aggregation、Sumo Logic 叫 Insight Trainer；底層概念相同（score aggregation + threshold notable）、細節差在 <em>modifier 寫法跟 ML 自動化程度</em>。跨平台遷移時 modifier 邏輯多半要重寫、threshold + decay tuning 經驗可以平移。</p>
<h3 id="跟-ueba">跟 UEBA</h3>
<p>RBA 跟 UEBA（user / entity behavior analytics）是 <em>互補不是替代</em> — UEBA 用 ML 算 baseline 偏差、輸出 anomaly score 餵進 RBA 當一個 modifier 來源。實作順序通常是 <em>先靜態 rule + RBA、再加 UEBA 補充</em>；直接從 ML-first 開始通常 tuning 成本爆炸。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Threat object correlation</strong>：跨 modifier 用 threat_object 串相同 attacker artifact、score 跨 user 跨 asset 聚合</li>
<li><strong>Kill chain coverage analysis</strong>：notable 拆成「ATT&amp;CK tactic 覆蓋 N/14」、覆蓋越廣 priority 越高</li>
<li><strong>Risk-based response automation</strong>：score 區間自動觸發不同 SOAR playbook、人工只 review tier 3</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a></li>
<li>對照案例：<a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">Okta Cross-Tenant Impersonation 2023</a>、<a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft Storm-0558</a></li>
<li>上游 chapter：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行 vendor：<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></li>
<li>平行 deep article：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>1.10 KV / Document DB 容量規劃</title><link>https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>KV / Document DB 的容量規劃跟傳統 OLTP 完全不同。OLTP 容量靠「instance type 升級 + read replica」、KV 靠「partition 切分 + capacity unit 配置」。兩者瓶頸不同、可擴範圍不同、設計取捨也不同。&lt;/p>
&lt;p>本章針對 DynamoDB、Azure Cosmos DB、Google Cloud Bigtable、MongoDB Atlas 等主流 KV / Document DB、整理容量規劃的共通方法論。讀完後讀者能回答：partition key 怎麼設計才不會 hot partition、on-demand vs provisioned 怎麼選、什麼時候從 single-region 升到 multi-region。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取&lt;/a> 的關係：1.1 處理 OLTP 高併發、本章處理 KV 高併發。兩者讀者群有重疊但解法不同。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 的關係：本章從 &lt;em>DB 視角&lt;/em> 看容量、9.4 / 9.6 從 &lt;em>workload 視角&lt;/em> 看容量、兩者互補。&lt;/p>
&lt;h2 id="kv--document-db-的容量模型">KV / Document DB 的容量模型&lt;/h2>
&lt;p>KV 容量模型可以簡化成一條公式：&lt;strong>總容量 = partition 數量 × 每 partition 上限&lt;/strong>。&lt;/p>
&lt;p>vendor 不同、細節不同，但都遵循這個邏輯。&lt;/p>
&lt;h3 id="http-api-db-vs-connection-based-db-的本質差異">HTTP API DB vs connection-based DB 的本質差異&lt;/h3>
&lt;p>KV DB 在 surge 場景比 OLTP 有結構性優勢的主因、不只是 partition 設計、是 &lt;em>連線模型&lt;/em> 的本質差異。&lt;/p>
&lt;p>&lt;strong>Connection-based DB&lt;/strong>（PostgreSQL、MySQL、MongoDB、Cassandra）：&lt;/p>
&lt;ul>
&lt;li>用戶端跟 DB 維持 TCP connection、connection 有 state（authenticated session）&lt;/li>
&lt;li>每個 connection 在 DB server 端佔記憶體 + 一個 process/thread&lt;/li>
&lt;li>connection 上限通常 1K-5K&lt;/li>
&lt;li>application 想開更多 connection、DB 直接拒絕&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>HTTP API DB&lt;/strong>（DynamoDB、Cosmos DB、Bigtable、Firestore）：&lt;/p>
&lt;ul>
&lt;li>用戶端每次 request 開新 HTTP connection（或用 keep-alive 池）&lt;/li>
&lt;li>DB 端沒有「per-user connection state」、是 stateless API server&lt;/li>
&lt;li>沒有 connection 上限概念、能力上限是 &lt;em>每 partition 的 RU / RCU&lt;/em>&lt;/li>
&lt;li>application 加多少 instance 都不影響 DB&lt;/li>
&lt;/ul>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">9.C29 Lemino&lt;/a> — NTT DOCOMO 串流服務選 DynamoDB 而非 RDB 的關鍵原因是 RDB 的 connection limit 在 surge 場景變成 bottleneck、HTTP API 模型沒這個問題。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>KV / Document DB 的容量規劃跟傳統 OLTP 完全不同。OLTP 容量靠「instance type 升級 + read replica」、KV 靠「partition 切分 + capacity unit 配置」。兩者瓶頸不同、可擴範圍不同、設計取捨也不同。</p>
<p>本章針對 DynamoDB、Azure Cosmos DB、Google Cloud Bigtable、MongoDB Atlas 等主流 KV / Document DB、整理容量規劃的共通方法論。讀完後讀者能回答：partition key 怎麼設計才不會 hot partition、on-demand vs provisioned 怎麼選、什麼時候從 single-region 升到 multi-region。</p>
<p>跟 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的關係：1.1 處理 OLTP 高併發、本章處理 KV 高併發。兩者讀者群有重疊但解法不同。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的關係：本章從 <em>DB 視角</em> 看容量、9.4 / 9.6 從 <em>workload 視角</em> 看容量、兩者互補。</p>
<h2 id="kv--document-db-的容量模型">KV / Document DB 的容量模型</h2>
<p>KV 容量模型可以簡化成一條公式：<strong>總容量 = partition 數量 × 每 partition 上限</strong>。</p>
<p>vendor 不同、細節不同，但都遵循這個邏輯。</p>
<h3 id="http-api-db-vs-connection-based-db-的本質差異">HTTP API DB vs connection-based DB 的本質差異</h3>
<p>KV DB 在 surge 場景比 OLTP 有結構性優勢的主因、不只是 partition 設計、是 <em>連線模型</em> 的本質差異。</p>
<p><strong>Connection-based DB</strong>（PostgreSQL、MySQL、MongoDB、Cassandra）：</p>
<ul>
<li>用戶端跟 DB 維持 TCP connection、connection 有 state（authenticated session）</li>
<li>每個 connection 在 DB server 端佔記憶體 + 一個 process/thread</li>
<li>connection 上限通常 1K-5K</li>
<li>application 想開更多 connection、DB 直接拒絕</li>
</ul>
<p><strong>HTTP API DB</strong>（DynamoDB、Cosmos DB、Bigtable、Firestore）：</p>
<ul>
<li>用戶端每次 request 開新 HTTP connection（或用 keep-alive 池）</li>
<li>DB 端沒有「per-user connection state」、是 stateless API server</li>
<li>沒有 connection 上限概念、能力上限是 <em>每 partition 的 RU / RCU</em></li>
<li>application 加多少 instance 都不影響 DB</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a> — NTT DOCOMO 串流服務選 DynamoDB 而非 RDB 的關鍵原因是 RDB 的 connection limit 在 surge 場景變成 bottleneck、HTTP API 模型沒這個問題。</p>
<p>判讀含義：選 KV DB 不只是「擴容容易」、是 <em>連線模型</em> 適合無 state HTTP 服務的天然契合。微服務數量增加時、HTTP API DB 不需要每次都 review connection pool 設定。但若 application 仍以 SQL transaction 為主流程設計、改 KV 需要 <em>改 application 架構</em>、不是換 driver 而已。</p>
<p><strong>Amazon DynamoDB</strong>：</p>
<ul>
<li>容量單位是 RCU（Read Capacity Unit）跟 WCU（Write Capacity Unit）</li>
<li>1 RCU = 1 strongly consistent read of 4KB / sec、2 eventually consistent reads</li>
<li>1 WCU = 1 write of 1KB / sec</li>
<li>每個 partition 上限：3000 RCU / 1000 WCU、底層 partition 數量透明</li>
</ul>
<p><strong>Azure Cosmos DB</strong>：</p>
<ul>
<li>容量單位是 RU（Request Unit）— 把 read / write / query 統一抽象</li>
<li>1 RU = strongly consistent read of 1KB document</li>
<li>寫成本約 5x read、複雜 query 可達數百 RU</li>
<li>每個 logical partition 上限：10,000 RU/s</li>
</ul>
<p><strong>Google Cloud Bigtable</strong>：</p>
<ul>
<li>容量單位是 node（SSD / HDD）</li>
<li>每個 node 約 10,000 reads/sec、10,000 writes/sec（依 row size）</li>
<li>partition 透明、靠 tablet 自動分裂</li>
</ul>
<p><strong>MongoDB Atlas</strong>：</p>
<ul>
<li>容量單位是 cluster tier（M10、M30、M60 等）+ shard</li>
<li>每個 shard 是獨立 mongod replica set、容量按 instance type 跟 storage</li>
<li>主動 sharding 設計、跟 DynamoDB 透明 partition 不同</li>
</ul>
<p><strong>共通點</strong>：容量上限不是「單一 number」、是「partition / shard 數量 × 每 partition 上限」。要擴容、要嘛加 partition、要嘛升級 partition、不能像 OLTP 一樣換更大 instance。</p>
<h2 id="partition-key-設計容量的命脈">Partition key 設計：容量的命脈</h2>
<p>partition key 設計不均勻、實際容量遠低於名義。這是 KV DB 最常見的 production issue。</p>
<p><strong>Hot partition 的成因</strong>：</p>
<ul>
<li>名義容量 = partition 數量 × 每 partition 上限</li>
<li>實際容量 = 最熱 partition 上限（如果分布不均）</li>
<li>100K RPS 名義能撐、若 80% 流量集中在 1 個 partition、實際 <em>只能撐 3K RPS（DynamoDB partition 上限）</em></li>
</ul>
<p><strong>識別 hot partition 的訊號</strong>：</p>
<ul>
<li>throughput 上不去、但 average resource utilization 低</li>
<li>某些 key 的 request latency 飆、其他 key 正常</li>
<li>DynamoDB throttling event 出現（即使 capacity 還沒滿）</li>
<li>Cosmos DB 顯示「per-partition RU consumption skew」</li>
</ul>
<p><strong>設計策略</strong>：</p>
<ol>
<li><strong>天然均勻 partition key</strong>：user_id、order_id、device_id 等天然分布廣的 ID。最簡單、最常用。</li>
<li><strong>Composite partition key</strong>：把容易集中的維度（event_id）跟均勻的維度（user_id_hash）組合。例如 <code>event_id#user_id_hash_mod_100</code>、強制把同一 event 的流量分散到 100 個 sub-partition。</li>
<li><strong>Write sharding</strong>：在 partition key 後加 random suffix。<code>event_id#0</code> ~ <code>event_id#9</code> 讓同一個 event 變成 10 個 partition。讀的時候要 scatter-gather 從 10 個 partition 讀回來。</li>
<li><strong>Time-bucket</strong>：對時序資料、加 minute / hour bucket。<code>metric#2026-05-13-T12</code>、每個時段一個 partition。</li>
</ol>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 9000 萬 reads/sec 靠 partition 設計均勻、不是純擴 capacity</li>
<li><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 event_id 天然容易 hot、必須用 composite key 或 write sharding 分散</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — Cosmos DB synthetic partition key 強制分散</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a>。</p>
<h3 id="彈性來自-partition-key-均勻分布">彈性來自 partition key 均勻分布</h3>
<p>KV DB 的吞吐彈性等於 partition key 均勻分布的結果。partition key 均勻時、總容量 ≈ partition 數量 × 單 partition 上限；partition key 不均時、實際容量 = 最熱 partition 上限（DynamoDB 每 partition 3000 RCU / 1000 WCU）、跟 partition 總數無關。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 IOPS 從 20 衝到 135K 的 6,750 倍彈性、前提是 partition key 把流量分散到大量 partition（合理做法是 composite key <code>event_id + user_id_hash</code> 或 write sharding <code>event_id + random_suffix</code>）。若用裸 <code>event_id</code> 當 partition key、同一場演唱會所有訂單擠進同一個 partition、實際 IOPS 上限被鎖在 1000 WCU、跟 partition 總數無關。</p>
<p>判讀重點：讀「Amazon Ads 9000 萬 reads/sec」、「DynamoDB 1.51 億 RPS」這類數字、要追問「partition 設計是什麼」、再判斷自己的服務能否複製。換 DynamoDB 是必要前提、partition key 設計是充分前提；只換 DB 而沒解決 partition key、會出「換了 DB 但 hot partition 依舊」的事故。</p>
<h2 id="capacity-modeon-demand-vs-provisioned">Capacity mode：on-demand vs provisioned</h2>
<p>DynamoDB / Cosmos DB 都提供兩種容量模式、各有適用場景。</p>
<p><strong>On-demand（pay-per-use）</strong>：</p>
<ul>
<li>不需事前配置 RCU / WCU / RU</li>
<li>自動 scale up / down、處理突發流量</li>
<li>單位成本高（約 7x provisioned）</li>
<li>適合：流量不可預測、burst 頻繁、開發 / 測試環境</li>
</ul>
<p><strong>Provisioned（預配置）</strong>：</p>
<ul>
<li>預先訂購 RCU / WCU / RU</li>
<li>超過配額會 throttle（除非開 auto-scaling）</li>
<li>單位成本低</li>
<li>適合：流量可預測、sustained workload、生產環境</li>
</ul>
<p><strong>選型決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 mode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>流量 peak/avg 比 &lt; 3x</td>
          <td>provisioned + auto-scaling</td>
      </tr>
      <tr>
          <td>流量 peak/avg 比 &gt; 5x</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>流量極端 bursty（flash-sale）</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>sustained growth 穩定上升</td>
          <td>provisioned + scheduled scaling</td>
      </tr>
      <tr>
          <td>短期測試 / POC</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>已知大事件（Black Friday）</td>
          <td>provisioned baseline + scheduled scale-up</td>
      </tr>
  </tbody>
</table>
<p><strong>對應案例</strong>：</p>
<ul>
<li><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> — TiDB 必須長期 over-provision、換 DynamoDB on-demand 後 pay-per-use、50% 成本下降</li>
<li><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a> — sustained 3 億 msg/day 適合 provisioned + auto-scaling</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 9000 萬 RPS sustained workload 必然 provisioned + careful tuning</li>
</ul>
<p>詳見 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的成本曲線分析。</p>
<h3 id="計費粒度-vs-工程顆粒">計費粒度 vs 工程顆粒</h3>
<p>KV / Document DB 的計費單位（DynamoDB 的 RCU/WCU、Cosmos DB 的 RU、Spanner 的 processing unit）決定容量規劃可以從多小開始。計費粒度太大、中小規模負載付過多錢；計費粒度太小、大規模負載要管理很多細項。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — Spanner 早期最小單位是 100 processing units（pu）≈ 1 node、對中小負載門檻過高。後來推出 100 pu 起跳的 granular sizing、讓容量規劃可以從小開始、降低 onboarding 門檻。</p>
<p><strong>選型含義</strong>：</p>
<ul>
<li><strong>新服務 / 中小規模</strong>：選計費粒度小的選項（Cosmos DB serverless、Spanner granular sizing、DynamoDB on-demand）、避免一開始就為了「未來會用到」過配。中小規模付過配成本、實際就是替「不確定的未來」付保險費、保險費過高代表選錯產品。</li>
<li><strong>穩定大規模</strong>：計費粒度可大（DynamoDB provisioned with reserved capacity、Spanner full-node provisioning）、單價較低。Reserved capacity 通常綁 1-3 年合約、要看業務 <em>未來 12-24 月需求是否穩定</em>、若業務量可能下降或遷移、Reserved 反成沉沒成本；若業務量穩定上升、Reserved 是合理 hedging。</li>
<li><strong>POC / 測試</strong>：選 on-demand 或 serverless、付實際用量、別為了未實際 production 的 workload 付 reserved 成本。</li>
</ul>
<p>判讀重點：計費粒度同時是 <em>vendor 商業策略</em> 跟 <em>工程顆粒</em>、選 vendor 時要看 <em>min sizing</em> 跟 <em>增量 granularity</em>、不只看 max throughput。</p>
<h3 id="業務邏輯變化--讀寫比跳量級">業務邏輯變化 → 讀寫比跳量級</h3>
<p>讀寫比變化是容量規劃的早期警訊、但常被忽略。原始容量規劃通常基於某個讀寫比（例如 1:1 或 5:1）、業務邏輯改變可能讓比例跳一個量級、原容量規劃失效。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 廣告事件量測讀寫比 18:1（曝光發生 1 次、後續查詢 18 次）。如果業務新增即時報表功能、讀次數從 18 跳到 50、容量規劃要重做、不是「再加一點 capacity」。</p>
<p><strong>常見業務變化導致讀寫比跳量級</strong>：</p>
<ul>
<li>新增即時 dashboard：每筆資料被查詢頻率從 1 次跳到 N 次</li>
<li>新增推薦演算法：每用戶 read profile 從每次登入 1 次變成每次推薦 1 次（× 推薦頻率）</li>
<li>新增 audit / compliance 查詢：每筆敏感資料額外被查 5-10 次</li>
<li>新增 cache：讀次數從 100 降到 5（cache hit rate 95%）— 跟其他變化方向相反、是 <em>capacity 該縮容</em> 的訊號、若沒同步 review 反而會繼續按舊容量付錢</li>
<li>新增 anti-fraud 檢測：每寫入觸發 N 次 read 驗證</li>
</ul>
<p>判讀重點：容量規劃 review cadence 不只看流量、要 review <em>讀寫比</em> 是否漂移。比例跳量級是設計需要重做的訊號、不是單純 capacity 增加（或減少）的訊號。</p>
<h2 id="一致性模型strong-vs-eventual-vs-session">一致性模型：strong vs eventual vs session</h2>
<p>KV / Document DB 通常提供多個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>、不同 level 對應不同延遲跟可用性。</p>
<p><strong>DynamoDB</strong>：</p>
<ul>
<li>Eventually consistent reads（預設、便宜）：1 sec 內收斂、cost = 0.5 RCU</li>
<li>Strongly consistent reads：跨 AZ <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、cost = 1 RCU、不可跨 region</li>
<li>沒有中間 level</li>
</ul>
<p><strong>Cosmos DB</strong>（最豐富）：</p>
<ul>
<li><strong>Strong</strong>：linearizable、跨 region quorum、最高 latency</li>
<li><strong>Bounded staleness</strong>：訂上限（時間 / 版本差異）</li>
<li><strong>Session</strong>：同一 session 內強一致（最常用）</li>
<li><strong>Consistent prefix</strong>：保證寫入順序、不保證收斂時間</li>
<li><strong>Eventual</strong>：最便宜、最終一致</li>
</ul>
<p><strong>Bigtable</strong>：</p>
<ul>
<li>Single-region：strongly consistent</li>
<li>Replicated：eventually consistent</li>
</ul>
<p><strong>選 consistency level 的工程後果</strong>：</p>
<ul>
<li>Strong consistency → 跨 region 延遲（quorum round-trip）</li>
<li>Eventual → 用戶可能看到舊資料、需要 application 容忍</li>
<li>Session → 大多數網路服務的 sweet spot（用戶看自己寫的東西要立即、別人寫的可以稍晚）</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — external consistency（線性化）跨地區、付出 quorum 延遲代價</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365 Cosmos DB</a> — 分析平台用 weakest consistency 換最大 throughput</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的一致性取捨。</p>
<h2 id="multi-model-取捨">Multi-model 取捨</h2>
<p>部分 KV / Document DB 支援多個 model interface、同一服務跑不同抽象。</p>
<p><strong>Cosmos DB（最廣 multi-model）</strong>：</p>
<ul>
<li>SQL API（document）</li>
<li>MongoDB API（document、wire-protocol compatible）</li>
<li>Cassandra API（wide-column）</li>
<li>Gremlin（graph）</li>
<li>Table（key-value）</li>
</ul>
<p><strong>DynamoDB（KV + document）</strong>：</p>
<ul>
<li>原生 KV、但 attribute 可以是 nested map / list（document-like）</li>
<li>沒有 SQL interface（PartiQL 是 query language、不是 model）</li>
</ul>
<p><strong>Bigtable（wide-column）</strong>：</p>
<ul>
<li>沒有 multi-model、純 wide-column</li>
<li>替代方案：用 Spanner + Bigtable 組合</li>
</ul>
<p><strong>Multi-model 的優缺</strong>：</p>
<ul>
<li>優勢：同一團隊不必管多個 vendor、ops 簡化</li>
<li>優勢：不同 use case 用同一 datastore、減少 data sync</li>
<li>限制：vendor lock-in 加深、難換</li>
<li>限制：每個 API 都不是 <em>最好</em> 的（compromise）— MongoDB API 跟 native MongoDB 有 behavior 差異</li>
</ul>
<p><strong>選型建議</strong>：</p>
<ul>
<li>已用 single model → 不必為 multi-model 而換</li>
<li>多種 use case 同時上 → 評估 Cosmos DB（特別是 MongoDB workload + 新需求）</li>
<li>純 KV 高吞吐 → DynamoDB / Bigtable 比 Cosmos DB 通常便宜</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB → Cosmos DB MongoDB API、應用層幾乎不改、底層改用 Cosmos 分散式架構</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — 用 SQL API、不需要 MongoDB compat</li>
</ul>
<h2 id="kv-db-作為寫入緩衝的特殊用法">KV DB 作為寫入緩衝的特殊用法</h2>
<p>本節展開 KV 在 <em>flash-sale 架構</em> 的特殊角色、屬於資料層責任、但跟 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a> 跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 互補（後者主寫 broker / queue 設計、本節聚焦把 KV 當 buffer 的取捨）。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 揭露一個非傳統用法：DynamoDB 不當 OLTP、當 <em>durable queue</em>。</p>
<p><strong>模式</strong>：前端把訂單塞進 DynamoDB（高吞吐、partition 均勻）、後端 legacy server 按自己能承受的速度從 DynamoDB 消費。</p>
<p><strong>為什麼用 DynamoDB 而非 SQS / Kafka</strong>：</p>
<ul>
<li>DynamoDB Stream 提供 change data capture、後端可以 stream 消費</li>
<li>寫入後立即可查（OLTP-like）、不是純 fire-and-forget</li>
<li>partition 設計讓單一事件可以分散到多個 partition</li>
<li>同樣 vendor、不必另起一個 broker 服務</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>突發流量遠超後端處理能力</li>
<li>後端是 legacy、不容易擴</li>
<li>需要寫入後立即可查（用戶看「我下單成功了」）</li>
</ul>
<p><strong>不適用場景</strong>：</p>
<ul>
<li>純 fire-and-forget（用 SQS 更便宜）</li>
<li>高吞吐 stream processing（用 Kafka 更專業）</li>
<li>順序性嚴格要求（DynamoDB Streams 只在 partition 內保證順序）</li>
</ul>
<p>詳見 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 案例</a> 的詳細分析。</p>
<h2 id="連線管理跟-oltp-完全不同">連線管理：跟 OLTP 完全不同</h2>
<p>KV / Document DB 通常是 <em>HTTP / gRPC 介面</em>、不是 <em>connection pool</em>。這是跟 OLTP 完全不同的設計、影響應用層架構。</p>
<p><strong>OLTP（PostgreSQL / MySQL）</strong>：</p>
<ul>
<li>每個 application instance 維護 connection pool（10-100 connections）</li>
<li>connection 是有狀態的（transaction、session variable）</li>
<li>pool size × instance 數量 ≤ DB 上限（PostgreSQL 預設 100、PgBouncer 可破百）</li>
<li><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> 揭露 RDB connection 是隱性 bottleneck</li>
</ul>
<p><strong>KV（DynamoDB / Cosmos DB）</strong>：</p>
<ul>
<li>純 HTTP / gRPC、無 stateful connection</li>
<li>每個 request 獨立、不必預先 establish connection</li>
<li>沒有 connection limit 概念</li>
<li>應用層擴容不會打爆 DB connection</li>
</ul>
<p>這個差異是 KV DB 在 <em>surge 場景</em> 比 OLTP 有優勢的主因 — KV 不會 connection saturate。</p>
<h2 id="隱性限流-vs-明確限流">隱性限流 vs 明確限流</h2>
<p>flash-sale 或極端負載場景的限流可能分散在多層元件、不是單一「rate limiter」。同一架構可能同時包含 <em>隱性</em> 限流（用 DB / LB 上限自然攔截）跟 <em>明確</em> 限流（用排隊系統精確控速）。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票架構圖上看不到明確「rate limiter」元件、但限流發生在多層：</p>
<ul>
<li><strong>DynamoDB 寫入排隊</strong>：DynamoDB 把訂單塞進 queue、傳統 server 按自己能力消費 — DynamoDB throughput 就是隱性限流</li>
<li><strong>ELB max connection</strong>：load balancer 上限自動拒絕超量請求</li>
<li><strong>Application 層 connection pool</strong>：超過 pool size 的 request 排隊或被拒</li>
<li><strong>付款層獨立</strong>：搶票流量塞爆時、付款不受影響、低頻路徑「自然限流」</li>
</ul>
<p>對比 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek Virtual Waiting Room</a> 的 <em>明確限流</em>：用 Counters table 精確控發 token 速率、用戶看得到排隊位置。</p>
<p><strong>選擇取捨</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>隱性限流（Tixcraft）</th>
          <th>明確限流（SeatGeek）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶體驗</td>
          <td>用戶以為成功、實際排隊</td>
          <td>用戶看得到等待時間</td>
      </tr>
      <tr>
          <td>流量吸收能力</td>
          <td>極高（DB 直接吸）</td>
          <td>受限於 token 發放速度</td>
      </tr>
      <tr>
          <td>開發複雜度</td>
          <td>低（用 DB 自帶 throughput）</td>
          <td>高（需要 token 系統）</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>DB 滿了用戶才被拒</td>
          <td>排隊系統爆了用戶被拒</td>
      </tr>
      <tr>
          <td>適合業務</td>
          <td>流量瞬間到頂、要全收</td>
          <td>流量持續高、要排序公平</td>
      </tr>
  </tbody>
</table>
<p><strong>失敗模式延伸</strong>：隱性限流的失敗特徵是「provisioned capacity / connection pool 飽和、用戶看到 5xx / timeout、沒人收到排隊位置」— 監控訊號是 DynamoDB throttling event 或 ELB queue length 飆。明確限流的失敗特徵是「排隊系統本身的 DB / counter 飽和、token 發不出來、所有用戶包含 VIP 都被擋」— 監控訊號是 token issuance success rate 掉。兩種失敗對應不同 runbook、混在同一 alert dashboard 會誤判。</p>
<p><strong>適合業務延伸</strong>：隱性限流適合「流量瞬間到頂、業務願意接受用戶看不見排隊」的場景（演唱會搶票、Black Friday 開賣瞬間、限量商品）— 業務優先收住流量、用戶體驗可以事後解釋。明確限流適合「流量持續高、用戶等待時間長、需要顯示進度減少跳離」的場景（IPO 開盤、長期熱門商品上架、跨小時的搶購事件）— 用戶能看到「我還有 30 分鐘」會繼續等。</p>
<p>判讀重點：選哪種限流取決於業務願意接受什麼用戶體驗、不是工程偏好。隱性限流用透明度換流量吸收能力、明確限流用流量吸收能力換體驗可見度。兩者並存、沒有「best practice」。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day 2025</a></td>
          <td>DynamoDB 24 小時 1.51 億 RPS、毫秒級延遲、可預期峰值上限參考</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>9000 萬 RPS + 99.999% 可用 — partition 均勻設計典範</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></td>
          <td>Cosmos DB 1M RU/s + multi-model + global distribution</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>DynamoDB 當 durable queue、IOPS 20→135K</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a></td>
          <td>DynamoDB 4 表 + Lambda 實作 virtual waiting room、跟 Tixcraft 的隱性緩衝形成姊妹案</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>30x DAU surge、DynamoDB 撐 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a></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>遊戲後端 KV、billions of requests + single-digit ms</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>TiDB → DynamoDB、50% 成本下降的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a></td>
          <td>Black Friday 1.67 億請求 / 24h、Cosmos DB 多 region</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a></td>
          <td>99.999% 跨 15 region、DynamoDB 為預設 DB</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>3 億訊息 / 天、TTL 自動清理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>billions of actions daily、watchlist + 播放進度</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>connection limit 才是 RDB bottleneck、改用 DynamoDB</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a> 把 DynamoDB 當 <em>排隊調度系統</em>、不只當 queue buffer：用 Counters table 控發 token 的速率、Queue table 紀錄序號、Connection table 串 WebSocket。這個架構跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的「全部塞進 DynamoDB 隱性緩衝」是兩種對立取捨 — Tixcraft 用透明度換流量吸收能力、SeatGeek 用流量吸收能力換體驗可見度。判讀重點：KV DB 不只能當 OLTP 替代品、4 張表組合就能變成業務級調度引擎、選表前要先確定業務需要哪一面。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 State Storage Selection</a> — KV vs OLTP vs SearchIndex 選型</li>
<li>平行：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>（OLTP 版本）/ <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>、<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（含「預設 DB 治理 pattern」— KV 在大規模平台的選型治理）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（hot partition 量測）、<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界</a></li>
<li>DynamoDB 深入：<a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition key 反模式</a>、<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">on-demand vs provisioned 切換</a>、<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table design</a>、<a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">GSI / LSI 設計</a></li>
<li>Cosmos DB 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">partition key 設計</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/" data-link-title="Cosmos DB RU/s 成本模型 &#43; 容量規劃：RU 思維、payload、index、provisioned vs autoscale vs serverless" data-link-desc="從 CPU&#43;IOPS 思維轉到 RU 思維的學習曲線、依負載形狀選容量模式、payload &#43; index policy 對 RU 的影響、autoscale reactive 限制 — 從 ASOS Black Friday &#43; Minecraft Earth 1M RU/s 壓測切入">RU 成本模型</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">一致性層次工程</a></li>
<li>MongoDB 深入：<a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key 選型</a>、<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a>、<a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">connection 管理與 cache 層</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
</ul>
]]></content:encoded></item><item><title>9.10 Production-Side 驗證</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Production-side 驗證的責任是回答「staging 過了 production 一定過嗎」。多數 staging 環境的硬體 / 流量 / 資料 / 第三方依賴都跟 production 不一樣、staging 通過不代表 production 安全。本章處理「在 production 安全驗證新配置」的工程做法。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary&lt;/a> 的關係：06.20 走「故障注入」的安全邊界（chaos）、9.10 走「正常負載」的 production 驗證（perf）。兩者方法論類似、目標完全不同。chaos test 是「主動破壞看會不會出事」、production perf validation 是「真實流量看新版本能不能跑」。&lt;/p>
&lt;p>本章四個工具（shadow traffic、dark launch、canary、production-like load test）按 &lt;em>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>&lt;/em> 從小到大排列、每個適合不同驗證場景。&lt;/p>
&lt;h2 id="shadow-traffic">Shadow traffic&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow traffic&lt;/a> 是 blast radius 最小的工具：複製 production traffic 到新版本、但 &lt;em>不把結果返回用戶&lt;/em>。&lt;/p>
&lt;p>&lt;strong>運作機制&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>用戶看到的還是舊版本回應、體驗不變&lt;/li>
&lt;li>新版本只是「並行跑、看會不會崩」&lt;/li>
&lt;li>新版本的結果可以跟舊版本對比、找出邏輯差異&lt;/li>
&lt;li>對下游的寫入要 &lt;em>特別處理&lt;/em>：要麼寫入 sandbox、要麼 dry-run（純驗證 query plan、不真寫）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>工具實作&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>GoReplay：tcpdump-based 開源、適合 HTTP&lt;/li>
&lt;li>Service mesh shadow（Istio、Linkerd mirror）：mesh 層 mirror、零 application invasion&lt;/li>
&lt;li>AWS VPC Traffic Mirroring：底層網路層、加密 traffic 要另處理&lt;/li>
&lt;li>Diffy（已 deprecated 但概念有效）：dual-write 對比結果&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>適合場景&lt;/strong>：架構大改、想驗證 &lt;em>是否能撐 production traffic&lt;/em> 但不能影響用戶。例如「DB 從 PostgreSQL 換 Aurora、想看新 DB 在真實 query pattern 下穩不穩」。&lt;/p>
&lt;p>&lt;strong>注意事項&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>shadow traffic 也消耗 production 下游資源（DB read、API call）— 必須算進容量&lt;/li>
&lt;li>加密 / PII 資料需要處理&lt;/li>
&lt;li>shadow 通常跑 1-7 天看 long-tail、不是 30 分鐘就下結論&lt;/li>
&lt;/ul>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測&lt;/a> — pre-event 壓測但走 staging；real shadow 則是 &lt;em>production-traffic-driven&lt;/em> 而非合成。&lt;/p>
&lt;h2 id="dark-launch">Dark launch&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark launch&lt;/a> 介於 shadow 跟 canary 之間：程式碼上線、走 production traffic、但 &lt;em>UI 入口暫不開放&lt;/em>。&lt;/p>
&lt;p>&lt;strong>跟 shadow 的差別&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Shadow：traffic 複製、新版本 &lt;em>不寫入真實狀態&lt;/em>&lt;/li>
&lt;li>Dark launch：&lt;em>真實寫入 production&lt;/em>、但用戶看不到 UI&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>運作機制&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Production-side 驗證的責任是回答「staging 過了 production 一定過嗎」。多數 staging 環境的硬體 / 流量 / 資料 / 第三方依賴都跟 production 不一樣、staging 通過不代表 production 安全。本章處理「在 production 安全驗證新配置」的工程做法。</p>
<p>跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 的關係：06.20 走「故障注入」的安全邊界（chaos）、9.10 走「正常負載」的 production 驗證（perf）。兩者方法論類似、目標完全不同。chaos test 是「主動破壞看會不會出事」、production perf validation 是「真實流量看新版本能不能跑」。</p>
<p>本章四個工具（shadow traffic、dark launch、canary、production-like load test）按 <em><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a></em> 從小到大排列、每個適合不同驗證場景。</p>
<h2 id="shadow-traffic">Shadow traffic</h2>
<p><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow traffic</a> 是 blast radius 最小的工具：複製 production traffic 到新版本、但 <em>不把結果返回用戶</em>。</p>
<p><strong>運作機制</strong>：</p>
<ul>
<li>用戶看到的還是舊版本回應、體驗不變</li>
<li>新版本只是「並行跑、看會不會崩」</li>
<li>新版本的結果可以跟舊版本對比、找出邏輯差異</li>
<li>對下游的寫入要 <em>特別處理</em>：要麼寫入 sandbox、要麼 dry-run（純驗證 query plan、不真寫）</li>
</ul>
<p><strong>工具實作</strong>：</p>
<ul>
<li>GoReplay：tcpdump-based 開源、適合 HTTP</li>
<li>Service mesh shadow（Istio、Linkerd mirror）：mesh 層 mirror、零 application invasion</li>
<li>AWS VPC Traffic Mirroring：底層網路層、加密 traffic 要另處理</li>
<li>Diffy（已 deprecated 但概念有效）：dual-write 對比結果</li>
</ul>
<p><strong>適合場景</strong>：架構大改、想驗證 <em>是否能撐 production traffic</em> 但不能影響用戶。例如「DB 從 PostgreSQL 換 Aurora、想看新 DB 在真實 query pattern 下穩不穩」。</p>
<p><strong>注意事項</strong>：</p>
<ul>
<li>shadow traffic 也消耗 production 下游資源（DB read、API call）— 必須算進容量</li>
<li>加密 / PII 資料需要處理</li>
<li>shadow 通常跑 1-7 天看 long-tail、不是 30 分鐘就下結論</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — pre-event 壓測但走 staging；real shadow 則是 <em>production-traffic-driven</em> 而非合成。</p>
<h2 id="dark-launch">Dark launch</h2>
<p><a href="/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark launch</a> 介於 shadow 跟 canary 之間：程式碼上線、走 production traffic、但 <em>UI 入口暫不開放</em>。</p>
<p><strong>跟 shadow 的差別</strong>：</p>
<ul>
<li>Shadow：traffic 複製、新版本 <em>不寫入真實狀態</em></li>
<li>Dark launch：<em>真實寫入 production</em>、但用戶看不到 UI</li>
</ul>
<p><strong>運作機制</strong>：</p>
<ul>
<li>後端 code 部署到 production</li>
<li>用 feature flag 控制 UI 暴露</li>
<li>從內部 API、cron job、employee-only access 觸發新功能</li>
<li>真正寫入 production DB / cache / queue</li>
<li>用戶看不到 UI 入口、無感</li>
</ul>
<p><strong>Exit criteria</strong>：</p>
<ul>
<li>跑足夠時間（通常 1-2 週）</li>
<li>內部使用沒有 critical issue</li>
<li>metric 在預期範圍</li>
</ul>
<p><strong>適合場景</strong>：新功能後端風險高、想 production-validate 再開放給用戶。
<strong>不適合</strong>：純 UI 改動（沒有後端風險、直接 canary）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">SeatGeek Virtual Waiting Room</a> 從第三方換到自建、必然有 dark launch 階段驗證 token 配發機制、再正式 cutover。</p>
<h2 id="canary">Canary</h2>
<p>Canary 是 production-side 驗證最常用工具：小比例流量導到新版本、跟舊版本對比。</p>
<p><strong>運作機制</strong>：</p>
<ul>
<li>小比例（1% / 5% / 10%）流量導到新版本</li>
<li>大部分流量（99% / 95% / 90%）走舊版本</li>
<li>比較 perf / error / business metric</li>
<li>通過 → 漸進放大；不通過 → 自動 rollback</li>
</ul>
<p><strong>漸進放大策略</strong>：1% → 5% → 25% → 50% → 100%、每階段觀察足夠時間（至少 15 分鐘看 long-tail）。</p>
<p><strong>自動 rollback 條件</strong>：</p>
<ul>
<li>error rate canary 比 control 高 X%（例如 50%）</li>
<li>p99 latency canary 比 control 退化 X%（例如 10%）</li>
<li>business metric（conversion rate）canary 比 control 低 X%</li>
</ul>
<p><strong>Canary perf check 跟一般 canary 的差異</strong>：</p>
<ul>
<li>一般 canary：看 error rate 為主</li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary perf check</a>：看 latency / throughput / cost、退化通常早於 error rate</li>
</ul>
<p><strong>比較的對象是 control（同時跑的舊版本）、不是 baseline</strong>：同樣流量同樣時段才能對比、不能拿 canary 跟昨天 baseline 比（外部變數太多）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day pre-event 驗證</a> / <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel canary across 20 州</a> — 按 region 漸進放大、控制 blast radius。</p>
<h2 id="production-like-load-test">Production-like load test</h2>
<p>當需要驗證 <em>peak 場景</em> 但 production 平日流量達不到時、在 production 跑額外的 synthetic load。</p>
<p><strong>為什麼要在 production 跑</strong>：</p>
<ul>
<li>staging 環境的硬體 / 網路 / 第三方依賴跟 production 不同</li>
<li>staging 沒有 production 級資料量、cache hit pattern 不一樣</li>
<li>只有 production 才能驗證真實 peak</li>
</ul>
<p><strong>風險高、必須有安全邊界</strong>：</p>
<ul>
<li>blast radius 限制（用 dedicated test endpoint、限制影響範圍）</li>
<li>abort condition（什麼訊號觸發停止）</li>
<li>rollback path（rollback 流程跟時間）</li>
<li>通訊（相關 oncall 通知、避免誤判 incident）</li>
</ul>
<p><strong>通常用在</strong>：</p>
<ul>
<li>Pre-event 壓測（Black Friday、Super Bowl、IPL 決賽 前一週）</li>
<li>重大架構變更後驗證</li>
<li>容量規劃 review（每年 / 每季）</li>
</ul>
<p><strong>跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 同等嚴格的安全要求</strong>：production 壓測本質是 controlled experiment、必須有 game day-level 的計畫跟人員。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day FIS 8x chaos</a> — 把 chaos test 跟 load test 結合、production-like 驗證；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — pre-event 大規模壓測模擬實際售票場景。</p>
<h2 id="ab-test-與-perf-對齊">A/B test 與 perf 對齊</h2>
<p>Product A/B test（測試新功能對 conversion 的影響）同時也是 perf A/B test。</p>
<p><strong>為什麼要對齊</strong>：</p>
<ul>
<li>新 feature 可能帶來 perf 退化（多 query、多 component、額外 logic）</li>
<li>純看 conversion lift 會誤判：「conversion 上升、所以 OK」可能掩蓋「但 p99 上升 30%」</li>
<li>A/B 同時看 conversion 跟 perf 兩個 metric</li>
</ul>
<p><strong>Guardrails</strong>：</p>
<ul>
<li>業務 metric 改善 + perf 退化 → 工程判斷是否值得（trade-off review）</li>
<li>業務 metric 沒改善 + perf 退化 → 直接 reject</li>
<li>業務 metric 改善 + perf 改善 → 直接 ship</li>
<li>業務 metric 退化 → 不論 perf 怎樣、reject</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 的 experiment guardrails。</p>
<h2 id="pre-event-readiness-checkgame-day">Pre-event readiness check（game day）</h2>
<p>大事件前跑「全系統 production-like 壓測」、是 production-side 驗證的整合演練。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a> 直接對接 — game day 是 readiness 流程的一個 stage。</p>
<p>Shopify game day、Stripe game day 是業界範本（<a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases</a> 有完整案例）。</p>
<h2 id="安全邊界設計">安全邊界設計</h2>
<p>任何 production-side 驗證都要有清楚的安全邊界、不能臨機應變。</p>
<p><strong>Blast radius</strong>：</p>
<ul>
<li>影響哪些用戶（X% 流量、特定 cohort、特定 region）</li>
<li>影響哪些 service（受 perf 影響的下游）</li>
<li>影響哪些 metric（哪些 business metric 可能變化）</li>
</ul>
<p><strong>Abort condition</strong>：</p>
<ul>
<li>什麼訊號觸發停止（error rate &gt; X%、latency &gt; Y ms、特定 alert 觸發）</li>
<li>由誰觸發（自動 vs oncall 手動）</li>
<li>觸發後多久內必須完成 abort（&lt; 60 秒）</li>
</ul>
<p><strong>Rollback path</strong>：</p>
<ul>
<li>rollback 流程是什麼（feature flag、deployment rollback、traffic shift）</li>
<li>rollback 需要多久（target &lt; 5 分鐘）</li>
<li>rollback 是否需要 data 處理（已寫入的資料怎麼處理）</li>
</ul>
<p><strong>通訊</strong>：</p>
<ul>
<li>啟動驗證前 notify 哪些 channel</li>
<li>期間 oncall 待命</li>
<li>結束後 retro</li>
</ul>
<h2 id="反模式">反模式</h2>
<ul>
<li><strong>Canary 比例太大</strong>（50% 起跳）：出事影響大、blast radius 失控</li>
<li><strong>沒 control group</strong>：不知道 baseline、看絕對數字會誤判</li>
<li><strong>Canary 跑太短時間</strong>（&lt; 15 分鐘）：看不到 long-tail、看不到 user pattern shift</li>
<li><strong>沒 abort condition</strong>：人工監控失誤就出事、不可預測</li>
<li><strong>shadow traffic 寫入真實狀態</strong>：可能造成 double charge、duplicate notification</li>
<li><strong>production load test 沒 notify 相關團隊</strong>：被當成 incident、誤觸 escalation</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day FIS 8x</a></td>
          <td>pre-event chaos + perf 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 10K t2.micro 壓測</a></td>
          <td>pre-event 大規模壓測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>跨 20 州 canary 控制 blast radius</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a></td>
          <td>從第三方換到自建的 dark launch</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> / <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">06.4 chaos testing</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark Launch</a></li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a></li>
</ul>
]]></content:encoded></item><item><title>9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/</guid><description>&lt;p>這個案例的核心責任是提供「全球一致性 OLTP」的容量參考點。Spanner 是 Google 內部支撐 Ads、Play、Cloud Search 等服務的核心 DB、後來開放為 GCP 服務、是少數公開能撐每秒 10 億請求且維持強一致性的 OLTP 資料庫。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Spanner 公開數字（引自 &lt;a href="https://cloud.google.com/spanner">Spanner overview&lt;/a> / &lt;a href="https://cloud.google.com/spanner/docs/performance">Spanner performance docs&lt;/a>）：&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>內部峰值&lt;/td>
 &lt;td>&amp;gt; 10 億 requests / 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Spanner Omni 區域峰值&lt;/td>
 &lt;td>數百萬 QPS、PB 級資料量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>線性擴展性&lt;/td>
 &lt;td>2 nodes → 45000 reads/sec、4 nodes → 90000 reads/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性模型&lt;/td>
 &lt;td>external consistency（強一致 + 線性化）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>代表性客戶：Google 內部所有支付、廣告計費、Play 商店、Search 索引；公開客戶包括 Blockchain.com、Niantic（部分服務）、Sharechat、ZEE5、Wayfair。&lt;/p>
&lt;p>關鍵設計：TrueTime API（GPS + 原子鐘）讓跨地區交易能維持 external consistency、不是 eventual。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Spanner 案例最值得讀的不是「能撐多大」、是「為什麼要這樣設計才能撐」。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>線性擴展是 OLTP 的最高設計目標&lt;/strong>：「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」這個 linear scaling 在傳統 OLTP（PostgreSQL、MySQL）做不到 — 因為 &lt;em>跨節點交易&lt;/em> 需要 coordinator、coordinator 是 bottleneck。Spanner 用 Paxos + TrueTime 把 coordinator 變成「拓樸感知的多 leader」、才達成線性。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a> 的設計取捨。&lt;/li>
&lt;li>&lt;strong>強一致 vs 全球部署不是必須二選&lt;/strong>：CAP 定理常被解讀為「全球部署只能 eventual consistency」、Spanner 顯示「投入專屬硬體（GPS、原子鐘）+ 演算法（TrueTime）可以同時拿到 strong consistency + global distribution」。但這套硬體投資對其他 vendor 不容易複製。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的全球 OLTP 選項。&lt;/li>
&lt;li>&lt;strong>計費粒度 = 容量規劃顆粒&lt;/strong>：Spanner 早期最小單位是 100 processing units（pu）≈ 1 node、太大讓中小負載難以用。後來推出 100 pu 起跳的 granular sizing、讓容量規劃可以從小開始。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的容量單位選擇。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「10 億 req/sec」是 Google 內部的某個峰值瞬間、是 Spanner 服務 &lt;em>全部使用者加總&lt;/em>、不是單一 instance 數字。讀案例時要區分「全球聚合峰值」跟「單一客戶能拿到的最大配額」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>跨地區一致性需求要在設計初期決定&lt;/strong>：如果業務必需 strong consistency（金融、ticketing）、選 Spanner 等對等服務；如果 eventual 可接受（社群、推薦）、選 Cassandra / DynamoDB Global Tables 等更便宜的選項。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的全球一致性需求識別。&lt;/li>
&lt;li>&lt;strong>節點數即容量單位、預先規劃 sizing&lt;/strong>：Spanner 容量 = 節點數 × 單節點 QPS。每年 capacity review 主要在調節點數、不在調 schema。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a>。&lt;/li>
&lt;li>&lt;strong>跨地區 latency 是強一致的代價&lt;/strong>：external consistency 必須等多區 quorum、跨洲交易延遲可達 100-200ms。延遲敏感型業務不能用跨地區 strong consistency。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 反推。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS Aurora DSQL（2024 推出、跨地區 strong consistency）、CockroachDB（自管）、TiDB（自管或 cloud）都是對等候選。差異是 TrueTime / 同等同步機制的成熟度。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「全球一致性 OLTP」的容量參考點。Spanner 是 Google 內部支撐 Ads、Play、Cloud Search 等服務的核心 DB、後來開放為 GCP 服務、是少數公開能撐每秒 10 億請求且維持強一致性的 OLTP 資料庫。</p>
<h2 id="觀察">觀察</h2>
<p>Spanner 公開數字（引自 <a href="https://cloud.google.com/spanner">Spanner overview</a> / <a href="https://cloud.google.com/spanner/docs/performance">Spanner performance docs</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部峰值</td>
          <td>&gt; 10 億 requests / 秒</td>
      </tr>
      <tr>
          <td>Spanner Omni 區域峰值</td>
          <td>數百萬 QPS、PB 級資料量</td>
      </tr>
      <tr>
          <td>線性擴展性</td>
          <td>2 nodes → 45000 reads/sec、4 nodes → 90000 reads/sec</td>
      </tr>
      <tr>
          <td>一致性模型</td>
          <td>external consistency（強一致 + 線性化）</td>
      </tr>
  </tbody>
</table>
<p>代表性客戶：Google 內部所有支付、廣告計費、Play 商店、Search 索引；公開客戶包括 Blockchain.com、Niantic（部分服務）、Sharechat、ZEE5、Wayfair。</p>
<p>關鍵設計：TrueTime API（GPS + 原子鐘）讓跨地區交易能維持 external consistency、不是 eventual。</p>
<h2 id="判讀">判讀</h2>
<p>Spanner 案例最值得讀的不是「能撐多大」、是「為什麼要這樣設計才能撐」。</p>
<ol>
<li><strong>線性擴展是 OLTP 的最高設計目標</strong>：「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」這個 linear scaling 在傳統 OLTP（PostgreSQL、MySQL）做不到 — 因為 <em>跨節點交易</em> 需要 coordinator、coordinator 是 bottleneck。Spanner 用 Paxos + TrueTime 把 coordinator 變成「拓樸感知的多 leader」、才達成線性。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的設計取捨。</li>
<li><strong>強一致 vs 全球部署不是必須二選</strong>：CAP 定理常被解讀為「全球部署只能 eventual consistency」、Spanner 顯示「投入專屬硬體（GPS、原子鐘）+ 演算法（TrueTime）可以同時拿到 strong consistency + global distribution」。但這套硬體投資對其他 vendor 不容易複製。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的全球 OLTP 選項。</li>
<li><strong>計費粒度 = 容量規劃顆粒</strong>：Spanner 早期最小單位是 100 processing units（pu）≈ 1 node、太大讓中小負載難以用。後來推出 100 pu 起跳的 granular sizing、讓容量規劃可以從小開始。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的容量單位選擇。</li>
</ol>
<p>需要警惕：「10 億 req/sec」是 Google 內部的某個峰值瞬間、是 Spanner 服務 <em>全部使用者加總</em>、不是單一 instance 數字。讀案例時要區分「全球聚合峰值」跟「單一客戶能拿到的最大配額」。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>跨地區一致性需求要在設計初期決定</strong>：如果業務必需 strong consistency（金融、ticketing）、選 Spanner 等對等服務；如果 eventual 可接受（社群、推薦）、選 Cassandra / DynamoDB Global Tables 等更便宜的選項。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的全球一致性需求識別。</li>
<li><strong>節點數即容量單位、預先規劃 sizing</strong>：Spanner 容量 = 節點數 × 單節點 QPS。每年 capacity review 主要在調節點數、不在調 schema。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a>。</li>
<li><strong>跨地區 latency 是強一致的代價</strong>：external consistency 必須等多區 quorum、跨洲交易延遲可達 100-200ms。延遲敏感型業務不能用跨地區 strong consistency。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency budget 反推。</li>
</ol>
<p>跨平台等效：AWS Aurora DSQL（2024 推出、跨地區 strong consistency）、CockroachDB（自管）、TiDB（自管或 cloud）都是對等候選。差異是 TrueTime / 同等同步機制的成熟度。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想評估全球一致性需求 → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a></li>
<li>想規劃 OLTP 容量 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想對照其他 OLTP 案例 → <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></li>
<li>想看不需要強一致的全球 KV → <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth Cosmos DB</a></li>
<li>想理解 TrueTime ε 與外部一致性實作 → <a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">Spanner TrueTime API 深入</a></li>
<li>想對照 Spanner / Aurora DSQL / CockroachDB 不同一致性層 → <a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">Spanner 一致性模型對照</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/spanner">Spanner: Always-on, virtually unlimited scale database</a></li>
<li><a href="https://cloud.google.com/spanner/docs/performance">Spanner Performance overview</a></li>
<li><a href="https://cloud.google.com/blog/products/databases/using-cloud-spanner-to-handle-high-throughput-writes/">Using Cloud Spanner to handle high throughput writes</a></li>
<li><a href="https://cloud.google.com/blog/products/databases/get-more-out-of-spanner-with-granular-instance-sizing">Get more out of Spanner with granular instance sizing</a></li>
<li><a href="https://aws.amazon.com/blogs/database/amazon-aurora-dsql-for-global-scale-financial-transactions/">Amazon Aurora DSQL for global-scale financial transactions</a></li>
</ul>
]]></content:encoded></item><item><title>2.C10 對照：規模差異下的快取策略</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/</guid><description>&lt;p>這篇對照的核心責任是避免把單一快取做法視為通用解。&lt;/p>
&lt;h2 id="小型服務常見判讀">小型服務常見判讀&lt;/h2>
&lt;p>小型服務最常遇到的問題是切換時沒有先保護回源，快取架構本身夠用。用 cache-aside + TTL 完全可行，但如果沒有 warmup 與簡單限流，某次部署就可能讓熱門 key 全部 miss，直接打爆資料庫。&lt;/p>
&lt;h2 id="中型服務常見判讀">中型服務常見判讀&lt;/h2>
&lt;p>中型服務開始同時承受活動流量與版本切換壓力。這時失敗通常出在「切換順序」而不是策略名稱。先改 key 結構還是先改 TTL，會決定是否出現 stampede 連鎖反應。&lt;/p>
&lt;h2 id="大型服務常見判讀">大型服務常見判讀&lt;/h2>
&lt;p>大型服務下，快取已經是資料平面的一部分。跨區路由、分層儲存與一致性窗口會直接影響業務正確性。這個階段若只盯 hit rate，會漏掉最關鍵的資料一致性風險。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>&lt;code>origin QPS&lt;/code> 在 5 分鐘內超過基線 2 倍且持續上升&lt;/li>
&lt;li>熱門 key miss 同步上升，並伴隨重試流量增加&lt;/li>
&lt;li>stale read 比例連續惡化&lt;/li>
&lt;/ul>
&lt;p>任何一條成立就先暫停切換，回退上一個策略狀態，優先保護回源與資料一致性。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是避免把單一快取做法視為通用解。</p>
<h2 id="小型服務常見判讀">小型服務常見判讀</h2>
<p>小型服務最常遇到的問題是切換時沒有先保護回源，快取架構本身夠用。用 cache-aside + TTL 完全可行，但如果沒有 warmup 與簡單限流，某次部署就可能讓熱門 key 全部 miss，直接打爆資料庫。</p>
<h2 id="中型服務常見判讀">中型服務常見判讀</h2>
<p>中型服務開始同時承受活動流量與版本切換壓力。這時失敗通常出在「切換順序」而不是策略名稱。先改 key 結構還是先改 TTL，會決定是否出現 stampede 連鎖反應。</p>
<h2 id="大型服務常見判讀">大型服務常見判讀</h2>
<p>大型服務下，快取已經是資料平面的一部分。跨區路由、分層儲存與一致性窗口會直接影響業務正確性。這個階段若只盯 hit rate，會漏掉最關鍵的資料一致性風險。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li><code>origin QPS</code> 在 5 分鐘內超過基線 2 倍且持續上升</li>
<li>熱門 key miss 同步上升，並伴隨重試流量增加</li>
<li>stale read 比例連續惡化</li>
</ul>
<p>任何一條成立就先暫停切換，回退上一個策略狀態，優先保護回源與資料一致性。</p>
]]></content:encoded></item><item><title>3.C10 對照：規模差異下的佇列模型</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/</guid><description>&lt;p>這篇對照的核心責任是說明 queue 選型要跟著流量與組織規模改變。&lt;/p>
&lt;h2 id="小型服務常見判讀">小型服務常見判讀&lt;/h2>
&lt;p>小型服務優先用 managed queue 往往最穩，因為運維成本最低。這時候最容易忽略的是語義邊界：重試次數、死信規則、重播責任如果沒先定義，規模一上來就會出現資料重複與漏處理。&lt;/p>
&lt;h2 id="中型服務常見判讀">中型服務常見判讀&lt;/h2>
&lt;p>中型服務常見問題是 lag 與 DLQ 長期累積。根因通常是 consumer idempotency、重播流程、下游承載能力沒有一起設計，broker 效能本身很少是單點問題。&lt;/p>
&lt;h2 id="大型服務常見判讀">大型服務常見判讀&lt;/h2>
&lt;p>大型服務需要處理跨租戶與跨區壓力。此時若還用單叢集思維，任何一類流量尖峰都會拖垮整體。重點會從「怎麼送訊息」轉成「怎麼隔離失敗」。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>&lt;code>consumer lag&lt;/code> 連續超出 SLO 窗口&lt;/li>
&lt;li>&lt;code>DLQ&lt;/code> 速率上升且無法在固定時間內回收&lt;/li>
&lt;li>重播後仍出現相同失敗模式&lt;/li>
&lt;/ul>
&lt;p>出現上述條件應先凍結切換，回到前一語義設定，再逐步修正 consumer 契約與重播流程。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是說明 queue 選型要跟著流量與組織規模改變。</p>
<h2 id="小型服務常見判讀">小型服務常見判讀</h2>
<p>小型服務優先用 managed queue 往往最穩，因為運維成本最低。這時候最容易忽略的是語義邊界：重試次數、死信規則、重播責任如果沒先定義，規模一上來就會出現資料重複與漏處理。</p>
<h2 id="中型服務常見判讀">中型服務常見判讀</h2>
<p>中型服務常見問題是 lag 與 DLQ 長期累積。根因通常是 consumer idempotency、重播流程、下游承載能力沒有一起設計，broker 效能本身很少是單點問題。</p>
<h2 id="大型服務常見判讀">大型服務常見判讀</h2>
<p>大型服務需要處理跨租戶與跨區壓力。此時若還用單叢集思維，任何一類流量尖峰都會拖垮整體。重點會從「怎麼送訊息」轉成「怎麼隔離失敗」。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li><code>consumer lag</code> 連續超出 SLO 窗口</li>
<li><code>DLQ</code> 速率上升且無法在固定時間內回收</li>
<li>重播後仍出現相同失敗模式</li>
</ul>
<p>出現上述條件應先凍結切換，回到前一語義設定，再逐步修正 consumer 契約與重播流程。</p>
]]></content:encoded></item><item><title>4.C10 對照：規模差異下的觀測遷移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/</guid><description>&lt;p>這篇對照的核心責任是提醒觀測遷移是治理能力轉換，工具替換只是表面動作。&lt;/p>
&lt;h2 id="小型團隊常見判讀">小型團隊常見判讀&lt;/h2>
&lt;p>小型團隊最怕雙軌過久。若同時維護兩套儀表，通常會先耗盡人力。小團隊更需要短期對照、快速收斂，而不是一次拉滿所有治理流程。&lt;/p>
&lt;h2 id="中型團隊常見判讀">中型團隊常見判讀&lt;/h2>
&lt;p>中型團隊會碰到 schema 漂移與標籤膨脹。這個階段的失敗常見於「看得到數據，但看不懂是否同一語意」，導致告警與容量判讀彼此矛盾。&lt;/p>
&lt;h2 id="大型團隊常見判讀">大型團隊常見判讀&lt;/h2>
&lt;p>大型團隊的觀測遷移會牽涉成本分攤、採樣策略、collector 拓撲。若只追求功能對齊，往往在遷移後才出現成本暴增與告警漂移。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>新舊管線 &lt;code>error rate&lt;/code> 或 &lt;code>burn rate&lt;/code> 偏差長期超標&lt;/li>
&lt;li>missing signal 比例持續上升&lt;/li>
&lt;li>同一事件在兩套儀表板得到相反結論&lt;/li>
&lt;/ul>
&lt;p>觸發條件時應停止切換，先修資料語意與採樣策略，再決定是否繼續遷移。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>判讀重點是「兩套觀測是否仍在描述同一個系統狀態」。當 error rate、burn rate、trace coverage 三者任一長期偏離，就代表遷移證據不可信，應先停切換再修資料品質。&lt;/p>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這篇對照只處理觀測遷移的判讀邊界，不處理各 vendor 的實作細節。主要風險是把資料語意不一致當成短暫噪音，導致團隊在錯誤證據上推進切換。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a> 修正語意與採樣，再到 &lt;a href="https://tarrragon.github.io/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&lt;/a> 校正雙軌管線。若已影響事故判讀，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp;amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是提醒觀測遷移是治理能力轉換，工具替換只是表面動作。</p>
<h2 id="小型團隊常見判讀">小型團隊常見判讀</h2>
<p>小型團隊最怕雙軌過久。若同時維護兩套儀表，通常會先耗盡人力。小團隊更需要短期對照、快速收斂，而不是一次拉滿所有治理流程。</p>
<h2 id="中型團隊常見判讀">中型團隊常見判讀</h2>
<p>中型團隊會碰到 schema 漂移與標籤膨脹。這個階段的失敗常見於「看得到數據，但看不懂是否同一語意」，導致告警與容量判讀彼此矛盾。</p>
<h2 id="大型團隊常見判讀">大型團隊常見判讀</h2>
<p>大型團隊的觀測遷移會牽涉成本分攤、採樣策略、collector 拓撲。若只追求功能對齊，往往在遷移後才出現成本暴增與告警漂移。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li>新舊管線 <code>error rate</code> 或 <code>burn rate</code> 偏差長期超標</li>
<li>missing signal 比例持續上升</li>
<li>同一事件在兩套儀表板得到相反結論</li>
</ul>
<p>觸發條件時應停止切換，先修資料語意與採樣策略，再決定是否繼續遷移。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>判讀重點是「兩套觀測是否仍在描述同一個系統狀態」。當 error rate、burn rate、trace coverage 三者任一長期偏離，就代表遷移證據不可信，應先停切換再修資料品質。</p>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這篇對照只處理觀測遷移的判讀邊界，不處理各 vendor 的實作細節。主要風險是把資料語意不一致當成短暫噪音，導致團隊在錯誤證據上推進切換。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a> 修正語意與採樣，再到 <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> 校正雙軌管線。若已影響事故判讀，交接到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake</a>。</p>
]]></content:encoded></item><item><title>5.C10 對照：規模差異下的平台遷移</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/</guid><description>&lt;p>這篇對照的核心責任是避免把同一套切流流程套到所有組織規模。遷移策略的切換單位、回退腳本化程度、依賴同步範圍與協同治理工具，在小中大型組織各有不同取捨。&lt;/p>
&lt;h2 id="小型組織常見判讀">小型組織常見判讀&lt;/h2>
&lt;p>小型組織通常能快速完成單叢集遷移，但最容易漏掉回退腳本化。結果是第一次回退就需要人工拼接操作，恢復時間不可預測。&lt;/p>
&lt;p>回退腳本化缺失的具體表現：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>手動 kubectl 操作&lt;/strong>：回退時 on-call 逐一執行 &lt;code>kubectl rollout undo&lt;/code>、手動修改 DNS 權重、手動切回 LB 規則。每一步都依賴執行者的記憶與判斷，步驟順序錯誤或遺漏都會延長恢復時間。&lt;/li>
&lt;li>&lt;strong>無 rollback script&lt;/strong>：回退流程沒有腳本化，也沒有在 staging 驗證過。第一次真正回退就是在 production 事故中。&lt;/li>
&lt;li>&lt;strong>恢復時間不可預測&lt;/strong>：手動操作的恢復時間取決於 on-call 的經驗與當下判斷力。同一個回退在不同人手上可能差 3-10 倍時間。&lt;/li>
&lt;/ul>
&lt;p>小型組織的回退投資最小可行版本是一個 shell script：按正確順序執行回退步驟、每步帶 dry-run 模式、在 staging 驗證過。這個投資的 ROI 在第一次真正回退時就回收。&lt;/p>
&lt;h2 id="中型組織常見判讀">中型組織常見判讀&lt;/h2>
&lt;p>中型組織的主要風險是依賴錯位。服務本身切過去了，但資料面、認證面、觀測面還沒同步，造成切換後局部成功、整體失敗。&lt;/p>
&lt;p>依賴錯位的常見維度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Database endpoint&lt;/strong>：應用在新叢集但仍連舊叢集的資料庫。跨網路延遲從 &amp;lt;1ms 跳到 5-20ms，慢查詢變多、connection pool 壓力增加。嚴重時跨 AZ / region 的網路分區直接斷開連線。&lt;/li>
&lt;li>&lt;strong>Auth service&lt;/strong>：新叢集的服務用舊叢集的 auth endpoint，token 驗證走跨網路。auth 延遲增加讓每個 request 的總延遲上升，高峰時 auth 成為瓶頸。&lt;/li>
&lt;li>&lt;strong>Observability pipeline&lt;/strong>：新叢集的 metrics / logs / traces 仍送到舊叢集的收集器，或送到新收集器但 dashboard 還指向舊資料源。事故時看不到新叢集的指標，判讀盲區。&lt;/li>
&lt;li>&lt;strong>DNS 解析路徑&lt;/strong>：新叢集的 CoreDNS 設定跟舊叢集不同（upstream resolver、search domain、ndots），服務的 DNS 解析行為改變但沒被偵測到。表現為間歇性連線失敗或解析延遲。&lt;/li>
&lt;/ul>
&lt;p>中型組織的遷移 checklist 要把這四個維度列為切換前驗證項目。每個維度各自有切換時機——資料庫通常最後切（風險最高），auth 跟 observability 要先切或同步切。切換順序規劃見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a>。&lt;/p>
&lt;h2 id="大型組織常見判讀">大型組織常見判讀&lt;/h2>
&lt;p>大型組織的遷移失敗主要來自協同節奏失控。若沒有固定升級節奏與責任分工，單次變更容易演變成廣域事故。&lt;/p>
&lt;p>協同節奏的具體治理工具：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Upgrade calendar&lt;/strong>：所有平台級變更（叢集升級、service mesh 升級、CNI 更新）排進共用日曆。避免兩個團隊同週做影響面重疊的變更。日曆的維護者是 platform team，變更申請需提供 blast radius 估算。&lt;/li>
&lt;li>&lt;strong>Freeze window&lt;/strong>：業務高峰期（促銷、財報季、年終）凍結非緊急平台變更。freeze window 的開始 / 結束時間要明確公告，例外申請需 VP 級批准。&lt;/li>
&lt;li>&lt;strong>Blast radius estimation&lt;/strong>：每次變更前估算影響範圍——影響幾個 namespace、幾個 service、幾個使用者。估算結果進 release gate 的判定條件。工具層面可用 admission webhook 掃描變更影響的 namespace 數量。&lt;/li>
&lt;li>&lt;strong>Responsibility matrix&lt;/strong>：遷移期間的 RACI 明確化——誰負責切換、誰負責監控、誰負責回退決策、誰負責對外溝通。大型組織的遷移通常跨 3+ 團隊，責任模糊是事故升級的主要原因。&lt;/li>
&lt;/ul>
&lt;p>大型組織的平台元件升級治理見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a>。&lt;/p>
&lt;h2 id="跨規模的共通判讀">跨規模的共通判讀&lt;/h2>
&lt;p>三個規模的失敗模式不同（小型漏回退腳本、中型漏依賴同步、大型漏協同節奏），但共通原則是「先定回退條件再開始切換」。回退條件包含三個面向：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>觸發條件&lt;/strong>：哪些指標偏離到什麼程度就停止切換（5xx 升幅、延遲惡化、reconnect rate）。&lt;/li>
&lt;li>&lt;strong>執行路徑&lt;/strong>：回退的具體步驟、順序、負責人，且在 staging 驗證過。&lt;/li>
&lt;li>&lt;strong>完成判定&lt;/strong>：回退完成的訊號是什麼（連線數回 baseline、error rate 回 baseline、持續 N 分鐘）。&lt;/li>
&lt;/ol>
&lt;p>三個面向任一缺失，回退就會變成臨時決策——壓力下的臨時決策品質不穩定，是切流事故擴大的共通機制。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>切流批次 &lt;code>5xx&lt;/code> 異常升高&lt;/li>
&lt;li>長連線重連率飆升&lt;/li>
&lt;li>回退時間超過既定 RTO&lt;/li>
&lt;li>跨叢集依賴延遲突增（中型組織特有）&lt;/li>
&lt;/ul>
&lt;p>任一條件成立就停止下一批切換，先完成上一批穩定化與回退驗證。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移&lt;/a> 看切換順序規劃。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 看遷移後的 lifecycle 重新驗證。回 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例&lt;/a> 看切流未 drain 的具體事故 timeline。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是避免把同一套切流流程套到所有組織規模。遷移策略的切換單位、回退腳本化程度、依賴同步範圍與協同治理工具，在小中大型組織各有不同取捨。</p>
<h2 id="小型組織常見判讀">小型組織常見判讀</h2>
<p>小型組織通常能快速完成單叢集遷移，但最容易漏掉回退腳本化。結果是第一次回退就需要人工拼接操作，恢復時間不可預測。</p>
<p>回退腳本化缺失的具體表現：</p>
<ul>
<li><strong>手動 kubectl 操作</strong>：回退時 on-call 逐一執行 <code>kubectl rollout undo</code>、手動修改 DNS 權重、手動切回 LB 規則。每一步都依賴執行者的記憶與判斷，步驟順序錯誤或遺漏都會延長恢復時間。</li>
<li><strong>無 rollback script</strong>：回退流程沒有腳本化，也沒有在 staging 驗證過。第一次真正回退就是在 production 事故中。</li>
<li><strong>恢復時間不可預測</strong>：手動操作的恢復時間取決於 on-call 的經驗與當下判斷力。同一個回退在不同人手上可能差 3-10 倍時間。</li>
</ul>
<p>小型組織的回退投資最小可行版本是一個 shell script：按正確順序執行回退步驟、每步帶 dry-run 模式、在 staging 驗證過。這個投資的 ROI 在第一次真正回退時就回收。</p>
<h2 id="中型組織常見判讀">中型組織常見判讀</h2>
<p>中型組織的主要風險是依賴錯位。服務本身切過去了，但資料面、認證面、觀測面還沒同步，造成切換後局部成功、整體失敗。</p>
<p>依賴錯位的常見維度：</p>
<ul>
<li><strong>Database endpoint</strong>：應用在新叢集但仍連舊叢集的資料庫。跨網路延遲從 &lt;1ms 跳到 5-20ms，慢查詢變多、connection pool 壓力增加。嚴重時跨 AZ / region 的網路分區直接斷開連線。</li>
<li><strong>Auth service</strong>：新叢集的服務用舊叢集的 auth endpoint，token 驗證走跨網路。auth 延遲增加讓每個 request 的總延遲上升，高峰時 auth 成為瓶頸。</li>
<li><strong>Observability pipeline</strong>：新叢集的 metrics / logs / traces 仍送到舊叢集的收集器，或送到新收集器但 dashboard 還指向舊資料源。事故時看不到新叢集的指標，判讀盲區。</li>
<li><strong>DNS 解析路徑</strong>：新叢集的 CoreDNS 設定跟舊叢集不同（upstream resolver、search domain、ndots），服務的 DNS 解析行為改變但沒被偵測到。表現為間歇性連線失敗或解析延遲。</li>
</ul>
<p>中型組織的遷移 checklist 要把這四個維度列為切換前驗證項目。每個維度各自有切換時機——資料庫通常最後切（風險最高），auth 跟 observability 要先切或同步切。切換順序規劃見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>。</p>
<h2 id="大型組織常見判讀">大型組織常見判讀</h2>
<p>大型組織的遷移失敗主要來自協同節奏失控。若沒有固定升級節奏與責任分工，單次變更容易演變成廣域事故。</p>
<p>協同節奏的具體治理工具：</p>
<ul>
<li><strong>Upgrade calendar</strong>：所有平台級變更（叢集升級、service mesh 升級、CNI 更新）排進共用日曆。避免兩個團隊同週做影響面重疊的變更。日曆的維護者是 platform team，變更申請需提供 blast radius 估算。</li>
<li><strong>Freeze window</strong>：業務高峰期（促銷、財報季、年終）凍結非緊急平台變更。freeze window 的開始 / 結束時間要明確公告，例外申請需 VP 級批准。</li>
<li><strong>Blast radius estimation</strong>：每次變更前估算影響範圍——影響幾個 namespace、幾個 service、幾個使用者。估算結果進 release gate 的判定條件。工具層面可用 admission webhook 掃描變更影響的 namespace 數量。</li>
<li><strong>Responsibility matrix</strong>：遷移期間的 RACI 明確化——誰負責切換、誰負責監控、誰負責回退決策、誰負責對外溝通。大型組織的遷移通常跨 3+ 團隊，責任模糊是事故升級的主要原因。</li>
</ul>
<p>大型組織的平台元件升級治理見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a>。</p>
<h2 id="跨規模的共通判讀">跨規模的共通判讀</h2>
<p>三個規模的失敗模式不同（小型漏回退腳本、中型漏依賴同步、大型漏協同節奏），但共通原則是「先定回退條件再開始切換」。回退條件包含三個面向：</p>
<ol>
<li><strong>觸發條件</strong>：哪些指標偏離到什麼程度就停止切換（5xx 升幅、延遲惡化、reconnect rate）。</li>
<li><strong>執行路徑</strong>：回退的具體步驟、順序、負責人，且在 staging 驗證過。</li>
<li><strong>完成判定</strong>：回退完成的訊號是什麼（連線數回 baseline、error rate 回 baseline、持續 N 分鐘）。</li>
</ol>
<p>三個面向任一缺失，回退就會變成臨時決策——壓力下的臨時決策品質不穩定，是切流事故擴大的共通機制。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li>切流批次 <code>5xx</code> 異常升高</li>
<li>長連線重連率飆升</li>
<li>回退時間超過既定 RTO</li>
<li>跨叢集依賴延遲突增（中型組織特有）</li>
</ul>
<p>任一條件成立就停止下一批切換，先完成上一批穩定化與回退驗證。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a> 看切換順序規劃。回 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 看遷移後的 lifecycle 重新驗證。回 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 看切流未 drain 的具體事故 timeline。</p>
]]></content:encoded></item><item><title>7.C10 對照：規模差異下的身份治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/contrast-identity-governance-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/contrast-identity-governance-by-scale/</guid><description>&lt;p>這篇對照的核心責任是讓身份治理隨規模調整，而不是固定流程複製。&lt;/p>
&lt;h2 id="小型服務常見判讀">小型服務常見判讀&lt;/h2>
&lt;p>小型服務先把 MFA 與最小權限做好通常最有效，但常見問題是例外權限累積卻沒有回收節奏。短期看似方便，長期會形成隱性高權限風險。&lt;/p>
&lt;h2 id="中型服務常見判讀">中型服務常見判讀&lt;/h2>
&lt;p>中型服務開始出現支援系統、管理員操作、跨團隊權限交接。這時候若身份治理仍只看產品面登入流程，管理面 token 與支援流程會成為主要缺口。&lt;/p>
&lt;h2 id="大型服務常見判讀">大型服務常見判讀&lt;/h2>
&lt;p>大型服務下，身份控制面會牽涉簽章金鑰、跨租戶隔離與供應鏈責任。這個階段如果沒有分域治理與輪替節奏，故障會以控制面方式快速擴散。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>身份異常事件連續增加&lt;/li>
&lt;li>權限例外核准量超出基線且未收斂&lt;/li>
&lt;li>輪替失敗率上升並波及多系統&lt;/li>
&lt;/ul>
&lt;p>觸發條件後要先凍結高風險變更，再啟動分域輪替與例外重審，避免控制面事故擴大。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是讓身份治理隨規模調整，而不是固定流程複製。</p>
<h2 id="小型服務常見判讀">小型服務常見判讀</h2>
<p>小型服務先把 MFA 與最小權限做好通常最有效，但常見問題是例外權限累積卻沒有回收節奏。短期看似方便，長期會形成隱性高權限風險。</p>
<h2 id="中型服務常見判讀">中型服務常見判讀</h2>
<p>中型服務開始出現支援系統、管理員操作、跨團隊權限交接。這時候若身份治理仍只看產品面登入流程，管理面 token 與支援流程會成為主要缺口。</p>
<h2 id="大型服務常見判讀">大型服務常見判讀</h2>
<p>大型服務下，身份控制面會牽涉簽章金鑰、跨租戶隔離與供應鏈責任。這個階段如果沒有分域治理與輪替節奏，故障會以控制面方式快速擴散。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li>身份異常事件連續增加</li>
<li>權限例外核准量超出基線且未收斂</li>
<li>輪替失敗率上升並波及多系統</li>
</ul>
<p>觸發條件後要先凍結高風險變更，再啟動分域輪替與例外重審，避免控制面事故擴大。</p>
]]></content:encoded></item><item><title>6.10 Contract Testing 與 Schema 演進</title><link>https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Contract testing 在服務邊界上驗證 producer 與 consumer 的相容性，把跨團隊協作的隱性期待變成可執行的&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">契約&lt;/a>。&lt;/p>
&lt;p>這一頁處理的是服務邊界上的信任問題。當服務彼此頻繁演進，契約測試是避免變更互相踩踏的最小保護層。契約對準的是真實 consumer 的期待，而不是抽象的 spec 文件。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>好的 contract testing 會明確劃出兼容視窗，並把驗證放進 CI 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>。&lt;/p>
&lt;p>判讀時看三件事：&lt;/p>
&lt;ul>
&lt;li>契約是否對準真實 consumer，而非假想 client&lt;/li>
&lt;li>schema evolution 是否有明確 compatibility window&lt;/li>
&lt;li>失敗是否能回到責任邊界，而非只看到測試紅燈&lt;/li>
&lt;/ul>
&lt;h2 id="consumer-driven-vs-provider-driven">Consumer-driven vs Provider-driven&lt;/h2>
&lt;p>契約驗證有兩個驅動方向，適用場景不同。&lt;/p>
&lt;p>&lt;strong>Consumer-driven&lt;/strong>：consumer 先定義對 producer 回應的期望（欄位、型別、值域），producer 驗證是否能滿足。這種做法讓驗證對準真實消費需求 — consumer 只關心它用到的欄位，producer 可以自由演進不被使用的部分。缺點是 consumer 數量多時，契約管理成本上升：每個 consumer 維護自己的契約檔，producer 需要跑所有 consumer 契約才能確認相容。&lt;/p>
&lt;p>&lt;strong>Provider-driven&lt;/strong>：producer 定義 API spec（OpenAPI / gRPC schema），consumer 驗證自己能否適配。producer 主導 schema 演進節奏，consumer 接收變更通知並更新。這種做法適合公開 API 或 consumer 數量大且不可控的服務。缺點是可能漏掉 consumer 依賴的隱性行為 — spec 上合規但語意變了，consumer 仍會失敗。&lt;/p>
&lt;p>判斷依據：consumer 少且已知（內部微服務）→ consumer-driven；consumer 多或不可控（公開 API / 平台整合）→ provider-driven。兩者可混用：核心 consumer 用 consumer-driven 保護關鍵路徑，其他 consumer 靠 provider spec 覆蓋。&lt;/p>
&lt;h2 id="契約驗證的三個層次">契約驗證的三個層次&lt;/h2>
&lt;p>契約驗證按深度分三層，每一層攔截不同類型的破壞。&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>Schema 結構&lt;/td>
 &lt;td>欄位是否存在、型別是否一致&lt;/td>
 &lt;td>JSON Schema validation / protobuf 編譯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語意相容&lt;/td>
 &lt;td>值域、enum 範圍、nullable 語意是否對齊&lt;/td>
 &lt;td>Pact interaction / custom assertion&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>向後相容性&lt;/td>
 &lt;td>新版輸出能否被舊版 consumer 解析&lt;/td>
 &lt;td>Avro compatibility check / Buf&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Schema 結構&lt;/strong>是最基礎的防線。欄位缺失或型別錯誤會直接導致 runtime 解析失敗。這一層成本低、回饋快，適合放在 CI fast path。&lt;/p>
&lt;p>&lt;strong>語意相容&lt;/strong>攔截的是「schema 通過但行為不同」的問題。例如某個欄位從 nullable 改成 required，或 enum 新增一個值但 consumer 的 switch 沒有 default branch。這類問題在結構層驗證不出來，需要 consumer 定義語意期望（Pact interaction 的 matcher / assertion）。&lt;/p>
&lt;p>&lt;strong>向後相容性&lt;/strong>是跨版本共存的保障。Avro 和 Protobuf 有內建 compatibility mode（backward / forward / full）；JSON Schema 需要外部工具（如 json-schema-diff）做版本比較。向後相容性驗證的成本最高，但能攔截最嚴重的破壞 — 一旦 event 寫入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>，舊版 consumer 就必須能解析它。&lt;/p>
&lt;h2 id="schema-演進規則">Schema 演進規則&lt;/h2>
&lt;p>Schema 演進按協議類型有不同的安全邊界。&lt;/p>
&lt;h3 id="api-schemaopenapi--grpc">API schema（OpenAPI / gRPC）&lt;/h3>
&lt;p>API schema 的演進判讀：新增可選欄位通常安全；移除欄位、重新命名欄位、或把可選改成必填是 breaking change；型別變更（如 int32 → int64）視 consumer 的容忍度而定。gRPC 的 field number 機制讓欄位新增與移除的相容性比 JSON 更明確 — 未知 field number 被忽略，已知 field number 被刪除會觸發 default value，兩者都有可預測行為。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Contract testing 在服務邊界上驗證 producer 與 consumer 的相容性，把跨團隊協作的隱性期待變成可執行的<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">契約</a>。</p>
<p>這一頁處理的是服務邊界上的信任問題。當服務彼此頻繁演進，契約測試是避免變更互相踩踏的最小保護層。契約對準的是真實 consumer 的期待，而不是抽象的 spec 文件。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>好的 contract testing 會明確劃出兼容視窗，並把驗證放進 CI 或 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a>。</p>
<p>判讀時看三件事：</p>
<ul>
<li>契約是否對準真實 consumer，而非假想 client</li>
<li>schema evolution 是否有明確 compatibility window</li>
<li>失敗是否能回到責任邊界，而非只看到測試紅燈</li>
</ul>
<h2 id="consumer-driven-vs-provider-driven">Consumer-driven vs Provider-driven</h2>
<p>契約驗證有兩個驅動方向，適用場景不同。</p>
<p><strong>Consumer-driven</strong>：consumer 先定義對 producer 回應的期望（欄位、型別、值域），producer 驗證是否能滿足。這種做法讓驗證對準真實消費需求 — consumer 只關心它用到的欄位，producer 可以自由演進不被使用的部分。缺點是 consumer 數量多時，契約管理成本上升：每個 consumer 維護自己的契約檔，producer 需要跑所有 consumer 契約才能確認相容。</p>
<p><strong>Provider-driven</strong>：producer 定義 API spec（OpenAPI / gRPC schema），consumer 驗證自己能否適配。producer 主導 schema 演進節奏，consumer 接收變更通知並更新。這種做法適合公開 API 或 consumer 數量大且不可控的服務。缺點是可能漏掉 consumer 依賴的隱性行為 — spec 上合規但語意變了，consumer 仍會失敗。</p>
<p>判斷依據：consumer 少且已知（內部微服務）→ consumer-driven；consumer 多或不可控（公開 API / 平台整合）→ provider-driven。兩者可混用：核心 consumer 用 consumer-driven 保護關鍵路徑，其他 consumer 靠 provider spec 覆蓋。</p>
<h2 id="契約驗證的三個層次">契約驗證的三個層次</h2>
<p>契約驗證按深度分三層，每一層攔截不同類型的破壞。</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>驗證內容</th>
          <th>常見工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 結構</td>
          <td>欄位是否存在、型別是否一致</td>
          <td>JSON Schema validation / protobuf 編譯</td>
      </tr>
      <tr>
          <td>語意相容</td>
          <td>值域、enum 範圍、nullable 語意是否對齊</td>
          <td>Pact interaction / custom assertion</td>
      </tr>
      <tr>
          <td>向後相容性</td>
          <td>新版輸出能否被舊版 consumer 解析</td>
          <td>Avro compatibility check / Buf</td>
      </tr>
  </tbody>
</table>
<p><strong>Schema 結構</strong>是最基礎的防線。欄位缺失或型別錯誤會直接導致 runtime 解析失敗。這一層成本低、回饋快，適合放在 CI fast path。</p>
<p><strong>語意相容</strong>攔截的是「schema 通過但行為不同」的問題。例如某個欄位從 nullable 改成 required，或 enum 新增一個值但 consumer 的 switch 沒有 default branch。這類問題在結構層驗證不出來，需要 consumer 定義語意期望（Pact interaction 的 matcher / assertion）。</p>
<p><strong>向後相容性</strong>是跨版本共存的保障。Avro 和 Protobuf 有內建 compatibility mode（backward / forward / full）；JSON Schema 需要外部工具（如 json-schema-diff）做版本比較。向後相容性驗證的成本最高，但能攔截最嚴重的破壞 — 一旦 event 寫入 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>，舊版 consumer 就必須能解析它。</p>
<h2 id="schema-演進規則">Schema 演進規則</h2>
<p>Schema 演進按協議類型有不同的安全邊界。</p>
<h3 id="api-schemaopenapi--grpc">API schema（OpenAPI / gRPC）</h3>
<p>API schema 的演進判讀：新增可選欄位通常安全；移除欄位、重新命名欄位、或把可選改成必填是 breaking change；型別變更（如 int32 → int64）視 consumer 的容忍度而定。gRPC 的 field number 機制讓欄位新增與移除的相容性比 JSON 更明確 — 未知 field number 被忽略，已知 field number 被刪除會觸發 default value，兩者都有可預測行為。</p>
<h3 id="event-schemaavro--protobuf--json-schema">Event schema（Avro / Protobuf / JSON Schema）</h3>
<p>Event schema 的相容性要求比 API 更嚴格。API 的 breaking change 可以靠 versioning（<code>/v2/</code>）隔離，event 一旦寫入 broker 就跟所有版本的 consumer 共存。backward compatibility（新 schema 能讀舊資料）是最低要求；forward compatibility（舊 schema 能讀新資料）讓 consumer 可以延遲升級。</p>
<p>Schema registry（Confluent Schema Registry / AWS Glue Schema Registry）提供集中式的相容性 gate：producer 註冊新版 schema 前，registry 自動比對相容性規則，拒絕 breaking change。這個 gate 比 CI 更早攔截，因為它在 schema 發布時就生效。</p>
<p>DB schema 演進的契約驗證銜接到 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a> — expand/contract pattern 讓新舊版本共存，本質上跟 event schema 的 backward compatibility 是同一個問題。</p>
<h2 id="ci-整合">CI 整合</h2>
<p>Contract test 在 CI 的位置跟 unit test 不同 — 需要跨服務的契約同步。</p>
<p><strong>Fast path</strong>：producer 的 schema 變更觸發 consumer 的 contract test。實作上需要 CI 能跨 repo 觸發（webhook / pipeline trigger），或用 contract broker（如 Pact Broker）做非同步驗證。fast path 只跑受影響 consumer 的契約，保持回饋速度。</p>
<p><strong>Slow path</strong>：完整 contract matrix 驗證 — 所有 consumer × producer 組合。這個矩陣在 merge gate 或 scheduled path 跑，覆蓋 fast path 漏掉的間接影響。矩陣規模隨服務數增長，需要 selective matrix（只跑有變更的 producer 相關 consumer）控制成本。</p>
<p><strong>失敗處理</strong>：contract test 失敗時的責任分派是關鍵流程。失敗可能來自 producer 的 breaking change，也可能來自 consumer 的 expectation 過期。Pact 的 can-i-deploy 機制提供自動化判斷：比對 producer 當前版本與 consumer 上次驗證通過的版本，定位責任方。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe</a>：外部整合的 API 需要嚴格的 backward compatibility — 交易 API 的 breaking change 會直接影響商戶收入，schema 演進靠 expand/contract 逐步過渡。</li>
<li><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify</a>：跨服務 deploy 順序錯誤是高峰期常見事故源 — contract test 攔截 schema 不相容，讓 deploy 順序有驗證依據。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：API 與 webhook 的契約覆蓋面廣，契約失配會直接影響整合生態。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨服務 deploy 順序錯誤導致 production 故障</td>
          <td>contract test 應在 CI 攔截相容性問題，deploy 順序才有驗證依據</td>
          <td>補 contract test 到 CI fast path</td>
      </tr>
      <tr>
          <td>API 文件跟實作漂移、新接入服務出意外</td>
          <td>provider-driven spec 需要自動化 diff 偵測，手動更新會漂移</td>
          <td>接 OpenAPI diff 工具到 CI、spec 變更自動 PR</td>
      </tr>
      <tr>
          <td>event schema 變更後下游 consumer 解析失敗</td>
          <td>schema registry 的 compatibility gate 應在 publish 前攔截</td>
          <td>啟用 schema registry 的 compatibility check</td>
      </tr>
      <tr>
          <td>breaking change 靠 release note 標註</td>
          <td>標註是通知、contract test 是攔截，兩者責任不同</td>
          <td>加 CI contract gate 攔截 breaking change</td>
      </tr>
      <tr>
          <td>contract 違規只在 staging 才發現</td>
          <td>contract test 應在 CI fast path 跑，staging 發現代表 CI 沒覆蓋</td>
          <td>把 contract test 從 staging 提前到 CI push 觸發</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：contract test 作為 fast path 的跨服務驗證</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：contract 通過作為放行條件</li>
<li><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>：DB schema 演進的契約驗證</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a>：依賴契約穩定性</li>
<li><a href="/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 environment parity</a>：契約覆蓋的環境邊界</li>
<li><a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 test data</a>：fixture shape 契約</li>
<li><a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 feature flag</a>：flag 不同分支的契約覆蓋</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：跨服務 deploy 順序協調</li>
</ul>
]]></content:encoded></item><item><title>8.10 Stakeholder 通訊與外部狀態頁</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>通訊對象分層：內部 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> team、跨部門 stakeholder、客戶、媒體 / 監管&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 incident communication&lt;/a> 的分工：8.4 是事中通訊節奏、8.10 是對外承諾與補償&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 設計：影響範圍、嚴重度標示、ETA、更新頻率&lt;/li>
&lt;li>對外溝通的三個窗：發現、定位、回復（什麼時候該說什麼）&lt;/li>
&lt;li>補償政策：SLA credit、refund、goodwill；何時主動 / 何時被動&lt;/li>
&lt;li>法規通報：資安事件 vs 可用性事件的法規差異（GDPR / 個資）&lt;/li>
&lt;li>反模式：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 滯後、語焉不詳、過度承諾 ETA、通報義務漏判&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Stakeholder 通訊與外部狀態頁是把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 與補償政策串成一個外部承諾流程，責任是讓不同對象在同一時間看到一致的事件敘述。&lt;/p>
&lt;p>這一頁處理的是對外責任，不只是發布訊息。當外部承諾過度或不一致，信任成本通常比故障本身更高。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 stakeholder communication 時，先看訊息是否分層，再看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 是否可執行。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>內部、客戶、媒體 / 監管是否有不同的訊息節奏&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 是否能清楚描述影響範圍與 ETA&lt;/li>
&lt;li>補償政策是否預先定義，不靠單次協商&lt;/li>
&lt;li>法規通報是否有 checklist 與 owner&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：面向大量工作團隊時，外部狀態頁就是產品的一部分。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365&lt;/a>：廣泛影響的協作服務需要很清楚的外部節奏。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台型服務的 status page 會直接影響信任。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>04.10 client-side / RUM：客戶感知影響的訊號來源&lt;/li>
&lt;li>07 資安：資料外送事件的通報路徑&lt;/li>
&lt;li>08.4 內部通訊：跨層通訊節奏對齊&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：對外公開的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 範圍判定&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 比客戶在 Twitter / 社群上的回報慢&lt;/li>
&lt;li>對外 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 跟內部 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 落差大、外部過度修飾&lt;/li>
&lt;li>補償政策 case-by-case、無預設規則、依個別協商&lt;/li>
&lt;li>法規通報窗口靠 IR commander 個人記憶、無 checklist&lt;/li>
&lt;li>ETA 過度承諾、後續多次延期、消耗信任&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>04.10 client-side / RUM：客戶感知影響的訊號來源&lt;/li>
&lt;li>07 資安：資料外送事件的通報路徑&lt;/li>
&lt;li>08.4 內部通訊：跨層通訊節奏對齊&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：對外公開的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 範圍判定&lt;/li>
&lt;li>08.14 multi-incident：多事故對外通訊不可矛盾&lt;/li>
&lt;li>08.15 vendor 事故：對外通訊的承擔邊界&lt;/li>
&lt;li>08.17 security vs operational：法規通訊的邊界差異&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>通訊對象分層：內部 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> team、跨部門 stakeholder、客戶、媒體 / 監管</li>
<li>跟 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 incident communication</a> 的分工：8.4 是事中通訊節奏、8.10 是對外承諾與補償</li>
<li><a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 設計：影響範圍、嚴重度標示、ETA、更新頻率</li>
<li>對外溝通的三個窗：發現、定位、回復（什麼時候該說什麼）</li>
<li>補償政策：SLA credit、refund、goodwill；何時主動 / 何時被動</li>
<li>法規通報：資安事件 vs 可用性事件的法規差異（GDPR / 個資）</li>
<li>反模式：<a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 滯後、語焉不詳、過度承諾 ETA、通報義務漏判</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Stakeholder 通訊與外部狀態頁是把 <a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a>、<a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 與補償政策串成一個外部承諾流程，責任是讓不同對象在同一時間看到一致的事件敘述。</p>
<p>這一頁處理的是對外責任，不只是發布訊息。當外部承諾過度或不一致，信任成本通常比故障本身更高。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 stakeholder communication 時，先看訊息是否分層，再看 <a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a> 與 <a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 是否可執行。</p>
<p>重點訊號包括：</p>
<ul>
<li>內部、客戶、媒體 / 監管是否有不同的訊息節奏</li>
<li><a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 是否能清楚描述影響範圍與 ETA</li>
<li>補償政策是否預先定義，不靠單次協商</li>
<li>法規通報是否有 checklist 與 owner</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：面向大量工作團隊時，外部狀態頁就是產品的一部分。</li>
<li><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365</a>：廣泛影響的協作服務需要很清楚的外部節奏。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台型服務的 status page 會直接影響信任。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>04.10 client-side / RUM：客戶感知影響的訊號來源</li>
<li>07 資安：資料外送事件的通報路徑</li>
<li>08.4 內部通訊：跨層通訊節奏對齊</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：對外公開的 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 範圍判定</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 比客戶在 Twitter / 社群上的回報慢</li>
<li>對外 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 跟內部 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 落差大、外部過度修飾</li>
<li>補償政策 case-by-case、無預設規則、依個別協商</li>
<li>法規通報窗口靠 IR commander 個人記憶、無 checklist</li>
<li>ETA 過度承諾、後續多次延期、消耗信任</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.10 client-side / RUM：客戶感知影響的訊號來源</li>
<li>07 資安：資料外送事件的通報路徑</li>
<li>08.4 內部通訊：跨層通訊節奏對齊</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：對外公開的 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 範圍判定</li>
<li>08.14 multi-incident：多事故對外通訊不可矛盾</li>
<li>08.15 vendor 事故：對外通訊的承擔邊界</li>
<li>08.17 security vs operational：法規通訊的邊界差異</li>
</ul>
]]></content:encoded></item><item><title>Toxiproxy</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/toxiproxy/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/toxiproxy/</guid><description>&lt;p>Toxiproxy 是 Shopify 開源的 TCP-level fault injection proxy、承擔三個責任：TCP 層 fault inject（latency / bandwidth / partition / slow_close）、integration test 中可程式化故障注入（reproducible）、client SDK 多語言（Go / Ruby / Python / JS）。設計取捨偏向「CI-friendly + reproducible + 細粒度 TCP control」、不適合 production chaos、適合 integration test 跟 dependency failure 模擬。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Toxiproxy server + 設 listener / upstream proxy&lt;/li>
&lt;li>用 client SDK 注入 latency / partition / bandwidth toxic&lt;/li>
&lt;li>整合 Toxiproxy 到 integration test（before/after test hook）&lt;/li>
&lt;li>用 Docker Compose 整合&lt;/li>
&lt;li>評估 Toxiproxy vs Chaos Mesh NetworkChaos 的選用&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-toxiproxy-跑起來">最短路徑：5 分鐘把 Toxiproxy 跑起來&lt;/h2>





&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"># 1. 啟動 server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker run -d -p 8474:8474 -p 26379:26379 ghcr.io/shopify/toxiproxy&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="c1"># 2. 建 proxy（Redis 為例）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: curl -X POST localhost:8474/proxies -d &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;redis&amp;#34;,&amp;#34;listen&amp;#34;:&amp;#34;0.0.0.0:26379&amp;#34;,&amp;#34;upstream&amp;#34;:&amp;#34;redis:6379&amp;#34;}&amp;#39;&lt;/span>
&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="c1"># 3. 注入 toxic&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: curl -X POST localhost:8474/proxies/redis/toxics -d &amp;#39;{&amp;#34;type&amp;#34;:&amp;#34;latency&amp;#34;,&amp;#34;attributes&amp;#34;:{&amp;#34;latency&amp;#34;:1000}}&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="toxic-types">Toxic types&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>latency：增加延遲&lt;/li>
&lt;li>bandwidth：限制頻寬&lt;/li>
&lt;li>slow_close：connection close 慢&lt;/li>
&lt;li>timeout：connection timeout&lt;/li>
&lt;li>slicer：把 TCP packet 切片&lt;/li>
&lt;li>limit_data：limit 傳輸量&lt;/li>
&lt;/ul>
&lt;h3 id="api--client-sdk">API + Client SDK&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>HTTP API（8474 default）&lt;/li>
&lt;li>Client SDK：Go / Ruby / Python / JS&lt;/li>
&lt;li>Programmatic toxic enable/disable&lt;/li>
&lt;/ul>
&lt;h3 id="integration-test-pattern">Integration test pattern&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>before each test 設 toxic&lt;/li>
&lt;li>after each test cleanup&lt;/li>
&lt;li>Test isolation：每 test reset proxy state&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="docker-compose-整合">Docker Compose 整合&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>service depends_on toxiproxy&lt;/li>
&lt;li>應用透過 toxiproxy connect 真正 DB / cache&lt;/li>
&lt;li>environment variable 切換 toxiproxy vs direct&lt;/li>
&lt;/ul>
&lt;h3 id="reproducible-chaos">Reproducible chaos&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Toxiproxy 是 Shopify 開源的 TCP-level fault injection proxy、承擔三個責任：TCP 層 fault inject（latency / bandwidth / partition / slow_close）、integration test 中可程式化故障注入（reproducible）、client SDK 多語言（Go / Ruby / Python / JS）。設計取捨偏向「CI-friendly + reproducible + 細粒度 TCP control」、不適合 production chaos、適合 integration test 跟 dependency failure 模擬。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Toxiproxy server + 設 listener / upstream proxy</li>
<li>用 client SDK 注入 latency / partition / bandwidth toxic</li>
<li>整合 Toxiproxy 到 integration test（before/after test hook）</li>
<li>用 Docker Compose 整合</li>
<li>評估 Toxiproxy vs Chaos Mesh NetworkChaos 的選用</li>
</ol>
<h2 id="最短路徑5-分鐘把-toxiproxy-跑起來">最短路徑：5 分鐘把 Toxiproxy 跑起來</h2>





<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. 啟動 server</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker run -d -p 8474:8474 -p 26379:26379 ghcr.io/shopify/toxiproxy</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="c1"># 2. 建 proxy（Redis 為例）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: curl -X POST localhost:8474/proxies -d &#39;{&#34;name&#34;:&#34;redis&#34;,&#34;listen&#34;:&#34;0.0.0.0:26379&#34;,&#34;upstream&#34;:&#34;redis:6379&#34;}&#39;</span>
</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="c1"># 3. 注入 toxic</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: curl -X POST localhost:8474/proxies/redis/toxics -d &#39;{&#34;type&#34;:&#34;latency&#34;,&#34;attributes&#34;:{&#34;latency&#34;:1000}}&#39;</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="toxic-types">Toxic types</h3>
<p>子議題：</p>
<ul>
<li>latency：增加延遲</li>
<li>bandwidth：限制頻寬</li>
<li>slow_close：connection close 慢</li>
<li>timeout：connection timeout</li>
<li>slicer：把 TCP packet 切片</li>
<li>limit_data：limit 傳輸量</li>
</ul>
<h3 id="api--client-sdk">API + Client SDK</h3>
<p>子議題：</p>
<ul>
<li>HTTP API（8474 default）</li>
<li>Client SDK：Go / Ruby / Python / JS</li>
<li>Programmatic toxic enable/disable</li>
</ul>
<h3 id="integration-test-pattern">Integration test pattern</h3>
<p>子議題：</p>
<ul>
<li>before each test 設 toxic</li>
<li>after each test cleanup</li>
<li>Test isolation：每 test reset proxy state</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="docker-compose-整合">Docker Compose 整合</h3>
<p>子議題：</p>
<ul>
<li>service depends_on toxiproxy</li>
<li>應用透過 toxiproxy connect 真正 DB / cache</li>
<li>environment variable 切換 toxiproxy vs direct</li>
</ul>
<h3 id="reproducible-chaos">Reproducible chaos</h3>
<p>子議題：</p>
<ul>
<li>Toxic seed（reproducible random）</li>
<li>Toxic stream（upstream / downstream）</li>
<li>對應 test reproducibility</li>
</ul>
<h3 id="跟-chaos-mesh-networkchaos-對比">跟 Chaos Mesh NetworkChaos 對比</h3>
<p>子議題：</p>
<ul>
<li>Toxiproxy：CI / integration test、TCP 層</li>
<li>Chaos Mesh：production、K8s pod 層</li>
<li>選擇判讀：testing CI → Toxiproxy；K8s staging chaos → Chaos Mesh</li>
</ul>
<h3 id="跟-client-retry--circuit-breaker-配合">跟 client retry / circuit breaker 配合</h3>
<p>子議題：</p>
<ul>
<li>驗證 client 對 dependency failure 的應對</li>
<li>Retry budget / backoff 測試</li>
<li>Circuit breaker trigger 測試</li>
<li>對應 <a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">knowledge cards retry-budget</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="proxy-連不上">Proxy 連不上</h3>
<p>操作原則：先 <code>curl :8474/proxies</code> 看 proxy state、再看 network。</p>
<h3 id="toxic-沒生效">Toxic 沒生效</h3>
<p>操作原則：toxic enabled 但 attribute 設錯。判讀：API GET toxics 看當前狀態。</p>
<h3 id="test-state-pollute">Test state pollute</h3>
<p>操作原則：test 間沒 reset proxy、state 殘留。修法：每 test 開頭 reset。</p>
<h3 id="performance-overhead">Performance overhead</h3>
<p>操作原則：Toxiproxy 本身有 latency overhead（μs 級）、不適合 production sensitivity。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s production chaos</td>
          <td><a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a> NetworkChaos</td>
      </tr>
      <tr>
          <td>商業跨平台</td>
          <td><a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></td>
      </tr>
      <tr>
          <td>Application-level error</td>
          <td>Mock / stub library</td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td>AWS Fault Injection Service</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Toxic 內部實作</li>
<li>各語言 SDK 完整 API</li>
<li>TCP protocol 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p><strong>Shopify 自家</strong>：Toxiproxy 是 Shopify 開源、Shopify reliability cases 多有引用。</p>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify：BFCM 容量治理與 Game Day</a></td>
          <td>resiliency matrix + TCP-level fault injection 的原生使用脈絡</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe：Idempotency 與零停機遷移</a></td>
          <td>integration test 模擬 dependency 失敗、驗證 retry 與 idempotency</td>
      </tr>
  </tbody>
</table>
<p><strong>Case 庫稀薄</strong>：Toxiproxy 主要 case 集中在 Shopify 自家、其他 adopter 案例待補。</p>
<ul>
<li><strong>待補 Toxiproxy adopter case</strong>：其他公司用 Toxiproxy 做 dependency failure 測試</li>
<li><strong>候選 case</strong>：Pinterest（cache failure mode integration test）、Spotify（squad 自管 integration chaos）— 若未來收錄需先在 cases/ 補正文</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a>、<a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></li>
<li>下游能力：<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">knowledge cards retry-budget</a></li>
</ul>
]]></content:encoded></item><item><title>0.10 知識網：容量、觀測與資安決策路徑</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-operations-security/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-operations-security/</guid><description>&lt;p>服務治理的核心原則是把可用性與安全性放在同一張決策圖上。&lt;code>timeout&lt;/code>、&lt;code>deadline&lt;/code>、&lt;code>readiness&lt;/code>、&lt;code>runbook&lt;/code>、&lt;code>RTO/RPO&lt;/code>、&lt;code>authentication&lt;/code>、&lt;code>authorization&lt;/code>、&lt;code>TLS/mTLS&lt;/code> 與 &lt;code>audit log&lt;/code> 描述的是同一件事：系統如何在壓力與風險下維持可運作。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用「容量-觀測-資安」三軸描述服務治理需求&lt;/li>
&lt;li>把術語連成可追蹤的決策鏈，而非獨立名詞&lt;/li>
&lt;li>判斷何時先補觀測與操作能力，何時先補安全控制&lt;/li>
&lt;li>明確區分概念決策與平台實作邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="判讀容量控制與恢復目標是一條線">【判讀】容量控制與恢復目標是一條線&lt;/h2>
&lt;p>容量治理的核心問題是「系統在壓力下如何守住核心能力」。&lt;code>timeout&lt;/code>、&lt;code>deadline&lt;/code>、&lt;code>backpressure&lt;/code>、&lt;code>rate limit&lt;/code> 與 &lt;code>fallback&lt;/code> 應該連到同一個恢復目標。&lt;/p>
&lt;p>對應卡片關係：&lt;/p>
&lt;ul>
&lt;li>請求邊界：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">Timeout&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">Deadline&lt;/a>&lt;/li>
&lt;li>壓力控制：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/token-bucket/" data-link-title="Token Bucket" data-link-desc="說明 token bucket 如何用配額與補充速率控制流量">Token Bucket&lt;/a>&lt;/li>
&lt;li>退讓策略：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">Fallback&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">Degradation&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover&lt;/a>&lt;/li>
&lt;li>恢復目標：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>如果只定義 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>，沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 與回復目標，系統仍缺少操作上的可控性。&lt;/p>
&lt;h2 id="判讀可觀測訊號要服務操作決策">【判讀】可觀測訊號要服務操作決策&lt;/h2>
&lt;p>可觀測性的核心問題是「問題出現時，團隊能否在時間內採取正確動作」。&lt;code>log&lt;/code>、&lt;code>metrics&lt;/code>、&lt;code>trace&lt;/code>、&lt;code>alert&lt;/code> 與 &lt;code>runbook&lt;/code> 必須一起設計。&lt;/p>
&lt;p>對應卡片關係：&lt;/p>
&lt;ul>
&lt;li>事件與脈絡：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">Log Schema&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">Correlation ID&lt;/a>&lt;/li>
&lt;li>趨勢與目標：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget&lt;/a>&lt;/li>
&lt;li>路徑與定位：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace Context&lt;/a>&lt;/li>
&lt;li>執行與回應：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">Alert Runbook&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>當觀測鏈完整後，才適合比較具體平台組合。&lt;/p>
&lt;h2 id="判讀資安控制要對齊資料流與角色責任">【判讀】資安控制要對齊資料流與角色責任&lt;/h2>
&lt;p>資安治理的核心問題是「誰可以在什麼條件下接觸哪類資料」。身份、授權、傳輸保護、秘密管理與稽核需要同時成立。&lt;/p>
&lt;p>對應卡片關係：&lt;/p>
&lt;ul>
&lt;li>身份與存取：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">Authentication&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">Least Privilege&lt;/a>&lt;/li>
&lt;li>傳輸與憑證：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS/mTLS&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/certificate-chain-trust/" data-link-title="Certificate Chain and Trust Root" data-link-desc="說明網站憑證鏈與信任根如何影響連線可用性與驗證結果">Certificate Chain and Trust&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">Certificate Revocation&lt;/a>&lt;/li>
&lt;li>秘密與輪替：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/certificate-rotation-renewal/" data-link-title="Certificate Rotation and Renewal" data-link-desc="說明網站憑證如何安全續期與輪替以避免停機">Certificate Rotation and Renewal&lt;/a>&lt;/li>
&lt;li>敏感資料與稽核：&lt;br>
&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">Data Masking&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>若資安設計只停在單一工具，缺少資料流路徑與角色責任描述，章節仍停在術語層。&lt;/p>
&lt;h2 id="判讀事故治理把容量觀測與資安接起來">【判讀】事故治理把容量、觀測與資安接起來&lt;/h2>
&lt;p>事故治理的核心問題是「異常發生時，如何在可接受風險下恢復服務」。severity、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a>、timeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day&lt;/a> 是將前面三軸落地的操作語言。&lt;/p></description><content:encoded><![CDATA[<p>服務治理的核心原則是把可用性與安全性放在同一張決策圖上。<code>timeout</code>、<code>deadline</code>、<code>readiness</code>、<code>runbook</code>、<code>RTO/RPO</code>、<code>authentication</code>、<code>authorization</code>、<code>TLS/mTLS</code> 與 <code>audit log</code> 描述的是同一件事：系統如何在壓力與風險下維持可運作。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用「容量-觀測-資安」三軸描述服務治理需求</li>
<li>把術語連成可追蹤的決策鏈，而非獨立名詞</li>
<li>判斷何時先補觀測與操作能力，何時先補安全控制</li>
<li>明確區分概念決策與平台實作邊界</li>
</ol>
<hr>
<h2 id="判讀容量控制與恢復目標是一條線">【判讀】容量控制與恢復目標是一條線</h2>
<p>容量治理的核心問題是「系統在壓力下如何守住核心能力」。<code>timeout</code>、<code>deadline</code>、<code>backpressure</code>、<code>rate limit</code> 與 <code>fallback</code> 應該連到同一個恢復目標。</p>
<p>對應卡片關係：</p>
<ul>
<li>請求邊界：<br>
<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">Timeout</a> / <a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">Deadline</a></li>
<li>壓力控制：<br>
<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure</a> / <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a> / <a href="/blog/backend/knowledge-cards/token-bucket/" data-link-title="Token Bucket" data-link-desc="說明 token bucket 如何用配額與補充速率控制流量">Token Bucket</a></li>
<li>退讓策略：<br>
<a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">Fallback</a> / <a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">Degradation</a> / <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover</a></li>
<li>恢復目標：<br>
<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></li>
</ul>
<p>如果只定義 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>，沒有 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 與回復目標，系統仍缺少操作上的可控性。</p>
<h2 id="判讀可觀測訊號要服務操作決策">【判讀】可觀測訊號要服務操作決策</h2>
<p>可觀測性的核心問題是「問題出現時，團隊能否在時間內採取正確動作」。<code>log</code>、<code>metrics</code>、<code>trace</code>、<code>alert</code> 與 <code>runbook</code> 必須一起設計。</p>
<p>對應卡片關係：</p>
<ul>
<li>事件與脈絡：<br>
<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log</a> / <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">Log Schema</a> / <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">Correlation ID</a></li>
<li>趨勢與目標：<br>
<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics</a> / <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a> / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget</a></li>
<li>路徑與定位：<br>
<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace</a> / <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace Context</a></li>
<li>執行與回應：<br>
<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a> / <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">Alert Runbook</a> / <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a></li>
</ul>
<p>當觀測鏈完整後，才適合比較具體平台組合。</p>
<h2 id="判讀資安控制要對齊資料流與角色責任">【判讀】資安控制要對齊資料流與角色責任</h2>
<p>資安治理的核心問題是「誰可以在什麼條件下接觸哪類資料」。身份、授權、傳輸保護、秘密管理與稽核需要同時成立。</p>
<p>對應卡片關係：</p>
<ul>
<li>身份與存取：<br>
<a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">Authentication</a> / <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> / <a href="/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">Least Privilege</a></li>
<li>傳輸與憑證：<br>
<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS/mTLS</a> / <a href="/blog/backend/knowledge-cards/certificate-chain-trust/" data-link-title="Certificate Chain and Trust Root" data-link-desc="說明網站憑證鏈與信任根如何影響連線可用性與驗證結果">Certificate Chain and Trust</a> / <a href="/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">Certificate Revocation</a></li>
<li>秘密與輪替：<br>
<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> / <a href="/blog/backend/knowledge-cards/certificate-rotation-renewal/" data-link-title="Certificate Rotation and Renewal" data-link-desc="說明網站憑證如何安全續期與輪替以避免停機">Certificate Rotation and Renewal</a></li>
<li>敏感資料與稽核：<br>
<a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> / <a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">Data Masking</a> / <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
</ul>
<p>若資安設計只停在單一工具，缺少資料流路徑與角色責任描述，章節仍停在術語層。</p>
<h2 id="判讀事故治理把容量觀測與資安接起來">【判讀】事故治理把容量、觀測與資安接起來</h2>
<p>事故治理的核心問題是「異常發生時，如何在可接受風險下恢復服務」。severity、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a>、timeline、<a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 與 <a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a> 是將前面三軸落地的操作語言。</p>
<p>對應卡片：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">Incident Severity</a></li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">On-call</a></li>
<li><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">Incident Timeline</a></li>
<li><a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a></li>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day</a></li>
</ul>
<p>這些概念建立後，事故處理不會只依賴個人臨場反應。</p>
<h2 id="邊界何時從概念章節進入實作章節">【邊界】何時從概念章節進入實作章節</h2>
<p>當以下問題都能回答時，代表概念層已完成，可以進入實作模組：</p>
<ol>
<li>核心服務的容量保護鏈是什麼（timeout 到 fallback）</li>
<li>告警觸發後，<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 的第一個與第二個動作是什麼</li>
<li>高風險資料在系統內的流動路徑與存取角色是什麼</li>
<li>事故升級與回報節點如何定義</li>
</ol>
<p>下一步建議路由：</p>
<ul>
<li>進入可觀測實作能力：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04-observability</a></li>
<li>進入部署與可靠性能力：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05-deployment-platform</a> / <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06-reliability</a></li>
<li>進入資安與資料保護能力：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07-security-data-protection</a></li>
<li>進入事故治理能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08-incident-response</a></li>
</ul>
]]></content:encoded></item><item><title>Datadog OTLP Ingestion 與 OTel 整合</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/otlp-ingestion-otel-integration/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/otlp-ingestion-otel-integration/</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「OTLP ingestion」段。初次接觸 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 的 OTLP ingestion：&lt;/p>
&lt;p>團隊已經使用 Datadog APM，但新服務或新語言想用 OTel SDK 避免 vendor lock-in。Datadog SDK 覆蓋的語言有限（Go / Java / Python / Ruby / Node / .NET / PHP / C++），如果服務用 Rust / Elixir / Kotlin multiplatform，OTel SDK 的覆蓋更廣。&lt;/p>
&lt;p>另一種情境是團隊原本用 OTel + Jaeger 或 OTel + Grafana，現在想把 visualization 遷到 Datadog 但不想重新 instrument。OTLP ingestion 讓 OTel SDK 產出的 traces / metrics / logs 直接送進 Datadog，不改 application code。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="datadog-agent-的-otlp-receiver">Datadog Agent 的 OTLP receiver&lt;/h3>
&lt;p>Datadog Agent 6.32+ 內建 OTLP receiver，接受 gRPC（port 4317）和 HTTP（port 4318）兩種 protocol。Agent 收到 OTLP 資料後轉換成 Datadog 內部格式，走跟 Datadog SDK 相同的 pipeline（sampling、tagging、forwarding to Datadog backend）。&lt;/p>
&lt;p>這代表 OTLP path 的資料在 Datadog UI 裡跟 Datadog SDK path 的資料一樣被處理 — 相同的 APM trace waterfall、相同的 service map、相同的 error tracking。差異在 metadata 完整度（見下方 feature parity）。&lt;/p>
&lt;h3 id="三種-signal-的-otlp-支援度">三種 signal 的 OTLP 支援度&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>OTLP 支援&lt;/th>
 &lt;th>到 Datadog 的對應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Traces&lt;/td>
 &lt;td>完整（OTLP gRPC / HTTP）&lt;/td>
 &lt;td>APM traces、service map、error tracking&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metrics&lt;/td>
 &lt;td>完整（OTLP gRPC / HTTP）&lt;/td>
 &lt;td>Custom metrics（按 metric 計費）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Logs&lt;/td>
 &lt;td>有限（Agent 7.54+ 支援 OTLP logs）&lt;/td>
 &lt;td>Datadog Logs（按 ingestion volume 計費）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Traces 的 OTLP 支援最成熟、metrics 次之、logs 最新。混合環境常見做法是 traces + metrics 走 OTLP、logs 走 Datadog Agent 的原生 log collection（file tailing / container stdout）。&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「OTLP ingestion」段。初次接觸 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 的 OTLP ingestion：</p>
<p>團隊已經使用 Datadog APM，但新服務或新語言想用 OTel SDK 避免 vendor lock-in。Datadog SDK 覆蓋的語言有限（Go / Java / Python / Ruby / Node / .NET / PHP / C++），如果服務用 Rust / Elixir / Kotlin multiplatform，OTel SDK 的覆蓋更廣。</p>
<p>另一種情境是團隊原本用 OTel + Jaeger 或 OTel + Grafana，現在想把 visualization 遷到 Datadog 但不想重新 instrument。OTLP ingestion 讓 OTel SDK 產出的 traces / metrics / logs 直接送進 Datadog，不改 application code。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="datadog-agent-的-otlp-receiver">Datadog Agent 的 OTLP receiver</h3>
<p>Datadog Agent 6.32+ 內建 OTLP receiver，接受 gRPC（port 4317）和 HTTP（port 4318）兩種 protocol。Agent 收到 OTLP 資料後轉換成 Datadog 內部格式，走跟 Datadog SDK 相同的 pipeline（sampling、tagging、forwarding to Datadog backend）。</p>
<p>這代表 OTLP path 的資料在 Datadog UI 裡跟 Datadog SDK path 的資料一樣被處理 — 相同的 APM trace waterfall、相同的 service map、相同的 error tracking。差異在 metadata 完整度（見下方 feature parity）。</p>
<h3 id="三種-signal-的-otlp-支援度">三種 signal 的 OTLP 支援度</h3>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>OTLP 支援</th>
          <th>到 Datadog 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Traces</td>
          <td>完整（OTLP gRPC / HTTP）</td>
          <td>APM traces、service map、error tracking</td>
      </tr>
      <tr>
          <td>Metrics</td>
          <td>完整（OTLP gRPC / HTTP）</td>
          <td>Custom metrics（按 metric 計費）</td>
      </tr>
      <tr>
          <td>Logs</td>
          <td>有限（Agent 7.54+ 支援 OTLP logs）</td>
          <td>Datadog Logs（按 ingestion volume 計費）</td>
      </tr>
  </tbody>
</table>
<p>Traces 的 OTLP 支援最成熟、metrics 次之、logs 最新。混合環境常見做法是 traces + metrics 走 OTLP、logs 走 Datadog Agent 的原生 log collection（file tailing / container stdout）。</p>
<h3 id="datadog-sdk-vs-otel-sdk-feature-parity">Datadog SDK vs OTel SDK feature parity</h3>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>Datadog SDK</th>
          <th>OTel SDK → Datadog</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Distributed tracing</td>
          <td>有</td>
          <td>有（完整）</td>
      </tr>
      <tr>
          <td>Continuous profiling</td>
          <td>有</td>
          <td>無（Datadog 專有）</td>
      </tr>
      <tr>
          <td>ASM（Application Security）</td>
          <td>有</td>
          <td>無（需要 Datadog library）</td>
      </tr>
      <tr>
          <td>CI Visibility</td>
          <td>有</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Dynamic instrumentation</td>
          <td>有</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Runtime metrics（GC、thread）</td>
          <td>自動</td>
          <td>需手動配置 OTel metric instrumentation</td>
      </tr>
      <tr>
          <td>Log correlation（trace_id 注入 log）</td>
          <td>自動</td>
          <td>需手動配置（MDC / context propagation）</td>
      </tr>
      <tr>
          <td>Unified service tagging</td>
          <td>自動（<code>DD_SERVICE</code> / <code>DD_ENV</code> / <code>DD_VERSION</code>）</td>
          <td>需 resource attribute mapping</td>
      </tr>
  </tbody>
</table>
<p>判讀：如果團隊需要 profiling / ASM / CI Visibility，對應服務仍需 Datadog SDK。其他服務可以用 OTel SDK + OTLP ingestion，兩者在同一個 Datadog org 共存。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="datadog-agent-otlp-設定">Datadog Agent OTLP 設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># datadog.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">otlp_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">receiver</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">protocols</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">grpc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4317</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4318</span></span></span></code></pre></div><p>Agent 重啟後用 <code>datadog-agent status</code> 確認 OTLP receiver 啟動。</p>
<h3 id="otel-sdk-endpoint-配置">OTel SDK endpoint 配置</h3>





<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"># 環境變數（語言無關）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT</span><span class="o">=</span><span class="s2">&#34;http://datadog-agent:4317&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_EXPORTER_OTLP_PROTOCOL</span><span class="o">=</span><span class="s2">&#34;grpc&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_SERVICE_NAME</span><span class="o">=</span><span class="s2">&#34;checkout-api&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_RESOURCE_ATTRIBUTES</span><span class="o">=</span><span class="s2">&#34;deployment.environment=production,service.version=1.2.3&#34;</span></span></span></code></pre></div><h3 id="resource-attribute--datadog-tag-mapping">Resource attribute → Datadog tag mapping</h3>
<p>Datadog Agent 自動把 OTel resource attributes 轉成 Datadog tags：</p>
<table>
  <thead>
      <tr>
          <th>OTel resource attribute</th>
          <th>Datadog tag</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>service.name</code></td>
          <td><code>service</code></td>
          <td>Datadog unified service tagging 的核心</td>
      </tr>
      <tr>
          <td><code>deployment.environment</code></td>
          <td><code>env</code></td>
          <td>必填、否則 Datadog UI 的環境篩選失效</td>
      </tr>
      <tr>
          <td><code>service.version</code></td>
          <td><code>version</code></td>
          <td>用於 deployment tracking</td>
      </tr>
      <tr>
          <td><code>host.name</code></td>
          <td><code>host</code></td>
          <td>Agent 通常自動帶、不需手動設</td>
      </tr>
      <tr>
          <td><code>container.name</code></td>
          <td><code>container_name</code></td>
          <td>K8s 環境自動帶</td>
      </tr>
  </tbody>
</table>
<p>如果 resource attribute 沒設 <code>deployment.environment</code>，Datadog 會把 trace 歸到 <code>env:none</code> — 在 APM 介面幾乎不可見。這是最常見的 OTLP onboarding 問題。</p>
<h3 id="otel-collector--datadogalternative-path">OTel Collector → Datadog（alternative path）</h3>
<p>如果不想讓 application 直連 Datadog Agent，可以在中間放 OTel Collector：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># otel-collector-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">exporters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">datadog</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">api</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${DD_API_KEY}</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">site</span><span class="p">:</span><span class="w"> </span><span class="l">datadoghq.com</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">pipelines</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">traces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">receivers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">otlp]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">processors</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">batch]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">exporters</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">datadog]</span></span></span></code></pre></div><p>OTel Collector 的 <code>datadog</code> exporter 直接把資料送到 Datadog backend（不經 Agent）。適合已有 OTel Collector 基礎設施、不想每個 node 都部署 Datadog Agent 的場景。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="resource-attribute-mapping-不對齊">Resource attribute mapping 不對齊</h3>
<p>OTel 的 <code>service.name</code> 用 dot notation（如 <code>com.example.checkout</code>），Datadog 預設用 hyphen（如 <code>checkout-api</code>）。如果 mapping 不一致，同一個服務在 Datadog APM 的 service map 會出現多個節點（OTel path 一個、Datadog SDK path 一個）。</p>
<p>修法：統一 <code>service.name</code> 命名。如果兩種 SDK 並存，在 OTel SDK 的 resource attribute 設跟 Datadog SDK 的 <code>DD_SERVICE</code> 完全相同的值。</p>
<h3 id="metric-naming-convention-差異">Metric naming convention 差異</h3>
<p>OTel metric 用 dot notation（<code>http.server.request.duration</code>），Datadog 預設用 underscore（<code>http_server_request_duration</code>）。Agent 會自動轉換（dot → underscore），但如果團隊同時有 Datadog SDK 產出的 metric 跟 OTel SDK 產出的 metric，兩者可能在 Datadog 裡產生重複（語意相同但名稱不同）。</p>
<p>修法：用 OTel Collector 的 <code>metricstransform</code> processor 在 export 前統一命名，或在 Datadog 用 metric alias 合併。</p>
<h3 id="log-correlation-在-otlp-path-的限制">Log correlation 在 OTLP path 的限制</h3>
<p>Datadog SDK 自動把 <code>dd.trace_id</code> 和 <code>dd.span_id</code> 注入 application log（如 Python logging、Java MDC）。OTel SDK 不做這件事 — log correlation 需要手動設定（把 <code>trace_id</code> 從 OTel context 注入 logging framework）。</p>
<p>如果 log correlation 缺失，Datadog 的 trace → log 跳轉功能失效。修法依語言不同：Java 用 MDC + OTel Java agent 的 log context instrumentation；Python 用 <code>opentelemetry-instrumentation-logging</code>；Go 需要手動從 span context 取 trace ID 寫到 log field。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>OTLP path 的計費跟 Datadog SDK path 相同：</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>計費單位</th>
          <th>OTLP vs Datadog SDK</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>APM traces</td>
          <td>Per ingested span</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>Metrics</td>
          <td>Per custom metric（unique metric name × tag combination）</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>Logs</td>
          <td>Per ingested GB</td>
          <td>相同</td>
      </tr>
  </tbody>
</table>
<p>成本差異不在 ingestion pricing，在 <strong>feature access</strong>。用 OTel SDK 失去 Profiling / ASM / CI Visibility，這些功能需要 Datadog SDK。如果團隊需要這些功能，走 OTLP 反而要為核心服務額外部署 Datadog SDK — 雙 SDK 的 maintenance cost 可能超過直接全用 Datadog SDK。</p>
<p>判斷分水嶺：如果 &gt; 80% 的服務不需要 Profiling / ASM，走 OTLP + 少數服務用 Datadog SDK 是合理的混合模式。如果核心服務都需要 Profiling，全用 Datadog SDK 更簡單。</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="../cost-governance-agent-config/">Datadog 成本治理</a>：Agent 配置與 cost control</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 SDK 轉向 OTel 相容模式的治理案例</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</a>：OTel Collector → Datadog 的 alternative path</li>
<li><a href="../migrate-from-new-relic/">← New Relic migration</a>：New Relic → Datadog 的遷移中 OTLP 扮演的橋接角色</li>
</ul>
]]></content:encoded></item><item><title>Grafana Loki 設計與操作限制</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/loki-design-operational-limits/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/loki-design-operational-limits/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a> 的 vendor deep article，深化 overview「Loki 設計與限制」段。初次接觸 Grafana Stack 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>團隊從 ELK stack 或 CloudWatch Logs 遷到 Grafana Stack 時，Loki 是 log backend 的預設選擇。遷移後最常遇到的衝擊是查詢模式的根本差異：Elasticsearch 做 full-text index（寫入時索引每個欄位、查詢時任意搜尋），Loki 只 index labels（寫入時只索引 stream labels、查詢時先篩 stream 再 grep content）。&lt;/p>
&lt;p>這個差異是刻意的設計選擇 — Loki 的目標是「Prometheus for logs」：用跟 Prometheus metrics 相同的 label 體系管理 logs，讓 log 查詢跟 metric 查詢使用同一組 label selector。代價是失去 full-text search 的即時性。理解這個設計哲學才能正確設計 label、寫出有效率的 LogQL、避免常見的效能陷阱。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="like-prometheus-but-for-logs">Like Prometheus, but for logs&lt;/h3>
&lt;p>Prometheus 用 label set 識別 time series — &lt;code>{job=&amp;quot;checkout&amp;quot;, instance=&amp;quot;10.0.1.5&amp;quot;}&lt;/code> 是一條 series。Loki 用相同概念識別 log stream — &lt;code>{job=&amp;quot;checkout&amp;quot;, namespace=&amp;quot;production&amp;quot;}&lt;/code> 是一條 stream。同一條 stream 的所有 log entries 存在同一組 chunks。&lt;/p>
&lt;p>Elasticsearch 的索引模式是「寫入時建 inverted index、查詢時走索引」。Loki 的索引模式是「寫入時只記錄 stream label → chunk 的 mapping、查詢時先用 label 選 stream、再在 chunk 內做 grep」。&lt;/p>
&lt;p>這代表：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>有 label filter 的查詢很快&lt;/strong> — Loki 只掃對應 stream 的 chunks&lt;/li>
&lt;li>&lt;strong>沒有 label filter 的查詢很慢&lt;/strong> — Loki 要掃所有 stream 的 chunks（相當於 full scan）&lt;/li>
&lt;li>&lt;strong>Label cardinality 跟 Prometheus 一樣敏感&lt;/strong> — 高 cardinality label 產生大量 stream、每個 stream 的 chunk 很小、index 膨脹&lt;/li>
&lt;/ul>
&lt;h3 id="stream-與-chunk">Stream 與 chunk&lt;/h3>
&lt;p>一條 stream = 一組唯一的 label set。每條 stream 的 log entries 依時間排序存在 chunks 裡。Chunk 是 Loki 的最小儲存單位。&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">Stream: {job=&amp;#34;checkout&amp;#34;, namespace=&amp;#34;production&amp;#34;}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> └─ Chunk 1: [2026-06-22T00:00 ~ 2026-06-22T01:00] (compressed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └─ Chunk 2: [2026-06-22T01:00 ~ 2026-06-22T02:00] (compressed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> └─ ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Chunk 存在 object storage（S3 / GCS / MinIO），index 存在 key-value store（BoltDB / TSDB，3.0 起預設 TSDB）。Object storage 便宜（相比 Elasticsearch 的 SSD），這是 Loki 成本優勢的來源。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> 的 vendor deep article，深化 overview「Loki 設計與限制」段。初次接觸 Grafana Stack 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>團隊從 ELK stack 或 CloudWatch Logs 遷到 Grafana Stack 時，Loki 是 log backend 的預設選擇。遷移後最常遇到的衝擊是查詢模式的根本差異：Elasticsearch 做 full-text index（寫入時索引每個欄位、查詢時任意搜尋），Loki 只 index labels（寫入時只索引 stream labels、查詢時先篩 stream 再 grep content）。</p>
<p>這個差異是刻意的設計選擇 — Loki 的目標是「Prometheus for logs」：用跟 Prometheus metrics 相同的 label 體系管理 logs，讓 log 查詢跟 metric 查詢使用同一組 label selector。代價是失去 full-text search 的即時性。理解這個設計哲學才能正確設計 label、寫出有效率的 LogQL、避免常見的效能陷阱。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="like-prometheus-but-for-logs">Like Prometheus, but for logs</h3>
<p>Prometheus 用 label set 識別 time series — <code>{job=&quot;checkout&quot;, instance=&quot;10.0.1.5&quot;}</code> 是一條 series。Loki 用相同概念識別 log stream — <code>{job=&quot;checkout&quot;, namespace=&quot;production&quot;}</code> 是一條 stream。同一條 stream 的所有 log entries 存在同一組 chunks。</p>
<p>Elasticsearch 的索引模式是「寫入時建 inverted index、查詢時走索引」。Loki 的索引模式是「寫入時只記錄 stream label → chunk 的 mapping、查詢時先用 label 選 stream、再在 chunk 內做 grep」。</p>
<p>這代表：</p>
<ul>
<li><strong>有 label filter 的查詢很快</strong> — Loki 只掃對應 stream 的 chunks</li>
<li><strong>沒有 label filter 的查詢很慢</strong> — Loki 要掃所有 stream 的 chunks（相當於 full scan）</li>
<li><strong>Label cardinality 跟 Prometheus 一樣敏感</strong> — 高 cardinality label 產生大量 stream、每個 stream 的 chunk 很小、index 膨脹</li>
</ul>
<h3 id="stream-與-chunk">Stream 與 chunk</h3>
<p>一條 stream = 一組唯一的 label set。每條 stream 的 log entries 依時間排序存在 chunks 裡。Chunk 是 Loki 的最小儲存單位。</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">Stream: {job=&#34;checkout&#34;, namespace=&#34;production&#34;}
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └─ Chunk 1: [2026-06-22T00:00 ~ 2026-06-22T01:00] (compressed)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  └─ Chunk 2: [2026-06-22T01:00 ~ 2026-06-22T02:00] (compressed)
</span></span><span class="line"><span class="ln">4</span><span class="cl">  └─ ...</span></span></code></pre></div><p>Chunk 存在 object storage（S3 / GCS / MinIO），index 存在 key-value store（BoltDB / TSDB，3.0 起預設 TSDB）。Object storage 便宜（相比 Elasticsearch 的 SSD），這是 Loki 成本優勢的來源。</p>
<h3 id="跟-elasticsearch-的根本差異">跟 Elasticsearch 的根本差異</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Loki</th>
          <th>Elasticsearch</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引對象</td>
          <td>只索引 labels（stream metadata）</td>
          <td>索引所有欄位（full-text + structured）</td>
      </tr>
      <tr>
          <td>查詢模式</td>
          <td>Label selector → stream → grep content</td>
          <td>Query DSL / KQL → inverted index lookup</td>
      </tr>
      <tr>
          <td>寫入成本</td>
          <td>低（不建 content index）</td>
          <td>高（建 inverted index + doc values）</td>
      </tr>
      <tr>
          <td>查詢成本</td>
          <td>取決於 stream 篩選效率（label 越精準越快）</td>
          <td>取決於 index 覆蓋度（indexed field 查詢快）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>低（object storage）</td>
          <td>高（SSD / local disk）</td>
      </tr>
      <tr>
          <td>Full-text search</td>
          <td>不支援（只有 line filter grep）</td>
          <td>原生支援</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>已有 Prometheus/Grafana 生態的 log aggregation</td>
          <td>需要 full-text search 的 log analytics / SIEM</td>
      </tr>
  </tbody>
</table>
<p>判讀：如果團隊的 log 查詢模式是「先選 service/namespace/pod、再看時間範圍內的 log entries」，Loki 足夠。如果查詢模式是「在所有 log 裡搜某個 error message 或 request ID」，Elasticsearch 的 full-text index 更適合。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="label-設計原則">Label 設計原則</h3>
<p>Label 設計是 Loki 最重要的操作決策。原則跟 Prometheus 相同：低 cardinality、穩定、有查詢意義。</p>
<table>
  <thead>
      <tr>
          <th>Label</th>
          <th>Cardinality</th>
          <th>適合當 label</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>job</code></td>
          <td>低（服務數量）</td>
          <td>適合</td>
          <td>篩選到特定服務</td>
      </tr>
      <tr>
          <td><code>namespace</code></td>
          <td>低</td>
          <td>適合</td>
          <td>篩選到特定環境</td>
      </tr>
      <tr>
          <td><code>pod_name</code></td>
          <td>中（pod 數量）</td>
          <td>視情境</td>
          <td>K8s 環境常用但 pod 頻繁重建會產生大量短命 stream</td>
      </tr>
      <tr>
          <td><code>level</code>（info/warn/error）</td>
          <td>低（3-5 值）</td>
          <td>適合</td>
          <td>快速篩選 error log</td>
      </tr>
      <tr>
          <td><code>request_id</code></td>
          <td>極高（per-request）</td>
          <td>不適合</td>
          <td>每個 request 一條 stream、chunk 極小、index 爆炸</td>
      </tr>
      <tr>
          <td><code>user_id</code></td>
          <td>高</td>
          <td>不適合</td>
          <td>同上</td>
      </tr>
      <tr>
          <td><code>trace_id</code></td>
          <td>極高</td>
          <td>不適合</td>
          <td>用 Tempo 查 trace、不用 Loki label</td>
      </tr>
  </tbody>
</table>
<p>request_id / user_id / trace_id 不應該是 label，它們應該在 log content 裡用 structured JSON 欄位表達，查詢時用 LogQL 的 line filter 或 parser 提取。</p>
<h3 id="logql-常見查詢模式">LogQL 常見查詢模式</h3>
<p><strong>Stream selector + line filter</strong>（最基本）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">{job=&#34;checkout&#34;, namespace=&#34;production&#34;} |= &#34;error&#34; |= &#34;timeout&#34;</code></pre><p>先選 stream、再 grep 包含 &ldquo;error&rdquo; 和 &ldquo;timeout&rdquo; 的 log lines。<code>|=</code> 是包含、<code>!=</code> 是不包含、<code>|~</code> 是 regex。</p>
<p><strong>Structured metadata parser</strong>（JSON log）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">{job=&#34;checkout&#34;} | json | status_code &gt;= 500 | line_format &#34;{{.method}} {{.path}} {{.status_code}}&#34;</code></pre><p><code>| json</code> 解析 JSON log entry 的欄位，後續可以用欄位做 filter 和格式化。</p>
<p><strong>Metric 聚合</strong>（log → metric）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">sum by (status_code) (rate({job=&#34;checkout&#34;} | json | __error__=&#34;&#34; [5m]))</code></pre><p>計算每 5 分鐘每個 status_code 的 log entry 速率。這是 Loki 的「metric from logs」能力 — 不需要額外的 metrics pipeline，直接從 log 產生 time series。</p>
<h3 id="loki-config-核心段">Loki config 核心段</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># loki-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">schema_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span>- <span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="ld">2024-01-01</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">store</span><span class="p">:</span><span class="w"> </span><span class="l">tsdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">object_store</span><span class="p">:</span><span class="w"> </span><span class="l">s3</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span><span class="l">v13</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">index</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">loki_index_</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">period</span><span class="p">:</span><span class="w"> </span><span class="l">24h</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">storage_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">tsdb_shipper</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">active_index_directory</span><span class="p">:</span><span class="w"> </span><span class="l">/loki/index</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">cache_location</span><span class="p">:</span><span class="w"> </span><span class="l">/loki/cache</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">aws</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">s3</span><span class="p">:</span><span class="w"> </span><span class="l">s3://loki-chunks-bucket</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">region</span><span class="p">:</span><span class="w"> </span><span class="l">us-east-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="nt">limits_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">ingestion_rate_mb</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">ingestion_burst_size_mb</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">max_streams_per_user</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="nt">max_label_name_length</span><span class="p">:</span><span class="w"> </span><span class="m">1024</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="nt">max_label_value_length</span><span class="p">:</span><span class="w"> </span><span class="m">2048</span></span></span></code></pre></div><p><code>limits_config</code> 是防護網。<code>max_streams_per_user</code> 限制每個 tenant 的 stream 數量，超過時新 stream 的 log 被拒（HTTP 429）。這是 label cardinality 爆炸的最後防線。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="label-cardinality-爆炸">Label cardinality 爆炸</h3>
<p><strong>觸發條件</strong>：label 包含高 cardinality 值（pod UID、request ID、container ID）。每個唯一 label set 產生一條 stream，stream 數量快速增長。</p>
<p><strong>表現</strong>：<code>loki_ingester_memory_streams</code> 持續上升、ingester memory 增長、最終觸發 <code>max_streams_per_user</code> 限制（429 error）。跟 Prometheus series explosion 是同一個問題的 log 版本。</p>
<p><strong>修法</strong>：檢查產出大量 stream 的 label。Loki 的 <code>/loki/api/v1/labels</code> 和 <code>/loki/api/v1/label/{name}/values</code> API 可以列出所有 label 值。找到高 cardinality label 後，從 promtail / alloy 的 pipeline 中移除該 label、改放進 log content 的 structured field。</p>
<h3 id="stream-rate-limit">Stream rate limit</h3>
<p><strong>觸發條件</strong>：單一 stream 的 ingestion rate 超過 <code>per_stream_rate_limit</code>（預設 3 MB/s）。通常是某個 service 大量噴 debug log。</p>
<p><strong>表現</strong>：Loki 回傳 429 + <code>rate limit exceeded</code> error。部分 log entries 被丟棄。</p>
<p><strong>修法</strong>：先解決 log 噴量問題（降低 debug log level 或加 sampling）。如果噴量合理（高 QPS 服務），調高 <code>per_stream_rate_limit</code> 或拆分 stream（加一層 label 分散流量）。</p>
<h3 id="大時間範圍查詢-timeout">大時間範圍查詢 timeout</h3>
<p><strong>觸發條件</strong>：LogQL 查詢沒有精確的 label filter、時間範圍 &gt; 24 小時。Loki 要掃描大量 chunks、query timeout（預設 3 分鐘）觸發。</p>
<p><strong>表現</strong>：Grafana 顯示 query timeout error。</p>
<p><strong>修法</strong>：查詢時先用 label selector 縮小 stream 範圍（<code>{job=&quot;checkout&quot;, namespace=&quot;production&quot;}</code> 而非 <code>{namespace=&quot;production&quot;}</code>），再用 line filter 進一步篩。如果業務需要長時間範圍的 log analytics，考慮用 LogQL 的 metric aggregation（<code>rate(...)</code> / <code>count_over_time(...)</code>）替代原始 log 掃描。</p>
<h3 id="chunk-target-size-與-ingestion-rate-的關係">Chunk target size 與 ingestion rate 的關係</h3>
<p><code>chunk_target_size</code>（預設 1.5 MB）控制 chunk 的大小。ingestion rate 低的 stream 可能幾個小時才填滿一個 chunk — 這段期間 chunk 停在 ingester memory 裡。大量低 ingestion rate 的 stream（= 高 cardinality label）會讓 ingester 同時持有大量未 flush 的 chunks，佔用記憶體。</p>
<p>修法方向：降低 <code>chunk_idle_period</code>（預設 30 分鐘，時間到即使 chunk 未滿也 flush），或減少低 cardinality stream 的數量。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Loki 的成本結構跟 Elasticsearch 根本不同：</p>
<table>
  <thead>
      <tr>
          <th>成本項</th>
          <th>Loki</th>
          <th>Elasticsearch</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>儲存</td>
          <td>Object storage（S3/GCS）— 便宜</td>
          <td>SSD / local disk — 貴</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>小（只索引 labels）</td>
          <td>大（inverted index + doc values）</td>
      </tr>
      <tr>
          <td>查詢 compute</td>
          <td>每次查詢 grep chunks — CPU 密集</td>
          <td>走 index — 相對輕</td>
      </tr>
      <tr>
          <td>適合的 workload</td>
          <td>高 volume、低 query frequency</td>
          <td>高 query frequency、需要 full-text</td>
      </tr>
  </tbody>
</table>
<p>Loki 在「每天寫 TB 級 log、偶爾查一下」的場景成本遠低於 Elasticsearch。但在「每天查數百次、需要快速 full-text search」的場景，Elasticsearch 的 pre-indexed 查詢效能更好，Loki 每次 grep 的 compute cost 反而更高。</p>
<p>成本治理的判讀：監控 <code>loki_ingester_bytes_received_total</code>（ingestion volume）和 <code>loki_querier_query_duration_seconds</code>（query cost）。如果 query duration 持續上升，先檢查是 label filter 不夠精確還是 query 時間範圍太大。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>：overview 與全棧操作</li>
<li><a href="../lgtm-stack-operations/">LGTM Stack Operations</a>：Loki 在 LGTM 全棧中的部署位置</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：Loki 不適合 audit log 的 compliance 查詢（無 immutable storage 保證、無 fine-grained access control）— 合規需求用 BigQuery 或 dedicated audit backend</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">Healthcare 存取追溯案例</a>：分層 retention 在 Loki 用 tenant-level retention policy 實現</li>
<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>：log 欄位設計影響 Loki 的 label 設計與 parser 效率</li>
<li><a href="/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/" data-link-title="Index Lifecycle Management 與 Log Pipeline" data-link-desc="說明 Elasticsearch ILM policy 設計、data stream / rollover、Beats vs Elastic Agent 採集選擇、ingest pipeline 與 shard sizing、cross-cluster 策略與 cost governance">Elasticsearch ILM 與 Log Pipeline</a>：需要 full-text search 時的替代方案</li>
</ul>
]]></content:encoded></item><item><title>4.C11 Uber：M3 大規模 Metrics 平台</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</guid><description>&lt;p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。&lt;/p>
&lt;p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="單機記憶體天花板">單機記憶體天花板&lt;/h3>
&lt;p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃&lt;/a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。&lt;/p>
&lt;p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。&lt;/p>
&lt;h3 id="retention-與長期趨勢">Retention 與長期趨勢&lt;/h3>
&lt;p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。&lt;/p>
&lt;p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。&lt;/p>
&lt;h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢&lt;/h3>
&lt;p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。&lt;/p>
&lt;p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。&lt;/p>
&lt;h2 id="解法m3-平台">解法：M3 平台&lt;/h2>
&lt;p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。&lt;/p>
&lt;h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage&lt;/h3>
&lt;p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 &lt;code>realtime&lt;/code> namespace 保留 2 天全精度，&lt;code>aggregated_1m&lt;/code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。&lt;/p></description><content:encoded><![CDATA[<p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。</p>
<h2 id="業務背景">業務背景</h2>
<p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。</p>
<p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="單機記憶體天花板">單機記憶體天花板</h3>
<p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 <a href="/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃</a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。</p>
<p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。</p>
<h3 id="retention-與長期趨勢">Retention 與長期趨勢</h3>
<p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。</p>
<p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。</p>
<h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢</h3>
<p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。</p>
<p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。</p>
<h2 id="解法m3-平台">解法：M3 平台</h2>
<p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。</p>
<h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage</h3>
<p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 <code>realtime</code> namespace 保留 2 天全精度，<code>aggregated_1m</code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。</p>
<p>M3DB 的記憶體模型跟 Prometheus 不同 — 近期資料在記憶體，冷資料在 disk，不像 Prometheus 把所有 active series 都放 head block。這讓它能處理遠超單機 Prometheus 的 series 數量。</p>
<h3 id="m3-coordinator統一查詢入口">M3 Coordinator：統一查詢入口</h3>
<p>M3 Coordinator 接收 PromQL 查詢，轉譯後分發到 M3DB 節點，聚合結果後返回。對 Grafana 和 alerting rules 來說，M3 Coordinator 的 API 跟 Prometheus 完全相容 — 不需要改 dashboard 或 alert config。</p>
<h3 id="m3-aggregator寫入路徑聚合">M3 Aggregator：寫入路徑聚合</h3>
<p>高 cardinality 的原始 series 在寫入 M3DB 前先經過 M3 Aggregator 做 pre-aggregation — 例如把每秒的 request count 聚合成每分鐘，再寫入長期 namespace。這控制了長期儲存的資料量跟成本。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Prometheus standalone</th>
          <th>M3 平台</th>
          <th>Mimir / Thanos（替代）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署複雜度</td>
          <td>低（單一 binary）</td>
          <td>高（M3DB + Coordinator + Aggregator）</td>
          <td>中到高</td>
      </tr>
      <tr>
          <td>單機 series 上限</td>
          <td>~500 萬-1000 萬</td>
          <td>不適用（分散式）</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Retention tiering</td>
          <td>無</td>
          <td>原生支援</td>
          <td>Thanos compactor / Mimir 支援</td>
      </tr>
      <tr>
          <td>PromQL 相容</td>
          <td>原生</td>
          <td>相容</td>
          <td>相容</td>
      </tr>
      <tr>
          <td>社群活躍度</td>
          <td>高（CNCF）</td>
          <td>低（Uber 主導、2023 後維護縮減）</td>
          <td>高（Grafana Labs / 社群）</td>
      </tr>
      <tr>
          <td>適用規模</td>
          <td>單團隊到中型組織</td>
          <td>大型組織（數十億 series）</td>
          <td>中型到大型</td>
      </tr>
  </tbody>
</table>
<p>M3 的最大風險是社群活躍度 — Uber 自 2023 年後縮減了 M3 的開發投入，Grafana Mimir 成為更活躍的替代。新專案選型時，Mimir 跟 Thanos 的社群支援度跟 Grafana 生態整合度都優於 M3。M3 的價值在於它驗證了「分散式 TSDB + 寫入路徑聚合 + retention tiering」這組設計模式，這組模式在 Mimir 跟 Thanos 裡以不同形式被採用。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 Metrics Basics</a>：active series、cardinality 與 recording rules 的基礎模型，M3 的 pre-aggregation 對應 recording rules 的平台化版本。</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>：M3 的 Aggregator 是 pipeline 中 processing 層的實例。</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/" data-link-title="Remote Write 與長期儲存整合" data-link-desc="說明 Prometheus remote write 的配置、三家長期儲存後端比較（Mimir / Thanos / Cortex）、故障模式與容量規劃">Prometheus Remote Write 與長期儲存</a>：M3 是 remote write 目標之一，跟 Mimir / Thanos / Cortex 的比較在該文。</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>：M3 的 per-namespace cardinality limit 是治理機制的生產實例。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>單一 Prometheus 實例 memory 接近機器上限，開始 OOM restart</li>
<li>多個 Prometheus 實例各自 scrape，跨服務查詢需要手動關聯</li>
<li>Retention 15 天不夠做季度趨勢分析，但拉長 retention 資源撐不住</li>
<li>團隊開始問「我們的 metrics 總共有多少 series、誰佔最多」但沒有統一的 cardinality 觀測</li>
<li>Grafana federation dashboard 查詢越來越慢或經常 timeout</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.uber.com/en-GB/blog/m3/">M3: Uber&rsquo;s Open Source, Large-scale Metrics Platform for Prometheus</a></li>
</ul>
]]></content:encoded></item><item><title>Cloud Logging 查詢、匯出與合規</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-logging-export-compliance/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-logging-export-compliance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a> 的 vendor deep article，深化 overview「Cloud Logging 結構化 logs」跟「BigQuery 匯出長期儲存」段。初次接觸 GCP 觀測的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Cloud Logging 對 GCP 服務是預設開啟的 — GKE、Cloud Run、Cloud Functions 的 stdout/stderr 自動進 Cloud Logging，工程師不需要配置就能查。問題出在後續階段：log 量成長後的成本控制（GCP 的 ingestion 計費讓高 volume 服務成本快速累積）、合規需求要求特定 log 保留特定時間（healthcare / fintech 的 7 年留存）、organization-level 的 log 聚合與存取控制（多 project 集中 audit）、以及 PII 在 log 中的遮罩與加密。理解 Cloud Logging 的 router / sink 架構跟 retention bucket 才能從「預設全收」走向「可治理的 log pipeline」。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="log-router-與-sink">Log Router 與 Sink&lt;/h3>
&lt;p>Cloud Logging 的資料流是 &lt;strong>log entry → log router → sink → destination&lt;/strong>。每一筆 log 進入 Cloud Logging 後，log router 根據 inclusion filter 跟 exclusion filter 決定這筆 log 送到哪些 destination。&lt;/p>
&lt;p>&lt;strong>Sink&lt;/strong> 是 log router 的輸出端點。每個 GCP project 預設有兩個 sink：&lt;code>_Required&lt;/code>（admin activity audit log、system event，不可關閉）和 &lt;code>_Default&lt;/code>（其他所有 log、送到 &lt;code>_Default&lt;/code> log bucket、可修改 filter）。工程師可以建立自訂 sink，把符合條件的 log 送到 BigQuery、Cloud Storage、Pub/Sub 或 Splunk。&lt;/p>
&lt;p>&lt;strong>Exclusion filter&lt;/strong> 在 log router 層攔截 — 被排除的 log 不會寫入任何 sink destination，也不計入 ingestion 計費。這是成本控制的第一道防線。&lt;/p>
&lt;p>&lt;strong>Inclusion filter&lt;/strong> 在 sink 層生效 — 只有符合 filter 的 log 會送到該 sink 的 destination。&lt;/p>
&lt;p>路由順序很重要：exclusion filter 先執行（全域攔截），然後 &lt;code>_Required&lt;/code> sink 攔走必留 log，然後 &lt;code>_Default&lt;/code> sink 跟自訂 sink 各自的 inclusion filter 平行執行。一筆 log 可以同時送到多個 sink。&lt;/p>
&lt;h3 id="retention-與-log-bucket">Retention 與 Log Bucket&lt;/h3>
&lt;p>Cloud Logging 的儲存單位是 &lt;strong>log bucket&lt;/strong>。每個 project 預設有兩個 bucket：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a> 的 vendor deep article，深化 overview「Cloud Logging 結構化 logs」跟「BigQuery 匯出長期儲存」段。初次接觸 GCP 觀測的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Cloud Logging 對 GCP 服務是預設開啟的 — GKE、Cloud Run、Cloud Functions 的 stdout/stderr 自動進 Cloud Logging，工程師不需要配置就能查。問題出在後續階段：log 量成長後的成本控制（GCP 的 ingestion 計費讓高 volume 服務成本快速累積）、合規需求要求特定 log 保留特定時間（healthcare / fintech 的 7 年留存）、organization-level 的 log 聚合與存取控制（多 project 集中 audit）、以及 PII 在 log 中的遮罩與加密。理解 Cloud Logging 的 router / sink 架構跟 retention bucket 才能從「預設全收」走向「可治理的 log pipeline」。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="log-router-與-sink">Log Router 與 Sink</h3>
<p>Cloud Logging 的資料流是 <strong>log entry → log router → sink → destination</strong>。每一筆 log 進入 Cloud Logging 後，log router 根據 inclusion filter 跟 exclusion filter 決定這筆 log 送到哪些 destination。</p>
<p><strong>Sink</strong> 是 log router 的輸出端點。每個 GCP project 預設有兩個 sink：<code>_Required</code>（admin activity audit log、system event，不可關閉）和 <code>_Default</code>（其他所有 log、送到 <code>_Default</code> log bucket、可修改 filter）。工程師可以建立自訂 sink，把符合條件的 log 送到 BigQuery、Cloud Storage、Pub/Sub 或 Splunk。</p>
<p><strong>Exclusion filter</strong> 在 log router 層攔截 — 被排除的 log 不會寫入任何 sink destination，也不計入 ingestion 計費。這是成本控制的第一道防線。</p>
<p><strong>Inclusion filter</strong> 在 sink 層生效 — 只有符合 filter 的 log 會送到該 sink 的 destination。</p>
<p>路由順序很重要：exclusion filter 先執行（全域攔截），然後 <code>_Required</code> sink 攔走必留 log，然後 <code>_Default</code> sink 跟自訂 sink 各自的 inclusion filter 平行執行。一筆 log 可以同時送到多個 sink。</p>
<h3 id="retention-與-log-bucket">Retention 與 Log Bucket</h3>
<p>Cloud Logging 的儲存單位是 <strong>log bucket</strong>。每個 project 預設有兩個 bucket：</p>
<ul>
<li><code>_Required</code> bucket：admin activity audit log 跟 system event，保留 400 天，不可刪除或修改 retention</li>
<li><code>_Default</code> bucket：其他所有 log，預設保留 30 天，可調整為 1-3650 天</li>
</ul>
<p>自訂 log bucket 可以設定不同 retention 期。常見用法：把 application log 留 30 天、把 audit log 留 7 年（送到自訂 bucket 或 BigQuery）。</p>
<p>Cloud Logging 的 ingestion 計費跟 storage 計費是分開的。前 50 GiB/month per billing account 的 ingestion 免費；超過後按 ingestion volume 計費。<code>_Required</code> log 的 ingestion 免費。Storage 在 <code>_Default</code> bucket 的前 0.5 GiB 免費，自訂 bucket 按用量計費。</p>
<p>成本治理判讀：高 volume 服務（例如 GKE 的 container stdout）的成本主要來自 ingestion，而非 storage。Exclusion filter 攔掉不需要的 log 是最直接的降成本方式。</p>
<h3 id="查詢語言">查詢語言</h3>
<p>Cloud Logging 的查詢語言用在 Logs Explorer 跟 gcloud CLI：</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">resource.type=&#34;k8s_container&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">resource.labels.cluster_name=&#34;prod-us-central1&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">severity&gt;=ERROR
</span></span><span class="line"><span class="ln">4</span><span class="cl">jsonPayload.order_id=&#34;ord-12345&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">timestamp&gt;=&#34;2026-06-22T00:00:00Z&#34;</span></span></code></pre></div><p>語法特點：field path 用 <code>.</code> 分隔、支援 comparison operators（<code>=</code> / <code>!=</code> / <code>&gt;</code> / <code>&gt;=</code> / <code>&lt;</code> / <code>&lt;=</code>）、支援 boolean（<code>AND</code> / <code>OR</code> / <code>NOT</code>）、支援 regex（<code>=~</code> / <code>!~</code>）。</p>
<p>跟 KQL（Elastic）或 LogQL（Loki）相比，Cloud Logging 查詢語言更接近 structured filter 而非 full-text search。Full-text 搜尋要用 <code>textPayload:</code> 或 <code>jsonPayload:</code> prefix。進階分析（aggregation、time bucketing、join）需要匯出到 BigQuery 後用 SQL 做。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="organization-level-log-聚合">Organization-level log 聚合</h3>
<p>多 project 環境下，集中 log 的標準做法是在 organization 或 folder level 建立 aggregated sink：</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">gcloud logging sinks create org-audit-sink \
</span></span><span class="line"><span class="ln">2</span><span class="cl">  bigquery.googleapis.com/projects/central-audit/datasets/org_audit_logs \
</span></span><span class="line"><span class="ln">3</span><span class="cl">  --organization=123456789 \
</span></span><span class="line"><span class="ln">4</span><span class="cl">  --include-children \
</span></span><span class="line"><span class="ln">5</span><span class="cl">  --log-filter=&#39;logName:&#34;cloudaudit.googleapis.com&#34;&#39;</span></span></code></pre></div><p><code>--include-children</code> 讓 organization 下所有 project、folder 的符合 log 都送到同一個 BigQuery dataset。Sink 的 service account 需要 destination 的寫入權限（BigQuery Data Editor）。</p>
<p>適用場景：SOC 團隊需要跨 project 的 audit log 查詢、compliance team 需要集中的 data access log 存檔、security team 需要異常 IAM 變更的全域偵測。</p>
<h3 id="data-access-audit-logs-啟用">Data Access Audit Logs 啟用</h3>
<p>GCP 的 audit log 分三類：</p>
<ul>
<li><strong>Admin Activity</strong>：對資源的管理操作（建立 / 刪除 / 修改 IAM）。預設開啟、不可關閉、不計費。</li>
<li><strong>Data Access</strong>：對資源的讀取操作（BigQuery query、GCS read、Cloud SQL connect）。預設關閉（除 BigQuery）、需手動啟用、計費。</li>
<li><strong>System Event</strong>：GCP 系統自動操作。預設開啟、不可關閉、不計費。</li>
</ul>
<p>Data Access audit log 的啟用是 per-service、per-project（或 org level）。啟用後 log 量會大幅增加 — 一個高 QPS 的 Cloud SQL 服務可能每秒產生數百筆 data access log。成本跟 volume 判讀要先做。</p>
<p>建議做法：先對 security-sensitive 服務啟用（IAM / KMS / Cloud SQL / GCS），其他服務按需啟用。用 exclusion filter 精細控制 — 例如只保留 <code>ADMIN_READ</code> 跟 <code>DATA_WRITE</code>、排除 <code>DATA_READ</code>（read 量通常遠大於 write）。</p>
<h3 id="vpc-flow-logs-與-dns-logs-的觀測用途">VPC Flow Logs 與 DNS Logs 的觀測用途</h3>
<p>VPC Flow Logs 記錄每一筆通過 VPC 的網路流量元資料（src/dst IP、port、protocol、bytes、packets）。啟用方式是 per-subnet 設定、支援 sampling rate（100% / 50% / 10%）。</p>
<p>DNS Logs 記錄 VPC 內的 DNS 查詢（query name、response code、source VM）。啟用方式是 per-VPC 或 per-policy 設定。</p>
<p>觀測用途：</p>
<ul>
<li><strong>異常流量偵測</strong>：VPC Flow Logs 送到 BigQuery 後用 SQL 找出異常流量模式（大量對外連線、非預期 port、跨 region 資料傳輸）</li>
<li><strong>網路效能分析</strong>：量測 inter-service latency、跨 AZ 流量比例</li>
<li><strong>安全稽核</strong>：DNS Logs 偵測 DNS tunneling 或 C2 callback</li>
</ul>
<p>成本注意：VPC Flow Logs 在高流量服務上的 ingestion 量非常大。100% sampling + 高 QPS 服務可能每天產生 TB 級 log。建議用 sampling rate 控制、或只對 security-sensitive subnet 啟用 100%。</p>
<h3 id="自建-vs-managed-pipeline-的取捨">自建 vs managed pipeline 的取捨</h3>
<p><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">Cloudflare 觀測案例</a>展示了自建觀測 pipeline 的理由 — 全球 300+ edge locations、每秒數十億 request 的規模下，SaaS 觀測平台的帳單不合理，自建 pipeline 的 compute 成本反而更低。</p>
<p>但多數團隊的結論是反過來的。GCP 環境下，Cloud Logging 的 managed pipeline（log entry → router → sink → BigQuery / Cloud Storage）幾乎不需要維運人力。自建等價的 pipeline（Fluent Bit → Kafka → Elasticsearch / BigQuery）需要維運 Kafka cluster、Elasticsearch cluster、Fluent Bit DaemonSet 的升級與監控。</p>
<p>判斷分水嶺的兩個維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>偏向 managed（Cloud Logging）</th>
          <th>偏向自建</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Log volume</td>
          <td>&lt; 1 TB/day</td>
          <td>&gt; 10 TB/day（SaaS ingestion 成本超過自建 compute）</td>
      </tr>
      <tr>
          <td>查詢需求</td>
          <td>Logs Insights + 偶爾 BigQuery</td>
          <td>需要 Elasticsearch 的全文搜尋 + aggregation + visualization</td>
      </tr>
  </tbody>
</table>
<p>1-10 TB/day 的灰色地帶取決於查詢模式 — 如果 Logs Insights 能滿足 90% 的查詢、BigQuery 能處理剩下 10% 的分析，不需要自建。如果團隊需要 Kibana dashboard、Elasticsearch alerting、或跨 cloud 的統一 log backend，自建可能更合理。</p>
<h3 id="healthcare-分層-retention-在-gcp-的實現">Healthcare 分層 retention 在 GCP 的實現</h3>
<p><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">Healthcare 案例</a>的核心需求是分層 retention — 不同 log 類型有不同的法規留存要求（data access audit log 要 6 年+、application operational log 要 90 天、debug log 要 7 天）。</p>
<p>在 GCP 上用三層架構實現：</p>
<p><strong>Hot 層（Cloud Logging custom bucket）</strong>：application log 保留 90 天、audit log 保留 1 年。設定 custom log bucket + retention。優點是 Logs Explorer 直接可查、延遲低。</p>
<p><strong>Warm 層（BigQuery）</strong>：audit log sink 到 BigQuery dataset，BigQuery 的 partition expiration 設 2 年。需要分析跟 correlation 時用 SQL 查。成本低於 Cloud Logging storage。</p>
<p><strong>Cold 層（Cloud Storage + Object Lifecycle）</strong>：BigQuery 的 scheduled export 或直接 Cloud Logging sink 到 GCS bucket。Object lifecycle rule 把 90 天以上的 object 轉 Nearline / Coldline / Archive class。最終刪除設定在 7 年。</p>
<p>三層各自的 access control 要獨立設定 — cold 層的 GCS bucket 只有 compliance team 有讀取權限，application team 看不到。CMEK 在三層都啟用（Cloud Logging custom bucket 的 CMEK + BigQuery dataset 的 CMEK + GCS bucket 的 CMEK），金鑰由安全團隊集中管理。</p>
<h3 id="pii-治理與-cmek">PII 治理與 CMEK</h3>
<p>Cloud Logging 中的 PII 治理有三層：</p>
<p><strong>第一層：不寫入</strong>。Application 端在 log 之前就遮罩 PII（email → <code>***@***.com</code>、credit card → last 4 digits）。這是最有效的方式，因為一旦寫入 Cloud Logging，即使後續刪除 log entry，在 deletion 前可能已經被 sink 匯出到 BigQuery / GCS。</p>
<p><strong>第二層：log 層過濾</strong>。用 exclusion filter 把含 PII 的 log field 排除（例如排除特定 jsonPayload field）。限制是 Cloud Logging 的 exclusion filter 只能排除整筆 log entry，不能 redact 單一 field。需要 field-level redaction 的話，在 OTel Collector 或 Fluentd 層做 processor 處理、再送到 Cloud Logging。</p>
<p><strong>第三層：加密</strong>。Cloud Logging 預設用 Google-managed encryption。需要自管金鑰的場景（HIPAA / PCI-DSS / 金融監管）用 CMEK（Customer-Managed Encryption Keys）。CMEK 設定在 log bucket 層 — 自訂 log bucket 可以指定 Cloud KMS key。<code>_Default</code> bucket 也可以啟用 CMEK（需要把 <code>_Default</code> bucket 的 region 從 <code>global</code> 改成特定 region）。</p>
<p>存取控制：Cloud Logging 的 IAM role 分 <code>roles/logging.viewer</code>（讀 log）、<code>roles/logging.privateLogViewer</code>（讀含 data access 的 log）、<code>roles/logging.admin</code>（管理 sink / bucket / filter）。Audit log 的存取用 <code>roles/logging.privateLogViewer</code>、不是一般的 <code>roles/logging.viewer</code>。對應 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">稽核追蹤與責任邊界</a> 的 GCP 實作。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="exclusion-filter-設太寬重要-log-被丟掉">Exclusion filter 設太寬，重要 log 被丟掉</h3>
<p><strong>觸發條件</strong>：為了降成本建立 exclusion filter，但 filter expression 太寬泛（例如排除整個 severity=INFO），連帶排除了 business-critical 的 info-level log。</p>
<p><strong>表現</strong>：事故時查不到關鍵 log、audit 證據鏈斷裂。因為 exclusion filter 在 ingestion 前執行，被排除的 log 無法回補。</p>
<p><strong>預防</strong>：exclusion filter 建立後先用 <code>gcloud logging read</code> 驗證哪些 log 會被排除。用 Logs Explorer 的 preview 功能確認 filter 不會命中關鍵 log。對 audit log 和 security log 不設 exclusion filter。</p>
<h3 id="bigquery-sink-匯出成本失控">BigQuery sink 匯出成本失控</h3>
<p><strong>觸發條件</strong>：org-level aggregated sink 把所有 log 送到 BigQuery，沒有 inclusion filter 限制。</p>
<p><strong>表現</strong>：BigQuery storage 跟 streaming insert 成本暴增。一個中型 GKE cluster 每天可能產生 100+ GB 的 container log，全部送 BigQuery 的月成本可能超過 Cloud Logging 本身。</p>
<p><strong>修復</strong>：在 sink 加 inclusion filter（只送 audit log 或 error-level log 到 BigQuery）。高 volume 的 application log 送 Cloud Storage（成本更低），需要查詢時用 BigQuery external table 做 federated query。</p>
<h3 id="log-entry-size-超過限制">Log entry size 超過限制</h3>
<p><strong>觸發條件</strong>：application log 寫入超過 256 KB 的單筆 log entry（Cloud Logging 的 per-entry 上限）。</p>
<p><strong>表現</strong>：超過限制的 log entry 被截斷或拒絕寫入。</p>
<p><strong>修復</strong>：application 端控制 log entry size — 大型 payload（request body / response body / stack trace）做 truncation 後再 log。需要完整內容的場景，把 payload 寫到 GCS、log 中只留 GCS URI。</p>
<h2 id="容量與成本">容量與成本</h2>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>免費額度</th>
          <th>超出後計費</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion（非 <code>_Required</code>）</td>
          <td>50 GiB/month per billing account</td>
          <td>per GiB ingested</td>
      </tr>
      <tr>
          <td>Storage（<code>_Default</code> bucket）</td>
          <td>0.5 GiB</td>
          <td>per GiB-month</td>
      </tr>
      <tr>
          <td>Storage（custom bucket）</td>
          <td>無免費額度</td>
          <td>per GiB-month</td>
      </tr>
      <tr>
          <td><code>_Required</code> log ingestion</td>
          <td>不計費</td>
          <td>不計費</td>
      </tr>
      <tr>
          <td>BigQuery sink streaming insert</td>
          <td>依 BigQuery 計費</td>
          <td>per GB inserted</td>
      </tr>
  </tbody>
</table>
<p>成本最佳化優先序：</p>
<ol>
<li><strong>Exclusion filter</strong>：攔掉不需要的 log、最直接</li>
<li><strong>降 log level</strong>：application 端把 verbose debug log 關掉</li>
<li><strong>Sampling</strong>：高 QPS 服務的 request log 做 sampling（在 application 端或 OTel Collector 層）</li>
<li><strong>BigQuery sink filter</strong>：只送需要長期分析的 log 到 BigQuery</li>
<li><strong>Cloud Storage sink</strong>：高 volume + 低查詢頻率的 log 送 GCS、按需用 BigQuery external table 查</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>：overview 與日常操作</li>
<li><a href="../cloud-monitoring-mql/">Cloud Monitoring Metrics Model 與 MQL</a>：同 vendor 的 metrics 面</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log 邊界與 PII 治理</a>：跨 vendor 的 audit log 治理策略</li>
<li><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit evidence</a>：審計證據鏈的案例回寫</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a>：長期保留的合規設計</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a>：data access audit log 的安全面</li>
</ul>
]]></content:encoded></item><item><title>CloudWatch Alarms 與 Composite Alarms 操作實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。&lt;/p>
&lt;h2 id="metric-alarm-基礎">Metric Alarm 基礎&lt;/h2>
&lt;h3 id="alarm-參數">Alarm 參數&lt;/h3>
&lt;p>每個 metric alarm 由五個參數決定行為：&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>Metric&lt;/td>
 &lt;td>要監控的 metric（namespace + metric name + dimension）&lt;/td>
 &lt;td>&lt;code>AWS/EC2 CPUUtilization InstanceId=i-xxx&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Statistic&lt;/td>
 &lt;td>聚合方式（Average / Sum / Maximum / Minimum / p99）&lt;/td>
 &lt;td>根據 metric 性質選擇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Period&lt;/td>
 &lt;td>每個 data point 的時間窗&lt;/td>
 &lt;td>60s（standard）/ 10s（high-resolution）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evaluation periods&lt;/td>
 &lt;td>連續幾個 period 超過閾值才觸發&lt;/td>
 &lt;td>3-5 個 period 減少 flapping&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Threshold&lt;/td>
 &lt;td>觸發閾值&lt;/td>
 &lt;td>跟 SLO 對齊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。&lt;/p>
&lt;h3 id="datapoints-to-alarm">Datapoints to Alarm&lt;/h3>
&lt;p>除了 evaluation periods，CloudWatch 還有 &lt;code>Datapoints to Alarm&lt;/code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 &lt;code>3 of 5&lt;/code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。&lt;/p>
&lt;p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，&lt;code>M of N&lt;/code> 模式避免因為缺失資料而延遲告警。&lt;/p>
&lt;h2 id="anomaly-detection-alarm">Anomaly Detection Alarm&lt;/h2>
&lt;h3 id="用途">用途&lt;/h3>
&lt;p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。</p>
<h2 id="metric-alarm-基礎">Metric Alarm 基礎</h2>
<h3 id="alarm-參數">Alarm 參數</h3>
<p>每個 metric alarm 由五個參數決定行為：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>說明</th>
          <th>常見設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>要監控的 metric（namespace + metric name + dimension）</td>
          <td><code>AWS/EC2 CPUUtilization InstanceId=i-xxx</code></td>
      </tr>
      <tr>
          <td>Statistic</td>
          <td>聚合方式（Average / Sum / Maximum / Minimum / p99）</td>
          <td>根據 metric 性質選擇</td>
      </tr>
      <tr>
          <td>Period</td>
          <td>每個 data point 的時間窗</td>
          <td>60s（standard）/ 10s（high-resolution）</td>
      </tr>
      <tr>
          <td>Evaluation periods</td>
          <td>連續幾個 period 超過閾值才觸發</td>
          <td>3-5 個 period 減少 flapping</td>
      </tr>
      <tr>
          <td>Threshold</td>
          <td>觸發閾值</td>
          <td>跟 SLO 對齊</td>
      </tr>
  </tbody>
</table>
<p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。</p>
<h3 id="datapoints-to-alarm">Datapoints to Alarm</h3>
<p>除了 evaluation periods，CloudWatch 還有 <code>Datapoints to Alarm</code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 <code>3 of 5</code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。</p>
<p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，<code>M of N</code> 模式避免因為缺失資料而延遲告警。</p>
<h2 id="anomaly-detection-alarm">Anomaly Detection Alarm</h2>
<h3 id="用途">用途</h3>
<p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。</p>
<h3 id="設定">設定</h3>





<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 cloudwatch put-anomaly-detector <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --namespace AWS/ApplicationELB <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --metric-name RequestCount <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --dimensions <span class="nv">Name</span><span class="o">=</span>LoadBalancer,Value<span class="o">=</span>app/my-alb/xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --stat Sum</span></span></code></pre></div><p>Anomaly Detection 需要至少兩週的歷史資料才能建立可靠 baseline。新服務上線初期先用固定閾值 alarm，等累積足夠資料後再切換。</p>
<h3 id="band-width-控制">Band width 控制</h3>
<p>Anomaly Detection band 的寬度用標準差倍數控制（預設 2）。band 太窄（1x）容易誤報，太寬（3x）漏報。生產經驗是 API latency 用 2x、batch job duration 用 3x（batch 的自然波動較大）。</p>
<h2 id="composite-alarm">Composite Alarm</h2>
<h3 id="問題alert-noise">問題：Alert noise</h3>
<p>單一 metric alarm 太多時，on-call 會收到大量相關但重複的通知。一個下游服務故障可能同時觸發 latency alarm、error rate alarm、timeout alarm、queue lag alarm — 都指向同一個根因，但各自通知。</p>
<h3 id="解法布林組合">解法：布林組合</h3>
<p>Composite Alarm 用布林表達式組合多個 alarm，只在組合條件成立時觸發。</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">ALARM(&#34;checkout-latency-high&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">AND ALARM(&#34;payment-error-rate-high&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">AND NOT ALARM(&#34;scheduled-maintenance-window&#34;)</span></span></code></pre></div><p>這個組合代表：checkout latency 高且 payment error rate 也高，但排除了計畫維護視窗 — 才通知 on-call。</p>
<h3 id="設計原則">設計原則</h3>
<p>Composite Alarm 的設計應該反映事故判讀邏輯，而非機械式組合。三個常見模式：</p>
<p><strong>Symptom + cause 組合</strong>：外部症狀（latency 高）加上內部原因（DB connection pool 飽和）同時成立才通知。避免 latency 短暫抖動就告警。</p>
<p><strong>Cross-service correlation</strong>：多個服務同時出現異常時觸發「可能是 shared dependency 問題」的 composite alarm。一個服務異常可能是部署問題，多個同時異常更可能是共用依賴（load balancer、DNS、shared database）。</p>
<p><strong>Suppression window</strong>：用 maintenance window alarm 做 NOT 條件，在計畫維護期間抑制告警。</p>
<h3 id="限制">限制</h3>
<ul>
<li>Composite Alarm 最多引用 5 個 child alarm</li>
<li>巢狀深度最多 1 層（composite 不能引用另一個 composite）</li>
<li>Composite Alarm 本身不產生 metric，只做觸發邏輯</li>
</ul>
<p>超過 5 個 child alarm 時，需要把相關 alarm 先組成一個 composite，再讓上層 composite 引用。但因為不支援巢狀，實際能組合的 alarm 數量有限。複雜告警邏輯需要用 EventBridge rule 搭配 Lambda 處理。</p>
<h2 id="alarm-actions">Alarm actions</h2>
<h3 id="常見-action-類型">常見 action 類型</h3>
<p>Alarm 進入 ALARM 狀態時可以觸發多種 action：</p>
<table>
  <thead>
      <tr>
          <th>Action 類型</th>
          <th>用途</th>
          <th>設定方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SNS Topic</td>
          <td>通知 on-call（email、SMS、PagerDuty integration）</td>
          <td>alarm action → SNS ARN</td>
      </tr>
      <tr>
          <td>Auto Scaling policy</td>
          <td>自動擴容</td>
          <td>alarm action → scaling policy ARN</td>
      </tr>
      <tr>
          <td>Lambda function</td>
          <td>自訂邏輯（建 ticket、關閉服務、修改 config）</td>
          <td>alarm action → Lambda ARN（透過 SNS）</td>
      </tr>
      <tr>
          <td>Systems Manager runbook</td>
          <td>自動執行 remediation runbook</td>
          <td>alarm action → SSM automation ARN</td>
      </tr>
      <tr>
          <td>EC2 action</td>
          <td>停止 / 重啟 / 終止 instance</td>
          <td>alarm action → EC2 action（僅限 EC2 metric）</td>
      </tr>
  </tbody>
</table>
<p>生產環境通常同時設定 ALARM 跟 OK action — ALARM 時通知 on-call，回到 OK 時自動 resolve incident。忘記設 OK action 會造成 on-call 收到告警但不知道何時恢復。</p>
<h3 id="跟-eventbridge-整合">跟 EventBridge 整合</h3>
<p>CloudWatch Alarm 狀態變更會自動送到 EventBridge（事件類型 <code>CloudWatch Alarm State Change</code>）。EventBridge rule 可以做更靈活的路由：</p>
<ul>
<li>根據 alarm name pattern 路由到不同 SNS topic</li>
<li>根據 alarm description 中的 severity tag 決定通知管道</li>
<li>多個 alarm 同時進入 ALARM 時觸發 incident 建立</li>
</ul>
<p>EventBridge 的路由能力彌補了 CloudWatch Alarm 本身路由邏輯簡單的限制。</p>
<h2 id="missing-data-處理">Missing data 處理</h2>
<h3 id="四種策略">四種策略</h3>
<p>Alarm evaluation 遇到缺失 datapoint 時，有四種處理方式：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>行為</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>missing</code></td>
          <td>維持上一個狀態</td>
          <td>多數場景的預設選擇</td>
      </tr>
      <tr>
          <td><code>breaching</code></td>
          <td>視為超過閾值</td>
          <td>metric 消失本身就是問題（heartbeat metric）</td>
      </tr>
      <tr>
          <td><code>notBreaching</code></td>
          <td>視為正常</td>
          <td>metric 在低流量時段自然消失</td>
      </tr>
      <tr>
          <td><code>ignore</code></td>
          <td>跳過該 period</td>
          <td>不影響 evaluation window</td>
      </tr>
  </tbody>
</table>
<p><code>breaching</code> 適合 heartbeat 類型的 metric — 服務應該持續回報 metric，停止回報代表服務掛了。<code>notBreaching</code> 適合流量驅動的 metric — 凌晨沒有 request 時自然沒有 latency datapoint，不應該觸發告警。</p>
<p>選錯 missing data 策略是 alarm flapping 的常見原因。Lambda function 的 metric 在沒有 invocation 時沒有 datapoint，用預設的 <code>missing</code> 或 <code>breaching</code> 都會造成問題。Lambda metric alarm 應該用 <code>notBreaching</code>。</p>
<h2 id="cross-region-限制">Cross-region 限制</h2>
<p>CloudWatch Alarm 跟 metric 綁定在同一個 region。跨 region 告警的兩種方式：</p>
<p><strong>Cross-account observability</strong>：monitoring account 可以看到 source account 的 CloudWatch 資料，但 alarm 仍然必須建在 metric 所在的 region。</p>
<p><strong>Custom metric replication</strong>：用 Lambda 或 Kinesis 把 metric 從 source region publish 到 central region，在 central region 建立統一 alarm。增加複雜度跟延遲，但能集中管理告警。</p>
<p>多數團隊選擇在每個 region 建各自的 alarm，用統一的 SNS topic（跨 region publish 到 central topic）收斂通知。告警邏輯去中心化，通知管道集中化。</p>
<h2 id="cost-考量">Cost 考量</h2>
<p>CloudWatch Alarm 的主要成本來自：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>計費方式</th>
          <th>常見數量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standard resolution alarm</td>
          <td>每 alarm / month</td>
          <td>多數服務 10-50 個 alarm</td>
      </tr>
      <tr>
          <td>High-resolution alarm（10s）</td>
          <td>每 alarm / month（3 倍 standard）</td>
          <td>只用在關鍵 SLI</td>
      </tr>
      <tr>
          <td>Anomaly Detection alarm</td>
          <td>每 alarm / month（含 ML 模型）</td>
          <td>比 standard 貴約 2-3 倍</td>
      </tr>
      <tr>
          <td>Composite Alarm</td>
          <td>免費</td>
          <td>只算 child alarm</td>
      </tr>
  </tbody>
</table>
<p>數量控制的判準：每個服務 10-30 個 metric alarm 加 2-5 個 composite alarm 是合理範圍。超過 100 個 alarm 時先檢查是否有冗餘（同一 metric 不同 period 的重複 alarm）。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>告警設計原則：alarm 跟 dashboard 的搭配，見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 Dashboard 與 Alert 設計</a></li>
<li>SLI/SLO 對齊：把 alarm 閾值跟 SLO 對齊，見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI 量測與 SLO 訊號設計</a></li>
<li>Log-based alerting：從 log 產生 metric 再建 alarm，見 <a href="../logs-insights-governance/">CloudWatch Logs Insights 查詢與日誌治理</a></li>
<li>事故響應整合：alarm → EventBridge → PagerDuty / incident tool，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>DragonflyDB → Redis / Valkey：回退到標準生態的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。反向路徑見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a>。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（RESP 協定相容），但 HA 和持久化有差異需要處理。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-dragonflydb-遷回">為什麼從 DragonflyDB 遷回&lt;/h2>
&lt;p>DragonflyDB 遷回 Redis/Valkey 的 driver 跟正向遷移互為鏡像：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Redis Modules 需求&lt;/strong>：業務開始需要 RedisJSON、RediSearch 或 RedisTimeSeries，DragonflyDB 不支援 Redis Modules 生態&lt;/li>
&lt;li>&lt;strong>Cluster mode 需求&lt;/strong>：DragonflyDB 設計為單機 scale-up，當資料量超過單機記憶體上限（數 TB）或需要跨 node sharding 時，Redis Cluster 或 Valkey Cluster 是成熟選擇&lt;/li>
&lt;li>&lt;strong>Sentinel / HA 生態&lt;/strong>：DragonflyDB 的 HA 用自家 replication，不支援 Sentinel。若團隊已有 Sentinel 或 Operator 基礎設施，回到 Redis/Valkey 整合成本更低&lt;/li>
&lt;li>&lt;strong>BSL 授權疑慮&lt;/strong>：DragonflyDB 是 BSL 1.1（4 年後轉 Apache 2.0），部分組織偏好 BSD（Valkey）或即使是 RSALv2（Redis）的已知授權&lt;/li>
&lt;/ul>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>RESP 相容、data types 一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>DragonflyDB replication → Sentinel/Cluster；snapshotting → RDB+AOF&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同（key-value cache）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>DragonflyDB 1-2 nodes → Redis primary + replica + Sentinel（或 Cluster 6 nodes）&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client config 微調（無 API 差異）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>DragonflyDB snapshot → Redis RDB 相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>全域 Low-Medium → &lt;strong>Type B drop-in&lt;/strong>，工作重心在 HA 架構切換和持久化模式對齊。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。反向路徑見 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（RESP 協定相容），但 HA 和持久化有差異需要處理。</p></blockquote>
<h2 id="為什麼從-dragonflydb-遷回">為什麼從 DragonflyDB 遷回</h2>
<p>DragonflyDB 遷回 Redis/Valkey 的 driver 跟正向遷移互為鏡像：</p>
<ul>
<li><strong>Redis Modules 需求</strong>：業務開始需要 RedisJSON、RediSearch 或 RedisTimeSeries，DragonflyDB 不支援 Redis Modules 生態</li>
<li><strong>Cluster mode 需求</strong>：DragonflyDB 設計為單機 scale-up，當資料量超過單機記憶體上限（數 TB）或需要跨 node sharding 時，Redis Cluster 或 Valkey Cluster 是成熟選擇</li>
<li><strong>Sentinel / HA 生態</strong>：DragonflyDB 的 HA 用自家 replication，不支援 Sentinel。若團隊已有 Sentinel 或 Operator 基礎設施，回到 Redis/Valkey 整合成本更低</li>
<li><strong>BSL 授權疑慮</strong>：DragonflyDB 是 BSL 1.1（4 年後轉 Apache 2.0），部分組織偏好 BSD（Valkey）或即使是 RSALv2（Redis）的已知授權</li>
</ul>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>RESP 相容、data types 一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>DragonflyDB replication → Sentinel/Cluster；snapshotting → RDB+AOF</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同（key-value cache）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>DragonflyDB 1-2 nodes → Redis primary + replica + Sentinel（或 Cluster 6 nodes）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client config 微調（無 API 差異）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>DragonflyDB snapshot → Redis RDB 相容</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>全域 Low-Medium → <strong>Type B drop-in</strong>，工作重心在 HA 架構切換和持久化模式對齊。</p>
<h2 id="相容性確認">相容性確認</h2>
<p>DragonflyDB → Redis 的相容方向跟 Redis → DragonflyDB 相反 — Redis 是 superset，回到 Redis 不會有功能缺失。但有幾個操作面差異需要處理：</p>
<table>
  <thead>
      <tr>
          <th>DragonflyDB 行為</th>
          <th>Redis 行為</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-threaded 吞吐量</td>
          <td>單主線程（I/O threads 輔助）</td>
          <td>回到 Redis 後 throughput 下降是預期行為；若單機不夠需要 Cluster 分片</td>
      </tr>
      <tr>
          <td>Fork-less snapshot</td>
          <td>BGSAVE fork + COW</td>
          <td>關注 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a>，大 dataset 的 fork 會造成延遲 spike</td>
      </tr>
      <tr>
          <td>自家 replication</td>
          <td>Redis replication + Sentinel 或 Cluster</td>
          <td>需要重建 HA 架構，見下方階段二</td>
      </tr>
      <tr>
          <td>無 AOF</td>
          <td>AOF + RDB 混合持久化</td>
          <td>依需求決定是否開 AOF；純 cache 場景可只用 RDB</td>
      </tr>
      <tr>
          <td>無 Cluster mode</td>
          <td>Redis Cluster 或 Valkey Cluster</td>
          <td>資料量大時需要規劃 sharding</td>
      </tr>
  </tbody>
</table>
<h2 id="階段一資料匯出">階段一：資料匯出</h2>
<p>DragonflyDB 支援 <code>SAVE</code> / <code>BGSAVE</code> 產生 RDB 格式 snapshot，跟 Redis RDB 相容。</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"># 在 DragonflyDB 觸發 snapshot</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli -h dragonfly-host BGSAVE
</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"># 等 BGSAVE 完成</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli -h dragonfly-host LASTSAVE
</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="c1"># 複製 snapshot 檔案到 Redis 資料目錄</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">cp /dragonfly-data/dump.rdb /redis-data/dump.rdb</span></span></code></pre></div><p>RDB 載入驗證：</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"># 啟動 Redis 載入 RDB</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-server --dbfilename dump.rdb --dir /redis-data
</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"># 驗證 key count</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli DBSIZE</span></span></code></pre></div><p>若 DragonflyDB 跑的是較新版本產出的 RDB，先在測試環境驗證 Redis 能正常載入。DragonflyDB 的 RDB 基於 Redis 6.x 格式，Redis 7.x 和 Valkey 8.x 向下相容無問題。</p>
<h2 id="階段二ha-架構重建">階段二：HA 架構重建</h2>
<p>DragonflyDB 回到 Redis/Valkey 後，HA 需要從 DragonflyDB replication 切換到 Sentinel 或 Cluster。</p>
<h3 id="sentinel-路徑適合非分片場景">Sentinel 路徑（適合非分片場景）</h3>
<p>1 primary + N replica + 3 Sentinel nodes。配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>。</p>
<h3 id="cluster-路徑適合需要分片的場景">Cluster 路徑（適合需要分片的場景）</h3>
<p>最小 3 primary + 3 replica。配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Resharding</a>。</p>
<p>選擇依據：資料量 &lt; 單機記憶體的 70% 用 Sentinel，需要水平擴展用 Cluster。</p>
<h2 id="階段三client-切換">階段三：Client 切換</h2>
<p>Application 的 Redis client 不需要改 API — DragonflyDB 跟 Redis 用同一套 RESP 協定。需要改的只有：</p>
<ol>
<li><strong>Endpoint</strong>：從 DragonflyDB host:port 改為 Redis primary（或 Sentinel/Cluster endpoint）</li>
<li><strong>認證</strong>：若 DragonflyDB 用 <code>requirepass</code>，Redis 同參數；若要升級到 ACL 趁此機會配置</li>
<li><strong>Sentinel/Cluster 配置</strong>：client library 需要啟用 Sentinel discovery 或 Cluster mode</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切換前：直連 DragonflyDB</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s2">&#34;dragonfly-host&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s2">&#34;secret&#34;</span><span class="p">)</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="c1"># 切換後：Sentinel 模式</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">sentinel</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Sentinel</span><span class="p">([(</span><span class="s2">&#34;sentinel-1&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;sentinel-2&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;sentinel-3&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">)])</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">master_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span></span></span></code></pre></div><h2 id="階段四效能-baseline-與回退">階段四：效能 baseline 與回退</h2>
<h3 id="效能預期">效能預期</h3>
<p>回到 Redis 後，單機 throughput 會低於 DragonflyDB（Redis 單主線程 vs DragonflyDB 多線程）。建立 baseline 時要跟 Redis 的歷史數據比，不是跟 DragonflyDB 比。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>預期變化</th>
          <th>應對</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐量</td>
          <td>下降（單線程限制）</td>
          <td>Cluster 分片或 read replica 分散</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>BGSAVE 期間可能有 spike</td>
          <td>調整 BGSAVE 排程避開高峰</td>
      </tr>
      <tr>
          <td>記憶體使用</td>
          <td>上升 ~30%（Redis 記憶體效率較低）</td>
          <td>預先調整 maxmemory 和 eviction policy</td>
      </tr>
  </tbody>
</table>
<h3 id="回退路徑">回退路徑</h3>
<p>回退到 DragonflyDB：把 Redis 的 RDB dump 回 DragonflyDB 載入，endpoint 改回。Cache 資料可重建，即使 RDB 不搬，DragonflyDB 重啟後 cache miss 回源到 DB 即可。</p>
<p>DragonflyDB 在遷移完成後保留 7 天再下線。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>反向路徑：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>HA 重建：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster Resharding</a></li>
<li>持久化注意：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Persistence Fork Latency</a></li>
</ul>
]]></content:encoded></item><item><title>KeyDB → Redis / Valkey：從多線程 fork 回歸主線的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（KeyDB 是 Redis fork、RESP 相容、RDB/AOF 相容），但 active-active replication 跟 multi-threading 特性回退需要額外處理。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-keydb-遷回">為什麼從 KeyDB 遷回&lt;/h2>
&lt;p>KeyDB 是 Snap 維護的 Redis fork，主要差異化在多線程和 active-active replication。遷回的 driver：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>維護活躍度疑慮&lt;/strong>：KeyDB 的 release cadence 跟 Redis/Valkey 主線比較慢，部分組織擔心長期維護與安全 patch 的及時性&lt;/li>
&lt;li>&lt;strong>Valkey 生態收斂&lt;/strong>：Valkey 在 Linux Foundation 治理下快速演進（8.x 多線程改進），KeyDB 的多線程優勢逐漸縮小&lt;/li>
&lt;li>&lt;strong>Active-active 不再需要&lt;/strong>：業務不再需要跨 region active-active、或改用 application 層處理衝突解析&lt;/li>
&lt;li>&lt;strong>社群與工具生態&lt;/strong>：Redis/Valkey 的 client library、monitoring exporter、Operator 支援度更廣&lt;/li>
&lt;/ul>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>完全相容（fork 自 Redis 6.x）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>active-active → Sentinel/Cluster；multi-thread config 移除&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>相近（1 primary + N replica + HA）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client config 微調&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB/AOF 完全相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type B drop-in，工作重心在 active-active replication 拆除和效能 baseline 對齊。&lt;/p>
&lt;h2 id="keydb-特有功能的處理">KeyDB 特有功能的處理&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>KeyDB 特有功能&lt;/th>
 &lt;th>Redis/Valkey 對應&lt;/th>
 &lt;th>遷移處理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-threading（&lt;code>server-threads&lt;/code>）&lt;/td>
 &lt;td>Redis I/O threads / Valkey 8 async I/O&lt;/td>
 &lt;td>回到 Redis 後吞吐量下降是預期，需要 benchmark 建立新 baseline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Active-active replication&lt;/td>
 &lt;td>無原生等價。Redis 需要 application 層解衝突或用 CRDTs（社群方案）&lt;/td>
 &lt;td>遷移前確認業務是否仍需 multi-master。不需要則直接切 Sentinel/Cluster&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>FLASH storage（&lt;code>storage-provider flash&lt;/code>）&lt;/td>
 &lt;td>無原生等價。Redis 純記憶體&lt;/td>
 &lt;td>遷移前把 FLASH 資料回收到記憶體，或接受遷移後記憶體需求上升。調整 &lt;code>maxmemory&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subkey expires&lt;/td>
 &lt;td>Redis 無 subkey expire（只有 top-level key TTL）&lt;/td>
 &lt;td>檢查 application 是否依賴 subkey expire；若有需要改寫為 top-level key 或用 sorted set 模擬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>EXPIREMEMBER&lt;/code> 命令&lt;/td>
 &lt;td>Redis 無此命令&lt;/td>
 &lt;td>grep application code 確認未使用；若有需改寫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>FLASH storage 的處理取決於冷資料比例。如果多數資料在 FLASH 上（用 &lt;code>OBJECT FREQ&lt;/code> 確認），遷移後的 Redis 記憶體需求會大幅上升 — 要提前計算純記憶體所需容量，調整 instance 規格或改用更積極的 eviction policy。Subkey expires 和 &lt;code>EXPIREMEMBER&lt;/code> 的影響範圍通常較小，但一旦 application 依賴就需要重構資料結構（用 top-level key + TTL 或 sorted set 模擬過期）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（KeyDB 是 Redis fork、RESP 相容、RDB/AOF 相容），但 active-active replication 跟 multi-threading 特性回退需要額外處理。</p></blockquote>
<h2 id="為什麼從-keydb-遷回">為什麼從 KeyDB 遷回</h2>
<p>KeyDB 是 Snap 維護的 Redis fork，主要差異化在多線程和 active-active replication。遷回的 driver：</p>
<ul>
<li><strong>維護活躍度疑慮</strong>：KeyDB 的 release cadence 跟 Redis/Valkey 主線比較慢，部分組織擔心長期維護與安全 patch 的及時性</li>
<li><strong>Valkey 生態收斂</strong>：Valkey 在 Linux Foundation 治理下快速演進（8.x 多線程改進），KeyDB 的多線程優勢逐漸縮小</li>
<li><strong>Active-active 不再需要</strong>：業務不再需要跨 region active-active、或改用 application 層處理衝突解析</li>
<li><strong>社群與工具生態</strong>：Redis/Valkey 的 client library、monitoring exporter、Operator 支援度更廣</li>
</ul>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>完全相容（fork 自 Redis 6.x）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>active-active → Sentinel/Cluster；multi-thread config 移除</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>相近（1 primary + N replica + HA）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client config 微調</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB/AOF 完全相容</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Type B drop-in，工作重心在 active-active replication 拆除和效能 baseline 對齊。</p>
<h2 id="keydb-特有功能的處理">KeyDB 特有功能的處理</h2>
<table>
  <thead>
      <tr>
          <th>KeyDB 特有功能</th>
          <th>Redis/Valkey 對應</th>
          <th>遷移處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-threading（<code>server-threads</code>）</td>
          <td>Redis I/O threads / Valkey 8 async I/O</td>
          <td>回到 Redis 後吞吐量下降是預期，需要 benchmark 建立新 baseline</td>
      </tr>
      <tr>
          <td>Active-active replication</td>
          <td>無原生等價。Redis 需要 application 層解衝突或用 CRDTs（社群方案）</td>
          <td>遷移前確認業務是否仍需 multi-master。不需要則直接切 Sentinel/Cluster</td>
      </tr>
      <tr>
          <td>FLASH storage（<code>storage-provider flash</code>）</td>
          <td>無原生等價。Redis 純記憶體</td>
          <td>遷移前把 FLASH 資料回收到記憶體，或接受遷移後記憶體需求上升。調整 <code>maxmemory</code></td>
      </tr>
      <tr>
          <td>Subkey expires</td>
          <td>Redis 無 subkey expire（只有 top-level key TTL）</td>
          <td>檢查 application 是否依賴 subkey expire；若有需要改寫為 top-level key 或用 sorted set 模擬</td>
      </tr>
      <tr>
          <td><code>EXPIREMEMBER</code> 命令</td>
          <td>Redis 無此命令</td>
          <td>grep application code 確認未使用；若有需改寫</td>
      </tr>
  </tbody>
</table>
<p>FLASH storage 的處理取決於冷資料比例。如果多數資料在 FLASH 上（用 <code>OBJECT FREQ</code> 確認），遷移後的 Redis 記憶體需求會大幅上升 — 要提前計算純記憶體所需容量，調整 instance 規格或改用更積極的 eviction policy。Subkey expires 和 <code>EXPIREMEMBER</code> 的影響範圍通常較小，但一旦 application 依賴就需要重構資料結構（用 top-level key + TTL 或 sorted set 模擬過期）。</p>
<h3 id="active-active-拆除">Active-active 拆除</h3>
<p>若 KeyDB 的 active-active replication 正在使用，遷移前需要先收斂為單主寫入：</p>
<ol>
<li>選定一個 region 的 KeyDB 為 primary，其他 region 停止寫入</li>
<li>等資料同步完成（replica 追上 primary offset）</li>
<li>從 primary 做 RDB export</li>
<li>用 RDB 建立 Redis/Valkey instance</li>
<li>各 region 的 application 切到新的 Redis/Valkey（Sentinel 或 Cluster）</li>
</ol>
<h2 id="資料搬遷">資料搬遷</h2>
<p>KeyDB 的 RDB 和 AOF 與 Redis 格式相容，搬遷流程跟 DragonflyDB 回退類似：</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"># KeyDB 端觸發 BGSAVE</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli -h keydb-host BGSAVE
</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"># 複製 RDB 到 Redis/Valkey 資料目錄</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">scp keydb-host:/data/dump.rdb redis-host:/data/dump.rdb
</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="c1"># Redis/Valkey 載入</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">redis-server --dbfilename dump.rdb --dir /data</span></span></code></pre></div><p>如果使用了 FLASH storage，RDB 只包含記憶體中的資料。FLASH 上的冷資料需要先用 <code>OBJECT FREQ</code> 確認存取頻率，決定是要 warm up 到記憶體再 export，還是接受遷移後冷資料 cache miss 回源。</p>
<h2 id="效能差異預期">效能差異預期</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>KeyDB → Redis 變化</th>
          <th>應對</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐量</td>
          <td>下降（KeyDB multi-thread → Redis single-thread）</td>
          <td>評估是否需要 Cluster 分片補償。Valkey 8 的 async I/O 可部分彌補</td>
      </tr>
      <tr>
          <td>記憶體</td>
          <td>上升（若使用了 FLASH storage 被移除）</td>
          <td>提前計算純記憶體所需容量，調整 instance 規格</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>BGSAVE fork spike 可能出現</td>
          <td>KeyDB 的多線程降低了 fork 影響，回到 Redis 需要關注 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a></td>
      </tr>
      <tr>
          <td>Active-active latency</td>
          <td>不適用（已拆除）</td>
          <td>N/A</td>
      </tr>
  </tbody>
</table>
<h2 id="回退路徑">回退路徑</h2>
<p>Cache 資料可重建，回退方式：</p>
<ol>
<li>Application endpoint 改回 KeyDB</li>
<li>若 KeyDB 已下線，重啟 KeyDB 載入 Redis 的 RDB（格式相容）</li>
<li>Cache miss 回源到 DB 自然 warm up</li>
</ol>
<p>KeyDB 保留 7 天再下線。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a>、<a href="/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/" data-link-title="KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入" data-link-desc="KeyDB 的 active-active 讓兩個 master 都能寫、互相同步，聽起來解決了跨區寫入的所有問題——直到兩邊同時寫同一個 key，last-write-wins 默默丟掉其中一筆。本文展開 active-active 的複製機制與衝突語意、實機驗證雙向同步、5 個把多主複製寫成資料遺失與迴圈的 production 踩坑，以及哪些資料能放 active-active、哪些不能的邊界">KeyDB Active-Active Replication</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>HA 重建：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a></li>
<li>效能參考：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Persistence Fork Latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Connection Pipeline Latency</a></li>
</ul>
]]></content:encoded></item><item><title>PromQL 與 Recording Rules 實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：&lt;/p>
&lt;p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。&lt;/p>
&lt;p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。&lt;/p>
&lt;p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異&lt;/h3>
&lt;p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。&lt;/p>
&lt;p>查詢 counter 必須用 &lt;code>rate()&lt;/code> 或 &lt;code>increase()&lt;/code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。&lt;code>rate()&lt;/code> 回傳每秒平均增量，&lt;code>increase()&lt;/code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。&lt;/p>
&lt;p>查詢 gauge 直接讀原始值即可，用 &lt;code>avg_over_time()&lt;/code>、&lt;code>max_over_time()&lt;/code> 等做區間統計。&lt;/p>
&lt;p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。&lt;/p>
&lt;h3 id="rate-與-increase-的差異">rate 與 increase 的差異&lt;/h3>
&lt;p>&lt;code>rate(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的平均每秒 request 數。&lt;code>increase(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的總增量，等於 &lt;code>rate() * 300&lt;/code>。&lt;/p>
&lt;p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。&lt;/p>
&lt;p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 &lt;code>rate(...[30s])&lt;/code> 是最小可用 range；&lt;code>rate(...[15s])&lt;/code> 可能只抓到一個 sample，回傳 NaN。production 常用 &lt;code>[5m]&lt;/code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：</p>
<p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。</p>
<p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。</p>
<p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異</h3>
<p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。</p>
<p>查詢 counter 必須用 <code>rate()</code> 或 <code>increase()</code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。<code>rate()</code> 回傳每秒平均增量，<code>increase()</code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。</p>
<p>查詢 gauge 直接讀原始值即可，用 <code>avg_over_time()</code>、<code>max_over_time()</code> 等做區間統計。</p>
<p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。</p>
<h3 id="rate-與-increase-的差異">rate 與 increase 的差異</h3>
<p><code>rate(http_requests_total[5m])</code> 回傳 5 分鐘內的平均每秒 request 數。<code>increase(http_requests_total[5m])</code> 回傳 5 分鐘內的總增量，等於 <code>rate() * 300</code>。</p>
<p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。</p>
<p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 <code>rate(...[30s])</code> 是最小可用 range；<code>rate(...[15s])</code> 可能只抓到一個 sample，回傳 NaN。production 常用 <code>[5m]</code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。</p>
<h3 id="histogram_quantile-的-bucket-設計">histogram_quantile 的 bucket 設計</h3>
<p>Prometheus histogram 使用預定義 bucket 邊界收集觀測值分布。<code>histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))</code> 計算 p95 延遲。</p>
<p>Bucket 邊界的設計直接影響精確度。預設 bucket（0.005, 0.01, 0.025, &hellip; 10）適合 HTTP request 延遲場景。如果服務的 p50 在 200ms 而 bucket 只有 0.1 跟 0.25 兩個相鄰邊界，p50 的計算會在 100ms-250ms 之間做線性內插，精確度受限。</p>
<p>設計 bucket 的判準：p50 和 p99 附近各要有 2-3 個相鄰 bucket，讓內插結果接近真實值。SLO 的 latency threshold 也應該落在某個 bucket 邊界上 — 例如 SLO 是 p95 &lt; 500ms，那 500ms 應該是一個 bucket 邊界。</p>
<p>每個 bucket 是一個 time series。10 個 bucket 的 histogram + 4 個 label 組合 = 40 個 series。Bucket 數量增加到 30 個時，同一個 metric 的 series 數量膨脹 3 倍。Bucket 設計要在精確度與 cardinality 之間取捨。</p>
<h3 id="label-matching-規則">Label matching 規則</h3>
<p>PromQL 的 binary operation（<code>/</code>、<code>+</code>、comparison）預設要求兩邊的 label set 完全一致才做 matching。這會在 error rate 計算時造成問題：<code>rate(http_requests_total{status=~&quot;5..&quot;}[5m])</code> 的 label set 含 status、但 <code>rate(http_requests_total[5m])</code> 的 total 不含 status。</p>
<p>解法是在分子做 aggregation 時 drop 掉 status label：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-promql" data-lang="promql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">{</span><span class="nl">status</span><span class="o">=~</span><span class="p">&#34;</span><span class="s">5..</span><span class="p">&#34;}[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="o">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span></span></span></code></pre></div><p><code>on()</code> 和 <code>ignoring()</code> 修飾符可以在不做 aggregation 的前提下控制 matching，但可讀性較差。production 推薦的做法是先用 <code>sum by()</code> 控制輸出的 label set，讓兩邊的 label 對齊。</p>
<h2 id="配置常見-sli-pattern">配置：常見 SLI Pattern</h2>
<h3 id="error-rate">Error rate</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># recording rule: 每 5 分鐘計算一次 error rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_error_rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total{status=~&#34;5..&#34;}[5m]))
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">          /
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>命名慣例 <code>level:metric:operations</code> 來自 Prometheus 官方建議：<code>job</code> 是聚合的 level、<code>http_request_error_rate</code> 是語意、<code>ratio_rate5m</code> 是操作。遵循慣例讓團隊成員看到 rule 名稱就知道它的聚合粒度與計算方式。</p>
<h3 id="latency-percentile">Latency percentile</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_duration_seconds:p95_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">          histogram_quantile(0.95,
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">            sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">          )</span></span></span></code></pre></div><p><code>le</code> label 是 histogram bucket 邊界，<code>sum by (job, le)</code> 把 instance 維度聚合掉、保留 bucket 結構。如果漏掉 <code>le</code>，<code>histogram_quantile</code> 會回傳錯誤結果。</p>
<h3 id="throughput">Throughput</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>三個 SLI — error rate、latency、throughput — 組成服務的 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">RED metrics</a>（Rate、Errors、Duration）。Recording rules 預先計算後，dashboard 只需讀三個 series。</p>
<h3 id="alerting-rule-搭配-recording-rule">Alerting rule 搭配 recording rule</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_alerts</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span>- <span class="nt">alert</span><span class="p">:</span><span class="w"> </span><span class="l">HighErrorRate</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m &gt; 0.01</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">for</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">          </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">page</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">        </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">          </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{ $labels.job }} error rate above 1% for 5 minutes&#34;</span></span></span></code></pre></div><p>Alert 表達式讀 recording rule 而非原始 metric。好處有二：alert evaluation 更快（讀預先計算的 series）、alert 表達式與 dashboard panel 使用同一組 recording rule（確保看到的數字一致）。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="series-churn-導致-absent-判斷失準">Series churn 導致 absent() 判斷失準</h3>
<p><code>absent(up{job=&quot;myapp&quot;})</code> 用來偵測 target 完全消失（沒在 scrape）。但在 K8s 環境，pod 頻繁 rolling update 會造成 series churn — 舊 pod 的 series 消失、新 pod 的 series 出現。短暫的時間窗內 <code>absent()</code> 可能誤觸。</p>
<p>修法：用 <code>absent_over_time(up{job=&quot;myapp&quot;}[5m])</code> 替代，要求整個 5 分鐘區間都沒有 series 才觸發。或用 <code>count(up{job=&quot;myapp&quot;}) == 0</code> 明確檢查 series 數量。</p>
<h3 id="recording-rules-circular-dependency">Recording rules circular dependency</h3>
<p>Rule group A 的 rule 讀 rule group B 的 recording rule、group B 又讀 group A 的結果。Prometheus 按 group name 字母序 evaluate，circular dependency 會讓一方讀到上一輪的 stale 結果。</p>
<p>預防方式：recording rules 形成 DAG（有向無環圖）。Prometheus 文件建議把 rule 分成 aggregation 層級 — 底層 group 算 raw metric 的 aggregation、上層 group 算 recording rule 的 aggregation。同一個 group 內的 rule 按宣告順序同步 evaluate。</p>
<h3 id="大-range-query-oom">大 range query OOM</h3>
<p>Dashboard panel 用 <code>rate(metric[30d])</code> 查詢 30 天 range — Prometheus 要載入 30 天的 samples 到記憶體做計算。100 萬 series × 30 天 × 15 秒 interval ≈ 1.7 億 samples per series 是不可能完成的查詢。</p>
<p>修法：長時間 range 必須用 recording rules 做 step-down aggregation。先用 <code>rate(...[5m])</code> recording rule 每 30 秒算一次、再用 <code>avg_over_time(recording_rule[30d])</code> 查詢。Recording rule 的 series 數量通常比原始 metric 少一到兩個數量級。</p>
<p>Prometheus 2.x 支援 <code>--query.max-samples</code> flag 限制單一 query 能處理的 sample 數量（預設 5000 萬），超過就回傳 error。這是 OOM 的最後防線、不是常態。</p>
<h3 id="counter-reset-導致-rate-異常">Counter reset 導致 rate 異常</h3>
<p>Process 重啟時 counter 歸零。<code>rate()</code> 和 <code>increase()</code> 自動偵測 counter reset 並補償，但有邊界條件：如果 scrape interval 內發生多次 restart（例如 crash loop），<code>rate()</code> 可能低估真實值（只能偵測到一次 reset）。</p>
<p>這種情境下的判讀：如果 <code>rate()</code> 的結果明顯低於預期、且同時段有 pod restart 紀錄，rate 低估是正常的。修法是解決 crash loop 本身、而非調整 PromQL。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<p>Recording rules 的 CPU 成本 = rule 數量 × 每條 rule 的 evaluation 時間 × (1 / evaluation interval)。</p>
<table>
  <thead>
      <tr>
          <th>Rule 數量</th>
          <th>平均 evaluation 時間</th>
          <th>Interval</th>
          <th>每秒 evaluation 消耗</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>50</td>
          <td>10ms</td>
          <td>30s</td>
          <td>50 × 0.01 / 30 = 0.017 core</td>
      </tr>
      <tr>
          <td>200</td>
          <td>50ms</td>
          <td>30s</td>
          <td>200 × 0.05 / 30 = 0.33 core</td>
      </tr>
      <tr>
          <td>500</td>
          <td>100ms</td>
          <td>15s</td>
          <td>500 × 0.1 / 15 = 3.33 core</td>
      </tr>
  </tbody>
</table>
<p>表中的 evaluation 時間是 10 萬到 50 萬 active series 規模下的經驗值。Series 數量影響 evaluation 時間 — 100 萬 series 的 complex aggregation 可能 500ms+，跟表中假設偏差很大。用 <code>prometheus_rule_group_last_duration_seconds</code> 量測自己環境的實際值。</p>
<p>500 條 complex rule 搭配 15 秒 interval 會消耗超過 3 個 CPU core 在 rule evaluation 上。這時候的修法方向有三：</p>
<ul>
<li>把 evaluation interval 放寬到 30s 或 60s（犧牲即時性）</li>
<li>把 rule 表達式最佳化（減少 aggregation 層數）</li>
<li>把 rule evaluation 卸載到 Mimir ruler（水平擴展）</li>
</ul>
<p>Recording rules 產生的新 series 也會增加 cardinality。200 條 recording rule × 平均 5 個 label 組合 = 1000 個新 series，通常可接受。但如果 recording rule 沒做 aggregation 而是直接 alias（<code>record: new_name expr: old_metric</code>），cardinality 不會減少，只增加了寫入成本。</p>
<p>判讀指標：<code>prometheus_rule_group_last_duration_seconds</code> 跟 <code>prometheus_rule_group_interval_seconds</code> 的比值。前者超過後者時，evaluation 跑不完、dashboard 跟 alert 都會延遲。見 <a href="../capacity-failure-modes/">容量規劃與故障模式</a> 的 Recording rule evaluation lag 段。</p>
<h3 id="recording-rules-作為成本控制工具">Recording rules 作為成本控制工具</h3>
<p><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">觀測成本治理案例</a>提出一個被低估的用法：recording rules 不只是加速查詢、也是控制 remote write 成本的手段。</p>
<p>模式是這樣的：application 暴露 200 個 label 組合的原始 metric（per-endpoint × per-status × per-region），recording rule 聚合成 5 個 label 組合（per-service × per-region）。如果 remote write 設定了 <code>write_relabel_configs</code> drop 掉原始 series、只 forward recording rule 產生的 aggregated series，remote write bandwidth 跟長期儲存的 cardinality 都大幅降低。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Step 1: recording rule 做 aggregation</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cost_optimized</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">service_region:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (service, region) (rate(http_requests_total[5m]))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c"># Step 2: remote write 只送 aggregated series</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service_region:.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">keep</span></span></span></code></pre></div><p>這個模式的取捨：長期儲存只有 aggregated 資料、無法回溯到原始 per-endpoint 維度。如果事故時需要 per-endpoint 的歷史資料，要麼保留原始 series 在本地 Prometheus（短期 retention）、要麼接受長期儲存只有 aggregated 粒度。</p>
<p>適用場景判斷：如果 dashboard 跟 alert 都只看 service-level 聚合、per-endpoint 維度只在即時除錯時才需要（Prometheus 本地 15 天 retention 夠用），這個模式的成本節省值得。如果有合規需求要 per-endpoint 歷史資料（例如 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">FinTech 案例</a> 的 evidence chain），就不能 drop 原始 series。</p>
<h3 id="evaluation-interval-對-cpu-的影響">Evaluation interval 對 CPU 的影響</h3>
<p>Rule group 的 <code>interval</code> 決定 evaluation 頻率。同一組 rules 從 30s interval 改成 15s interval，CPU 消耗翻倍。從 30s 改成 60s，CPU 減半但 alert 跟 dashboard 的即時性下降。</p>
<p>經驗值：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 interval</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLI / SLO recording rules</td>
          <td>30s</td>
          <td>平衡即時性跟成本、多數 burn rate alert 的最小 window 是 5 分鐘</td>
      </tr>
      <tr>
          <td>Capacity trending rules</td>
          <td>60s-120s</td>
          <td>趨勢不需要秒級即時性</td>
      </tr>
      <tr>
          <td>High-frequency operational rules</td>
          <td>15s</td>
          <td>需要跟 scrape interval 對齊的場景（例如 real-time anomaly detection）</td>
      </tr>
  </tbody>
</table>
<p>15 秒 interval 的 rule group 要特別注意 evaluation 時間 — 如果 evaluation 本身花 12 秒，只剩 3 秒 buffer。<code>prometheus_rule_group_last_duration_seconds</code> 持續接近 <code>prometheus_rule_group_interval_seconds</code> 時，要麼拆 rule group 到不同 Prometheus instance、要麼放寬 interval。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="alertmanager">Alertmanager</h3>
<p>Alert rule 寫在 Prometheus 的 <code>rule_files</code> 內、觸發後送到 Alertmanager。Alertmanager 負責去重、分組、抑制與路由（route to PagerDuty / Slack / email）。Alert rule 的表達式跟 recording rule 共用同一組語意 — 讀 recording rule 而非原始 metric。</p>
<h3 id="grafana-dashboard">Grafana dashboard</h3>
<p>Grafana 的 Prometheus datasource 直接查 PromQL。Dashboard panel 推薦讀 recording rule series 而非寫 raw PromQL — 減少 dashboard 載入時間、確保 dashboard 跟 alert 看到的數字一致。</p>
<h3 id="對齊-slislo">對齊 SLI/SLO</h3>
<p>Recording rules 產生的 SLI metrics 是 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a> 的資料來源。SLO burn rate alert 也讀同一組 recording rule。確保 SLI recording rule 的 time window 跟 SLO window 對齊（例如 SLO 用 30 天 rolling window，recording rule 至少提供 5m 和 1h 兩個 aggregation 粒度給 burn rate 計算）。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：recording rules 成長後的資源衝擊</li>
<li><a href="../remote-write-long-term-storage/">Remote Write 與長期儲存整合</a>：recording rule 在 remote write 架構下的部署選擇</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>：recording rules 如何餵給 SLO burn rate</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>：recording rules 作為 cardinality 減量手段</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rules 在 pre-aggregation 與 query tiering 中的定位</li>
</ul>
]]></content:encoded></item><item><title>Sentry Release Tracking 與 Session Replay</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/release-tracking-session-replay/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/release-tracking-session-replay/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 的 vendor deep article，深化 overview「Release / source map」跟「Session Replay」段。初次接觸 Sentry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Release tracking 讓 Sentry 從「error 收集器」升級成「部署品質追蹤器」。每次部署標記一個 release，Sentry 自動計算 crash-free sessions、regressed errors 跟 release health。Session Replay 進一步把 error 的觸發脈絡從 stack trace 擴展到使用者操作錄影。兩者搭配使用時，團隊能看到「這個版本部署後、哪些使用者遇到什麼操作導致什麼錯誤」的完整鏈路。&lt;/p>
&lt;h2 id="release-health">Release Health&lt;/h2>
&lt;h3 id="核心概念">核心概念&lt;/h3>
&lt;p>Release health 追蹤每個版本的使用者體驗品質。核心指標：&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>Crash-free sessions&lt;/td>
 &lt;td>沒有 unhandled error 的 session 百分比&lt;/td>
 &lt;td>99.5% 以上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Crash-free users&lt;/td>
 &lt;td>沒有遇到 unhandled error 的使用者百分比&lt;/td>
 &lt;td>99.5% 以上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Adoption rate&lt;/td>
 &lt;td>使用此版本的 session 佔比&lt;/td>
 &lt;td>依 rollout 策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error count&lt;/td>
 &lt;td>此版本的 error event 數量&lt;/td>
 &lt;td>不應比前一版高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Crash-free sessions 跟 crash-free users 的差異：sessions 是頻率加權（一個使用者一天開 10 次 app，10 次都算），users 是去重的。Mobile app 通常看 crash-free users（使用者感知），web 通常看 crash-free sessions（頻率反映服務品質）。&lt;/p>
&lt;h3 id="release-標記">Release 標記&lt;/h3>
&lt;p>在 SDK 初始化時傳入 release 標記：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">sentry_sdk&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">dsn&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">release&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;checkout-api@1.2.3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">environment&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Release 命名慣例：&lt;code>&amp;lt;service&amp;gt;@&amp;lt;version&amp;gt;&lt;/code> 或 git SHA。用語意版本方便比較，用 git SHA 方便對應 commit。CI/CD pipeline 在 deploy step 自動設定。&lt;/p>
&lt;h3 id="deploy-標記">Deploy 標記&lt;/h3>
&lt;p>Release 建立後，用 Sentry CLI 或 API 標記 deploy：&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">sentry-cli releases deploys checkout-api@1.2.3 new &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --env production &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> --started &lt;span class="k">$(&lt;/span>date -u +%s&lt;span class="k">)&lt;/span> &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> --finished &lt;span class="k">$(&lt;/span>date -u +%s&lt;span class="k">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deploy 標記讓 Sentry 知道某個 release 何時部署到哪個環境。issue list 的 &amp;ldquo;First seen in release&amp;rdquo; 跟 &amp;ldquo;Regressed in release&amp;rdquo; 依賴這個資訊。&lt;/p>
&lt;h3 id="regressed-error-偵測">Regressed Error 偵測&lt;/h3>
&lt;p>Sentry 會追蹤已 resolve 的 issue。如果新 release 重新觸發了已 resolve 的 issue，Sentry 標記為 regression。這比人工追蹤有效 — 團隊不需要記住哪些 bug 修過，Sentry 自動偵測回歸。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 的 vendor deep article，深化 overview「Release / source map」跟「Session Replay」段。初次接觸 Sentry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Release tracking 讓 Sentry 從「error 收集器」升級成「部署品質追蹤器」。每次部署標記一個 release，Sentry 自動計算 crash-free sessions、regressed errors 跟 release health。Session Replay 進一步把 error 的觸發脈絡從 stack trace 擴展到使用者操作錄影。兩者搭配使用時，團隊能看到「這個版本部署後、哪些使用者遇到什麼操作導致什麼錯誤」的完整鏈路。</p>
<h2 id="release-health">Release Health</h2>
<h3 id="核心概念">核心概念</h3>
<p>Release health 追蹤每個版本的使用者體驗品質。核心指標：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>定義</th>
          <th>健康閾值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Crash-free sessions</td>
          <td>沒有 unhandled error 的 session 百分比</td>
          <td>99.5% 以上</td>
      </tr>
      <tr>
          <td>Crash-free users</td>
          <td>沒有遇到 unhandled error 的使用者百分比</td>
          <td>99.5% 以上</td>
      </tr>
      <tr>
          <td>Adoption rate</td>
          <td>使用此版本的 session 佔比</td>
          <td>依 rollout 策略</td>
      </tr>
      <tr>
          <td>Error count</td>
          <td>此版本的 error event 數量</td>
          <td>不應比前一版高</td>
      </tr>
  </tbody>
</table>
<p>Crash-free sessions 跟 crash-free users 的差異：sessions 是頻率加權（一個使用者一天開 10 次 app，10 次都算），users 是去重的。Mobile app 通常看 crash-free users（使用者感知），web 通常看 crash-free sessions（頻率反映服務品質）。</p>
<h3 id="release-標記">Release 標記</h3>
<p>在 SDK 初始化時傳入 release 標記：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">release</span><span class="o">=</span><span class="s2">&#34;checkout-api@1.2.3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">environment</span><span class="o">=</span><span class="s2">&#34;production&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>Release 命名慣例：<code>&lt;service&gt;@&lt;version&gt;</code> 或 git SHA。用語意版本方便比較，用 git SHA 方便對應 commit。CI/CD pipeline 在 deploy step 自動設定。</p>
<h3 id="deploy-標記">Deploy 標記</h3>
<p>Release 建立後，用 Sentry CLI 或 API 標記 deploy：</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">sentry-cli releases deploys checkout-api@1.2.3 new <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --env production <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --started <span class="k">$(</span>date -u +%s<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --finished <span class="k">$(</span>date -u +%s<span class="k">)</span></span></span></code></pre></div><p>Deploy 標記讓 Sentry 知道某個 release 何時部署到哪個環境。issue list 的 &ldquo;First seen in release&rdquo; 跟 &ldquo;Regressed in release&rdquo; 依賴這個資訊。</p>
<h3 id="regressed-error-偵測">Regressed Error 偵測</h3>
<p>Sentry 會追蹤已 resolve 的 issue。如果新 release 重新觸發了已 resolve 的 issue，Sentry 標記為 regression。這比人工追蹤有效 — 團隊不需要記住哪些 bug 修過，Sentry 自動偵測回歸。</p>
<p>Regression 通知的準確度取決於 grouping 品質。如果 grouping 不準（見 <a href="../error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting</a>），regression 偵測也會不準 — 不同 bug 被合成同一 issue 時，resolve 一個 bug 後另一個觸發會被誤判為 regression。</p>
<h3 id="source-map-上傳">Source map 上傳</h3>
<p>前端 minified code 的 stack trace 不可讀。上傳 source map 讓 Sentry 還原原始 source 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">sentry-cli releases files checkout-api@1.2.3 upload-sourcemaps <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --url-prefix <span class="s1">&#39;~/static/js&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  ./build/static/js</span></span></code></pre></div><p>Source map 上傳必須在 deploy 前完成，且 release 版本跟前端 build 版本一致。版本不一致時，Sentry 找不到對應的 source map，stack trace 仍然是 minified。</p>
<p>CI/CD 整合：在 build step 之後、deploy step 之前上傳 source map。多數框架（Next.js、Vite、Webpack）有 Sentry plugin 自動處理。</p>
<h2 id="session-replay">Session Replay</h2>
<h3 id="核心能力">核心能力</h3>
<p>Session Replay 錄製使用者在網頁上的操作。Sentry 記錄的是 DOM mutation 跟使用者事件的結構化資料，播放時 replay DOM 變化，效果類似影片但資料量遠小於螢幕錄影。</p>
<p>replay 跟 error 關聯：Sentry 在 error event 中附帶 replay ID，讓工程師從 issue detail 直接跳到 error 發生前後的使用者操作。</p>
<h3 id="隱私設定">隱私設定</h3>
<p>Session Replay 預設會遮罩敏感資訊：</p>
<table>
  <thead>
      <tr>
          <th>遮罩類型</th>
          <th>預設行為</th>
          <th>自訂方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>文字內容</td>
          <td>所有文字替換成 <code>*</code></td>
          <td><code>maskAllText: false</code> 關閉、或用 CSS class <code>sentry-mask</code> 指定</td>
      </tr>
      <tr>
          <td>輸入框</td>
          <td>所有 input value 遮罩</td>
          <td><code>maskAllInputs: false</code> 關閉（注意 PII 風險）</td>
      </tr>
      <tr>
          <td>圖片</td>
          <td>不遮罩（但 <code>&lt;img&gt;</code> 從原始 URL 載入）</td>
          <td><code>blockAllMedia: true</code> 遮蔽所有媒體</td>
      </tr>
      <tr>
          <td>特定元素</td>
          <td>不遮罩</td>
          <td>加 <code>data-sentry-block</code> attribute 完全隱藏</td>
      </tr>
  </tbody>
</table>
<p>PII 合規考量：</p>
<ul>
<li>預設 <code>maskAllText: true</code> + <code>maskAllInputs: true</code> 是安全起點</li>
<li>GDPR / CCPA 場景需要額外確認：replay 資料存在 Sentry SaaS（美國資料中心），跨境傳輸需要評估</li>
<li>Self-hosted Sentry 可以把 replay 資料留在自己的基礎設施</li>
</ul>
<h3 id="sampling-策略">Sampling 策略</h3>
<p>Session Replay 會增加前端 SDK 的 payload 大小跟 Sentry 的 event quota。用 sampling rate 控制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">Sentry</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">dsn</span><span class="o">:</span> <span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">replaysSessionSampleRate</span><span class="o">:</span> <span class="mf">0.1</span><span class="p">,</span>  <span class="c1">// 10% 的 session 錄影
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">replaysOnErrorSampleRate</span><span class="o">:</span> <span class="mf">1.0</span><span class="p">,</span>  <span class="c1">// error 發生時 100% 錄影
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>推薦策略：<code>replaysSessionSampleRate</code> 用低值（1-10%），<code>replaysOnErrorSampleRate</code> 用 100%。目的是確保每個 error 都有 replay 可看，但不錄所有正常 session。</p>
<p>高流量網站（每日百萬 session 以上）可能需要把 <code>replaysSessionSampleRate</code> 設到 0，只在 error 時才錄。session replay 的 quota 消耗速度可以在 Sentry Usage Stats 頁面監控。</p>
<h2 id="performance-monitoring">Performance Monitoring</h2>
<h3 id="transaction-based-tracing">Transaction-based tracing</h3>
<p>Sentry 的 performance monitoring 用 transaction / span 結構（跟 OpenTelemetry 的 trace / span 概念對齊）。每個 HTTP request、page load 或自訂操作是一個 transaction，transaction 內的子操作是 span。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_transaction</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;checkout&#34;</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&#34;POST /api/checkout&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_span</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;db&#34;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;insert order&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="c1"># DB operation</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_span</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;http&#34;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;payment gateway&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="c1"># External API call</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">pass</span></span></span></code></pre></div><p>自動 instrumentation 會自動建立 transaction 跟 span（HTTP framework、DB driver、HTTP client）。手動 span 用在自訂業務邏輯或自動 instrumentation 沒覆蓋的路徑。</p>
<h3 id="otel-context-整合">OTel context 整合</h3>
<p>Sentry SDK 支援 OTel context propagation — 如果 upstream service 用 OTel SDK 產生 trace，Sentry SDK 會接受 <code>traceparent</code> header 中的 trace_id 跟 parent_span_id，把自己的 transaction 接到同一條 trace。</p>
<p>整合方式：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sentry SDK 接收 OTel context</td>
          <td>預設支援 W3C Trace Context、不需額外設定</td>
      </tr>
      <tr>
          <td>Sentry 資料送到 OTel backend</td>
          <td>用 Sentry 的 OTel exporter（experimental）</td>
      </tr>
      <tr>
          <td>OTel SDK 送資料到 Sentry</td>
          <td>OTel SDK → OTLP exporter → Sentry（Sentry 支援 OTLP ingestion）</td>
      </tr>
  </tbody>
</table>
<p>常見架構：backend service 用 OTel SDK + Collector，frontend 用 Sentry SDK（前端 error tracking 跟 session replay 是 Sentry 的強項）。兩者透過 trace_id 關聯，在 Sentry 看 frontend error + replay，在 OTel backend 看 backend trace。</p>
<h3 id="web-vitals">Web Vitals</h3>
<p>前端 SDK 自動收集 Core Web Vitals（LCP、FID / INP、CLS）跟 TTFB。這些指標跟 error 在同一個 dashboard，讓團隊在 release 後同時看 error regression 跟效能 regression。</p>
<p>Web Vitals 的觀測不需要額外設定 — 前端 SDK 自動收集。但 sampling rate 會影響資料量 — <code>tracesSampleRate</code> 設太低時，Web Vitals 的 sample 數量可能不夠做統計比較。</p>
<h2 id="self-hosted-vs-saas">Self-hosted vs SaaS</h2>
<h3 id="決策維度">決策維度</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>SaaS（sentry.io）</th>
          <th>Self-hosted</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>維運</td>
          <td>Sentry 負責</td>
          <td>自己維運（docker-compose、20+ 容器）</td>
      </tr>
      <tr>
          <td>資料位置</td>
          <td>Sentry 資料中心（美國為主）</td>
          <td>自己的基礎設施</td>
      </tr>
      <tr>
          <td>功能完整度</td>
          <td>全功能</td>
          <td>社群版功能略少（部分企業功能不含）</td>
      </tr>
      <tr>
          <td>升級</td>
          <td>自動</td>
          <td>手動（每月有新版、升級需要停機）</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>Event-based pricing</td>
          <td>基礎設施 + 人力成本</td>
      </tr>
      <tr>
          <td>Replay / Profiling</td>
          <td>含</td>
          <td>含（但 storage 自負）</td>
      </tr>
  </tbody>
</table>
<h3 id="何時選-self-hosted">何時選 self-hosted</h3>
<p>資料必須留在特定地理區域（GDPR / 特定產業法規）、或企業 security policy 不允許 error data 送到第三方 — 這是 self-hosted 的核心理由。</p>
<p>Self-hosted Sentry 的維運成本常被低估：20+ 個容器（Kafka、ClickHouse、PostgreSQL、Redis、Snuba、Relay 等）、升級可能需要資料庫 migration、troubleshooting 時沒有 vendor 支援。中小團隊通常 SaaS 的 event pricing 比 self-hosted 的人力成本低。</p>
<h3 id="混合模式">混合模式</h3>
<p>部分團隊用混合模式：production error 送 Sentry SaaS（低維運），但 audit-sensitive 的資料（PII-heavy environment）走 self-hosted。兩套 Sentry instance 各自獨立，不共享 issue。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>Error grouping 策略：在 issue 數量失控前建立 fingerprint rule，見 <a href="../error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting</a></li>
<li>觀測證據整合：把 Sentry issue link 放進 evidence package，見 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>Client-side monitoring：Sentry 的前端 SDK 跟 RUM 的定位互補，見 <a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side Monitoring</a></li>
<li>事故響應整合：Sentry alert → PagerDuty / incident.io，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>Rate Limit 實作</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/rate-limit-implementation/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/rate-limit-implementation/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate limit&lt;/a> 的實作分成三個層次：單機 middleware（一個 server instance 內的限速）、分散式限速（多個 instance 共用的限速狀態）、配額設計（不同 client 和 endpoint 的差異化配額）。Rate limit 的概念基礎（token bucket / sliding window / 和背壓的區別）見 &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">DevOps 流量管控&lt;/a>，本章聚焦後端的程式碼實作。&lt;/p>
&lt;h2 id="單機-middleware-實作">單機 Middleware 實作&lt;/h2>
&lt;p>Rate limit middleware 在 HTTP handler 之前攔截請求。每個 request 過一次 limiter，通過就進入 handler，超限就回 429。&lt;/p>
&lt;h3 id="go-實作">Go 實作&lt;/h3>
&lt;p>Go 標準生態的 &lt;code>golang.org/x/time/rate&lt;/code> 提供 token bucket 的 &lt;code>rate.Limiter&lt;/code>。&lt;/p>





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





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





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





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





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





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





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





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Rate limit hit 時送 metric 事件</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">monitor</span><span class="p">.</span><span class="nf">Metric</span><span class="p">(</span><span class="s">&#34;ratelimit.hit&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">any</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;client_id&#34;</span><span class="p">:</span> <span class="nx">clientID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;endpoint&#34;</span><span class="p">:</span>  <span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Path</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;limit&#34;</span><span class="p">:</span>     <span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s">&#34;window&#34;</span><span class="p">:</span>    <span class="s">&#34;1m&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>Dashboard 視圖：rate limit hit 的時間趨勢 + 按 client 和 endpoint 分群。Hit 數持續上升代表配額設太低（正常使用被限速）或某個 client 在濫用。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Rate limit 的概念基礎 → <a href="/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">DevOps 流量管控 — Rate Limiting</a></li>
<li>背壓機制（被動的流量控制）→ <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制</a></li>
<li>Rate limit 知識卡 → <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a></li>
<li>監控系統中的 ingestion 限速 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Monitoring Ingestion Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/</link><pubDate>Wed, 17 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/</guid><description>&lt;p>本案例屬於 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護&lt;/a> 的選型比較。&lt;/p>
&lt;p>選型情境是&lt;strong>單人自用遠端 shell 存取&lt;/strong>：人在外、手機操作家中或辦公室本機的真實終端機（zsh）。兩個候選方案代表兩種根本不同的安全模型——「公開端點 + 多層防護」vs「私有網路 + 端點不存在」。&lt;/p>
&lt;h2 id="情境約束">情境約束&lt;/h2>
&lt;ul>
&lt;li>單人自用（owner = 開發 = 維運 = 唯一用戶）&lt;/li>
&lt;li>失敗代價高：整台機器的 shell 外洩&lt;/li>
&lt;li>手機端需自建 Flutter 終端機 UI（兩方案皆需）&lt;/li>
&lt;li>預算趨近零（免費方案）&lt;/li>
&lt;/ul>
&lt;h2 id="兩方案架構對比">兩方案架構對比&lt;/h2>
&lt;h3 id="方案-acloudflare-tunnel--cloudflare-access">方案 A：Cloudflare Tunnel + Cloudflare Access&lt;/h3>





&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">Flutter app（Face ID）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> │ WSS，帶三組憑證
&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">Cloudflare Tunnel（named，固定網域）
&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">Cloudflare Access（邊緣：驗 Service Token）── 未授權流量在此被擋
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">Go proxy（本機：驗 X-App-Tunnel-Token）── 第二道
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">ttyd（本機：basic auth）── 第三道
&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">zsh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="方案-btailscale-mesh-vpn">方案 B：Tailscale mesh VPN&lt;/h3>





&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">Flutter app（Face ID）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> │ WS，帶 ttyd basic auth
&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">Tailscale mesh VPN（WireGuard 加密隧道，裝置級認證）
&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">Go proxy（本機：稽核 log + 透明轉發，不做認證）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">ttyd（本機：basic auth）── 應用層最後防線
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">zsh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="核心選型維度">核心選型維度&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Cloudflare Tunnel + Access&lt;/th>
 &lt;th>Tailscale&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>網路模型&lt;/strong>&lt;/td>
 &lt;td>出站連線到 CF 邊緣，產生&lt;strong>公開 URL&lt;/strong>&lt;/td>
 &lt;td>&lt;a href="https://www.wireguard.com/">WireGuard&lt;/a> mesh VPN，裝置間&lt;strong>私有 IP&lt;/strong>，無公開端點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>攻擊面&lt;/strong>&lt;/td>
 &lt;td>公開 URL 存在，需層層防護&lt;/td>
 &lt;td>服務端點不存在於公開網路，攻擊者連 IP 都到不了&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>認證層數&lt;/strong>&lt;/td>
 &lt;td>三層：CF Access + proxy token + ttyd&lt;/td>
 &lt;td>兩層：Tailscale 裝置認證 + ttyd&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Go proxy 職責&lt;/strong>&lt;/td>
 &lt;td>驗 token + 稽核 log + 轉發&lt;/td>
 &lt;td>稽核 log + 轉發（不做認證）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>元件數&lt;/strong>&lt;/td>
 &lt;td>5（app → CF → CF Access → proxy → ttyd）&lt;/td>
 &lt;td>3（app → Tailscale → proxy/ttyd）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>需要自有網域&lt;/strong>&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>否（MagicDNS 自動分配）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>啟停行程數&lt;/strong>&lt;/td>
 &lt;td>3（cloudflared + ttyd + proxy）&lt;/td>
 &lt;td>2（ttyd + proxy），Tailscale daemon 常駐&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>憑證包欄位&lt;/strong>&lt;/td>
 &lt;td>8 欄（含 CF Access 憑證 + proxy token）&lt;/td>
 &lt;td>~5 欄（endpoint + ttyd 帳密）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>密鑰管理複雜度&lt;/strong>&lt;/td>
 &lt;td>高（proxy token 需可插拔後端 keychain/file/env）&lt;/td>
 &lt;td>低（僅 ttyd 帳密）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>費用&lt;/strong>&lt;/td>
 &lt;td>免費（Cloudflare 個人方案）&lt;/td>
 &lt;td>免費（Tailscale 個人方案，100 裝置內）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>外部依賴&lt;/strong>&lt;/td>
 &lt;td>Cloudflare 邊緣網路 + CF Access 控制面&lt;/td>
 &lt;td>Tailscale 協調伺服器 + DERP relay&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>供應商不可用時降級&lt;/strong>&lt;/td>
 &lt;td>邊緣不可用 = 全部連不進來；已建立連線可能存活&lt;/td>
 &lt;td>協調伺服器不可用時已建立的 WireGuard 連線存活；DERP relay 不可用只影響 NAT 穿越&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型判讀">選型判讀&lt;/h2>
&lt;h3 id="tailscale-勝出的場景本情境適用">Tailscale 勝出的場景（本情境適用）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>攻擊面最小化是首要目標&lt;/strong>：shell 閘道的失敗代價極高，「端點不存在」比「保護公開端點」本質上更安全&lt;/li>
&lt;li>&lt;strong>單人自用&lt;/strong>：不需要 CF Access 的多人 policy / IdP 整合 / Device Posture 等企業功能&lt;/li>
&lt;li>&lt;strong>架構簡單性&lt;/strong>：從 5 元件 3 層認證縮為 3 元件 2 層認證，Go proxy 職責大幅簡化（砍認證閘道，只留 log + 轉發）&lt;/li>
&lt;li>&lt;strong>密鑰管理簡化&lt;/strong>：不再需要為 proxy token 建可插拔多後端（keychain/file/env），只管 ttyd 帳密&lt;/li>
&lt;li>&lt;strong>不需要自有網域&lt;/strong>：Tailscale MagicDNS 或直接用 Tailscale IP&lt;/li>
&lt;/ul>
&lt;h3 id="cloudflare-tunnel-勝出的場景本情境不適用但值得記錄">Cloudflare Tunnel 勝出的場景（本情境不適用，但值得記錄）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>需要對外提供服務&lt;/strong>（非自用）：CF 的 WAF / CDN / rate limit / bot protection 生態豐富&lt;/li>
&lt;li>&lt;strong>需要 HTTP 層細粒度存取控制&lt;/strong>：CF Access 的 Application + Policy 模型適合管多個 internal web app&lt;/li>
&lt;li>&lt;strong>需要 Device Posture 檢查&lt;/strong>：CF 整合 CrowdStrike / SentinelOne 等 EDR 做裝置健康判斷（Device Posture：在授權前先檢查裝置的安全狀態 — 作業系統版本、磁碟加密、防毒軟體是否啟用）&lt;/li>
&lt;li>&lt;strong>已在用 Cloudflare 生態&lt;/strong>：共用控制面的管理紅利（同一 Logpush / API token / Audit Log）&lt;/li>
&lt;li>&lt;strong>多人 / 多團隊 / 合規場景&lt;/strong>：CF Access 的 IdP 整合 + Service Auth + Audit Log 比 Tailscale 個人方案完整&lt;/li>
&lt;/ul>
&lt;h3 id="邊界情境">邊界情境&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>多人但仍小規模&lt;/strong>（2-5 人）：Tailscale ACL（存取控制清單，定義哪些裝置可存取哪些服務）足以控制；超過此規模再評估 CF Access 或 Teleport&lt;/li>
&lt;li>&lt;strong>需要 session recording&lt;/strong>：兩者都沒有一流方案——Tailscale 需 Enterprise tier，CF Access 只記 metadata 不錄 keystroke。重 audit 走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a>&lt;/li>
&lt;li>&lt;strong>需要從固定 IP 出網&lt;/strong>：Tailscale Exit Node 可做但不是設計核心；CF 有更成熟的方案&lt;/li>
&lt;/ul>
&lt;h2 id="tailscale-採用後的安全底線">Tailscale 採用後的安全底線&lt;/h2>
&lt;p>即使 Tailscale 攻擊面更小，仍需維持以下底線：&lt;/p></description><content:encoded><![CDATA[<p>本案例屬於 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 的選型比較。</p>
<p>選型情境是<strong>單人自用遠端 shell 存取</strong>：人在外、手機操作家中或辦公室本機的真實終端機（zsh）。兩個候選方案代表兩種根本不同的安全模型——「公開端點 + 多層防護」vs「私有網路 + 端點不存在」。</p>
<h2 id="情境約束">情境約束</h2>
<ul>
<li>單人自用（owner = 開發 = 維運 = 唯一用戶）</li>
<li>失敗代價高：整台機器的 shell 外洩</li>
<li>手機端需自建 Flutter 終端機 UI（兩方案皆需）</li>
<li>預算趨近零（免費方案）</li>
</ul>
<h2 id="兩方案架構對比">兩方案架構對比</h2>
<h3 id="方案-acloudflare-tunnel--cloudflare-access">方案 A：Cloudflare Tunnel + Cloudflare Access</h3>





<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">Flutter app（Face ID）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   │  WSS，帶三組憑證
</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">Cloudflare Tunnel（named，固定網域）
</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">Cloudflare Access（邊緣：驗 Service Token）── 未授權流量在此被擋
</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">Go proxy（本機：驗 X-App-Tunnel-Token）── 第二道
</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">ttyd（本機：basic auth）── 第三道
</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">zsh</span></span></code></pre></div><h3 id="方案-btailscale-mesh-vpn">方案 B：Tailscale mesh VPN</h3>





<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">Flutter app（Face ID）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   │  WS，帶 ttyd basic auth
</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">Tailscale mesh VPN（WireGuard 加密隧道，裝置級認證）
</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">Go proxy（本機：稽核 log + 透明轉發，不做認證）
</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">ttyd（本機：basic auth）── 應用層最後防線
</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">zsh</span></span></code></pre></div><h2 id="核心選型維度">核心選型維度</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Cloudflare Tunnel + Access</th>
          <th>Tailscale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>網路模型</strong></td>
          <td>出站連線到 CF 邊緣，產生<strong>公開 URL</strong></td>
          <td><a href="https://www.wireguard.com/">WireGuard</a> mesh VPN，裝置間<strong>私有 IP</strong>，無公開端點</td>
      </tr>
      <tr>
          <td><strong>攻擊面</strong></td>
          <td>公開 URL 存在，需層層防護</td>
          <td>服務端點不存在於公開網路，攻擊者連 IP 都到不了</td>
      </tr>
      <tr>
          <td><strong>認證層數</strong></td>
          <td>三層：CF Access + proxy token + ttyd</td>
          <td>兩層：Tailscale 裝置認證 + ttyd</td>
      </tr>
      <tr>
          <td><strong>Go proxy 職責</strong></td>
          <td>驗 token + 稽核 log + 轉發</td>
          <td>稽核 log + 轉發（不做認證）</td>
      </tr>
      <tr>
          <td><strong>元件數</strong></td>
          <td>5（app → CF → CF Access → proxy → ttyd）</td>
          <td>3（app → Tailscale → proxy/ttyd）</td>
      </tr>
      <tr>
          <td><strong>需要自有網域</strong></td>
          <td>是</td>
          <td>否（MagicDNS 自動分配）</td>
      </tr>
      <tr>
          <td><strong>啟停行程數</strong></td>
          <td>3（cloudflared + ttyd + proxy）</td>
          <td>2（ttyd + proxy），Tailscale daemon 常駐</td>
      </tr>
      <tr>
          <td><strong>憑證包欄位</strong></td>
          <td>8 欄（含 CF Access 憑證 + proxy token）</td>
          <td>~5 欄（endpoint + ttyd 帳密）</td>
      </tr>
      <tr>
          <td><strong>密鑰管理複雜度</strong></td>
          <td>高（proxy token 需可插拔後端 keychain/file/env）</td>
          <td>低（僅 ttyd 帳密）</td>
      </tr>
      <tr>
          <td><strong>費用</strong></td>
          <td>免費（Cloudflare 個人方案）</td>
          <td>免費（Tailscale 個人方案，100 裝置內）</td>
      </tr>
      <tr>
          <td><strong>外部依賴</strong></td>
          <td>Cloudflare 邊緣網路 + CF Access 控制面</td>
          <td>Tailscale 協調伺服器 + DERP relay</td>
      </tr>
      <tr>
          <td><strong>供應商不可用時降級</strong></td>
          <td>邊緣不可用 = 全部連不進來；已建立連線可能存活</td>
          <td>協調伺服器不可用時已建立的 WireGuard 連線存活；DERP relay 不可用只影響 NAT 穿越</td>
      </tr>
  </tbody>
</table>
<h2 id="選型判讀">選型判讀</h2>
<h3 id="tailscale-勝出的場景本情境適用">Tailscale 勝出的場景（本情境適用）</h3>
<ul>
<li><strong>攻擊面最小化是首要目標</strong>：shell 閘道的失敗代價極高，「端點不存在」比「保護公開端點」本質上更安全</li>
<li><strong>單人自用</strong>：不需要 CF Access 的多人 policy / IdP 整合 / Device Posture 等企業功能</li>
<li><strong>架構簡單性</strong>：從 5 元件 3 層認證縮為 3 元件 2 層認證，Go proxy 職責大幅簡化（砍認證閘道，只留 log + 轉發）</li>
<li><strong>密鑰管理簡化</strong>：不再需要為 proxy token 建可插拔多後端（keychain/file/env），只管 ttyd 帳密</li>
<li><strong>不需要自有網域</strong>：Tailscale MagicDNS 或直接用 Tailscale IP</li>
</ul>
<h3 id="cloudflare-tunnel-勝出的場景本情境不適用但值得記錄">Cloudflare Tunnel 勝出的場景（本情境不適用，但值得記錄）</h3>
<ul>
<li><strong>需要對外提供服務</strong>（非自用）：CF 的 WAF / CDN / rate limit / bot protection 生態豐富</li>
<li><strong>需要 HTTP 層細粒度存取控制</strong>：CF Access 的 Application + Policy 模型適合管多個 internal web app</li>
<li><strong>需要 Device Posture 檢查</strong>：CF 整合 CrowdStrike / SentinelOne 等 EDR 做裝置健康判斷（Device Posture：在授權前先檢查裝置的安全狀態 — 作業系統版本、磁碟加密、防毒軟體是否啟用）</li>
<li><strong>已在用 Cloudflare 生態</strong>：共用控制面的管理紅利（同一 Logpush / API token / Audit Log）</li>
<li><strong>多人 / 多團隊 / 合規場景</strong>：CF Access 的 IdP 整合 + Service Auth + Audit Log 比 Tailscale 個人方案完整</li>
</ul>
<h3 id="邊界情境">邊界情境</h3>
<ul>
<li><strong>多人但仍小規模</strong>（2-5 人）：Tailscale ACL（存取控制清單，定義哪些裝置可存取哪些服務）足以控制；超過此規模再評估 CF Access 或 Teleport</li>
<li><strong>需要 session recording</strong>：兩者都沒有一流方案——Tailscale 需 Enterprise tier，CF Access 只記 metadata 不錄 keystroke。重 audit 走 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a></li>
<li><strong>需要從固定 IP 出網</strong>：Tailscale Exit Node 可做但不是設計核心；CF 有更成熟的方案</li>
</ul>
<h2 id="tailscale-採用後的安全底線">Tailscale 採用後的安全底線</h2>
<p>即使 Tailscale 攻擊面更小，仍需維持以下底線：</p>
<table>
  <thead>
      <tr>
          <th>底線</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ttyd 綁 Tailscale 介面或 localhost</td>
          <td>不監聽公開網路介面</td>
      </tr>
      <tr>
          <td>Tailscale ACL 限制裝置</td>
          <td>只有 owner 裝置可存取 proxy port</td>
      </tr>
      <tr>
          <td>ttyd basic auth</td>
          <td>Tailscale 萬一被穿越的最後防線</td>
      </tr>
      <tr>
          <td>稽核 log</td>
          <td>proxy 記錄每次連線（client_ip，不含 PTY 內容）</td>
      </tr>
      <tr>
          <td>不開機自啟（ttyd/proxy）</td>
          <td>手動起停最小化服務暴露窗</td>
      </tr>
  </tbody>
</table>
<h2 id="此選型的-tripwire">此選型的 tripwire</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>觸發後重評</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>從單人變多人</td>
          <td>Tailscale ACL 是否足夠，或需升級為 Teleport / CF Access</td>
      </tr>
      <tr>
          <td>需要對外暴露服務</td>
          <td>Tailscale Funnel 不適合 production hardened ingress，改走 CF</td>
      </tr>
      <tr>
          <td>需要合規 session recording</td>
          <td>Tailscale Enterprise 或改走 Teleport</td>
      </tr>
      <tr>
          <td>需要 WAF / bot protection</td>
          <td>Tailscale 沒有應用層防護，改走 CF</td>
      </tr>
      <tr>
          <td>Tailscale key 即將到期</td>
          <td>確認 key expiry 政策（預設 180 天）、設提醒避免裝置靜默掉線</td>
      </tr>
  </tbody>
</table>
<h2 id="從本情境到-vendor-詳頁">從本情境到 vendor 詳頁</h2>
<ul>
<li>Tailscale 完整 vendor 判讀 → <a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a></li>
<li>Cloudflare Access 完整 vendor 判讀 → <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></li>
<li>Infrastructure access + 合規場景 → <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a></li>
<li>本選型的章節歸屬 → <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
</ul>
]]></content:encoded></item><item><title>2.11 Redis data types 實作</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/</guid><description>&lt;p>Redis data types 的核心責任是把服務語意映射到適合的內建結構，讓讀寫操作的複雜度、原子性與記憶體成本由結構本身保證。選對型別，排行榜更新是一次 O(log N) 操作；選錯型別，同一個需求要拉回整包資料在應用端重算再寫回。本章承接 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape&lt;/a> 的形狀選型，往下談每個型別的實作判讀與容量行為。&lt;/p>
&lt;h2 id="與-28-的分工">與 2.8 的分工&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8&lt;/a> 回答「這份資料是單 key、集合、排序還是計數」這層形狀選型，本章回答「選定形狀後，這個型別的操作語意、原子性與記憶體曲線是什麼」。形狀選型決定方向，型別實作決定它在真實流量下的成本與正確性邊界。兩章分工互補：2.8 判斷形狀，本章確認該型別能不能撐住預期的存取節奏。本章涵蓋 sorted set、bitmap、HyperLogLog、counter 與 hash 這五個快取場景最常用的型別；list 與 stream 的責任偏向佇列與事件流，由 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue&lt;/a> 涵蓋，geo 這類空間型別不在本章範圍。&lt;/p>
&lt;h2 id="sorted-set排行榜與時間線">sorted set：排行榜與時間線&lt;/h2>
&lt;p>sorted set 的責任是維護一組帶 score 的成員，並讓「依 score 排序取範圍」成為一次操作。它適合排行榜、時間線、優先佇列這類「要排序、要取 top-N、要查排名」的場景。&lt;/p>
&lt;p>排行榜是最直接的應用。&lt;code>ZADD leaderboard 5000 player:42&lt;/code> 寫入或更新分數，&lt;code>ZREVRANGE leaderboard 0 9 WITHSCORES&lt;/code> 取前十名，&lt;code>ZREVRANK leaderboard player:42&lt;/code> 查某玩家的排名。每個操作都是 O(log N)，不需要把整個排行榜拉到應用端排序。分數變動用 &lt;code>ZINCRBY&lt;/code> 原子遞增，避免「讀分數、加分、寫回」的競態。&lt;/p>
&lt;p>時間線是第二類應用。把訊息或事件的時間戳當 score，&lt;code>ZADD timeline &amp;lt;timestamp&amp;gt; &amp;lt;event-id&amp;gt;&lt;/code>，就能用 &lt;code>ZRANGEBYSCORE&lt;/code> 取某個時間窗口的事件，或用 &lt;code>ZREVRANGE&lt;/code> 取最新 N 則。這個用法要注意容量：時間線會持續增長，需要搭配 &lt;code>ZREMRANGEBYRANK&lt;/code> 或 &lt;code>ZREMRANGEBYSCORE&lt;/code> 定期裁剪舊資料，否則 key 會無限膨脹。&lt;/p>
&lt;p>sorted set 的判讀重點是 score 語意的正確性。score 是排序的唯一依據，score 設計錯誤會造成排序漂移：用浮點數當 score 時要注意精度，相同 score 的成員按字典序排列，需要穩定排序時要把 tie-break 維度編進 score 或成員名。容量上，sorted set 內部同時維護一個支援 O(1) 查找的 hash 與一個支援 O(log N) 排序的跳躍表（skiplist），兩份索引讓查找與排序都快，但每個成員要在兩個結構各存一份，記憶體成本高於單純的 set，成員數很大的排行榜要評估記憶體佔用。&lt;/p>
&lt;h2 id="bitmap布林狀態的省記憶體表示">bitmap：布林狀態的省記憶體表示&lt;/h2>
&lt;p>bitmap 的責任是用單一 bit 表示每個實體的布林狀態，讓「大量實體的是否」用極小記憶體承載。它建構在 string 上、以 bit 操作存取，適合日活躍標記、功能開關位、簽到記錄這類「每個 id 對應一個是否」的場景。&lt;/p>
&lt;p>日活躍使用者追蹤是典型應用。用日期當 key、使用者 id 當 offset，&lt;code>SETBIT active:20260616 &amp;lt;user-id&amp;gt; 1&lt;/code> 標記某使用者當天活躍，&lt;code>BITCOUNT active:20260616&lt;/code> 算當天活躍總數。一千萬個使用者只需要約 1.2 MB（一千萬 bit），相比為每個使用者存一筆記錄，記憶體成本低一到兩個數量級。多天的留存分析用 &lt;code>BITOP AND&lt;/code> 把多天的 bitmap 做交集，算出連續活躍的使用者。&lt;/p>
&lt;p>bitmap 的判讀重點是 offset 的密度。bitmap 的記憶體取決於最大 offset 而非實際設置的 bit 數：如果 user id 是稀疏的大整數（例如雪花 id），直接當 offset 會撐爆記憶體，需要先把 id 映射成稠密的連續整數。offset 稠密時 bitmap 極省空間，稀疏時反而浪費，這條判讀決定 bitmap 能不能用。&lt;/p>
&lt;h2 id="hyperloglog基數估計">HyperLogLog：基數估計&lt;/h2>
&lt;p>HyperLogLog 的責任是用固定的小記憶體估算一個集合的不重複元素數量，代價是放棄精確值換取近乎常數的空間。它適合 UV 統計、不重複事件計數這類「只要不重複的數量、不需要知道具體是誰」的場景。&lt;/p>
&lt;p>獨立訪客（UV）統計是典型應用。&lt;code>PFADD uv:20260616 &amp;lt;user-id&amp;gt;&lt;/code> 把訪客加入估計，&lt;code>PFCOUNT uv:20260616&lt;/code> 取得不重複訪客數的估計值。HyperLogLog 每個 key 的記憶體在 dense 表示下固定在約 12 KB，無論加入一千還是一億個元素都不增長，標準誤差約 0.81%；元素數少時 Redis 用 sparse 編碼、記憶體遠低於 12 KB，超過可配置的閾值（&lt;code>hll-sparse-max-bytes&lt;/code>，預設 3000 bytes）後才切換成 dense 表示。多天 UV 合併用 &lt;code>PFMERGE&lt;/code> 把多個 HLL 合成一個再 count，算出跨天的不重複訪客。&lt;/p></description><content:encoded><![CDATA[<p>Redis data types 的核心責任是把服務語意映射到適合的內建結構，讓讀寫操作的複雜度、原子性與記憶體成本由結構本身保證。選對型別，排行榜更新是一次 O(log N) 操作；選錯型別，同一個需求要拉回整包資料在應用端重算再寫回。本章承接 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape</a> 的形狀選型，往下談每個型別的實作判讀與容量行為。</p>
<h2 id="與-28-的分工">與 2.8 的分工</h2>
<p><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a> 回答「這份資料是單 key、集合、排序還是計數」這層形狀選型，本章回答「選定形狀後，這個型別的操作語意、原子性與記憶體曲線是什麼」。形狀選型決定方向，型別實作決定它在真實流量下的成本與正確性邊界。兩章分工互補：2.8 判斷形狀，本章確認該型別能不能撐住預期的存取節奏。本章涵蓋 sorted set、bitmap、HyperLogLog、counter 與 hash 這五個快取場景最常用的型別；list 與 stream 的責任偏向佇列與事件流，由 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 涵蓋，geo 這類空間型別不在本章範圍。</p>
<h2 id="sorted-set排行榜與時間線">sorted set：排行榜與時間線</h2>
<p>sorted set 的責任是維護一組帶 score 的成員，並讓「依 score 排序取範圍」成為一次操作。它適合排行榜、時間線、優先佇列這類「要排序、要取 top-N、要查排名」的場景。</p>
<p>排行榜是最直接的應用。<code>ZADD leaderboard 5000 player:42</code> 寫入或更新分數，<code>ZREVRANGE leaderboard 0 9 WITHSCORES</code> 取前十名，<code>ZREVRANK leaderboard player:42</code> 查某玩家的排名。每個操作都是 O(log N)，不需要把整個排行榜拉到應用端排序。分數變動用 <code>ZINCRBY</code> 原子遞增，避免「讀分數、加分、寫回」的競態。</p>
<p>時間線是第二類應用。把訊息或事件的時間戳當 score，<code>ZADD timeline &lt;timestamp&gt; &lt;event-id&gt;</code>，就能用 <code>ZRANGEBYSCORE</code> 取某個時間窗口的事件，或用 <code>ZREVRANGE</code> 取最新 N 則。這個用法要注意容量：時間線會持續增長，需要搭配 <code>ZREMRANGEBYRANK</code> 或 <code>ZREMRANGEBYSCORE</code> 定期裁剪舊資料，否則 key 會無限膨脹。</p>
<p>sorted set 的判讀重點是 score 語意的正確性。score 是排序的唯一依據，score 設計錯誤會造成排序漂移：用浮點數當 score 時要注意精度，相同 score 的成員按字典序排列，需要穩定排序時要把 tie-break 維度編進 score 或成員名。容量上，sorted set 內部同時維護一個支援 O(1) 查找的 hash 與一個支援 O(log N) 排序的跳躍表（skiplist），兩份索引讓查找與排序都快，但每個成員要在兩個結構各存一份，記憶體成本高於單純的 set，成員數很大的排行榜要評估記憶體佔用。</p>
<h2 id="bitmap布林狀態的省記憶體表示">bitmap：布林狀態的省記憶體表示</h2>
<p>bitmap 的責任是用單一 bit 表示每個實體的布林狀態，讓「大量實體的是否」用極小記憶體承載。它建構在 string 上、以 bit 操作存取，適合日活躍標記、功能開關位、簽到記錄這類「每個 id 對應一個是否」的場景。</p>
<p>日活躍使用者追蹤是典型應用。用日期當 key、使用者 id 當 offset，<code>SETBIT active:20260616 &lt;user-id&gt; 1</code> 標記某使用者當天活躍，<code>BITCOUNT active:20260616</code> 算當天活躍總數。一千萬個使用者只需要約 1.2 MB（一千萬 bit），相比為每個使用者存一筆記錄，記憶體成本低一到兩個數量級。多天的留存分析用 <code>BITOP AND</code> 把多天的 bitmap 做交集，算出連續活躍的使用者。</p>
<p>bitmap 的判讀重點是 offset 的密度。bitmap 的記憶體取決於最大 offset 而非實際設置的 bit 數：如果 user id 是稀疏的大整數（例如雪花 id），直接當 offset 會撐爆記憶體，需要先把 id 映射成稠密的連續整數。offset 稠密時 bitmap 極省空間，稀疏時反而浪費，這條判讀決定 bitmap 能不能用。</p>
<h2 id="hyperloglog基數估計">HyperLogLog：基數估計</h2>
<p>HyperLogLog 的責任是用固定的小記憶體估算一個集合的不重複元素數量，代價是放棄精確值換取近乎常數的空間。它適合 UV 統計、不重複事件計數這類「只要不重複的數量、不需要知道具體是誰」的場景。</p>
<p>獨立訪客（UV）統計是典型應用。<code>PFADD uv:20260616 &lt;user-id&gt;</code> 把訪客加入估計，<code>PFCOUNT uv:20260616</code> 取得不重複訪客數的估計值。HyperLogLog 每個 key 的記憶體在 dense 表示下固定在約 12 KB，無論加入一千還是一億個元素都不增長，標準誤差約 0.81%；元素數少時 Redis 用 sparse 編碼、記憶體遠低於 12 KB，超過可配置的閾值（<code>hll-sparse-max-bytes</code>，預設 3000 bytes）後才切換成 dense 表示。多天 UV 合併用 <code>PFMERGE</code> 把多個 HLL 合成一個再 count，算出跨天的不重複訪客。</p>
<p>HyperLogLog 的判讀重點是「估計值能不能接受」。它回答的是「大約多少不重複」，不能回答「某個特定元素在不在集合裡」，也不能取出集合成員。需要精確去重、或需要判斷成員存在性時，用 set 或 bitmap；只要量級且能容忍百分之一以內的誤差時，HyperLogLog 用固定小記憶體換取巨大的空間節省。把 HLL 的估計值當精確值報給財務或計費，是越界用法。</p>
<h2 id="原子計數器counter">原子計數器：counter</h2>
<p>counter 的責任是提供一個原子遞增的整數，讓並發場景下的計數不需要鎖。它建構在 string 上，<code>INCR</code>、<code>INCRBY</code>、<code>DECR</code> 都是原子操作，適合限流、配額、瀏覽計數這類高並發累加。</p>
<p>限流計數是典型應用，也跟 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 卡片直接相關。固定窗口限流用 <code>INCR rate:&lt;user&gt;:&lt;minute&gt;</code> 累加當前窗口的請求數，第一次寫入時 <code>EXPIRE</code> 設定窗口長度，超過閾值就拒絕。原子性讓多個並發請求的計數不會互相覆蓋，這是用一般 <code>GET</code>/<code>SET</code> 做計數會踩到的競態。</p>
<p>counter 的判讀重點是原子性與過期窗口的對齊。<code>INCR</code> 本身原子，但「INCR 後再 EXPIRE」是兩個操作，若第一次 INCR 成功、EXPIRE 失敗，這個 key 會永不過期變成髒計數。最穩健的做法是用 Lua script 把 INCR 與 EXPIRE 包成一個原子單元；<code>SET key 1 EX &lt;ttl&gt; NX</code> 配合後續 INCR 能減少 EXPIRE 漏掉的機率（窗口第一次寫入時就帶上過期），但這個組合的兩步之間仍非原子，不視為與 Lua script 等效。這條對齊跟 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 counter 形狀</a> 提到的「原子性與過期窗口要對齊」是同一件事，本章補上具體實作。</p>
<h2 id="hash結構化欄位的局部更新">hash：結構化欄位的局部更新</h2>
<p>hash 的責任是把一個實體的多個欄位存在同一個 key 下，並讓單一欄位可以獨立讀寫。它適合使用者摘要、商品局部欄位這類「整體是一個實體、但欄位會分別更新」的場景。</p>
<p>相比把整個實體序列化成一個 JSON blob，hash 的優勢是局部更新：<code>HSET user:42 last_seen &lt;ts&gt;</code> 只改一個欄位，不需要讀出整包、改一個值、再寫回。這在欄位更新頻繁的場景省下大量序列化成本與競態風險。<code>HGET</code> 取單一欄位、<code>HGETALL</code> 取全部、<code>HINCRBY</code> 對數值欄位原子遞增。</p>
<p>hash 的判讀重點是欄位責任要清楚。hash 讓欄位能獨立更新，但這也讓它容易滑向「半正式狀態」：當不同欄位由不同來源在不同時間更新，整個 hash 的一致性就變得模糊，某些欄位新、某些欄位舊。判讀條件是這些欄位是否真的能獨立成立；如果它們必須一起更新才有意義，blob 的整體替換反而比 hash 的局部更新更安全。</p>
<p>容量上 hash 有一個要注意的轉折：欄位數與欄位值在閾值內時（<code>hash-max-listpack-entries</code> 預設 128 個欄位、<code>hash-max-listpack-value</code> 預設 64 bytes）用緊湊的 listpack 編碼、記憶體很省，超過任一閾值就轉成 hashtable 編碼，記憶體成本明顯上升。設計大 hash 時要確認欄位數落在閾值內，否則會在某個規模點遇到非線性的記憶體增長。</p>
<h2 id="型別選型的容量與原子性判讀">型別選型的容量與原子性判讀</h2>
<p>選型前要把存取語意、原子性需求與記憶體曲線一起考慮，而不是只看「能不能存」。</p>
<table>
  <thead>
      <tr>
          <th>型別</th>
          <th>承擔語意</th>
          <th>原子操作</th>
          <th>記憶體行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>sorted set</td>
          <td>排序、排名、時間線</td>
          <td><code>ZINCRBY</code>、範圍操作</td>
          <td>隨成員數線性增長，單成員成本偏高</td>
      </tr>
      <tr>
          <td>bitmap</td>
          <td>大量實體的布林狀態</td>
          <td><code>SETBIT</code>、<code>BITOP</code></td>
          <td>取決於最大 offset，稠密時極省</td>
      </tr>
      <tr>
          <td>HyperLogLog</td>
          <td>不重複數量估計</td>
          <td><code>PFADD</code>、<code>PFMERGE</code></td>
          <td>固定約 12 KB，與元素數無關</td>
      </tr>
      <tr>
          <td>counter</td>
          <td>並發累加計數</td>
          <td><code>INCR</code>、<code>INCRBY</code></td>
          <td>單一整數，極小</td>
      </tr>
      <tr>
          <td>hash</td>
          <td>實體的可獨立更新欄位</td>
          <td><code>HINCRBY</code>、<code>HSET</code> 單欄位</td>
          <td>隨欄位數增長，小 hash 有編碼優化</td>
      </tr>
  </tbody>
</table>
<p>sorted set 與 bitmap 都能做「統計」，但語意不同：sorted set 保留每個成員與其分數、可取明細，bitmap 只保留是否、取不出成員但極省空間。需要明細與排名用 sorted set，只需要聚合數量用 bitmap 或 HLL。</p>
<p>HyperLogLog 與 set 的分界是「要不要精確、要不要成員」。set 精確且可列舉，記憶體隨成員數增長；HLL 估計且不可列舉，記憶體固定。同一個 UV 需求，用 set 在大流量下記憶體會失控，用 HLL 換取固定成本但放棄精確值，選擇取決於誤差容忍度。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 sorted set 當成「能排序的 set」而忽略 score 設計，會造成排序漂移。score 是排序的唯一依據，相同 score 按字典序，需要穩定且可預測的排序時要把 tie-break 維度設計進 score。</p>
<p>把 bitmap 用在稀疏 id 上，會讓記憶體被最大 offset 撐爆。bitmap 省記憶體的前提是 offset 稠密，稀疏 id 要先映射成連續整數，或改用其他結構。</p>
<p>把 HyperLogLog 的估計值當精確計數，會在計費、財務這類要求精確的場景出錯。HLL 是有誤差的估計，它的價值在用固定小記憶體換量級判斷，不是替代精確計數。</p>
<p>把多步操作當成原子，會在並發下產生競態。<code>INCR</code> 加 <code>EXPIRE</code>、<code>ZADD</code> 加裁剪都是多個命令，需要原子保證時用 Lua script 或 <code>MULTI</code>/<code>EXEC</code> 包起來。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>排行榜在應用端拉全量排序</td>
          <td>沒用 sorted set 的範圍操作</td>
          <td>改 <code>ZREVRANGE</code> / <code>ZREVRANK</code> 在 Redis 排序</td>
      </tr>
      <tr>
          <td>bitmap key 記憶體異常膨脹</td>
          <td>offset 稀疏、被最大 id 撐大</td>
          <td>把 id 映射成稠密整數，或換結構</td>
      </tr>
      <tr>
          <td>UV 統計記憶體隨流量無上限增長</td>
          <td>用 set 做大基數去重</td>
          <td>容忍誤差時改 HyperLogLog 固定成本</td>
      </tr>
      <tr>
          <td>限流計數出現永不過期的髒 key</td>
          <td>INCR 與 EXPIRE 未原子化</td>
          <td>Lua script 包成原子單元</td>
      </tr>
      <tr>
          <td>hash 欄位新舊不一致、難判讀</td>
          <td>欄位責任不清、滑向半正式狀態</td>
          <td>重新判斷欄位能否獨立，必要時改 blob 整體替換</td>
      </tr>
  </tbody>
</table>
<p>排行榜在應用端拉全量排序是最常見的浪費：明明 sorted set 能 O(log N) 取 top-N，卻把整個集合讀回應用端用程式排序，在成員數大時造成不必要的網路與 CPU 成本。判讀方法是看排序邏輯在哪裡發生，把它推回 Redis 的範圍操作。</p>
<p>limit 計數的髒 key 不產生任何錯誤訊息，因此特別容易被忽略：INCR 成功但 EXPIRE 漏掉，這個 key 不會報錯，只是悄悄永不過期，問題要等到記憶體監控異常或限流誤判時才間接浮現。把 INCR 與 EXPIRE 原子化是最可靠的修法。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要回到資料形狀的選型判斷，回到 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape 與 access pattern</a>。要看這些型別在高並發下的讀寫邊界與連線管理，接著讀 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發下的 Redis 讀寫邊界</a>。要看 stream 型別承擔的事件流責任，接著讀 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a>。</p>
]]></content:encoded></item><item><title>AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 &lt;a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件&lt;/a>、&lt;a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價&lt;/a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。&lt;/p>&lt;/blockquote>
&lt;h2 id="managed-不等於-hands-off">managed 不等於 hands-off&lt;/h2>
&lt;p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 &lt;a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog&lt;/a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。&lt;/p>
&lt;p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。&lt;/p>
&lt;h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側&lt;/h2>
&lt;p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>AWS 的責任（managed）&lt;/th>
 &lt;th>你的責任（仍要自己做）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>硬體 / OS / patching&lt;/td>
 &lt;td>全包&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>failover&lt;/td>
 &lt;td>自動偵測 + replica 晉升&lt;/td>
 &lt;td>client 要有 reconnect 邏輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 AZ 複製&lt;/td>
 &lt;td>Multi-AZ 自動複製&lt;/td>
 &lt;td>接受非同步複製的 stale window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>snapshot / backup&lt;/td>
 &lt;td>自動 + 手動 snapshot&lt;/td>
 &lt;td>決定保留策略、驗證能還原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>eviction&lt;/td>
 &lt;td>提供 maxmemory-policy 參數&lt;/td>
 &lt;td>選對 policy、設對 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache stampede&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>client-side jitter / singleflight 自己做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>key 設計 / hot key&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>key 分布、hot key 兩層 cache 自己處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>連線管理&lt;/td>
 &lt;td>提供 endpoint&lt;/td>
 &lt;td>連線池、socket timeout 自己設&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede&lt;/a> 的雪崩、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯&lt;/a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 <a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件</a>、<a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價</a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。</p></blockquote>
<h2 id="managed-不等於-hands-off">managed 不等於 hands-off</h2>
<p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog</a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。</p>
<p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。</p>
<h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側</h2>
<p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>AWS 的責任（managed）</th>
          <th>你的責任（仍要自己做）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬體 / OS / patching</td>
          <td>全包</td>
          <td>—</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自動偵測 + replica 晉升</td>
          <td>client 要有 reconnect 邏輯</td>
      </tr>
      <tr>
          <td>跨 AZ 複製</td>
          <td>Multi-AZ 自動複製</td>
          <td>接受非同步複製的 stale window</td>
      </tr>
      <tr>
          <td>snapshot / backup</td>
          <td>自動 + 手動 snapshot</td>
          <td>決定保留策略、驗證能還原</td>
      </tr>
      <tr>
          <td>eviction</td>
          <td>提供 maxmemory-policy 參數</td>
          <td>選對 policy、設對 TTL</td>
      </tr>
      <tr>
          <td>cache stampede</td>
          <td>不管</td>
          <td>client-side jitter / singleflight 自己做</td>
      </tr>
      <tr>
          <td>key 設計 / hot key</td>
          <td>不管</td>
          <td>key 分布、hot key 兩層 cache 自己處理</td>
      </tr>
      <tr>
          <td>連線管理</td>
          <td>提供 endpoint</td>
          <td>連線池、socket timeout 自己設</td>
      </tr>
  </tbody>
</table>
<p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。<a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede</a> 的雪崩、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯</a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。</p>
<h3 id="engine-選擇與-cluster-mode">engine 選擇與 cluster mode</h3>
<p>ElastiCache 的兩個結構性決策：</p>
<p><strong>engine</strong>：2024 起 default 是 Valkey（成本約低 20%、OSI 開源、Redis 7.2.4 fork、API 相容）；Redis OSS 仍可選但 AWS 不推；Memcached 是另一條線（純 KV、無 cluster mode 概念）。新部署或既有 Redis 遷移都走 Valkey（相容、便宜），純 cache 才考慮 Memcached。</p>
<p><strong>cluster mode</strong>：disabled 是 1 primary + 最多 5 replica、單 shard、上限約 340GB；enabled 是多 shard（最多 500）、自動 sharding、橫向擴展。判讀：dataset &lt; 300GB 且不需 sharding 用 disabled（簡單），&gt; 300GB 或要橫向擴展用 enabled（但 client 要 cluster-aware）。</p>
<h2 id="配置建立與治理的設定路徑">配置：建立與治理的設定路徑</h2>





<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"># 建立 Valkey replication group（Multi-AZ、auto failover、cluster mode disabled）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elasticache create-replication-group <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --replication-group-id prod-cache <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --replication-group-description <span class="s2">&#34;prod cache&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine valkey <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --cache-node-type cache.r7g.large <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --num-cache-clusters <span class="m">3</span> <span class="se">\ </span>          <span class="c1"># 1 primary + 2 replica</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  --automatic-failover-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --multi-az-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --snapshot-retention-limit <span class="m">7</span> <span class="se">\ </span>    <span class="c1"># 自動 snapshot 保留 7 天</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  --at-rest-encryption-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --transit-encryption-enabled
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 自訂 parameter group（maxmemory-policy 等仍是你的責任）</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">aws elasticache create-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --cache-parameter-group-family valkey8 <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  --description <span class="s2">&#34;prod cache params&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">aws elasticache modify-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --parameter-name-values <span class="s2">&#34;ParameterName=maxmemory-policy,ParameterValue=allkeys-lru&#34;</span></span></span></code></pre></div><p>配置判讀：</p>
<ul>
<li><code>--automatic-failover-enabled</code> + <code>--multi-az-enabled</code> 是 HA 的核心，把 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 那條 failover 時序鏈</a>託管掉</li>
<li><code>maxmemory-policy</code> 透過 parameter group 設定——AWS 給旋鈕、選哪個是你的責任（見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 調校</a>）</li>
<li><code>--transit-encryption-enabled</code> 加 TLS，但 TLS 增加 client 建連成本，連線池更重要</li>
<li>IAM authentication（Redis 7+）取代 AUTH password，對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a></li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1failover-期間-client-持續-error">Case 1：failover 期間 client 持續 error</h3>
<p><strong>徵兆</strong>：ElastiCache 觸發 failover（看 <code>describe-events</code>），AWS 端 replica 晉升完成，但 application 持續 30 秒到幾分鐘大量連線 error。</p>
<p><strong>根因</strong>：failover 時 primary endpoint 的 DNS 切到新 primary，但 client 的連線池還握著舊 primary 的連線、DNS 也可能有快取。AWS 完成了 failover，但 client 重連是你的責任——ElastiCache 不會幫你的 application 重連。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 用支援自動重連的 library，設合理的 socket timeout 與 retry（見 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線調校</a>）</li>
<li>連到 primary endpoint（會跟著 failover 更新 DNS），不要連到特定 node 的 endpoint</li>
<li>縮短 client 的 DNS 快取 TTL，讓 failover 後的 DNS 切換更快被看到</li>
<li>failover 期間的寫入中斷無法完全避免（非同步複製 + 重連時間），latency-sensitive 服務要設計降級</li>
</ol>
<h3 id="case-2跨-az-replication-lag-造成-stale-read">Case 2：跨 AZ replication lag 造成 stale read</h3>
<p><strong>徵兆</strong>：寫入 primary 後立刻從 replica 讀，偶爾讀到舊值；CloudWatch 的 <code>ReplicationLag</code> 在高寫入時段上升。</p>
<p><strong>根因</strong>：ElastiCache 的跨 AZ 複製是非同步的，replica 有 lag。AWS 保證複製會發生，但不保證即時——read-from-replica 在寫後立即讀的場景會看到 stale window。這跟自管 Redis 的 replica 行為一致，managed 沒有消除它。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>寫後需要立即一致讀的路徑，強制 read from primary</li>
<li>監控 CloudWatch <code>ReplicationLag</code>，持續高代表寫入超過複製能力，要 scale up node 或降寫入</li>
<li>接受 cache 的最終一致性——這是 cache copy 的本質，不是 bug（見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a>）</li>
<li>需要強一致 + durability 走 MemoryDB（見本文 Capacity / cost 邊界段）</li>
</ol>
<h3 id="case-3serverless-計費超出預期">Case 3：Serverless 計費超出預期</h3>
<p><strong>徵兆</strong>：用了 ElastiCache Serverless 想省容量規劃，月底帳單遠超預期。</p>
<p><strong>根因</strong>：Serverless 按 ECPU（運算）+ storage 計費，流量尖峰或低效的 access pattern（大量小命令、大 value）會推高 ECPU 消耗。Serverless 解的是「不想規劃容量」，不是「一定更便宜」——可預測的穩態流量用 node-based + Reserved Instance 通常更省。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>流量可預測、穩態高的 workload 用 node-based + Reserved Instance（1/3 年承諾、折扣約 30-60%）</li>
<li>流量不可預測、有大量閒置時段的才適合 Serverless</li>
<li>監控 ECPU 消耗，找出推高成本的 access pattern（用 pipeline 合併小命令降 ECPU）</li>
<li>成本模型對比要算實際 workload，不要假設 Serverless 一定划算</li>
</ol>
<h3 id="case-4cluster-mode-enabled-但-client-不是-cluster-aware">Case 4：cluster mode enabled 但 client 不是 cluster-aware</h3>
<p><strong>徵兆</strong>：建了 cluster mode enabled 的 cluster，application 連線報 <code>MOVED</code> redirect 或連不上某些 key。</p>
<p><strong>根因</strong>：cluster mode enabled 把 keyspace 分到多 shard，client 必須 cluster-aware（懂 <code>CLUSTER SLOTS</code>、處理 <code>MOVED</code>/<code>ASK</code> redirect）才能正確路由。普通 standalone client 連 cluster mode enabled 會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cluster mode enabled 一律用 cluster-aware client（連 configuration endpoint 不是單一 node）</li>
<li>確認 application 的多 key 操作用 hash tag 把相關 key co-locate 同 slot（見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding</a>）</li>
<li>dataset &lt; 300GB 且不需 sharding，用 cluster mode disabled 省掉這層複雜度</li>
<li>從 disabled 升 enabled 是有成本的架構變更，初期規劃就要決定</li>
</ol>
<h3 id="case-5snapshot-期間記憶體尖峰node-不穩">Case 5：snapshot 期間記憶體尖峰、node 不穩</h3>
<p><strong>徵兆</strong>：自動 snapshot 時段 node 延遲上升、<code>DatabaseMemoryUsagePercentage</code> 衝高，偶爾 snapshot 失敗。</p>
<p><strong>根因</strong>：Redis engine 的 snapshot 靠 fork（見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a>），fork 期間 copy-on-write 推高記憶體。如果 node 記憶體已吃緊，snapshot 的 fork 把它推爆。AWS 託管了 snapshot 排程，但 fork 的記憶體成本仍在 engine 層存在。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>node 記憶體留 headroom（不要長期 &gt; 80%），給 snapshot 的 fork copy-on-write 空間</li>
<li>snapshot window 設在低流量時段，減少 fork 期間被改的 page</li>
<li>監控 CloudWatch <code>DatabaseMemoryUsagePercentage</code>，&gt; 80% 考慮 scale up node type</li>
<li>Valkey engine 繼承 Redis 的 fork 模型，這個成本換 engine 到 Valkey 也還在（fork-less 要 DragonflyDB、但 ElastiCache 不提供）</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>ElastiCache 的容量判讀，混合了 AWS 的 metric 與 engine 層的行為：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DatabaseMemoryUsagePercentage</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 80% → scale up node 或調 maxmemory-policy</td>
      </tr>
      <tr>
          <td><code>ReplicationLag</code></td>
          <td>&lt; 1 秒</td>
          <td>持續高 → 寫入超過複製能力</td>
      </tr>
      <tr>
          <td><code>CurrConnections</code></td>
          <td>遠低於 node 上限</td>
          <td>接近上限 → client 連線池問題</td>
      </tr>
      <tr>
          <td><code>CacheHitRate</code></td>
          <td>&gt; 90%（多數 cache）</td>
          <td>下滑 → TTL / eviction / key 設計問題</td>
      </tr>
      <tr>
          <td>Serverless ECPU</td>
          <td>對齊預算</td>
          <td>暴衝 → access pattern 低效、用 pipeline 合併</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 source-of-truth 的 Redis API（不是 cache）</strong>：ElastiCache 是 cache 語意（資料可重建）。需要 durability 走 <strong>AWS MemoryDB</strong>——Redis-compatible 但有 multi-AZ transaction log、提供 source-of-truth 語意，成本約 ElastiCache 的 2-3 倍。判讀：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 把 feature store 從 ScyllaDB 遷到 ElastiCache</a> 的前提是「feature 可重新計算」——可重建選 ElastiCache，不可重建選 MemoryDB 或 database。</li>
<li><strong>跨雲 / 不在 AWS 生態</strong>：ElastiCache 綁 AWS，跨雲走自管 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> 或 GCP Memorystore / Azure Cache。</li>
<li><strong>極端單機 throughput</strong>：要榨單機多核走自管 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（ElastiCache 不提供 Dragonfly engine）。</li>
<li><strong>跨 region active-passive DR</strong>：ElastiCache 的 Global Datastore（1 primary region + 多 secondary read replica、跨 region lag &lt; 1 秒），不支援 active-active multi-master。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>ElastiCache 的 deep article 本質是「劃清 managed 邊界」，它跟 engine 層的調校知識緊密相連：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis 全系列 deep article</a></strong>：eviction、persistence/fork、連線的調校在 ElastiCache 上仍適用（engine 是 Redis/Valkey），AWS 託管的是 failover/patching/snapshot 排程，不是這些 engine 行為。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></strong>：ElastiCache 的 default engine 就是 Valkey，相容性與 io-threads 的判讀直接適用。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 是 Netflix 自管的 Memcached-based 全域 cache，對照 ElastiCache for Memcached + Global Datastore——展示了自管跨區 vs managed 跨區的取捨。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi</a></strong>：兩個 ElastiCache 規模化案例，一個是 sub-ms 配對引擎、一個是 ML feature store p99&lt;10ms，都展示了「AWS 給吞吐、你給設計」的邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>engine 層 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 與 failover 時序</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></li>
<li>上游能力：<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>AWS SQS → Google Pub/Sub：queue 模型搬到 topic + subscription 模型的跨雲遷移</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/migrate-to-google-pubsub/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/migrate-to-google-pubsub/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &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> 跟 &lt;a href="https://tarrragon.github.io/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&lt;/a>。這是一個 &lt;em>跨雲 managed-to-managed&lt;/em> 遷移：兩端都是 cloud-managed、運維負擔都低、但 &lt;em>資料拓樸&lt;/em> 跟 &lt;em>消費抽象&lt;/em> 不同 — SQS 是 region-scoped 的單一 pull queue、Pub/Sub 是 global topic + 多個 first-class subscription。主結構走 operational redesign hybrid（Type C）、額外為 components / data topology 兩個高維度抽獨立段。&lt;/p>&lt;/blockquote>
&lt;h2 id="sqs-跟-pubsub-不是同一種訊息抽象">SQS 跟 Pub/Sub 不是同一種訊息抽象&lt;/h2>
&lt;p>SQS 跟 Pub/Sub 都是 cloud-managed 非同步訊息服務、都解「解耦 producer / consumer、不自管 broker」這個問題、application 程式碼裡都是「發訊息、收訊息、處理完確認」的形狀。從這層看兩者可互換、遷移像是換 SDK。&lt;/p>
&lt;p>差別在 &lt;em>消費抽象&lt;/em> 跟 &lt;em>資料拓樸&lt;/em>。SQS 的核心實體是 queue：一條 region-scoped 的訊息隊列、訊息被一個 consumer 領走（in-flight）就對其他 consumer 隱形、處理完 DeleteMessage 就消失。要讓同一筆事件送給多個下游、SQS 端的做法是在 SNS 前面 fan-out、再各接一條 SQS queue。Pub/Sub 的核心實體是 topic + subscription 兩層：topic 收訊息、subscription 是 &lt;em>first-class&lt;/em> 的消費端點、一個 topic 可掛 N 個 subscription、每個 subscription 各自維護消費進度、fan-out 是模型內建而不是外接。&lt;/p>
&lt;p>這個差別決定了遷移的形狀。如果原系統只是「一條 queue、一群 worker 競爭領取」、那 Pub/Sub 端是「一個 topic、一個 pull subscription」、對位乾淨、application 改動小。如果原系統靠 SNS-to-many-SQS 做扇出、那 Pub/Sub 端是「一個 topic、多個 subscription」、整個 fan-out 拓樸要重畫、這不是換 SDK、是重設計訊息流。先判斷自己屬於哪一種、再決定 playbook 的重量。&lt;/p>
&lt;h2 id="為什麼會跨雲遷這條路徑">為什麼會跨雲遷這條路徑&lt;/h2>
&lt;p>跨雲從 SQS 遷到 Pub/Sub 的 driver 跟同雲 vendor 切換不同、通常不是「Pub/Sub 比 SQS 好」、而是 &lt;em>整體 workload 的重心移到 GCP&lt;/em>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>資料平台落在 GCP&lt;/strong>：下游分析走 BigQuery、streaming 走 Dataflow、容器跑 Cloud Run。事件如果留在 AWS、每筆都要跨雲搬到 GCP 才能進 BigQuery、跨雲 egress 費用跟延遲都是常態成本。把訊息層也移到 Pub/Sub、事件可以用 BigQuery subscription 直接落地、省掉中間搬運。&lt;/li>
&lt;li>&lt;strong>需要 global topic、不想管 region&lt;/strong>：SQS queue 綁 region、跨 region 要自己複製或在前面架路由。Pub/Sub topic 沒有 region 概念、publish 進去全球可訂閱、多區域服務的事件分發是 first-class。&lt;/li>
&lt;li>&lt;strong>fan-out 從外接變內建&lt;/strong>：原本靠 SNS + 多條 SQS 維護的扇出拓樸、在 Pub/Sub 是「一個 topic 掛多個 subscription」、少一層 SNS、扇出關係在 subscription 列表一覽。&lt;/li>
&lt;/ol>
&lt;p>這三條 driver 都假設 &lt;em>重心已經或即將在 GCP&lt;/em>。如果系統長期紮根 AWS、只為了「換個 queue」跨雲、會付出跨雲 IAM 重對位、雙雲計費、跨雲網路延遲的代價、ROI 通常不成立。遷移前先確認 driver 是 workload 重心轉移、不是單純偏好。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <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> 跟 <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>。這是一個 <em>跨雲 managed-to-managed</em> 遷移：兩端都是 cloud-managed、運維負擔都低、但 <em>資料拓樸</em> 跟 <em>消費抽象</em> 不同 — SQS 是 region-scoped 的單一 pull queue、Pub/Sub 是 global topic + 多個 first-class subscription。主結構走 operational redesign hybrid（Type C）、額外為 components / data topology 兩個高維度抽獨立段。</p></blockquote>
<h2 id="sqs-跟-pubsub-不是同一種訊息抽象">SQS 跟 Pub/Sub 不是同一種訊息抽象</h2>
<p>SQS 跟 Pub/Sub 都是 cloud-managed 非同步訊息服務、都解「解耦 producer / consumer、不自管 broker」這個問題、application 程式碼裡都是「發訊息、收訊息、處理完確認」的形狀。從這層看兩者可互換、遷移像是換 SDK。</p>
<p>差別在 <em>消費抽象</em> 跟 <em>資料拓樸</em>。SQS 的核心實體是 queue：一條 region-scoped 的訊息隊列、訊息被一個 consumer 領走（in-flight）就對其他 consumer 隱形、處理完 DeleteMessage 就消失。要讓同一筆事件送給多個下游、SQS 端的做法是在 SNS 前面 fan-out、再各接一條 SQS queue。Pub/Sub 的核心實體是 topic + subscription 兩層：topic 收訊息、subscription 是 <em>first-class</em> 的消費端點、一個 topic 可掛 N 個 subscription、每個 subscription 各自維護消費進度、fan-out 是模型內建而不是外接。</p>
<p>這個差別決定了遷移的形狀。如果原系統只是「一條 queue、一群 worker 競爭領取」、那 Pub/Sub 端是「一個 topic、一個 pull subscription」、對位乾淨、application 改動小。如果原系統靠 SNS-to-many-SQS 做扇出、那 Pub/Sub 端是「一個 topic、多個 subscription」、整個 fan-out 拓樸要重畫、這不是換 SDK、是重設計訊息流。先判斷自己屬於哪一種、再決定 playbook 的重量。</p>
<h2 id="為什麼會跨雲遷這條路徑">為什麼會跨雲遷這條路徑</h2>
<p>跨雲從 SQS 遷到 Pub/Sub 的 driver 跟同雲 vendor 切換不同、通常不是「Pub/Sub 比 SQS 好」、而是 <em>整體 workload 的重心移到 GCP</em>：</p>
<ol>
<li><strong>資料平台落在 GCP</strong>：下游分析走 BigQuery、streaming 走 Dataflow、容器跑 Cloud Run。事件如果留在 AWS、每筆都要跨雲搬到 GCP 才能進 BigQuery、跨雲 egress 費用跟延遲都是常態成本。把訊息層也移到 Pub/Sub、事件可以用 BigQuery subscription 直接落地、省掉中間搬運。</li>
<li><strong>需要 global topic、不想管 region</strong>：SQS queue 綁 region、跨 region 要自己複製或在前面架路由。Pub/Sub topic 沒有 region 概念、publish 進去全球可訂閱、多區域服務的事件分發是 first-class。</li>
<li><strong>fan-out 從外接變內建</strong>：原本靠 SNS + 多條 SQS 維護的扇出拓樸、在 Pub/Sub 是「一個 topic 掛多個 subscription」、少一層 SNS、扇出關係在 subscription 列表一覽。</li>
</ol>
<p>這三條 driver 都假設 <em>重心已經或即將在 GCP</em>。如果系統長期紮根 AWS、只為了「換個 queue」跨雲、會付出跨雲 IAM 重對位、雙雲計費、跨雲網路延遲的代價、ROI 通常不成立。遷移前先確認 driver 是 workload 重心轉移、不是單純偏好。</p>
<h2 id="結構為什麼是-operational-hybrid-加兩個高維度獨立段">結構為什麼是 operational hybrid 加兩個高維度獨立段</h2>
<p>寫這篇前先跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">diff dimension audit</a>、6 維評級如下：</p>
<table>
  <thead>
      <tr>
          <th>Diff 維度</th>
          <th>評級</th>
          <th>SQS → Pub/Sub 的具體差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Medium</td>
          <td>都是「發 / 收 / 確認」、但 API 名詞與參數全換（QueueUrl → topic+subscription）</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>High</td>
          <td>IAM policy → Service Account、CloudWatch → Cloud Monitoring、redrive → DLT 重訂閱</td>
      </tr>
      <tr>
          <td>Abstraction</td>
          <td>Medium</td>
          <td>都是訊息服務、但 pull queue ↔ topic/subscription 的消費抽象不同</td>
      </tr>
      <tr>
          <td>Components（數量）</td>
          <td>High</td>
          <td>單一 queue ↔ topic + N subscription 兩層實體；SNS+SQS 扇出 ↔ topic 內建扇出</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Medium</td>
          <td>SDK 換、ack / fan-out 邏輯改、但商業邏輯多數可保留</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>High</td>
          <td>region-scoped queue ↔ global topic；single-consumer ↔ multi-subscription fan-out</td>
      </tr>
  </tbody>
</table>
<p>主導維度是 <em>operational model</em>（跨雲身份與監控全換）、所以主結構走 Type C operational redesign hybrid。但 components 跟 data topology 也是 High — 不是把它們塞進 operational 段就能講清楚的、消費抽象從「一條 queue」變「topic + 多 subscription」是讀者最容易踩雷的地方。按 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論的 multi-axis 規則</a>、高維度抽成獨立段補充、不硬塞進單一 type 標籤。所以本篇結構是：operational 對位主軸 + 「消費抽象重設計」獨立段（components / topology 軸）+ 跨雲特有的 IAM 與網路段。</p>
<h2 id="operational-對位機制名詞換語意要逐一確認">Operational 對位：機制名詞換、語意要逐一確認</h2>
<p>跨雲遷移最容易出錯的環節、是 <em>找到語意相近的功能、卻假設行為一致</em>。SQS 跟 Pub/Sub 多數機制都有對位、但每一組都有行為差、找得到對應功能只是第一步。下表先給對照、後面逐項展開語意陷阱。</p>
<table>
  <thead>
      <tr>
          <th>SQS 機制</th>
          <th>Pub/Sub 對位</th>
          <th>語意是否等價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visibility timeout</td>
          <td>Ack deadline</td>
          <td>近似、但上限與延長機制不同</td>
      </tr>
      <tr>
          <td>DeleteMessage</td>
          <td>Ack（acknowledge）</td>
          <td>近似、但 Pub/Sub 自動 extension 改變實際行為</td>
      </tr>
      <tr>
          <td>maxReceiveCount + DLQ + redrive</td>
          <td>Dead-letter topic + 重訂閱</td>
          <td>概念對應、DLT 是 topic 不是 queue、重處理方式不同</td>
      </tr>
      <tr>
          <td>Long polling（WaitTimeSeconds）</td>
          <td>Streaming pull</td>
          <td>不等價、streaming pull 是長連線串流、不是輪詢</td>
      </tr>
      <tr>
          <td>Message attributes</td>
          <td>Message attributes</td>
          <td>概念對應、型別與大小限制不同</td>
      </tr>
      <tr>
          <td>FIFO queue（MessageGroupId）</td>
          <td>Ordering key</td>
          <td>都給順序、但去重與吞吐取捨不同</td>
      </tr>
      <tr>
          <td>IAM policy + Queue policy</td>
          <td>IAM role + Service Account</td>
          <td>跨雲身份模型完全不同、不是改語法是重對位</td>
      </tr>
      <tr>
          <td>CloudWatch metric / alarm</td>
          <td>Cloud Monitoring metric / alert</td>
          <td>metric 名詞與語意不同、alarm 邏輯要重寫</td>
      </tr>
  </tbody>
</table>
<h3 id="visibility-timeout--ack-deadline">Visibility timeout → ack deadline</h3>
<p>Visibility timeout 跟 ack deadline 都回答同一個問題：consumer 領走訊息後、多久沒確認就視為失敗、把訊息重新投遞。語意對位成立、但兩端的數字與延長機制不同。</p>
<p>SQS visibility timeout 預設 30 秒、上限 12 小時、consumer 要延長就主動呼叫 ChangeMessageVisibility。Pub/Sub ack deadline 預設 10 秒、上限 600 秒（10 分鐘）、而且 client library 預設會 <em>自動</em> 在背景延長 deadline（lease management）。這個自動延長是最容易踩到的差異：在 SQS 端習慣「設一個夠長的 visibility timeout、處理完再 delete」、搬到 Pub/Sub 如果只把 ack deadline 設成 600 秒上限、卻沒意識到 client library 在背景幫忙延長、長任務的行為會跟預期不同；反過來、如果關掉自動延長又設了預設 10 秒、處理稍久就重投。對位的正確做法是先理解 client library 的 lease 行為、再決定 ack deadline 跟 MaxAckPending、而不是把 SQS 的 timeout 數字直接搬過去。</p>
<h3 id="maxreceivecount--redrive--dead-letter-topic">maxReceiveCount / redrive → dead-letter topic</h3>
<p>兩端都用「重試 N 次仍失敗就隔離」防止 poison message 阻塞 pipeline、但隔離後的容器不同。SQS 的 DLQ 是另一條 <em>queue</em>、用 maxReceiveCount 控制門檻、修好下游後用 redrive policy 把訊息放回原 queue。Pub/Sub 的 dead-letter topic 是另一個 <em>topic</em>、用 subscription 的 max delivery attempt 控制門檻、超過就 publish 到 DLT。</p>
<p>差別在重處理路徑。SQS redrive 是把 DLQ 訊息搬回 main queue、是一個 queue-to-queue 的搬移動作。Pub/Sub 的 DLT 是 topic、要重處理得在 DLT 上再開一個 subscription 來消費、沒有內建的「放回原 topic」按鈕。<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari item feed 的案例</a>就是用 DLT 把重試多次仍失敗的訊息隔離、讓後續訊息優先處理、同時把 topic 當突發流量的 load-leveling buffer。從 SQS 搬過來時、redrive 的心智模型要換成「DLT 是一個獨立 topic、重處理是另開 subscription」、不是「按一個按鈕放回去」。設定 DLT 還需要給 Pub/Sub service account 對 DLT 的 publisher 權限跟對原 subscription 的 subscriber 權限、漏設會讓訊息卡住不進 DLT。</p>
<h3 id="long-polling--streaming-pull">Long polling → streaming pull</h3>
<p>這一組不是等價對位、是機制不同。SQS long polling 是 consumer 發一個 ReceiveMessage 請求、最多等 20 秒、有訊息就回、沒有就空回、本質仍是 <em>輪詢</em>、只是把空輪詢的頻率降下來省 cost。Pub/Sub 的 pull 在 client library 預設是 <em>streaming pull</em>：consumer 跟 Pub/Sub 建一條長連線、訊息一到就推過來、不是 consumer 反覆問。</p>
<p>對位時不要把 long polling 的「WaitTimeSeconds 20 秒」翻譯成某個 Pub/Sub 參數 — 沒有對應參數、因為機制不同。要關注的是 flow control：streaming pull 因為訊息會主動推來、要用 MaxOutstandingMessages / MaxAckPending 控制同時在處理的訊息量、否則 consumer 會被一次塞太多訊息壓垮。SQS 端「一次拉最多 10 條」的批次節流、在 Pub/Sub 端變成 flow control 設定。<a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">Spotify autoscaling 的案例</a>揭露了相關陷阱：下游失敗時 consumer 不 ack 仍持續消耗 CPU、autoscaling 反而把資源越拉越高 — autoscale 訊號要看處理成功率、不是 backlog 加 CPU。</p>
<h3 id="iam-policy--service-account">IAM policy → Service Account</h3>
<p>跨雲遷移裡、身份模型是 <em>重對位</em> 而不是改語法的部分。SQS 的存取控制是 IAM policy（identity-based、掛在 user / role）加 queue policy（resource-based、掛在 queue）兩層、cross-account 靠這兩層互動。Pub/Sub 是 GCP IAM role（publisher / subscriber / viewer 等）加 Service Account、push subscription 要用 Service Account 認證到目標 endpoint。</p>
<p>兩套身份模型沒有自動轉換工具、要逐條重畫：誰能 publish 對應誰有 topic 的 publisher role、誰能消費對應誰有 subscription 的 subscriber role。跨雲場景還多一層 — 如果遷移期 AWS 端的服務要 publish 到 GCP 的 topic、得用 workload identity federation 或 service account key、讓 AWS 的工作負載拿到 GCP 身份。這部分沒有 case 可引、依 GCP 官方 IAM 文件加最小權限原則設計：每個 service account 只給它實際需要的 role、不要為了遷移方便給 broad role 再說以後收緊、那個「以後」通常不會來。</p>
<h3 id="cloudwatch--cloud-monitoring">CloudWatch → Cloud Monitoring</h3>
<p>監控訊號要重建、不是改名。SQS 在 CloudWatch 看 ApproximateNumberOfMessagesVisible（queue 深度）跟 ApproximateAgeOfOldestMessage（lag）。Pub/Sub 在 Cloud Monitoring 看 num_undelivered_messages（backlog）跟 oldest_unacked_message_age（最老未確認訊息年齡）。語意相近、但 alarm 邏輯要重寫、而且 Pub/Sub 的 backlog 數字要配合 subscription 維度看 — 同一個 topic 的不同 subscription 各自有 backlog、一個堵住不代表全部堵住。遷移時要把原本對 queue 深度的告警、改成對每個 subscription 的 backlog 與 age 告警。</p>
<h2 id="消費抽象重設計從一條-queue-到-topic-加多-subscription">消費抽象重設計：從一條 queue 到 topic 加多 subscription</h2>
<p>這是 components 跟 data topology 兩個高維度的核心、也是從 SQS 搬到 Pub/Sub 最需要重新畫圖的地方。SQS 的世界裡、一條 queue 對應一群競爭領取的 worker；要扇出就在前面架 SNS、SNS 後面接多條 SQS、每條 queue 各一群 worker。Pub/Sub 把這個拓樸壓平：一個 topic 收訊息、掛多少個 subscription 就有多少條獨立的消費流、每個 subscription 各自記進度、彼此不影響。</p>
<p>重設計從盤點現有拓樸開始。先列出：哪些是「單一 queue、一群 worker」的簡單情境、哪些是「SNS fan-out 到多條 SQS」的扇出情境。簡單情境對位乾淨 — 一個 topic、一個 pull subscription、原本競爭領取的 worker 改成同一個 subscription 的多個 consumer、Pub/Sub 自動把訊息分給它們。扇出情境要把 SNS + 多 SQS 換成「一個 topic + 多 subscription」、原本每條 SQS queue 變成一個 subscription、SNS 那一層消失。</p>
<p>扇出情境裡有個方向相反的陷阱要避免：不要把「多個下游」誤設計成「多個 consumer 共用一個 subscription」。同一個 subscription 的多個 consumer 是 <em>競爭</em> 關係、訊息只會給其中一個 — 那是負載分攤、不是扇出。要每個下游都收到完整一份、就要每個下游一個 <em>獨立</em> subscription。這跟 SQS 端「一條 queue 一個下游、扇出靠 SNS 複製」的直覺方向一致、但實體換了：在 SQS 是多條 queue、在 Pub/Sub 是多個 subscription。畫遷移圖時、SQS 的每條 fan-out queue 一對一映射到 Pub/Sub 的一個 subscription、不要合併。</p>
<h2 id="application-重設計範例sqs-receive-delete-換成-pubsub-pull-ack">Application 重設計範例：SQS receive-delete 換成 Pub/Sub pull-ack</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// SQS 端：long polling receive、處理完 DeleteMessage</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">svc</span> <span class="o">:=</span> <span class="nx">sqs</span><span class="p">.</span><span class="nf">NewFromConfig</span><span class="p">(</span><span class="nx">cfg</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">out</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">svc</span><span class="p">.</span><span class="nf">ReceiveMessage</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">sqs</span><span class="p">.</span><span class="nx">ReceiveMessageInput</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">QueueUrl</span><span class="p">:</span>            <span class="o">&amp;</span><span class="nx">queueURL</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">MaxNumberOfMessages</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">WaitTimeSeconds</span><span class="p">:</span>     <span class="mi">20</span><span class="p">,</span> <span class="c1">// long polling</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">out</span><span class="p">.</span><span class="nx">Messages</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nf">process</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">svc</span><span class="p">.</span><span class="nf">DeleteMessage</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">sqs</span><span class="p">.</span><span class="nx">DeleteMessageInput</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nx">QueueUrl</span><span class="p">:</span>      <span class="o">&amp;</span><span class="nx">queueURL</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">ReceiptHandle</span><span class="p">:</span> <span class="nx">m</span><span class="p">.</span><span class="nx">ReceiptHandle</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Pub/Sub 端：streaming pull、處理完 Ack、用 flow control 節流</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">sub</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Subscription</span><span class="p">(</span><span class="s">&#34;orders-sub&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">sub</span><span class="p">.</span><span class="nx">ReceiveSettings</span><span class="p">.</span><span class="nx">MaxOutstandingMessages</span> <span class="p">=</span> <span class="mi">100</span> <span class="c1">// flow control、取代「一次拉 10 條」</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">sub</span><span class="p">.</span><span class="nf">Receive</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">pubsub</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nf">process</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> <span class="c1">// 取代 DeleteMessage；client library 在背景自動延長 ack deadline</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>SQS 主動輪詢（ReceiveMessage 迴圈）→ Pub/Sub 回呼模型（Receive 把訊息推進 callback）</li>
<li>SQS DeleteMessage → Pub/Sub msg.Ack()、語意都是「確認處理完、別重投」</li>
<li>SQS WaitTimeSeconds 控制輪詢等待 → Pub/Sub MaxOutstandingMessages 控制 flow control</li>
<li>SQS 一次最多 10 條的批次上限 → Pub/Sub 沒有這個上限、改用 flow control 設同時在途量</li>
<li>ack deadline 的延長在 SQS 要主動 ChangeMessageVisibility、在 Pub/Sub 由 client library 自動處理</li>
</ul>
<p>application 邏輯的商業處理部分（process 函式）多數可保留、改動集中在收訊息的框架跟確認語意、估計 20-40% 程式碼。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1fan-out-設計成共用-subscription下游各收到一部分">Case 1：fan-out 設計成共用 subscription、下游各收到一部分</h3>
<p><strong>徵兆</strong>：把原本 SNS fan-out 到 3 條 SQS 的拓樸搬到 Pub/Sub、為了省事建一個 topic + 一個 subscription、讓 3 個下游服務都連這個 subscription。上線後發現每個下游只收到大約三分之一的訊息、不是各收完整一份。</p>
<p><strong>根因</strong>：同一個 subscription 的多個 consumer 是負載分攤關係、Pub/Sub 把訊息分給其中一個 consumer、不是每個都送。這對應到 SQS 端「一條 queue 多個 worker 競爭領取」的行為、但被誤用在需要扇出的場景。SQS 端的扇出靠 SNS 複製訊息到多條 queue、那個複製動作在 Pub/Sub 應該由「多個 subscription」承擔、不是多個 consumer 共用一個 subscription。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>每個下游一個獨立 subscription</strong>：3 個下游就建 3 個 subscription 掛同一個 topic、每個各收完整一份</li>
<li><strong>遷移圖一對一映射</strong>：SQS 的每條 fan-out queue 對應一個 Pub/Sub subscription、不合併</li>
<li><strong>負載分攤跟扇出分開設計</strong>：同一下游要多 worker 分攤、是同一 subscription 多 consumer；不同下游各收一份、是多 subscription</li>
</ol>
<h3 id="case-2ack-deadline-沿用-sqs-數字太短長任務反覆重投">Case 2：ack deadline 沿用 SQS 數字太短、長任務反覆重投</h3>
<p><strong>徵兆</strong>：SQS 端 visibility timeout 設 5 分鐘跑得好好的、搬到 Pub/Sub 隨手把 ack deadline 設成預設或一個小數字、結果處理時間稍長的訊息被反覆重投、同一筆訊息處理多次、下游出現重複副作用。</p>
<p><strong>根因</strong>：Pub/Sub ack deadline 預設 10 秒、上限 600 秒、跟 SQS visibility timeout 上限 12 小時差很多。如果關掉 client library 的自動 lease extension、又把 ack deadline 設小、處理時間一超過就被判定失敗重投。SQS 的「設一個夠長的 timeout」直覺搬過來不適用、因為 Pub/Sub 的上限低很多、且延長機制是 client library 自動做。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>理解 client library 的 lease 行為</strong>：多數 client library 預設會背景自動延長 ack deadline 到處理完、優先依賴這個而不是手動設超長 deadline</li>
<li><strong>長任務拆短或改架構</strong>：單筆處理超過 10 分鐘上限的、考慮拆成多階段或把長任務移出訊息處理路徑</li>
<li><strong>下游做 idempotency</strong>：跟 SQS 一樣、Pub/Sub 是 at-least-once、重投本來就會發生、下游用 message ID 去重才是根本解</li>
</ol>
<h3 id="case-3fifo-順序需求對位到-ordering-key吞吐落差超出預期">Case 3：FIFO 順序需求對位到 ordering key、吞吐落差超出預期</h3>
<p><strong>徵兆</strong>：原系統用 SQS FIFO queue + MessageGroupId 保證同一群訊息順序處理、搬到 Pub/Sub 啟用 ordering key 對位、上線後吞吐比預期低很多、且某些情境順序仍亂。</p>
<p><strong>根因</strong>：SQS FIFO 跟 Pub/Sub ordering key 都提供順序、但取捨點不同。SQS FIFO 同時給「順序」跟「5 分鐘去重窗口」、吞吐受限（每 MessageGroupId 串行）。Pub/Sub ordering key 給「同一 key 的訊息按 publish 順序送達」、但要 publish 端跟 subscription 端都正確設定（publish 要設 ordering key、subscription 要 enableMessageOrdering）、漏一邊順序就不保證；而且啟用 ordering 後同一 key 串行、吞吐同樣受限。把 FIFO 的「去重 + 順序」一包功能、誤以為 ordering key 也一包提供、是落差來源。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>拆開「順序」跟「去重」兩個需求</strong>：Pub/Sub ordering key 只管順序、去重要 application 端自己用 message ID 做</li>
<li><strong>publish 跟 subscription 兩端都設 ordering</strong>：缺一邊順序不保證、遷移檢查清單要把兩端都列上</li>
<li><strong>重新評估是否真需要全域順序</strong>：FIFO 常被過度使用、很多場景只需要 per-entity 順序、用 ordering key 按 entity 分 key、比強制全域串行吞吐高很多</li>
</ol>
<h3 id="case-4跨雲遷移期雙雲都在跑egress-成本與延遲被低估">Case 4：跨雲遷移期雙雲都在跑、egress 成本與延遲被低估</h3>
<p><strong>徵兆</strong>：漸進 cutover 期間 AWS 跟 GCP 兩邊都在處理訊息、為了對帳把訊息在兩雲之間搬、月底帳單跨雲 egress 費用遠超預估、且跨雲呼叫的尾延遲拖慢端到端處理。</p>
<p><strong>根因</strong>：SQS 在 AWS region 內、Pub/Sub 在 GCP、遷移期的 dual publish 或對帳如果讓資料反覆跨雲、每一筆出 AWS 的訊息都計 egress 費。跨雲不只是錢、跨雲網路的延遲跟抖動比同雲高、放在同步處理路徑上會放大尾延遲。同雲 vendor 切換沒有這個維度、跨雲遷移必須把它列進成本模型。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>縮短雙雲並行窗口</strong>：dual publish 的對帳期越短越省、設明確的並行截止日、不要無限期雙跑</li>
<li><strong>對帳用抽樣不用全量搬運</strong>：驗證一致性用抽樣比對 message ID / count、不要把所有訊息都搬到對面雲比對</li>
<li><strong>生產者就近落點</strong>：遷移後讓 producer 直接 publish 到 Pub/Sub、不要繞 AWS 再跨雲、消除穩態的跨雲 egress</li>
</ol>
<h3 id="case-5dead-letter-topic-權限沒配齊毒訊息卡住不進-dlt">Case 5：dead-letter topic 權限沒配齊、毒訊息卡住不進 DLT</h3>
<p><strong>徵兆</strong>：subscription 設了 dead-letter topic 跟 max delivery attempt、預期重試超限的訊息進 DLT、實際上毒訊息一直在原 subscription 反覆重投、DLT 是空的、後續訊息被堵。</p>
<p><strong>根因</strong>：Pub/Sub 要把訊息送進 DLT、是由 Pub/Sub 的 service account 代為 publish 到 DLT topic；同時它也要對原 subscription 有 subscriber 權限才能 ack 掉原訊息。這兩個權限漏任一個、forwarding 到 DLT 就失敗、訊息卡在原 subscription。SQS 端 DLQ 是 queue 屬性、不需要額外給 service 權限、所以這個跨雲差異容易被漏掉。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>配齊 DLT 雙權限</strong>：給 Pub/Sub service account 對 DLT topic 的 publisher role、跟對原 subscription 的 subscriber role</li>
<li><strong>遷移後做毒訊息演練</strong>：故意 publish 一筆會失敗的訊息、確認它真的在 max attempt 後進 DLT、不是卡在原 subscription</li>
<li><strong>監控 DLT backlog</strong>：DLT 開一個 subscription 監控其 num_undelivered_messages、確認毒訊息有被導流且有人處理、對照 <a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari DLT 案例</a>的設計</li>
</ol>
<h2 id="漸進-cutoverdual-publish-加雙消費對帳">漸進 cutover：dual publish 加雙消費對帳</h2>
<p>跨雲遷移風險高、不適合一次切換、走漸進 cutover 把可逆邊界拉長：</p>
<ol>
<li><strong>Phase 0：拓樸盤點</strong> — 列出所有 SQS queue、標記哪些是單一 queue、哪些是 SNS fan-out、各自映射到 Pub/Sub 的 topic / subscription 結構</li>
<li><strong>Phase 1：Pub/Sub 端建好對位資源</strong> — 建 topic / subscription / DLT、配齊 IAM 與 service account、重建 Cloud Monitoring 告警、application 寫好 Pub/Sub consumer 但先不收流量</li>
<li><strong>Phase 2：dual publish</strong> — producer 同時 publish 到 SQS 跟 Pub/Sub、兩邊 consumer 都跑、Pub/Sub 端的處理結果先寫到隔離區或標記、不影響正式下游</li>
<li><strong>Phase 3：雙消費對帳</strong> — 抽樣比對兩邊處理的訊息 ID 與數量、確認 Pub/Sub 端沒漏、沒重複到無法接受的程度、ack deadline / fan-out / ordering 行為都符合預期</li>
<li><strong>Phase 4：流量切換</strong> — 對帳通過後、把正式下游切到 Pub/Sub 端、SQS 端轉成備援、保留一段觀察期可回切</li>
<li><strong>Phase 5：下線 SQS</strong> — 觀察期穩定後停掉 dual publish、移除 SQS 資源、消除穩態跨雲 egress（這是不可逆階段、不要在對帳沒過時提前做）</li>
</ol>
<p>對帳期是這套流程的核心保險、也是 Case 4 跨雲成本的來源 — 對帳用抽樣、並行窗口設明確截止日、平衡「驗證信心」跟「雙雲成本」。</p>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>AWS SQS</th>
          <th>Google Pub/Sub</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>每百萬 request（含 send / receive / delete）</td>
          <td>按 throughput（publish + subscribe 的資料量計費）</td>
      </tr>
      <tr>
          <td>Region 模型</td>
          <td>Region-scoped、跨 region 自己處理</td>
          <td>Global topic、無 region 概念</td>
      </tr>
      <tr>
          <td>扇出成本</td>
          <td>SNS + 多 SQS、每條 queue 各計費</td>
          <td>一個 topic 多 subscription、按各 subscription throughput</td>
      </tr>
      <tr>
          <td>訊息保留</td>
          <td>預設 4 天、上限 14 天</td>
          <td>預設 7 天、可調</td>
      </tr>
      <tr>
          <td>順序成本</td>
          <td>FIFO queue 比 standard 貴</td>
          <td>ordering key 啟用後吞吐受限、計費同 standard</td>
      </tr>
      <tr>
          <td>跨雲 egress</td>
          <td>出 AWS 計 egress</td>
          <td>出 GCP 計 egress；穩態應讓 producer 就近 publish</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>CloudWatch（隨用量計費）</td>
          <td>Cloud Monitoring</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：穩態成本兩者量級相近、真正的成本差在 <em>遷移期</em> — dual publish 雙雲並行加跨雲對帳搬運是一次性高峰、不是穩態。把這段窗口縮短、是控制跨雲遷移成本的關鍵、不是去比 SQS 跟 Pub/Sub 的單價。扇出重度的系統遷到 Pub/Sub 後、少掉 SNS 那一層、扇出的計費結構也變簡單。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="遷移後事件可直接落-gcp-資料平台">遷移後事件可直接落 GCP 資料平台</h3>
<p>遷到 Pub/Sub 的一個結構性好處、是事件可以用 BigQuery subscription 直接寫進 BigQuery、不需要再寫 Dataflow pipeline 搬運；或用 Cloud Storage subscription 批次落 GCS。這正是「workload 重心在 GCP」這條 driver 的回報 — 事件層跟資料平台同雲、省掉跨雲搬運。這也是評估是否該跨雲遷移時、要放進 ROI 的一邊。</p>
<h3 id="跟-kafka-遷移的結構對照">跟 Kafka 遷移的結構對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>主導差異維度</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></td>
          <td>Paradigm（高）</td>
          <td>partial + 長期混合</td>
      </tr>
      <tr>
          <td>SQS → Pub/Sub（本篇）</td>
          <td>Operational（高）+ components / topology（高）</td>
          <td>operational hybrid + 高維度獨立段</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：SQS → Pub/Sub 不是 paradigm shift（兩端都是 cloud-managed 訊息服務、可收斂成單一目標）、是 operational redesign 為主、消費抽象重設計為輔的跨雲遷移；結構由主導差異維度（operational）決定主軸、高維度（components / topology）抽獨立段補充。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<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> / <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></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>平行 migration playbook：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>引用案例：<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64 Mercari Item Feed DLT</a> / <a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61 Spotify autoscaling</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a></li>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a> / <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
</ul>
]]></content:encoded></item><item><title>Caffeine + Redis 兩層 cache：搭起來很容易，跨實例失效才是全部的問題</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine&lt;/a> overview 的 implementation-layer deep article。選型層（Caffeine vs Redis、process-local 的定位）見 overview；本文只處理「決定用 L1 Caffeine + L2 Redis 後，跨實例一致性怎麼處理」。API 以 &lt;a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="兩層-cache-搭起來容易難的在後面">兩層 cache 搭起來容易，難的在後面&lt;/h2>
&lt;p>L1 Caffeine + L2 Redis 的兩層 cache，讀寫路徑三十行 Java 就寫完：讀的時候先查 L1（process-local、奈秒級），miss 再查 L2（Redis、毫秒級），再 miss 才回源。它擋掉了大部分 Redis 的網路往返，對「每個請求重複讀同一份小資料」的場景效果立竿見影。&lt;/p>
&lt;p>真正的難度不在搭兩層，在「每個 JVM 實例有自己的 L1 副本」這個事實。假設有 10 個 application 實例，就有 10 份獨立的 Caffeine cache。實例 A 更新了某個 user 的資料、寫進 L2 Redis，但實例 B、C、D&amp;hellip; 的 L1 還握著舊值——它們不知道資料變了。下一個打到實例 B 的請求，L1 命中，回的是舊值。Redis 是對的，但讀不到 Redis，因為 L1 先攔截了。&lt;/p>
&lt;p>這就是兩層 cache 的核心問題：L1 的速度來自「不跟任何人協調」，而一致性恰恰需要協調。本文聚焦這個矛盾——兩層讀寫路徑只是背景，跨實例 invalidation 才是全部的工程量。&lt;/p>
&lt;h2 id="核心概念l1-的-stale-從哪裡來">核心概念：L1 的 stale 從哪裡來&lt;/h2>
&lt;p>兩層 cache 的一致性問題，根源是 L1 的三個特性：&lt;/p>
&lt;p>&lt;strong>L1 是 per-instance 的私有副本&lt;/strong>。Caffeine 活在 JVM heap 內，每個實例一份。這是它快的原因（無網路、無序列化），也是它難一致的原因（無法被其他實例直接更新或清除）。L2 Redis 是共享的，所以 L2 一致相對容易；L1 才是 stale 的來源。&lt;/p>
&lt;p>&lt;strong>寫入只更新本地 L1 + 共享 L2&lt;/strong>。實例 A 處理一個更新：寫 L2 Redis（所有實例可見）+ 更新或清除自己的 L1。但 A 沒有辦法直接碰 B 的 L1——B 的 L1 還是舊的，直到它自己過期或被通知。&lt;/p>
&lt;p>&lt;strong>沒有通知機制，L1 只能靠 TTL 自然過期&lt;/strong>。如果不做任何跨實例協調，L1 的 stale window 就等於 L1 的 TTL。把 L1 TTL 設短（幾秒到幾十秒）是最簡單的「容忍 stale」策略——犧牲一點新鮮度換掉協調的複雜度。需要更快失效就得主動廣播。&lt;/p>
&lt;p>跨實例失效的標準解法是用 L2 Redis 的 pub/sub 當廣播通道：任一實例更新資料時，往一個 channel 發一條「key X 失效了」的訊息，所有實例訂閱這個 channel、收到就清掉自己 L1 對應的 entry。這把「各自為政的 L1」連成一個能協同失效的網。&lt;/p>
&lt;h2 id="配置兩層讀寫--pubsub-失效的程式碼">配置：兩層讀寫 + pub/sub 失效的程式碼&lt;/h2>
&lt;p>兩層讀取路徑（L1 → L2 → origin）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1：Caffeine、奈秒級、命中就回&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getIfPresent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1 miss → L2 Redis、毫秒級&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">redis&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deserialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 回填 L1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L2 miss → 回源 + 雙層回填&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">redis&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// L2 TTL 5 分鐘&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跨實例失效（寫入時往 Redis pub/sub 廣播、所有實例清 L1）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> overview 的 implementation-layer deep article。選型層（Caffeine vs Redis、process-local 的定位）見 overview；本文只處理「決定用 L1 Caffeine + L2 Redis 後，跨實例一致性怎麼處理」。API 以 <a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="兩層-cache-搭起來容易難的在後面">兩層 cache 搭起來容易，難的在後面</h2>
<p>L1 Caffeine + L2 Redis 的兩層 cache，讀寫路徑三十行 Java 就寫完：讀的時候先查 L1（process-local、奈秒級），miss 再查 L2（Redis、毫秒級），再 miss 才回源。它擋掉了大部分 Redis 的網路往返，對「每個請求重複讀同一份小資料」的場景效果立竿見影。</p>
<p>真正的難度不在搭兩層，在「每個 JVM 實例有自己的 L1 副本」這個事實。假設有 10 個 application 實例，就有 10 份獨立的 Caffeine cache。實例 A 更新了某個 user 的資料、寫進 L2 Redis，但實例 B、C、D&hellip; 的 L1 還握著舊值——它們不知道資料變了。下一個打到實例 B 的請求，L1 命中，回的是舊值。Redis 是對的，但讀不到 Redis，因為 L1 先攔截了。</p>
<p>這就是兩層 cache 的核心問題：L1 的速度來自「不跟任何人協調」，而一致性恰恰需要協調。本文聚焦這個矛盾——兩層讀寫路徑只是背景，跨實例 invalidation 才是全部的工程量。</p>
<h2 id="核心概念l1-的-stale-從哪裡來">核心概念：L1 的 stale 從哪裡來</h2>
<p>兩層 cache 的一致性問題，根源是 L1 的三個特性：</p>
<p><strong>L1 是 per-instance 的私有副本</strong>。Caffeine 活在 JVM heap 內，每個實例一份。這是它快的原因（無網路、無序列化），也是它難一致的原因（無法被其他實例直接更新或清除）。L2 Redis 是共享的，所以 L2 一致相對容易；L1 才是 stale 的來源。</p>
<p><strong>寫入只更新本地 L1 + 共享 L2</strong>。實例 A 處理一個更新：寫 L2 Redis（所有實例可見）+ 更新或清除自己的 L1。但 A 沒有辦法直接碰 B 的 L1——B 的 L1 還是舊的，直到它自己過期或被通知。</p>
<p><strong>沒有通知機制，L1 只能靠 TTL 自然過期</strong>。如果不做任何跨實例協調，L1 的 stale window 就等於 L1 的 TTL。把 L1 TTL 設短（幾秒到幾十秒）是最簡單的「容忍 stale」策略——犧牲一點新鮮度換掉協調的複雜度。需要更快失效就得主動廣播。</p>
<p>跨實例失效的標準解法是用 L2 Redis 的 pub/sub 當廣播通道：任一實例更新資料時，往一個 channel 發一條「key X 失效了」的訊息，所有實例訂閱這個 channel、收到就清掉自己 L1 對應的 entry。這把「各自為政的 L1」連成一個能協同失效的網。</p>
<h2 id="配置兩層讀寫--pubsub-失效的程式碼">配置：兩層讀寫 + pub/sub 失效的程式碼</h2>
<p>兩層讀取路徑（L1 → L2 → origin）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">User</span><span class="w"> </span><span class="nf">getUser</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="c1">// L1：Caffeine、奈秒級、命中就回</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">l1</span><span class="p">.</span><span class="na">getIfPresent</span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="c1">// L1 miss → L2 Redis、毫秒級</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="n">String</span><span class="w"> </span><span class="n">json</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">redis</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">json</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">deserialize</span><span class="p">(</span><span class="n">json</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="n">l1</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">);</span><span class="w">                 </span><span class="c1">// 回填 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="c1">// L2 miss → 回源 + 雙層回填</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">userRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">setex</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">300</span><span class="p">,</span><span class="w"> </span><span class="n">serialize</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w">  </span><span class="c1">// L2 TTL 5 分鐘</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="n">l1</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">);</span><span class="w">                     </span><span class="c1">// L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div><p>跨實例失效（寫入時往 Redis pub/sub 廣播、所有實例清 L1）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// L1 設短 TTL 當保險（廣播漏掉時的上界）</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">Cache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">l1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">expireAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">30</span><span class="p">))</span><span class="w">  </span><span class="c1">// 廣播失效之外的兜底</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">// 寫入：更新 L2 + 廣播失效</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">updateUser</span><span class="p">(</span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">userRepository</span><span class="p">.</span><span class="na">save</span><span class="p">(</span><span class="n">u</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">setex</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">(),</span><span class="w"> </span><span class="n">300</span><span class="p">,</span><span class="w"> </span><span class="n">serialize</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w">  </span><span class="c1">// 更新 L2（TTL 對齊讀路徑的 300s）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">publish</span><span class="p">(</span><span class="s">&#34;cache:invalidate&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">());</span><span class="w">   </span><span class="c1">// 廣播給所有實例</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="n">l1</span><span class="p">.</span><span class="na">invalidate</span><span class="p">(</span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">());</span><span class="w">                        </span><span class="c1">// 清自己的 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">// 每個實例啟動時訂閱、收到就清本地 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="n">redis</span><span class="p">.</span><span class="na">subscribe</span><span class="p">(</span><span class="s">&#34;cache:invalidate&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">l1</span><span class="p">.</span><span class="na">invalidate</span><span class="p">(</span><span class="n">message</span><span class="p">));</span></span></span></code></pre></div><p>關鍵：L1 的短 TTL 是廣播機制的兜底——即使某個實例漏掉一條 pub/sub 訊息（pub/sub 是 fire-and-forget、訂閱者離線會錯過），L1 最多 stale 到 TTL 過期。廣播負責「快」，TTL 負責「最終」。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1更新後其他實例持續回舊值">Case 1：更新後其他實例持續回舊值</h3>
<p><strong>徵兆</strong>：使用者改了資料、自己刷新看到新值（打到處理寫入的實例），但同事看到的還是舊值（打到別的實例），且持續好幾分鐘。</p>
<p><strong>根因</strong>：只更新了寫入實例的 L1 與 L2，沒有跨實例廣播。其他實例的 L1 還握著舊值、攔截了讀取、根本沒查到已更新的 L2。stale window 等於 L1 TTL（如果 TTL 設很長就是好幾分鐘）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>加 Redis pub/sub 廣播失效，寫入時通知所有實例清 L1</li>
<li>廣播之外把 L1 TTL 設短當兜底（幾秒到幾十秒），縮短漏訊息時的 stale 上界</li>
<li>強一致需求的資料根本不該進 L1——L1 的本質就是「容忍一個 stale window 換速度」</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a> 的新鮮度邊界判斷</li>
</ol>
<h3 id="case-2pubsub-漏訊息個別實例-l1-卡舊值">Case 2：pub/sub 漏訊息、個別實例 L1 卡舊值</h3>
<p><strong>徵兆</strong>：多數實例更新後正常，但偶爾某個實例持續回舊值，直到重啟或 TTL 過期。</p>
<p><strong>根因</strong>：Redis pub/sub 是 fire-and-forget——訂閱者在訊息發出的瞬間若斷線（網路抖動、GC pause、重連中），就永久錯過那條失效訊息。沒有兜底的話，那個實例的 L1 會一直 stale 到 TTL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>L1 TTL 設短是必要兜底，不要依賴 pub/sub 100% 送達（它不保證）</li>
<li>需要可靠失效用 Redis Streams（有 consumer group + 重放）取代 pub/sub，代價是複雜度</li>
<li>監控各實例的 L1 命中率與 stale 投訴，個別實例異常代表漏訊息</li>
<li>接受 pub/sub 的 at-most-once 語意，用 TTL 補足最終一致</li>
</ol>
<h3 id="case-3l1-太大撐爆-heapfull-gc-風暴">Case 3：L1 太大撐爆 heap、Full GC 風暴</h3>
<p><strong>徵兆</strong>：加了 L1 後 application 的 GC 時間變長、偶發 Full GC 導致請求暫停（STW），延遲尖刺。</p>
<p><strong>根因</strong>：Caffeine 預設 on-heap，L1 的 <code>maximumSize</code> 設太大、cache 的物件佔據大量 heap，增加 GC 掃描與回收壓力。大物件 + 大容量直接推高 old gen 佔用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>maximumSize</code> 對齊 heap 預算，用 <code>recordStats()</code> 看實際記憶體佔用</li>
<li>用 <code>maximumWeight</code> + weigher 按物件實際大小限制（不只筆數），避免大物件撐爆</li>
<li>L1 只放「小、熱、重複讀」的資料，大物件留 L2 Redis（off-heap 視角）</li>
<li>監控 GC 時間與 old gen 佔用，L1 容量是可調的 GC 旋鈕</li>
</ol>
<h3 id="case-4l1-快取了不該快取的-per-user-大物件">Case 4：L1 快取了不該快取的 per-user 大物件</h3>
<p><strong>徵兆</strong>：L1 命中率偏低、heap 壓力大、效果不如預期。</p>
<p><strong>根因</strong>：把 per-user 的大物件或低重複率的資料放 L1。L1 的價值在「少量資料被大量重複讀」（如設定檔、熱門商品、權限表），per-user 資料每個 user 一份、重複率低、塞滿 L1 又命中率低。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>L1 只放高重複率的共享熱資料（config、feature flag、熱門 item、權限）</li>
<li>per-user 低重複資料放 L2 Redis 就好，不要進 L1</li>
<li>用 <code>recordStats()</code> 的 hit rate 驗證——L1 命中率低代表放錯資料</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a> 的存取形狀判斷</li>
</ol>
<h3 id="case-5refreshafterwrite-與-expireafterwrite-混淆行為不如預期">Case 5：refreshAfterWrite 與 expireAfterWrite 混淆、行為不如預期</h3>
<p><strong>徵兆</strong>：以為設了自動刷新、結果到期還是 miss 阻塞回源；或以為會過期、結果一直回舊值。</p>
<p><strong>根因</strong>：<code>expireAfterWrite</code>（到期 entry 失效、下次讀 miss + 阻塞載入）跟 <code>refreshAfterWrite</code>（到期後第一個讀觸發背景刷新、舊值立即回、不阻塞）語意不同，混用導致行為不符預期。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>要「到期就不可用」用 <code>expireAfterWrite</code>；要「到期背景刷新、舊值先頂」用 <code>refreshAfterWrite</code></li>
<li>兩者可組合：<code>refreshAfterWrite</code> 短 + <code>expireAfterWrite</code> 長，得到「背景刷新 + 最終過期」</li>
<li><code>refreshAfterWrite</code> 避免 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">stampede</a>（舊值先服務、單一背景刷新），適合熱 key</li>
<li>用 <code>LoadingCache</code> 的 <code>build(key -&gt; load)</code> 配 refresh，行為以官方 wiki 為準</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>兩層 cache 的容量判讀，核心在 L1 命中率、stale window 與 GC：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hit rate</td>
          <td>高（放對高重複資料）</td>
          <td>低 → 放錯資料（per-user 大物件）、改放 L2</td>
      </tr>
      <tr>
          <td>L1 stale window</td>
          <td>≤ L1 TTL（廣播正常更短）</td>
          <td>過長 → TTL 太長或廣播沒做</td>
      </tr>
      <tr>
          <td>GC 時間 / old gen 佔用</td>
          <td>穩定、無 Full GC 風暴</td>
          <td>升高 → L1 太大、降 maximumSize / maximumWeight</td>
      </tr>
      <tr>
          <td>pub/sub 失效送達率</td>
          <td>高（但不保證 100%）</td>
          <td>漏訊息 → TTL 兜底、或改 Streams</td>
      </tr>
      <tr>
          <td>L1 vs L2 命中分層</td>
          <td>L1 擋大部分、L2 擋 L1 miss</td>
          <td>L1 命中低 → 兩層沒分工好</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要強一致 / 不能容忍任何 stale</strong>：L1 process-local 本質有 stale window，不該放這類資料。強一致只用 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> 共享層（甚至直接回源）。</li>
<li><strong>L1 容量需求超過 heap</strong>：on-heap Caffeine 撐不住，用 off-heap 方案（Ehcache off-heap tier）或把資料留 L2 Redis。</li>
<li><strong>可靠失效（不能漏）</strong>：pub/sub 是 at-most-once，要可靠用 Redis Streams 的 consumer group，代價是複雜度。</li>
<li><strong>非 JVM 服務</strong>：Caffeine 綁 JVM，其他語言用對應的 process-local cache（Go ristretto、Rust moka），兩層架構的思路相同。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>兩層 cache 的工程量集中在跨實例一致性，它跟多個議題交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine overview</a></strong>：overview 點到「跨實例 invalidation 是固有限制」、本文展開 pub/sub 廣播 + TTL 兜底的具體解法。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis connection / pipeline</a></strong>：L1 的價值正是消除 L2 Redis 的 RTT 稅，兩層 cache 是 RTT 優化的極致（L1 命中連網路都省）。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></strong>：hot key 的兩層解法（local cache + Redis）就是這個架構，L1 擋掉打在單一熱 key 的洪峰。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder</a></strong>：每次互動查多個 cache 的服務，L1 Caffeine 可擋掉重複讀、降低 L2（ElastiCache）的壓力與 RTT——但 per-user 配對資料重複率低、要判斷哪些放得進 L1。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a></li>
<li>L2 對照：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a> overview 的 implementation-layer deep article。選型層（為何選 DragonflyDB、BSL 授權、相容度）見 overview；本文只處理「決定用 DragonflyDB 後，多核架構怎麼用、相容邊界在哪」。命令實機驗證於 dragonfly df-v1.39.0（&lt;code>redis_version:7.4.0&lt;/code>）、最後檢查日 2026-06-16；效能數字以 &lt;a href="https://www.dragonflydb.io/">DragonflyDB 官方 benchmark&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="scale-up-還是-scale-out一個架構賭注">scale-up 還是 scale-out：一個架構賭注&lt;/h2>
&lt;p>把一台 32 核機器交給 Redis，Redis 的主執行緒只用得到其中一核處理命令——要榨乾這台機器，你得在同一台上跑好幾個 Redis 進程、組成 Cluster、用 hash slot 把 key 分片。多核利用變成了一個分散式系統問題（&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding&lt;/a>、cross-slot transaction、hash tag 治理全都來了）。&lt;/p>
&lt;p>DragonflyDB 賭的是相反方向：一個進程、thread-per-core、shared-nothing，讓單機在不分片的情況下用滿所有核。它的論點是——多數「需要 Redis Cluster」的場景，真正的需求是吞吐與記憶體，不是跨機器分散；如果單機就能撐到那個規模，Cluster 的複雜度就不必付。實機可以看到這個架構：&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">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;thread_count|redis_version|dragonfly_version&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="c1"># thread_count:8 ← 自動對齊 CPU 核數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.4.0 ← 對 client 裝成 Redis 7.4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># dragonfly_version:df-v1.39.0&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>thread_count:8&lt;/code> 在一個進程內，不是 8 個 Redis 進程組 Cluster。這就是賭注的核心：把 Redis Cluster 的水平分片，收進單一進程的垂直多核。理解 DragonflyDB 就是理解這個賭注成立的條件與它撞牆的地方。&lt;/p>
&lt;p>對高吞吐單機 workload，這個賭注有現成的對照。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 用 KeyDB&lt;/a>（Redis 的 multi-threaded fork、單實例吞吐提升 5-10x）撐超高吞吐 cache，正是「不想為了多核去組 Cluster」的同一類需求；DragonflyDB 是這條路線更激進的版本（從零用 C++ 重寫、不是在 Redis 上加 thread）。&lt;/p>
&lt;h2 id="核心概念thread-per-core-與-shared-nothing">核心概念：thread-per-core 與 shared-nothing&lt;/h2>
&lt;p>DragonflyDB 的多核不是「多個執行緒搶同一份資料」，而是把資料切給各個執行緒、彼此不共享——這是它能線性擴展到多核的關鍵。&lt;/p>
&lt;p>&lt;strong>thread-per-core + 資料分區&lt;/strong>。每個 thread 綁一個核，keyspace 被 hash 切成多個 slice，每個 slice 只由一個 thread 擁有。一個命令進來，被路由到擁有該 key 的 thread 處理。因為一個 key 只有一個 thread 碰，單 key 操作不需要鎖——這消除了 Redis 多執行緒方案最大的開銷（lock contention）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> overview 的 implementation-layer deep article。選型層（為何選 DragonflyDB、BSL 授權、相容度）見 overview；本文只處理「決定用 DragonflyDB 後，多核架構怎麼用、相容邊界在哪」。命令實機驗證於 dragonfly df-v1.39.0（<code>redis_version:7.4.0</code>）、最後檢查日 2026-06-16；效能數字以 <a href="https://www.dragonflydb.io/">DragonflyDB 官方 benchmark</a> 為準。</p></blockquote>
<h2 id="scale-up-還是-scale-out一個架構賭注">scale-up 還是 scale-out：一個架構賭注</h2>
<p>把一台 32 核機器交給 Redis，Redis 的主執行緒只用得到其中一核處理命令——要榨乾這台機器，你得在同一台上跑好幾個 Redis 進程、組成 Cluster、用 hash slot 把 key 分片。多核利用變成了一個分散式系統問題（<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding</a>、cross-slot transaction、hash tag 治理全都來了）。</p>
<p>DragonflyDB 賭的是相反方向：一個進程、thread-per-core、shared-nothing，讓單機在不分片的情況下用滿所有核。它的論點是——多數「需要 Redis Cluster」的場景，真正的需求是吞吐與記憶體，不是跨機器分散；如果單機就能撐到那個規模，Cluster 的複雜度就不必付。實機可以看到這個架構：</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">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;thread_count|redis_version|dragonfly_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># thread_count:8               ← 自動對齊 CPU 核數</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># redis_version:7.4.0          ← 對 client 裝成 Redis 7.4</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0</span></span></span></code></pre></div><p><code>thread_count:8</code> 在一個進程內，不是 8 個 Redis 進程組 Cluster。這就是賭注的核心：把 Redis Cluster 的水平分片，收進單一進程的垂直多核。理解 DragonflyDB 就是理解這個賭注成立的條件與它撞牆的地方。</p>
<p>對高吞吐單機 workload，這個賭注有現成的對照。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 用 KeyDB</a>（Redis 的 multi-threaded fork、單實例吞吐提升 5-10x）撐超高吞吐 cache，正是「不想為了多核去組 Cluster」的同一類需求；DragonflyDB 是這條路線更激進的版本（從零用 C++ 重寫、不是在 Redis 上加 thread）。</p>
<h2 id="核心概念thread-per-core-與-shared-nothing">核心概念：thread-per-core 與 shared-nothing</h2>
<p>DragonflyDB 的多核不是「多個執行緒搶同一份資料」，而是把資料切給各個執行緒、彼此不共享——這是它能線性擴展到多核的關鍵。</p>
<p><strong>thread-per-core + 資料分區</strong>。每個 thread 綁一個核，keyspace 被 hash 切成多個 slice，每個 slice 只由一個 thread 擁有。一個命令進來，被路由到擁有該 key 的 thread 處理。因為一個 key 只有一個 thread 碰，單 key 操作不需要鎖——這消除了 Redis 多執行緒方案最大的開銷（lock contention）。</p>
<p><strong>dashtable 取代 Redis 的 dict</strong>。DragonflyDB 用自製的 dashtable（一種 hash table）取代 Redis 的 dictionary，記憶體佈局更緊湊、resize 時不需要像 Redis 那樣漸進式 rehash 全表，同樣的 dataset 通常比 Redis 省 20-40% 記憶體（依資料形狀，以官方 benchmark 為準）。</p>
<p><strong>fork-less snapshot</strong>。Redis 的持久化靠 <code>fork()</code>，大記憶體下會凍結主執行緒並讓記憶體接近翻倍（見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence deep article</a>）。DragonflyDB 不用 fork——它用自己的快照演算法在不複製整個進程的前提下做一致性快照，大記憶體場景不付 fork 的延遲尖峰與記憶體翻倍代價。這是它對「fork 是 Redis 結構性瓶頸」這個痛點的直接回答。</p>
<p><strong>多執行緒的代價：沒有 Redis Cluster mode</strong>。資料分區在單進程內，DragonflyDB 不提供 Redis Cluster mode（它的哲學是單機撐大、不跨機器分片）。這個取捨決定了它的相容邊界與容量天花板，是後面踩坑的根源。</p>
<h2 id="配置多核與持久化的設定路徑">配置：多核與持久化的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run -d --name dragonfly -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    --threads <span class="m">8</span> <span class="se">\ </span>             <span class="c1"># thread 數、預設等於 CPU 核數（一般不需手動設）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    --maxmemory 4gb <span class="se">\ </span>         <span class="c1"># 記憶體上限、行為類似 Redis maxmemory</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    --cache_mode <span class="nb">true</span> <span class="se">\ </span>       <span class="c1"># 純 cache 模式：記憶體滿時自動 evict（類似 allkeys-lru）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    --snapshot_cron <span class="s2">&#34;0 3 * * *&#34;</span> <span class="c1"># fork-less snapshot 排程（cron 格式、這裡每天 3 點）</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>--threads</code> 預設對齊 CPU 核數，多數情況不需手動設；設小於核數會浪費核，設大於核數沒有意義</li>
<li><code>--cache_mode true</code> 讓 DragonflyDB 在記憶體滿時自動淘汰（純 cache 行為）；不開則記憶體滿時拒絕寫入（類似 Redis noeviction）</li>
<li><code>--maxmemory</code> 留 headroom，但因為 fork-less，headroom 不需要像 Redis 留那麼多給 fork copy-on-write</li>
<li>snapshot 用 <code>--snapshot_cron</code> 排程，fork-less 機制讓大記憶體快照不產生延遲尖峰</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1client-配-cluster-mode連不上">Case 1：client 配 Cluster mode、連不上</h3>
<p><strong>徵兆</strong>：從 Redis Cluster 遷來，application 的 client library 還配著 cluster mode，連 DragonflyDB 報錯或 hang，<code>CLUSTER</code> 相關命令行為不如預期。</p>
<p><strong>根因</strong>：DragonflyDB 不提供 Redis Cluster mode（單進程多核、不跨機器分片）。cluster-aware client 會嘗試 <code>CLUSTER SLOTS</code> 之類的拓樸發現，跟 standalone 的 DragonflyDB 對不上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 改回 standalone 配置（不要 cluster mode）</li>
<li>評估原本用 Cluster 的理由：若是為了多核吞吐，DragonflyDB 單進程多核已涵蓋，不需要 cluster mode</li>
<li>若原本用 Cluster 是為了超過單機的容量 / 跨機器分散，DragonflyDB 的 scale-up 模型撐不住，該留在 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a></li>
<li>確認 application 沒有依賴 cluster-specific 行為（hash tag 的跨 slot 語意等）</li>
</ol>
<h3 id="case-2某些-redis-命令--module-不支援">Case 2：某些 Redis 命令 / module 不支援</h3>
<p><strong>徵兆</strong>：核心 SET/GET/HASH 等正常，但某個命令報 <code>unknown command</code> 或行為跟 Redis 不同，特別是 module 命令（RedisJSON / RedisSearch）與部分冷門命令。</p>
<p><strong>根因</strong>：DragonflyDB 相容大多數 Redis 命令但不是 100%；它宣稱相容 <code>redis_version:7.4.0</code>，但部分 module、部分冷門命令、部分 Lua 行為有差異。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>遷移前盤點 application 用到的命令，對照 DragonflyDB 的 API 相容清單（官方 docs）</li>
<li>module 重度依賴（RedisJSON / RedisSearch）要特別確認——DragonflyDB 的 module 生態比 Redis 淺</li>
<li>Lua script 行為差異要實測，不要假設跟 Redis 完全一致</li>
<li>相容性是遷移的主要風險，跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 的相容性驗證</a>同理但 DragonflyDB 邊界更寬（重寫而非 fork）</li>
</ol>
<h3 id="case-3thread-沒對齊核數多核優勢沒發揮">Case 3：thread 沒對齊核數、多核優勢沒發揮</h3>
<p><strong>徵兆</strong>：吞吐沒有達到預期、CPU 使用率不均（部分核閒置），<code>thread_count</code> 跟機器核數對不上。</p>
<p><strong>根因</strong>：<code>--threads</code> 被手動設成小於 CPU 核數，或容器的 CPU limit 限制了實際可用核數，DragonflyDB 沒能用滿所有核。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>redis-cli INFO server | grep thread_count</code> 確認 thread 數對齊實體核數</li>
<li>容器環境確認 CPU limit 沒有卡住 DragonflyDB 的核數（cgroup CPU quota）</li>
<li>不要手動把 <code>--threads</code> 設小，預設對齊核數就是最佳</li>
<li>吞吐沒到預期也可能是 workload 本身（大命令、網路 RTT），用 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線 / pipeline</a> 的 RTT 分析交叉判斷</li>
</ol>
<h3 id="case-4跨-partition-的多-key-操作有額外成本">Case 4：跨 partition 的多 key 操作有額外成本</h3>
<p><strong>徵兆</strong>：大量多 key 命令（MGET 跨很多 key、跨 key 的 Lua）的延遲比預期高，單 key 操作則很快。</p>
<p><strong>根因</strong>：shared-nothing 下 key 分散在不同 thread，多 key 操作要跨 thread 協調——單 key 免鎖的好處在多 key 跨 partition 時要付協調成本。這跟 Redis Cluster 的 cross-slot 是類似的本質（資料分散的代價），只是發生在單進程內。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>高頻的多 key 操作盡量讓 key 落在同 partition（DragonflyDB 的 key 分布規則）</li>
<li>評估能否用單 key 結構（hash）取代多個 key 的聚合</li>
<li>跨 partition 協調是分區架構的固有成本，不是 bug，量大時要設計繞過</li>
<li>對照 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis Cluster 的 cross-slot 限制</a>，兩者都是「資料分散換吞吐」的代價</li>
</ol>
<h3 id="case-5bsl-授權踩到商業使用限制">Case 5：BSL 授權踩到商業使用限制</h3>
<p><strong>徵兆</strong>：準備把 DragonflyDB 包成對外的 managed service 提供給客戶，法務 review 卡關。</p>
<p><strong>根因</strong>：DragonflyDB 用 BSL（Business Source License），商業使用受限——具體限制是不可把 DragonflyDB 當成 managed service 對外提供（4 年後該版本轉 Apache 2.0）。內部使用無限制，但 SaaS 對外提供 DragonflyDB 即服務受限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>內部使用（多數企業場景）無限制，直接用</li>
<li>要把 DragonflyDB 當 managed service 對外賣，聯絡 DragonflyDB 取得商業 license</li>
<li>開源合規敏感（公部門 / 企業 OSI 政策）走 OSI 認可的 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（BSD）</li>
<li>授權法律解讀諮詢法務，不要憑技術判斷</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>DragonflyDB 的容量判讀，核心在 scale-up 的天花板與多核效率：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>thread_count</code></td>
          <td>= CPU 實體核數</td>
          <td>&lt; 核數 → 沒用滿多核、查 &ndash;threads / cgroup</td>
      </tr>
      <tr>
          <td>單機吞吐</td>
          <td>遠高於單 Redis 進程</td>
          <td>撞單機網路 / CPU 上限 → scale-up 到頂</td>
      </tr>
      <tr>
          <td>記憶體效率</td>
          <td>比 Redis 省 20-40%（依形狀）</td>
          <td>以官方 benchmark + 自己量為準</td>
      </tr>
      <tr>
          <td>snapshot 延遲尖峰</td>
          <td>接近 0（fork-less）</td>
          <td>有尖峰 → 確認用的是 DragonflyDB 快照不是相容路徑</td>
      </tr>
      <tr>
          <td>單機容量 / 跨 AZ 需求</td>
          <td>單機 + replica 撐得住</td>
          <td>超單機 / 要跨機器分散 → DragonflyDB 撐不住</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>超過單機容量、需要跨機器分散</strong>：DragonflyDB 的 scale-up 賭注在這裡輸——它沒有 Cluster mode。要跨機器分片走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis / Valkey Cluster</a>。</li>
<li><strong>需要 OSI 認可開源授權</strong>：BSL 不是 OSI 認可，合規敏感走 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（BSD）。</li>
<li><strong>不想自管</strong>：DragonflyDB 目前沒有 fully managed offering（無 ElastiCache for Dragonfly），必須自管。要 managed 走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a>（Redis / Valkey / Memcached）。</li>
<li><strong>跨 AZ / 跨 region HA</strong>：DragonflyDB 有 replica 模式（primary-replica）跨 AZ 可行，但跨 region 需自建——大規模跨區走 managed 的 Global Datastore。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>DragonflyDB 的定位是「Redis 相容 + 激進多核」，它在 Redis 相容服務的光譜上有明確座標：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：兩者都打「Redis 相容 + 更好的多核」，但 Valkey 是 fork（同源、最高相容、漸進加 thread），DragonflyDB 是 C++ 重寫（相容核心但架構激進、多核更徹底）。相容度要極致選 Valkey，多核吞吐要極致選 DragonflyDB。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> / Garnet</strong>：KeyDB 是 Redis 的 multi-threaded fork（<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 採用</a>、Snap 收購後相對停滯）；Garnet 是 Microsoft 的研究型高吞吐 store（生態淺）。DragonflyDB 是這個「高吞吐 Redis 替代」群裡商業化最積極、生態最活躍的。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster re-sharding</a></strong>：如果你的 Redis Cluster re-sharding 頻繁觸發、運維負擔重，DragonflyDB 的 scale-up 模型可能用單機取代整個 Cluster——這是評估遷移的主要動機。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">Shopify write-through</a></strong>：write-through 在 DragonflyDB 上行為一致，但單進程多核能承接比單 Redis 進程更大的 throughput，是 read-heavy + write-through 場景的 scale-up 選項。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性與 io-threads</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence 與 fork latency</a>（fork-less 對照的痛點）</li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Google Pub/Sub push vs pull：不是實作偏好，是下游容量的判讀</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/push-pull-ack-flow-control/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/push-pull-ack-flow-control/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/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 Cloud Pub/Sub&lt;/a> overview 的 implementation-layer deep article。選型層（Pub/Sub vs Kafka / SQS）見 overview；本文只處理「決定用 Pub/Sub 後，subscription 與 ack 怎麼設」。Pub/Sub 是 managed SaaS、無法本機 docker 驗證，本文 config 依 &lt;a href="https://cloud.google.com/pubsub/docs/subscriber">Pub/Sub 官方文件&lt;/a> 與下列 production case、最後檢查日 2026-06-16；引數與計費以官方為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="push-vs-pull-不是實作偏好">push vs pull 不是實作偏好&lt;/h2>
&lt;p>把 Pub/Sub 的 subscription 設成 push 還是 pull，常被當成「看團隊習慣」的實作選擇。但它其實是一個關於下游容量的判讀。差別在流量控制權在誰手上：push subscription 由 Pub/Sub 主動把訊息 HTTP POST 到目標 endpoint——流量節奏由 Pub/Sub 決定，尖峰時瞬間打過來；pull subscription 由 consumer 主動拉，要拉多少、多快由 consumer 自己控制。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">Mercari 的 LINE 整合&lt;/a>把這個判讀講得很具體：Braze webhook 進來轉成 Pub/Sub event，下游要呼叫 LINE API——而 &lt;strong>LINE API 有 RPS 限制&lt;/strong>。如果用 push，Pub/Sub 會把訊息瞬間打到 worker、worker 再打 LINE、直接超過 LINE 的 RPS 上限。所以他們用 pull subscription，worker「精確控制每秒處理訊息數」來對齊 LINE 的限制。這個案例揭露的原則是——&lt;strong>push vs pull 不是實作偏好，是「下游能不能承受 push 的流量衝擊」的判讀&lt;/strong>：下游有速率限制、處理能力有限、或需要平滑流量，就走 pull 自我節流。&lt;/p>
&lt;p>本文展開 subscription 模型、ack deadline、flow control 與 dead-letter topic——這些決定了訊息怎麼被可靠地、以下游能承受的速度消費。&lt;/p>
&lt;h2 id="核心概念subscriptionack-deadline-與-flow-control">核心概念：subscription、ack deadline 與 flow control&lt;/h2>
&lt;p>Pub/Sub 把「topic（發布）」跟「subscription（訂閱）」分開，可靠消費的旋鈕都在 subscription 上。&lt;/p>
&lt;p>&lt;strong>一個 topic、多個 subscription、各自獨立&lt;/strong>。發布者發到 topic，每個 subscription 收到一份完整的訊息流、各自維護消費進度。這天然支援 fanout（多個服務各建一個 subscription）。&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari 的另一個案例&lt;/a>還揭露 topic 的雙重角色——它同時是「dispatch」跟「load-leveling buffer」，突發流量先進 topic 緩衝、consumer 按自己節奏消化。&lt;/p>
&lt;p>&lt;strong>ack deadline 是 Pub/Sub 版的可見性逾時&lt;/strong>。consumer 收到訊息後，有一段 ack deadline 來處理並 &lt;code>ack&lt;/code>。在 deadline 內沒 ack，Pub/Sub 重新投遞（at-least-once）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/" data-link-title="AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀" data-link-desc="SQS deep article：visibility timeout 對齊 consumer 處理時間（ChangeMessageVisibility）、long vs short polling 的 cost 取捨（WaitTimeSeconds）、SQS &amp;#43; Lambda event source mapping（batch size / batch window / 並行 ramp-up）、DLQ &amp;#43; redrive policy（maxReceiveCount）、message size 與 extended client、per-request cost 模型；含 5 個 production 故障演練（VT &amp;lt; 處理時間 redelivery、polling 設定省成本、Lambda 部分失敗整批重投、DLQ maxReceiveCount、FIFO 吞吐上限）">SQS visibility timeout&lt;/a> 同樣是雙邊風險：太短→處理中就重投、太長→失敗後恢復慢。處理中可用 &lt;code>modifyAckDeadline&lt;/code>（client library 通常自動 lease extension）延長。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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 Cloud Pub/Sub</a> overview 的 implementation-layer deep article。選型層（Pub/Sub vs Kafka / SQS）見 overview；本文只處理「決定用 Pub/Sub 後，subscription 與 ack 怎麼設」。Pub/Sub 是 managed SaaS、無法本機 docker 驗證，本文 config 依 <a href="https://cloud.google.com/pubsub/docs/subscriber">Pub/Sub 官方文件</a> 與下列 production case、最後檢查日 2026-06-16；引數與計費以官方為準。</p></blockquote>
<h2 id="push-vs-pull-不是實作偏好">push vs pull 不是實作偏好</h2>
<p>把 Pub/Sub 的 subscription 設成 push 還是 pull，常被當成「看團隊習慣」的實作選擇。但它其實是一個關於下游容量的判讀。差別在流量控制權在誰手上：push subscription 由 Pub/Sub 主動把訊息 HTTP POST 到目標 endpoint——流量節奏由 Pub/Sub 決定，尖峰時瞬間打過來；pull subscription 由 consumer 主動拉，要拉多少、多快由 consumer 自己控制。</p>
<p><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">Mercari 的 LINE 整合</a>把這個判讀講得很具體：Braze webhook 進來轉成 Pub/Sub event，下游要呼叫 LINE API——而 <strong>LINE API 有 RPS 限制</strong>。如果用 push，Pub/Sub 會把訊息瞬間打到 worker、worker 再打 LINE、直接超過 LINE 的 RPS 上限。所以他們用 pull subscription，worker「精確控制每秒處理訊息數」來對齊 LINE 的限制。這個案例揭露的原則是——<strong>push vs pull 不是實作偏好，是「下游能不能承受 push 的流量衝擊」的判讀</strong>：下游有速率限制、處理能力有限、或需要平滑流量，就走 pull 自我節流。</p>
<p>本文展開 subscription 模型、ack deadline、flow control 與 dead-letter topic——這些決定了訊息怎麼被可靠地、以下游能承受的速度消費。</p>
<h2 id="核心概念subscriptionack-deadline-與-flow-control">核心概念：subscription、ack deadline 與 flow control</h2>
<p>Pub/Sub 把「topic（發布）」跟「subscription（訂閱）」分開，可靠消費的旋鈕都在 subscription 上。</p>
<p><strong>一個 topic、多個 subscription、各自獨立</strong>。發布者發到 topic，每個 subscription 收到一份完整的訊息流、各自維護消費進度。這天然支援 fanout（多個服務各建一個 subscription）。<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari 的另一個案例</a>還揭露 topic 的雙重角色——它同時是「dispatch」跟「load-leveling buffer」，突發流量先進 topic 緩衝、consumer 按自己節奏消化。</p>
<p><strong>ack deadline 是 Pub/Sub 版的可見性逾時</strong>。consumer 收到訊息後，有一段 ack deadline 來處理並 <code>ack</code>。在 deadline 內沒 ack，Pub/Sub 重新投遞（at-least-once）。跟 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/" data-link-title="AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀" data-link-desc="SQS deep article：visibility timeout 對齊 consumer 處理時間（ChangeMessageVisibility）、long vs short polling 的 cost 取捨（WaitTimeSeconds）、SQS &#43; Lambda event source mapping（batch size / batch window / 並行 ramp-up）、DLQ &#43; redrive policy（maxReceiveCount）、message size 與 extended client、per-request cost 模型；含 5 個 production 故障演練（VT &lt; 處理時間 redelivery、polling 設定省成本、Lambda 部分失敗整批重投、DLQ maxReceiveCount、FIFO 吞吐上限）">SQS visibility timeout</a> 同樣是雙邊風險：太短→處理中就重投、太長→失敗後恢復慢。處理中可用 <code>modifyAckDeadline</code>（client library 通常自動 lease extension）延長。</p>
<p><strong>flow control 限制 client 端同時持有的未 ack 量</strong>。pull subscription 的 client library 可設 <code>max_outstanding_messages</code> / <code>max_outstanding_bytes</code>——consumer 最多同時持有多少未 ack 訊息。這是 consumer 端自我節流的旋鈕，避免一次拉太多撐爆自己或下游。Mercari 對齊 LINE RPS 靠的就是這層控制。</p>
<p><strong>dead-letter topic（DLT）給毒訊息出口</strong>。subscription 設 dead-letter policy（<code>maxDeliveryAttempts</code> + dead-letter topic）後，重投超過上限的訊息被轉到 DLT，不再阻塞後續。Mercari item feed 正是「重試多次仍失敗送 DLT、後續訊息優先處理」——避免 poison message 卡住 pipeline。</p>
<h2 id="配置subscription--ack-deadline--dlt依官方文件">配置：subscription + ack deadline + DLT（依官方文件）</h2>
<p>Pub/Sub 是 managed、以下 gcloud 依官方文件（未本機 docker 驗證、引數以官方為準）：</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. 建 topic + dead-letter topic</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud pubsub topics create orders
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">gcloud pubsub topics create orders-dlt
</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="c1"># 2. pull subscription：ack deadline + dead-letter policy</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">gcloud pubsub subscriptions create orders-worker <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --topic<span class="o">=</span>orders <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --ack-deadline<span class="o">=</span><span class="m">60</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --dead-letter-topic<span class="o">=</span>orders-dlt <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --max-delivery-attempts<span class="o">=</span><span class="m">5</span>
</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 class="c1"># 3. consumer 端 flow control（client library、以 Python 為例、概念跨語言一致）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">#    flow_control = FlowControl(max_messages=100, max_bytes=10*1024*1024)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">#    subscriber.subscribe(sub_path, callback=handle, flow_control=flow_control)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">#    handle 內：處理成功 message.ack()、失敗 message.nack()</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 class="c1"># push subscription（僅當下游能承受 Pub/Sub 主動推的流量時）：</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># gcloud pubsub subscriptions create orders-push \</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1">#   --topic=orders --push-endpoint=https://my-svc/handler --ack-deadline=60</span></span></span></code></pre></div><p>判讀：</p>
<ul>
<li>下游有 RPS 限制 / 處理能力有限 → pull + flow control（self-throttle，Mercari 模式）</li>
<li>下游能吸收推送尖峰、要 serverless 簡單 → push</li>
<li><code>ack-deadline</code> 略高於處理時間；長任務靠 client library 的 lease extension</li>
<li><code>max-delivery-attempts</code> + DLT 給毒訊息出口</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用-push下游被瞬間流量打爆">Case 1：用 push、下游被瞬間流量打爆</h3>
<p><strong>徵兆</strong>：流量尖峰時下游 endpoint 5xx 暴增、或下游的第三方 API 回 429（rate limited），訊息大量重投惡化。</p>
<p><strong>根因</strong>：用 push subscription，Pub/Sub 把訊息瞬間 POST 到 endpoint，超過下游（或下游依賴的外部 API）的處理 / 速率上限。正是 <a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">Mercari LINE</a> 要避開的情形。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>下游有速率限制改用 pull subscription + flow control，由 consumer 自我節流</li>
<li>flow control 的 <code>max_outstanding_messages</code> 對齊下游能承受的並發</li>
<li>push 只用在下游能吸收推送尖峰的場景</li>
<li>push 場景下游要自己擋（rate limit / 佇列），不能假設 Pub/Sub 會幫你平滑</li>
</ol>
<h3 id="case-2ack-deadline-太短訊息處理中就被重投">Case 2：ack deadline 太短、訊息處理中就被重投</h3>
<p><strong>徵兆</strong>：同一則訊息被處理多次，尤其處理較慢時；訂閱的 redelivery 指標偏高。</p>
<p><strong>根因</strong>：ack deadline 設得比處理時間短，訊息在處理途中 deadline 到期、Pub/Sub 重投。跟 SQS visibility timeout 太短同類。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>ack deadline 設成略高於處理時間 p99</li>
<li>用 client library 的自動 lease extension（modifyAckDeadline）處理長尾任務</li>
<li>消費端冪等——at-least-once 本來就可能重投（見 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency</a>）</li>
<li>監控 redelivery 率，偏高代表 deadline 偏短或處理變慢</li>
</ol>
<h3 id="case-3沒設-dlt毒訊息一直重投阻塞">Case 3：沒設 DLT、毒訊息一直重投阻塞</h3>
<p><strong>徵兆</strong>：某則訊息一直失敗、一直被重投，後續訊息處理被拖慢。</p>
<p><strong>根因</strong>：subscription 沒設 dead-letter policy。處理失敗（nack 或沒 ack）的訊息一再重投、沒有上限與出口，毒訊息反覆消耗 consumer。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 dead-letter policy（<code>max-delivery-attempts</code> + DLT），重投達上限轉 DLT</li>
<li>DLT 是另一個 topic，要有處理 / 告警流程（Mercari「送 DLT、後續訊息優先處理」）</li>
<li><code>max-delivery-attempts</code> 平衡暫時性失敗重試與毒訊息隔離</li>
<li>對照 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/" data-link-title="AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀" data-link-desc="SQS deep article：visibility timeout 對齊 consumer 處理時間（ChangeMessageVisibility）、long vs short polling 的 cost 取捨（WaitTimeSeconds）、SQS &#43; Lambda event source mapping（batch size / batch window / 並行 ramp-up）、DLQ &#43; redrive policy（maxReceiveCount）、message size 與 extended client、per-request cost 模型；含 5 個 production 故障演練（VT &lt; 處理時間 redelivery、polling 設定省成本、Lambda 部分失敗整批重投、DLQ maxReceiveCount、FIFO 吞吐上限）">SQS redrive</a>：兩者都是 managed 原生 DLQ/DLT、比自建省事</li>
</ol>
<h3 id="case-4flow-control-沒設consumer-一次拉太多撐爆">Case 4：flow control 沒設、consumer 一次拉太多撐爆</h3>
<p><strong>徵兆</strong>：consumer 記憶體暴增 / OOM，或一次拉太多把下游打爆。</p>
<p><strong>根因</strong>：pull subscription 沒設 flow control，client library 預設可能持有大量未 ack 訊息，consumer 端記憶體與下游壓力失控。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>max_outstanding_messages</code> / <code>max_outstanding_bytes</code> 限制同時持有量</li>
<li>對齊 consumer 處理能力與下游容量（Mercari 對齊 LINE RPS）</li>
<li>監控 consumer 記憶體與未 ack 數，調 flow control 參數</li>
<li>flow control 是 pull 自我節流的核心，不設等於放棄背壓</li>
</ol>
<h3 id="case-5誤用-ordering-key吞吐受限">Case 5：誤用 ordering key、吞吐受限</h3>
<p><strong>徵兆</strong>：開了 message ordering 後吞吐明顯下降、特定 ordering key 的訊息處理變慢。</p>
<p><strong>根因</strong>：Pub/Sub 的順序保證是 per-ordering-key 的——同一個 ordering key 的訊息嚴格按序、必須序列處理（前一則 ack 才處理下一則）。把所有訊息塞同一個 ordering key 等於序列化整條流、吞吐崩。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>ordering key 用細粒度（per-entity，如 per-user），讓不同 key 可並行</li>
<li>不需要嚴格順序的就別開 ordering（預設無序、吞吐高）</li>
<li>評估順序需求的真實範圍——多數場景只需 per-entity 順序，不是全域</li>
<li>嚴格全域順序 + 高吞吐有本質衝突，重新審視需求或走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> 的 partition 模型</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Pub/Sub 的容量判讀（managed、無 broker 運維）：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>subscription backlog（未 ack 數 / 最舊訊息 age）</td>
          <td>在 SLA 內</td>
          <td>持續成長 → consumer 跟不上、加 consumer / 調 flow control</td>
      </tr>
      <tr>
          <td>redelivery 率</td>
          <td>低</td>
          <td>偏高 → ack deadline 太短 / 下游失敗</td>
      </tr>
      <tr>
          <td>DLT 深度</td>
          <td>低且有處理流程</td>
          <td>成長 → 上游系統性失敗</td>
      </tr>
      <tr>
          <td>consumer 記憶體 / 未 ack 量</td>
          <td>在 flow control 限制內</td>
          <td>暴增 → flow control 沒設好</td>
      </tr>
      <tr>
          <td>訊息量（計費基礎）</td>
          <td>對齊預算</td>
          <td>暴增 → 評估 throughput 計費、batch / 壓縮</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要長期保留 + 任意 replay</strong>：Pub/Sub 有 retention（可設、seek 到時間點）但事件流長期 replay + 生態走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。</li>
<li><strong>嚴格全域順序 + 高吞吐</strong>：Pub/Sub ordering 是 per-key 序列化，全域順序高吞吐走 Kafka partition 設計。</li>
<li><strong>不在 GCP 生態</strong>：Pub/Sub 綁 GCP，跨雲走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 或對應雲的 managed（<a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS</a>）。</li>
<li><strong>複雜 routing（topic exchange 式）</strong>：Pub/Sub 是 topic→subscription 扇出，複雜 routing 規則走 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> exchange。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>push/pull 判讀與 ack 是 Pub/Sub 可靠消費的核心，它跟其他議題交織：</p>
<ul>
<li><strong>跟 <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 design</a></strong>：push/pull、ack deadline、flow control 是 consumer 設計的具體選項。</li>
<li><strong>跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></strong>：at-least-once + 重投要求消費冪等。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/" data-link-title="AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀" data-link-desc="SQS deep article：visibility timeout 對齊 consumer 處理時間（ChangeMessageVisibility）、long vs short polling 的 cost 取捨（WaitTimeSeconds）、SQS &#43; Lambda event source mapping（batch size / batch window / 並行 ramp-up）、DLQ &#43; redrive policy（maxReceiveCount）、message size 與 extended client、per-request cost 模型；含 5 個 production 故障演練（VT &lt; 處理時間 redelivery、polling 設定省成本、Lambda 部分失敗整批重投、DLQ maxReceiveCount、FIFO 吞吐上限）">SQS visibility timeout</a></strong>：ack deadline 對應 visibility timeout、DLT 對應 redrive，兩個 managed queue 的可靠消費模型高度對位、可對照閱讀。</li>
<li><strong>跟 webhook buffer 模式</strong>：Pub/Sub topic 當 load-leveling buffer（Mercari）對應 <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">SQS Twilio webhook buffer</a>——把不可控的外部 webhook 流量先緩衝再按自己節奏消化。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<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 Cloud Pub/Sub</a></li>
<li>對照 vendor：<a href="/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/" data-link-title="AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀" data-link-desc="SQS deep article：visibility timeout 對齊 consumer 處理時間（ChangeMessageVisibility）、long vs short polling 的 cost 取捨（WaitTimeSeconds）、SQS &#43; Lambda event source mapping（batch size / batch window / 並行 ramp-up）、DLQ &#43; redrive policy（maxReceiveCount）、message size 與 extended client、per-request cost 模型；含 5 個 production 故障演練（VT &lt; 處理時間 redelivery、polling 設定省成本、Lambda 部分失敗整批重投、DLQ maxReceiveCount、FIFO 吞吐上限）">AWS SQS visibility timeout</a>、<a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ</a></li>
<li>對應案例：<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65 Mercari LINE flow control</a>、<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64 Mercari item feed DLT</a></li>
<li>上游概念：<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 design</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka Consumer Group Rebalance 與 Lag 診斷：從 protocol 到故障演練</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/consumer-rebalance-lag-diagnosis/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/consumer-rebalance-lag-diagnosis/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a> overview「進階主題」的 implementation-layer deep article，承接 overview「Consumer lag 暴增」與「Rebalance storm」兩段判讀原則的展開。Overview 給判讀方向，本文給 protocol 機制、診斷指令與故障演練。&lt;/p>&lt;/blockquote>
&lt;h2 id="rebalance-是-consumer-group-重新分配-partition-所有權的協調過程">Rebalance 是 consumer group 重新分配 partition 所有權的協調過程&lt;/h2>
&lt;p>Rebalance 是 consumer group coordinator 把 topic 的 partition 重新分配給 group 內 consumer 的協調動作，承擔「在成員數變動時維持每個 partition 恰好被一個 consumer 消費」這個責任。觸發條件是 group membership 改變：consumer 加入、consumer 離開、consumer 被判定失效，或 topic partition 數增加。Rebalance 完成前，受影響的 partition 暫停消費，這段空窗就是 rebalance 對 lag 的直接代價。&lt;/p>
&lt;p>Consumer group 是 Kafka 把「一份 event stream 分給多個 worker 平行處理」與「同一份 stream 給多個獨立應用各自 replay」兩種需求統一的抽象。同一個 group 內的 consumer 瓜分 partition、彼此不重複消費；不同 group 各自維護 offset、互不干擾。Rebalance 只在 group 內部發生，調整的是 group 內 partition 對 consumer 的 mapping。本文聚焦 group 內 rebalance 的機制與診斷，group 概念本身見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group 知識卡&lt;/a>。&lt;/p>
&lt;p>實機觀察 partition 如何在兩個 consumer 間分配：同一 group 起兩個 consumer，coordinator 把 3 個 partition 拆給它們。&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">GROUP CONSUMER-ID CLIENT-ID #PARTITIONS CURRENT-ASSIGNMENT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">live-cg consumer-A-... consumer-A 2 orders:0,1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">live-cg consumer-B-... consumer-B 1 orders:2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">GROUP ASSIGNMENT-STRATEGY STATE #MEMBERS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">live-cg range Stable 2&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>consumer-A 拿到 partition 0、1，consumer-B 拿到 partition 2，STATE 是 Stable 代表 rebalance 已收斂。&lt;code>ASSIGNMENT-STRATEGY&lt;/code> 顯示 range，是預設的 partition 分配演算法。&lt;/p>
&lt;h2 id="eager-與-cooperative-incremental-是兩種-rebalance-protocol">Eager 與 cooperative incremental 是兩種 rebalance protocol&lt;/h2>
&lt;p>Rebalance protocol 決定「rebalance 期間 consumer 要不要交出手上全部 partition」，這個選擇直接決定 rebalance 的 stop-the-world 範圍。Kafka 提供兩種：eager 與 cooperative incremental。&lt;/p>
&lt;p>Eager rebalance 是早期預設行為：rebalance 觸發時，group 內所有 consumer 先放棄手上全部 partition（revoke all），等 coordinator 算完新分配後再各自重新 assign。代價是 rebalance 期間整個 group 完全停止消費，即使某個 consumer 的 partition 在新舊分配中根本沒變，它也得先放掉再拿回。Group 規模越大、partition 越多，這個全停窗口越痛。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> overview「進階主題」的 implementation-layer deep article，承接 overview「Consumer lag 暴增」與「Rebalance storm」兩段判讀原則的展開。Overview 給判讀方向，本文給 protocol 機制、診斷指令與故障演練。</p></blockquote>
<h2 id="rebalance-是-consumer-group-重新分配-partition-所有權的協調過程">Rebalance 是 consumer group 重新分配 partition 所有權的協調過程</h2>
<p>Rebalance 是 consumer group coordinator 把 topic 的 partition 重新分配給 group 內 consumer 的協調動作，承擔「在成員數變動時維持每個 partition 恰好被一個 consumer 消費」這個責任。觸發條件是 group membership 改變：consumer 加入、consumer 離開、consumer 被判定失效，或 topic partition 數增加。Rebalance 完成前，受影響的 partition 暫停消費，這段空窗就是 rebalance 對 lag 的直接代價。</p>
<p>Consumer group 是 Kafka 把「一份 event stream 分給多個 worker 平行處理」與「同一份 stream 給多個獨立應用各自 replay」兩種需求統一的抽象。同一個 group 內的 consumer 瓜分 partition、彼此不重複消費；不同 group 各自維護 offset、互不干擾。Rebalance 只在 group 內部發生，調整的是 group 內 partition 對 consumer 的 mapping。本文聚焦 group 內 rebalance 的機制與診斷，group 概念本身見 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group 知識卡</a>。</p>
<p>實機觀察 partition 如何在兩個 consumer 間分配：同一 group 起兩個 consumer，coordinator 把 3 個 partition 拆給它們。</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">GROUP    CONSUMER-ID    CLIENT-ID    #PARTITIONS  CURRENT-ASSIGNMENT
</span></span><span class="line"><span class="ln">2</span><span class="cl">live-cg  consumer-A-... consumer-A   2            orders:0,1
</span></span><span class="line"><span class="ln">3</span><span class="cl">live-cg  consumer-B-... consumer-B   1            orders:2
</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">GROUP    ASSIGNMENT-STRATEGY  STATE    #MEMBERS
</span></span><span class="line"><span class="ln">6</span><span class="cl">live-cg  range                Stable   2</span></span></code></pre></div><p>consumer-A 拿到 partition 0、1，consumer-B 拿到 partition 2，STATE 是 Stable 代表 rebalance 已收斂。<code>ASSIGNMENT-STRATEGY</code> 顯示 range，是預設的 partition 分配演算法。</p>
<h2 id="eager-與-cooperative-incremental-是兩種-rebalance-protocol">Eager 與 cooperative incremental 是兩種 rebalance protocol</h2>
<p>Rebalance protocol 決定「rebalance 期間 consumer 要不要交出手上全部 partition」，這個選擇直接決定 rebalance 的 stop-the-world 範圍。Kafka 提供兩種：eager 與 cooperative incremental。</p>
<p>Eager rebalance 是早期預設行為：rebalance 觸發時，group 內所有 consumer 先放棄手上全部 partition（revoke all），等 coordinator 算完新分配後再各自重新 assign。代價是 rebalance 期間整個 group 完全停止消費，即使某個 consumer 的 partition 在新舊分配中根本沒變，它也得先放掉再拿回。Group 規模越大、partition 越多，這個全停窗口越痛。</p>
<p>Cooperative incremental rebalance 改成「只 revoke 真正要換手的 partition」。Consumer 先回報自己想保留的 partition，coordinator 算出哪些 partition 需要從 A 搬到 B，只有這些 partition 經歷一次 revoke + reassign，其餘 partition 持續消費不中斷。代價是一次完整 rebalance 可能需要兩輪（第一輪 revoke、第二輪 assign），但每輪只影響少數 partition，整體可用性遠高於 eager。Kafka 2.4 起的 <code>CooperativeStickyAssignor</code> 實作這套協議。</p>
<p>實機驗證 cooperative-sticky 可由 consumer 端 config 啟用，<code>ASSIGNMENT-STRATEGY</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">kafka-console-consumer.sh --topic orders --bootstrap-server localhost:9092 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --group coop-cg <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --consumer-property partition.assignment.strategy<span class="o">=</span>org.apache.kafka.clients.consumer.CooperativeStickyAssignor</span></span></code></pre></div>




<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">GROUP    ASSIGNMENT-STRATEGY  STATE    #MEMBERS
</span></span><span class="line"><span class="ln">2</span><span class="cl">coop-cg  cooperative-sticky   Stable   1</span></span></code></pre></div><p>選 protocol 的判準是 group 規模與消費中斷的容忍度：</p>
<table>
  <thead>
      <tr>
          <th>Protocol</th>
          <th>revoke 範圍</th>
          <th>rebalance 期間消費</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Eager (range / sticky)</td>
          <td>全部 partition</td>
          <td>全停</td>
          <td>小 group、partition 少、rebalance 不頻繁</td>
      </tr>
      <tr>
          <td>Cooperative incremental</td>
          <td>僅換手 partition</td>
          <td>未換手 partition 持續</td>
          <td>大 group、partition 多、要求消費連續性</td>
      </tr>
  </tbody>
</table>
<p>對 partition 數上百、consumer 數十的 group，eager 的全停窗口會讓每次 deploy 都產生明顯 lag spike。Walmart 每天 trillions of message、25K+ consumer 跑在 K8s，pod scaling 與 deploy 觸發的 rebalance 是最大痛點（<a href="/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17</a>）；這種規模下 eager 的全停代價無法接受，cooperative 把中斷限縮到換手 partition 是基本要求。但 Walmart 進一步發現，即使換成 cooperative，partition-consumer 1:1 模型本身在 K8s 規模仍撞到擴張極限，最終把 consumer 解耦成 stateless service。Protocol 選擇降低單次 rebalance 代價，架構解耦才解決 rebalance 頻率本身。</p>
<p>切換 protocol 不能直接全量改：eager 與 cooperative 的 consumer 不能在同一 group 共存。滾動升級時，consumer 需先支援兩種 protocol、再分批切換 config，否則混用會導致 rebalance 失敗或 assignment 不一致。</p>
<h2 id="三個-timeout-各自負責不同的失效判定">三個 timeout 各自負責不同的失效判定</h2>
<p>Consumer 存活由三個 timeout 共同把關，每個負責不同層次的失效訊號，混為一談是 rebalance 誤判的主要來源。</p>
<p><code>session.timeout.ms</code> 是 coordinator 等待 consumer heartbeat 的上限。Consumer 背景執行緒週期性送 heartbeat，coordinator 在這個時間內沒收到就判定 consumer 死亡、觸發 rebalance。預設 45 秒（早期版本 10 秒）。值太小，短暫 GC pause 或網路抖動就誤判離線；值太大，真正死掉的 consumer 要拖很久才被踢出，lag 持續累積。</p>
<p><code>heartbeat.interval.ms</code> 是 consumer 送 heartbeat 的頻率，必須明顯小於 <code>session.timeout.ms</code>，慣例設成 1/3。它決定 coordinator 多快能感知 consumer 變化，也決定 rebalance 訊號的傳播速度。值太大，session window 內 heartbeat 次數不足，容錯空間消失。</p>
<p><code>max.poll.interval.ms</code> 是兩次 <code>poll()</code> 呼叫之間的上限，負責偵測「consumer 活著但卡住」。Consumer 主執行緒在 <code>poll()</code> 之間處理拉到的訊息，如果單批處理太久（下游 I/O 慢、batch 太大、業務邏輯重）超過這個時間，coordinator 判定 consumer 失去處理能力、把它踢出 group。預設 5 分鐘。它跟 session.timeout.ms 的分工是：heartbeat 偵測「行程是否還在」，max.poll.interval 偵測「行程是否還在前進」。</p>
<table>
  <thead>
      <tr>
          <th>Timeout</th>
          <th>偵測對象</th>
          <th>預設</th>
          <th>調整方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>session.timeout.ms</code></td>
          <td>heartbeat 是否中斷</td>
          <td>45000</td>
          <td>環境抖動大調高、要求快速偵測死亡調低</td>
      </tr>
      <tr>
          <td><code>heartbeat.interval.ms</code></td>
          <td>heartbeat 傳送頻率</td>
          <td>3000</td>
          <td>維持在 session.timeout 的 1/3 左右</td>
      </tr>
      <tr>
          <td><code>max.poll.interval.ms</code></td>
          <td>兩次 poll 的間隔</td>
          <td>300000</td>
          <td>單批處理慢就調高，或縮小 max.poll.records</td>
      </tr>
  </tbody>
</table>
<p>這三個值的常見錯配，是把處理變慢誤當成 consumer 死亡。下游 DB 變慢導致每批處理超過 <code>max.poll.interval.ms</code>，consumer 被踢出觸發 rebalance，partition 搬到別的 consumer，那個 consumer 同樣被同一個慢下游拖垮，再次被踢，形成連環 rebalance。這種情況調 <code>session.timeout.ms</code> 沒用，因為 heartbeat 執行緒一直正常送；要調的是 <code>max.poll.interval.ms</code> 或縮小 <code>max.poll.records</code> 讓單批更快做完。</p>
<h2 id="static-group-membership-讓-consumer-重啟不觸發-rebalance">Static group membership 讓 consumer 重啟不觸發 rebalance</h2>
<p>Static membership 給 consumer 一個固定身分 <code>group.instance.id</code>，讓 coordinator 在 consumer 短暫離線後保留它的 partition 分配，承擔「滾動重啟與短暫中斷不觸發 rebalance」的責任。沒有 static membership 時，consumer 每次重啟都產生一個新的 member id，coordinator 視為「舊成員離開、新成員加入」、觸發兩次 rebalance。</p>
<p>設定方式是給每個 consumer 一個跨重啟穩定的 <code>group.instance.id</code>。Coordinator 看到帶 instance id 的 consumer 離線時，不立即 revoke 它的 partition，而是等到 <code>session.timeout.ms</code> 真正超時才判定永久離線。在這個窗口內 consumer 帶同一個 instance id 回來，直接接回原本的 partition，不觸發 rebalance。</p>
<p>實機驗證 <code>group.instance.id</code> 生效後，<code>--members</code> 輸出多出 <code>GROUP-INSTANCE-ID</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">kafka-console-consumer.sh --topic orders --bootstrap-server localhost:9092 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --group static-cg --consumer-property group.instance.id<span class="o">=</span>static-member-1</span></span></code></pre></div>




<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">GROUP      CONSUMER-ID            GROUP-INSTANCE-ID  CLIENT-ID  #PARTITIONS
</span></span><span class="line"><span class="ln">2</span><span class="cl">static-cg  static-member-1-...    static-member-1    static-A   3</span></span></code></pre></div><p>static membership 的關鍵搭配是把 <code>session.timeout.ms</code> 設得比預期的重啟時間長。K8s 滾動更新一個 pod 重啟可能 10-30 秒，session.timeout.ms 要涵蓋這段，否則 pod 還在重啟、coordinator 已判定永久離線、partition 已搬走，static membership 失去意義。代價是真正死掉的 consumer 也要拖到 session.timeout.ms 才被踢出，這段 partition 無人消費。Static membership 用「容忍較長的真實故障偵測延遲」換「消除重啟造成的 rebalance」，適合重啟頻繁但硬故障罕見的環境。</p>
<h2 id="用-kafka-consumer-groupssh-讀-lag-分布">用 kafka-consumer-groups.sh 讀 lag 分布</h2>
<p>診斷 lag 的起點是 <code>kafka-consumer-groups.sh --describe</code>，它逐 partition 列出 current offset、log end offset 與兩者差值 lag，承擔「定位 lag 集中在哪、規模多大」的責任。Lag 是某 partition 已產出的最新 offset 減去 consumer 已 commit 的 offset，代表還沒被消費的訊息量。</p>
<p>實機製造 lag：produce 30 筆訊息、consumer 只消費 12 筆就停掉，<code>--describe</code> 顯示逐 partition 的消費進度落後：</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">kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group analytics-cg</span></span></code></pre></div>




<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">GROUP         TOPIC   PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG  CONSUMER-ID
</span></span><span class="line"><span class="ln">2</span><span class="cl">analytics-cg  orders  0          9               9               0    -
</span></span><span class="line"><span class="ln">3</span><span class="cl">analytics-cg  orders  1          3               9               6    -
</span></span><span class="line"><span class="ln">4</span><span class="cl">analytics-cg  orders  2          0               12              12   -</span></span></code></pre></div><p>這份輸出本身就是診斷的第一個分岔點：lag 是均勻分布還是集中在少數 partition。這裡 partition 0 lag=0、partition 1 lag=6、partition 2 lag=12，明顯集中在後兩個 partition，指向 partition 層的不平衡而非整體 consumer 不足。</p>
<p><code>--state</code> 看 group 的健康狀態與分配策略，<code>--members --verbose</code> 看每個 consumer 實際拿到哪些 partition：</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">kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group live-cg --state</span></span></code></pre></div>




<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">GROUP    COORDINATOR (ID)     ASSIGNMENT-STRATEGY  STATE    #MEMBERS
</span></span><span class="line"><span class="ln">2</span><span class="cl">live-cg  localhost:9092 (1)   range                Stable   2</span></span></code></pre></div><p>STATE 的取值是診斷訊號：<code>Stable</code> 代表分配已收斂正常消費；<code>PreparingRebalance</code> / <code>CompletingRebalance</code> 代表正在 rebalance；<code>Empty</code> 代表 group 沒有 active member（offset 還在但沒人消費），對應上面 lag 輸出裡 <code>CONSUMER-ID</code> 全是 <code>-</code> 的情況。看到 lag 持續累積又長期停在 rebalance 狀態，問題就在 rebalance 本身而非消費速度。</p>
<h2 id="lag-均勻分布與集中單一-partition-指向不同根因">Lag 均勻分布與集中單一 partition 指向不同根因</h2>
<p>Lag 的分布形狀是診斷的主軸：均勻分布指向消費總能力不足，集中在少數 partition 指向 key 分布或單 partition 的局部問題。同樣是 lag 高，這兩種形狀的修法完全相反，先讀分布再決定方向。</p>
<p>Lag 均勻分布在所有 partition，代表 consumer group 整體消費速度跟不上 producer 寫入速度。根因在消費側的總吞吐：consumer 數量不足、單 consumer 處理慢（CPU / GC / 下游 I/O）、或 producer 突發流量超過 group 設計容量。修法是擴消費能力：加 consumer（上限是 partition 數）、優化單筆處理、或對下游加 batch。如果 lag 隨時間線性成長且各 partition 同步成長，是穩態的容量不足，要重新評估 partition 數與 consumer 數。</p>
<p>Lag 集中在少數 partition、其餘 partition lag 接近零，代表負載不均，根因通常在 key 分布。Producer 用 key 決定 partition（<code>hash(key) % partition_count</code>），如果某些 key 是熱點（例如某個大客戶的 id、某個 null key 全落同一 partition），對應 partition 的訊息量遠高於其他，負責它的 consumer 再快也追不上，而其他 consumer 閒著。加 consumer 不解決這個問題，因為瓶頸 partition 仍只能被一個 consumer 消費。修法在 key 設計：拆熱點 key、加 salt 打散、或對熱點走獨立 topic。</p>
<p>Airbnb 的 logging pipeline 遇到的正是 partition 層 skew：event size 從幾百 bytes 到幾百 KB、QPS 跨數個量級，Spark 一個 partition 對一個 task，造成 data skew，catch-up 一個 4 小時 lag 要再花 4 小時（<a href="/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15</a>）。它的解法揭露一個關鍵判準：partition 數不該等同 consumer parallelism。當 lag 集中在少數重 partition，加 consumer 受限於 partition 數的天花板無效，要把 parallelism 從 partition 數解耦、按 event volume × size 重新分派 work。這把「lag 集中」的診斷從 key 分布延伸到了 work 分派模型本身。</p>
<table>
  <thead>
      <tr>
          <th>Lag 分布形狀</th>
          <th>根因方向</th>
          <th>修法</th>
          <th>加 consumer 是否有效</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>均勻分布、各 partition 相近</td>
          <td>消費總能力不足</td>
          <td>加 consumer、優化處理、batch 下游</td>
          <td>有效（上限 partition 數）</td>
      </tr>
      <tr>
          <td>集中少數 partition</td>
          <td>key 分布熱點 / data skew</td>
          <td>拆 key、salt、熱點獨立 topic、解耦 parallelism</td>
          <td>無效（瓶頸 partition 仍單線）</td>
      </tr>
  </tbody>
</table>
<p>判讀順序固定：先 <code>--describe</code> 看分布形狀，再決定往「擴容」還是「重分布」走。跳過分布判讀直接加 consumer，遇到熱點 partition 場景會白花資源還解不了 lag。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1consumer-處理慢被踢出-group-形成-rebalance-連環">Case 1：consumer 處理慢被踢出 group 形成 rebalance 連環</h3>
<p>徵兆：consumer log 反覆出現 <code>Member ... sending LeaveGroup request</code> 與 <code>Attempt to heartbeat failed since group is rebalancing</code>；lag 持續成長；group STATE 在 <code>Stable</code> 與 <code>PreparingRebalance</code> 之間反覆跳；同一批 partition 在不同 consumer 間反覆搬移。</p>
<p>根因：下游 I/O 變慢（DB 連線池打滿、外部 API 延遲升高），consumer 單批 <code>poll()</code> 後處理超過 <code>max.poll.interval.ms</code>（預設 5 分鐘），coordinator 判定該 consumer 失去處理能力、踢出 group、觸發 rebalance。partition 搬到另一個 consumer，後者面對同樣慢的下游、同樣超時被踢，rebalance 連環觸發，每次 rebalance 又讓所有 consumer 暫停消費，lag 加速惡化。</p>
<p>修法：</p>
<ol>
<li>確認瓶頸是處理慢而非 heartbeat 中斷：consumer log 若有正常 heartbeat 但仍被踢，問題在 <code>max.poll.interval.ms</code> 不是 <code>session.timeout.ms</code>。</li>
<li>縮小 <code>max.poll.records</code>：一次拉少一點，讓單批在 <code>max.poll.interval.ms</code> 內做完，這是不改下游就能止血的第一步。</li>
<li>拉高 <code>max.poll.interval.ms</code>：給單批更長處理時間，但這只是延後而非解決，要搭配下游修復。</li>
<li>修復下游根因：DB 連線池、外部 API 超時、batch 寫入策略，這才是消除連環 rebalance 的根本。</li>
</ol>
<h3 id="case-2lag-集中單一-partition加-consumer-無效">Case 2：lag 集中單一 partition、加 consumer 無效</h3>
<p>徵兆：<code>--describe</code> 顯示一兩個 partition lag 數十萬、其餘 partition lag 接近零；加了 consumer 之後 lag 不降，新 consumer 處於閒置（<code>--members</code> 顯示它分到的 partition 都沒 lag）。</p>
<p>根因：producer 的 key 分布有熱點，大量訊息落在同一 partition。Partition 是 Kafka 平行消費的最小單位，一個 partition 只能被 group 內一個 consumer 消費，熱點 partition 的消費速度被單 consumer 鎖死，加再多 consumer 都分不到這個 partition 的工作。</p>
<p>修法：</p>
<ol>
<li><code>--describe</code> 確認 lag 集中形狀，排除「整體容量不足」的均勻分布情境。</li>
<li>找出熱點 key：抽樣訊息看 key 分布，常見是 null key（全落同一 partition）或單一大租戶 id。</li>
<li>重設計 key：對熱點加 salt 打散到多 partition，或讓熱點走獨立 topic 用更多 partition。</li>
<li>若 work 本身有 skew（單筆訊息處理成本差異大），把 parallelism 從 partition 數解耦，按工作量重新分派，如 Airbnb 的 balanced reader（<a href="/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15</a>）。</li>
</ol>
<blockquote>
<p>key 重分布需要 producer 端配合改 key 策略，對既有 topic 是破壞性變更（舊訊息 key 不變），通常搭配新 topic 切換。本文未實機驗證 producer key 重設計的線上切換流程，依官方分區語義說明。</p></blockquote>
<h3 id="case-3deploy-每次都產生-lag-spike">Case 3：deploy 每次都產生 lag spike</h3>
<p>徵兆：每次滾動部署 consumer 服務，lag 在部署窗口內明顯上升、部署完成後緩慢回落；group STATE 在部署期間進入 rebalance；部署越頻繁，累積 lag 越明顯。</p>
<p>根因：每個 consumer pod 重啟，coordinator 看到舊 member 離開、新 member 加入，觸發 rebalance；若用 eager protocol，每次 rebalance 全 group 停止消費；滾動部署逐個重啟 N 個 pod 就觸發 N 次 rebalance，每次全停，lag 在這串全停窗口中累積。</p>
<p>修法：</p>
<ol>
<li>啟用 static membership：給每個 consumer 固定 <code>group.instance.id</code>，重啟時帶同一身分回來、不觸發 rebalance。</li>
<li>把 <code>session.timeout.ms</code> 設得比 pod 重啟時間長：涵蓋 K8s 重啟一個 pod 的 10-30 秒，否則 static membership 在窗口內失效。</li>
<li>切換到 cooperative incremental protocol：即使仍有 rebalance，只有換手 partition 中斷，未換手 partition 持續消費。</li>
<li>控制部署並行度：一次重啟太多 pod 會放大同時 rebalance 的影響，分批滾動。</li>
</ol>
<p>Walmart 在 25K+ consumer 規模下，正是 pod scaling / deploy / heartbeat fail 三類事件持續觸發 rebalance lag spike（<a href="/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17</a>）；static membership 與 cooperative 降低單次代價，但它最終把 consumer 解耦成可獨立 auto-scale 的 stateless service，從架構層消除 rebalance 與 partition 數的綁定。</p>
<h3 id="case-4scale-to-zero-後冷啟動-lag">Case 4：scale-to-zero 後冷啟動 lag</h3>
<p>徵兆：低流量時段 consumer 被縮到 0，流量回來時 lag 已累積一批、需要一段 catch-up；autoscaler 若看 CPU / memory 反應遲鈍，因為 sink 多為 I/O bottleneck、CPU 平坦不觸發擴容。</p>
<p>根因：event-driven workload 的工作量是 backlog（lag）而非 resource usage。用 CPU / memory 當 scaling signal，在 I/O-bound 的 sink consumer 上失靈：訊息堆積但 CPU 不高，autoscaler 不動，lag 持續成長。</p>
<p>修法：</p>
<ol>
<li>用 consumer lag 當 scaling signal：lag 超過閾值就擴 consumer、lag 清空就縮，直接對齊工作量。</li>
<li>接受 scale-to-zero 的冷啟動 lag 為設計取捨：minReplicaCount=0 省下 idle 成本，代價是流量回來時的 catch-up 窗口，對非即時 sink 可接受。</li>
<li>設 lag 閾值與擴容步長：閾值太高 catch-up 久、太低頻繁擴縮，依 SLA 對 backlog 的容忍度設定。</li>
</ol>
<p>Trivago 跨 3 region 跑 50+ Kafka sink、每個 always-on 用 1 CPU + 1 GB，CPU/mem autoscaling 對 I/O-bound sink 無效；改用 KEDA 以 consumer lag 為 scaling signal、minReplicaCount=0 達到 scale-to-zero，daily replica-hour 從 50 降到 1-2（<a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22</a>）。這個案例的判準是 resource usage 不等於工作量，event-driven 場景該看 backlog signal。</p>
<h2 id="capacity-與-cost">Capacity 與 cost</h2>
<p>Rebalance 與 lag 的容量規劃圍繞三個變數：partition 數、consumer 數、單次 rebalance 的中斷成本。partition 數是消費平行度的天花板，consumer 數超過 partition 數時多出的 consumer 閒置，所以 partition 數要按峰值需要的平行度規劃，但 partition 過多會推高 metadata 壓力與 rebalance 計算成本。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Consumer 數上限</td>
          <td>等於 partition 數，超出即閒置</td>
          <td>consumer = partition 仍跟不上要加 partition</td>
      </tr>
      <tr>
          <td>Eager rebalance 中斷</td>
          <td>全 group 停止消費直到分配收斂</td>
          <td>partition 多、group 大時窗口顯著</td>
      </tr>
      <tr>
          <td>Cooperative rebalance</td>
          <td>僅換手 partition 中斷，可能兩輪</td>
          <td>換手比例高時優勢縮小</td>
      </tr>
      <tr>
          <td>session.timeout.ms 窗口</td>
          <td>consumer 死亡到被踢出、partition 無人消費</td>
          <td>設太大則故障偵測慢、lag 累積</td>
      </tr>
      <tr>
          <td>加 partition 的代價</td>
          <td>提高平行度上限，但增加 rebalance 與 metadata 成本</td>
          <td>過度分區推高 controller 壓力</td>
      </tr>
  </tbody>
</table>
<p>實務 default：partition 數按峰值平行度設、保留成長餘量但不過度分區；consumer 數對齊 partition 數、用 lag 而非 CPU 當 autoscaling signal；rebalance 頻繁的環境優先 static membership + cooperative，再評估是否需要把 consumer 從 partition 解耦。加 partition 是單向操作（無法縮回），且改變既有 key 的 partition 對應，要在規劃期一次設足而非事後頻繁調整。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<p>Rebalance 與 lag 診斷接在 consumer 設計與交付語義之上：commit 策略決定 lag 的計算基準與 rebalance 後的重複消費風險，交付語義決定 rebalance 中斷期間訊息是否可能丟失或重放。</p>
<h3 id="跟-consumer-設計對位">跟 consumer 設計對位</h3>
<p><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> 涵蓋 commit 策略（auto vs manual）、commit 時機與 partition 分配的整體設計。本文的 rebalance 是 consumer 設計在「成員變動」維度的展開，lag 是 commit 進度的可觀測量。commit 策略選錯會在 rebalance 後放大重複消費或丟失。</p>
<h3 id="跟交付與復原語義對位">跟交付與復原語義對位</h3>
<p><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing 與 recovery 語義</a> 涵蓋 rebalance 中斷期間的 at-least-once / at-most-once 行為。rebalance revoke partition 時，未 commit 的進度會在新 consumer 接手後重放（at-least-once）；commit 太早則可能在 rebalance 中丟失（at-most-once）。idempotency 與 replay 的整體設計見 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a>。</p>
<h3 id="相關案例">相關案例</h3>
<ul>
<li><a href="/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15 Airbnb Spark Streaming</a> — partition-task 1:1 造成 data skew、parallelism 從 partition 數解耦</li>
<li><a href="/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17 Walmart MPS</a> — 25K+ consumer 在 K8s 的 rebalance storm、consumer 解耦成 stateless service</li>
<li><a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA</a> — consumer lag 驅動 scale-to-zero、backlog signal 取代 resource usage</li>
</ul>
<h3 id="相關連結">相關連結</h3>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a></li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a></li>
<li>下游能力：<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>、<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing 與 recovery 語義</a></li>
</ul>
]]></content:encoded></item><item><title>KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB&lt;/a> overview 的 implementation-layer deep article。選型層（KeyDB vs Redis / DragonflyDB / Valkey、為何選 fork）見 overview；本文只處理「決定用 KeyDB active-active 後，衝突與一致性怎麼判」。命令實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；複製機制以 &lt;a href="https://docs.keydb.dev/docs/active-rep/">KeyDB active-replication 文件&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="兩邊都能寫聽起來太美好">兩邊都能寫，聽起來太美好&lt;/h2>
&lt;p>Redis 的複製是單向的：一個 master 寫、replica 唯讀。要跨區讓兩邊都能就近寫入，Redis 本身做不到（得靠應用層分區或外部工具）。KeyDB 的 active-active 把這個限制拿掉——兩個（含以上）KeyDB 節點都是 master、都能接受寫入、互相把寫入同步給對方。對「兩個 region 都要低延遲寫入同一份 cache」的場景，這聽起來解決了所有問題。&lt;/p>
&lt;p>問題藏在「兩邊同時寫同一個 key」的那一刻。active-active 沒有全域協調者來仲裁誰對誰錯，它用 last-write-wins（LWW）：比較兩筆寫入的時間戳，留下較晚的、默默丟掉較早的。多數時候沒事，但當兩個 region 在幾毫秒內各自更新同一個 key，其中一筆寫入會無聲消失——沒有錯誤、沒有日誌、application 以為自己寫成功了。&lt;/p>
&lt;p>理解 KeyDB active-active 就是理解這個取捨：它用 LWW 換到了「兩邊都能寫」的可用性，代價是放棄了強一致與「不丟寫入」的保證。本文展開複製機制、衝突語意，以及哪些資料放得進這個模型、哪些放進去就是 bug。&lt;/p>
&lt;h2 id="核心概念active-active-的複製與衝突語意">核心概念：active-active 的複製與衝突語意&lt;/h2>
&lt;p>active-active 不是「分散式交易」，它是「雙向非同步複製 + LWW 衝突解決」。理解它要抓三個點：&lt;/p>
&lt;p>&lt;strong>每個節點都是 active-replica&lt;/strong>。一般 Redis replica 是唯讀的；KeyDB 的 active-replica 既接受本地寫入、又接收對方的複製流。兩個節點互相設定對方為 master，形成雙向複製環。實機看到的 role 就是 &lt;code>active-replica&lt;/code>（不是 master / slave）。&lt;/p>
&lt;p>&lt;strong>複製是非同步的&lt;/strong>。本地寫入立即回 OK 給 client，之後才非同步傳給對方節點。這意味著兩個節點之間永遠有一個複製延遲窗口——在這個窗口內，兩邊看到的資料可能不同。這是 active-active 是 AP（可用性 + 分區容忍）而非 CP 的根本原因。&lt;/p>
&lt;p>&lt;strong>衝突用 last-write-wins 解決&lt;/strong>。同一個 key 在兩個節點被並發修改時，KeyDB 比較版本，保留較晚的寫入、丟棄較早的。沒有 merge、沒有 vector clock、沒有 application callback——就是比誰較晚。KeyDB 用 hybrid logical clock（HLC）排序、不是純 wall-clock，但 HLC 仍綁節點實體時鐘——時鐘不同步（clock skew）會直接影響哪一筆被判定為「較晚」。同步的是 key 的「值」不是「操作」，這也是為什麼並發 INCR 會互相覆蓋而非累加（見故障演練 Case 1）。&lt;/p>
&lt;p>&lt;strong>每筆寫入帶來源標記避免無限迴圈&lt;/strong>。A 的寫入同步給 B 後，B 不會再把它當成新寫入傳回 A（否則會無限循環）。KeyDB 用來源標記處理這個，但複製拓樸設計錯（例如環狀多節點）仍可能放大流量。&lt;/p>
&lt;h2 id="配置兩節點-active-active-的設定路徑">配置：兩節點 active-active 的設定路徑&lt;/h2>
&lt;p>實機驗證的最小雙主設定（兩個節點互相複製）：&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"># 節點 A 與 B 都開 active-replica + multi-master&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d --name kdb-a --network kdbnet -p 6401:6379 &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> eqalpha/keydb keydb-server --active-replica yes --multi-master yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">docker run -d --name kdb-b --network kdbnet -p 6402:6379 &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> eqalpha/keydb keydb-server --active-replica yes --multi-master yes
&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="c1"># 互相指向對方（形成雙向複製）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> replicaof kdb-b &lt;span class="m">6379&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> replicaof kdb-a &lt;span class="m">6379&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機驗證雙向同步（最後檢查日 2026-06-16）：&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"># 寫 A、讀 B&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> SET fromA hello &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> GET fromA &lt;span class="c1"># → hello （A 的寫入同步到 B）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 寫 B、讀 A（雙向）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> SET fromB world &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> GET fromB &lt;span class="c1"># → world （B 的寫入同步到 A）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 確認 role 與複製鏈路&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> INFO replication &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;role|master_link_status|connected_slaves&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># role:active-replica&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># master_link_status:up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># connected_slaves:1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個節點都回報 &lt;code>role:active-replica&lt;/code>（不是傳統的 master / slave），&lt;code>master_link_status:up&lt;/code> 確認複製鏈路健康。寫入任一節點、另一節點都讀得到，這就是 active-active 的核心行為。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> overview 的 implementation-layer deep article。選型層（KeyDB vs Redis / DragonflyDB / Valkey、為何選 fork）見 overview；本文只處理「決定用 KeyDB active-active 後，衝突與一致性怎麼判」。命令實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；複製機制以 <a href="https://docs.keydb.dev/docs/active-rep/">KeyDB active-replication 文件</a> 為準。</p></blockquote>
<h2 id="兩邊都能寫聽起來太美好">兩邊都能寫，聽起來太美好</h2>
<p>Redis 的複製是單向的：一個 master 寫、replica 唯讀。要跨區讓兩邊都能就近寫入，Redis 本身做不到（得靠應用層分區或外部工具）。KeyDB 的 active-active 把這個限制拿掉——兩個（含以上）KeyDB 節點都是 master、都能接受寫入、互相把寫入同步給對方。對「兩個 region 都要低延遲寫入同一份 cache」的場景，這聽起來解決了所有問題。</p>
<p>問題藏在「兩邊同時寫同一個 key」的那一刻。active-active 沒有全域協調者來仲裁誰對誰錯，它用 last-write-wins（LWW）：比較兩筆寫入的時間戳，留下較晚的、默默丟掉較早的。多數時候沒事，但當兩個 region 在幾毫秒內各自更新同一個 key，其中一筆寫入會無聲消失——沒有錯誤、沒有日誌、application 以為自己寫成功了。</p>
<p>理解 KeyDB active-active 就是理解這個取捨：它用 LWW 換到了「兩邊都能寫」的可用性，代價是放棄了強一致與「不丟寫入」的保證。本文展開複製機制、衝突語意，以及哪些資料放得進這個模型、哪些放進去就是 bug。</p>
<h2 id="核心概念active-active-的複製與衝突語意">核心概念：active-active 的複製與衝突語意</h2>
<p>active-active 不是「分散式交易」，它是「雙向非同步複製 + LWW 衝突解決」。理解它要抓三個點：</p>
<p><strong>每個節點都是 active-replica</strong>。一般 Redis replica 是唯讀的；KeyDB 的 active-replica 既接受本地寫入、又接收對方的複製流。兩個節點互相設定對方為 master，形成雙向複製環。實機看到的 role 就是 <code>active-replica</code>（不是 master / slave）。</p>
<p><strong>複製是非同步的</strong>。本地寫入立即回 OK 給 client，之後才非同步傳給對方節點。這意味著兩個節點之間永遠有一個複製延遲窗口——在這個窗口內，兩邊看到的資料可能不同。這是 active-active 是 AP（可用性 + 分區容忍）而非 CP 的根本原因。</p>
<p><strong>衝突用 last-write-wins 解決</strong>。同一個 key 在兩個節點被並發修改時，KeyDB 比較版本，保留較晚的寫入、丟棄較早的。沒有 merge、沒有 vector clock、沒有 application callback——就是比誰較晚。KeyDB 用 hybrid logical clock（HLC）排序、不是純 wall-clock，但 HLC 仍綁節點實體時鐘——時鐘不同步（clock skew）會直接影響哪一筆被判定為「較晚」。同步的是 key 的「值」不是「操作」，這也是為什麼並發 INCR 會互相覆蓋而非累加（見故障演練 Case 1）。</p>
<p><strong>每筆寫入帶來源標記避免無限迴圈</strong>。A 的寫入同步給 B 後，B 不會再把它當成新寫入傳回 A（否則會無限循環）。KeyDB 用來源標記處理這個，但複製拓樸設計錯（例如環狀多節點）仍可能放大流量。</p>
<h2 id="配置兩節點-active-active-的設定路徑">配置：兩節點 active-active 的設定路徑</h2>
<p>實機驗證的最小雙主設定（兩個節點互相複製）：</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"># 節點 A 與 B 都開 active-replica + multi-master</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name kdb-a --network kdbnet -p 6401:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --active-replica yes --multi-master yes
</span></span><span class="line"><span class="ln">4</span><span class="cl">docker run -d --name kdb-b --network kdbnet -p 6402:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --active-replica yes --multi-master yes
</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="c1"># 互相指向對方（形成雙向複製）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">keydb-cli -p <span class="m">6401</span> replicaof kdb-b <span class="m">6379</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">keydb-cli -p <span class="m">6402</span> replicaof kdb-a <span class="m">6379</span></span></span></code></pre></div><p>實機驗證雙向同步（最後檢查日 2026-06-16）：</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"># 寫 A、讀 B</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">keydb-cli -p <span class="m">6401</span> SET fromA hello   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">keydb-cli -p <span class="m">6402</span> GET fromA         <span class="c1"># → hello   （A 的寫入同步到 B）</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="c1"># 寫 B、讀 A（雙向）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">keydb-cli -p <span class="m">6402</span> SET fromB world   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">keydb-cli -p <span class="m">6401</span> GET fromB         <span class="c1"># → world   （B 的寫入同步到 A）</span>
</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="c1"># 確認 role 與複製鏈路</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">keydb-cli -p <span class="m">6401</span> INFO replication <span class="p">|</span> grep -E <span class="s2">&#34;role|master_link_status|connected_slaves&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># role:active-replica</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># master_link_status:up</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># connected_slaves:1</span></span></span></code></pre></div><p>兩個節點都回報 <code>role:active-replica</code>（不是傳統的 master / slave），<code>master_link_status:up</code> 確認複製鏈路健康。寫入任一節點、另一節點都讀得到，這就是 active-active 的核心行為。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1並發寫同一-key一筆寫入無聲消失">Case 1：並發寫同一 key、一筆寫入無聲消失</h3>
<p><strong>徵兆</strong>：兩個 region 的 application 各自更新同一個 user 的 cache（例如 profile），事後發現其中一個 region 的更新「沒生效」——但寫入時 application 收到的是 OK，沒有任何錯誤。</p>
<p><strong>根因</strong>：active-active 的 LWW。兩筆寫入在複製延遲窗口內並發發生，KeyDB 比較時間戳保留較晚的、默默丟棄較早的。application 兩邊都以為自己寫成功了（本地確實 OK），但同步後只有一筆存活。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>不要讓同一個 key 被多個 region 並發寫——按 key 分區（user X 的寫入永遠路由到 region A），把多主退化成「就近讀 + 單點寫」</li>
<li>真的需要多點寫的計數器類資料，用 CRDT 語意的結構（KeyDB 的 LWW 不適合 counter，並發 INCR 會互相覆蓋而非累加）</li>
<li>接受 LWW 是 cache 的取捨——可重建的 cache 副本丟一筆寫入可回源重算，不可重建的資料不該放 active-active</li>
<li>衝突無聲是最危險的——加應用層的寫入審計（不靠 KeyDB 告警）</li>
</ol>
<h3 id="case-2clock-skew-讓較晚的判定錯亂">Case 2：clock skew 讓「較晚」的判定錯亂</h3>
<p><strong>徵兆</strong>：明明 region B 後寫的值，最後存活的卻是 region A 先寫的值——LWW 的「後寫者勝」失效。</p>
<p><strong>根因</strong>：LWW 比較時間戳，但兩個節點的系統時鐘若沒同步（clock skew），「較晚」的判定就錯了。B 的時鐘慢了 200ms，B 後寫的值帶的時間戳反而比 A 早，被判定為「較舊」丟棄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>所有 KeyDB 節點強制 NTP 時鐘同步，把 skew 壓到毫秒級</li>
<li>監控節點間的時鐘偏差，skew 超過複製延遲就有 LWW 判定錯亂風險</li>
<li>對時間敏感的衝突，LWW 本質不可靠——時鐘永遠無法完美同步，這是 LWW 模型的固有弱點</li>
<li>需要正確衝突解決的場景，不要用 LWW 的 active-active，改強一致儲存</li>
</ol>
<h3 id="case-3複製延遲下的-stale-read">Case 3：複製延遲下的 stale read</h3>
<p><strong>徵兆</strong>：region A 寫入後，立刻有請求打到 region B 讀同一 key，讀到舊值；幾百毫秒後再讀才是新值。</p>
<p><strong>根因</strong>：active-active 是非同步複製，A 的寫入要經過網路傳到 B 才可見。在這個複製延遲窗口內，B 讀到的是 stale 值。跨 region 的延遲窗口比同 AZ 大得多。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>寫後需要立即一致讀的路徑，讀同一個寫入的節點（read-your-writes 綁定到寫入 region）</li>
<li>監控節點間複製延遲，跨 region 的延遲是 stale window 的下界</li>
<li>接受最終一致——這是 active-active 的本質，cache 場景多數可容忍短暫 stale</li>
<li>不可容忍 stale 的資料不適合 active-active，走單寫入點 + 跨區唯讀 replica</li>
</ol>
<h3 id="case-4複製拓樸設計錯流量放大或迴圈">Case 4：複製拓樸設計錯、流量放大或迴圈</h3>
<p><strong>徵兆</strong>：加了第三個 active 節點組成環狀後，節點間流量異常放大、CPU 升高，甚至同一筆寫入被反覆傳遞。</p>
<p><strong>根因</strong>：active-active 多節點（&gt; 2）的拓樸需要小心設計。全互連（full mesh）下每筆寫入要傳給所有其他節點、流量隨節點數平方成長；環狀拓樸若來源標記處理不當可能放大傳遞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>多節點 active-active 優先用 full mesh 但控制節點數（active-active 不適合大量節點）</li>
<li>監控節點間複製流量，異常放大代表拓樸或來源標記問題</li>
<li>大規模多區優先考慮「每區單寫入點 + 跨區唯讀」而非全 active-active</li>
<li>active-active 的甜蜜點是 2-3 個區的雙向就近寫，不是大規模 mesh</li>
</ol>
<h3 id="case-5節點重連後的全量重同步衝擊">Case 5：節點重連後的全量重同步衝擊</h3>
<p><strong>徵兆</strong>：一個節點短暫斷線後重連，重連瞬間 CPU / 網路尖峰，期間延遲升高。</p>
<p><strong>根因</strong>：節點斷線時間過長、超過複製 backlog 能覆蓋的範圍，重連時要做全量重同步（full resync）——對方節點要產生快照（fork、見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence 的 fork 成本</a>，KeyDB 繼承 Redis 的 fork 機制）並傳輸整個 dataset。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設足夠大的 <code>repl-backlog-size</code>，讓短暫斷線走部分同步（partial resync）而非全量</li>
<li>重同步的 fork 成本跟記憶體 headroom 相關，節點要留 fork 空間</li>
<li>監控 <code>master_link_status</code>，頻繁 down / up 代表網路不穩、要先修網路</li>
<li>跨 region 的 active-active 對網路穩定性敏感，不穩的鏈路會頻繁觸發重同步</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>active-active 的容量判讀，核心在衝突率與複製健康：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同 key 跨節點並發寫入率</td>
          <td>接近 0（key 按區分區）</td>
          <td>高 → LWW 丟寫入風險、改 key 分區</td>
      </tr>
      <tr>
          <td>節點間 clock skew</td>
          <td>&lt; 複製延遲（毫秒級）</td>
          <td>大 → LWW 判定錯亂、強制 NTP</td>
      </tr>
      <tr>
          <td>節點間複製延遲</td>
          <td>跨 region 可接受的 stale 窗</td>
          <td>過大 → stale read 嚴重、檢查網路</td>
      </tr>
      <tr>
          <td><code>master_link_status</code></td>
          <td><code>up</code></td>
          <td>頻繁 down → 網路不穩、會觸發重同步</td>
      </tr>
      <tr>
          <td>active 節點數</td>
          <td>2-3（雙向就近寫）</td>
          <td>過多 → mesh 流量平方成長、改單寫入點拓樸</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要正確的衝突解決 / 不能丟寫入</strong>：LWW 不保證，走強一致儲存（<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a> 的 multi-region 一致性方案）或單寫入點架構。</li>
<li><strong>需要 counter / 累加語意的多點寫</strong>：LWW 會讓並發 INCR 互相覆蓋，KeyDB active-active 不適合，改 CRDT 或單點 counter。</li>
<li><strong>跨 region 但可接受單寫入點</strong>：用 Redis / Valkey 的單向複製（一區寫、其他區唯讀），比 active-active 簡單且無衝突。</li>
<li><strong>大規模多區</strong>：active-active 的甜蜜點是 2-3 區，更大規模走 managed 的跨區方案（<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache Global Datastore</a> 的 active-passive）。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>active-active 是 KeyDB 區別於 Redis 的核心能力之一，但它的取捨跨多個子系統：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB overview</a></strong>：overview 點到 active-active 是 last-write-wins、本文展開它什麼時候默默丟資料。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence / fork latency</a></strong>：KeyDB 繼承 Redis 的 fork 機制，節點重連的全量重同步付 fork 成本。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></strong>：active-active 的 stale window 與 LWW 丟寫入，本質是「cache 副本的新鮮度與一致性邊界」議題的多主版本。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap KeyDB cross-cloud case</a></strong>：Snap 用 KeyDB 的主因是 cross-cloud latency 治理（cache 與 application 共置），active-active 的雙向就近寫是這類 multi-cloud 場景的工具，但要按 key 分區避開 LWW 衝突。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/" data-link-title="DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster" data-link-desc="Redis 要靠 Cluster 分片才能用滿一台多核機器，DragonflyDB 賭的是相反方向——單一進程 thread-per-core、shared-nothing、把單機推到 Redis 要好幾個 shard 才達到的規模。本文展開 thread-per-core 與 dashtable 的架構、fork-less snapshot、5 個把架構假設寫成 production 事故的踩坑，以及 scale-up 撞牆該回 Cluster 的邊界">DragonflyDB 多核架構</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover</a>（單向複製的 HA）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Memcached slab allocator 與記憶體經濟學：明明有記憶體卻在 evict</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 &lt;code>memcached:1.6&lt;/code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 &lt;a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict&lt;/h2>
&lt;p>Memcached 最違反直覺的故障是這樣：監控顯示 &lt;code>evictions&lt;/code> 持續上升、hit rate 在掉，但 &lt;code>stats&lt;/code> 算下來實際用掉的記憶體遠低於 &lt;code>-m&lt;/code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。&lt;/p>
&lt;p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。&lt;/p>
&lt;p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。&lt;/p>
&lt;h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型&lt;/h2>
&lt;p>Memcached 啟動時不會把 &lt;code>-m&lt;/code> 指定的記憶體一次配掉，而是按需求以 &lt;strong>page&lt;/strong>（預設 1MB）為單位分配給 &lt;strong>slab class&lt;/strong>，每個 class 存放某個大小區間的 item。&lt;/p>
&lt;p>&lt;strong>slab class 與 chunk size&lt;/strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 &lt;code>growth_factor&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="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;stats settings\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep growth_factor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT growth_factor 1.25&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="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;chunk_size|active_slabs&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 class="c1"># STAT 1:chunk_size 96 ← 最小的 slab class、chunk 96 bytes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT active_slabs 1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>growth_factor 1.25&lt;/code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。&lt;/p>
&lt;p>&lt;strong>page 分配是單向的&lt;/strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。&lt;code>-o slab_automove&lt;/code> 與手動 &lt;code>slabs reassign&lt;/code> 可以把 page 在 class 間搬移，但預設行為偏保守。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 <code>memcached:1.6</code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 <a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki</a> 為準。</p></blockquote>
<h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict</h2>
<p>Memcached 最違反直覺的故障是這樣：監控顯示 <code>evictions</code> 持續上升、hit rate 在掉，但 <code>stats</code> 算下來實際用掉的記憶體遠低於 <code>-m</code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。</p>
<p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。</p>
<p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。</p>
<h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型</h2>
<p>Memcached 啟動時不會把 <code>-m</code> 指定的記憶體一次配掉，而是按需求以 <strong>page</strong>（預設 1MB）為單位分配給 <strong>slab class</strong>，每個 class 存放某個大小區間的 item。</p>
<p><strong>slab class 與 chunk size</strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 <code>growth_factor</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="nb">printf</span> <span class="s1">&#39;stats settings\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep growth_factor
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># STAT growth_factor 1.25</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="nb">printf</span> <span class="s1">&#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;chunk_size|active_slabs&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># STAT 1:chunk_size 96      ← 最小的 slab class、chunk 96 bytes</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># STAT active_slabs 1</span></span></span></code></pre></div><p><code>growth_factor 1.25</code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。</p>
<p><strong>page 分配是單向的</strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。<code>-o slab_automove</code> 與手動 <code>slabs reassign</code> 可以把 page 在 class 間搬移，但預設行為偏保守。</p>
<p><strong>LRU 是 per-slab-class 的</strong>。淘汰不是全域的，是每個 slab class 維護自己的 LRU。所以「class 2 滿了開始淘汰、但 class 5 有空閒 page」是正常現象——淘汰看的是該 class 自己的空間，不是全域記憶體。</p>
<p>這三點合起來解釋了開頭的悖論：evict 發生在某個 class 內，跟全域剩餘記憶體無關。</p>
<h2 id="配置slab-與多執行緒的設定路徑">配置：slab 與多執行緒的設定路徑</h2>





<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"># 啟動參數（Memcached 的調校多在啟動參數、不像 Redis 有大量 runtime CONFIG SET）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  memcached <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>    -m <span class="m">1024</span> <span class="se">\ </span>         <span class="c1"># 記憶體上限 1024 MB</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    -t <span class="m">4</span> <span class="se">\ </span>            <span class="c1"># worker thread 數（多執行緒、對齊 CPU 核數）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    -f 1.25 <span class="se">\ </span>         <span class="c1"># slab growth factor（預設 1.25、調小→class 更密集→浪費更少但 class 更多）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    -I 2m <span class="se">\ </span>           <span class="c1"># 單一 item 大小上限（預設 1MB、超過要調大或拆 value）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    -o <span class="nv">slab_automove</span><span class="o">=</span><span class="m">1</span> <span class="c1"># 自動把空閒 page 從一個 class 搬到吃緊的 class（緩解 calcification）</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>-m</code> 是給 item 資料的上限，Memcached 自身的 hash table、連線 buffer 等 overhead 在 <code>-m</code> 之外，機器要留 headroom</li>
<li><code>-t</code> 對齊 CPU 核數——Memcached 從早期就是 multi-threaded，這是它跟早期單執行緒 Redis 的核心差異</li>
<li><code>-f</code> 調小（例如 1.08）讓 slab class 更密集、向上取整浪費更少，代價是 class 數變多、管理開銷略增</li>
<li><code>-I</code> 是單 item 上限，超過會 store 失敗（見故障演練 Case 3）</li>
<li><code>slab_automove=1</code> 是緩解 calcification 的關鍵，預設視版本而定，明確開啟較穩</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1slab-calcificationvalue-大小漂移造成假性記憶體不足">Case 1：slab calcification——value 大小漂移造成假性記憶體不足</h3>
<p><strong>徵兆</strong>：<code>evictions</code> 上升、hit rate 下降，但 <code>stats</code> 顯示 <code>bytes</code> 遠低於 <code>limit_maxbytes</code>。<code>stats slabs</code> 看到某個 class 的 page 用滿在淘汰，另一個 class 有大量空閒 chunk。</p>
<p><strong>根因</strong>：value 大小分布隨時間漂移。早期 value 小、記憶體被分配給小 slab class；後來 value 變大、需要大 class，但 page 已被小 class 鎖住不還，大 class 空間不足開始淘汰。整體記憶體沒滿，但「對的 class」沒空間。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>開 <code>-o slab_automove=1</code>，讓 Memcached 自動把空閒 page 從冷 class 搬到吃緊的 class</li>
<li>手動觸發搬移：<code>slabs reassign &lt;src_class&gt; &lt;dst_class&gt;</code>（緊急救火用）</li>
<li>監控 <code>stats slabs</code> 各 class 的 <code>used_chunks</code> vs <code>total_chunks</code> 與 <code>stats items</code> 的 per-class evicted，找出失衡的 class</li>
<li>從源頭穩定 value 大小分布——序列化格式統一、避免同類資料時大時小</li>
</ol>
<h3 id="case-2chunk-向上取整浪費大量記憶體">Case 2：chunk 向上取整浪費大量記憶體</h3>
<p><strong>徵兆</strong>：存的 value 總大小算起來只有 600MB，但 Memcached 報用掉接近 1GB，記憶體效率異常低。</p>
<p><strong>根因</strong>：value 大小剛好落在 slab class chunk size 的「上緣之外」，被向上取整到下一個更大的 class，每個 item 浪費接近一個 growth step 的空間。例如大量 130 bytes 的 value 被放進 152 bytes 的 class，每個浪費 22 bytes，量大就顯著。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-f</code> 調小（1.25 → 1.08）讓 class 粒度更細，向上取整的浪費變小</li>
<li><code>stats slabs</code> 看主要 class 的 <code>chunk_size</code> 跟你的 value 實際大小差多少，量化浪費</li>
<li>value 設計上靠近 chunk 邊界（例如壓縮或裁剪 metadata 讓 value 剛好塞進較小的 class）</li>
<li>浪費是 slab 模型的固有成本，純 KV 的 trade-off——換到的是永不碎片化與 O(1) 分配</li>
</ol>
<h3 id="case-3value-超過-item-大小上限store-直接失敗">Case 3：value 超過 item 大小上限、store 直接失敗</h3>
<p><strong>徵兆</strong>：某些大 value 的寫入回 <code>SERVER_ERROR object too large for cache</code>，application 端 cache 寫入靜默失敗、之後一直 miss。</p>
<p><strong>根因</strong>：單一 item 超過 <code>-I</code> 設的上限（預設 1MB）。Memcached 設計上不適合存大 object，預設 1MB 是刻意的純 cache 邊界。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 value 大小分布，大 value 是否真該進 Memcached（純 KV cache 不適合大 blob）</li>
<li>必要時調大 <code>-I</code>（例如 <code>-I 2m</code>），但這會改變 slab class 結構、增加大 chunk 的記憶體佔用</li>
<li>大 object 考慮壓縮、或拆成多個小 key、或改放適合的儲存（物件儲存 / <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> 的 hash）</li>
<li>application 端要處理 store 失敗，不要假設 set 一定成功——失敗就走 origin</li>
</ol>
<h3 id="case-4thread-數設太高lock-contention-反而拖慢">Case 4：thread 數設太高、lock contention 反而拖慢</h3>
<p><strong>徵兆</strong>：把 <code>-t</code> 從 4 調到 32 想榨多核效能，throughput 沒升反降，CPU 在 system time 飆高。</p>
<p><strong>根因</strong>：Memcached 的多執行緒有 per-item lock（hash bucket lock），thread 數遠超核數時，執行緒互搶 lock 與 CPU、context switch 開銷超過平行收益。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-t</code> 對齊實體核數，不要超配（多數場景 4-8 已足夠，極高核機器再往上調並壓測）</li>
<li>用實際 workload 壓測對比不同 <code>-t</code> 的 throughput，找拐點</li>
<li>hot key 集中時 lock contention 更明顯（同 bucket），這是資料分布問題不是 thread 數問題</li>
<li>跨機器水平擴展（client-side consistent hashing）比單機堆 thread 更能解規模，見本文整合段</li>
</ol>
<h3 id="case-5連線數打到上限新連線被拒">Case 5：連線數打到上限、新連線被拒</h3>
<p><strong>徵兆</strong>：高並發下新連線報錯或 hang，<code>stats</code> 的 <code>curr_connections</code> 接近 <code>max_connections</code>，<code>listen_disabled_num</code> 在增加。</p>
<p><strong>根因</strong>：每個 client 連線佔一個 connection slot，Memcached 預設 <code>-c 1024</code>。大量 client（尤其沒用連線池、每請求建連）會打滿 connection 上限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 端用連線池重用連線，不要每請求建連</li>
<li>調高 <code>-c</code>（例如 <code>-c 4096</code>），但連線本身有記憶體 overhead（在 <code>-m</code> 之外），要算進機器容量</li>
<li>監控 <code>curr_connections</code> 與 <code>listen_disabled_num</code>，後者非零代表曾達上限拒絕連線</li>
<li>連線數爆炸常是 client fan-out 問題，跨多 Memcached node 分散（consistent hashing）能攤平單 node 連線壓力</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Memcached 的容量判讀，核心在 slab 效率與多執行緒擴展：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>evictions</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高但記憶體沒滿 → calcification、開 slab_automove</td>
      </tr>
      <tr>
          <td>各 class <code>used / total chunks</code></td>
          <td>各 class 均衡</td>
          <td>單 class 滿、其他空 → calcification</td>
      </tr>
      <tr>
          <td>chunk 向上取整浪費</td>
          <td>小（value 貼近 chunk size）</td>
          <td>大 → 調小 <code>-f</code> 或調整 value 大小</td>
      </tr>
      <tr>
          <td><code>curr_connections / -c</code></td>
          <td>&lt; 80%</td>
          <td>接近上限 → 用連線池或調高 <code>-c</code></td>
      </tr>
      <tr>
          <td>多執行緒 CPU</td>
          <td>核數內、system time 低</td>
          <td>system time 高 → <code>-t</code> 超配、lock contention</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 data types / 持久化 / distributed lock</strong>：Memcached 是純 KV、刻意不做這些。需要這些走 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a>，這是 capability 差異不是調校能補。</li>
<li><strong>單機容量 / throughput 不夠</strong>：Memcached 沒有 server-side cluster，靠 client-side consistent hashing（ketama）水平擴展到多 node，見整合。</li>
<li><strong>想要 Memcached 的多執行緒 + Redis 的 data types</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 兼具多核與 Redis 相容，是兩者的中間點。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Memcached 的單機很簡單，它的工程深度在「如何把多個 Memcached node 組成一個 cache 層」——而這發生在 client 端與代理層，不在 server：</p>
<ul>
<li><strong>client-side consistent hashing（ketama）</strong>：Memcached server 之間互不知道彼此，sharding 由 client library 用 consistent hashing 決定 key 去哪個 node，加減 node 時最小化 key 重新分布。這是 Memcached 水平擴展的基礎。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">Meta mcrouter</a></strong>：Meta 的 mcrouter 是 Memcached 專屬的 protocol-aware routing proxy，把跨叢集 / 跨區的流量收斂、失效隔離、pool 管理從 client 端移到代理層——這是 Memcached 大規模治理的標準答案。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 基於 Memcached，Netflix 在上面加跨 AZ replication 與 client-side smart routing，補足 Memcached 沒有的跨區 HA。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">Meta TAO</a></strong>：TAO 底層用 Memcached 作為 social graph 的 cache 層，上層加一致性與關聯查詢——展示了純 KV 之上如何疊加語意。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">Meta CacheLib + Kangaroo</a></strong>：當 DRAM 的記憶體經濟撞到極限，Meta 用 CacheLib 把 cache 分層到 flash——這是 slab 記憶體經濟學的下一個邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a>（jemalloc 池 vs slab class 的差異）、<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>NATS core 到 JetStream：fire-and-forget 在哪裡不夠、跨過去要付什麼</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS&lt;/a> overview 的 implementation-layer deep article、定位在「要不要從 core NATS 跨進 JetStream」的決策入口。選型層（NATS vs Kafka / RabbitMQ）見 overview；本文只處理 core 與 JetStream 的邊界與基本 consumer 設定。決定採用 JetStream 後的完整實作（stream / consumer 每個旋鈕、跨區拓樸、多租戶）見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster / leaf node&lt;/a>。JetStream 實機驗證於 nats:latest（-js）、最後檢查日 2026-06-16；機制以 &lt;a href="https://docs.nats.io/nats-concepts/jetstream">NATS JetStream 官方文件&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="fire-and-forget-在-rolling-deploy-那一刻掉訊息">fire-and-forget 在 rolling deploy 那一刻掉訊息&lt;/h2>
&lt;p>Core NATS 的低延遲來自它什麼都不記——一則訊息發布出去，當下有訂閱者就送達、沒有就丟棄。沒有儲存、沒有 ack、沒有重送。這適合「即時但可丟」的場景（metrics、presence、即時通知）：訂閱者暫時離線錯過幾則無所謂，下一則馬上來。&lt;/p>
&lt;p>但這個設計有一條清楚的邊界。&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&amp;#43; 訊息 100% uptime。">Clarifai 用 NATS 跑 ML 模型訓練的非同步任務&lt;/a>，任務從幾秒到幾分鐘，原本同步呼叫——結果每次 rolling deployment（pod 輪流重啟）就掉訊息：訊息發布的瞬間目標 worker 正在重啟，core NATS 找不到訂閱者就丟了。他們的解法是改用 NATS（當時是 NATS Streaming、JetStream 的前身）的 &lt;strong>at-least-once delivery + redelivery + queue group&lt;/strong>，每日 100k+ 訊息、達成 100% uptime。這個案例揭露的邊界是——&lt;strong>ML 長尾任務不能容忍 rolling deploy 掉訊息，core NATS 的 fire-and-forget 到此為止，要跨進 JetStream。&lt;/strong>&lt;/p>
&lt;p>JetStream 在 core NATS 之上加了一層持久化的 stream + 可重送的 consumer。本文處理這條邊界：什麼時候 core 夠用、什麼時候要 JetStream、跨過去的 consumer 模型怎麼設才不會丟訊息或重投風暴。&lt;/p>
&lt;h2 id="核心概念stream-與-consumer-的求值模型">核心概念：stream 與 consumer 的求值模型&lt;/h2>
&lt;p>JetStream 把「訊息儲存」跟「消費進度」拆成兩個獨立物件——stream（存什麼、留多久）跟 consumer（誰讀、怎麼 ack）。理解 JetStream 就是理解這兩者。&lt;/p>
&lt;p>&lt;strong>stream 決定訊息怎麼被儲存與保留&lt;/strong>。一個 stream 綁定一組 subject、把符合的訊息持久化。三個關鍵維度：storage（&lt;code>file&lt;/code> 持久 / &lt;code>memory&lt;/code> 重啟即失）、retention（&lt;code>limits&lt;/code> 依大小/時間/數量保留、&lt;code>workqueue&lt;/code> 消費後即刪、&lt;code>interest&lt;/code> 有訂閱者才留）、limits（max-msgs / max-bytes / max-age）。retention 選錯是常見陷阱——&lt;code>workqueue&lt;/code> 是「每則訊息只被一個 consumer 消費一次就刪」，&lt;code>limits&lt;/code> 是「保留著、多個 consumer 各自讀」。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> overview 的 implementation-layer deep article、定位在「要不要從 core NATS 跨進 JetStream」的決策入口。選型層（NATS vs Kafka / RabbitMQ）見 overview；本文只處理 core 與 JetStream 的邊界與基本 consumer 設定。決定採用 JetStream 後的完整實作（stream / consumer 每個旋鈕、跨區拓樸、多租戶）見 <a href="/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster / leaf node</a>。JetStream 實機驗證於 nats:latest（-js）、最後檢查日 2026-06-16；機制以 <a href="https://docs.nats.io/nats-concepts/jetstream">NATS JetStream 官方文件</a> 為準。</p></blockquote>
<h2 id="fire-and-forget-在-rolling-deploy-那一刻掉訊息">fire-and-forget 在 rolling deploy 那一刻掉訊息</h2>
<p>Core NATS 的低延遲來自它什麼都不記——一則訊息發布出去，當下有訂閱者就送達、沒有就丟棄。沒有儲存、沒有 ack、沒有重送。這適合「即時但可丟」的場景（metrics、presence、即時通知）：訂閱者暫時離線錯過幾則無所謂，下一則馬上來。</p>
<p>但這個設計有一條清楚的邊界。<a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">Clarifai 用 NATS 跑 ML 模型訓練的非同步任務</a>，任務從幾秒到幾分鐘，原本同步呼叫——結果每次 rolling deployment（pod 輪流重啟）就掉訊息：訊息發布的瞬間目標 worker 正在重啟，core NATS 找不到訂閱者就丟了。他們的解法是改用 NATS（當時是 NATS Streaming、JetStream 的前身）的 <strong>at-least-once delivery + redelivery + queue group</strong>，每日 100k+ 訊息、達成 100% uptime。這個案例揭露的邊界是——<strong>ML 長尾任務不能容忍 rolling deploy 掉訊息，core NATS 的 fire-and-forget 到此為止，要跨進 JetStream。</strong></p>
<p>JetStream 在 core NATS 之上加了一層持久化的 stream + 可重送的 consumer。本文處理這條邊界：什麼時候 core 夠用、什麼時候要 JetStream、跨過去的 consumer 模型怎麼設才不會丟訊息或重投風暴。</p>
<h2 id="核心概念stream-與-consumer-的求值模型">核心概念：stream 與 consumer 的求值模型</h2>
<p>JetStream 把「訊息儲存」跟「消費進度」拆成兩個獨立物件——stream（存什麼、留多久）跟 consumer（誰讀、怎麼 ack）。理解 JetStream 就是理解這兩者。</p>
<p><strong>stream 決定訊息怎麼被儲存與保留</strong>。一個 stream 綁定一組 subject、把符合的訊息持久化。三個關鍵維度：storage（<code>file</code> 持久 / <code>memory</code> 重啟即失）、retention（<code>limits</code> 依大小/時間/數量保留、<code>workqueue</code> 消費後即刪、<code>interest</code> 有訂閱者才留）、limits（max-msgs / max-bytes / max-age）。retention 選錯是常見陷阱——<code>workqueue</code> 是「每則訊息只被一個 consumer 消費一次就刪」，<code>limits</code> 是「保留著、多個 consumer 各自讀」。</p>
<p><strong>consumer 是 stream 上的一個可重播視圖</strong>。同一個 stream 可以有多個 consumer，各自維護自己的消費位置。consumer 的關鍵屬性：</p>
<ul>
<li>push vs pull：push 由 server 主動推給訂閱者；pull 由 client 主動拉（<code>consumer next</code>），pull 對流量控制與 worker pool 更可控</li>
<li>durable vs ephemeral：durable consumer 的進度持久（重啟後從上次位置續讀），ephemeral 在 client 斷線後消失（進度丟失）</li>
<li>ack policy：<code>explicit</code>（每則都要 ack、at-least-once 的基礎）/ <code>all</code>（ack 一則等於 ack 之前所有）/ <code>none</code>（不需 ack、近似 fire-and-forget）</li>
<li>max_deliver + ack_wait：沒 ack 的訊息在 <code>ack_wait</code> 後重送，最多 <code>max_deliver</code> 次</li>
</ul>
<p><strong>at-least-once 來自「explicit ack + redelivery」</strong>。consumer 取出訊息、處理、明確 ack；沒 ack（處理失敗或 crash）的訊息在 ack_wait 逾時後重送。這就是 Clarifai 要的「rolling deploy 不丟訊息」——worker 重啟時沒 ack 的任務會被重送給其他 worker。</p>
<h2 id="配置durable-pull-consumer實機驗證">配置：durable pull consumer（實機驗證）</h2>





<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"># 啟動 JetStream（server 加 -js）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># docker run -d --name nats nats:latest -js</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="c1"># 1. 建 stream：file storage、limits retention</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">nats stream add ORDERS --subjects <span class="s2">&#34;orders.&gt;&#34;</span> --storage file --defaults
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#   Subjects: orders.&gt;   Storage: File   Retention: Limits   Replicas: 1</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. publish</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">nats pub orders.new <span class="s2">&#34;order-1&#34;</span>   <span class="c1"># Published 7 bytes to &#34;orders.new&#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="c1"># 3. stream info 確認持久化</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">nats stream info ORDERS
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">#   Storage: File   Messages: 3   Bytes: 141 B   ← 訊息已落盤、consumer 重啟不丟</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 4. durable pull consumer（explicit ack、可重送）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">nats consumer add ORDERS workers --pull --ack explicit --deliver all --defaults
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">#   Pull Mode: true   Ack Policy: Explicit</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 5. 拉取消費（worker pool 多個實例共用同一 durable consumer = queue group 語意）</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">nats consumer next ORDERS workers --count <span class="m">3</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">#   order-1  order-2  order-3</span></span></span></code></pre></div><p>實機驗證於 nats:latest（最後檢查日 2026-06-16）：file storage 的 stream 把訊息落盤（Messages: 3）、durable pull consumer 用 explicit ack 消費。多個 worker 連到同一個 durable pull consumer 形成 worker pool（訊息分給其中一個），這正是 Clarifai 的 queue group 模式。</p>
<p>判讀：</p>
<ul>
<li>worker pool 用同一個 durable pull consumer（共享進度、訊息分流），不是每個 worker 一個 consumer</li>
<li><code>--ack explicit</code> 是 at-least-once 的前提；處理成功才 ack</li>
<li>pull 模式比 push 對 worker pool 更可控（worker 按自己能力拉、不會被 push 淹）</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用-core-nats-跑該持久的任務rolling-deploy-掉訊息">Case 1：用 core NATS 跑該持久的任務、rolling deploy 掉訊息</h3>
<p><strong>徵兆</strong>：平時正常，但每次部署（pod 輪流重啟）就有一批任務消失、沒有錯誤。</p>
<p><strong>根因</strong>：用 core NATS（fire-and-forget）跑需要可靠處理的任務。發布瞬間目標訂閱者正在重啟，core NATS 找不到訂閱者就丟棄——這是 core 的設計，不是故障。正是 <a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">Clarifai 的原始問題</a>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要不丟的任務用 JetStream（持久 stream + durable consumer + explicit ack）</li>
<li>訊息落盤後 consumer 重啟從上次位置續讀，rolling deploy 不丟</li>
<li>釐清邊界：可丟的即時資料（metrics / presence）留 core NATS、不可丟的跨 JetStream</li>
<li>不要用 core NATS 當任務隊列——它沒有持久化與重送</li>
</ol>
<h3 id="case-2ephemeral-consumer-斷線消費進度全丟">Case 2：ephemeral consumer 斷線、消費進度全丟</h3>
<p><strong>徵兆</strong>：consumer 重連後從頭重讀整個 stream、或漏掉斷線期間的訊息，進度不連續。</p>
<p><strong>根因</strong>：用了 ephemeral consumer——它的進度不持久，client 斷線後 consumer 本身消失。重連是建一個全新 consumer，從 <code>deliver</code> policy 的起點開始（all 從頭、new 只看新的），不接續之前的進度。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要跨重啟接續的用 durable consumer（具名、進度持久）</li>
<li>ephemeral 只適合臨時、一次性的讀取（debug、一次性掃描）</li>
<li>worker pool 一定用 durable（多 worker 共享持久進度）</li>
<li>確認 <code>deliver</code> policy（all / new / last）符合預期的起讀位置</li>
</ol>
<h3 id="case-3ack_wait-太短處理還沒完就重送風暴">Case 3：ack_wait 太短、處理還沒完就重送風暴</h3>
<p><strong>徵兆</strong>：長任務還在處理中就被重送給另一個 worker，同一任務被多個 worker 重複執行，負載放大。</p>
<p><strong>根因</strong>：<code>ack_wait</code>（等 ack 的逾時）設得比任務處理時間短。JetStream 以為訊息處理失敗（沒在 ack_wait 內 ack），重送給別人——但其實第一個 worker 還在跑。ML 長尾任務（幾秒到幾分鐘）特別容易踩。</p>
<p><strong>修法（本文層級的判讀）</strong>：ack_wait 必須涵蓋任務的 p99 處理時間，否則長任務會在處理中被重送。設值方法（量測 p99、長任務用 in-progress ack 延長 deadline、消費端冪等兜底）與實機重現（AckWait 設 1s 觀察 tries 1→2、Redelivered 計數）在 <a href="/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster/leaf node</a> 的故障演練有完整步驟，採用 JetStream 後依該篇落地。</p>
<h3 id="case-4retention-選-workqueue-但想多-consumer-fanout">Case 4：retention 選 workqueue 但想多 consumer fanout</h3>
<p><strong>徵兆</strong>：想讓多個獨立服務各自消費同一 stream，但發現訊息被一個消費掉就消失、其他服務讀不到。</p>
<p><strong>根因</strong>：stream retention 設成 <code>workqueue</code>——每則訊息只被消費一次就從 stream 刪除（隊列語意）。它不適合 fanout（多個 consumer 各自要完整一份）。fanout 要 <code>limits</code> 或 <code>interest</code> retention。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>fanout（多服務各讀一份）用 <code>limits</code> retention（訊息保留、多 consumer 各自 offset）</li>
<li>單一 worker pool 競爭消費用 <code>workqueue</code>（消費即刪、省空間）</li>
<li>釐清需求：競爭消費（worker pool）vs 廣播消費（fanout）對應不同 retention</li>
<li>Clarifai 用「3 個獨立 NATS 實例做 fanout 隔離」是另一種 fanout 做法，按隔離需求選</li>
</ol>
<h3 id="case-5memory-storage-的-stream-重啟全失">Case 5：memory storage 的 stream 重啟全失</h3>
<p><strong>徵兆</strong>：broker 重啟後 stream 裡的訊息全沒了，consumer 從空的開始。</p>
<p><strong>根因</strong>：stream storage 設成 <code>memory</code>——快但不持久，broker 重啟即失。誤把它當持久 stream 用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要持久的 stream 用 <code>file</code> storage（落盤、重啟不丟，實機驗證過）</li>
<li><code>memory</code> 只適合「快取式、可重建」的 stream（如即時聚合的中間狀態）</li>
<li>要更高可靠性加 <code>replicas</code>（JetStream 用 Raft 跨節點複製 stream）</li>
<li>容量規劃時 file storage 的磁碟與 memory 的 RAM 是不同維度</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>JetStream 的容量判讀：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>stream storage 用量</td>
          <td>在 max-bytes / max-age 內</td>
          <td>接近上限 → 訊息被 discard、調 limits 或加容量</td>
      </tr>
      <tr>
          <td>redelivery 次數</td>
          <td>低（多數一次 ack 成功）</td>
          <td>高 → ack_wait 太短或處理卡住</td>
      </tr>
      <tr>
          <td>consumer pending</td>
          <td>可消化</td>
          <td>持續堆高 → consumer 跟不上 producer</td>
      </tr>
      <tr>
          <td>ack_wait vs 處理時間</td>
          <td>ack_wait &gt; p99 處理時間</td>
          <td>反了 → 重送風暴</td>
      </tr>
      <tr>
          <td>storage 型別</td>
          <td>持久需求用 file</td>
          <td>誤用 memory → 重啟丟訊息</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>可丟的即時資料</strong>：不需要 JetStream 的持久化開銷，用 core NATS（更快更輕）。</li>
<li><strong>超大吞吐 + 長期保留 + 複雜 replay</strong>：JetStream 適合中等規模可靠 messaging；超大規模 event streaming + 長期保留走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（log-based、生態成熟）。</li>
<li><strong>複雜 routing / 任務隊列語意</strong>：JetStream 的 subject 是樹狀，複雜 routing + DLQ 拓樸用 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> 更直接。</li>
<li><strong>不想自管</strong>：NATS 的 managed 選項（Synadia Cloud）或其他 managed broker。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>JetStream 的邊界判斷是 NATS 使用的核心，它跟其他議題交織：</p>
<ul>
<li><strong>跟 <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 design</a></strong>：push/pull、durable/ephemeral、ack policy 是 consumer 設計的具體選項。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></strong>：JetStream 的 file storage stream 是 NATS 的 durable queue 實現。</li>
<li><strong>跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></strong>：at-least-once + redelivery 要求消費冪等，否則重送造成重複副作用。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ deep article</a></strong>：max_deliver 達上限後的處理對應 RabbitMQ 的 DLQ，兩者都是「重試上限後往哪去」的問題。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>對照 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ 與分層 retry</a>、<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>對應案例：<a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">3.C38 Clarifai NATS ML 非同步任務</a></li>
<li>上游概念：<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 design</a>、<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></li>
</ul>
]]></content:encoded></item><item><title>Pub/Sub Ordering Key、Dead-Letter Topic 與 Schema Enforcement：三道交付治理</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/ordering-dlt-schema/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/ordering-dlt-schema/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/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 Cloud Pub/Sub&lt;/a> overview 的 implementation-layer deep article。Overview 回答「Pub/Sub 該不該選、跟 Kafka / SQS 差在哪」；本文回答「ordering key 怎麼設、DLT 怎麼擋 poison message、schema 怎麼守契約，各自踩哪些坑」。閱讀前可先讀 overview 的 ordering / DLT / schema 各段建立 context。&lt;/p>
&lt;p>文中 gcloud 指令的語法以 Pub/Sub emulator 實機驗證（topic / subscription / schema / ordering key / DLT / push 各操作均跑通），標準版的雲端配額、IAM 與計費行為依官方文件。&lt;/p>&lt;/blockquote>
&lt;h2 id="三道治理共用同一個交付骨架">三道治理共用同一個交付骨架&lt;/h2>
&lt;p>Pub/Sub 的 ordering key、dead-letter topic、schema enforcement 看似三個獨立功能，實際都掛在同一個交付骨架上：subscription 是消費進度的 first-class 抽象、訊息經 ackDeadline 控制重投、失敗訊息經投遞次數計數決定去留。理解這個骨架之後，三道治理只是骨架上的三個切面 — ordering 切的是「投遞順序」、DLT 切的是「投遞次數上限」、schema 切的是「投遞前的內容守門」。&lt;/p>
&lt;p>這條骨架跟 Kafka 思路不同。Kafka 的消費進度綁在 consumer group + partition offset；Pub/Sub 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 是 first-class，subscription 才是 consumer 抽象，一個 topic 可以掛 N 個 subscription、各自有獨立進度與獨立的 ackDeadline / DLT / ordering 設定。同一份 event 流，A subscription 可以開 ordering 嚴格有序、B subscription 可以不開 ordering 換吞吐，互不影響。&lt;/p>
&lt;p>把這三道治理寫進一篇的理由是：它們在 production 會互相牽制。Ordering key 開了之後 DLT 的隔離行為會變（有序流裡一則 poison message 會卡住整把 key 的後續訊息）；schema enforcement 擋下的不相容 publish 不會進 DLT（根本沒進 topic）。分開讀三個官方頁面看不到這層耦合。&lt;/p>
&lt;h2 id="subscription-是-first-classackdeadline-與-extension">subscription 是 first-class：ackDeadline 與 extension&lt;/h2>
&lt;p>subscription 承擔「這個消費者讀到哪、還有多少沒 ack」的責任。每則訊息投遞給 subscriber 後，Pub/Sub 啟動一個 ackDeadline 倒數；倒數內收到 ack 就移除訊息、倒數結束沒收到 ack 就重投。預設 ackDeadline 是 10 秒、上限 600 秒。&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"># subscription 的 ackDeadline 預設 10 秒、retention 預設 7 天&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">gcloud pubsub subscriptions describe demo-sub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># ackDeadlineSeconds: 10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># messageRetentionDuration: 604800s # 7 天&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"># 建 subscription 時可顯式設更長的 ackDeadline 與更短的 retention&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">gcloud pubsub subscriptions create cfg-sub &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> --topic&lt;span class="o">=&lt;/span>demo-topic &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> --ack-deadline&lt;span class="o">=&lt;/span>&lt;span class="m">120&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --message-retention-duration&lt;span class="o">=&lt;/span>3d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># ackDeadlineSeconds: 120&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># messageRetentionDuration: 259200s # 3 天&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ackDeadline 是一道「處理時間預算」。設太短，處理還沒完訊息就被重投，consumer 會收到重複；設太長，consumer crash 後訊息要等滿 deadline 才重投，延遲拉高。長任務不靠把 ackDeadline 一次設到 600 秒解決，而是靠 ack deadline extension：consumer 在處理中週期性發 &lt;code>modifyAckDeadline&lt;/code> 把單則訊息的 deadline 往後延，處理完才 ack。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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 Cloud Pub/Sub</a> overview 的 implementation-layer deep article。Overview 回答「Pub/Sub 該不該選、跟 Kafka / SQS 差在哪」；本文回答「ordering key 怎麼設、DLT 怎麼擋 poison message、schema 怎麼守契約，各自踩哪些坑」。閱讀前可先讀 overview 的 ordering / DLT / schema 各段建立 context。</p>
<p>文中 gcloud 指令的語法以 Pub/Sub emulator 實機驗證（topic / subscription / schema / ordering key / DLT / push 各操作均跑通），標準版的雲端配額、IAM 與計費行為依官方文件。</p></blockquote>
<h2 id="三道治理共用同一個交付骨架">三道治理共用同一個交付骨架</h2>
<p>Pub/Sub 的 ordering key、dead-letter topic、schema enforcement 看似三個獨立功能，實際都掛在同一個交付骨架上：subscription 是消費進度的 first-class 抽象、訊息經 ackDeadline 控制重投、失敗訊息經投遞次數計數決定去留。理解這個骨架之後，三道治理只是骨架上的三個切面 — ordering 切的是「投遞順序」、DLT 切的是「投遞次數上限」、schema 切的是「投遞前的內容守門」。</p>
<p>這條骨架跟 Kafka 思路不同。Kafka 的消費進度綁在 consumer group + partition offset；Pub/Sub 的 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 是 first-class，subscription 才是 consumer 抽象，一個 topic 可以掛 N 個 subscription、各自有獨立進度與獨立的 ackDeadline / DLT / ordering 設定。同一份 event 流，A subscription 可以開 ordering 嚴格有序、B subscription 可以不開 ordering 換吞吐，互不影響。</p>
<p>把這三道治理寫進一篇的理由是：它們在 production 會互相牽制。Ordering key 開了之後 DLT 的隔離行為會變（有序流裡一則 poison message 會卡住整把 key 的後續訊息）；schema enforcement 擋下的不相容 publish 不會進 DLT（根本沒進 topic）。分開讀三個官方頁面看不到這層耦合。</p>
<h2 id="subscription-是-first-classackdeadline-與-extension">subscription 是 first-class：ackDeadline 與 extension</h2>
<p>subscription 承擔「這個消費者讀到哪、還有多少沒 ack」的責任。每則訊息投遞給 subscriber 後，Pub/Sub 啟動一個 ackDeadline 倒數；倒數內收到 ack 就移除訊息、倒數結束沒收到 ack 就重投。預設 ackDeadline 是 10 秒、上限 600 秒。</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"># subscription 的 ackDeadline 預設 10 秒、retention 預設 7 天</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud pubsub subscriptions describe demo-sub
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># ackDeadlineSeconds: 10</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># messageRetentionDuration: 604800s   # 7 天</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"># 建 subscription 時可顯式設更長的 ackDeadline 與更短的 retention</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">gcloud pubsub subscriptions create cfg-sub <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --topic<span class="o">=</span>demo-topic <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --ack-deadline<span class="o">=</span><span class="m">120</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --message-retention-duration<span class="o">=</span>3d
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># ackDeadlineSeconds: 120</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># messageRetentionDuration: 259200s   # 3 天</span></span></span></code></pre></div><p>ackDeadline 是一道「處理時間預算」。設太短，處理還沒完訊息就被重投，consumer 會收到重複；設太長，consumer crash 後訊息要等滿 deadline 才重投，延遲拉高。長任務不靠把 ackDeadline 一次設到 600 秒解決，而是靠 ack deadline extension：consumer 在處理中週期性發 <code>modifyAckDeadline</code> 把單則訊息的 deadline 往後延，處理完才 ack。</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"># pull 一則但不 auto-ack，拿到 ackId</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">ACKID</span><span class="o">=</span><span class="k">$(</span>gcloud pubsub subscriptions pull demo-sub --limit<span class="o">=</span><span class="m">1</span> --format<span class="o">=</span><span class="s1">&#39;value(ackId)&#39;</span><span class="k">)</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="c1"># 處理中動態延長這則訊息的 ackDeadline 到 300 秒</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud pubsub subscriptions modify-message-ack-deadline demo-sub <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --ack-ids<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$ACKID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --ack-deadline<span class="o">=</span><span class="m">300</span></span></span></code></pre></div><p>實務上不手動發 <code>modifyAckDeadline</code>，而是用 client library 的自動 lease 管理：client 在背景對 outstanding 訊息週期性續約，直到 application code 回 ack / nack。這跟 SQS 的 visibility timeout 語意類似 — 都是「訊息正在被處理、暫時別重投」的租約 — 但 Pub/Sub 是 per-message lease + client 自動續約，SQS 是 per-receive visibility window + 手動 <code>ChangeMessageVisibility</code>。</p>
<blockquote>
<p>ackDeadline 的陷阱在 batch 邊界。client library 常以 batch 為單位 pull，但 ackDeadline lease 是 per-message。若 application 把整個 batch 當一個工作單元處理、處理時間超過單則 ackDeadline 且 client 未對每則續約，未 ack 的訊息會被重投。Mercari 的 actionable history pipeline 揭露的正是這個 client library 行為：ack deadline 以整批 batch 為粒度運作，同批只要有一則過期或被 nack，已 ack 的訊息會跟著一起重投（<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/" data-link-title="3.C63 Mercari Actionable History：ack deadline 是 batch-level" data-link-desc="Merpay 支付流水帳用 Pub/Sub、ack deadline 是整批 batch 而非單訊息、acked 訊息會跟同批 expired 一起 redeliver。">3.C63</a>）。</p></blockquote>
<h2 id="pushpullstreaming-pull-與-flow-control">Push、Pull、Streaming Pull 與 flow control</h2>
<p>subscription 有兩種交付方向，pull 之下又分 unary pull 與 streaming pull。三者對應不同的下游承壓能力。</p>
<table>
  <thead>
      <tr>
          <th>交付模型</th>
          <th>機制</th>
          <th>適合場景</th>
          <th>flow control 由誰掌握</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Push</td>
          <td>Pub/Sub 主動 POST 到 HTTPS endpoint</td>
          <td>無狀態 worker、Cloud Run、Cloud Functions</td>
          <td>Pub/Sub（按 ack 動態調速）</td>
      </tr>
      <tr>
          <td>Unary Pull</td>
          <td>consumer 每次發一個 pull 請求拿一批</td>
          <td>低頻、批次拉取、簡單腳本</td>
          <td>consumer（自己控拉取頻率）</td>
      </tr>
      <tr>
          <td>Streaming Pull</td>
          <td>consumer 開長連線、Pub/Sub 持續推送到該連線</td>
          <td>高吞吐長 worker、需要精確 flow control</td>
          <td>consumer（client lib 設定）</td>
      </tr>
  </tbody>
</table>
<p>Push 把投遞節奏交給 Pub/Sub：endpoint 回 2xx 視為 ack、回非 2xx 或逾時視為 nack 並 backoff 重投。Pull 把節奏交給 consumer：consumer 想拉才拉、拉多少自己定。Streaming pull 是 production 高吞吐場景的主力 — client library 預設用它，因為它能在單一長連線上做精細的 flow control。</p>
<p>flow control 是 pull 的核心優勢：consumer 用 <code>max_outstanding_messages</code> 與 <code>max_outstanding_bytes</code> 設定「同時最多持有多少未 ack 訊息」，超過上限 client 就暫停從連線拉取，等 application ack 釋放額度才繼續。這讓 consumer 能把消費速率對齊到下游能吃的速率，而不是被 broker 灌爆。</p>
<blockquote>
<p>Push vs pull 不是實作偏好，是「下游能否接受 push 衝擊」的判讀。Mercari 把外部行銷 webhook（Braze）轉成 Pub/Sub event 後，下游 worker 刻意用 pull subscription 精確控制每秒處理訊息數，因為下游要呼叫的外部 LINE API 有 RPS 限制 — push 會把瞬間流量直接打到受限的外部 API（<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65</a>）。下游有硬性 RPS 上限時，pull + flow control 是讓消費速率可控的手段。</p></blockquote>
<h2 id="ordering-key有序的代價是吞吐">Ordering Key：有序的代價是吞吐</h2>
<p>Ordering key 讓「帶同一個 ordering key 的訊息，在 subscription 端按 publish 順序投遞」。它把全域無序的 Pub/Sub 變成 per-key 有序 — 不同 key 之間仍可並行、亂序，只有同 key 內部保證順序。要生效需要兩端配合：subscription 建立時開 <code>--enable-message-ordering</code>，publish 時帶 <code>--ordering-key</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"># subscription 端開啟 ordering</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud pubsub subscriptions create ord-sub <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --topic<span class="o">=</span>ord-topic <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --enable-message-ordering
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># describe 可見 enableMessageOrdering: true</span>
</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="c1"># publish 端帶 ordering key（同一 key 的訊息會保序）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">gcloud pubsub topics publish ord-topic --message<span class="o">=</span>m1 --ordering-key<span class="o">=</span>user-123
</span></span><span class="line"><span class="ln">9</span><span class="cl">gcloud pubsub topics publish ord-topic --message<span class="o">=</span>m2 --ordering-key<span class="o">=</span>user-123</span></span></code></pre></div><p>Ordering key 的設計責任在於選對 key 的粒度。粒度太粗（例如所有訊息共用一個 key）會把整條 topic 退化成單線序列、吞吐崩塌；粒度太細（例如每則訊息一個 key）等於沒開 ordering。正確做法是按「需要保序的業務實體」選 key — 同一個 <code>user-123</code> 的事件要保序、不同 user 之間不需要 — 這樣並行度等於活躍 key 數，既保序又不犧牲整體吞吐。</p>
<p>跟 Kafka 對照能看清取捨。Kafka 用 partition + 同 key hash 到同 partition 達成保序，partition 數是固定預先規劃的並行上限；Pub/Sub 沒有顯式 partition，ordering key 的並行度是動態的、由活躍 key 數決定。代價是 Pub/Sub 的有序投遞要求同 key 訊息送到同一個內部處理單元，這個約束讓單一 ordering key 的吞吐有上限（官方標稱單 ordering key 約 1 MB/s）。</p>
<blockquote>
<p>Ordering 跟 DLT 在 production 會耦合：有序流裡若一則訊息反覆失敗、Pub/Sub 為維持順序不會跳過它去投後面的訊息，整把 key 的後續訊息全卡住，直到該訊息 ack 或送進 DLT。沒開 ordering 時 poison message 只卡自己；開了 ordering 後它卡住整條 key 序列。這是下一節 DLT 要解的問題在 ordering 場景下被放大的原因。</p></blockquote>
<h2 id="dead-letter-topic投遞次數上限決定隔離時機">Dead-Letter Topic：投遞次數上限決定隔離時機</h2>
<p>Dead-letter topic 是 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison-message quarantine</a> 在 Pub/Sub 的實作：subscription 對每則訊息計數投遞次數，超過 <code>max-delivery-attempts</code> 就把訊息轉發到另一個 topic（DLT），主 subscription 不再重投它，後續正常訊息得以前進。</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">gcloud pubsub topics create main-topic
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud pubsub topics create dl-topic
</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">gcloud pubsub subscriptions create main-sub <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --topic<span class="o">=</span>main-topic <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --dead-letter-topic<span class="o">=</span>dl-topic <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --max-delivery-attempts<span class="o">=</span><span class="m">5</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># deadLetterPolicy:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">#   deadLetterTopic: projects/&lt;proj&gt;/topics/dl-topic</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#   maxDeliveryAttempts: 5</span></span></span></code></pre></div><p>DLT 是 topic 不是 queue，這是 Pub/Sub 跟 SQS DLQ 的關鍵差異。SQS 的 DLQ 是另一個 queue、消費者直接 receive；Pub/Sub 的 DLT 是 topic，要再掛一個 subscription 才能讀。好處是 DLT 上可以同時掛多個 subscription — 一個給人工檢視、一個給自動 replay、一個給長期歸檔 — fan-out 內建。代價是多一層 subscription 配置，且 DLT 也有自己的 retention（同樣預設 7 天，poison message 要在這之內處理掉）。</p>
<p><code>max-delivery-attempts</code> 設定的是「容忍多少次暫時性失敗」與「多快放棄」之間的平衡。設太低（例如 1-2 次），下游短暫抖動就把訊息丟進 DLT、誤殺可恢復的訊息；設太高（例如 50 次），一則真正壞掉的訊息會反覆重試半天、占用 consumer 資源、在有序流裡還會長時間卡住整條 key。官方允許範圍 5-100，常見起點是 5。</p>
<p>搭配 retry policy 的 backoff 能讓重投不至於太密集：</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">gcloud pubsub subscriptions create retry-sub <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --topic<span class="o">=</span>main-topic <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --min-retry-delay<span class="o">=</span>10s <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --max-retry-delay<span class="o">=</span>600s
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># retryPolicy:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#   minimumBackoff: 10s</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#   maximumBackoff: 600s</span></span></span></code></pre></div><blockquote>
<p>啟用 DLT 需要把 Pub/Sub service account 授權對主 subscription 有 subscriber、對 DLT 有 publisher（emulator 不校驗 IAM，正式環境若漏授權，訊息超過 max attempts 後不會進 DLT、而是繼續留在主 subscription 重投，看起來像 DLT 沒生效）。授權細節依 GCP 官方 IAM 文件。</p></blockquote>
<p>Mercari 的商品 feed 同步示範了 DLT 的標準用法：pull subscription + 自家 batch requester、成功 ack 整批、失敗 nack 讓 Pub/Sub 重送、重試多次仍失敗送 DLT、後續訊息優先處理；同一個 topic 還兼當突發流量的 load-leveling buffer（<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64</a>）。</p>
<h2 id="schema-enforcement投遞前的契約守門">Schema Enforcement：投遞前的契約守門</h2>
<p>Schema enforcement 把 <a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">event schema compatibility</a> 從「應用層約定」提升到「broker 強制」。topic 綁定一個 Avro 或 Protobuf schema 後，不符 schema 的 publish 在進 topic 前就被拒絕 — 訊息根本不會被儲存、不會投遞、不會進 DLT。</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. 建 schema（Avro，一個必填 string 欄位 id）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud pubsub schemas create order-schema <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --type<span class="o">=</span>avro <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --definition<span class="o">=</span><span class="s1">&#39;{&#34;type&#34;:&#34;record&#34;,&#34;name&#34;:&#34;Order&#34;,&#34;fields&#34;:[{&#34;name&#34;:&#34;id&#34;,&#34;type&#34;:&#34;string&#34;}]}&#39;</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"># 2. topic 綁 schema + 指定 message encoding</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">gcloud pubsub topics create sch-topic <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --schema<span class="o">=</span>order-schema <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --message-encoding<span class="o">=</span>json</span></span></code></pre></div><p>綁定後的 publish 行為（emulator 實機驗證 enforce）：</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"># 符合 schema：通過</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud pubsub topics publish sch-topic --message<span class="o">=</span><span class="s1">&#39;{&#34;id&#34;:&#34;abc&#34;}&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># messageIds: [&#39;4&#39;]</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="c1"># 欄位不符 schema：被拒</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">gcloud pubsub topics publish sch-topic --message<span class="o">=</span><span class="s1">&#39;{&#34;wrong&#34;:123}&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># ERROR: INVALID_ARGUMENT: Could not parse message</span>
</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="c1"># 非 JSON 垃圾：被拒</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">gcloud pubsub topics publish sch-topic --message<span class="o">=</span><span class="s1">&#39;not-json&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># ERROR: INVALID_ARGUMENT: Could not parse message</span></span></span></code></pre></div><p>schema 守門的價值在於把契約破壞擋在 producer 端、而不是 consumer 端。沒有 schema enforcement 時，producer 改了 payload 結構、不相容的訊息照樣進 topic、要到 consumer 解析失敗才爆 — 此時訊息已經在系統裡流動、可能已 fan-out 到多個 subscription、修復成本高。有 schema enforcement 時，不相容的 publish 在源頭就失敗，問題暴露在「誰送了壞訊息」而不是「誰收到壞訊息」。</p>
<p>schema evolution 要在「擋住破壞性改版」與「不阻塞合理演進」之間取捨。新增可選欄位或帶預設值的欄位維持相容、可以平滑演進；新增必填欄位、刪欄位、改型別是破壞性改版，會讓既有 producer 或 consumer 失效。設計上先定相容性等級（backward / forward / full）再演進，刪欄位分兩步（先停用再移除），避免一次破壞性改版打掛下游。</p>
<p>跟 Kafka Schema Registry 對照：Kafka 的 schema 校驗在 client 端（producer / consumer 各自向 Registry 查 schema、序列化時校驗），broker 本身不認識 schema；Pub/Sub 的 schema 綁在 topic、校驗在 broker 端 publish 路徑上。前者校驗點分散、靈活但要求所有 client 守規矩；後者校驗點集中在 broker、強制但耦合到 topic 配置。</p>
<h2 id="五個-production-故障演練">五個 Production 故障演練</h2>
<p>deep article 的差異化價值在故障演練。以下五個徵兆對應前述三道治理在 production 的典型失效。</p>
<h3 id="演練一ordering-key-把吞吐限到單線">演練一：Ordering key 把吞吐限到單線</h3>
<p><strong>徵兆</strong>：開了 ordering 後整條 topic 的吞吐從數萬 msg/s 掉到數百 msg/s，subscription backlog（<code>num_undelivered_messages</code>）持續攀升、<code>oldest_unacked_message_age</code> 越拉越長，但 consumer CPU 並不滿載 — consumer 在等訊息、不是在忙。</p>
<p><strong>根因</strong>：ordering key 粒度太粗。最常見是「所有訊息共用同一個 ordering key」（例如固定字串、或單一租戶 ID），整條 topic 退化成單一有序序列，並行度等於 1。單一 ordering key 的吞吐有上限（官方標稱約 1 MB/s），所有訊息擠進一個 key 就被這個上限封頂。</p>
<p><strong>判讀與修法</strong>：</p>
<ol>
<li>確認 ordering key 的基數（cardinality）。<code>gcloud pubsub topics publish</code> 帶的 <code>--ordering-key</code> 在 production 是業務欄位映射來的 — 檢查映射邏輯是否塌縮成低基數。</li>
<li>把 key 粒度對齊到「真正需要保序的業務實體」：同一筆訂單 / 同一個 user / 同一個 device 內要保序，跨實體不需要。粒度從「全域一個 key」改成「per-user 一個 key」，並行度從 1 拉到活躍 user 數。</li>
<li>評估是否真的需要 ordering。多數 pipeline 靠 consumer 端 idempotency + 版本號就能容忍亂序，不需要 broker 層保序 — 把保序成本從吞吐換成 consumer 設計（見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract</a> 的 idempotency key 段）。</li>
</ol>
<h3 id="演練二ack-deadline-太短導致重複投遞">演練二：Ack deadline 太短導致重複投遞</h3>
<p><strong>徵兆</strong>：consumer 處理邏輯正確、下游也成功，但同一則訊息被處理多次；<code>DELIVERY_ATTEMPT</code> 計數異常偏高、下游出現重複副作用（重複扣款 / 重複發信）。Backlog 不一定高，但「處理量」遠大於「publish 量」。</p>
<p><strong>根因</strong>：ackDeadline 比實際處理時間短。預設 10 秒對「呼叫一個慢的外部 API」「處理大 payload」這類任務不夠，訊息在 application 還沒 ack 前就過了 deadline、被 Pub/Sub 重投，於是同一則訊息有多個 consumer 副本在跑。若 client library 的自動 lease extension 沒生效（例如 application 阻塞在同步呼叫、background lease thread 餓死），重投更嚴重。</p>
<p><strong>判讀與修法</strong>：</p>
<ol>
<li>量測 p99 處理時間，把 ackDeadline 設到 p99 之上留 buffer，但不要不加判斷地設到 600 秒上限 — deadline 越長，consumer crash 後訊息重投的延遲越長。</li>
<li>長任務靠 lease extension 而非長 ackDeadline：確認 client library 的自動續約有在跑，application code 不要在處理迴圈裡阻塞到讓 background 續約 thread 餓死。</li>
<li>consumer 端做 idempotency：用 message 的 dedup key（<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7</a>）讓重複投遞變成無害 — at-least-once 交付下重複是常態，不靠調 ackDeadline 消除、靠 consumer 設計吸收。</li>
</ol>
<h3 id="演練三dlt-max-delivery-attempts-設定誤判">演練三：DLT max delivery attempts 設定誤判</h3>
<p><strong>徵兆</strong>：兩種反向徵兆。其一，DLT 堆滿了「其實能恢復」的訊息 — 下游一抖動就被丟進 DLT，DLT backlog 暴增、人工 replay 不完。其二，主 subscription 卡著一則壞訊息反覆重投半天都不進 DLT、後面訊息（尤其在 ordering 流裡）全堵住。</p>
<p><strong>根因</strong>：第一種是 <code>max-delivery-attempts</code> 設太低（1-2 次），暫時性失敗就被當成 poison。第二種是設太高（數十次）或根本沒設 DLT，真正的 poison message 反覆重試、占資源、卡序列。</p>
<p><strong>判讀與修法</strong>：</p>
<ol>
<li>區分「暫時性失敗」與「結構性失敗」。暫時性（下游超時、限流）需要重試容忍度，結構性（payload 解析不了、業務規則永久拒絕）越早隔離越好。</li>
<li><code>max-delivery-attempts</code> 起點設 5，搭配 retry policy backoff（<code>--min-retry-delay</code> / <code>--max-retry-delay</code>）讓重試之間有間隔、給下游恢復時間，而不是密集重打。</li>
<li>確認 DLT 真的接得到訊息：檢查 Pub/Sub service account 對 DLT 的 publisher 授權（漏授權會讓訊息超過 attempts 後繼續留在主 subscription、看起來像沒進 DLT）。</li>
<li>DLT 要掛 subscription 才讀得到 — DLT 是 topic 不是 queue，建完 DLT 還要建 DLT 的 subscription 並設好 retention，否則 poison message 在 DLT 裡放滿 7 天後一樣丟失。</li>
</ol>
<h3 id="演練四push-endpoint-500-觸發-retry-storm">演練四：Push endpoint 500 觸發 retry storm</h3>
<p><strong>徵兆</strong>：push subscription 的下游 HTTP endpoint 開始大量回 500，Pub/Sub backoff 重投、但 endpoint 仍 500，重投量隨 backlog 累積越滾越大；endpoint 一旦短暫恢復就被積壓的重投流量瞬間打回 500，形成「恢復即再掛」的震盪。</p>
<p><strong>根因</strong>：push 的 flow control 由 Pub/Sub 掌握、按 ack 動態調速 — endpoint 回 2xx 視為 ack、非 2xx 視為 nack 並重投。當 endpoint 因下游依賴（DB / 外部 API）掛掉而持續 500，Pub/Sub 的 backoff 重投跟累積的 backlog 疊加，恢復瞬間的流量遠超 endpoint 平時負載。這正是「下游能否接受 push 衝擊」的反面 — push 沒有 consumer 端的 flow control 閥門。</p>
<p><strong>判讀與修法</strong>：</p>
<ol>
<li>先判訊息毒性 vs endpoint 健康。若是 endpoint 整體掛（所有訊息都 500），是容量 / 依賴問題；若是特定訊息 500（多數成功、少數失敗），是 poison message，該走 DLT。</li>
<li>endpoint 整體掛的場景，push 不是好選擇 — 改 pull + flow control，讓 consumer 用 <code>max_outstanding_messages</code> 把消費速率對齊到下游能吃的速率，避免恢復瞬間被積壓流量打垮（對照 <a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65</a> 的下游 RPS 限制場景）。</li>
<li>對 push 配 DLT，把反覆 500 的特定訊息隔離出去，避免單一 poison message 混在正常流量裡放大 retry。</li>
<li>endpoint 側對「Pub/Sub 重投」做 idempotency，因為 push 也是 at-least-once、500 後的重投會帶來重複。</li>
</ol>
<h3 id="演練五schema-enforcement-擋下不相容-publish">演練五：Schema enforcement 擋下不相容 publish</h3>
<p><strong>徵兆</strong>：某次 producer 部署後，該 service 的 publish 開始大量回 <code>INVALID_ARGUMENT: Could not parse message</code>，訊息發不出去；但 consumer 端風平浪靜、沒有任何解析錯誤、backlog 也沒異常。</p>
<p><strong>根因</strong>：這通常不是故障、是 schema enforcement 正常運作。producer 改了 payload 結構（加必填欄位 / 改型別 / 漏欄位），新 payload 不符 topic 綁定的 schema，broker 在 publish 路徑上擋下、訊息根本沒進 topic。徵兆出現在 producer 端（publish 失敗）而非 consumer 端（解析失敗），正是 schema 守門把問題前移到源頭的設計意圖。</p>
<p><strong>判讀與修法</strong>：</p>
<ol>
<li>先確認是「該擋」還是「誤擋」。對照 producer 的新 payload 與 topic schema：若是破壞性改版（加必填欄位 / 改型別），enforcement 擋對了 — 該回滾 producer 或先演進 schema。</li>
<li>用 <code>gcloud pubsub schemas validate-message</code> 在部署前 dry-run 校驗 payload 對 schema，把「不相容」暴露在 CI 而不是 production publish。</li>
<li>schema 演進走相容路徑：新增欄位帶預設或設可選、刪欄位分兩步、避免一次破壞性改版。先升 schema 再升 producer，順序反了就會出現這個徵兆。</li>
<li>區分 schema enforcement 失敗與 DLT：schema 擋下的訊息不進 topic、不進 DLT（DLT 隔離的是「進了 topic 但消費反覆失敗」的訊息）。兩者是交付管線的不同關卡，徵兆與修法都不同。</li>
</ol>
<h2 id="容量與選型邊界標準版-vs-pubsub-lite">容量與選型邊界：標準版 vs Pub/Sub Lite</h2>
<p>前述配置適用標準版 Pub/Sub。標準版的計費與容量模型偏向「全域路由內建、按用量計費、不需預先規劃容量」；當吞吐極高且 region 確定時，Pub/Sub Lite 的 partition-based / zonal 模型成本更低。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>標準版 Pub/Sub</th>
          <th>Pub/Sub Lite</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>路由</td>
          <td>全域、無 region 概念</td>
          <td>zonal / regional、需指定</td>
      </tr>
      <tr>
          <td>容量模型</td>
          <td>自動擴縮、按用量計費</td>
          <td>partition-based、預先 provision throughput</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>高吞吐時單位成本較高</td>
          <td>高吞吐 + 確定 region 時顯著較低</td>
      </tr>
      <tr>
          <td>CLI surface</td>
          <td><code>gcloud pubsub topics</code></td>
          <td><code>gcloud pubsub lite-topics</code>（獨立）</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>全域分發、彈性流量、不想管容量</td>
          <td>已知高且穩定的吞吐、成本敏感、region 確定</td>
      </tr>
  </tbody>
</table>
<p>Pub/Sub Lite 是獨立的 CLI surface（<code>gcloud pubsub lite-topics</code> / <code>gcloud pubsub lite-subscriptions</code>），不是標準版的一個 flag。選 Lite 的代價是要自己 provision partition 數與 throughput capacity（回到接近 Kafka 的容量規劃），換來的是高吞吐穩定流量下顯著更低的成本。判準是吞吐「夠高且夠穩定到值得自己管容量」— 流量彈性大、或不想管 partition 的場景仍該留在標準版。</p>
<blockquote>
<p>Spotify 的 autoscaling 案例揭露 backlog 不等於 consumer healthy：下游 export 失敗時 consumer 不 ack 仍持續耗 CPU，autoscaling 把 CPU 越拉越高、反而擴出更多空轉 consumer；解法是 exponential backoff 抑制 CPU 消耗（<a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61</a>）。容量規劃的 autoscale signal 要看「處理成功率」而非「CPU + backlog」，否則擴縮方向會反。</p></blockquote>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="bigquery--cloud-storage-subscription免-consumer-的落地路徑">BigQuery / Cloud Storage subscription：免 consumer 的落地路徑</h3>
<p>標準版提供兩種「不需要自寫 consumer」的 subscription，直接把訊息落地到分析 / 儲存層：</p>
<ul>
<li><strong>BigQuery subscription</strong>（<code>--bigquery-table</code>）：訊息直接寫進 BQ table，免 Dataflow 中介，適合 streaming analytics。可搭配 <code>--use-topic-schema</code> 讓 BQ table schema 對齊 topic schema — schema enforcement 在這裡延伸成「落地結構也受契約約束」。</li>
<li><strong>Cloud Storage subscription</strong>（<code>--cloud-storage-bucket</code>）：訊息批次寫成 GCS object，適合 data lake / 歸檔。</li>
</ul>
<p>這兩種 subscription 把「event 流 → 分析 / 儲存」的常見管線收進 Pub/Sub 配置，省掉一層自管 consumer。它們仍受同一套 ackDeadline / DLT 骨架管轄。</p>
<h3 id="cross-link">Cross-link</h3>
<ul>
<li>上游 vendor 頁：<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 Cloud Pub/Sub overview</a> — 選型層、跟 Kafka / SQS 取捨</li>
<li>契約與重播邊界：<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 Event Contract 與 Replay Boundary</a> — schema / idempotency key / replay window 先於 broker 選型</li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Event Schema Compatibility</a>（schema enforcement 守的契約等級）、<a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">Poison-Message Quarantine</a>（DLT 的隔離機制）</li>
<li>對應 case：<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64 Mercari Item Feed DLT</a>、<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65 Mercari LINE flow control</a>、<a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61 Spotify autoscaling</a>、<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/" data-link-title="3.C63 Mercari Actionable History：ack deadline 是 batch-level" data-link-desc="Merpay 支付流水帳用 Pub/Sub、ack deadline 是整批 batch 而非單訊息、acked 訊息會跟同批 expired 一起 redeliver。">3.C63 Mercari actionable history</a></li>
<li>方法論：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
<h3 id="何時-revisit">何時 revisit</h3>
<ul>
<li>ordering key 吞吐撞上單 key 上限、且無法再細分 key：評估改用 Kafka partition 模型，或把保序成本移到 consumer 端 idempotency</li>
<li>高吞吐穩定流量 + 成本壓力浮現：評估標準版 → Pub/Sub Lite，接受自管 partition 容量換成本</li>
<li>schema 需要跨多 vendor 共用契約（同一份 event 同時進 Pub/Sub 與 Kafka）：評估把 schema source of truth 抽到 broker 外的 registry</li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ → Kafka：從『處理即承諾』到『寫入即承諾 + 可 replay』的 paradigm shift</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-kafka/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-kafka/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a>。跟同類產品的 drop-in 或 operational 遷移不同、本篇是 &lt;em>paradigm shift&lt;/em> — 兩端不是「同類 broker 的不同實作」、是 &lt;em>不同責任模型的 messaging system&lt;/em>：RabbitMQ 是「處理即承諾」的 work queue、Kafka 是「寫入即承諾、可長期 replay」的 event log。&lt;/p>&lt;/blockquote>
&lt;h2 id="rabbitmq--kafka-不是把-queue-換成-topic">RabbitMQ → Kafka 不是把 queue 換成 topic&lt;/h2>
&lt;p>RabbitMQ 跟 Kafka 都被歸在「message queue」這個傘狀詞下、但兩者承擔的責任不同。RabbitMQ 的可靠性建立在 &lt;em>consumer 處理完才 ack、未 ack 的訊息 broker 重新投遞&lt;/em>；訊息一旦被成功消費就從 queue 移除、broker 是「任務分派 + 重試」的中介。Kafka 的可靠性建立在 &lt;em>訊息寫進 partition log 就持久化、consumer 各自維護 offset&lt;/em>；訊息在 retention 期內一直留著、broker 是「事件儲存 + 多方各自讀取」的 log。&lt;/p>
&lt;p>把 RabbitMQ「migration」成 Kafka 的字面理解通常是：queue 對 topic、exchange 對 producer key、consumer 對 consumer group。這個對映在 transport 層成立、在責任層不成立。RabbitMQ 一個 message 被 ack 後就消失、Kafka 一個 message 寫進 log 後對所有 consumer group 都還在；RabbitMQ 的 routing 由 broker 端 exchange + binding 決定、Kafka 的「routing」由 producer 端 partition key 決定、broker 不做內容路由。先確認這層差異、再決定哪些 workload 值得遷。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;p>跨 vendor 遷移前先盤點 source 跟 target 在六個維度的落差、用最大落差維度決定 playbook 結構、而不是反過來套既有模板。RabbitMQ → Kafka 的 audit 結果：&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>Schema / API&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>AMQP client → Kafka client、wire protocol 全換、但都是 publish / consume 心智模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>單 broker + management UI → multi-broker + KRaft / Schema Registry / Connect、運維資產變重&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction/paradigm&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>&lt;/td>
 &lt;td>work queue「處理即承諾、ack 後即刪」→ event log「寫入即承諾、offset replay」、責任模型整個不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>兩端都是單一 messaging system、不是一站式拆多工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>&lt;/td>
 &lt;td>consumer 要重設計（ack → offset commit）、producer 要重設計（exchange routing → partition key）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>&lt;/td>
 &lt;td>exchange + queue + binding 的 routing 拓樸 → topic + partition + key 的 log 拓樸、資料分佈邏輯不同&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三個維度 High：paradigm、application change、data topology。其中 paradigm 是主導維度 —— application change 跟 data topology 的落差都是 paradigm 落差的下游結果。consumer 要重寫，是因為「ack 後即刪」變成「offset 不刪」；資料拓樸要重劃，是因為「broker 路由到 queue」變成「producer 決定 partition」。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> 跟 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。跟同類產品的 drop-in 或 operational 遷移不同、本篇是 <em>paradigm shift</em> — 兩端不是「同類 broker 的不同實作」、是 <em>不同責任模型的 messaging system</em>：RabbitMQ 是「處理即承諾」的 work queue、Kafka 是「寫入即承諾、可長期 replay」的 event log。</p></blockquote>
<h2 id="rabbitmq--kafka-不是把-queue-換成-topic">RabbitMQ → Kafka 不是把 queue 換成 topic</h2>
<p>RabbitMQ 跟 Kafka 都被歸在「message queue」這個傘狀詞下、但兩者承擔的責任不同。RabbitMQ 的可靠性建立在 <em>consumer 處理完才 ack、未 ack 的訊息 broker 重新投遞</em>；訊息一旦被成功消費就從 queue 移除、broker 是「任務分派 + 重試」的中介。Kafka 的可靠性建立在 <em>訊息寫進 partition log 就持久化、consumer 各自維護 offset</em>；訊息在 retention 期內一直留著、broker 是「事件儲存 + 多方各自讀取」的 log。</p>
<p>把 RabbitMQ「migration」成 Kafka 的字面理解通常是：queue 對 topic、exchange 對 producer key、consumer 對 consumer group。這個對映在 transport 層成立、在責任層不成立。RabbitMQ 一個 message 被 ack 後就消失、Kafka 一個 message 寫進 log 後對所有 consumer group 都還在；RabbitMQ 的 routing 由 broker 端 exchange + binding 決定、Kafka 的「routing」由 producer 端 partition key 決定、broker 不做內容路由。先確認這層差異、再決定哪些 workload 值得遷。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<p>跨 vendor 遷移前先盤點 source 跟 target 在六個維度的落差、用最大落差維度決定 playbook 結構、而不是反過來套既有模板。RabbitMQ → Kafka 的 audit 結果：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>落差</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>中</td>
          <td>AMQP client → Kafka client、wire protocol 全換、但都是 publish / consume 心智模型</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>中</td>
          <td>單 broker + management UI → multi-broker + KRaft / Schema Registry / Connect、運維資產變重</td>
      </tr>
      <tr>
          <td>Abstraction/paradigm</td>
          <td><strong>高</strong></td>
          <td>work queue「處理即承諾、ack 後即刪」→ event log「寫入即承諾、offset replay」、責任模型整個不同</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>低</td>
          <td>兩端都是單一 messaging system、不是一站式拆多工具</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td><strong>高</strong></td>
          <td>consumer 要重設計（ack → offset commit）、producer 要重設計（exchange routing → partition key）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td><strong>高</strong></td>
          <td>exchange + queue + binding 的 routing 拓樸 → topic + partition + key 的 log 拓樸、資料分佈邏輯不同</td>
      </tr>
  </tbody>
</table>
<p>三個維度 High：paradigm、application change、data topology。其中 paradigm 是主導維度 —— application change 跟 data topology 的落差都是 paradigm 落差的下游結果。consumer 要重寫，是因為「ack 後即刪」變成「offset 不刪」；資料拓樸要重劃，是因為「broker 路由到 queue」變成「producer 決定 partition」。</p>
<p>主導維度是 paradigm、對映 <em>Type E paradigm shift</em> 結構：先講「字面 migration 不成立」、再講適配度（什麼能遷什麼不能）、再講 application 重設計與部分 cutover、最後是長期混合架構。application change 跟 data topology 這兩個高維度不另起 playbook、而是落在 application 重設計段與故障演練段裡展開。</p>
<h3 id="為什麼-paradigm-是主導不是-application-change">為什麼 paradigm 是主導、不是 application change</h3>
<p>application change 看起來工作量最大（consumer / producer 都要改），直覺會把它當主導維度。但 application change 的方向跟難度是由 paradigm 決定的：如果只是 AMQP client 換 Kafka client、心智模型不變，那 application change 是機械式翻譯、屬於 Schema/API 維度。實際上 consumer 不只是換 SDK、是要把「處理完才 ack、失敗就 nack 重投」的設計改成「拉一批、處理、commit offset、失敗自己重試或寫 DLQ topic」—— 這是責任模型的改變，不是 API 的改變。所以主結構走 paradigm、application change 是它的展開。</p>
<h2 id="什麼-workload-真該遷什麼不該">什麼 workload 真該遷、什麼不該</h2>
<table>
  <thead>
      <tr>
          <th>Application 模式</th>
          <th>RabbitMQ 適配</th>
          <th>Kafka 適配</th>
          <th>遷移可行性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任務分派（寄信 / 轉檔 / webhook）</td>
          <td>強</td>
          <td>中（overkill）</td>
          <td>不該遷（保留 RabbitMQ）</td>
      </tr>
      <tr>
          <td>複雜 routing（topic exchange + binding）</td>
          <td>強</td>
          <td>弱（broker 不做路由）</td>
          <td>不該遷或要重新設計拓樸</td>
      </tr>
      <tr>
          <td>RPC over messaging（request-reply）</td>
          <td>強</td>
          <td>弱（不適合）</td>
          <td>不該遷</td>
      </tr>
      <tr>
          <td>Event sourcing（多 consumer 各自 replay）</td>
          <td>弱（ack 即刪）</td>
          <td>強</td>
          <td>該遷（這是 Kafka 的主場）</td>
      </tr>
      <tr>
          <td>CDC / 跨系統事件總線</td>
          <td>弱</td>
          <td>強</td>
          <td>該遷</td>
      </tr>
      <tr>
          <td>高吞吐事件流 + 長期 retention</td>
          <td>弱</td>
          <td>強</td>
          <td>該遷</td>
      </tr>
      <tr>
          <td>同一事件要被多個獨立團隊各自消費</td>
          <td>中（多 queue）</td>
          <td>強（多 consumer group）</td>
          <td>該遷</td>
      </tr>
  </tbody>
</table>
<p>判讀的核心問題是：<em>這個 workload 需要的是「處理一次就完成的任務」、還是「被多方各自讀取、可回放的事件」</em>。</p>
<p>任務分派場景不該遷。寄信、轉檔、生成縮圖這類 workload 的本質是「有一個工人池、把任務做完就結束」、RabbitMQ 的 manual ack + prefetch + DLX 對這條路徑是貼合的設計。把它搬到 Kafka 會引入不需要的複雜度：partition 數要規劃、consumer group rebalance 要管、offset commit 時機要自己設計、而換來的 replay 能力在「任務做完就丟」的場景根本用不到。單純 work queue 不需要 Kafka 是這篇 playbook 最該先說清楚的判讀。</p>
<p>事件流場景該遷。當同一份事件要被 analytics pipeline、search index sync、audit log、下游微服務各自消費、而且各自進度不同、偶爾要回放過去 N 天重算 —— RabbitMQ 的「ack 後即刪」就會逼出「為每個 consumer 複製一份 queue」的反模式，這正是 Kafka 的 consumer group + retention 要解的問題。</p>
<p>複雜 routing 場景要重新設計、不是平移。RabbitMQ 的 topic exchange 用 <code>order.*.created</code> 這種 binding pattern 在 broker 端做內容路由、consumer 訂閱 binding 就收到符合的訊息。Kafka broker 不做內容路由，要嘛把路由邏輯前移到 producer（按內容決定寫哪個 topic / partition key），要嘛 consumer 端全收後自己 filter。直接平移會發現 Kafka 沒有 exchange 這個概念，routing 拓樸必須重新設計。</p>
<h2 id="為什麼會考慮這個-paradigm-shift">為什麼會考慮這個 paradigm shift</h2>
<p>實務上從 RabbitMQ 評估遷往 Kafka 通常由三條 driver 觸發：</p>
<ol>
<li><strong>同一事件要 fan-out 給愈來愈多 consumer</strong>：初期一個 queue 一個 worker、後來下游團隊一個個來要「也給我一份」。RabbitMQ 要嘛加 fanout exchange + 每團隊一個 queue、要嘛 consumer 互搶。Kafka 的 consumer group 天然支援「N 個獨立團隊各自從頭讀」、這是最常見的 driver。</li>
<li><strong>需要 replay 重算</strong>：下游邏輯出 bug、要重跑過去 7 天的事件修資料；RabbitMQ ack 後訊息已刪、無從回放。Kafka retention 期內可以從任意 offset 重讀。</li>
<li><strong>吞吐量壓到 RabbitMQ 的設計邊界</strong>：單 queue 的 throughput 受限於單一 queue 的處理模型、量大時要拆 queue 手動分流；Kafka 的 partition 並行是 first-class。</li>
</ol>
<p>這三條 driver 都指向 event streaming 的特性、不是「Kafka 普遍比較好」。任務隊列場景套不上這三條 driver、就不該被這個評估帶著走。</p>
<h2 id="migration-結構application-重設計--部分-cutover--長期混合">Migration 結構：application 重設計 + 部分 cutover + 長期混合</h2>
<p>RabbitMQ → Kafka 不是一次性 cutover，是按 workload 拆分、漸進遷移、長期共存：</p>
<ol>
<li><strong>Phase 0：workload 盤點</strong> — 把現有 queue / exchange 逐一分類「適合 Kafka（event 性質）」vs「保留 RabbitMQ（task 性質）」。盤點輸出是清單，不是「全遷」。</li>
<li><strong>Phase 1：application code 重設計</strong> — 對判定要遷的 workload，重寫 producer（exchange routing → topic + partition key）跟 consumer（manual ack → offset commit + 自管重試 / DLQ）。這是 paradigm 翻譯，不是 SDK 替換。</li>
<li><strong>Phase 2：dual-write 並行</strong> — producer 同時寫 RabbitMQ 跟 Kafka、新 consumer 從 Kafka shadow consume 驗證行為對齊、舊 consumer 持續從 RabbitMQ 消費。</li>
<li><strong>Phase 3：cutover 個別 workload</strong> — shadow 驗證通過後、把該 workload 的真正消費切到 Kafka、停掉 RabbitMQ 端的對應 consumer 與 dual-write。</li>
<li><strong>Phase 4：長期混合</strong> — task 性質的 workload 永遠留在 RabbitMQ、event 性質的在 Kafka。兩者共存是終態、不是過渡。</li>
</ol>
<p>整體不是「把 RabbitMQ 換成 Kafka」、是「把適合 event log 的部分搬到 Kafka、其餘留在 RabbitMQ」。多數環境的終態是兩者並存。</p>
<h2 id="application-重設計範例manual-ack--offset-commit">Application 重設計範例：manual ack → offset commit</h2>
<p>RabbitMQ consumer 的核心是 <em>每個 message 處理完顯式 ack、broker 才認定投遞成功</em>；失敗就 nack、broker 重投或進 DLX。Kafka consumer 沒有 per-message ack 的概念、是 <em>批次拉取、處理、commit offset</em>；commit 的是「讀到哪了」、不是「哪幾條成功了」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># RabbitMQ 端：manual ack、per-message 成敗</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">basic_qos</span><span class="p">(</span><span class="n">prefetch_count</span><span class="o">=</span><span class="mi">10</span><span class="p">)</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">def</span> <span class="nf">on_message</span><span class="p">(</span><span class="n">ch</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">properties</span><span class="p">,</span> <span class="n">body</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">process</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_ack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># 拒絕並不重新入列、由 DLX 接住</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_nack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">,</span> <span class="n">requeue</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</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 class="n">channel</span><span class="o">.</span><span class="n">basic_consume</span><span class="p">(</span><span class="n">queue</span><span class="o">=</span><span class="s2">&#34;orders&#34;</span><span class="p">,</span> <span class="n">on_message_callback</span><span class="o">=</span><span class="n">on_message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">start_consuming</span><span class="p">()</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Kafka 端：批次 poll、處理後 commit offset</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">consumer</span> <span class="o">=</span> <span class="n">KafkaConsumer</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">group_id</span><span class="o">=</span><span class="s2">&#34;orders-worker&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">enable_auto_commit</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>        <span class="c1"># 關掉 auto commit、自己控制時機</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">auto_offset_reset</span><span class="o">=</span><span class="s2">&#34;earliest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">max_poll_records</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>             <span class="c1"># 對應 RabbitMQ 的 prefetch</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="n">iter_batches</span><span class="p">(</span><span class="n">consumer</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">batch</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="n">process</span><span class="p">(</span><span class="n">msg</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="n">send_to_dlq_topic</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>   <span class="c1"># 自建 DLQ topic、Kafka broker 不提供 DLX</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">consumer</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>                <span class="c1"># commit 的是 offset、不是個別 message</span></span></span></code></pre></div><p>差異的關鍵不在 API 形狀、在責任邊界：</p>
<ul>
<li>RabbitMQ 一條失敗就 nack 一條、其餘正常 ack；Kafka commit 的是 offset 這個「水位線」、水位線以下視為已處理。失敗的單條訊息無法「跳過不 commit 但繼續往後」—— 要嘛阻塞、要嘛自己寫 DLQ topic 後讓 offset 照常前進。</li>
<li>RabbitMQ 重試由 broker 負責（重投 / DLX）；Kafka 重試要 application 自己設計（原地重試 / 寫 retry topic / 寫 DLQ topic）。</li>
<li>RabbitMQ prefetch 控制「broker 一次推幾條未 ack 的給我」；Kafka <code>max.poll.records</code> 控制「我一次 poll 拉幾條」—— 方向相反，一個是 broker push、一個是 consumer pull。</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1manual-ack-觀念帶到-offset-commit誤判已處理">Case 1：manual ack 觀念帶到 offset commit、誤判「已處理」</h3>
<p><strong>徵兆</strong>：cutover 後某 worker crash 重啟、發現一批訊息被重複處理；或反過來、一批訊息明明沒處理成功卻再也讀不到。RabbitMQ 端跑了多年的 ack 邏輯搬過來就出事。</p>
<p><strong>根因</strong>：把 RabbitMQ 的「per-message ack」心智直接套到 Kafka 的 offset commit。常見錯法是 <code>enable.auto.commit=true</code> + 預設 <code>auto.commit.interval.ms</code>、消費迴圈還沒處理完、背景 thread 已經把 offset commit 出去了 —— crash 後 offset 已前進、未處理的訊息永遠跳過（資料遺失）。或反過來、處理完才 commit 但 commit 失敗、重啟後從舊 offset 重讀（重複處理）。RabbitMQ 的 ack 是「這一條我處理完了」、Kafka 的 commit 是「這個 offset 之前我都讀過了」—— 後者是水位線、不是逐條確認。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>關掉 auto commit、手動 commit</strong>：<code>enable.auto.commit=false</code>、在一批訊息確實處理完之後才 <code>commit()</code>。</li>
<li><strong>接受 at-least-once、設計 idempotency</strong>：Kafka 的預設語意是 at-least-once、重啟重讀無法完全避免、consumer 端要用 message key + dedup store 顯式去重。對應 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a>。</li>
<li><strong>commit 時機對齊處理邊界</strong>：批次處理完才 commit、不要一邊處理一邊讓背景 commit 跑在前面。</li>
</ol>
<h3 id="case-2routing-key--partition-keyordering-邊界悄悄改變">Case 2：routing key → partition key、ordering 邊界悄悄改變</h3>
<p><strong>徵兆</strong>：cutover 後同一個訂單的 <code>created</code> / <code>paid</code> / <code>shipped</code> 事件偶爾亂序到達 consumer；RabbitMQ 端用 consistent hash exchange 跑了兩年、同一訂單的事件一直是有序的。</p>
<p><strong>根因</strong>：RabbitMQ 用 consistent hash exchange 把同 key 的訊息路由到同一個 queue、單一 consumer 順序處理就有序。Kafka 的 ordering 保證範圍是 <em>單一 partition 內</em>、跨 partition 無序。如果 producer 沒設 partition key、或設了但 key 選得不對（例如用 event type 當 key 而不是 order id）、同一訂單的事件就散到不同 partition、被不同 consumer 並行處理、ordering 就斷了。RabbitMQ 的 ordering 邊界是「queue」、Kafka 的 ordering 邊界是「partition key」—— 邊界從 broker 端的 binding 移到了 producer 端的 key 選擇。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>ordering 單位當 partition key</strong>：需要保序的單位（order id / user id）設成 partition key、同 key 落同 partition。</li>
<li><strong>盤點現有 RabbitMQ 的保序假設</strong>：哪些 queue 隱含「同 key 有序」、把那個 key 顯式提升為 Kafka partition key。</li>
<li><strong>接受 partition 數限制並行</strong>：保序的代價是同 key 只能單一 partition、partition 數是並行上限；保序需求跟並行度需要一起設計。對應 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition</a> 卡。</li>
</ol>
<h3 id="case-3dlx--自建-dlq-topic毒訊息卡住整個-partition">Case 3：DLX → 自建 DLQ topic、毒訊息卡住整個 partition</h3>
<p><strong>徵兆</strong>：某條訊息 application 處理永遠拋例外、consumer 不斷在這條上重試、整個 partition 後面的訊息全卡住、consumer lag 暴增；RabbitMQ 端這種毒訊息會被 nack 進 DLX、不影響後面。</p>
<p><strong>根因</strong>：RabbitMQ 有原生 DLX、處理失敗的訊息 nack 後自動進 dead-letter exchange、queue 繼續往下。Kafka broker 沒有 DLX 概念、也沒有「跳過這一條」的機制 —— offset 是連續水位線、要往後就得處理掉當前這條。如果 application 在毒訊息上無限重試、offset 永遠不前進、後面所有訊息餓死。把 RabbitMQ「broker 幫我處理毒訊息」的假設帶過來、就會卡死。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>自建 DLQ topic</strong>：consumer 端設重試上限、超過上限把訊息寫進專屬的 <code>orders.DLQ</code> topic、然後 commit offset 讓主流程前進。對應 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-letter queue</a> 卡。</li>
<li><strong>retry topic 分層</strong>：仿 RabbitMQ 的延遲重試、可以設 <code>orders.retry.5s</code> / <code>orders.retry.1m</code> 多層 retry topic、由獨立 consumer 延遲後重投主 topic。</li>
<li><strong>DLQ 要有人看</strong>：自建 DLQ topic 不像 RabbitMQ management UI 有現成可視化、要主動監控 DLQ topic 的訊息數、否則毒訊息靜默堆積。</li>
</ol>
<h3 id="case-4prefetch--maxpollrecordspoll-間隔超時觸發-rebalance">Case 4：prefetch → max.poll.records，poll 間隔超時觸發 rebalance</h3>
<p><strong>徵兆</strong>：consumer 處理一批訊息花的時間偏長、Kafka 突然判定這個 consumer 死了、觸發 rebalance、partition 被重新分配、同一批訊息被另一個 consumer 重複處理；RabbitMQ 端用 prefetch 控制併發從沒這問題。</p>
<p><strong>根因</strong>：RabbitMQ prefetch 只控制「broker 一次最多推幾條未 ack 給這個 consumer」、處理多久 broker 不管。Kafka 用 <code>max.poll.interval.ms</code> 監控「兩次 poll 之間最多隔多久」、如果一批 <code>max.poll.records</code> 拉太多、處理超過 <code>max.poll.interval.ms</code> 還沒回來 poll、broker 認定 consumer 卡死、踢出 group 觸發 rebalance。把 prefetch 的數值直接套成 <code>max.poll.records</code>、又沒考慮單批處理時間、就會超時。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>max.poll.records</code> 配合單條處理時間設</strong>：一批的總處理時間要明顯小於 <code>max.poll.interval.ms</code>；處理慢就把 batch 設小。</li>
<li><strong>長處理 workload 調大 <code>max.poll.interval.ms</code></strong>：單條本來就慢（呼叫外部 API）的、把 interval 放寬、或把處理移到另一個 thread pool、poll 迴圈只負責拉取。</li>
<li><strong>理解 push vs pull 的差異</strong>：RabbitMQ 是 broker push、consumer 慢只是堆積；Kafka 是 consumer pull、consumer 慢會被誤判為死亡。這層差異是 prefetch 跟 max.poll.records 不能直接對映的根因。對應 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a> 卡。</li>
</ol>
<h3 id="case-5rabbitmq-即刪-vs-kafka-retentionreplay-行為差異炸出資料量">Case 5：RabbitMQ 即刪 vs Kafka retention、replay 行為差異炸出資料量</h3>
<p><strong>徵兆</strong>：團隊以為 Kafka「跟 RabbitMQ 一樣處理完就沒了」、結果 disk 持續長大；或反過來、需要 replay 時才發現 retention 設太短、要回放的事件已經被清掉。RabbitMQ 心智下「訊息消費完就不佔空間」的假設不成立。</p>
<p><strong>根因</strong>：RabbitMQ ack 後訊息即刪、queue 的空間隨消費釋放。Kafka 寫進 log 後在 <em>retention 期內一直留著</em>、不管有沒有被消費 —— 這正是 replay 能力的來源、也是 disk 成本的來源。沒設好 retention，要嘛留太久 disk 爆、要嘛留太短該 replay 時沒得 replay。RabbitMQ 沒有「retention」這個旋鈕（它是 ack 即刪），Kafka 必須顯式設 retention policy。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>按 replay 需求設 retention</strong>：event sourcing 要回放幾天就設幾天的 <code>retention.ms</code>、不是抄 RabbitMQ 的「處理完即刪」心智。</li>
<li><strong>算清 retention 的 disk 成本</strong>：retention × 寫入速率 = 佔用空間、納入容量規劃；對比 RabbitMQ 只佔「未消費」的量、Kafka 佔「retention 期內全部」的量。</li>
<li><strong>compact topic 給狀態類資料</strong>：如果只需要「每個 key 最新值」（像 RabbitMQ 不存在的場景）、用 <code>cleanup.policy=compact</code> 而非 time-based delete、避免無限長大。對應 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a> 卡的 retention policy。</li>
</ol>
<h2 id="漸進-cutoverdual-write-與-shadow-consume">漸進 cutover：dual-write 與 shadow consume</h2>
<p>paradigm shift 不能一次切換、因為 consumer 行為（offset 語意、ordering、DLQ、重試）全變了、需要在真實流量下驗證新 consumer 跟舊 consumer 結果一致才敢切。漸進 cutover 用兩個機制：</p>
<p><strong>dual-write</strong>：producer 同時往 RabbitMQ 跟 Kafka 寫同一份事件。RabbitMQ 端維持舊 consumer 正常生產、Kafka 端讓新 consumer 接收。dual-write 期間 RabbitMQ 仍是 source of truth、Kafka 只是並行驗證。要處理的細節是雙寫的一致性 —— 寫了 RabbitMQ 但 Kafka 寫失敗時怎麼辦、實務上通常容忍 Kafka 端短期缺漏（因為還沒切過去）、但要監控雙端的訊息數落差。</p>
<p><strong>shadow consume</strong>：新的 Kafka consumer 跑完整處理邏輯、但 <em>side effect 導到影子環境</em>（寫影子 DB、不發真實 webhook、不寄真實信）。把 Kafka consumer 的處理結果跟 RabbitMQ consumer 的真實結果比對、確認 ordering、去重、DLQ 行為都對齊。shadow 期是 paradigm 翻譯正確性的驗證窗口、不是效能測試。</p>
<p>cutover 是 per-workload 的：某個 workload shadow 驗證通過、就把它的真實消費切到 Kafka、停掉該 workload 的 RabbitMQ consumer 與 dual-write；其他 workload 維持原狀繼續驗證。不是全站一次切。</p>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RabbitMQ（self-managed）</th>
          <th>Kafka（self-managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster baseline</td>
          <td>1-3 node（含 management plugin）</td>
          <td>3-5 broker + KRaft controller</td>
      </tr>
      <tr>
          <td>RAM / node baseline</td>
          <td>4-16GB</td>
          <td>16-64GB</td>
      </tr>
      <tr>
          <td>Storage 模型</td>
          <td>未消費訊息量（ack 即刪）</td>
          <td>retention 期內全部訊息（與消費無關）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.2-0.5 FTE</td>
          <td>0.5-2 FTE</td>
      </tr>
      <tr>
          <td>額外運維元件</td>
          <td>通常無</td>
          <td>Schema Registry / Connect / 監控 lag</td>
      </tr>
      <tr>
          <td>Throughput / node</td>
          <td>數萬到數十萬 msg/s</td>
          <td>100K-1M+ msg/s</td>
      </tr>
      <tr>
          <td>Replay 能力</td>
          <td>無（ack 即刪）</td>
          <td>retention 期內任意 offset</td>
      </tr>
      <tr>
          <td>複雜 routing</td>
          <td>強（exchange + binding）</td>
          <td>弱（producer 端決定、broker 不路由）</td>
      </tr>
      <tr>
          <td>學習與運維成本</td>
          <td>低</td>
          <td>高（partition / offset / rebalance 都要懂）</td>
      </tr>
  </tbody>
</table>
<p>判讀：純 work queue 場景 RabbitMQ 的運維成本顯著低、Kafka 的 storage 跟運維是為了 replay 與高吞吐付的價。如果 workload 用不到 replay 跟跨 consumer group fan-out、遷到 Kafka 是用更高的成本換用不到的能力。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是-long-term-default">混合架構是 long-term default</h3>
<p>多數環境的終態是 RabbitMQ 與 Kafka 共存、各管各的責任：</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">[task 分派：寄信 / 轉檔 / webhook]        [event log：CDC / 事件總線 / replay]
</span></span><span class="line"><span class="ln">2</span><span class="cl">         RabbitMQ                                    Kafka
</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">         └──────── Bridge（Connect source / 自寫）────┘</span></span></code></pre></div><p>RabbitMQ 跑「處理即承諾」的任務隊列、Kafka 跑「寫入即承諾」的事件流。需要從任務流產生事件記錄時、用 Kafka Connect 的 RabbitMQ source connector 或自寫 bridge 把選定的訊息搬到 Kafka topic。</p>
<h3 id="跟-outbox-pattern-對位">跟 outbox pattern 對位</h3>
<p>從 RabbitMQ 遷往 Kafka 常伴隨 <em>資料庫交易與事件發布一致性</em> 的需求 —— 因為 event sourcing 場景要求事件不能丟。直接在交易中寫 Kafka 有雙寫一致性問題、應該走 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>：交易內只寫 outbox 表、再由 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Debezium CDC</a> 把 outbox 變更發到 Kafka topic。</p>
<h3 id="跟其他-migration-結構的對照">跟其他 migration 結構的對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Schema 差</th>
          <th>Operational 差</th>
          <th>Paradigm 差</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kafka ↔ NATS</td>
          <td>中</td>
          <td>中</td>
          <td><strong>高</strong></td>
          <td>partial + 混合</td>
      </tr>
      <tr>
          <td>RabbitMQ → Kafka（本篇）</td>
          <td>中</td>
          <td>中</td>
          <td><strong>高</strong></td>
          <td>partial + 混合</td>
      </tr>
  </tbody>
</table>
<p>兩篇都是 paradigm shift、都是 partial migration + 長期混合。差別在落差的方向：Kafka ↔ NATS 是 log vs subject messaging 的抽象層差異、RabbitMQ → Kafka 是 work queue vs event log 的責任模型差異 —— 後者的核心翻譯是「處理即承諾」如何重新表達成「寫入即承諾 + offset replay」。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> / <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></li>
<li>平行 migration playbook：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a></li>
<li>關鍵概念卡：<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition</a> / <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a> / <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a> / <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">Dead-letter queue</a> / <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack/nack</a></li>
<li>下游能力：<a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> / <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> / <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> overview 的 implementation-layer deep article。選型層（RabbitMQ vs Kafka / SQS、何時選 RabbitMQ）見 overview；本文只處理「決定用 RabbitMQ 後，失敗訊息怎麼 retry 才不會卡死隊列」。DLX 拓樸實機驗證於 rabbitmq:3-management、最後檢查日 2026-06-16；機制以 &lt;a href="https://www.rabbitmq.com/docs/dlx">RabbitMQ DLX 官方文件&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="失敗訊息-requeue-回隊首會卡住整條隊列">失敗訊息 requeue 回隊首，會卡住整條隊列&lt;/h2>
&lt;p>消費一則訊息失敗了——下游 API 超時、資料還沒就緒、暫時性錯誤。最直覺的處理是 &lt;code>nack&lt;/code> 加 &lt;code>requeue=true&lt;/code>，讓它重新排隊再試一次。問題是 RabbitMQ 的 requeue 把訊息放回&lt;strong>原隊列的隊首&lt;/strong>，於是它立刻又被同一個 consumer 取出、再次失敗、再 requeue……在「下游還沒恢復」的那段時間裡，這則訊息反覆佔據隊首，後面所有正常訊息全被卡住。這就是 head-of-line blocking：一則毒訊息（poison message）拖垮整條隊列。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &amp;#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&amp;#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">Indeed 每天處理 35M+ 職缺訊息&lt;/a>，原本的架構正是把失敗訊息 requeue 回隊首，造成阻塞。他們的解法是設計 &lt;strong>Requeue → Delay queue → Dead Letter Queue 三層 escalation&lt;/strong>：retry 幾次後讓訊息進延遲隊列（隔一段時間再試）、再失敗幾次才進 DLQ（停止重試、留待人工或專門處理）。這個案例揭露的核心原則是——&lt;strong>retry 策略要跟隊列拓樸一起設計，不是純 client 端的 backoff&lt;/strong>。&lt;/p>
&lt;p>本文展開 RabbitMQ 實現這套分層 retry 的機制（dead-letter exchange + TTL）、實機驗證的拓樸、以及把它寫成事故的踩坑。&lt;/p>
&lt;h2 id="核心概念dead-letter-exchange-的求值模型">核心概念：dead-letter exchange 的求值模型&lt;/h2>
&lt;p>RabbitMQ 的失敗訊息處理建立在 dead-letter exchange（DLX）上。理解它要抓住「訊息在什麼條件下被 dead-letter、去哪裡」。&lt;/p>
&lt;p>&lt;strong>訊息在三種情況被 dead-letter&lt;/strong>。一則訊息會從它所在的隊列被轉送到該隊列設定的 DLX：(1) 被 consumer &lt;code>nack&lt;/code> / &lt;code>reject&lt;/code> 且 &lt;code>requeue=false&lt;/code>；(2) 訊息 TTL 到期（&lt;code>x-message-ttl&lt;/code> 或 per-message expiration）；(3) 隊列達到長度上限（&lt;code>x-max-length&lt;/code>）被擠掉。這三種 reason 會記在訊息的 &lt;code>x-death&lt;/code> header 裡。&lt;/p>
&lt;p>&lt;strong>DLX 是隊列的屬性、不是訊息的&lt;/strong>。在宣告隊列時用 &lt;code>x-dead-letter-exchange&lt;/code> 指定這個隊列的「死信要送去哪個 exchange」，搭配 &lt;code>x-dead-letter-routing-key&lt;/code> 指定送過去時用什麼 routing key。死信被當成一則新訊息發布到那個 exchange，再依綁定路由到 DLQ。&lt;/p>
&lt;p>&lt;strong>TTL + DLX 組出「延遲隊列」&lt;/strong>。RabbitMQ 沒有原生的延遲投遞，但可以用「一個沒有 consumer、只設 TTL + DLX 的隊列」模擬：訊息進這個隊列、躺到 TTL 到期、被 dead-letter 回工作 exchange——等於延遲了 TTL 那麼久才重新可被消費。這是分層 retry 的關鍵積木。&lt;/p>
&lt;p>&lt;strong>&lt;code>x-death&lt;/code> header 累積重試歷史&lt;/strong>。每次 dead-letter，RabbitMQ 在 &lt;code>x-death&lt;/code> header 追加一筆記錄（哪個隊列、什麼 reason、次數 count）。消費端讀這個 count 就能判斷「這則訊息重試幾次了」，決定要再延遲還是進 DLQ。這是實現「retry n 次後升級」的依據。&lt;/p>
&lt;h2 id="配置work--delay--dlq-三層拓樸">配置：work → delay → DLQ 三層拓樸&lt;/h2>
&lt;p>實機驗證的最小 DLX 拓樸（工作隊列的訊息 TTL 到期後 dead-letter 到 DLQ）：&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"># 宣告 DLX exchange 與 DLQ&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> exchange &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>dlx &lt;span class="nv">type&lt;/span>&lt;span class="o">=&lt;/span>direct
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>dlq
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> binding &lt;span class="nv">source&lt;/span>&lt;span class="o">=&lt;/span>dlx &lt;span class="nv">destination&lt;/span>&lt;span class="o">=&lt;/span>dlq &lt;span class="nv">routing_key&lt;/span>&lt;span class="o">=&lt;/span>app.work
&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"># 工作隊列：設 TTL + 指向 DLX（TTL 到期或 nack(requeue=false) 都會 dead-letter）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>app.work &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> &lt;span class="nv">arguments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{&amp;#34;x-message-ttl&amp;#34;:2000,&amp;#34;x-dead-letter-exchange&amp;#34;:&amp;#34;dlx&amp;#34;,&amp;#34;x-dead-letter-routing-key&amp;#34;:&amp;#34;app.work&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 驗證：發一則、等 2s TTL 到期、它從 app.work 搬到 dlq&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">rabbitmqadmin publish &lt;span class="nv">routing_key&lt;/span>&lt;span class="o">=&lt;/span>app.work &lt;span class="nv">payload&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;poison-msg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># 等 TTL（2s）過期後（實測等 4s 確保）：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">rabbitmqctl list_queues name messages
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># app.work 0 ← TTL 到期被搬走&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1"># dlq 1 ← 落到 DLQ（訊息帶 x-death header、reason=expired）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機驗證於 rabbitmq:3-management（最後檢查日 2026-06-16）：publish 後等 TTL 過期，&lt;code>app.work&lt;/code> 歸零、&lt;code>dlq&lt;/code> 出現該訊息。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> overview 的 implementation-layer deep article。選型層（RabbitMQ vs Kafka / SQS、何時選 RabbitMQ）見 overview；本文只處理「決定用 RabbitMQ 後，失敗訊息怎麼 retry 才不會卡死隊列」。DLX 拓樸實機驗證於 rabbitmq:3-management、最後檢查日 2026-06-16；機制以 <a href="https://www.rabbitmq.com/docs/dlx">RabbitMQ DLX 官方文件</a> 為準。</p></blockquote>
<h2 id="失敗訊息-requeue-回隊首會卡住整條隊列">失敗訊息 requeue 回隊首，會卡住整條隊列</h2>
<p>消費一則訊息失敗了——下游 API 超時、資料還沒就緒、暫時性錯誤。最直覺的處理是 <code>nack</code> 加 <code>requeue=true</code>，讓它重新排隊再試一次。問題是 RabbitMQ 的 requeue 把訊息放回<strong>原隊列的隊首</strong>，於是它立刻又被同一個 consumer 取出、再次失敗、再 requeue……在「下游還沒恢復」的那段時間裡，這則訊息反覆佔據隊首，後面所有正常訊息全被卡住。這就是 head-of-line blocking：一則毒訊息（poison message）拖垮整條隊列。</p>
<p><a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">Indeed 每天處理 35M+ 職缺訊息</a>，原本的架構正是把失敗訊息 requeue 回隊首，造成阻塞。他們的解法是設計 <strong>Requeue → Delay queue → Dead Letter Queue 三層 escalation</strong>：retry 幾次後讓訊息進延遲隊列（隔一段時間再試）、再失敗幾次才進 DLQ（停止重試、留待人工或專門處理）。這個案例揭露的核心原則是——<strong>retry 策略要跟隊列拓樸一起設計，不是純 client 端的 backoff</strong>。</p>
<p>本文展開 RabbitMQ 實現這套分層 retry 的機制（dead-letter exchange + TTL）、實機驗證的拓樸、以及把它寫成事故的踩坑。</p>
<h2 id="核心概念dead-letter-exchange-的求值模型">核心概念：dead-letter exchange 的求值模型</h2>
<p>RabbitMQ 的失敗訊息處理建立在 dead-letter exchange（DLX）上。理解它要抓住「訊息在什麼條件下被 dead-letter、去哪裡」。</p>
<p><strong>訊息在三種情況被 dead-letter</strong>。一則訊息會從它所在的隊列被轉送到該隊列設定的 DLX：(1) 被 consumer <code>nack</code> / <code>reject</code> 且 <code>requeue=false</code>；(2) 訊息 TTL 到期（<code>x-message-ttl</code> 或 per-message expiration）；(3) 隊列達到長度上限（<code>x-max-length</code>）被擠掉。這三種 reason 會記在訊息的 <code>x-death</code> header 裡。</p>
<p><strong>DLX 是隊列的屬性、不是訊息的</strong>。在宣告隊列時用 <code>x-dead-letter-exchange</code> 指定這個隊列的「死信要送去哪個 exchange」，搭配 <code>x-dead-letter-routing-key</code> 指定送過去時用什麼 routing key。死信被當成一則新訊息發布到那個 exchange，再依綁定路由到 DLQ。</p>
<p><strong>TTL + DLX 組出「延遲隊列」</strong>。RabbitMQ 沒有原生的延遲投遞，但可以用「一個沒有 consumer、只設 TTL + DLX 的隊列」模擬：訊息進這個隊列、躺到 TTL 到期、被 dead-letter 回工作 exchange——等於延遲了 TTL 那麼久才重新可被消費。這是分層 retry 的關鍵積木。</p>
<p><strong><code>x-death</code> header 累積重試歷史</strong>。每次 dead-letter，RabbitMQ 在 <code>x-death</code> header 追加一筆記錄（哪個隊列、什麼 reason、次數 count）。消費端讀這個 count 就能判斷「這則訊息重試幾次了」，決定要再延遲還是進 DLQ。這是實現「retry n 次後升級」的依據。</p>
<h2 id="配置work--delay--dlq-三層拓樸">配置：work → delay → DLQ 三層拓樸</h2>
<p>實機驗證的最小 DLX 拓樸（工作隊列的訊息 TTL 到期後 dead-letter 到 DLQ）：</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"># 宣告 DLX exchange 與 DLQ</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> exchange <span class="nv">name</span><span class="o">=</span>dlx <span class="nv">type</span><span class="o">=</span>direct
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>dlq
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> binding <span class="nv">source</span><span class="o">=</span>dlx <span class="nv">destination</span><span class="o">=</span>dlq <span class="nv">routing_key</span><span class="o">=</span>app.work
</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"># 工作隊列：設 TTL + 指向 DLX（TTL 到期或 nack(requeue=false) 都會 dead-letter）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>app.work <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  <span class="nv">arguments</span><span class="o">=</span><span class="s1">&#39;{&#34;x-message-ttl&#34;:2000,&#34;x-dead-letter-exchange&#34;:&#34;dlx&#34;,&#34;x-dead-letter-routing-key&#34;:&#34;app.work&#34;}&#39;</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="c1"># 驗證：發一則、等 2s TTL 到期、它從 app.work 搬到 dlq</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">rabbitmqadmin publish <span class="nv">routing_key</span><span class="o">=</span>app.work <span class="nv">payload</span><span class="o">=</span><span class="s2">&#34;poison-msg&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 等 TTL（2s）過期後（實測等 4s 確保）：</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">rabbitmqctl list_queues name messages
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># app.work   0     ← TTL 到期被搬走</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># dlq        1     ← 落到 DLQ（訊息帶 x-death header、reason=expired）</span></span></span></code></pre></div><p>實機驗證於 rabbitmq:3-management（最後檢查日 2026-06-16）：publish 後等 TTL 過期，<code>app.work</code> 歸零、<code>dlq</code> 出現該訊息。</p>
<p>三層 escalation 的完整拓樸（對應 Indeed 模式）：</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">app.work（主工作隊列）
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └─ consumer nack(requeue=false) 或處理失敗
</span></span><span class="line"><span class="ln">3</span><span class="cl">       ↓ dead-letter 到
</span></span><span class="line"><span class="ln">4</span><span class="cl">app.retry（延遲隊列：x-message-ttl=30s、無 consumer、DLX 指回 app.work）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  └─ TTL 到期
</span></span><span class="line"><span class="ln">6</span><span class="cl">       ↓ dead-letter 回
</span></span><span class="line"><span class="ln">7</span><span class="cl">app.work（再次嘗試；消費端讀 x-death count）
</span></span><span class="line"><span class="ln">8</span><span class="cl">  └─ 重試達上限（例如 count &gt;= 3）→ 消費端主動 nack 到
</span></span><span class="line"><span class="ln">9</span><span class="cl">app.dlq（死信終點：無自動重試、人工 / 專門 consumer 處理）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>延遲時間靠 <code>app.retry</code> 的 TTL 控制；要指數退避就設多個不同 TTL 的 delay 隊列（30s / 5m / 1h）逐層升級</li>
<li>「重試幾次」由消費端讀 <code>x-death</code> 的 count 判斷、達上限才送終點 DLQ</li>
<li>DLQ 不該有自動重試的 consumer（否則又是迴圈）；它是給人看的、或給冪等的專門修復流程</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1requeue-回隊首毒訊息卡死整條隊列">Case 1：requeue 回隊首、毒訊息卡死整條隊列</h3>
<p><strong>徵兆</strong>：下游短暫故障期間，整條隊列的消費停滯、consumer CPU 衝高但吞吐歸零，恢復後發現大量正常訊息延遲。</p>
<p><strong>根因</strong>：失敗時用 <code>nack(requeue=true)</code>，訊息回到隊首被立刻重取、反覆失敗，head-of-line blocking。下游故障越久，毒訊息霸佔隊首越久。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>失敗一律 <code>nack(requeue=false)</code> 走 DLX，不要 requeue 回原隊列</li>
<li>用 delay 隊列（TTL + DLX）讓重試隔一段時間，給下游恢復時間</li>
<li>重試有上限，達上限進終點 DLQ，停止自動重試</li>
<li>這正是 <a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">Indeed 案例</a> 的核心教訓：retry 拓樸化，不要 requeue-to-head</li>
</ol>
<h3 id="case-2delay-隊列綁錯retry-變無限迴圈">Case 2：delay 隊列綁錯、retry 變無限迴圈</h3>
<p><strong>徵兆</strong>：某些訊息永遠在重試、<code>x-death</code> count 累積到幾百次，DLQ 卻一直是空的。</p>
<p><strong>根因</strong>：delay 隊列的 DLX 指回工作隊列，但消費端沒有檢查 <code>x-death</code> count、或上限判斷寫錯，訊息在 work ↔ retry 之間無限往返、永遠到不了終點 DLQ。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>消費端每次處理前讀 <code>x-death</code> 的 count，超過上限就主動投遞到終點 DLQ（不再走 retry）</li>
<li>上限判斷要涵蓋所有 retry 路徑，不要漏掉某條</li>
<li>監控 <code>x-death</code> count 分布，出現高 count 訊息代表升級邏輯漏了</li>
<li>終點 DLQ 絕對不要接會 nack-to-DLX 的 consumer，否則迴圈</li>
</ol>
<h3 id="case-3per-queue-ttl-的隊首阻塞陷阱">Case 3：per-queue TTL 的隊首阻塞陷阱</h3>
<p><strong>徵兆</strong>：用 <code>x-message-ttl</code> 設隊列級 TTL 做延遲，但發現訊息沒有按預期時間 dead-letter，延遲時間忽長忽短。</p>
<p><strong>根因</strong>：隊列級 TTL（<code>x-message-ttl</code>）只在訊息到達隊首時才檢查是否過期。如果用 per-message TTL 且不同訊息 TTL 不同，前面一則長 TTL 的訊息會擋住後面短 TTL 的——後者明明過期了卻因為不在隊首而沒被 dead-letter。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>delay 隊列用統一的隊列級 TTL（同一個 delay 隊列裡所有訊息延遲時間相同），不要在同隊列混用 per-message TTL</li>
<li>要多種延遲時間就開多個 delay 隊列（每個固定 TTL），不要靠 per-message TTL</li>
<li>理解 TTL 是「到隊首才檢查」的惰性求值，不是精準定時器</li>
<li>需要精準排程的延遲用專門的 delay 機制（rabbitmq-delayed-message-exchange plugin），不靠 TTL 模擬</li>
</ol>
<h3 id="case-4dlx-沒綁好死信靜默消失">Case 4：DLX 沒綁好、死信靜默消失</h3>
<p><strong>徵兆</strong>：訊息明明該 dead-letter，但 DLQ 一直收不到，訊息憑空消失。</p>
<p><strong>根因</strong>：DLX exchange 存在、隊列也設了 <code>x-dead-letter-exchange</code>，但 DLX 到 DLQ 的 binding 不存在或 routing key 對不上。死信被發布到 DLX 後沒有任何隊列接收（unroutable），直接被丟棄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 DLX → DLQ 的 binding 存在且 routing key 匹配（<code>x-dead-letter-routing-key</code> 對上 binding key）</li>
<li>沒設 <code>x-dead-letter-routing-key</code> 時死信沿用原 routing key，binding 要對應原 key</li>
<li>給 DLX 設 alternate exchange 或在 DLX 上掛一個 catch-all 隊列，避免 unroutable 死信靜默消失</li>
<li>監控 DLX 的 unroutable / drop 指標，死信消失是嚴重的資料遺失</li>
</ol>
<h3 id="case-5dlq-無上限成長變成第二個問題">Case 5：DLQ 無上限成長、變成第二個問題</h3>
<p><strong>徵兆</strong>：DLQ 累積到幾十萬則訊息、記憶體吃緊，沒人處理。</p>
<p><strong>根因</strong>：DLQ 是終點但沒有處理流程——訊息一直進、沒人消費，DLQ 變成一個越長越大的垃圾堆，最終吃光 broker 記憶體（classic queue 訊息在記憶體）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>DLQ 要有處理流程：告警 + 人工 / 自動修復 consumer（冪等地重新投遞或記錄）</li>
<li>DLQ 設 <code>x-max-length</code> 或自己的 TTL，避免無限成長（但要先確認丟棄可接受）</li>
<li>監控 DLQ 深度與成長速率，持續成長代表上游有系統性失敗、要根治而非堆 DLQ</li>
<li>quorum queue 對 DLQ 是合理選擇（持久、不純靠記憶體），見 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">quorum vs mirrored queue deep article</a></li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>分層 retry 拓樸的容量判讀：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主隊列消費吞吐</td>
          <td>穩定、無停滯</td>
          <td>歸零但有積壓 → 毒訊息 head-of-line blocking</td>
      </tr>
      <tr>
          <td><code>x-death</code> count 分布</td>
          <td>多數低（1-2 次成功）</td>
          <td>高 count 訊息多 → 下游系統性故障 / 升級邏輯漏</td>
      </tr>
      <tr>
          <td>DLQ 深度</td>
          <td>低且有處理流程</td>
          <td>持續成長 → 無人處理、會吃光記憶體</td>
      </tr>
      <tr>
          <td>delay 隊列堆積</td>
          <td>隨重試量波動、可消化</td>
          <td>持續堆高 → 重試量超過下游恢復速度</td>
      </tr>
      <tr>
          <td>unroutable 死信</td>
          <td>0</td>
          <td>&gt; 0 → DLX binding 錯、死信靜默遺失</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>重試量大、delay 隊列堆積</strong>：重試治標、下游系統性故障要根治；考慮 circuit breaker 在上游擋住而非無限重試。</li>
<li><strong>需要精準延遲排程</strong>：TTL 模擬的延遲不精準（惰性求值），用 rabbitmq-delayed-message-exchange plugin。</li>
<li><strong>DLQ / 隊列要持久可靠</strong>：classic queue 靠記憶體 + 鏡像，大量積壓有風險；用 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">quorum queue</a>（Raft 持久）。</li>
<li><strong>吞吐 / 保留需求超過 RabbitMQ</strong>：retry / replay 是 log-based broker 的強項，大規模 replay 走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（consumer 各自 offset、可重讀）。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>分層 retry 是 RabbitMQ 可靠消費的核心，它跟其他議題交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></strong>：DLQ 要持久才不會在 broker 重啟時丟失死信。</li>
<li><strong>跟 <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 design</a></strong>：prefetch / ack 策略決定毒訊息影響範圍，跟 retry 拓樸一起設計。</li>
<li><strong>跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></strong>：retry 與 DLQ 重新投遞都要求消費冪等，否則重試造成重複副作用。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">quorum vs mirrored queue</a></strong>：DLQ 與重試隊列的持久性選 quorum queue，避開 mirrored queue 的網路成本。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></li>
<li>同 vendor deep article：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">quorum vs mirrored queue</a></li>
<li>對應案例：<a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25 Indeed delay queue + DLQ 三層 escalation</a></li>
<li>上游概念：<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>、<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 design</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="同一份程式碼不同授權">同一份程式碼、不同授權&lt;/h2>
&lt;p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。&lt;/p>
&lt;p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是&lt;strong>授權&lt;/strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是&lt;strong>fork 後的分歧&lt;/strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。&lt;/p>
&lt;p>&lt;code>INFO server&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">valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version&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="c1"># redis_version:7.2.4 ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自己的演進線&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>redis_version:7.2.4&lt;/code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；&lt;code>valkey_version&lt;/code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B&lt;/h2>
&lt;p>跑 diff dimension audit，Redis → Valkey 全維度 Low：&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>Schema / API&lt;/td>
 &lt;td>同 Redis 7.2.4（fork 同源）、RESP 協定一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>同 redis.conf、同監控指標、同 CLI 命令&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同一份 code base 演進）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1（單服務換單服務）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>零（所有 Redis client library 直接相容）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB / AOF 檔案相容、可直接拷資料目錄&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>全 Low → &lt;strong>Type B drop-in&lt;/strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="同一份程式碼不同授權">同一份程式碼、不同授權</h2>
<p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。</p>
<p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是<strong>授權</strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是<strong>fork 後的分歧</strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。</p>
<p><code>INFO server</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">valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自己的演進線</span></span></span></code></pre></div><p><code>redis_version:7.2.4</code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；<code>valkey_version</code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。</p>
<h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B</h2>
<p>跑 diff dimension audit，Redis → Valkey 全維度 Low：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis 7.2.4（fork 同源）、RESP 協定一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 redis.conf、同監控指標、同 CLI 命令</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同一份 code base 演進）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1（單服務換單服務）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>零（所有 Redis client library 直接相容）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB / AOF 檔案相容、可直接拷資料目錄</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>全 Low → <strong>Type B drop-in</strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。</p>
<p>這個遷移的特殊之處是 driver 在資料層之外：它是<strong>授權 / 合規驅動</strong>。依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的漏類處理，政策 / 合規驅動的遷移資料層仍走 Type B，但 audit 重點多一塊<strong>授權驗證與證據收集</strong>。</p>
<h2 id="相容性-auditcutover-前要確認的清單">相容性 audit：cutover 前要確認的清單</h2>
<p>Valkey 號稱 100% 相容 Redis 7.2.4，但「100%」的邊界在 fork 之後的分歧。Pre-migration 必跑的 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>Valkey 相容程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core data types / commands / RESP</td>
          <td>完全相容（fork 自 7.2.4）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB / AOF 檔案格式</td>
          <td>完全相容（可直接拷資料目錄）</td>
          <td>無需轉檔</td>
      </tr>
      <tr>
          <td>Eviction / persistence / pub-sub</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Client libraries</td>
          <td>完全相容（透過 redis_version 協商）</td>
          <td>無需改 code</td>
      </tr>
      <tr>
          <td>Cluster / Sentinel</td>
          <td>完全相容（同 Redis 模型）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Redis 7.4+ 新功能（fork 後新增）</td>
          <td>Valkey 不一定跟進</td>
          <td>盤點是否用到、確認 Valkey 對應</td>
      </tr>
      <tr>
          <td>Redis Stack 商業 module（JSON/Search）</td>
          <td>不相容（Valkey 有 valkey-search / valkey-bloom）</td>
          <td>盤點 module 使用、確認替代或改寫</td>
      </tr>
      <tr>
          <td>RedisInsight 等 Redis Inc 監控工具</td>
          <td>部分 vendor-specific 命令缺</td>
          <td>改通用工具（valkey-cli / redis_exporter）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：兩份清單——(1) 用到的 Redis 7.4+ 功能（fork 後新增、Valkey 可能沒有）、(2) 載入的 Redis Stack module。這兩塊是僅有的相容風險，其餘資料層零工作。盤點方法：</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"># 盤點載入的 module（最大相容風險）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli MODULE LIST
</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"># 盤點是否用到 7.4+ 功能（抓 production traffic 對照 Redis 7.4 changelog）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli MONITOR    <span class="c1"># 限時抓樣、grep 可疑的新命令</span></span></span></code></pre></div><h2 id="step-by-step-cutover">Step-by-step cutover</h2>
<p>因為 RDB 檔案相容，cutover 比 DragonflyDB 更簡單（無版本轉換風險）：</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. 部署 Valkey（同 Redis 配置、可直接沿用 redis.conf）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name valkey -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/valkey:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  valkey/valkey:8 valkey-server /etc/valkey/valkey.conf
</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"># 2. Redis 端 BGSAVE 產生 RDB</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</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="c1"># 3. 把 dump.rdb 拷給 Valkey（檔案格式相容、無需轉換）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb valkey-host:/data/valkey/
</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="c1"># 4. 重啟 Valkey 載入 RDB</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">docker restart valkey
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 5. 驗證資料一致 + 版本</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> DBSIZE          <span class="c1"># 對齊 Redis DBSIZE</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> INFO server <span class="p">|</span> grep redis_version  <span class="c1"># 7.2.4</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 6. 替代方案（零停機）：用 replicaof 讓 Valkey 當 Redis 的 replica、即時同步後 promote</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">#    valkey-cli -h valkey-host REPLICAOF redis-primary 6379</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">#    重要邊界：此路徑只在 source 是 Redis 7.2 或更早版本時成立。</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1">#    Redis 7.4+（Community Edition）改了複製格式、Valkey 無法當其 replica</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1">#    → source 為 7.4+ 時改走上面的 RDB 拷貝路徑（步驟 2-4）。</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 7. Cutover：client 配置切到 Valkey endpoint、Redis 留 standby</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>RDB 拷貝 + load</strong>：100GB 約 5-15 分鐘（無版本轉換、比 DragonflyDB 少一道風險）</li>
<li><strong>replicaof 路徑</strong>：要零停機可讓 Valkey 當 Redis replica 即時同步、確認 lag 趨零後 promote + 切 client（僅限 source 為 Redis 7.2 或更早；7.4+ 複製格式已分歧、不適用、改走 RDB 拷貝）</li>
<li><strong>Cutover</strong>：client 配置切換（單次完成、硬邊界）、Redis 留 standby 1-2 週</li>
<li><strong>Decom</strong>：無相容問題後關閉 Redis</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用到-redis-74-功能valkey-沒有">Case 1：用到 Redis 7.4+ 功能、Valkey 沒有</h3>
<p><strong>徵兆</strong>：cutover 後某功能報 <code>unknown command</code> 或行為不同，命令是 Redis 在 7.4 之後（fork 點之後）才加的。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，Redis 7.4+ 新增的功能 Valkey 不一定跟進。pre-migration audit 漏掉了這些 fork 後的新功能。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 對照 Redis 7.4+ changelog 盤點用到的新功能（audit 清單第一項）</li>
<li>Valkey 有對應就確認版本、沒有就評估改寫或留在 Redis 商業版</li>
<li>多數標準 cache 用法不碰 7.4+ 新功能，這個風險集中在用了較新進階功能的部署</li>
<li>Valkey 自己的 roadmap（valkey.io）會逐步補上 Redis 新功能，可追蹤</li>
</ol>
<h3 id="case-2載入了-redis-stack-商業-module">Case 2：載入了 Redis Stack 商業 module</h3>
<p><strong>徵兆</strong>：cutover 後 <code>JSON.SET</code> / <code>FT.SEARCH</code> 報 <code>unknown command</code>，application 部分功能失效。</p>
<p><strong>根因</strong>：用了 Redis Stack 的商業 module（RedisJSON / RedisSearch），這些不在 fork 範圍。Valkey 有自己的 valkey-search / valkey-bloom，但不是同一套命令、要另外安裝。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration <code>MODULE LIST</code> 盤點所有載入的 module（audit 清單第二項）</li>
<li>確認 Valkey 對應替代（valkey-search 對 RedisSearch）、確認命令相容度</li>
<li>沒有對應的評估改 module-free 設計（JSON 操作拉回 application 層）或留在 Redis Inc 商業版</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性 deep article</a> 的三層相容邊界</li>
</ol>
<h3 id="case-3以為換-valkey-解決了記憶體--fork-問題">Case 3：以為換 Valkey 解決了記憶體 / fork 問題</h3>
<p><strong>徵兆</strong>：因為 Redis 的 OOM 或 fork 延遲尖峰而遷 Valkey，遷完發現同樣問題還在。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，繼承了完全相同的記憶體模型、eviction 演算法、AOF/RDB fork 機制。這些行為在 Valkey 上一模一樣——遷移沒有改變它們。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / fork 調校在 Valkey 上跟 Redis 完全相同，直接套用 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體調校</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></li>
<li>遷 Valkey 的理由應是授權合規 / 多執行緒吞吐 / managed 成本，不是記憶體問題</li>
<li>fork 尖峰要根治走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 fork-less，不是換 Valkey</li>
<li>遷移前釐清痛點是授權（Valkey 解）還是架構（Valkey 不解）</li>
</ol>
<h3 id="case-4授權合規驗證沒做完整合規卡關">Case 4：授權合規驗證沒做完整、合規卡關</h3>
<p><strong>徵兆</strong>：技術遷移完成、但法務 / 合規 review 要求證明「不再使用 RSALv2 / SSPL 授權的軟體」，缺少證據。</p>
<p><strong>根因</strong>：這個遷移的 driver 是授權合規，但團隊只做了技術 cutover、沒收集合規證據。Redis 的 binary / image / 相依套件若還殘留在某些環境，合規目標沒真正達成。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>盤點所有環境（dev / staging / prod / CI）的 Redis binary / image / 相依，確認全部換成 Valkey</li>
<li>收集合規證據：image SBOM、套件清單、部署 manifest 顯示 Valkey BSD 授權</li>
<li>把「不再使用非 OSI 授權 cache」寫成可驗證的 CI 檢查（掃 image / 依賴）</li>
<li>依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的合規驅動漏類，audit 重點就是 evidence collection</li>
</ol>
<h3 id="case-5監控-dashboard-部分指標斷掉">Case 5：監控 dashboard 部分指標斷掉</h3>
<p><strong>徵兆</strong>：cutover 後 RedisInsight 或某監控 dashboard 部分面板空白、vendor-specific 命令回錯。</p>
<p><strong>根因</strong>：RedisInsight 等 Redis Inc 工具有部分偏商業版的命令，Valkey 不一定實作。核心指標通用，但進階面板可能缺。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>監控改用通用工具：valkey-cli INFO、Prometheus + redis_exporter（相容 Valkey）、Grafana</li>
<li>核心指標（used_memory / keyspace_hits / connected_clients）在 Valkey 完全相容、覆蓋不受影響</li>
<li>把監控相容性納入 cutover 前驗證、不要遷完才發現面板空白</li>
<li>RedisInsight 連 Valkey 多數仍可用、只是部分 vendor 進階面板缺</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>Valkey（self-managed）</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>授權</td>
          <td>RSALv2 / SSPL（非 OSI）</td>
          <td>BSD 3-clause（OSI、Linux Foundation）</td>
          <td>Valkey 對合規敏感場景是決定性優勢</td>
      </tr>
      <tr>
          <td>核心效能</td>
          <td>baseline</td>
          <td>同 Redis 7.2.4 + 8.x 多執行緒選項</td>
          <td>Valkey 多核 workload 可更高（依 workload）</td>
      </tr>
      <tr>
          <td>相容度</td>
          <td>原生</td>
          <td>100%（fork、檔案相容）</td>
          <td>平手（同源）</td>
      </tr>
      <tr>
          <td>記憶體 / fork</td>
          <td>baseline</td>
          <td>完全相同（同源）</td>
          <td>平手（遷移不改變這層）</td>
      </tr>
      <tr>
          <td>7.4+ 新功能</td>
          <td>有</td>
          <td>不一定跟進</td>
          <td>Redis 領先（用到才在意）</td>
      </tr>
      <tr>
          <td>Redis Stack module</td>
          <td>RedisJSON / Search / Graph</td>
          <td>valkey-search / valkey-bloom（不同套）</td>
          <td>Redis 商業 module 較全</td>
      </tr>
      <tr>
          <td>managed 選項</td>
          <td>ElastiCache for Redis（legacy）</td>
          <td>ElastiCache for Valkey（AWS default、約低 20%）</td>
          <td>Valkey 在 AWS 生態成本優勢</td>
      </tr>
      <tr>
          <td>遷移成本</td>
          <td>—</td>
          <td>極低（drop-in + 檔案相容）</td>
          <td>Valkey 是最容易的遷移目標</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：合規敏感（公部門 / 企業 OSI 政策）或想降 managed 成本 → 遷 Valkey（drop-in、風險集中在 module / 7.4+ 盤點）；重度依賴 Redis Stack 商業 module → 留 Redis Inc 商業版。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-elasticache-for-valkey-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a> 對位</h3>
<p>AWS 已把 ElastiCache default engine 設為 Valkey（約低 Redis 20%）。自管 Redis → ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位，但要同時處理 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a>（failover / cluster mode / client 重連）。</p>
<h3 id="跟-client--監控整合">跟 client / 監控整合</h3>
<p>client library 零改（透過 redis_version 協商）；監控把 exporter 指向 Valkey 即可（redis_exporter 相容）、RedisInsight 部分面板需換通用工具。</p>
<h3 id="跟-valkey-8-多執行緒對位">跟 Valkey 8 多執行緒對位</h3>
<p>遷移後可評估開 Valkey 8 的 io-threads 榨多核吞吐（Redis 7.2.4 沒有的能力），見 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性與 io-threads deep article</a>。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>反向遷移</strong>（Valkey → Redis）：僅在重度依賴 Redis 7.4+ 功能或 Stack 商業 module 時需要、同樣 drop-in</li>
<li><strong>跨雲 managed Valkey</strong>：GCP Memorystore / Azure Cache 的 Valkey 支援陸續推出、評估 vendor boundary</li>
<li><strong>授權合規 CI 化</strong>：把「不使用非 OSI 授權 cache」寫成持續檢查</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（重寫型 drop-in）、<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type B drop-in + 合規驅動漏類）</li>
</ul>
]]></content:encoded></item><item><title>Redis Streams → Kafka：從 embedded stream 長成 dedicated event streaming</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/migrate-to-kafka/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/migrate-to-kafka/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a>。對位 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> 的 &lt;em>paradigm shift&lt;/em> 模板 — 兩端不是同類產品的不同實作、是不同抽象層的系統：一個是 Redis 行程內的 append-only log data structure、一個是專用的 distributed event streaming platform。&lt;/p>&lt;/blockquote>
&lt;h2 id="redis-streams-跟-kafka-是不同抽象層的東西">Redis Streams 跟 Kafka 是不同抽象層的東西&lt;/h2>
&lt;p>Redis Streams 是 Redis 行程內的一個 data structure、Kafka 是一整套獨立的 distributed event streaming platform。這個區別決定整趟遷移的性質：要把 messaging 能力從「既有 Redis 行程的一塊記憶體」搬到「自成一格、要獨立運維的多節點叢集」，遠超過換個相容 broker 的工作量。&lt;/p>
&lt;p>Redis Streams 的責任邊界是「在已經跑著的 Redis 裡多一個 append-only log」。它共用 Redis 的記憶體、持久化（AOF / RDB）、failover（Sentinel / Cluster）跟運維團隊。寫入用 &lt;code>XADD&lt;/code>、消費用 &lt;code>XREADGROUP&lt;/code>，consumer group 跟 pending entries list（PEL）都活在同一個 Redis 行程。它的設計取捨偏向「低延遲、低運維增量、跟 Redis 生命週期綁定」。&lt;/p>
&lt;p>Kafka 的責任邊界是「成為跨系統的事件總線」。它把訊息寫成 partition 化的 log、落在獨立 broker 的磁碟、用 replication 保護、用 consumer group offset 追蹤各 consumer 進度，可長期保留並隨意 replay。它的設計取捨偏向「寫入即承諾、磁碟級長期保留、多 consumer 各自重播、水平擴展吞吐」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Redis Streams&lt;/th>
 &lt;th>Kafka&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>部署形態&lt;/td>
 &lt;td>Redis 行程內的 data structure&lt;/td>
 &lt;td>獨立 broker 叢集（3-5 broker + KRaft）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存後端&lt;/td>
 &lt;td>RAM-bound（受 &lt;code>maxmemory&lt;/code> 限制）&lt;/td>
 &lt;td>Broker 本地磁碟（可加 tiered storage to S3）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>拓樸單位&lt;/td>
 &lt;td>單一 stream key（綁單一 shard）&lt;/td>
 &lt;td>Topic + 多 partition（跨 broker 分布）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention 機制&lt;/td>
 &lt;td>&lt;code>MAXLEN&lt;/code> / &lt;code>MINID&lt;/code>、application 主動 trim&lt;/td>
 &lt;td>Broker 端 retention policy（time / size）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>消費進度&lt;/td>
 &lt;td>PEL + &lt;code>XACK&lt;/code>（broker 維護待 ack 集合）&lt;/td>
 &lt;td>Consumer offset commit（per partition）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗接管&lt;/td>
 &lt;td>&lt;code>XCLAIM&lt;/code> / &lt;code>XAUTOCLAIM&lt;/code>（手動 / 半自動）&lt;/td>
 &lt;td>Rebalance protocol（broker 協調自動分配）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replay&lt;/td>
 &lt;td>從 entry ID 重讀（受 retention 內資料限制）&lt;/td>
 &lt;td>從任意 offset 重讀（受磁碟 retention 限制）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲&lt;/td>
 &lt;td>亞毫秒（記憶體操作）&lt;/td>
 &lt;td>5-50ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>運維增量&lt;/td>
 &lt;td>近乎零（沿用 Redis）&lt;/td>
 &lt;td>顯著（多養一套叢集 + schema / connect 生態）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵在「拓樸單位」這列。Redis Streams 的一個 stream key 只能落在單一 shard、沒有 partition 概念，吞吐與資料量受單 shard 的記憶體與單執行緒處理能力封頂。Kafka 的 topic 天然切成多 partition、分散到多 broker，這是兩者在規模上的分水嶺，也是後面所有對位與故障演練的根。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a> 跟 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。對位 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 的 <em>paradigm shift</em> 模板 — 兩端不是同類產品的不同實作、是不同抽象層的系統：一個是 Redis 行程內的 append-only log data structure、一個是專用的 distributed event streaming platform。</p></blockquote>
<h2 id="redis-streams-跟-kafka-是不同抽象層的東西">Redis Streams 跟 Kafka 是不同抽象層的東西</h2>
<p>Redis Streams 是 Redis 行程內的一個 data structure、Kafka 是一整套獨立的 distributed event streaming platform。這個區別決定整趟遷移的性質：要把 messaging 能力從「既有 Redis 行程的一塊記憶體」搬到「自成一格、要獨立運維的多節點叢集」，遠超過換個相容 broker 的工作量。</p>
<p>Redis Streams 的責任邊界是「在已經跑著的 Redis 裡多一個 append-only log」。它共用 Redis 的記憶體、持久化（AOF / RDB）、failover（Sentinel / Cluster）跟運維團隊。寫入用 <code>XADD</code>、消費用 <code>XREADGROUP</code>，consumer group 跟 pending entries list（PEL）都活在同一個 Redis 行程。它的設計取捨偏向「低延遲、低運維增量、跟 Redis 生命週期綁定」。</p>
<p>Kafka 的責任邊界是「成為跨系統的事件總線」。它把訊息寫成 partition 化的 log、落在獨立 broker 的磁碟、用 replication 保護、用 consumer group offset 追蹤各 consumer 進度，可長期保留並隨意 replay。它的設計取捨偏向「寫入即承諾、磁碟級長期保留、多 consumer 各自重播、水平擴展吞吐」。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis Streams</th>
          <th>Kafka</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署形態</td>
          <td>Redis 行程內的 data structure</td>
          <td>獨立 broker 叢集（3-5 broker + KRaft）</td>
      </tr>
      <tr>
          <td>儲存後端</td>
          <td>RAM-bound（受 <code>maxmemory</code> 限制）</td>
          <td>Broker 本地磁碟（可加 tiered storage to S3）</td>
      </tr>
      <tr>
          <td>拓樸單位</td>
          <td>單一 stream key（綁單一 shard）</td>
          <td>Topic + 多 partition（跨 broker 分布）</td>
      </tr>
      <tr>
          <td>Retention 機制</td>
          <td><code>MAXLEN</code> / <code>MINID</code>、application 主動 trim</td>
          <td>Broker 端 retention policy（time / size）</td>
      </tr>
      <tr>
          <td>消費進度</td>
          <td>PEL + <code>XACK</code>（broker 維護待 ack 集合）</td>
          <td>Consumer offset commit（per partition）</td>
      </tr>
      <tr>
          <td>失敗接管</td>
          <td><code>XCLAIM</code> / <code>XAUTOCLAIM</code>（手動 / 半自動）</td>
          <td>Rebalance protocol（broker 協調自動分配）</td>
      </tr>
      <tr>
          <td>Replay</td>
          <td>從 entry ID 重讀（受 retention 內資料限制）</td>
          <td>從任意 offset 重讀（受磁碟 retention 限制）</td>
      </tr>
      <tr>
          <td>延遲</td>
          <td>亞毫秒（記憶體操作）</td>
          <td>5-50ms</td>
      </tr>
      <tr>
          <td>運維增量</td>
          <td>近乎零（沿用 Redis）</td>
          <td>顯著（多養一套叢集 + schema / connect 生態）</td>
      </tr>
  </tbody>
</table>
<p>關鍵在「拓樸單位」這列。Redis Streams 的一個 stream key 只能落在單一 shard、沒有 partition 概念，吞吐與資料量受單 shard 的記憶體與單執行緒處理能力封頂。Kafka 的 topic 天然切成多 partition、分散到多 broker，這是兩者在規模上的分水嶺，也是後面所有對位與故障演練的根。</p>
<h2 id="先確認是不是真的該遷多數中小規模不該遷">先確認是不是真的該遷：多數中小規模不該遷</h2>
<p>決定遷移前先做反向確認：在中小規模、且團隊已熟 Redis 的情境，Redis Streams 往往已經夠用，把它換成 Kafka 多半是引入運維負擔而非解決問題。遷移的正當理由來自規模或保留需求真的超出 Redis Streams 的能力邊界，而不是 Kafka 更主流。</p>
<p><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。">Arcjet</a> 的方向恰好相反、值得當反向參照。Arcjet 的 security / bot detection 平台需要低延遲請求處理，原本評估 Kafka，發現 managed Kafka 要六位數美元年費、自管運維難度也高；他們把既有的 Redis cache 層升級成 Streams，總成本掉到約一千美元年費。代價是 Redis Streams 沒有自動 retention，他們自寫一個 Janitor process，依約每分鐘一百則的實際處理速度監測 stream 長度跟 consumer group 狀態、selectively trim。</p>
<p>Arcjet 的判讀對遷移方向的啟示：當 workload 是低延遲、資料量留在記憶體可承受的範圍、團隊本來就在跑 Redis，Redis Streams 是務實且便宜的選擇；願意自寫 retention 工具就能補上它缺的治理能力。這條路成立時，遷去 Kafka 是用六位數年費跟一整套叢集運維，去換一個現有方案已能覆蓋的需求。</p>
<p><a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso</a> 是另一個 Redis Streams 站得住的高壓案例。Bitso 的撮合引擎微服務要扛每秒上千則訊息、亞毫秒延遲、撐住 BTC 價格暴動的尖峰；他們先後評估 Kafka（延遲不符）跟 SQS（vendor lock-in + 延遲）後選 Redis Streams，自建一層 Reliable Streams 抽象封裝 PEL + retry + DLQ，走 idempotent processing 接受重複勝過遺失。Bitso 揭露 Redis Streams 是「資料結構」而非「broker 系統」，可靠性責任在 application 層；但在亞毫秒延遲是硬指標的撮合場景，這個取捨反而讓 Redis Streams 勝過 Kafka。</p>
<p>兩個案例共同點：當延遲是硬指標、資料量在 RAM 可承受範圍、團隊能自建缺的治理層，Redis Streams 就站得住。遷去 Kafka 的決策該建立在這些前提不再成立之上，而不是建立在 Kafka 更有名之上。</p>
<h2 id="真正該遷的訊號">真正該遷的訊號</h2>
<p>決定遷移的依據是 Redis Streams 的三個能力邊界被實際 workload 突破：retention 需求超出 RAM 的成本曲線、需要長期 replay、consumer group 或 partition 規模超出單一 Redis 行程。三個訊號中任一個被觸發、且自建工具補不回來時，遷去 Kafka 才划算。</p>
<p>第一個訊號是 retention 超出 RAM 的成本翻轉。Redis Streams 的資料活在記憶體，保留越久、stream 越長、佔的 RAM 越多，而 RAM 是 Redis 叢集裡最貴的資源。當 retention 需求從「幾小時的緩衝」長到「數天到數週的事件保留」，把這些資料留在 RAM 的成本會快速超過 Kafka 把同樣資料留在 broker 磁碟（甚至 tiered storage 到 S3）的成本。<a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 退場案例</a>就是這條線被突破的反例 — 把 Redis 當長期事件儲存（Stream 是其中一塊），事件量每週以 GB 成長、AOF fsync 與 EBS I/O 變成 latency 痛點，最終退回 PostgreSQL。成本曲線翻轉是最常見、也最該觸發遷移的訊號。</p>
<p>第二個訊號是需要長期 replay。事件溯源（event sourcing）或合規稽核場景，需要保留並重播數週、數月甚至數年的歷史事件。Redis Streams 的 replay 只能重讀 retention 內還在的資料，而 retention 受 RAM 限制無法拉得很長；Kafka 的磁碟保留加 tiered storage 讓長期 replay 變成 first-class 能力。當 replay 視窗的需求超出 RAM 能承受的 retention，這個訊號成立。</p>
<p>第三個訊號是 consumer group 或 partition 規模超出單一 Redis。Redis Streams 的單一 stream key 綁在單一 shard，吞吐受單 shard 封頂、沒有 partition 可以水平拆分並行度；要跨 shard 只能手動用 hash tag 切成多個獨立 stream，application 自己路由。當單一邏輯 stream 的吞吐需求、或 consumer 並行度需求超過單 shard 能給的，且手動切 stream 的複雜度已經失控，Kafka 的原生 partition 才值得換。</p>
<p>這三個訊號之外，還有一個放大條件：是否需要 Kafka 生態（Schema Registry、Connect / Debezium CDC、Streams 流處理）。如果遷移同時要接上 CDC pipeline 或 schema 強制治理，那 Kafka 帶來的不只是 retention 跟 partition、而是整套生態，這會讓遷移的價值天平更傾向 Kafka。但若只是想要更長 retention、生態用不到，先評估 Redis tiered 方案或自建 Janitor 是否更便宜。</p>
<h2 id="概念對位xaddxreadgroupxackmaxlenxclaim">概念對位：XADD/XREADGROUP/XACK/MAXLEN/XCLAIM</h2>
<p>遷移的核心工作是把 Redis Streams 的五個核心操作對應到 Kafka 的等價概念、並理解每個對位背後語意的偏移，這比換 SDK 重得多。直接照字面搬會在 retention、消費進度、失敗接管三處踩雷，這三處正是後面故障演練的來源。</p>
<table>
  <thead>
      <tr>
          <th>Redis Streams 操作</th>
          <th>Kafka 等價</th>
          <th>語意偏移</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>XADD stream * field val</code></td>
          <td><code>producer.send(topic, key, val)</code></td>
          <td>Kafka 用 key 決定 partition、Redis 單 stream 無 partition</td>
      </tr>
      <tr>
          <td><code>XREADGROUP GROUP g c</code></td>
          <td>consumer group + <code>poll()</code></td>
          <td>Kafka rebalance 自動分配 partition、Redis 要手動 <code>XCLAIM</code></td>
      </tr>
      <tr>
          <td><code>XACK stream g id</code></td>
          <td>offset commit</td>
          <td>PEL 是逐則待 ack 集合、offset 是單調位移、語意不同</td>
      </tr>
      <tr>
          <td><code>MAXLEN</code> / <code>MINID</code> / <code>XTRIM</code></td>
          <td>retention policy（time / size）</td>
          <td>application 主動 trim → broker 端被動 retention</td>
      </tr>
      <tr>
          <td><code>XCLAIM</code> / <code>XAUTOCLAIM</code></td>
          <td>rebalance protocol</td>
          <td>手動 / 半自動接管 → broker 協調自動 reassign</td>
      </tr>
  </tbody>
</table>
<p><code>XADD</code> 對 <code>producer.send</code> 的最大偏移是 partition key。Redis 的單一 stream key 沒有 partition，所有 entry 都在同一條序列上嚴格有序；Kafka 把訊息依 key 雜湊分到不同 partition，只有同一 partition 內保證有序。遷移時要決定哪個欄位當 partition key、這個決定同時決定了 ordering 的範圍跟 hot partition 的風險。</p>
<p><code>XREADGROUP</code> 對 consumer group 的偏移在 rebalance。Redis consumer group 沒有自動 rebalance，consumer 掛掉後它名下未 ack 的訊息留在 PEL，要靠其他 consumer 主動 <code>XCLAIM</code> 接管；Kafka 的 consumer group 有 rebalance protocol，consumer 加入或離開時 broker 自動把 partition 重新分配。從手動接管搬到自動 rebalance，application 端負責接管的那段邏輯可以刪掉、但要改成理解 rebalance 行為。</p>
<p><code>XACK</code> 對 offset commit 是最容易誤用的一處，獨立成下一節的故障演練。<code>MAXLEN</code> 對 retention policy 是成本模型翻轉的核心，也獨立成故障演練。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1retention-模型從-ram-限制翻成-log-成本磁碟與成本失準">Case 1：Retention 模型從 RAM 限制翻成 log 成本，磁碟與成本失準</h3>
<p><strong>徵兆</strong>：團隊把 Redis Streams 的 <code>MAXLEN 100000</code>（保留最近十萬則、控制 RAM）習慣直接對映成 Kafka 的某個數字，結果 cutover 後不是 broker 磁碟暴漲超出預期、就是資料保留遠短於業務需要、replay 視窗對不上。</p>
<p><strong>根因</strong>：Redis Streams 的 <code>MAXLEN</code> 是 application 在每次 <code>XADD</code> 主動修剪的「條數上限」，目的是壓住 RAM 佔用，是一個 count-based 的記憶體預算旋鈕。Kafka 的 retention 是 broker 端被動執行的 policy、預設是 time-based（<code>retention.ms</code>）或 size-based（<code>retention.bytes</code>），目的是控制磁碟保留窗，而磁碟比 RAM 便宜一到兩個數量級。兩者的單位、執行主體、成本曲線都不同 — 把「保留十萬則以省 RAM」直接搬成 Kafka 設定，會錯估磁碟用量，也會把 Redis 時代「為了省 RAM 而被迫短保留」的限制錯誤地帶進一個本來就能長保留的系統。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>從業務需求重算 retention、不沿用 Redis 的 RAM 預算</strong>：Redis 的 <code>MAXLEN</code> 數字是 RAM 成本的妥協、不是業務的真實保留需求；遷移時回到「業務需要 replay 多久」重新算 <code>retention.ms</code>，這正是遷移要解鎖的能力。</li>
<li><strong>改用 time-based 為主、size-based 當保險絲</strong>：Kafka 設 <code>retention.ms</code> 對齊業務 replay 窗、再設 <code>retention.bytes</code> 防單 partition 磁碟失控。</li>
<li><strong>長保留接 tiered storage</strong>：retention 需求拉到數週數月時，把冷資料分層到 S3、熱資料留本地磁碟，成本曲線進一步壓平，而這在 Redis 的 RAM 模型下做不到。</li>
</ol>
<h3 id="case-2pel-觀念被帶進-offset造成重複或漏消費">Case 2：PEL 觀念被帶進 offset，造成重複或漏消費</h3>
<p><strong>徵兆</strong>：遷移後 consumer 出現「明明處理過的訊息又被重新消費」或「某些訊息整批沒被處理」；團隊照 Redis 時代「逐則 <code>XACK</code>」的心智模型管理 Kafka offset commit，結果對不上。</p>
<p><strong>根因</strong>：PEL 跟 offset 是兩個不同的進度模型。Redis Streams 的 PEL 是 broker 維護的「逐則待 ack 集合」，每則訊息獨立追蹤是否已 ack，consumer 可以亂序 ack 某幾則、其他留在 PEL；<code>XACK</code> 是針對特定 entry ID 的點狀確認。Kafka 的 offset 是 per partition 的單調位移、代表「這個位置之前都算消費完」，commit offset N 意味著 0 到 N-1 全部視為已處理。把 PEL 的逐則語意套到 offset 上會出兩種錯：一是處理完亂序的訊息後 commit 了較大的 offset，中間沒處理完的訊息被當成已消費而漏掉；二是 commit 時機錯置（auto-commit 在處理前就 commit），crash 後從錯誤位置重讀造成重複。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>理解 offset 是區間承諾、不是逐則確認</strong>：commit offset 前確保該 offset 之前的訊息都已處理完、不要對亂序處理的批次 commit 最大 offset。</li>
<li><strong>關 auto-commit、改 manual commit 在處理之後</strong>：<code>enable.auto.commit=false</code>，處理完一批再 commit，對齊 at-least-once。</li>
<li><strong>保留 application 端 idempotency</strong>：這點從 Redis 時代就該有、遷到 Kafka 仍成立 — at-least-once 下重複難免，用 message ID + dedup store 顯式去重，對位 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>跟 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 的 idempotent processing</a>。</li>
</ol>
<h3 id="case-3單-stream-key-換成多-partitionordering-假設破裂">Case 3：單 stream key 換成多 partition，ordering 假設破裂</h3>
<p><strong>徵兆</strong>：遷移前所有事件在單一 Redis stream 上嚴格有序、downstream 依賴這個順序（例如同一筆訂單的 created → paid → shipped）；切到 Kafka 多 partition 後，同一筆訂單的事件被分到不同 partition、處理順序錯亂。</p>
<p><strong>根因</strong>：Redis Streams 的單一 stream key 綁單一 shard、所有 entry 在一條序列上全域有序，application 不需要思考 ordering 範圍就免費得到全序。Kafka 把 topic 切成多 partition 來換取水平吞吐，代價是只保證 <em>同一 partition 內</em> 有序、partition 之間無序。遷移時若沒指定 partition key、訊息會被 round-robin 或依預設雜湊散開，同一個業務實體（訂單、帳戶、裝置）的事件落到不同 partition，全序假設就破了。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>用業務實體當 partition key</strong>：把需要保序的實體 ID（訂單 ID、帳戶 ID）當 Kafka message key，同 key 雜湊到同 partition、partition 內保序，把「全域有序」收斂成「per-entity 有序」這個多數業務真正需要的粒度。</li>
<li><strong>辨識哪些流真的需要全序</strong>：若某條流真的需要全域嚴格有序且無法拆成 per-entity，設單 partition topic（犧牲該 topic 的水平吞吐）；這也是個訊號 — 若大量流都需要全序，遷 Kafka 的吞吐優勢用不上、該重新評估遷移。</li>
<li><strong>規劃 partition 數對齊並行度跟 hot key</strong>：partition 數決定 consumer 並行上限，同時注意熱門 key 造成的 hot partition，對位 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka topic 設計</a>的 key 策略段。</li>
</ol>
<h3 id="case-4redis-既有低延遲被-kafka-吞吐換掉延遲敏感路徑受傷">Case 4：Redis 既有低延遲被 Kafka 吞吐換掉，延遲敏感路徑受傷</h3>
<p><strong>徵兆</strong>：遷移後某些原本靠 Redis Streams 亞毫秒延遲的路徑（即時風控判斷、撮合前置）延遲跳到數十毫秒，下游 SLA 破線。</p>
<p><strong>根因</strong>：Redis Streams 的亞毫秒延遲來自記憶體操作 + 行程內 data structure；Kafka 為了長期保留跟高吞吐，訊息要落磁碟、過 replication、走網路到獨立 broker，單則訊息延遲落在 5-50ms 區間，這是它換吞吐跟持久性付出的代價。把延遲敏感路徑無差別搬上 Kafka，等於用一個為吞吐優化的系統去服務一個為延遲優化的需求。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>按延遲需求分流、不要全遷</strong>：把延遲敏感的即時路徑留在 Redis Streams（或 Redis 其他結構）、把需要長保留 / 高吞吐 / replay 的事件流遷到 Kafka，這正是 Bitso 在撮合場景堅持 Redis Streams 的理由。</li>
<li><strong>接受混合架構是常態</strong>：Redis Streams 跟 Kafka 共存、各自服務適配的 workload，不追求「全部統一到 Kafka」；對位 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS 的混合架構是 long-term default</a> 思路。</li>
<li><strong>若 Kafka 延遲必須壓低</strong>：調 producer <code>linger.ms=0</code> + <code>acks=1</code>、consumer <code>fetch.min.bytes=1</code> 換取較低延遲，但這會犧牲吞吐與部分可靠性、是 trade-off 不是免費午餐。</li>
</ol>
<h2 id="migration-結構漸進-cutover--長期混合">Migration 結構：漸進 cutover + 長期混合</h2>
<p>這趟遷移的結構是漸進拆分而非一次性切換：先按 workload 性質分流、再對需要遷的事件流做 dual-write 並行、逐流 cutover、最終留下 Redis Streams 跟 Kafka 共存的混合架構。一次性把所有 stream 搬上 Kafka 既無必要、也會把延遲敏感路徑拖下水。</p>
<ol>
<li><strong>Phase 0：scope 分流</strong> — 對每條 stream 跑前面三個訊號的判讀，分成「該遷 Kafka」（retention / replay / 規模超界）跟「留 Redis Streams」（延遲敏感 / 規模在範圍內）兩類。這一步直接決定後續工作量、也避免無差別遷移。</li>
<li><strong>Phase 1：Kafka 叢集與 topic 設計</strong> — 建 broker 叢集、依 Case 3 的 partition key 設計建 topic、依 Case 1 的業務需求設 retention，這時做的是基礎設施準備、還沒碰流量。</li>
<li><strong>Phase 2：dual-write 並行</strong> — producer 同時寫 Redis Streams 跟 Kafka、新 consumer 接 Kafka 驗證正確性、舊 consumer 持續吃 Redis Streams，這是可逆階段、出問題退回只讀 Redis 即可。</li>
<li><strong>Phase 3：逐流 cutover</strong> — 逐條 stream 把流量切到 Kafka、確認 consumer 進度（offset）跟 idempotency 都對、再停掉該 stream 的 Redis 端寫入；cutover 以 stream 為單位、不是整批。</li>
<li><strong>Phase 4：長期混合</strong> — 留在 Redis Streams 的延遲敏感流跟遷到 Kafka 的事件流共存、各自運維；需要時用 bridge（消費 Redis Streams 寫入 Kafka、或反向）同步必要資料。</li>
</ol>
<p>dual-write 階段的可逆性是這個結構的安全邊界：在 Phase 2 之前一切可退回純 Redis、Phase 3 逐流 cutover 把不可逆動作（停 Redis 寫入）切到最小粒度，單條 stream 出問題不影響其他流。</p>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis Streams（既有 Redis 內）</th>
          <th>Kafka（self-managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署增量</td>
          <td>近乎零（沿用 Redis 行程）</td>
          <td>3-5 broker + KRaft、獨立叢集</td>
      </tr>
      <tr>
          <td>儲存成本曲線</td>
          <td>RAM-bound（最貴的資源）</td>
          <td>磁碟為主（便宜 1-2 數量級）+ tiered to S3</td>
      </tr>
      <tr>
          <td>Retention 上限</td>
          <td>受 <code>maxmemory</code> 限制、實務數小時到數天</td>
          <td>數週到數月（磁碟）、數年（tiered storage）</td>
      </tr>
      <tr>
          <td>吞吐 / 單邏輯 stream</td>
          <td>受單 shard 封頂</td>
          <td>多 partition 水平擴展</td>
      </tr>
      <tr>
          <td>延遲</td>
          <td>亞毫秒</td>
          <td>5-50ms</td>
      </tr>
      <tr>
          <td>運維 FTE 增量</td>
          <td>近乎零</td>
          <td>0.5-2 FTE（含 schema / connect 生態）</td>
      </tr>
      <tr>
          <td>Replay 能力</td>
          <td>retention 內重讀（受 RAM 限制）</td>
          <td>任意 offset 重讀（受磁碟 retention 限制）</td>
      </tr>
      <tr>
          <td>生態</td>
          <td>Redis 工具鏈</td>
          <td>Schema Registry / Connect / Streams</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：成本的核心翻轉在「儲存成本曲線」這列。Redis Streams 把資料壓在最貴的 RAM、retention 越長越貴，所以實務上被迫短保留；Kafka 把資料攤到便宜的磁碟、再分層到 S3，讓長保留變得可負擔。但這個翻轉只在「retention 需求真的長」時成立 — 若 retention 只需數小時、資料量小，Redis Streams 沒有獨立叢集跟 0.5-2 FTE 的運維增量，總成本反而低，這正是 Arcjet 的處境。遷移划不划算取決於 retention 跟規模需求落在這條曲線的哪一段。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是常見終態">混合架構是常見終態</h3>
<p>多數從 Redis Streams 起步、因規模長出 Kafka 需求的系統，終態是兩者共存而非取代：</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">[延遲敏感即時路徑]                    [長保留 / replay / 高吞吐事件流]
</span></span><span class="line"><span class="ln">2</span><span class="cl">   Redis Streams                              Kafka
</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">        └──────────── Bridge（雙向同步）────────┘</span></span></code></pre></div><p>Redis Streams 服務亞毫秒延遲的即時路徑（風控、撮合前置）、Kafka 服務需要長保留與 replay 的事件流；需要打通時寫一段 bridge 同步必要 stream。這跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS 的混合架構是 long-term default</a> 是同一個 paradigm shift 結論的兩個實例。</p>
<h3 id="接上-kafka-生態">接上 Kafka 生態</h3>
<p>遷到 Kafka 後可解鎖 Redis Streams 沒有的生態能力：</p>
<ul>
<li>Schema 治理：用 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Schema Registry</a> 強制 producer / consumer 契約，補上 Redis Streams 缺的 schema enforcement（對位 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 自建抽象層</a>的紀律性責任）。</li>
<li>CDC pipeline：接 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Debezium</a> 把資料庫變更流進 Kafka topic，做事件溯源主軸。</li>
<li>長期 replay：tiered storage 把冷事件分層到 S3、支援數年 replay。</li>
</ul>
<h3 id="反向確認的-tripwire">反向確認的 tripwire</h3>
<p>遷移後若觀察到：延遲敏感路徑 SLA 破線、Kafka 叢集運維成本超出省下的 RAM 成本、實際 retention 需求遠短於規劃 — 這些是「該遷的訊號其實不成立」的回溯訊號，應重新評估該 stream 是否該退回 Redis Streams，對位 <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。">Arcjet</a> 的成本判讀。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>反向案例：<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。">Arcjet Redis Streams 取代 Kafka</a> / <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso Reliable Streams</a> / <a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 退場</a></li>
<li>平行 migration playbook（同 paradigm shift）：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>（Type E paradigm shift）</li>
</ul>
]]></content:encoded></item><item><title>Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a> overview 的 implementation-layer deep article。選型層（為何 fork、授權治理、何時選 Valkey）見 overview；本文只處理「決定用 Valkey 後，相容性怎麼驗、執行緒怎麼調」。命令實機驗證於 &lt;code>valkey/valkey:8&lt;/code> image（valkey_version 8.1.8）、最後檢查日 2026-06-16；效能數字以 &lt;a href="https://valkey.io/blog/">valkey.io 官方 benchmark&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="100-相容要能驗證才敢切">「100% 相容」要能驗證才敢切&lt;/h2>
&lt;p>Valkey 從 Redis 7.2.4 fork、宣稱 100% API 相容、drop-in 替換——這對選型是好消息，對上線前的工程師卻是一個需要證據的斷言。把 production 的 Redis 換成 Valkey，最怕的不是「大部分指令能跑」，而是某個邊角行為、某個 client library 的版本協商、某個 module 沒有對應 fork，在切換後才浮現。相容性不能靠信任，要靠驗證。&lt;/p>
&lt;p>驗證的起點是一個容易被忽略的細節：Valkey 的 &lt;code>INFO server&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">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version|server_name&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="c1"># redis_version:7.2.4 ← client library 以此協商相容行為&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># server_name:valkey&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自身的演進線&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個雙版本回報就是相容性的機制本身：client library 看到 &lt;code>redis_version:7.2.4&lt;/code>，就以 Redis 7.2.4 的協定與行為運作，完全不知道背後是 Valkey；&lt;code>valkey_version&lt;/code> 才是 Valkey 自己的版本，記錄它在 fork 之後加了什麼（例如 8.x 的多執行緒）。理解這條雙線——「對外裝成 Redis 7.2.4、對內持續演進」——是判斷相容性邊界的鑰匙。&lt;/p>
&lt;p>對大規模生產驗證，&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>是現成的證據：4700 萬月活、每次滑動讀多個 cache、sub-millisecond 延遲，跑在 Amazon ElastiCache for Valkey 上——這個規模的服務跑在 Valkey 上，本身就是相容性的背書。另一個訊號是 AWS 在 2024 把 ElastiCache 的 default engine 從 Redis 改成 Valkey（AWS 宣稱成本較 Redis OSS 低約 20%、以 &lt;a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價&lt;/a> 為準、最後檢查日 2026-06-16）。這些都是外部背書，但各服務有自己的 client library、module 與邊角用法，仍需自行驗證。&lt;/p>
&lt;h2 id="核心概念相容性的三層邊界">核心概念：相容性的三層邊界&lt;/h2>
&lt;p>「100% 相容」在不同層次有不同的精確度，驗證要分三層做。&lt;/p>
&lt;p>&lt;strong>協定與核心指令層：完全相容&lt;/strong>。string / hash / list / set / sorted set / stream / hyperloglog / geo 的所有指令、TTL / eviction / persistence / pub-sub / transaction、RESP 協定——這層是 fork 自 Redis 7.2.4 的部分，行為一致。所有標準 Redis client library 透過 &lt;code>redis_version&lt;/code> 協商，直接連、不改 code。&lt;/p>
&lt;p>&lt;strong>檔案格式層：相容&lt;/strong>。RDB 與 AOF 的檔案格式跟 Redis 7.2.4 一致，可以直接把 Redis 的資料目錄拷給 Valkey 載入——這是 drop-in 遷移的基礎，不需要 dump / reload。&lt;/p>
&lt;p>&lt;strong>生態與新功能層：要逐項確認&lt;/strong>。Redis 7.4+ 在 fork 之後新增的功能（Valkey 不一定跟進）、Redis Stack 的商業 module（RedisJSON / RedisSearch，Valkey 有自己的 valkey-search / valkey-bloom 但不是同一套）、偏 Redis Inc 的監控工具（RedisInsight 部分 vendor-specific 命令）——這層是相容性的真實風險所在，驗證要集中在這裡。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a> overview 的 implementation-layer deep article。選型層（為何 fork、授權治理、何時選 Valkey）見 overview；本文只處理「決定用 Valkey 後，相容性怎麼驗、執行緒怎麼調」。命令實機驗證於 <code>valkey/valkey:8</code> image（valkey_version 8.1.8）、最後檢查日 2026-06-16；效能數字以 <a href="https://valkey.io/blog/">valkey.io 官方 benchmark</a> 為準。</p></blockquote>
<h2 id="100-相容要能驗證才敢切">「100% 相容」要能驗證才敢切</h2>
<p>Valkey 從 Redis 7.2.4 fork、宣稱 100% API 相容、drop-in 替換——這對選型是好消息，對上線前的工程師卻是一個需要證據的斷言。把 production 的 Redis 換成 Valkey，最怕的不是「大部分指令能跑」，而是某個邊角行為、某個 client library 的版本協商、某個 module 沒有對應 fork，在切換後才浮現。相容性不能靠信任，要靠驗證。</p>
<p>驗證的起點是一個容易被忽略的細節：Valkey 的 <code>INFO server</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">docker <span class="nb">exec</span> valkey valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version|server_name&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 以此協商相容行為</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># server_name:valkey</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自身的演進線</span></span></span></code></pre></div><p>這個雙版本回報就是相容性的機制本身：client library 看到 <code>redis_version:7.2.4</code>，就以 Redis 7.2.4 的協定與行為運作，完全不知道背後是 Valkey；<code>valkey_version</code> 才是 Valkey 自己的版本，記錄它在 fork 之後加了什麼（例如 8.x 的多執行緒）。理解這條雙線——「對外裝成 Redis 7.2.4、對內持續演進」——是判斷相容性邊界的鑰匙。</p>
<p>對大規模生產驗證，<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>是現成的證據：4700 萬月活、每次滑動讀多個 cache、sub-millisecond 延遲，跑在 Amazon ElastiCache for Valkey 上——這個規模的服務跑在 Valkey 上，本身就是相容性的背書。另一個訊號是 AWS 在 2024 把 ElastiCache 的 default engine 從 Redis 改成 Valkey（AWS 宣稱成本較 Redis OSS 低約 20%、以 <a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價</a> 為準、最後檢查日 2026-06-16）。這些都是外部背書，但各服務有自己的 client library、module 與邊角用法，仍需自行驗證。</p>
<h2 id="核心概念相容性的三層邊界">核心概念：相容性的三層邊界</h2>
<p>「100% 相容」在不同層次有不同的精確度，驗證要分三層做。</p>
<p><strong>協定與核心指令層：完全相容</strong>。string / hash / list / set / sorted set / stream / hyperloglog / geo 的所有指令、TTL / eviction / persistence / pub-sub / transaction、RESP 協定——這層是 fork 自 Redis 7.2.4 的部分，行為一致。所有標準 Redis client library 透過 <code>redis_version</code> 協商，直接連、不改 code。</p>
<p><strong>檔案格式層：相容</strong>。RDB 與 AOF 的檔案格式跟 Redis 7.2.4 一致，可以直接把 Redis 的資料目錄拷給 Valkey 載入——這是 drop-in 遷移的基礎，不需要 dump / reload。</p>
<p><strong>生態與新功能層：要逐項確認</strong>。Redis 7.4+ 在 fork 之後新增的功能（Valkey 不一定跟進）、Redis Stack 的商業 module（RedisJSON / RedisSearch，Valkey 有自己的 valkey-search / valkey-bloom 但不是同一套）、偏 Redis Inc 的監控工具（RedisInsight 部分 vendor-specific 命令）——這層是相容性的真實風險所在，驗證要集中在這裡。</p>
<p>驗證的操作順序：先確認 client library 連得上且核心指令正常（第一層），再確認資料能載入（第二層），最後盤點你實際用到的 module 與 7.4+ 功能（第三層）。前兩層幾乎必過，工夫花在第三層。</p>
<h2 id="配置io-threads-多執行緒調校">配置：io-threads 多執行緒調校</h2>
<p>Valkey 跟 Redis 7.2.4 拉開的第一個實質技術差異是執行緒模型。Redis 的命令處理是單執行緒（I/O threads 只分擔 socket 讀寫，命令仍在主執行緒），Valkey 8.x 把更多 I/O 路徑非同步化，在多核機器上能讓單實例吞吐明顯高於 Redis——具體倍數依 workload 與核數而定，以 <a href="https://valkey.io/blog/">valkey.io 官方 benchmark</a> 為準，這裡不複述未經自己壓測的數字。</p>
<p>執行緒由 <code>io-threads</code> 控制，預設 1（單執行緒，跟 Redis 行為一致）：</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）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">valkey-cli CONFIG GET io-threads
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 1) &#34;io-threads&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2) &#34;1&#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 class="c1"># 調高 I/O 執行緒數（建議不超過機器實體核數、留核給其他進程）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># redis.conf / valkey.conf:</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">#   io-threads 4</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>io-threads</code> 是啟動參數，多數版本需要重啟生效（不是所有 CONFIG SET 都能熱套），改 conf 後 rolling restart</li>
<li>設定值對齊機器核數但留 headroom，例如 8 核機器設 4-6，不要設滿</li>
<li>單核或低核機器設 1（預設）即可，多執行緒在核數不足時沒有收益反而增加切換開銷</li>
<li>I/O 密集（大量小命令、高連線數）的 workload 收益最明顯；CPU 密集的重命令（大 Lua、大 collection 操作）收益有限</li>
</ul>
<p>調完用實際 workload 壓測驗證，不要假設「開了就快」——執行緒配置的收益高度依賴 workload 形狀。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1切換後-module-指令報-unknown-command">Case 1：切換後 module 指令報 unknown command</h3>
<p><strong>徵兆</strong>：drop-in 換成 Valkey 後核心功能正常，但某些路徑報 <code>ERR unknown command 'JSON.SET'</code> 或 <code>FT.SEARCH</code>，application 部分功能失效。</p>
<p><strong>根因</strong>：用到了 Redis Stack 的商業 module（RedisJSON / RedisSearch）。這些 module 不在 fork 範圍內，Valkey 有自己的 valkey-search / valkey-bloom，但不是同一套指令、需要另外安裝。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>切換前用 <code>MODULE LIST</code> 在原 Redis 上盤點所有載入的 module</li>
<li>逐個確認 Valkey 是否有對應替代（valkey-search 對 RedisSearch 等），確認指令相容度</li>
<li>沒有對應的 module，評估改用 module-free 設計（例如把 JSON 操作拉回 application 層）</li>
<li>重度依賴 Redis Stack 商業 module 的場景，相容性邊界在這裡，可能該留在 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> Inc 商業版</li>
</ol>
<h3 id="case-2client-library-太舊協商失敗">Case 2：client library 太舊、協商失敗</h3>
<p><strong>徵兆</strong>：絕大多數 client 正常，但某個老服務的 client library 連 Valkey 報協定錯誤或行為異常。</p>
<p><strong>根因</strong>：Valkey 回報 <code>redis_version:7.2.4</code>，client library 若太舊（不支援 Redis 7.2 對應的協定特性，例如 RESP3）會協商失敗。這不是 Valkey 的問題，是 client 本來就跟不上 Redis 7.2。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>valkey-cli INFO server</code> 確認回報的 <code>redis_version</code>，對照 client library 支援到哪個 Redis 版本</li>
<li>升級過舊的 client library 到支援 Redis 7.2 的版本</li>
<li>必要時 client 端強制用 RESP2（多數 library 可配置），避開 RESP3 協商</li>
<li>這類問題在升級 Redis 7.2 時也會遇到，不是 Valkey 特有</li>
</ol>
<h3 id="case-3監控工具部分指標消失">Case 3：監控工具部分指標消失</h3>
<p><strong>徵兆</strong>：切換後 RedisInsight 或某監控 dashboard 部分面板空白、某些 vendor-specific 命令回錯。</p>
<p><strong>根因</strong>：RedisInsight 等 Redis Inc 工具有部分偏 Redis 商業版的命令，Valkey 不一定實作。核心指標（memory / hit rate / connections）通用，但 vendor-specific 的進階面板可能缺。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>監控改用通用工具：<code>valkey-cli INFO</code>、Prometheus + redis_exporter（相容 Valkey）、Grafana</li>
<li>核心指標（<code>used_memory</code> / <code>keyspace_hits</code> / <code>connected_clients</code>）在 Valkey 完全相容，監控覆蓋不受影響</li>
<li>把監控的相容性納入切換前驗證清單，不要切換後才發現面板空白</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線</a> 調校用到的 INFO 指標，這些在 Valkey 都通用</li>
</ol>
<h3 id="case-4io-threads-開太多效能反而下降">Case 4：io-threads 開太多、效能反而下降</h3>
<p><strong>徵兆</strong>：把 <code>io-threads</code> 從 1 調到 16 想榨效能，結果延遲不降反升、CPU 使用率異常。</p>
<p><strong>根因</strong>：<code>io-threads</code> 設成超過機器實體核數，執行緒互搶 CPU、context switch 開銷超過平行收益。或 workload 是 CPU 密集（重命令），I/O 多執行緒對它沒幫助。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>io-threads</code> 不超過實體核數，留 headroom 給 OS 與其他進程（8 核設 4-6）</li>
<li>用實際 workload 壓測對比不同 io-threads 值的延遲與吞吐，不要憑感覺調滿</li>
<li>CPU 密集 workload 收益有限，問題可能在命令本身太重（大 collection / 大 Lua），先優化命令</li>
<li>多執行緒解的是 I/O 平行度，不是單命令執行速度，分清楚瓶頸在哪</li>
</ol>
<h3 id="case-5以為換-valkey-就解決了-redis-的記憶體--fork-問題">Case 5：以為換 Valkey 就解決了 Redis 的記憶體 / fork 問題</h3>
<p><strong>徵兆</strong>：因為 Redis 的 fork 延遲尖峰或記憶體 OOM 而切到 Valkey，切完發現同樣的尖峰與 OOM 還在。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，繼承了 Redis 的記憶體模型、eviction 演算法、AOF/RDB fork 機制。這些行為在 Valkey 上完全一致——Valkey 的差異在執行緒與授權，不在記憶體與持久化架構。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / 淘汰 / fork 的調校在 Valkey 上跟 Redis 完全一樣，直接套用 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體調校</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></li>
<li>fork 尖峰是 Redis 系列的共同架構限制，要根治走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 fork-less 機制，不是換 Valkey</li>
<li>切換 Valkey 的理由應該是授權合規、多執行緒吞吐或 managed 成本，不是記憶體問題</li>
<li>切換前釐清痛點：是授權 / 成本（Valkey 解）還是記憶體 / fork 架構（Valkey 不解）</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Valkey 的容量判讀，多數沿用 Redis（同源），差異集中在執行緒與授權成本：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Valkey 的情況</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心指標（記憶體 / hit rate）</td>
          <td>跟 Redis 完全一致</td>
          <td>直接套用 Redis 的容量判讀</td>
      </tr>
      <tr>
          <td><code>io-threads</code></td>
          <td>預設 1、可調至接近核數</td>
          <td>多核 + I/O 密集才有收益、需壓測驗證</td>
      </tr>
      <tr>
          <td>單實例吞吐</td>
          <td>多執行緒下高於 Redis（依 workload）</td>
          <td>以 valkey.io benchmark 為準、自己壓測</td>
      </tr>
      <tr>
          <td>授權成本</td>
          <td>BSD 3-clause、商業使用無限制</td>
          <td>合規敏感場景的決定性優勢</td>
      </tr>
      <tr>
          <td>managed 成本</td>
          <td>ElastiCache for Valkey 約低 Redis 20%</td>
          <td>AWS 生態的成本優化路徑</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>記憶體 / fork 是瓶頸</strong>：Valkey 同源、不解這層，走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（fork-less + 更省記憶體）或 Redis 系列的 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>。</li>
<li><strong>需要 Redis Stack 商業 module</strong>：Valkey 的 valkey-search / valkey-bloom 覆蓋不到全部，重度依賴走 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> Inc 商業版。</li>
<li><strong>不想自管</strong>：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a> 是 AWS 的 default engine，managed failover / snapshot / patching 全託管，成本比 ElastiCache for Redis 低約 20%。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Valkey 的 deep article 大量複用 Redis 的調校知識（同源），它自己的獨特性在相容性驗證、執行緒與授權：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis 全系列 deep article</a></strong>：記憶體、持久化、Sentinel、連線的調校在 Valkey 上完全一致，Valkey 不重寫這些，直接套用。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a></strong>：managed Valkey 把執行緒與 failover 託管，省掉自管的調校與演練。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的 ElastiCache for Valkey 案例</a></strong>：4700 萬月活的 sub-millisecond 配對引擎是相容性與規模化的生產證據，但 module / client 的相容性仍需逐案驗證。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></strong>：兩者都打「Redis 相容 + 更好的執行緒」，但 Valkey 是 fork（同源、最高相容），DragonflyDB 是 C++ 重寫（相容核心但架構不同），選型差異在相容度 vs 架構激進度。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>同源 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（default engine 即 Valkey）、<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/migrate-to-relational/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/migrate-to-relational/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore&lt;/a> overview 的 migration playbook。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。BaaS 託管平台整場遷出的資產線盤點與並行期總覽見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出&lt;/a>；本文聚焦資料層的跨 paradigm 重建模。&lt;/p>&lt;/blockquote>
&lt;p>「我們把 Firestore 整包匯出，匯進 PostgreSQL 就好。」這句話低估了遷移的真正內容 — Firestore 遷往自建 relational 的難點是&lt;strong>反轉整個存取模型&lt;/strong>，搬資料只是其中最容易的一條線。Firestore 是 client 用 SDK 直連資料庫、授權寫在 Security Rules；自建 relational 是 client 打自己的後端 API、授權在後端中介層。資料可以匯出，但反正規化的 document 形狀、沿查詢限制長出來的資料模型、realtime listener 與 offline 同步能力，都沒有 1:1 的對應物。字面意義的「匯出再匯入」只搬走了最容易的那部分。本文走 paradigm shift 結構：先講為何字面遷移不成立、再講哪些該遷哪些先留、最後才是階段化執行。&lt;/p>
&lt;h2 id="遷移的-driver三面牆不是relational-比較好">遷移的 driver：三面牆，不是「relational 比較好」&lt;/h2>
&lt;p>Firestore 遷往自建很少因為「relational 比較好」這種空泛動機，而是撞到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21&lt;/a> BaaS 段描述的三面具體的牆。先確認 driver 真的成立、再啟動遷移：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>撞牆訊號&lt;/th>
 &lt;th>遷移要解的問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>報表 / 分析查詢&lt;/td>
 &lt;td>跨 collection 報表查不出來、已經在維護資料複製管線&lt;/td>
 &lt;td>把資料放回支援 JOIN / aggregation 的 relational&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本曲線轉折&lt;/td>
 &lt;td>read / write 計費隨流量線性成長、超過自建 + cache 的成本&lt;/td>
 &lt;td>用自管資料庫 + 應用層快取壓低單位成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>授權控制面失控&lt;/td>
 &lt;td>Security Rules 長到難以測試 / review、授權邏輯沒有版本治理&lt;/td>
 &lt;td>把授權拉回後端 API 中介層、可測試可審查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>No-go condition&lt;/strong>：產品仍以多裝置 realtime 同步與 offline-first 為核心賣點、且查詢需求簡單、成本仍在舒適區 → 先不要遷。這些正是 Firestore 的主場，硬遷會把 realtime / offline 這層平台白送的能力變成自己要重建的工程。遷移前先問「撞的是哪面牆」，三面牆都沒撞到就是 &lt;a href="https://tarrragon.github.io/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&lt;/a> 講的偽自建。&lt;/p>&lt;/blockquote>
&lt;p>逐能力遷出是常態而非整包搬離：&lt;a href="https://tarrragon.github.io/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 的「成長期 SaaS」例子&lt;/a> 就是只把撞牆的資料層搬到自管 PostgreSQL、認證留在原平台。本文預設的也是這種逐能力遷出 — 遷的是資料層，不一定連認證、儲存一起搬。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore</a> overview 的 migration playbook。寫作參照 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。BaaS 託管平台整場遷出的資產線盤點與並行期總覽見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>；本文聚焦資料層的跨 paradigm 重建模。</p></blockquote>
<p>「我們把 Firestore 整包匯出，匯進 PostgreSQL 就好。」這句話低估了遷移的真正內容 — Firestore 遷往自建 relational 的難點是<strong>反轉整個存取模型</strong>，搬資料只是其中最容易的一條線。Firestore 是 client 用 SDK 直連資料庫、授權寫在 Security Rules；自建 relational 是 client 打自己的後端 API、授權在後端中介層。資料可以匯出，但反正規化的 document 形狀、沿查詢限制長出來的資料模型、realtime listener 與 offline 同步能力，都沒有 1:1 的對應物。字面意義的「匯出再匯入」只搬走了最容易的那部分。本文走 paradigm shift 結構：先講為何字面遷移不成立、再講哪些該遷哪些先留、最後才是階段化執行。</p>
<h2 id="遷移的-driver三面牆不是relational-比較好">遷移的 driver：三面牆，不是「relational 比較好」</h2>
<p>Firestore 遷往自建很少因為「relational 比較好」這種空泛動機，而是撞到 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a> BaaS 段描述的三面具體的牆。先確認 driver 真的成立、再啟動遷移：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>撞牆訊號</th>
          <th>遷移要解的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>報表 / 分析查詢</td>
          <td>跨 collection 報表查不出來、已經在維護資料複製管線</td>
          <td>把資料放回支援 JOIN / aggregation 的 relational</td>
      </tr>
      <tr>
          <td>成本曲線轉折</td>
          <td>read / write 計費隨流量線性成長、超過自建 + cache 的成本</td>
          <td>用自管資料庫 + 應用層快取壓低單位成本</td>
      </tr>
      <tr>
          <td>授權控制面失控</td>
          <td>Security Rules 長到難以測試 / review、授權邏輯沒有版本治理</td>
          <td>把授權拉回後端 API 中介層、可測試可審查</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>No-go condition</strong>：產品仍以多裝置 realtime 同步與 offline-first 為核心賣點、且查詢需求簡單、成本仍在舒適區 → 先不要遷。這些正是 Firestore 的主場，硬遷會把 realtime / offline 這層平台白送的能力變成自己要重建的工程。遷移前先問「撞的是哪面牆」，三面牆都沒撞到就是 <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</a> 講的偽自建。</p></blockquote>
<p>逐能力遷出是常態而非整包搬離：<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 的「成長期 SaaS」例子</a> 就是只把撞牆的資料層搬到自管 PostgreSQL、認證留在原平台。本文預設的也是這種逐能力遷出 — 遷的是資料層，不一定連認證、儲存一起搬。</p>
<h2 id="6-維-diff-audit主導維度是-paradigm--application-change">6 維 diff audit：主導維度是 paradigm + application change</h2>
<p>遷移前先盤點 source 跟 target 的差異落在哪幾維、決定 playbook 結構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Firestore → 自建 relational</th>
          <th>程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>document / collection → 正規 table、SDK query → 後端 API + SQL</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>serverless 全託管 → 自管 / managed 資料庫、自己擔 backup / failover</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>client 直連 + 規則授權 → API 中介 + 後端授權</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Components 數量</td>
          <td>單一平台 → 新增一層自建後端服務 + 資料庫</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>前端拔 SDK 改打 API、realtime / offline 要重建</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>平台複製 → 自己設計 replica / 多 region / DR</td>
          <td>Medium</td>
      </tr>
  </tbody>
</table>
<p>主導維度是 <strong>paradigm 與 application change</strong>：六維裡五維落在 High。這定義了結構 — <strong>Type E paradigm shift</strong>（排除 schema 翻譯 Type A 和 drop-in Type B）：存取模型反轉、部分能力重建、可能長期混合（資料層自建、認證仍留平台）。</p>
<h2 id="為什麼字面遷移不成立存取模型反轉">為什麼字面遷移不成立：存取模型反轉</h2>
<p>Firestore 的存取模型是 <em>前端即客戶端、資料庫直接面向公網、授權在規則層</em>；自建 relational 是 <em>前端打後端、後端面向資料庫、授權在服務層</em>。這個反轉是遷移的核心難點，不在資料搬運。</p>
<p><strong>反正規化 document → 正規 schema</strong>：</p>
<ul>
<li>Firestore 為了繞開查詢限制，常把關聯資料冗餘寫進同一 document（一份資料複製多處）</li>
<li>遷往 relational 要把冗餘拆回正規化 table、重建外鍵關係，這是逆向工程：要先讀懂當初為什麼這樣存</li>
<li>反過來說，有些 document 的巢狀結構在 relational 用 JSONB 保留更省事（見 <a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">PostgreSQL jsonb</a>）— 不是所有 document 都要拆成 table</li>
</ul>
<p><strong>Security Rules 授權 → 後端授權</strong>：</p>
<ul>
<li>Firestore 的授權邏輯散在 Security Rules DSL 裡，遷移要把每一條規則翻譯成後端 API 的權限檢查</li>
<li>這層翻譯是安全敏感的：漏一條規則等於開一個越權查詢的洞，對應 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a></li>
</ul>
<p><strong>SDK 直連 → API 中介</strong>：</p>
<ul>
<li>前端原本用 Firestore SDK 直接讀寫，遷移後要拔掉 SDK、改打自建 API</li>
<li>這是 application 層的大改，不是資料庫換連線字串</li>
</ul>
<p><strong>realtime listener / offline persistence → 自己重建</strong>：</p>
<ul>
<li>snapshot listener 的即時推送、offline 讀寫快取，是平台白送的能力</li>
<li>自建要用 WebSocket / SSE 重建即時層（見 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a> 與 presence 設計）、用前端本地儲存重建 offline — 這是遷移最容易被漏估的工作量</li>
</ul>
<p>所以遷移的第一步不是匯資料，是<strong>盤點 application 對 Firestore 的所有依賴面</strong>：查詢路徑、授權規則、realtime 訂閱、offline 行為。這份清單決定哪些能直接遷、哪些要重建、哪些先留在平台。</p>
<h2 id="哪些該遷哪些先留逐能力混合">哪些該遷、哪些先留（逐能力混合）</h2>
<p>Type E 的本質是不收斂 — 不必把所有 Firebase 能力一次搬完。判讀標準：</p>
<table>
  <thead>
      <tr>
          <th>Workload / 能力特徵</th>
          <th>去向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要報表 / JOIN / aggregation 的資料</td>
          <td>遷自建 relational</td>
      </tr>
      <tr>
          <td>讀取量大、成本敏感、access pattern 穩定的資料</td>
          <td>遷自建 + <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">應用層快取</a></td>
      </tr>
      <tr>
          <td>仍以 realtime 同步為核心、查詢簡單的資料</td>
          <td>先留 Firestore / 或最後再遷</td>
      </tr>
      <tr>
          <td>認證（Firebase Auth）</td>
          <td>可留平台、逐能力決定（見 0.22）</td>
      </tr>
      <tr>
          <td>檔案儲存（Firebase Storage）</td>
          <td>可留平台、與資料層解耦後再評估</td>
      </tr>
  </tbody>
</table>
<p><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 的成長期 SaaS</a> 是這個判讀的 case anchor：撞牆的是資料層的 query 複雜度與成本，遷的就是資料層，認證留在原地。混合不是過渡失敗，是逐能力選型的穩態。</p>
<h2 id="phase-plan存取模型反轉的階段化">Phase plan：存取模型反轉的階段化</h2>
<p>paradigm shift 的階段化把不可逆動作放到最後、每階段有獨立驗證門檻：</p>
<h4 id="phase-1依賴面盤點">Phase 1：依賴面盤點</h4>
<p>列出 application 對 Firestore 的所有讀寫路徑、Security Rules 授權條件、realtime 訂閱點、offline 行為。標每項的頻率、安全敏感度、是否可重建。這份清單不完整不進下一階段。</p>
<h4 id="phase-2relational-重建模">Phase 2：relational 重建模</h4>
<p>把反正規化 document 設計回正規 schema、決定哪些巢狀結構用 JSONB 保留。同步設計後端 API 的端點與授權檢查、把 Security Rules 逐條翻譯成服務層權限。對應 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a> 與 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a>。</p>
<h4 id="phase-3自建後端--dual-write">Phase 3：自建後端 + dual-write</h4>
<p>立起自建後端 API 與資料庫，前端關鍵寫入路徑同時寫 Firestore 與新後端。Firestore 仍是 source of truth、新庫累積資料。dual-write 要處理一邊失敗的補償（對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a>）。</p>
<h4 id="phase-4backfill-歷史資料">Phase 4：backfill 歷史資料</h4>
<p>把 Firestore 既有 document 按新 schema 轉換寫入新庫。backfill 與 dual-write 並行時要處理覆蓋順序，backfill 不能蓋掉 dual-write 的新值。轉換過程記 checksum / row count 對照。</p>
<h4 id="phase-5shadow-read-驗證">Phase 5：shadow read 驗證</h4>
<p>讀路徑同時打 Firestore 與新後端、比對結果、記錄差異但仍以 Firestore 回應用戶。差異率降到可接受才進 cutover。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a> 的 evidence 方法。</p>
<h4 id="phase-6漸進-cutover--重建即時層">Phase 6：漸進 cutover + 重建即時層</h4>
<p>前端逐步把讀寫從 Firestore SDK 切到自建 API（按比例 / 按功能模組），保留切回能力。若產品需要 realtime，這階段要把 snapshot listener 換成自建即時層（WebSocket / SSE）並驗證延遲與斷線重連。cutover 完成後資料層的 source of truth 轉到自建；未遷的能力（認證、儲存）仍在平台 — 混合架構成立。</p>
<h2 id="evidence每階段的前進依據">Evidence：每階段的前進依據</h2>
<p>每個階段用資料證明可前進、不靠感覺：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dual-write</td>
          <td>雙寫成功率、寫入失敗補償紀錄、兩邊 document / row 數差異</td>
      </tr>
      <tr>
          <td>backfill</td>
          <td>已轉換比例、轉換錯誤數、checksum 對照、反正規化還原正確性抽查</td>
      </tr>
      <tr>
          <td>shadow read</td>
          <td>新舊結果差異率、差異分類（建模差異 vs 真錯誤）、授權翻譯漏洞掃描</td>
      </tr>
      <tr>
          <td>cutover</td>
          <td>切流比例、新 API latency p99、error rate、realtime 推送延遲、rollback 是否觸發</td>
      </tr>
  </tbody>
</table>
<p>這些 evidence 對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>（Source / Time range / Query link / Owner / Data quality）與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。授權翻譯這項要特別當成 gate 條件 — 它是安全邊界、不只是功能正確性。</p>
<h2 id="cutover-與-rollback-決策">Cutover 與 rollback 決策</h2>
<p>資料庫切流失敗代價高、加上這裡牽涉授權正確性，決策權責要寫清楚：</p>
<ul>
<li><strong>cutover window</strong>：選低流量時段、明確切流比例階梯（如 1% → 10% → 50% → 100%），按功能模組切比按全站切安全</li>
<li><strong>rollback condition</strong>：新 API error rate / latency 超閾值、shadow read 差異率異常、或發現授權翻譯漏洞 → 切回 Firestore</li>
<li><strong>decision owner</strong>：誰有權喊停、依據什麼 evidence、記錄在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a></li>
<li><strong>realtime 連續性</strong>：若即時層同步切換，要驗證切換期間訂閱不中斷、或明確告知短暫降級</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、<a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<h2 id="cleanup-與長期混合">Cleanup 與長期混合</h2>
<p>Type E 的 cleanup 通常不是「關掉整個 Firebase」— 多數情況認證、儲存仍留平台：</p>
<ul>
<li>已遷資料路徑的 Firestore collection、Security Rules、dual-write code path 退役</li>
<li>shadow read 比對 code 移除</li>
<li>前端殘留的 Firestore SDK 依賴清掉（資料層已不走它）</li>
<li>但 Firebase Auth / Storage 若仍在用，保留；明確標示哪條資料路徑的 source of truth 是自建庫、哪條仍在平台</li>
<li>Firestore 的資料匯出備份保留到確認新庫穩定，對應 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3</a> 的並行期退役判準</li>
</ul>
<p>混合架構不是遷移失敗、是逐能力選型的穩態 — 撞牆的資料層自建、沒撞牆的認證 / 儲存留在平台。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1只匯資料漏了存取模型反轉">Case 1：只匯資料、漏了存取模型反轉</h4>
<p>把 Firestore 匯出匯進 PostgreSQL 就以為遷完、忘了前端還在打 SDK、授權還在 Security Rules。修法：依賴面盤點是 Phase 1、資料搬運只是其中一條線，存取模型反轉才是主體。</p>
<h4 id="case-2security-rules-翻譯漏洞">Case 2：Security Rules 翻譯漏洞</h4>
<p>把規則翻成後端授權時漏一條、開了越權查詢的洞、上線後資料外洩。修法：授權翻譯要逐條對照 + 紅隊驗證（<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5</a>）、當成 cutover gate 條件、不是功能 bug。</p>
<h4 id="case-3反正規化還原錯誤">Case 3：反正規化還原錯誤</h4>
<p>document 的冗餘副本拆回 table 時還原錯關係、新庫資料關聯接錯。修法：Phase 2 先讀懂當初為何反正規化、backfill 後抽查還原正確性、shadow read 比對抓出建模差異。</p>
<h4 id="case-4低估-realtime--offline-重建工作量">Case 4：低估 realtime / offline 重建工作量</h4>
<p>以為遷資料庫就好、上線才發現 snapshot listener 與 offline 同步整層要自己重建、進度爆炸。修法：依賴面盤點就把 realtime 訂閱點與 offline 行為標出來、列入工作量、必要時這層最後遷或先保留。</p>
<h4 id="case-5dual-write-一邊失敗沒補償">Case 5：dual-write 一邊失敗沒補償</h4>
<p>dual-write 時新庫寫成功 Firestore 失敗（或反之）、兩邊分歧、cutover 後資料不完整。修法：dual-write 要有失敗補償（記錄、重試、標記人工對帳），對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a>。</p>
<p><strong>Anti-recommendation</strong>：產品仍重度依賴 realtime / offline、或團隊還沒有自建後端與資料庫的營運能力（backup、failover、授權設計）→ 先不要遷。可先把一塊撞牆最明顯、realtime 需求最低的資料（例如報表來源資料）試點、累積自建營運經驗再擴大。</p>
<h2 id="容量與成本crossover-判讀">容量與成本：crossover 判讀</h2>
<p>遷移的成本判讀關鍵是 <em>遷移後的總帳</em>、不是只看 Firestore 帳單：</p>
<ul>
<li><strong>遷移當下</strong>：高 read 流量下，自管資料庫 + 應用層快取的單位成本常低於 Firestore 的 per-read 計費</li>
<li><strong>但要加回自建的隱性成本</strong>：後端服務的開發與維運、資料庫的 backup / failover / 擴容、realtime 層的重建與維護、團隊人力</li>
<li><strong>判讀分層</strong>：撞到成本牆且已有後端團隊 → 自建總帳通常划算；仍是小團隊、realtime 是核心、流量不大 → Firestore 的「平台白送能力」可能仍比自建總帳便宜</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：crossover 隨流量形狀、region pricing、團隊成本結構變動、無通用閾值。遷移省下的 Firestore 帳單要扣掉自建後端 + 資料庫 + 即時層的維運成本後再比，不是直接拿兩邊資料庫帳單對照。</p></blockquote>
<p>接回 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="跟其他遷移路徑的關係">跟其他遷移路徑的關係</h3>
<ul>
<li><strong>保留 document model</strong>：若只是要逃離 Firestore 的查詢限制、但 document 形狀仍適合，遷 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a> 比遷 relational 的 paradigm 跨度小、不必反正規化還原</li>
<li><strong>整包託管遷出</strong>：若連認證、儲存一起搬離 Firebase，整場資產線盤點與並行期走 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>、本文是其中資料層那一條</li>
<li><strong>反向視角</strong>：哪些資料當初就不該進 Firestore（報表來源、強一致交易），見 <a href="/blog/backend/01-database/vendors/firestore/#%e4%b8%8d%e9%81%a9%e7%94%a8%e5%a0%b4%e6%99%af" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview 的不適用場景</a></li>
</ul>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a> — 服務定位與查詢邊界</li>
<li><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> — 通用 dual-write / shadow read / cutover 框架</li>
<li><a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a> — Security Rules 授權翻譯的安全驗證</li>
<li><a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a> — dual-write 失敗補償與資料對帳</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/migrate-rds-mongodb-to-dynamodb/" data-link-title="從 RDS / MongoDB 遷移到 DynamoDB：access-pattern-first 重建模、混合架構與 cost crossover" data-link-desc="RDS / MongoDB → DynamoDB 不是搬 schema 而是換 paradigm；本文走 Type E paradigm shift 結構，展開為何字面遷移不成立、access pattern 重建模、哪些 workload 該遷哪些該留的混合架構、dual-write &#43; shadow read 階段化，以及 Zomato cost crossover 的長期成本判讀">從 RDS / MongoDB 遷往 DynamoDB</a> — 同為 Type E paradigm shift 的對照（方向相反：遷入 NoSQL vs 遷出 BaaS）</li>
<li><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> / <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> — 遷移 driver 的選型層背景</li>
</ul>
]]></content:encoded></item><item><title>Docker Swarm → Kubernetes：5 個 Swarm production cluster 撞牆數據</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/migrate-from-docker-swarm/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/migrate-from-docker-swarm/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link Docker Swarm 跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（Swarm 簡單 container orchestration → K8s declarative resource model）→ Type E paradigm shift&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="5-個-swarm-production-cluster-撞牆數據">5 個 Swarm production cluster 撞牆數據&lt;/h2>
&lt;p>從 2020-2024 觀察 5 個中型 organization 的 Swarm production cluster lifecycle、典型撞牆點：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Cluster&lt;/th>
 &lt;th>規模 (peak)&lt;/th>
 &lt;th>撞牆點&lt;/th>
 &lt;th>觸發遷移時間&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A (SaaS startup)&lt;/td>
 &lt;td>80 service / 12 node&lt;/td>
 &lt;td>service discovery latency 升、無 sidecar mesh&lt;/td>
 &lt;td>2022&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B (E-commerce)&lt;/td>
 &lt;td>150 service / 25 node&lt;/td>
 &lt;td>rolling update + canary 邏輯自寫複雜&lt;/td>
 &lt;td>2023&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C (Fintech)&lt;/td>
 &lt;td>60 service / 15 node&lt;/td>
 &lt;td>secret rotation + RBAC 自管、合規難&lt;/td>
 &lt;td>2023&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D (Media)&lt;/td>
 &lt;td>200 service / 40 node&lt;/td>
 &lt;td>autoscaling 自寫、預測流量失敗&lt;/td>
 &lt;td>2024&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E (Logistics)&lt;/td>
 &lt;td>100 service / 20 node&lt;/td>
 &lt;td>multi-region 不支援&lt;/td>
 &lt;td>2024&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>5 個共同 pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Swarm 簡單但 ceiling 100-200 service / 20-40 node&lt;/strong>&lt;/li>
&lt;li>&lt;strong>跨 service 治理（mesh / RBAC / secret / autoscale）需要 &lt;em>外掛&lt;/em> 工具、複雜度反超 K8s&lt;/strong>&lt;/li>
&lt;li>&lt;strong>無 multi-region native&lt;/strong>、災備受限&lt;/li>
&lt;li>&lt;strong>生態縮、社群活躍度低、新 feature 緩&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>撞牆點不是「Swarm 跑不動」、是「Swarm 不會幫你解 &lt;em>跨 service 治理&lt;/em> 問題、要自寫」。Kubernetes 不是 simpler、是 &lt;em>把治理問題納入框架&lt;/em>。&lt;/p>
&lt;h2 id="為什麼遷ceiling--ecosystem--multi-region-三條-driver">為什麼遷：ceiling / ecosystem / multi-region 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ceiling&lt;/td>
 &lt;td>Swarm 跑 100-200 service 後 service discovery latency / scheduling 跟不上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ecosystem&lt;/td>
 &lt;td>K8s ecosystem (Helm / Operator / mesh / GitOps) 成熟、Swarm 對等工具缺&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-region&lt;/td>
 &lt;td>Swarm 不支援、K8s 多 cluster federation 成熟&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（K8s → Swarm）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link Docker Swarm 跟 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（Swarm 簡單 container orchestration → K8s declarative resource model）→ Type E paradigm shift</em>。</p></blockquote>
<h2 id="5-個-swarm-production-cluster-撞牆數據">5 個 Swarm production cluster 撞牆數據</h2>
<p>從 2020-2024 觀察 5 個中型 organization 的 Swarm production cluster lifecycle、典型撞牆點：</p>
<table>
  <thead>
      <tr>
          <th>Cluster</th>
          <th>規模 (peak)</th>
          <th>撞牆點</th>
          <th>觸發遷移時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A (SaaS startup)</td>
          <td>80 service / 12 node</td>
          <td>service discovery latency 升、無 sidecar mesh</td>
          <td>2022</td>
      </tr>
      <tr>
          <td>B (E-commerce)</td>
          <td>150 service / 25 node</td>
          <td>rolling update + canary 邏輯自寫複雜</td>
          <td>2023</td>
      </tr>
      <tr>
          <td>C (Fintech)</td>
          <td>60 service / 15 node</td>
          <td>secret rotation + RBAC 自管、合規難</td>
          <td>2023</td>
      </tr>
      <tr>
          <td>D (Media)</td>
          <td>200 service / 40 node</td>
          <td>autoscaling 自寫、預測流量失敗</td>
          <td>2024</td>
      </tr>
      <tr>
          <td>E (Logistics)</td>
          <td>100 service / 20 node</td>
          <td>multi-region 不支援</td>
          <td>2024</td>
      </tr>
  </tbody>
</table>
<p>5 個共同 pattern：</p>
<ul>
<li><strong>Swarm 簡單但 ceiling 100-200 service / 20-40 node</strong></li>
<li><strong>跨 service 治理（mesh / RBAC / secret / autoscale）需要 <em>外掛</em> 工具、複雜度反超 K8s</strong></li>
<li><strong>無 multi-region native</strong>、災備受限</li>
<li><strong>生態縮、社群活躍度低、新 feature 緩</strong></li>
</ul>
<p>撞牆點不是「Swarm 跑不動」、是「Swarm 不會幫你解 <em>跨 service 治理</em> 問題、要自寫」。Kubernetes 不是 simpler、是 <em>把治理問題納入框架</em>。</p>
<h2 id="為什麼遷ceiling--ecosystem--multi-region-三條-driver">為什麼遷：ceiling / ecosystem / multi-region 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ceiling</td>
          <td>Swarm 跑 100-200 service 後 service discovery latency / scheduling 跟不上</td>
      </tr>
      <tr>
          <td>Ecosystem</td>
          <td>K8s ecosystem (Helm / Operator / mesh / GitOps) 成熟、Swarm 對等工具缺</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>Swarm 不支援、K8s 多 cluster federation 成熟</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（K8s → Swarm）：</p>
<ul>
<li>純 internal tool / 小規模（&lt; 30 service）、K8s 過度複雜</li>
<li>Edge / IoT scenario、Swarm footprint 小</li>
</ul>
<h2 id="6-維-audit">6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td><strong>High</strong>（docker-compose stack.yml → K8s YAML、syntax 完全不同）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Medium（Swarm 自管 → K8s self-host or managed）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td><strong>High</strong>（簡單 container orchestration → declarative resource model）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low（同 1 個 orchestration 系統）</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low（container image 不變）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Schema + Paradigm 雙 High → <strong>Type E paradigm shift</strong> 為主、Schema 高維獨立段。</p>
<h2 id="paradigm-對位">Paradigm 對位</h2>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Swarm</th>
          <th>K8s</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workload unit</td>
          <td>Service</td>
          <td>Deployment + Pod + Service</td>
      </tr>
      <tr>
          <td>Stack 定義</td>
          <td>stack.yml (docker-compose 格式)</td>
          <td>YAML manifest (multiple resources)</td>
      </tr>
      <tr>
          <td>Networking</td>
          <td>Overlay network (built-in)</td>
          <td>CNI plugin (Calico / Cilium / etc)</td>
      </tr>
      <tr>
          <td>Service discovery</td>
          <td>DNS-based built-in</td>
          <td>DNS-based (CoreDNS) + Service object</td>
      </tr>
      <tr>
          <td>Load balancing</td>
          <td>Built-in routing mesh</td>
          <td>Service + Ingress + LoadBalancer</td>
      </tr>
      <tr>
          <td>Secret management</td>
          <td>Docker secrets</td>
          <td>K8s Secret + 外部 Vault / Secrets Manager</td>
      </tr>
      <tr>
          <td>Rolling update</td>
          <td><code>docker service update --image ...</code></td>
          <td>Deployment + rolling update + readiness probe</td>
      </tr>
      <tr>
          <td>Autoscaling</td>
          <td>手動 scale</td>
          <td>HPA (Horizontal Pod Autoscaler)</td>
      </tr>
      <tr>
          <td>RBAC</td>
          <td>Limited (Swarm enterprise)</td>
          <td>First-class (Role / RoleBinding / ServiceAccount)</td>
      </tr>
      <tr>
          <td>Persistent storage</td>
          <td>Volume + driver plugin</td>
          <td>PV / PVC + CSI driver</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>無 (要外掛 Traefik)</td>
          <td>Istio / Linkerd / Cilium</td>
      </tr>
      <tr>
          <td>GitOps</td>
          <td>無 native</td>
          <td>Argo CD / Flux (first-class)</td>
      </tr>
  </tbody>
</table>
<h2 id="schema-gapdocker-compose-vs-k8s-yaml">Schema gap：docker-compose vs K8s YAML</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Docker Swarm stack.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">webapp</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">myapp:1.0</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">update_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">parallelism</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">restart_policy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="kc">on</span>-<span class="l">failure</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="l">frontend</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:8080&#34;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># K8s equivalent (Deployment + Service + Ingress)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RollingUpdate</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">rollingUpdate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">maxSurge</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">maxUnavailable</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">webapp }</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">webapp }</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">myapp:1.0</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">            </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">128Mi</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">500m</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512Mi</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w"></span><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w"></span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">webapp }</span><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span></span></span></code></pre></div><p>1 Swarm service → 2-3 K8s resource（Deployment + Service + 可能 Ingress / HPA）；application 不改但 <em>deployment 端工作量 5-10x</em>。</p>
<h2 id="migration-流程">Migration 流程</h2>
<h3 id="partial-migration--混合架構">Partial migration + 混合架構</h3>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/" data-link-title="etcd → Consul：KV &#43; N 個 extras feature matrix" data-link-desc="etcd → Consul 是 Type E paradigm shift expansion — 從 pure KV store 升到 service mesh / discovery / health check / multi-DC；本文用對照表 &#43; paradigm expansion 路線、5 個 production 踩雷（API 對位 / lock semantics / watch event model / multi-DC topology / ACL system）">etcd → Consul</a> 同 Type E pattern：</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. Audit application：列所有 Swarm stack + service
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - 簡單 stateless: 先切 K8s (低風險)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Stateful (DB / queue): 評估 K8s operator 或保留 Swarm
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - Critical service: 雙跑期確認 K8s 行為對等
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">3. K8s cluster 建置:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - Managed (EKS / GKE / AKS) vs self-host (kubeadm)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - 配 ingress controller / cert-manager / monitoring
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. Application 遷移 (per stack)
</span></span><span class="line"><span class="ln">10</span><span class="cl">   - 寫 K8s YAML / Helm chart
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 配 readiness/liveness probe / resource request
</span></span><span class="line"><span class="ln">12</span><span class="cl">   - Networking + secret 對位
</span></span><span class="line"><span class="ln">13</span><span class="cl">5. Cutover + Swarm decommission
</span></span><span class="line"><span class="ln">14</span><span class="cl">   - 部分 stack 切完、評估 Swarm 是否保留 (legacy / edge)
</span></span><span class="line"><span class="ln">15</span><span class="cl">   - 多數 organization 完全 decommission Swarm</span></span></code></pre></div><p>整體 3-6 個月、依 stack 數量跟 application 複雜度。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1networking-model-差cross-service-connectivity-失效">Case 1：Networking model 差、cross-service connectivity 失效</h3>
<p><strong>徵兆</strong>：cutover 後 service A 連 service B 失敗、Swarm 端 <code>tasks.service_b</code> DNS 對位 K8s 端 <code>service-b.namespace.svc.cluster.local</code> 不通。</p>
<p><strong>根因</strong>：Swarm overlay network 內 service-to-service 用 short name (<code>service_b</code>)、K8s 用 FQDN；application 端 service URL 寫死。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Application 端用 short name + cluster DNS search domain</li>
<li>K8s 端設 <code>dnsPolicy: ClusterFirst</code> 預設、確認 <code>kubectl get svc -A</code> 對應</li>
<li>NetworkPolicy 預設 deny-all、明示 allow rule</li>
</ol>
<h3 id="case-2secret-rotation-從-swarm-secrets-換-vault--secrets-manager">Case 2：Secret rotation 從 Swarm secrets 換 Vault / Secrets Manager</h3>
<p><strong>徵兆</strong>：原本 Swarm 用 <code>docker secret</code> 旋轉 secret、切 K8s 後 K8s Secret 是 <em>static value</em>、rotation 不自動。</p>
<p><strong>根因</strong>：K8s Secret 是 K8s-native 但 <em>not auto-rotated</em>、需要外部 Vault / Secrets Manager + agent (vault-agent-injector / external-secrets-operator)。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>K8s 端 deploy external-secrets-operator + AWS Secrets Manager / Vault integration</li>
<li>Application 端 mount file or env variable、不在 code 寫死</li>
<li>Rotation 走 vendor-side、K8s 端 sidecar 自動 reload</li>
</ol>
<h3 id="case-3readiness-probe-沒設rolling-update-期間-traffic-loss">Case 3：Readiness probe 沒設、rolling update 期間 traffic loss</h3>
<p><strong>徵兆</strong>：cutover 後 deploy 期間 application 5-10% request 失敗；發現 pod startup 完成前就接 traffic。</p>
<p><strong>根因</strong>：Swarm 簡單 restart_policy 沒對等 probe 概念；K8s 預設 deploy 後 immediate ready、若沒 readiness probe、startup 時間長的 application 會在未 ready 時接流量。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>必加 readiness probe</strong>：HTTP / TCP / exec check</li>
<li><strong>配 initial delay</strong>：JVM application 預留 30-60s</li>
<li><strong>配 <code>minReadySeconds</code></strong>：deployment 端設 30s 確保 stable</li>
</ol>
<h3 id="case-4hpa-預設不啟autoscaling-失效">Case 4：HPA 預設不啟、autoscaling 失效</h3>
<p><strong>徵兆</strong>：Swarm 端寫了 cron-based autoscale script、切 K8s 後 script 失效、流量高峰沒 scale up。</p>
<p><strong>根因</strong>：K8s HPA 不是預設啟動、需要 <em>明示配置</em> + metrics-server install。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">autoscaling/v2</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HorizontalPodAutoscaler</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webapp-hpa</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">scaleTargetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">webapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">minReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">maxReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Resource</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">resource</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cpu</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Utilization</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">averageUtilization</span><span class="p">:</span><span class="w"> </span><span class="m">70</span></span></span></code></pre></div><p>裝 metrics-server / Keda（event-driven autoscaling）+ 配 HPA per Deployment。</p>
<h3 id="case-5yaml-維護地獄helm--kustomize-配置遲">Case 5：YAML 維護地獄、Helm / Kustomize 配置遲</h3>
<p><strong>徵兆</strong>：cutover 後 K8s YAML 從 5 個檔（Swarm stack）變 50+ 個 K8s manifest；每個 application 端要改一個 config 都要動 N 個 file。</p>
<p><strong>根因</strong>：K8s YAML 是 <em>very verbose</em>、不像 docker-compose 簡潔；缺 templating 跟 environment 抽象。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Helm chart</strong>：對 application 包成 chart、用 <code>values.yaml</code> 抽象環境差異</li>
<li><strong>Kustomize</strong>：base + overlay pattern、不靠 templating</li>
<li><strong>GitOps with Argo CD / Flux</strong>：宣告式部署、降 manual kubectl 操作</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Docker Swarm</th>
          <th>Kubernetes (managed)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster cost (mid-tier)</td>
          <td>$300-800 / mo</td>
          <td>$500-1500 / mo（EKS/GKE/AKS control plane + nodes）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.5-1.5（除非 managed、降到 0.3-0.7）</td>
      </tr>
      <tr>
          <td>Ecosystem maturity</td>
          <td>低、衰退</td>
          <td>高、active growth</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>不支援</td>
          <td>多 cluster federation 成熟</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>2-4 FTE × 3-6 個月</td>
      </tr>
      <tr>
          <td>Long-term ROI</td>
          <td>Negative（社群縮）</td>
          <td>Positive（feature growth）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：&lt; 30 service 小 organization 可不切；50+ service 開始撞 Swarm ceiling、值得評估；100+ service / multi-region 必切。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-service-mesh-整合">跟 Service mesh 整合</h3>
<p>Cutover 後 <em>順便</em> 評估 Istio / Linkerd / Cilium service mesh、cover mTLS / observability / traffic policy；不要在 Swarm migration 後立刻上 mesh、分階段。</p>
<h3 id="跟-gitops-整合">跟 GitOps 整合</h3>
<p>K8s + Argo CD / Flux 是 <em>natural pair</em>；migration 時直接走 GitOps、避免 manual kubectl 操作累積。</p>
<h3 id="跟-vault--aws-secrets-manager-對齊">跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager</a> 對齊</h3>
<p>Swarm secrets → K8s Secret → external secrets management 是 <em>3-step 演進</em>、不是 1-step；migration 期間先用 K8s Secret、之後切 Vault / Secrets Manager。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a> / <a href="/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/" data-link-title="etcd → Consul：KV &#43; N 個 extras feature matrix" data-link-desc="etcd → Consul 是 Type E paradigm shift expansion — 從 pure KV store 升到 service mesh / discovery / health check / multi-DC；本文用對照表 &#43; paradigm expansion 路線、5 個 production 踩雷（API 對位 / lock semantics / watch event model / multi-DC topology / ACL system）">etcd → Consul</a> / <a href="/blog/backend/04-observability/vendors/honeycomb/migrate-from-sentry/" data-link-title="Sentry → Honeycomb：trace 不是 error、是不同 observability paradigm" data-link-desc="Sentry → Honeycomb 是 paradigm shift — Sentry 主軸是 error tracking &#43; transaction trace、Honeycomb 主軸是 high-cardinality wide-event observability；本文釐清 paradigm 邊界、5 個 production 踩雷（event schema 對位 / sampling 行為 / error grouping 失效 / cost 模型差 / alert paradigm shift）">Sentry → Honeycomb</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。同時是 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation&lt;/a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 &lt;em>consistency 軸驗證&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="same-protocol-different-contractconsistency-model-對照">Same protocol, different contract：consistency model 對照&lt;/h2>
&lt;p>DynamoDB 的 read 操作支援兩種 consistency：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>Strongly Consistent Read&lt;/th>
 &lt;th>Eventually Consistent Read&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Protocol&lt;/td>
 &lt;td>同（DynamoDB API）&lt;/td>
 &lt;td>同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API call&lt;/td>
 &lt;td>同 &lt;code>GetItem&lt;/code> / &lt;code>Query&lt;/code> / &lt;code>Scan&lt;/code>&lt;/td>
 &lt;td>同（多 &lt;code>ConsistentRead=false&lt;/code> flag）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結果&lt;/td>
 &lt;td>最新 commit 的值&lt;/td>
 &lt;td>可能 stale 0-100ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency p99&lt;/td>
 &lt;td>5-15ms&lt;/td>
 &lt;td>1-5ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Throughput cost (RCU)&lt;/td>
 &lt;td>1 RCU per 4KB read&lt;/td>
 &lt;td>&lt;strong>0.5 RCU per 4KB read&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-AZ&lt;/td>
 &lt;td>跨 AZ 讀（quorum）&lt;/td>
 &lt;td>單 AZ 讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障行為&lt;/td>
 &lt;td>leader unavailable 時 read 失敗&lt;/td>
 &lt;td>secondary alive 時 read 仍 work&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者 &lt;em>同 protocol, same API, same table&lt;/em> — 唯一差異是 &lt;em>application contract&lt;/em>：能否接受 0-100ms 的 staleness。&lt;/p>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">6 維 diff dimension audit&lt;/a> 對「strongly consistent → eventually consistent」遷移：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。同時是 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation</a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 <em>consistency 軸驗證</em>。</p></blockquote>
<h2 id="same-protocol-different-contractconsistency-model-對照">Same protocol, different contract：consistency model 對照</h2>
<p>DynamoDB 的 read 操作支援兩種 consistency：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>Strongly Consistent Read</th>
          <th>Eventually Consistent Read</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Protocol</td>
          <td>同（DynamoDB API）</td>
          <td>同</td>
      </tr>
      <tr>
          <td>API call</td>
          <td>同 <code>GetItem</code> / <code>Query</code> / <code>Scan</code></td>
          <td>同（多 <code>ConsistentRead=false</code> flag）</td>
      </tr>
      <tr>
          <td>結果</td>
          <td>最新 commit 的值</td>
          <td>可能 stale 0-100ms</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>5-15ms</td>
          <td>1-5ms</td>
      </tr>
      <tr>
          <td>Throughput cost (RCU)</td>
          <td>1 RCU per 4KB read</td>
          <td><strong>0.5 RCU per 4KB read</strong></td>
      </tr>
      <tr>
          <td>Cross-AZ</td>
          <td>跨 AZ 讀（quorum）</td>
          <td>單 AZ 讀</td>
      </tr>
      <tr>
          <td>故障行為</td>
          <td>leader unavailable 時 read 失敗</td>
          <td>secondary alive 時 read 仍 work</td>
      </tr>
  </tbody>
</table>
<p>兩者 <em>同 protocol, same API, same table</em> — 唯一差異是 <em>application contract</em>：能否接受 0-100ms 的 staleness。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">6 維 diff dimension audit</a> 對「strongly consistent → eventually consistent」遷移：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 API、只改 ConsistentRead flag</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 cluster、operational stack 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 NoSQL document store</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 table</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>每個 read site 評估、可改</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 partition / replication</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Consistency contract</strong></td>
          <td><strong>strong → eventual、application semantic 完全改</strong></td>
          <td><strong>High</strong></td>
      </tr>
  </tbody>
</table>
<p>6 維 audit 抓不到「Consistency contract = High」這軸。用既有 6 維歸類、會走 Type B drop-in + application change 中維獨立段；但這個歸類 <em>漏掉真正的工作量</em>：</p>
<ul>
<li>Application code change（加 ConsistentRead flag）：~10%</li>
<li>Operational verification：~5%</li>
<li><strong>Application contract review（每個 read site 評估 staleness 是否可接受）：~85%</strong></li>
</ul>
<p>工作量主軸在 <em>contract semantic 重審</em>、不在既有 6 維任一個。Consistency 是 <em>候選的第 7 維</em>（或 8 維、跟 identity 並列）。</p>
<h2 id="consistency-axis-是否獨立3-個論據">Consistency axis 是否獨立：3 個論據</h2>
<p><strong>Yes、consistency 是獨立軸</strong>：</p>
<ol>
<li><strong>Schema / paradigm / operational 不變 → consistency 仍可變</strong>：同 DynamoDB table、同 application、同 IAM、只改 <code>ConsistentRead</code> flag、cost 砍半但 application contract 改；其他 6 維皆 Low、但工作量 80%+ 在 contract review</li>
<li><strong>Paradigm 是 high-level、consistency 是 low-level</strong>：Kafka ↔ NATS 是 paradigm 差（log-based vs subject-based）；DynamoDB strong → eventual 是 <em>同 paradigm 內的 consistency 子議題</em>；歸 paradigm 維度太粗</li>
<li><strong>可獨立發生</strong>：PostgreSQL <code>READ COMMITTED → SERIALIZABLE</code> migration 同 vendor 同 schema 同 operational、只改 isolation level；Cassandra <code>LOCAL_QUORUM → EACH_QUORUM</code> 同 vendor、只改 consistency level — 都是 consistency 獨立變動的 case</li>
</ol>
<p><strong>No、consistency 可塞 paradigm</strong>：</p>
<ul>
<li>反論：consistency 是 paradigm 的子議題</li>
<li>拒絕：paradigm 涵蓋 <em>核心抽象</em>（OLTP / log / pub-sub / document）、consistency 是 <em>正確性 contract</em> 屬不同 axis</li>
</ul>
<p>實證：本文 migration 工作量 85% 在 contract review、確認 consistency 是 <em>獨立工作量主軸</em>。</p>
<h2 id="結構類-type-b--consistency-contract-review-獨立段">結構：類 Type B + consistency contract review 獨立段</h2>
<p>跟既有 Type B <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對照、本文多出 <em>consistency contract review</em> 獨立段：</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. Same protocol, different contract（consistency axis 對照表開頭）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Consistency axis 是否獨立的論據
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 結構 differentiator（類 Type B + contract review）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Read site audit (per-call site review)
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Migration 流程（dual-read 觀察 + canary cutover）
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Production 故障演練
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. Capacity / cost
</span></span><span class="line"><span class="ln">8</span><span class="cl">8. 整合 / 下一步</span></span></code></pre></div><p>8 章節、200-260 行。比標準 Type B 多 1 段（contract review）+ 1 段（axis 獨立論據）。</p>
<h2 id="read-site-auditper-call-site-contract-review">Read site audit：per-call site contract review</h2>
<p>不是 <em>table-level</em> 決定 consistency、是 <em>call site-level</em> 決定。每個 <code>GetItem</code> / <code>Query</code> / <code>Scan</code> 必須單獨 audit：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Pre-audit application code</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># Find all DynamoDB read sites</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="err">$</span> <span class="n">grep</span> <span class="o">-</span><span class="n">r</span> <span class="s2">&#34;table.get_item\|table.query\|table.scan&#34;</span> <span class="n">src</span><span class="o">/</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="c1"># Per-site contract review template:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># - Site: src/order_service.py:123 - get_item by order_id</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># - Context: 顯示 order detail page、user 剛點「我的訂單」</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># - Contract: user 可接受 100ms 內 stale data?</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># - Decision: YES → ConsistentRead=False, saves 50% RCU</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#             NO  → keep ConsistentRead=True</span></span></span></code></pre></div><p>Audit 分類矩陣（典型 application）：</p>
<table>
  <thead>
      <tr>
          <th>Read pattern</th>
          <th>預設 consistency</th>
          <th>Eventual 是否可接受</th>
          <th>估佔比</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>User read 自己剛 commit 的 data</td>
          <td>Strong（read-your-write）</td>
          <td>通常 NO</td>
          <td>5-10%</td>
      </tr>
      <tr>
          <td>List query（顯示用 / search 結果）</td>
          <td>Strong（過度保守）</td>
          <td>YES</td>
          <td>30-40%</td>
      </tr>
      <tr>
          <td>Background job / analytics</td>
          <td>Strong（過度保守）</td>
          <td>YES</td>
          <td>20-30%</td>
      </tr>
      <tr>
          <td>Real-time dashboard refresh</td>
          <td>Strong</td>
          <td>depends（refresh 間隔）</td>
          <td>10-15%</td>
      </tr>
      <tr>
          <td>跟 strongly consistent write 同 transaction</td>
          <td>Strong（必要）</td>
          <td>NO</td>
          <td>5-10%</td>
      </tr>
      <tr>
          <td>Health check / monitoring</td>
          <td>Strong（不必要）</td>
          <td>YES</td>
          <td>5-10%</td>
      </tr>
  </tbody>
</table>
<p>audit 完後 application 端 60-80% read site 可改 eventual、剩餘 20-40% 保留 strong；整體 RCU cost 降 30-40%。</p>
<h2 id="migration-流程">Migration 流程</h2>
<h3 id="phase-0audit--classify">Phase 0：Audit + classify</h3>
<ul>
<li>Grep application code 找所有 read site</li>
<li>per-site contract review、決定 strong / eventual</li>
<li>估計 RCU saving</li>
</ul>
<h3 id="phase-1低風險-site-切換">Phase 1：低風險 site 切換</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Before</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;order_id&#39;</span><span class="p">:</span> <span class="n">order_id</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">ConsistentRead</span><span class="o">=</span><span class="kc">True</span>  <span class="c1"># 預設保守</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">)</span>
</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="c1"># After（顯式設）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;order_id&#39;</span><span class="p">:</span> <span class="n">order_id</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">ConsistentRead</span><span class="o">=</span><span class="kc">False</span>  <span class="c1"># 明示 eventual OK</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>從 <em>background job / search result</em> 開始（低風險、staleness impact 低）、跑 1 週觀察 application metric。</p>
<h3 id="phase-2中風險-site-切換">Phase 2：中風險 site 切換</h3>
<ul>
<li>User-facing list query</li>
<li>Dashboard refresh</li>
<li>配 application-side 「last updated X seconds ago」hint 讓 user 知道是 cached/stale</li>
</ul>
<h3 id="phase-3審慎-site-保留-strong">Phase 3：審慎 site 保留 strong</h3>
<ul>
<li>Read-your-write pattern</li>
<li>Transactional read</li>
<li>Financial / payment-critical lookup</li>
</ul>
<p>Decision document 寫進 ADR、之後新 read site 直接套規則。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1read-your-write-失效user-看到自己沒提交的舊資料">Case 1：Read-your-write 失效、user 看到自己沒提交的舊資料</h3>
<p><strong>徵兆</strong>：user 在 settings page 改了 email、submit 後跳轉首頁、首頁 widget 顯示舊 email 5-30 秒；user feedback「我改了但沒生效」。</p>
<p><strong>根因</strong>：首頁 widget 用 <code>ConsistentRead=False</code> 讀 user profile、剛 commit 的 write 還在 propagate；違反 read-your-write semantic。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Read-your-write 場景強制 strong read</strong>：user 自己 fetch 自己的 data、加 <code>ConsistentRead=True</code></li>
<li><strong>Application-side cache invalidation</strong>：write 後立刻 invalidate local cache、避免 stale read 餵 user</li>
<li><strong>Routing</strong>：user-self-fetch 路由到 strong read、其他 user 看 user 用 eventual read（90% 流量仍便宜）</li>
</ol>
<h3 id="case-2跨-record-consistency-假設失效">Case 2：跨 record consistency 假設失效</h3>
<p><strong>徵兆</strong>：application 寫 order + 寫 inventory（兩個 record）、之後 read order + read inventory；發現有時 order 已寫 inventory 沒寫、application 顯示「order created but inventory not updated」、business state inconsistent。</p>
<p><strong>根因</strong>：DynamoDB <em>沒 transaction 跨多 record</em>（除非用 <code>TransactWriteItems</code> API）；eventual read 加劇 inconsistency window；strong read 並不解決根因。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：跨 record 寫入用 <code>TransactWriteItems</code>、確保 atomic</li>
<li><strong>read 端 saga pattern</strong>：accept eventual + application-level retry/reconcile</li>
<li><strong>eventual consistency 不是 root cause</strong>：strong read 也會看到 inconsistency、修跨 record write 是根因解</li>
</ol>
<h3 id="case-3background-job-retry-跑舊資料">Case 3：Background job retry 跑舊資料</h3>
<p><strong>徵兆</strong>：background job 每 5 分鐘掃 unprocessed orders、用 <code>ConsistentRead=False</code>；偶爾 job retry 2 次都 process 同 order、duplicate processing。</p>
<p><strong>根因</strong>：job round 1 抓到 unprocessed order → mark as processed；job round 2 read 仍看到 <em>未 mark</em> 的舊狀態（eventual stale）、又 process 一次。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Idempotent processing</strong>：用 order ID + 自己 dedup 表、不依賴 DynamoDB consistency</li>
<li><strong>Conditional write</strong>：<code>UpdateItem</code> 加 <code>ConditionExpression: attribute_not_exists(processed_at)</code>、duplicate 由 DynamoDB 拒絕</li>
<li><strong>不切 strong</strong>：background job 切 strong 也只是 <em>減少</em> duplicate 機率、不解決；用 idempotent + conditional 才對</li>
</ol>
<h3 id="case-4cost-沒降反升application-改錯方向">Case 4：Cost 沒降反升、application 改錯方向</h3>
<p><strong>徵兆</strong>：切換 6 個月後 RCU 成本反而上升 20%；audit 後發現 application 加了大量 background scan 用 <code>ConsistentRead=False</code>、scan 本身就比 query 貴、cost 飆。</p>
<p><strong>根因</strong>：team 把「consistency 砍半 = cost 砍半」過度推廣、加了原本不存在的 read site；新 read 即使 eventual 也是 <em>新 cost</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Migration scope 內 freeze new read</strong>：consistency 切換期間禁止加新 read 邏輯</li>
<li><strong>Cost monitoring 在切換前 baseline</strong>：對齊原 RCU usage、新 read 出現必須單獨 review</li>
<li><strong>Scan vs Query</strong>：跑 sample data、確認 application 用 Query 不是 Scan（Scan 對所有 partition 讀 / Query 對 partition key 讀）</li>
</ol>
<h3 id="case-5故障期間-eventual-read-還能-work應變流程沒覆蓋">Case 5：故障期間 eventual read 還能 work、應變流程沒覆蓋</h3>
<p><strong>徵兆</strong>：us-east-1 partial outage、strong read 開始 timeout、application 切到 fallback；但 fallback 邏輯只 cover「全 region fail」、沒 cover「strong fail / eventual ok」中間狀態；流量打到 fallback 路徑、出乎預期慢。</p>
<p><strong>根因</strong>：DynamoDB 提供 <em>partial consistency degradation</em> — leader replica 不可用時 strong read 失敗、secondary 仍 alive、eventual read 仍可；application 沒設計這個中間狀態的處理。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>明示 fallback strategy</strong>：strong read 失敗時 application 端 retry with eventual + warning user「showing potentially stale data due to system degradation」</li>
<li><strong>Circuit breaker per-consistency-level</strong>：strong read circuit 跟 eventual read circuit 分開、避免一邊 fail 拖另一邊</li>
<li><strong>DR drill 覆蓋此 case</strong>：故障演練不只「全失敗 vs 全 work」、要演 <em>partial degradation</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>All strongly consistent</th>
          <th>Mixed（70% eventual + 30% strong）</th>
          <th>All eventually consistent</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RCU per read</td>
          <td>1 RCU per 4KB</td>
          <td>0.65 RCU per 4KB（avg）</td>
          <td>0.5 RCU per 4KB</td>
      </tr>
      <tr>
          <td>Read latency p99</td>
          <td>10-15ms</td>
          <td>5-10ms</td>
          <td>1-5ms</td>
      </tr>
      <tr>
          <td>Cost saving</td>
          <td>baseline</td>
          <td>~35%</td>
          <td>~50%</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low</td>
          <td>Medium（per-site decision）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Audit / migration cost</td>
          <td>-</td>
          <td>2-3 FTE 月 × audit</td>
          <td>同 mixed</td>
      </tr>
      <tr>
          <td>Cross-AZ failure</td>
          <td>Strong read fail</td>
          <td>Strong fail, eventual work</td>
          <td>All work</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：完全 strong 是 <em>過度保守</em>、完全 eventual 是 <em>過度激進</em>；mixed 是 sweet spot、但 audit 工作量大。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql-read-committed--serializable-對照">跟 <a href="https://www.postgresql.org/docs/current/transaction-iso.html">PostgreSQL READ COMMITTED → SERIALIZABLE</a> 對照</h3>
<p>PostgreSQL isolation level migration 也是 consistency axis 變動、但方向相反（弱 → 強）；同樣需要 per-call-site review、application 端可能撞 serialization failure 處理。</p>
<h3 id="跟-cassandra-local_-對照">跟 <a href="https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html#tunable-consistency">Cassandra LOCAL_QUORUM → EACH_QUORUM</a> 對照</h3>
<p>Cassandra tunable consistency 是另一個 consistency 獨立軸 case；EACH_QUORUM 跨 DC 需所有 DC quorum、latency 增、availability 降。</p>
<h3 id="跟-aurora-read-replica-對照">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">Aurora read replica</a> 對照</h3>
<p>Aurora read replica 也涉 eventual read decision；application 路由策略類似但 mechanism 不同（DNS-based vs API flag）。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Consistency axis 升級為第 7 維 audit dimension</strong>：累積 PostgreSQL isolation level / Cassandra tunable consistency / Aurora reader endpoint 3-5 個 case 後評估</li>
<li><strong>Sub-dimension proposal</strong>：consistency axis 可拆 sub-dimension - read consistency / write consistency / replication lag tolerance / serialization level</li>
<li><strong>跟 paradigm 軸的邊界釐清</strong>：CRDT / event sourcing 是 paradigm 還是 consistency model 選擇？</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<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>平行 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（Type B drop-in 對照）</li>
<li>平行 axis 候選驗證 (sibling)：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager</a>（identity 候選） / <a href="/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/" data-link-title="PostgreSQL Multi-Region GDPR Rollout：政策驅動的 migration 屬本 methodology 嗎" data-link-desc="PostgreSQL 單 region → multi-region 同時滿足 GDPR EU residency 是 *政策驅動* 兼 *topology 變動* 兼 *operational redesign* 的多軸 migration；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 residency axis 候選 — residency 是 driver 還是獨立 audit 軸；涵蓋 logical replication 配 GDPR / 5 個 production 踩雷 / cross-region cost">PostgreSQL Multi-Region GDPR Rollout</a>（residency 候選）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation 第 1 點</a>（consistency axis 候選驗證、本文是該驗證的 dogfood）</li>
</ul>
]]></content:encoded></item><item><title>Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS&lt;/a>。跟前四篇 migration（schema 差 / drop-in / operational redesign / multi-tool 拆分）對照、本篇是 &lt;em>paradigm shift&lt;/em> — 兩端不是「同類產品的不同實作」、是 &lt;em>不同抽象層的 messaging system&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="kafka--nats-migration字面上不成立">「Kafka → NATS migration」字面上不成立&lt;/h2>
&lt;p>前面四篇 migration 都隱含一個前提：source 跟 target 是 &lt;em>同類產品&lt;/em>、只是不同實作或 deployment 模型。「Kafka → NATS」字面上看起來也是 &lt;em>messaging migration&lt;/em>、但實際上：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Kafka&lt;/th>
 &lt;th>NATS Core&lt;/th>
 &lt;th>NATS JetStream&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Core abstraction&lt;/td>
 &lt;td>Distributed log（partition + offset）&lt;/td>
 &lt;td>Pub/Sub subject（fire-and-forget）&lt;/td>
 &lt;td>Stream（subject group + retention）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Message persistence&lt;/td>
 &lt;td>Default persistent（log retention）&lt;/td>
 &lt;td>&lt;strong>不持久化&lt;/strong>（subscriber 缺席 = lost）&lt;/td>
 &lt;td>持久化（K/V backend / file）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Delivery semantic&lt;/td>
 &lt;td>At-least-once / exactly-once（事務）&lt;/td>
 &lt;td>At-most-once&lt;/td>
 &lt;td>At-least-once / exactly-once&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consumer model&lt;/td>
 &lt;td>Consumer group + offset&lt;/td>
 &lt;td>Subscriber + subject pattern&lt;/td>
 &lt;td>Durable consumer + pull / push&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ordering&lt;/td>
 &lt;td>Per partition strict&lt;/td>
 &lt;td>無 ordering guarantee&lt;/td>
 &lt;td>Per stream / per consumer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replay&lt;/td>
 &lt;td>隨意 from offset&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;td>from sequence number&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Throughput&lt;/td>
 &lt;td>高（M msg/s）&lt;/td>
 &lt;td>極高（10M+ msg/s）&lt;/td>
 &lt;td>中（100K-1M msg/s）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency&lt;/td>
 &lt;td>5-50ms&lt;/td>
 &lt;td>&amp;lt; 1ms&lt;/td>
 &lt;td>5-20ms&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Kafka 跟 NATS Core 是 &lt;em>不同類產品&lt;/em> — 一個是 durable event log、一個是 transient pub/sub。「migration」需要先決定 &lt;em>target 是 NATS Core 還是 JetStream&lt;/em>、然後判斷 &lt;em>application 模式能否重設計&lt;/em> 對應。&lt;/p>
&lt;h2 id="什麼情境真的能換什麼不能">什麼情境真的能換、什麼不能&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Application 模式&lt;/th>
 &lt;th>Kafka 適配度&lt;/th>
 &lt;th>NATS Core 適配&lt;/th>
 &lt;th>NATS JetStream 適配&lt;/th>
 &lt;th>「migration」可行性&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Event sourcing（replay 過去事件）&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>不可（無 replay）&lt;/td>
 &lt;td>中（JetStream replay）&lt;/td>
 &lt;td>部分（移到 JetStream）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Microservice async messaging&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Real-time pub/sub（低延遲、可丟）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高（移到 Core）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 service 命令 / RPC&lt;/td>
 &lt;td>弱（不適合）&lt;/td>
 &lt;td>強（request-reply）&lt;/td>
 &lt;td>弱&lt;/td>
 &lt;td>不需要遷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大量 log / metric / event collection&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>弱&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低（保留 Kafka）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-tenant message bus&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Strict ordering + transactional&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>不可&lt;/td>
 &lt;td>中（per stream）&lt;/td>
 &lt;td>部分（部分功能犧牲）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5+ 年歷史 retention&lt;/td>
 &lt;td>強&lt;/td>
 &lt;td>不可&lt;/td>
 &lt;td>中（retention 設長）&lt;/td>
 &lt;td>部分&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> 跟 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a>。跟前四篇 migration（schema 差 / drop-in / operational redesign / multi-tool 拆分）對照、本篇是 <em>paradigm shift</em> — 兩端不是「同類產品的不同實作」、是 <em>不同抽象層的 messaging system</em>。</p></blockquote>
<h2 id="kafka--nats-migration字面上不成立">「Kafka → NATS migration」字面上不成立</h2>
<p>前面四篇 migration 都隱含一個前提：source 跟 target 是 <em>同類產品</em>、只是不同實作或 deployment 模型。「Kafka → NATS」字面上看起來也是 <em>messaging migration</em>、但實際上：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Kafka</th>
          <th>NATS Core</th>
          <th>NATS JetStream</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core abstraction</td>
          <td>Distributed log（partition + offset）</td>
          <td>Pub/Sub subject（fire-and-forget）</td>
          <td>Stream（subject group + retention）</td>
      </tr>
      <tr>
          <td>Message persistence</td>
          <td>Default persistent（log retention）</td>
          <td><strong>不持久化</strong>（subscriber 缺席 = lost）</td>
          <td>持久化（K/V backend / file）</td>
      </tr>
      <tr>
          <td>Delivery semantic</td>
          <td>At-least-once / exactly-once（事務）</td>
          <td>At-most-once</td>
          <td>At-least-once / exactly-once</td>
      </tr>
      <tr>
          <td>Consumer model</td>
          <td>Consumer group + offset</td>
          <td>Subscriber + subject pattern</td>
          <td>Durable consumer + pull / push</td>
      </tr>
      <tr>
          <td>Ordering</td>
          <td>Per partition strict</td>
          <td>無 ordering guarantee</td>
          <td>Per stream / per consumer</td>
      </tr>
      <tr>
          <td>Replay</td>
          <td>隨意 from offset</td>
          <td><strong>無</strong></td>
          <td>from sequence number</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>高（M msg/s）</td>
          <td>極高（10M+ msg/s）</td>
          <td>中（100K-1M msg/s）</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>5-50ms</td>
          <td>&lt; 1ms</td>
          <td>5-20ms</td>
      </tr>
  </tbody>
</table>
<p>Kafka 跟 NATS Core 是 <em>不同類產品</em> — 一個是 durable event log、一個是 transient pub/sub。「migration」需要先決定 <em>target 是 NATS Core 還是 JetStream</em>、然後判斷 <em>application 模式能否重設計</em> 對應。</p>
<h2 id="什麼情境真的能換什麼不能">什麼情境真的能換、什麼不能</h2>
<table>
  <thead>
      <tr>
          <th>Application 模式</th>
          <th>Kafka 適配度</th>
          <th>NATS Core 適配</th>
          <th>NATS JetStream 適配</th>
          <th>「migration」可行性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Event sourcing（replay 過去事件）</td>
          <td>強</td>
          <td>不可（無 replay）</td>
          <td>中（JetStream replay）</td>
          <td>部分（移到 JetStream）</td>
      </tr>
      <tr>
          <td>Microservice async messaging</td>
          <td>強</td>
          <td>強</td>
          <td>強</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Real-time pub/sub（低延遲、可丟）</td>
          <td>中</td>
          <td>強</td>
          <td>中</td>
          <td>高（移到 Core）</td>
      </tr>
      <tr>
          <td>跨 service 命令 / RPC</td>
          <td>弱（不適合）</td>
          <td>強（request-reply）</td>
          <td>弱</td>
          <td>不需要遷</td>
      </tr>
      <tr>
          <td>大量 log / metric / event collection</td>
          <td>強</td>
          <td>弱</td>
          <td>中</td>
          <td>低（保留 Kafka）</td>
      </tr>
      <tr>
          <td>Multi-tenant message bus</td>
          <td>中</td>
          <td>強</td>
          <td>強</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Strict ordering + transactional</td>
          <td>強</td>
          <td>不可</td>
          <td>中（per stream）</td>
          <td>部分（部分功能犧牲）</td>
      </tr>
      <tr>
          <td>5+ 年歷史 retention</td>
          <td>強</td>
          <td>不可</td>
          <td>中（retention 設長）</td>
          <td>部分</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：</p>
<ul>
<li><em>Microservice async messaging + 低延遲需求</em> → NATS Core 更合適、是 <em>真正的 migration</em></li>
<li><em>Event sourcing + replay</em> → JetStream 部分對等、但 partition / offset 觀念變了</li>
<li><em>Log collection / event streaming</em> → 不該遷、保留 Kafka</li>
</ul>
<h2 id="為什麼會考慮這個-paradigm-shift">為什麼會考慮這個 paradigm shift</h2>
<p>實務上觸發評估 NATS 通常三條 driver：</p>
<ol>
<li><strong>Cost + operational complexity</strong>：Kafka cluster + ZooKeeper（或 KRaft）+ Schema Registry + Connect 是重資產、3-5 broker + ops 1+ FTE；NATS 單 binary、無依賴、輕量</li>
<li><strong>Latency 要求 &lt; 1ms</strong>：Kafka 對單 message latency 不是 SLA、NATS Core 是</li>
<li><strong>Multi-tenant / multi-region 簡化</strong>：NATS 內建 <em>account</em> + <em>leaf node</em> 拓樸、跨 region 是 first-class</li>
</ol>
<p>但這三條 driver 都 <em>只在特定 application 模式有效</em>。不是普世 better、是 <em>某類 workload 適合</em>。</p>
<h2 id="migration-結構application-重設計--部分-stream-cutover">Migration 結構：application 重設計 + 部分 stream cutover</h2>
<p>跟前面四篇 migration 結構都不同、Kafka ↔ NATS 是 <em>混合</em>：</p>
<ol>
<li><strong>Phase 0：scope 判讀</strong> — 列 application、區分「適合 NATS」vs「保留 Kafka」</li>
<li><strong>Phase 1：application code 重設計</strong> — 不是 SDK 換、是 <em>messaging pattern 改</em>（event sourcing → message bus / consumer group → durable consumer）</li>
<li><strong>Phase 2：部分 stream parallel run</strong> — 新 application 走 NATS、舊 application 持續 Kafka</li>
<li><strong>Phase 3：cutover 適合的 stream</strong></li>
<li><strong>Phase 4：長期混合架構</strong> — Kafka 跟 NATS <em>共存</em>、不消滅一邊</li>
</ol>
<p>整體不是 <em>一次 migration</em>、是 <em>漸進拆分</em>。多數 production 環境 <em>永遠</em> 是混合架構。</p>
<h2 id="application-重設計範例consumer-group--durable-consumer">Application 重設計範例：consumer group → durable consumer</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// Kafka 端 consumer group pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">consumer</span> <span class="o">:=</span> <span class="nx">kafka</span><span class="p">.</span><span class="nf">NewConsumer</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">kafka</span><span class="p">.</span><span class="nx">ConfigMap</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s">&#34;bootstrap.servers&#34;</span><span class="p">:</span> <span class="s">&#34;kafka:9092&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s">&#34;group.id&#34;</span><span class="p">:</span>          <span class="s">&#34;myapp-orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s">&#34;auto.offset.reset&#34;</span><span class="p">:</span> <span class="s">&#34;earliest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">consumer</span><span class="p">.</span><span class="nf">SubscribeTopics</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;orders&#34;</span><span class="p">},</span> <span class="kc">nil</span><span class="p">)</span>
</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">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">msg</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">consumer</span><span class="p">.</span><span class="nf">ReadMessage</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// process msg.Value</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">consumer</span><span class="p">.</span><span class="nf">CommitMessage</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// NATS JetStream durable consumer</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">js</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">nc</span><span class="p">.</span><span class="nf">JetStream</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">sub</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">js</span><span class="p">.</span><span class="nf">PullSubscribe</span><span class="p">(</span><span class="s">&#34;orders.&gt;&#34;</span><span class="p">,</span> <span class="s">&#34;myapp-orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">nats</span><span class="p">.</span><span class="nf">AckExplicit</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">nats</span><span class="p">.</span><span class="nf">MaxAckPending</span><span class="p">(</span><span class="mi">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">)</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="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">msgs</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">sub</span><span class="p">.</span><span class="nf">Fetch</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="nx">nats</span><span class="p">.</span><span class="nf">MaxWait</span><span class="p">(</span><span class="mi">5</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">msgs</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="c1">// process msg.Data</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>Kafka <code>auto.offset.reset</code> → NATS <code>DeliverPolicy</code>（多種選項）</li>
<li>Kafka commit message → NATS explicit Ack（per message）</li>
<li>Kafka partition → NATS subject hierarchy（<code>orders.&gt;</code> 通配）</li>
<li>Kafka rebalance → NATS 不需要、durable consumer 跨 instance 共享</li>
</ul>
<p>Application 邏輯改動 30-60%、不是 SDK 換。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1consumer-offset-觀念差replay-不對等">Case 1：Consumer offset 觀念差，replay 不對等</h3>
<p><strong>徵兆</strong>：application 設計「跑歷史 7 天事件 catch-up」、Kafka 設 <code>auto.offset.reset=earliest</code> + <code>seek_to(timestamp)</code> 跑；換 NATS JetStream 後找不到 <code>seek_to</code> 等價 API、catch-up 失敗。</p>
<p><strong>根因</strong>：Kafka offset 是 <em>broker-side 維護 + consumer-side commit</em>；NATS JetStream 用 <em>sequence number</em> + <code>DeliverPolicy.ByStartTime</code>、但 time-based seek 精度低、且 application code 必須改。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先設計</strong>：NATS JetStream 用 <code>DeliverPolicy.ByStartSequence</code> + 自管 sequence-time mapping</li>
<li><strong>保留 Kafka 給 replay-heavy use case</strong>：不是所有 application 都遷</li>
<li><strong>混合架構</strong>：歷史 replay 走 Kafka、新事件流走 NATS、application 處理雙來源</li>
</ol>
<h3 id="case-2retention-model-差異磁碟使用炸">Case 2：Retention model 差異、磁碟使用炸</h3>
<p><strong>徵兆</strong>：NATS JetStream stream 設 <code>retention=interest</code>（subscriber 收到就刪）、cutover 後 disk 持續長大；預期跟 Kafka log retention 7 天類似、實際資料留 30+ 天沒清。</p>
<p><strong>根因</strong>：NATS JetStream retention 有 3 種：<code>limits</code> / <code>interest</code> / <code>workqueue</code>。<code>interest</code> 是 <em>至少一個 subscriber 還沒 ack 就保留</em>；application 端 silent consumer（已下線但沒 unsubscribe）讓 message 永留。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預設 <code>retention=limits</code></strong>：用 <code>MaxAge</code> / <code>MaxBytes</code> 跟 Kafka log retention 對應、明確控制</li>
<li><strong><code>interest</code> retention 慎用</strong>：只在 <em>確認所有 subscriber lifecycle 受控</em> 場景</li>
<li><strong>Subscriber cleanup</strong>：application graceful shutdown 必須主動 unsubscribe、不留 zombie consumer</li>
</ol>
<h3 id="case-3exactly-once-假設不對等">Case 3：Exactly-once 假設不對等</h3>
<p><strong>徵兆</strong>：cutover 後發現某 application（payment processor）開始出現 <em>duplicate transaction</em>；Kafka 端用 transactional producer + idempotent consumer 跑了 2 年沒問題。</p>
<p><strong>根因</strong>：Kafka exactly-once 是 <em>producer transaction + consumer offset commit atomic</em>；NATS JetStream exactly-once 概念不一樣 — 是 <em>publish ack</em> + <em>consumer ack</em> 跨層 atomic、application 端要主動處理 idempotency。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>重新審視 application 端 idempotency</strong>：用 message ID + dedup store（Redis SETEX）顯式 dedup</li>
<li><strong>NATS JetStream 對 exactly-once 不該假設「自動」</strong>：application 端責任、不是 broker 端</li>
<li><strong>Payment / financial 場景慎遷</strong>：保留 Kafka transactional pattern 較穩</li>
</ol>
<h3 id="case-4schema-registry-缺位ad-hoc-schema-漂移">Case 4：Schema registry 缺位、ad-hoc schema 漂移</h3>
<p><strong>徵兆</strong>：NATS 部署 3 個月後、producer / consumer 間 schema 對不上、application bug；Kafka 端有 Confluent Schema Registry 強 enforce、NATS 沒對等服務。</p>
<p><strong>根因</strong>：NATS 哲學是 <em>minimalist</em>、不內建 schema registry；application 自己決定 payload format。Kafka 生態的 Avro / Protobuf + Registry 模式不直接搬。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>外部 schema management</strong>：用 BSR（Buf Schema Registry）或自家 Git-based registry、producer / consumer build-time 驗證</li>
<li><strong>NATS Object Store</strong>：JetStream 提供 K/V + Object Store、可存 schema 文件</li>
<li><strong>接受紀律性 trade-off</strong>：NATS 簡潔代價是 application 端紀律、不能靠 broker 強 enforce</li>
</ol>
<h3 id="case-5fan-out-模式跟-kafka-不一致">Case 5：Fan-out 模式跟 Kafka 不一致</h3>
<p><strong>徵兆</strong>：同一 event 要送 5 個 downstream service、Kafka 端用 consumer group + 5 個 group 跑；NATS 端設計 5 個 durable consumer、結果某些 message 漏 fan-out。</p>
<p><strong>根因</strong>：Kafka consumer group 對 <em>同 group 內 partition 分配</em>、不同 group 各自完整消費；NATS JetStream <code>Durable consumer</code> 預設行為跟 group 不同 — <em>單 durable consumer 是 shared subscription</em>、要 fan-out 需多個獨立 durable。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>明確設計 fan-out</strong>：N 個 downstream 對應 N 個 <em>獨立 durable consumer</em>、不共用</li>
<li><strong>用 <code>AckPolicy.None</code> + push subscriber</strong>：不需要 ack 的 fan-out 場景、用 ephemeral push subscriber</li>
<li><strong>檢查 application stream config</strong>：fan-out 失敗多半是 consumer config 錯、不是 NATS bug</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Kafka（self-managed）</th>
          <th>NATS（JetStream）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster size baseline</td>
          <td>3-5 broker + ZooKeeper / KRaft</td>
          <td>3 server（含 JetStream cluster）</td>
      </tr>
      <tr>
          <td>RAM / broker baseline</td>
          <td>16-64GB</td>
          <td>2-16GB</td>
      </tr>
      <tr>
          <td>Storage requirement</td>
          <td>高（log retention）</td>
          <td>中（JetStream file backend）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-2 FTE</td>
          <td>0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Throughput / single node</td>
          <td>100K-1M msg/s</td>
          <td>NATS Core：10M+、JetStream：100K-1M</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>5-50ms</td>
          <td>NATS Core：&lt; 1ms、JetStream：5-20ms</td>
      </tr>
      <tr>
          <td>Retention 1TB / month cost</td>
          <td>$400-800（含 HA）</td>
          <td>$200-400</td>
      </tr>
      <tr>
          <td>Operational complexity</td>
          <td>高（Schema Registry / Connect / Streams）</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Ecosystem maturity</td>
          <td>高（10+ 年）</td>
          <td>中（JetStream 2021+）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：簡單 messaging workload NATS 顯著便宜；complex event streaming（Schema Registry / Streams / Connect 重度用）Kafka 不替代。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是-long-term-default">混合架構是 long-term default</h3>
<p>多數 production 環境最終是 <em>Kafka + NATS 共存</em>：</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">[event sourcing / log collection]        [microservice async messaging]
</span></span><span class="line"><span class="ln">2</span><span class="cl">         Kafka                                       NATS
</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">         └──────── Bridge (Connect / Custom) ────────┘</span></span></code></pre></div><p>NATS 跑微服務間 messaging、Kafka 跑 event log / analytics pipeline；中間用 Kafka Connect NATS connector 或自寫 bridge 同步必要 stream。</p>
<h3 id="跟-logical-replication--debezium-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a> 對位</h3>
<p>CDC pipeline 設計：</p>
<ul>
<li>DB → Debezium → Kafka topic（event sourcing 主軸）</li>
<li>Kafka → NATS bridge → microservice fan-out</li>
<li>不直接 DB → Debezium → NATS（Debezium 不原生支援 NATS sink）</li>
</ul>
<h3 id="跟前-4-篇-migration-的結構對照">跟前 4 篇 migration 的結構對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Schema 差</th>
          <th>Operational 差</th>
          <th>Paradigm 差</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Splunk → Elastic</td>
          <td>高</td>
          <td>中</td>
          <td>低</td>
          <td>6-phase</td>
      </tr>
      <tr>
          <td>Redis → DragonflyDB</td>
          <td>無</td>
          <td>低</td>
          <td>低</td>
          <td>6-section + audit</td>
      </tr>
      <tr>
          <td>PostgreSQL → Aurora</td>
          <td>無</td>
          <td>高</td>
          <td>低</td>
          <td>hybrid</td>
      </tr>
      <tr>
          <td>Datadog → Grafana Stack</td>
          <td>中</td>
          <td>中</td>
          <td>低</td>
          <td>parallel streams</td>
      </tr>
      <tr>
          <td>Kafka ↔ NATS（本篇）</td>
          <td>中</td>
          <td>中</td>
          <td><strong>高</strong></td>
          <td>partial + 混合</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：migration 結構由 <em>最大差異維度</em> 決定、不是 universal phased playbook。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB → Atlas：Atlas 不是 MongoDB + managed、是另一個 product</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a> 跟 MongoDB Atlas。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type C operational redesign hybrid 的標準形態實證。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關 — 4 phase 之間的驗證條件就是 gate。&lt;/p>&lt;/blockquote>
&lt;h2 id="atlas-不是-mongodb--managed是另一個-product">Atlas 不是 MongoDB + managed、是另一個 product&lt;/h2>
&lt;p>「MongoDB Atlas 是 MongoDB 的 managed 版本」這個 framing 看似合理、實際誤導：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Protocol 相容&lt;/strong>：MongoDB wire protocol 一致、driver 不改、&lt;code>mongosh&lt;/code> 連線跟 self-managed 一樣&lt;/li>
&lt;li>&lt;strong>Storage 一致&lt;/strong>：WiredTiger storage engine 一樣、document model 一樣&lt;/li>
&lt;li>&lt;strong>API 一致&lt;/strong>：Aggregation framework、indexing、change stream 都一樣&lt;/li>
&lt;/ul>
&lt;p>但 &lt;em>operational surface 完全不同&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Operational concept&lt;/th>
 &lt;th>Self-managed MongoDB&lt;/th>
 &lt;th>Atlas&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cluster bootstrap&lt;/td>
 &lt;td>mongod + replica set config + cfgsvr + shard 手動&lt;/td>
 &lt;td>UI / API 一鍵建集群、全自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA&lt;/td>
 &lt;td>Replica set 自管 + arbiter + priority&lt;/td>
 &lt;td>自動跨 AZ replica + automatic failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup&lt;/td>
 &lt;td>mongodump + S3 archive 自管&lt;/td>
 &lt;td>內建 cloud backup + PITR（按 region 設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network access&lt;/td>
 &lt;td>VPC + security group + IP whitelist 自管&lt;/td>
 &lt;td>Atlas private endpoint / VPC peering / IP access list&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Authentication&lt;/td>
 &lt;td>mongod 內部 user / x.509 自管&lt;/td>
 &lt;td>Atlas Database User + 整合 LDAP / SSO / AWS IAM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Monitoring&lt;/td>
 &lt;td>Self-deploy Prometheus + grafana&lt;/td>
 &lt;td>Atlas Performance Advisor + APM 內建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sizing&lt;/td>
 &lt;td>Manual instance class + scale&lt;/td>
 &lt;td>Auto-tier scaling + tier-based pricing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Patching&lt;/td>
 &lt;td>Manual + outage window&lt;/td>
 &lt;td>Automatic（可配置 maintenance window）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Migration 主要工作不在 &lt;em>資料層&lt;/em> — protocol drop-in 已 cover；是 &lt;em>operational stack 全換&lt;/em>：SRE runbook、monitoring dashboard、access control、IAM 整合、cost 預估全要重做。「Atlas 是 managed MongoDB」這個 framing 低估了 operational 工作量。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a> 跟 MongoDB Atlas。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type C operational redesign hybrid 的標準形態實證。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關 — 4 phase 之間的驗證條件就是 gate。</p></blockquote>
<h2 id="atlas-不是-mongodb--managed是另一個-product">Atlas 不是 MongoDB + managed、是另一個 product</h2>
<p>「MongoDB Atlas 是 MongoDB 的 managed 版本」這個 framing 看似合理、實際誤導：</p>
<ul>
<li><strong>Protocol 相容</strong>：MongoDB wire protocol 一致、driver 不改、<code>mongosh</code> 連線跟 self-managed 一樣</li>
<li><strong>Storage 一致</strong>：WiredTiger storage engine 一樣、document model 一樣</li>
<li><strong>API 一致</strong>：Aggregation framework、indexing、change stream 都一樣</li>
</ul>
<p>但 <em>operational surface 完全不同</em>：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>Self-managed MongoDB</th>
          <th>Atlas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>mongod + replica set config + cfgsvr + shard 手動</td>
          <td>UI / API 一鍵建集群、全自動</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Replica set 自管 + arbiter + priority</td>
          <td>自動跨 AZ replica + automatic failover</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>mongodump + S3 archive 自管</td>
          <td>內建 cloud backup + PITR（按 region 設）</td>
      </tr>
      <tr>
          <td>Network access</td>
          <td>VPC + security group + IP whitelist 自管</td>
          <td>Atlas private endpoint / VPC peering / IP access list</td>
      </tr>
      <tr>
          <td>Authentication</td>
          <td>mongod 內部 user / x.509 自管</td>
          <td>Atlas Database User + 整合 LDAP / SSO / AWS IAM</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Self-deploy Prometheus + grafana</td>
          <td>Atlas Performance Advisor + APM 內建</td>
      </tr>
      <tr>
          <td>Sizing</td>
          <td>Manual instance class + scale</td>
          <td>Auto-tier scaling + tier-based pricing</td>
      </tr>
      <tr>
          <td>Patching</td>
          <td>Manual + outage window</td>
          <td>Automatic（可配置 maintenance window）</td>
      </tr>
  </tbody>
</table>
<p>Migration 主要工作不在 <em>資料層</em> — protocol drop-in 已 cover；是 <em>operational stack 全換</em>：SRE runbook、monitoring dashboard、access control、IAM 整合、cost 預估全要重做。「Atlas 是 managed MongoDB」這個 framing 低估了 operational 工作量。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>MongoDB protocol / API 完全相容</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>HA / backup / monitoring / IAM / network 全換</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 document DB</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個 cluster</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Connection string / IAM 整合改、application logic 不改</td>
          <td>Low/Medium</td>
      </tr>
  </tbody>
</table>
<p>主導維度 Operational = High、Schema / Paradigm 都 Low — 對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Type C operational redesign hybrid</a>。</p>
<h2 id="結構4-phase-operational--drop-in-cutover">結構：4-phase operational + drop-in cutover</h2>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 結構對齊（同 Type C）：</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">Phase 0：Pre-migration audit（1-2 週）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  - Workload sizing（IOPS / connection / storage）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  - Application connection pattern audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  - Compliance requirement audit
</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">Phase 1：Operational infrastructure 準備（2-3 週）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  - Atlas cluster 建立
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  - VPC peering / private endpoint
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  - IAM role + Atlas Database User
</span></span><span class="line"><span class="ln">10</span><span class="cl">  - Monitoring + alert
</span></span><span class="line"><span class="ln">11</span><span class="cl">  - Backup retention 設定
</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">Phase 2：Data migration（取決於 dataset 大小）
</span></span><span class="line"><span class="ln">14</span><span class="cl">  - mongomirror / Atlas Live Migration tool
</span></span><span class="line"><span class="ln">15</span><span class="cl">  - 或 mongodump → mongorestore（小 DB）
</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">Phase 3：Cutover 跟 verification
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">Phase 4：Cleanup（self-managed decommission）</span></span></code></pre></div><p>整體 4-12 週、依 dataset 大小跟 organization 流程複雜度。</p>
<h2 id="phase-0pre-migration-audit">Phase 0：Pre-migration audit</h2>
<h3 id="workload-sizing--atlas-tier">Workload sizing → Atlas tier</h3>





<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">Self-managed observations:
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- Peak IOPS: 8000
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- P99 read latency: 5ms
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- Connection count peak: 1500
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- Storage: 800GB
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">- Cross-region replication needed: yes
</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">Atlas tier mapping:
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">- M40 (8 vCPU, 16GB RAM): IOPS 3000、不夠
</span></span><span class="line"><span class="ln">10</span><span class="cl">- M60 (16 vCPU, 64GB RAM): IOPS 6000、邊界
</span></span><span class="line"><span class="ln">11</span><span class="cl">- M80 (32 vCPU, 128GB RAM): IOPS 9000、安全（選此）
</span></span><span class="line"><span class="ln">12</span><span class="cl">- Storage: 1TB tier（足夠 800GB + 25% buffer）
</span></span><span class="line"><span class="ln">13</span><span class="cl">- Cross-region replication add-on</span></span></code></pre></div><p>Atlas 不是 <em>自由 instance class</em>、是 <em>固定 tier</em>；workload 跨 tier 邊界時要選 <em>上一級</em> 而不是 push 下一級。</p>
<h3 id="connection-pattern-audit">Connection pattern audit</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Application connection pool config
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">maxPoolSize</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span>     <span class="c1">// ← Atlas 端 tier-specific connection limit
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">minPoolSize</span><span class="o">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">maxIdleTimeMS</span><span class="o">:</span> <span class="mi">60000</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>Atlas tier 對 <em>single user connection</em> 有限制（M40 ~1500、M80 ~3000）；多 application instance 跑同帳號連 Atlas 可能撞 limit。預先計算 total connection = <code>pod_count × maxPoolSize</code>、對照 tier limit。</p>
<h3 id="compliance-audit">Compliance audit</h3>
<ul>
<li><strong>Data residency</strong>：Atlas 部署 region 是否符合 GDPR / 客戶合約</li>
<li><strong>Encryption at rest</strong>：Atlas 預設 enable、但 <em>encryption key 是 Atlas-managed</em> — 合規嚴格要用 CMK / BYOK</li>
<li><strong>Audit log</strong>：Atlas 提供 audit log、export 到 S3 / Splunk</li>
</ul>
<h2 id="phase-1operational-infrastructure-準備">Phase 1：Operational infrastructure 準備</h2>
<h3 id="atlas-cluster-配置">Atlas cluster 配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 用 Terraform mongodbatlas provider</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="l">resource &#34;mongodbatlas_cluster&#34; &#34;production&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="l">project_id   = var.project_id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="l">name         = &#34;production-cluster&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="l">cluster_type = &#34;REPLICASET&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="l">provider_name         = &#34;AWS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="l">provider_region_name  = &#34;US_EAST_1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="l">provider_instance_size_name = &#34;M80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="l">backup_enabled         = true</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="l">pit_enabled            = true  </span><span class="w"> </span><span class="c"># PITR</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="l">mongo_db_major_version = &#34;7.0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="l">advanced_configuration {</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="l">javascript_enabled                   = false</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="l">minimum_enabled_tls_protocol         = &#34;TLS1_2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="l">no_table_scan                        = false</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="l">oplog_size_mb                        = 51200</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="c"># Backup retention</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="l">resource &#34;mongodbatlas_cloud_backup_schedule&#34; &#34;production&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="l">project_id   = var.project_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="l">cluster_name = mongodbatlas_cluster.production.name</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="l">reference_hour_of_day    = 3</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="l">reference_minute_of_hour = 0</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">  </span><span class="l">restore_window_days      = 7</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="l">policy_item_daily {</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">    </span><span class="l">frequency_interval = 1</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">    </span><span class="l">retention_unit     = &#34;days&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">    </span><span class="l">retention_value    = 7</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w"></span>}</span></span></code></pre></div><h3 id="vpc-peering--private-endpoint">VPC peering / private endpoint</h3>





<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">Pattern A: VPC Peering
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  AWS VPC &lt;──peering──&gt; Atlas project VPC
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  - 跨 region 跑、routing table 對齊
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  - 適合中型 / 大型 workload、stable network topology
</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">Pattern B: Private Endpoint (Atlas private link)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  AWS VPC ──private link──&gt; Atlas
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  - 不需要 routing table 改
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  - 適合 multi-account / multi-region 複雜場景
</span></span><span class="line"><span class="ln">10</span><span class="cl">  - Cost 略高</span></span></code></pre></div><p>production default 走 Private Endpoint、設定簡單跟 IAM 整合好。</p>
<h3 id="atlas-database-user-跟-iam-整合">Atlas Database User 跟 IAM 整合</h3>





<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">Pattern A: 傳統 username / password
</span></span><span class="line"><span class="ln">2</span><span class="cl">  - 設 Database User、application 用 SCRAM-SHA-256 連
</span></span><span class="line"><span class="ln">3</span><span class="cl">  - 適合 legacy application
</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">Pattern B: AWS IAM authentication（推薦）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  - Atlas Database User type: &#34;AWS IAM&#34;
</span></span><span class="line"><span class="ln">7</span><span class="cl">  - Application 用 AWS IAM role + Atlas SDK
</span></span><span class="line"><span class="ln">8</span><span class="cl">  - Token 15 分鐘輪換、application 自管 refresh</span></span></code></pre></div><p>cutover 時間表內加 IAM authentication migration、不要事後補。</p>
<h2 id="phase-2data-migration">Phase 2：Data migration</h2>
<h3 id="atlas-live-migration-tool小到中型">Atlas Live Migration tool（小到中型）</h3>
<p>Atlas UI 內建 Live Migration tool：</p>
<ol>
<li>Source cluster URI（self-managed MongoDB）</li>
<li>Atlas target cluster</li>
<li>tool 自動 full sync + oplog tailing</li>
<li>Cutover window 內 final cutover</li>
</ol>
<p>支援 dataset &lt; 100GB 簡單；100GB-1TB 需要分批 / collection 順序設計。</p>
<h3 id="mongomirror大型">mongomirror（大型）</h3>





<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"># Mongomirror: source → atlas</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mongomirror <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --host source-replicaset/host1:27017,host2:27017 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --destination atlas-cluster-host:27017 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --destinationUsername admin <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --destinationPassword <span class="nv">$ATLAS_PASSWORD</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --ssl</span></span></code></pre></div><p>mongomirror 分兩段：</p>
<ol>
<li>Initial sync（full dump + restore）</li>
<li>Oplog tailing（continuous CDC）</li>
</ol>
<p>Cutover 期間 application 切 connection string、mongomirror 跟著 stream 收尾。</p>
<h2 id="phase-3cutover--verification">Phase 3：Cutover + verification</h2>





<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. Application 端設 maintenance mode（block write）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Wait mongomirror catch up（oplog gap → 0）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 驗證 Atlas 端 collection count + sample query
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Application connection string 切到 Atlas
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 解除 maintenance、monitor 24-48 小時
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Self-managed mongo read-only standby 1-2 週</span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1atlas-tier-connection-limit-撞牆">Case 1：Atlas tier connection limit 撞牆</h3>
<p><strong>徵兆</strong>：cutover 後 application 流量高峰時大量 <code>Connection refused</code>、Atlas 端顯示 connection limit reached；self-managed 階段沒有這問題。</p>
<p><strong>根因</strong>：M80 tier connection limit ~3000、application 100 個 pod × maxPoolSize=50 = 5000 connection；超出 limit。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 計算</strong>：total connection 對照 Atlas tier、超出選上一級 tier</li>
<li><strong>降 maxPoolSize</strong>：100 pod × 30 = 3000、剛好 cap；但 burst 仍可能撞</li>
<li><strong>加 connection proxy</strong>：在 application 跟 Atlas 之間放 connection pooler（如 mongos sharded 或 ProxySQL-style proxy）</li>
</ol>
<h3 id="case-2ip-whitelist-漏-application-vpccutover-後完全連不上">Case 2：IP whitelist 漏 application VPC、cutover 後完全連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 直接報 <code>connection timeout</code>、Atlas dashboard 顯示 zero traffic；troubleshooting 1 小時才發現是 IP access list 漏掉某 application VPC CIDR。</p>
<p><strong>根因</strong>：Atlas IP access list 預設 deny all、必須明示加 application VPC；Phase 1 設定漏看某個 VPC（如 multi-account organization 內的 staging account）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover 連線測試</strong>：每個 application VPC 跑 sample MongoDB 連線、確認 ping 通</li>
<li><strong>改 Private Endpoint</strong>：不靠 IP whitelist、用 PrivateLink 自動 routing</li>
<li><strong>Backup access</strong>：保留 bastion host with whitelisted IP、incident 期間能直連</li>
</ol>
<h3 id="case-3backup-retention-設不夠compliance-audit-抓到">Case 3：Backup retention 設不夠、compliance audit 抓到</h3>
<p><strong>徵兆</strong>：cutover 3 個月後 SOX audit 發現 backup retention 設 7 天、合規要求 90 天；急忙改 Atlas config 設 90 天、但 <em>過去 3 個月 backup 已不可恢復</em>。</p>
<p><strong>根因</strong>：Atlas backup retention 是 <em>向前生效</em>、不能回追加；Phase 1 預設配置漏對合規 review。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-Phase 1 跑 compliance review</strong>：跟 legal / security team 確認 retention / data residency / audit log</li>
<li><strong>預設 retention 設保守值</strong>（30 / 60 天）、之後可降不能升</li>
<li><strong>PITR 跟 backup retention 分開設</strong>：PITR window 7-30 天、full backup 90-365 天</li>
</ol>
<h3 id="case-4iam-token-過期application-端-reconnect-storm">Case 4：IAM token 過期、application 端 reconnect storm</h3>
<p><strong>徵兆</strong>：production 切到 IAM authentication 後、每 15 分鐘出現一波 connection failure；Atlas log 顯示「auth token expired」。</p>
<p><strong>根因</strong>：AWS IAM token 15 分鐘輪換、application 用舊 token 重連失敗；token refresh 邏輯沒寫對。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 用 Atlas SDK + AWS SDK 整合、自動 token refresh
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">MongoClient</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;mongodb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">fromIni</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@aws-sdk/credential-providers&#39;</span><span class="p">);</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="kr">const</span> <span class="nx">credentials</span> <span class="o">=</span> <span class="nx">fromIni</span><span class="p">({</span> <span class="nx">profile</span><span class="o">:</span> <span class="s1">&#39;production&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">authMechanism</span><span class="o">:</span> <span class="s1">&#39;MONGODB-AWS&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="c1">// SDK 自動 refresh token
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>不要自管 token rotation、用 vendor SDK 抽象掉。</p>
<h3 id="case-5billing-暴漲iops-跟-backup-storage-超預估">Case 5：Billing 暴漲、IOPS 跟 backup storage 超預估</h3>
<p><strong>徵兆</strong>：第一個月 Atlas 帳單 $15K USD、預估 $8K；Atlas dashboard 顯示 backup storage 跟 IOPS 各超 1.5-2x 預估。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Atlas backup 預設 <em>跨 region replicated</em>、storage cost 2x</li>
<li>IOPS-heavy workload 在 M tier 內可能撞 burst credit、auto-tier-up 暫時觸發更貴 tier</li>
<li>Data transfer 跨 region / 跨 cloud 計費沒算</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration cost estimate</strong>：用 self-managed metrics 估 IOPS / bandwidth、套 Atlas pricing</li>
<li><strong>Backup region 設單一</strong>：若不要跨 region DR、設 same-region backup 省 50%</li>
<li><strong>Reserved Instance</strong>：穩定 workload 預付 1-3 年、省 30-40%</li>
<li><strong>Performance Advisor 早用</strong>：第一週就跑、找 inefficient query 降 IOPS</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed MongoDB</th>
          <th>Atlas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster cost (M80)</td>
          <td>EC2 r6g.4xlarge × 3 ≈ $1.5K / mo</td>
          <td>M80 + storage + backup ≈ $3K / mo</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 FTE</td>
          <td>0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Backup cost</td>
          <td>S3 + tooling 自管</td>
          <td>內建 + tiered storage</td>
      </tr>
      <tr>
          <td>Cross-region DR cost</td>
          <td>Manual + 2x infrastructure</td>
          <td>1-click + 1.5-2x billing</td>
      </tr>
      <tr>
          <td>Time to value</td>
          <td>1-3 個月（HA + ops setup）</td>
          <td>1-2 週（cluster ready + IAM）</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 2-3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>Break-even</strong>：~200GB / 中型 workload、Atlas operational savings 平攤 1-2 年後比 self-managed cheaper；TB+ 大型 workload self-managed 仍可能便宜、但需要 ops team。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql--aurora-migration-對照">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora migration</a> 對照</h3>
<p>兩篇都是 Type C operational redesign hybrid、模板共用、細節差：</p>
<ul>
<li>Aurora 端 RDS Proxy 是推薦做法、Atlas 端 Private Endpoint 更標準</li>
<li>Aurora 端 IAM authentication 是 <em>optional best practice</em>、Atlas IAM 是 <em>推薦預設</em></li>
<li>兩家 cost model 都複雜、I/O cost 是 surprise 主要來源</li>
</ul>
<h3 id="跟-application-端-iam-token-rotation-整合">跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Application 端 IAM token rotation</a> 整合</h3>
<p>Vault dynamic credential 可 issue Atlas Database User credential、lease lifecycle 對齊 application；對 high-stakes workload 是好做法、但 setup 複雜。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Atlas Data Federation</strong>：跨 Atlas 集群 query S3 / 跨 region；如果走 multi-region 評估這 feature</li>
<li><strong>Atlas Online Archive</strong>：cold data 自動 archive 到 S3、查 query 透明；對 retention 重的 workload 省 storage cost</li>
<li><strong>Atlas Serverless</strong>：burst workload 適合、steady 不划算</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a>（Type A schema 差） / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a>（Type E paradigm shift）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 Type C 標準形態）</li>
</ul>
]]></content:encoded></item><item><title>MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a>。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type A 的標準形態實證。&lt;/p>&lt;/blockquote>
&lt;h2 id="三類-sql-dialect-diff-sample先看具體差距">三類 SQL dialect diff sample：先看具體差距&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 1. Auto increment / sequence
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AUTO_INCREMENT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- PostgreSQL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SERIAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 或 PG 10+:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GENERATED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ALWAYS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IDENTITY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 2. String concatenation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL: CONCAT(a, b) 或 a || b 在 ANSI mode
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CONCAT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">first_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">last_name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- PostgreSQL: a || b 或 CONCAT(a, b)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">first_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">last_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 注意: PostgreSQL 對 NULL || x = NULL、MySQL CONCAT 對 NULL 處理不同
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 3. UPSERT
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Alice&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DUPLICATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UPDATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- PostgreSQL (9.5+)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Alice&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CONFLICT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UPDATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXCLUDED&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 4. Index hint / FORCE INDEX
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FORCE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">idx_created_at&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2025-01-01&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- PostgreSQL: 沒對應 syntax、依賴 planner + statistics
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 必要時用 enable_seqscan=off 或 pg_hint_plan extension
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 5. JSON path
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL 5.7+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="s1">&amp;#39;$.name&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- PostgreSQL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">data&lt;/span>&lt;span class="o">-&amp;gt;&amp;gt;&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 取出 text&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>5 個 sample 看出 MySQL → PostgreSQL 主要工作是 &lt;em>SQL dialect translation&lt;/em>；不是 5-10 個函數差、是 &lt;em>跨整個 application SQL surface 的 audit + 改寫&lt;/em>。對應 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 結果：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> 跟 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type A 的標準形態實證。</p></blockquote>
<h2 id="三類-sql-dialect-diff-sample先看具體差距">三類 SQL dialect diff sample：先看具體差距</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1. Auto increment / sequence
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">-- MySQL
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- 或 PG 10+:
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="w"> </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">IDENTITY</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- 2. String concatenation
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- MySQL: CONCAT(a, b) 或 a || b 在 ANSI mode
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">CONCAT</span><span class="p">(</span><span class="n">first_name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="p">,</span><span class="w"> </span><span class="n">last_name</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL: a || b 或 CONCAT(a, b)
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">first_name</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">last_name</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- 注意: PostgreSQL 對 NULL || x = NULL、MySQL CONCAT 對 NULL 處理不同
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="c1">-- 3. UPSERT
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">-- MySQL
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Alice&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">DUPLICATE</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">VALUES</span><span class="p">(</span><span class="n">name</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL (9.5+)
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Alice&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">CONFLICT</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="w"> </span><span class="k">DO</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">EXCLUDED</span><span class="p">.</span><span class="n">name</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="c1">-- 4. Index hint / FORCE INDEX
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1">-- MySQL
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">FORCE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">(</span><span class="n">idx_created_at</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2025-01-01&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL: 沒對應 syntax、依賴 planner + statistics
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1">-- 必要時用 enable_seqscan=off 或 pg_hint_plan extension
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w"></span><span class="c1">-- 5. JSON path
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1">-- MySQL 5.7+
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">data</span><span class="o">-&gt;</span><span class="s1">&#39;$.name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">data</span><span class="o">-&gt;</span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 取出 text</span></span></span></code></pre></div><p>5 個 sample 看出 MySQL → PostgreSQL 主要工作是 <em>SQL dialect translation</em>；不是 5-10 個函數差、是 <em>跨整個 application SQL surface 的 audit + 改寫</em>。對應 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 結果：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>SQL dialect 差大、CREATE TABLE / INDEX / function 都差</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>兩者都 OLTP RDBMS、replication 概念對等但語法不同</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 SQL RDBMS</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>ORM 多數能 cover、raw SQL 必改</td>
          <td>Medium</td>
      </tr>
  </tbody>
</table>
<p>主導維度 Schema = High、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Type A 6-phase playbook</a> 標準結構。</p>
<h2 id="phase-0rule-audit--sql-surface-盤點">Phase 0：rule audit + SQL surface 盤點</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1. 列所有 stored procedure
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">routine_schema</span><span class="p">,</span><span class="w"> </span><span class="k">routine_name</span><span class="p">,</span><span class="w"> </span><span class="n">routine_type</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">routines</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">routine_schema</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;mysql&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;sys&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;information_schema&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;performance_schema&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- 2. 列所有 trigger
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">trigger_name</span><span class="p">,</span><span class="w"> </span><span class="n">event_object_table</span><span class="p">,</span><span class="w"> </span><span class="n">action_statement</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">triggers</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 3. 列所有 view
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">view_definition</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">views</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- 4. 列所有 index 含 prefix length
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="c1">-- PostgreSQL 對 prefix index 處理不同、要逐個 audit</span></span></span></code></pre></div><p>Audit 主要產出三類清單：</p>
<ul>
<li><strong>Direct port</strong>：標準 SQL feature、PG 直接接受</li>
<li><strong>Translate</strong>：MySQL-specific syntax、需要改寫（UPSERT / CONCAT NULL 行為 / index hint）</li>
<li><strong>Refactor</strong>：MySQL-specific behavior（auto_increment session-level / SELECT FOUND_ROWS / GROUP BY 寬鬆 / TEXT 隱性 cast）— 不能直接 port、application code 也要改</li>
</ul>
<h2 id="phase-1schema-對位">Phase 1：schema 對位</h2>
<table>
  <thead>
      <tr>
          <th>MySQL</th>
          <th>PostgreSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>INT AUTO_INCREMENT</code></td>
          <td><code>INT GENERATED ALWAYS AS IDENTITY</code> 或 <code>SERIAL</code></td>
      </tr>
      <tr>
          <td><code>TINYINT(1)</code> (boolean usage)</td>
          <td><code>BOOLEAN</code></td>
      </tr>
      <tr>
          <td><code>DATETIME</code></td>
          <td><code>TIMESTAMP WITHOUT TIME ZONE</code></td>
      </tr>
      <tr>
          <td><code>DATETIME(6)</code> (microsecond)</td>
          <td><code>TIMESTAMP(6)</code></td>
      </tr>
      <tr>
          <td><code>VARCHAR(N)</code> with charset</td>
          <td><code>VARCHAR(N)</code> (UTF-8 always)</td>
      </tr>
      <tr>
          <td><code>TEXT</code></td>
          <td><code>TEXT</code> (no length limit)</td>
      </tr>
      <tr>
          <td><code>LONGTEXT</code></td>
          <td><code>TEXT</code></td>
      </tr>
      <tr>
          <td><code>JSON</code></td>
          <td><code>JSONB</code> (推薦、indexed) 或 <code>JSON</code></td>
      </tr>
      <tr>
          <td><code>ENUM('a','b','c')</code></td>
          <td>自定 <code>TYPE foo AS ENUM('a','b','c')</code> 或 <code>VARCHAR + CHECK</code></td>
      </tr>
      <tr>
          <td><code>SET('a','b')</code></td>
          <td>Array <code>TEXT[]</code> + CHECK</td>
      </tr>
      <tr>
          <td><code>BINARY(N)</code></td>
          <td><code>BYTEA</code></td>
      </tr>
      <tr>
          <td>Index prefix <code>KEY (col(10))</code></td>
          <td>Functional index <code>CREATE INDEX ON t (LEFT(col, 10))</code></td>
      </tr>
      <tr>
          <td><code>FULLTEXT INDEX</code></td>
          <td><code>tsvector</code> + GIN index</td>
      </tr>
      <tr>
          <td>Geographic types</td>
          <td>PostGIS extension（必須先裝）</td>
      </tr>
  </tbody>
</table>
<p>Schema 對位表存版控、application code refactor 時對照。</p>
<h2 id="phase-2translation-pipeline3-tier-跟-splunk--elastic-類似">Phase 2：Translation pipeline（3-tier 跟 Splunk → Elastic 類似）</h2>
<h3 id="tier-1vendor--community-tool">Tier 1：vendor / community tool</h3>





<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"># pgloader：成熟工具、cover ~70-80% schema + data</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgloader mysql://user:pass@mysql-host/dbname <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>         postgresql://user:pass@pg-host/dbname
</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="c1"># 或 AWS DMS（managed、適合 RDS / Aurora target）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># DMS task: Full Load + CDC</span></span></span></code></pre></div><h3 id="tier-2自家-sql-refactor">Tier 2：自家 SQL refactor</h3>
<p>對 ORM 不能 cover 的 raw SQL：</p>
<ul>
<li>Manual grep <code>application code</code> 找 <code>auto_increment</code> / <code>ON DUPLICATE KEY</code> / <code>FORCE INDEX</code> / <code>FOUND_ROWS()</code> / <code>CONCAT NULL</code></li>
<li>寫 codemod / lint rule、CI 強制 check（PG-incompatible SQL block PR）</li>
</ul>
<h3 id="tier-3tricky-case-manual">Tier 3：tricky case manual</h3>
<p>例：MySQL <code>SELECT * FROM t1, t2 WHERE t1.id = t2.id GROUP BY t1.id</code>（implicit GROUP BY 寬鬆）— PG 嚴格 GROUP BY 必須 list 所有 non-aggregate column；application code refactor 必要。</p>
<h2 id="phase-3parallel-run">Phase 3：Parallel run</h2>
<p>雙寫 + 雙讀比對 1-2 個月：</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">Application ──→ MySQL (write + read primary)
</span></span><span class="line"><span class="ln">2</span><span class="cl">            └─→ PostgreSQL (write only + read shadow)
</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">                            Diff checker (latency / result diff)</span></span></code></pre></div><p><code>pt-table-checksum</code> (MySQL) + 自家 checksum scanner 對 sample table 跑 daily checksum、找 schema 對位錯。</p>
<h2 id="phase-4cutover">Phase 4：Cutover</h2>
<ul>
<li>設 application maintenance window（30 分鐘）</li>
<li>Drain MySQL write、等 last LSN propagated to PG</li>
<li>Application switch connection string → PG</li>
<li>解除 maintenance、monitor 24-48 hours</li>
</ul>
<h2 id="phase-5cleanup">Phase 5：Cleanup</h2>
<ul>
<li>MySQL read-only 1-2 週（fallback window）</li>
<li>之後 stop replication、decommission MySQL</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1auto_increment-vs-serial-跨-transaction-行為差">Case 1：Auto_increment vs SERIAL 跨 transaction 行為差</h3>
<p><strong>徵兆</strong>：cutover 後某 batch job 跑得比 MySQL 慢 5-10x、PG log 顯示 sequence 競爭。</p>
<p><strong>根因</strong>：MySQL <code>AUTO_INCREMENT</code> 取值受 <code>innodb_autoinc_lock_mode</code> 控制（8.0 預設 mode=2 interleaved 可並行、mode=0 才是 table-level lock；詳見 <a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">Lock contention</a>）、PG <code>SERIAL</code> 是 <em>sequence-level non-transactional</em>；mode=0 場景跟 PG SERIAL 差異最大、mode=2 跟 PG SERIAL 行為較接近（皆可亂號、皆可並行）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>改 UUID v7 / bigserial</strong>：消除 sequence 競爭</li>
<li><strong>bigserial + cache</strong>：<code>CREATE SEQUENCE ... CACHE 100</code>、batch 預取 100 個 ID 降 contention</li>
<li><strong>批量 insert 改 COPY</strong>：<code>COPY t FROM STDIN</code> 是 PG 對 batch 最快路徑</li>
</ol>
<h3 id="case-2charset--collation-跑出-unicode-異常">Case 2：Charset / collation 跑出 unicode 異常</h3>
<p><strong>徵兆</strong>：cutover 後某些用戶名 / 中文文字 query 對不到結果、<code>SELECT * WHERE name = '張三'</code> 返回空。</p>
<p><strong>根因</strong>：MySQL default <code>utf8mb3</code>（3-byte UTF-8、不能存 emoji / 部分 unicode）、PG default <code>UTF8</code> 全 unicode；資料遷移時 MySQL 端的 utf8mb3 column 帶到 PG 後 <em>bytes 不變</em> 但 <em>collation rule 變</em>；string comparison 結果差。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit</strong>：MySQL 強制 <code>utf8mb4</code>、avoid utf8mb3 data</li>
<li><strong>Collation 對位</strong>：MySQL <code>utf8mb4_unicode_ci</code> → PG <code>LC_COLLATE = 'C.utf8'</code> 或 ICU collation</li>
<li><strong>Application encoding contract</strong>：明示 UTF-8 全範圍、不接受 utf8mb3-only client</li>
</ol>
<h3 id="case-3case-sensitivity-反轉">Case 3：Case sensitivity 反轉</h3>
<p><strong>徵兆</strong>：cutover 後 application query <code>SELECT * FROM users</code> 報錯 <code>relation does not exist</code>；但 <code>SELECT * FROM &quot;Users&quot;</code> works。</p>
<p><strong>根因</strong>：MySQL Linux default <em>table name case-sensitive</em>、Windows <em>case-insensitive</em>、配置 <code>lower_case_table_names</code> 影響；PG <em>all identifier folded to lowercase unless quoted</em>。MySQL on macOS 開發環境是 case-insensitive、PG 嚴格 case-sensitive、application code 端可能用 mixed case。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Schema migration 階段強制 lowercase</strong>：所有 table / column name 統一 lowercase</li>
<li><strong>Application code refactor</strong>：grep raw SQL 找 mixed case identifier、改 lowercase</li>
<li><strong>ORM 端設定 <code>naming_strategy</code></strong>：JPA / Hibernate 等明示 lowercase mapping</li>
</ol>
<h3 id="case-4replication-行為差cdc-pipeline-失效">Case 4：Replication 行為差、CDC pipeline 失效</h3>
<p><strong>徵兆</strong>：MySQL 端 binlog-based CDC（Debezium MySQL connector）跑得好好的、cutover 後 PG 端要重建 CDC pipeline、初期 1-2 週 message 模式異常。</p>
<p><strong>根因</strong>：MySQL binlog row format vs PG logical replication slot 完全不同 protocol；Debezium 對兩家連接器是 <em>獨立</em> binary、message schema 部分對等但不直通。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover 建 PG 端 CDC</strong>：Debezium PG connector 提前部署、初期跟 MySQL CDC 並存比對</li>
<li><strong>Schema registry 同步</strong>：Avro schema 從 MySQL 端 export、註冊 PG 端 connector 用同 schema</li>
<li><strong>Consumer 端 idempotent</strong>：cutover 期間 dual-source、consumer 必須 idempotent 避免 duplicate</li>
</ol>
<h3 id="case-5fulltext-index-對應-tsvectorapplication-search-broken">Case 5：FULLTEXT INDEX 對應 tsvector、application search broken</h3>
<p><strong>徵兆</strong>：cutover 後 application 全文搜尋功能失效、<code>MATCH(name) AGAINST('xxx')</code> 不被 PG 認；application 端 raw SQL 對 search 寫死。</p>
<p><strong>根因</strong>：MySQL <code>FULLTEXT INDEX</code> + <code>MATCH ... AGAINST</code> syntax PG 不支援；PG 用 <code>tsvector + ts_rank + to_tsquery</code>、概念對等但 syntax 完全不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration</strong>：列 application 用到的 fulltext search 場景、改寫成 tsvector pattern</li>
<li><strong>大型 search 改 Elasticsearch / Meilisearch</strong>：fulltext 是專門 search engine 的本職、不該用 RDBMS 解</li>
<li><strong>降級為 LIKE</strong>：簡單 case <code>WHERE name ILIKE '%xxx%'</code>、performance 較差但相容性好</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL</th>
          <th>PostgreSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Instance cost</td>
          <td>對等（同 EC2 / RDS spec）</td>
          <td>對等</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>對等</td>
          <td>對等</td>
      </tr>
      <tr>
          <td>Connection pooling</td>
          <td>proxysql / mysql-proxy</td>
          <td>PgBouncer（更成熟）</td>
      </tr>
      <tr>
          <td>Index performance</td>
          <td>對等</td>
          <td>對等</td>
      </tr>
      <tr>
          <td>JSON performance</td>
          <td>Improving</td>
          <td>JSONB 領先</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Async binlog</td>
          <td>Async streaming + logical</td>
      </tr>
      <tr>
          <td>Extension ecosystem</td>
          <td>少</td>
          <td>大（PostGIS / TimescaleDB / pgvector）</td>
      </tr>
      <tr>
          <td>Migration cost (one-time)</td>
          <td>-</td>
          <td>2-6 FTE 月 × project length（含 application）</td>
      </tr>
  </tbody>
</table>
<p>Migration 主要 cost 在 <em>application code refactor + dual-write window operational</em>、不是 DB itself。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql--aurora-migration-串接">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora migration</a> 串接</h3>
<p>部分組織走 <em>MySQL → PostgreSQL → Aurora</em> 兩段：</p>
<ul>
<li>先 MySQL → self-managed PostgreSQL（schema 對位 + application 改）</li>
<li>穩定後 self-managed PostgreSQL → Aurora（operational simplification）</li>
</ul>
<p>不要一次跑 <em>MySQL → Aurora PostgreSQL compat</em>、認知負擔太大、failure mode 互相干擾。</p>
<h3 id="跟-logical-replication--debezium-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a> 對位</h3>
<p>PG 端 CDC pipeline 在 cutover 完成後立刻可用；可作為 <em>downstream CDC 重建</em> 的契機、設計 outbox pattern 更穩。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>MySQL 8 vs PostgreSQL 16 feature gap</strong>：MySQL 8 加了 CTE / window function / generated column；2025+ feature parity 漸高、migration ROI 評估會變</li>
<li><strong>Reverse migration</strong>（PG → MySQL）：少見、通常是 application 端 dependency lock-in（用了 MySQL-specific stored procedure）</li>
<li><strong>MariaDB → PostgreSQL</strong>：跟 MySQL → PG 類似、MariaDB 部分 syntax 略接近 PG（如 <code>RETURNING</code>）</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> / <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>後續路線：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a>（同為 Type A 高 schema 差）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 Type A 標準形態）</li>
</ul>
]]></content:encoded></item><item><title>New Relic → Datadog：APM schema 對位 + agent 替換 + dashboard 重建</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-from-new-relic/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-from-new-relic/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://newrelic.com/">New Relic&lt;/a> 跟 &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;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Schema = High（NRQL ↔ Datadog query、APM agent 不同）→ Type A phased translation&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>中型 SaaS 跑 New Relic 3-5 年、production observability 飽和、團隊發現幾個問題：cost 暴漲（per-host APM + custom event + synthetic）、APM trace 對 Kubernetes-native workload 不夠細、跟 PagerDuty / Slack integration 雖然有但 latency 偏高。同期 Datadog 在 K8s monitoring + APM 端深度整合、cost model 在 100-500 host 規模更可預測。&lt;/p>
&lt;p>評估遷移時、發現 New Relic → Datadog 不是「換個 agent 就好」 — APM schema、NRQL 查詢語言、custom dashboard、synthetic monitoring rule 全部要 &lt;em>重新對位&lt;/em>；application code 端的 agent 也要 &lt;em>完全換 binary&lt;/em>。是 Type A 高 schema 差 migration、不是 drop-in。&lt;/p>
&lt;h2 id="為什麼遷cost--k8s-native--vendor-consolidation-三條-driver">為什麼遷：cost / k8s-native / vendor consolidation 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Cost&lt;/strong>&lt;/td>
 &lt;td>New Relic per-host pricing + custom event + synthetic 加總爆、Datadog 在 K8s 場景單 host 多 container 更划算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>K8s-native&lt;/strong>&lt;/td>
 &lt;td>Datadog agent 對 K8s sidecar / DaemonSet / autodiscovery 更深&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Vendor consolidation&lt;/strong>&lt;/td>
 &lt;td>已用 Datadog log / metric、APM 統一 vendor 降工具切換 cost&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（Datadog → New Relic）：&lt;/p>
&lt;ul>
&lt;li>New Relic 對 &lt;em>full-stack observability&lt;/em>（APM + browser + mobile + synthetic）的整合包仍領先&lt;/li>
&lt;li>已深用 New Relic NRQL 跟 New Relic University 培訓的 organization、不切&lt;/li>
&lt;/ul>
&lt;h2 id="schema-對位">Schema 對位&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>New Relic concept&lt;/th>
 &lt;th>Datadog 對應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>APM agent (NR Java / Python / Node)&lt;/td>
 &lt;td>Datadog agent + APM tracer library&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NRQL query&lt;/td>
 &lt;td>Datadog query (Metric / Log / Trace)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Synthetic monitor&lt;/td>
 &lt;td>Datadog Synthetic Tests&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Custom event&lt;/td>
 &lt;td>Datadog custom metric / log event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NRQL alert condition&lt;/td>
 &lt;td>Datadog monitor&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>New Relic dashboard&lt;/td>
 &lt;td>Datadog dashboard (need rebuild)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apdex score&lt;/td>
 &lt;td>Datadog APM &lt;code>apm.service.errors&lt;/code> + &lt;code>apm.service.latency&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distributed trace&lt;/td>
 &lt;td>Datadog APM trace（OpenTelemetry-compatible）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="phase-0audit--classify">Phase 0：Audit + classify&lt;/h2>
&lt;ul>
&lt;li>列所有 application 跟對應 NR agent version&lt;/li>
&lt;li>列所有 NRQL alert / dashboard / synthetic monitor&lt;/li>
&lt;li>估每月 cost 跟 Datadog 對比&lt;/li>
&lt;/ul>
&lt;h2 id="phase-1schema-對位--datadog-cluster-建置">Phase 1：Schema 對位 + Datadog cluster 建置&lt;/h2>
&lt;ul>
&lt;li>Datadog organization 申請 / IAM integration&lt;/li>
&lt;li>VPC peering / private link (如果用 self-hosted agent)&lt;/li>
&lt;/ul>
&lt;h2 id="phase-2translation-pipeline-3-tier">Phase 2：Translation pipeline (3-tier)&lt;/h2>
&lt;ul>
&lt;li>Tier 1: Datadog 端 import tool（API-based NRQL → Datadog query 轉換、cover ~40-60%）&lt;/li>
&lt;li>Tier 2: LLM-assisted（剩餘 query / dashboard）&lt;/li>
&lt;li>Tier 3: manual (synthetic / complex correlation)&lt;/li>
&lt;/ul>
&lt;h2 id="phase-3parallel-run-dual-agent-4-8-週">Phase 3：Parallel run (dual-agent 4-8 週)&lt;/h2>
&lt;p>兩個 agent 跑同 application、metric / trace / log 雙端輸出、SOC 比對 detection coverage / alert / dashboard 一致性。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="https://newrelic.com/">New Relic</a> 跟 <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>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Schema = High（NRQL ↔ Datadog query、APM agent 不同）→ Type A phased translation</em>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>中型 SaaS 跑 New Relic 3-5 年、production observability 飽和、團隊發現幾個問題：cost 暴漲（per-host APM + custom event + synthetic）、APM trace 對 Kubernetes-native workload 不夠細、跟 PagerDuty / Slack integration 雖然有但 latency 偏高。同期 Datadog 在 K8s monitoring + APM 端深度整合、cost model 在 100-500 host 規模更可預測。</p>
<p>評估遷移時、發現 New Relic → Datadog 不是「換個 agent 就好」 — APM schema、NRQL 查詢語言、custom dashboard、synthetic monitoring rule 全部要 <em>重新對位</em>；application code 端的 agent 也要 <em>完全換 binary</em>。是 Type A 高 schema 差 migration、不是 drop-in。</p>
<h2 id="為什麼遷cost--k8s-native--vendor-consolidation-三條-driver">為什麼遷：cost / k8s-native / vendor consolidation 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Cost</strong></td>
          <td>New Relic per-host pricing + custom event + synthetic 加總爆、Datadog 在 K8s 場景單 host 多 container 更划算</td>
      </tr>
      <tr>
          <td><strong>K8s-native</strong></td>
          <td>Datadog agent 對 K8s sidecar / DaemonSet / autodiscovery 更深</td>
      </tr>
      <tr>
          <td><strong>Vendor consolidation</strong></td>
          <td>已用 Datadog log / metric、APM 統一 vendor 降工具切換 cost</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Datadog → New Relic）：</p>
<ul>
<li>New Relic 對 <em>full-stack observability</em>（APM + browser + mobile + synthetic）的整合包仍領先</li>
<li>已深用 New Relic NRQL 跟 New Relic University 培訓的 organization、不切</li>
</ul>
<h2 id="schema-對位">Schema 對位</h2>
<table>
  <thead>
      <tr>
          <th>New Relic concept</th>
          <th>Datadog 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>APM agent (NR Java / Python / Node)</td>
          <td>Datadog agent + APM tracer library</td>
      </tr>
      <tr>
          <td>NRQL query</td>
          <td>Datadog query (Metric / Log / Trace)</td>
      </tr>
      <tr>
          <td>Synthetic monitor</td>
          <td>Datadog Synthetic Tests</td>
      </tr>
      <tr>
          <td>Custom event</td>
          <td>Datadog custom metric / log event</td>
      </tr>
      <tr>
          <td>NRQL alert condition</td>
          <td>Datadog monitor</td>
      </tr>
      <tr>
          <td>New Relic dashboard</td>
          <td>Datadog dashboard (need rebuild)</td>
      </tr>
      <tr>
          <td>Apdex score</td>
          <td>Datadog APM <code>apm.service.errors</code> + <code>apm.service.latency</code></td>
      </tr>
      <tr>
          <td>Distributed trace</td>
          <td>Datadog APM trace（OpenTelemetry-compatible）</td>
      </tr>
  </tbody>
</table>
<h2 id="phase-0audit--classify">Phase 0：Audit + classify</h2>
<ul>
<li>列所有 application 跟對應 NR agent version</li>
<li>列所有 NRQL alert / dashboard / synthetic monitor</li>
<li>估每月 cost 跟 Datadog 對比</li>
</ul>
<h2 id="phase-1schema-對位--datadog-cluster-建置">Phase 1：Schema 對位 + Datadog cluster 建置</h2>
<ul>
<li>Datadog organization 申請 / IAM integration</li>
<li>VPC peering / private link (如果用 self-hosted agent)</li>
</ul>
<h2 id="phase-2translation-pipeline-3-tier">Phase 2：Translation pipeline (3-tier)</h2>
<ul>
<li>Tier 1: Datadog 端 import tool（API-based NRQL → Datadog query 轉換、cover ~40-60%）</li>
<li>Tier 2: LLM-assisted（剩餘 query / dashboard）</li>
<li>Tier 3: manual (synthetic / complex correlation)</li>
</ul>
<h2 id="phase-3parallel-run-dual-agent-4-8-週">Phase 3：Parallel run (dual-agent 4-8 週)</h2>
<p>兩個 agent 跑同 application、metric / trace / log 雙端輸出、SOC 比對 detection coverage / alert / dashboard 一致性。</p>
<h2 id="phase-4cutover--cleanup">Phase 4：Cutover + cleanup</h2>
<ul>
<li>Application 端切 agent</li>
<li>New Relic license downgrade / cancel</li>
<li>Decommission timeline 3-6 個月（保留歷史查詢能力）</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1nrql-不直接對位-datadog-query">Case 1：NRQL 不直接對位 Datadog query</h3>
<p><strong>徵兆</strong>：NRQL <code>SELECT count(*) FROM Transaction FACET name WHERE duration &gt; 5 SINCE 1 hour ago</code> 在 Datadog 端需要拆 metric query + filter + group by；翻譯後語意對等但 syntax 完全不同、SOC analyst 學習曲線陡。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>翻譯腳本 + LLM-assisted、保留 NRQL 字面 + Datadog query 對照表（runbook）</li>
<li>SOC training，1-2 週 hands-on</li>
<li>部分 query 改 <em>Datadog dashboard widget</em>、不用直接 query</li>
</ol>
<h3 id="case-2synthetic-monitor-對位失敗">Case 2：Synthetic monitor 對位失敗</h3>
<p><strong>徵兆</strong>：NR Synthetic 跑 100+ ping / browser / API test、切 Datadog Synthetic 後發現 <em>step-based</em> monitor 對應的「Browser Test」配置複雜、setup 工作量 2-3 倍預估。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 跑 sample synthetic、估真實 setup cost</li>
<li>優先遷 critical synthetic、其他評估退役</li>
<li>用 Datadog API + Terraform 自動化、避免 UI 手動建</li>
</ol>
<h3 id="case-3cost-模型反轉">Case 3：Cost 模型反轉</h3>
<p><strong>徵兆</strong>：cutover 後第一個月 Datadog 帳單比 NR 高 30%；breakdown 後發現 <em>log retention + custom metric series + log indexing</em> 三個項目超預估。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-migration 估 Datadog cost 必須含 <em>log indexing pricing</em>（按 indexed event 計）、不是純 ingest</li>
<li>Application 端 log scrub PII + sample debug log、降 ingest GB</li>
<li>Custom metric cardinality control（tag combination 爆 series count）</li>
</ol>
<h3 id="case-4dashboard-自動轉失敗人工-rebuild-80">Case 4：Dashboard 自動轉失敗、人工 rebuild 80%</h3>
<p><strong>徵兆</strong>：用 Datadog import tool 跑 NR dashboard、80% widget 缺 / 對應錯；team 估 2 週 dashboard rebuild、實際跑 6-8 週。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受重建</strong>：production dashboard 必須人工重建、不要期待自動轉</li>
<li><strong>Prioritize</strong>：先重建 SOC critical 30%、其他 deprecate</li>
<li><strong>Migration window 增 4-6 週</strong>：dashboard rebuild 是 underestimated effort</li>
</ol>
<h3 id="case-5cross-platform-metric-命名差">Case 5：Cross-platform metric 命名差</h3>
<p><strong>徵兆</strong>：NR 端 metric <code>Apdex/Apdex</code> 在 Datadog 沒對應、application code 寫死 metric name 失效；alert query 對 NR-specific metric 全失效。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 列所有 NR-specific metric、application code 改用 OpenTelemetry-style metric 命名</li>
<li>Datadog query 端 rebuild、用 application-level metric name 而非 vendor-specific</li>
<li>長期：metric naming 用 OpenTelemetry semantic conventions、避免 vendor lock</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>New Relic</th>
          <th>Datadog</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pricing model</td>
          <td>per-host + custom event / synthetic</td>
          <td>per-host APM + log indexing + custom metric</td>
      </tr>
      <tr>
          <td>K8s-friendly</td>
          <td>中、autodiscovery 有但配置複雜</td>
          <td>高、K8s-native autodiscovery first-class</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>2-4 FTE × 2-3 個月</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.6</td>
          <td>0.3-0.6（相當）</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-datadog--grafana-stack-migration-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack migration</a> 對位</h3>
<p>兩種 Datadog 端的後續路線：</p>
<ul>
<li>切到 Datadog 後 <em>繼續用</em>（穩定 multi-year）</li>
<li>切到 Datadog 後 <em>再切 Grafana Stack</em> 省 cost（multi-tool 拆分、Type D）</li>
</ul>
<p>多數 organization 第一輪 NR → Datadog 已花 2-3 個月、不會立刻再切；至少穩定 1-2 年。</p>
<h3 id="跟-opentelemetry-對齊">跟 OpenTelemetry 對齊</h3>
<p>Migration 順便升 OTel 化 application、避免下次 vendor 切換重複工作量。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<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></li>
<li>平行 migration playbook (Type A)：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a></li>
<li>平行 migration playbook (D-type 對位)：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Self-managed Prometheus → Grafana Cloud Metrics：feature × ops × cost 對照</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>（Grafana Cloud Metrics、Mimir-backed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High → Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Self-managed Prometheus&lt;/th>
 &lt;th>Grafana Cloud Metrics&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Storage backend&lt;/td>
 &lt;td>Local disk + remote_write (optional)&lt;/td>
 &lt;td>Mimir + S3 (auto cold tier)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>TSDB local 15 天 default&lt;/td>
 &lt;td>13 個月 default、可延長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA&lt;/td>
 &lt;td>Two Prometheus + sidecar&lt;/td>
 &lt;td>Built-in multi-AZ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cardinality limit&lt;/td>
 &lt;td>自管 limit + recording rule&lt;/td>
 &lt;td>1.5M active series / tier、scale-up 配額&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query API&lt;/td>
 &lt;td>PromQL + Prometheus HTTP API&lt;/td>
 &lt;td>完全相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>Alertmanager self-managed&lt;/td>
 &lt;td>Grafana Cloud Alerting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>Grafana self-managed&lt;/td>
 &lt;td>Grafana Cloud (included)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term storage&lt;/td>
 &lt;td>Thanos / Cortex / Mimir 自管&lt;/td>
 &lt;td>Mimir 內建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost (mid-tier)&lt;/td>
 &lt;td>$500-2000 / mo + ops FTE&lt;/td>
 &lt;td>$300-1500 / mo (按 series)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational FTE&lt;/td>
 &lt;td>0.3-0.8&lt;/td>
 &lt;td>0.05-0.15&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&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>Schema / API&lt;/td>
 &lt;td>Low（PromQL + API 完全相容）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>（HA / retention / scaling 全託管）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Low（同 Prometheus metric paradigm）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Low（remote_write endpoint 改）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational = High → Type C standard。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 跟 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Grafana Cloud Metrics、Mimir-backed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High → Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Prometheus</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage backend</td>
          <td>Local disk + remote_write (optional)</td>
          <td>Mimir + S3 (auto cold tier)</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>TSDB local 15 天 default</td>
          <td>13 個月 default、可延長</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus + sidecar</td>
          <td>Built-in multi-AZ</td>
      </tr>
      <tr>
          <td>Cardinality limit</td>
          <td>自管 limit + recording rule</td>
          <td>1.5M active series / tier、scale-up 配額</td>
      </tr>
      <tr>
          <td>Query API</td>
          <td>PromQL + Prometheus HTTP API</td>
          <td>完全相容</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>Alertmanager self-managed</td>
          <td>Grafana Cloud Alerting</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-managed</td>
          <td>Grafana Cloud (included)</td>
      </tr>
      <tr>
          <td>Long-term storage</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Mimir 內建</td>
      </tr>
      <tr>
          <td>Cost (mid-tier)</td>
          <td>$500-2000 / mo + ops FTE</td>
          <td>$300-1500 / mo (按 series)</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.05-0.15</td>
      </tr>
  </tbody>
</table>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low（PromQL + API 完全相容）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td><strong>High</strong>（HA / retention / scaling 全託管）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low（同 Prometheus metric paradigm）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low（remote_write endpoint 改）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High → Type C standard。</p>
<h2 id="為什麼遷retention--ops--vendor-consolidation-三條-driver">為什麼遷：retention / ops / vendor consolidation 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Retention</td>
          <td>Prometheus TSDB local 預設 15 天、長期 retention 需要 Thanos / Cortex / Mimir 自管</td>
      </tr>
      <tr>
          <td>Ops FTE</td>
          <td>Self-managed Prometheus + Alertmanager + Grafana 自管全部加起來 0.5-1 FTE</td>
      </tr>
      <tr>
          <td>Vendor consolidation</td>
          <td>已用 Grafana Cloud（logs / traces）、metric 加進 stack 統一</td>
      </tr>
  </tbody>
</table>
<h2 id="operational-redesign">Operational redesign</h2>
<table>
  <thead>
      <tr>
          <th>Concept</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>Helm chart + manual config</td>
          <td>UI 一鍵建</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus 配置</td>
          <td>內建 multi-AZ Mimir</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in (S3-backed)</td>
      </tr>
      <tr>
          <td>Cardinality control</td>
          <td>Manual recording rule + relabel</td>
          <td>Adaptive sampling + cardinality limit</td>
      </tr>
      <tr>
          <td>Alerting</td>
          <td>Alertmanager 自管</td>
          <td>Grafana Cloud Alerting (integrated)</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-host</td>
          <td>Grafana Cloud (free tier 包含)</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-4-phase">Migration 4-phase</h2>
<h3 id="phase-0audit">Phase 0：Audit</h3>
<ul>
<li>列所有 Prometheus job / scrape config</li>
<li>統計 active series 數（Mimir tier 計費基準）</li>
<li>估 retention 需求</li>
</ul>
<h3 id="phase-1grafana-cloud-setup">Phase 1：Grafana Cloud setup</h3>
<ul>
<li>Account + organization 設定</li>
<li>API key for <code>remote_write</code></li>
<li>Grafana Cloud Mimir endpoint 啟用</li>
</ul>
<h3 id="phase-2dual-write">Phase 2：Dual-write</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://prometheus-prod-XX-prod-us-central-0.grafana.net/api/prom/push</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">basic_auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;INSTANCE_ID&gt;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;API_KEY&gt;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="c"># Optional: drop high-cardinality before sending</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;high_card_metric_.*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p>跑 4-8 週、確認 query 結果一致 + cost 在預期。</p>
<h3 id="phase-3cutover">Phase 3：Cutover</h3>
<ul>
<li>Dashboard / alert 切到 Grafana Cloud endpoint</li>
<li>應用層 / Grafana 自管 instance 關閉 query 對 self-managed Prometheus</li>
</ul>
<h3 id="phase-4cleanup">Phase 4：Cleanup</h3>
<ul>
<li>Self-managed Prometheus stop scrape</li>
<li>留 1-2 月歷史查詢能力（用 archive snapshot）</li>
<li>Decommission</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cardinality-爆cost-暴漲">Case 1：Cardinality 爆、cost 暴漲</h3>
<p><strong>徵兆</strong>：dual-write 第 2 週 Grafana Cloud series 從預估 100K 漲到 800K、cost 翻 8 倍。</p>
<p><strong>根因</strong>：application-level high-cardinality label（user_id / request_id）沒被 drop、scraped 進來。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>write_relabel_configs</code> drop unbounded label</li>
<li>Application metric 設計改 fixed-bucket histogram、不用 unbounded label</li>
<li>Mimir cardinality limit 設保護 + alert</li>
</ol>
<h3 id="case-2recording-rule-對應失效">Case 2：Recording rule 對應失效</h3>
<p><strong>徵兆</strong>：cutover 後 Grafana dashboard 某些 panel 顯示空；發現用了 Prometheus 端 recording rule (<code>job:request_count:rate5m</code>)、Grafana Cloud 端沒對應 rule。</p>
<p><strong>根因</strong>：Prometheus 端 recording rule 是 <em>server-side</em>、不會跟著 remote_write 帶過去；Grafana Cloud 需要自己 setup recording rule。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Export 所有 recording rule、import 到 Grafana Cloud Mimir</li>
<li>或改用 <em>raw query</em> + Grafana query template、不依賴 recording rule</li>
</ol>
<h3 id="case-3promql-微差行為">Case 3：PromQL 微差行為</h3>
<p><strong>徵兆</strong>：某些 query 在 self-managed Prometheus 跑得好好的、切 Grafana Cloud Mimir 後 returns slightly different results。</p>
<p><strong>根因</strong>：Mimir 對某些 edge case（empty result handling / staleness marker timing）行為跟 Prometheus 略不同；多數 query 一致、&lt; 1% query 受影響。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover dual-query 驗證、用 critical dashboard 比對</li>
<li>Affected query 重寫、用更 robust PromQL pattern</li>
<li>文件 known incompatibility list</li>
</ol>
<h3 id="case-4alert-routing-改變">Case 4：Alert routing 改變</h3>
<p><strong>徵兆</strong>：Cutover 後 PagerDuty / Slack 收不到 alert；發現 Alertmanager 端 webhook 沒切。</p>
<p><strong>根因</strong>：alert 邏輯從 self-managed Alertmanager 搬到 Grafana Cloud Alerting、routing / contact 配置完全重做。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 在 Grafana Cloud 端 rebuild alert + routing</li>
<li>雙 alert pipeline 跑 1-2 週、確認 Grafana Cloud 收到</li>
<li>Cutover 切 routing、SOC drill 一次</li>
</ol>
<h3 id="case-5歷史資料查不到">Case 5：歷史資料查不到</h3>
<p><strong>徵兆</strong>：Cutover 後 SOC 想 query 6 個月前事件、Grafana Cloud 只有 2 個月（dual-write 後的）資料。</p>
<p><strong>根因</strong>：Grafana Cloud 從 dual-write 開始才有資料、之前的 self-managed Prometheus historical data 沒 backfill。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Phase 2 期間用 <code>promtool tsdb dump</code> + <code>mimirtool</code> 把 self-managed historical 灌進 Mimir</li>
<li>或保留 self-managed Prometheus read-only 6 個月（給 historical query）</li>
<li>Long-term：retention 從 cutover 開始算、historical 是 <em>one-time backfill</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute (100 host, 100K series)</td>
          <td>$500-1000 / mo + ops</td>
          <td>$300-800 / mo</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8 = $3K-8K</td>
          <td>0.05-0.15 = $500-1500</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in 13 個月</td>
      </tr>
      <tr>
          <td>Total (mid-tier)</td>
          <td>$4K-9K / mo (含 FTE)</td>
          <td>$1K-2.5K / mo</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-2 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-datadog--grafana-stack-migration-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack migration</a> 對位</h3>
<p>兩條 Grafana Stack 路線：</p>
<ul>
<li>Self-host (Mimir + Loki + Tempo) on K8s：開源、自管</li>
<li>Grafana Cloud：SaaS、operational simplification</li>
</ul>
<p>本篇是「self-managed Prometheus → Grafana Cloud」、互補；如果跑兩階段（self-host → Cloud）跟「Datadog → Grafana Cloud」差不多。</p>
<h3 id="跟-opentelemetry-整合">跟 OpenTelemetry 整合</h3>
<p>OTel Collector 可同時 ship 到 Mimir (metric) + Loki (log) + Tempo (trace)；Migration 順便升 OTel 化避免下次 vendor 切換重複。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a> / <a href="/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/" data-link-title="Self-managed ELK → Elastic Cloud：5 年 ELK 集群的 lifecycle 收尾" data-link-desc="Self-managed ELK Stack → Elastic Cloud 是 Type C operational redesign — protocol drop-in、operational stack（cluster sizing / shard 治理 / upgrade / backup）全託管；本文按 5 年 ELK lifecycle (build → scale → degrade → save → migrate) 組織、5 個 production 踩雷">ELK → Elastic Cloud</a></li>
<li>平行 D-type 對位：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Sentry → Honeycomb：trace 不是 error、是不同 observability paradigm</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/migrate-from-sentry/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/migrate-from-sentry/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（error tracking ↔ wide-event observability）→ Type E paradigm shift&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="trace-不是-error是不同-paradigm">Trace 不是 error、是不同 paradigm&lt;/h2>
&lt;p>把 Sentry → Honeycomb 當「trace tool 替換」是最常見的誤判 — Sentry trace 是 &lt;em>error 上下文&lt;/em>、Honeycomb trace 是 &lt;em>observability 第一性&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>Sentry&lt;/th>
 &lt;th>Honeycomb&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Error tracking + transaction trace&lt;/td>
 &lt;td>High-cardinality wide-event observability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第一性 unit&lt;/td>
 &lt;td>Error event&lt;/td>
 &lt;td>Wide event (span with N fields)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trace 角色&lt;/td>
 &lt;td>Error 的「附帶 context」&lt;/td>
 &lt;td>Observability 主軸、每 event 是 trace span&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sampling&lt;/td>
 &lt;td>Error 全收 + transaction sample&lt;/td>
 &lt;td>Adaptive sampling、保留 &lt;em>anomaly&lt;/em>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query model&lt;/td>
 &lt;td>Filter + group by + aggregation&lt;/td>
 &lt;td>High-cardinality 多維 query (BubbleUp / heatmap)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>User base&lt;/td>
 &lt;td>Developer (debug error)&lt;/td>
 &lt;td>SRE + Platform (debug system behavior)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost model&lt;/td>
 &lt;td>Per-error event + transaction&lt;/td>
 &lt;td>Per-event (wide event volume)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Honeycomb 是 better Sentry」、在「兩者是不同 observability paradigm」&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Sentry 適合 &lt;em>application-level error debug&lt;/em> — 拿到 error stack trace + minimal context、快速 fix&lt;/li>
&lt;li>Honeycomb 適合 &lt;em>system-level behavior debug&lt;/em> — 看流量分佈 / 多維 correlation / 異常 outlier、找 &lt;em>為什麼這個 user 在這個時段在這個 endpoint 慢&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Migration scope 包含 &lt;em>paradigm reset&lt;/em> — 不是 SDK 換、是 SRE / Dev team 對 observability 的心智模型重設&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 跟 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（error tracking ↔ wide-event observability）→ Type E paradigm shift</em>。</p></blockquote>
<h2 id="trace-不是-error是不同-paradigm">Trace 不是 error、是不同 paradigm</h2>
<p>把 Sentry → Honeycomb 當「trace tool 替換」是最常見的誤判 — Sentry trace 是 <em>error 上下文</em>、Honeycomb trace 是 <em>observability 第一性</em>：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Sentry</th>
          <th>Honeycomb</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Error tracking + transaction trace</td>
          <td>High-cardinality wide-event observability</td>
      </tr>
      <tr>
          <td>第一性 unit</td>
          <td>Error event</td>
          <td>Wide event (span with N fields)</td>
      </tr>
      <tr>
          <td>Trace 角色</td>
          <td>Error 的「附帶 context」</td>
          <td>Observability 主軸、每 event 是 trace span</td>
      </tr>
      <tr>
          <td>Sampling</td>
          <td>Error 全收 + transaction sample</td>
          <td>Adaptive sampling、保留 <em>anomaly</em></td>
      </tr>
      <tr>
          <td>Query model</td>
          <td>Filter + group by + aggregation</td>
          <td>High-cardinality 多維 query (BubbleUp / heatmap)</td>
      </tr>
      <tr>
          <td>User base</td>
          <td>Developer (debug error)</td>
          <td>SRE + Platform (debug system behavior)</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>Per-error event + transaction</td>
          <td>Per-event (wide event volume)</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Honeycomb 是 better Sentry」、在「兩者是不同 observability paradigm」</strong>：</p>
<ul>
<li>Sentry 適合 <em>application-level error debug</em> — 拿到 error stack trace + minimal context、快速 fix</li>
<li>Honeycomb 適合 <em>system-level behavior debug</em> — 看流量分佈 / 多維 correlation / 異常 outlier、找 <em>為什麼這個 user 在這個時段在這個 endpoint 慢</em></li>
</ul>
<p><strong>Migration scope 包含 <em>paradigm reset</em> — 不是 SDK 換、是 SRE / Dev team 對 observability 的心智模型重設</strong>。</p>
<h2 id="為什麼遷observability-成熟度--cardinality--cost-三條-driver">為什麼遷：observability 成熟度 / cardinality / cost 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability 成熟度</td>
          <td>Application 規模到 <em>跨多 service / multi-tenant</em>、Sentry error tracking 不夠細、SRE 要看 <em>high-cardinality</em> 多維 query</td>
      </tr>
      <tr>
          <td>High-cardinality</td>
          <td>Sentry tag system 限制 cardinality（~1000 unique value）、Honeycomb native 支援 millions cardinality</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>Per-error pricing 對 high-error volume 場景爆、Honeycomb per-event 在 <em>wide event</em> 場景更可預測</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Honeycomb → Sentry）：</p>
<ul>
<li>Pure error tracking 場景、Honeycomb wide-event 過度設計</li>
<li>Frontend / mobile 客戶端 error tracking、Sentry 對 web/mobile/desktop SDK 成熟度高</li>
</ul>
<h2 id="6-維-audit">6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Medium（event schema 概念不同、SDK 完全換）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Low（兩者都 SaaS、operational 對等）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td><strong>High</strong>（error tracking ↔ wide-event observability）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low（同 1 個 observability vendor）</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td><strong>High</strong>（SDK 換 + instrumentation 重設計）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Paradigm = High（其他 Low-Medium）→ Type E paradigm shift；application change 雖 High 但是 paradigm 的 downstream。</p>
<h2 id="結構partial-migration--混合架構是-long-term-default">結構：partial migration + 混合架構是 long-term default</h2>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a> 同 Type E pattern：</p>
<ul>
<li><strong>不存在 complete migration</strong>：Sentry 對 <em>frontend error tracking</em> 強項、Honeycomb 對 <em>backend system observability</em> 強項</li>
<li><strong>長期混合架構</strong>：frontend / mobile 保留 Sentry、backend / SRE 走 Honeycomb</li>
<li><strong>Application 重設計</strong>：instrumentation 用 OpenTelemetry、避免 vendor SDK lock-in</li>
</ul>
<h2 id="application-重設計範例">Application 重設計範例</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Before: Sentry SDK</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sentry_sdk</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">dsn</span><span class="o">=</span><span class="s1">&#39;https://x@sentry.io/y&#39;</span><span class="p">)</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="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">process_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">capture_exception</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">raise</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="c1"># After: OpenTelemetry + Honeycomb</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.sdk.trace</span> <span class="kn">import</span> <span class="n">TracerProvider</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.sdk.trace.export</span> <span class="kn">import</span> <span class="n">BatchSpanProcessor</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.exporter.otlp.proto.grpc.trace_exporter</span> <span class="kn">import</span> <span class="n">OTLPSpanExporter</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 class="n">trace</span><span class="o">.</span><span class="n">set_tracer_provider</span><span class="p">(</span><span class="n">TracerProvider</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">trace</span><span class="o">.</span><span class="n">get_tracer_provider</span><span class="p">()</span><span class="o">.</span><span class="n">add_span_processor</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">BatchSpanProcessor</span><span class="p">(</span><span class="n">OTLPSpanExporter</span><span class="p">(</span><span class="n">endpoint</span><span class="o">=</span><span class="s1">&#39;https://api.honeycomb.io&#39;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;x-honeycomb-team&#39;</span><span class="p">:</span> <span class="s1">&#39;YOUR_API_KEY&#39;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">tracer</span> <span class="o">=</span> <span class="n">trace</span><span class="o">.</span><span class="n">get_tracer</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s1">&#39;process_order&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.id&#39;</span><span class="p">,</span> <span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;user.id&#39;</span><span class="p">,</span> <span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.amount&#39;</span><span class="p">,</span> <span class="n">order</span><span class="o">.</span><span class="n">amount</span><span class="p">)</span>  <span class="c1"># high-cardinality 自然</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.region&#39;</span><span class="p">,</span> <span class="n">region</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">process_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">set_status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">Status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">StatusCode</span><span class="o">.</span><span class="n">OK</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">set_status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">Status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">StatusCode</span><span class="o">.</span><span class="n">ERROR</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)))</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">record_exception</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="k">raise</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>Sentry 只 capture exception + 簡 context</li>
<li>Honeycomb 對每 operation 寫 <em>wide event</em> 含 high-cardinality field（user.id / order.amount / order.region）</li>
<li>SRE 端能跑 <code>WHERE order.region = &quot;us-west-2&quot; AND duration &gt; 5000</code> 的 multi-dim query</li>
</ul>
<h2 id="migration-流程">Migration 流程</h2>





<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. Audit application：列所有 Sentry SDK 使用 + capture pattern
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Pure error tracking (frontend): 保留 Sentry
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Backend system trace: 切 Honeycomb / OTel
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - Error + context (混合): 雙寫期 evaluate
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">3. OpenTelemetry instrumentation 化:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - 用 OTel SDK 取代 vendor SDK
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - Honeycomb 是 OTLP target、跟 vendor lock 解耦
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. Backend application 切 Honeycomb (3-6 個月)
</span></span><span class="line"><span class="ln">10</span><span class="cl">5. Frontend / mobile 保留 Sentry
</span></span><span class="line"><span class="ln">11</span><span class="cl">6. SRE training: Honeycomb BubbleUp / heatmap / multi-dim query</span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1event-schema-對位失敗sre-不會用-bubbleup">Case 1：Event schema 對位失敗、SRE 不會用 BubbleUp</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後 SRE 用 Sentry 思維 — 找 error → fix；Honeycomb BubbleUp / heatmap 沒人會用、observability 退化到 <em>只看 error count</em>。</p>
<p><strong>根因</strong>：Sentry → Honeycomb migration 不只是 tool 換、是 <em>observability mindset 換</em>；SRE 沒培訓 wide-event query / BubbleUp anomaly detection。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>SRE training</strong>：1-2 週 hands-on Honeycomb BubbleUp + heatmap + multi-dim query</li>
<li><strong>Migration scope 含 sample query playbook</strong>：每個 incident type 對應 Honeycomb query 寫成 runbook</li>
<li><strong>保留 Sentry frontend / mobile</strong>：不要逼 SRE 全切、保留 <em>paradigm fit</em> 的部分</li>
</ol>
<h3 id="case-2sampling-行為差production-cost-飛">Case 2：Sampling 行為差、production cost 飛</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後第 1 個月 event volume 比 Sentry 高 100x；帳單暴漲。</p>
<p><strong>根因</strong>：Sentry 對 transaction 端 sample（10% 預設）、error 全收；Honeycomb 端 <em>每 span 都 wide event</em>、application 端沒設 sampling 全送、event volume 爆。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Honeycomb Refinery (sampling proxy)</strong>：deploy refinery 在 application 端跟 Honeycomb 之間、tail-based sampling</li>
<li><strong>Sample rule</strong>：保留 <em>anomaly</em> (error / slow / outlier)、drop <em>boring success</em> 90%+</li>
<li><strong>Cost monitoring 第一週密集</strong>：cardinality + event volume + cost dashboard、catch 預期外 spike</li>
</ol>
<h3 id="case-3error-grouping-失效">Case 3：Error grouping 失效</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後 <em>相似 error</em> 沒被 group 成「同類 issue」、SRE 看每 event 獨立、failure 模式淹沒在 noise。</p>
<p><strong>根因</strong>：Sentry 自動 error grouping (by stack trace fingerprint)、Honeycomb 沒對等 — wide event 是 first-class、event grouping 需要 application 端 explicit 設 <code>error.type</code> field。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 端設 error type field</strong>：<code>span.set_attribute('error.type', exception_class)</code></li>
<li><strong>Honeycomb derived column</strong>：用 derived column 算 error fingerprint</li>
<li><strong>保留 Sentry error tracking</strong>：純 error grouping 場景 Sentry 強項、別硬切</li>
</ol>
<h3 id="case-4cost-模型差預估錯">Case 4：Cost 模型差、預估錯</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後預估 50% cost saving、實際只省 10-15%。</p>
<p><strong>根因</strong>：Sentry per-error pricing 對 error-heavy application 貴；Honeycomb per-event pricing 對 <em>wide event volume</em> application 貴；如果 application 是 <em>event volume 高 但 error 少</em>、Honeycomb 反而貴。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 估</strong>：用 OTel pilot 跑 1-2 週、估真實 event volume</li>
<li><strong>Sample rule 設計</strong>：retention 7 天 hot + 30 天 cold + 1 年 archive、降 cost</li>
<li><strong>混合架構保留</strong>：frontend / mobile 走 Sentry、backend 走 Honeycomb、避免一邊 cost 爆</li>
</ol>
<h3 id="case-5alert-paradigm-不對等">Case 5：Alert paradigm 不對等</h3>
<p><strong>徵兆</strong>：Sentry alert 簡單（error rate / latency p99 threshold）、Honeycomb trigger 配置複雜（SLO + burn rate + BubbleUp）；SOC 學習曲線 1-2 個月。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Migration 含 alert rebuild scope</strong>：Honeycomb trigger 不直接對位 Sentry alert、要重寫</li>
<li><strong>SLO-driven alert</strong>：用 Honeycomb SLO 取代 Sentry threshold alert、降 alert fatigue</li>
<li><strong>PagerDuty integration</strong>：兩家都支援、routing rule 跟 dedup 要 review</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Sentry</th>
          <th>Honeycomb</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pricing model</td>
          <td>Per-error + transaction</td>
          <td>Per-event (wide event)</td>
      </tr>
      <tr>
          <td>Cost (mid-tier)</td>
          <td>$500-2000 / mo</td>
          <td>$400-3000 / mo (依 event volume)</td>
      </tr>
      <tr>
          <td>Sampling</td>
          <td>Built-in transaction sampling</td>
          <td>Refinery (additional component)</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>~1000 unique value / tag</td>
          <td>Millions / field</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low (SDK + capture exception)</td>
          <td>Medium (OTel + wide event instrument)</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>2-4 FTE × 2-3 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-opentelemetry-整合">跟 OpenTelemetry 整合</h3>
<p>OTel 是 vendor-neutral instrumentation、Honeycomb 是 OTLP backend；application 端 OTel 化後可以同時 ship 到多個 backend（dev 端 Jaeger / production 端 Honeycomb / fallback 端 Tempo）。</p>
<h3 id="跟-datadog--grafana-stack-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a> 對位</h3>
<p>兩條 observability 路線：</p>
<ul>
<li>Grafana Stack (Mimir / Loki / Tempo)：self-host or Grafana Cloud、open source baseline</li>
<li>Honeycomb：SaaS-only、focus wide-event observability</li>
</ul>
<p>選擇取決於 <em>observability paradigm</em>：trace-heavy 走 Tempo / Honeycomb、metric-heavy 走 Mimir / Datadog。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a> / <a href="/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/" data-link-title="etcd → Consul：KV &#43; N 個 extras feature matrix" data-link-desc="etcd → Consul 是 Type E paradigm shift expansion — 從 pure KV store 升到 service mesh / discovery / health check / multi-DC；本文用對照表 &#43; paradigm expansion 路線、5 個 production 踩雷（API 對位 / lock semantics / watch event model / multi-DC topology / ACL system）">etcd → Consul</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Terraform → OpenTofu：HCL 跟 state file 級 drop-in、CI runner 切 binary 完成</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/migrate-to-opentofu/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/migrate-to-opentofu/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform&lt;/a>（source）跟 OpenTofu（target）。Type B drop-in migration 標準形態、跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>6 維皆 Low → Type B drop-in&lt;/em>；本文驗證 skill 的 Type B anatomy 在 IaC 領域成立。&lt;/p>&lt;/blockquote>
&lt;h2 id="hcl--state-file--provider-三層-diff-sample">HCL / state file / provider 三層 diff sample&lt;/h2>
&lt;p>跟前批 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> 同為 Type B drop-in、本文用 code-led entry — 直接給 3 種 diff sample 證明「真 drop-in」：&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="c1"># 1. HCL syntax: 完全相同 (Terraform 1.5.x baseline)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket&amp;#34; &amp;#34;logs&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"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;myapp-logs&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"> tags&lt;/span> &lt;span class="o">=&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"> Env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="err">#&lt;/span> &lt;span class="k">兩家&lt;/span> &lt;span class="k">binary&lt;/span> &lt;span class="k">都接受&lt;/span>&lt;span class="err">、&lt;/span>&lt;span class="k">執行結果一致&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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"># 2. State file: 完全相同 schema&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">$ cat terraform.tfstate &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.version, .terraform_version&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s2">&amp;#34;1.5.7&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 class="c1"># 切 OpenTofu 後 re-init、state 保留&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">$ tofu init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">$ cat terraform.tfstate &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.version, .terraform_version&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s2">&amp;#34;1.6.0&amp;#34;&lt;/span> &lt;span class="c1"># tool version 標記變、其他不變&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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="c1"># 3. Provider: registry 路徑唯一明顯差異
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">required_providers&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"> aws&lt;/span> &lt;span class="o">=&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"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;hashicorp/aws&amp;#34;&lt;/span>&lt;span class="c1"> # 兩家共用 source 字串
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n"> version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;~&amp;gt; 5.0&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># Terraform 從 registry.terraform.io 拉
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="err">#&lt;/span> &lt;span class="k">OpenTofu&lt;/span> &lt;span class="k">預設從&lt;/span> &lt;span class="k">registry&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">opentofu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">org&lt;/span> &lt;span class="k">拉&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">fallback&lt;/span> &lt;span class="k">到&lt;/span> &lt;span class="k">terraform&lt;/span> &lt;span class="k">registry&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>3 層 diff sample 顯示：HCL / state schema / 主流 provider 配置完全相容；唯一明顯差異在 &lt;em>registry routing&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a>（source）跟 OpenTofu（target）。Type B drop-in migration 標準形態、跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>6 維皆 Low → Type B drop-in</em>；本文驗證 skill 的 Type B anatomy 在 IaC 領域成立。</p></blockquote>
<h2 id="hcl--state-file--provider-三層-diff-sample">HCL / state file / provider 三層 diff sample</h2>
<p>跟前批 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 同為 Type B drop-in、本文用 code-led entry — 直接給 3 種 diff sample 證明「真 drop-in」：</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="c1"># 1. HCL syntax: 完全相同 (Terraform 1.5.x baseline)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket&#34; &#34;logs&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="s2">&#34;myapp-logs&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    Env</span> <span class="o">=</span> <span class="s2">&#34;production&#34;</span>
</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></span><span class="line"><span class="ln">8</span><span class="cl"><span class="err">#</span> <span class="k">兩家</span> <span class="k">binary</span> <span class="k">都接受</span><span class="err">、</span><span class="k">執行結果一致</span></span></span></code></pre></div>




<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"># 2. State file: 完全相同 schema</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ cat terraform.tfstate <span class="p">|</span> jq <span class="s1">&#39;.version, .terraform_version&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">&#34;1.5.7&#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 class="c1"># 切 OpenTofu 後 re-init、state 保留</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">$ tofu init
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ cat terraform.tfstate <span class="p">|</span> jq <span class="s1">&#39;.version, .terraform_version&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">&#34;1.6.0&#34;</span>  <span class="c1"># tool version 標記變、其他不變</span></span></span></code></pre></div>




<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="c1"># 3. Provider: registry 路徑唯一明顯差異
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    aws</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">      source</span>  <span class="o">=</span> <span class="s2">&#34;hashicorp/aws&#34;</span><span class="c1">     # 兩家共用 source 字串
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="n">      version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 5.0&#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></span><span class="line"><span class="ln"> 9</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Terraform 從 registry.terraform.io 拉
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">OpenTofu</span> <span class="k">預設從</span> <span class="k">registry</span><span class="p">.</span><span class="k">opentofu</span><span class="p">.</span><span class="k">org</span> <span class="k">拉</span> <span class="p">(</span><span class="k">fallback</span> <span class="k">到</span> <span class="k">terraform</span> <span class="k">registry</span><span class="p">)</span></span></span></code></pre></div><p>3 層 diff sample 顯示：HCL / state schema / 主流 provider 配置完全相容；唯一明顯差異在 <em>registry routing</em>。</p>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>HCL 完全相容、CLI command 對映 (terraform → tofu)</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 workflow (init / plan / apply)</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 IaC declarative</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 single binary</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>無（不是 application、是 infrastructure tool）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single state file backend</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>6 維皆 Low → Type B drop-in。</p>
<h2 id="為什麼遷license--governance--community-三條-driver">為什麼遷：license / governance / community 三條 driver</h2>
<p>跟前批 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 不同（cost / performance driver）、Terraform → OpenTofu 主要 driver 在 governance：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>License</strong></td>
          <td>Terraform 在 2023-08 改 BSL（Business Source License）、商業使用限制；OpenTofu 維持 MPL 2.0 開源</td>
      </tr>
      <tr>
          <td><strong>Vendor neutrality</strong></td>
          <td>多雲 / 多客戶情境想避免 HashiCorp lock-in、用 Linux Foundation 治理的 OpenTofu</td>
      </tr>
      <tr>
          <td><strong>Community / feature</strong></td>
          <td>OpenTofu 1.6+ 加 state encryption、跟 Terraform 商業版差異化、社群驅動 feature</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（OpenTofu → Terraform）：</p>
<ul>
<li>Terraform Cloud / Enterprise 特定 feature 依賴（policy as code 用 Sentinel、跟 OpenTofu 自家 OPA 不對等）</li>
<li>既有 module 在 Terraform registry 維護、未同步 OpenTofu registry</li>
</ul>
<h2 id="相容性-audit">相容性 audit</h2>
<p>Pre-cutover 必跑：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Terraform version pin（<code>required_version = &quot;&gt;= 1.5.0, &lt; 1.6.0&quot;</code>）</td>
          <td>改 <code>&gt;= 1.6.0</code> 涵蓋 OpenTofu / 移除 upper bound</td>
      </tr>
      <tr>
          <td>Provider 來源 (registry path)</td>
          <td>主流 provider（aws / azurerm / gcp / k8s）都同源、自家 / 第三方 provider 確認 OpenTofu registry mirror</td>
      </tr>
      <tr>
          <td>Terraform Cloud / Enterprise feature</td>
          <td>Sentinel policy → OpenTofu OPA / Conftest；workspace API 對等性逐項 check</td>
      </tr>
      <tr>
          <td>CLI binary name 在 CI pipeline</td>
          <td><code>terraform plan</code> → <code>tofu plan</code>、或 alias <code>terraform=tofu</code> 保留兼容</td>
      </tr>
      <tr>
          <td>State backend (S3 / GCS / Azure / Consul / Terraform Cloud)</td>
          <td>S3/GCS/Azure 完全相容；Consul backend 兩家都支援；Terraform Cloud 走自家 remote backend、不直通</td>
      </tr>
      <tr>
          <td>Module source</td>
          <td>git-based module 完全相容；registry module 確認 OpenTofu registry 有 mirror</td>
      </tr>
  </tbody>
</table>
<p>Audit output：列「100% drop-in」block + 「需處理」block；後者通常 &lt; 5% 範圍。</p>
<h2 id="step-by-step-cutover">Step-by-step cutover</h2>





<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. Install OpenTofu (跨 OS)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install opentofu                <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">snap install --classic opentofu      <span class="c1"># Ubuntu</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># https://opentofu.org/docs/intro/install/</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"># 2. 在 workspace 跑 tofu init</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">$ <span class="nb">cd</span> terraform-workspace/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ tofu init -upgrade
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 升級 provider / module、re-init backend、保留 state</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="c1"># 3. Plan diff（應該 = 0 changes）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">$ tofu plan
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Plan: 0 to add, 0 to change, 0 to destroy.</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 如果有 diff、表示 provider version 不對齊、檢查 lock file</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 4. Apply（保險起見、staging 先跑）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">$ tofu apply
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 5. CI / CD pipeline 切 binary</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># Before</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">terraform init
</span></span><span class="line"><span class="ln">22</span><span class="cl">terraform plan -out<span class="o">=</span>tfplan
</span></span><span class="line"><span class="ln">23</span><span class="cl">terraform apply tfplan
</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 class="c1"># After</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">tofu init
</span></span><span class="line"><span class="ln">27</span><span class="cl">tofu plan -out<span class="o">=</span>tfplan
</span></span><span class="line"><span class="ln">28</span><span class="cl">tofu apply tfplan
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"># 或保留 terraform 字面、用 alias / symlink</span></span></span></code></pre></div><p>整個 cutover 通常 &lt; 1 天（單 workspace）；多 workspace organization 視規模 1-4 週逐個切。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1provider-version-driftstaging-plan-出現意外-diff">Case 1：Provider version drift、staging plan 出現意外 diff</h3>
<p><strong>徵兆</strong>：<code>tofu plan</code> 顯示 100+ resource 有 in-place update、實際業務沒改任何 config。</p>
<p><strong>根因</strong>：<code>.terraform.lock.hcl</code> 鎖的 provider version 在 Terraform / OpenTofu registry 不一致（同 version 但 binary checksum 微差）；OpenTofu 在 init 時拉新 checksum、視為「provider 變了」。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先對齊</strong>：<code>tofu init -upgrade</code> 重建 lock file、把 OpenTofu 端 checksum 寫進去</li>
<li><strong>CI lockfile commit</strong>：lock file 進版控、不同 binary 端跑前先 lockfile 對齊</li>
<li><strong>若 plan 仍有差異</strong>：通常是 provider 內部 schema 對 nil 值處理不同、用 <code>lifecycle.ignore_changes</code> 暫忽略、後續逐項 fix</li>
</ol>
<h3 id="case-2state-file-lock-機制微差">Case 2：State file lock 機制微差</h3>
<p><strong>徵兆</strong>：兩個 CI pipeline 同時跑 <code>tofu apply</code>、其中一個應該 lock 拒絕、實際兩個都跑、production 端 race condition。</p>
<p><strong>根因</strong>：Terraform DynamoDB lock 跟 OpenTofu lock 用相同 schema 但 lock_id 規則略不同；舊 lock entry 殘留時 OpenTofu 端解析失敗、視為「無 lock」繼續跑。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>DynamoDB lock table 手動清舊 entry</strong>：cutover 期間先 <code>aws dynamodb delete-item</code> 清舊 lock</li>
<li><strong>單向流量切換</strong>：cutover 期間 freeze 所有 CI、只一個 pipeline 跑、避免 race</li>
<li><strong>架構</strong>：用 <em>fully replicated lock backend</em>（如 Consul）avoid backend-specific lock 怪異</li>
</ol>
<h3 id="case-3terraform-cloud-workspace-不能直接搬">Case 3：Terraform Cloud workspace 不能直接搬</h3>
<p><strong>徵兆</strong>：team 已用 Terraform Cloud workspace 跑 100+ pipeline、想切 OpenTofu、發現 <code>terraform login</code> / workspace API / VCS integration 全 HashiCorp-specific。</p>
<p><strong>根因</strong>：OpenTofu 沒對等 Terraform Cloud 服務；自家 backend 用 S3 + Atlantis / Spacelift / env0 等第三方 platform 對接、不是 1:1 替代。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>保留 Terraform Cloud 跑 production</strong>（OpenTofu 不替代）、用 OpenTofu 跑 dev / sandbox</li>
<li><strong>遷出 Terraform Cloud</strong>：state 遷 S3 + 用 Atlantis 跑 PR-based plan/apply（mature open source）</li>
<li><strong>評估 Spacelift / env0</strong> 商業替代、支援 OpenTofu + 對等 workspace feature</li>
</ol>
<h3 id="case-4ci-pipeline-寫死-terraform-binary-name">Case 4：CI pipeline 寫死 <code>terraform</code> binary name</h3>
<p><strong>徵兆</strong>：cutover 後 CI 跑 <code>terraform plan</code> 報「command not found」；team 100+ pipeline / GitHub Action / GitLab CI / shell script 都寫死 <code>terraform</code>。</p>
<p><strong>根因</strong>：rollout 計畫沒 grep 全 organization 找 binary name 引用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Alias 策略</strong>：CI image 內 <code>ln -s /usr/local/bin/tofu /usr/local/bin/terraform</code>、保留兼容 1-3 個月</li>
<li><strong>逐步改 <code>tofu</code></strong>：跟著 IaC team 修 pipeline file、target 100% 改完才 remove alias</li>
<li><strong>架構</strong>：避免在 pipeline / script 寫死 binary、用 env variable <code>IAC_BINARY=${IAC_BINARY:-tofu}</code></li>
</ol>
<h3 id="case-5registry-routing自家-module-拉不到">Case 5：Registry routing、自家 module 拉不到</h3>
<p><strong>徵兆</strong>：cutover 後 <code>tofu init</code> 對自家 private module 報「not found」；同 module 在 Terraform 端跑得好好的。</p>
<p><strong>根因</strong>：private module 註冊在 <em>Terraform Cloud private registry</em>、OpenTofu 預設不知道這個 endpoint；需要顯式設 registry source URL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>顯式 source URL</strong>：<code>source = &quot;app.terraform.io/myorg/myapp/aws&quot;</code> 改 git source 或自架 module registry</li>
<li><strong>架構</strong>：用 git-based module source（<code>source = &quot;git::ssh://git@github.com/myorg/myapp.git&quot;</code>）、避開 registry lock-in</li>
<li><strong>長期</strong>：自家 module 同時 publish 到 OpenTofu registry / Terraform Cloud / git、跨 tool 兼容</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Terraform</th>
          <th>OpenTofu</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Binary cost</td>
          <td>免費 (community edition)</td>
          <td>免費（永遠）</td>
      </tr>
      <tr>
          <td>Terraform Cloud cost</td>
          <td>$20 / user / month、enterprise 高</td>
          <td>無對等服務（用 Atlantis / Spacelift / env0）</td>
      </tr>
      <tr>
          <td>State storage</td>
          <td>S3 / 自家 backend、低</td>
          <td>S3 / 自家 backend、低</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-5 person-day（含 audit + cutover + CI 改）</td>
      </tr>
      <tr>
          <td>License risk</td>
          <td>BSL 限制商業使用</td>
          <td>MPL 2.0 開源、無 license risk</td>
      </tr>
      <tr>
          <td>Long-term governance</td>
          <td>HashiCorp 單一供應商</td>
          <td>Linux Foundation + 多廠商貢獻</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 IaC 用戶切 OpenTofu 風險低 + 省 license 風險；重度依賴 Terraform Cloud feature 的 organization 保留或評估 commercial alternatives（Spacelift / env0）。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-atlantis--spacelift--env0-整合">跟 <a href="https://www.runatlantis.io/">Atlantis / Spacelift / env0</a> 整合</h3>
<p>OpenTofu 沒對等 Terraform Cloud、需要 third-party orchestrator：</p>
<ul>
<li><strong>Atlantis</strong>：自架、開源、輕量、適合中小型 team</li>
<li><strong>Spacelift</strong>：SaaS、policy as code、支援 OpenTofu first-class</li>
<li><strong>env0</strong>：SaaS、cost estimation、workflow 完整</li>
</ul>
<h3 id="跟-terragrunt-整合">跟 <a href="https://terragrunt.gruntwork.io/">Terragrunt</a> 整合</h3>
<p>Terragrunt（OpenTofu / Terraform 共用 wrapper）已支援 OpenTofu 1.6+；多環境配置抽象保留、底層 binary 切換無感。</p>
<h3 id="反向-migrationopentofu--terraform">反向 migration（OpenTofu → Terraform）</h3>
<p>罕見、通常是 organization 走商業合約綁 HashiCorp Enterprise 才會做；流程鏡像對稱、注意 OpenTofu 1.6+ 自家 feature（state encryption / provider for_each）在 Terraform 端可能缺。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>State encryption（OpenTofu 1.7+）</strong>：sensitive state 加密、Terraform 商業版才有對等 feature</li>
<li><strong>跨 IaC tool（Pulumi / CDK）</strong>：Pulumi / AWS CDK 是不同 paradigm（imperative）、不在本 migration scope</li>
<li><strong>Provider ecosystem 長期分裂</strong>：兩家 registry 自我演化、需要 quarterly review provider compat</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a></li>
<li>平行 migration playbook（Type B）：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定</a></li>
</ul>
]]></content:encoded></item><item><title>3.C11 Pinterest：Kafka tiered storage broker-decoupled</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/</guid><description>&lt;p>這個案例的核心責任是說明 tiered storage 不只是「冷資料 offload」、是 broker 與儲存解耦的架構選擇。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Pinterest 從 Kafka broker 卸 ~200 TB/day 熱資料到 S3、2024 年 5 月起 20+ production topic 上線、跟 KIP-405 native tiered storage 不同、採 broker-decoupled 設計。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Broker-decoupled 設計讓 consumer 直接從 S3 拉、broker 不再是熱路徑。揭露「broker resource 跟 cross-AZ network cost」其實該分離治理、而非綁在 broker 容量擴張上。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：tiered storage / 跨層儲存成本。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/pinterest-tiered-storage-for-apache-kafka-%EF%B8%8F-a-broker-decoupled-approach-c33c69e9958b">Pinterest Tiered Storage for Apache Kafka — a Broker-Decoupled Approach&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 tiered storage 不只是「冷資料 offload」、是 broker 與儲存解耦的架構選擇。</p>
<h2 id="觀察">觀察</h2>
<p>Pinterest 從 Kafka broker 卸 ~200 TB/day 熱資料到 S3、2024 年 5 月起 20+ production topic 上線、跟 KIP-405 native tiered storage 不同、採 broker-decoupled 設計。</p>
<h2 id="判讀">判讀</h2>
<p>Broker-decoupled 設計讓 consumer 直接從 S3 拉、broker 不再是熱路徑。揭露「broker resource 跟 cross-AZ network cost」其實該分離治理、而非綁在 broker 容量擴張上。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：tiered storage / 跨層儲存成本。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/pinterest-engineering/pinterest-tiered-storage-for-apache-kafka-%EF%B8%8F-a-broker-decoupled-approach-c33c69e9958b">Pinterest Tiered Storage for Apache Kafka — a Broker-Decoupled Approach</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Patroni-based HA&lt;/em> 的 lifecycle 設計 — 從正常運作到 failover 完成的 5 段、每段配置 + failure mode + recovery。&lt;/p>&lt;/blockquote>
&lt;h2 id="failover-lifecycle5-段不是一條曲線">Failover lifecycle：5 段不是一條曲線&lt;/h2>
&lt;p>PostgreSQL 原生沒有 auto-failover；primary 掛了、application 卡死、SRE 手動 promote standby — 整個過程通常 5-30 分鐘。Patroni 把這條鏈拆成 &lt;em>自動化的 5 段 lifecycle&lt;/em>、每段有自己的 trigger、配置、失敗模式：&lt;/p>
&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;strong>1. Detection&lt;/strong>&lt;/td>
 &lt;td>Leader heartbeat 在 DCS（etcd / Consul）失聯&lt;/td>
 &lt;td>Standby 們開始觀察、累積失聯時間到 TTL&lt;/td>
 &lt;td>DCS 本身分裂 → false detection 啟動失敗 failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>2. Election&lt;/strong>&lt;/td>
 &lt;td>TTL 過、DCS 開放 leader lock&lt;/td>
 &lt;td>Standby 競爭寫 leader key（DCS quorum-based）&lt;/td>
 &lt;td>Network partition → 兩邊都自認 leader（split-brain）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>3. Promotion&lt;/strong>&lt;/td>
 &lt;td>新 leader 寫 DCS key 成功&lt;/td>
 &lt;td>跑 &lt;code>pg_ctl promote&lt;/code>、停 streaming replication、開始接寫&lt;/td>
 &lt;td>Standby 落後太多 → 拒 promote 或承接時資料缺&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>4. Reconfiguration&lt;/strong>&lt;/td>
 &lt;td>Patroni REST API 通知 routing 層&lt;/td>
 &lt;td>HAProxy / PgBouncer 切流量到新 leader&lt;/td>
 &lt;td>Routing 層 health check 慢 → 流量持續打舊 leader&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>5. Recovery&lt;/strong>&lt;/td>
 &lt;td>舊 leader 恢復（手動 / 自動）&lt;/td>
 &lt;td>跑 &lt;code>pg_rewind&lt;/code> + 重接 streaming replication 為 standby&lt;/td>
 &lt;td>WAL divergence 太大 → 必須重 base backup&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每段都有獨立配置、不是「設一個 timeout 就好」。後面分段展開。&lt;/p>
&lt;h2 id="stage-1detection--dcs-heartbeat-跟-ttl">Stage 1：Detection — DCS heartbeat 跟 TTL&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># patroni.yml 核心配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">scope&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp-pg-cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/db/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pg-node-1 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 跟 hostname 一致&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">etcd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">etcd1:2379,etcd2:2379,etcd3:2379 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># DCS quorum&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">bootstrap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dcs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ttl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># leader lock TTL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loop_wait&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># patroni 主循環間隔&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retry_timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># DCS retry 上限&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maximum_lag_on_failover&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1048576&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># standby 落後 1MB 內才能 promote&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">synchronous_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># async / sync 取捨&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵直覺：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL 在 OLTP 譜系的定位、本文聚焦 <em>Patroni-based HA</em> 的 lifecycle 設計 — 從正常運作到 failover 完成的 5 段、每段配置 + failure mode + recovery。</p></blockquote>
<h2 id="failover-lifecycle5-段不是一條曲線">Failover lifecycle：5 段不是一條曲線</h2>
<p>PostgreSQL 原生沒有 auto-failover；primary 掛了、application 卡死、SRE 手動 promote standby — 整個過程通常 5-30 分鐘。Patroni 把這條鏈拆成 <em>自動化的 5 段 lifecycle</em>、每段有自己的 trigger、配置、失敗模式：</p>
<table>
  <thead>
      <tr>
          <th>段</th>
          <th>觸發</th>
          <th>動作</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>1. Detection</strong></td>
          <td>Leader heartbeat 在 DCS（etcd / Consul）失聯</td>
          <td>Standby 們開始觀察、累積失聯時間到 TTL</td>
          <td>DCS 本身分裂 → false detection 啟動失敗 failover</td>
      </tr>
      <tr>
          <td><strong>2. Election</strong></td>
          <td>TTL 過、DCS 開放 leader lock</td>
          <td>Standby 競爭寫 leader key（DCS quorum-based）</td>
          <td>Network partition → 兩邊都自認 leader（split-brain）</td>
      </tr>
      <tr>
          <td><strong>3. Promotion</strong></td>
          <td>新 leader 寫 DCS key 成功</td>
          <td>跑 <code>pg_ctl promote</code>、停 streaming replication、開始接寫</td>
          <td>Standby 落後太多 → 拒 promote 或承接時資料缺</td>
      </tr>
      <tr>
          <td><strong>4. Reconfiguration</strong></td>
          <td>Patroni REST API 通知 routing 層</td>
          <td>HAProxy / PgBouncer 切流量到新 leader</td>
          <td>Routing 層 health check 慢 → 流量持續打舊 leader</td>
      </tr>
      <tr>
          <td><strong>5. Recovery</strong></td>
          <td>舊 leader 恢復（手動 / 自動）</td>
          <td>跑 <code>pg_rewind</code> + 重接 streaming replication 為 standby</td>
          <td>WAL divergence 太大 → 必須重 base backup</td>
      </tr>
  </tbody>
</table>
<p>每段都有獨立配置、不是「設一個 timeout 就好」。後面分段展開。</p>
<h2 id="stage-1detection--dcs-heartbeat-跟-ttl">Stage 1：Detection — DCS heartbeat 跟 TTL</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># patroni.yml 核心配置</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">scope</span><span class="p">:</span><span class="w"> </span><span class="l">myapp-pg-cluster</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">/db/</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pg-node-1                               </span><span class="w"> </span><span class="c"># 跟 hostname 一致</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">etcd</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l">etcd1:2379,etcd2:2379,etcd3:2379      </span><span class="w"> </span><span class="c"># DCS quorum</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">dcs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">ttl</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">                                     </span><span class="c"># leader lock TTL</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">loop_wait</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">                               </span><span class="c"># patroni 主循環間隔</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">retry_timeout</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">                           </span><span class="c"># DCS retry 上限</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">maximum_lag_on_failover</span><span class="p">:</span><span class="w"> </span><span class="m">1048576</span><span class="w">            </span><span class="c"># standby 落後 1MB 內才能 promote</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">synchronous_mode</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">                     </span><span class="c"># async / sync 取捨</span></span></span></code></pre></div><p>關鍵直覺：</p>
<ul>
<li><strong>TTL (30s) = leader 失聯多久才被視為 dead</strong>。設太短（&lt; 15s）會把 transient network jitter 當 dead；設太長（&gt; 60s）unavailability 拖長</li>
<li><strong>loop_wait + retry_timeout &lt; TTL</strong>：Patroni 必須在 TTL 內成功跟 DCS 互動 N 次、<code>loop_wait=10 + retry_timeout=10</code> 給每個循環 20s buffer</li>
<li><strong>maximum_lag_on_failover</strong>：standby WAL 落後超過這個閾值就 <em>不參與 election</em>；防止「promote 一個落後 5 分鐘的 standby」資料丟失</li>
</ul>
<h2 id="stage-2election--dcs-quorum--watchdog-防-split-brain">Stage 2：Election — DCS quorum + watchdog 防 split-brain</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">watchdog</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">required                               </span><span class="w"> </span><span class="c"># required / automatic / off</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">device</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/watchdog</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">safety_margin</span><span class="p">:</span><span class="w"> </span><span class="m">5</span></span></span></code></pre></div><p>Election 期間最大風險是 <em>split-brain</em> — network partition 下、舊 leader 還活著但跟 DCS 斷線；新 leader 從 standby 升上來、application 同時連兩個 PostgreSQL 寫。資料 divergence 後 <em>無法自動 reconcile</em>。</p>
<p>防護機制兩層：</p>
<ol>
<li><strong>DCS quorum</strong>：etcd / Consul 至少 3 node、過半 quorum 才能寫 leader key — 少數派 partition 無法 elect 新 leader</li>
<li><strong>Watchdog (Linux kernel)</strong>：required mode 強制 — Patroni 必須定期 <em>poke</em> <code>/dev/watchdog</code>、若 Patroni 自己掛或被 OS 凍結、kernel 自動 reboot 整台機器、避免舊 leader 在 DCS 失聯後繼續接寫</li>
</ol>
<p>Watchdog <code>required</code> 是 production-grade 的硬要求 — <code>automatic</code> / <code>off</code> 在 split-brain 場景下無法防護。</p>
<h2 id="stage-3promotion--pg_ctl--replication-slot-切換">Stage 3：Promotion — pg_ctl + replication slot 切換</h2>
<p>新 leader 寫 DCS key 成功後、Patroni 自動執行：</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"># Patroni 內部、不要手動跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pg_ctl promote -D /var/lib/postgresql/data
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># postgresql.auto.conf 移除 primary_conninfo</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># postgresql.auto.conf 重新計算 timeline ID</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 啟動接寫</span></span></span></code></pre></div><p>Promotion 期間關鍵議題：</p>
<ul>
<li><strong>timeline divergence</strong>：新 leader 開新 timeline ID（從 leader 失聯時的 LSN 開始）；其他 standby 需要 <code>pg_rewind</code> 把自己的 WAL fork 點對齊新 timeline</li>
<li><strong>replication slot 處理</strong>：舊 leader 上的 replication slot 在 DCS 中已 stale、新 leader 重建 slot；如果 logical replication consumer 沒 idempotent、會 replay 部分訊息</li>
<li><strong>promotion latency</strong>：通常 3-10 秒（pg_ctl 本身 &lt; 5s、加 DCS 寫確認）</li>
</ul>
<h2 id="stage-4reconfiguration--client-routing-切換">Stage 4：Reconfiguration — client routing 切換</h2>
<p>PostgreSQL 自己升 leader 還不夠、application 不知道；要靠前端 routing 層轉發。三種典型 pattern：</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">[client] → [HAProxy / pgBouncer] → [pg-node-1 (leader)]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                 → [pg-node-2 (standby, read)]
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                 → [pg-node-3 (standby, read)]</span></span></code></pre></div><p>Patroni REST API 暴露 <code>/leader</code> / <code>/replica</code> / <code>/health</code> endpoint、HAProxy 用 <em>health check</em> 跑這些 endpoint：</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"># haproxy.cfg
</span></span><span class="line"><span class="ln">2</span><span class="cl">backend pg-write
</span></span><span class="line"><span class="ln">3</span><span class="cl">  option httpchk OPTIONS /leader
</span></span><span class="line"><span class="ln">4</span><span class="cl">  http-check expect status 200
</span></span><span class="line"><span class="ln">5</span><span class="cl">  server pg-node-1 pg-node-1:5432 check port 8008
</span></span><span class="line"><span class="ln">6</span><span class="cl">  server pg-node-2 pg-node-2:5432 check port 8008 backup
</span></span><span class="line"><span class="ln">7</span><span class="cl">  server pg-node-3 pg-node-3:5432 check port 8008 backup</span></span></code></pre></div><p>Reconfiguration 期間關鍵延遲：</p>
<ul>
<li>HAProxy health check 間隔（預設 2s）+ failure threshold（預設 3 次）= ~6s 切換感應</li>
<li>PgBouncer 不主動 health check、要靠 application 端 retry 跟 connection drop 觸發重連</li>
<li>整個 reconfiguration 端到端通常 10-20s（含 PostgreSQL promotion 時間）</li>
</ul>
<h2 id="stage-5recovery--pg_rewind-跟-base-backup-取捨">Stage 5：Recovery — pg_rewind 跟 base backup 取捨</h2>
<p>舊 leader 恢復後變 standby，但 WAL 已 divergence — 必須選一條 recovery path：</p>
<ul>
<li><strong><code>pg_rewind</code></strong>：rewind 舊 leader WAL 到分歧點、重新接 streaming replication；條件 = 分歧 WAL 量小（&lt; 幾 GB）且 timeline 可對齊</li>
<li><strong>重 base backup</strong>：用 <code>pg_basebackup</code> 從新 leader 拉完整 base + WAL；條件 = 任何時候都可、但時間長（TB 級 1-4 小時）</li>
</ul>
<p>Patroni 預設嘗試 pg_rewind、失敗才退 base backup。production 配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">postgresql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">use_pg_rewind</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">remove_data_directory_on_rewind_failure</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># rewind 失敗自動清 data dir、再 base backup</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">remove_data_directory_on_diverged_timelines</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1split-brain-due-to-dcs-partition">Case 1：Split-brain due to DCS partition</h3>
<p><strong>徵兆</strong>：兩個 PostgreSQL node 都在接寫、application 大量寫入 conflict / unique constraint violation。</p>
<p><strong>根因</strong>：DCS（etcd）partition — 兩個 etcd node 在 partition 兩側、都自認 quorum；其實是 split-vote、兩邊都不應該。Patroni 在兩邊各 elect 一個 leader。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>DCS 必須奇數 node（3 / 5 / 7）、過半 quorum 嚴格 enforce</li>
<li>DCS 部署跨 AZ / region 時、quorum size 要考慮 partition 機率（3 AZ 各 1 node 是 production 最低標）</li>
<li>Watchdog <code>required</code> mode 是最後一道閘門 — DCS partition 加 quorum 失靈時、watchdog 強制 reboot 失聯 node</li>
</ol>
<h3 id="case-2standby-落後太多無法-failover">Case 2：Standby 落後太多、無法 failover</h3>
<p><strong>徵兆</strong>：primary 失聯後、Patroni log 顯示 <code>Following members have lag greater than maximum_lag_on_failover</code>、所有 standby 都被拒 promote、cluster unavailable。</p>
<p><strong>根因</strong>：maximum_lag_on_failover 設 1MB、但 standby replication lag 累積到 50MB（write-heavy workload + slow disk on standby）。安全機制觸發、但代價是 <em>無 standby 可升</em>、需要人工降低門檻或等 standby catch up。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：standby 容量 / IO 對齊 primary、避免 lag 累積；prometheus alert <code>pg_replication_lag_bytes &gt; 10MB</code> 觸發前 catch</li>
<li><strong>臨時</strong>：手動 <code>patronictl edit-config</code> 把 maximum_lag_on_failover 暫時拉到 50MB、接受可能丟 50MB worth of writes、換 availability</li>
<li><strong>長期</strong>：sync replication（一個 standby 強制同步）、保證至少一個 standby zero-lag</li>
</ol>
<h3 id="case-3promotion-後-application-connection-storm">Case 3：Promotion 後 application connection storm</h3>
<p><strong>徵兆</strong>：failover 完成後 30-120 秒內、application log 大量 <code>connection refused</code> / <code>password authentication failed</code>、application 自己 retry storm。</p>
<p><strong>根因</strong>：新 leader 剛 promote、PostgreSQL <code>max_connections</code> 容量還在 warm up（shared memory / cache 未 prime）、application 同時湧入大量 connection request；應用 retry 不夠 jitter、queue 堆積。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Application 用 <em>exponential backoff with jitter</em>、不要 immediate retry</li>
<li>PgBouncer / connection pool 限制每 application instance 對 PG 的 connection 上限、不直連 PG</li>
<li>預先在 standby 跑 <code>pg_prewarm</code> 把熱表 cache 預熱、promotion 後 cache miss 不爆</li>
</ol>
<h3 id="case-4pg_rewind-失敗退到-base-backup-沒做">Case 4：pg_rewind 失敗、退到 base backup 沒做</h3>
<p><strong>徵兆</strong>：舊 leader 恢復後、Patroni log 顯示 <code>pg_rewind failed</code>、舊 leader 一直 STARTING、無法重接 cluster；SRE 手動跑 pg_basebackup 才恢復。</p>
<p><strong>根因</strong>：<code>remove_data_directory_on_rewind_failure: false</code>（預設）— rewind 失敗時 Patroni 不主動清 data dir、需要 SRE 手動處理；運維沒 runbook、卡在這步幾小時。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Production 設 <code>remove_data_directory_on_rewind_failure: true</code> + <code>remove_data_directory_on_diverged_timelines: true</code>、讓 Patroni 自動 fallback</li>
<li>data dir 跑在獨立 PV / disk、清掉風險可控（不要跑 root disk）</li>
<li>容量規劃：base backup 時間預估納入 RTO（TB 級 base backup 1-4 小時、不是 RTO 30 分鐘所能承受）</li>
</ol>
<h3 id="case-5watchdog-觸發整機-reboot誤殺">Case 5：Watchdog 觸發整機 reboot、誤殺</h3>
<p><strong>徵兆</strong>：production server 在無故障時 unexpected reboot、<code>dmesg</code> 顯示 <code>watchdog: BUG: soft lockup</code>。</p>
<p><strong>根因</strong>：Patroni 主循環因 etcd 短暫慢回應卡住 60+ 秒、kernel watchdog 觸發 reboot；但實際 PostgreSQL 沒 hang、是 Patroni-watchdog 鏈過敏。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>safety_margin</code> 設大一點（10-15）、給 Patroni loop_wait 抖動空間</li>
<li>etcd 跟 Patroni 部署在低延遲 network 內（同 AZ &lt; 5ms）、跨 region etcd 不建議</li>
<li>watchdog device 用 softdog（軟體模擬）vs 硬體 watchdog、debug 時 softdog 容易觀察</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster size</td>
          <td>3-5 node（含 leader + 2-4 standby）</td>
          <td>&lt; 3 不能 HA（單 standby 失敗整 cluster 掛）</td>
      </tr>
      <tr>
          <td>DCS size</td>
          <td>3 / 5 / 7 node（奇數 quorum）</td>
          <td>etcd 5 node 是 prod standard</td>
      </tr>
      <tr>
          <td>TTL</td>
          <td>30s（default 30、production 20-60）</td>
          <td>&lt; 15s 過敏、&gt; 60s 過鈍</td>
      </tr>
      <tr>
          <td>maximum_lag_on_failover</td>
          <td>1MB（default）</td>
          <td>大表 write-heavy 可放 10-100MB</td>
      </tr>
      <tr>
          <td>Synchronous standby</td>
          <td>1 個 sync + N 個 async 是 production 預設</td>
          <td>全 async 容易丟資料、全 sync write latency 爆</td>
      </tr>
      <tr>
          <td>RTO</td>
          <td>10-30 秒（detection 30s 內 + promotion 5-10s + reconfig 5s）</td>
          <td>&gt; 60s 要 audit 鏈路</td>
      </tr>
      <tr>
          <td>RPO</td>
          <td>sync mode 接近 0、async mode 跟 lag 同數量級</td>
          <td>async 在 disk IO 慢時 lag 可能 MB-GB level</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-pgbouncer-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> 整合</h3>
<p>PgBouncer 不主動感知 Patroni failover、要靠：</p>
<ol>
<li><strong>HAProxy 在 PgBouncer 上層</strong>：HAProxy 跑 Patroni health check、PgBouncer connection 重新路由</li>
<li><strong>PgBouncer reload</strong>：failover 後 SRE / automation 跑 <code>pgbouncer -R</code>、強制重連 backend</li>
<li><strong>Connection pool drain</strong>：application 端 connection pool 設 <code>pool_lifetime_max=5min</code>、舊 connection 自然汰換</li>
</ol>
<h3 id="跟-cert-managertls-rotation">跟 cert-manager（TLS rotation）</h3>
<p>Patroni REST API 跟 PostgreSQL streaming replication 都用 TLS、cert rotation 不能停服務：</p>
<ol>
<li>cert-manager 自動換證後、Patroni 跟 PostgreSQL 都需要 reload（不是 restart）</li>
<li><code>patronictl reload &lt;cluster&gt;</code> 不會觸發 failover、只 reload config</li>
<li>PostgreSQL <code>pg_ctl reload</code> 是 SIGHUP、平滑載入新 cert</li>
</ol>
<h3 id="跟-backup--pitr">跟 backup / PITR</h3>
<p>Patroni 不管 backup — 但 standby promotion 後、WAL archive 必須跟新 leader 的 timeline 對齊：</p>
<ol>
<li>WAL archive 命令模板含 <code>%t</code>（timeline）：<code>archive_command = 'wal-g wal-push %p'</code></li>
<li>Backup tool（pgBackRest / WAL-G）支援 timeline 切換、archive 不會中斷</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL archiving deep article</a></li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Multi-region Patroni</strong>：跨 region 部署的 DCS quorum 設計、跟單 region 的取捨完全不同</li>
<li><strong>PostgreSQL 16+ streaming replication slot 持久化</strong>：簡化 standby promotion 後 logical consumer 重連</li>
<li><strong>跟 Kubernetes operator 整合</strong>：Patroni 跑在 K8s 時、StatefulSet + pod identity + DCS 部署模式</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">High Concurrency Access</a> — connection / replication / HA 全鏈</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>SPIRE</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/spire/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/spire/</guid><description>&lt;p>SPIRE（SPIFFE Runtime Environment）是 SPIFFE 規範的 reference 實作、CNCF graduated 專案、解決 &lt;em>workload identity attestation&lt;/em> 的核心問題：在 service mesh / 跨 cluster / 跨組織的環境裡、一個 workload 必須能 &lt;em>被驗證&lt;/em> 它是誰（是哪個 namespace 的哪個 service account、跑在哪台 attested host 上）、而不是依靠 IP / hostname / 共用 API key 這種可偽造的識別。SPIRE 發出的識別憑證叫 &lt;em>SVID&lt;/em>（SPIFFE Verifiable Identity Document）、識別格式是 URI 形式的 &lt;em>SPIFFE ID&lt;/em>（例如 &lt;code>spiffe://example.org/ns/prod/sa/api-gateway&lt;/code>）、TTL 是分鐘級短期、workload 透過本地 Unix socket（Workload API）持續拉新 SVID、不 mount file 一勞永逸。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>SPIRE 的核心定位是 &lt;em>attestation-first 的 workload identity 控制面&lt;/em>、解的問題是「這個 workload 在執行時是不是它聲稱的那個」— 識別語意是 &lt;em>attested SPIFFE ID&lt;/em>、不是 DNS name 也不是 cluster-internal ServiceAccount。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&amp;#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &amp;#43; Challenge solver">cert-manager&lt;/a> 的 &lt;em>cert lifecycle&lt;/em>（DNS name 為主）、Kubernetes ServiceAccount 的 &lt;em>cluster-internal scope&lt;/em>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> AppRole 的 &lt;em>pull-based secret&lt;/em>（workload 要先持有 secret_id）都解不同問題、不是替代關係。&lt;/p>
&lt;p>跟雲端 workload identity（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM Roles Anywhere&lt;/a> / GCP Workload Identity Federation / Azure Federated Identity Credential）相比、SPIRE 多了 &lt;em>跨雲統一抽象&lt;/em> + &lt;em>跨組織 federation&lt;/em>（兩個 SPIRE deployment 互相信任只需要交換 trust bundle）。代價是 &lt;em>自管控制面&lt;/em>（SPIRE Server HA + Agent rollout + Registration Entry 維護）。詳細跟其他 vendor 的場景對比見「核心取捨表」與「何時改走其他服務」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>何時用 SPIRE（zero-trust mesh、跨 cluster / 跨組織 federation、需要 attestation）、何時用 cert-manager + Service Account / cloud-native workload identity 就夠&lt;/li>
&lt;li>SPIRE deployment 的最低安全骨架（Server / Agent 拓樸、Node Attestor、Workload Attestor、Registration Entry、SVID TTL）&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> / Istio / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> Roles Anywhere 的整合形狀&lt;/li>
&lt;li>失敗模式如何排錯（Attestor 設計太寬、SVID 過期、Trust Bundle 不同步）&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 SPIRE deployment 是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>SPIRE（SPIFFE Runtime Environment）是 SPIFFE 規範的 reference 實作、CNCF graduated 專案、解決 <em>workload identity attestation</em> 的核心問題：在 service mesh / 跨 cluster / 跨組織的環境裡、一個 workload 必須能 <em>被驗證</em> 它是誰（是哪個 namespace 的哪個 service account、跑在哪台 attested host 上）、而不是依靠 IP / hostname / 共用 API key 這種可偽造的識別。SPIRE 發出的識別憑證叫 <em>SVID</em>（SPIFFE Verifiable Identity Document）、識別格式是 URI 形式的 <em>SPIFFE ID</em>（例如 <code>spiffe://example.org/ns/prod/sa/api-gateway</code>）、TTL 是分鐘級短期、workload 透過本地 Unix socket（Workload API）持續拉新 SVID、不 mount file 一勞永逸。</p>
<h2 id="服務定位">服務定位</h2>
<p>SPIRE 的核心定位是 <em>attestation-first 的 workload identity 控制面</em>、解的問題是「這個 workload 在執行時是不是它聲稱的那個」— 識別語意是 <em>attested SPIFFE ID</em>、不是 DNS name 也不是 cluster-internal ServiceAccount。跟 <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> 的 <em>cert lifecycle</em>（DNS name 為主）、Kubernetes ServiceAccount 的 <em>cluster-internal scope</em>、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> AppRole 的 <em>pull-based secret</em>（workload 要先持有 secret_id）都解不同問題、不是替代關係。</p>
<p>跟雲端 workload identity（<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM Roles Anywhere</a> / GCP Workload Identity Federation / Azure Federated Identity Credential）相比、SPIRE 多了 <em>跨雲統一抽象</em> + <em>跨組織 federation</em>（兩個 SPIRE deployment 互相信任只需要交換 trust bundle）。代價是 <em>自管控制面</em>（SPIRE Server HA + Agent rollout + Registration Entry 維護）。詳細跟其他 vendor 的場景對比見「核心取捨表」與「何時改走其他服務」。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>何時用 SPIRE（zero-trust mesh、跨 cluster / 跨組織 federation、需要 attestation）、何時用 cert-manager + Service Account / cloud-native workload identity 就夠</li>
<li>SPIRE deployment 的最低安全骨架（Server / Agent 拓樸、Node Attestor、Workload Attestor、Registration Entry、SVID TTL）</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / Istio / <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> Roles Anywhere 的整合形狀</li>
<li>失敗模式如何排錯（Attestor 設計太寬、SVID 過期、Trust Bundle 不同步）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 SPIRE deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Server / Agent 拓樸</strong>：SPIRE Server 是 trust domain 的 root、發 SVID、簽 Trust Bundle；SPIRE Agent 跑在每個 host / node 上、向 Server 註冊、為本機 workload attest 身份。Server HA（多副本 + 共享 DB）跟 Agent rollout coverage 缺一就會出現 <em>節點上 workload 拿不到 SVID</em>。</li>
<li><strong>Attestor 設計</strong>：Node Attestor 驗 <em>這台 host 是真的</em>（K8s SAT / AWS IID / Azure MSI / GCP IIT / TPM 等）、Workload Attestor 驗 <em>這個 process 是誰</em>（K8s pod selector、unix UID/GID、systemd unit）。Selector 太寬等於整個 namespace 任何 pod 都拿同一個 SPIFFE ID、blast radius 失控。</li>
<li><strong>SVID lifetime</strong>：X.509-SVID 預設 TTL 1 小時、production 建議 5–15 分鐘；workload 必須走 Workload API（Unix socket）持續拉新 SVID、不能 mount 成 file。Workload 不支援 SDK 整合就被擋在 SPIRE 之外。</li>
<li><strong>Registration Entry</strong>：定義「哪個 SPIFFE ID 可以被哪個 attestation selector 取得」、是 SPIRE 的 <em>authorization 設計核心</em>。一個 entry 寫錯（selector 用了 <code>k8s:ns:default</code> 沒鎖 service account）就等於 default namespace 任何 pod 都拿 admin SPIFFE ID。</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">Workload Identity and Federated Trust</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Server / Agent 拓樸</strong>：SPIRE Server 是 trust domain 的 root CA + 註冊中心、必須 HA（至少兩副本 + 共享 PostgreSQL / MySQL）、production 通常每個 cluster 一個 Server cluster；SPIRE Agent 以 DaemonSet 跑在每個 K8s node、或以 systemd unit 跑在每台 VM、負責本機的 attestation 與 SVID 派發。Agent 跟 Server 之間用 mutual TLS、Agent 自己也走 Node Attestation 才能向 Server 註冊。</p>
<p><strong>Node Attestor</strong>：決定「這個 Agent 是不是真的跑在它聲稱的 host 上」。K8s SAT / PSAT（projected service account token）驗 Agent 的 ServiceAccount + Pod；AWS IID 驗 EC2 instance identity document；GCP IIT 驗 GCE metadata；Azure MSI 驗 Managed Identity；TPM attestor 驗硬體 TPM 簽章。選錯 attestor 等於 host 識別被偽造 — 例如 K8s SAT 沒鎖 audience、外部能用任何 K8s SA token 註冊 fake Agent。</p>
<p><strong>Workload Attestor</strong>：決定「這個 process 是哪個 workload」。Kubernetes attestor 用 pod label / annotation / namespace / service account；Unix attestor 用 UID / GID / parent process / binary hash；Docker attestor 用 container label / image。Workload 連到 Agent 的 Workload API Unix socket、Agent 透過 attestor 收集 selector、比對 Registration Entry、決定能發哪個 SPIFFE ID。Selector 設計是 <em>least privilege</em> 的 enforcement point — 寫得越精確、blast radius 越小。</p>
<p><strong>Registration Entry</strong>：定義 SPIFFE ID 到 selector 的 mapping、例如「<code>spiffe://example.org/ns/prod/sa/api-gateway</code> 對應 <code>k8s:ns:prod</code>、<code>k8s:sa:api-gateway</code>、<code>k8s:pod-label:app:api-gateway</code>」。Entry 透過 SPIRE Server API 或 GitOps 維護、變更走 PR review（policy-as-code）、避免單一 admin 偷加 entry 拿 admin SPIFFE ID。</p>
<p><strong>SVID 生命週期</strong>：X.509-SVID 是 mTLS 用的 cert（含 SPIFFE ID 作 URI SAN）、JWT-SVID 是給 non-mTLS 場景（HTTP header bearer token、跟 OIDC 整合）。workload 透過 Workload API stream 接 SVID、TTL 過半就 Agent 主動 push 新 SVID — workload 不需要自己排程 renew。Trust Bundle（trust domain 的 root cert）也透過 Workload API 同步、自動更新。</p>
<p><strong>Federation between trust domains</strong>：兩個獨立 SPIRE deployment（不同組織、不同 trust domain）要互信、交換 <em>trust bundle</em>（自簽 root cert）、走 SPIFFE Federation API。<code>example.org</code> 的 workload 可以驗證 <code>partner.com</code> 的 SVID、不需要共用 PKI、不需要在中間放 broker。對應 <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">Workload Identity and Federated Trust</a> 的 federation 章節。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>SPIRE</th>
          <th>cert-manager</th>
          <th>Kubernetes ServiceAccount</th>
          <th>Vault AppRole</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>識別語意</td>
          <td>Attested SPIFFE ID（who is this workload）</td>
          <td>DNS name（who owns this name）</td>
          <td>Cluster-internal SA name</td>
          <td>Pull-based role + secret_id</td>
      </tr>
      <tr>
          <td>信任邊界</td>
          <td>Trust domain、可跨 cluster / cloud / 組織</td>
          <td>Cluster 內、外部走 ACME / Vault PKI</td>
          <td>單一 cluster</td>
          <td>Vault 範圍內</td>
      </tr>
      <tr>
          <td>Attestation</td>
          <td>First-class — Node + Workload Attestor 雙層</td>
          <td>無 — 僅驗 DNS / cert request</td>
          <td>TokenReview API、cluster-scoped</td>
          <td>無 — secret_id 即是 proof</td>
      </tr>
      <tr>
          <td>Cert TTL</td>
          <td>分鐘級短期、Workload API 自動 rotate</td>
          <td>天 / 月級、cert-manager 排程 renew</td>
          <td>Token TTL（projected: 短）</td>
          <td>Token TTL（lease 治理）</td>
      </tr>
      <tr>
          <td>Workload 改動</td>
          <td>需走 SPIFFE Workload API SDK 或 sidecar</td>
          <td>Mount file 即可</td>
          <td>Mount file 即可</td>
          <td>拉 secret_id + 換 token</td>
      </tr>
      <tr>
          <td>跨組織 federation</td>
          <td>強 — 交換 trust bundle 即可</td>
          <td>弱 — 需共用 CA 或 ACME</td>
          <td>不支援</td>
          <td>弱 — 需共用 Vault 或 OIDC bridge</td>
      </tr>
      <tr>
          <td>運維成本</td>
          <td>高 — Server HA + Agent rollout + Entry 治理</td>
          <td>低 — Operator 模式</td>
          <td>內建</td>
          <td>中 — Vault 自管</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Zero-trust mesh、跨 cluster / 跨組織、需要 attestation</td>
          <td>K8s app cert lifecycle、ACME / Vault issuer</td>
          <td>Cluster-internal 簡單 app</td>
          <td>不在雲 metadata 內的 workload</td>
      </tr>
  </tbody>
</table>
<p>選 SPIRE 的核心訴求：<em>需要 attested workload identity</em>（不只是「有 cert 就信」）+ <em>跨 cluster 或跨組織</em>（單 cluster 內 ServiceAccount 已夠）+ <em>workload 能整合 SPIFFE SDK 或 sidecar</em>。三個條件缺一就先用 cert-manager + ServiceAccount 組合、別硬上 SPIRE。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>跟 Istio / Linkerd / Envoy 整合</strong>：Istio 1.14+ 支援 SPIRE 作 identity provider（取代 Citadel）、Envoy SDS（Secret Discovery Service）走 SPIRE Workload API 拉 SVID、service mesh 內 mTLS 用 SPIFFE ID 做 peer 驗證 + authz policy（<code>source.principal == &quot;spiffe://example.org/ns/prod/sa/api-gateway&quot;</code>）。Linkerd 也有實驗性整合（policy controller 接受 SPIFFE ID）。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 整合</strong>：Vault 可以用 SPIRE JWT-SVID 作 auth method、workload 拿 SVID 換 Vault token、不需要 AppRole secret_id — 等於把 Vault auth 的 <em>bootstrap secret 問題</em> 交給 SPIRE attestation 處理。workload 同時拿 SPIFFE 身份（mTLS）跟 Vault secret（DB credential、PKI cert）、兩條鏈共用同一個 attestation root。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> Roles Anywhere 整合</strong>：AWS IAM Roles Anywhere 接受 X.509 cert 換 IAM credential、SPIRE 發的 X.509-SVID 可以當這個 cert — non-AWS workload（on-prem、其他雲、CI runner）用 SPIFFE ID 拿 short-term AWS STS credential、不需要存 long-lived AWS access key。</p>
<p><strong>Nested SPIRE（多層 trust domain）</strong>：大型組織把 trust domain 切成 <em>parent + child</em>（例如 <code>example.org</code> 作 parent、每個 BU 各自 <code>bu1.example.org</code>、<code>bu2.example.org</code>）、child Server 向 parent Server 註冊作 downstream、child trust domain 的 workload 還是被 parent root 信任。適合需要 <em>部門自治 + 全公司互通</em> 的場景。</p>
<p><strong>JWT-SVID 給 non-mTLS workload</strong>：HTTP service 不一定能跑 mTLS（CDN 後面、legacy app）、SPIRE 發 JWT-SVID（標準 JWT、aud / sub claim、SPIFFE ID 在 sub）給這類 workload、走 HTTP <code>Authorization: Bearer</code> 傳遞、收方驗 SPIRE trust bundle 簽章。代價是失去 mTLS 的 mutual auth、需要 application-level 驗 JWT-SVID。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Workload Attestor selector 太寬</strong>：Entry 只鎖 <code>k8s:ns:prod</code> 沒鎖 <code>k8s:sa:*</code> — namespace 內任何 pod 都拿同一個 admin SPIFFE ID。修法：selector 必含 namespace + service account + （建議）pod label，policy review 走 GitOps PR。</li>
<li><strong>SVID 過期但 workload 沒接 Workload API</strong>：workload 把 SVID dump 成 file 後不再連 Workload API、TTL 過期之後 mTLS 失敗 — workload 必須用 SPIFFE SDK 或 sidecar（envoy / spiffe-helper）持續 stream SVID。</li>
<li><strong>Node Attestor audience 未鎖</strong>：K8s SAT attestor 沒設 <code>audience</code>、外部能用任何 K8s SA token 註冊 fake Agent — 改用 PSAT（projected SA token）+ 明確 audience 鎖到 SPIRE Server URL。</li>
<li><strong>Trust Bundle 不同步</strong>：federation 對端 rotate root cert、本端沒抓到新 bundle、跨 trust domain mTLS 失敗 — federation endpoint 必須走 HTTPS + 定期 refresh、SPIRE Server metric 監控 federation fetch 失敗。</li>
<li><strong>Registration Entry 漂移</strong>：手動加的 entry 沒進 GitOps、admin 離職後沒人知道為何某個 SPIFFE ID 存在 — entry 必須走 declarative source（YAML in Git）+ CI apply、禁止直接 <code>spire-server entry create</code>。</li>
<li><strong>Server DB 單點</strong>：SPIRE Server SQLite mode 跑在 production、節點掛了 = 整個 trust domain 不能發 SVID — production 必走 PostgreSQL / MySQL + HA Server 副本。</li>
<li><strong>Audit log gap</strong>：SPIRE Server audit log 沒接 SIEM、SVID 派發紀錄 7 天後輪轉掉、事故時無法回查誰拿過 admin SPIFFE ID — audit log 同步到外部 SIEM 是基本要求、對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 卡。</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 cluster 簡單 K8s app + DNS-named cert</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> + Kubernetes ServiceAccount</td>
      </tr>
      <tr>
          <td>公開 serving cert（HTTPS endpoint）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a> / <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a></td>
      </tr>
      <tr>
          <td>Static secret + dynamic credential</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></td>
      </tr>
      <tr>
          <td>AWS-only workload + IAM role</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> IRSA / Roles Anywhere</td>
      </tr>
      <tr>
          <td>GCP-only workload</td>
          <td>GCP Workload Identity Federation</td>
      </tr>
      <tr>
          <td>純 human identity / SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a> / <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></td>
      </tr>
      <tr>
          <td>跨組織 OIDC federation（human + machine）</td>
          <td><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">Workload Identity and Federated Trust</a>（章節層）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>SPIFFE 規範完整逐條解讀（spec 各 section 細節）</li>
<li>SPIRE Server / Agent 完整 CLI 與 config reference</li>
<li>每個 Attestor plugin 的內部實作細節</li>
<li>Istio / Linkerd / Envoy 整合的完整步驟（屬 service mesh 章節）</li>
<li>SPIFFE Helper / spire-agent sidecar 各語言 SDK 用法</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>SPIRE 在 07 案例庫沒有直接 vendor-level 事件、以下為對照引用：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 SPIRE 的關係（對照）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">Workload Identity and Federated Trust (section)</a></td>
          <td>SPIRE 是 federation 信任邊界的具體實作 — 跨 trust domain 交換 bundle 是 SPIFFE federation 的標準形狀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>對照啟示 — JWT-SVID 是 <em>short-lived + attested</em> 設計、跟 Storm-0558 的 long-lived signing key 是相反 mindset；attestation + 分鐘級 TTL 限制了 key 外洩後的 blast radius</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain (red-team)</a></td>
          <td>對照啟示 — 傳統 OAuth token 過寬 + 過長、SPIRE 設計是 short TTL + scope-narrow SPIFFE ID + Registration Entry 走 declarative authz、把 secret-leak 路徑收掉</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">Workload Identity and Federated Trust</a>、<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity &amp; Access Boundary</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>（DNS-named cert lifecycle）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（用 SPIRE JWT-SVID 作 Vault auth method）、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>（Roles Anywhere 接 SPIRE 發的 X.509-SVID）</li>
<li>下游：service mesh（Istio / Linkerd / Envoy）整合層、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
<li>官方：<a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-overview/">SPIFFE Specification</a>、<a href="https://spiffe.io/docs/latest/spire-about/">SPIRE Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a>（target）兩個 vendor overview。Migration playbook 跟 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a> 的 6-section flow 不同 — 是 &lt;em>phased process&lt;/em>（audit → translation → parallel run → cutover → cleanup）、強調 &lt;em>時間軸&lt;/em> 跟 &lt;em>回退邊界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷cost--multi-vendor--cloud-native-三條-driver">為什麼遷：cost / multi-vendor / cloud-native 三條 driver&lt;/h2>
&lt;p>Splunk → Elastic 遷移在 2022+ 變主流選項、driver 通常三條疊加：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Cost&lt;/strong>&lt;/td>
 &lt;td>Splunk per-GB ingest pricing 在 5+ TB/day 規模累積到無法接受、Elastic fixed-tier pricing 可省 50-70%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Multi-vendor&lt;/strong>&lt;/td>
 &lt;td>想避免 SIEM lock-in、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Sentinel&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 同時跑形成 portfolio&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Cloud-native&lt;/strong>&lt;/td>
 &lt;td>已用 Elasticsearch / Kibana 做 application observability、想統一 stack 走 Elastic Cloud / ECK&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（Elastic → Splunk）也存在但少數 — 主要是 &lt;em>合規 / 政府客戶要 Splunk Cloud GovCloud&lt;/em>、或 &lt;em>Splunk Premium ES 的 RBA + UEBA 成熟度仍領先&lt;/em>。本文聚焦 Splunk → Elastic、反向流程結構相同但 &lt;em>schema 對位方向相反&lt;/em>。&lt;/p>
&lt;h2 id="結構phased-migration-不是-6-section-deep-article">結構：phased migration 不是 6-section deep article&lt;/h2>
&lt;p>跟 single-feature deep article（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &amp;#43; case management 整合">Splunk RBA&lt;/a>、Vault dynamic credential）不同、migration playbook 的核心是 &lt;em>time-sequenced phase&lt;/em> + &lt;em>回退邊界&lt;/em>。6 段 phase：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（source）跟 <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（target）兩個 vendor overview。Migration playbook 跟 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a> 的 6-section flow 不同 — 是 <em>phased process</em>（audit → translation → parallel run → cutover → cleanup）、強調 <em>時間軸</em> 跟 <em>回退邊界</em>。</p></blockquote>
<h2 id="為什麼遷cost--multi-vendor--cloud-native-三條-driver">為什麼遷：cost / multi-vendor / cloud-native 三條 driver</h2>
<p>Splunk → Elastic 遷移在 2022+ 變主流選項、driver 通常三條疊加：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Cost</strong></td>
          <td>Splunk per-GB ingest pricing 在 5+ TB/day 規模累積到無法接受、Elastic fixed-tier pricing 可省 50-70%</td>
      </tr>
      <tr>
          <td><strong>Multi-vendor</strong></td>
          <td>想避免 SIEM lock-in、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Sentinel</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 同時跑形成 portfolio</td>
      </tr>
      <tr>
          <td><strong>Cloud-native</strong></td>
          <td>已用 Elasticsearch / Kibana 做 application observability、想統一 stack 走 Elastic Cloud / ECK</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Elastic → Splunk）也存在但少數 — 主要是 <em>合規 / 政府客戶要 Splunk Cloud GovCloud</em>、或 <em>Splunk Premium ES 的 RBA + UEBA 成熟度仍領先</em>。本文聚焦 Splunk → Elastic、反向流程結構相同但 <em>schema 對位方向相反</em>。</p>
<h2 id="結構phased-migration-不是-6-section-deep-article">結構：phased migration 不是 6-section deep article</h2>
<p>跟 single-feature deep article（<a href="/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &#43; case management 整合">Splunk RBA</a>、Vault dynamic credential）不同、migration playbook 的核心是 <em>time-sequenced phase</em> + <em>回退邊界</em>。6 段 phase：</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>內容</th>
          <th>預估時長</th>
          <th>回退邊界</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Phase 0：rule audit</strong></td>
          <td>盤點 Splunk 端 rule、量化 precision / FP rate / alert volume</td>
          <td>1-2 週</td>
          <td>不影響 production</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Phase 1：schema 對位</strong></td>
          <td>SPL ↔ KQL / ES</td>
          <td>QL、CIM ↔ ECS、index ↔ data view 對應規格</td>
          <td>1-2 週</td>
          <td>不影響 production</td>
      </tr>
      <tr>
          <td><strong>Phase 2：translation</strong></td>
          <td>rule 一條條轉、AI-assisted + 人工 verify</td>
          <td>4-12 週</td>
          <td>翻譯失敗的 rule 退回 manual / 標 deferred</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Phase 3：parallel run</strong></td>
          <td>兩 SIEM 同時跑、alert 兩邊產出、累積 confidence</td>
          <td>4-8 週</td>
          <td>切回單 Splunk、Elastic 端關 alert</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Phase 4：cutover</strong></td>
          <td>alert routing 切到 Elastic、Splunk 仍 ingest 但不送 alert</td>
          <td>1 週</td>
          <td>routing 切回 Splunk、半小時內可逆</td>
          <td></td>
      </tr>
      <tr>
          <td><strong>Phase 5：cleanup</strong></td>
          <td>Splunk ingest 停、歷史資料 archive 到 S3、license decommission</td>
          <td>2-4 週</td>
          <td><strong>不可逆</strong> — 過早走會失去歷史查詢能力</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>整個遷移週期 4-9 個月、跟 single deep article 1-2 小時完全不同 scale。</p>
<h2 id="phase-0rule-audit-建-baseline">Phase 0：rule audit 建 baseline</h2>
<p>遷移前必須先知道 <em>current state</em>：</p>





<pre tabindex="0"><code class="language-spl" data-lang="spl">-- Splunk rule 盤點
| rest /servicesNS/-/-/saved/searches
  splunk_server=local search=&#34;alert&#34;
| where disabled=0
| eval rule_age=now()-strptime(updated, &#34;%Y-%m-%dT%H:%M:%S&#34;)
| stats count, avg(rule_age) by app, owner</code></pre><p>每條 rule 量化四個指標：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>怎麼算</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert volume / day</td>
          <td><code>index=_audit action=alert_fired rule_name=X</code> 過 30 天</td>
          <td>高 volume 先翻、cutover 期間影響大</td>
      </tr>
      <tr>
          <td>Precision (TP / total)</td>
          <td>SOC review 過去 30 天 alert、標 TP / FP / unknown</td>
          <td>低 precision 先翻（藉機 fix、不是直接複製問題）</td>
      </tr>
      <tr>
          <td>Detection coverage</td>
          <td>對應 MITRE ATT&amp;CK technique</td>
          <td>確認 Elastic 端有對應 coverage、不能漏 tactic</td>
      </tr>
      <tr>
          <td>Owner / 維護狀態</td>
          <td>rule 的 owner team + 最後 update 時間</td>
          <td>Owner 失聯的 rule 翻譯成本爆、考慮直接退役</td>
      </tr>
  </tbody>
</table>
<p><strong>Audit 階段的關鍵決策：哪些 rule 不翻譯</strong> — production 通常 30-50% rule 是 legacy / dead code / 已 deprecated；遷移是 <em>清理機會</em>、不是「全部複製過去」。</p>
<h2 id="phase-1schema-對位">Phase 1：Schema 對位</h2>
<p>Splunk 跟 Elastic 的 data model 沒有 1:1 mapping、必須先建對位 spec：</p>
<table>
  <thead>
      <tr>
          <th>Splunk concept</th>
          <th>Elastic 對應</th>
          <th>對位難度</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SPL search language</td>
          <td>KQL（簡單）/ ES</td>
          <td>QL（複雜 query、PG 14+ piped）</td>
          <td>中、語法差距大但概念對齊</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>Data view（read）/ data stream（write）</td>
          <td>低、概念相同</td>
          <td></td>
      </tr>
      <tr>
          <td>CIM data model</td>
          <td>Elastic Common Schema (ECS)</td>
          <td>中、欄位命名差、有對照表（CIM→ECS open source）</td>
          <td></td>
      </tr>
      <tr>
          <td>Macros</td>
          <td>Runtime fields / transforms / ingest pipeline</td>
          <td>高、Splunk macro 是 SPL fragment、Elastic 沒對等概念</td>
          <td></td>
      </tr>
      <tr>
          <td>Lookups</td>
          <td>Enrich processors / lookup index</td>
          <td>中、邏輯對等但 lifecycle 管法不同</td>
          <td></td>
      </tr>
      <tr>
          <td>Correlation search</td>
          <td>Detection rule（KQL / EQL / Threshold / ML）</td>
          <td>中、Splunk 一條 search、Elastic 拆 rule type</td>
          <td></td>
      </tr>
      <tr>
          <td>Summary index</td>
          <td>Transform / rollup</td>
          <td>高、Splunk <code>tstats</code> summary index 概念複雜</td>
          <td></td>
      </tr>
      <tr>
          <td>Notable event</td>
          <td>Alert + signal（Security app）</td>
          <td>低、Elastic 7.x+ 已成熟</td>
          <td></td>
      </tr>
      <tr>
          <td>Saved search</td>
          <td>Saved query</td>
          <td>低</td>
          <td></td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Kibana dashboard</td>
          <td>中、Splunk XML/SimpleXML 跟 Kibana JSON 不可直接轉</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p><strong>Field mapping 是最大坑</strong>：Splunk 自由 schema（<code>extract</code> runtime）vs Elastic 強 type ECS。Splunk 端 <code>src_ip</code> 可能是 string；Elastic 端必須 <code>source.ip</code> 是 <code>ip</code> type — 任何 ingest pipeline 都要先把 raw event 轉成 ECS 結構。</p>
<h2 id="phase-2translation-pipeline">Phase 2：Translation pipeline</h2>
<p>實務 translation 用 <em>3-tier hybrid</em>：</p>
<h3 id="tier-1-vendor-toolcover-30-50">Tier 1: vendor tool（cover 30-50%）</h3>
<p>Elastic 官方提供 <code>splunk-to-elastic</code> migration assistant（SaaS / on-prem）— 對 <em>簡單 SPL search</em> 自動轉 KQL；cover ratio 視 SPL 複雜度而定。</p>
<h3 id="tier-2-llm-assistedcover-30-40">Tier 2: LLM-assisted（cover 30-40%）</h3>
<p>對 <em>中等複雜</em> SPL（含 stats / eval / where）、用 Claude / GPT 翻譯：</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">prompt template:
</span></span><span class="line"><span class="ln">2</span><span class="cl">&#34;Convert this Splunk SPL to Elastic ES|QL. Preserve detection logic. List any
</span></span><span class="line"><span class="ln">3</span><span class="cl">unmappable functions.
</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">SPL:
</span></span><span class="line"><span class="ln">6</span><span class="cl">index=auth action=login user=* | bucket _time span=5m
</span></span><span class="line"><span class="ln">7</span><span class="cl">| stats count by user, src_ip, _time | where count &gt; 10&#34;</span></span></code></pre></div><p>LLM output 必須 <em>人工 verify</em>：</p>
<ul>
<li>對相同樣本資料跑 SPL vs ES|QL、output 對齊</li>
<li>FP rate 不能 <em>惡化</em></li>
<li>Threshold / window 對等（5m window 跟 5m window 對應）</li>
</ul>
<h3 id="tier-3-manualcover-10-30">Tier 3: manual（cover 10-30%）</h3>
<p>剩下的是：</p>
<ul>
<li>含 macro 跨 SPL fragment 的 rule（macro 必須先展開或 inline）</li>
<li>含 summary index 跟 tstats 的高效能 rule</li>
<li>用 <code>transaction</code> / <code>streamstats</code> 的 stateful query</li>
</ul>
<p>這類 rule 翻譯成 KQL 邏輯後、通常 <em>效能差 5-20x</em>（Splunk summary index 是 precomputed、KQL 是 runtime）；要評估 <em>改用 Elastic transform</em> 或 <em>接受效能下降</em>。</p>
<h2 id="phase-3parallel-run">Phase 3：Parallel run</h2>
<p>雙 SIEM 同時跑是 <em>最重要的 confidence-building 階段</em>：</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">                 ┌─→ Splunk ──→ alert ──┐
</span></span><span class="line"><span class="ln">2</span><span class="cl">data source ─┤                          ├─→ alert dedup ──→ SOAR / SOC
</span></span><span class="line"><span class="ln">3</span><span class="cl">                 └─→ Elastic ──→ alert ─┘</span></span></code></pre></div><p>Dedup 策略：</p>
<ul>
<li><strong>Key</strong>：<code>rule_name + event_id + timestamp_5min_bucket</code></li>
<li><strong>Window</strong>：5-10 分鐘（兩端有不同處理 latency）</li>
<li><strong>Routing</strong>：dedup 後送 SOAR、SOC 看「來自哪個 SIEM」標籤</li>
</ul>
<p>跑 4-8 週累積：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>期望</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert coverage 一致性</td>
          <td>Elastic 抓到 Splunk 的 95%+ 對應 alert</td>
      </tr>
      <tr>
          <td>FP rate 不惡化</td>
          <td>Elastic FP / Splunk FP ≤ 1.2（允許 20% 浮動）</td>
      </tr>
      <tr>
          <td>Detection latency 對等</td>
          <td>Elastic 端 alert 時間在 Splunk 端 ± 5 分鐘內</td>
      </tr>
      <tr>
          <td>Volume / day</td>
          <td>Alert 總數兩端對齊（10% 內）</td>
      </tr>
  </tbody>
</table>
<p>不對齊的 rule 退回 Phase 2 重新 translation；累積到 95%+ 對齊才能進 Phase 4。</p>
<h2 id="phase-4cutover--routing-切換">Phase 4：Cutover — routing 切換</h2>





<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">Before cutover:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Splunk → SOAR (active routing)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Elastic → SOAR (parallel, marked test)
</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">After cutover:
</span></span><span class="line"><span class="ln">6</span><span class="cl">  Splunk → ingest 持續 / alert disabled
</span></span><span class="line"><span class="ln">7</span><span class="cl">  Elastic → SOAR (active routing)</span></span></code></pre></div><p>Cutover 期間：</p>
<ol>
<li>PagerDuty / Opsgenie 端 <em>先建 Elastic integration</em>、不立刻 disable Splunk</li>
<li>切換 dedup key 的 routing priority — 同一 alert 優先取 Elastic 那條</li>
<li><strong>保留 Splunk ingest</strong> — 不立刻停、提供 fallback 半小時</li>
<li>SOC 24h 監視、無異常進入 Phase 5</li>
</ol>
<p>回退邊界：cutover 失敗（Elastic 端 alert 大量遺漏 / 延遲）→ routing 切回 Splunk、Elastic 端 alert 再標 test、回 Phase 3。回退時間 30 分鐘內。</p>
<h2 id="phase-5cleanup--不可逆階段">Phase 5：Cleanup — 不可逆階段</h2>
<p>Splunk ingest 停、license decommission、歷史資料 archive：</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. 歷史 archive 到 S3（Splunk DDAS / Smart Store / 第三方）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">splunk <span class="nb">export</span> ... <span class="p">|</span> aws s3 cp - s3://splunk-archive/
</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"># 2. 確認 archive 可查（cold storage retrieve test）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 3. Splunk indexer disable / Splunk Cloud subscription downgrade</span></span></span></code></pre></div><p><strong>不可逆邊界</strong>：Splunk license 退掉、historical query 必須走 S3 + 重 ingest 才能跑、SLA 從即時變天級。決策關鍵：</p>
<ul>
<li>法規 retention（GDPR / SOX / HIPAA）多久</li>
<li>Incident response 需要 historical query 的頻率</li>
<li>翻譯後的歷史資料 indexable in Elastic？多數情況 ECS 跟 CIM 結構差太大、historical 不直接可查</li>
</ul>
<p>實務 default：Splunk Cloud 保留最低 tier 1 年、Elastic 接新資料；1 年後再評估 archive 策略。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1macro-跨-spl-沒對應-kql-function">Case 1：Macro 跨 SPL 沒對應 KQL function</h3>
<p><strong>徵兆</strong>：translation tool 把 macro <code>\</code>my_internal_lookup(&hellip;)`` 標 unmappable、人工翻譯後發現 macro 含 3 個巢狀 macro、共 80 行 SPL 邏輯；KQL 端拆成 5 個 runtime field + 2 個 ingest processor 才對等。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit 階段</strong> 用 <code>splunk btool savedsearches list | grep &lt;macro&gt;</code> 找所有 macro 使用點、估翻譯成本</li>
<li><strong>Inline 策略</strong>：macro 在 5 處以下、直接 inline 到 detection rule、不重建 KQL macro</li>
<li><strong>Ingest processor 策略</strong>：macro 是 <em>資料轉換</em> 邏輯、放 Elastic ingest pipeline、不放 detection rule</li>
<li><strong>退役策略</strong>：macro 已 deprecated、不翻譯、把使用的 rule 一起退役</li>
</ol>
<h3 id="case-2time-zone-parsing-差異">Case 2：Time zone parsing 差異</h3>
<p><strong>徵兆</strong>：parallel run 階段、Splunk 跟 Elastic 對同一個 raw event 解出的 <code>_time</code> 差 8 小時；dedup key 沒對齊、雙 alert 都觸發。</p>
<p><strong>根因</strong>：Splunk <code>_time</code> 是 epoch、time zone 由 <code>props.conf</code> 端決定；Elastic ingest pipeline 用 <code>date</code> processor、time zone 預設 UTC。raw event 有 <code>Asia/Taipei</code> timestamp、Splunk 解 UTC、Elastic 解 local。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Ingest pipeline 統一</strong>：所有 raw event 在 ingest 時轉 UTC、不依賴 source-side time zone</li>
<li><strong>dedup 容忍 window</strong>：dedup window 拉到 30 分鐘、cover time zone 漂移</li>
<li><strong>schema 對位 spec 明示時區處理</strong>：Phase 1 spec 要列「所有時間戳統一 UTC」</li>
</ol>
<h3 id="case-3summary-index-翻譯效能爆">Case 3：Summary index 翻譯效能爆</h3>
<p><strong>徵兆</strong>：Splunk 端 <code>tstats count from datamodel=Authentication where _time&gt;=-7d</code> 跑 2 秒、翻譯成 KQL 後 Elastic 跑 45 秒；SOC dashboard 端 timeout。</p>
<p><strong>根因</strong>：Splunk summary index 是 <em>precomputed</em>（小時 / 天聚合預先算好）、<code>tstats</code> 直接讀 summary；KQL 直接跑 search 是 <em>raw event scan</em>、效能差數量級。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Elastic Transform</strong>：Elastic 端建 <em>continuous transform</em>、把 raw event 預先 aggregate 到 transform index、KQL 查 transform index、效能對等</li>
<li><strong>Rollup index</strong>（Elastic legacy）：給 metric-style data 用、deprecated 但仍可</li>
<li><strong>接受 latency</strong>：dashboard query 可接受 30s、不必精準對等 Splunk</li>
</ol>
<h3 id="case-4cutover-期間-pagerduty-dedup-key-衝突">Case 4：Cutover 期間 PagerDuty dedup key 衝突</h3>
<p><strong>徵兆</strong>：cutover 後 24h、SOC 收到雙倍 alert；PagerDuty 兩條 incident 各標 <code>splunk</code> 跟 <code>elastic</code> source、實際是同一事件。</p>
<p><strong>根因</strong>：PagerDuty 的 dedup key 用 <code>rule_name + alert_id</code>、Splunk alert_id 跟 Elastic signal_id 命名空間不同、PagerDuty 視為兩個獨立 incident。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先設計 dedup key</strong>：用 <code>rule_name + event_hash</code>、不用 SIEM 內部 ID</li>
<li><strong>PagerDuty routing rule</strong>：cutover 期間 disable Splunk source routing、不要靠 dedup</li>
<li><strong>Phase 3 parallel run 期間就測試 dedup</strong>：不要拖到 cutover 才發現</li>
</ol>
<h3 id="case-5過早-decommission-splunk歷史-incident-無法回溯">Case 5：過早 decommission Splunk、歷史 incident 無法回溯</h3>
<p><strong>徵兆</strong>：cutover 後 6 個月、發生 incident、需要回查 12 個月前的 auth log；Splunk 已 decom、Elastic 端歷史資料缺、S3 archive 無索引、4 小時找不到 evidence。</p>
<p><strong>根因</strong>：Cleanup phase 過早走、沒先做 <em>historical query rehearsal</em>；S3 archive 沒可用的索引層。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：Phase 5 前跑 <em>5 個 historical query drill</em>、驗證 incident response 時能用</li>
<li><strong>架構</strong>：S3 archive 配 Elastic frozen tier（searchable snapshot）、6 個月 retrieve latency 接受</li>
<li><strong>法規對齊</strong>：Cleanup 時間表必須跟 compliance retention requirement 對齊、不只是 cost-driven</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Splunk Enterprise / Cloud</th>
          <th>Elastic Security</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pricing model</td>
          <td>per-GB ingest（昂貴 in scale）</td>
          <td>fixed tier / data tier / per-resource</td>
          <td>Elastic 5+ TB/day 規模便宜 50-70%</td>
      </tr>
      <tr>
          <td>Ingest performance</td>
          <td>強、Splunk forwarder 成熟</td>
          <td>強、Elastic Agent / Filebeat</td>
          <td>略接近、Splunk 對 unstructured raw 略優</td>
      </tr>
      <tr>
          <td>Search performance</td>
          <td>強、SPL + summary index</td>
          <td>中、KQL runtime + transform</td>
          <td>Splunk 對複雜 query 仍領先</td>
      </tr>
      <tr>
          <td>Detection content</td>
          <td>ES content + SOC content</td>
          <td>Elastic Security 内建 detection rule + 開源</td>
          <td>兩端都有、Elastic 對 cloud-native 較強</td>
      </tr>
      <tr>
          <td>UEBA / ML</td>
          <td>ES Premium UEBA、成熟</td>
          <td>Elastic ML + 7.x+ rule type</td>
          <td>Splunk 領先、Elastic 追趕中</td>
      </tr>
      <tr>
          <td>Cloud-native</td>
          <td>Splunk Cloud（managed but proprietary）</td>
          <td>Elastic Cloud / ECK on K8s</td>
          <td>Elastic 更 K8s-friendly</td>
      </tr>
      <tr>
          <td>Lock-in</td>
          <td>高（SPL / 自家 forwarder / ES app）</td>
          <td>中（open-source core + commercial extension）</td>
          <td>Elastic 較易遷出（理論上）</td>
      </tr>
      <tr>
          <td>Total cost (5y, 10TB/day)</td>
          <td>$5-15M USD</td>
          <td>$1.5-5M USD</td>
          <td>5-3 倍差</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-soar-整合">跟 SOAR 整合</h3>
<p><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / Tines / Splunk SOAR：</p>
<ul>
<li>cutover 期間 SOAR playbook 仍用 Splunk-shaped event、Phase 5 後改 Elastic-shaped</li>
<li>Playbook 內 SPL query 必須改寫 KQL / ES|QL、可 hybrid（短期保留 SOAR 端原 SPL 邏輯）</li>
</ul>
<h3 id="跟-case-management-整合">跟 case management 整合</h3>
<p>Jira / ServiceNow / Elastic Cases：</p>
<ul>
<li>Splunk notable → Jira ticket 用 link field 帶 <code>splunk_url</code></li>
<li>Elastic alert → Jira 用 <code>elastic_url</code></li>
<li>兩個 URL field 期間同時存在、Phase 5 後 archive</li>
</ul>
<h3 id="反向遷移elastic--splunk">反向遷移（Elastic → Splunk）</h3>
<p>結構 mirror 對稱、phase 仍 6 段、但 schema 對位方向相反：</p>
<ul>
<li>KQL → SPL 翻譯（vendor tool 對等度低、ES|QL → SPL 更困難）</li>
<li>ECS → CIM 對位</li>
<li>多數企業 <em>不會</em> 反向遷、reverse migration 多半是合規驅動（特定客戶要 Splunk）</li>
</ul>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Multi-vendor SIEM portfolio</strong>：不選一家、Splunk + Elastic + Sentinel 同時跑、routing 邏輯按 cost / use case 切</li>
<li><strong>AI-native detection</strong>：兩家都在發展、translation 流程可能再次重來</li>
<li><strong>Compliance migration constraints</strong>：金融 / 政府客戶 SIEM migration 需通過 audit、phase 時間表會被拉長</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a></li>
<li>Target vendor：<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></li>
<li>上游 chapter：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行 deep article：<a href="/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &#43; case management 整合">Splunk RBA</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>1.11 全球分散式 OLTP</title><link>https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>全球分散式 OLTP 解決一個傳統 DB 做不到的問題：跨地理位置 &lt;em>同時&lt;/em> 維持強一致性、低延遲、高可用性。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理&lt;/a>過往把這視為「三選二」，但近 15 年的工程進展（Google Spanner、AWS Aurora DSQL、CockroachDB、Microsoft Cosmos DB 等）顯示「在投入 &lt;em>專屬硬體&lt;/em> 或 &lt;em>特殊演算法&lt;/em> 的條件下、可以同時拿到 strong consistency + global distribution + 可接受 latency」。&lt;/p>
&lt;p>本章整理這類系統的工程設計、容量取捨、跟傳統 single-region OLTP 的差異。讀完後讀者能回答：什麼業務需求需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/global-oltp/" data-link-title="Global OLTP" data-link-desc="跨地理區域仍維持交易一致性的 OLTP 設計責任與代價">global OLTP&lt;/a>、跨 region &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum&lt;/a> 的延遲代價、選 Spanner vs Aurora DSQL vs Cosmos DB 的決策依據。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary&lt;/a> 的關係：1.3 處理 single-region OLTP 的 transaction 設計、本章處理 multi-region OLTP 的特殊取捨。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃&lt;/a> 的關係：1.10 KV 通常 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency&lt;/a> 全球分散容易、本章處理 &lt;em>強一致&lt;/em> 全球分散的工程挑戰。&lt;/p>
&lt;h2 id="cap-跟-pacelc理論工具">CAP 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC&lt;/a>：理論工具&lt;/h2>
&lt;p>選擇全球 DB 前要先理解兩個理論框架。&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理&lt;/a>&lt;/strong>：分散式系統 &lt;em>發生分區（network partition）&lt;/em> 時、必須在 Consistency 跟 Availability 二選一。&lt;/p>
&lt;ul>
&lt;li>CP 系統：強一致、partition 時拒絕服務（Spanner、Cosmos DB strong）&lt;/li>
&lt;li>AP 系統：高可用、partition 時可能回舊資料（Cassandra、DynamoDB Global Tables）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>PACELC（Daniel Abadi 提出）&lt;/strong>：擴充 CAP、加上「沒 partition 時」的取捨。&lt;/p>
&lt;ul>
&lt;li>沒 partition 時：Latency vs Consistency 二選一&lt;/li>
&lt;li>結合表示：PA/EL（partition 時選 Availability、平時選 Latency）vs PC/EC（partition 時選 Consistency、平時選 Consistency）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>工程含義&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Spanner、Aurora DSQL、Cosmos DB strong：PC/EC — 永遠選一致、付出 latency&lt;/li>
&lt;li>Cassandra、DynamoDB Global Tables：PA/EL — 永遠選快、付出可能不一致&lt;/li>
&lt;li>Cosmos DB session：PA/EL 但對同一 session 內保持 EC — 妥協方案&lt;/li>
&lt;/ul>
&lt;p>選 global DB 不是「哪個最好」、是「業務需要哪一邊」。金融交易、ticketing inventory、payment ledger 通常需要 EC；社群 feed、推薦、analytics 通常 EL 夠用。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>全球分散式 OLTP 解決一個傳統 DB 做不到的問題：跨地理位置 <em>同時</em> 維持強一致性、低延遲、高可用性。<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理</a>過往把這視為「三選二」，但近 15 年的工程進展（Google Spanner、AWS Aurora DSQL、CockroachDB、Microsoft Cosmos DB 等）顯示「在投入 <em>專屬硬體</em> 或 <em>特殊演算法</em> 的條件下、可以同時拿到 strong consistency + global distribution + 可接受 latency」。</p>
<p>本章整理這類系統的工程設計、容量取捨、跟傳統 single-region OLTP 的差異。讀完後讀者能回答：什麼業務需求需要 <a href="/blog/backend/knowledge-cards/global-oltp/" data-link-title="Global OLTP" data-link-desc="跨地理區域仍維持交易一致性的 OLTP 設計責任與代價">global OLTP</a>、跨 region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> 的延遲代價、選 Spanner vs Aurora DSQL vs Cosmos DB 的決策依據。</p>
<p>跟 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的關係：1.3 處理 single-region OLTP 的 transaction 設計、本章處理 multi-region OLTP 的特殊取捨。</p>
<p>跟 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 的關係：1.10 KV 通常 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a> 全球分散容易、本章處理 <em>強一致</em> 全球分散的工程挑戰。</p>
<h2 id="cap-跟-pacelc理論工具">CAP 跟 <a href="/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC</a>：理論工具</h2>
<p>選擇全球 DB 前要先理解兩個理論框架。</p>
<p><strong><a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理</a></strong>：分散式系統 <em>發生分區（network partition）</em> 時、必須在 Consistency 跟 Availability 二選一。</p>
<ul>
<li>CP 系統：強一致、partition 時拒絕服務（Spanner、Cosmos DB strong）</li>
<li>AP 系統：高可用、partition 時可能回舊資料（Cassandra、DynamoDB Global Tables）</li>
</ul>
<p><strong>PACELC（Daniel Abadi 提出）</strong>：擴充 CAP、加上「沒 partition 時」的取捨。</p>
<ul>
<li>沒 partition 時：Latency vs Consistency 二選一</li>
<li>結合表示：PA/EL（partition 時選 Availability、平時選 Latency）vs PC/EC（partition 時選 Consistency、平時選 Consistency）</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>Spanner、Aurora DSQL、Cosmos DB strong：PC/EC — 永遠選一致、付出 latency</li>
<li>Cassandra、DynamoDB Global Tables：PA/EL — 永遠選快、付出可能不一致</li>
<li>Cosmos DB session：PA/EL 但對同一 session 內保持 EC — 妥協方案</li>
</ul>
<p>選 global DB 不是「哪個最好」、是「業務需要哪一邊」。金融交易、ticketing inventory、payment ledger 通常需要 EC；社群 feed、推薦、analytics 通常 EL 夠用。</p>
<h2 id="spanner--truetime-模型">Spanner / <a href="/blog/backend/knowledge-cards/truetime/" data-link-title="TrueTime" data-link-desc="分散式資料庫用來界定時間不確定性的時間語意機制">TrueTime</a> 模型</h2>
<p><a href="https://cloud.google.com/spanner">Google Cloud Spanner</a> 是目前最成熟的 global strong-consistency OLTP。</p>
<p><strong>TrueTime API</strong>：用 GPS + 原子鐘提供「全球 <em>unambiguous</em> 時間戳」、解決分散式系統最難的問題之一 — 跨節點時序排序。</p>
<p><strong><a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">External consistency</a>（線性化）</strong>：用 TrueTime 保證「全球任何節點看到的交易順序、跟 wall clock 一致」。比 CAP 的 strong consistency 更強。</p>
<p><strong>容量特性</strong>（引自 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner 案例</a>）：</p>
<ul>
<li>內部峰值 &gt; 10 億 requests / 秒</li>
<li>線性擴展：2 nodes → 45K reads/sec、4 nodes → 90K reads/sec</li>
<li>跨地區交易延遲 100-200ms（quorum round-trip 不可壓縮）</li>
<li>multi-region instance 可設定 quorum location（影響哪幾個 region 必須同意）</li>
</ul>
<h3 id="線性擴展為什麼是-oltp-設計的最高目標">線性擴展為什麼是 OLTP 設計的最高目標</h3>
<p>「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」這個線性對應在傳統 OLTP（PostgreSQL、MySQL）做不到。原因是 <em>跨節點交易需要 coordinator 確認順序、coordinator 本身是 bottleneck</em>。加更多節點不會線性加吞吐、因為 coordinator 處理速度跟不上、其他節點得排隊等。</p>
<p>Spanner 用 Paxos + TrueTime 把 coordinator 變成「拓樸感知的多 leader」、每個 leader 只管自己 partition、不需要全域 coordinator。這層演算法 + 硬體（GPS + 原子鐘）配合、才達成線性擴展。</p>
<p><strong>為什麼這個 frame 對選型重要</strong>：讀「Spanner 撐 10 億 req/sec」不該理解成「能力差距」、而是「設計差距」— 傳統 OLTP 不是「沒它快」、是「結構上做不到線性」。如果業務未來會跨 region 擴展、必須在最初就選 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a>、不是先用 PostgreSQL 再「之後加 sharding」。</p>
<p><strong>對等技術跟取捨</strong>：</p>
<ul>
<li><strong>AWS Aurora DSQL</strong>：用其他協議（OCC + 分散式時鐘）達成跨 region strong consistency、不用 TrueTime 硬體。</li>
<li><strong>CockroachDB</strong>：用 HLC（Hybrid Logical Clock）+ Raft、可在通用硬體上跑、但 cross-region linearizability 需要 OCC retry。</li>
<li><strong>TiDB</strong>：用 TSO（Timestamp Oracle）服務發 global timestamp、TSO 本身是 single point、可用性要靠 TSO failover 設計。</li>
</ul>
<p>TrueTime 是 <em>專屬硬體投資</em>、其他方案是 <em>軟體 only</em>、兩者一致性保證等級類似、但運維成本跟認證難度差很大。可複製性低的 TrueTime 是 Google 的競爭優勢、不是普遍 best practice。</p>
<p><strong>容量規劃</strong>：</p>
<ul>
<li>節點數量 = 容量單位（每年 review）</li>
<li>跨 region quorum 配置決定 latency baseline</li>
<li>不能像 single-region OLTP 那樣短期擴容、需要提前 ramp</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>金融交易、ticketing inventory</li>
<li>全球客戶但需要強一致</li>
<li>不能容忍跨地區 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 的業務</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>跨洲低延遲（沒辦法、TrueTime 也壓不下 100ms 跨洲）</li>
<li>高 throughput 但容忍 eventual consistency（Bigtable / Cassandra 更便宜）</li>
</ul>
<h3 id="分散式-sql-的-over-provision-屬結構性成本">分散式 SQL 的 over-provision 屬結構性成本</h3>
<p>分散式 SQL（TiDB、CockroachDB、Spanner）要求恆常 over-provision、是結構性成本、不是 capacity planning 失誤。三個原因都來自跨節點協調的物理需求：</p>
<ul>
<li>跨節點 transaction 需要 coordinator 角色、leader election 在尖峰當下不能發生、否則整個 cluster 卡住。</li>
<li>預留 buffer 讓 leader / follower lag 在尖峰時仍能收斂、否則 replication lag 爆增、讀走 replica 的 query 拿到太舊資料。</li>
<li>跨 region quorum 在某個 region 暫時不可用時、剩下 region 要能繼續 quorum、所以每 region 的容量都要 &gt;= quorum 所需。</li>
</ul>
<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%">9.C20 Zomato</a> — Zomato 從 TiDB 遷出是業務需求側的判斷：該 workload 本身就能接受 eventually consistent、為 strong consistency 付的 over-provision 屬於浪費。判讀重點：strong consistency 是業務需求時、distributed SQL 的常態 over-provision 是合理代價；業務需求不到這個層級時、KV / 傳統 OLTP 是更划算的選項。</p>
<p>選型公式：先問業務需求要什麼一致性層級、再選 DB 類型、避免倒過來「先選 DB 再硬塞需求」。</p>
<h2 id="aurora-dsqlaws-的全球-strong-consistency-答案">Aurora DSQL：AWS 的全球 strong consistency 答案</h2>
<p>AWS 在 2024 re:Invent 推出 Aurora DSQL、是 AWS 對 Spanner 的回應。</p>
<p><strong>設計特點</strong>（引自 <a href="https://aws.amazon.com/blogs/database/amazon-aurora-dsql-for-global-scale-financial-transactions/">Aurora DSQL announcement</a>）：</p>
<ul>
<li>跨 region active-active write</li>
<li>強一致性（線性化）</li>
<li>PostgreSQL wire protocol compatible（應用層改動小）</li>
<li>Serverless（不必管 instance）</li>
</ul>
<p><strong>跟 Spanner 的差異</strong>：</p>
<ul>
<li>Spanner 用 TrueTime 硬體、Aurora DSQL 用其他協議</li>
<li>Aurora DSQL 跟 PostgreSQL 相容（容易遷移）、Spanner 是專屬 SQL dialect</li>
<li>Aurora DSQL 較新（2024）、生態還在成長</li>
<li>Spanner 服務時間長（內部 2007、外部 2017）、production 案例多</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>AWS 生態用戶想要 global strong consistency</li>
<li>已用 Aurora / PostgreSQL、想擴展到 multi-region</li>
<li>應用層想保留 PostgreSQL ORM</li>
</ul>
<h2 id="cockroachdb-跟-tidb自管選項">CockroachDB 跟 TiDB：自管選項</h2>
<p>如果不想 vendor lock-in、或需要 on-prem 部署、選擇是 <em>self-managed</em> distributed SQL。</p>
<p><strong>CockroachDB</strong>：</p>
<ul>
<li>開源、可自管或用 Cockroach Cloud</li>
<li>跟 PostgreSQL wire protocol compatible</li>
<li>線性擴展、跨 region 部署、強一致</li>
<li>設計理念近 Spanner、但不用 TrueTime（用 HLC + Raft）</li>
</ul>
<p><strong>TiDB</strong>：</p>
<ul>
<li>開源（PingCAP）、可自管或用 TiDB Cloud</li>
<li>跟 MySQL wire protocol compatible</li>
<li>TiKV + TiDB 分層架構</li>
<li>中國市場大量使用、亞洲生態成熟</li>
</ul>
<p><strong>選擇取捨</strong>：</p>
<ul>
<li>vendor lock-in 風險 → 選 CockroachDB / TiDB</li>
<li>想 managed → 選 Spanner / Aurora DSQL</li>
<li>已用 PostgreSQL → 選 CockroachDB / Aurora DSQL（migration 容易）</li>
<li>已用 MySQL → 選 TiDB</li>
</ul>
<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%">9.C20 Zomato</a> 從 TiDB 遷出（理由不是 TiDB 不好、是 NewSQL 必須 over-provision、KV NoSQL 對該 workload 更划算）。</p>
<h2 id="cosmos-db-multi-region-write-模式">Cosmos DB multi-region write 模式</h2>
<p><a href="https://azure.microsoft.com/products/cosmos-db/">Azure Cosmos DB</a> 提供 <em>五個一致性層級</em>、是 multi-region OLTP 最有彈性的選擇之一。</p>
<p><strong>五個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a></strong>（從強到弱）：</p>
<ol>
<li><strong>Strong</strong>：<a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizable</a>、跨 region quorum</li>
<li><strong><a href="/blog/backend/knowledge-cards/bounded-staleness/" data-link-title="Bounded Staleness" data-link-desc="允許資料延遲，但把落後上限限制在可量化範圍內的一致性語意">Bounded staleness</a></strong>：訂版本 / 時間上限</li>
<li><strong><a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">Session consistency</a></strong>：同 session 內強一致</li>
<li><strong>Consistent prefix</strong>：保證寫入順序</li>
<li><strong>Eventual</strong>：最便宜、最終一致</li>
</ol>
<p><strong>Multi-region write 特色</strong>：</p>
<ul>
<li>每個 region 都能寫、不必所有寫入回主 region</li>
<li>conflict resolution 用 LWW（Last-Writer-Wins）或自訂 stored procedure</li>
<li>跟 Spanner 的 strong consistency 不同 — 是 <em>AP 系統</em>、不保證 <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a></li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>全球用戶分布、想 <em>寫入本地 region</em> 減延遲</li>
<li>容忍 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a>（電商商品評論、社群動態）</li>
<li>不能容忍跨 region failover 中斷</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — AR 玩家位置用 session consistency、跨 region 寫入</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a> — Black Friday 全球用戶、Cosmos DB 跨 region 複製</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 分析 platform 用 weakest acceptable consistency、最大 throughput</li>
</ul>
<h2 id="跨地理合規法規限制下的-global-oltp">跨地理合規：法規限制下的 global OLTP</h2>
<p>部分產業（金融、醫療、政府）有 <em>資料駐留</em> 要求 — 特定國家的資料不能離境。這跟全球分散式 OLTP 的設計有 conflict。</p>
<p><strong>典型法規</strong>：</p>
<ul>
<li>歐盟 GDPR：歐洲用戶資料應留歐</li>
<li>中國《網路安全法》、《資料安全法》：中國用戶資料留中國</li>
<li>印度資料保護法：印度金融資料留印度</li>
<li>美國各州 healthcare（HIPAA）：醫療資料規範</li>
<li>金融業：各國央行通常規定本地交易資料留本地</li>
</ul>
<p><strong>設計策略</strong>：</p>
<ul>
<li><em>多個獨立 cluster</em>、每個合規區一個。不是 single global cluster。</li>
<li>meta-data 可以 global（用戶 profile 摘要）、transaction 必須 local</li>
<li>跨區查詢通過 federated query 或 ETL、不是直接 join</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 7 個受監管市場、各自獨立 Aurora cluster、不能合併</li>
<li><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 15 主 region + 5 衛星、按合規區分布</li>
<li><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent</a> — 美國支付業務、Azure SQL Hyperscale + 美國 region</li>
</ul>
<h2 id="延遲代價跨-region-quorum-不可壓縮">延遲代價：跨 region quorum 不可壓縮</h2>
<p>全球 strong consistency 必須付的延遲代價來自物理。光速跑跨大西洋（紐約 ↔ 倫敦 5500 km）大約 27ms one-way、實際網路延遲 70-90ms（含路由 / 處理）。任何 strong consistency 系統都不能比這個快。</p>
<p><strong>典型跨 region quorum latency</strong>：</p>
<ul>
<li>同 region 跨 AZ：1-3ms</li>
<li>同 continent 跨 region（us-east-1 ↔ us-west-2）：50-80ms</li>
<li>跨 continent（us ↔ eu）：80-120ms</li>
<li>跨地球（us ↔ asia）：150-250ms</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>SLO 訂 p99 &lt; 50ms 跨 continent strong consistency → 不可能達成</li>
<li>必須在 SLO 設計時就接受跨 region 的物理 floor</li>
<li>業務不需要 strong consistency 的話、用 session / eventual 換 latency</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> — sub-ms 需求、無法跨 region、用 single-AZ cluster placement</li>
<li><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a> — 35ms VALORANT 延遲門檻、靠 region cluster 滿足、不靠 global DB</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget 卡片</a>。</p>
<h3 id="業務的不同延遲代價曲線">業務的不同延遲代價曲線</h3>
<p>讀「100-200ms 跨洲延遲」這種數字、不能只看絕對值、要看 <em>業務代價怎麼隨延遲變化</em>。不同業務型態的延遲代價曲線不同、決定能不能用 strong consistency 全球分散。</p>
<p><strong>B2B agent 操作介面</strong>（客服平台、CRM）：延遲代價的特性是 <em>累積</em>。agent 一通客戶電話內連續操作數十次、每次卡 1 秒、累積 30 秒讓 agent 在用戶面前沉默 — 客服效率直接掉一半、客戶等不及掛電話、agent 績效跟 NPS 同時下降。專屬訊號是「單次 latency 看似可接受、agent 體感卻變慢」。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> 用 15 個 region 把任一 agent 的 DB 延遲壓到 &lt; 50ms — 客服 SaaS 對單次延遲的容忍區間遠窄於一般網路服務。</p>
<p><strong>B2C 終端用戶</strong>（社群、電商）：延遲代價是 <em>一次性跳離</em>。用戶等 1 秒會抱怨、等 3 秒會跳離；但完成一個操作就走、不會像 B2B 累積多次。容忍區間在 200ms-500ms、超過就掉 conversion。專屬訊號是「session bounce rate 跟 latency p99 高度相關」、不是看平均。</p>
<p><strong>金融交易</strong>（payment、trading）：延遲代價有兩面、是其他業務型態少見的結構。一面是用戶體驗（付款卡 = 結帳放棄）、另一面是 <em>系統正確性</em>（交易順序錯 = 對帳異常、稽核失敗）。後者讓金融業願意付 100-200ms 換 strong consistency、因為對帳成本遠高於延遲成本。專屬訊號是「願意接受比 B2C 更高的 latency budget、但拒絕任何 consistency 妥協」。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 7 個受監管市場的設計。</p>
<p><strong>IoT / Telemetry</strong>：延遲幾乎無業務代價（資料晚 10 秒進來、報表還是準）、但 throughput 才是主導指標。原因是這類業務的價值來自 <em>大量裝置的聚合趨勢</em>、不是 <em>單一裝置即時回應</em>；只要事件最終到達且順序合理、晚一點不影響決策。專屬訊號是「百萬裝置同時上報、寫入吞吐才是 SLO、latency 不在 alert 條件裡」。選型上 KV 或時序 DB 比 strong-consistency OLTP 更划算。</p>
<p>判讀重點：選 global OLTP 前先畫業務的延遲代價曲線、再決定能付多少 latency budget 給 strong consistency。「100ms 跨洲太慢」這個直覺反射只在沒有對帳 / 累積 / 趨勢這些業務代價時成立。</p>
<h2 id="容量規劃跟-single-region-oltp-完全不同">容量規劃：跟 single-region OLTP 完全不同</h2>
<p>全球分散式 OLTP 的容量規劃有獨特挑戰。</p>
<p><strong>容量單位</strong>：</p>
<ul>
<li>Spanner：節點數</li>
<li>Aurora DSQL：serverless 自動（按 ACU 計費）</li>
<li>Cosmos DB：RU/s（每個 region 獨立配置）</li>
<li>CockroachDB / TiDB：節點數 + storage</li>
</ul>
<p><strong>規劃要點</strong>：</p>
<ul>
<li>每個 region 獨立規劃（跨 region 不能 amortize）</li>
<li>quorum 配置決定哪些 region 必須同意（影響 failure domain）</li>
<li>跨 region replication lag 是 SLO 一部分</li>
<li>不能像 single-region 那樣 reactive 擴容、必須 predictive</li>
</ul>
<p><strong>對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></strong>：全球 OLTP 是「不可水平擴容服務」的延伸 — 不只「單機極限」、是「跨 region 協調的物理極限」。</p>
<h2 id="可用性目標的成本曲線">可用性目標的成本曲線</h2>
<p>「我們要 99.99% 還是 99.999%」這個問題不該用直覺答、要先看每多一個 9 帶來的成本是多少。可用性是非線性、不是線性。</p>
<p><strong>九的數學意義</strong>：</p>
<table>
  <thead>
      <tr>
          <th>可用性</th>
          <th>年停機時間</th>
          <th>月停機時間</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>99%</td>
          <td>87.6 小時 / 年</td>
          <td>7.3 小時 / 月</td>
          <td>開發 / 內部工具</td>
      </tr>
      <tr>
          <td>99.9%</td>
          <td>8.76 小時 / 年</td>
          <td>43.8 分鐘 / 月</td>
          <td>一般 B2C 網站</td>
      </tr>
      <tr>
          <td>99.95%</td>
          <td>4.38 小時 / 年</td>
          <td>21.9 分鐘 / 月</td>
          <td>B2C SaaS、有 SLA 但非 mission-critical</td>
      </tr>
      <tr>
          <td>99.99%</td>
          <td>52.6 分鐘 / 年</td>
          <td>4.38 分鐘 / 月</td>
          <td>受監管產業、付款</td>
      </tr>
      <tr>
          <td>99.999%</td>
          <td>5.26 分鐘 / 年</td>
          <td>26 秒 / 月</td>
          <td>客服 SaaS、telco、5x9 是合約義務</td>
      </tr>
      <tr>
          <td>99.9999%</td>
          <td>31.5 秒 / 年</td>
          <td>2.6 秒 / 月</td>
          <td>極特殊（核電、航空管制）</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼 99.99 → 99.999 是指數成本而非線性</strong>：每多一個 9、要求 <em>每一層基礎設施</em> 都要對等冗餘。</p>
<ul>
<li>99.9 → 99.99：加 multi-AZ active-active、~2-3x 成本</li>
<li>99.99 → 99.999：加 multi-region active-active、+ DR 演練、+ failover 自動化、+ 監控覆蓋率拉滿、~5-10x 成本</li>
<li>99.999 → 99.9999：加多 cloud、+ 異地災備、+ 全自動 failover、+ 全鏈路演練、~20-50x 成本</li>
</ul>
<p><strong>適用場景的業務理由</strong>：</p>
<ul>
<li><strong>99.99%（受監管產業、付款）</strong>：合約 SLA 通常落在這層。受監管金融在中央銀行 / 金融監管機關的書面要求下、年度書面合規會審查 downtime 紀錄、超過 52 分鐘 / 年要解釋；付款 gateway 對商家 SLA 通常承諾 99.99%、低於這個值會被合作夥伴扣保證金。</li>
<li><strong>99.999%（客服 SaaS / telco）</strong>：5x9 是 B2B 客服 SaaS 跟電信業的 <em>合約義務</em>、不是行銷話術。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 客服平台用 15 主 region + 5 衛星 region 達 99.999%、架構成本約是 single-region 的 15 倍、但 B2B 客服合約要 5x9、這是合理投資。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 廣告計費 1 分鐘斷線可能損失幾百萬美金廣告收入、5x9 對應真實營收邊界。電信業 911 緊急通話必須 5x9 是更嚴格的法規層級。</li>
<li><strong>99.9999%（核電、航空管制）</strong>：6x9 不只是工程目標、是 <em>公共安全法規</em>。核電廠 SCADA 系統、空管雷達、軌道交通信號這類業務 30 秒 / 年的中斷會威脅生命、所以付得起跨多 cloud / 異地災備 / 全鏈路演練的成本。一般網路服務談 6x9 通常是過度設計。</li>
</ul>
<p><strong>SLO 木桶效應</strong>：99.999% 是 <em>系統整體</em> 數字、不是 DB 單獨。DNS、load balancer、application、DB、storage 任何一層 single-region 就破壞整體 SLO。傳統工程師常以為「DB 多 region 就好」、忽略 application 跑在 single-region 的話、application down = 整體 down。</p>
<p>要達成 5x9、要 <em>每一層</em> 都 multi-region active-active、且 <em>failover 流程能自動執行</em>（人類在事故當下做不到 5 分鐘內完成切換）。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的跨 region 部署、跟 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性驗證模組</a> 的 DR 演練。</p>
<p><strong>Region 成本曲線</strong>：N 個 region 的成本約是 1 個 region 的 N 倍（DB + compute + storage 都要複製）、但業務收益不是線性。</p>
<ul>
<li>1 region：覆蓋本國用戶</li>
<li>3 region（同 continent）：覆蓋整 continent、延遲 &lt; 50ms</li>
<li>6 region（跨 continent）：覆蓋全球、延遲 100-200ms</li>
<li>15 region：每個用戶 &lt; 50ms 接入（如 Genesys 模式）</li>
</ul>
<p>從 6 region → 15 region 的成本是 2.5x、但用戶體驗改善（50ms 延遲）對 B2B 客服很關鍵、對 B2C 推薦系統幾乎無感。region 數量選擇要看 <em>業務模型對延遲的敏感度</em>、不是工程「越多越好」。</p>
<h2 id="sharding-粒度跟業務一致性需求">Sharding 粒度跟業務一致性需求</h2>
<p>distributed SQL 跟 single-cluster SQL 之間還有一層：<strong>多個獨立 cluster + 應用層 sharding</strong>。選哪個跟業務的一致性需求有關。</p>
<p><strong>Hyperscale / Aurora 同類設計</strong>（storage / compute 分離）：</p>
<ul>
<li>AWS Aurora、Azure SQL Hyperscale、GCP AlloyDB、Spanner 都採類似工程哲學 — log-structured 分散式 storage + 獨立 compute scale</li>
<li>storage 最高通常 100 TB（Hyperscale）、超過要 sharding</li>
<li>compute 上限是 instance type（80 vCore 等）、超過要 sharding 或換 distributed SQL</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent</a> — 5 億筆/年支付交易、用 Hyperscale 撐單一 cluster、沒拆 sharding 是因為支付業需要 <em>跨 merchant 對帳一致性</em>、共用 OLTP 比拆 cluster 划算。</p>
<p><strong>選 vendor 看生態、不看技術</strong>：Hyperscale 跟 Aurora 工程哲學一致、選哪家取決於 application 已在哪個 cloud。AWS 客戶選 Aurora、Azure 客戶選 Hyperscale、GCP 客戶選 AlloyDB / Spanner。技術差異小、生態差異大（IAM 整合、observability tooling、計費綁定）。</p>
<p><strong>業務一致性需求決定 sharding 粒度</strong>：</p>
<ul>
<li><strong>微服務各自 OLTP</strong>（Netflix Aurora consolidation）：每個微服務有自己的 Aurora cluster、跨服務一致性靠 application 層 saga / outbox。適合服務間業務 <em>天然解耦</em>（用戶服務、訂單服務、商品服務各自 owned data）。Query path 上、跨服務查詢必須走 API 而非 SQL JOIN、要接受查多個服務多次往返；一致性 path 上、跨服務 transaction 用 saga + compensation、容忍中間態。</li>
<li><strong>微服務共用 OLTP</strong>（Clearent Hyperscale）：所有微服務共用一個大 cluster、跨服務一致性靠 DB transaction。適合業務 <em>天然耦合</em>（payment 跟 refund 跟 chargeback 必須在同一 transaction）。Query path 上、可以用 SQL JOIN 直接查跨服務資料、簡單；一致性 path 上、所有微服務共享一個 schema 演進邊界、schema migration 影響所有服務、要協調。</li>
<li><strong>Sharding by tenant</strong>（B2B SaaS）：每個 enterprise tenant 自己 cluster、適合 tenant 之間完全隔離、大客戶可能要求專屬 cluster。Query path 上、跨 tenant 查詢（例如平台級報表）要走 federated query 或 ETL 聚合、不能直接 join；運維 path 上、每個 tenant cluster 的容量規劃、backup、upgrade 都獨立、運維工時隨 tenant 數量線性成長。</li>
<li><strong>Sharding by region</strong>（受監管產業）：每個合規市場自己 cluster、合規驅動、不是性能驅動。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 7 個市場各自獨立。</li>
</ul>
<p>判讀重點：sharding 不是「擴容到不夠才做」、是「業務模型決定的初始設計」。等到 single cluster 撐不住才開始 shard、會踩進「跨 shard 一致性」的工程地雷區、修改成本遠高於初期設計成本。Managed DB（Aurora、Hyperscale）的容量上限是 <em>已知</em> 的、設計時就該知道未來何時觸發 sharding。對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的 storage 層 replication 段 — Hyperscale / Aurora / Spanner 同類設計的容量上限同樣是 sharding 觸發點。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>10 億 req/sec 線性擴展、TrueTime 實作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth Cosmos DB</a></td>
          <td>turnkey global distribution、5 consistency levels</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>受監管金融跨市場、必須各自獨立 cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Cosmos DB</a></td>
          <td>全球零售 multi-region、Black Friday 持續高峰</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a></td>
          <td>跨 15 region active-active 達 5 個 9 可用性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a></td>
          <td>美國支付業、storage / compute 分離擴展</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>（single-region OLTP）</li>
<li>平行：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（KV 全球分散）</li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（含「預設 DB 治理 pattern」— 平台規模化階段的 OLTP 選型治理）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>、<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 State Storage Selection</a>、<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11 Data Residency</a></li>
<li>Spanner 深入：<a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">TrueTime API 深入</a>、<a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">一致性模型對照</a>、<a href="/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/" data-link-title="Spanner Schema Migration Without Downtime &#43; Interleaved Tables" data-link-desc="Spanner DDL 是 long-running operation、用 TrueTime 給每次 schema change 分配 version timestamp、所有 read / write 對應自己 transaction timestamp 看到對應 schema。Interleaved table 是 storage-level parent-child 物理交錯、不是 logical FK。本文走 schema change lifecycle、interleaved layout 機制、backfill capacity 影響、5 production 踩雷、跟 PostgreSQL online schema change 對照">interleaved table schema migration</a></li>
<li>CockroachDB / Aurora DSQL 深入：<a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/survival-goals/" data-link-title="CockroachDB Survival Goals：zone 級 vs region 級配置與業務 SLO 倒推流程" data-link-desc="CockroachDB 用 SURVIVE ZONE FAILURE / SURVIVE REGION FAILURE 兩種 survival goal 宣告式控制 Raft replica 分佈、決定 RTO / RPO。本文走 Hard Rock Digital bet placement RPO=0 倒推流程、Netflix Gaming 48-node 跨 4 region 「為求 survival 而非 latency」的反直覺判讀、配置語法、寫入 latency 暴漲跟 cost 暴漲兩條失敗模式、合規邊界對比">survival goals</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/" data-link-title="CockroachDB Locality-Aware Schema：跨州合規 &#43; 邏輯一個 cluster 的 region placement 策略" data-link-desc="Hard Rock Digital 跨 8 州 sportsbook、用 AWS Outposts &#43; region placement 把運算釘在州內、邏輯上仍是一個 CockroachDB cluster。本文走 REGIONAL BY ROW / REGIONAL BY TABLE / GLOBAL 三種 locality、Hard Rock 拓樸創新對比 Standard Chartered Aurora 7 cluster fleet、AWS Outposts 是合規工具不是 latency 工具的反直覺判讀">locality-aware schema</a></li>
<li>Aurora 多 region 深入：<a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">global database multi-region</a>、<a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">跨 AZ failover RTO</a></li>
<li>Cosmos DB 多 region 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">一致性層次工程</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">多 region write 衝突</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">Transaction Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
</ul>
]]></content:encoded></item><item><title>9.11 高峰事件準備</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>高峰事件準備的責任是把「事件臨頭才動手」變成「事前數週流程化準備」。沒有 readiness 流程時、年度活動靠 oncall 撐、出事率高；有流程之後、活動成「routine event」、工程資源穩定釋放。&lt;/p>
&lt;p>本章 &lt;em>是&lt;/em> &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 在「事件型場景」的應用組合、不重新建立方法論。要看具體方法回到那兩章、本章聚焦在 &lt;em>流程整合&lt;/em>。&lt;/p>
&lt;p>讀完後讀者能設計一個 T-90 → T-0 的事件準備時程、回答「Black Friday 該怎麼準備、Super Bowl 該怎麼準備、新片發布該怎麼準備」。&lt;/p>
&lt;h2 id="事件分類五種負載形狀">事件分類：五種負載形狀&lt;/h2>
&lt;p>不同事件對應不同準備強度、第一步要分類。&lt;/p>
&lt;p>&lt;strong>可預期極端峰值&lt;/strong>：年度活動、預售、賽事決賽。提前數月已知時間、業務影響大。例：Prime Day、Black Friday、Super Bowl、IPL 決賽。
&lt;strong>事件型不可預期峰值&lt;/strong>：賽事高潮、突發新聞、KOL 推廣。時間或大小不完全可預測。例：賽事進球瞬間、KOL 帶貨、突發新聞引發的流量。
&lt;strong>Flash-sale 瞬間爆量&lt;/strong>：售票開賣、報名活動、限量搶購。t=0 瞬間爆量、5-30 分鐘結束。例：演唱會售票、限量商品搶購、報名截止前最後一小時。
&lt;strong>產品爆紅 surge&lt;/strong>：新 app 紅、病毒擴散。完全不可預期、流量會隨熱度消退。例：Pokemon GO、ChatGPT 爆紅初期、TikTok challenge。
&lt;strong>結構性 surge&lt;/strong>：COVID 類外部衝擊、永久 baseline 上移。不會回到舊水準。例：COVID 期間遠距工作工具、烏俄戰爭期間能源類 app。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C1 / 9.C13 / 9.C21 / 9.C27 / 9.C29&lt;/a>（predictable）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C2 / 9.C4 / 9.C7 / 9.C28&lt;/a>（event）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C15 / 9.C16 / 9.C17&lt;/a>（flash-sale）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C8 / 9.C18&lt;/a>（surge）。&lt;/p>
&lt;h2 id="t-90--t-0-準備時程">T-90 → T-0 準備時程&lt;/h2>
&lt;p>可預期極端峰值的完整準備時程：&lt;/p>
&lt;p>&lt;strong>T-90 天&lt;/strong>：流量 forecast + 容量計畫敲定。確認預期峰值倍數、確認 headroom 比例、確認跨 region / AZ 分布。產出 &lt;em>容量計畫文件&lt;/em>。&lt;/p>
&lt;p>&lt;strong>T-30 天&lt;/strong>：基礎設施 quota 申請。雲端 instance limit、connection pool、API rate limit、DynamoDB throughput、Lambda concurrency 都要 &lt;em>提前申請&lt;/em>、不能事件當天才發現 quota 不夠。AWS Infrastructure Event Management（IEM）等服務在這階段啟動。&lt;/p>
&lt;p>&lt;strong>T-14 天&lt;/strong>：第一輪 production-like 壓測。驗證容量計畫是否真的能撐預期峰值、找出第一輪 bottleneck。&lt;/p>
&lt;p>&lt;strong>T-7 天&lt;/strong>：完整 game day 演練。注入故障場景（DB failure、AZ outage、第三方 quota 耗盡）、驗證降級、failover、rollback 流程。修正最後問題、更新 runbook。&lt;/p>
&lt;p>&lt;strong>T-2 天&lt;/strong>：pre-scaling 開始。CDN cache pre-warm、Lambda provisioned concurrency 啟動、autoscaler scheduled 開始、DB capacity 預先 scale up。避免事件當天還在 boot。&lt;/p>
&lt;p>&lt;strong>T-0 day&lt;/strong>：watch room 待命、runbook 開機可執行。所有相關 oncall 跨團隊聯合 channel、dashboard 集中、escalation path 清楚。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>高峰事件準備的責任是把「事件臨頭才動手」變成「事前數週流程化準備」。沒有 readiness 流程時、年度活動靠 oncall 撐、出事率高；有流程之後、活動成「routine event」、工程資源穩定釋放。</p>
<p>本章 <em>是</em> <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 在「事件型場景」的應用組合、不重新建立方法論。要看具體方法回到那兩章、本章聚焦在 <em>流程整合</em>。</p>
<p>讀完後讀者能設計一個 T-90 → T-0 的事件準備時程、回答「Black Friday 該怎麼準備、Super Bowl 該怎麼準備、新片發布該怎麼準備」。</p>
<h2 id="事件分類五種負載形狀">事件分類：五種負載形狀</h2>
<p>不同事件對應不同準備強度、第一步要分類。</p>
<p><strong>可預期極端峰值</strong>：年度活動、預售、賽事決賽。提前數月已知時間、業務影響大。例：Prime Day、Black Friday、Super Bowl、IPL 決賽。
<strong>事件型不可預期峰值</strong>：賽事高潮、突發新聞、KOL 推廣。時間或大小不完全可預測。例：賽事進球瞬間、KOL 帶貨、突發新聞引發的流量。
<strong>Flash-sale 瞬間爆量</strong>：售票開賣、報名活動、限量搶購。t=0 瞬間爆量、5-30 分鐘結束。例：演唱會售票、限量商品搶購、報名截止前最後一小時。
<strong>產品爆紅 surge</strong>：新 app 紅、病毒擴散。完全不可預期、流量會隨熱度消退。例：Pokemon GO、ChatGPT 爆紅初期、TikTok challenge。
<strong>結構性 surge</strong>：COVID 類外部衝擊、永久 baseline 上移。不會回到舊水準。例：COVID 期間遠距工作工具、烏俄戰爭期間能源類 app。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C1 / 9.C13 / 9.C21 / 9.C27 / 9.C29</a>（predictable）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C2 / 9.C4 / 9.C7 / 9.C28</a>（event）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C15 / 9.C16 / 9.C17</a>（flash-sale）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C8 / 9.C18</a>（surge）。</p>
<h2 id="t-90--t-0-準備時程">T-90 → T-0 準備時程</h2>
<p>可預期極端峰值的完整準備時程：</p>
<p><strong>T-90 天</strong>：流量 forecast + 容量計畫敲定。確認預期峰值倍數、確認 headroom 比例、確認跨 region / AZ 分布。產出 <em>容量計畫文件</em>。</p>
<p><strong>T-30 天</strong>：基礎設施 quota 申請。雲端 instance limit、connection pool、API rate limit、DynamoDB throughput、Lambda concurrency 都要 <em>提前申請</em>、不能事件當天才發現 quota 不夠。AWS Infrastructure Event Management（IEM）等服務在這階段啟動。</p>
<p><strong>T-14 天</strong>：第一輪 production-like 壓測。驗證容量計畫是否真的能撐預期峰值、找出第一輪 bottleneck。</p>
<p><strong>T-7 天</strong>：完整 game day 演練。注入故障場景（DB failure、AZ outage、第三方 quota 耗盡）、驗證降級、failover、rollback 流程。修正最後問題、更新 runbook。</p>
<p><strong>T-2 天</strong>：pre-scaling 開始。CDN cache pre-warm、Lambda provisioned concurrency 啟動、autoscaler scheduled 開始、DB capacity 預先 scale up。避免事件當天還在 boot。</p>
<p><strong>T-0 day</strong>：watch room 待命、runbook 開機可執行。所有相關 oncall 跨團隊聯合 channel、dashboard 集中、escalation path 清楚。</p>
<p><strong>T+7 天</strong>：retro。對比預測 vs 實際、紀錄 incident 跟 near-miss、列下個事件要改的事。寫進 <a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases</a> 或本模組 cases。</p>
<h2 id="pre-scaling-策略">Pre-scaling 策略</h2>
<p>T-2 階段的 pre-scaling 是「不依賴 autoscaler 反應」的容量保險。</p>
<p><strong>Pre-scaling 涵蓋層次</strong>：</p>
<ul>
<li><strong>ELB warm-up</strong>：請 AWS 預先 warm up ELB，避免流量上來時 ELB 自身需要時間擴容</li>
<li><strong>Lambda provisioned concurrency</strong>：預先 boot 一定數量 instance、避免 cold start</li>
<li><strong>DynamoDB / Cosmos DB capacity</strong>：scheduled 提前 scale up</li>
<li><strong>EC2 ASG</strong>：min instances 提前拉高</li>
<li><strong>CDN cache pre-warm</strong>：重要 URL 提前 invalidate / pre-populate</li>
<li><strong>DB connection pool</strong>：應用層提前 warm up connection</li>
<li><strong>Cache warmup</strong>：把 hot key 提前 populate 進 cache</li>
</ul>
<p><strong>Pre-warm window 通常 30 分鐘到 2 小時</strong>、取決於：</p>
<ul>
<li>Instance boot time（VM-based 慢、container 快）</li>
<li>Cache warmup 時間（cold cache 命中率低、要時間 populate）</li>
<li>Connection pool 預熱（DB connection establish 有 latency）</li>
</ul>
<h3 id="cdn-pre-warm-操作細節">CDN Pre-warm 操作細節</h3>
<p>CDN pre-warm 在 T-2 階段是 high-impact 操作、但跟其他 pre-scaling 的特性不同。具體做法：</p>
<ul>
<li><strong>找出活動會大量被讀取的 URL 清單</strong>：商品頁、活動 landing page、新 release 內容</li>
<li><strong>在每個 CDN edge POP 觸發 cache populate</strong>：可以用 vendor warmup API（Cloudflare Argo、Fastly Image Optimizer pre-fetch、Akamai NetStorage push），或從多個 region 發 synthetic request 強制 edge 拉取</li>
<li><strong>驗證 hit ratio 已升高</strong>：用 vendor dashboard 觀察 cache_status=HIT 比例、確認 pre-warm 生效</li>
<li><strong>預估 origin 流量曲線</strong>：pre-warm 完成後、活動開始時 edge miss 流量應該大幅降低、origin 容量規劃可以對應放鬆</li>
</ul>
<p>跟其他 pre-scaling 不同的是 <strong>CDN pre-warm 沒有「容量上限」這個概念</strong> — edge cache 是被動填的、warm 完就是 warm、不像 EC2 / Lambda 那樣需要 reserve 容量。風險不在「填不夠」、在「填錯」（key 不對、TTL 設錯讓 pre-warm 立刻過期）。詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發</a> 的 purge 與 cacheable 判讀。</p>
<p><strong>事件結束後也要 <em>scheduled scale down</em></strong>：autoscaler 通常 scale up 快、scale down 慢、長期 over-provision 浪費錢。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 30 分鐘擴 130 倍</a> — pre-scaling + Auto Scaling Group + AMI prebuild + ELB warmup 組合；<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day pre-scaling</a> — predictive scaling + scheduled scaling 兩種組合。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling 卡片</a> 跟 <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling 卡片</a>。</p>
<h2 id="watch-room-設計">Watch room 設計</h2>
<p>T-0 當天的指揮中心、跨團隊聯合 channel。</p>
<p><strong>人員配置</strong>：</p>
<ul>
<li>跨團隊聯合 channel：app / infra / network / SRE / business / customer support</li>
<li>24/7 輪班（國際事件可能跨 24 小時）</li>
<li>明確 incident commander（<a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">08.7 incident command roles</a>）</li>
</ul>
<p><strong>Dashboard 集中</strong>：</p>
<ul>
<li>流量 dashboard：總 RPS、按 region 拆分、按 endpoint 拆分</li>
<li>延遲 dashboard：p50 / p95 / p99 即時、按 service 拆分</li>
<li>錯誤 dashboard：error rate、按 endpoint、按 status code</li>
<li>成本 dashboard：當前 hourly cost、預估全天 cost</li>
<li>業務 dashboard：訂單數、轉換率、收入</li>
</ul>
<p><strong>Runbook 隨手可用</strong>：常見問題 → 對應動作的明確指引。不要事件當下還在 wiki 找資料。</p>
<p><strong>Escalation path</strong>：什麼狀況找誰、多久升級。寫成決策樹、不要靠人記。對應 <a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">08.7 incident command roles</a>。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day 卡片</a>。</p>
<h2 id="vendor-緊急支援">Vendor 緊急支援</h2>
<p>戰略事件可以申請 vendor 工程師待命、是「人力 backup」。</p>
<p><strong>AWS Infrastructure Event Management（IEM）</strong>：年度重大事件可以申請、提供 pre-scaling 與專屬監控通道。
<strong>GCP Customer Reliability Engineering（CRE）</strong>：戰略客戶的 24/7 工程支援、能即時為客戶補容量。
<strong>Azure Premier Support + CSAM</strong>：對等服務。</p>
<p><strong>注意</strong>：這類服務通常綁定 enterprise 等級合約、不是所有客戶都能用。設計事件準備時要假設「沒有 vendor 救援」、vendor 是 bonus 而非 primary plan。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech World Cup IEM</a> — AWS Infrastructure Event Management 在 2022 FIFA World Cup 期間支援；<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO CRE</a> — GCP CRE 即時補容量、撐過 50x surge。</p>
<h2 id="game-day-演練">Game day 演練</h2>
<p>T-7 階段的核心活動、把 readiness 從計畫變實戰。</p>
<p><strong>演練場景</strong>：</p>
<ul>
<li>模擬「事件當天 worst case」</li>
<li>注入故障：DB primary failure、AZ outage、第三方 quota 達標、network partition</li>
<li>演練降級：哪些功能關閉、用戶看到什麼</li>
<li>演練 failover：流量切到備援</li>
<li>演練 rollback：發現新版本問題、能不能快速回退</li>
</ul>
<p><strong>Game day 學習目標</strong>：</p>
<ul>
<li>runbook 不夠詳細 → 補</li>
<li>訊號不夠 → 加 metric / alert</li>
<li>人員不夠 → 排班補</li>
<li>工具不夠 → 工程補</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">06 cases Shopify game day</a> — Shopify game day 是業界範本、值得直接參考。</p>
<h2 id="event-tier-分級">Event tier 分級</h2>
<p>不同事件規模對應不同準備強度、不能一律照 T-90 流程跑。</p>
<p><strong>Regular event</strong>（每週 promo、small feature launch）：</p>
<ul>
<li>scheduled scaling 即可</li>
<li>無 dedicated watch room</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a> 的常規 release</li>
</ul>
<p><strong>Major event</strong>（季度行銷、新功能發布）：</p>
<ul>
<li>pre-scaling + watch room</li>
<li>簡化版 T-14 → T-0 流程</li>
<li>跨 team coordination</li>
</ul>
<p><strong>Critical event</strong>（年度大促、Super Bowl、IPL）：</p>
<ul>
<li>完整 T-90 流程</li>
<li>vendor IEM + game day</li>
<li>24/7 watch room</li>
<li>C-level visibility</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel</a> regular game → playoff → Super Bowl 三 tier — NFL 賽季 baseline → playoffs 升 2-3x → championship 升 4-5x → Super Bowl 升 5-10x、每 tier 對應不同準備強度。</p>
<h2 id="事後-retro">事後 retro</h2>
<p>T+7 retro 是讓 readiness 持續改進的關鍵。</p>
<p><strong>Retro 必答的問題</strong>：</p>
<ul>
<li>流量 forecast 跟實際差多少？（forecast 改進方向）</li>
<li>容量 utilization 峰值多少？（headroom 是否合適）</li>
<li>有沒有 incident 跟 near-miss？（runbook 更新方向）</li>
<li>下個事件要改的事是什麼？</li>
</ul>
<p><strong>Retro 產出</strong>：</p>
<ul>
<li>forecast 改進建議（給 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6</a>）</li>
<li>新 runbook 或 runbook 更新</li>
<li>新 monitoring / alert</li>
<li>新工程任務（補容量、補工具）</li>
</ul>
<p>對應 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">08.13 post-incident review</a> — retro 不只用在 incident、event readiness 也需要。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a></td>
          <td>可預期極端峰值教科書範本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>flash-sale T-2 pre-scaling</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a></td>
          <td>全球直播 watch room</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a></td>
          <td>AWS IEM + 自家 AI 預測組合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>event tier 分級（playoff → SB）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO</a></td>
          <td>surge 場景的 vendor 救援（CRE）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> / <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>（pre-scaling 前要分辨可不可水平擴展）</li>
<li>跨模組：<a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a>（CDN pre-warm / origin protection 是 T-2 核心）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> / <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理模組</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day</a></li>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/</guid><description>&lt;p>這個案例的核心責任是說明「全球分散式 multi-model DB」的容量設計取捨。Minecraft Earth 是 AR 手機遊戲（已停運、但案例本身保留）、跟 Pokémon GO 同類負載 — 玩家位置即時更新、跨地區即時互動、預期會在熱門地區 surge。Cosmos DB 的設計回應這類「跨地區 + 多 model」需求。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Minecraft Earth 在 Azure Cosmos DB 的關鍵敘述（引自 &lt;a href="https://azure.microsoft.com/en-us/blog/minecraft-earth-and-azure-cosmos-db-part-2-delivering-turnkey-geographic-distribution/">Minecraft Earth and Azure Cosmos DB&lt;/a>）：&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>容量測試&lt;/td>
 &lt;td>100 萬 RU/s（Request Units / 秒）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲承諾&lt;/td>
 &lt;td>99 百分位 &amp;lt; 10ms（地區內讀）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性選項&lt;/td>
 &lt;td>5 個一致性層級（strong → eventual）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>地理分散&lt;/td>
 &lt;td>turnkey global distribution&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性 SLA&lt;/td>
 &lt;td>99.99%（multi-region 99.999%）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Cosmos DB 平台特性（引自 &lt;a href="https://azure.microsoft.com/en-us/blog/a-technical-overview-of-azure-cosmos-db/">Cosmos DB technical overview&lt;/a>）：&lt;/p>
&lt;ul>
&lt;li>配置擴容延遲：99 百分位 5 秒內生效&lt;/li>
&lt;li>多 model 支援：SQL API、MongoDB API、Cassandra API、Gremlin、Table&lt;/li>
&lt;li>partition 動態分裂：透明&lt;/li>
&lt;li>5 個 well-defined consistency levels（strong / bounded staleness / session / consistent prefix / eventual）&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Cosmos DB 設計揭露三個全球 KV / document DB 的容量設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>一致性是 spectrum、不是 binary&lt;/strong>：Cosmos DB 提供 5 個層級、每個延遲與吞吐特性不同。AR 遊戲的玩家位置不需要 strong consistency（位置稍微 stale 沒問題）、但庫存交易需要 strong。同一 application 內不同操作選不同 consistency、是進階的容量設計策略。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a> 的一致性取捨。&lt;/li>
&lt;li>&lt;strong>Request Unit (RU) 是抽象容量單位&lt;/strong>：1 RU = 1 KB document 的 strong read 成本、寫成本約 5 RU、複雜 query 可達數百 RU。容量規劃變成「估每個操作多少 RU × 操作頻率」、跟「估 CPU / IOPS」是不同的思維。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的容量單位設計。&lt;/li>
&lt;li>&lt;strong>turnkey global distribution = 容量單位的全球複製&lt;/strong>：開啟跨地區後、容量在每個地區都 mirror 一份、成本乘以地區數。對中等規模團隊、turnkey 省下大量 ops、但要算「全球複製的成本是否值得業務需求」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「100 萬 RU/s 通過測試」是 &lt;em>壓測通過&lt;/em>、不是 &lt;em>生產持續跑&lt;/em>。實際營運要看 partition key 設計是否均勻、是否有 hot partition、跨地區複製延遲是否符合業務需求。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>一致性需求分流到不同 collection / table&lt;/strong>：同一 application 不同操作有不同一致性需求、用不同 collection 配不同 consistency level、不要一刀切。&lt;/li>
&lt;li>&lt;strong>partition key 設計影響容量上限&lt;/strong>：跟 DynamoDB 一樣、hot partition 會讓名義容量達不到。Cosmos DB 的特殊性是「synthetic partition key」可以混合多個 field 強制分散。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 hot partition 識別。&lt;/li>
&lt;li>&lt;strong>RU-based pricing 鼓勵 query 最佳化&lt;/strong>：每個 expensive query 都吃 RU、優化 query 直接降成本。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9 Performance Improvement Loop&lt;/a> 的持續改進迴圈。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS DynamoDB Global Tables（global KV）、GCP Spanner（global SQL with strong consistency）、ScyllaDB Cloud（自管 Cassandra）都是對等候選。差異是 multi-model 廣度（Cosmos 最廣）vs 一致性深度（Spanner 最強）。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「全球分散式 multi-model DB」的容量設計取捨。Minecraft Earth 是 AR 手機遊戲（已停運、但案例本身保留）、跟 Pokémon GO 同類負載 — 玩家位置即時更新、跨地區即時互動、預期會在熱門地區 surge。Cosmos DB 的設計回應這類「跨地區 + 多 model」需求。</p>
<h2 id="觀察">觀察</h2>
<p>Minecraft Earth 在 Azure Cosmos DB 的關鍵敘述（引自 <a href="https://azure.microsoft.com/en-us/blog/minecraft-earth-and-azure-cosmos-db-part-2-delivering-turnkey-geographic-distribution/">Minecraft Earth and Azure Cosmos DB</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字 / 內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>容量測試</td>
          <td>100 萬 RU/s（Request Units / 秒）</td>
      </tr>
      <tr>
          <td>延遲承諾</td>
          <td>99 百分位 &lt; 10ms（地區內讀）</td>
      </tr>
      <tr>
          <td>一致性選項</td>
          <td>5 個一致性層級（strong → eventual）</td>
      </tr>
      <tr>
          <td>地理分散</td>
          <td>turnkey global distribution</td>
      </tr>
      <tr>
          <td>可用性 SLA</td>
          <td>99.99%（multi-region 99.999%）</td>
      </tr>
  </tbody>
</table>
<p>Cosmos DB 平台特性（引自 <a href="https://azure.microsoft.com/en-us/blog/a-technical-overview-of-azure-cosmos-db/">Cosmos DB technical overview</a>）：</p>
<ul>
<li>配置擴容延遲：99 百分位 5 秒內生效</li>
<li>多 model 支援：SQL API、MongoDB API、Cassandra API、Gremlin、Table</li>
<li>partition 動態分裂：透明</li>
<li>5 個 well-defined consistency levels（strong / bounded staleness / session / consistent prefix / eventual）</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>Cosmos DB 設計揭露三個全球 KV / document DB 的容量設計重點。</p>
<ol>
<li><strong>一致性是 spectrum、不是 binary</strong>：Cosmos DB 提供 5 個層級、每個延遲與吞吐特性不同。AR 遊戲的玩家位置不需要 strong consistency（位置稍微 stale 沒問題）、但庫存交易需要 strong。同一 application 內不同操作選不同 consistency、是進階的容量設計策略。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的一致性取捨。</li>
<li><strong>Request Unit (RU) 是抽象容量單位</strong>：1 RU = 1 KB document 的 strong read 成本、寫成本約 5 RU、複雜 query 可達數百 RU。容量規劃變成「估每個操作多少 RU × 操作頻率」、跟「估 CPU / IOPS」是不同的思維。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的容量單位設計。</li>
<li><strong>turnkey global distribution = 容量單位的全球複製</strong>：開啟跨地區後、容量在每個地區都 mirror 一份、成本乘以地區數。對中等規模團隊、turnkey 省下大量 ops、但要算「全球複製的成本是否值得業務需求」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
</ol>
<p>需要警惕：「100 萬 RU/s 通過測試」是 <em>壓測通過</em>、不是 <em>生產持續跑</em>。實際營運要看 partition key 設計是否均勻、是否有 hot partition、跨地區複製延遲是否符合業務需求。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>一致性需求分流到不同 collection / table</strong>：同一 application 不同操作有不同一致性需求、用不同 collection 配不同 consistency level、不要一刀切。</li>
<li><strong>partition key 設計影響容量上限</strong>：跟 DynamoDB 一樣、hot partition 會讓名義容量達不到。Cosmos DB 的特殊性是「synthetic partition key」可以混合多個 field 強制分散。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 hot partition 識別。</li>
<li><strong>RU-based pricing 鼓勵 query 最佳化</strong>：每個 expensive query 都吃 RU、優化 query 直接降成本。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9 Performance Improvement Loop</a> 的持續改進迴圈。</li>
</ol>
<p>跨平台等效：AWS DynamoDB Global Tables（global KV）、GCP Spanner（global SQL with strong consistency）、ScyllaDB Cloud（自管 Cassandra）都是對等候選。差異是 multi-model 廣度（Cosmos 最廣）vs 一致性深度（Spanner 最強）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計全球分散 KV → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想對照強一致全球 OLTP → <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想對照單區 KV 高吞吐 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a></li>
<li>想理解 consistency level 的取捨 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a></li>
<li>想理解 Cosmos DB 五層一致性的工程選擇 → <a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">Cosmos DB 一致性層次工程</a></li>
<li>想做全球 multi-region write 衝突收斂 → <a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB 多 region write 衝突</a></li>
<li>想拆 partition key 設計與全球分散搭配 → <a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">Cosmos DB partition key 設計</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://azure.microsoft.com/en-us/blog/minecraft-earth-and-azure-cosmos-db-part-2-delivering-turnkey-geographic-distribution/">Minecraft Earth and Azure Cosmos DB part 2: Delivering turnkey geographic distribution</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/a-technical-overview-of-azure-cosmos-db/">A technical overview of Azure Cosmos DB</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/azure-cosmos-db-pushing-the-frontier-of-globally-distributed-databases/">Azure Cosmos DB: Pushing the frontier of globally distributed databases</a></li>
</ul>
]]></content:encoded></item><item><title>Google：Error Budget 政策如何決定發布節奏</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/</guid><description>&lt;p>Error budget policy 的核心責任是把「可靠性目標」轉成「發布節奏控制」。團隊不需要在每次風險升高時重新爭論要不要繼續推版，而是用同一套 SLO 消耗判準決定放行、限流或凍結。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>高變更頻率服務最常見的失效是小幅回歸連續累積，單點故障反而少見。每次回歸都不夠大，不會立刻觸發全停；但連續幾週後，使用者體感持續惡化，團隊才發現可靠性債已經超標。&lt;/p>
&lt;p>這種情境需要的是「連續消耗判讀」，不是單次事故判讀。error budget policy 就是把連續消耗變成可操作的放行規則。&lt;/p>
&lt;h2 id="決策機制">決策機制&lt;/h2>
&lt;p>政策設計先做三個對齊，再做門檻定義。&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>使用者行為對齊&lt;/td>
 &lt;td>哪些 journey 直接反映服務價值&lt;/td>
 &lt;td>SLI 範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可靠性承諾對齊&lt;/td>
 &lt;td>什麼水準算服務仍可接受&lt;/td>
 &lt;td>SLO 目標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付節奏對齊&lt;/td>
 &lt;td>可靠性消耗到哪裡要改變發布策略&lt;/td>
 &lt;td>Budget gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>有了這三個對齊後，release gate 可以從「主觀風險判斷」轉成「政策驅動」：&lt;/p>
&lt;ol>
&lt;li>budget 健康：正常發版。&lt;/li>
&lt;li>budget 快速消耗：啟用變更限速、提高驗證門檻。&lt;/li>
&lt;li>budget 透支：凍結非必要變更，先修復與回補訊號。&lt;/li>
&lt;/ol>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&lt;p>政策有效與否要靠訊號判讀，不靠會議共識。&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>burn rate&lt;/td>
 &lt;td>是否進入短期高消耗區&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>release failure ratio&lt;/td>
 &lt;td>發版後回歸是否集中&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>alert noise&lt;/td>
 &lt;td>告警是否支持 gate 判讀&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recovery latency&lt;/td>
 &lt;td>凍結後修復是否收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把 error budget 當 KPI 會讓政策失真。這個機制的責任是「保護可靠性與交付節奏的平衡」，不是讓團隊追求某個固定分數。當 KPI 化開始主導行為，常見結果是 SLI 縮小、告警延後或例外條件過度擴張，最終反而降低判讀可信度。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要把這個案例落到制度層，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6&lt;/a> 定義政策欄位，再到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a> 實作 gate。若你發現訊號不足，先補 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Error budget policy 的核心責任是把「可靠性目標」轉成「發布節奏控制」。團隊不需要在每次風險升高時重新爭論要不要繼續推版，而是用同一套 SLO 消耗判準決定放行、限流或凍結。</p>
<h2 id="問題場景">問題場景</h2>
<p>高變更頻率服務最常見的失效是小幅回歸連續累積，單點故障反而少見。每次回歸都不夠大，不會立刻觸發全停；但連續幾週後，使用者體感持續惡化，團隊才發現可靠性債已經超標。</p>
<p>這種情境需要的是「連續消耗判讀」，不是單次事故判讀。error budget policy 就是把連續消耗變成可操作的放行規則。</p>
<h2 id="決策機制">決策機制</h2>
<p>政策設計先做三個對齊，再做門檻定義。</p>
<table>
  <thead>
      <tr>
          <th>對齊項目</th>
          <th>核心問題</th>
          <th>產出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者行為對齊</td>
          <td>哪些 journey 直接反映服務價值</td>
          <td>SLI 範圍</td>
      </tr>
      <tr>
          <td>可靠性承諾對齊</td>
          <td>什麼水準算服務仍可接受</td>
          <td>SLO 目標</td>
      </tr>
      <tr>
          <td>交付節奏對齊</td>
          <td>可靠性消耗到哪裡要改變發布策略</td>
          <td>Budget gate</td>
      </tr>
  </tbody>
</table>
<p>有了這三個對齊後，release gate 可以從「主觀風險判斷」轉成「政策驅動」：</p>
<ol>
<li>budget 健康：正常發版。</li>
<li>budget 快速消耗：啟用變更限速、提高驗證門檻。</li>
<li>budget 透支：凍結非必要變更，先修復與回補訊號。</li>
</ol>
<h2 id="可觀測訊號">可觀測訊號</h2>
<p>政策有效與否要靠訊號判讀，不靠會議共識。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>burn rate</td>
          <td>是否進入短期高消耗區</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a></td>
      </tr>
      <tr>
          <td>release failure ratio</td>
          <td>發版後回歸是否集中</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>alert noise</td>
          <td>告警是否支持 gate 判讀</td>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a></td>
      </tr>
      <tr>
          <td>recovery latency</td>
          <td>凍結後修復是否收斂</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把 error budget 當 KPI 會讓政策失真。這個機制的責任是「保護可靠性與交付節奏的平衡」，不是讓團隊追求某個固定分數。當 KPI 化開始主導行為，常見結果是 SLI 縮小、告警延後或例外條件過度擴張，最終反而降低判讀可信度。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要把這個案例落到制度層，先回到 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a> 定義政策欄位，再到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 實作 gate。若你發現訊號不足，先補 <a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16</a> 與 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a>。</p>
]]></content:encoded></item><item><title>Slack：2022 連線恢復與狀態通訊節奏</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/</guid><description>&lt;p>這起案例的核心責任是維持「恢復動作」與「外部通訊」同步。對通訊平台來說，狀態揭露本身就是事故處理的一級控制面。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>reconnect spike&lt;/td>
 &lt;td>回復是否造成新一輪壓力&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>status update cadence&lt;/td>
 &lt;td>對外節奏是否穩定&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>workspace impact spread&lt;/td>
 &lt;td>影響是否跨租戶擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「連線恢復節奏」與「對外通訊節奏」必須同步。主要風險是恢復動作先行但通訊滯後，造成客戶端行為與狀態頁資訊脫節。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先保住連線層穩態，再做狀態同步。事故後把通訊節奏與指揮欄位回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是維持「恢復動作」與「外部通訊」同步。對通訊平台來說，狀態揭露本身就是事故處理的一級控制面。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>reconnect spike</td>
          <td>回復是否造成新一輪壓力</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>status update cadence</td>
          <td>對外節奏是否穩定</td>
          <td><a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a></td>
      </tr>
      <tr>
          <td>workspace impact spread</td>
          <td>影響是否跨租戶擴散</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「連線恢復節奏」與「對外通訊節奏」必須同步。主要風險是恢復動作先行但通訊滯後，造成客戶端行為與狀態頁資訊脫節。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先保住連線層穩態，再做狀態同步。事故後把通訊節奏與指揮欄位回寫 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 與 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a>。</p>
]]></content:encoded></item><item><title>後端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/backend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/backend-deploy/</guid><description>&lt;p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration&lt;/a>）、外部依賴、runtime config、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check&lt;/a>）、流量切換與 rollback。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。&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>Build&lt;/td>
 &lt;td>binary、package、container image&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>unit、integration、contract、migration&lt;/td>
 &lt;td>是否覆蓋跨服務契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>schema change、backfill、rollback path&lt;/td>
 &lt;td>是否可漸進、可停止、可驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a>&lt;/td>
 &lt;td>rolling、canary、blue-green&lt;/td>
 &lt;td>health / readiness 是否可信&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>app rollback、migration rollback / forward fix&lt;/td>
 &lt;td>回復路徑是否演練&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。&lt;/p>
&lt;p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。&lt;/li>
&lt;li>Health check 只代表 process alive，readiness 才能判斷能否接流量。&lt;/li>
&lt;li>Worker / consumer 部署要考慮重複處理、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>。&lt;/li>
&lt;li>Config rollout 需要版本化與回退路徑（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a>）。&lt;/li>
&lt;li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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>&lt;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>&lt;/td>
 &lt;td>Migration rollout and rollback&lt;/td>
 &lt;td>拆分資料變更、流量推進與回復路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>後端發布主流程：讀 &lt;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Backend reliability：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程&lt;/a>。&lt;/li>
&lt;li>Release gate：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration</a>）、外部依賴、runtime config、<a href="/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness</a> / <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check</a>）、流量切換與 rollback。</p>
<h2 id="場域定位">場域定位</h2>
<p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>後端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>binary、package、container image</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>unit、integration、contract、migration</td>
          <td>是否覆蓋跨服務契約</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>schema change、backfill、rollback path</td>
          <td>是否可漸進、可停止、可驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a></td>
          <td>rolling、canary、blue-green</td>
          <td>health / readiness 是否可信</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>app rollback、migration rollback / forward fix</td>
          <td>回復路徑是否演練</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。</p>
<p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。</p>
<p><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。</li>
<li>Health check 只代表 process alive，readiness 才能判斷能否接流量。</li>
<li>Worker / consumer 部署要考慮重複處理、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>。</li>
<li>Config rollout 需要版本化與回退路徑（深入見 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a>）。</li>
<li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy</a>）。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a></td>
          <td>Migration rollout and rollback</td>
          <td>拆分資料變更、流量推進與回復路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>後端發布主流程：讀 <a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Backend reliability：讀 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a>。</li>
<li>Release gate：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
</ul>
]]></content:encoded></item><item><title>4.11 Telemetry Pipeline 架構</title><link>https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何要把 telemetry 當 pipeline 看：每層有獨立失敗模式與成本邊界&lt;/li>
&lt;li>分層責任：agent（採集）、collector（聚合 / 轉換）、ingest（寫入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>）、storage（保留 / 查詢）、query（dashboard / alert）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>：collector 端緩衝、ingest 滿時的降級策略&lt;/li>
&lt;li>OpenTelemetry Collector 的角色：vendor-neutral 中介層&lt;/li>
&lt;li>pipeline 失敗時的 graceful &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation&lt;/a>：訊號斷一層、其他層仍可用&lt;/li>
&lt;li>multi-tenant 環境的 quota / 隔離&lt;/li>
&lt;li>觀測遷移流程：先換 collector 再換 instrumentation、雙軌期保留對照&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality&lt;/a> 的分工：4.7 是治理輸入、4.11 是 pipeline 執行&lt;/li>
&lt;li>反模式：pipeline 是黑盒、無 self-monitoring；agent 直連 vendor 無 collector 中介；ingest 滿時直接 drop 無告警&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Telemetry pipeline 是把訊號從 service process 帶到查詢與告警面的資料路徑，責任是讓採集、轉換、寫入、儲存與查詢各層都有可觀測的邊界。&lt;/p>
&lt;p>這一頁處理的是觀測系統本身的可靠性。當 pipeline 是黑盒，訊號消失時團隊需要額外排查服務是否真的沒事件，或 agent、collector、ingest、query 哪一層失效。&lt;/p>
&lt;p>Pipeline 視角的另一個價值是把採集策略跟儲存後端解耦。應用層只需要產生標準訊號，pipeline 處理 schema 轉換、sampling、enrichment、routing 與 vendor 對接；當儲存後端或 vendor 改變時，應用層不必重新 instrument。&lt;/p>
&lt;h2 id="分層責任與失敗模式">分層責任與失敗模式&lt;/h2>
&lt;p>Pipeline 各層責任不同，失敗模式也不同。把 pipeline 視為單一黑盒會讓事故定位停在「訊號不見了」這層觀察，無法回答是哪一層的問題。&lt;/p>
&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>Agent&lt;/td>
 &lt;td>從 process / host 抓取原始訊號&lt;/td>
 &lt;td>升版需重啟、container restart 造成短期缺洞&lt;/td>
 &lt;td>export queue depth、dropped batches&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Collector&lt;/td>
 &lt;td>聚合、轉換、enrichment、routing&lt;/td>
 &lt;td>OOM、配置漂移、規則衝突&lt;/td>
 &lt;td>receiver / processor / exporter 指標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ingest&lt;/td>
 &lt;td>接收並寫入 buffer 或排隊&lt;/td>
 &lt;td>滿載拒收（429）、區域故障&lt;/td>
 &lt;td>ingestion success rate、queue depth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>保留資料、支援查詢索引&lt;/td>
 &lt;td>索引膨脹、保留策略誤刪、查詢退化&lt;/td>
 &lt;td>storage size、query latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>dashboard / alert / 即席查詢&lt;/td>
 &lt;td>查詢逾時、aggregate 失真、permission 漂移&lt;/td>
 &lt;td>query QPS、p95 latency、permission 拒絕&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Agent 層的關鍵風險是部署綁定。若 agent 跟應用同進程，升版需要重啟服務；若 agent 是獨立 DaemonSet 或 sidecar，升版可以獨立進行，但要承擔網路與資源額外開銷。Agent 自身故障時，service 看起來健康，dashboard 看起來空，事故指揮會把這個空白誤讀成系統靜默。&lt;/p>
&lt;p>Collector 層是 pipeline 最有彈性的地方，也是最容易漏掉自我觀測的地方。OpenTelemetry Collector 的 receiver / processor / exporter 各自有 metrics，部署時要把這些 metrics 自身送回觀測平台。配置漂移是長期維護的主要失敗：sampling 規則改了沒紀錄、attribute 重命名沒同步、tail sampling decision window 縮短，都會讓下游看到的訊號跟以前不同。Collector 的三種部署位置（agent / gateway / sidecar）與 pipeline 設計細節見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何要把 telemetry 當 pipeline 看：每層有獨立失敗模式與成本邊界</li>
<li>分層責任：agent（採集）、collector（聚合 / 轉換）、ingest（寫入 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>）、storage（保留 / 查詢）、query（dashboard / alert）</li>
<li><a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>：collector 端緩衝、ingest 滿時的降級策略</li>
<li>OpenTelemetry Collector 的角色：vendor-neutral 中介層</li>
<li>pipeline 失敗時的 graceful <a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a>：訊號斷一層、其他層仍可用</li>
<li>multi-tenant 環境的 quota / 隔離</li>
<li>觀測遷移流程：先換 collector 再換 instrumentation、雙軌期保留對照</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> 的分工：4.7 是治理輸入、4.11 是 pipeline 執行</li>
<li>反模式：pipeline 是黑盒、無 self-monitoring；agent 直連 vendor 無 collector 中介；ingest 滿時直接 drop 無告警</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Telemetry pipeline 是把訊號從 service process 帶到查詢與告警面的資料路徑，責任是讓採集、轉換、寫入、儲存與查詢各層都有可觀測的邊界。</p>
<p>這一頁處理的是觀測系統本身的可靠性。當 pipeline 是黑盒，訊號消失時團隊需要額外排查服務是否真的沒事件，或 agent、collector、ingest、query 哪一層失效。</p>
<p>Pipeline 視角的另一個價值是把採集策略跟儲存後端解耦。應用層只需要產生標準訊號，pipeline 處理 schema 轉換、sampling、enrichment、routing 與 vendor 對接；當儲存後端或 vendor 改變時，應用層不必重新 instrument。</p>
<h2 id="分層責任與失敗模式">分層責任與失敗模式</h2>
<p>Pipeline 各層責任不同，失敗模式也不同。把 pipeline 視為單一黑盒會讓事故定位停在「訊號不見了」這層觀察，無法回答是哪一層的問題。</p>
<table>
  <thead>
      <tr>
          <th>分層</th>
          <th>主要責任</th>
          <th>典型失敗模式</th>
          <th>健康訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Agent</td>
          <td>從 process / host 抓取原始訊號</td>
          <td>升版需重啟、container restart 造成短期缺洞</td>
          <td>export queue depth、dropped batches</td>
      </tr>
      <tr>
          <td>Collector</td>
          <td>聚合、轉換、enrichment、routing</td>
          <td>OOM、配置漂移、規則衝突</td>
          <td>receiver / processor / exporter 指標</td>
      </tr>
      <tr>
          <td>Ingest</td>
          <td>接收並寫入 buffer 或排隊</td>
          <td>滿載拒收（429）、區域故障</td>
          <td>ingestion success rate、queue depth</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>保留資料、支援查詢索引</td>
          <td>索引膨脹、保留策略誤刪、查詢退化</td>
          <td>storage size、query latency</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>dashboard / alert / 即席查詢</td>
          <td>查詢逾時、aggregate 失真、permission 漂移</td>
          <td>query QPS、p95 latency、permission 拒絕</td>
      </tr>
  </tbody>
</table>
<p>Agent 層的關鍵風險是部署綁定。若 agent 跟應用同進程，升版需要重啟服務；若 agent 是獨立 DaemonSet 或 sidecar，升版可以獨立進行，但要承擔網路與資源額外開銷。Agent 自身故障時，service 看起來健康，dashboard 看起來空，事故指揮會把這個空白誤讀成系統靜默。</p>
<p>Collector 層是 pipeline 最有彈性的地方，也是最容易漏掉自我觀測的地方。OpenTelemetry Collector 的 receiver / processor / exporter 各自有 metrics，部署時要把這些 metrics 自身送回觀測平台。配置漂移是長期維護的主要失敗：sampling 規則改了沒紀錄、attribute 重命名沒同步、tail sampling decision window 縮短，都會讓下游看到的訊號跟以前不同。Collector 的三種部署位置（agent / gateway / sidecar）與 pipeline 設計細節見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式</a>。</p>
<p>Ingest 層的失敗模式集中在容量邊界。當 vendor 端 quota 觸發或內部 queue 滿，ingest 會回 429 或直接丟棄；應用層通常無感、dashboard 顯示流量下降。這層需要把拒收事件本身變成告警訊號、讓事故定位即時看到拒收量、避免靠事後對賬發現。</p>
<p>Storage 跟 query 層的失敗多半是漸進式：保留策略誤刪、查詢隨時間退化、索引隨流量膨脹。這類失敗不會在當下觸發告警，要靠週期性審視 storage size、query latency 與 retention compliance 才能發現。</p>
<h2 id="buffer-與-backpressure">Buffer 與 Backpressure</h2>
<p>Buffer 是 pipeline 吸收瞬時尖峰的緩衝，責任是讓 collector 跟 ingest 在後端短暫故障或速率不足時仍保住高價值訊號。</p>
<ul>
<li><strong>In-memory queue</strong>：吸收秒級尖峰、容量小、process 重啟會丟。</li>
<li><strong>Persistent queue</strong>（local disk、Kafka）：吸收分鐘到小時級積壓、有持久性、需要額外運維成本。</li>
<li><strong>Spillover storage</strong>（S3 等冷儲存）：當 hot path 滿載時，把低優先訊號暫存到便宜後端、之後 replay。</li>
</ul>
<p>Backpressure 策略決定 buffer 滿時的行為。<code>block</code> 策略會讓上游採集慢下來、可能影響應用；<code>drop oldest</code> 跟 <code>drop newest</code> 各自影響 timeline 的開始或結束；<code>sample-by-priority</code> 則保留錯誤、長尾與低流量樣本、丟棄一般成功 request。Buffer 跟 backpressure 策略要在容量規劃階段顯式設定、進 release flow、避免事故時臨時拍定。</p>
<p>Buffer 對事故判讀的影響是 freshness。當 buffer 累積分鐘級資料時，dashboard 看到的指標其實落後當前狀態；incident commander 看到 error rate 下降時，需要知道是真的恢復還是 buffer 尚未排空。把 buffer depth 跟 ingest delay 暴露成 dashboard 指標，能避免事中決策建立在過期資料上。</p>
<p>Buffer 跟 backpressure 怎麼選：低延遲容忍 + 容量充足的場景用 in-memory queue + <code>drop oldest</code>（保留最新狀態）；高訊號完整性需求（例：audit log、事故證據）用 persistent queue + <code>block</code> 或 <code>sample-by-priority</code>；高流量爆量但允許部分遺失（例：debug log）用 spillover storage + <code>drop newest</code>。事故時的回退路徑是「在 backpressure 政策中先標明哪類訊號絕對保留、哪類訊號可丟」、避免事故當下臨時決定。</p>
<h2 id="opentelemetry-collector-的中介定位">OpenTelemetry Collector 的中介定位</h2>
<p>OpenTelemetry Collector 把採集、轉換與 routing 從應用程式抽離，責任是讓觀測 vendor 跟採集 SDK 各自演進。</p>
<p>Collector 在 pipeline 中扮演三個角色：</p>
<ol>
<li><strong>Vendor-neutral 中介</strong>：應用層只需 export OTLP，collector 端決定要不要把資料同時送到多個後端（Datadog、Honeycomb、self-hosted Prometheus）。切換 vendor 時不需要改應用層。</li>
<li><strong>Schema / sampling 集中治理</strong>：attribute 重命名、敏感欄位 redaction、tail sampling decision、cardinality 限制都集中在 collector，不分散在每個服務。</li>
<li><strong>Topology 適配層</strong>：collector 可以部署為 sidecar（與應用同 Pod）、DaemonSet（每個 node 一份）或 gateway（集中接收）。不同部署形態適合不同規模與隔離需求，並不互斥；大型部署常見「應用 → sidecar → cluster gateway → 後端」的多級拓樸。</li>
</ol>
<p>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP 導入</a>：標準化傳輸協定降低跨環境的 instrumentation 重複，揭露「資料通道標準化」是觀測平台轉換的常見起點。對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT on EKS 管線遷移</a>：多代理混用在規模化時放大配置漂移，揭露 collector 集中治理的營運價值。兩個案例的具體實作差異留給原案例，本章關注的是 collector 在 pipeline 中的責任邊界。</p>
<h2 id="觀測遷移的執行順序">觀測遷移的執行順序</h2>
<p>觀測遷移的執行順序決定短期雙軌成本能否轉化為長期語意一致性。把替換風險限制在採集中介層、是先換 collector / agent、再換應用層 instrumentation 的設計理由。</p>
<p>可重複套用的順序是先換採集中介、再換採集點：</p>
<ol>
<li><strong>先換 collector / agent</strong>：把 collector 從 vendor-specific 換成 vendor-neutral（如 OTel Collector），同時保留舊 vendor 的 exporter，讓資料同時送到新舊後端。這層替換對應用層無感，可以快速完成。</li>
<li><strong>建立雙軌對照</strong>：以新舊後端對照 SLI 是否一致（query 設計、偏差閾值、退出條件等對照細節由 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> 處理）、差異超過閾值時停止下一步。</li>
<li><strong>逐步改應用端 instrumentation</strong>：把應用層的 vendor-specific SDK 換成 OTel SDK，分服務分批進行。每批切換後重跑對照驗證。</li>
<li><strong>以對照驗證進入 release gate</strong>：在 release pipeline 加上「新舊管線 SLI 偏差」檢查，作為遷移階段的閘門。對照穩定後才能關閉舊管線。</li>
</ol>
<p>執行順序的設計理由：collector 是 vendor-neutral 抽象、可以雙軌並存承受對照成本；應用層 instrumentation 改動會跨眾多 service team、變更面廣、要在 collector 對照穩定後才大規模推進。把次序反過來容易在 instrumentation 全面改完才發現 collector 抽象有缺失、被迫重做。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray 到 OpenTelemetry 轉換</a>：揭露「先 collector 後 instrumentation」的階段切換方向。對應 <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 相容遷移實務</a>：揭露「雙軌期成本跟語意漂移是遷移期主要風險」（單一 agent 安裝是次要議題）。本章關注的是執行順序，schema drift 跟資料品質的對照驗證細節由 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> 處理。</p>
<h2 id="規模差異下的遷移節奏">規模差異下的遷移節奏</h2>
<p>遷移節奏由團隊規模、可承受雙軌成本、配置漂移風險與治理成熟度共同決定。本段聚焦遷移期的節奏取捨；常態 ownership 配置由 <a href="/blog/backend/04-observability/observability-operating-model/#%e8%a6%8f%e6%a8%a1%e5%b7%ae%e7%95%b0%e4%b8%8b%e7%9a%84%e8%a7%92%e8%89%b2%e9%85%8d%e7%bd%ae" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 規模差異下的角色配置</a> 處理，兩者 lens 不同。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模差異下觀測遷移</a>：揭露三種規模團隊的失敗模式骨架；以下三段的具體操作做法均屬通用工程知識展開、case 本身只列方向。</p>
<p>小團隊的核心風險是雙軌維護消耗人力。同時看兩套 dashboard、雙倍 alert noise、雙倍 on-call 負擔，很容易讓遷移本身拖累業務維運。小團隊適合用「短期對照、快速收斂」策略：把對照期壓到一個迭代週期內，固定一個服務作為先導，把問題在小範圍內收斂，再快速複製到其他服務。</p>
<p>中型團隊的失敗模式集中在 schema 漂移。服務數量增加後，attribute 命名一致性、service name 規約、label cardinality 邊界容易在雙軌期擴散。中型團隊要在遷移開始前先固化 semantic convention，並在 collector 層自動校驗；不固化會在遷移後拼湊出多套互相矛盾的 dashboard。</p>
<p>大型團隊的主要失敗集中在治理面：collector 拓樸（sidecar / DaemonSet / gateway 的選擇）、sampling 政策、成本分攤、tenant 隔離都會在遷移後顯著影響成本與告警品質。大型團隊用「pilot region 先行、其他 region 批次跟進」策略、把 collector 配置版本化、變更接到 release gate。大型團隊的回退單位通常是 region 或 tenant 群、不是整體切回。</p>
<p>三類團隊的共同教訓是：先決定「何時可以關閉舊管線」的退出條件，再開始遷移。沒有退出條件的雙軌會無限期延長，最後在成本壓力下被動關閉，反而失去對照驗證的能力。</p>
<h2 id="遷移漂移的回退判讀">遷移漂移的回退判讀</h2>
<p>漂移回退的責任是把降級決策權跟資料採集分離、讓回退保留可分析的對照證據。直接關閉新管線會失去漂移原因的線索、後續再遷移容易出同樣的事故。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel 遷移訊號漂移反例</a>：揭露遷移失敗的主要型態是語意漂移、回退要保留對照證據。</p>
<p>漂移發生時，主要訊號是「兩套儀表板看似都有資料、但對同一事故的判讀不同」。新舊管線對同一服務的 error rate 長期偏離、missing span 或 missing metric 比例上升、alert 噪音增加但事故量沒對應增加，都是漂移在 pipeline 層的表現。</p>
<p>回退判讀的核心是分辨「遷移問題」跟「服務問題」。比較穩定的回退節奏：</p>
<ol>
<li>先停止讓新管線主導告警跟 SLO 判定，把告警入口切回舊管線。</li>
<li>保留新管線採集、但只作為對照證據，不參與決策。</li>
<li>用對照資料找出語意漂移點（attribute 名稱、sampling 規則、aggregation 視窗），分項修正。</li>
<li>修正後重新進入雙軌對照、確認偏差收斂、再讓新管線恢復主導。</li>
</ol>
<p>這個流程把回退視為降級決策權的釋放、而非整體關閉訊號採集。把回退做成可重播流程，下次遷移才能避免在錯誤訊號上做服務回退。</p>
<h2 id="multi-tenant-與-quota">Multi-tenant 與 Quota</h2>
<p>Pipeline 的多租戶治理責任是讓單一服務或團隊的爆量不會拖累其他租戶。沒有租戶隔離時，單一服務的 cardinality 爆炸或 sampling 失控會直接耗盡 pipeline 容量。</p>
<p>可操作的隔離手段：</p>
<ul>
<li><strong>Ingestion quota per tenant</strong>：限制單一服務的 ingest rate，超過時觸發降級或退單。</li>
<li><strong>Buffer 與 storage 分區</strong>：高優先 tenant 使用獨立 buffer 或 storage shard，避免 noisy neighbor。</li>
<li><strong>Sampling 政策 per tenant</strong>：成本敏感 tenant 走較高採樣比例，關鍵 tenant 走 minimum sample floor。</li>
<li><strong>Cost attribution</strong>：把 ingestion、storage、query 成本拆到 tenant，回到 <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>
</ul>
<p>Quota 觸發時的告警設計比 quota 本身更重要。沒有告警的 quota 等於沒有 quota，因為觸發後訊號靜默，事故定位會把靜默誤讀為系統穩定。</p>
<h2 id="讀取路徑作為-pipeline-的延伸">讀取路徑作為 pipeline 的延伸</h2>
<p>Pipeline 的分層敘事（agent → collector → ingest → storage → query）在 query 這層停得太早。寫入路徑的資料從 agent 流到 storage 是單向的；讀取路徑從 query engine 向 storage 發起請求，方向相反、效能瓶頸不同、治理責任也不同。把 query 視為 pipeline 的終端消費者而非獨立系統，才能完整理解觀測資料的生命週期。</p>
<h3 id="query-engine-的責任邊界">Query engine 的責任邊界</h3>
<p>Query engine 在 pipeline 中的責任是把儲存層的資料轉換成使用者可操作的回應。這包括 query planning（決定掃描哪些 shard、哪些 tier）、聚合計算（rate / sum / quantile）、結果快取與 query 排程。</p>
<p>Query engine 的設計取捨跟儲存層不同。儲存層追求寫入吞吐與持久性；query engine 追求查詢延遲與併發能力。兩者獨立擴展 — 寫入量大但查詢量小的場景，storage 需要更多容量但 query engine 不需要；反過來，dashboard 多但寫入量穩定的場景，query engine 需要更多 CPU 但 storage 不需要。</p>
<h3 id="query-time-的資源隔離">Query-time 的資源隔離</h3>
<p>Query engine 服務三種查詢模式：alert rule evaluation（系統關鍵、定期、不可延遲）、dashboard 刷新（高頻、穩定、可容忍短暫延遲）、即席診斷（偶發、突增、事故中最需要低延遲）。三者搶同一個 query engine 時，穩定的背景負載會擠壓突發的即席查詢。</p>
<p>資源隔離的可操作方式：</p>
<ul>
<li><strong>Query priority</strong>：alert evaluation 最高、即席查詢次之、dashboard 最低。Alert 不能因為 dashboard 重查詢排隊而漏發。</li>
<li><strong>Query queue 分離</strong>：不同類型的查詢進不同的 queue，各自有併發上限。Thanos / Mimir 的 query-frontend 支援 query 分類與排程。</li>
<li><strong>Query timeout 差異化</strong>：alert evaluation 設短 timeout（跑不完就是問題）、即席查詢設中等 timeout、dashboard 的大範圍查詢允許較長 timeout。</li>
<li><strong>Query cost estimation</strong>：在查詢執行前估算掃描量，超過閾值的查詢降級或拒絕，避免單一 heavy query 拖垮整個 query engine。</li>
</ul>
<h3 id="buffer-lag-對查詢-freshness-的影響">Buffer lag 對查詢 freshness 的影響</h3>
<p>寫入面的 buffer lag 會直接影響讀取面的 freshness。當 collector 或 ingest 端有分鐘級的 buffer 累積，query engine 讀到的是延遲過的資料。Dashboard 顯示的 error rate 可能反映的是兩分鐘前的狀態；incident commander 看到 error rate 下降，可能是 buffer 開始排空而非服務真的恢復。</p>
<p>把 buffer lag 轉成查詢面的可見指標是基本的設計要求。在 dashboard 上顯示「資料延遲：目前最新資料點是 N 秒前」，讓讀取者知道自己看到的資料有多新。當 lag 超過告警閾值，除了觸發 pipeline 健康告警外，dashboard 本身也應該標示警告狀態。</p>
<p>跨訊號類型的查詢設計見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 telemetry pipeline 時，先看每一層是否有健康訊號，再看滿載時是否能降級。</p>
<p>重點訊號包括：</p>
<ul>
<li>agent、collector、ingest、storage、query 是否各自有 SLI</li>
<li>buffer 與 backpressure 是否能保住高價值訊號</li>
<li>multi-tenant quota 是否能隔離單一服務爆量</li>
<li>collector 是否保留 vendor-neutral 的轉換空間</li>
<li>遷移期是否有雙軌對照、是否有退出條件</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>訊號間歇性消失、需要人工判斷是 pipeline 還是 service 問題</li>
<li>agent 升版需要 service 重啟、運維成本高</li>
<li>ingest 拒收（429）發生時、應用層無感</li>
<li>切換 vendor 需要改所有 service 的 instrumentation</li>
<li>pipeline 自身無 SLI、健康度靠經驗判斷</li>
<li>遷移期雙軌維護過久、退出條件不明</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pipeline 是黑盒</td>
          <td>訊號消失時靠經驗判斷層級</td>
          <td>每層暴露 SLI、量化 self-monitoring</td>
      </tr>
      <tr>
          <td>Agent 直連 vendor 無中介層</td>
          <td>切換 vendor 要改所有應用層</td>
          <td>加 collector 作為 vendor-neutral 中介</td>
      </tr>
      <tr>
          <td>Ingest 拒收靜默</td>
          <td>429 觸發但應用層 / 告警都無感</td>
          <td>把拒收事件變成告警與 dashboard 指標</td>
      </tr>
      <tr>
          <td>雙軌無退出條件</td>
          <td>遷移期無限延長、成本不斷雙倍</td>
          <td>預設退出 SLI 偏差閾值、加入 release gate</td>
      </tr>
      <tr>
          <td>配置漂移無版本控制</td>
          <td>collector 規則改了沒紀錄</td>
          <td>collector 配置進 git、變更走 release flow</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<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 / cost</a>：pipeline 各層的 quota</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：雙軌對照的資料品質判讀</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：collector / pipeline 的 ownership 邊界</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：讀取路徑的系統設計與資源治理</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：collector 部署形態（DaemonSet / sidecar / gateway）</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.4 chaos</a>：pipeline 故障模擬作為 chaos 場景</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>：pipeline 各層的成本歸屬</li>
<li><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12 Cloudflare 內部觀測</a>：大規模自建 pipeline 的三層能力設計</li>
</ul>
]]></content:encoded></item><item><title>6.11 Migration Safety 與 DB Rollout</title><link>https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>migration 的核心約束：schema 變更必須跟程式碼版本相容&lt;/li>
&lt;li>expand / contract 模式：先擴展（雙寫 / 雙讀）、再收斂（移除舊欄位）&lt;/li>
&lt;li>雙寫驗證：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read&lt;/a>、checksum 比對、流量採樣&lt;/li>
&lt;li>線上 DDL 工具：pt-online-schema-change / gh-ost / Vitess online schema change&lt;/li>
&lt;li>大表 migration 策略：批次、節流、避開 peak&lt;/li>
&lt;li>rollback 路徑設計：每階段必須可逆&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing&lt;/a> 的整合：schema 契約驗證&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 的整合：migration 可逆性作為 gate 條件&lt;/li>
&lt;li>反模式：schema change 跟 code deploy 同 PR、rollback 變不可能；大表 ALTER 直接打、production 鎖表；新欄位 NOT NULL 無 default&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">Schema migration&lt;/a> 是把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程，責任是避免資料結構變更直接把 production 推向不可回復狀態。&lt;/p>
&lt;p>這一頁關心的是結構變更的節奏。當 code 與 schema 必須一起演進，安全做法是保留回退與相容窗口，一次到位的思路會壓縮容錯空間。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 migration 時，先看每一步是否可逆，再看它是否能在 peak 外執行。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>expand / contract 是否真的分開&lt;/li>
&lt;li>rollback 路徑是否先於 production 變更設計&lt;/li>
&lt;li>大表操作是否有節流與 dry-run&lt;/li>
&lt;li>雙寫 / shadow read 是否有一致性驗證&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/" data-link-title="Pinterest" data-link-desc="Pinterest Capacity Planning 與儲存架構可靠性">Pinterest&lt;/a>：資料結構與產品演進常同步變化。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：大規模平台 migration 容易把結構風險放大。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">Stripe&lt;/a>：金流系統對 migration rollback 與一致性要求特別高。&lt;/li>
&lt;/ul>
&lt;h2 id="交易類-migration-的特殊性">交易類 migration 的特殊性&lt;/h2>
&lt;p>交易類 migration 同時承擔可用性跟正確性兩條軸。一般 schema migration 失敗的代價是停機、交易類失敗的代價額外包含結果不一致（重複扣款、訂單漏建、reconciliation 缺口）。守住兩條軸需要 idempotency + 漸進遷移 + 可回退發布 + 交易路徑可追溯四件事配合。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Stripe Idempotency 與零停機遷移&lt;/a>：揭露四個機制對應上述四件事 — idempotency key（同一交易重送如何得到同一結果）、expand/contract migration（資料變更如何與新舊版本共存）、canary + rollback gate（發版異常如何快速收斂）、transaction-path observability（交易路徑是否可追溯）。&lt;/p>
&lt;p>交易類 migration 的關鍵 observables：&lt;/p>
&lt;ul>
&lt;li>duplicate request collapse ratio：重試是否被正確合併&lt;/li>
&lt;li>migration phase error drift：遷移各階段錯誤是否收斂&lt;/li>
&lt;li>canary transaction anomaly：小流量交易是否出現偏差&lt;/li>
&lt;li>payment trace consistency：trace 是否完整覆蓋交易關鍵欄位&lt;/li>
&lt;/ul>
&lt;p>把這四個機制視為「交易類 migration 的安全 baseline」、跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency-replay&lt;/a> 共用 idempotency key 設計、跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/#%e4%ba%a4%e6%98%93%e9%a1%9e%e8%ae%8a%e6%9b%b4%e7%9a%84-gate-%e8%a8%ad%e8%a8%88" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate 交易類變更段&lt;/a> 共用 canary 條件。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>migration 的核心約束：schema 變更必須跟程式碼版本相容</li>
<li>expand / contract 模式：先擴展（雙寫 / 雙讀）、再收斂（移除舊欄位）</li>
<li>雙寫驗證：<a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a>、checksum 比對、流量採樣</li>
<li>線上 DDL 工具：pt-online-schema-change / gh-ost / Vitess online schema change</li>
<li>大表 migration 策略：批次、節流、避開 peak</li>
<li>rollback 路徑設計：每階段必須可逆</li>
<li>跟 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a> 的整合：schema 契約驗證</li>
<li>跟 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的整合：migration 可逆性作為 gate 條件</li>
<li>反模式：schema change 跟 code deploy 同 PR、rollback 變不可能；大表 ALTER 直接打、production 鎖表；新欄位 NOT NULL 無 default</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">Schema migration</a> 是把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程，責任是避免資料結構變更直接把 production 推向不可回復狀態。</p>
<p>這一頁關心的是結構變更的節奏。當 code 與 schema 必須一起演進，安全做法是保留回退與相容窗口，一次到位的思路會壓縮容錯空間。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 migration 時，先看每一步是否可逆，再看它是否能在 peak 外執行。</p>
<p>重點訊號包括：</p>
<ul>
<li>expand / contract 是否真的分開</li>
<li>rollback 路徑是否先於 production 變更設計</li>
<li>大表操作是否有節流與 dry-run</li>
<li>雙寫 / shadow read 是否有一致性驗證</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/pinterest/" data-link-title="Pinterest" data-link-desc="Pinterest Capacity Planning 與儲存架構可靠性">Pinterest</a>：資料結構與產品演進常同步變化。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：大規模平台 migration 容易把結構風險放大。</li>
<li><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">Stripe</a>：金流系統對 migration rollback 與一致性要求特別高。</li>
</ul>
<h2 id="交易類-migration-的特殊性">交易類 migration 的特殊性</h2>
<p>交易類 migration 同時承擔可用性跟正確性兩條軸。一般 schema migration 失敗的代價是停機、交易類失敗的代價額外包含結果不一致（重複扣款、訂單漏建、reconciliation 缺口）。守住兩條軸需要 idempotency + 漸進遷移 + 可回退發布 + 交易路徑可追溯四件事配合。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Stripe Idempotency 與零停機遷移</a>：揭露四個機制對應上述四件事 — idempotency key（同一交易重送如何得到同一結果）、expand/contract migration（資料變更如何與新舊版本共存）、canary + rollback gate（發版異常如何快速收斂）、transaction-path observability（交易路徑是否可追溯）。</p>
<p>交易類 migration 的關鍵 observables：</p>
<ul>
<li>duplicate request collapse ratio：重試是否被正確合併</li>
<li>migration phase error drift：遷移各階段錯誤是否收斂</li>
<li>canary transaction anomaly：小流量交易是否出現偏差</li>
<li>payment trace consistency：trace 是否完整覆蓋交易關鍵欄位</li>
</ul>
<p>把這四個機制視為「交易類 migration 的安全 baseline」、跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency-replay</a> 共用 idempotency key 設計、跟 <a href="/blog/backend/06-reliability/release-gate/#%e4%ba%a4%e6%98%93%e9%a1%9e%e8%ae%8a%e6%9b%b4%e7%9a%84-gate-%e8%a8%ad%e8%a8%88" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate 交易類變更段</a> 共用 canary 條件。</p>
<p>交易類 migration 的反模式是把 migration 當「資料庫任務」獨立執行、跟 release gate 分離。正確做法是把 migration 跟 release 綁定治理、用同一套 evidence 跟 rollback 條件判讀。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>01.6 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>：雙寫、回填、切流與回滾</li>
<li>01.7 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據</a>：把 migration plan 落成 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、evidence package、release gate 與 decision log</li>
<li>06.8 release gate：把可逆性放進放行條件</li>
<li>06.10 contract testing：先驗 schema 相容性</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：migration 類事故通常需要結構化復盤</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>migration 失敗只能 forward-fix、無 rollback 路徑</li>
<li>大表 ALTER 在 peak 時段執行造成鎖表</li>
<li>程式碼跟 schema 必須同步部署、deploy 失敗風險高</li>
<li>雙寫期間無一致性驗證、cutover 後才發現資料漂移</li>
<li>migration 工具無 dry-run、production 才知道執行時間</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>01.6 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>：執行層流程</li>
<li>01.7 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據</a>：production rollout evidence 與 gate 欄位</li>
<li>0.C4 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">營運後技術轉換</a>：決策層判讀</li>
<li>06.7 DR / rollback：migration rollback 演練</li>
<li>06.8 release gate：可逆性檢查</li>
<li>06.10 contract testing：schema 契約驗證</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：migration 引發的事故型態</li>
</ul>
]]></content:encoded></item><item><title>8.11 Observability / Reliability / Incident Response 閉環</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/observability-reliability-incident-loop/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/observability-reliability-incident-loop/</guid><description>&lt;p>服務的可靠性工程不是單向 pipeline、是循環反饋系統。觀測（04）偵測訊號驅動事故響應（08）、事故學習回寫到驗證設計（06）、驗證實踐又反過來定義觀測訊號（04）。任一段缺失閉環就斷裂、組織會以可預測的方式陷入特定失能模式。&lt;/p>
&lt;p>本章把三個模組當一個閉環看、定義各方向交接、每個方向的健康度判讀訊號、與斷裂後的失能模式。本章不重複 04 / 06 / 08 各自的概念內容、只承擔「把三者串成閉環」的責任。&lt;/p>
&lt;h2 id="為何要把三者當閉環看">為何要把三者當閉環看&lt;/h2>
&lt;p>單獨看任一模組會錯估它的責任邊界：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>04 單獨看&lt;/strong>：把訊號當成「服務狀態的視覺化」、忽略訊號是 6.6 SLO 政策的依據、是 8.1 事故啟動條件的觸發器。&lt;/li>
&lt;li>&lt;strong>06 單獨看&lt;/strong>：把驗證當成「測試完整度的驗證」、忽略驗證 hypothesis 來自事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>、SLO 來自觀測訊號。&lt;/li>
&lt;li>&lt;strong>08 單獨看&lt;/strong>：把事故當成「響應流程演練」、忽略事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 的價值在回寫 04 訊號與 06 驗證、不在響應本身。&lt;/li>
&lt;/ul>
&lt;p>閉環視角讓三個模組各自的設計受其他兩者約束、避免局部最佳化。&lt;/p>
&lt;h2 id="閉環四個方向">閉環四個方向&lt;/h2>
&lt;h3 id="04--08訊號驅動事故響應">04 → 08：訊號驅動事故響應&lt;/h3>
&lt;p>最直觀的方向、訊號（SLO &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> / error rate spike / latency p99 / queue lag）達標後觸發告警、進入事故響應流程。&lt;/p>
&lt;p>判讀邊界由 04 定義（什麼算異常）、響應節奏由 08 定義（誰響應、怎麼分級、怎麼通訊）。交接點是 alert routing：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert&lt;/a> 連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook&lt;/a>、再連到事故指揮流程。&lt;/p>
&lt;p>具體例子：&lt;/p>
&lt;ul>
&lt;li>Checkout API p99 latency 超過 SLO burn rate 2x → 觸發 PagerDuty alert → 進入 Sev2 事故流程&lt;/li>
&lt;li>Queue consumer lag 持續上升 → 訊號觸發 → 進入 capacity incident 流程&lt;/li>
&lt;li>Error rate spike 超過 baseline 5σ → alert → 進入 release rollback 流程&lt;/li>
&lt;/ul>
&lt;h3 id="08--06事故回寫驗證設計">08 → 06：事故回寫驗證設計&lt;/h3>
&lt;p>事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 的 action items 不應該只是「補 runbook」這類局部修正、而應該回寫到事前驗證設計、讓下一次同類事故在 production 前被攔截。&lt;/p>
&lt;p>交接點是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> action items 的分類：哪些回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos experiment&lt;/a>、哪些回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal&lt;/a>、哪些回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>、哪些回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策&lt;/a>。&lt;/p>
&lt;p>具體例子：&lt;/p>
&lt;ul>
&lt;li>事故揭露 cache 失效時 DB 雪崩 → 回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos experiment&lt;/a>（注入 cache failure）&lt;/li>
&lt;li>事故揭露 region failover 演練不足 → 回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal&lt;/a> 排程&lt;/li>
&lt;li>事故揭露 migration 沒測 rollback → 回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>（migration check）&lt;/li>
&lt;li>事故揭露 SLO 太鬆、導致客戶感知問題前沒人發現 → 回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策&lt;/a>收緊&lt;/li>
&lt;/ul>
&lt;h3 id="06--04驗證需求驅動訊號設計">06 → 04：驗證需求驅動訊號設計&lt;/h3>
&lt;p>事前驗證會暴露當前訊號的不足：chaos experiment 需要新 metric 確認 steady state、load test 需要新 dashboard 看 capacity headroom、SLO 政策需要新 alert rule 偵測 burn rate。&lt;/p></description><content:encoded><![CDATA[<p>服務的可靠性工程不是單向 pipeline、是循環反饋系統。觀測（04）偵測訊號驅動事故響應（08）、事故學習回寫到驗證設計（06）、驗證實踐又反過來定義觀測訊號（04）。任一段缺失閉環就斷裂、組織會以可預測的方式陷入特定失能模式。</p>
<p>本章把三個模組當一個閉環看、定義各方向交接、每個方向的健康度判讀訊號、與斷裂後的失能模式。本章不重複 04 / 06 / 08 各自的概念內容、只承擔「把三者串成閉環」的責任。</p>
<h2 id="為何要把三者當閉環看">為何要把三者當閉環看</h2>
<p>單獨看任一模組會錯估它的責任邊界：</p>
<ul>
<li><strong>04 單獨看</strong>：把訊號當成「服務狀態的視覺化」、忽略訊號是 6.6 SLO 政策的依據、是 8.1 事故啟動條件的觸發器。</li>
<li><strong>06 單獨看</strong>：把驗證當成「測試完整度的驗證」、忽略驗證 hypothesis 來自事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>、SLO 來自觀測訊號。</li>
<li><strong>08 單獨看</strong>：把事故當成「響應流程演練」、忽略事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 的價值在回寫 04 訊號與 06 驗證、不在響應本身。</li>
</ul>
<p>閉環視角讓三個模組各自的設計受其他兩者約束、避免局部最佳化。</p>
<h2 id="閉環四個方向">閉環四個方向</h2>
<h3 id="04--08訊號驅動事故響應">04 → 08：訊號驅動事故響應</h3>
<p>最直觀的方向、訊號（SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> / error rate spike / latency p99 / queue lag）達標後觸發告警、進入事故響應流程。</p>
<p>判讀邊界由 04 定義（什麼算異常）、響應節奏由 08 定義（誰響應、怎麼分級、怎麼通訊）。交接點是 alert routing：<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a> 連到 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>、再連到事故指揮流程。</p>
<p>具體例子：</p>
<ul>
<li>Checkout API p99 latency 超過 SLO burn rate 2x → 觸發 PagerDuty alert → 進入 Sev2 事故流程</li>
<li>Queue consumer lag 持續上升 → 訊號觸發 → 進入 capacity incident 流程</li>
<li>Error rate spike 超過 baseline 5σ → alert → 進入 release rollback 流程</li>
</ul>
<h3 id="08--06事故回寫驗證設計">08 → 06：事故回寫驗證設計</h3>
<p>事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 的 action items 不應該只是「補 runbook」這類局部修正、而應該回寫到事前驗證設計、讓下一次同類事故在 production 前被攔截。</p>
<p>交接點是 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> action items 的分類：哪些回到 <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos experiment</a>、哪些回到 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal</a>、哪些回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>、哪些回到 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策</a>。</p>
<p>具體例子：</p>
<ul>
<li>事故揭露 cache 失效時 DB 雪崩 → 回寫到 <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos experiment</a>（注入 cache failure）</li>
<li>事故揭露 region failover 演練不足 → 回寫到 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal</a> 排程</li>
<li>事故揭露 migration 沒測 rollback → 回寫到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（migration check）</li>
<li>事故揭露 SLO 太鬆、導致客戶感知問題前沒人發現 → 回寫到 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO 政策</a>收緊</li>
</ul>
<h3 id="06--04驗證需求驅動訊號設計">06 → 04：驗證需求驅動訊號設計</h3>
<p>事前驗證會暴露當前訊號的不足：chaos experiment 需要新 metric 確認 steady state、load test 需要新 dashboard 看 capacity headroom、SLO 政策需要新 alert rule 偵測 burn rate。</p>
<p>交接點是 4.1（log schema）/ 4.2（metrics）/ 4.4（dashboard / alert）的擴充來源：哪些訊號是驗證 hypothesis 必要的、就應該在 04 提供。</p>
<p>具體例子：</p>
<ul>
<li><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 Chaos experiment</a> 注入 broker partition、需要新 metric 看 consumer rebalance 時間 → 4.2 補</li>
<li><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO</a> 定義要求 burn rate alert → 4.4 補對應 alert rule</li>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rehearsal</a> 需要看 cross-region replication lag → 4.4 補 dashboard</li>
</ul>
<h3 id="08--04事故揭露偵測缺口">08 → 04：事故揭露偵測缺口</h3>
<p>事故發生後、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 通常會發現「訊號其實有、但太晚 / 太雜 / 看不出 user impact」、這些是 04 的偵測缺口。</p>
<p>交接點跟 06 → 04 不同：06 → 04 是預期性新增訊號、08 → 04 是修正既有訊號治理問題。回寫到 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a> 與 04 的訊號設計。</p>
<p>具體例子：</p>
<ul>
<li>事故揭露 alert 太晚（用 cause-based 而不是 symptom-based）→ 回寫 alert design</li>
<li>事故揭露 dashboard cardinality 不足、看不到單一 user 影響 → 回寫 metric design</li>
<li>事故揭露 alert 太雜、值班疲乏錯過真實訊號 → 回寫 alert noise reduction（4.4 / <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a>）</li>
</ul>
<h2 id="閉環健康度判讀訊號">閉環健康度判讀訊號</h2>
<p>閉環是否運作的判讀訊號 — 三個方向都應該定期觀察是否在動：</p>
<table>
  <thead>
      <tr>
          <th>方向</th>
          <th>健康訊號</th>
          <th>失能訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>04 → 08</td>
          <td>多數 Sev2+ 事故由 alert 觸發、不是客戶通報</td>
          <td>客戶通報先於 alert 的比例上升、值班發現 alert 沒人接</td>
      </tr>
      <tr>
          <td>08 → 06</td>
          <td>每次 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 至少產出一個事前驗證 action</td>
          <td><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> action items 都是 runbook 補丁、無事前驗證</td>
      </tr>
      <tr>
          <td>06 → 04</td>
          <td>Chaos / SLO 工作會驅動新訊號出現</td>
          <td>驗證活動孤立、不會反向擴充 04 訊號集</td>
      </tr>
      <tr>
          <td>08 → 04</td>
          <td><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 會具名指出哪個訊號不足、有 follow-up</td>
          <td><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 提到「訊號不夠」但沒落實到具體 metric / alert</td>
      </tr>
  </tbody>
</table>
<h2 id="閉環斷裂的失能模式">閉環斷裂的失能模式</h2>
<p>每個方向斷裂會導致可預測的問題：</p>
<ul>
<li><strong>04 → 08 斷</strong>：alert 沒接 IR 流程、訊號變成「儀表板好看」但不驅動行動。常見於把 04 當成 BI 工具的團隊。</li>
<li><strong>08 → 06 斷</strong>：每次事故重複同類根因、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 變成 ritual、對下一次事故沒影響。常見於沒有 6.7 DR rehearsal 文化的團隊。</li>
<li><strong>06 → 04 斷</strong>：驗證活動成為孤立工程實踐、chaos 結果不影響 dashboard / alert 設計。常見於 SRE 跟 platform 團隊割裂時。</li>
<li><strong>08 → 04 斷</strong>：訊號治理停滯、alert noise 累積、值班疲乏。常見於沒有 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 主題的成熟度檢視。</li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<p>判讀完閉環現況後沿兩條 chain 進入 implementation：</p>
<ol>
<li><strong>方向強化 chain</strong>：找出最弱的方向、補對應模組的章節 — 04 → 08 弱補 4.4 alert design + 8.2 command；08 → 06 弱補 8.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 模板 + <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a> / <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a>；06 → 04 弱補 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO</a> + 4.2 metrics；08 → 04 弱補 8.5 + 4.4。</li>
<li><strong>跨模組演練 chain</strong>：用 6.6 <a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">game day</a> 同時驗證三個方向是否串通 — 注入故障、看 04 是否觸發、08 是否響應、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 是否回寫 06 / 04。</li>
</ol>
]]></content:encoded></item><item><title>LinkedIn</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/</guid><description>&lt;p>LinkedIn 是大規模社交平台、capacity planning 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 結構的工程文章公開度高、是「中型公司如何規模化 SRE」的教學標竿。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Capacity Planning：跨 region / 跨服務的容量預測方法&lt;/li>
&lt;li>On-call 結構：primary / secondary / SME escalation&lt;/li>
&lt;li>Operability culture：把可運維性納入服務設計門檻&lt;/li>
&lt;li>Internal tooling：LinkedIn engineering blog 公開的內部工具設計&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Capacity Planning&lt;/td>
 &lt;td>預測模型、headroom、growth rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>On-call Tiers&lt;/td>
 &lt;td>多層 escalation 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Site Reliability Eng&lt;/td>
 &lt;td>LinkedIn SRE 組織演化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Internal Chaos / Drills&lt;/td>
 &lt;td>Project Waterbear 等內部演練&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>LinkedIn 這個案例在講的是中大型平台如何把容量規劃、自動化壓測與 metrics 收集做成可運營的系統。讀者先抓 capacity planning、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> tiers 與 self-service metrics 的關係，再看它們怎麼把 operability 變成團隊責任。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 replication latency 上升時，先看 headroom 是否足夠，再看壓測與自動化是否真的覆蓋了常見瓶頸。當 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 需要多層升級時，重點是每一層是否知道何時接手、何時回退，階層形式本身是次要的。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否把容量預測連到實際 growth rate&lt;/li>
&lt;li>能否讓 load testing 自動化到可重用&lt;/li>
&lt;li>能否把 metrics collection 做成 self-service&lt;/li>
&lt;li>能否清楚劃分 primary、secondary 與 SME escalation&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>LinkedIn 的焦點是把 operability 變成日常流程，這和 Shopify 的峰值準備、Microsoft 的治理模式、Spotify 的平台化做法都很接近。差別在於 LinkedIn 更強調內部工具與 metrics pipeline，適合拿來當「中型平台如何長大」的範本。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>automated load testing 把壓測變成日常流程，而不是臨時活動。&lt;/li>
&lt;li>self-service metrics 讓團隊不用等平台工程師才能看見關鍵訊號。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> tiers 讓升級與接手邏輯有固定路徑。&lt;/li>
&lt;li>capacity planning 讓 replication latency 與 headroom 直接相連。&lt;/li>
&lt;li>site reliability engineering 讓中型平台開始形成自己的可靠性職能。&lt;/li>
&lt;li>internal tooling 讓 operability 變成平台化能力而不是個人技巧。&lt;/li>
&lt;li>project waterbear 類演練讓內部故障情境能被規律化測試。&lt;/li>
&lt;li>primary / secondary / SME escalation 讓責任與知識分工更清楚。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1&lt;/a>&lt;/td>
 &lt;td>Capacity 與 On-call 分層&lt;/td>
 &lt;td>把容量邊界與值班交接綁成同一套治理節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">L2&lt;/a>&lt;/td>
 &lt;td>Automated Load Testing 與 Forecasting&lt;/td>
 &lt;td>用持續壓測驅動容量預測，取代一次性壓測的容量規劃&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.linkedin.com/20/welcome-linkedin-engineering-blog">Welcome to the LinkedIn Engineering Blog&lt;/a>：LinkedIn Engineering Blog 的入口。&lt;/li>
&lt;li>&lt;a href="https://engineering.linkedin.com/performance/taming-database-replication-latency-capacity-planning">Taming Database Replication Latency by Capacity Planning&lt;/a>：容量規劃與 replication latency 的經典案例。&lt;/li>
&lt;li>&lt;a href="https://engineering.linkedin.com/content/engineering/en-us/blog/2019/eliminating-toil-with-fully-automated-load-testing">Eliminating toil with fully automated load testing&lt;/a>：自動化壓測與 operability 的實踐。&lt;/li>
&lt;li>&lt;a href="https://engineering.linkedin.com/metrics/scaling-collection-self-service-metrics">Scaling the collection of self-service metrics&lt;/a>：metrics pipeline 與可運維性基礎。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>LinkedIn 是大規模社交平台、capacity planning 與 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 結構的工程文章公開度高、是「中型公司如何規模化 SRE」的教學標竿。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Capacity Planning：跨 region / 跨服務的容量預測方法</li>
<li>On-call 結構：primary / secondary / SME escalation</li>
<li>Operability culture：把可運維性納入服務設計門檻</li>
<li>Internal tooling：LinkedIn engineering blog 公開的內部工具設計</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Capacity Planning</td>
          <td>預測模型、headroom、growth rate</td>
      </tr>
      <tr>
          <td>On-call Tiers</td>
          <td>多層 escalation 設計</td>
      </tr>
      <tr>
          <td>Site Reliability Eng</td>
          <td>LinkedIn SRE 組織演化</td>
      </tr>
      <tr>
          <td>Internal Chaos / Drills</td>
          <td>Project Waterbear 等內部演練</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>LinkedIn 這個案例在講的是中大型平台如何把容量規劃、自動化壓測與 metrics 收集做成可運營的系統。讀者先抓 capacity planning、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> tiers 與 self-service metrics 的關係，再看它們怎麼把 operability 變成團隊責任。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 replication latency 上升時，先看 headroom 是否足夠，再看壓測與自動化是否真的覆蓋了常見瓶頸。當 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 需要多層升級時，重點是每一層是否知道何時接手、何時回退，階層形式本身是次要的。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否把容量預測連到實際 growth rate</li>
<li>能否讓 load testing 自動化到可重用</li>
<li>能否把 metrics collection 做成 self-service</li>
<li>能否清楚劃分 primary、secondary 與 SME escalation</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>LinkedIn 的焦點是把 operability 變成日常流程，這和 Shopify 的峰值準備、Microsoft 的治理模式、Spotify 的平台化做法都很接近。差別在於 LinkedIn 更強調內部工具與 metrics pipeline，適合拿來當「中型平台如何長大」的範本。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>automated load testing 把壓測變成日常流程，而不是臨時活動。</li>
<li>self-service metrics 讓團隊不用等平台工程師才能看見關鍵訊號。</li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> tiers 讓升級與接手邏輯有固定路徑。</li>
<li>capacity planning 讓 replication latency 與 headroom 直接相連。</li>
<li>site reliability engineering 讓中型平台開始形成自己的可靠性職能。</li>
<li>internal tooling 讓 operability 變成平台化能力而不是個人技巧。</li>
<li>project waterbear 類演練讓內部故障情境能被規律化測試。</li>
<li>primary / secondary / SME escalation 讓責任與知識分工更清楚。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1</a></td>
          <td>Capacity 與 On-call 分層</td>
          <td>把容量邊界與值班交接綁成同一套治理節奏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">L2</a></td>
          <td>Automated Load Testing 與 Forecasting</td>
          <td>用持續壓測驅動容量預測，取代一次性壓測的容量規劃</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.linkedin.com/20/welcome-linkedin-engineering-blog">Welcome to the LinkedIn Engineering Blog</a>：LinkedIn Engineering Blog 的入口。</li>
<li><a href="https://engineering.linkedin.com/performance/taming-database-replication-latency-capacity-planning">Taming Database Replication Latency by Capacity Planning</a>：容量規劃與 replication latency 的經典案例。</li>
<li><a href="https://engineering.linkedin.com/content/engineering/en-us/blog/2019/eliminating-toil-with-fully-automated-load-testing">Eliminating toil with fully automated load testing</a>：自動化壓測與 operability 的實踐。</li>
<li><a href="https://engineering.linkedin.com/metrics/scaling-collection-self-service-metrics">Scaling the collection of self-service metrics</a>：metrics pipeline 與可運維性基礎。</li>
</ul>
]]></content:encoded></item><item><title>Nobl9</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/nobl9/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/nobl9/</guid><description>&lt;p>Nobl9 是商業 SLO 平台、承擔三個責任：跨 data source SLO 統一治理（Datadog / Prometheus / New Relic / CloudWatch / Splunk 等）、error budget + burn rate alerting、organizational SLO governance（service catalog / project / role）。設計取捨偏向「multi-source + governance + OpenSLO standard」、創辦人來自 Google SRE、推動 OpenSLO 標準。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>在 Nobl9 定義 SLO（SLI / target / time window）&lt;/li>
&lt;li>配置 error budget + burn rate alert（multi-window）&lt;/li>
&lt;li>設計 composite SLO（跨服務組合）&lt;/li>
&lt;li>用 OpenSLO YAML 管 SLO as code&lt;/li>
&lt;li>評估 Nobl9 vs Sloth / Pyrra / vendor 內建 SLO&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-nobl9-跑起來">最短路徑：5 分鐘把 Nobl9 跑起來&lt;/h2>





&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"># 1. 註冊 Nobl9 + connect data source&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: app.nobl9.com、connect Datadog / Prometheus&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="c1"># 2. 寫 SLO YAML（OpenSLO）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: SLO spec with service / indicator / objective&lt;/span>
&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="c1"># 3. sloctl apply&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: sloctl apply -f slo.yaml&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="slo-定義">SLO 定義&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>SLI（Service Level Indicator）：metric to measure&lt;/li>
&lt;li>Objective：target percentage&lt;/li>
&lt;li>Time window：rolling / calendar&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="error-budget">Error budget&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Budget = (1 - SLO target) × time window&lt;/li>
&lt;li>Consumed budget / remaining budget&lt;/li>
&lt;li>跟 release gate 對應（budget 用完 → freeze deploy）&lt;/li>
&lt;/ul>
&lt;h3 id="burn-rate-alert">Burn rate alert&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Multi-window multi-burn-rate alert&lt;/li>
&lt;li>Fast burn alert（短期 high rate）+ slow burn alert（長期 low rate）&lt;/li>
&lt;li>對應 Google SRE burn rate alerting&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="composite-slo">Composite SLO&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>跨多 service 組合成單一 SLO&lt;/li>
&lt;li>適合：user journey SLO（不只單一 service）&lt;/li>
&lt;/ul>
&lt;h3 id="openslo-標準">OpenSLO 標準&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Nobl9 是商業 SLO 平台、承擔三個責任：跨 data source SLO 統一治理（Datadog / Prometheus / New Relic / CloudWatch / Splunk 等）、error budget + burn rate alerting、organizational SLO governance（service catalog / project / role）。設計取捨偏向「multi-source + governance + OpenSLO standard」、創辦人來自 Google SRE、推動 OpenSLO 標準。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>在 Nobl9 定義 SLO（SLI / target / time window）</li>
<li>配置 error budget + burn rate alert（multi-window）</li>
<li>設計 composite SLO（跨服務組合）</li>
<li>用 OpenSLO YAML 管 SLO as code</li>
<li>評估 Nobl9 vs Sloth / Pyrra / vendor 內建 SLO</li>
</ol>
<h2 id="最短路徑5-分鐘把-nobl9-跑起來">最短路徑：5 分鐘把 Nobl9 跑起來</h2>





<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. 註冊 Nobl9 + connect data source</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: app.nobl9.com、connect Datadog / Prometheus</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="c1"># 2. 寫 SLO YAML（OpenSLO）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: SLO spec with service / indicator / objective</span>
</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="c1"># 3. sloctl apply</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: sloctl apply -f slo.yaml</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="slo-定義">SLO 定義</h3>
<p>子議題：</p>
<ul>
<li>SLI（Service Level Indicator）：metric to measure</li>
<li>Objective：target percentage</li>
<li>Time window：rolling / calendar</li>
<li>對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a></li>
</ul>
<h3 id="error-budget">Error budget</h3>
<p>子議題：</p>
<ul>
<li>Budget = (1 - SLO target) × time window</li>
<li>Consumed budget / remaining budget</li>
<li>跟 release gate 對應（budget 用完 → freeze deploy）</li>
</ul>
<h3 id="burn-rate-alert">Burn rate alert</h3>
<p>子議題：</p>
<ul>
<li>Multi-window multi-burn-rate alert</li>
<li>Fast burn alert（短期 high rate）+ slow burn alert（長期 low rate）</li>
<li>對應 Google SRE burn rate alerting</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="composite-slo">Composite SLO</h3>
<p>子議題：</p>
<ul>
<li>跨多 service 組合成單一 SLO</li>
<li>適合：user journey SLO（不只單一 service）</li>
</ul>
<h3 id="openslo-標準">OpenSLO 標準</h3>
<p>子議題：</p>
<ul>
<li>Vendor-neutral SLO spec</li>
<li>YAML 配置</li>
<li>跟 Nobl9 主導</li>
<li>對應 vendor lock-in 取捨</li>
</ul>
<h3 id="data-source-整合">Data source 整合</h3>
<p>子議題：</p>
<ul>
<li>Datadog / Prometheus / New Relic / CloudWatch / Splunk / AppDynamics / Honeycomb / Lightstep</li>
<li>多 source SLO 統一 view</li>
<li>對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 模組</li>
</ul>
<h3 id="alert-routing">Alert routing</h3>
<p>子議題：</p>
<ul>
<li>跟 PagerDuty / Opsgenie / Slack 整合</li>
<li>跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a> 對應</li>
</ul>
<h3 id="service-catalog--governance">Service catalog + governance</h3>
<p>子議題：</p>
<ul>
<li>Project / Service / SLO 階層</li>
<li>Role-based access</li>
<li>Audit log</li>
</ul>
<h3 id="slo-as-code">SLO as code</h3>
<p>子議題：</p>
<ul>
<li>sloctl CLI</li>
<li>YAML version control</li>
<li>CI integration</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="slo-calculation-不準">SLO calculation 不準</h3>
<p>操作原則：SLI query 不對 / data source 延遲。判讀：raw metric vs SLO calculation 比對。</p>
<h3 id="alert-noise">Alert noise</h3>
<p>操作原則：burn rate window 設過短 / threshold 過嚴。</p>
<h3 id="data-source-disconnect">Data source disconnect</h3>
<p>操作原則：API key / network / quota。</p>
<h3 id="composite-slo-行為不符預期">Composite SLO 行為不符預期</h3>
<p>操作原則：composite 算法（AND / OR / custom）不對。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSS / 預算敏感</td>
          <td><a href="/blog/backend/06-reliability/vendors/sloth/" data-link-title="Sloth" data-link-desc="OSS SLO generator for Prometheus">Sloth</a> / Pyrra</td>
      </tr>
      <tr>
          <td>單一 vendor 環境</td>
          <td>Datadog SLO / Honeycomb SLO / Grafana SLO</td>
      </tr>
      <tr>
          <td>K8s-native CRD</td>
          <td>Pyrra（K8s Operator）</td>
      </tr>
      <tr>
          <td>純 Prometheus</td>
          <td>Sloth（Prometheus generator）</td>
      </tr>
      <tr>
          <td>Enterprise + multi-cloud</td>
          <td>Nobl9（本頁）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>OpenSLO 完整 spec</li>
<li>Nobl9 pricing</li>
<li>sloctl 完整 CLI reference</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 與 Release Gating</a></td>
          <td>SLI / SLO / error budget 原典、多源聚合 SLO 平台的對齊對象</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動可靠性</a></td>
          <td>burn rate alert 對應 SLO 平台的 alert policy</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft：變更治理與可靠性門檻</a></td>
          <td>企業合規 + SLO 治理的對應路徑</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Nobl9 customer case</strong>：企業 SLO 治理採用案例、OpenSLO adopter。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/sloth/" data-link-title="Sloth" data-link-desc="OSS SLO generator for Prometheus">Sloth</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a>、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a></li>
</ul>
]]></content:encoded></item><item><title>Slack</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/</guid><description>&lt;p>Slack 是即時通訊服務、事故時通訊管道本身受影響、是「monitor your own monitor」議題的代表。Slack engineering blog 公開度高、status page 設計細緻。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>通訊管道自身故障：客戶用 Slack 通報 Slack 事故的 paradox&lt;/li>
&lt;li>外部狀態頁設計：細粒度 region / feature 揭露&lt;/li>
&lt;li>WebSocket 連線風暴：reconnection storm 在大規模長連線服務的特殊風險&lt;/li>
&lt;li>跨 workspace 隔離：multi-tenant 事故的部分擴散模式&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2022&lt;/td>
 &lt;td>Jan 全球登入失效&lt;/td>
 &lt;td>配置變更、跨服務依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2022&lt;/td>
 &lt;td>2-22 事故&lt;/td>
 &lt;td>reconnection storm、status 揭露&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Slack 這個案例在講的是通訊平台本身失效時，事故通訊也會一起受影響。讀者先抓 Slack status API、service delivery index 與 incident blog 的責任，再把這類事件看成「監控自己的監控」問題。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當登入或連線異常出現時，使用者需要的是清楚知道狀態頁、回復進度與替代通訊方式，術語在此幫助有限。當 reconnection storm 發生時，恢復節奏也要先保住連線，再回頭處理狀態同步。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否讓 status page 與實際事故節奏同步&lt;/li>
&lt;li>能否把通訊工具失效當成獨立風險&lt;/li>
&lt;li>能否清楚說出哪些 workspace 受影響&lt;/li>
&lt;li>能否在恢復時先控制 reconnection 壓力&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Slack 和 Discord、Microsoft 365 一起看，最能理解通訊工具本身失效時的 IR 難點。它也和 Datadog 有關，因為當你連通訊都不能穩定時，監控與狀態揭露就必須先變成對外的第一路由。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2-22 事故顯示通訊平台本身失效時，status 與 incident blog 也會成為核心資產。&lt;/li>
&lt;li>Slack Status API 則是讓客戶能獨立查詢事故與歷史狀態的樣本。&lt;/li>
&lt;li>reconnection storm 讓通訊平台的容量問題直接變成客戶體感。&lt;/li>
&lt;li>service delivery index 反映的是可靠性與對外揭露如何一起運作。&lt;/li>
&lt;li>workspace 層的部分失效讓多租戶通訊平台必須做細粒度揭露。&lt;/li>
&lt;li>monitor your own monitor 是 Slack 這類平台最直接的 IR 警示。&lt;/li>
&lt;li>incident blog 讓對外敘事與對內修復節奏保持一致。&lt;/li>
&lt;li>multi-workspace failure 會把對外通訊也一起拖進事故。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/" data-link-title="Slack：2022 連線恢復與狀態通訊節奏" data-link-desc="在通訊平台自身失效時，如何同步恢復節奏與對外狀態揭露。">SL1&lt;/a>&lt;/td>
 &lt;td>連線恢復與狀態通訊&lt;/td>
 &lt;td>將恢復節奏與外部更新維持同頻&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://api.slack.com/apis/slack-status">Checking up on Slack with the Slack Status API&lt;/a>：Slack 狀態與歷史 incident 的官方 API。&lt;/li>
&lt;li>&lt;a href="https://slack.engineering/slacks-incident-on-2-22-22/">Slack’s Incident on 2-22-22&lt;/a>：Slack 事故技術復盤。&lt;/li>
&lt;li>&lt;a href="https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/">A Terrible, Horrible, No-Good, Very Bad Day at Slack&lt;/a>：另一篇詳細事故回顧。&lt;/li>
&lt;li>&lt;a href="https://slack.engineering/service-delivery-index-a-driver-for-reliability/">Service Delivery Index: A Driver for Reliability&lt;/a>：Slack 的可靠性指標與 status 文化。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Slack 是即時通訊服務、事故時通訊管道本身受影響、是「monitor your own monitor」議題的代表。Slack engineering blog 公開度高、status page 設計細緻。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>通訊管道自身故障：客戶用 Slack 通報 Slack 事故的 paradox</li>
<li>外部狀態頁設計：細粒度 region / feature 揭露</li>
<li>WebSocket 連線風暴：reconnection storm 在大規模長連線服務的特殊風險</li>
<li>跨 workspace 隔離：multi-tenant 事故的部分擴散模式</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2022</td>
          <td>Jan 全球登入失效</td>
          <td>配置變更、跨服務依賴</td>
      </tr>
      <tr>
          <td>2022</td>
          <td>2-22 事故</td>
          <td>reconnection storm、status 揭露</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Slack 這個案例在講的是通訊平台本身失效時，事故通訊也會一起受影響。讀者先抓 Slack status API、service delivery index 與 incident blog 的責任，再把這類事件看成「監控自己的監控」問題。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當登入或連線異常出現時，使用者需要的是清楚知道狀態頁、回復進度與替代通訊方式，術語在此幫助有限。當 reconnection storm 發生時，恢復節奏也要先保住連線，再回頭處理狀態同步。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否讓 status page 與實際事故節奏同步</li>
<li>能否把通訊工具失效當成獨立風險</li>
<li>能否清楚說出哪些 workspace 受影響</li>
<li>能否在恢復時先控制 reconnection 壓力</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Slack 和 Discord、Microsoft 365 一起看，最能理解通訊工具本身失效時的 IR 難點。它也和 Datadog 有關，因為當你連通訊都不能穩定時，監控與狀態揭露就必須先變成對外的第一路由。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2-22 事故顯示通訊平台本身失效時，status 與 incident blog 也會成為核心資產。</li>
<li>Slack Status API 則是讓客戶能獨立查詢事故與歷史狀態的樣本。</li>
<li>reconnection storm 讓通訊平台的容量問題直接變成客戶體感。</li>
<li>service delivery index 反映的是可靠性與對外揭露如何一起運作。</li>
<li>workspace 層的部分失效讓多租戶通訊平台必須做細粒度揭露。</li>
<li>monitor your own monitor 是 Slack 這類平台最直接的 IR 警示。</li>
<li>incident blog 讓對外敘事與對內修復節奏保持一致。</li>
<li>multi-workspace failure 會把對外通訊也一起拖進事故。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/" data-link-title="Slack：2022 連線恢復與狀態通訊節奏" data-link-desc="在通訊平台自身失效時，如何同步恢復節奏與對外狀態揭露。">SL1</a></td>
          <td>連線恢復與狀態通訊</td>
          <td>將恢復節奏與外部更新維持同頻</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://api.slack.com/apis/slack-status">Checking up on Slack with the Slack Status API</a>：Slack 狀態與歷史 incident 的官方 API。</li>
<li><a href="https://slack.engineering/slacks-incident-on-2-22-22/">Slack’s Incident on 2-22-22</a>：Slack 事故技術復盤。</li>
<li><a href="https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/">A Terrible, Horrible, No-Good, Very Bad Day at Slack</a>：另一篇詳細事故回顧。</li>
<li><a href="https://slack.engineering/service-delivery-index-a-driver-for-reliability/">Service Delivery Index: A Driver for Reliability</a>：Slack 的可靠性指標與 status 文化。</li>
</ul>
]]></content:encoded></item><item><title>0.11 攻擊者視角（紅隊）：跨服務弱點判讀總表</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/red-team-cross-service-weaknesses/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/red-team-cross-service-weaknesses/</guid><description>&lt;p>跨服務紅隊判讀的核心目標是把「哪裡最容易被打穿」先標出來，再決定服務能力的補強順序。這裡的紅隊是「攻擊者視角的風險檢查方法」：用攻擊者可能採取的路徑反向驗證系統設計。這份總表維持純概念層，不進入實作細節，重點是先回答四件事：暴露面在哪裡、弱點訊號長什麼樣、失敗代價是什麼、最低控制面要先有哪些。&lt;/p>
&lt;h2 id="總表服務類型與弱點判讀">【總表】服務類型與弱點判讀&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務類型&lt;/th>
 &lt;th>常見弱點&lt;/th>
 &lt;th>可觀察訊號&lt;/th>
 &lt;th>失敗代價&lt;/th>
 &lt;th>最低控制面&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>&lt;/td>
 &lt;td>越權查詢、交易邊界混亂、schema 變更風險&lt;/td>
 &lt;td>權限模型複雜、跨租戶查詢、migration 頻繁&lt;/td>
 &lt;td>資料錯誤、資料洩漏、長時間修復&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a>&lt;/td>
 &lt;td>資料陳舊、快取污染、索引暴露&lt;/td>
 &lt;td>hit rate 波動、回源突增、欄位暴露不一致&lt;/td>
 &lt;td>錯誤決策、客訴、壓力擴散到主存&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data classification&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>message &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> / stream&lt;/td>
 &lt;td>重複投遞、重放濫用、毒訊息擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a>、重試風暴&lt;/td>
 &lt;td>重複執行、狀態偏移、恢復時間拉長&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>observability&lt;/td>
 &lt;td>盲區告警、敏感資料進 log、追蹤斷點&lt;/td>
 &lt;td>告警無法定位、trace 斷鏈、log 欄位失衡&lt;/td>
 &lt;td>修復延遲、誤判、資安風險提升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>deployment / network entry&lt;/td>
 &lt;td>隱藏入口、錯誤設定、切換窗口失控&lt;/td>
 &lt;td>readiness 不穩、error rate 突增、unknown endpoint 被命中&lt;/td>
 &lt;td>擴散式故障、服務中斷、恢復成本升高&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀攻擊者視角總表在選型流程的位置">【判讀】攻擊者視角總表在選型流程的位置&lt;/h2>
&lt;p>攻擊者視角總表放在產品需求與服務實體之間。流程上先做需求分類，再用這份總表檢查弱點與代價，最後才進入產品比較。這個順序能讓選型討論同步納入攻擊面與操作成本，避免把風險留到上線後才處理。&lt;/p>
&lt;h2 id="判讀弱點討論要對齊成本模型">【判讀】弱點討論要對齊成本模型&lt;/h2>
&lt;p>弱點判讀的核心價值是提早看見操作成本。若只看開發速度，常見結果是上線後才補 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、權限分級、告警路由與備援切換。把弱點表納入選型初期，可以同時估算人力成本、容量成本與事故成本，讓服務能力與團隊負擔一起被評估。&lt;/p>
&lt;h2 id="下一步對應模組">【下一步】對應模組&lt;/h2>
&lt;ul>
&lt;li>資料層弱點路徑：模組一 database&lt;/li>
&lt;li>訊息層弱點路徑：模組三 message queue&lt;/li>
&lt;li>平台與入口弱點路徑：模組五 deployment platform&lt;/li>
&lt;li>可觀測性弱點路徑：模組四 observability&lt;/li>
&lt;li>資安與紅隊弱點路徑：模組七 security / red-team&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>跨服務紅隊判讀的核心目標是把「哪裡最容易被打穿」先標出來，再決定服務能力的補強順序。這裡的紅隊是「攻擊者視角的風險檢查方法」：用攻擊者可能採取的路徑反向驗證系統設計。這份總表維持純概念層，不進入實作細節，重點是先回答四件事：暴露面在哪裡、弱點訊號長什麼樣、失敗代價是什麼、最低控制面要先有哪些。</p>
<h2 id="總表服務類型與弱點判讀">【總表】服務類型與弱點判讀</h2>
<table>
  <thead>
      <tr>
          <th>服務類型</th>
          <th>常見弱點</th>
          <th>可觀察訊號</th>
          <th>失敗代價</th>
          <th>最低控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a></td>
          <td>越權查詢、交易邊界混亂、schema 變更風險</td>
          <td>權限模型複雜、跨租戶查詢、migration 頻繁</td>
          <td>資料錯誤、資料洩漏、長時間修復</td>
          <td><a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a></td>
      </tr>
      <tr>
          <td>cache / <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a></td>
          <td>資料陳舊、快取污染、索引暴露</td>
          <td>hit rate 波動、回源突增、欄位暴露不一致</td>
          <td>錯誤決策、客訴、壓力擴散到主存</td>
          <td><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>、<a href="/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data classification</a>、<a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a></td>
      </tr>
      <tr>
          <td>message <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> / stream</td>
          <td>重複投遞、重放濫用、毒訊息擴散</td>
          <td><a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a>、重試風暴</td>
          <td>重複執行、狀態偏移、恢復時間拉長</td>
          <td><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a>、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a></td>
      </tr>
      <tr>
          <td>observability</td>
          <td>盲區告警、敏感資料進 log、追蹤斷點</td>
          <td>告警無法定位、trace 斷鏈、log 欄位失衡</td>
          <td>修復延遲、誤判、資安風險提升</td>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a></td>
      </tr>
      <tr>
          <td>deployment / network entry</td>
          <td>隱藏入口、錯誤設定、切換窗口失控</td>
          <td>readiness 不穩、error rate 突增、unknown endpoint 被命中</td>
          <td>擴散式故障、服務中斷、恢復成本升高</td>
          <td><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a></td>
      </tr>
  </tbody>
</table>
<h2 id="判讀攻擊者視角總表在選型流程的位置">【判讀】攻擊者視角總表在選型流程的位置</h2>
<p>攻擊者視角總表放在產品需求與服務實體之間。流程上先做需求分類，再用這份總表檢查弱點與代價，最後才進入產品比較。這個順序能讓選型討論同步納入攻擊面與操作成本，避免把風險留到上線後才處理。</p>
<h2 id="判讀弱點討論要對齊成本模型">【判讀】弱點討論要對齊成本模型</h2>
<p>弱點判讀的核心價值是提早看見操作成本。若只看開發速度，常見結果是上線後才補 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、權限分級、告警路由與備援切換。把弱點表納入選型初期，可以同時估算人力成本、容量成本與事故成本，讓服務能力與團隊負擔一起被評估。</p>
<h2 id="下一步對應模組">【下一步】對應模組</h2>
<ul>
<li>資料層弱點路徑：模組一 database</li>
<li>訊息層弱點路徑：模組三 message queue</li>
<li>平台與入口弱點路徑：模組五 deployment platform</li>
<li>可觀測性弱點路徑：模組四 observability</li>
<li>資安與紅隊弱點路徑：模組七 security / red-team</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>ElastiCache → 自管 Redis / Valkey：脫離 managed 的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/migrate-to-self-managed/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/migrate-to-self-managed/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type C operational redesign hybrid&lt;/strong>：engine 層相容（Low）但 operational model 差異大（IAM auth → password/ACL、CloudWatch → 自管監控、auto failover → Sentinel/自建 HA）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-managed-遷出">為什麼從 managed 遷出&lt;/h2>
&lt;p>ElastiCache 遷出的 driver 通常不是 engine 層問題 — 它跑的就是 Redis 或 Valkey。常見遷出原因：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>成本&lt;/strong>：managed premium 在大規模（數百 GB、多叢集）下比自管 + 運維人力更貴，尤其跨帳戶大量叢集時&lt;/li>
&lt;li>&lt;strong>跨雲或混合雲&lt;/strong>：業務需要在 GCP、Azure 或 on-prem 同時運行 cache 層，ElastiCache 只在 AWS&lt;/li>
&lt;li>&lt;strong>功能限制&lt;/strong>：ElastiCache 不支援所有 Redis module（RediSearch、RedisJSON 等），或 Valkey 8.x 新功能 ElastiCache 尚未上線&lt;/li>
&lt;li>&lt;strong>控制權&lt;/strong>：自管可以自訂 redis.conf、自選 kernel 參數、自決 upgrade 時機&lt;/li>
&lt;/ul>
&lt;p>資料搬遷用 RDB export + import 就完成，真正的工程量在 operational model 重建 — ElastiCache 幫你管的 HA、monitoring、backup、security，遷出後全要自建。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>同 Redis/Valkey engine、RESP 相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>IAM auth → ACL/password、CloudWatch → 自管監控、auto failover → Sentinel 或手動&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同（key-value cache）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>ElastiCache 1 → Redis/Valkey + Sentinel/HA + 監控 + backup 多元件&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、認證方式換、少量 client config 修改&lt;/td>
 &lt;td>Low-Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB 相容、cluster mode 對應 Redis Cluster&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational model 是 High — 這是 Type C 的判定依據。遷移重心在重建 ElastiCache 幫你做的那些事。&lt;/p>
&lt;h2 id="階段一盤點-elasticache-依賴">階段一：盤點 ElastiCache 依賴&lt;/h2>
&lt;p>在動手之前，先列出 ElastiCache 幫你管的所有東西，每一項都要在自管環境重建或決定不要。&lt;/p>
&lt;h3 id="認證與網路">認證與網路&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>IAM auth&lt;/strong>：ElastiCache 支援 IAM auth token（短效 token），自管 Redis 改用 &lt;code>requirepass&lt;/code> 或 Redis 6+ ACL&lt;/li>
&lt;li>&lt;strong>VPC / Security Group&lt;/strong>：自管 Redis 仍需 VPC 隔離，但 security group 規則要自己維護&lt;/li>
&lt;li>&lt;strong>TLS&lt;/strong>：ElastiCache 原生 in-transit encryption，自管要自己配 redis TLS 憑證&lt;/li>
&lt;/ul>
&lt;h3 id="高可用">高可用&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Auto failover&lt;/strong>：ElastiCache 自動偵測 primary failure 並 promote replica。自管用 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA failover&lt;/a> 或 Redis Cluster 內建 failover&lt;/li>
&lt;li>&lt;strong>Cross-AZ replication&lt;/strong>：ElastiCache 自動跨 AZ。自管要自己在不同 AZ 部署 replica&lt;/li>
&lt;/ul>
&lt;h3 id="監控與備份">監控與備份&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>CloudWatch metrics&lt;/strong>：ElastiCache 自動發 &lt;code>CurrConnections&lt;/code>、&lt;code>CacheHitRate&lt;/code>、&lt;code>ReplicationLag&lt;/code> 等。自管用 &lt;code>INFO&lt;/code> 指令 + Prometheus redis_exporter&lt;/li>
&lt;li>&lt;strong>Snapshot&lt;/strong>：ElastiCache 自動 daily snapshot + 手動 snapshot。自管用 &lt;code>BGSAVE&lt;/code> + cron + 外部 storage&lt;/li>
&lt;/ul>
&lt;h3 id="跨-region-replication">跨 region replication&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Global Datastore&lt;/strong>：ElastiCache 支援跨 region active-passive replication。自管 Redis 沒有原生跨 region replication — 若目前使用 Global Datastore，遷出前需要決定是用 application-level replication、第三方工具（Redis Enterprise Active-Active）還是放棄跨 region cache 同步&lt;/li>
&lt;/ul>
&lt;h3 id="升級與維護">升級與維護&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Engine 升級&lt;/strong>：ElastiCache 在維護窗口自動或手動升級。自管要自己做 rolling upgrade&lt;/li>
&lt;li>&lt;strong>Patch&lt;/strong>：安全 patch 由 AWS 負責。自管要自己追蹤 CVE&lt;/li>
&lt;/ul>
&lt;h2 id="階段二建立自管環境">階段二：建立自管環境&lt;/h2>
&lt;h3 id="部署架構">部署架構&lt;/h3>
&lt;p>最小 production 架構：1 primary + 1 replica + 3 Sentinel（或 Redis Cluster 3 primary + 3 replica）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type C operational redesign hybrid</strong>：engine 層相容（Low）但 operational model 差異大（IAM auth → password/ACL、CloudWatch → 自管監控、auto failover → Sentinel/自建 HA）。</p></blockquote>
<h2 id="為什麼從-managed-遷出">為什麼從 managed 遷出</h2>
<p>ElastiCache 遷出的 driver 通常不是 engine 層問題 — 它跑的就是 Redis 或 Valkey。常見遷出原因：</p>
<ul>
<li><strong>成本</strong>：managed premium 在大規模（數百 GB、多叢集）下比自管 + 運維人力更貴，尤其跨帳戶大量叢集時</li>
<li><strong>跨雲或混合雲</strong>：業務需要在 GCP、Azure 或 on-prem 同時運行 cache 層，ElastiCache 只在 AWS</li>
<li><strong>功能限制</strong>：ElastiCache 不支援所有 Redis module（RediSearch、RedisJSON 等），或 Valkey 8.x 新功能 ElastiCache 尚未上線</li>
<li><strong>控制權</strong>：自管可以自訂 redis.conf、自選 kernel 參數、自決 upgrade 時機</li>
</ul>
<p>資料搬遷用 RDB export + import 就完成，真正的工程量在 operational model 重建 — ElastiCache 幫你管的 HA、monitoring、backup、security，遷出後全要自建。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis/Valkey engine、RESP 相容</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>IAM auth → ACL/password、CloudWatch → 自管監控、auto failover → Sentinel 或手動</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同（key-value cache）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>ElastiCache 1 → Redis/Valkey + Sentinel/HA + 監控 + backup 多元件</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、認證方式換、少量 client config 修改</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB 相容、cluster mode 對應 Redis Cluster</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational model 是 High — 這是 Type C 的判定依據。遷移重心在重建 ElastiCache 幫你做的那些事。</p>
<h2 id="階段一盤點-elasticache-依賴">階段一：盤點 ElastiCache 依賴</h2>
<p>在動手之前，先列出 ElastiCache 幫你管的所有東西，每一項都要在自管環境重建或決定不要。</p>
<h3 id="認證與網路">認證與網路</h3>
<ul>
<li><strong>IAM auth</strong>：ElastiCache 支援 IAM auth token（短效 token），自管 Redis 改用 <code>requirepass</code> 或 Redis 6+ ACL</li>
<li><strong>VPC / Security Group</strong>：自管 Redis 仍需 VPC 隔離，但 security group 規則要自己維護</li>
<li><strong>TLS</strong>：ElastiCache 原生 in-transit encryption，自管要自己配 redis TLS 憑證</li>
</ul>
<h3 id="高可用">高可用</h3>
<ul>
<li><strong>Auto failover</strong>：ElastiCache 自動偵測 primary failure 並 promote replica。自管用 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA failover</a> 或 Redis Cluster 內建 failover</li>
<li><strong>Cross-AZ replication</strong>：ElastiCache 自動跨 AZ。自管要自己在不同 AZ 部署 replica</li>
</ul>
<h3 id="監控與備份">監控與備份</h3>
<ul>
<li><strong>CloudWatch metrics</strong>：ElastiCache 自動發 <code>CurrConnections</code>、<code>CacheHitRate</code>、<code>ReplicationLag</code> 等。自管用 <code>INFO</code> 指令 + Prometheus redis_exporter</li>
<li><strong>Snapshot</strong>：ElastiCache 自動 daily snapshot + 手動 snapshot。自管用 <code>BGSAVE</code> + cron + 外部 storage</li>
</ul>
<h3 id="跨-region-replication">跨 region replication</h3>
<ul>
<li><strong>Global Datastore</strong>：ElastiCache 支援跨 region active-passive replication。自管 Redis 沒有原生跨 region replication — 若目前使用 Global Datastore，遷出前需要決定是用 application-level replication、第三方工具（Redis Enterprise Active-Active）還是放棄跨 region cache 同步</li>
</ul>
<h3 id="升級與維護">升級與維護</h3>
<ul>
<li><strong>Engine 升級</strong>：ElastiCache 在維護窗口自動或手動升級。自管要自己做 rolling upgrade</li>
<li><strong>Patch</strong>：安全 patch 由 AWS 負責。自管要自己追蹤 CVE</li>
</ul>
<h2 id="階段二建立自管環境">階段二：建立自管環境</h2>
<h3 id="部署架構">部署架構</h3>
<p>最小 production 架構：1 primary + 1 replica + 3 Sentinel（或 Redis Cluster 3 primary + 3 replica）。</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"># Docker Compose 驗證用（production 用 VM 或 K8s）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># Primary</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run -d --name redis-primary -p 6379:6379 redis:7 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  redis-server --requirepass <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> --appendonly yes
</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"># Replica</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">docker run -d --name redis-replica -p 6380:6379 redis:7 <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  redis-server --replicaof redis-primary <span class="m">6379</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --masterauth <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> --requirepass <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span></span></span></code></pre></div><p>Sentinel 或 Redis Cluster 配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>。</p>
<h3 id="監控重建">監控重建</h3>
<p>ElastiCache CloudWatch metrics 對應的自管替代：</p>
<table>
  <thead>
      <tr>
          <th>ElastiCache metric</th>
          <th>自管替代</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CurrConnections</td>
          <td><code>connected_clients</code></td>
          <td><code>INFO clients</code></td>
      </tr>
      <tr>
          <td>CacheHitRate</td>
          <td><code>keyspace_hits / (keyspace_hits + keyspace_misses)</code></td>
          <td><code>INFO stats</code></td>
      </tr>
      <tr>
          <td>ReplicationLag</td>
          <td><code>master_repl_offset - slave_repl_offset</code></td>
          <td><code>INFO replication</code></td>
      </tr>
      <tr>
          <td>EngineCPUUtilization</td>
          <td><code>used_cpu_sys + used_cpu_user</code></td>
          <td><code>INFO cpu</code></td>
      </tr>
      <tr>
          <td>DatabaseMemoryUsagePercentage</td>
          <td><code>used_memory / maxmemory</code></td>
          <td><code>INFO memory</code></td>
      </tr>
      <tr>
          <td>Evictions</td>
          <td><code>evicted_keys</code></td>
          <td><code>INFO stats</code></td>
      </tr>
  </tbody>
</table>
<p>用 <a href="https://github.com/oliver006/redis_exporter">Prometheus redis_exporter</a> 自動採集，接 Grafana dashboard。</p>
<h3 id="backup-重建">Backup 重建</h3>





<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"># cron job: 每日 BGSAVE + 等完成 + 上傳 S3</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># LASTSAVE 回傳 Unix timestamp，BGSAVE 完成後 LASTSAVE 會更新</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="m">0</span> <span class="m">3</span> * * * <span class="nv">BEFORE</span><span class="o">=</span><span class="k">$(</span>redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> LASTSAVE<span class="k">)</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> BGSAVE <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  <span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="k">$(</span>redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> LASTSAVE<span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$BEFORE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span> sleep 5<span class="p">;</span> <span class="k">done</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  aws s3 cp /data/dump.rdb s3://backup-bucket/redis/<span class="k">$(</span>date +<span class="se">\%</span>Y<span class="se">\%</span>m<span class="se">\%</span>d<span class="k">)</span>.rdb</span></span></code></pre></div><p>Production 建議搭配 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a> 的監控，確認 BGSAVE 的 fork 不會造成延遲 spike。</p>
<h2 id="階段三資料搬遷與切換">階段三：資料搬遷與切換</h2>
<h3 id="搬遷策略">搬遷策略</h3>
<p>ElastiCache 的資料搬遷有兩條路：</p>
<p><strong>RDB export + import（適合 downtime 可接受的場景）</strong>：</p>
<ol>
<li>ElastiCache 建立手動 snapshot</li>
<li>把 snapshot export 到 S3（ElastiCache console → Export snapshot）</li>
<li>下載 RDB 檔，放到自管 Redis 的資料目錄</li>
<li>重啟自管 Redis 載入 RDB</li>
</ol>
<p><strong>雙寫期間遷移（適合零停機需求）</strong>：</p>
<ol>
<li>Application 同時寫 ElastiCache 和自管 Redis（雙寫）</li>
<li>讀取仍走 ElastiCache</li>
<li>監控自管 Redis 的資料量與命中率追上後，切讀取到自管</li>
<li>移除 ElastiCache 寫入</li>
<li>下線 ElastiCache</li>
</ol>
<p>雙寫的複雜度高於 RDB export。Cache 資料可重建的特性讓第一種策略在多數場景夠用 — 短暫 cache miss 的代價是回源到 DB，通常可接受。</p>
<h3 id="endpoint-切換">Endpoint 切換</h3>
<p>Application 用 endpoint 連 ElastiCache。切換時：</p>
<ol>
<li>把 application config 的 Redis host 改為自管 Redis endpoint</li>
<li>確認 TLS 與認證方式對齊（IAM token → password/ACL）</li>
<li>Rolling restart application</li>
<li>監控 cache hit rate 與 latency 回到 baseline</li>
</ol>
<p>如果用 DNS CNAME 間接指向 ElastiCache endpoint，可以直接改 CNAME 指向自管 Redis，application 不用改 config。</p>
<h2 id="階段四驗證與回退">階段四：驗證與回退</h2>
<h3 id="驗證清單">驗證清單</h3>
<table>
  <thead>
      <tr>
          <th>驗證項目</th>
          <th>通過條件</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連線正常</td>
          <td>application 能 PING、無 auth error</td>
          <td>redis-cli + application log</td>
      </tr>
      <tr>
          <td>資料完整</td>
          <td>key count 跟 ElastiCache 一致（容許 TTL 過期差異）</td>
          <td><code>DBSIZE</code> 比對</td>
      </tr>
      <tr>
          <td>效能 baseline</td>
          <td>latency p99 與 hit rate 跟遷移前一致</td>
          <td>Prometheus + Grafana</td>
      </tr>
      <tr>
          <td>HA 測試</td>
          <td>kill primary，Sentinel promote replica，application 自動重連</td>
          <td>手動 failover drill</td>
      </tr>
      <tr>
          <td>Backup 測試</td>
          <td>BGSAVE 產生 RDB、上傳成功、可還原</td>
          <td>還原到測試 instance 驗證</td>
      </tr>
  </tbody>
</table>
<h3 id="回退路徑">回退路徑</h3>
<p>Cache 遷移的回退比 DB 遷移簡單 — cache 資料可重建。回退步驟：</p>
<ol>
<li>Application config 改回 ElastiCache endpoint（或 CNAME 指回）</li>
<li>Rolling restart</li>
<li>Cache miss 回源到 DB，自然 warm up</li>
</ol>
<p>ElastiCache 在遷移期間不要下線，保留 7-14 天作為回退保險。確認自管 Redis 穩定運行後再刪除 ElastiCache cluster。</p>
<h2 id="成本對照">成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>ElastiCache</th>
          <th>自管 Redis</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute</td>
          <td>managed node pricing（含 premium）</td>
          <td>EC2 / K8s 原價</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>auto failover 內建</td>
          <td>Sentinel 或 Cluster 自建</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>CloudWatch 內建</td>
          <td>redis_exporter + Prometheus 自建</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>自動 snapshot</td>
          <td>cron + S3 自建</td>
      </tr>
      <tr>
          <td>人力</td>
          <td>低（AWS 管）</td>
          <td>高（on-call + upgrade + patch）</td>
      </tr>
      <tr>
          <td>靈活度</td>
          <td>受限（engine version、module）</td>
          <td>完全自控</td>
      </tr>
  </tbody>
</table>
<p>小規模（&lt; 50 GB、&lt; 5 cluster）通常 ElastiCache 的 managed premium 比自管人力便宜。Compute 跟 HA 的差額在小規模可忽略，但監控跟 backup 的自建成本是固定開銷 — 即使只管一個 cluster，redis_exporter + Prometheus + cron backup 的設定跟維護都要做。大規模（數百 GB、多叢集）或跨雲場景下，managed premium 累積到 cluster 數 × node 數的倍數，自管的邊際成本反而更低，遷出 ROI 才成立。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor overview：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>Target vendor 操作：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel HA</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Resharding</a></li>
<li>監控重建：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis Memory Eviction Tuning</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis Persistence Fork Latency</a></li>
<li>反向路徑：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/" data-link-title="自管 Redis / Valkey → AWS ElastiCache：engine 不變、變的是誰運維" data-link-desc="自管 Redis/Valkey 遷到 ElastiCache 的特殊之處：engine 沒變（Redis 還是 Redis）、data model 沒變、API 沒變——變的只有運維責任歸屬。本文跑 6 維 diff audit 對映 Type C operational hybrid、展開 VPC/安全/cutover 的實際工作、以及『把 failover/patching 交出去、同時交出哪些控制權』的責任邊界，5 個 production 踩坑">Redis → ElastiCache</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka → Google Cloud Pub/Sub：從 partition 到 topic-subscription 的模型轉換</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/migrate-from-kafka/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/migrate-from-kafka/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/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 Cloud Pub/Sub&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type E paradigm shift&lt;/strong>：兩者投遞模型本質不同（partition-based log vs topic-subscription pub/sub）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-kafka-遷到-pubsub">為什麼從 Kafka 遷到 Pub/Sub&lt;/h2>
&lt;p>這個遷移的 driver 通常是平台策略：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>All-in GCP&lt;/strong>：組織決定收斂到 GCP 生態，Kafka 是唯一非 GCP 的 stateful 服務，維運孤島成本高&lt;/li>
&lt;li>&lt;strong>運維簡化&lt;/strong>：自管 Kafka cluster 的 broker、ZooKeeper/KRaft、partition rebalance、retention 管理需要專職團隊；Pub/Sub 是全託管&lt;/li>
&lt;li>&lt;strong>GCP 整合&lt;/strong>：下游是 BigQuery、Dataflow、Cloud Run — Pub/Sub 原生串接，Kafka 要加 connector 層&lt;/li>
&lt;li>&lt;strong>全球路由&lt;/strong>：Pub/Sub topic 是 global（不綁 region），Kafka 需要 MirrorMaker 做跨 region 同步&lt;/li>
&lt;/ul>
&lt;p>遷移的工作量不在資料搬遷（message queue 通常不搬歷史資料），在 &lt;strong>模型轉換&lt;/strong> — Kafka 的 partition ordering、consumer group、offset commit 跟 Pub/Sub 的 topic-subscription、ack deadline、ordering key 是不同抽象。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>Kafka producer/consumer API → Pub/Sub client library，完全不同 API&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>自管 broker/ZK/KRaft → 全託管&lt;/td>
 &lt;td>High（方向：簡化）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>partition-based log vs topic-subscription pub/sub&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>Kafka + Schema Registry + Connect → Pub/Sub + (optional) Dataflow&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Producer/Consumer 全部改寫&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>Partition × offset → Topic × subscription × ack&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>五維 High — &lt;strong>Type E paradigm shift&lt;/strong>，是兩套模型的橋接，工程量遠超 drop-in 或翻譯。&lt;/p>
&lt;h2 id="模型差異對照">模型差異對照&lt;/h2>
&lt;p>遷移前必須理解兩套模型的對應關係。對應不是一對一 — 有些概念在對方沒有直接等價物。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Kafka 概念&lt;/th>
 &lt;th>Pub/Sub 對應&lt;/th>
 &lt;th>差異重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Topic&lt;/td>
 &lt;td>Topic&lt;/td>
 &lt;td>名稱相同但語意不同：Kafka topic 有 partition，Pub/Sub topic 沒有&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Partition&lt;/td>
 &lt;td>無直接對應&lt;/td>
 &lt;td>Pub/Sub 的 ordering 用 ordering key 實現，但 ordering key 不保證全域順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consumer group&lt;/td>
 &lt;td>Subscription&lt;/td>
 &lt;td>每個 subscription 獨立消費 topic 的全部訊息，類似 Kafka 的 consumer group&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Offset&lt;/td>
 &lt;td>無直接對應&lt;/td>
 &lt;td>Pub/Sub 用 ack/nack 而非 offset commit。ack 後訊息不可重讀（除非用 seek）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Offset commit&lt;/td>
 &lt;td>Ack&lt;/td>
 &lt;td>Kafka 可以 commit 到任意 offset（replay）；Pub/Sub ack 是 per-message、seek 可以回到 timestamp&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>Message retention&lt;/td>
 &lt;td>Kafka retention 期內可任意 seek；Pub/Sub retention 期內可用 timestamp seek&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consumer lag&lt;/td>
 &lt;td>Oldest unacked message age&lt;/td>
 &lt;td>觀測指標不同：Kafka 看 offset lag、Pub/Sub 看 oldest_unacked_message_age&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Partition rebalance&lt;/td>
 &lt;td>無（Pub/Sub 自動負載分散）&lt;/td>
 &lt;td>Kafka rebalance 是操作痛點，Pub/Sub 消除了這個概念&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema Registry&lt;/td>
 &lt;td>Pub/Sub Schema&lt;/td>
 &lt;td>Pub/Sub 原生支援 Avro/Protobuf schema validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kafka Connect&lt;/td>
 &lt;td>Dataflow / BigQuery subscription&lt;/td>
 &lt;td>下游整合的對應工具不同&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="ordering-語意是最大差異">Ordering 語意是最大差異&lt;/h3>
&lt;p>Kafka 的 ordering 保證是 partition 內全域有序。同一個 partition 的訊息按寫入順序消費，consumer group 內每個 partition 只有一個 consumer。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a>（source）跟 <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 Cloud Pub/Sub</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type E paradigm shift</strong>：兩者投遞模型本質不同（partition-based log vs topic-subscription pub/sub）。</p></blockquote>
<h2 id="為什麼從-kafka-遷到-pubsub">為什麼從 Kafka 遷到 Pub/Sub</h2>
<p>這個遷移的 driver 通常是平台策略：</p>
<ul>
<li><strong>All-in GCP</strong>：組織決定收斂到 GCP 生態，Kafka 是唯一非 GCP 的 stateful 服務，維運孤島成本高</li>
<li><strong>運維簡化</strong>：自管 Kafka cluster 的 broker、ZooKeeper/KRaft、partition rebalance、retention 管理需要專職團隊；Pub/Sub 是全託管</li>
<li><strong>GCP 整合</strong>：下游是 BigQuery、Dataflow、Cloud Run — Pub/Sub 原生串接，Kafka 要加 connector 層</li>
<li><strong>全球路由</strong>：Pub/Sub topic 是 global（不綁 region），Kafka 需要 MirrorMaker 做跨 region 同步</li>
</ul>
<p>遷移的工作量不在資料搬遷（message queue 通常不搬歷史資料），在 <strong>模型轉換</strong> — Kafka 的 partition ordering、consumer group、offset commit 跟 Pub/Sub 的 topic-subscription、ack deadline、ordering key 是不同抽象。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Kafka producer/consumer API → Pub/Sub client library，完全不同 API</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>自管 broker/ZK/KRaft → 全託管</td>
          <td>High（方向：簡化）</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>partition-based log vs topic-subscription pub/sub</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>Kafka + Schema Registry + Connect → Pub/Sub + (optional) Dataflow</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Producer/Consumer 全部改寫</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Partition × offset → Topic × subscription × ack</td>
          <td>High</td>
      </tr>
  </tbody>
</table>
<p>五維 High — <strong>Type E paradigm shift</strong>，是兩套模型的橋接，工程量遠超 drop-in 或翻譯。</p>
<h2 id="模型差異對照">模型差異對照</h2>
<p>遷移前必須理解兩套模型的對應關係。對應不是一對一 — 有些概念在對方沒有直接等價物。</p>
<table>
  <thead>
      <tr>
          <th>Kafka 概念</th>
          <th>Pub/Sub 對應</th>
          <th>差異重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Topic</td>
          <td>Topic</td>
          <td>名稱相同但語意不同：Kafka topic 有 partition，Pub/Sub topic 沒有</td>
      </tr>
      <tr>
          <td>Partition</td>
          <td>無直接對應</td>
          <td>Pub/Sub 的 ordering 用 ordering key 實現，但 ordering key 不保證全域順序</td>
      </tr>
      <tr>
          <td>Consumer group</td>
          <td>Subscription</td>
          <td>每個 subscription 獨立消費 topic 的全部訊息，類似 Kafka 的 consumer group</td>
      </tr>
      <tr>
          <td>Offset</td>
          <td>無直接對應</td>
          <td>Pub/Sub 用 ack/nack 而非 offset commit。ack 後訊息不可重讀（除非用 seek）</td>
      </tr>
      <tr>
          <td>Offset commit</td>
          <td>Ack</td>
          <td>Kafka 可以 commit 到任意 offset（replay）；Pub/Sub ack 是 per-message、seek 可以回到 timestamp</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>Message retention</td>
          <td>Kafka retention 期內可任意 seek；Pub/Sub retention 期內可用 timestamp seek</td>
      </tr>
      <tr>
          <td>Consumer lag</td>
          <td>Oldest unacked message age</td>
          <td>觀測指標不同：Kafka 看 offset lag、Pub/Sub 看 oldest_unacked_message_age</td>
      </tr>
      <tr>
          <td>Partition rebalance</td>
          <td>無（Pub/Sub 自動負載分散）</td>
          <td>Kafka rebalance 是操作痛點，Pub/Sub 消除了這個概念</td>
      </tr>
      <tr>
          <td>Schema Registry</td>
          <td>Pub/Sub Schema</td>
          <td>Pub/Sub 原生支援 Avro/Protobuf schema validation</td>
      </tr>
      <tr>
          <td>Kafka Connect</td>
          <td>Dataflow / BigQuery subscription</td>
          <td>下游整合的對應工具不同</td>
      </tr>
  </tbody>
</table>
<h3 id="ordering-語意是最大差異">Ordering 語意是最大差異</h3>
<p>Kafka 的 ordering 保證是 partition 內全域有序。同一個 partition 的訊息按寫入順序消費，consumer group 內每個 partition 只有一個 consumer。</p>
<p>Pub/Sub 預設不保證 ordering。要 ordering 需開啟 ordering key — 同一 ordering key 的訊息有序，但不同 ordering key 之間無序。ordering key 的並行度由 key 的 cardinality 決定（類似 Kafka 的 partition key）。</p>
<p>遷移時的判斷：</p>
<ul>
<li>若 Kafka 的 ordering 只依賴 partition key（常見），ordering key 直接對應</li>
<li>若依賴 partition 內的全域順序（少見但存在），需要重新設計 — Pub/Sub 沒有 partition 全域順序的概念</li>
<li>若完全不需要 ordering（fan-out 場景），Pub/Sub 預設行為更簡單</li>
</ul>
<h3 id="component-數量轉換">Component 數量轉換</h3>
<p>Kafka 生態的 Schema Registry 在 Pub/Sub 由原生 Schema 功能替代（topic-level schema validation）；Kafka Connect 的 sink connector 由 BigQuery subscription 或 Dataflow job 替代。Dataflow 不是必要 — 簡單的 push/pull consumer 不需要 Dataflow，只有 stream processing（windowed aggregation、join）才需要。</p>
<h2 id="階段一producer-遷移雙寫">階段一：Producer 遷移（雙寫）</h2>
<p>雙寫策略是 paradigm shift 遷移的標準起手。Application 同時把訊息寫入 Kafka 和 Pub/Sub，consumer 仍從 Kafka 消費。</p>
<h3 id="producer-改造">Producer 改造</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 示意：雙寫 wrapper（實際生產用各自語言的 client library）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">publish_order_event</span><span class="p">(</span><span class="n">event</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># 原有 Kafka producer</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">kafka_producer</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s2">&#34;order-events&#34;</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">event</span><span class="o">.</span><span class="n">order_id</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="n">event</span><span class="o">.</span><span class="n">to_bytes</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1"># 新增 Pub/Sub producer</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">pubsub_publisher</span><span class="o">.</span><span class="n">publish</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s2">&#34;projects/my-project/topics/order-events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">data</span><span class="o">=</span><span class="n">event</span><span class="o">.</span><span class="n">to_bytes</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">ordering_key</span><span class="o">=</span><span class="n">event</span><span class="o">.</span><span class="n">order_id</span>  <span class="c1"># 對應 Kafka partition key</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">)</span></span></span></code></pre></div><h3 id="雙寫驗證">雙寫驗證</h3>
<table>
  <thead>
      <tr>
          <th>驗證項目</th>
          <th>方法</th>
          <th>通過條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊息數量一致</td>
          <td>比對 Kafka produce count 與 Pub/Sub publish count</td>
          <td>差異 &lt; 0.01%（允許 timing 差異）</td>
      </tr>
      <tr>
          <td>Ordering 一致</td>
          <td>同一 ordering key 的訊息在兩端順序相同</td>
          <td>抽樣驗證 100 個 key</td>
      </tr>
      <tr>
          <td>Latency 影響</td>
          <td>監控 request latency 變化</td>
          <td>p99 增加 &lt; 10ms</td>
      </tr>
      <tr>
          <td>失敗隔離</td>
          <td>Pub/Sub publish 失敗不影響 Kafka publish</td>
          <td>Pub/Sub timeout 時 Kafka 正常</td>
      </tr>
  </tbody>
</table>
<p>雙寫的失敗隔離要嚴格設計。Pub/Sub publish 失敗時，application 應該 log + metric 但不 block request。Kafka 是已驗證的正式路徑，Pub/Sub 在這個階段是 shadow。</p>
<h2 id="階段二consumer-遷移逐-subscription-切換">階段二：Consumer 遷移（逐 subscription 切換）</h2>
<p>Producer 雙寫穩定後，逐一把 consumer 從 Kafka 切到 Pub/Sub subscription。</p>
<h3 id="consumer-改造重點">Consumer 改造重點</h3>
<p><strong>Ack 模型差異</strong>：Kafka consumer 是 poll + commit offset；Pub/Sub 是 pull（或 push）+ per-message ack。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Kafka consumer pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">kafka_consumer</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">process</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">kafka_consumer</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># Pub/Sub pull subscriber pattern</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">callback</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">process</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">message</span><span class="o">.</span><span class="n">ack</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">message</span><span class="o">.</span><span class="n">nack</span><span class="p">()</span>  <span class="c1"># 會被重新投遞</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">subscriber</span><span class="o">.</span><span class="n">subscribe</span><span class="p">(</span><span class="s2">&#34;projects/my-project/subscriptions/order-processor&#34;</span><span class="p">,</span> <span class="n">callback</span><span class="o">=</span><span class="n">callback</span><span class="p">)</span></span></span></code></pre></div><p><strong>Idempotency 更重要</strong>：Pub/Sub 的 at-least-once delivery 加上 ack deadline 機制，redelivery 比 Kafka 更容易觸發（ack deadline 內沒 ack 就重投）。Consumer 的 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 設計要比 Kafka 時更嚴格。</p>
<p><strong>Flow control</strong>：Pub/Sub client library 支援 <code>max_outstanding_messages</code> 和 <code>max_outstanding_bytes</code> 做 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 控制，對應 Kafka 的 <code>max.poll.records</code>。</p>
<h3 id="切換順序">切換順序</h3>
<p>依 consumer 的重要度和複雜度排序：</p>
<ol>
<li>先切 stateless consumer（log pipeline、metrics aggregation）— 低風險</li>
<li>再切有 side effect 但 idempotent 的 consumer（search index sync、notification）</li>
<li>最後切核心 consumer（payment processing、inventory update）— 需要完整 idempotency 驗證</li>
</ol>
<p>每切一組 consumer：</p>
<ol>
<li>建立對應的 Pub/Sub subscription</li>
<li>部署新 consumer（讀 Pub/Sub）</li>
<li>驗證處理正確性（比對 Kafka consumer 和 Pub/Sub consumer 的輸出）</li>
<li>停止舊 Kafka consumer</li>
<li>觀察 7 天無異常</li>
</ol>
<h2 id="階段三停止雙寫">階段三：停止雙寫</h2>
<p>所有 consumer 切完後：</p>
<ol>
<li>停止 Kafka producer（移除雙寫邏輯）</li>
<li>觀察 Kafka topic 不再有新訊息</li>
<li>等 Kafka retention 過期</li>
<li>下線 Kafka cluster</li>
</ol>
<p>Kafka cluster 不要在 consumer 切完後立即下線。保留 retention period + 7 天作為回退保險。</p>
<h2 id="回退路徑">回退路徑</h2>
<p>Type E 遷移的回退要在每個階段都設計：</p>
<ul>
<li><strong>階段一回退</strong>：移除 Pub/Sub publish 邏輯，Kafka 路徑不受影響</li>
<li><strong>階段二回退</strong>：重啟 Kafka consumer、停止 Pub/Sub subscriber。Kafka 的 offset 要確認是否仍在 retention 內</li>
<li><strong>階段三回退</strong>：如果 Kafka 已下線，需要重新建 cluster 並從 Pub/Sub 反向雙寫回 Kafka — 成本高，所以階段三前要確認穩定</li>
</ul>
<p>回退的關鍵指標：consumer lag（Pub/Sub 的 <code>oldest_unacked_message_age</code>）持續上升、error rate 上升、或 redelivery rate 異常。</p>
<h2 id="遷移後的監控對照">遷移後的監控對照</h2>
<table>
  <thead>
      <tr>
          <th>Kafka 監控指標</th>
          <th>Pub/Sub 對應指標</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Consumer lag (offset)</td>
          <td><code>subscription/oldest_unacked_message_age</code></td>
          <td>Cloud Monitoring</td>
      </tr>
      <tr>
          <td>Produce rate</td>
          <td><code>topic/send_message_operation_count</code></td>
          <td>Cloud Monitoring</td>
      </tr>
      <tr>
          <td>Consume rate</td>
          <td><code>subscription/pull_message_operation_count</code></td>
          <td>Cloud Monitoring</td>
      </tr>
      <tr>
          <td>Redelivery count</td>
          <td><code>subscription/dead_letter_message_count</code> + nack rate</td>
          <td>Cloud Monitoring</td>
      </tr>
      <tr>
          <td>Broker disk usage</td>
          <td>無需關注（fully managed）</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>Rebalance events</td>
          <td>無（Pub/Sub 自動分散）</td>
          <td>N/A</td>
      </tr>
  </tbody>
</table>
<h2 id="不適合遷移的場景">不適合遷移的場景</h2>
<p>以下場景 Kafka → Pub/Sub 的 ROI 不成立：</p>
<ul>
<li><strong>需要 exactly-once semantics</strong>：Kafka 的 transactional producer + idempotent producer 提供 exactly-once；Pub/Sub 是 at-least-once，application 層做 dedup</li>
<li><strong>需要長期 replay</strong>：Kafka retention 可設數月甚至永久（tiered storage）；Pub/Sub message retention 最長 31 天（若需超過 31 天的 replay，可用 BigQuery subscription 做長期歸檔，但查詢模式不同於 Kafka 的 offset-based replay）</li>
<li><strong>大量 ordering 依賴</strong>：如果 Kafka topology 重度依賴 partition ordering 且 key cardinality 低，Pub/Sub ordering key 的並行度會比 Kafka 差</li>
<li><strong>使用 Kafka Streams / ksqlDB 做 stateful processing</strong>：stream processing 邏輯跟 Kafka 綁定（state store backed by changelog topic），遷到 Pub/Sub 要同時遷移 processing 框架（→ Dataflow / Beam），工程量額外翻倍且 API 完全不同</li>
<li><strong>多雲 / 非 GCP 環境</strong>：Pub/Sub 是 GCP-only，跨雲場景反而讓 Kafka 更合理</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor overview：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a></li>
<li>Target vendor overview：<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 Cloud Pub/Sub</a></li>
<li>Pub/Sub 操作細節：<a href="/blog/backend/03-message-queue/vendors/google-pubsub/push-pull-ack-flow-control/" data-link-title="Google Pub/Sub push vs pull：不是實作偏好，是下游容量的判讀" data-link-desc="Pub/Sub 的 push 與 pull subscription 常被當成實作偏好二選一，但它其實是一個容量判讀：push 把流量瞬間打到 endpoint，pull 讓 consumer 自己節流。下游有 RPS 限制就只能 pull。本文展開 subscription 模型、ack deadline、flow control 與 dead-letter topic，5 個把 push/pull 與 ack deadline 寫成下游打爆與重投的 production 踩坑">Push / Pull / Ack Flow Control</a>、<a href="/blog/backend/03-message-queue/vendors/google-pubsub/ordering-dlt-schema/" data-link-title="Pub/Sub Ordering Key、Dead-Letter Topic 與 Schema Enforcement：三道交付治理" data-link-desc="Pub/Sub overview 之下的 implementation-layer deep article — 把 ordering key 的有序代價、dead-letter topic 的 poison message 隔離、schema enforcement 的契約守門三件事寫到可操作：subscription 是 first-class、ackDeadline 與 extension、push vs pull vs streaming pull &#43; flow control、Avro / Protobuf schema、Pub/Sub Lite 與標準版差異、BigQuery / Cloud Storage subscription，含 5 個 production 故障演練（ordering 限流 / ack deadline 太短重投 / DLT max delivery attempts / push 500 retry storm / schema 擋下不相容 publish）">Ordering / DLT / Schema</a></li>
<li>Consumer idempotency：<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 Design</a>、<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Processing Recovery Semantics</a></li>
<li>反向路徑（SQS → Pub/Sub）：<a href="/blog/backend/03-message-queue/vendors/aws-sqs/migrate-to-google-pubsub/" data-link-title="AWS SQS → Google Pub/Sub：queue 模型搬到 topic &#43; subscription 模型的跨雲遷移" data-link-desc="SQS 是單一 region-scoped pull queue、Pub/Sub 是 global topic &#43; first-class subscription 的 pub/sub 模型；這篇跨雲 migration playbook 走 6 維 diff dimension audit（components / data topology 軸 High）、對位 visibility timeout → ack deadline、maxReceiveCount → dead-letter topic、long polling → streaming pull、IAM policy → Service Account、SQS-to-many-consumer 要重設計成 topic fan-out；含 5 個 production 故障演練（fan-out 行為差 / ack deadline 太短重投 / ordering key vs FIFO / 跨雲網路成本 / DLT 設定差）跟 dual-publish 漸進 cutover">AWS SQS → Google Pub/Sub</a></li>
</ul>
]]></content:encoded></item><item><title>Remote Write 與長期儲存整合</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：&lt;/p>
&lt;p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。&lt;/p>
&lt;p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。&lt;/p>
&lt;p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。&lt;/p>
&lt;p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="remote-write-protocol">Remote write protocol&lt;/h3>
&lt;p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。&lt;/p>
&lt;p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（&lt;code>--storage.tsdb.wal-segment-size&lt;/code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。&lt;/p>
&lt;h3 id="exemplar-forwarding">Exemplar forwarding&lt;/h3>
&lt;p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。&lt;/p>
&lt;p>啟用方式：scrape config 加 &lt;code>enable_features: [exemplar-storage]&lt;/code>，remote write endpoint 支援 exemplar 即可自動 forward。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：</p>
<p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。</p>
<p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。</p>
<p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。</p>
<p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="remote-write-protocol">Remote write protocol</h3>
<p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。</p>
<p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（<code>--storage.tsdb.wal-segment-size</code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。</p>
<h3 id="exemplar-forwarding">Exemplar forwarding</h3>
<p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。</p>
<p>啟用方式：scrape config 加 <code>enable_features: [exemplar-storage]</code>，remote write endpoint 支援 exemplar 即可自動 forward。</p>
<h3 id="dedup-策略">Dedup 策略</h3>
<p>跑兩個 Prometheus HA pair 時，兩個實例都 scrape 同一組 target、都 remote write 到同一個後端。後端會收到兩份幾乎相同但不完全一致的 samples（scrape 時間差 ±1-2 秒）。</p>
<p>Thanos 和 Mimir 都有 dedup 機制：Thanos 在 query 層根據 <code>external_labels</code>（replica label）做 dedup，每個 time window 只取一個 replica 的值。Mimir 在 ingester 層做 dedup，同一個 series 的重複 sample 在寫入時合併。</p>
<p>Dedup 的前提是兩個 Prometheus 實例設定不同的 <code>external_labels</code>（例如 <code>replica: a</code> / <code>replica: b</code>），讓後端能辨別哪些 series 是同一組的不同副本。</p>
<h2 id="配置">配置</h2>
<h3 id="remote-write-基本設定">Remote write 基本設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir-distributor:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">queue_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">capacity</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">max_shards</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">max_samples_per_send</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">batch_send_deadline</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;go_.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p><code>queue_config</code> 控制 remote write 的並行度與批次大小：</p>
<ul>
<li><code>capacity</code>：內存中暫存的 sample 數量。太小會頻繁 flush、太大會佔記憶體</li>
<li><code>max_shards</code>：並行的 write goroutine 數量。Shard 太少會造成 backlog、太多會壓垮遠端</li>
<li><code>max_samples_per_send</code>：每次 POST 的 sample 數量。5000 是常用值</li>
<li><code>batch_send_deadline</code>：即使 batch 沒滿也在這個時間內 flush，避免低流量時 sample 延遲太久</li>
</ul>
<p><code>write_relabel_configs</code> 在 remote write 前過濾 series — 不需要長期保存的 internal metrics（go runtime、scrape metadata）可以在這裡 drop，減少長期儲存的 cardinality 與成本。</p>
<h3 id="external-labelsha-與多叢集">External labels（HA 與多叢集）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">external_labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">prod-us</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">replica</span><span class="p">:</span><span class="w"> </span><span class="l">a</span></span></span></code></pre></div><p><code>cluster</code> label 區分來源叢集，<code>replica</code> label 讓長期儲存做 dedup。每個 Prometheus 實例的 external_labels 必須唯一。</p>
<h3 id="三家長期儲存比較">三家長期儲存比較</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Mimir</th>
          <th>Thanos</th>
          <th>Cortex</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>架構模式</td>
          <td>Microservice（distributor / ingester / compactor / querier）</td>
          <td>Sidecar + Store Gateway + Compactor + Query</td>
          <td>Microservice（跟 Mimir 同源、Mimir 是 Cortex fork）</td>
      </tr>
      <tr>
          <td>部署複雜度</td>
          <td>中（Helm chart，最少 4 個元件）</td>
          <td>中高（sidecar 綁 Prometheus pod，元件分散）</td>
          <td>高（元件多、已進入維護模式）</td>
      </tr>
      <tr>
          <td>Query layer</td>
          <td>原生 PromQL + split/merge</td>
          <td>Thanos Query 做 fan-out + dedup</td>
          <td>原生 PromQL（跟 Mimir 共用）</td>
      </tr>
      <tr>
          <td>多租戶</td>
          <td>原生（X-Scope-OrgID header）</td>
          <td>有限（靠 label 或獨立部署）</td>
          <td>原生（Mimir 繼承）</td>
      </tr>
      <tr>
          <td>Downsampling</td>
          <td>支援（compactor 做 1h/5m 降取樣）</td>
          <td>支援（compactor）</td>
          <td>支援</td>
      </tr>
      <tr>
          <td>開發狀態</td>
          <td>活躍（Grafana Labs 主推）</td>
          <td>活躍（CNCF incubating）</td>
          <td>維護模式（Grafana Labs 把精力轉到 Mimir）</td>
      </tr>
      <tr>
          <td>對象儲存</td>
          <td>S3 / GCS / Azure Blob</td>
          <td>S3 / GCS / Azure Blob / 本地</td>
          <td>S3 / GCS</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>自管 compute + storage；Grafana Cloud 按 active series 計費</td>
          <td>自管 compute + storage</td>
          <td>自管（不推薦新部署）</td>
      </tr>
  </tbody>
</table>
<p>選擇判準依三個維度排序：</p>
<p><strong>已經在用 Grafana 生態</strong>（Grafana dashboard、Loki、Tempo）：Mimir 是最自然的選擇，跟 Grafana Stack 的整合最深，Grafana Cloud 可以免管 Mimir。</p>
<p><strong>需要最小化對 Prometheus 的改動</strong>：Thanos sidecar 模式不改 Prometheus 配置（sidecar 讀本地 TSDB block），適合「先加長期儲存、Prometheus 維持現狀」的漸進路徑。但 sidecar 綁 Prometheus pod，K8s 環境外的部署更複雜。</p>
<p><strong>多租戶需求</strong>：Mimir 原生支援多租戶隔離（每個 tenant 獨立 TSDB、query isolation），Thanos 的多租戶靠 label 或獨立部署。</p>
<p>Cortex 是 Mimir 的前身，新部署不推薦。既有 Cortex 部署可參考 Grafana Labs 的 Mimir migration guide。</p>
<h3 id="uber-m3-的第四條路">Uber M3 的第四條路</h3>
<p><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber M3 案例</a>選擇了自建 M3DB 而非 Mimir / Thanos / Cortex — 原因是 M3DB 在 2018 年啟動時、Mimir 尚未存在、Cortex 還在早期階段、Thanos 也剛開源。M3DB 的設計核心是 namespace-level retention（不同 namespace 不同 retention 跟 resolution）、跟 Uber 的 etcd service discovery 深度整合。</p>
<p>M3 的經驗對後來的三家有直接影響：Mimir 的 per-tenant retention、Thanos 的 downsampling compactor、都能追溯到 M3 先踩過的問題。今天做新部署不需要重走 M3 的路 — Mimir 跟 Thanos 已經成熟。但 M3 案例揭露的設計判準仍然有效：</p>
<ul>
<li><strong>跨 cluster 查詢需要 fan-out + dedup</strong>：三家都實作了這個能力，但部署配置跟 dedup 策略各有差異</li>
<li><strong>Downsampling 是長期成本控制的必要手段</strong>：不做 downsampling、90 天 range query 的效能跟成本都不可接受</li>
<li><strong>多租戶隔離不只是 query 層面</strong>：ingestion rate limit 跟 storage quota per tenant 才能防止「一個團隊的 cardinality 爆炸拖垮整個平台」</li>
</ul>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="remote-write-backlog-佔滿-wal">Remote write backlog 佔滿 WAL</h3>
<p><strong>觸發條件</strong>：遠端不可達（network 問題、後端過載）持續超過數分鐘，WAL segment 堆積。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_bytes_total</code> 停止增長（寫不出去）、<code>prometheus_wal_storage_size_bytes</code> 持續增長、disk 使用率上升。嚴重時 WAL 佔滿 disk，Prometheus 無法寫入新 sample、連 local scrape 也受影響。</p>
<p><strong>修復</strong>：先恢復遠端連線。WAL backlog 會在連線恢復後自動 catch up — Prometheus 按 WAL 順序重送積壓的 samples。如果 catch up 時間太長（例如堆了數小時），remote write 的 max_shards 可以暫時調高加速回補，但要注意不要壓垮剛恢復的遠端。</p>
<p><strong>預防</strong>：監控 <code>prometheus_remote_storage_queue_highest_sent_timestamp_seconds</code> 跟 current time 的差距 — 差距代表 remote write 延遲。差距超過 5 分鐘時告警。設定 WAL 的 disk 空間上限（<code>--storage.tsdb.max-block-duration</code> 搭配 retention 控制 total disk）。</p>
<h3 id="target-不可達時的-retry-storm">Target 不可達時的 retry storm</h3>
<p><strong>觸發條件</strong>：remote write endpoint 回傳 5xx 或 429（rate limit），Prometheus 進入指數退避重試。大量 shard 同時 retry，CPU 跟 network 消耗上升。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_retried_samples_total</code> 增長、CPU 使用上升、remote write 延遲拉大。如果後端本來就過載，retry storm 會讓情況惡化。</p>
<p><strong>修復</strong>：remote write 配置中的 <code>min_backoff</code> / <code>max_backoff</code> 控制 retry 間隔（預設 30ms / 5s）。可以調高 <code>min_backoff</code> 減緩 retry 頻率。長期修法是讓後端回傳 429 搭配 <code>Retry-After</code> header，Prometheus 會遵守。</p>
<h3 id="metrics-語意-drift">Metrics 語意 drift</h3>
<p><strong>觸發條件</strong>：多個 Prometheus 實例的 <code>write_relabel_configs</code> 不一致、或 external_labels 設定有誤。</p>
<p><strong>表現</strong>：同一個 metric 在長期儲存中出現語意不同的 series — 有些 instance 保留了某個 label、有些 drop 掉了。Dashboard 查詢結果不一致（取決於查到哪個實例的 series）。</p>
<p><strong>修復</strong>：remote write 的 <code>write_relabel_configs</code> 集中管理（配置模板或 Prometheus Operator 的 PrometheusSpec.remoteWrite）。每次修改 relabel 規則後，驗證所有實例的 series label set 一致。Mimir 的 <code>active_series</code> API 可以列出目前所有 active series 的 label set。</p>
<h3 id="remote-write-protocol-版本不匹配">Remote write protocol 版本不匹配</h3>
<p><strong>觸發條件</strong>：Prometheus 版本跟長期儲存後端期望的 remote write protocol 版本不一致。Prometheus 2.x 使用 remote write v1（protobuf + snappy），部分較新後端開始支援 v2（native histogram 支援、metadata 改進）。</p>
<p><strong>表現</strong>：後端回傳 400 Bad Request。Prometheus 對 4xx 的預設行為是不 retry（視為 client error、retry 無意義），samples 被 drop。<code>prometheus_remote_storage_samples_failed_total</code> 增長但不像 5xx 那樣有明顯的 retry storm — 靜默丟失更難察覺。</p>
<p><strong>修復</strong>：確認 Prometheus 版本跟後端的 protocol 相容性。Mimir / Thanos 的文件通常標明支援的 remote write protocol 版本。版本不匹配時升級 Prometheus 或降級後端配置。</p>
<h3 id="何時單機-prometheus-不夠">何時單機 Prometheus 不夠</h3>
<p>三個訊號同時出現時，remote write + 長期儲存從「可選」變成「必要」：</p>
<p><strong>Active series 超過 500 萬</strong>。單機 Prometheus 在 500 萬 series 左右開始出現記憶體壓力（head block ~20 GB）、WAL replay 時間拉長（重啟要數分鐘）、compaction 佔用 CPU。<a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber 在 M3 專案</a>遇到的正是這個天花板 — 數十個叢集各自 scrape 的 metrics 匯總後 series 數遠超單機能力，但「用更大的 VM 跑 Prometheus」不是解法，因為 Prometheus 的 TSDB 是單線程 compaction、垂直擴展的效益有上限。</p>
<p><strong>Retention 需求超過 30 天</strong>。本地 TSDB 的 retention 拉長時，range query 的效能線性退化 — 查 90 天 range 要掃描的 block 數量是 15 天的 6 倍。Downsampling 是長期儲存後端的標準能力（Mimir / Thanos compactor 把 5 分鐘 resolution 降到 1 小時），但 Prometheus 本地 TSDB 不做 downsampling。Uber 的 M3DB 設計了 namespace-level retention（short-term 48h full resolution、long-term 1y downsampled），讓查詢成本不隨 retention 線性成長。</p>
<p><strong>跨叢集統一查詢</strong>。多個 Prometheus 各自 scrape 不同 cluster 時，工程師需要一個入口看「所有 cluster 的 checkout error rate」。手動切 Grafana datasource 容易遺漏。Remote write 把所有 Prometheus 的 metrics 匯入同一個長期儲存、用單一查詢入口（Mimir querier / Thanos Query）做 fan-out。</p>
<p>這三個需求在中型公司（50-200 服務、3+ K8s cluster）通常在 1-2 年內同時浮現。規劃 remote write 時不用等三個都出現 — 任一個出現就是啟動的合理時機。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<h3 id="remote-write-bandwidth">Remote write bandwidth</h3>
<p>Remote write 的 bandwidth ≈ ingestion rate × 每 sample 壓縮後大小（約 1-2 bytes with snappy）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>估算 bandwidth</th>
          <th>對應規模參考</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>~100-200 KB/s</td>
          <td>小型：5-10 服務、1 cluster</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>~500 KB/s-1 MB/s</td>
          <td>中型：50 服務、2-3 cluster</td>
      </tr>
      <tr>
          <td>200 萬 samples/sec</td>
          <td>~2-4 MB/s</td>
          <td>大型：200 服務、5+ cluster</td>
      </tr>
      <tr>
          <td>1000 萬 samples/sec</td>
          <td>~10-20 MB/s</td>
          <td>平台級：Uber M3 等級</td>
      </tr>
  </tbody>
</table>
<p>每個 active series 在 15 秒 scrape interval 下每秒產生 ~0.067 個 sample。100 萬 active series 的 ingestion rate ≈ 6.7 萬 samples/sec，對應 ~70-140 KB/s remote write bandwidth。這個數字在內網環境下通常不是瓶頸。</p>
<p>真正的瓶頸在兩個地方：<strong>roundtrip latency</strong> 決定單 shard 吞吐上限（每次 POST 等回應才發下一批）、<strong>後端 ingestion capacity</strong> 決定能消化多少 samples/sec。Mimir 的 distributor 跟 ingester 可以水平擴展，但每加一個 ingester 增加 compute 成本。bandwidth 只是 capacity planning 的第一步，實際規模要用 Mimir 的 <code>cortex_distributor_received_samples_total</code> 跟 <code>cortex_ingester_memory_series</code> 做持續觀測。</p>
<h3 id="長期儲存的-compaction-與-downsampling-cost">長期儲存的 compaction 與 downsampling cost</h3>
<p>Mimir 和 Thanos 的 compactor 定期合併 block 並做 downsampling（5m → 1h 粒度）。Compaction 消耗 CPU 和 disk I/O，但跑在長期儲存自己的 compute 上，不影響 Prometheus。</p>
<p>成本結構：</p>
<ul>
<li><strong>Compute</strong>：distributor + ingester + querier + compactor 的 CPU / memory。Mimir 官方建議 ingester 是最吃資源的元件（記憶體中保存 active series）</li>
<li><strong>Object storage</strong>：S3 / GCS 的儲存量 ≈ ingestion rate × retention × 壓縮率。Compaction 跟 downsampling 會降低儲存量（通常 2-5x 壓縮）</li>
<li><strong>Query cost</strong>：長 range query 需要讀大量 block — 在 cloud object storage 上是 GET request 成本。Mimir 用 index cache（memcached）降低重複查詢的 GET request</li>
</ul>
<p>跟 Prometheus 本地 TSDB 比，長期儲存把 disk cost 換成 object storage cost（通常更便宜），但增加了 compute cost（長期儲存的 ingester / querier / compactor）。判斷轉折點的方式是比較本地 SSD cost × retention 跟 object storage cost + compute cost。retention 超過 30 天時，object storage 的成本優勢通常明顯。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="接-grafana-stack-lgtm">接 Grafana Stack LGTM</h3>
<p>Mimir 是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> LGTM（Loki + Grafana + Tempo + Mimir）的 metrics 後端。Prometheus remote write 到 Mimir 後，Grafana 用 Mimir 作為 Prometheus-compatible datasource，查詢語言仍是 PromQL。Exemplar forwarding 讓 Mimir metrics 可以連結到 Tempo traces。</p>
<h3 id="接-telemetry-pipeline">接 Telemetry Pipeline</h3>
<p>Remote write 在 <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> 中扮演 metrics ingestion 段。如果同時使用 OpenTelemetry Collector，Collector 可以作為 remote write 的中繼（接收 Prometheus scrape → OTLP export → Mimir OTLP endpoint），但多一層中繼增加了 failure point。直接 Prometheus → Mimir remote write 是最簡路徑。</p>
<h3 id="接-cost-attribution">接 Cost Attribution</h3>
<p>長期儲存的多租戶能力讓 <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> 可以按 tenant / team / service 拆分 metrics 成本。Mimir 的 per-tenant active series quota 同時控制 cardinality 與成本。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../promql-recording-rules/">PromQL 與 Recording Rules 實務</a>：remote write 架構下 recording rules 的部署位置選擇</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：remote write 作為容量超限時的卸載路徑</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為長期儲存的完整操作指南</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>：remote write 在 pipeline 架構中的定位</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>：多租戶 metrics 的成本拆分</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>Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore&lt;/a> overview 的 deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論&lt;/a>。規則語法以 &lt;a href="https://firebase.google.com/docs/firestore/security/get-started">官方 Security Rules 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境授權沒有後端可以藏">問題情境：授權沒有後端可以藏&lt;/h2>
&lt;p>自建後端的授權有一個天然的藏身處：所有讀寫都過 API，權限檢查寫在 service 層，前端拿不到的資料就是拿不到。Firestore 的 client 直連模型把這個藏身處拿掉了——前端 SDK 直接連資料庫，唯一擋在「任何人都能讀整個 collection」與「正確授權」之間的，就是 Security Rules。規則寫錯一條，等於把資料庫對公網敞開。&lt;/p>
&lt;p>這個責任轉移最常見的引爆點是上線後的滲透測試或 bug bounty：報告指出「未登入就能用 REST API 拉出整張 &lt;code>users&lt;/code> collection」。根因幾乎都是同一類——開發期為了方便把規則設成 &lt;code>allow read, write: if true&lt;/code>，上線忘了收。Firestore 的規則是控制面的全部，這篇處理它的求值模型、如何把它寫成可測試的程式碼、以及它撐不住時的退場路線。&lt;/p>
&lt;h2 id="核心概念規則的求值模型">核心概念：規則的求值模型&lt;/h2>
&lt;p>Firestore Security Rules 是一套宣告式 DSL，掛在 &lt;code>match&lt;/code> path 上、對每個讀寫請求求值。理解它要抓住四個跟後端授權不同的點：&lt;/p>
&lt;p>&lt;strong>規則不是 filter，是 allow/deny 判定&lt;/strong>。一條 &lt;code>allow read: if &amp;lt;condition&amp;gt;&lt;/code> 不會「只回傳符合條件的 document」——它是對「這次請求能不能執行」的布林判定。query 若可能讀到任何不符合規則的 document，整個 query 被拒絕，不是默默過濾。這逼著 client 的 query 必須自帶與規則一致的條件（例如 &lt;code>where('ownerId', '==', uid)&lt;/code>），規則才放行。&lt;/p>
&lt;p>&lt;strong>規則預設拒絕&lt;/strong>。沒有 &lt;code>match&lt;/code> 命中的 path 一律拒絕。&lt;code>rules_version = '2'&lt;/code> 下，&lt;code>match /{document=**}&lt;/code> 遞迴匹配所有 subcollection，要小心別用一條寬鬆的遞迴規則蓋掉底下該嚴格的 path。&lt;/p>
&lt;p>&lt;strong>請求脈絡來自 &lt;code>request&lt;/code> 與 &lt;code>resource&lt;/code>&lt;/strong>。&lt;code>request.auth&lt;/code> 是已驗證的身分（&lt;code>request.auth.uid&lt;/code>、&lt;code>request.auth.token&lt;/code> 的 custom claims）；&lt;code>request.resource.data&lt;/code> 是寫入後的 document 狀態；&lt;code>resource.data&lt;/code> 是寫入前的既有狀態。授權與資料驗證都在這幾個物件上展開。&lt;/p>
&lt;p>&lt;strong>跨 document 查詢用 &lt;code>get()&lt;/code> / &lt;code>exists()&lt;/code>&lt;/strong>。判斷「這個 user 是不是這個 project 的成員」要去讀另一份 document，用 &lt;code>get(/databases/$(database)/documents/projects/$(pid)/members/$(uid))&lt;/code>。每個 &lt;code>get()&lt;/code> 是一次額外讀取、計入計費，也有每請求次數上限（規則內 document access 有上限，設計時要省著用）。&lt;/p>
&lt;p>基本骨架：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nx">rules_version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;2&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nx">service&lt;/span> &lt;span class="nx">cloud&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firestore&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">match&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">databases&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">database&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">documents&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">match&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">notes&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">noteId&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ownerId&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">create&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ownerId&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">delete&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ownerId&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>read&lt;/code> 用 &lt;code>resource.data&lt;/code>（既有 document），&lt;code>create&lt;/code> 用 &lt;code>request.resource.data&lt;/code>（沒有既有狀態），&lt;code>update&lt;/code> 兩者都要看——把 &lt;code>read&lt;/code> / &lt;code>create&lt;/code> / &lt;code>update&lt;/code> / &lt;code>delete&lt;/code> 分開是建模的起點，混成一條 &lt;code>allow read, write&lt;/code> 是後面所有漏洞的源頭。&lt;/p>
&lt;h2 id="配置把授權拆成可組合-function">配置：把授權拆成可組合 function&lt;/h2>
&lt;p>規則一旦超過幾個 collection，inline 的 &lt;code>if&lt;/code> 條件會重複且難讀。把授權判斷抽成 &lt;code>function&lt;/code>，讓每條規則讀起來像在描述意圖，是讓規則可維護的核心手段：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nx">rules_version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;2&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nx">service&lt;/span> &lt;span class="nx">cloud&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firestore&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">match&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">databases&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">database&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">documents&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">isSignedIn&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">isOwner&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">docData&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">isSignedIn&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">docData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ownerId&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="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="kd">function&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">isSignedIn&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">exists&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/databases/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">database&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">documents&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">projects&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">members&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&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="kd">function&lt;/span> &lt;span class="nx">hasRole&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">role&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/databases/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">database&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">documents&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">projects&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">members&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">role&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">role&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &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>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 寫入時欄位白名單：禁止 client 竄改 ownerId / createdAt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fieldsUnchanged&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fields&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">diff&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">affectedKeys&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">hasOnly&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fields&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nx">match&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">projects&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">hasRole&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;admin&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">fieldsUnchanged&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;description&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;updatedAt&amp;#39;&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="k">delete&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">hasRole&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;owner&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="nx">match&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="nx">tasks&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">taskId&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">create&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resource&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createdBy&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">auth&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="nx">allow&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">delete&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">isProjectMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這裡有三個建模手段值得展開。第一，&lt;code>isProjectMember&lt;/code> / &lt;code>hasRole&lt;/code> 把「成員資格」與「角色」的判斷集中成單一定義，授權邏輯改一處全站生效，避免同一條規則散落在十個 collection。第二，&lt;code>fieldsUnchanged&lt;/code> 用 &lt;code>diff().affectedKeys().hasOnly()&lt;/code> 把「這次 update 只准動哪些欄位」寫成白名單——這擋掉 client 直接改 &lt;code>ownerId&lt;/code> 把別人的資料佔為己有的攻擊，是 client 直連模型必備的欄位級防護。第三，custom claims（&lt;code>request.auth.token.role&lt;/code>）適合放跨專案、低頻變動的全域角色；per-resource 的成員資格用 &lt;code>get()&lt;/code> 查 membership document，因為 claims 改動要等 token 刷新、不適合表達即時變動的權限。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore</a> overview 的 deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a>。規則語法以 <a href="https://firebase.google.com/docs/firestore/security/get-started">官方 Security Rules 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="問題情境授權沒有後端可以藏">問題情境：授權沒有後端可以藏</h2>
<p>自建後端的授權有一個天然的藏身處：所有讀寫都過 API，權限檢查寫在 service 層，前端拿不到的資料就是拿不到。Firestore 的 client 直連模型把這個藏身處拿掉了——前端 SDK 直接連資料庫，唯一擋在「任何人都能讀整個 collection」與「正確授權」之間的，就是 Security Rules。規則寫錯一條，等於把資料庫對公網敞開。</p>
<p>這個責任轉移最常見的引爆點是上線後的滲透測試或 bug bounty：報告指出「未登入就能用 REST API 拉出整張 <code>users</code> collection」。根因幾乎都是同一類——開發期為了方便把規則設成 <code>allow read, write: if true</code>，上線忘了收。Firestore 的規則是控制面的全部，這篇處理它的求值模型、如何把它寫成可測試的程式碼、以及它撐不住時的退場路線。</p>
<h2 id="核心概念規則的求值模型">核心概念：規則的求值模型</h2>
<p>Firestore Security Rules 是一套宣告式 DSL，掛在 <code>match</code> path 上、對每個讀寫請求求值。理解它要抓住四個跟後端授權不同的點：</p>
<p><strong>規則不是 filter，是 allow/deny 判定</strong>。一條 <code>allow read: if &lt;condition&gt;</code> 不會「只回傳符合條件的 document」——它是對「這次請求能不能執行」的布林判定。query 若可能讀到任何不符合規則的 document，整個 query 被拒絕，不是默默過濾。這逼著 client 的 query 必須自帶與規則一致的條件（例如 <code>where('ownerId', '==', uid)</code>），規則才放行。</p>
<p><strong>規則預設拒絕</strong>。沒有 <code>match</code> 命中的 path 一律拒絕。<code>rules_version = '2'</code> 下，<code>match /{document=**}</code> 遞迴匹配所有 subcollection，要小心別用一條寬鬆的遞迴規則蓋掉底下該嚴格的 path。</p>
<p><strong>請求脈絡來自 <code>request</code> 與 <code>resource</code></strong>。<code>request.auth</code> 是已驗證的身分（<code>request.auth.uid</code>、<code>request.auth.token</code> 的 custom claims）；<code>request.resource.data</code> 是寫入後的 document 狀態；<code>resource.data</code> 是寫入前的既有狀態。授權與資料驗證都在這幾個物件上展開。</p>
<p><strong>跨 document 查詢用 <code>get()</code> / <code>exists()</code></strong>。判斷「這個 user 是不是這個 project 的成員」要去讀另一份 document，用 <code>get(/databases/$(database)/documents/projects/$(pid)/members/$(uid))</code>。每個 <code>get()</code> 是一次額外讀取、計入計費，也有每請求次數上限（規則內 document access 有上限，設計時要省著用）。</p>
<p>基本骨架：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">rules_version</span> <span class="o">=</span> <span class="s1">&#39;2&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">service</span> <span class="nx">cloud</span><span class="p">.</span><span class="nx">firestore</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">match</span> <span class="o">/</span><span class="nx">databases</span><span class="o">/</span><span class="p">{</span><span class="nx">database</span><span class="p">}</span><span class="o">/</span><span class="nx">documents</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">match</span> <span class="o">/</span><span class="nx">notes</span><span class="o">/</span><span class="p">{</span><span class="nx">noteId</span><span class="p">}</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">allow</span> <span class="nx">read</span><span class="o">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span> <span class="o">!=</span> <span class="kc">null</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                  <span class="o">&amp;&amp;</span> <span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">ownerId</span> <span class="o">==</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">allow</span> <span class="nx">create</span><span class="o">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span> <span class="o">!=</span> <span class="kc">null</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                    <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">ownerId</span> <span class="o">==</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nx">allow</span> <span class="nx">update</span><span class="p">,</span> <span class="k">delete</span><span class="o">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span> <span class="o">!=</span> <span class="kc">null</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                            <span class="o">&amp;&amp;</span> <span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">ownerId</span> <span class="o">==</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>read</code> 用 <code>resource.data</code>（既有 document），<code>create</code> 用 <code>request.resource.data</code>（沒有既有狀態），<code>update</code> 兩者都要看——把 <code>read</code> / <code>create</code> / <code>update</code> / <code>delete</code> 分開是建模的起點，混成一條 <code>allow read, write</code> 是後面所有漏洞的源頭。</p>
<h2 id="配置把授權拆成可組合-function">配置：把授權拆成可組合 function</h2>
<p>規則一旦超過幾個 collection，inline 的 <code>if</code> 條件會重複且難讀。把授權判斷抽成 <code>function</code>，讓每條規則讀起來像在描述意圖，是讓規則可維護的核心手段：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">rules_version</span> <span class="o">=</span> <span class="s1">&#39;2&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">service</span> <span class="nx">cloud</span><span class="p">.</span><span class="nx">firestore</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">match</span> <span class="o">/</span><span class="nx">databases</span><span class="o">/</span><span class="p">{</span><span class="nx">database</span><span class="p">}</span><span class="o">/</span><span class="nx">documents</span> <span class="p">{</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="kd">function</span> <span class="nx">isSignedIn</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="k">return</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">}</span>
</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="kd">function</span> <span class="nx">isOwner</span><span class="p">(</span><span class="nx">docData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="k">return</span> <span class="nx">isSignedIn</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">docData</span><span class="p">.</span><span class="nx">ownerId</span> <span class="o">==</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <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="kd">function</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="k">return</span> <span class="nx">isSignedIn</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="o">&amp;&amp;</span> <span class="nx">exists</span><span class="p">(</span><span class="sr">/databases/</span><span class="nx">$</span><span class="p">(</span><span class="nx">database</span><span class="p">)</span><span class="o">/</span><span class="nx">documents</span><span class="o">/</span><span class="nx">projects</span><span class="o">/</span><span class="nx">$</span><span class="p">(</span><span class="nx">projectId</span><span class="p">)</span><span class="o">/</span><span class="nx">members</span><span class="o">/</span><span class="nx">$</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</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="kd">function</span> <span class="nx">hasRole</span><span class="p">(</span><span class="nx">projectId</span><span class="p">,</span> <span class="nx">role</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="k">return</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="o">&amp;&amp;</span> <span class="nx">get</span><span class="p">(</span><span class="sr">/databases/</span><span class="nx">$</span><span class="p">(</span><span class="nx">database</span><span class="p">)</span><span class="o">/</span><span class="nx">documents</span><span class="o">/</span><span class="nx">projects</span><span class="o">/</span><span class="nx">$</span><span class="p">(</span><span class="nx">projectId</span><span class="p">)</span><span class="o">/</span><span class="nx">members</span><span class="o">/</span><span class="nx">$</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">)).</span><span class="nx">data</span><span class="p">.</span><span class="nx">role</span> <span class="o">==</span> <span class="nx">role</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="c1">// 寫入時欄位白名單：禁止 client 竄改 ownerId / createdAt
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span>    <span class="kd">function</span> <span class="nx">fieldsUnchanged</span><span class="p">(</span><span class="nx">fields</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">      <span class="k">return</span> <span class="nx">request</span><span class="p">.</span><span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">diff</span><span class="p">(</span><span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">).</span><span class="nx">affectedKeys</span><span class="p">().</span><span class="nx">hasOnly</span><span class="p">(</span><span class="nx">fields</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="nx">match</span> <span class="o">/</span><span class="nx">projects</span><span class="o">/</span><span class="p">{</span><span class="nx">projectId</span><span class="p">}</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">      <span class="nx">allow</span> <span class="nx">read</span><span class="o">:</span> <span class="k">if</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">      <span class="nx">allow</span> <span class="nx">update</span><span class="o">:</span> <span class="k">if</span> <span class="nx">hasRole</span><span class="p">(</span><span class="nx">projectId</span><span class="p">,</span> <span class="s1">&#39;admin&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">                    <span class="o">&amp;&amp;</span> <span class="nx">fieldsUnchanged</span><span class="p">([</span><span class="s1">&#39;name&#39;</span><span class="p">,</span> <span class="s1">&#39;description&#39;</span><span class="p">,</span> <span class="s1">&#39;updatedAt&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">      <span class="nx">allow</span> <span class="k">delete</span><span class="o">:</span> <span class="k">if</span> <span class="nx">hasRole</span><span class="p">(</span><span class="nx">projectId</span><span class="p">,</span> <span class="s1">&#39;owner&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl">      <span class="nx">match</span> <span class="o">/</span><span class="nx">tasks</span><span class="o">/</span><span class="p">{</span><span class="nx">taskId</span><span class="p">}</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="nx">allow</span> <span class="nx">read</span><span class="o">:</span> <span class="k">if</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="nx">allow</span> <span class="nx">create</span><span class="o">:</span> <span class="k">if</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">                      <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">resource</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">createdBy</span> <span class="o">==</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="nx">allow</span> <span class="nx">update</span><span class="p">,</span> <span class="k">delete</span><span class="o">:</span> <span class="k">if</span> <span class="nx">isProjectMember</span><span class="p">(</span><span class="nx">projectId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡有三個建模手段值得展開。第一，<code>isProjectMember</code> / <code>hasRole</code> 把「成員資格」與「角色」的判斷集中成單一定義，授權邏輯改一處全站生效，避免同一條規則散落在十個 collection。第二，<code>fieldsUnchanged</code> 用 <code>diff().affectedKeys().hasOnly()</code> 把「這次 update 只准動哪些欄位」寫成白名單——這擋掉 client 直接改 <code>ownerId</code> 把別人的資料佔為己有的攻擊，是 client 直連模型必備的欄位級防護。第三，custom claims（<code>request.auth.token.role</code>）適合放跨專案、低頻變動的全域角色；per-resource 的成員資格用 <code>get()</code> 查 membership document，因為 claims 改動要等 token 刷新、不適合表達即時變動的權限。</p>
<h2 id="配置用-emulator-把規則寫成單元測試">配置：用 emulator 把規則寫成單元測試</h2>
<p>規則是安全邊界，改一條就要驗證沒開新洞——這要求規則像程式碼一樣有測試。Firebase Emulator + <code>@firebase/rules-unit-testing</code> 讓規則在本地用真實求值引擎跑斷言，不必碰雲端：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// rules.test.js — 用 Jest / Mocha 跑
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">initializeTestEnvironment</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">assertFails</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">assertSucceeds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@firebase/rules-unit-testing&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">setDoc</span><span class="p">,</span> <span class="nx">getDoc</span><span class="p">,</span> <span class="nx">doc</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;firebase/firestore&#39;</span><span class="p">);</span>
</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="kd">let</span> <span class="nx">testEnv</span><span class="p">;</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="nx">beforeAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">testEnv</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">initializeTestEnvironment</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">projectId</span><span class="o">:</span> <span class="s1">&#39;demo-notes&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">firestore</span><span class="o">:</span> <span class="p">{</span> <span class="nx">rules</span><span class="o">:</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;fs&#39;</span><span class="p">).</span><span class="nx">readFileSync</span><span class="p">(</span><span class="s1">&#39;firestore.rules&#39;</span><span class="p">,</span> <span class="s1">&#39;utf8&#39;</span><span class="p">)</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">});</span>
</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="nx">afterAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">cleanup</span><span class="p">();</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">clearFirestore</span><span class="p">();</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;owner 能讀自己的 note&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="c1">// 用 admin context 預先種一筆資料、繞過規則
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span>  <span class="kr">await</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">withSecurityRulesDisabled</span><span class="p">(</span><span class="kr">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="kr">await</span> <span class="nx">setDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">firestore</span><span class="p">(),</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">),</span> <span class="p">{</span> <span class="nx">ownerId</span><span class="o">:</span> <span class="s1">&#39;alice&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="kr">const</span> <span class="nx">alice</span> <span class="o">=</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">authenticatedContext</span><span class="p">(</span><span class="s1">&#39;alice&#39;</span><span class="p">).</span><span class="nx">firestore</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="kr">await</span> <span class="nx">assertSucceeds</span><span class="p">(</span><span class="nx">getDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">alice</span><span class="p">,</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;非 owner 不能讀別人的 note&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="kr">await</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">withSecurityRulesDisabled</span><span class="p">(</span><span class="kr">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="kr">await</span> <span class="nx">setDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">firestore</span><span class="p">(),</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">),</span> <span class="p">{</span> <span class="nx">ownerId</span><span class="o">:</span> <span class="s1">&#39;alice&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">  <span class="kr">const</span> <span class="nx">bob</span> <span class="o">=</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">authenticatedContext</span><span class="p">(</span><span class="s1">&#39;bob&#39;</span><span class="p">).</span><span class="nx">firestore</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">  <span class="kr">await</span> <span class="nx">assertFails</span><span class="p">(</span><span class="nx">getDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">bob</span><span class="p">,</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;未登入完全擋下&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="kr">const</span> <span class="nx">anon</span> <span class="o">=</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">unauthenticatedContext</span><span class="p">().</span><span class="nx">firestore</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="kr">await</span> <span class="nx">assertFails</span><span class="p">(</span><span class="nx">getDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">anon</span><span class="p">,</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;client 不能竄改 ownerId&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">  <span class="kr">await</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">withSecurityRulesDisabled</span><span class="p">(</span><span class="kr">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="kr">await</span> <span class="nx">setDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">firestore</span><span class="p">(),</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">),</span> <span class="p">{</span> <span class="nx">ownerId</span><span class="o">:</span> <span class="s1">&#39;alice&#39;</span><span class="p">,</span> <span class="nx">text</span><span class="o">:</span> <span class="s1">&#39;hi&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="kr">const</span> <span class="nx">alice</span> <span class="o">=</span> <span class="nx">testEnv</span><span class="p">.</span><span class="nx">authenticatedContext</span><span class="p">(</span><span class="s1">&#39;alice&#39;</span><span class="p">).</span><span class="nx">firestore</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">  <span class="kr">await</span> <span class="nx">assertFails</span><span class="p">(</span><span class="nx">setDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">alice</span><span class="p">,</span> <span class="s1">&#39;notes/n1&#39;</span><span class="p">),</span> <span class="p">{</span> <span class="nx">ownerId</span><span class="o">:</span> <span class="s1">&#39;bob&#39;</span><span class="p">,</span> <span class="nx">text</span><span class="o">:</span> <span class="s1">&#39;hi&#39;</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>啟動方式 <code>firebase emulators:exec --only firestore &quot;npm test&quot;</code>，讓測試在 CI 跑。測試要覆蓋的不只是 happy path——每條規則至少要有「正向放行」「越權拒絕」「未登入拒絕」「欄位竄改拒絕」四類斷言。<code>assertFails</code> 比 <code>assertSucceeds</code> 更重要：它證明的是「該擋的有擋住」，正是滲透測試會打的點。把這套測試接進 release gate，規則變更才有 evidence 可交（對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>）。</p>
<h2 id="故障演練五個把規則寫成漏洞的-production-踩坑">故障演練：五個把規則寫成漏洞的 production 踩坑</h2>
<h4 id="case-1allow-read-write-if-true-上線沒收">Case 1：<code>allow read, write: if true</code> 上線沒收</h4>
<p>開發期為了快，把規則開全放，上線忘改。任何人用公開的 project config（前端 bundle 裡就有）就能 REST 拉整個資料庫。修法：規則預設從 deny 起手，開發期的寬鬆規則進不了 main branch；CI 跑一條 lint 掃 <code>if true</code>，命中即 fail。這是 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a> 越權查詢路徑的最便宜目標。</p>
<h4 id="case-2read-沒拆-get-與-list">Case 2：<code>read</code> 沒拆 <code>get</code> 與 <code>list</code></h4>
<p><code>allow read</code> 同時涵蓋讀單一 document（<code>get</code>）與查整個 collection（<code>list</code>）。規則只想開「讀自己那筆」，卻因為沒拆 <code>list</code>，讓 client 能 <code>list</code> 整個 collection 撈別人的資料。修法：對 collection-level query 敏感的 path，把 <code>read</code> 拆成 <code>allow get</code> 與 <code>allow list</code>，<code>list</code> 條件更嚴或直接關閉、改走後端彙整。</p>
<h4 id="case-3信任-requestresourcedata-的內容沒驗證">Case 3：信任 <code>request.resource.data</code> 的內容沒驗證</h4>
<p><code>create</code> 規則只檢查 <code>request.auth != null</code>，沒驗證寫入內容。client 自己塞 <code>role: 'admin'</code> 或 <code>balance: 999999</code> 進 document。修法：寫入規則要驗證關鍵欄位的值與型別（<code>request.resource.data.role == 'member'</code>、<code>request.resource.data.amount is int</code>），敏感欄位（角色、金額、狀態）的權威值不該由 client 寫入、改由 Cloud Function 或後端寫。</p>
<h4 id="case-4遞迴-match-document-蓋掉嚴格規則">Case 4：遞迴 <code>match /{document=**}</code> 蓋掉嚴格規則</h4>
<p>頂層放一條 <code>match /{document=**} { allow read: if isSignedIn(); }</code> 圖方便，結果它遞迴命中所有 subcollection，把底下本來該按成員資格嚴格控管的 <code>members</code> collection 也開成「登入即可讀」。修法：避免寬鬆的遞迴萬用規則；授權顆粒不同的 path 各自寫明確 <code>match</code>。</p>
<h4 id="case-5規則複雜到沒人能-review">Case 5：規則複雜到沒人能 review</h4>
<p>授權邏輯長到幾百行、巢狀 <code>get()</code> 互相依賴，改一條沒人敢保證沒開新洞、也沒有測試。修法：這是規則撐不住的訊號（見下方邊界段）——超過這個複雜度，授權該拉回後端中介層，而不是繼續在 DSL 裡長。</p>
<h2 id="容量與觀測get-計費與規則複雜度上限">容量與觀測：<code>get()</code> 計費與規則複雜度上限</h2>
<p>規則內的每個 <code>get()</code> / <code>exists()</code> 是一次 document 讀取，計入計費，且單次請求的 document access 有數量上限（以 <a href="https://firebase.google.com/docs/firestore/security/rules-conditions">官方限制</a> 為準）。高頻讀取路徑若每次都 <code>get()</code> 查 membership，成本與延遲都會浮現。優化方向有二：把低頻變動的權限（全域角色）放進 custom claims，從 token 直接讀、零額外 document access；把成員資格設計成可由 document path 直接判斷（例如 membership document 的 ID 就是 uid，用 <code>exists()</code> 而非 <code>get()</code> 撈整份）。</p>
<p>觀測上，授權問題不會在規則層留下豐富 log——被拒的請求 client 端收到 <code>permission-denied</code>。要把這類錯誤從 client 回報、或在關鍵寫入路徑改走 Cloud Function 以取得 server 端 audit log，接回 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7.7 稽核軌跡</a>。規則本身的變更要進版本控制、每次 deploy 留 diff，授權變更才可回溯。</p>
<h2 id="邊界與整合規則撐不住時把授權拉回後端">邊界與整合：規則撐不住時把授權拉回後端</h2>
<p>Security Rules 適合表達「資源的擁有者與成員能做什麼」這類 resource-scoped 授權。它撐不住的訊號很明確：授權依賴跨多個 document 的複雜聚合判斷、需要呼叫外部系統、規則複雜到無法 review、或業務規則頻繁變動到規則 deploy 跟不上。撞到這些訊號時，正確的動作是把該塊授權移出 client 直連路徑，而非把規則寫得更巧：</p>
<ul>
<li><strong>敏感寫入改走 Cloud Function / 後端 API</strong>：金額、狀態機轉換、跨實體一致性的寫入，由 server 端驗證後以 admin 權限寫入，規則對 client 直接關閉這些 path 的寫入</li>
<li><strong>複雜授權整體下沉</strong>：當規則複雜度本身成為風險，這是 <a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a> playbook 裡「授權控制面失控」這面牆——把授權拉回後端中介層是遷移的 driver 之一</li>
</ul>
<p>判讀的單位仍是逐路徑：簡單的 owner-scoped 資料留在規則 + client 直連，複雜或敏感的部分走後端。不是非此即彼。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上層：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a>（服務定位與查詢邊界）</li>
<li>安全驗證：<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a>（越權查詢與資料外洩路徑）</li>
<li>遷移 driver：<a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a>（授權控制面失控的退場）</li>
<li>發布證據：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（規則測試接進 gate）</li>
<li>官方：<a href="https://firebase.google.com/docs/firestore/security/get-started">Security Rules get started</a>、<a href="https://firebase.google.com/docs/rules/unit-tests">Rules unit testing</a>、<a href="https://firebase.google.com/docs/firestore/security/rules-conditions">Rules conditions limits</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka Replication、ISR 與 exactly-once：從 acks 到端到端不重不漏</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/replication-isr-exactly-once/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/replication-isr-exactly-once/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a> overview「Replication 與 exactly-once 升級」段的 implementation-layer deep article。Overview 已給出 partition / replication 的選型定位、本文展開 &lt;em>寫入承諾&lt;/em> 跟 &lt;em>處理語義&lt;/em> 兩條獨立軸線怎麼設、邊界在哪、成本是什麼。對應反例 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 Queue 語義誤配&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="寫入承諾與處理語義是兩條獨立軸線">寫入承諾與處理語義是兩條獨立軸線&lt;/h2>
&lt;p>Kafka 的可靠性拆成兩個彼此正交的問題、混在一起談是多數誤配的起點。第一條軸線是 &lt;em>寫入承諾&lt;/em>：一筆訊息寫進 broker 後、在多少 replica 落地才算「成功」、broker 掛掉時這筆訊息會不會消失。這條軸線由 replication factor、ISR、&lt;code>acks&lt;/code> 與 &lt;code>min.insync.replicas&lt;/code> 共同決定、屬於 broker 端的耐久性保證。第二條軸線是 &lt;em>處理語義&lt;/em>：同一筆訊息在 producer 重送、consumer 重啟、partition rebalance 等情境下、會不會被寫進去兩次或被處理兩次。這條軸線由 producer idempotence、transaction 與 consumer 端的 commit 設計決定、屬於端到端的正確性保證。&lt;/p>
&lt;p>兩條軸線可以獨立調整：可以有「寫入承諾很強但處理語義是 at-least-once」的配置（acks=all + 非冪等 consumer）、也可以有「寫入承諾較弱但已開冪等」的配置。把 exactly-once 當成單一開關去找、是因為沒看出這兩條軸線存在。本文先講第一條（replication / ISR / acks）、再講第二條（idempotence / transaction）、最後談兩者疊起來能達成什麼、達不成什麼。&lt;/p>
&lt;p>這個拆分對映 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 兩張知識卡：前者描述 broker 承諾的送達次數、後者描述處理端怎麼讓「送達多次」不等於「生效多次」。&lt;/p>
&lt;h2 id="isr誰算跟得上的副本">ISR：誰算「跟得上」的副本&lt;/h2>
&lt;p>ISR（in-sync replica、同步副本集）是一個 partition 當前「跟得上 leader」的 replica 集合、是 Kafka 把 replication factor 這個 &lt;em>靜態配置&lt;/em> 轉成 &lt;em>動態保證&lt;/em> 的關鍵概念。Replication factor = 3 只說明這個 partition 有 3 份 replica；但任一時刻真正跟得上 leader 的可能只有 2 份或 1 份。ISR 就是這個「當前實際同步」的集合、寫入承諾的判斷都基於 ISR、不是基於 replication factor。&lt;/p>
&lt;p>一個 follower 留在 ISR 內的條件是：它在 &lt;code>replica.lag.time.max.ms&lt;/code>（預設 30 秒）內持續向 leader 拉取資料、且追上 leader 的 log end offset。當 follower 因為 broker 慢、網路抖動、GC 停頓或 disk 壓力而落後超過這個時間窗、leader 會把它移出 ISR — 這就是 ISR shrink（收縮）。當它恢復、重新追上、再被加回 ISR — 這是 ISR expand（擴張）。&lt;/p>
&lt;p>ISR 收縮本身不是故障、是 Kafka 對「這個 follower 暫時不可信」的誠實表態。真正的風險在於：ISR 收縮到某個程度後、&lt;code>acks=all&lt;/code> 的寫入承諾會無法滿足 &lt;code>min.insync.replicas&lt;/code> 而開始拒絕寫入。下一段的 acks 取捨直接建立在 ISR 這個概念上。&lt;/p>
&lt;p>實機看 ISR 的方式是 &lt;code>kafka-topics.sh --describe&lt;/code>、Isr 欄位列出當前同步的 broker id：&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"># RF=3、min.insync.replicas=2 的 topic、三 broker 都同步時&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">kafka-topics.sh --describe --topic repl-demo --bootstrap-server kafka1:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># Topic: repl-demo PartitionCount: 1 ReplicationFactor: 3 Configs: min.insync.replicas=2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Topic: repl-demo Partition: 0 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replicas 欄位是 &lt;em>配置上&lt;/em> 的 3 份副本、Isr 欄位是 &lt;em>當前實際同步&lt;/em> 的集合。兩者一致代表健康；Isr 比 Replicas 短代表有副本落後。日常巡檢用 &lt;code>kafka-topics.sh --describe --under-replicated-partitions&lt;/code> 直接列出 Isr 短於 Replicas 的 partition。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> overview「Replication 與 exactly-once 升級」段的 implementation-layer deep article。Overview 已給出 partition / replication 的選型定位、本文展開 <em>寫入承諾</em> 跟 <em>處理語義</em> 兩條獨立軸線怎麼設、邊界在哪、成本是什麼。對應反例 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 Queue 語義誤配</a>。</p></blockquote>
<h2 id="寫入承諾與處理語義是兩條獨立軸線">寫入承諾與處理語義是兩條獨立軸線</h2>
<p>Kafka 的可靠性拆成兩個彼此正交的問題、混在一起談是多數誤配的起點。第一條軸線是 <em>寫入承諾</em>：一筆訊息寫進 broker 後、在多少 replica 落地才算「成功」、broker 掛掉時這筆訊息會不會消失。這條軸線由 replication factor、ISR、<code>acks</code> 與 <code>min.insync.replicas</code> 共同決定、屬於 broker 端的耐久性保證。第二條軸線是 <em>處理語義</em>：同一筆訊息在 producer 重送、consumer 重啟、partition rebalance 等情境下、會不會被寫進去兩次或被處理兩次。這條軸線由 producer idempotence、transaction 與 consumer 端的 commit 設計決定、屬於端到端的正確性保證。</p>
<p>兩條軸線可以獨立調整：可以有「寫入承諾很強但處理語義是 at-least-once」的配置（acks=all + 非冪等 consumer）、也可以有「寫入承諾較弱但已開冪等」的配置。把 exactly-once 當成單一開關去找、是因為沒看出這兩條軸線存在。本文先講第一條（replication / ISR / acks）、再講第二條（idempotence / transaction）、最後談兩者疊起來能達成什麼、達不成什麼。</p>
<p>這個拆分對映 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a> 與 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 兩張知識卡：前者描述 broker 承諾的送達次數、後者描述處理端怎麼讓「送達多次」不等於「生效多次」。</p>
<h2 id="isr誰算跟得上的副本">ISR：誰算「跟得上」的副本</h2>
<p>ISR（in-sync replica、同步副本集）是一個 partition 當前「跟得上 leader」的 replica 集合、是 Kafka 把 replication factor 這個 <em>靜態配置</em> 轉成 <em>動態保證</em> 的關鍵概念。Replication factor = 3 只說明這個 partition 有 3 份 replica；但任一時刻真正跟得上 leader 的可能只有 2 份或 1 份。ISR 就是這個「當前實際同步」的集合、寫入承諾的判斷都基於 ISR、不是基於 replication factor。</p>
<p>一個 follower 留在 ISR 內的條件是：它在 <code>replica.lag.time.max.ms</code>（預設 30 秒）內持續向 leader 拉取資料、且追上 leader 的 log end offset。當 follower 因為 broker 慢、網路抖動、GC 停頓或 disk 壓力而落後超過這個時間窗、leader 會把它移出 ISR — 這就是 ISR shrink（收縮）。當它恢復、重新追上、再被加回 ISR — 這是 ISR expand（擴張）。</p>
<p>ISR 收縮本身不是故障、是 Kafka 對「這個 follower 暫時不可信」的誠實表態。真正的風險在於：ISR 收縮到某個程度後、<code>acks=all</code> 的寫入承諾會無法滿足 <code>min.insync.replicas</code> 而開始拒絕寫入。下一段的 acks 取捨直接建立在 ISR 這個概念上。</p>
<p>實機看 ISR 的方式是 <code>kafka-topics.sh --describe</code>、Isr 欄位列出當前同步的 broker id：</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"># RF=3、min.insync.replicas=2 的 topic、三 broker 都同步時</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kafka-topics.sh --describe --topic repl-demo --bootstrap-server kafka1:9092
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Topic: repl-demo  PartitionCount: 1  ReplicationFactor: 3  Configs: min.insync.replicas=2</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#   Topic: repl-demo  Partition: 0  Leader: 2  Replicas: 2,3,1  Isr: 2,3,1</span></span></span></code></pre></div><p>Replicas 欄位是 <em>配置上</em> 的 3 份副本、Isr 欄位是 <em>當前實際同步</em> 的集合。兩者一致代表健康；Isr 比 Replicas 短代表有副本落後。日常巡檢用 <code>kafka-topics.sh --describe --under-replicated-partitions</code> 直接列出 Isr 短於 Replicas 的 partition。</p>
<h2 id="acks-與-mininsyncreplicas寫入承諾的兩個旋鈕">acks 與 min.insync.replicas：寫入承諾的兩個旋鈕</h2>
<p>寫入承諾由 producer 端的 <code>acks</code> 跟 broker / topic 端的 <code>min.insync.replicas</code> 共同決定、兩者必須一起設才有意義。<code>acks</code> 決定 producer 在收到「成功」回應前、要等多少 replica 確認；<code>min.insync.replicas</code> 決定 broker 在 ISR 不足時是否拒絕寫入。前者是 producer 的等待策略、後者是 broker 的拒絕底線。</p>
<p><code>acks</code> 三個值對應遞增的耐久性與遞增的延遲成本：</p>
<table>
  <thead>
      <tr>
          <th>acks 值</th>
          <th>承諾</th>
          <th>資料風險</th>
          <th>延遲</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0</td>
          <td>不等任何確認、送出即視為成功</td>
          <td>leader 沒收到也不知道、broker 掛掉直接丟</td>
          <td>最低</td>
      </tr>
      <tr>
          <td>1</td>
          <td>leader 寫入本地 log 即回成功</td>
          <td>leader 確認後、follower 同步前掛掉、這筆訊息遺失</td>
          <td>中</td>
      </tr>
      <tr>
          <td>all</td>
          <td>ISR 內所有 replica 都確認才回成功</td>
          <td>ISR 內任一存活即不丟；ISR 不足 min.insync 時拒絕寫入</td>
          <td>最高</td>
      </tr>
  </tbody>
</table>
<p><code>acks=0</code> 適用「丟一兩筆無所謂」的場景、例如高頻 metric 上報、log shipping 的非關鍵層。它把網路往返成本壓到最低、代價是 producer 完全不知道 broker 有沒有收到。任何牽涉金流、訂單、狀態變更的訊息都不該用 acks=0。</p>
<p><code>acks=1</code> 是一個容易被誤以為安全的中間值。它只等 leader 寫入本地、不等 follower 同步。多數時候運作正常、但存在一個明確的資料遺失窗口：leader 回了成功、follower 還沒拉到這筆訊息、此時 leader 所在 broker 崩潰、新 leader 從 follower 中選出 — 那筆「已回成功」的訊息在新 leader 上不存在、producer 卻以為寫成功了。這個窗口在正常運行時很窄、但在 broker 滾動重啟、硬體故障、AZ 中斷時會被放大。</p>
<p><code>acks=all</code> 是耐久性配置的正解、但只有搭配 <code>min.insync.replicas ≥ 2</code> 才完整。單獨設 acks=all、若 <code>min.insync.replicas=1</code>、那麼當 ISR 收縮到只剩 leader 一份時、acks=all 等同 acks=1 — 「所有 ISR 確認」這個條件在 ISR 只剩 1 份時形同虛設。<code>min.insync.replicas=2</code> 補上這個漏洞：它要求 ISR 至少有 2 份才接受 acks=all 寫入、否則直接拒絕、把「靜默遺失」轉成「明確拒絕」。</p>
<p><code>min.insync.replicas</code> 是 topic-level 可動態調整的配置、不需重啟 broker：</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"># 動態調整單一 topic 的 min.insync.replicas</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kafka-configs.sh --alter --topic repl-demo <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --add-config min.insync.replicas<span class="o">=</span><span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --bootstrap-server kafka1:9092
</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"># 查當前值、synonyms 會顯示 topic override 蓋過 broker default</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">kafka-configs.sh --describe --topic repl-demo --bootstrap-server kafka1:9092
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># min.insync.replicas=2 synonyms={DYNAMIC_TOPIC_CONFIG:min.insync.replicas=2,</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">#   DYNAMIC_DEFAULT_BROKER_CONFIG:min.insync.replicas=1, DEFAULT_CONFIG:min.insync.replicas=1}</span></span></span></code></pre></div><p>RF=3 + acks=all + min.insync.replicas=2 是業界對「不能丟資料」topic 的標準三件組：3 份副本提供冗餘、acks=all 要求同步確認、min.insync=2 在容忍一台 broker 掛掉的同時仍保證每筆寫入落在至少兩份 replica。容忍度的算術是 <code>RF - min.insync.replicas</code>：3 - 2 = 1、代表可以掉一台 broker 仍正常寫入、掉兩台則寫入被拒（但已寫入的資料不丟）。</p>
<h2 id="producer-idempotence去掉重送造成的重複">Producer idempotence：去掉重送造成的重複</h2>
<p>Producer idempotence（冪等生產者、<code>enable.idempotence=true</code>）解決的是 <em>producer 重送</em> 造成的 broker 端重複。它讓「producer 因為沒收到 ack 而重送同一筆訊息」這件事、在 broker 端被去重、不會寫進兩筆。這是處理語義軸線的第一塊、獨立於前面的寫入承諾。</p>
<p>問題的根源是：producer 送出訊息後、若因網路超時沒收到 broker 的 ack、它無法分辨是「訊息沒送到」還是「訊息送到了但 ack 在回程丟了」。預設行為是重送。在沒有冪等保護時、若實際是後者、broker 就收到兩筆相同訊息、partition 裡出現重複。</p>
<p>冪等機制的做法是給每個 producer 分配一個 producer ID（PID）、並為每個 partition 維護一個遞增的 sequence number。Broker 記住每個 (PID, partition) 已接受的最大 sequence；重送的訊息帶相同 sequence、broker 認出是重複、直接丟棄並回成功。這個保證的範圍是 <em>單一 producer session 內、單一 partition</em> 的精確一次寫入。</p>
<p>開啟方式是 producer 端設 <code>enable.idempotence=true</code>。在較新版 Kafka 這已是預設值、且它會隱含要求 <code>acks=all</code>、<code>retries &gt; 0</code>、<code>max.in.flight.requests.per.connection ≤ 5</code> — 因為冪等去重依賴這些前提。冪等的成本極低（broker 多維護 PID/sequence 的少量 metadata）、幾乎沒有理由關閉。</p>
<p>需要明確的邊界是：冪等只覆蓋 <em>同一個 producer session</em>。Producer 重啟後拿到新的 PID、broker 無法把新舊 session 的訊息關聯起來。跨 session 的去重、以及「寫多個 partition 要嘛全成功要嘛全失敗」的需求、要靠下一段的 transaction。</p>
<h2 id="kafka-transaction-與-read_committed跨-partition-的原子寫入">Kafka transaction 與 read_committed：跨 partition 的原子寫入</h2>
<p>Kafka transaction（交易）解決的是 <em>跨多個 partition 的原子寫入</em> 與 <em>consume-process-produce 的原子提交</em>。它讓一組寫入（可能跨多個 topic / partition）以及對應的 consumer offset commit、要嘛全部對下游可見、要嘛全部不可見。這是處理語義軸線的第二塊、建立在冪等之上。</p>
<p>典型場景是 stream processing 的 consume-process-produce 迴圈：consumer 讀入一批訊息、處理後產出結果寫到另一個 topic、然後 commit 讀取進度。若這三步不是原子的、崩潰時可能出現「結果已產出但 offset 沒 commit」（重啟後重複處理、重複產出）或「offset 已 commit 但結果沒寫成功」（訊息遺失）。Transaction 把「產出結果」跟「commit offset」綁成一個原子操作、消除這個窗口。</p>
<p>啟用 transaction 需要 producer 設一個穩定的 <code>transactional.id</code>、並在程式碼中走完整的 transaction 生命週期：</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">producer.initTransactions()      // 向 transaction coordinator 註冊、fence 掉舊 session
</span></span><span class="line"><span class="ln">2</span><span class="cl">producer.beginTransaction()
</span></span><span class="line"><span class="ln">3</span><span class="cl">  producer.send(record1)          // 跨多個 topic/partition 的寫入
</span></span><span class="line"><span class="ln">4</span><span class="cl">  producer.send(record2)
</span></span><span class="line"><span class="ln">5</span><span class="cl">  producer.sendOffsetsToTransaction(offsets, groupMetadata)  // consumer 進度也納入交易
</span></span><span class="line"><span class="ln">6</span><span class="cl">producer.commitTransaction()      // 全部原子提交；失敗則 abortTransaction()</span></span></code></pre></div><p><code>transactional.id</code> 提供跨 session 的 fencing（隔離）：同一個 transactional.id 的新 producer 啟動時、coordinator 會 fence 掉舊的、避免「殭屍 producer」在崩潰後復活還繼續寫。這是冪等的 PID 機制做不到的跨 session 保證。</p>
<blockquote>
<p><strong>實機限制</strong>：<code>kafka-console-producer.sh</code> 帶 <code>--producer-property transactional.id=...</code> 不會自動呼叫 <code>initTransactions()</code>、會直接報 <code>IllegalStateException: Cannot add partition ... before completing a call to initTransactions</code>。完整 transaction 生命週期只能在 client code 中驗證、無法用 console 工具演示。本文的 transaction 行為描述依官方 producer API 語義、生命週期程式碼未經本地 client 實機跑通。</p></blockquote>
<p>Transaction 的另一半在 consumer 端：<code>isolation.level=read_committed</code>。預設的 <code>read_uncommitted</code> 會讀到尚未 commit、甚至最終被 abort 的 transactional 訊息。設成 <code>read_committed</code> 後、consumer 只會看到已 commit 的 transactional 訊息、abort 的訊息對它不可見、未 commit 的訊息會被擋在 last stable offset（LSO）之前等待。</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 以 read_committed 隔離級別讀取、只看已 commit 的 transactional 訊息</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kafka-console-consumer.sh --topic repl-demo --from-beginning <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --isolation-level read_committed <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --bootstrap-server kafka1:9092</span></span></code></pre></div><p>需要注意：對非 transactional 的普通訊息、read_committed 跟 read_uncommitted 行為相同 — 普通訊息一律可見。隔離級別只對 transactional 訊息產生差異。這也是為什麼若上游沒有任何 transactional producer、把 consumer 改成 read_committed 不會有任何可觀察的效果。</p>
<h2 id="端到端-exactly-once-的邊界與成本">端到端 exactly-once 的邊界與成本</h2>
<p>端到端 exactly-once 的意思是：訊息從 producer 到 consumer 處理結果、整條路徑上「不重不漏」。它由前面所有零件疊出來、但有明確的適用邊界、不是萬用保證。</p>
<p>Kafka 原生能提供 exactly-once 的範圍是 <em>Kafka-to-Kafka 的封閉迴圈</em>：consume from Kafka、process、produce to Kafka、commit offset、整個用 transaction 綁定。Kafka Streams 框架把這套封裝成 <code>processing.guarantee=exactly_once_v2</code> 一個配置、底層就是 transaction + 冪等 + read_committed 的組合。在這個封閉迴圈內、exactly-once 是真實成立的。</p>
<p>邊界出現在 <em>離開 Kafka 的那一刻</em>。當處理結果要寫進外部系統（資料庫、HTTP API、第三方服務、寄信、扣款）、Kafka 的 transaction 管不到外部系統的提交。一筆訊息「已扣款但 offset commit 前崩潰」這種跨系統不一致、Kafka transaction 無法消除 — 它只保證 Kafka 內部的原子性。跨系統的 exactly-once 要靠外部系統自己的冪等鍵（idempotency key）、或 outbox pattern、或兩階段提交、由應用層補上、不是 Kafka 送的。</p>
<p>成本方面、exactly-once 不是免費的耐久性升級：</p>
<table>
  <thead>
      <tr>
          <th>成本維度</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐</td>
          <td>transaction 的 begin/commit 與 coordinator 往返增加 per-batch overhead、吞吐下降</td>
      </tr>
      <tr>
          <td>延遲</td>
          <td>read_committed 要等 LSO 推進、consumer 端引入額外延遲</td>
      </tr>
      <tr>
          <td>複雜度</td>
          <td>producer 要管 transaction 生命週期、abort 路徑、fencing；錯誤處理比 fire-forget 重</td>
      </tr>
      <tr>
          <td>coordinator 壓力</td>
          <td>transaction coordinator 與 <code>__transaction_state</code> topic 成為新的關鍵路徑與容量點</td>
      </tr>
  </tbody>
</table>
<p>務實的判斷是：先確認需求真的是 exactly-once、還是「at-least-once + 下游冪等」就夠。多數業務（包括金流）用 at-least-once 送達 + 下游用業務冪等鍵去重、就達到了「效果上不重複」、且吞吐與複雜度成本遠低於完整 transaction exactly-once。完整的 Kafka transaction exactly-once 留給 Kafka-to-Kafka 的 stream processing pipeline、那是它的甜蜜點。這個取捨對映 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a> 對「在哪一層放冪等」的判讀。</p>
<h2 id="故障演練">故障演練</h2>
<p>可靠性配置的價值在故障時才顯現。以下演練在 3-broker KRaft 叢集（RF=3、min.insync.replicas=2）上跑、用停 broker 製造 ISR 收縮、觀察各配置的真實行為。</p>
<h3 id="isr-收縮到低於-mininsyncreplicas-時-acksall-被拒">ISR 收縮到低於 min.insync.replicas 時 acks=all 被拒</h3>
<p><strong>演練</strong>：起 3-broker 叢集、建 RF=3 / min.insync.replicas=2 的 topic、初始 ISR = 三台全在。依序停掉兩個 follower broker、觀察 ISR 收縮、再用 acks=all produce。</p>
<p><strong>初始狀態</strong>（ISR 三份全在、acks=all 正常）：</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">Topic: repl-demo  Partition: 0  Leader: 2  Replicas: 2,3,1  Isr: 2,3,1
</span></span><span class="line"><span class="ln">2</span><span class="cl"># acks=all produce → exit=0</span></span></code></pre></div><p><strong>停一個 follower（broker 3）</strong>、ISR 收縮到 2 份、仍滿足 min.insync=2：</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">Topic: repl-demo  Partition: 0  Leader: 2  Replicas: 2,3,1  Isr: 2,1
</span></span><span class="line"><span class="ln">2</span><span class="cl"># acks=all produce → exit=0（ISR=2 仍 &gt;= min.insync=2、寫入接受）</span></span></code></pre></div><p><strong>再停一個 follower（broker 1）</strong>、ISR 收縮到只剩 leader 1 份、低於 min.insync=2：</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"># acks=all produce → broker 拒絕：
</span></span><span class="line"><span class="ln">2</span><span class="cl">[Producer] Got error produce response ... Error: NOT_ENOUGH_REPLICAS, retrying
</span></span><span class="line"><span class="ln">3</span><span class="cl">org.apache.kafka.common.errors.NotEnoughReplicasException:
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Messages are rejected since there are fewer in-sync replicas than required.</span></span></code></pre></div><p><strong>判讀</strong>：這正是 min.insync.replicas 的設計意圖在運作。ISR 不足時、broker 選擇 <em>明確拒絕寫入</em>（NOT_ENOUGH_REPLICAS）、而不是降級成 acks=1 默默接受。對 producer 而言、寫入失敗會觸發 retry、retry 耗盡後拋例外、上游應用感知到「現在寫不進去」、可以 fail-fast 或 backpressure — 而不是寫了一筆只在單一 broker 上、隨時可能隨那台 broker 一起消失的「假成功」訊息。把資料遺失轉成可觀測的寫入拒絕、是這個配置的全部目的。</p>
<p><strong>恢復</strong>：重啟兩個 broker、ISR 自動 expand 回三份、acks=all 恢復接受寫入：</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">Topic: repl-demo  Partition: 0  Leader: 2  Replicas: 2,3,1  Isr: 1,2,3</span></span></code></pre></div><blockquote>
<p>附帶觀察：在 KRaft 模式下、controller 也是 quorum（本演練三台都兼任 controller）。同時停掉兩台、controller quorum 失去多數、<code>kafka-topics.sh --describe</code> 對 metadata 的查詢會 timeout（DisconnectException）。production 叢集應把 controller 數量與 broker 故障域分開規劃、避免 broker 故障連帶打垮 metadata 平面。</p></blockquote>
<h3 id="unclean-leader-election-的取捨">Unclean leader election 的取捨</h3>
<p>當一個 partition 的所有 ISR replica 都不可用、只剩一個 <em>曾經落後、已被踢出 ISR</em> 的 replica 還活著、Kafka 面臨一個無法兩全的選擇。<code>unclean.leader.election.enable=false</code>（預設）會選擇 <em>不選 leader</em>：這個 partition 進入不可用狀態、拒絕讀寫、直到某個 ISR replica 恢復。<code>unclean.leader.election.enable=true</code> 會選擇 <em>把那個落後的 replica 提為 leader</em>：partition 立刻恢復可用、代價是那個 replica 上缺失的訊息（leader 掛掉前已 commit 但它還沒同步到的部分）永久遺失。</p>
<p><strong>判讀</strong>：這是一個 <em>可用性 vs 耐久性</em> 的直接取捨、沒有正確答案、只有對映業務的選擇。對金流、訂單、審計這類「丟一筆都不行」的 topic、保持 false、寧可 partition 短暫不可用也不接受靜默資料遺失。對 metric、log、可重算的衍生資料、開 true 換可用性、丟幾筆可接受。預設 false 是合理的安全預設、但要意識到它的代價是「所有 replica 都不在 ISR 時、partition 會卡住不可用」、這在多 broker 同時故障時會發生。</p>
<h3 id="idempotent-producer-對重送去重">Idempotent producer 對重送去重</h3>
<p><strong>演練</strong>：producer 開 <code>enable.idempotence=true</code>、acks=all、模擬 ack 丟失導致的重送。</p>
<p><strong>判讀</strong>：冪等開啟後、producer 因網路超時重送的訊息帶相同 (PID, partition, sequence)、broker 認出 sequence 重複、丟棄重送並回成功、partition 內不出現重複。實機上 <code>enable.idempotence=true</code> 的 produce 寫入正常（exit=0）、消費端讀回的訊息數等於實際送出的邏輯訊息數、重送不放大。要記住的邊界仍是：這只覆蓋單一 producer session；producer 重啟換 PID 後、跨 session 的重複要靠 transaction 或下游冪等鍵處理。</p>
<h3 id="transaction-中途失敗的-read_committed-隔離">Transaction 中途失敗的 read_committed 隔離</h3>
<p><strong>演練</strong>：transactional producer 在 beginTransaction 後寫入若干訊息、然後 abortTransaction（模擬處理中途失敗）；consumer 分別用 read_uncommitted 與 read_committed 讀取。</p>
<p><strong>判讀</strong>：read_committed 的 consumer 看不到被 abort 的訊息 — 中途失敗的 transaction 對它等於沒發生過、不會讀到「處理一半的髒資料」。read_uncommitted 的 consumer 則會讀到這些最終被 abort 的訊息、若據此處理就產生了不該發生的副作用。這是 transaction 隔離的核心價值：把「transaction 失敗」的可見性控制在 commit 邊界內。</p>
<blockquote>
<p>本段的 abort 行為依官方 transaction 語義描述。本地以 <code>kafka-console-consumer.sh --isolation-level read_committed</code> 驗證了隔離級別參數可用、且對已 commit 的普通訊息 read_committed 與 read_uncommitted 輸出一致（普通訊息一律可見、隔離級別只對 transactional 訊息產生差異）；完整的 begin/abort transaction 生命週期需 client code、未用 console 工具跑通。</p></blockquote>
<h2 id="capacity--cost">Capacity / cost</h2>
<p>各配置的容量與成本影響、決定它適用的規模與 topic 類別：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>吞吐 / 延遲影響</th>
          <th>適用</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>acks=0</td>
          <td>最低延遲、最高吞吐</td>
          <td>可丟的 metric / log shipping</td>
          <td>任何狀態變更類訊息不可用</td>
      </tr>
      <tr>
          <td>acks=1</td>
          <td>中等、單次往返</td>
          <td>容忍極少量遺失的衍生資料</td>
          <td>誤當安全選項、broker 故障窗口會遺失</td>
      </tr>
      <tr>
          <td>acks=all + min.insync=2 + RF=3</td>
          <td>延遲 +1 次跨 broker 往返、吞吐略降</td>
          <td>不能丟的業務訊息</td>
          <td>min.insync 沒設則 acks=all 在 ISR=1 時失效</td>
      </tr>
      <tr>
          <td>enable.idempotence=true</td>
          <td>幾乎無額外成本</td>
          <td>所有 producer 預設開</td>
          <td>只覆蓋單一 session</td>
      </tr>
      <tr>
          <td>transaction + read_committed</td>
          <td>begin/commit overhead、read 端 LSO 等待延遲</td>
          <td>Kafka-to-Kafka stream processing 封閉迴圈</td>
          <td>跨外部系統不成立、coordinator 成新關鍵路徑</td>
      </tr>
  </tbody>
</table>
<p>務實 default：</p>
<ul>
<li>業務 topic 一律 RF=3 + acks=all + min.insync.replicas=2、idempotence 預設開</li>
<li>容忍度算術 <code>RF - min.insync.replicas</code> 要 ≥ 1、否則單台 broker 維護就會中斷寫入</li>
<li>完整 transaction exactly-once 只給 Kafka-to-Kafka pipeline；跨系統用 at-least-once + 下游冪等鍵</li>
<li>unclean.leader.election 保持 false、除非該 topic 明確可丟資料換可用性</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-processing-recovery-semantics-對位">跟 processing-recovery-semantics 對位</h3>
<p>寫入承諾保證訊息留在 broker、但 <em>處理</em> 的不重不漏在 consumer 端。<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a> 展開 consumer 的 commit 時機、崩潰恢復的 replay 範圍、以及「冪等放在哪一層」的判讀 — 跟本文的 transaction exactly-once 邊界互補：本文界定 Kafka 能送什麼、那篇界定處理端怎麼接才不放大重複。</p>
<h3 id="跟-event-contract-replay-boundary-對位">跟 event-contract-replay-boundary 對位</h3>
<p>Exactly-once 的封閉迴圈假設訊息格式穩定、replay 可重現。<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event-contract-replay-boundary</a> 展開 schema 演進與 replay 邊界 — 當 transaction 提供的原子性遇上 schema 變更、replay 舊訊息的可重現性會受 contract 影響、是 exactly-once 在時間維度上的延伸限制。</p>
<h3 id="對應反例-3c9">對應反例 3.C9</h3>
<p><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 Queue 語義誤配</a> 是本文兩條軸線混淆的真實後果：broker 遷移後「名稱上相近的 delivery semantics」在失敗重播時產生不同結果、出現重複扣款與狀態漏更新。判讀路徑正是本文的拆分 — 先確認是寫入承諾（acks / ISR）還是處理語義（idempotence / commit 時機）出問題、不要用 queue depth 這種寫入承諾層的指標去判斷處理語義層的故障。</p>
<h3 id="對應案例-3c21-goldman-sachs-msk-遷移">對應案例 3.C21 Goldman Sachs MSK 遷移</h3>
<p><a href="/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/" data-link-title="3.C21 Goldman Sachs：MSK 遷移 with MirrorMaker 2" data-link-desc="Goldman Sachs Global Investment Research 從 on-prem Kafka 遷到 MSK、用 MM2 同步 topic/ACL/offset、atomic cutover 7 小時完成。">3.C21 Goldman Sachs MSK 遷移</a> 揭露遷移時可靠性配置的細節風險集中在 client 端的 timeout / flush / LB 配置、而非 broker 本身。本文的 acks=all 在 ISR 不足時拒絕寫入、若 client 端的 retry 與 timeout 沒對齊（如 flush timeout 太短）、會把「broker 正常的 backpressure」誤判成「遷移失敗」。可靠性配置與 client 容錯參數要一起驗證。</p>
<h3 id="下一步路由">下一步路由</h3>
<ul>
<li>上游概念：<a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 知識卡</li>
<li>同 vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka overview</a> 的 producer / consumer 設計段</li>
<li>下游能力：<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>、<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event-contract-replay-boundary</a>、<a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
<li>方法論：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS&lt;/a> overview 的 implementation-layer deep article。Overview 回答「NATS 該不該選、Core NATS vs JetStream 怎麼分」；要不要從 core NATS 跨進 JetStream 的決策入口見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/" data-link-title="NATS core 到 JetStream：fire-and-forget 在哪裡不夠、跨過去要付什麼" data-link-desc="Core NATS 的 fire-and-forget 在 consumer 重啟或 rolling deploy 時掉訊息——這不是 bug、是設計。需要訊息不丟就跨進 JetStream（persistence &amp;#43; at-least-once &amp;#43; redelivery）。本文展開 core 與 JetStream 的邊界、stream 與 consumer 的求值模型、實機驗證的 durable pull consumer、5 個把 JetStream consumer 寫成丟訊息與重投風暴的 production 踩坑">core 到 JetStream 的邊界&lt;/a>；本文回答「JetStream stream / consumer 的每個旋鈕怎麼設、設錯踩什麼坑、跨區拓樸怎麼鋪、多租戶怎麼隔離」。寫作結構依 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論&lt;/a> 的 6 段框架。&lt;/p>&lt;/blockquote>
&lt;h2 id="jetstream-把-fire-and-forget-升級成-durable-log">JetStream 把 fire-and-forget 升級成 durable log&lt;/h2>
&lt;p>JetStream 是 NATS 內建的持久化層、責任是把 Core NATS 的 fire-and-forget subject 轉成 append-only 的 durable stream、並讓 consumer 能 ack、重投、replay。Core NATS 的訊息一旦沒有 active subscriber 就消失；JetStream 把符合特定 subject 的訊息攔截下來寫進 stream、即使沒有任何 consumer 在線也會留存到 retention 上限。&lt;/p>
&lt;p>兩個概念要先分清楚、後面所有配置都掛在這個分界上。Stream 是 &lt;em>儲存&lt;/em> 責任：定義「哪些 subject 的訊息要存、存多久、存多少、存哪裡」。Consumer 是 &lt;em>投遞&lt;/em> 責任：定義「從 stream 的哪個位置開始讀、怎麼 ack、ack 不回來要不要重投、重投幾次」。同一個 stream 可以掛多個 consumer、各自有獨立的讀取游標跟重投狀態、互不影響。這個 stream / consumer 二分是 JetStream 跟 Kafka（topic / consumer group）對應、但跟 RabbitMQ（queue 本身就綁消費）不同的核心模型差異。&lt;/p>
&lt;p>本文用一個訂單事件流當主線：subject 設計成 &lt;code>orders.created.&amp;lt;region&amp;gt;&lt;/code>、stream 名 &lt;code>orders&lt;/code>、subject filter &lt;code>orders.&amp;gt;&lt;/code>。實機環境用單機 NATS server 加 &lt;code>-js&lt;/code>、CLI 用 &lt;code>natsio/nats-box&lt;/code> 容器；跨節點的 Cluster / quorum 段用 3 節點 docker compose 驗證、Supercluster / Leaf node 因拓樸複雜以 case 敘述加官方文件 caveat 標註。&lt;/p>
&lt;h2 id="stream-設計storageretentiondiscard容量上限">Stream 設計：storage、retention、discard、容量上限&lt;/h2>
&lt;p>Stream 的設計責任是回答四個彼此獨立的問題：訊息存在哪種介質、用什麼規則決定保留、超過上限時丟哪一端、上限本身設多大。這四個旋鈕組合錯了不會在建立時報錯、而是在 production 流量打進來才以丟訊息或塞爆 disk 的形式爆出來。&lt;/p>
&lt;h3 id="storagefile-vs-memory">Storage：file vs memory&lt;/h3>
&lt;p>Storage type 決定訊息寫在 disk 還是 RAM。&lt;code>file&lt;/code> storage 把 stream 寫進 disk、server 重啟後資料還在、是需要 durability 的事件流預設選擇；&lt;code>memory&lt;/code> storage 把 stream 放 RAM、吞吐跟延遲更好但 server 重啟即全失、適合短期 fan-out 或可重建的快取型資料。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> overview 的 implementation-layer deep article。Overview 回答「NATS 該不該選、Core NATS vs JetStream 怎麼分」；要不要從 core NATS 跨進 JetStream 的決策入口見 <a href="/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/" data-link-title="NATS core 到 JetStream：fire-and-forget 在哪裡不夠、跨過去要付什麼" data-link-desc="Core NATS 的 fire-and-forget 在 consumer 重啟或 rolling deploy 時掉訊息——這不是 bug、是設計。需要訊息不丟就跨進 JetStream（persistence &#43; at-least-once &#43; redelivery）。本文展開 core 與 JetStream 的邊界、stream 與 consumer 的求值模型、實機驗證的 durable pull consumer、5 個把 JetStream consumer 寫成丟訊息與重投風暴的 production 踩坑">core 到 JetStream 的邊界</a>；本文回答「JetStream stream / consumer 的每個旋鈕怎麼設、設錯踩什麼坑、跨區拓樸怎麼鋪、多租戶怎麼隔離」。寫作結構依 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a> 的 6 段框架。</p></blockquote>
<h2 id="jetstream-把-fire-and-forget-升級成-durable-log">JetStream 把 fire-and-forget 升級成 durable log</h2>
<p>JetStream 是 NATS 內建的持久化層、責任是把 Core NATS 的 fire-and-forget subject 轉成 append-only 的 durable stream、並讓 consumer 能 ack、重投、replay。Core NATS 的訊息一旦沒有 active subscriber 就消失；JetStream 把符合特定 subject 的訊息攔截下來寫進 stream、即使沒有任何 consumer 在線也會留存到 retention 上限。</p>
<p>兩個概念要先分清楚、後面所有配置都掛在這個分界上。Stream 是 <em>儲存</em> 責任：定義「哪些 subject 的訊息要存、存多久、存多少、存哪裡」。Consumer 是 <em>投遞</em> 責任：定義「從 stream 的哪個位置開始讀、怎麼 ack、ack 不回來要不要重投、重投幾次」。同一個 stream 可以掛多個 consumer、各自有獨立的讀取游標跟重投狀態、互不影響。這個 stream / consumer 二分是 JetStream 跟 Kafka（topic / consumer group）對應、但跟 RabbitMQ（queue 本身就綁消費）不同的核心模型差異。</p>
<p>本文用一個訂單事件流當主線：subject 設計成 <code>orders.created.&lt;region&gt;</code>、stream 名 <code>orders</code>、subject filter <code>orders.&gt;</code>。實機環境用單機 NATS server 加 <code>-js</code>、CLI 用 <code>natsio/nats-box</code> 容器；跨節點的 Cluster / quorum 段用 3 節點 docker compose 驗證、Supercluster / Leaf node 因拓樸複雜以 case 敘述加官方文件 caveat 標註。</p>
<h2 id="stream-設計storageretentiondiscard容量上限">Stream 設計：storage、retention、discard、容量上限</h2>
<p>Stream 的設計責任是回答四個彼此獨立的問題：訊息存在哪種介質、用什麼規則決定保留、超過上限時丟哪一端、上限本身設多大。這四個旋鈕組合錯了不會在建立時報錯、而是在 production 流量打進來才以丟訊息或塞爆 disk 的形式爆出來。</p>
<h3 id="storagefile-vs-memory">Storage：file vs memory</h3>
<p>Storage type 決定訊息寫在 disk 還是 RAM。<code>file</code> storage 把 stream 寫進 disk、server 重啟後資料還在、是需要 durability 的事件流預設選擇；<code>memory</code> storage 把 stream 放 RAM、吞吐跟延遲更好但 server 重啟即全失、適合短期 fan-out 或可重建的快取型資料。</p>
<p>實機建一個 file storage、limits retention、discard old 的 stream：</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">nats --server nats://localhost:4232 stream add orders <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --subjects <span class="s1">&#39;orders.&gt;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --storage file <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --retention limits <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --discard old <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --max-msgs <span class="m">1000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --max-bytes 10MB <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --max-age 1h <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --replicas <span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --defaults</span></span></code></pre></div><p><code>nats stream info orders</code> 回報的配置確認旋鈕都生效：</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">                     Subjects: orders.&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">                      Storage: File
</span></span><span class="line"><span class="ln">3</span><span class="cl">                    Retention: Limits
</span></span><span class="line"><span class="ln">4</span><span class="cl">               Discard Policy: Old
</span></span><span class="line"><span class="ln">5</span><span class="cl">             Maximum Messages: 1,000
</span></span><span class="line"><span class="ln">6</span><span class="cl">                Maximum Bytes: 10 MiB
</span></span><span class="line"><span class="ln">7</span><span class="cl">                  Maximum Age: 1h0m0s</span></span></code></pre></div><p>選 memory 的判讀訊號：訊息可從上游重建（例如 metrics 採樣、可重抓的 snapshot）、或 consumer 一定在線且消費速度跟得上、且單 stream 資料量遠小於可用 RAM。一旦這三條有一條不成立、預設回到 file storage。</p>
<h3 id="retentionlimits-vs-interest-vs-workqueue">Retention：limits vs interest vs workqueue</h3>
<p>Retention policy 決定「訊息什麼時候從 stream 移除」、是 stream 三種使用形態的分水嶺。</p>
<p><code>limits</code> retention 是時間 / 容量驅動：訊息留到撞上 MaxMsgs / MaxBytes / MaxAge 任一上限才移除、跟有沒有人消費無關。這是「事件 log」形態、適合需要 replay、多個獨立 consumer 各讀各的場景。訂單事件流用 limits、因為審計、對帳、即時處理可能是三個獨立 consumer、訊息不能因為某個 consumer ack 了就消失。</p>
<p><code>interest</code> retention 是訂閱驅動：當 stream 上 <em>所有</em> 已註冊的 consumer 都 ack 了某筆訊息、該訊息立刻移除。它介於 limits 跟 workqueue 之間、適合「只要所有關心的 consumer 都收到就不必再留」的扇出場景。</p>
<p><code>workqueue</code> retention 是任務佇列形態：每筆訊息只會被 <em>一個</em> consumer 成功 ack、ack 後立刻刪除。它把 stream 當成工作分派佇列、語意接近 RabbitMQ 的 work queue。實機驗證 workqueue 的 retention 在 info 反映：</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">nats --server nats://localhost:4232 stream add wq <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --subjects <span class="s1">&#39;wq.&gt;&#39;</span> --storage memory --retention work <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --max-msgs <span class="m">100</span> --replicas <span class="m">1</span> --defaults
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># nats stream info wq → Retention: WorkQueue</span></span></span></code></pre></div><p>判讀路由：需要多 consumer 各自 replay → limits；需要扇出且所有訂閱者收齊就清 → interest；需要競爭式單次消費的任務派工 → workqueue。選 workqueue 卻又掛兩個 filter 重疊的 consumer 會在建 consumer 時被拒、因為 workqueue 不允許同一筆訊息被兩個 consumer 認領。</p>
<h3 id="discardold-vs-new">Discard：old vs new</h3>
<p>Discard policy 決定 stream <em>撞上 MaxMsgs / MaxBytes 上限後</em> 丟哪一端。這個旋鈕的選擇直接對應業務對「舊資料」跟「新資料」誰更重要的判斷、選錯會靜默丟訊息。</p>
<p><code>discard old</code> 在達上限時丟掉最舊的訊息、騰空間給新訊息。實機驗證：max-msgs 設 3、連發 5 筆、stream 留下最後 3 筆：</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">discard old, max-msgs 3, published 5:
</span></span><span class="line"><span class="ln">2</span><span class="cl">                     Messages: 3
</span></span><span class="line"><span class="ln">3</span><span class="cl">               First Sequence: 3
</span></span><span class="line"><span class="ln">4</span><span class="cl">                Last Sequence: 5</span></span></code></pre></div><p>最舊的 seq 1、2 被丟、保留 seq 3-5。這對應「新資料比舊資料重要」的場景：即時儀表板、最新狀態快照、寧可丟歷史也要保住最新。</p>
<p><code>discard new</code> 在達上限時拒絕新訊息、保住已存的舊訊息。同樣 max-msgs 3、連發 5 筆：</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">discard new, max-msgs 3, published 5:
</span></span><span class="line"><span class="ln">2</span><span class="cl">                     Messages: 3
</span></span><span class="line"><span class="ln">3</span><span class="cl">               First Sequence: 1
</span></span><span class="line"><span class="ln">4</span><span class="cl">                Last Sequence: 3</span></span></code></pre></div><p>保留 seq 1-3、後到的 seq 4、5 進不來。這對應「舊資料是已承諾的工作、不能丟」的場景：任務佇列在塞滿時應拒收新任務（並對上游施加 backpressure）、而不是把排隊中的任務擠掉。</p>
<p>discard new 有個容易踩的投遞行為差異、見故障演練 Case 2。</p>
<h3 id="容量上限maxmsgs--maxbytes--maxage">容量上限：MaxMsgs / MaxBytes / MaxAge</h3>
<p>三個上限是 OR 關係：任一撞到就觸發 discard / 移除。MaxMsgs 限筆數、MaxBytes 限總位元組、MaxAge 限訊息存活時間。實務上三者搭配使用：MaxAge 防止無限累積（例如事件流只保留 7 天）、MaxBytes 是 disk 的硬護欄（防單 stream 撐爆 volume）、MaxMsgs 在訊息大小均勻時當作粗略筆數控制。</p>
<p>容量規劃的判讀順序是先定 MaxAge（業務需要 replay 多久）、再用「平均訊息大小 × 預估 throughput × MaxAge」反推 MaxBytes 是否在 disk 預算內、超出就縮短 MaxAge 或拆 stream。把 MaxBytes 設成 unlimited 而只靠 MaxMsgs 是常見的容量事故來源：訊息大小一旦變大（例如 payload 夾帶了 base64 附件）、筆數沒到上限但 disk 已滿。</p>
<h2 id="consumer-設計pullpushackackwaitmaxdeliverreplay">Consumer 設計：pull/push、ack、AckWait、MaxDeliver、replay</h2>
<p>Consumer 的設計責任是控制「訊息怎麼從 stream 送到處理端、處理端怎麼確認、確認不回來怎麼辦」。它的每個旋鈕都圍繞同一個核心張力：在 at-least-once 投遞下、如何在「不漏處理」跟「不過度重投」之間取得平衡。對應的概念基礎見 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">Delivery Semantics</a> 與 <a href="/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">Processing Semantics</a> 知識卡。</p>
<h3 id="pull-vs-push">Pull vs push</h3>
<p>Pull consumer 由處理端主動拉：consumer 發 pull request 帶 batch size、server 才送對應數量的訊息。流量控制天然落在消費端、消費端有多少處理能力就拉多少、是現代 JetStream 應用的預設模式。Push consumer 由 server 主動推到一個 delivery subject、處理端訂閱那個 subject、適合需要 server 端 flow control 或既有 Core NATS 訂閱模型遷移的場景。</p>
<p>實機建一個 pull consumer、explicit ack、AckWait 30s、MaxDeliver 5、replay instant：</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">nats --server nats://localhost:4232 consumer add orders worker <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --pull <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --deliver all <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --ack explicit <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --wait 30s <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --max-deliver <span class="m">5</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --replay instant <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;orders.&gt;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --defaults</span></span></code></pre></div><p><code>nats consumer info orders worker</code> 確認配置：</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">                    Name: worker
</span></span><span class="line"><span class="ln">2</span><span class="cl">               Pull Mode: true
</span></span><span class="line"><span class="ln">3</span><span class="cl">          Deliver Policy: All
</span></span><span class="line"><span class="ln">4</span><span class="cl">              Ack Policy: Explicit
</span></span><span class="line"><span class="ln">5</span><span class="cl">                Ack Wait: 30.00s
</span></span><span class="line"><span class="ln">6</span><span class="cl">           Replay Policy: Instant
</span></span><span class="line"><span class="ln">7</span><span class="cl">      Maximum Deliveries: 5</span></span></code></pre></div><p>push consumer 改用 <code>--target &lt;subject&gt;</code> 取代 <code>--pull</code>、info 會回報 <code>Delivery Subject:</code> 而非 Pull Mode。</p>
<h3 id="ackpolicyexplicit-是預設選擇">AckPolicy：explicit 是預設選擇</h3>
<p>Ack policy 決定 consumer 怎麼確認訊息已處理。<code>explicit</code> 要求對每一筆訊息單獨 ack、是 at-least-once 處理的基礎、production 預設選擇。<code>all</code> 用累積 ack：ack 第 N 筆等於 ack 了第 N 筆以前全部、吞吐高但一筆處理失敗會讓整段重投。<code>none</code> 完全不 ack、投遞即視為完成、語意退化成接近 fire-and-forget、只適合可容忍丟失的場景。</p>
<p>explicit ack 之所以是預設、是因為它讓每筆訊息的處理結果獨立可追蹤：哪筆 ack 了、哪筆還 outstanding、哪筆重投超限、都能在 consumer info 看到。實機發 3 筆訊息後、consumer info 的 <code>Unprocessed Messages</code> 反映 stream 中尚未投遞的 backlog：</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">nats --server nats://localhost:4232 pub orders.created.us-1 <span class="s2">&#34;order-1&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 發 3 筆後：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># nats consumer info orders worker →</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#     Unprocessed Messages: 3</span></span></span></code></pre></div><p>拉出訊息但不 ack、consumer info 的 <code>Outstanding Acks</code> 反映已投遞但未確認的數量：</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">        Outstanding Acks: 3 out of maximum 1,000</span></span></code></pre></div><p>這兩個數字是診斷 consumer 健康的第一手訊號：<code>Unprocessed</code> 高代表 consumer 拉得太慢或停了（stream backlog）；<code>Outstanding Acks</code> 持續高代表訊息拉出去了但處理端沒 ack（處理慢或卡住）。這個區分對應 overview 排錯段的「pending 是 ack-pending 還是 stream backlog」判讀。</p>
<h3 id="ackwait--maxdeliver重投的兩個邊界">AckWait + MaxDeliver：重投的兩個邊界</h3>
<p>AckWait 是 server 等待 ack 的時間窗：訊息投遞後、若 AckWait 內沒收到 ack、server 視為投遞失敗、重新投遞。MaxDeliver 是同一筆訊息的投遞次數上限：達到後不再重投、訊息進入 terminal 狀態（可導向 advisory / DLQ 機制）。</p>
<p>這兩個旋鈕共同定義重投行為。AckWait 要設成 <em>略大於 consumer 處理一筆訊息的 p99 時間</em>：太短會在 consumer 還在正常處理時就誤判失敗重投、造成重複處理（見故障演練 Case 1）；太長會讓真正卡死的訊息遲遲不重投、拖慢 recovery。MaxDeliver 是 poison message 的護欄：一筆訊息若處理永遠失敗（例如 payload 格式壞）、沒有 MaxDeliver 它會無限重投佔住 consumer。對應 <a href="/blog/backend/knowledge-cards/redelivery-loop/" data-link-title="Redelivery Loop" data-link-desc="說明同一訊息反覆投遞失敗如何消耗 consumer 容量">Redelivery Loop</a> 知識卡描述的失控重投。</p>
<h3 id="replayinstant-vs-original">Replay：instant vs original</h3>
<p>Replay policy 只在 consumer 從歷史位置讀（例如 <code>--deliver all</code> 重讀整個 stream）時生效、決定投遞節奏。<code>instant</code> 以 server 最快速度投遞、是處理 backlog 或重建狀態的預設。<code>original</code> 按訊息 <em>原始寫入的時間間隔</em> 重放：若原始訊息間隔 1 秒寫入、replay 也間隔 1 秒投遞、用於需要重現時序的測試或模擬。實機兩種都可建：</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">nats consumer add orders replayorig ... --replay original  <span class="c1"># Replay Policy: Original</span></span></span></code></pre></div><h2 id="cluster--supercluster--leaf-node三層拓樸">Cluster / Supercluster / Leaf node：三層拓樸</h2>
<p>NATS 的拓樸分三層、各解一個不同尺度的問題：Cluster 解單區內的高可用、Supercluster 解跨區的延展、Leaf node 解邊緣到中心的連接。三者可組合、但職責不重疊。</p>
<h3 id="cluster單區-raft-高可用">Cluster：單區 Raft 高可用</h3>
<p>Cluster 是同一 region 內多個 NATS server 用 full mesh route 互連、JetStream 的 stream 透過 Raft 在多個 replica 間複製。Replica 數（R1 / R3 / R5）決定容錯：R3 容忍 1 節點失效、R5 容忍 2 節點。Raft 要求多數派（quorum）才能寫入、所以 R3 需要至少 2 節點健康。</p>
<p>實機用 3 節點 docker compose 起 cluster、建 R3 stream、stream info 顯示 Raft group 與 replica 狀態：</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">nats --server nats://n1:4222 stream add rep3 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --subjects <span class="s1">&#39;rep3.&gt;&#39;</span> --storage file --retention limits <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --discard old --max-msgs <span class="m">1000</span> --replicas <span class="m">3</span> --defaults</span></span></code></pre></div>




<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">                     Replicas: 3
</span></span><span class="line"><span class="ln">2</span><span class="cl">Cluster Information:
</span></span><span class="line"><span class="ln">3</span><span class="cl">                Cluster Group: S-R3F-unEqlH8C
</span></span><span class="line"><span class="ln">4</span><span class="cl">                       Leader: n2 (222ms)
</span></span><span class="line"><span class="ln">5</span><span class="cl">                      Replica: n1, current, seen 217ms ago
</span></span><span class="line"><span class="ln">6</span><span class="cl">                      Replica: n3, current, seen 219ms ago</span></span></code></pre></div><p>Leader 是 Raft 選出的寫入協調者、其餘 replica 跟隨。<code>current</code> 代表該 replica 與 leader 同步；落後會顯示 <code>outdated</code> 加落後的 operation 數。失去 quorum 的行為見故障演練 Case 4。</p>
<h3 id="supercluster跨區-gateway-延展">Supercluster：跨區 gateway 延展</h3>
<p>Supercluster 用 gateway 連接多個 Cluster、形成跨 region / 跨雲的單一 NATS 邏輯網路。Gateway 之間是按需轉發、不是 full mesh：訊息只在有訂閱者的 region 之間流動、避免跨區頻寬被無謂的全量複製吃掉。Supercluster 讓 publisher 在任一 region 發訊息、訂閱者在另一 region 收到、同時讓每個 Cluster 維持自己的 JetStream Raft 群組與本地高可用。</p>
<blockquote>
<p>以下 Supercluster 行為依 <a href="https://docs.nats.io/running-a-nats-service/configuration/gateways">NATS 官方文件</a> 描述、未在本文實機環境驗證（gateway 多區拓樸需要跨 region 部署）。</p></blockquote>
<p><a href="/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/" data-link-title="3.C35 Form3：NATS JetStream 多雲低延遲支付" data-link-desc="Form3 服務 Tier-1 銀行、500ms SLA、SNS/SQS 吃 300ms 預算、改 NATS&#43;JetStream 跨雲 6x 延遲改善。">3.C35 Form3</a> 是 Leaf node 跨雲橋接的代表案例（Supercluster 為相應的一般拓樸選項、case 本身明確點到的是 Leaf node）：服務 Tier-1 銀行、要求 500ms 端到端 SLA、AWS SNS/SQS 約 300ms 延遲吃掉預算。Form3 用 JetStream 跨雲橋接、達到約 6× 延遲改善、並做到「AWS 整個 region 掛掉時不喪失處理能力」。這個案例揭露的判讀是：金融支付的硬 latency 預算逼出特定拓樸選型、不是把 Kafka / SQS 通用化套上去。</p>
<h3 id="leaf-node邊緣連中心">Leaf node：邊緣連中心</h3>
<p>Leaf node 是輕量 NATS server、跑在邊緣（工廠、店面、IoT gateway）、透過單一 leaf connection 連回中心 hub。它在邊緣本地提供完整的 NATS / JetStream 能力（本地 publish / subscribe / 本地持久化）、同時把需要的 subject 透過 leaf connection 雙向橋接到 hub。Leaf node 的價值在於：邊緣到中心的網路斷線時、邊緣端的本地 JetStream 持續收訊息、連線恢復後再同步、不丟資料。</p>
<blockquote>
<p>以下 Leaf node 行為依 <a href="https://docs.nats.io/running-a-nats-service/configuration/leafnodes">NATS 官方文件</a> 與下列 case 描述、未在本文實機環境驗證（leaf 拓樸需要 hub + edge 雙端部署）。</p></blockquote>
<p><a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37 MachineMetrics</a> 是 Leaf node 邊緣到雲端的完整案例：跨數百客戶廠區、數千機台、單機最高 1000Hz 採樣、工廠網路斷斷續續、Kinesis 等 cloud-only 工具無法跑在資源受限 edge。MachineMetrics 用 Leaf node 做 hub-and-spoke、edge 端用 JetStream 做本地持久化抵抗斷線。這個案例揭露的判讀是：broker 的功能集合（messaging + 本地持久化 + KV + Object Store + auth）決定它能不能取代邊緣的多套工具。</p>
<p><a href="/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">3.C41 i-flow</a> 是多工廠 leaf node 拓樸的另一證據：每日 4 億筆 data operation、200+ OT/IT connector、用 leaf node hub-and-spoke 把多工廠接到 central、而不是每工廠自管一套 cluster。判讀：多工廠場景的運維成本由「每個邊緣點是不是要獨立維運一套 cluster」決定、leaf node 把邊緣端壓到單一 server。</p>
<h2 id="subject-based-acl-與多租戶">Subject-based ACL 與多租戶</h2>
<p>NATS 多租戶的主機制是 account：account 是完全隔離的 subject 命名空間、不同 account 之間預設互不可見、即使 subject 名稱相同也不會互通。Account 之內再用 subject-level permission 控制每個 user 能 publish / subscribe 哪些 subject。這兩層組合起來：account 給租戶硬隔離、subject permission 給租戶內的角色細分權限。</p>
<p>跨 account 的受控互通用 import / export：一個 account 把特定 subject export 出來、另一個 account 顯式 import、才會打通那條 subject。預設不通、互通是顯式授權的結果、這讓多租戶的資料流動可審計。對應 MachineMetrics 案例用 decentralized auth 隔離不同客戶廠區的設計：每個客戶是一個 account、廠區設備在 account 內用 subject permission 限定只能發自己廠區的 subject。</p>
<p>多租戶設計的判讀訊號：租戶之間要完全隔離、用 account；同租戶內的不同服務 / 角色要限權、用 subject permission；少數需要跨租戶共享的 subject（例如全域控制信號）、用 import / export 顯式打通、不要為了方便把不同租戶塞進同 account。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<p>deep article 的差異化價值在故障演練。以下四個都是 JetStream stream / consumer / 拓樸層的典型事故、前兩個有本文實機驗證、後兩個結合實機（quorum）與 case 敘述。</p>
<h3 id="case-1ackwait-太短造成重複處理">Case 1：AckWait 太短造成重複處理</h3>
<p><strong>徵兆</strong>：consumer 正常運行、處理邏輯沒報錯、但下游出現大量重複副作用（重複扣款、重複寄信、重複寫入）。consumer info 的 <code>Redelivered Messages</code> 持續上升、即使處理端沒有任何 exception。</p>
<p><strong>根因</strong>：AckWait 設得比 consumer 處理一筆訊息的實際耗時短。訊息投遞後 consumer 還在處理、AckWait 就到期、server 判定投遞失敗、把同一筆訊息重投給（可能是另一個）consumer 實例、於是同一筆訊息被處理兩次。實機重現：建一個 AckWait 1s 的 consumer、拉出訊息不 ack、過 1s 後再拉、<code>tries</code> 從 1 變 2：</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">第一次拉：subj: orders.created.us-1 / tries: 1 / str seq: 1
</span></span><span class="line"><span class="ln">2</span><span class="cl">過 1s 後：subj: orders.created.us-1 / tries: 2 / str seq: 1
</span></span><span class="line"><span class="ln">3</span><span class="cl">consumer info → Redelivered Messages: 3</span></span></code></pre></div><p><strong>修法</strong>：</p>
<ol>
<li><strong>量測再設值</strong>：AckWait 設成 consumer 處理 p99 時間的 2-3 倍、而不是拍腦袋設 30s。處理一筆要 5s 的 worker 配 AckWait 30s、處理一筆要 45s 的 worker 配 AckWait 30s 就會持續誤判重投。</li>
<li><strong>長任務用 in-progress ack</strong>：處理時間本就偏長且方差大的任務、處理端在處理中定期送 <code>AckProgress</code>（working ack）延長 AckWait、而不是把 AckWait 設成一個無法涵蓋最壞情況的固定大值。</li>
<li><strong>處理端做冪等</strong>：at-least-once 投遞下重複是常態而非異常、副作用以業務 key 去重（對應 <a href="/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">Processing Semantics</a> 的冪等要求）。AckWait 只能降低重複頻率、不能消除重複。</li>
</ol>
<h3 id="case-2discard-policy-選錯靜默丟訊息">Case 2：discard policy 選錯靜默丟訊息</h3>
<p><strong>徵兆</strong>：上游 publisher 一切正常、沒收到任何 error、但下游 consumer 發現訊息有缺口（seq 跳號）、或最舊的歷史訊息神祕消失。對帳時帳目對不上、但日誌裡找不到任何失敗紀錄。</p>
<p><strong>根因</strong>：兩種情況。其一、stream 用 <code>discard old</code>、流量超過 MaxMsgs / MaxBytes、最舊的訊息被靜默丟棄騰空間——這在「事件 log 需要完整 replay」的場景是資料遺失。其二、stream 用 <code>discard new</code>、滿了之後新訊息被拒、但 publisher 用的是 <em>Core NATS publish</em>（不等 stream ack）、所以 publisher 端看到「發送成功」、訊息其實沒進 stream。實機重現後者的危險：對一個 discard new 已滿的 stream 用 Core pub 與 JetStream-aware pub、結果完全不同：</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">Core pub（不等 ack）：    Published 8 bytes to &#34;dnew.x&#34;        ← 看似成功、實際丟失
</span></span><span class="line"><span class="ln">2</span><span class="cl">JetStream pub（等 ack）： nats: error: maximum messages exceeded (10077)  ← 正確報錯</span></span></code></pre></div><p><strong>修法</strong>：</p>
<ol>
<li><strong>publisher 一律用 JetStream-aware publish</strong>：等 stream 的 PubAck 回來才算發送成功、才能在 stream 滿、quorum 失效、subject 不匹配時收到明確 error。用 Core pub 發進 JetStream subject 等於放棄所有投遞保證。</li>
<li><strong>discard policy 對齊業務語意</strong>：事件 log（需要完整歷史）配 limits + 充足 MaxAge、絕不靠 discard old 當容量控制；任務佇列配 discard new + 上游 backpressure、滿了就讓 producer 慢下來而不是擠掉排隊任務。</li>
<li><strong>監控 discard 計數</strong>：stream 的 discard 不是錯誤狀態、不會觸發 alert。要主動監控訊息 seq 連續性與 stream 的訊息移除速率、把「非預期的 discard」變成可觀測訊號。</li>
</ol>
<h3 id="case-3leaf-node-斷線重連">Case 3：Leaf node 斷線重連</h3>
<p><strong>徵兆</strong>：邊緣端（工廠 / 店面）到中心 hub 的網路抖動、leaf connection 反覆斷開重連、hub 端看到某些 subject 的訊息延遲尖刺、邊緣端 reconnect 計數持續累加。網路恢復後、邊緣累積的訊息一次湧入 hub、造成 hub 端短暫的處理尖峰。</p>
<p><strong>根因</strong>：邊緣到中心是廣域網、品質不如資料中心內網。Leaf connection 斷線期間、邊緣端的本地 JetStream 持續收訊息並本地持久化（這正是 leaf node 的設計目的）；連線恢復後、累積的 backlog 一次同步到 hub、形成尖峰。若邊緣端沒有本地 JetStream、斷線期間的訊息直接丟失。</p>
<blockquote>
<p>以下根因與修法依 NATS 官方 leaf node 文件與 <a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">MachineMetrics</a> / <a href="/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">i-flow</a> case 描述、未在本文實機環境驗證。</p></blockquote>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>邊緣端必開本地 JetStream</strong>：把斷線容忍從「依賴網路不斷」改成「斷線期間本地持久化、恢復後同步」。這是 MachineMetrics 用 edge JetStream 取代 SQLite 的核心理由——工廠網路斷斷續續是常態、不是異常。</li>
<li><strong>hub 端對同步尖峰做 flow control</strong>：恢復連線後的 backlog 同步用 consumer 端的 pull batch 限速、避免邊緣 backlog 一次打爆 hub 的處理能力。</li>
<li><strong>監控 reconnect 與 latency</strong>：leaf 連線的 reconnect 次數與 subject mapping latency 是邊緣網路品質的直接訊號（對應 overview 排錯段「leaf node 連線不穩」）。reconnect 頻繁代表網路或 hub 容量要處理、不是調 leaf 參數能解。</li>
</ol>
<h3 id="case-4stream-replica-失去-quorum">Case 4：Stream replica 失去 quorum</h3>
<p><strong>徵兆</strong>：R3 stream 突然無法寫入、publisher 的 JetStream publish 卡住後回 <code>no responders available</code>；stream info 顯示 <code>Leader:</code> 欄位空白、多數 replica 標 OFFLINE。讀取可能還能從存活節點拿到舊資料、但寫入完全停擺。</p>
<p><strong>根因</strong>：JetStream 的 stream 用 Raft 複製、寫入需要多數派確認。R3 stream 需要至少 2 節點健康才有 quorum；同時失去 2 節點就只剩 1 節點、達不到多數、Raft 無法選出 leader、stream 變成無法寫入。實機重現：3 節點 cluster 的 R3 stream、停掉 2 個節點、stream info 顯示無 leader、JetStream publish 報錯：</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">停 2 節點後 stream info：
</span></span><span class="line"><span class="ln">2</span><span class="cl">                       Leader:
</span></span><span class="line"><span class="ln">3</span><span class="cl">                      Replica: n1, current, seen 3.35s ago
</span></span><span class="line"><span class="ln">4</span><span class="cl">                      Replica: n2, outdated, OFFLINE, not seen
</span></span><span class="line"><span class="ln">5</span><span class="cl">                      Replica: n3, outdated, OFFLINE, not seen
</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">此時 JetStream publish：
</span></span><span class="line"><span class="ln">8</span><span class="cl">                      nats: error: nats: no responders available for request</span></span></code></pre></div><p>恢復 1 個節點（回到 2/3 多數）後、Raft 立即重選 leader、stream 恢復可寫：</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">啟動 n2 後：
</span></span><span class="line"><span class="ln">2</span><span class="cl">                       Leader: n1 (506ms)
</span></span><span class="line"><span class="ln">3</span><span class="cl">                      Replica: n2, current, seen 499ms ago
</span></span><span class="line"><span class="ln">4</span><span class="cl">                      Replica: n3, outdated, OFFLINE, not seen, 4 operations behind</span></span></code></pre></div><p><strong>修法</strong>：</p>
<ol>
<li><strong>replica 數對齊容錯目標</strong>：要容忍 1 節點失效用 R3、容忍 2 節點用 R5；不要為了省資源把關鍵 stream 設 R1（單點、節點掛了 stream 直接不可用）。</li>
<li><strong>replica 跨 failure domain 散開</strong>：R3 的 3 個 replica 要落在不同 availability zone / rack、避免單一 AZ 故障同時帶走 2 個 replica 直接失去 quorum。</li>
<li><strong>監控 replica 健康而非只看 leader</strong>：stream info 的每個 replica 的 <code>current</code> / <code>outdated</code> / <code>OFFLINE</code> 狀態是 quorum 餘裕的直接訊號。R3 已經有 1 個 replica OFFLINE 時 quorum 餘裕只剩 0、要當成 P1 處理、不能等到第 2 個也掛才反應（對應 overview 排錯段「JetStream raft 不一致」）。</li>
</ol>
<h2 id="容量與規模判讀">容量與規模判讀</h2>
<p>JetStream 的配置在不同規模下適用性不同、超出範圍要換拓樸而非調參數。</p>
<table>
  <thead>
      <tr>
          <th>規模訊號</th>
          <th>適用拓樸</th>
          <th>換檔訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單區、中等吞吐、需要 HA</td>
          <td>單 Cluster R3</td>
          <td>單區頻寬 / 節點數撐不住 → 加節點 reshard 或拆 stream</td>
      </tr>
      <tr>
          <td>跨 region / 跨雲、訂閱者分散各區</td>
          <td>Supercluster（多 Cluster + gateway）</td>
          <td>需要邊緣本地持久化 → 疊加 Leaf node</td>
      </tr>
      <tr>
          <td>大量邊緣點、網路不穩、邊緣要本地能力</td>
          <td>Leaf node hub-and-spoke</td>
          <td>邊緣點 &gt; 數百、每點要獨立運維 → 評估 managed（Synadia）</td>
      </tr>
  </tbody>
</table>
<p><strong>單 Cluster R3</strong> 是多數中等規模服務的起點：單區內高可用、JetStream Raft 處理節點故障、運維只有一套 cluster。撞到天花板的訊號是單區頻寬或單節點 disk / CPU 到上限、此時先評估加節點重分配或把熱 stream 拆出去、而不是急著上 supercluster。</p>
<p><strong>Supercluster</strong> 在訂閱者地理分散、或要求單區整個掛掉仍能服務時才值得引入。它的成本是跨區 gateway 的運維複雜度與跨區頻寬、不該為了「以後可能要跨區」提前鋪。Form3 的判讀是硬 SLA（500ms、region 全掛仍可用）逼出來的、不是預設架構。</p>
<p><strong>Leaf node hub-and-spoke</strong> 在邊緣點多、邊緣網路不穩、邊緣要本地持久化 / KV / 計算能力時適用。當邊緣點數量大到每點獨立運維成本不可接受、評估走 managed NATS（Synadia Cloud）把運維外包、而不是自建更大的 hub。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<p>本文聚焦 JetStream stream / consumer / 拓樸的 implementation；以下是往上下游的銜接。</p>
<h3 id="回-vendor-overview-與相鄰章節">回 vendor overview 與相鄰章節</h3>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS overview</a>——Core NATS vs JetStream 的選型判讀、排錯快速判讀、何時改走其他 broker</li>
<li>跨 vendor consumer 設計：<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>——本文的 pull/push、ack、重投放回語言無關的 consumer 設計框架</li>
<li>投遞與處理語意基礎：<a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">Delivery Semantics</a> / <a href="/blog/backend/knowledge-cards/processing-semantics/" data-link-title="Processing Semantics" data-link-desc="說明 consumer 處理事件後業務結果是否正確，與投遞成功分屬不同責任">Processing Semantics</a> / <a href="/blog/backend/knowledge-cards/redelivery-loop/" data-link-title="Redelivery Loop" data-link-desc="說明同一訊息反覆投遞失敗如何消耗 consumer 容量">Redelivery Loop</a> 知識卡</li>
</ul>
<h3 id="對應-case">對應 case</h3>
<ul>
<li><a href="/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/" data-link-title="3.C35 Form3：NATS JetStream 多雲低延遲支付" data-link-desc="Form3 服務 Tier-1 銀行、500ms SLA、SNS/SQS 吃 300ms 預算、改 NATS&#43;JetStream 跨雲 6x 延遲改善。">3.C35 Form3</a>——Supercluster + Leaf node 跨雲低延遲支付、硬 SLA 驅動拓樸</li>
<li><a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37 MachineMetrics</a>——Leaf node + edge JetStream + KV + Object Store + 多租戶 auth 的完整邊緣案例</li>
<li><a href="/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">3.C41 i-flow</a>——多工廠 leaf node hub-and-spoke、運維成本驅動拓樸選型</li>
</ul>
<h3 id="後續可深入的議題">後續可深入的議題</h3>
<ul>
<li><strong>JetStream KV / Object Store</strong>：基於 stream 的 key-value 與 blob 儲存、何時用 NATS KV vs 真的 KV 服務（Redis / etcd）、見 overview 進階主題段</li>
<li><strong>Leaf node 多節點實機驗證</strong>：本文 Supercluster / Leaf node 段以 case + 官方文件敘述；補一篇 hub + edge 雙端 compose 的實機演練（含斷線注入、backlog 同步觀測）是自然延伸</li>
<li><strong>Subject mapping 與 transform</strong>：leaf node 跨層的 subject 重映射、跨 account import / export 的細部配置</li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ → AWS SQS：交出 broker 維運、把 routing 收斂進 application</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-aws-sqs/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-aws-sqs/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> 跟 &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>。對照 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> 的 paradigm shift、本篇主導差異維度是 &lt;em>operational model&lt;/em>：source 跟 target 都是任務隊列、能力大致對得上、但運維責任從「自管 broker 叢集」整批交給 AWS managed 服務。&lt;/p>&lt;/blockquote>
&lt;p>RabbitMQ → AWS SQS 的核心是把 broker 運維責任轉移給 managed 服務、同時接受 SQS 沒有 exchange routing 這個事實、把路由邏輯收斂回 application 或改用 SNS fan-out。這個遷移不是 protocol drop-in（AMQP client 不能直接連 SQS）、application 端需要改 delivery 控制機制（manual ack → visibility timeout + delete）；但它也不是 paradigm shift（兩端都是 at-least-once 任務隊列、DLQ / 重試 / 解耦的語意一致）。主導差異落在 operational 維度、所以本文走 Type C operational redesign hybrid 結構。&lt;/p>
&lt;h2 id="為什麼遷不想再養-rabbitmq-叢集">為什麼遷：不想再養 RabbitMQ 叢集&lt;/h2>
&lt;p>觸發評估 SQS 的最常見壓力是 broker 維運成本、不是功能缺口。自管 RabbitMQ 叢集要承擔的運維責任包含 Erlang cluster 拓樸維護、network partition（腦裂）處理、quorum queue 的 Raft 一致性調校、disk / memory alarm 的容量規劃、版本升級的 rolling restart。這些責任需要至少 0.5-1 FTE 的持續投入、且在 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">network partition&lt;/a> 這類事故發生時需要熟悉 Erlang runtime 的人即時介入。&lt;/p>
&lt;p>SQS 把這整層責任移除。沒有 broker 實例、沒有 cluster 拓樸、沒有 disk / memory watermark、沒有版本升級。換來的代價是 routing 能力消失（SQS 沒有 exchange）、application 要改 delivery 控制機制、以及 AWS 生態綁定。這個交換在三種情境下成立：&lt;/p>
&lt;p>第一種是 AWS 生態原生服務。若 producer / consumer 已經跑在 Lambda、ECS、EKS 上、SQS 的 event source mapping 跟 IAM 整合讓 application 不必自管連線池跟認證。RabbitMQ 在 AWS 上要嘛自管 EC2 叢集、要嘛用 Amazon MQ（仍是 broker 模型、運維責任只是部分轉移）、都不如 SQS 的 serverless 整合直接。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> 跟 <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>。對照 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 的 paradigm shift、本篇主導差異維度是 <em>operational model</em>：source 跟 target 都是任務隊列、能力大致對得上、但運維責任從「自管 broker 叢集」整批交給 AWS managed 服務。</p></blockquote>
<p>RabbitMQ → AWS SQS 的核心是把 broker 運維責任轉移給 managed 服務、同時接受 SQS 沒有 exchange routing 這個事實、把路由邏輯收斂回 application 或改用 SNS fan-out。這個遷移不是 protocol drop-in（AMQP client 不能直接連 SQS）、application 端需要改 delivery 控制機制（manual ack → visibility timeout + delete）；但它也不是 paradigm shift（兩端都是 at-least-once 任務隊列、DLQ / 重試 / 解耦的語意一致）。主導差異落在 operational 維度、所以本文走 Type C operational redesign hybrid 結構。</p>
<h2 id="為什麼遷不想再養-rabbitmq-叢集">為什麼遷：不想再養 RabbitMQ 叢集</h2>
<p>觸發評估 SQS 的最常見壓力是 broker 維運成本、不是功能缺口。自管 RabbitMQ 叢集要承擔的運維責任包含 Erlang cluster 拓樸維護、network partition（腦裂）處理、quorum queue 的 Raft 一致性調校、disk / memory alarm 的容量規劃、版本升級的 rolling restart。這些責任需要至少 0.5-1 FTE 的持續投入、且在 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">network partition</a> 這類事故發生時需要熟悉 Erlang runtime 的人即時介入。</p>
<p>SQS 把這整層責任移除。沒有 broker 實例、沒有 cluster 拓樸、沒有 disk / memory watermark、沒有版本升級。換來的代價是 routing 能力消失（SQS 沒有 exchange）、application 要改 delivery 控制機制、以及 AWS 生態綁定。這個交換在三種情境下成立：</p>
<p>第一種是 AWS 生態原生服務。若 producer / consumer 已經跑在 Lambda、ECS、EKS 上、SQS 的 event source mapping 跟 IAM 整合讓 application 不必自管連線池跟認證。RabbitMQ 在 AWS 上要嘛自管 EC2 叢集、要嘛用 Amazon MQ（仍是 broker 模型、運維責任只是部分轉移）、都不如 SQS 的 serverless 整合直接。</p>
<p>第二種是 routing 邏輯本來就簡單。若 RabbitMQ 的用法是 direct exchange + 少數固定 routing key、或單純 worker pool 消費單一 queue、那 exchange 的靈活性本來就沒被用到、遷到 SQS 不損失能力。Airbnb 的 Dynein 分散式延遲任務系統就是這個形狀：用 SQS at-least-once + DLQ 取代原本受限於單 Redis 的 Resque、每 scheduler instance 達約 1000 QPS、水平擴展（見 <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>）。任務排程對「不丟資料」的需求 at-least-once 足夠、不需要 broker 級 routing。</p>
<p>第三種是團隊規模不支撐 broker 專業。小團隊養一套 RabbitMQ 叢集、真正用到的是「可靠的任務隊列 + DLQ」、但要付出整套 Erlang 運維學習曲線。把這層交給 SQS、團隊把精力放回 application 邏輯。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<p>遷移前先跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">diff dimension audit</a>、對每個維度評估 source 跟 target 的差異程度、決定主導維度跟結構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RabbitMQ（self-managed）</th>
          <th>AWS SQS（managed）</th>
          <th>差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>AMQP 0-9-1 協議、exchange / queue</td>
          <td>HTTP API、SendMessage / ReceiveMessage</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>自管 Erlang 叢集、cluster / disk / 升級</td>
          <td>Fully managed、無實例、無版本</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>任務隊列 + 重試 + DLQ</td>
          <td>任務隊列 + 重試 + DLQ</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Components（1 vs N）</td>
          <td>broker 一站式（routing 內建）</td>
          <td>SQS + 需要 SNS 補 fan-out routing</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>manual ack / nack、prefetch、AMQP client</td>
          <td>visibility timeout + delete、batch、SDK</td>
          <td>中高</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>單叢集 / federation 拓樸</td>
          <td>region-scoped queue、無拓樸概念</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>主導維度是 operational（高）</strong>：遷移的核心價值跟核心風險都在「broker 運維責任整批轉移」。Application change 維度評中高、因為 delivery 控制機制要改、但這是受控的 SDK 層改寫、不是 paradigm 重設計。Components 維度評中、因為 exchange routing 在 SQS 沒有對等物、要靠 SNS fan-out 或多 queue 補回來。其餘三維度低或中。</p>
<p>主導維度落在 operational、所以主結構走 Type C：以 operational redesign 對位開頭、phased 執行、故障演練聚焦在「以為對等其實不對等」的運維陷阱。Application change 跟 Components 兩個次高維度不硬塞進主結構、各自抽出獨立段（下面「application 改寫」跟「routing 收斂」兩段）。</p>
<h3 id="operational-redesign-對位">Operational redesign 對位</h3>
<p>Operational 維度差異最大、先逐項對位「原本自己做的事、現在誰做、怎麼做」：</p>
<table>
  <thead>
      <tr>
          <th>運維責任</th>
          <th>RabbitMQ（自己做）</th>
          <th>SQS（managed / application）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高可用</td>
          <td>quorum queue + cluster + partition 處理</td>
          <td>AWS 跨 AZ 自動冗餘、無需配置</td>
      </tr>
      <tr>
          <td>容量規劃</td>
          <td>disk / memory watermark、queue length 限</td>
          <td>自動擴展、無實例容量概念</td>
      </tr>
      <tr>
          <td>版本升級</td>
          <td>rolling restart、相容性驗證</td>
          <td>無、AWS 維護</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>Management UI + Prometheus exporter</td>
          <td>CloudWatch metric（depth / age）</td>
      </tr>
      <tr>
          <td>Delivery 控制</td>
          <td>broker-side ack / nack 狀態機</td>
          <td>client-side visibility timeout + delete</td>
      </tr>
      <tr>
          <td>重試 / DLQ</td>
          <td>DLX + dead-letter routing key</td>
          <td>redrive policy + maxReceiveCount</td>
      </tr>
      <tr>
          <td>Routing</td>
          <td>exchange + binding（broker 內建）</td>
          <td>application 或 SNS（broker 外）</td>
      </tr>
  </tbody>
</table>
<p>前四列是純收益：責任消失、不需要對等實作。後三列是責任轉移、不是消失 — delivery 控制從 broker 移到 client、重試從 DLX 移到 redrive policy、routing 從 broker 移到 application。這三列正是故障演練聚焦的地方、因為「以為功能還在、其實機制換了」是這類遷移的主要事故來源。</p>
<p>監控這列值得展開。RabbitMQ 的 queue depth、unacked、consumer 數量是從 broker 直接讀；SQS 改看 CloudWatch 的 <code>ApproximateNumberOfMessagesVisible</code>（queue depth）跟 <code>ApproximateAgeOfOldestMessage</code>（lag 訊號）。差異在於 SQS 的 metric 是 approximate、且有分鐘級延遲、不適合用來做秒級的 backpressure 決策。原本靠 RabbitMQ Management UI 即時看 queue 狀態的 runbook 要改寫成 CloudWatch alarm 驅動。</p>
<h2 id="application-改寫manual-ack--visibility-timeout--delete">Application 改寫：manual ack → visibility timeout + delete</h2>
<p>Application change 維度的核心是 delivery 控制機制換了一套模型。RabbitMQ 是 broker-side 維護訊息狀態、consumer 用 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 回報處理結果；SQS 是 client-side 用 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">visibility timeout</a> + 顯式 delete、broker 不維護「處理中」以外的狀態。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># RabbitMQ 端：manual ack pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">basic_qos</span><span class="p">(</span><span class="n">prefetch_count</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>  <span class="c1"># 一次最多領 10 條未 ack</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">def</span> <span class="nf">callback</span><span class="p">(</span><span class="n">ch</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">properties</span><span class="p">,</span> <span class="n">body</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">process</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_ack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># nack + requeue，或丟 DLX</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_nack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">,</span> <span class="n">requeue</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</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 class="n">channel</span><span class="o">.</span><span class="n">basic_consume</span><span class="p">(</span><span class="n">queue</span><span class="o">=</span><span class="s2">&#34;orders&#34;</span><span class="p">,</span> <span class="n">on_message_callback</span><span class="o">=</span><span class="n">callback</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">start_consuming</span><span class="p">()</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># SQS 端：visibility timeout + delete pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">sqs</span><span class="o">.</span><span class="n">receive_message</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">QueueUrl</span><span class="o">=</span><span class="n">queue_url</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">MaxNumberOfMessages</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>        <span class="c1"># batch、對應 prefetch</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">WaitTimeSeconds</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span>            <span class="c1"># long polling</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">VisibilityTimeout</span><span class="o">=</span><span class="mi">60</span><span class="p">,</span>          <span class="c1"># 處理中對其他 consumer 隱藏</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">resp</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;Messages&#34;</span><span class="p">,</span> <span class="p">[]):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="n">process</span><span class="p">(</span><span class="n">msg</span><span class="p">[</span><span class="s2">&#34;Body&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="n">sqs</span><span class="o">.</span><span class="n">delete_message</span><span class="p">(</span>           <span class="c1"># 顯式 delete = ack</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                <span class="n">QueueUrl</span><span class="o">=</span><span class="n">queue_url</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="n">ReceiptHandle</span><span class="o">=</span><span class="n">msg</span><span class="p">[</span><span class="s2">&#34;ReceiptHandle&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="k">pass</span>  <span class="c1"># 不 delete、visibility timeout 後自動回 queue 重試</span></span></span></code></pre></div><p>對應關係：</p>
<ul>
<li>RabbitMQ <code>basic_ack</code> → SQS <code>delete_message</code>：處理成功的訊息要顯式刪除、否則 visibility timeout 後重新可見。「不做事」在 SQS 等於「重試」、在 RabbitMQ 等於「卡住 unacked」。</li>
<li>RabbitMQ <code>prefetch_count</code> → SQS <code>MaxNumberOfMessages</code>（上限 10）+ visibility timeout：併發控制從「broker 限制未 ack 數量」變成「一次 receive 的 batch 大小 + 隱藏時間窗」。</li>
<li>RabbitMQ <code>basic_nack(requeue=False)</code>（丟 DLX）→ SQS redrive policy：失敗不再是 application 主動丟 DLX、而是「達到 maxReceiveCount 次數後 SQS 自動送 DLQ」。</li>
<li>RabbitMQ push 模型（broker 主動推給 consumer）→ SQS pull 模型（consumer 主動 long polling）：consumer loop 結構不同、SQS 沒有 broker 主動推送、要嘛自己 poll、要嘛交給 Lambda event source mapping 代 poll。</li>
</ul>
<p>application 邏輯改動集中在 consumer 的 receive / ack / 重試三段、producer 端從 <code>basic_publish</code> 改成 <code>send_message</code> 相對單純。整體改動量取決於原本用了多少 AMQP 特性、典型情境是 consumer 端 20-40% 改寫。</p>
<h2 id="routing-收斂exchange-沒了靠-sns-fan-out-或多-queue">Routing 收斂：exchange 沒了、靠 SNS fan-out 或多 queue</h2>
<p>Components 維度的核心是 SQS 沒有 exchange、RabbitMQ 的 routing 能力要在 broker 外重建。RabbitMQ 的 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">exchange</a> 在 broker 內承擔分流：一條訊息經 routing key 跟 binding 決定進哪些 queue。SQS 是裸 queue、producer 直接指定 queue、沒有中間分流層。</p>
<table>
  <thead>
      <tr>
          <th>RabbitMQ routing 模式</th>
          <th>SQS 對應方案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Direct（固定 key）</td>
          <td>直接 send 到對應 queue、routing 收斂進 producer 程式碼</td>
      </tr>
      <tr>
          <td>Fanout（廣播）</td>
          <td>SNS topic → 多個 SQS queue 訂閱（SNS-to-SQS fan-out）</td>
      </tr>
      <tr>
          <td>Topic（層級 key 匹配）</td>
          <td>SNS + message filtering（subscription filter policy）</td>
      </tr>
      <tr>
          <td>Headers</td>
          <td>SNS message attribute filtering</td>
      </tr>
  </tbody>
</table>
<p>判讀：</p>
<ul>
<li><strong>Direct exchange + 少數固定 key</strong>：最容易遷。routing 邏輯本來就是「key X 進 queue X」、改成 producer 直接 <code>send_message</code> 到對應 queue url。routing 從 broker 收斂進 application、程式碼多幾行 if/else 或 map 查表。</li>
<li><strong>Fanout（一條訊息給多個 downstream）</strong>：用 SNS-to-SQS。SNS topic 當 fan-out 點、每個 downstream 訂閱一個自己的 SQS queue。Twitch EventSub 就是這個形狀（見 <a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54 Twitch EventSub</a>）：SNS fan-out 到多個 SQS、各 consumer 獨立消費。這比 RabbitMQ fanout exchange 多一層 SNS、但換來 managed 運維。</li>
<li><strong>Topic exchange（複雜層級匹配）</strong>：SNS 的 subscription filter policy 能做 attribute-based 過濾、但表達力不如 AMQP topic 的 <code>*</code> / <code>#</code> 通配。複雜 topic routing 是「不該遷」的訊號（見下節）。</li>
</ul>
<p>關鍵取捨：SQS + SNS 把 RabbitMQ 的單一 broker（routing 內建）拆成兩個 managed 服務（SQS 排隊 + SNS 分流）。好處是各自 managed、壞處是 routing 從宣告式 binding 變成要管 SNS topic + subscription + filter policy 的組合、跨服務除錯多一層。</p>
<h2 id="什麼不該遷保留-rabbitmq-的訊號">什麼不該遷：保留 RabbitMQ 的訊號</h2>
<p>SQS 的 managed 簡潔有代價、三類用法遷過去會損失能力或增加複雜度：</p>
<p><strong>複雜 topic routing</strong>。若 RabbitMQ 重度使用 topic exchange 的 <code>*</code> / <code>#</code> 層級通配、binding 規則數十條、那 routing 的表達力是核心價值。SNS subscription filter 的 attribute 匹配做不到對等表達、勉強遷會把 broker 內的宣告式 routing 拆成散落在 SNS filter policy + application 程式碼的命令式邏輯、維護成本反而上升。GoCardless 用單一 topic exchange 當服務 mesh（見 <a href="/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26 GoCardless Hutch</a>）這類設計、routing 就是架構本身、不該拆。</p>
<p><strong>需要 broker 級 ordering</strong>。RabbitMQ 單 queue 預設 FIFO、consistent hash exchange 還能做 per-key ordering（見 <a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/" data-link-title="3.C28 WeWork：Consistent hash exchange 保證帳戶順序" data-link-desc="WeWork 固定數量 queue &#43; account ID hash 路由、每 queue 一個 worker &#43; exclusive consumer 保 partition-level ordering。">3.C28 WeWork hash ordering</a>）。SQS standard queue <em>無 ordering</em>；要 ordering 只能用 FIFO queue、而 FIFO 吞吐受限（每 MessageGroupId 有序、整體 3000 msg/sec with batching）。若 workload 同時要高吞吐跟嚴格 ordering、SQS FIFO 兩者不可兼得、RabbitMQ 反而更適合。</p>
<p><strong>RPC over messaging（request-reply）</strong>。RabbitMQ 的 reply-to + correlation-id 做同步 RPC 模式、SQS 沒有原生 request-reply、要自己用兩條 queue + correlation 拼、延遲也不適合（SQS 是 task queue 不是低延遲傳輸）。這類用法該考慮 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 的 request-reply 或直接 HTTP。</p>
<h2 id="migration-結構漸進-cutover">Migration 結構：漸進 cutover</h2>
<p>operational redesign 的 cutover 走 dual-run、按 queue（不是按整個叢集）漸進切、每步都保留回退邊界：</p>
<ol>
<li><strong>Phase 0：scope 盤點</strong> — 列出所有 exchange / queue / binding、標註 routing 模式（direct / fanout / topic）跟 ordering 需求。判斷哪些 queue 適合遷（簡單 routing、at-least-once 夠用）、哪些保留（複雜 topic、需 broker ordering、RPC）。</li>
<li><strong>Phase 1：SQS / SNS 基礎建設</strong> — 對適合遷的 queue 建對應 SQS queue + DLQ（設 redrive policy + maxReceiveCount）、fanout 場景建 SNS topic + subscription。設好 IAM policy、visibility timeout 對齊 consumer 最大處理時間。</li>
<li><strong>Phase 2：consumer 改寫 + dual-consume</strong> — application consumer 改成 SQS pull 模型（或 Lambda event source）、先讓新 consumer 跟舊 RabbitMQ consumer <em>並存</em>、producer 暫時雙寫到 RabbitMQ + SQS、驗證 SQS 端處理正確。</li>
<li><strong>Phase 3：producer cutover</strong> — 逐 queue 把 producer 從 RabbitMQ 切到 SQS / SNS、停掉該 queue 的雙寫。這步可逆：發現問題切回 RabbitMQ producer 即可。</li>
<li><strong>Phase 4：下線 RabbitMQ queue</strong> — 確認某 queue 在 SQS 穩定運行、且 RabbitMQ 端該 queue 已排空、才停掉 RabbitMQ 對應的 exchange / queue。這是不可逆步驟、不該過早。</li>
<li><strong>Phase 5：叢集退役</strong> — 所有適合遷的 queue 都切完、RabbitMQ 只剩保留的複雜 routing queue（或完全清空）、才縮編或退役叢集。</li>
</ol>
<p>漸進 cutover 的關鍵是 <em>按 queue 切、不按叢集切</em>。每條 queue 是獨立的遷移單元、各自走 Phase 2-4、互不阻塞。複雜 routing 的 queue 可以永遠留在 RabbitMQ、形成 RabbitMQ + SQS 長期共存的混合架構。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1dlx-改-redrive-policy重試語意不對等">Case 1：DLX 改 redrive policy，重試語意不對等</h3>
<p><strong>徵兆</strong>：RabbitMQ 端用 DLX 配 message TTL 做「延遲重試 + 多層 escalation」（如 <a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25 Indeed Delay + DLQ</a> 的三層 retry）；遷到 SQS 後發現 redrive policy 只能設「失敗 N 次直接進 DLQ」、做不出原本的延遲重試階梯。</p>
<p><strong>根因</strong>：RabbitMQ DLX 是 routing 機制、能配 TTL + 多個中繼 queue 組出任意 escalation 拓樸；SQS redrive policy 是單一規則（maxReceiveCount 到了就送 DLQ）、沒有中繼層。兩者都叫「DLQ」、但 RabbitMQ 的是可編程 routing、SQS 的是固定計數。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>指數退避用 visibility timeout 做</strong>：失敗時 application 主動 <code>ChangeMessageVisibility</code> 延長隱藏時間、實現退避、而不是依賴 DLX TTL。</li>
<li><strong>多層 escalation 用多 queue 串</strong>：若真需要 N 層、建 N 個 SQS queue、application 失敗時把訊息 send 到下一層 queue、每層設不同 redrive policy。複雜度比 DLX 高、是「複雜 routing 不該遷」的訊號之一。</li>
<li><strong>接受簡化</strong>：多數 task queue 的重試需求是「重試幾次後進 DLQ 人工檢視」、SQS redrive policy 直接對應、不需要重建 escalation 階梯。</li>
</ol>
<h3 id="case-2prefetch-改-batch--visibility併發控制行為變了">Case 2：prefetch 改 batch + visibility，併發控制行為變了</h3>
<p><strong>徵兆</strong>：RabbitMQ 端 <code>prefetch_count=1</code> 確保 worker 一次只處理一條（公平派發、慢任務不囤積）；遷 SQS 後 consumer 一次 <code>receive_message</code> 領 10 條、其中一條慢任務拖累整批、且 visibility timeout 對整批同時計時、處理到一半超時導致前面已處理的訊息重複。</p>
<p><strong>根因</strong>：RabbitMQ prefetch 是 per-message 的未 ack 上限、broker 逐條控制；SQS 的 batch 是一次領多條、visibility timeout 對 batch 內每條<em>獨立</em>計時、但 application 若同步處理整批、慢的那條會讓後面的訊息在處理前就接近超時。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>慢任務用 batch size 1</strong>：對等 RabbitMQ <code>prefetch=1</code> 就設 <code>MaxNumberOfMessages=1</code>、一次領一條、避免批內互相拖累。</li>
<li><strong>visibility timeout 設成略高於最大處理時間</strong>：Capital One 的 SQS + Lambda 實務明示這點（見 <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>）— timeout 太短重複處理、太長延遲 retry。長任務處理中主動 <code>ChangeMessageVisibility</code> 續期。</li>
<li><strong>逐條 delete 不等整批</strong>：每條處理完立刻 <code>delete_message</code>、不要等整批做完才一起刪、降低整批超時導致部分重複的風險。</li>
</ol>
<h3 id="case-3fanout-改-sns-to-sqs漏訂閱導致部分-downstream-收不到">Case 3：fanout 改 SNS-to-SQS，漏訂閱導致部分 downstream 收不到</h3>
<p><strong>徵兆</strong>：RabbitMQ fanout exchange 廣播到所有 binding queue、新增 downstream 只要 bind 上去就收得到；遷成 SNS-to-SQS 後、某個新 downstream 的 SQS queue 沒訂閱到 SNS topic、或 subscription filter policy 設錯、導致該 downstream 靜默漏訊息。</p>
<p><strong>根因</strong>：RabbitMQ fanout 的廣播是 broker 內建語意、binding 一建立就生效；SNS-to-SQS 的 fan-out 是「每個 downstream 各自建 SQS queue + 訂閱 SNS topic + 設 queue policy 允許 SNS 投遞」三步、任一步漏掉或 filter policy 寫錯就靜默漏。多一層服務 = 多一層配置出錯點。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>訂閱關係 IaC 管理</strong>：SNS subscription + SQS queue policy 用 Terraform / CloudFormation 宣告、避免手動建漏。</li>
<li><strong>驗證 fan-out 完整性</strong>：cutover 前發測試訊息、確認<em>每個</em> downstream queue 都收到（對照 RabbitMQ 端 binding 清單逐一核對）。</li>
<li><strong>filter policy 預設寬鬆</strong>：除非明確要過濾、subscription 不設 filter policy（全收）、避免「以為廣播、實際被 filter 擋掉」。</li>
</ol>
<h3 id="case-4訊息超過-256kbsqs-拒收">Case 4：訊息超過 256KB，SQS 拒收</h3>
<p><strong>徵兆</strong>：RabbitMQ 對單訊息大小無硬性低上限（受 frame_max / memory 限制、實務常見 MB 級 payload）；遷 SQS 後、原本能傳的大 payload 訊息被拒、SendMessage 報 message 超過 256KB 上限。</p>
<p><strong>根因</strong>：SQS 單訊息上限 256KB（含 message attribute）。RabbitMQ 沒有這個低上限、application 可能習慣直接把大 payload（如完整文件、序列化大物件）塞進訊息體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Claim-check pattern</strong>：大 payload 存 S3、訊息只放 S3 物件的引用（key / presigned URL）、consumer 收到後從 S3 取。FINRA 的大檔案處理是 S3 event notification → SQS（檔案上傳 S3 後由 S3 推通知），結果同樣讓訊息只帶 S3 物件引用，但機制是 S3 觸發、不是 producer 主動 offload（見 <a href="/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/" data-link-title="3.C53 FINRA：S3 → SQS notification 大檔上傳" data-link-desc="FINRA 金融監管、broker 上傳大檔、S3 → SQS notification → LFS、KMS &#43; bucket policy &#43; queue policy 三層稽核。">3.C53 FINRA Large File</a>）。</li>
<li><strong>SQS Extended Client Library</strong>：AWS 官方 library 自動把超過上限的 payload 透明存 S3、訊息存指標、consumer 端自動取回、application 程式碼幾乎不改。</li>
<li><strong>盤點 payload 大小分佈</strong>：Phase 0 audit 時量測現有訊息大小、超 256KB 的比例決定是否需要 claim-check、避免 cutover 後才發現大量訊息被拒。</li>
</ol>
<h3 id="case-5ordering-從-rabbitmq-到-sqs-fifo吞吐撞天花板">Case 5：ordering 從 RabbitMQ 到 SQS FIFO，吞吐撞天花板</h3>
<p><strong>徵兆</strong>：RabbitMQ 單 queue 提供順序消費、原本靠這個保證同一筆訂單的事件有序處理；遷 SQS standard queue 後 ordering 消失、改用 SQS FIFO queue 恢復 ordering、但吞吐從原本的數萬 msg/sec 掉到 3000 msg/sec 上限、隊列堆積。</p>
<p><strong>根因</strong>：SQS standard queue 無 ordering（為了吞吐跟可用性的設計取捨）；FIFO queue 提供 per-MessageGroupId 有序 + 去重、但整體吞吐上限 3000 msg/sec（with batching）。RabbitMQ 單 queue 的有序消費吞吐遠高於此。SQS FIFO 的吞吐上限是 300 TPS（不 batch）／ 3000 TPS（batch，後者為通用 SQS FIFO 數值）。Twilio 的 webhook buffer 文件特別點出 FIFO 300 TPS 這個限制（見 <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook</a>）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>重新審視 ordering 粒度</strong>：用 MessageGroupId 把 ordering 限縮到真正需要的範圍（如 per-訂單、per-用戶）、不同 group 平行處理、整體吞吐 = group 數 × per-group 吞吐、繞過單 queue 3000 上限。</li>
<li><strong>拆分 ordered 跟 unordered 流量</strong>：只有真需要 ordering 的訊息走 FIFO、其餘走 standard queue 拿高吞吐。多數 workload 只有一小部分需要嚴格 ordering。</li>
<li><strong>ordering 是「不該遷」的硬訊號</strong>：若 workload 整體都需要高吞吐 + 嚴格 ordering、SQS FIFO 兩者不可兼得、保留 RabbitMQ 或考慮 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（per-partition ordering + 高吞吐）。</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RabbitMQ（self-managed EC2）</th>
          <th>AWS SQS（managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>叢集 baseline</td>
          <td>3 broker（HA）+ EBS</td>
          <td>無實例</td>
      </tr>
      <tr>
          <td>運維 FTE</td>
          <td>0.5-1 FTE</td>
          <td>~0.1 FTE（IAM / alarm 配置）</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>EC2 instance hour + EBS + 流量</td>
          <td>per-request（每百萬 request）+ 跨 region 流量</td>
      </tr>
      <tr>
          <td>吞吐上限</td>
          <td>受 broker 規格 / 網路限制</td>
          <td>standard 近乎無限、FIFO 3000 msg/sec</td>
      </tr>
      <tr>
          <td>Ordering</td>
          <td>單 queue 有序、consistent hash per-key</td>
          <td>standard 無、FIFO per-group</td>
      </tr>
      <tr>
          <td>Routing</td>
          <td>broker 內建 exchange</td>
          <td>無（需 SNS / application）</td>
      </tr>
      <tr>
          <td>訊息大小上限</td>
          <td>受 frame_max / memory（MB 級可行）</td>
          <td>256KB（超過用 S3 claim-check）</td>
      </tr>
      <tr>
          <td>監控延遲</td>
          <td>即時（Management UI）</td>
          <td>CloudWatch approximate、分鐘級</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：低到中吞吐、簡單 routing、AWS 生態的 task queue、SQS 在運維成本上顯著划算（FTE 從 0.5-1 降到約 0.1）。高吞吐 + 嚴格 ordering、或重度 exchange routing 的 workload、SQS 的 per-request 成本跟能力限制可能讓 RabbitMQ（或 Kafka）反而合適。SQS 的 cost 是用量驅動、流量大時 per-request 費用要納入評估、對照 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是常見終態">混合架構是常見終態</h3>
<p>多數遷移不會把 RabbitMQ 完全清空。簡單 task queue 遷 SQS、複雜 topic routing / broker ordering / RPC 留 RabbitMQ、形成長期共存：</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">[簡單 task queue / fanout]              [複雜 topic routing / RPC / ordering]
</span></span><span class="line"><span class="ln">2</span><span class="cl">        AWS SQS / SNS                              RabbitMQ
</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">   Lambda / ECS consumer                    自管叢集（縮編後）</span></span></code></pre></div><p>按 queue 漸進切的結果就是混合架構 — 不需要為了「遷乾淨」勉強把不適合的 queue 也搬過去。</p>
<h3 id="跟-rabbitmq--kafka-的對照">跟 RabbitMQ → Kafka 的對照</h3>
<p>RabbitMQ 還有另一條遷移路徑是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">RabbitMQ → Kafka</a>（work queue → event streaming）。兩條路的差異：遷 SQS 是 <em>交出運維、能力對等簡化</em>（仍是 task queue）；遷 Kafka 是 <em>換 paradigm、要 replay / 高吞吐 streaming</em>（從任務隊列變 event log）。選哪條看的是「想擺脫運維」還是「需要 streaming 能力」、不是同一個決策。</p>
<h3 id="跟前面-migration-playbook-的結構對照">跟前面 migration playbook 的結構對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>主導差異維度</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kafka ↔ NATS</td>
          <td>Paradigm（高）</td>
          <td>partial + 混合</td>
      </tr>
      <tr>
          <td>RabbitMQ → SQS（本篇）</td>
          <td>Operational（高）</td>
          <td>Type C operational hybrid</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：兩篇都是 message queue 跨 vendor、但主導差異維度不同 — Kafka ↔ NATS 卡在 paradigm（不同抽象層）、RabbitMQ → SQS 卡在 operational（運維責任轉移）。結構由主導維度決定、不是 universal phased playbook。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <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></li>
<li>平行 vendor：<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> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>平行 migration playbook：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>引用案例：<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> / <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> / <a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54 Twitch EventSub</a> / <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Streams XCLAIM / PEL 失敗接管與 Cluster 影響</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/xclaim-pel-recovery/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/xclaim-pel-recovery/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams&lt;/a> overview 的 implementation-layer deep article。Overview 給選型與最短路徑、本文聚焦「consumer crash 之後、卡在 PEL 的訊息怎麼回到處理流程」這條 implementation flow。實機輸出來自 &lt;code>redis:7&lt;/code>（7.4.9）單節點。&lt;/p>&lt;/blockquote>
&lt;h2 id="consumer-crash-後訊息卡在哪裡">consumer crash 後、訊息卡在哪裡&lt;/h2>
&lt;p>Redis Streams 的 consumer group 設計是「先投遞、後 ack」：&lt;code>XREADGROUP&lt;/code> 把 entry 投給某個 consumer 的同時、entry 進入該 group 的 &lt;strong>PEL（Pending Entries List）&lt;/strong>、標記為「已投遞、未確認」。consumer 處理完才呼叫 &lt;code>XACK&lt;/code> 把 entry 移出 PEL。這一段「已投遞未 ack」的視窗、是 Redis Streams 提供 at-least-once 的全部依據。&lt;/p>
&lt;p>問題在於 consumer crash 時機落在這個視窗內。consumer 已經拿到訊息、PEL 已經記了它的名字、但它在 ack 之前就死了。Redis 沒有 broker 級的「重新投遞」背景程序——不像 RabbitMQ consumer 斷線後 unacked 訊息自動 requeue。Redis 把這筆訊息留在 PEL、owner 仍是那個死掉的 consumer、然後什麼都不做。要讓這筆訊息回到處理流程、只有 application 主動呼叫 &lt;code>XCLAIM&lt;/code> 或 &lt;code>XAUTOCLAIM&lt;/code> 改寫 owner。&lt;/p>
&lt;p>這就是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &amp;#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &amp;#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &amp;#43; retry &amp;#43; DLQ、idempotent processing。">Bitso 自建 Reliable Streams 抽象&lt;/a> 揭露的核心事實：Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。本文展開的就是這個責任的具體形狀——PEL 怎麼累積、怎麼判讀、接管機制怎麼運作、以及哪些操作會讓接管失效。&lt;/p>
&lt;h2 id="pel-機制xreadgroup-進xack-出">PEL 機制：XREADGROUP 進、XACK 出&lt;/h2>
&lt;p>PEL 是 per-group 的結構、記錄每個 entry 的四個欄位：entry ID、目前 owner consumer、idle time（距上次投遞的毫秒數）、delivery count（被投遞過幾次）。先用實機輸出建立基礎。寫入 5 筆、建 group、兩個 consumer 各讀一部分：&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">$ redis-cli XADD mystream &lt;span class="s1">&amp;#39;*&amp;#39;&lt;/span> event order_1 amount &lt;span class="m">100&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">1781584105202-0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># ... order_2 ~ order_5、各得遞增 entry ID&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">$ redis-cli XGROUP CREATE mystream g1 &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">$ redis-cli XREADGROUP GROUP g1 c1 COUNT &lt;span class="m">3&lt;/span> STREAMS mystream &lt;span class="s1">&amp;#39;&amp;gt;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># c1 拿到 order_1 / order_2 / order_3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">$ redis-cli XREADGROUP GROUP g1 c2 COUNT &lt;span class="m">10&lt;/span> STREAMS mystream &lt;span class="s1">&amp;#39;&amp;gt;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># c2 拿到 order_4 / order_5&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>'&amp;gt;'&lt;/code> 代表「只取從未投遞給本 group 的新訊息」。投遞後這 5 筆全進 PEL。&lt;code>XPENDING&lt;/code> 的 summary 形式給總覽：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a> overview 的 implementation-layer deep article。Overview 給選型與最短路徑、本文聚焦「consumer crash 之後、卡在 PEL 的訊息怎麼回到處理流程」這條 implementation flow。實機輸出來自 <code>redis:7</code>（7.4.9）單節點。</p></blockquote>
<h2 id="consumer-crash-後訊息卡在哪裡">consumer crash 後、訊息卡在哪裡</h2>
<p>Redis Streams 的 consumer group 設計是「先投遞、後 ack」：<code>XREADGROUP</code> 把 entry 投給某個 consumer 的同時、entry 進入該 group 的 <strong>PEL（Pending Entries List）</strong>、標記為「已投遞、未確認」。consumer 處理完才呼叫 <code>XACK</code> 把 entry 移出 PEL。這一段「已投遞未 ack」的視窗、是 Redis Streams 提供 at-least-once 的全部依據。</p>
<p>問題在於 consumer crash 時機落在這個視窗內。consumer 已經拿到訊息、PEL 已經記了它的名字、但它在 ack 之前就死了。Redis 沒有 broker 級的「重新投遞」背景程序——不像 RabbitMQ consumer 斷線後 unacked 訊息自動 requeue。Redis 把這筆訊息留在 PEL、owner 仍是那個死掉的 consumer、然後什麼都不做。要讓這筆訊息回到處理流程、只有 application 主動呼叫 <code>XCLAIM</code> 或 <code>XAUTOCLAIM</code> 改寫 owner。</p>
<p>這就是 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 自建 Reliable Streams 抽象</a> 揭露的核心事實：Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。本文展開的就是這個責任的具體形狀——PEL 怎麼累積、怎麼判讀、接管機制怎麼運作、以及哪些操作會讓接管失效。</p>
<h2 id="pel-機制xreadgroup-進xack-出">PEL 機制：XREADGROUP 進、XACK 出</h2>
<p>PEL 是 per-group 的結構、記錄每個 entry 的四個欄位：entry ID、目前 owner consumer、idle time（距上次投遞的毫秒數）、delivery count（被投遞過幾次）。先用實機輸出建立基礎。寫入 5 筆、建 group、兩個 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">$ redis-cli XADD mystream <span class="s1">&#39;*&#39;</span> event order_1 amount <span class="m">100</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">1781584105202-0
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># ... order_2 ~ order_5、各得遞增 entry ID</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">$ redis-cli XGROUP CREATE mystream g1 <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">OK
</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">$ redis-cli XREADGROUP GROUP g1 c1 COUNT <span class="m">3</span> STREAMS mystream <span class="s1">&#39;&gt;&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># c1 拿到 order_1 / order_2 / order_3</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">$ redis-cli XREADGROUP GROUP g1 c2 COUNT <span class="m">10</span> STREAMS mystream <span class="s1">&#39;&gt;&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># c2 拿到 order_4 / order_5</span></span></span></code></pre></div><p><code>'&gt;'</code> 代表「只取從未投遞給本 group 的新訊息」。投遞後這 5 筆全進 PEL。<code>XPENDING</code> 的 summary 形式給總覽：</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">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">5</span>                  <span class="c1"># PEL 總數</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">1781584105202-0    <span class="c1"># 最小 pending ID</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584105578-0    <span class="c1"># 最大 pending ID</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">c1                 <span class="c1"># 各 consumer 的 pending 數</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="m">3</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">c2
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="m">2</span></span></span></code></pre></div><p>5 筆全在 PEL、c1 扛 3 筆、c2 扛 2 筆。展開形式 <code>XPENDING &lt;key&gt; &lt;group&gt; - + &lt;count&gt;</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">1781584105202-0  c1  <span class="m">6318</span>  <span class="m">1</span>    <span class="c1"># entry ID / owner / idle ms / delivery count</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">1781584105278-0  c1  <span class="m">6318</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584105373-0  c1  <span class="m">6318</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">1781584105466-0  c2  <span class="m">6224</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">1781584105578-0  c2  <span class="m">6224</span>  <span class="m">1</span></span></span></code></pre></div><p><code>idle</code> 是 6318ms（距投遞已過 6.3 秒）、<code>delivery count</code> 都是 1（只投過一次）。這兩個數字是後面接管決策的核心輸入：idle 判斷「owner 是不是死了」、delivery count 判斷「這筆是不是 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message</a>」。</p>
<p><code>XACK</code> 把處理完的 entry 移出 PEL：</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">$ redis-cli XACK mystream g1 1781584105202-0
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="m">1</span>                  <span class="c1"># 成功移除 1 筆</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">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="m">4</span>                  <span class="c1"># PEL 剩 4 筆</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">1781584105578-0
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">c1
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">2</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">c2
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="m">2</span></span></span></code></pre></div><p>PEL 從 5 降到 4。判讀原則固定：<strong>PEL 持續成長就是 consumer 健康訊號異常</strong>——不是 crash 沒 ack、就是處理速度跟不上、再不然是 ACK 程式碼漏寫。三者用 idle time 區分：crash 的 entry idle 會單調成長、處理慢的 idle 在 timeout 附近震盪、漏 ACK 的 entry delivery count 停在 1 但 idle 無上限成長。</p>
<h2 id="xclaim-與-xautoclaim改寫-owner-的兩條路">XCLAIM 與 XAUTOCLAIM：改寫 owner 的兩條路</h2>
<p>接管的本質是把 PEL entry 的 owner 從死掉的 consumer 改成活著的 consumer。<code>XCLAIM</code> 是手動指定 entry ID 接管、<code>XAUTOCLAIM</code> 是自動掃 idle 超過門檻的 entry 批次接管。兩者都接受 min-idle-time 參數當安全閥。</p>
<p><code>XCLAIM &lt;key&gt; &lt;group&gt; &lt;new-consumer&gt; &lt;min-idle-time&gt; &lt;id...&gt;</code>：把指定 entry 改判給新 consumer、條件是該 entry 的 idle 已達 min-idle-time。下面用 min-idle-time 0（無條件接管）把 c1 的一筆轉給 c3：</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">$ redis-cli XCLAIM mystream g1 c3 <span class="m">0</span> 1781584105278-0
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">event
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">order_2
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">amount
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="m">200</span>               <span class="c1"># 回傳被接管 entry 的完整內容</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">1781584105278-0  c3  <span class="m">66</span>     <span class="m">2</span>    <span class="c1"># owner 變 c3、idle 歸零(66ms)、delivery count 升到 2</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">1781584105373-0  c1  <span class="m">14590</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">1781584105466-0  c2  <span class="m">14496</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105578-0  c2  <span class="m">14496</span>  <span class="m">1</span></span></span></code></pre></div><p>接管後三件事同時發生：owner 改成 c3、idle 重置（剛 claim、66ms）、<strong>delivery count 從 1 升到 2</strong>。delivery count 自增是接管機制留下的審計軌跡——一筆訊息 delivery count 累積到 5、10、代表它反覆被接管又反覆沒處理完、這就是 poison message 的訊號、該路由到隔離區（見 <a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">recovery semantics</a> 與 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>）。</p>
<p><code>XAUTOCLAIM &lt;key&gt; &lt;group&gt; &lt;new-consumer&gt; &lt;min-idle-time&gt; &lt;start-id&gt;</code>（Redis 6.2+）省掉「先 XPENDING 找 ID、再逐筆 XCLAIM」兩步、一次掃描接管：</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">$ redis-cli XAUTOCLAIM mystream g1 c3 <span class="m">0</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">0-0                          <span class="c1"># 下次掃描的 cursor（0-0 代表掃完一輪）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">1781584105278-0 ...          <span class="c1"># 接管的 entry 內容（order_2）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">1781584105373-0 ...          <span class="c1"># order_3</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">1781584105466-0 ...          <span class="c1"># order_4</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">1781584105578-0 ...          <span class="c1"># order_5</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>                <span class="c1"># 第三個回傳值：已從 stream 刪除的 entry ID 清單</span>
</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">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105578-0
</span></span><span class="line"><span class="ln">13</span><span class="cl">c3                           <span class="c1"># 全部 4 筆 owner 變 c3</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="m">4</span></span></span></code></pre></div><p>一次呼叫把整個 group 的 idle 訊息全歸到 c3。<code>XAUTOCLAIM</code> 是 consumer crash 後接管的主力——consumer 在啟動或處理迴圈裡固定跑一輪 <code>XAUTOCLAIM</code>、把孤兒訊息撿回來。回傳的 cursor 支援分批（一次掃不完時帶 cursor 續掃）、第三個回傳值（被刪 entry 清單）對應後面 MAXLEN 修剪的故障。</p>
<h2 id="min-idle-time防止活-consumer-被搶單">min-idle-time：防止活 consumer 被搶單</h2>
<p>min-idle-time 不是裝飾參數、是接管機制的安全閥：它要求「只有 idle 超過門檻的 entry 才能被接管」。沒有這個門檻、兩個 consumer 會互相搶對方正在處理的訊息。</p>
<p>驗證搶單防護——剛被 c3 claim 的訊息 idle 很低、用 60 秒門檻去 claim 會落空：</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">$ redis-cli XCLAIM mystream g1 c4 <span class="m">60000</span> 1781584105278-0
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>               <span class="c1"># 回空：該 entry idle 未達 60000ms、c4 搶不到</span></span></span></code></pre></div><p>回空陣列代表 claim 失敗、owner 不變、訊息留在 c3 手上。這就是 min-idle-time 的作用：<strong>門檻 = 我願意相信 owner consumer 還活著的最長時間</strong>。</p>
<p>門檻設定是接管設計的核心取捨、沒有通用值、由訊息處理時間分佈決定。門檻設太短、正常處理中的訊息被當成孤兒搶走、變成多 consumer 重複處理同一筆。門檻設太長、真正 crash 的訊息要等很久才有人接管、recovery 延遲拉高。<a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness 的 event-driven 案例</a> 正是用 XAUTOCLAIM 重派來解 head-of-line blocking（慢訊息阻塞 consumer 進度）、並自設 redelivery 策略避免上述反覆搶單。實務基準是「門檻 &gt; p99 處理時間 + 安全係數」：若單筆處理 p99 是 2 秒、門檻設 30-60 秒、確保只有真的死掉（遠超正常處理時間）的 owner 才被接管。</p>
<p>接管後仍需 application 層去重。XCLAIM 改寫 owner、不代表原 consumer 真的沒處理完——它可能正在 ack 的瞬間被 claim、結果兩邊都處理一次。at-least-once 的去重責任永遠在 application、靠 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 兜底、這跟接管門檻設多準無關。</p>
<h2 id="memory-與-retentionmaxlen--xtrim-的取捨">Memory 與 retention：MAXLEN / XTRIM 的取捨</h2>
<p>Stream 是 append-only、不主動丟資料、佔用的 Redis 記憶體單調成長。retention 的唯一旋鈕是修剪：<code>MAXLEN</code>（保留最近 N 筆）或 <code>MINID</code>（保留 ID 大於某值的 entry）。可以在 <code>XADD</code> 寫入時順帶修剪、也可以用 <code>XTRIM</code> 獨立執行。</p>
<p>精確修剪 <code>MAXLEN =</code> 跟近似修剪 <code>MAXLEN ~</code> 的差別在性能。stream 內部是 radix tree of macro-nodes（每個 node 打包多筆 entry）。精確修剪要拆 node 才能剛好留 N 筆、近似修剪只刪「整個可以丟掉的 node」、留下的筆數會略多於 N、但省掉拆 node 的開銷。<code>~</code> 是 production 預設、<code>=</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">$ redis-cli XADD mystream MAXLEN <span class="s1">&#39;~&#39;</span> <span class="m">1000</span> <span class="s1">&#39;*&#39;</span> event order_6 amount <span class="m">600</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">1781584152570-0             <span class="c1"># 近似修剪：超過 ~1000 才整 node 刪</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli XADD mystream MAXLEN <span class="s1">&#39;=&#39;</span> <span class="m">3</span> <span class="s1">&#39;*&#39;</span> event order_7 amount <span class="m">700</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584152871-0
</span></span><span class="line"><span class="ln">5</span><span class="cl">$ redis-cli XLEN mystream
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="m">3</span>                           <span class="c1"># 精確修剪到剛好 3 筆</span></span></span></code></pre></div><p>stream 不受 <code>maxmemory-policy</code> eviction 管理——一般 key 在記憶體壓力下會被 evict、stream entry 不會。這代表 stream 是「只進不出、除非主動修剪」的記憶體成長源。<a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 把 Redis 當長期事件儲存、最終因成本與延遲退場</a> 就是沒設修剪上限的反例（該案例涵蓋 Redis 事件儲存整體、Stream 是其中一塊）：事件量每週以 GB 成長、AOF fsync 與 EBS I/O 變成 latency 痛點、最終退回 PostgreSQL。判讀訊號是 <code>MEMORY USAGE mystream</code> 對比實例 <code>maxmemory</code>、超過預算就調低 MAXLEN。</p>
<h2 id="故障演練">故障演練</h2>
<h3 id="case-1consumer-crash-後-pel-訊息卡死沒人接">Case 1：consumer crash 後 PEL 訊息卡死沒人接</h3>
<p><strong>徵兆</strong>：<code>XPENDING</code> 總數持續成長、某個 consumer 的 pending 數停在固定值不降、那些 entry 的 idle time 單調往上爬（幾分鐘、幾小時）、業務端對應的訊息「進了 stream 但沒被處理」。</p>
<p><strong>根因</strong>：consumer 進程 crash（OOM kill / 部署滾動 / panic）、留下的 PEL entry owner 仍是死掉的 consumer。Redis 不會自動重投——沒有任何背景程序會碰這些 entry。它們會永遠卡在 PEL、直到有人主動接管。新啟動的 consumer 用 <code>XREADGROUP ... '&gt;'</code> 只會拿到「從未投遞」的新訊息、不會碰到前任留下的孤兒。</p>
<p><strong>修法</strong>：consumer 啟動時跟處理迴圈裡固定跑 <code>XAUTOCLAIM</code>、把超過 idle 門檻的孤兒撿回來：</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 週期性執行、min-idle-time 設 60s</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ redis-cli XAUTOCLAIM mystream g1 self_consumer_id <span class="m">60000</span> <span class="m">0</span></span></span></code></pre></div><ol>
<li><strong>min-idle-time 設成 &gt; p99 處理時間 + 安全係數</strong>：避免把處理中的訊息誤判成孤兒（接 Case 2）。</li>
<li><strong>用回傳 cursor 分批掃</strong>：PEL 大時一次 <code>XAUTOCLAIM</code> 不掃完、帶 cursor 續掃、避免單次 block 太久。</li>
<li><strong>接管後檢查 delivery count</strong>：超過閾值（如 5）的 entry 不再處理、路由到 DLQ（Redis Streams 沒原生 DLQ、Bitso 自建一個 stream 當 DLQ）。</li>
<li><strong>監控 PEL 最大 idle</strong>：alert 設在「最老 pending entry 的 idle 超過 N 倍接管門檻」、代表接管機制本身停了。</li>
</ol>
<h3 id="case-2min-idle-time-設太短活-consumer-被搶單">Case 2：min-idle-time 設太短、活 consumer 被搶單</h3>
<p><strong>徵兆</strong>：同一筆訊息被多個 consumer 處理、下游出現重複副作用（重複扣款、重複發信）；<code>XPENDING</code> 展開看到某些 entry 的 delivery count 異常高（5、10+）但 stream 流量正常、沒有 consumer crash。</p>
<p><strong>根因</strong>：接管門檻低於正常處理時間。consumer A 拿到一筆要處理 10 秒的訊息、門檻設了 5 秒、consumer B 跑 <code>XAUTOCLAIM</code> 時這筆 idle 已過 5 秒、B 把還在 A 手上處理的訊息搶走、兩邊都處理一次。這是接管門檻設計的通用競態——一筆慢訊息被反覆搶、delivery count 暴衝、卻沒人真正完成。（<a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness 案例</a> 用 XAUTOCLAIM 重派解 head-of-line blocking 時、正是靠門檻與 redelivery 策略避開這種搶單。）</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>量測真實處理時間分佈、門檻設 &gt; p99</strong>：先用 metric 抓單筆處理 p50 / p99、門檻設 p99 的數倍。</li>
<li><strong>delivery count 當搶單偵測器</strong>：同一 entry delivery count 快速成長、代表它在被搶來搶去、調高門檻或隔離該訊息。</li>
<li><strong>idempotency 兜底</strong>：門檻再準也防不了「ack 瞬間被 claim」的競態、application 層去重是最後防線、不可省（見 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>）。</li>
</ol>
<h3 id="case-3maxlen-修剪掉-pel-內還沒-ack-的訊息">Case 3：MAXLEN 修剪掉 PEL 內還沒 ack 的訊息</h3>
<p><strong>徵兆</strong>：<code>XPENDING</code> 顯示某些 entry 仍 pending、但 <code>XCLAIM</code> 接管它時拿不到內容；consumer 接手後發現訊息 body 是空的、無法處理、又無法判斷該不該 ack。</p>
<p><strong>根因</strong>：<strong>修剪只看 entry ID 的新舊、不看它在不在 PEL</strong>。<code>XTRIM MAXLEN</code> 把最舊的 entry 從 stream 物理刪除、即使這些 entry 還在某個 group 的 PEL 裡等 ack。PEL 只記 entry ID、不存 body；body 存在 stream 本體。entry 被 trim 掉、PEL 還記得這個 ID、但 body 已經不存在了。實機驗證——4 筆全在 PEL、把 stream 修剪到剩 2 筆：</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">$ redis-cli XLEN mystream
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="m">5</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="m">4</span>                           <span class="c1"># 4 筆未 ack 在 PEL</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">$ redis-cli XTRIM mystream MAXLEN <span class="m">2</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="m">3</span>                           <span class="c1"># 刪掉 3 筆（含 PEL 內的未 ack entry）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ redis-cli XLEN mystream
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">2</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105278-0  c3  <span class="m">19307</span>  <span class="m">3</span>   <span class="c1"># PEL 還記得這些 ID</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">1781584105373-0  c3  <span class="m">19307</span>  <span class="m">2</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">1781584105466-0  c3  <span class="m">19307</span>  <span class="m">2</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">1781584105578-0  c3  <span class="m">19307</span>  <span class="m">2</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">$ redis-cli XCLAIM mystream g1 c5 <span class="m">0</span> 1781584105278-0
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>               <span class="c1"># 接管成功改 owner、但 entry body 已被 trim、拿不到內容</span></span></span></code></pre></div><p>PEL 還有 4 筆記錄、但對應的 body 已從 stream 消失。<code>XCLAIM</code> 接管這種 entry、改得了 owner、拿不到 body——這是訊息靜默遺失。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>修剪上限要 &gt; 處理 backlog 深度</strong>：MAXLEN / 流入速率 = 訊息在被修剪前的最長存活時間、這個時間要遠大於「最慢 consumer 清空 backlog 的時間」。</li>
<li><strong>修剪前檢查 PEL 最舊 ID</strong>：自動修剪前比對 <code>XPENDING</code> 的最小 pending ID、確保不會修到還在 PEL 的 entry。</li>
<li><strong>慢 consumer 監控優先於積極修剪</strong>：先解決 consumer 處理太慢導致 PEL 積壓的根因、再談用小 MAXLEN 壓記憶體；倒過來只會修掉未 ack 訊息。</li>
<li><strong>MINID 修剪比 MAXLEN 安全</strong>：MINID 用時間/業務邊界（如「保留 24 小時內」）、比 MAXLEN 的「保留 N 筆」更容易保證涵蓋未 ack 視窗。</li>
</ol>
<h3 id="case-4redis-cluster-對單-stream-的-shard-限制">Case 4：Redis Cluster 對單 stream 的 shard 限制</h3>
<p><strong>徵兆</strong>：stream 流量成長到單 node 容量上限、想像 Kafka 那樣「加 partition 分流」、卻發現 Redis Cluster 沒有這個機制；單一 stream key 的全部讀寫永遠打在同一個 node。</p>
<p><strong>根因</strong>：Redis Cluster 用 <code>CRC16(key) % 16384</code> 把 key 映射到 slot、slot 分佈在 node 上。<strong>一個 stream 是一個 key、永遠落在單一 slot、永遠在單一 shard</strong>。Streams 沒有 Kafka partition 那種「同一 topic 切多片、分散到多 broker」的概念。單 stream 的吞吐天花板就是單 node 的天花板。</p>
<p>實機驗證 keyslot 計算（cluster-enabled 節點）：</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">$ redis-cli CLUSTER KEYSLOT stream:orders
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">6139</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli CLUSTER KEYSLOT stream:payments
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="m">3696</span>                        <span class="c1"># 不同 key 落不同 slot、可能在不同 shard</span></span></span></code></pre></div><p><strong>修法</strong>：要分流就在 application 層切多個 stream key（<code>stream:orders:0</code>、<code>stream:orders:1</code> &hellip;）、自己做 partition 路由。若需要某幾個 stream 保證落同一 shard（為了跨 stream 的原子操作或 co-located 處理）、用 hash tag——只有 <code>{}</code> 內的部分參與 CRC16：</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">$ redis-cli CLUSTER KEYSLOT <span class="s1">&#39;{shard1}:stream:orders&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">10271</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli CLUSTER KEYSLOT <span class="s1">&#39;{shard1}:stream:payments&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="m">10271</span>                       <span class="c1"># 同 hash tag、強制落同 slot</span></span></span></code></pre></div><p>兩個不同 key 因為共用 <code>{shard1}</code> hash tag、CRC16 算出同一個 slot 10271、保證在同一 shard。判讀邊界：需要真正的 partition + replication + 跨節點水平擴展、Redis Streams 不是答案、改走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。Redis Streams 的定位是中等規模、單 shard 容量內、不跨節點分片。</p>
<blockquote>
<p>Cluster 多節點分片下的端到端行為（resharding 期間 stream key 隨 slot 搬移、client topology cache）需要多節點環境、本文未實機驗證；slot migration 機制與踩雷見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Re-sharding</a>。</p></blockquote>
<h3 id="case-5failover-後-pel-狀態不一致">Case 5：failover 後 PEL 狀態不一致</h3>
<p><strong>徵兆</strong>：Sentinel / Cluster failover 後（replica 升 primary）、原本在 PEL 的部分訊息「消失」或「重複投遞」；<code>XPENDING</code> 數字跟 failover 前對不上；consumer 接管邏輯撿到不該撿的訊息、或漏撿該撿的。</p>
<p><strong>根因</strong>：Redis 的 replication 是非同步的。primary 上的 <code>XADD</code> / <code>XACK</code> / <code>XCLAIM</code> 先在本地生效、再非同步傳給 replica。failover 那一刻、replica 的 PEL 狀態落後 primary 一個 replication lag 的視窗。新 primary 從它當下的（落後的）PEL 狀態接手：lag 視窗內已 ack 的訊息在新 primary 上仍 pending（重複投遞）、lag 視窗內剛 claim 的 owner 改寫可能丟失（接管邏輯錯亂）。AOF / RDB 持久化只保證單機重啟的恢復、不改變跨 replica 的非同步本質。</p>
<blockquote>
<p>failover 對 PEL 一致性的影響需要多節點 Sentinel / Cluster 環境跨節點觀測、本文未實機驗證；以下依官方 replication 語義與案例敘述判讀。</p></blockquote>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受 at-least-once、靠 idempotency 收斂</strong>：failover 造成的重複投遞跟正常的重複投遞同一性質、application 去重邏輯本來就要處理（見 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>）。</li>
<li><strong>failover 後主動全量 XAUTOCLAIM 對帳</strong>：failover 偵測到後、consumer 跑一輪低門檻 <code>XAUTOCLAIM</code> 重新接管、用 application 端的處理紀錄判斷哪些真的沒處理。</li>
<li><strong>降低 replication lag</strong>：lag 越小、failover 視窗的 PEL 偏差越小；監控 <code>master_repl_offset</code> 與 replica offset 差。</li>
<li><strong>語義誤配風險</strong>：把 Redis Streams 當「不丟訊息的 broker」用、在 failover 邊界會破功——這是 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 語義誤配</a> 的思路、選型時就要認清 Redis Streams 的一致性等級。</li>
</ol>
<h2 id="capacity-與判讀路由">Capacity 與判讀路由</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>判讀訊號</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PEL 深度</td>
          <td><code>XPENDING</code> 總數持續成長</td>
          <td>成長不停 = consumer 健康問題、不是調 MAXLEN 能解</td>
      </tr>
      <tr>
          <td>接管門檻</td>
          <td>delivery count 異常高（搶單）/ 最老 idle 不收斂</td>
          <td>門檻 &gt; p99 處理時間 + 安全係數</td>
      </tr>
      <tr>
          <td>Stream 記憶體</td>
          <td><code>MEMORY USAGE</code> 對比 <code>maxmemory</code></td>
          <td>stream 不被 eviction、唯一旋鈕是 MAXLEN / MINID 修剪</td>
      </tr>
      <tr>
          <td>修剪 vs 未 ack 視窗</td>
          <td>修剪上限 / 流入速率 &lt; backlog 清空時間</td>
          <td>違反就會修掉 PEL 內未 ack 訊息（Case 3）</td>
      </tr>
      <tr>
          <td>單 stream 吞吐</td>
          <td>單 node CPU / memory 打滿、無法加 partition</td>
          <td>達單 shard 天花板 = 該評估 Kafka</td>
      </tr>
  </tbody>
</table>
<p>判讀路由固定三層：先看 PEL 是「整 group 成長」（流入 &gt; 處理、擴 consumer）還是「單 consumer 卡住」（crash、要接管）；接管時先確認 min-idle-time 對得上處理時間分佈、再看 delivery count 篩 poison message；retention 調整前先確認修剪上限涵蓋 PEL 未 ack 視窗。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<p>接管機制是 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計</a> 在 Redis Streams 上的具體落地——consumer 不只是讀訊息的迴圈、還要承擔「撿前任孤兒」的責任。設計 consumer 時把 <code>XAUTOCLAIM</code> 排進處理迴圈、跟 <code>XREADGROUP '&gt;'</code> 並列、不是事後補丁。</p>
<p>知識卡對位：delivery count 超閾值的訊息對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>（Redis Streams 沒原生 DLQ、自建一個 stream 當隔離區）；接管後的去重對應 <a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">recovery semantics</a> 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>（at-least-once 的收斂責任在 application）。</p>
<p>案例延伸：<a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso</a> 把本文這些機制封裝成 Reliable Streams 抽象層 + 自建 DLQ、是「application 層補可靠性」的完整實作參考；<a href="/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/" data-link-title="3.C45 Klaxit：Rust &#43; Redis Streams 處理 Heroku Logplex" data-link-desc="Klaxit carpool 用 Redis Streams 處理 Heroku Logplex 匯流、自動偵測修復平台 perf 問題、6 個月 production Rust。">Klaxit Rust + Logplex</a> 是高吞吐 log ingestion 下 consumer group 分流長時間穩定運轉的範例；接管門檻搶單的反面教訓在 <a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness event-driven</a>。</p>
<p>選型回路：單 stream 撞到單 shard 天花板、或 failover 一致性要求超出 at-least-once、回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/#%e4%bd%95%e6%99%82%e6%94%b9%e8%b5%b0%e5%85%b6%e4%bb%96%e6%9c%8d%e5%8b%99" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams overview 的「何時改走其他服務」</a>、評估 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（partition + replication）。Cluster 層的 slot / topology 行為見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Re-sharding</a>。</p>
]]></content:encoded></item><item><title>Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &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>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>（target）。跟前三篇 migration（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic&lt;/a> phased / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> drop-in / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> hybrid）對照、本篇是 &lt;em>cost-driven multi-tool migration&lt;/em> — 不是換一個產品、是把 &lt;em>一站式 SaaS&lt;/em> 拆成 &lt;em>五個專責 OSS / cloud component&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <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>（source）跟 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（target）。跟前三篇 migration（<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a> phased / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> drop-in / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> hybrid）對照、本篇是 <em>cost-driven multi-tool migration</em> — 不是換一個產品、是把 <em>一站式 SaaS</em> 拆成 <em>五個專責 OSS / cloud component</em>。</p></blockquote>
<h2 id="50kmonth-bill-拆解先看錢花在哪再決定怎麼遷">$50K/month bill 拆解：先看錢花在哪、再決定怎麼遷</h2>
<p>中型 SaaS（100-500 host、5K-50K metric series、TB-level log/day）的 Datadog 月帳單長這樣：</p>
<table>
  <thead>
      <tr>
          <th>計費項</th>
          <th>平均單價</th>
          <th>中型 SaaS 估算 / month</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure host</td>
          <td>$15-23 / host</td>
          <td>200 host × $20 = $4,000</td>
      </tr>
      <tr>
          <td>APM host</td>
          <td>$31 / host</td>
          <td>100 host × $31 = $3,100</td>
      </tr>
      <tr>
          <td>Custom metrics</td>
          <td>$0.05 / 100 series</td>
          <td>30K series × $0.05 = $1,500</td>
      </tr>
      <tr>
          <td>Log ingest</td>
          <td>$0.10 / GB ingested</td>
          <td>50TB × $0.10 = $5,000</td>
      </tr>
      <tr>
          <td>Log retention（15-day）</td>
          <td>$1.27 / million events</td>
          <td>50G event × $1.27 = $6,350</td>
      </tr>
      <tr>
          <td>Log indexing</td>
          <td>$1.70 / million events</td>
          <td>50G × $1.70 = $8,500</td>
      </tr>
      <tr>
          <td>Network</td>
          <td>$5 / host</td>
          <td>200 × $5 = $1,000</td>
      </tr>
      <tr>
          <td>RUM / Session</td>
          <td>$1.50 / 1000 session</td>
          <td>30M session × $1.5 = $4,500</td>
      </tr>
      <tr>
          <td>Synthetics</td>
          <td>$5 / 10K test runs</td>
          <td>50K test = $25</td>
      </tr>
      <tr>
          <td>Total</td>
          <td>-</td>
          <td><strong>$34,000 / month</strong>（保守估）</td>
      </tr>
  </tbody>
</table>
<p>擴張到 500 host / 100TB log 的 production：$80K-150K / month 範圍。Grafana stack（self-hosted on K8s + Grafana Cloud 部分服務）對等 capacity 通常 $8K-30K / month — <em>2.5-5x cost reduction</em>。</p>
<p>但 cost 不是唯一 driver。其他 driver：</p>
<ul>
<li><strong>Multi-cloud / hybrid</strong>：Datadog 集中、Grafana 可分散部署符合資料 residency</li>
<li><strong>OpenTelemetry-first</strong>：Grafana stack 對 OTel 是 native、Datadog 仍 vendor-specific agent</li>
<li><strong>Long-term retention</strong>：Loki 用 S3 cold tier 跑 1 年 retention 比 Datadog 便宜 10-50x</li>
</ul>
<h2 id="五個責任五個-component不是替換一個產品">五個責任、五個 component：不是替換一個產品</h2>
<p>Datadog 是 <em>一站式 SaaS</em>、單一 agent + 單一 UI 包 5 個責任。Grafana stack 把責任拆給 5 個專責 component：</p>
<table>
  <thead>
      <tr>
          <th>責任</th>
          <th>Datadog 處理</th>
          <th>Grafana Stack 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>Datadog metric</td>
          <td>Mimir（Prometheus-compatible long-term）</td>
      </tr>
      <tr>
          <td>Log</td>
          <td>Datadog Logs</td>
          <td>Loki（label-indexed log）</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>Datadog APM</td>
          <td>Tempo（trace-only object storage）</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Datadog dashboard</td>
          <td>Grafana</td>
      </tr>
      <tr>
          <td>Agent / shipper</td>
          <td>Datadog Agent</td>
          <td>Alloy（OTel-based collector）+ Grafana Agent / Promtail</td>
      </tr>
  </tbody>
</table>
<p>Migration 是 <em>五個獨立 stream</em>、不是單一 cutover。SRE 對「一個 agent 包所有」的心智模型要拆。</p>
<h2 id="migration-結構每個-component-各自-phased整體-staggered">Migration 結構：每個 component 各自 phased、整體 staggered</h2>
<p>不像前三篇 migration 是線性流程、本篇是 <em>5 個 parallel migration stream</em> + 跨 stream coordination：</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">           Phase 0           Phase 1            Phase 2          Phase 3
</span></span><span class="line"><span class="ln">2</span><span class="cl">           Audit             Deploy             Dual-ship        Cutover
</span></span><span class="line"><span class="ln">3</span><span class="cl">Metric    [audit]──→        [deploy Mimir]──→ [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">4</span><span class="cl">APM       [audit]──→        [deploy Tempo]──→ [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">5</span><span class="cl">Log       [audit]──→        [deploy Loki]──→  [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">6</span><span class="cl">Dashboard [audit]──→        [deploy Grafana]──→ [rebuild]──→   [cutover]
</span></span><span class="line"><span class="ln">7</span><span class="cl">Alert     [audit]──→        [deploy Alertmgr]──→ [parallel]──→ [cutover]</span></span></code></pre></div><p>每個 stream 獨立做 dual-ship + cutover、不必同步；通常 <em>Metric 先遷</em>（cardinality 議題暴露最快）、然後 Log、最後 APM（trace correlation 最依賴 dashboard / alert）。</p>
<h2 id="agent-migrationdatadog-agent--otel-collector--alloy">Agent migration：Datadog Agent → OTel Collector / Alloy</h2>
<p>Datadog Agent 是 vendor-specific binary、抽出來換成 OpenTelemetry Collector / Grafana Alloy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># alloy config (HCL-like)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="l">prometheus.scrape &#34;k8s_pods&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="l">targets = discovery.kubernetes.pods.targets</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="l">forward_to = [prometheus.remote_write.mimir.receiver]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="l">prometheus.remote_write &#34;mimir&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="l">endpoint {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="l">url = &#34;https://mimir.internal/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="l">loki.source.kubernetes &#34;pods&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="l">targets = discovery.kubernetes.pods.targets</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="l">forward_to = [loki.write.production.receiver]</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="l">otelcol.receiver.otlp &#34;default&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="l">grpc {}</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="l">output {</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="l">traces = [otelcol.exporter.otlp.tempo.input]</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span>}</span></span></code></pre></div><p>Migration 期間 <em>dual-shipper</em> 是標準作法：</p>
<ul>
<li>Datadog Agent 跟 Alloy 並存（短期 capacity 兩倍）</li>
<li>同 host 同時 ship 兩端、觀察一致性</li>
<li>漸進 disable Datadog Agent 的 metric / log / APM 子模組</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cardinality-爆mimir-端-series-暴增">Case 1：Cardinality 爆，Mimir 端 series 暴增</h3>
<p><strong>徵兆</strong>：Datadog 端 30K series、ship 到 Mimir 後 series 變 500K、Mimir indexer OOM。</p>
<p><strong>根因</strong>：Datadog 內部對 tag 做 <em>自動 aggregation</em> 跟 <em>low-cardinality enforcement</em>；Prometheus / Mimir 對 <em>每個 unique label set</em> 算一個 series、application code 的 high-cardinality label（user_id / request_id）直接爆。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit 階段</strong> 跑 <code>topk(100, count by (__name__) ({__name__=~&quot;.+&quot;}))</code> 找 high-cardinality metric</li>
<li><strong>drop high-cardinality label</strong>：Alloy / OTel collector 端 <code>relabel</code> 規則 drop user_id 等 unbounded label</li>
<li><strong>改 histogram bucket</strong>：高 cardinality 通常來自 label combination、改用 fixed-bucket histogram</li>
<li><strong>適當改 metric 為 log</strong>：請求 ID 是 trace context、不該是 metric label</li>
</ol>
<h3 id="case-2log-volume-cost-預估失準">Case 2：Log volume cost 預估失準</h3>
<p><strong>徵兆</strong>：Loki 部署 1 個月後 S3 帳單比預估高 2x；object storage 跟 query GB-scan 都超預期。</p>
<p><strong>根因</strong>：Datadog 對 log 做自動 sampling / aggregation、bill 是 indexed event；Loki 是 <em>全量 raw ingest</em> + S3 cold storage、按實際 byte 計費。raw log volume 比 indexed event 高 3-10x。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Ingest-side sampling</strong>：Alloy / Promtail 端 sample debug / info log、只 ingest warn / error 全量</li>
<li><strong>Log structure</strong>：JSON log 比 text log 壓縮率高、Loki S3 size 少 50%</li>
<li><strong>Retention tier</strong>：hot 7 天 S3 standard / cold 1 年 S3 Glacier、retention budget 控制</li>
</ol>
<h3 id="case-3datadog-dashboard-不能直接轉-grafana">Case 3：Datadog dashboard 不能直接轉 Grafana</h3>
<p><strong>徵兆</strong>：Migration 計畫設「dashboard 自動轉換」、實際跑 Datadog API export → Grafana import、80% dashboard 缺 widget / metric 對不上。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog query syntax 跟 Grafana / Mimir 的 PromQL 不直接相容</li>
<li>Datadog widget type（top-list / hostmap）Grafana 沒對應</li>
<li>Tag-based aggregation 對應 Prometheus label 但語法不同</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受重建</strong>：production-grade dashboard 必須人工重建、不要期待自動轉</li>
<li><strong>Prioritize</strong>：先重建 <em>SOC 用 / production-critical</em> 30%、其他 deprecate</li>
<li><strong>migration window 增 4-6 週</strong>：dashboard rebuild 是 underestimated effort</li>
</ol>
<h3 id="case-4alert-routing-換邏輯pagerduty-integration-不通">Case 4：Alert routing 換邏輯，PagerDuty integration 不通</h3>
<p><strong>徵兆</strong>：Cutover 後 alert 不送 PagerDuty、SOC 半小時才發現；alert 端 webhook 配置正確、但 payload format 跟 Datadog 不同、PagerDuty 端 rule 過濾掉。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog alert payload 含 <code>event_type=alert</code>、PagerDuty integration 用這個 routing</li>
<li>Alertmanager 預設 payload 結構不同</li>
<li>PagerDuty rule 端針對 Datadog event 寫 schema、Alertmanager event 不 match</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover test</strong>：Alertmanager → PagerDuty 跑 dry-run、send test alert 驗證</li>
<li><strong>PagerDuty Service</strong>：建獨立 Grafana-source Service、不共用 Datadog Service</li>
<li><strong>Alertmanager template</strong>：用 webhook 自定 JSON template、payload 接近 Datadog 結構</li>
</ol>
<h3 id="case-5slo-definition-跟-monitor-type-對不上">Case 5：SLO definition 跟 monitor type 對不上</h3>
<p><strong>徵兆</strong>：Datadog SLO 跑 99.9% availability、轉到 Grafana SLO + Mimir 後實際 9X% 數字不一致；SOC 跑 dashboard 比對 5 個 SLO、4 個誤差 0.1-0.3%。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog SLO 計算 over time window 用內部 query；Grafana SLO 用 PromQL 寫公式</li>
<li>Datadog 對 <code>success_rate</code> 處理 missing data 跟 PromQL 預設不同</li>
<li>Time bucket boundary 處理差異</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>重定義 SLO 在 PromQL</strong>：不嘗試「複製」、是「重定義」、認真寫 PromQL 表達式</li>
<li><strong>接受 ±0.1% drift</strong>：production-critical SLO 跑 dual-track 1-2 個月、tune PromQL 到 acceptable drift</li>
<li><strong>SLO migration 不是 dashboard migration 子集</strong>：獨立 stream、留更多時間</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Datadog</th>
          <th>Grafana Stack（self-hosted on K8s）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Setup cost</td>
          <td>低（SaaS）</td>
          <td>中高（K8s deploy + storage backend）</td>
      </tr>
      <tr>
          <td>Operational cost (200 host)</td>
          <td>$34K / month</td>
          <td>$8-12K / month（含 S3 + K8s）</td>
      </tr>
      <tr>
          <td>Operational cost (500 host)</td>
          <td>$80-150K / month</td>
          <td>$15-30K / month</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.1-0.3</td>
          <td>1-2 FTE（K8s + storage + Grafana operator）</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>$1.27 / million event for 15+ day</td>
          <td>S3 + Loki：~$0.02 / GB / month</td>
      </tr>
      <tr>
          <td>Multi-cloud / hybrid</td>
          <td>受 Datadog region 限</td>
          <td>自由部署</td>
      </tr>
      <tr>
          <td>Vendor lock-in</td>
          <td>高</td>
          <td>低（OSS + OTel）</td>
      </tr>
      <tr>
          <td>Time to value</td>
          <td>1-2 週</td>
          <td>4-8 週</td>
      </tr>
      <tr>
          <td>Migration cost (one-time)</td>
          <td>-</td>
          <td>1-3 FTE × 3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>Break-even point</strong>：~150 host 規模、3 年 amortized 後 self-hosted cheaper；&lt; 100 host 規模 SaaS 較 ROI 高。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-opentelemetry-對齊">跟 OpenTelemetry 對齊</h3>
<p>Migration 是 <em>OTel-first 轉型</em> 的機會：</p>
<ul>
<li>Application code 用 OTel SDK、避免 Datadog SDK lock-in</li>
<li>Trace context propagation 走 W3C Trace Context</li>
<li>未來換 backend 不用再改 application</li>
</ul>
<h3 id="跟-splunk--elastic-對照">跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a> 對照</h3>
<p>兩篇都是 <em>cost-driven SaaS migration</em>、但細節差：</p>
<ul>
<li>Splunk → Elastic 是 SIEM 領域、schema translation 是核心議題</li>
<li>Datadog → Grafana 是 multi-tool 拆分、agent + dashboard 重建是核心</li>
<li>共同 pattern：dual-ship → parallel run → cutover</li>
</ul>
<h3 id="反向遷移grafana-stack--datadog">反向遷移（Grafana Stack → Datadog）</h3>
<p>存在但少數 — 主要是 <em>operational complexity reduction</em>（不想自管 Mimir / Loki）；schema 對位方向相反、agent 換回 Datadog Agent。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Grafana Cloud 混合</strong>：部分 component（Tempo）用 Grafana Cloud SaaS、其他 self-host、混合架構</li>
<li><strong>OpenTelemetry Collector 跟 Alloy 取捨</strong>：兩者都是 OTel-based、Alloy 是 Grafana 自家 fork</li>
<li><strong>Vector vs Alloy vs Fluentd</strong>：log shipper 戰場、cost / 功能 / OTel 整合度比較</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<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></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>etcd → Consul：KV + N 個 extras feature matrix</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://etcd.io/">etcd&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（pure KV → service mesh paradigm）→ Type E paradigm shift&lt;/em>；跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &amp;#43; 資料結構 &amp;#43; pub/sub &amp;#43; Lua &amp;#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &amp;#43; paradigm reduction 路線">Redis → Memcached&lt;/a>（paradigm reduction）對偶、本文是 &lt;em>paradigm expansion&lt;/em>（upgrade）方向。&lt;/p>&lt;/blockquote>
&lt;h2 id="kv--n-個-extrasfeature-matrix">KV + N 個 extras：feature matrix&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>etcd&lt;/th>
 &lt;th>Consul&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Pure KV with Raft consensus&lt;/td>
 &lt;td>Service mesh（KV + 6 個其他）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data store&lt;/td>
 &lt;td>KV with versioned values + watch&lt;/td>
 &lt;td>KV + service catalog + health checks + sessions&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API style&lt;/td>
 &lt;td>gRPC + HTTP/REST&lt;/td>
 &lt;td>HTTP/REST + gRPC（Connect）+ DNS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service discovery&lt;/td>
 &lt;td>無（application 自管）&lt;/td>
 &lt;td>Built-in（DNS / HTTP API）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Health check&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>Built-in（HTTP / TCP / script / TTL）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service mesh&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>Connect（mTLS + intentions + service-to-service）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-DC&lt;/td>
 &lt;td>不支援（per-cluster only）&lt;/td>
 &lt;td>Built-in WAN federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ACL system&lt;/td>
 &lt;td>RBAC (etcd 3.5+)&lt;/td>
 &lt;td>Token-based ACL + namespaces (Enterprise)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lock primitive&lt;/td>
 &lt;td>Lease + transaction&lt;/td>
 &lt;td>Session + KV check-and-set&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Watch event model&lt;/td>
 &lt;td>Event stream（gRPC stream）&lt;/td>
 &lt;td>Long-polling blocking query (X-Consul-Index)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distributed config&lt;/td>
 &lt;td>KV + watch&lt;/td>
 &lt;td>KV + watch + template rendering (consul-template)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Use case 對映&lt;/td>
 &lt;td>K8s control plane / 純 distributed KV&lt;/td>
 &lt;td>Service mesh + service discovery + config + KV&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Consul 多功能」、在「Consul 是 service mesh paradigm」&lt;/strong>：service discovery / health check / Connect mTLS 是 first-class、KV 只是其中一個 sub-feature。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="https://etcd.io/">etcd</a> 跟 <a href="/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（pure KV → service mesh paradigm）→ Type E paradigm shift</em>；跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a>（paradigm reduction）對偶、本文是 <em>paradigm expansion</em>（upgrade）方向。</p></blockquote>
<h2 id="kv--n-個-extrasfeature-matrix">KV + N 個 extras：feature matrix</h2>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>etcd</th>
          <th>Consul</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Pure KV with Raft consensus</td>
          <td>Service mesh（KV + 6 個其他）</td>
      </tr>
      <tr>
          <td>Data store</td>
          <td>KV with versioned values + watch</td>
          <td>KV + service catalog + health checks + sessions</td>
      </tr>
      <tr>
          <td>API style</td>
          <td>gRPC + HTTP/REST</td>
          <td>HTTP/REST + gRPC（Connect）+ DNS</td>
      </tr>
      <tr>
          <td>Service discovery</td>
          <td>無（application 自管）</td>
          <td>Built-in（DNS / HTTP API）</td>
      </tr>
      <tr>
          <td>Health check</td>
          <td>無</td>
          <td>Built-in（HTTP / TCP / script / TTL）</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>無</td>
          <td>Connect（mTLS + intentions + service-to-service）</td>
      </tr>
      <tr>
          <td>Multi-DC</td>
          <td>不支援（per-cluster only）</td>
          <td>Built-in WAN federation</td>
      </tr>
      <tr>
          <td>ACL system</td>
          <td>RBAC (etcd 3.5+)</td>
          <td>Token-based ACL + namespaces (Enterprise)</td>
      </tr>
      <tr>
          <td>Lock primitive</td>
          <td>Lease + transaction</td>
          <td>Session + KV check-and-set</td>
      </tr>
      <tr>
          <td>Watch event model</td>
          <td>Event stream（gRPC stream）</td>
          <td>Long-polling blocking query (X-Consul-Index)</td>
      </tr>
      <tr>
          <td>Distributed config</td>
          <td>KV + watch</td>
          <td>KV + watch + template rendering (consul-template)</td>
      </tr>
      <tr>
          <td>Use case 對映</td>
          <td>K8s control plane / 純 distributed KV</td>
          <td>Service mesh + service discovery + config + KV</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Consul 多功能」、在「Consul 是 service mesh paradigm」</strong>：service discovery / health check / Connect mTLS 是 first-class、KV 只是其中一個 sub-feature。</p>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>KV API 對位 + 多 N 個 extra API</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>兩者 Raft-based、ops similar</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Pure KV → service mesh</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 cluster</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>KV API 改 + 新增 service registration / health</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>單 DC → multi-DC（如果用 federation）</td>
          <td>Low-Medium</td>
      </tr>
  </tbody>
</table>
<p>Paradigm = High（其他 Low-Medium）→ <strong>Type E paradigm shift</strong>；KV 是 sub-feature、不是 migration scope 全部。</p>
<h2 id="為什麼遷3-條-expansion-driver">為什麼遷：3 條 expansion driver</h2>
<ul>
<li><strong>Service mesh adoption</strong>：本來用 etcd 跑 K8s control plane、現在 application 端要 service mesh（mTLS / intentions / 流量切換）、Consul 一站式 cover</li>
<li><strong>Multi-DC strategy</strong>：etcd 不支援跨 DC、要 active-passive failover；Consul WAN federation 支援 active-active 多 DC</li>
<li><strong>Configuration management</strong>：consul-template + envconsul 比 etcd watch + 自寫 reloader 簡單</li>
</ul>
<p>反向 driver（Consul → etcd）：</p>
<ul>
<li>純 K8s control plane scenario、不需要 service discovery / health check / mesh、etcd 簡單足夠</li>
<li>Resource constraint：Consul agent 比 etcd 更吃資源、low-end VM 上不夠</li>
</ul>
<h2 id="paradigm-expansion-路線">Paradigm expansion 路線</h2>
<p>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached paradigm reduction</a>（移除 features）對偶、Consul 是 <em>補進 features</em>：</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">etcd KV pattern         → Consul KV API (1:1 對位)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">etcd watch              → Consul blocking query / consul-template
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">etcd lease + lock       → Consul session + KV CAS
</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></span><span class="line"><span class="ln"> 6</span><span class="cl">無                      → Consul service registration (services.json / API)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">無                      → Consul health check (HTTP / TCP / TTL)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">無                      → Consul service discovery (DNS / HTTP)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">無                      → Consul Connect (mTLS + intentions)
</span></span><span class="line"><span class="ln">10</span><span class="cl">無                      → Consul WAN federation (multi-DC)
</span></span><span class="line"><span class="ln">11</span><span class="cl">無                      → Consul ACL token + policy</span></span></code></pre></div><p>Migration 不只是 KV API 對位、是 <em>application 增能</em>。</p>
<h2 id="api-對位">API 對位</h2>





<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"># etcd basic KV</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">etcdctl put /myapp/config/db_url <span class="s1">&#39;postgres://...&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">etcdctl get /myapp/config/db_url
</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="c1"># Consul KV (對位)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">consul kv put myapp/config/db_url <span class="s1">&#39;postgres://...&#39;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">consul kv get myapp/config/db_url</span></span></code></pre></div>




<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"># etcd watch</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">etcdctl watch --prefix /myapp/config/
</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"># Consul blocking query (long polling)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">curl <span class="s1">&#39;http://consul:8500/v1/kv/myapp/config?recurse&amp;index=5&amp;wait=10s&#39;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># X-Consul-Index header 為 watch cursor</span></span></span></code></pre></div>




<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"># etcd transaction (multi-key atomic)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">etcdctl txn <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">compares:
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">mod(&#34;/myapp/lock&#34;) = &#34;0&#34;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">success requests:
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">put /myapp/lock &#34;owner1&#34;
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">EOF</span>
</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="c1"># Consul session + KV CAS (對位)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">SESSION_ID</span><span class="o">=</span><span class="k">$(</span>curl -X PUT <span class="s1">&#39;http://consul:8500/v1/session/create&#39;</span> <span class="p">|</span> jq -r .ID<span class="k">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">curl -X PUT <span class="s1">&#39;http://consul:8500/v1/kv/myapp/lock?acquire=&#39;</span><span class="nv">$SESSION_ID</span> -d <span class="s1">&#39;owner1&#39;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 若失敗 lock 已被別人持有</span></span></span></code></pre></div><h2 id="application-重設計">Application 重設計</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Before: etcd</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">etcd3</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">etcd</span> <span class="o">=</span> <span class="n">etcd3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s1">&#39;etcd&#39;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">2379</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">etcd</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="s1">&#39;/myapp/config/db_url&#39;</span><span class="p">,</span> <span class="s1">&#39;postgres://...&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">db_url</span> <span class="o">=</span> <span class="n">etcd</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;/myapp/config/db_url&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</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="c1"># After: Consul (KV-only)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kn">import</span> <span class="nn">consul</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">c</span> <span class="o">=</span> <span class="n">consul</span><span class="o">.</span><span class="n">Consul</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s1">&#39;consul&#39;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8500</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">c</span><span class="o">.</span><span class="n">kv</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="s1">&#39;myapp/config/db_url&#39;</span><span class="p">,</span> <span class="s1">&#39;postgres://...&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">_</span><span class="p">,</span> <span class="n">kv</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">kv</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;myapp/config/db_url&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">db_url</span> <span class="o">=</span> <span class="n">kv</span><span class="p">[</span><span class="s1">&#39;Value&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># (額外加進) After: Consul service discovery</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">c</span><span class="o">.</span><span class="n">agent</span><span class="o">.</span><span class="n">service</span><span class="o">.</span><span class="n">register</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">name</span><span class="o">=</span><span class="s1">&#39;myapp&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">service_id</span><span class="o">=</span><span class="s1">&#39;myapp-1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">address</span><span class="o">=</span><span class="s1">&#39;10.0.0.10&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">port</span><span class="o">=</span><span class="mi">8080</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">check</span><span class="o">=</span><span class="n">consul</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">http</span><span class="p">(</span><span class="s1">&#39;http://10.0.0.10:8080/health&#39;</span><span class="p">,</span> <span class="s1">&#39;10s&#39;</span><span class="p">,</span> <span class="s1">&#39;5s&#39;</span><span class="p">,</span> <span class="s1">&#39;30s&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># DNS-based discovery (其他 service 找 myapp)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># dig +short myapp.service.consul SRV</span></span></span></code></pre></div><h2 id="migration-流程">Migration 流程</h2>





<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. Pre-migration audit
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   - 列 etcd 使用的所有 application
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - 評估每個 application 是否 *需要* Consul extras（service discovery / health / mesh）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 純 KV use case 標 *low-effort migration*、用得到 extras 標 *value-add migration*
</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">2. Consul cluster build
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - 跨 DC 設計（WAN federation 規劃）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - ACL system 配置（不要 default open）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   - 性能 sizing（Consul agent 比 etcd 重）
</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">3. Application migration（per-app）
</span></span><span class="line"><span class="ln">12</span><span class="cl">   - 純 KV: SDK 換、API 對位、cutover
</span></span><span class="line"><span class="ln">13</span><span class="cl">   - Service discovery: 加 registration + health check + DNS lookup
</span></span><span class="line"><span class="ln">14</span><span class="cl">   - Service mesh: 加 Connect proxy + intentions
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">4. Dual-run period
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - etcd 仍跑、application 漸進切到 Consul
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - 每 application cutover 後驗證
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">5. etcd decommission
</span></span><span class="line"><span class="ln">21</span><span class="cl">   - 確認所有 application 已切
</span></span><span class="line"><span class="ln">22</span><span class="cl">   - K8s control plane（如果是 etcd 唯一 user）保留不切</span></span></code></pre></div><p>整體 2-4 個月、依 application 數量跟 extras 採用程度。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1kv-api-對位看似-11watch-event-model-不同">Case 1：KV API 對位看似 1:1、watch event model 不同</h3>
<p><strong>徵兆</strong>：application 端從 etcd watch 切 Consul blocking query 後、event 處理 latency 從 50ms 漲到 1-5s；應用以為 event push 即時、實際變 polling。</p>
<p><strong>根因</strong>：etcd watch 是 gRPC stream、event 即時 push；Consul blocking query 是 long-polling、有 <code>wait</code> timeout、event 在 timeout 內到才即時收到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>降 <code>wait</code> timeout</strong> 跟業務需求對齊（default 5min、可設 10s）</li>
<li><strong>多 instance 並發 polling</strong>：N 個 application instance 各自 polling、降單點 event 延遲</li>
<li><strong>架構</strong>：critical event 用 Consul event API（<code>PUT /v1/event/fire/&lt;name&gt;</code>）+ blocking query event endpoint、跟 KV change 分開</li>
<li><strong>保留 etcd for critical watch</strong>：mission-critical watch 用 etcd 不切</li>
</ol>
<h3 id="case-2session-based-lock-跟-etcd-lease-差">Case 2：Session-based lock 跟 etcd lease 差</h3>
<p><strong>徵兆</strong>：原本 etcd lease 5s TTL、lease holder application 失聯時 5s 內 lock 自動釋放；切 Consul session 後、session TTL 仍生效、但 health check 整合複雜、偶發 lock not released。</p>
<p><strong>根因</strong>：Consul session 有兩種模式 — <code>delete</code>（session expire 時 release lock）vs <code>release</code>（release lock 但 KV 保留）；TTL 配 health check 時行為複雜。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 明示 session behavior</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">session_id</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">name</span><span class="o">=</span><span class="s1">&#39;myapp-lock&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ttl</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span>           <span class="c1"># 15s TTL</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">behavior</span><span class="o">=</span><span class="s1">&#39;delete&#39;</span> <span class="c1"># session 過期時 lock 自動 release</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">c</span><span class="o">.</span><span class="n">kv</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="s1">&#39;myapp/lock&#39;</span><span class="p">,</span> <span class="s1">&#39;owner1&#39;</span><span class="p">,</span> <span class="n">acquire</span><span class="o">=</span><span class="n">session_id</span><span class="p">)</span></span></span></code></pre></div><p>session TTL 範圍 10s-86400s、不能 &lt; 10s（etcd 可以 1s）；critical low-latency lock 不適用 Consul。</p>
<h3 id="case-3multi-dc-failoverkv-寫到-wrong-dc">Case 3：Multi-DC failover、KV 寫到 wrong DC</h3>
<p><strong>徵兆</strong>：跨 DC 部署後、某 application 寫 KV、但 read 不到；發現 application 端 hardcode 一個 DC 端點、write 到 us-east 但 read 來自 us-west。</p>
<p><strong>根因</strong>：Consul WAN federation 跨 DC 不自動同步 KV；KV 是 <em>per-DC</em>、跨 DC sync 需要 <em>Consul Enterprise license</em> 或自管 <em>consul-replicate</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>每 application instance 連 local DC Consul</strong>：write/read 同 DC</li>
<li><strong>KV replication 跨 DC</strong>：用 consul-replicate 自管、或升 Enterprise</li>
<li><strong>Architecture</strong>：跨 DC 共享 config 改用 <em>DB-backed config</em>（持久 + 跨 DC）+ Consul KV 只存 DC-local config</li>
</ol>
<h3 id="case-4acl-system-預設-opencutover-後曝險">Case 4：ACL system 預設 open、cutover 後曝險</h3>
<p><strong>徵兆</strong>：Consul cluster 上線 1 個月後 SOC 跑 audit、發現任何 application 都能 read 任何 KV；ACL 沒設、所有 token 都全權限。</p>
<p><strong>根因</strong>：Consul ACL 預設 disabled、需要 <em>bootstrap</em>；很多 setup tutorial 簡化跳過 ACL、cutover 後沒補。</p>
<p><strong>修法</strong>：</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"># Bootstrap ACL system</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">consul acl bootstrap
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 生成 management token、保留為 root credential</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="c1"># 建 policy</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">consul acl policy create -name <span class="s1">&#39;myapp-readonly&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  -rules <span class="s1">&#39;key_prefix &#34;myapp/&#34; { policy = &#34;read&#34; }&#39;</span>
</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="c1"># 建 token 給 application</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">consul acl token create -policy-name <span class="s1">&#39;myapp-readonly&#39;</span></span></span></code></pre></div><p>Production setup 第一步就 bootstrap ACL、不可以延後。</p>
<h3 id="case-5health-check-failure-連鎖service-discovery-失效">Case 5：Health check failure 連鎖、service discovery 失效</h3>
<p><strong>徵兆</strong>：某 application instance 因 GC pause 5 秒未 respond health check、被 Consul 標 failed；DNS query 不返回該 instance；流量切走；GC 結束後 instance 仍 healthy 但 Consul 端 still failed、需要 minutes recover。</p>
<p><strong>根因</strong>：Consul health check 失敗後進入 critical state、需要 <em>連續 N 次成功</em> 才回 passing；default 1-2 次成功即可、但實際時間視 check interval 而定。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>success_before_passing</code> 設低</strong>（1）讓快速恢復</li>
<li><strong><code>failures_before_critical</code> 設高</strong>（3-5）容忍 transient failure</li>
<li><strong>Multi-check strategy</strong>：HTTP + TCP + script check 三軸、不靠單 check</li>
<li><strong>Application-side hint</strong>：JVM application 配 <code>MaxGCPauseMillis</code> 限制 GC pause &lt; health check interval</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>etcd</th>
          <th>Consul</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster baseline</td>
          <td>3-5 node Raft cluster</td>
          <td>3-5 server + N agent (per host)</td>
      </tr>
      <tr>
          <td>Memory per node</td>
          <td>2-8GB</td>
          <td>4-16GB（含 agent）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.2-0.5</td>
          <td>0.5-1.0（多 features 多運維）</td>
      </tr>
      <tr>
          <td>Feature surface</td>
          <td>Pure KV</td>
          <td>KV + service mesh + multi-DC + ACL</td>
      </tr>
      <tr>
          <td>Setup complexity</td>
          <td>Low</td>
          <td>Medium-High</td>
      </tr>
      <tr>
          <td>Multi-DC support</td>
          <td>不支援</td>
          <td>Built-in WAN federation</td>
      </tr>
      <tr>
          <td>License</td>
          <td>Apache 2.0 (open)</td>
          <td>MPL 2.0 (community) / commercial (enterprise)</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 2-4 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 KV use case 走 etcd；service mesh / multi-DC / discovery 需求大走 Consul；混合 deployment 是 long-term default（K8s control plane 仍跑 etcd、service mesh 跑 Consul）。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-kubernetes-對位">跟 Kubernetes 對位</h3>
<p>K8s control plane <em>永遠</em> 用 etcd、不切 Consul；Consul 是 K8s <em>外</em> 的 service mesh + 跨 cluster discovery。兩者並存、不互斥。</p>
<h3 id="跟-vault-整合">跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 整合</h3>
<p>Consul + Vault 是 HashiCorp 同生態、Consul 跑 service discovery / mesh、Vault 跑 secrets；Consul ACL token 可從 Vault dynamic engine 取得。</p>
<h3 id="跟-istio--linkerd-對位">跟 <a href="https://istio.io/">Istio / Linkerd</a> 對位</h3>
<p>Consul Connect 是 service mesh paradigm、跟 Istio / Linkerd 並列；多數 K8s-native organization 用 Istio / Linkerd、Consul 強項在 <em>跨 K8s + VM + multi-DC</em> mesh。</p>
<h3 id="反向-migrationconsul--etcd">反向 migration（Consul → etcd）</h3>
<p>少數 organization 簡化 stack 時做、流程鏡像對稱、但 <em>退掉 service mesh / multi-DC 是有意識降級</em>、不能假裝功能等價。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Consul Connect production rollout</strong>：mesh adoption 是 incremental、per-service intentions 漸進</li>
<li><strong>Multi-DC topology 設計</strong>：active-active vs active-passive、依 RPO/RTO 跟 cost trade-off</li>
<li><strong>跟 Kubernetes Gateway API 整合</strong>：service mesh paradigm 在 K8s 內 vs 外整合策略</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<a href="/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a>（paradigm reduction 對偶）/ <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>平行整合：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Jenkins → GitHub Actions：Pipeline 5 段 lifecycle 的對位 + 翻譯</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/migrate-from-jenkins/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/migrate-from-jenkins/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://www.jenkins.io/">Jenkins&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Schema = High（Groovy DSL ↔ YAML workflow）→ Type A phased translation&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="pipeline-5-段-lifecycle-的對位--翻譯">Pipeline 5 段 lifecycle 的對位 + 翻譯&lt;/h2>
&lt;p>本文按 &lt;em>pipeline lifecycle 5 段&lt;/em> 組織內容（variant E）— 不是「為什麼遷」driver 開頭，是 &lt;em>Jenkins vs GHA 對 5 段各自的處理&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Lifecycle 段&lt;/th>
 &lt;th>Jenkins 機制&lt;/th>
 &lt;th>GHA 機制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. Source / SCM&lt;/td>
 &lt;td>SCM polling / webhook trigger&lt;/td>
 &lt;td>&lt;code>on: [push, pull_request]&lt;/code> event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. Build / Package&lt;/td>
 &lt;td>&lt;code>stage('Build') { sh 'mvn package' }&lt;/code>&lt;/td>
 &lt;td>&lt;code>jobs.build.steps[].run: mvn package&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Test / 並行 matrix&lt;/td>
 &lt;td>&lt;code>parallel { ... }&lt;/code> + agents&lt;/td>
 &lt;td>&lt;code>jobs.test.strategy.matrix: ...&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. Security scan&lt;/td>
 &lt;td>Plugin（Snyk / SonarQube / Aqua）&lt;/td>
 &lt;td>Action（snyk/actions / sonarsource-actions）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5. Deploy / promote&lt;/td>
 &lt;td>Deploy plugin + approval gate&lt;/td>
 &lt;td>&lt;code>environment: production&lt;/code> + reviewer approval&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&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>Schema / API&lt;/td>
 &lt;td>Groovy DSL ↔ YAML、syntax 完全不同&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>Self-hosted Jenkins → GHA SaaS / self-hosted runners&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Imperative pipeline → declarative workflow + events&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Jenkins + plugins → GHA + actions marketplace&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Build script 多數不改、CI integration 端要改&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>同單一 build state&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Schema = High（其他 Medium-Low）→ &lt;strong>Type A phased translation&lt;/strong> 為主、加 paradigm + operational 獨立段。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="https://www.jenkins.io/">Jenkins</a> 跟 <a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Schema = High（Groovy DSL ↔ YAML workflow）→ Type A phased translation</em>。</p></blockquote>
<h2 id="pipeline-5-段-lifecycle-的對位--翻譯">Pipeline 5 段 lifecycle 的對位 + 翻譯</h2>
<p>本文按 <em>pipeline lifecycle 5 段</em> 組織內容（variant E）— 不是「為什麼遷」driver 開頭，是 <em>Jenkins vs GHA 對 5 段各自的處理</em>：</p>
<table>
  <thead>
      <tr>
          <th>Lifecycle 段</th>
          <th>Jenkins 機制</th>
          <th>GHA 機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. Source / SCM</td>
          <td>SCM polling / webhook trigger</td>
          <td><code>on: [push, pull_request]</code> event</td>
      </tr>
      <tr>
          <td>2. Build / Package</td>
          <td><code>stage('Build') { sh 'mvn package' }</code></td>
          <td><code>jobs.build.steps[].run: mvn package</code></td>
      </tr>
      <tr>
          <td>3. Test / 並行 matrix</td>
          <td><code>parallel { ... }</code> + agents</td>
          <td><code>jobs.test.strategy.matrix: ...</code></td>
      </tr>
      <tr>
          <td>4. Security scan</td>
          <td>Plugin（Snyk / SonarQube / Aqua）</td>
          <td>Action（snyk/actions / sonarsource-actions）</td>
      </tr>
      <tr>
          <td>5. Deploy / promote</td>
          <td>Deploy plugin + approval gate</td>
          <td><code>environment: production</code> + reviewer approval</td>
      </tr>
  </tbody>
</table>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Groovy DSL ↔ YAML、syntax 完全不同</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Self-hosted Jenkins → GHA SaaS / self-hosted runners</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Imperative pipeline → declarative workflow + events</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Jenkins + plugins → GHA + actions marketplace</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Build script 多數不改、CI integration 端要改</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同單一 build state</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Schema = High（其他 Medium-Low）→ <strong>Type A phased translation</strong> 為主、加 paradigm + operational 獨立段。</p>
<h2 id="為什麼遷cost--vendor--cloud-native-三條-driver">為什麼遷：cost / vendor / cloud-native 三條 driver</h2>
<ul>
<li><strong>Cost</strong>：Jenkins self-hosted 是「免費 software + 高 ops cost」、GHA 按 minute 計費對中小團隊更便宜</li>
<li><strong>Vendor consolidation</strong>：repository 已在 GitHub、整合進 GHA 省一個外部系統</li>
<li><strong>Cloud-native</strong>：GHA matrix build + reusable workflow 對 cloud-native deploy（K8s / serverless）有 first-class action</li>
</ul>
<h2 id="phase-0audit--classify">Phase 0：Audit + classify</h2>





<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"># Jenkins workspace 盤點</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">find . -name <span class="s2">&#34;Jenkinsfile&#34;</span> -o -name <span class="s2">&#34;*.groovy&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 列所有 pipeline file</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="c1"># 統計 plugin 使用</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># Jenkinsfile 內 import / @Library / sh &#34;tool plugin...&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">grep -rE <span class="s2">&#34;@Library|import|tools\s*\{&#34;</span> Jenkinsfile*
</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="c1"># 每 pipeline 評估 complexity</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># - Simple linear pipeline: 1-3 stage、無 shared library</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># - Medium: parallel stage + 2-5 shared library</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># - Complex: 條件分支 + 動態 stage + 10+ plugin / 5+ shared library</span></span></span></code></pre></div><p>Audit output：</p>
<ul>
<li>列「100 個 pipeline、35 simple / 50 medium / 15 complex」</li>
<li>每 complexity level 估翻譯時間（simple 0.5 day / medium 2 day / complex 5-10 day）</li>
<li>Plugin 依賴清單對應 GHA action 替代品</li>
</ul>
<h2 id="phase-1schema-對位groovy-dsl--yaml">Phase 1：Schema 對位（Groovy DSL ↔ YAML）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// Jenkins Declarative Pipeline
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">pipeline</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">agent</span> <span class="o">{</span> <span class="n">label</span> <span class="s1">&#39;docker-build&#39;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">stages</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Test&#39;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="n">parallel</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Unit&#39;</span><span class="o">)</span> <span class="o">{</span> <span class="n">steps</span> <span class="o">{</span> <span class="n">sh</span> <span class="s1">&#39;mvn test&#39;</span> <span class="o">}</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Integration&#39;</span><span class="o">)</span> <span class="o">{</span> <span class="n">steps</span> <span class="o">{</span> <span class="n">sh</span> <span class="s1">&#39;mvn verify&#39;</span> <span class="o">}</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="n">post</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">failure</span> <span class="o">{</span> <span class="n">mail</span> <span class="nl">to:</span> <span class="s1">&#39;devops@&#39;</span><span class="o">,</span> <span class="nl">subject:</span> <span class="s1">&#39;Build failed&#39;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="o">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># GHA Workflow 對等</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">push]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">self-hosted, docker-build]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">suite</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">unit, integration]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run ${{ matrix.suite }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">          case &#34;${{ matrix.suite }}&#34; in
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">            unit) mvn test ;;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">            integration) mvn verify ;;
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">          esac</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span><span class="nt">notify-failure</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">failure()</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">dawidd6/action-send-mail@v3</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">to</span><span class="p">:</span><span class="w"> </span><span class="l">devops@</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">          </span><span class="nt">subject</span><span class="p">:</span><span class="w"> </span><span class="l">Build failed</span></span></span></code></pre></div><p>對位差異：</p>
<ul>
<li><code>parallel { ... }</code> → <code>strategy.matrix</code>（粒度不同、matrix 是「同 step 不同參數」、parallel 是「不同 step」）</li>
<li><code>post.failure</code> → 獨立 job + <code>if: failure()</code></li>
<li><code>@Library</code> shared library → reusable workflow（<code>uses: ./.github/workflows/reusable.yml</code>）</li>
<li>Jenkins <code>tools { jdk 'java17' }</code> → setup-java action（手動配 toolchain）</li>
</ul>
<h2 id="phase-2translation-pipeline3-tier-hybrid">Phase 2：Translation pipeline（3-tier hybrid）</h2>
<p>對應 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic translation</a> 同 3-tier：</p>
<ul>
<li><strong>Tier 1</strong>：community tool（jenkins-to-actions converter、cover 簡單 pipeline 30-50%）</li>
<li><strong>Tier 2</strong>：LLM-assisted（Claude / GPT 翻 medium complexity、人工 verify）</li>
<li><strong>Tier 3</strong>：manual（shared library 改 reusable workflow / conditional 動態 stage 重寫）</li>
</ul>
<h2 id="phase-3parallel-run雙-ci-跑-4-8-週">Phase 3：Parallel run（雙 CI 跑 4-8 週）</h2>





<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">Repository ──┬─→ Jenkins webhook ──→ Jenkinsfile pipeline
</span></span><span class="line"><span class="ln">2</span><span class="cl">             └─→ GitHub Action ────→ .github/workflows/ci.yml
</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">Compare:
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 同 commit 兩端結果一致
</span></span><span class="line"><span class="ln">6</span><span class="cl">- Latency / cost / artifact location 對齊</span></span></code></pre></div><p>Diff dashboard 列「test pass rate / build time / failure mode」三 metric、跑到 95%+ 一致才進 cutover。</p>
<h2 id="phase-4cutover--cleanup">Phase 4：Cutover + cleanup</h2>
<ul>
<li>Disable Jenkins webhook</li>
<li>GHA 成 primary CI</li>
<li>Jenkins 留 standby 2 週 fallback</li>
<li>Decommission Jenkins controller + agents</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1shared-library-equivalencereusable-workflow-表達不足">Case 1：Shared library equivalence、reusable workflow 表達不足</h3>
<p><strong>徵兆</strong>：複雜 Jenkins shared library（含 Groovy class / closure / 動態變數）翻成 reusable workflow 後失準、某些動態邏輯無法表達。</p>
<p><strong>根因</strong>：Jenkins Groovy 是 imperative + 完整 programming language；GHA reusable workflow 是 declarative YAML、limited expressiveness。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>複雜邏輯外包到 script</strong>：reusable workflow 只當 <em>orchestrator</em>、複雜邏輯放 <code>.github/scripts/*.sh</code> 或 <code>actions/javascript-action</code></li>
<li><strong>自定 composite action</strong>：multi-step logic 包進 composite action、reuse 程度比 reusable workflow 高</li>
<li><strong>退役過度設計的 shared library</strong>：trans 過程暴露 90% library code 其實只用 10%</li>
</ol>
<h3 id="case-2ephemeral-workspacebuild-cache-失敗">Case 2：Ephemeral workspace、build cache 失敗</h3>
<p><strong>徵兆</strong>：cutover 後 build time 從 5 分鐘漲到 20 分鐘；Maven / Gradle / node_modules / Docker layer 每次都重抓。</p>
<p><strong>根因</strong>：Jenkins agent workspace persistent、build cache 跨 build 保留；GHA ephemeral runner 每次新 VM、cache 預設沒帶。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>actions/cache@v4</code></strong>：cache key 用 <code>hashFiles('**/pom.xml')</code> 等 lock file、cross-build 復用</li>
<li><strong>Self-hosted runner with cache</strong>：critical pipeline 跑 self-hosted runner、persistent volume</li>
<li><strong>Docker layer cache</strong>：用 <code>docker/build-push-action</code> 配 BuildKit cache、不 rebuild full image</li>
</ol>
<h3 id="case-3plugin-不對等ci-feature-退化">Case 3：Plugin 不對等、CI feature 退化</h3>
<p><strong>徵兆</strong>：Jenkins 用 50+ plugin、GHA action marketplace 找不到對應；team 對 SonarQube quality gate / Jira integration / custom report 等失去 first-class 支援。</p>
<p><strong>根因</strong>：Jenkins plugin ecosystem 20+ 年累積、GHA marketplace 5 年；某些 niche plugin 在 GHA 沒對等 action。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>API-based integration</strong>：用 <code>curl</code> 對 vendor API 直接 call、不依賴 plugin / action</li>
<li><strong>自寫 action</strong>：critical feature 自寫 composite / JavaScript action、publish 到 marketplace</li>
<li><strong>退役舊 plugin</strong>：trans 期間 audit plugin 真實使用、80% 可退役</li>
</ol>
<h3 id="case-4self-hosted-runner-setup--scaling">Case 4：Self-hosted runner setup + scaling</h3>
<p><strong>徵兆</strong>：production workload 需要 GPU / large memory runner；GHA hosted runner spec 不夠、想用 self-hosted runner、發現 scaling / security / monitoring 比 Jenkins agent 複雜。</p>
<p><strong>根因</strong>：GHA self-hosted runner 是 ephemeral、scaling 需要 <em>runner controller</em>（actions-runner-controller on K8s）；跟 Jenkins agent / Kubernetes plugin 對應但 setup 不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>actions-runner-controller (ARC)</strong>：K8s-native runner scaling、跟 Jenkins K8s plugin 對應</li>
<li><strong>Runner labels</strong>：用 label 路由 job（<code>runs-on: [self-hosted, gpu, linux]</code>）</li>
<li><strong>Security</strong>：ephemeral runner 用 short-lived token、不跨 job persist secret</li>
</ol>
<h3 id="case-5matrix-build-vs-parallel-stage-表達差">Case 5：Matrix build vs parallel stage 表達差</h3>
<p><strong>徵兆</strong>：Jenkins 有 <em>動態 parallel</em>（runtime 決定要跑哪些 stage、按 input 變動）；GHA matrix 是 <em>static at workflow load time</em>、表達不到。</p>
<p><strong>根因</strong>：GHA matrix 是 declarative、workflow parse 時 expand；runtime 動態決定 stage 需要用 <code>if:</code> condition + 多 job。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>動態 matrix</strong>：用 <code>jobs.set-matrix</code> 先跑一個 job 算 matrix、輸出 JSON、後續 job <code>strategy.matrix: ${{ needs.set-matrix.outputs.matrix }}</code></li>
<li><strong>conditional job</strong>：每個 dynamic stage 寫獨立 job + <code>if:</code> 控制觸發</li>
<li><strong>重設計</strong>：90% 動態邏輯其實可改 static matrix + condition、純 runtime 動態通常是 over-engineering</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Jenkins</th>
          <th>GitHub Actions</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute cost</td>
          <td>EC2 + agent licenses</td>
          <td>per-minute billing（free tier + over-cap）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 FTE</td>
          <td>0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Plugin / action ecosystem</td>
          <td>20+ 年成熟</td>
          <td>5 年快速成長</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>Agent ready &lt; 1 min</td>
          <td>Hosted runner 30-60s spin-up</td>
      </tr>
      <tr>
          <td>Self-hosted scaling</td>
          <td>Jenkins K8s plugin</td>
          <td>ARC（actions-runner-controller）</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>Self-managed VPC + secret</td>
          <td>OIDC + repository secret + environment</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 1-3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：100+ pipeline organization 切 GHA 通常 6-12 月 ROI 持平、之後省 ops cost；&lt; 30 pipeline 早就該切。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-gitlab-ci-對位">跟 <a href="https://docs.gitlab.com/ee/ci/">GitLab CI</a> 對位</h3>
<p>GitLab CI YAML 語法跟 GHA 接近、shared library 對應 <code>include:</code>、self-hosted runner 對等；Jenkins → GitLab CI migration 流程跟本文鏡像對稱、3-tier translation pipeline 通用。</p>
<h3 id="跟-circle-ci-對位">跟 <a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">Circle CI</a> 對位</h3>
<p>CircleCI orb 對等 GHA composite action；跨 SaaS CI 切換比 Jenkins → GHA 簡單（都 YAML-based）。</p>
<h3 id="反向-migrationgha--jenkins">反向 migration（GHA → Jenkins）</h3>
<p>少數 enterprise（金融 / 政府）合規要求 self-hosted CI / on-prem；GHA → Jenkins 鏡像對稱、注意 Jenkins shared library 表達力更強、reusable workflow 內 dynamic 邏輯可不必拆。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Reusable workflow + composite action 混用</strong>：reusable workflow 適合 <em>跨 repo orchestration</em>、composite action 適合 <em>單 repo logic encapsulation</em></li>
<li><strong>OIDC + cloud deploy</strong>：用 OIDC token 取代 long-lived cloud credential、是 GHA migration 順便升級的機會</li>
<li><strong>Cost optimization</strong>：minute-based billing 對 high-volume CI 需要 monitoring + budget alert</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a></li>
<li>平行 migration playbook（Type A）：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Shard Expansion + Multi-DC：Type F「不需要 parallel run」的 multi-region 例外</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a> overview 的 implementation-layer deep article。對應 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Type F「Topology re-layout」&lt;/a> 第 3 個 dogfood、特別驗證 self-aware limitation 第 3 點「不需要 parallel run」claim 的 &lt;em>multi-region rollout 例外&lt;/em> — 本文是反例的具體實證。&lt;/p>&lt;/blockquote>
&lt;h2 id="reviewer-d-的質疑type-f-一定不需要-parallel-run-嗎">Reviewer D 的質疑：Type F 一定不需要 parallel run 嗎&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Self-aware limitation&lt;/a> 第 3 點承認：&lt;/p>
&lt;blockquote>
&lt;p>「不需要 parallel run」claim 部分不成立：multi-region rollout（#128 列為 Type F 情境）必須 parallel run — 兩 region 同時跑然後切流量、不然就是停機切換、跟 Type A phase 3 機制相同。&lt;/p>&lt;/blockquote>
&lt;p>本文是該 claim 的 &lt;em>正面實證&lt;/em> — MongoDB sharded cluster 從 single-DC 加 shard + 加 secondary DC、確實需要 parallel run + 流量切換、跟 Type A phased migration 局部同構：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a> overview 的 implementation-layer deep article。對應 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Type F「Topology re-layout」</a> 第 3 個 dogfood、特別驗證 self-aware limitation 第 3 點「不需要 parallel run」claim 的 <em>multi-region rollout 例外</em> — 本文是反例的具體實證。</p></blockquote>
<h2 id="reviewer-d-的質疑type-f-一定不需要-parallel-run-嗎">Reviewer D 的質疑：Type F 一定不需要 parallel run 嗎</h2>
<p><a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Self-aware limitation</a> 第 3 點承認：</p>
<blockquote>
<p>「不需要 parallel run」claim 部分不成立：multi-region rollout（#128 列為 Type F 情境）必須 parallel run — 兩 region 同時跑然後切流量、不然就是停機切換、跟 Type A phase 3 機制相同。</p></blockquote>
<p>本文是該 claim 的 <em>正面實證</em> — MongoDB sharded cluster 從 single-DC 加 shard + 加 secondary DC、確實需要 parallel run + 流量切換、跟 Type A phased migration 局部同構：</p>
<table>
  <thead>
      <tr>
          <th>Type F 假設</th>
          <th>Single-DC re-sharding（Redis case）</th>
          <th><strong>Multi-DC expansion（本文）</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同 cluster 不同 state</td>
          <td>yes</td>
          <td>yes（同 MongoDB cluster）</td>
      </tr>
      <tr>
          <td>不需 schema translation</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>不需 parallel run</td>
          <td>yes（slot migration 內部完成）</td>
          <td><strong>no — 兩 DC 同跑後切流量</strong></td>
      </tr>
      <tr>
          <td>不需 cleanup phase</td>
          <td>yes</td>
          <td>partial（舊 DC 角色降為 standby）</td>
      </tr>
      <tr>
          <td>Step-by-step + rollback boundary</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
  </tbody>
</table>
<p>→ Type F anatomy 仍適用、但「不需 parallel run」是 <em>子情境條件</em>、不是 universal claim。</p>
<h2 id="兩個操作合併shard-加--dc-加">兩個操作合併：shard 加 + DC 加</h2>
<p>實務上中型公司常 <em>同時</em> 跑兩個 topology 變動：</p>
<ol>
<li><strong>Shard expansion</strong>：現有 3-shard cluster 加到 5-shard、chunk migration 平均分佈</li>
<li><strong>Multi-DC</strong>：從 single-DC（us-east-1）加到 multi-DC（us-east-1 + us-west-2）</li>
</ol>
<p>兩個操作的 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Shard 加（單獨）</th>
          <th>Multi-DC（單獨）</th>
          <th>兩者同跑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Low</td>
          <td>Medium（跨 DC ops）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low（加 shard、同 cluster）</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low</td>
          <td>Low-Medium（cross-DC latency aware）</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>High</strong>（sharding strategy）</td>
          <td><strong>High</strong>（replication + region）</td>
          <td><strong>High</strong>（雙變、複合 topology）</td>
      </tr>
  </tbody>
</table>
<p>兩者主導維度都是 topology = High、組合走 Type F multi-axis 子情境。</p>
<h2 id="pre-layout-analysis當前--目標-topology">Pre-layout analysis：當前 + 目標 topology</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 當前 shard 分佈
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">({</span><span class="nx">verbose</span><span class="o">:</span> <span class="kc">false</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// 期望輸出: 3 shard、每個 ~33% chunks、no migration in progress
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">printShardingStatus</span><span class="p">({</span><span class="nx">verbose</span><span class="o">:</span> <span class="kc">false</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 找 hot shard、imbalanced chunk distribution
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 2. Replication topology
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">status</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 各 replica set primary/secondary 健康度、replication lag
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// 3. Cross-DC network baseline (在 add DC 前測)
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// us-east-1 → us-west-2 RTT、bandwidth
</span></span></span></code></pre></div><p>Pre-layout 階段 output：</p>
<ul>
<li><strong>當前</strong>：3 shard × 1 replica set per shard (3 member) = 9 node、全在 us-east-1</li>
<li><strong>目標</strong>：5 shard × 1 replica set per shard (5 member: 3 us-east + 2 us-west) = 25 node</li>
<li><strong>Migration scope</strong>：加 2 shard + 加 2 DC member 每 shard、共 +16 node</li>
<li><strong>Chunk migration estimate</strong>：30% chunk 需重分（從 33% × 3 變 20% × 5）</li>
</ul>
<h2 id="re-layout-機制">Re-layout 機制</h2>
<p>兩個 mechanism 平行進行：</p>
<h3 id="shard-expansion-mechanism">Shard expansion mechanism</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 新增 shard 到 cluster
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">addShard</span><span class="p">(</span><span class="s2">&#34;rs-shard4/host10:27017,host11:27017,host12:27017&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShard</span><span class="p">(</span><span class="s2">&#34;rs-shard5/host13:27017,host14:27017,host15:27017&#34;</span><span class="p">);</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="c1">// 2. balancer 自動 chunk migration
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">startBalancer</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 觀察 progress: db.adminCommand({balancerStatus: 1})
</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 class="c1">// 3. 完成後 verify shard distribution
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">();</span></span></span></code></pre></div><p>Chunk migration 是 <em>background</em> job、balancer 控制 throttle；不阻塞 production query、但 CPU / network 上升 30-50%。</p>
<h3 id="multi-dc-expansion-mechanism">Multi-DC expansion mechanism</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 對每 shard 的 replica set 加 us-west-2 member (priority 0)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">add</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">host</span><span class="o">:</span> <span class="s2">&#34;us-west-2-host:27017&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">priority</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>           <span class="c1">// 不能當 primary
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="nx">votes</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>              <span class="c1">// 參與投票
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="nx">hidden</span><span class="o">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">});</span>
</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="c1">// 2. 等 initial sync 完成（依資料量 1 小時 - 1 天）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">printReplicationInfo</span><span class="p">();</span>
</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 class="c1">// 3. 確認 secondary 健康後、提升 priority 或 votes
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 不要立刻設 priority 1、避免 unintended failover
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">// 4. Cross-DC routing 透過 readPreference 在 application 設
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;secondaryPreferred&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nx">readPreferenceTags</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">region</span><span class="o">:</span> <span class="s1">&#39;us-west-2&#39;</span> <span class="p">},</span> <span class="p">{}],</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>關鍵：multi-DC 是 <em>漸進加 member</em>、不是 atomic switch；每 shard 獨立加、整體耗時 = shard 數 × initial sync time。</p>
<h2 id="execution-flow含-parallel-run--流量切換">Execution flow（含 parallel run + 流量切換）</h2>
<p>8 step、包含 <em>parallel run + 切流量</em> 段——驗證 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation</a> 第 3 點：</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>動作</th>
          <th>Parallel run?</th>
          <th>Rollback boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 Pre-check</td>
          <td>量化當前 topology、確認 cluster 健康</td>
          <td>no</td>
          <td>-</td>
      </tr>
      <tr>
          <td>2 加 us-east shard</td>
          <td>sh.addShard、balancer migrate chunk</td>
          <td>no（cluster 內）</td>
          <td>removeShard、chunk migrate 回</td>
      </tr>
      <tr>
          <td>3 加 us-west member</td>
          <td>對每 shard rs.add 跨 DC member</td>
          <td>no</td>
          <td>rs.remove、initial sync 投入廢棄</td>
      </tr>
      <tr>
          <td>4 <strong>Initial sync wait</strong></td>
          <td>等所有 us-west member catch up</td>
          <td><strong>parallel run starts</strong>：兩 DC 同時 serve</td>
          <td>-</td>
      </tr>
      <tr>
          <td>5 <strong>Cross-DC dual-serve</strong></td>
          <td>兩 DC 都跑 read traffic（不切 write）</td>
          <td><strong>yes、parallel run</strong>：app 用 secondary preferred us-west</td>
          <td>readPref 切回 us-east primary</td>
      </tr>
      <tr>
          <td>6 <strong>流量切換</strong></td>
          <td>application us-west traffic 走 us-west read</td>
          <td><strong>yes</strong></td>
          <td>DNS / readPref 切回</td>
      </tr>
      <tr>
          <td>7 Promote us-west（optional）</td>
          <td>一個 shard 的 us-west member priority 提到 1</td>
          <td>post-cutover</td>
          <td>demote priority 回 0</td>
      </tr>
      <tr>
          <td>8 Cleanup</td>
          <td>Verify、archive log、document new topology</td>
          <td>no</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>Step 4-6 是 <em>parallel run + 切流量</em> — <strong>Type F 有此例外、跟 Type A phase 3 機制同構</strong>；anatomy 中「Execution flow per-step」段必須含 parallel run 子段。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1balancer-跑-chunk-migration-撞-production-peak">Case 1：Balancer 跑 chunk migration 撞 production peak</h3>
<p><strong>徵兆</strong>：加 shard 後 balancer 開始 migrate chunk、production write latency p99 從 10ms 跳到 100ms；application 端 timeout 大量。</p>
<p><strong>根因</strong>：MongoDB balancer 預設 24×7 跑、chunk migrate 是 <em>blocking</em> 操作（migration lock 期間阻塞 write 到該 chunk）；產線高峰時間 balancer 不會自動暫停。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 限 balancer 跑在 low-traffic window
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">setBalancerState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">settings</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;balancer&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">activeWindow</span><span class="o">:</span> <span class="p">{</span> <span class="nx">start</span><span class="o">:</span> <span class="s2">&#34;02:00&#34;</span><span class="p">,</span> <span class="nx">stop</span><span class="o">:</span> <span class="s2">&#34;06:00&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">{</span> <span class="nx">upsert</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">);</span></span></span></code></pre></div><p>且設 <code>chunkSize</code> 較小（128MB → 64MB）讓 migration 步驟細、單次 lock 時間短。</p>
<h3 id="case-2cross-dc-initial-sync-期間-oplog-跑出窗口">Case 2：Cross-DC initial sync 期間 oplog 跑出窗口</h3>
<p><strong>徵兆</strong>：加 us-west member 後、initial sync 跑 4 小時、結束時 member 顯示「too stale to catch up」、需要 full re-sync。</p>
<p><strong>根因</strong>：MongoDB oplog 是 capped collection、預設 size 5% disk；4 小時 initial sync 期間 primary 寫入量超出 oplog 保留範圍、member 拿到的 oplog start point 已被覆蓋。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先擴 oplog size</strong>：<code>db.adminCommand({replSetResizeOplog: 1, size: 51200})</code> 加到 50GB、覆蓋 sync window</li>
<li><strong>Off-peak initial sync</strong>：跑在低流量時間、oplog 寫入較慢</li>
<li><strong>Manual initial sync via snapshot</strong>：用 <code>mongodump</code> 從 primary snapshot、restore 到 new member、跳過 oplog tail catch-up</li>
</ol>
<h3 id="case-3跨-dc-read-路由錯誤stale-data-影響業務">Case 3：跨 DC read 路由錯誤、stale data 影響業務</h3>
<p><strong>徵兆</strong>：切流量到 us-west 後、application 偶爾抓到 5-30 秒前的 stale data；customer 報告「明明剛改了 setting、refresh 又變回去」。</p>
<p><strong>根因</strong>：us-west member 是 secondary、replication lag 5-30 秒；application readPreference 設 <code>secondaryPreferred</code> 但沒 <code>maxStalenessSeconds</code>、可能讀到嚴重 stale member。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;secondaryPreferred&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">readPreferenceTags</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">region</span><span class="o">:</span> <span class="s1">&#39;us-west-2&#39;</span> <span class="p">},</span> <span class="p">{}],</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">maxStalenessSeconds</span><span class="o">:</span> <span class="mi">90</span><span class="p">,</span>  <span class="c1">// 限 stale 不超過 90 秒
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</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="c1">// 對 strict consistency 場景強制 primary
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client_strict</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;primary&#39;</span><span class="p">,</span>  <span class="c1">// 強制讀 us-east primary
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>Application-level read pattern 必須區分「accept stale read」vs「require fresh read」、不是 cluster-level 統一配置。</p>
<h3 id="case-4shard-tag-aware-routing-沒設cross-dc-traffic-爆-cost">Case 4：Shard tag-aware routing 沒設、cross-DC traffic 爆 cost</h3>
<p><strong>徵兆</strong>：multi-DC 跑了 1 個月、AWS egress cost 從 $500 / month 漲到 $8000 / month；99% 流量還是 us-east → us-west 跨 DC。</p>
<p><strong>根因</strong>：sharded cluster 沒設 <em>zone sharding</em>、application 不知道哪些 chunk 在哪個 DC、所有 query 預設打 us-east primary、跨 DC bandwidth 爆。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 注意: MongoDB 4.2+ API、舊版 sh.addShardTag / sh.addTagRange 已 deprecated
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// 對應改 sh.addShardToZone / sh.updateZoneKeyRange
</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="c1">// 1. 給 shard 加 zone (MongoDB 4.2+)
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard1&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard2&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard3&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard4&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard5&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west&#34;</span><span class="p">);</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="c1">// 2. 對 collection 加 zone range
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">updateZoneKeyRange</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="s2">&#34;myapp.events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-east&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MinKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-east&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MaxKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="s2">&#34;us-east&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">updateZoneKeyRange</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="s2">&#34;myapp.events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-west&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MinKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-west&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MaxKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="s2">&#34;us-west&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">);</span>
</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 class="c1">// 3. balancer 重新分配 chunk 到對應 zone
</span></span></span></code></pre></div><p>Zone sharding 是 multi-DC 必要設計、不設等於白付 egress cost。</p>
<h3 id="case-5failover-後跨-dc-primary-切換application-連線中斷">Case 5：Failover 後跨 DC primary 切換、application 連線中斷</h3>
<p><strong>徵兆</strong>：production 跑 6 個月後、us-east-1 outage、某 shard primary 切到 us-west member；application 5-10 秒內大量 connection error。</p>
<p><strong>根因</strong>：MongoDB driver 預設 election timeout 10 秒、application 沒設 server selection retry；primary 切換期間 client 沒重連。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">serverSelectionTimeoutMS</span><span class="o">:</span> <span class="mi">30000</span><span class="p">,</span>    <span class="c1">// 等 30 秒給 election
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">retryWrites</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">retryReads</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">heartbeatFrequencyMS</span><span class="o">:</span> <span class="mi">5000</span><span class="p">,</span>         <span class="c1">// 更頻繁 detect topology 變動
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>且 multi-DC primary 應該設 <em>priority asymmetry</em>：us-east member priority 2、us-west priority 1；正常情況不切換、災難時自動切。</p>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Single-DC 3-shard</th>
          <th>Multi-DC 5-shard</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Node count</td>
          <td>9</td>
          <td>25</td>
          <td>~3x infrastructure cost</td>
      </tr>
      <tr>
          <td>Storage redundancy</td>
          <td>3 replica</td>
          <td>5 replica (3 east + 2 west)</td>
          <td>+2 copy、storage cost +66%</td>
      </tr>
      <tr>
          <td>Network egress</td>
          <td>內部 VPC、低</td>
          <td>Cross-DC、高（需 zone sharding）</td>
          <td>$500 → $8000 / month if no zone sharding</td>
      </tr>
      <tr>
          <td>Latency p99 (write)</td>
          <td>5-10ms</td>
          <td>5-15ms（primary 仍 us-east）</td>
          <td>略升</td>
      </tr>
      <tr>
          <td>Latency p99 (read)</td>
          <td>5-10ms</td>
          <td>2-5ms (local DC)</td>
          <td>Multi-DC 區域 read 加快</td>
      </tr>
      <tr>
          <td>Disaster recovery</td>
          <td>RTO 30 分鐘（rebuild）</td>
          <td>RTO &lt; 1 分鐘（auto failover）</td>
          <td>顯著改善</td>
      </tr>
      <tr>
          <td>Operational complexity</td>
          <td>低</td>
          <td>高（zone sharding / DR drill）</td>
          <td>+1 SRE FTE 維護</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：multi-DC 是 <em>DR 投資</em>、不是 cost optimization；只在 <em>availability SLA &gt; 99.9% 或合規要求</em> 場景值得。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-mongodb--atlas-migration-對位">跟 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas migration</a> 對位</h3>
<p>Self-managed multi-DC 複雜度高、Atlas 把 multi-cluster + cross-region 簡化成 UI 配置；如果走 multi-DC、考慮直接遷 Atlas。</p>
<h3 id="跟-application-read-pattern-整合">跟 Application read pattern 整合</h3>
<p>zone sharding + readPreference 跟 application logic 緊密耦合；不能事後補、應在 multi-DC 設計階段就設計 application 端的 region-aware routing。</p>
<h3 id="跟-cassandra-keyspace-re-balance-對比">跟 <a href="https://cassandra.apache.org/">Cassandra keyspace re-balance</a> 對比</h3>
<p>Cassandra 是另一個 Type F multi-DC 典型 case；用 <em>NetworkTopologyStrategy + replication factor per DC</em>、跟 MongoDB zone sharding 概念對等但 mechanism 完全不同。Reviewer D 把 Cassandra 列為 Type F 反例 — 本文以 MongoDB 替代驗證。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Cross-region active-active</strong>：MongoDB 不支援 multi-primary、cross-region active-active 需要 application-level conflict resolution</li>
<li><strong>PostgreSQL Citus / CockroachDB multi-region</strong> 對比：distributed SQL 對 multi-region 有不同設計</li>
<li><strong>Cost optimization</strong>：跨 DC egress 是 long-term concern、zone sharding 設好後仍要 quarterly review</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a></li>
<li>平行 migration playbook：<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a></li>
<li>平行 Type F dogfood：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Re-sharding</a>（dogfood #1）/ <a href="/blog/backend/01-database/vendors/postgresql/partition-redesign/" data-link-title="PostgreSQL Partition Redesign：當 monthly partition 越跑越慢" data-link-desc="PostgreSQL partition redesign 是 Type F「topology re-layout」第 2 個 dogfood — 從 monthly partition 改 daily / 從 range 改 list / 從單軸改 sub-partition；6 維 audit 皆 Low &#43; topology 軸 High；涵蓋 partition 不平衡偵測、ATTACH/DETACH 線上重劃、5 個 production 踩雷、跟 partition_pruning &#43; autovacuum 整合">PostgreSQL Partition Redesign</a>（dogfood #2）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Data topology 是第 6 audit 維度</a>（本文驗證 self-aware limitation 第 3 點）</li>
</ul>
]]></content:encoded></item><item><title>MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/replication-topology/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/replication-topology/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>replication topology&lt;/em> — 從 single primary 到 multi-replica 部署的 3 個 trade-off 軸跟 5 段配置。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="replication-的-3-個-trade-off-軸--mode-選擇">Replication 的 3 個 trade-off 軸 + mode 選擇&lt;/h2>
&lt;p>Replication mode 選擇看起來是「選 async 還是 semi-sync」、但決策實際是 3 個獨立 trade-off 軸的權衡、async / semi-sync 是這些軸的兩個常見組合 &lt;em>名稱&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>軸&lt;/th>
 &lt;th>端 A&lt;/th>
 &lt;th>端 B&lt;/th>
 &lt;th>MySQL 旋鈕&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Durability&lt;/strong>&lt;/td>
 &lt;td>primary 寫完就 commit&lt;/td>
 &lt;td>至少一個 standby 收到才 commit&lt;/td>
 &lt;td>&lt;code>rpl_semi_sync_master_enabled&lt;/code> / sync ack count&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Latency&lt;/strong>&lt;/td>
 &lt;td>client 等 primary 寫完 OK&lt;/td>
 &lt;td>client 等 standby ack（額外 RTT）&lt;/td>
 &lt;td>&lt;code>rpl_semi_sync_master_timeout&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Consistency&lt;/strong>&lt;/td>
 &lt;td>replica 隨時可能 stale&lt;/td>
 &lt;td>replica 跟 primary 保證讀到一致&lt;/td>
 &lt;td>application read routing rule（不是 replication 旋鈕）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「async vs semi-sync」實際上是 &lt;em>durability + latency 兩軸&lt;/em> 的選擇、不影響 &lt;em>consistency 軸&lt;/em>（consistency 在 read routing 層決定）。Group Replication / MySQL Cluster（synchronous multi-primary）會同時改三軸、是另一個故事、不在本文 scope。&lt;/p>
&lt;p>跟這三軸獨立的、是 &lt;em>replication 機制本身的可維護性&lt;/em>。binlog position-based replication 用 &lt;code>(file, position)&lt;/code> 標 replica 進度、failover 時要對齊 position 容易出錯；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/gtid/" data-link-title="GTID" data-link-desc="說明全域交易識別碼如何讓複製進度與故障切換不依賴實體 log 位置">&lt;strong>GTID（Global Transaction Identifier）&lt;/strong>&lt;/a>用全域 transaction ID 標進度、failover / re-pointing 不必算 position。GTID 是 &lt;em>跨 mode 的 infrastructure&lt;/em>、不是第三種 mode。&lt;/p>
&lt;h2 id="async-replicationdefault--高-throughput-的代價">Async replication：default + 高 throughput 的代價&lt;/h2>
&lt;p>Async 是 MySQL 預設、行為：&lt;/p>
&lt;ol>
&lt;li>Primary 寫 binlog、立刻 commit、回應 client OK&lt;/li>
&lt;li>Replica 的 IO thread 從 primary pull binlog event 到 local relay log&lt;/li>
&lt;li>Replica 的 SQL thread apply relay log（單 thread 或 multi-thread parallel）&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Trade-off&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>replication topology</em> — 從 single primary 到 multi-replica 部署的 3 個 trade-off 軸跟 5 段配置。</p></blockquote>
<hr>
<h2 id="replication-的-3-個-trade-off-軸--mode-選擇">Replication 的 3 個 trade-off 軸 + mode 選擇</h2>
<p>Replication mode 選擇看起來是「選 async 還是 semi-sync」、但決策實際是 3 個獨立 trade-off 軸的權衡、async / semi-sync 是這些軸的兩個常見組合 <em>名稱</em>：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>端 A</th>
          <th>端 B</th>
          <th>MySQL 旋鈕</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Durability</strong></td>
          <td>primary 寫完就 commit</td>
          <td>至少一個 standby 收到才 commit</td>
          <td><code>rpl_semi_sync_master_enabled</code> / sync ack count</td>
      </tr>
      <tr>
          <td><strong>Latency</strong></td>
          <td>client 等 primary 寫完 OK</td>
          <td>client 等 standby ack（額外 RTT）</td>
          <td><code>rpl_semi_sync_master_timeout</code></td>
      </tr>
      <tr>
          <td><strong>Consistency</strong></td>
          <td>replica 隨時可能 stale</td>
          <td>replica 跟 primary 保證讀到一致</td>
          <td>application read routing rule（不是 replication 旋鈕）</td>
      </tr>
  </tbody>
</table>
<p>「async vs semi-sync」實際上是 <em>durability + latency 兩軸</em> 的選擇、不影響 <em>consistency 軸</em>（consistency 在 read routing 層決定）。Group Replication / MySQL Cluster（synchronous multi-primary）會同時改三軸、是另一個故事、不在本文 scope。</p>
<p>跟這三軸獨立的、是 <em>replication 機制本身的可維護性</em>。binlog position-based replication 用 <code>(file, position)</code> 標 replica 進度、failover 時要對齊 position 容易出錯；<a href="/blog/backend/knowledge-cards/gtid/" data-link-title="GTID" data-link-desc="說明全域交易識別碼如何讓複製進度與故障切換不依賴實體 log 位置"><strong>GTID（Global Transaction Identifier）</strong></a>用全域 transaction ID 標進度、failover / re-pointing 不必算 position。GTID 是 <em>跨 mode 的 infrastructure</em>、不是第三種 mode。</p>
<h2 id="async-replicationdefault--高-throughput-的代價">Async replication：default + 高 throughput 的代價</h2>
<p>Async 是 MySQL 預設、行為：</p>
<ol>
<li>Primary 寫 binlog、立刻 commit、回應 client OK</li>
<li>Replica 的 IO thread 從 primary pull binlog event 到 local relay log</li>
<li>Replica 的 SQL thread apply relay log（單 thread 或 multi-thread parallel）</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>Durability：primary 寫完 commit、replica 還沒 pull = primary 在這瞬間 crash + 永久故障 → <em>data loss</em>（已 commit 的 transaction 在 replica 不存在）</li>
<li>Latency：client 不等 replica、寫入延遲 = primary 自身寫 binlog 的時間（通常 &lt; 1ms with <code>innodb_flush_log_at_trx_commit=1</code>）</li>
<li>Consistency：replica 可能 lag、application 讀 replica 會 stale；用 <code>SHOW SLAVE STATUS</code> 看 <code>Seconds_Behind_Master</code></li>
</ul>
<p><strong>適用</strong>：</p>
<ul>
<li>主流選擇（90% 場景）</li>
<li>Failover loss 在容忍範圍（多數 web 應用容忍 1-2 秒 data loss）</li>
<li>Read scaling 為主要 driver、絕對 durability 非首要</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>金融交易 / 訂單系統、不允許 any data loss</li>
<li>Compliance 要求 zero data loss（PCI-DSS / 部分監管場景）</li>
</ul>
<h2 id="semi-sync-replication至少一個-standby-ack-才-commit">Semi-sync replication：至少一個 standby ack 才 commit</h2>
<p>Semi-sync 在 async 基礎上加 <em>primary 等至少 N 個 replica ack 才 commit</em> 的步驟：</p>
<ol>
<li>Primary 寫 binlog</li>
<li>Primary 發送 binlog event 到所有 replica</li>
<li><em>Primary 等至少 N 個 replica 回 ack</em>（N 是 <code>rpl_semi_sync_master_wait_for_slave_count</code>、預設 1）</li>
<li>Primary commit、回應 client</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>Durability：至少 N 個 replica 收到 binlog（不一定 apply）、primary crash 後 replica 還有 binlog 可 promote、保證 zero data loss（但是 <em>binlog-level</em>、不是 <em>applied-level</em>）</li>
<li>Latency：client 等 primary + 一輪 replica ack RTT；跨 AZ 通常 +1-3ms、跨 region 可能 +50-200ms</li>
<li>Consistency：跟 async 一樣、replica apply 仍 async、application 讀 replica 仍可能 stale</li>
</ul>
<p><strong>MySQL 5.7+ 區分 <em>standard</em> 跟 <em>Loss-Less</em> semi-sync</strong>：</p>
<ul>
<li>Standard semi-sync（5.5-5.6）：primary 先 commit 再等 ack、ack 超時 fallback 成 async — <em>仍可能 lose data</em></li>
<li>Loss-Less semi-sync（5.7+、<code>rpl_semi_sync_master_wait_point=AFTER_SYNC</code>）：primary 寫完 binlog 但 <em>先等 ack 再 commit</em>、ack 超時 fallback async 之前已寫 binlog 仍保證 durable</li>
</ul>
<p>Production 場景必須用 Loss-Less semi-sync、不是 standard。</p>
<p><strong>適用</strong>：</p>
<ul>
<li>金融交易 / 訂單 / payment ledger</li>
<li>不允許 data loss、可接受寫入延遲 +1-3ms</li>
<li>已有 multi-AZ / multi-region 部署、replica 物理上可靠</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>跨 region semi-sync（RTT 50-200ms）通常不划算 — 寫吞吐砍半、改用 <em>region-local sync replica + cross-region async chain</em></li>
<li>寫吞吐 &gt; 50K WPS 且容忍 sub-second loss — async 即可</li>
</ul>
<h2 id="gtid-based-replication機制升級跨-mode-都需要">GTID-based replication：機制升級、跨 mode 都需要</h2>
<p>GTID 把每個 transaction 標一個全域 ID：<code>&lt;server_uuid&gt;:&lt;transaction_id&gt;</code>。Replica 紀錄「已 apply 的 GTID set」、不再用 <code>(binlog_file, position)</code>。</p>
<p><strong>為什麼 GTID 比 binlog position 好</strong>：</p>
<ul>
<li><strong>Failover re-pointing 簡單</strong>：promote 新 primary 後、其他 replica 重新 attach 不必算 <code>MASTER_LOG_FILE</code> + <code>MASTER_LOG_POS</code>、用 <code>CHANGE MASTER TO MASTER_AUTO_POSITION=1</code> 即可</li>
<li><strong>Multi-source replication 可行</strong>：一個 replica 從多個 primary 拉、各 primary 的 GTID set 獨立 track</li>
<li><strong>Consistency check 容易</strong>：兩個 server 對 GTID set、就知道誰落後、有無 gap</li>
<li><strong>跟 group replication / MySQL Cluster 必需</strong>：5.7+ 多 primary 場景 GTID 是前提</li>
</ul>
<p><strong>設定流程</strong>（兩階段、不能直接開）：</p>
<ol>
<li>
<p><strong>Phase 1 (預備、所有 server 同 mode)</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">gtid_mode</span> <span class="o">=</span> <span class="s">ON_PERMISSIVE  -- 接受 GTID 跟 non-GTID transaction</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">enforce_gtid_consistency</span> <span class="o">=</span> <span class="s">ON  -- 拒絕無法用 GTID 表達的 statement（CREATE TABLE...SELECT 等）</span></span></span></code></pre></div></li>
<li>
<p><strong>Phase 2 (rolling、全部 server 都 Phase 1 後)</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">gtid_mode</span> <span class="o">=</span> <span class="s">ON  -- 只接受 GTID transaction</span></span></span></code></pre></div></li>
</ol>
<p>跳 phase 直接 <code>gtid_mode=ON</code> 會讓 replication break（既有 non-GTID transaction 無法處理）。Production 啟用 GTID 要排 maintenance window、跑完 phase 1 觀察 1-2 天再進 phase 2。</p>
<h2 id="配置-step-by-steploss-less-semi-sync--gtid-組合">配置 step-by-step（Loss-Less semi-sync + GTID 組合）</h2>
<p>實務最常見組合：Loss-Less semi-sync + GTID。配置順序：</p>
<h3 id="step-1primary--replica-都開-gtid兩-phase-跑完">Step 1：Primary + replica 都開 GTID（兩 phase 跑完）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># my.cnf on primary AND replica</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">gtid_mode</span> <span class="o">=</span> <span class="s">ON</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">enforce_gtid_consistency</span> <span class="o">=</span> <span class="s">ON</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">log_bin</span> <span class="o">=</span> <span class="s">mysql-bin</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">log_slave_updates</span> <span class="o">=</span> <span class="s">1  -- replica 也記 binlog (chained replication 需要)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">binlog_format</span> <span class="o">=</span> <span class="s">ROW    -- ROW 比 STATEMENT 安全</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">sync_binlog</span> <span class="o">=</span> <span class="s">1        -- 每次 commit fsync binlog</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="na">innodb_flush_log_at_trx_commit</span> <span class="o">=</span> <span class="s">1  -- 每次 commit fsync InnoDB log</span></span></span></code></pre></div><h3 id="step-2primary-安裝-semi-sync-plugin">Step 2：Primary 安裝 semi-sync plugin</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">PLUGIN</span><span class="w"> </span><span class="n">rpl_semi_sync_master</span><span class="w"> </span><span class="n">SONAME</span><span class="w"> </span><span class="s1">&#39;semisync_master.so&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">rpl_semi_sync_master_enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">rpl_semi_sync_master_wait_for_slave_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 至少 1 個 ack
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">rpl_semi_sync_master_wait_point</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">AFTER_SYNC</span><span class="p">;</span><span class="w">   </span><span class="c1">-- Loss-Less
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">rpl_semi_sync_master_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10000</span><span class="p">;</span><span class="w">           </span><span class="c1">-- 10s timeout、超時 fallback async</span></span></span></code></pre></div><h3 id="step-3replica-安裝-semi-sync-plugin">Step 3：Replica 安裝 semi-sync plugin</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">PLUGIN</span><span class="w"> </span><span class="n">rpl_semi_sync_slave</span><span class="w"> </span><span class="n">SONAME</span><span class="w"> </span><span class="s1">&#39;semisync_slave.so&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">rpl_semi_sync_slave_enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">STOP</span><span class="w"> </span><span class="n">SLAVE</span><span class="w"> </span><span class="n">IO_THREAD</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">START</span><span class="w"> </span><span class="n">SLAVE</span><span class="w"> </span><span class="n">IO_THREAD</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 重啟 IO thread 啟用 semi-sync</span></span></span></code></pre></div><h3 id="step-4replica-attach-primary">Step 4：Replica attach primary</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">CHANGE</span><span class="w"> </span><span class="n">MASTER</span><span class="w"> </span><span class="k">TO</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="n">MASTER_HOST</span><span class="o">=</span><span class="s1">&#39;primary.example.com&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="n">MASTER_PORT</span><span class="o">=</span><span class="mi">3306</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="n">MASTER_USER</span><span class="o">=</span><span class="s1">&#39;repl&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="n">MASTER_PASSWORD</span><span class="o">=</span><span class="s1">&#39;...&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="n">MASTER_AUTO_POSITION</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 用 GTID auto-position
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">START</span><span class="w"> </span><span class="n">SLAVE</span><span class="p">;</span></span></span></code></pre></div><h3 id="step-5驗證">Step 5：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Primary: 確認 semi-sync 啟用 + 有 active client
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Rpl_semi_sync_master_status&#39;</span><span class="p">;</span><span class="w">      </span><span class="c1">-- ON
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Rpl_semi_sync_master_clients&#39;</span><span class="p">;</span><span class="w">     </span><span class="c1">-- ≥ 1
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Rpl_semi_sync_master_yes_tx&#39;</span><span class="p">;</span><span class="w">      </span><span class="c1">-- &gt; 0 (有 transaction 走 semi-sync)
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Rpl_semi_sync_master_no_tx&#39;</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 應該 = 0 (沒有 fallback 成 async)
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- Replica: 確認 GTID + IO thread 正常
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">SLAVE</span><span class="w"> </span><span class="n">STATUS</span><span class="err">\</span><span class="k">G</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- Slave_IO_Running: Yes
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- Slave_SQL_Running: Yes
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">-- Retrieved_Gtid_Set: 跟 primary Executed_Gtid_Set 接近
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">-- Seconds_Behind_Master: 觀察 lag</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-replication-lag-暴衝--單-sql-thread-bottleneck">1. Replication lag 暴衝 — 單 SQL thread bottleneck</h3>
<p>預設 replica 的 SQL thread 是 <em>單 thread</em> apply、primary 多 thread 寫入時 replica 跟不上、lag 從 &lt; 100ms 飆到分鐘級。常見觸發：批次 UPDATE / DELETE、大 transaction、index rebuild。</p>
<p>修法：</p>
<ul>
<li>啟用 <em>multi-thread replication</em>：<code>slave_parallel_workers = 8</code>（per database 或 per logical clock parallel）</li>
<li>5.7+ 用 <code>slave_parallel_type = LOGICAL_CLOCK</code>：依 primary 上的 group commit 並行度自動 parallel</li>
<li>8.0+ 的 <em>writeset-based parallel</em>：<code>binlog_transaction_dependency_tracking = WRITESET</code>、更細粒度並行</li>
</ul>
<p>監控：<code>Seconds_Behind_Master</code> 是 <em>表面指標</em>、實際看 <code>Executed_Gtid_Set</code> 跟 primary 對比的 GTID gap 更準。</p>
<h3 id="2-semi-sync-timeout-fallback-成-async沒監控就看不見">2. Semi-sync timeout fallback 成 async（沒監控就看不見）</h3>
<p><code>rpl_semi_sync_master_timeout</code> 預設 10000ms（10 秒）、超時後 <em>自動 fallback async</em>、直到 replica 重連。Application 視角看不到任何 error、但 <em>durability guarantee 已失效</em>。</p>
<p>修法：</p>
<ul>
<li>監控 <code>Rpl_semi_sync_master_status</code> — fallback 後變 OFF</li>
<li>監控 <code>Rpl_semi_sync_master_no_tx</code> — fallback 期間每個 transaction 都計數</li>
<li>Alert 規則：5 分鐘內 <code>no_tx</code> 增加 &gt; 0 即告警</li>
<li>Timeout 設太短（&lt; 5s）容易 false positive、設太長（&gt; 30s）crash 時 data loss 風險增</li>
</ul>
<h3 id="3-gtid-gap--replica-無法-attach">3. GTID gap — replica 無法 attach</h3>
<p>Replica 重新 attach primary 時報 <code>ERROR 1236: ... transactions you need from master are purged</code>、原因是 primary 的 <code>binlog_expire_logs_seconds</code> 過短、需要的 binlog 已被清掉。GTID 模式下這個錯誤更明顯（直接看 GTID gap）、但 binlog position 模式下也一樣。</p>
<p>修法：</p>
<ul>
<li><code>binlog_expire_logs_seconds = 604800</code>（7 天）作為 baseline</li>
<li>大流量 server 確認 disk 容量能撐 7 天 binlog（一個高峰小時 binlog 可能 GB 級）</li>
<li>真的 gap 太大時用 <em>base backup + replay binlog</em> 重建 replica、不要硬 reset GTID</li>
</ul>
<h3 id="4-loss-less-semi-sync-不一定真的-loss-less">4. Loss-Less semi-sync 不一定真的 loss-less</h3>
<p><code>AFTER_SYNC</code> 模式 <em>primary 寫 binlog → 等 ack → commit</em>、看起來 zero loss。但 <em>primary 寫完 binlog 還沒等 ack 時 crash</em> + replica <em>剛好沒收到那個 binlog event</em> + replica promote — 這個 binlog event 在新 primary 不存在、但舊 primary 的 binlog 仍紀錄為 <em>已寫 binlog 未 commit</em>。client 收到 <em>connection lost</em>、不知道 transaction 是否成功。</p>
<p>修法：</p>
<ul>
<li>接受這個 <em>edge case unknown state</em>、application 用 idempotency key + retry 處理</li>
<li>Loss-Less semi-sync 保證的是 <em>已 commit transaction 不會丟</em>、不是 <em>所有寫入都 ack-and-tell</em></li>
<li>真的 zero unknown state 需要 group replication / Galera Cluster / MySQL Cluster（synchronous multi-primary）</li>
</ul>
<h3 id="5-chained-replication-雪崩">5. Chained replication 雪崩</h3>
<p>Topology 是 <code>primary → replica1 → replica2 → ...</code>（hub-and-spoke 之外的選擇、節省 primary 出口頻寬）。Replica1 SQL thread 卡住、replica2 跟 replica3 都被 block、整條 chain 雪崩。</p>
<p>修法：</p>
<ul>
<li>避免超過 2 層 chain（primary → tier1 replica → tier2 replica 是上限）</li>
<li>用 <em>parallel binary log relay</em>（5.7+ <code>slave_pending_jobs_size_max</code> + parallel workers）讓 chain 中段不阻塞</li>
<li>規模真的大、改用 <em>binlog server</em>（如 Maxwell / MaxScale）解耦 chain dependency</li>
<li>跨 region 用 <em>region-local hub + cross-region async</em>、不是長 chain</li>
</ul>
<h2 id="容量--cost-對照">容量 / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>寫吞吐影響</th>
          <th>Replica overhead</th>
          <th>適合 workload</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Async + binlog position</td>
          <td>baseline</td>
          <td>低（IO + SQL thread）</td>
          <td>高吞吐、容忍 sub-second loss</td>
      </tr>
      <tr>
          <td>Async + GTID</td>
          <td>baseline</td>
          <td>同上、failover 容易</td>
          <td>大多數 production 預設</td>
      </tr>
      <tr>
          <td>Loss-Less semi-sync + GTID（1 ack）</td>
          <td>-10% ~ -20%</td>
          <td>同上 + ack RTT</td>
          <td>金融、訂單、不容忍 data loss</td>
      </tr>
      <tr>
          <td>Loss-Less semi-sync + GTID（2 ack）</td>
          <td>-15% ~ -30%</td>
          <td>同上、跨 AZ</td>
          <td>強 durability + multi-AZ HA</td>
      </tr>
      <tr>
          <td>Group Replication（synchronous）</td>
          <td>-30% ~ -50%</td>
          <td>高（每 transaction quorum）</td>
          <td>不允許 single-primary、multi-primary 寫入</td>
      </tr>
  </tbody>
</table>
<p>跨 AZ semi-sync 通常加 1-3ms、跨 region 加 50-200ms — 寫密集 workload 跨 region semi-sync 通常不划算、改用 <em>region-local sync + cross-region async chain</em>。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="aurora-mysql">Aurora MySQL</h3>
<p>Aurora MySQL 用 <em>AWS-managed storage layer</em>、storage 自動 replicate 6 份跨 3 AZ、不需要應用層配 semi-sync。從自管 MySQL 遷 Aurora 時、上方所有 semi-sync 配置 <em>消失</em>、改成 Aurora storage quorum（4 of 6 write、3 of 6 read）。</p>
<p>trade-off 軸的 <em>durability</em> 完全交給 Aurora、application 只關心 <em>latency</em> + <em>consistency</em>。詳見 <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 page</a>。</p>
<h3 id="vitesssharding-layer">Vitess（sharding layer）</h3>
<p>Vitess shard 內部仍用 MySQL replication（async or semi-sync）、Vitess 不取代 replication topology、是 <em>上層 routing</em>。Vitess <code>vttablet</code> 每個 shard 有自己的 primary + replica、跟本文 topology 設計一致。</p>
<p>Vitess 比較大議題在 <em>cross-shard transaction</em>（VReplication 跨 shard binlog stream）、不是 replication topology — 詳見 MySQL backlog 中 <em>Vitess sharding 設計</em> 篇（待寫）。</p>
<h3 id="proxysqlread-replica-routing">ProxySQL（read replica routing）</h3>
<p>ProxySQL 是 MySQL 生態的 <em>connection pool + query routing</em> 標準、按 query type（SELECT vs DML）跟 replica lag 自動 route。寫入路 primary、讀走 replica、replica lag &gt; N 秒時暫時退路 primary 維持 consistency。</p>
<p>ProxySQL 跟本文 replication topology 是 <em>互補不重疊</em> — replication 設定哪些 server 有什麼資料、ProxySQL 設定 query 怎麼分配。詳見 MySQL backlog 中 <em>ProxySQL 配置</em> 篇（待寫）。</p>
<h3 id="orchestratorha-failover">Orchestrator（HA failover）</h3>
<p>Orchestrator 是 MySQL HA topology 管理 + 自動 failover 工具、用 GTID 偵測 replica 進度、failover 時自動 promote 最新 replica。對比 PostgreSQL 的 Patroni（詳見 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a>）— 兩者角色相同、Orchestrator 需要 GTID + 對 MySQL 行為熟、Patroni 需要 DCS（etcd / Consul）+ 對 PG 行為熟。</p>
<p>詳見 MySQL backlog 中 <em>Orchestrator failover 設計</em> 篇（待寫）。</p>
<h3 id="cdcmaxwell--debezium">CDC（Maxwell / Debezium）</h3>
<p>Maxwell（Zendesk 出品、MySQL-only）跟 Debezium（Red Hat、MySQL / PG / MongoDB 都支援）都讀 MySQL binlog 轉成 event stream（Kafka / Kinesis / Pulsar）。Binlog 必須 <code>ROW</code> format、GTID 啟用後 <em>exactly-once</em> delivery 更好維護（不需算 binlog position）。</p>
<p>跟 PG logical replication + Debezium 對比、MySQL 用 binlog（physical / row-level）不是 logical decoding、所以 schema change 時 <em>CDC consumer 要 schema-aware</em> 處理。詳見 MySQL backlog 中 <em>Binary log + Maxwell / Debezium CDC</em> 篇（待寫）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PostgreSQL Replication Topology</a>（PG sibling、streaming + LSN + slot 機制 vs MySQL binlog 對位）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni HA</a>（PG sibling、不同 HA 機制）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PostgreSQL Logical Replication + Debezium</a>（PG CDC sibling、不同 replication 抽象層）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-slot-management/" data-link-title="PostgreSQL Replication Slot Management：Physical / Logical / Failover Slot 治理" data-link-desc="PG replication slot 是 *primary 端的 standby 進度紀錄*、防 WAL premature deletion。但 orphan slot 會吃 disk、failover 後 logical slot 不會自動跟新 primary、是 PG 操作的 hidden complexity。本文走 physical / logical slot 差異、slot lifecycle、failover slot synchronization（PG 17&#43; 新特性）、orphan slot 治理、5 production 踩雷（orphan slot disk 爆 / logical slot lag / failover 後 slot 丟 / wal_keep_size 跟 slot 衝突 / connection 同時打 slot 數量限制）">PostgreSQL Replication Slot Management</a>（PG slot 治理、MySQL 無對應概念）</li>
<li><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 page</a>（managed MySQL、replication 交給 storage layer）</li>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>（transaction 行為跟 replication 互動）</li>
<li><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（替代路徑）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡片</a> / <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency 卡片</a> / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡片</a></li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/replication.html">MySQL Replication</a> / <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-semisync.html">Semi-Sync</a> / <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-gtids.html">GTID</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN + replication slot 的三軸組合</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/replication-topology/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/replication-topology/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>streaming replication topology&lt;/em> — 從 single primary 到 multi-standby 部署的 3 個 trade-off 軸 + LSN + replication slot 機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="replication-的-3-個-trade-off-軸--mode-選擇">Replication 的 3 個 trade-off 軸 + mode 選擇&lt;/h2>
&lt;p>PG streaming replication mode 選擇看起來是「async 還是 sync」、實際是 3 個獨立 trade-off 軸的組合、async / sync / quorum-based sync 是這些軸的常見組合 &lt;em>名稱&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>軸&lt;/th>
 &lt;th>端 A&lt;/th>
 &lt;th>端 B&lt;/th>
 &lt;th>PG 旋鈕&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Durability&lt;/strong>&lt;/td>
 &lt;td>primary 寫完就 commit&lt;/td>
 &lt;td>至少一個 standby 收到才 commit&lt;/td>
 &lt;td>&lt;code>synchronous_commit&lt;/code> / &lt;code>synchronous_standby_names&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Latency&lt;/strong>&lt;/td>
 &lt;td>client 等 primary 寫完 OK&lt;/td>
 &lt;td>client 等 standby ack（額外 RTT）&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Consistency&lt;/strong>&lt;/td>
 &lt;td>standby 隨時可能 stale&lt;/td>
 &lt;td>standby 跟 primary 保證讀到一致&lt;/td>
 &lt;td>application read routing rule（不是 replication 旋鈕）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟這三軸獨立的、是 &lt;em>replication 機制本身的可維護性&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LSN（Log Sequence Number）&lt;/strong>：PG 用全域 byte offset 標 WAL 進度、所有 standby 同步用 LSN 對齊、不像 MySQL 早期 binlog position + file 雙欄&lt;/li>
&lt;li>&lt;strong>Replication slot&lt;/strong>：primary 紀錄每個 standby 已接收的 LSN、防 standby 失聯期間 WAL 被清掉、是 streaming replication 的 &lt;em>持久化進度追蹤&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology&lt;/a> 對比、PG 的 LSN + replication slot 直接內建 &lt;em>standby 進度追蹤&lt;/em>、不像 MySQL 5.7- 要靠 binlog position + GTID 雙機制；但 slot 是 &lt;em>primary 紀錄&lt;/em>、orphan slot 是 PG-specific 議題（slot 留 WAL 直到 standby 重連、standby 永久失聯 → primary disk 爆）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>streaming replication topology</em> — 從 single primary 到 multi-standby 部署的 3 個 trade-off 軸 + LSN + replication slot 機制。</p></blockquote>
<hr>
<h2 id="replication-的-3-個-trade-off-軸--mode-選擇">Replication 的 3 個 trade-off 軸 + mode 選擇</h2>
<p>PG streaming replication mode 選擇看起來是「async 還是 sync」、實際是 3 個獨立 trade-off 軸的組合、async / sync / quorum-based sync 是這些軸的常見組合 <em>名稱</em>：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>端 A</th>
          <th>端 B</th>
          <th>PG 旋鈕</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Durability</strong></td>
          <td>primary 寫完就 commit</td>
          <td>至少一個 standby 收到才 commit</td>
          <td><code>synchronous_commit</code> / <code>synchronous_standby_names</code></td>
      </tr>
      <tr>
          <td><strong>Latency</strong></td>
          <td>client 等 primary 寫完 OK</td>
          <td>client 等 standby ack（額外 RTT）</td>
          <td>同上</td>
      </tr>
      <tr>
          <td><strong>Consistency</strong></td>
          <td>standby 隨時可能 stale</td>
          <td>standby 跟 primary 保證讀到一致</td>
          <td>application read routing rule（不是 replication 旋鈕）</td>
      </tr>
  </tbody>
</table>
<p>跟這三軸獨立的、是 <em>replication 機制本身的可維護性</em>：</p>
<ul>
<li><strong>LSN（Log Sequence Number）</strong>：PG 用全域 byte offset 標 WAL 進度、所有 standby 同步用 LSN 對齊、不像 MySQL 早期 binlog position + file 雙欄</li>
<li><strong>Replication slot</strong>：primary 紀錄每個 standby 已接收的 LSN、防 standby 失聯期間 WAL 被清掉、是 streaming replication 的 <em>持久化進度追蹤</em></li>
</ul>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a> 對比、PG 的 LSN + replication slot 直接內建 <em>standby 進度追蹤</em>、不像 MySQL 5.7- 要靠 binlog position + GTID 雙機制；但 slot 是 <em>primary 紀錄</em>、orphan slot 是 PG-specific 議題（slot 留 WAL 直到 standby 重連、standby 永久失聯 → primary disk 爆）。</p>
<h2 id="async-streamingdefault--高-throughput-的代價">Async streaming：default + 高 throughput 的代價</h2>
<p>Async 是 PG 預設、行為：</p>
<ol>
<li>Primary 寫 WAL 進 <code>pg_wal/</code> 目錄、commit、回應 client OK</li>
<li>WAL sender process 把 WAL stream 給 standby</li>
<li>Standby WAL receiver 寫 standby 的 <code>pg_wal/</code>、startup 進程 redo 套用</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>Durability：primary commit 後 standby 還沒收 → primary 永久故障 → <em>data loss</em>（已 commit 的 transaction 在 standby 不存在）</li>
<li>Latency：client 寫入延遲 = primary 自身 fsync WAL 的時間（<code>fsync=on</code> + <code>synchronous_commit=on</code> 預設、通常 &lt; 1ms 在 SSD / NVMe）</li>
<li>Consistency：standby 可能 lag、application 讀 standby 會 stale；用 <code>pg_stat_replication.write_lag / flush_lag / replay_lag</code> 看</li>
</ul>
<p><strong>配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf on primary</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">wal_level</span> <span class="o">=</span> <span class="s">replica          # 至少 replica（logical 是 superset）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">max_wal_senders</span> <span class="o">=</span> <span class="s">10         # 並行 WAL sender process 數（依 standby 數量）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">wal_keep_size</span> <span class="o">=</span> <span class="s">1024MB       # WAL 保留量（slot 為主、但 backup buffer）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">synchronous_commit</span> <span class="o">=</span> <span class="s">on      # 預設、primary 自己 fsync WAL</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># synchronous_standby_names 留空 = async</span></span></span></code></pre></div><p><strong>適用</strong>：</p>
<ul>
<li>主流選擇（90% 場景）</li>
<li>Failover loss 在容忍範圍（多數 web 應用容忍 1-2 秒 data loss）</li>
<li>Read scaling 為主要 driver、絕對 durability 非首要</li>
</ul>
<h2 id="sync-streaming至少一個-standby-flush-wal-才-commit">Sync streaming：至少一個 standby flush WAL 才 commit</h2>
<p>Sync mode 在 async 基礎上加 <em>primary 等指定 standby flush WAL 才回 client</em>：</p>
<ol>
<li>Primary 寫 WAL、send to standby</li>
<li>Standby 收到 WAL、寫進 <code>pg_wal/</code>、fsync、回 ack</li>
<li><em>Primary 等 ack</em> → commit → 回 client</li>
</ol>
<p><code>synchronous_commit</code> 有 5 個 level、不是 binary：</p>
<table>
  <thead>
      <tr>
          <th>Level</th>
          <th>行為</th>
          <th>Latency 影響</th>
          <th>Crash data loss</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>off</code></td>
          <td>primary 不等自己 fsync、background flush</td>
          <td>+0</td>
          <td>primary crash 丟 0-1 秒</td>
      </tr>
      <tr>
          <td><code>local</code></td>
          <td>primary fsync own WAL（不等 standby）</td>
          <td>baseline</td>
          <td>primary crash 0、standby 丟</td>
      </tr>
      <tr>
          <td><code>remote_write</code></td>
          <td>primary fsync + standby 收到（不必 standby fsync）</td>
          <td>+1 RTT 大致</td>
          <td>OS crash on standby 丟</td>
      </tr>
      <tr>
          <td><code>on</code> (預設)</td>
          <td>primary fsync + standby fsync（standby 收進 disk）</td>
          <td>+1 RTT + fsync</td>
          <td>全 crash 都不丟</td>
      </tr>
      <tr>
          <td><code>remote_apply</code></td>
          <td>primary fsync + standby fsync + standby 已 <em>replay</em>（visible to read）</td>
          <td>+1 RTT + fsync + replay</td>
          <td>全 crash 都不丟 + replica 立刻可讀</td>
      </tr>
  </tbody>
</table>
<p><strong>配置（synchronous）</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">synchronous_commit</span> <span class="o">=</span> <span class="s">on</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">synchronous_standby_names</span> <span class="o">=</span> <span class="s">&#39;FIRST 1 (standby1, standby2)&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># &#39;FIRST 1&#39; = 第一個 active standby ack 即可</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># &#39;ANY 2 (s1, s2, s3)&#39; = 任 2 個 ack 即可（quorum-based）</span></span></span></code></pre></div><p><strong>Quorum-based sync</strong>：用 <code>ANY N</code> 語法、達到 N 個 ack 就 commit、提高 latency stability（不依賴特定 standby）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">synchronous_standby_names</span> <span class="o">=</span> <span class="s">&#39;ANY 2 (standby1, standby2, standby3)&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 3 個 standby 中任 2 個 ack 即 commit</span></span></span></code></pre></div><p><strong>適用</strong>：</p>
<ul>
<li>金融交易 / 訂單 / payment ledger（不允許 data loss）</li>
<li>已有 multi-AZ deploy、replica 物理上可靠</li>
<li>可接受寫入延遲 +1-3ms (跨 AZ)</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>跨 region sync（RTT 50-200ms）— 寫吞吐砍半、改用 <em>region-local sync + cross-region async</em></li>
<li>寫吞吐 &gt; 50K WPS + 容忍 sub-second loss — async 即可</li>
</ul>
<h2 id="lsn--replication-slotpg-的進度追蹤機制">LSN + Replication Slot：PG 的進度追蹤機制</h2>
<p>PG 每個 WAL 寫入都標 <em>LSN</em>（64-bit byte offset）。Standby 紀錄 <em>已收到 / 已 flush / 已 replay</em> 的 LSN、primary 透過 streaming protocol 知道每個 standby 進度。</p>
<p><strong>Replication slot</strong> 是 <em>primary 端的 standby 進度紀錄</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 建 physical replication slot（給 streaming replication 用）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_create_physical_replication_slot</span><span class="p">(</span><span class="s1">&#39;standby1_slot&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 查 slot 狀態
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">slot_name</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">,</span><span class="w"> </span><span class="n">confirmed_flush_lsn</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_wal_lsn_diff</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">lag</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="p">;</span></span></span></code></pre></div><p><strong>Slot 的核心責任</strong>：</p>
<ul>
<li><em>防 WAL premature deletion</em>：standby 失聯（restart / network blip）、primary 仍保留 slot 對應 LSN 之後的 WAL、standby 重連可繼續 stream</li>
<li><em>無需 base backup re-build</em>：跟沒 slot 的 standby 對比、有 slot 的 standby 失聯後重連、不用重建</li>
</ul>
<p><strong>Slot 跟 <code>wal_keep_size</code></strong>：</p>
<ul>
<li><code>wal_keep_size</code>（PG 13+）/ <code>wal_keep_segments</code>（&lt; 13）：minimum WAL 保留量、不依賴 slot</li>
<li>Slot 是 <em>動態保留</em>：直到 slot 的 standby 推進 LSN 才釋放對應 WAL</li>
<li>兩者組合：<code>wal_keep_size</code> 是底線、slot 是 standby-specific 動態保留</li>
</ul>
<p><strong>Standby 配置（用 slot）</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># standby1 postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">primary_conninfo</span> <span class="o">=</span> <span class="s">&#39;host=primary.example.com port=5432 user=replication password=...&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">primary_slot_name</span> <span class="o">=</span> <span class="s">&#39;standby1_slot&#39;   # 用 primary 上預先建的 slot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">hot_standby</span> <span class="o">=</span> <span class="s">on                       # 讓 standby 接受 read query</span></span></span></code></pre></div><p><code>standby.signal</code> 空檔案在 PG_DATA 內、告訴 PG 這是 standby、進入 recovery mode。</p>
<h2 id="配置-step-by-stepsync-streaming--slot">配置 step-by-step（sync streaming + slot）</h2>
<p>實務最常見組合：sync streaming + replication slot + cross-AZ replica。</p>
<h3 id="step-1primary-配置">Step 1：Primary 配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">wal_level</span> <span class="o">=</span> <span class="s">replica</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">max_wal_senders</span> <span class="o">=</span> <span class="s">10</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">max_replication_slots</span> <span class="o">=</span> <span class="s">10</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">synchronous_commit</span> <span class="o">=</span> <span class="s">on</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">synchronous_standby_names</span> <span class="o">=</span> <span class="s">&#39;FIRST 1 (standby1, standby2)&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">wal_keep_size</span> <span class="o">=</span> <span class="s">1024MB</span>
</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="c1"># pg_hba.conf — 允許 replication 連線</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">host replication replication 10.0.0.0/16 scram-sha-256</span></span></span></code></pre></div><p>Restart primary 套用。</p>
<h3 id="step-2建-replication-user--slot">Step 2：建 replication user + slot</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="n">replication</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">REPLICATION</span><span class="w"> </span><span class="n">PASSWORD</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_create_physical_replication_slot</span><span class="p">(</span><span class="s1">&#39;standby1_slot&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_create_physical_replication_slot</span><span class="p">(</span><span class="s1">&#39;standby2_slot&#39;</span><span class="p">);</span></span></span></code></pre></div><h3 id="step-3standby-base-backup">Step 3：Standby base backup</h3>





<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"># 在 standby 上跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pg_basebackup -h primary.example.com -D /var/lib/postgresql/data <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -U replication -P -X stream <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -S standby1_slot -R
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># -R: 自動生成 standby.signal + primary_conninfo</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># -X stream: 邊 backup 邊 stream 增量 WAL（避免 backup 期間 WAL gap）</span></span></span></code></pre></div><h3 id="step-4standby-啟動">Step 4：Standby 啟動</h3>





<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"># standby /var/lib/postgresql/data/postgresql.auto.conf 已有：</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># primary_conninfo = &#39;host=primary.example.com user=replication password=... application_name=standby1&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># primary_slot_name = &#39;standby1_slot&#39;</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">pg_ctl -D /var/lib/postgresql/data start</span></span></code></pre></div><h3 id="step-5驗證">Step 5：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Primary: 確認 standby 連上
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">application_name</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">sync_state</span><span class="p">,</span><span class="w"> </span><span class="n">write_lag</span><span class="p">,</span><span class="w"> </span><span class="n">flush_lag</span><span class="p">,</span><span class="w"> </span><span class="n">replay_lag</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_replication</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 應顯示 standby1 / streaming / sync / 各 lag
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Standby: 確認在 recovery + 收到 WAL
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_is_in_recovery</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_receive_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_replay_lsn</span><span class="p">();</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-standby-lag-暴衝--single-replay-process-bottleneck">1. Standby lag 暴衝 — Single replay process bottleneck</h3>
<p>PG standby 是 <em>single startup process</em> 套用 WAL（不像 MySQL multi-thread replication）、primary 高並發寫入時 standby 跟不上、lag 從 &lt; 100ms 飆到分鐘級。常見觸發：批次 UPDATE / DELETE、大 transaction、index 建立、autovacuum 大量 dead tuple cleanup。</p>
<p>修法：</p>
<ul>
<li><em>Parallel WAL apply</em>（PG 14+）：<code>max_parallel_workers_per_gather</code> 增加 background worker、但仍受 startup process 主導</li>
<li>對 <em>read scaling</em> 場景接受 standby lag、application 用 <em>primary read 對 latency-critical query</em></li>
<li><em>Cascading replication</em> 對 high-fan-out 解決 sender CPU bottleneck、但 standby replay 仍 single-thread</li>
</ul>
<p>監控：<code>pg_stat_replication.replay_lag</code> 是 <em>最後一個 commit 到 standby replay 的時間差</em>、超過 threshold 即告警。</p>
<h3 id="2-sync-standby-失聯時-primary-commit-卡住">2. Sync standby 失聯時 primary commit 卡住</h3>
<p><code>synchronous_standby_names = 'FIRST 1 (standby1)'</code> + standby1 down → primary commit <em>等永遠</em>。Application 全部 timeout。</p>
<p>修法：</p>
<ul>
<li>用 <code>ANY N</code> quorum：<code>synchronous_standby_names = 'ANY 1 (standby1, standby2)'</code> — 任一 standby ack 即可</li>
<li>設多 standby、防單一失聯</li>
<li>監控 sync standby 健康、自動 failover 切 sync mode 到其他 standby（Patroni 自動做）</li>
<li>緊急情況：在 primary 跑 <code>ALTER SYSTEM SET synchronous_standby_names = ''; SELECT pg_reload_conf();</code> 暫時退 async（接受 data loss risk）</li>
</ul>
<h3 id="3-orphan-replication-slot--primary-disk-爆">3. Orphan replication slot — Primary disk 爆</h3>
<p>Standby 失聯（永久故障 / 重 decommission 但忘了 drop slot）、primary slot 持續保留 WAL、<code>pg_wal/</code> 累積到 disk 滿、primary 也掛。</p>
<p>修法：</p>
<ul>
<li>
<p>監控 <code>pg_replication_slots.active</code> — <code>false</code> 持續 &gt; N 小時是警訊</p>
</li>
<li>
<p>監控 slot lag：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">slot_name</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_wal_lsn_diff</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">retained_wal</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">retained_wal</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span><span class="n">GB</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>設 <code>max_slot_wal_keep_size</code>（PG 13+）— slot 對應 WAL 超過 limit 自動 invalidate slot（standby 之後要 base backup 重來）</p>
</li>
<li>
<p>DR runbook 紀錄 <em>standby 退役流程</em> 必須包含 <code>pg_drop_replication_slot('xxx')</code></p>
</li>
</ul>
<h3 id="4-cascading-replication-雪崩">4. Cascading replication 雪崩</h3>
<p>Topology <code>primary → standby1 → standby2 → ...</code>（每層遞迴 stream）。Standby1 startup process 卡住、後續 standby 都被 block、整條 chain 雪崩。</p>
<p>修法：</p>
<ul>
<li>避免超過 2 層 cascade（primary → tier1 → tier2 是上限）</li>
<li>跨 region 用 <em>region-local tier1 + cross-region tier2</em>、不是長 chain</li>
<li>真的大規模、改用 <em>binlog server</em> style：<a href="https://github.com/postgresml/PgCat">Citus / PgCat</a> 等中介、或 logical replication 解耦</li>
</ul>
<h3 id="5-failover-後-timeline-分歧">5. Failover 後 timeline 分歧</h3>
<p>Primary 失敗、standby1 promote 為新 primary、其他 standby（standby2 / 3）原本連舊 primary、必須重新連 standby1。但 PG 用 <em>timeline</em>（每次 promotion 增 1）標 WAL 分支、原 standby 的 timeline 跟新 primary 不同。重連時看到 timeline mismatch、報錯。</p>
<p>修法：</p>
<ul>
<li><em>pg_rewind</em> 工具：對比新 primary 跟舊 standby 的 timeline 分歧點、把舊 standby 上 <em>新 primary 沒有的 WAL</em> 倒退、然後從分歧點重新跟新 primary 同步</li>
<li><em>Base backup re-build</em>：對舊 standby 重建 — 慢但保證乾淨</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni</a> 自動處理 pg_rewind / base backup 選擇</li>
</ul>
<h2 id="容量--cost-對照">容量 / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>寫吞吐影響</th>
          <th>Standby overhead</th>
          <th>適合 workload</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Async streaming + slot</td>
          <td>baseline</td>
          <td>低（WAL receive + startup）</td>
          <td>高吞吐、容忍 sub-second loss</td>
      </tr>
      <tr>
          <td>Sync <code>remote_write</code> + 1 standby</td>
          <td>-5% ~ -10%</td>
          <td>同上 + RTT</td>
          <td>一般 production、可接受 OS crash 丟</td>
      </tr>
      <tr>
          <td>Sync <code>on</code> + 1 standby</td>
          <td>-10% ~ -20%</td>
          <td>同上 + fsync</td>
          <td>金融、訂單、不容忍 data loss</td>
      </tr>
      <tr>
          <td>Sync <code>on</code> + ANY 2 quorum</td>
          <td>-15% ~ -30%</td>
          <td>同上、跨 AZ</td>
          <td>強 durability + multi-AZ HA</td>
      </tr>
      <tr>
          <td>Sync <code>remote_apply</code> + 1 standby</td>
          <td>-20% ~ -40%</td>
          <td>同上 + replay</td>
          <td>強一致 read on standby（少用、成本高）</td>
      </tr>
  </tbody>
</table>
<p>跨 AZ sync 通常加 1-3ms、跨 region 加 50-200ms — 寫密集 workload 跨 region sync 通常不划算、改用 <em>region-local sync + cross-region async chain</em>。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="patroni-ha">Patroni HA</h3>
<p><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni</a> 是 PG HA 自動 failover 標準、依賴 DCS（etcd / Consul）+ 本文 replication topology。Patroni 自動：</p>
<ul>
<li>偵測 primary 失聯、promote 適合 standby</li>
<li>處理 timeline 分歧（pg_rewind）</li>
<li>重配 sync standby（避免 sync standby 失聯卡 primary）</li>
</ul>
<h3 id="logical-replication--debezium">Logical Replication + Debezium</h3>
<p><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical replication + Debezium</a> 是 <em>跟 streaming replication 共用 WAL</em> 但不同 abstraction — logical decoding output event、streaming replication output physical bytes。Logical replication slot 跟 physical slot 共存、各自獨立 retention。</p>
<h3 id="pitr--wal-archiving">PITR + WAL Archiving</h3>
<p><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a> 用 <em>archive_command</em> 把 WAL ship 到 S3、跟 streaming replication 並行：</p>
<ul>
<li>Streaming：給 <em>活的 standby</em>（real-time read scaling / HA）</li>
<li>Archive：給 <em>PITR + 新 standby base backup source</em></li>
</ul>
<p>兩者使用同一 WAL stream、不衝突。</p>
<h3 id="connection-路由pgbouncer--readwrite-split">Connection 路由（PgBouncer + read/write split）</h3>
<p><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> 不做 read/write split（transaction pool 不看 SQL）。Read replica routing 通常用 <em>application-level</em> 或 <em>HAProxy 監控 standby health</em>。</p>
<h3 id="跟-mysql-replication-topology-對比">跟 MySQL Replication Topology 對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG streaming replication</th>
          <th>MySQL replication</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>進度追蹤</td>
          <td>LSN（單一 byte offset）</td>
          <td>GTID 或 binlog (file, position)</td>
      </tr>
      <tr>
          <td>標準工具</td>
          <td>streaming replication（physical）+ logical</td>
          <td>binlog ROW format</td>
      </tr>
      <tr>
          <td>Sync 機制</td>
          <td><code>synchronous_commit</code> + standby names</td>
          <td>semi-sync plugin</td>
      </tr>
      <tr>
          <td>Quorum</td>
          <td><code>ANY N</code> syntax</td>
          <td><code>rpl_semi_sync_master_wait_for_slave_count</code></td>
      </tr>
      <tr>
          <td>Replay parallelism</td>
          <td>Single startup process</td>
          <td>Multi-thread (logical clock / writeset)</td>
      </tr>
      <tr>
          <td>Replica routing</td>
          <td>PgBouncer 不看 SQL、需外接</td>
          <td>ProxySQL 內建 query routing</td>
      </tr>
  </tbody>
</table>
<p>兩者 high-level 對等、低層機制有顯著差異。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PG Patroni HA</a>（HA failover、依賴本文 replication topology）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PG Logical Replication + Debezium</a>（不同 abstraction、共用 WAL）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PG PITR + WAL Archiving</a>（streaming + archive 並行）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PG PgBouncer</a>（connection pool、不做 read/write split）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（sibling、不同機制）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡片</a> / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read 卡片</a> / <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual-consistency 卡片</a></li>
<li>官方：<a href="https://www.postgresql.org/docs/current/warm-standby.html">PG Streaming Replication</a> / <a href="https://www.postgresql.org/docs/current/app-pgbasebackup.html">pg_basebackup</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → DragonflyDB：drop-in 相容下的容量躍升 + 5 個踩雷</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>（target）。跟前一篇 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security&lt;/a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 &lt;em>drop-in 相容&lt;/em> 形態的 migration、結構更接近 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a> 的 6-section flow + 一段「相容性驗證」前置。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Memory cost&lt;/strong>&lt;/td>
 &lt;td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Single-thread bottleneck&lt;/strong>&lt;/td>
 &lt;td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Multi-tenancy&lt;/strong>&lt;/td>
 &lt;td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 &lt;em>Redis Modules 依賴&lt;/em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 &lt;em>Lua script 用了 redis.call 進階 API&lt;/em>。&lt;/p>
&lt;h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased&lt;/h2>
&lt;p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration &lt;em>結構接近 standard deep article&lt;/em>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（target）。跟前一篇 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 <em>drop-in 相容</em> 形態的 migration、結構更接近 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a> 的 6-section flow + 一段「相容性驗證」前置。</p></blockquote>
<h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Memory cost</strong></td>
          <td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM</td>
      </tr>
      <tr>
          <td><strong>Single-thread bottleneck</strong></td>
          <td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x</td>
      </tr>
      <tr>
          <td><strong>Multi-tenancy</strong></td>
          <td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 <em>Redis Modules 依賴</em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 <em>Lua script 用了 redis.call 進階 API</em>。</p>
<h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased</h2>
<p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration <em>結構接近 standard deep article</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Splunk → Elastic（phased）</th>
          <th>Redis → DragonflyDB（drop-in）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 對位</td>
          <td>需要（SPL ↔ KQL / CIM ↔ ECS）</td>
          <td>不需要（RESP protocol 相容）</td>
      </tr>
      <tr>
          <td>Rule translation</td>
          <td>4-12 週 SOC engineering 工作</td>
          <td>不需要（command 直接相容）</td>
      </tr>
      <tr>
          <td>Parallel run</td>
          <td>4-8 週 dual-SIEM 跑</td>
          <td>1-7 天 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Cutover 邊界</td>
          <td>軟邊界（routing 切換、可逆 30 分鐘）</td>
          <td>硬邊界（client 配置切換、單次完成）</td>
      </tr>
      <tr>
          <td>不可逆 cleanup</td>
          <td>1 年後 archive</td>
          <td>立刻（DragonflyDB 接管後 Redis 可關）</td>
      </tr>
      <tr>
          <td>整體週期</td>
          <td>4-9 個月</td>
          <td>1-4 週</td>
      </tr>
  </tbody>
</table>
<p><strong>判斷依據</strong>：migration 結構由 <em>source 跟 target 的 schema / protocol 差異程度</em> 決定、不是 universal phased playbook。本批第 2 篇驗證 <em>deep article methodology 的 6-section 框架</em> 在 drop-in migration 仍適用（只需前置 <em>相容性驗證</em> 段、其他 6 段對位）。</p>
<h2 id="相容性驗證在-cutover-前要確認的清單">相容性驗證：在 cutover 前要確認的清單</h2>
<p>DragonflyDB 號稱 Redis drop-in、但「drop-in」涵蓋範圍依 Redis feature 使用程度而定。Pre-migration 必跑的相容性 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>DragonflyDB 支援程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Basic data types (String / Hash / List / Set / ZSet)</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RESP protocol v2 / v3</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB load</td>
          <td>Redis 6.x RDB 完全相容；7.x 部分 feature 待測</td>
          <td>用 BGSAVE → 切換 → load 驗證</td>
      </tr>
      <tr>
          <td>AOF</td>
          <td>DragonflyDB 不用 AOF、改 <em>snapshotting</em> 模式</td>
          <td>不直接 import AOF、需經 RDB 中介</td>
      </tr>
      <tr>
          <td>Lua scripts</td>
          <td>90% 相容、部分 redis.call API + EVAL 邊界 case 差異</td>
          <td>Lua script audit 必跑、不能假設全相容</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>相容、但 message fanout 行為差異（多 thread 處理）</td>
          <td>高 fanout pub/sub 場景需測 latency</td>
      </tr>
      <tr>
          <td>Cluster mode</td>
          <td>DragonflyDB <em>單機</em> 即可達 cluster throughput、不必 cluster；emulated cluster mode 部分相容</td>
          <td>評估是否仍需 cluster</td>
      </tr>
      <tr>
          <td>Sentinel HA</td>
          <td>不直接支援、用 DragonflyDB 自家 replication</td>
          <td>HA 架構重設計</td>
      </tr>
      <tr>
          <td>Redis Modules (RedisJSON / Search / Graph)</td>
          <td><strong>不支援</strong></td>
          <td>必須前置改寫 application</td>
      </tr>
      <tr>
          <td>Streams</td>
          <td>相容、但 consumer group 行為部分差異</td>
          <td>Stream consumer 跑 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Keyspace notifications</td>
          <td>相容</td>
          <td>無需處理</td>
      </tr>
  </tbody>
</table>
<p><strong>Audit 的關鍵 output</strong>：列「不相容功能」清單 + 對應 application code 修改範圍；若 Modules 在 production 使用、migration <em>退役</em>。</p>
<h2 id="step-by-step-cutover">Step-by-step cutover</h2>





<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. 部署 DragonflyDB</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name dragonfly -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/dragonfly:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly:latest <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --logtostderr --requirepass<span class="o">=</span>&lt;your_password&gt;
</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="c1"># 2. Redis 端 BGSAVE</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 等到 BGSAVE 完成</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</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 class="c1"># 3. 把 dump.rdb 拷到 DragonflyDB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb dragonfly-host:/data/dragonfly/
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 4. 重啟 DragonflyDB 載入 RDB</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">docker restart dragonfly
</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="c1"># 5. 驗證資料一致</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">redis-cli -h dragonfly-host -p <span class="m">6380</span> DBSIZE
</span></span><span class="line"><span class="ln">20</span><span class="cl">redis-cli -h redis-primary DBSIZE
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 兩端 key 數對齊</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 6. Dual-write 1-7 天（application 同時寫兩端）</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># 7. Read 切換到 DragonflyDB、Redis 端只寫不讀</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># 8. Write 切換、Redis 端 standby</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 9. 觀察 1-2 週、無異常後 Redis decommission</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>BGSAVE → load</strong>：100GB RDB 約 5-15 分鐘、跨網路 SCP 時間另算</li>
<li><strong>Dual-write window</strong>：1-7 天觀察、application 寫兩端、read 仍走 Redis</li>
<li><strong>Cutover</strong>：read switch → write switch、每步間隔 24 小時</li>
<li><strong>Decom</strong>：Redis 保留 standby 1-2 週、無異常後關閉</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1rdb-版本差dragonflydb-load-失敗">Case 1：RDB 版本差，DragonflyDB load 失敗</h3>
<p><strong>徵兆</strong>：Redis 7.2 端 BGSAVE 出的 <code>dump.rdb</code> 在 DragonflyDB load 時報 <code>Unsupported RDB version</code>、DragonflyDB 啟動失敗。</p>
<p><strong>根因</strong>：Redis 7.2 RDB version 11 含新 feature（function library / sharded pubsub）DragonflyDB 當前 release 沒支援；版本相容性需逐 release 確認。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 版本相容矩陣 audit</strong>：DragonflyDB release note 對照 Redis version、確認 RDB version 支援</li>
<li><strong>降級 BGSAVE</strong>：Redis 端設 <code>rdb-version 9</code>（Redis 6.x 兼容版本）、犧牲 Redis 7.x 新 feature</li>
<li><strong>替代方案</strong>：用 <code>redis-cli --scan</code> + <code>MIGRATE</code> 命令 incremental 搬、不用 RDB；速度慢 100x 但相容性好</li>
</ol>
<h3 id="case-2lua-script-跑進-eval-不一致">Case 2：Lua script 跑進 EVAL 不一致</h3>
<p><strong>徵兆</strong>：dual-write 階段、發現某些 EVAL script 在 Redis 跟 DragonflyDB 結果不同；具體是某個 <code>redis.call(&quot;OBJECT&quot;, &quot;ENCODING&quot;, key)</code> 在 DragonflyDB 回不一樣的 encoding 字串。</p>
<p><strong>根因</strong>：DragonflyDB 內部不用 Redis 的 ziplist / listpack encoding（dashtable 不需要）、<code>OBJECT ENCODING</code> 返回值不對等；script 邏輯依賴 encoding 來決定行為、結果不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit Lua script</strong>：grep 所有 <code>redis.call(&quot;OBJECT&quot;</code>、列出依賴 encoding 的 script</li>
<li><strong>改寫 application</strong>：不依賴 encoding、改用 <code>MEMORY USAGE</code> 或 high-level check</li>
<li><strong>接受差異</strong>：DragonflyDB 不會回 encoding 但 functional 結果對等、SOC review 確認可接受</li>
</ol>
<h3 id="case-3pubsub-fanout-高負載-latency">Case 3：Pub/Sub fanout 高負載 latency</h3>
<p><strong>徵兆</strong>：production 切到 DragonflyDB 後、Pub/Sub 訂閱端 latency p99 從 5ms 漲到 20-50ms；topic fanout &gt;10K subscriber 場景。</p>
<p><strong>根因</strong>：DragonflyDB 多 thread 設計、Pub/Sub message 在 thread 間 dispatch 需要 routing；Redis single-thread 沒這個 overhead。高 fanout 是 DragonflyDB 設計取捨。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：高 fanout Pub/Sub 不用 DragonflyDB、改 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> / Redis Streams + consumer group</li>
<li><strong>DragonflyDB 配置調整</strong>：<code>--proactor_threads</code> 對 Pub/Sub 影響大、調到符合 CPU 核心數</li>
<li><strong>接受 latency</strong>：&lt; 10K subscriber 差異可忽略、不必動</li>
</ol>
<h3 id="case-4cluster-mode-看似相容但-slot-routing-行為差">Case 4：Cluster mode 看似相容但 slot routing 行為差</h3>
<p><strong>徵兆</strong>：application 用 Redis Cluster client（lettuce / Jedis cluster mode）連 DragonflyDB emulated cluster、運行幾天後 <code>MOVED</code> redirect 異常、key 找不到。</p>
<p><strong>根因</strong>：DragonflyDB emulated cluster mode 是 <em>single node 模擬</em>、CLUSTER SLOTS 返回固定 mapping；某些 client 端 cluster topology cache 跟實際 routing 不對齊、發 redirect。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 改 standalone client</strong>：DragonflyDB single node 已能達 cluster 級 throughput、不必用 cluster client</li>
<li><strong>Client config</strong>：lettuce 端 <code>clusterTopologyRefreshOptions(...)</code> 設較長 refresh、減少 redirect 機會</li>
<li><strong>長期</strong>：等 DragonflyDB cluster 正式 GA 後再評估</li>
</ol>
<h3 id="case-5modules-用了沒注意migration-卡住">Case 5：Modules 用了沒注意，migration 卡住</h3>
<p><strong>徵兆</strong>：cutover 後幾天、application 某個功能完全壞、log 顯示 <code>ERR unknown command 'JSON.SET'</code>；DragonflyDB 不支援 RedisJSON。</p>
<p><strong>根因</strong>：Pre-migration audit 漏掉 application 用了 RedisJSON（透過某 client library 抽象）；DragonflyDB 不支援該 Module 命令、application 直接壞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit 必跑</strong>：<code>MONITOR</code> 抓 1 小時 production traffic、grep 非 standard command（<code>JSON.*</code> / <code>FT.*</code> / <code>GRAPH.*</code>）</li>
<li><strong>應急回退</strong>：Redis standby 還在、application client config 切回</li>
<li><strong>長期</strong>：JSON 改用 standard Hash + serialization、Search 改 Elasticsearch / Meilisearch、Graph 改 Neo4j</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>DragonflyDB</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-node throughput</td>
          <td>~100K-200K ops/s</td>
          <td>~2-5M ops/s（號稱 25x）</td>
          <td>DragonflyDB 領先、實測依 workload 而定</td>
      </tr>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>-30% 平均、依資料分佈</td>
          <td>DragonflyDB 領先</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF 雙模式</td>
          <td>Snapshotting 為主、不用 AOF</td>
          <td>Redis 對 durability 要求高的 workload 仍領先</td>
      </tr>
      <tr>
          <td>HA / Replication</td>
          <td>Sentinel + Cluster 成熟</td>
          <td>自家 replication、HA 文件相對少</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Modules ecosystem</td>
          <td>RedisJSON / Search / Graph / TimeSeries</td>
          <td>不支援</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Cluster scaling</td>
          <td>Cluster mode 成熟</td>
          <td>單機效能高、cluster 仍 emerging</td>
          <td>Redis 領先、但 DragonflyDB 單機已能 cover 多數 use case</td>
      </tr>
      <tr>
          <td>Total cost (10TB cache)</td>
          <td>$8-15K USD / month</td>
          <td>$2-5K USD / month</td>
          <td>DragonflyDB 顯著便宜</td>
      </tr>
      <tr>
          <td>Operational maturity</td>
          <td>高（10+ 年 production）</td>
          <td>中（2022+、production 案例 1000+）</td>
          <td>Redis 領先</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：cache use case 簡單（pure cache / session store）走 DragonflyDB；複雜 use case（Modules / Pub/Sub fanout / strict durability）保留 Redis。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-client-library-整合">跟 client library 整合</h3>
<p>主流 Redis client（lettuce / Jedis / redis-py / node-redis / go-redis）都直接相容 DragonflyDB；唯一例外是 <em>cluster client</em> 模式行為差（見 Case 4）。</p>
<h3 id="跟-monitoring-整合">跟 monitoring 整合</h3>
<p>DragonflyDB exporter 提供 Prometheus metric、跟 Redis exporter 對應 metric 名稱 80% 相同；grafana dashboard 需小改：</p>
<ul>
<li><code>redis_memory_used_bytes</code> → <code>dragonfly_memory_used_bytes</code></li>
<li><code>redis_commands_processed_total</code> → <code>dragonfly_commands_processed_total</code></li>
</ul>
<h3 id="跟-redis-sentinel-ha-對位">跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Redis Sentinel HA</a> 對位</h3>
<p>DragonflyDB 不直接支援 Sentinel、HA 走自家 <em>master-replica</em> + DNS-based failover：</p>
<ol>
<li>DragonflyDB primary + replica</li>
<li>K8s 用 StatefulSet + Service + readiness probe</li>
<li>失敗 failover 比 Sentinel 慢（30s-2min vs 5-15s）</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>DragonflyDB Cluster GA</strong>：正式 cluster mode 出來後重評估</li>
<li><strong>Stream + consumer group 細節</strong>：dual-write 期間驗證每個 consumer pattern</li>
<li><strong>Modules 替代方案</strong>：JSON / Search / Graph 各自的 cloud-native 替代評估</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Self-managed ELK → Elastic Cloud：5 年 ELK 集群的 lifecycle 收尾</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a> 跟 Elastic Cloud。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High（self-managed → Elastic managed）→ Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="5-年-elk-集群的-lifecycle-收尾">5 年 ELK 集群的 lifecycle 收尾&lt;/h2>
&lt;p>跟前批 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> 同 Type C、本文用 &lt;em>lifecycle-driven&lt;/em> entry — 看 5 年 ELK 集群典型壽命曲線：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>年份&lt;/th>
 &lt;th>Phase&lt;/th>
 &lt;th>集群狀態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>0-1&lt;/td>
 &lt;td>Build&lt;/td>
 &lt;td>3 node、簡單部署、SOC 學 Lucene query / dashboard / alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1-2&lt;/td>
 &lt;td>Scale-out&lt;/td>
 &lt;td>5-7 node、shard 計畫、hot/warm/cold tier、index lifecycle management&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2-3&lt;/td>
 &lt;td>Degrade&lt;/td>
 &lt;td>10+ node、shard 過多、query latency 升、upgrade window 開始痛&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3-4&lt;/td>
 &lt;td>Save&lt;/td>
 &lt;td>加 dedicated master / cross-cluster replication、ops cost 飛漲&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4-5&lt;/td>
 &lt;td>Migrate decision&lt;/td>
 &lt;td>評估走 Elastic Cloud（managed）或下一個 SIEM vendor&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數中型 organization 在 lifecycle 第 4-5 年遇到 &lt;em>operational ceiling&lt;/em> — SRE team 0.5-1.5 FTE 跑 ELK ops、新 feature 開發停滯、cost 跟 alternative observability vendor 比較。Elastic Cloud 把 operational stack 全託管、SOC 留在 &lt;em>Lucene query + dashboard + alert&lt;/em> 層、不再管 cluster sizing。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> 跟 Elastic Cloud。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High（self-managed → Elastic managed）→ Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="5-年-elk-集群的-lifecycle-收尾">5 年 ELK 集群的 lifecycle 收尾</h2>
<p>跟前批 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 同 Type C、本文用 <em>lifecycle-driven</em> entry — 看 5 年 ELK 集群典型壽命曲線：</p>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>Phase</th>
          <th>集群狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-1</td>
          <td>Build</td>
          <td>3 node、簡單部署、SOC 學 Lucene query / dashboard / alert</td>
      </tr>
      <tr>
          <td>1-2</td>
          <td>Scale-out</td>
          <td>5-7 node、shard 計畫、hot/warm/cold tier、index lifecycle management</td>
      </tr>
      <tr>
          <td>2-3</td>
          <td>Degrade</td>
          <td>10+ node、shard 過多、query latency 升、upgrade window 開始痛</td>
      </tr>
      <tr>
          <td>3-4</td>
          <td>Save</td>
          <td>加 dedicated master / cross-cluster replication、ops cost 飛漲</td>
      </tr>
      <tr>
          <td>4-5</td>
          <td>Migrate decision</td>
          <td>評估走 Elastic Cloud（managed）或下一個 SIEM vendor</td>
      </tr>
  </tbody>
</table>
<p>多數中型 organization 在 lifecycle 第 4-5 年遇到 <em>operational ceiling</em> — SRE team 0.5-1.5 FTE 跑 ELK ops、新 feature 開發停滯、cost 跟 alternative observability vendor 比較。Elastic Cloud 把 operational stack 全託管、SOC 留在 <em>Lucene query + dashboard + alert</em> 層、不再管 cluster sizing。</p>
<h2 id="為什麼遷fte--availability--version-cadence-三條-driver">為什麼遷：FTE / availability / version cadence 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FTE</td>
          <td>Self-managed ELK 0.5-1.5 FTE 跑 ops、Elastic Cloud 降到 0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Availability</td>
          <td>Cross-AZ failover 自管太複雜、Cloud 內建</td>
      </tr>
      <tr>
          <td>Version cadence</td>
          <td>Elasticsearch 8.x quarterly release、self-managed upgrade window 是痛點、Cloud 自動</td>
      </tr>
  </tbody>
</table>
<h2 id="6-維-audit">6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low（Elasticsearch API 完全相容）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td><strong>High</strong>（cluster mgmt 全託管）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low（同 Elasticsearch + Kibana + Beats / Logstash）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low-Medium（連線 endpoint + auth 改）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High → Type C standard。</p>
<h2 id="operational-redesign-對位">Operational redesign 對位</h2>
<table>
  <thead>
      <tr>
          <th>Concept</th>
          <th>Self-managed ELK</th>
          <th>Elastic Cloud</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>手動 install + config</td>
          <td>UI / API 一鍵建 deployment</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>自管 master / dedicated voting / cross-AZ</td>
          <td>內建 multi-AZ</td>
      </tr>
      <tr>
          <td>Upgrade</td>
          <td>手動 rolling restart 6-12 小時</td>
          <td>自動 patch + minor version</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>自管 snapshot to S3</td>
          <td>內建 snapshot lifecycle</td>
      </tr>
      <tr>
          <td>Shard management</td>
          <td>手動 ILM policy</td>
          <td>UI-driven ILM</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>自管 X-Pack / SSL cert</td>
          <td>內建 + 自動 cert rotation</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>自管 Metricbeat → 自己集群</td>
          <td>內建 deployment monitoring</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-4-phase">Migration 4-phase</h2>
<h3 id="phase-0pre-migration-audit">Phase 0：Pre-migration audit</h3>
<ul>
<li>列 application 連線 endpoint (Logstash / Beats / SDK direct)</li>
<li>列 ILM policy + retention setting</li>
<li>估 deployment size（hot tier RAM / cold tier storage）</li>
</ul>
<h3 id="phase-1elastic-cloud-deployment-建置">Phase 1：Elastic Cloud deployment 建置</h3>
<ul>
<li>選 region + provider（AWS / GCP / Azure）</li>
<li>Hot tier RAM × N + cold tier S3-backed × N</li>
<li>Snapshot lifecycle 配置</li>
</ul>
<h3 id="phase-2data-migration">Phase 2：Data migration</h3>
<ul>
<li><strong>Cross-cluster replication (CCR)</strong> 從 self-managed → Cloud（推薦、incremental）</li>
<li>或 <strong>snapshot + restore</strong>（簡單但需要 maintenance window）</li>
</ul>
<h3 id="phase-3cutover--cleanup">Phase 3：Cutover + cleanup</h3>
<ul>
<li>Application 端切 endpoint</li>
<li>Self-managed 端 read-only 1-2 月</li>
<li>Decommission</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1application-endpoint-hardcodecutover-失敗">Case 1：Application endpoint hardcode、cutover 失敗</h3>
<p><strong>徵兆</strong>：cutover 後 N 個 application 仍連舊 endpoint、log / metric 斷流。</p>
<p><strong>根因</strong>：endpoint 寫死在 config file、deploy 時沒一起改。</p>
<p><strong>修法</strong>：endpoint 用 ENV variable + service discovery、cutover 是 single deploy。</p>
<h3 id="case-2ccr-replication-lagcutover-時資料-gap">Case 2：CCR replication lag、cutover 時資料 gap</h3>
<p><strong>徵兆</strong>：CCR 跑 1 週、cutover 前 lag 200ms 看似 OK；application 切到 Cloud 後 search 顯示 <em>缺最近 5 分鐘 data</em>。</p>
<p><strong>根因</strong>：CCR replication 不保證即時 catch up、cutover 期間仍可能 lag；且 follower index 對 <em>write</em> 不接受。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Cutover 流程加 <em>drain window</em> — 停 application write 5-10 分鐘、等 CCR catch up</li>
<li>確認 follower index 已 <em>promote</em> 成 write-capable</li>
<li>監控 CCR lag、&lt; 100ms 才 cutover</li>
</ol>
<h3 id="case-3auth-改變soc-alert-失效">Case 3：Auth 改變、SOC alert 失效</h3>
<p><strong>徵兆</strong>：cutover 後 SOC dashboard 顯示「authentication failed」、SIEM rule 全失效。</p>
<p><strong>根因</strong>：self-managed 用 X-Pack basic auth、Cloud 用 API key + SSO；SOC tooling 沒改 auth。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 列所有 tool 連線 ELK 的 auth</li>
<li>改 API key、用 IAM-friendly token rotation</li>
<li>Cloud 端 enable SSO + 設 service account</li>
</ol>
<h3 id="case-4cost-暴漲cold-tier-設定錯">Case 4：Cost 暴漲、cold tier 設定錯</h3>
<p><strong>徵兆</strong>：第一個月 Cloud 帳單比預估高 50%；cold tier 用 <em>fast storage</em>（hot-tier-level）而非 S3-backed。</p>
<p><strong>根因</strong>：Cloud deployment template 預設 hot 是 fast、cold 也是 fast（slow 需要明示）；team 沒 review template。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover review deployment template、確認 cold tier = searchable snapshot to S3</li>
<li>Cost monitor 第一週密集 check</li>
<li>Hot tier RAM 估算 conservative</li>
</ol>
<h3 id="case-5snapshot-跨-region-失效">Case 5：Snapshot 跨 region 失效</h3>
<p><strong>徵兆</strong>：DR drill 切 region 失敗；Cloud 內建 snapshot 是 same-region、不跨 region。</p>
<p><strong>根因</strong>：multi-region DR 需要 <em>cross-region snapshot</em> 或 <em>multi-deployment</em>、不是預設。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>評估 DR 需求、是否需要 cross-region</li>
<li>配 <em>additional deployment in DR region</em> + CCR</li>
<li>Cost 增 50-100%、是 DR 投資不是 cost optimization</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed ELK</th>
          <th>Elastic Cloud</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute cost (5 node)</td>
          <td>$1,000-2,000 / mo</td>
          <td>$1,500-3,000 / mo</td>
      </tr>
      <tr>
          <td>Storage cost</td>
          <td>EBS</td>
          <td>included + 加 S3 cold tier</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 = $5K-15K</td>
          <td>0.1-0.3 = $1K-3K</td>
      </tr>
      <tr>
          <td>Total (5 node, mid-tier)</td>
          <td>$6K-17K / mo</td>
          <td>$2.5K-6K / mo</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-2 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-splunk--elastic-security-migration-對位">跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security migration</a> 對位</h3>
<p>兩篇都到 Elastic 生態、但 Splunk → Elastic Security 是 Schema 高差 Type A、本篇是 Operational 高差 Type C；如果同時跑兩個 migration、Splunk → Elastic Security 先、ELK Cloud 後（避免雙重變動）。</p>
<h3 id="跟-application-observability-stack-整合">跟 Application observability stack 整合</h3>
<p>Elastic Cloud + APM + OpenTelemetry：cutover 後可以 <em>順便升 OTel 化 application</em>、避免下次 vendor 切換重複工作。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a> 跟 AWS MSK。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High（self-managed → AWS managed）→ Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="15kmonth-operational-cost-拆解">$15K/month operational cost 拆解&lt;/h2>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack&lt;/a>（H cost variant）同 framing — 用 cost 拆解開頭、不是「為什麼遷」driver list：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Self-managed Kafka cost 項&lt;/th>
 &lt;th>中型 (3 broker + 3 ZK + monitoring) / month&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>EC2 (3× r6g.xlarge broker)&lt;/td>
 &lt;td>$660&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>EBS (3× 1TB io2)&lt;/td>
 &lt;td>$1,500&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>EC2 (3× t3.medium ZK / KRaft)&lt;/td>
 &lt;td>$90&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Monitoring (Prometheus + Grafana on EC2)&lt;/td>
 &lt;td>$200&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup S3 (1TB)&lt;/td>
 &lt;td>$25&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-AZ traffic&lt;/td>
 &lt;td>$300&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Operational FTE (0.5)&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>$5,000-8,000&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Patching window cost&lt;/td>
 &lt;td>$200 (downtime opportunity)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Total infrastructure&lt;/td>
 &lt;td>$7,975-10,975&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Total with FTE&lt;/td>
 &lt;td>&lt;strong>$13,000-18,975&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>最大成本塊是 operational FTE、不是 infrastructure&lt;/strong>。MSK 把 50-80% operational 工作轉嫁 AWS、留 application + cost monitoring 給 SRE。&lt;/p>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> 跟 AWS MSK。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High（self-managed → AWS managed）→ Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="15kmonth-operational-cost-拆解">$15K/month operational cost 拆解</h2>
<p>跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a>（H cost variant）同 framing — 用 cost 拆解開頭、不是「為什麼遷」driver list：</p>
<table>
  <thead>
      <tr>
          <th>Self-managed Kafka cost 項</th>
          <th>中型 (3 broker + 3 ZK + monitoring) / month</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>EC2 (3× r6g.xlarge broker)</td>
          <td>$660</td>
      </tr>
      <tr>
          <td>EBS (3× 1TB io2)</td>
          <td>$1,500</td>
      </tr>
      <tr>
          <td>EC2 (3× t3.medium ZK / KRaft)</td>
          <td>$90</td>
      </tr>
      <tr>
          <td>Monitoring (Prometheus + Grafana on EC2)</td>
          <td>$200</td>
      </tr>
      <tr>
          <td>Backup S3 (1TB)</td>
          <td>$25</td>
      </tr>
      <tr>
          <td>Cross-AZ traffic</td>
          <td>$300</td>
      </tr>
      <tr>
          <td><strong>Operational FTE (0.5)</strong></td>
          <td><strong>$5,000-8,000</strong></td>
      </tr>
      <tr>
          <td>Patching window cost</td>
          <td>$200 (downtime opportunity)</td>
      </tr>
      <tr>
          <td>Total infrastructure</td>
          <td>$7,975-10,975</td>
      </tr>
      <tr>
          <td>Total with FTE</td>
          <td><strong>$13,000-18,975</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>最大成本塊是 operational FTE、不是 infrastructure</strong>。MSK 把 50-80% operational 工作轉嫁 AWS、留 application + cost monitoring 給 SRE。</p>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Kafka protocol、client SDK 不改</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Self-managed → AWS managed、HA / patch / backup 全託管</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 Kafka log-based</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 Kafka cluster</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Auth config 改（IAM / SASL）、其他不變</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 broker + partition 配置</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High（其他 Low-Medium）→ <strong>Type C operational redesign hybrid</strong>。</p>
<h2 id="為什麼遷fte--availability--consistency-三條-driver">為什麼遷：FTE / availability / consistency 三條 driver</h2>
<ul>
<li><strong>Operational FTE</strong>：Kafka self-managed + ZooKeeper / KRaft + Prometheus 端到端 ops 是 0.5-1 FTE、MSK 把 patch / HA / backup 全託管</li>
<li><strong>Availability</strong>：MSK 自動 multi-AZ broker + auto-recovery、self-managed 自管 broker 故障 RTO 30 分鐘-2 小時</li>
<li><strong>Consistency with cloud stack</strong>：已 deep on AWS（RDS / S3 / Lambda）、MSK 進 same VPC + IAM auth、降低 cross-vendor 設置成本</li>
</ul>
<p>反向 driver（MSK → self-managed）：</p>
<ul>
<li>Throughput / GB 規模大時 MSK 跨 broker cost 反轉（cost &gt; self-managed）</li>
<li>需要 Kafka 客製化（custom plugin / kraft early adopter / 非 AWS region）</li>
<li>Multi-cloud / hybrid 架構不想 vendor lock</li>
</ul>
<h2 id="operational-redesign-對位">Operational redesign 對位</h2>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a> 同 Type C pattern：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>Self-managed Kafka</th>
          <th>MSK</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>手動配置 broker + ZK + brokers.properties</td>
          <td>UI / Terraform 一鍵建</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>自管 replica + ISR + broker placement</td>
          <td>自動 multi-AZ + auto-recovery</td>
      </tr>
      <tr>
          <td>Patching</td>
          <td>Rolling restart 手動 / 工具</td>
          <td>MSK 自動 monthly maintenance window</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>自管 MirrorMaker / cluster snapshot</td>
          <td>MSK 內建 backup（S3、自動）</td>
      </tr>
      <tr>
          <td>Authentication</td>
          <td>SASL/SCRAM / mTLS 自管</td>
          <td>IAM auth（推薦）/ SASL/SCRAM via Secrets Manager</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Prometheus + JMX exporter 自建</td>
          <td>CloudWatch + open monitoring + Prometheus</td>
      </tr>
      <tr>
          <td>Sizing</td>
          <td>手動 broker instance class</td>
          <td>MSK broker size（kafka.m5.large+）</td>
      </tr>
      <tr>
          <td>Configuration</td>
          <td>server.properties 全控</td>
          <td>Configuration set（限制可調 parameter）</td>
      </tr>
      <tr>
          <td>Cluster topology</td>
          <td>自管 placement / rack awareness</td>
          <td>MSK 自動 multi-AZ + rack-aware</td>
      </tr>
      <tr>
          <td>Tiered storage</td>
          <td>Kafka 3.6+ 自管</td>
          <td>MSK Tiered Storage（auto-tier 到 S3）</td>
      </tr>
  </tbody>
</table>
<p>每行 operational concept 都需要 migration plan、application code 不變但 <em>運維知識體系全換</em>。</p>
<h2 id="4-phase-migrationtype-c-標準流程">4-phase migration（Type C 標準流程）</h2>
<h3 id="phase-0pre-migration-audit">Phase 0：Pre-migration audit</h3>
<ul>
<li><strong>Workload sizing → MSK broker class</strong>：當前 throughput / partition count / topic count</li>
<li><strong>Application connection pattern audit</strong>：客戶端 producer / consumer 用 SASL / mTLS / plaintext？哪個 application</li>
<li><strong>Topic config audit</strong>：retention / replication factor / cleanup policy</li>
<li><strong>Backup pattern audit</strong>：有 MirrorMaker / cross-cluster mirror 嗎</li>
</ul>
<h3 id="phase-1msk-cluster-建置2-3-週">Phase 1：MSK cluster 建置（2-3 週）</h3>





<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_msk_cluster&#34; &#34;main&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  cluster_name</span>           <span class="o">=</span> <span class="s2">&#34;production&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  kafka_version</span>          <span class="o">=</span> <span class="s2">&#34;3.6.0&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  number_of_broker_nodes</span> <span class="o">=</span> <span class="m">3</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="k">broker_node_group_info</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    instance_type</span>   <span class="o">=</span> <span class="s2">&#34;kafka.m5.large&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    client_subnets</span>  <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">private_subnets</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">    security_groups</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_security_group</span><span class="p">.</span><span class="k">msk</span><span class="p">.</span><span class="k">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">storage_info</span> {
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="k">ebs_storage_info</span> {
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">        volume_size</span> <span class="o">=</span> <span class="m">1000</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">provisioned_throughput</span> {
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">          enabled</span>           <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">          volume_throughput</span> <span class="o">=</span> <span class="m">500</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></span><span class="line"><span class="ln">19</span><span class="cl">  }
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="k">client_authentication</span> {
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">sasl</span> {
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">      iam</span> <span class="o">=</span> <span class="kt">true</span><span class="c1">        # IAM auth (推薦)
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span><span class="n">      scram</span> <span class="o">=</span> <span class="kt">false</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    }
</span></span><span class="line"><span class="ln">26</span><span class="cl">  }
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="k">configuration_info</span> {
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="n">    arn</span>      <span class="o">=</span> <span class="k">aws_msk_configuration</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="n">    revision</span> <span class="o">=</span> <span class="k">aws_msk_configuration</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">latest_revision</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  }
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="k">encryption_info</span> {
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="k">encryption_in_transit</span> {
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="n">      client_broker</span> <span class="o">=</span> <span class="s2">&#34;TLS&#34;</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    }
</span></span><span class="line"><span class="ln">37</span><span class="cl">  }
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="k">logging_info</span> {
</span></span><span class="line"><span class="ln">40</span><span class="cl">    <span class="k">broker_logs</span> {
</span></span><span class="line"><span class="ln">41</span><span class="cl">      <span class="k">cloudwatch_logs</span> {
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="n">        enabled</span>   <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="n">        log_group</span> <span class="o">=</span> <span class="k">aws_cloudwatch_log_group</span><span class="p">.</span><span class="k">msk</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">      }
</span></span><span class="line"><span class="ln">45</span><span class="cl">    }
</span></span><span class="line"><span class="ln">46</span><span class="cl">  }
</span></span><span class="line"><span class="ln">47</span><span class="cl">}</span></span></code></pre></div><h3 id="phase-2data-migrationmirrormaker-20">Phase 2：Data migration（MirrorMaker 2.0）</h3>





<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">Self-managed Kafka ──(MM2)──→ MSK
</span></span><span class="line"><span class="ln">2</span><span class="cl">                       │
</span></span><span class="line"><span class="ln">3</span><span class="cl">                consumer offset sync
</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">                topic config sync</span></span></code></pre></div><p>MM2 跑 1-7 天、依 topic 量 + retention 期間；replica.lag 對齊後進 cutover。</p>
<h3 id="phase-3cutover">Phase 3：Cutover</h3>
<ul>
<li>Application 端切 bootstrap.servers 從 self-managed → MSK</li>
<li>Producer 漸進切（10% → 50% → 100%）</li>
<li>Consumer 切換時 offset 從 MM2 sync 過的位置開始</li>
<li>Self-managed cluster read-only standby 2 週</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1iam-auth-沒設application-連不上">Case 1：IAM auth 沒設、application 連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 報 <code>SaslAuthenticationException: Access denied</code>；MSK 端 cloudWatch log 顯示 IAM principal 不認。</p>
<p><strong>根因</strong>：MSK IAM auth 要求 client 跑 <em>MSK IAM auth library</em>（Java 用 <code>aws-msk-iam-auth</code>、Python 用 <code>aws-msk-iam-sasl-signer-python</code>）；application 端用 standard Kafka client、不知道怎麼 sign IAM signature。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Python kafka-python + IAM auth</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">aws_msk_iam_sasl_signer</span> <span class="kn">import</span> <span class="n">MSKAuthTokenProvider</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">kafka</span> <span class="kn">import</span> <span class="n">KafkaProducer</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="k">class</span> <span class="nc">AwsMskIamProvider</span><span class="p">(</span><span class="n">MSKAuthTokenProvider</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">def</span> <span class="nf">token</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">generate_auth_token</span><span class="p">(</span><span class="s1">&#39;us-east-1&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</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="n">producer</span> <span class="o">=</span> <span class="n">KafkaProducer</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">bootstrap_servers</span><span class="o">=</span><span class="s1">&#39;b-1.mycluster.kafka.us-east-1.amazonaws.com:9098&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">security_protocol</span><span class="o">=</span><span class="s1">&#39;SASL_SSL&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">sasl_mechanism</span><span class="o">=</span><span class="s1">&#39;OAUTHBEARER&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">sasl_oauth_token_provider</span><span class="o">=</span><span class="n">AwsMskIamProvider</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>EKS pod 必須有 IAM role（IRSA）對 MSK cluster <code>kafka-cluster:Connect</code> action。</p>
<h3 id="case-2version-pinning360-跟-self-managed-行為差">Case 2：Version pinning、3.6.0 跟 self-managed 行為差</h3>
<p><strong>徵兆</strong>：cutover 到 MSK 3.6.0 後、某些 consumer 跑舊 client 失敗；新 broker 改 default <code>inter.broker.protocol.version</code> 但 client 不認。</p>
<p><strong>根因</strong>：MSK 升 Kafka version 後 broker config 變動、舊 client（&lt; 2.8）跟新 broker 協議不對；self-managed 端可能用更舊 broker version 跑、看不出問題。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration</strong>：所有 client 升 Kafka client library 2.8+</li>
<li><strong>MSK kafka_version 對齊 self-managed</strong>：先建 MSK 3.0 / 3.5、跟 self-managed 一致、cutover 後再升</li>
<li><strong>Phase rollout</strong>：用 <em>Tiered Storage</em> + retention 策略保留舊資料、新 producer / consumer 用新 version</li>
</ol>
<h3 id="case-3metric-pipeline-失效soc-dashboard-無數據">Case 3：Metric pipeline 失效、SOC dashboard 無數據</h3>
<p><strong>徵兆</strong>：cutover 後 Grafana dashboard 顯示 MSK metric 0；舊 JMX exporter 抓不到 MSK；CloudWatch 有 metric 但 SOC 端不接 CloudWatch。</p>
<p><strong>根因</strong>：MSK 不暴露 JMX、metric 走 CloudWatch / open monitoring (Prometheus + Grafana)、跟自建 JMX-based pipeline 不對等。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Open monitoring enabled</strong>：MSK config 設 <code>open_monitoring.prometheus.jmx_exporter.enabled = true</code>、跑 Prometheus 對 MSK broker 拉 metric</li>
<li><strong>CloudWatch → Prometheus</strong>：用 <code>cloudwatch-exporter</code> 拉 CloudWatch metric 進 Prometheus</li>
<li><strong>Dashboard refresh</strong>：Grafana dashboard 對 MSK-specific metric name 重寫（<code>kafka_server_*</code> → <code>aws_kafka_*</code> 或統一 alias）</li>
</ol>
<h3 id="case-4cross-cluster-mirrormm2--msk配置複雜">Case 4：Cross-cluster mirror（MM2 → MSK）配置複雜</h3>
<p><strong>徵兆</strong>：MM2 跑了 1 週、self-managed 跟 MSK consumer offset 沒同步；application 切過去後 <em>重新讀整批舊資料</em>、duplicate processing。</p>
<p><strong>根因</strong>：MM2 consumer offset sync 需要 <em>跨 cluster</em> mapping、source 端 offset 跟 target 端 offset 不直通；MM2 預設 offset sync 沒打開。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-properties" data-lang="properties"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># MM2 config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">source.consumer.bootstrap.servers</span><span class="o">=</span><span class="s">self-managed-kafka:9092</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">target.consumer.bootstrap.servers</span><span class="o">=</span><span class="s">msk-cluster:9098</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">target.security.protocol</span><span class="o">=</span><span class="s">SASL_SSL</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">sync.group.offsets.enabled</span><span class="o">=</span><span class="s">true       # 必須打開</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">emit.checkpoints.enabled</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">checkpoints.topic.replication.factor</span><span class="o">=</span><span class="s">3</span></span></span></code></pre></div><p><strong>Architecture</strong>：consumer 切換時讀 <em>MM2 checkpoint</em> topic、不直接讀 internal offset；application 端用 <em>idempotent</em> + <em>dedup key</em>、avoid duplicate processing。</p>
<h3 id="case-5msk-billing-暴漲tiered-storage--cross-az-沒控">Case 5：MSK billing 暴漲、Tiered Storage / cross-AZ 沒控</h3>
<p><strong>徵兆</strong>：MSK 第一個月帳單比預估高 50%；breakdown 後發現 cross-AZ traffic（producer/consumer 跨 AZ）+ Tiered Storage 退到 S3 的 hot tier。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>MSK auto multi-AZ deployment 不可避免 cross-AZ traffic、producer 寫 partition leader 可能跨 AZ</li>
<li>Tiered Storage 對 hot data（retention &lt; 24 小時）會多 storage cost；cold data 才 cost-effective</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application AZ-aware routing</strong>：producer 走 same-AZ broker（用 rack-aware producer config）、降 cross-AZ</li>
<li><strong>Retention 對齊 hot tier</strong>：&lt; 24 小時 retention 用 broker local storage、24 小時+ 才走 Tiered Storage</li>
<li><strong>Reserved instance</strong>：MSK 不直接 reserved、但 EBS / data transfer 可預付、降 10-20%</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Kafka</th>
          <th>MSK</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster cost (3 broker)</td>
          <td>$660 EC2 + $1500 EBS = $2,160</td>
          <td>$2,500-3,500（含 storage + multi-AZ）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1 FTE = $5K-10K</td>
          <td>0.1-0.3 FTE = $1K-3K</td>
      </tr>
      <tr>
          <td>Patch / maintenance</td>
          <td>Manual + downtime opportunity</td>
          <td>Auto + maintenance window scheduled</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>Self-managed MirrorMaker</td>
          <td>Built-in（S3 archive、auto）</td>
      </tr>
      <tr>
          <td>Metric / monitoring</td>
          <td>Prometheus + Grafana self-deploy</td>
          <td>CloudWatch + open monitoring</td>
      </tr>
      <tr>
          <td>Cross-AZ traffic</td>
          <td>Limited by VPC layout</td>
          <td>Auto multi-AZ、cross-AZ traffic cost 注意</td>
      </tr>
      <tr>
          <td>Tiered storage</td>
          <td>Kafka 3.6+ self-managed</td>
          <td>MSK built-in tiered storage</td>
      </tr>
      <tr>
          <td>Total (3 broker, 中型)</td>
          <td>$7K-11K / mo (含 FTE)</td>
          <td>$3.5K-6.5K / mo (含 FTE)</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：&lt; 50 broker organization MSK ROI 通常 6-12 月持平、之後省 FTE；50+ broker 大 organization 自管 cost 可能反而低。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-kafka--nats-migration-對位">跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS migration</a> 對位</h3>
<p>兩條 Kafka 出路：</p>
<ul>
<li>MSK：operational simplification、protocol drop-in、cost 中等漲；適合 <em>繼續用 Kafka paradigm</em> 的 organization</li>
<li>NATS：paradigm shift、application 必須改、適合 <em>單純 messaging 不要 event sourcing</em> 的 use case</li>
</ul>
<p>多數 organization 不需要 paradigm shift、MSK 更合理；真正需要 lightweight messaging 才走 NATS。</p>
<h3 id="跟-confluent-cloud-對位">跟 <a href="https://www.confluent.io/confluent-cloud/">Confluent Cloud</a> 對位</h3>
<p>Confluent Cloud 是另一個 managed Kafka、跨 cloud（AWS / GCP / Azure）；MSK 是 AWS-only、但跟 IAM / VPC 整合更深。Multi-cloud organization 走 Confluent、AWS-deep organization 走 MSK。</p>
<h3 id="跟-iam--secrets-manager-整合">跟 IAM / Secrets Manager 整合</h3>
<p>MSK + IAM auth + Secrets Manager（連 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager migration</a>）是 AWS-deep stack 的標準組合；short-lived credential + IRSA 是 production best practice。</p>
<h3 id="反向-migrationmsk--self-managed">反向 migration（MSK → self-managed）</h3>
<p>少見、通常是 <em>cost 反轉</em>（大 scale）或 <em>multi-cloud strategy</em>；流程鏡像對稱、注意 MSK Tiered Storage data 不直接 export、需要 <em>先 disable tiered storage</em> + recall data。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>MSK Connect</strong>：managed Kafka Connect、降 connector 運維、但 plugin ecosystem 比 self-managed Connect 少</li>
<li><strong>MSK Serverless</strong>：burst workload 適合、steady workload 反而貴</li>
<li><strong>Cost monitoring playbook</strong>：MSK billing 拆解每月跑一次、catch unexpected egress / tiered storage cost</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a></li>
<li>平行 H cost variant：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>平行 paradigm shift：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a>。本文同時是 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation&lt;/a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 &lt;em>identity 軸驗證&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="secret不是secret兩家對secret的定義不同">「secret」不是「secret」：兩家對「secret」的定義不同&lt;/h2>
&lt;p>把 Vault → AWS Secrets Manager 當成「secret store 替換」是最常見的誤判 — 兩家的「secret」概念跨完全不同的 identity model：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>HashiCorp Vault&lt;/th>
 &lt;th>AWS Secrets Manager&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Secret 本身&lt;/td>
 &lt;td>一個 secret path（&lt;code>secret/data/myapp/db&lt;/code>）&lt;/td>
 &lt;td>一個 ARN（&lt;code>arn:aws:secretsmanager:us-east-1:...&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>存取者身份&lt;/td>
 &lt;td>Vault token（self-managed token TTL）&lt;/td>
 &lt;td>AWS principal（IAM user / role / federation）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>授權模型&lt;/td>
 &lt;td>Vault policy（capabilities：read/create/&amp;hellip;）&lt;/td>
 &lt;td>IAM policy + Resource policy（雙層）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Authentication&lt;/td>
 &lt;td>AppRole / Kubernetes / LDAP / OIDC / 自管 auth method&lt;/td>
 &lt;td>AWS Sigv4 + STS token / Identity Federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dynamic credential&lt;/td>
 &lt;td>Vault database secrets engine（lease + renew）&lt;/td>
 &lt;td>Lambda rotation（無 lease 概念）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audit log&lt;/td>
 &lt;td>Vault audit log（自管 endpoint）&lt;/td>
 &lt;td>CloudTrail event（AWS 統一）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-tenant 隔離&lt;/td>
 &lt;td>Namespace + path-level policy&lt;/td>
 &lt;td>Account boundary + resource policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tooling 整合&lt;/td>
 &lt;td>Application 端 Vault SDK / agent injector&lt;/td>
 &lt;td>AWS SDK + Lambda&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「存 secret 的地方」、在「身份從哪來、怎麼 enforce、怎麼 audit」。&lt;/strong> Migration 的真實工作量在 &lt;em>identity model 重設計&lt;/em>、不是 secret 搬遷。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> 跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>。本文同時是 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation</a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 <em>identity 軸驗證</em>。</p></blockquote>
<h2 id="secret不是secret兩家對secret的定義不同">「secret」不是「secret」：兩家對「secret」的定義不同</h2>
<p>把 Vault → AWS Secrets Manager 當成「secret store 替換」是最常見的誤判 — 兩家的「secret」概念跨完全不同的 identity model：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>HashiCorp Vault</th>
          <th>AWS Secrets Manager</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Secret 本身</td>
          <td>一個 secret path（<code>secret/data/myapp/db</code>）</td>
          <td>一個 ARN（<code>arn:aws:secretsmanager:us-east-1:...</code>）</td>
      </tr>
      <tr>
          <td>存取者身份</td>
          <td>Vault token（self-managed token TTL）</td>
          <td>AWS principal（IAM user / role / federation）</td>
      </tr>
      <tr>
          <td>授權模型</td>
          <td>Vault policy（capabilities：read/create/&hellip;）</td>
          <td>IAM policy + Resource policy（雙層）</td>
      </tr>
      <tr>
          <td>Authentication</td>
          <td>AppRole / Kubernetes / LDAP / OIDC / 自管 auth method</td>
          <td>AWS Sigv4 + STS token / Identity Federation</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>Vault database secrets engine（lease + renew）</td>
          <td>Lambda rotation（無 lease 概念）</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>Vault audit log（自管 endpoint）</td>
          <td>CloudTrail event（AWS 統一）</td>
      </tr>
      <tr>
          <td>Multi-tenant 隔離</td>
          <td>Namespace + path-level policy</td>
          <td>Account boundary + resource policy</td>
      </tr>
      <tr>
          <td>Tooling 整合</td>
          <td>Application 端 Vault SDK / agent injector</td>
          <td>AWS SDK + Lambda</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「存 secret 的地方」、在「身份從哪來、怎麼 enforce、怎麼 audit」。</strong> Migration 的真實工作量在 <em>identity model 重設計</em>、不是 secret 搬遷。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>API 完全不同（Vault HTTP API vs AWS SDK）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Self-managed Vault cluster → AWS managed</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>兩家都是 secret store paradigm</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Vault binary + storage backend → AWS SaaS</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>必改（SDK 換、auth method 換、retry pattern 換）</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single instance, no sharding</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Identity model</strong></td>
          <td><strong>完全不同（Vault token vs IAM principal）</strong></td>
          <td><strong>High</strong></td>
      </tr>
  </tbody>
</table>
<p>6 維 audit 抓不到「Identity model = High」這軸 — 用既有 6 維歸類、會走 Type C operational redesign + Application change 高維獨立段；但實際工作量分佈：</p>
<ul>
<li>Operational redesign（vault cluster 拆 / Lambda 配 / 監控換）：~25%</li>
<li>Application change（SDK / retry / token 換 IAM credential）：~30%</li>
<li><strong>Identity model 重設計（每個 secret 對應的 principal / policy / 跨 service auth chain）：~45%</strong></li>
</ul>
<p>最大工作量塊在 <em>identity model 重設計</em>、不在既有 6 維任一個。Identity 是 <em>候選的第 7 維</em>。</p>
<h2 id="identity-axis-是否獨立4-個論據">Identity axis 是否獨立：4 個論據</h2>
<p><strong>Yes、identity 是獨立軸</strong>：</p>
<ol>
<li><strong>Identity 不變 → operational 仍可變</strong>：Vault on-prem → Vault on-EKS、operational 變 high 但 identity model 不變（仍 Vault token）；可分開 audit</li>
<li><strong>Operational 不變 → identity 仍可變</strong>：Vault namespace 重組（管理 50 個 namespace → 5 個 namespace + namespace-level policy）、operational 不變但 identity boundary 重劃；可分開 audit</li>
<li><strong>Application change 不變 → identity 仍可變</strong>：純 infrastructure-level rotation（手動 → 自動）、application code 不變但 identity issuance flow 變；可分開 audit</li>
<li><strong>Paradigm 不變 → identity 仍可變</strong>：同樣是 secret store paradigm、Vault token vs IAM principal 是 identity model 差、不是 paradigm 差</li>
</ol>
<p><strong>No、identity 可塞 application change</strong>：</p>
<ul>
<li>反論：application code 改 SDK + IAM signer 都算 application change</li>
<li>拒絕：application change 是 <em>consequence</em>、不是 <em>root cause</em>；identity model 變動才是驅動 application change 的原因</li>
</ul>
<p>實證上、本文 migration 工作量 45% 在 identity 對位、確認 identity 是 <em>獨立的工作量主軸</em>、不該被壓進 application change 軸。</p>
<h2 id="結構type-c--identity-model-對位獨立段">結構：Type C + identity model 對位獨立段</h2>
<p>跟既有 Type C <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 對照、本文多出 <em>identity model 對位</em> 獨立段：</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. 「secret」不是「secret」（identity axis paradox 開頭）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Identity axis 是否獨立的論據
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 結構 differentiator（Type C + identity 獨立段）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Identity model 對位（Vault → AWS principal mapping）
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Operational migration（4 phase）
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Application change（SDK + retry pattern）
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. Production 故障演練
</span></span><span class="line"><span class="ln">8</span><span class="cl">8. Capacity / cost
</span></span><span class="line"><span class="ln">9</span><span class="cl">9. 整合 / 下一步</span></span></code></pre></div><p>9 章節、260-280 行。比標準 Type C 多 1 段（identity model 對位）+ 1 段（axis 獨立論據）。</p>
<h2 id="identity-model-對位">Identity model 對位</h2>





<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">Vault concept                    →  AWS Secrets Manager 對應
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">─────────────────────────────────   ────────────────────────────
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Vault token (auth 結果)           →  AWS STS temporary credential
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">AppRole (auth method)             →  IAM role + AssumeRoleWithWebIdentity
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Kubernetes auth method            →  IAM Role for Service Account (IRSA)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">LDAP auth method                  →  IAM Identity Center (formerly SSO)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Vault policy (capabilities)       →  IAM policy + Resource policy
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Path-level ACL (secret/db/*)      →  Resource ARN pattern (arn:aws:secretsmanager:...:secret:db/*)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Namespace                         →  AWS account + resource-based isolation
</span></span><span class="line"><span class="ln">10</span><span class="cl">Audit device                      →  CloudTrail event
</span></span><span class="line"><span class="ln">11</span><span class="cl">Database secrets engine           →  Lambda rotation function</span></span></code></pre></div><p>每行對位都有 <em>語意差</em>、不是 1:1 mapping：</p>
<ul>
<li><strong>Vault token TTL vs AWS STS credential expiration</strong>：Vault token TTL 可由 application 主動 renew；STS credential 不能 renew、必須 re-assume</li>
<li><strong>Vault policy capabilities vs IAM action</strong>：Vault <code>read</code> capability 對應 AWS <code>secretsmanager:GetSecretValue</code>、但 AWS 還要 resource policy 允許；雙層授權</li>
<li><strong>Vault Kubernetes auth vs IRSA</strong>：兩者都是 K8s service account → secret access、但 IRSA 需要 EKS + OIDC provider 設置、Vault K8s auth 不需要</li>
</ul>
<p>Migration scope 包含每行對位的 <em>application-level 適配</em>、不是 secret 搬。</p>
<h2 id="operational-migration-4-phase">Operational migration (4 phase)</h2>
<h3 id="phase-0audit--design">Phase 0：Audit + design</h3>
<ul>
<li>列所有 Vault secret + path + 使用 application</li>
<li>每個 secret 對應 AWS principal（IAM role / IRSA / federation）</li>
<li>設計 ARN 命名規則（按 namespace / application / environment）</li>
<li>規劃 AWS account boundary（dev / staging / prod 分 account）</li>
</ul>
<h3 id="phase-1aws-secrets-manager--iam-設置">Phase 1：AWS Secrets Manager + IAM 設置</h3>
<ul>
<li>Terraform / CloudFormation 建 secret + IAM role + resource policy</li>
<li>設 IRSA / WebIdentity provider</li>
<li>預先建 staging secret、跑 application test</li>
</ul>
<h3 id="phase-2application-dual-read">Phase 2：Application dual-read</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Application 同時讀 Vault + AWS Secrets Manager</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">get_db_password</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">aws_value</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">&#39;secretsmanager&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">get_secret_value</span><span class="p">(</span><span class="n">SecretId</span><span class="o">=</span><span class="s1">&#39;myapp/db&#39;</span><span class="p">)[</span><span class="s1">&#39;SecretString&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">vault_value</span> <span class="o">=</span> <span class="n">vault_client</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="s1">&#39;secret/data/myapp/db&#39;</span><span class="p">)[</span><span class="s1">&#39;data&#39;</span><span class="p">][</span><span class="s1">&#39;data&#39;</span><span class="p">][</span><span class="s1">&#39;password&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">if</span> <span class="n">aws_value</span> <span class="o">!=</span> <span class="n">vault_value</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Secret diff between Vault and AWS!&#34;</span><span class="p">)</span>
</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">return</span> <span class="n">aws_value</span>  <span class="c1"># Use AWS as source of truth</span></span></span></code></pre></div><p>跑 1-2 週、確認兩端一致 + AWS API latency / error rate 接受。</p>
<h3 id="phase-3cutover--cleanup">Phase 3：Cutover + cleanup</h3>
<ul>
<li>Application 端切到 AWS Secrets Manager only</li>
<li>Vault read-only 1-2 週 standby</li>
<li>之後 decommission Vault cluster</li>
</ul>
<h2 id="application-change">Application change</h2>
<p>Application 端必改的 4 個 pattern：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Before: Vault SDK</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kn">import</span> <span class="nn">hvac</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">vault_client</span> <span class="o">=</span> <span class="n">hvac</span><span class="o">.</span><span class="n">Client</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="s1">&#39;https://vault.internal&#39;</span><span class="p">,</span> <span class="n">token</span><span class="o">=</span><span class="n">vault_token</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">secret</span> <span class="o">=</span> <span class="n">vault_client</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="s1">&#39;secret/data/myapp/db&#39;</span><span class="p">)[</span><span class="s1">&#39;data&#39;</span><span class="p">][</span><span class="s1">&#39;data&#39;</span><span class="p">][</span><span class="s1">&#39;password&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># After: AWS SDK + IAM</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kn">import</span> <span class="nn">boto3</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">sm</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">&#39;secretsmanager&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">secret</span> <span class="o">=</span> <span class="n">sm</span><span class="o">.</span><span class="n">get_secret_value</span><span class="p">(</span><span class="n">SecretId</span><span class="o">=</span><span class="s1">&#39;myapp/db&#39;</span><span class="p">)[</span><span class="s1">&#39;SecretString&#39;</span><span class="p">]</span></span></span></code></pre></div><p>關鍵差異點：</p>
<ul>
<li><strong>Authentication</strong>：Vault token 由 application 自管 / refresh；AWS SDK 自動處理 STS credential（透過 IAM role / instance profile / IRSA）</li>
<li><strong>Caching</strong>：Vault secret read 通常 cache 5-15 分鐘；AWS Secrets Manager 有 cache library（aws-secretsmanager-caching-python）需顯式啟用</li>
<li><strong>Retry pattern</strong>：Vault 用 exponential backoff；AWS SDK 自帶 retry but boto3 default 跟 application requirement 不一定 match</li>
<li><strong>Rotation hook</strong>：Vault 用 SDK 端 lease renewal；AWS 用 Lambda rotation function、application 端只需要 re-read</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1iam-principal-對位錯production-application-拿不到-secret">Case 1：IAM principal 對位錯、production application 拿不到 secret</h3>
<p><strong>徵兆</strong>：cutover 後 application 啟動失敗、log 顯示 <code>AccessDeniedException: User: arn:aws:sts::...:assumed-role/EKS-NodeRole/i-xxx is not authorized to perform: secretsmanager:GetSecretValue</code>。</p>
<p><strong>根因</strong>：EKS pod 用 <em>node role</em> 而非 <em>pod IRSA role</em>；Phase 0 audit 沒設 service account 對應的 OIDC trust。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先設 IRSA</strong>：建 IAM OIDC provider for EKS、設 service account annotation</li>
<li><strong>驗證 principal</strong>：<code>aws sts get-caller-identity</code> 從 pod 內跑、確認 returned role 是預期的</li>
<li><strong>Resource policy + IAM policy 雙層</strong>：確認 secret 的 resource policy allow 該 role、IAM policy 也 allow</li>
</ol>
<h3 id="case-2dynamic-credential-對等失敗application-連-db-失敗">Case 2：Dynamic credential 對等失敗、application 連 DB 失敗</h3>
<p><strong>徵兆</strong>：Vault 端用 database secrets engine 自動 rotate DB password、application 透過 Vault SDK 拿 lease；切到 AWS Secrets Manager + Lambda rotation 後、Lambda rotation 完成、但 application 端仍用 cached old password、連 DB 拒絕。</p>
<p><strong>根因</strong>：Vault SDK 自帶 lease renewal logic、application 知道 password 即將過期會主動 re-read；AWS SDK 沒 lease 概念、application 自己決定多久 re-read 一次。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>設 cache TTL 短於 rotation interval</strong>：rotation 24 小時、cache TTL 1 小時、最壞情況 1 小時 stale</li>
<li><strong>顯式 cache invalidation</strong>：rotation Lambda 跑完發 SNS、application subscribe 主動 refresh</li>
<li><strong>Connection-level retry</strong>：DB connection 認證失敗時 application 重 fetch secret 跟重連</li>
<li><strong>重新評估 rotation cadence</strong>：AWS Lambda rotation 不是 <em>Vault dynamic</em>、是 <em>scheduled rotation</em>；不能假設兩者同 semantic</li>
</ol>
<h3 id="case-3audit-log-結構差soc-dashboard-失效">Case 3：Audit log 結構差、SOC dashboard 失效</h3>
<p><strong>徵兆</strong>：cutover 後 SOC 端 dashboard 顯示 secret access metric 全 0；舊 Vault audit log 結構在 Splunk 端 parse 過、AWS CloudTrail 結構完全不同、search query 全失效。</p>
<p><strong>根因</strong>：Vault audit log 是 <em>Vault-specific</em> JSON 結構（含 lease_id / policy / token）；CloudTrail event 是 <em>AWS-specific</em>（含 eventName / requestParameters / userIdentity）；SOC parse rule 不能搬。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover 重寫 SOC rule</strong>：CloudTrail event 對應 Vault audit log 的 detection coverage 必須 1:1 mapping</li>
<li><strong>GuardDuty integration</strong>：AWS GuardDuty 自動 surface secret access anomaly、降低自寫 rule 工作量</li>
<li><strong>CloudTrail → S3 → Athena</strong>：long-term audit query 走 Athena、tooling 跟 Vault 完全不同、SOC re-training</li>
</ol>
<h3 id="case-4calling-cost-反轉aws-比-vault-自管貴">Case 4：Calling cost 反轉、AWS 比 Vault 自管貴</h3>
<p><strong>徵兆</strong>：Vault on-prem 跑了 $200 / month（EC2 + ops），切到 AWS Secrets Manager 後 $1500 / month；帳單拆解後 <code>GetSecretValue</code> API call 是大頭。</p>
<p><strong>根因</strong>：AWS Secrets Manager <code>$0.05 per 10K API call</code> — application 高頻 read（每 request 都讀 secret + 沒 cache）會爆 cost；Vault 端 application 自管 cache + token TTL 內無 API call。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>強制 application-side cache</strong>：用 aws-secretsmanager-caching library、cache TTL 5-15 分鐘、API call 從 100M/month 降到 10K/month</li>
<li><strong>Re-architect application</strong>：把 high-frequency secret read 改 connection-level（建 DB connection 時讀一次、connection lifecycle 內復用）</li>
<li><strong>Cost monitoring</strong>：對 secret access 設 CloudWatch alarm、過 threshold 立即 alert</li>
</ol>
<h3 id="case-5跨-region-replication-對位失敗dr-演練失效">Case 5：跨 region replication 對位失敗、DR 演練失效</h3>
<p><strong>徵兆</strong>：DR drill 切 region 後、application 連不到 secret；發現 us-west-2 的 Secrets Manager 沒有 us-east-1 的 secret。</p>
<p><strong>根因</strong>：AWS Secrets Manager 不是 <em>global resource</em>、是 <em>region-scoped</em>；Vault 自管 multi-DC replication；cutover 漏設 <em>cross-region replication</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>設 secret replication</strong>：AWS Secrets Manager 內建 replication 到其他 region（<code>ReplicaRegions</code>）</li>
<li><strong>DR drill 必跑</strong>：cutover 前 + cutover 後各 drill 一次、驗證 region failover 順</li>
<li><strong>架構</strong>：考慮用 <em>AWS Backup</em> 對 Secrets Manager 做 cross-region backup 補強</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Vault self-managed</th>
          <th>AWS Secrets Manager</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Setup cost</td>
          <td>Mid（自管 cluster + storage + HA）</td>
          <td>Low（一鍵建 secret）</td>
          <td>AWS 顯著低</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-1 FTE</td>
          <td>0.05-0.1 FTE</td>
          <td>AWS 省 SRE</td>
      </tr>
      <tr>
          <td>Per-secret cost</td>
          <td>~$0（含在 cluster）</td>
          <td>$0.40 / month</td>
          <td>AWS 按 secret 數計費</td>
      </tr>
      <tr>
          <td>API call cost</td>
          <td>~$0（含在 cluster）</td>
          <td>$0.05 / 10K call</td>
          <td>High-frequency app 顯著貴</td>
      </tr>
      <tr>
          <td>Cross-region</td>
          <td>自管 replication</td>
          <td>內建 <code>ReplicaRegions</code></td>
          <td>AWS 簡化</td>
      </tr>
      <tr>
          <td>Audit</td>
          <td>Vault audit device</td>
          <td>CloudTrail（內建）</td>
          <td>AWS 跟 SOC pipeline 統一</td>
      </tr>
      <tr>
          <td>Identity integration</td>
          <td>多 auth method</td>
          <td>IAM + IRSA + Identity Center</td>
          <td>AWS 跟 cloud-native 整合好</td>
      </tr>
      <tr>
          <td>Total cost (100 secret, 50K read/day)</td>
          <td>$200 / mo (含 ops)</td>
          <td>$40 + $7 + replication = ~$50 / mo + ops 省</td>
          <td>AWS 1/4 cost、若 read 不爆</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：少 secret + 中頻 read 走 AWS Secrets Manager；高頻 read + multi-cloud / on-prem 約束走 Vault。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-vault-dynamic-credential-對比">跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a> 對比</h3>
<p>Vault dynamic credential 是 Vault 特有 feature、AWS Secrets Manager 用 <em>Lambda rotation</em> 對應、但 semantic 不同：</p>
<ul>
<li>Vault: per-application lease、application-aware lifecycle</li>
<li>AWS: scheduled rotation、application 不知道何時被 rotate</li>
</ul>
<p>Migration scope 應該 <em>降級</em> dynamic credential 場景、用 Lambda rotation 替代、application logic 改 cache + retry pattern。</p>
<h3 id="跟-iam-identity-center-整合">跟 IAM Identity Center 整合</h3>
<p>人類存取 secret（emergency break-glass）走 IAM Identity Center + temporary role assumption；不要直接給 user IAM key。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Reverse migration（AWS → Vault）</strong>：通常是 multi-cloud / on-prem 約束驅動、cost 在大 scale 反轉</li>
<li><strong>Hybrid pattern</strong>：cloud-native secret 走 AWS、cross-cloud / on-prem secret 走 Vault；應用程式根據 secret 來源 routing</li>
<li><strong>identity axis 驗證</strong>：本文認為 identity 是獨立軸、未來累積 LDAP → OIDC / 自管 RBAC → IAM 等 migration 驗證</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>Target vendor：<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a></li>
<li>平行 deep article：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a>（標準 Type C） / <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a></li>
<li>平行 axis 候選驗證 (sibling)：<a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">DynamoDB Consistency Model</a>（consistency 候選） / <a href="/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/" data-link-title="PostgreSQL Multi-Region GDPR Rollout：政策驅動的 migration 屬本 methodology 嗎" data-link-desc="PostgreSQL 單 region → multi-region 同時滿足 GDPR EU residency 是 *政策驅動* 兼 *topology 變動* 兼 *operational redesign* 的多軸 migration；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 residency axis 候選 — residency 是 driver 還是獨立 audit 軸；涵蓋 logical replication 配 GDPR / 5 個 production 踩雷 / cross-region cost">PostgreSQL Multi-Region GDPR Rollout</a>（residency 候選）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation 第 1 點</a>（identity axis 候選驗證、本文是該驗證的 dogfood）</li>
</ul>
]]></content:encoded></item><item><title>3.C12 Pinterest：Shallow Mirror 優化 MirrorMaker</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/</guid><description>&lt;p>這個案例的核心責任是說明 cross-region replication 的 CPU/memory 成本是被低估的工程議題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Pinterest 三個 AWS region（us-east-1 / us-east-2 / eu-west-1）跑 MirrorMaker v1、原版設計把 record 解壓+重壓、memory 用量 2-10x 於網路 bytes、CPU spike 與 OOM 頻繁。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Shallow Mirror 在 RecordBatch 層淺迭代 + ByteBuffer pointer 共享、避開 deserialize/re-compress。揭露「跨區同步不是純 I/O 問題、是 CPU + memory + 網路三維壓力」。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：cross-region MirrorMaker / MirrorMaker 2 配置。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/shallow-mirror-f543b14bb25">Pinterest Shallow Mirror&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 cross-region replication 的 CPU/memory 成本是被低估的工程議題。</p>
<h2 id="觀察">觀察</h2>
<p>Pinterest 三個 AWS region（us-east-1 / us-east-2 / eu-west-1）跑 MirrorMaker v1、原版設計把 record 解壓+重壓、memory 用量 2-10x 於網路 bytes、CPU spike 與 OOM 頻繁。</p>
<h2 id="判讀">判讀</h2>
<p>Shallow Mirror 在 RecordBatch 層淺迭代 + ByteBuffer pointer 共享、避開 deserialize/re-compress。揭露「跨區同步不是純 I/O 問題、是 CPU + memory + 網路三維壓力」。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：cross-region MirrorMaker / MirrorMaker 2 配置。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/pinterest-engineering/shallow-mirror-f543b14bb25">Pinterest Shallow Mirror</a></li>
</ul>
]]></content:encoded></item><item><title>Teleport</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/</guid><description>&lt;p>Teleport 是 &lt;em>Identity-Aware Proxy + PAM&lt;/em>（Privileged Access Management）、把 SSH / Database / Kubernetes / Windows Desktop / Cloud API / 內部 web app 的 &lt;em>privileged session&lt;/em> 統一收到一個 zero-trust 入口、所有 session 改走 &lt;em>short-lived cert + per-session MFA + 全程錄影&lt;/em>、取代傳統「long-lived SSH key + bastion + 手動 audit」。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 是兩層職責 — Okta 認證 &lt;em>人是誰&lt;/em>、Teleport 控制 &lt;em>拿到身份後 privileged session 怎麼進、留什麼證據&lt;/em>；典型部署是 &lt;em>Okta SSO into Teleport、Teleport proxies SSH/DB/K8s session&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Teleport 的核心定位是 &lt;em>infrastructure access plane&lt;/em>、不是 IdP、不是 secret store、也不是 network mesh。它的責任是 &lt;em>把 admin / engineer 對 production 資源的 session 通通走可治理的入口&lt;/em>、每個 session 有 &lt;em>identity-bound short-lived cert&lt;/em>、有 &lt;em>audit log&lt;/em>、有 &lt;em>錄影&lt;/em>、有 &lt;em>MFA gate&lt;/em>。比較對象：&lt;/p>
&lt;ul>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / Azure AD 等 IdP 比、Teleport 不取代 SSO、而是 &lt;em>把 SSO identity 帶到 infrastructure layer&lt;/em> — Okta 給 user identity + group、Teleport 把這個 identity 翻譯成 SSH cert / DB cert / K8s cert&lt;/li>
&lt;li>跟傳統 bastion + SSH key 比、Teleport 把 &lt;em>long-lived SSH key&lt;/em> 換成 &lt;em>short-lived cert&lt;/em>（預設 TTL 數小時、過期自動失效）、把 &lt;em>看不到的 session&lt;/em> 換成 &lt;em>全程錄影 + searchable audit log&lt;/em>&lt;/li>
&lt;li>跟 HashiCorp Boundary 比、Teleport 走 &lt;em>protocol-aware proxy&lt;/em>（懂 SSH / PostgreSQL / Kubernetes API 協議、可以 decode keystroke 跟 query）、Boundary 走 &lt;em>generic TCP proxy&lt;/em>（協議無感、不能錄 keystroke 但部署更輕）&lt;/li>
&lt;li>跟 Tailscale SSH 比、Tailscale 是 &lt;em>network mesh 加 SSH&lt;/em>、適合小團隊 flat network；Teleport 是 &lt;em>PAM + 多協議 + 跨環境 audit&lt;/em>、適合需要 SOC handoff 的環境&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare Access&lt;/a> 比、Cloudflare Access 是 &lt;em>application-layer ZTNA&lt;/em>（內部 web app / API 用）、Teleport 是 &lt;em>infrastructure-layer ZTNA&lt;/em>（SSH / DB / K8s 用）、兩者互補&lt;/li>
&lt;/ul>
&lt;p>關鍵張力：&lt;em>PAM 的覆蓋完整度&lt;/em> ↔ &lt;em>operator 摩擦&lt;/em>。Teleport 開越多（per-session MFA、Access Request 要 approval、Device Trust 強制企業裝置）、helpdesk SE 那種「拿到密碼直接進 prod」的 blast radius 越小、但 on-call engineer 在凌晨三點修事故時的摩擦也越大。要根據 &lt;em>資源敏感度分層&lt;/em> 設定、不是一刀切。&lt;/p></description><content:encoded><![CDATA[<p>Teleport 是 <em>Identity-Aware Proxy + PAM</em>（Privileged Access Management）、把 SSH / Database / Kubernetes / Windows Desktop / Cloud API / 內部 web app 的 <em>privileged session</em> 統一收到一個 zero-trust 入口、所有 session 改走 <em>short-lived cert + per-session MFA + 全程錄影</em>、取代傳統「long-lived SSH key + bastion + 手動 audit」。它跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 是兩層職責 — Okta 認證 <em>人是誰</em>、Teleport 控制 <em>拿到身份後 privileged session 怎麼進、留什麼證據</em>；典型部署是 <em>Okta SSO into Teleport、Teleport proxies SSH/DB/K8s session</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>Teleport 的核心定位是 <em>infrastructure access plane</em>、不是 IdP、不是 secret store、也不是 network mesh。它的責任是 <em>把 admin / engineer 對 production 資源的 session 通通走可治理的入口</em>、每個 session 有 <em>identity-bound short-lived cert</em>、有 <em>audit log</em>、有 <em>錄影</em>、有 <em>MFA gate</em>。比較對象：</p>
<ul>
<li>跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD 等 IdP 比、Teleport 不取代 SSO、而是 <em>把 SSO identity 帶到 infrastructure layer</em> — Okta 給 user identity + group、Teleport 把這個 identity 翻譯成 SSH cert / DB cert / K8s cert</li>
<li>跟傳統 bastion + SSH key 比、Teleport 把 <em>long-lived SSH key</em> 換成 <em>short-lived cert</em>（預設 TTL 數小時、過期自動失效）、把 <em>看不到的 session</em> 換成 <em>全程錄影 + searchable audit log</em></li>
<li>跟 HashiCorp Boundary 比、Teleport 走 <em>protocol-aware proxy</em>（懂 SSH / PostgreSQL / Kubernetes API 協議、可以 decode keystroke 跟 query）、Boundary 走 <em>generic TCP proxy</em>（協議無感、不能錄 keystroke 但部署更輕）</li>
<li>跟 Tailscale SSH 比、Tailscale 是 <em>network mesh 加 SSH</em>、適合小團隊 flat network；Teleport 是 <em>PAM + 多協議 + 跨環境 audit</em>、適合需要 SOC handoff 的環境</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare Access</a> 比、Cloudflare Access 是 <em>application-layer ZTNA</em>（內部 web app / API 用）、Teleport 是 <em>infrastructure-layer ZTNA</em>（SSH / DB / K8s 用）、兩者互補</li>
</ul>
<p>關鍵張力：<em>PAM 的覆蓋完整度</em> ↔ <em>operator 摩擦</em>。Teleport 開越多（per-session MFA、Access Request 要 approval、Device Trust 強制企業裝置）、helpdesk SE 那種「拿到密碼直接進 prod」的 blast radius 越小、但 on-call engineer 在凌晨三點修事故時的摩擦也越大。要根據 <em>資源敏感度分層</em> 設定、不是一刀切。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Teleport 在 access stack 中承擔哪一段（infrastructure session）、哪些不屬於它（user identity 屬 Okta、long-lived service secret 屬 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>、application access 可用 Cloudflare Access）</li>
<li>Cluster / Proxy / Auth Service / Node 拓樸的部署選擇（Cloud SaaS vs Self-hosted、Trusted Cluster 跨環境）</li>
<li>Roles + Access Requests + Per-session MFA + Session Recording 四件套的工程化設定（誰能 approve、TTL 多長、錄影存哪）</li>
<li>何時用 Teleport、何時走 Boundary / Tailscale SSH / Cloudflare Access 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Teleport deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>是否還有 long-lived credential 旁路</strong>：production host 是否仍接受 <code>~/.ssh/authorized_keys</code> 的長期 key、DB 是否仍有 shared admin password、K8s kubeconfig 是否還在 engineer laptop 永存 — Teleport 收編失敗的最大訊號是 <em>存在 bypass Teleport 的捷徑</em></li>
<li><strong>Per-session MFA 是否對 sensitive resource 強制</strong>：prod SSH / prod DB / payment system 進 session 時是否每次都 re-MFA、不是「早上登入一次後 8 小時都通行」、role 設定有沒有 <code>require_session_mfa: true</code></li>
<li><strong>Access Request 的 standing privilege 是否收零</strong>：日常 role 是否只有 read-only、所有 write / admin operation 是否走 <em>Access Request</em> + approver gate + TTL、approver 是否 SOC / SRE on-call 而非任意 lead</li>
<li><strong>Session Recording 是否真的可回查</strong>：SSH / K8s / DB session 錄影是否落地 S3 / GCS、是否可在 audit log 透過 user / time / resource 三軸搜尋並回放、recording retention 是否符合合規（金融通常 7 年）</li>
</ul>
<p>四件事任一缺失、就回到 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> 補設定。最容易踩的是第三點 — Teleport 裝了但日常 role 仍給 standing admin、Access Request 變裝飾、helpdesk SE 場景的 mitigation 等於沒上。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Cluster + Proxy + Auth Service 拓樸</strong>：Teleport cluster 由三個 first-class component 組成 — <em>Auth Service</em>（CA、簽 cert、存 audit log、policy decision point）、<em>Proxy</em>（user 連線入口、做 protocol translation、把 SSH / DB / K8s request 轉到 Node）、<em>Node</em>（被保護的資源、裝 Teleport agent 或走 agentless 模式）。Cloud（SaaS）把 Auth + Proxy 託管、客戶只管 Node；Self-hosted 三層都自管、適合需要 data residency / FedRAMP 的環境。</p>
<p><strong>多協議 Resource Access</strong>：Teleport 是 <em>protocol-aware proxy</em>、不是 generic TCP tunnel — SSH Access 懂 OpenSSH、Database Access 懂 PostgreSQL / MySQL / MongoDB / Snowflake / Redis wire protocol、Kubernetes Access 懂 K8s API + RBAC impersonation、Desktop Access 懂 RDP、Application Access 懂 HTTP（包 AWS / GCP console 跟內部 web app）。協議感知的價值是 <em>可以錄 keystroke / query / 滑鼠移動</em>、可以做 <em>per-query approval</em>（DB Access 可設「DROP TABLE 要 approver」）、generic proxy 做不到。</p>
<p><strong>Roles + RBAC</strong>：Teleport role 是 YAML 定義的 RBAC policy、控制 <em>誰可以連哪些 resource、用什麼 OS user、執行什麼指令、session TTL 多長、要不要 per-session MFA</em>。Role 跟 Okta group 透過 SAML / OIDC attribute mapping 綁定 — Okta <code>group=sre-prod</code> 自動拿到 Teleport <code>role=prod-ssh-readonly</code>、不用 Teleport 端維護 user list。</p>
<p><strong>Access Requests（JIT approval）</strong>：standing privilege 收零的核心機制 — engineer 平常只有 read-only role、需要 write / admin 時透過 CLI / web UI 開 <em>Access Request</em>、指定 role + reason + TTL、approver 在 Slack / web 收到通知後 approve / deny、approve 後該 user 拿到該 role TTL（例如 4 小時）、過期自動 revoke。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a> 的 mitigation — 即使 helpdesk SE 拿到 user 密碼、該 user 也沒有 standing admin 可用、要進 prod 必須額外開 Access Request + approver 看到 reason 異常會 deny。</p>
<p><strong>Per-session MFA</strong>：高敏 session 強制每次連線都 re-MFA、不是登入一次後 session TTL 內都通行。role 設 <code>require_session_mfa: true</code>、user <code>tsh ssh prod-db-01</code> 時會跳 Yubikey / WebAuthn 提示、過了才連得進去。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a> 的 lesson — 即使 attacker 用 push fatigue 拿到 IdP session、要進 prod infrastructure 還會撞到第二道 MFA。</p>
<p><strong>Session Recording + Audit</strong>：所有 SSH / K8s / DB / Desktop session 全程錄影、SSH 錄 keystroke + output、DB 錄 SQL query、K8s 錄 API call、Desktop 錄畫面。錄影預設存 Auth Service local disk、production 應該設 <em>sync mode</em> 即時寫 S3 / GCS、不要等 session 結束才上傳（attacker 結束前 wipe）。Audit log 走結構化 JSON、可 export 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Elastic、是 SOC 的 first-class signal。</p>
<p><strong>Trusted Cluster 跨環境 federation</strong>：dev / staging / prod 各自跑 Teleport cluster、用 <em>Trusted Cluster</em> 建立信任關係、user 從 root cluster 一次 login 就能 <code>tsh ssh --cluster=prod node-01</code>、不用每個環境各 login。設計重點是 <em>root cluster 是 SSO + 政策中心、leaf cluster 是各環境本地控制</em>、leaf 出事不會把 root identity 拖下水。</p>
<p><strong>跟 Okta / GitHub OIDC SSO 整合</strong>：Teleport 不做 user identity、authentication 全部委派給 IdP — Okta 設 SAML app、Teleport 設 SAML connector、user <code>tsh login</code> 跳 Okta 認證後拿 Teleport short-lived cert。GitHub Actions 也可以用 OIDC token 換 Teleport cert（給 CI 用、見下方 Machine ID）、不用埋 GitHub Actions secret。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Teleport</th>
          <th>HashiCorp Boundary</th>
          <th>Tailscale SSH</th>
          <th>Cloudflare Access</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要 surface</td>
          <td>Infrastructure（SSH / DB / K8s / Desktop）</td>
          <td>Infrastructure（generic TCP）</td>
          <td>Network mesh + SSH</td>
          <td>Application（web app / API）</td>
      </tr>
      <tr>
          <td>協議感知</td>
          <td>強 — 懂 SSH / DB / K8s / RDP / HTTP</td>
          <td>弱 — generic TCP proxy、不解協議</td>
          <td>弱 — SSH 為主、其他靠 network</td>
          <td>HTTP-only</td>
      </tr>
      <tr>
          <td>Short-lived cert</td>
          <td>強 — 各協議都有專屬 cert（SSH / DB / K8s）</td>
          <td>中 — 主要靠 Vault credential broker</td>
          <td>中 — SSH cert by Tailscale CA</td>
          <td>N/A（HTTP token）</td>
      </tr>
      <tr>
          <td>Session 錄影</td>
          <td>全程 keystroke / query / 畫面</td>
          <td>TCP-level 連線 metadata、不錄內容</td>
          <td>基本 SSH log、不錄 keystroke</td>
          <td>HTTP request log</td>
      </tr>
      <tr>
          <td>JIT access</td>
          <td>Access Request + approver + TTL</td>
          <td>Vault dynamic credential lease</td>
          <td>ACL tag、無 approver workflow</td>
          <td>Policy + identity gate</td>
      </tr>
      <tr>
          <td>Per-session MFA</td>
          <td>第一級支援、role 級別 toggle</td>
          <td>透過 Vault MFA、間接</td>
          <td>透過 Tailscale identity、間接</td>
          <td>App-level MFA（透過 Cloudflare）</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Cloud SaaS / Self-hosted（含 air-gapped）</td>
          <td>Self-hosted（OSS）+ HCP Boundary（SaaS）</td>
          <td>SaaS only</td>
          <td>SaaS only（Cloudflare 邊緣）</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Per protected resource + MAU、Cloud / Self</td>
          <td>跟 Vault Enterprise 綁定</td>
          <td>Per user / device</td>
          <td>Per user</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>需要 PAM + audit + JIT 的 admin session 治理</td>
          <td>已是 Vault 重度使用者、generic TCP 多</td>
          <td>小團隊 flat network、SSH 為主</td>
          <td>內部 web app / API 走 ZTNA、非 infra</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — role YAML / Trusted Cluster 設定多</td>
          <td>中 — Boundary target 設定</td>
          <td>低 — ACL 移植性高</td>
          <td>低 — policy 簡單</td>
      </tr>
  </tbody>
</table>
<p>選 Teleport 的核心訴求：<em>多協議 infrastructure session</em> + <em>session recording + JIT + per-session MFA 是 SOC 必要證據</em> + <em>跨環境 federation</em>（dev / staging / prod / partner）+ <em>願意承擔 cluster 維運成本（self-hosted）或 SaaS 訂閱</em>。純小團隊 flat network 走 Tailscale 更輕、純內部 web app 走 Cloudflare Access 更便宜、純 Vault-driven workflow 走 Boundary 整合更順。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Machine ID — service-to-service short-lived cert</strong>：CI / 內部 worker / cron job 也走 Teleport 拿 short-lived cert、不用埋長期 SSH key 或 DB password。Machine ID agent（<code>tbot</code>）跑在 CI runner、用 IAM role / GitHub OIDC token / Kubernetes service account 證明自己身份、Teleport 簽 short-lived SSH cert / DB cert（TTL 通常 1 小時）。對應 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> 的 workload identity 概念、Teleport Machine ID 是 SPIRE 在 infrastructure access surface 的對等實作。</p>
<p><strong>Device Trust — 裝置驗證</strong>：除了 user identity + MFA、Teleport Enterprise 還可以強制 <em>只有企業 enrolled 裝置可以連 prod</em>。裝置透過 TPM / Secure Enclave 註冊 hardware-bound key、Teleport login 時驗證裝置 cert。對應 BYOD 風險 — 即使 attacker 拿到 user credential + MFA token、沒有企業裝置就連不進 prod。</p>
<p><strong>Moderated Session + Session Live View</strong>：高敏 session 設定 <em>需要第二人在線 moderate</em>、SOC analyst 即時看 keystroke、可以 <code>kill session</code>。對應金融 / 政府的「四眼原則」合規要求。Live View 也可以給 SOC 在 incident 進行中即時看 attacker 操作（如果 attacker 不知道被監聽）。</p>
<p><strong>FedRAMP / HIPAA / PCI compliance</strong>：Teleport Enterprise 有 FedRAMP Moderate authorization、Self-hosted 模式可部署 air-gapped 環境、audit log 滿足 HIPAA / PCI 的 access logging 要求。Cloud 版本走 SOC 2 Type II、FedRAMP 版本走 GovCloud 部署。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> 的職責切分</strong>：Vault 管 <em>service-to-service secret</em>（DB password、API key、PKI CA）、SPIRE 管 <em>workload identity</em>（SVID、跨服務 mTLS）、Teleport 管 <em>人類 admin session + service short-lived cert（透過 Machine ID）</em>。三者互補不重疊 — Vault 不該直接給 engineer 拿 SSH key、SPIRE 不該管 helpdesk admin 怎麼進 prod、Teleport 不該變成長期 API key 倉庫。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>裝了 Teleport 但 engineer 還在用直接 SSH key</strong>：production host 沒收掉 <code>authorized_keys</code>、long-lived key 旁路存在 — host onboarding 流程強制走 Teleport Node enrollment、CI 跑 <code>sshd_config</code> audit 抓 <code>AuthorizedKeysFile</code></li>
<li><strong>Access Request 變裝飾、approver 秒按</strong>：approver 是同團隊 lead 沒看 reason、TTL 設 24 小時等於 standing — approver 改 SOC on-call / cross-team、TTL 預設 1-4 小時、high-impact role 強制兩人 approve</li>
<li><strong>Per-session MFA 開了但 user 抱怨太煩</strong>：所有 role 一刀切要 MFA — 分層：dev / staging role 只要登入 MFA、prod role 才 per-session MFA、payment / PII DB 加 moderated session</li>
<li><strong>Session recording 沒存到 S3、attacker 結束前 wipe</strong>：用 default async mode、recording 留在 Auth Service local — 改 <em>sync mode</em> 即時寫 S3、S3 開 object lock 防刪除</li>
<li><strong>Trusted Cluster leaf 出事拖累 root</strong>：leaf cluster admin 也有 root cluster 權限 — leaf 用獨立 role mapping、leaf admin 不繼承 root identity、leaf 出事只影響該環境</li>
<li><strong>Cloud SaaS 跨區 latency 高</strong>：team 在亞太但 Teleport Cloud 在 us-east — 選 Teleport Cloud 地區 / 改 Self-hosted 部署在自家最近 region</li>
<li><strong>Machine ID cert TTL 短導致 CI 中途失效</strong>：long-running job &gt; cert TTL — 在 job 內定期 <code>tbot</code> renew、或拉長 TTL 但收緊 IAM role binding</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純內部 web app / API access</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare Access</a>（application-layer ZTNA）</td>
      </tr>
      <tr>
          <td>小團隊 flat network + SSH</td>
          <td>Tailscale SSH（network mesh + 輕量 SSH cert）</td>
      </tr>
      <tr>
          <td>已重度使用 Vault、generic TCP 為主</td>
          <td>HashiCorp Boundary（跟 Vault credential broker 整合）</td>
      </tr>
      <tr>
          <td>Service-to-service secret 跟 long-lived</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a></td>
      </tr>
      <tr>
          <td>Workload identity / SVID</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
      </tr>
      <tr>
          <td>人類 SSO / IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>Session audit log 進 SIEM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Teleport role YAML 完整 reference、predicate language 進階用法</li>
<li>Teleport Cloud vs Self-hosted 的 SLA / pricing 細節</li>
<li>Teleport Connect（桌面 client app）的具體操作流程</li>
<li>Air-gapped 部署的 license server 跟 update workflow</li>
<li>各協議的 wire protocol 解析（PostgreSQL / MySQL session 怎麼被 decode）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Teleport 沒有 vendor-level 公開事故、但 07 案例庫的 identity / access 系列都是 PAM 設計的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Teleport 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 Identity Lateral Impact</a></td>
          <td>helpdesk SE 拿到 reset 密碼後直接進 prod admin — Teleport JIT Access Request + per-session MFA 是 first-class mitigation、standing access 收零後 SE 拿到密碼也進不了 prod</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>push-based MFA fail 後 attacker 拿到 standing internal tool access — Teleport per-session MFA 是第二道 gate（即使 IdP session 被劫、進 prod infra 還要 re-MFA）+ session recording 給 SOC 事後重建</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System 2023</a></td>
          <td>IdP 端 support tool compromise 後 attacker 拿到客戶 session token — 客戶側 Teleport audit log 仍能看到「異常 source IP / device 進 SSH session」、是 IdP 失守時的補位偵測層</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>平行：HashiCorp Boundary / Tailscale SSH / <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare Access</a></li>
<li>互補：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP、user identity）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（service secret）、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity）</li>
<li>偵測：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（session audit log 入 SIEM）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（compromise session 走 IR workflow）</li>
<li>官方：<a href="https://goteleport.com/docs/">Teleport Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>1.12 大規模 DB 遷移實戰</title><link>https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>DB 遷移是後端工程中 &lt;em>風險最高的長期工作&lt;/em> 之一。一次失敗的遷移可能造成資料丟失、用戶體驗劣化、合規違約、團隊信心受挫。本章整理近 5 年公開的大規模 DB 遷移案例、提煉出可重用的工程流程。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook&lt;/a> 的關係：1.6 是 &lt;em>generic playbook&lt;/em>、本章針對「&lt;em>跨 DB 種類&lt;/em>」遷移（PostgreSQL → Aurora、TiDB → DynamoDB、MongoDB → Cosmos DB）、規模較大、風險較高。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a> 的關係：1.7 處理 &lt;em>同一 DB 內&lt;/em> 的 schema 演進、本章處理 &lt;em>換 DB engine&lt;/em> 的遷移。兩者都用 evidence-based gate、但 stakes 不同。&lt;/p>
&lt;p>讀完後讀者能回答：跨 DB 遷移該怎麼分階段、dual-write 怎麼設計、shadow read 怎麼驗證、cutover 怎麼安全進行、rollback window 訂多久。&lt;/p>
&lt;h2 id="遷移類型分類">遷移類型分類&lt;/h2>
&lt;p>DB 遷移不是單一概念、按 &lt;em>變動範圍&lt;/em> 分四類、每類風險跟流程不同。&lt;/p>
&lt;p>&lt;strong>Type 1：scale-up（換 instance）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：m5.large → m5.4xlarge&lt;/li>
&lt;li>變動：硬體規格、不變 schema、不變 DB engine&lt;/li>
&lt;li>風險：低、通常 minutes downtime 即可&lt;/li>
&lt;li>工具：vendor 提供 in-place scaling&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 2：schema migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：加欄位、加 index、改 data type&lt;/li>
&lt;li>變動：schema 結構、不變 DB engine&lt;/li>
&lt;li>風險：中、需要 expand-contract 模式&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 3：cross-DB engine migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：PostgreSQL → Aurora、SQL Server → PostgreSQL、TiDB → DynamoDB&lt;/li>
&lt;li>變動：DB engine、可能 schema、可能 query language&lt;/li>
&lt;li>風險：高、可能需要應用層改寫、cutover 風險大&lt;/li>
&lt;li>本章重點&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 4：cross-model migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：RDBMS → KV、Document → Graph&lt;/li>
&lt;li>變動：資料模型、必須應用層大改寫&lt;/li>
&lt;li>風險：極高、通常分 service 漸進遷移、不會一次切完&lt;/li>
&lt;li>對應 &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%">9.C20 Zomato TiDB → DynamoDB&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼要做大規模-db-遷移">為什麼要做大規模 DB 遷移&lt;/h2>
&lt;p>不是所有遷移都值得做。理由要強過 &lt;em>成本 + 風險&lt;/em>、不然不該開工。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>DB 遷移是後端工程中 <em>風險最高的長期工作</em> 之一。一次失敗的遷移可能造成資料丟失、用戶體驗劣化、合規違約、團隊信心受挫。本章整理近 5 年公開的大規模 DB 遷移案例、提煉出可重用的工程流程。</p>
<p>跟 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a> 的關係：1.6 是 <em>generic playbook</em>、本章針對「<em>跨 DB 種類</em>」遷移（PostgreSQL → Aurora、TiDB → DynamoDB、MongoDB → Cosmos DB）、規模較大、風險較高。</p>
<p>跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> 的關係：1.7 處理 <em>同一 DB 內</em> 的 schema 演進、本章處理 <em>換 DB engine</em> 的遷移。兩者都用 evidence-based gate、但 stakes 不同。</p>
<p>讀完後讀者能回答：跨 DB 遷移該怎麼分階段、dual-write 怎麼設計、shadow read 怎麼驗證、cutover 怎麼安全進行、rollback window 訂多久。</p>
<h2 id="遷移類型分類">遷移類型分類</h2>
<p>DB 遷移不是單一概念、按 <em>變動範圍</em> 分四類、每類風險跟流程不同。</p>
<p><strong>Type 1：scale-up（換 instance）</strong>：</p>
<ul>
<li>例：m5.large → m5.4xlarge</li>
<li>變動：硬體規格、不變 schema、不變 DB engine</li>
<li>風險：低、通常 minutes downtime 即可</li>
<li>工具：vendor 提供 in-place scaling</li>
</ul>
<p><strong>Type 2：schema migration</strong>：</p>
<ul>
<li>例：加欄位、加 index、改 data type</li>
<li>變動：schema 結構、不變 DB engine</li>
<li>風險：中、需要 expand-contract 模式</li>
<li>詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a></li>
</ul>
<p><strong>Type 3：cross-DB engine migration</strong>：</p>
<ul>
<li>例：PostgreSQL → Aurora、SQL Server → PostgreSQL、TiDB → DynamoDB</li>
<li>變動：DB engine、可能 schema、可能 query language</li>
<li>風險：高、可能需要應用層改寫、cutover 風險大</li>
<li>本章重點</li>
</ul>
<p><strong>Type 4：cross-model migration</strong>：</p>
<ul>
<li>例：RDBMS → KV、Document → Graph</li>
<li>變動：資料模型、必須應用層大改寫</li>
<li>風險：極高、通常分 service 漸進遷移、不會一次切完</li>
<li>對應 <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 TiDB → DynamoDB</a></li>
</ul>
<h2 id="為什麼要做大規模-db-遷移">為什麼要做大規模 DB 遷移</h2>
<p>不是所有遷移都值得做。理由要強過 <em>成本 + 風險</em>、不然不該開工。</p>
<p><strong>合理動機</strong>：</p>
<ul>
<li><strong>舊系統規模上限</strong>：<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> TiDB 必須長期 over-provision 應付 spike、成本不划算 → 換 DynamoDB on-demand 後 50% 成本下降</li>
<li><strong>舊系統運維成本</strong>：<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 的容量規劃成本">9.C9 Spotify</a> 自管 Kafka 工程成本太高 → 換 managed Pub/Sub 釋放 SRE</li>
<li><strong>舊系統失能</strong>：<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> 多套 RDBMS（PostgreSQL、MySQL、Oracle）DBA 負擔重 → 統一到 Aurora、效能 +75% 成本 -28%</li>
<li><strong>vendor 終止支援</strong>：mongoDB 改授權、TiDB 改授權、Mesos 被棄、Oracle 升級費高</li>
<li><strong>合規要求</strong>：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 新市場上線、需要本地合規 cluster</li>
<li><strong>新功能需求</strong>：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 需要 global distribution、原 MongoDB 達不到</li>
</ul>
<p><strong>不合理動機（要警惕）</strong>：</p>
<ul>
<li>「新技術好酷」：fad-driven、通常會後悔</li>
<li>「vendor sales 推銷」：sales 利益跟你 ROI 不一致</li>
<li>「同行 X 也在遷」：人家的場景跟你不同</li>
<li>「主管要看到 transformation」：政治、不是工程</li>
</ul>
<h2 id="遷移階段流程">遷移階段流程</h2>
<p>成熟的大規模 DB 遷移分五階段、每階段有明確 exit criteria。</p>
<h3 id="階段-1可行性評估t-180--t-90">階段 1：可行性評估（T-180 ~ T-90）</h3>
<p><strong>輸出</strong>：可行性報告、決定 go / no-go。</p>
<p><strong>評估項目</strong>：</p>
<ul>
<li>workload 在新 DB 上是否真的能跑（不是 marketing、是實測 POC）</li>
<li>應用層改寫成本（哪些 query 需要改、哪些 ORM 需要換）</li>
<li>遷移時程預估（含 <em>合規審查</em> lead time、如金融業可能 3-12 個月）</li>
<li>成本對比（總成本曲線、不只當下 snapshot）</li>
<li>失敗代價（如果遷移失敗、business 影響多大）</li>
</ul>
<p><strong>跨雲遷移特有 gap 分析</strong>：當遷移橫跨雲廠商時、評估項目要加上 <a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖</a> 的「對應 ≠ 等價」差異維度：</p>
<ul>
<li>一致性模型差異（如 DynamoDB eventual vs Cosmos DB 五級可選）</li>
<li>failover 時間差異（vendor 文件 vs 實測長尾）</li>
<li>計價模型差異（per-request vs provisioned capacity 換算）</li>
<li>配額差異（partition 上限、batch size、throttling 行為）</li>
<li>Data gravity / egress lock-in（PB 級資料的 egress fee 常是被低估的單筆最大成本）</li>
</ul>
<p>跨雲遷移的失敗多數來自 0.19 對照表沒做完整 gap 分析、把「名稱對應」當「能力等價」。</p>
<p><strong>對應案例</strong>：</p>
<ul>
<li><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> — POC 驗證 DynamoDB 撐得住、再決定遷移</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB API 相容讓 POC 成本低、加速決策</li>
</ul>
<h3 id="階段-2應用層相容性改造t-90--t-30">階段 2：應用層相容性改造（T-90 ~ T-30）</h3>
<p><strong>輸出</strong>：應用層支援 <em>新舊 DB 雙寫</em>、可以隨時切換。</p>
<p><strong>改造項目</strong>：</p>
<ul>
<li>Repository adapter 抽象化（<a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter</a>）</li>
<li>新增 <em>新 DB</em> 的 adapter 實作</li>
<li>配置「寫入 mode」：old only / dual-write / new only</li>
<li>query 端「讀取 mode」：old / new / shadow（讀兩邊比對）</li>
<li>error handling 兼容（不同 DB 的錯誤碼）</li>
</ul>
<p><strong>API-compatible 遷移的優勢</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> MongoDB → Cosmos DB MongoDB API — 應用層幾乎不用改、只換 connection string</li>
<li>Aurora PostgreSQL-compatible → 不改 SQL 跟 ORM</li>
<li>缺點：API 相容不等於行為完全相同、要 <em>特定 query pattern</em> 驗證</li>
</ul>
<h3 id="階段-3dual-write--shadow-read-驗證t-30--t-7">階段 3：Dual-write + shadow read 驗證（T-30 ~ T-7）</h3>
<p>dual-write / shadow read / backfill 的 <em>generic 機制</em> 詳見 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a>（含 Dual-write divergence schema 詳細分類）；本章只強調 <em>跨 DB engine</em> 遷移的特殊取捨。</p>
<p><strong>輸出</strong>：新 DB 已 <em>並行寫入</em>、跟舊 DB 結果一致。</p>
<p><strong>Dual-write 流程</strong>：</p>
<ol>
<li>應用層同時寫入 old 跟 new DB</li>
<li>用 old DB 結果回應用戶</li>
<li>log 兩邊寫入是否成功、有差異就 alert</li>
<li>backfill 之前的歷史資料到 new DB</li>
</ol>
<p><strong>Shadow read 驗證</strong>：</p>
<ol>
<li>應用層查 old DB 拿結果回用戶</li>
<li><em>也</em> 查 new DB、比對結果是否一致</li>
<li>不一致記錄到 audit log</li>
<li>跑 N 天（建議 7-14 天）確認一致性高</li>
</ol>
<p><strong>注意事項</strong>：</p>
<ul>
<li>Dual-write 期間 <em>兩邊都要可寫</em>、寫失敗的 fallback 流程明確</li>
<li>新 DB 還沒承擔流量、容量規劃要 <em>提前 ramp up</em>、不要等 cutover 才發現容量不夠</li>
<li>監控指標：write success rate、cross-DB inconsistency rate、replication lag、performance metrics</li>
</ul>
<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%">9.C20 Zomato</a> — 遷移前用 dual-write 驗證 4 倍吞吐改善是真的、不是 POC marketing。</p>
<h3 id="階段-4cutovert-7--t-0">階段 4：Cutover（T-7 ~ T-0）</h3>
<p><strong>輸出</strong>：用戶流量切到 new DB、old DB 變成 fallback。</p>
<p><strong>Cutover 策略</strong>：</p>
<p><strong>Big-bang cutover</strong>：一次切全部流量</p>
<ul>
<li>優點：簡單、不必維護 <em>跨 DB consistency</em></li>
<li>缺點：風險集中、rollback 困難</li>
<li>適合：小規模、low-stakes</li>
</ul>
<p><strong>Gradual cutover</strong>（推薦）：分階段切</p>
<ul>
<li>T-7：1% 流量到 new DB、觀察 1 天</li>
<li>T-6：5% → 觀察 1 天</li>
<li>T-5：25% → 觀察 1 天</li>
<li>T-3：50% → 觀察 2 天</li>
<li>T-1：100%</li>
</ul>
<p><strong>Reverse rollout</strong>：某些工作負載先切（read-only first、再 write）</p>
<ul>
<li>T-7：所有 read 切到 new DB（write 還在 old）</li>
<li>T-3：write 切到 new DB（read 已驗證）</li>
</ul>
<h3 id="階段-5rollback-window--清理t0--t30">階段 5：Rollback window + 清理（T+0 ~ T+30+）</h3>
<p><strong>Rollback window</strong>：cutover 後保持 <em>可隨時 rollback 回 old DB</em> 的狀態。</p>
<p><strong>Rollback window 設計</strong>：</p>
<ul>
<li>短期（T+7）：保持 dual-write、可以即時切回 old DB</li>
<li>中期（T+30）：保留 old DB read-only、需要 manual 切回但快</li>
<li>長期（T+90）：保留 old DB snapshot、disaster recovery 用</li>
<li>結束：徹底刪除 old DB（含 backup、ETL pipeline 改寫）</li>
</ul>
<p><strong>Cleanup 工作</strong>：</p>
<ul>
<li>移除 dual-write code</li>
<li>移除 shadow read code</li>
<li>簡化 repository adapter（只保留 new DB）</li>
<li>文件更新（runbook、onboarding doc）</li>
<li>decommission old DB（不立即砍、保留至少 90 天備援）</li>
</ul>
<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 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub</a> — 大規模事件交付系統的 multi-month 漸進遷移、有明確 rollback path。</p>
<h2 id="api-compatible-vs-應用層改寫">API-compatible vs 應用層改寫</h2>
<p>跨 DB 遷移的關鍵決策：要不要追求 <em>應用層零改動</em>。</p>
<p><strong>API-compatible 遷移</strong>：</p>
<ul>
<li>新 DB 提供舊 DB 的 wire protocol / API</li>
<li>應用層只換 connection string、不改 query</li>
<li>例：MongoDB → Cosmos DB（MongoDB API）、Cassandra → Cosmos DB（Cassandra API）、MySQL → Aurora（MySQL）</li>
</ul>
<p><strong>優點</strong>：</p>
<ul>
<li>遷移成本低（不必改 application code）</li>
<li>風險低（不會引入 query bug）</li>
<li>時程快（不必等 application 改寫）</li>
</ul>
<p><strong>缺點</strong>：</p>
<ul>
<li>行為可能不完全一致（subtle bug）</li>
<li>性能可能不是最佳（compat 層有 overhead）</li>
<li>vendor lock-in 更深</li>
</ul>
<p><strong>應用層改寫</strong>：</p>
<ul>
<li>換 query 風格、ORM、access pattern</li>
<li>例：PostgreSQL → DynamoDB（SQL → NoSQL access pattern）</li>
</ul>
<p><strong>何時必須應用層改寫</strong>：</p>
<ul>
<li>跨 model（RDBMS → KV）</li>
<li>跨 query paradigm（SQL → MongoDB 風格）</li>
<li>想拿 native 性能 / 成本優勢</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB API compat、應用層幾乎不改</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — 多套 RDBMS → Aurora、PostgreSQL / MySQL 相容、最小應用層改動</li>
<li><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> — TiDB（SQL）→ DynamoDB（KV）、必須改 access pattern、不能 API compat</li>
</ul>
<h2 id="容量規劃在遷移中的角色">容量規劃在遷移中的角色</h2>
<p>DB 遷移期間有特殊的容量挑戰、跟一般 capacity planning 不同。</p>
<p><strong>遷移期容量需求</strong>：</p>
<ul>
<li>old DB 持續服務 production</li>
<li>new DB 接 dual-write（額外負載）</li>
<li>backfill historical data（額外負載）</li>
<li>shadow read（讀兩倍）</li>
<li>應用層擴容（dual-write 邏輯吃 CPU）</li>
</ul>
<p><strong>典型容量增加</strong>：</p>
<ul>
<li>應用層 +20-30%（dual-write、cross-DB logic、metric）</li>
<li>new DB 必須 <em>提前 provision</em> 接 100% 流量</li>
<li>監控 / log 容量 +50%（要追蹤更多事件）</li>
</ul>
<p><strong>對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></strong>：遷移期是「臨時 over-provisioning 期」、要算進 cost。遷移完才能 right-sizing。</p>
<p><strong>對應 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></strong>：dual-write 跟 shadow read 是 production validation 的特殊形式、要按 9.10 的安全邊界設計。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>遷移類型</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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 的容量規劃成本">9.C9 Spotify</a></td>
          <td>self-managed → managed</td>
          <td>7500 萬用戶事件交付系統遷移、人力成本驅動</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>NewSQL → KV NoSQL</td>
          <td>對照 over-provisioning 成本、50% 帳單下降</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>多套 RDBMS → 統一 Aurora</td>
          <td>DB consolidation 釋放 DBA、效能 +75%</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a></td>
          <td>MongoDB → Cosmos DB（API compat）</td>
          <td>API 相容遷移路徑、planet-scale 分析</td>
      </tr>
  </tbody>
</table>
<h2 id="遷移評估的成本曲線">遷移評估的成本曲線</h2>
<p>遷移 ROI 評估常見錯誤是 <em>只看當下流量下的成本對照</em>、忽略未來流量曲線。決策時要算 12-24 個月的累積成本、不是 snapshot。</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%">9.C20 Zomato TiDB → DynamoDB</a> — Zomato 帳單系統「成本降 50%」是當下流量下的對照。如果未來流量繼續成長、DynamoDB on-demand 的單位成本可能比 TiDB 自管 cluster 高、達到某規模後 TiDB 反而更便宜。</p>
<p><strong>評估公式</strong>：</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">未來 N 個月累積成本 = sum(月流量 × 月單位成本)</span></span></code></pre></div><p>各 DB 的「月單位成本 vs 流量」曲線形狀不同：</p>
<ul>
<li><strong>DynamoDB on-demand</strong>：線性、按用量計費、單位成本固定</li>
<li><strong>DynamoDB provisioned + reserved</strong>：階梯、預訂量越大單價越低</li>
<li><strong>自管 TiDB / PostgreSQL</strong>：階梯 + 固定基線、低流量時單位成本高（基線分攤）、高流量時單位成本低</li>
<li><strong>Aurora Serverless</strong>：線性、但有最低 ACU 基線</li>
<li><strong>Spanner</strong>：節點數 × 單價、增量是 100 pu 一單位</li>
</ul>
<p><strong>曲線交叉點是選型決策的關鍵</strong>：DynamoDB on-demand 跟自管 PostgreSQL 在某個流量水位交叉、流量低於此值前者便宜（無基線成本）、高於此值後者便宜（基線分攤後單價低）。Aurora Serverless 跟 Aurora provisioned 也有類似交叉、波動大的 workload 在 Serverless 划算、穩定的在 provisioned 划算。Spanner 因為節點數階梯式增加、跨節點交叉點通常在 <em>每節點 70-80% 利用率</em> — 過了就要加節點、新節點利用率掉回 50% 是常態。判讀重點：選型不該只看 <em>當下流量點</em>、要看未來 12-24 月的流量曲線會跨過哪些交叉點、再決定哪種計費模式總成本最低。</p>
<p><strong>遷移 ROI 評估的維度</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>應該算進去</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infra 成本</td>
          <td>當下 + 預期成長下的累積、不是 snapshot</td>
      </tr>
      <tr>
          <td>人力成本</td>
          <td>DBA、SRE、on-call 工時、跟 vendor 整合工時</td>
      </tr>
      <tr>
          <td>機會成本</td>
          <td>遷移期間不能做新功能的時間成本</td>
      </tr>
      <tr>
          <td>Lock-in 成本</td>
          <td>換 vendor 的退場成本、合約年限</td>
      </tr>
      <tr>
          <td>合規 lead time</td>
          <td>受監管產業每市場 3-12 月審查、不算進來時程會崩</td>
      </tr>
      <tr>
          <td>Migration 本身成本</td>
          <td>dual-write infra、shadow read 雙倍負載、人力、風險</td>
      </tr>
  </tbody>
</table>
<p><strong>機會成本延伸</strong>：機會成本是遷移期間 <em>不能做新功能</em> 的時間。大型遷移通常綁住核心 team 6-12 個月、期間業務側看不到產品演進、可能流失市場機會。實務上要算「如果這 6 個月去做新產品、營收 / 競爭優勢值多少」、若超過遷移節省的 infra 成本、遷移不划算。</p>
<p><strong>Lock-in 成本延伸</strong>：vendor lock-in 不是「不能換」、是「換的時候要付多少」。包含：(1) 應用層改寫成本（DynamoDB → Spanner 要改 access pattern）、(2) 合約終止 penalty（reserved capacity 提前解約罰款）、(3) 資料導出成本（雲商出口流量費）、(4) 人才再訓練（DBA 從 Aurora 轉 Spanner 需要時間）。選 vendor 時就要評估這四項、即使沒打算換、合約年限到時也要面對。</p>
<p>判讀重點：「遷移後成本降 50%」這種敘述只看 infra 成本、且只看當下。完整評估要看所有六個維度跨 12-24 月、決策才不會出「短期省、長期更貴」或「短期看似賺、合規卡 1 年」的事故。</p>
<h2 id="合規審查-lead-time-是時程主要拉力">合規審查 lead time 是時程主要拉力</h2>
<p>受監管產業（金融、醫療、電信、政府）的 DB 遷移、<em>合規審查</em> 通常是時程主導因素、不是技術整合。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨 7 個受監管市場遷移到 Aurora、每個市場各自審查（中央銀行 / 金融監管機關 / 個資主管機關）、單一市場審查 3-12 個月、總時程是「市場數 × 平均審查月份」、不是「技術遷移月份」。</p>
<p><strong>合規 lead time 的常見項目</strong>：</p>
<ul>
<li>中央銀行核心系統變更審查（金融業）</li>
<li>個資主管機關的跨境傳輸審批（GDPR / 各國個資法）</li>
<li>醫療資料的隱私審查（HIPAA / 各國醫療法）</li>
<li>雲端服務商的合規認證對應（PCI-DSS、ISO 27001、SOC 2）</li>
<li>跨市場資料駐留限制（中國《數據安全法》、印度資料保護法、歐盟 GDPR）</li>
</ul>
<p><strong>規劃含義</strong>：</p>
<ul>
<li>技術側 ready ≠ 可上線、合規簽核才是 cutover gate</li>
<li>合規審查通常 serial、不能 parallel（單一審查機關沒法平行處理多 case）</li>
<li>高風險變更（DB 換 vendor、cross-border）審查週期最長</li>
<li>跨市場部署、各市場各自審、不能用某市場結果代替</li>
</ul>
<p>判讀重點：受監管產業的遷移計畫、預設技術側 50%、合規 50% 工時、不是「技術 90% / 合規 10%」。低估合規 lead time 會讓專案在最後關頭卡關、且無法用工程資源補。</p>
<h2 id="benchmark-對照基準的解讀">Benchmark 對照基準的解讀</h2>
<p>遷移案例的「X% improvement」要追問 <em>跟什麼基準比</em>、否則容易誤導。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 「10x throughput」是 <em>vs 舊系統</em>、不是 <em>vs 競爭對手</em>。受監管銀行的舊系統通常是 1990s-2000s 的 mainframe 或自建 OLTP、性能本來就低、改善幅度大不代表絕對性能領先。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — 「up to 75% improvement」是 <em>跨多個 workload 的最大改善幅度</em>、不是「每個 workload 都 +75%」。實際每個 workload 改善從 10% 到 75% 不等、平均可能 30-40%。</p>
<p><strong>benchmark 解讀的關鍵問題</strong>（遷移情境專屬）：</p>
<ul>
<li><em>vs 什麼基準</em>：跟舊系統比 vs 跟競爭對手比 vs 跟理論最佳比</li>
<li><em>哪個 workload</em>：是平均 vs 最快 vs 最慢</li>
<li><em>規模對照</em>：在多大流量下測的、自家業務規模類似嗎</li>
</ul>
<p>讀 vendor 案例研究時、這三個遷移專屬維度都要對照、否則「75% 改善」可能變成「在某個 cherry-picked workload、跟舊系統比、規模跟自家不同」、實際搬過去未必有對應收益。</p>
<p><strong>規模對照延伸</strong>：vendor 案例研究最容易誤判的維度。讀者要識別三個訊號才能判斷規模是否類似 — (1) <em>資料量</em>（vendor 揭露的是 GB 還是 PB？自家在哪個量級？）、(2) <em>QPS 分布</em>（vendor 是 sustained 還是 bursty？自家流量形狀是否類似？）、(3) <em>讀寫比</em>（vendor 案例是 write-heavy 還是 read-heavy？自家業務性質是否吻合？）。三個訊號至少要有兩個跟自家對齊、benchmark 數字才有參考價值。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 案例的 18:1 讀寫比、跟一般電商的 5:1 完全不同、不能用同一份 benchmark 推論。</p>
<p><strong>Percentile 跟時間窗口維度</strong> — 是更通用的容量數字判讀問題、詳見 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取的「讀峰值數字的工程細節」</a> 段（容量三口徑、p50/p99/p999 解讀）。遷移情境只需在這個基礎上加「vs 基準 / workload / 規模對照」三個遷移專屬問題。</p>
<h2 id="預設-db治理-pattern">「預設 DB」治理 pattern</h2>
<p>大規模平台選 DB 的做法是建立「預設 DB」規則、新團隊用其他要 <em>justify</em>、逐案決定在這個規模行不通。這個治理 pattern 簡化 onboarding、降低 DB 種類太多的運維成本。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — Genesys Cloud 的 Chief Architect 明確說「Amazon DynamoDB is our primary data layer by default, and teams have to justify the use of something else」。對應 <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> — 把多套 RDB 整合到 Aurora、降低 DB 種類就是降低運維 surface area。</p>
<p><strong>預設 DB 治理的工程含義</strong>：</p>
<ul>
<li>新團隊預設用 X、特殊需求才評估其他、減少 DB 評估的認知負擔</li>
<li>DBA / SRE 知識集中、不必養多個 vendor 的專業</li>
<li>監控、backup、compliance 流程統一、運維成本下降</li>
<li>多個服務的 schema migration / capacity planning 可以共用 tooling</li>
</ul>
<p><strong>選擇預設 DB 的判讀條件</strong>：</p>
<ul>
<li>平台規模夠大（10+ 微服務）、運維 surface area 是真實成本</li>
<li>業務需求大部分可以收斂到單一 DB（OLTP 90%、KV 10% 可以選 OLTP 為預設）</li>
<li>vendor 提供完整能力組合（managed + multi-region + auto-scaling）</li>
</ul>
<p><strong>預設 DB 對應</strong>：</p>
<ul>
<li>AWS 生態大規模 OLTP → Aurora（Netflix）</li>
<li>AWS 生態大規模 KV → DynamoDB（Genesys、Capcom、Disney+）</li>
<li>Azure 生態 multi-model → Cosmos DB</li>
<li>GCP 生態 OLTP → Spanner / AlloyDB</li>
</ul>
<p><strong>同一雲廠商兩個預設 DB 怎麼選邊界</strong>：AWS 生態同時有 Aurora（OLTP 預設）跟 DynamoDB（KV 預設）、不衝突、但要清楚兩者邊界。預設選 Aurora 的條件是「需要 SQL JOIN / ACID 跨表 transaction / 既有 ORM」、預設選 DynamoDB 的條件是「access pattern 已知且固定 / 預期跨 region 寫入 / surge 場景下 connection-based DB 撐不住」。這條邊界要寫進平台的 onboarding doc、否則新 team 會在「Aurora 還是 DynamoDB」之間反覆 review、抵消預設 DB 治理的價值。</p>
<p>判讀重點：小規模平台（&lt; 5 微服務）不必預設 DB 治理、case-by-case 決定即可。隨著服務數量增加、DB 種類失控成為大規模平台的隱性成本、預設 DB 治理變成規模化階段的工程紀律。</p>
<h2 id="vendor-dogfood-是-selection-signal">Vendor dogfood 是 selection signal</h2>
<p>Vendor dogfood signal 是 vendor 自家 production-critical workload 對該服務的使用程度、反映 vendor 對自家服務的真實信任度。讀 vendor 案例研究時、這個訊號比 sales material 更可信、因為 vendor 自己賭身家。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a> — Amazon Prime Day 用自家 DynamoDB + Aurora 撐 1.51 億 RPS + 500B txn。對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — Google 自家 Ads、Play、Search 都用 Spanner。對應 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — Microsoft 365 usage analytics 用自家 Cosmos DB。</p>
<p><strong>Dogfood 訊號為什麼重要</strong>：</p>
<ul>
<li>vendor 自家賭身家、出問題自己第一個踩</li>
<li>內部 dogfood 通常比外部 customer earlier 用、bug 修得快</li>
<li>vendor sales team 的「能撐 X」如果跟內部 dogfood 不一致、是 marketing</li>
<li>內部用量大、vendor 對該服務的工程投入比 marginal customer 多</li>
</ul>
<p><strong>Dogfood 訊號的限制</strong>：</p>
<ul>
<li>vendor 內部享有專屬資源配額跟內部成本機制、外部用戶在公開計費下、單位成本邊界不同</li>
<li>vendor 內部享有深度 API 客製化跟特殊 SLA、外部用戶實際可取得的能力是公開版本</li>
<li>vendor 自家業務的 workload pattern 反映 vendor 自己的業務需求、跟你業務的 workload 可能不同</li>
</ul>
<p>判讀重點：dogfood 是必要訊號、不是充分訊號。看 vendor 自家用代表服務經過嚴格驗證；但「自家業務 vs 你業務」的相似度（資料量、QPS、讀寫比、一致性需求）才是 dogfood signal 是否能套用的判讀條件。</p>
<h2 id="反模式">反模式</h2>
<p>大規模 DB 遷移的常見錯誤：</p>
<ul>
<li><strong>沒做 POC 就 commit 遷移</strong>：發現新 DB 撐不住某個 query pattern、時程崩</li>
<li><strong>dual-write 沒 monitoring</strong>：兩邊不一致沒被發現、cutover 後資料錯亂。divergence 該怎麼分類追蹤、詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Dual-write divergence schema</a></li>
<li><strong>shadow read 跑太短</strong>：1-2 天就 cutover、long-tail bug 沒暴露</li>
<li><strong>沒 rollback path</strong>：cutover 後發現問題、回不去</li>
<li><strong>app 跟 DB 一起遷</strong>：兩個 risk source 疊加、追根因困難</li>
<li><strong>忽略合規 lead time</strong>：技術側 ready 但合規審查還在跑、整個 stuck</li>
<li><strong>忽略 ETL pipeline</strong>：production cutover 完、下游 BI / analytics 還在打 old DB</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a>（基本流程）/ <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>（schema 演進）</li>
<li>平行：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a>（dual-write、shadow）、<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/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>跨 vendor 實戰深入：<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>（document → multi-model）、<a href="/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/" data-link-title="從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook" data-link-desc="PostgreSQL / MySQL → Aurora 的 Type C operational redesign hybrid playbook、6 規格面（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、Standard Chartered 合規 lead time 模型、Netflix 非 all-purpose store 邊界">Aurora 從自管 PG / MySQL 遷入</a>、<a href="/blog/backend/01-database/vendors/spanner/migrate-from-cloud-sql-pg/" data-link-title="Migration Playbook：Cloud SQL for PostgreSQL → Cloud Spanner" data-link-desc="Cloud SQL → Spanner 是 paradigm shift 級遷移、不是 drop-in。本 playbook 走 6 規格面 Driver / Diff / Phase / Evidence / Cutover / Cleanup：Driver 段明示 sizing barrier（100 pu 起跳）跟 &lt; 50ms write latency 兩條 no-go；Diff 段加 sizing / cost 第 7 規格面；Phase 0 含 sizing audit；Evidence 段補 cost crossover 報告；對照 9.C10 Google internal dogfood 邊界跟 Standard Chartered 受監管 banking case">Spanner 從 Cloud SQL PG 遷入</a>、<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB 遷入 Atlas</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">Schema Migration</a></li>
<li><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a></li>
<li><a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">Dual Write</a></li>
<li><a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill</a></li>
<li><a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">Cutover Window</a></li>
<li><a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/fallback-read/" data-link-title="Fallback Read" data-link-desc="說明讀取路徑切換失敗時如何暫時回到舊資料語意或舊讀取來源">Fallback Read</a></li>
</ul>
]]></content:encoded></item><item><title>9.12 SLO 與 Performance Budget</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>SLO 與 performance budget 的責任是讓容量決策有「可衡量的目標 + 可審查的代價」。沒有 SLO 時、容量規劃容易變「越大越好」、沒邊界；有 SLO + budget 之後、所有決策都能回答「是否在 budget 內」、「超出 budget 該怎麼辦」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget&lt;/a> 的關係：06.6 處理「可靠性 SLO」（用 error budget 凍結 release）、9.12 處理「效能 SLO」（用 performance budget 約束容量）。兩者用同一套方法論、目標不同。讀者可以把本章當作 06.6 的 &lt;em>效能對應&lt;/em> 章節。&lt;/p>
&lt;p>本章覆蓋 SLI/SLO/SLA 分層、latency budget 分解、performance budget vs error budget、SLO 等級的成本含義、多 SLO 對齊、SLO drift 維護。讀完後讀者能設計一套完整的 SLO + budget 系統、把容量決策跟 SLO 對接。&lt;/p>
&lt;h2 id="sli--slo--sla-三層分清">SLI / SLO / SLA 三層分清&lt;/h2>
&lt;p>三個名詞常被混用、實際是三個不同層的概念。&lt;/p>
&lt;p>&lt;strong>SLI（Service Level Indicator）&lt;/strong>：客觀量測值。p99 latency、availability、throughput、error rate 都是 SLI。
&lt;strong>SLO（Service Level Objective）&lt;/strong>：團隊內部目標。「99.95% 用戶請求 &amp;lt; 500ms」這類具體承諾。
&lt;strong>SLA（Service Level Agreement）&lt;/strong>：對外合約承諾。達不到要退款、違約金、信用補償。&lt;/p>
&lt;p>&lt;strong>SLO 比 SLA 嚴 — 給內部 buffer&lt;/strong>。SLA 訂 99.9%、SLO 訂 99.95% — 萬一 SLO 沒達到、SLA 還沒違約、有反應時間。&lt;/p>
&lt;p>&lt;strong>容量規劃針對 SLO、不是 SLA&lt;/strong>：SLA 是「最低不能跌破」、SLO 才是「日常目標」。用 SLA 做容量規劃會經常 violate SLA、給用戶 / 客戶不好體驗。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO 卡片&lt;/a>。&lt;/p>
&lt;h2 id="latency-budget-分解">Latency budget 分解&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency budget&lt;/a> 是把 SLO 翻成可分解工程目標的關鍵工具。&lt;/p>
&lt;p>&lt;strong>從 end-to-end latency 開始&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>用戶感受到的 latency：DNS resolution + TLS handshake + CDN + load balancer + application + cache + DB + serialization + network back&lt;/li>
&lt;li>SLO 訂在 user-perceived：例如「p99 end-to-end &amp;lt; 500ms」&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>拆到每個 stage 的 budget&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>DNS：5ms（assume cached）&lt;/li>
&lt;li>TLS handshake：50ms（first request）&lt;/li>
&lt;li>CDN：20ms&lt;/li>
&lt;li>Load balancer：5ms&lt;/li>
&lt;li>Application：100ms&lt;/li>
&lt;li>Cache lookup：5ms（hit）/ 100ms（miss）&lt;/li>
&lt;li>DB query：30ms&lt;/li>
&lt;li>Serialization：10ms&lt;/li>
&lt;li>Network return：15ms&lt;/li>
&lt;li>&lt;strong>總和&lt;/strong>：240ms（cache hit）/ 335ms（miss）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>每個 stage 的 budget 必須 &lt;em>跟 SLO 對齊&lt;/em>&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>SLO 與 performance budget 的責任是讓容量決策有「可衡量的目標 + 可審查的代價」。沒有 SLO 時、容量規劃容易變「越大越好」、沒邊界；有 SLO + budget 之後、所有決策都能回答「是否在 budget 內」、「超出 budget 該怎麼辦」。</p>
<p>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget</a> 的關係：06.6 處理「可靠性 SLO」（用 error budget 凍結 release）、9.12 處理「效能 SLO」（用 performance budget 約束容量）。兩者用同一套方法論、目標不同。讀者可以把本章當作 06.6 的 <em>效能對應</em> 章節。</p>
<p>本章覆蓋 SLI/SLO/SLA 分層、latency budget 分解、performance budget vs error budget、SLO 等級的成本含義、多 SLO 對齊、SLO drift 維護。讀完後讀者能設計一套完整的 SLO + budget 系統、把容量決策跟 SLO 對接。</p>
<h2 id="sli--slo--sla-三層分清">SLI / SLO / SLA 三層分清</h2>
<p>三個名詞常被混用、實際是三個不同層的概念。</p>
<p><strong>SLI（Service Level Indicator）</strong>：客觀量測值。p99 latency、availability、throughput、error rate 都是 SLI。
<strong>SLO（Service Level Objective）</strong>：團隊內部目標。「99.95% 用戶請求 &lt; 500ms」這類具體承諾。
<strong>SLA（Service Level Agreement）</strong>：對外合約承諾。達不到要退款、違約金、信用補償。</p>
<p><strong>SLO 比 SLA 嚴 — 給內部 buffer</strong>。SLA 訂 99.9%、SLO 訂 99.95% — 萬一 SLO 沒達到、SLA 還沒違約、有反應時間。</p>
<p><strong>容量規劃針對 SLO、不是 SLA</strong>：SLA 是「最低不能跌破」、SLO 才是「日常目標」。用 SLA 做容量規劃會經常 violate SLA、給用戶 / 客戶不好體驗。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO 卡片</a>。</p>
<h2 id="latency-budget-分解">Latency budget 分解</h2>
<p><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency budget</a> 是把 SLO 翻成可分解工程目標的關鍵工具。</p>
<p><strong>從 end-to-end latency 開始</strong>：</p>
<ul>
<li>用戶感受到的 latency：DNS resolution + TLS handshake + CDN + load balancer + application + cache + DB + serialization + network back</li>
<li>SLO 訂在 user-perceived：例如「p99 end-to-end &lt; 500ms」</li>
</ul>
<p><strong>拆到每個 stage 的 budget</strong>：</p>
<ul>
<li>DNS：5ms（assume cached）</li>
<li>TLS handshake：50ms（first request）</li>
<li>CDN：20ms</li>
<li>Load balancer：5ms</li>
<li>Application：100ms</li>
<li>Cache lookup：5ms（hit）/ 100ms（miss）</li>
<li>DB query：30ms</li>
<li>Serialization：10ms</li>
<li>Network return：15ms</li>
<li><strong>總和</strong>：240ms（cache hit）/ 335ms（miss）</li>
</ul>
<p><strong>每個 stage 的 budget 必須 <em>跟 SLO 對齊</em></strong>：</p>
<ul>
<li>每個 stage 加總 = SLO 上限</li>
<li>任何 stage 超 budget → 該 stage 必須改善（不是其他 stage 來補）</li>
<li>每個 stage 必須有 <em>current measurement</em> — 不能訂了沒量</li>
</ul>
<p><strong>Cross-region call 自帶不可壓縮 latency</strong>：</p>
<ul>
<li>同 AZ：&lt; 1ms</li>
<li>跨 AZ：1-2ms</li>
<li>跨 region 同 continent：20-30ms</li>
<li>跨 continent：100-200ms</li>
<li>SLO 訂 50ms 但服務要跨 region 設計 → 不可能達成</li>
</ul>
<p><strong>任何新增 stage 都會吃 budget</strong>：middleware、sidecar、interceptor、API gateway 都會增加 latency。設計時要明確認知這層代價。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — sub-millisecond 反推所有架構選擇（Cluster Placement Group 壓網路、z1d 壓 CPU、RAFT 壓共識）；<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML inference 多 stage 各自分配 budget。</p>
<h2 id="performance-budget">Performance budget</h2>
<p><a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">Performance budget</a> 跟 error budget 是 <em>姊妹概念</em> — 用同一套方法論處理可靠性 vs 效能。</p>
<p><strong>Error budget</strong>（<a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6</a>）：</p>
<ul>
<li>每月有允許的 unavailability 額度</li>
<li>例如 SLO 99.95% → error budget = 0.05% × 30 days = 21.6 分鐘 / 月</li>
<li>額度用完 → freeze new release、focus on reliability</li>
</ul>
<p><strong>Performance budget</strong>（本章）：</p>
<ul>
<li>每月有允許的 latency 退化額度</li>
<li>例如「p99 允許比 baseline 高 10ms 連續 X 分鐘」、用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alert</li>
<li>額度用完 → freeze new feature release、focus on perf</li>
</ul>
<p><strong>兩個 budget 並列、不衝突</strong>：</p>
<ul>
<li>一個燒一個健康 → 部分 freeze（freeze 對應的那條）</li>
<li>兩個都健康 → 全速 release</li>
<li>兩個都燒 → 全面 freeze、deep review</li>
</ul>
<p><strong>Burn rate alert 比 threshold alert 好</strong>：</p>
<ul>
<li>threshold：p99 &gt; 500ms 就 alert → false positive 多</li>
<li>burn rate：過去 1 小時 budget burn rate &gt; 14.4x 就 alert（Google SRE 推薦）→ 對應「再這樣下去 budget 5 分鐘內燒光」</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase 延遲就是收入</a> — 沒 performance budget 等於沒 release control；<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel 多 SLO</a> — 直播 vs 投注不同 budget。</p>
<h2 id="slo-等級的成本含義">SLO 等級的成本含義</h2>
<p>不同 SLO 等級對應不同容量成本、選 SLO 就是選成本。</p>
<table>
  <thead>
      <tr>
          <th>SLO</th>
          <th>年 downtime 上限</th>
          <th>工程含義</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>99%</td>
          <td>年 87.6 小時</td>
          <td>單 AZ 部署可接受</td>
          <td>B2C 內部工具、非 critical SaaS</td>
      </tr>
      <tr>
          <td>99.9%</td>
          <td>年 8.76 小時</td>
          <td>多 AZ、reactive failover</td>
          <td>B2C consumer-facing</td>
      </tr>
      <tr>
          <td>99.95%</td>
          <td>年 4.38 小時</td>
          <td>多 AZ active-active、autoscale 必要</td>
          <td>B2B SaaS minimum</td>
      </tr>
      <tr>
          <td>99.99%</td>
          <td>年 52.6 分鐘</td>
          <td>多 region active-active、無人工介入</td>
          <td>mission-critical SaaS</td>
      </tr>
      <tr>
          <td>99.999%</td>
          <td>年 5.26 分鐘</td>
          <td>全球多 region、即時 failover、人工極少</td>
          <td>金融 / 醫療 / 電信</td>
      </tr>
  </tbody>
</table>
<p><strong>每多一個 9、容量成本指數成長</strong>：</p>
<ul>
<li>99 → 99.9：成本 +30-50%</li>
<li>99.9 → 99.99：成本 +50-100%</li>
<li>99.99 → 99.999：成本 +200-500%</li>
</ul>
<p><strong>選 SLO 不是 marketing 決策、是工程經濟決策</strong>：選太高、燒錢；選太低、用戶不滿。要算 <em>每個 9 對應的業務價值</em>、是否值得對應的容量投資。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 99.999%</a> — 廣告計費 1 分鐘斷線損失幾百萬美金、5 個 9 是真實營收邊界；<a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 99.999%</a> — B2B 客服 SaaS、客戶停線 = 客戶失去用戶信任、5 個 9 是合約義務。</p>
<h2 id="多-slo-對齊">多 SLO 對齊</h2>
<p>同一系統不同工作負載可以有不同 SLO、按業務重要性分級。</p>
<p><strong>設計原則</strong>：</p>
<ul>
<li>按「業務重要性 × 用戶感知」分級</li>
<li>同一個 endpoint 不同情境可能有不同 SLO（例如登入 vs 結帳）</li>
<li>多 SLO 必須有 <em>優先順序</em>、衝突時知道犧牲哪個</li>
</ul>
<p><strong>範例</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Endpoint</th>
          <th>SLO</th>
          <th>業務影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>登入</td>
          <td>p99 200ms</td>
          <td>用戶 onboarding</td>
      </tr>
      <tr>
          <td>瀏覽商品</td>
          <td>p99 500ms</td>
          <td>用戶 retention</td>
      </tr>
      <tr>
          <td>結帳</td>
          <td>p99 300ms</td>
          <td>直接影響收入</td>
      </tr>
      <tr>
          <td>推薦</td>
          <td>p99 1000ms</td>
          <td>影響 conversion 但非阻斷</td>
      </tr>
  </tbody>
</table>
<p><strong>衝突處理</strong>：當 capacity 不夠時、優先保 <em>結帳</em> 而非 <em>推薦</em>、即使技術上推薦比較好擴容。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel</a> 直播秒級 SLO vs 投注毫秒級 SLO、同一個 user 同一場 NFL Super Bowl、兩個服務必須分開部署、各自 SLO。</p>
<h2 id="slo-演進baseline-drift">SLO 演進：baseline drift</h2>
<p><a href="/blog/backend/knowledge-cards/slo-baseline-drift/" data-link-title="SLO Baseline Drift" data-link-desc="SLO baseline 因業務變化 / surge / 架構改動而需要重新校準的現象">SLO 不是訂了就不動</a> — 業務變化要重新校準。</p>
<p><strong>SLO drift 來源</strong>：</p>
<ul>
<li>Structural surge：COVID 類外部衝擊讓 baseline 永久上移</li>
<li>Product change：新 feature 改變用戶 journey</li>
<li>Architectural improvement：DB 換型、cache 加強、CDN 擴點</li>
<li>User behavior：mobile share 上升、跨 region 比例變化</li>
</ul>
<p><strong>Drift 不是 anomaly、是 <em>新常態</em></strong>。</p>
<p><strong>Review 節奏</strong>：</p>
<ul>
<li>每季 review SLO：拉過去 90 天 SLI 分布、看是否需要調整</li>
<li>重大產品改動立即 review</li>
<li>Drift 確認後要更新：alert threshold、autoscaler trigger、performance budget 額度、容量規劃 baseline</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID</a> — 30 倍成長後 baseline 永久上移、SLO threshold 跟著重新校準、不能套用 COVID 前的標準。</p>
<h2 id="slo-跟容量規劃對接">SLO 跟容量規劃對接</h2>
<p>回到本章開頭的論點 — SLO 是容量決策的目標。</p>
<p><strong>容量公式</strong>：能撐多少 RPS @ SLO 條件。
<strong>規劃時用「SLO-constrained capacity」、不是「max capacity」</strong>：</p>
<ul>
<li>max capacity：絕對極限、進 cliff</li>
<li>SLO-constrained capacity：知道在 SLO 條件下能撐多少</li>
<li>兩者差 30-50%（headroom）</li>
</ul>
<p><strong>9.4 saturation 找 knee 是技術指標、9.6 容量規劃用 SLO-constrained knee</strong>：</p>
<ul>
<li>saturation 在 utilization 80% 時開始</li>
<li>但 SLO 可能要求 utilization 60% 以下</li>
<li>容量規劃用 60% 而非 80%</li>
</ul>
<p><strong>跟 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本工程</a> 對接</strong>：</p>
<ul>
<li>每多一個 9 多花多少錢</li>
<li>業務需要這個 9 嗎</li>
<li>不需要的話降 SLO 省成本</li>
</ul>
<h2 id="slo-跟-performance-budget-一起用">SLO 跟 performance budget 一起用</h2>
<p>最後的整合 — error budget + performance budget 一起治理 release 節奏。</p>
<p><strong>Error budget 控制 <em>變更節奏</em></strong>：</p>
<ul>
<li>error budget 健康 → release 可以快</li>
<li>error budget 燒光 → freeze release</li>
</ul>
<p><strong>Performance budget 控制 <em>容量決策</em></strong>：</p>
<ul>
<li>performance budget 健康 → 新 feature 可以引入 perf cost</li>
<li>performance budget 燒光 → freeze new feature</li>
</ul>
<p><strong>兩個 budget 並列</strong>：</p>
<ul>
<li>都健康 → 全速 release + 新 feature</li>
<li>error 健康 + perf 燒 → release 但只接 perf-neutral 變更</li>
<li>error 燒 + perf 健康 → 暫停 release、修可靠性</li>
<li>都燒 → 全面 freeze、deep review</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO</a> 跟 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>latency budget 反推架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 / C24 99.999%</a></td>
          <td>5 個 9 的容量代價</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML stage budget</a></td>
          <td>p99 多 stage 分配</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 多 SLO</a></td>
          <td>直播 vs 投注不同 SLO 並存</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>SLO baseline 重新校準</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a>（latency budget 反推）</li>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（SLO-constrained capacity）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget 政策</a>（可靠性 SLO）</li>
<li>跨模組：<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a>（量測層）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">Performance Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/slo-baseline-drift/" data-link-title="SLO Baseline Drift" data-link-desc="SLO baseline 因業務變化 / surge / 架構改動而需要重新校準的現象">SLO Baseline Drift</a></li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/</guid><description>&lt;p>這個案例的核心責任是說明「K8s 多 cluster 治理」對容量規劃的影響。Riot Games 經營 League of Legends、VALORANT、TFT 等多款全球遊戲、單一遊戲跨多地區、需要 &amp;lt; 35ms 延遲、需要做到「快速部署新遊戲 / 新區域」— 這套需求把容量規劃的單位從「instance」改成「cluster」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Riot Games 遷移到 EKS 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/riot-games-case-study/">Riot Games case study&lt;/a>）：&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>月活用戶&lt;/td>
 &lt;td>1.8 億 +&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cluster 數量&lt;/td>
 &lt;td>246 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>基礎設施年省&lt;/td>
 &lt;td>1000 萬美金&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署速度提升&lt;/td>
 &lt;td>12x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>基礎設施設定速度&lt;/td>
 &lt;td>+90%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲門檻&lt;/td>
 &lt;td>35ms（VALORANT 等競技遊戲）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>標準化覆蓋率&lt;/td>
 &lt;td>80% 基礎設施移到中央管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開發者基礎設施工作下降&lt;/td>
 &lt;td>-40%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件回應時間下降&lt;/td>
 &lt;td>-50%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Amazon EKS（主要）、AWS Local Zones（低延遲就近部署）、AWS Outposts（on-prem edge）、Karpenter（node lifecycle）、Terraform（IaC）。&lt;/p>
&lt;p>關鍵架構決策：從 multi-tenant cluster 模型改成 &lt;em>single-tenant per game&lt;/em> — 每個遊戲一個獨立 cluster、避免跨遊戲互相影響。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Riot Games 案例揭露三個多 cluster K8s 容量治理重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cluster 隔離是容量規劃的單位&lt;/strong>：246 個 cluster 看似很多、但 &lt;em>每個 cluster 是獨立容量單位&lt;/em>、不互相影響。一個遊戲的擴容不會吃掉另一個遊戲的容量。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 multi-tenant vs single-tenant 取捨。&lt;/li>
&lt;li>&lt;strong>延遲門檻反推 region 部署&lt;/strong>：35ms 是競技遊戲（VALORANT、League）的可接受上限、超過會「卡」。從這個門檻反推：玩家所在 region 不能跨洲、需要區域 cluster。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget。Local Zones / Outposts 是這個門檻的工程回應。&lt;/li>
&lt;li>&lt;strong>Karpenter + Terraform = cluster 容量自動化&lt;/strong>：246 個 cluster 手動管理會崩。Karpenter（node 動態 lifecycle）+ Terraform（IaC）讓 cluster 級操作可重複、可審查。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9 Performance Improvement Loop&lt;/a> 的自動化迴圈。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「年省 1000 萬」是 &lt;em>vs 自管 Mesos&lt;/em>、不是 &lt;em>vs 沒上雲&lt;/em>。EKS 仍有 vendor cost、只是比自管便宜。讀案例時要看 baseline 是什麼。另外、單一 cluster 的容量上限（pod 數、node 數）仍是工程現實、超過時要做 cluster sharding（這正是 Riot 走 246 個 cluster 的部分原因）。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>single-tenant cluster per workload&lt;/strong>：每個高敏感度工作負載（每個遊戲、每個關鍵服務）一個獨立 cluster、避免 noisy neighbor。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a>。&lt;/li>
&lt;li>&lt;strong>延遲門檻反推 region 部署數量&lt;/strong>：先訂 latency budget、再算 &lt;em>玩家分布 × region cluster 數量&lt;/em>。region 增加會線性增加 ops 成本、要在 latency 跟 cost 之間找平衡。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;li>&lt;strong>cluster 級 IaC + 自動化是 multi-cluster 治理前置&lt;/strong>：Terraform / Pulumi / Crossplane + Karpenter / Cluster Autoscaler 是基本工具。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP GKE Fleet management（multi-cluster）、Azure Fleet Manager、自建 Cluster API + ArgoCD 都可以做 multi-cluster 治理。差異是 vendor 整合度跟政策。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「K8s 多 cluster 治理」對容量規劃的影響。Riot Games 經營 League of Legends、VALORANT、TFT 等多款全球遊戲、單一遊戲跨多地區、需要 &lt; 35ms 延遲、需要做到「快速部署新遊戲 / 新區域」— 這套需求把容量規劃的單位從「instance」改成「cluster」。</p>
<h2 id="觀察">觀察</h2>
<p>Riot Games 遷移到 EKS 的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/riot-games-case-study/">Riot Games case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月活用戶</td>
          <td>1.8 億 +</td>
      </tr>
      <tr>
          <td>Cluster 數量</td>
          <td>246 個</td>
      </tr>
      <tr>
          <td>基礎設施年省</td>
          <td>1000 萬美金</td>
      </tr>
      <tr>
          <td>部署速度提升</td>
          <td>12x</td>
      </tr>
      <tr>
          <td>基礎設施設定速度</td>
          <td>+90%</td>
      </tr>
      <tr>
          <td>延遲門檻</td>
          <td>35ms（VALORANT 等競技遊戲）</td>
      </tr>
      <tr>
          <td>標準化覆蓋率</td>
          <td>80% 基礎設施移到中央管理</td>
      </tr>
      <tr>
          <td>開發者基礎設施工作下降</td>
          <td>-40%</td>
      </tr>
      <tr>
          <td>事件回應時間下降</td>
          <td>-50%</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Amazon EKS（主要）、AWS Local Zones（低延遲就近部署）、AWS Outposts（on-prem edge）、Karpenter（node lifecycle）、Terraform（IaC）。</p>
<p>關鍵架構決策：從 multi-tenant cluster 模型改成 <em>single-tenant per game</em> — 每個遊戲一個獨立 cluster、避免跨遊戲互相影響。</p>
<h2 id="判讀">判讀</h2>
<p>Riot Games 案例揭露三個多 cluster K8s 容量治理重點。</p>
<ol>
<li><strong>Cluster 隔離是容量規劃的單位</strong>：246 個 cluster 看似很多、但 <em>每個 cluster 是獨立容量單位</em>、不互相影響。一個遊戲的擴容不會吃掉另一個遊戲的容量。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 multi-tenant vs single-tenant 取捨。</li>
<li><strong>延遲門檻反推 region 部署</strong>：35ms 是競技遊戲（VALORANT、League）的可接受上限、超過會「卡」。從這個門檻反推：玩家所在 region 不能跨洲、需要區域 cluster。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency budget。Local Zones / Outposts 是這個門檻的工程回應。</li>
<li><strong>Karpenter + Terraform = cluster 容量自動化</strong>：246 個 cluster 手動管理會崩。Karpenter（node 動態 lifecycle）+ Terraform（IaC）讓 cluster 級操作可重複、可審查。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.9 Performance Improvement Loop</a> 的自動化迴圈。</li>
</ol>
<p>需要警惕：「年省 1000 萬」是 <em>vs 自管 Mesos</em>、不是 <em>vs 沒上雲</em>。EKS 仍有 vendor cost、只是比自管便宜。讀案例時要看 baseline 是什麼。另外、單一 cluster 的容量上限（pod 數、node 數）仍是工程現實、超過時要做 cluster sharding（這正是 Riot 走 246 個 cluster 的部分原因）。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>single-tenant cluster per workload</strong>：每個高敏感度工作負載（每個遊戲、每個關鍵服務）一個獨立 cluster、避免 noisy neighbor。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a>。</li>
<li><strong>延遲門檻反推 region 部署數量</strong>：先訂 latency budget、再算 <em>玩家分布 × region cluster 數量</em>。region 增加會線性增加 ops 成本、要在 latency 跟 cost 之間找平衡。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
<li><strong>cluster 級 IaC + 自動化是 multi-cluster 治理前置</strong>：Terraform / Pulumi / Crossplane + Karpenter / Cluster Autoscaler 是基本工具。</li>
</ol>
<p>跨平台等效：GCP GKE Fleet management（multi-cluster）、Azure Fleet Manager、自建 Cluster API + ArgoCD 都可以做 multi-cluster 治理。差異是 vendor 整合度跟政策。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 multi-cluster K8s → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想做延遲門檻反推部署 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></li>
<li>想對照微服務 vs multi-cluster → <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/riot-games-case-study/">Riot Games Cuts $10M Annual Infrastructure Costs by Migrating to Amazon EKS</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/riot-games-reinvent/">Riot Games on Using AWS to Improve Gaming</a></li>
</ul>
]]></content:encoded></item><item><title>Google：Postmortem Action Item Closure 治理</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/</guid><description>&lt;p>Postmortem 的核心責任是把事故轉成會被完成的工程改進，解釋事故只是第一步。Google 的做法重點在 action item closure：每個改進項都要有 owner、完成條件、追蹤節奏與逾期處理規則。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>很多團隊 postmortem 寫得完整，但事故仍反覆發生。根因通常是 action item 沒有被制度化追蹤，分析能力本身不是瓶頸。當改進工作和日常 feature 競爭同一批資源時，沒有 closure 機制的 action item 很容易被延後到失效。&lt;/p>
&lt;h2 id="治理機制">治理機制&lt;/h2>
&lt;p>可靠的 closure 機制要先把 action item 分級，再對應不同完成標準。&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>P0&lt;/td>
 &lt;td>重複事故高機率再發生&lt;/td>
 &lt;td>需在下個 release 週期前完成並驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>P1&lt;/td>
 &lt;td>會放大事故影響面&lt;/td>
 &lt;td>要有落地日期與 gate 條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>P2&lt;/td>
 &lt;td>提升診斷或操作效率&lt;/td>
 &lt;td>可排入 backlog，但要保留追蹤節點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分級之後要做三件事：&lt;/p>
&lt;ol>
&lt;li>為每個 action item 指派單一 owner。&lt;/li>
&lt;li>寫出可驗證完成條件（不是「優化」「強化」這類抽象字）。&lt;/li>
&lt;li>把 closure 狀態納入固定 review cadence。&lt;/li>
&lt;/ol>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>overdue action-item ratio&lt;/td>
 &lt;td>是否長期積壓高風險改進&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>repeated-incident similarity&lt;/td>
 &lt;td>同型事故是否仍反覆發生&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">8.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>gate bypass count&lt;/td>
 &lt;td>是否在高風險情況下跳過治理閘門&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>verification evidence coverage&lt;/td>
 &lt;td>完成項是否附驗證證據&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>最常見陷阱是把 action item 當作「會後待辦」而不是 release policy 的一部分。這會讓高風險改進沒有實際約束力。正確做法是把 P0/P1 項目直接綁到 release gate，未完成時不得放行關聯變更。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先在 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a> 保留 action item 的決策脈絡，再到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a> 回寫觀測與驗證項目。若要把 closure 變成制度，回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog&lt;/a> 進行排序治理。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sre.google/sre-book/table-of-contents/">Google SRE Book&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://sre.google/workbook/table-of-contents/">Google SRE Workbook&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Postmortem 的核心責任是把事故轉成會被完成的工程改進，解釋事故只是第一步。Google 的做法重點在 action item closure：每個改進項都要有 owner、完成條件、追蹤節奏與逾期處理規則。</p>
<h2 id="問題場景">問題場景</h2>
<p>很多團隊 postmortem 寫得完整，但事故仍反覆發生。根因通常是 action item 沒有被制度化追蹤，分析能力本身不是瓶頸。當改進工作和日常 feature 競爭同一批資源時，沒有 closure 機制的 action item 很容易被延後到失效。</p>
<h2 id="治理機制">治理機制</h2>
<p>可靠的 closure 機制要先把 action item 分級，再對應不同完成標準。</p>
<table>
  <thead>
      <tr>
          <th>分級</th>
          <th>風險型態</th>
          <th>最低完成標準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>P0</td>
          <td>重複事故高機率再發生</td>
          <td>需在下個 release 週期前完成並驗證</td>
      </tr>
      <tr>
          <td>P1</td>
          <td>會放大事故影響面</td>
          <td>要有落地日期與 gate 條件</td>
      </tr>
      <tr>
          <td>P2</td>
          <td>提升診斷或操作效率</td>
          <td>可排入 backlog，但要保留追蹤節點</td>
      </tr>
  </tbody>
</table>
<p>分級之後要做三件事：</p>
<ol>
<li>為每個 action item 指派單一 owner。</li>
<li>寫出可驗證完成條件（不是「優化」「強化」這類抽象字）。</li>
<li>把 closure 狀態納入固定 review cadence。</li>
</ol>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>overdue action-item ratio</td>
          <td>是否長期積壓高風險改進</td>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5</a></td>
      </tr>
      <tr>
          <td>repeated-incident similarity</td>
          <td>同型事故是否仍反覆發生</td>
          <td><a href="/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">8.13</a></td>
      </tr>
      <tr>
          <td>gate bypass count</td>
          <td>是否在高風險情況下跳過治理閘門</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>verification evidence coverage</td>
          <td>完成項是否附驗證證據</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>最常見陷阱是把 action item 當作「會後待辦」而不是 release policy 的一部分。這會讓高風險改進沒有實際約束力。正確做法是把 P0/P1 項目直接綁到 release gate，未完成時不得放行關聯變更。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a> 保留 action item 的決策脈絡，再到 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a> 回寫觀測與驗證項目。若要把 closure 變成制度，回到 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a> 進行排序治理。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sre.google/sre-book/table-of-contents/">Google SRE Book</a></li>
<li><a href="https://sre.google/workbook/table-of-contents/">Google SRE Workbook</a></li>
</ul>
]]></content:encoded></item><item><title>0.12 觀測、可靠性與事故服務選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/</guid><description>&lt;p>觀測、可靠性與事故服務選型的核心責任是把操作風險拆成「看得見、驗得過、接得住」三層能力。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>處理訊號是否足以支援判讀，&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程&lt;/a>處理失敗是否能被安全預演，&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>處理事故是否能被接住、分工與回寫。&lt;/p>
&lt;p>這三類服務常被一起採購或一起導入，但它們回答不同問題。觀測平台回答「現在發生什麼」，可靠性工具回答「失敗前能否先驗證」，事故平台回答「事情發生後誰做什麼」。選型時先分清能力層，再比較 vendor、SaaS、OSS 或自建方案，能降低工具堆疊與流程空轉的風險。&lt;/p>
&lt;h2 id="選型錨點">選型錨點&lt;/h2>
&lt;p>選型錨點是先問服務要降低哪一種操作不確定性。當團隊只知道系統「好像怪怪的」，優先補訊號；當團隊知道風險但缺少安全驗證路徑，優先補可靠性驗證；當團隊知道事故已發生但協作混亂，優先補事故流程。&lt;/p>
&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>發生什麼、影響哪裡&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>&lt;/td>
 &lt;td>telemetry、APM、log、dashboard&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗證層&lt;/td>
 &lt;td>風險能否提前預演&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程&lt;/a>&lt;/td>
 &lt;td>CI、load test、chaos、SLO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>響應層&lt;/td>
 &lt;td>誰接手、如何收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>&lt;/td>
 &lt;td>on-call、IR、status、postmortem&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>閉環層&lt;/td>
 &lt;td>教訓如何回寫&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">觀測、驗證與事故閉環&lt;/a>&lt;/td>
 &lt;td>workflow、action tracking&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>訊號層的責任是讓系統行為可被查詢與判讀。這一層的選型重點是資料模型、查詢能力、關聯能力、保留成本與告警品質；產品名稱排在後面，因為 log、metric、trace 與 error event 是否能互相串接，才是事故時真正影響判讀速度的條件。&lt;/p>
&lt;p>驗證層的責任是讓風險在事故前被安全暴露。這一層的選型重點是測試是否接近真實 workload、故障注入是否有停止條件、SLO 是否能被量測、release gate 是否能阻止高風險變更；工具越強，越需要 blast radius 與權限邊界。&lt;/p>
&lt;p>響應層的責任是讓事故進入可交接流程。這一層的選型重點是 paging、升級、角色分工、狀態更新、decision log、stakeholder mapping 與 post-incident action tracking；工具的價值來自流程一致性，通知訊息數量只是輔助訊號。&lt;/p>
&lt;p>閉環層的責任是把事故與演練教訓回寫到系統設計。這一層可能由 incident platform、ticket system、runbook repository 或內部 workflow 承擔；判準是 action item 是否能被排序、驗證、關閉，並回到訊號治理、可靠性演練或事故流程。&lt;/p>
&lt;h2 id="判讀順序">判讀順序&lt;/h2>
&lt;p>操作服務選型的穩定順序是「症狀 → 缺口 → 能力 → 工具」。症狀描述使用者痛點或工程痛點，缺口描述目前缺少的判讀或流程，能力描述需要補的系統責任，工具才是最後的落地選項。&lt;/p>
&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>訊號覆蓋不足&lt;/td>
 &lt;td>symptom-based alert&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">dashboard 與 alert&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故時 trace 接不上 queue&lt;/td>
 &lt;td>關聯線索斷裂&lt;/td>
 &lt;td>context propagation&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">tracing 與 context link&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發版後才發現容量曲線崩壞&lt;/td>
 &lt;td>失敗前驗證不足&lt;/td>
 &lt;td>load / perf gate&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">load test&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>chaos 實驗影響超出預期&lt;/td>
 &lt;td>實驗安全邊界不足&lt;/td>
 &lt;td>experiment guardrail&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">experiment safety boundary&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多人同時修事故但決策互相覆蓋&lt;/td>
 &lt;td>指揮與紀錄不足&lt;/td>
 &lt;td>command / decision log&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外狀態更新慢於內部復原&lt;/td>
 &lt;td>stakeholder 節奏不足&lt;/td>
 &lt;td>status / comms&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">stakeholder comms&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>客訴比告警早代表系統的外部痛點先於內部訊號出現。這種情境應先補服務健康指標、使用者可感知訊號與 alert runbook，再討論要用哪個監控平台；否則平台上線後仍可能只收集到工程師方便看的資料。&lt;/p></description><content:encoded><![CDATA[<p>觀測、可靠性與事故服務選型的核心責任是把操作風險拆成「看得見、驗得過、接得住」三層能力。<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>處理訊號是否足以支援判讀，<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>處理失敗是否能被安全預演，<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>處理事故是否能被接住、分工與回寫。</p>
<p>這三類服務常被一起採購或一起導入，但它們回答不同問題。觀測平台回答「現在發生什麼」，可靠性工具回答「失敗前能否先驗證」，事故平台回答「事情發生後誰做什麼」。選型時先分清能力層，再比較 vendor、SaaS、OSS 或自建方案，能降低工具堆疊與流程空轉的風險。</p>
<h2 id="選型錨點">選型錨點</h2>
<p>選型錨點是先問服務要降低哪一種操作不確定性。當團隊只知道系統「好像怪怪的」，優先補訊號；當團隊知道風險但缺少安全驗證路徑，優先補可靠性驗證；當團隊知道事故已發生但協作混亂，優先補事故流程。</p>
<table>
  <thead>
      <tr>
          <th>能力層</th>
          <th>核心問題</th>
          <th>對應模組</th>
          <th>常見服務類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號層</td>
          <td>發生什麼、影響哪裡</td>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></td>
          <td>telemetry、APM、log、dashboard</td>
      </tr>
      <tr>
          <td>驗證層</td>
          <td>風險能否提前預演</td>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
          <td>CI、load test、chaos、SLO</td>
      </tr>
      <tr>
          <td>響應層</td>
          <td>誰接手、如何收斂</td>
          <td><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
          <td>on-call、IR、status、postmortem</td>
      </tr>
      <tr>
          <td>閉環層</td>
          <td>教訓如何回寫</td>
          <td><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">觀測、驗證與事故閉環</a></td>
          <td>workflow、action tracking</td>
      </tr>
  </tbody>
</table>
<p>訊號層的責任是讓系統行為可被查詢與判讀。這一層的選型重點是資料模型、查詢能力、關聯能力、保留成本與告警品質；產品名稱排在後面，因為 log、metric、trace 與 error event 是否能互相串接，才是事故時真正影響判讀速度的條件。</p>
<p>驗證層的責任是讓風險在事故前被安全暴露。這一層的選型重點是測試是否接近真實 workload、故障注入是否有停止條件、SLO 是否能被量測、release gate 是否能阻止高風險變更；工具越強，越需要 blast radius 與權限邊界。</p>
<p>響應層的責任是讓事故進入可交接流程。這一層的選型重點是 paging、升級、角色分工、狀態更新、decision log、stakeholder mapping 與 post-incident action tracking；工具的價值來自流程一致性，通知訊息數量只是輔助訊號。</p>
<p>閉環層的責任是把事故與演練教訓回寫到系統設計。這一層可能由 incident platform、ticket system、runbook repository 或內部 workflow 承擔；判準是 action item 是否能被排序、驗證、關閉，並回到訊號治理、可靠性演練或事故流程。</p>
<h2 id="判讀順序">判讀順序</h2>
<p>操作服務選型的穩定順序是「症狀 → 缺口 → 能力 → 工具」。症狀描述使用者痛點或工程痛點，缺口描述目前缺少的判讀或流程，能力描述需要補的系統責任，工具才是最後的落地選項。</p>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>主要缺口</th>
          <th>優先能力</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客訴比告警早</td>
          <td>訊號覆蓋不足</td>
          <td>symptom-based alert</td>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">dashboard 與 alert</a></td>
      </tr>
      <tr>
          <td>事故時 trace 接不上 queue</td>
          <td>關聯線索斷裂</td>
          <td>context propagation</td>
          <td><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">tracing 與 context link</a></td>
      </tr>
      <tr>
          <td>發版後才發現容量曲線崩壞</td>
          <td>失敗前驗證不足</td>
          <td>load / perf gate</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">load test</a></td>
      </tr>
      <tr>
          <td>chaos 實驗影響超出預期</td>
          <td>實驗安全邊界不足</td>
          <td>experiment guardrail</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">experiment safety boundary</a></td>
      </tr>
      <tr>
          <td>多人同時修事故但決策互相覆蓋</td>
          <td>指揮與紀錄不足</td>
          <td>command / decision log</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a></td>
      </tr>
      <tr>
          <td>對外狀態更新慢於內部復原</td>
          <td>stakeholder 節奏不足</td>
          <td>status / comms</td>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">stakeholder comms</a></td>
      </tr>
  </tbody>
</table>
<p>客訴比告警早代表系統的外部痛點先於內部訊號出現。這種情境應先補服務健康指標、使用者可感知訊號與 alert runbook，再討論要用哪個監控平台；否則平台上線後仍可能只收集到工程師方便看的資料。</p>
<p>trace 接不上 queue 代表跨邊界關聯失效。這種情境應先檢查 trace context、correlation id、message metadata 與 sampling 策略，再選擇 OpenTelemetry backend、APM SaaS 或 log search 方案。</p>
<p>發版後才發現容量曲線崩壞代表驗證層缺少 gate。這種情境應先建立 workload model、baseline、回歸門檻與 release gate，再選 load test 工具或 performance dashboard。</p>
<p>chaos 實驗影響超出預期代表驗證工具先於安全邊界。這種情境應先定義 steady state、blast radius、停止條件與授權範圍，再決定使用 chaos mesh、fault proxy 或商業 chaos 平台。</p>
<p>多人同時修事故但決策互相覆蓋代表響應層缺少 command model。這種情境應先定義 incident commander、scribe、owner、decision log 與 handoff，再導入 IR 平台或 chat workflow。</p>
<p>對外狀態更新慢於內部復原代表 stakeholder 節奏不足。這種情境應先定義影響評估、更新頻率、外部狀態頁與客戶溝通責任，再選 status page 或 customer comms 工具。</p>
<h2 id="服務組合策略">服務組合策略</h2>
<p>服務組合策略的核心原則是先選最小閉環，再擴展平台覆蓋。完整閉環至少包含一個可判讀訊號、一個可驗證門檻、一個可接手流程與一個可回寫的 action tracking；缺任一層時，工具組合就會變成單點能力。</p>
<table>
  <thead>
      <tr>
          <th>組合型態</th>
          <th>適合情境</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>雲端原生整合</td>
          <td>團隊集中在單一 cloud provider</td>
          <td>跨雲、跨 SaaS 與高階查詢受限</td>
      </tr>
      <tr>
          <td>OSS 可組裝平台</td>
          <td>團隊有平台工程能力</td>
          <td>維護、升級、容量與成本治理重</td>
      </tr>
      <tr>
          <td>All-in-one SaaS</td>
          <td>團隊需要快速覆蓋與低維運</td>
          <td>成本、資料鎖定與自訂邊界受限</td>
      </tr>
      <tr>
          <td>混合式最小閉環</td>
          <td>既有工具已分散</td>
          <td>整合責任與 ownership 容易模糊</td>
      </tr>
  </tbody>
</table>
<p>雲端原生整合適合雲端邊界清楚的團隊。它能快速取得 infrastructure 訊號、IAM 整合與預設 dashboard，但跨外部 SaaS、跨語言 trace 或高基數探索時，需要提前確認資料出口與查詢能力。</p>
<p>OSS 可組裝平台適合有平台團隊維護 ingestion、storage、query 與 dashboard 的組織。它能降低 vendor lock-in 並保留彈性，但容量規劃、升級、安全修補、保留策略與 on-call 都會變成內部成本。</p>
<p>All-in-one SaaS 適合需要快速建立可觀測、告警與事故協作的團隊。它能把 log、metric、trace、APM、paging 或 workflow 整合在單一產品，但成本模型、資料保留、客製化限制與資料治理要在導入前確認。</p>
<p>混合式最小閉環適合已經有多套工具的團隊。它的重點是定義哪個系統是 alert source、哪個系統是 incident source of truth、哪個系統負責 action item closure；整合邊界比新增工具更重要。</p>
<h2 id="導入順序">導入順序</h2>
<p>導入順序的責任是降低一次導入多套工具的失敗風險。觀測、驗證與事故服務應依照事故風險與團隊成熟度逐層補齊，功能清單只適合放在能力判準之後。</p>
<ol>
<li>先補最小訊號：定義 SLI、error rate、latency、dependency failure、queue lag 與 customer-facing symptom。</li>
<li>再補最小告警與 runbook：讓 alert 指向可執行動作，避免只把噪音送到 on-call。</li>
<li>接著補驗證門檻：把 load、contract、migration、chaos 或 SLO 變成 release 前後的 gate。</li>
<li>然後補事故協作：定義 paging、severity、角色、decision log、status update 與 post-incident review。</li>
<li>最後補閉環治理：把偵測缺口、演練缺口與 action item 回寫到觀測、驗證與事故流程。</li>
</ol>
<p>這個順序讓工具投資跟風險暴露同步。若團隊在沒有基本訊號時先導入 incident workflow，事故流程會缺少證據；若在沒有實驗安全邊界時先導入 chaos 工具，驗證本身會變成風險來源；若在沒有 action tracking 時只做 postmortem，復盤會停在文字紀錄。</p>
<h2 id="交接路由">交接路由</h2>
<p>交接路由的責任是把服務選型判斷送到正確模組。選型章只決定「需要哪一類能力」，後續模組負責欄位、流程、工具與實作細節。</p>
<ul>
<li>需要判斷訊號是否足以支援診斷時，進入 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</li>
<li>需要判斷失敗是否能被安全驗證時，進入 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>。</li>
<li>需要判斷事故是否能被接住與回寫時，進入 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>。</li>
<li>需要比較具體 vendor 時，先讀各模組的 vendors index，再回到本章確認工具是否補到正確能力層。</li>
</ul>
<h2 id="完成判準">完成判準</h2>
<p>本章完成的判準是能把工具需求翻成能力需求。當團隊能說清楚「我們缺的是訊號、驗證、響應還是閉環」，選型討論才適合進入 vendor 比較。</p>
<p>檢查時可以問四個問題：</p>
<ol>
<li>現在的痛點是看不見、驗不過、接不住，還是回寫斷掉？</li>
<li>這個工具補的是哪一層能力，會產生哪些新操作成本？</li>
<li>導入後誰負責維護資料品質、流程品質與 action closure？</li>
<li>如果三個月後事故型態改變，哪個 tripwire 會提醒團隊重新評估？</li>
</ol>
]]></content:encoded></item><item><title>4.12 Audit Log 邊界與 PII 治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a> 跟 operational log 的本質差異：對象、不變性、保留、法規&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a> 該記什麼：who / what / when / where / outcome、不可被應用層改寫&lt;/li>
&lt;li>不變性保證：append-only storage、tamper-evident hash chain、independent retention&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII&lt;/a> 治理：log 中的 PII 偵測、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking&lt;/a>、tokenization、最小揭露原則&lt;/li>
&lt;li>法規維度：GDPR / HIPAA / SOC2 / 個資法 對保留期與存取的要求&lt;/li>
&lt;li>跨團隊存取證據連續性：避免責任鏈斷在團隊邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a> 的分工：4.1 是欄位設計、4.12 是治理邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的交接：稽核責任邊界&lt;/li>
&lt;li>反模式：audit 跟 operational 混在同 stream；PII 直接打進 log；audit log 跟 application DB 同保留期&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit log&lt;/a> 是把責任、授權與敏感操作留下可稽核證據的訊號，責任是支援合規、責任追蹤與安全事件調查。&lt;/p>
&lt;p>這一頁處理的是 governance 邊界。Operational log 服務於除錯，audit log 服務於證據；兩者可以共享部分欄位，但保留、不變性、存取權限與 PII 規則不同。&lt;/p>
&lt;p>Audit log 的治理優先序跟 operational log 相反。Operational log 優先服務 &lt;em>當下&lt;/em> 的事故定位、追求即時性與覆蓋廣度；audit log 優先服務 &lt;em>未來&lt;/em> 的責任追蹤、追求完整性、不變性與長期可查詢。當這兩種優先序衝突時，audit 治理要勝過 operational 便利性。&lt;/p>
&lt;h2 id="兩種-log-的責任分工">兩種 log 的責任分工&lt;/h2>
&lt;p>Audit log 跟 operational log 承擔兩條獨立治理鏈：前者服務證據與責任追蹤、後者服務除錯與事故定位。兩者在對象、保留、不變性、權限與粒度上的差異決定它們需要走分開的 pipeline、storage 與保留策略。把 audit log 視為 operational log 的子集、混在同一 stream 治理、會在第一次合規稽核或法規請求時讓證據鏈被打斷（典型徵兆是「靠 grep operational log 拼湊稽核需求」）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Operational log&lt;/th>
 &lt;th>Audit log&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>主要對象&lt;/td>
 &lt;td>工程師、SRE、IC&lt;/td>
 &lt;td>合規、法務、安全事件調查、外部稽核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要目的&lt;/td>
 &lt;td>還原事件、定位 root cause&lt;/td>
 &lt;td>證明授權、責任追蹤、事件不可否認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保留期&lt;/td>
 &lt;td>7-30 天為典型、依除錯需求&lt;/td>
 &lt;td>數月到數年、依法規與合約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不變性&lt;/td>
 &lt;td>通常可被 rotate、aggregate、re-index&lt;/td>
 &lt;td>append-only、tamper-evident&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>存取權限&lt;/td>
 &lt;td>工程團隊廣泛存取&lt;/td>
 &lt;td>最小授權、存取本身也要被稽核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內容粒度&lt;/td>
 &lt;td>高頻、雜訊容忍&lt;/td>
 &lt;td>低頻、語意精準、欄位穩定&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>Operational log 在 incident timeline 還原時是主力證據。它的失分容忍度高：丟掉 1% 的 log 通常不影響 root cause 分析。&lt;/p>
&lt;p>Audit log 的失分容忍度極低。一次授權記錄遺失、一個欄位漂移、一段時區錯位，都可能讓事後責任追蹤失效。這個差異決定 audit log 必須走獨立 pipeline、獨立 storage、獨立保留策略。&lt;/p>
&lt;h2 id="核心欄位與不變性">核心欄位與不變性&lt;/h2>
&lt;p>Audit event 的核心責任是回答五個問題：誰（who）、做了什麼（what）、何時（when）、在哪（where）、結果如何（outcome）。任一欄位缺失，責任追蹤鏈就有缺口。&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>who&lt;/td>
 &lt;td>認證主體（user id、service account）&lt;/td>
 &lt;td>用 IP 代替主體 → 多人共用無法區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>what&lt;/td>
 &lt;td>操作類型 + 對象 ID&lt;/td>
 &lt;td>只記操作不記對象 → 無法重現範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>when&lt;/td>
 &lt;td>事件時間（含時區）+ ingest 時間&lt;/td>
 &lt;td>單一 timestamp → 無法判斷漂移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>where&lt;/td>
 &lt;td>來源 IP、region、tenant、session&lt;/td>
 &lt;td>缺 tenant → 跨租戶事件無法區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>outcome&lt;/td>
 &lt;td>成功 / 失敗 / 拒絕 + 拒絕原因&lt;/td>
 &lt;td>只記成功 → 失敗操作無痕跡&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不變性保證有三層遞進：&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 跟 operational log 的本質差異：對象、不變性、保留、法規</li>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 該記什麼：who / what / when / where / outcome、不可被應用層改寫</li>
<li>不變性保證：append-only storage、tamper-evident hash chain、independent retention</li>
<li><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 治理：log 中的 PII 偵測、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a>、tokenization、最小揭露原則</li>
<li>法規維度：GDPR / HIPAA / SOC2 / 個資法 對保留期與存取的要求</li>
<li>跨團隊存取證據連續性：避免責任鏈斷在團隊邊界</li>
<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> 的分工：4.1 是欄位設計、4.12 是治理邊界</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的交接：稽核責任邊界</li>
<li>反模式：audit 跟 operational 混在同 stream；PII 直接打進 log；audit log 跟 application DB 同保留期</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit log</a> 是把責任、授權與敏感操作留下可稽核證據的訊號，責任是支援合規、責任追蹤與安全事件調查。</p>
<p>這一頁處理的是 governance 邊界。Operational log 服務於除錯，audit log 服務於證據；兩者可以共享部分欄位，但保留、不變性、存取權限與 PII 規則不同。</p>
<p>Audit log 的治理優先序跟 operational log 相反。Operational log 優先服務 <em>當下</em> 的事故定位、追求即時性與覆蓋廣度；audit log 優先服務 <em>未來</em> 的責任追蹤、追求完整性、不變性與長期可查詢。當這兩種優先序衝突時，audit 治理要勝過 operational 便利性。</p>
<h2 id="兩種-log-的責任分工">兩種 log 的責任分工</h2>
<p>Audit log 跟 operational log 承擔兩條獨立治理鏈：前者服務證據與責任追蹤、後者服務除錯與事故定位。兩者在對象、保留、不變性、權限與粒度上的差異決定它們需要走分開的 pipeline、storage 與保留策略。把 audit log 視為 operational log 的子集、混在同一 stream 治理、會在第一次合規稽核或法規請求時讓證據鏈被打斷（典型徵兆是「靠 grep operational log 拼湊稽核需求」）。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Operational log</th>
          <th>Audit log</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要對象</td>
          <td>工程師、SRE、IC</td>
          <td>合規、法務、安全事件調查、外部稽核</td>
      </tr>
      <tr>
          <td>主要目的</td>
          <td>還原事件、定位 root cause</td>
          <td>證明授權、責任追蹤、事件不可否認</td>
      </tr>
      <tr>
          <td>保留期</td>
          <td>7-30 天為典型、依除錯需求</td>
          <td>數月到數年、依法規與合約</td>
      </tr>
      <tr>
          <td>不變性</td>
          <td>通常可被 rotate、aggregate、re-index</td>
          <td>append-only、tamper-evident</td>
      </tr>
      <tr>
          <td>存取權限</td>
          <td>工程團隊廣泛存取</td>
          <td>最小授權、存取本身也要被稽核</td>
      </tr>
      <tr>
          <td>內容粒度</td>
          <td>高頻、雜訊容忍</td>
          <td>低頻、語意精準、欄位穩定</td>
      </tr>
      <tr>
          <td>查詢期望</td>
          <td>秒級、即席</td>
          <td>分鐘到小時級、結構化、可重現</td>
      </tr>
  </tbody>
</table>
<p>Operational log 在 incident timeline 還原時是主力證據。它的失分容忍度高：丟掉 1% 的 log 通常不影響 root cause 分析。</p>
<p>Audit log 的失分容忍度極低。一次授權記錄遺失、一個欄位漂移、一段時區錯位，都可能讓事後責任追蹤失效。這個差異決定 audit log 必須走獨立 pipeline、獨立 storage、獨立保留策略。</p>
<h2 id="核心欄位與不變性">核心欄位與不變性</h2>
<p>Audit event 的核心責任是回答五個問題：誰（who）、做了什麼（what）、何時（when）、在哪（where）、結果如何（outcome）。任一欄位缺失，責任追蹤鏈就有缺口。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
          <th>失分風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>who</td>
          <td>認證主體（user id、service account）</td>
          <td>用 IP 代替主體 → 多人共用無法區分</td>
      </tr>
      <tr>
          <td>what</td>
          <td>操作類型 + 對象 ID</td>
          <td>只記操作不記對象 → 無法重現範圍</td>
      </tr>
      <tr>
          <td>when</td>
          <td>事件時間（含時區）+ ingest 時間</td>
          <td>單一 timestamp → 無法判斷漂移</td>
      </tr>
      <tr>
          <td>where</td>
          <td>來源 IP、region、tenant、session</td>
          <td>缺 tenant → 跨租戶事件無法區分</td>
      </tr>
      <tr>
          <td>outcome</td>
          <td>成功 / 失敗 / 拒絕 + 拒絕原因</td>
          <td>只記成功 → 失敗操作無痕跡</td>
      </tr>
  </tbody>
</table>
<p>不變性保證有三層遞進：</p>
<ol>
<li><strong>Append-only storage</strong>：寫入後不可修改、不可刪除。一般 object storage（S3 Object Lock、GCS Bucket Lock）或 immutable database table 可實作。</li>
<li><strong>Tamper-evident hash chain</strong>：每個 audit event 含前一個 event 的 hash，篡改任一筆會破壞整條 chain。需要週期性 anchor 到外部時間戳服務或第三方公證。</li>
<li><strong>Independent retention</strong>：audit log 的保留期跟 application DB 解耦，application 刪資料不影響 audit。retention 由合規團隊定義、不由應用團隊調整。</li>
</ol>
<p>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 FinTech 審計證據鏈</a>：揭露「audit log completeness、event correlation integrity、retention policy drift」是合規場景的核心治理項目，本章關注的是治理邊界跟欄位設計，事件相關的 evidence 包裝由 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 處理。</p>
<h2 id="跨團隊存取證據連續性">跨團隊存取證據連續性</h2>
<p>跨團隊 audit 治理的核心責任是維持責任鏈在團隊邊界上的連續性。應用團隊記應用層事件、基礎設施團隊記 infra 層存取、IAM 團隊記授權變更，三段證據各自必要、但只有拼接後才能還原一次跨團隊敏感操作。常見失敗來自團隊邊界上的責任鏈斷裂 — 而非單一團隊技術不到位 — 任一段缺失都會讓事後復盤無法閉合。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare 存取可追溯性與保留邊界</a>：揭露「access evidence continuity、retention boundary violations、timestamp integrity」三個方向。Healthcare 場景把這個問題放大，但跨團隊存取連續性是所有合規場景的共同議題。</p>
<p>讓存取證據跨團隊連續的可操作做法：</p>
<ol>
<li><strong>共用 correlation field</strong>：把 request id、trace id、session id 拉到應用層、infra 層、IAM 層共用，讓三段 log 可以拼起來。</li>
<li><strong>明確團隊 ownership 邊界</strong>：每類 audit event 指定唯一 owner team，避免「應該是另一隊負責」的責任轉嫁。</li>
<li><strong>跨團隊 retention 對齊</strong>：應用 audit、infra audit、IAM audit 的保留期要對齊或互為超集，避免一段過期一段還在的拼接斷裂。</li>
<li><strong>跨團隊查詢入口</strong>：合規團隊有單一查詢介面能跨三段 log 拉同一 correlation id 的完整證據鏈。</li>
</ol>
<p>把這些做法寫進 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a> 的 ownership 矩陣，能避免單次合規請求引發跨團隊的拼接工作。</p>
<h2 id="retention-與保留策略漂移">Retention 與保留策略漂移</h2>
<p>Retention 是 audit log 跟 operational log 最大的治理差異。Operational log 通常用 30-90 天 rotation；audit log 依資料類型跟法規可能要 1-10 年。</p>
<p>把 audit log 跟 operational log 用同一條 retention 策略治理，會在合規稽核時被抓出來。常見的失敗：</p>
<ul>
<li>audit log 跟 application DB 同保留 90 天、不符 GDPR / HIPAA / 金融法規。</li>
<li>audit log 經過 aggregation 處理、原始事件丟失、但 aggregated view 無法滿足法規要求。</li>
<li>retention 策略由應用團隊調整、不經合規團隊審批、容易在成本壓力下被縮短。</li>
</ul>
<p>Retention 漂移的偵測手段：把 retention compliance 變成可查詢的訊號。週期性對照各類 audit log 的實際留存時間跟政策要求、偏差超過閾值時觸發告警、讓漂移在治理週期內就被處理、避免等到稽核時才發現。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 FinTech retention policy drift</a> 跟 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention boundary violations</a>：兩個案例的判讀訊號都把 retention 偏離列為一級訊號（兩 case 的表格行明示這點）；本章在此基礎上補上「偏離視為治理事件、retention compliance 變成可查詢訊號」的展開、屬章節推論。</p>
<p>保留階梯（hot / warm / cold tier）與成本歸屬的詳細設計見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#%e6%8e%a7%e5%88%b6%e9%9d%a2%e8%88%87%e4%bf%9d%e7%95%99%e9%9a%8e%e6%a2%af" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 控制面與保留階梯</a>。</p>
<h2 id="pii-治理與最小揭露">PII 治理與最小揭露</h2>
<p><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 在 log 治理裡是雙重風險：寫入時的合規風險、長期保留時的外洩風險。Audit log 的長保留期讓 PII 風險被放大。</p>
<p>可操作的 PII 治理層次：</p>
<ol>
<li><strong>寫入前 redaction</strong>：應用層在輸出 log 時用結構化欄位 + 顯式 marking，避免把整個 request body 序列化進 log。</li>
<li><strong>Pipeline 層 PII 偵測</strong>：collector 加上 PII pattern 偵測（信用卡號、身分證、token），預設遮罩、例外要顯式授權。</li>
<li><strong>Tokenization / pseudonymization</strong>：把直接識別碼換成 token，token 跟原值的映射存在獨立、受嚴格授權的 vault 中。</li>
<li><strong>存取本身的稽核</strong>：誰存取了哪段 audit log、何時存取、為什麼存取，本身也是 audit event。</li>
</ol>
<p>最小揭露原則的實作關鍵是「預設遮罩、需要時申請」。把預設值設成揭露，會在某次事故除錯為了方便而打開、之後忘記關閉。預設遮罩讓每次解碼都是可追蹤的事件。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 audit log 時，先看事件是否能回答 who / what / when / where / outcome，再看資料是否受到獨立保護。</p>
<p>重點訊號包括：</p>
<ul>
<li>audit event 是否不可由一般應用流程修改</li>
<li><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 是否經過 redaction、tokenization 或最小揭露</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否符合法規與客戶合約要求</li>
<li>security incident 與 operational incident 是否能引用同一條證據鏈</li>
<li>跨團隊存取的 correlation field 是否連續</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>稽核需求出現時、靠 grep operational log 拼湊</li>
<li>log 中發現 credit card / 身分證 / token 等 PII</li>
<li>audit log 跟 application 同 retention（30 / 90 天）、不符法規</li>
<li>應用層帳號可寫入 / 修改 audit log</li>
<li>法規稽核請求耗時數週、事件鏈定位需要人工補洞</li>
<li>跨團隊查詢同一 correlation id 拼不出完整鏈</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audit 跟 operational 同 stream</td>
          <td>用一條 pipeline 處理所有 log</td>
          <td>拆獨立 pipeline、獨立 storage</td>
      </tr>
      <tr>
          <td>PII 直接進 log</td>
          <td>信用卡、身分證在 raw log 中可見</td>
          <td>Pipeline 層偵測 + 預設 redaction</td>
      </tr>
      <tr>
          <td>同保留期治理</td>
          <td>audit log 跟 application DB 同 90 天</td>
          <td>依法規重訂保留期、retention compliance 變成告警</td>
      </tr>
      <tr>
          <td>應用層可改寫 audit</td>
          <td>service account 對 audit storage 有 write/delete 權限</td>
          <td>append-only + tamper-evident hash chain</td>
      </tr>
      <tr>
          <td>跨團隊責任鏈斷裂</td>
          <td>同一事件三段 log 互不關聯</td>
          <td>共用 correlation field、跨團隊 retention 對齊</td>
      </tr>
  </tbody>
</table>
<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>：欄位設計</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 / cost</a>：audit 的長期保留成本</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：跨團隊 audit ownership 矩陣</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 evidence package</a>：audit log 進入 evidence 交接</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護</a>：<a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> redaction 與責任邊界</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：事故證據鏈引用 audit log</li>
<li><a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.17 security vs operational IR</a>：證據鏈來源</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：鑑識回溯查詢模式跟 audit log 的長期查詢設計</li>
</ul>
]]></content:encoded></item><item><title>6.12 Idempotency 與 Replay 驗證</title><link>https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何 idempotency 是分散式系統一級屬性：retry / failover / replay 的前提&lt;/li>
&lt;li>idempotency key 的設計：來源、生命週期、儲存&lt;/li>
&lt;li>exactly-once 是幻象、at-least-once + idempotent 才實際&lt;/li>
&lt;li>replay 驗證：從 log / event store 重播能否得到相同最終狀態&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message-queue&lt;/a> 的關係：consumer idempotency 是延伸專題&lt;/li>
&lt;li>payment / order / messaging 的 idempotency 模式差異&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos&lt;/a> 的整合：注入重複訊息驗證冪等&lt;/li>
&lt;li>反模式：idempotency 只靠 DB unique constraint、無 key 設計；retry 後副作用重複；replay 路徑從未驗證&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency&lt;/a> 與 replay 驗證是把重試、重播與副作用控制變成可驗證屬性，責任是讓 at-least-once 與 failover 不會把系統推向重複執行。&lt;/p>
&lt;p>這一頁處理的是分散式系統的重複輸入問題。只要有 retry、補償或訊息重送，冪等性就是正確性前提，把它當優化項會低估風險。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 idempotency 時，先看 key 的生命週期，再看 replay 是否能落在同一狀態。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>idempotency key 是否由 server 可控、可追蹤&lt;/li>
&lt;li>replay 路徑是否與 production 對齊&lt;/li>
&lt;li>late retry 是否會被誤視為新請求&lt;/li>
&lt;li>重複副作用是否能靠狀態機吸收&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">Stripe&lt;/a>：交易流程需要嚴格控制重複請求。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：webhook / event replay 經常直接暴露冪等缺口。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：訊息與通知類流程特別依賴重複輸入控制。&lt;/li>
&lt;/ul>
&lt;h2 id="支付類-idempotency-的設計約束">支付類 Idempotency 的設計約束&lt;/h2>
&lt;p>支付類 idempotency 的核心約束是「key 邊界跟業務操作邊界一致」 — 同一筆支付的所有 retry 必須共用 key、跨支付 key 必須不同、key 不可被偽造、且要保留足夠重放證據。失敗代價（重複扣款、重複建單）讓這四個約束從 best practice 變成正確性前提。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Stripe Idempotency 與零停機遷移&lt;/a> 揭露的 idempotency key 跟 transaction-path observability 兩個機制（S1 case 直接列出）；以下實作層判讀條件屬通用工程知識展開、case 本身只給「key 跟業務邊界一致」這一條方向。&lt;/p>
&lt;p>實作層的判讀條件：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Key 邊界跟業務一致&lt;/strong>：同一筆支付的 retry 共用 idempotency key、跨支付 key 不同。Key 來源 / TTL / fallback 設計屬實作細節、跟 6.12 SSoT 描述的 server 端 key 設計呼應&lt;/li>
&lt;li>&lt;strong>保留足夠證據供重放&lt;/strong>：transaction-path observability 要覆蓋交易關鍵欄位、讓 reconciliation 跟稽核可重放判讀&lt;/li>
&lt;/ul>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/#%e4%ba%a4%e6%98%93%e9%a1%9e-migration-%e7%9a%84%e7%89%b9%e6%ae%8a%e6%80%a7" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration-safety 交易類段&lt;/a> 共用 transaction-path observability、避免 migration 期間 idempotency 判讀失效。支付 reconciliation 跟交易語義詳見 01 資料庫模組（具體章節依 reconciliation / transaction 主題、目前待 01 模組對應頁建立）。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何 idempotency 是分散式系統一級屬性：retry / failover / replay 的前提</li>
<li>idempotency key 的設計：來源、生命週期、儲存</li>
<li>exactly-once 是幻象、at-least-once + idempotent 才實際</li>
<li>replay 驗證：從 log / event store 重播能否得到相同最終狀態</li>
<li>跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message-queue</a> 的關係：consumer idempotency 是延伸專題</li>
<li>payment / order / messaging 的 idempotency 模式差異</li>
<li>跟 <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 chaos</a> 的整合：注入重複訊息驗證冪等</li>
<li>反模式：idempotency 只靠 DB unique constraint、無 key 設計；retry 後副作用重複；replay 路徑從未驗證</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> 與 replay 驗證是把重試、重播與副作用控制變成可驗證屬性，責任是讓 at-least-once 與 failover 不會把系統推向重複執行。</p>
<p>這一頁處理的是分散式系統的重複輸入問題。只要有 retry、補償或訊息重送，冪等性就是正確性前提，把它當優化項會低估風險。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 idempotency 時，先看 key 的生命週期，再看 replay 是否能落在同一狀態。</p>
<p>重點訊號包括：</p>
<ul>
<li>idempotency key 是否由 server 可控、可追蹤</li>
<li>replay 路徑是否與 production 對齊</li>
<li>late retry 是否會被誤視為新請求</li>
<li>重複副作用是否能靠狀態機吸收</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">Stripe</a>：交易流程需要嚴格控制重複請求。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：webhook / event replay 經常直接暴露冪等缺口。</li>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：訊息與通知類流程特別依賴重複輸入控制。</li>
</ul>
<h2 id="支付類-idempotency-的設計約束">支付類 Idempotency 的設計約束</h2>
<p>支付類 idempotency 的核心約束是「key 邊界跟業務操作邊界一致」 — 同一筆支付的所有 retry 必須共用 key、跨支付 key 必須不同、key 不可被偽造、且要保留足夠重放證據。失敗代價（重複扣款、重複建單）讓這四個約束從 best practice 變成正確性前提。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Stripe Idempotency 與零停機遷移</a> 揭露的 idempotency key 跟 transaction-path observability 兩個機制（S1 case 直接列出）；以下實作層判讀條件屬通用工程知識展開、case 本身只給「key 跟業務邊界一致」這一條方向。</p>
<p>實作層的判讀條件：</p>
<ul>
<li><strong>Key 邊界跟業務一致</strong>：同一筆支付的 retry 共用 idempotency key、跨支付 key 不同。Key 來源 / TTL / fallback 設計屬實作細節、跟 6.12 SSoT 描述的 server 端 key 設計呼應</li>
<li><strong>保留足夠證據供重放</strong>：transaction-path observability 要覆蓋交易關鍵欄位、讓 reconciliation 跟稽核可重放判讀</li>
</ul>
<p>跟 <a href="/blog/backend/06-reliability/migration-safety/#%e4%ba%a4%e6%98%93%e9%a1%9e-migration-%e7%9a%84%e7%89%b9%e6%ae%8a%e6%80%a7" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration-safety 交易類段</a> 共用 transaction-path observability、避免 migration 期間 idempotency 判讀失效。支付 reconciliation 跟交易語義詳見 01 資料庫模組（具體章節依 reconciliation / transaction 主題、目前待 01 模組對應頁建立）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>03 message-queue：consumer 端冪等設計</li>
<li>06.4 chaos：注入重複訊息驗證</li>
<li>06.7 DR：replay 作為回復手段的前提</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>用戶被重複扣款 / 重複建立資源、靠人工對帳發現</li>
<li>retry policy 開啟後事故變嚴重、不敢開 retry</li>
<li>replay 從 event store 跑一次、結果跟 production 不同</li>
<li>idempotency key 從 client 端帶上來、無 server 端 fallback</li>
<li>key TTL 過短、晚到的 retry 變成新請求</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>03 message-queue：consumer idempotency 實作</li>
<li>06.4 chaos：注入重複訊息 / 故障 retry 場景</li>
<li>06.7 DR：replay 作為回復手段的前提</li>
<li>07 資安：idempotency key 不可被預測 / 偽造</li>
</ul>
]]></content:encoded></item><item><title>8.12 IC Handoff 與長事故跨班次協調</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何長事故需要獨立節點：8.2 角色分工假設單班次、長事故需要 handoff 協議&lt;/li>
&lt;li>handoff 的核心：context、open decision、外部承諾、現場狀態&lt;/li>
&lt;li>接班 checklist：incident state、active mitigations、stakeholder commitments、open hypothesis&lt;/li>
&lt;li>timezone follow-the-sun：班次邊界、值班池、跨區語言差異&lt;/li>
&lt;li>疲勞管理：強制換班門檻、決策權移轉、休息保護&lt;/li>
&lt;li>跨班次的決策一致性：避免新班次推翻前班次方向&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 command roles&lt;/a> 的延伸：8.2 是角色、8.12 是時序&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 communication&lt;/a> 的整合：接班同時對外通訊節奏不可斷&lt;/li>
&lt;li>反模式：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 連續工作 12h+ 才換班；接班用口頭交接、無書面 state；新班次重做已驗證假設&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol&lt;/a> 是把長事故的 context、未決策事項與外部承諾安全交接給下一班的流程，責任是讓事故在跨班次後仍維持同一條推進線。
在本章語境中，&lt;code>IC handoff&lt;/code> 指的是 &lt;code>[incident command system](/backend/knowledge-cards/incident-command-system/)&lt;/code> 的交接流程，不是一般輪班交接。&lt;/p>
&lt;p>這一頁處理的是時序延續。沒有 handoff，長事故最容易在交班時失去 momentum，甚至回到已排除的假設。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 handoff 時，先看資訊是否完整，再看新班次是否能延續決策。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>接班 checklist 是否固定&lt;/li>
&lt;li>open decision / open hypothesis 是否有明確記錄&lt;/li>
&lt;li>stakeholder commitments 是否會隨班次延續&lt;/li>
&lt;li>疲勞管理是否真的觸發換班&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台級事故常跨班次推進。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">Roblox&lt;/a>：大流量事故的持續協調很依賴接班品質。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：跨時區團隊需要很強的 handoff discipline。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>08.2 command roles：角色定義&lt;/li>
&lt;li>08.4 communication：跨班次對外節奏&lt;/li>
&lt;li>08.6 drills：handoff 演練&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：長事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>長事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 連續超過 8h 仍未換班&lt;/li>
&lt;li>接班後重複跑前班次已排除的假設&lt;/li>
&lt;li>跨區團隊事故無人擁有「現在誰是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a>」的單一答案&lt;/li>
&lt;li>handoff 後 stakeholder 收到矛盾訊息&lt;/li>
&lt;li>班次邊界事故進度停滯、無 forward momentum&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>08.2 command roles：角色定義&lt;/li>
&lt;li>08.4 communication：跨班次對外節奏&lt;/li>
&lt;li>08.6 drills：handoff 演練&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：長事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何長事故需要獨立節點：8.2 角色分工假設單班次、長事故需要 handoff 協議</li>
<li>handoff 的核心：context、open decision、外部承諾、現場狀態</li>
<li>接班 checklist：incident state、active mitigations、stakeholder commitments、open hypothesis</li>
<li>timezone follow-the-sun：班次邊界、值班池、跨區語言差異</li>
<li>疲勞管理：強制換班門檻、決策權移轉、休息保護</li>
<li>跨班次的決策一致性：避免新班次推翻前班次方向</li>
<li>跟 <a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 command roles</a> 的延伸：8.2 是角色、8.12 是時序</li>
<li>跟 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4 communication</a> 的整合：接班同時對外通訊節奏不可斷</li>
<li>反模式：<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 連續工作 12h+ 才換班；接班用口頭交接、無書面 state；新班次重做已驗證假設</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/handover-protocol/" data-link-title="Handover Protocol" data-link-desc="說明事故與值班交接時要傳遞哪些資訊、責任與完成條件">handover protocol</a> 是把長事故的 context、未決策事項與外部承諾安全交接給下一班的流程，責任是讓事故在跨班次後仍維持同一條推進線。
在本章語境中，<code>IC handoff</code> 指的是 <code>[incident command system](/backend/knowledge-cards/incident-command-system/)</code> 的交接流程，不是一般輪班交接。</p>
<p>這一頁處理的是時序延續。沒有 handoff，長事故最容易在交班時失去 momentum，甚至回到已排除的假設。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 handoff 時，先看資訊是否完整，再看新班次是否能延續決策。</p>
<p>重點訊號包括：</p>
<ul>
<li>接班 checklist 是否固定</li>
<li>open decision / open hypothesis 是否有明確記錄</li>
<li>stakeholder commitments 是否會隨班次延續</li>
<li>疲勞管理是否真的觸發換班</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台級事故常跨班次推進。</li>
<li><a href="/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">Roblox</a>：大流量事故的持續協調很依賴接班品質。</li>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：跨時區團隊需要很強的 handoff discipline。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>08.2 command roles：角色定義</li>
<li>08.4 communication：跨班次對外節奏</li>
<li>08.6 drills：handoff 演練</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：長事故 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>長事故 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 連續超過 8h 仍未換班</li>
<li>接班後重複跑前班次已排除的假設</li>
<li>跨區團隊事故無人擁有「現在誰是 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a>」的單一答案</li>
<li>handoff 後 stakeholder 收到矛盾訊息</li>
<li>班次邊界事故進度停滯、無 forward momentum</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.2 command roles：角色定義</li>
<li>08.4 communication：跨班次對外節奏</li>
<li>08.6 drills：handoff 演練</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：長事故 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
</ul>
]]></content:encoded></item><item><title>Datadog</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/</guid><description>&lt;p>Datadog 2023 multi-region 事故是「監控供應商自己掛」的代表案例。當客戶依賴的 observability 平台失效、客戶失去判讀自己服務的能力、IR 流程出現 second-order 影響。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>監控失效的 second-order 影響：客戶失去判讀工具、無法自我評估事故規模&lt;/li>
&lt;li>Multi-region 同時失效：region 隔離假設破裂時的全面失明&lt;/li>
&lt;li>客戶溝通：監控廠商如何向「正在 blind 的客戶」溝通&lt;/li>
&lt;li>自我監控：observability 廠商的 self-observability 設計&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2023-03&lt;/td>
 &lt;td>Multi-region 全球停擺&lt;/td>
 &lt;td>region 隔離破裂、客戶觀測落差&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Datadog 這個案例在講的是監控供應商自己失效時，客戶會同時失去判讀與協作能力。讀者先抓 multi-region、status page 與 incident management 的責任，再把 observability outage 看成 second-order 風險。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當監控平台自己出現連線或區域問題時，最先失去的是判讀服務健康的能力，資料本身通常還在。當客戶仍在 blind 狀態時，對外溝通與備援觀測通道就要先回來，否則事故會因資訊不足而延長。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否辨認 observability 平台本身就是依賴&lt;/li>
&lt;li>能否把 multi-region 隔離失效視為核心風險&lt;/li>
&lt;li>能否提供客戶替代觀測路徑&lt;/li>
&lt;li>能否把 self-observability 放進平台設計&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Datadog 這頁最適合和 Honeycomb、Slack 一起看：前者是觀測平台本身，後者是事故通訊路徑。三者放在一起時，讀者會更清楚地看到「當你看不見系統時，連協作也會失明」這件事怎麼發生。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2023 multi-region 事故說明監控廠商自己也會失明。&lt;/li>
&lt;li>status page 與 incident management 的銜接，決定客戶能否持續觀測自己服務。&lt;/li>
&lt;li>客戶在 blind 狀態時需要備援觀測路徑。&lt;/li>
&lt;li>self-observability 是 observability 廠商自己的基本要求。&lt;/li>
&lt;li>multi-region 同時失效會讓區域隔離假設失靈。&lt;/li>
&lt;li>incident response 的第一優先是把客戶從盲區拉回來。&lt;/li>
&lt;li>observability 平台失效會造成 second-order 事故。&lt;/li>
&lt;li>status page 與 incident workflow 需要維持同一條節奏。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/" data-link-title="Datadog：2023 多區觀測中斷事件" data-link-desc="監控平台自身退化時，如何避免客戶誤判系統健康狀態。">DD1&lt;/a>&lt;/td>
 &lt;td>多區觀測中斷&lt;/td>
 &lt;td>處理監控平台失效造成的判讀盲區&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.datadoghq.com/blog/2023-03-08-multiregion-infrastructure-connectivity-issue/">2023-03-08 Incident: Infrastructure connectivity issue affecting multiple regions&lt;/a>：Datadog 2023 多區事故的官方回顧。&lt;/li>
&lt;li>&lt;a href="https://www.datadoghq.com/blog/how-datadog-manages-incidents/">How we manage incidents at Datadog&lt;/a>：Datadog incident response 與 postmortem 的流程。&lt;/li>
&lt;li>&lt;a href="https://docs.datadoghq.com/incident_response/status_pages/">Status Pages&lt;/a>：Datadog status page 的官方文件。&lt;/li>
&lt;li>&lt;a href="https://docs.datadoghq.com/incident_response/incident_management/integrations/statuspage/">Integrate Atlassian Statuspage with Datadog Incident Management&lt;/a>：Statuspage 與 incident management 的交接。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Datadog 2023 multi-region 事故是「監控供應商自己掛」的代表案例。當客戶依賴的 observability 平台失效、客戶失去判讀自己服務的能力、IR 流程出現 second-order 影響。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>監控失效的 second-order 影響：客戶失去判讀工具、無法自我評估事故規模</li>
<li>Multi-region 同時失效：region 隔離假設破裂時的全面失明</li>
<li>客戶溝通：監控廠商如何向「正在 blind 的客戶」溝通</li>
<li>自我監控：observability 廠商的 self-observability 設計</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2023-03</td>
          <td>Multi-region 全球停擺</td>
          <td>region 隔離破裂、客戶觀測落差</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Datadog 這個案例在講的是監控供應商自己失效時，客戶會同時失去判讀與協作能力。讀者先抓 multi-region、status page 與 incident management 的責任，再把 observability outage 看成 second-order 風險。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當監控平台自己出現連線或區域問題時，最先失去的是判讀服務健康的能力，資料本身通常還在。當客戶仍在 blind 狀態時，對外溝通與備援觀測通道就要先回來，否則事故會因資訊不足而延長。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否辨認 observability 平台本身就是依賴</li>
<li>能否把 multi-region 隔離失效視為核心風險</li>
<li>能否提供客戶替代觀測路徑</li>
<li>能否把 self-observability 放進平台設計</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Datadog 這頁最適合和 Honeycomb、Slack 一起看：前者是觀測平台本身，後者是事故通訊路徑。三者放在一起時，讀者會更清楚地看到「當你看不見系統時，連協作也會失明」這件事怎麼發生。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2023 multi-region 事故說明監控廠商自己也會失明。</li>
<li>status page 與 incident management 的銜接，決定客戶能否持續觀測自己服務。</li>
<li>客戶在 blind 狀態時需要備援觀測路徑。</li>
<li>self-observability 是 observability 廠商自己的基本要求。</li>
<li>multi-region 同時失效會讓區域隔離假設失靈。</li>
<li>incident response 的第一優先是把客戶從盲區拉回來。</li>
<li>observability 平台失效會造成 second-order 事故。</li>
<li>status page 與 incident workflow 需要維持同一條節奏。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/" data-link-title="Datadog：2023 多區觀測中斷事件" data-link-desc="監控平台自身退化時，如何避免客戶誤判系統健康狀態。">DD1</a></td>
          <td>多區觀測中斷</td>
          <td>處理監控平台失效造成的判讀盲區</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.datadoghq.com/blog/2023-03-08-multiregion-infrastructure-connectivity-issue/">2023-03-08 Incident: Infrastructure connectivity issue affecting multiple regions</a>：Datadog 2023 多區事故的官方回顧。</li>
<li><a href="https://www.datadoghq.com/blog/how-datadog-manages-incidents/">How we manage incidents at Datadog</a>：Datadog incident response 與 postmortem 的流程。</li>
<li><a href="https://docs.datadoghq.com/incident_response/status_pages/">Status Pages</a>：Datadog status page 的官方文件。</li>
<li><a href="https://docs.datadoghq.com/incident_response/incident_management/integrations/statuspage/">Integrate Atlassian Statuspage with Datadog Incident Management</a>：Statuspage 與 incident management 的交接。</li>
</ul>
]]></content:encoded></item><item><title>Honeycomb</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/</guid><description>&lt;p>Honeycomb 是 observability platform、由創辦人之一 Charity Majors 推動的 observability-driven SRE 是領域 thought leadership 來源。教學重點在「以 observability 為主軸的 SRE 工程文化」。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>High-cardinality observability：相對 metrics-first 的觀測哲學&lt;/li>
&lt;li>Service Level Objective 實作：SLO budget、burn rate alert&lt;/li>
&lt;li>Test in production：feature flag + observability 的 production testing&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 文化：Charity Majors 的 SRE / on-call 觀點&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Observability Engineering&lt;/td>
 &lt;td>high-cardinality 與 unknown-unknowns&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLO Burn Rate Alert&lt;/td>
 &lt;td>error budget 速率告警設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test in Production&lt;/td>
 &lt;td>feature flag + observability 的安全推進&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production Excellence&lt;/td>
 &lt;td>Honeycomb 推動的 SRE 文化框架&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Honeycomb 這個案例在講的是 observability 如何變成工程決策，而不是只剩看板與指標。讀者先抓 high-cardinality、burn rate 與 test in production 這三個原語，再把它們看成觀測能力如何支撐 SRE 文化。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當訊號維度開始膨脹時，重點是先判斷資料還能不能回答問題，增加更多圖表解決不了維度膨脹。當 SLO 進入 burn 速率區間時，觀測系統要能直接幫團隊看見風險，而不是等事故發生後才補證據。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否辨認 high cardinality 何時讓查詢與告警失真&lt;/li>
&lt;li>能否把 SLO burn rate 轉成當下可行動的訊號&lt;/li>
&lt;li>能否在 production testing 中保住 blast radius&lt;/li>
&lt;li>能否把 observability 當成工程責任，而不是 ops 專屬工作&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Honeycomb 把觀測責任直接拉到每個工程團隊，這和 Google 的 SLO 制度、Datadog 的自我觀測、Slack 的狀態揭露形成一組互補視角。當讀者先懂這頁，就比較容易看懂為什麼高 cardinality 與 burn rate 是決策前提，當成報表細節會低估它們的影響。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>high cardinality 讓問題能按 tenant、feature、path 切開，而不是只看總平均。&lt;/li>
&lt;li>burn rate alert 直接把 SLO 消耗速度變成行動訊號。&lt;/li>
&lt;li>test in production 讓觀測訊號在真實流量下被驗證。&lt;/li>
&lt;li>observability engineering 把看板轉成工程決策入口。&lt;/li>
&lt;li>unknown-unknowns 讓觀測系統要先能回答「不知道要查什麼」的問題。&lt;/li>
&lt;li>production excellence 讓 observability 成為每個工程師的日常責任。&lt;/li>
&lt;li>query latency 會反過來告訴你資料建模是否已經失真。&lt;/li>
&lt;li>feature flag 配合觀測訊號，讓 production testing 可以安全推進。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">HC1&lt;/a>&lt;/td>
 &lt;td>Burn Rate 驅動可靠性&lt;/td>
 &lt;td>把 SLO 消耗速度轉成值班與改善優先序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/" data-link-title="Honeycomb：Production Excellence 與 Test in Production" data-link-desc="用 high-cardinality observability 把 production 變成安全的驗證環境：feature flag、progressive rollout 與即時回饋的配合。">HC2&lt;/a>&lt;/td>
 &lt;td>Production Excellence 與 Test in Prod&lt;/td>
 &lt;td>用 observability 把 production 變成安全的驗證環境&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.honeycomb.io/resources/getting-started/what-is-observability-engineering">What Is Observability Engineering?&lt;/a>：Honeycomb 對 observability engineering 的核心定義。&lt;/li>
&lt;li>&lt;a href="https://docs.honeycomb.io/get-started/basics/observability/concepts/high-cardinality">High Cardinality&lt;/a>：高 cardinality / dimensionality 的官方說明。&lt;/li>
&lt;li>&lt;a href="https://docs.honeycomb.io/reference/honeycomb-ui/slos/slo-detail-view/">SLO Detail View&lt;/a>：burn rate 與 budget burndown 的產品視角。&lt;/li>
&lt;li>&lt;a href="https://www.honeycomb.io/blog/observability-every-engineers-job-not-just-ops-problem">Observability: It&amp;rsquo;s Every Engineer’s Job, Not Just Ops’ Problem&lt;/a>：觀測責任不只在 ops 的實踐論述。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Honeycomb 是 observability platform、由創辦人之一 Charity Majors 推動的 observability-driven SRE 是領域 thought leadership 來源。教學重點在「以 observability 為主軸的 SRE 工程文化」。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>High-cardinality observability：相對 metrics-first 的觀測哲學</li>
<li>Service Level Objective 實作：SLO budget、burn rate alert</li>
<li>Test in production：feature flag + observability 的 production testing</li>
<li><a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 文化：Charity Majors 的 SRE / on-call 觀點</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability Engineering</td>
          <td>high-cardinality 與 unknown-unknowns</td>
      </tr>
      <tr>
          <td>SLO Burn Rate Alert</td>
          <td>error budget 速率告警設計</td>
      </tr>
      <tr>
          <td>Test in Production</td>
          <td>feature flag + observability 的安全推進</td>
      </tr>
      <tr>
          <td>Production Excellence</td>
          <td>Honeycomb 推動的 SRE 文化框架</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Honeycomb 這個案例在講的是 observability 如何變成工程決策，而不是只剩看板與指標。讀者先抓 high-cardinality、burn rate 與 test in production 這三個原語，再把它們看成觀測能力如何支撐 SRE 文化。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當訊號維度開始膨脹時，重點是先判斷資料還能不能回答問題，增加更多圖表解決不了維度膨脹。當 SLO 進入 burn 速率區間時，觀測系統要能直接幫團隊看見風險，而不是等事故發生後才補證據。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否辨認 high cardinality 何時讓查詢與告警失真</li>
<li>能否把 SLO burn rate 轉成當下可行動的訊號</li>
<li>能否在 production testing 中保住 blast radius</li>
<li>能否把 observability 當成工程責任，而不是 ops 專屬工作</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Honeycomb 把觀測責任直接拉到每個工程團隊，這和 Google 的 SLO 制度、Datadog 的自我觀測、Slack 的狀態揭露形成一組互補視角。當讀者先懂這頁，就比較容易看懂為什麼高 cardinality 與 burn rate 是決策前提，當成報表細節會低估它們的影響。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>high cardinality 讓問題能按 tenant、feature、path 切開，而不是只看總平均。</li>
<li>burn rate alert 直接把 SLO 消耗速度變成行動訊號。</li>
<li>test in production 讓觀測訊號在真實流量下被驗證。</li>
<li>observability engineering 把看板轉成工程決策入口。</li>
<li>unknown-unknowns 讓觀測系統要先能回答「不知道要查什麼」的問題。</li>
<li>production excellence 讓 observability 成為每個工程師的日常責任。</li>
<li>query latency 會反過來告訴你資料建模是否已經失真。</li>
<li>feature flag 配合觀測訊號，讓 production testing 可以安全推進。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">HC1</a></td>
          <td>Burn Rate 驅動可靠性</td>
          <td>把 SLO 消耗速度轉成值班與改善優先序</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/" data-link-title="Honeycomb：Production Excellence 與 Test in Production" data-link-desc="用 high-cardinality observability 把 production 變成安全的驗證環境：feature flag、progressive rollout 與即時回饋的配合。">HC2</a></td>
          <td>Production Excellence 與 Test in Prod</td>
          <td>用 observability 把 production 變成安全的驗證環境</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.honeycomb.io/resources/getting-started/what-is-observability-engineering">What Is Observability Engineering?</a>：Honeycomb 對 observability engineering 的核心定義。</li>
<li><a href="https://docs.honeycomb.io/get-started/basics/observability/concepts/high-cardinality">High Cardinality</a>：高 cardinality / dimensionality 的官方說明。</li>
<li><a href="https://docs.honeycomb.io/reference/honeycomb-ui/slos/slo-detail-view/">SLO Detail View</a>：burn rate 與 budget burndown 的產品視角。</li>
<li><a href="https://www.honeycomb.io/blog/observability-every-engineers-job-not-just-ops-problem">Observability: It&rsquo;s Every Engineer’s Job, Not Just Ops’ Problem</a>：觀測責任不只在 ops 的實踐論述。</li>
</ul>
]]></content:encoded></item><item><title>Sloth</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/sloth/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/sloth/</guid><description>&lt;p>Sloth 是 OSS Prometheus SLO generator、承擔三個責任：輸入簡單 YAML 定義 SLO、輸出 Prometheus recording rules + alerting rules（multi-window multi-burn-rate）、降低 SLO 維護成本。設計取捨偏向「Prometheus-only + OSS + GitOps-friendly」、適合 Prometheus-based 環境的純 OSS SLO 流程、跟 Nobl9 的 SaaS / multi-source 是不同定位。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>寫 Sloth SLO YAML&lt;/li>
&lt;li>產生 Prometheus recording / alerting rules&lt;/li>
&lt;li>設計 multi-window multi-burn-rate alert&lt;/li>
&lt;li>用 K8s Operator mode 自動同步&lt;/li>
&lt;li>評估從 Sloth 升級到 Nobl9 / OpenSLO 路徑&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-sloth-跑起來">最短路徑：5 分鐘把 Sloth 跑起來&lt;/h2>





&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"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: brew install slok/sloth/sloth / docker run&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="c1"># 2. 寫 SLO spec YAML&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: version: prometheus/v1, service, slos: [{name, objective, sli}]&lt;/span>
&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="c1"># 3. Generate rules&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: sloth generate -i slo.yaml &amp;gt; rules.yaml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 把 rules.yaml 載入 Prometheus&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="slo-yaml-結構">SLO YAML 結構&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>version + service&lt;/li>
&lt;li>slos[]：name / objective / SLI（events / raw）&lt;/li>
&lt;li>Alerting（page / ticket）&lt;/li>
&lt;/ul>
&lt;h3 id="multi-window-multi-burn-rate-alert">Multi-window multi-burn-rate alert&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Sloth 預設產生 Google SRE recommended alert（4 windows）&lt;/li>
&lt;li>Fast burn / slow burn&lt;/li>
&lt;li>對應 page（urgent）vs ticket（non-urgent）&lt;/li>
&lt;/ul>
&lt;h3 id="generate-rules-workflow">Generate rules workflow&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>CLI generate&lt;/li>
&lt;li>Output: recording rules + alert rules&lt;/li>
&lt;li>放進 Prometheus rule_files 載入&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="kubernetes-operator-mode">Kubernetes Operator mode&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Sloth K8s Operator&lt;/li>
&lt;li>PrometheusServiceLevel CRD&lt;/li>
&lt;li>自動 reconcile + 同步 Prometheus rules&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="slo-types">SLO types&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Events-based SLI（好 events / 總 events）&lt;/li>
&lt;li>Raw query SLI（自訂 PromQL）&lt;/li>
&lt;li>對應 PromQL 撰寫&lt;/li>
&lt;/ul>
&lt;h3 id="ci--gitops">CI / GitOps&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Sloth 是 OSS Prometheus SLO generator、承擔三個責任：輸入簡單 YAML 定義 SLO、輸出 Prometheus recording rules + alerting rules（multi-window multi-burn-rate）、降低 SLO 維護成本。設計取捨偏向「Prometheus-only + OSS + GitOps-friendly」、適合 Prometheus-based 環境的純 OSS SLO 流程、跟 Nobl9 的 SaaS / multi-source 是不同定位。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>寫 Sloth SLO YAML</li>
<li>產生 Prometheus recording / alerting rules</li>
<li>設計 multi-window multi-burn-rate alert</li>
<li>用 K8s Operator mode 自動同步</li>
<li>評估從 Sloth 升級到 Nobl9 / OpenSLO 路徑</li>
</ol>
<h2 id="最短路徑5-分鐘把-sloth-跑起來">最短路徑：5 分鐘把 Sloth 跑起來</h2>





<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. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: brew install slok/sloth/sloth / docker run</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="c1"># 2. 寫 SLO spec YAML</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: version: prometheus/v1, service, slos: [{name, objective, sli}]</span>
</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="c1"># 3. Generate rules</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: sloth generate -i slo.yaml &gt; rules.yaml</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: 把 rules.yaml 載入 Prometheus</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="slo-yaml-結構">SLO YAML 結構</h3>
<p>子議題：</p>
<ul>
<li>version + service</li>
<li>slos[]：name / objective / SLI（events / raw）</li>
<li>Alerting（page / ticket）</li>
</ul>
<h3 id="multi-window-multi-burn-rate-alert">Multi-window multi-burn-rate alert</h3>
<p>子議題：</p>
<ul>
<li>Sloth 預設產生 Google SRE recommended alert（4 windows）</li>
<li>Fast burn / slow burn</li>
<li>對應 page（urgent）vs ticket（non-urgent）</li>
</ul>
<h3 id="generate-rules-workflow">Generate rules workflow</h3>
<p>子議題：</p>
<ul>
<li>CLI generate</li>
<li>Output: recording rules + alert rules</li>
<li>放進 Prometheus rule_files 載入</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="kubernetes-operator-mode">Kubernetes Operator mode</h3>
<p>子議題：</p>
<ul>
<li>Sloth K8s Operator</li>
<li>PrometheusServiceLevel CRD</li>
<li>自動 reconcile + 同步 Prometheus rules</li>
<li>對應 <a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes vendor 頁</a></li>
</ul>
<h3 id="slo-types">SLO types</h3>
<p>子議題：</p>
<ul>
<li>Events-based SLI（好 events / 總 events）</li>
<li>Raw query SLI（自訂 PromQL）</li>
<li>對應 PromQL 撰寫</li>
</ul>
<h3 id="ci--gitops">CI / GitOps</h3>
<p>子議題：</p>
<ul>
<li>Sloth 在 CI 跑 generate</li>
<li>Git commit rules.yaml</li>
<li>Prometheus pull rules.yaml</li>
</ul>
<h3 id="vs-pyrra">vs Pyrra</h3>
<p>子議題：</p>
<ul>
<li>Sloth：CLI + Operator、產生 rules</li>
<li>Pyrra：K8s-native CRD、UI 內建</li>
<li>選擇判讀：簡單 / CI-first → Sloth；K8s-native + UI → Pyrra</li>
</ul>
<h3 id="vs-nobl9">vs Nobl9</h3>
<p>子議題：</p>
<ul>
<li>Sloth：OSS / Prometheus-only / 無 SaaS</li>
<li>Nobl9：商業 SaaS / 多 source / governance</li>
<li>升級路徑：OpenSLO YAML 部分相容</li>
</ul>
<h3 id="alert-tuning">Alert tuning</h3>
<p>子議題：</p>
<ul>
<li>Burn rate threshold 調整（依 service criticality）</li>
<li>Inhibition（alert 之間互相壓制）</li>
<li>對應 Alertmanager routing</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="generate-fail">Generate fail</h3>
<p>操作原則：YAML 格式錯 / SLI query 語法錯。判讀：sloth validate。</p>
<h3 id="alert-noise">Alert noise</h3>
<p>操作原則：burn rate threshold 過嚴。</p>
<h3 id="recording-rule-太多">Recording rule 太多</h3>
<p>操作原則：每 SLO 產生 N recording rules、cardinality 累積快。判讀：Prometheus series count。</p>
<h3 id="operator-reconcile-失敗">Operator reconcile 失敗</h3>
<p>操作原則：CRD permission / Prometheus rule API 連不上。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-source</td>
          <td><a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a></td>
      </tr>
      <tr>
          <td>K8s-native CRD + UI</td>
          <td>Pyrra</td>
      </tr>
      <tr>
          <td>Vendor 內建 SLO</td>
          <td>Datadog / Grafana / Honeycomb SLO</td>
      </tr>
      <tr>
          <td>純 SaaS</td>
          <td><a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a></td>
      </tr>
      <tr>
          <td>完整 OpenSLO</td>
          <td>OpenSLO + 對應 generator</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>PromQL 語法基礎</li>
<li>Prometheus alerting rule 內部</li>
<li>Sloth 完整 CLI option</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例方向</th>
          <th>對應主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 與 Release Gating</a></td>
          <td>SLI / SLO 原典、用來生成 Prometheus rule 的對齊對象</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動可靠性</a></td>
          <td>multi-window multi-burn-rate alert 的 PromQL 落地語意</td>
      </tr>
  </tbody>
</table>
<p><strong>Case 庫稀薄</strong>：本 cases/ 目錄目前沒有以 Sloth 為主軸的案例。</p>
<ul>
<li><strong>待補 Sloth customer case</strong>：Prometheus 重度團隊採用、Kubernetes Operator 落地案例</li>
<li><strong>候選 case</strong>：Spotify（Backstage + Prometheus 結合 SLO metadata）、LinkedIn（self-service metrics + SLO rule generation）— 若未來收錄需先在 cases/ 補正文</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a>、<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a>、<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Alertmanager</a></li>
</ul>
]]></content:encoded></item><item><title>4.C13 Discord：從儲存問題回推觀測缺口</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/</guid><description>&lt;p>Discord 的儲存演進案例從觀測角度回推一個教訓：儲存成長問題通常先表現為觀測缺口。不是資料庫變慢了才去看 metric，是該有的 metric 從一開始就沒設計。每一次儲存遷移（MongoDB → Cassandra → ScyllaDB）都揭露了上一階段缺少的訊號。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Discord 處理 trillions of messages。訊息是核心 user journey — 文字、圖片、附件、thread、搜尋全部依賴訊息儲存層。從 2015 年到 2023 年，Discord 的訊息儲存經歷三代架構。&lt;/p>
&lt;p>每一代遷移都由 production 問題觸發 — 追查後發現儲存層已經撐不住，才啟動下一代架構。追查過程中反覆出現的盲區是：觀測訊號不夠早、不夠細或不夠可信。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="mongodb-階段latency-tail-不可見">MongoDB 階段：latency tail 不可見&lt;/h3>
&lt;p>早期用 MongoDB 儲存訊息。隨著使用者成長，部分大型 server（Discord 的群組概念）的訊息量遠超平均值。這些 server 的查詢 latency 偶爾飆升到秒級，但 aggregated latency metric（p50、p95）看起來正常 — 因為大型 server 的 request 數量在整體中佔比極低。&lt;/p>
&lt;p>缺少的訊號：per-server latency breakdown。aggregated metric 遮蔽了局部惡化。&lt;/p>
&lt;h3 id="cassandra-階段hot-partition-沒有早期訊號">Cassandra 階段：hot partition 沒有早期訊號&lt;/h3>
&lt;p>遷移到 Cassandra 後，partition key 設計（channel ID）讓某些高流量 channel 成為 hot partition。Cassandra 的 compaction 在 hot partition 上延遲，讀取 latency 上升。&lt;/p>
&lt;p>問題由使用者回報「訊息載入很慢」才被發現，alert 沒有提前攔截。事後回看，Cassandra 的 read latency per partition 跟 compaction pending bytes per table 這兩個 metric 都有異常，但沒有人在 dashboard 上設 alert — 因為這兩個 metric 在 Cassandra 的預設 monitoring 裡不是 first-class 告警對象。&lt;/p>
&lt;p>缺少的訊號：hot partition 識別跟 compaction health 的主動告警。&lt;/p>
&lt;h3 id="scylladb-遷移階段dual-read-沒有比對-metric">ScyllaDB 遷移階段：dual-read 沒有比對 metric&lt;/h3>
&lt;p>從 Cassandra 遷移到 ScyllaDB 的過程中，Discord 做了 dual-read（同時讀舊資料庫跟新資料庫、比對結果）。dual-read 的正確性比對有做，但 latency 跟 error rate 的比對 metric 設計不完整 — 知道結果一致，但不知道 ScyllaDB 在特定 query pattern 下是否比 Cassandra 慢。&lt;/p>
&lt;p>遷移後才發現某些 query pattern 在 ScyllaDB 上的 tail latency 比 Cassandra 高，需要額外的 schema 調整。如果 dual-read 階段就有 per-query-pattern latency comparison metric，這個問題可以在 cutover 前發現。&lt;/p>
&lt;p>缺少的訊號：migration 期間的 per-pattern latency comparison。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>三次遷移暴露的觀測缺口有共同結構：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>缺口類型&lt;/th>
 &lt;th>MongoDB 階段&lt;/th>
 &lt;th>Cassandra 階段&lt;/th>
 &lt;th>ScyllaDB 遷移&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>維度不夠細&lt;/td>
 &lt;td>aggregated latency 遮蔽局部惡化&lt;/td>
 &lt;td>table-level metric 遮蔽 partition-level 問題&lt;/td>
 &lt;td>整體 dual-read match rate 遮蔽 per-pattern 差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>告警設計缺失&lt;/td>
 &lt;td>沒有 per-entity latency alert&lt;/td>
 &lt;td>沒有 hot partition alert&lt;/td>
 &lt;td>沒有 latency comparison alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發現方式&lt;/td>
 &lt;td>使用者回報&lt;/td>
 &lt;td>使用者回報&lt;/td>
 &lt;td>遷移後才發現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>共同模式：觀測訊號的粒度不夠、或告警只設在 aggregated 層 — 局部惡化被平均值淹沒，直到使用者感受到影響才被發現。&lt;/p></description><content:encoded><![CDATA[<p>Discord 的儲存演進案例從觀測角度回推一個教訓：儲存成長問題通常先表現為觀測缺口。不是資料庫變慢了才去看 metric，是該有的 metric 從一開始就沒設計。每一次儲存遷移（MongoDB → Cassandra → ScyllaDB）都揭露了上一階段缺少的訊號。</p>
<h2 id="業務背景">業務背景</h2>
<p>Discord 處理 trillions of messages。訊息是核心 user journey — 文字、圖片、附件、thread、搜尋全部依賴訊息儲存層。從 2015 年到 2023 年，Discord 的訊息儲存經歷三代架構。</p>
<p>每一代遷移都由 production 問題觸發 — 追查後發現儲存層已經撐不住，才啟動下一代架構。追查過程中反覆出現的盲區是：觀測訊號不夠早、不夠細或不夠可信。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="mongodb-階段latency-tail-不可見">MongoDB 階段：latency tail 不可見</h3>
<p>早期用 MongoDB 儲存訊息。隨著使用者成長，部分大型 server（Discord 的群組概念）的訊息量遠超平均值。這些 server 的查詢 latency 偶爾飆升到秒級，但 aggregated latency metric（p50、p95）看起來正常 — 因為大型 server 的 request 數量在整體中佔比極低。</p>
<p>缺少的訊號：per-server latency breakdown。aggregated metric 遮蔽了局部惡化。</p>
<h3 id="cassandra-階段hot-partition-沒有早期訊號">Cassandra 階段：hot partition 沒有早期訊號</h3>
<p>遷移到 Cassandra 後，partition key 設計（channel ID）讓某些高流量 channel 成為 hot partition。Cassandra 的 compaction 在 hot partition 上延遲，讀取 latency 上升。</p>
<p>問題由使用者回報「訊息載入很慢」才被發現，alert 沒有提前攔截。事後回看，Cassandra 的 read latency per partition 跟 compaction pending bytes per table 這兩個 metric 都有異常，但沒有人在 dashboard 上設 alert — 因為這兩個 metric 在 Cassandra 的預設 monitoring 裡不是 first-class 告警對象。</p>
<p>缺少的訊號：hot partition 識別跟 compaction health 的主動告警。</p>
<h3 id="scylladb-遷移階段dual-read-沒有比對-metric">ScyllaDB 遷移階段：dual-read 沒有比對 metric</h3>
<p>從 Cassandra 遷移到 ScyllaDB 的過程中，Discord 做了 dual-read（同時讀舊資料庫跟新資料庫、比對結果）。dual-read 的正確性比對有做，但 latency 跟 error rate 的比對 metric 設計不完整 — 知道結果一致，但不知道 ScyllaDB 在特定 query pattern 下是否比 Cassandra 慢。</p>
<p>遷移後才發現某些 query pattern 在 ScyllaDB 上的 tail latency 比 Cassandra 高，需要額外的 schema 調整。如果 dual-read 階段就有 per-query-pattern latency comparison metric，這個問題可以在 cutover 前發現。</p>
<p>缺少的訊號：migration 期間的 per-pattern latency comparison。</p>
<h2 id="教訓">教訓</h2>
<p>三次遷移暴露的觀測缺口有共同結構：</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>MongoDB 階段</th>
          <th>Cassandra 階段</th>
          <th>ScyllaDB 遷移</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>維度不夠細</td>
          <td>aggregated latency 遮蔽局部惡化</td>
          <td>table-level metric 遮蔽 partition-level 問題</td>
          <td>整體 dual-read match rate 遮蔽 per-pattern 差異</td>
      </tr>
      <tr>
          <td>告警設計缺失</td>
          <td>沒有 per-entity latency alert</td>
          <td>沒有 hot partition alert</td>
          <td>沒有 latency comparison alert</td>
      </tr>
      <tr>
          <td>發現方式</td>
          <td>使用者回報</td>
          <td>使用者回報</td>
          <td>遷移後才發現</td>
      </tr>
  </tbody>
</table>
<p>共同模式：觀測訊號的粒度不夠、或告警只設在 aggregated 層 — 局部惡化被平均值淹沒，直到使用者感受到影響才被發現。</p>
<p>三個缺口的修正方向也一致：</p>
<ol>
<li>把 entity-level metric（per-server、per-partition、per-query-pattern）從 debug-only 提升為 first-class 觀測訊號</li>
<li>在 aggregated alert 之外加 percentile 跟 tail latency alert（p99.9 而非只看 p95）</li>
<li>Migration 期間把 latency comparison 做成 per-pattern 的 real-time dashboard，不只看 overall match rate</li>
</ol>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：aggregated metric 遮蔽局部惡化是 data quality 問題 — 訊號存在但粒度不足以判讀。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：觀測缺口反覆出現代表 operating model 缺少「新服務上線 / 遷移時強制檢查觀測覆蓋」的 gate。</li>
<li><a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19 Debuggability by Design</a>：per-entity latency breakdown 跟 migration comparison metric 應該在系統設計時就規劃，不是事故後補。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>使用者回報問題但 dashboard 看起來正常 — aggregated metric 可能遮蔽局部惡化</li>
<li>資料庫或儲存層偶爾變慢但找不到原因 — 可能缺少 per-entity 或 per-partition metric</li>
<li>Migration 做了 dual-read 但只比對正確性、沒比對 latency — 遷移後才發現效能回歸</li>
<li>告警設計只有 error rate 跟 aggregated latency — 缺少 tail latency 跟 entity-level alert</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://discord.com/blog/how-discord-stores-billions-of-messages">How Discord Stores Billions of Messages</a>（MongoDB → Cassandra 階段）</li>
<li><a href="https://discord.com/blog/how-discord-stores-trillions-of-messages">How Discord Stores Trillions of Messages</a>（Cassandra → ScyllaDB 階段）</li>
</ul>
]]></content:encoded></item><item><title>Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore&lt;/a> overview 的 deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論&lt;/a>。寫入限制以 &lt;a href="https://firebase.google.com/docs/firestore/best-practices">官方 best practices&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境一個讚數欄位拖垮整條寫入">問題情境：一個讚數欄位拖垮整條寫入&lt;/h2>
&lt;p>直播平台上線一個「即時按讚數」功能：每個貼文一個 document，按讚就 &lt;code>update&lt;/code> 它的 &lt;code>likes&lt;/code> 欄位 &lt;code>+1&lt;/code>。內測沒問題，上了熱門直播——同一個貼文每秒湧入上千次按讚，寫入開始大量失敗、retry，延遲飆高，連帶其他寫入路徑被拖累。&lt;/p>
&lt;p>根因是流量全壓在&lt;strong>單一 document&lt;/strong> 上，而非流量總量超過 Firestore。Firestore 對單一 document 的持續寫入有軟上限（官方長期建議維持在每秒個位數量級、以當前文件為準），因為每次寫入要更新該 document 的所有索引、且並行寫同一 document 會觸發 contention 重試。把高頻變動的值塞進一個 document，等於替自己造一個寫入熱點。這篇處理 contention 的成因、用 distributed counter 把熱點打散的實作，以及這個手段的能力邊界。&lt;/p>
&lt;h2 id="核心概念寫入-contention-從哪來">核心概念：寫入 contention 從哪來&lt;/h2>
&lt;p>Firestore 的寫入成本不只是「寫一個值」。理解 contention 要抓三點：&lt;/p>
&lt;p>&lt;strong>每次寫入維護該 document 的所有索引&lt;/strong>。document 上有幾個被索引的欄位，一次寫入就要更新幾份索引條目。索引越多、單次寫入越重，這是寫入吞吐與索引數量綁定的根因。&lt;/p>
&lt;p>&lt;strong>並行寫同一 document 會序列化&lt;/strong>。Firestore 保證單一 document 的寫入一致性，並行的 &lt;code>+1&lt;/code> 不能各寫各的——它們競爭同一份狀態，後到的要重試。&lt;code>transaction&lt;/code> 與 &lt;code>FieldValue.increment()&lt;/code> 都受這個限制：&lt;code>increment&lt;/code> 省掉「讀-改-寫」的來回，但多個 increment 打同一 document 仍在同一個寫入熱點上排隊。&lt;/p>
&lt;p>&lt;strong>熱點是 per-document，不是 per-collection&lt;/strong>。把 1000 個貼文的讚數分在 1000 個 document，每個 document 每秒個位數寫入，完全沒問題；問題只在「單一 document 每秒上千寫入」。所以解法的方向是&lt;strong>把一個邏輯計數拆成多個物理 document&lt;/strong>。&lt;/p>
&lt;h2 id="配置distributed-counter-分片計數">配置：distributed counter 分片計數&lt;/h2>
&lt;p>distributed counter 的核心是把「一個計數」拆成 N 個 shard document，寫入時隨機挑一個 shard &lt;code>+1&lt;/code>，讀取時把所有 shard 加總。寫入壓力被分散到 N 個 document，每個 shard 的寫入頻率降為原本的 1/N。&lt;/p>
&lt;p>資料結構：在計數目標下建一個 &lt;code>shards&lt;/code> subcollection，N 個 shard document，每個存一段 partial count。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// counter.js（用 Firebase Web SDK v9 modular API）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">doc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">collection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">runTransaction&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">getDocs&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">writeBatch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">increment&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;firebase/firestore&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">NUM_SHARDS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">// 初始化：建立 N 個 shard、每個 count = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">createCounter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">counterRef&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">writeBatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">NUM_SHARDS&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">doc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">counterRef&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;shards&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">)),&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">count&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">commit&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&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="c1">// 寫入：隨機挑一個 shard +1（用 increment 省掉 read-modify-write）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">incrementCounter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">counterRef&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">shardId&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Math&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">floor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Math&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">random&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">NUM_SHARDS&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">shardRef&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">doc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">counterRef&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;shards&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shardId&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="kr">await&lt;/span> &lt;span class="nx">setDoc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shardRef&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">count&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">increment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">merge&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&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 class="p">}&lt;/span>
&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 class="c1">// 讀取：加總所有 shard
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">getCount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">counterRef&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">snap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getDocs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">counterRef&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;shards&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">total&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="nx">snap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">s&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">total&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="nx">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">total&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三個設計點要展開。第一，寫入用 &lt;code>increment(1)&lt;/code> 而非 transaction 的讀-改-寫：&lt;code>increment&lt;/code> 是 atomic 的 server-side 操作，省掉一次讀取，且本身就避開了「讀到舊值再寫」的 race。第二，shard 選擇用隨機分佈，讓寫入均勻打散到 N 個 shard——這是分片有效的前提，若選 shard 有偏（例如按 user id hash 但 user 分佈不均），熱點會在某幾個 shard 復現。第三，讀取要讀 N 個 document 加總，這是分片的代價：寫入便宜了，讀取從「讀 1 筆」變成「讀 N 筆」，計費與延遲都乘以 N。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore</a> overview 的 deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a>。寫入限制以 <a href="https://firebase.google.com/docs/firestore/best-practices">官方 best practices</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="問題情境一個讚數欄位拖垮整條寫入">問題情境：一個讚數欄位拖垮整條寫入</h2>
<p>直播平台上線一個「即時按讚數」功能：每個貼文一個 document，按讚就 <code>update</code> 它的 <code>likes</code> 欄位 <code>+1</code>。內測沒問題，上了熱門直播——同一個貼文每秒湧入上千次按讚，寫入開始大量失敗、retry，延遲飆高，連帶其他寫入路徑被拖累。</p>
<p>根因是流量全壓在<strong>單一 document</strong> 上，而非流量總量超過 Firestore。Firestore 對單一 document 的持續寫入有軟上限（官方長期建議維持在每秒個位數量級、以當前文件為準），因為每次寫入要更新該 document 的所有索引、且並行寫同一 document 會觸發 contention 重試。把高頻變動的值塞進一個 document，等於替自己造一個寫入熱點。這篇處理 contention 的成因、用 distributed counter 把熱點打散的實作，以及這個手段的能力邊界。</p>
<h2 id="核心概念寫入-contention-從哪來">核心概念：寫入 contention 從哪來</h2>
<p>Firestore 的寫入成本不只是「寫一個值」。理解 contention 要抓三點：</p>
<p><strong>每次寫入維護該 document 的所有索引</strong>。document 上有幾個被索引的欄位，一次寫入就要更新幾份索引條目。索引越多、單次寫入越重，這是寫入吞吐與索引數量綁定的根因。</p>
<p><strong>並行寫同一 document 會序列化</strong>。Firestore 保證單一 document 的寫入一致性，並行的 <code>+1</code> 不能各寫各的——它們競爭同一份狀態，後到的要重試。<code>transaction</code> 與 <code>FieldValue.increment()</code> 都受這個限制：<code>increment</code> 省掉「讀-改-寫」的來回，但多個 increment 打同一 document 仍在同一個寫入熱點上排隊。</p>
<p><strong>熱點是 per-document，不是 per-collection</strong>。把 1000 個貼文的讚數分在 1000 個 document，每個 document 每秒個位數寫入，完全沒問題；問題只在「單一 document 每秒上千寫入」。所以解法的方向是<strong>把一個邏輯計數拆成多個物理 document</strong>。</p>
<h2 id="配置distributed-counter-分片計數">配置：distributed counter 分片計數</h2>
<p>distributed counter 的核心是把「一個計數」拆成 N 個 shard document，寫入時隨機挑一個 shard <code>+1</code>，讀取時把所有 shard 加總。寫入壓力被分散到 N 個 document，每個 shard 的寫入頻率降為原本的 1/N。</p>
<p>資料結構：在計數目標下建一個 <code>shards</code> subcollection，N 個 shard document，每個存一段 partial count。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// counter.js（用 Firebase Web SDK v9 modular API）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">doc</span><span class="p">,</span> <span class="nx">collection</span><span class="p">,</span> <span class="nx">runTransaction</span><span class="p">,</span> <span class="nx">getDocs</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">writeBatch</span><span class="p">,</span> <span class="nx">increment</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;firebase/firestore&#39;</span><span class="p">;</span>
</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="kr">const</span> <span class="nx">NUM_SHARDS</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
</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="c1">// 初始化：建立 N 個 shard、每個 count = 0
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">createCounter</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">counterRef</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="nx">writeBatch</span><span class="p">(</span><span class="nx">db</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">NUM_SHARDS</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">batch</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">counterRef</span><span class="p">,</span> <span class="s1">&#39;shards&#39;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nx">i</span><span class="p">)),</span> <span class="p">{</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">0</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="kr">await</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">commit</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span>
</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="c1">// 寫入：隨機挑一個 shard +1（用 increment 省掉 read-modify-write）
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">incrementCounter</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">counterRef</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="kr">const</span> <span class="nx">shardId</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="nx">NUM_SHARDS</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="kr">const</span> <span class="nx">shardRef</span> <span class="o">=</span> <span class="nx">doc</span><span class="p">(</span><span class="nx">counterRef</span><span class="p">,</span> <span class="s1">&#39;shards&#39;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nx">shardId</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="kr">await</span> <span class="nx">setDoc</span><span class="p">(</span><span class="nx">shardRef</span><span class="p">,</span> <span class="p">{</span> <span class="nx">count</span><span class="o">:</span> <span class="nx">increment</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">merge</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span>
</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 class="c1">// 讀取：加總所有 shard
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">getCount</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">counterRef</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="kr">const</span> <span class="nx">snap</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getDocs</span><span class="p">(</span><span class="nx">collection</span><span class="p">(</span><span class="nx">counterRef</span><span class="p">,</span> <span class="s1">&#39;shards&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="kd">let</span> <span class="nx">total</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="nx">snap</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">total</span> <span class="o">+=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">data</span><span class="p">().</span><span class="nx">count</span><span class="p">;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="k">return</span> <span class="nx">total</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>三個設計點要展開。第一，寫入用 <code>increment(1)</code> 而非 transaction 的讀-改-寫：<code>increment</code> 是 atomic 的 server-side 操作，省掉一次讀取，且本身就避開了「讀到舊值再寫」的 race。第二，shard 選擇用隨機分佈，讓寫入均勻打散到 N 個 shard——這是分片有效的前提，若選 shard 有偏（例如按 user id hash 但 user 分佈不均），熱點會在某幾個 shard 復現。第三，讀取要讀 N 個 document 加總，這是分片的代價：寫入便宜了，讀取從「讀 1 筆」變成「讀 N 筆」，計費與延遲都乘以 N。</p>
<p>如果即時讀取頻率也很高（每個觀眾畫面都要顯示即時讚數），讀 N 個 shard 的成本會反過來變成瓶頸。這時把彙總值定期寫回一個 summary document，client 訂閱 summary 而非每次加總：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 由 Cloud Function 定時（或 onWrite 觸發 + debounce）彙總寫回 summary
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">aggregateToSummary</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">counterRef</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kr">const</span> <span class="nx">total</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getCount</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">counterRef</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kr">await</span> <span class="nx">setDoc</span><span class="p">(</span><span class="nx">doc</span><span class="p">(</span><span class="nx">counterRef</span><span class="p">,</span> <span class="s1">&#39;summary&#39;</span><span class="p">,</span> <span class="s1">&#39;current&#39;</span><span class="p">),</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">count</span><span class="o">:</span> <span class="nx">total</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">updatedAt</span><span class="o">:</span> <span class="nx">serverTimestamp</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這把「即時精確」換成「近即時」：summary 有刷新間隔的延遲，但讀取從 N 筆降回 1 筆。讚數、觀看數這類「差幾個不影響體驗」的計數，這個取捨幾乎總是對的。</p>
<h2 id="故障演練五個高頻寫入踩坑">故障演練：五個高頻寫入踩坑</h2>
<h4 id="case-1直接-increment-單一-document-沒分片">Case 1：直接 <code>increment</code> 單一 document 沒分片</h4>
<p>最常見的起手——以為 <code>FieldValue.increment()</code> 就解決了並行，忽略它仍在單一 document 的寫入熱點上。低流量沒事、熱門事件寫爆。修法：判斷該計數的峰值寫入頻率，超過單 document 軟上限就上 distributed counter；不確定峰值就先分片，分片對低流量無害（只是多讀幾筆）。</p>
<h4 id="case-2shard-數量拍腦袋定太小">Case 2：shard 數量拍腦袋定太小</h4>
<p>設了 3 個 shard，峰值流量下每個 shard 仍每秒上百寫入、照樣 contention。修法：shard 數要對齊峰值寫入頻率除以單 shard 安全寫入率（每秒個位數）。預期峰值每秒 500 寫入、單 shard 安全 5/s，就需要約 100 個 shard。寧可估高。</p>
<h4 id="case-3shard-太多拖垮讀取">Case 3：shard 太多拖垮讀取</h4>
<p>反向錯誤——為了保險設 1000 個 shard，結果每次讀計數要讀 1000 個 document，讀取計費與延遲爆炸。修法：shard 數是寫入分散與讀取成本的取捨；高寫入低讀取用多 shard + 直接加總，高寫入高讀取用多 shard + summary 彙總，別用「讀 N 筆加總」硬扛高頻讀取。</p>
<h4 id="case-4選-shard-有偏導致熱點復現">Case 4：選 shard 有偏導致熱點復現</h4>
<p>用 <code>userId</code> 的 hash 選 shard、但活躍 user 集中在少數，寫入仍打在某幾個 shard 上。修法：shard 選擇要與寫入來源無關的隨機分佈，不要綁任何可能傾斜的 key。</p>
<h4 id="case-5把分片計數當強一致餘額用">Case 5：把分片計數當強一致餘額用</h4>
<p>把 distributed counter 拿來記帳戶餘額、庫存這類需要強一致與精確讀的值。分片計數的讀取是「加總當下各 shard」，並行寫入下讀到的是近似值，不適合做扣款判斷。修法：強一致的計數（餘額、庫存、配額）不該用分片計數，也通常不該用 Firestore 的單欄位累加——這類值要走 transaction 嚴格控制、或放關聯式資料庫用 row lock，見邊界段。</p>
<h2 id="容量與觀測shard-數的估算與監控">容量與觀測：shard 數的估算與監控</h2>
<p>shard 數量的估算從峰值寫入頻率反推：<code>shard 數 ≈ 峰值每秒寫入 / 單 shard 安全寫入率</code>。單 shard 安全寫入率以官方當前的單 document 持續寫入建議為基準（個位數量級），估算時取保守值。讀取成本同步要算：每次讀計數 = N 次 document read，乘上讀取頻率與日活，這是 distributed counter 的隱性帳。</p>
<p>監控的訊號是寫入失敗率與 contention 重試。寫入大量失敗 + retry 是 contention 的直接徵兆；單一 shard 的寫入頻率若明顯高於其他 shard，是 shard 選擇有偏的徵兆。這些訊號接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>，把高頻寫入的健康度當成可觀測指標而非事故才發現。</p>
<p>容量規劃還要考慮 shard 數的可調整性：shard 數寫死在 client 程式裡，事後要加 shard 得同時改寫入與讀取邏輯、並補建新 shard document。預期會成長的計數，起步就把 shard 數設在峰值對應的量級，比事後擴容省事。</p>
<h2 id="邊界與整合什麼計數不該用分片什麼該離開-firestore">邊界與整合：什麼計數不該用分片，什麼該離開 Firestore</h2>
<p>distributed counter 解的是「高頻、可接受近似、不需強一致」的計數——讚數、觀看數、瀏覽量、即時參與人數。它的邊界很清楚：</p>
<ul>
<li><strong>需要強一致與精確的計數</strong>：帳戶餘額、庫存、配額扣減。這些要嘛用 Firestore transaction 嚴格序列化（但就回到單 document 寫入上限的限制、不適合高頻），要嘛放關聯式資料庫用 row-level lock 與交易保護（見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>）</li>
<li><strong>需要任意維度聚合的計數</strong>：要算「各地區、各時段的累計」這類多維彙總，分片計數表達不了，該把事件流寫進分析系統或關聯式資料庫做 aggregation</li>
<li><strong>計數本身是核心交易資料</strong>：當計數驅動扣款、結算這類有金錢後果的流程，把它留在 client 直連的 Firestore 是控制面風險，該移到後端——這呼應 <a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a> 的成本與授權 driver</li>
</ul>
<p>判讀順序是先問「這個計數能不能容忍近似與最終一致」。能，distributed counter 是 Firestore 內的正解；不能，這個計數從一開始就不該用 Firestore 的單欄位累加表達。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上層：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a>（容量特性與寫入熱點）</li>
<li>一致性邊界：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>（強一致計數的去處）</li>
<li>容量背景：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>觀測：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>（寫入失敗率與 contention 監控）</li>
<li>官方：<a href="https://firebase.google.com/docs/firestore/best-practices">Firestore best practices</a>、<a href="https://firebase.google.com/docs/firestore/solutions/counters">Distributed counters solution</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka Retention 與 Tiered Storage：保留策略、log compaction 與冷熱分層</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/retention-tiered-storage/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/retention-tiered-storage/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a> overview 的 implementation-layer deep article、聚焦保留與分層儲存。選型層的「該不該選 Kafka」「跟其他 broker 差在哪」見 overview；本文回答「保留策略怎麼設、log compaction 怎麼運作、冷熱分層怎麼讓容量跟保留期解耦、踩哪些坑」。配置段在 Apache Kafka KRaft 單節點實機驗證；tiered storage 段標註未實機驗證的範圍。&lt;/p>&lt;/blockquote>
&lt;h2 id="retention-是-replay-window-的物理邊界">Retention 是 replay window 的物理邊界&lt;/h2>
&lt;p>Retention 的核心責任是決定「一筆訊息在 broker 上能存活多久」、而這條邊界直接界定 consumer 能往回重播多遠。Kafka 的 log 是 append-only 的事件序列、訊息寫入後不會被原地修改；retention 是唯一會把舊訊息從磁碟移除的機制。設多久、用什麼條件刪、刪掉之後 consumer 還能不能讀到，全由保留策略決定。&lt;/p>
&lt;p>這條邊界之所以重要、是因為 Kafka 的多 consumer 模型讓「重播」變成一級能力。同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 可以被多組 consumer 各自從任意 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 開始讀、每組維護自己的進度；只要訊息還在 retention 範圍內、新加入的 consumer 或出事後要補算的 consumer 都能從頭重讀。一旦訊息超過 retention 被刪、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window&lt;/a> 就到此為止、補償只能改走資料庫或上游來源。&lt;/p>
&lt;p>Kafka 提供兩條獨立的保留軸、可單獨用也可同時用：&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>&lt;code>retention.ms&lt;/code>&lt;/td>
 &lt;td>訊息寫入時間超過設定值（時間軸）&lt;/td>
 &lt;td>「保留 7 天事件供事故 replay」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>retention.bytes&lt;/code>&lt;/td>
 &lt;td>該 partition log 總大小超過設定值（容量軸）&lt;/td>
 &lt;td>「每 partition 上限 50 GB、防止磁碟塞爆」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩者同時設&lt;/td>
 &lt;td>任一條件先達到就刪（取交集、誰先到誰生效）&lt;/td>
 &lt;td>「保留 7 天、但單 partition 不超過 50 GB」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>時間軸對齊的是 replay 需求：把 retention 設成「事故從發生到偵測到修復的最長時間」、確保發現要補算時事件還在。容量軸對齊的是成本與磁碟保護：避免某個突發高流量 topic 把 broker 磁碟寫滿、拖垮同 broker 上其他 partition。兩者同時設時是「誰先觸發誰生效」、所以容量軸常常會在高流量時段提前砍掉本來預期能保留 7 天的事件——這個交互是後面故障演練的重點之一。&lt;/p>
&lt;p>實機建立一個同時設兩軸的 topic、&lt;code>--describe&lt;/code> 會把保留配置直接列在 Configs：&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"># CLI 在容器內 /opt/kafka/bin/、bootstrap-server 指向 broker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">kafka-topics.sh --create --topic ret-delete --partitions &lt;span class="m">1&lt;/span> &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> --config retention.ms&lt;span class="o">=&lt;/span>&lt;span class="m">60000&lt;/span> &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> --config retention.bytes&lt;span class="o">=&lt;/span>&lt;span class="m">10485760&lt;/span> &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> --config segment.ms&lt;span class="o">=&lt;/span>&lt;span class="m">10000&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> --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">kafka-topics.sh --describe --topic ret-delete --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># Configs: retention.ms=60000,retention.bytes=10485760,segment.ms=10000,...&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>retention 不是寫死在建 topic 當下、線上可以用 &lt;code>kafka-configs.sh --alter&lt;/code> 動態調整、立即生效不需重啟 broker：&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">kafka-configs.sh --alter --entity-type topics --entity-name ret-delete &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --add-config retention.ms&lt;span class="o">=&lt;/span>&lt;span class="m">3600000&lt;/span> &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> --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Completed updating config for topic ret-delete.&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">kafka-configs.sh --describe --entity-type topics --entity-name ret-delete &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> --bootstrap-server localhost:9092
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># retention.ms=3600000 sensitive=false synonyms={DYNAMIC_TOPIC_CONFIG:retention.ms=3600000}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>動態調整的 retention 屬於 &lt;code>DYNAMIC_TOPIC_CONFIG&lt;/code>、優先於 broker 層的 &lt;code>log.retention.*&lt;/code> 預設值；synonyms 欄位會把覆蓋關係列出來、排查時可確認當前生效的是哪一層。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> overview 的 implementation-layer deep article、聚焦保留與分層儲存。選型層的「該不該選 Kafka」「跟其他 broker 差在哪」見 overview；本文回答「保留策略怎麼設、log compaction 怎麼運作、冷熱分層怎麼讓容量跟保留期解耦、踩哪些坑」。配置段在 Apache Kafka KRaft 單節點實機驗證；tiered storage 段標註未實機驗證的範圍。</p></blockquote>
<h2 id="retention-是-replay-window-的物理邊界">Retention 是 replay window 的物理邊界</h2>
<p>Retention 的核心責任是決定「一筆訊息在 broker 上能存活多久」、而這條邊界直接界定 consumer 能往回重播多遠。Kafka 的 log 是 append-only 的事件序列、訊息寫入後不會被原地修改；retention 是唯一會把舊訊息從磁碟移除的機制。設多久、用什麼條件刪、刪掉之後 consumer 還能不能讀到，全由保留策略決定。</p>
<p>這條邊界之所以重要、是因為 Kafka 的多 consumer 模型讓「重播」變成一級能力。同一個 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 可以被多組 consumer 各自從任意 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 開始讀、每組維護自己的進度；只要訊息還在 retention 範圍內、新加入的 consumer 或出事後要補算的 consumer 都能從頭重讀。一旦訊息超過 retention 被刪、<a href="/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window</a> 就到此為止、補償只能改走資料庫或上游來源。</p>
<p>Kafka 提供兩條獨立的保留軸、可單獨用也可同時用：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>觸發條件</th>
          <th>典型場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>retention.ms</code></td>
          <td>訊息寫入時間超過設定值（時間軸）</td>
          <td>「保留 7 天事件供事故 replay」</td>
      </tr>
      <tr>
          <td><code>retention.bytes</code></td>
          <td>該 partition log 總大小超過設定值（容量軸）</td>
          <td>「每 partition 上限 50 GB、防止磁碟塞爆」</td>
      </tr>
      <tr>
          <td>兩者同時設</td>
          <td>任一條件先達到就刪（取交集、誰先到誰生效）</td>
          <td>「保留 7 天、但單 partition 不超過 50 GB」</td>
      </tr>
  </tbody>
</table>
<p>時間軸對齊的是 replay 需求：把 retention 設成「事故從發生到偵測到修復的最長時間」、確保發現要補算時事件還在。容量軸對齊的是成本與磁碟保護：避免某個突發高流量 topic 把 broker 磁碟寫滿、拖垮同 broker 上其他 partition。兩者同時設時是「誰先觸發誰生效」、所以容量軸常常會在高流量時段提前砍掉本來預期能保留 7 天的事件——這個交互是後面故障演練的重點之一。</p>
<p>實機建立一個同時設兩軸的 topic、<code>--describe</code> 會把保留配置直接列在 Configs：</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"># CLI 在容器內 /opt/kafka/bin/、bootstrap-server 指向 broker</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">kafka-topics.sh --create --topic ret-delete --partitions <span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --config retention.ms<span class="o">=</span><span class="m">60000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --config retention.bytes<span class="o">=</span><span class="m">10485760</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --config segment.ms<span class="o">=</span><span class="m">10000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</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">kafka-topics.sh --describe --topic ret-delete --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># Configs: retention.ms=60000,retention.bytes=10485760,segment.ms=10000,...</span></span></span></code></pre></div><p>retention 不是寫死在建 topic 當下、線上可以用 <code>kafka-configs.sh --alter</code> 動態調整、立即生效不需重啟 broker：</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">kafka-configs.sh --alter --entity-type topics --entity-name ret-delete <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --add-config retention.ms<span class="o">=</span><span class="m">3600000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Completed updating config for topic ret-delete.</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">kafka-configs.sh --describe --entity-type topics --entity-name ret-delete <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># retention.ms=3600000 sensitive=false synonyms={DYNAMIC_TOPIC_CONFIG:retention.ms=3600000}</span></span></span></code></pre></div><p>動態調整的 retention 屬於 <code>DYNAMIC_TOPIC_CONFIG</code>、優先於 broker 層的 <code>log.retention.*</code> 預設值；synonyms 欄位會把覆蓋關係列出來、排查時可確認當前生效的是哪一層。</p>
<h2 id="segment-是刪除的最小單位">Segment 是刪除的最小單位</h2>
<p>Retention 刪資料的最小單位是 log segment、不是單筆訊息。理解這一點才能解釋「為什麼設了 retention.ms 之後，過期的訊息有時還在」。每個 partition 的 log 在磁碟上被切成多個 segment 檔、只有 active segment（當前正在寫入的那一個）以外、已經 roll over 的 segment 才會被 retention 檢查並整段刪除。</p>
<p>Segment 何時 roll over 由兩個條件決定：<code>segment.bytes</code>（檔案大到上限、預設 1 GB、最小 1 MB）或 <code>segment.ms</code>（檔案存在時間超過設定）。實機寫入 ~6 MB 資料到一個 <code>segment.bytes=1048576</code>（1 MB）的 topic、磁碟上會看到 6 個 roll 過的 segment：</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">00000000000000000000.log   1045229   # 已 roll，可被 retention 刪
</span></span><span class="line"><span class="ln">2</span><span class="cl">00000000000000001024.log   1046336   # 已 roll
</span></span><span class="line"><span class="ln">3</span><span class="cl">00000000000000002048.log   1046336   # 已 roll
</span></span><span class="line"><span class="ln">4</span><span class="cl">00000000000000003072.log   1046336   # 已 roll
</span></span><span class="line"><span class="ln">5</span><span class="cl">00000000000000004096.log   1037748   # 已 roll
</span></span><span class="line"><span class="ln">6</span><span class="cl">00000000000000005112.log    904737   # active segment，不會被刪</span></span></code></pre></div><p>Retention 的實際刪除動作由背景執行緒週期性執行、頻率是 broker 層的 <code>log.retention.check.interval.ms</code>、預設 300000 毫秒（5 分鐘）。這代表「過期」跟「被刪」之間有最長一個檢查週期的延遲：訊息超過 retention.ms 的瞬間不會立刻消失、要等下一次檢查跑到、且該訊息所在的 segment 已經 roll over、整段才會被刪。實機把 retention.bytes 設成 2 MB、寫進 6 MB（6 個 segment）、在 5 分鐘檢查週期內查 earliest offset 仍是 0——超量的 segment 還沒被回收、因為檢查執行緒還沒跑到下一輪。</p>
<p>這個機制有兩個操作後果。其一、磁碟用量會在「超過 retention 上限」到「下一次檢查」之間短暫超標、容量規劃要把這段 overshoot 算進緩衝。其二、把 retention.ms 設得比 segment.ms 還短沒有意義：訊息要等所在 segment roll 才可能被刪、active segment 永遠刪不掉、所以實際最短保留時間是 <code>max(retention.ms, segment 尚未 roll 的時間)</code>。</p>
<h2 id="cleanuppolicydelete-與-compact-是兩種回收語意">cleanup.policy：delete 與 compact 是兩種回收語意</h2>
<p><code>cleanup.policy</code> 決定 retention 用哪種語意回收空間、是保留策略最關鍵的分岔。預設值 <code>delete</code> 是時間或容量到期就整段刪除、適合事件流（event stream）：訊息代表「發生過的事實」、過了 replay window 就沒有保留價值。另一個值 <code>compact</code> 是 log compaction、語意完全不同：它保留每個 key 的最新值、刪除同 key 的歷史版本、適合「狀態快照」型資料。</p>
<p>兩者的判準是這份 log 表達的是「事件序列」還是「最終狀態」。訂單建立、付款完成、商品瀏覽這類事件、每一筆都是獨立事實、用 <code>delete</code>；使用者個人設定、商品庫存當前值、CDC 同步出來的資料表鏡像這類「同一個 key 不斷被覆寫、只關心最新值」的資料、用 <code>compact</code>。Kafka 內部的 <code>__consumer_offsets</code> topic 就是 compact——它只需要每個 consumer group 的最新 offset、不需要歷史 commit 記錄。</p>
<p>兩者可以同時開（<code>cleanup.policy=compact,delete</code>）：先按 key 壓縮保留最新值、同時對壓縮後的結果再套時間 / 容量上限。用 <code>kafka-configs.sh</code> 切換時、逗號分隔的值要用中括號群組、否則會被解析成兩個獨立 config：</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">kafka-configs.sh --alter --entity-type topics --entity-name ret-delete <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --add-config <span class="s1">&#39;cleanup.policy=[compact,delete]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Completed updating config for topic ret-delete.</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># describe: cleanup.policy=compact,delete</span></span></span></code></pre></div><h2 id="log-compaction-用最新值取代歷史">Log compaction 用最新值取代歷史</h2>
<p>Log compaction 的核心責任是讓一個 topic 收斂成「每個 key 的最新狀態」、同時保有 Kafka 的 log 重播能力。它的運作方式是背景的 log cleaner 執行緒掃描已 roll 的 segment、對每個 key 只保留 offset 最大的那筆、把同 key 的舊版本標記移除、再把存活的記錄重寫成新 segment。Compaction 後、新加入的 consumer 從頭讀一次、拿到的就是整個 keyspace 的最新快照、而非完整變更歷史。</p>
<p>實機驗證最直接：建一個 compact topic、對 3 個 key 各寫 2 個版本（舊值在前、新值在後）、等 compaction 跑完、從頭消費：</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">kafka-topics.sh --create --topic ret-compact --partitions <span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --config cleanup.policy<span class="o">=</span>compact <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --config min.cleanable.dirty.ratio<span class="o">=</span>0.01 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --config segment.ms<span class="o">=</span><span class="m">5000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --config delete.retention.ms<span class="o">=</span><span class="m">100</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</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"># 寫 k1/k2/k3 各舊值一筆、再各新值一筆（key:value 用冒號分隔）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;k1:v1-old\nk2:v1-old\nk3:v1-old\nk1:v2-new\nk2:v2-new\nk3:v2-new\n&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  kafka-console-producer.sh --topic ret-compact <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --property parse.key<span class="o">=</span><span class="nb">true</span> --property key.separator<span class="o">=</span>: <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 等 segment roll + compaction，再從頭消費</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">kafka-console-consumer.sh --topic ret-compact --from-beginning <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --property print.key<span class="o">=</span><span class="nb">true</span> --property print.offset<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --timeout-ms <span class="m">6000</span> --bootstrap-server localhost:9092
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># Offset:3  k1  v2-new</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># Offset:4  k2  v2-new</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># Offset:5  k3  v2-new</span></span></span></code></pre></div><p>寫進 6 筆、從頭只讀到 3 筆——k1/k2/k3 的 <code>v1-old</code>（offset 0-2）被壓縮移除、只留每個 key 的 <code>v2-new</code>。關鍵細節：offset 沒有重新編號、留存記錄保留原始 offset（3、4、5）、log 的位置語意不變、其他 consumer 的 offset 進度不會錯位。</p>
<p>Compaction 的觸發不是即時的、由幾個參數共同決定。<code>min.cleanable.dirty.ratio</code> 是「髒比例」門檻、髒記錄（已被新版本取代但還沒清掉的舊版本）佔 log 比例超過這個值、cleaner 才會處理該 partition、預設 0.5（驗證時調成 0.01 加速觸發）。<code>segment.ms</code> 控制 active segment 多久 roll、只有 roll 過的 segment 能被 compact。<code>delete.retention.ms</code> 控制 tombstone（value 為 null 的刪除標記）保留多久——compaction topic 用 null value 表示「這個 key 已刪除」、tombstone 要保留夠久讓所有 consumer 都讀到刪除事件、之後才清掉。</p>
<p>Tombstone 是 compaction 表達「刪除」的方式：寫一筆 key 存在、value 為 null 的記錄、compaction 會把該 key 的所有歷史連同這筆 tombstone 在 <code>delete.retention.ms</code> 之後一起清除。這讓 compact topic 能表達「key 從存在到被刪」的完整生命週期、而不只是「永遠累積最新值」。</p>
<h2 id="tiered-storage-讓容量與保留期解耦">Tiered Storage 讓容量與保留期解耦</h2>
<blockquote>
<p>以下 tiered storage 段落依 Apache Kafka 官方文件（KIP-405）與 Pinterest / LinkedIn 公開案例敘述、未在本文的 KRaft 單節點環境實機驗證。Apache Kafka 的原生 tiered storage（<code>remote.storage.enable</code>）在當前版本屬 early-access、需要額外的 RemoteStorageManager plugin 與 broker 設定；正式採用前以官方文件版本標註為準。</p></blockquote>
<p>Tiered storage 的核心責任是把 broker 的「儲存容量」跟「保留期長度」解耦。傳統 Kafka 的保留期受限於 broker 本機磁碟：想保留 30 天、就得讓每個 broker 的 local disk 容納 30 天的全量資料、retention 拉長等於 broker 數量或單機磁碟線性增長、而 broker 的 CPU / 記憶體 / 網路其實沒用到那麼多。Tiered storage 把 log 分成兩層：熱資料（近期、頻繁讀）留在 broker local disk（local tier）、冷資料（過期門檻之外、偶爾 replay）卸載到遠端物件儲存如 S3（remote tier）。Broker 只需放得下熱資料、保留期可以拉到數月甚至更久、成本變成 S3 的物件儲存費而非 broker 機群。</p>
<p>分層的觸發由 <code>local.retention.ms</code> / <code>local.retention.bytes</code>（本機保留多久 / 多大、超過就卸到 remote）跟整體的 <code>retention.ms</code> / <code>retention.bytes</code>（含 remote 的總保留邊界、超過才真正刪除）共同界定。一筆訊息的生命週期變成：寫入 local tier、超過 local retention 卸到 remote tier、超過整體 retention 從 remote 刪除。Replay window 因此可以遠大於 broker local disk 容量。</p>
<p>讀取路徑分熱冷兩條、效能特性不同。Consumer 讀近期 offset、資料在 local tier、走的是 Kafka 一向的 page cache + 順序讀路徑、低延遲高吞吐。Consumer 讀很舊的 offset（例如出事後從幾週前重播）、資料在 remote tier、broker 要先從 S3 把對應 segment 拉回來才能 serve、第一次讀的延遲明顯高於熱路徑、吞吐受 S3 頻寬與 broker 拉取並行度限制。這個熱冷讀差異是 tiered storage 的核心取捨——也是故障演練要處理的場景。</p>
<p>業界對 tiered storage 有兩條不同的工程路線、對應不同的 broker 角色定位：</p>
<table>
  <thead>
      <tr>
          <th>路線</th>
          <th>broker 角色</th>
          <th>代表案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Broker-coupled（KIP-405 原生）</td>
          <td>broker 仍是 remote 讀的熱路徑、代理拉取</td>
          <td>Apache Kafka 原生 tiered storage</td>
      </tr>
      <tr>
          <td>Broker-decoupled</td>
          <td>consumer 直接從 S3 拉、broker 不在熱路徑</td>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">3.C11 Pinterest Tiered Storage</a></td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">Pinterest 的 broker-decoupled 做法</a>把 ~200 TB/day 熱資料卸到 S3、讓 consumer 直接從 S3 拉冷資料、broker 不再是冷讀的熱路徑。它揭露的設計判讀是「broker 運算資源」跟「跨 AZ 網路成本」其實該分開治理、而不是綁在 broker 容量擴張上——保留期變長不該等於 broker 機群變大。</p>
<p><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">LinkedIn 的分層叢集策略</a>是另一個層次的「分層」：把不同業務特性與可靠性需求的 workload 拆到不同叢集（依關鍵程度分群、例如關鍵 / 一般 / 實驗性，分層名稱為示意而非案例原文用詞）、避免混在同一叢集時故障與資源競爭互相放大。這裡的「分層」指叢集隔離、不是儲存的冷熱分層。兩種「分層」常被混談、但解的是不同問題：tiered storage 解單一 topic 的儲存成本、tiered clusters 解多 workload 的隔離治理。</p>
<h2 id="故障演練">故障演練</h2>
<h3 id="retention-太短replay-window-不夠補事故">Retention 太短、replay window 不夠補事故</h3>
<p><strong>徵兆</strong>：下游 consumer 出 bug、產出錯誤的衍生資料、幾天後才被對帳發現；要從原始事件重播修復時、發現最舊的事件已經被刪、replay 從某個時間點之後才有資料、之前的修不回來。</p>
<p><strong>根因</strong>：retention.ms 設得比「事故從發生到偵測到開始修復的最長時間」短。Replay window 由 broker retention 與 consumer checkpoint 共同界定、retention 是其物理上限；偵測延遲一旦超過 retention、要補算時原始事件已過期。常見的隱性誘因是把 retention 按「正常 consumer 跟得上的進度」來設（例如 consumer 通常落後幾分鐘、就設 1 天保險）、卻沒按「最壞情況下多久才會發現問題」來設。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>把 retention.ms 對齊事故偵測到修復的最長時間、而非 consumer 正常落後量；對帳 / 審計類 pipeline 的偵測週期常以天計、retention 要跟著拉到對應天數。</li>
<li>對「偵測延遲可能很長」的關鍵 topic、在下游另留可重算的來源（資料庫快照、上游 source of truth）、不把 Kafka retention 當唯一補償依據。</li>
<li>用 <code>kafka-configs.sh --alter</code> 動態延長 retention 是即時生效的、但只對「還沒被刪」的訊息有用——已刪的救不回來；所以調整要趁事故升級前、發現偵測週期被低估的當下就改、不是等出事才改。</li>
<li>Replay 邊界對齊見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 Event Contract 與 Replay Boundary</a>：replay 要能指定 time range、超出 retention 的 time range 直接無效。</li>
</ol>
<h3 id="compaction-開了磁碟卻沒回收">Compaction 開了、磁碟卻沒回收</h3>
<p><strong>徵兆</strong>：topic 設了 <code>cleanup.policy=compact</code>、預期同 key 舊版本會被清掉、磁碟用量卻持續上漲、<code>--describe</code> 看 partition log 一直變大；從頭消費仍讀到大量同 key 的歷史版本。</p>
<p><strong>根因</strong>：compaction 觸發條件沒滿足。log cleaner 只處理已 roll 的 segment、active segment 永遠不壓縮；<code>min.cleanable.dirty.ratio</code> 預設 0.5、髒比例沒到一半 cleaner 不動手；如果寫入集中在少數 key、active segment 遲遲不 roll（segment.bytes / segment.ms 都沒到）、髒記錄全積在 active segment 裡、compaction 看不到它們。另一個常見原因是 broker 的 log cleaner 執行緒數（<code>log.cleaner.threads</code>）不足以跟上高寫入量、cleaner backlog 累積。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 active segment 會適時 roll：對寫入量不大但需要及時壓縮的 topic、設 <code>segment.ms</code>（例如數小時）強制 roll、讓髒記錄離開 active segment 進入可壓縮範圍。</li>
<li>視壓縮急迫度調 <code>min.cleanable.dirty.ratio</code>：要更積極壓縮就調低（驗證時用 0.01）、但調太低會讓 cleaner 頻繁重寫 segment、增加 I/O——這是壓縮及時性跟 cleaner 開銷的取捨。</li>
<li>監控 cleaner backlog：看 broker 的 <code>log-cleaner</code> 相關 metric、backlog 持續成長代表 cleaner 執行緒不夠、加 <code>log.cleaner.threads</code>。</li>
<li>確認沒有把 compact 用在「其實該 delete」的事件流上——事件流每筆 key 多半唯一、compaction 沒有舊版本可壓、磁碟自然不會降；那種情況該用 <code>delete</code> 加 retention。</li>
</ol>
<h3 id="cold-tier-讀延遲拖垮-replay">Cold tier 讀延遲拖垮 replay</h3>
<p><strong>徵兆</strong>：開了 tiered storage、平時讀近期資料正常、一旦發起從幾週前的舊 offset 大規模 replay、consumer 的吞吐驟降、p99 拉取延遲飆高、broker S3 拉取頻寬打滿、同 broker 上其他正常 consumer 也跟著受影響。</p>
<p><strong>根因</strong>：舊 offset 的資料在 remote tier、每次讀要先從 S3 把 segment 拉回 broker、第一次冷讀延遲遠高於 local tier 的順序讀。大規模 replay 等於一次要從 S3 拉大量冷 segment、S3 頻寬與 broker 拉取並行成為瓶頸；broker-coupled 架構下這些拉取流量全經過 broker、會排擠到熱路徑的正常服務。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>把大規模冷 replay 排到低流量時段、避免跟線上熱路徑爭 broker 資源與 S3 頻寬。</li>
<li>控制 replay 的並行度與範圍：依 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">replay boundary</a> 指定 time range / tenant / partition、分批拉冷資料、不要一次全量回放整個保留期。</li>
<li>評估 broker-decoupled 架構（如 <a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">Pinterest 做法</a>）：consumer 直接從 S3 拉冷資料、把冷讀流量從 broker 熱路徑移開、保護線上服務。</li>
<li>容量規劃把「冷讀延遲」算進 RTO：replay window 拉很長能補很久以前的事故、但補的速度受 cold tier 吞吐限制、事故修復時間估算要把這段拉取時間算進去。</li>
</ol>
<h3 id="retentionbytes-在高流量時段提早刪">retention.bytes 在高流量時段提早刪</h3>
<p><strong>徵兆</strong>：retention.ms 明明設了 7 天、某次流量突增後、consumer 卻發現幾小時前的事件就已經被刪、replay 拿不到本該還在的資料；earliest offset 在沒人預期的時候大幅前移。</p>
<p><strong>根因</strong>：retention.ms 與 retention.bytes 同時設時是「誰先觸發誰生效」。流量突增讓 partition log 在遠不到 7 天時就撞到 retention.bytes 容量上限、容量軸先觸發、舊 segment 被提前刪除——時間軸的 7 天承諾在高流量下失效。常見於「按平均流量估容量上限、卻遇到尖峰流量」、或多個 topic 共享磁碟時為了保護磁碟把每 topic 容量上限壓得偏低。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>釐清這個 topic 的保留承諾是時間還是容量主導：以 replay window 為準的關鍵 topic、容量上限要按「尖峰流量 × 保留天數」估、而非平均流量、否則尖峰時容量軸會偷走時間承諾。</li>
<li>監控 earliest offset 與 log 大小的變化率：earliest offset 在非預期時間前移、就是 retention.bytes 提前觸發的訊號、加進告警。</li>
<li>要硬保證時間保留、就把 retention.bytes 設成 -1（不限容量、純時間軸）、改用獨立的磁碟告警與容量規劃來防磁碟塞爆、而不是用 retention.bytes 兼做兩件事。</li>
<li>評估 tiered storage：把保留壓力從 broker local disk 移到 remote tier、local 只留熱資料、就不必為了保護 broker 磁碟而把 retention.bytes 壓低、時間承諾不再被容量上限侵蝕。</li>
</ol>
<h2 id="容量與成本">容量與成本</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算與判讀</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local disk 用量</td>
          <td>partition 數 × 單 partition log 大小 × replication factor</td>
          <td>接近磁碟上限時 retention.bytes 會提前砍時間承諾</td>
      </tr>
      <tr>
          <td>保留期 vs 成本</td>
          <td>純 local 時 retention 線性推高 broker 磁碟成本</td>
          <td>數月保留 + 純 local = broker 機群為冷資料買單</td>
      </tr>
      <tr>
          <td>Tiered remote 成本</td>
          <td>S3 物件儲存費 + 冷讀時的拉取 / egress 流量費</td>
          <td>跨 AZ / 跨 region 冷讀 egress 成本易被低估</td>
      </tr>
      <tr>
          <td>Retention 檢查延遲</td>
          <td>過期到實際刪除最長一個 <code>log.retention.check.interval.ms</code>（預設 5 分）</td>
          <td>容量規劃要預留 overshoot 緩衝</td>
      </tr>
      <tr>
          <td>Compaction 開銷</td>
          <td>cleaner 重寫 segment 的 I/O、隨 dirty.ratio 調低而上升</td>
          <td>dirty.ratio 過低 = cleaner 頻繁重寫、I/O 壓力升</td>
      </tr>
      <tr>
          <td>Cold replay 吞吐</td>
          <td>受 remote tier（S3）頻寬與 broker 拉取並行度限制</td>
          <td>大規模 cold replay 排低流量時段、分批進行</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>事件流 topic 用 <code>delete</code>、retention.ms 對齊事故偵測到修復的最長時間、retention.bytes 設 -1 或按尖峰流量估、不讓容量軸偷走時間承諾。</li>
<li>狀態快照 / CDC 鏡像 topic 用 <code>compact</code>、確認 active segment 會適時 roll、監控 cleaner backlog。</li>
<li>需要長保留期（數月以上）且 broker 磁碟成本敏感時、評估 tiered storage、把冷資料移到 S3、broker 只放熱資料。</li>
<li>任何 retention 調整前先確認當前生效層級（<code>kafka-configs.sh --describe</code> 看 synonyms）、避免 broker 預設與 topic 動態配置混淆。</li>
</ul>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="跟-replay-邊界對齊">跟 replay 邊界對齊</h3>
<p>Retention 是 <a href="/blog/backend/knowledge-cards/replay-window/" data-link-title="Replay Window" data-link-desc="說明事件可重播的時間或 offset 範圍邊界，由 retention 與 checkpoint 決定">replay window</a> 的物理上限、但 replay 能不能正確執行還要看 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">event contract</a> 是否齊備（event id / schema version / occurred time / dedup key）。保留策略設計要跟 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 Event Contract 與 Replay Boundary</a> 一起看：retention 決定「能不能讀到」、event contract 決定「讀到了能不能正確重播」、兩者缺一 replay 都不成立。相關概念見 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 與 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 知識卡。</p>
<h3 id="跟分層叢集治理對位">跟分層叢集治理對位</h3>
<p>本文的 tiered storage 解的是單一 topic 的儲存成本；<a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn 分層叢集</a>解的是多 workload 的隔離——把不同可靠性需求的 topic 拆到不同叢集、避免資源競爭互相放大。保留策略在分層叢集裡會按層差異化：critical 叢集拉長 retention 保 replay、experimental 叢集縮短 retention 控成本。</p>
<h3 id="跟-broker-decoupled-架構的取捨">跟 broker-decoupled 架構的取捨</h3>
<p><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">3.C11 Pinterest broker-decoupled tiered storage</a> 把冷讀流量從 broker 熱路徑移開、是「cold tier 讀延遲拖垮 replay」故障演練的架構級解法；它跟 <a href="/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/" data-link-title="3.C12 Pinterest：Shallow Mirror 優化 MirrorMaker" data-link-desc="Pinterest 跨 3 region MirrorMaker、原版解壓&#43;重壓造成 CPU/memory 2-10x spike、改 RecordBatch 層淺迭代。">3.C12 Pinterest Shallow Mirror</a> 揭露的「跨區同步是 CPU + memory + 網路三維壓力」一起、構成 Pinterest 在儲存與複製兩條路徑上的成本治理。</p>
<h3 id="回上游">回上游</h3>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a>（「Tiered storage」與「Cross-region 與分層叢集」段）</li>
<li>平行 deep article：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">consumer rebalance 與 lag 診斷</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">replication、ISR 與 exactly-once</a>（同 vendor 其他實作層議題）</li>
<li>下游能力：<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> / <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> overview 的 implementation-layer deep article、對應 overview「Classic queue vs Quorum queue vs Stream」段。Overview 回答「RabbitMQ 該不該選、跟 Kafka / SQS 差在哪」、本文回答「選了 RabbitMQ 之後、同一個 broker 內三種 queue type 怎麼挑、各自的容量與故障形狀」。&lt;/p>&lt;/blockquote>
&lt;h2 id="同一個-broker三套儲存引擎">同一個 broker、三套儲存引擎&lt;/h2>
&lt;p>RabbitMQ 的 queue 由三種 &lt;em>儲存引擎&lt;/em> 構成、共用同一套 AMQP 協議與 management 介面。Queue type 決定訊息怎麼持久化、怎麼跨節點複製、消費後是否保留 — 這些差異在宣告 queue 的那一刻就鎖定、之後無法原地切換。選錯 queue type 的代價不是參數調整、是 &lt;em>重建 queue + 遷移 in-flight 訊息&lt;/em>。&lt;/p>
&lt;p>三種 type 各自承擔不同責任：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Classic queue&lt;/strong>：單節點的 durable / transient queue、訊息消費即刪除、是 RabbitMQ 最原始的工作隊列模型。跨節點高可用曾靠 &lt;em>mirrored queue&lt;/em>（鏡像複製）達成、但該機制在 3.x 已標記 deprecated、4.0 移除。&lt;/li>
&lt;li>&lt;strong>Quorum queue&lt;/strong>：Raft 共識協議實作的 replicated queue、跨節點維持強一致的訊息狀態、設計目標是 &lt;em>取代 mirrored queue&lt;/em> 提供可靠的工作隊列高可用。訊息仍是消費即刪除的隊列語意。&lt;/li>
&lt;li>&lt;strong>Stream&lt;/strong>：3.9 引入的 append-only log、訊息寫入後 &lt;em>不因消費而刪除&lt;/em>、多個 consumer 可從各自的 offset 重複讀取、retention 由時間 / 大小上限控制。語意接近 Kafka 的 partition log、但跑在 RabbitMQ 體系內、共用 AMQP 與專屬 stream protocol。&lt;/li>
&lt;/ul>
&lt;p>判讀起點是一個問題：訊息被消費後該不該保留。需要 replay、多 consumer 各自進度、長期事件流 → stream；訊息是一次性任務、處理完即丟、要跨節點不丟 → quorum；單節點夠用、可接受節點故障時該 queue 暫時不可用 → classic。&lt;/p>
&lt;p>本文用 RabbitMQ 3.13.7（OrbStack 單節點）實機驗證宣告語意差異。生產的跨節點行為（Raft 選舉、replica lag）單節點環境無法重現、相關段落標注來源。&lt;/p>
&lt;h2 id="三種-queue-type-的宣告語意差異實機驗證">三種 queue type 的宣告語意差異（實機驗證）&lt;/h2>
&lt;p>Queue type 由宣告時的 &lt;code>x-queue-type&lt;/code> argument 決定。三種 type 在同一 broker 宣告後、&lt;code>type&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">rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>q-classic &lt;span class="nv">durable&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>q-quorum &lt;span class="nv">durable&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="nv">arguments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{&amp;#34;x-queue-type&amp;#34;:&amp;#34;quorum&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">rabbitmqadmin &lt;span class="nb">declare&lt;/span> queue &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>q-stream &lt;span class="nv">durable&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="nv">arguments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{&amp;#34;x-queue-type&amp;#34;:&amp;#34;stream&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">rabbitmqctl list_queues name &lt;span class="nb">type&lt;/span> durable leader members&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機輸出（節錄、單節點所以 leader / members 都是同一節點）：&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">name type durable leader members
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">q-classic classic true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">q-quorum quorum true rabbit@&amp;lt;node&amp;gt; [rabbit@&amp;lt;node&amp;gt;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">q-stream stream true rabbit@&amp;lt;node&amp;gt; [rabbit@&amp;lt;node&amp;gt;]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個關鍵差異在這裡浮現。&lt;/p>
&lt;p>第一、&lt;strong>quorum 與 stream 強制 durable&lt;/strong>。Classic queue 可宣告為 transient（&lt;code>durable=false&lt;/code>、broker 重啟後消失、適合臨時 RPC reply queue）；quorum 與 stream 不允許 transient — 嘗試宣告會直接被拒：&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">*** invalid property &amp;#39;non-durable&amp;#39; for queue &amp;#39;q-quorum-nondur&amp;#39; in vhost &amp;#39;/&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">*** invalid property &amp;#39;non-durable&amp;#39; for queue &amp;#39;q-stream-nondur&amp;#39; in vhost &amp;#39;/&amp;#39;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個限制反映設計意圖：quorum 與 stream 存在的理由是 &lt;em>資料安全&lt;/em>、transient 模式與該目標矛盾、所以從宣告層就封死。Classic queue 保留 transient 選項、是因為它要同時服務「臨時隊列」與「持久隊列」兩種場景。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> overview 的 implementation-layer deep article、對應 overview「Classic queue vs Quorum queue vs Stream」段。Overview 回答「RabbitMQ 該不該選、跟 Kafka / SQS 差在哪」、本文回答「選了 RabbitMQ 之後、同一個 broker 內三種 queue type 怎麼挑、各自的容量與故障形狀」。</p></blockquote>
<h2 id="同一個-broker三套儲存引擎">同一個 broker、三套儲存引擎</h2>
<p>RabbitMQ 的 queue 由三種 <em>儲存引擎</em> 構成、共用同一套 AMQP 協議與 management 介面。Queue type 決定訊息怎麼持久化、怎麼跨節點複製、消費後是否保留 — 這些差異在宣告 queue 的那一刻就鎖定、之後無法原地切換。選錯 queue type 的代價不是參數調整、是 <em>重建 queue + 遷移 in-flight 訊息</em>。</p>
<p>三種 type 各自承擔不同責任：</p>
<ul>
<li><strong>Classic queue</strong>：單節點的 durable / transient queue、訊息消費即刪除、是 RabbitMQ 最原始的工作隊列模型。跨節點高可用曾靠 <em>mirrored queue</em>（鏡像複製）達成、但該機制在 3.x 已標記 deprecated、4.0 移除。</li>
<li><strong>Quorum queue</strong>：Raft 共識協議實作的 replicated queue、跨節點維持強一致的訊息狀態、設計目標是 <em>取代 mirrored queue</em> 提供可靠的工作隊列高可用。訊息仍是消費即刪除的隊列語意。</li>
<li><strong>Stream</strong>：3.9 引入的 append-only log、訊息寫入後 <em>不因消費而刪除</em>、多個 consumer 可從各自的 offset 重複讀取、retention 由時間 / 大小上限控制。語意接近 Kafka 的 partition log、但跑在 RabbitMQ 體系內、共用 AMQP 與專屬 stream protocol。</li>
</ul>
<p>判讀起點是一個問題：訊息被消費後該不該保留。需要 replay、多 consumer 各自進度、長期事件流 → stream；訊息是一次性任務、處理完即丟、要跨節點不丟 → quorum；單節點夠用、可接受節點故障時該 queue 暫時不可用 → classic。</p>
<p>本文用 RabbitMQ 3.13.7（OrbStack 單節點）實機驗證宣告語意差異。生產的跨節點行為（Raft 選舉、replica lag）單節點環境無法重現、相關段落標注來源。</p>
<h2 id="三種-queue-type-的宣告語意差異實機驗證">三種 queue type 的宣告語意差異（實機驗證）</h2>
<p>Queue type 由宣告時的 <code>x-queue-type</code> argument 決定。三種 type 在同一 broker 宣告後、<code>type</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">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>q-classic <span class="nv">durable</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>q-quorum  <span class="nv">durable</span><span class="o">=</span><span class="nb">true</span> <span class="nv">arguments</span><span class="o">=</span><span class="s1">&#39;{&#34;x-queue-type&#34;:&#34;quorum&#34;}&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>q-stream  <span class="nv">durable</span><span class="o">=</span><span class="nb">true</span> <span class="nv">arguments</span><span class="o">=</span><span class="s1">&#39;{&#34;x-queue-type&#34;:&#34;stream&#34;}&#39;</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">rabbitmqctl list_queues name <span class="nb">type</span> durable leader members</span></span></code></pre></div><p>實機輸出（節錄、單節點所以 leader / members 都是同一節點）：</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">name       type     durable  leader              members
</span></span><span class="line"><span class="ln">2</span><span class="cl">q-classic  classic  true
</span></span><span class="line"><span class="ln">3</span><span class="cl">q-quorum   quorum   true     rabbit@&lt;node&gt;       [rabbit@&lt;node&gt;]
</span></span><span class="line"><span class="ln">4</span><span class="cl">q-stream   stream   true     rabbit@&lt;node&gt;       [rabbit@&lt;node&gt;]</span></span></code></pre></div><p>兩個關鍵差異在這裡浮現。</p>
<p>第一、<strong>quorum 與 stream 強制 durable</strong>。Classic queue 可宣告為 transient（<code>durable=false</code>、broker 重啟後消失、適合臨時 RPC reply queue）；quorum 與 stream 不允許 transient — 嘗試宣告會直接被拒：</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">*** invalid property &#39;non-durable&#39; for queue &#39;q-quorum-nondur&#39; in vhost &#39;/&#39;
</span></span><span class="line"><span class="ln">2</span><span class="cl">*** invalid property &#39;non-durable&#39; for queue &#39;q-stream-nondur&#39; in vhost &#39;/&#39;</span></span></code></pre></div><p>這個限制反映設計意圖：quorum 與 stream 存在的理由是 <em>資料安全</em>、transient 模式與該目標矛盾、所以從宣告層就封死。Classic queue 保留 transient 選項、是因為它要同時服務「臨時隊列」與「持久隊列」兩種場景。</p>
<p>第二、<strong>quorum 與 stream 有 leader / members、classic 沒有</strong>。Classic queue 的訊息只存在宣告它的節點上（mirrored policy 另算）；quorum 與 stream 在設計上就是 <em>cluster-aware</em> 的 replicated 結構、leader 處理讀寫、members 列出 replica 所在節點。單節點環境下 members 只有一個、但欄位本身揭露了複製拓樸的存在。</p>
<p>Stream 的 retention 與 segment 參數在宣告時設定、宣告後可查：</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">rabbitmqadmin <span class="nb">declare</span> queue <span class="nv">name</span><span class="o">=</span>q-stream-ret <span class="nv">durable</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="nv">arguments</span><span class="o">=</span><span class="s1">&#39;{&#34;x-queue-type&#34;:&#34;stream&#34;,&#34;x-max-length-bytes&#34;:20000000000,&#34;x-max-age&#34;:&#34;7D&#34;,&#34;x-stream-max-segment-size-bytes&#34;:100000000}&#39;</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">rabbitmqctl list_queues name <span class="nb">type</span> arguments</span></span></code></pre></div>




<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">q-stream-ret  stream  [{&#34;x-max-age&#34;,&#34;7D&#34;},{&#34;x-max-length-bytes&#34;,20000000000},
</span></span><span class="line"><span class="ln">2</span><span class="cl">                       {&#34;x-queue-type&#34;,&#34;stream&#34;},{&#34;x-stream-max-segment-size-bytes&#34;,100000000}]</span></span></code></pre></div><p><code>x-max-age</code>（保留 7 天）與 <code>x-max-length-bytes</code>（保留 20GB）是 stream 獨有的 retention 控制 — classic 與 quorum 沒有這個概念、因為它們消費即刪除、不存在「保留多久」的問題。Quorum queue 對應的是 <code>x-delivery-limit</code>（投遞次數上限、超過進 dead-letter）這類 <em>重試治理</em> 參數、而非 retention：</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">q-quorum-dl  quorum  [{&#34;x-delivery-limit&#34;,5},{&#34;x-queue-type&#34;,&#34;quorum&#34;}]</span></span></code></pre></div><p>宣告參數的差異就是責任邊界的縮影：stream 的參數圍繞「保留多少歷史」、quorum 的參數圍繞「重試到第幾次放棄」、classic 兩者都精簡。</p>
<h2 id="三軸選型判讀">三軸選型判讀</h2>
<p>Queue type 的選擇由三個軸決定：消費後是否保留（retention / replay）、跨節點一致性需求、記憶體與 throughput 成本。</p>
<table>
  <thead>
      <tr>
          <th>判讀軸</th>
          <th>Classic</th>
          <th>Quorum</th>
          <th>Stream</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>消費語意</td>
          <td>消費即刪除</td>
          <td>消費即刪除</td>
          <td>消費不刪除、offset 各自獨立</td>
      </tr>
      <tr>
          <td>Replay</td>
          <td>不支援</td>
          <td>不支援</td>
          <td>支援、consumer 可重設 offset 重讀</td>
      </tr>
      <tr>
          <td>跨節點一致性</td>
          <td>無（mirrored deprecated）</td>
          <td>Raft 強一致、majority 寫入才 ack</td>
          <td>Leader-follower 複製、append-only</td>
      </tr>
      <tr>
          <td>高 throughput</td>
          <td>中（單節點 fsync 上限）</td>
          <td>中（Raft majority round-trip 成本）</td>
          <td>高（順序寫 log、批次讀）</td>
      </tr>
      <tr>
          <td>記憶體成本</td>
          <td>高（訊息常駐記憶體、lazy 例外）</td>
          <td>中（on-disk 為主、index 在記憶體）</td>
          <td>低（log 在磁碟、讀靠 page cache）</td>
      </tr>
      <tr>
          <td>典型場景</td>
          <td>單節點任務隊列、臨時 RPC reply</td>
          <td>跨節點不可丟的工作隊列</td>
          <td>事件流、多 consumer、需要 replay 的審計</td>
      </tr>
  </tbody>
</table>
<h3 id="消費後是否保留retention-與-replay">消費後是否保留：retention 與 replay</h3>
<p>Stream 與 classic / quorum 的根本分界是訊息生命週期。Classic 與 quorum 是 <em>隊列</em>：訊息被 ack 後從 queue 移除、後到的 consumer 看不到歷史。Stream 是 <em>log</em>：訊息寫入後常駐到 retention 上限為止、consumer 各自維護 offset、可以從 offset 0 重讀整段歷史、也可以從 timestamp 起讀。</p>
<p>實機可觀察到 stream 的訊息在 publish 後保留在 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">rabbitmqadmin publish <span class="nv">exchange</span><span class="o">=</span>amq.default <span class="nv">routing_key</span><span class="o">=</span>q-stream <span class="nv">payload</span><span class="o">=</span><span class="s2">&#34;msg1&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rabbitmqadmin publish <span class="nv">exchange</span><span class="o">=</span>amq.default <span class="nv">routing_key</span><span class="o">=</span>q-stream <span class="nv">payload</span><span class="o">=</span><span class="s2">&#34;msg2&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">rabbitmqadmin publish <span class="nv">exchange</span><span class="o">=</span>amq.default <span class="nv">routing_key</span><span class="o">=</span>q-stream <span class="nv">payload</span><span class="o">=</span><span class="s2">&#34;msg3&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">rabbitmqctl list_queues name <span class="nb">type</span> messages messages_ready</span></span></code></pre></div>




<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">q-stream   stream  3  3</span></span></code></pre></div><p>對 classic queue、同樣 publish 後 consumer ack 一次、訊息歸零；對 stream、即使一個 consumer 讀完、<code>messages</code> 仍維持 3、因為訊息保留供其他 consumer 與未來 replay。這個差異決定了選型：需要「新上線的 consumer 補讀歷史事件」「同一份事件流餵給多個下游」「審計與重算」→ stream 是唯一選項；只要「一個任務交給一個 worker 處理一次」→ classic 或 quorum、不要用 stream（log 保留會吃磁碟、且隊列語意更貼合任務分派）。</p>
<p>需要在 RabbitMQ 體系外做大規模事件流（跨團隊 schema 治理、tiered storage、生態工具）時、stream 不是終點、改評估 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。Stream 的定位是「已經在用 RabbitMQ、需要 replay 但不想引入第二套 broker」。</p>
<h3 id="跨節點一致性mirrored-的退場與-quorum-的接手">跨節點一致性：mirrored 的退場與 quorum 的接手</h3>
<p>Classic queue 在單節點上沒有複製。早期要跨節點高可用、靠 <em>mirrored queue</em> — 一個 master、多個 mirror、master 寫入同步到所有 mirror。這個機制的問題在 <a href="/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30 Runtastic</a> 揭露：mirror 數越多、每筆訊息的網路放大越大、規模化時網路元件先被壓垮。RabbitMQ 3.x 已將 mirrored queue 標記 deprecated、4.0 移除。</p>
<p>Quorum queue 用 Raft 共識取代 mirroring。差異在「同步多少 replica 才算寫成功」：mirrored queue 要求 <em>所有</em> mirror 同步（全量放大）；Raft 只要求 <em>majority</em>（多數派）寫入即 ack，少數派慢或暫時離線不阻塞寫入。majority 機制讓 quorum queue 在「容忍少數節點故障」與「寫入延遲」之間取得 mirrored 做不到的平衡。</p>
<p>代價是 Raft 的 round-trip 成本：每筆訊息要等多數派落盤、單筆延遲高於 classic 單節點 fsync。所以 quorum queue 適合「不可丟、可接受中等延遲」的工作隊列、不適合追求極致低延遲的場景。</p>
<h3 id="記憶體與-throughput-成本">記憶體與 throughput 成本</h3>
<p>Classic queue 的歷史包袱是訊息傾向常駐記憶體、queue 堆積時記憶體壓力大（lazy queue 模式可緩解、但仍是 classic 的調校負擔）。Quorum queue 預設 on-disk 為主、記憶體只放 index 與近期訊息、堆積時記憶體曲線比 classic 平緩。Stream 是 append-only log、寫入是順序磁碟 I/O、讀取靠 OS page cache、是三者中記憶體效率最高、throughput 最高的 — 順序寫與批次讀讓它在高吞吐事件流場景接近 Kafka 的量級。</p>
<p>throughput 排序大致是 stream &gt; classic ≈ quorum（quorum 因 Raft round-trip 略低於單節點 classic、但換得一致性）。選型時 throughput 不該是唯一軸：stream throughput 高但語意是 log、用它跑任務隊列會錯配；quorum throughput 中但提供 classic 給不了的高可用。</p>
<h2 id="故障演練">故障演練</h2>
<p>三種 queue type 的故障形狀完全不同。以下四個場景對應實際遷移與運維會踩的坑。</p>
<h3 id="mirrored-queue-的網路放大成本">Mirrored queue 的網路放大成本</h3>
<p><strong>徵兆</strong>：流量暴增期間、RabbitMQ cluster 出現高延遲與間歇中斷、但 CPU 與磁碟未飽和；performance test 指向網路元件被壓垮。這正是 <a href="/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30 Runtastic</a> 2020 lockdown 期間的情況。</p>
<p><strong>根因</strong>：mirrored queue 把每筆訊息同步到 <em>所有</em> mirror。一個 master + 2 mirror 的 queue、每筆 publish 產生 2 份額外的跨節點複製流量；mirror 數與訊息量相乘、網路頻寬隨規模線性放大。可靠性看似免費（多一個 mirror 就多一份備援）、實際成本藏在網路層、平時不顯、流量尖峰才爆。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>量化 mirror 的網路成本</strong>：mirror 數不是越多越安全、每個 mirror 都是固定的複製流量稅。生產上 mirror 數很少需要超過總節點的 majority。</li>
<li><strong>遷移到 quorum queue</strong>：Raft 的 majority 寫入取代全量同步、把網路放大從「mirror 數」降到「majority round-trip」。Runtastic case 是「為何該遷 quorum」的典型動機。</li>
<li><strong>監控網路而非只看 CPU / 磁碟</strong>：mirrored queue 的瓶頸常在網路、用 Prometheus integration 把跨節點複製流量納入告警基線。</li>
</ol>
<h3 id="quorum-queue-的-quorum-loss">Quorum queue 的 quorum loss</h3>
<p><strong>徵兆</strong>：cluster 有節點故障後、某些 quorum queue 變成不可寫、publisher confirm 卡住超時、<code>rabbitmq-diagnostics check_if_node_is_quorum_critical</code> 報警。</p>
<blockquote>
<p>以下跨節點行為依官方文件、單節點環境未實機驗證。</p></blockquote>
<p><strong>根因</strong>：quorum queue 靠 Raft majority 運作。一個 3-replica 的 queue 容忍 1 個節點故障（剩 2 個構成 majority）；故障 2 個節點時、剩 1 個無法構成多數派、queue 進入 <em>無 leader</em> 狀態、拒絕寫入以保證一致性。這是 Raft 的設計選擇：寧可不可用、不可不一致。replica 數設成偶數（如 2 或 4）更糟 — 偶數的 majority 門檻不會提升容錯、反而浪費資源。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>replica 數設奇數</strong>：3 replica 容忍 1 故障、5 replica 容忍 2 故障。奇數讓 majority 計算最有效率。</li>
<li><strong>監控 quorum critical 狀態</strong>：<code>rabbitmq-diagnostics check_if_node_is_quorum_critical</code> 在「再掛一個節點就會失去 quorum」時提前告警、在維護重啟前先確認不會打破 majority。</li>
<li><strong>跨故障域分佈 replica</strong>：把 3 個 replica 放在不同 AZ / 機架、避免單一故障域同時帶走多數派。</li>
<li><strong>理解不可用是預期行為</strong>：quorum loss 時 queue 拒寫是 <em>正確</em> 的、不是 bug。恢復路徑是把故障節點拉回 cluster 重組 majority、不是強制覆寫。</li>
</ol>
<h3 id="stream-retention-超量">Stream retention 超量</h3>
<p><strong>徵兆</strong>：stream queue 所在節點磁碟使用率持續上升、最終觸發 disk alarm、broker 暫停所有 publisher；或 consumer 嘗試讀取較舊的 offset 時拿到「offset 不存在」、發現歷史訊息已被截斷。</p>
<p><strong>根因</strong>：stream 是 append-only log、訊息 <em>不因消費而刪除</em>、只靠 retention 上限（<code>x-max-age</code> 時間 / <code>x-max-length-bytes</code> 大小）回收。retention 設太寬、或寫入速率超過預估、log 持續長大直到塞滿磁碟。反過來 retention 設太緊、consumer 還沒讀到的舊訊息就被截斷、replay 場景拿不到完整歷史。Stream 的容量管理是「設定 retention」、不是「靠消費清空」 — 這跟隊列直覺相反。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>retention 雙保險</strong>：同時設 <code>x-max-age</code>（時間上限、對齊業務 replay 窗口、如 7 天）與 <code>x-max-length-bytes</code>（大小上限、對齊磁碟容量）。先到的條件先觸發截斷、避免單一維度失控。</li>
<li><strong>segment 大小對齊回收粒度</strong>：<code>x-stream-max-segment-size-bytes</code> 決定 log 分段大小、retention 以 segment 為單位回收。segment 太大、retention 觸發後一次釋放大量空間、磁碟曲線鋸齒；太小、segment 檔案數量爆炸。</li>
<li><strong>容量公式先算再設</strong>：預估 <code>寫入速率 × 訊息平均大小 × retention 時間</code>、確認低於節點磁碟可用空間的安全水位（如 70%）、再上線。</li>
<li><strong>monitor disk_free_limit</strong>：stream 節點的磁碟告警閾值要比一般節點更早、因為 stream 是磁碟密集型、disk alarm 觸發會凍結整個 broker 的 publisher。</li>
</ol>
<h3 id="classic--quorum-遷移的-in-flight-message">Classic → Quorum 遷移的 in-flight message</h3>
<p><strong>徵兆</strong>：把工作隊列從 classic（或 deprecated mirrored）遷到 quorum 時、切換瞬間有訊息遺失、或重複處理 — queue 重建期間 publisher 已經在發、consumer 還沒接上新 queue。</p>
<p><strong>根因</strong>：queue type 無法原地變更、遷移本質是 <em>建新 queue + 切流量 + 排空舊 queue</em>。最大的坑是 in-flight 訊息：舊 classic queue 裡還有未消費的訊息、若直接刪除舊 queue、這些訊息就丟了；若 publisher 提前切到新 queue、舊 queue 的 consumer 還在處理、就出現新舊兩條路徑並存的一致性窗口。<a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27 Zalando</a> 跨版本升級用 federation 過渡、正是為了平滑搬移而非硬切。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>新 queue 先建、binding 並存</strong>：用新 routing key 或新 queue 名建立 quorum queue、舊 classic queue 暫不刪。</li>
<li><strong>consumer 先切、publisher 後切</strong>：先讓 consumer 同時消費新舊兩個 queue、確認新 queue 路徑正常、再把 publisher 切到只發新 queue。順序顛倒（publisher 先切）會讓舊 queue 的 in-flight 訊息沒人消費。</li>
<li><strong>排空舊 queue 再刪</strong>：publisher 切換後、等舊 classic queue <code>messages</code> 歸零（用 <code>list_queues name messages</code> 確認）、才刪除舊 queue。</li>
<li><strong>依賴 idempotency 兜底</strong>：遷移窗口內訊息可能重複投遞、consumer 端的 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 是最後一道防線（語義誤配的後果見 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9</a>）、不要假設遷移零重複。</li>
<li><strong>用 federation / shovel 做大規模搬移</strong>：跨 cluster 或跨版本場景、用 federation upstream 把舊 cluster 訊息引流到新 cluster、避免一次性硬切（Zalando case 的做法）。</li>
</ol>
<h2 id="容量與成本規劃">容量與成本規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Classic</th>
          <th>Quorum</th>
          <th>Stream</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單筆寫入延遲</td>
          <td>低（單節點 fsync）</td>
          <td>中（Raft majority round-trip）</td>
          <td>低（順序 append、批次 ack）</td>
      </tr>
      <tr>
          <td>記憶體 / 訊息</td>
          <td>高（常駐、lazy 緩解）</td>
          <td>中（on-disk 為主 + index）</td>
          <td>低（log 在磁碟、靠 page cache）</td>
      </tr>
      <tr>
          <td>磁碟成長</td>
          <td>隨未消費堆積</td>
          <td>隨未消費堆積</td>
          <td>隨 retention 上限、消費不回收</td>
      </tr>
      <tr>
          <td>節點故障容忍</td>
          <td>無（該 queue 不可用）</td>
          <td>容忍少數派故障（3 replica 容 1）</td>
          <td>Leader 故障可切 follower</td>
      </tr>
      <tr>
          <td>適用規模上限訊號</td>
          <td>堆積導致記憶體壓力 / 需要跨節點 HA</td>
          <td>Raft 延遲成為瓶頸 / 超高吞吐</td>
          <td>事件流規模需要跨團隊 schema 治理</td>
      </tr>
      <tr>
          <td>超出後改走</td>
          <td>Quorum（要 HA）/ Stream（要 replay）</td>
          <td>Stream（要 replay）/ Kafka（要生態）</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（跨團隊事件平台）</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li><strong>單節點開發 / 臨時隊列</strong>：classic、最簡單、transient 模式適合 RPC reply。</li>
<li><strong>生產工作隊列、不可丟訊息</strong>：quorum、3 replica 跨 AZ、replica 數設奇數。</li>
<li><strong>事件流 / 多 consumer / 需要 replay</strong>：stream、retention 雙保險、磁碟容量先算。</li>
<li><strong>判斷該不該升級到 Kafka</strong>：當 stream 場景開始需要跨團隊 schema registry、tiered storage、或成熟的 streaming 生態工具時、stream 是過渡、Kafka 是終點。</li>
</ul>
<h2 id="整合與下一步">整合與下一步</h2>
<p>Queue type 的選擇與 RabbitMQ 其他能力交織：</p>
<ul>
<li><strong>回 vendor overview</strong>：三種 queue type 的取捨在 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ overview</a>「Classic queue vs Quorum queue vs Stream」段有 vendor-level 定位；本文是其 implementation 展開。</li>
<li><strong>durable queue 能力層</strong>：queue type 的持久化語意建立在 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a> 的概念上 — quorum 與 stream 強制 durable、正是把「處理即承諾」的可靠性從單節點延伸到跨節點。</li>
<li><strong>durable queue 知識卡</strong>：訊息持久化的概念基礎見 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue 知識卡</a>。</li>
<li><strong>mirrored → quorum 的遷移動機</strong>：<a href="/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30 Runtastic</a> 量化 mirrored 網路成本、是遷 quorum 的證據。</li>
<li><strong>跨版本 / 跨 cluster 平滑遷移</strong>：<a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27 Zalando</a> 用 federation 過渡、是 in-flight message 安全搬移的範本。</li>
</ul>
<p>何時 revisit queue type 選擇：classic queue 開始出現記憶體壓力或需要跨節點 HA 時、評估 quorum；任何 queue 場景開始需要「補讀歷史」「多 consumer 各自進度」「replay 重算」時、評估 stream；stream 場景開始需要跨團隊事件治理時、評估遷 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>。</p>
]]></content:encoded></item><item><title>1.13 應用層查詢反模式與 Query 預算</title><link>https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/</guid><description>&lt;p>應用程式變慢、第一個直覺常常是「資料庫不夠力」。多數團隊的真實瓶頸在應用程式發給資料庫的查詢方式、資料庫本身反而不是問題：N+1、select *、缺索引、ORM lazy load、長 transaction。本章把這些反模式列成可診斷、可修正的清單、並提出「每請求的 query 預算」作為發布前的判讀基準 — 讓讀者在資料層撞牆之前、先在應用層發現問題。&lt;/p>
&lt;h2 id="為什麼查詢反模式比-vendor-細節更重要">為什麼查詢反模式比 vendor 細節更重要&lt;/h2>
&lt;p>多數團隊面對「資料庫變慢」時，會先去看 vendor 的調校（buffer pool、配置升級、replica 加開）。這些調校通常把基礎效能拉高 1-2 倍；一個 N+1 query 反模式可以讓回應時間慢 10-1000 倍（具體倍數取決於 N 跟 RTT — N=100 + RTT=1ms 約慢 100 倍）。先解掉應用層的反模式、再去調 vendor 配置，整體效益遠高於反過來。&lt;/p>
&lt;p>這條優先序也對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a> 的精神：先定位真正的瓶頸再決定是否加資源。應用層 query 是最常被忽略的瓶頸來源。&lt;/p>
&lt;h2 id="n1-query最常見也最隱性的反模式">N+1 Query：最常見也最隱性的反模式&lt;/h2>
&lt;p>N+1 query 指「先發一個 query 取回 N 筆資料、再對每一筆各發一個 query 取相關資料」，總共 1 + N 次 round trip。N 越大、整體越慢。&lt;/p>
&lt;p>典型範例：列出 100 個訂單跟每筆訂單的客戶資料。錯誤寫法是先 &lt;code>SELECT * FROM orders LIMIT 100&lt;/code> 拿到 100 筆訂單、再對每一筆訂單做 &lt;code>SELECT * FROM customers WHERE id = ?&lt;/code>，總共 101 次 query。正確寫法是 JOIN 或 IN 一次取回：&lt;code>SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id = c.id LIMIT 100&lt;/code>，1 次 query 完成。&lt;/p>
&lt;p>N+1 在 ORM 環境特別隱性，因為它常被框架的 lazy loading 機制隱藏。Django ORM 的 &lt;code>order.customer&lt;/code> 看起來像存取 attribute，背後對應一次 query。寫程式時看不到 SQL，發布後才從 slow log 發現問題。&lt;/p>
&lt;p>判讀方式：開啟 ORM 的 query log（debug mode）、看一個 API request 跑出幾個 query。預期是個位數；若 query 數隨著資料集大小線性成長（例如 list 100 筆觸發 100 query、list 1000 筆觸發 1000 query），這條 scaling 訊號就是 N+1 — 比固定閾值更可靠的判讀。&lt;/p>
&lt;p>修正方向：&lt;/p>
&lt;ul>
&lt;li>ORM 端用 eager loading（Django &lt;code>select_related&lt;/code> / &lt;code>prefetch_related&lt;/code>、Rails &lt;code>includes&lt;/code>、SQLAlchemy &lt;code>joinedload&lt;/code>）&lt;/li>
&lt;li>自己寫 SQL 用 JOIN 或 IN 條件批次取&lt;/li>
&lt;li>確認 ORM 預設不是 lazy（有些 ORM 的設計鼓勵 lazy，需要明確標示 eager）&lt;/li>
&lt;/ul>
&lt;h2 id="select--與超量讀取">Select * 與超量讀取&lt;/h2>
&lt;p>&lt;code>SELECT *&lt;/code> 把表的所有欄位都拉出來，包含可能很大的欄位（content、blob、JSON）跟根本用不到的欄位。代價有三：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>網路傳輸成本&lt;/strong>：query 結果在 DB 跟應用之間傳輸，欄位越多越大。&lt;/li>
&lt;li>&lt;strong>記憶體成本&lt;/strong>：應用程式要 deserialize 整個 row，物件越大記憶體佔越多。&lt;/li>
&lt;li>&lt;strong>隱性耦合&lt;/strong>：欄位有變動（新增、刪除、改型別）時，所有 &lt;code>SELECT *&lt;/code> 的 query 都會被影響。&lt;/li>
&lt;/ol>
&lt;p>修正方向是明確列出需要的欄位：&lt;code>SELECT id, name, status FROM orders&lt;/code>。如果擔心欄位列表太長，問自己是不是 query 試圖一次處理太多責任。&lt;/p></description><content:encoded><![CDATA[<p>應用程式變慢、第一個直覺常常是「資料庫不夠力」。多數團隊的真實瓶頸在應用程式發給資料庫的查詢方式、資料庫本身反而不是問題：N+1、select *、缺索引、ORM lazy load、長 transaction。本章把這些反模式列成可診斷、可修正的清單、並提出「每請求的 query 預算」作為發布前的判讀基準 — 讓讀者在資料層撞牆之前、先在應用層發現問題。</p>
<h2 id="為什麼查詢反模式比-vendor-細節更重要">為什麼查詢反模式比 vendor 細節更重要</h2>
<p>多數團隊面對「資料庫變慢」時，會先去看 vendor 的調校（buffer pool、配置升級、replica 加開）。這些調校通常把基礎效能拉高 1-2 倍；一個 N+1 query 反模式可以讓回應時間慢 10-1000 倍（具體倍數取決於 N 跟 RTT — N=100 + RTT=1ms 約慢 100 倍）。先解掉應用層的反模式、再去調 vendor 配置，整體效益遠高於反過來。</p>
<p>這條優先序也對應 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 的精神：先定位真正的瓶頸再決定是否加資源。應用層 query 是最常被忽略的瓶頸來源。</p>
<h2 id="n1-query最常見也最隱性的反模式">N+1 Query：最常見也最隱性的反模式</h2>
<p>N+1 query 指「先發一個 query 取回 N 筆資料、再對每一筆各發一個 query 取相關資料」，總共 1 + N 次 round trip。N 越大、整體越慢。</p>
<p>典型範例：列出 100 個訂單跟每筆訂單的客戶資料。錯誤寫法是先 <code>SELECT * FROM orders LIMIT 100</code> 拿到 100 筆訂單、再對每一筆訂單做 <code>SELECT * FROM customers WHERE id = ?</code>，總共 101 次 query。正確寫法是 JOIN 或 IN 一次取回：<code>SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id = c.id LIMIT 100</code>，1 次 query 完成。</p>
<p>N+1 在 ORM 環境特別隱性，因為它常被框架的 lazy loading 機制隱藏。Django ORM 的 <code>order.customer</code> 看起來像存取 attribute，背後對應一次 query。寫程式時看不到 SQL，發布後才從 slow log 發現問題。</p>
<p>判讀方式：開啟 ORM 的 query log（debug mode）、看一個 API request 跑出幾個 query。預期是個位數；若 query 數隨著資料集大小線性成長（例如 list 100 筆觸發 100 query、list 1000 筆觸發 1000 query），這條 scaling 訊號就是 N+1 — 比固定閾值更可靠的判讀。</p>
<p>修正方向：</p>
<ul>
<li>ORM 端用 eager loading（Django <code>select_related</code> / <code>prefetch_related</code>、Rails <code>includes</code>、SQLAlchemy <code>joinedload</code>）</li>
<li>自己寫 SQL 用 JOIN 或 IN 條件批次取</li>
<li>確認 ORM 預設不是 lazy（有些 ORM 的設計鼓勵 lazy，需要明確標示 eager）</li>
</ul>
<h2 id="select--與超量讀取">Select * 與超量讀取</h2>
<p><code>SELECT *</code> 把表的所有欄位都拉出來，包含可能很大的欄位（content、blob、JSON）跟根本用不到的欄位。代價有三：</p>
<ol>
<li><strong>網路傳輸成本</strong>：query 結果在 DB 跟應用之間傳輸，欄位越多越大。</li>
<li><strong>記憶體成本</strong>：應用程式要 deserialize 整個 row，物件越大記憶體佔越多。</li>
<li><strong>隱性耦合</strong>：欄位有變動（新增、刪除、改型別）時，所有 <code>SELECT *</code> 的 query 都會被影響。</li>
</ol>
<p>修正方向是明確列出需要的欄位：<code>SELECT id, name, status FROM orders</code>。如果擔心欄位列表太長，問自己是不是 query 試圖一次處理太多責任。</p>
<p>例外是 ad-hoc query 跟 DB tool 環境，可以接受 <code>SELECT *</code>。production code 不應該有。</p>
<h2 id="缺索引查詢計畫沒走索引">缺索引：查詢計畫沒走索引</h2>
<p>缺索引的徵兆是 query 在小資料量時很快、資料一多就突然慢。原因是 query 走了 full table scan，資料量小時 scan 還快、資料量上百萬筆就慢。</p>
<p>判讀方式是用 <code>EXPLAIN</code> 看查詢計畫：</p>
<ul>
<li><code>type=ALL</code> 或 <code>Seq Scan</code> 代表沒走索引</li>
<li><code>rows</code> 估計值跟實際表大小接近，代表掃描範圍過大</li>
<li><code>Using filesort</code> / <code>Using temporary</code> 代表排序或暫存資料的成本</li>
</ul>
<p>修正方向不是「對每個 WHERE 條件都建索引」，這會讓寫入變慢、索引變大。要建索引的判讀條件：</p>
<ul>
<li>該 query 是熱路徑（頻率高、影響 user）</li>
<li>該欄位有足夠選擇性（distinct 值多）</li>
<li>該欄位沒有跟其他索引重複覆蓋</li>
<li>寫入路徑能承受多一個索引的維護成本</li>
</ul>
<p>複合索引的欄位順序也要對齊 query 的 WHERE 條件。<code>WHERE a = ? AND b = ?</code> 適合 <code>(a, b)</code> 複合索引，不適合 <code>(b, a)</code>。這部分屬於 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a> 的範圍、本章只標出徵兆跟診斷起點。</p>
<h2 id="orm-lazy-load-陷阱">ORM Lazy Load 陷阱</h2>
<p>ORM 的 lazy load 預設行為是「存取 attribute 時才發 query」，這在開發時讓 code 很乾淨，但隱藏了 query 的數量。</p>
<p>常見陷阱：</p>
<ul>
<li><strong>跨 transaction 邊界存取 lazy attribute</strong>：query 在原 transaction 已關閉後才發，連線狀態錯誤。</li>
<li><strong>在 template / serializer 裡存取 lazy attribute</strong>：一個 page render 觸發數十個額外 query。</li>
<li><strong>lazy load 跨服務邊界</strong>：DTO 傳遞時不知道哪些 attribute 是 lazy、哪些是 eager，前端拿到 DTO 後 trigger 額外 query。</li>
</ul>
<p>修正方向：</p>
<ul>
<li>明確標示 eager loading 邊界，serializer 之前完成所有需要的資料載入</li>
<li>ORM 配置改成 default eager 或 strict mode（query 太多會 warning）</li>
<li>DTO 出 service 邊界前做 fully materialized</li>
</ul>
<h2 id="long-running-transaction">Long-Running Transaction</h2>
<p>長時間佔住的 transaction 會擋住其他 query、產生 lock 等待、消耗連線池資源。</p>
<p>常見成因：</p>
<ul>
<li>在 transaction 內做 HTTP call 或外部 API 呼叫</li>
<li>在 transaction 內做檔案 I/O 或長計算</li>
<li>用 transaction 包住整個 request handler（從 request 開始到 response 結束都在 transaction）</li>
<li>ORM 設定 default transaction-per-request 但業務只需要短交易</li>
</ul>
<p>修正方向是把 transaction 範圍縮到最小：只包住「需要原子性」的那幾個 SQL 操作。外部呼叫、計算、檔案 I/O 都要在 transaction 之外。詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>。</p>
<h2 id="其他常見反模式">其他常見反模式</h2>
<p>上面五個是讀路徑高頻反模式。實務上其他幾類在 slow log 出現頻率不低、要一併列入發布前檢查：</p>
<ul>
<li><strong><a href="/blog/backend/knowledge-cards/cardinality-explosion/" data-link-title="Query Cardinality Explosion" data-link-desc="Query 結果行數因 join / cross product / 條件缺失爆炸性放大的反模式">Cardinality explosion</a> / cross join 誤用</strong>：兩個多對多關聯 join 沒加 filter、結果集從 N 行炸成 N×M 行。判讀訊號：query 結果行數遠超業務直覺、<code>EXPLAIN</code> 估計 rows 異常大。修正方向：補 filter、改 EXISTS / IN 半連接、或拆兩段 query。</li>
<li><strong>OFFSET-based pagination on large tables</strong>：<code>LIMIT 20 OFFSET 100000</code> 在大表退化成「掃描 100020 行 + skip 100000 行」。修正方向：用 <a href="/blog/backend/knowledge-cards/keyset-pagination/" data-link-title="Keyset Pagination" data-link-desc="用上一頁最後一筆的 key 當下一頁起點、避開 OFFSET 大表時的線性退化">keyset / cursor pagination</a>（<code>WHERE id &gt; last_seen_id LIMIT 20</code>）— 一致 O(LIMIT) 而非 O(OFFSET + LIMIT)。</li>
<li><strong>隱式型別轉換讓 index 失效</strong>：<code>WHERE varchar_col = 123</code> 把 column 轉成 int 比較、index 失效退到 full scan。判讀訊號：EXPLAIN 顯示 index 沒命中但 schema 上有 index。修正方向：明示型別（<code>WHERE varchar_col = '123'</code>）。</li>
<li><strong>應用層做大結果集排序 / 聚合</strong>：把 100 萬行拉回應用、在記憶體 sort 或 group。應該 push 給 DB 做 <code>ORDER BY</code> / <code>GROUP BY</code> + <code>LIMIT</code>。判讀訊號：應用程式記憶體用量隨 endpoint 流量線性升高。</li>
<li><strong>N+1 write</strong>：在 loop 內單筆 insert / update 而非 bulk insert。每筆觸發一次 round trip + 可能的 fsync。修正方向：用 <code>INSERT ... VALUES (), (), ()</code> 或 <code>executemany</code> / <code>bulk_create</code>。</li>
</ul>
<p>NoSQL / KV DB 也有 sibling 反模式（hot partition、read amplification、scan-and-filter），不在本章 SQL 範疇但邏輯類似 — 詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="每請求的-query-預算">每請求的 Query 預算</h2>
<p>把上面這些反模式收斂成一個發布前可檢查的判準：每個 API request 允許發多少個 query。</p>
<table>
  <thead>
      <tr>
          <th>API 類型</th>
          <th>建議 query 預算</th>
          <th>判讀說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>簡單 read（取單筆）</td>
          <td>1–3 個</td>
          <td>主資源 1 個 + 相關資源 join 或 1–2 個額外</td>
      </tr>
      <tr>
          <td>List read（取列表）</td>
          <td>1–5 個</td>
          <td>主列表 1 個 + filter / pagination / 關聯 batch query</td>
      </tr>
      <tr>
          <td>Write（單筆操作）</td>
          <td>2–5 個</td>
          <td>check 1 個 + write 1 個 + 觸發後續 query</td>
      </tr>
      <tr>
          <td>Complex（多步驟業務）</td>
          <td>5–15 個</td>
          <td>視業務複雜度，但每多 1 個都要能講出為什麼</td>
      </tr>
  </tbody>
</table>
<p>超過預算不一定錯，但需要解釋。CI / staging 可以加 middleware 統計每個 endpoint 的 query 數，超過閾值在 PR review 時觸發討論。這比事後從 slow log 找問題更有效。</p>
<p>這張表以 OLTP API 為主。Dashboard / report / search endpoint 常需要 10-30 query 解 join / aggregation、用「Complex」涵蓋不夠精確；batch / bulk write（一次寫入 1000 筆訂單）不該用 query count 評估、應該看 batch size 跟 transaction 範圍。預算是判讀工具、不是硬閾值。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API 在資料量增加後突然變慢</td>
          <td>缺索引或查詢計畫退化</td>
          <td>跑 EXPLAIN、檢查 query plan</td>
      </tr>
      <tr>
          <td>同一個 API 跑出 dozens 個 query</td>
          <td>N+1 反模式</td>
          <td>加 eager loading 或改寫成 JOIN</td>
      </tr>
      <tr>
          <td>應用程式記憶體用量隨流量線性升高</td>
          <td><code>SELECT *</code> 載入過多資料</td>
          <td>改成明確欄位、加 pagination</td>
      </tr>
      <tr>
          <td>DB connection 等待時間升高</td>
          <td>long transaction 或 connection pool 不足</td>
          <td>縮 transaction 範圍、評估 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 上限</td>
      </tr>
      <tr>
          <td>Lock wait timeout 變多</td>
          <td>long transaction 或 hot row 競爭</td>
          <td>拆 transaction、檢查 hot row 設計</td>
      </tr>
      <tr>
          <td>Slow query log 集中在某類 SQL</td>
          <td>該 query 走了 full scan 或 join 順序錯誤</td>
          <td>EXPLAIN + 加索引或改寫 query</td>
      </tr>
      <tr>
          <td>ORM debug log 顯示 hundreds query</td>
          <td>lazy load 失控</td>
          <td>換 eager loading 策略、檢視 serializer 邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「資料庫變慢」直接解讀成「該升級資料庫」。先看應用層 query。多數效能問題是反模式造成的、而不是 DB 規格不夠。</p>
<p>把索引當「想加就加」。每個索引都有寫入成本跟空間成本。索引太多會讓 INSERT/UPDATE 變慢、backup 變大。要建索引前先驗證該 query 是熱路徑。</p>
<p>把 N+1 當「在 ORM 環境無解」。多數 ORM 都有 eager loading 選項，只是預設 lazy。問題是團隊沒把這當作預設策略。設定 ORM 為 default eager 或在 CI 加 query 數量檢查就能避免。</p>
<p>把 transaction 範圍當「越大越安全」。長 transaction 是 lock 風險來源，不是一致性保證。一致性靠正確的 isolation level 跟業務邏輯，不是靠長 transaction 鎖住整個流程。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「應用層發給資料庫的 query 反模式」。當問題進入 schema 設計（要不要拆表？要不要 partition？）交給 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>；進入 transaction 語意（什麼時候用 SERIALIZABLE？怎麼 retry？）交給 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>；進入跨服務的查詢責任拆分（哪些查詢屬於該服務？）交給 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 state ownership 與 query boundary</a>；進入瓶頸定位的工程流程交給 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫的主軸是規模、vendor 與容量壓力，直接以「query 反模式」為主題的案例較少。下列案例可以反向讀：每一個都展示了「在沒有先用 query 反模式優化收回壓力的前提下、團隊直接走 vendor 遷移或 scale-out 路徑」的決策。讀者讀完應追問：這些 case 啟動遷移前、是否有可能用本章的反模式清單先收回一部分容量？</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB</a> — DoorDash 撞到 Aurora single-primary write 天花板（瓶頸在 primary CPU + WAL flush rate）、用 PostgreSQL wire protocol 相容的 CockroachDB 換成多主寫入、ORM 不必重寫。對照本章可問：寫入熱點是否伴隨長 transaction 或熱 row 競爭？這些是 vendor 遷移前可以先用本章「Long-Running Transaction」清單檢查的點。</li>
<li><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：TiDB 遷到 DynamoDB</a> — Zomato 判斷 billing 事件本身可接受 eventually consistent、用一致性語意換取 4 倍吞吐 + 50% 成本。對照本章可問：遷移前每筆業務動作平均發了多少 query、是否有 N+1 或 select * 在放大壓力？把這條問題擺進「每請求 Query 預算」段一起讀。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered：Aurora 4000 TPS 合規容量</a> — Standard Chartered 在 7 個受監管市場各跑獨立 Aurora cluster（資料不能跨境）、容量規劃單位是「per 市場」、合規邊界決定了 cluster 拓樸。對照本章可問：query 預算假設是否進入容量模型？預算寫鬆、規劃出的 per-cluster TPS 上限會偏低。</li>
</ul>
<p>DoorDash 案例是這條反向追問最直接的應用 — 寫入瓶頸的判讀不該停在 vendor 規格、而是先檢查 transaction 範圍跟熱 row 競爭。Zomato 跟 Standard Chartered 的反向追問則退一步問「query 預算假設是否進入容量模型」。三條追問共享同一條診斷邏輯：應用層 query 不是事後解釋的細節、是事前可以收回的容量。這個讀法承認案例本身不直接示範 query 反模式、是用反向追問把案例當成 query 反模式重要性的反證。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a> 的交接：1.1 處理連線池與 read replica 機制、1.13 處理 query 寫法本身。高併發場景下兩者要同步檢查。</li>
<li>與 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a> 的交接：索引設計是 schema 層的事、本章只指出徵兆。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 的交接：slow query log、APM、query trace 是判讀反模式的主要訊號來源。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 的交接：先在應用層查反模式，再考慮 DB 配置升級。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：規模成長路線上、9.13 解擴展軸選擇後、1.13 是緊接著的下一站 — 在加機器或加 replica 前、先用本章反模式清單收回單機能撐住的容量。</li>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 的交接：拆服務常被用來「解決 DB 慢」，但本章的反模式優化通常比拆服務 ROI 更高、應該優先嘗試。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a></strong>：query 反模式收完後、處理連線池與 read replica 的擴展。</p>
<p>其他延伸方向：</p>
<ul>
<li>Schema 與索引設計 → <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a></li>
<li>Transaction 範圍收斂 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a></li>
<li>瓶頸定位完整流程 → <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
</ul>
]]></content:encoded></item><item><title>9.13 擴展軸與 Stateless 前提</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/</guid><description>&lt;p>「要換更大的機器、還是要加更多臺機器？」這個問題在規模成長過程中會反覆出現。垂直擴展（scale-up）與水平擴展（scale-out）對應不同壓力來源、各自承擔不同代價：垂直擴展用「換更大的機器」換取簡單、水平擴展用「加更多機器」換取彈性。規劃容量時先判讀自己的壓力屬於哪一種、再選對應的擴展軸 — 選錯軸的代價會在事故時放大。&lt;/p>
&lt;h2 id="兩個軸的責任差異">兩個軸的責任差異&lt;/h2>
&lt;p>垂直擴展指把單一機器換成更高規格（更多 CPU / 記憶體 / IOPS），水平擴展指增加機器數量。同樣是「加資源」，兩者面對的工程問題完全不同。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>垂直擴展（scale-up）&lt;/th>
 &lt;th>水平擴展（scale-out）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>操作單位&lt;/td>
 &lt;td>換一臺機器&lt;/td>
 &lt;td>加 N 臺機器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式假設&lt;/td>
 &lt;td>不需要改&lt;/td>
 &lt;td>必須是 stateless 或有狀態同步機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量上限&lt;/td>
 &lt;td>單機物理規格上限&lt;/td>
 &lt;td>理論上線性擴展，實際受協調成本限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本曲線&lt;/td>
 &lt;td>規格升級非線性（高階機器溢價）&lt;/td>
 &lt;td>線性，但每臺要付 baseline 成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障代價&lt;/td>
 &lt;td>單點失敗影響整個服務&lt;/td>
 &lt;td>一臺壞了還有其他臺、可分流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變更節奏&lt;/td>
 &lt;td>變更要停機或 failover、頻率低&lt;/td>
 &lt;td>隨時可加減、頻率高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合場景&lt;/td>
 &lt;td>資料庫主節點、stateful 服務、單點計算&lt;/td>
 &lt;td>API、worker、無狀態服務&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者要從「程式假設」這欄反推自己的選項。如果服務本身是 stateful（資料庫、cache、session store），水平擴展需要設計 partitioning 或 replication；如果是 stateless API server，水平擴展幾乎可以無腦複製。把這個前提搞錯，就會用水平擴展的策略去動 stateful 服務、然後撞牆。&lt;/p>
&lt;h3 id="第三軸拆功能--拆-partitionakf-scale-cube-y--z-軸">第三軸：拆功能 / 拆 partition（AKF Scale Cube Y / Z 軸）&lt;/h3>
&lt;p>兩個軸的對比把擴展簡化成 capacity scaling 的雙軸、但 AKF Scale Cube 模型提了第三軸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>X 軸（複製 / 水平擴展）&lt;/strong>：本表 scale-out 即此軸、適合 stateless 服務&lt;/li>
&lt;li>&lt;strong>Y 軸（functional decomposition）&lt;/strong>：沿業務邊界拆服務、跟 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分&lt;/a> 對應、適合處理「不同功能的擴展需求差距大」&lt;/li>
&lt;li>&lt;strong>Z 軸（data partition / sharding）&lt;/strong>：沿資料拆 partition、適合處理「stateful 服務超出單機容量」&lt;/li>
&lt;/ul>
&lt;p>實務系統常同時動兩到三軸：API 走 X 軸水平、按業務拆 Y 軸（user service / order service / payment service）、user service 內部再用 user ID hash 做 Z 軸 sharding。本章焦點在 X 軸、但讀者規劃容量時要記住 Y / Z 軸是同時可用的工具。&lt;/p>
&lt;h2 id="stateless-是水平擴展的前提">Stateless 是水平擴展的前提&lt;/h2>
&lt;p>Stateless 的核心定義是「處理一個請求不依賴前一個請求留下的本機狀態」。Session、本機快取、檔案系統暫存都會破壞 stateless 假設。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態類型&lt;/th>
 &lt;th>是否破壞 stateless&lt;/th>
 &lt;th>緩解方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Session 存本機&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>把 session 搬到外部 store（Redis、DB），改用 token 認證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>上傳檔案存本機&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>改用物件儲存（S3、GCS）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本機快取&lt;/td>
 &lt;td>視情境&lt;/td>
 &lt;td>共用快取可接受（每臺 cache 各自 build）；強一致快取要外接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WebSocket 長連線&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a> 或外部 broker（Pub/Sub、Redis）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本機 cron / 排程&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>改用分散式排程（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/leader-election/" data-link-title="Leader Election" data-link-desc="從一群對等節點中選出單一主節點負責獨佔工作、leader 失效時自動選新 leader">leader election&lt;/a> 或外部排程服務）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨請求的記憶體狀態&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>移到外部 state store&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>很多人以為自己的服務是 stateless、但一上水平擴展就出事，原因常常在這張表的某一行。判讀方式：把單一機器停掉、重新分配流量到其他機器，使用者體驗是否完全無感？如果有任何「重新登入」「上傳消失」「資料看不到」的情境，就有 stateful 殘留。&lt;/p>
&lt;p>這張表覆蓋顯式狀態。&lt;strong>隱式狀態&lt;/strong>（implicit state）是另一類常被忽略的破壞 stateless 因素：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>In-flight request state&lt;/strong>：HTTP/2 stream、gRPC bidirectional stream — 跨多個請求保持的連線級狀態&lt;/li>
&lt;li>&lt;strong>TLS session resumption&lt;/strong>：session ticket 跟 session ID cache 跨連線、若不集中存會降低重連性能&lt;/li>
&lt;li>&lt;strong>Rate limiter state&lt;/strong>：per-user token bucket、滑動視窗 — 看似無狀態的 middleware 其實在記每個 user 的計數&lt;/li>
&lt;li>&lt;strong>連線預熱（connection warm-up）&lt;/strong>：HTTP/2 / gRPC 連線建立成本高、機器接到流量後需要時間熱起來&lt;/li>
&lt;/ul>
&lt;p>這類「看似 stateless 但有 implicit state」是水平擴展撞牆的常見主因。處理方式是把隱式狀態抽到外部 store（rate limit 用 Redis、TLS session 用共用 cache）或設計連線級 sticky。&lt;/p></description><content:encoded><![CDATA[<p>「要換更大的機器、還是要加更多臺機器？」這個問題在規模成長過程中會反覆出現。垂直擴展（scale-up）與水平擴展（scale-out）對應不同壓力來源、各自承擔不同代價：垂直擴展用「換更大的機器」換取簡單、水平擴展用「加更多機器」換取彈性。規劃容量時先判讀自己的壓力屬於哪一種、再選對應的擴展軸 — 選錯軸的代價會在事故時放大。</p>
<h2 id="兩個軸的責任差異">兩個軸的責任差異</h2>
<p>垂直擴展指把單一機器換成更高規格（更多 CPU / 記憶體 / IOPS），水平擴展指增加機器數量。同樣是「加資源」，兩者面對的工程問題完全不同。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>垂直擴展（scale-up）</th>
          <th>水平擴展（scale-out）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>操作單位</td>
          <td>換一臺機器</td>
          <td>加 N 臺機器</td>
      </tr>
      <tr>
          <td>程式假設</td>
          <td>不需要改</td>
          <td>必須是 stateless 或有狀態同步機制</td>
      </tr>
      <tr>
          <td>容量上限</td>
          <td>單機物理規格上限</td>
          <td>理論上線性擴展，實際受協調成本限制</td>
      </tr>
      <tr>
          <td>成本曲線</td>
          <td>規格升級非線性（高階機器溢價）</td>
          <td>線性，但每臺要付 baseline 成本</td>
      </tr>
      <tr>
          <td>故障代價</td>
          <td>單點失敗影響整個服務</td>
          <td>一臺壞了還有其他臺、可分流</td>
      </tr>
      <tr>
          <td>變更節奏</td>
          <td>變更要停機或 failover、頻率低</td>
          <td>隨時可加減、頻率高</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>資料庫主節點、stateful 服務、單點計算</td>
          <td>API、worker、無狀態服務</td>
      </tr>
  </tbody>
</table>
<p>讀者要從「程式假設」這欄反推自己的選項。如果服務本身是 stateful（資料庫、cache、session store），水平擴展需要設計 partitioning 或 replication；如果是 stateless API server，水平擴展幾乎可以無腦複製。把這個前提搞錯，就會用水平擴展的策略去動 stateful 服務、然後撞牆。</p>
<h3 id="第三軸拆功能--拆-partitionakf-scale-cube-y--z-軸">第三軸：拆功能 / 拆 partition（AKF Scale Cube Y / Z 軸）</h3>
<p>兩個軸的對比把擴展簡化成 capacity scaling 的雙軸、但 AKF Scale Cube 模型提了第三軸：</p>
<ul>
<li><strong>X 軸（複製 / 水平擴展）</strong>：本表 scale-out 即此軸、適合 stateless 服務</li>
<li><strong>Y 軸（functional decomposition）</strong>：沿業務邊界拆服務、跟 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 對應、適合處理「不同功能的擴展需求差距大」</li>
<li><strong>Z 軸（data partition / sharding）</strong>：沿資料拆 partition、適合處理「stateful 服務超出單機容量」</li>
</ul>
<p>實務系統常同時動兩到三軸：API 走 X 軸水平、按業務拆 Y 軸（user service / order service / payment service）、user service 內部再用 user ID hash 做 Z 軸 sharding。本章焦點在 X 軸、但讀者規劃容量時要記住 Y / Z 軸是同時可用的工具。</p>
<h2 id="stateless-是水平擴展的前提">Stateless 是水平擴展的前提</h2>
<p>Stateless 的核心定義是「處理一個請求不依賴前一個請求留下的本機狀態」。Session、本機快取、檔案系統暫存都會破壞 stateless 假設。</p>
<table>
  <thead>
      <tr>
          <th>狀態類型</th>
          <th>是否破壞 stateless</th>
          <th>緩解方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 存本機</td>
          <td>破壞</td>
          <td>把 session 搬到外部 store（Redis、DB），改用 token 認證</td>
      </tr>
      <tr>
          <td>上傳檔案存本機</td>
          <td>破壞</td>
          <td>改用物件儲存（S3、GCS）</td>
      </tr>
      <tr>
          <td>本機快取</td>
          <td>視情境</td>
          <td>共用快取可接受（每臺 cache 各自 build）；強一致快取要外接</td>
      </tr>
      <tr>
          <td>WebSocket 長連線</td>
          <td>破壞</td>
          <td>用 <a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a> 或外部 broker（Pub/Sub、Redis）</td>
      </tr>
      <tr>
          <td>本機 cron / 排程</td>
          <td>破壞</td>
          <td>改用分散式排程（<a href="/blog/backend/knowledge-cards/leader-election/" data-link-title="Leader Election" data-link-desc="從一群對等節點中選出單一主節點負責獨佔工作、leader 失效時自動選新 leader">leader election</a> 或外部排程服務）</td>
      </tr>
      <tr>
          <td>跨請求的記憶體狀態</td>
          <td>破壞</td>
          <td>移到外部 state store</td>
      </tr>
  </tbody>
</table>
<p>很多人以為自己的服務是 stateless、但一上水平擴展就出事，原因常常在這張表的某一行。判讀方式：把單一機器停掉、重新分配流量到其他機器，使用者體驗是否完全無感？如果有任何「重新登入」「上傳消失」「資料看不到」的情境，就有 stateful 殘留。</p>
<p>這張表覆蓋顯式狀態。<strong>隱式狀態</strong>（implicit state）是另一類常被忽略的破壞 stateless 因素：</p>
<ul>
<li><strong>In-flight request state</strong>：HTTP/2 stream、gRPC bidirectional stream — 跨多個請求保持的連線級狀態</li>
<li><strong>TLS session resumption</strong>：session ticket 跟 session ID cache 跨連線、若不集中存會降低重連性能</li>
<li><strong>Rate limiter state</strong>：per-user token bucket、滑動視窗 — 看似無狀態的 middleware 其實在記每個 user 的計數</li>
<li><strong>連線預熱（connection warm-up）</strong>：HTTP/2 / gRPC 連線建立成本高、機器接到流量後需要時間熱起來</li>
</ul>
<p>這類「看似 stateless 但有 implicit state」是水平擴展撞牆的常見主因。處理方式是把隱式狀態抽到外部 store（rate limit 用 Redis、TLS session 用共用 cache）或設計連線級 sticky。</p>
<h2 id="auto-scaling-的操作模型">Auto Scaling 的操作模型</h2>
<p>水平擴展通常搭配 <a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">auto scaling</a> — 根據訊號自動加減機器數量。常見的擴展訊號跟對應的判讀重點：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>反應速度</th>
          <th>判讀重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU 使用率</td>
          <td>中</td>
          <td>通用、但對 I/O bound 服務失準</td>
      </tr>
      <tr>
          <td>記憶體使用率</td>
          <td>慢</td>
          <td>適合判 leak、不適合判尖峰流量</td>
      </tr>
      <tr>
          <td>Request rate (RPS)</td>
          <td>快</td>
          <td>適合 API 服務、需要設定 cool-down 避免抖動</td>
      </tr>
      <tr>
          <td>Queue depth</td>
          <td>快</td>
          <td>適合 worker 服務、queue 是天然 buffer</td>
      </tr>
      <tr>
          <td>Latency P95</td>
          <td>中</td>
          <td>用戶體驗訊號、但已經出現延遲才擴展可能來不及</td>
      </tr>
      <tr>
          <td>自訂業務訊號</td>
          <td>視訊號</td>
          <td>訂單數、活動人數，貼近業務但要自己維護 metric pipeline</td>
      </tr>
  </tbody>
</table>
<p>設定 auto scaling 的判讀順序：先選訊號（CPU vs RPS vs queue depth），再設閾值（避免過早觸發或過晚觸發），最後加 cool-down（避免反覆擴縮造成抖動）。三步驟有一步沒做好就會撞牆。</p>
<p>Auto scaling 不是萬靈丹。三類問題它無法解決：擴展速度跟不上（冷啟動時間視 stack 範圍 5-300 秒、流量尖峰若集中在秒級就來不及）、預測式流量（黑五、新片上線、活動）、stateful 服務（資料庫不能用 auto scaling 加 primary）。這三類要分別用 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>、<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a> 跟 partitioning 處理。</p>
<h2 id="垂直擴展的天花板">垂直擴展的天花板</h2>
<p>垂直擴展看起來簡單但有兩道牆。</p>
<p>第一道是物理上限。雲端機型的最大規格是有限的：以 2025 年公開資料為例、AWS 的 u 系列 instance（如 <code>u7i-12tb</code>、<code>u-24tb1.metal</code>）可達 24 TiB 記憶體級別、vCPU 數量視 SKU 而異；GCP / Azure 也有對應的 memory-optimized 系列、但具體上限隨年份更新。要查最新規格走 vendor 官方文件、不要拿這裡數字當決策依據。對 stateful workload（例如 OLTP 主節點）真實天花板通常出現在 32-64 vCPU 級別、是 lock contention / context switch / memory bandwidth 等架構因素而非規格上限。</p>
<p>第二道是成本曲線。雲端機型的價格不是線性的、越高階的機型每單位資源越貴。以 AWS general-purpose 機型（m 系列）為例、4 vCPU → 8 vCPU 約 ×1.8、8 → 16 約 ×1.9（接近線性）、但到 48 vCPU 以上會明顯偏離線性外推、特別是 memory-optimized（r 系列）跟 high-memory（x 系列）的高階規格溢價更陡。具體曲線依機型 family 跟雲廠商而異 — 走 vendor calculator 算實際 workload 的成本曲線比抓單一倍數可靠。垂直擴展到一定規模、就算物理上撐得住、財務上也會比水平擴展貴。</p>
<p>對 stateful 服務（特別是主資料庫），垂直擴展常常是第一選擇，因為水平擴展需要重新設計 partitioning。但要清楚兩道牆會在什麼時候撞上：基於目前流量增長率，預估垂直擴展能撐多久？多久之後必須改成水平擴展？這個答案要在「還沒撞牆時」就準備好，不是等到下一次撞牆才開始討論。</p>
<h2 id="水平擴展的隱性成本">水平擴展的隱性成本</h2>
<p>水平擴展看起來彈性、但有它自己的代價。</p>
<p><strong>協調成本</strong>：多臺機器要處理「誰是 leader、誰來執行排程、誰來處理同一筆訂單」這類問題。<a href="/blog/backend/knowledge-cards/consensus-protocol/" data-link-title="Consensus Protocol" data-link-desc="讓多個獨立節點在訊息可能延遲、丟失、亂序的網路下對單一決策達成一致的演算法">consensus protocol</a> 跟 <a href="/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">distributed lock</a>（含 leader election、Raft / Paxos 演算法）都會引入新的故障模式跟 latency 代價。</p>
<p><strong>連線池放大</strong>：100 臺機器、每臺對資料庫開 10 個連線，等於對 DB 開 1000 個連線。DB 連線是有限資源，水平擴展應用層的同時要評估資料層連線壓力。常見緩解：connection pooler（PgBouncer）、serverless DB（DynamoDB）、讀寫分離。</p>
<p><strong>狀態同步成本</strong>：cache、session、配置這些「跨機器需要一致」的狀態，要靠外部 store 或 broadcast 機制同步。同步延遲跟頻率會反過來影響服務行為。</p>
<p><strong>Cold start</strong>：新機器啟動到接流量需要時間（image pull、init container、warm-up）。auto scaling 觸發跟流量到達之間的延遲就是這段。冷啟動長的服務（JVM、需要載入大量資料的服務）要預留更多 buffer。</p>
<p><strong>Debug 變難</strong>：請求散落在多臺機器，排查問題需要 log 聚合、trace context。沒有這些基礎設施，水平擴展只會把「一臺機器壞」的問題變成「不知道哪一臺機器壞」的問題。</p>
<h2 id="混合策略">混合策略</h2>
<p>純垂直或純水平在實際系統中都罕見。常見的混合模式：</p>
<ul>
<li><strong>小規模垂直、大規模水平</strong>：早期單機就能撐，先用較大規格降低運維複雜度；流量上來後再轉水平，把每臺機器規格降回中等。</li>
<li><strong>stateless 水平、stateful 垂直</strong>：API server 水平擴展、資料庫主節點垂直擴展、加 read replica 做讀路徑水平擴展。</li>
<li><strong>熱資料水平 sharding、冷資料保持單庫</strong>：把熱表用 partition key 拆到多個 shard，冷表保留在主庫不動。</li>
<li><strong>核心服務垂直保底、邊緣服務水平彈性</strong>：核心交易服務用更大規格降低事故風險，前端、推薦等服務走 auto scaling。</li>
</ul>
<p>選混合策略時，要明確標記每個服務在哪個軸上、極限在哪、下一步轉換點在什麼條件下觸發。沒有這張對照表，混合策略容易變成「每個服務都是特例」、最後沒人記得當初為什麼這樣設計。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>加機器後 QPS 沒提升</td>
          <td>stateful 殘留（本機快取 / session / 鎖）</td>
          <td>找出 stateful 點、移到外部 store，或改回垂直擴展</td>
      </tr>
      <tr>
          <td>加機器後 DB 連線爆掉</td>
          <td>連線池放大、DB 是瓶頸</td>
          <td>加 connection pooler、評估讀寫分離、考慮資料層擴展</td>
      </tr>
      <tr>
          <td>Auto scaling 反覆擴縮</td>
          <td>cool-down 太短或訊號抖動</td>
          <td>加 cool-down、改用更穩定訊號（移動平均、business metric）</td>
      </tr>
      <tr>
          <td>流量尖峰時新機器來不及啟動</td>
          <td>cold start 太長 / 預測訊號不夠早</td>
          <td>改 <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a> 或 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>、warm pool</td>
      </tr>
      <tr>
          <td>垂直擴展後成本曲線陡升</td>
          <td>撞到高階機型溢價</td>
          <td>評估水平擴展轉型 / 重構 stateful 部分</td>
      </tr>
      <tr>
          <td>水平擴展後事故 MTTR 拉長</td>
          <td>觀測能力跟不上</td>
          <td>補 trace context、結構化 log、service topology</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「加機器」當作所有效能問題的萬靈丹。如果瓶頸在演算法、SQL query、序列化、locks，加機器只會讓問題變得更貴。先用 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 確定瓶頸位置，再決定擴展軸。</p>
<p>把 auto scaling 當成「設定完就不用管」。auto scaling 是 reactive 策略，它無法處理可預期的尖峰（活動、新片上線、節日）。預期型流量要用 scheduled / predictive scaling 提前準備。</p>
<p>把 stateless 當成「沒有狀態就好」。WebSocket、long-polling、上傳、檔案處理這類服務天然 stateful、強行水平擴展會出事。要分辨「業務本質 stateful」跟「實作偷懶 stateful」，前者用 partitioning 處理、後者用重構移除。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「擴展軸的選擇與前提」。當問題進入具體量化（要加多少臺機器？headroom 多少？），交給 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>；進入瓶頸定位（瓶頸在哪一層？），交給 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>；進入服務拆分（要不要先把 stateful 部分拆出來再水平擴展？），交給 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>擴展軸選擇可用以下案例回寫。每個案例對應的軸不同，引用時要先辨識案例的主要壓力來源，再對照本章相應段落。</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — 案例主軸是「stateless API 層水平擴展、stateful 資料層改用 DynamoDB 移除單點」，直接對應本章「stateless 是水平擴展的前提」段。是本批最貼近 scaling axis 主題的案例。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理</a> — 案例展示水平擴展到極端規模後，協調成本（cluster 治理、版本一致性）變成新的瓶頸；對照本章「水平擴展的隱性成本 / 協調成本」段。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom：DynamoDB + EKS 上的遊戲後端</a> — 案例主軸是 KV 業務語意、不是 scaling axis 取捨；但可反向追問「stateful 玩家狀態為何適合 KV vs RDB」、對照本章「stateless 是水平擴展的前提」段中的「狀態類型 vs 緩解方向」表。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：把關聯式 DB 統一到 Aurora</a> — 案例主軸是「DB 種類整併」、不直接對應 scale-up vs scale-out；但 Aurora 在 single-primary 規格選擇上隱含了「先垂直、再考慮分散」的策略，可作為「垂直擴展天花板」段的對照組。</li>
</ul>
<p>Zomato 跟 Netflix 不在這份案例清單裡的原因要先講清楚：擴展軸的真實示範案例在後端教材中相對稀缺、09 模組多數案例的主軸落在 vendor 或容量規劃。Zoom 是這四個案例中最貼近教科書 — stateless API 水平 + stateful 改用 DynamoDB 的組合直接示範本章核心。Riot Games 揭示水平到極端規模後協調成本翻轉成新瓶頸。Capcom 跟 Netflix Aurora 不直接示範擴展軸取捨、但用反向追問「為什麼選 KV / 為什麼 single-primary 仍是 default」能把它們的決策放回擴展軸框架。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論與系統行為</a> 的交接：USL 跟 Little&rsquo;s Law 在理論上推導水平擴展的曲線、本章解釋這道牆在運維現場長什麼樣。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：擴展軸選定後，容量規劃決定具體數字。</li>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 的交接：水平擴展常常是服務拆分的觸發點，反之亦然。</li>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01 database high-concurrency-access</a> 的交接：資料層水平擴展（sharding、replica）的具體機制。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a></strong>：選定擴展軸後、在加機器前先用反模式清單收回單機可撐住的容量。</p>
<p>其他延伸方向：</p>
<ul>
<li>容量計算與 headroom 模型 → <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
<li>擴展前的瓶頸定位 → <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>服務拆分如何配合水平擴展 → <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/online-schema-change-tools/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/online-schema-change-tools/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>online schema change&lt;/em> — gh-ost 跟 pt-online-schema-change 兩條工具路徑的機制對比。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>機制&lt;/th>
 &lt;th>pt-online-schema-change（Percona）&lt;/th>
 &lt;th>gh-ost（GitHub）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同步機制&lt;/td>
 &lt;td>&lt;strong>MySQL trigger&lt;/strong>（原表 INSERT/UPDATE/DELETE 觸發寫 ghost）&lt;/td>
 &lt;td>&lt;strong>Binlog stream&lt;/strong>（讀 primary binlog 寫 ghost）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary 寫入 overhead&lt;/td>
 &lt;td>trigger 觸發成本（同 transaction 內）&lt;/td>
 &lt;td>0（binlog 已存在）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replica lag 影響&lt;/td>
 &lt;td>trigger 在 primary 跑、replica 自然 lag&lt;/td>
 &lt;td>從 replica 讀 binlog、可主動 throttle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Foreign key&lt;/td>
 &lt;td>部分支援（drop/recreate strategy）&lt;/td>
 &lt;td>不支援（必須先 drop FK）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Roll back（過程中）&lt;/td>
 &lt;td>困難（trigger 已建、要清乾淨）&lt;/td>
 &lt;td>容易（drop ghost table 即可）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>暫停 / resume&lt;/td>
 &lt;td>不支援&lt;/td>
 &lt;td>支援（gh-ost interactive command）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切換時 lock 持續&lt;/td>
 &lt;td>rename 期間 metadata lock（毫秒級）&lt;/td>
 &lt;td>rename 期間 metadata lock（毫秒級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工具 binary&lt;/td>
 &lt;td>Perl 腳本（Percona Toolkit）&lt;/td>
 &lt;td>Go binary（單一可執行檔）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推出年份&lt;/td>
 &lt;td>2011&lt;/td>
 &lt;td>2016&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩工具最終結果一樣（ghost table 取代原表）、但 &lt;em>過程中對 production 的影響非常不同&lt;/em>。選哪個取決於：trigger overhead 可不可接受、是否有 foreign key、是否需要 resume/throttle 能力、團隊熟悉哪條工具鏈。&lt;/p>
&lt;h2 id="為什麼-alter-table-需要-online-path">為什麼 ALTER TABLE 需要 online path&lt;/h2>
&lt;p>MySQL 8.0 之前的 &lt;code>ALTER TABLE&lt;/code> 多數情況下 &lt;em>rebuild 整張表&lt;/em> — 過程中 &lt;em>primary key 之外的 read/write 都 block&lt;/em>。100 GB 表 ALTER 跑 hours、production write 全部失敗。&lt;/p>
&lt;p>MySQL 8.0 加 &lt;em>Instant DDL&lt;/em>（部分 ALTER 不 rebuild、只改 metadata、毫秒級完成）、但 &lt;em>能用 instant 的 ALTER 是 subset&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>支援：ADD COLUMN（末尾）、DROP COLUMN（部分情境）、RENAME COLUMN&lt;/li>
&lt;li>不支援：ADD INDEX、CHANGE COLUMN type、ADD/DROP PRIMARY KEY、ADD FOREIGN KEY&lt;/li>
&lt;/ul>
&lt;p>不支援 instant 的場景仍要走 ghost table。Percona 跟 GitHub 各自從 production 痛點出發、產出 pt-osc（2011）跟 gh-ost（2016）。&lt;/p>
&lt;h2 id="pt-online-schema-change用-trigger-同步寫入">pt-online-schema-change：用 trigger 同步寫入&lt;/h2>
&lt;p>pt-osc 流程：&lt;/p>
&lt;ol>
&lt;li>CREATE ghost table（跟原表同 schema + 你要的 ALTER）&lt;/li>
&lt;li>在原表上 &lt;em>建 3 個 trigger&lt;/em>：INSERT / UPDATE / DELETE&lt;/li>
&lt;li>任何寫入原表的 transaction &lt;em>同時觸發 trigger&lt;/em> 寫對應 ghost&lt;/li>
&lt;li>背景 chunk-by-chunk copy 既有 row 到 ghost&lt;/li>
&lt;li>全部 copy 完後 &lt;code>RENAME TABLE&lt;/code>：原表 → archive、ghost → 原表名（atomic、metadata lock 毫秒級）&lt;/li>
&lt;li>Drop trigger、drop archive&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Trade-off&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>online schema change</em> — gh-ost 跟 pt-online-schema-change 兩條工具路徑的機制對比。</p></blockquote>
<hr>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>pt-online-schema-change（Percona）</th>
          <th>gh-ost（GitHub）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同步機制</td>
          <td><strong>MySQL trigger</strong>（原表 INSERT/UPDATE/DELETE 觸發寫 ghost）</td>
          <td><strong>Binlog stream</strong>（讀 primary binlog 寫 ghost）</td>
      </tr>
      <tr>
          <td>Primary 寫入 overhead</td>
          <td>trigger 觸發成本（同 transaction 內）</td>
          <td>0（binlog 已存在）</td>
      </tr>
      <tr>
          <td>Replica lag 影響</td>
          <td>trigger 在 primary 跑、replica 自然 lag</td>
          <td>從 replica 讀 binlog、可主動 throttle</td>
      </tr>
      <tr>
          <td>Foreign key</td>
          <td>部分支援（drop/recreate strategy）</td>
          <td>不支援（必須先 drop FK）</td>
      </tr>
      <tr>
          <td>Roll back（過程中）</td>
          <td>困難（trigger 已建、要清乾淨）</td>
          <td>容易（drop ghost table 即可）</td>
      </tr>
      <tr>
          <td>暫停 / resume</td>
          <td>不支援</td>
          <td>支援（gh-ost interactive command）</td>
      </tr>
      <tr>
          <td>切換時 lock 持續</td>
          <td>rename 期間 metadata lock（毫秒級）</td>
          <td>rename 期間 metadata lock（毫秒級）</td>
      </tr>
      <tr>
          <td>工具 binary</td>
          <td>Perl 腳本（Percona Toolkit）</td>
          <td>Go binary（單一可執行檔）</td>
      </tr>
      <tr>
          <td>推出年份</td>
          <td>2011</td>
          <td>2016</td>
      </tr>
  </tbody>
</table>
<p>兩工具最終結果一樣（ghost table 取代原表）、但 <em>過程中對 production 的影響非常不同</em>。選哪個取決於：trigger overhead 可不可接受、是否有 foreign key、是否需要 resume/throttle 能力、團隊熟悉哪條工具鏈。</p>
<h2 id="為什麼-alter-table-需要-online-path">為什麼 ALTER TABLE 需要 online path</h2>
<p>MySQL 8.0 之前的 <code>ALTER TABLE</code> 多數情況下 <em>rebuild 整張表</em> — 過程中 <em>primary key 之外的 read/write 都 block</em>。100 GB 表 ALTER 跑 hours、production write 全部失敗。</p>
<p>MySQL 8.0 加 <em>Instant DDL</em>（部分 ALTER 不 rebuild、只改 metadata、毫秒級完成）、但 <em>能用 instant 的 ALTER 是 subset</em>：</p>
<ul>
<li>支援：ADD COLUMN（末尾）、DROP COLUMN（部分情境）、RENAME COLUMN</li>
<li>不支援：ADD INDEX、CHANGE COLUMN type、ADD/DROP PRIMARY KEY、ADD FOREIGN KEY</li>
</ul>
<p>不支援 instant 的場景仍要走 ghost table。Percona 跟 GitHub 各自從 production 痛點出發、產出 pt-osc（2011）跟 gh-ost（2016）。</p>
<h2 id="pt-online-schema-change用-trigger-同步寫入">pt-online-schema-change：用 trigger 同步寫入</h2>
<p>pt-osc 流程：</p>
<ol>
<li>CREATE ghost table（跟原表同 schema + 你要的 ALTER）</li>
<li>在原表上 <em>建 3 個 trigger</em>：INSERT / UPDATE / DELETE</li>
<li>任何寫入原表的 transaction <em>同時觸發 trigger</em> 寫對應 ghost</li>
<li>背景 chunk-by-chunk copy 既有 row 到 ghost</li>
<li>全部 copy 完後 <code>RENAME TABLE</code>：原表 → archive、ghost → 原表名（atomic、metadata lock 毫秒級）</li>
<li>Drop trigger、drop archive</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li><em>寫入 overhead</em>：每個 primary 寫入 transaction 都多一次 trigger 執行、寫吞吐降 10-30%</li>
<li><em>Replica lag</em>：trigger 跟原寫入同 transaction、replica 上每個 row 也跑 trigger、replica lag 可能暴增（缺少主動 throttle）</li>
<li><em>Roll back 困難</em>：tool 跑到一半失敗、trigger 已建、要手動清掉才能 retry</li>
<li><em>FK 處理</em>：原表有 FK 指向時、ghost table 要先 drop FK 再 recreate、操作複雜</li>
</ul>
<p><strong>適用</strong>：</p>
<ul>
<li>寫吞吐 &lt; 50% capacity（有 buffer 撐 trigger overhead）</li>
<li>無 FK 或 FK 簡單</li>
<li>沒有 replica lag 敏感的 read（trigger 在 replica 也跑）</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>高寫吞吐（&gt; 80% capacity）— trigger overhead 直接 saturate</li>
<li>大量 FK 結構</li>
<li>需要 throttle / pause / resume</li>
</ul>
<h2 id="gh-ost用-binlog-stream-同步寫入">gh-ost：用 binlog stream 同步寫入</h2>
<p>gh-ost 流程：</p>
<ol>
<li>CREATE ghost table</li>
<li><em>從 replica 讀 binlog</em>（不在 primary 加 trigger）</li>
<li>同步 <em>primary 上的寫入</em> 透過 binlog event 寫到 ghost</li>
<li>背景 chunk-by-chunk copy 既有 row 到 ghost</li>
<li>全部 copy 完後 swap：<code>RENAME TABLE</code></li>
<li>Drop archive</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li><em>寫入 overhead</em>：0（binlog 已經寫了、gh-ost 只是 consumer）</li>
<li><em>Replica lag 影響</em>：gh-ost 可監測 replica lag、超過 threshold 自動 throttle copy（不影響 primary 寫入）</li>
<li><em>Roll back 容易</em>：取消時直接 drop ghost table、原表完全沒被改動</li>
<li><em>FK 不支援</em>：gh-ost 設計上不處理 FK、有 FK 必須先 drop / restructure</li>
</ul>
<p><strong>適用</strong>：</p>
<ul>
<li>高寫吞吐 production（trigger overhead 不可接受）</li>
<li>需要 throttle / pause / resume（gh-ost interactive command 可動態調 chunk size、cut-over 時點）</li>
<li>已用 GitHub-flavored MySQL operations workflow</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>有複雜 FK 結構、不想動 schema</li>
<li>Replica 跑不了 binlog（極少數場景）</li>
</ul>
<h2 id="配置-step-by-stepgh-ost">配置 step-by-step（gh-ost）</h2>
<p>實務 production 多用 gh-ost（GitHub / Slack / Booking.com 等）。pt-osc 用於有 FK 或舊系統。</p>
<h3 id="gh-ost-一個-alter-命令">gh-ost 一個 ALTER 命令</h3>





<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">gh-ost <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --host<span class="o">=</span>replica.example.com <span class="se">\ </span>          <span class="c1"># 從 replica 讀 binlog</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  --user<span class="o">=</span>ghost <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --database<span class="o">=</span>production <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --table<span class="o">=</span>orders <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --alter<span class="o">=</span><span class="s1">&#39;ADD COLUMN status VARCHAR(20) DEFAULT NULL, ADD INDEX idx_status (status)&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --allow-on-master<span class="o">=</span><span class="nb">false</span> <span class="se">\ </span>             <span class="c1"># 不直接連 primary 讀 binlog</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  --chunk-size<span class="o">=</span><span class="m">1000</span> <span class="se">\ </span>                   <span class="c1"># 每批 copy 1000 row</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  --max-load<span class="o">=</span><span class="s1">&#39;Threads_running=50&#39;</span> <span class="se">\ </span>     <span class="c1"># primary load 限制</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  --critical-load<span class="o">=</span><span class="s1">&#39;Threads_running=200&#39;</span> <span class="se">\ </span><span class="c1"># 超過直接 abort</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  --max-lag-millis<span class="o">=</span><span class="m">1500</span> <span class="se">\ </span>               <span class="c1"># replica lag 限制</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  --throttle-additional-flag-file<span class="o">=</span>/tmp/throttle <span class="se">\ </span> <span class="c1"># touch 此檔 throttle</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  --postpone-cut-over-flag-file<span class="o">=</span>/tmp/postpone <span class="se">\ </span>   <span class="c1"># touch 此檔延後 cut-over</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  --execute                              <span class="c1"># 真的執行（沒這個只 dry-run）</span></span></span></code></pre></div><h3 id="interactive-commandgh-ost-跑起來後">Interactive command（gh-ost 跑起來後）</h3>





<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"># 連 gh-ost socket（同 directory）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;status&#34;</span> <span class="p">|</span> nc -U /tmp/gh-ost.production.orders.sock
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 動態調 chunk size</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;chunk-size=500&#34;</span> <span class="p">|</span> nc -U /tmp/gh-ost.production.orders.sock
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 立即觸發 cut-over（不再等）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;unpostpone&#34;</span> <span class="p">|</span> nc -U /tmp/gh-ost.production.orders.sock
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Abort 並 drop ghost</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;panic&#34;</span> <span class="p">|</span> nc -U /tmp/gh-ost.production.orders.sock</span></span></code></pre></div><h2 id="配置-step-by-steppt-osc">配置 step-by-step（pt-osc）</h2>
<p>對比 gh-ost 的 binlog reader、pt-osc 命令更短但配置義務同樣多：</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">pt-online-schema-change <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --host<span class="o">=</span>primary.example.com <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --user<span class="o">=</span>ghost <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --alter<span class="o">=</span><span class="s1">&#39;ADD COLUMN status VARCHAR(20) DEFAULT NULL, ADD INDEX idx_status (status)&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  <span class="nv">D</span><span class="o">=</span>production,t<span class="o">=</span>orders <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --chunk-size<span class="o">=</span><span class="m">1000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --max-load<span class="o">=</span><span class="s1">&#39;Threads_running=50&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --critical-load<span class="o">=</span><span class="s1">&#39;Threads_running=200&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --max-lag<span class="o">=</span>1.5 <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --check-replication-filters <span class="se">\ </span>          <span class="c1"># 防 binlog filter 漏 trigger</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  --alter-foreign-keys-method<span class="o">=</span>auto <span class="se">\ </span>     <span class="c1"># auto / rebuild_constraints / drop_swap / none</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  --execute</span></span></code></pre></div><p><code>--alter-foreign-keys-method</code> 是 pt-osc 對 FK 處理的策略選項、四種選擇對 production 影響非常不同（rebuild 重建 FK / drop_swap 用更快但少了 atomic、none 是不處理）。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-pt-osc-trigger-overhead-不可預期">1. pt-osc trigger overhead 不可預期</h3>
<p><code>--max-load='Threads_running=50'</code> 看起來保護了 server、但 trigger 在 transaction 內、production 的 <em>每個寫入</em> 都加 trigger 開銷。<code>Threads_running</code> 是 <em>當下</em> 數字、看不到 trigger 累積 latency。常見場景：高峰時段下 pt-osc、預期 30% overhead、實際 60%、p99 飆 5x。</p>
<p>修法：</p>
<ul>
<li>高峰時段不跑 pt-osc、排 off-peak window</li>
<li>用 <em>staging environment</em> 跑 production-like load 預估 trigger overhead</li>
<li>對寫吞吐 &gt; 50% capacity 的 server 改用 gh-ost</li>
</ul>
<h3 id="2-gh-ost-binlog-lag-跟-primary-寫入率追不上">2. gh-ost binlog lag 跟 primary 寫入率追不上</h3>
<p>gh-ost 從 replica 讀 binlog、binlog event 進來速度有上限。如果 <em>primary 寫入率超過 gh-ost binlog consume 速度</em>（每秒幾千 transaction 對某些 server 已是 ceiling）、gh-ost 永遠追不上、cut-over 會長時間卡住。</p>
<p>修法：</p>
<ul>
<li>gh-ost 預設用 <em>replica binlog</em>、改用 <code>--allow-on-master</code> 直接從 primary 讀（如果 primary 容量夠）</li>
<li>提高 <code>--chunk-size</code> 加快 copy（同時用 <code>--max-load</code> 防過載）</li>
<li>真的追不上、考慮 <em>暫停部分寫入流量</em>（throttle traffic，而非 throttle tool）</li>
</ul>
<h3 id="3-foreign-key-constraint--兩工具都尷尬">3. Foreign key constraint — 兩工具都尷尬</h3>
<p>原表有 FK 指向（其他 table FK references 這張表）、ghost table 切換時 <em>新 ghost 沒有那些 FK 指向</em>。Cut-over 一瞬間、FK 從指向「原表」變成指向「archive 表」、外部 constraint 失效。</p>
<p>修法（pt-osc）：</p>
<ul>
<li>用 <code>--alter-foreign-keys-method=rebuild_constraints</code>：先 ALTER 外部 table FK 指向 ghost、再 cut-over</li>
<li>或 <code>drop_swap</code>：cut-over 前 drop FK、cut-over 後 recreate（更快但 cut-over 期間 FK 失效）</li>
</ul>
<p>修法（gh-ost）：</p>
<ul>
<li>gh-ost 不支援 — 手動 drop FK / 重 setup FK</li>
<li>或維護 schema 改 FK 結構（FK 改在 application 層 enforce）</li>
</ul>
<h3 id="4-pt-osc-trigger-跟-application-既有-trigger-衝突">4. pt-osc trigger 跟 application 既有 trigger 衝突</h3>
<p>原表上已經有 application 自建 trigger、pt-osc 在原表 <em>再加 3 個 trigger</em>、新舊 trigger 執行順序 MySQL 不保證（多 trigger 同事件按 <em>未定義順序</em>）。Application 行為可能 subtly broken。</p>
<p>修法：</p>
<ul>
<li>跑 pt-osc 前 audit 原表 trigger（<code>SHOW TRIGGERS FROM production LIKE 'orders'</code>）</li>
<li>如果有 application trigger、考慮 <em>暫時 disable 再 ALTER</em> 或改 gh-ost</li>
<li>gh-ost 不在原表加 trigger、不會碰到這個問題</li>
</ul>
<h3 id="5-cut-over-瞬間-deadlock--兩工具都有但表現不同">5. Cut-over 瞬間 deadlock — 兩工具都有但表現不同</h3>
<p>Cut-over 用 <code>RENAME TABLE original TO archive, ghost TO original</code>（atomic operation）。但 cut-over 瞬間需要 <em>metadata lock</em>、跟 <em>進行中的 long-running transaction</em> 衝突會 wait。Long-running transaction 持續、cut-over 永遠 wait、最後 timeout 失敗。</p>
<p>修法（gh-ost）：</p>
<ul>
<li><code>--cut-over-lock-timeout-seconds=3</code>、超時 abort、稍後 retry</li>
<li><code>--postpone-cut-over-flag-file</code>：先把 copy 跑完、等流量空檔再觸發 cut-over</li>
</ul>
<p>修法（pt-osc）：</p>
<ul>
<li><code>--set-vars=&quot;lock_wait_timeout=60&quot;</code>、cut-over 等更久（風險：long transaction 撐住更久 server 更多 lock wait）</li>
<li>或排在 long transaction 已知不會跑的時段（nightly backup 後）</li>
</ul>
<h2 id="容量--時間估算">容量 / 時間估算</h2>
<p>對 100 GB 表、ALTER 加 column + 加 index 為例：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>pt-osc</th>
          <th>gh-ost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>估算總時間</td>
          <td>6-12 小時（依 chunk size + load）</td>
          <td>5-10 小時（同上、可動態調整）</td>
      </tr>
      <tr>
          <td>寫吞吐影響</td>
          <td>-10% ~ -30%（trigger overhead）</td>
          <td>&lt; 5%（binlog 已存在）</td>
      </tr>
      <tr>
          <td>Replica lag</td>
          <td>1-10 秒（trigger 在 replica 跑）</td>
          <td>自動 throttle 在 threshold 內</td>
      </tr>
      <tr>
          <td>Disk 額外需求</td>
          <td>~原表大小 + index（ghost 用）</td>
          <td>同左</td>
      </tr>
      <tr>
          <td>Rollback 成本</td>
          <td>中（清 trigger）</td>
          <td>低（drop ghost）</td>
      </tr>
  </tbody>
</table>
<p>兩工具總時間接近、<em>影響 production 的差異大</em>。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-gtid--replication-topology">跟 GTID / Replication topology</h3>
<p>兩工具都 <em>依賴 replication</em> — pt-osc 透過 trigger 確保 replica 同步、gh-ost 直接從 replica 讀 binlog。Pre-requisite：</p>
<ul>
<li>Binlog <code>ROW</code> format（兩工具都要）</li>
<li>GTID 啟用（gh-ost 更需要、binlog re-pointing 容易）</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a></li>
</ul>
<h3 id="跟-vitess">跟 Vitess</h3>
<p>Vitess 有自己的 <em>VReplication-based online DDL</em>、不用 gh-ost 或 pt-osc。Vitess online DDL 在 shard 內部用類似 gh-ost 的 binlog stream 機制、但有 Vitess-aware schema management。詳見 <em>Vitess sharding 設計</em> 篇（待寫）。</p>
<h3 id="跟-aurora-mysql">跟 Aurora MySQL</h3>
<p>Aurora MySQL 仍支援 gh-ost / pt-osc、但 <em>Aurora 自己的 fast DDL</em>（部分 ALTER） 比 8.0 Instant DDL 更廣。先檢查 Aurora 文件、能用 native fast DDL 就不用 ghost table tool。詳見 <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 page</a>。</p>
<h3 id="跟-planetscale">跟 PlanetScale</h3>
<p>PlanetScale（managed Vitess）走 <em>branch-based schema migration</em> — 建 schema branch、跑 schema change、deploy 時 atomic merge。schema change 由 PlanetScale 內建流程承擔。詳見 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">PlanetScale migration playbook</a>。</p>
<h2 id="production-casegh-ost-operation-workflow">Production case：gh-ost operation workflow</h2>
<p>Online schema change 的 production 責任是把大表 DDL 拆成可暫停、可節流、可切換的資料搬移流程。gh-ost 作為 GitHub 開源工具，把 schema change 轉成 ghost table copy、binlog tailing 與 controlled cutover；這讓 operator 可以在 replica lag、application load 或部署窗口變化時調整速度。</p>
<p>這個案例要回收到三個操作判準。第一，throttle 指標要接 production SLO，例如 replica lag、thread running、application latency 或錯誤率，而非只看 copy rows/sec。第二，pause / resume 是變更治理能力，代表 schema change 可以配合 incident response、deploy freeze 與商業尖峰窗口。第三，cutover 要設 rollback window 與 owner，因為 rename table 的瞬間仍是高風險控制點。</p>
<p>gh-ost workflow 的 sibling 路由是 <a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PostgreSQL Online Schema Change</a>。PostgreSQL 常靠 fast ALTER、MVCC 與 extension 工具解決同類需求；MySQL 的 ghost table tool 更常成為標準路徑，主因是大表 DDL、metadata lock 與 replication event 的組合壓力不同。</p>
<h2 id="何時用哪一個">何時用哪一個</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>選擇</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>標準 production write &lt; 50% capacity</td>
          <td>gh-ost（預設）</td>
          <td>寫入 overhead 0、控制更細</td>
      </tr>
      <tr>
          <td>高寫吞吐 (&gt; 80% capacity)</td>
          <td>gh-ost（必須）</td>
          <td>pt-osc trigger overhead 直接 OOM</td>
      </tr>
      <tr>
          <td>有 FK constraint 需要保留</td>
          <td>pt-osc</td>
          <td>gh-ost 不處理 FK</td>
      </tr>
      <tr>
          <td>有 application-side trigger 在原表</td>
          <td>gh-ost</td>
          <td>pt-osc trigger 跟既有 trigger 不可預期</td>
      </tr>
      <tr>
          <td>需要 pause / resume 能力</td>
          <td>gh-ost</td>
          <td>pt-osc 不支援</td>
      </tr>
      <tr>
          <td>已用 Percona Toolkit 整套（pt-table-checksum / pt-archiver）</td>
          <td>pt-osc</td>
          <td>工具鏈一致</td>
      </tr>
      <tr>
          <td>已用 Vitess</td>
          <td>Vitess online DDL</td>
          <td>維持 Vitess schema workflow</td>
      </tr>
      <tr>
          <td>已用 PlanetScale</td>
          <td>branch-based</td>
          <td>維持 PlanetScale schema workflow</td>
      </tr>
      <tr>
          <td>已用 Aurora MySQL + native fast DDL OK</td>
          <td>不用 ghost table</td>
          <td>直接 ALTER</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（binlog ROW format + GTID 是 pre-requisite）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PostgreSQL Online Schema Change</a>（PG sibling、為什麼 PG 比 MySQL 少用 ghost table — fast ALTER 覆蓋多數變更）</li>
<li><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 page</a>（managed MySQL fast DDL）</li>
<li><a href="https://planetscale.com/">PlanetScale</a>（branch-based 不用 ghost table）</li>
<li><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a>（schema migration 治理）</li>
<li><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract 卡片</a>（schema migration 設計原則）</li>
<li>官方：<a href="https://github.com/github/gh-ost">gh-ost</a> / <a href="https://docs.percona.com/percona-toolkit/pt-online-schema-change.html">pt-online-schema-change</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/online-schema-change/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/online-schema-change/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>online schema change&lt;/em> — 先看 PG ALTER 哪些已 fast catalog-only、再看 pg_repack / pg-osc 何時必要。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>跟 MySQL 不同：PG 大量 schema change &lt;em>內建&lt;/em> fast catalog-only 行為、不必走 ghost table tool。MySQL 對應的 gh-ost / pt-online-schema-change 之於 PG 是 &lt;em>少數場景才需要的 escape hatch&lt;/em>、不是 standard practice。&lt;/p>
&lt;p>寫作 OSC 時必須 &lt;em>先看 PG 自身 ALTER 行為&lt;/em>、確認真的需要再上 pg_repack / pg-osc — 否則徒增複雜度。&lt;/p>
&lt;h2 id="pg-alter-table-的-fast--slow-分類">PG ALTER TABLE 的 fast / slow 分類&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- ALTER TABLE 的操作大致三類&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="類-afast-catalog-only-1-秒metadata-改">類 A：Fast catalog-only（&amp;lt; 1 秒、metadata 改）&lt;/h3>
&lt;p>PG 9.4+ / 11+ 多數 ALTER 已 catalog-only：&lt;/p>
&lt;ul>
&lt;li>&lt;code>ADD COLUMN col TYPE NULL DEFAULT NULL&lt;/code> — 直接 metadata、不 rewrite&lt;/li>
&lt;li>&lt;code>ADD COLUMN col TYPE NOT NULL DEFAULT &amp;lt;constant&amp;gt;&lt;/code>（PG 11+）— optimizer 把 default 存在 metadata、舊 row read 時動態返回 default、不 rewrite&lt;/li>
&lt;li>&lt;code>DROP COLUMN&lt;/code> — metadata 標 dropped、實際 row 不 rewrite（VACUUM 之後逐步清理）&lt;/li>
&lt;li>&lt;code>ALTER COLUMN ... SET DEFAULT &amp;lt;constant&amp;gt;&lt;/code> — metadata&lt;/li>
&lt;li>&lt;code>RENAME COLUMN&lt;/code> / &lt;code>RENAME TABLE&lt;/code> — metadata&lt;/li>
&lt;li>&lt;code>ADD CONSTRAINT ... NOT VALID&lt;/code> — 標記 constraint 不 validate、之後 &lt;code>VALIDATE CONSTRAINT&lt;/code> 才 scan&lt;/li>
&lt;li>&lt;code>ALTER COLUMN ... TYPE&lt;/code> 同 binary-compat 類型（&lt;code>VARCHAR(10) → VARCHAR(20)&lt;/code>、&lt;code>TEXT → VARCHAR&lt;/code> 等）— catalog-only&lt;/li>
&lt;/ul>
&lt;p>這類 ALTER &lt;em>直接跑、不必任何工具&lt;/em>。&lt;/p>
&lt;h3 id="類-block-heavyrewrites-tableproduction-慎用">類 B：Lock heavy（rewrites table、production 慎用）&lt;/h3>
&lt;p>需要 &lt;em>rewrite 整張 table&lt;/em>、ACCESS EXCLUSIVE lock 整個 ALTER 期間：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>online schema change</em> — 先看 PG ALTER 哪些已 fast catalog-only、再看 pg_repack / pg-osc 何時必要。</p></blockquote>
<hr>
<p>跟 MySQL 不同：PG 大量 schema change <em>內建</em> fast catalog-only 行為、不必走 ghost table tool。MySQL 對應的 gh-ost / pt-online-schema-change 之於 PG 是 <em>少數場景才需要的 escape hatch</em>、不是 standard practice。</p>
<p>寫作 OSC 時必須 <em>先看 PG 自身 ALTER 行為</em>、確認真的需要再上 pg_repack / pg-osc — 否則徒增複雜度。</p>
<h2 id="pg-alter-table-的-fast--slow-分類">PG ALTER TABLE 的 fast / slow 分類</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- ALTER TABLE 的操作大致三類</span></span></span></code></pre></div><h3 id="類-afast-catalog-only-1-秒metadata-改">類 A：Fast catalog-only（&lt; 1 秒、metadata 改）</h3>
<p>PG 9.4+ / 11+ 多數 ALTER 已 catalog-only：</p>
<ul>
<li><code>ADD COLUMN col TYPE NULL DEFAULT NULL</code> — 直接 metadata、不 rewrite</li>
<li><code>ADD COLUMN col TYPE NOT NULL DEFAULT &lt;constant&gt;</code>（PG 11+）— optimizer 把 default 存在 metadata、舊 row read 時動態返回 default、不 rewrite</li>
<li><code>DROP COLUMN</code> — metadata 標 dropped、實際 row 不 rewrite（VACUUM 之後逐步清理）</li>
<li><code>ALTER COLUMN ... SET DEFAULT &lt;constant&gt;</code> — metadata</li>
<li><code>RENAME COLUMN</code> / <code>RENAME TABLE</code> — metadata</li>
<li><code>ADD CONSTRAINT ... NOT VALID</code> — 標記 constraint 不 validate、之後 <code>VALIDATE CONSTRAINT</code> 才 scan</li>
<li><code>ALTER COLUMN ... TYPE</code> 同 binary-compat 類型（<code>VARCHAR(10) → VARCHAR(20)</code>、<code>TEXT → VARCHAR</code> 等）— catalog-only</li>
</ul>
<p>這類 ALTER <em>直接跑、不必任何工具</em>。</p>
<h3 id="類-block-heavyrewrites-tableproduction-慎用">類 B：Lock heavy（rewrites table、production 慎用）</h3>
<p>需要 <em>rewrite 整張 table</em>、ACCESS EXCLUSIVE lock 整個 ALTER 期間：</p>
<ul>
<li><code>ALTER COLUMN ... TYPE</code> binary 不相容類型（<code>INT → BIGINT</code> 永遠 rewrite、<code>TEXT → INT</code> 也是）— 雖然語意「擴大」、底層 4-byte 跟 8-byte storage 不同、全表 rewrite + ACCESS EXCLUSIVE 不可省</li>
<li><code>ALTER COLUMN ... SET NOT NULL</code> 對既有 nullable column（要 scan 整 table）</li>
<li><code>ALTER COLUMN ... DROP IDENTITY</code></li>
<li><code>ALTER TABLE ... SET TABLESPACE</code></li>
</ul>
<p>這類 ALTER 對大表 <em>production 不能直接跑</em>、要 ghost table tool。</p>
<h3 id="類-cconcurrent-index--online-operation無-table-lock">類 C：Concurrent index / online operation（無 table lock）</h3>
<ul>
<li><code>CREATE INDEX CONCURRENTLY</code> — 不 lock 寫入、background build、慢但安全</li>
<li><code>REINDEX INDEX CONCURRENTLY</code>（PG 12+） — 同上</li>
<li><code>DROP INDEX CONCURRENTLY</code> — 短 ACCESS EXCLUSIVE lock 只在最後 swap</li>
</ul>
<h2 id="何時需要-ghost-table-tool">何時需要 ghost table tool</h2>
<p>只在以下場景才需要 pg_repack / pg-osc：</p>
<ol>
<li><strong>Rewrite-required type change</strong>（類 B <code>ALTER COLUMN TYPE</code>）對大表</li>
<li><strong>VACUUM FULL 替代</strong>：pg_repack 比 VACUUM FULL 安全（不 lock 整表）</li>
<li><strong>Bloat 重組</strong>：大表 dead tuple 累積、想完整 rewrite</li>
</ol>
<p>對「add column」「drop column」「create index」等場景 <em>PG 內建 fast 已夠</em>、不必 ghost table tool。</p>
<h2 id="tool-1pg_repack--trigger-based--雙-table-swap">Tool 1：pg_repack — Trigger-based + 雙 table swap</h2>
<p>pg_repack 是 PG community 標準 online table rewrite 工具：</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">pg_repack -h primary.example.com -p <span class="m">5432</span> -d production -U postgres <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table<span class="o">=</span>orders --no-superuser-check</span></span></code></pre></div><p><strong>Mechanism</strong>：</p>
<ol>
<li>CREATE <code>repack.table_&lt;oid&gt;</code> 跟原表同 schema</li>
<li>在原表加 3 個 trigger：INSERT / UPDATE / DELETE → 寫入 log table <code>repack.log_&lt;oid&gt;</code></li>
<li>從原表 <code>INSERT INTO repack.table_&lt;oid&gt; SELECT * FROM original</code> 複製 row</li>
<li>邊複製邊 apply log table 紀錄的變更</li>
<li>切換：rename 原表 → original_old、rename repack.table_<oid> → original（atomic）</li>
<li>Drop 舊原表跟 trigger / log</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li><em>Trigger overhead</em>：每個 primary 寫入加 trigger 執行（10-30% 寫吞吐降）</li>
<li><em>FK 處理</em>：需要 drop &amp; re-create FK referencing original table（pg_repack 自動處理但有 lock window）</li>
<li>適用 <em>PG-version 綁定</em> — pg_repack 13 不能對 PG 14 cluster 跑</li>
</ul>
<p><strong>配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Primary 安裝
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_repack</span><span class="p">;</span></span></span></code></pre></div>




<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"># Repack orders</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pg_repack -d production --table<span class="o">=</span>orders
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 監控 lock：另一 session 跑 SELECT * FROM pg_stat_activity</span></span></span></code></pre></div><h2 id="tool-2pg-osc--pg-online-schema-change--wal-shipping-style">Tool 2：pg-osc / pg-online-schema-change — WAL-shipping style</h2>
<p><a href="https://github.com/shayonj/pg-osc">pg-osc</a>（Shayon Mukherjee、2023）是較新的工具、模仿 gh-ost mechanism：</p>
<p><strong>Mechanism</strong>：</p>
<ol>
<li>用 logical replication slot 從 primary WAL stream 變更</li>
<li>CREATE shadow table + 套 ALTER 變更</li>
<li>Stream WAL event 同步 shadow table（不靠 trigger）</li>
<li>完成後 swap</li>
</ol>
<p><strong>Trade-off</strong>：</p>
<ul>
<li><em>Primary 寫入 overhead</em>：0（WAL 已存在）</li>
<li>比 pg_repack 較新（社群驗證度低）</li>
<li>適合 <em>trigger overhead 不可接受</em> 的高吞吐 production</li>
</ul>
<p><strong>配置</strong>：</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"># 用 gem install</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gem install pg_online_schema_change
</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"># Run</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pg-online-schema-change perform <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --alter-statement<span class="o">=</span><span class="s2">&#34;ALTER TABLE orders ADD COLUMN status VARCHAR(20)&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --schema<span class="o">=</span>public <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --dbname<span class="o">=</span>production <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --host<span class="o">=</span>primary.example.com</span></span></code></pre></div><h2 id="配置-step-by-steppg_repack-為主">配置 step-by-step（pg_repack 為主）</h2>
<p>實務多數 PG OSC 用 pg_repack。pg-osc 是 high-write-throughput escape hatch。</p>
<h3 id="step-1安裝--確認版本">Step 1：安裝 + 確認版本</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 安裝 pg_repack（versioned）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_repack</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_available_extensions</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pg_repack&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 確認 installed_version 跟 PG major version 對齊</span></span></span></code></pre></div><h3 id="step-2跑-pg_repack">Step 2：跑 pg_repack</h3>





<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">pg_repack -h primary -d production -U postgres <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table<span class="o">=</span>orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --jobs<span class="o">=</span><span class="m">4</span> <span class="se">\ </span>                      <span class="c1"># 並行 worker</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  --wait-timeout<span class="o">=</span><span class="m">60</span> <span class="se">\ </span>             <span class="c1"># 等 lock 超時（秒）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  --no-kill-backend                <span class="c1"># 不主動 kill 卡 lock 的 query</span></span></span></code></pre></div><h3 id="step-3監控">Step 3：監控</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 pg_repack 進度
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">wait_event_type</span><span class="p">,</span><span class="w"> </span><span class="n">wait_event</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">query</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;%repack%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- 看 lock 狀態
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_locks</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">relation</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> </span><span class="n">oid</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_class</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">relname</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;orders&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;repack.table_xxx&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><h3 id="step-4驗證">Step 4：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 跑完後對比 row count + 抽樣 query
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 跟 pg_repack 之前 count 對比</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-alter-直接跑沒看是不是-fast-變-lock-heavy">1. ALTER 直接跑沒看是不是 fast 變 lock heavy</h3>
<p><code>ALTER TABLE orders ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending'</code> — 預期 catalog-only（PG 11+）、但若 PG 10 跑這個就會 rewrite 整表、ACCESS EXCLUSIVE lock 幾小時。</p>
<p>修法：</p>
<ul>
<li>寫 schema migration 前 <em>確認 PG version</em></li>
<li>看 <a href="https://www.postgresql.org/docs/current/sql-altertable.html">PG ALTER doc</a>、each subcommand 標 <em>Note</em> 段是否 fast</li>
<li>Production 跑前 staging 測 + 監控 <code>pg_stat_activity</code> lock wait</li>
</ul>
<h3 id="2-vacuum-full-誤用--production-downtime">2. VACUUM FULL 誤用 — Production downtime</h3>
<p><code>VACUUM FULL</code> 等於「rewrite 整表 + ACCESS EXCLUSIVE lock」。Production 跑 = 表變 unavailable 幾分鐘到幾小時。</p>
<p>修法：</p>
<ul>
<li><em>永遠用 pg_repack</em> 取代 VACUUM FULL（除非 maintenance window）</li>
<li>對 bloat 議題、定期跑 pg_repack</li>
<li>autovacuum tuning 第一優先（<a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum-tuning</a> 詳細）</li>
</ul>
<h3 id="3-pg_repack-version-mismatch">3. pg_repack version mismatch</h3>
<p>PG cluster 升 14、但 <code>pg_repack</code> extension 還是 13 版本。試 ALTER 跑 <code>pg_repack</code> 命令、ERROR: <code>program &quot;pg_repack 14.x&quot; does not match installed extension &quot;pg_repack 13.x&quot;</code>。</p>
<p>修法：</p>
<ul>
<li>升 PG cluster 後 <em>立即 ALTER EXTENSION pg_repack UPDATE</em></li>
<li>若 pg_repack 還沒釋出對應 PG 版本（早期升級）、暫時用 pg-osc 替代或等待</li>
<li>升級 runbook 紀錄 pg_repack 是 <em>必同步升級的 extension</em></li>
</ul>
<h3 id="4-create-index-concurrently-失敗清理">4. CREATE INDEX CONCURRENTLY 失敗清理</h3>
<p><code>CREATE INDEX CONCURRENTLY</code> 跑到一半被 cancel（用戶 Ctrl-C / connection drop）、產生 <em>invalid index</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">indexrelid</span><span class="p">::</span><span class="n">regclass</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_index</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="n">indisvalid</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 顯示一個 idx_orders_status_invalid</span></span></span></code></pre></div><p>Invalid index 仍佔 disk、但 optimizer 不會用。</p>
<p>修法：</p>
<ul>
<li>跑 <code>DROP INDEX CONCURRENTLY idx_orders_status_invalid</code></li>
<li>之後重新 <code>CREATE INDEX CONCURRENTLY</code></li>
<li>避免在 connection 不穩的 session 跑長時間 CREATE INDEX CONCURRENTLY、改用 cron 或 deploy pipeline</li>
</ul>
<h3 id="5-generated-stored-column-不能-online-add">5. Generated stored column 不能 online ADD</h3>
<p><code>ADD COLUMN total NUMERIC GENERATED ALWAYS AS (price * qty) STORED</code> — <em>stored</em> generated column 必須 rewrite 整表計算 column value、不是 catalog-only。</p>
<p>修法：</p>
<ul>
<li>
<p>用 <code>GENERATED ALWAYS AS (...) VIRTUAL</code>（PG 18+）— 不存實際 value、catalog-only</p>
</li>
<li>
<p>或 <em>先加 nullable column + backfill + 加 NOT NULL constraint</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="nb">NUMERIC</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">price</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">qty</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="p">...;</span><span class="w">  </span><span class="c1">-- chunked
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 之後加 trigger 或 application 層維護 total</span></span></span></code></pre></div></li>
<li>
<p>或用 pg_repack 跑 rewrite ADD GENERATED STORED</p>
</li>
</ul>
<h2 id="容量--時間估算">容量 / 時間估算</h2>
<p>對 100 GB 表、ADD COLUMN 加 index 為例：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>時間</th>
          <th>Lock 影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ADD COLUMN col TYPE NULL</code> (PG 11+)</td>
          <td>&lt; 1 秒</td>
          <td>ACCESS EXCLUSIVE（毫秒級）</td>
      </tr>
      <tr>
          <td><code>ADD COLUMN col TYPE NOT NULL DEFAULT 0</code> (PG 11+)</td>
          <td>&lt; 1 秒</td>
          <td>ACCESS EXCLUSIVE（毫秒級）</td>
      </tr>
      <tr>
          <td><code>CREATE INDEX CONCURRENTLY</code></td>
          <td>2-6 小時</td>
          <td>無 table lock</td>
      </tr>
      <tr>
          <td><code>pg_repack table</code></td>
          <td>4-8 小時</td>
          <td>短 ACCESS EXCLUSIVE（swap）</td>
      </tr>
      <tr>
          <td><code>ALTER COLUMN TYPE</code> rewrite</td>
          <td>4-8 小時</td>
          <td>ACCESS EXCLUSIVE 全程</td>
      </tr>
      <tr>
          <td><code>VACUUM FULL</code></td>
          <td>同 pg_repack</td>
          <td>ACCESS EXCLUSIVE 全程（不要跑）</td>
      </tr>
  </tbody>
</table>
<h2 id="跟-mysql-gh-ost--pt-osc-對照">跟 MySQL gh-ost / pt-osc 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG pg_repack</th>
          <th>PG pg-osc</th>
          <th>MySQL gh-ost</th>
          <th>MySQL pt-osc</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>機制</td>
          <td>Trigger + log table</td>
          <td>WAL logical stream</td>
          <td>Binlog stream</td>
          <td>Trigger + log table</td>
      </tr>
      <tr>
          <td>Primary 寫 overhead</td>
          <td>中（trigger）</td>
          <td>0（WAL 已存在）</td>
          <td>0（binlog 已存在）</td>
          <td>中（trigger）</td>
      </tr>
      <tr>
          <td>Throttle 支援</td>
          <td>部分</td>
          <td>支援</td>
          <td>強</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>Pause / Resume</td>
          <td>不支援</td>
          <td>不支援</td>
          <td>支援</td>
          <td>不支援</td>
      </tr>
      <tr>
          <td>工具成熟度</td>
          <td>高</td>
          <td>中（2023+）</td>
          <td>高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Use case 比例</td>
          <td>PG 主流（90% case）</td>
          <td>高吞吐 escape hatch</td>
          <td>MySQL 主流（dev）</td>
          <td>MySQL legacy + FK</td>
      </tr>
  </tbody>
</table>
<p>PG OSC tool 使用頻率比 MySQL 低 — 因為 PG 內建 fast ALTER 已 cover 90% schema change、ghost table tool 只對 <em>少數 rewrite-required</em> 場景。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a> — sibling、不同 use case mix。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>ALTER TABLE / pg_repack / pg-osc 都產生 WAL、會 replicate 到 standby。Standby 上的 long-running query 可能跟 ALTER 衝突、被 <code>hot_standby_feedback</code> 影響 primary autovacuum。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-autovacuum-tuning">跟 Autovacuum Tuning</h3>
<p>Schema change 後常產生 dead tuple、autovacuum 需要重新 cover。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h3 id="跟-logical-replication">跟 Logical Replication</h3>
<p>logical replication 透過 publication / subscription 同步 — DDL <em>不會</em> logical replicate（PG 16 之前）、必須 <em>在 publisher / subscriber 各自跑 DDL</em>。詳見 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a>。</p>
<h3 id="跟-patroni-ha">跟 Patroni HA</h3>
<p>Patroni promote 新 primary 後、pg_repack extension state（slot / catalog）跟著走、新 primary 仍可繼續 pg_repack。詳見 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a>。</p>
<h2 id="何時用哪個">何時用哪個</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ADD COLUMN nullable / DROP COLUMN / RENAME 等</td>
          <td>直接 ALTER（fast catalog-only）</td>
      </tr>
      <tr>
          <td>CREATE INDEX 大表</td>
          <td><code>CREATE INDEX CONCURRENTLY</code></td>
      </tr>
      <tr>
          <td>ALTER COLUMN TYPE rewrite（大表）</td>
          <td>pg_repack</td>
      </tr>
      <tr>
          <td>Bloat 重組</td>
          <td>pg_repack</td>
      </tr>
      <tr>
          <td>高吞吐 + trigger overhead 不可接受</td>
          <td>pg-osc</td>
      </tr>
      <tr>
          <td>ADD GENERATED STORED column</td>
          <td>nullable + backfill + constraint</td>
      </tr>
      <tr>
          <td>Cluster on Cloud（RDS / Aurora）</td>
          <td>RDS / Aurora 內建 fast DDL 多數已 cover、pg_repack 視 vendor 支援</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（ALTER 跟 streaming replication 互動）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（schema change 後 vacuum 議題）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PG Logical Replication + Debezium</a>（DDL 不 replicate 議題）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PG Patroni HA</a>（HA 跟 pg_repack 整合）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（sibling、tool ecosystem 不同）</li>
<li><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract 卡片</a>（schema migration 設計原則）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/sql-altertable.html">ALTER TABLE</a> / <a href="https://github.com/reorg/pg_repack">pg_repack GitHub</a> / <a href="https://github.com/shayonj/pg-osc">pg-osc GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift&lt;/em>；本文是 &lt;em>paradigm reduction&lt;/em>（downgrade 方向）的 dogfood。&lt;/p>&lt;/blockquote>
&lt;h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm&lt;/h2>
&lt;p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>Redis&lt;/th>
 &lt;th>Memcached&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Multi-paradigm（KV + 資料結構 + pub/sub + script）&lt;/td>
 &lt;td>Pure cache（KV + TTL）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Value 類型&lt;/td>
 &lt;td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog&lt;/td>
 &lt;td>byte string only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atomic operations&lt;/td>
 &lt;td>100+（INCR / LPUSH / ZADD / &amp;hellip;）&lt;/td>
 &lt;td>INCR / DECR / APPEND / CAS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Server-side scripting&lt;/td>
 &lt;td>Lua scripts (&lt;code>EVAL&lt;/code>)&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pub/Sub&lt;/td>
 &lt;td>Native&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Persistence&lt;/td>
 &lt;td>RDB / AOF&lt;/td>
 &lt;td>無（restart 全失）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication&lt;/td>
 &lt;td>Async / sync replication&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cluster&lt;/td>
 &lt;td>Redis Cluster + Sentinel HA&lt;/td>
 &lt;td>Memcached cluster（client-side sharding）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Eviction policy&lt;/td>
 &lt;td>8 種（LRU / LFU / random / &amp;hellip;）&lt;/td>
 &lt;td>LRU only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expiration accuracy&lt;/td>
 &lt;td>TTL 精確到 ms&lt;/td>
 &lt;td>TTL 精確到 second、lazy expiration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。&lt;/strong> Redis 的 features（hash / sorted set / pub/sub）多數 &lt;em>不該移除&lt;/em>、是 &lt;em>重新分配到對應 specialized service&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>Hash / sorted set → application 端用 JSON + 自管 index&lt;/li>
&lt;li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）&lt;/li>
&lt;li>Lua scripts → application code&lt;/li>
&lt;li>Persistence → 真正需要的 data 該存 DB、不是 cache&lt;/li>
&lt;li>Replication / cluster → Memcached 自己 cluster strategy&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Operational simplification&lt;/strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong>：對 &lt;em>純 cache use case&lt;/em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）&lt;/li>
&lt;li>&lt;strong>Strict cache discipline&lt;/strong>：Memcached &lt;em>逼&lt;/em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 &lt;em>poor man&amp;rsquo;s database&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>反向 driver（Memcached → Redis）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> 跟 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift</em>；本文是 <em>paradigm reduction</em>（downgrade 方向）的 dogfood。</p></blockquote>
<h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm</h2>
<p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Multi-paradigm（KV + 資料結構 + pub/sub + script）</td>
          <td>Pure cache（KV + TTL）</td>
      </tr>
      <tr>
          <td>Value 類型</td>
          <td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog</td>
          <td>byte string only</td>
      </tr>
      <tr>
          <td>Atomic operations</td>
          <td>100+（INCR / LPUSH / ZADD / &hellip;）</td>
          <td>INCR / DECR / APPEND / CAS</td>
      </tr>
      <tr>
          <td>Server-side scripting</td>
          <td>Lua scripts (<code>EVAL</code>)</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>Native</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF</td>
          <td>無（restart 全失）</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Async / sync replication</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Cluster</td>
          <td>Redis Cluster + Sentinel HA</td>
          <td>Memcached cluster（client-side sharding）</td>
      </tr>
      <tr>
          <td>Eviction policy</td>
          <td>8 種（LRU / LFU / random / &hellip;）</td>
          <td>LRU only</td>
      </tr>
      <tr>
          <td>Expiration accuracy</td>
          <td>TTL 精確到 ms</td>
          <td>TTL 精確到 second、lazy expiration</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。</strong> Redis 的 features（hash / sorted set / pub/sub）多數 <em>不該移除</em>、是 <em>重新分配到對應 specialized service</em>：</p>
<ul>
<li>Hash / sorted set → application 端用 JSON + 自管 index</li>
<li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）</li>
<li>Lua scripts → application code</li>
<li>Persistence → 真正需要的 data 該存 DB、不是 cache</li>
<li>Replication / cluster → Memcached 自己 cluster strategy</li>
</ul>
<h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver</h2>
<ul>
<li><strong>Operational simplification</strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family</li>
<li><strong>Cost</strong>：對 <em>純 cache use case</em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）</li>
<li><strong>Strict cache discipline</strong>：Memcached <em>逼</em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 <em>poor man&rsquo;s database</em></li>
</ul>
<p>反向 driver（Memcached → Redis）：</p>
<ul>
<li>Application 寫到 Memcached 後發現需要 <em>atomic counter / leaderboard / queue / lock</em>、應該升 Redis（不是繼續 wrap Memcached）</li>
</ul>
<h2 id="跑-6-維-audit">跑 6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Redis 命令集 → Memcached 命令集、相容度 &lt; 20%</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>兩者都簡單、Memcached 略簡單</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Multi-paradigm → pure cache</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 cache service</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>必改（任何 hash / list / sorted set / pubsub 用法）</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single instance / cluster</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>3 維 High（Schema / Paradigm / Application change）多軸高、主導維度 = Paradigm → <strong>Type E paradigm shift</strong>；Schema + Application change 抽獨立段補充。</p>
<h2 id="結構類-type-e--paradigm-reduction-分配路線">結構：類 Type E + paradigm reduction 分配路線</h2>





<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. Memcached 不是 simpler Redis（concept reverse 開頭）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 為什麼遷
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">3. 6 維 audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">4. Paradigm reduction 路線（Redis features 對應的 specialized service）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">5. Schema 差段（Redis vs Memcached command set）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">6. Application 重設計（per-call-site refactor）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">7. Migration 流程（漸進、部分 use case 切）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">8. Production 故障演練
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">9. Capacity / cost
</span></span><span class="line"><span class="ln">10</span><span class="cl">10. 整合 / 下一步</span></span></code></pre></div><p>10 章節、220-260 行。比 Type E（<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a>）多 <em>paradigm reduction 路線</em> 段。</p>
<h2 id="paradigm-reduction-路線">Paradigm reduction 路線</h2>
<p>Redis features 對應的 specialized service：</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">Redis Hash           → Application 端 JSON.stringify + Memcached SET
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">                       (or 直接存 DB + Memcached cache layer)
</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">Redis List (queue)   → NATS / Kafka / RabbitMQ / SQS
</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">Redis List (stack)   → Application 端用 array + 自管 LIFO
</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">Redis Set            → Application 端用 array + dedup OR 用 DB unique index
</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">Redis Sorted Set     → Application 端用 ordered list + comparator
</span></span><span class="line"><span class="ln">11</span><span class="cl">                       OR PostgreSQL + index
</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">Redis Stream         → Kafka / Redis Streams (保留) / NATS JetStream
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">Redis Pub/Sub        → NATS Core / Redis Streams / Kafka
</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">Redis Lua script     → Application code（避免 atomic 假設）
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">Redis distributed lock → Consul / etcd / DB advisory lock / Redis (保留)
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">Redis Bitmap         → DB bit column / 應用端 bitset
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">Redis HyperLogLog    → DB approx_count_distinct / 應用端 cardinality estimator</span></span></code></pre></div><p>Migration scope 包含 <em>每個 Redis-specific feature use case 對應的 service 評估</em>；不是「移除」、是「重新分配」。</p>
<h2 id="application-重設計">Application 重設計</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Before: Redis hash</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;email&#39;</span><span class="p">,</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">,</span> <span class="s1">&#39;Alice&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># After: Memcached + JSON</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">user_data</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;email&#39;</span><span class="p">:</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Alice&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">user_data</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">mc</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</span> <span class="ow">or</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">&#39;</span><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Before: Redis sorted set (leaderboard)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">zadd</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="p">{</span><span class="s1">&#39;alice&#39;</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s1">&#39;bob&#39;</span><span class="p">:</span> <span class="mi">95</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">top_10</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">zrevrange</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="n">withscores</span><span class="o">=</span><span class="kc">True</span><span class="p">)</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="c1"># After: PostgreSQL + index + Memcached cache</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Persistent: write to DB</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Cache: pre-compute top 10 in DB query, cache in Memcached</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;leaderboard:top10&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="s1">&#39;SELECT user, score FROM scores ORDER BY score DESC LIMIT 10&#39;</span><span class="p">)))</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Before: Redis distributed lock</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">with</span> <span class="n">redis</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="s1">&#39;resource:1&#39;</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</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="c1"># After: PostgreSQL advisory lock OR Consul session</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">with</span> <span class="n">db</span><span class="o">.</span><span class="n">advisory_lock</span><span class="p">(</span><span class="n">resource_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</span></span></span></code></pre></div><p>每個 Redis-specific pattern 都要 per-call-site refactor、不是 SDK 換。</p>
<h2 id="migration-流程">Migration 流程</h2>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 同 <em>partial migration</em>：</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. Audit application code、列所有 Redis call site + feature 使用
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 按 feature 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Pure KV (GET/SET/DEL/TTL): 切 Memcached 直接
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Hash → JSON + Memcached: per-call-site refactor
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - List/Sorted Set: 評估是 queue / leaderboard / 其他用途、對應 service
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   - Pub/Sub: 移到 message queue
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - Lock: 移到 DB 或保留 Redis
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">3. 部分 application 先切（純 KV use case）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. 複雜 use case 逐步 refactor 到對應 service
</span></span><span class="line"><span class="ln">10</span><span class="cl">5. Memcached 跑 production 後、Redis 可降為 *narrow scope*（只跑剩餘 Redis-specific feature）
</span></span><span class="line"><span class="ln">11</span><span class="cl">   或完全退役（如果 application 已 refactor 乾淨）
</span></span><span class="line"><span class="ln">12</span><span class="cl">6. 長期混合架構：Memcached cache layer + DB persistent state + 可選的 Redis（locks / specialty）</span></span></code></pre></div><p>整體 3-12 個月、依 Redis-specific feature 使用深度。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1hash--json-後-getset-round-trip-變-n1">Case 1：Hash → JSON 後 GET/SET round-trip 變 N+1</h3>
<p><strong>徵兆</strong>：cutover 後 application latency p99 從 5ms 漲到 50ms；profiling 顯示「為了改 user.email、要先 GET user object → modify → SET」、原本 Redis <code>HSET</code> 1 個 round-trip 現在 2 個。</p>
<p><strong>根因</strong>：JSON-encoded value 不能 partial update、每次改一欄都要 read-modify-write。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 端 cache JSON object in memory</strong>：read-modify-write 仍 1 個 SET、但 read 是 memory</li>
<li><strong>Compare-and-swap (CAS)</strong>：Memcached CAS 防止 concurrent update lost</li>
<li><strong>Field-level cache key</strong>：把 hash 拆成 N 個 Memcached key（<code>user:123:email</code> / <code>user:123:name</code>）、避開 JSON</li>
</ol>
<h3 id="case-2sorted-set-leaderboard-退化recomputation-cost-爆">Case 2：Sorted set leaderboard 退化、recomputation cost 爆</h3>
<p><strong>徵兆</strong>：原本 Redis leaderboard <code>ZADD</code> + <code>ZREVRANGE</code> &lt; 1ms；切 DB-backed leaderboard 後 <code>SELECT ... ORDER BY ... LIMIT 10</code> 在 1M+ row 跑 100-500ms。</p>
<p><strong>根因</strong>：Memcached 不支援 sorted set、leaderboard 必須在 DB 算、N 大時 sort 慢。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Cache pre-computed top N</strong>：DB scheduled job 每分鐘算 top 100、寫 Memcached、application 讀 cache 不直查 DB</li>
<li><strong>Materialized view + index</strong>：DB 端用 materialized view + index、毫秒級 query</li>
<li><strong>保留 Redis sorted set</strong>：leaderboard 是 Redis 強項、不該退到 Memcached、走混合架構</li>
</ol>
<h3 id="case-3pubsub-移除缺-fan-out-機制">Case 3：Pub/Sub 移除、缺 fan-out 機制</h3>
<p><strong>徵兆</strong>：原本 Redis Pub/Sub 跑 cache invalidation broadcast、N 個 application instance 都收 invalidation msg；切 Memcached 後失去 broadcast、cache stale。</p>
<p><strong>根因</strong>：Memcached 沒 Pub/Sub；application 需要外部 fan-out 機制。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>NATS / Redis Streams + consumer group</strong>：each application instance 是 consumer、收 invalidation</li>
<li><strong>Database trigger + LISTEN/NOTIFY</strong>：PostgreSQL <code>LISTEN/NOTIFY</code> 對中型 fan-out 足夠</li>
<li><strong>Architecture rethink</strong>：是否真需要 broadcast invalidation？通常用 <em>TTL-based cache</em> + <em>cache key versioning</em> 就能 cover 多數 invalidation use case</li>
</ol>
<h3 id="case-4atomic-incr-沒對等race-condition">Case 4：Atomic INCR 沒對等、race condition</h3>
<p><strong>徵兆</strong>：rate limiter / counter pattern 切 Memcached、<code>mc.incr(key)</code> 在 key 不存在時 return None（不 auto-init 為 0）；application 端 <code>if None: mc.set(key, 1)</code> race condition、低機率 counter reset。</p>
<p><strong>根因</strong>：Memcached INCR 對 missing key 不像 Redis 自動 init；application 端 init logic 容易 race。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 用 ADD（atomic put-if-absent）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># only sets if missing</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">incr</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>    <span class="c1"># always works after add</span></span></span></code></pre></div><p><code>ADD</code> + <code>INCR</code> 兩個 atomic operation 合起來 race-free。</p>
<h3 id="case-5eviction-policy-差異production-cache-hit-rate-降">Case 5：Eviction policy 差異、production cache hit rate 降</h3>
<p><strong>徵兆</strong>：cutover 後 cache hit rate 從 95% 降到 80%；profiling 發現「重要 key 沒在 cache」、新 key 一直擠走熱 key。</p>
<p><strong>根因</strong>：Redis 預設 <code>allkeys-lfu</code> (least frequently used)、長期熱 key 不被擠；Memcached 只有 LRU、單純按 access time、burst access 的 cold key 擠走 long-tail hot key。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Memory headroom</strong>：Memcached memory 限制拉高 30-50%、避免 eviction pressure</li>
<li><strong>Application-side cache priority</strong>：critical key 用 <em>no-expiration set</em> + 主動 refresh</li>
<li><strong>保留 Redis for LFU workload</strong>：long-tail hot key 場景 Redis LFU 更合適、不該退 Memcached</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>+10-20%（無 metadata overhead）</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~100K ops/s single-thread</td>
          <td>~500K-1M ops/s multi-threaded</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>1-3ms</td>
          <td>0.5-1ms</td>
      </tr>
      <tr>
          <td>Persistence overhead</td>
          <td>5-15% CPU</td>
          <td>0</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.1-0.3</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low（feature 豐富）</td>
          <td>Higher（feature 移到 application）</td>
      </tr>
      <tr>
          <td>Cost per GB memory</td>
          <td>baseline</td>
          <td>略低（無 persistence I/O / replication overhead）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 cache use case 走 Memcached 省 ops + 略省 cost；application 已用 Redis-specific feature 不該切；混合架構是 long-term default。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對比</h3>
<p>兩條路：</p>
<ul>
<li>DragonflyDB：保留 Redis paradigm、優化 throughput + memory；application 不用改</li>
<li>Memcached：退到 pure cache paradigm、application 必須改、但 ops 簡化</li>
</ul>
<p>選擇取決於 <em>是否真的需要 Redis multi-paradigm features</em>：用得到就 DragonflyDB / Redis、用不到就 Memcached。</p>
<h3 id="跟-nats-整合">跟 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 整合</h3>
<p>Redis Pub/Sub 移除後、應用端 fan-out / messaging 需求轉到 NATS / Redis Streams / Kafka；本文 cross-link migration playbook <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 有 paradigm shift 流程參考。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Memcached Cluster strategy</strong>：client-side consistent hashing vs server-side cluster mode、ops 簡化 vs scalability 取捨</li>
<li><strong>Long-term mixed architecture</strong>：80% Memcached + 20% Redis 是常見 stable state、不一定要完全消除 Redis</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB</a></li>
<li>平行 Type B 對照：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（保留 paradigm）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 &lt;em>data topology 重劃&lt;/em>、不在 5 type 內。&lt;/p>&lt;/blockquote>
&lt;h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃&lt;/h2>
&lt;p>Migration 通常假設 &lt;em>source 跟 target 是不同 cluster / vendor&lt;/em>；re-sharding 是 &lt;em>同 cluster 內的 slot 重分配&lt;/em>、source 跟 target 是 &lt;em>同一個 Redis Cluster 的不同 state&lt;/em>：&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">Before re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ~ 33% load ~ 50% load ~ 17% load (heavy imbalance)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">After re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> ~ 25% load ~ 25% load ~ 25% load ~ 25% load&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>source 跟 target 是 &lt;em>同 cluster&lt;/em>、區別在 &lt;em>slot 對 node 的 mapping&lt;/em>。Application connection string 不變、cluster API 不變、data model 不變。但 &lt;em>slot migration 期間&lt;/em> application 行為跟 &lt;em>normal operation&lt;/em> 差很多 — 這是 re-sharding 主要工作。&lt;/p>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 對 Redis cluster re-sharding：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 <em>data topology 重劃</em>、不在 5 type 內。</p></blockquote>
<h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃</h2>
<p>Migration 通常假設 <em>source 跟 target 是不同 cluster / vendor</em>；re-sharding 是 <em>同 cluster 內的 slot 重分配</em>、source 跟 target 是 <em>同一個 Redis Cluster 的不同 state</em>：</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">Before re-shard:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
</span></span><span class="line"><span class="ln">3</span><span class="cl">              ~ 33% load           ~ 50% load              ~ 17% load (heavy imbalance)
</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">After re-shard:
</span></span><span class="line"><span class="ln">6</span><span class="cl">  Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
</span></span><span class="line"><span class="ln">7</span><span class="cl">              ~ 25% load           ~ 25% load              ~ 25% load              ~ 25% load</span></span></code></pre></div><p>source 跟 target 是 <em>同 cluster</em>、區別在 <em>slot 對 node 的 mapping</em>。Application connection string 不變、cluster API 不變、data model 不變。但 <em>slot migration 期間</em> application 行為跟 <em>normal operation</em> 差很多 — 這是 re-sharding 主要工作。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 對 Redis cluster re-sharding：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis、無變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 Redis Cluster、operational 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 Redis Cluster、無 paradigm 差</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個（cluster）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>多數不改、client cluster mode 自處理</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>重劃</strong> — slot mapping 跟 node 數</td>
          <td><strong>New axis</strong></td>
      </tr>
  </tbody>
</table>
<p>5 維皆 Low、對映 Type B drop-in；但 <em>data topology</em> 是 5 type 沒有的 <em>第 6 維度</em>。本文採用 <em>re-sharding-specific 結構</em>、不是 5 type 任一個。</p>
<h2 id="4-種-re-sharding-driver">4 種 re-sharding driver</h2>
<p>不同 driver 對應不同 re-sharding 策略：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
          <th>對應 re-sharding 操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot imbalance</td>
          <td>業務熱點打到部分 slot、單 node CPU / memory 80%+</td>
          <td>Rebalance（slot 重分配、不加 node）</td>
      </tr>
      <tr>
          <td>Capacity expansion</td>
          <td>整 cluster memory / throughput 上限快到、要加 node</td>
          <td>Add node + slot migration（從現有 node 搬部分 slot 過去）</td>
      </tr>
      <tr>
          <td>Node decommission</td>
          <td>老 node 硬體淘汰 / cloud instance 換代</td>
          <td>Drain（該 node 的 slot 全搬走）+ remove</td>
      </tr>
      <tr>
          <td>Hash tag refactor</td>
          <td>業務 access pattern 變、需要 co-located key 群重分組</td>
          <td>Application-side migration（不是 cluster-level）</td>
      </tr>
  </tbody>
</table>
<p>前 3 種是 cluster-internal、用 <code>redis-cli --cluster</code> 工具完成；第 4 種需要 application 端 dual-write + migration、本文不展開。</p>
<h2 id="slot-migration-機制">Slot migration 機制</h2>
<p>Redis Cluster 16384 個 slot、每個 key 經 <code>CRC16(key) % 16384</code> 對應 slot。Slot migration 過程：</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">Source node:     [slot N: MIGRATING to dest]
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Dest node:       [slot N: IMPORTING from source]
</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">Source node:     SCAN slot N → for each key:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                 1. DUMP key (serialize value)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                 2. send to dest via MIGRATE command
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                 3. dest RESTORE key
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                 4. source DEL key
</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">Source node:     [slot N: OWNED by dest]
</span></span><span class="line"><span class="ln">11</span><span class="cl">Dest node:       [slot N: OWNED]
</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">跨 cluster broadcast: slot N 屬於 dest</span></span></code></pre></div><p>期間 client 行為：</p>
<ul>
<li>Key 在 source 端（未 migrate）：source 直接 serve</li>
<li>Key 在 dest 端（已 migrate）：source 回 <code>-ASK</code> redirect、client 重發到 dest</li>
<li>寫入 MIGRATING slot 的新 key：source serve、之後也會 migrate</li>
<li>Application 不需要改 code、cluster-aware client 自動處理 <code>-ASK</code> redirect</li>
</ul>
<h2 id="redis-cli-cluster-工具">redis-cli &ndash;cluster 工具</h2>
<p>production 用 official tool、不要手寫 slot migration：</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. Rebalance（slot 重分配、適合 imbalance）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">redis-cli --cluster rebalance 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --cluster-use-empty-masters <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --cluster-threshold <span class="m">5</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"># 2. Reshard（指定來源 → 目標、適合 capacity expansion）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli --cluster reshard 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --cluster-from &lt;source-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --cluster-to &lt;dest-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --cluster-slots <span class="m">4096</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --cluster-yes
</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="c1"># 3. Add-node（加新 node 進 cluster）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli --cluster add-node 10.0.0.4:6379 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --cluster-master-id &lt;existing-master-id&gt;
</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 class="c1"># 4. Del-node（移除 node、需先 drain slot）</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">redis-cli --cluster del-node 10.0.0.1:6379 &lt;node-to-remove&gt;</span></span></code></pre></div><p>關鍵：</p>
<ul>
<li><code>--cluster-threshold 5</code>：load 差異超過 5% 才 rebalance、避免反覆觸發</li>
<li><code>--cluster-slots</code>：一次 migrate 多少 slot；太大 lock 久、太小步驟多</li>
<li>Rebalance / reshard 過程 cluster 仍 serve traffic、但 <em>latency 升高</em>（migration overhead）</li>
</ul>
<h2 id="5-段執行流程">5 段執行流程</h2>





<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. Pre-resharding analysis
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   - 當前 slot 分佈跟 load
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Hot key 識別（CLUSTER COUNTKEYSINSLOT）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 預估 migration 時間
</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">2. Backup checkpoint
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - BGSAVE on all master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - 確認 replica 跟得上（replication offset diff &lt; 10MB）
</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">3. Execute re-sharding
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 用 redis-cli --cluster 工具
</span></span><span class="line"><span class="ln">12</span><span class="cl">   - Monitor cluster health（CLUSTER INFO + CLUSTER NODES）
</span></span><span class="line"><span class="ln">13</span><span class="cl">   - Migration 期間 application 端 latency baseline 比對
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">4. Verify
</span></span><span class="line"><span class="ln">16</span><span class="cl">   - Slot distribution 對 expected mapping
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - Application traffic pattern 對 baseline
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - 跑 cross-node sanity check
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">5. Cleanup
</span></span><span class="line"><span class="ln">21</span><span class="cl">   - 舊 node（若 decommission）reset / 釋放
</span></span><span class="line"><span class="ln">22</span><span class="cl">   - Monitoring dashboard 更新 (Prometheus target / Grafana panel)
</span></span><span class="line"><span class="ln">23</span><span class="cl">   - Document new topology</span></span></code></pre></div><p>整體 1-7 天、依 cluster 大小（10GB ~ 1 小時、TB 級 1-3 天）。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cluster-busy-期間-application-timeout">Case 1：Cluster busy 期間 application timeout</h3>
<p><strong>徵兆</strong>：re-sharding 跑到一半、application 端開始大量 <code>CLUSTER BUSY</code> error / <code>OOM</code> warning / latency p99 從 5ms 跳到 200-2000ms；某些 batch operation 完全失敗。</p>
<p><strong>根因</strong>：MIGRATE command 對單 key 是 <em>blocking</em>（DUMP + send + RESTORE + DEL atomic）— 大 value（HASH / SORTED SET / LIST 含 100K+ entry）migration 可能 lock node 數秒；同期間其他 query 阻塞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：<code>MEMORY USAGE</code> 跑 sample key、找 &gt; 1MB 的 <em>fat key</em>、列出單獨處理</li>
<li><strong>MIGRATE timeout 調</strong>：<code>redis.conf</code> 設 <code>cluster-migration-timeout 10000</code>（10s）、避免單 key migration 卡爆 cluster</li>
<li><strong>降低並行</strong>：<code>--cluster-pipeline 1</code> 一次只搬一個 slot（預設 10）、減少 CPU 壓力</li>
<li><strong>Fat key refactor</strong>：production 不該有 1M+ entry 的 collection、refactor 拆分</li>
</ol>
<h3 id="case-2replica-lag-during-re-sharding">Case 2：Replica lag during re-sharding</h3>
<p><strong>徵兆</strong>：reshard 完成後、replica 顯示 stale data 數分鐘、application 端 read from replica 拿到舊值。</p>
<p><strong>根因</strong>：master 端 slot migration 產生大量 <code>DEL</code> + <code>RESTORE</code> 命令、replication stream 量爆、replica 跟不上、accumulated lag。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding 確認 replica lag &lt; 5MB</strong>、否則先 fix replica issue 再開始</li>
<li><strong>Throttle migration</strong>：用 <code>--cluster-replace</code> + lower pipeline、放慢 master 寫入速度</li>
<li><strong>Application 端 read-write split policy</strong>：reshard 期間強制 read from master、暫時放棄 replica read</li>
<li><strong>預備計畫</strong>：若 lag &gt; 30s 撐了 5+ 分鐘、考慮暫停 reshard、wait replica catch up</li>
</ol>
<h3 id="case-3client-side-topology-cache-stale">Case 3：Client-side topology cache stale</h3>
<p><strong>徵兆</strong>：reshard 完、application 端持續報 <code>MOVED &lt;slot&gt; &lt;new-node&gt;</code> redirect、但隔 30s 又 redirect 一次；某些 client 直接 connection refused（連到已 decommission node）。</p>
<p><strong>根因</strong>：cluster-aware client（lettuce / Jedis cluster mode）有 <em>topology cache</em>、reshard 後不主動 refresh；遇 MOVED 後 refresh 一次、但 cache TTL 內可能繼續用舊 mapping。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Client config</strong>：lettuce <code>clusterTopologyRefreshOptions(...)</code> 設較短 refresh interval（60s）+ <code>enablePeriodicRefresh()</code></li>
<li><strong>Reshard 完後 trigger refresh</strong>：application 端可主動發 <code>CLUSTER NODES</code> 拿最新 topology、不依賴 client lib 自動 refresh</li>
<li><strong>Graceful client shutdown / restart</strong>：對 latency-sensitive 服務、reshard 完 rolling restart application pod、避免 stale cache</li>
<li><strong>Decommissioned node 保留 5 分鐘</strong>：不立刻 stop node、給 stale client 自然 retry 機會</li>
</ol>
<h3 id="case-4cross-slot-transaction-失敗">Case 4：Cross-slot transaction 失敗</h3>
<p><strong>徵兆</strong>：application 用 <code>MULTI/EXEC</code> 跨多 key、reshard 期間部分 transaction 報 <code>MOVED</code> error、整個 transaction 失敗、business logic 不一致。</p>
<p><strong>根因</strong>：Redis Cluster transaction 要求 <em>所有 key 在同 slot</em>（用 hash tag <code>{user:123}</code>）；reshard 期間如果 transaction 內某 key migrate 到 dest、cluster topology 暫時 inconsistent、transaction 拒絕。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：grep application code 找 MULTI / pipeline 使用、確認所有都用 hash tag co-locate</li>
<li><strong>Reshard 期間 application 端加 retry</strong>：transaction failure 後 backoff retry、cluster stabilize 後成功</li>
<li><strong>架構</strong>：transaction-heavy 場景考慮不用 Redis Cluster、用 Redis Sentinel single master（無 slot 概念）</li>
</ol>
<h3 id="case-5monitor-visibility-gap-during-reshard">Case 5：Monitor visibility gap during reshard</h3>
<p><strong>徵兆</strong>：reshard 期間 Prometheus dashboard 對某 node 的 metric 突然顯示 <em>錯位</em> — load = 95% 但 slot count 顯示 6% slot；SOC 不知道 node 健康狀況。</p>
<p><strong>根因</strong>：Prometheus exporter 對 <em>slot count</em> 跟 <em>traffic load</em> 分開計算；reshard 期間 slot count 已 migrate 但流量仍打 source node（client cache stale）— metric 看似矛盾。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Reshard 期間關 alert</strong>：knownmaintenance window、Prometheus silence alert</li>
<li><strong>加 reshard-aware metric</strong>：用 <code>redis_cluster_migration_slots</code> 量化 in-flight migration</li>
<li><strong>Dashboard 加註解</strong>：reshard 期間 SOC 看 dashboard 知道是 <em>normal anomaly</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot migration 速度</td>
          <td>1-10K key / sec（依 key size + network）</td>
          <td>TB 級 10K key / sec → 1 天</td>
      </tr>
      <tr>
          <td>Application latency impact</td>
          <td>p99 +50-200% during migration</td>
          <td>設 latency budget、超出暫停</td>
      </tr>
      <tr>
          <td>Memory / node</td>
          <td>不變、但 temporary 雙寫期間 +5-15%</td>
          <td>不能在 memory 90%+ 時 reshard</td>
      </tr>
      <tr>
          <td>Network bandwidth</td>
          <td>跨 node 大流量、~100-500 Mbps per migration stream</td>
          <td>跨 AZ reshard egress cost 注意</td>
      </tr>
      <tr>
          <td>Recovery time</td>
          <td>Reshard 失敗回退 = 反向 reshard（時間相同）</td>
          <td>不能在 incident 期間 reshard</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>跑在 <em>低流量時段</em>（夜間 / 週末）</li>
<li>Throughput 容忍度 &lt; 50% 再 reshard、不要 80%+ 時操作</li>
<li>預留 <em>回退 window</em> — reshard 卡住時能 abort + 恢復原狀</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-migration-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB migration</a> 對位</h3>
<p>DragonflyDB 設計上 <em>單機效能取代 cluster</em>、re-sharding 議題消失；如果 cluster re-sharding 頻繁觸發、評估直接遷 DragonflyDB 是否更便宜。</p>
<h3 id="跟-sentinel-ha-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Sentinel HA</a> 對比</h3>
<p>Sentinel 模式無 slot 概念、re-sharding 不適用；但 <em>manual sharding by application</em> 場景仍可能需要類似 topology re-layout、application 端要自己處理。</p>
<h3 id="跟-redis-7-function--cluster-v2">跟 Redis 7+ Function / Cluster v2</h3>
<p>Redis 7 推 Cluster v2 跟 Functions、slot migration 機制部分升級；keyspace migration 仍是核心議題、但 API 跟 monitoring 改進。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Auto-rebalance via operator</strong>：Redis Enterprise / Aiven 等 managed Redis 提供自動 rebalance、不需手動觸發</li>
<li><strong>Cross-DC slot migration</strong>：跨 region cluster slot migration 對 latency / cost 影響大、通常用 <em>application-level sharding</em> 取代 cluster-level</li>
<li><strong>Hash tag 治理</strong>：application code grep / lint 強制 hash tag、避免 cross-slot transaction 反模式</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>對位 deep article：<a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major version upgrade</a>（另一個 5 type 漏類驗證）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a> / <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 <em>容量重劃漏類</em>）</li>
</ul>
]]></content:encoded></item><item><title>3.C13 Shopify：Debezium CDC over sharded MySQL</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/</guid><description>&lt;p>Shopify 的 CDC pipeline 揭露了 sharded monolith 上大規模 log-based CDC 的真實工程壓力。壓力集中在 snapshot 跟 oversized payload，穩態複製本身反而是最穩定的部分。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Shopify 的核心資料儲存是 100+ 個 MySQL shard，每個 shard 承載不同商家的交易資料。下游系統（搜尋索引、analytics、資料倉儲）需要近即時地取得資料變更。原本用 query-based 方案（內部系統 Longboat）輪詢資料庫，但隨 shard 數量跟資料量成長，輪詢的延遲跟資料庫負載壓力持續惡化。&lt;/p>
&lt;p>遷移到 log-based CDC（Debezium over Kafka Connect）後，pipeline 的穩態規模是 ~150 個 Debezium connector 跑在 12 個 Kubernetes pod、Black Friday peak 100K records/sec、P99 latency &amp;lt; 10s。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="snapshot-鎖定-read-replica">Snapshot 鎖定 read replica&lt;/h3>
&lt;p>Debezium 在初始同步（snapshot）時需要取得一致性快照。MySQL connector 的預設行為是對 read replica 取 global read lock，鎖住的時間跟表大小成正比。Shopify 的大表 snapshot 可能鎖住 read replica 數小時，影響線上查詢。&lt;/p>
&lt;p>Shopify 工程師直接向 Debezium 上游貢獻了「lock-free snapshot」機制 — 用 MySQL 的 GTID（Global Transaction ID）確保一致性，取代 global read lock。這個改動後來合併進 Debezium 主線，所有使用者都受益。&lt;/p>
&lt;h3 id="oversized-record">Oversized record&lt;/h3>
&lt;p>MySQL 的 blob / text 欄位可能產生超過 1 MB 的 CDC record。Kafka 的 message size limit（預設 1 MB）會讓這些 record 被 producer 拒絕。調大 &lt;code>max.message.bytes&lt;/code> 是一個選項，但會影響 broker 的記憶體跟 replication 效率。&lt;/p>
&lt;p>Shopify 的解法是把 oversized payload 寫到 GCS（Google Cloud Storage），CDC record 只帶 GCS pointer。Consumer 端在需要完整資料時再從 GCS 取。這個 pattern 把 Kafka 維持在「傳遞事件 metadata」的定位，大型 payload 走 object storage。&lt;/p>
&lt;h3 id="connector-故障隔離">Connector 故障隔離&lt;/h3>
&lt;p>150 個 connector 跑在 12 個 pod 上，一個 connector 的 failure（例如某個 shard 的 MySQL 做了 schema change、binlog 格式不相容）可能影響同 pod 上的其他 connector。Shopify 用 Kafka Connect 的 distributed mode + task rebalance 做故障隔離，但 rebalance 本身在 connector 數量多時有延遲。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Snapshot 鎖定&lt;/td>
 &lt;td>Lock-free snapshot（GTID）&lt;/td>
 &lt;td>需要 MySQL 啟用 GTID、upstream contribution 維護成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Oversized record&lt;/td>
 &lt;td>GCS pointer 替代 inline data&lt;/td>
 &lt;td>Consumer 端要多一步 GCS 讀取、增加端到端延遲&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connector 隔離&lt;/td>
 &lt;td>Distributed mode + rebalance&lt;/td>
 &lt;td>Rebalance storm 在大量 connector 時可能造成全域暫停&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高峰流量&lt;/td>
 &lt;td>12 pod K8s 部署、水平擴展&lt;/td>
 &lt;td>Pod 數量增加讓 Kafka Connect worker 的 rebalance 更複雜&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="回寫教材的連結">回寫教材的連結&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern&lt;/a>：CDC 是 outbox pattern 的 log-based 替代方案。Shopify 的 case 揭露 CDC 的工程成本集中在 snapshot 跟 schema evolution，outbox 的成本集中在應用層 dual-write。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a>：Kafka Connect / CDC 的進階主題。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a>：message size limit 跟 broker 資源的關係。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>讀者在自己的系統看到以下訊號時，應該回讀本案例：&lt;/p></description><content:encoded><![CDATA[<p>Shopify 的 CDC pipeline 揭露了 sharded monolith 上大規模 log-based CDC 的真實工程壓力。壓力集中在 snapshot 跟 oversized payload，穩態複製本身反而是最穩定的部分。</p>
<h2 id="業務背景">業務背景</h2>
<p>Shopify 的核心資料儲存是 100+ 個 MySQL shard，每個 shard 承載不同商家的交易資料。下游系統（搜尋索引、analytics、資料倉儲）需要近即時地取得資料變更。原本用 query-based 方案（內部系統 Longboat）輪詢資料庫，但隨 shard 數量跟資料量成長，輪詢的延遲跟資料庫負載壓力持續惡化。</p>
<p>遷移到 log-based CDC（Debezium over Kafka Connect）後，pipeline 的穩態規模是 ~150 個 Debezium connector 跑在 12 個 Kubernetes pod、Black Friday peak 100K records/sec、P99 latency &lt; 10s。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="snapshot-鎖定-read-replica">Snapshot 鎖定 read replica</h3>
<p>Debezium 在初始同步（snapshot）時需要取得一致性快照。MySQL connector 的預設行為是對 read replica 取 global read lock，鎖住的時間跟表大小成正比。Shopify 的大表 snapshot 可能鎖住 read replica 數小時，影響線上查詢。</p>
<p>Shopify 工程師直接向 Debezium 上游貢獻了「lock-free snapshot」機制 — 用 MySQL 的 GTID（Global Transaction ID）確保一致性，取代 global read lock。這個改動後來合併進 Debezium 主線，所有使用者都受益。</p>
<h3 id="oversized-record">Oversized record</h3>
<p>MySQL 的 blob / text 欄位可能產生超過 1 MB 的 CDC record。Kafka 的 message size limit（預設 1 MB）會讓這些 record 被 producer 拒絕。調大 <code>max.message.bytes</code> 是一個選項，但會影響 broker 的記憶體跟 replication 效率。</p>
<p>Shopify 的解法是把 oversized payload 寫到 GCS（Google Cloud Storage），CDC record 只帶 GCS pointer。Consumer 端在需要完整資料時再從 GCS 取。這個 pattern 把 Kafka 維持在「傳遞事件 metadata」的定位，大型 payload 走 object storage。</p>
<h3 id="connector-故障隔離">Connector 故障隔離</h3>
<p>150 個 connector 跑在 12 個 pod 上，一個 connector 的 failure（例如某個 shard 的 MySQL 做了 schema change、binlog 格式不相容）可能影響同 pod 上的其他 connector。Shopify 用 Kafka Connect 的 distributed mode + task rebalance 做故障隔離，但 rebalance 本身在 connector 數量多時有延遲。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<table>
  <thead>
      <tr>
          <th>挑戰</th>
          <th>解法</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Snapshot 鎖定</td>
          <td>Lock-free snapshot（GTID）</td>
          <td>需要 MySQL 啟用 GTID、upstream contribution 維護成本</td>
      </tr>
      <tr>
          <td>Oversized record</td>
          <td>GCS pointer 替代 inline data</td>
          <td>Consumer 端要多一步 GCS 讀取、增加端到端延遲</td>
      </tr>
      <tr>
          <td>Connector 隔離</td>
          <td>Distributed mode + rebalance</td>
          <td>Rebalance storm 在大量 connector 時可能造成全域暫停</td>
      </tr>
      <tr>
          <td>高峰流量</td>
          <td>12 pod K8s 部署、水平擴展</td>
          <td>Pod 數量增加讓 Kafka Connect worker 的 rebalance 更複雜</td>
      </tr>
  </tbody>
</table>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>：CDC 是 outbox pattern 的 log-based 替代方案。Shopify 的 case 揭露 CDC 的工程成本集中在 snapshot 跟 schema evolution，outbox 的成本集中在應用層 dual-write。</li>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>：Kafka Connect / CDC 的進階主題。</li>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：message size limit 跟 broker 資源的關係。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>CDC snapshot 過程持續數小時、鎖住 read replica 影響線上查詢</li>
<li>CDC record size 頻繁超過 Kafka 的 message size limit</li>
<li>Kafka Connect connector 數量超過 50 個、rebalance 時間開始明顯增長</li>
<li>從 query-based 同步（輪詢）切換到 log-based CDC 的評估階段</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/capturing-every-change-shopify-sharded-monolith">Capturing Every Change From Shopify&rsquo;s Sharded Monolith</a></li>
</ul>
]]></content:encoded></item><item><title>HashiCorp Boundary</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/boundary/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/boundary/</guid><description>&lt;p>HashiCorp Boundary 是 &lt;em>identity-based access broker&lt;/em>、把「使用者要連到某個內部資源」這件事拆成 &lt;em>identity 驗證&lt;/em> + &lt;em>target 授權&lt;/em> + &lt;em>動態 credential 注入&lt;/em> 三段、由 Boundary 統一仲介。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault&lt;/a> 同生態、設計上預期兩者組合：&lt;em>Boundary 控制誰能連到哪個資源、Vault 提供連線當下的 short-lived credential&lt;/em>。單獨用 Boundary 而不接 Vault、會失去它最大的價值。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Boundary 的核心定位是 &lt;em>連線層級的存取仲介&lt;/em>、不是傳統的 bastion host、也不是 identity-aware proxy。它把 &lt;em>連線發起權&lt;/em> 收回控制面、user 不需要直接拿到 SSH key / DB password / cloud token、只需要對 Boundary 認證、由 Boundary 把 &lt;em>target 資源的網路位置&lt;/em> + &lt;em>Vault 動態簽發的 credential&lt;/em> 在 session 開始時注入連線。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a> 比、Boundary 走 &lt;em>network broker + dynamic credential injection&lt;/em>、Teleport 走 &lt;em>identity-aware proxy + session recording&lt;/em>。Teleport 是 &lt;em>看見每一個指令、可重播&lt;/em> 的 PAM；Boundary 是 &lt;em>不存 credential、不錄影、靠 Vault short-lived token 來控制 blast radius&lt;/em>。兩者解的是同一類問題（內部資源存取治理）、但工程取捨完全不同 — Boundary 把「攻擊者拿到 credential 也只有 minutes-level 有效期」當主要防線、Teleport 把「全部 session 留下不可否認證據」當主要防線。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &amp;#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH&lt;/a> 比、Tailscale 走 mesh network + SSH-only、無 credential 仲介、無 dynamic injection；Boundary 走 broker 模式、支援 SSH / RDP / DB / TCP / HTTP 等多協議、且 credential 從 Vault 拉。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &amp;#43; Device Posture &amp;#43; IdP integration">Cloudflare Access&lt;/a> 比、Cloudflare 走 &lt;em>Zero Trust portal + identity-aware reverse proxy&lt;/em>、是 HTTP-first；Boundary 是 &lt;em>protocol-agnostic broker&lt;/em>、原生支援非 HTTP 協議（DB / SSH / RDP）。&lt;/p></description><content:encoded><![CDATA[<p>HashiCorp Boundary 是 <em>identity-based access broker</em>、把「使用者要連到某個內部資源」這件事拆成 <em>identity 驗證</em> + <em>target 授權</em> + <em>動態 credential 注入</em> 三段、由 Boundary 統一仲介。它跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> 同生態、設計上預期兩者組合：<em>Boundary 控制誰能連到哪個資源、Vault 提供連線當下的 short-lived credential</em>。單獨用 Boundary 而不接 Vault、會失去它最大的價值。</p>
<h2 id="服務定位">服務定位</h2>
<p>Boundary 的核心定位是 <em>連線層級的存取仲介</em>、不是傳統的 bastion host、也不是 identity-aware proxy。它把 <em>連線發起權</em> 收回控制面、user 不需要直接拿到 SSH key / DB password / cloud token、只需要對 Boundary 認證、由 Boundary 把 <em>target 資源的網路位置</em> + <em>Vault 動態簽發的 credential</em> 在 session 開始時注入連線。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> 比、Boundary 走 <em>network broker + dynamic credential injection</em>、Teleport 走 <em>identity-aware proxy + session recording</em>。Teleport 是 <em>看見每一個指令、可重播</em> 的 PAM；Boundary 是 <em>不存 credential、不錄影、靠 Vault short-lived token 來控制 blast radius</em>。兩者解的是同一類問題（內部資源存取治理）、但工程取捨完全不同 — Boundary 把「攻擊者拿到 credential 也只有 minutes-level 有效期」當主要防線、Teleport 把「全部 session 留下不可否認證據」當主要防線。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a> 比、Tailscale 走 mesh network + SSH-only、無 credential 仲介、無 dynamic injection；Boundary 走 broker 模式、支援 SSH / RDP / DB / TCP / HTTP 等多協議、且 credential 從 Vault 拉。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a> 比、Cloudflare 走 <em>Zero Trust portal + identity-aware reverse proxy</em>、是 HTTP-first；Boundary 是 <em>protocol-agnostic broker</em>、原生支援非 HTTP 協議（DB / SSH / RDP）。</p>
<p>關鍵張力：<em>Boundary + Vault 組合的工程複雜度</em> ↔ <em>不靠 session recording 的審計可信度</em>。已用 HashiCorp 生態（Terraform + Vault + Consul）的組織、Boundary 是 <em>最後一塊拼圖</em>；沒用 Vault 的組織用 Boundary 等於只剩一個 bastion 的弱化版、不如直接走 Teleport。合規強要求 keystroke audit 的場域、Boundary 預設不錄 session、要走 Enterprise add-on 才有、不如 Teleport first-class。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Boundary 在 PAM stack 中承擔哪一段（broker / target / session）、哪些要外接（Vault 給 credential、IdP 給 auth、Enterprise add-on 給 session recording）</li>
<li>Controller + Worker + Multi-hop 拓樸怎麼對應實際網路分段（DMZ / internal / restricted subnet）</li>
<li>Vault Credential Library 怎麼設計、誰負責 host catalog、role / scope 怎麼劃</li>
<li>何時用 Boundary、何時改走 Teleport / Tailscale SSH / Cloudflare Access 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Boundary deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>是否真的接 Vault</strong>：Credential Library 是否從 Vault 拉 dynamic credential（DB / SSH cert / cloud token）、session 結束是否自動 revoke、還是仍有 static credential 存在 Boundary 或人手裡</li>
<li><strong>Scope 結構是否反映組織邊界</strong>：Global → Org → Project 的三層 scope、Org 對應 BU / tenant、Project 對應應用或環境；role / grant 是否按 Project 切、還是全部塞 Global scope 變共享密碼</li>
<li><strong>Worker 拓樸是否反映網路分段</strong>：Controller 在 control plane、Worker 在每個網路 segment（DMZ / internal / restricted DB subnet）、Multi-hop 是否走 segment-aware routing、還是把所有 worker 塞同一個 VPC</li>
<li><strong>Auth Method 是不是 IdP-backed</strong>：OIDC（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD / Google）/ LDAP / Password — production 應該走 OIDC、Password auth method 只該存在於 break-glass</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">Privileged Access and Just-in-Time Authority</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Controller + Worker 拓樸</strong>：Controller 負責 control plane（auth、policy、session 管理、API endpoint）、Worker 負責 data plane（實際代理連線到 target）。Controller 通常 cluster 部署（3 個以上、HA）、Worker 按網路 segment 分散部署。Controller 從不直接連 target — user 跟 Controller 認證、Controller 告訴 user 走哪個 Worker、Worker 才實際代理連線。</p>
<p><strong>Target + Host Set + Host Catalog</strong>：Target 是 user 看到的「可連對象」抽象（例如 <code>prod-db-cluster</code>）、Host Set 是 Target 對應的實際 host 集合、Host Catalog 是 host 的來源（static list 或從 cloud auto-discover）。Dynamic Host Catalog 可以從 AWS / Azure / GCP 用 tag 自動 enroll host、不需要手動維護 host list — 例如 <code>tag:role=prod-db</code> 的 EC2 自動進 <code>prod-db-cluster</code> Target。</p>
<p><strong>Credential Library（Vault 整合）</strong>：Boundary 不存 credential、靠 Credential Library 從 Vault 拉。設計支援三種：<em>Vault Generic</em>（拉任意 Vault secret path）、<em>Vault SSH Certificate</em>（拉 Vault SSH CA 簽發的 short-lived cert）、<em>Vault Database</em>（拉 Vault Database Secret Engine 簽發的 DB user / password）。session 開始時 Boundary 拉 credential、注入連線、session 結束時 Vault 自動 revoke。這是 Boundary 的核心價值 — 沒接 Vault 等於丟掉 dynamic credential rotation 這個最大賣點。</p>
<p><strong>Auth Method</strong>：支援 OIDC（OAuth2 / OpenID Connect、給 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD / Google）、LDAP（給 internal directory）、Password（給 break-glass）。Production 預設走 OIDC、跟 IdP 同源、user lifecycle 隨 IdP 變動（離職 IdP 鎖、Boundary 自動失效）。Password auth method 只該存在於 break-glass account、密碼進 Vault、單獨 audit。</p>
<p><strong>Role + Grant + Scope</strong>：Boundary 的權限模型是 <em>scope-bound role</em>、role 屬於某個 scope（Global / Org / Project）、grant 是 role 內的具體權限（例如 <code>target=&lt;id&gt;;actions=authorize-session</code>）。Scope 三層分別對應：<em>Global</em> — platform-level admin、<em>Org</em> — 某 BU 或 tenant、<em>Project</em> — 應用或環境（prod / staging / dev）。設計時把 role 按 Project 切、不要全部塞 Global scope 變共享密碼。</p>
<p><strong>Session 生命週期</strong>：user 對 Boundary 認證（OIDC）→ list authorized target → 對某 target 發起 <code>authorize-session</code>、Boundary 從 Credential Library 拉 credential → user 透過 Boundary CLI / Desktop / SDK 連線、實際走 Worker 代理 → session 有 <em>max duration</em>（預設 8 小時、可調短）、過期自動斷 + Vault credential revoke。session metadata（誰、何時、target、worker、duration）一律 audit log。</p>
<p><strong>Multi-hop Worker</strong>：跨網路 segment（例如 user 在 corp 網、target 在 DMZ → internal → restricted DB subnet）時、Boundary 支援 worker chain — corp Worker 連到 DMZ Worker、DMZ Worker 連到 internal Worker、internal Worker 連到 DB。每段 worker 只看得到下一段、不需要 VPN trunk 把整個網路打通。這是 Boundary 相對 Teleport / Tailscale 的網路工程優勢、特別適合金融 / 政府 / 製造業的多層網路分段。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>HashiCorp Boundary</th>
          <th>Teleport</th>
          <th>Tailscale SSH</th>
          <th>Cloudflare Access</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心模式</td>
          <td>Network broker + dynamic credential injection</td>
          <td>Identity-aware proxy + session recording</td>
          <td>Mesh VPN + SSH CA</td>
          <td>Zero Trust portal + identity-aware proxy</td>
      </tr>
      <tr>
          <td>Credential 處理</td>
          <td>從 Vault 拉 short-lived、不存</td>
          <td>Teleport CA 簽發 short-lived cert</td>
          <td>Tailscale SSH CA 簽發</td>
          <td>OAuth token、無 SSH credential 處理</td>
      </tr>
      <tr>
          <td>Session recording</td>
          <td>Enterprise add-on（2023+、非 first-class）</td>
          <td>First-class（SSH / kubectl / DB 都錄）</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>協議支援</td>
          <td>SSH / RDP / DB（Postgres / MySQL）/ TCP / HTTP</td>
          <td>SSH / kubectl / DB / RDP / Web Apps</td>
          <td>SSH only（mesh 內任意 TCP）</td>
          <td>HTTP / SSH（透過 cloudflared）/ RDP</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted (OSS / Enterprise) / HCP (HashiCorp)</td>
          <td>Self-hosted / Teleport Cloud</td>
          <td>SaaS only</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>網路拓樸</td>
          <td>Controller + Worker、Multi-hop 跨 segment 友善</td>
          <td>Proxy + Agent、單層 proxy</td>
          <td>Mesh、所有節點對等</td>
          <td>Cloudflare edge + cloudflared tunnel</td>
      </tr>
      <tr>
          <td>IdP 整合</td>
          <td>OIDC / LDAP / Password</td>
          <td>OIDC / SAML / GitHub</td>
          <td>OIDC（Okta / Google / Azure）</td>
          <td>OIDC / SAML / 內建 IdP</td>
      </tr>
      <tr>
          <td>跟其他 vendor 鎖</td>
          <td>預設假設用 Vault、單獨用價值有限</td>
          <td>獨立完整、不依賴特定 secret store</td>
          <td>獨立、Tailscale 生態</td>
          <td>獨立、Cloudflare 生態</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>已用 HashiCorp 生態 + 多協議 + 多層網路分段</td>
          <td>強合規 + session audit + kubectl-heavy</td>
          <td>小團隊 + SSH-only + 不要 PAM 複雜度</td>
          <td>Cloud-native + Zero Trust portal + HTTP-first</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — Vault 整合複雜、target / role / scope 量多</td>
          <td>中 — Teleport-specific config + recording</td>
          <td>低 — Tailscale 拆掉就回 plain SSH</td>
          <td>低 — Cloudflare 拆掉就回 origin</td>
      </tr>
  </tbody>
</table>
<p>選 Boundary 的核心訴求：<em>已用 HashiCorp 生態（特別是 Vault）</em> + <em>多協議內部資源（不只 SSH、還有 DB / RDP / TCP）</em> + <em>多層網路分段需要 Multi-hop</em>、可以接受 session recording 不是 first-class。沒用 Vault 的組織、Boundary 失去最大價值、應該直接走 Teleport。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Multi-hop Worker 跟網路分段</strong>：金融 / 政府常見三段網路（corp → DMZ → restricted）、傳統做法是打 VPN trunk 把整個網路扁平化、accept 大 blast radius。Boundary 用 worker chain 反向 — 每個 segment 部署一個 worker、worker 之間用 mTLS 認證、user 只進 corp worker、後面 hop 由 Boundary control plane 編排。每段 worker 不知道後一段的 target 細節、只知道下一段 worker 的位置。配對 <a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">Segmentation and Blast Radius Containment</a> 的章節原則。</p>
<p><strong>Dynamic Host Catalog</strong>：手動維護 host list 在 cloud-native 環境會壞 — auto-scaling group 起一台新 EC2、沒人去 Boundary 加 target。Dynamic Host Catalog 配 cloud provider plugin（AWS / Azure / GCP）、用 tag 自動 enroll：例如 <code>tag:env=prod tag:role=app</code> 的 EC2 自動進 <code>prod-app</code> Target、scale-down 也自動移除。這配 IaC（<a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a> 管 tag）是 HashiCorp 生態一致性的核心賣點。</p>
<p><strong>Session Recording（Enterprise 才有）</strong>：2023+ Boundary Enterprise 引入 session recording、支援 SSH 跟 RDP 的 keystroke + screen recording、output 加密存到 S3 / Azure Blob、metadata 走 audit。OSS Community Edition 沒有、只記 session metadata（who / when / what target / how long）。組織要 session recording 但又要 Boundary、要評估 Enterprise license cost vs Teleport license cost — 通常 Teleport 在 session recording 場景成本效益更好。</p>
<p><strong>Vault credential brokering 設計</strong>：Boundary 連 Vault 的設計支援多種 secret engine — Database（Postgres / MySQL / Redis 等、簽 short-lived DB user）、SSH Certificate（簽 short-lived SSH cert）、AWS / Azure / GCP（簽 cloud STS token）、KV v2（拉靜態 secret、不推薦）。Production 預設用 dynamic engine、不要用 KV v2 — 靜態 secret 失去 Boundary 最大價值。Vault namespace / policy 設計要對齊 Boundary scope、否則 cross-scope credential 暴露變大問題。</p>
<p><strong>HCP Boundary（HashiCorp Cloud Platform）</strong>：HashiCorp 託管的 SaaS 版、Controller 由 HashiCorp 管、user 只部署 Worker 到自己網路。優點是省去 Controller HA / upgrade 維運；缺點是 control plane 在 HashiCorp 雲、合規敏感場域要評估 data residency。SMB / 中型團隊適合走 HCP、大型 enterprise 通常 Self-hosted。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Session 拿到 credential 但 target 連不上</strong>：Worker 跟 target 之間網路不通、或 Worker 沒部署到 target 所在 segment — 檢查 Worker tag 跟 Target worker_filter、用 <code>boundary workers list</code> 確認 Worker 健康</li>
<li><strong>OIDC login 失敗</strong>：IdP redirect URI 沒對齊、或 IdP signing key 過期 — 對照 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558</a> 的啟示、Boundary OIDC auth method 依賴上游 IdP signing key、IdP 端 key rotation 不對 Boundary 通知會整批 session 認不過</li>
<li><strong>Vault credential 拉不到 / 過期太快</strong>：Boundary 服務帳戶在 Vault 的 policy 沒給 <code>creds/&lt;role&gt;</code> 權限、或 Vault 簽的 credential TTL 短於 session max duration — 對齊 TTL、加 Vault telemetry alert credential issuance 失敗</li>
<li><strong>Multi-hop 連線中斷</strong>：中間 hop 的 Worker 健康但 connection drop — 通常是中間 segment 的 firewall idle timeout 短於 session activity gap、調 firewall 或在 client 端開 keepalive</li>
<li><strong>Target 量爆炸 / role 管不動</strong>：所有 target 塞 Global scope、role 量線性漲 — 重構 scope 結構、按 Org / Project 切、role 從 Global 移到 Project 層</li>
<li><strong>Dynamic Host Catalog 漏 host</strong>：cloud tag 沒打 / IAM 沒給 Boundary 描述權限 — 檢查 cloud plugin 的 service account permission、加 catalog sync error 的 alert</li>
<li><strong>OSS Community 升 Enterprise 才發現缺 feature</strong>：選 OSS 之前沒確認需求 — session recording / SAML / 高級 RBAC / multi-region HA 都是 Enterprise 才有、評估時就要列清楚</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>強合規要 session recording / keystroke audit</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a></td>
      </tr>
      <tr>
          <td>小團隊 + SSH-only + 不要 PAM 複雜度</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a></td>
      </tr>
      <tr>
          <td>Cloud-native + Zero Trust portal</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></td>
      </tr>
      <tr>
          <td>Kubernetes kubectl-first PAM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a>（kubectl proxy first-class）</td>
      </tr>
      <tr>
          <td>Secret storage / rotation 核心</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Boundary 的搭檔）</td>
      </tr>
      <tr>
          <td>IdP / SSO 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD</td>
      </tr>
      <tr>
          <td>Cloud IAM role assumption</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / 對應雲</td>
      </tr>
      <tr>
          <td>事故路由</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Boundary CLI / Desktop / Terraform provider 的完整指令 reference</li>
<li>HCP Boundary 跟 Self-hosted 的功能對照細節（HashiCorp 官方有 matrix）</li>
<li>Vault 內部的 secret engine 設計（在 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> 頁）</li>
<li>OIDC / SAML 協議本身的攻擊面（<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">2.3 SSO 攻擊面</a>）</li>
<li>Network segmentation 的整體設計（<a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">Segmentation and Blast Radius Containment</a>）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Boundary 在 07 案例庫沒有直接 vendor-level 事件、但 PAM / credential rotation / IdP 相關 case 都是它的設計取捨對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Boundary 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Boundary + Vault Credential Library 直接解此 case 的 scope map 問題 — 每 session 拿 Vault 簽的 short-lived credential、session 結束自動 revoke、不需要 batch rotation、scope map 由 Vault policy + Boundary role 雙向約束</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 Identity Lateral Impact</a></td>
          <td>helpdesk 走 SE 拿到 reset 後的密碼、但 Boundary 仍要求 session 開始時拿 Vault dynamic credential、attacker 在 Vault policy 端被擋；前提是 Boundary OIDC auth method 不依賴可被 SE 重置的 password、IdP 要走 phishing-resistant MFA</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>Boundary OIDC auth method 依賴上游 IdP signing key、IdP 出事時 Boundary access 也要 rotate；對應啟示是 <em>broker 的 trust chain 取決於上游 IdP</em>、不要把 OIDC 當無責任接口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>static credential 在離職 / 外洩後仍可用是核心問題、Boundary + Vault Database Secret Engine 直接消除 static credential 存在、改成每 session 簽 short-lived DB user</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">Privileged Access and JIT Authority (section)</a></td>
          <td>Boundary 的 <em>authorize-session</em> 模型是 JIT authority 的具體實作、session 期限 + Vault TTL 雙重約束 blast radius</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">7.B Privileged Access and JIT Authority</a>、<a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">7.B Segmentation and Blast Radius Containment</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a>、<a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></li>
<li>搭檔：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Credential Library 核心）、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（OIDC IdP）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>（cloud target 的 STS token 來源）、<a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a>（target / scope / role 進 IaC）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Boundary audit log → SIEM → IR routing）、<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">2.3 SSO 攻擊面</a></li>
<li>官方：<a href="https://developer.hashicorp.com/boundary">Boundary Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C13 Disney+ Hotstar：IPL 板球決賽 1860 萬人同時直播</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/</guid><description>&lt;p>這個案例的核心責任是說明「全球大型直播」的容量設計 — 跟 Prime Day 同屬「可預期極端峰值」、但形狀完全不同：Prime Day 是分散全球的購物峰值、Hotstar IPL 是 &lt;em>單一時間點 + 高度集中地理區&lt;/em> 的直播峰值。容量規劃的挑戰在於 CDN、串流伺服器、live encoder、message queue 同時 saturate。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Hotstar IPL 直播的關鍵數字（引自 &lt;a href="https://aws.amazon.com/blogs/media/in-the-news-hotstar-sets-new-global-record-for-live-viewership/">Hotstar global record&lt;/a>）：&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>同時觀看峰值&lt;/td>
 &lt;td>1860 萬 人（2021-03 IPL 決賽）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全球記錄&lt;/td>
 &lt;td>該時點全球同時觀看直播的最高記錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>AWS Media Services + AWS CloudFront&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客戶基礎&lt;/td>
 &lt;td>印度為主、跨亞洲&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>AWS Media Services 在大型事件的歷史記錄：Olympics、Super Bowl、IPL Cricket（引自 &lt;a href="https://aws.amazon.com/developer/application-security-performance/articles/large-scale-video-streaming-events/">AWS large-scale streaming events&lt;/a>）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Hotstar 案例揭露三個全球直播容量重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>集中地理區 = CDN 壓力集中&lt;/strong>：Prime Day 的流量分散全球、單一地區 CDN 不會 saturate；IPL 主要觀眾在印度、所有印度 PoP 同一時間 saturate。CDN 容量規劃必須按地區獨立做、不能用「全球總容量」當保證。對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 的 cardinality 與地區訊號治理、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的「地理分片容量」。&lt;/li>
&lt;li>&lt;strong>直播跟 VoD 是不同容量問題&lt;/strong>：VoD 觀眾分散時間、CDN 可預先 cache；直播觀眾集中時間、每一個 manifest / segment 都是 live 拉取、cache hit 反而是危險（拉到舊的 segment）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache freshness boundary、跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列&lt;/a> 的 fan-out 設計。&lt;/li>
&lt;li>&lt;strong>多 bitrate 動態切換 = 真實容量是 bitrate 加權&lt;/strong>：1860 萬觀眾不是都看 1080p — 印度行動網路下大多看 720p 或 480p、bitrate 加權後的 total bandwidth 可能比想像低。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling&lt;/a> 的真實 workload shape。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「1860 萬同時觀看」是 &lt;em>峰值瞬間&lt;/em>、不是全程平均。決賽 4 小時、觀眾數呈鐘形曲線、峰值維持時間可能只有 10-30 分鐘（比賽關鍵時刻）。容量規劃要看峰值持續時間、不只看峰值高度。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>CDN 容量規劃按地理區分割&lt;/strong>：不要假設「全球 CDN 總量」夠用、要按主要觀眾分布的地區做容量保證。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a>。&lt;/li>
&lt;li>&lt;strong>直播必須 pre-scaling、不能依賴 reactive&lt;/strong>：直播開始之後 CDN reactive 擴容已經太晚、觀眾體驗已壞。事件型 scheduled scaling + over-provisioning 是必須。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a>。&lt;/li>
&lt;li>&lt;strong>multi-bitrate / ABR streaming 是容量緩衝&lt;/strong>：當網路擁塞、player 自動降 bitrate、總頻寬壓力下降。這層降級是隱性容量緩衝、要在壓測時驗證。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 saturation 行為。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP CDN + Media CDN、Azure Front Door + Media Services、Akamai / Cloudflare / Fastly 等 multi-CDN 都是對等候選。差異是 PoP 地理分布跟 manifest 處理能力。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「全球大型直播」的容量設計 — 跟 Prime Day 同屬「可預期極端峰值」、但形狀完全不同：Prime Day 是分散全球的購物峰值、Hotstar IPL 是 <em>單一時間點 + 高度集中地理區</em> 的直播峰值。容量規劃的挑戰在於 CDN、串流伺服器、live encoder、message queue 同時 saturate。</p>
<h2 id="觀察">觀察</h2>
<p>Hotstar IPL 直播的關鍵數字（引自 <a href="https://aws.amazon.com/blogs/media/in-the-news-hotstar-sets-new-global-record-for-live-viewership/">Hotstar global record</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同時觀看峰值</td>
          <td>1860 萬 人（2021-03 IPL 決賽）</td>
      </tr>
      <tr>
          <td>全球記錄</td>
          <td>該時點全球同時觀看直播的最高記錄</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>AWS Media Services + AWS CloudFront</td>
      </tr>
      <tr>
          <td>客戶基礎</td>
          <td>印度為主、跨亞洲</td>
      </tr>
  </tbody>
</table>
<p>AWS Media Services 在大型事件的歷史記錄：Olympics、Super Bowl、IPL Cricket（引自 <a href="https://aws.amazon.com/developer/application-security-performance/articles/large-scale-video-streaming-events/">AWS large-scale streaming events</a>）。</p>
<h2 id="判讀">判讀</h2>
<p>Hotstar 案例揭露三個全球直播容量重點。</p>
<ol>
<li><strong>集中地理區 = CDN 壓力集中</strong>：Prime Day 的流量分散全球、單一地區 CDN 不會 saturate；IPL 主要觀眾在印度、所有印度 PoP 同一時間 saturate。CDN 容量規劃必須按地區獨立做、不能用「全球總容量」當保證。對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 的 cardinality 與地區訊號治理、跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的「地理分片容量」。</li>
<li><strong>直播跟 VoD 是不同容量問題</strong>：VoD 觀眾分散時間、CDN 可預先 cache；直播觀眾集中時間、每一個 manifest / segment 都是 live 拉取、cache hit 反而是危險（拉到舊的 segment）。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache freshness boundary、跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a> 的 fan-out 設計。</li>
<li><strong>多 bitrate 動態切換 = 真實容量是 bitrate 加權</strong>：1860 萬觀眾不是都看 1080p — 印度行動網路下大多看 720p 或 480p、bitrate 加權後的 total bandwidth 可能比想像低。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a> 的真實 workload shape。</li>
</ol>
<p>需要警惕：「1860 萬同時觀看」是 <em>峰值瞬間</em>、不是全程平均。決賽 4 小時、觀眾數呈鐘形曲線、峰值維持時間可能只有 10-30 分鐘（比賽關鍵時刻）。容量規劃要看峰值持續時間、不只看峰值高度。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>CDN 容量規劃按地理區分割</strong>：不要假設「全球 CDN 總量」夠用、要按主要觀眾分布的地區做容量保證。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a>。</li>
<li><strong>直播必須 pre-scaling、不能依賴 reactive</strong>：直播開始之後 CDN reactive 擴容已經太晚、觀眾體驗已壞。事件型 scheduled scaling + over-provisioning 是必須。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a>。</li>
<li><strong>multi-bitrate / ABR streaming 是容量緩衝</strong>：當網路擁塞、player 自動降 bitrate、總頻寬壓力下降。這層降級是隱性容量緩衝、要在壓測時驗證。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 saturation 行為。</li>
</ol>
<p>跨平台等效：GCP CDN + Media CDN、Azure Front Door + Media Services、Akamai / Cloudflare / Fastly 等 multi-CDN 都是對等候選。差異是 PoP 地理分布跟 manifest 處理能力。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃全球直播 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想做 CDN 容量設計 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a></li>
<li>想理解 cache freshness 在直播的影響 → <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a></li>
<li>對照其他可預期峰值 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a>（分散全球的峰值）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/media/in-the-news-hotstar-sets-new-global-record-for-live-viewership/">In the news: Hotstar sets new global record for live viewership</a></li>
<li><a href="https://aws.amazon.com/developer/application-security-performance/articles/large-scale-video-streaming-events/">Large scale streaming events on AWS</a></li>
<li><a href="https://aws.amazon.com/media/direct-to-consumer-d2c-streaming/">Direct to Consumer &amp; Streaming on AWS</a></li>
</ul>
]]></content:encoded></item><item><title>0.13 操作控制 vertical slice 實作入口</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/</guid><description>&lt;p>操作控制 vertical slice 的核心責任是把「看得見、驗得過、接得住、回寫得動」落到同一個服務流程。這一章把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 與 action item closure 串成第一個可實作切片。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>實作目標：選一個核心 user journey，建立最小操作控制閉環&lt;/li>
&lt;li>輸入：服務入口、核心依賴、SLO / SLI、告警、驗證場景、事故流程&lt;/li>
&lt;li>產出：evidence package、verification evidence handoff、incident decision log、write-back item&lt;/li>
&lt;li>邊界：先做 artifact 與路由，工具與語言實作留給 04 / 06 / 08 與語言教材&lt;/li>
&lt;li>驗收：能從一次異常走完 triage、verification、decision、write-back&lt;/li>
&lt;/ul>
&lt;h2 id="實作目標">實作目標&lt;/h2>
&lt;p>Vertical slice 的目標是先做一條可回放的操作控制路徑。選一個核心 user journey，例如 checkout、message delivery、document publish、login 或 invoice generation，讓這條路徑同時具備觀測證據、驗證門檻、事故決策與回寫機制。&lt;/p>
&lt;p>這一輪的交付是 artifact 與流程責任。工具可以是現有 log search、dashboard、ticket、runbook repository 與 chat；重點是資料欄位與流程責任先成立，後續才判斷是否需要 Prometheus、OpenTelemetry backend、PagerDuty、incident.io 或 chaos tooling。&lt;/p>
&lt;h2 id="選擇服務切片">選擇服務切片&lt;/h2>
&lt;p>服務切片的選擇責任是找到最能暴露 04 / 06 / 08 交接問題的路徑。第一條 slice 應該具備使用者影響、依賴邊界、可量測訊號與可驗證失敗模式。&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>Checkout&lt;/td>
 &lt;td>直接連到收入與客戶痛點&lt;/td>
 &lt;td>payment timeout、inventory lag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Message delivery&lt;/td>
 &lt;td>同時包含同步入口與非同步處理&lt;/td>
 &lt;td>queue lag、redelivery loop&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Login&lt;/td>
 &lt;td>影響所有後續功能&lt;/td>
 &lt;td>identity provider outage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Document publish&lt;/td>
 &lt;td>涵蓋寫入、背景工作與通知&lt;/td>
 &lt;td>stale read、worker backlog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Invoice&lt;/td>
 &lt;td>牽涉正確性與客戶信任&lt;/td>
 &lt;td>duplicate charge、missing file&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Checkout 適合第一輪，因為它同時暴露 latency、dependency failure、customer impact 與 rollback decision。若團隊沒有交易路徑，可以選 message delivery 或 login；判準是這條路徑一旦失效，on-call 需要在 15 分鐘內做出明確決策。&lt;/p>
&lt;p>Message delivery 適合用來驗證 async observability。它能暴露 request id、correlation id、queue lag、DLQ、retry policy 與 replay runbook 的交接品質。&lt;/p>
&lt;p>Login 適合用來驗證外部依賴事故。它能暴露 identity provider、fallback、status page、security split 與 customer communication 的邊界。&lt;/p>
&lt;h2 id="artifact-契約">Artifact 契約&lt;/h2>
&lt;p>Artifact 契約的責任是讓每個環節都有可交接輸出。這些 artifact 可以先用 Markdown、ticket 欄位或 incident template 表達，等流程跑通後再導入工具自動化。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Artifact&lt;/th>
 &lt;th>最小欄位&lt;/th>
 &lt;th>來源章節&lt;/th>
 &lt;th>下游使用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Observability evidence package&lt;/td>
 &lt;td>source、time range、query link、owner、data quality、confidence、known gap&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>&lt;/td>
 &lt;td>triage、release gate、PIR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verification evidence handoff&lt;/td>
 &lt;td>hypothesis、scope、steady state、workload / fault、result、decision、owner&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;td>release gate、runbook、drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident decision log&lt;/td>
 &lt;td>timestamp、decision、context、evidence、owner、expected effect、rollback condition&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>&lt;/td>
 &lt;td>handoff、stakeholder update、PIR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident evidence write-back&lt;/td>
 &lt;td>finding、evidence、target artifact、owner、closure signal、review date&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>&lt;/td>
 &lt;td>dashboard、experiment、runbook&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Observability evidence package 是第一個 artifact。它保存查詢、時間窗、資料品質與 owner，讓後面的驗證與事故流程使用同一組事實。&lt;/p></description><content:encoded><![CDATA[<p>操作控制 vertical slice 的核心責任是把「看得見、驗得過、接得住、回寫得動」落到同一個服務流程。這一章把 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>、<a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 與 action item closure 串成第一個可實作切片。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>實作目標：選一個核心 user journey，建立最小操作控制閉環</li>
<li>輸入：服務入口、核心依賴、SLO / SLI、告警、驗證場景、事故流程</li>
<li>產出：evidence package、verification evidence handoff、incident decision log、write-back item</li>
<li>邊界：先做 artifact 與路由，工具與語言實作留給 04 / 06 / 08 與語言教材</li>
<li>驗收：能從一次異常走完 triage、verification、decision、write-back</li>
</ul>
<h2 id="實作目標">實作目標</h2>
<p>Vertical slice 的目標是先做一條可回放的操作控制路徑。選一個核心 user journey，例如 checkout、message delivery、document publish、login 或 invoice generation，讓這條路徑同時具備觀測證據、驗證門檻、事故決策與回寫機制。</p>
<p>這一輪的交付是 artifact 與流程責任。工具可以是現有 log search、dashboard、ticket、runbook repository 與 chat；重點是資料欄位與流程責任先成立，後續才判斷是否需要 Prometheus、OpenTelemetry backend、PagerDuty、incident.io 或 chaos tooling。</p>
<h2 id="選擇服務切片">選擇服務切片</h2>
<p>服務切片的選擇責任是找到最能暴露 04 / 06 / 08 交接問題的路徑。第一條 slice 應該具備使用者影響、依賴邊界、可量測訊號與可驗證失敗模式。</p>
<table>
  <thead>
      <tr>
          <th>候選切片</th>
          <th>適合原因</th>
          <th>常見失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Checkout</td>
          <td>直接連到收入與客戶痛點</td>
          <td>payment timeout、inventory lag</td>
      </tr>
      <tr>
          <td>Message delivery</td>
          <td>同時包含同步入口與非同步處理</td>
          <td>queue lag、redelivery loop</td>
      </tr>
      <tr>
          <td>Login</td>
          <td>影響所有後續功能</td>
          <td>identity provider outage</td>
      </tr>
      <tr>
          <td>Document publish</td>
          <td>涵蓋寫入、背景工作與通知</td>
          <td>stale read、worker backlog</td>
      </tr>
      <tr>
          <td>Invoice</td>
          <td>牽涉正確性與客戶信任</td>
          <td>duplicate charge、missing file</td>
      </tr>
  </tbody>
</table>
<p>Checkout 適合第一輪，因為它同時暴露 latency、dependency failure、customer impact 與 rollback decision。若團隊沒有交易路徑，可以選 message delivery 或 login；判準是這條路徑一旦失效，on-call 需要在 15 分鐘內做出明確決策。</p>
<p>Message delivery 適合用來驗證 async observability。它能暴露 request id、correlation id、queue lag、DLQ、retry policy 與 replay runbook 的交接品質。</p>
<p>Login 適合用來驗證外部依賴事故。它能暴露 identity provider、fallback、status page、security split 與 customer communication 的邊界。</p>
<h2 id="artifact-契約">Artifact 契約</h2>
<p>Artifact 契約的責任是讓每個環節都有可交接輸出。這些 artifact 可以先用 Markdown、ticket 欄位或 incident template 表達，等流程跑通後再導入工具自動化。</p>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>最小欄位</th>
          <th>來源章節</th>
          <th>下游使用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability evidence package</td>
          <td>source、time range、query link、owner、data quality、confidence、known gap</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
          <td>triage、release gate、PIR</td>
      </tr>
      <tr>
          <td>Verification evidence handoff</td>
          <td>hypothesis、scope、steady state、workload / fault、result、decision、owner</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
          <td>release gate、runbook、drill</td>
      </tr>
      <tr>
          <td>Incident decision log</td>
          <td>timestamp、decision、context、evidence、owner、expected effect、rollback condition</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
          <td>handoff、stakeholder update、PIR</td>
      </tr>
      <tr>
          <td>Incident evidence write-back</td>
          <td>finding、evidence、target artifact、owner、closure signal、review date</td>
          <td><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
          <td>dashboard、experiment、runbook</td>
      </tr>
  </tbody>
</table>
<p>Observability evidence package 是第一個 artifact。它保存查詢、時間窗、資料品質與 owner，讓後面的驗證與事故流程使用同一組事實。</p>
<p>Verification evidence handoff 是第二個 artifact。它把一次 load test、chaos drill、DR rehearsal 或 readiness review 的結果轉成 release gate 與 incident drill 可用的證據。</p>
<p>Incident decision log 是第三個 artifact。它把事中決策、證據、預期效果與回退條件保存下來，讓交班與復盤可以直接引用。</p>
<p>Incident evidence write-back 是第四個 artifact。它把事故學習轉成 dashboard、alert、SLO、experiment、runbook 或 automation boundary 的修改項。</p>
<h2 id="實作步驟">實作步驟</h2>
<p>實作步驟的責任是讓 slice 能被單次演練走完。每一步都產生一個可檢查輸出，避免流程只停在口頭共識。</p>
<ol>
<li>選定服務切片與核心 user journey。</li>
<li>定義 steady state：success rate、latency、queue lag、data correctness、customer impact。</li>
<li>補 observability evidence package：dashboard、query、trace、log、audit、data quality。</li>
<li>補 verification evidence handoff：load、chaos、DR 或 rollback rehearsal 的 hypothesis 與 result。</li>
<li>建 incident intake template：source、confidence、impact scope、evidence link、severity candidate。</li>
<li>建 incident decision log template：decision、owner、expected effect、rollback condition。</li>
<li>建 write-back template：finding、target artifact、closure signal、review date。</li>
<li>跑一次 tabletop 或 game day，確認 artifact 能被實際填寫。</li>
<li>把缺口回寫到 04 readiness、06 experiment 或 08 runbook。</li>
</ol>
<p>第一步要避免選太大的系統。選「checkout」比選「整個支付平台」更好，因為 slice 需要在一輪演練中跑完。</p>
<p>第二步要先定義穩態。沒有 steady state，load test、chaos 與 incident recovery 都會缺少共同終點。</p>
<p>第三步要保留 data quality 限制。若 trace sampling、log drop 或 metric ingest delay 會影響判讀，限制要跟 evidence 一起交接。</p>
<p>第四步要把驗證結果變成下游可用語言。Pass、conditional、fail 都要附上 scope、hypothesis 與下一步路由。</p>
<p>第五到第七步要先用輕量 template。template 跑通後，再把欄位搬進 incident tool、ticket system 或 runbook platform。</p>
<p>第八步要實際演練。tabletop 可以先驗證欄位與角色，game day 再驗證工具與訊號。</p>
<h2 id="最小-template">最小 template</h2>
<p>最小 template 的責任是讓第一輪不用等待工具導入。以下欄位可以直接放進 Markdown、ticket、incident doc 或 runbook。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">service_slice</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">journey</span><span class="p">:</span><span class="w"> </span><span class="l">checkout</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">payments-team</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">steady_state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">success_rate</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&gt;= 99.9% over 30m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">latency</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;p95 &lt;= 800ms&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">queue_lag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&lt;= 5m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">customer_impact</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;failed checkout count &lt;= threshold&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">evidence_package</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dashboard / log query / trace / audit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">time_range</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;incident window plus baseline&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">query_link</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;stable query URL or saved query name&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service or platform owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">data_quality</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;sampling, freshness, missing fields&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">confidence</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;confirmed / suspected / weak&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="nt">known_gap</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;missing signal or schema drift&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="nt">verification_handoff</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="nt">hypothesis</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;payment provider timeout triggers fallback within 2m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">scope</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;staging or 10% production traffic&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">workload_or_fault</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout injection against provider adapter&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">result</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pass / conditional / fail&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;release / block / follow-up / runbook update&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;closure owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-05-07T10:15:00Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;enable checkout fallback&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;provider timeout and rising failed checkout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;evidence_package link&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;incident commander or service owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;failed checkout drops within 10m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;fallback stale data exceeds threshold&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w"></span><span class="nt">write_back</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">  </span><span class="nt">finding</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;provider timeout alert lacks tenant dimension&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">  </span><span class="nt">target_artifact</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dashboard / alert / experiment / runbook&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">  </span><span class="nt">closure_signal</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;game day triggers tenant-scoped alert within 5m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">  </span><span class="nt">review_date</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;next readiness review&#34;</span></span></span></code></pre></div><p>這份 template 的價值是把四個 artifact 放在同一份文件中。第一輪可以手動填寫，第二輪再拆到不同工具。</p>
<h2 id="驗收門檻">驗收門檻</h2>
<p>驗收門檻的責任是判斷 slice 是否已經能支援實際事故。完成狀態要由團隊能否沿著 artifact 做出同一組判斷來確認。</p>
<table>
  <thead>
      <tr>
          <th>驗收項目</th>
          <th>通過訊號</th>
          <th>回寫位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Triage</td>
          <td>on-call 能用 evidence 判斷是否啟動事故</td>
          <td>8.18 intake</td>
      </tr>
      <tr>
          <td>Verification</td>
          <td>release owner 能讀 handoff 做放行判斷</td>
          <td>6.8 release gate</td>
      </tr>
      <tr>
          <td>Decision</td>
          <td>IC 能用 decision log 交班與回退</td>
          <td>8.19 decision log</td>
      </tr>
      <tr>
          <td>Communication</td>
          <td>stakeholder update 能引用同一組 impact</td>
          <td>8.10 comms</td>
      </tr>
      <tr>
          <td>Write-back</td>
          <td>PIR action item 有 target 與 closure</td>
          <td>8.22 write-back</td>
      </tr>
  </tbody>
</table>
<p>Triage 通過代表 evidence 能支援事故啟動。若 on-call 還需要臨場重新找資料，回到 4.16 readiness 與 4.20 evidence package。</p>
<p>Verification 通過代表驗證結果能支援 release 決策。若 release owner 只看到 pass / fail，回到 6.23 handoff 補 hypothesis、scope 與 data quality。</p>
<p>Decision 通過代表事故現場有共同記憶。若交班後需要重問背景，回到 8.19 decision log 補 context、evidence 與 rollback condition。</p>
<p>Write-back 通過代表事故學習有落點。若 action item 只有「補監控」或「更新文件」，回到 8.22 write-back 補 target artifact 與 closure signal。</p>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提醒團隊何時回到概念層補缺口。Vertical slice 的目的在於快速暴露 routing chain 哪裡斷掉，再用最小修正補上 artifact 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>evidence 找不到 owner</td>
          <td>觀測 operating model 缺口</td>
          <td>回到 4.18 owner 與 review cadence</td>
      </tr>
      <tr>
          <td>pass / fail 缺少決策力</td>
          <td>verification handoff 缺口</td>
          <td>回到 6.23 補 scope、hypothesis、decision</td>
      </tr>
      <tr>
          <td>IC 交班缺少共同記憶</td>
          <td>decision log 缺口</td>
          <td>回到 8.19 補最近決策、未完成動作與 rollback 條件</td>
      </tr>
      <tr>
          <td>PIR action 缺少關閉力</td>
          <td>write-back 缺口</td>
          <td>回到 8.22 補 closure signal 與 review date</td>
      </tr>
      <tr>
          <td>template 填寫成本過高</td>
          <td>欄位過多或工具摩擦</td>
          <td>刪到最小欄位，再跑一次 tabletop</td>
      </tr>
  </tbody>
</table>
<p>這些 tripwire 出現時，先修 artifact 與流程，再考慮導入新工具。工具能降低填寫成本，但欄位責任與 owner 需要先清楚。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">0.12 operations control service selection</a>：判斷目前缺的是訊號、驗證、響應還是閉環。</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence package</a>：建立可交接觀測證據。</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：定義實驗與事故共用成功條件。</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>：把驗證結果交給 release 與 incident。</li>
<li><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a>：保存事中決策與回退條件。</li>
<li><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 incident evidence write-back</a>：把事故學習回寫成可關閉改善。</li>
</ul>
]]></content:encoded></item><item><title>Google：Toil Budget 與 Automation 投資政策</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/</guid><description>&lt;p>Toil budget 的核心責任是把重複手動工作變成可治理成本。Google SRE 的關鍵做法是先量化 toil，再把超額部分強制導向自動化投資，而不是持續靠人力吸收。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>許多團隊的可靠性工作會被 incident handling 與手動修復吃掉。短期看似把事情解決，長期會造成兩個後果：一是 on-call 壓力升高，二是系統問題持續累積。沒有 toil budget 時，團隊很難判斷何時該停止加功能、先補工程基礎。&lt;/p>
&lt;h2 id="決策機制">決策機制&lt;/h2>
&lt;p>Toil budget 是把工時結果接到 release 與 backlog 決策的機制，單純統計工時只完成一半。&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>Toil 分類&lt;/td>
 &lt;td>哪些工作屬於可自動化 toil&lt;/td>
 &lt;td>toil taxonomy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間配比&lt;/td>
 &lt;td>toil 比例是否超過可承受區&lt;/td>
 &lt;td>budget 門檻（例如 50%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>超標處理&lt;/td>
 &lt;td>超標後怎麼調整優先序&lt;/td>
 &lt;td>凍結部分 feature、轉投自動化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>改善驗證&lt;/td>
 &lt;td>自動化是否真的回收工時&lt;/td>
 &lt;td>closure 指標與 evidence&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>toil ratio&lt;/td>
 &lt;td>是否長期超出預算&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>incident manual-step count&lt;/td>
 &lt;td>事故處理是否過度依賴人工&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/runbook-lifecycle/" data-link-title="8.16 Runbook Lifecycle 管理" data-link-desc="把 runbook 從一次性文件變成有版本、有演練、會過期的 artifact">8.16&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>automation closure rate&lt;/td>
 &lt;td>改善項是否真的落地&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>on-call overload signal&lt;/td>
 &lt;td>值班負荷是否持續上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>最常見錯誤是把 toil 視為「正常運維工作」，結果讓超標狀態常態化。另一個錯誤是只記錄工時，不把結果接到 release gate 與優先序調整。這兩種做法都會讓可靠性債繼續滾大。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>把 toil budget 落地時，先在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog&lt;/a> 建立分類與排序，再把超標條件接到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>。事後改善要回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sre.google/sre-book/table-of-contents/">Google SRE Book&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://sre.google/workbook/table-of-contents/">Google SRE Workbook&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Toil budget 的核心責任是把重複手動工作變成可治理成本。Google SRE 的關鍵做法是先量化 toil，再把超額部分強制導向自動化投資，而不是持續靠人力吸收。</p>
<h2 id="問題場景">問題場景</h2>
<p>許多團隊的可靠性工作會被 incident handling 與手動修復吃掉。短期看似把事情解決，長期會造成兩個後果：一是 on-call 壓力升高，二是系統問題持續累積。沒有 toil budget 時，團隊很難判斷何時該停止加功能、先補工程基礎。</p>
<h2 id="決策機制">決策機制</h2>
<p>Toil budget 是把工時結果接到 release 與 backlog 決策的機制，單純統計工時只完成一半。</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>實際輸出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Toil 分類</td>
          <td>哪些工作屬於可自動化 toil</td>
          <td>toil taxonomy</td>
      </tr>
      <tr>
          <td>時間配比</td>
          <td>toil 比例是否超過可承受區</td>
          <td>budget 門檻（例如 50%）</td>
      </tr>
      <tr>
          <td>超標處理</td>
          <td>超標後怎麼調整優先序</td>
          <td>凍結部分 feature、轉投自動化</td>
      </tr>
      <tr>
          <td>改善驗證</td>
          <td>自動化是否真的回收工時</td>
          <td>closure 指標與 evidence</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>toil ratio</td>
          <td>是否長期超出預算</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a></td>
      </tr>
      <tr>
          <td>incident manual-step count</td>
          <td>事故處理是否過度依賴人工</td>
          <td><a href="/blog/backend/08-incident-response/runbook-lifecycle/" data-link-title="8.16 Runbook Lifecycle 管理" data-link-desc="把 runbook 從一次性文件變成有版本、有演練、會過期的 artifact">8.16</a></td>
      </tr>
      <tr>
          <td>automation closure rate</td>
          <td>改善項是否真的落地</td>
          <td><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
      <tr>
          <td>on-call overload signal</td>
          <td>值班負荷是否持續上升</td>
          <td><a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>最常見錯誤是把 toil 視為「正常運維工作」，結果讓超標狀態常態化。另一個錯誤是只記錄工時，不把結果接到 release gate 與優先序調整。這兩種做法都會讓可靠性債繼續滾大。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>把 toil budget 落地時，先在 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a> 建立分類與排序，再把超標條件接到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。事後改善要回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sre.google/sre-book/table-of-contents/">Google SRE Book</a></li>
<li><a href="https://sre.google/workbook/table-of-contents/">Google SRE Workbook</a></li>
</ul>
]]></content:encoded></item><item><title>4.13 Service Topology 與 Dependency Map</title><link>https://tarrragon.github.io/blog/backend/04-observability/service-topology/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/service-topology/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何依賴拓撲需要獨立節點：人工維護的依賴圖永遠過時&lt;/li>
&lt;li>拓撲訊號的來源：trace（4.3）、service mesh（mTLS / sidecar）、network flow log&lt;/li>
&lt;li>服務 graph 的維度：呼叫頻率、latency、錯誤率、版本&lt;/li>
&lt;li>依賴變化告警：新增依賴、刪除依賴、依賴方向反轉&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 分析：上游失效時下游影響範圍預測&lt;/li>
&lt;li>動態叢集下的拓撲追蹤：擴縮事件如何回寫拓撲訊號&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/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&lt;/a> 的分工：trace 是單 request、topology 是統計聚合&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform&lt;/a> 的整合：service mesh 部署&lt;/li>
&lt;li>反模式：架構圖只在 wiki 上、跟實際流量漂移；新依賴上線缺 review；拓撲圖回答「這服務掛了誰受影響」需要人工追查&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Service topology 是把跨服務依賴從文件轉成可觀測資料的能力，責任是讓團隊能用實際呼叫關係判斷依賴、影響面與變更風險。&lt;/p>
&lt;p>這一頁處理的是服務關係圖。Trace 解釋單次 request、topology 解釋一段時間內的依賴結構；兩者合起來才能回答「這個服務壞了會影響誰」。&lt;/p>
&lt;p>人工維護的依賴圖在快速變動的微服務環境下會持續漂移。新服務上線、舊服務下架、依賴方向反轉、版本切換都會發生在 wiki 圖更新之前；事故時依賴 wiki 圖判讀 blast radius，會把過期的依賴結構誤當成當前事實。&lt;/p>
&lt;h2 id="拓撲訊號的來源">拓撲訊號的來源&lt;/h2>
&lt;p>Service topology 的可信度取決於資料來源是否反映真實流量。常見的訊號來源各有覆蓋範圍跟限制：&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>Trace（4.3）&lt;/td>
 &lt;td>應用層呼叫關係、含 latency / 錯誤率&lt;/td>
 &lt;td>需要 instrumentation 覆蓋、有採樣偏誤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service mesh&lt;/td>
 &lt;td>sidecar / mTLS 拦截的所有跨服務流量&lt;/td>
 &lt;td>依賴 mesh 部署、不含外部依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network flow log&lt;/td>
 &lt;td>L3 / L4 連線記錄、含外部依賴&lt;/td>
 &lt;td>缺少應用語意、難判斷哪個 service&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API gateway log&lt;/td>
 &lt;td>外部入口流量、含 client / API 維度&lt;/td>
 &lt;td>只看到 gateway 視角、不知道內部呼叫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實務上常用組合：trace 作為主要來源（提供應用語意跟錯誤率），service mesh 作為補充（補上未 instrument 的服務），network flow log 作為兜底（揭露未管理的外部依賴）。&lt;/p>
&lt;p>把不同來源的拓撲訊號合併時，要顯式記錄每段依賴的來源。當 trace 看不到某段依賴、service mesh 卻看得到時，可能意味著 instrumentation 缺失或服務 bypass mesh，這本身是治理訊號。&lt;/p>
&lt;h2 id="服務-graph-的維度">服務 Graph 的維度&lt;/h2>
&lt;p>服務 graph 的責任是把跨服務依賴量化成可判讀的訊號、支援事故決策跟容量規劃。每段依賴關係要帶上維度（頻率、latency、錯誤率、版本、可選性）、才能在事故時被直接使用、而非只能呈現拓撲輪廓。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>呼叫頻率&lt;/strong>：高頻依賴跟低頻依賴的失效影響不同。高頻依賴失效會立即放大成 5xx，低頻依賴失效可能要數小時才浮現。&lt;/li>
&lt;li>&lt;strong>Latency 分布&lt;/strong>：依賴 p50 / p99 latency 決定下游 timeout 應該設多少。沒有 latency 訊號的依賴圖無法支援 timeout 設計。&lt;/li>
&lt;li>&lt;strong>Error rate&lt;/strong>：依賴的錯誤率提供 budget 訊號。當某依賴錯誤率上升，下游應觸發降級、保護自身可用性、避免進入無限重試放大故障。&lt;/li>
&lt;li>&lt;strong>版本 / API contract&lt;/strong>：依賴的版本變化跟 API contract 變更要進拓撲訊號。版本升級後若某段依賴消失，可能是 contract breaking。&lt;/li>
&lt;li>&lt;strong>方向跟可選性&lt;/strong>：是必要依賴（失效 = 服務失敗）還是可選依賴（失效 = 功能降級），影響事故分級。&lt;/li>
&lt;/ul>
&lt;p>這些維度進入拓撲訊號後，配合 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget&lt;/a> 才能把依賴可靠性變成可量化決策。&lt;/p>
&lt;h2 id="依賴變化的治理">依賴變化的治理&lt;/h2>
&lt;p>依賴關係的變化本身是訊號。新增依賴、刪除依賴、依賴方向反轉，都是值得告警的事件。沒有依賴變化偵測時，新服務接入往往跳過依賴 review，事故發生才從 trace 反查到「原來這條 path 已經接了三個月」。&lt;/p>
&lt;p>可操作的依賴變化告警：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>新增依賴 alert&lt;/strong>：當 trace 出現新的 service-to-service 呼叫，觸發 review。新依賴是否在預期內、是否經過 contract review、是否有 fallback。&lt;/li>
&lt;li>&lt;strong>依賴消失 alert&lt;/strong>：某段穩定存在的依賴在 N 分鐘內 trace 看不到，可能是 instrumentation 漏、可能是上游被誤改、可能是真實事故的早期訊號。&lt;/li>
&lt;li>&lt;strong>依賴方向反轉&lt;/strong>：A → B 變成 B → A 通常意味著 refactor 或誤改、應該觸發 review。&lt;/li>
&lt;li>&lt;strong>循環依賴偵測&lt;/strong>：環狀依賴會在事故時放大恢復難度、應該在拓撲訊號層級就阻擋。&lt;/li>
&lt;/ol>
&lt;h2 id="動態叢集下的拓撲訊號">動態叢集下的拓撲訊號&lt;/h2>
&lt;p>動態叢集下拓撲訊號的責任是讓觀測模型追上實際依賴結構的變化。Pod 數量浮動、node 換代、service IP 變化、跨 cluster 流量重新分配都會在分鐘級內改變服務間的可達性、若拓撲訊號停留在週期性快照、事故時看到的會是過期結構。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何依賴拓撲需要獨立節點：人工維護的依賴圖永遠過時</li>
<li>拓撲訊號的來源：trace（4.3）、service mesh（mTLS / sidecar）、network flow log</li>
<li>服務 graph 的維度：呼叫頻率、latency、錯誤率、版本</li>
<li>依賴變化告警：新增依賴、刪除依賴、依賴方向反轉</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 分析：上游失效時下游影響範圍預測</li>
<li>動態叢集下的拓撲追蹤：擴縮事件如何回寫拓撲訊號</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</a> 的分工：trace 是單 request、topology 是統計聚合</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform</a> 的整合：service mesh 部署</li>
<li>反模式：架構圖只在 wiki 上、跟實際流量漂移；新依賴上線缺 review；拓撲圖回答「這服務掛了誰受影響」需要人工追查</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Service topology 是把跨服務依賴從文件轉成可觀測資料的能力，責任是讓團隊能用實際呼叫關係判斷依賴、影響面與變更風險。</p>
<p>這一頁處理的是服務關係圖。Trace 解釋單次 request、topology 解釋一段時間內的依賴結構；兩者合起來才能回答「這個服務壞了會影響誰」。</p>
<p>人工維護的依賴圖在快速變動的微服務環境下會持續漂移。新服務上線、舊服務下架、依賴方向反轉、版本切換都會發生在 wiki 圖更新之前；事故時依賴 wiki 圖判讀 blast radius，會把過期的依賴結構誤當成當前事實。</p>
<h2 id="拓撲訊號的來源">拓撲訊號的來源</h2>
<p>Service topology 的可信度取決於資料來源是否反映真實流量。常見的訊號來源各有覆蓋範圍跟限制：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>覆蓋範圍</th>
          <th>主要限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trace（4.3）</td>
          <td>應用層呼叫關係、含 latency / 錯誤率</td>
          <td>需要 instrumentation 覆蓋、有採樣偏誤</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>sidecar / mTLS 拦截的所有跨服務流量</td>
          <td>依賴 mesh 部署、不含外部依賴</td>
      </tr>
      <tr>
          <td>Network flow log</td>
          <td>L3 / L4 連線記錄、含外部依賴</td>
          <td>缺少應用語意、難判斷哪個 service</td>
      </tr>
      <tr>
          <td>API gateway log</td>
          <td>外部入口流量、含 client / API 維度</td>
          <td>只看到 gateway 視角、不知道內部呼叫</td>
      </tr>
  </tbody>
</table>
<p>實務上常用組合：trace 作為主要來源（提供應用語意跟錯誤率），service mesh 作為補充（補上未 instrument 的服務），network flow log 作為兜底（揭露未管理的外部依賴）。</p>
<p>把不同來源的拓撲訊號合併時，要顯式記錄每段依賴的來源。當 trace 看不到某段依賴、service mesh 卻看得到時，可能意味著 instrumentation 缺失或服務 bypass mesh，這本身是治理訊號。</p>
<h2 id="服務-graph-的維度">服務 Graph 的維度</h2>
<p>服務 graph 的責任是把跨服務依賴量化成可判讀的訊號、支援事故決策跟容量規劃。每段依賴關係要帶上維度（頻率、latency、錯誤率、版本、可選性）、才能在事故時被直接使用、而非只能呈現拓撲輪廓。</p>
<ul>
<li><strong>呼叫頻率</strong>：高頻依賴跟低頻依賴的失效影響不同。高頻依賴失效會立即放大成 5xx，低頻依賴失效可能要數小時才浮現。</li>
<li><strong>Latency 分布</strong>：依賴 p50 / p99 latency 決定下游 timeout 應該設多少。沒有 latency 訊號的依賴圖無法支援 timeout 設計。</li>
<li><strong>Error rate</strong>：依賴的錯誤率提供 budget 訊號。當某依賴錯誤率上升，下游應觸發降級、保護自身可用性、避免進入無限重試放大故障。</li>
<li><strong>版本 / API contract</strong>：依賴的版本變化跟 API contract 變更要進拓撲訊號。版本升級後若某段依賴消失，可能是 contract breaking。</li>
<li><strong>方向跟可選性</strong>：是必要依賴（失效 = 服務失敗）還是可選依賴（失效 = 功能降級），影響事故分級。</li>
</ul>
<p>這些維度進入拓撲訊號後，配合 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a> 才能把依賴可靠性變成可量化決策。</p>
<h2 id="依賴變化的治理">依賴變化的治理</h2>
<p>依賴關係的變化本身是訊號。新增依賴、刪除依賴、依賴方向反轉，都是值得告警的事件。沒有依賴變化偵測時，新服務接入往往跳過依賴 review，事故發生才從 trace 反查到「原來這條 path 已經接了三個月」。</p>
<p>可操作的依賴變化告警：</p>
<ol>
<li><strong>新增依賴 alert</strong>：當 trace 出現新的 service-to-service 呼叫，觸發 review。新依賴是否在預期內、是否經過 contract review、是否有 fallback。</li>
<li><strong>依賴消失 alert</strong>：某段穩定存在的依賴在 N 分鐘內 trace 看不到，可能是 instrumentation 漏、可能是上游被誤改、可能是真實事故的早期訊號。</li>
<li><strong>依賴方向反轉</strong>：A → B 變成 B → A 通常意味著 refactor 或誤改、應該觸發 review。</li>
<li><strong>循環依賴偵測</strong>：環狀依賴會在事故時放大恢復難度、應該在拓撲訊號層級就阻擋。</li>
</ol>
<h2 id="動態叢集下的拓撲訊號">動態叢集下的拓撲訊號</h2>
<p>動態叢集下拓撲訊號的責任是讓觀測模型追上實際依賴結構的變化。Pod 數量浮動、node 換代、service IP 變化、跨 cluster 流量重新分配都會在分鐘級內改變服務間的可達性、若拓撲訊號停留在週期性快照、事故時看到的會是過期結構。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s 規模化下的觀測訊號治理</a>：揭露「叢集擴縮跟工作負載變動需要回寫觀測模型」「叢集層指標跟服務層指標要分開治理」「擴縮事件跟事故關聯要可回溯」三個方向（case 直接列出）；以下展開的 service 層級節點、跨 cluster failover、drill-down 設計屬通用 K8s observability 經驗、case 本身未細說。</p>
<p>動態叢集對拓撲訊號的挑戰有三個面向、性質不同、各自的對應做法也不同。</p>
<p><strong>拓撲節點不穩定</strong> 是資料模型層的問題。Pod 短暫存在、IP 不固定、若直接把 Pod 當拓撲節點、graph 會分鐘級持續抖動、事故時看到的依賴結構不可信。對應做法是把節點層級從 Pod / IP 提升到 service（service name + version + region）、把 instance / Pod 層級放到 dashboard drill-down、讓主拓撲圖反映穩定的服務依賴而非瞬時實例分布。</p>
<p><strong>擴縮事件 vs 真實事故區分</strong> 是訊號分辨層的問題。HPA scale-up / scale-down、cluster autoscaler 加 node 失敗、Pod 重啟、health check 短暫失敗，這些擴縮動作本身會產生跟事故相似的訊號（5xx 短暫升高、reconnect、依賴連線中斷）、若沒分辨機制、值班會把擴縮過程的正常波動誤判成事故、或把真正的事故誤判成擴縮。對應做法是把擴縮事件本身打進 timeline、跟事故 timeline 共用同一張圖、判讀時對齊看。</p>
<p><strong>跨 cluster 流量變化</strong> 是視角層的問題。multi-cluster 部署下、流量可能因 cluster 變更從 cluster A 切到 cluster B、若拓撲圖只看單 cluster 視角、B cluster 突增的流量會被解讀為 traffic spike、漏掉真正的 failover 事件。對應做法是讓拓撲圖呈現跨 cluster 邊界、把 cluster 間流量變化也標到圖上、避免 cluster 邊界成為觀測盲區。</p>
<p>把叢集層指標（node count、Pod count、HPA event）跟服務層指標（call rate、error rate、latency）分開治理，是動態叢集環境的基本要求。叢集層指標的 owner 通常是 platform team、服務層指標的 owner 通常是 service team，兩者放在同一 dashboard 上要清楚標示來源跟責任。</p>
<p>擴縮事件回溯到事故關聯的另一個價值是 capacity retrospective。當 HPA 在事故前後觸發、scale-up 是否足夠、scale-down 是否過快，都需要把擴縮 timeline 跟事故 timeline 拼起來看，回到 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量成本</a> 跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃</a> 的回寫。</p>
<h2 id="blast-radius-推導">Blast Radius 推導</h2>
<p>Blast radius 分析的核心責任是回答「如果這個服務或依賴失效、哪些上游 / 下游會受影響、影響多深」。沒有實時拓撲訊號時，這個分析靠經驗、容易低估或高估。</p>
<p>實時 topology 加上依賴可選性標記後，blast radius 可以分層推導：</p>
<ul>
<li><strong>直接下游</strong>：直接呼叫該服務的服務、立即受影響。</li>
<li><strong>間接下游</strong>：透過中間服務間接依賴、影響時間延後。</li>
<li><strong>可降級下游</strong>：依賴是 optional、失效會觸發降級但不失敗。</li>
<li><strong>必要下游</strong>：依賴是 mandatory、失效會傳播成服務失敗。</li>
</ul>
<p>事故時把 blast radius 從拓撲推導出來、再對照實際看到的 5xx 跟 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、能驗證影響面是否符合預期。當實際影響超出推導 blast radius、通常意味著存在未紀錄依賴。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 topology 時，先看資料是否來自真實流量，再看依賴變化是否能被治理。</p>
<p>重點訊號包括：</p>
<ul>
<li>service graph 是否包含呼叫方向、頻率、latency 與 error rate</li>
<li>新增依賴是否能觸發 review 或 alert</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 是否能從上游 / 下游關係推導</li>
<li>topology 是否能餵給 dependency budget 與事故型態判讀</li>
<li>動態擴縮事件是否打進 timeline、能跟事故區分</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時回答「誰呼叫這服務」需要人工追查</li>
<li>新服務接入無依賴 review、出事後才發現連結</li>
<li>架構文件跟實際呼叫關係漂移、半年沒更新</li>
<li>service mesh 部署但拓撲訊號未被使用</li>
<li>循環依賴存在但無人發現</li>
<li>擴縮事件造成的短暫錯誤被誤判成事故</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Wiki 架構圖</td>
          <td>圖跟實際流量漂移半年</td>
          <td>從 trace / mesh 自動生成、持續更新</td>
      </tr>
      <tr>
          <td>新依賴無 review</td>
          <td>trace 出現新依賴沒人知道</td>
          <td>新依賴 alert、依賴 review 進 release flow</td>
      </tr>
      <tr>
          <td>拓撲節點用 Pod / Instance</td>
          <td>動態叢集下圖持續抖動</td>
          <td>service 層級節點、Pod 放 drill-down</td>
      </tr>
      <tr>
          <td>叢集跟服務指標混在一張圖</td>
          <td>platform 跟 service 責任不清</td>
          <td>分層 dashboard、明確 owner</td>
      </tr>
      <tr>
          <td>Blast radius 靠經驗推導</td>
          <td>影響面評估不準、事後才發現遺漏</td>
          <td>從拓撲訊號自動推導、跟實際影響對照</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<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</a>：拓撲訊號的原始來源</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：叢集層 / 服務層 ownership 分工</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：service mesh 配置</li>
<li>6.5 pre-mortem（規劃中）：依賴失效路徑分析</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity cost</a>：擴縮事件 retrospective</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a>：拓撲是依賴可靠性評估的資料來源</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.9 事故型態庫</a>：<a href="/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">cascading failure</a> 型態的拓撲依據</li>
</ul>
]]></content:encoded></item><item><title>6.13 Performance Regression Gate</title><link>https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Performance regression gate 守住系統的效能餘裕 — 避免看似功能正確的變更悄悄拖垮延遲、吞吐或成本。&lt;/p>
&lt;p>這一頁關心的是變更有沒有偷走系統的效能餘裕。沒有 gate，效能退化常常要等使用者感受到才會被看見。跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test&lt;/a> 的分工是：6.2 訂定 baseline 與 saturation point，6.13 確保每次變更不會讓 baseline 被偷走。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>效能 gate 的健康度取決於 baseline 是否穩定、regression 偵測是否足夠敏感。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>baseline 是否來自 production-like workload&lt;/li>
&lt;li>regression 是否能分辨 noise 與真實退化&lt;/li>
&lt;li>perf budget 是否跟 release gate 綁定&lt;/li>
&lt;li>當退化出現時，是否能快速定位到 code path 或依賴&lt;/li>
&lt;/ul>
&lt;h2 id="baseline-設定">Baseline 設定&lt;/h2>
&lt;p>Baseline 的責任是提供可比較的效能基準。沒有穩定 baseline，gate 判讀就無法區分「系統真的變慢了」跟「環境噪音」。&lt;/p>
&lt;p>Baseline 有三種來源，各自的可信度與維護成本不同。&lt;/p>
&lt;p>&lt;strong>Production percentile&lt;/strong>：從 production 的 latency / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput&lt;/a> 分佈取 p50 / p95 / p99 作為基準。優點是最接近真實使用者體驗；限制是 production 流量本身有時段波動，需要選定穩定時段的統計窗口。適合作為最終判準，但不適合作為 CI 內的即時 gate（CI 環境跟 production 差異太大）。&lt;/p>
&lt;p>&lt;strong>CI benchmark history&lt;/strong>：在同一 CI 環境、同一 workload 下累積歷史趨勢。優點是環境一致，regression 可歸因到 code 變更；限制是 CI 環境本身可能有波動（runner 硬體、鄰居效應），需要 variance 控制。適合作為每次 merge 的即時 gate。&lt;/p>
&lt;p>&lt;strong>Load test 結果&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test&lt;/a> 產出的 saturation point 與 latency inflection。優點是覆蓋高負載場景；限制是執行成本高、不適合每次 push 跑。適合作為 scheduled path 的 baseline 校準來源。&lt;/p>
&lt;p>Baseline 更新頻率跟系統變更頻率對齊。高頻變更服務（每日多次 deploy）需要 rolling baseline（取最近 N 次 CI 結果的中位數）；低頻變更服務可以用固定 baseline 搭配季度校準。&lt;/p>
&lt;p>Baseline 品質的判準是自身 variance。若 baseline 的 p99 波動超過 5-10%，任何小於這個幅度的 regression 都落在噪音區間內，gate 無法可靠判讀。此時應先控制 variance（見下段），再設定 regression 門檻。&lt;/p>
&lt;h2 id="regression-判讀方法">Regression 判讀方法&lt;/h2>
&lt;p>Regression 判讀有三種方法，選擇取決於 CI 環境的穩定性與測試時間預算。&lt;/p>
&lt;h3 id="絕對門檻">絕對門檻&lt;/h3>
&lt;p>設定 p99 latency 上限（例如 200ms）或 throughput 下限（例如 1000 RPS），超過就 fail。&lt;/p>
&lt;p>這種方法實作最簡單，適合有明確 SLA 的服務。限制是容易誤報（環境噪音造成的瞬間飆高）或漏報（慢速退化每次只惡化 2-3ms，始終低於門檻，累積半年後才被注意到）。適合作為安全網而非主要判讀手段。&lt;/p>
&lt;h3 id="相對退化">相對退化&lt;/h3>
&lt;p>跟前一版 baseline 比較，退化超過 Y%（例如 latency 增加 &amp;gt; 10%）就 fail。&lt;/p>
&lt;p>這種方法能抓到漸進退化，因為每一次小幅惡化都會觸發。前提是 baseline 穩定 — 若 baseline 自身波動 8%，設定 10% 門檻幾乎沒有判讀空間。適合 variance 已被控制到 3-5% 以內的 CI 環境。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Performance regression gate 守住系統的效能餘裕 — 避免看似功能正確的變更悄悄拖垮延遲、吞吐或成本。</p>
<p>這一頁關心的是變更有沒有偷走系統的效能餘裕。沒有 gate，效能退化常常要等使用者感受到才會被看見。跟 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a> 的分工是：6.2 訂定 baseline 與 saturation point，6.13 確保每次變更不會讓 baseline 被偷走。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>效能 gate 的健康度取決於 baseline 是否穩定、regression 偵測是否足夠敏感。</p>
<p>重點訊號包括：</p>
<ul>
<li>baseline 是否來自 production-like workload</li>
<li>regression 是否能分辨 noise 與真實退化</li>
<li>perf budget 是否跟 release gate 綁定</li>
<li>當退化出現時，是否能快速定位到 code path 或依賴</li>
</ul>
<h2 id="baseline-設定">Baseline 設定</h2>
<p>Baseline 的責任是提供可比較的效能基準。沒有穩定 baseline，gate 判讀就無法區分「系統真的變慢了」跟「環境噪音」。</p>
<p>Baseline 有三種來源，各自的可信度與維護成本不同。</p>
<p><strong>Production percentile</strong>：從 production 的 latency / <a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a> 分佈取 p50 / p95 / p99 作為基準。優點是最接近真實使用者體驗；限制是 production 流量本身有時段波動，需要選定穩定時段的統計窗口。適合作為最終判準，但不適合作為 CI 內的即時 gate（CI 環境跟 production 差異太大）。</p>
<p><strong>CI benchmark history</strong>：在同一 CI 環境、同一 workload 下累積歷史趨勢。優點是環境一致，regression 可歸因到 code 變更；限制是 CI 環境本身可能有波動（runner 硬體、鄰居效應），需要 variance 控制。適合作為每次 merge 的即時 gate。</p>
<p><strong>Load test 結果</strong>：<a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a> 產出的 saturation point 與 latency inflection。優點是覆蓋高負載場景；限制是執行成本高、不適合每次 push 跑。適合作為 scheduled path 的 baseline 校準來源。</p>
<p>Baseline 更新頻率跟系統變更頻率對齊。高頻變更服務（每日多次 deploy）需要 rolling baseline（取最近 N 次 CI 結果的中位數）；低頻變更服務可以用固定 baseline 搭配季度校準。</p>
<p>Baseline 品質的判準是自身 variance。若 baseline 的 p99 波動超過 5-10%，任何小於這個幅度的 regression 都落在噪音區間內，gate 無法可靠判讀。此時應先控制 variance（見下段），再設定 regression 門檻。</p>
<h2 id="regression-判讀方法">Regression 判讀方法</h2>
<p>Regression 判讀有三種方法，選擇取決於 CI 環境的穩定性與測試時間預算。</p>
<h3 id="絕對門檻">絕對門檻</h3>
<p>設定 p99 latency 上限（例如 200ms）或 throughput 下限（例如 1000 RPS），超過就 fail。</p>
<p>這種方法實作最簡單，適合有明確 SLA 的服務。限制是容易誤報（環境噪音造成的瞬間飆高）或漏報（慢速退化每次只惡化 2-3ms，始終低於門檻，累積半年後才被注意到）。適合作為安全網而非主要判讀手段。</p>
<h3 id="相對退化">相對退化</h3>
<p>跟前一版 baseline 比較，退化超過 Y%（例如 latency 增加 &gt; 10%）就 fail。</p>
<p>這種方法能抓到漸進退化，因為每一次小幅惡化都會觸發。前提是 baseline 穩定 — 若 baseline 自身波動 8%，設定 10% 門檻幾乎沒有判讀空間。適合 variance 已被控制到 3-5% 以內的 CI 環境。</p>
<h3 id="統計顯著性">統計顯著性</h3>
<p>用統計檢定（t-test、Mann-Whitney U）判斷兩組測量的分佈是否有顯著差異。</p>
<p>這種方法最準確，能在高 variance 環境中篩掉噪音。限制是需要足夠樣本量 — CI 短時間測試可能只跑 10-20 次 iteration，樣本不足時統計功效低，真實退化也可能被判為不顯著。適合測試時間預算充裕的 scheduled path。</p>
<p>三種方法可以組合：fast path 用絕對門檻做安全網，slow path 用相對退化做主要判讀，scheduled path 用統計檢定做精確校準。</p>
<h2 id="variance-控制">Variance 控制</h2>
<p>CI 環境的噪音是 perf gate 最大的干擾源。噪音讓真實退化被遮蓋，也讓正常變更被誤報，兩者都會侵蝕團隊對 gate 的信任。</p>
<p>主要噪音來源與對應控制方式：</p>
<table>
  <thead>
      <tr>
          <th>噪音來源</th>
          <th>機制</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Shared runner 鄰居效應</td>
          <td>其他 job 搶 CPU / memory / I/O</td>
          <td>Dedicated runner 或 ephemeral instance</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>JIT warmup、cache miss、connection 建立</td>
          <td>Warmup iteration（丟棄前 N 次結果）</td>
      </tr>
      <tr>
          <td>GC pause</td>
          <td>記憶體壓力觸發 stop-the-world GC</td>
          <td>固定 heap size、GC log 同步收集</td>
      </tr>
      <tr>
          <td>Network jitter</td>
          <td>跨服務通訊的延遲波動</td>
          <td>Local dependency（mock / sidecar）</td>
      </tr>
      <tr>
          <td>Hardware 差異</td>
          <td>不同世代 runner 的 CPU 效能不同</td>
          <td>Pinned hardware config / instance type</td>
      </tr>
  </tbody>
</table>
<p>Variance 控制的投資報酬是讓 regression 門檻可以設得更敏感。當 variance 從 15% 降到 3%，gate 就能攔住 5% 的退化；否則只能設 20% 門檻，等於放過大量漸進退化。</p>
<p>連到 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a> 的 environment 隔離段 — perf gate 需要的 runner 隔離等級通常高於一般功能測試。</p>
<h2 id="micro-benchmark-vs-end-to-end-perf-test">Micro benchmark vs End-to-end perf test</h2>
<p>兩種測試粒度服務不同的判讀需求，分工而非替代。</p>
<p><strong>Micro benchmark</strong> 對準單一函式、code path 或演算法。variance 小（不涉及 I/O、network、GC 壓力低）、回饋快（秒級）、定位精準（退化直接指向特定函式）。限制是覆蓋不到跨服務退化、serialization 成本或 middleware 堆疊的效能影響。適合跑在 CI fast path（每次 push）。</p>
<p><strong>End-to-end perf test</strong> 覆蓋真實請求路徑，從 API gateway 到 database 到 response。能抓到跨層退化（middleware 累積、serialization 成本、connection pool 競爭），但 variance 大、定位困難（退化可能來自任何一層）。適合跑在 CI slow path（merge gate）或 scheduled path。</p>
<p>分工原則：micro benchmark 負責守住 code-level baseline，end-to-end perf test 負責守住 service-level baseline。兩者都 fail 時，micro benchmark 的結果通常能直接定位 regression 來源；只有 end-to-end fail 時，需要搭配 profiling diff 做進一步歸因。</p>
<h2 id="退化定位與行動">退化定位與行動</h2>
<p>Gate 攔住 regression 後，下一步是定位來源並決定行動。</p>
<p><strong>Profiling diff</strong>：比較兩版的 flame graph 或 CPU profile，找出新增的 hot path。連到 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling</a> — 若 production 已有 continuous profiling，可以直接比較 canary 與 stable 版本的 profile 差異，定位精度高於 CI 環境的 benchmark。</p>
<p><strong>Commit bisect</strong>：在 CI benchmark history 中二分搜尋 regression 引入點。當多個 commit 合併後才觸發 gate fail，bisect 能縮小到具體 commit。前提是 CI benchmark 有逐 commit 的歷史紀錄。</p>
<p>定位後的行動有三種：</p>
<ul>
<li><strong>修復</strong>：regression 來源明確、修復成本可接受。這是預設行動。</li>
<li><strong>接受</strong>：regression 是預期的 trade-off（例如安全性改善帶來的加密成本）。此時更新 baseline，並在 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 evidence handoff</a> 記錄接受理由。</li>
<li><strong>延後</strong>：regression 來源複雜、修復需要大幅重構。記錄到 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a> 並設定修復期限。延後的風險是多次延後累積成使用者可感知的退化。</li>
</ul>
<h2 id="產業情境串流與媒體服務">產業情境：串流與媒體服務</h2>
<p>串流服務的效能 regression 量測維度跟一般 web service 不同。API latency 只是其中一層，媒體交付品質才是使用者直接感受的指標。</p>
<p>串流特有的 regression 指標包含 video start time（TTFB to first frame）、rebuffering rate（播放中斷頻率）、bitrate switches per session（畫質跳動次數）與 ABR algorithm response time（adaptive bitrate 的反應速度）。這些指標需要專門的量測管線，CI 環境的 mock player 很難完全模擬真實觀看行為，canary 階段的 real user monitoring 是更可靠的 regression 偵測來源。</p>
<p>Transcoding pipeline 的 regression 需要三維判讀。新 codec 或 encoder 版本可能改善壓縮率但增加 encoding latency，CI gate 需要同時量化 encoding speed、output quality 與 cost — 只看其中一個維度會漏掉 trade-off。例如 AV1 encoder 比 H.264 壓縮率更好，但 encoding 時間可能增加數倍，若 gate 只看 latency 就會擋住合理的品質升級。</p>
<p>CDN cache hit rate 是隱性的 regression 指標。code 變更如果改變了 cache key 策略或 content fingerprint，CDN cache hit rate 會下降，回源流量上升，間接造成 origin latency 惡化與成本跳升。這類 regression 在 staging 壓測中看不到（staging 沒有 CDN 快取層），需要 canary 階段的 CDN 層監控才能偵測。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google G1</a>：效能退化會加速 error budget 消耗。當 latency regression 導致 SLO breach 頻率上升，perf gate 的門檻應與 error budget 政策連動 — budget 健康時接受較寬鬆的門檻，budget 緊繃時收緊。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">LinkedIn L1</a>：效能退化直接壓縮 capacity headroom。當 p99 latency 上升 20%，等效 headroom 下降，可能觸發 on-call 層級升級。perf gate 的門檻應考慮 headroom ratio 的安全邊界。</li>
<li><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify H1</a>：高峰前的效能退化風險比平時更高。BFCM 前收緊 perf gate 門檻，避免峰值期間 latency regression 與流量尖峰疊加。</li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">LinkedIn L2</a>：持續壓測作為 regression 偵測的輸入來源 — 自動化壓測的 saturation point 趨勢可以補充 CI benchmark 看不到的系統級退化。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連續多版微小退化、累積後才被發現</td>
          <td>相對退化門檻未設或太寬鬆，改用 rolling baseline + 相對退化判讀</td>
          <td>設 rolling baseline + 5-10% 相對退化 threshold</td>
      </tr>
      <tr>
          <td>大版本升級 latency 漲、定位困難</td>
          <td>缺少逐 commit benchmark history，補 commit bisect 機制</td>
          <td>每個 commit 跑 micro benchmark、保留歷史</td>
      </tr>
      <tr>
          <td>Benchmark variance &gt; 退化幅度</td>
          <td>CI 環境噪音未控制，先降 variance 再設門檻</td>
          <td>改用 dedicated runner + warmup iteration</td>
      </tr>
      <tr>
          <td>Canary 只看 error rate、不看 latency</td>
          <td>perf gate 與 canary 判讀脫鉤，把 latency percentile 加入 canary</td>
          <td>補 p95/p99 latency 到 canary 判讀指標</td>
      </tr>
      <tr>
          <td>第三方依賴效能變化未納入 baseline</td>
          <td>baseline 只看本服務、漏掉依賴，補 end-to-end perf test 覆蓋</td>
          <td>加 end-to-end perf test 到 slow path</td>
      </tr>
      <tr>
          <td>Gate 頻繁誤報、團隊開始忽略</td>
          <td>門檻未對齊 variance，或測試環境不穩定，先修 variance 再調門檻</td>
          <td>先量測 variance、再設 threshold = baseline + 2σ</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling</a>：退化定位到 callstack</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 部署平台</a>：canary 階段的 perf gate</li>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：perf test 在 CI 分層中的位置與 runner 隔離</li>
<li><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load test</a>：baseline 來源與 saturation point</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：退化觸發 freeze</li>
<li><a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 feature flag</a>：flag 切換後的效能驗證</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt</a>：延後修復的 regression 進入 debt backlog</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 evidence handoff</a>：接受 regression 時的理由留存</li>
</ul>
]]></content:encoded></item><item><title>8.13 Repeated Incident 與 Toil 治理</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/repeated-incident-toil/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/repeated-incident-toil/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何 repeated incident 需要獨立節點：單次 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 解不了系統性問題&lt;/li>
&lt;li>識別 repeated pattern：靠 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">8.9 事故型態庫&lt;/a> 標籤分類、跨 incident 統計&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 的定義：重複、手動、無永久價值、可自動化（Google SRE Book）&lt;/li>
&lt;li>從 manual runbook 到 automation 的演進路徑&lt;/li>
&lt;li>repeated incident 的根因類別：監控盲區、架構缺陷、流程斷點、人力不足&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 撥用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> reduction 的政策&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review&lt;/a> 的差異：8.5 處理單事故、8.13 處理 pattern&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO error budget&lt;/a> 的整合：error budget 餘額分配給 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> reduction&lt;/li>
&lt;li>反模式：每次事故 action items 都是「補 alert / 補 runbook」；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 視為值班個人問題；repeated pattern 無人擁有&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Repeated incident 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 治理是把同型事故反覆發生與重複手動修復當成工程化治理對象，責任是把「一直在處理」轉成「一次修掉」。&lt;/p>
&lt;p>這一頁處理的是 pattern 層級問題。單次 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 只能修一個事件，重複事故需要的是跨事件的抽象與自動化。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 repeated incident 時，先看是否真的重複，再看能否用 automation 吃掉手動成本。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>同類 alert 是否週期性觸發&lt;/li>
&lt;li>action items 是否在多次 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 重複出現&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 是否佔據過多值班時間&lt;/li>
&lt;li>是否已經有明確 automation 路線&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台級事故常會形成重複修復與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a>。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：通知與協作流程容易留下固定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a>。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog&lt;/a>：監控依賴失效時，值班可能被重複告警拖住。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>06.6 error budget：撥用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> reduction 的政策&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：跨事故 pattern 分析&lt;/li>
&lt;li>08.6 drills：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 自動化後的演練更新&lt;/li>
&lt;li>08.9 pattern library：repeated pattern 抽卡&lt;/li>
&lt;li>08.14 multi-incident：同源事故合併判讀&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同類 alert 每週 / 每月固定觸發、靠值班手動處理&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> action items 跨多次事故重複出現&lt;/li>
&lt;li>值班滿意度低、招募 / 留任困難&lt;/li>
&lt;li>「這個我上次也修過」是值班共通語&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 占值班時間 &amp;gt; 50%、無工程化 budget&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>06.6 error budget：撥用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> reduction 的政策&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：跨事故 pattern 分析&lt;/li>
&lt;li>08.6 drills：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 自動化後的演練更新&lt;/li>
&lt;li>08.9 pattern library：repeated pattern 抽卡&lt;/li>
&lt;li>08.14 multi-incident：同源事故合併判讀&lt;/li>
&lt;li>08.16 runbook lifecycle：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 自動化後 runbook 退場&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何 repeated incident 需要獨立節點：單次 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 解不了系統性問題</li>
<li>識別 repeated pattern：靠 <a href="/blog/backend/08-incident-response/incident-pattern-library/" data-link-title="8.9 事故型態庫入口" data-link-desc="把跨服務的共通事故型態抽成型態卡，作為新事故的判讀錨點">8.9 事故型態庫</a> 標籤分類、跨 incident 統計</li>
<li><a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 的定義：重複、手動、無永久價值、可自動化（Google SRE Book）</li>
<li>從 manual runbook 到 automation 的演進路徑</li>
<li>repeated incident 的根因類別：監控盲區、架構缺陷、流程斷點、人力不足</li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 撥用 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> reduction 的政策</li>
<li>跟 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a> 的差異：8.5 處理單事故、8.13 處理 pattern</li>
<li>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO error budget</a> 的整合：error budget 餘額分配給 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> reduction</li>
<li>反模式：每次事故 action items 都是「補 alert / 補 runbook」；<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 視為值班個人問題；repeated pattern 無人擁有</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Repeated incident 與 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 治理是把同型事故反覆發生與重複手動修復當成工程化治理對象，責任是把「一直在處理」轉成「一次修掉」。</p>
<p>這一頁處理的是 pattern 層級問題。單次 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 只能修一個事件，重複事故需要的是跨事件的抽象與自動化。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 repeated incident 時，先看是否真的重複，再看能否用 automation 吃掉手動成本。</p>
<p>重點訊號包括：</p>
<ul>
<li>同類 alert 是否週期性觸發</li>
<li>action items 是否在多次 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 重複出現</li>
<li><a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 是否佔據過多值班時間</li>
<li>是否已經有明確 automation 路線</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台級事故常會形成重複修復與 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a>。</li>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：通知與協作流程容易留下固定 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a>。</li>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog</a>：監控依賴失效時，值班可能被重複告警拖住。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>06.6 error budget：撥用 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> reduction 的政策</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：跨事故 pattern 分析</li>
<li>08.6 drills：<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 自動化後的演練更新</li>
<li>08.9 pattern library：repeated pattern 抽卡</li>
<li>08.14 multi-incident：同源事故合併判讀</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同類 alert 每週 / 每月固定觸發、靠值班手動處理</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> action items 跨多次事故重複出現</li>
<li>值班滿意度低、招募 / 留任困難</li>
<li>「這個我上次也修過」是值班共通語</li>
<li><a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 占值班時間 &gt; 50%、無工程化 budget</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>06.6 error budget：撥用 <a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> reduction 的政策</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：跨事故 pattern 分析</li>
<li>08.6 drills：<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 自動化後的演練更新</li>
<li>08.9 pattern library：repeated pattern 抽卡</li>
<li>08.14 multi-incident：同源事故合併判讀</li>
<li>08.16 runbook lifecycle：<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 自動化後 runbook 退場</li>
</ul>
]]></content:encoded></item><item><title>Discord</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/</guid><description>&lt;p>Discord 是大規模長連線 gateway 的代表、事故多源自 capacity surprise（用戶行為意外觸發 fan-out 放大）。Discord engineering blog 揭露多次 scaling 事故。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Long-lived WebSocket：與短連線 HTTP 服務的故障模式差異&lt;/li>
&lt;li>Fan-out 放大：單一訊息推播到大量連線的容量風險&lt;/li>
&lt;li>Sharding 與 cluster topology：超大型 guild 的特殊處理&lt;/li>
&lt;li>Gradual rollout 限制：長連線服務的 deploy 節奏&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2023&lt;/td>
 &lt;td>Authentication outage&lt;/td>
 &lt;td>capacity surprise、reconnection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2026&lt;/td>
 &lt;td>Voice outage&lt;/td>
 &lt;td>session state 規模化的失敗模式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Discord 這個案例在講的是長連線與 session state 一旦失衡，事故就會直接反映在使用者連線體感上。讀者先看懂 Gateway、authentication 與 voice 這些路由的責任，再把 reconnection storm 視為核心風險。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 gateway 或 session 基礎設施出現問題時，復原順序必須同時照顧連線穩定與服務容量。當流量重新接回來時，先保住重連與驗證，再處理後續聊天與 voice 路徑，能減少二次抖動。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否看出問題在連線層還是 session state&lt;/li>
&lt;li>能否把 capacity surprise 轉成可預測的壓力模型&lt;/li>
&lt;li>能否讓 reconnection path 比一般流量更早恢復&lt;/li>
&lt;li>能否把 gateway 事故寫成客戶體感可理解的時間線&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Discord 和 Slack 是兩種不同的長連線通訊平台，但都會遇到 reconnection 與 status communication 問題。它也可和 Heroku 一起讀，因為多租戶入口與 session state 一旦不穩，故障就會直接表現在使用者連線上。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2023 authentication outage 是連線層與驗證路徑失衡的樣本。&lt;/li>
&lt;li>2026 voice outage 則展示 session state 與 voice path 的恢復難度。&lt;/li>
&lt;li>reconnect storm 是長連線平台事故的常見擴散器。&lt;/li>
&lt;li>gateway 與 voice path 的分工會直接影響恢復順序。&lt;/li>
&lt;li>shard topology 會決定大型 guild 的故障擴散方式。&lt;/li>
&lt;li>long-lived WebSocket 讓 gradual rollout 的風險比短連線服務更高。&lt;/li>
&lt;li>authentication 與 voice path 分層，讓不同失效能有不同恢復路徑。&lt;/li>
&lt;li>capacity surprise 讓平時看似正常的流量，在事故時突然失控。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">DC1&lt;/a>&lt;/td>
 &lt;td>Gateway 容量事件&lt;/td>
 &lt;td>在長連線平台中控制回復造成的二次擁塞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.discord.com/developers/events/gateway">Gateway&lt;/a>：Discord Gateway 的官方文檔，補 long-lived WebSocket 語意。&lt;/li>
&lt;li>&lt;a href="https://discord.com/blog/authentication-outage">25% or 6 to 4: The 11/6/23 Authentication Outage&lt;/a>：Discord 服務中斷的技術回顧。&lt;/li>
&lt;li>&lt;a href="https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage">You’ve Got (Too Much) Mail: Behind the Scenes of the 3/25/26 Voice Outage&lt;/a>：Discord 最近的 voice outage 回顧。&lt;/li>
&lt;li>&lt;a href="https://discord.com/blog">Discord Blog&lt;/a>：Discord engineering 與 outage 類文章總入口。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Discord 是大規模長連線 gateway 的代表、事故多源自 capacity surprise（用戶行為意外觸發 fan-out 放大）。Discord engineering blog 揭露多次 scaling 事故。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Long-lived WebSocket：與短連線 HTTP 服務的故障模式差異</li>
<li>Fan-out 放大：單一訊息推播到大量連線的容量風險</li>
<li>Sharding 與 cluster topology：超大型 guild 的特殊處理</li>
<li>Gradual rollout 限制：長連線服務的 deploy 節奏</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2023</td>
          <td>Authentication outage</td>
          <td>capacity surprise、reconnection</td>
      </tr>
      <tr>
          <td>2026</td>
          <td>Voice outage</td>
          <td>session state 規模化的失敗模式</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Discord 這個案例在講的是長連線與 session state 一旦失衡，事故就會直接反映在使用者連線體感上。讀者先看懂 Gateway、authentication 與 voice 這些路由的責任，再把 reconnection storm 視為核心風險。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 gateway 或 session 基礎設施出現問題時，復原順序必須同時照顧連線穩定與服務容量。當流量重新接回來時，先保住重連與驗證，再處理後續聊天與 voice 路徑，能減少二次抖動。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否看出問題在連線層還是 session state</li>
<li>能否把 capacity surprise 轉成可預測的壓力模型</li>
<li>能否讓 reconnection path 比一般流量更早恢復</li>
<li>能否把 gateway 事故寫成客戶體感可理解的時間線</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Discord 和 Slack 是兩種不同的長連線通訊平台，但都會遇到 reconnection 與 status communication 問題。它也可和 Heroku 一起讀，因為多租戶入口與 session state 一旦不穩，故障就會直接表現在使用者連線上。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2023 authentication outage 是連線層與驗證路徑失衡的樣本。</li>
<li>2026 voice outage 則展示 session state 與 voice path 的恢復難度。</li>
<li>reconnect storm 是長連線平台事故的常見擴散器。</li>
<li>gateway 與 voice path 的分工會直接影響恢復順序。</li>
<li>shard topology 會決定大型 guild 的故障擴散方式。</li>
<li>long-lived WebSocket 讓 gradual rollout 的風險比短連線服務更高。</li>
<li>authentication 與 voice path 分層，讓不同失效能有不同恢復路徑。</li>
<li>capacity surprise 讓平時看似正常的流量，在事故時突然失控。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">DC1</a></td>
          <td>Gateway 容量事件</td>
          <td>在長連線平台中控制回復造成的二次擁塞</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://docs.discord.com/developers/events/gateway">Gateway</a>：Discord Gateway 的官方文檔，補 long-lived WebSocket 語意。</li>
<li><a href="https://discord.com/blog/authentication-outage">25% or 6 to 4: The 11/6/23 Authentication Outage</a>：Discord 服務中斷的技術回顧。</li>
<li><a href="https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage">You’ve Got (Too Much) Mail: Behind the Scenes of the 3/25/26 Voice Outage</a>：Discord 最近的 voice outage 回顧。</li>
<li><a href="https://discord.com/blog">Discord Blog</a>：Discord engineering 與 outage 類文章總入口。</li>
</ul>
]]></content:encoded></item><item><title>Microsoft / Azure SRE</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/</guid><description>&lt;p>Microsoft Azure 的 SRE 文章與 Resilience patterns 文件是大型雲端供應商的可靠性工程公開素材。教學重點在「企業導向 cloud 的可靠性 patterns 與 governance」。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Azure Well-Architected Framework：Reliability pillar 的設計指導&lt;/li>
&lt;li>Resilience patterns：retry、circuit breaker、bulkhead 的官方範例&lt;/li>
&lt;li>Site Reliability Engineering at Microsoft：內部 SRE 組織與實踐&lt;/li>
&lt;li>Compliance-driven reliability：企業客戶要求下的可靠性 SLA&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Well-Architected Framework&lt;/td>
 &lt;td>Reliability pillar 結構與審查流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resilience Design Patterns&lt;/td>
 &lt;td>retry / breaker / bulkhead 等實作範例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Azure SRE Engineering&lt;/td>
 &lt;td>Microsoft 內部 SRE 演化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Chaos Studio&lt;/td>
 &lt;td>Azure 平台原生 chaos 工具&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Microsoft 這個案例在講的是企業雲端如何把可靠性寫進架構規範與設計模式。讀者先抓 reliability pillar、self-healing 與 design patterns 的分工，再把它們視為治理語言，而不是單純的文件清單。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當服務要面對企業客戶的 SLA 要求時，先看設計模式能否對應 failure mode，再看治理流程是否能把 pattern 真的落到架構審查。當團隊需要做 retry 或 bulkhead 時，重點是能不能選到正確的位置與層級。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否從 failure mode 反推適合的 reliability pattern&lt;/li>
&lt;li>能否把 self-healing 寫成可驗證的設計要求&lt;/li>
&lt;li>能否把架構審查和 SLA 約束對齊&lt;/li>
&lt;li>能否把 Azure SRE 實踐轉成團隊可用的治理語言&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Microsoft 這頁和 Stripe、Google 的差異在於它更偏治理與設計審查，而不是單一事故。讀者若先懂這頁，再看 Azure AD 和 M365，就能把 identity 失效與企業雲端的 reliability pattern 串成同一條理解路徑。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>self-healing 把故障轉成可恢復的設計要求，而不是單靠人工補救。&lt;/li>
&lt;li>reliability pillar 讓團隊在架構審查時就對齊失效模式與補救方式。&lt;/li>
&lt;li>retry / circuit breaker / bulkhead 提供可重複使用的設計模式。&lt;/li>
&lt;li>compliance-driven reliability 把 SLA 約束寫進雲端治理。&lt;/li>
&lt;li>chaos studio 讓雲端平台本身提供測試失效的工具。&lt;/li>
&lt;li>Well-Architected Framework 讓可靠性審查變成標準流程。&lt;/li>
&lt;li>health check / retry policy 讓應用層能和平台層恢復節奏對齊。&lt;/li>
&lt;li>governance 語言把企業 SLA 與技術決策連起來。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">MS1&lt;/a>&lt;/td>
 &lt;td>變更治理與可靠性門檻&lt;/td>
 &lt;td>以風險分層與 release gate 降低系統性回歸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/" data-link-title="Microsoft：Safe Deployment Practices 與 Resilience Patterns" data-link-desc="大型 SaaS 用 ring-based deployment 控制變更擴散，用標準化 resilience patterns 讓依賴失效時的降級行為可預測。">MS2&lt;/a>&lt;/td>
 &lt;td>Safe Deployment Practices 與 Resilience Patterns&lt;/td>
 &lt;td>ring-based deployment 與標準化韌性設計模式的制度化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/">Azure Architecture Center&lt;/a>：Azure 架構中心總入口。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/well-architected/resiliency/overview">Reliability quick links&lt;/a>：Azure Well-Architected Reliability 入口。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/guide/design-principles/self-healing">Design for self-healing&lt;/a>：self-healing 與 failover 的官方設計原則。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-gb/azure/well-architected/reliability/design-patterns">Architecture design patterns that support reliability&lt;/a>：可靠性設計模式總覽。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Microsoft Azure 的 SRE 文章與 Resilience patterns 文件是大型雲端供應商的可靠性工程公開素材。教學重點在「企業導向 cloud 的可靠性 patterns 與 governance」。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Azure Well-Architected Framework：Reliability pillar 的設計指導</li>
<li>Resilience patterns：retry、circuit breaker、bulkhead 的官方範例</li>
<li>Site Reliability Engineering at Microsoft：內部 SRE 組織與實踐</li>
<li>Compliance-driven reliability：企業客戶要求下的可靠性 SLA</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Well-Architected Framework</td>
          <td>Reliability pillar 結構與審查流程</td>
      </tr>
      <tr>
          <td>Resilience Design Patterns</td>
          <td>retry / breaker / bulkhead 等實作範例</td>
      </tr>
      <tr>
          <td>Azure SRE Engineering</td>
          <td>Microsoft 內部 SRE 演化</td>
      </tr>
      <tr>
          <td>Chaos Studio</td>
          <td>Azure 平台原生 chaos 工具</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Microsoft 這個案例在講的是企業雲端如何把可靠性寫進架構規範與設計模式。讀者先抓 reliability pillar、self-healing 與 design patterns 的分工，再把它們視為治理語言，而不是單純的文件清單。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當服務要面對企業客戶的 SLA 要求時，先看設計模式能否對應 failure mode，再看治理流程是否能把 pattern 真的落到架構審查。當團隊需要做 retry 或 bulkhead 時，重點是能不能選到正確的位置與層級。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否從 failure mode 反推適合的 reliability pattern</li>
<li>能否把 self-healing 寫成可驗證的設計要求</li>
<li>能否把架構審查和 SLA 約束對齊</li>
<li>能否把 Azure SRE 實踐轉成團隊可用的治理語言</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Microsoft 這頁和 Stripe、Google 的差異在於它更偏治理與設計審查，而不是單一事故。讀者若先懂這頁，再看 Azure AD 和 M365，就能把 identity 失效與企業雲端的 reliability pattern 串成同一條理解路徑。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>self-healing 把故障轉成可恢復的設計要求，而不是單靠人工補救。</li>
<li>reliability pillar 讓團隊在架構審查時就對齊失效模式與補救方式。</li>
<li>retry / circuit breaker / bulkhead 提供可重複使用的設計模式。</li>
<li>compliance-driven reliability 把 SLA 約束寫進雲端治理。</li>
<li>chaos studio 讓雲端平台本身提供測試失效的工具。</li>
<li>Well-Architected Framework 讓可靠性審查變成標準流程。</li>
<li>health check / retry policy 讓應用層能和平台層恢復節奏對齊。</li>
<li>governance 語言把企業 SLA 與技術決策連起來。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">MS1</a></td>
          <td>變更治理與可靠性門檻</td>
          <td>以風險分層與 release gate 降低系統性回歸</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/" data-link-title="Microsoft：Safe Deployment Practices 與 Resilience Patterns" data-link-desc="大型 SaaS 用 ring-based deployment 控制變更擴散，用標準化 resilience patterns 讓依賴失效時的降級行為可預測。">MS2</a></td>
          <td>Safe Deployment Practices 與 Resilience Patterns</td>
          <td>ring-based deployment 與標準化韌性設計模式的制度化</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/azure/architecture/">Azure Architecture Center</a>：Azure 架構中心總入口。</li>
<li><a href="https://learn.microsoft.com/en-us/azure/well-architected/resiliency/overview">Reliability quick links</a>：Azure Well-Architected Reliability 入口。</li>
<li><a href="https://learn.microsoft.com/en-us/azure/architecture/guide/design-principles/self-healing">Design for self-healing</a>：self-healing 與 failover 的官方設計原則。</li>
<li><a href="https://learn.microsoft.com/en-gb/azure/well-architected/reliability/design-patterns">Architecture design patterns that support reliability</a>：可靠性設計模式總覽。</li>
</ul>
]]></content:encoded></item><item><title>4.14 Anomaly Detection</title><link>https://tarrragon.github.io/blog/backend/04-observability/anomaly-detection/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/anomaly-detection/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Anomaly detection 跟 rule-based alert 的分工&lt;/li>
&lt;li>Baseline 模型類別&lt;/li>
&lt;li>Anomaly 訊號的處理路徑&lt;/li>
&lt;li>False positive 與 alert noise 共用預算&lt;/li>
&lt;li>Explainability：anomaly 要能定位到維度&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Anomaly detection 是用統計基線或模型找出偏離常態的訊號，責任是補上 rule-based alert 難以事先列舉的變化。&lt;/p>
&lt;p>Rule-based alert 抓已知模式 — 團隊事先定義「error rate &amp;gt; 1% 就告警」。Anomaly detection 抓未知模式 — 系統觀察到「今天的 latency 分布跟過去 30 天的同時段不同」。兩者互補：rule-based 精確但只能抓團隊已預見的問題，anomaly detection 有噪音但能發現團隊沒想到的退化。&lt;/p>
&lt;p>Anomaly 適合作為提示層（hint），通常先進 dashboard 或低 severity 路由，再由 SLO 判讀或人工確認決定是否升級。把 anomaly 直接接 page 是噪音爆量的常見原因。&lt;/p>
&lt;h2 id="跟-rule-based-alert-的分工">跟 Rule-based Alert 的分工&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Rule-based alert&lt;/th>
 &lt;th>Anomaly detection&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發條件&lt;/td>
 &lt;td>固定閾值或 burn rate&lt;/td>
 &lt;td>偏離統計基線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抓什麼&lt;/td>
 &lt;td>已知模式（團隊事先定義）&lt;/td>
 &lt;td>未知模式（歷史基線判斷）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>精確度&lt;/td>
 &lt;td>高（閾值明確）&lt;/td>
 &lt;td>低到中（統計偏差 = 候選，需要確認）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>False positive&lt;/td>
 &lt;td>閾值對齊時低&lt;/td>
 &lt;td>較高（季節性未建模、促銷、release）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合的 severity&lt;/td>
 &lt;td>Critical / Warning&lt;/td>
 &lt;td>Info / Warning（確認後才升級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護成本&lt;/td>
 &lt;td>隨服務變化需調整閾值&lt;/td>
 &lt;td>模型要持續 retrain 或校正&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最有效的整合方式：rule-based alert 處理已知的 SLO violation（symptom-based、高 severity），anomaly detection 處理趨勢異常跟 novel failure mode（低 severity、dashboard widget）。兩者共用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 的 noise budget — anomaly 的 false positive 也算進整體 noise rate。&lt;/p>
&lt;h2 id="baseline-模型類別">Baseline 模型類別&lt;/h2>
&lt;h3 id="seasonal-baseline">Seasonal baseline&lt;/h3>
&lt;p>按日夜、週末、節慶、促銷等週期建立基線。同一個指標的「正常範圍」在週一上午跟週日凌晨不同。Seasonal model 用歷史同期資料建立預期帶（expected band），偏離帶外視為 anomaly。&lt;/p>
&lt;p>Seasonal baseline 的失敗模式是週期性假設錯誤 — 業務改變後流量模式跟歷史不同（新產品上線改變了週末流量），模型用錯誤的基線判斷。需要定期驗證模型跟實際流量的吻合度。&lt;/p>
&lt;h3 id="moving-window-baseline">Moving window baseline&lt;/h3>
&lt;p>用過去 N 分鐘 / 小時的資料建立動態基線。比 seasonal model 簡單、延遲更低，但對突發變化更敏感（release 後 latency 自然變化可能觸發 anomaly）。&lt;/p>
&lt;p>Moving window 適合不需要週期性建模的指標 — 連線數、queue depth、goroutine count 等「預期穩定、突變代表問題」的指標。&lt;/p>
&lt;h3 id="ml-basedforecast--clustering">ML-based（forecast / clustering）&lt;/h3>
&lt;p>用機器學習模型做時間序列預測（Prophet、ARIMA）或高維度聚類（isolation forest、DBSCAN）。能處理複雜的多變量異常（A 指標上升 + B 指標下降 = 異常，但各自單獨看都在正常範圍）。&lt;/p>
&lt;p>ML 模型的成本是訓練、retrain、模型版本管理跟 explainability。多數團隊的起步方式是先用 seasonal + moving window（不需要 ML pipeline），等 false positive 管理穩定後再引入 ML。&lt;/p>
&lt;h2 id="anomaly-訊號的處理路徑">Anomaly 訊號的處理路徑&lt;/h2>
&lt;p>Anomaly detection 的輸出是「這個指標在這段時間偏離基線」— 候選訊號，不是確認的問題。處理路徑決定 anomaly 是有用的提示還是噪音來源。&lt;/p>
&lt;p>&lt;strong>Dashboard widget&lt;/strong>：anomaly 標記在 time series panel 上（標色、annotation），讓巡視 dashboard 的工程師注意到。低成本、零噪音（不通知任何人）、但需要有人主動看。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Anomaly detection 跟 rule-based alert 的分工</li>
<li>Baseline 模型類別</li>
<li>Anomaly 訊號的處理路徑</li>
<li>False positive 與 alert noise 共用預算</li>
<li>Explainability：anomaly 要能定位到維度</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Anomaly detection 是用統計基線或模型找出偏離常態的訊號，責任是補上 rule-based alert 難以事先列舉的變化。</p>
<p>Rule-based alert 抓已知模式 — 團隊事先定義「error rate &gt; 1% 就告警」。Anomaly detection 抓未知模式 — 系統觀察到「今天的 latency 分布跟過去 30 天的同時段不同」。兩者互補：rule-based 精確但只能抓團隊已預見的問題，anomaly detection 有噪音但能發現團隊沒想到的退化。</p>
<p>Anomaly 適合作為提示層（hint），通常先進 dashboard 或低 severity 路由，再由 SLO 判讀或人工確認決定是否升級。把 anomaly 直接接 page 是噪音爆量的常見原因。</p>
<h2 id="跟-rule-based-alert-的分工">跟 Rule-based Alert 的分工</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Rule-based alert</th>
          <th>Anomaly detection</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發條件</td>
          <td>固定閾值或 burn rate</td>
          <td>偏離統計基線</td>
      </tr>
      <tr>
          <td>抓什麼</td>
          <td>已知模式（團隊事先定義）</td>
          <td>未知模式（歷史基線判斷）</td>
      </tr>
      <tr>
          <td>精確度</td>
          <td>高（閾值明確）</td>
          <td>低到中（統計偏差 = 候選，需要確認）</td>
      </tr>
      <tr>
          <td>False positive</td>
          <td>閾值對齊時低</td>
          <td>較高（季節性未建模、促銷、release）</td>
      </tr>
      <tr>
          <td>適合的 severity</td>
          <td>Critical / Warning</td>
          <td>Info / Warning（確認後才升級）</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>隨服務變化需調整閾值</td>
          <td>模型要持續 retrain 或校正</td>
      </tr>
  </tbody>
</table>
<p>最有效的整合方式：rule-based alert 處理已知的 SLO violation（symptom-based、高 severity），anomaly detection 處理趨勢異常跟 novel failure mode（低 severity、dashboard widget）。兩者共用 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的 noise budget — anomaly 的 false positive 也算進整體 noise rate。</p>
<h2 id="baseline-模型類別">Baseline 模型類別</h2>
<h3 id="seasonal-baseline">Seasonal baseline</h3>
<p>按日夜、週末、節慶、促銷等週期建立基線。同一個指標的「正常範圍」在週一上午跟週日凌晨不同。Seasonal model 用歷史同期資料建立預期帶（expected band），偏離帶外視為 anomaly。</p>
<p>Seasonal baseline 的失敗模式是週期性假設錯誤 — 業務改變後流量模式跟歷史不同（新產品上線改變了週末流量），模型用錯誤的基線判斷。需要定期驗證模型跟實際流量的吻合度。</p>
<h3 id="moving-window-baseline">Moving window baseline</h3>
<p>用過去 N 分鐘 / 小時的資料建立動態基線。比 seasonal model 簡單、延遲更低，但對突發變化更敏感（release 後 latency 自然變化可能觸發 anomaly）。</p>
<p>Moving window 適合不需要週期性建模的指標 — 連線數、queue depth、goroutine count 等「預期穩定、突變代表問題」的指標。</p>
<h3 id="ml-basedforecast--clustering">ML-based（forecast / clustering）</h3>
<p>用機器學習模型做時間序列預測（Prophet、ARIMA）或高維度聚類（isolation forest、DBSCAN）。能處理複雜的多變量異常（A 指標上升 + B 指標下降 = 異常，但各自單獨看都在正常範圍）。</p>
<p>ML 模型的成本是訓練、retrain、模型版本管理跟 explainability。多數團隊的起步方式是先用 seasonal + moving window（不需要 ML pipeline），等 false positive 管理穩定後再引入 ML。</p>
<h2 id="anomaly-訊號的處理路徑">Anomaly 訊號的處理路徑</h2>
<p>Anomaly detection 的輸出是「這個指標在這段時間偏離基線」— 候選訊號，不是確認的問題。處理路徑決定 anomaly 是有用的提示還是噪音來源。</p>
<p><strong>Dashboard widget</strong>：anomaly 標記在 time series panel 上（標色、annotation），讓巡視 dashboard 的工程師注意到。低成本、零噪音（不通知任何人）、但需要有人主動看。</p>
<p><strong>Low severity alert（info / warning）</strong>：anomaly 進入 alerting pipeline，但 severity 設為 info 或 warning。不 page on-call、但記錄在 alert history 中。事故發生後可以回溯「事故前有沒有 anomaly 提早預警」。</p>
<p><strong>Conditional escalation</strong>：anomaly 搭配 rule-based 條件升級。「Latency 偏離基線 + error rate 超過 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>」→ 升級為 critical。單獨的 anomaly 不足以 page，但跟其他訊號組合時有判讀價值。</p>
<h2 id="explainability">Explainability</h2>
<p>Anomaly 觸發時，工程師需要回答「為什麼異常」 — 是哪個服務、哪個 endpoint、哪個 tenant、哪個地區導致的。只告訴你「overall latency 異常」但不說維度，診斷價值有限。</p>
<p>可操作的 explainability 有兩層：</p>
<p><strong>維度歸因</strong>：anomaly detection 系統自動拆分異常到子維度 — 「overall latency 異常，主要來自 region=us-east + endpoint=/api/search」。Datadog Watchdog 跟 New Relic AI 提供這種維度下鑽能力。</p>
<p><strong>Root cause hint</strong>：anomaly 跟其他訊號（deploy event、config change、dependency error spike）的時間關聯。「Latency anomaly 開始的時間跟 v2.3.1 deploy 吻合」— 提示 root cause 可能跟 deploy 有關。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>定位</th>
          <th>特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog Watchdog</td>
          <td>託管 anomaly + 維度歸因</td>
          <td>跟 APM / log / metric 整合、auto-detect</td>
      </tr>
      <tr>
          <td>New Relic AI</td>
          <td>託管 anomaly + root cause suggest</td>
          <td>全棧觀測整合</td>
      </tr>
      <tr>
          <td>Prophet（自建）</td>
          <td>開源 time series forecast</td>
          <td>需要自建 pipeline、training、serving</td>
      </tr>
      <tr>
          <td>Anomalo</td>
          <td>資料品質 anomaly</td>
          <td>偏 data pipeline、非 infra 觀測</td>
      </tr>
  </tbody>
</table>
<p>自建 vs 託管的判準：團隊是否有 ML pipeline 維運能力。託管方案的好處是零 ML 運維、跟觀測平台深度整合；自建的好處是可控性高、可以針對業務邏輯客製模型。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Anomaly detection 最常見的失敗是 baseline 沒對齊流量週期（週末自然下降被判成異常）跟異常觸發後無法歸因到具體維度（只知道「latency 異常」但看不出是哪個 service、哪個 region）。</p>
<p>重點訊號包括：</p>
<ul>
<li>Baseline 是否理解日夜、週末、節慶與促銷週期</li>
<li>Anomaly 是否能指出 service、tenant、region 或 endpoint 維度</li>
<li>False positive 是否納入 alert noise governance</li>
<li>Anomaly 與 rule-based alert 是否有清楚分工</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 規則寫到數百條、仍漏掉 novel failure mode</li>
<li>已知 anomaly 訊號被忽略、靠人工巡視 dashboard</li>
<li>Anomaly 觸發後無人能解釋「為什麼異常」</li>
<li>模型未對齊週期性（週末 / 節慶 / promo）造成噪音</li>
<li>同一指標 anomaly + rule alert 重複觸發、無協調</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anomaly 直接接 page</td>
          <td>On-call 被統計偏差淹沒</td>
          <td>Anomaly 先走 info/warning、conditional 才升級</td>
      </tr>
      <tr>
          <td>Baseline 沒對齊季節性</td>
          <td>週末 / 節慶流量自然變化觸發 false positive</td>
          <td>用 seasonal model 或 exclude 已知事件窗口</td>
      </tr>
      <tr>
          <td>Anomaly 跟 rule alert 重複</td>
          <td>同一問題兩個來源觸發、noise 翻倍</td>
          <td>共用 noise budget、anomaly 在 rule 已觸發時抑制</td>
      </tr>
      <tr>
          <td>模型不可解釋</td>
          <td>Anomaly fired 但工程師不知道看什麼</td>
          <td>要求維度歸因能力、否則只作 dashboard widget</td>
      </tr>
      <tr>
          <td>自建 ML 但無 retrain pipeline</td>
          <td>模型用半年前的 baseline、precision 持續下降</td>
          <td>建立定期 retrain 或改用託管方案</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：anomaly 升級 alert 的條件</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：跟 SLO burn rate 的訊號分工</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance</a>：anomaly false positive 的淘汰</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：anomaly 系統的 ownership</li>
</ul>
]]></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>Firestore document 反正規化與一致性維護：fan-out write、副本同步與資料修復</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/denormalization-fanout-consistency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/denormalization-fanout-consistency/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore&lt;/a> overview 的 deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境改一個使用者名稱要改一千筆">問題情境：改一個使用者名稱要改一千筆&lt;/h2>
&lt;p>一個社群 app 的貼文列表要顯示作者頭像與名稱。關聯式思路是貼文存 &lt;code>authorId&lt;/code>、查詢時 JOIN &lt;code>users&lt;/code> 表。但 Firestore 沒有 JOIN——要嘛 client 每顯示一則貼文就多查一次 &lt;code>users&lt;/code>（列表 20 則就 20 次額外讀取），要嘛在貼文 document 裡直接存一份 &lt;code>authorName&lt;/code> 與 &lt;code>authorAvatar&lt;/code> 副本。為了讀取效率，多數人選後者。&lt;/p>
&lt;p>副本一上線就埋了一致性債：使用者改了名稱，他過去發的一千則貼文裡的 &lt;code>authorName&lt;/code> 還是舊的。改名這個動作從「更新一筆 &lt;code>users&lt;/code> document」變成「更新一千筆貼文 document」。這篇處理 Firestore 反正規化的建模決策、如何用 fan-out write 維護副本一致、以及這套手段撐不住時的退場。&lt;/p>
&lt;h2 id="核心概念反正規化是查詢邊界逼出來的">核心概念：反正規化是查詢邊界逼出來的&lt;/h2>
&lt;p>關聯式資料庫預設正規化，靠 JOIN 在查詢時組合資料；Firestore 沒有 server 端 JOIN，組合資料只有兩條路：client 多次查詢自己組，或寫入時就把要一起讀的資料存在一起。後者就是反正規化——它不是 Firestore 的「壞習慣」，是 client 直連 + 無 JOIN 的查詢模型逼出來的必然建模。&lt;/p>
&lt;p>反正規化的判斷單位是 access pattern，不是資料的「正規與否」。問題不是「該不該複製」，而是「這份資料在哪些讀取路徑上要被一起讀到，複製它的一致性維護成本，比每次多查一次划不划算」。判斷有三個輸入：&lt;/p>
&lt;p>&lt;strong>讀寫比&lt;/strong>。讀多寫少的資料適合反正規化——複製成本攤在少數寫入上、省下大量讀取的額外查詢。作者名稱顯示在每則貼文（高讀），但改名很少（低寫），複製划算。反過來，高頻變動的資料複製多份，每次變動要 fan-out 到所有副本，成本可能超過省下的讀取。&lt;/p>
&lt;p>&lt;strong>副本數量的可預測性&lt;/strong>。複製到「一個 user 的 profile 摘要」這種固定副本可控；複製到「該 user 的所有貼文」這種隨資料成長無上限的副本，fan-out 的寫入量會隨規模膨脹，要特別評估。&lt;/p>
&lt;p>&lt;strong>一致性容忍度&lt;/strong>。副本短暫不一致（改名後幾秒內舊貼文還顯示舊名）能不能接受。能容忍最終一致的，反正規化的維護可以非同步、用 Cloud Function 慢慢 fan-out；不能容忍的，要嘛同步 fan-out（貴且有規模上限），要嘛這份資料根本不該複製。&lt;/p>
&lt;h2 id="配置fan-out-write-維護副本一致">配置：fan-out write 維護副本一致&lt;/h2>
&lt;p>fan-out write 是「一次邏輯更新，寫多個 document」。Firestore 的 &lt;code>writeBatch&lt;/code> 讓多個寫入 atomic 提交（最多 500 個操作一批），是固定且可控副本數的標準手段：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">writeBatch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">doc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">collection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">query&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">where&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">getDocs&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;firebase/firestore&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">// 改名：更新 users/{uid} + fan-out 到該 user 的所有貼文副本
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">renameUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uid&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">newName&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 1. 更新權威來源
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">userRef&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">doc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;users&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uid&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 2. 查出所有要同步的副本
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">postsSnap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getDocs&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;posts&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">where&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;authorId&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;==&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uid&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="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="c1">// 3. batch 提交（超過 500 要分批）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">ops&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nx">ref&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">userRef&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">displayName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">newName&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">postsSnap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">ops&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">ref&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">authorName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">newName&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">ops&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">writeBatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">ops&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">op&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">op&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">op&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&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="kr">await&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">commit&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 class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這裡的關鍵取捨是同步 fan-out 與非同步 fan-out。上面的同步版本在使用者點「儲存」時就把一千筆貼文改完，使用者等待時間隨副本數成長、且超過 500 要分批多次提交，副本數無上限時會撞到不可接受的延遲。非同步版本把權威來源（&lt;code>users/{uid}&lt;/code>）同步更新，副本同步丟給 Cloud Function 在背景慢慢做：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// Cloud Function：onUpdate users document 時 fan-out 到副本
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">exports&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fanoutUserName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">functions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firestore&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;users/{uid}&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">onUpdate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">change&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">before&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">change&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">before&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">after&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">change&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">after&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">before&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">displayName&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">after&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">displayName&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 名稱沒變不做
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">uid&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uid&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">postsSnap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">admin&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firestore&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;posts&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">where&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;authorId&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;==&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">uid&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">get&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="c1">// 分批 fan-out，背景執行、使用者不等待
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">docs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">postsSnap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">docs&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">docs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">admin&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firestore&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">batch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">docs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">authorName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">after&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">displayName&lt;/span> &lt;span class="p">}));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">commit&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>非同步 fan-out 把「使用者體驗的即時性」與「副本的最終一致」分開：權威來源立刻更新、副本最終收斂。代價是中間有一段不一致窗口（改名後到 fan-out 完成前，舊貼文顯示舊名），這對社群 app 的顯示名稱通常可接受。&lt;code>writeBatch&lt;/code> 與 &lt;code>transaction&lt;/code> 的選擇在這裡也要分清：fan-out 是「寫多個獨立 document、不依賴彼此既有值」用 &lt;code>writeBatch&lt;/code>；若更新要依賴讀到的當前值（例如同時扣 A 加 B 且要看當前餘額）才用 &lt;code>transaction&lt;/code>，但 transaction 在大量 document 的 fan-out 上不適用。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore</a> overview 的 deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a>。</p></blockquote>
<h2 id="問題情境改一個使用者名稱要改一千筆">問題情境：改一個使用者名稱要改一千筆</h2>
<p>一個社群 app 的貼文列表要顯示作者頭像與名稱。關聯式思路是貼文存 <code>authorId</code>、查詢時 JOIN <code>users</code> 表。但 Firestore 沒有 JOIN——要嘛 client 每顯示一則貼文就多查一次 <code>users</code>（列表 20 則就 20 次額外讀取），要嘛在貼文 document 裡直接存一份 <code>authorName</code> 與 <code>authorAvatar</code> 副本。為了讀取效率，多數人選後者。</p>
<p>副本一上線就埋了一致性債：使用者改了名稱，他過去發的一千則貼文裡的 <code>authorName</code> 還是舊的。改名這個動作從「更新一筆 <code>users</code> document」變成「更新一千筆貼文 document」。這篇處理 Firestore 反正規化的建模決策、如何用 fan-out write 維護副本一致、以及這套手段撐不住時的退場。</p>
<h2 id="核心概念反正規化是查詢邊界逼出來的">核心概念：反正規化是查詢邊界逼出來的</h2>
<p>關聯式資料庫預設正規化，靠 JOIN 在查詢時組合資料；Firestore 沒有 server 端 JOIN，組合資料只有兩條路：client 多次查詢自己組，或寫入時就把要一起讀的資料存在一起。後者就是反正規化——它不是 Firestore 的「壞習慣」，是 client 直連 + 無 JOIN 的查詢模型逼出來的必然建模。</p>
<p>反正規化的判斷單位是 access pattern，不是資料的「正規與否」。問題不是「該不該複製」，而是「這份資料在哪些讀取路徑上要被一起讀到，複製它的一致性維護成本，比每次多查一次划不划算」。判斷有三個輸入：</p>
<p><strong>讀寫比</strong>。讀多寫少的資料適合反正規化——複製成本攤在少數寫入上、省下大量讀取的額外查詢。作者名稱顯示在每則貼文（高讀），但改名很少（低寫），複製划算。反過來，高頻變動的資料複製多份，每次變動要 fan-out 到所有副本，成本可能超過省下的讀取。</p>
<p><strong>副本數量的可預測性</strong>。複製到「一個 user 的 profile 摘要」這種固定副本可控；複製到「該 user 的所有貼文」這種隨資料成長無上限的副本，fan-out 的寫入量會隨規模膨脹，要特別評估。</p>
<p><strong>一致性容忍度</strong>。副本短暫不一致（改名後幾秒內舊貼文還顯示舊名）能不能接受。能容忍最終一致的，反正規化的維護可以非同步、用 Cloud Function 慢慢 fan-out；不能容忍的，要嘛同步 fan-out（貴且有規模上限），要嘛這份資料根本不該複製。</p>
<h2 id="配置fan-out-write-維護副本一致">配置：fan-out write 維護副本一致</h2>
<p>fan-out write 是「一次邏輯更新，寫多個 document」。Firestore 的 <code>writeBatch</code> 讓多個寫入 atomic 提交（最多 500 個操作一批），是固定且可控副本數的標準手段：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">writeBatch</span><span class="p">,</span> <span class="nx">doc</span><span class="p">,</span> <span class="nx">collection</span><span class="p">,</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">where</span><span class="p">,</span> <span class="nx">getDocs</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;firebase/firestore&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// 改名：更新 users/{uid} + fan-out 到該 user 的所有貼文副本
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="kr">async</span> <span class="kd">function</span> <span class="nx">renameUser</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">uid</span><span class="p">,</span> <span class="nx">newName</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// 1. 更新權威來源
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">userRef</span> <span class="o">=</span> <span class="nx">doc</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="s1">&#39;users&#39;</span><span class="p">,</span> <span class="nx">uid</span><span class="p">);</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. 查出所有要同步的副本
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">postsSnap</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getDocs</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">query</span><span class="p">(</span><span class="nx">collection</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="s1">&#39;posts&#39;</span><span class="p">),</span> <span class="nx">where</span><span class="p">(</span><span class="s1">&#39;authorId&#39;</span><span class="p">,</span> <span class="s1">&#39;==&#39;</span><span class="p">,</span> <span class="nx">uid</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <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="c1">// 3. batch 提交（超過 500 要分批）
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">ops</span> <span class="o">=</span> <span class="p">[{</span> <span class="nx">ref</span><span class="o">:</span> <span class="nx">userRef</span><span class="p">,</span> <span class="nx">data</span><span class="o">:</span> <span class="p">{</span> <span class="nx">displayName</span><span class="o">:</span> <span class="nx">newName</span> <span class="p">}</span> <span class="p">}];</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="nx">postsSnap</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">ops</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span> <span class="nx">ref</span><span class="o">:</span> <span class="nx">p</span><span class="p">.</span><span class="nx">ref</span><span class="p">,</span> <span class="nx">data</span><span class="o">:</span> <span class="p">{</span> <span class="nx">authorName</span><span class="o">:</span> <span class="nx">newName</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">ops</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">500</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="nx">writeBatch</span><span class="p">(</span><span class="nx">db</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nx">ops</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">i</span> <span class="o">+</span> <span class="mi">500</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">op</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">op</span><span class="p">.</span><span class="nx">ref</span><span class="p">,</span> <span class="nx">op</span><span class="p">.</span><span class="nx">data</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="kr">await</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">commit</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡的關鍵取捨是同步 fan-out 與非同步 fan-out。上面的同步版本在使用者點「儲存」時就把一千筆貼文改完，使用者等待時間隨副本數成長、且超過 500 要分批多次提交，副本數無上限時會撞到不可接受的延遲。非同步版本把權威來源（<code>users/{uid}</code>）同步更新，副本同步丟給 Cloud Function 在背景慢慢做：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// Cloud Function：onUpdate users document 時 fan-out 到副本
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">exports</span><span class="p">.</span><span class="nx">fanoutUserName</span> <span class="o">=</span> <span class="nx">functions</span><span class="p">.</span><span class="nx">firestore</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">.</span><span class="nb">document</span><span class="p">(</span><span class="s1">&#39;users/{uid}&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">.</span><span class="nx">onUpdate</span><span class="p">(</span><span class="kr">async</span> <span class="p">(</span><span class="nx">change</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kr">const</span> <span class="nx">before</span> <span class="o">=</span> <span class="nx">change</span><span class="p">.</span><span class="nx">before</span><span class="p">.</span><span class="nx">data</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kr">const</span> <span class="nx">after</span> <span class="o">=</span> <span class="nx">change</span><span class="p">.</span><span class="nx">after</span><span class="p">.</span><span class="nx">data</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">before</span><span class="p">.</span><span class="nx">displayName</span> <span class="o">===</span> <span class="nx">after</span><span class="p">.</span><span class="nx">displayName</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> <span class="c1">// 名稱沒變不做
</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 class="kr">const</span> <span class="nx">uid</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">uid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kr">const</span> <span class="nx">postsSnap</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">admin</span><span class="p">.</span><span class="nx">firestore</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="p">.</span><span class="nx">collection</span><span class="p">(</span><span class="s1">&#39;posts&#39;</span><span class="p">).</span><span class="nx">where</span><span class="p">(</span><span class="s1">&#39;authorId&#39;</span><span class="p">,</span> <span class="s1">&#39;==&#39;</span><span class="p">,</span> <span class="nx">uid</span><span class="p">).</span><span class="nx">get</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="c1">// 分批 fan-out，背景執行、使用者不等待
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">docs</span> <span class="o">=</span> <span class="nx">postsSnap</span><span class="p">.</span><span class="nx">docs</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">docs</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">500</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="nx">admin</span><span class="p">.</span><span class="nx">firestore</span><span class="p">().</span><span class="nx">batch</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="nx">docs</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">i</span> <span class="o">+</span> <span class="mi">500</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">d</span><span class="p">)</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">batch</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">ref</span><span class="p">,</span> <span class="p">{</span> <span class="nx">authorName</span><span class="o">:</span> <span class="nx">after</span><span class="p">.</span><span class="nx">displayName</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="kr">await</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">commit</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">});</span></span></span></code></pre></div><p>非同步 fan-out 把「使用者體驗的即時性」與「副本的最終一致」分開：權威來源立刻更新、副本最終收斂。代價是中間有一段不一致窗口（改名後到 fan-out 完成前，舊貼文顯示舊名），這對社群 app 的顯示名稱通常可接受。<code>writeBatch</code> 與 <code>transaction</code> 的選擇在這裡也要分清：fan-out 是「寫多個獨立 document、不依賴彼此既有值」用 <code>writeBatch</code>；若更新要依賴讀到的當前值（例如同時扣 A 加 B 且要看當前餘額）才用 <code>transaction</code>，但 transaction 在大量 document 的 fan-out 上不適用。</p>
<h2 id="故障演練五個副本不一致的-production-踩坑">故障演練：五個副本不一致的 production 踩坑</h2>
<h4 id="case-1複製了卻沒建-fan-out-路徑">Case 1：複製了卻沒建 fan-out 路徑</h4>
<p>貼文存了 <code>authorName</code> 副本，但改名邏輯只更新 <code>users</code>，沒人寫 fan-out。副本永遠停在建立時的值。修法：反正規化的建模決策必須連同「誰負責同步副本」一起定，複製一份資料就要有對應的 fan-out write 路徑，沒有 fan-out 的副本是一致性債。</p>
<h4 id="case-2同步-fan-out-撞到副本數上限">Case 2：同步 fan-out 撞到副本數上限</h4>
<p>改名時同步更新所有貼文，某個高產出使用者有幾萬則貼文，提交分成幾十批、使用者等了半分鐘還在轉圈、甚至 timeout。修法：副本數無上限的 fan-out 改非同步（Cloud Function 背景做），同步 fan-out 只用在副本數固定且小的場景。</p>
<h4 id="case-3fan-out-中途失敗留下部分更新">Case 3：fan-out 中途失敗留下部分更新</h4>
<p>非同步 fan-out 跑到一半 function 掛了，前 500 筆改了、後面沒改，副本處於半新半舊。修法：fan-out function 要可重入（重跑能補完未完成的），或記錄 fan-out 進度；殘留的不一致由對帳流程掃出修復（對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>）。</p>
<h4 id="case-4雙向反正規化造成更新環">Case 4：雙向反正規化造成更新環</h4>
<p>A 存 B 的副本、B 也存 A 的副本，改 A 觸發 fan-out 改 B、又觸發 fan-out 改回 A，function 互相觸發成環。修法：反正規化要有明確的權威方向（誰是 source of truth、誰是副本），副本不反向觸發權威來源的更新。</p>
<h4 id="case-5把副本當權威來源讀來做判斷">Case 5：把副本當權威來源讀來做判斷</h4>
<p>拿貼文裡的 <code>authorName</code> 副本去做權限或業務判斷，而非讀 <code>users</code> 權威來源。副本在不一致窗口內是舊值，判斷出錯。修法：副本只供顯示，任何需要正確性的判斷讀權威來源；明確標示哪個 document 是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、哪些是顯示副本。</p>
<h2 id="容量與觀測fan-out-寫入量與不一致窗口">容量與觀測：fan-out 寫入量與不一致窗口</h2>
<p>反正規化的容量帳要算 fan-out 的寫入放大。一次邏輯更新放大成 N 次寫入，N 是副本數，這 N 次寫入計入計費。高頻變動 + 高副本數的組合會讓寫入成本失控——這正是判斷「該不該反正規化」的成本面：省下的讀取 vs 放大的寫入。</p>
<p>不一致窗口是要監控的健康指標：權威來源更新到所有副本收斂的延遲。非同步 fan-out 下這個窗口隨副本數與 function 吞吐變動，異常拉長是 fan-out 積壓的徵兆。觀測還要涵蓋 fan-out 失敗率與重試，接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。定期跑對帳掃描副本與權威來源的差異，是把潛在不一致從「使用者回報才知道」變成「主動發現修復」，對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a> 的可驗證、可修復、可稽核流程。</p>
<h2 id="邊界與整合反正規化複雜到該回關聯式">邊界與整合：反正規化複雜到該回關聯式</h2>
<p>反正規化適合「讀多寫少、副本數可控、能容忍最終一致」的顯示資料。它撐不住的訊號是複製關係長成一張難以追蹤的網——資料被複製到十幾個地方、fan-out 路徑互相依賴、改一個欄位要同步的副本沒人說得清、對帳越來越頻繁。撞到這些訊號時，方向不是把 fan-out 寫得更巧：</p>
<ul>
<li><strong>關聯查詢成為主導需求</strong>：當資料的核心價值在「任意關聯與聚合」（報表、跨實體分析），反正規化是在用副本模擬 JOIN，成本與複雜度都不划算。這是 <a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a> 的報表牆——relational 的 JOIN 在查詢時組合，省掉整套副本維護</li>
<li><strong>副本維護成本超過查詢省下的成本</strong>：高頻變動的資料反正規化，fan-out 放大的寫入成本超過正規化後多查一次的成本，反正規化的前提就不成立</li>
<li><strong>巢狀結構保留比拆表更省</strong>：相反方向——有些一起讀寫、不需獨立查詢的關聯資料，在 Firestore 用巢狀 map / array 保留在同一 document 反而比拆 collection 簡單，遷到 relational 時用 <a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">PostgreSQL JSONB</a> 保留，不是所有東西都要拆成正規表</li>
</ul>
<p>判讀的起點永遠是 access pattern 與讀寫比，不是「正規化是對的、反正規化是妥協」這種預設立場。在 Firestore 裡反正規化是正解，問題只在它的維護成本何時翻轉。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上層：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a>（資料形狀與查詢邊界）</li>
<li>資料修復：<a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>（副本不一致的對帳與修復）</li>
<li>狀態歸屬：<a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary</a>（權威來源與派生副本的分辨）</li>
<li>遷移 driver：<a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a>（報表牆與反正規化還原）</li>
<li>官方：<a href="https://firebase.google.com/docs/firestore/data-model">Firestore data model</a>、<a href="https://firebase.google.com/docs/firestore/manage-data/transactions">Batched writes</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka Schema Registry 與 schema 演進：wire format、compatibility level 與安全演進規則</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/schema-registry-evolution/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/schema-registry-evolution/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka&lt;/a> overview「KRaft 與 Schema Registry」段的 implementation-layer deep article。Overview 已交代 Schema Registry 在事件總線中的定位；本文聚焦 &lt;em>怎麼設 compatibility、wire format 長什麼樣、schema 怎麼安全演進、演進設錯會打掛什麼&lt;/em>。對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Event Schema Compatibility&lt;/a> 知識卡的 implementation 展開。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼事件總線需要一個獨立的-schema-治理元件">為什麼事件總線需要一個獨立的 schema 治理元件&lt;/h2>
&lt;p>Schema Registry 是把「event 的結構契約」從 producer 與 consumer 的程式碼裡抽出來、集中存放並強制版本相容性的元件。它承擔的責任是讓不同 service、不同部署節奏的 producer 與 consumer 在 schema 改版時仍能互通，而不需要全體同時上線。Kafka broker 本身只存 bytes、不理解 payload 結構；一旦多個團隊往同一個 topic 寫事件、又各自獨立發版，schema 漂移就會在 consumer 端炸開。&lt;/p>
&lt;p>這個責任在單一 service 內部不存在。一個 service 自己 produce、自己 consume，schema 改版同一個 deploy 就同步了，序列化用什麼格式都行。Schema Registry 解的是 &lt;em>跨 service、跨團隊、跨部署時間&lt;/em> 的契約問題：A 團隊升級了訂單事件加一個欄位，B 團隊的對帳服務還跑舊版 consumer，C 團隊的風控服務跑更舊版——三方不同步演進，靠的就是 registry 在 producer 註冊新 schema 時先擋下破壞相容性的改動。&lt;/p>
&lt;p>Yelp 的 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">Schematizer 案例&lt;/a> 把這個責任拉到極端：一天數十億訊息、數百個 service、數千個 schema，自建 registry 強制所有 message 走 Avro、訊息只帶 schema ID。它揭露 schema 治理是 data pipeline 的核心責任、不是 add-on——當規模到了數千 schema，沒有集中強制的相容性檢查，跨服務事件契約會在某次發版後悄悄斷掉，而 broker 不會報任何錯。&lt;/p>
&lt;p>Confluent Schema Registry 是業界事實標準的實作；Apicurio 是 CNCF 生態的開源替代，額外支援 OpenAPI / AsyncAPI artifact、且提供 Confluent-compatible API endpoint，遷移成本低。兩者都把 schema 存進一個 Kafka topic（Confluent 用 &lt;code>_schemas&lt;/code>，single-partition、compacted），registry 自己是無狀態的，掛掉重啟後從該 topic rebuild。&lt;/p>
&lt;h2 id="schema-id-嵌進訊息的-wire-format">Schema ID 嵌進訊息的 wire format&lt;/h2>
&lt;p>Confluent wire format 在每筆訊息的 value（或 key）前面加 5 個 byte：1 個 magic byte（固定 &lt;code>0x00&lt;/code>）加 4 個 big-endian byte 的 schema ID，後面才接序列化後的 payload。Consumer 拿到訊息先讀這 5 個 byte，用 schema ID 去 registry 查對應 schema，再用該 schema 反序列化。這是「訊息只帶 schema ID、不帶 schema 本體」的機制——schema 本體只在 registry 存一份，訊息裡放的是指標。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a> overview「KRaft 與 Schema Registry」段的 implementation-layer deep article。Overview 已交代 Schema Registry 在事件總線中的定位；本文聚焦 <em>怎麼設 compatibility、wire format 長什麼樣、schema 怎麼安全演進、演進設錯會打掛什麼</em>。對應 <a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Event Schema Compatibility</a> 知識卡的 implementation 展開。</p></blockquote>
<h2 id="為什麼事件總線需要一個獨立的-schema-治理元件">為什麼事件總線需要一個獨立的 schema 治理元件</h2>
<p>Schema Registry 是把「event 的結構契約」從 producer 與 consumer 的程式碼裡抽出來、集中存放並強制版本相容性的元件。它承擔的責任是讓不同 service、不同部署節奏的 producer 與 consumer 在 schema 改版時仍能互通，而不需要全體同時上線。Kafka broker 本身只存 bytes、不理解 payload 結構；一旦多個團隊往同一個 topic 寫事件、又各自獨立發版，schema 漂移就會在 consumer 端炸開。</p>
<p>這個責任在單一 service 內部不存在。一個 service 自己 produce、自己 consume，schema 改版同一個 deploy 就同步了，序列化用什麼格式都行。Schema Registry 解的是 <em>跨 service、跨團隊、跨部署時間</em> 的契約問題：A 團隊升級了訂單事件加一個欄位，B 團隊的對帳服務還跑舊版 consumer，C 團隊的風控服務跑更舊版——三方不同步演進，靠的就是 registry 在 producer 註冊新 schema 時先擋下破壞相容性的改動。</p>
<p>Yelp 的 <a href="/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">Schematizer 案例</a> 把這個責任拉到極端：一天數十億訊息、數百個 service、數千個 schema，自建 registry 強制所有 message 走 Avro、訊息只帶 schema ID。它揭露 schema 治理是 data pipeline 的核心責任、不是 add-on——當規模到了數千 schema，沒有集中強制的相容性檢查，跨服務事件契約會在某次發版後悄悄斷掉，而 broker 不會報任何錯。</p>
<p>Confluent Schema Registry 是業界事實標準的實作；Apicurio 是 CNCF 生態的開源替代，額外支援 OpenAPI / AsyncAPI artifact、且提供 Confluent-compatible API endpoint，遷移成本低。兩者都把 schema 存進一個 Kafka topic（Confluent 用 <code>_schemas</code>，single-partition、compacted），registry 自己是無狀態的，掛掉重啟後從該 topic rebuild。</p>
<h2 id="schema-id-嵌進訊息的-wire-format">Schema ID 嵌進訊息的 wire format</h2>
<p>Confluent wire format 在每筆訊息的 value（或 key）前面加 5 個 byte：1 個 magic byte（固定 <code>0x00</code>）加 4 個 big-endian byte 的 schema ID，後面才接序列化後的 payload。Consumer 拿到訊息先讀這 5 個 byte，用 schema ID 去 registry 查對應 schema，再用該 schema 反序列化。這是「訊息只帶 schema ID、不帶 schema 本體」的機制——schema 本體只在 registry 存一份，訊息裡放的是指標。</p>
<p>本文用 OrbStack 起 <code>confluentinc/cp-kafka</code> + <code>confluentinc/cp-schema-registry</code>，用 Avro console producer 寫一筆 <code>{&quot;id&quot;:1,&quot;name&quot;:&quot;alice&quot;}</code>，再 dump 出 raw bytes 驗證 wire format：</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">000000 00 00 00 00 01 02 0a 61 6c 69 63 65 0a   &gt;.......alice.&lt;</span></span></code></pre></div><p>逐 byte 拆解：</p>
<ul>
<li><code>00</code>：magic byte，標識這是 Confluent wire format</li>
<li><code>00 00 00 01</code>：4-byte big-endian schema ID = 1，consumer 拿這個去 registry 查 schema</li>
<li><code>02</code>：Avro 把 <code>id</code>（long）以 zigzag varint 編碼，<code>1</code> 編成 <code>0x02</code></li>
<li><code>0a 61 6c 69 63 65</code>：<code>name</code>（string）長度 5（zigzag <code>0x0a</code>）加 UTF-8 的 <code>alice</code></li>
</ul>
<p>這個格式有兩個工程後果。第一，consumer 反序列化任何訊息前都要能連到 registry——registry 掛掉，已 cache schema ID 的 consumer 還能跑，但遇到沒見過的 schema ID 就卡住。第二，schema ID 是全域單調遞增的整數、跨 subject 共用：同一份 schema 被多個 topic 註冊只會有一個 ID。實機驗證可以看到，先註冊到 <code>user-value</code> 的 schema 拿到 <code>id:1</code>，之後用同樣結構寫 <code>users-demo</code> topic 時，registry 認出是同一份 schema、複用 <code>id:1</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;subject&#34;</span><span class="p">:</span><span class="s2">&#34;users-demo-value&#34;</span><span class="p">,</span><span class="nt">&#34;version&#34;</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="nt">&#34;schemaType&#34;</span><span class="p">:</span><span class="s2">&#34;AVRO&#34;</span><span class="p">,</span> <span class="err">...</span><span class="p">}</span></span></span></code></pre></div><p><code>version</code> 是 subject 內的序號（每個 subject 從 1 開始）、<code>id</code> 是全域的。除錯時看到某筆訊息反序列化失敗，第一步就是讀那 4-byte schema ID、去 registry 撈出它指向哪個 schema、跟 consumer 預期的對不對。</p>
<h2 id="序列化格式取捨avroprotobufjson-schema">序列化格式取捨：Avro、Protobuf、JSON Schema</h2>
<p>Schema Registry 支援三種格式，差異不只是語法、而是演進規則與生態的取捨。</p>
<table>
  <thead>
      <tr>
          <th>格式</th>
          <th>演進機制</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Avro</td>
          <td>reader / writer schema resolution</td>
          <td>data pipeline、強 schema 演進需求、JVM 生態</td>
      </tr>
      <tr>
          <td>Protobuf</td>
          <td>field number 標記</td>
          <td>已用 gRPC、跨語言 RPC + 事件共用 schema</td>
      </tr>
      <tr>
          <td>JSON Schema</td>
          <td>結構 + validation keyword</td>
          <td>已大量 JSON、要人類可讀、容忍較弱的型別保證</td>
      </tr>
  </tbody>
</table>
<p>Avro 的演進靠 <em>reader schema 與 writer schema 分離</em>：訊息用 writer schema（寫入時的版本）序列化，consumer 用自己的 reader schema（讀取時的版本）反序列化，registry 提供兩者做 schema resolution。這是 Avro 在 data pipeline 場景的核心優勢——欄位帶 default 時，舊資料用新 schema 讀會自動填 default，新資料用舊 schema 讀會自動忽略多出來的欄位。Yelp、多數 Kafka-native data platform 都選 Avro，正是因為它的演進語意最完整。</p>
<p>Protobuf 用 field number 而非欄位名做 wire 識別：欄位改名不破壞相容性（number 沒變即可），刪欄位要 reserve 掉 number 避免重用。已經用 gRPC 的團隊讓 RPC 與事件共用同一份 <code>.proto</code>，省一套 schema 維護。代價是 Protobuf 的 default 語意較弱（proto3 沒有 explicit presence 的 scalar 一律有 zero value），某些演進判斷不如 Avro 直觀。</p>
<p>JSON Schema 適合既有系統已經大量用 JSON、且看重人類可讀與 validation keyword（<code>required</code>、<code>minimum</code>、<code>pattern</code>）的場景。代價是 payload 較大（欄位名重複出現在每筆訊息）、型別保證弱於前兩者。當吞吐量大、payload size 敏感時，JSON Schema 的頻寬成本會顯著高於 Avro 的 binary 編碼。</p>
<p>選型判準：data pipeline 為主、重演進安全 → Avro；已有 gRPC、RPC 與事件共用 → Protobuf；既有 JSON 生態、重可讀性而吞吐量不極端 → JSON Schema。三者可在同一個 registry 並存（每個 subject 各自標 schemaType），但同一個 subject 內不能混用格式。</p>
<h2 id="subject-naming-strategy-決定相容性檢查的邊界">Subject naming strategy 決定相容性檢查的邊界</h2>
<p>Subject 是 registry 裡做版本管理與相容性檢查的基本單位；naming strategy 決定「哪些 schema 被歸進同一個 subject、因而要互相相容」。選錯 strategy 會讓相容性檢查管太寬或太窄，是後面故障演練的根源之一。</p>
<table>
  <thead>
      <tr>
          <th>Strategy</th>
          <th>Subject 名</th>
          <th>相容性檢查邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TopicNameStrategy</td>
          <td><code>&lt;topic&gt;-value</code> / <code>&lt;topic&gt;-key</code></td>
          <td>整個 topic 只能有一種 value schema 演進</td>
      </tr>
      <tr>
          <td>RecordNameStrategy</td>
          <td><code>&lt;record 全名&gt;</code></td>
          <td>同名 record 跨所有 topic 一起演進</td>
      </tr>
      <tr>
          <td>TopicRecordNameStrategy</td>
          <td><code>&lt;topic&gt;-&lt;record 全名&gt;</code></td>
          <td>同 topic 內可放多種 record、各自演進</td>
      </tr>
  </tbody>
</table>
<p>TopicNameStrategy 是預設，subject 名就是 <code>&lt;topic&gt;-value</code>。實機驗證可以看到，用 Avro producer 寫 <code>users-demo</code> topic 時，registry 自動建立 <code>users-demo-value</code> subject：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="s2">&#34;user-value&#34;</span><span class="p">,</span><span class="s2">&#34;users-demo-value&#34;</span><span class="p">]</span></span></span></code></pre></div><p>預設策略的隱含假設是「一個 topic 只承載一種事件型別」。這對多數 topic 成立，但當業務要把多種相關事件（例如 <code>OrderCreated</code> 與 <code>OrderCancelled</code>）放進同一個 topic 以保證跨事件 ordering 時，TopicNameStrategy 會把兩種 record 當成同一個 subject 的版本演進、互相做相容性檢查——這幾乎一定失敗，因為兩種事件結構本來就不同。</p>
<p>這時要改 RecordNameStrategy（subject = record 全名，跨 topic 同名 record 共用一份演進歷史）或 TopicRecordNameStrategy（subject = topic + record 名，同 topic 多型別各自獨立演進）。判準：一個 topic 一種事件 → 預設即可；一個 topic 多種事件且要保 ordering → TopicRecordNameStrategy；同一種 record 散在多個 topic 要強制全域一致 → RecordNameStrategy。Producer 與 consumer 必須設成同一個 strategy，否則 consumer 會用錯 subject 去查 schema。</p>
<h2 id="compatibility-level四種基礎--transitive">Compatibility level：四種基礎 × transitive</h2>
<p>Compatibility level 是 registry 在 producer 註冊新 schema 時套用的相容性規則，決定哪些 schema 改動會被擋下。它回答的問題是「新 schema 跟既有 schema 比，誰應該能讀誰寫的資料」。設定可以是全域預設、也可以 per-subject 覆寫。</p>
<table>
  <thead>
      <tr>
          <th>Level</th>
          <th>規則</th>
          <th>保護對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BACKWARD</td>
          <td>新 schema 能讀舊 schema 寫的資料</td>
          <td>consumer 先升級、producer 後升級</td>
      </tr>
      <tr>
          <td>FORWARD</td>
          <td>舊 schema 能讀新 schema 寫的資料</td>
          <td>producer 先升級、consumer 後升級</td>
      </tr>
      <tr>
          <td>FULL</td>
          <td>同時滿足 BACKWARD 與 FORWARD</td>
          <td>雙向都能不同步演進</td>
      </tr>
      <tr>
          <td>NONE</td>
          <td>不檢查</td>
          <td>不保護（演進風險全交給人）</td>
      </tr>
  </tbody>
</table>
<p>BACKWARD 是 Confluent 預設，實機驗證可以確認：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;compatibilityLevel&#34;</span><span class="p">:</span><span class="s2">&#34;BACKWARD&#34;</span><span class="p">}</span></span></span></code></pre></div><p>BACKWARD 保護的是「consumer 先升級」的演進順序——新版 consumer 必須能讀舊版 producer 還在寫的舊資料。它允許的安全改動是「加帶 default 的欄位」與「刪欄位」：新 schema 讀舊資料時，舊資料缺的新欄位用 default 補；新 schema 不要的欄位讀舊資料時忽略。它擋下的是「加沒有 default 的必填欄位」——舊資料沒這欄位、新 consumer 又要求它存在，就讀不出來。</p>
<p>FORWARD 反過來保護「producer 先升級」：舊版 consumer 要能讀新版 producer 寫的資料。它允許「刪帶 default 的欄位」與「加欄位」。當演進順序是 producer 先上、consumer 慢慢跟（例如先讓 producer 開始寫新欄位、consumer 之後才用）時選 FORWARD。</p>
<p>FULL 同時滿足兩者，代價是只能做「加帶 default 的欄位」與「刪帶 default 的欄位」這類雙向安全的改動，演進自由度最低但最安全。當 producer 與 consumer 的升級順序無法協調（大型組織、多團隊各自排程）時，FULL 把演進約束到怎麼改都不會斷。</p>
<p>四種各有一個 transitive 變體（<code>BACKWARD_TRANSITIVE</code> 等）。非 transitive 只檢查新 schema 對 <em>最近一版</em>；transitive 檢查新 schema 對 <em>該 subject 所有歷史版本</em>。差別在這個場景：v1 → v2 相容、v2 → v3 相容，但 v3 對 v1 不相容。非 transitive 會放行 v3（因為只比 v2）；transitive 會擋下。當 consumer 可能 replay 很舊的歷史資料（Kafka 的長期保留 + replay 正是常態），transitive 才能保證任何歷史版本都讀得出來。<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary</a> 講的 replay 邊界，在 schema 層的對應就是 transitive compatibility。</p>
<h2 id="安全演進規則實機驗證註冊與拒絕">安全演進規則：實機驗證註冊與拒絕</h2>
<p>把上面的規則落到實際操作。在預設 BACKWARD 下，註冊 v1（<code>id</code> + <code>name</code>）後，加一個帶 default 的 <code>email</code> 欄位是安全的，registry 接受並記為 v2：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span><span class="nt">&#34;version&#34;</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span><span class="nt">&#34;schemaType&#34;</span><span class="p">:</span><span class="s2">&#34;AVRO&#34;</span><span class="p">,</span> <span class="err">...</span><span class="p">}</span></span></span></code></pre></div><p><code>user-value</code> 的版本列表確認累積成兩版：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">]</span></span></span></code></pre></div><p>接著嘗試加一個 <em>沒有 default</em> 的 <code>age</code>（int）必填欄位——這破壞 BACKWARD，因為新 consumer 讀舊資料時 <code>age</code> 沒值也沒 default。registry 回 HTTP 409 並指出確切原因：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;error_code&#34;</span><span class="p">:</span><span class="mi">40901</span><span class="p">,</span><span class="nt">&#34;message&#34;</span><span class="p">:</span><span class="s2">&#34;Schema being registered is incompatible with an earlier schema for subject \&#34;user-value\&#34;</span><span class="p">,</span> <span class="err">details:</span> <span class="err">[{errorType:&#39;READER_FIELD_MISSING_DEFAULT_VALUE&#39;,</span> <span class="err">description:&#39;The</span> <span class="err">field</span> <span class="err">&#39;age&#39;</span> <span class="err">at</span> <span class="err">path</span> <span class="err">&#39;/fields/3&#39;</span> <span class="err">in</span> <span class="err">the</span> <span class="err">new</span> <span class="err">schema</span> <span class="err">has</span> <span class="err">no</span> <span class="err">default</span> <span class="err">value</span> <span class="err">and</span> <span class="err">is</span> <span class="err">missing</span> <span class="err">in</span> <span class="err">the</span> <span class="err">old</span> <span class="err">schema&#39;,</span> <span class="err">...</span><span class="p">}</span><span class="err">],</span> <span class="err">compatibility:</span> <span class="err">&#39;BACKWARD&#39;}</span></span></span></code></pre></div><p><code>READER_FIELD_MISSING_DEFAULT_VALUE</code> 精確命中規則：reader（新 schema）多了一個舊資料沒有、又無 default 的欄位。registry 另外提供 compatibility check API，可以在不真正註冊的前提下先問「相不相容」，給 CI pipeline 在 PR 階段擋下破壞性改動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;is_compatible&#34;</span><span class="p">:</span><span class="kc">false</span><span class="p">}</span></span></span></code></pre></div><p>由此導出兩條安全演進的操作規則。<strong>加欄位</strong>：一律帶 default（BACKWARD / FULL 都要），舊資料才能用新 schema 讀出。沒有合理 default 的「必填新欄位」不能直接加——要嘛在 producer 端先全部開始寫該欄位、確認資料齊全後再 promote，要嘛走新 topic / 新 record 而非原地演進。<strong>刪欄位</strong>：分步做。先讓所有 consumer 停止依賴該欄位（部署一輪），確認沒人讀之後，下一輪才從 schema 拿掉。一步到位刪掉還在被讀的欄位，會在 FORWARD / FULL 下被擋、在 BACKWARD 下放行但打掛還沒升級的 consumer。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1producer-加必填欄位無-default打掛舊-consumer">Case 1：producer 加必填欄位無 default，打掛舊 consumer</h3>
<p><strong>徵兆</strong>：某團隊 producer 發版後，另一團隊的舊 consumer 開始大量反序列化失敗、<code>SerializationException</code> 或 <code>AvroTypeException: Found X, expecting Y</code>，consumer lag 暴衝、訊息卡在 poll 階段。producer 端與 broker 端完全沒報錯——訊息照寫成功。</p>
<p><strong>根因</strong>：subject 的 compatibility level 被設成 NONE（或該欄位走了 FORWARD 不檢查 reader 缺欄位的路徑）。producer 加了一個沒有 default 的必填欄位、registry 沒擋，新訊息帶新 schema ID 寫進 topic。舊 consumer 用自己的舊 reader schema 去反序列化新 writer schema 的資料，遇到自己不認識又無從補值的結構就炸。問題不在 producer 也不在 broker，在 <em>registry 沒在註冊時擋下這次演進</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>把 compatibility level 改回至少 BACKWARD</strong>：實機驗證過 NONE 會直接放行破壞性 schema——把 <code>compatibility</code> 設成 NONE 後，前面被 409 拒絕的破壞性 schema 立刻被接受成 v3。NONE 等於把演進安全完全交給人，多團隊場景幾乎一定出事。</li>
<li><strong>回退 producer</strong>：先讓 producer 退回舊 schema 止血，恢復舊 consumer 可讀。</li>
<li><strong>重新演進</strong>：欄位帶 default 重發，或若該欄位語意上必填、走「先讓 producer 寫、consumer 升級、再 promote」的分步路徑。</li>
<li><strong>CI 防線</strong>：把 compatibility check API（<code>/compatibility/subjects/&lt;s&gt;/versions/latest</code>）接進 producer repo 的 CI，PR 階段就用 <code>is_compatible:false</code> 擋掉，不等到 production 註冊時才發現。</li>
</ol>
<h3 id="case-2compatibility-level-設錯放行破壞性變更">Case 2：compatibility level 設錯，放行破壞性變更</h3>
<p><strong>徵兆</strong>：team 以為有 registry 把關所以放心演進，某次刪掉一個還在被下游讀的欄位、registry 接受了，下游服務隔天開始拿到 null / 缺欄位、business logic 走錯分支，但沒有任何 exception——資料「看起來正常」只是少了東西。</p>
<p><strong>根因</strong>：compatibility level 設成了 FORWARD 而需求其實是 BACKWARD，或設成 NONE。實機驗證可以看到 per-subject 覆寫的行為——對 <code>user-value</code> 單獨 PUT <code>FORWARD</code> 後查 config 回 <code>{&quot;compatibilityLevel&quot;:&quot;FORWARD&quot;}</code>，這個 subject 的檢查方向就跟全域預設不同了。FORWARD 允許刪帶 default 的欄位（保護 producer 先升級的順序），但團隊實際的演進順序是 consumer 後升級——方向錯配，registry 放行的正是會打掛 consumer 的那類改動。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>依演進順序選 level，不是隨手設</strong>：consumer 先升級選 BACKWARD；producer 先升級選 FORWARD；順序無法協調選 FULL。把這個決策寫進 topic ownership 文件、不是留給註冊當下的人臨時判斷。</li>
<li><strong>可能 replay 歷史就用 transitive</strong>：Kafka 長期保留 + replay 是常態，非 transitive 只擋最近一版、replay 舊資料時舊 schema 仍可能讀不出。長期保留的 topic 預設用 <code>*_TRANSITIVE</code>。</li>
<li><strong>per-subject 覆寫要留審計</strong>：全域預設外的每一個 per-subject 覆寫都是一個風險點，要能查出「誰、何時、為什麼把這個 subject 改成跟預設不同」。</li>
</ol>
<h3 id="case-3schema-id-對不上consumer-反序列化失敗">Case 3：schema ID 對不上，consumer 反序列化失敗</h3>
<p><strong>徵兆</strong>：consumer 報 <code>Schema not found; error code: 40403</code> 或反序列化拿到亂碼、欄位錯位。某些訊息正常、某些失敗，跟特定 producer 或特定時間段相關。</p>
<p><strong>根因</strong>有幾種，靠讀訊息前 5 byte 的 schema ID 定位：</p>
<ul>
<li><strong>registry 換過、ID 不一致</strong>：跨環境（dev / staging / prod）各自一套 registry，schema ID 全域遞增的順序不同，同一份 schema 在不同環境是不同 ID。如果有人把 prod 的訊息 mirror 到 staging 而沒搬 schema，staging consumer 拿 prod 的 schema ID 去 staging registry 查就 404。</li>
<li><strong>訊息根本不是 Confluent wire format</strong>：有 producer 沒走 schema-aware serializer、直接寫 raw bytes，前 5 byte 不是 magic + ID。consumer 把第一個 byte 當 magic、後 4 byte 當 ID 去查，撈到不存在或錯誤的 schema。</li>
<li><strong>registry 不可達或 cache 失效</strong>：consumer 端 schema cache 沒命中、又連不上 registry。</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>讀 wire format 確認</strong>：dump 訊息 raw bytes，確認第一個 byte 是 <code>00</code>、接下來 4 byte 解出來的 ID 在目標 registry 查得到。本文驗證過 <code>00 00 00 00 01</code> 對應 schema id 1，這是除錯的第一手證據。</li>
<li><strong>跨環境 schema 搬遷</strong>：mirror 訊息時用 registry 的 import / export，或 MirrorMaker 搭配 schema 同步，不要只搬資料不搬 schema。</li>
<li><strong>隔離非 schema-aware producer</strong>：用 ACL 或 topic 命名規範強制所有 producer 走 schema serializer，避免 raw bytes 混進 schema-managed topic。</li>
</ol>
<h3 id="case-4subject-naming-strategy-衝突">Case 4：subject naming strategy 衝突</h3>
<p><strong>徵兆</strong>：把第二種事件型別寫進既有 topic 時，producer 直接註冊失敗報 incompatible，或多 producer 寫同 topic 互相把對方的 schema 判成不相容、彼此發版互相擋。</p>
<p><strong>根因</strong>：用 TopicNameStrategy（預設）卻往同一個 topic 放多種 record。subject 是 <code>&lt;topic&gt;-value</code>、整個 topic 共用一條演進線，registry 拿 <code>OrderCancelled</code> 去跟既有的 <code>OrderCreated</code> 做相容性檢查——兩種結構不同的事件當然不相容。strategy 的隱含假設（一 topic 一事件型別）跟實際用法（一 topic 多事件保 ordering）衝突。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>改 strategy 配合用法</strong>：一 topic 多事件 → TopicRecordNameStrategy，subject 變成 <code>&lt;topic&gt;-&lt;record 全名&gt;</code>，每種 record 各自一條演進線、不互相檢查。</li>
<li><strong>producer 與 consumer 設同一個 strategy</strong>：strategy 不一致時 consumer 會用錯 subject 查 schema，拿到 null 或錯 schema。這是部署層的硬約束，要在共用 config 統一。</li>
<li><strong>若只是不小心寫錯 topic</strong>：那不是 strategy 問題、是路由問題，修 producer 的 topic 選擇邏輯，別為了繞過檢查改成 RecordNameStrategy。</li>
</ol>
<h2 id="容量與運維邊界">容量與運維邊界</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算 / 邊界</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 數量</td>
          <td>數千 schema registry 仍可運作（Yelp 等級）</td>
          <td><code>_schemas</code> topic 是 single-partition</td>
      </tr>
      <tr>
          <td>Wire format overhead</td>
          <td>每筆訊息固定 +5 byte</td>
          <td>高頻小訊息時相對 overhead 不可忽略</td>
      </tr>
      <tr>
          <td>Registry 可用性</td>
          <td>consumer cache 命中時可短暫容忍 registry 不可達</td>
          <td>冷 consumer / 新 schema ID 時硬依賴</td>
      </tr>
      <tr>
          <td>Compatibility 檢查</td>
          <td>註冊時做、非 hot path</td>
          <td>transitive 對長歷史 subject 檢查較慢</td>
      </tr>
      <tr>
          <td>環境隔離</td>
          <td>每環境一套 registry、schema ID 不跨環境一致</td>
          <td>跨環境 mirror 要同步搬 schema</td>
      </tr>
  </tbody>
</table>
<p>實務 default：data pipeline 場景選 Avro + 至少 BACKWARD；長期保留 + replay 的 topic 用 transitive；compatibility check 接進 CI 在 PR 階段擋破壞性改動，不依賴註冊當下把關；一 topic 一事件型別當預設、要多型別才動 naming strategy。Schema Registry 自己也是個要 HA 的元件——production 跑多副本、<code>_schemas</code> topic 的 replication factor 拉高，registry 是事件總線的單點時要當關鍵基礎設施對待。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="跟-cdc-pipeline-的銜接">跟 CDC pipeline 的銜接</h3>
<p><a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">Shopify Debezium CDC 案例</a> 跑在 100+ MySQL shard、150 個 Debezium connector 的規模（該案例記載的重點是 lock-free snapshot 與 oversized record 處理）。CDC pipeline 有一個一般性的 schema 演進壓力，以下依 CDC 機制推導、非該案例的結論：上游 DDL 一改，Debezium 產生的 Kafka record schema 跟著變，下游 consumer 受影響。Schema Registry 的 compatibility 檢查就是把這道衝擊在進 Kafka 時攔下的關卡——選錯 compatibility level，一次 ALTER TABLE 就可能透過 CDC 打穿整條 pipeline。Debezium 與 Kafka Connect 原生整合 Schema Registry，connector 設定裡指定 registry URL 與 naming strategy。</p>
<h3 id="跟-replay-邊界與事件契約">跟 replay 邊界與事件契約</h3>
<p><a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary</a> 講的是事件契約能 replay 多遠；schema 層的對應就是本文的 transitive compatibility。Replay 跨越多個 schema 版本時，只有 transitive 能保證任何歷史版本都讀得出來。兩者一起界定「這條事件流的契約能安全回放到多久以前」。</p>
<h3 id="下游能力">下游能力</h3>
<ul>
<li>概念索引：<a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Event Schema Compatibility</a> 知識卡（本文的 implementation 來源）</li>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a>（KRaft 與 Schema Registry 段）</li>
<li>對應案例：<a href="/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">3.C14 Yelp Schematizer</a>（schema 治理拉到平台層）、<a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">3.C13 Shopify Debezium CDC</a>（CDC 場景的 schema evolution）</li>
<li>方法論：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>RabbitMQ Network Partition 與 Cluster 一致性：腦裂下要保誰</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/network-partition-clustering/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/network-partition-clustering/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> overview「Erlang clustering 與 network partition」段的 implementation-layer deep article。Overview 回答「RabbitMQ cluster 是什麼、跟同類 broker 差在哪」；本文回答「partition 發生時 broker 怎麼決策、各策略保住什麼、丟掉什麼」。&lt;/p>&lt;/blockquote>
&lt;p>Network partition 是 cluster 節點之間的網路連線中斷、雙方各自仍存活但互相不可達的狀態。RabbitMQ cluster 建立在 Erlang distribution 之上、節點靠固定心跳（net_tick）互相確認存活；心跳連續數次收不到、Erlang 就判定對方失聯、把單一 cluster 切成兩個互不知道對方狀態的子群。此時的核心問題不是「怎麼避免 partition」——跨機房、跨 AZ、雲端 VPC 路由抖動都會造成短暫不可達、partition 在分散式系統是必然會遇到的物理事件——而是「分裂的瞬間、broker 要犧牲可用性保一致性、還是犧牲一致性保可用性」。&lt;code>cluster_partition_handling&lt;/code> 設定就是這個取捨的開關。&lt;/p>
&lt;h2 id="問題情境兩邊都覺得自己是對的">問題情境：兩邊都覺得自己是對的&lt;/h2>
&lt;p>腦裂（split-brain）的破壞性在於分裂的兩個子群各自繼續服務、各自接受寫入、各自認為對方已死。等到網路恢復、兩邊的狀態已經分歧：同一個 queue 在 A 子群被消費掉的訊息、在 B 子群還在；同一個 exchange 的 binding 在兩邊被改成不同樣子；同一筆業務在兩邊各被處理一次。&lt;/p>
&lt;p>RabbitMQ 的 classic queue 沒有內建的衝突解決機制。當兩個子群在 partition 期間各自修改了 cluster metadata（queue / exchange / binding 的定義）、恢復連線後 RabbitMQ 無法自動合併這些分歧、預設行為是 &lt;em>拒絕自動重新加入&lt;/em>、把節點停在 partition 狀態等人工處置。這就是為什麼 partition handling 策略的選擇、本質是「願意在分裂瞬間付出什麼代價、來換取恢復時的可預測性」。&lt;/p>
&lt;p>這個取捨跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing semantics 與 recovery semantics&lt;/a> 的判斷同源：投遞成功、處理成功、恢復成功是三件事。Partition 期間「broker 還在收訊息」（投遞層可用）不代表「訊息會被正確處理一次」（處理層一致）、更不代表「partition 結束後狀態能無損合併」（恢復層一致）。&lt;/p>
&lt;h2 id="核心概念一disc-node-與-ram-node">核心概念一：disc node 與 ram node&lt;/h2>
&lt;p>RabbitMQ cluster 的每個節點承擔一種角色、決定它存哪些資料。Cluster metadata（vhost、user、exchange、queue 定義、binding）在所有節點間複製、但 &lt;em>持久化到磁碟&lt;/em> 與否分兩種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>節點類型&lt;/th>
 &lt;th>metadata 存放&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Disc node&lt;/td>
 &lt;td>記憶體 + 磁碟&lt;/td>
 &lt;td>預設、cluster 必須至少有一個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ram node&lt;/td>
 &lt;td>僅記憶體&lt;/td>
 &lt;td>metadata 變更極頻繁的特殊場景、現代極少使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Disc node 把 cluster metadata 寫到磁碟、整個 cluster 重啟後能從磁碟恢復拓樸定義。Ram node 只把 metadata 放記憶體、metadata 操作（宣告大量 queue / binding）較快、但 cluster 若全部節點同時掛掉就會遺失定義。&lt;/p>
&lt;p>Ram node 是早期為了加速高頻 metadata 變更而設計的角色。實務上現代 RabbitMQ 部署幾乎都用全 disc node：metadata 操作的效能瓶頸在現代硬體上不再顯著、而全 disc 換來的「任意節點重啟都能恢復拓樸」的可預測性、價值遠高於那點 metadata 寫入速度。官方文件也建議 cluster 內 disc node 至少兩個、避免唯一的 disc node 掛掉時整個 cluster 的 metadata 無法持久化。&lt;/p>
&lt;p>本文實機演練的 3-node cluster 全部是 disc node、這也是 &lt;code>rabbitmqctl cluster_status&lt;/code> 在 OrbStack 上的實際輸出：&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">Disk Nodes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">rabbit@rmq1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">rabbit@rmq2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">rabbit@rmq3&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要特別區分的是：disc / ram 講的是 &lt;em>cluster metadata&lt;/em> 的持久化、跟 &lt;em>訊息本身&lt;/em> 是否持久化（durable queue + persistent message）是兩個獨立軸。Disc node 不會讓 transient queue 的訊息變持久、ram node 也不會讓 durable queue 的訊息變揮發。訊息持久化的判讀見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> overview「Erlang clustering 與 network partition」段的 implementation-layer deep article。Overview 回答「RabbitMQ cluster 是什麼、跟同類 broker 差在哪」；本文回答「partition 發生時 broker 怎麼決策、各策略保住什麼、丟掉什麼」。</p></blockquote>
<p>Network partition 是 cluster 節點之間的網路連線中斷、雙方各自仍存活但互相不可達的狀態。RabbitMQ cluster 建立在 Erlang distribution 之上、節點靠固定心跳（net_tick）互相確認存活；心跳連續數次收不到、Erlang 就判定對方失聯、把單一 cluster 切成兩個互不知道對方狀態的子群。此時的核心問題不是「怎麼避免 partition」——跨機房、跨 AZ、雲端 VPC 路由抖動都會造成短暫不可達、partition 在分散式系統是必然會遇到的物理事件——而是「分裂的瞬間、broker 要犧牲可用性保一致性、還是犧牲一致性保可用性」。<code>cluster_partition_handling</code> 設定就是這個取捨的開關。</p>
<h2 id="問題情境兩邊都覺得自己是對的">問題情境：兩邊都覺得自己是對的</h2>
<p>腦裂（split-brain）的破壞性在於分裂的兩個子群各自繼續服務、各自接受寫入、各自認為對方已死。等到網路恢復、兩邊的狀態已經分歧：同一個 queue 在 A 子群被消費掉的訊息、在 B 子群還在；同一個 exchange 的 binding 在兩邊被改成不同樣子；同一筆業務在兩邊各被處理一次。</p>
<p>RabbitMQ 的 classic queue 沒有內建的衝突解決機制。當兩個子群在 partition 期間各自修改了 cluster metadata（queue / exchange / binding 的定義）、恢復連線後 RabbitMQ 無法自動合併這些分歧、預設行為是 <em>拒絕自動重新加入</em>、把節點停在 partition 狀態等人工處置。這就是為什麼 partition handling 策略的選擇、本質是「願意在分裂瞬間付出什麼代價、來換取恢復時的可預測性」。</p>
<p>這個取捨跟 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing semantics 與 recovery semantics</a> 的判斷同源：投遞成功、處理成功、恢復成功是三件事。Partition 期間「broker 還在收訊息」（投遞層可用）不代表「訊息會被正確處理一次」（處理層一致）、更不代表「partition 結束後狀態能無損合併」（恢復層一致）。</p>
<h2 id="核心概念一disc-node-與-ram-node">核心概念一：disc node 與 ram node</h2>
<p>RabbitMQ cluster 的每個節點承擔一種角色、決定它存哪些資料。Cluster metadata（vhost、user、exchange、queue 定義、binding）在所有節點間複製、但 <em>持久化到磁碟</em> 與否分兩種：</p>
<table>
  <thead>
      <tr>
          <th>節點類型</th>
          <th>metadata 存放</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disc node</td>
          <td>記憶體 + 磁碟</td>
          <td>預設、cluster 必須至少有一個</td>
      </tr>
      <tr>
          <td>Ram node</td>
          <td>僅記憶體</td>
          <td>metadata 變更極頻繁的特殊場景、現代極少使用</td>
      </tr>
  </tbody>
</table>
<p>Disc node 把 cluster metadata 寫到磁碟、整個 cluster 重啟後能從磁碟恢復拓樸定義。Ram node 只把 metadata 放記憶體、metadata 操作（宣告大量 queue / binding）較快、但 cluster 若全部節點同時掛掉就會遺失定義。</p>
<p>Ram node 是早期為了加速高頻 metadata 變更而設計的角色。實務上現代 RabbitMQ 部署幾乎都用全 disc node：metadata 操作的效能瓶頸在現代硬體上不再顯著、而全 disc 換來的「任意節點重啟都能恢復拓樸」的可預測性、價值遠高於那點 metadata 寫入速度。官方文件也建議 cluster 內 disc node 至少兩個、避免唯一的 disc node 掛掉時整個 cluster 的 metadata 無法持久化。</p>
<p>本文實機演練的 3-node cluster 全部是 disc node、這也是 <code>rabbitmqctl cluster_status</code> 在 OrbStack 上的實際輸出：</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">Disk Nodes
</span></span><span class="line"><span class="ln">2</span><span class="cl">rabbit@rmq1
</span></span><span class="line"><span class="ln">3</span><span class="cl">rabbit@rmq2
</span></span><span class="line"><span class="ln">4</span><span class="cl">rabbit@rmq3</span></span></code></pre></div><p>要特別區分的是：disc / ram 講的是 <em>cluster metadata</em> 的持久化、跟 <em>訊息本身</em> 是否持久化（durable queue + persistent message）是兩個獨立軸。Disc node 不會讓 transient queue 的訊息變持久、ram node 也不會讓 durable queue 的訊息變揮發。訊息持久化的判讀見 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。</p>
<h2 id="核心概念二partition-偵測機制">核心概念二：partition 偵測機制</h2>
<p>RabbitMQ 不自己實作節點存活偵測、而是直接用 Erlang distribution 的 net_tick 機制。每個節點對 cluster 內其他節點定期送 tick、<code>net_ticktime</code> 預設 60 秒；連續數個 tick interval（預設約 4 個、即 net_ticktime 區間內）收不到對方回應、Erlang 就判定該節點 <code>nodedown</code>、向上層的 RabbitMQ partition handler 報告。</p>
<p>這個機制有兩個實務後果。第一、partition 偵測有 <em>延遲</em>：短於 net_ticktime 的網路抖動（幾秒的 GC pause、瞬間封包遺失）不會觸發 partition、避免把暫時性抖動誤判成永久分裂。第二、偵測延遲是雙刃：net_ticktime 設太長、真的 partition 了也要等很久才反應、期間腦裂持續擴大；設太短、雲端環境正常的網路抖動會頻繁誤觸發 partition handler、造成不必要的節點暫停。</p>
<p>本文實機演練用 <code>docker network disconnect</code> 切斷一個節點的網路、實測偵測延遲：disconnect 後約 60 秒（吻合 net_ticktime 預設值）、多數派側的 <code>cluster_status</code> 的 Running Nodes 才從三個掉到兩個：</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">disconnect 後立即查 → Running Nodes 仍顯示 3 個（尚未偵測）
</span></span><span class="line"><span class="ln">2</span><span class="cl">等待約 60 秒 → Running Nodes 掉到 2 個（partition 已偵測）</span></span></code></pre></div><p>偵測到 partition 之後、broker 怎麼處置、完全取決於 <code>cluster_partition_handling</code> 設定。</p>
<h2 id="核心概念三cluster_partition_handling-三策略">核心概念三：cluster_partition_handling 三策略</h2>
<p>這個設定決定 broker 在偵測到 partition 後的行為、是整個 cluster 一致性與可用性取捨的單一開關。三種策略對應三種不同的 CAP 立場。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>partition 時行為</th>
          <th>保住</th>
          <th>犧牲</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ignore</code></td>
          <td>兩邊都繼續服務、不做任何處置</td>
          <td>可用性</td>
          <td>一致性（會腦裂）</td>
          <td>單機 / 不在乎一致性的場景</td>
      </tr>
      <tr>
          <td><code>pause_minority</code></td>
          <td>少數派節點暫停 broker、多數派繼續</td>
          <td>一致性</td>
          <td>少數派可用性</td>
          <td>奇數節點 cluster（推薦）</td>
      </tr>
      <tr>
          <td><code>autoheal</code></td>
          <td>partition 結束後自動選贏家、輸家重啟丟狀態</td>
          <td>自動恢復</td>
          <td>輸家側的訊息</td>
          <td>可容忍少量訊息遺失的場景</td>
      </tr>
  </tbody>
</table>
<p>設定方式在 <code>rabbitmq.conf</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">cluster_partition_handling</span> <span class="o">=</span> <span class="s">pause_minority</span></span></span></code></pre></div><p>或在舊版 advanced config（Erlang term 格式）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-erlang" data-lang="erlang"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span><span class="n">rabbit</span><span class="p">,</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="p">{</span><span class="n">cluster_partition_handling</span><span class="p">,</span> <span class="n">pause_minority</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">]}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">].</span></span></span></code></pre></div><p>三個策略的差異不在「哪個比較好」、而在「分裂瞬間願意讓誰停下來」。下面三段把每個策略在真實服務裡長什麼樣展開。</p>
<h3 id="ignore兩邊都活恢復時等人來">ignore：兩邊都活、恢復時等人來</h3>
<p><code>ignore</code> 是預設值（OrbStack 起的 cluster <code>rabbitmqctl environment</code> 實測輸出 <code>{cluster_partition_handling, ignore}</code>）。它的行為是 partition 偵測到了、但 broker 什麼都不做、兩個子群繼續各自服務。</p>
<p>這在單節點部署完全沒問題——沒有 cluster 就沒有 partition。問題出在多節點 cluster：兩個子群會各自接受 publish、各自讓 consumer 消費、各自修改 metadata。網路恢復後、RabbitMQ 偵測到兩邊狀態分歧、會把節點停在 partition 狀態、不自動重新加入、在 log 留下 partition 警告等人工介入。此時 metadata 已經分歧、需要人工決定保留哪一邊、reset 另一邊重新 join。</p>
<p><code>ignore</code> 適合的場景很窄：單機部署、或刻意接受腦裂並在應用層做衝突解決的特殊架構。多數需要 cluster 的場景不該用 <code>ignore</code>——它把一致性的責任完全推給人工處置、而人工處置在凌晨三點的 incident 現場是最不可靠的環節。</p>
<h3 id="pause_minority少數派主動停下">pause_minority：少數派主動停下</h3>
<p><code>pause_minority</code> 是奇數節點 cluster 的推薦策略、它的設計直接對應 quorum 的數學：partition 把 cluster 切成兩半時、節點數較少的那一側（少數派）主動 <em>暫停自己的 broker</em>、停止接受任何 client 連線；節點數較多的那一側（多數派）繼續服務。</p>
<p>這保證了任何時刻最多只有一個子群在服務、從根本上杜絕腦裂。代價是少數派側的所有 client 在 partition 期間完全失去服務。</p>
<p>3-node cluster 是這個策略的最小有效配置。實機演練：把 rmq3 從 network disconnect、製造「rmq1 + rmq2 多數派 vs rmq3 少數派」的分裂、約 60 秒後查少數派 rmq3 的狀態：</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">$ rabbitmqctl cluster_status   # 在被孤立的 rmq3 上執行
</span></span><span class="line"><span class="ln">2</span><span class="cl">Error: this command requires the &#39;rabbit&#39; app to be running on the target node.
</span></span><span class="line"><span class="ln">3</span><span class="cl">       Start it with &#39;rabbitmqctl start_app&#39;.</span></span></code></pre></div><p>少數派 rmq3 的 rabbit 應用被 partition handler 主動停止——這正是 pause_minority 的預期行為。同時多數派側 rmq1 的 cluster_status 顯示 Running Nodes 只剩 rmq1 + rmq2、繼續正常服務。</p>
<p>恢復也是自動的。把 rmq3 重新 network connect、約 15 秒後它自動重啟 rabbit 應用、重新加入 cluster、Running Nodes 回到三個、Network Partitions 顯示 <code>(none)</code>、無殘留 partition 需要人工處置。這是 pause_minority 相對 ignore 的關鍵優勢：恢復路徑自動化、不依賴凌晨的人工判斷。</p>
<p>pause_minority 有一個硬性前提：cluster 必須是奇數節點、且要能形成明確的多數。2-node cluster 用 pause_minority 是反模式——partition 時兩邊各 1 個、都不是多數、結果兩邊都暫停、整個 cluster 完全不可用。4-node cluster 切成 2:2 也同樣兩邊都停。要用 pause_minority、節點數必須是 3、5、7 這種能在最常見的 1-node 失聯情境下仍形成多數的奇數。</p>
<h3 id="autoheal分裂時都活恢復時選贏家丟輸家">autoheal：分裂時都活、恢復時選贏家丟輸家</h3>
<p><code>autoheal</code> 走另一條路：partition 期間 <em>兩個子群都繼續服務</em>（跟 ignore 一樣）、但在 partition <em>結束</em> 的瞬間、broker 自動裁決——選出一個「贏家」子群、強制「輸家」子群的節點重啟、丟棄輸家在 partition 期間累積的狀態、然後重新加入贏家。</p>
<p>贏家的選擇規則是：先比 client 連線數（連線多的贏）、連線數相同比節點數、再相同比節點名稱。</p>
<p>autoheal 的取捨點跟 pause_minority 相反。pause_minority 在分裂瞬間就讓少數派停止、犧牲的是少數派 partition 期間的 <em>可用性</em>；autoheal 讓兩邊都活、犧牲的是輸家 partition 期間累積的 <em>訊息與狀態</em>。輸家側在 partition 期間被消費掉的訊息、被接受的新 publish、被修改的 binding、在 autoheal 重啟輸家後全部丟失。</p>
<p>這讓 autoheal 適合一種特定場景：可用性比訊息完整性重要、且訊息本身是冪等或可重送的。例如純粹的快取失效通知、可重算的衍生事件——丟幾條重新觸發即可。對「丟一條訊息等於丟一筆訂單」的場景、autoheal 的自動丟棄是不可接受的。</p>
<h2 id="quorum-queue-在-partition-下的行為">quorum queue 在 partition 下的行為</h2>
<p>前面三個 <code>cluster_partition_handling</code> 策略管的是 <em>classic queue 與 cluster metadata</em> 的 partition 行為。Quorum queue 是另一套機制——它不依賴 <code>cluster_partition_handling</code>、而是用 Raft 共識協議自己決定 partition 下的行為。這是 RabbitMQ 對腦裂問題的根本性改寫。</p>
<p>Quorum queue 把每個 queue 實作成一個獨立的 Raft 複製群組：一個 leader 加數個 follower、預設複製到奇數個節點（3-node cluster 通常 3 副本）。每筆 publish 必須被 <em>多數副本</em> 確認寫入、leader 才回 publisher confirm。實機驗證 3-node cluster 上 quorum queue 的 Raft 拓樸：</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">$ rabbitmq-queues quorum_status qq.test
</span></span><span class="line"><span class="ln">2</span><span class="cl">Node Name      Raft State   Membership
</span></span><span class="line"><span class="ln">3</span><span class="cl">rabbit@rmq1    leader       voter
</span></span><span class="line"><span class="ln">4</span><span class="cl">rabbit@rmq2    follower     voter
</span></span><span class="line"><span class="ln">5</span><span class="cl">rabbit@rmq3    follower     voter</span></span></code></pre></div><p>Partition 切斷 Raft 群組時、行為完全由 Raft 的 majority 規則決定、不需要 <code>cluster_partition_handling</code> 介入：</p>
<p>含 majority 副本的那一側選出（或維持）leader、繼續接受讀寫；不含 majority 的那一側無法 commit 任何寫入、自動進入唯讀或拒絕狀態。因為 commit 需要 majority 確認、少數派永遠湊不到 majority、所以少數派 <em>物理上不可能</em> 接受新寫入並確認——腦裂在協議層被排除、不靠運維設定。</p>
<p>實機演練最關鍵的一段：把 rmq2 與 rmq3 <em>同時</em> disconnect、讓 quorum queue 的 leader（在 rmq1）只剩自己一個副本、3 副本只剩 1 副本、失去 majority（1/3 &lt; 2/3）。此時 <code>quorum_status</code> 顯示其他兩個節點變 <code>timeout</code> 狀態：</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">Node Name      Raft State   Membership
</span></span><span class="line"><span class="ln">2</span><span class="cl">rabbit@rmq1    leader       voter
</span></span><span class="line"><span class="ln">3</span><span class="cl">rabbit@rmq2    timeout
</span></span><span class="line"><span class="ln">4</span><span class="cl">rabbit@rmq3    timeout</span></span></code></pre></div><p>然後對這個失去 quorum 的 queue 嘗試 publish：</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">$ rabbitmqadmin publish routing_key=qq.test payload=&#34;during-quorum-loss&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">[實測：publish 阻塞、12 秒後仍未返回——Raft 無 majority 可 commit]</span></span></code></pre></div><p>Publish 被阻塞、不返回 publisher confirm。因為 leader 拿不到任何 follower 的確認、無法達成 majority、寫入永遠 commit 不了。這是 quorum queue 用 <em>阻塞</em> 換 <em>一致性</em>：寧可不接受寫入、也不接受一筆無法被多數副本保證的寫入。</p>
<p>同一個 partition 情境下、對 classic queue 做同樣的 publish 作為對照：</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">$ rabbitmqadmin publish routing_key=cq.test payload=&#34;classic-during-partition&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">Message published   # 立即成功</span></span></code></pre></div><p>Classic queue 立即接受寫入。它沒有 Raft、leader 節點獨自決定、可用性優先——但這也正是它在腦裂下會分歧的根源：rmq1 接受的這筆、partition 結束後可能跟另一側的狀態衝突。</p>
<p>把兩邊 disconnect 的節點重新 connect、quorum 恢復、<code>quorum_status</code> 三個節點回到 leader + 2 follower、原本被阻塞的 publish 路徑恢復、新 publish 立即成功。Quorum queue 的恢復是協議自動完成的、不需要人工 reset 任何節點。</p>
<p>這就是 classic queue 加 <code>cluster_partition_handling</code> 與 quorum queue 的根本差異：前者是 <em>用運維策略事後補救</em> 一個本身會腦裂的資料結構、後者是 <em>用共識協議從設計上排除</em> 腦裂。現代 RabbitMQ 對需要跨節點一致性的 queue、官方建議直接用 quorum queue、把 partition 一致性交給 Raft、而不是依賴 <code>cluster_partition_handling</code> 的 classic queue 補救。Classic / quorum / stream 的完整選型判讀見 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">Queue Type 選型</a>。</p>
<h2 id="真實-cluster-治理以-zalando-為例">真實 cluster 治理：以 Zalando 為例</h2>
<p><a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27 Zalando RabbitMQ on AWS</a> 案例揭露了 K8s 普及之前、雲端 RabbitMQ cluster 治理的工程模式（master selection 與成員協調），跟 cluster 拓樸治理相關。</p>
<p>Zalando 的 communication platform 把 RabbitMQ cluster 跑在 EC2 上、自建 sidekick 服務查 AWS API 動態識別 cluster 成員、指定「最老的 instance」當 master、master 死後晉升下一個最老的節點。這套機制本質是在 RabbitMQ 內建的 partition handling 之外、額外加一層 <em>外部協調者</em> 來決定 cluster 拓樸（case 記載的直接動機是用 AWS API 動態識別成員、配合每 region 5 個 Elastic IP 的限制處理 master 角色）。把它讀作「早期雲端 RabbitMQ 在節點角色確定性上需要外部補強」是本文的判讀、非 case 明述的結論。</p>
<p>這個案例對映到本文的判讀是：早期 RabbitMQ cluster 的 partition 一致性需要大量外部工程（sidekick + AWS API + 自訂 master selection）來補足。Quorum queue 用 Raft 把這套外部協調內化進 broker——Raft 的 leader election 與 majority commit 取代了 Zalando 手寫的「最老 instance 當 master」邏輯。現代部署若用 quorum queue + pause_minority、不再需要外部 sidekick 來決定誰是 master。</p>
<p>語義誤配的風險在 partition 場景同樣存在。<a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 Queue 語義切換誤配</a> 指出 broker 行為改變時、「表面上訊息仍被送達、但業務資料開始出現重複或遺漏」。Partition 恢復正是這種高風險時刻：autoheal 丟棄輸家狀態、或人工從 ignore 的腦裂中合併、都可能讓同一批事件被處理零次或兩次。Partition 恢復後的 reconciliation、要對照 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 recovery semantics</a> 確認哪一段資料已被哪一側處理過、而不是假設「broker 恢復了 = 狀態正確了」。</p>
<h2 id="容量與規模判讀">容量與規模判讀</h2>
<p>Partition 處理策略的選擇隨 cluster 規模與一致性需求變化、不存在單一最佳解。</p>
<table>
  <thead>
      <tr>
          <th>規模 / 場景</th>
          <th>建議策略</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單節點</td>
          <td><code>ignore</code>（無 partition 可言）</td>
          <td>沒有 cluster、不需要 partition 處理</td>
      </tr>
      <tr>
          <td>3 / 5 / 7 奇數節點、需一致性</td>
          <td><code>pause_minority</code> + quorum queue</td>
          <td>少數派暫停、quorum queue 用 Raft 保一致</td>
      </tr>
      <tr>
          <td>偶數節點</td>
          <td>加一個節點變奇數、再用 pause_minority</td>
          <td>偶數節點對 pause_minority 是反模式</td>
      </tr>
      <tr>
          <td>可容忍訊息遺失、可用性優先</td>
          <td><code>autoheal</code> + classic queue</td>
          <td>接受輸家丟狀態、換 partition 期間雙邊可用</td>
      </tr>
      <tr>
          <td>跨 AZ / 跨 region</td>
          <td>重新評估是否該用單一 cluster</td>
          <td>partition 機率高、考慮 federation 拆成獨立 cluster</td>
      </tr>
  </tbody>
</table>
<p>幾個容量相關的硬性邊界：</p>
<p>跨 region 拉一個 RabbitMQ cluster 是高風險配置。跨 region 網路延遲與抖動讓 partition 從「偶發事件」變成「常態」——net_tick 頻繁逾時、pause_minority 頻繁暫停節點、cluster 實質不穩定。跨 region 的正確做法是每個 region 一個獨立 cluster、用 federation 或 shovel 做 region 間的訊息搬運、partition 限制在單一 region 內。</p>
<p>quorum queue 的副本數要對齊 cluster 規模。3-node cluster 配 3 副本能容忍 1 節點失聯（仍有 2/3 majority）；5-node 配 5 副本能容忍 2 節點失聯。副本數越多、容錯越高、但每筆寫入要等的確認也越多、寫入延遲上升。多數場景 3 副本是延遲與容錯的平衡點。</p>
<p>net_ticktime 的調整要保守。把它調短以加速 partition 偵測、會讓雲端正常抖動頻繁誤觸發 partition handler——pause_minority 下就是節點被頻繁暫停、可用性反而下降。除非有明確證據顯示偵測延遲是問題、否則保留 60 秒預設值。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<p>Partition 處理是 RabbitMQ cluster 可靠性的一環、跟以下能力環環相扣：</p>
<p>queue 類型的選擇直接決定 partition 行為。Classic queue 靠 <code>cluster_partition_handling</code> 事後補救、quorum queue 靠 Raft 從設計排除腦裂、stream 又是另一套複製模型。三者在 partition、throughput、retention 上的完整取捨、見 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">Queue Type 選型</a>。</p>
<p>partition 恢復的核心是恢復語義、不是連線恢復。Broker 重新連上不等於狀態一致——這正是 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing semantics 與 recovery semantics</a> 區分投遞、處理、恢復三層的價值。Partition 後的 reconciliation 要對照這三層判斷。</p>
<p>雲端 cluster 治理的歷史脈絡見 <a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27 Zalando AWS master selection</a>——理解外部協調者怎麼被 Raft 內化、有助於判斷現代部署該把多少責任交給 broker、多少留給運維。</p>
<p>語義誤配在 partition 恢復時的具體告警條件見 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 Queue 語義切換誤配</a>——下游同時出現重複與遺漏、是 partition 恢復處置出錯的典型訊號。</p>
<p>回到上游：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ overview</a> 的進階主題段列了 Erlang clustering 之外的 federation / shovel / Cluster Operator 議題；<a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a> 是 broker 通用概念的起點。</p>
]]></content:encoded></item><item><title>Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM&lt;/h2>
&lt;p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 &lt;code>OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。&lt;/p>
&lt;p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：&lt;code>maxmemory&lt;/code> 設多少、&lt;code>maxmemory-policy&lt;/code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。&lt;/p>
&lt;p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。&lt;/p>
&lt;h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型&lt;/h2>
&lt;p>要調校記憶體，先要分清楚 &lt;code>used_memory&lt;/code> 這個數字到底由什麼組成。&lt;code>INFO memory&lt;/code> 回報的是幾層疊加的記憶體會計，每一層去處不同：&lt;/p>
&lt;p>&lt;strong>&lt;code>used_memory&lt;/code>&lt;/strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。&lt;strong>&lt;code>used_memory_rss&lt;/code>&lt;/strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 &lt;code>used_memory&lt;/code>——兩者的比值就是 &lt;code>mem_fragmentation_ratio&lt;/code>。&lt;strong>&lt;code>used_memory_dataset&lt;/code>&lt;/strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。&lt;/p>
&lt;p>理解三個跟 OOM 直接相關的記憶體去處：&lt;/p>
&lt;p>&lt;strong>資料本身的編碼會放大或縮小記憶體&lt;/strong>。一個小 hash（field 數少於 &lt;code>hash-max-listpack-entries&lt;/code>、value 短於 &lt;code>hash-max-listpack-value&lt;/code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。&lt;/p>
&lt;p>&lt;strong>client output buffer 不計入 dataset 但會吃光記憶體&lt;/strong>。慢速 consumer、&lt;code>MONITOR&lt;/code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。&lt;code>client-output-buffer-limit&lt;/code> 沒設好，一個讀很慢的 replica 或一個掛著的 &lt;code>MONITOR&lt;/code> 連線就能把記憶體推到 maxmemory。&lt;/p>
&lt;p>&lt;strong>fork 期間記憶體會短暫翻倍&lt;/strong>。RDB save 與 AOF rewrite 都靠 &lt;code>fork()&lt;/code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM</h2>
<p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。</p>
<p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：<code>maxmemory</code> 設多少、<code>maxmemory-policy</code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。</p>
<p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。</p>
<h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型</h2>
<p>要調校記憶體，先要分清楚 <code>used_memory</code> 這個數字到底由什麼組成。<code>INFO memory</code> 回報的是幾層疊加的記憶體會計，每一層去處不同：</p>
<p><strong><code>used_memory</code></strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。<strong><code>used_memory_rss</code></strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 <code>used_memory</code>——兩者的比值就是 <code>mem_fragmentation_ratio</code>。<strong><code>used_memory_dataset</code></strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。</p>
<p>理解三個跟 OOM 直接相關的記憶體去處：</p>
<p><strong>資料本身的編碼會放大或縮小記憶體</strong>。一個小 hash（field 數少於 <code>hash-max-listpack-entries</code>、value 短於 <code>hash-max-listpack-value</code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。</p>
<p><strong>client output buffer 不計入 dataset 但會吃光記憶體</strong>。慢速 consumer、<code>MONITOR</code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。<code>client-output-buffer-limit</code> 沒設好，一個讀很慢的 replica 或一個掛著的 <code>MONITOR</code> 連線就能把記憶體推到 maxmemory。</p>
<p><strong>fork 期間記憶體會短暫翻倍</strong>。RDB save 與 AOF rewrite 都靠 <code>fork()</code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article</a>。</p>
<p><code>maxmemory</code> 框住的是 <code>used_memory</code>，不是 <code>used_memory_rss</code>。所以 maxmemory 設成機器 RAM 的 100% 是錯的——碎片化、fork copy-on-write、client buffer 都在 maxmemory 之外，會把 RSS 推爆系統，觸發 Linux OOM killer 直接砍掉 Redis 進程（比 Redis 自己的 noeviction 更糟，因為是無預警 SIGKILL）。</p>
<h2 id="配置maxmemory-與-policy-的設定路徑">配置：maxmemory 與 policy 的設定路徑</h2>
<p>設定分兩步：先框住記憶體上限，再決定撞到上限時的行為。</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. 設定記憶體上限（留 headroom 給 fork / fragmentation / client buffer）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 機器 RAM 8GB → maxmemory 設 ~5-6GB、留 25-35% headroom</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">redis-cli CONFIG SET maxmemory 6gb
</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="c1"># 2. 設定撞到上限時的淘汰行為</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli CONFIG SET maxmemory-policy allkeys-lfu
</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"># 3. 永久化到 redis.conf（CONFIG SET 重啟後失效）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#   maxmemory 6gb</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#   maxmemory-policy allkeys-lfu</span></span></span></code></pre></div><p>八個 <code>maxmemory-policy</code> 選項分三類，選型靠「資料是不是全部都能淘汰」與「淘汰要靠存取頻率還是 TTL」兩個問題：</p>
<table>
  <thead>
      <tr>
          <th>policy</th>
          <th>淘汰範圍</th>
          <th>淘汰依據</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>noeviction</code></td>
          <td>不淘汰</td>
          <td>寫入直接報錯</td>
          <td>資料是 source-of-truth、不能丟（少見）</td>
      </tr>
      <tr>
          <td><code>allkeys-lru</code></td>
          <td>所有 key</td>
          <td>最近最少使用</td>
          <td>純 cache、無法預判哪些該留</td>
      </tr>
      <tr>
          <td><code>allkeys-lfu</code></td>
          <td>所有 key</td>
          <td>最少使用頻率</td>
          <td>純 cache、有明顯熱資料（多數 cache 場景）</td>
      </tr>
      <tr>
          <td><code>allkeys-random</code></td>
          <td>所有 key</td>
          <td>隨機</td>
          <td>key 存取均勻、省 LRU/LFU 計算</td>
      </tr>
      <tr>
          <td><code>volatile-lru</code></td>
          <td>有 TTL 的 key</td>
          <td>最近最少使用</td>
          <td>cache 與持久資料混存、只淘汰可過期的</td>
      </tr>
      <tr>
          <td><code>volatile-lfu</code></td>
          <td>有 TTL 的 key</td>
          <td>最少使用頻率</td>
          <td>同上、有熱資料</td>
      </tr>
      <tr>
          <td><code>volatile-random</code></td>
          <td>有 TTL 的 key</td>
          <td>隨機</td>
          <td>同上、省計算</td>
      </tr>
      <tr>
          <td><code>volatile-ttl</code></td>
          <td>有 TTL 的 key</td>
          <td>最接近過期的先淘汰</td>
          <td>想讓近期過期的提早讓位</td>
      </tr>
  </tbody>
</table>
<h3 id="lru-跟-lfu-的真實差異">LRU 跟 LFU 的真實差異</h3>
<p><code>allkeys-lru</code> 跟 <code>allkeys-lfu</code> 看起來像同一件事的兩種寫法，但選錯會在特定 workload 下讓 hit rate 掉一截。LRU 看「最後一次被存取是多久以前」，LFU 看「被存取的頻率」。差別在一次性掃描（scan pollution）：某個批次任務一次讀過大量冷 key，LRU 會把這些剛被碰過的冷 key 排到淘汰隊伍最後面，反而把真正的熱 key 擠出去。LFU 因為看頻率，一次性的存取不會讓冷 key 假裝成熱 key。</p>
<p>Redis 4.0 後的 LFU 用的是 probabilistic counter（Morris counter）加 decay，不是精確計數，靠兩個參數調：</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"># lfu-log-factor：counter 增長的對數速度、越大越能區分高頻 key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET lfu-log-factor <span class="m">10</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># lfu-decay-time：counter 衰減的分鐘數、越小越快遺忘舊熱度</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli CONFIG SET lfu-decay-time <span class="m">1</span></span></span></code></pre></div><p>對 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 這類有明顯熱資料</a>（熱門 profile、熱區域推薦池）的服務，<code>allkeys-lfu</code> 比 <code>allkeys-lru</code> 更能保護熱 key 不被批次掃描或冷流量擠出。</p>
<h3 id="approximate-eviction-的取樣">approximate eviction 的取樣</h3>
<p>Redis 的 LRU/LFU 都是近似演算法，不掃全 keyspace，而是每次取樣 <code>maxmemory-samples</code> 個 key（預設 5）挑最該淘汰的。樣本數越大越接近精確 LRU/LFU，但越吃 CPU。記憶體壓力大、淘汰頻繁時，預設 5 已夠；要更精準可調到 10，代價是淘汰路徑的 CPU 上升。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1noeviction-讓寫入全滅讀取假裝健康">Case 1：noeviction 讓寫入全滅、讀取假裝健康</h3>
<p><strong>徵兆</strong>：application 寫入路徑大量 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，但 <code>GET</code> 仍正常、health check（通常打 <code>PING</code> 或 <code>GET</code>）綠燈，on-call 收到的是 application 層的 500、不是 Redis 告警。</p>
<p><strong>根因</strong>：<code>maxmemory-policy</code> 預設是 <code>noeviction</code>。當 Redis 把 cache 當 cache 用，但 policy 留在 <code>noeviction</code>，記憶體一滿，所有會增加記憶體的命令（<code>SET</code>、<code>LPUSH</code>、<code>HSET</code>）直接報錯，唯讀命令照常。health check 若只測讀取，完全偵測不到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>純 cache 場景把 policy 改成 <code>allkeys-lru</code> 或 <code>allkeys-lfu</code>，讓記憶體壓力自動透過淘汰釋放</li>
<li>health check 加一個寫入探針（<code>SET healthcheck:probe &lt;ts&gt; EX 10</code>），讓 OOM 寫入失敗能被偵測</li>
<li>告警掛在 <code>used_memory / maxmemory &gt; 0.85</code>，不要等 OOM 才反應</li>
<li>若資料真的不能淘汰（誤把 Redis 當 source-of-truth），那不該用 cache 配置，見本文 Capacity / cost 邊界段的路由判斷</li>
</ol>
<h3 id="case-2碎片化吃掉-30-記憶體">Case 2：碎片化吃掉 30% 記憶體</h3>
<p><strong>徵兆</strong>：<code>used_memory</code> 顯示 4GB、但 <code>used_memory_rss</code> 是 5.5GB，<code>mem_fragmentation_ratio</code> 是 1.37，機器 RAM 開始吃緊但資料量沒漲。重啟 Redis 後 RSS 掉回 4GB 出頭。</p>
<p><strong>根因</strong>：大量寫入後刪除、或 value 大小頻繁變動（例如 list 一直 push/pop），jemalloc 的記憶體頁出現空洞——配出去的 page 還佔著 RSS，但裡面只有零星資料。<code>mem_fragmentation_ratio</code> 持續 &gt; 1.5 是明確訊號。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>開 active defrag 讓 Redis 在背景整理（4.0+）：</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">redis-cli CONFIG SET activedefrag yes
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET active-defrag-ignore-bytes 100mb
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET active-defrag-threshold-lower <span class="m">10</span></span></span></code></pre></div></li>
<li>
<p>fragmentation_ratio &lt; 1.0 是另一種警訊——代表 Redis 在用 swap，比碎片化更危險，要立刻降記憶體壓力</p>
</li>
<li>
<p>結構選擇上避免大幅波動的 collection；穩態大小的資料碎片化天然較低</p>
</li>
<li>
<p>計算 maxmemory headroom 時把 1.2-1.4 的 fragmentation 算進去</p>
</li>
</ol>
<h3 id="case-3一個-monitor-連線把記憶體推爆">Case 3：一個 MONITOR 連線把記憶體推爆</h3>
<p><strong>徵兆</strong>：某次 debug 後記憶體莫名持續上升，<code>used_memory_dataset</code> 沒變但 <code>used_memory</code> 一直漲，<code>CLIENT LIST</code> 看到一個連線的 <code>omem</code>（output buffer memory）有幾百 MB。</p>
<p><strong>根因</strong>：有人開了 <code>MONITOR</code> 去看即時命令流、然後忘了關（或 client crash 但連線沒斷）。<code>MONITOR</code> 把每一條命令都推給該連線，高 QPS 下 server 端 output buffer 爆量堆積，計入 <code>used_memory</code> 但不在 dataset。慢速 replica 或大量 pub/sub 訂閱者也會觸發同類問題。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>設定 client output buffer 上限，超過就斷線：</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"># normal client / replica / pubsub 分開設</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;normal 256mb 64mb 60&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;pubsub 32mb 8mb 60&#34;</span></span></span></code></pre></div></li>
<li>
<p><code>MONITOR</code> 在 production 嚴格禁用或限時，它本身也拖慢整個 server</p>
</li>
<li>
<p>監控加 <code>CLIENT LIST</code> 的 <code>omem</code> 巡檢，找出異常 buffer 的連線</p>
</li>
<li>
<p>replica lag 過大時 output buffer 會堆，對應 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">replication / failover deep article</a></p>
</li>
</ol>
<h3 id="case-4欄位設計讓記憶體多用三倍">Case 4：欄位設計讓記憶體多用三倍</h3>
<p><strong>徵兆</strong>：資料筆數跟預估一致，但 <code>used_memory</code> 是試算的 3 倍。<code>MEMORY USAGE &lt;key&gt;</code> 抽樣發現單筆 object 的記憶體遠超 value 本身的 byte 數。</p>
<p><strong>根因</strong>：把一個有 10 個欄位的 user object 拆成 10 個獨立 string key（<code>user:123:name</code>、<code>user:123:age</code>&hellip;），每個 key 都帶 Redis 的 key overhead（dict entry、expire dict entry、key 字串本身）。10 個 key 的 overhead 是一個 hash 的好幾倍。反過來，超過 <code>hash-max-listpack-entries</code> 的大 hash 從緊湊的 listpack 退化成 hashtable 編碼，也會放大記憶體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>同一 entity 的欄位用一個 hash 存，共享 key overhead</p>
</li>
<li>
<p>保持 hash 在 listpack 閾值內以用緊湊編碼：</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">redis-cli CONFIG GET hash-max-listpack-entries  <span class="c1"># 預設 128</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-value    <span class="c1"># 預設 64</span></span></span></code></pre></div></li>
<li>
<p>用 <code>MEMORY USAGE &lt;key&gt;</code> 跟 <code>redis-cli --bigkeys</code> 抽樣驗證實際記憶體，不靠試算</p>
</li>
<li>
<p><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">Shopify 的 serialization 遷移</a>（Marshal → MessagePack）正是用更省的編碼壓 payload，欄位編碼決策對記憶體與頻寬同時有效</p>
</li>
</ol>
<h3 id="case-5淘汰把熱-key-一起帶走hit-rate-崩">Case 5：淘汰把熱 key 一起帶走、hit rate 崩</h3>
<p><strong>徵兆</strong>：記憶體壓力下開始 eviction（<code>evicted_keys</code> 持續上升），同時 <code>keyspace_hits / (hits + misses)</code> 從 95% 掉到 70%，origin QPS 跟著飆，下游 DB 開始吃緊。</p>
<p><strong>根因</strong>：用了 <code>allkeys-random</code>，或 <code>allkeys-lru</code> 撞上批次掃描污染，淘汰演算法把熱 key 跟冷 key 一視同仁，熱 key 被淘汰後下一個請求 miss、回源、再寫回，形成淘汰與回填的拉鋸，hit rate 持續惡化。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>有明顯熱資料就用 <code>allkeys-lfu</code>，讓頻率高的 key 留下</li>
<li>把 maxmemory-samples 調到 10 提高淘汰精準度</li>
<li>根因常是記憶體真的不夠——<code>evicted_keys</code> 持續高代表 working set 超過 maxmemory，該擴容或分片，不是純調 policy 能解</li>
<li>熱 key 本身過熱（單 key QPS 遠超其他）要走 local cache + Redis 兩層，對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>記憶體配置的容量判讀，核心是「working set 對 maxmemory 的比值」與「淘汰是否健康」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>used_memory / maxmemory</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 85% 告警、&gt; 95% 接近 OOM 或大量淘汰</td>
      </tr>
      <tr>
          <td><code>mem_fragmentation_ratio</code></td>
          <td>1.0 - 1.5</td>
          <td>&gt; 1.5 開 active defrag、&lt; 1.0 在用 swap 要救火</td>
      </tr>
      <tr>
          <td><code>evicted_keys</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高 → working set 超量、該擴容 / 分片</td>
      </tr>
      <tr>
          <td>hit rate</td>
          <td>&gt; 90%（多數 cache）</td>
          <td>持續下滑 → 淘汰太兇或 TTL 太短</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足、降 maxmemory</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單機記憶體不夠、working set 持續超量</strong>：垂直擴容（換更大記憶體機型）是第一步，但有單機上限。超過後走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster 分片</a>，把 keyspace 切到多 node。</li>
<li><strong>想用 Redis API 但要極致單機記憶體效率</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 dashtable 在同 dataset 下通常比 Redis 省 20-40% 記憶體（依資料形狀、以官方 benchmark 為準），且單機多核能撐到 Redis 要靠 cluster 才能達到的規模——若 cluster re-sharding 頻繁觸發，評估直接遷 DragonflyDB 是否更省維運。</li>
<li><strong>資料其實不能淘汰（被當 source-of-truth）</strong>：那它不是 cache，該走 durable store。AWS 生態下用 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a>（Redis-compatible durable），或把正式狀態放回 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>記憶體與淘汰是 Redis 運維的第一層旋鈕，但它跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：fork 期間的 copy-on-write 是 maxmemory headroom 的主要消耗者，記憶體調校跟持久化調校必須一起看。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction 概念</a></strong>：TTL 設計決定哪些 key 帶過期時間，直接影響 <code>volatile-*</code> policy 的淘汰範圍。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">cache stampede</a></strong>：大量 key 同時被淘汰或同時過期會引發回源雪崩，eviction 調校要跟 TTL jitter / singleflight 一起設計。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB 遷到 ElastiCache，前提是「feature 可重新計算」——這個判斷決定了 eviction 是可接受的，記憶體調校才有意義。資料若不可重建，問題不在淘汰 policy，在選錯了儲存層。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>1.14 Production Slow Log Closed Loop</title><link>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式&lt;/a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：&lt;strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」&lt;/strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。&lt;/p>
&lt;h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法&lt;/h2>
&lt;p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。&lt;/p>
&lt;p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。&lt;/p>
&lt;p>兩種讀法的對比決定了 closed loop 的設計方向：&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>觸發時機&lt;/td>
 &lt;td>服務變慢時被動翻&lt;/td>
 &lt;td>排程定期掃&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>比較對象&lt;/td>
 &lt;td>跟絕對閾值比（query &amp;gt; 1 秒）&lt;/td>
 &lt;td>跟上週 / 上次 release 的 slow log 分布比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理路徑&lt;/td>
 &lt;td>找出 root cause → 立即修&lt;/td>
 &lt;td>收進 PR backlog → 排序 → 規律修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>介入點&lt;/td>
 &lt;td>事故發生後&lt;/td>
 &lt;td>反模式被引入後、影響使用者前&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應角色&lt;/td>
 &lt;td>On-call / SRE&lt;/td>
 &lt;td>整個團隊（每週輪流 review）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。&lt;/p>
&lt;h2 id="loop-第一步採集">Loop 第一步：採集&lt;/h2>
&lt;p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Threshold 設定&lt;/strong>：MySQL &lt;code>long_query_time&lt;/code>、PostgreSQL &lt;code>log_min_duration_statement&lt;/code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。&lt;/li>
&lt;li>&lt;strong>採集對象&lt;/strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。&lt;/li>
&lt;li>&lt;strong>Retention&lt;/strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。&lt;/li>
&lt;li>&lt;strong>Sample rate&lt;/strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。&lt;/li>
&lt;/ul>
&lt;p>採集出來的 raw log 不適合直接讀、要先 normalize。&lt;/p>
&lt;h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合&lt;/h2>
&lt;p>Raw slow log 每筆都帶具體參數（&lt;code>WHERE user_id = 12345&lt;/code>、&lt;code>WHERE user_id = 67890&lt;/code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。&lt;/p>
&lt;p>Normalize 動作把參數抽掉、留 query shape：&lt;/p>
&lt;ul>
&lt;li>&lt;code>WHERE user_id = 12345&lt;/code> → &lt;code>WHERE user_id = ?&lt;/code>&lt;/li>
&lt;li>&lt;code>IN (1, 2, 3, 4, 5)&lt;/code> → &lt;code>IN (?)&lt;/code>&lt;/li>
&lt;li>字串常數同樣抽掉&lt;/li>
&lt;/ul>
&lt;p>工具上：MySQL 用 &lt;code>pt-query-digest&lt;/code>（Percona Toolkit）；PostgreSQL 用 &lt;code>pg_stat_statements&lt;/code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：<strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」</strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。</p>
<h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法</h2>
<p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。</p>
<p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。</p>
<p>兩種讀法的對比決定了 closed loop 的設計方向：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>事故診斷工具</th>
          <th>定期審視訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發時機</td>
          <td>服務變慢時被動翻</td>
          <td>排程定期掃</td>
      </tr>
      <tr>
          <td>比較對象</td>
          <td>跟絕對閾值比（query &gt; 1 秒）</td>
          <td>跟上週 / 上次 release 的 slow log 分布比</td>
      </tr>
      <tr>
          <td>處理路徑</td>
          <td>找出 root cause → 立即修</td>
          <td>收進 PR backlog → 排序 → 規律修</td>
      </tr>
      <tr>
          <td>介入點</td>
          <td>事故發生後</td>
          <td>反模式被引入後、影響使用者前</td>
      </tr>
      <tr>
          <td>對應角色</td>
          <td>On-call / SRE</td>
          <td>整個團隊（每週輪流 review）</td>
      </tr>
  </tbody>
</table>
<p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。</p>
<h2 id="loop-第一步採集">Loop 第一步：採集</h2>
<p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：</p>
<ul>
<li><strong>Threshold 設定</strong>：MySQL <code>long_query_time</code>、PostgreSQL <code>log_min_duration_statement</code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。</li>
<li><strong>採集對象</strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。</li>
<li><strong>Retention</strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。</li>
<li><strong>Sample rate</strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。</li>
</ul>
<p>採集出來的 raw log 不適合直接讀、要先 normalize。</p>
<h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合</h2>
<p>Raw slow log 每筆都帶具體參數（<code>WHERE user_id = 12345</code>、<code>WHERE user_id = 67890</code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。</p>
<p>Normalize 動作把參數抽掉、留 query shape：</p>
<ul>
<li><code>WHERE user_id = 12345</code> → <code>WHERE user_id = ?</code></li>
<li><code>IN (1, 2, 3, 4, 5)</code> → <code>IN (?)</code></li>
<li>字串常數同樣抽掉</li>
</ul>
<p>工具上：MySQL 用 <code>pt-query-digest</code>（Percona Toolkit）；PostgreSQL 用 <code>pg_stat_statements</code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。</p>
<p>聚合後產出三條訊號：</p>
<ol>
<li><strong>Top-N by total time</strong>：累計時間最長的 query — 改一條就能省最多 DB 壓力</li>
<li><strong>Top-N by count</strong>：出現次數最多的 query — 改一條就能降最多 connection 占用</li>
<li><strong>Top-N by avg latency</strong>：平均延遲最高的 query — 個別 request 體驗最差的</li>
</ol>
<p>三條訊號可能指向不同 query、各自值得 attention。</p>
<h2 id="loop-第三步pr-review-整合">Loop 第三步：PR review 整合</h2>
<p>把 slow log 的 top-N 帶回 PR review 是 closed loop 的關鍵。常見三種整合機制：</p>
<ul>
<li><strong>每週 slow log review 會議</strong>：固定時段（每週 30 分鐘）、團隊輪流 owner、把 top-10 過一輪、決定每筆是修 / 留 / 標 acceptable。產出進 backlog、不是當場修。</li>
<li><strong>PR-level query budget check</strong>：CI 加 middleware 統計每個 endpoint 的 query 數（per <a href="/blog/backend/01-database/query-anti-patterns/#%e6%af%8f%e8%ab%8b%e6%b1%82%e7%9a%84-query-%e9%a0%90%e7%ae%97" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 預算</a>）、超過閾值的 PR 在 review 時觸發討論。這層比 slow log 早、catch 的是「新引入」反模式。</li>
<li><strong>Production regression alert</strong>：當某個 query shape 的 P99 latency 比上週 baseline 偏高 50%+、自動發 alert 給該服務 owner。這層 catch 的是「漸進惡化」反模式（如資料量增加、index 失效）。</li>
</ul>
<p>三層機制按介入點分層：PR check 是「進 production 前」、weekly review 是「進 production 後的固定盤點」、regression alert 是「漸進惡化的訊號偵測」。三層覆蓋率最高、單跑任一層都會漏。</p>
<h2 id="loop-第四步regression-偵測">Loop 第四步：Regression 偵測</h2>
<p>Slow log 的對比基線需要主動維護。沒有基線、定期審視會退化成「每次都看到同樣的 top-10、習以為常」。建立基線的常見做法：</p>
<ul>
<li><strong>每 release 凍結 baseline</strong>：上線新版本前抓一份 slow log snapshot、release 後跟它比。新增的 query shape 跟惡化的 query shape 都會浮出來。</li>
<li><strong>資料量分位點 marker</strong>：在 schema 加註「這張表預期 1M / 10M / 100M 行的 query 計畫」、實際成長到對應規模時驗證 plan 是否還對。Index 失效常常是「資料量過某個門檻、optimizer 改用 full scan」造成的。</li>
<li><strong>跨 release 趨勢圖</strong>：把 slow log top-10 的累計時間做時序圖、看一年的趨勢。穩定升高代表反模式 / 資料成長壓力、突然升高代表新引入問題。</li>
</ul>
<p>Regression 偵測的 false-positive 風險是「業務本身在變、流量本身在長」、不是反模式造成的。用「query shape 佔比」而非「絕對延遲」當訊號可以降低 false positive — 某個 query shape 從佔 5% 變成佔 30%，不論絕對延遲是否升高、都值得審視。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slow log top-10 一直是同一批 query</td>
          <td>Closed loop 沒形成、review 退化成擺設</td>
          <td>啟動 PR-level query budget check 或 weekly review</td>
      </tr>
      <tr>
          <td>某個 query shape 突然從 top-100 升到 top-10</td>
          <td>新版本引入反模式 / 流量結構變化</td>
          <td>對照最近 release diff、找出引入時點</td>
      </tr>
      <tr>
          <td>Top-N 累計時間穩定升高、但 query shape 沒變</td>
          <td>資料量增加、index 退化或 query 計畫漂移</td>
          <td>EXPLAIN 對比、檢查是否該加 covering index 或 partition</td>
      </tr>
      <tr>
          <td>Slow log 異常稀少（&lt; 預期）</td>
          <td>Threshold 設太寬、或採集 sample rate 太低</td>
          <td>降 threshold、提高 sample rate</td>
      </tr>
      <tr>
          <td>同一個 endpoint 在 PR check 過、production 卻爆</td>
          <td>PR 環境資料量太小、CI 無法 catch 大資料量退化</td>
          <td>加 production-like load test、或在 CI 用 anonymized prod data</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 slow log 當「事故工具」、不做定期審視。事故時的 slow log 是 lagging indicator — 反模式已經影響使用者一段時間才被看見。定期審視是把它變成 leading indicator 的關鍵。</p>
<p>把 threshold 設太鬆（1 秒、5 秒）。多數反模式落在 100ms-1s 區間、設 1 秒會漏掉。Threshold 應該對齊「user-perceived 慢」門檻、通常 100-500ms。</p>
<p>把 top-10 當「不能動」。一些 top-10 是業務本質慢（複雜 report、bulk write）、改起來代價遠超效益。Review 時要明示標記「acceptable」、避免下週又被當未解決問題討論。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「production slow log 怎麼變成 closed loop」。當問題進入具體反模式分析（這條 query 是哪種反模式？怎麼改？）、回到 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>；進入 EXPLAIN 解讀細節、回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>；進入 application-side query 數量控制機制（ORM middleware、query log 觀察），跨到 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 模組。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫中、slow log closed loop 直接示範的案例稀少（多數案例談規模 / vendor、不談 ops loop 設計）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：Aurora Postgres 寫入瓶頸</a> — 寫入飽和被識別為 vendor 層問題、但若 production slow log loop 早期就 catch 到 transaction 範圍跟熱 row 競爭、可能延後遷移時點。對照本章可問：DoorDash 在啟動遷移前、是否有定期 slow log review 機制？</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered：合規驅動容量規劃</a> — 容量規劃以合規為驅動、但 query 預算假設若無 production 驗證、規劃出的 TPS 上限會偏低。對照本章「Regression 偵測」段：合規 cluster 是否有 query shape 趨勢圖？</li>
</ul>
<p>反向追問框架（per <a href="/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146</a>）：案例本身不直接示範 closed loop、但用「啟動 vendor 升級前、closed loop 能不能延後撞牆」這條追問、能看出 slow log loop 的事前價值。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 的交接：1.13 給反模式清單、本章給「定期 catch 它們」的機制。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 的交接：slow log 採集跟聚合是 observability 的子問題、跨服務的 query trace 需要 04 的 telemetry pipeline。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位</a> 的交接：9.5 用 USE / RED method 定位、本章用 slow log 在 DB 層做更精細的 query-level 定位。</li>
<li>與 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">06 reliability ci-pipeline</a> 的交接：PR-level query budget check 是 CI 環節、屬 06 模組的 release gate 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看具體反模式怎麼修、回 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>。要把 query 觀測接進完整 telemetry pipeline、進 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a>。要看 PR-level check 怎麼接 release gate、進 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
]]></content:encoded></item><item><title>9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提&lt;/a> 指出了水平擴展應用層時的隱性成本之一：連線池放大 — 100 臺機器 × 每臺 10 個連線 = 對 DB 開 1000 個連線、超過 PostgreSQL &lt;code>max_connections&lt;/code> default（100）十倍。本章把這條撞牆訊號的具體解法說清楚 — connection pooler 是什麼、PgBouncer / RDS Proxy / ProxySQL 怎麼選、不同場景的取捨。&lt;/p>
&lt;h2 id="連線池放大的物理本質">連線池放大的物理本質&lt;/h2>
&lt;p>PostgreSQL / MySQL 每個連線都會在 DB server 端配一個 backend process / thread。Backend 佔 5-15 MB 記憶體、context switch 也有成本。當應用層連線數超過 DB 機器能負擔的數量，會出現三類問題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>記憶體吃光&lt;/strong>：500 個 backend × 10 MB = 5 GB、再加 shared buffer、可能直接 OOM&lt;/li>
&lt;li>&lt;strong>Context switch 抖動&lt;/strong>：上百個 backend 競爭 CPU、上下文切換 overhead 變成主要消耗&lt;/li>
&lt;li>&lt;strong>連線建立失敗&lt;/strong>：超過 &lt;code>max_connections&lt;/code> 後、新請求拿不到連線、即使現有連線多數 idle&lt;/li>
&lt;/ul>
&lt;p>問題的根因不是「連線多」、是「連線&lt;strong>生命週期跟使用率不對齊&lt;/strong>」。應用層 connection pool 通常維持「每臺機器 N 個常駐連線、避免每個 request 重新建連」、但 100 臺機器各自 keep 10 個常駐就是 1000 個 idle 連線。&lt;/p>
&lt;p>解法的方向不是「砍應用層連線數」（會讓 connection acquisition 變慢、影響 latency）、是「在 DB 跟應用層之間放一層 multiplexer」— 把多個應用層連線複用到少數 DB 連線上。這層中介就是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pooler/" data-link-title="Connection Pooler" data-link-desc="應用層跟資料庫之間的連線複用中介層、解水平擴展時的連線數放大問題">connection pooler&lt;/a>。&lt;/p>
&lt;h2 id="connection-pooler-三大選項">Connection Pooler 三大選項&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>部署模式&lt;/th>
 &lt;th>主要適用 DB&lt;/th>
 &lt;th>主要特點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>PgBouncer&lt;/td>
 &lt;td>Self-managed / sidecar&lt;/td>
 &lt;td>PostgreSQL only&lt;/td>
 &lt;td>輕量（C 寫的 single process）、三種 pooling 模式可選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS RDS Proxy&lt;/td>
 &lt;td>Managed&lt;/td>
 &lt;td>RDS / Aurora (PG / MySQL)&lt;/td>
 &lt;td>整合 IAM auth、自動 failover、計價 per vCPU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ProxySQL&lt;/td>
 &lt;td>Self-managed&lt;/td>
 &lt;td>MySQL&lt;/td>
 &lt;td>規則型 routing、可做 query rewriting、自動 failover&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="pgbouncer--三種-pooling-模式決定一切">PgBouncer — 三種 pooling 模式決定一切&lt;/h3>
&lt;p>PgBouncer 的核心參數是 &lt;code>pool_mode&lt;/code>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Session mode&lt;/strong>：應用層 client 拿到的連線、跟 DB backend 1:1 綁定、整個 session 結束才釋放。其實沒做 multiplexing、只是 connection caching。&lt;/li>
&lt;li>&lt;strong>Transaction mode&lt;/strong>：每個 transaction 結束、應用層 client 的連線釋放回 pool、下個 transaction 再分配 DB backend。multiplexing 比較強、但&lt;strong>不支援 transaction-scoped state&lt;/strong>（如 &lt;code>SET LOCAL&lt;/code>、prepared statement、temporary table）。&lt;/li>
&lt;li>&lt;strong>Statement mode&lt;/strong>：每個 statement 結束就釋放、最強 multiplexing 但&lt;strong>不支援 transaction&lt;/strong>。極少用、只在純 stateless query workload 適用。&lt;/li>
&lt;/ul>
&lt;p>Transaction mode 是多數場景的 default。但要注意：應用層的 ORM / driver 可能默認用 prepared statement、跟 transaction mode 衝突。PostgreSQL 14+ 的 protocol-level prepared statement 才相容、JDBC / asyncpg 等需要特別配置。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a> 指出了水平擴展應用層時的隱性成本之一：連線池放大 — 100 臺機器 × 每臺 10 個連線 = 對 DB 開 1000 個連線、超過 PostgreSQL <code>max_connections</code> default（100）十倍。本章把這條撞牆訊號的具體解法說清楚 — connection pooler 是什麼、PgBouncer / RDS Proxy / ProxySQL 怎麼選、不同場景的取捨。</p>
<h2 id="連線池放大的物理本質">連線池放大的物理本質</h2>
<p>PostgreSQL / MySQL 每個連線都會在 DB server 端配一個 backend process / thread。Backend 佔 5-15 MB 記憶體、context switch 也有成本。當應用層連線數超過 DB 機器能負擔的數量，會出現三類問題：</p>
<ul>
<li><strong>記憶體吃光</strong>：500 個 backend × 10 MB = 5 GB、再加 shared buffer、可能直接 OOM</li>
<li><strong>Context switch 抖動</strong>：上百個 backend 競爭 CPU、上下文切換 overhead 變成主要消耗</li>
<li><strong>連線建立失敗</strong>：超過 <code>max_connections</code> 後、新請求拿不到連線、即使現有連線多數 idle</li>
</ul>
<p>問題的根因不是「連線多」、是「連線<strong>生命週期跟使用率不對齊</strong>」。應用層 connection pool 通常維持「每臺機器 N 個常駐連線、避免每個 request 重新建連」、但 100 臺機器各自 keep 10 個常駐就是 1000 個 idle 連線。</p>
<p>解法的方向不是「砍應用層連線數」（會讓 connection acquisition 變慢、影響 latency）、是「在 DB 跟應用層之間放一層 multiplexer」— 把多個應用層連線複用到少數 DB 連線上。這層中介就是 <a href="/blog/backend/knowledge-cards/connection-pooler/" data-link-title="Connection Pooler" data-link-desc="應用層跟資料庫之間的連線複用中介層、解水平擴展時的連線數放大問題">connection pooler</a>。</p>
<h2 id="connection-pooler-三大選項">Connection Pooler 三大選項</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>部署模式</th>
          <th>主要適用 DB</th>
          <th>主要特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PgBouncer</td>
          <td>Self-managed / sidecar</td>
          <td>PostgreSQL only</td>
          <td>輕量（C 寫的 single process）、三種 pooling 模式可選</td>
      </tr>
      <tr>
          <td>AWS RDS Proxy</td>
          <td>Managed</td>
          <td>RDS / Aurora (PG / MySQL)</td>
          <td>整合 IAM auth、自動 failover、計價 per vCPU</td>
      </tr>
      <tr>
          <td>ProxySQL</td>
          <td>Self-managed</td>
          <td>MySQL</td>
          <td>規則型 routing、可做 query rewriting、自動 failover</td>
      </tr>
  </tbody>
</table>
<h3 id="pgbouncer--三種-pooling-模式決定一切">PgBouncer — 三種 pooling 模式決定一切</h3>
<p>PgBouncer 的核心參數是 <code>pool_mode</code>：</p>
<ul>
<li><strong>Session mode</strong>：應用層 client 拿到的連線、跟 DB backend 1:1 綁定、整個 session 結束才釋放。其實沒做 multiplexing、只是 connection caching。</li>
<li><strong>Transaction mode</strong>：每個 transaction 結束、應用層 client 的連線釋放回 pool、下個 transaction 再分配 DB backend。multiplexing 比較強、但<strong>不支援 transaction-scoped state</strong>（如 <code>SET LOCAL</code>、prepared statement、temporary table）。</li>
<li><strong>Statement mode</strong>：每個 statement 結束就釋放、最強 multiplexing 但<strong>不支援 transaction</strong>。極少用、只在純 stateless query workload 適用。</li>
</ul>
<p>Transaction mode 是多數場景的 default。但要注意：應用層的 ORM / driver 可能默認用 prepared statement、跟 transaction mode 衝突。PostgreSQL 14+ 的 protocol-level prepared statement 才相容、JDBC / asyncpg 等需要特別配置。</p>
<h3 id="aws-rds-proxy--managed-換掉運維">AWS RDS Proxy — managed 換掉運維</h3>
<p>RDS Proxy 是 PgBouncer / ProxySQL 同類功能的 managed 版本：AWS 負責部署、HA、failover、IAM 整合。應用層連到 RDS Proxy endpoint、Proxy 在背後維持跟 RDS / Aurora 的連線池。</p>
<p>特點：</p>
<ul>
<li><strong>連線 share 模式類似 transaction mode</strong>：自動 detect 連線是否在 transaction、空閒時釋放</li>
<li><strong>IAM auth 整合</strong>：應用層用 IAM token、不用維護 DB password</li>
<li><strong>Failover 加速</strong>：DB failover 時 Proxy 維持應用層連線不斷、background 重連 new primary。Failover 期間應用層感受最小化。</li>
<li><strong>計價</strong>：per vCPU-hour、Aurora 約 $0.015/vCPU-hr、RDS 約 $0.02/vCPU-hr — 加在 RDS 計價上面</li>
</ul>
<p>不適用場景：很多 read-only / analytics workload 不需要 connection pooler、純讀 replica 直接連通常更便宜。RDS Proxy 是給「寫入混合」「連線抖動嚴重」這類場景。</p>
<h3 id="proxysql--mysql-規則型-routing">ProxySQL — MySQL 規則型 routing</h3>
<p>ProxySQL 是 MySQL 生態的 connection pooler、但比 PgBouncer 更全功能：</p>
<ul>
<li><strong>Query routing rules</strong>：可以按 query pattern 把 query 導去不同 backend（讀路徑去 replica、寫路徑去 primary、特定 query 強制 cache）</li>
<li><strong>Connection multiplexing</strong>：類似 PgBouncer transaction mode</li>
<li><strong>Query rewriting</strong>：可以攔截 query 改寫（debug / 漸進遷移 schema）</li>
<li><strong>Auto failover</strong>：監控 backend 健康、自動切流</li>
</ul>
<p>ProxySQL 的代價是學習曲線跟運維成本 — 規則設計需要對 query pattern 跟 DB topology 有掌控、設錯規則會把 query 導去錯誤 backend、debug 困難。</p>
<h2 id="選型對照">選型對照</h2>
<p>實務選型的關鍵變數是「DB 廠商 / managed 程度 / 規模 / 預算」：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>推薦</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS RDS / Aurora、團隊不想自管</td>
          <td>RDS Proxy</td>
          <td>Managed、整合度高、failover 加速是 free value</td>
      </tr>
      <tr>
          <td>AWS RDS / Aurora、需要極致省成本</td>
          <td>PgBouncer（PG）/ ProxySQL（MySQL）on EC2</td>
          <td>比 RDS Proxy 便宜、但要自管 HA</td>
      </tr>
      <tr>
          <td>GCP Cloud SQL / 自管 PostgreSQL</td>
          <td>PgBouncer</td>
          <td>PG 生態事實標準、配置文件多</td>
      </tr>
      <tr>
          <td>Azure Database for PostgreSQL</td>
          <td>PgBouncer 或 Azure 內建 connection pooling</td>
          <td>Azure 部分 SKU 內建類似功能、檢查 vendor 文件</td>
      </tr>
      <tr>
          <td>MySQL 需要讀寫分離 + query routing</td>
          <td>ProxySQL</td>
          <td>規則型 routing 是 ProxySQL 強項</td>
      </tr>
      <tr>
          <td>不確定要不要 connection pooler</td>
          <td>先用 vendor 內建（RDS Proxy / PG managed pooler）跑一段、再評估自管</td>
          <td>降低初期決策成本</td>
      </tr>
  </tbody>
</table>
<h2 id="不裝-pooler-的判讀">不裝 pooler 的判讀</h2>
<p>Connection pooler 不是必要 — 在以下情境可以暫時不裝：</p>
<ul>
<li><strong>應用層機器數 &lt; 10</strong>：對 DB 連線總數壓力小、deferred 安裝 pooler 沒問題</li>
<li><strong>每臺機器連線數 &lt; 5</strong>：應用層 connection pool 已經很省、再加 pooler 改善有限</li>
<li><strong>DB 機器規格大、<code>max_connections</code> 充裕</strong>：高階 RDS instance 可開到 5000-10000 連線、有 buffer 之前不必加 pooler</li>
<li><strong>Workload 全是長 transaction</strong>：transaction mode pooler 在這種 workload 跟 session mode 沒差、收益低</li>
</ul>
<p>該裝 pooler 的訊號是相反：應用層機器數 ≥ 20、每臺連線數 ≥ 10、<code>max_connections</code> 使用率 ≥ 70%、或 P99 connection wait time 升高。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB <code>pg_stat_activity</code> 顯示大量 idle 連線</td>
          <td>應用層 keep-alive 連線、實際使用率低</td>
          <td>加 connection pooler 把 idle 釋放回 DB</td>
      </tr>
      <tr>
          <td>應用層 connection acquisition 等待時間升高</td>
          <td>應用層 pool 太小、或 DB 連線數已撞 <code>max_connections</code></td>
          <td>加 pooler 把連線總數壓低、應用層 pool size 維持原樣</td>
      </tr>
      <tr>
          <td>DB failover 後應用層 5-10 分鐘錯誤率高</td>
          <td>應用層 connection pool 沒 detect 到 backend 切換</td>
          <td>RDS Proxy 的 failover 加速、或應用層 connection validation 加強</td>
      </tr>
      <tr>
          <td>Pooler 上線後出現「unexpected error」</td>
          <td>transaction mode 跟 prepared statement / SET LOCAL 衝突</td>
          <td>改 ORM 配置、用 protocol-level prepared statement 或避開 SET LOCAL</td>
      </tr>
      <tr>
          <td>應用層 N+1 query 仍然存在</td>
          <td>Pooler 沒解 N+1、它只解連線數放大</td>
          <td>回 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 修反模式</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 connection pooler 當「N+1 解藥」。Pooler 解的是「連線數放大」、不是「query 數量過多」。N+1 query 在裝完 pooler 後仍然慢、只是 DB 不會因為連線爆掉而當機。兩個是正交問題、各自要解。</p>
<p>把 RDS Proxy 當「免費功能」。Proxy 的計價跟 RDS / Aurora 本體疊加、高 connection volume 場景 Proxy 成本可能可觀。要算實際的 cost-per-request、不是預設「managed 一定值得」。</p>
<p>把 transaction mode 配置當「裝完就好」。Prepared statement / SET LOCAL / temporary table 都會跟 transaction mode 衝突、ORM 預設行為要 audit 過、不然會在 production 出現難 debug 的「query 隨機失敗」。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「連線池放大的解法」。當問題進入擴展軸選擇（要垂直 vs 水平？stateful 前提？）、回 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>；進入 DB 本身的容量規劃（要多大規格 instance？要不要 read replica？）、進 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a>；進入 application-level connection 設計（per-request pool / persistent pool）、進 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫多數案例規模到 connection pool 已是 secondary concern、但兩個案例有對應參考：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — Zoom 把 stateful 資料層改用 DynamoDB、繞過 SQL connection pool 問題（KV 沒有 backend process 概念）。對照本章可問：若 Zoom 保留 SQL、connection pool 怎麼設計才撐得住 30 倍突發？</li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：CockroachDB 多主寫入</a> — DoorDash 從 Aurora single-primary 換成 CockroachDB 多主、connection pool 設計從「集中在 primary」變成「分散在多 node」。對照本章可問：CockroachDB 是否仍需要 connection pooler？</li>
</ul>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：9.13 提出隱性成本、本章給具體解法。</li>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL 讀寫邊界</a> 的交接：1.1 講應用層 connection pool 設計、本章補 DB 端 pooler 中介層。</li>
<li>與 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01 vendors</a> 的交接：各 DB vendor 的內建 pooler 能力詳見 vendor deep article。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：pooler 加上後、DB 容量規劃的單位從「連線數」變成「DB backend 數 + Pooler vCPU」。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看擴展軸選擇的完整 framing、回 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a>。要看 DB-side 高併發處理、進 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL 讀寫邊界</a>。要看具體 vendor 的 pooler 文件、進對應 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendor deep article</a>。</p>
]]></content:encoded></item><item><title>MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/proxysql-config/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/proxysql-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>ProxySQL 配置&lt;/em> — connection pool + query routing 的 4 段 lifecycle 跟 rule chain 設計。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="proxysql-lifecycle每個-query-走-4-段">ProxySQL Lifecycle：每個 query 走 4 段&lt;/h2>
&lt;p>從 application 連 ProxySQL 到拿到 response、每個 query 都走完整 4 段：&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">1. Connection 接入 → application connect 到 ProxySQL（不是 MySQL）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. Query parse + rule match → ProxySQL 解析 query、match query rule chain
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. Backend route → 決定走哪個 hostgroup（primary / replica）+ 哪個 server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. Response 返回 → 將 result set 回 application、connection 可被 reuse&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每段都有獨立配置 + failure mode + 觀測 metric。ProxySQL 不是 &lt;em>簡單的 connection pool&lt;/em>、是 &lt;em>query-aware proxy&lt;/em> — 看得到 SQL 內容才能做 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">read/write split&lt;/a>、replica lag-aware routing、query mirroring。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer&lt;/a> 比、pgBouncer 是 &lt;em>transaction-level pool&lt;/em>（只看連線、不看 SQL）、ProxySQL 是 &lt;em>query-level proxy&lt;/em>（看 SQL、做 routing decision）。能力不同、target use case 不同。&lt;/p>
&lt;h2 id="stage-1connection-接入--hostgroup--server--user-三層-schema">Stage 1：Connection 接入 — Hostgroup / Server / User 三層 schema&lt;/h2>
&lt;p>ProxySQL 不直接 expose backend MySQL、用 &lt;em>hostgroup&lt;/em> 作為 routing 抽象。Application 不知道有幾個 backend、只知道 ProxySQL。&lt;/p>
&lt;p>&lt;strong>核心 table（在 &lt;code>main&lt;/code> database）&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Table&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>mysql_servers&lt;/code>&lt;/td>
 &lt;td>列每個 backend MySQL server、屬於哪個 hostgroup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_replication_hostgroups&lt;/code>&lt;/td>
 &lt;td>定義 writer hostgroup ↔ reader hostgroup 配對、自動偵測 primary 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_users&lt;/code>&lt;/td>
 &lt;td>列允許連 ProxySQL 的 application user、預設 hostgroup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_query_rules&lt;/code>&lt;/td>
 &lt;td>Query rule chain、決定哪個 query 走哪個 hostgroup&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>典型部署&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>ProxySQL 配置</em> — connection pool + query routing 的 4 段 lifecycle 跟 rule chain 設計。</p></blockquote>
<hr>
<h2 id="proxysql-lifecycle每個-query-走-4-段">ProxySQL Lifecycle：每個 query 走 4 段</h2>
<p>從 application 連 ProxySQL 到拿到 response、每個 query 都走完整 4 段：</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. Connection 接入        →  application connect 到 ProxySQL（不是 MySQL）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Query parse + rule match  → ProxySQL 解析 query、match query rule chain
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Backend route          →  決定走哪個 hostgroup（primary / replica）+ 哪個 server
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Response 返回          →  將 result set 回 application、connection 可被 reuse</span></span></code></pre></div><p>每段都有獨立配置 + failure mode + 觀測 metric。ProxySQL 不是 <em>簡單的 connection pool</em>、是 <em>query-aware proxy</em> — 看得到 SQL 內容才能做 <a href="/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">read/write split</a>、replica lag-aware routing、query mirroring。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer</a> 比、pgBouncer 是 <em>transaction-level pool</em>（只看連線、不看 SQL）、ProxySQL 是 <em>query-level proxy</em>（看 SQL、做 routing decision）。能力不同、target use case 不同。</p>
<h2 id="stage-1connection-接入--hostgroup--server--user-三層-schema">Stage 1：Connection 接入 — Hostgroup / Server / User 三層 schema</h2>
<p>ProxySQL 不直接 expose backend MySQL、用 <em>hostgroup</em> 作為 routing 抽象。Application 不知道有幾個 backend、只知道 ProxySQL。</p>
<p><strong>核心 table（在 <code>main</code> database）</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Table</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>mysql_servers</code></td>
          <td>列每個 backend MySQL server、屬於哪個 hostgroup</td>
      </tr>
      <tr>
          <td><code>mysql_replication_hostgroups</code></td>
          <td>定義 writer hostgroup ↔ reader hostgroup 配對、自動偵測 primary 切換</td>
      </tr>
      <tr>
          <td><code>mysql_users</code></td>
          <td>列允許連 ProxySQL 的 application user、預設 hostgroup</td>
      </tr>
      <tr>
          <td><code>mysql_query_rules</code></td>
          <td>Query rule chain、決定哪個 query 走哪個 hostgroup</td>
      </tr>
  </tbody>
</table>
<p><strong>典型部署</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 進 ProxySQL admin (6032 port)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">mysql</span><span class="w"> </span><span class="o">-</span><span class="n">uadmin</span><span class="w"> </span><span class="o">-</span><span class="n">padmin</span><span class="w"> </span><span class="o">-</span><span class="n">h127</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="n">P6032</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 設 2 個 hostgroup：10=writer、20=reader
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_servers</span><span class="p">(</span><span class="n">hostgroup_id</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">weight</span><span class="p">,</span><span class="w"> </span><span class="n">max_connections</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;primary.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">200</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;replica1.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;replica2.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 自動偵測 primary（用 read_only flag）
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_replication_hostgroups</span><span class="p">(</span><span class="n">writer_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">reader_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="k">comment</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;production cluster&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">-- 設 application user、預設走 reader（保守）
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_users</span><span class="p">(</span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="p">,</span><span class="w"> </span><span class="n">default_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">max_connections</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;app&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;app_password&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="c1">-- 套用設定到 runtime
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">USERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="c1">-- 持久化到 disk（重啟保留）
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">USERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p>注意 ProxySQL 的 <em>三層 state</em>：<code>disk</code>（持久化）→ <code>memory</code>（編輯區）→ <code>runtime</code>（實際運作）。每次改完要 <code>LOAD ... TO RUNTIME</code> 才生效、<code>SAVE ... TO DISK</code> 才能 reboot 保留。沒 <code>SAVE</code> 重啟後 config 消失是新手最常踩的雷。</p>
<h2 id="stage-2query-parse--rule-match--query-rule-engine">Stage 2：Query Parse + Rule Match — query rule engine</h2>
<p>ProxySQL 不只 forward connection、看 <em>SQL 內容</em> 決定怎麼 route。Query rule 是 <em>ordered chain</em>、match 第一個符合的 rule。</p>
<p><strong>Query rule 核心欄位</strong>：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>rule_id</code></td>
          <td>排序（越小越先 match）</td>
      </tr>
      <tr>
          <td><code>match_pattern</code></td>
          <td>regex 比對 SQL（支援 <code>^SELECT</code> / <code>FOR UPDATE</code> 等）</td>
      </tr>
      <tr>
          <td><code>destination_hostgroup</code></td>
          <td>match 後送哪個 hostgroup</td>
      </tr>
      <tr>
          <td><code>apply</code></td>
          <td>match 後是否停 chain（1=stop、0=繼續看後面 rule）</td>
      </tr>
      <tr>
          <td><code>cache_ttl</code></td>
          <td>result cache TTL（毫秒）— ProxySQL 內建 query cache</td>
      </tr>
      <tr>
          <td><code>mirror_hostgroup</code></td>
          <td>query 鏡像送到第二個 hostgroup（不等 response、用於 shadow test）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型讀寫分離 rule</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Rule 100: SELECT ... FOR UPDATE 必須走 primary
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT.*FOR UPDATE$&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- Rule 200: 一般 SELECT 走 replica（reader）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- Rule 300: BEGIN / START TRANSACTION 走 primary
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">300</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^(BEGIN|START TRANSACTION)&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 其他（INSERT / UPDATE / DELETE）預設走 default_hostgroup（user 設的）
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- application user default 設 10 (writer)、所以寫入自動走 primary
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p><strong>Rule 順序很重要</strong>：<code>rule_id</code> 100 先 match、200 再 match、依此類推。Rule 200 比 100 寬鬆（任何 SELECT）、所以 <code>FOR UPDATE</code> 必須先 match rule 100 才不會誤送 replica。</p>
<h2 id="stage-3backend-route--replica-lag-aware--circuit-breaker">Stage 3：Backend Route — replica lag-aware + circuit breaker</h2>
<p>Rule match 後 ProxySQL 從 hostgroup 內挑一個 server。Backend selection 不是 pure round-robin、考慮：</p>
<ul>
<li><em>Weight</em>：每個 server <code>weight</code> 比例分配（典型用於 replica capacity 不同）</li>
<li><em>Replica lag</em>：若 hostgroup 設 <code>max_replication_lag</code>、lag 超過 threshold 的 replica 自動暫時退出</li>
<li><em>Connection count</em>：避免某個 server connection 滿</li>
<li><em>Server status</em>：<code>mysql_servers.status</code> (ONLINE / SHUNNED / OFFLINE_SOFT / OFFLINE_HARD) 決定是否可用</li>
</ul>
<p><strong>Replica lag-aware routing 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 給整個 reader hostgroup 設 lag threshold
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">mysql_servers</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">max_replication_lag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">  </span><span class="c1">-- 秒
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">hostgroup_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span></span></span></code></pre></div><p>ProxySQL 內部用 <em>monitor module</em> 定期跑 <code>SHOW SLAVE STATUS</code>、lag 超過 5 秒 → 該 replica 暫時退出 reader hostgroup。讀 query 自動避開 lagging replica。</p>
<p><strong>Circuit breaker（自動 shun）</strong>：server 連續失敗 → ProxySQL 自動 <code>SHUNNED</code>、避免持續打 broken server。但 <em>application 層仍要處理 retry</em>、ProxySQL 不保證 query 100% 成功。</p>
<h2 id="stage-4response-返回--connection-multiplexing">Stage 4：Response 返回 — connection multiplexing</h2>
<p>ProxySQL 對 application connection 跟 backend connection 是 <em>N:M 多工</em>：</p>
<ul>
<li>Application connection 跟 ProxySQL 1:1</li>
<li>ProxySQL 跟 backend MySQL connection 共用 pool（multiplexing）</li>
</ul>
<p><strong>Multiplexing 條件</strong>：</p>
<ul>
<li>Transaction 內：connection 綁定特定 backend（保 transaction atomicity）</li>
<li>跨 transaction：connection 可以換 backend</li>
<li><code>SET</code> statement 改 session variable：connection 黏死 backend（防 session state leak）</li>
<li>User variable（<code>@var</code>）：connection 黏死 backend</li>
</ul>
<p><strong>結果</strong>：application 看到的是「自己有 1000 個 connection」、ProxySQL 後端可能只有 100 connection 到 MySQL。對 connection-bound MySQL（max_connections 限制）是關鍵 cost saving。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-query-rule-順序錯亂--for-update-被-select-route-到-replica">1. Query rule 順序錯亂 — <code>FOR UPDATE</code> 被 SELECT route 到 replica</h3>
<p>Rule 200（<code>^SELECT</code>）寫在 rule 100（<code>^SELECT.*FOR UPDATE$</code>）之前、ProxySQL match 第一個 rule（rule 200）就停、<code>SELECT ... FOR UPDATE</code> 被送 replica、replica 沒 lock、application 假設有 lock 跑 race condition。</p>
<p>修法：</p>
<ul>
<li><code>rule_id</code> 排序：精確 rule（多條件 regex）放小、寬鬆 rule 放大</li>
<li>用 <code>apply=1</code> 強制停 chain、不要讓 query 繼續往下 match</li>
<li>跑 ProxySQL <code>SHOW PROCESSLIST</code> + audit log 確認 routing 正確</li>
</ul>
<h3 id="2-connection-漂移--multiplexing-把-session-variable-弄丟">2. Connection 漂移 — Multiplexing 把 session variable 弄丟</h3>
<p>Application 跑 <code>SET sql_mode=...</code>、ProxySQL 把這 connection 暫時黏死 backend 1。下個 query ProxySQL forget、把 connection unstick、實際 forward 到 backend 2（沒 <code>SET sql_mode</code>）、SQL 解析行為不同、application bug。</p>
<p>修法：</p>
<ul>
<li>用 <code>mysql-multiplexing=false</code> 全 disable（最簡單但浪費 connection pool 效率）</li>
<li>或在 application init 連線後跑的 <code>SET</code> 全列在 <code>mysql_users.connect_init</code>（每個 connection ProxySQL 自動跑、不會漂移）</li>
<li>避免 application 中途改 session variable、改成全部走 ProxySQL connect_init</li>
</ul>
<h3 id="3-write-不小心-route-到-replica--default_hostgroup-設錯">3. Write 不小心 route 到 replica — <code>default_hostgroup</code> 設錯</h3>
<p>Application user <code>default_hostgroup</code> 設 20 (reader)、INSERT / UPDATE / DELETE 沒 match 到任何 rule（沒寫 catch-all write rule）、走 default → 送 replica → replica 是 read-only → error。或更糟：replica 不是 read-only mode、寫入 <em>寫到 replica 上</em>、replication 反向不同步、data corruption。</p>
<p>修法：</p>
<ul>
<li>Application user <code>default_hostgroup</code> 設 10 (writer) — 寫入預設走 primary</li>
<li>Replica MySQL 一定要 <code>read_only=1</code>（防 stale write 寫到 replica）</li>
<li>監控 <code>mysql_query_rules</code> match 率、寫入 query 應該大部分透過 default_hostgroup 路由、不是個別 rule</li>
</ul>
<h3 id="4-runtime--disk-schema-drift--改了-runtime-沒-save重啟-config-消失">4. Runtime / disk schema drift — 改了 runtime 沒 save、重啟 config 消失</h3>
<p><code>LOAD ... TO RUNTIME</code> 跟 <code>SAVE ... TO DISK</code> 是兩個獨立操作。On-call 在事故中改 ProxySQL 配置（add server、調 query rule）、<code>LOAD</code> 套到 runtime 但忘記 <code>SAVE</code>、隔天 ProxySQL 重啟（OS update / crash）、config 回到 disk 版本、半夜 alert。</p>
<p>修法：</p>
<ul>
<li>每次 <code>LOAD ... TO RUNTIME</code> 後立刻 <code>SAVE ... TO DISK</code>（變成 habit）</li>
<li>用 IaC（Terraform / Ansible）管 ProxySQL config、不要手動改 admin</li>
<li>監控：對比 <code>runtime_mysql_servers</code> 跟 <code>mysql_servers</code>（disk）、有 diff 即告警</li>
</ul>
<h3 id="5-mirror-traffic-副作用--insert-鏡像到-staging-寫了兩次">5. Mirror traffic 副作用 — INSERT 鏡像到 staging 寫了兩次</h3>
<p><code>mirror_hostgroup</code> 把 query 鏡像送到第二個 hostgroup（不等 response、用於 shadow test 新 schema）。但 <em>鏡像是真實執行</em>、不是 dry-run。鏡像 INSERT 到 staging hostgroup → staging 真的多了 row。如果 staging hostgroup 接到 production 表（誤接）、production 寫入 doubled。</p>
<p>修法：</p>
<ul>
<li>Mirror 只用於 <em>獨立 staging cluster</em>、不混用 production schema</li>
<li>Mirror 設定要 review（規則 <code>match_pattern</code> 跟 <code>mirror_hostgroup</code> 配對）</li>
<li>開 mirror 前在 staging 跑 dry-run、確認 schema 跟 production isolated</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>對 100 application instance × 50 connection / instance = 5000 application connection 場景：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>ProxySQL 設定</th>
          <th>MySQL backend 配置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application → ProxySQL</td>
          <td><code>mysql-max_connections=10000</code></td>
          <td>不影響</td>
      </tr>
      <tr>
          <td>ProxySQL → MySQL primary</td>
          <td><code>max_connections=200</code>（per server）</td>
          <td>MySQL <code>max_connections=300</code>（多 100 buffer for admin）</td>
      </tr>
      <tr>
          <td>ProxySQL → MySQL replica</td>
          <td><code>max_connections=200</code>（per server）</td>
          <td>同上</td>
      </tr>
      <tr>
          <td>ProxySQL 數量（HA）</td>
          <td>至少 2 instance（HAProxy / VIP）</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Memory per ProxySQL</td>
          <td>2-4 GB（query rule cache + connection pool）</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>ProxySQL 本身需要 HA：放兩個 instance 後面接 VIP（keepalived）或 HAProxy。Application 連 VIP / HAProxy、不直接連 ProxySQL hostname（單點失效）。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>ProxySQL 透過 <em>monitor module</em> 自動偵測 primary（檢查 <code>read_only</code> flag）+ replica lag（檢查 <code>Seconds_Behind_Master</code>）。這個 monitor 依賴 MySQL replication 已配好（GTID + binlog ROW format）。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-orchestrator-ha">跟 Orchestrator HA</h3>
<p>Orchestrator 自動 failover 後新 primary 的 <code>read_only</code> flag 變 0、舊 primary 變 1。ProxySQL monitor 偵測到、自動把 hostgroup 10（writer）的 server 切換、application 不必改 connection string。</p>
<p>詳見 <em>Orchestrator failover 設計</em> 篇（待寫）。</p>
<h3 id="跟-osc-toolgh-ost--pt-osc">跟 OSC tool（gh-ost / pt-osc）</h3>
<p>ProxySQL 可以 <em>暫時 throttle</em> application 對某張表的寫入（query rule <code>delay</code> 欄位）、配合 OSC tool cut-over 時段降低 metadata lock 衝突。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-aurora-mysql--rds-proxy">跟 Aurora MySQL / RDS Proxy</h3>
<p>Aurora MySQL 推 <em>RDS Proxy</em>（AWS managed proxy）取代 ProxySQL — 跟 IAM 整合、failover &lt; 30 秒。但 RDS Proxy <em>沒有 query routing rule engine</em>（只做 connection pool）、不能讀寫分離。Aurora user 仍可能用 ProxySQL 在前面、再用 RDS Proxy 作 backend connection pool。</p>
<p>詳見 <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 page</a>。</p>
<h3 id="跟-postgresql-pgbouncer-對比">跟 PostgreSQL pgBouncer 對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>ProxySQL（MySQL）</th>
          <th>pgBouncer（PostgreSQL）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>抽象層</td>
          <td>Query-level proxy</td>
          <td>Transaction-level pool</td>
      </tr>
      <tr>
          <td>Query routing</td>
          <td>內建（rule engine）</td>
          <td>無（不看 SQL）</td>
      </tr>
      <tr>
          <td>Connection pool</td>
          <td>內建</td>
          <td>核心功能</td>
      </tr>
      <tr>
          <td>Read/write split</td>
          <td>內建（自動 + rule）</td>
          <td>要 application 層或 HAProxy 配</td>
      </tr>
      <tr>
          <td>Replica lag-aware</td>
          <td>內建</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Query cache</td>
          <td>內建</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>ProxySQL 是 <em>query 層中介</em>、pgBouncer 是 <em>connection 層中介</em>。詳見 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（read replica routing 前提）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（OSC + ProxySQL throttle 整合）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer</a>（PG sibling、不同抽象層）</li>
<li><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 page</a>（RDS Proxy + ProxySQL 取捨）</li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a></li>
<li>官方：<a href="https://proxysql.com/documentation/">ProxySQL Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Connection Scaling：process-per-connection model 跟為什麼 pooler 是必裝</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/connection-scaling/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/connection-scaling/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>connection scaling 的根因&lt;/em> — 為什麼 PG 比多數 DB 更需要 pooler、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config&lt;/a> 是 &lt;em>根因 vs 配置&lt;/em> 的關係。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="connection-per-process-model-是-pg-的結構性選擇">Connection-per-Process Model 是 PG 的結構性選擇&lt;/h2>
&lt;p>PG 接受 client connection 時的行為跟多數現代 DB 不同：每個 connection 由 postmaster &lt;code>fork()&lt;/code> 一個獨立的 OS process（backend）來服務。這個 process 在 connection lifetime 內專屬該 client、不跟其他 client 共享。&lt;/p>
&lt;p>對比常見 DB 的 connection model：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Vendor&lt;/th>
 &lt;th>Connection model&lt;/th>
 &lt;th>每 connection 資源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>PostgreSQL&lt;/td>
 &lt;td>Process-per-connection（fork）&lt;/td>
 &lt;td>5-15MB RAM、獨立 PID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MySQL&lt;/td>
 &lt;td>Thread-per-connection&lt;/td>
 &lt;td>256KB-2MB RAM、共享 process&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Oracle&lt;/td>
 &lt;td>Shared server / dedicated 可選&lt;/td>
 &lt;td>配置決定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SQL Server&lt;/td>
 &lt;td>Thread-per-connection（pooled）&lt;/td>
 &lt;td>~512KB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MongoDB&lt;/td>
 &lt;td>Thread-per-connection&lt;/td>
 &lt;td>~1MB&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>PG 選 process 不選 thread 是 1990s 設計決定 — 當時 thread library 在多 UNIX 平台不穩定、process 隔離性更好（一個 backend crash 不會帶倒整個 DB）。這個 trade-off 一路保留到今天、是 PG 在 high-connection-count workload 的 &lt;em>結構性負擔&lt;/em>。&lt;/p>
&lt;h2 id="量化connection-數量對-ram-跟-cpu-的壓力">量化：connection 數量對 RAM 跟 CPU 的壓力&lt;/h2>
&lt;p>一個 PG backend process 的 RAM footprint 由三部分組成：&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">backend_rss ≈ shared_buffers_attach + process_private + work_mem 高水位&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>shared_buffers&lt;/code> 是所有 backend 共享的、不重複計、但 &lt;code>process_private&lt;/code>（catalog cache / plan cache / temp buffer）跟 &lt;code>work_mem&lt;/code> 是 per-backend：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Workload 類型&lt;/th>
 &lt;th>process_private&lt;/th>
 &lt;th>work_mem 高水位&lt;/th>
 &lt;th>單 backend RAM&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Idle / 簡單 OLTP&lt;/td>
 &lt;td>3-5MB&lt;/td>
 &lt;td>4MB&lt;/td>
 &lt;td>7-9MB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中等 query（join / sort）&lt;/td>
 &lt;td>5-8MB&lt;/td>
 &lt;td>16-64MB&lt;/td>
 &lt;td>21-72MB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Heavy analytical（CTE / window）&lt;/td>
 &lt;td>8-15MB&lt;/td>
 &lt;td>256MB+&lt;/td>
 &lt;td>264MB+&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>500 個 connection、平均 30MB 各 ≈ 15GB RAM 給 backend processes（還沒算 shared_buffers）。這是 PG 在 cloud instance 上很快撞到 RAM ceiling 的根因。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>connection scaling 的根因</em> — 為什麼 PG 比多數 DB 更需要 pooler、跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a> 是 <em>根因 vs 配置</em> 的關係。</p></blockquote>
<hr>
<h2 id="connection-per-process-model-是-pg-的結構性選擇">Connection-per-Process Model 是 PG 的結構性選擇</h2>
<p>PG 接受 client connection 時的行為跟多數現代 DB 不同：每個 connection 由 postmaster <code>fork()</code> 一個獨立的 OS process（backend）來服務。這個 process 在 connection lifetime 內專屬該 client、不跟其他 client 共享。</p>
<p>對比常見 DB 的 connection model：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Connection model</th>
          <th>每 connection 資源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PostgreSQL</td>
          <td>Process-per-connection（fork）</td>
          <td>5-15MB RAM、獨立 PID</td>
      </tr>
      <tr>
          <td>MySQL</td>
          <td>Thread-per-connection</td>
          <td>256KB-2MB RAM、共享 process</td>
      </tr>
      <tr>
          <td>Oracle</td>
          <td>Shared server / dedicated 可選</td>
          <td>配置決定</td>
      </tr>
      <tr>
          <td>SQL Server</td>
          <td>Thread-per-connection（pooled）</td>
          <td>~512KB</td>
      </tr>
      <tr>
          <td>MongoDB</td>
          <td>Thread-per-connection</td>
          <td>~1MB</td>
      </tr>
  </tbody>
</table>
<p>PG 選 process 不選 thread 是 1990s 設計決定 — 當時 thread library 在多 UNIX 平台不穩定、process 隔離性更好（一個 backend crash 不會帶倒整個 DB）。這個 trade-off 一路保留到今天、是 PG 在 high-connection-count workload 的 <em>結構性負擔</em>。</p>
<h2 id="量化connection-數量對-ram-跟-cpu-的壓力">量化：connection 數量對 RAM 跟 CPU 的壓力</h2>
<p>一個 PG backend process 的 RAM footprint 由三部分組成：</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">backend_rss ≈ shared_buffers_attach + process_private + work_mem 高水位</span></span></code></pre></div><p><code>shared_buffers</code> 是所有 backend 共享的、不重複計、但 <code>process_private</code>（catalog cache / plan cache / temp buffer）跟 <code>work_mem</code> 是 per-backend：</p>
<table>
  <thead>
      <tr>
          <th>Workload 類型</th>
          <th>process_private</th>
          <th>work_mem 高水位</th>
          <th>單 backend RAM</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Idle / 簡單 OLTP</td>
          <td>3-5MB</td>
          <td>4MB</td>
          <td>7-9MB</td>
      </tr>
      <tr>
          <td>中等 query（join / sort）</td>
          <td>5-8MB</td>
          <td>16-64MB</td>
          <td>21-72MB</td>
      </tr>
      <tr>
          <td>Heavy analytical（CTE / window）</td>
          <td>8-15MB</td>
          <td>256MB+</td>
          <td>264MB+</td>
      </tr>
  </tbody>
</table>
<p>500 個 connection、平均 30MB 各 ≈ 15GB RAM 給 backend processes（還沒算 shared_buffers）。這是 PG 在 cloud instance 上很快撞到 RAM ceiling 的根因。</p>
<p>CPU 層面、<code>fork()</code> 系統呼叫在 Linux 通常 1-3ms、context switch ~3-5μs。100 connection burst 在 1 秒內進來、accumulated fork cost 100-300ms、加 query 本身的 CPU 跟 scheduler latency、平均 query 延遲會跳 2-5x。</p>
<h2 id="三個-guc-互動max_connections--shared_buffers--work_mem">三個 GUC 互動：max_connections / shared_buffers / work_mem</h2>
<p>PG 的 memory 規劃由這三個 GUC 互動決定、不能獨立調：</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">total_RAM ≈ shared_buffers + (max_connections × work_mem 高水位) + OS overhead</span></span></code></pre></div><p>實務 sizing 規則（16GB instance、OLTP workload）：</p>
<table>
  <thead>
      <tr>
          <th>GUC</th>
          <th>建議值</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>shared_buffers</code></td>
          <td>25% RAM（4GB）</td>
          <td>太大 OS file cache 收益遞減、&lt; 25% wastes RAM</td>
      </tr>
      <tr>
          <td><code>work_mem</code></td>
          <td>8-32MB</td>
          <td>每 query operation 用一份、不是每 connection 一份</td>
      </tr>
      <tr>
          <td><code>max_connections</code></td>
          <td>100-200</td>
          <td>超過 200 需 pooler、不是調更大</td>
      </tr>
      <tr>
          <td><code>effective_cache_size</code></td>
          <td>50-75% RAM</td>
          <td>planner 估 cost 用、不是實際配置</td>
      </tr>
      <tr>
          <td><code>maintenance_work_mem</code></td>
          <td>64-512MB</td>
          <td>VACUUM / CREATE INDEX 用</td>
      </tr>
  </tbody>
</table>
<p><code>max_connections = 1000</code> 是常見 anti-pattern — 真實 active query 可能只 50-100、剩下都 idle、但每個還是吃 RAM 跟 process slot、context switch overhead 還在。</p>
<h2 id="pooler-為什麼是-production-prerequisite">Pooler 為什麼是 <em>production prerequisite</em></h2>
<blockquote>
<p>本段是「為什麼必裝」、實際 PgBouncer 配置看 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a>。</p></blockquote>
<p>Pooler 的核心責任是 <em>把 N 個 application connection multiplex 成 M 個 PG backend（M ≪ N）</em>：</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">Application (3000 connection)
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">Pooler（PgBouncer / PgCat）
</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">PostgreSQL (50 backend process)</span></span></code></pre></div><p>Application 看到的是 <em>無限 connection 池</em>、PG 看到的是 <em>穩定 50 個 backend</em>。三個層次的效益：</p>
<ol>
<li><strong>RAM 節省</strong>：3000 connection × 30MB = 90GB → 50 backend × 30MB = 1.5GB</li>
<li><strong>Fork() cost 攤平</strong>：backend 重用、不是每個 client 都 fork</li>
<li><strong>Connection storm 緩衝</strong>：application 重啟 / scaling event 不會直接打到 PG</li>
</ol>
<p>Pooler 有三種 pool mode、各有 application 層相容性 trade-off：</p>
<table>
  <thead>
      <tr>
          <th>Pool mode</th>
          <th>Session 隔離</th>
          <th>適用 application</th>
          <th>PG feature 限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session</td>
          <td>每 client 獨佔 1 backend</td>
          <td>用 prepared statement、SET、temp table</td>
          <td>等同沒 pool、僅救 fork cost</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>每 transaction 換 backend</td>
          <td>多數 stateless API（最常用）</td>
          <td>不能用 session-level state</td>
      </tr>
      <tr>
          <td>Statement</td>
          <td>每 statement 換 backend</td>
          <td>Read-only / analytical</td>
          <td>不能用 transaction</td>
      </tr>
  </tbody>
</table>
<p>Production 多數選 transaction pool — 救 RAM 又保留 transaction semantics、代價是 application 不能用 session-level <code>SET</code>、<code>LISTEN/NOTIFY</code>、prepared statement（部分 pooler 已支援）。</p>
<h2 id="application-side-pool-vs-middleware-pool-vs-rds-proxy">Application-side Pool vs Middleware Pool vs RDS Proxy</h2>
<p>三層 pool 都能解 connection 問題、但解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>代表</th>
          <th>解的問題</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application-side（driver）</td>
          <td>HikariCP（Java）/ pgx pool（Go）/ asyncpg / Sequelize</td>
          <td>Connection 重用 + lifecycle 管理</td>
          <td>仍每 app instance 開 N 個到 PG、總量沒收斂</td>
      </tr>
      <tr>
          <td>Middleware pooler</td>
          <td>PgBouncer / PgCat</td>
          <td>Multiplex 所有 application instance 到少數 backend</td>
          <td>多一跳 latency 0.1-1ms、需自管 HA</td>
      </tr>
      <tr>
          <td>Cloud-managed proxy</td>
          <td>RDS Proxy / Cloud SQL Proxy</td>
          <td>Multiplex + IAM auth + Secrets Manager integration</td>
          <td>Latency 1-3ms、cost premium、PG feature 受限</td>
      </tr>
  </tbody>
</table>
<p><strong>典型 production 拓撲</strong>：</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">Application (HikariCP pool 10/instance × 50 instance = 500)
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">PgBouncer transaction pool（50 backend）
</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">PostgreSQL primary</span></span></code></pre></div><p>Application pool 救 fork cost、PgBouncer 救 backend 總量、兩層各做各的事不衝突。</p>
<p><strong>雙層 pool 配置容易出錯</strong>：application pool size 5 + PgBouncer default_pool_size 50 + 100 個 app instance、application 願意開 500 connection、PgBouncer 只給 50 個 backend — 多 450 個 application connection wait、看起來像「DB 慢」但實際是 pool 不足。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1connection-storm重啟--autoscale-同時打進來">Case 1：Connection storm（重啟 / autoscale 同時打進來）</h3>
<p><strong>情境</strong>：Kubernetes rolling restart、200 個 pod 同時重連、每 pod 開 20 個 connection、瞬間 4000 個 connection 嘗試打到 PG。</p>
<p>PG <code>max_connections = 500</code> 直接拒絕 3500 個、application 看到 <code>FATAL: sorry, too many clients already</code>、retry storm 雪上加霜。</p>
<p>修法：</p>
<ul>
<li>PgBouncer 在前面、application 連 PgBouncer 不直連 PG</li>
<li><code>reserve_pool_size = 5</code> 給管理流量留 buffer</li>
<li>Application 端加 jittered exponential backoff、避免 retry 同步</li>
</ul>
<h3 id="case-2fork-cost-在-burst-流量">Case 2：fork() cost 在 burst 流量</h3>
<p><strong>情境</strong>：Cron job 每分鐘整點觸發、500 個 worker 同時開 short-lived connection 跑 30ms query、結束關閉。</p>
<p>每分鐘 500 次 <code>fork()</code> + 500 次 <code>exit()</code>、fork cost 500-1500ms、CPU spike、其他 OLTP query 延遲飆。</p>
<p>修法：</p>
<ul>
<li>Worker 改 connect 到 PgBouncer transaction pool、backend 重用、fork 只在 PgBouncer 首次拓展時</li>
<li>或 worker 改成 long-lived process + 內部 task queue、避免每分鐘重 fork</li>
</ul>
<h3 id="case-3shared_buffers-跟-max_connections-互相壓縮">Case 3：shared_buffers 跟 max_connections 互相壓縮</h3>
<p><strong>情境</strong>：16GB instance、<code>shared_buffers = 8GB</code>（50%）、<code>max_connections = 800</code>、<code>work_mem = 16MB</code>。</p>
<p>預估 RAM：8GB + 800 × ~30MB = 32GB ≫ 16GB instance、OOM kill 來訪。</p>
<p>修法（重新分配）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">shared_buffers</span> <span class="o">=</span> <span class="s">4GB           # 25%</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">200          # 透過 PgBouncer multiplex</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">work_mem</span> <span class="o">=</span> <span class="s">16MB</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">effective_cache_size</span> <span class="o">=</span> <span class="s">12GB</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">maintenance_work_mem</span> <span class="o">=</span> <span class="s">512MB</span></span></span></code></pre></div><p>關鍵：<code>max_connections</code> 不是調更大救 connection 不足、是調 <em>PgBouncer pool size</em> 拓展 application 容量。</p>
<h3 id="case-4double-pool-配置失敗">Case 4：Double-pool 配置失敗</h3>
<p><strong>情境</strong>：Application HikariCP pool size = 50、50 個 instance、PgBouncer <code>default_pool_size = 20</code>、PG <code>max_connections = 100</code>。</p>
<p>Application 願意開 2500 個 connection、PgBouncer 只給 20 個 backend、application thread 大量 block 在 PgBouncer 等 backend 釋出。</p>
<p>修法：</p>
<ul>
<li>計算 <em>application 願意的並發</em> vs <em>PgBouncer 允許的 backend</em> vs <em>PG max_connections</em> 三層匹配</li>
<li>通常 <code>application_total_connection ≪ pgbouncer_max_client_conn</code> + <code>pgbouncer_default_pool_size + reserve ≪ pg_max_connections</code></li>
<li>Monitor PgBouncer <code>SHOW POOLS</code> 的 <code>cl_waiting</code>、長期 &gt; 0 表示 pool 不足</li>
</ul>
<h3 id="case-5max_connections-設太大反而慢">Case 5：max_connections 設太大反而慢</h3>
<p><strong>情境</strong>：team 看到 <code>connection refused</code>、把 <code>max_connections</code> 從 200 調到 2000、想說「給更多 connection 應該更好」。</p>
<p>調完 throughput 反而降 30% — context switch overhead、planner cache 競爭、lock manager 競爭都跟 connection 數線性放大。</p>
<p>修法：</p>
<ul>
<li><code>max_connections</code> 上限通常 200-500、超過要靠 pooler multiplex</li>
<li>用 <code>pg_stat_activity</code> 看真實 active connection（state != &lsquo;idle&rsquo;）、通常 &lt; 100</li>
<li>真實上限 = active 高水位 × 安全係數 1.5、不是「未來可能會用到的數量」</li>
</ul>
<h2 id="跟-mysql-connection-model-對比">跟 MySQL connection model 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL</th>
          <th>MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Connection 模型</td>
          <td>Process-per-connection（fork）</td>
          <td>Thread-per-connection</td>
      </tr>
      <tr>
          <td>單 connection RAM</td>
          <td>5-15MB（idle）/ 30-200MB（heavy）</td>
          <td>256KB-2MB</td>
      </tr>
      <tr>
          <td>Fork / spawn cost</td>
          <td>1-3ms</td>
          <td>&lt; 100μs</td>
      </tr>
      <tr>
          <td>Pooler 必要性</td>
          <td><strong>強烈必要</strong>（300+ connection 必裝）</td>
          <td>中等（ProxySQL 對特定 case 有用）</td>
      </tr>
      <tr>
          <td>主流 pooler</td>
          <td>PgBouncer / PgCat</td>
          <td>ProxySQL / MySQL Router</td>
      </tr>
  </tbody>
</table>
<p>MySQL thread-per-connection model 讓它在 high-connection-count workload 上 <em>看起來</em> 更省 — 但 PG 透過 PgBouncer 達到的 application 看到的容量跟 MySQL 直連是一樣的、只是多一層 indirection。</p>
<p>實務影響：</p>
<ul>
<li>MySQL 直連 1000 connection 還 OK、PG 直連 1000 connection 通常 OOM</li>
<li>PG + PgBouncer 1000 application connection、後端 50 backend、表現跟 MySQL 1000 直連相當</li>
<li>沒有 <em>PG 更耗 RAM</em> 的本質結論、是 <em>PG 預設不 multiplex、需要外掛 multiplex 層</em></li>
</ul>
<h2 id="pg-17-的-connection-進展">PG 17+ 的 connection 進展</h2>
<p>PG 17（2024）對 connection 仍維持 process-per-connection、但有幾個減壓改進：</p>
<ul>
<li><strong>Per-process memory 降低</strong>：catalog cache 改 generational allocator、idle backend RAM 降 ~20%</li>
<li><strong>Subscriber-side parallel apply</strong>：logical replication 減少 connection 開銷</li>
<li><strong><code>io_combine_limit</code></strong>：buffered read 合併、降 syscall overhead</li>
</ul>
<p>但 <em>process-per-connection model 本身</em> 沒換 — 短期內 PG 仍需 pooler。長期方向（PG 18+ 討論）可能引入 thread-based backend、但目前是 experimental patch。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a>：PgBouncer 操作配置 + 5 case</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">replication-topology</a>：Read replica + connection 分流</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a>：<code>work_mem</code> 影響 plan</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">mvcc-lock-model</a>：connection idle in transaction 卡 vacuum</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum-tuning</a>：autovacuum 也吃 connection slot</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>連到 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a> 學配置細節</li>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 回到全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C14 Yelp：Schematizer 自建 Schema Registry</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/</guid><description>&lt;p>這個案例的核心責任是說明 schema 治理是 data pipeline 的核心責任、不是 add-on。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Yelp data pipeline 一天數十億訊息、跨數百個 service、數千 schema、用自建 Schematizer 強制所有 message 走 Avro schema、訊息只帶 schema ID。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Schematizer 不只是 schema store、還做 schema evolution compatibility 與 topic 自動分配（不相容 schema 強制新 topic）。揭露 producer / consumer schema 治理要拉到平台層、靠工具強制、不靠人約定。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：Schema Registry / Schema evolution。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineeringblog.yelp.com/2016/08/more-than-just-a-schema-store.html">Yelp Schematizer: More than just a schema store&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 schema 治理是 data pipeline 的核心責任、不是 add-on。</p>
<h2 id="觀察">觀察</h2>
<p>Yelp data pipeline 一天數十億訊息、跨數百個 service、數千 schema、用自建 Schematizer 強制所有 message 走 Avro schema、訊息只帶 schema ID。</p>
<h2 id="判讀">判讀</h2>
<p>Schematizer 不只是 schema store、還做 schema evolution compatibility 與 topic 自動分配（不相容 schema 強制新 topic）。揭露 producer / consumer schema 治理要拉到平台層、靠工具強制、不靠人約定。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：Schema Registry / Schema evolution。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineeringblog.yelp.com/2016/08/more-than-just-a-schema-store.html">Yelp Schematizer: More than just a schema store</a></li>
</ul>
]]></content:encoded></item><item><title>Tailscale SSH</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/tailscale-ssh/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/tailscale-ssh/</guid><description>&lt;p>Tailscale 是 WireGuard-based zero-trust mesh VPN、Tailscale SSH 是其上的 SSH on overlay network 模組。核心 mindset 是 &lt;em>不用 SSH key、不用 jump host&lt;/em>：所有 device 加入同一個 tailnet、ACL 控制誰能 SSH 到誰、user identity 從 Tailscale 的 IdP 整合（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / Google / Microsoft / GitHub SSO）來。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &amp;#43; Device Posture &amp;#43; IdP integration">Cloudflare Access&lt;/a> 的差異在 &lt;em>網路模型 + identity binding + audit 深度&lt;/em>、SSH 管理能力本身都具備 — Tailscale 走 overlay mesh + identity-bound SSH，Teleport 走 Identity-Aware Proxy + first-class session recording，Boundary 走 network broker + dynamic credential。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Tailscale 的核心定位是 &lt;em>WireGuard overlay mesh + identity-bound 連線&lt;/em>、Tailscale SSH 是其上 &lt;em>取代 sshd 的 SSH 模組&lt;/em>。底層是 Tailscale daemon（每台 device 跑、建立 WireGuard tunnel）+ Tailscale control plane（管 ACL、key exchange、IdP integration、node enrollment）。Tailscale SSH 不是把 OpenSSH 套上 VPN — 它是把 SSH server 換成 Tailscale daemon 內建版本、用 tailnet identity 取代 SSH key、ACL 跟 sshd 設定脫鉤。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a> 比、Tailscale 走 &lt;em>zero-config + developer-friendly&lt;/em>、Teleport 走 &lt;em>audit-first + compliance-friendly&lt;/em> — Teleport session recording / RBAC / approval workflow 是 first-class、Tailscale Enterprise 才補 session recording、approval workflow 偏簡單。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary&lt;/a> 比、Boundary 是 &lt;em>network broker&lt;/em>（client → broker → target、target 不在 client 網路上）、Tailscale 是 &lt;em>overlay network&lt;/em>（client / target 都在 tailnet 上、直接點對點）；Boundary 配 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault&lt;/a> 發 dynamic credential、Tailscale 直接 bypass credential。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &amp;#43; Device Posture &amp;#43; IdP integration">Cloudflare Access&lt;/a> 比、Cloudflare Access 走 &lt;em>application-layer reverse proxy&lt;/em>、Tailscale 走 &lt;em>network-layer mesh&lt;/em>；application（HTTP / API）走 Cloudflare、機器存取（SSH / RDP / DB port）走 Tailscale。&lt;/p></description><content:encoded><![CDATA[<p>Tailscale 是 WireGuard-based zero-trust mesh VPN、Tailscale SSH 是其上的 SSH on overlay network 模組。核心 mindset 是 <em>不用 SSH key、不用 jump host</em>：所有 device 加入同一個 tailnet、ACL 控制誰能 SSH 到誰、user identity 從 Tailscale 的 IdP 整合（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Google / Microsoft / GitHub SSO）來。它跟 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> / <a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a> / <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a> 的差異在 <em>網路模型 + identity binding + audit 深度</em>、SSH 管理能力本身都具備 — Tailscale 走 overlay mesh + identity-bound SSH，Teleport 走 Identity-Aware Proxy + first-class session recording，Boundary 走 network broker + dynamic credential。</p>
<h2 id="服務定位">服務定位</h2>
<p>Tailscale 的核心定位是 <em>WireGuard overlay mesh + identity-bound 連線</em>、Tailscale SSH 是其上 <em>取代 sshd 的 SSH 模組</em>。底層是 Tailscale daemon（每台 device 跑、建立 WireGuard tunnel）+ Tailscale control plane（管 ACL、key exchange、IdP integration、node enrollment）。Tailscale SSH 不是把 OpenSSH 套上 VPN — 它是把 SSH server 換成 Tailscale daemon 內建版本、用 tailnet identity 取代 SSH key、ACL 跟 sshd 設定脫鉤。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> 比、Tailscale 走 <em>zero-config + developer-friendly</em>、Teleport 走 <em>audit-first + compliance-friendly</em> — Teleport session recording / RBAC / approval workflow 是 first-class、Tailscale Enterprise 才補 session recording、approval workflow 偏簡單。跟 <a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a> 比、Boundary 是 <em>network broker</em>（client → broker → target、target 不在 client 網路上）、Tailscale 是 <em>overlay network</em>（client / target 都在 tailnet 上、直接點對點）；Boundary 配 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 發 dynamic credential、Tailscale 直接 bypass credential。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a> 比、Cloudflare Access 走 <em>application-layer reverse proxy</em>、Tailscale 走 <em>network-layer mesh</em>；application（HTTP / API）走 Cloudflare、機器存取（SSH / RDP / DB port）走 Tailscale。</p>
<p>關鍵張力：<em>developer 易用性</em> ↔ <em>audit / compliance 深度</em> 是 Tailscale 客戶的最大 trade-off。Tailscale 把 SSH 變成「裝完 Tailscale 客戶端、加入 tailnet、不用設 sshd」、developer onboarding 從幾天縮到幾分鐘；但 session recording、approval workflow、keystroke audit 在 Enterprise tier 才有、且深度仍不及 Teleport。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Tailscale 在 access stack 承擔哪一段（mesh network / identity-bound SSH / Funnel external access）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> IdP、<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> audit log、Teleport 補 session recording）</li>
<li>ACL JSON policy 的 ownership 設計（src / dst / group / tag、誰寫、誰 review、tag 命名空間如何治理）</li>
<li>Tailscale SSH vs Teleport vs Boundary vs Cloudflare Access 的選型判讀</li>
<li>何時用 Tailscale、何時補上 Teleport（compliance）、何時補上 Boundary（dynamic credential）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Tailscale SSH deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>ACL 是否走 tag 而非 IP</strong>：production node 是否標 <code>tag:prod-*</code>、ACL 用 tag / group 寫（<code>src: [&quot;group:sre&quot;]</code>、<code>dst: [&quot;tag:prod-db:22&quot;]</code>）而非寫 device hostname；ACL JSON 是否進版控（Git → Tailscale GitOps integration）、change 經 PR review</li>
<li><strong>Identity provider 是不是組織 IdP</strong>：tailnet 是否綁 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Google Workspace / Microsoft Entra ID、user 從 IdP SCIM 同步、離職時 IdP deprovision 是否連動 tailnet（不是手動撤 tailnet user）</li>
<li><strong>Tailscale SSH 是否取代 sshd</strong>：production node 是否關掉 OpenSSH 的 port 22 listener、只允許 Tailscale SSH（避免 fallback 到 SSH key auth、繞過 tailnet ACL）</li>
<li><strong>Audit log 是否進 SIEM</strong>：Tailscale audit log（device add / ACL change / SSH session start）是否串到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、跟 IdP log correlation；Enterprise tier 的 SSH session recording 是否啟用</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Access Boundary</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Tailnet 與 Node enrollment</strong>：Tailnet 是一個邏輯網路（一個組織通常一個）、Node 是加入 tailnet 的 device（laptop / server / container）。Enrollment 兩種路徑 — <em>interactive</em>（人類 device 跑 <code>tailscale up</code>、瀏覽器跳 IdP 登入）、<em>auth key</em>（ephemeral / reusable / preauthorized key、CI / IaC 用）。Production server 通常用 <em>auth key + tag</em> 加入、tag 在 enrollment 時就綁定、不能事後改。</p>
<p><strong>ACL JSON policy</strong>：Tailscale ACL 是 HuJSON（JSON with comments）文件、由 <code>acls</code> / <code>groups</code> / <code>tagOwners</code> / <code>ssh</code> 區塊組成。<code>acls</code> 寫 <code>action: accept</code> + <code>src</code> + <code>dst</code> + <code>proto</code> + <code>port</code>、<code>groups</code> 把 user 抽成角色（<code>group:sre</code>、<code>group:helpdesk</code>）、<code>tagOwners</code> 控制誰能 mint 某個 tag、<code>ssh</code> 區塊定義誰能用 Tailscale SSH 連到哪些 tag（額外於 <code>acls</code>）。ACL 寫得好不好直接決定 <em>lateral movement blast radius</em>。</p>
<p><strong>Tailscale SSH（取代 sshd）</strong>：Tailscale SSH 是 daemon 內建的 SSH server、user 連線時不出示 SSH key、Tailscale 用 <em>tailnet identity</em>（從 IdP 來）做 authn、用 ACL 的 <code>ssh</code> 區塊做 authz。SSH session 的 OS user 由 ACL 指定（<code>users: [&quot;root&quot;, &quot;ubuntu&quot;]</code>）、不是 user 自己挑。意義是 <em>SSH key rotation 從 lifecycle 移除</em>、user 離職 IdP deprovision 後立即失去所有 SSH access。</p>
<p><strong>Identity provider 整合</strong>：Tailscale 自身不存 password、user identity 完全外包給 IdP。Okta / Google Workspace 通常用 SCIM 同步 user + group、GitHub SSO 走 OAuth、Microsoft Entra ID 走 SAML。Group 從 IdP 同步進 tailnet 後、ACL 直接用 <code>group:sre</code>、<code>group:contractor</code>。IdP 的 MFA / Conditional Access policy 自動套用到 tailnet authn。</p>
<p><strong>Tag-based machine identity</strong>：Tag 是 Tailscale 的 <em>machine identity primitive</em>、語意接近 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> workload identity（但 Tailscale-specific、不是 SPIFFE 標準）。Production 用 tag 把 node 分類（<code>tag:prod-db</code>、<code>tag:prod-app</code>、<code>tag:ci-runner</code>）、ACL 用 tag 寫規則。Tag 在 enrollment 時 bind、之後不能改（要重新 enroll）；<code>tagOwners</code> 控制誰能 mint 該 tag、防止 dev tag 升 prod tag。</p>
<p><strong>Subnet Router 與 Exit Node</strong>：<em>Subnet Router</em> 把 on-prem subnet（例如 <code>10.0.0.0/16</code> 的舊資料中心）route 到 tailnet、不用在每台舊機器裝 Tailscale daemon — 適合 legacy infra migration。<em>Exit Node</em> 把所有流量（不只 tailnet）走某個 node 出去、適合 remote worker 需要從固定 IP 出網。兩者都是 mesh 之外的擴展、不是 first-class、容易擴大 blast radius 要謹慎用。</p>
<p><strong>Funnel（external HTTPS access）</strong>：Funnel 把 tailnet 上的 internal service 暴露到 internet（透過 Tailscale relay、Tailscale 出 TLS cert）、適合 webhook receiver、dev preview environment、demo URL。Production-grade external access 應該走 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a> 或 reverse proxy + WAF — Funnel 沒有 WAF、bot protection、rate limit，是 <em>zero-config 暴露</em>、不是 <em>production hardened ingress</em>。</p>
<p><strong>跟 OS firewall 互動</strong>：Tailscale 是 overlay network、不取代 OS firewall。Production node 應該用 OS firewall（iptables / nftables / Windows Firewall）封鎖 <em>非 tailnet</em> 流量到 port 22 / 3306 / 5432、只允許 <code>tailscale0</code> 介面進來；不然攻擊者拿到 node IP 後仍能繞過 ACL 直接 SSH。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Tailscale SSH</th>
          <th>Teleport</th>
          <th>Boundary</th>
          <th>Cloudflare Access</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>網路模型</td>
          <td>WireGuard overlay mesh（peer-to-peer）</td>
          <td>Identity-Aware Proxy（client → proxy → target）</td>
          <td>Network broker（client → broker → target）</td>
          <td>Application-layer reverse proxy</td>
      </tr>
      <tr>
          <td>Identity binding</td>
          <td>tailnet identity（IdP-bound、無 SSH key）</td>
          <td>Teleport cert（SSO-issued、short-lived）</td>
          <td>Boundary session token（IdP-bound）</td>
          <td>Cloudflare identity（SSO-issued、跟 ZTNA 整合）</td>
      </tr>
      <tr>
          <td>Session recording</td>
          <td>Enterprise tier、Tailscale-specific</td>
          <td>First-class、所有 tier、tsh play 回放</td>
          <td>無（依賴 target 自身）</td>
          <td>無（屬 application layer、不 record SSH）</td>
      </tr>
      <tr>
          <td>Audit 深度</td>
          <td>ACL change / device add / session start</td>
          <td>Full session recording + RBAC audit + approval</td>
          <td>Session log + dynamic credential audit</td>
          <td>HTTP request log（不適用 SSH）</td>
      </tr>
      <tr>
          <td>Credential model</td>
          <td>No credential（identity-bound）</td>
          <td>Short-lived cert（per-session）</td>
          <td>Dynamic credential（Vault-issued）</td>
          <td>OAuth / JWT（per-request）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>緩 — 裝 client 即用</td>
          <td>中 — RBAC role / tsh CLI / approval workflow</td>
          <td>陡 — broker / target / credential brokering</td>
          <td>緩 — Cloudflare 既有用戶上手快</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>SaaS（Tailscale）+ self-hosted（Headscale OSS）</td>
          <td>Self-hosted / Teleport Cloud</td>
          <td>Self-hosted（HashiCorp）/ HCP Boundary</td>
          <td>SaaS only（Cloudflare）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>developer-heavy、SSH-first、zero-config 訴求</td>
          <td>Compliance / SOC 2 / 重 audit 場景</td>
          <td>Dynamic credential + Vault 已用</td>
          <td>Application 層存取（HTTP / API）</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — 拆 client + 開 sshd 即可</td>
          <td>中 — RBAC / approval workflow 已 codify</td>
          <td>中 — broker 設定 + Vault integration</td>
          <td>中 — ZTNA policy + IdP 整合</td>
      </tr>
  </tbody>
</table>
<p>選 Tailscale SSH 的核心訴求：<em>developer 易用性 + zero-trust mesh + 願意接受 Tailscale 控制面信任</em>、且 audit / compliance 要求是中度而非極致（SOC 2 Type II + 內部 SOX 等級就配 Enterprise tier session recording、HIPAA / FedRAMP / 重 compliance 走 Teleport）。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Tailscale SSH session recording（Enterprise）</strong>：2023 後 Enterprise tier 提供 SSH session 錄影、存到組織自己的 S3 / GCS（不是 Tailscale 控制面）、用 <em>recorder node</em>（tag:tailscale-recorder）攔流量寫盤。意義是 <em>audit 不再依賴 OS-level 工具（auditd / OSSEC）</em>；但跟 Teleport 比、Tailscale recording 仍偏簡單、approval workflow 是基本版、structured query 跟 keystroke replay UI 不如 Teleport。</p>
<p><strong>Subnet Router 的 blast radius</strong>：Subnet Router 把整個 subnet route 到 tailnet、ACL 控制粒度從 <em>device-level</em> 退到 <em>subnet-level</em>（除非搭 tag）— 一台 Subnet Router 給太多人用就是 jump host 復活。production 應該 <em>每個 subnet 至少兩個 Subnet Router</em>（HA）、tag 區分（<code>tag:subnet-router-prod</code>）、ACL 限定誰能透過它走。</p>
<p><strong>Headscale（OSS control plane alternative）</strong>：Headscale 是社群維護的 Tailscale control plane OSS 重實作、self-hosted、跟官方 Tailscale client 相容。適用 <em>資料主權 / air-gapped / 不信任 Tailscale 控制面</em> 場景。代價是 ACL JSON 編輯器 / GitOps / SCIM / IdP integration 都要自己拼、沒有官方 SaaS 的 console UX 跟 SLA。production 用 Headscale 通常配 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> workload identity 補 machine identity。</p>
<p><strong>跟 SPIRE workload identity 對照</strong>：Tailscale tag 是 Tailscale-specific 的 machine identity primitive、語意接近 SPIRE 的 SPIFFE ID（<code>spiffe://example.org/prod-db</code>）；差異在 SPIRE 走 SPIFFE 開放標準、跨 platform（Kubernetes / VM / serverless）、tag 只在 tailnet 內有意義。重 multi-platform workload identity 走 SPIRE、SSH access 為主走 Tailscale tag。</p>
<p><strong>Just-In-Time access pattern</strong>：Tailscale 預設是 <em>standing access</em>（user 在 group:sre、永遠能 SSH 到 prod-db）、不是 JIT。要做 JIT 通常 <em>IdP 端做</em>（Okta Workflows 加 user 進 group:sre-oncall、SCIM 同步進 tailnet、ACL 給 group:sre-oncall 對 prod-db 的 SSH 權限）、或 <em>Tailscale API 自寫 ACL 寫入腳本</em>。Teleport / Boundary 有 first-class JIT approval、Tailscale 要自己拼。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>ACL 改錯把全公司鎖在外面</strong>：ACL JSON 寫錯 default deny 規則、Tailscale 控制面套用後沒人能連 — 用 Tailscale 控制面的 ACL preview / test 功能、production 走 GitOps PR review、保留 <code>admin-emergency-access</code> group bypass</li>
<li><strong>離職員工還能 SSH</strong>：IdP deprovision 沒連動 tailnet（手動管 user）— 改走 SCIM 同步 + IdP group binding、ACL 用 group 而非個別 user</li>
<li><strong>OpenSSH 還在 listen port 22 給 fallback</strong>：node 沒關 sshd、攻擊者拿到 IP 後用 SSH key 繞過 tailnet ACL — production node 關掉 sshd、OS firewall 只允許 tailscale0 介面的 22 port</li>
<li><strong>tag 被誤升 prod</strong>：dev user 自己 mint <code>tag:prod-db</code> 給 node、ACL 給 prod-db SSH 權限就此擴散 — <code>tagOwners</code> 限定 <code>tag:prod-*</code> 只有 group:sre 能 mint</li>
<li><strong>Funnel 暴露 internal service</strong>：dev 為了 demo 開 Funnel、忘了關、production data 外洩 — Funnel 走 audit log + alert、預設不該開、要開走 short-lived auth key + tag isolation</li>
<li><strong>Subnet Router 變新 jump host</strong>：一台 Subnet Router 給全公司用 legacy subnet、ACL 退到 subnet-level — tag 區分 router、ACL 限定誰能透過它、HA 跑兩台以上</li>
<li><strong>Audit log 沒進 SIEM</strong>：Tailscale console 看 audit log 很慢、跟 IdP / cloud control plane 沒 correlation — 啟用 Tailscale audit log streaming 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、跨來源 correlation</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compliance / SOC 2 / 重 audit</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a></td>
      </tr>
      <tr>
          <td>Dynamic credential + Vault 已用</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a> + <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></td>
      </tr>
      <tr>
          <td>Application 層存取（HTTP / API）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></td>
      </tr>
      <tr>
          <td>Workload identity 跨 platform</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
      </tr>
      <tr>
          <td>External HTTPS production ingress</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> + reverse proxy</td>
      </tr>
      <tr>
          <td>Audit log SIEM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>WireGuard 協定本身的密碼學細節跟 NAT traversal 機制</li>
<li>Tailscale 計費 tier 的逐項功能對照（看 Tailscale 官方 pricing page）</li>
<li>Headscale 完整部署 + GitOps + SCIM 自拼方案</li>
<li>Tailscale 跟 OPNsense / pfSense 等傳統 VPN gateway 的整合</li>
<li>Tailscale 內網 DNS（MagicDNS）跟 split-horizon DNS 的細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Tailscale SSH 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022 MFA Fatigue</a></td>
          <td>Tailscale SSH 走 IdP identity、push MFA fail 後 attacker 仍要拿 IdP 通過 + tailnet enrollment、雙層 mitigation 比 SSH key 強；但 standing tailnet access 本身是風險、需配合 short-lived auth key 或 JIT group assignment</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>Tailscale 上游 IdP（Okta / Google / Microsoft）signing key 出事時、tailnet enrollment 也跟著受影響、要 force re-auth；Tailscale 自身的 control plane 信任也是同一條鏈、要 audit</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 Identity Lateral Impact</a></td>
          <td>Tailscale ACL 做 tag-based scope（helpdesk group 不能 SSH 到 <code>tag:prod-db</code>）、限制 lateral movement blast radius；對照啟示是 helpdesk 工具不該共享 tailnet 跟 prod node、或 ACL 要切乾淨</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a>、<a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity 補位）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（dynamic credential 補位）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP 來源）、<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（audit log SIEM）、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（external HTTPS production ingress）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（tailnet compromise IR routing）</li>
<li>官方：<a href="https://tailscale.com/kb/">Tailscale Documentation</a>、<a href="https://tailscale.com/kb/1193/tailscale-ssh/">Tailscale SSH</a>、<a href="https://github.com/juanfont/headscale">Headscale</a></li>
</ul>
]]></content:encoded></item><item><title>9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/</guid><description>&lt;p>這個案例的核心責任是說明「受監管產業」的容量規劃跟「網路服務」的本質差異。銀行交易系統的容量目標不只是「能撐多少」、還要同時滿足合規（資料駐留、稽核、加密、可恢復性）、跟一般工程性能優化的取捨完全不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Standard Chartered 在 Aurora 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/search/">AWS search results&lt;/a> 與相關 case study）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>遷移前&lt;/th>
 &lt;th>遷移後 (Aurora)&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>交易吞吐 (TPS)&lt;/td>
 &lt;td>（未公開、基線值）&lt;/td>
 &lt;td>4000 TPS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>吞吐倍數&lt;/td>
 &lt;td>1x baseline&lt;/td>
 &lt;td>10x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>受監管市場&lt;/td>
 &lt;td>-&lt;/td>
 &lt;td>7 個（首批遷移）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本下降&lt;/td>
 &lt;td>-&lt;/td>
 &lt;td>「顯著」（未公開具體數字）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要驅動&lt;/td>
 &lt;td>韌性 + 性能&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Amazon Aurora（PostgreSQL 或 MySQL 相容）、加密 at rest / in transit、多 AZ 部署、跨地區複製（受監管市場各自獨立）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>受監管銀行案例揭露三個合規驅動容量規劃的重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>資料駐留限制 = 容量規劃的單位是「per 市場」&lt;/strong>：7 個受監管市場代表 7 個獨立 cluster（資料不能跨境）、容量規劃變成「7 個獨立規劃 × 各自合規門檻」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的合規要求識別、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的地理分片。&lt;/li>
&lt;li>&lt;strong>「韌性 + 性能」並列、不是 trade-off&lt;/strong>：傳統工程文化常把可靠性跟性能視為對立、銀行業務要求兩者同時達標。Aurora 的多 AZ storage + replica 同時提供性能（讀分流）跟韌性（故障切換）、達成 &lt;em>韌性即性能&lt;/em> 的目標。對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">06.18 reliability metrics governance&lt;/a> 的可靠性指標。&lt;/li>
&lt;li>&lt;strong>遷移本身的合規驗證 = 容量規劃延伸&lt;/strong>：受監管系統遷移不只是技術測試、還要過合規審查（中央銀行 / 金融監管機關）、每個市場各自審。這個審查 lead time（數月）必須算進遷移時程。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook&lt;/a> 的合規驅動 migration。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「10x throughput」是 &lt;em>vs 舊系統&lt;/em>、不是 &lt;em>vs 競爭對手&lt;/em>。受監管銀行的舊系統通常是 1990s-2000s 的 mainframe 或自建 OLTP、性能本來就低。讀案例時要對標的是「自家改善幅度」、不是「絕對性能」。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>資料駐留是容量規劃的硬限制、不是優化選項&lt;/strong>：受監管市場必須各自獨立 cluster、不能用「全球單一 cluster」優化。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">00.4 traffic data scale&lt;/a> 的合規限制。&lt;/li>
&lt;li>&lt;strong>多 AZ + 跨地區複製是合規基線、不是優化&lt;/strong>：銀行業務 RPO / RTO 通常由監管要求（不能丟資料、必須 X 小時內恢復）、不是業務 SLA 選項。對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">06.7 DR rollback rehearsal&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遷移時程要算合規 lead time&lt;/strong>：每個受監管市場的審查可能 3-12 個月、合計遷移時程是「市場數 × 平均審查月份」、不是「技術遷移月份」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：Azure SQL Hyperscale + Azure regions、GCP Cloud SQL / Spanner + regional configurations、各家雲端的受監管雲端方案（AWS GovCloud、Azure Government、GCP Assured Workloads）都是對等候選。差異是各家對特定監管框架（PCI-DSS、ISO27001、各國金融法規）的認證覆蓋。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「受監管產業」的容量規劃跟「網路服務」的本質差異。銀行交易系統的容量目標不只是「能撐多少」、還要同時滿足合規（資料駐留、稽核、加密、可恢復性）、跟一般工程性能優化的取捨完全不同。</p>
<h2 id="觀察">觀察</h2>
<p>Standard Chartered 在 Aurora 的關鍵敘述（引自 <a href="https://aws.amazon.com/search/">AWS search results</a> 與相關 case study）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>遷移前</th>
          <th>遷移後 (Aurora)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易吞吐 (TPS)</td>
          <td>（未公開、基線值）</td>
          <td>4000 TPS</td>
      </tr>
      <tr>
          <td>吞吐倍數</td>
          <td>1x baseline</td>
          <td>10x</td>
      </tr>
      <tr>
          <td>受監管市場</td>
          <td>-</td>
          <td>7 個（首批遷移）</td>
      </tr>
      <tr>
          <td>成本下降</td>
          <td>-</td>
          <td>「顯著」（未公開具體數字）</td>
      </tr>
      <tr>
          <td>主要驅動</td>
          <td>韌性 + 性能</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Amazon Aurora（PostgreSQL 或 MySQL 相容）、加密 at rest / in transit、多 AZ 部署、跨地區複製（受監管市場各自獨立）。</p>
<h2 id="判讀">判讀</h2>
<p>受監管銀行案例揭露三個合規驅動容量規劃的重點。</p>
<ol>
<li><strong>資料駐留限制 = 容量規劃的單位是「per 市場」</strong>：7 個受監管市場代表 7 個獨立 cluster（資料不能跨境）、容量規劃變成「7 個獨立規劃 × 各自合規門檻」。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的合規要求識別、跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的地理分片。</li>
<li><strong>「韌性 + 性能」並列、不是 trade-off</strong>：傳統工程文化常把可靠性跟性能視為對立、銀行業務要求兩者同時達標。Aurora 的多 AZ storage + replica 同時提供性能（讀分流）跟韌性（故障切換）、達成 <em>韌性即性能</em> 的目標。對應 <a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">06.18 reliability metrics governance</a> 的可靠性指標。</li>
<li><strong>遷移本身的合規驗證 = 容量規劃延伸</strong>：受監管系統遷移不只是技術測試、還要過合規審查（中央銀行 / 金融監管機關）、每個市場各自審。這個審查 lead time（數月）必須算進遷移時程。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> 的合規驅動 migration。</li>
</ol>
<p>需要警惕：「10x throughput」是 <em>vs 舊系統</em>、不是 <em>vs 競爭對手</em>。受監管銀行的舊系統通常是 1990s-2000s 的 mainframe 或自建 OLTP、性能本來就低。讀案例時要對標的是「自家改善幅度」、不是「絕對性能」。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>資料駐留是容量規劃的硬限制、不是優化選項</strong>：受監管市場必須各自獨立 cluster、不能用「全球單一 cluster」優化。對應 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">00.4 traffic data scale</a> 的合規限制。</li>
<li><strong>多 AZ + 跨地區複製是合規基線、不是優化</strong>：銀行業務 RPO / RTO 通常由監管要求（不能丟資料、必須 X 小時內恢復）、不是業務 SLA 選項。對應 <a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">06.7 DR rollback rehearsal</a>。</li>
<li><strong>遷移時程要算合規 lead time</strong>：每個受監管市場的審查可能 3-12 個月、合計遷移時程是「市場數 × 平均審查月份」、不是「技術遷移月份」。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a>。</li>
</ol>
<p>跨平台等效：Azure SQL Hyperscale + Azure regions、GCP Cloud SQL / Spanner + regional configurations、各家雲端的受監管雲端方案（AWS GovCloud、Azure Government、GCP Assured Workloads）都是對等候選。差異是各家對特定監管框架（PCI-DSS、ISO27001、各國金融法規）的認證覆蓋。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃受監管產業 OLTP → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想做合規驅動的容量規劃 → <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">00.4 traffic data scale</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想理解韌性跟性能的同步達成 → <a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">06.18 reliability metrics governance</a></li>
<li>對照其他金融交易案例 → <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> / <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></li>
<li>想拆解跨 AZ failover RTO 量級與合規 anti-recommendation → <a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">Aurora 跨 AZ failover RTO</a></li>
<li>想評估全球資料常駐與多 region 部署 → <a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">Aurora global database 多 region</a></li>
<li>想對照 distributed SQL（CockroachDB / Aurora DSQL / Spanner）的合規場景 → <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/rds/aurora/customers/">Amazon Aurora Customer Stories</a></li>
<li><a href="https://aws.amazon.com/blogs/industries/amazon-aurora-for-core-banking-systems/">Amazon Aurora for Core Banking Systems</a></li>
<li><a href="https://aws.amazon.com/blogs/database/amazon-aurora-dsql-for-global-scale-financial-transactions/">Amazon Aurora DSQL for global-scale financial transactions</a></li>
</ul>
]]></content:encoded></item><item><title>0.14 企業選型案例圖譜</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/enterprise-selection-case-atlas/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/enterprise-selection-case-atlas/</guid><description>&lt;p>企業選型案例圖譜的核心責任是提供「跨規模、跨產業、跨階段」的選型樣本，讓讀者知道同一種技術問題在不同公司會如何被定義、取捨與落地。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>這一頁的責任是回答三件事：這家公司遇到什麼壓力、做了什麼選型決策、代價與回寫是什麼。提供企業層面的選型壓力對照，跟 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖&lt;/a> 是 sibling：本頁是「實際企業怎麼決策」、0.19 是「能力 × vendor 名稱對照」。兩者並讀能避免「光看對照表選 vendor」或「光看案例抄架構」的兩種誤用。&lt;/p>
&lt;p>使用方式是先從你的需求壓力切入，再對照對應案例，而不是先選喜歡的公司再倒推技術。這樣可以避免「抄架構」而忽略上下文差異。&lt;/p>
&lt;h2 id="使用方式">使用方式&lt;/h2>
&lt;ol>
&lt;li>先回到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖&lt;/a> 定位你的問題類型。&lt;/li>
&lt;li>用本頁找 2 到 3 個不同規模企業的對照案例。&lt;/li>
&lt;li>把案例中的決策壓力回寫到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨&lt;/a>。&lt;/li>
&lt;li>再進入對應模組（01-08）看實作與控制面細節。&lt;/li>
&lt;/ol>
&lt;h2 id="案例地圖">案例地圖&lt;/h2>
&lt;p>案例按照「企業型態 × 規模階段」分組，目的是讓你先找到最接近自己情境的壓力來源，再看選型動作。&lt;/p>
&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>SaaS（成長期，單體資料庫瓶頸）&lt;/td>
 &lt;td>&lt;a href="https://www.notion.com/blog/sharding-postgres-at-notion">Notion: Sharding Postgres&lt;/a>&lt;/td>
 &lt;td>單體 Postgres 何時拆分成分片架構&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DevTool（成長期，職能拆分）&lt;/td>
 &lt;td>&lt;a href="https://about.gitlab.com/blog/2022/06/02/splitting-database-into-main-and-ci/">GitLab: Splitting Main and CI DB&lt;/a>&lt;/td>
 &lt;td>功能分解如何換取容量與可靠性&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DevTool（成熟期，升級風險控制）&lt;/td>
 &lt;td>&lt;a href="https://about.gitlab.com/blog/2020/09/11/gitlab-pg-upgrade/">GitLab: Major PostgreSQL Upgrade&lt;/a>&lt;/td>
 &lt;td>高流量環境下升級策略與回退設計&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commerce（高速成長，資料庫升級）&lt;/td>
 &lt;td>&lt;a href="https://shopify.engineering/upgrading-mysql-shopify">Shopify: Upgrading MySQL&lt;/a>&lt;/td>
 &lt;td>大規模 MySQL 維運成本與可靠性治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commerce（超大規模，水平擴充）&lt;/td>
 &lt;td>&lt;a href="https://shopify.engineering/blogs/engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">Shopify: Scaling with Vitess&lt;/a>&lt;/td>
 &lt;td>什麼時候引入 Vitess 以取得水平擴充能力&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Social / Chat（高吞吐事件流）&lt;/td>
 &lt;td>&lt;a href="https://slack.engineering/scaling-slacks-job-queue/">Slack: Scaling Job Queue&lt;/a>&lt;/td>
 &lt;td>高吞吐背景工作為何改採 Kafka + Redis&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Social（超大規模，多租戶優先序）&lt;/td>
 &lt;td>&lt;a href="https://engineering.fb.com/2021/02/22/production-engineering/foqs-scaling-a-distributed-priority-queue/">Meta: FOQS Distributed Priority Queue&lt;/a>&lt;/td>
 &lt;td>多租戶 priority queue 如何做持久化與隔離&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ride-hailing（全球規模，監控平台）&lt;/td>
 &lt;td>&lt;a href="https://www.uber.com/en-GB/blog/m3/">Uber: M3 Metrics Platform&lt;/a>&lt;/td>
 &lt;td>單點監控系統何時要走平台化與多租戶存儲&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN / Security（邊緣規模，可觀測）&lt;/td>
 &lt;td>&lt;a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Cloudflare: Building Cloudflare on Cloudflare&lt;/a>&lt;/td>
 &lt;td>logs/metrics/traces 如何一起成為操作能力&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commerce（成熟期，韌性驗證）&lt;/td>
 &lt;td>&lt;a href="https://shopify.engineering/four-steps-creating-effective-game-day-tests">Shopify: Effective Game Day Tests&lt;/a>&lt;/td>
 &lt;td>如何把演練從活動變成驗證制度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commerce（大促前容量治理）&lt;/td>
 &lt;td>&lt;a href="https://shopify.engineering/resiliency-planning-for-high-traffic-events">Shopify: Resiliency Planning for High-Traffic Events&lt;/a>&lt;/td>
 &lt;td>高峰活動前容量與風險如何建模&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloud Platform（多租戶隔離）&lt;/td>
 &lt;td>&lt;a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">AWS Builders’ Library: Shuffle-sharding&lt;/a>&lt;/td>
 &lt;td>多租戶故障隔離如何影響資料與佇列設計&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Platform（組織擴張，邊界重整）&lt;/td>
 &lt;td>&lt;a href="https://www.uber.com/en-GB/blog/microservice-architecture/">Uber: Domain-Oriented Microservice Architecture&lt;/a>&lt;/td>
 &lt;td>微服務規模變大後如何重新治理邊界與依賴&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Social（儲存成本壓力）&lt;/td>
 &lt;td>&lt;a href="https://engineering.fb.com/2016/08/31/core-infra/myrocks-a-space-and-write-optimized-mysql-database/">Meta: MyRocks&lt;/a>&lt;/td>
 &lt;td>何時用新 storage engine 換取成本與寫入效率&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Social（平台化分片）&lt;/td>
 &lt;td>&lt;a href="https://engineering.fb.com/2020/08/24/production-engineering/scaling-services-with-shard-manager/">Meta: Shard Manager&lt;/a>&lt;/td>
 &lt;td>分片能力何時應該平台化而不是各隊自建&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="類型覆蓋檢查">類型覆蓋檢查&lt;/h2>
&lt;p>案例蒐集的完成條件是覆蓋度，篇數本身沒有意義。每次補案例都用這四個維度檢查缺口。&lt;/p></description><content:encoded><![CDATA[<p>企業選型案例圖譜的核心責任是提供「跨規模、跨產業、跨階段」的選型樣本，讓讀者知道同一種技術問題在不同公司會如何被定義、取捨與落地。</p>
<h2 id="概念定位">概念定位</h2>
<p>這一頁的責任是回答三件事：這家公司遇到什麼壓力、做了什麼選型決策、代價與回寫是什麼。提供企業層面的選型壓力對照，跟 <a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖</a> 是 sibling：本頁是「實際企業怎麼決策」、0.19 是「能力 × vendor 名稱對照」。兩者並讀能避免「光看對照表選 vendor」或「光看案例抄架構」的兩種誤用。</p>
<p>使用方式是先從你的需求壓力切入，再對照對應案例，而不是先選喜歡的公司再倒推技術。這樣可以避免「抄架構」而忽略上下文差異。</p>
<h2 id="使用方式">使用方式</h2>
<ol>
<li>先回到 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖</a> 定位你的問題類型。</li>
<li>用本頁找 2 到 3 個不同規模企業的對照案例。</li>
<li>把案例中的決策壓力回寫到 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a>。</li>
<li>再進入對應模組（01-08）看實作與控制面細節。</li>
</ol>
<h2 id="案例地圖">案例地圖</h2>
<p>案例按照「企業型態 × 規模階段」分組，目的是讓你先找到最接近自己情境的壓力來源，再看選型動作。</p>
<table>
  <thead>
      <tr>
          <th>企業型態與規模階段</th>
          <th>企業案例</th>
          <th>主要選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SaaS（成長期，單體資料庫瓶頸）</td>
          <td><a href="https://www.notion.com/blog/sharding-postgres-at-notion">Notion: Sharding Postgres</a></td>
          <td>單體 Postgres 何時拆分成分片架構</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>DevTool（成長期，職能拆分）</td>
          <td><a href="https://about.gitlab.com/blog/2022/06/02/splitting-database-into-main-and-ci/">GitLab: Splitting Main and CI DB</a></td>
          <td>功能分解如何換取容量與可靠性</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>DevTool（成熟期，升級風險控制）</td>
          <td><a href="https://about.gitlab.com/blog/2020/09/11/gitlab-pg-upgrade/">GitLab: Major PostgreSQL Upgrade</a></td>
          <td>高流量環境下升級策略與回退設計</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06</a></td>
      </tr>
      <tr>
          <td>Commerce（高速成長，資料庫升級）</td>
          <td><a href="https://shopify.engineering/upgrading-mysql-shopify">Shopify: Upgrading MySQL</a></td>
          <td>大規模 MySQL 維運成本與可靠性治理</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>Commerce（超大規模，水平擴充）</td>
          <td><a href="https://shopify.engineering/blogs/engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">Shopify: Scaling with Vitess</a></td>
          <td>什麼時候引入 Vitess 以取得水平擴充能力</td>
          <td><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a>、<a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>Social / Chat（高吞吐事件流）</td>
          <td><a href="https://slack.engineering/scaling-slacks-job-queue/">Slack: Scaling Job Queue</a></td>
          <td>高吞吐背景工作為何改採 Kafka + Redis</td>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03</a></td>
      </tr>
      <tr>
          <td>Social（超大規模，多租戶優先序）</td>
          <td><a href="https://engineering.fb.com/2021/02/22/production-engineering/foqs-scaling-a-distributed-priority-queue/">Meta: FOQS Distributed Priority Queue</a></td>
          <td>多租戶 priority queue 如何做持久化與隔離</td>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>Ride-hailing（全球規模，監控平台）</td>
          <td><a href="https://www.uber.com/en-GB/blog/m3/">Uber: M3 Metrics Platform</a></td>
          <td>單點監控系統何時要走平台化與多租戶存儲</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a>、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04</a></td>
      </tr>
      <tr>
          <td>CDN / Security（邊緣規模，可觀測）</td>
          <td><a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Cloudflare: Building Cloudflare on Cloudflare</a></td>
          <td>logs/metrics/traces 如何一起成為操作能力</td>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td>Commerce（成熟期，韌性驗證）</td>
          <td><a href="https://shopify.engineering/four-steps-creating-effective-game-day-tests">Shopify: Effective Game Day Tests</a></td>
          <td>如何把演練從活動變成驗證制度</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06</a></td>
      </tr>
      <tr>
          <td>Commerce（大促前容量治理）</td>
          <td><a href="https://shopify.engineering/resiliency-planning-for-high-traffic-events">Shopify: Resiliency Planning for High-Traffic Events</a></td>
          <td>高峰活動前容量與風險如何建模</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a>、<a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>Cloud Platform（多租戶隔離）</td>
          <td><a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">AWS Builders’ Library: Shuffle-sharding</a></td>
          <td>多租戶故障隔離如何影響資料與佇列設計</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a>、<a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
      <tr>
          <td>Platform（組織擴張，邊界重整）</td>
          <td><a href="https://www.uber.com/en-GB/blog/microservice-architecture/">Uber: Domain-Oriented Microservice Architecture</a></td>
          <td>微服務規模變大後如何重新治理邊界與依賴</td>
          <td><a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0</a>、<a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a></td>
      </tr>
      <tr>
          <td>Social（儲存成本壓力）</td>
          <td><a href="https://engineering.fb.com/2016/08/31/core-infra/myrocks-a-space-and-write-optimized-mysql-database/">Meta: MyRocks</a></td>
          <td>何時用新 storage engine 換取成本與寫入效率</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>Social（平台化分片）</td>
          <td><a href="https://engineering.fb.com/2020/08/24/production-engineering/scaling-services-with-shard-manager/">Meta: Shard Manager</a></td>
          <td>分片能力何時應該平台化而不是各隊自建</td>
          <td><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a>、<a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
  </tbody>
</table>
<h2 id="類型覆蓋檢查">類型覆蓋檢查</h2>
<p>案例蒐集的完成條件是覆蓋度，篇數本身沒有意義。每次補案例都用這四個維度檢查缺口。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>已覆蓋示例</th>
          <th>常見缺口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>企業型態</td>
          <td>SaaS、DevTool、Commerce、Social、Ride-hailing、Cloud Platform、CDN/Security</td>
          <td>FinTech、Gaming、Healthcare、製造業平台</td>
      </tr>
      <tr>
          <td>規模階段</td>
          <td>成長期、成熟期、超大規模</td>
          <td>早期產品（小團隊）與跨國多區治理</td>
      </tr>
      <tr>
          <td>選型問題類型</td>
          <td>資料分片、佇列架構、可觀測平台、容量韌性、多租戶隔離、組織邊界</td>
          <td>成本治理、合規（PCI/SOX/GDPR）與資料主權</td>
      </tr>
      <tr>
          <td>決策生命週期</td>
          <td>遷移、升級、平台化、演練</td>
          <td>退場策略（decommission）與 vendor 轉移</td>
      </tr>
  </tbody>
</table>
<h2 id="第一批缺口回填清單">第一批缺口回填清單</h2>
<p>第一批回填先補三個目前缺口最大的產業類型，目標是讓案例圖譜從「網路平台公司視角」擴展到「高合規與高事件密度」場景。</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>優先蒐集的選型議題</th>
          <th>回寫章節起點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td>合規壓力下的資料分區、審計留存、變更放行與風險隔離</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a>、<a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td>高峰事件流、低延遲路徑、規則推送風險與跨區回復</td>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a>、<a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td>資料主權、存取邊界、可追溯性與災難回復流程</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
      </tr>
  </tbody>
</table>
<p>這份清單的用途是定義下一輪蒐集方向。每補一個案例，至少要同步回寫一個 04 觀測章節、一個 06 驗證章節與一個 08 事故章節，避免案例只停留在選型敘事。</p>
<h2 id="第一批案例清單fintech--gaming--healthcare">第一批案例清單（FinTech / Gaming / Healthcare）</h2>
<p>第一批案例的責任是先補齊產業覆蓋，並建立可直接回寫到 04/06/08 的共同語言。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>企業案例</th>
          <th>主要選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td><a href="https://stripe.com/blog">Stripe: Scaling Payments APIs</a></td>
          <td>金流 API 的一致性、冪等與放行門檻</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>FinTech</td>
          <td><a href="https://www.adyen.com/knowledge-hub">Adyen Engineering</a></td>
          <td>合規要求下的資料保留、稽核追溯與跨區部署</td>
          <td><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a>、<a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td><a href="https://technology.riotgames.com/">Riot Games Tech Blog</a></td>
          <td>高峰活動期間的低延遲路徑與跨區容量治理</td>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a>、<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td><a href="https://dev.epicgames.com/community/">Epic Games Unreal Engine / Fortnite Scale Articles</a></td>
          <td>大型即時服務的事件流、匹配與故障隔離</td>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td><a href="https://cloud.google.com/architecture/healthcare-life-sciences">Google Cloud Healthcare Architecture Guides</a></td>
          <td>資料主權、存取邊界與審計證據鏈</td>
          <td><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a>、<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td><a href="https://aws.amazon.com/health/">AWS Healthcare and Life Sciences Architecture</a></td>
          <td>多區備援下的資料保護與恢復順序</td>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a>、<a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
      </tr>
  </tbody>
</table>
<p>這批案例以「產業壓力類型」為主，不以單一公司唯一做法當標準答案。後續第二批再補製造業平台與跨國多區治理案例。</p>
<h2 id="對應正文入口">對應正文入口</h2>
<p>第一批缺口已補對應正文，圖譜可直接連到可回寫文章：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>正文入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td><a href="/blog/backend/00-service-selection/cases/fintech-compliance-and-selection-pressure/" data-link-title="FinTech：合規壓力下的後端選型" data-link-desc="在審計、留存與交易正確性要求下，如何平衡成本、風險與交付速度。">0.C1 FinTech：合規壓力下的後端選型</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td><a href="/blog/backend/00-service-selection/cases/gaming-peak-traffic-and-isolation/" data-link-title="Gaming：高峰流量與隔離邊界選型" data-link-desc="大型活動流量下，如何在低延遲與穩定性之間做可持續取捨。">0.C2 Gaming：高峰流量與隔離邊界選型</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td><a href="/blog/backend/00-service-selection/cases/healthcare-data-sovereignty-and-recovery/" data-link-title="Healthcare：資料主權與回復順序選型" data-link-desc="醫療場景下，如何把資料主權、存取邊界與災難回復放進同一套決策。">0.C3 Healthcare：資料主權與回復順序選型</a></td>
      </tr>
  </tbody>
</table>
<p>營運一段時間後的語言、工具或架構轉換案例，見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a>。</p>
<h2 id="讀法提醒">讀法提醒</h2>
<p>同一家公司不代表同一答案。公司不同時期的選型結論可能相反，因為負載、組織、預算與產品階段已經改變。把案例當成「決策壓力樣本」，比當成「標準答案」更可靠。</p>
<p>當兩個案例做出不同選擇，先檢查四件事：流量形狀、資料生命週期、失敗代價、維運能力。這四件事通常比語言與框架更能解釋選型差異。</p>
]]></content:encoded></item><item><title>6.14 Dependency Reliability Budget</title><link>https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何依賴需要 budget：自家服務的 SLO 是依賴 SLO 的乘積&lt;/li>
&lt;li>依賴類別：內部服務、第三方 API、SaaS、基礎設施（DB / cache / queue）&lt;/li>
&lt;li>依賴 SLA 對照：vendor 公布的 SLA 跟 observed reliability 的差距&lt;/li>
&lt;li>budget 計算：依賴 99.9% × 自家 99.9% = 99.8% 上限&lt;/li>
&lt;li>降級設計：依賴失效時的 fallback / cache / 隊列緩衝&lt;/li>
&lt;li>circuit breaker 與 budget 的關聯&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO&lt;/a> 的整合：依賴 budget 是 SLO 算式的一部分&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 topology&lt;/a> 的整合：依賴拓撲提供 budget 評估資料&lt;/li>
&lt;li>反模式：SLO 訂目標時忽略依賴可靠性；vendor SLA 抄進合約但無監測；依賴掛了才發現有依賴&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Dependency reliability budget 是把外部服務與跨團隊依賴的可靠性納入設計約束，責任是避免把自己系統的目標建立在不可控前提上。&lt;/p>
&lt;p>這一頁處理的是依賴一旦變差，自己服務還能保住多少功能。當依賴不是自己能修的時候，budget 就是把不確定性明文化。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀依賴風險時，不只看 SLA，而是看依賴失效後的降級能力與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>依賴是否有明確 failure domain&lt;/li>
&lt;li>是否有 graceful degradation 或 fallback&lt;/li>
&lt;li>budget 是否會隨依賴變更而更新&lt;/li>
&lt;li>外部 outage 是否能快速路由到替代策略&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3&lt;/a>：基礎儲存依賴的邊界一旦縮小，整體可靠性就會被放大影響。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare&lt;/a>：edge / control-plane 依賴需要有明確降級路徑。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">Azure AD&lt;/a>：身份依賴失效時，影響通常跨產品、跨流程。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">Amazon A2&lt;/a>：static stability 讓資料面在控制面失效時仍能服務，constant work 避免恢復放大。控制面是依賴 budget 中風險最高的項目。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">Shopify H2&lt;/a>：pod 隔離把依賴 budget 從全域帳本拆成 per-pod 結構，resiliency matrix 把依賴缺口可視化。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">Meta M2&lt;/a>：回復工具依賴被回復的系統（BGP / DNS / 遠端存取），揭露控制面的隱性循環依賴。&lt;/li>
&lt;/ul>
&lt;h2 id="失效局部化cell-邊界跟-shuffle-sharding">失效局部化：cell 邊界跟 shuffle sharding&lt;/h2>
&lt;p>失效局部化是把單一依賴退化限制在最小可影響範圍的能力。把「依賴 budget」從統一全域帳本拆成 per-cell 可用度結構、是這層治理的核心責任。失效局部化要解四個子問題：擴散邊界、熱點重疊、控制面解耦、失敗模式工作量恆定。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1 Amazon Shuffle Sharding 與 Cell 邊界&lt;/a>：揭露四個機制對應上述四個子問題 — cell 邊界（擴散邊界）、shuffle sharding（熱點重疊）、static stability（控制面解耦）、constant work（失敗模式工作量恆定）。這四個機制把恢復策略從「全域搶救」轉為「分批收斂」。Cell 邊界是 6.14 SSoT；實驗時 blast radius 的邊界控制由 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment-safety-boundary&lt;/a> 處理、兩者邊界互補（前者是常態架構、後者是實驗範圍控制）。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何依賴需要 budget：自家服務的 SLO 是依賴 SLO 的乘積</li>
<li>依賴類別：內部服務、第三方 API、SaaS、基礎設施（DB / cache / queue）</li>
<li>依賴 SLA 對照：vendor 公布的 SLA 跟 observed reliability 的差距</li>
<li>budget 計算：依賴 99.9% × 自家 99.9% = 99.8% 上限</li>
<li>降級設計：依賴失效時的 fallback / cache / 隊列緩衝</li>
<li>circuit breaker 與 budget 的關聯</li>
<li>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO</a> 的整合：依賴 budget 是 SLO 算式的一部分</li>
<li>跟 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 topology</a> 的整合：依賴拓撲提供 budget 評估資料</li>
<li>反模式：SLO 訂目標時忽略依賴可靠性；vendor SLA 抄進合約但無監測；依賴掛了才發現有依賴</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Dependency reliability budget 是把外部服務與跨團隊依賴的可靠性納入設計約束，責任是避免把自己系統的目標建立在不可控前提上。</p>
<p>這一頁處理的是依賴一旦變差，自己服務還能保住多少功能。當依賴不是自己能修的時候，budget 就是把不確定性明文化。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀依賴風險時，不只看 SLA，而是看依賴失效後的降級能力與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>。</p>
<p>重點訊號包括：</p>
<ul>
<li>依賴是否有明確 failure domain</li>
<li>是否有 graceful degradation 或 fallback</li>
<li>budget 是否會隨依賴變更而更新</li>
<li>外部 outage 是否能快速路由到替代策略</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">AWS S3</a>：基礎儲存依賴的邊界一旦縮小，整體可靠性就會被放大影響。</li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">Cloudflare</a>：edge / control-plane 依賴需要有明確降級路徑。</li>
<li><a href="/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">Azure AD</a>：身份依賴失效時，影響通常跨產品、跨流程。</li>
<li><a href="/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">Amazon A2</a>：static stability 讓資料面在控制面失效時仍能服務，constant work 避免恢復放大。控制面是依賴 budget 中風險最高的項目。</li>
<li><a href="/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">Shopify H2</a>：pod 隔離把依賴 budget 從全域帳本拆成 per-pod 結構，resiliency matrix 把依賴缺口可視化。</li>
<li><a href="/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">Meta M2</a>：回復工具依賴被回復的系統（BGP / DNS / 遠端存取），揭露控制面的隱性循環依賴。</li>
</ul>
<h2 id="失效局部化cell-邊界跟-shuffle-sharding">失效局部化：cell 邊界跟 shuffle sharding</h2>
<p>失效局部化是把單一依賴退化限制在最小可影響範圍的能力。把「依賴 budget」從統一全域帳本拆成 per-cell 可用度結構、是這層治理的核心責任。失效局部化要解四個子問題：擴散邊界、熱點重疊、控制面解耦、失敗模式工作量恆定。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1 Amazon Shuffle Sharding 與 Cell 邊界</a>：揭露四個機制對應上述四個子問題 — cell 邊界（擴散邊界）、shuffle sharding（熱點重疊）、static stability（控制面解耦）、constant work（失敗模式工作量恆定）。這四個機制把恢復策略從「全域搶救」轉為「分批收斂」。Cell 邊界是 6.14 SSoT；實驗時 blast radius 的邊界控制由 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment-safety-boundary</a> 處理、兩者邊界互補（前者是常態架構、後者是實驗範圍控制）。</p>
<p>把 cell 邊界跟 shuffle sharding 視為依賴 budget 的前置結構：先限制擴散邊界、再談恢復策略。budget 算式裡的「依賴失效」應該對應到「最大可影響 cell」、不是「整個服務全停」。</p>
<h2 id="跨區故障跟回復順序">跨區故障跟回復順序</h2>
<p>跨區故障的核心責任是把「單區極限失效」跟「跨區連鎖退化」拆成兩個治理面。fault domain 限制單區擴散、ordered failover 控制回復節奏、dependency isolation 切斷共享路徑放大風險、三者構成跨區治理 contract。大規模平台的關鍵風險來自跨區相依引發的連鎖退化 — 單點失效只是觸發點、真正的擴散面在共享相依路徑。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">M1 Meta Region Failover 邊界治理</a>：揭露三個機制 — region fault domain（影響面最多到哪裡）、ordered failover（先恢復哪條路徑）、dependency isolation（共享相依如何降風險）。</p>
<p>回復順序的核心是分批恢復、不同時恢復所有路徑。同時恢復多條路徑可能在剛恢復的依賴上引發回源放大或連鎖過載、把原本可控的回復變成第二次故障。實際的做法跟 ordered failover 對齊：依事故 timeline 跟團隊既定 runbook 安排回復批次、每批驗證 baseline 穩定後再進下一批。具體的批次設計跟 ordered failover 證據交給 <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 containment-recovery-strategy</a>。</p>
<h2 id="跨團隊-reliability-契約">跨團隊 reliability 契約</h2>
<p>跨團隊 reliability 契約的核心責任是讓「依賴 budget」變成「契約欄位」：每個被依賴的服務承諾哪些 SLI、提供哪些降級路徑、failure mode 是什麼。團隊自治程度高的組織需要共同契約把跨服務的可靠性最低標準對齊、避免風險在整合時集中爆發。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">SP1 Spotify 平台工程與可靠性契約</a>：揭露三個機制 — reliability contract（每個服務最低要提供什麼）、platform self-service（標準如何降低導入成本）、cross-team evidence（證據如何跨團隊共享）。SP1 case 主場景是內部跨團隊契約、不是 vendor 軸；vendor SLA 治理請見前段「依賴類別」跟 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">04.18 operating model</a> 的 ownership 邊界。</p>
<p>契約讓內部依賴 budget 可以基於 observed reliability（被依賴服務實際的 SLI 觀測值）、補強只靠 vendor SLA 的不足 — 後者通常是上界、不反映實際失效特性。</p>
<h2 id="產業情境saas-與-b2b-服務的依賴約束">產業情境：SaaS 與 B2B 服務的依賴約束</h2>
<p>SaaS 服務的可靠性直接綁定客戶合約，依賴 budget 的分配需要按最嚴格的 SLA 需求設計。enterprise 客戶要求 99.99%、self-serve 客戶接受 99.9% — 共享依賴的 budget 必須對齊最高 SLA，否則高階客戶的承諾無法兌現。</p>
<p>多租戶共享依賴的 budget 分配是 SaaS 特有的治理問題。所有租戶共用同一組 DB / cache / queue，但高 SLA 客戶對依賴可靠性的要求更嚴格。實務做法是把高 SLA 客戶路由到獨立依賴池（dedicated instance / priority queue），或在共享依賴上做租戶級隔離（connection pool per tenant / rate limit per tenant）。隔離策略跟 <a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon A1 的 shuffle sharding</a> 同源 — 差異在 SaaS 的隔離單位是租戶合約等級而非 cell。</p>
<p>第三方依賴的 SLA 傳遞是另一個 SaaS 常見壓力。SaaS 產品常依賴其他 SaaS（payment provider / email service / auth provider），這些依賴的 SLA 是自身 SLA 的理論上限。若 payment provider 只承諾 99.9%，自身對客戶承諾 99.99% 的結帳成功率就需要 fallback 設計（如多 provider 切換、本地排隊 + 延遲處理）。budget 計算時要把第三方依賴的 observed reliability 納入，而非照抄 vendor SLA。</p>
<p>跟 <a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">Spotify SP1 平台工程與可靠性契約</a> 的關聯：分散團隊共用可靠性基線的契約模型，在 SaaS 組織中同時服務內部團隊對齊與外部客戶 SLA 承諾兩個面向。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>6.6 SLO / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a>：把依賴可靠性納入目標計算</li>
<li>6.8 release gate：把依賴健康度變成放行條件</li>
<li>08.15 vendor 事故：第三方事故的事中處理</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>自家服務 SLO 高於依賴 SLA 的乘積、目標不可達</li>
<li>第三方 API 退化時無 observed metric、靠用戶投訴發現</li>
<li>vendor SLA credit 從未請領、無流程</li>
<li>新依賴接入無 reliability review</li>
<li>關鍵路徑上有「不知道掛了會怎樣」的依賴</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.13 topology：依賴自動發現</li>
<li>06.6 SLO：依賴 budget 納入 SLO 算式</li>
<li>06.10 contract testing：依賴契約穩定性</li>
<li>08.15 vendor 事故：依賴方掛掉的決策模型</li>
</ul>
]]></content:encoded></item><item><title>8.14 Multi-incident Coordination</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何需要獨立節點：8.2 假設單事故、規模化組織同時 3+ 事故是常態&lt;/li>
&lt;li>衝突資源：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> pool、subject expert、stakeholder communication channel&lt;/li>
&lt;li>優先序判準：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>、不可逆性、復原成本&lt;/li>
&lt;li>meta-&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色：協調多事故 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a>、分配資源、防止 cascading&lt;/li>
&lt;li>共通根因檢測：兩個 incident 是否同源、避免重複 IR&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 command roles&lt;/a> 的延伸：8.2 是單事故、8.14 是事故組合&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder&lt;/a> 的整合：多事故對外通訊不可矛盾&lt;/li>
&lt;li>反模式：多事故各自開戰情室、無協調；同事被 page 到不同事故；meta-&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色缺失、靠 senior 臨時補位&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Multi-incident coordination 是把同時多事故的優先序、資源分配與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> pool 協調變成可執行流程，責任是避免組織在高壓下把有限的人力切碎。&lt;/p>
&lt;p>這一頁處理的是事故之間的協調，而不是單一事故處理。當 active incident 數量上升，沒有協調層就會出現資源互搶與對外訊息互相衝突。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀多事故協調時，先看是否能先排優先序，再看是否能共用資源而不互相拖累。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>是否能快速分辨哪個事故的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope&lt;/a> 最大&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> pool 是否有可替補與輪換&lt;/li>
&lt;li>同一 SME 被 page 到多事故時是否有分流規則&lt;/li>
&lt;li>對外通訊是否由單一協調面統一&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：多渠道通訊很容易在多事故時互相打架。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog&lt;/a>：監控與協調平台失效時，多事故處理會同步劣化。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台級事故常伴隨多條工作流同時受影響。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>08.1 severity：跨事故優先序判準&lt;/li>
&lt;li>08.2 command roles：meta-&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色定義&lt;/li>
&lt;li>08.10 stakeholder：多事故對外節奏&lt;/li>
&lt;li>08.13 repeated：同源事故合併判讀&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同時 3+ active incident 時、沒人能說「最嚴重的是哪個」&lt;/li>
&lt;li>同 SME 被 page 到多事故、靠人力切換&lt;/li>
&lt;li>多事故對外通訊出現矛盾資訊&lt;/li>
&lt;li>共通根因事故被當獨立 IR 處理、重複工&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> pool 不足、事故等待 incident commander 啟動&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>08.1 severity：跨事故優先序判準&lt;/li>
&lt;li>08.2 command roles：meta-&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色定義&lt;/li>
&lt;li>08.10 stakeholder：多事故對外節奏&lt;/li>
&lt;li>08.13 repeated：同源事故合併判讀&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何需要獨立節點：8.2 假設單事故、規模化組織同時 3+ 事故是常態</li>
<li>衝突資源：<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> pool、subject expert、stakeholder communication channel</li>
<li>優先序判準：<a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、不可逆性、復原成本</li>
<li>meta-<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色：協調多事故 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a>、分配資源、防止 cascading</li>
<li>共通根因檢測：兩個 incident 是否同源、避免重複 IR</li>
<li>跟 <a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 command roles</a> 的延伸：8.2 是單事故、8.14 是事故組合</li>
<li>跟 <a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder</a> 的整合：多事故對外通訊不可矛盾</li>
<li>反模式：多事故各自開戰情室、無協調；同事被 page 到不同事故；meta-<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色缺失、靠 senior 臨時補位</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Multi-incident coordination 是把同時多事故的優先序、資源分配與 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> pool 協調變成可執行流程，責任是避免組織在高壓下把有限的人力切碎。</p>
<p>這一頁處理的是事故之間的協調，而不是單一事故處理。當 active incident 數量上升，沒有協調層就會出現資源互搶與對外訊息互相衝突。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀多事故協調時，先看是否能先排優先序，再看是否能共用資源而不互相拖累。</p>
<p>重點訊號包括：</p>
<ul>
<li>是否能快速分辨哪個事故的 <a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact scope</a> 最大</li>
<li><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> pool 是否有可替補與輪換</li>
<li>同一 SME 被 page 到多事故時是否有分流規則</li>
<li>對外通訊是否由單一協調面統一</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：多渠道通訊很容易在多事故時互相打架。</li>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog</a>：監控與協調平台失效時，多事故處理會同步劣化。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台級事故常伴隨多條工作流同時受影響。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>08.1 severity：跨事故優先序判準</li>
<li>08.2 command roles：meta-<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色定義</li>
<li>08.10 stakeholder：多事故對外節奏</li>
<li>08.13 repeated：同源事故合併判讀</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同時 3+ active incident 時、沒人能說「最嚴重的是哪個」</li>
<li>同 SME 被 page 到多事故、靠人力切換</li>
<li>多事故對外通訊出現矛盾資訊</li>
<li>共通根因事故被當獨立 IR 處理、重複工</li>
<li><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> pool 不足、事故等待 incident commander 啟動</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.1 severity：跨事故優先序判準</li>
<li>08.2 command roles：meta-<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色定義</li>
<li>08.10 stakeholder：多事故對外節奏</li>
<li>08.13 repeated：同源事故合併判讀</li>
</ul>
]]></content:encoded></item><item><title>Azure AD / Entra ID</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/</guid><description>&lt;p>Azure AD（現 Entra ID）是 Microsoft 生態的 identity 控制面、其失效會讓所有依賴 SSO 的服務無法登入、是 identity-as-cascading-point 的代表。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Identity 控制面 single point of cascading：SSO 失效擴散到所有下游&lt;/li>
&lt;li>配置變更 staged rollout 的限制：identity 服務難以 region-staged&lt;/li>
&lt;li>Token cache 緩衝：客戶端 token 有效期決定 outage 感受時間&lt;/li>
&lt;li>跨產品依賴：M365 / Teams / GitHub Enterprise 等的隱性依賴&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2020&lt;/td>
 &lt;td>多次全球登入失效&lt;/td>
 &lt;td>Identity cascading、staged rollout 限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2021&lt;/td>
 &lt;td>DNS / token service&lt;/td>
 &lt;td>Identity 服務的 sub-component 風險&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Azure AD 這個案例在講的是 identity 控制面一旦退化，許多看似獨立的服務都會一起受影響。讀者先看懂 Entra ID、Service Health 與 M365 health console 的分工，再把身份驗證視為跨服務的基礎路由。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 identity control plane 出現異常時，恢復順序往往比單一服務本身更重要。先讓監控與通訊路徑回穩，再處理驗證與登入流量，才能避免修復過程再度放大故障。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否把身份驗證失效與單一應用失效分開判讀&lt;/li>
&lt;li>能否從 Service Health 找到影響範圍與恢復節奏&lt;/li>
&lt;li>能否把 PIR 與 health dashboard 當成同一條對外路由&lt;/li>
&lt;li>能否辨識哪些障礙來自 identity，哪些來自下游服務&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Azure AD 是 Microsoft 365、GitHub Enterprise 與其他 SaaS 服務的基礎路由，這讓它和 AWS S3、GCP 一樣都屬於「控制面失效會放大」的案例。它最適合拿來和 Microsoft 365 一起讀，因為兩者分別描述了 identity 層與協作層的相依關係。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2020 年多次全球登入失效是 identity cascading 的典型樣本。&lt;/li>
&lt;li>2021 年 DNS / token service 問題則顯示 sub-component 也能放大成平台級風險。&lt;/li>
&lt;li>Azure Service Health 與 M365 health console 是對外路由的關鍵。&lt;/li>
&lt;li>token cache 會決定 outage 在使用者端維持多久。&lt;/li>
&lt;li>identity 是所有 SSO 服務的基礎路由。&lt;/li>
&lt;li>staged rollout 在 identity 服務上特別難做，因為影響面太大。&lt;/li>
&lt;li>token service 與 DNS 故障會把身份驗證整體拉下來。&lt;/li>
&lt;li>service health 變成客戶理解影響範圍的第一手資訊。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">AZ1&lt;/a>&lt;/td>
 &lt;td>身分控制面中斷&lt;/td>
 &lt;td>盤點跨產品身份依賴與分級回復順序&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-sla-performance">Service Level Agreement performance for Microsoft Entra ID&lt;/a>：Entra ID 的 SLA / incident history 入口。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/service-health/service-health-overview">What is Azure Service Health?&lt;/a>：Azure Service Health 與 status / advisories 的官方說明。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/microsoft-365/enterprise/view-service-health?view=o365-worldwide">How to check Microsoft 365 service health&lt;/a>：M365/Entra 相關 health console 的用法。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/mt-mt/azure/reliability">Azure reliability documentation&lt;/a>：Azure 可靠性文件總入口。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Azure AD（現 Entra ID）是 Microsoft 生態的 identity 控制面、其失效會讓所有依賴 SSO 的服務無法登入、是 identity-as-cascading-point 的代表。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Identity 控制面 single point of cascading：SSO 失效擴散到所有下游</li>
<li>配置變更 staged rollout 的限制：identity 服務難以 region-staged</li>
<li>Token cache 緩衝：客戶端 token 有效期決定 outage 感受時間</li>
<li>跨產品依賴：M365 / Teams / GitHub Enterprise 等的隱性依賴</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2020</td>
          <td>多次全球登入失效</td>
          <td>Identity cascading、staged rollout 限制</td>
      </tr>
      <tr>
          <td>2021</td>
          <td>DNS / token service</td>
          <td>Identity 服務的 sub-component 風險</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Azure AD 這個案例在講的是 identity 控制面一旦退化，許多看似獨立的服務都會一起受影響。讀者先看懂 Entra ID、Service Health 與 M365 health console 的分工，再把身份驗證視為跨服務的基礎路由。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 identity control plane 出現異常時，恢復順序往往比單一服務本身更重要。先讓監控與通訊路徑回穩，再處理驗證與登入流量，才能避免修復過程再度放大故障。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否把身份驗證失效與單一應用失效分開判讀</li>
<li>能否從 Service Health 找到影響範圍與恢復節奏</li>
<li>能否把 PIR 與 health dashboard 當成同一條對外路由</li>
<li>能否辨識哪些障礙來自 identity，哪些來自下游服務</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Azure AD 是 Microsoft 365、GitHub Enterprise 與其他 SaaS 服務的基礎路由，這讓它和 AWS S3、GCP 一樣都屬於「控制面失效會放大」的案例。它最適合拿來和 Microsoft 365 一起讀，因為兩者分別描述了 identity 層與協作層的相依關係。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2020 年多次全球登入失效是 identity cascading 的典型樣本。</li>
<li>2021 年 DNS / token service 問題則顯示 sub-component 也能放大成平台級風險。</li>
<li>Azure Service Health 與 M365 health console 是對外路由的關鍵。</li>
<li>token cache 會決定 outage 在使用者端維持多久。</li>
<li>identity 是所有 SSO 服務的基礎路由。</li>
<li>staged rollout 在 identity 服務上特別難做，因為影響面太大。</li>
<li>token service 與 DNS 故障會把身份驗證整體拉下來。</li>
<li>service health 變成客戶理解影響範圍的第一手資訊。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">AZ1</a></td>
          <td>身分控制面中斷</td>
          <td>盤點跨產品身份依賴與分級回復順序</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-sla-performance">Service Level Agreement performance for Microsoft Entra ID</a>：Entra ID 的 SLA / incident history 入口。</li>
<li><a href="https://learn.microsoft.com/en-us/azure/service-health/service-health-overview">What is Azure Service Health?</a>：Azure Service Health 與 status / advisories 的官方說明。</li>
<li><a href="https://learn.microsoft.com/microsoft-365/enterprise/view-service-health?view=o365-worldwide">How to check Microsoft 365 service health</a>：M365/Entra 相關 health console 的用法。</li>
<li><a href="https://learn.microsoft.com/mt-mt/azure/reliability">Azure reliability documentation</a>：Azure 可靠性文件總入口。</li>
</ul>
]]></content:encoded></item><item><title>0.15 跨模組 Checkout Episode：從資料寫入到觀測證據</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cross-module-checkout-episode/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cross-module-checkout-episode/</guid><description>&lt;p>跨模組 checkout episode 的核心責任是用同一條服務路徑，把資料庫、快取、訊息佇列與可觀測性四個模組的責任串在一起。讀者看完後能判斷一次 checkout 請求觸發的狀態寫入、快取失效、事件發布與訊號記錄分別由誰負責，以及任何一層失敗時該看哪組訊號。&lt;/p>
&lt;p>本篇與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13 操作控制 vertical slice&lt;/a> 互補：0.13 走的是 04/06/08 的操作控制閉環（觀測 → 驗證 → 事故 → 回寫），本篇走的是 01/02/03/04 的資料基礎設施鏈（狀態 → 副本 → 事件 → 訊號）。&lt;/p>
&lt;h2 id="服務路徑">服務路徑&lt;/h2>
&lt;p>一次 checkout 的最小路徑：&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">client
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → checkout-api
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → order-db (01: 寫入正式狀態)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → cache invalidation (02: 失效商品快取)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> → event publish (03: 發布 order.created 事件)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> → telemetry (04: span / log / metric 記錄)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條路徑刻意簡化。真實系統可能還有 payment adapter、inventory lock、notification service、search index sync 等環節，但四層串聯的責任分工用最小路徑就能說明。後續章節把各層展開。&lt;/p>
&lt;h2 id="第一層資料庫寫入01">第一層：資料庫寫入（01）&lt;/h2>
&lt;p>Checkout 的正式狀態是訂單紀錄。這筆寫入必須在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary&lt;/a> 內完成，確保訂單、明細與付款紀錄一起成功或一起失敗。&lt;/p>
&lt;p>&lt;strong>責任邊界&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>訂單狀態是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，快取和事件都是下游副本&lt;/li>
&lt;li>Transaction 範圍盡量小：寫入訂單 + 明細 + outbox record，不在同一個 transaction 裡做外部 API 呼叫&lt;/li>
&lt;li>Schema 需要支援狀態演進：訂單從 &lt;code>pending&lt;/code> → &lt;code>paid&lt;/code> → &lt;code>shipped&lt;/code> 的欄位設計見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>失敗判讀&lt;/strong>：&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>Transaction timeout&lt;/td>
 &lt;td>連線池飽和或長 transaction 鎖等待&lt;/td>
 &lt;td>回 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發讀寫邊界&lt;/a> 檢查連線池與 transaction 範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deadlock&lt;/td>
 &lt;td>多個 checkout 同時更新重疊資源&lt;/td>
 &lt;td>回 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary&lt;/a> 檢查 lock ordering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema migration 中斷&lt;/td>
 &lt;td>欄位變更與正在執行的寫入衝突&lt;/td>
 &lt;td>回 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 migration playbook&lt;/a> 確認 expand/contract 流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>交接給下一層的資訊&lt;/strong>：transaction commit 成功後，訂單 ID 與狀態就緒。Outbox record 已寫入同一個 transaction。&lt;/p></description><content:encoded><![CDATA[<p>跨模組 checkout episode 的核心責任是用同一條服務路徑，把資料庫、快取、訊息佇列與可觀測性四個模組的責任串在一起。讀者看完後能判斷一次 checkout 請求觸發的狀態寫入、快取失效、事件發布與訊號記錄分別由誰負責，以及任何一層失敗時該看哪組訊號。</p>
<p>本篇與 <a href="/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13 操作控制 vertical slice</a> 互補：0.13 走的是 04/06/08 的操作控制閉環（觀測 → 驗證 → 事故 → 回寫），本篇走的是 01/02/03/04 的資料基礎設施鏈（狀態 → 副本 → 事件 → 訊號）。</p>
<h2 id="服務路徑">服務路徑</h2>
<p>一次 checkout 的最小路徑：</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">client
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → checkout-api
</span></span><span class="line"><span class="ln">3</span><span class="cl">    → order-db          (01: 寫入正式狀態)
</span></span><span class="line"><span class="ln">4</span><span class="cl">    → cache invalidation (02: 失效商品快取)
</span></span><span class="line"><span class="ln">5</span><span class="cl">    → event publish      (03: 發布 order.created 事件)
</span></span><span class="line"><span class="ln">6</span><span class="cl">    → telemetry          (04: span / log / metric 記錄)</span></span></code></pre></div><p>這條路徑刻意簡化。真實系統可能還有 payment adapter、inventory lock、notification service、search index sync 等環節，但四層串聯的責任分工用最小路徑就能說明。後續章節把各層展開。</p>
<h2 id="第一層資料庫寫入01">第一層：資料庫寫入（01）</h2>
<p>Checkout 的正式狀態是訂單紀錄。這筆寫入必須在 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 內完成，確保訂單、明細與付款紀錄一起成功或一起失敗。</p>
<p><strong>責任邊界</strong>：</p>
<ul>
<li>訂單狀態是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，快取和事件都是下游副本</li>
<li>Transaction 範圍盡量小：寫入訂單 + 明細 + outbox record，不在同一個 transaction 裡做外部 API 呼叫</li>
<li>Schema 需要支援狀態演進：訂單從 <code>pending</code> → <code>paid</code> → <code>shipped</code> 的欄位設計見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a></li>
</ul>
<p><strong>失敗判讀</strong>：</p>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Transaction timeout</td>
          <td>連線池飽和或長 transaction 鎖等待</td>
          <td>回 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發讀寫邊界</a> 檢查連線池與 transaction 範圍</td>
      </tr>
      <tr>
          <td>Deadlock</td>
          <td>多個 checkout 同時更新重疊資源</td>
          <td>回 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a> 檢查 lock ordering</td>
      </tr>
      <tr>
          <td>Schema migration 中斷</td>
          <td>欄位變更與正在執行的寫入衝突</td>
          <td>回 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 migration playbook</a> 確認 expand/contract 流程</td>
      </tr>
  </tbody>
</table>
<p><strong>交接給下一層的資訊</strong>：transaction commit 成功後，訂單 ID 與狀態就緒。Outbox record 已寫入同一個 transaction。</p>
<h2 id="第二層快取失效02">第二層：快取失效（02）</h2>
<p>訂單成功後，商品庫存或價格的快取副本可能已經過期。快取失效的責任是讓後續讀取拿到正確狀態，同時保護資料庫不被回源壓力打穿。</p>
<p><strong>責任邊界</strong>：</p>
<ul>
<li>快取是 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">可重建副本</a>，資料來源是資料庫的正式狀態。失效後的 cache miss 會回源到資料庫</li>
<li>失效策略用 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside</a>：寫入後主動 invalidate，下次讀取時 lazy reload</li>
<li>Invalidation 的順序：先 invalidate 應用層快取（Redis），再考慮是否需要 purge CDN 層（若商品頁有 edge cache）</li>
</ul>
<p><strong>失敗判讀</strong>：</p>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Invalidation 失敗但 DB 已 commit</td>
          <td>快取短暫提供舊資料，freshness window 內自動修正</td>
          <td>確認 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL</a> 是否足夠短，或補 retry</td>
      </tr>
      <tr>
          <td>Cache stampede</td>
          <td>大量 invalidation 同時觸發 origin 回源</td>
          <td>回 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 cache migration stampede rollback</a> 補 singleflight 或 lock</td>
      </tr>
      <tr>
          <td>Hot key 集中失效</td>
          <td>單一商品被大量並發 checkout 同時 invalidate</td>
          <td>回 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發讀寫邊界</a> 檢查 hot key 分散策略</td>
      </tr>
  </tbody>
</table>
<p><strong>交接給下一層的資訊</strong>：快取失效完成（或 TTL 保底）。接下來的事件發布不依賴快取狀態 — 事件內容來自 DB 寫入結果。</p>
<h2 id="第三層事件發布03">第三層：事件發布（03）</h2>
<p>訂單寫入後，<code>order.created</code> 事件需要傳遞到下游：通知服務寄信、庫存服務更新、搜尋索引同步、分析管道記錄。這些下游不在 checkout request 內完成，要用非同步傳遞。</p>
<p><strong>責任邊界</strong>：</p>
<ul>
<li>事件發布與 DB 寫入的一致性用 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">outbox pattern</a>：outbox record 在 DB transaction 內寫入，poller 或 CDC 負責把 record 發到 broker</li>
<li>Broker 保證 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">at-least-once delivery</a>，consumer 需要做 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 處理</li>
<li>Event contract（schema、idempotency key、replay window）見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract replay boundary</a></li>
</ul>
<p><strong>失敗判讀</strong>：</p>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Outbox poller 延遲</td>
          <td>事件延遲但不遺失，DB 已 commit</td>
          <td>監控 outbox table 的 pending row count，回 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a></td>
      </tr>
      <tr>
          <td>Consumer lag 上升</td>
          <td>下游處理速度跟不上，事件在 broker 堆積</td>
          <td>回 <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 design</a> 檢查 consumer 數量與 backpressure</td>
      </tr>
      <tr>
          <td>DLQ 堆積</td>
          <td>毒訊息或下游持續失敗，已超過 retry 預算</td>
          <td>回 <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。">3.8 retry replay handoff</a> 啟動 DLQ drain runbook</td>
      </tr>
      <tr>
          <td>重複事件造成下游重複副作用</td>
          <td>Consumer idempotency 沒擋住</td>
          <td>回 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a> 確認去重機制</td>
      </tr>
  </tbody>
</table>
<p><strong>交接給下一層的資訊</strong>：事件已發到 broker，每一步（publish、ack、consume、DLQ）都需要觀測訊號。</p>
<h2 id="第四層觀測訊號04">第四層：觀測訊號（04）</h2>
<p>以上三層的每一步都需要被記錄成可查詢的訊號。Checkout 路徑的觀測責任是讓事故判讀者能用同一組 trace ID 串起完整鏈路。</p>
<p><strong>責任邊界</strong>：</p>
<ul>
<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">Trace context</a> 從 client 一路 propagate 到 consumer，跨 sync（HTTP）與 async（queue）邊界</li>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">Log schema</a> 使用統一欄位：<code>order_id</code>、<code>trace_id</code>、<code>tenant_id</code>、<code>region</code></li>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics</a> 覆蓋三組 SLI：checkout latency（p50/p95/p99）、checkout error rate、event publish lag</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">Dashboard</a> 把上述三組 SLI 放在同一個 checkout 服務面板</li>
<li><a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">Evidence package</a> 把查詢、時間窗、資料品質與 owner 打包成可交接證據</li>
</ul>
<p><strong>失敗判讀</strong>：</p>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trace 在 DB commit 後斷鏈</td>
          <td>Context propagation 沒跨到 async 邊界</td>
          <td>回 <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> 補 queue span link</td>
      </tr>
      <tr>
          <td>Checkout metric 正常但客訴增加</td>
          <td>觀測盲區或 sampling 偏差</td>
          <td>回 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> 標示 known gap</td>
      </tr>
      <tr>
          <td>Alert 太吵但真正事件沒被抓到</td>
          <td>告警粒度與閾值設計問題</td>
          <td>回 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard alert</a> 調整 symptom-based alert</td>
      </tr>
      <tr>
          <td>訊號延遲導致事故判讀困難</td>
          <td>Pipeline ingest delay 或 metric scrape interval 太長</td>
          <td>回 <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 健康</td>
      </tr>
  </tbody>
</table>
<h2 id="四層交接總覽">四層交接總覽</h2>





<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">┌─────────────┐    commit     ┌──────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">│  01 DB      │──────────────→│  02 Cache    │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│  order-db   │    ok         │  invalidate  │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│  write      │               │  product key │
</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">       │ outbox
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">       │ record
</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></span><span class="line"><span class="ln">10</span><span class="cl">│  03 Event   │
</span></span><span class="line"><span class="ln">11</span><span class="cl">│  publish    │
</span></span><span class="line"><span class="ln">12</span><span class="cl">│  order.     │
</span></span><span class="line"><span class="ln">13</span><span class="cl">│  created    │
</span></span><span class="line"><span class="ln">14</span><span class="cl">└─────────────┘
</span></span><span class="line"><span class="ln">15</span><span class="cl">       │
</span></span><span class="line"><span class="ln">16</span><span class="cl">       │ all layers emit
</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></span><span class="line"><span class="ln">19</span><span class="cl">│  04 Observability        │
</span></span><span class="line"><span class="ln">20</span><span class="cl">│  span + log + metric     │
</span></span><span class="line"><span class="ln">21</span><span class="cl">│  per layer               │
</span></span><span class="line"><span class="ln">22</span><span class="cl">└──────────────────────────┘</span></span></code></pre></div><p>每一層都有明確的失敗判讀與交接資訊。四層合在一起的判讀順序是：先看 04 的 trace 確認斷點在哪一層，再進那一層的失敗訊號表。</p>
<h2 id="跨層失敗場景">跨層失敗場景</h2>
<p>單層失敗表只處理各自的責任。跨層失敗需要同時看多組訊號：</p>
<h3 id="db-commit-成功但快取沒失效且事件沒發出">DB commit 成功，但快取沒失效且事件沒發出</h3>
<p>原因通常是 outbox poller 和 cache invalidation 在同一個 request 內串行、前者失敗後沒做到後者。判讀順序：</p>
<ol>
<li>04 的 trace 看 checkout span 是否有 error tag</li>
<li>01 的 outbox table 看 pending row 是否堆積</li>
<li>02 的 cache key 是否仍是舊值（TTL 保底正常時可接受）</li>
</ol>
<p>修正方向：invalidation 和 outbox 解耦 — invalidation 在 DB commit 後同步執行（失敗可 retry），outbox 非同步由 poller 負責。兩者不應互相阻塞。</p>
<h3 id="event-consumer-重複處理造成庫存扣兩次">Event consumer 重複處理造成庫存扣兩次</h3>
<p>原因是 consumer 的 idempotency 沒做好，broker redelivery 導致重複副作用。判讀順序：</p>
<ol>
<li>04 的 consumer span 看 redelivery count</li>
<li>03 的 DLQ 看是否有 poison message</li>
<li>01 的 inventory table 看同一 order_id 是否有多筆扣減</li>
</ol>
<p>修正方向：回 <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 design</a> 補 idempotency key 驗證，用 order_id 當去重鍵。</p>
<h3 id="checkout-latency-上升但-db-和-cache-都正常">Checkout latency 上升但 DB 和 cache 都正常</h3>
<p>原因可能是 outbox poller 或 event publish 在 request path 內同步等待（設計錯誤）。判讀順序：</p>
<ol>
<li>04 的 checkout span 看 child span 時間分布</li>
<li>確認 event publish 是否在 request 返回前完成（不該）</li>
<li>如果是，回到 03 確認 outbox pattern 是否正確實作（寫 outbox record 應在 DB transaction 內、publish 應由 poller 異步執行）</li>
</ol>
<h2 id="各模組回讀路由">各模組回讀路由</h2>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>主要回讀章節</th>
          <th>回讀時機</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>01 DB</td>
          <td><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a>、<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a>、<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7</a></td>
          <td>transaction 或 schema 問題</td>
      </tr>
      <tr>
          <td>02 Cache</td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9</a></td>
          <td>invalidation 或 stampede 問題</td>
      </tr>
      <tr>
          <td>03 Event</td>
          <td><a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3</a>、<a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4</a>、<a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6</a>、<a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7</a></td>
          <td>delivery、idempotency 或 replay 問題</td>
      </tr>
      <tr>
          <td>04 Observability</td>
          <td><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</a>、<a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>、<a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22</a></td>
          <td>訊號斷鏈、盲區或 evidence 問題</td>
      </tr>
      <tr>
          <td>操作閉環</td>
          <td><a href="/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13</a></td>
          <td>從訊號進入驗證、事故與回寫流程</td>
      </tr>
  </tbody>
</table>
<h2 id="使用方式">使用方式</h2>
<p>本篇是索引型讀物。讀者第一次讀時順著四層走一遍，建立跨模組的交接心智模型。之後遇到具體問題時，用失敗訊號表定位到對應模組的章節。</p>
<p>已經有某一層經驗的讀者可以從那一層開始讀，看該層與相鄰層的交接欄位是否對齊。資料庫工程師從第一層開始看事件發布的交接；觀測工程師從第四層反推前三層需要哪些欄位。</p>
<p>本篇不處理 payment adapter、inventory lock、notification 等更複雜的分支。這些分支的模式相同 — 確認責任邊界、交接欄位與失敗判讀 — 讀者可以自行延伸。</p>
]]></content:encoded></item><item><title>4.15 Cost Attribution / Chargeback</title><link>https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何需要 attribution：共享平台模式下成本無人擁有&lt;/li>
&lt;li>拆分維度：team / service / environment / tenant / cost driver&lt;/li>
&lt;li>拆分的訊號來源：metric label / log tag / span attribute&lt;/li>
&lt;li>Showback vs chargeback&lt;/li>
&lt;li>Attribution dashboard 設計&lt;/li>
&lt;li>Vendor 帳單拆分能力&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Cost attribution 是把 observability 成本拆回團隊、服務、環境與成本來源的治理能力，責任是讓使用訊號的人也看見訊號成本。&lt;/p>
&lt;p>Observability 平台（自架或託管）的成本來自三個層面：ingestion（收了多少資料）、storage / retention（保留了多久）、query（查了多少次跟多大範圍）。沒有 attribution 時，這三層的成本由平台團隊背，產品團隊把 observability 當免費資源 — 新增 metric label、延長 retention、加 dashboard panel 都沒有成本意識。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality&lt;/a> 的分工：4.7 是技術治理工具（控制 cardinality、sampling、retention 階梯），4.15 是組織治理工具（讓成本對應到 owner、驅動 owner 採取行動）。&lt;/p>
&lt;h2 id="拆分維度">拆分維度&lt;/h2>
&lt;h3 id="按-service--team">按 service / team&lt;/h3>
&lt;p>最基本的拆分。每個服務產生的 ingestion 量（events/sec、series count、log volume）歸到服務 owner。團隊是多個服務的集合。&lt;/p>
&lt;p>實作方式：metric 跟 log 的 &lt;code>service&lt;/code> label / tag 是拆分的基礎。如果 label 穩定且全覆蓋，用 &lt;code>sum by (service)&lt;/code> 就能拆分 ingestion 成本。Label 不穩定（部分服務沒打 service tag）或 label 值漂移（service name 改名但 cost 系統沒更新）會讓拆分不準。&lt;/p>
&lt;h3 id="按-environment">按 environment&lt;/h3>
&lt;p>Production / staging / dev 環境的成本各自歸因。常見發現是 staging 環境的 observability 成本跟 production 相當 — staging 開了跟 production 一樣的 retention、sampling 率、dashboard，但 staging 的觀測需求遠低於 production。&lt;/p>
&lt;p>可操作的做法：staging 跟 dev 環境用更短的 retention（7 天 vs production 的 30 天）、更高的 sampling 比例、關閉不需要的 dashboard。把 environment 的成本差異展示在 attribution dashboard 上，讓團隊自行判斷 staging 的 observability 是否過度。&lt;/p>
&lt;h3 id="按-cost-driver-type">按 cost driver type&lt;/h3>
&lt;p>Ingestion / storage / query 三層的成本增長模式不同、控制手段也不同。&lt;/p>
&lt;p>&lt;strong>Ingestion 成本&lt;/strong>：跟 events/sec 跟 series count 成正比。控制手段是 sampling、cardinality 限制、低價值訊號過濾。歸因到產生訊號的服務。&lt;/p>
&lt;p>&lt;strong>Storage / retention 成本&lt;/strong>：跟資料量 × 保留期成正比。控制手段是 retention 階梯（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a>。歸因到資料保留政策的 owner。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何需要 attribution：共享平台模式下成本無人擁有</li>
<li>拆分維度：team / service / environment / tenant / cost driver</li>
<li>拆分的訊號來源：metric label / log tag / span attribute</li>
<li>Showback vs chargeback</li>
<li>Attribution dashboard 設計</li>
<li>Vendor 帳單拆分能力</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Cost attribution 是把 observability 成本拆回團隊、服務、環境與成本來源的治理能力，責任是讓使用訊號的人也看見訊號成本。</p>
<p>Observability 平台（自架或託管）的成本來自三個層面：ingestion（收了多少資料）、storage / retention（保留了多久）、query（查了多少次跟多大範圍）。沒有 attribution 時，這三層的成本由平台團隊背，產品團隊把 observability 當免費資源 — 新增 metric label、延長 retention、加 dashboard panel 都沒有成本意識。</p>
<p>跟 <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> 的分工：4.7 是技術治理工具（控制 cardinality、sampling、retention 階梯），4.15 是組織治理工具（讓成本對應到 owner、驅動 owner 採取行動）。</p>
<h2 id="拆分維度">拆分維度</h2>
<h3 id="按-service--team">按 service / team</h3>
<p>最基本的拆分。每個服務產生的 ingestion 量（events/sec、series count、log volume）歸到服務 owner。團隊是多個服務的集合。</p>
<p>實作方式：metric 跟 log 的 <code>service</code> label / tag 是拆分的基礎。如果 label 穩定且全覆蓋，用 <code>sum by (service)</code> 就能拆分 ingestion 成本。Label 不穩定（部分服務沒打 service tag）或 label 值漂移（service name 改名但 cost 系統沒更新）會讓拆分不準。</p>
<h3 id="按-environment">按 environment</h3>
<p>Production / staging / dev 環境的成本各自歸因。常見發現是 staging 環境的 observability 成本跟 production 相當 — staging 開了跟 production 一樣的 retention、sampling 率、dashboard，但 staging 的觀測需求遠低於 production。</p>
<p>可操作的做法：staging 跟 dev 環境用更短的 retention（7 天 vs production 的 30 天）、更高的 sampling 比例、關閉不需要的 dashboard。把 environment 的成本差異展示在 attribution dashboard 上，讓團隊自行判斷 staging 的 observability 是否過度。</p>
<h3 id="按-cost-driver-type">按 cost driver type</h3>
<p>Ingestion / storage / query 三層的成本增長模式不同、控制手段也不同。</p>
<p><strong>Ingestion 成本</strong>：跟 events/sec 跟 series count 成正比。控制手段是 sampling、cardinality 限制、低價值訊號過濾。歸因到產生訊號的服務。</p>
<p><strong>Storage / retention 成本</strong>：跟資料量 × 保留期成正比。控制手段是 retention 階梯（<a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a>。歸因到資料保留政策的 owner。</p>
<p><strong>Query 成本</strong>：跟查詢次數 × 掃描量成正比。控制手段是 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a>、query cache、query cost estimation（<a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a>）。歸因到 dashboard 跟 alert rule 的 owner。</p>
<p>三層分開歸因的價值是精確定位成本增長來源。「這個月成本增長 30%」→ 是 ingestion 增長（某服務開了新 metric）還是 query 增長（某人加了 heavy dashboard panel）？分層歸因讓回答這個問題只需要查一個 dashboard。</p>
<h3 id="按-tenant多租戶場景">按 tenant（多租戶場景）</h3>
<p>Multi-tenant 平台的 observability 成本跟 tenant 的活躍度有關。大 tenant 產生的事件量可能是小 tenant 的 100 倍，但如果 observability 成本平攤，小 tenant 補貼大 tenant。</p>
<p>Tenant-level attribution 需要 metric / log / trace 帶 tenant label。Label 的 cardinality 問題在 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a> 處理 — tenant label 在 metric 層通常過高 cardinality（每個 tenant 一條 series），可以改在 log 或 trace 層按 tenant 統計 ingestion 量。</p>
<h2 id="showback-vs-chargeback">Showback vs Chargeback</h2>
<p><strong>Showback</strong>：讓團隊看到自己產生的 observability 成本，但不實際扣款。透明化驅動行為改變 — 當 team A 發現自己的 log ingestion 成本是其他團隊的 5 倍時，自然會開始檢視「是不是 debug log 開太多」。</p>
<p><strong>Chargeback</strong>：把 observability 成本從團隊的預算中實際扣除。驅動力更強，但需要精確的 attribution（誤差會讓團隊不信任系統）跟組織層面的支持（財務流程、管理層買單）。</p>
<p>多數團隊的起步方式是 showback。Showback 的 attribution 精度要求比 chargeback 低 — 差 10-20% 的歸因不影響行為改變的驅動力。Chargeback 需要差 &lt; 5% 才能讓團隊接受。</p>
<h2 id="attribution-dashboard-設計">Attribution Dashboard 設計</h2>
<p>Attribution dashboard 回答三個問題：</p>
<ol>
<li><strong>誰在燒？</strong> — 按 service / team 排序的成本排行榜。前 10 個服務通常佔 70-80% 的成本。</li>
<li><strong>燒在哪一層？</strong> — 前 10 個服務的 ingestion / storage / query 成本比例。</li>
<li><strong>趨勢是什麼？</strong> — 月對月的成本趨勢、哪些服務的成本增長最快。</li>
</ol>
<p>Dashboard 的更新頻率可以低（每天或每週），因為 attribution 驅動的是策略決策而非即時操作。Panel 讀 pre-aggregated 資料（daily cost summary table），查詢成本本身很低。</p>
<p>Attribution dashboard 的 owner 是 observability platform team，但 actionable insight 的 owner 是各服務團隊。Platform team 負責維護 attribution 的精確性跟 dashboard 的正確性；服務團隊負責看自己的成本趨勢跟採取控制行動。</p>
<h2 id="vendor-帳單拆分能力">Vendor 帳單拆分能力</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>帳單拆分能力</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog</td>
          <td>Usage attribution by tag（service / team / env）</td>
          <td>需要事先定義 attribution tag</td>
      </tr>
      <tr>
          <td>Honeycomb</td>
          <td>Team-based usage tracking</td>
          <td>按 dataset 拆分、不按 service</td>
      </tr>
      <tr>
          <td>Grafana Cloud</td>
          <td>Usage dashboard by data source</td>
          <td>需自建 attribution layer</td>
      </tr>
      <tr>
          <td>自架 Prometheus + Loki</td>
          <td>自建 cost model（series count × price / log volume × price）</td>
          <td>完全自定義但維護成本高</td>
      </tr>
  </tbody>
</table>
<p>自架的 attribution 精度最高（因為完全可控），但維護成本也最高。託管 vendor 通常提供 service 或 team 級的 usage attribution，但跨 ingestion / storage / query 的分層拆分需要用 vendor API 自建 dashboard。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Cost attribution 的核心目標是讓成本對應到能採取行動的 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner</a> — 成本只有總額而無歸屬時，沒有團隊有動力控制。</p>
<p>重點訊號包括：</p>
<ul>
<li>Ingestion、retention、query 是否能分開歸因</li>
<li>Team / service / environment label 是否穩定</li>
<li>Showback 是否足以改變行為，或需要 chargeback</li>
<li>高成本訊號是否能對應事故、SLO 或除錯價值</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>成本季度增長、無人能說「哪個團隊 / 服務在燒」</li>
<li>高成本服務跟高價值服務不對應、無 ROI 視角</li>
<li>平台團隊背所有預算、產品團隊把 observability 當免費資源</li>
<li>Attribution dashboard 存在但無 owner、半年沒看</li>
<li>Vendor 帳單只有總額、無服務級拆分</li>
<li>Staging 的 observability 成本跟 production 相當但無人注意</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台吸收所有成本</td>
          <td>產品團隊沒成本意識、ingestion 無限增長</td>
          <td>Showback 起步、讓團隊看到自己的成本</td>
      </tr>
      <tr>
          <td>Attribution 顆粒度太粗</td>
          <td>只有總額、定位成本來源要人工拆帳</td>
          <td>按 service + cost driver type 拆分</td>
      </tr>
      <tr>
          <td>Chargeback 精度不夠</td>
          <td>團隊質疑歸因結果、不信任系統</td>
          <td>先用 showback、精度穩定後再轉 chargeback</td>
      </tr>
      <tr>
          <td>Attribution label 漂移</td>
          <td>Service name 改了但 cost 系統沒更新</td>
          <td>Label 同步機制 + 定期 reconciliation</td>
      </tr>
      <tr>
          <td>成本只看帳單不看 ROI</td>
          <td>砍最貴的 metric 但那是 SLO 唯一訊號來源</td>
          <td>成本決策同時評估「砍掉後事故定位會變慢多少」</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<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 / cost</a>：技術層面的成本治理工具</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 各層的成本歸屬</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：platform team 跟 service team 的 cost ownership</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：query 成本的 estimation 跟治理</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity / cost</a>：observability 成本作為整體容量規劃的一部分</li>
<li><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14 觀測平台成本治理</a>：從帳單驚嚇到可預測成本的綜合情境</li>
</ul>
]]></content:encoded></item><item><title>Firestore realtime listener 扇出與成本：snapshot 訂閱、re-read 計費與連線規模</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/realtime-listener-fanout-cost/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/realtime-listener-fanout-cost/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore&lt;/a> overview 的 deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論&lt;/a>。計費模型以 &lt;a href="https://firebase.google.com/docs/firestore/pricing">官方 pricing&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境即時很爽帳單很痛">問題情境：即時很爽，帳單很痛&lt;/h2>
&lt;p>Firestore 的 snapshot listener 是它最有吸引力的能力——client &lt;code>onSnapshot&lt;/code> 訂閱一個 query，資料一變就即時推送，多裝置同步、協作介面幾乎免費得到。團隊很快把所有列表都改成 listener：訊息列表、通知、儀表板計數，全部即時更新，體驗很好。&lt;/p>
&lt;p>帳單在用戶量上來後出問題。Firestore 對 listener 的計費規則是——query 結果裡每個被推送的 document 都計一次 read。一個列表有 100 名觀眾各自訂閱、列表變動推送 50 筆，就是 100 × 50 = 5000 次 read。即時的爽感建立在 re-read 計費上，扇出越大、變動越頻繁，成本成乘積成長。這篇處理 listener 的推送與計費模型、如何設計訂閱範圍把成本壓住、以及即時需求超過 listener 能力時的退場。&lt;/p>
&lt;h2 id="核心概念listener-的推送與計費模型">核心概念：listener 的推送與計費模型&lt;/h2>
&lt;p>snapshot listener 不是「推送變動的那一筆」這麼簡單。理解它的成本要抓三點：&lt;/p>
&lt;p>&lt;strong>初次訂閱讀整個結果集，之後讀變動的部分&lt;/strong>。&lt;code>onSnapshot(query)&lt;/code> 第一次觸發時，query 結果的每個 document 計一次 read（跟一次性 &lt;code>getDocs&lt;/code> 相同）。之後 query 結果有 document 新增、修改、移出，推送那些變動的 document，各計一次 read。所以 listener 的計費 = 初次結果集大小 + 後續每次變動推送的 document 數。&lt;/p>
&lt;p>&lt;strong>計費是 per-listener 的&lt;/strong>。同一個 query 被 N 個 client 各自訂閱，是 N 個獨立 listener，變動推送計 N 次。扇出（同一資料多少人在看）直接乘進成本。這跟自建後端用一個 WebSocket broadcast 推給 N 個連線的模型不同——那裡資料讀一次、推 N 份；Firestore listener 是每個訂閱各自從資料庫讀。&lt;/p>
&lt;p>&lt;strong>query 範圍決定推送頻率&lt;/strong>。訂閱一個寬的 query（整個 collection），collection 裡任何符合的 document 變動都推；訂閱窄的 query（只我相關的那幾筆），只有那幾筆變動才推。listener 成本的設計槓桿是「把訂閱範圍縮到 client 真正要即時看到的最小集合」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">onSnapshot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">query&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">collection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">where&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">orderBy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">limit&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;firebase/firestore&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">// 寬訂閱：整個 messages collection 任何變動都推（成本失控）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">wide&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;messages&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">// 窄訂閱：只訂這個對話的最近 50 則（成本可控）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">narrow&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">collection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;messages&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">where&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;conversationId&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;==&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">convId&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">orderBy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;createdAt&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;desc&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">limit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">unsub&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">onSnapshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">narrow&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">snap&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">snap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">docChanges&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">change&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 只處理變動的部分，不是每次重畫整個列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">change&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">type&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;added&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="cm">/* ... */&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">change&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">type&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;modified&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="cm">/* ... */&lt;/span> &lt;span class="p">}&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">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">change&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">type&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;removed&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="cm">/* ... */&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&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="c1">// 畫面離開時務必取消訂閱，否則 listener 與計費持續
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1">// unsub();
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>docChanges()&lt;/code> 是控制成本與效能的關鍵——它只給「跟上次相比變動的 document」，而不是每次都拿整個結果集重畫。用 &lt;code>limit&lt;/code> 把結果集封頂、用 &lt;code>where&lt;/code> 把範圍縮到 client 相關，是 listener 成本設計的兩個主要手段。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore</a> overview 的 deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a>。計費模型以 <a href="https://firebase.google.com/docs/firestore/pricing">官方 pricing</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="問題情境即時很爽帳單很痛">問題情境：即時很爽，帳單很痛</h2>
<p>Firestore 的 snapshot listener 是它最有吸引力的能力——client <code>onSnapshot</code> 訂閱一個 query，資料一變就即時推送，多裝置同步、協作介面幾乎免費得到。團隊很快把所有列表都改成 listener：訊息列表、通知、儀表板計數，全部即時更新，體驗很好。</p>
<p>帳單在用戶量上來後出問題。Firestore 對 listener 的計費規則是——query 結果裡每個被推送的 document 都計一次 read。一個列表有 100 名觀眾各自訂閱、列表變動推送 50 筆，就是 100 × 50 = 5000 次 read。即時的爽感建立在 re-read 計費上，扇出越大、變動越頻繁，成本成乘積成長。這篇處理 listener 的推送與計費模型、如何設計訂閱範圍把成本壓住、以及即時需求超過 listener 能力時的退場。</p>
<h2 id="核心概念listener-的推送與計費模型">核心概念：listener 的推送與計費模型</h2>
<p>snapshot listener 不是「推送變動的那一筆」這麼簡單。理解它的成本要抓三點：</p>
<p><strong>初次訂閱讀整個結果集，之後讀變動的部分</strong>。<code>onSnapshot(query)</code> 第一次觸發時，query 結果的每個 document 計一次 read（跟一次性 <code>getDocs</code> 相同）。之後 query 結果有 document 新增、修改、移出，推送那些變動的 document，各計一次 read。所以 listener 的計費 = 初次結果集大小 + 後續每次變動推送的 document 數。</p>
<p><strong>計費是 per-listener 的</strong>。同一個 query 被 N 個 client 各自訂閱，是 N 個獨立 listener，變動推送計 N 次。扇出（同一資料多少人在看）直接乘進成本。這跟自建後端用一個 WebSocket broadcast 推給 N 個連線的模型不同——那裡資料讀一次、推 N 份；Firestore listener 是每個訂閱各自從資料庫讀。</p>
<p><strong>query 範圍決定推送頻率</strong>。訂閱一個寬的 query（整個 collection），collection 裡任何符合的 document 變動都推；訂閱窄的 query（只我相關的那幾筆），只有那幾筆變動才推。listener 成本的設計槓桿是「把訂閱範圍縮到 client 真正要即時看到的最小集合」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">onSnapshot</span><span class="p">,</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">collection</span><span class="p">,</span> <span class="nx">where</span><span class="p">,</span> <span class="nx">orderBy</span><span class="p">,</span> <span class="nx">limit</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;firebase/firestore&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// 寬訂閱：整個 messages collection 任何變動都推（成本失控）
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">wide</span> <span class="o">=</span> <span class="nx">query</span><span class="p">(</span><span class="nx">collection</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="s1">&#39;messages&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 窄訂閱：只訂這個對話的最近 50 則（成本可控）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">narrow</span> <span class="o">=</span> <span class="nx">query</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">collection</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="s1">&#39;messages&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">where</span><span class="p">(</span><span class="s1">&#39;conversationId&#39;</span><span class="p">,</span> <span class="s1">&#39;==&#39;</span><span class="p">,</span> <span class="nx">convId</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">orderBy</span><span class="p">(</span><span class="s1">&#39;createdAt&#39;</span><span class="p">,</span> <span class="s1">&#39;desc&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">limit</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kr">const</span> <span class="nx">unsub</span> <span class="o">=</span> <span class="nx">onSnapshot</span><span class="p">(</span><span class="nx">narrow</span><span class="p">,</span> <span class="p">(</span><span class="nx">snap</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="nx">snap</span><span class="p">.</span><span class="nx">docChanges</span><span class="p">().</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">change</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="c1">// 只處理變動的部分，不是每次重畫整個列表
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">change</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;added&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">change</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;modified&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">change</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;removed&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">// 畫面離開時務必取消訂閱，否則 listener 與計費持續
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1">// unsub();
</span></span></span></code></pre></div><p><code>docChanges()</code> 是控制成本與效能的關鍵——它只給「跟上次相比變動的 document」，而不是每次都拿整個結果集重畫。用 <code>limit</code> 把結果集封頂、用 <code>where</code> 把範圍縮到 client 相關，是 listener 成本設計的兩個主要手段。</p>
<h2 id="配置訂閱範圍與生命週期設計">配置：訂閱範圍與生命週期設計</h2>
<p>listener 的成本與效能由訂閱範圍和生命週期決定。三個設計原則：</p>
<p><strong>訂閱跟著畫面生命週期</strong>。listener 在畫面進入時建立、離開時 <code>unsubscribe()</code>。最常見的成本洩漏是忘記取消訂閱——使用者切走了，listener 還在背景持續接收推送計費。在元件 unmount、路由切換、app 進背景時取消所有 listener。</p>
<p><strong>用 <code>limit</code> 封頂結果集，配分頁</strong>。即時列表只訂最近 N 筆，往前翻歷史用一次性 <code>getDocs</code> 分頁，不訂閱。歷史資料不會變、不需要即時，訂閱它只是白付 re-read。即時的部分小而精，歷史的部分按需一次性拉。</p>
<p><strong>高扇出的即時值改訂閱彙總 document</strong>。一萬名觀眾要看同一個即時計數，正解是由後端把彙總值寫進一個 summary document、所有人訂閱那一份，而非各自訂閱原始資料加總。扇出仍是一萬個 listener，但每次變動只推一份小 document，而不是推整個結果集——把推送的 payload 壓到最小。這跟 <a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter</a> 的 summary 彙總是同一個手段的兩面：那裡解寫入熱點，這裡解讀取扇出。</p>
<h2 id="故障演練五個-realtime-成本踩坑">故障演練：五個 realtime 成本踩坑</h2>
<h4 id="case-1把不需要即時的列表也做成-listener">Case 1：把不需要即時的列表也做成 listener</h4>
<p>歷史訊息、已讀通知、靜態設定全用 <code>onSnapshot</code>，這些資料根本不變或極少變，訂閱它們只是把一次性讀取變成持續掛著的 listener。修法：先問「這個資料 client 在看的時候會不會變、變了要不要立刻看到」，否才用 listener；不變或不需即時的用一次性 <code>getDocs</code>。</p>
<h4 id="case-2忘記-unsubscribe-造成-listener-洩漏">Case 2：忘記 unsubscribe 造成 listener 洩漏</h4>
<p>路由切換、元件重建時建了新 listener 沒取消舊的，listener 越積越多、計費持續、記憶體也漏。修法：listener 的建立與取消綁死畫面生命週期，用框架的 cleanup hook（React <code>useEffect</code> return、Vue <code>onUnmounted</code>）統一管理，app 進背景時主動斷。</p>
<h4 id="case-3訂閱寬-query-被無關變動轟炸">Case 3：訂閱寬 query 被無關變動轟炸</h4>
<p>訂了整個 <code>orders</code> collection 想看自己的訂單，結果別人的訂單一變也推給你（雖然規則可能擋讀，但寬 query 本身設計就錯）。修法：query 用 <code>where</code> 縮到 client 相關的最小集合，訂閱範圍與 <a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules</a> 的授權範圍對齊。</p>
<h4 id="case-4每次-snapshot-重畫整個列表">Case 4：每次 snapshot 重畫整個列表</h4>
<p><code>onSnapshot</code> callback 裡拿 <code>snap.docs</code> 整個重建 UI，而不用 <code>docChanges()</code>，列表大時每次推送都重畫、UI 卡頓。修法：用 <code>docChanges()</code> 只處理 added / modified / removed 的增量，UI 做局部更新。</p>
<h4 id="case-5高扇出直接訂閱原始資料">Case 5：高扇出直接訂閱原始資料</h4>
<p>直播觀看數讓每個觀眾訂閱原始事件流自己算，扇出 × 結果集大小的 re-read 爆炸。修法：後端彙總寫 summary document，觀眾訂閱 summary 一份，把推送 payload 與 re-read 都壓到最小。</p>
<h2 id="容量與觀測扇出--變動頻率的成本估算">容量與觀測：扇出 × 變動頻率的成本估算</h2>
<p>listener 成本估算的公式是 <code>初次訂閱 read + Σ(訂閱數 × 每次變動推送的 document 數)</code>。把它拆開算：高扇出（很多人訂同一資料）× 高變動頻率（資料常變）× 大結果集（每次推很多筆）三者相乘，是成本爆炸的組合；任一維壓低都有效。設計時對每個 listener 問這三維的量級，乘起來對照預算。</p>
<p>連線數也有規模考量：Firestore 對並行連線與 listener 有規模上限（以官方當前限制為準），超大扇出（百萬級同時在線）會撞到連線層的天花板，而不只是計費問題。觀測上要監控 read 用量的來源拆分——哪些 collection 的 read 來自 listener 推送、哪些來自一次性查詢，把 listener 的 re-read 成本獨立出來看，接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界</a>。</p>
<h2 id="邊界與整合即時需求超過-listener-該換推送架構">邊界與整合：即時需求超過 listener 該換推送架構</h2>
<p>snapshot listener 適合「中等扇出、client 要即時看到自己相關資料變動」的場景——協作編輯、聊天、個人通知、儀表板。它撐不住的訊號是扇出或變動頻率推高 re-read 成本到不划算，或連線規模撞到天花板：</p>
<ul>
<li><strong>超高扇出的廣播</strong>：百萬人看同一場直播的即時數據，per-listener 的 re-read 模型成本遠高於自建一次讀取、WebSocket broadcast 推 N 份的模型。這類純廣播（一份資料推給海量訂閱者）用專門的推送層（自建 WebSocket / SSE、或 pub/sub + 邊緣推送）更划算，見 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a> 的 fan-out 設計</li>
<li><strong>複雜事件處理的即時</strong>：即時推送需要先做跨資料聚合、過濾、轉換，listener 只能訂 query 結果、表達不了。這類要後端處理後再推，listener 不是合適的傳輸層</li>
<li><strong>即時是核心且規模化</strong>：當即時同步是產品核心且扇出規模化，整個即時層自建是 <a href="/blog/backend/01-database/vendors/firestore/migrate-to-relational/" data-link-title="從 Firestore 遷往自建 relational：撞牆驅動的 Type E 重建模、存取模型反轉與並行期" data-link-desc="Firestore → 自建後端 &#43; relational 不是匯資料而是反轉存取模型：client 直連變 API 中介、Security Rules 授權變後端授權、document 反正規化變正規 schema、realtime listener 與 offline 同步要重建；本文走 Type E paradigm shift 結構、展開為何字面遷移不成立、哪些該遷哪些先留、dual-write &#43; shadow read 階段化與遷出代價判讀">Firestore → 自建 relational</a> 裡「realtime / offline 要重建」這項工作量——遷移時這層最容易被低估</li>
</ul>
<p>判讀的起點是「這份即時是 client 看自己相關的少量資料，還是海量訂閱者看同一份廣播」。前者 listener 是正解，後者從一開始就該用推送架構，而不是把 listener 的扇出推到極限。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上層：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a>（realtime / offline 能力與容量特性）</li>
<li>sibling：<a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入</a>（summary 彙總的另一面）</li>
<li>授權對齊：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a>（訂閱範圍與授權範圍一致）</li>
<li>推送架構：<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a>（超高扇出 broadcast 的去處）</li>
<li>成本邊界：<a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
<li>官方：<a href="https://firebase.google.com/docs/firestore/pricing">Firestore pricing</a>、<a href="https://firebase.google.com/docs/firestore/query-data/listen">Listen to realtime updates</a></li>
</ul>
]]></content:encoded></item><item><title>Kafka Multi-tenant 治理：quota 限流、ACL 授權與 topic 生命週期</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/multi-tenant-quota-acl/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/multi-tenant-quota-acl/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka&lt;/a> overview「Multi-tenant 與配額治理」「Topic 生命週期治理」兩段的 implementation-layer deep article。Overview 說明這些議題對應哪些案例跟子議題、本文展開具體的 quota / ACL 配置、授權模型推導、故障徵兆與修法。&lt;/p>&lt;/blockquote>
&lt;h2 id="共享叢集的治理問題一個叢集多個互不信任的租戶">共享叢集的治理問題：一個叢集、多個互不信任的租戶&lt;/h2>
&lt;p>Multi-tenant Kafka 的核心問題是把一個物理叢集切成多個彼此隔離的邏輯空間、讓每個團隊用同一組 broker 卻不互相干擾。當 Kafka 從單一團隊的工具長成全公司的事件總線、叢集承載的不再是一條 pipeline、而是數十到數百個團隊的 producer 跟 consumer。這時叢集的瓶頸從「broker 夠不夠快」轉成「怎麼防止某個團隊的流量、權限、或 topic 失控波及其他所有人」。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">Uber 的 Kafka 平台演進&lt;/a>把這個轉換描述為「從單隊列問題提升到平台治理問題」。當事件平台服務眾多團隊、重點是配額、隔離、觀測與運維標準化、而非只擴 broker。擴 broker 解決的是總容量、解決不了「單一租戶吃光共享資源」這類隔離問題。&lt;/p>
&lt;p>共享叢集的治理分三個獨立的軸、各自處理不同的失控來源：&lt;/p>
&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>Quota（資源配額）&lt;/td>
 &lt;td>單租戶吃滿頻寬 / request 容量、餓死其他租戶&lt;/td>
 &lt;td>&lt;code>kafka-configs.sh&lt;/code> 設 byte rate&lt;/td>
 &lt;td>鄰居 producer 寫入卡死、consumer lag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ACL（存取授權）&lt;/td>
 &lt;td>租戶讀寫不屬於自己的 topic、或被未授權方寫入&lt;/td>
 &lt;td>&lt;code>kafka-acls.sh&lt;/code> + broker authorizer&lt;/td>
 &lt;td>資料外洩、跨租戶污染、誤刪 topic&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生命週期（治理）&lt;/td>
 &lt;td>死 topic 累積、partition 數爆炸壓垮 metadata 面&lt;/td>
 &lt;td>命名規範 + 活躍判準 + 自動回收&lt;/td>
 &lt;td>controller 變慢、rebalance 風暴&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三軸正交：quota 設好不代表權限對、ACL 鎖好不代表 topic 不會爆炸。下面逐軸展開、每軸都對應 production 踩過的失控場景。本文 quota 與 ACL 操作以 Kafka 4.2.0（KRaft 模式、&lt;code>apache/kafka:latest&lt;/code>）實機驗證。&lt;/p>
&lt;h2 id="quota把頻寬與-request-容量切給租戶">Quota：把頻寬與 request 容量切給租戶&lt;/h2>
&lt;p>Quota 是 broker 端對 client 的流量上限、由 broker 在超限時主動 throttle（延遲回應）而非拒絕、讓單一租戶無法把共享頻寬吃光。Kafka 的 quota 是 broker-side 強制、不依賴 client 自律 —— 即使 client 不配合、broker 也會在回應裡插入 throttle 延遲、把該 client 的有效吞吐壓回配額內。&lt;/p>
&lt;h3 id="三類-quota-度量">三類 quota 度量&lt;/h3>
&lt;p>Kafka quota 度量三種資源、對應三類飽和：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Quota 鍵&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>producer_byte_rate&lt;/code>&lt;/td>
 &lt;td>bytes/sec&lt;/td>
 &lt;td>單一 client 每秒寫入 broker 的 bytes&lt;/td>
 &lt;td>寫入端 network / disk I/O 飽和&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>consumer_byte_rate&lt;/code>&lt;/td>
 &lt;td>bytes/sec&lt;/td>
 &lt;td>單一 client 每秒從 broker 讀取的 bytes&lt;/td>
 &lt;td>讀取端 network 飽和、fan-out 過大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>request_percentage&lt;/code>&lt;/td>
 &lt;td>百分比&lt;/td>
 &lt;td>單一 client 佔用 broker request handler 的 CPU 時間&lt;/td>
 &lt;td>broker CPU 飽和、小訊息高頻請求&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前兩個 byte rate 防的是頻寬類飽和、適合「大訊息、穩定流量」的租戶。&lt;code>request_percentage&lt;/code> 防的是另一種失控 —— 某租戶送大量極小的 request（例如每筆一個 byte、每秒幾萬筆）、byte rate 看起來很低、卻把 broker 的 request handler thread 佔滿。這種「請求數爆炸但流量不大」的攻擊型 pattern 只有 &lt;code>request_percentage&lt;/code> 抓得到。一個 broker 預設有 N 個 request handler thread、&lt;code>request_percentage=200&lt;/code> 代表允許該 client 用掉 2 條 thread 的時間（100% = 1 條）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a> overview「Multi-tenant 與配額治理」「Topic 生命週期治理」兩段的 implementation-layer deep article。Overview 說明這些議題對應哪些案例跟子議題、本文展開具體的 quota / ACL 配置、授權模型推導、故障徵兆與修法。</p></blockquote>
<h2 id="共享叢集的治理問題一個叢集多個互不信任的租戶">共享叢集的治理問題：一個叢集、多個互不信任的租戶</h2>
<p>Multi-tenant Kafka 的核心問題是把一個物理叢集切成多個彼此隔離的邏輯空間、讓每個團隊用同一組 broker 卻不互相干擾。當 Kafka 從單一團隊的工具長成全公司的事件總線、叢集承載的不再是一條 pipeline、而是數十到數百個團隊的 producer 跟 consumer。這時叢集的瓶頸從「broker 夠不夠快」轉成「怎麼防止某個團隊的流量、權限、或 topic 失控波及其他所有人」。</p>
<p><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">Uber 的 Kafka 平台演進</a>把這個轉換描述為「從單隊列問題提升到平台治理問題」。當事件平台服務眾多團隊、重點是配額、隔離、觀測與運維標準化、而非只擴 broker。擴 broker 解決的是總容量、解決不了「單一租戶吃光共享資源」這類隔離問題。</p>
<p>共享叢集的治理分三個獨立的軸、各自處理不同的失控來源：</p>
<table>
  <thead>
      <tr>
          <th>治理軸</th>
          <th>防的是什麼</th>
          <th>工具</th>
          <th>失控後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quota（資源配額）</td>
          <td>單租戶吃滿頻寬 / request 容量、餓死其他租戶</td>
          <td><code>kafka-configs.sh</code> 設 byte rate</td>
          <td>鄰居 producer 寫入卡死、consumer lag</td>
      </tr>
      <tr>
          <td>ACL（存取授權）</td>
          <td>租戶讀寫不屬於自己的 topic、或被未授權方寫入</td>
          <td><code>kafka-acls.sh</code> + broker authorizer</td>
          <td>資料外洩、跨租戶污染、誤刪 topic</td>
      </tr>
      <tr>
          <td>生命週期（治理）</td>
          <td>死 topic 累積、partition 數爆炸壓垮 metadata 面</td>
          <td>命名規範 + 活躍判準 + 自動回收</td>
          <td>controller 變慢、rebalance 風暴</td>
      </tr>
  </tbody>
</table>
<p>三軸正交：quota 設好不代表權限對、ACL 鎖好不代表 topic 不會爆炸。下面逐軸展開、每軸都對應 production 踩過的失控場景。本文 quota 與 ACL 操作以 Kafka 4.2.0（KRaft 模式、<code>apache/kafka:latest</code>）實機驗證。</p>
<h2 id="quota把頻寬與-request-容量切給租戶">Quota：把頻寬與 request 容量切給租戶</h2>
<p>Quota 是 broker 端對 client 的流量上限、由 broker 在超限時主動 throttle（延遲回應）而非拒絕、讓單一租戶無法把共享頻寬吃光。Kafka 的 quota 是 broker-side 強制、不依賴 client 自律 —— 即使 client 不配合、broker 也會在回應裡插入 throttle 延遲、把該 client 的有效吞吐壓回配額內。</p>
<h3 id="三類-quota-度量">三類 quota 度量</h3>
<p>Kafka quota 度量三種資源、對應三類飽和：</p>
<table>
  <thead>
      <tr>
          <th>Quota 鍵</th>
          <th>單位</th>
          <th>限制對象</th>
          <th>飽和訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>producer_byte_rate</code></td>
          <td>bytes/sec</td>
          <td>單一 client 每秒寫入 broker 的 bytes</td>
          <td>寫入端 network / disk I/O 飽和</td>
      </tr>
      <tr>
          <td><code>consumer_byte_rate</code></td>
          <td>bytes/sec</td>
          <td>單一 client 每秒從 broker 讀取的 bytes</td>
          <td>讀取端 network 飽和、fan-out 過大</td>
      </tr>
      <tr>
          <td><code>request_percentage</code></td>
          <td>百分比</td>
          <td>單一 client 佔用 broker request handler 的 CPU 時間</td>
          <td>broker CPU 飽和、小訊息高頻請求</td>
      </tr>
  </tbody>
</table>
<p>前兩個 byte rate 防的是頻寬類飽和、適合「大訊息、穩定流量」的租戶。<code>request_percentage</code> 防的是另一種失控 —— 某租戶送大量極小的 request（例如每筆一個 byte、每秒幾萬筆）、byte rate 看起來很低、卻把 broker 的 request handler thread 佔滿。這種「請求數爆炸但流量不大」的攻擊型 pattern 只有 <code>request_percentage</code> 抓得到。一個 broker 預設有 N 個 request handler thread、<code>request_percentage=200</code> 代表允許該 client 用掉 2 條 thread 的時間（100% = 1 條）。</p>
<h3 id="三種套用層級">三種套用層級</h3>
<p>Quota 可以套在三種 entity 上、精度遞增：</p>
<table>
  <thead>
      <tr>
          <th>套用層級</th>
          <th>entity 指定</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>client-id</td>
          <td><code>--entity-type clients --entity-name &lt;id&gt;</code></td>
          <td>沒有認證、用 client.id 區分服務</td>
      </tr>
      <tr>
          <td>user</td>
          <td><code>--entity-type users --entity-name &lt;user&gt;</code></td>
          <td>有 SASL 認證、整個租戶共用一個 quota</td>
      </tr>
      <tr>
          <td>user + client-id</td>
          <td>兩個 entity 同時指定</td>
          <td>同租戶內不同服務分別配額（最細）</td>
      </tr>
  </tbody>
</table>
<p>層級的選擇取決於認證模型。沒開認證的叢集只能用 client-id —— 但 client.id 由 client 自行宣告、可偽造、只適合內部信任環境的粗略區分。開了 SASL 認證後、user 才是可信的租戶邊界、quota 綁 user 才有隔離意義。最細的 user + client-id 組合用在「同一個租戶內、batch 匯入服務跟即時 API 服務要分開限流」這種情境：整個 billing 租戶有一個總配額、但裡面的 <code>batch-importer</code> 再單獨壓低、避免夜間批次把租戶配額吃光、害同租戶的即時服務沒頻寬。</p>
<h3 id="設定與查詢實機驗證">設定與查詢（實機驗證）</h3>
<p>設 client-id 層級、同時給 producer 跟 consumer byte rate：</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">kafka-configs.sh --bootstrap-server localhost:9092 --alter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --add-config <span class="s1">&#39;producer_byte_rate=1048576,consumer_byte_rate=2097152&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entity-type clients --entity-name svc-orders
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Completed updating config for client svc-orders.</span></span></span></code></pre></div><p>設 user 層級、含 <code>request_percentage</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">kafka-configs.sh --bootstrap-server localhost:9092 --alter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --add-config <span class="s1">&#39;producer_byte_rate=5242880,consumer_byte_rate=10485760,request_percentage=200&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entity-type users --entity-name tenant-billing
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Completed updating config for user tenant-billing.</span></span></span></code></pre></div><p>設 user + client-id 組合層級（同租戶內單獨壓低 batch 服務）：</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">kafka-configs.sh --bootstrap-server localhost:9092 --alter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --add-config <span class="s1">&#39;producer_byte_rate=524288&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entity-type users --entity-name tenant-billing <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --entity-type clients --entity-name batch-importer
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># Completed updating config for user tenant-billing.</span></span></span></code></pre></div><p>查詢時 entity 指定要對齊設定時的層級。查 user 層級：</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">kafka-configs.sh --bootstrap-server localhost:9092 --describe <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --entity-type users --entity-name tenant-billing
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Quota configs for user-principal &#39;tenant-billing&#39; are</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#   consumer_byte_rate=1.048576E7, request_percentage=200.0, producer_byte_rate=5242880.0</span></span></span></code></pre></div><p>組合層級要兩個 entity 都帶、否則查不到：</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">kafka-configs.sh --bootstrap-server localhost:9092 --describe <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --entity-type users --entity-name tenant-billing <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entity-type clients --entity-name batch-importer
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Quota configs for user-principal &#39;tenant-billing&#39;, client-id &#39;batch-importer&#39; are</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">#   producer_byte_rate=524288.0</span></span></span></code></pre></div><p>不帶 <code>--entity-name</code> 而只給 <code>--entity-type clients</code> 會列出所有 client-id 層級的 quota、適合稽核整個叢集的 quota 分布。</p>
<h2 id="acl把存取權限綁到-principal">ACL：把存取權限綁到 principal</h2>
<p>ACL 是 broker 對每個操作的授權檢查、把「誰（principal）能對什麼資源（resource）做什麼操作（operation）從哪裡來（host）」綁成一條規則、broker 在每次 produce / fetch / admin 操作前比對。Quota 管的是「用多少」、ACL 管的是「能不能用」—— 兩者正交、quota 不限制權限、ACL 不限制流量。</p>
<h3 id="授權模型四要素">授權模型四要素</h3>
<p>一條 ACL 由四個維度構成、四個維度交集才決定一次操作是否放行：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>含義</th>
          <th>範例值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>principal</td>
          <td>操作的發起身分</td>
          <td><code>User:svc-orders</code></td>
      </tr>
      <tr>
          <td>resource</td>
          <td>被操作的對象（type + name + pattern）</td>
          <td>topic <code>orders.events</code>、group <code>fulfillment-workers</code></td>
      </tr>
      <tr>
          <td>operation</td>
          <td>動作</td>
          <td><code>Write</code> / <code>Read</code> / <code>Describe</code> / <code>All</code></td>
      </tr>
      <tr>
          <td>host</td>
          <td>來源 IP（<code>*</code> 為不限）</td>
          <td><code>10.0.3.21</code></td>
      </tr>
  </tbody>
</table>
<p>resource 的 pattern type 是隔離設計的關鍵：<code>LITERAL</code> 精確匹配單一資源名、<code>PREFIXED</code> 匹配整個前綴。多租戶的 topic 隔離靠 prefixed ACL 加命名規範 —— 給 <code>tenant-billing</code> 一條 <code>billing.</code> 前綴的 <code>All</code> 權限、它就能自由管理所有 <code>billing.</code> 開頭的 topic、卻碰不到 <code>orders.</code> 或別租戶的命名空間。命名規範在這裡不只是整潔、是授權邊界本身。</p>
<p>operation 的選擇要對齊角色。一個 producer 需要 topic 的 <code>Write</code> 跟 <code>Describe</code>（描述 partition metadata）；一個 consumer 需要 topic 的 <code>Read</code> <code>Describe</code> 加上 consumer group 的 <code>Read</code> <code>Describe</code>（commit offset 要對 group 有權）。漏掉 group 的 ACL 是常見錯誤：consumer 能讀到訊息、卻 commit 不了 offset、表現成不斷重複消費。</p>
<h3 id="kraft-的-standardauthorizer">KRaft 的 StandardAuthorizer</h3>
<p>ACL 的儲存與判定由 broker 的 authorizer 負責。KRaft 模式用 <code>org.apache.kafka.metadata.authorizer.StandardAuthorizer</code>、ACL 存在 metadata log（取代 ZooKeeper 時代的 <code>AclAuthorizer</code> 把 ACL 存在 ZK）。預設的 <code>apache/kafka</code> 容器不開 authorizer —— 不開時所有操作放行、ACL 指令也無從生效。啟用需要在 broker 設三項：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-properties" data-lang="properties"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">authorizer.class.name</span><span class="o">=</span><span class="s">org.apache.kafka.metadata.authorizer.StandardAuthorizer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">super.users</span><span class="o">=</span><span class="s">User:admin</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">allow.everyone.if.no.acl.found</span><span class="o">=</span><span class="s">false</span></span></span></code></pre></div><p><code>super.users</code> 列出繞過所有 ACL 檢查的管理身分、用來開機跟救援；少了它、開 authorizer 後第一個操作就會把自己鎖在外面。<code>allow.everyone.if.no.acl.found=false</code> 是隔離的前提 —— 設 <code>true</code> 時「沒有任何 ACL 的資源對所有人開放」、等於 deny-list 模式、漏設一個 topic 就全公司可讀。多租戶必須走 <code>false</code> 的 allow-list 模式：預設拒絕、明確授權才放行。</p>
<blockquote>
<p>本文 ACL 操作以實機驗證：用上述三項 env（<code>KAFKA_AUTHORIZER_CLASS_NAME</code> / <code>KAFKA_SUPER_USERS='User:ANONYMOUS'</code> / <code>KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND=false</code>）配完整 KRaft single-node 設定起容器、PLAINTEXT 連線的 principal 為 <code>User:ANONYMOUS</code>、設為 super user 後即可用 <code>kafka-acls.sh</code> 操作。</p></blockquote>
<h3 id="acl-配置實機驗證">ACL 配置（實機驗證）</h3>
<p>給 producer 對單一 topic 的 write + describe：</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">kafka-acls.sh --bootstrap-server localhost:9092 --add <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --allow-principal User:svc-orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --operation Write --operation Describe <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --topic orders.events</span></span></code></pre></div><p>給 consumer topic 的 read + describe、外加 consumer group 的權限（一條指令同時建兩個 resource 的 ACL）：</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">kafka-acls.sh --bootstrap-server localhost:9092 --add <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --allow-principal User:svc-fulfillment <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --operation Read --operation Describe <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --topic orders.events <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --group fulfillment-workers</span></span></code></pre></div><p>prefixed ACL 把整個命名空間授權給一個租戶：</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">kafka-acls.sh --bootstrap-server localhost:9092 --add <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --allow-principal User:tenant-billing <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --operation All <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --resource-pattern-type prefixed <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --topic billing.
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Adding ACLs for resource</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#   `ResourcePattern(resourceType=TOPIC, name=billing., patternType=PREFIXED)`</span></span></span></code></pre></div><p>host 限制把同一 principal 的權限綁到特定來源 IP：</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">kafka-acls.sh --bootstrap-server localhost:9092 --add <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --allow-principal User:svc-orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --allow-host 10.0.3.21 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --operation Write <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --topic orders.events</span></span></code></pre></div><p>deny 規則的優先序高於 allow —— 同一 principal 即使有 allow、命中 deny 就拒絕。用來在大範圍 allow（如 prefixed <code>All</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">kafka-acls.sh --bootstrap-server localhost:9092 --add <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --deny-principal User:svc-orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --deny-host 10.0.9.99 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --operation Write <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --topic orders.events</span></span></code></pre></div><p>列出特定 topic 的全部 ACL、用於稽核：</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">kafka-acls.sh --bootstrap-server localhost:9092 --list --topic orders.events</span></span></code></pre></div><h2 id="topic-生命週期治理命名ownership-與回收">Topic 生命週期治理：命名、ownership 與回收</h2>
<p>Topic 生命週期治理把「topic 的建立、歸屬、淘汰」變成有規則的流程、避免死 topic 累積與 partition 數爆炸壓垮叢集的 metadata 面。Kafka 的每個 partition 都是 controller 要追蹤的 metadata 單位；topic 只增不減時、partition 總數隨團隊數線性成長、最終 controller 的 metadata 處理、broker 的 leader election、client 的 metadata fetch 都跟著變慢。</p>
<h3 id="命名規範劃出-ownership">命名規範劃出 ownership</h3>
<p>Topic 命名規範把 ownership 跟隔離邊界編碼進名字本身。一個可治理的命名規範通常含三段：租戶 / 領域前綴、語意名、版本。例如 <code>billing.invoices.v1</code> —— <code>billing.</code> 前綴對齊 prefixed ACL 的隔離邊界跟 quota 的租戶歸屬、<code>invoices</code> 是語意、<code>v1</code> 給 schema 演進留出平行存在的空間。命名規範在多租戶不是風格問題、是三個治理軸的共同錨點：ACL 靠前綴授權、quota 靠前綴歸屬、回收靠前綴找 owner。</p>
<p>實機建 topic 時 Kafka 4.2.0 對 <code>.</code> 跟 <code>_</code> 混用會出 metric 名稱碰撞警告：</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">WARNING: Due to limitations in metric names, topics with a period (&#39;.&#39;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">or underscore (&#39;_&#39;) could collide. To avoid issues it is best to use
</span></span><span class="line"><span class="ln">3</span><span class="cl">either, but not both.</span></span></code></pre></div><p>成因是 metric 名把 topic 名裡的 <code>.</code> 跟 <code>_</code> 都正規化掉、<code>billing.invoices</code> 跟 <code>billing_invoices</code> 可能對映到同一條 metric。命名規範應在 <code>.</code> 跟 <code>_</code> 之間選一個當分隔符、全叢集一致、避免監控數據互相污染。</p>
<h3 id="活躍判準與自動回收">活躍判準與自動回收</h3>
<p>死 topic 的回收靠可量化的活躍判準。<a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">LinkedIn 的 TopicGC</a>以自動治理取代手動清理未使用 topic、降低 metadata 壓力並改善 produce / consume 效能。它的判讀是：當 queue 規模擴大、僅靠容量擴充不夠、topic 生命週期與治理自動化會成為可靠性關鍵。</p>
<p>TopicGC 是 LinkedIn 的內部系統、不是 Kafka 內建指令；它揭示的是一套可借鏡的回收流程結構：</p>
<ol>
<li>定義活躍判準：以 last produce / last consume timestamp 判斷 topic 是否仍在使用、設一段觀察窗（例如 N 天無寫入且無讀取）。</li>
<li>分級回收：先標記（soft）、進入待回收狀態並通知 owner、保留一段 grace period、無人認領才真正刪除（hard）。兩段式避免誤刪仍有低頻流量的 topic。</li>
<li>保留稽核：每次標記與刪除留紀錄、回收前後比對 controller log、partition 數量、produce / consume 效能指標、確認治理有效且無誤傷。</li>
</ol>
<p>回收條件的設定要對齊業務節奏。純看 produce timestamp 會誤判「低頻但關鍵」的 topic（如月結批次）；活躍判準要同時看 produce 跟 consume、且觀察窗要長於最長的合法閒置週期。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1單一租戶暴衝吃滿頻寬quota-缺位">Case 1：單一租戶暴衝吃滿頻寬（quota 缺位）</h3>
<p><strong>徵兆</strong>：某團隊上線一支新 backfill job、開始全速寫入；同叢集其他租戶的 producer 端 <code>request-latency</code> p99 從個位數 ms 跳到數百 ms、consumer lag 全面上升；broker network out 打到網卡上限、但 CPU 不高。受害的不是暴衝者自己、是所有共用 broker 的鄰居。</p>
<p><strong>根因</strong>：叢集沒設任何 producer quota、或只對部分租戶設了 quota。沒有 broker-side throttle 時、單一 client 能用滿 broker 的 network / disk I/O、把共享頻寬擠光。byte rate 飽和的特徵是 network 打滿但 CPU 不高 —— 區別於 <code>request_percentage</code> 缺位導致的 CPU 飽和。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>立即對暴衝 client 設 <code>producer_byte_rate</code>、broker 即時 throttle、無需重啟。</li>
<li>建立 quota 預設值：對所有 client-id（或 user）設一個保守的 default byte rate、新租戶上線自動受限、避免「漏設就無限」。</li>
<li>區分 byte rate 與 request_percentage 飽和：network 打滿設 byte rate、CPU 打滿（高頻小訊息）補 <code>request_percentage</code>。</li>
<li>容量規劃：把各租戶 quota 總和對齊 broker 的 network / disk 容量、留 headroom、避免「每個 quota 都合理但加總超過物理上限」。</li>
</ol>
<h3 id="case-2acl-設太鬆或太緊">Case 2：ACL 設太鬆或太緊</h3>
<p><strong>徵兆（太鬆）</strong>：稽核發現某 consumer 服務能讀到不屬於它的租戶 topic；或某 topic 被預期外的 principal 寫入、資料被污染。最壞情況是 <code>allow.everyone.if.no.acl.found=true</code> 下漏設 ACL 的 topic 對全叢集可讀寫。</p>
<p><strong>徵兆（太緊）</strong>：consumer 能讀訊息卻不斷重複消費、log 顯示 commit offset 被拒；或 producer 報 <code>TOPIC_AUTHORIZATION_FAILED</code>、明明該有權限。</p>
<p><strong>根因</strong>：太鬆來自 deny-list 心態 —— <code>allow.everyone.if.no.acl.found=true</code> 把「沒設 ACL」當成「開放」、漏設就外洩。太緊通常是漏掉 operation 或 resource：consumer 只給了 topic 的 <code>Read</code>、漏給 consumer group 的 <code>Read</code> <code>Describe</code>、於是讀得到但 commit 不了、表現成重複消費；producer 漏給 <code>Describe</code>、拿不到 partition metadata。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>走 allow-list：<code>allow.everyone.if.no.acl.found=false</code>、預設拒絕、明確授權才放行。</li>
<li>ACL 對齊角色模板：producer = topic Write + Describe；consumer = topic Read + Describe 加 group Read + Describe；漏 group ACL 是重複消費的常見根因。</li>
<li>用 prefixed ACL 而非逐 topic 設、把授權邊界對齊命名規範前綴、減少漏設。</li>
<li>稽核流程：定期 <code>kafka-acls.sh --list</code> 比對預期授權矩陣、把 ACL 納入版本控制與 review、而非手動逐條加。</li>
</ol>
<h3 id="case-3topic-數量爆炸壓垮-metadata-面">Case 3：Topic 數量爆炸壓垮 metadata 面</h3>
<p><strong>徵兆</strong>：叢集 topic / partition 總數隨團隊增長爬到數萬以上；controller failover 時間從秒級拉長到分鐘級；broker 啟動載入 metadata 變慢；client 的 metadata fetch 變大變慢、rebalance 期間出現連鎖延遲。容量沒滿、但整個叢集的 control plane 變鈍。</p>
<p><strong>根因</strong>：partition 是 controller 要追蹤的 metadata 單位、數量只增不減。每個團隊隨手建 topic、每個 topic 又開高 partition 數、總 partition 數線性甚至超線性成長、壓垮 metadata 處理。KRaft 相比 ZooKeeper 提高了 metadata 上限、但上限仍存在、不是無限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Partition 數規劃納入 topic 建立流程：partition 數對應並行度上限、不是越多越好；多餘 partition 是純 metadata 成本。詳見 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition</a> 卡。</li>
<li>回收死 topic 釋放 partition slot：見 Case 4 與生命週期治理段。</li>
<li>監控 metadata 壓力訊號：controller log、partition 總數、controller failover 時間設告警、在壓垮前介入。</li>
<li>規模化路徑：單叢集 metadata 逼近上限時、評估分群（依關鍵程度分多叢集）、見 overview 的 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Cross-region 與分層叢集</a>段與 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">LinkedIn Tiered Clusters</a>案例。</li>
</ol>
<h3 id="case-4unused-topic-未回收">Case 4：Unused topic 未回收</h3>
<p><strong>徵兆</strong>：叢集裡大量 topic 數月無 produce 也無 consume、卻持續佔 partition slot 跟 metadata；沒人記得某些 topic 屬於哪個團隊、不敢刪；新 topic 想建時撞到 partition 上限、被迫先擴叢集而非先回收。</p>
<p><strong>根因</strong>：沒有活躍判準與回收流程、topic 只建不刪。歸屬資訊沒編碼進命名、回收時找不到 owner、於是「不敢刪」成為預設、死 topic 無限累積。這是 Case 3（metadata 爆炸）的慢性來源。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>建立活躍判準：以 last produce / last consume timestamp 加觀察窗判定死 topic、觀察窗長於最長合法閒置週期（避免誤刪月結類低頻 topic）。</li>
<li>兩段式回收：先 soft 標記並通知 owner、grace period 內無人認領才 hard 刪除、避免誤刪。</li>
<li>命名規範補 ownership：前綴對齊團隊、回收時能直接找到 owner、消除「不敢刪」。</li>
<li>自動化加稽核：參考 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">TopicGC</a>的流程結構、回收前後比對 metadata 與效能指標、留稽核紀錄。</li>
</ol>
<h2 id="容量與規模邊界">容量與規模邊界</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算 / 訊號</th>
          <th>警戒與下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quota 總和 vs 物理容量</td>
          <td>各租戶 byte rate 加總對 broker network / disk 容量</td>
          <td>加總逼近物理上限要重新切分、留 headroom</td>
      </tr>
      <tr>
          <td>ACL 條目數</td>
          <td>逐 topic 設會隨 topic 數線性成長</td>
          <td>改 prefixed ACL 對齊命名規範、降條目數與漏設風險</td>
      </tr>
      <tr>
          <td>Partition 總數</td>
          <td>controller failover 時間、metadata fetch 延遲</td>
          <td>逼近上限先回收死 topic、再評估分群</td>
      </tr>
      <tr>
          <td>Topic 活躍率</td>
          <td>有 produce / consume 的 topic 佔比</td>
          <td>死 topic 比例高代表缺回收流程、補活躍判準</td>
      </tr>
  </tbody>
</table>
<p>Quota 與 ACL 是 broker-side 即時生效、不需重啟、可隨租戶調整、運維成本低。生命週期治理是持續流程、不是一次性操作 —— 死 topic 會持續產生、回收要常態化。三軸的共同前提是命名規範：沒有可治理的命名、quota 找不到歸屬、ACL 邊界對不齊、回收找不到 owner。多租戶治理的第一步是先把命名規範立起來、再談 quota 與 ACL。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="跟-overview-與案例的對位">跟 overview 與案例的對位</h3>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a> —— 本文展開其「Multi-tenant 與配額治理」「Topic 生命週期治理」兩段</li>
<li>平台治理案例：<a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka 事件平台</a> —— 單隊列問題提升到平台治理</li>
<li>生命週期案例：<a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn TopicGC</a> —— 自動回收與 metadata 壓力</li>
<li>規模化分群：<a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a> —— metadata 逼近上限時的多叢集路徑</li>
<li>自管轉 managed 的 ACL cutover：<a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware → MSK</a></li>
</ul>
<h3 id="跟安全模組對位">跟安全模組對位</h3>
<p>ACL 是 Kafka 內建的授權層、處理 broker 級的 principal × resource 授權。完整的 secret 管理（SASL 認證憑證怎麼發、輪替、撤銷）屬於 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護與安全模組</a>的範疇 —— ACL 綁的 principal 從哪來、由認證層決定、ACL 只負責「這個 principal 能做什麼」。多租戶的完整信任鏈是「認證確認身分（07）→ ACL 授權操作（本文）→ quota 限制用量（本文）」三層。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li>Schema 治理：跨租戶共用 topic 時、schema compatibility 是另一層契約治理、見 overview 的 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">KRaft 與 Schema Registry</a>段</li>
<li>Consumer group ACL 細節：跟 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a> rebalance 的互動</li>
<li>Quota 與 <a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">delivery semantics</a>：throttle 延遲對 producer timeout / retry 的影響</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a></li>
<li>對位 deep article（同模組）：本模組其他 Kafka deep article 見 vendor 頁進階主題段</li>
<li>跨模組授權鏈：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護與安全模組</a></li>
<li>方法論：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">Topic</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition</a>、<a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">Consumer group</a></li>
</ul>
]]></content:encoded></item><item><title>Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。持久化跟&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校&lt;/a>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="fork-那一瞬間">fork 那一瞬間&lt;/h2>
&lt;p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 &lt;code>fork()&lt;/code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。&lt;/p>
&lt;p>問題在 &lt;code>fork()&lt;/code> 本身不是免費的。Linux 的 &lt;code>fork()&lt;/code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。&lt;/p>
&lt;p>更糟的是 fork 之後。&lt;code>fork()&lt;/code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。&lt;/p>
&lt;p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。&lt;/p>
&lt;h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意&lt;/h2>
&lt;p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。&lt;/p>
&lt;p>&lt;strong>RDB 是某個時間點的記憶體快照&lt;/strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（&lt;code>dump.rdb&lt;/code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。&lt;/p>
&lt;p>&lt;strong>AOF 是命令的 append-only log&lt;/strong>。每個改變資料的命令（&lt;code>SET&lt;/code>、&lt;code>LPUSH&lt;/code>&amp;hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 &lt;code>fsync&lt;/code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。&lt;/p>
&lt;p>兩者的 fork 觸發點不同但機制相同：RDB 是 &lt;code>BGSAVE&lt;/code>（手動或 save 規則觸發）fork，AOF 是 &lt;code>BGREWRITEAOF&lt;/code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。&lt;/p>
&lt;h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料&lt;/h3>
&lt;p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。&lt;code>appendfsync&lt;/code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;code>appendfsync&lt;/code>&lt;/th>
 &lt;th>fsync 時機&lt;/th>
 &lt;th>崩潰最多丟&lt;/th>
 &lt;th>延遲影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>always&lt;/code>&lt;/td>
 &lt;td>每個寫命令&lt;/td>
 &lt;td>幾乎不丟&lt;/td>
 &lt;td>每次寫都等磁碟、延遲最高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>everysec&lt;/code>&lt;/td>
 &lt;td>每秒一次（背景）&lt;/td>
 &lt;td>最多 1 秒&lt;/td>
 &lt;td>多數場景的平衡點（預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>no&lt;/code>&lt;/td>
 &lt;td>交給 OS（~30 秒）&lt;/td>
 &lt;td>OS 決定、可能丟很多&lt;/td>
 &lt;td>延遲最低、持久性最弱&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>everysec&lt;/code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 &lt;code>always&lt;/code> 一樣明顯。&lt;/p>
&lt;h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail&lt;/h3>
&lt;p>Redis 4.0 後的 &lt;code>aof-use-rdb-preamble yes&lt;/code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。持久化跟<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="fork-那一瞬間">fork 那一瞬間</h2>
<p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 <code>fork()</code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。</p>
<p>問題在 <code>fork()</code> 本身不是免費的。Linux 的 <code>fork()</code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。</p>
<p>更糟的是 fork 之後。<code>fork()</code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。</p>
<p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。</p>
<h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意</h2>
<p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。</p>
<p><strong>RDB 是某個時間點的記憶體快照</strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（<code>dump.rdb</code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。</p>
<p><strong>AOF 是命令的 append-only log</strong>。每個改變資料的命令（<code>SET</code>、<code>LPUSH</code>&hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 <code>fsync</code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。</p>
<p>兩者的 fork 觸發點不同但機制相同：RDB 是 <code>BGSAVE</code>（手動或 save 規則觸發）fork，AOF 是 <code>BGREWRITEAOF</code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。</p>
<h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料</h3>
<p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。<code>appendfsync</code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：</p>
<table>
  <thead>
      <tr>
          <th><code>appendfsync</code></th>
          <th>fsync 時機</th>
          <th>崩潰最多丟</th>
          <th>延遲影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>always</code></td>
          <td>每個寫命令</td>
          <td>幾乎不丟</td>
          <td>每次寫都等磁碟、延遲最高</td>
      </tr>
      <tr>
          <td><code>everysec</code></td>
          <td>每秒一次（背景）</td>
          <td>最多 1 秒</td>
          <td>多數場景的平衡點（預設）</td>
      </tr>
      <tr>
          <td><code>no</code></td>
          <td>交給 OS（~30 秒）</td>
          <td>OS 決定、可能丟很多</td>
          <td>延遲最低、持久性最弱</td>
      </tr>
  </tbody>
</table>
<p><code>everysec</code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 <code>always</code> 一樣明顯。</p>
<h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail</h3>
<p>Redis 4.0 後的 <code>aof-use-rdb-preamble yes</code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。</p>
<h2 id="配置持久化的設定路徑">配置：持久化的設定路徑</h2>





<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"># --- RDB snapshot 規則（多久 + 多少改動觸發 BGSAVE）---</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#   save 900 1      # 900 秒內有 1 個 key 改動</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#   save 300 100    # 300 秒內有 100 個改動</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#   save 60 10000   # 60 秒內有 10000 個改動</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 純 cache 不需要 RDB 可關閉：</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#   save &#34;&#34;</span>
</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="c1"># --- AOF 設定 ---</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli CONFIG SET appendonly yes
</span></span><span class="line"><span class="ln">11</span><span class="cl">redis-cli CONFIG SET appendfsync everysec
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># AOF rewrite 觸發條件：比上次 rewrite 大 100% 且至少 64MB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-percentage <span class="m">100</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-min-size 64mb
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 混合持久化（4.0+ 預設）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">redis-cli CONFIG SET aof-use-rdb-preamble yes</span></span></code></pre></div><p>降低 fork 衝擊的兩個系統層設定：</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. 關閉 Transparent Huge Pages（THP）——THP 會讓 copy-on-write 以 2MB 為單位複製、放大 fork 後的記憶體與延遲</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> never &gt; /sys/kernel/mm/transparent_hugepage/enabled
</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"># 2. 允許 overcommit memory——fork 時 Linux 預設可能因 overcommit 檢查拒絕 fork、導致 BGSAVE 失敗</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># /etc/sysctl.conf:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#   vm.overcommit_memory = 1</span></span></span></code></pre></div><p>這兩個是 Redis 官方明確建議的系統設定，沒設好會直接讓 fork 失敗或放大延遲尖峰。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1bgsave-那一刻-p99-延遲尖峰">Case 1：BGSAVE 那一刻 p99 延遲尖峰</h3>
<p><strong>徵兆</strong>：監控上每隔一段時間（對齊 save 規則）出現規律的延遲尖峰，p99 從 2ms 跳到 300-800ms，持續一兩秒後恢復。<code>INFO stats</code> 的 <code>latest_fork_usec</code> 顯示某次 fork 花了 700000 微秒（0.7 秒）。</p>
<p><strong>根因</strong>：大記憶體實例的 <code>fork()</code> 要複製分頁表，這個動作阻塞主執行緒。實例越大尖峰越明顯，THP 開著會更嚴重。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 THP 關閉（最常見的放大原因）</li>
<li>把 RDB save 規則放寬或關閉——純 cache 場景靠 AOF 或乾脆不持久化</li>
<li>大實例考慮分片，把單實例記憶體降下來，fork 成本隨之降低</li>
<li>在 replica 上做持久化（master 只服務、replica 負責 BGSAVE），把 fork 尖峰移出服務路徑</li>
</ol>
<h3 id="case-2fork-期間記憶體翻倍觸發-oom">Case 2：fork 期間記憶體翻倍觸發 OOM</h3>
<p><strong>徵兆</strong>：BGSAVE 開始後記憶體快速上升，<code>used_memory_rss</code> 在 snapshot 期間衝高，撞到機器 RAM 上限，Linux OOM killer 把 redis-server 進程 SIGKILL，無預警下線。</p>
<p><strong>根因</strong>：copy-on-write 在寫入密集期間複製大量分頁，maxmemory 沒留足夠 headroom。maxmemory 設成 RAM 的 90%+ 時，fork 期間的分頁複製把 RSS 推爆系統。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>maxmemory 設成 RAM 的 60-70%，留 30-40% 給 fork copy-on-write（寫入越密集留越多）</li>
<li>設 <code>vm.overcommit_memory = 1</code> 避免 fork 直接被拒</li>
<li>在低寫入時段（夜間）排程 BGSAVE，減少 fork 期間被複製的分頁</li>
<li>監控 <code>latest_fork_usec</code> 與 BGSAVE 期間的 RSS 峰值，跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a>的 headroom 計算合看</li>
</ol>
<h3 id="case-3aof-everysec-在磁碟飽和時退化成-always">Case 3：AOF everysec 在磁碟飽和時退化成 always</h3>
<p><strong>徵兆</strong>：平常延遲穩定，某段時間（通常伴隨大量寫入或磁碟被其他進程佔用）延遲全面上升，<code>INFO</code> 的 <code>aof_delayed_fsync</code> 計數持續增加。</p>
<p><strong>根因</strong>：<code>everysec</code> 的背景 fsync 應該每秒完成，但磁碟 I/O 飽和時 fsync 跑超過 1 秒。Redis 為了不讓 AOF buffer 無限堆積，會在主執行緒上阻塞等 fsync 完成——<code>everysec</code> 在這個情境下退化成接近 <code>always</code> 的延遲行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用獨立的高 IOPS 磁碟給 AOF（不要跟 OS / log / 其他服務共用 I/O）</li>
<li>監控 <code>aof_delayed_fsync</code>，持續增加代表磁碟跟不上寫入</li>
<li>評估 <code>no-appendfsync-on-rewrite yes</code>——AOF rewrite 期間暫停 fsync，避免 rewrite 的 I/O 跟 fsync 互搶（代價是 rewrite 期間崩潰丟更多）</li>
<li>寫入吞吐超過單磁碟負荷是擴容訊號，不是調 fsync 能解</li>
</ol>
<h3 id="case-4aof-檔尾損壞讓-redis-起不來">Case 4：AOF 檔尾損壞讓 Redis 起不來</h3>
<p><strong>徵兆</strong>：Redis 崩潰後重啟失敗，log 顯示 <code>Bad file format reading the append only file</code>，服務無法載入 AOF。</p>
<p><strong>根因</strong>：崩潰發生在 AOF 寫到一半，最後一條命令只寫了部分 byte，AOF 檔尾不完整。Redis 預設 <code>aof-load-truncated yes</code> 應能容忍尾端截斷，但若損壞在中段（罕見的磁碟錯誤）或設了 <code>aof-load-truncated no</code>，載入直接失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 <code>aof-load-truncated yes</code>（預設），容忍尾端截斷自動修復</li>
<li>中段損壞用 <code>redis-check-aof --fix appendonly.aof</code> 修復（會截掉損壞點之後的內容、有資料遺失）</li>
<li>修復前先備份原 AOF 檔，不要直接覆蓋</li>
<li>混合持久化下還原優先用 RDB preamble，降低純 AOF replay 的損壞風險</li>
</ol>
<h3 id="case-5以為有持久化其實-bgsave-一直在失敗">Case 5：以為有持久化、其實 BGSAVE 一直在失敗</h3>
<p><strong>徵兆</strong>：某次需要從 RDB 還原時發現 <code>dump.rdb</code> 是好幾天前的，期間的資料全沒了。回查 log 發現 BGSAVE 一直報 <code>Can't save in background: fork: Cannot allocate memory</code>。</p>
<p><strong>根因</strong>：<code>vm.overcommit_memory</code> 是預設的 0，Linux 在 fork 時做嚴格的記憶體檢查——當 Redis 已用掉大半 RAM，fork 估算可能需要翻倍記憶體而被拒。BGSAVE 靜默失敗，RDB 停留在最後一次成功的版本，但沒人在看 log。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>vm.overcommit_memory = 1</code>，讓 fork 在記憶體吃緊時仍能成功（靠 copy-on-write 實際不會真的翻倍）</li>
<li>監控 <code>rdb_last_bgsave_status</code> 與 <code>aof_last_bgrewrite_status</code>，<code>err</code> 要立刻告警</li>
<li>監控 <code>rdb_last_save_time</code>，距今太久代表持久化已停擺</li>
<li>持久化的存在不等於可用——定期演練從備份還原，驗證 RDB / AOF 真的能載入</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>持久化的容量判讀，圍繞 fork 成本與磁碟負荷：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>latest_fork_usec</code></td>
          <td>&lt; 100ms（小實例）</td>
          <td>&gt; 數百 ms → 實例太大、考慮分片或 replica 持久化</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足</td>
      </tr>
      <tr>
          <td><code>aof_delayed_fsync</code></td>
          <td>接近 0</td>
          <td>持續增加 → 磁碟 I/O 跟不上、換高 IOPS 磁碟</td>
      </tr>
      <tr>
          <td><code>rdb_last_bgsave_status</code></td>
          <td><code>ok</code></td>
          <td><code>err</code> → fork 失敗、查 overcommit / 記憶體</td>
      </tr>
      <tr>
          <td>AOF 檔大小 / dataset</td>
          <td>rewrite 後接近 dataset 大小</td>
          <td>遠大於 dataset → rewrite 沒觸發、檢查閾值</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>fork 尖峰無法接受、實例又必須大</strong>：把持久化移到 replica（master 純服務），或走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>降低單實例記憶體。</li>
<li><strong>大記憶體下 fork 成本是結構性瓶頸</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 用 fork-less snapshot 機制，大記憶體場景的快照不付 fork 的延遲與記憶體翻倍代價——若 fork 尖峰是主要痛點，這是值得評估的架構替代。</li>
<li><strong>需要真正的 source-of-truth 持久性（不是盡力而為）</strong>：Redis 持久化本質是 cache 的回填保險，不是交易級持久性。要強持久性走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a>（multi-AZ transaction log）或 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>持久化決策的起點其實是一個選型問題：這份資料是 cache 還是 source-of-truth。</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者，兩者必須一起算。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">replication / failover</a></strong>：replica 是承接持久化負擔的地方，也是 fork 尖峰的替代執行點。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB（durable）遷到 ElastiCache，判斷是「feature 可重新計算」——這正是「不需要持久化」的判斷，持久化配置應隨之簡化甚至關閉。反過來，若資料不可重建，問題在選錯儲存層，不在持久化調校。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></strong>：服務若把 Redis 當主要 serving layer，持久化決定了重啟後是冷啟動回源雪崩還是溫啟動，跟 stampede 防護直接相關。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster + GTID-based promotion 的兩段 paradox</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/orchestrator-failover/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/orchestrator-failover/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Orchestrator failover&lt;/em> — 自動 HA 的工具雙層架構跟 5 段 decision tree。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;blockquote>
&lt;p>用詞註：Orchestrator 工具命名與 MySQL 5.7- SQL 命令（&lt;code>SHOW SLAVE STATUS&lt;/code> / &lt;code>CHANGE MASTER TO&lt;/code> / &lt;code>STOP SLAVE&lt;/code> 等）沿用 &lt;em>master / slave&lt;/em>。MySQL 8.0+ 改採 &lt;em>primary / replica&lt;/em>、但 SQL syntax 仍保留別名。本文出現 master / slave 處對應 8.0 primary / replica 概念。&lt;/p>&lt;/blockquote>
&lt;p>讀者第一個會問的問題：「Orchestrator 自己會壞嗎？壞了誰 failover Orchestrator？」這個 paradox 是 &lt;em>任何 HA 工具&lt;/em> 的核心議題、PostgreSQL 的 Patroni 用 DCS（etcd / Consul）解決、MySQL 的 Orchestrator 用 &lt;em>內建 raft cluster&lt;/em> 解決：&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">被管的 (Layer 1): primary MySQL → replica MySQL → replica MySQL → ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">管理者 (Layer 2): orchestrator instance × 3 (or 5) — 用 raft 自己選 leader
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">管理者狀態存放 (Layer 3): 每個 orchestrator instance 自己有 MySQL backend (state)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Orchestrator 3 個 instance 構成 &lt;em>raft cluster&lt;/em>、自己選 leader。Leader 才有 &lt;em>寫入 state&lt;/em> + &lt;em>發起 failover&lt;/em> 權限、其他 instance follower 同步 state。Leader 失聯 → raft 重新選 leader（&amp;lt; 10 秒）、新 leader 繼續 manage MySQL topology。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &amp;#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni&lt;/a> 不同：Patroni 需要 &lt;em>外部 DCS&lt;/em>（etcd / Consul）作為 source of truth、Patroni 本身 stateless；Orchestrator 內建 raft、不需要外部 DCS、但每個 orchestrator instance 需要 &lt;em>自己的 MySQL backend&lt;/em> 存 state。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>Orchestrator failover</em> — 自動 HA 的工具雙層架構跟 5 段 decision tree。</p></blockquote>
<hr>
<blockquote>
<p>用詞註：Orchestrator 工具命名與 MySQL 5.7- SQL 命令（<code>SHOW SLAVE STATUS</code> / <code>CHANGE MASTER TO</code> / <code>STOP SLAVE</code> 等）沿用 <em>master / slave</em>。MySQL 8.0+ 改採 <em>primary / replica</em>、但 SQL syntax 仍保留別名。本文出現 master / slave 處對應 8.0 primary / replica 概念。</p></blockquote>
<p>讀者第一個會問的問題：「Orchestrator 自己會壞嗎？壞了誰 failover Orchestrator？」這個 paradox 是 <em>任何 HA 工具</em> 的核心議題、PostgreSQL 的 Patroni 用 DCS（etcd / Consul）解決、MySQL 的 Orchestrator 用 <em>內建 raft cluster</em> 解決：</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">被管的 (Layer 1):       primary MySQL → replica MySQL → replica MySQL → ...
</span></span><span class="line"><span class="ln">2</span><span class="cl">管理者 (Layer 2):       orchestrator instance × 3 (or 5) — 用 raft 自己選 leader
</span></span><span class="line"><span class="ln">3</span><span class="cl">管理者狀態存放 (Layer 3): 每個 orchestrator instance 自己有 MySQL backend (state)</span></span></code></pre></div><p>Orchestrator 3 個 instance 構成 <em>raft cluster</em>、自己選 leader。Leader 才有 <em>寫入 state</em> + <em>發起 failover</em> 權限、其他 instance follower 同步 state。Leader 失聯 → raft 重新選 leader（&lt; 10 秒）、新 leader 繼續 manage MySQL topology。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni</a> 不同：Patroni 需要 <em>外部 DCS</em>（etcd / Consul）作為 source of truth、Patroni 本身 stateless；Orchestrator 內建 raft、不需要外部 DCS、但每個 orchestrator instance 需要 <em>自己的 MySQL backend</em> 存 state。</p>
<h2 id="orchestrator-雙層架構管-mysql-的-layer-2">Orchestrator 雙層架構：管 MySQL 的 Layer 2</h2>
<p>Layer 1 是 <em>被管的</em> MySQL cluster — primary + replica 群。Layer 2 是 <em>管理者</em> — orchestrator instance 群。Layer 2 監視 Layer 1、Layer 2 自己用 raft 自管。</p>
<p><strong>Layer 1 對 Orchestrator 的需求</strong>：</p>
<ul>
<li>所有 MySQL server 啟用 <code>binlog</code> + <code>log_slave_updates</code>（讓 Orchestrator 看得到 binlog event）</li>
<li>啟用 GTID（Orchestrator failover decision 依賴 GTID 比較進度、不用算 binlog position）</li>
<li>每個 server 有 <em>orchestrator user</em>（<code>GRANT SUPER, REPLICATION CLIENT, REPLICATION SLAVE, PROCESS ON *.* TO 'orchestrator'@'%'</code>）</li>
</ul>
<p><strong>Layer 2 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># /etc/orchestrator.conf.json (簡化)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorHost&#34;: &#34;orchestrator-backend.example.com&#34;,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorPort&#34;: 3306,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorDatabase&#34;: &#34;orchestrator&#34;,</span>
</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="c1"># 用 backend MySQL（每個 orchestrator instance 自己一個）+ raft 同步</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="na">&#34;RaftEnabled&#34;: true,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="na">&#34;RaftDataDir&#34;: &#34;/var/lib/orchestrator&#34;,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="na">&#34;RaftBind&#34;: &#34;10.0.1.10:10008&#34;,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="na">&#34;RaftNodes&#34;: [</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="na">&#34;orchestrator1.example.com:10008&#34;,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="na">&#34;orchestrator2.example.com:10008&#34;,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="na">&#34;orchestrator3.example.com:10008&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="na">],</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 class="c1"># Topology discovery</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="na">&#34;DiscoverByShowSlaveHosts&#34;: true,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="na">&#34;InstancePollSeconds&#34;: 5,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="c1"># Failover detection</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="na">&#34;FailureDetectionPeriodBlockMinutes&#34;: 60,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="na">&#34;RecoveryPeriodBlockSeconds&#34;: 3600,</span>
</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 class="c1"># Failover automation</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="na">&#34;RecoverMasterClusterFilters&#34;: [&#34;*&#34;],</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="na">&#34;RecoverIntermediateMasterClusterFilters&#34;: [&#34;*&#34;],</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="na">&#34;PreFailoverProcesses&#34;: [&#34;/usr/local/bin/orchestrator-fence-master.sh&#34;],</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="na">&#34;PostFailoverProcesses&#34;: [&#34;/usr/local/bin/orchestrator-notify-proxysql.sh&#34;]</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="na">}</span></span></span></code></pre></div><h2 id="stage-1topology-discovery--自動發現--manual-seed">Stage 1：Topology Discovery — 自動發現 + manual seed</h2>
<p>Orchestrator 啟動後 <em>seed</em> 一個或多個 MySQL server、自動發現整個 topology：</p>
<ul>
<li>連 seed server → <code>SHOW SLAVE HOSTS</code> → 發現所有 replica</li>
<li>對每個 replica 跑 <code>SHOW MASTER STATUS</code> + <code>SHOW SLAVE STATUS</code> → 建立 <em>父子關係 graph</em></li>
<li>持續 poll（<code>InstancePollSeconds=5</code>）每 5 秒更新 topology state</li>
</ul>
<p><strong>Topology graph 的 node</strong>：</p>
<ul>
<li><em>Master</em>：no slave status、被多個 replica 指</li>
<li><em>Intermediate master</em>：有 slave status 也有下游 replica（chained replication）</li>
<li><em>Co-master</em>：互相 replicate（罕見、active-passive failover 場景）</li>
<li><em>Replica</em>：有 slave status、無下游</li>
</ul>
<p>Topology 可視化：Orchestrator UI（web）顯示 cluster 樹狀圖、操作員可手動 drag-and-drop replica 重新 attach。</p>
<h2 id="stage-2failure-detection--區分真壞跟假壞">Stage 2：Failure Detection — 區分真壞跟假壞</h2>
<p>Orchestrator 不是 <em>單一 ping 失敗就 failover</em>、有 <em>holistic detection</em>：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>解讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Master <code>connect fail</code></td>
          <td>可能 network blip、不一定真壞</td>
      </tr>
      <tr>
          <td>Master <code>timeout poll</code></td>
          <td>可能 master loaded、不一定真壞</td>
      </tr>
      <tr>
          <td><strong>Replica 全部 <code>IO error</code></strong></td>
          <td>Master 真的對 replica 不可達、強訊號</td>
      </tr>
      <tr>
          <td>Replica 看到 master 還活著</td>
          <td>Master 對 orchestrator 不可達、可能是 <em>orchestrator network</em> 問題、不是 master</td>
      </tr>
      <tr>
          <td>Replica lag 暴增</td>
          <td>Master 可能還活著但 overload、不一定要 failover</td>
      </tr>
  </tbody>
</table>
<p><strong>Detection rule</strong>：Master <em>自己連不上</em> + <em>至少一個 replica 也看 master IO error</em> → 判定 <code>DeadMaster</code>。單一 orchestrator 連不上 master 不觸發 — 防 orchestrator network 隔離造成的 false positive failover。</p>
<h2 id="stage-3failover-decision-tree--選哪個-replica-promote">Stage 3：Failover Decision Tree — 選哪個 replica promote</h2>
<p>判定 <code>DeadMaster</code> 後不是 <em>選最近的 replica</em>、用 decision tree：</p>
<ol>
<li><strong>GTID 最新的 replica</strong>：跟舊 master 同步最完整（用 <code>Executed_Gtid_Set</code> 對比）</li>
<li><strong>同 DC / AZ 的 replica</strong>（如果有 multi-DC 配置）</li>
<li><strong>手動指定的 promotion candidate</strong>（<code>promote_rule=must</code> 或 <code>prefer</code>）</li>
<li><strong>Semi-sync ack 的 replica</strong>（如果 semi-sync 啟用）</li>
</ol>
<p>GTID 最新是基本要求。其他規則是 <em>tie-breaker</em>。</p>
<p><strong>Errant transaction 處理</strong>：選出的 candidate replica 如果有 <em>errant GTID</em>（master 沒有但 replica 有的 transaction）、Orchestrator <em>不會 promote 這個 replica</em>（怕 errant transaction 變成 new master state）。改選次優 candidate。</p>
<h2 id="stage-4promote-action--5-步-atomic理想情況">Stage 4：Promote Action — 5 步 atomic（理想情況）</h2>
<p>選好 candidate 後執行：</p>
<ol>
<li><strong>Fence 舊 master</strong>（pre-failover hook）：把舊 master 對外停掉、防 split-brain</li>
<li><strong>STOP SLAVE on candidate</strong>：candidate 不再從舊 master pull binlog</li>
<li><strong>RESET SLAVE ALL on candidate</strong>：candidate 清掉 slave 配置、變成獨立 master</li>
<li><strong>Re-attach 其他 replica</strong>：用 <code>CHANGE MASTER TO MASTER_HOST=&lt;candidate&gt;, MASTER_AUTO_POSITION=1</code>（GTID auto-position）</li>
<li><strong>Post-failover hook</strong>：通知 ProxySQL / HAProxy / DNS 切流量</li>
</ol>
<p>每步任一失敗、Orchestrator 可能停在中間狀態、需要 <em>人工介入</em>。</p>
<h2 id="stage-5recovery--old-master-怎麼處理">Stage 5：Recovery — Old master 怎麼處理</h2>
<p>Failover 完、舊 master 可能：</p>
<ul>
<li><em>真的死了</em>：物理 server 故障 / region outage → 不必處理、未來修好作為新 replica re-attach</li>
<li><em>Network blip 後復活</em>：舊 master 自己 <em>仍認為自己是 master</em>、再次接受寫入會造成 split-brain</li>
</ul>
<p>修法：</p>
<ul>
<li><em>Fencing</em>（必須）：pre-failover hook 把舊 master 對外 firewall 掉、或 force <code>read_only=1</code>、防舊 master 復活後接受寫入</li>
<li><em>Manual reset</em>：舊 master 復活後人工 confirm 是否變成新 master 的 replica（不要自動、自動容易誤判）</li>
</ul>
<p>Orchestrator UI 在偵測到 errant master 時會標 warning、不會自動處理。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-split-brain--pre-failover-hook-沒-fence-舊-master">1. Split-brain — pre-failover hook 沒 fence 舊 master</h3>
<p>舊 master network blip 後復活、orchestrator 已 promote 新 master、application 部分 instance 連舊 master、部分連新 master、雙寫造成 data divergence。</p>
<p>修法：</p>
<ul>
<li><em>Pre-failover hook 必須 fence</em>（不是可選）：
<ul>
<li>物理 fencing：透過 IPMI 重啟 / 關 server</li>
<li>Network fencing：透過 firewall rule 切斷 server 對外連線</li>
<li>MySQL fencing：<code>SET GLOBAL read_only=1</code> + <code>KILL</code> 所有 active connection</li>
</ul>
</li>
<li>用 <em>VIP / DNS</em> 配合：fence 完才切 VIP / DNS 到新 master、避免 application 連舊 IP</li>
<li>不依賴 application 連線 string 動態變更（DNS TTL 期間仍可能連舊 IP）</li>
</ul>
<h3 id="2-pre-failover-hook-失敗--orchestrator-該停還是該繼續">2. Pre-failover hook 失敗 — Orchestrator 該停還是該繼續</h3>
<p>Pre-failover hook 跑失敗（fence script 因為 SSH 不通、IPMI 沒回應）。Orchestrator 有兩種策略：</p>
<ul>
<li><em>PostponeReplicaRecoveryOnLagMinutes</em>：等 hook 成功才繼續、可能永遠 stuck</li>
<li><em>FailMasterPromotionOnLagMinutes</em>：放棄 promotion、留 cluster degraded（無 master）</li>
</ul>
<p>兩者都不理想。多數 production 選 <em>PostponeReplicaRecoveryOnLagMinutes=10</em>：等 10 分鐘 hook 成功、超時則 alert 人工介入、不繼續 auto-promote（人工 review 才是正確選擇）。</p>
<h3 id="3-anti-flapping-窗口太短--master-抖動-vs-真死">3. Anti-flapping 窗口太短 — Master 抖動 vs 真死</h3>
<p><code>FailureDetectionPeriodBlockMinutes=60</code>：偵測一次 failure 後 60 分鐘內不再 trigger failover（即使再偵測到 failure）。預設 60 分鐘對 <em>第一次 failover 後 master 仍不穩</em> 的場景太長 — 60 分鐘內 master 真的死了第二次、orchestrator 不 failover。預設 60 分鐘對 <em>網路抖動</em> 的場景太短 — 60 分鐘內可能 multiple failover、cluster 一直在 promote。</p>
<p>修法：</p>
<ul>
<li>評估自己 cluster 的 <em>typical recovery time</em>：1-2 小時、設 <code>FailureDetectionPeriodBlockMinutes=120</code></li>
<li>監控 <em>failover 頻率</em>、單週 &gt; 2 次表示底層問題（網路 / hardware）、不是調 anti-flapping window 解決</li>
</ul>
<h3 id="4-gtid-errant-transaction--orchestrator-拒絕-promote-但沒講原因">4. GTID errant transaction — Orchestrator 拒絕 promote 但沒講原因</h3>
<p>Candidate replica 有 <em>errant GTID</em>（從別處 inject 的 transaction）、Orchestrator 拒絕 promote、log 訊息 <code>errant GTID detected</code>、但 <em>沒寫實際是哪個 GTID</em>。On-call 在事故中沒辦法 debug。</p>
<p>修法：</p>
<ul>
<li>平時 <em>監控 errant GTID</em>：定期跑 <code>pt-show-grants</code> + GTID 比對、不要等 failover 才發現</li>
<li>Orchestrator 的 <code>OrchestratorIssuesAGtidPurge</code> 設 true：preview mode 看 errant GTID 的位置</li>
<li>Errant GTID 來源通常是 <em>人為 inject</em>（DBA 直接寫 replica 然後 binlog 出現）、教育 DBA 不要直接連 replica 寫</li>
</ul>
<h3 id="5-vip--proxysql-整合斷層--切流量延遲">5. VIP / ProxySQL 整合斷層 — 切流量延遲</h3>
<p>Post-failover hook 跑完 <em>script 上報</em>「我切完了」、但實際 <em>VIP / DNS / ProxySQL 還沒看到變化</em>。Application 連 stale endpoint 30 秒、寫入失敗。</p>
<p>修法：</p>
<ul>
<li><em>Post-failover hook 不只 trigger 切換、要 wait 切換完成</em>：
<ul>
<li>VIP：等 <code>arping</code> 確認新 IP 已 propagate</li>
<li>ProxySQL：等 <code>mysql_servers</code> runtime table 更新 + 確認 monitor module 看到新 primary</li>
<li>DNS：先把 TTL 降到極短（5 秒）、再切 DNS、等 TTL 過</li>
</ul>
</li>
<li>Orchestrator <code>PostFailoverProcessesFailOnError=true</code>：hook 失敗整個 failover 標記失敗、人工檢查</li>
<li>ProxySQL 用 <code>mysql_replication_hostgroups</code> 自動偵測 read_only flag、可不依賴 hook（推薦）</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>配置建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Orchestrator instance 數量</td>
          <td>3（raft cluster 最小、odd number、容忍 1 個故障）</td>
      </tr>
      <tr>
          <td>每個 instance MySQL backend</td>
          <td>1 個獨立 MySQL（不要共用、不要用被管的 cluster）</td>
      </tr>
      <tr>
          <td>Backend MySQL spec</td>
          <td>t3.small 級別、Orchestrator state ~1 GB</td>
      </tr>
      <tr>
          <td>Network latency</td>
          <td>raft 同 region 內、跨 AZ 可接受（&lt; 5ms）、跨 region 不推薦</td>
      </tr>
      <tr>
          <td>InstancePollSeconds</td>
          <td>5 秒（預設）— 越小越敏感、越大越省連線</td>
      </tr>
  </tbody>
</table>
<p>3 instance raft cluster 容忍 1 instance 故障。5 instance 容忍 2 instance 故障但 quorum cost 高、99% 場景 3 個夠用。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>Orchestrator 100% 依賴 GTID + binlog ROW format（<a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>）。沒 GTID 用 binlog position、failover 時 re-pointing 容易出錯、Orchestrator 強烈建議 GTID。</p>
<h3 id="跟-proxysql">跟 ProxySQL</h3>
<p><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL</a> 用 <code>mysql_replication_hostgroups</code> 自動偵測 <code>read_only</code> flag — orchestrator 切完新 master 後、ProxySQL monitor module 自動看到新 master 的 <code>read_only=0</code>、自動更新 routing、application 不用改 connection string。</p>
<p>這個 <em>無需 post-failover hook 通知 ProxySQL</em> 的整合是 ProxySQL + Orchestrator 組合的最大優勢、比手動 hook 通知 VIP / DNS 可靠。</p>
<h3 id="跟-patronipostgresql-對應">跟 Patroni（PostgreSQL 對應）</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Orchestrator</th>
          <th>Patroni</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DCS</td>
          <td>內建 raft（不需外部）</td>
          <td>外部（etcd / Consul / ZooKeeper）</td>
      </tr>
      <tr>
          <td>State storage</td>
          <td>每 instance 一個 MySQL backend</td>
          <td>DCS 本身</td>
      </tr>
      <tr>
          <td>Topology discovery</td>
          <td>自動 + manual seed</td>
          <td>自動（透過 DCS）</td>
      </tr>
      <tr>
          <td>Fencing</td>
          <td>Pre-failover hook（自實作）</td>
          <td>Watchdog（內建）</td>
      </tr>
      <tr>
          <td>5+ year 生產驗證</td>
          <td>GitHub / Booking.com / Shopify</td>
          <td>Zalando / 多個歐美企業</td>
      </tr>
  </tbody>
</table>
<p>兩者角色相同、設計取捨不同。Patroni 對 DCS 高依賴、Orchestrator 對自己 backend MySQL 高依賴。</p>
<h3 id="跟-rds--aurora-mysql">跟 RDS / Aurora MySQL</h3>
<p>AWS RDS / Aurora 內建 multi-AZ failover、<em>不用 Orchestrator</em>。Aurora failover &lt; 30 秒、RDS failover ~60-120 秒。Aurora 把 replication / failover 整套封進 storage layer、application 看到的是 reader endpoint + writer endpoint。</p>
<p>詳見 <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 page</a>。</p>
<h3 id="跟-vitess">跟 Vitess</h3>
<p>Vitess shard 內部用 <em>VTOrc</em>（Vitess fork of Orchestrator）— 概念跟 Orchestrator 一致、針對 Vitess topology metadata 適配。</p>
<p>詳見 <em>Vitess sharding 設計</em> 篇（待寫）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（GTID 是 Orchestrator pre-requisite）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（Orchestrator + ProxySQL 自動失效切換組合）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni HA</a>（PG sibling、不同 HA 機制）</li>
<li><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 page</a>（managed MySQL、Orchestrator 不需要）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡片</a> / <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover 卡片</a></li>
<li>官方：<a href="https://github.com/openark/orchestrator">orchestrator GitHub</a> / <a href="https://github.com/openark/orchestrator/tree/master/docs">orchestrator docs</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Index Selection：B-tree / GIN / GiST / BRIN / Hash 對應 workload 的決策樹</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/index-selection/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/index-selection/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>index 選型&lt;/em> — 何時用哪種 index、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization&lt;/a> 的「為什麼這個 plan 慢」互補。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="6-種-index-method-對應-workload">6 種 Index Method 對應 Workload&lt;/h2>
&lt;p>PG 有 6 種 index access method、各有自己擅長的 query pattern：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Index method&lt;/th>
 &lt;th>適用 query pattern&lt;/th>
 &lt;th>典型 column type&lt;/th>
 &lt;th>儲存成本&lt;/th>
 &lt;th>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>B-tree&lt;/td>
 &lt;td>&lt;code>=&lt;/code> / &lt;code>&amp;lt;&lt;/code> / &lt;code>&amp;gt;&lt;/code> / &lt;code>BETWEEN&lt;/code> / &lt;code>IS NULL&lt;/code> / &lt;code>LIKE 'prefix%'&lt;/code>&lt;/td>
 &lt;td>任何 scalar、最常用&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hash&lt;/td>
 &lt;td>純 &lt;code>=&lt;/code> 比對&lt;/td>
 &lt;td>scalar、不常用&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GIN&lt;/td>
 &lt;td>&lt;code>@&amp;gt;&lt;/code> / &lt;code>?&lt;/code> / `?&lt;/td>
 &lt;td>` / FTS / array 包含&lt;/td>
 &lt;td>JSONB / tsvector / array&lt;/td>
 &lt;td>高（write 慢）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GiST&lt;/td>
 &lt;td>範圍 / 空間 / 自訂 operator&lt;/td>
 &lt;td>geometry / tsvector / range&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SP-GiST&lt;/td>
 &lt;td>Non-balanced 樹結構&lt;/td>
 &lt;td>IP / phone prefix / quad-tree&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>BRIN&lt;/td>
 &lt;td>大表的 range scan、physical order 跟 logical order 相關&lt;/td>
 &lt;td>timestamp / id（append-only）&lt;/td>
 &lt;td>極低&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選錯 index 的代價：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>index 選型</em> — 何時用哪種 index、跟 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a> 的「為什麼這個 plan 慢」互補。</p></blockquote>
<hr>
<h2 id="6-種-index-method-對應-workload">6 種 Index Method 對應 Workload</h2>
<p>PG 有 6 種 index access method、各有自己擅長的 query pattern：</p>
<table>
  <thead>
      <tr>
          <th>Index method</th>
          <th>適用 query pattern</th>
          <th>典型 column type</th>
          <th>儲存成本</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B-tree</td>
          <td><code>=</code> / <code>&lt;</code> / <code>&gt;</code> / <code>BETWEEN</code> / <code>IS NULL</code> / <code>LIKE 'prefix%'</code></td>
          <td>任何 scalar、最常用</td>
          <td>中</td>
          <td></td>
      </tr>
      <tr>
          <td>Hash</td>
          <td>純 <code>=</code> 比對</td>
          <td>scalar、不常用</td>
          <td>低</td>
          <td></td>
      </tr>
      <tr>
          <td>GIN</td>
          <td><code>@&gt;</code> / <code>?</code> / `?</td>
          <td>` / FTS / array 包含</td>
          <td>JSONB / tsvector / array</td>
          <td>高（write 慢）</td>
      </tr>
      <tr>
          <td>GiST</td>
          <td>範圍 / 空間 / 自訂 operator</td>
          <td>geometry / tsvector / range</td>
          <td>中</td>
          <td></td>
      </tr>
      <tr>
          <td>SP-GiST</td>
          <td>Non-balanced 樹結構</td>
          <td>IP / phone prefix / quad-tree</td>
          <td>中</td>
          <td></td>
      </tr>
      <tr>
          <td>BRIN</td>
          <td>大表的 range scan、physical order 跟 logical order 相關</td>
          <td>timestamp / id（append-only）</td>
          <td>極低</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>選錯 index 的代價：</p>
<ul>
<li><strong>Write workload</strong>：每 write 都更新所有相關 index、5 個 unused index = 5x write 放大</li>
<li><strong>Storage</strong>：JSONB 加 GIN 可能比表本身還大</li>
<li><strong>Plan misjudge</strong>：planner 看到 index 不一定用、<code>EXPLAIN</code> 才確認</li>
</ul>
<h2 id="b-tree預設選擇95-workload-適用">B-tree：預設選擇、95% workload 適用</h2>
<p>B-tree 是 PG 預設 index、CREATE INDEX 不指定 method 就是 B-tree：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_user_id</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_created_at</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">created_at</span><span class="p">);</span></span></span></code></pre></div><p>B-tree 擅長的 query：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 等值
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 範圍
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;2025-01-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;2025-01-31&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- IS NULL
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">shipped_at</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- Prefix LIKE
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">sku</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;ABC%&#39;</span><span class="p">;</span></span></span></code></pre></div><p>B-tree 不擅長：</p>
<ul>
<li><code>LIKE '%suffix'</code>（前綴 wildcard）→ 改 trigram + GIN</li>
<li><code>column @&gt; array</code>（包含）→ 改 GIN</li>
<li>JSON 內部 path query → 改 GIN on JSONB</li>
</ul>
<p><strong>Multi-column B-tree</strong> 的順序很重要：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 假設常 query: WHERE user_id = ? AND status = ?
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_user_status</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 對
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_status_user</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 錯（status 選擇性低）</span></span></span></code></pre></div><p>順序原則：</p>
<ol>
<li><strong>等值 column 在前</strong>（高選擇性）</li>
<li><strong>範圍 column 在後</strong>（B-tree leftmost 規則）</li>
<li><strong>selectivity 高的在前</strong>（filter 更多 row）</li>
</ol>
<h2 id="ginjsonb--fts--array-的標配">GIN：JSONB / FTS / Array 的標配</h2>
<p>GIN（Generalized Inverted Index）對「一個 value 內含多個 sub-element」的 column 高效：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- JSONB
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- Array
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_articles_tags</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">tags</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- Full-text search
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_articles_content</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">content</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- Trigram（fuzzy match）
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_trgm</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_name_trgm</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">name</span><span class="w"> </span><span class="n">gin_trgm_ops</span><span class="p">);</span></span></span></code></pre></div><p>GIN 代價：</p>
<ul>
<li><strong>Write 慢 2-10x</strong>：每個 sub-element 都要更新 inverted index</li>
<li><strong>Storage 大</strong>：可能比表還大</li>
<li><strong>Vacuum 沉重</strong>：bloat 累積快</li>
</ul>
<p><strong>Operator class</strong> 選擇影響大：</p>
<table>
  <thead>
      <tr>
          <th>Op class</th>
          <th>適用</th>
          <th>索引大小</th>
          <th>支援 operator</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>jsonb_ops</code>（預設）</td>
          <td>通用</td>
          <td>大</td>
          <td><code>@&gt;</code> / <code>?</code> / `?</td>
          <td><code>/</code>?&amp;`</td>
      </tr>
      <tr>
          <td><code>jsonb_path_ops</code></td>
          <td>只 <code>@&gt;</code> containment</td>
          <td>1/3-1/2</td>
          <td>只 <code>@&gt;</code></td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>只用 <code>@&gt;</code> query 時、<code>jsonb_path_ops</code> 救大量 storage。</p>
<h2 id="gist範圍--空間--自訂">GiST：範圍 / 空間 / 自訂</h2>
<p>GiST（Generalized Search Tree）擅長範圍跟空間：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 範圍 type（PostgreSQL 內建 int4range / tsrange 等）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_bookings_period</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">bookings</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GiST</span><span class="w"> </span><span class="p">(</span><span class="n">period</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 空間（PostGIS）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_locations_geom</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">locations</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GiST</span><span class="w"> </span><span class="p">(</span><span class="n">geom</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- Exclusion constraint（範圍不重疊）
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bookings</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">CONSTRAINT</span><span class="w"> </span><span class="n">no_overlap</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="n">EXCLUDE</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GiST</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">=</span><span class="p">,</span><span class="w"> </span><span class="n">period</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="p">);</span></span></span></code></pre></div><p>GiST vs GIN 對 FTS 的選擇：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>GIN</th>
          <th>GiST</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lookup 速度</td>
          <td>快 3x</td>
          <td>慢</td>
      </tr>
      <tr>
          <td>Update 速度</td>
          <td>慢 3x</td>
          <td>快</td>
      </tr>
      <tr>
          <td>索引大小</td>
          <td>大</td>
          <td>小</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Read-heavy FTS</td>
          <td>Write-heavy / 即時更新</td>
      </tr>
  </tbody>
</table>
<p>多數 FTS workload 選 GIN — read 占多、index size 換 query latency 划算。</p>
<h2 id="brin大表--physical-order-correlated">BRIN：大表 + Physical Order Correlated</h2>
<p>BRIN（Block Range Index）對 <em>physical 儲存順序跟 logical 順序強相關</em> 的 column 高效：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- timestamp column（append-only insert、physical 順序 = 時間順序）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_events_created_at</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">BRIN</span><span class="w"> </span><span class="p">(</span><span class="n">created_at</span><span class="p">);</span></span></span></code></pre></div><p>BRIN 機制：每個 block range（預設 128 page）記 min/max、query 時跳過 range 外的 block。</p>
<p>適用場景：</p>
<ul>
<li><strong>append-only 表</strong>：log、metrics、events</li>
<li><strong>大表</strong>（10GB+）：B-tree 太貴、BRIN 1/1000 大小</li>
<li><strong>column physical order 跟 query 一致</strong>：時間欄、自增 id</li>
</ul>
<p><strong>BRIN 失效情境</strong>：</p>
<ul>
<li>UPDATE 破壞 physical order（row 被 vacuum 移到別 block）→ BRIN 失效</li>
<li>隨機 insert（uuid / hash id）→ BRIN range 完全沒選擇性</li>
</ul>
<p><strong>何時不該用 BRIN</strong>：表 &lt; 1GB（沒省 storage 收益）、column 沒 physical order correlation（CLUSTER 後可能改善）。</p>
<h2 id="partial-index條件式-index-救-storage">Partial Index：條件式 index 救 storage</h2>
<p>對 <em>只 query 部分 row</em> 的 column、partial index 救大量 storage：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 只 index unshipped order
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_unshipped</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">shipped_at</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 只 index active user
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_active</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- 只 index 高金額 transaction
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_high_value</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span></span></span></code></pre></div><p>Partial index 的 query 要 <em>完全匹配 WHERE 條件</em> 才用得到：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 用得到 partial index
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">shipped_at</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2025-01-01&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 用不到（planner 不 prove WHERE 包含 partial 條件）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2025-01-01&#39;</span><span class="p">;</span></span></span></code></pre></div><p>實務 size 救法：unshipped order 只 1% 總量、partial index 1/100 大小。</p>
<h2 id="expression-index對函式結果-index">Expression Index：對函式結果 index</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 對 lowercased email index（case-insensitive search）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_email_lower</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="k">lower</span><span class="p">(</span><span class="n">email</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">lower</span><span class="p">(</span><span class="n">email</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">lower</span><span class="p">(</span><span class="s1">&#39;USER@example.com&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 對 JSONB 內部欄位
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_category</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="p">((</span><span class="n">metadata</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;category&#39;</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;category&#39;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shoes&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- 對日期截斷
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_day</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">date_trunc</span><span class="p">(</span><span class="s1">&#39;day&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">));</span></span></span></code></pre></div><p>Expression 必須 IMMUTABLE — <code>now()</code> / <code>random()</code> 不能用、<code>timezone('UTC', ts)</code> 可以。</p>
<h2 id="covering-indexinclude避免回表">Covering Index（INCLUDE）：避免回表</h2>
<p>PG 11+ 支援 INCLUDE column：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 只 index user_id、但 query 常要 email
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_user_id_covering</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="n">INCLUDE</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Index-only scan：不用回表
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="p">;</span></span></span></code></pre></div><p>INCLUDE column 不參與 sorting / equality、只放 leaf node、救 IO。</p>
<h2 id="index-選擇決策樹">Index 選擇決策樹</h2>





<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">Query pattern 是什麼？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├─ 等值 / 範圍 / prefix LIKE / IS NULL
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│  └─ B-tree（90% 場景）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│     ├─ 只 query 部分 row？→ Partial B-tree
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│     ├─ 對函式結果？→ Expression B-tree
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│     └─ 需要回表更多 column？→ Covering（INCLUDE）
</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">├─ JSONB 內部 query / array 包含 / FTS
</span></span><span class="line"><span class="ln">10</span><span class="cl">│  └─ GIN
</span></span><span class="line"><span class="ln">11</span><span class="cl">│     ├─ 只用 @&gt;？→ jsonb_path_ops 救 storage
</span></span><span class="line"><span class="ln">12</span><span class="cl">│     └─ FTS write-heavy？→ 改 GiST
</span></span><span class="line"><span class="ln">13</span><span class="cl">│
</span></span><span class="line"><span class="ln">14</span><span class="cl">├─ 範圍 type（int4range / tsrange）/ 空間
</span></span><span class="line"><span class="ln">15</span><span class="cl">│  └─ GiST
</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">├─ 大表 + append-only + physical order correlated
</span></span><span class="line"><span class="ln">18</span><span class="cl">│  └─ BRIN
</span></span><span class="line"><span class="ln">19</span><span class="cl">│
</span></span><span class="line"><span class="ln">20</span><span class="cl">├─ 純 equality + 簡單 column
</span></span><span class="line"><span class="ln">21</span><span class="cl">│  └─ Hash（很少用、B-tree 通常更好）
</span></span><span class="line"><span class="ln">22</span><span class="cl">│
</span></span><span class="line"><span class="ln">23</span><span class="cl">└─ Non-balanced 樹（IP prefix / quad-tree）
</span></span><span class="line"><span class="ln">24</span><span class="cl">   └─ SP-GiST（罕見）</span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1過度-indexwrite-放大">Case 1：過度 index（write 放大）</h3>
<p><strong>情境</strong>：team「為了 query 快」對 20 個 column 各建 index、寫入量大時 INSERT 慢 10x。</p>
<p>每個 INSERT 要更新 20 個 index、WAL volume 也跟著放大、replication lag 拉長。</p>
<p>修法：</p>
<ul>
<li>用 <code>pg_stat_user_indexes</code> 找 <em>idx_scan = 0</em> 的 index、可能根本沒用</li>
<li>用 <code>pg_stat_statements</code> 找實際被執行的 query、反推真正需要的 index</li>
<li>同 column 多 index（user_id 單欄 + (user_id, status) 多欄）通常可拆掉單欄</li>
</ul>
<h3 id="case-2partial-index-條件跟-query-不匹配">Case 2：Partial index 條件跟 query 不匹配</h3>
<p><strong>情境</strong>：建 <code>WHERE status = 'active'</code> partial index、application query 寫 <code>WHERE status IN ('active')</code>、planner 不 prove 等價、不用 index。</p>
<p>修法：</p>
<ul>
<li>Partial 條件用最 generic form（避免 IN / OR 跟 = 的差異）</li>
<li>寫完用 <code>EXPLAIN</code> 驗證 query 真的用到 partial index</li>
<li>Application 統一 query 寫法、不要混 <code>=</code> 跟 <code>IN</code> 跟 <code>ANY</code></li>
</ul>
<h3 id="case-3b-tree-對-jsonb-內部欄位無效">Case 3：B-tree 對 JSONB 內部欄位無效</h3>
<p><strong>情境</strong>：對 <code>metadata</code> JSONB column 建 B-tree、query <code>metadata-&gt;&gt;'category' = 'shoes'</code> 不用 index。</p>
<p>B-tree 對 <em>整個 JSONB</em> 排序、但 path query 不是整個 JSONB 的比對。</p>
<p>修法：</p>
<ul>
<li>對固定 path 建 expression index：<code>CREATE INDEX ... ON products ((metadata-&gt;&gt;'category'))</code></li>
<li>對動態 path 建 GIN index：<code>CREATE INDEX ... USING GIN (metadata)</code></li>
<li>兩者並存可、<code>EXPLAIN</code> 看 planner 選哪個</li>
</ul>
<h3 id="case-4brin-對非-correlated-資料無效">Case 4：BRIN 對非 correlated 資料無效</h3>
<p><strong>情境</strong>：對 <code>user_id</code> 建 BRIN index（user_id 是隨機 UUID）、query 完全跑 seq scan。</p>
<p>UUID 沒 physical order correlation、每個 block range 的 min/max 涵蓋整個 ID space、BRIN 完全沒 prune 效果。</p>
<p>修法：</p>
<ul>
<li>BRIN 只用 <code>timestamp</code> / 自增 <code>id</code> / 其他自然 correlate 的 column</li>
<li>用 <code>pg_stats</code> 看 <code>correlation</code> value、&lt; 0.1 就不適合 BRIN</li>
<li>真要對 random column 加 index、回 B-tree</li>
</ul>
<h3 id="case-5multi-column-index-順序錯">Case 5：Multi-column index 順序錯</h3>
<p><strong>情境</strong>：常見 query <code>WHERE status = 'pending' AND user_id = 42</code>、建 index <code>(status, user_id)</code>、效能差。</p>
<p><code>status</code> 只 5 個 distinct value、選擇性 1/5；<code>user_id</code> 1M distinct、選擇性 1/1M。Index leftmost 是 status、scan range 太大。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 拆兩個或調順序
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_user_status</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 或加 partial 限定低選擇性 column
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_pending</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="p">;</span></span></span></code></pre></div><h2 id="跟-mysql-index-差異">跟 MySQL Index 差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL</th>
          <th>MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Index method</td>
          <td>6 種（B-tree / Hash / GIN / GiST / SP-GiST / BRIN）</td>
          <td>主要 B-tree、空間另算 R-tree</td>
      </tr>
      <tr>
          <td>預設</td>
          <td>B-tree</td>
          <td>B-tree（InnoDB clustered）</td>
      </tr>
      <tr>
          <td>Clustered index</td>
          <td>沒有原生（CLUSTER 一次性）</td>
          <td>InnoDB primary key 永遠 clustered</td>
      </tr>
      <tr>
          <td>Covering</td>
          <td>INCLUDE（PG 11+）</td>
          <td>自然支援（secondary index 帶 PK）</td>
      </tr>
      <tr>
          <td>JSON index</td>
          <td>GIN on JSONB（強）</td>
          <td>functional index on JSON（弱）</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>原生支援</td>
          <td>8.0+ 支援（受限）</td>
      </tr>
      <tr>
          <td>Expression index</td>
          <td>原生支援</td>
          <td>5.7+ functional index</td>
      </tr>
      <tr>
          <td>BRIN-like</td>
          <td>原生</td>
          <td>沒有</td>
      </tr>
      <tr>
          <td>Spatial</td>
          <td>GiST / PostGIS</td>
          <td>R-tree（基本）</td>
      </tr>
  </tbody>
</table>
<p>PG index 系統比 MySQL 表達力高、但代價是 <em>選對 index method 是 application 責任</em>、MySQL 預設 B-tree 多數場景夠用。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a>：EXPLAIN 看 index 用沒用</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">jsonb-deep-dive</a>：JSONB + GIN 細節</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/full-text-search/" data-link-title="PostgreSQL Full-Text Search：tsvector / tsquery / GIN index 跟 pg_trgm fuzzy 三層搜尋" data-link-desc="PG 內建 full-text search 用 *tsvector / tsquery / GIN index* 三件組、適合中小規模搜尋（&lt; 100M 文件）；pg_trgm 提供 fuzzy match。本文走 FTS 機制（tsvector 是 lexeme &#43; position 的 vector）、3 種 query（match / ranking / weighted）、multi-language support、跟 pg_trgm fuzzy match 互補、5 production 踩雷（dictionary 選錯 / GIN 跟 GiST 取捨 / ranking 評分權重 / multi-language column 處理 / 何時不該用 PG FTS 改 Elasticsearch）">full-text-search</a>：FTS + GIN</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum-tuning</a>：index bloat</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">online-schema-change</a>：CREATE INDEX CONCURRENTLY</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a> 驗證 index 有沒有被 plan 用到</li>
<li>回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 看全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C15 Airbnb：Spark Streaming Kafka reader rebalance</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/</guid><description>&lt;p>這個案例的核心責任是說明 stream processor 與 Kafka partition 數的緊耦合是 production scaling 瓶頸。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb logging pipeline 跨多個 topic、event size 從幾百 bytes 到幾百 KB、QPS 跨數個量級差異、Spark 一個 partition 對一個 task 造成 data skew、catch-up 一個 4 小時 lag 要再花 4 小時。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>自建 balanced Spark Kafka reader、把 parallelism 從 partition 數解耦、按 event volume × size 重新分派 work。揭露 partition 數不該等同 consumer parallelism、要看 event 形狀。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：Consumer 設計 / consumer lag / rebalance / partition + consumer group。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/airbnb-engineering/scaling-spark-streaming-for-logging-event-ingestion-4a03141d135d">Scaling Spark Streaming for Logging Event Ingestion&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 stream processor 與 Kafka partition 數的緊耦合是 production scaling 瓶頸。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb logging pipeline 跨多個 topic、event size 從幾百 bytes 到幾百 KB、QPS 跨數個量級差異、Spark 一個 partition 對一個 task 造成 data skew、catch-up 一個 4 小時 lag 要再花 4 小時。</p>
<h2 id="判讀">判讀</h2>
<p>自建 balanced Spark Kafka reader、把 parallelism 從 partition 數解耦、按 event volume × size 重新分派 work。揭露 partition 數不該等同 consumer parallelism、要看 event 形狀。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：Consumer 設計 / consumer lag / rebalance / partition + consumer group。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/airbnb-engineering/scaling-spark-streaming-for-logging-event-ingestion-4a03141d135d">Scaling Spark Streaming for Logging Event Ingestion</a></li>
</ul>
]]></content:encoded></item><item><title>Cloudflare Access</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-access/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-access/</guid><description>&lt;p>Cloudflare Access 是 application-layer Zero Trust Network Access (ZTNA) portal、定位是 &lt;em>取代 VPN&lt;/em> — 使用者不再先撥 VPN 進內網再連 internal app、而是 IdP 認證後 Access policy 直接判斷能不能進該 application、流量走 Cloudflare global edge。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &amp;#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary&lt;/a> 解 &lt;em>不同層的 access&lt;/em> — Cloudflare Access 解 &lt;em>application 層 ZTNA&lt;/em>、Teleport 解 &lt;em>infrastructure 層 PAM + session recording&lt;/em>、Tailscale 解 &lt;em>device-level mesh VPN&lt;/em>、Boundary 解 &lt;em>credential brokering&lt;/em>。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Cloudflare Access 的核心責任是 &lt;em>application-level 認證 + authorization&lt;/em>、不是 network-level routing。一個 Application（hostname / subdomain）對應一組 Access Policy（rule with identity / device / network condition）、user 從 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / Google / Azure AD / GitHub 等 IdP 認證後、policy engine 決定能不能進、不能進連到 application backend 都沒機會。它是 Cloudflare Zero Trust suite 的核心、跟 &lt;em>WARP client&lt;/em>（device agent）、&lt;em>Gateway&lt;/em>（DNS / HTTP filtering、取代 Cisco Umbrella 類）、&lt;em>Argo Tunnel&lt;/em>（origin-side outbound、不開 ingress port）組成完整 SASE / Cloudflare One 平台。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &amp;#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &amp;#43; session recording &amp;#43; JIT、跟 Okta / Vault 互補">Teleport&lt;/a> 比、Cloudflare Access 走 &lt;em>application-layer + Cloudflare edge&lt;/em>、Teleport 走 &lt;em>infrastructure-layer + 完整 session recording&lt;/em>。需要 keystroke / RDP / kubectl 完整錄影做合規（PCI / HIPAA）走 Teleport、需要把所有 internal web app 收進統一 ZTNA portal 走 Cloudflare Access、兩者並存常見：Teleport 管 SSH / DB / Kubernetes、Cloudflare Access 管 internal web。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &amp;#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH&lt;/a> 比、Tailscale 是 &lt;em>mesh VPN + device-to-device WireGuard&lt;/em>、Cloudflare Access 是 &lt;em>application proxy via edge&lt;/em>。Tailscale 適合 developer 直接 SSH 到雲機、Cloudflare Access 適合 internal app（GitLab / Jenkins / 內部 dashboard）統一收口。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a> 的關係：同 Cloudflare 控制面、共用 API token / Audit Log / Logpush、但解不同問題 — WAF 防 &lt;em>public app&lt;/em>（attacker 從外打 production web）、Access 防 &lt;em>internal app&lt;/em>（員工 / 廠商存取後台）、兩者常在同一個 Cloudflare account 共存。&lt;/p></description><content:encoded><![CDATA[<p>Cloudflare Access 是 application-layer Zero Trust Network Access (ZTNA) portal、定位是 <em>取代 VPN</em> — 使用者不再先撥 VPN 進內網再連 internal app、而是 IdP 認證後 Access policy 直接判斷能不能進該 application、流量走 Cloudflare global edge。它跟 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> / <a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a> / <a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a> 解 <em>不同層的 access</em> — Cloudflare Access 解 <em>application 層 ZTNA</em>、Teleport 解 <em>infrastructure 層 PAM + session recording</em>、Tailscale 解 <em>device-level mesh VPN</em>、Boundary 解 <em>credential brokering</em>。</p>
<h2 id="服務定位">服務定位</h2>
<p>Cloudflare Access 的核心責任是 <em>application-level 認證 + authorization</em>、不是 network-level routing。一個 Application（hostname / subdomain）對應一組 Access Policy（rule with identity / device / network condition）、user 從 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Google / Azure AD / GitHub 等 IdP 認證後、policy engine 決定能不能進、不能進連到 application backend 都沒機會。它是 Cloudflare Zero Trust suite 的核心、跟 <em>WARP client</em>（device agent）、<em>Gateway</em>（DNS / HTTP filtering、取代 Cisco Umbrella 類）、<em>Argo Tunnel</em>（origin-side outbound、不開 ingress port）組成完整 SASE / Cloudflare One 平台。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> 比、Cloudflare Access 走 <em>application-layer + Cloudflare edge</em>、Teleport 走 <em>infrastructure-layer + 完整 session recording</em>。需要 keystroke / RDP / kubectl 完整錄影做合規（PCI / HIPAA）走 Teleport、需要把所有 internal web app 收進統一 ZTNA portal 走 Cloudflare Access、兩者並存常見：Teleport 管 SSH / DB / Kubernetes、Cloudflare Access 管 internal web。跟 <a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a> 比、Tailscale 是 <em>mesh VPN + device-to-device WireGuard</em>、Cloudflare Access 是 <em>application proxy via edge</em>。Tailscale 適合 developer 直接 SSH 到雲機、Cloudflare Access 適合 internal app（GitLab / Jenkins / 內部 dashboard）統一收口。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 的關係：同 Cloudflare 控制面、共用 API token / Audit Log / Logpush、但解不同問題 — WAF 防 <em>public app</em>（attacker 從外打 production web）、Access 防 <em>internal app</em>（員工 / 廠商存取後台）、兩者常在同一個 Cloudflare account 共存。</p>
<p>關鍵張力：<em>Cloudflare 控制面信任成本</em> ↔ <em>統一 ZTNA portal 的工程紅利</em> 是 Cloudflare Access 客戶的長期取捨。Cloudflare 自家 control plane 出事（<a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare 2023 control plane token</a>）會直接打到 Access policy 變更權、客戶側必須有非 Cloudflare 路徑的 break-glass。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Cloudflare Access 在 ZTNA / PAM stack 中承擔哪一段（application access）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a> 管 infrastructure session、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 管 IdP source of truth）</li>
<li>Application + Access Policy + Argo Tunnel 三者的 ownership 設計（誰建 Application、誰寫 policy、誰跑 cloudflared agent）</li>
<li>Cloudflare control plane 信任邊界 — 自家事故的 blast radius 跟客戶側 break-glass 預案</li>
<li>何時用 Cloudflare Access、何時走 Teleport / Tailscale / Zscaler 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Cloudflare Access deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能改 Access Policy</strong>：Cloudflare account 的 Super Admin / Access Admin 人數、policy change 是否走 Terraform / API + PR review、是否有 IdP claim 跟 Cloudflare group 雙重 enforcement</li>
<li><strong>Application 收口完整度</strong>：internal web / SSH / RDP / API 是否都進 Application 清單、是否還有 <em>bypass Cloudflare 直連 origin</em> 的暴露 IP、Argo Tunnel 是否強制（origin 防火牆只開 cloudflared outbound、不開 ingress）</li>
<li><strong>Device Posture / Service Auth 治理</strong>：human user 是否有 WARP + Device Posture 檢查（OS 版本 / EDR / disk encryption）、non-human（CI / 機器）是否走 Service Auth（mTLS cert / service token）而非共用 user account</li>
<li><strong>Logpush + break-glass</strong>：Access event / Audit Log 是否 Logpush 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 或外部 SIEM、Cloudflare 自家 control plane 出事時是否有 <em>非 Cloudflare</em> 路徑可進關鍵 application（例如 emergency bastion 走獨立 IdP）</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity and Access Boundary</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Application + Access Policy</strong>：Application 是 first-class concept、對應一個 hostname（<code>gitlab.internal.example.com</code>）或一組 subdomain。Application 綁多個 Access Policy、每個 policy 是 <em>Allow / Block / Bypass</em> rule、條件可組合 identity（IdP group / email / SAML claim）、device（Device Posture 結果 / WARP enrolled）、network（country / IP range / Service Token）。policy 順序決定優先級、第一個 match 的生效。Production 寫法是 <em>deny by default + allow specific group</em>、不是 <em>allow all + block bad</em>。</p>
<p><strong>IdP integration</strong>：Cloudflare Access 不存身份、只接 IdP — SAML / OIDC 對 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD / Google Workspace、OAuth 對 GitHub / GitLab、One-Time PIN 對外部廠商（沒 IdP 的合作方）。同一個 Application 可接多 IdP、policy 用 <code>identity.email ends with @vendor.com</code> 區分。IdP 是 source of truth、Cloudflare 是 enforcement point — IdP 出事（Okta / Azure AD 故障或被打）會直接擋住所有 Access user 登入、break-glass 預案必要。</p>
<p><strong>Argo Tunnel（cloudflared）</strong>：internal app 不開 ingress port、不需要 public IP、由 <code>cloudflared</code> agent 在 origin 主動建 outbound tunnel 到 Cloudflare edge。攻擊面從「IP + port + WAF rule」收成「cloudflared agent + Tunnel token」— attacker 從外掃不到 origin，必須先拿到 Tunnel token 或 compromise cloudflared host。Argo Tunnel 的 token 是高敏 secret、應該存 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 或 cloud secret manager、定期 rotate。</p>
<p><strong>Browser-based SSH / RDP / VNC</strong>：Cloudflare Access 對 SSH / RDP / VNC 提供 browser-based render — user 不裝 SSH client、瀏覽器直接連、session 經 Cloudflare edge proxy。可 log session metadata（user / app / time）但 <em>不像 Teleport 完整錄 keystroke / 螢幕</em>。合規場景（PCI 要求 session recording）需要外接 Teleport 或自己跑 session recording proxy、Cloudflare Access 解決的是 access enforcement 不是 audit replay。</p>
<p><strong>Service Auth（non-human access）</strong>：CI runner / 機器人 / API client 走 Service Auth、不需要 user identity。兩種模式：<em>mTLS</em>（client cert + Cloudflare 驗 CA）、<em>Service Token</em>（HTTP header 帶 <code>CF-Access-Client-Id</code> + <code>CF-Access-Client-Secret</code>）。token 進 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 或 GitHub Actions secret、定期 rotate、access log 標 service token ID 做事後追蹤。</p>
<p><strong>Device Posture</strong>：跟 WARP client / Gateway 整合、policy 可加 device 條件 — OS 版本最低、EDR（CrowdStrike / SentinelOne）running、disk encryption enabled、device certificate 已 enrolled。Device Posture check fail 時 deny access 或 fallback 到只讀 application。對應 <a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">zero-trust workforce architecture</a> 的章節原則。</p>
<p><strong>Gateway DNS / HTTP filtering</strong>：Cloudflare Gateway 是 secure web gateway（SWG）、取代 Cisco Umbrella / Zscaler ZIA 類。WARP client 把 device DNS / HTTP traffic 導到 Gateway、policy 過濾 malicious domain / category / DLP。跟 Access 共用 Cloudflare 帳號、policy 跨 Access + Gateway + WARP 統一 — 這是 Cloudflare One / SASE 的核心賣點。</p>
<p><strong>Logpush 進 SIEM</strong>：Access event（login / policy decision / session）+ Audit Log（policy change / admin action）透過 Logpush 推到 S3 / GCS / Splunk HEC / Datadog / Elastic。跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 共用同一個 Logpush job 配置、SIEM 端做 cross-product correlation（WAF block + Access deny 同 IP）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Cloudflare Access</th>
          <th>Teleport</th>
          <th>Tailscale SSH</th>
          <th>Zscaler ZIA / ZPA</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>控制層級</td>
          <td>Application layer（hostname / subdomain）</td>
          <td>Infrastructure layer（SSH / DB / k8s / RDP）</td>
          <td>Network layer（device mesh WireGuard）</td>
          <td>Network + application（SASE 整套）</td>
      </tr>
      <tr>
          <td>流量路徑</td>
          <td>Cloudflare global edge proxy</td>
          <td>Teleport proxy（self-hosted / Cloud）</td>
          <td>Device-to-device WireGuard（peer-to-peer）</td>
          <td>Zscaler global cloud</td>
      </tr>
      <tr>
          <td>Session 錄影</td>
          <td>不錄 keystroke、只記 metadata</td>
          <td>完整 keystroke / 螢幕 / kubectl 錄影</td>
          <td>不錄（mesh 性質）</td>
          <td>HTTP / web session 可錄、SSH 弱</td>
      </tr>
      <tr>
          <td>取代 VPN</td>
          <td>強 — application-layer ZTNA 核心訴求</td>
          <td>部分 — 偏 PAM、需配 VPN 補 catch-all</td>
          <td>強 — mesh VPN 直接替代</td>
          <td>強 — ZPA 核心訴求</td>
      </tr>
      <tr>
          <td>Origin 暴露</td>
          <td>Argo Tunnel：origin 零 ingress</td>
          <td>Proxy 收口：origin 只開 Teleport node port</td>
          <td>不需 ingress：mesh peer 直連</td>
          <td>App connector：類似 Argo Tunnel</td>
      </tr>
      <tr>
          <td>IdP integration</td>
          <td>SAML / OIDC / OAuth、One-Time PIN for vendor</td>
          <td>SAML / OIDC</td>
          <td>SSO via IdP（簡單）</td>
          <td>SAML / OIDC</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Free（50 user）/ Standard / Premium per-user</td>
          <td>Per-user（Teleport Cloud / Enterprise）</td>
          <td>Per-user（含 free tier）</td>
          <td>Per-user enterprise license</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Internal web app + browser SSH/RDP 統一 portal</td>
          <td>Infrastructure access + 合規 session recording</td>
          <td>Developer SSH mesh + 小型 team</td>
          <td>大型企業全 SASE（含 SWG / CASB / DLP）</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — policy 跟 Tunnel 改設可遷</td>
          <td>中 — session log 鎖在 Teleport</td>
          <td>低 — WireGuard 標準</td>
          <td>高 — 全 stack 鎖在 Zscaler</td>
      </tr>
  </tbody>
</table>
<p>選 Cloudflare Access 的核心訴求：<em>application-layer ZTNA 取代 VPN</em> + <em>internal web app 為主 + 偶爾 browser SSH/RDP</em> + <em>已用 Cloudflare WAF / CDN 控制面</em>。需要 infrastructure-level session recording 走 Teleport、developer SSH mesh 為主走 Tailscale、要全 SASE / SWG / CASB 套裝走 Zscaler。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Service Auth 的 non-human access 設計</strong>：CI / 機器人 / 第三方 API client 不該共用 user account、改走 Service Token 或 mTLS。設計重點：<em>token 不進 git</em>（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / GitHub Actions secret）、<em>per-service token</em>（不共用、追蹤責任）、<em>rotation lifecycle</em>（90 天 / 半年 rotate）、<em>access log 標 token ID</em>（事後追責）。token leak 的處理是 <em>rotate + audit log review</em>、不是 <em>all-users password reset</em>。</p>
<p><strong>Device Posture + EDR 整合</strong>：Gateway / WARP 可接 CrowdStrike Falcon / SentinelOne / Microsoft Defender for Endpoint 的 device health、policy 可寫 <code>require posture: crowdstrike.running == true AND crowdstrike.last_check &lt; 1h</code>。意義是 endpoint compromise 時 EDR 標紅、Access policy 自動 deny — 不需要 SOC 手動把 user disable。前提是 EDR fleet coverage 接近 100%、不然 fallback 設不好會誤殺。</p>
<p><strong>Cloudflare One（Access + WARP + Gateway + Magic Transit）</strong>：Cloudflare 把 ZTNA + SWG + CASB + 網路骨幹整成 SASE 套裝、競爭對手是 Zscaler / Netskope / Palo Alto Prisma Access。買整套的紅利是 policy / log / IdP 統一、痛點是 <em>Cloudflare 控制面信任成本指數放大</em> — 一個 admin 角色失控影響 Access + Gateway + WAF + DNS 全部、不只是單一產品。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> 共用 control plane 的取捨</strong>：紅利是同一 Logpush job / 同一 API token 管理 / 同一 Audit Log、SIEM 端 correlation 容易。信任成本是 <a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare 2023 control plane token</a> 那類事故會同時影響 WAF rule + Access policy + DNS、客戶側必須有 <em>non-Cloudflare break-glass</em>（例如保留一條 emergency bastion 走獨立 IdP + 獨立網路、不經過 Cloudflare edge）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>User 一進 Application 就被 deny</strong>：policy 順序錯（Block rule 在 Allow 前面 match）、或 IdP group claim 沒帶到 — 看 Access log 的 <code>decision_reason</code>、確認 policy 順序跟 IdP claim mapping</li>
<li><strong>Argo Tunnel 斷線 / 找不到 origin</strong>：cloudflared 程序掛或 token 過期、origin 防火牆把 outbound 443 擋了 — 重啟 cloudflared、確認 outbound 規則、token rotate 後重新 deploy</li>
<li><strong>Service Token 大量 leak / 在 GitHub repo 出現</strong>：CI secret 設定錯放成 plaintext、或第三方 vendor commit 了 — Cloudflare dashboard rotate token、audit log 找受影響時間窗、補 secret scanning（<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>）</li>
<li><strong>Device Posture 把合法 user 鎖在外</strong>：EDR agent 暫時離線或 OS 升級導致 posture check fail — fallback 設 graceful（降級到只讀 / 加 step-up MFA）、不是直接 deny；EDR fleet coverage 沒到位前不要 hard enforcement</li>
<li><strong>IdP 出事 Access user 全進不來</strong>：Okta / Azure AD downtime 把 Access login 全鎖死 — break-glass 走 <em>Service Token + 緊急 Application</em>（不接 IdP、只接 mTLS / token），預先 staging tested</li>
<li><strong>Bypass 流量直連 origin</strong>：Application 收口不完整、origin 還有 public IP + 沒設 firewall 只接 Cloudflare IP — Argo Tunnel 收完、origin firewall 只允許 Cloudflare IP range 或完全只開 cloudflared outbound</li>
<li><strong>Cloudflare control plane 出事</strong>：Cloudflare 自家 admin token / control plane 被打、客戶側 Access policy 暫時改不了或被偷改 — 預案：保留 <em>非 Cloudflare emergency bastion</em> + 關鍵 application 的 Logpush 進外部 SIEM（不只 Cloudflare dashboard）</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure access + 合規錄影</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a></td>
      </tr>
      <tr>
          <td>Developer SSH mesh / 小型 team</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a></td>
      </tr>
      <tr>
          <td>Credential brokering / 動態 DB cred</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a> + <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a></td>
      </tr>
      <tr>
          <td>全 SASE 套裝（SWG + CASB + DLP）</td>
          <td>Zscaler / Netskope / Palo Alto Prisma Access</td>
      </tr>
      <tr>
          <td>Public app 入口防護</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></td>
      </tr>
      <tr>
          <td>IdP 本體（身份 source of truth）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>SIEM 接 Access log</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Cloudflare WARP client 完整部署 / device enrollment 細節</li>
<li>Gateway DNS / HTTP filtering 完整 policy 語法</li>
<li>Cloudflare One / Magic Transit / Magic WAN 的網路骨幹細節（屬 SD-WAN / SASE 整套架構、不在 ZTNA 範圍）</li>
<li>cloudflared agent 進階配置（multi-region / HA / load balancing）</li>
<li>Cloudflare account / API token 管理本身（屬 Cloudflare 平台治理、跨產品共用）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Cloudflare Access 在 07 案例庫的關聯來自 <em>Cloudflare 控制面信任</em> 跟 <em>IdP 上游事故傳導</em>：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Cloudflare Access 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare Control Plane Token 2023</a></td>
          <td>Cloudflare 自家 control plane 出事時、Access policy 變更權跟著受影響、客戶側必須有非 Cloudflare 路徑的 break-glass、關鍵 application 的 Logpush 進外部 SIEM</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 Identity Lateral Impact</a></td>
          <td>Cloudflare Access 在 helpdesk SE 拿到 IdP credential 後、Device Posture + Application policy + Service Token 是額外 hop 成本、不是 IdP 一拿就全通</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta-Cloudflare 2023 Support Supply Chain</a></td>
          <td>上游 IdP（Okta）出事傳導到 Cloudflare Access enrollment、需要 force re-auth + service token rotate + Logpush audit 找受影響時間窗、IdP 跟 Access 不可同時失能</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">zero-trust workforce architecture</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a>、<a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a>、<a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（共用 control plane）、<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（Logpush 目的地）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a>（IdP source）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Tunnel token / Service Token 儲存）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Access deny / 異常登入 routing）、<a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">5 deployment vendors</a>（Argo Tunnel + CI 部署整合）</li>
<li>官方：<a href="https://developers.cloudflare.com/cloudflare-one/">Cloudflare Zero Trust Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/</guid><description>&lt;p>這個案例的核心責任是說明「售票搶購型 flash-sale」的負載形狀 — 跟現有所有案例都不同的極端形狀。售票開賣在精確時間點（例如 12:00:00）瞬間湧入數十萬使用者、5 分鐘內賣完、之後流量歸零。這種「t=0 起跳、t=300 結束」的負載沒有「峰值預測」可言、只有「瞬間吸收」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>拓元 Tixcraft 在 AWS 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/tixcraft/">tixCraft Case Study&lt;/a> 與 &lt;a href="https://www.slideshare.net/slideshow/case-sharing-tixcraft-on-aws-reinvent-2015-recap/55681198">AWS re:Invent 2015 簡報&lt;/a>）：&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>同時選位用戶&lt;/td>
 &lt;td>100,000+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訂單峰值&lt;/td>
 &lt;td>每分鐘 70,000+ 訂單、單秒最高 2,500+ 訂單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3 分鐘內售出&lt;/td>
 &lt;td>30,000+ 張票&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DynamoDB IOPS 範圍&lt;/td>
 &lt;td>20 → 135,000（2015/8/29 峰值）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資源擴張幅度&lt;/td>
 &lt;td>30 分鐘內從 6 台擴到 800 台（130x）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署時間&lt;/td>
 &lt;td>1,600 工時 → 20 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>壓測規模&lt;/td>
 &lt;td>10,000 台 t2.micro、$130 / 小時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任務總成本&lt;/td>
 &lt;td>&amp;lt; 2 台 MacBook Pro（約 $4,200）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>vs 傳統基礎設施成本&lt;/td>
 &lt;td>0.26%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成立年份&lt;/td>
 &lt;td>2013 年底（雲原生）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合（依用戶提供的架構圖）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>入口&lt;/strong>：Amazon Route 53（DNS）+ CloudFront + S3（靜態資源 static.tixcraft.com）&lt;/li>
&lt;li>&lt;strong>UI 層&lt;/strong>：Elastic Load Balancing → EC2 跨 3 個 Availability Zone（Tixcraft UI）&lt;/li>
&lt;li>&lt;strong>API 層&lt;/strong>：ELB → EC2 跨 3 個 AZ（API）+ ElastiCache 加速 session&lt;/li>
&lt;li>&lt;strong>資料層&lt;/strong>：DynamoDB 作為主要寫入目標（接 UI 寫入跟 API 寫入）&lt;/li>
&lt;li>&lt;strong>付款層&lt;/strong>：獨立的 EC2 Payment、連到 traditional server（合作金流、跑於企業 data center）&lt;/li>
&lt;li>&lt;strong>同步層&lt;/strong>：S3 Sync + EC2 Bridge 跟 corporate data center 的 backend 雙向同步&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>拓元案例最值得讀的、是它揭露三個 flash-sale 工程設計的非直覺事實。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>DynamoDB 作為寫入緩衝、不是 OLTP&lt;/strong>：搶票時的「訂單」先丟進 DynamoDB、傳統 server 用自己能承受的速度消費、即時生效在此架構下不是目標。架構上 DynamoDB 扮演 &lt;em>durable queue&lt;/em> 的角色、不是傳統 OLTP DB。這層解耦讓「前端可以擴 130 倍、後端不用同步擴」、避免後端被前端拖垮。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> 的 outbox / async delivery 概念、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 transaction boundary 分離。&lt;/li>
&lt;li>&lt;strong>DynamoDB IOPS 從 20 衝到 135,000 = partition 設計能撐&lt;/strong>：這個 6,750 倍的彈性不是 DynamoDB 魔法、是 &lt;em>partition key 設計均勻&lt;/em> 的結果。partition key 不均、IOPS 上限是「最熱 partition 上限」、不是「總和」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 的同一判讀重點、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 hot partition 識別。&lt;/li>
&lt;li>&lt;strong>30 分鐘擴 130 倍 = 雲原生架構的存在證明&lt;/strong>：6 台 → 800 台不是手動操作、是 Auto Scaling Group + AMI prebuild + load balancer warmup 的組合。傳統 IDC 做不到。這層彈性是「30 秒內」flash-sale 的前置條件。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 autoscaling 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>需要警惕的判讀盲點：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「售票搶購型 flash-sale」的負載形狀 — 跟現有所有案例都不同的極端形狀。售票開賣在精確時間點（例如 12:00:00）瞬間湧入數十萬使用者、5 分鐘內賣完、之後流量歸零。這種「t=0 起跳、t=300 結束」的負載沒有「峰值預測」可言、只有「瞬間吸收」。</p>
<h2 id="觀察">觀察</h2>
<p>拓元 Tixcraft 在 AWS 的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/tixcraft/">tixCraft Case Study</a> 與 <a href="https://www.slideshare.net/slideshow/case-sharing-tixcraft-on-aws-reinvent-2015-recap/55681198">AWS re:Invent 2015 簡報</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同時選位用戶</td>
          <td>100,000+</td>
      </tr>
      <tr>
          <td>訂單峰值</td>
          <td>每分鐘 70,000+ 訂單、單秒最高 2,500+ 訂單</td>
      </tr>
      <tr>
          <td>3 分鐘內售出</td>
          <td>30,000+ 張票</td>
      </tr>
      <tr>
          <td>DynamoDB IOPS 範圍</td>
          <td>20 → 135,000（2015/8/29 峰值）</td>
      </tr>
      <tr>
          <td>資源擴張幅度</td>
          <td>30 分鐘內從 6 台擴到 800 台（130x）</td>
      </tr>
      <tr>
          <td>部署時間</td>
          <td>1,600 工時 → 20 分鐘</td>
      </tr>
      <tr>
          <td>壓測規模</td>
          <td>10,000 台 t2.micro、$130 / 小時</td>
      </tr>
      <tr>
          <td>任務總成本</td>
          <td>&lt; 2 台 MacBook Pro（約 $4,200）</td>
      </tr>
      <tr>
          <td>vs 傳統基礎設施成本</td>
          <td>0.26%</td>
      </tr>
      <tr>
          <td>成立年份</td>
          <td>2013 年底（雲原生）</td>
      </tr>
  </tbody>
</table>
<p>服務組合（依用戶提供的架構圖）：</p>
<ul>
<li><strong>入口</strong>：Amazon Route 53（DNS）+ CloudFront + S3（靜態資源 static.tixcraft.com）</li>
<li><strong>UI 層</strong>：Elastic Load Balancing → EC2 跨 3 個 Availability Zone（Tixcraft UI）</li>
<li><strong>API 層</strong>：ELB → EC2 跨 3 個 AZ（API）+ ElastiCache 加速 session</li>
<li><strong>資料層</strong>：DynamoDB 作為主要寫入目標（接 UI 寫入跟 API 寫入）</li>
<li><strong>付款層</strong>：獨立的 EC2 Payment、連到 traditional server（合作金流、跑於企業 data center）</li>
<li><strong>同步層</strong>：S3 Sync + EC2 Bridge 跟 corporate data center 的 backend 雙向同步</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>拓元案例最值得讀的、是它揭露三個 flash-sale 工程設計的非直覺事實。</p>
<ol>
<li><strong>DynamoDB 作為寫入緩衝、不是 OLTP</strong>：搶票時的「訂單」先丟進 DynamoDB、傳統 server 用自己能承受的速度消費、即時生效在此架構下不是目標。架構上 DynamoDB 扮演 <em>durable queue</em> 的角色、不是傳統 OLTP DB。這層解耦讓「前端可以擴 130 倍、後端不用同步擴」、避免後端被前端拖垮。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 outbox / async delivery 概念、跟 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 transaction boundary 分離。</li>
<li><strong>DynamoDB IOPS 從 20 衝到 135,000 = partition 設計能撐</strong>：這個 6,750 倍的彈性不是 DynamoDB 魔法、是 <em>partition key 設計均勻</em> 的結果。partition key 不均、IOPS 上限是「最熱 partition 上限」、不是「總和」。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 的同一判讀重點、跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 hot partition 識別。</li>
<li><strong>30 分鐘擴 130 倍 = 雲原生架構的存在證明</strong>：6 台 → 800 台不是手動操作、是 Auto Scaling Group + AMI prebuild + load balancer warmup 的組合。傳統 IDC 做不到。這層彈性是「30 秒內」flash-sale 的前置條件。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 autoscaling 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a>。</li>
</ol>
<p>需要警惕的判讀盲點：</p>
<ul>
<li>「限流到底怎麼做」這個工程社群關心的問題、架構圖上看不到明確元件。可能是「DynamoDB 寫入排隊 = 隱性限流」、也可能是 ELB / WAF / 應用層限流。沒有公開資訊不要過度推測。</li>
<li>2015 年的數字、用的還是 t2.micro 跟舊版 DynamoDB throughput model。現在等效實作可能會用 DynamoDB on-demand、AWS WAF、CloudFront WAF rules、或 SeatGeek-style Virtual Waiting Room（見 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16</a>）。</li>
<li>「30,000 張 / 3 分鐘」是 <em>票房成績</em>、不是 <em>系統極限</em>。系統能撐遠不止這個量、只是票本身賣完了。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>flash-sale 的核心架構模式：寫入緩衝 + 慢速消費</strong>：前端把訂單塞進可彈性擴容的儲存（DynamoDB / Redis Stream / Kafka）、後端按自己能力消費。這個模式讓「短時間吸收洪峰」跟「實際處理」解耦。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 與 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a>。</li>
<li><strong>partition key 設計是 flash-sale 的命脈</strong>：搶票場景天然容易 hot partition（同一場演唱會 = 同一 event_id）、必須用 composite key（event_id + user_id_hash）或 write sharding（event_id + random_suffix）分散。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a>。</li>
<li><strong>flash-sale 必須事先 ELB / Auto Scaling 預熱</strong>：開賣前 30-60 分鐘 pre-warm ELB、預先啟動最低額度的 EC2、避免 t=0 時冷啟動。對應 AWS 官方 <a href="https://aws.amazon.com/blogs/mt/top-considerations-for-flash-sale-events/">Flash Sale 工程指引</a>。</li>
<li><strong>付款層獨立、不被搶票流量影響</strong>：拓元把 Payment EC2 拉出來、直連傳統金流 server。讓「選位 + 下單」的高頻流量不會塞爆「付款」的低頻流量。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的關鍵路徑切分。</li>
<li><strong>限流（rate limiting）通常是隱性的、不一定看得到 component</strong>：DynamoDB 寫入排隊本身就是隱性限流；也可以加 WAF rate-based rule、ELB request throttling、或前置 Virtual Waiting Room 做明確限流（見 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16</a>）。</li>
</ol>
<p>跨平台等效：GCP Cloud Spanner / Bigtable + Cloud Pub/Sub 作 buffer + GKE autoscaling；Azure Cosmos DB + Service Bus + AKS；自建 PostgreSQL + Kafka + Kubernetes 都可以實作對等架構。差異是 vendor 整合度跟擴容速度。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 flash-sale 緩衝架構 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想做 partition key 設計 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> + <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a></li>
<li>想做明確限流 / 排隊機制 → <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek Virtual Waiting Room</a></li>
<li>想預熱 ELB / Auto Scaling → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a></li>
<li>對照其他售票市場 → <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a>（印度市場、年售 2 億張）</li>
<li>想理解 flash-sale 場景的 partition key 反模式 → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a></li>
<li>想評估 on-demand vs provisioned 在 flash-sale 的搭配 → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/tixcraft/">tixCraft Case Study (AWS)</a></li>
<li><a href="https://www.slideshare.net/slideshow/case-sharing-tixcraft-on-aws-reinvent-2015-recap/55681198">tixCraft on AWS re:Invent 2015 Recap (SlideShare)</a></li>
<li><a href="https://www.youtube.com/watch?v=Bi-1xjXvKgs">tixCraft: Handling Millions of Ticketing Requests with AWS (YouTube)</a></li>
<li><a href="https://aws.amazon.com/blogs/mt/top-considerations-for-flash-sale-events/">Top considerations for Flash sale events (AWS Cloud Operations Blog)</a></li>
<li><a href="https://aws.amazon.com/blogs/database/handle-traffic-spikes-with-amazon-dynamodb-provisioned-capacity/">Handle traffic spikes with Amazon DynamoDB provisioned capacity</a></li>
</ul>
]]></content:encoded></item><item><title>6.15 Environment Parity 與漂移控制</title><link>https://tarrragon.github.io/blog/backend/06-reliability/environment-parity/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/environment-parity/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Staging 通過但 production 上線失敗 — 這類事故的根因常常是環境差異。Environment parity 把 staging 與 production 的差異視為一級風險，要求會影響行為的差異被識別與管理。&lt;/p>
&lt;p>三個環境完全相同既不可能也不必要，但未被追蹤的差異會讓測試結論與真實服務脫鉤。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>Parity 漂移最先暴露的訊號是差異是否可見，接著決定差異是否會改變驗證結果。&lt;/p>
&lt;p>判讀時看四件事：&lt;/p>
&lt;ul>
&lt;li>config drift 是否有清單與責任人&lt;/li>
&lt;li>data shape 是否接近 production&lt;/li>
&lt;li>infra parity 是否涵蓋 network、storage、identity&lt;/li>
&lt;li>release 前是否知道哪些差異會影響判讀&lt;/li>
&lt;/ul>
&lt;h2 id="漂移來源分類">漂移來源分類&lt;/h2>
&lt;p>Parity 漂移按來源分類，不同來源的風險特徵與偵測手段不同。&lt;/p>
&lt;h3 id="config-drift">Config drift&lt;/h3>
&lt;p>環境變數、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> size、retry config、feature flag 在 staging 與 prod 不同步。這是最常見的漂移來源，因為 config 變更頻率高且通常不走完整 review 流程。&lt;/p>
&lt;p>典型暴露時機：staging 測試通過，但 prod 上線後 timeout 觸發或 pool 耗盡，根因是 staging 的 timeout 設定比 prod 寬鬆。偵測手段：定期 config snapshot diff，標註差異項目與 owner。&lt;/p>
&lt;h3 id="scale-drift">Scale drift&lt;/h3>
&lt;p>Staging 用單機或少量 replica，prod 用多區多 replica。query plan 在小資料集走 index scan、在大資料集走 table scan；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 在低併發下不飽和、在高併發下排隊；load balancer 在少 replica 時的路由行為跟多 replica 時不同。&lt;/p>
&lt;p>典型暴露時機：壓測在 staging 通過，但 prod 出現 connection pool 耗盡或 load balancer 的 least-connection 策略在高 replica 數下行為不同。偵測手段：對照 staging 與 prod 的 replica count、resource quota、auto-scaling 設定。&lt;/p>
&lt;h3 id="data-drift">Data drift&lt;/h3>
&lt;p>Staging 資料量遠小於 prod，資料分佈也不同。index scan vs table scan 的切換點跟資料量直接相關；cache hit ratio 跟 key 分佈與資料量相關；pagination 行為在千筆與百萬筆資料下差異顯著。&lt;/p>
&lt;p>典型暴露時機：staging 的查詢 &amp;lt; 50ms，prod 同一查詢 &amp;gt; 2s，根因是 staging 資料量不足以觸發 full table scan。偵測手段：比較 staging 與 prod 的資料表 row count 與 key 分佈統計。&lt;/p>
&lt;h3 id="dependency-drift">Dependency drift&lt;/h3>
&lt;p>Staging 跟 prod 使用不同版本的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> engine、cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 或 cloud service。版本差異的行為差異通常在 edge case 才暴露：不同版本的 SQL dialect、cache eviction policy、message ordering guarantee 可能不同。&lt;/p>
&lt;p>典型暴露時機：DB engine 小版本升級改變了 query optimizer 行為，staging 早已升級但 prod 延遲升級，兩邊 query plan 不同。偵測手段：維護 dependency version matrix，每次版本變更時檢查跨環境一致性。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Staging 通過但 production 上線失敗 — 這類事故的根因常常是環境差異。Environment parity 把 staging 與 production 的差異視為一級風險，要求會影響行為的差異被識別與管理。</p>
<p>三個環境完全相同既不可能也不必要，但未被追蹤的差異會讓測試結論與真實服務脫鉤。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Parity 漂移最先暴露的訊號是差異是否可見，接著決定差異是否會改變驗證結果。</p>
<p>判讀時看四件事：</p>
<ul>
<li>config drift 是否有清單與責任人</li>
<li>data shape 是否接近 production</li>
<li>infra parity 是否涵蓋 network、storage、identity</li>
<li>release 前是否知道哪些差異會影響判讀</li>
</ul>
<h2 id="漂移來源分類">漂移來源分類</h2>
<p>Parity 漂移按來源分類，不同來源的風險特徵與偵測手段不同。</p>
<h3 id="config-drift">Config drift</h3>
<p>環境變數、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> size、retry config、feature flag 在 staging 與 prod 不同步。這是最常見的漂移來源，因為 config 變更頻率高且通常不走完整 review 流程。</p>
<p>典型暴露時機：staging 測試通過，但 prod 上線後 timeout 觸發或 pool 耗盡，根因是 staging 的 timeout 設定比 prod 寬鬆。偵測手段：定期 config snapshot diff，標註差異項目與 owner。</p>
<h3 id="scale-drift">Scale drift</h3>
<p>Staging 用單機或少量 replica，prod 用多區多 replica。query plan 在小資料集走 index scan、在大資料集走 table scan；<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 在低併發下不飽和、在高併發下排隊；load balancer 在少 replica 時的路由行為跟多 replica 時不同。</p>
<p>典型暴露時機：壓測在 staging 通過，但 prod 出現 connection pool 耗盡或 load balancer 的 least-connection 策略在高 replica 數下行為不同。偵測手段：對照 staging 與 prod 的 replica count、resource quota、auto-scaling 設定。</p>
<h3 id="data-drift">Data drift</h3>
<p>Staging 資料量遠小於 prod，資料分佈也不同。index scan vs table scan 的切換點跟資料量直接相關；cache hit ratio 跟 key 分佈與資料量相關；pagination 行為在千筆與百萬筆資料下差異顯著。</p>
<p>典型暴露時機：staging 的查詢 &lt; 50ms，prod 同一查詢 &gt; 2s，根因是 staging 資料量不足以觸發 full table scan。偵測手段：比較 staging 與 prod 的資料表 row count 與 key 分佈統計。</p>
<h3 id="dependency-drift">Dependency drift</h3>
<p>Staging 跟 prod 使用不同版本的 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> engine、cache、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 或 cloud service。版本差異的行為差異通常在 edge case 才暴露：不同版本的 SQL dialect、cache eviction policy、message ordering guarantee 可能不同。</p>
<p>典型暴露時機：DB engine 小版本升級改變了 query optimizer 行為，staging 早已升級但 prod 延遲升級，兩邊 query plan 不同。偵測手段：維護 dependency version matrix，每次版本變更時檢查跨環境一致性。</p>
<h3 id="infra-drift">Infra drift</h3>
<p>Network topology、DNS 解析路徑、TLS 配置、identity provider 設定在不同環境不同。跨服務通訊路徑的差異最難偵測，因為這些差異通常在正常流量下不可見，只在跨區切換、failover 或 mTLS 驗證時才暴露。</p>
<p>典型暴露時機：staging 用同區呼叫、prod 跨區呼叫，latency 差異導致 timeout 觸發條件不同。偵測手段：infra-as-code diff 與定期 topology audit。</p>
<h2 id="漂移偵測機制">漂移偵測機制</h2>
<p>偵測環境漂移需要多種手段組合，單一手段無法覆蓋所有漂移來源。</p>
<h3 id="automated-config-diff">Automated config diff</h3>
<p>定期比較 staging 與 prod 的 config snapshot，輸出差異清單並標註 owner。diff 結果按風險等級分類：會影響行為的差異（timeout、pool size、retry policy）標為高風險；只影響標籤或描述的差異標為低風險。高風險差異在 release review 時必須被討論。</p>
<h3 id="contract--parity-test">Contract + parity test</h3>
<p><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Contract test</a> 驗證 API 邊界（schema、欄位、狀態碼）在不同環境的一致性。Parity test 更進一步，驗證同一請求在 staging 與 prod 的行為結果是否相同。兩者互補：contract test 抓結構差異，parity test 抓行為差異。</p>
<h3 id="shadow-traffic">Shadow traffic</h3>
<p>用 prod 流量的副本打 staging，比較回應差異。shadow traffic 能偵測 data drift 和 dependency drift，因為它用真實請求觸發真實查詢路徑。限制是寫入操作需要隔離處理（shadow write 不能影響 prod 資料），且 staging 需要有足夠容量承接 prod 流量副本。跟 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load testing</a> 的 synthetic traffic 限制互補 — synthetic traffic 偵測不到的環境差異，shadow traffic 通常能暴露。</p>
<h3 id="canary-作為中間層">Canary 作為中間層</h3>
<p>Canary 環境處於 staging 與 prod 之間，用少量真實流量驗證變更。parity 差異在 canary 階段暴露的成本遠低於在 prod 全量暴露。canary 的偵測價值在於它跑在 prod infra 上但只承接部分流量，能暴露 scale drift 和 infra drift。</p>
<p>canary 的限制是覆蓋時間：流量比例低時，low-frequency 的 edge case 可能在 canary 期間不出現。canary 時間越長覆蓋率越高，但拉長 canary 會延遲交付。這個 trade-off 要對齊變更風險等級 — 高風險變更拉長 canary，低風險變更可以縮短。</p>
<h2 id="production-like-data-策略">Production-like data 策略</h2>
<p>Staging 需要接近 prod 的資料才能讓驗證結果可信。三種策略各有 trade-off。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>真實度</th>
          <th>隱私風險</th>
          <th>維護成本</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production sample（脫敏）</td>
          <td>高</td>
          <td>中</td>
          <td>高</td>
          <td>query plan 敏感、資料分佈關鍵</td>
      </tr>
      <tr>
          <td>Synthetic generation</td>
          <td>中</td>
          <td>低</td>
          <td>中</td>
          <td>功能驗證為主、分佈次要</td>
      </tr>
      <tr>
          <td>Schema-only + seed</td>
          <td>低</td>
          <td>低</td>
          <td>低</td>
          <td>早期開發、schema 驗證</td>
      </tr>
  </tbody>
</table>
<p>Production sample 從 prod 抽樣後做 PII masking，資料分佈最接近真實，但需要遮罩管線且每次 schema 變更後要重新抽樣。Synthetic generation 用程式生成接近 prod 分佈的假資料，安全性高但分佈模型需要維護，偏移累積後資料特徵會跟 prod 脫鉤。Schema-only + seed 只複製 schema、用 seed 填少量資料，速度最快但跟 prod 差距最大，query plan 幾乎無法對齊。</p>
<p>選擇策略的判斷條件：如果系統的風險集中在 query performance 或 data-dependent 行為，production sample 是必要的；如果風險集中在功能正確性，synthetic generation 足夠；如果還在早期開發階段，schema-only + seed 可以先用，但上線前要升級。詳見 <a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 test data management</a>。</p>
<h2 id="parity-治理流程">Parity 治理流程</h2>
<p>環境漂移是持續的，一次對齊不代表之後不會漂移。治理流程的責任是讓漂移保持可見且可決策。</p>
<p><strong>維護環境差異清單</strong>：記錄所有已知的環境差異，每項標注 owner、風險等級與存在理由。有些差異是刻意的（staging 用較小 instance 節省成本），有些是遺忘的（某次 prod hotfix 沒同步到 staging）。區分刻意與遺忘的差異，才能知道哪些差異需要修復、哪些需要在判讀時考慮。</p>
<p><strong>Release 前 review 差異清單</strong>：每次 release 前把差異清單跟變更內容交叉比對。如果本次變更涉及 connection pool 設定，但 staging 的 pool size 跟 prod 不同，這個差異就會影響驗證結論，必須在放行時被標記。連到 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a> 的 pre-release checklist。</p>
<p><strong>Infra 變更同步</strong>：新增 infra 變更時，同步更新 staging 或在差異清單中標記新增風險。infra-as-code 讓同步變得可自動化，但仍需要 review 確認 staging 的資源配額是否需要調整。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">Heroku</a>：平台抽象越高，環境行為差異越不可見，漂移偵測需要更主動的手段。</li>
<li><a href="/blog/backend/08-incident-response/cases/gcp/" data-link-title="Google Cloud Platform" data-link-desc="GCP 重大事故時間線與架構脈絡">GCP</a>：區域、網路與權限設定差異會直接影響驗證結論，infra drift 在跨區場景最先暴露。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：大規模部署時，環境差異通常先變成事故放大器，漂移控制是降低放大倍數的前置工作。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>staging 通過、prod 上線失敗，根因是 config / scale 差異</td>
          <td>parity 差異未被 release review 識別</td>
          <td>把失敗根因加入環境差異清單 + release checklist</td>
      </tr>
      <tr>
          <td>staging 跟 prod 用不同 DB engine 版本 / cache 配置</td>
          <td>dependency drift 未被 version matrix 追蹤</td>
          <td>建 dependency version matrix、定期 diff</td>
      </tr>
      <tr>
          <td>shadow traffic 從未啟用、staging 流量靠手動測試</td>
          <td>data drift 和 dependency drift 沒有持續偵測機制</td>
          <td>啟用 shadow traffic 或 canary 驗證</td>
      </tr>
      <tr>
          <td>prod-only bug 反覆出現、staging 無法重現</td>
          <td>環境差異是 bug 的根因，差異清單可能遺漏關鍵項目</td>
          <td>回查差異清單、補漏項 + owner</td>
      </tr>
      <tr>
          <td>環境差異無 owner、漂移無 review</td>
          <td>parity 治理流程不存在或已停止運作</td>
          <td>指定 parity owner、加入 release review 流程</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a>：環境拓撲一致性與 canary 機制</li>
<li><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load testing</a>：staging 壓測結果的可信度受 parity 影響</li>
<li><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a>：契約覆蓋環境邊界</li>
<li><a href="/blog/backend/06-reliability/test-data-management/" data-link-title="6.16 Test Data Management" data-link-desc="把 fixture / seed / production-like data 作為跨模組共用 artifact，治理資料層次、遮罩策略與可重現性">6.16 test data management</a>：production-like data 來源與策略</li>
<li><a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a>：release 前的 parity review</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety boundary</a>：staging vs production 測試的安全邊界</li>
<li><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 post-incident review</a>：parity 漂移作為事故根因類別</li>
</ul>
]]></content:encoded></item><item><title>8.15 Vendor / 第三方依賴事故處理</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendor-dependency-incident/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendor-dependency-incident/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>依賴事故的特殊性：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a> 在外、自家 IR 流程多數工具失效&lt;/li>
&lt;li>決策模型：等 / 切換 / 降級 / 主動止血 的判讀&lt;/li>
&lt;li>vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 的可信度：滯後、語焉不詳、單點訊號&lt;/li>
&lt;li>等待 vs 切換 的成本對照：vendor ETA 不可信時的決策&lt;/li>
&lt;li>多區 / 多 vendor 的 failover 路徑（跟 6.7 DR 整合）&lt;/li>
&lt;li>跟客戶溝通：vendor 事故的對外承擔邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget&lt;/a> 的整合：事故是 budget 耗盡的事件&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder&lt;/a> 的整合：對外溝通不能單純甩鍋給 vendor&lt;/li>
&lt;li>反模式：依賴掛了只能等、無 fallback；對客戶說「是 vendor 的問題」就不更新；vendor SLA credit 從未請領&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Vendor / 第三方依賴事故處理是面對自己無法直接修正的故障時，選擇等待、切換、降級或止血的決策流程，責任是把控制權不足轉成可執行的判斷。&lt;/p>
&lt;p>這一頁處理的是外部控制面的失效。當 vendor 的狀態與自家觀測不一致時，最重要的是先決定自己還能做什麼。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 vendor 事故時，先看可替代路徑，再看等待的成本是否可接受。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 是否可信&lt;/li>
&lt;li>自家服務是否有 fallback 或 multi-vendor 策略&lt;/li>
&lt;li>等待 vendor ETA 的成本是否高於切換成本&lt;/li>
&lt;li>對外說明是否能清楚承擔自己服務的影響&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog&lt;/a>：監控平台本身是許多團隊的 vendor 依賴。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">Heroku&lt;/a>：PaaS 型依賴掛掉時，使用者常沒有太多控制面。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365&lt;/a>：身份與協作依賴故障會跨產品擴散。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>06.7 DR：多 vendor / 多區 failover&lt;/li>
&lt;li>06.14 dependency budget：事故事件的 budget 影響&lt;/li>
&lt;li>08.3 containment：對 vendor 故障的止血手段&lt;/li>
&lt;li>08.10 stakeholder：對外通訊的承擔邊界&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>依賴掛了、自家 IR 流程進入「等」狀態無 alternative&lt;/li>
&lt;li>vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page&lt;/a> 跟自家 observed 訊號不一致&lt;/li>
&lt;li>客戶投訴「為什麼你們的服務也掛」、無對外說明 playbook&lt;/li>
&lt;li>同 vendor 反覆出事、無多 vendor 策略&lt;/li>
&lt;li>vendor 事故事後無 SLA credit 請領記錄&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>06.7 DR：多 vendor / 多區 failover&lt;/li>
&lt;li>06.14 dependency budget：事故事件的 budget 影響&lt;/li>
&lt;li>08.3 containment：對 vendor 故障的止血手段&lt;/li>
&lt;li>08.10 stakeholder：對外通訊的承擔邊界&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>依賴事故的特殊性：<a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> 在外、自家 IR 流程多數工具失效</li>
<li>決策模型：等 / 切換 / 降級 / 主動止血 的判讀</li>
<li>vendor <a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 的可信度：滯後、語焉不詳、單點訊號</li>
<li>等待 vs 切換 的成本對照：vendor ETA 不可信時的決策</li>
<li>多區 / 多 vendor 的 failover 路徑（跟 6.7 DR 整合）</li>
<li>跟客戶溝通：vendor 事故的對外承擔邊界</li>
<li>跟 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a> 的整合：事故是 budget 耗盡的事件</li>
<li>跟 <a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder</a> 的整合：對外溝通不能單純甩鍋給 vendor</li>
<li>反模式：依賴掛了只能等、無 fallback；對客戶說「是 vendor 的問題」就不更新；vendor SLA credit 從未請領</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Vendor / 第三方依賴事故處理是面對自己無法直接修正的故障時，選擇等待、切換、降級或止血的決策流程，責任是把控制權不足轉成可執行的判斷。</p>
<p>這一頁處理的是外部控制面的失效。當 vendor 的狀態與自家觀測不一致時，最重要的是先決定自己還能做什麼。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 vendor 事故時，先看可替代路徑，再看等待的成本是否可接受。</p>
<p>重點訊號包括：</p>
<ul>
<li>vendor <a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 是否可信</li>
<li>自家服務是否有 fallback 或 multi-vendor 策略</li>
<li>等待 vendor ETA 的成本是否高於切換成本</li>
<li>對外說明是否能清楚承擔自己服務的影響</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog</a>：監控平台本身是許多團隊的 vendor 依賴。</li>
<li><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">Heroku</a>：PaaS 型依賴掛掉時，使用者常沒有太多控制面。</li>
<li><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365</a>：身份與協作依賴故障會跨產品擴散。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>06.7 DR：多 vendor / 多區 failover</li>
<li>06.14 dependency budget：事故事件的 budget 影響</li>
<li>08.3 containment：對 vendor 故障的止血手段</li>
<li>08.10 stakeholder：對外通訊的承擔邊界</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>依賴掛了、自家 IR 流程進入「等」狀態無 alternative</li>
<li>vendor <a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 跟自家 observed 訊號不一致</li>
<li>客戶投訴「為什麼你們的服務也掛」、無對外說明 playbook</li>
<li>同 vendor 反覆出事、無多 vendor 策略</li>
<li>vendor 事故事後無 SLA credit 請領記錄</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>06.7 DR：多 vendor / 多區 failover</li>
<li>06.14 dependency budget：事故事件的 budget 影響</li>
<li>08.3 containment：對 vendor 故障的止血手段</li>
<li>08.10 stakeholder：對外通訊的承擔邊界</li>
</ul>
]]></content:encoded></item><item><title>Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding&lt;/a>，兩者解的問題不同。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間&lt;/h2>
&lt;p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：&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">T0 master 失去回應
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> ↓ (down-after-milliseconds)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">T1 單一 Sentinel 標記 master 為 SDOWN（主觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間互問)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">T2 達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間選出 leader 來主導 failover)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">T3 leader Sentinel 從 replica 中挑一個當新 master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">T4 新 master 提升完成
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">T5 client 發現新 master、重連、恢復寫入&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 &lt;code>down-after-milliseconds&lt;/code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。&lt;/p>
&lt;p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>，兩者解的問題不同。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間</h2>
<p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：</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">T0   master 失去回應
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">     ↓ (down-after-milliseconds)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">T1   單一 Sentinel 標記 master 為 SDOWN（主觀下線）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">     ↓ (Sentinel 之間互問)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">T2   達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">     ↓ (Sentinel 之間選出 leader 來主導 failover)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">T3   leader Sentinel 從 replica 中挑一個當新 master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">     ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">T4   新 master 提升完成
</span></span><span class="line"><span class="ln">10</span><span class="cl">     ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
</span></span><span class="line"><span class="ln">11</span><span class="cl">T5   client 發現新 master、重連、恢復寫入</span></span></code></pre></div><p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 <code>down-after-milliseconds</code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。</p>
<p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。</p>
<h2 id="核心概念sentinel-的判定模型">核心概念：Sentinel 的判定模型</h2>
<p>Sentinel 是獨立於 Redis 資料節點的監控進程，它的判定靠兩層共識避免單一 Sentinel 誤判。</p>
<p><strong>SDOWN（Subjectively Down，主觀下線）</strong>：單一 Sentinel 在 <code>down-after-milliseconds</code> 內收不到 master 的有效回應（<code>PING</code>），就主觀認定它下線。這只是一個 Sentinel 的意見，不觸發 failover。</p>
<p><strong>ODOWN（Objectively Down，客觀下線）</strong>：當標記 SDOWN 的 Sentinel 數量達到 <code>quorum</code> 設定值，master 被客觀認定下線。只有 master 的 ODOWN 才會觸發 failover（replica 的下線只標記不 failover）。</p>
<p><code>quorum</code> 是「多少個 Sentinel 同意才算真的下線」，它跟「多少個 Sentinel 同意才能執行 failover」是兩個不同的數字——後者需要 Sentinel 的多數（majority），確保同時只有一個 leader 主導 failover，避免兩個 Sentinel 各自提升不同 replica 造成腦裂。</p>
<p><strong>為什麼 Sentinel 要部署奇數個且至少三個</strong>：quorum 跟 majority 都需要足夠的 Sentinel 投票。兩個 Sentinel 無法在其中一個故障時達成 majority；三個才能容忍一個故障。Sentinel 應部署在不同故障域（不同 AZ / 機架），且不要跟 Redis 資料節點同生共死。</p>
<p><strong>Sentinel 不是 proxy</strong>：client 不透過 Sentinel 讀寫資料。client 向 Sentinel 查詢「現在的 master 是誰」，拿到地址後直連 Redis。failover 後 client 必須重新向 Sentinel 查詢——這是 T4→T5 的關鍵，client library 要支援 Sentinel 模式才能自動完成。</p>
<h2 id="配置sentinel-的設定路徑">配置：Sentinel 的設定路徑</h2>
<p>最小三 Sentinel 配置，每個 Sentinel 一份 <code>sentinel.conf</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"># sentinel.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 監控名為 mymaster 的 master、quorum=2（三個 Sentinel 中兩個同意算 ODOWN）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sentinel monitor mymaster 10.0.0.1 <span class="m">6379</span> <span class="m">2</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="c1"># 多久收不到回應算 SDOWN（5 秒）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sentinel down-after-milliseconds mymaster <span class="m">5000</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"># failover 後同時最多幾個 replica 去 resync 新 master</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 設 1 = 串行 resync、避免所有 replica 同時 resync 拖垮新 master</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">sentinel parallel-syncs mymaster <span class="m">1</span>
</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 class="c1"># failover 整體逾時（三分鐘內沒完成算失敗、可重試）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">sentinel failover-timeout mymaster <span class="m">180000</span></span></span></code></pre></div><p>啟動 Sentinel：</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">redis-sentinel /path/to/sentinel.conf
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或 redis-server /path/to/sentinel.conf --sentinel</span></span></span></code></pre></div><p>client 端要用 Sentinel-aware 連線（以 Python redis-py 為例）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">redis.sentinel</span> <span class="kn">import</span> <span class="n">Sentinel</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">sentinel</span> <span class="o">=</span> <span class="n">Sentinel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">[(</span><span class="s2">&#34;10.0.0.10&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.11&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.12&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 寫入走 master（failover 後自動重新發現）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">master</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">master_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">master</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 讀取可走 replica</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">replica</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">slave_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">replica</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">)</span></span></span></code></pre></div><p>關鍵：client 透過 <code>master_for</code> 拿到的是一個會在 failover 後重新查詢 Sentinel 的連線封裝，不是寫死的 IP。直接寫死 master IP 的 client 在 failover 後會持續連到死掉的舊 master。</p>
<h3 id="防腦裂的兩個-master-端設定">防腦裂的兩個 master 端設定</h3>
<p>Sentinel 選主的同時，要防止舊 master 復活後繼續接受寫入（split-brain）。在 Redis master 端設：</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 個 replica 連著、且 replica lag &lt; 10 秒、master 才接受寫入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET min-replicas-to-write <span class="m">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET min-replicas-max-lag <span class="m">10</span></span></span></code></pre></div><p>這讓被網路隔離的舊 master（連不到 replica）自動停止接受寫入，避免它在隔離期間累積的寫入在復活後跟新 master 衝突。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1down-after-太短網路抖動誤觸-failover">Case 1：down-after 太短、網路抖動誤觸 failover</h3>
<p><strong>徵兆</strong>：master 其實沒死，只是一次短暫的網路抖動或 GC 暫停，Sentinel 卻觸發了 failover，造成一次不必要的中斷；甚至反覆 failover（flapping）。</p>
<p><strong>根因</strong>：<code>down-after-milliseconds</code> 設太短（例如 1000ms），master 一個短暫的 STW GC 或跨 AZ 網路抖動就超過閾值，被誤判 SDOWN→ODOWN。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>down-after-milliseconds</code> 設成能容忍正常抖動的值（5000-10000ms 是常見起點），用實際 RTT 與 GC pause 分布反推</li>
<li>quorum 設成多數而非 1，要求多個 Sentinel 同時看到下線，過濾單一 Sentinel 的網路問題</li>
<li>Sentinel 跟 Redis 不要跨高延遲鏈路放，網路品質直接影響誤判率</li>
<li>監控 failover 觸發頻率，flapping 是調參訊號</li>
</ol>
<h3 id="case-2failover-後-client-連到死掉的舊-master">Case 2：failover 後 client 連到死掉的舊 master</h3>
<p><strong>徵兆</strong>：failover 完成、Sentinel 日誌顯示新 master 已提升，但部分 application 持續寫入失敗或寫到舊 master（資料進黑洞），<code>CLIENT LIST</code> 在新 master 上看不到這些 client。</p>
<p><strong>根因</strong>：client 寫死了 master IP，或用的 client library 不支援 Sentinel 模式，failover 後不會重新向 Sentinel 查詢新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 一律用 Sentinel-aware 連線（<code>master_for</code> / lettuce 的 Sentinel 配置），不寫死 IP</li>
<li>確認 client library 版本支援 Sentinel 且配置正確（連的是 Sentinel port 26379，不是 Redis 6379）</li>
<li>對 latency-sensitive 服務，failover 後可主動 rolling restart application，清掉殘留連線</li>
<li>設 <code>min-replicas-to-write</code> 讓被隔離的舊 master 自動停寫，即使 client 連上去也寫不進，避免資料進黑洞</li>
</ol>
<h3 id="case-3選到-lag-大的-replicafailover-丟資料">Case 3：選到 lag 大的 replica、failover 丟資料</h3>
<p><strong>徵兆</strong>：failover 後發現最近幾秒的寫入不見了，新 master 的資料比預期舊。</p>
<p><strong>根因</strong>：Redis replication 是非同步的，replica 之間 lag 不一。Sentinel 選主會優先選 lag 小的（靠 <code>replica-priority</code> 與複製 offset），但若所有 replica 都 lag 大（master 寫入遠快於複製），無論選哪個都會丟掉未複製的寫入。Sentinel 的 failover 保證可用性，不保證零資料遺失。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>min-replicas-to-write</code> + <code>min-replicas-max-lag</code>，lag 過大時 master 主動停寫，限制資料遺失窗口</li>
<li>監控 replication lag（<code>master_repl_offset</code> vs replica 的 offset），lag 持續大代表複製跟不上寫入，要降寫入或擴容</li>
<li>用 <code>replica-priority</code> 把不適合當 master 的 replica（例如做備份的、跨區的）設成 0 排除</li>
<li>需要零資料遺失的場景，Sentinel 的非同步複製不夠，走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log（強一致持久性）</li>
</ol>
<h3 id="case-4腦裂舊-master-復活後雙寫衝突">Case 4：腦裂——舊 master 復活後雙寫衝突</h3>
<p><strong>徵兆</strong>：網路分區期間 Sentinel 提升了新 master，分區恢復後舊 master 回來，兩個 master 各自接受過寫入，資料出現衝突或舊 master 的寫入被覆蓋遺失。</p>
<p><strong>根因</strong>：舊 master 在分區期間被隔離（連不到 Sentinel 多數），但 client 若還連得到它且它沒設停寫保護，就繼續接受寫入。分區恢復後舊 master 被降為 replica，它在分區期間的寫入被新 master 的資料覆蓋。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>min-replicas-to-write 1</code> + <code>min-replicas-max-lag 10</code> 是核心防護——被隔離的舊 master 連不到 replica，自動停寫</li>
<li>Sentinel 部署在多數能存活的故障域，確保分區時多數 Sentinel 在新 master 那側</li>
<li>接受 Redis 的 CAP 取捨：Sentinel 偏向可用性，極端分區下無法完全避免資料遺失，要強一致走別的儲存層</li>
<li>failover 後監控舊 master 復活的降級流程，確認它正確變成 replica 且 resync</li>
</ol>
<h3 id="case-5parallel-syncs-設太大failover-後新-master-被-resync-拖垮">Case 5：parallel-syncs 設太大、failover 後新 master 被 resync 拖垮</h3>
<p><strong>徵兆</strong>：failover 完成的瞬間新 master 延遲暴增、甚至短暫無回應，所有 replica 同時對它發起全量同步。</p>
<p><strong>根因</strong>：<code>parallel-syncs</code> 設成大於 1（或等於 replica 數），failover 後所有 replica 同時對新 master 做 full resync。full resync 要新 master 做 BGSAVE（fork、見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence deep article</a>）並把 RDB 傳給每個 replica，多個同時進行直接打爆新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>parallel-syncs</code> 設 1，replica 串行 resync，犧牲一點恢復速度換新 master 不被拖垮</li>
<li>確認 master 端 <code>repl-backlog-size</code> 夠大，讓短暫斷線的 replica 走部分同步（partial resync）而非全量</li>
<li>監控 failover 後新 master 的 CPU / 記憶體，resync 期間是脆弱窗口</li>
<li>resync 的 fork 成本跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體 headroom</a> 直接相關，新 master 也要留 fork 空間</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Sentinel 的容量判讀，圍繞 failover 時間與資料遺失窗口：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>failover 總時間（T0→T5）</td>
          <td>數秒到十幾秒</td>
          <td>過長 → 查 down-after / parallel-syncs / client</td>
      </tr>
      <tr>
          <td>failover 觸發頻率</td>
          <td>罕見（真實故障才觸發）</td>
          <td>flapping → down-after 太短、quorum 太低</td>
      </tr>
      <tr>
          <td>replication lag</td>
          <td>&lt; 1 秒</td>
          <td>持續大 → 寫入超過複製能力、failover 會丟資料</td>
      </tr>
      <tr>
          <td>Sentinel 數量</td>
          <td>奇數、≥ 3、跨故障域</td>
          <td>&lt; 3 或同故障域 → 無法容忍 Sentinel 故障</td>
      </tr>
      <tr>
          <td>寫入中斷可容忍時間</td>
          <td>業務定義</td>
          <td>不可容忍 → Sentinel 不夠、走 managed multi-AZ</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單 master 容量不夠（記憶體 / 吞吐超過單機）</strong>：Sentinel 解 HA 不解容量。要橫向擴容走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a>，它自帶 sharding 與 per-shard failover。</li>
<li><strong>不想自己運維 Sentinel 與 failover 演練</strong>：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a> 的 Multi-AZ 自動 failover 把這條時序鏈託管，failover ~30 秒到幾分鐘，省掉 Sentinel 部署與調參，代價是 managed premium。</li>
<li><strong>需要零資料遺失的強持久性</strong>：Sentinel 的非同步複製在 failover 時會丟未複製的寫入。要強一致走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Sentinel 是 HA 的一層，但它的每一段都跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></strong>：Sentinel 是「不分片的 HA」，Cluster 是「分片 + 每 shard 自帶 failover」。容量需求決定走哪條，本文是前者。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：failover 後的 resync 靠 BGSAVE（fork），新 master 的 fork 成本是 resync 期間的脆弱點。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：新 master 提升後要承接全部寫入並支援 replica resync 的 fork，記憶體 headroom 不能少。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">Meta cache consistency</a></strong>：failover / replica promotion 期間的 stale read 與一致性議題，是大規模 cache 治理的核心，Sentinel 的非同步複製是 stale window 的來源之一。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（managed multi-AZ failover）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/innodb-tuning/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/innodb-tuning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>InnoDB engine tuning&lt;/em> — 4 個影響最大的 knob 跟對應 production 行為。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="開場常見痛點">開場：常見痛點&lt;/h2>
&lt;p>一個 100 GB MySQL DB、64 GB RAM 的 server、p99 query latency 從 5ms 飆到 50ms。第一直覺是 server overload — 但 CPU &amp;lt; 30%、disk IO 50 IOPS。為什麼慢？&lt;/p>
&lt;p>打開 &lt;code>SHOW VARIABLES LIKE 'innodb_buffer_pool_size'&lt;/code>：&lt;code>134217728&lt;/code>（128 MB）。對 64 GB RAM server、buffer pool 只用了 128 MB、剩 99.9% 的 working set 每次 query 都要從 disk 讀。CPU 閒、disk 沒滿、是因為 &lt;em>MySQL 自己不用 RAM&lt;/em> — 用 InnoDB 預設值跑 100 GB DB 等於 disk-only 模式。&lt;/p>
&lt;p>這個案例展示 InnoDB tuning 的核心：MySQL 預設值是 &lt;em>為 16 GB RAM 設計&lt;/em>、production server RAM 越大、預設值離 optimal 越遠。&lt;/p>
&lt;h2 id="4-個-critical-knob">4 個 critical knob&lt;/h2>
&lt;p>對 90% production case、調這 4 個就解決大部分 InnoDB 性能問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Knob&lt;/th>
 &lt;th>預設&lt;/th>
 &lt;th>對 production 建議&lt;/th>
 &lt;th>影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>innodb_buffer_pool_size&lt;/code>&lt;/td>
 &lt;td>128 MB&lt;/td>
 &lt;td>系統 RAM 50-75%（dedicated server 75%）&lt;/td>
 &lt;td>讀效能（資料能否在 RAM）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>innodb_log_file_size&lt;/code>&lt;/td>
 &lt;td>48 MB（×2 file）&lt;/td>
 &lt;td>1-4 GB（依寫吞吐、8.0.30+ 改 &lt;code>innodb_redo_log_capacity&lt;/code>）&lt;/td>
 &lt;td>寫效能（flush 頻率）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>innodb_flush_log_at_trx_commit&lt;/code>&lt;/td>
 &lt;td>1 (full ACID)&lt;/td>
 &lt;td>1（金融 / 訂單）/ 2（高吞吐可容 1 秒 loss）&lt;/td>
 &lt;td>寫吞吐 vs durability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>innodb_io_capacity&lt;/code> + &lt;code>_max&lt;/code>&lt;/td>
 &lt;td>200 / 2000&lt;/td>
 &lt;td>SSD: 2000 / 20000; NVMe: 10000 / 40000&lt;/td>
 &lt;td>flush 速度（適配儲存）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>其他 knob（&lt;code>innodb_thread_concurrency&lt;/code> / &lt;code>innodb_buffer_pool_instances&lt;/code> / &lt;code>innodb_read_io_threads&lt;/code> 等）也有影響、但對多數 case &lt;em>先把這 4 個調對&lt;/em> 比微調其他 20 個重要。&lt;/p>
&lt;h2 id="knob-1buffer-pool--把-working-set-拉進-ram">Knob 1：Buffer pool — 把 working set 拉進 RAM&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer-pool/" data-link-title="Buffer Pool" data-link-desc="說明資料庫如何用記憶體快取磁碟頁，以降低 I/O 並影響查詢效能">InnoDB buffer pool&lt;/a> 是 &lt;em>page cache&lt;/em> — 從 disk 讀過的 16 KB page 快取在 RAM、下次 query 直接 RAM 讀。Buffer pool 越大、cache hit ratio 越高、disk IO 越少。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>InnoDB engine tuning</em> — 4 個影響最大的 knob 跟對應 production 行為。</p></blockquote>
<hr>
<h2 id="開場常見痛點">開場：常見痛點</h2>
<p>一個 100 GB MySQL DB、64 GB RAM 的 server、p99 query latency 從 5ms 飆到 50ms。第一直覺是 server overload — 但 CPU &lt; 30%、disk IO 50 IOPS。為什麼慢？</p>
<p>打開 <code>SHOW VARIABLES LIKE 'innodb_buffer_pool_size'</code>：<code>134217728</code>（128 MB）。對 64 GB RAM server、buffer pool 只用了 128 MB、剩 99.9% 的 working set 每次 query 都要從 disk 讀。CPU 閒、disk 沒滿、是因為 <em>MySQL 自己不用 RAM</em> — 用 InnoDB 預設值跑 100 GB DB 等於 disk-only 模式。</p>
<p>這個案例展示 InnoDB tuning 的核心：MySQL 預設值是 <em>為 16 GB RAM 設計</em>、production server RAM 越大、預設值離 optimal 越遠。</p>
<h2 id="4-個-critical-knob">4 個 critical knob</h2>
<p>對 90% production case、調這 4 個就解決大部分 InnoDB 性能問題：</p>
<table>
  <thead>
      <tr>
          <th>Knob</th>
          <th>預設</th>
          <th>對 production 建議</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>innodb_buffer_pool_size</code></td>
          <td>128 MB</td>
          <td>系統 RAM 50-75%（dedicated server 75%）</td>
          <td>讀效能（資料能否在 RAM）</td>
      </tr>
      <tr>
          <td><code>innodb_log_file_size</code></td>
          <td>48 MB（×2 file）</td>
          <td>1-4 GB（依寫吞吐、8.0.30+ 改 <code>innodb_redo_log_capacity</code>）</td>
          <td>寫效能（flush 頻率）</td>
      </tr>
      <tr>
          <td><code>innodb_flush_log_at_trx_commit</code></td>
          <td>1 (full ACID)</td>
          <td>1（金融 / 訂單）/ 2（高吞吐可容 1 秒 loss）</td>
          <td>寫吞吐 vs durability</td>
      </tr>
      <tr>
          <td><code>innodb_io_capacity</code> + <code>_max</code></td>
          <td>200 / 2000</td>
          <td>SSD: 2000 / 20000; NVMe: 10000 / 40000</td>
          <td>flush 速度（適配儲存）</td>
      </tr>
  </tbody>
</table>
<p>其他 knob（<code>innodb_thread_concurrency</code> / <code>innodb_buffer_pool_instances</code> / <code>innodb_read_io_threads</code> 等）也有影響、但對多數 case <em>先把這 4 個調對</em> 比微調其他 20 個重要。</p>
<h2 id="knob-1buffer-pool--把-working-set-拉進-ram">Knob 1：Buffer pool — 把 working set 拉進 RAM</h2>
<p><a href="/blog/backend/knowledge-cards/buffer-pool/" data-link-title="Buffer Pool" data-link-desc="說明資料庫如何用記憶體快取磁碟頁，以降低 I/O 並影響查詢效能">InnoDB buffer pool</a> 是 <em>page cache</em> — 從 disk 讀過的 16 KB page 快取在 RAM、下次 query 直接 RAM 讀。Buffer pool 越大、cache hit ratio 越高、disk IO 越少。</p>
<p><strong>Sizing</strong>：</p>
<ul>
<li><em>Dedicated MySQL server</em>：RAM 70-80%（剩 20-30% 給 OS / MySQL 其他結構 / connection buffer）</li>
<li><em>Shared server</em>：RAM 30-50%（看其他 process 需求）</li>
<li><em>Container / Kubernetes</em>：對 container memory limit 70%（不是 host RAM）</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 64 GB RAM dedicated server</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">48G</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">innodb_buffer_pool_instances</span> <span class="o">=</span> <span class="s">8  # 分 8 個 instance 降 mutex contention（每 instance 6 GB）</span></span></span></code></pre></div><p><strong>Buffer pool warm-up</strong>：MySQL 重啟後 buffer pool 是空的、要慢慢從 disk 把熱資料拉回 RAM。預設 5.7+ MySQL 啟動時 <em>dump buffer pool LRU list 到 disk</em>、重啟時 <em>自動 restore</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">innodb_buffer_pool_dump_at_shutdown</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">innodb_buffer_pool_load_at_startup</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">innodb_buffer_pool_dump_pct</span> <span class="o">=</span> <span class="s">75  # 只 dump 最 hot 的 75% page list</span></span></span></code></pre></div><p>沒這個 warm-up、重啟後第 1 個小時 query latency 都偏高、application 看到 p99 spike。</p>
<h2 id="knob-2redo-log--flush-頻率跟寫吞吐">Knob 2：Redo log — flush 頻率跟寫吞吐</h2>
<p>InnoDB 寫入 <em>先寫 redo log（順序寫）</em>、再非同步寫到 data file（隨機寫）。Redo log 滿了強迫 flush data file、flush 期間寫吞吐降。</p>
<p><code>innodb_log_file_size</code> 控制每個 log file 大小（預設 2 個 file）：</p>
<ul>
<li>5.7：預設 48 MB × 2 = 96 MB total</li>
<li>8.0：預設仍是 48 MB × 2、8.0.30+ 改用動態 <code>innodb_redo_log_capacity</code>（default 100 MB total）</li>
</ul>
<p>對 5K WPS server、預設容量可能 <em>每分鐘 flush 一次</em>、寫吞吐持續 stall。提高到 1-4 GB total、flush 改成每 30 分鐘一次、寫吞吐穩定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">2G       # 大寫吞吐 server 設 1-4 GB</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">innodb_log_files_in_group</span> <span class="o">=</span> <span class="s">2   # 預設 2 個就夠</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">innodb_log_buffer_size</span> <span class="o">=</span> <span class="s">64M    # log 寫 disk 前的 RAM buffer</span></span></span></code></pre></div><p><strong>Trade-off</strong>：log file 越大、recovery 時間越長（crash 後 InnoDB 要 replay 全部 log）。1 GB log 通常 &lt; 1 分鐘 recovery、4 GB 可能 5 分鐘以上。SSD / NVMe 這個 trade-off 不嚴重、HDD 要注意。</p>
<p>MySQL 8.0+ 改進：log file 可動態調整（不用重啟）、且 <em>automatic redo log writer threads</em> 降低 mutex contention。</p>
<h2 id="knob-3flush-method--acid-vs-吞吐">Knob 3：Flush method — ACID vs 吞吐</h2>
<p><code>innodb_flush_log_at_trx_commit</code> 控制 <em>每個 transaction commit 時要不要 flush log 到 disk</em>：</p>
<ul>
<li><code>1</code>（預設）：每次 commit fsync log file → <em>zero data loss on crash</em></li>
<li><code>2</code>：每次 commit 寫 log file（但 OS-level cache、不 fsync）→ <em>server crash 不丟、OS crash 丟 1 秒</em></li>
<li><code>0</code>：每秒 fsync 一次 → <em>任何 crash 丟 1 秒</em></li>
</ul>
<p><code>sync_binlog</code> 對應 binlog（不是 InnoDB log）：</p>
<ul>
<li><code>1</code>（建議）：每次 commit fsync binlog</li>
<li><code>0</code>：依賴 OS sync、容易丟 binlog → replication / CDC 風險</li>
</ul>
<p><strong>Production 組合</strong>：</p>
<table>
  <thead>
      <tr>
          <th>用途</th>
          <th><code>innodb_flush_log_at_trx_commit</code></th>
          <th><code>sync_binlog</code></th>
          <th>寫吞吐</th>
          <th>Crash data loss</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>金融 / 訂單 / 支付</td>
          <td>1</td>
          <td>1</td>
          <td>baseline</td>
          <td>0</td>
      </tr>
      <tr>
          <td>一般 web 應用</td>
          <td>1</td>
          <td>1</td>
          <td>baseline</td>
          <td>0</td>
      </tr>
      <tr>
          <td>高寫吞吐 + 容忍 1 sec loss</td>
          <td>2</td>
          <td>1</td>
          <td>+30-50%</td>
          <td>OS crash 丟 1 秒</td>
      </tr>
      <tr>
          <td>Dev / test</td>
          <td>2</td>
          <td>0</td>
          <td>+50-100%</td>
          <td>不重要</td>
      </tr>
      <tr>
          <td>不要這樣設</td>
          <td>0</td>
          <td>0</td>
          <td>+100%</td>
          <td>任意 crash 丟資料</td>
      </tr>
  </tbody>
</table>
<p>多數 production 用 <code>1 + 1</code>、雖然慢但 <em>簡單可預測</em>。改成 <code>2 + 1</code> 之前要明確 <em>能容忍 1 秒 data loss</em>、且通常 review 過 Disaster Recovery Plan。</p>
<h2 id="knob-4io-capacity--適配儲存">Knob 4：IO capacity — 適配儲存</h2>
<p>InnoDB 後台 flush 速度受 <code>innodb_io_capacity</code> 限制：</p>
<ul>
<li><code>innodb_io_capacity</code>（一般）：後台 flush 目標 IOPS</li>
<li><code>innodb_io_capacity_max</code>（突發）：emergency flush 上限</li>
</ul>
<p><strong>對應儲存類型</strong>：</p>
<table>
  <thead>
      <tr>
          <th>儲存</th>
          <th>IOPS 能力</th>
          <th><code>innodb_io_capacity</code></th>
          <th><code>innodb_io_capacity_max</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>7200 RPM HDD</td>
          <td>~80 IOPS</td>
          <td>100</td>
          <td>200</td>
      </tr>
      <tr>
          <td>SSD (SATA)</td>
          <td>10K-50K IOPS</td>
          <td>2000</td>
          <td>20000</td>
      </tr>
      <tr>
          <td>NVMe SSD</td>
          <td>100K-500K IOPS</td>
          <td>10000</td>
          <td>40000</td>
      </tr>
      <tr>
          <td>EBS gp3</td>
          <td>3000-16000 IOPS</td>
          <td>5000</td>
          <td>16000</td>
      </tr>
      <tr>
          <td>EBS io2</td>
          <td>50K-256K IOPS</td>
          <td>20000</td>
          <td>60000</td>
      </tr>
  </tbody>
</table>
<p>預設 <code>200 / 2000</code> 是 <em>為 HDD 設計</em>、SSD / NVMe server 用預設值 = InnoDB 自我限速、flush 慢、寫入瓶頸。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># NVMe SSD server</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">innodb_io_capacity</span> <span class="o">=</span> <span class="s">10000</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">innodb_io_capacity_max</span> <span class="o">=</span> <span class="s">40000</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">innodb_flush_neighbors</span> <span class="o">=</span> <span class="s">0  # NVMe 不需要 group flush 相鄰 page</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-buffer-pool-沒-warm-up--重啟後-1-小時-p99-飆">1. Buffer pool 沒 warm-up — 重啟後 1 小時 p99 飆</h3>
<p>MySQL 重啟（OS upgrade / config change / failover）後、buffer pool 是空的、所有 query 第一次都 disk 讀、p99 latency 飆 5-10x、application 看到 timeout。</p>
<p>修法：</p>
<ul>
<li>啟用 <code>innodb_buffer_pool_dump_at_shutdown=1</code> + <code>innodb_buffer_pool_load_at_startup=1</code></li>
<li>對 <em>沒 graceful shutdown</em> 的 crash（OOM / kernel panic）、buffer pool 沒 dump、warm-up 後第一個小時仍辛苦</li>
<li>重要 server 重啟前手動 dump：<code>SET GLOBAL innodb_buffer_pool_dump_now=ON</code></li>
<li>對於不能容忍 cold cache 的場景、failover 前 <em>先 pre-warm new primary</em>（用 query replay 把 hot data 拉到 buffer pool）</li>
</ul>
<h3 id="2-log-file-size-設太小--checkpoint-storm">2. Log file size 設太小 — checkpoint storm</h3>
<p><code>innodb_log_file_size=48M</code> 預設、高寫吞吐 server log 每分鐘 flush 一次、flush 期間 <em>checkpoint storm</em> — 寫吞吐降 50%、p99 暴增。錯誤訊號是 <code>innodb_log_waits</code> 持續 &gt; 0。</p>
<p>修法：</p>
<ul>
<li>監控 <code>SHOW STATUS LIKE 'Innodb_log_waits'</code> — 應該長期接近 0</li>
<li>提高 <code>innodb_log_file_size</code> 到 1-4 GB（依寫吞吐）</li>
<li>8.0+ 可動態調整、5.7 需要 <em>正常 shutdown</em> 後改、開啟前先 dump buffer pool（避免 cold cache）</li>
</ul>
<h3 id="3-sync_binlog0-換速度--replication-永久-broken-風險">3. <code>sync_binlog=0</code> 換速度 — replication 永久 broken 風險</h3>
<p>開發 / staging 改 <code>sync_binlog=0</code>（加快寫入）、後來複製到 production 配置、production 同樣 <code>sync_binlog=0</code>。OS crash 後 binlog 缺最後幾秒 transaction、replica 跟 primary GTID set diverge、replication broken、要 <em>重建 replica from base backup</em>（小時級 recovery）。</p>
<p>修法：</p>
<ul>
<li><em>Production 永遠用 <code>sync_binlog=1</code></em>、不要為了寫吞吐犧牲 binlog durability</li>
<li>開發 / staging 配置跟 production 隔離、不要直接 copy config</li>
<li>Replica 失聯後 <em>用 GTID 自動 re-attach</em>（不是 binlog position）— 仍然需要 binlog 完整、<code>sync_binlog=0</code> 仍是風險</li>
</ul>
<h3 id="4-io-scheduler--不是-innodb-tuning-但影響大">4. IO scheduler — 不是 InnoDB tuning 但影響大</h3>
<p>Linux <code>noop</code> / <code>deadline</code> / <code>cfq</code> IO scheduler 對 SSD / NVMe 影響大：</p>
<ul>
<li><code>cfq</code>（traditional spinning disk default）：對 SSD 嚴重 bottleneck</li>
<li><code>deadline</code>：對 SSD 較好、但有 latency cap</li>
<li><code>noop</code> / <code>none</code>：對 NVMe 最好（讓 device 自己處理 queue）</li>
</ul>
<p><strong>Production check</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">cat /sys/block/sda/queue/scheduler
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 應該顯示： [none] mq-deadline (NVMe)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 或：         noop deadline [cfq] (cfq 是錯的)</span></span></span></code></pre></div><p>不是 InnoDB knob、但影響 InnoDB IO behavior &gt; 30%。InnoDB tuning 前先確認 OS-level IO scheduler 對。</p>
<h3 id="5-undo-log-膨脹--purge-跟不上">5. Undo log 膨脹 — purge 跟不上</h3>
<p>Undo log 紀錄 <em>未來可能 rollback 需要的舊版本 row</em>。長 transaction（hours-level）讓 undo log 持續累積、不能 purge、最後 InnoDB tablespace 膨脹幾 GB、disk 滿。</p>
<p>訊號：</p>
<ul>
<li><code>SHOW ENGINE INNODB STATUS</code> 看 <code>History list length</code> 持續成長（正常 &lt; 1000、異常 millions）</li>
<li><code>information_schema.innodb_metrics</code> 的 <code>trx_rseg_history_len</code></li>
</ul>
<p>修法：</p>
<ul>
<li>找 long-running transaction：<code>SELECT * FROM information_schema.innodb_trx WHERE trx_started &lt; NOW() - INTERVAL 1 HOUR</code></li>
<li>KILL 該 transaction（謹慎、可能 application bug）</li>
<li>8.0+ 用 separate undo tablespace（<code>innodb_undo_tablespaces</code>）、不污染 main tablespace、且可以 truncate</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>對 64 GB RAM、NVMe SSD、5K WPS、100 GB DB 的 server：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># my.cnf production-ready baseline</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">[mysqld]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Buffer pool (75% RAM)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">48G</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">innodb_buffer_pool_instances</span> <span class="o">=</span> <span class="s">8</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">innodb_buffer_pool_dump_at_shutdown</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">innodb_buffer_pool_load_at_startup</span> <span class="o">=</span> <span class="s">1</span>
</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="c1"># Redo log</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">2G</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">innodb_log_files_in_group</span> <span class="o">=</span> <span class="s">2</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">innodb_log_buffer_size</span> <span class="o">=</span> <span class="s">64M</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Flush behavior</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="na">innodb_flush_log_at_trx_commit</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="na">sync_binlog</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="na">innodb_flush_method</span> <span class="o">=</span> <span class="s">O_DIRECT  # 跳過 OS page cache 避免 double cache</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># IO capacity (NVMe)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="na">innodb_io_capacity</span> <span class="o">=</span> <span class="s">10000</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="na">innodb_io_capacity_max</span> <span class="o">=</span> <span class="s">40000</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="na">innodb_flush_neighbors</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="na">innodb_lru_scan_depth</span> <span class="o">=</span> <span class="s">1024</span>
</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 class="c1"># Concurrency</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="na">innodb_thread_concurrency</span> <span class="o">=</span> <span class="s">0  # 0 = no limit (8.0+ 推薦)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="na">innodb_read_io_threads</span> <span class="o">=</span> <span class="s">8</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="na">innodb_write_io_threads</span> <span class="o">=</span> <span class="s">8</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"># 額外</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="na">innodb_file_per_table</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="na">innodb_strict_mode</span> <span class="o">=</span> <span class="s">1</span></span></span></code></pre></div><p>跨不同 server spec、<code>buffer_pool_size</code> / <code>io_capacity</code> 隨硬體調整、其他 knob 變動小。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p><code>sync_binlog=1</code> + <code>innodb_flush_log_at_trx_commit=1</code> 是 <em>durability baseline</em>、影響 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a> 的 <em>primary durability</em>。Semi-sync 加在這基礎上提供 <em>跨 server durability</em>。</p>
<h3 id="跟-proxysql">跟 ProxySQL</h3>
<p>ProxySQL connection pool 降低 <em>MySQL connection 開銷</em>、但 <em>每個 connection</em> 仍消耗 8-10 MB RAM（thread stack + session buffer）。Buffer pool 設 75% RAM 後、剩 25% 給 connection / temporary buffer / OS。Connection 太多會擠掉 buffer pool。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL 配置</a>。</p>
<h3 id="跟-aurora-mysql">跟 Aurora MySQL</h3>
<p>Aurora 改寫 InnoDB storage layer、上方 knob 大多 <em>Aurora 自動管理</em>：</p>
<ul>
<li>Buffer pool size：Aurora compute instance 自動配</li>
<li>Redo log：Aurora 自己的 distributed log、不用 <code>innodb_log_file_size</code></li>
<li><code>sync_binlog</code> / <code>innodb_flush_log_at_trx_commit</code>：Aurora storage layer 保證 durability、應用層 knob 影響小</li>
</ul>
<p>Aurora user 仍可 tune <code>innodb_buffer_pool_size</code> 等、但操作面從 InnoDB 內部議題變成 <em>Aurora instance class 選擇</em>。詳見 <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 page</a>。</p>
<h3 id="跟-osc-tool">跟 OSC tool</h3>
<p>InnoDB tuning 不直接影響 OSC 工具行為、但 <em>log file size 太小</em> 時 gh-ost / pt-osc 寫 ghost table 容易 trigger checkpoint storm、放慢整個 schema migration。詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<p><code>SHOW STATUS LIKE</code> + Performance Schema 提供：</p>
<ul>
<li><code>Innodb_buffer_pool_read_requests</code> / <code>_reads</code> → cache hit ratio = <code>1 - reads/read_requests</code>、應該 &gt; 99%</li>
<li><code>Innodb_log_waits</code> → checkpoint pressure、應該 = 0</li>
<li><code>Innodb_log_write_requests</code> / <code>_writes</code> → log buffer 效率</li>
<li><code>Innodb_rows_inserted</code> / <code>_updated</code> / <code>_read</code> → workload 形狀</li>
<li><code>Innodb_row_lock_waits</code> / <code>_time</code> → lock contention</li>
</ul>
<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> / <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 透過 <a href="https://github.com/prometheus/mysqld_exporter">mysqld_exporter</a> / <a href="https://www.percona.com/software/database-tools/percona-monitoring-and-management">Percona Monitoring</a> 持續 trend。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（<code>sync_binlog</code> 跟 replication 互動）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（connection 跟 buffer pool 爭 RAM）</li>
<li><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 page</a>（managed MySQL、InnoDB tuning 部分轉手）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PostgreSQL Autovacuum Tuning</a>（PG sibling、不同 engine 內部 tuning）</li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-default-se.html">InnoDB Configuration</a> / <a href="https://www.percona.com/blog/mysql-101-tuning-mysql-after-installation/">Percona Tuning Guide</a></li>
</ul>
]]></content:encoded></item><item><title>3.C16 Robinhood：Faust Python stream processing</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/</guid><description>&lt;p>這個案例的核心責任是說明語言生態與 stream framework 的選型張力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Robinhood 每天處理 billions of events / TB 資料、用於 risk signal、order quality、market data、fraud detection；team 多為 Python、不想用 JVM 生態。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>把 Kafka Streams 的 stateful streaming 模式（topology、tables、windowing）移植到 Python library 形式、不需要 Yarn / Mesos resource manager。揭露 stream processing framework 選型常被語言生態主導、不是技術 feature。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：跨語言 client / Streams framework / stream processing on Kafka。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/robinhood-engineering/faust-stream-processing-for-python-a66d3a51212d">Faust: Stream Processing for Python&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明語言生態與 stream framework 的選型張力。</p>
<h2 id="觀察">觀察</h2>
<p>Robinhood 每天處理 billions of events / TB 資料、用於 risk signal、order quality、market data、fraud detection；team 多為 Python、不想用 JVM 生態。</p>
<h2 id="判讀">判讀</h2>
<p>把 Kafka Streams 的 stateful streaming 模式（topology、tables、windowing）移植到 Python library 形式、不需要 Yarn / Mesos resource manager。揭露 stream processing framework 選型常被語言生態主導、不是技術 feature。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：跨語言 client / Streams framework / stream processing on Kafka。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/robinhood-engineering/faust-stream-processing-for-python-a66d3a51212d">Faust: Stream Processing for Python</a></li>
</ul>
]]></content:encoded></item><item><title>Wiz</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/</guid><description>&lt;p>Wiz 是 &lt;em>agentless CNAPP&lt;/em>（Cloud-Native Application Protection Platform）的代表、用 &lt;em>cloud API + snapshot scan&lt;/em> 從外面看雲、不在 workload 上裝 agent。2020 年由前 Microsoft Cloud Security Group 創辦人成立、2024 估值約 $12B、是 CNAPP 賽道的後起黑馬。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> 的差異在 &lt;em>風險優先級的組合方式&lt;/em>、vulnerability 掃描能力本身都具備 — Wiz 用 &lt;em>Security Graph + Toxic Combination&lt;/em> 把多個 low-risk finding 串成 &lt;em>attack path&lt;/em>、而不是給你 10000 個獨立 CVE。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Wiz 的核心定位是 &lt;em>agentless cloud posture + workload protection platform&lt;/em>、把 CSPM（Cloud Security Posture Management）/ CWP（Cloud Workload Protection）/ CIEM（Cloud Infrastructure Entitlement Management）/ KSPM（Kubernetes Security Posture Management）/ DSPM（Data Security Posture Management）整合在同一個 Security Graph 上面。底層是 &lt;em>Connector&lt;/em>（讀 AWS / GCP / Azure / OCI / K8s 的 read-only API + snapshot scan）、頂層是 &lt;em>Issues + Projects + Toxic Combination rules&lt;/em>。&lt;/p>
&lt;p>跟 Prisma Cloud / Lacework 比、Wiz 走 &lt;em>graph-first&lt;/em> — 把 resource / IAM / vulnerability / secret / network exposure 連成圖（跳過 finding list 層）、可以用 query 問「哪些 EC2 有 RCE CVE 且 IMDS v1 且能 assume 跨帳戶 admin role」。跟 CrowdStrike Falcon Cloud Security 比、Wiz 是 &lt;em>agentless-first&lt;/em>（CWP 才用 sensor、posture / 漏洞掃描 0 agent）、Falcon CS 走 &lt;em>endpoint agent 延伸到雲&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a> CSPM 比、Datadog 是 &lt;em>observability platform 上的 security view&lt;/em>、Wiz 是 &lt;em>security-first CNAPP&lt;/em>、Wiz 的 graph 跟 toxic combination 深度大幅領先、但獨立 SIEM / log 能力不如 Datadog / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a> 比、Snyk 走 &lt;em>developer-first SAST + SCA + container&lt;/em>、Wiz 走 &lt;em>cloud posture + agentless workload scan&lt;/em>、兩者場景互補不替代 — 多數客戶 Snyk 管 left-shift dev 階段、Wiz 管 runtime cloud。&lt;/p></description><content:encoded><![CDATA[<p>Wiz 是 <em>agentless CNAPP</em>（Cloud-Native Application Protection Platform）的代表、用 <em>cloud API + snapshot scan</em> 從外面看雲、不在 workload 上裝 agent。2020 年由前 Microsoft Cloud Security Group 創辦人成立、2024 估值約 $12B、是 CNAPP 賽道的後起黑馬。它跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 的差異在 <em>風險優先級的組合方式</em>、vulnerability 掃描能力本身都具備 — Wiz 用 <em>Security Graph + Toxic Combination</em> 把多個 low-risk finding 串成 <em>attack path</em>、而不是給你 10000 個獨立 CVE。</p>
<h2 id="服務定位">服務定位</h2>
<p>Wiz 的核心定位是 <em>agentless cloud posture + workload protection platform</em>、把 CSPM（Cloud Security Posture Management）/ CWP（Cloud Workload Protection）/ CIEM（Cloud Infrastructure Entitlement Management）/ KSPM（Kubernetes Security Posture Management）/ DSPM（Data Security Posture Management）整合在同一個 Security Graph 上面。底層是 <em>Connector</em>（讀 AWS / GCP / Azure / OCI / K8s 的 read-only API + snapshot scan）、頂層是 <em>Issues + Projects + Toxic Combination rules</em>。</p>
<p>跟 Prisma Cloud / Lacework 比、Wiz 走 <em>graph-first</em> — 把 resource / IAM / vulnerability / secret / network exposure 連成圖（跳過 finding list 層）、可以用 query 問「哪些 EC2 有 RCE CVE 且 IMDS v1 且能 assume 跨帳戶 admin role」。跟 CrowdStrike Falcon Cloud Security 比、Wiz 是 <em>agentless-first</em>（CWP 才用 sensor、posture / 漏洞掃描 0 agent）、Falcon CS 走 <em>endpoint agent 延伸到雲</em>。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> CSPM 比、Datadog 是 <em>observability platform 上的 security view</em>、Wiz 是 <em>security-first CNAPP</em>、Wiz 的 graph 跟 toxic combination 深度大幅領先、但獨立 SIEM / log 能力不如 Datadog / <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>。跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 比、Snyk 走 <em>developer-first SAST + SCA + container</em>、Wiz 走 <em>cloud posture + agentless workload scan</em>、兩者場景互補不替代 — 多數客戶 Snyk 管 left-shift dev 階段、Wiz 管 runtime cloud。</p>
<p>關鍵張力：<em>agentless + multi-cloud + Security Graph</em> ↔ <em>單一 workload count 計費 + 多模組組合容易踩 sticker shock</em>。Wiz 的價值前提是組織夠大、cloud account / workload 夠多到 <em>toxic combination</em> 比 <em>單點 CVE list</em> 更有意義；小型團隊 + 單一雲 + 預算敏感、用 Wiz 等於買保時捷送外賣。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Wiz 在 cloud security stack 中承擔哪一段（CSPM / CWP / CIEM / KSPM / DSPM / Wiz Code）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 等 SIEM 接 Issues、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 是否要保留 dev-time scan）</li>
<li>Security Graph query 跟 Toxic Combination rule 的 ownership 設計（誰寫 rule、誰 triage Issue、誰調 Project scope）</li>
<li>Agentless scan 的可見性邊界（snapshot 能看到 / 看不到什麼、需不需要 Wiz Sensor / Defend 補 runtime）</li>
<li>何時用 Wiz、何時走 Prisma Cloud / Lacework / CrowdStrike Falcon CS / Datadog CSPM 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Wiz deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Connector 覆蓋率</strong>：所有 prod cloud account（AWS / GCP / Azure / OCI）跟 K8s cluster 是否都接上、IAM role 是否最小權限（Wiz 給的 CloudFormation / Terraform template 不要自己加權限）、snapshot scan 是否涵蓋所有 region / disk type</li>
<li><strong>Toxic Combination rule 設計</strong>：是不是只開預設 rule、有沒有針對自家環境 anti-pattern 寫 custom rule（例如 <em>cross-account assume + payment service + secret access</em>）、rule 走不走 PR review</li>
<li><strong>Issue triage SLA</strong>：critical / high Issue 的 mean-time-to-resolve、是否跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Jira / Slack 整合、Project scope 是否依 service owner 切（不是丟整包給 SecOps）</li>
<li><strong>Wiz Code / Wiz Defend coverage 邊界</strong>：IaC scan 跟 dev-time CI 是 Wiz Code 還是 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>、runtime detection 是 Wiz Defend 還是 Falco / CrowdStrike、不要兩邊都裝又都沒人 triage</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Connector 跟 agentless scan</strong>：Wiz 透過 <em>Connector</em>（每個 cloud account 一個 IAM role）讀 cloud control plane API、定期 snapshot EBS / Persistent Disk / Managed Disk、在 Wiz 自家環境裡 mount snapshot 做 vulnerability / secret / malware scan。對 workload 0 影響、不需要在 EC2 / GKE node / VM 裡裝任何 agent。代價是 <em>runtime 行為看不到</em>（process / network connection / syscall）— 那段要 Wiz Defend / Wiz Sensor 或外接 Falco / CrowdStrike。</p>
<p><strong>Security Graph</strong>：Wiz 把所有 resource（compute / storage / IAM principal / network / secret / vulnerability finding）建成 graph、用 GraphQL-like query 跨類型查詢。Security Graph 是 first-class concept、不只是 visualization — Toxic Combination rule、Issue correlation、blast radius 估算都走 graph。寫 SPL / KQL 跟寫 Wiz query 的 mindset 不一樣 — Wiz query 是 <em>relationship-first</em>（從 resource A 走幾跳到 resource B）、SPL 是 <em>event-first</em>（時間序列上的 log）。</p>
<p><strong>Toxic Combination</strong>：CNAPP vs 傳統 vulnerability scanner 的根本差異。單一 finding 是 low risk（一個 CVE / 一條 over-permission / 一個 public S3）、組合起來是 critical attack path（<em>public-facing EC2 + RCE CVE + IMDS v1 + assume admin role + 觸碰 customer PII bucket</em>）。Wiz 預設帶幾十條 toxic combination rule（attack-path-style）、organization 應該加自家 anti-pattern。對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> 跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.5 應用層風險</a> 的跨 control 整合。</p>
<p><strong>Issues + Projects</strong>：finding 進 Wiz 後變 <em>Issue</em>、按 <em>Project</em> 路由 — Project 是邏輯切分（按 BU / service / 環境）、每個 Project 有 owner、Issue 自動分派。反例是 <em>單一 default Project 收所有 Issue</em>、SecOps 一天看 5000 個 Issue 看不完、跟 <a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality</a> 同樣模式。production 要 Project scope 對齊 service ownership、跟 Jira / Slack / <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 整合自動建 ticket。</p>
<p><strong>Wiz Code</strong>：dev-time / IaC scan、覆蓋 Terraform / CloudFormation / K8s manifest / Helm + container image build-time scan + SCA、跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> Code/IaC 跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> Config 重疊。多數客戶選一邊用、不會雙開。Wiz Code 的賣點是 <em>跟 runtime Wiz finding 同一個 graph</em> — 在 IDE / PR 階段就能看到「這條 IaC 改動會在 prod 產生哪條 toxic combination」。</p>
<p><strong>Wiz Defend (Gem)</strong>：2024 收購 Gem Security 整合 runtime detection / cloud detection、補 Wiz 早期缺的 runtime 層。Wiz Defend 走 <em>cloud-native log + Wiz Sensor</em>（K8s eBPF sensor）混合、跟 CrowdStrike Falcon EDR / Falco 競爭。產品成熟度仍在跟進、2024-2025 才大量 GA、不要假設它已經是 CrowdStrike / SentinelOne 等級的 EDR 替代品。</p>
<p><strong>Wiz Sensor</strong>：K8s admission controller + eBPF runtime sensor、補 agentless 看不到的 runtime 行為（container process / network connection / file integrity）。是 <em>選配</em>、不裝 Wiz 仍能做 posture / vulnerability scan、裝了才有 runtime detection。資源開銷比 Falco 大、跟 CrowdStrike Falcon container sensor 競爭。</p>
<p><strong>SIEM 整合</strong>：Wiz Issues / Detections 可推到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 走 SOAR playbook。常見 pattern：Wiz 偵測到 toxic combination → 推 Issue 到 Splunk → SOAR playbook 自動 isolate workload 或 rotate credential、走 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> API。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Wiz</th>
          <th>Prisma Cloud (Palo Alto)</th>
          <th>Lacework</th>
          <th>CrowdStrike Falcon CS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>Agentless-first（snapshot scan）+ 選配 Sensor</td>
          <td>Agent + agentless 混合</td>
          <td>Agent + agentless 混合、Polygraph behavior-base</td>
          <td>Agent-first（Falcon sensor 延伸）</td>
      </tr>
      <tr>
          <td>核心 concept</td>
          <td>Security Graph + Toxic Combination</td>
          <td>Cloud Security 套件（CSPM/CWP/CIEM/DSPM)</td>
          <td>Polygraph（ML behavior model）</td>
          <td>Falcon platform（EDR + cloud workload）</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>Per workload + module</td>
          <td>Credit-based（modular）</td>
          <td>Per workload + data ingestion</td>
          <td>Per endpoint + module</td>
      </tr>
      <tr>
          <td>Multi-cloud</td>
          <td>強（AWS/GCP/Azure/OCI/K8s）</td>
          <td>強</td>
          <td>強</td>
          <td>強（但 Falcon-first 文化）</td>
      </tr>
      <tr>
          <td>Runtime 偵測</td>
          <td>Wiz Defend（2024+、成熟度仍在跟進）</td>
          <td>Prisma Cloud Defender（成熟）</td>
          <td>Polygraph 行為偵測（成熟）</td>
          <td>業界最強（EDR 出身）</td>
      </tr>
      <tr>
          <td>Developer 整合</td>
          <td>Wiz Code（IaC/SCA/PR scan）</td>
          <td>Prisma Cloud Code Security</td>
          <td>弱</td>
          <td>弱</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — Graph query 是新語法但結構清楚</td>
          <td>陡 — 模組多、UX 較重</td>
          <td>中</td>
          <td>中 — Falcon UI 一致</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Multi-cloud + 大型 org + 看重 attack path</td>
          <td>已用 Palo Alto 生態、需要 NGFW 整合</td>
          <td>ML-first 偵測、不想自己寫 rule</td>
          <td>已用 Falcon EDR、想擴到 cloud workload</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — Graph query / Toxic Combination 量大</td>
          <td>高 — 跟 Palo Alto 生態耦合</td>
          <td>中</td>
          <td>高 — Falcon sensor 已大規模部署很難換</td>
      </tr>
  </tbody>
</table>
<p>選 Wiz 的核心訴求：<em>multi-cloud + 中大型組織 + 願意接受 agentless 的 runtime 邊界 + 重視 toxic combination 的優先級</em>。如果組織已重度使用 CrowdStrike Falcon EDR、走 Falcon CS 延伸更一致；如果已重度 Palo Alto、走 Prisma Cloud 整合更深。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Security Graph query language</strong>：類 GraphQL 的 query syntax、可以寫「找所有 public-facing EC2、有 CVE-2024-XXX、能 assume role 到 admin account、且該 role 可讀 prod-pii bucket」這種 5-hop query。production 用法：把高頻 query 存成 <em>Saved Query</em> + alert、把 attack pattern 寫成 <em>Toxic Combination rule</em>。Graph query 寫得好不好直接決定 <em>attack path 是否被涵蓋</em>、跟 SPL 寫 correlation rule 是同一個 ownership 議題。</p>
<p><strong>Toxic Combination 設計</strong>：預設 rule 是 <em>generic 雲安全 anti-pattern</em>（public + vulnerable + over-permission）、organization 應該補 <em>industry-specific</em> 跟 <em>organization-specific</em> anti-pattern — 金融業要看「payment workload + cross-region replication + non-encrypted snapshot」、SaaS 多租戶要看「tenant-A workload + assume tenant-B role + 跨 tenant data access」。Toxic combination rule 走 PR review + staging tenant 驗證、跟 <a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a> 同流程。</p>
<p><strong>Wiz AI (2024+)</strong>：LLM-assisted investigation — 用自然語言查 graph（「show me all critical issues touching prod payment service」翻譯成 graph query）、Issue triage 自動 summarize attack path、根因建議。實務上是 query 翻譯 + summarize、不是替代 analyst 判讀；高 stake 決策仍要人類 review。</p>
<p><strong>Agentless secret scan</strong>：Wiz snapshot scan disk 時也掃 hardcoded secret（AWS access key / API token / private key）、跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> 整合做 rotation 路由。對應 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a> 的偵測層。</p>
<p><strong>Sigstore / SBOM 整合</strong>：Wiz 可消費 SBOM（<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a> 或 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 產出）+ verify Cosign / Sigstore 簽章、把 <em>artifact trust</em> 接進 Security Graph。對應 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與工件信任</a> 的 build-to-runtime 證據鏈。</p>
<p><strong>Wiz for AI</strong>：2024+ 新 module、針對 AI workload（LLM model storage / training dataset / inference endpoint）做 posture scan、找 misconfigured model bucket / exposed inference endpoint / training data leak。早期產品、定位是 <em>AI workload 的 CSPM 延伸</em>、不是替代 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">AI red team / prompt injection 偵測</a> 工具。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Issue 爆炸 / 沒人 triage</strong>：default Project 收所有 finding、沒對齊 service ownership — 切 Project 給每個 BU / service、autoclose 已知 accepted risk、跟 Jira / Slack 整合自動分派</li>
<li><strong>Toxic Combination 沒命中真實 incident</strong>：只開預設 rule、沒寫 organization-specific rule — 從 <a href="/blog/backend/07-security-data-protection/red-team/" data-link-title="7.1 攻擊者視角（紅隊）與攻擊面驗證" data-link-desc="從攻擊者角度盤點暴露面、邊界、濫用路徑與資料外洩風險">red team case 庫</a> 反推自家環境的 attack path、寫成 custom rule</li>
<li><strong>Snapshot scan 漏掉 ephemeral workload</strong>：scan 間隔 12-24hr、短命 Lambda / Fargate task 沒掃到 — 補 Wiz runtime sensor 或外接 Falco；ephemeral workload 改用 build-time scan（<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / Wiz Code）</li>
<li><strong>Connector IAM role 權限漂移</strong>：自己加了權限結果踩 over-permission — Connector role 用 Wiz 提供的 CloudFormation / Terraform template、走 <a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a> 版控、不手改</li>
<li><strong>Sticker shock / 計費爆炸</strong>：開了所有 module（CSPM + CWP + CIEM + KSPM + DSPM + Wiz Code + Defend）、workload count 暴衝 — 只開核心 module、ephemeral workload 走 sampling、enterprise contract 談 cap</li>
<li><strong>Wiz Code 跟 Snyk / Trivy 雙開</strong>：dev team 用 Snyk、SecOps 用 Wiz Code、PR 兩邊 finding 重複 — 選一邊做 dev-time gate、另一邊只當 visibility、不要兩邊都 block PR</li>
<li><strong>Wiz Defend 當 EDR 用結果偵測能力不夠</strong>：runtime detection 期待 CrowdStrike 等級 — Wiz Defend 仍在跟進、純 EDR 需求保留 CrowdStrike / SentinelOne、Wiz Defend 補 cloud context 層</li>
<li><strong>Audit log retention 不夠</strong>：Wiz 預設 audit retention 偏短、incident 回查時資料缺 — push Issue 跟 audit log 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog</a> 做長期保存</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已用 Palo Alto NGFW / Prisma 生態</td>
          <td>Prisma Cloud</td>
      </tr>
      <tr>
          <td>已用 CrowdStrike Falcon EDR</td>
          <td>Falcon Cloud Security</td>
      </tr>
      <tr>
          <td>ML-first 偵測 / 不想寫 rule</td>
          <td>Lacework</td>
      </tr>
      <tr>
          <td>小型 / 單一雲 / 預算敏感</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> + cloud-native scanner（AWS Inspector / GCP SCC）</td>
      </tr>
      <tr>
          <td>Developer-first SAST + SCA</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a></td>
      </tr>
      <tr>
          <td>Observability 已用 Datadog、不想再買 CNAPP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security CSPM</a></td>
      </tr>
      <tr>
          <td>SIEM / 跨 source correlation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>Runtime container 行為偵測 (OSS)</td>
          <td>Falco / Cilium Tetragon</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Security Graph query syntax 完整 reference 跟所有 built-in toxic combination rule 清單</li>
<li>Wiz Defend / Wiz Sensor 的 eBPF 內部實作細節</li>
<li>Wiz Code 跟 IDE plugin（VSCode / JetBrains）的具體設定</li>
<li>Cloud-native scanner（AWS Inspector / GCP Security Command Center / Azure Defender）的對照細節</li>
<li>Wiz API 的具體 SDK 用法跟 Terraform Provider 配置</li>
<li>CNAPP 市場的完整 vendor 比較（Gartner Magic Quadrant 等）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Wiz 在 07 案例庫沒有直接 vendor-level 事件、但多個 case 是 CNAPP 風險組合的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Wiz 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>Security Graph 可關聯「leaked credential + 過寬 IAM + 缺 MFA + 大量 data egress」四個 low-risk finding 成 toxic combination、提前 alert</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a></td>
          <td>Wiz 可掃 S3 bucket public exposure + sensitive data + IAM scope、發現 backup bucket 配置漂移、對應 DSPM 場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Agentless snapshot scan 在 Log4Shell 期間可秒級回答「哪些 prod workload 有 log4j-core vulnerable version」、不需 endpoint agent rollout</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>Wiz Code + Sigstore 整合可驗證 build artifact 來源、Security Graph 可串「signed artifact + 異常 runtime behavior」</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護 (section)</a></td>
          <td>Network exposure scan + IAM analysis 對應 section 原則、把「public + over-permission + sensitive」串成 toxic combination</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性 (section)</a></td>
          <td>Wiz Code IaC scan + image scan + SBOM 消費對應 build-to-runtime 證據鏈</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>、<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>（dev-first SAST/SCA）、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（OSS scanner）、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>（observability + CSPM）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（Issues → SIEM）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（SOAR 自動 rotation）、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a>（SBOM 接 Wiz Code）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>（CIEM 分析對象）、<a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform</a>（Connector / IaC 版控）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Toxic combination → IR routing）、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5 部署平台</a>（cloud account / K8s onboarding）</li>
<li>官方：<a href="https://docs.wiz.io/">Wiz Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C16 SeatGeek：DynamoDB + Lambda 打造的虛擬等候室</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/</guid><description>&lt;p>這個案例的核心責任是說明「flash-sale 場景下、限流如何明確設計」。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a> 的「DynamoDB 隱性緩衝」是姊妹案 — Tixcraft 用 DynamoDB 作為寫入緩衝吸收洪峰、SeatGeek 走更上游一層、在用戶到達系統前就明確排隊。兩種架構並存於票務業界、適合不同業務場景。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>SeatGeek Virtual Waiting Room 架構（引自 &lt;a href="https://aws.amazon.com/blogs/architecture/build-a-virtual-waiting-room-with-amazon-dynamodb-and-aws-lambda-at-seatgeek/">AWS Architecture Blog&lt;/a>）：&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>Protected Zone table&lt;/td>
 &lt;td>紀錄受保護資源的 metadata（哪個 event 受 waiting room 保護）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Counters table&lt;/td>
 &lt;td>紀錄「每分鐘發出多少 access token」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>User Connection table&lt;/td>
 &lt;td>紀錄訪客 token 與 WebSocket connection ID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Queue table&lt;/td>
 &lt;td>把訪客 token 對映到 access token（排隊序號）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bouncer Lambda&lt;/td>
 &lt;td>配發與失效 access token 的「守門員」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API Gateway&lt;/td>
 &lt;td>接受外部請求、轉發 Bouncer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>業務動機：取代「第三方 waiting room 服務」、原因是缺乏客製化（VIP 規則、優先級）跟 metrics 可見度。&lt;/p>
&lt;p>關鍵機制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Token = 庫存單位&lt;/strong>：access token 總數 = 可售票數量。沒拿到 token 的用戶被導到 waiting room 頁面、看到排隊位置與預估等待時間。&lt;/li>
&lt;li>&lt;strong>FIFO 或 priority queue&lt;/strong>：可以按進入順序、也可以對 VIP 客戶優先發 token。&lt;/li>
&lt;li>&lt;strong>Token 失效機制&lt;/strong>：用戶完成購票 / 主動退出時、token 釋放回 pool、給下一位等候用戶。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>SeatGeek 案例揭露三個明確限流設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>隱性緩衝 vs 明確排隊是兩種架構取捨&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">Tixcraft 模式&lt;/a>「全部塞進 DynamoDB」、用戶以為下單成功、實際處理排隊。SeatGeek 模式「明確告訴你排隊位置」、用戶看得到等待時間。前者犧牲透明度換流量吸收、後者犧牲流量吸收換體驗。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.10 Production-Side 驗證&lt;/a> 的用戶體驗 vs 系統行為取捨。&lt;/li>
&lt;li>&lt;strong>WebSocket connection 是 stateful 容量單位&lt;/strong>：100 萬個 active waiting room 用戶 = 100 萬個 WebSocket connection、每個 connection 都吃記憶體跟 file descriptor。Lambda 沒辦法保持 WebSocket、需要 API Gateway WebSocket API 或 AppSync 配合。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 stateful service 容量規劃。&lt;/li>
&lt;li>&lt;strong>限流粒度 = 業務粒度&lt;/strong>：「每分鐘發 N 個 token」這個參數直接決定「每分鐘成交 N 張票」。N 太小、賣不完；N 太大、後端撐不住。N 不是技術參數、是業務 × 後端容量的協商結果。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 把容量規劃跟業務 KPI 對接。&lt;/li>
&lt;/ol>
&lt;p>需要警惕的判讀盲點：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「flash-sale 場景下、限流如何明確設計」。跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的「DynamoDB 隱性緩衝」是姊妹案 — Tixcraft 用 DynamoDB 作為寫入緩衝吸收洪峰、SeatGeek 走更上游一層、在用戶到達系統前就明確排隊。兩種架構並存於票務業界、適合不同業務場景。</p>
<h2 id="觀察">觀察</h2>
<p>SeatGeek Virtual Waiting Room 架構（引自 <a href="https://aws.amazon.com/blogs/architecture/build-a-virtual-waiting-room-with-amazon-dynamodb-and-aws-lambda-at-seatgeek/">AWS Architecture Blog</a>）：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Protected Zone table</td>
          <td>紀錄受保護資源的 metadata（哪個 event 受 waiting room 保護）</td>
      </tr>
      <tr>
          <td>Counters table</td>
          <td>紀錄「每分鐘發出多少 access token」</td>
      </tr>
      <tr>
          <td>User Connection table</td>
          <td>紀錄訪客 token 與 WebSocket connection ID</td>
      </tr>
      <tr>
          <td>Queue table</td>
          <td>把訪客 token 對映到 access token（排隊序號）</td>
      </tr>
      <tr>
          <td>Bouncer Lambda</td>
          <td>配發與失效 access token 的「守門員」</td>
      </tr>
      <tr>
          <td>API Gateway</td>
          <td>接受外部請求、轉發 Bouncer</td>
      </tr>
  </tbody>
</table>
<p>業務動機：取代「第三方 waiting room 服務」、原因是缺乏客製化（VIP 規則、優先級）跟 metrics 可見度。</p>
<p>關鍵機制：</p>
<ol>
<li><strong>Token = 庫存單位</strong>：access token 總數 = 可售票數量。沒拿到 token 的用戶被導到 waiting room 頁面、看到排隊位置與預估等待時間。</li>
<li><strong>FIFO 或 priority queue</strong>：可以按進入順序、也可以對 VIP 客戶優先發 token。</li>
<li><strong>Token 失效機制</strong>：用戶完成購票 / 主動退出時、token 釋放回 pool、給下一位等候用戶。</li>
</ol>
<h2 id="判讀">判讀</h2>
<p>SeatGeek 案例揭露三個明確限流設計重點。</p>
<ol>
<li><strong>隱性緩衝 vs 明確排隊是兩種架構取捨</strong>：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 模式</a>「全部塞進 DynamoDB」、用戶以為下單成功、實際處理排隊。SeatGeek 模式「明確告訴你排隊位置」、用戶看得到等待時間。前者犧牲透明度換流量吸收、後者犧牲流量吸收換體驗。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.10 Production-Side 驗證</a> 的用戶體驗 vs 系統行為取捨。</li>
<li><strong>WebSocket connection 是 stateful 容量單位</strong>：100 萬個 active waiting room 用戶 = 100 萬個 WebSocket connection、每個 connection 都吃記憶體跟 file descriptor。Lambda 沒辦法保持 WebSocket、需要 API Gateway WebSocket API 或 AppSync 配合。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 stateful service 容量規劃。</li>
<li><strong>限流粒度 = 業務粒度</strong>：「每分鐘發 N 個 token」這個參數直接決定「每分鐘成交 N 張票」。N 太小、賣不完；N 太大、後端撐不住。N 不是技術參數、是業務 × 後端容量的協商結果。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 把容量規劃跟業務 KPI 對接。</li>
</ol>
<p>需要警惕的判讀盲點：</p>
<ul>
<li>AWS Architecture Blog 沒提具體流量數字（concurrent users、queue depth、throughput）。讀者無法直接套用到自家容量規劃、必須自己壓測。</li>
<li>DynamoDB 4 張表的設計 <em>看似簡單</em>、實際上每張表的 partition key / sort key 設計都要仔細想。複製這個架構不等於拿到 SeatGeek 的吞吐能力。</li>
<li>「token expiration」機制如果設計不好（例如用戶關閉瀏覽器、token 沒回收）、會導致「排隊很長但實際空著」、影響轉換率。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>明確 vs 隱性限流的選擇</strong>：高價值門票（演唱會、限量周邊）適合明確排隊（用戶願意等）；高頻低價值商品（FCFS 折扣）適合隱性緩衝（讓用戶快速完成）。</li>
<li><strong>Virtual Waiting Room 是 stateful service、要規劃連線容量</strong>：不是 stateless Lambda 一招到底、需要 WebSocket gateway + DynamoDB state store。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的混合架構。</li>
<li><strong>token 過期策略要寫進設計初稿</strong>：用戶離開、付款超時、瀏覽器當掉 — 三種狀況的 token 回收邏輯都不一樣、要明確設計。</li>
<li><strong>可觀測性是「自建 waiting room」勝過「第三方」的關鍵</strong>：SeatGeek 換掉第三方就是要 metrics 可見、知道每分鐘 token issue rate、queue depth distribution、token expiration rate、conversion funnel。對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>。</li>
</ol>
<p>跨平台等效：GCP Cloud Functions + Firestore + Pub/Sub；Azure Functions + Cosmos DB + SignalR；自建 Redis（INCR / TTL）+ WebSocket gateway（Soketi / Socket.IO + Redis adapter）都可以實作對等架構。AWS 還推出官方 <a href="https://aws.amazon.com/solutions/implementations/virtual-waiting-room-on-aws/">Virtual Waiting Room on AWS</a> Solutions、是 SeatGeek 模式的可重用版本。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計明確排隊限流 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a></li>
<li>對照隱性緩衝模式 → <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></li>
<li>想做 conversion funnel 可觀測性 → <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> + <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a></li>
<li>想了解 stateful service 容量規劃 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/architecture/build-a-virtual-waiting-room-with-amazon-dynamodb-and-aws-lambda-at-seatgeek/">Build a Virtual Waiting Room with Amazon DynamoDB and AWS Lambda at SeatGeek</a></li>
<li><a href="https://aws.amazon.com/solutions/implementations/virtual-waiting-room-on-aws/">Virtual Waiting Room on AWS (Solutions)</a></li>
<li><a href="https://aws.amazon.com/blogs/apn/how-to-manage-peak-traffic-on-aws-using-queue-its-virtual-waiting-room/">How to manage peak traffic on AWS using Queue-it&rsquo;s virtual waiting room</a></li>
</ul>
]]></content:encoded></item><item><title>4.16 Observability Readiness Review</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>readiness review 的責任：在 production 前確認訊號能支援分級、定位、回復與復盤&lt;/li>
&lt;li>檢查面向：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>&lt;/li>
&lt;li>上線前判準：核心 user journey 是否有 SLI、錯誤是否有 correlation key、依賴是否可追蹤&lt;/li>
&lt;li>變更前判準：新依賴、新 queue、新 feature flag 是否帶出新訊號需求&lt;/li>
&lt;li>演練前判準：game day / chaos / DR drill 是否能被 04 訊號觀察&lt;/li>
&lt;li>跟 06 的交接：readiness 缺口進入 reliability readiness / release gate&lt;/li>
&lt;li>跟 08 的交接：readiness 缺口影響 severity trigger、runbook 與 decision log&lt;/li>
&lt;li>反模式：服務先上線、事故後才補 dashboard；alert 有通知但缺定位欄位；trace 需要人工對回 log&lt;/li>
&lt;/ul>
&lt;p>Observability readiness review 的價值在於把「事故時才會被問到的問題」提前成上線條件。服務進 production 前，團隊需要先確認訊號能回答三件事：哪裡出問題、影響到誰、下一步由誰處理。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability readiness review 是把「訊號是否足以支援操作」變成上線前檢查的流程，責任是讓服務進入 production 前已具備基本診斷能力。&lt;/p>
&lt;p>這一頁處理的是準備度。工具已存在時，仍需要確認訊號是否對應使用者旅程、依賴邊界、事故分級與復盤證據。&lt;/p>
&lt;p>readiness review 不等於打勾清單。它是一次跨角色對齊：服務團隊確認事件語意，平台團隊確認採集與查詢路徑，on-call 確認事故前 10 分鐘真的能定位。三者同時成立，才算可操作準備度。&lt;/p>
&lt;h2 id="適用情境">適用情境&lt;/h2>
&lt;p>Observability readiness review 適合放在服務生命週期的高風險節點。這些節點共同特徵是：一旦變更進入 production，第一次異常就會依賴既有訊號做判讀。&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>新服務上線&lt;/td>
 &lt;td>核心旅程、依賴、owner 是否可觀測&lt;/td>
 &lt;td>事故初期只能靠人工猜測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重大變更&lt;/td>
 &lt;td>新 queue、新依賴、新 flag 的訊號&lt;/td>
 &lt;td>新風險進 production 後才暴露&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>架構拆分&lt;/td>
 &lt;td>trace、correlation、service name&lt;/td>
 &lt;td>事件鏈跨服務後斷裂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>演練前&lt;/td>
 &lt;td>chaos、load、DR 行為是否可被看見&lt;/td>
 &lt;td>演練結果缺少可驗證證據&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>新服務上線時，readiness review 的責任是確認基本診斷能力已經存在。典型服務至少要能從 request、tenant、region、dependency 與錯誤分類回到同一條事件鏈，讓 on-call 能在前 10 分鐘判斷影響範圍。&lt;/p>
&lt;p>重大變更時，readiness review 的責任是確認變更帶來的新風險已有訊號。加入新的外部 API、queue、background job、feature flag 或資料同步流程，都會增加新的失效面；每個失效面都應有對應 log、metric、trace 或 alert。&lt;/p>
&lt;p>演練前，readiness review 的責任是確認驗證行為能被觀測。chaos experiment、load test 或 DR drill 需要同時產生故障與判讀證據，讓團隊能確認 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與回復狀態。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 observability readiness 時，先看服務的核心旅程是否有訊號，再看事故時能否從症狀走到原因。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>核心 user journey 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO&lt;/a> 與 error rate&lt;/li>
&lt;li>log 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 與 tenant 欄位&lt;/li>
&lt;li>trace 是否覆蓋同步、async、queue 與 background job 邊界&lt;/li>
&lt;li>dashboard 是否能支援 on-call 的前 10 分鐘判讀&lt;/li>
&lt;li>alert 是否能連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 owner&lt;/li>
&lt;/ul>
&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>事件關聯&lt;/td>
 &lt;td>request / trace / tenant 可串成同一條事件鏈&lt;/td>
 &lt;td>欄位命名不一致、跨服務拼接失敗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務健康&lt;/td>
 &lt;td>SLI 與 error rate 能反映核心旅程&lt;/td>
 &lt;td>指標只反映系統資源、不反映用戶結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路徑可視&lt;/td>
 &lt;td>trace 能覆蓋 sync + async + queue&lt;/td>
 &lt;td>background job 與 queue 邊界斷鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作入口&lt;/td>
 &lt;td>dashboard / alert 能支撐前 10 分鐘&lt;/td>
 &lt;td>告警有通知、沒有定位與下一步&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="review-流程">Review 流程&lt;/h2>
&lt;p>Readiness review 的流程是從使用者旅程走向操作路由。先從服務承諾的體驗開始，再反推工具與訊號清單，才能讓監控資產對應事故時的實際判讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>readiness review 的責任：在 production 前確認訊號能支援分級、定位、回復與復盤</li>
<li>檢查面向：<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a></li>
<li>上線前判準：核心 user journey 是否有 SLI、錯誤是否有 correlation key、依賴是否可追蹤</li>
<li>變更前判準：新依賴、新 queue、新 feature flag 是否帶出新訊號需求</li>
<li>演練前判準：game day / chaos / DR drill 是否能被 04 訊號觀察</li>
<li>跟 06 的交接：readiness 缺口進入 reliability readiness / release gate</li>
<li>跟 08 的交接：readiness 缺口影響 severity trigger、runbook 與 decision log</li>
<li>反模式：服務先上線、事故後才補 dashboard；alert 有通知但缺定位欄位；trace 需要人工對回 log</li>
</ul>
<p>Observability readiness review 的價值在於把「事故時才會被問到的問題」提前成上線條件。服務進 production 前，團隊需要先確認訊號能回答三件事：哪裡出問題、影響到誰、下一步由誰處理。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability readiness review 是把「訊號是否足以支援操作」變成上線前檢查的流程，責任是讓服務進入 production 前已具備基本診斷能力。</p>
<p>這一頁處理的是準備度。工具已存在時，仍需要確認訊號是否對應使用者旅程、依賴邊界、事故分級與復盤證據。</p>
<p>readiness review 不等於打勾清單。它是一次跨角色對齊：服務團隊確認事件語意，平台團隊確認採集與查詢路徑，on-call 確認事故前 10 分鐘真的能定位。三者同時成立，才算可操作準備度。</p>
<h2 id="適用情境">適用情境</h2>
<p>Observability readiness review 適合放在服務生命週期的高風險節點。這些節點共同特徵是：一旦變更進入 production，第一次異常就會依賴既有訊號做判讀。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>檢查重點</th>
          <th>缺口代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新服務上線</td>
          <td>核心旅程、依賴、owner 是否可觀測</td>
          <td>事故初期只能靠人工猜測</td>
      </tr>
      <tr>
          <td>重大變更</td>
          <td>新 queue、新依賴、新 flag 的訊號</td>
          <td>新風險進 production 後才暴露</td>
      </tr>
      <tr>
          <td>架構拆分</td>
          <td>trace、correlation、service name</td>
          <td>事件鏈跨服務後斷裂</td>
      </tr>
      <tr>
          <td>演練前</td>
          <td>chaos、load、DR 行為是否可被看見</td>
          <td>演練結果缺少可驗證證據</td>
      </tr>
      <tr>
          <td>事故後</td>
          <td>復盤缺口是否回寫成新訊號</td>
          <td>同類事故仍以相同盲區重演</td>
      </tr>
  </tbody>
</table>
<p>新服務上線時，readiness review 的責任是確認基本診斷能力已經存在。典型服務至少要能從 request、tenant、region、dependency 與錯誤分類回到同一條事件鏈，讓 on-call 能在前 10 分鐘判斷影響範圍。</p>
<p>重大變更時，readiness review 的責任是確認變更帶來的新風險已有訊號。加入新的外部 API、queue、background job、feature flag 或資料同步流程，都會增加新的失效面；每個失效面都應有對應 log、metric、trace 或 alert。</p>
<p>演練前，readiness review 的責任是確認驗證行為能被觀測。chaos experiment、load test 或 DR drill 需要同時產生故障與判讀證據，讓團隊能確認 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與回復狀態。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 observability readiness 時，先看服務的核心旅程是否有訊號，再看事故時能否從症狀走到原因。</p>
<p>重點訊號包括：</p>
<ul>
<li>核心 user journey 是否有 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a> 與 error rate</li>
<li>log 是否有 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 與 tenant 欄位</li>
<li>trace 是否覆蓋同步、async、queue 與 background job 邊界</li>
<li>dashboard 是否能支援 on-call 的前 10 分鐘判讀</li>
<li>alert 是否能連到 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 owner</li>
</ul>
<table>
  <thead>
      <tr>
          <th>檢查面向</th>
          <th>最小可用判準</th>
          <th>常見失真</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事件關聯</td>
          <td>request / trace / tenant 可串成同一條事件鏈</td>
          <td>欄位命名不一致、跨服務拼接失敗</td>
      </tr>
      <tr>
          <td>服務健康</td>
          <td>SLI 與 error rate 能反映核心旅程</td>
          <td>指標只反映系統資源、不反映用戶結果</td>
      </tr>
      <tr>
          <td>路徑可視</td>
          <td>trace 能覆蓋 sync + async + queue</td>
          <td>background job 與 queue 邊界斷鏈</td>
      </tr>
      <tr>
          <td>操作入口</td>
          <td>dashboard / alert 能支撐前 10 分鐘</td>
          <td>告警有通知、沒有定位與下一步</td>
      </tr>
  </tbody>
</table>
<h2 id="review-流程">Review 流程</h2>
<p>Readiness review 的流程是從使用者旅程走向操作路由。先從服務承諾的體驗開始，再反推工具與訊號清單，才能讓監控資產對應事故時的實際判讀。</p>
<ol>
<li>定義核心旅程與失敗後果。</li>
<li>對每個旅程列出依賴、async workflow 與資料寫入點。</li>
<li>為每個失效點指定 log、metric、trace 或 dashboard。</li>
<li>驗證 alert 是否連到 owner、runbook 與下一步動作。</li>
<li>標記尚未補齊的訊號缺口，決定是否阻擋上線或納入 follow-up。</li>
</ol>
<p>核心旅程是 readiness review 的錨點。購物服務的核心旅程可能是 checkout、payment、order confirmation；內容平台可能是 upload、publish、read path；B2B API 可能是 authentication、request processing、webhook delivery。訊號需要優先對到這些旅程，再補 CPU、memory 與 pod restart 等資源層訊號。</p>
<p>依賴圖是 readiness review 的第二層。每個資料庫、cache、broker、third-party API、object storage 與 internal service 都應能被定位為 upstream 或 downstream，並且在 trace、metric 或 log 中留下可查詢欄位。</p>
<p>操作路由是 readiness review 的交付物。當 alert 觸發時，on-call 需要知道先看哪個 dashboard、用哪個 query、找哪個 owner、用哪個 runbook、何時升級到 incident commander。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>服務上線 checklist 有監控項目，但沒有事故判讀欄位</li>
<li>新依賴上線後，dashboard 看不到 upstream / downstream 影響</li>
<li>alert 觸發後仍需要人工 grep 多個系統拼事件鏈</li>
<li>chaos 或 DR 演練產生故障，但 04 訊號沒有反映出預期現象</li>
<li>事故復盤 action item 反覆要求「補監控」</li>
</ul>
<p>在真實服務中，最常見的 readiness 缺口是工具已存在，但工具沒有對到決策。例如 alert 可以 page on-call，但查詢第一步就要跨三個系統手動對帳，代表 readiness 還停在可見層，尚未進入可操作層。</p>
<h2 id="控制面">控制面</h2>
<p>Readiness review 的控制面是把檢查結果轉成可執行決策。每個缺口都要被分類為阻擋、降級接受或後續改善，並且留下 owner 與期限。</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>判斷方式</th>
          <th>處理路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>阻擋</td>
          <td>影響核心旅程、事故時無替代判讀</td>
          <td>暫停上線，補 04 訊號或 06 readiness</td>
      </tr>
      <tr>
          <td>降級接受</td>
          <td>風險可被 runbook 或人工查證承接</td>
          <td>標記限制，接到 08 intake 與 decision log</td>
      </tr>
      <tr>
          <td>後續改善</td>
          <td>不影響首輪定位，但影響長期治理</td>
          <td>進入 04.8 signal governance loop</td>
      </tr>
      <tr>
          <td>淘汰整理</td>
          <td>舊 dashboard 或 alert 干擾判讀</td>
          <td>進入 4.18 operating model</td>
      </tr>
  </tbody>
</table>
<p>阻擋條件應該以「事故時是否能決策」為核心。核心旅程 SLI、request correlation、upstream / downstream 分辨能力與 alert owner 都是第一次事故能否被接住的基本條件。</p>
<p>降級接受需要明確寫出限制。若某個低流量背景任務暫時缺 trace，但有 log query、DLQ dashboard 與人工 replay 流程可以承接，團隊可以接受短期限制；限制需要進入 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，避免事中被誤讀為完整訊號。</p>
<p>後續改善適合處理長期品質問題。dashboard 可用但查詢成本過高、alert 可行但 noise 偏高、欄位命名需要統一，這些缺口適合進入 signal governance，讓上線決策與長期治理分流。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Observability readiness 的反模式通常來自把「有監控」誤當成「可操作」。監控存在只是起點，能支援判讀、路由與回復才是 readiness。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事後補 dashboard</td>
          <td>事故發生後才知道缺哪些面板</td>
          <td>把核心旅程面板列為上線條件</td>
      </tr>
      <tr>
          <td>告警只有通知</td>
          <td>on-call 收到 page 後仍需重新找證據</td>
          <td>alert 必須帶 owner 與 runbook</td>
      </tr>
      <tr>
          <td>trace 需要人工拼 log</td>
          <td>跨服務路徑靠 request id 手動對回</td>
          <td>統一 trace context 與 log 欄位</td>
      </tr>
      <tr>
          <td>readiness 只看平台工具</td>
          <td>平台 green，但服務旅程不可判讀</td>
          <td>從 user journey 反推訊號需求</td>
      </tr>
      <tr>
          <td>checklist 無阻擋條件</td>
          <td>每次都勾選通過，但缺口持續存在</td>
          <td>定義 block / accept / follow-up</td>
      </tr>
  </tbody>
</table>
<p>事後補 dashboard 的風險是把第一次事故變成探索行為。事故期間的主要工作應是止血與決策；如果團隊還在建立第一個查詢、猜欄位語意、找 owner，代表 readiness 沒有完成。</p>
<p>告警只有通知會把壓力丟給 on-call。有效 alert 應該同時提供症狀、範圍、第一個查詢入口與下一步路由，讓值班者能直接進入判讀流程。</p>
<h2 id="與-06-和-08-的關係">與 06 和 08 的關係</h2>
<p>Observability readiness 是可靠性驗證與事故處理的輸入層。06 需要用它判斷驗證前提是否成立，08 需要用它判斷事故 evidence 是否足以啟動流程。</p>
<p>在 06 中，readiness 缺口會影響 load test、chaos、DR drill 與 release gate。驗證行為需要可觀測訊號支撐，測試結果才足以證明系統維持在可接受狀態內。</p>
<p>在 08 中，readiness 缺口會影響 severity trigger、incident intake 與 decision log。若 evidence 不完整，事故指揮需要先標記資料限制，再決定是否升級、降級或等待更多證據。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.1 log schema：補事件關聯欄位</li>
<li>04.2 metrics：補服務健康與容量指標</li>
<li>04.3 tracing：補跨服務與 async context</li>
<li>04.4 dashboard / alert：補操作入口與通知條件</li>
<li><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5 威脅建模</a>：觀測盲區跟資料暴露的上線前檢查</li>
<li>06.19 reliability readiness：把觀測準備度納入上線前門檻</li>
<li>08.18 incident intake：把訊號接進事故 intake 與 evidence triage</li>
</ul>
]]></content:encoded></item><item><title>6.16 Test Data Management</title><link>https://tarrragon.github.io/blog/backend/06-reliability/test-data-management/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/test-data-management/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>測試常常失敗在資料而非邏輯 — fixture 過期、seed 跟 schema 漂移、staging 資料分佈跟 production 差太遠。Test data management 把 fixture、seed 與 production-like data 當成共用資產來治理，讓測試建立在可控且可重播的資料基礎上。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>Test data 的健康度先看資料是否足夠代表真實情境，再看資料是否能安全重建與清理。&lt;/p>
&lt;p>關鍵判準：&lt;/p>
&lt;ul>
&lt;li>fixture 是否覆蓋關鍵情境，而不是只有 happy path&lt;/li>
&lt;li>seed 是否可版本化與重播&lt;/li>
&lt;li>production-like data 是否完成去識別化與權限隔離&lt;/li>
&lt;li>data lifecycle 是否和 CI / migration / contract testing 互相對齊&lt;/li>
&lt;/ul>
&lt;h2 id="資料層次">資料層次&lt;/h2>
&lt;p>測試資料按用途分四層，每層的責任、治理成本與真實度不同。&lt;/p>
&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>Unit fixture&lt;/td>
 &lt;td>跟 test case 綁定&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integration seed&lt;/td>
 &lt;td>跟 test suite 綁定&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Staging dataset&lt;/td>
 &lt;td>長期存在於環境中&lt;/td>
 &lt;td>中高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production sample&lt;/td>
 &lt;td>定期從 prod 抽樣&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Unit fixture&lt;/strong> 是硬編碼或 factory-generated 的資料，不碰外部系統。fixture 的責任是提供可控的輸入與預期輸出，讓 unit test 驗證邏輯正確性。fixture 覆蓋 happy path 與 edge case，但不反映 production 資料分佈 — 這是設計取捨，因為分佈驗證的責任在更高層次。&lt;/p>
&lt;p>&lt;strong>Integration seed&lt;/strong> 寫進真實 DB / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> / cache，生命週期跟 test suite 綁定（setup 建立、teardown 清理）。seed 需要版本化，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a> 對齊 — 見下方「可重現性與版本化」段。seed 品質的判準是：它是否能讓 integration test 驗證跨服務邊界的行為，而不是只驗證資料是否存在。&lt;/p>
&lt;p>&lt;strong>Staging dataset&lt;/strong> 長期存在於 staging 環境，模擬 production 規模與分佈。這一層的挑戰是漂移：production 的資料結構、量體與分佈持續變化，staging dataset 需要定期更新才能維持代表性。更新頻率跟 schema 變更頻率對齊 — 每次重大 schema 變更後，staging dataset 應同步重建。&lt;/p>
&lt;p>&lt;strong>Production sample（脫敏）&lt;/strong> 從 production 抽樣加 PII masking，是真實度最高的選項。它的價值在於保留真實資料的分佈、關聯與邊界條件 — 這些是 synthetic data 很難完整模擬的。代價是隱私風險與合規成本，需要遮罩管線、存取控制與定期稽核。連到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護&lt;/a>。&lt;/p>
&lt;h2 id="遮罩與合成策略">遮罩與合成策略&lt;/h2>
&lt;p>當測試需要接近 production 的資料，PII 處理策略決定了安全性與真實度的平衡。&lt;/p>
&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>Tokenization&lt;/td>
 &lt;td>PII 替換成無意義 token、保留格式&lt;/td>
 &lt;td>需要 referential integrity&lt;/td>
 &lt;td>token mapping 本身需要安全儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Format-preserving encryption&lt;/td>
 &lt;td>保留原始格式但值不可逆&lt;/td>
 &lt;td>需要格式驗證（信用卡位數）&lt;/td>
 &lt;td>加密強度受格式限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Synthetic generation&lt;/td>
 &lt;td>用規則或統計模型生成假資料&lt;/td>
 &lt;td>無 PII 風險、合規最簡單&lt;/td>
 &lt;td>資料分佈可能偏移&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Tokenization 適合需要跨表關聯的場景：同一個 user ID 在 order、payment、session 表中需要一致替換，referential integrity 才不會被破壞。format-preserving encryption 適合需要通過格式驗證的場景（信用卡號通過 Luhn check）。synthetic generation 最安全，但資料分佈偏移會讓某些測試結論失真 — &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest 的快取可靠性案例&lt;/a>說明資料分佈差異會改變 cache 命中率，進而改變瓶頸位置。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>測試常常失敗在資料而非邏輯 — fixture 過期、seed 跟 schema 漂移、staging 資料分佈跟 production 差太遠。Test data management 把 fixture、seed 與 production-like data 當成共用資產來治理，讓測試建立在可控且可重播的資料基礎上。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Test data 的健康度先看資料是否足夠代表真實情境，再看資料是否能安全重建與清理。</p>
<p>關鍵判準：</p>
<ul>
<li>fixture 是否覆蓋關鍵情境，而不是只有 happy path</li>
<li>seed 是否可版本化與重播</li>
<li>production-like data 是否完成去識別化與權限隔離</li>
<li>data lifecycle 是否和 CI / migration / contract testing 互相對齊</li>
</ul>
<h2 id="資料層次">資料層次</h2>
<p>測試資料按用途分四層，每層的責任、治理成本與真實度不同。</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>生命週期</th>
          <th>真實度</th>
          <th>治理成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Unit fixture</td>
          <td>跟 test case 綁定</td>
          <td>低</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Integration seed</td>
          <td>跟 test suite 綁定</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Staging dataset</td>
          <td>長期存在於環境中</td>
          <td>中高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Production sample</td>
          <td>定期從 prod 抽樣</td>
          <td>高</td>
          <td>最高</td>
      </tr>
  </tbody>
</table>
<p><strong>Unit fixture</strong> 是硬編碼或 factory-generated 的資料，不碰外部系統。fixture 的責任是提供可控的輸入與預期輸出，讓 unit test 驗證邏輯正確性。fixture 覆蓋 happy path 與 edge case，但不反映 production 資料分佈 — 這是設計取捨，因為分佈驗證的責任在更高層次。</p>
<p><strong>Integration seed</strong> 寫進真實 DB / <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> / cache，生命週期跟 test suite 綁定（setup 建立、teardown 清理）。seed 需要版本化，跟 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a> 對齊 — 見下方「可重現性與版本化」段。seed 品質的判準是：它是否能讓 integration test 驗證跨服務邊界的行為，而不是只驗證資料是否存在。</p>
<p><strong>Staging dataset</strong> 長期存在於 staging 環境，模擬 production 規模與分佈。這一層的挑戰是漂移：production 的資料結構、量體與分佈持續變化，staging dataset 需要定期更新才能維持代表性。更新頻率跟 schema 變更頻率對齊 — 每次重大 schema 變更後，staging dataset 應同步重建。</p>
<p><strong>Production sample（脫敏）</strong> 從 production 抽樣加 PII masking，是真實度最高的選項。它的價值在於保留真實資料的分佈、關聯與邊界條件 — 這些是 synthetic data 很難完整模擬的。代價是隱私風險與合規成本，需要遮罩管線、存取控制與定期稽核。連到 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護</a>。</p>
<h2 id="遮罩與合成策略">遮罩與合成策略</h2>
<p>當測試需要接近 production 的資料，PII 處理策略決定了安全性與真實度的平衡。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>原理</th>
          <th>適用場景</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tokenization</td>
          <td>PII 替換成無意義 token、保留格式</td>
          <td>需要 referential integrity</td>
          <td>token mapping 本身需要安全儲存</td>
      </tr>
      <tr>
          <td>Format-preserving encryption</td>
          <td>保留原始格式但值不可逆</td>
          <td>需要格式驗證（信用卡位數）</td>
          <td>加密強度受格式限制</td>
      </tr>
      <tr>
          <td>Synthetic generation</td>
          <td>用規則或統計模型生成假資料</td>
          <td>無 PII 風險、合規最簡單</td>
          <td>資料分佈可能偏移</td>
      </tr>
  </tbody>
</table>
<p>Tokenization 適合需要跨表關聯的場景：同一個 user ID 在 order、payment、session 表中需要一致替換，referential integrity 才不會被破壞。format-preserving encryption 適合需要通過格式驗證的場景（信用卡號通過 Luhn check）。synthetic generation 最安全，但資料分佈偏移會讓某些測試結論失真 — <a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest 的快取可靠性案例</a>說明資料分佈差異會改變 cache 命中率，進而改變瓶頸位置。</p>
<p>三者的選擇取決於測試需要的真實度與隱私風險。多數團隊會混合使用：unit fixture 用 synthetic、integration seed 用 tokenization、staging dataset 用 production sample + format-preserving encryption。</p>
<h2 id="可重現性與版本化">可重現性與版本化</h2>
<p>Seed 資料需要版本化，跟 schema migration 對齊。當 DB schema 新增欄位或改型別，既有 seed 如果沒同步更新，integration test 會因資料問題失敗而非邏輯問題 — 這類 failure 的除錯成本高，因為錯誤訊息指向 schema 不符，團隊會懷疑是 migration bug 還是 seed bug。</p>
<p><strong>Seed migration</strong> 是把 seed 更新綁進 schema migration workflow 的做法：每次 DB migration 加一份對應的 seed migration。這讓 seed 狀態跟 schema 狀態同步演進，CI 跑 integration test 時永遠拿到匹配的組合。</p>
<p><strong>Fixture factory</strong> 用 factory pattern 生成測試資料，讓新增欄位自動帶 default。factory 的優勢是欄位變更只需改 factory 定義，不需要手動更新每個 fixture file — 這在高頻 schema 變更的服務中可以顯著降低 fixture 維護負擔。</p>
<p><strong>資料清理</strong> 策略決定 integration test 的隔離性。transaction rollback 最乾淨（每個 test case 跑在 transaction 內、結束後 rollback），但不適用於跨 transaction 的流程測試。truncate 較快但需要處理外鍵順序。獨立 DB per suite 隔離最強但成本最高 — 每個 test suite 用自己的 database instance。選擇時對齊 CI 的隔離需求（連到 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a> 的 environment 隔離段）。</p>
<h2 id="fixture-與-contract-testing-的整合">Fixture 與 contract testing 的整合</h2>
<p><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Contract testing</a> 定義 schema shape，fixture factory 可以用 contract 作為資料生成的來源。當 contract 變更時（新增欄位、型別調整），fixture factory 自動更新生成邏輯，讓 test data 跟 contract 保持同步。</p>
<p>這個整合的價值是把「契約變更是否影響測試資料」從人工 review 變成自動化流程。<a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe 的交易正確性實踐</a>對此有額外要求：交易路徑的 test data 需要能重播到相同狀態，確保 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 驗證的資料基礎一致。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">Pinterest</a>：資料分佈差異改變 cache 命中率與瓶頸位置，staging dataset 若分佈偏離 production，壓測結論會失真。</li>
<li><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe</a>：交易資料需要嚴格控制可重播性，fixture 與 seed 要能產出一致的 idempotency 驗證結果。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工程師為 debug 把 production data 拷到 local</td>
          <td>PII 暴露風險 — 需要遮罩管線而非直接複製</td>
          <td>建立遮罩 pipeline、禁止直接複製 production DB</td>
      </tr>
      <tr>
          <td>staging DB 含真實用戶 PII</td>
          <td>合規風險 — 需要用 tokenization 或 synthetic 替代</td>
          <td>導入 tokenization 工具或 synthetic generation</td>
      </tr>
      <tr>
          <td>fixture 跟 schema 漂移、測試常壞</td>
          <td>seed migration 未跟 schema migration 對齊</td>
          <td>每次 schema migration 同步更新 seed 版本</td>
      </tr>
      <tr>
          <td>新測試靠拷貼舊 fixture</td>
          <td>缺少 fixture factory — 變更範圍模糊、維護成本累積</td>
          <td>導入 factory pattern 自動帶 default</td>
      </tr>
      <tr>
          <td>production bug 重現不出</td>
          <td>staging dataset 分佈跟 production 差異太大 — 需更新或用 production sample</td>
          <td>定期用脫敏 production sample 更新 staging data</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：test data 如何進入 fast / slow stage</li>
<li><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a>：contract 定義 fixture shape</li>
<li><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>：seed migration 跟 schema migration 對齊</li>
<li><a href="/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 environment parity</a>：production-like data 是 parity 的一部分</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護</a>：PII 遮罩與最小揭露</li>
</ul>
]]></content:encoded></item><item><title>8.16 Runbook Lifecycle 管理</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/runbook-lifecycle/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/runbook-lifecycle/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>runbook 是會腐敗的資產：架構變更、依賴更新、人員流動都讓 runbook 失效&lt;/li>
&lt;li>runbook 生命週期：建立 → 演練 → 修訂 → 淘汰&lt;/li>
&lt;li>有效性驗證：演練時實際跑、不是讀&lt;/li>
&lt;li>版本對應：runbook 對應的服務版本、依賴版本&lt;/li>
&lt;li>過期偵測：上次演練時間、上次修訂時間、上次成功使用時間&lt;/li>
&lt;li>runbook 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 的整合：每次事故後檢視 runbook&lt;/li>
&lt;li>runbook 跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">drills&lt;/a> 的整合：演練是有效性的證明&lt;/li>
&lt;li>反模式：runbook 寫了沒人演練；事故時發現 runbook 步驟跟現實不符；runbook 無 owner、無修訂時間戳&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Runbook lifecycle 管理是把 runbook 當成會老化的工程 artifact 來治理，責任是讓文件內容持續對齊服務現況與事故實務。&lt;/p>
&lt;p>這一頁處理的是文件壽命。沒有 lifecycle，runbook 很快會變成看起來完整、實際失效的紙上流程。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 runbook 時，先看是否有使用與演練記錄，再看是否有明確淘汰條件。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>runbook 是否有 owner、版本與修訂時間&lt;/li>
&lt;li>是否有演練證明其可執行性&lt;/li>
&lt;li>過期或無法使用的 runbook 是否有淘汰流程&lt;/li>
&lt;li>每次事故後是否回寫修訂&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">Atlassian&lt;/a>：協作工具事故很依賴 runbook 的版本同步。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub&lt;/a>：平台型服務的 runbook 常要跟著架構快速更新。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack&lt;/a>：通訊平台的 runbook 若過期，事故時會直接放大混亂。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：事故後 runbook 修訂&lt;/li>
&lt;li>08.6 drills：runbook 演練驗證&lt;/li>
&lt;li>08.13 repeated：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 後 runbook 退場&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>事故時 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 找出 runbook、發現步驟過期&lt;/li>
&lt;li>runbook 上次修訂時間 &amp;gt; 12 個月、依賴的服務早已換版本&lt;/li>
&lt;li>新 oncall 找不到「該事故對應的 runbook」&lt;/li>
&lt;li>runbook 數量只增不減、無淘汰流程&lt;/li>
&lt;li>runbook 質量靠 author 個人風格、無模板&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：事故後 runbook 修訂&lt;/li>
&lt;li>08.6 drills：runbook 演練驗證&lt;/li>
&lt;li>08.13 repeated：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil&lt;/a> 後 runbook 退場&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>runbook 是會腐敗的資產：架構變更、依賴更新、人員流動都讓 runbook 失效</li>
<li>runbook 生命週期：建立 → 演練 → 修訂 → 淘汰</li>
<li>有效性驗證：演練時實際跑、不是讀</li>
<li>版本對應：runbook 對應的服務版本、依賴版本</li>
<li>過期偵測：上次演練時間、上次修訂時間、上次成功使用時間</li>
<li>runbook 跟 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 的整合：每次事故後檢視 runbook</li>
<li>runbook 跟 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">drills</a> 的整合：演練是有效性的證明</li>
<li>反模式：runbook 寫了沒人演練；事故時發現 runbook 步驟跟現實不符；runbook 無 owner、無修訂時間戳</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Runbook lifecycle 管理是把 runbook 當成會老化的工程 artifact 來治理，責任是讓文件內容持續對齊服務現況與事故實務。</p>
<p>這一頁處理的是文件壽命。沒有 lifecycle，runbook 很快會變成看起來完整、實際失效的紙上流程。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 runbook 時，先看是否有使用與演練記錄，再看是否有明確淘汰條件。</p>
<p>重點訊號包括：</p>
<ul>
<li>runbook 是否有 owner、版本與修訂時間</li>
<li>是否有演練證明其可執行性</li>
<li>過期或無法使用的 runbook 是否有淘汰流程</li>
<li>每次事故後是否回寫修訂</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">Atlassian</a>：協作工具事故很依賴 runbook 的版本同步。</li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">GitHub</a>：平台型服務的 runbook 常要跟著架構快速更新。</li>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">Slack</a>：通訊平台的 runbook 若過期，事故時會直接放大混亂。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：事故後 runbook 修訂</li>
<li>08.6 drills：runbook 演練驗證</li>
<li>08.13 repeated：<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 後 runbook 退場</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時 <a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 找出 runbook、發現步驟過期</li>
<li>runbook 上次修訂時間 &gt; 12 個月、依賴的服務早已換版本</li>
<li>新 oncall 找不到「該事故對應的 runbook」</li>
<li>runbook 數量只增不減、無淘汰流程</li>
<li>runbook 質量靠 author 個人風格、無模板</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：事故後 runbook 修訂</li>
<li>08.6 drills：runbook 演練驗證</li>
<li>08.13 repeated：<a href="/blog/backend/knowledge-cards/toil/" data-link-title="Toil" data-link-desc="說明重複、手動、無永久價值的工作如何成為工程治理對象">toil</a> 後 runbook 退場</li>
</ul>
]]></content:encoded></item><item><title>Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。連線與往返是 application 端量到的延遲主因，跟 server 端的&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">持久化&lt;/a>調校互補。pipeline 機制以 &lt;a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返&lt;/h2>
&lt;p>把單一 &lt;code>GET&lt;/code> 丟進 &lt;code>redis-cli --latency&lt;/code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。&lt;/p>
&lt;p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。&lt;/p>
&lt;p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點&lt;/a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。&lt;/p>
&lt;h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段&lt;/h2>
&lt;p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：&lt;/p>
&lt;p>&lt;strong>連線池消除「每次都建連線」的稅&lt;/strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。連線與往返是 application 端量到的延遲主因，跟 server 端的<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">持久化</a>調校互補。pipeline 機制以 <a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返</h2>
<p>把單一 <code>GET</code> 丟進 <code>redis-cli --latency</code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。</p>
<p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。</p>
<p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點</a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。</p>
<h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段</h2>
<p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：</p>
<p><strong>連線池消除「每次都建連線」的稅</strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。</p>
<p><strong>pipelining 把 N 次 RTT 壓成 1 次</strong>。連續送 N 個命令而不等每個的回應，一次讀回 N 個結果。這要求這 N 個命令彼此無依賴（後一個不需要前一個的結果）。</p>
<p><strong>Lua script / 多 key 命令把多操作合成 1 次往返且原子</strong>。當命令之間有依賴（讀了再決定怎麼寫），pipeline 不適用（後面的命令送出時前面的結果還沒回來），這時用 Lua script 把邏輯放到 server 端一次執行，省 RTT 又拿到原子性。</p>
<h3 id="pipeline-跟-multi-是不同的東西">pipeline 跟 MULTI 是不同的東西</h3>
<p>這兩個常被混淆，但解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>pipeline</th>
          <th>MULTI / EXEC（transaction）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要目的</td>
          <td>省 RTT（效能）</td>
          <td>原子性（多命令不被打斷）</td>
      </tr>
      <tr>
          <td>原子性</td>
          <td>無——命令間可能插入其他 client</td>
          <td>有——EXEC 內命令連續執行不被插入</td>
      </tr>
      <tr>
          <td>回應時機</td>
          <td>全部送完一次讀回</td>
          <td>EXEC 後一次回所有結果</td>
      </tr>
      <tr>
          <td>失敗處理</td>
          <td>各命令獨立成敗</td>
          <td>入隊期語法錯整批拒、執行期錯不回滾</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>大量無依賴命令的批次讀寫</td>
          <td>需要「一組命令不被其他 client 插隊」</td>
      </tr>
  </tbody>
</table>
<p>pipeline 純粹是傳輸層優化，不保證原子性——pipeline 裡的命令在 server 端仍可能跟其他 client 的命令交錯。要原子性用 MULTI/EXEC 或 Lua。兩者也可以組合（在 pipeline 裡送 MULTI&hellip;EXEC）。</p>
<p>注意 Redis 的 MULTI/EXEC 不是關聯式 DB 的 transaction：執行期某命令出錯（例如對 string 做 list 操作）不會回滾已執行的命令，它沒有 rollback。</p>
<h2 id="配置連線池與-pipeline-的設定路徑">配置：連線池與 pipeline 的設定路徑</h2>
<p>連線池配置（以 Python redis-py 為例，多數 client library 概念一致）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">redis</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">pool</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">ConnectionPool</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">host</span><span class="o">=</span><span class="s2">&#34;10.0.0.1&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">max_connections</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>          <span class="c1"># 池上限、依並發量與 Redis maxclients 反推</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>          <span class="c1"># 單命令逾時（秒）——必設、否則慢命令拖垮 caller</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">socket_connect_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>  <span class="c1"># 建連逾時</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">health_check_interval</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>    <span class="c1"># 定期檢查連線存活、清掉壞連線</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">connection_pool</span><span class="o">=</span><span class="n">pool</span><span class="p">)</span></span></span></code></pre></div><p><code>socket_timeout</code> 是最常被遺漏卻最關鍵的設定——沒設逾時，一個慢命令或網路黑洞會讓 caller 無限等待，連鎖拖垮上游。</p>
<p>pipeline 的使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># pipeline：N 個無依賴命令、一次往返</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">pipe</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">pipeline</span><span class="p">(</span><span class="n">transaction</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>  <span class="c1"># transaction=False 純 pipeline、不包 MULTI</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="n">uid</span> <span class="ow">in</span> <span class="n">user_ids</span><span class="p">:</span>                  <span class="c1"># 假設要拿 100 個 user 的 profile</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">pipe</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;user:</span><span class="si">{</span><span class="n">uid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="n">pipe</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>              <span class="c1"># 一次往返拿回 100 個結果</span></span></span></code></pre></div><p>依賴型操作改用 Lua（命令間有讀後寫的依賴，pipeline 不適用）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 原子的 check-and-set：讀目前值、符合條件才更新——一次往返且原子</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">lua</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">local current = redis.call(&#39;GET&#39;, KEYS[1])
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">if current == ARGV[1] then
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">  redis.call(&#39;SET&#39;, KEYS[1], ARGV[2])
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">  return 1
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">end
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">return 0
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">cas</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">register_script</span><span class="p">(</span><span class="n">lua</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">cas</span><span class="p">(</span><span class="n">keys</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;lock:resource&#34;</span><span class="p">],</span> <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;old_token&#34;</span><span class="p">,</span> <span class="s2">&#34;new_token&#34;</span><span class="p">])</span></span></span></code></pre></div><p><code>MGET</code> / <code>MSET</code> / <code>HMGET</code> 等原生多 key 命令是最簡單的省 RTT 手段——能用多 key 命令就不用 pipeline，更省事且原子。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1每請求新建連線延遲全是建連稅">Case 1：每請求新建連線、延遲全是建連稅</h3>
<p><strong>徵兆</strong>：Redis 呼叫延遲偏高且不穩，<code>INFO stats</code> 的 <code>total_connections_received</code> 速率極高（接近 QPS），Redis 的 <code>connected_clients</code> 反覆上下震盪。</p>
<p><strong>根因</strong>：application 沒用連線池，或每個請求 <code>redis.Redis(...)</code> 重新建立 client。每次請求付一趟 TCP 握手（加 TLS 更多）的 RTT，建連稅疊在每個請求上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用連線池並重用，client 物件在 application 生命週期內共用，不是每請求建立</li>
<li>短生命週期環境（Lambda / serverless）把連線池放在 handler 外（容器重用時連線存活）</li>
<li>監控 <code>total_connections_received</code> 速率，遠高於合理重連頻率代表沒重用</li>
<li>TLS 場景的建連稅更高，連線重用的收益更大</li>
</ol>
<h3 id="case-2沒設-socket_timeout一個慢命令拖垮整條鏈">Case 2：沒設 socket_timeout、一個慢命令拖垮整條鏈</h3>
<p><strong>徵兆</strong>：某次 Redis 短暫卡頓（fork 尖峰、網路抖動），application 端大量請求 hang 住不回，thread / connection 被耗盡，影響擴散到跟 Redis 無關的請求。</p>
<p><strong>根因</strong>：連線沒設 <code>socket_timeout</code>。Redis 一旦慢回應或網路黑洞，caller 無限等待，佔住 thread 與連線，連鎖拖垮整個服務。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>一律設 <code>socket_timeout</code>（cache 場景通常幾百 ms 就該逾時，cache 本來就該快）</li>
<li>逾時後 application 要有 fallback（回源或降級），不是把逾時當 fatal</li>
<li>連線池 <code>max_connections</code> 設上限，避免無限建連把 Redis 的 <code>maxclients</code> 打滿</li>
<li>fork 尖峰是常見的慢源頭，對應 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence deep article</a> 的延遲尖峰治理</li>
</ol>
<h3 id="case-3一個巨大-pipeline-把-server-跟-client-都撐爆">Case 3：一個巨大 pipeline 把 server 跟 client 都撐爆</h3>
<p><strong>徵兆</strong>：用 pipeline 批次處理時，某次塞了幾十萬個命令進一個 pipeline，Redis 記憶體尖峰、client 端記憶體爆，甚至 OOM。</p>
<p><strong>根因</strong>：pipeline 把所有命令的 request 跟 response 都 buffer 起來。一次塞太多，server 端要 buffer 全部 reply（計入 <code>used_memory</code>、見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a> 的 output buffer），client 端要 hold 全部結果，雙邊記憶體尖峰。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pipeline 分批（chunk），每批幾百到幾千命令，不要一個 pipeline 塞無上限</li>
<li>大量資料的掃描用 <code>SCAN</code> 游標分批，不要 <code>KEYS *</code> 一次撈</li>
<li>監控 client output buffer（<code>CLIENT LIST</code> 的 <code>omem</code>），異常大代表有巨型 pipeline 或慢 consumer</li>
<li>批次大小靠 RTT 與記憶體權衡——批次越大省越多 RTT，但記憶體尖峰越高</li>
</ol>
<h3 id="case-4在-cluster-模式對跨-slot-key-開-pipeline--transaction-失敗">Case 4：在 cluster 模式對跨 slot key 開 pipeline / transaction 失敗</h3>
<p><strong>徵兆</strong>：單機 Redis 上運作正常的 pipeline 或 MULTI，搬到 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a> 後報 <code>CROSSSLOT Keys in request don't hash to the same slot</code>。</p>
<p><strong>根因</strong>：Cluster 模式下 MULTI/EXEC 與某些多 key 命令要求所有 key 在同一個 hash slot。pipeline 在 cluster 下也要按 slot 分組送到對應 node——若 client library 不自動處理跨 slot，會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>同組操作的 key 用 hash tag <code>{...}</code> 強制同 slot（例如 <code>user:{123}:profile</code>、<code>user:{123}:settings</code>）</li>
<li>用支援 cluster pipeline 的 client library，它會自動按 slot 分組</li>
<li>設計階段就考慮 key 的 slot 分布，避免事後重構，對應 cluster re-sharding 的 hash tag 治理</li>
<li>跨 slot 的批次邏輯改用 application 端聚合，不依賴 server 端原子性</li>
</ol>
<h3 id="case-5把-pipeline-當-transaction-用出現資料競態">Case 5：把 pipeline 當 transaction 用、出現資料競態</h3>
<p><strong>徵兆</strong>：用 pipeline 做「讀一個值、根據它決定寫什麼」的邏輯，高並發下偶發資料不一致——兩個 client 讀到同樣的舊值、各自寫入，一方覆蓋另一方。</p>
<p><strong>根因</strong>：把 pipeline 誤當原子操作。pipeline 只是把命令打包傳輸，命令之間 server 端仍可能插入其他 client 的命令——它沒有原子性。讀後寫的依賴邏輯放 pipeline 裡，等於沒有任何併發保護。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>讀後寫的依賴邏輯用 Lua script（server 端原子執行），不用 pipeline</li>
<li>樂觀鎖場景用 <code>WATCH</code> + MULTI/EXEC（watch 的 key 被改則 EXEC 失敗、重試）</li>
<li>分清楚需求：要省 RTT 用 pipeline，要原子性用 Lua / MULTI，兩者目的不同</li>
<li>distributed lock 場景見 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a>，Redis 的鎖有自己的正確性陷阱</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>連線與往返的容量判讀，圍繞連線數與每請求往返次數：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>connected_clients</code></td>
          <td>穩定、遠低於 <code>maxclients</code></td>
          <td>接近 maxclients → 池太大或洩漏、調池上限</td>
      </tr>
      <tr>
          <td><code>total_connections_received</code> 速率</td>
          <td>低（連線重用）</td>
          <td>接近 QPS → 沒用連線池、每請求建連</td>
      </tr>
      <tr>
          <td>每請求 Redis 往返次數</td>
          <td>盡量合併（多 key / pipeline）</td>
          <td>多次獨立往返 → 用 pipeline / MGET 合併</td>
      </tr>
      <tr>
          <td>client output buffer (<code>omem</code>)</td>
          <td>小</td>
          <td>大 → 巨型 pipeline 或慢 consumer</td>
      </tr>
      <tr>
          <td>Redis CPU</td>
          <td>有餘裕</td>
          <td>單執行緒 CPU 滿 → 命令太重或 QPS 超單機</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單執行緒 CPU 打滿、命令吞吐到頂</strong>：Redis 主執行緒單線處理命令，pipeline 省 RTT 但不增加 server 端平行度。CPU 到頂走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>把命令分散到多 node。</li>
<li><strong>想要單機多核平行處理命令</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 shared-nothing 多核架構讓命令在單機就能多核平行，Redis 要靠 cluster 才能達到的吞吐它單機就能撐——高吞吐單機 workload 的替代。</li>
<li><strong>跨 cloud / 跨 region 的 RTT 是結構性瓶頸</strong>：<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 的解法</a>是把 cache 部署到跟 application 同 cloud / 同 region，從根本消除跨區 RTT——這是架構層決策，不是 pipeline 能補的。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>連線與往返是 application 端延遲的主因，但它跟 server 端調校互補：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：巨型 pipeline 的 server 端 reply buffer 計入 <code>used_memory</code>、慢 consumer 的 output buffer 是記憶體洩漏源頭。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：fork 尖峰是 socket_timeout 必須存在的理由之一——慢源頭不只網路。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></strong>：cluster 模式改變 pipeline / transaction 的 key 分布規則，hash tag 治理是前提。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></strong>：高並發下的連線數爆炸與熱 key 是同一組壓力的不同面向，連線池上限與 local cache 兩層都是解法。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 與 failover 時序</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>、<a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL Binary Log + CDC：Maxwell / Debezium 是 binlog 第二消費者</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/binlog-cdc/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/binlog-cdc/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>CDC&lt;/em> — Maxwell / Debezium 怎麼讀 binlog 產生 event stream。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>MySQL CDC 的核心定位是 &lt;em>binlog consumer&lt;/em>。&lt;/p>
&lt;p>這個誤解來自跟 PostgreSQL CDC（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &amp;#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium&lt;/a>）混用名詞。PG 的 logical decoding 是 &lt;em>MySQL 沒有的能力&lt;/em> — PG 有 logical event（INSERT / UPDATE / DELETE 加上欄位 metadata）、輸出格式是 logical（人可讀、schema-aware）。MySQL 的 binlog 是 &lt;em>physical&lt;/em> — 紀錄的是 row 的 binary image、不帶 schema 資訊。&lt;/p>
&lt;p>Maxwell / Debezium 對 MySQL 是 &lt;em>binlog 第二消費者&lt;/em>：&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">Primary MySQL → binlog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ├→ Replica 1（讀 binlog 同步）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ├→ Replica 2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> └→ Maxwell / Debezium（讀 binlog 解析、發 Kafka）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟 replica 同一份 binlog stream，並非 separate logical decoding output。這個結構決定 CDC consumer 的設計：必須 &lt;em>自己處理 schema&lt;/em>（從 information_schema 拉、跟 binlog event 對齊）、必須 &lt;em>自己 track position&lt;/em>（binlog file + position 或 GTID）。&lt;/p>
&lt;h2 id="binlog-formatstatement--row--mixed">Binlog format：STATEMENT / ROW / MIXED&lt;/h2>
&lt;p>MySQL binlog 有 3 種 format、CDC 只能用 ROW：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Format&lt;/th>
 &lt;th>紀錄內容&lt;/th>
 &lt;th>CDC 可用？&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>STATEMENT&lt;/td>
 &lt;td>原始 SQL statement&lt;/td>
 &lt;td>不可用（CDC 看不到實際改的 row）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ROW&lt;/td>
 &lt;td>每個改變的 row（before + after image）&lt;/td>
 &lt;td>CDC 標準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MIXED&lt;/td>
 &lt;td>預設 STATEMENT、特殊情況用 ROW&lt;/td>
 &lt;td>不推薦（CDC 行為不一致）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ROW 是 CDC 唯一選擇、production 強制：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>CDC</em> — Maxwell / Debezium 怎麼讀 binlog 產生 event stream。</p></blockquote>
<hr>
<p>MySQL CDC 的核心定位是 <em>binlog consumer</em>。</p>
<p>這個誤解來自跟 PostgreSQL CDC（<a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a>）混用名詞。PG 的 logical decoding 是 <em>MySQL 沒有的能力</em> — PG 有 logical event（INSERT / UPDATE / DELETE 加上欄位 metadata）、輸出格式是 logical（人可讀、schema-aware）。MySQL 的 binlog 是 <em>physical</em> — 紀錄的是 row 的 binary image、不帶 schema 資訊。</p>
<p>Maxwell / Debezium 對 MySQL 是 <em>binlog 第二消費者</em>：</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">Primary MySQL → binlog
</span></span><span class="line"><span class="ln">2</span><span class="cl">              ├→ Replica 1（讀 binlog 同步）
</span></span><span class="line"><span class="ln">3</span><span class="cl">              ├→ Replica 2
</span></span><span class="line"><span class="ln">4</span><span class="cl">              └→ Maxwell / Debezium（讀 binlog 解析、發 Kafka）</span></span></code></pre></div><p>跟 replica 同一份 binlog stream，並非 separate logical decoding output。這個結構決定 CDC consumer 的設計：必須 <em>自己處理 schema</em>（從 information_schema 拉、跟 binlog event 對齊）、必須 <em>自己 track position</em>（binlog file + position 或 GTID）。</p>
<h2 id="binlog-formatstatement--row--mixed">Binlog format：STATEMENT / ROW / MIXED</h2>
<p>MySQL binlog 有 3 種 format、CDC 只能用 ROW：</p>
<table>
  <thead>
      <tr>
          <th>Format</th>
          <th>紀錄內容</th>
          <th>CDC 可用？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>STATEMENT</td>
          <td>原始 SQL statement</td>
          <td>不可用（CDC 看不到實際改的 row）</td>
      </tr>
      <tr>
          <td>ROW</td>
          <td>每個改變的 row（before + after image）</td>
          <td>CDC 標準</td>
      </tr>
      <tr>
          <td>MIXED</td>
          <td>預設 STATEMENT、特殊情況用 ROW</td>
          <td>不推薦（CDC 行為不一致）</td>
      </tr>
  </tbody>
</table>
<p>ROW 是 CDC 唯一選擇、production 強制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">binlog_format</span> <span class="o">=</span> <span class="s">ROW</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">binlog_row_image</span> <span class="o">=</span> <span class="s">FULL  # FULL (all columns) / MINIMAL (only changed) / NOBLOB</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">log_bin_use_v1_row_events</span> <span class="o">=</span> <span class="s">0  # 用新版 event format</span></span></span></code></pre></div><p><code>binlog_row_image</code> 取捨：</p>
<ul>
<li><code>FULL</code>：每個 row event 包含所有 column（before + after）、binlog 大、CDC 完整</li>
<li><code>MINIMAL</code>：只包含 changed column + primary key、binlog 省 30-50% 空間、CDC 看不到 <em>未變 column</em></li>
<li><code>NOBLOB</code>：跟 FULL 一樣但 BLOB / TEXT column 只在 changed 時包含、平衡選擇</li>
</ul>
<p>對 <em>CDC 需要 full row payload</em>（例如下游 search index 重建）必須 <code>FULL</code>。對 <em>純 audit log</em> 可以 <code>MINIMAL</code>。</p>
<h2 id="row-format-的-raw-event-結構">ROW format 的 raw event 結構</h2>
<p>Binlog ROW event 的資料形狀是 <em>binary row image</em>，而非 <em>INSERT INTO orders VALUES (1, &lsquo;foo&rsquo;, 100)</em>：</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">TABLE_MAP_EVENT     - 對應 table schema metadata (table id + column type)
</span></span><span class="line"><span class="ln">2</span><span class="cl">                      ↓ 接續同一個 transaction 內所有 row event
</span></span><span class="line"><span class="ln">3</span><span class="cl">WRITE_ROWS_EVENT    - INSERT 的新 row image（column values）
</span></span><span class="line"><span class="ln">4</span><span class="cl">UPDATE_ROWS_EVENT   - UPDATE 的 before + after image
</span></span><span class="line"><span class="ln">5</span><span class="cl">DELETE_ROWS_EVENT   - DELETE 的 row image（被刪的 row）
</span></span><span class="line"><span class="ln">6</span><span class="cl">XID_EVENT           - transaction commit marker</span></span></code></pre></div><p>CDC consumer（Maxwell / Debezium）必須：</p>
<ol>
<li>接收 binlog event stream</li>
<li>看到 <code>TABLE_MAP_EVENT</code> 從中拿 table id → 對應 table name（cache 一份）</li>
<li>看到 <code>WRITE/UPDATE/DELETE_ROWS_EVENT</code> 用 table id 反查 schema、把 binary 解析成 column value</li>
<li>包成 JSON / Avro / Protobuf 推到 Kafka</li>
</ol>
<p>關鍵：<em>table schema 不在 binlog 內</em>、CDC consumer 必須 <em>獨立查 information_schema</em>。如果 schema 變了（ALTER TABLE）、CDC 必須 invalidate cache、重新查、否則新 column 的 row event 解析錯亂。</p>
<h2 id="maxwell-vs-debezium">Maxwell vs Debezium</h2>
<p>兩個是 MySQL CDC 主流選擇、不同設計取捨：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Maxwell</th>
          <th>Debezium MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開發者</td>
          <td>Zendesk</td>
          <td>Red Hat</td>
      </tr>
      <tr>
          <td>語言</td>
          <td>Java（單一 binary）</td>
          <td>Java（Kafka Connect plugin）</td>
      </tr>
      <tr>
          <td>部署模式</td>
          <td>Standalone process</td>
          <td>Kafka Connect cluster</td>
      </tr>
      <tr>
          <td>支援 DB</td>
          <td>MySQL only</td>
          <td>MySQL / PostgreSQL / MongoDB / SQL Server / Oracle</td>
      </tr>
      <tr>
          <td>Output format</td>
          <td>JSON（內建）</td>
          <td>JSON / Avro / Protobuf（Kafka Connect）</td>
      </tr>
      <tr>
          <td>Producer</td>
          <td>Kafka / Kinesis / RabbitMQ / Pub/Sub</td>
          <td>Kafka（Kafka Connect 限制）</td>
      </tr>
      <tr>
          <td>Schema registry</td>
          <td>不支援</td>
          <td>支援（Confluent Schema Registry / Apicurio）</td>
      </tr>
      <tr>
          <td>Transformation</td>
          <td>filter / stream-level（內建）</td>
          <td>Single Message Transform (SMT)</td>
      </tr>
      <tr>
          <td>Bootstrapping</td>
          <td>一個 utility 從 <code>SELECT *</code> snapshot</td>
          <td>Built-in snapshot mode</td>
      </tr>
      <tr>
          <td>GTID 支援</td>
          <td>支援</td>
          <td>支援</td>
      </tr>
      <tr>
          <td>簡單性</td>
          <td>高（單一 binary）</td>
          <td>中（Kafka Connect 框架成本）</td>
      </tr>
  </tbody>
</table>
<p>選擇邏輯：</p>
<ul>
<li><em>只用 MySQL + 想要 simple operations</em> → Maxwell</li>
<li><em>已用 Kafka Connect、需要 schema registry、跨多種 DB</em> → Debezium</li>
<li><em>需要 Avro / Protobuf schema 嚴格 governance</em> → Debezium</li>
</ul>
<h2 id="配置-step-by-stepdebezium-mysql-connector">配置 step-by-step（Debezium MySQL connector）</h2>
<p>Debezium 是 Kafka Connect plugin、整套 stack：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># debezium-mysql.json - 部署到 Kafka Connect REST API</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span>{<span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">&#34;name&#34;: </span><span class="s2">&#34;orders-mysql-connector&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">&#34;config&#34;: </span>{<span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">&#34;connector.class&#34;: </span><span class="s2">&#34;io.debezium.connector.mysql.MySqlConnector&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.hostname&#34;: </span><span class="s2">&#34;primary.example.com&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.port&#34;: </span><span class="s2">&#34;3306&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.user&#34;: </span><span class="s2">&#34;debezium&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.password&#34;: </span><span class="s2">&#34;...&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.server.id&#34;: </span><span class="s2">&#34;184054&#34;</span><span class="p">,</span><span class="w">          </span><span class="c"># 唯一 server ID (跟 MySQL replica 一樣)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">&#34;topic.prefix&#34;: </span><span class="s2">&#34;production&#34;</span><span class="p">,</span><span class="w">            </span><span class="c"># Debezium 2.x（舊 1.x 用 database.server.name）</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.include.list&#34;: </span><span class="s2">&#34;orders_db&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">&#34;table.include.list&#34;: </span><span class="s2">&#34;orders_db.orders,orders_db.payments&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.history.kafka.bootstrap.servers&#34;: </span><span class="s2">&#34;kafka:9092&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">&#34;database.history.kafka.topic&#34;: </span><span class="s2">&#34;dbhistory.orders&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">&#34;include.schema.changes&#34;: </span><span class="s2">&#34;true&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">&#34;snapshot.mode&#34;: </span><span class="s2">&#34;initial&#34;</span><span class="p">,</span><span class="w">              </span><span class="c"># 或 schema_only / when_needed / never</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">&#34;snapshot.locking.mode&#34;: </span><span class="s2">&#34;minimal&#34;</span><span class="p">,</span><span class="w">      </span><span class="c"># 避免 FLUSH TABLES WITH READ LOCK</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">&#34;gtid.source.includes&#34;: </span><span class="s2">&#34;...&#34;</span><span class="p">,</span><span class="w">           </span><span class="c"># 可選 GTID filter</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">&#34;tombstones.on.delete&#34;: </span><span class="s2">&#34;true&#34;</span><span class="p">,</span><span class="w">          </span><span class="c"># DELETE event 同 partition 跟一個 null tombstone</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nt">&#34;decimal.handling.mode&#34;: </span><span class="s2">&#34;double&#34;</span><span class="w">        </span><span class="c"># DECIMAL 處理: precise / string / double</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w"></span>}</span></span></code></pre></div><p>deploy：</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">curl -X POST -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --data @debezium-mysql.json <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  http://kafka-connect:8083/connectors</span></span></code></pre></div><p>Output topic：<code>production.orders_db.orders</code> / <code>production.orders_db.payments</code> 等 — 每張 table 一個 topic。</p>
<h2 id="配置-step-by-stepmaxwell">配置 step-by-step（Maxwell）</h2>
<p>Maxwell 簡單很多：</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">maxwell <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --host<span class="o">=</span>primary.example.com <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --user<span class="o">=</span>maxwell <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --producer<span class="o">=</span>kafka <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --kafka.bootstrap.servers<span class="o">=</span>kafka:9092 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --kafka_topic<span class="o">=</span><span class="s2">&#34;maxwell.%{database}.%{table}&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --filter<span class="o">=</span><span class="s1">&#39;exclude: *.*, include: orders_db.*&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --gtid_mode<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --output_ddl<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --output_xoffset<span class="o">=</span>true</span></span></code></pre></div><p>Maxwell event format：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;database&#34;</span><span class="p">:</span> <span class="s2">&#34;orders_db&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;table&#34;</span><span class="p">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;update&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;ts&#34;</span><span class="p">:</span> <span class="mi">1715000000</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;xid&#34;</span><span class="p">:</span> <span class="mi">12345</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;commit&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;shipped&#34;</span><span class="p">,</span> <span class="nt">&#34;amount&#34;</span><span class="p">:</span> <span class="mf">100.50</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&#34;old&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;pending&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Debezium 對應的 event 格式更複雜（envelope + before + after + source + ts_ms 各 nested）、但跟 schema registry 整合好。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-binlog-retention-太短--cdc-consumer-落後就-re-bootstrap">1. Binlog retention 太短 — CDC consumer 落後就 re-bootstrap</h3>
<p>CDC consumer 失聯（Kafka Connect cluster down、network issue）超過 binlog retention（預設 <code>binlog_expire_logs_seconds=2592000</code>、30 天、但有些 production 縮短到 1 天）、需要的 binlog event 已被 purge、consumer error。</p>
<p>修法：</p>
<ul>
<li><em>Production binlog retention &gt;= 7 天</em>（避免為了 disk 過度縮短）</li>
<li>監控 <code>Master_Log_File</code> 是否還在（如果 retention 設 7 天、確認當前 file 仍存在）</li>
<li>CDC consumer 失聯 alert 設 <em>早於 retention 期</em>（例如 6 天告警、給 24 小時修）</li>
<li>真的 missed binlog、必須 <em>re-snapshot table</em>（用 Debezium <code>snapshot.new.tables</code>）— 24 小時級工作</li>
</ul>
<h3 id="2-ddl-event-處理--schema-change-跟-row-event-對齊">2. DDL event 處理 — schema change 跟 row event 對齊</h3>
<p><code>ALTER TABLE orders ADD COLUMN status VARCHAR(20)</code> 之後、<code>UPDATE_ROWS_EVENT</code> 多一個 column。CDC consumer 如果還用舊 schema cache、解析 row 時欄位數對不上、event 丟。</p>
<p>修法（Debezium）：</p>
<ul>
<li><code>include.schema.changes=true</code>：DDL 進獨立 topic、consumer 監聽更新自己的 schema cache</li>
<li><code>database.history.kafka.topic</code>：Debezium 自己 track schema 歷史</li>
</ul>
<p>修法（Maxwell）：</p>
<ul>
<li><code>--output_ddl=true</code>：DDL 也進 stream、downstream 看到 DDL event 自己更新</li>
<li>沒有內建 schema history、要 <em>application 層處理</em></li>
</ul>
<p>修法（兩者通用）：</p>
<ul>
<li>用 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a> 取代直接 ALTER — 工具操作的 DDL 對 CDC consumer 更可預期</li>
<li>Schema 改動 <em>優先 add column 為 nullable</em>、避免 backfill 期間 CDC consumer 看到 mid-state</li>
</ul>
<h3 id="3-binlog_row_imageminimal-讓下游錯亂">3. <code>binlog_row_image=MINIMAL</code> 讓下游錯亂</h3>
<p><code>MINIMAL</code> 省 binlog 空間、但 row event 只含 changed column。下游 <em>search index 重建</em> 需要 <em>full row payload</em> 的場景下、<code>MINIMAL</code> 看不到未變的 column、index 缺欄位。</p>
<p>修法：</p>
<ul>
<li>CDC 需要 full payload 的場景 <em>必須 <code>FULL</code></em>、這項成本要納入容量規劃</li>
<li>如果空間真緊、考慮 <code>NOBLOB</code>（BLOB / TEXT 只在 changed 時包含、其他 column 仍 FULL）</li>
<li><em>統一設定</em>：production 全部 server 同一 binlog_row_image 設定</li>
</ul>
<h3 id="4-kafka-producer-跟-binlog-reader-速度差--lag-累積">4. Kafka producer 跟 binlog reader 速度差 — lag 累積</h3>
<p>Binlog reader 從 MySQL 讀 1000 event/sec、Kafka producer 寫得只有 800 event/sec、CDC consumer 自身 lag 累積、最終 disk 滿（producer 內部 buffer）。</p>
<p>修法：</p>
<ul>
<li>監控 <em>CDC consumer lag</em>：對 Debezium 看 Kafka Connect 的 <code>source-record-poll-rate</code> vs <code>source-record-write-rate</code></li>
<li>Kafka producer tuning：<code>batch.size</code> / <code>linger.ms</code> / <code>compression.type=snappy</code></li>
<li>Kafka broker capacity：partition 數量 ≥ Debezium task 數量、避免 partition 瓶頸</li>
<li>避免把 <em>過多 table</em> 給單一 Debezium connector — 用 <em>table grouping</em>（按 traffic 拆 connector）</li>
</ul>
<h3 id="5-schema-change-跟-downstream-consumer-不同步">5. Schema change 跟 downstream consumer 不同步</h3>
<p>CDC producer（Debezium）正確處理了 schema change、但 <em>downstream Kafka consumer</em> 用舊 schema deserialize、新 column 看不到 / type 解析錯。</p>
<p>修法：</p>
<ul>
<li>用 <em>Schema Registry</em>（Confluent / Apicurio）+ Avro：consumer 訂閱 schema、自動 evolve</li>
<li>不用 schema registry 時、CDC payload 設計 <em>backward-compatible</em>（新 column 為 optional）</li>
<li><em>Application 層 schema change protocol</em>：<a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> — 先加 column、deploy consumer 認 column、再 backfill、最後 application 寫新 column</li>
<li>大型 schema change 跨多服務、建議 <em>先 freeze CDC stream、做 schema migration、resume stream</em>（極端但確定）</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>容量考量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MySQL binlog disk</td>
          <td>retention × 寫吞吐 × event size（5K WPS × 1 KB × 7 天 ~= 3 GB / 天 = 21 GB）</td>
      </tr>
      <tr>
          <td>Debezium / Maxwell process</td>
          <td>1 vCPU + 2-4 GB RAM（per connector、視 throughput）</td>
      </tr>
      <tr>
          <td>Kafka topic partition</td>
          <td>每 table 1-10 partition（依寫吞吐）、保 key-based ordering</td>
      </tr>
      <tr>
          <td>Kafka 保留期</td>
          <td>7-30 天（讓 downstream consumer 有 recover window）</td>
      </tr>
      <tr>
          <td>Schema Registry</td>
          <td>&lt; 100 MB storage、replicate 跨 3 broker</td>
      </tr>
  </tbody>
</table>
<p>對 100K WPS server、CDC pipeline cost 大致是 <em>MySQL infra 的 5-10%</em>。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>CDC 是 <em>binlog 第二消費者</em>、需要 <em>GTID + binlog ROW format</em>（<a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>）。Debezium / Maxwell 都偏好從 <em>replica</em> 讀 binlog（不增加 primary 負擔）、但要小心 replica lag 加在 CDC lag 上。</p>
<h3 id="跟-osc-tool">跟 OSC tool</h3>
<p><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">gh-ost / pt-osc</a> 跑 schema change 時、會在 binlog 留下大量 row event（copy 既有 row 到 ghost）。CDC consumer 看到這些 event <em>是 normal-looking INSERT</em>、可能誤觸發 downstream side effect。</p>
<p>修法：</p>
<ul>
<li>CDC consumer 過濾 <em>ghost table prefix</em>（<code>_orders_new</code> / <code>_orders_gho</code>）— 不發 downstream</li>
<li>或暫停 CDC 期間跑 OSC（用 Debezium pause API）</li>
</ul>
<h3 id="跟-postgresql-logical-replication--debezium">跟 PostgreSQL Logical Replication + Debezium</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL（binlog）</th>
          <th>PostgreSQL（logical decoding）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>抽象層</td>
          <td>Physical（row binary）</td>
          <td>Logical（row + schema-aware）</td>
      </tr>
      <tr>
          <td>Schema metadata</td>
          <td>不在 event 內、要查 information_schema</td>
          <td>在 event 內（plugin output）</td>
      </tr>
      <tr>
          <td>DDL handling</td>
          <td>DDL 本身是 binlog event</td>
          <td>DDL 不在 logical decoding output（要 trigger 自己 capture）</td>
      </tr>
      <tr>
          <td>啟用成本</td>
          <td>binlog ROW + GTID（基本 MySQL replication setup）</td>
          <td>logical replication slot + publication</td>
      </tr>
      <tr>
          <td>Snapshot</td>
          <td><code>SELECT *</code> + binlog catchup</td>
          <td>logical replication initial sync</td>
      </tr>
  </tbody>
</table>
<p>詳見 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PostgreSQL Logical Replication + Debezium</a> — 這是 sibling 對照，用來區分不同 abstraction。</p>
<h3 id="跟-aurora-mysql">跟 Aurora MySQL</h3>
<p>Aurora MySQL 5.7 / 8.0 都支援 binlog + GTID、CDC 可用。但 Aurora 推薦走 <em>Aurora-native database activity streams</em>（不同 abstraction）— 跟 Debezium 共存但有 overlapping。生產上 Debezium 仍是 cross-cloud 跟 vendor-neutral 選項、優先用 Debezium。</p>
<p>詳見 <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 page</a>。</p>
<h2 id="production-caseshopify-sharded-mysql-cdc">Production case：Shopify sharded MySQL CDC</h2>
<p>Sharded MySQL CDC 的核心責任是把多個 shard 的 binlog 轉成可消費、可回放、可觀測的事件流。<a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">Shopify Debezium CDC over sharded MySQL</a> 提供的工程訊號是 100+ shard、約 150 個 Debezium connector、BFCM 期間 100K records/sec，以及 snapshot lock 與 oversized payload 對 CDC pipeline 的壓力。</p>
<p>這個案例要回收到三個操作判準。第一，connector 數量應跟 shard 拓撲一起設計，避免單一 connector 變成跨 shard bottleneck。第二，snapshot window 要排進 schema migration 與 event consumer 的變更計畫，避免 initial snapshot 把 production read path 壓滿。第三，oversized payload 要在 schema / outbox / topic 分流階段處理，避免 Kafka partition 與 downstream consumer 同時承受大訊息。</p>
<p>Shopify 案例的下一步路由是把本篇和 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a> 一起讀。若讀者關心 broker 層的 partition、consumer lag 與 replay 策略，接到 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor</a>；若關心資料庫端壓力，回到 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a> 與 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（binlog ROW + GTID 是 CDC pre-requisite）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（OSC + CDC 整合）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PostgreSQL Logical Replication + Debezium</a>（PG sibling、不同 abstraction）</li>
<li><a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox pattern 卡片</a>（CDC 跟 outbox 在 application-level event publishing 的關係）</li>
<li><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract 卡片</a>（schema migration 跟 CDC consumer）</li>
<li>官方：<a href="https://debezium.io/documentation/reference/stable/connectors/mysql.html">Debezium MySQL Connector</a> / <a href="https://maxwells-daemon.io/">Maxwell</a></li>
</ul>
]]></content:encoded></item><item><title>3.C17 Walmart：Messaging Proxy Service 解 rebalance storm</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/</guid><description>&lt;p>這個案例的核心責任是說明 partition-consumer 1:1 模型在大規模 K8s 環境的擴張極限。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Walmart 每天 trillions of message、25K+ Kafka consumer 跑在 WCNP Kubernetes 多雲環境；最大痛點是 pod scaling / deploy / heartbeat fail 觸發 consumer rebalance、lag spike。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>自建 Messaging Proxy Service（MPS、Kafka Connect sink connector）、把 consumer 從 partition-bound 解耦成 stateless REST service、可獨立 auto-scale、不用增 partition；內建 DLQ 處理 poison pill。揭露「consumer 該跟 partition 數綁定」這個假設在 K8s 規模化下不再成立。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：rebalance storm / consumer lag / multi-tenant 配額。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/walmartglobaltech/reliably-processing-trillions-of-kafka-messages-per-day-23494f553ef9">Reliably Processing Trillions of Kafka Messages Per Day&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 partition-consumer 1:1 模型在大規模 K8s 環境的擴張極限。</p>
<h2 id="觀察">觀察</h2>
<p>Walmart 每天 trillions of message、25K+ Kafka consumer 跑在 WCNP Kubernetes 多雲環境；最大痛點是 pod scaling / deploy / heartbeat fail 觸發 consumer rebalance、lag spike。</p>
<h2 id="判讀">判讀</h2>
<p>自建 Messaging Proxy Service（MPS、Kafka Connect sink connector）、把 consumer 從 partition-bound 解耦成 stateless REST service、可獨立 auto-scale、不用增 partition；內建 DLQ 處理 poison pill。揭露「consumer 該跟 partition 數綁定」這個假設在 K8s 規模化下不再成立。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：rebalance storm / consumer lag / multi-tenant 配額。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/walmartglobaltech/reliably-processing-trillions-of-kafka-messages-per-day-23494f553ef9">Reliably Processing Trillions of Kafka Messages Per Day</a></li>
</ul>
]]></content:encoded></item><item><title>Prisma Cloud</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/</guid><description>&lt;p>Prisma Cloud 是 Palo Alto Networks 旗下的 CNAPP（Cloud-Native Application Protection Platform）、把 &lt;em>runtime workload 防護&lt;/em>（Defender agent）跟 &lt;em>agentless cloud posture&lt;/em> 同一個 Console 整合。它的歷史是多次併購疊起來的 — Twistlock（container security）/ Redlock（CSPM）/ Bridgecrew（IaC scan）/ Aporeto（microsegmentation）— 五個模組各自有獨立的 data model 與 UI 軌跡。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &amp;#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &amp;#43; workload &amp;#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security&lt;/a> 的差異在 &lt;em>是否走 host-level agent + 是否綁 Palo Alto 生態&lt;/em>、功能清單相近。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Prisma Cloud 的核心定位是 &lt;em>agent + agentless 雙軌 CNAPP&lt;/em>、五模組覆蓋 cloud workload 從 IaC 到 runtime 的完整鏈：&lt;em>Compute Security&lt;/em>（前 Twistlock、container / serverless / host workload 的 Defender agent + image scan）、&lt;em>CSPM&lt;/em>（cloud posture、misconfiguration、compliance baseline）、&lt;em>Code Security&lt;/em>（前 Bridgecrew、IaC 與 SCM scan）、&lt;em>Data Security&lt;/em>（DSPM、雲端資料庫與 bucket 敏感資料偵測）、&lt;em>CIEM&lt;/em>（cloud entitlement、跨雲 over-permission 治理）。Defender agent 是 host / pod / Lambda extension 上跑的常駐元件、提供 runtime IDS、file integrity、process anomaly 等 &lt;em>agentless 抓不到的訊號&lt;/em>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &amp;#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz&lt;/a> 比、Prisma 走 &lt;em>agent + agentless 雙軌&lt;/em>、Wiz 走 &lt;em>agentless-only&lt;/em>。Wiz 用 cloud snapshot scan + control-plane API 抽訊號、部署快、不踩 host；Prisma Defender agent 補上 &lt;em>runtime behavior&lt;/em> 的覆蓋（process spawn pattern、JNDI lookup、anomalous network connect）、代價是要在 host / pod / Lambda 上佈 agent、deployment 複雜度高一個層級。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework&lt;/a> 比、Lacework 用 &lt;em>Polygraph behavior graph&lt;/em> 做 host-level anomaly、focus 在 detection；Prisma 覆蓋面廣（含 IaC + CIEM + DSPM）、但每個模組深度比 Lacework 偵測單點淺一點。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &amp;#43; workload &amp;#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security&lt;/a> 比、CrowdStrike 的 endpoint agent 已在多數 enterprise 環境跑、Cloud Security 直接共用 endpoint plane；Prisma 是 &lt;em>cloud-first CNAPP 加上 agent&lt;/em>、不是 endpoint EDR 延伸。&lt;/p></description><content:encoded><![CDATA[<p>Prisma Cloud 是 Palo Alto Networks 旗下的 CNAPP（Cloud-Native Application Protection Platform）、把 <em>runtime workload 防護</em>（Defender agent）跟 <em>agentless cloud posture</em> 同一個 Console 整合。它的歷史是多次併購疊起來的 — Twistlock（container security）/ Redlock（CSPM）/ Bridgecrew（IaC scan）/ Aporeto（microsegmentation）— 五個模組各自有獨立的 data model 與 UI 軌跡。它跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> / <a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a> / <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a> 的差異在 <em>是否走 host-level agent + 是否綁 Palo Alto 生態</em>、功能清單相近。</p>
<h2 id="服務定位">服務定位</h2>
<p>Prisma Cloud 的核心定位是 <em>agent + agentless 雙軌 CNAPP</em>、五模組覆蓋 cloud workload 從 IaC 到 runtime 的完整鏈：<em>Compute Security</em>（前 Twistlock、container / serverless / host workload 的 Defender agent + image scan）、<em>CSPM</em>（cloud posture、misconfiguration、compliance baseline）、<em>Code Security</em>（前 Bridgecrew、IaC 與 SCM scan）、<em>Data Security</em>（DSPM、雲端資料庫與 bucket 敏感資料偵測）、<em>CIEM</em>（cloud entitlement、跨雲 over-permission 治理）。Defender agent 是 host / pod / Lambda extension 上跑的常駐元件、提供 runtime IDS、file integrity、process anomaly 等 <em>agentless 抓不到的訊號</em>。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> 比、Prisma 走 <em>agent + agentless 雙軌</em>、Wiz 走 <em>agentless-only</em>。Wiz 用 cloud snapshot scan + control-plane API 抽訊號、部署快、不踩 host；Prisma Defender agent 補上 <em>runtime behavior</em> 的覆蓋（process spawn pattern、JNDI lookup、anomalous network connect）、代價是要在 host / pod / Lambda 上佈 agent、deployment 複雜度高一個層級。跟 <a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a> 比、Lacework 用 <em>Polygraph behavior graph</em> 做 host-level anomaly、focus 在 detection；Prisma 覆蓋面廣（含 IaC + CIEM + DSPM）、但每個模組深度比 Lacework 偵測單點淺一點。跟 <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a> 比、CrowdStrike 的 endpoint agent 已在多數 enterprise 環境跑、Cloud Security 直接共用 endpoint plane；Prisma 是 <em>cloud-first CNAPP 加上 agent</em>、不是 endpoint EDR 延伸。</p>
<p>關鍵張力：<em>覆蓋面廣度</em> ↔ <em>模組整合成熟度</em> 是 Prisma 客戶最常踩的 trade-off。五模組來自不同收購、UI / API / data model 整合仍在進行中、客戶常遇到「同一個 finding 在 Compute Console 顯示是 critical、在 CSPM 是 medium」、或「Code Security 報的 IaC issue 跟 Runtime 報的實際 config 對不起來」。預算允許就用 Prisma 拿覆蓋面廣度、不允許就走 Wiz（agentless 部署快）或 Lacework（單模組偵測深）。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Prisma Cloud 在 cloud security stack 中承擔哪幾段（Compute / CSPM / Code / Data / CIEM）、哪些跟既有 SIEM / EDR / IdP 重疊或互補</li>
<li>Defender agent 該佈在 host / pod / Lambda 哪幾層、跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> 等 OSS / SaaS image scanner 的分工</li>
<li>Compliance template（PCI / HIPAA / NIST / FedRAMP）跟自家 custom policy 的混用方式、誰能改 policy、誰 review</li>
<li>何時用 Prisma、何時改走 Wiz（agentless-only）/ Lacework（detection-focused）/ Trivy（OSS CLI）/ EDR</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Prisma 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>Defender agent 覆蓋率</strong>：production K8s cluster 的 DaemonSet 是否所有 node 跑、VM workload 的 agent install rate、Lambda function 的 extension 啟用比例；缺一塊就有 runtime 偵測盲點</li>
<li><strong>Console 跟模組一致性</strong>：Compute Console / CSPM dashboard / Code Security finding / CIEM report 同一個 resource 的風險評級是否一致、不一致時誰是 SSoT</li>
<li><strong>Compliance template 對齊</strong>：啟用了哪幾套（PCI-DSS / HIPAA / NIST 800-53 / CIS / FedRAMP / SOC2）、跟內部 baseline 的客製 rule 是否走版控</li>
<li><strong>Alert 跟 SOC handoff</strong>：Prisma alert 是進自家 incident queue 還是 forward 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>、Cortex XSOAR 是否串 playbook</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Defender agent deployment</strong>：agent 三種 footprint — <em>K8s DaemonSet</em>（每個 node 一個、攔截 container syscall + image runtime scan）、<em>VM / host agent</em>（Linux / Windows 安裝、file integrity + process anomaly）、<em>Lambda extension</em>（function runtime 注入、serverless 行為偵測）。Production 通常是 DaemonSet + VM agent 雙軌、Lambda extension 視 serverless workload 規模啟用。deployment 比 Wiz 多一步 — 要走 IaC（Helm chart / Terraform module）管 agent rollout、不能手動裝。</p>
<p><strong>Console 跟 RBAC</strong>：Prisma Cloud Console 是統一入口、但底下 <em>Compute</em>（前 Twistlock UI 殘留）跟 <em>Cloud</em>（CSPM / Code / Data / CIEM）兩個 plane 分開。RBAC 角色設計常踩坑 — Compute 的 collection（host group）跟 Cloud 的 account group 是不同概念、需要分別給權限。</p>
<p><strong>CSPM connector</strong>：CSPM 走 read-only cloud API（AWS Cross-Account Role / GCP Service Account / Azure App Registration）抽 config snapshot、定期 reconcile baseline。連 cloud account 是 onboarding 第一步、跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> / <a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a> 同樣的 pattern、Prisma 的 connector permission template 偏寬、要 review 哪些 API 真用得到。</p>
<p><strong>Code Security（Bridgecrew）</strong>：IaC scan 走 GitHub / GitLab / Bitbucket App、Terraform / CloudFormation / Kubernetes manifest / Dockerfile 在 PR 階段攔截 misconfiguration。Checkov 是 Bridgecrew 開源的底層引擎、Prisma 把 Checkov 規則庫 + 自家 policy 包成 SaaS。對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.6 release gate</a>、IaC issue 在 PR 階段擋比 runtime 抓便宜兩個量級。</p>
<p><strong>CIEM</strong>：cloud entitlement 治理、跨 AWS IAM / GCP IAM / Azure RBAC 找 over-permission 跟 toxic combination（例如 user 同時有 <code>iam:PassRole</code> + <code>lambda:CreateFunction</code> 可 privilege escalation）。CIEM 報告通常是大量「建議收權限」、實際 remediation 要跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> owner 排優先序、不是看到全收。</p>
<p><strong>Data Security（DSPM）</strong>：雲端資料庫（RDS / BigQuery / Snowflake）+ object store（S3 / GCS）的 sensitive data discovery、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 功能重疊。Prisma DSPM 的優勢是跟 CSPM / CIEM 同 Console、可以看「敏感資料所在的 bucket 是否有 public ACL + 哪些 role 可以讀」、是 <em>資料 + 入口 + 身份</em> 同 plane 的關聯。</p>
<p><strong>Runtime Protection（Aporeto microsegmentation）</strong>：Defender agent 提供 process / network level 的 runtime IDS / IPS — JNDI lookup 行為、異常 outbound callback、container escape 嘗試、unsigned binary 執行。比起 image-scan-only 多了 <em>已知 CVE 沒 patch 但 runtime 行為偵測到</em> 的覆蓋層。</p>
<p><strong>跟 Palo Alto 生態整合</strong>：Prisma alert 可直接打到 Palo Alto <em>Cortex XSOAR</em>（SOAR / playbook）/ <em>Cortex XDR</em>（endpoint + cloud unified detection）/ <em>NGFW / SASE</em>（firewall rule 自動 push）。對已是 Palo Alto-heavy 環境是生態一致性增加；對非 Palo Alto 環境、Prisma 也 forward 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> 走 webhook / Syslog。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Prisma Cloud</th>
          <th>Wiz</th>
          <th>Lacework</th>
          <th>CrowdStrike Falcon Cloud Security</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>Agent (Defender) + Agentless 雙軌</td>
          <td>Agentless-only（snapshot scan + API）</td>
          <td>Lightweight agent + agentless 雙軌</td>
          <td>沿用 CrowdStrike endpoint agent + agentless</td>
      </tr>
      <tr>
          <td>部署速度</td>
          <td>慢 — agent rollout 走 IaC + RBAC 設定多</td>
          <td>快 — connector 連 cloud account 就開始 scan</td>
          <td>中 — agent 較輕、但仍需安裝</td>
          <td>快（若已用 CrowdStrike）/ 中（新導入）</td>
      </tr>
      <tr>
          <td>Runtime 偵測</td>
          <td>強 — Defender 攔 syscall / network / file IDS</td>
          <td>弱 — runtime behavior 靠 snapshot 對照、延遲高</td>
          <td>強 — Polygraph behavior graph 為核心</td>
          <td>強 — endpoint agent runtime telemetry</td>
      </tr>
      <tr>
          <td>Posture / CSPM</td>
          <td>強 — Redlock 出身、compliance template 最完整</td>
          <td>強 — graph-based blast radius 視覺化最好</td>
          <td>中 — 有 CSPM 但 focus 在 detection</td>
          <td>中 — CSPM 後加入、深度比 Prisma / Wiz 淺</td>
      </tr>
      <tr>
          <td>IaC scan</td>
          <td>強 — Bridgecrew 整合、Checkov 底層</td>
          <td>中 — IaC scan 較新</td>
          <td>弱 — 非主力</td>
          <td>弱 — 非主力</td>
      </tr>
      <tr>
          <td>CIEM</td>
          <td>強 — 五模組原生</td>
          <td>強 — graph-based entitlement analysis</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>DSPM</td>
          <td>中 — Data Security 模組</td>
          <td>強 — DSPM 是近年強推</td>
          <td>弱</td>
          <td>弱</td>
      </tr>
      <tr>
          <td>模組整合成熟度</td>
          <td>中 — 五次收購、UI / data model 仍在整合</td>
          <td>強 — single platform 原生設計</td>
          <td>強 — 單一 data model</td>
          <td>中 — endpoint + cloud 整合中</td>
      </tr>
      <tr>
          <td>Compliance 廣度</td>
          <td>強 — PCI / HIPAA / NIST / FedRAMP / SOC2 完整</td>
          <td>中 — 主要 compliance 都有但模板較淺</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>生態整合</td>
          <td>強 — Palo Alto NGFW / Cortex XDR / XSOAR 同 plane</td>
          <td>中 — vendor-neutral、走 webhook / API</td>
          <td>中</td>
          <td>強 — CrowdStrike Falcon 生態</td>
      </tr>
      <tr>
          <td>計費複雜度</td>
          <td>高 — module + credit + workload + multi-year</td>
          <td>中 — workload / cloud account 為主</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Palo Alto-heavy、agent + posture 雙覆蓋、合規重</td>
          <td>Cloud-native、agentless-first、部署速度優先</td>
          <td>Detection-heavy、Polygraph anomaly 為核心</td>
          <td>CrowdStrike-heavy、endpoint + cloud 統一</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — agent + policy + Cortex 整合多</td>
          <td>中 — agentless、移除 connector 就乾淨</td>
          <td>中</td>
          <td>高（若深度整合 CrowdStrike）</td>
      </tr>
  </tbody>
</table>
<p>選 Prisma 的核心訴求：<em>已在 Palo Alto 生態（NGFW / SASE / Cortex XDR / XSOAR）+ 需要 runtime agent + posture 雙覆蓋 + compliance audit heavy（PCI / HIPAA / FedRAMP）</em>、且能承擔模組整合不完美 + 部署複雜度 + multi-year contract。純 agentless 用 Wiz、detection-focused 用 Lacework、純 OSS PR scan 用 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> + <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Defender agent rollout 策略</strong>：K8s DaemonSet 用 Helm chart 管 version + tolerations、staging cluster 先跑 1-2 週觀察 syscall overhead 跟 false positive、再 promote 到 production。VM agent 走 Ansible / SCCM / Terraform user-data、image baking 把 agent 包進 golden AMI 比每次 boot 安裝穩。Lambda extension 走 Lambda Layer、只對 high-value function 開（金流 / IdP / secret access）、不是全 function 都包。</p>
<p><strong>Runtime IDS / IPS 模式</strong>：Defender 可設 <em>audit only</em>（只 log、不阻擋）或 <em>prevent</em>（自動 kill process / block network）。production 多數 workload 走 audit、只對 <em>確定無誤報</em> 的 rule 開 prevent（已知 malware hash、明確 CVE exploit pattern）。誤判 prevent 業務 process 比放過 alert 代價高、應該預設 audit + SIEM forward + SOC triage。</p>
<p><strong>Compliance template + Custom policy 混用</strong>：Prisma 提供 PCI-DSS / HIPAA / NIST 800-53 / CIS Benchmark / FedRAMP / SOC2 完整 baseline、可直接啟用。但 baseline 通常太嚴或太寬、實務做法是 <em>fork baseline + 加自家 exception + 加 organization-specific rule</em>、走 Git 版控（policy as code、JSON / YAML）、PR review 後 sync 回 Console。policy 不能 console 直改、否則跟既有 SIEM rule 一樣失去 change history。</p>
<p><strong>Cortex XSOAR / XDR 整合</strong>：Prisma alert → XSOAR playbook 是 Palo Alto 環境的標準路徑、playbook 自動執行 enrichment（拉 threat intel）/ containment（NGFW block / disable IAM user）/ remediation（Terraform PR auto-create）。playbook 要走版控 + dry-run、高影響動作（disable IAM user / delete resource）走 approval gate、不能 fire-and-forget。</p>
<p><strong>計費結構</strong>：Prisma 按 <em>module 選購</em> + 按 <em>credit 消耗</em> + 按 <em>workload count</em> 三層計費、enterprise 通常是 <em>multi-year package</em>（3 年 commit 拿折扣）。實務坑 — 加新 cloud account 沒控管會吃 credit、Code Security 對大 monorepo scan 也吃 credit、Data Security 對大 bucket scan 是高成本項。月底常見的 sticker shock 來自 <em>Defender agent 數量爆衝</em>（K8s auto-scale 把 node count 推高）跟 <em>新 cloud account onboard 沒走 quota 控管</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Defender agent overhead 影響業務 process</strong>：DaemonSet pod 吃 CPU / memory 過高、container syscall hook 拖慢 latency-sensitive workload — 把 <em>runtime rule</em> 從 prevent 改 audit、調 collection scope 排除 latency-critical namespace、向 Palo Alto support 開 ticket 看 agent profile</li>
<li><strong>同一個 finding 模組評級不一致</strong>：Compute Console 顯示 critical、CSPM 顯示 medium — 確認 <em>SSoT 是哪個模組</em>、Compute 是 workload-level、CSPM 是 cloud-config-level、兩者本來就看不同 layer、用 Cortex XSOAR 統一 prioritization 而非靠 Console 對齊</li>
<li><strong>CSPM connector permission 報錯</strong>：Prisma 預設 IAM policy 太寬 / 太窄 — 走 <em>least privilege</em> 版本、跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> owner 確認哪些 API 真用到、不要照官方 template 全開</li>
<li><strong>Compliance template 一啟用就 1000+ finding</strong>：baseline 太嚴 + 既有環境本來就沒符合 — 走 <em>staged adoption</em>、先選 critical control（IAM / encryption / public exposure）、剩下進 backlog 排期、不要一次全開</li>
<li><strong>Code Security PR scan block 太多</strong>：Bridgecrew rule 對既有 IaC noisy — 用 <em>baseline mode</em>（既有 issue 標記、只 block 新 issue）、給 team 12 週 SLA 清 backlog、不要 day-1 block 全部</li>
<li><strong>CIEM 報告太多 over-permission</strong>：5000+ unused permission 看不完 — 排序看 <em>toxic combination</em>（privilege escalation path）優先、單純 unused 走每季 access review、不一次處理</li>
<li><strong>Cortex XSOAR playbook 誤殺</strong>：自動 disable IAM user 結果關到 CI/CD service account — 高影響動作走 <em>approval gate</em>、playbook default 是 <em>containment</em>（temporary block）not <em>deletion</em></li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 agentless / 部署速度優先</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a></td>
      </tr>
      <tr>
          <td>Polygraph behavior detection 為核心</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a></td>
      </tr>
      <tr>
          <td>CrowdStrike-heavy 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a></td>
      </tr>
      <tr>
          <td>純 OSS image / IaC scan</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a> / <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>DLP / sensitive data 為主</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>SIEM 偵測 / SOC</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>入口 WAF</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a></td>
      </tr>
      <tr>
          <td>事故 routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Defender agent 完整 syscall hook 清單 / runtime rule 語法 reference</li>
<li>Bridgecrew Checkov 規則庫的逐條解釋 / 自寫 Checkov rule 細節</li>
<li>Palo Alto Cortex XSOAR playbook 的 Python SDK 實作</li>
<li>Prisma SASE / NGFW / Cortex XDR 完整功能（屬 network security / EDR、不在 CNAPP 範圍）</li>
<li>Compliance 法規的逐條解讀（PCI-DSS / HIPAA 法律面）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Prisma 在 07 案例庫沒有直接 vendor-level 事件、但多個 supply chain / edge exposure case 是 Defender runtime + image scan 雙層的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Prisma 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Defender agent runtime 可偵測 JNDI lookup 行為、補 SBOM / image scan 看不到的 <em>dynamic class load 在 runtime 才觸發</em> 缺口、跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> image scan 互補</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>Defender runtime 偵測異常 process spawn + outbound C2 callback、補 image-level scan 對 <em>已簽章但 runtime 行為異常</em> 的缺口、不能只靠 IoC</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>runtime behavior anomaly（DNS beacon + dormant period）優於 IoC-only 規則、配合 image signing 雙層覆蓋、Defender + Code Security 在 build / runtime 雙閘</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023 Session Hijack</a></td>
          <td>Defender host-layer 偵測異常 session 動作（不能阻擋上游 edge zero-day、但事後 forensic 跟 lateral movement containment 有用）、補 edge appliance 看不到的 host-side 軌跡</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.10 供應鏈與第三方信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a>、<a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a>、<a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（Prisma alert forward）、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（CIEM remediation 落地）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a>（OSS image scan 互補）、<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>（SCA 互補）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.6 release gate</a>（Code Security PR scan）、<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（alert handoff）</li>
<li>官方：<a href="https://docs.prismacloud.io/">Prisma Cloud Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/</guid><description>&lt;p>這個案例的核心責任是說明「規模化 ticketing 平台」的長期工程議題 — 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a> 的「單一搶票事件」不同、BookMyShow 是 &lt;em>每天都有上百個 flash-sale 事件&lt;/em> 的平台、年售 2 億張票、跨 5 個國家。容量問題從「單一峰值」變成「峰值的常態化」、加上「資料層怎麼跟得上業務變化」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>BookMyShow 在 AWS 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/blogs/business-intelligence/how-bookmyshow-saved-80-in-costs-by-migrating-to-an-aws-modern-data-architecture/">BookMyShow AWS Migration Blog&lt;/a>）：&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>年售票量&lt;/td>
 &lt;td>2 億張 / 年（pre-COVID baseline）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>印度 + 斯里蘭卡 + 新加坡 + 印尼 + 中東&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移時程&lt;/td>
 &lt;td>4 個月完成&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>舊系統年數&lt;/td>
 &lt;td>15 年自建 analytics solution&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存成本下降&lt;/td>
 &lt;td>90%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分析成本下降&lt;/td>
 &lt;td>80%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料整合&lt;/td>
 &lt;td>從 80 TB 多份副本 → 單一 source of truth&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>資料架構：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data Lake&lt;/strong>：Amazon S3 統一儲存&lt;/li>
&lt;li>&lt;strong>Ingestion&lt;/strong>：Kafka consumers、AWS Glue ETL、AWS IoT Core（MQTT）&lt;/li>
&lt;li>&lt;strong>Processing&lt;/strong>：Amazon EMR（streaming permanent cluster + batch transient cluster）&lt;/li>
&lt;li>&lt;strong>Data Warehouse&lt;/strong>：Amazon Redshift + materialized views&lt;/li>
&lt;li>&lt;strong>Analytics&lt;/strong>：Amazon Athena（ad-hoc）+ Amazon QuickSight（dashboard）&lt;/li>
&lt;li>&lt;strong>ML&lt;/strong>：Amazon SageMaker（內容熱度、活動熱度、搜尋趨勢模型）&lt;/li>
&lt;li>&lt;strong>Orchestration&lt;/strong>：Amazon MWAA + AWS Step Functions&lt;/li>
&lt;/ul>
&lt;p>關鍵業務支撐：「sudden spikes with new movies or events launched」靠 serverless（S3、Glue、Athena、Step Functions、Lambda）自動擴容、無需人工介入。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>BookMyShow 案例揭露三個規模化 ticketing 平台的長期工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>單一搶票 → 常態多事件 = 架構從「為峰值設計」變「為流量分佈設計」&lt;/strong>：每天上百場電影 + 數十場演唱會 + 各種活動同時開票、每場都是 mini flash-sale。容量問題不再是「為一場演唱會準備」、而是「為每天上百個峰值同時準備」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling&lt;/a> 從單一 workload 變成 workload portfolio。&lt;/li>
&lt;li>&lt;strong>資料層比交易層更難擴&lt;/strong>：8 TB → 80 TB 過程中、舊 analytics 系統用 15 年才走到極限。交易層擴容靠 stateless EC2 + auto-scaling 相對容易、資料層 schema migration、ETL 重寫、報表回對都是長 lead time 工作。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 schema migration 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 的 cost attribution。&lt;/li>
&lt;li>&lt;strong>跨國市場 = 多重合規約束&lt;/strong>：印度、新加坡、印尼、中東各自有資料駐留 / 加密 / 報稅規則。S3 + EMR + Redshift 的「資料分區」不只是性能議題、也是合規議題。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 的合規容量規劃。&lt;/li>
&lt;/ol>
&lt;p>需要警惕的判讀盲點：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「規模化 ticketing 平台」的長期工程議題 — 跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的「單一搶票事件」不同、BookMyShow 是 <em>每天都有上百個 flash-sale 事件</em> 的平台、年售 2 億張票、跨 5 個國家。容量問題從「單一峰值」變成「峰值的常態化」、加上「資料層怎麼跟得上業務變化」。</p>
<h2 id="觀察">觀察</h2>
<p>BookMyShow 在 AWS 的關鍵敘述（引自 <a href="https://aws.amazon.com/blogs/business-intelligence/how-bookmyshow-saved-80-in-costs-by-migrating-to-an-aws-modern-data-architecture/">BookMyShow AWS Migration Blog</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>年售票量</td>
          <td>2 億張 / 年（pre-COVID baseline）</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>印度 + 斯里蘭卡 + 新加坡 + 印尼 + 中東</td>
      </tr>
      <tr>
          <td>遷移時程</td>
          <td>4 個月完成</td>
      </tr>
      <tr>
          <td>舊系統年數</td>
          <td>15 年自建 analytics solution</td>
      </tr>
      <tr>
          <td>儲存成本下降</td>
          <td>90%</td>
      </tr>
      <tr>
          <td>分析成本下降</td>
          <td>80%</td>
      </tr>
      <tr>
          <td>資料整合</td>
          <td>從 80 TB 多份副本 → 單一 source of truth</td>
      </tr>
  </tbody>
</table>
<p>資料架構：</p>
<ul>
<li><strong>Data Lake</strong>：Amazon S3 統一儲存</li>
<li><strong>Ingestion</strong>：Kafka consumers、AWS Glue ETL、AWS IoT Core（MQTT）</li>
<li><strong>Processing</strong>：Amazon EMR（streaming permanent cluster + batch transient cluster）</li>
<li><strong>Data Warehouse</strong>：Amazon Redshift + materialized views</li>
<li><strong>Analytics</strong>：Amazon Athena（ad-hoc）+ Amazon QuickSight（dashboard）</li>
<li><strong>ML</strong>：Amazon SageMaker（內容熱度、活動熱度、搜尋趨勢模型）</li>
<li><strong>Orchestration</strong>：Amazon MWAA + AWS Step Functions</li>
</ul>
<p>關鍵業務支撐：「sudden spikes with new movies or events launched」靠 serverless（S3、Glue、Athena、Step Functions、Lambda）自動擴容、無需人工介入。</p>
<h2 id="判讀">判讀</h2>
<p>BookMyShow 案例揭露三個規模化 ticketing 平台的長期工程重點。</p>
<ol>
<li><strong>單一搶票 → 常態多事件 = 架構從「為峰值設計」變「為流量分佈設計」</strong>：每天上百場電影 + 數十場演唱會 + 各種活動同時開票、每場都是 mini flash-sale。容量問題不再是「為一場演唱會準備」、而是「為每天上百個峰值同時準備」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a> 從單一 workload 變成 workload portfolio。</li>
<li><strong>資料層比交易層更難擴</strong>：8 TB → 80 TB 過程中、舊 analytics 系統用 15 年才走到極限。交易層擴容靠 stateless EC2 + auto-scaling 相對容易、資料層 schema migration、ETL 重寫、報表回對都是長 lead time 工作。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema migration 與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 的 cost attribution。</li>
<li><strong>跨國市場 = 多重合規約束</strong>：印度、新加坡、印尼、中東各自有資料駐留 / 加密 / 報稅規則。S3 + EMR + Redshift 的「資料分區」不只是性能議題、也是合規議題。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 的合規容量規劃。</li>
</ol>
<p>需要警惕的判讀盲點：</p>
<ul>
<li>「年售 2 億張」是 <em>年度總和</em>、不是 <em>峰值</em>。實際單秒峰值（板球比賽決賽開票、寶萊塢新片首映）案例本身沒揭露。</li>
<li>案例聚焦在 <em>資料分析層</em> 的遷移、不是 <em>交易層</em> 的 flash-sale 設計。讀者若想學「單場 flash-sale 怎麼撐」、應該回 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 或 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a>。</li>
<li>「80% 成本下降」是 <em>vs 15 年舊系統</em>、不是 <em>vs 競爭對手</em>。舊系統的儲存效率、運維成本本來就低、改善幅度部分來自「現代化紅利」、不只是 AWS 服務本身。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>大規模 ticketing 平台要分「交易層」跟「資料層」兩條容量規劃</strong>：交易層為單一 event flash-sale 設計（<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15</a> / <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16</a> 模式）；資料層為「上千場活動的長期分析」設計（BookMyShow 模式）。兩者用不同服務、不同 SLO。</li>
<li><strong>跨國平台先解決資料駐留、再規劃跨國 analytics</strong>：印度資料不能搬到新加坡分析、合規必須各國資料本地處理、再彙整 metadata。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>。</li>
<li><strong>serverless data stack 是 ticketing 平台的長期方向</strong>：S3 + Glue + Athena + Step Functions 的成本曲線比 EMR cluster 平穩、沒事件時近乎 0、有事件時自動擴。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
<li><strong>遷移時程 4 個月 = 計畫密度極高</strong>：15 年資產 4 個月遷完不是常態、需要先把 <em>資料模型</em> canonical 化、再 batch 平行遷。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> 的 schema 對映先行。</li>
</ol>
<p>跨平台等效：GCP BigQuery + Dataflow + Cloud Storage + Pub/Sub 是對等 stack；Azure Synapse + Data Lake + Event Hubs；自建 Delta Lake + Spark + Kafka 都可以實作對等架構。差異是 vendor 整合度跟 serverless 透明度。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃多事件 ticketing 平台 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想看單一 flash-sale 設計 → <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> + <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a></li>
<li>想做跨國合規容量規劃 → <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> + <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a></li>
<li>想做大規模 migration → <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> + <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 的容量規劃成本">9.C9 Spotify migration</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/business-intelligence/how-bookmyshow-saved-80-in-costs-by-migrating-to-an-aws-modern-data-architecture/">How BookMyShow saved 80% in costs by migrating to an AWS modern data architecture</a></li>
<li><a href="https://aws.amazon.com/architecture/analytics-big-data/">AWS Modern Data Architecture</a></li>
</ul>
]]></content:encoded></item><item><title>4.17 Telemetry Data Quality</title><link>https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>telemetry data quality 的責任：確認觀測資料本身可信&lt;/li>
&lt;li>缺漏類型：missing signal、partial trace、dropped log、stale metric&lt;/li>
&lt;li>漂移類型：schema drift、label drift、service name drift、semantic convention drift&lt;/li>
&lt;li>偏誤類型：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> bias、low-traffic bias、high-cardinality truncation&lt;/li>
&lt;li>時間類型：clock skew、ingest delay、out-of-order event、timezone mismatch&lt;/li>
&lt;li>品質指標：completeness、freshness、consistency、accuracy、coverage&lt;/li>
&lt;li>跟 4.11 telemetry pipeline 的分工：pipeline 看路徑，data quality 看資料可信度&lt;/li>
&lt;li>反模式：dashboard 看起來正常但資料少一半；trace sample 漏掉錯誤；timestamp 導致 timeline 錯序&lt;/li>
&lt;/ul>
&lt;p>Telemetry data quality 的核心是把「觀測資料失真」當成一級事件。服務事故判讀建立在觀測資料上，資料品質不穩時，團隊會把資料缺口誤讀成系統行為，進而做出錯誤分級、錯誤回復或錯誤 SLO 判斷。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Telemetry data quality 是把觀測資料當成資料產品治理的能力，責任是讓 log、metric、trace 與 alert 的判讀建立在可信資料上。&lt;/p>
&lt;p>這一頁處理的是資料可信度。訊號存在不等於訊號可信；缺漏、漂移、偏誤與時間錯位都會讓事故判讀走向錯誤路徑。&lt;/p>
&lt;p>資料品質治理最有效的做法是把品質指標產品化：讓 completeness、freshness、drift、sampling coverage 也進 dashboard 與告警，讓團隊在事故前就能看見資料限制。&lt;/p>
&lt;h2 id="品質模型">品質模型&lt;/h2>
&lt;p>Telemetry data quality 的品質模型由五個面向組成。這五個面向分別回答資料是否存在、是否及時、是否一致、是否代表真實流量，以及是否足以覆蓋關鍵旅程。&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>Completeness&lt;/td>
 &lt;td>該出現的訊號是否完整出現&lt;/td>
 &lt;td>drop rate、coverage、gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>訊號是否足夠接近事件發生時間&lt;/td>
 &lt;td>ingest delay、stale metric&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency&lt;/td>
 &lt;td>欄位、命名與語意是否跨服務一致&lt;/td>
 &lt;td>schema drift、label drift&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Accuracy&lt;/td>
 &lt;td>數值與事件語意是否反映真實狀態&lt;/td>
 &lt;td>duplicate event、wrong unit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Coverage&lt;/td>
 &lt;td>高風險旅程與低流量邊界是否被涵蓋&lt;/td>
 &lt;td>sampling policy、trace ratio&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Completeness 是事故判讀的基礎。log、metric 或 trace 的缺口如果沒有被標示，dashboard 會呈現一條看似平順的線，實際上可能只是 ingestion pipeline 丟了資料。&lt;/p>
&lt;p>Freshness 決定資料能否支援事中決策。告警延遲、metric scrape delay、trace export queue backlog 與 log indexing lag 都會讓 incident commander 用過期資料判斷是否擴大或回復。&lt;/p>
&lt;p>Consistency 決定資料能否跨服務拼接。service name、region、tenant、environment、error class 與 semantic convention 若在不同系統漂移，單一服務看起來正常，跨服務事件鏈卻會斷裂。&lt;/p>
&lt;p>Accuracy 決定資料能否代表真實狀態。常見問題包含錯誤單位、重複計數、counter reset 誤判、histogram bucket 設錯與 status code mapping 錯誤。&lt;/p>
&lt;p>Coverage 決定資料能否覆蓋高風險邊界。低流量服務、VIP tenant、錯誤樣本、長尾 latency 與 rare dependency failure 常被 sampling 或聚合策略稀釋。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 telemetry data quality 時，先看資料是否完整與新鮮，再看不同訊號之間是否能互相對齊。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>log / metric / trace 是否有 coverage 與 drop rate&lt;/li>
&lt;li>schema 是否有版本與 drift 偵測&lt;/li>
&lt;li>sampling 是否保留錯誤、高延遲與低流量樣本&lt;/li>
&lt;li>timestamp 是否能支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;li>dashboard 是否標示資料延遲、缺口與查詢範圍&lt;/li>
&lt;/ul>
&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>完整性&lt;/td>
 &lt;td>drop rate、coverage 可被量測&lt;/td>
 &lt;td>事故定位依賴不完整證據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性&lt;/td>
 &lt;td>欄位語意與命名跨服務一致&lt;/td>
 &lt;td>事件鏈需要人工拼接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>代表性&lt;/td>
 &lt;td>sampling 覆蓋高風險樣本&lt;/td>
 &lt;td>錯誤被平均化，誤判風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間性&lt;/td>
 &lt;td>timestamp 與 delay 可追蹤&lt;/td>
 &lt;td>timeline 錯序，決策先後顛倒&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="缺漏與漂移">缺漏與漂移&lt;/h2>
&lt;p>缺漏是 telemetry data quality 最容易造成錯誤安全感的問題。缺漏發生時，圖表通常不會直接報錯，而是呈現較低的流量、較少的錯誤或不完整的 trace。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>telemetry data quality 的責任：確認觀測資料本身可信</li>
<li>缺漏類型：missing signal、partial trace、dropped log、stale metric</li>
<li>漂移類型：schema drift、label drift、service name drift、semantic convention drift</li>
<li>偏誤類型：<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> bias、low-traffic bias、high-cardinality truncation</li>
<li>時間類型：clock skew、ingest delay、out-of-order event、timezone mismatch</li>
<li>品質指標：completeness、freshness、consistency、accuracy、coverage</li>
<li>跟 4.11 telemetry pipeline 的分工：pipeline 看路徑，data quality 看資料可信度</li>
<li>反模式：dashboard 看起來正常但資料少一半；trace sample 漏掉錯誤；timestamp 導致 timeline 錯序</li>
</ul>
<p>Telemetry data quality 的核心是把「觀測資料失真」當成一級事件。服務事故判讀建立在觀測資料上，資料品質不穩時，團隊會把資料缺口誤讀成系統行為，進而做出錯誤分級、錯誤回復或錯誤 SLO 判斷。</p>
<h2 id="概念定位">概念定位</h2>
<p>Telemetry data quality 是把觀測資料當成資料產品治理的能力，責任是讓 log、metric、trace 與 alert 的判讀建立在可信資料上。</p>
<p>這一頁處理的是資料可信度。訊號存在不等於訊號可信；缺漏、漂移、偏誤與時間錯位都會讓事故判讀走向錯誤路徑。</p>
<p>資料品質治理最有效的做法是把品質指標產品化：讓 completeness、freshness、drift、sampling coverage 也進 dashboard 與告警，讓團隊在事故前就能看見資料限制。</p>
<h2 id="品質模型">品質模型</h2>
<p>Telemetry data quality 的品質模型由五個面向組成。這五個面向分別回答資料是否存在、是否及時、是否一致、是否代表真實流量，以及是否足以覆蓋關鍵旅程。</p>
<table>
  <thead>
      <tr>
          <th>品質面向</th>
          <th>核心問題</th>
          <th>常見資料</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Completeness</td>
          <td>該出現的訊號是否完整出現</td>
          <td>drop rate、coverage、gap</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>訊號是否足夠接近事件發生時間</td>
          <td>ingest delay、stale metric</td>
      </tr>
      <tr>
          <td>Consistency</td>
          <td>欄位、命名與語意是否跨服務一致</td>
          <td>schema drift、label drift</td>
      </tr>
      <tr>
          <td>Accuracy</td>
          <td>數值與事件語意是否反映真實狀態</td>
          <td>duplicate event、wrong unit</td>
      </tr>
      <tr>
          <td>Coverage</td>
          <td>高風險旅程與低流量邊界是否被涵蓋</td>
          <td>sampling policy、trace ratio</td>
      </tr>
  </tbody>
</table>
<p>Completeness 是事故判讀的基礎。log、metric 或 trace 的缺口如果沒有被標示，dashboard 會呈現一條看似平順的線，實際上可能只是 ingestion pipeline 丟了資料。</p>
<p>Freshness 決定資料能否支援事中決策。告警延遲、metric scrape delay、trace export queue backlog 與 log indexing lag 都會讓 incident commander 用過期資料判斷是否擴大或回復。</p>
<p>Consistency 決定資料能否跨服務拼接。service name、region、tenant、environment、error class 與 semantic convention 若在不同系統漂移，單一服務看起來正常，跨服務事件鏈卻會斷裂。</p>
<p>Accuracy 決定資料能否代表真實狀態。常見問題包含錯誤單位、重複計數、counter reset 誤判、histogram bucket 設錯與 status code mapping 錯誤。</p>
<p>Coverage 決定資料能否覆蓋高風險邊界。低流量服務、VIP tenant、錯誤樣本、長尾 latency 與 rare dependency failure 常被 sampling 或聚合策略稀釋。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 telemetry data quality 時，先看資料是否完整與新鮮，再看不同訊號之間是否能互相對齊。</p>
<p>重點訊號包括：</p>
<ul>
<li>log / metric / trace 是否有 coverage 與 drop rate</li>
<li>schema 是否有版本與 drift 偵測</li>
<li>sampling 是否保留錯誤、高延遲與低流量樣本</li>
<li>timestamp 是否能支援 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
<li>dashboard 是否標示資料延遲、缺口與查詢範圍</li>
</ul>
<table>
  <thead>
      <tr>
          <th>品質面向</th>
          <th>最小可用判準</th>
          <th>失真後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完整性</td>
          <td>drop rate、coverage 可被量測</td>
          <td>事故定位依賴不完整證據</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>欄位語意與命名跨服務一致</td>
          <td>事件鏈需要人工拼接</td>
      </tr>
      <tr>
          <td>代表性</td>
          <td>sampling 覆蓋高風險樣本</td>
          <td>錯誤被平均化，誤判風險</td>
      </tr>
      <tr>
          <td>時間性</td>
          <td>timestamp 與 delay 可追蹤</td>
          <td>timeline 錯序，決策先後顛倒</td>
      </tr>
  </tbody>
</table>
<h2 id="缺漏與漂移">缺漏與漂移</h2>
<p>缺漏是 telemetry data quality 最容易造成錯誤安全感的問題。缺漏發生時，圖表通常不會直接報錯，而是呈現較低的流量、較少的錯誤或不完整的 trace。</p>
<table>
  <thead>
      <tr>
          <th>缺漏類型</th>
          <th>真實服務樣貌</th>
          <th>判讀風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Missing signal</td>
          <td>新服務路徑沒有 instrument</td>
          <td>核心旅程失敗但 dashboard 正常</td>
      </tr>
      <tr>
          <td>Partial trace</td>
          <td>async job 或 queue consumer 缺 span</td>
          <td>事件鏈停在同步 request</td>
      </tr>
      <tr>
          <td>Dropped log</td>
          <td>ingest burst 時 log 被丟棄</td>
          <td>錯誤率下降被誤判為恢復</td>
      </tr>
      <tr>
          <td>Stale metric</td>
          <td>scrape 成功但資料停在舊 timestamp</td>
          <td>incident timeline 被拉歪</td>
      </tr>
  </tbody>
</table>
<p>Missing signal 代表觀測需求沒有覆蓋服務路徑。常見場景是新 feature flag 開啟後走到新 code path，但 SLI、log schema 與 trace 還停在舊路徑。</p>
<p>Partial trace 代表跨邊界 context 缺少完整傳遞。request 進入 queue 後，如果 message 缺少 correlation id 或 consumer 缺少 span，團隊只能知道 request 發出去，背景流程的失敗時間與失敗點會留在盲區。</p>
<p>Dropped log 代表資料流量超過 pipeline 或成本限制。burst error 發生時，如果 log pipeline 開始 sampling 或丟棄，事故團隊看到的錯誤量會比真實狀態少。</p>
<p>Schema drift 是長期維護最常見的品質問題。欄位改名、label 粒度改變、service name 不一致、semantic convention 升級，都會讓查詢與 dashboard 在沒有明顯錯誤的情況下失準。</p>
<h2 id="sampling-與代表性">Sampling 與代表性</h2>
<p>本段聚焦 sampling 對資料品質的失真風險；sampling 策略（Head / Tail / Adaptive / Exemplar）的 SSoT 在 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Sampling 策略</a>。</p>
<p>Sampling 的責任是控制觀測成本，同時保留足以判讀的高價值樣本。sampling policy 若只按固定比例抽樣，最容易丟掉低頻但高風險的事件。</p>
<table>
  <thead>
      <tr>
          <th>Sampling 風險</th>
          <th>失真方式</th>
          <th>控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Low-traffic bias</td>
          <td>低流量服務樣本太少</td>
          <td>對低流量服務設定 minimum sample floor</td>
      </tr>
      <tr>
          <td>Error sample loss</td>
          <td>錯誤 request 被普通比例抽掉</td>
          <td>對 error、timeout、high latency 強制保留</td>
      </tr>
      <tr>
          <td>Tenant skew</td>
          <td>大 tenant 壓過小 tenant</td>
          <td>以 tenant 或 plan 做分層 sampling</td>
      </tr>
      <tr>
          <td>Cardinality truncation</td>
          <td>高維度 label 被截斷或合併</td>
          <td>標示 truncation，保留 top-K 與 overflow</td>
      </tr>
      <tr>
          <td>Tail latency loss</td>
          <td>長尾 latency 被平均值掩蓋</td>
          <td>使用 histogram 與 exemplar</td>
      </tr>
  </tbody>
</table>
<p>Low-traffic bias 會讓小服務或小 tenant 的問題長期不可見。這些路徑平時量小，但可能承擔高價值客戶、管理操作或資安事件；抽樣策略需要保留最低樣本量。</p>
<p>Error sample loss 會直接破壞事故判讀。錯誤、timeout、retry exhausted、DLQ、payment failure 與 authorization failure 應該有更高保留權重，因為它們代表決策價值高於普通成功 request。</p>
<p>Cardinality truncation 需要明確揭露。當平台為了成本截斷 label 或聚合 tenant 維度時，dashboard 應標示資料限制，讓讀者知道當下看的是聚合視角與可用粒度。</p>
<h2 id="時間對齊">時間對齊</h2>
<p>時間對齊是 incident timeline 的基礎能力。事件發生時間、採集時間、寫入時間、查詢時間與顯示時區若未分清，事故復盤會把原因與結果順序看反。</p>
<table>
  <thead>
      <tr>
          <th>時間問題</th>
          <th>常見來源</th>
          <th>事故後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Clock skew</td>
          <td>host、container、client 時鐘不同</td>
          <td>事件先後被重排</td>
      </tr>
      <tr>
          <td>Ingest delay</td>
          <td>exporter queue 或 indexing lag</td>
          <td>告警與圖表晚於真實事件</td>
      </tr>
      <tr>
          <td>Out-of-order event</td>
          <td>async pipeline 或 retry 寫入</td>
          <td>同一 trace 的 span 順序錯亂</td>
      </tr>
      <tr>
          <td>Timezone mismatch</td>
          <td>人工紀錄與平台顯示時區不同</td>
          <td>對外通訊與內部 timeline 衝突</td>
      </tr>
  </tbody>
</table>
<p>Clock skew 會讓跨服務事件鏈失去可信度。若 API、worker、database proxy 與 observability collector 的時間基準不同，trace 中的等待點可能看起來是負時間或錯誤順序。</p>
<p>Ingest delay 會影響事中決策。incident commander 看到 error rate 下降時，需要知道資料是即時下降，還是 pipeline 還沒收完高峰區段。</p>
<p>Timezone mismatch 常出現在 status page、support ticket、vendor notice 與內部 timeline 對接時。所有事故證據都應保留原始時間與標準化時間，避免復盤時重排錯誤。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一事故在 log、metric、trace 中呈現不同時間線</li>
<li>service name / region / tenant label 在不同系統拼不起來</li>
<li>低流量服務的錯誤被 sampling 稀釋</li>
<li>pipeline drop 發生但 dashboard 沒提示資料缺口</li>
<li>post-incident review 發現判讀基於不完整資料</li>
</ul>
<p>常見場景是「圖看起來穩，但資料在悄悄掉」。例如 ingest 層 partial drop 後 error rate 下降，看似健康，實際是訊號少了高風險區段。這類情況若沒有資料品質指標，會讓事故決策建立在錯誤安全感上。</p>
<h2 id="控制面">控制面</h2>
<p>Telemetry data quality 的控制面是把資料限制顯性化。資料品質不需要追求完美，但需要讓讀者知道目前能相信什麼、限制在哪裡、何時需要改用其他 evidence。</p>
<ol>
<li>為每種 telemetry 設定品質指標。</li>
<li>在 dashboard 標示 freshness、coverage 與 known gap。</li>
<li>對 schema drift、drop rate 與 sampling policy 建立告警。</li>
<li>在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a> 記錄資料限制。</li>
<li>在 post-incident review 中回寫造成判讀錯誤的資料品質缺口。</li>
</ol>
<p>品質指標本身也需要 owner。平台團隊可以維護 pipeline drop、ingest delay 與 semantic convention；服務團隊需要維護 service-specific schema、business event 與 user journey coverage。</p>
<p>資料限制應直接出現在操作入口。若某 dashboard 的 trace sample 只保留 10%、某 tenant label 被聚合、某時間區段有 log gap，讀者應在同一個畫面看到限制，並把限制納入當下決策。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Telemetry data quality 的反模式來自把查詢結果視為事實本身。查詢結果只是資料產品的輸出，仍然受採集、轉換、抽樣、儲存與查詢限制影響。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dashboard 即事實</td>
          <td>圖表下降就判斷服務恢復</td>
          <td>顯示資料延遲與 coverage</td>
      </tr>
      <tr>
          <td>schema 漂移無治理</td>
          <td>查詢突然少資料但沒人知道</td>
          <td>欄位版本與 drift 偵測</td>
      </tr>
      <tr>
          <td>sampling policy 黑箱</td>
          <td>錯誤樣本被抽掉仍用比例推估</td>
          <td>公開 sampling policy 與例外規則</td>
      </tr>
      <tr>
          <td>timeline 單時間戳</td>
          <td>只記顯示時間，不記事件原始時間</td>
          <td>同時保留 event / ingest / query</td>
      </tr>
      <tr>
          <td>成本截斷不標示</td>
          <td>高 cardinality 被合併但仍當完整資料</td>
          <td>標示 truncation 與聚合粒度</td>
      </tr>
  </tbody>
</table>
<p>dashboard 即事實會讓事故決策失去資料謙遜。圖表顯示健康時，仍要確認資料有沒有缺口、延遲或抽樣偏誤，尤其在 pipeline 自身承受壓力時。</p>
<p>sampling policy 黑箱會降低服務團隊的風險判讀品質。平台可以為成本抽樣，但抽樣規則要能被服務團隊理解，並且允許錯誤、高延遲與低流量關鍵路徑保留更高權重。</p>
<h2 id="遷移期的雙軌對照驗證">遷移期的雙軌對照驗證</h2>
<p>觀測平台遷移是資料品質最容易失分的窗口。新舊管線並存期間，若沒有顯式對照驗證，語意漂移會在 dashboard 看起來「都有資料」的情況下緩慢偏離，直到事故時才浮現。</p>
<p>雙軌對照的核心責任是把新管線當被檢驗的對象、用舊管線作為對照基準。新舊管線同時採集相同訊號、用相同 query 對照 error rate、p95 latency、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、trace coverage 是否一致；偏差超過閾值時先停止下一步遷移、保留證據後再決定下一步。</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 相容遷移實務</a>：揭露「先建立雙軌採集對照、用品質指標決定何時關閉舊管線」的做法。對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel 遷移訊號漂移反例</a>：揭露遷移失敗的主要風險來自語意漂移 — metric 名稱、label、sampling、aggregation 在新舊管線間出現微小差異，導致同一現象被歸到不同 service / label / latency bucket。</p>
<p>可重複套用的對照驗證做法：</p>
<ol>
<li><strong>固定一組 baseline query</strong>：選定關鍵服務的核心 SLI query（error rate、p99 latency、throughput），新舊管線各跑一份、定期比對。</li>
<li><strong>設定偏差閾值</strong>：每個 SLI 設可接受偏差（例如 ±5%）。超過閾值的時段標記為待調查，不能無視。</li>
<li><strong>追蹤 missing signal 比例</strong>：missing span、missing metric、missing log 的比例是漂移的早期指標。比例持續上升時，停止下一批服務切換。</li>
<li><strong>退出條件顯式化</strong>：「對照偏差連續 N 天 &lt; X%」作為關閉舊管線的退出條件，把雙軌期變成有界的、不是無限延長。</li>
</ol>
<p>遷移期的告警條件本身也是治理項目。新舊管線對同服務的 error rate 長期偏離、missing span / missing metric 比例持續上升、同一事件在兩套 dashboard 得到相反結論、這些都該成為高優先告警、讓漂移在發生當下即時可見、避免堆積到 retrospective 才被注意。</p>
<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> 處理，本章關注的是品質指標的對照設計。</p>
<h2 id="與-slo-和事故的關係">與 SLO 和事故的關係</h2>
<p>Telemetry data quality 是 SLO 與事故 evidence 的可信度前提。SLI 若建立在失真資料上，<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a>、burn rate alert 與 release freeze 都會被錯誤資料牽動。</p>
<p>在 SLO 場景中，資料品質缺口會直接改變可靠性政策。若 availability SLI 漏掉 mobile client、region label 漂移、error sample 被抽掉，團隊會高估可靠性並繼續放行高風險變更。</p>
<p>在事故場景中，資料品質限制需要進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a>。當 IC 做出升級、降級、等待或 rollback 決策時，應同時記錄當下 evidence 的 completeness、freshness 與 confidence。</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>：治理欄位漂移</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 / cost</a>：sampling 策略矩陣、高維度截斷與成本取捨</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>：追查 drop、delay 與 ingest 問題</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：避免模型學到偏誤資料</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：品質指標的 platform / service ownership 邊界</li>
<li><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a>：標記事中判讀使用的資料品質限制</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：pre-aggregation 跟 raw data 的一致性驗證</li>
<li><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13 Discord 儲存→觀測缺口</a>：儲存演進反覆暴露觀測盲區的教訓</li>
</ul>
]]></content:encoded></item><item><title>6.17 Feature Flag Governance</title><link>https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Feature flag 在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 之後提供 runtime 層的細粒度控制。Flag governance 把這個控制從單次開關提升為有生命週期的 artifact，涵蓋灰度、實驗與緊急止血的風險管理。&lt;/p>
&lt;p>當 flag 變多，真正的風險是狀態分支不透明、技術債累積與權限混用。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>Flag governance 的健康度先看旗標角色是否分離，再看移除與審計是否有固定流程。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>release / experiment / ops / permission 是否分流&lt;/li>
&lt;li>stale flag 是否有回收機制&lt;/li>
&lt;li>progressive rollout 是否有可觀測的 cohort&lt;/li>
&lt;li>flag 變更是否可審計、可追責&lt;/li>
&lt;/ul>
&lt;h2 id="flag-角色分類">Flag 角色分類&lt;/h2>
&lt;p>Flag 按用途分離，不同角色的 lifecycle、權限與治理策略差異顯著。混用會讓審計失真、移除困難、權限控制失效。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>角色&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>Lifecycle 預期&lt;/th>
 &lt;th>Owner&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Release flag&lt;/td>
 &lt;td>控制新功能是否對使用者可見&lt;/td>
 &lt;td>天到週&lt;/td>
 &lt;td>功能團隊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Experiment flag&lt;/td>
 &lt;td>控制 A/B test 流量分配&lt;/td>
 &lt;td>週到月&lt;/td>
 &lt;td>實驗平台團隊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ops flag&lt;/td>
 &lt;td>緊急止血、降級、流量限制&lt;/td>
 &lt;td>長期存在&lt;/td>
 &lt;td>SRE / 值班&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission flag&lt;/td>
 &lt;td>控制使用者 / 租戶功能存取&lt;/td>
 &lt;td>跟隨權限策略&lt;/td>
 &lt;td>產品 / IAM&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Release flag&lt;/strong> 上線後應在固定時限內收斂為 always-on 或移除。它的存在意義是灰度期間的安全網。灰度結束後，flag 的控制作用消失，只剩代碼分支 — 這段分支就是 flag debt 的來源。&lt;/p>
&lt;p>&lt;strong>Experiment flag&lt;/strong> 的 lifecycle 受實驗週期決定。實驗結束後，flag 應收斂為勝出變體的行為並移除。實驗 flag 的特殊風險是依賴統計引擎的流量分配 — 引擎異常時，flag 的行為取決於 fallback 配置。&lt;/p>
&lt;p>&lt;strong>Ops flag&lt;/strong> 是長期存在的 kill switch 與降級控制。它與其他三類 flag 的關鍵差異是觸發頻率低但影響範圍大 — 觸發時通常是事故情境，需要秒級生效與審計紀錄。ops flag 的設計需求見下方「Kill switch 設計」段。&lt;/p>
&lt;p>&lt;strong>Permission flag&lt;/strong> 本質是權限控制，應走 RBAC 或 entitlement 系統。當 permission flag 混入 feature flag 系統，功能存取權會繞過正式權限審核流程 — 修改一個 flag 值就能改變租戶的功能範圍，沒有對應的審計軌跡。判斷標準：如果 flag 的值決定「誰能用什麼功能」，它是 permission，應該從 feature flag 系統遷移到權限系統。&lt;/p>
&lt;h2 id="lifecycle-管理">Lifecycle 管理&lt;/h2>
&lt;p>Flag 的生命週期是 create → rollout → converge → remove。每個階段有明確的輸入與交付物。&lt;/p>
&lt;p>&lt;strong>Create&lt;/strong>：flag 建立時記錄 owner、用途分類（release / experiment / ops）、預計移除日期與關聯 ticket。這些 metadata 是後續治理的基礎 — 沒有 owner 的 flag 在移除階段會變成無人認領的 debt。&lt;/p>
&lt;p>&lt;strong>Rollout&lt;/strong>：progressive rollout 按 percentage、cohort 或 region 逐步放量。每一步有可觀測指標確認行為正常 — error rate、latency、business KPI。rollout 節奏跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 的放行條件對齊：gate 通過後用 flag 做細粒度控制，flag 異常時 gate 提供回退依據。&lt;/p>
&lt;p>&lt;strong>Converge&lt;/strong>：功能穩定後，flag 設定 100%（always-on）或 0%（移除功能）。此時 flag 已無控制作用，只是代碼中的條件分支。converge 階段是 flag 治理的關鍵轉折 — 很多 flag 停在這裡不再前進，持續佔用代碼路徑。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Feature flag 在 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 之後提供 runtime 層的細粒度控制。Flag governance 把這個控制從單次開關提升為有生命週期的 artifact，涵蓋灰度、實驗與緊急止血的風險管理。</p>
<p>當 flag 變多，真正的風險是狀態分支不透明、技術債累積與權限混用。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Flag governance 的健康度先看旗標角色是否分離，再看移除與審計是否有固定流程。</p>
<p>重點訊號包括：</p>
<ul>
<li>release / experiment / ops / permission 是否分流</li>
<li>stale flag 是否有回收機制</li>
<li>progressive rollout 是否有可觀測的 cohort</li>
<li>flag 變更是否可審計、可追責</li>
</ul>
<h2 id="flag-角色分類">Flag 角色分類</h2>
<p>Flag 按用途分離，不同角色的 lifecycle、權限與治理策略差異顯著。混用會讓審計失真、移除困難、權限控制失效。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>責任</th>
          <th>Lifecycle 預期</th>
          <th>Owner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Release flag</td>
          <td>控制新功能是否對使用者可見</td>
          <td>天到週</td>
          <td>功能團隊</td>
      </tr>
      <tr>
          <td>Experiment flag</td>
          <td>控制 A/B test 流量分配</td>
          <td>週到月</td>
          <td>實驗平台團隊</td>
      </tr>
      <tr>
          <td>Ops flag</td>
          <td>緊急止血、降級、流量限制</td>
          <td>長期存在</td>
          <td>SRE / 值班</td>
      </tr>
      <tr>
          <td>Permission flag</td>
          <td>控制使用者 / 租戶功能存取</td>
          <td>跟隨權限策略</td>
          <td>產品 / IAM</td>
      </tr>
  </tbody>
</table>
<p><strong>Release flag</strong> 上線後應在固定時限內收斂為 always-on 或移除。它的存在意義是灰度期間的安全網。灰度結束後，flag 的控制作用消失，只剩代碼分支 — 這段分支就是 flag debt 的來源。</p>
<p><strong>Experiment flag</strong> 的 lifecycle 受實驗週期決定。實驗結束後，flag 應收斂為勝出變體的行為並移除。實驗 flag 的特殊風險是依賴統計引擎的流量分配 — 引擎異常時，flag 的行為取決於 fallback 配置。</p>
<p><strong>Ops flag</strong> 是長期存在的 kill switch 與降級控制。它與其他三類 flag 的關鍵差異是觸發頻率低但影響範圍大 — 觸發時通常是事故情境，需要秒級生效與審計紀錄。ops flag 的設計需求見下方「Kill switch 設計」段。</p>
<p><strong>Permission flag</strong> 本質是權限控制，應走 RBAC 或 entitlement 系統。當 permission flag 混入 feature flag 系統，功能存取權會繞過正式權限審核流程 — 修改一個 flag 值就能改變租戶的功能範圍，沒有對應的審計軌跡。判斷標準：如果 flag 的值決定「誰能用什麼功能」，它是 permission，應該從 feature flag 系統遷移到權限系統。</p>
<h2 id="lifecycle-管理">Lifecycle 管理</h2>
<p>Flag 的生命週期是 create → rollout → converge → remove。每個階段有明確的輸入與交付物。</p>
<p><strong>Create</strong>：flag 建立時記錄 owner、用途分類（release / experiment / ops）、預計移除日期與關聯 ticket。這些 metadata 是後續治理的基礎 — 沒有 owner 的 flag 在移除階段會變成無人認領的 debt。</p>
<p><strong>Rollout</strong>：progressive rollout 按 percentage、cohort 或 region 逐步放量。每一步有可觀測指標確認行為正常 — error rate、latency、business KPI。rollout 節奏跟 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的放行條件對齊：gate 通過後用 flag 做細粒度控制，flag 異常時 gate 提供回退依據。</p>
<p><strong>Converge</strong>：功能穩定後，flag 設定 100%（always-on）或 0%（移除功能）。此時 flag 已無控制作用，只是代碼中的條件分支。converge 階段是 flag 治理的關鍵轉折 — 很多 flag 停在這裡不再前進，持續佔用代碼路徑。</p>
<p><strong>Remove</strong>：移除 flag 代碼、清理條件分支、移除 flag 定義。移除動作困難的原因是 flag 可能被多處引用（server / client / config / test），每處都需要確認行為收斂到同一分支。自動化掃描（dead code detection、unused flag audit）能降低手動風險，但最終決策仍需要 flag owner 確認沒有殘留依賴。</p>
<h2 id="flag-debt-治理">Flag debt 治理</h2>
<p>每個未移除的 flag 讓測試需要覆蓋的狀態空間翻倍。10 個 stale flag 代表 1024 種潛在的狀態組合 — 實際測試覆蓋率遠低於這個數字，代碼行為的可預測性持續下降。</p>
<p><strong>TTL policy</strong>：flag 建立時設定預計移除日期。超過 TTL 且沒有活躍修改的 flag 自動標記為 debt，進入清理 backlog。TTL 按角色設定：release flag 兩週到一個月，experiment flag 與實驗週期對齊，ops flag 免 TTL 但需要年度 review。</p>
<p><strong>定期掃描</strong>：每月或每季掃描 stale flag（超過 TTL + 無活躍修改），生成清理 backlog。掃描結果對應到 flag owner，由 owner 決定是移除、延長 TTL 還是升級為 ops flag。無 owner 的 stale flag 是最高風險 — 沒有人能確認移除是否安全。</p>
<p><strong>Flag count dashboard</strong>：追蹤活躍 flag 數量趨勢。flag 數量持續上升是治理失敗的訊號 — 代表建立速度超過移除速度，debt 在累積。dashboard 按角色分類顯示，讓團隊知道 debt 集中在哪一類 flag。</p>
<h2 id="kill-switch-設計">Kill switch 設計</h2>
<p>Ops flag 作為事中止血工具，需要跟一般 feature flag 不同的設計約束。</p>
<p><strong>觸發延遲</strong>：kill switch 需要秒級生效。依賴 redeploy 才能生效的 flag 在事故中無法作為止血工具 — 部署流程本身需要數分鐘到數十分鐘。實作通常靠 flag evaluation service 的即時推送或短間隔 polling，讓 flag 值變更能在秒級傳播到所有 instance。</p>
<p><strong>權限控制</strong>：kill switch 的觸發權限應受控。值班人員與 SRE 有觸發權，一般開發者沒有。觸發記錄包含誰、什麼時間、因什麼原因觸發，接到 <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血策略</a> 的決策 log。</p>
<p><strong>Fallback 行為明確</strong>：每個 kill switch 在觸發後的預期行為應事前定義。「關掉這個 flag 後會怎樣」的答案應寫在 flag 定義中，讓觸發者在壓力下可快速判斷副作用，而不是臨場推理。</p>
<h2 id="experimentation-平台可靠性">Experimentation 平台可靠性</h2>
<p>A/B test 平台本身是 feature flag 的下游消費者。平台的可用性直接影響所有走 experiment flag 的流量分配。</p>
<p>平台掛掉時，flag 的行為取決於 fallback 配置：default-on 會讓所有使用者看到實驗中的變體，default-off 會讓所有使用者回到 control group。兩者的商業影響完全不同，fallback 行為應在每個 experiment flag 建立時明確配置。</p>
<p>experimentation 平台的 SLO 應獨立定義。當平台自身的 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 消耗過快時，影響的是所有進行中的實驗的流量分配正確性。平台故障不只是「實驗暫停」— 如果 fallback 行為配置錯誤，使用者可能被導向尚未驗證的功能路徑。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe</a>：progressive rollout 用 flag 控制 migration 的流量切換比例，每一步驗證交易正確性後再擴大，flag 的 rollout 節奏跟 migration safety 綁定。</li>
<li><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">Shopify</a>：高峰流量期間 ops flag 用於細粒度降級控制 — 關閉非核心功能釋放容量給 checkout 路徑。flag 的降級策略在 game day 驗證，確認觸發後的行為符合預期。</li>
<li><a href="/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">Stripe S2</a>：progressive rollout 用 flag 控制 canary 放量比例，每一步用交易指標判斷是否繼續。flag 的 rollout 節奏跟金流風險的延遲確認窗綁定。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>程式碼中存在 &gt; 6 個月未切換的 flag</td>
          <td>flag 已停在 converge 階段，應進入移除流程或升級為 ops flag</td>
          <td>啟動 stale flag 掃描 + 移除 sprint</td>
      </tr>
      <tr>
          <td>flag 移除流程靠 grep 跟人工 PR</td>
          <td>缺少自動化掃描，移除成本高導致 debt 累積</td>
          <td>導入 dead code detection 工具自動標記</td>
      </tr>
      <tr>
          <td>flag 實際分支跟預期不一致</td>
          <td>flag 狀態與代碼路徑脫鉤，通常在事故時才被發現</td>
          <td>建 flag 狀態 dashboard 定期對齊</td>
      </tr>
      <tr>
          <td>experimentation 平台掛掉影響所有 A/B 流量</td>
          <td>平台 fallback 行為未配置或未驗證</td>
          <td>配置 default-on/off fallback + 定期演練</td>
      </tr>
      <tr>
          <td>ops flag 跟 release flag 混在同系統、無權限隔離</td>
          <td>止血操作的審計軌跡與一般功能開關無法區分，事後回查困難</td>
          <td>分離 flag 系統或加 RBAC 權限隔離</td>
      </tr>
      <tr>
          <td>活躍 flag 數量每季持續上升</td>
          <td>建立速度超過移除速度，測試覆蓋的狀態空間在膨脹</td>
          <td>設 flag count budget、超額暫停新 flag 建立</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：flag 是 gate 通過後的細粒度 rollout 控制</li>
<li><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 contract testing</a>：flag 不同分支的契約覆蓋</li>
<li><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 perf regression gate</a>：flag 切換後的效能驗證</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：stale flag 進入 debt 治理</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安與資料保護</a>：permission flag 的權限約束</li>
<li><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血策略</a>：ops flag 作為事中止血手段</li>
</ul>
]]></content:encoded></item><item><title>8.17 Security Incident vs Operational Incident 分流</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/security-vs-operational-incident/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/security-vs-operational-incident/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何需要分流：兩類事故的決策模型、責任、通報、證據要求都不同&lt;/li>
&lt;li>分支判讀：影響類型（資料 / 可用性 / 機密）、是否有外部 actor、是否觸發法規通報&lt;/li>
&lt;li>平行 vs 切換：同事故可能同時是 operational + security（如 ransomware 同時影響可用性 + 資料）&lt;/li>
&lt;li>證據保全的優先序差異：operational 重 forensic-light、security 重 chain of custody&lt;/li>
&lt;li>通報差異：operational 對客戶 / 內部、security 還要法規 / 執法 / 律師&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的交接：07 提供 security IR 的概念基底、08 提供 operational IR 的流程主幹&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 containment&lt;/a> 的整合：security 事故的止血優先序跟 operational 不同（隔離 vs 復原）&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder&lt;/a> 的整合：security 事故對外通訊邊界更嚴&lt;/li>
&lt;li>反模式：security 事故走 operational 流程、證據被 IR 操作覆蓋；operational 套 security 流程、復原速度被法務拖慢&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Security Incident vs Operational Incident 分流是把事故的法規、證據與復原責任拆開判讀，責任是讓不同類型的事故走不同的處理主幹。&lt;/p>
&lt;p>這一頁處理的是流程分支，不是事故定性本身。當事故同時牽涉可用性與機密性，分流判斷會直接影響後續證據保全與通報義務。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀分流時，先看是否存在外部 actor 或資料外洩風險，再看是否需要切換到 security 流程。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>影響是否涉及資料、機密或外部 actor&lt;/li>
&lt;li>是否需要 chain of custody&lt;/li>
&lt;li>是否觸發法規通報&lt;/li>
&lt;li>是否需要同時保留 operational 與 security 兩條記錄&lt;/li>
&lt;/ul>
&lt;h2 id="案例對照">案例對照&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">Azure AD&lt;/a>：身份事故常同時碰到安全與可用性邊界。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365&lt;/a>：協作平台的事故容易踩到資料與存取邊界。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog&lt;/a>：觀測與控制面失效時，先要判斷是 operational 還是 security 風險。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>07 資安：security IR 的概念框架&lt;/li>
&lt;li>08.1 severity：分流影響 severity 計算&lt;/li>
&lt;li>08.3 containment：止血策略差異&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：證據保全與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 範圍&lt;/li>
&lt;li>08.10 stakeholder：對外通訊的法規邊界&lt;/li>
&lt;li>04.12 audit log：證據鏈來源&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>事故啟動時無人能說「這是 ops 還是 security」&lt;/li>
&lt;li>security 事故 IR 操作覆蓋了 forensic 證據&lt;/li>
&lt;li>operational 事故法務介入過多、復原拖慢&lt;/li>
&lt;li>兼具兩類性質的事故（如 ransomware）流程冗餘 / 衝突&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system&lt;/a> 角色 vs Security IC（CISO 線）責任邊界不清&lt;/li>
&lt;/ul>
&lt;h2 id="交接路由">交接路由&lt;/h2>
&lt;ul>
&lt;li>07 資安：security IR 的概念框架&lt;/li>
&lt;li>08.1 severity：分流影響 severity 計算&lt;/li>
&lt;li>08.3 containment：止血策略差異&lt;/li>
&lt;li>08.5 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：證據保全與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA&lt;/a> 範圍&lt;/li>
&lt;li>08.10 stakeholder：對外通訊的法規邊界&lt;/li>
&lt;li>04.12 audit log：證據鏈來源&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何需要分流：兩類事故的決策模型、責任、通報、證據要求都不同</li>
<li>分支判讀：影響類型（資料 / 可用性 / 機密）、是否有外部 actor、是否觸發法規通報</li>
<li>平行 vs 切換：同事故可能同時是 operational + security（如 ransomware 同時影響可用性 + 資料）</li>
<li>證據保全的優先序差異：operational 重 forensic-light、security 重 chain of custody</li>
<li>通報差異：operational 對客戶 / 內部、security 還要法規 / 執法 / 律師</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的交接：07 提供 security IR 的概念基底、08 提供 operational IR 的流程主幹</li>
<li>跟 <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 containment</a> 的整合：security 事故的止血優先序跟 operational 不同（隔離 vs 復原）</li>
<li>跟 <a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10 stakeholder</a> 的整合：security 事故對外通訊邊界更嚴</li>
<li>反模式：security 事故走 operational 流程、證據被 IR 操作覆蓋；operational 套 security 流程、復原速度被法務拖慢</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Security Incident vs Operational Incident 分流是把事故的法規、證據與復原責任拆開判讀，責任是讓不同類型的事故走不同的處理主幹。</p>
<p>這一頁處理的是流程分支，不是事故定性本身。當事故同時牽涉可用性與機密性，分流判斷會直接影響後續證據保全與通報義務。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀分流時，先看是否存在外部 actor 或資料外洩風險，再看是否需要切換到 security 流程。</p>
<p>重點訊號包括：</p>
<ul>
<li>影響是否涉及資料、機密或外部 actor</li>
<li>是否需要 chain of custody</li>
<li>是否觸發法規通報</li>
<li>是否需要同時保留 operational 與 security 兩條記錄</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">Azure AD</a>：身份事故常同時碰到安全與可用性邊界。</li>
<li><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">Microsoft 365</a>：協作平台的事故容易踩到資料與存取邊界。</li>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog</a>：觀測與控制面失效時，先要判斷是 operational 還是 security 風險。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>07 資安：security IR 的概念框架</li>
<li>08.1 severity：分流影響 severity 計算</li>
<li>08.3 containment：止血策略差異</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：證據保全與 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 範圍</li>
<li>08.10 stakeholder：對外通訊的法規邊界</li>
<li>04.12 audit log：證據鏈來源</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故啟動時無人能說「這是 ops 還是 security」</li>
<li>security 事故 IR 操作覆蓋了 forensic 證據</li>
<li>operational 事故法務介入過多、復原拖慢</li>
<li>兼具兩類性質的事故（如 ransomware）流程冗餘 / 衝突</li>
<li><a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident command system</a> 角色 vs Security IC（CISO 線）責任邊界不清</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>07 資安：security IR 的概念框架</li>
<li>08.1 severity：分流影響 severity 計算</li>
<li>08.3 containment：止血策略差異</li>
<li>08.5 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：證據保全與 <a href="/blog/backend/knowledge-cards/rca/" data-link-title="RCA" data-link-desc="說明根因分析如何區分觸發事件、系統弱點與防線缺口">RCA</a> 範圍</li>
<li>08.10 stakeholder：對外通訊的法規邊界</li>
<li>04.12 audit log：證據鏈來源</li>
</ul>
]]></content:encoded></item><item><title>MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Vitess sharding&lt;/em> — 4 個 component 協作的完整 sharding 系統。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境mysql-寫吞吐撞上-single-primary-上限">問題情境：MySQL 寫吞吐撞上 single primary 上限&lt;/h2>
&lt;p>MySQL primary 單機極限大致 50K-100K WPS（依 schema / hardware）。超過這個級別、選項三條：&lt;/p>
&lt;ol>
&lt;li>&lt;em>Application 層 sharding&lt;/em>：每張 table 自己決定怎麼分片、application 寫 routing logic、跨 shard query / migration 都要自己處理&lt;/li>
&lt;li>&lt;em>Vitess&lt;/em>：proxy layer 自動 routing、cross-shard query 可選自動 split、resharding 自動化&lt;/li>
&lt;li>&lt;em>Distributed SQL&lt;/em>（CockroachDB / Spanner / Aurora DSQL）：跟 MySQL 不同 engine、application 改 driver&lt;/li>
&lt;/ol>
&lt;p>選 Vitess 的核心 driver：&lt;em>保留 MySQL wire protocol + 應用層幾乎不必改 + 透明分片&lt;/em>。代價是 4 個 component 的 operational complexity — Vitess 的責任範圍是完整分散式系統，而非單純 proxy。&lt;/p>
&lt;p>閱讀本文前可先對齊 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding&lt;/a> 的 shard key、routing、resharding 與 cross-shard query 語意；容量失衡時再接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition&lt;/a>。&lt;/p>
&lt;h2 id="vitess-四件套每個-component-的責任">Vitess 四件套：每個 component 的責任&lt;/h2>





&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"> ┌─────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> Application ────→ │ VTGate │ ← 對外 MySQL wire protocol
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> │ (proxy + parse + route + aggregate) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> └────┬─────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> │ │
&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ┌──────────────┐ ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> │ VTTablet │ │ VTTablet │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> │ (per-MySQL │ │ (per-MySQL │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> │ sidecar) │ │ sidecar) │
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> ▼ ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> ┌──────────────┐ ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> │ MySQL │ │ MySQL │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> │ (Shard -80) │ │ (Shard 80-) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> └──────────────┘ └──────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> Topology Service (etcd / Consul / ZooKeeper)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> ↑↓ 所有 component 共享 metadata
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> VSchema：keyspace 結構、shard 範圍、Vindex 定義&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="vtgate--query-routing-layer">VTGate — query routing layer&lt;/h3>
&lt;p>對 application 看起來像 MySQL（同樣 port、同樣 wire protocol、同樣 query 語法）、實際是 stateless proxy。每個 query VTGate：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>Vitess sharding</em> — 4 個 component 協作的完整 sharding 系統。</p></blockquote>
<hr>
<h2 id="問題情境mysql-寫吞吐撞上-single-primary-上限">問題情境：MySQL 寫吞吐撞上 single primary 上限</h2>
<p>MySQL primary 單機極限大致 50K-100K WPS（依 schema / hardware）。超過這個級別、選項三條：</p>
<ol>
<li><em>Application 層 sharding</em>：每張 table 自己決定怎麼分片、application 寫 routing logic、跨 shard query / migration 都要自己處理</li>
<li><em>Vitess</em>：proxy layer 自動 routing、cross-shard query 可選自動 split、resharding 自動化</li>
<li><em>Distributed SQL</em>（CockroachDB / Spanner / Aurora DSQL）：跟 MySQL 不同 engine、application 改 driver</li>
</ol>
<p>選 Vitess 的核心 driver：<em>保留 MySQL wire protocol + 應用層幾乎不必改 + 透明分片</em>。代價是 4 個 component 的 operational complexity — Vitess 的責任範圍是完整分散式系統，而非單純 proxy。</p>
<p>閱讀本文前可先對齊 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a> 的 shard key、routing、resharding 與 cross-shard query 語意；容量失衡時再接 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>。</p>
<h2 id="vitess-四件套每個-component-的責任">Vitess 四件套：每個 component 的責任</h2>





<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">                        ┌─────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   Application ────→    │     VTGate      │  ← 對外 MySQL wire protocol
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                        │  (proxy + parse + route + aggregate)  │
</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></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></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">        │   VTTablet   │                  │   VTTablet   │
</span></span><span class="line"><span class="ln">10</span><span class="cl">        │ (per-MySQL   │                  │ (per-MySQL   │
</span></span><span class="line"><span class="ln">11</span><span class="cl">        │  sidecar)    │                  │  sidecar)    │
</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></span><span class="line"><span class="ln">14</span><span class="cl">              ▼                                 ▼
</span></span><span class="line"><span class="ln">15</span><span class="cl">        ┌──────────────┐                  ┌──────────────┐
</span></span><span class="line"><span class="ln">16</span><span class="cl">        │    MySQL     │                  │    MySQL     │
</span></span><span class="line"><span class="ln">17</span><span class="cl">        │  (Shard -80) │                  │  (Shard 80-) │
</span></span><span class="line"><span class="ln">18</span><span class="cl">        └──────────────┘                  └──────────────┘
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">   Topology Service (etcd / Consul / ZooKeeper)
</span></span><span class="line"><span class="ln">21</span><span class="cl">   ↑↓ 所有 component 共享 metadata
</span></span><span class="line"><span class="ln">22</span><span class="cl">   VSchema：keyspace 結構、shard 範圍、Vindex 定義</span></span></code></pre></div><h3 id="vtgate--query-routing-layer">VTGate — query routing layer</h3>
<p>對 application 看起來像 MySQL（同樣 port、同樣 wire protocol、同樣 query 語法）、實際是 stateless proxy。每個 query VTGate：</p>
<ol>
<li>Parse SQL → 找出 routing key（從 WHERE column 拿）</li>
<li>查 VSchema → 計算 routing key 對應的 shard</li>
<li>把 query 送該 shard 的 VTTablet</li>
<li>等 response、aggregate（如果是 cross-shard query）、回 application</li>
</ol>
<p>Stateless 設計 → VTGate 可以隨意 scale、放 N 個前面接 LB。多數 production 部署 3-10 個 VTGate per region。</p>
<h3 id="vttablet--per-mysql-agent">VTTablet — per-MySQL agent</h3>
<p>每個 MySQL instance 旁邊都跑一個 VTTablet。VTTablet 責任：</p>
<ul>
<li>把 MySQL primary 標記、上報給 topology</li>
<li>接 VTGate 的 query、轉發給 local MySQL</li>
<li>跑 <em>connection pool</em>（VTGate 跟 VTTablet 之間少量連線、VTTablet 跟 local MySQL 共享 connection）</li>
<li>跑 <em>query plan cache</em> / <em>transactional consistency check</em></li>
<li>處理 <em>online schema change</em>（Vitess 內建 OSC）</li>
<li>跟 VTOrc（fork of Orchestrator）配合做 failover</li>
</ul>
<p>VTTablet 是 Vitess 跟 MySQL 唯一連接點 — 沒 VTTablet 直接連 MySQL 不在 Vitess 管理下。</p>
<h3 id="vreplication--跨-shard-資料移動">VReplication — 跨 shard 資料移動</h3>
<p>VReplication 是 Vitess <em>跨 shard / 跨 keyspace / 跨 cluster</em> 資料移動引擎、底層用 MySQL binlog。用途：</p>
<ul>
<li><em>Resharding</em>：把 shard -80 拆成 -40 + 40-80、VReplication 自動拆 binlog event 對應 shard</li>
<li><em>Materialized view</em>：cross-shard aggregation 預計算</li>
<li><em>MoveTables</em>：跨 keyspace 移 table（schema-level migration）</li>
<li><em>VStream</em>：CDC、binlog event 對外輸出（可接 Kafka / Debezium）</li>
</ul>
<p>VReplication 的主要使用者是 <em>Vitess operator</em>，它和 application 行為直接相關（resharding 期間有 write split 行為）。</p>
<h3 id="vschema--sharding-metadata">VSchema — sharding metadata</h3>
<p>VSchema 是 keyspace 內 <em>哪張 table 怎麼 shard</em> 的定義、JSON 格式存 topology service。例子：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;sharded&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;vindexes&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;hash&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;hash&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;tables&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nt">&#34;orders&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nt">&#34;column_vindexes&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="nt">&#34;column&#34;</span><span class="p">:</span> <span class="s2">&#34;user_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;hash&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nt">&#34;users&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nt">&#34;column_vindexes&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">          <span class="nt">&#34;column&#34;</span><span class="p">:</span> <span class="s2">&#34;user_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;hash&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>orders.user_id</code> 跟 <code>users.user_id</code> 用同一個 Vindex（hash）+ 同一個 column → 同 user_id 的 orders + users 落在同 shard、可以 JOIN 不跨 shard。</p>
<h2 id="vindexvitess-的-sharding-function">Vindex：Vitess 的 sharding function</h2>
<p>Vindex 是 Vitess 的 <em>shard key 計算函數</em>。內建多種：</p>
<table>
  <thead>
      <tr>
          <th>Vindex 類型</th>
          <th>計算方式</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>hash</code></td>
          <td>3DES-based null hash（非 MD5）→ 對應 shard range</td>
          <td>預設、均勻分布、適合 primary key</td>
      </tr>
      <tr>
          <td><code>binary_md5</code></td>
          <td>MD5(binary)</td>
          <td>binary key</td>
      </tr>
      <tr>
          <td><code>unicode_loose_xxhash</code></td>
          <td>xxHash on lowercased unicode</td>
          <td>string key</td>
      </tr>
      <tr>
          <td><code>numeric</code></td>
          <td>直接 numeric value</td>
          <td>連續 numeric range（適合 time-based）</td>
      </tr>
      <tr>
          <td><code>numeric_static_map</code></td>
          <td>預定義 map</td>
          <td>國家 code / region 等少 enum</td>
      </tr>
      <tr>
          <td><code>lookup_hash</code></td>
          <td>透過 lookup table 查 shard</td>
          <td>多個 column 都要 shard、需要二級 index</td>
      </tr>
  </tbody>
</table>
<p>最常用：<code>hash</code>（primary key）+ <code>lookup_hash</code>（secondary access pattern）。</p>
<h2 id="keyspace--shard--tablet-階層">Keyspace / Shard / Tablet 階層</h2>





<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">Keyspace (邏輯 database)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   └── Shards
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        ├── -80 (shard range 0-128)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        │     ├── Primary tablet (1 MySQL primary)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        │     ├── Replica tablet × 2
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        │     └── RDOnly tablet × 1 (analytics)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        └── 80- (shard range 128-256)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">              ├── Primary tablet
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">              ├── Replica tablet × 2
</span></span><span class="line"><span class="ln">10</span><span class="cl">              └── RDOnly tablet × 1</span></span></code></pre></div><p>Shard range 用 <em>binary hex prefix</em>（<code>-80</code> 表示 0 到 0x80、<code>80-</code> 表示 0x80 到 max）— 給 resharding 留 split 餘地（<code>-80</code> 可切成 <code>-40</code> + <code>40-80</code>）。</p>
<p>Tablet type：</p>
<ul>
<li><em>Primary</em>：寫入入口</li>
<li><em>Replica</em>：read traffic（Vitess query rules 控制）</li>
<li><em>RDOnly</em>：純 analytics / backup / VReplication source、低 SLA、不上 production read traffic</li>
</ul>
<h2 id="配置-step-by-steplocal-cluster">配置 step-by-step（local cluster）</h2>
<p>Production 通常用 Kubernetes operator（vitess-operator）部署、但理解概念用 local cluster 最快：</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"># 用 vtctldclient 操作（替代舊的 vtctlclient）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 1. 建 unsharded keyspace</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">vtctldclient CreateKeyspace --durability-policy<span class="o">=</span>semi_sync commerce
</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"># 2. 從一個 MySQL primary 開始（unsharded）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">vtctldclient ApplySchema --sql<span class="o">=</span><span class="s2">&#34;CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)&#34;</span> commerce
</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="c1"># 3. 把 keyspace 改成 sharded、定義 VSchema</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">vtctldclient ApplyVSchema --vschema<span class="o">=</span><span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s1">  &#34;sharded&#34;: true,
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s1">  &#34;vindexes&#34;: {&#34;hash&#34;: {&#34;type&#34;: &#34;hash&#34;}},
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s1">  &#34;tables&#34;: {
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s1">    &#34;orders&#34;: {
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s1">      &#34;column_vindexes&#34;: [{&#34;column&#34;: &#34;user_id&#34;, &#34;name&#34;: &#34;hash&#34;}]
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s1">  }
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s1">}&#39;</span> commerce
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 4. 觸發 resharding：unsharded → 2 shards (-80, 80-)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">vtctldclient Reshard --workflow<span class="o">=</span>initial-shard create <span class="se">\
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="se"></span>  --source-shards<span class="o">=</span><span class="s2">&#34;commerce/0&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="se"></span>  --target-shards<span class="o">=</span><span class="s2">&#34;commerce/-80,commerce/80-&#34;</span>
</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 class="c1"># 5. 等資料 copy 完（VReplication 跑）</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">vtctldclient Workflow --keyspace<span class="o">=</span>commerce show initial-shard
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># 6. SwitchTraffic：先切 RDOnly → 再切 Replica → 最後切 Primary</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">vtctldclient Reshard --workflow<span class="o">=</span>initial-shard switchtraffic <span class="se">\
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="se"></span>  --tablet-types<span class="o">=</span><span class="s2">&#34;rdonly,replica&#34;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">vtctldclient Reshard --workflow<span class="o">=</span>initial-shard switchtraffic <span class="se">\
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="se"></span>  --tablet-types<span class="o">=</span><span class="s2">&#34;primary&#34;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"># 7. 完成、cleanup old shard</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">vtctldclient Reshard --workflow<span class="o">=</span>initial-shard complete</span></span></code></pre></div><p>實際 production 走 <em>Vitess Kubernetes operator</em>、用 <code>VitessCluster</code> CRD 宣告 desired state、operator 自動操作上面這些 step。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-cross-shard-transaction--vitess-不支援-atomic預設">1. Cross-shard transaction — Vitess 不支援 atomic（預設）</h3>
<p>兩個 user 的 order 在不同 shard、<code>BEGIN; UPDATE orders WHERE user_id=1; UPDATE orders WHERE user_id=2; COMMIT;</code> 跨兩個 shard。Vitess 預設 <em>不保證 atomic</em> — 兩個 shard 各自 commit、可能一個成功一個失敗、application 看到 partial state。</p>
<p>修法：</p>
<ul>
<li><em>避免 cross-shard transaction</em>：schema design 讓 transaction boundary 落在單一 shard 內</li>
<li>啟用 <em>atomic 2-phase commit</em>（Vitess <code>transaction_mode=TWOPC</code>、實驗性、performance penalty 大）</li>
<li>大規模需要 atomic 的場景應該換 distributed SQL（CockroachDB / Spanner），讓資料庫層承擔跨節點一致性</li>
</ul>
<h3 id="2-vstream-lag--resharding-期間-cdc-落後">2. VStream lag — Resharding 期間 CDC 落後</h3>
<p>Resharding 過程 VReplication 大量寫 binlog event、application <em>本來在用</em> 的 VStream（接 Kafka 等）共享同 binlog stream、可能 lag。Downstream consumer 看到 stale data 1-2 小時。</p>
<p>修法：</p>
<ul>
<li>Resharding 期間 <em>暫停非關鍵 VStream</em>（analytics ETL 可暫停、real-time recommendation 需要保留）</li>
<li>確認 binlog disk capacity &gt; resharding 期間預估 binlog 量 × 2（buffer）</li>
<li>Resharding 完成後 <em>手動驗證</em> VStream offset 已 catch up，把驗證結果留成 cutover evidence</li>
</ul>
<h3 id="3-vindex-不均勻--hot-shard">3. Vindex 不均勻 — Hot shard</h3>
<p>Vindex 預設 <code>hash</code> 對 <em>primary key 均勻分布</em>、但對 <em>natural key</em>（country / region / company_id 等）可能不均勻。10 個 country、其中 1 個 country 佔 80% traffic、單一 shard 永遠 hot。</p>
<p>修法：</p>
<ul>
<li><em>Composite Vindex</em>：combine <code>country + user_id</code> 兩 column 作為 shard key、user-level 仍均勻</li>
<li><em>Synthetic shard key</em>：application 層加 <code>sharding_key=hash(actual_key) % N</code>、控制分布</li>
<li>監控 <em>per-shard QPS</em>：<code>vtctldclient ShowVDiff</code> + Prometheus exporter</li>
<li>Hot shard 出現後 Vitess 可以 resharding 解（split hot shard 為 2 個小 shard）、但工作量大</li>
</ul>
<h3 id="4-resharding-切流量瞬間-deadlock">4. Resharding 切流量瞬間 deadlock</h3>
<p>Resharding 最後的 SwitchTraffic 切 primary 階段、舊 shard 仍接 write、Vitess 切 routing、Application 一瞬間連兩個 shard、相同 user_id 寫入可能跑兩邊、deadlock 或 lost update。</p>
<p>修法：</p>
<ul>
<li><em>SwitchTraffic 用 ReverseTraffic 預備</em>：先 switch、確認問題後可 reverse 回去</li>
<li>切流量 <em>只在 known quiet period</em>（夜間 / 週末早上）</li>
<li>VTGate <code>--retry-count=2</code> + <code>--track-vtgate-deadlock-events</code>：deadlock 自動 retry、不暴露給 application</li>
<li>真的失敗用 <code>Reshard cancel</code> 回 old state，讓 workflow 回到可驗證狀態</li>
</ul>
<h3 id="5-vreplication-workflow-卡住--cancel-前需要保護狀態">5. VReplication workflow 卡住 — cancel 前需要保護狀態</h3>
<p>VReplication workflow 跑到 50% 但 <em>某個 row 解析錯誤</em>（schema mismatch / blob 大小超過 limit）、workflow stuck、進度條卡住、無 timeout。整個 resharding flow halt。</p>
<p>修法：</p>
<ul>
<li>平時跑 <em>staging 資料 dry-run</em>、發現 schema 跟 blob 邊界問題</li>
<li>Workflow 卡住時 <code>vtctldclient Workflow show</code> 看 last_message / row_state</li>
<li>手動修問題 row（直接 MySQL 改）後 <em>resume workflow</em></li>
<li>大 cluster 建議 <em>VReplication 跑前先 SchemaApply audit</em>、確認 source / target schema 兼容</li>
</ul>
<h2 id="vitess-跟自管-sharding-對照">Vitess 跟自管 sharding 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Vitess</th>
          <th>Application-level sharding</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application 改動</td>
          <td>幾乎不必（保留 MySQL wire）</td>
          <td>大改（routing logic 寫 application）</td>
      </tr>
      <tr>
          <td>Cross-shard query</td>
          <td>VTGate 自動 split（受限）</td>
          <td>Application 自己處理</td>
      </tr>
      <tr>
          <td>Resharding</td>
          <td>VReplication 自動</td>
          <td>手寫腳本、操作複雜</td>
      </tr>
      <tr>
          <td>Online schema change</td>
          <td>Vitess 內建（VReplication-based）</td>
          <td>用 gh-ost / pt-osc</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>VTOrc 整合</td>
          <td>自管 Orchestrator</td>
      </tr>
      <tr>
          <td>Operational cost</td>
          <td>高（4 component 要懂）</td>
          <td>中（fewer abstractions、但 application logic 多）</td>
      </tr>
      <tr>
          <td>Cross-keyspace 共用 vindex</td>
          <td>內建（lookup_hash 跨 keyspace）</td>
          <td>自寫</td>
      </tr>
  </tbody>
</table>
<p>Vitess 的 <em>operational complexity</em> 是它的代價。10-20 人 SRE 團隊撐得住、5 人團隊用 <em>managed Vitess（PlanetScale）</em> 更實際。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>Vitess shard 內部仍用 MySQL replication（<a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>）— 每個 shard 有 primary + replica + rdonly。Vitess durability-policy 控制 primary 寫入是否等 replica ack（semi-sync）。</p>
<h3 id="跟-osc-tool">跟 OSC tool</h3>
<p>Vitess <em>不用 gh-ost / pt-osc</em>、用 VReplication-based online DDL。Vitess online DDL：</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">vtctldclient ApplySchema --strategy<span class="o">=</span>vitess <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --sql<span class="o">=</span><span class="s2">&#34;ALTER TABLE orders ADD COLUMN status VARCHAR(20)&#34;</span> commerce</span></span></code></pre></div><p>詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-proxysql">跟 ProxySQL</h3>
<p><em>Vitess 取代 ProxySQL</em>。VTGate 本身做 connection pool + query routing、不再需要 ProxySQL。混用會造成 routing 衝突（VTGate 期待自己決定 shard、ProxySQL 跟 VTGate 競爭）。詳見 <a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL 配置</a>。</p>
<h3 id="跟-orchestrator">跟 Orchestrator</h3>
<p>Vitess 用 <em>VTOrc</em>（fork of Orchestrator）作 failover、跟 Vitess topology metadata 整合。不用獨立 Orchestrator。詳見 <a href="/blog/backend/01-database/vendors/mysql/orchestrator-failover/" data-link-title="MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster &#43; GTID-based promotion 的兩段 paradox" data-link-desc="Orchestrator 是 MySQL HA 自動 failover 的 de facto standard、但讀者第一個問題往往是「HA 工具自己會壞嗎」。本文走 Orchestrator 的雙層架構（管 MySQL 的 raft cluster &#43; 被 raft 管的 orchestrator instance）→ topology discovery → failure detection → failover decision tree → promote action → 5 production 踩雷（split-brain 跟 fencing / pre-failover hook 失敗 / anti-flapping window / GTID errant transaction / VIP 跟 ProxySQL 整合斷層）→ 跟 ProxySQL / Patroni / RDS 對比">Orchestrator failover 設計</a>。</p>
<h3 id="跟-planetscalemanaged-vitess">跟 PlanetScale（managed Vitess）</h3>
<p>PlanetScale 是 <em>Vitess managed service</em>、隱藏 4 component operational complexity、加 branch-based schema workflow。詳見 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">PlanetScale migration playbook</a>。</p>
<h3 id="跟-aurora-mysql">跟 Aurora MySQL</h3>
<p>Aurora 跟 Vitess 是 <em>不同 scale 路徑</em>：</p>
<ul>
<li>Aurora：single-region scaling（storage / compute 分離、最高 ~128 TB）</li>
<li>Vitess：horizontal sharding（無上限、靠加 shard scaling）</li>
</ul>
<p>兩者承擔的容量與操作責任不同。超過 Aurora single-region 上限的場景才考慮 Vitess。詳見 <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 page</a>。</p>
<h2 id="production-caseyoutube--vitess">Production case：YouTube / Vitess</h2>
<p>Vitess 的 production 責任是把 MySQL shard 拓撲變成應用可查詢、可遷移、可操作的資料庫層。YouTube / Vitess 的公開歷史提供的工程訊號是 VTGate、VTTablet、VReplication 與 VSchema 這組元件分工：application query 進 VTGate、tablet 層包住 MySQL、VSchema 描述 routing / sharding 規則、VReplication 支援 resharding 與資料搬移。</p>
<p>這個案例要回收到三個操作判準。第一，Vitess 是一套 database control plane，而非單一 proxy；導入時要把 topology service、tablet lifecycle、backup、failover 與 schema workflow 一起納入 ownership。第二，VSchema 是 application contract，shard key、lookup vindex 與 cross-shard query 都會影響產品功能設計。第三，VReplication 讓 resharding 可操作，但它仍需要 capacity window、backfill 監控與 cutover plan。</p>
<p>Vitess 的 sibling 路由是 <a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">PostgreSQL Citus Distributed</a> 與 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。Citus 保留 PostgreSQL 生態並用 coordinator / worker 拆分資料；CockroachDB / Spanner 則用 distributed SQL 重新定義交易與一致性邊界。選型時要先判斷自己是在延伸 MySQL 投資，還是在重新選 global OLTP model。</p>
<h2 id="何時用-vitess">何時用 Vitess</h2>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>評估</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>流量 &gt; 50K WPS、單 primary 撐不住</td>
          <td>是 Vitess scope</td>
      </tr>
      <tr>
          <td>已有大量 MySQL 投資、不想換 distributed SQL</td>
          <td>是</td>
      </tr>
      <tr>
          <td>有 5-10 人 SRE / DBA 團隊</td>
          <td>是</td>
      </tr>
      <tr>
          <td>流量 &lt; 10K WPS</td>
          <td>否（過度設計、用單 MySQL + replica）</td>
      </tr>
      <tr>
          <td>5 人團隊、不想養 DBA</td>
          <td>否（用 PlanetScale managed）</td>
      </tr>
      <tr>
          <td>必須 multi-region 強一致 transaction</td>
          <td>否（CockroachDB / Spanner 才對）</td>
      </tr>
      <tr>
          <td>需要複雜 cross-shard analytics</td>
          <td>否（搭配 BigQuery / Snowflake）</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（Vitess shard 內部）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（Vitess 不用 gh-ost / pt-osc）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（Vitess 取代 ProxySQL）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/orchestrator-failover/" data-link-title="MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster &#43; GTID-based promotion 的兩段 paradox" data-link-desc="Orchestrator 是 MySQL HA 自動 failover 的 de facto standard、但讀者第一個問題往往是「HA 工具自己會壞嗎」。本文走 Orchestrator 的雙層架構（管 MySQL 的 raft cluster &#43; 被 raft 管的 orchestrator instance）→ topology discovery → failure detection → failover decision tree → promote action → 5 production 踩雷（split-brain 跟 fencing / pre-failover hook 失敗 / anti-flapping window / GTID errant transaction / VIP 跟 ProxySQL 整合斷層）→ 跟 ProxySQL / Patroni / RDS 對比">MySQL Orchestrator failover</a>（VTOrc fork）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">PostgreSQL Citus Distributed</a>（PG sibling、coordinator + worker 模型 vs Vitess VTGate + tablet）</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（Vitess vs CockroachDB vs Spanner）</li>
<li><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>（shard key、routing、resharding 與 cross-shard query）</li>
<li>官方：<a href="https://vitess.io/docs/">Vitess Documentation</a> / <a href="https://github.com/planetscale/vitess-operator">Vitess Operator</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/citus-distributed/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/citus-distributed/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>Citus distributed extension&lt;/em> — 把 PG 變成 sharded cluster 的方式。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>當 PG single-primary 寫吞吐撞上單機極限（50K-100K WPS）、選項三條：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Application 層 sharding&lt;/strong>：應用層自管 shard routing&lt;/li>
&lt;li>&lt;strong>Citus&lt;/strong>：PG extension、自動 routing + cross-shard query&lt;/li>
&lt;li>&lt;strong>Distributed SQL&lt;/strong>（CockroachDB / Aurora DSQL / Spanner）：不同 engine&lt;/li>
&lt;/ol>
&lt;p>選 Citus 的核心 driver：&lt;em>保留 PG SQL syntax + extension 生態&lt;/em>。但「應用層幾乎不必改」是樂觀說法 — 實際上 application 必須圍繞 distribution column 重設計（query 加 filter / transaction 限定同 shard / reference table 量控制）、跟 Vitess 比 cross-shard query 自動化弱。代價是 &lt;em>coordinator / worker 部署複雜度 + cross-shard query 限制 + application schema 改造工作量&lt;/em>。&lt;/p>
&lt;p>閱讀本文前可先對齊 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding&lt;/a> 的 shard key、routing、resharding 與 cross-shard query 語意；容量失衡時再接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition&lt;/a>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess sharding&lt;/a> 的核心差異：Citus 是 &lt;em>PG extension&lt;/em>（PG 自己跑）、Vitess 是 &lt;em>獨立 proxy + tablet 系統&lt;/em>（包 MySQL）。Citus 用 PG 原生機制（FDW / extension hook）、Vitess 是 &lt;em>外部包裝&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>Citus distributed extension</em> — 把 PG 變成 sharded cluster 的方式。</p></blockquote>
<hr>
<p>當 PG single-primary 寫吞吐撞上單機極限（50K-100K WPS）、選項三條：</p>
<ol>
<li><strong>Application 層 sharding</strong>：應用層自管 shard routing</li>
<li><strong>Citus</strong>：PG extension、自動 routing + cross-shard query</li>
<li><strong>Distributed SQL</strong>（CockroachDB / Aurora DSQL / Spanner）：不同 engine</li>
</ol>
<p>選 Citus 的核心 driver：<em>保留 PG SQL syntax + extension 生態</em>。但「應用層幾乎不必改」是樂觀說法 — 實際上 application 必須圍繞 distribution column 重設計（query 加 filter / transaction 限定同 shard / reference table 量控制）、跟 Vitess 比 cross-shard query 自動化弱。代價是 <em>coordinator / worker 部署複雜度 + cross-shard query 限制 + application schema 改造工作量</em>。</p>
<p>閱讀本文前可先對齊 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a> 的 shard key、routing、resharding 與 cross-shard query 語意；容量失衡時再接 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess sharding</a> 的核心差異：Citus 是 <em>PG extension</em>（PG 自己跑）、Vitess 是 <em>獨立 proxy + tablet 系統</em>（包 MySQL）。Citus 用 PG 原生機制（FDW / extension hook）、Vitess 是 <em>外部包裝</em>。</p>
<h2 id="citus-架構coordinator--worker">Citus 架構：Coordinator + Worker</h2>





<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">                ┌─────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   Application  │   Coordinator   │  ← 對外 PG wire protocol、planner、routing
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                │   (Citus + PG)  │
</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></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></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">        │ Worker 1 │         │ Worker 2 │  ← 各跑 PG + Citus extension
</span></span><span class="line"><span class="ln">10</span><span class="cl">        │  (PG)    │         │  (PG)    │
</span></span><span class="line"><span class="ln">11</span><span class="cl">        │ shard 1,3│         │ shard 2,4│
</span></span><span class="line"><span class="ln">12</span><span class="cl">        └──────────┘         └──────────┘</span></span></code></pre></div><p><strong>Coordinator</strong>：</p>
<ul>
<li>對 application 看起來像 PG（同 port / 同 wire protocol）</li>
<li>接 SQL → Citus planner 把 query 分解 + route 給 worker</li>
<li>不存 data（distributed table 的 shard 在 worker 上）</li>
<li>存 <em>metadata</em>（哪個 shard 在哪個 worker）</li>
</ul>
<p><strong>Worker</strong>：</p>
<ul>
<li>標準 PG instance + Citus extension</li>
<li>各存若干 shard</li>
<li>接 coordinator 來的 query、跑 local execute、回結果</li>
</ul>
<p><strong>Shard</strong>：</p>
<ul>
<li>Distributed table 拆成 N 個 shard（預設 32）</li>
<li>每 shard 是 worker 上的 <em>physical PG table</em>（含 <code>_&lt;shardid&gt;</code> 後綴）</li>
<li>行為跟一般 PG table 一樣、可以直接連 worker 用 PG 工具 access</li>
</ul>
<h2 id="3-種-table-type">3 種 Table Type</h2>
<h3 id="distributed-table--跨-shard-切分">Distributed table — 跨 shard 切分</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 建一般 PG table
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="n">BIGSERIAL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">)</span><span class="w">  </span><span class="c1">-- PK 必須含 distribution column
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 用 Citus 把它變 distributed
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_distributed_table</span><span class="p">(</span><span class="s1">&#39;orders&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;user_id&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>user_id</code> 是 <em>distribution column</em> — Citus 用它的 hash 決定 row 屬哪個 shard。<code>PK 必須含 distribution column</code>（跟 MySQL partitioning 同要求）。</p>
<p>跟 Vitess Vindex 對比：</p>
<ul>
<li>Citus：hash distribution column → shard（單一 hash function、不可選 algorithm）</li>
<li>Vitess：Vindex 可選多種（hash / lookup_hash / xxhash / null）</li>
</ul>
<h3 id="reference-table--全-shard-共有">Reference table — 全 shard 共有</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">name</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">100</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">price</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_reference_table</span><span class="p">(</span><span class="s1">&#39;products&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>products</code> 在 <em>每個 worker 都有完整 copy</em>、寫入 coordinator 廣播給所有 worker。</p>
<p>用途：</p>
<ul>
<li>小 lookup table（country code / product category 等）</li>
<li>跨 distributed table JOIN 時、reference table 在每 worker 上、不必 cross-shard</li>
<li>寫入頻率低（廣播 cost 跟 worker 數 linear）</li>
</ul>
<h3 id="local-table--coordinator-上的-pg-table">Local table — Coordinator 上的 PG table</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">audit_log</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">event</span><span class="w"> </span><span class="n">JSONB</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 不調用 Citus function、預設留在 coordinator</span></span></span></code></pre></div><p>行為跟一般 PG table 一樣。用於 <em>不需 distribute</em> 的 table（如 admin metadata）。</p>
<h2 id="colocation跨-distributed-table-同-shard-對齊">Colocation：跨 distributed table 同 shard 對齊</h2>
<p>當兩個 distributed table 都用 <em>同 distribution column</em>（例如 <code>user_id</code>）+ 同 shard count、Citus 自動 colocate：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">create_distributed_table</span><span class="p">(</span><span class="s1">&#39;orders&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;user_id&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_distributed_table</span><span class="p">(</span><span class="s1">&#39;user_addresses&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;user_id&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">colocate_with</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;orders&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Colocate 後：</p>
<ul>
<li><code>user_id = 100</code> 的 orders 跟 user_addresses 在 <em>同一 worker shard</em></li>
<li>JOIN 不跨 worker、效率高</li>
<li>可用 PG 原生 FK constraint（cross-table 但同 shard）</li>
</ul>
<p>Colocate 是 Citus 設計的核心 <em>跨 table 一致性</em> 機制。沒 colocate 的 cross-table query 變 cross-worker、效率大降。</p>
<h2 id="配置-step-by-steplocal-cluster">配置 step-by-step（local cluster）</h2>
<p>Production 用 Citus Cloud（Microsoft 託管）或 Azure Cosmos DB for PostgreSQL（同 engine）。Self-hosted：</p>
<h3 id="step-1coordinator--worker-都裝-pg--citus">Step 1：Coordinator + worker 都裝 PG + Citus</h3>





<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"># 在每個 node（coordinator + 2 worker）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">apt install postgresql-14
</span></span><span class="line"><span class="ln">3</span><span class="cl">apt install postgresql-14-citus-12.0
</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="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nv">shared_preload_libraries</span> <span class="o">=</span> <span class="s1">&#39;citus&#39;</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">systemctl restart postgresql</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 在每個 node 跑
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">citus</span><span class="p">;</span></span></span></code></pre></div><h3 id="step-2coordinator-註冊-worker">Step 2：Coordinator 註冊 worker</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 在 coordinator 跑
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">citus_add_node</span><span class="p">(</span><span class="s1">&#39;worker1.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">5432</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">citus_add_node</span><span class="p">(</span><span class="s1">&#39;worker2.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">5432</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 確認
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">citus_get_active_worker_nodes</span><span class="p">();</span></span></span></code></pre></div><h3 id="step-3建-distributed-table">Step 3：建 distributed table</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="n">BIGSERIAL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_distributed_table</span><span class="p">(</span><span class="s1">&#39;orders&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;user_id&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Citus 自動把 <code>orders</code> 拆成 32 個 shard（<code>orders_102008</code> 等）、分配到 worker。</p>
<h3 id="step-4application-連-coordinator">Step 4：Application 連 coordinator</h3>
<p>Application connection string 連 coordinator IP / port（不必知道 worker 存在）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 從 application 跑 query、Citus 透明 route
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">12345</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- → Citus 看 user_id=12345 hash 屬 shard 17、route 給對應 worker
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">12345</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- → Single-shard query、極快
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="c1">-- → Cross-shard aggregation、Citus 並行跑、合併結果</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-distribution-column-選錯--cross-shard-query-變主流">1. Distribution column 選錯 — Cross-shard query 變主流</h3>
<p>選 <code>created_at</code> 或 <code>id</code>（auto increment）作 distribution column、看起來均勻、實際 <em>application query 多以 user_id 為主</em>、變成 <em>每個 query 都 cross-shard</em>、performance 雪崩。</p>
<p>修法：</p>
<ul>
<li><em>Distribution column 選 application 最常 filter / join 的 column</em>（通常是 <code>tenant_id</code> / <code>user_id</code>）</li>
<li>Audit application top query、確認 distribution column 對齊 query pattern</li>
<li>改 distribution column 要 <em>rewrite 所有 shard</em>、像 resharding、大工程</li>
</ul>
<h3 id="2-cross-shard-transaction-限制">2. Cross-shard transaction 限制</h3>
<p>跨多 shard 的 transaction（如：UPDATE 兩個 user_id 不同的 row）Citus 用 <em>2PC</em>（two-phase commit）但有限制：</p>
<ul>
<li>Multi-statement transaction 跨 shard 需明確開 <code>SET citus.multi_shard_modify_mode = 'sequential'</code></li>
<li>部分 isolation level 不保證 serializable across shards</li>
<li>DDL 跨 shard 是 sequential</li>
</ul>
<p>修法：</p>
<ul>
<li>Schema design 避免 cross-shard transaction（同 colocation group 內 transaction 沒問題）</li>
<li>必要 cross-shard 場景明確設 multi-shard mode</li>
<li>對 <em>strict cross-shard consistency</em>、考慮 distributed SQL（CockroachDB / Aurora DSQL）</li>
</ul>
<h3 id="3-reference-table-過大--寫入廣播-cost-爆">3. Reference table 過大 — 寫入廣播 cost 爆</h3>
<p>Reference table 在每 worker 都有 copy、寫入 <em>廣播給所有 worker</em>。Reference table 100K row + 高頻寫入 → 寫一次寫 N worker、cost N x。</p>
<p>修法：</p>
<ul>
<li>Reference table 限 <em>小 + 寫入頻率低</em> 的 lookup data</li>
<li>超大表不該是 reference table、考慮 distributed</li>
<li>監控 reference table 寫入 rate、超 threshold 重新評估</li>
</ul>
<h3 id="4-colocate-沒對齊--隱性-cross-shard-join">4. Colocate 沒對齊 — 隱性 cross-shard JOIN</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看似可以、實際 cross-shard 慢
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">user_addresses</span><span class="w"> </span><span class="n">ua</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ua</span><span class="p">.</span><span class="n">user_id</span><span class="p">;</span></span></span></code></pre></div><p>若 <code>user_addresses</code> 沒 <code>colocate_with =&gt; 'orders'</code>、兩表 shard 分配獨立、JOIN 跨 worker。</p>
<p>修法：</p>
<ul>
<li>建相關 table 時 <code>colocate_with</code> 對齊</li>
<li>用 <code>SELECT * FROM citus_tables</code> 看 colocation_id、確認對齊</li>
<li>跨非 colocate table 的 JOIN 用 <em>materialized view</em> 或 application 層拆 query 避開</li>
</ul>
<h3 id="5-worker-failover--coordinator-必須知道">5. Worker failover — Coordinator 必須知道</h3>
<p>Worker 故障、Citus 預設 <em>coordinator 看到 query 失敗、不自動 failover</em>。</p>
<p>修法（Citus 11+）：</p>
<ul>
<li>用 <em>shard replication</em>（<code>citus.shard_replication_factor = 2</code>）— 每 shard 在 2 個 worker 有 copy</li>
<li>配 PG streaming replication 在 worker 層、外加 Patroni 管 failover</li>
<li>Coordinator 失敗 → 整個 cluster 失能、coordinator 也要 HA（Patroni）</li>
</ul>
<p>跟 Vitess 對比 Citus 的 HA story 較弱、production 必須認真規劃。</p>
<h2 id="何時用-citus">何時用 Citus</h2>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-tenant SaaS、tenant_id 為自然 distribution</td>
          <td>是</td>
      </tr>
      <tr>
          <td>寫吞吐 &gt; 50K WPS、單 PG 撐不住</td>
          <td>是</td>
      </tr>
      <tr>
          <td>需要保留 PG SQL + extension（pgvector / TimescaleDB）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>應用 query pattern 80% 都用同一 distribution column</td>
          <td>是</td>
      </tr>
      <tr>
          <td>應用大量 ad-hoc cross-tenant aggregation</td>
          <td>否（cross-shard 慢）</td>
      </tr>
      <tr>
          <td>強 cross-shard consistency 需求</td>
          <td>否（用 CockroachDB）</td>
      </tr>
      <tr>
          <td>想 zero-ops managed</td>
          <td>Azure Cosmos DB for PostgreSQL（同 engine）</td>
      </tr>
  </tbody>
</table>
<h2 id="容量規劃">容量規劃</h2>
<ul>
<li>Coordinator: 中等 CPU + RAM、metadata 不大、不存 data</li>
<li>Worker: per-worker spec 同 single PG production</li>
<li>Shard count: 預設 32、實務常設 worker count × 4-8</li>
<li>Replication factor: production 至少 2</li>
</ul>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>Coordinator + worker 各跑 PG streaming replication、Citus 不取代 PG replication。Worker failover 用 Patroni / streaming replication。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-pg-extensions">跟 PG Extensions</h3>
<p>Citus 跟其他 PG extension 多數兼容（pgvector / TimescaleDB / pg_stat_statements）— 它維持 <em>extension</em> 形態，保留 PostgreSQL 生態接點。詳見 <em>PG Extension Ecosystem</em> 篇（待寫）。</p>
<h3 id="跟-mysql-vitess">跟 MySQL Vitess</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Citus</th>
          <th>Vitess</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>PG extension</td>
          <td>獨立 proxy + tablet</td>
      </tr>
      <tr>
          <td>主要場景</td>
          <td>Multi-tenant SaaS</td>
          <td>超大規模分片</td>
      </tr>
      <tr>
          <td>Cross-shard JOIN</td>
          <td>colocate 對齊 + reference table</td>
          <td>VTGate 自動 split + aggregate</td>
      </tr>
      <tr>
          <td>FK</td>
          <td>同 colocation 內可用</td>
          <td>Vitess 18+ 支援、cross-shard 限制</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>依賴 Patroni + replication factor</td>
          <td>VTOrc + replication</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中（PG ops 經驗夠）</td>
          <td>高（4 component）</td>
      </tr>
  </tbody>
</table>
<p>Citus 對 <em>PG-native</em> 場景更平順、Vitess 對 <em>MySQL-native</em> 場景更平順、不直接競爭。詳見 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess Sharding</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（per-worker replication）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PG MVCC + Lock Model</a>（cross-shard transaction lock 行為）</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（Citus vs CockroachDB vs Spanner）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess Sharding</a>（sibling、不同實作）</li>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a>（Azure Cosmos DB for PostgreSQL = managed Citus）</li>
<li>官方：<a href="https://docs.citusdata.com/">Citus Documentation</a> / <a href="https://github.com/citusdata/citus">Citus on GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>3.C18 Wix：Greyhound TLLSR 解 consumer 卡住</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-greyhound-troubleshooting/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-greyhound-troubleshooting/</guid><description>&lt;p>這個案例的核心責任是說明大規模 multi-tenant Kafka 的營運可視性需求遠超原生 metric。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Wix 2000+ microservice、每天 66 billion Kafka 訊息、用自建 Greyhound（JVM library + polyglot sidecar）抽象 Kafka；troubleshooting 痛點是「卡住的 consumer 看不到原因、只能寫 DB 修復腳本」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>TLLSR 框架（Trace / Lookup / Longest-running / Skip-replay / Redistribute）解 single-partition lag、單筆 poison pill、handler 卡住等情境；consumer lag alert &amp;gt; 30 分鐘觸發。揭露原生 lag metric 無法定位「卡在哪」、需要 message-level trace + 操作介面。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：consumer lag / observability / multi-tenant / poison message。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/wix-engineering/troubleshooting-kafka-for-2000-microservices-at-wix-986ee382fd1e">Troubleshooting Kafka for 2000 Microservices at Wix&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明大規模 multi-tenant Kafka 的營運可視性需求遠超原生 metric。</p>
<h2 id="觀察">觀察</h2>
<p>Wix 2000+ microservice、每天 66 billion Kafka 訊息、用自建 Greyhound（JVM library + polyglot sidecar）抽象 Kafka；troubleshooting 痛點是「卡住的 consumer 看不到原因、只能寫 DB 修復腳本」。</p>
<h2 id="判讀">判讀</h2>
<p>TLLSR 框架（Trace / Lookup / Longest-running / Skip-replay / Redistribute）解 single-partition lag、單筆 poison pill、handler 卡住等情境；consumer lag alert &gt; 30 分鐘觸發。揭露原生 lag metric 無法定位「卡在哪」、需要 message-level trace + 操作介面。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：consumer lag / observability / multi-tenant / poison message。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/wix-engineering/troubleshooting-kafka-for-2000-microservices-at-wix-986ee382fd1e">Troubleshooting Kafka for 2000 Microservices at Wix</a></li>
</ul>
]]></content:encoded></item><item><title>Lacework</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/</guid><description>&lt;p>Lacework 是 CNAPP（Cloud-Native Application Protection Platform）走 &lt;em>Polygraph ML behavioral baseline&lt;/em> 路線的代表廠商、2024 年跟 Fortinet 合併、新品牌叫 &lt;em>Fortinet Lacework FortiCNAPP&lt;/em>、但 Lacework 名稱與獨立產品線仍在運作。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &amp;#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &amp;#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &amp;#43; workload &amp;#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security&lt;/a> 的差異在 &lt;em>偵測設計哲學&lt;/em>、覆蓋面相近 — Lacework 的核心競爭力是 Polygraph 自動從 log + process + network + cloud API call 學 baseline、anomaly 自動觸發、不需 SOC 手寫 detection rule。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Lacework 的核心定位是 &lt;em>Polygraph 驅動的 CNAPP&lt;/em>、以 ML 自動學習正常行為作為偵測基礎。產品線涵蓋四個能力面：&lt;em>CSPM&lt;/em>（Cloud Security Posture Management、misconfiguration 與合規 scan）、&lt;em>CWPP&lt;/em>（Cloud Workload Protection Platform、host + container runtime 防護）、&lt;em>Code Security&lt;/em>（IaC scan、container image scan、SAST baseline）、以及貫穿全平台的 &lt;em>Polygraph behavioral baseline engine&lt;/em>。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &amp;#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz&lt;/a> 比、設計哲學是相反的：Wiz 走 &lt;em>Security Graph + Toxic Combination&lt;/em>（你顯式定義「EC2 + RCE + IMDS v1 + cross-account role」是 toxic、graph 找匹配 path）、Lacework 走 &lt;em>Polygraph implicit baseline&lt;/em>（你不定義、ML 從 30 天歷史學 normal、偏離就 alert）。兩種都是 graph、但一個是 rule-driven graph、一個是 behavior-learned graph。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &amp;#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud&lt;/a> 比、Prisma 是 &lt;em>多模組 agent + agentless 寬覆蓋&lt;/em>、Lacework 主打 Polygraph 為單一核心引擎、不靠堆模組廣度競爭。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &amp;#43; workload &amp;#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS&lt;/a> 比、Falcon CS 是 &lt;em>endpoint EDR 延伸到 cloud&lt;/em>、Lacework 從第一天就為 cloud-native designed、沒 endpoint EDR 包袱。&lt;/p></description><content:encoded><![CDATA[<p>Lacework 是 CNAPP（Cloud-Native Application Protection Platform）走 <em>Polygraph ML behavioral baseline</em> 路線的代表廠商、2024 年跟 Fortinet 合併、新品牌叫 <em>Fortinet Lacework FortiCNAPP</em>、但 Lacework 名稱與獨立產品線仍在運作。它跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> / <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a> / <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a> 的差異在 <em>偵測設計哲學</em>、覆蓋面相近 — Lacework 的核心競爭力是 Polygraph 自動從 log + process + network + cloud API call 學 baseline、anomaly 自動觸發、不需 SOC 手寫 detection rule。</p>
<h2 id="服務定位">服務定位</h2>
<p>Lacework 的核心定位是 <em>Polygraph 驅動的 CNAPP</em>、以 ML 自動學習正常行為作為偵測基礎。產品線涵蓋四個能力面：<em>CSPM</em>（Cloud Security Posture Management、misconfiguration 與合規 scan）、<em>CWPP</em>（Cloud Workload Protection Platform、host + container runtime 防護）、<em>Code Security</em>（IaC scan、container image scan、SAST baseline）、以及貫穿全平台的 <em>Polygraph behavioral baseline engine</em>。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> 比、設計哲學是相反的：Wiz 走 <em>Security Graph + Toxic Combination</em>（你顯式定義「EC2 + RCE + IMDS v1 + cross-account role」是 toxic、graph 找匹配 path）、Lacework 走 <em>Polygraph implicit baseline</em>（你不定義、ML 從 30 天歷史學 normal、偏離就 alert）。兩種都是 graph、但一個是 rule-driven graph、一個是 behavior-learned graph。跟 <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a> 比、Prisma 是 <em>多模組 agent + agentless 寬覆蓋</em>、Lacework 主打 Polygraph 為單一核心引擎、不靠堆模組廣度競爭。跟 <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS</a> 比、Falcon CS 是 <em>endpoint EDR 延伸到 cloud</em>、Lacework 從第一天就為 cloud-native designed、沒 endpoint EDR 包袱。</p>
<p>關鍵張力：<em>implicit behavioral baseline</em> ↔ <em>explicit auditable rule</em> 是 Lacework 客戶最大的取捨。Polygraph 內部用 ML 學行為、好處是 zero rule maintenance、自動覆蓋未知 attack pattern；代價是內部邏輯不透明、false positive / false negative 都不容易 debug、強合規場景需要 explicit rule 可審計時會卡住。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Lacework 在 cloud security stack 中承擔哪段（CSPM / CWPP / Code Security / behavioral detection）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 等 SIEM 接 alert、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 事故處理</a> 接 IR routing）</li>
<li>Polygraph ML baseline 的 ownership 設計（誰調 anomaly threshold、false positive 由誰判讀、ML model retraining cadence 誰負責）</li>
<li><em>implicit baseline</em> vs <em>explicit rule</em> 的取捨何時偏 Lacework、何時要補 Wiz / Prisma 的 explicit rule layer</li>
<li>何時用 Lacework、何時走 Wiz / Prisma Cloud / Falcon CS</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Lacework deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Polygraph baseline 覆蓋面</strong>：哪些 cloud account / workload / container 進了 Polygraph 學習、baseline window 多長（預設 30 天）、新 workload 進來幾天才視為 baseline 成熟、未覆蓋的 workload 是否走 fallback rule</li>
<li><strong>Anomaly tuning ownership</strong>：誰看 Polygraph alert、false positive 由誰標記、標記後怎麼回饋 model、有沒有 <em>alert backlog grooming</em> lifecycle（不是黑箱 fire-and-forget）</li>
<li><strong>CSPM 跟合規 mapping</strong>：CIS / PCI / SOC 2 / HIPAA framework 哪些開、misconfiguration finding 走 ticket workflow（誰修、deadline）、Compliance report 多久 export 一次給 audit team</li>
<li><strong>跟 SIEM / SOAR handoff</strong>：Polygraph alert 是否同步進 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 給 SOC、是否跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> playbook 對接、high severity 是否觸發 SOAR</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Polygraph behavioral baseline</strong>：Lacework 的 first-class concept、從 cloud API call（CloudTrail / Audit Log）+ host process tree + container syscall + network connection 四種 source 同時學習、用 time-series graph 表達「正常情況下 user X 在 workload Y 上會 spawn process Z、連 destination W」。anomaly 是 graph 上不在 baseline 中的 edge、自動 trigger alert。baseline window 預設 30 天、新 workload 進來時用同類 workload 的 baseline 過渡、避免 cold start 全部 alert。</p>
<p><strong>CSPM（misconfiguration + compliance）</strong>：agentless 從 cloud API 拉 resource 設定、對照 CIS Benchmark / PCI / SOC 2 / HIPAA / CSA CCM 等 framework 跑 rule、出 finding。這部分是 <em>explicit rule</em>、不靠 Polygraph、跟 Wiz / Prisma 的 CSPM 能力同等級。Compliance report 可 schedule export 給 audit team。</p>
<p><strong>CWPP（host + container runtime）</strong>：兩種模式 — <em>agentless</em>（從 cloud API + snapshot 掃 vulnerability + misconfiguration、低 overhead 但無 runtime signal）、<em>agent-based</em>（Lacework agent on host / DaemonSet on K8s、提供 process tree + syscall + file integrity monitoring 給 Polygraph）。production runtime detection 必須 agent、不然 Polygraph 沒 process / syscall 資料源。</p>
<p><strong>Code Security（IaC + container image）</strong>：Terraform / CloudFormation / Helm chart 掃 misconfiguration、container image 掃 CVE + secret + SBOM、跟 <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 同層級。整合 GitHub / GitLab PR check、release gate 前 block 高風險 IaC。</p>
<p><strong>Compliance reporting</strong>：CSPM finding 自動 map 到 framework（CIS AWS / PCI DSS / SOC 2 等）、定期 export PDF / CSV 給 audit team、不需 SOC 手工整理。跨 cloud 帳號 aggregate view 對 multi-account 治理有用。</p>
<p><strong>跟 SIEM 整合</strong>：Polygraph alert 走 webhook / S3 export / Splunk Add-on 進 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>、做 cross-source correlation。Lacework 不取代 SIEM、是 cloud-native detection 的 <em>upstream signal source</em>。</p>
<p><strong>計費模型</strong>：按 workload count（vCPU 數 / container 數 / cloud account 數）+ 啟用模組。enterprise contract 為主、不公開 list price。跟 Wiz / Prisma 同模型、預算敏感場景需試算。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Lacework</th>
          <th>Wiz</th>
          <th>Prisma Cloud</th>
          <th>CrowdStrike Falcon CS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偵測設計哲學</td>
          <td>Polygraph ML implicit baseline</td>
          <td>Security Graph + 顯式 Toxic Combination</td>
          <td>多模組 rule + ML 混合</td>
          <td>EDR 延伸到 cloud、process-centric</td>
      </tr>
      <tr>
          <td>主要訴求</td>
          <td>zero rule maintenance、自動覆蓋未知 attack</td>
          <td>顯式 rule 可審計、cross-asset 關聯路徑清楚</td>
          <td>寬覆蓋、agent + agentless 混合</td>
          <td>endpoint + cloud 同 console、process tree 一致</td>
      </tr>
      <tr>
          <td>Runtime 偵測</td>
          <td>agent (Polygraph syscall + process tree)</td>
          <td>agent (Runtime Sensor、後加)</td>
          <td>agent (Defender)</td>
          <td>強 — 沿用 Falcon EDR agent</td>
      </tr>
      <tr>
          <td>Agentless scan</td>
          <td>強 — CSPM + vulnerability snapshot</td>
          <td>強 — agentless 為 design 起點</td>
          <td>強 — 雙模式並重</td>
          <td>中 — 為 Falcon agent 補位</td>
      </tr>
      <tr>
          <td>合規可審計</td>
          <td>中 — Polygraph 黑箱、CSPM 部分清楚</td>
          <td>強 — 顯式 rule、規則邏輯可審查</td>
          <td>強 — rule-based、模組化清楚</td>
          <td>中</td>
      </tr>
      <tr>
          <td>跟 SIEM 整合</td>
          <td>webhook / Splunk Add-on / S3</td>
          <td>webhook / 多家 SIEM connector</td>
          <td>多家 SIEM connector</td>
          <td>Falcon 自家 NG-SIEM 為主、外接次要</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>cloud-native + 信任 ML、不想自寫 detection rule</td>
          <td>多雲 + 要顯式 rule 治理、需 cross-asset 攻擊路徑</td>
          <td>Palo Alto-heavy 環境、寬覆蓋優先</td>
          <td>CrowdStrike-heavy 環境、endpoint + cloud 統一</td>
      </tr>
      <tr>
          <td>不適合場景</td>
          <td>強合規要 explicit rule 可審計、SOC 要 rule 客製化</td>
          <td>不想自己寫 rule、想 ML 自動覆蓋</td>
          <td>預算敏感（多模組計費容易膨脹）</td>
          <td>沒在用 Falcon EDR、純 cloud-native</td>
      </tr>
      <tr>
          <td>Fortinet 整合</td>
          <td>強（2024+ FortiCNAPP、跟 NGFW / FortiSOAR 整合）</td>
          <td>無 Fortinet 直接整合</td>
          <td>無 Fortinet 直接整合</td>
          <td>無 Fortinet 直接整合</td>
      </tr>
  </tbody>
</table>
<p>選 Lacework 的核心訴求：<em>cloud-native + 信任 ML behavioral baseline + 不想養 detection engineering team 寫 rule</em> + 願意接受 Polygraph 是相對黑箱、false positive 要由 ML retraining 而非 rule edit 解決。強合規要 explicit rule 可審計、或 SOC 要深度 rule 客製化、走 Wiz / Prisma 更合適。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Polygraph internals</strong>：Polygraph 不是單一 ML model、是 time-series behavioral graph + 多個 detection algorithm 組合。node 是 entity（user / workload / process / network endpoint）、edge 是 observed interaction、edge 上掛 frequency + temporal pattern。anomaly detection 用 unsupervised learning（clustering + outlier detection）找 baseline 外的 edge。優點是 <em>zero-day attack pattern 不需事先定義也可能偵測到</em>（行為偏離即可）、缺點是 detection 為何 trigger / 為何沒 trigger 都不易解釋、tuning 不是改 rule、是調整 baseline window 或標記 false positive 回饋 model。</p>
<p><strong>Fortinet FortiCNAPP 整合（2024+）</strong>：Fortinet 收購後加速跟 <em>Fortinet NGFW</em>（network log 進 Polygraph 當 source）、<em>FortiSOAR</em>（Lacework alert 自動觸發 firewall block / endpoint isolation playbook）、<em>FortiSandbox</em>（suspicious file 進 sandbox 再回饋 baseline）整合。Fortinet-heavy 環境吃整合紅利、非 Fortinet 環境 Polygraph 跟原 connector 仍獨立運作。</p>
<p><strong>Anomaly tuning lifecycle</strong>：Polygraph alert 出來不是終點、要走 <em>triage → label false positive → ML model retraining</em> lifecycle。實務上 SOC 看 alert 標 <em>true positive / false positive / benign anomaly</em>（合法但意外）、Lacework 後台用 label 重訓 model、下一個 baseline cycle 調整。組織要決定 <em>誰負責 label</em>（SOC analyst / detection engineer）、<em>backlog grooming cadence</em>（每週 / 每月）、<em>retraining cycle</em>（自動 / 手動觸發）。沒 lifecycle 就是「alert 看一陣子放著」、Polygraph 退化成噪音源。</p>
<p><strong>跨 SIEM webhook / SOAR 整合</strong>：alert 推 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 後、SOC 可用 SIEM correlation 補 cross-source（例如 Polygraph anomaly + Okta MFA fail + GitHub clone spike）、再進 SOAR playbook 自動 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> rotate / <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> block。Lacework 是 <em>detective layer</em>、SIEM 是 <em>correlation + orchestration layer</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>新 workload 進來大量 alert（cold start）</strong>：baseline 還沒建好、ML 把正常當異常 — 用同類 workload baseline 過渡、給 7-14 天 warm-up 再 enforce alert</li>
<li><strong>Polygraph alert 看不懂為何 trigger</strong>：ML 黑箱本質、不像 explicit rule 可指 line — 看 alert 帶的 <em>involved entities + observed deviation</em>、跨 entity 對 baseline 看差異、必要時補 Wiz / Prisma explicit rule 在強合規場景</li>
<li><strong>False positive 持續多但 model 沒進步</strong>：label lifecycle 沒跑、analyst 把 alert dismiss 沒打 label — 強制走 <em>true positive / false positive / benign anomaly</em> triage、不能直接 close</li>
<li><strong>Agent 沒裝 / 裝不到的 workload</strong>：legacy host / serverless / edge node 沒 agent、Polygraph 只有 cloud API source 沒 process / syscall — 接受 agentless-only 覆蓋面、不要假設 Polygraph 全 stack 看得到</li>
<li><strong>CSPM finding backlog 爆炸</strong>：framework 一次開全、misconfiguration 數千條沒人修 — 分批 enable framework、按 severity + asset criticality 排優先級、走 ticket workflow + deadline</li>
<li><strong>Compliance audit 要 explicit rule 可審查</strong>：Polygraph 內部邏輯不能交給 auditor — CSPM 部分可以審（是 explicit rule）、Polygraph 部分要補 detection engineering 文件 + label history 證明 ML 有治理</li>
<li><strong>Alert 進 SIEM 後沒 correlation</strong>：Lacework alert 跟 IdP / WAF / cloud control plane log 沒在 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 跨 source 串 — 寫 correlation rule 把 Polygraph anomaly 當 <em>one signal</em>、不是當 final verdict</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>顯式 rule + 多雲 cross-asset 路徑</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a></td>
      </tr>
      <tr>
          <td>寬覆蓋 + Palo Alto-heavy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a></td>
      </tr>
      <tr>
          <td>Endpoint EDR + cloud 統一</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a></td>
      </tr>
      <tr>
          <td>SIEM 主導、CNAPP signal 進 SOC</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>Container image / IaC scan 為主</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></td>
      </tr>
      <tr>
          <td>資料分類 / DLP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Polygraph ML 演算法的學術細節（unsupervised clustering / graph anomaly detection 具體方法）</li>
<li>FortiCNAPP 跟 Fortinet 其他產品（FortiGate / FortiAnalyzer / FortiSIEM）的 deep integration 設定</li>
<li>Lacework Labs threat research 報告的逐篇解讀</li>
<li>完整 CIS / PCI / SOC 2 framework 對應的 rule 清單</li>
<li>Container runtime 防護的 OS-level 細節（cgroup / namespace / seccomp）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Lacework 在 07 案例庫沒有直接 vendor-level 事件、但多個 case 是 Polygraph behavioral baseline 的對照啟示：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Lacework 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>Polygraph 在 SolarWinds 期間可從 Orion 程序的 DNS callback 行為偏離 baseline 偵測、不靠 IoC list — Lacework marketing 強打的 zero-day 案例</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>Desktop app process spawn 異常 + unusual outbound 是 Polygraph baseline 可抓的 pattern、補簽章驗證通過後的 runtime 偵測窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Polygraph 偵測 JNDI lookup 後的 outbound LDAP 連線異常、補 CVE scanner agent rollout 之前的偵測窗口、不依賴事先 CVE 公開</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>對照啟示：Polygraph 對 cloud API call pattern 異常（短時間大量 GetObject / 跨 schema query）可 baseline-based 偵測、不需事先寫 query rule</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>Polygraph 把 detection lifecycle 從「寫 rule → tune → review」改成「baseline → label false positive → retrain」、流程不同但治理責任沒消失</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>Polygraph 自動 baseline 不等於免 alert fatigue — label lifecycle 跟 retraining cadence 沒做、false positive 一樣會淹 SOC</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a>、<a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a>、<a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon Cloud Security</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（SIEM 接 Polygraph alert）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（Code Security 重疊、CI 階段優先級）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（SOAR playbook 拉 API rotate）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Polygraph alert → IR routing）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（log pipeline 共用）</li>
<li>官方：<a href="https://docs.lacework.net/">Lacework Documentation</a> / <a href="https://www.fortinet.com/products/forticnapp">Fortinet Lacework FortiCNAPP</a></li>
</ul>
]]></content:encoded></item><item><title>9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/</guid><description>&lt;p>這個案例的核心責任是說明「SaaS 類 surge」跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO&lt;/a> 的「product surge」差異。Zoom 的 30 倍成長不是「產品爆紅」、是「外部事件（COVID）逼全世界改變工作模式」、突發是 &lt;em>結構性&lt;/em> 的、不是回歸均值的暫時現象。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Zoom 在 2020 年 COVID 期間的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers&lt;/a>）：&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>日活參與者&lt;/td>
 &lt;td>1000 萬 → 3 億（2020 年 3 月）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成長倍數&lt;/td>
 &lt;td>30x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主資料層&lt;/td>
 &lt;td>Amazon DynamoDB（會議 metadata）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>擴容描述&lt;/td>
 &lt;td>「nearly infinitely with no performance issues」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵敘述：「On the backend, they were able to manage this surge with Amazon DynamoDB for Zoom Meetings.」&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Zoom surge 揭露三個 SaaS 突發成長的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>SaaS surge 是結構性、不是暫時性&lt;/strong>：Pokemon GO 上線爆紅後流量會隨熱度消退、Zoom COVID 成長是「永久 baseline 上移」。容量規劃不能假設「過幾個月會回來」、必須假設「3 億 DAU 是新常態」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的長期 baseline 重新校準。&lt;/li>
&lt;li>&lt;strong>DynamoDB 「無限擴容」對 SaaS 元資料層特別適用&lt;/strong>：Zoom 會議 metadata（room ID、participant list、permission state）是典型 KV 工作負載、partition key（meeting_id）天然均勻、不會 hot partition。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 同樣的 partition 均勻優勢。&lt;/li>
&lt;li>&lt;strong>媒體串流不在 DynamoDB&lt;/strong>：Zoom 的影音流量是 P2P + edge servers、不經 DynamoDB。DynamoDB 只承擔「control plane」、不承擔「data plane」。這個分離是擴 30 倍的前提 — 控制面跟資料面解耦、控制面用 managed 服務、資料面用專屬基礎設施。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 的關鍵路徑切分。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「nearly infinitely」是行銷敘述、不是工程承諾。實務上 Zoom 在 COVID 初期確實遇到 outage 與性能問題、後續才穩定。讀案例時要看 &lt;em>最終狀態&lt;/em> 跟 &lt;em>過程中的 incident&lt;/em>。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>控制面跟資料面分離&lt;/strong>：高頻 metadata 操作放 managed KV（DynamoDB / Cosmos DB / Firestore）、大資料量串流放專屬基礎設施（CDN / WebRTC / 自管 servers）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a>。&lt;/li>
&lt;li>&lt;strong>surge 後重新校準 SLO baseline&lt;/strong>：30x 成長之後、SLO 的「正常範圍」要更新、否則 monitoring 會誤報。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 SLO 演進。&lt;/li>
&lt;li>&lt;strong>長期 surge 觸發架構重新評估&lt;/strong>：DynamoDB 是「擴大量」的好選擇、但成本也跟著放大。當 baseline 從 1000 萬永久升到 3 億、原本的 on-demand 模式可能變得貴、要考慮 provisioned + auto-scaling 組合。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：Google Meet 也用 Spanner / Firestore、Microsoft Teams 用 Cosmos DB — 三家視訊會議都靠 managed KV 撐 metadata、是同一個架構模式的不同 vendor 實作。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「SaaS 類 surge」跟 <a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO</a> 的「product surge」差異。Zoom 的 30 倍成長不是「產品爆紅」、是「外部事件（COVID）逼全世界改變工作模式」、突發是 <em>結構性</em> 的、不是回歸均值的暫時現象。</p>
<h2 id="觀察">觀察</h2>
<p>Zoom 在 2020 年 COVID 期間的關鍵敘述（引自 <a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日活參與者</td>
          <td>1000 萬 → 3 億（2020 年 3 月）</td>
      </tr>
      <tr>
          <td>成長倍數</td>
          <td>30x</td>
      </tr>
      <tr>
          <td>主資料層</td>
          <td>Amazon DynamoDB（會議 metadata）</td>
      </tr>
      <tr>
          <td>擴容描述</td>
          <td>「nearly infinitely with no performance issues」</td>
      </tr>
  </tbody>
</table>
<p>關鍵敘述：「On the backend, they were able to manage this surge with Amazon DynamoDB for Zoom Meetings.」</p>
<h2 id="判讀">判讀</h2>
<p>Zoom surge 揭露三個 SaaS 突發成長的工程重點。</p>
<ol>
<li><strong>SaaS surge 是結構性、不是暫時性</strong>：Pokemon GO 上線爆紅後流量會隨熱度消退、Zoom COVID 成長是「永久 baseline 上移」。容量規劃不能假設「過幾個月會回來」、必須假設「3 億 DAU 是新常態」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的長期 baseline 重新校準。</li>
<li><strong>DynamoDB 「無限擴容」對 SaaS 元資料層特別適用</strong>：Zoom 會議 metadata（room ID、participant list、permission state）是典型 KV 工作負載、partition key（meeting_id）天然均勻、不會 hot partition。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 同樣的 partition 均勻優勢。</li>
<li><strong>媒體串流不在 DynamoDB</strong>：Zoom 的影音流量是 P2P + edge servers、不經 DynamoDB。DynamoDB 只承擔「control plane」、不承擔「data plane」。這個分離是擴 30 倍的前提 — 控制面跟資料面解耦、控制面用 managed 服務、資料面用專屬基礎設施。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的關鍵路徑切分。</li>
</ol>
<p>需要警惕：「nearly infinitely」是行銷敘述、不是工程承諾。實務上 Zoom 在 COVID 初期確實遇到 outage 與性能問題、後續才穩定。讀案例時要看 <em>最終狀態</em> 跟 <em>過程中的 incident</em>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>控制面跟資料面分離</strong>：高頻 metadata 操作放 managed KV（DynamoDB / Cosmos DB / Firestore）、大資料量串流放專屬基礎設施（CDN / WebRTC / 自管 servers）。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a>。</li>
<li><strong>surge 後重新校準 SLO baseline</strong>：30x 成長之後、SLO 的「正常範圍」要更新、否則 monitoring 會誤報。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 SLO 演進。</li>
<li><strong>長期 surge 觸發架構重新評估</strong>：DynamoDB 是「擴大量」的好選擇、但成本也跟著放大。當 baseline 從 1000 萬永久升到 3 億、原本的 on-demand 模式可能變得貴、要考慮 provisioned + auto-scaling 組合。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
</ol>
<p>跨平台等效：Google Meet 也用 Spanner / Firestore、Microsoft Teams 用 Cosmos DB — 三家視訊會議都靠 managed KV 撐 metadata、是同一個架構模式的不同 vendor 實作。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照 product surge → <a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO</a></li>
<li>想理解 control plane vs data plane → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> + <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
<li>想規劃 surge 後的 SLO → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a></li>
<li>想評估 surge 下的 on-demand vs provisioned 切換 → <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 on-demand vs provisioned</a></li>
<li>想避免 surge 觸發 hot partition → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/innovators/zoom/">Zoom Video Communications on AWS</a></li>
</ul>
]]></content:encoded></item><item><title>4.18 Observability Operating Model</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>operating model 的責任：定義誰擁有訊號、誰維護 dashboard、誰處理 alert、誰承擔成本&lt;/li>
&lt;li>角色分工：platform team、service team、on-call、incident commander、security / compliance&lt;/li>
&lt;li>ownership 欄位：owner、review cadence、retention、cost center、runbook link、deprecation date&lt;/li>
&lt;li>生命週期：新增、審核、使用、修訂、淘汰&lt;/li>
&lt;li>治理節奏：dashboard review、alert review、cost review、post-incident write-back&lt;/li>
&lt;li>跟 4.15 cost attribution 的關係：成本歸屬是 operating model 的一部分&lt;/li>
&lt;li>跟 08 的關係：事故時使用同一組 owner 與 escalation route&lt;/li>
&lt;li>反模式：平台團隊擁有所有 alert；service team 不看 dashboard；成本無 owner&lt;/li>
&lt;/ul>
&lt;p>Observability operating model 的價值是把觀測從「工具責任」改成「服務責任」。平台團隊提供共用能力，服務團隊提供業務語意，on-call 使用這些資產做決策；operating model 負責固定三者的接口。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability operating model 是把觀測資產的責任分配明確化的治理模型，責任是讓訊號有人維護、告警有人回應、成本有人決策。&lt;/p>
&lt;p>這一頁處理的是 ownership。可觀測性需要平台工具、服務脈絡、操作責任與淘汰條件一起維持。&lt;/p>
&lt;p>這層的判準是事故當下能否立刻知道誰要看哪個面板、誰有權調整閾值、誰負責決定淘汰過期訊號。dashboard 數量與 alert 覆蓋率只是輔助訊號。&lt;/p>
&lt;h2 id="角色分工">角色分工&lt;/h2>
&lt;p>Observability operating model 的角色分工以「誰能做決策」為核心。owner 是有權維護、調整、下架或升級觀測資產的人，名義聯絡人只能作為補充欄位。&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>Platform team&lt;/td>
 &lt;td>採集、儲存、查詢、成本與標準&lt;/td>
 &lt;td>pipeline、schema convention、quota&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service team&lt;/td>
 &lt;td>服務語意、核心旅程與業務事件&lt;/td>
 &lt;td>service dashboard、SLI、alert rule&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>On-call&lt;/td>
 &lt;td>事中判讀、runbook 使用與升級&lt;/td>
 &lt;td>silence、escalate、incident intake&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident commander&lt;/td>
 &lt;td>事故優先序、通訊節奏與決策紀錄&lt;/td>
 &lt;td>severity、rollback、status update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security / compliance&lt;/td>
 &lt;td>audit log、PII、retention 與 evidence&lt;/td>
 &lt;td>retention、masking、access review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Finance / cost owner&lt;/td>
 &lt;td>成本歸屬、預算與 chargeback&lt;/td>
 &lt;td>quota、retention tier、cost review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Platform team 的責任是維持共同語言。它需要定義 service name、environment、region、tenant、trace context、retention tier 與成本政策，讓跨服務查詢可行。&lt;/p>
&lt;p>Service team 的責任是維持服務語意。它需要定義哪些 user journey 是核心、哪些錯誤影響用戶、哪些 dependency failure 需要 alert、哪些 dashboard 仍有操作價值。&lt;/p>
&lt;p>On-call 的責任是把資產用在事中決策。alert 應能帶到 dashboard、runbook 與 owner，讓 operating model 真正進入操作流程。&lt;/p>
&lt;p>Security / compliance 的責任是把觀測資料的證據價值與資料風險同時納入治理。audit log、PII redaction、retention 與 access review 需要在觀測模型中有明確 owner。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 operating model 時，先看每個觀測資產是否有 owner，再看 owner 是否有權限與節奏採取行動。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>dashboard 是否有明確使用者與 review cadence&lt;/li>
&lt;li>alert 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、owner 與 escalation path&lt;/li>
&lt;li>高成本訊號是否能對應服務價值與成本中心&lt;/li>
&lt;li>post-incident review 是否能回寫到訊號 owner&lt;/li>
&lt;li>orphan dashboard 與 stale alert 是否有清理流程&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資產類型&lt;/th>
 &lt;th>Owner&lt;/th>
 &lt;th>週期&lt;/th>
 &lt;th>關閉條件&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>service team + on-call&lt;/td>
 &lt;td>月檢&lt;/td>
 &lt;td>無使用者、無判讀價值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>service owner&lt;/td>
 &lt;td>週檢&lt;/td>
 &lt;td>重複、誤報高、無行動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query / Schema&lt;/td>
 &lt;td>platform + service&lt;/td>
 &lt;td>變更檢&lt;/td>
 &lt;td>欄位漂移、查詢成本失控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost Attribution&lt;/td>
 &lt;td>cost owner&lt;/td>
 &lt;td>月檢&lt;/td>
 &lt;td>成本缺少服務價值對應&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="觀測資產欄位">觀測資產欄位&lt;/h2>
&lt;p>Observability asset 需要像服務 artifact 一樣有 metadata。沒有 metadata 的 dashboard、alert、query 與 schema 會在幾個月後變成無人敢刪、無人敢改、也無人信任的資產。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>operating model 的責任：定義誰擁有訊號、誰維護 dashboard、誰處理 alert、誰承擔成本</li>
<li>角色分工：platform team、service team、on-call、incident commander、security / compliance</li>
<li>ownership 欄位：owner、review cadence、retention、cost center、runbook link、deprecation date</li>
<li>生命週期：新增、審核、使用、修訂、淘汰</li>
<li>治理節奏：dashboard review、alert review、cost review、post-incident write-back</li>
<li>跟 4.15 cost attribution 的關係：成本歸屬是 operating model 的一部分</li>
<li>跟 08 的關係：事故時使用同一組 owner 與 escalation route</li>
<li>反模式：平台團隊擁有所有 alert；service team 不看 dashboard；成本無 owner</li>
</ul>
<p>Observability operating model 的價值是把觀測從「工具責任」改成「服務責任」。平台團隊提供共用能力，服務團隊提供業務語意，on-call 使用這些資產做決策；operating model 負責固定三者的接口。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability operating model 是把觀測資產的責任分配明確化的治理模型，責任是讓訊號有人維護、告警有人回應、成本有人決策。</p>
<p>這一頁處理的是 ownership。可觀測性需要平台工具、服務脈絡、操作責任與淘汰條件一起維持。</p>
<p>這層的判準是事故當下能否立刻知道誰要看哪個面板、誰有權調整閾值、誰負責決定淘汰過期訊號。dashboard 數量與 alert 覆蓋率只是輔助訊號。</p>
<h2 id="角色分工">角色分工</h2>
<p>Observability operating model 的角色分工以「誰能做決策」為核心。owner 是有權維護、調整、下架或升級觀測資產的人，名義聯絡人只能作為補充欄位。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>核心責任</th>
          <th>決策權限</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Platform team</td>
          <td>採集、儲存、查詢、成本與標準</td>
          <td>pipeline、schema convention、quota</td>
      </tr>
      <tr>
          <td>Service team</td>
          <td>服務語意、核心旅程與業務事件</td>
          <td>service dashboard、SLI、alert rule</td>
      </tr>
      <tr>
          <td>On-call</td>
          <td>事中判讀、runbook 使用與升級</td>
          <td>silence、escalate、incident intake</td>
      </tr>
      <tr>
          <td>Incident commander</td>
          <td>事故優先序、通訊節奏與決策紀錄</td>
          <td>severity、rollback、status update</td>
      </tr>
      <tr>
          <td>Security / compliance</td>
          <td>audit log、PII、retention 與 evidence</td>
          <td>retention、masking、access review</td>
      </tr>
      <tr>
          <td>Finance / cost owner</td>
          <td>成本歸屬、預算與 chargeback</td>
          <td>quota、retention tier、cost review</td>
      </tr>
  </tbody>
</table>
<p>Platform team 的責任是維持共同語言。它需要定義 service name、environment、region、tenant、trace context、retention tier 與成本政策，讓跨服務查詢可行。</p>
<p>Service team 的責任是維持服務語意。它需要定義哪些 user journey 是核心、哪些錯誤影響用戶、哪些 dependency failure 需要 alert、哪些 dashboard 仍有操作價值。</p>
<p>On-call 的責任是把資產用在事中決策。alert 應能帶到 dashboard、runbook 與 owner，讓 operating model 真正進入操作流程。</p>
<p>Security / compliance 的責任是把觀測資料的證據價值與資料風險同時納入治理。audit log、PII redaction、retention 與 access review 需要在觀測模型中有明確 owner。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 operating model 時，先看每個觀測資產是否有 owner，再看 owner 是否有權限與節奏採取行動。</p>
<p>重點訊號包括：</p>
<ul>
<li>dashboard 是否有明確使用者與 review cadence</li>
<li>alert 是否有 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、owner 與 escalation path</li>
<li>高成本訊號是否能對應服務價值與成本中心</li>
<li>post-incident review 是否能回寫到訊號 owner</li>
<li>orphan dashboard 與 stale alert 是否有清理流程</li>
</ul>
<table>
  <thead>
      <tr>
          <th>資產類型</th>
          <th>Owner</th>
          <th>週期</th>
          <th>關閉條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dashboard</td>
          <td>service team + on-call</td>
          <td>月檢</td>
          <td>無使用者、無判讀價值</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>service owner</td>
          <td>週檢</td>
          <td>重複、誤報高、無行動</td>
      </tr>
      <tr>
          <td>Query / Schema</td>
          <td>platform + service</td>
          <td>變更檢</td>
          <td>欄位漂移、查詢成本失控</td>
      </tr>
      <tr>
          <td>Cost Attribution</td>
          <td>cost owner</td>
          <td>月檢</td>
          <td>成本缺少服務價值對應</td>
      </tr>
  </tbody>
</table>
<h2 id="觀測資產欄位">觀測資產欄位</h2>
<p>Observability asset 需要像服務 artifact 一樣有 metadata。沒有 metadata 的 dashboard、alert、query 與 schema 會在幾個月後變成無人敢刪、無人敢改、也無人信任的資產。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Owner</td>
          <td>指定維護與決策責任</td>
          <td>事故時知道找誰</td>
      </tr>
      <tr>
          <td>User</td>
          <td>說明誰會使用這個資產</td>
          <td>判斷是否仍有操作價值</td>
      </tr>
      <tr>
          <td>Runbook link</td>
          <td>連到下一步操作</td>
          <td>讓 alert 能轉成行動</td>
      </tr>
      <tr>
          <td>Review cadence</td>
          <td>定義檢視頻率</td>
          <td>避免 stale dashboard / alert</td>
      </tr>
      <tr>
          <td>Cost center</td>
          <td>對應服務或團隊成本</td>
          <td>支援 chargeback 與 retention 決策</td>
      </tr>
      <tr>
          <td>Retention tier</td>
          <td>指定保存時間與查詢粒度</td>
          <td>平衡法規、事故與成本</td>
      </tr>
      <tr>
          <td>Deprecation date</td>
          <td>標示預計下架或重檢日期</td>
          <td>避免觀測資產永久堆積</td>
      </tr>
      <tr>
          <td>Data limitation</td>
          <td>標示抽樣、缺口與聚合限制</td>
          <td>避免事中誤讀資料</td>
      </tr>
  </tbody>
</table>
<p>Owner 欄位要搭配權限才有意義。有效 owner 需要能調整 threshold、更新 dashboard、下架 query 或決定 retention，讓 ownership 成為可執行責任。</p>
<p>User 欄位能避免 dashboard 變成展示資產。面板若沒有明確使用者，例如 on-call、service owner、capacity planner 或 compliance reviewer，就很難判斷它是否仍值得維護。</p>
<p>Runbook link 是 alert 從通知變成行動的關鍵。每個可 page 的 alert 都應連到第一步查詢、初始判讀、升級條件與 rollback / degrade / wait 的決策路由。</p>
<p>Cost center 讓觀測成本有業務語意。高 cardinality、長 retention、full-fidelity trace 與大量 log indexing 都有價值，但價值需要由能受益的服務或團隊承擔與檢視。</p>
<h2 id="生命週期">生命週期</h2>
<p>Observability operating model 的生命週期是新增、審核、使用、修訂與淘汰。這個生命週期讓訊號保持有用，並讓觀測資產累積在可治理範圍內。</p>
<ol>
<li>新增：服務變更、事故復盤、演練需求或合規要求產生新訊號。</li>
<li>審核：確認 schema、成本、owner、runbook 與 retention。</li>
<li>使用：進入 dashboard、alert、incident intake 或 SLO 計算。</li>
<li>修訂：根據噪音、缺口、成本與使用頻率調整。</li>
<li>淘汰：移除 stale alert、orphan dashboard、過期 query 與無價值高成本訊號。</li>
</ol>
<p>新增訊號需要清楚的需求來源。最好的來源是 user journey、SLO、incident review、game day 或 audit requirement；最弱的來源是「可能有用」。</p>
<p>審核訊號需要同時看語意與成本。欄位是否穩定、cardinality 是否可控、retention 是否合理、PII 是否被遮罩、owner 是否能維護，都是訊號上線前的固定問題。</p>
<p>淘汰是 operating model 的必要能力。舊 alert 沒有人敢關，會增加 alert fatigue；舊 dashboard 沒有人敢刪，會讓事故時不知道哪個面板可信。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>alert 觸發後沒人知道該由平台或服務團隊處理</li>
<li>dashboard 存在但半年無人打開</li>
<li>成本暴增時只能找平台團隊吸收</li>
<li>post-incident review 指派 action item，但沒有訊號 owner</li>
<li>service team 調整欄位後，平台查詢與 dashboard 斷裂</li>
</ul>
<p>實務上常見的治理斷點是「有 owner 名字，缺 owner 權限」。owner 需要能調整 alert、建立或下架 dashboard、分配成本，治理流程才會停在資產責任人，減少回流到平台集中處理的積壓。</p>
<h2 id="治理節奏">治理節奏</h2>
<p>Operating model 的治理節奏把觀測資產拉回日常工程流程。review cadence 的重點是定期回答「這個資產還能支援決策嗎」，會議只是其中一種執行形式。</p>
<table>
  <thead>
      <tr>
          <th>節奏</th>
          <th>核心問題</th>
          <th>典型輸出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dashboard review</td>
          <td>面板是否仍有人用、是否對應旅程</td>
          <td>更新、合併、下架</td>
      </tr>
      <tr>
          <td>Alert review</td>
          <td>alert 是否可行動、噪音是否可接受</td>
          <td>threshold 調整、silence、runbook</td>
      </tr>
      <tr>
          <td>Cost review</td>
          <td>成本是否對應服務價值</td>
          <td>retention tier、sampling policy</td>
      </tr>
      <tr>
          <td>Schema review</td>
          <td>欄位是否穩定、是否跨服務一致</td>
          <td>schema migration、drift 修正</td>
      </tr>
      <tr>
          <td>Post-incident write-back</td>
          <td>復盤缺口是否回寫到訊號與 owner</td>
          <td>新 alert、新 dashboard、新 runbook</td>
      </tr>
  </tbody>
</table>
<p>Dashboard review 應看使用情境與操作價值。面板需要支援 on-call 的前 10 分鐘、capacity planning 或 SLO review；脫離這些用途的面板適合合併、重命名或下架。</p>
<p>Alert review 應看行動品質。alert 若經常觸發但缺少明確處置，通常更適合變成 dashboard signal、ticket 或長期治理項。</p>
<p>Cost review 應看服務價值。觀測成本上升不一定是壞事，但需要能說明這些成本降低了哪一種事故風險、合規風險或容量風險。</p>
<h2 id="規模差異下的角色配置">規模差異下的角色配置</h2>
<p>Operating model 的角色配置隨組織規模調整。可投入的治理人力、可承受的協調成本、可維持的審核頻率三項一起決定當前該採哪種配置。把大組織的治理模型套到小團隊會造成過度治理；把小團隊的鬆散模型套到大組織會造成責任懸空。</p>
<p>本段聚焦常態 ownership 配置（不同規模下角色矩陣的差異）；遷移期的節奏取捨由 <a href="/blog/backend/04-observability/telemetry-pipeline/#%e8%a6%8f%e6%a8%a1%e5%b7%ae%e7%95%b0%e4%b8%8b%e7%9a%84%e9%81%b7%e7%a7%bb%e7%af%80%e5%a5%8f" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 規模差異下的遷移節奏</a> 處理、兩者 lens 不同。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模差異下觀測遷移</a>：揭露「規模差異會放大不同治理失分模式」的方向；case 主場景是觀測遷移、本章將此 frame 借用到常態 operating model 場景、以下展開屬通用工程知識補充。</p>
<p>小型組織的 operating model 重點是「角色合一、節奏明確」。一個 SRE 同時承擔 platform、service、on-call、cost owner 多重身份。治理重點是顯式記錄當前 ownership 跟 review cadence、避免角色合一被誤讀成默契傳遞（「大家都管 = 沒人管」是典型失敗）。Dashboard review、alert review、cost review 可以合併在同一個月會中，但要有具體的決議紀錄。</p>
<p>中型組織開始出現 platform 跟 service team 的分化，治理失分集中在介面定義。schema convention、cardinality 限制、cost center 命名規約若未在 platform / service 之間明確化，會在跨服務查詢時持續出現拼接斷裂。中型組織適合先固化「平台保證什麼、服務保證什麼」的契約，再擴大角色拆分。</p>
<p>大型組織的 operating model 牽涉多層 platform team、跨地區 on-call、合規 / 安全 / 財務的橫切責任。治理失分的核心來源是審核節奏跟不上資產成長速度 — 角色分工通常已經清晰，但每週 / 每月人工 review 數百個 dashboard / alert 不切實際。大型組織需要自動化的 stale dashboard 偵測、orphan alert 提示、retention compliance 報表，把 review 從手動週期變成事件驅動，讓治理隨資產數量自動擴展。</p>
<p>三類組織的共同前提是先把 ownership 視為可演進的、再決定當前該採哪種配置。組織成長過程中 ownership 矩陣會反覆調整，每次調整都要把新配置寫進文件、進入 release / runbook 流程、讓 ownership 變更跟釋出流程同步可見。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Observability operating model 的反模式通常是責任集中或責任懸空。前者讓平台團隊成為所有訊號的瓶頸，後者讓服務團隊在事故時找不到可信入口。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台擁有所有 alert</td>
          <td>服務語意缺失，告警只能看基礎設施</td>
          <td>service owner 擁有服務級 alert</td>
      </tr>
      <tr>
          <td>服務各自為政</td>
          <td>欄位、命名、retention 不一致</td>
          <td>platform 提供 schema convention</td>
      </tr>
      <tr>
          <td>owner 缺權限</td>
          <td>只能被追責，缺少資產修正能力</td>
          <td>owner 取得調整、下架與預算權限</td>
      </tr>
      <tr>
          <td>成本無歸屬</td>
          <td>高成本訊號由平台吸收</td>
          <td>cost center 與 retention tier</td>
      </tr>
      <tr>
          <td>復盤無回寫</td>
          <td>action item 停在文件</td>
          <td>write-back 到 dashboard / alert</td>
      </tr>
  </tbody>
</table>
<p>平台擁有所有 alert 會讓服務語意被削弱。平台知道 pipeline 與 infra，但通常不知道某個錯誤是否影響 checkout、資料同步、帳單或客戶 SLA。</p>
<p>服務各自為政會讓跨服務事故難以判讀。每個服務都可以有自己的 dashboard，但 service name、environment、region、tenant、error class 與 trace context 需要共用標準。</p>
<p>復盤無回寫會讓 operating model 停在文件。post-incident review 揭露的偵測缺口、runbook 缺口與成本缺口都應回到對應 owner 的資產生命週期。</p>
<h2 id="與事故流程的關係">與事故流程的關係</h2>
<p>Observability operating model 是事故流程的責任基礎。事故期間，IC 需要知道哪些訊號可信、哪個 owner 能解釋欄位、誰能調整 alert、誰能決定保留或匯出 evidence。</p>
<p>在 incident command 中，observability owner 不一定是 incident commander，但必須能提供訊號解釋與操作建議。當 telemetry data quality 有限制時，owner 需要把限制交給 scribe 或 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a>。</p>
<p>在 runbook lifecycle 中，dashboard、alert 與 query 都應被視為 runbook 的依賴。runbook 更新時，如果沒有同步更新觀測資產，下一次事故仍會走到舊入口。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard / alert</a>：設計 owner、runbook 與停止條件</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance loop</a>：淘汰 stale alert 與 orphan dashboard</li>
<li><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>：動態叢集環境下、cluster 層 vs 服務層的 ownership 路由</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>：把成本接回 owner 與服務</li>
<li>08.2 incident command roles：事故時使用相同 ownership 模型</li>
<li>08.16 runbook lifecycle：把觀測資產接進 runbook 版本治理</li>
</ul>
]]></content:encoded></item><item><title>8.18 Incident Intake &amp; Evidence Triage</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>intake 的責任：把不同來源的異常輸入轉成可判讀的事故候選&lt;/li>
&lt;li>來源類型：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、customer ticket、support escalation、status page、vendor notice、security signal&lt;/li>
&lt;li>evidence 類型：log、metric、trace、audit log、customer report、vendor status、deployment event&lt;/li>
&lt;li>triage 欄位：time, source, impact, scope, confidence, owner, next action&lt;/li>
&lt;li>分級前判讀：是否真實、是否擴大、是否影響用戶、是否需要 incident commander&lt;/li>
&lt;li>跟 04 的交接：訊號品質與 evidence availability&lt;/li>
&lt;li>跟 07 的交接：security evidence 與 audit chain&lt;/li>
&lt;li>反模式：每個入口各自處理；客訴早於告警但沒有進 incident flow；vendor notice 無 owner&lt;/li>
&lt;/ul>
&lt;p>Incident intake &amp;amp; evidence triage 的價值是把「來源混亂」轉成「判讀一致」。事故入口天然分散，共用 intake 欄位能讓團隊把時間集中在判斷影響與處置優先序。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Incident intake &amp;amp; evidence triage 是事故流程的入口，責任是把異常來源轉成可分級、可指派、可追蹤的事故候選。&lt;/p>
&lt;p>這一頁處理的是事故啟動前的資料整理。事故不一定從 alert 開始，也可能從客訴、支援、第三方狀態或資安訊號開始；intake 讓這些來源使用同一組判讀欄位。&lt;/p>
&lt;p>這層的關鍵是資料可路由。只要 intake 能快速回答「來源可信度」「初步影響範圍」「下一步 owner」，事故分級就能提早進入可執行節奏。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 incident intake 時，先看輸入是否有 evidence，再看 evidence 是否足以支持分級與指派。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>source 是否可追溯且時間戳穩定&lt;/li>
&lt;li>impact scope 是否能初步估計&lt;/li>
&lt;li>evidence 是否能連到 log、metric、trace 或 audit log&lt;/li>
&lt;li>owner 是否能接手下一步查證&lt;/li>
&lt;li>confidence 是否標示為 confirmed、suspected 或 external-only&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Intake 欄位&lt;/th>
 &lt;th>最小可用判準&lt;/th>
 &lt;th>常見斷點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source / Time&lt;/td>
 &lt;td>可追溯來源與一致時間戳&lt;/td>
 &lt;td>多入口時間基準不一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Impact / Scope&lt;/td>
 &lt;td>至少可估「受影響對象與範圍」&lt;/td>
 &lt;td>只知有問題，不知影響面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence Link&lt;/td>
 &lt;td>可連到 log / metric / trace / status&lt;/td>
 &lt;td>證據散落，需要人工補交接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner / Next Action&lt;/td>
 &lt;td>有接手人與下一步查證動作&lt;/td>
 &lt;td>警報停在通知，無處置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Confidence&lt;/td>
 &lt;td>明確標示確定性等級&lt;/td>
 &lt;td>分級時反覆確認真偽&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="入口來源">入口來源&lt;/h2>
&lt;p>Incident intake 的入口來源天然分散。共用 intake 模型的責任是讓不同來源先進同一組欄位，再進 severity trigger、IC 指派與 evidence triage。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>來源&lt;/th>
 &lt;th>典型訊號&lt;/th>
 &lt;th>Intake 重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a>、error rate、latency&lt;/td>
 &lt;td>服務、範圍、runbook、owner&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Customer ticket&lt;/td>
 &lt;td>客訴、支援回報、客戶成功團隊&lt;/td>
 &lt;td>受影響帳戶、功能、時間、重現步驟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vendor notice&lt;/td>
 &lt;td>status page、support email、RSS&lt;/td>
 &lt;td>依賴服務、區域、ETA、替代路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security signal&lt;/td>
 &lt;td>audit log、SIEM、WAF、IAM alert&lt;/td>
 &lt;td>evidence chain、資料風險、分流條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deployment event&lt;/td>
 &lt;td>deploy、config rollout、feature flag&lt;/td>
 &lt;td>變更時間、owner、rollback path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client-side signal&lt;/td>
 &lt;td>RUM、synthetic probe、mobile crash&lt;/td>
 &lt;td>用戶感知、region、browser / device&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Alert 適合作為高可信自動入口。它應該帶著 service、severity suggestion、dashboard、runbook 與 owner，讓 on-call 能直接判斷是否啟動 incident。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>intake 的責任：把不同來源的異常輸入轉成可判讀的事故候選</li>
<li>來源類型：<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、customer ticket、support escalation、status page、vendor notice、security signal</li>
<li>evidence 類型：log、metric、trace、audit log、customer report、vendor status、deployment event</li>
<li>triage 欄位：time, source, impact, scope, confidence, owner, next action</li>
<li>分級前判讀：是否真實、是否擴大、是否影響用戶、是否需要 incident commander</li>
<li>跟 04 的交接：訊號品質與 evidence availability</li>
<li>跟 07 的交接：security evidence 與 audit chain</li>
<li>反模式：每個入口各自處理；客訴早於告警但沒有進 incident flow；vendor notice 無 owner</li>
</ul>
<p>Incident intake &amp; evidence triage 的價值是把「來源混亂」轉成「判讀一致」。事故入口天然分散，共用 intake 欄位能讓團隊把時間集中在判斷影響與處置優先序。</p>
<h2 id="概念定位">概念定位</h2>
<p>Incident intake &amp; evidence triage 是事故流程的入口，責任是把異常來源轉成可分級、可指派、可追蹤的事故候選。</p>
<p>這一頁處理的是事故啟動前的資料整理。事故不一定從 alert 開始，也可能從客訴、支援、第三方狀態或資安訊號開始；intake 讓這些來源使用同一組判讀欄位。</p>
<p>這層的關鍵是資料可路由。只要 intake 能快速回答「來源可信度」「初步影響範圍」「下一步 owner」，事故分級就能提早進入可執行節奏。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 incident intake 時，先看輸入是否有 evidence，再看 evidence 是否足以支持分級與指派。</p>
<p>重點訊號包括：</p>
<ul>
<li>source 是否可追溯且時間戳穩定</li>
<li>impact scope 是否能初步估計</li>
<li>evidence 是否能連到 log、metric、trace 或 audit log</li>
<li>owner 是否能接手下一步查證</li>
<li>confidence 是否標示為 confirmed、suspected 或 external-only</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Intake 欄位</th>
          <th>最小可用判準</th>
          <th>常見斷點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source / Time</td>
          <td>可追溯來源與一致時間戳</td>
          <td>多入口時間基準不一致</td>
      </tr>
      <tr>
          <td>Impact / Scope</td>
          <td>至少可估「受影響對象與範圍」</td>
          <td>只知有問題，不知影響面</td>
      </tr>
      <tr>
          <td>Evidence Link</td>
          <td>可連到 log / metric / trace / status</td>
          <td>證據散落，需要人工補交接</td>
      </tr>
      <tr>
          <td>Owner / Next Action</td>
          <td>有接手人與下一步查證動作</td>
          <td>警報停在通知，無處置</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>明確標示確定性等級</td>
          <td>分級時反覆確認真偽</td>
      </tr>
  </tbody>
</table>
<h2 id="入口來源">入口來源</h2>
<p>Incident intake 的入口來源天然分散。共用 intake 模型的責任是讓不同來源先進同一組欄位，再進 severity trigger、IC 指派與 evidence triage。</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>典型訊號</th>
          <th>Intake 重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert</td>
          <td><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、error rate、latency</td>
          <td>服務、範圍、runbook、owner</td>
      </tr>
      <tr>
          <td>Customer ticket</td>
          <td>客訴、支援回報、客戶成功團隊</td>
          <td>受影響帳戶、功能、時間、重現步驟</td>
      </tr>
      <tr>
          <td>Vendor notice</td>
          <td>status page、support email、RSS</td>
          <td>依賴服務、區域、ETA、替代路徑</td>
      </tr>
      <tr>
          <td>Security signal</td>
          <td>audit log、SIEM、WAF、IAM alert</td>
          <td>evidence chain、資料風險、分流條件</td>
      </tr>
      <tr>
          <td>Deployment event</td>
          <td>deploy、config rollout、feature flag</td>
          <td>變更時間、owner、rollback path</td>
      </tr>
      <tr>
          <td>Client-side signal</td>
          <td>RUM、synthetic probe、mobile crash</td>
          <td>用戶感知、region、browser / device</td>
      </tr>
  </tbody>
</table>
<p>Alert 適合作為高可信自動入口。它應該帶著 service、severity suggestion、dashboard、runbook 與 owner，讓 on-call 能直接判斷是否啟動 incident。</p>
<p>Customer ticket 適合補足平台盲區。客戶常先看到單一流程失敗、特定 tenant 受影響或前端體驗退化；這類 evidence 需要被轉成 impact scope，並送入事故候選流程。</p>
<p>Vendor notice 適合啟動依賴事故候選。當外部供應商狀態頁更新時，內部仍要判斷自己有哪些功能、客戶與 SLA 被影響，並指定 owner 追蹤替代路徑。</p>
<p>Security signal 適合啟動分流 triage。資安訊號可能需要保護 evidence chain、限制討論頻道、控制對外說法與啟動法規通報，因此 intake 欄位要能標示 security-sensitive。</p>
<p>Deployment event 適合連接近期變更。事故候選如果發生在 deploy、config rollout、migration 或 feature flag 之後，intake 應直接帶出 rollback path 與 change owner。</p>
<h2 id="evidence-類型">Evidence 類型</h2>
<p>Evidence triage 的責任是把「我們看到了什麼」和「我們相信到什麼程度」分開。證據可以不足，但限制要被明確標示。</p>
<table>
  <thead>
      <tr>
          <th>Evidence 類型</th>
          <th>判讀價值</th>
          <th>常見限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Log</td>
          <td>事件細節、request / tenant</td>
          <td>schema drift、drop、PII masking</td>
      </tr>
      <tr>
          <td>Metric</td>
          <td>趨勢、SLO、容量、error rate</td>
          <td>聚合過粗、延遲、cardinality cut</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>跨服務路徑與等待點</td>
          <td>sampling、async 斷鏈</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>權限、資料、責任鏈</td>
          <td>access restriction、retention</td>
      </tr>
      <tr>
          <td>Customer report</td>
          <td>用戶感知與實際影響</td>
          <td>主觀描述、時間不精準</td>
      </tr>
      <tr>
          <td>Vendor status</td>
          <td>外部依賴狀態</td>
          <td>ETA 不穩、粒度不符內部功能</td>
      </tr>
      <tr>
          <td>Deployment event</td>
          <td>變更與時間線</td>
          <td>owner 缺失、rollout 粒度不清</td>
      </tr>
  </tbody>
</table>
<p>Log evidence 適合回答單一事件發生了什麼。它需要 request id、tenant、region、error class 與 timestamp 才能支援 triage。</p>
<p>Metric evidence 適合回答影響是否擴大。error rate、latency、burn rate、queue lag 與 throughput 能幫 IC 判斷是否升級或縮小範圍。</p>
<p>Trace evidence 適合回答失效在哪個邊界。跨服務 request、queue、worker 與 dependency call 若能串起來，triage 就能更快分辨本地問題與下游問題。</p>
<p>Customer report evidence 適合補足使用者感知。即使 backend 指標尚未超標，客戶回報仍能提供高價值影響訊號，尤其是高價值 tenant 或關鍵功能。</p>
<h2 id="triage-流程">Triage 流程</h2>
<p>Incident intake 的 triage 流程是從異常輸入走到分級候選。流程要快，但每一步都要保留 confidence 與下一步 owner。</p>
<ol>
<li>建立 intake item，記錄 source、time、summary 與初始 owner。</li>
<li>收集至少一個 evidence link，標示 confirmed、suspected 或 external-only。</li>
<li>初估 impact scope，包括 users、tenant、region、feature 與 duration。</li>
<li>判斷是否需要啟動 severity trigger 或 incident commander。</li>
<li>指定下一步查證、通訊或分流路由。</li>
</ol>
<p>Confidence 欄位讓團隊在資訊不足時仍能前進。Confirmed 代表已有內部證據支持；suspected 代表有強烈訊號但仍需查證；external-only 代表目前只來自 vendor、customer 或第三方來源。</p>
<p>Impact scope 初估可以粗，但要可更新。第一次 triage 只要能回答「可能影響哪些功能、哪些客戶、是否正在擴大」，就足以支援 severity trigger。</p>
<p>Next action 要具體。好的 next action 會指定 owner、查詢入口、預期回報時間與升級條件，避免 intake 停在通知層。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>客戶回報已經累積，但 on-call 沒有收到事故候選</li>
<li>vendor 狀態頁更新後，內部沒有 owner 追蹤影響</li>
<li>alert 觸發但缺少服務、區域、tenant 或 user impact</li>
<li>security signal 與 operational signal 各自分流，沒有共同 evidence view</li>
<li>分級會議花大量時間確認事故真實性</li>
</ul>
<p>典型場景是客訴先於平台告警出現，support 知道影響、on-call 只看到局部指標。若 intake 層能把 ticket、RUM、status 與後端訊號合併成同一筆候選事件，IC 可以更早做出正確分級。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Incident intake 的反模式通常來自入口分散但欄位不一致。入口分散是現實，欄位一致才是治理重點。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個入口各自處理</td>
          <td>alert、support、vendor 各走各的</td>
          <td>統一 intake 欄位</td>
      </tr>
      <tr>
          <td>客訴停在客服系統</td>
          <td>support 知道影響，on-call 不知道</td>
          <td>ticket 轉 incident candidate</td>
      </tr>
      <tr>
          <td>Vendor notice 無 owner</td>
          <td>外部狀態更新但內部無人追蹤</td>
          <td>指定 dependency owner</td>
      </tr>
      <tr>
          <td>Evidence 無 confidence</td>
          <td>分級時反覆確認真偽</td>
          <td>標示 confirmed / suspected</td>
      </tr>
      <tr>
          <td>Security signal 混流</td>
          <td>敏感 evidence 進一般事故頻道</td>
          <td>security-sensitive 分流</td>
      </tr>
  </tbody>
</table>
<p>客訴停在客服系統會延後事故啟動。support ticket 應能轉成 incident candidate，並帶上客戶、功能、時間與重現資訊。</p>
<p>Evidence 缺 confidence 會讓分級會議重複查證同一件事。confidence 的責任是標示當下決策建立在哪個可信度上，證據可以在後續流程持續補強。</p>
<h2 id="與-04-和-07-的關係">與 04 和 07 的關係</h2>
<p>Incident intake 依賴 04 的 evidence availability。若 log、metric、trace、audit log 或 client-side signal 缺失，intake 需要標示資料限制，並把缺口回寫到 observability readiness 與 telemetry data quality。</p>
<p>Incident intake 也需要 07 的 security evidence 邊界。涉及資料外洩、權限濫用、audit chain 或法規通報的候選事件，應在 intake 階段標示 security-sensitive，讓後續溝通、證據保留與權限控管走正確路由。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.16 observability readiness：補 intake 所需訊號</li>
<li>04.17 telemetry data quality：標示 evidence 資料限制</li>
<li>08.1 severity trigger：把 intake 結果轉成分級判斷</li>
<li>08.2 incident command roles：指派 IC、scribe 與 owner</li>
<li>08.19 incident decision log：保留 intake 假設與證據</li>
<li>07.7 audit trail：資安 evidence chain 來源</li>
</ul>
]]></content:encoded></item><item><title>6.18 Reliability Metrics Governance</title><link>https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Reliability metrics governance 確保團隊量測到的指標能反映真實的可靠性狀態。指標的價值在於引導討論與暴露趨勢，一旦指標被直接當成目標，治理就開始退化。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>指標是否對準使用者感受、是否能驅動工程決策 — 這兩個問題決定 metrics governance 的有效性。&lt;/p>
&lt;p>判讀的核心問題：&lt;/p>
&lt;ul>
&lt;li>SLI 是否有明確觀測窗口與採樣邊界&lt;/li>
&lt;li>SLO 是否能轉成 release / alert / incident 決策&lt;/li>
&lt;li>DORA / SPACE / CFR 是否被混用成單一成績單&lt;/li>
&lt;li>metric drift 是否被記錄與校正&lt;/li>
&lt;/ul>
&lt;h2 id="dora-四指標">DORA 四指標&lt;/h2>
&lt;p>DORA 量測的是交付與可靠性流程的效率，四個指標各自回答不同問題。&lt;/p>
&lt;p>&lt;strong>Deploy frequency&lt;/strong> 量測交付節奏 — 團隊多頻繁把變更送到 production。高頻 deploy 通常代表小批次、低風險；但判讀陷阱是拆碎 deploy 只為衝頻率。辨別方式是同時看 deploy size distribution — 若平均 deploy 的變更量持續縮小但 frequency 持續上升，gaming 的可能性高。deploy frequency 要搭配 change failure rate 一起看，頻率高但 CFR 也高代表品質沒跟上。&lt;/p>
&lt;p>&lt;strong>Lead time for changes&lt;/strong> 量測從 commit 到 production 的時間。長 lead time 通常指向 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">CI pipeline&lt;/a> bottleneck、approval queue 或 staging 排隊。判讀陷阱是把 lead time 壓短但跳過驗證步驟 — 縮短的時間可能來自移除 slow path 測試，表面效率提升但風險轉移到 production。改善 lead time 的投資方向先看 CI 分層（6.1）是否合理，再看 review queue 是否成為瓶頸。&lt;/p>
&lt;p>&lt;strong>Change failure rate (CFR)&lt;/strong> 量測 deploy 後需要 rollback 或 hotfix 的比率。CFR 是 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">release gate&lt;/a> 健康度的直接指標 — gate 有效時 CFR 應該維持穩定或下降。判讀陷阱是團隊避免標記 rollback 來壓低 CFR，或把 hotfix 歸類為「正常 deploy」。偵測方式是把 CFR 跟 customer complaint rate 做相關性分析 — 若 CFR 持續下降但客訴未減，代表量測漏洞存在。&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>&lt;/strong> 量測從故障到恢復的時間。MTTR 的量測邊界需要明確定義：從 alert 觸發開始算、從 customer impact 開始算、到 recovery complete 還是到 root cause 修復。不同定義會產出完全不同的數字。判讀陷阱是延遲標記 incident 起始時間來壓低 MTTR。連到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response&lt;/a> 的事故分級與復盤流程。&lt;/p>
&lt;h2 id="space-補充維度">SPACE 補充維度&lt;/h2>
&lt;p>DORA 偏重 delivery 效率，SPACE 補人因與協作維度。五個面向各捕捉 DORA 看不到的訊號。&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>Satisfaction&lt;/td>
 &lt;td>團隊對工具、流程、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 負擔的滿意度&lt;/td>
 &lt;td>滿意度下降常先於效能指標退化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Performance&lt;/td>
 &lt;td>code review 品質、bug escape rate&lt;/td>
 &lt;td>補 DORA 缺的品質維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Activity&lt;/td>
 &lt;td>commit / PR / deploy 頻率&lt;/td>
 &lt;td>activity 是描述性指標，不等於 productivity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Communication&lt;/td>
 &lt;td>跨團隊協作效率、incident communication 品質&lt;/td>
 &lt;td>協作瓶頸在 DORA 中完全看不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Efficiency&lt;/td>
 &lt;td>flow state time、context switch frequency&lt;/td>
 &lt;td>高 context switch 會拖慢 lead time 但原因不在 CI&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>SPACE 同樣需要 governance。Satisfaction 被 KPI 化後團隊會避免誠實回饋；Activity 被當成 productivity 量測後會鼓勵 commit 拆碎。治理原則跟 DORA 相同：指標是討論的起點，不是績效的終點。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Reliability metrics governance 確保團隊量測到的指標能反映真實的可靠性狀態。指標的價值在於引導討論與暴露趨勢，一旦指標被直接當成目標，治理就開始退化。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>指標是否對準使用者感受、是否能驅動工程決策 — 這兩個問題決定 metrics governance 的有效性。</p>
<p>判讀的核心問題：</p>
<ul>
<li>SLI 是否有明確觀測窗口與採樣邊界</li>
<li>SLO 是否能轉成 release / alert / incident 決策</li>
<li>DORA / SPACE / CFR 是否被混用成單一成績單</li>
<li>metric drift 是否被記錄與校正</li>
</ul>
<h2 id="dora-四指標">DORA 四指標</h2>
<p>DORA 量測的是交付與可靠性流程的效率，四個指標各自回答不同問題。</p>
<p><strong>Deploy frequency</strong> 量測交付節奏 — 團隊多頻繁把變更送到 production。高頻 deploy 通常代表小批次、低風險；但判讀陷阱是拆碎 deploy 只為衝頻率。辨別方式是同時看 deploy size distribution — 若平均 deploy 的變更量持續縮小但 frequency 持續上升，gaming 的可能性高。deploy frequency 要搭配 change failure rate 一起看，頻率高但 CFR 也高代表品質沒跟上。</p>
<p><strong>Lead time for changes</strong> 量測從 commit 到 production 的時間。長 lead time 通常指向 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">CI pipeline</a> bottleneck、approval queue 或 staging 排隊。判讀陷阱是把 lead time 壓短但跳過驗證步驟 — 縮短的時間可能來自移除 slow path 測試，表面效率提升但風險轉移到 production。改善 lead time 的投資方向先看 CI 分層（6.1）是否合理，再看 review queue 是否成為瓶頸。</p>
<p><strong>Change failure rate (CFR)</strong> 量測 deploy 後需要 rollback 或 hotfix 的比率。CFR 是 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">release gate</a> 健康度的直接指標 — gate 有效時 CFR 應該維持穩定或下降。判讀陷阱是團隊避免標記 rollback 來壓低 CFR，或把 hotfix 歸類為「正常 deploy」。偵測方式是把 CFR 跟 customer complaint rate 做相關性分析 — 若 CFR 持續下降但客訴未減，代表量測漏洞存在。</p>
<p><strong><a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a></strong> 量測從故障到恢復的時間。MTTR 的量測邊界需要明確定義：從 alert 觸發開始算、從 customer impact 開始算、到 recovery complete 還是到 root cause 修復。不同定義會產出完全不同的數字。判讀陷阱是延遲標記 incident 起始時間來壓低 MTTR。連到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a> 的事故分級與復盤流程。</p>
<h2 id="space-補充維度">SPACE 補充維度</h2>
<p>DORA 偏重 delivery 效率，SPACE 補人因與協作維度。五個面向各捕捉 DORA 看不到的訊號。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>量測重點</th>
          <th>判讀價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Satisfaction</td>
          <td>團隊對工具、流程、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 負擔的滿意度</td>
          <td>滿意度下降常先於效能指標退化</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td>code review 品質、bug escape rate</td>
          <td>補 DORA 缺的品質維度</td>
      </tr>
      <tr>
          <td>Activity</td>
          <td>commit / PR / deploy 頻率</td>
          <td>activity 是描述性指標，不等於 productivity</td>
      </tr>
      <tr>
          <td>Communication</td>
          <td>跨團隊協作效率、incident communication 品質</td>
          <td>協作瓶頸在 DORA 中完全看不到</td>
      </tr>
      <tr>
          <td>Efficiency</td>
          <td>flow state time、context switch frequency</td>
          <td>高 context switch 會拖慢 lead time 但原因不在 CI</td>
      </tr>
  </tbody>
</table>
<p>SPACE 同樣需要 governance。Satisfaction 被 KPI 化後團隊會避免誠實回饋；Activity 被當成 productivity 量測後會鼓勵 commit 拆碎。治理原則跟 DORA 相同：指標是討論的起點，不是績效的終點。</p>
<h2 id="指標選用與團隊階段">指標選用與團隊階段</h2>
<p>指標投資的 ROI 跟團隊規模正相關。團隊小時指標治理成本高，應集中在最少的關鍵指標。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>建議指標</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Startup（&lt; 10 人）</td>
          <td>deploy frequency + CFR</td>
          <td>兩個指標足以判讀交付節奏與品質平衡，其他指標 noise 太大</td>
      </tr>
      <tr>
          <td>Scale（10-100 人）</td>
          <td>完整 DORA</td>
          <td>加入 lead time + MTTR，開始治理跨團隊 baseline</td>
      </tr>
      <tr>
          <td>Mature（100+ 人）</td>
          <td>DORA + SPACE + trend</td>
          <td>完整框架加趨勢分析，composite metrics 需要專人維護</td>
      </tr>
  </tbody>
</table>
<p>baseline 對齊的判準是跟自己的歷史趨勢比，而非抄業界數字。DORA 報告的 elite / high / medium / low 分類提供方向參考，但直接套用會忽略產業、架構與團隊結構的差異。</p>
<h2 id="anti-gaming-與-goodharts-law">Anti-gaming 與 Goodhart&rsquo;s law</h2>
<p>當指標直接變成目標，量測的行為會改變被量測的對象。這就是 Goodhart&rsquo;s law 在工程指標上的實現。</p>
<p>常見 gaming 模式與偵測方式：</p>
<table>
  <thead>
      <tr>
          <th>Gaming 模式</th>
          <th>偵測方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>拆碎 deploy 衝 frequency</td>
          <td>deploy size distribution 出現異常小的 cluster</td>
      </tr>
      <tr>
          <td>延遲標記 incident 降 MTTR</td>
          <td>incident 起始時間 vs alert 觸發時間的 gap 分析</td>
      </tr>
      <tr>
          <td>避免 rollback 降 CFR</td>
          <td>CFR vs customer complaint rate 的相關性斷裂</td>
      </tr>
      <tr>
          <td>跳過 slow path 測試縮短 lead time</td>
          <td>lead time 下降同時 CFR 上升</td>
      </tr>
      <tr>
          <td>壓下同類 incident 不報</td>
          <td>incident recurrence rate 與 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 數量不匹配</td>
      </tr>
  </tbody>
</table>
<p>治理原則：指標是診斷工具，用來發現問題方向與引導團隊討論。指標跨團隊強制排名會讓 gaming 成為理性選擇 — 團隊會優化數字而非優化系統。有效做法是把指標用在團隊自身的趨勢追蹤，跨團隊只分享經驗與改善策略。</p>
<h2 id="跟-slo-的差異">跟 SLO 的差異</h2>
<p>SLO 是面向使用者的服務承諾 — 量測的是「我的服務給使用者什麼品質」。6.18 metrics 是面向團隊的工程能力量測 — 量測的是「我的交付與可靠性流程效率如何」。</p>
<p>兩者的消費者不同：SLO 的消費者是 product / business stakeholder 與 on-call 團隊；DORA / SPACE 的消費者是工程管理與團隊自身。治理節奏也不同：SLO 跟 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 政策綁定，burn rate 驅動即時決策；DORA 趨勢按月或按季 review。</p>
<p>混用的風險是 SLO 失去商業對齊的價值。當 SLO 被當成工程 KPI 而非使用者承諾，團隊會開始縮小 SLI 範圍或放寬目標來讓數字好看，SLO 政策的放行判讀也跟著失真。</p>
<h2 id="案例對照">案例對照</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google：Error Budget 與 Release Gating</a>：SLO 與 DORA 的邊界在這個案例中最清楚 — error budget 是服務承諾的消耗量測，DORA 是交付流程的效率量測，兩者在 release gate 交會但責任不同。</li>
<li><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">Honeycomb：Burn Rate 驅動可靠性</a>：用觀測資料驅動判讀，而非先設定指標再找資料。這個案例說明指標治理的起點是觀測能力，指標是觀測的摘要，觀測是指標的來源。</li>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">Datadog</a>：指標平台的可靠性直接影響事故判讀品質。當指標平台本身不穩定，所有基於它的 DORA / SLO 量測都會失真。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀條件</th>
          <th>行動建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>指標數字持續改善、客戶投訴未減</td>
          <td>量測覆蓋不足或 gaming — 先檢查 CFR vs complaint 相關性</td>
          <td>把 complaint 率加入 dashboard 交叉比對</td>
      </tr>
      <tr>
          <td>跨團隊強制排名</td>
          <td>gaming 風險高 — 改為團隊自身趨勢追蹤</td>
          <td>取消排名、改為各團隊獨立看自身 trend</td>
      </tr>
      <tr>
          <td>DORA 採集靠人工、滯後超過一個月</td>
          <td>指標失去即時性 — 自動化採集連到 CI / deploy pipeline</td>
          <td>串接 CI/CD pipeline 自動產出 DORA 資料</td>
      </tr>
      <tr>
          <td>指標無 owner、半年無人 review</td>
          <td>治理已停擺 — 指定 owner 與季度 review 節奏</td>
          <td>指定 metrics owner + 排入季度 review 議程</td>
      </tr>
      <tr>
          <td>deploy frequency 上升同時 CFR 上升</td>
          <td>速度與品質失衡 — 先補 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">release gate</a> 再追 frequency</td>
          <td>暫停追 frequency、先讓 CFR 回到 baseline</td>
      </tr>
      <tr>
          <td>MTTR 定義跨團隊不一致</td>
          <td>量測不可比 — 先統一量測邊界（alert → recovery complete）</td>
          <td>發布 MTTR 量測定義文件、統一 start/end 判準</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>：lead time 的主要改善入口</li>
<li><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6 SLO / error budget</a>：商業承諾層的指標，跟 DORA 互補但責任不同</li>
<li><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>：CFR 是 gate 健康度訊號</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：指標趨勢揭露的可靠性債</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.6 SLI/SLO 訊號層</a>：指標的觀測來源</li>
<li><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">08.5 post-incident review</a>：MTTR 計算的事件來源、指標漂移通常先在復盤裡被看見</li>
<li><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">08.11 觀測 / 可靠性 / 事故閉環</a>：指標治理回寫到三模組閉環</li>
</ul>
]]></content:encoded></item><item><title>0.19 雲端服務對照地圖（AWS / GCP / Azure）</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/</guid><description>&lt;p>面對「我該選 AWS 還是 GCP？」這類問題、第一步是把後端能力分類對應到三家雲廠商的具體服務名稱、技術細節放後面。本章提供這份對照地圖、同時警告一件事：AWS、GCP、Azure 在大部分能力上都有對應產品，但「對應」不等於「等價」— 同樣是 managed SQL、AWS RDS、GCP Cloud SQL、Azure SQL 在備份頻率、replica 行為、failover 時間、跨區複製成本上都有差異。對照表是入口、不是決策本身。&lt;/p>
&lt;h2 id="為什麼需要這張對照地圖">為什麼需要這張對照地圖&lt;/h2>
&lt;p>兩種使用情境會需要這張表。第一是初次選型時，讀者已經選定主要雲廠商，要對照各能力分類找出 vendor 名稱。第二是跨雲遷移評估，讀者要對照源端跟目標端的能力 gap。沒有這張表，每次都要重新查文件、容易漏掉某個能力。&lt;/p>
&lt;p>但這張表不能取代深入評估。每個 vendor 都有不在表格內的差異，例如配額、區域可用性、跨服務整合、計價模型。表格是路由起點，後續判讀要進到該 vendor 的 deep article。&lt;/p>
&lt;h2 id="能力--雲廠商對照表">能力 × 雲廠商對照表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>能力分類&lt;/th>
 &lt;th>AWS&lt;/th>
 &lt;th>GCP&lt;/th>
 &lt;th>Azure&lt;/th>
 &lt;th>對照判讀重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>關聯式 DB（OLTP）&lt;/td>
 &lt;td>RDS / Aurora&lt;/td>
 &lt;td>Cloud SQL / AlloyDB&lt;/td>
 &lt;td>Azure SQL / Azure Database for Postgres&lt;/td>
 &lt;td>failover 時間、跨區 replica、IOPS 計價&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全球分散式 DB&lt;/td>
 &lt;td>Aurora DSQL / DynamoDB Global Tables&lt;/td>
 &lt;td>Spanner&lt;/td>
 &lt;td>Cosmos DB&lt;/td>
 &lt;td>一致性模型、寫入延遲、計價單位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>KV / Document DB&lt;/td>
 &lt;td>DynamoDB&lt;/td>
 &lt;td>Firestore / Bigtable&lt;/td>
 &lt;td>Cosmos DB&lt;/td>
 &lt;td>partition key 設計、capacity mode、跨區一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>快取&lt;/td>
 &lt;td>ElastiCache（Redis / Memcached）&lt;/td>
 &lt;td>Memorystore&lt;/td>
 &lt;td>Azure Cache for Redis&lt;/td>
 &lt;td>跨區複製、persistence、容量上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訊息佇列&lt;/td>
 &lt;td>SQS / SNS / Kinesis&lt;/td>
 &lt;td>Pub/Sub&lt;/td>
 &lt;td>Service Bus / Event Hubs&lt;/td>
 &lt;td>delivery guarantee、ordering、retention 期&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件流（Kafka）&lt;/td>
 &lt;td>MSK / Kinesis&lt;/td>
 &lt;td>Pub/Sub&lt;/td>
 &lt;td>Event Hubs (Kafka compatibility)&lt;/td>
 &lt;td>Kafka 相容性、partition 數量、跨區複製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>物件儲存&lt;/td>
 &lt;td>S3&lt;/td>
 &lt;td>Cloud Storage&lt;/td>
 &lt;td>Blob Storage&lt;/td>
 &lt;td>一致性模型、跨區複製、lifecycle policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容器執行平台&lt;/td>
 &lt;td>ECS / EKS / Fargate&lt;/td>
 &lt;td>GKE / Cloud Run&lt;/td>
 &lt;td>AKS / Container Apps&lt;/td>
 &lt;td>managed 程度、cold start、計價單位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serverless 函式&lt;/td>
 &lt;td>Lambda&lt;/td>
 &lt;td>Cloud Functions / Cloud Run&lt;/td>
 &lt;td>Azure Functions&lt;/td>
 &lt;td>最大執行時間、cold start、整合方式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load Balancer&lt;/td>
 &lt;td>ELB（ALB / NLB / CLB）&lt;/td>
 &lt;td>Cloud Load Balancing&lt;/td>
 &lt;td>Azure Load Balancer / App Gateway&lt;/td>
 &lt;td>L4 vs L7、跨區 LB、TLS termination&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API Gateway&lt;/td>
 &lt;td>API Gateway&lt;/td>
 &lt;td>API Gateway / Apigee&lt;/td>
 &lt;td>API Management&lt;/td>
 &lt;td>rate limit、auth 整合、計價&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN / 邊緣&lt;/td>
 &lt;td>CloudFront&lt;/td>
 &lt;td>Cloud CDN / Media CDN&lt;/td>
 &lt;td>Azure Front Door / CDN&lt;/td>
 &lt;td>edge POP 數、purge API、cache key 彈性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>監控&lt;/td>
 &lt;td>CloudWatch&lt;/td>
 &lt;td>Cloud Monitoring&lt;/td>
 &lt;td>Azure Monitor&lt;/td>
 &lt;td>metric retention、dashboard 表達力、整合範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log 聚合&lt;/td>
 &lt;td>CloudWatch Logs&lt;/td>
 &lt;td>Cloud Logging&lt;/td>
 &lt;td>Log Analytics&lt;/td>
 &lt;td>ingestion 成本、query 語言、retention&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tracing&lt;/td>
 &lt;td>X-Ray&lt;/td>
 &lt;td>Cloud Trace&lt;/td>
 &lt;td>Application Insights&lt;/td>
 &lt;td>sampling 策略、跨服務 trace、整合 SDK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Secret Management&lt;/td>
 &lt;td>Secrets Manager / SSM Parameter&lt;/td>
 &lt;td>Secret Manager&lt;/td>
 &lt;td>Key Vault&lt;/td>
 &lt;td>旋轉支援、整合 IAM、稽核 log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Identity / IAM&lt;/td>
 &lt;td>IAM&lt;/td>
 &lt;td>IAM&lt;/td>
 &lt;td>Entra ID（前 AAD） + Azure RBAC&lt;/td>
 &lt;td>跨服務 policy、token lifetime、federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI/CD&lt;/td>
 &lt;td>CodePipeline / CodeBuild&lt;/td>
 &lt;td>Cloud Build / Cloud Deploy&lt;/td>
 &lt;td>Azure Pipelines&lt;/td>
 &lt;td>整合 Git 平台、執行環境彈性、計價單位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表以全球 hyperscaler 三巨頭為主、不是市場全貌。&lt;strong>Oracle Cloud (OCI)&lt;/strong> 在 enterprise / Java workload 跟金融受監管環境有顯著市佔；&lt;strong>Alibaba Cloud&lt;/strong> 在亞太 / 跨境電商是主流；&lt;strong>IBM Cloud&lt;/strong> 在金融 / 受監管環境仍存在；&lt;strong>Hetzner / DigitalOcean / Vultr&lt;/strong> 在 cost-leader 區段提供完全不同的計價模型；&lt;strong>Sovereign cloud&lt;/strong>（GDPR Schrems II 後在歐洲、JEDI / JWCC 在美國政府）是另一條獨立軸、跟資料主權合規綁定、比較對象不在這張表內。對照判讀邏輯（「對應 ≠ 等價」）可以同樣套用、但具體 vendor 名稱與差異維度要按目標廠商各自查證。&lt;/p></description><content:encoded><![CDATA[<p>面對「我該選 AWS 還是 GCP？」這類問題、第一步是把後端能力分類對應到三家雲廠商的具體服務名稱、技術細節放後面。本章提供這份對照地圖、同時警告一件事：AWS、GCP、Azure 在大部分能力上都有對應產品，但「對應」不等於「等價」— 同樣是 managed SQL、AWS RDS、GCP Cloud SQL、Azure SQL 在備份頻率、replica 行為、failover 時間、跨區複製成本上都有差異。對照表是入口、不是決策本身。</p>
<h2 id="為什麼需要這張對照地圖">為什麼需要這張對照地圖</h2>
<p>兩種使用情境會需要這張表。第一是初次選型時，讀者已經選定主要雲廠商，要對照各能力分類找出 vendor 名稱。第二是跨雲遷移評估，讀者要對照源端跟目標端的能力 gap。沒有這張表，每次都要重新查文件、容易漏掉某個能力。</p>
<p>但這張表不能取代深入評估。每個 vendor 都有不在表格內的差異，例如配額、區域可用性、跨服務整合、計價模型。表格是路由起點，後續判讀要進到該 vendor 的 deep article。</p>
<h2 id="能力--雲廠商對照表">能力 × 雲廠商對照表</h2>
<table>
  <thead>
      <tr>
          <th>能力分類</th>
          <th>AWS</th>
          <th>GCP</th>
          <th>Azure</th>
          <th>對照判讀重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>關聯式 DB（OLTP）</td>
          <td>RDS / Aurora</td>
          <td>Cloud SQL / AlloyDB</td>
          <td>Azure SQL / Azure Database for Postgres</td>
          <td>failover 時間、跨區 replica、IOPS 計價</td>
      </tr>
      <tr>
          <td>全球分散式 DB</td>
          <td>Aurora DSQL / DynamoDB Global Tables</td>
          <td>Spanner</td>
          <td>Cosmos DB</td>
          <td>一致性模型、寫入延遲、計價單位</td>
      </tr>
      <tr>
          <td>KV / Document DB</td>
          <td>DynamoDB</td>
          <td>Firestore / Bigtable</td>
          <td>Cosmos DB</td>
          <td>partition key 設計、capacity mode、跨區一致性</td>
      </tr>
      <tr>
          <td>快取</td>
          <td>ElastiCache（Redis / Memcached）</td>
          <td>Memorystore</td>
          <td>Azure Cache for Redis</td>
          <td>跨區複製、persistence、容量上限</td>
      </tr>
      <tr>
          <td>訊息佇列</td>
          <td>SQS / SNS / Kinesis</td>
          <td>Pub/Sub</td>
          <td>Service Bus / Event Hubs</td>
          <td>delivery guarantee、ordering、retention 期</td>
      </tr>
      <tr>
          <td>事件流（Kafka）</td>
          <td>MSK / Kinesis</td>
          <td>Pub/Sub</td>
          <td>Event Hubs (Kafka compatibility)</td>
          <td>Kafka 相容性、partition 數量、跨區複製</td>
      </tr>
      <tr>
          <td>物件儲存</td>
          <td>S3</td>
          <td>Cloud Storage</td>
          <td>Blob Storage</td>
          <td>一致性模型、跨區複製、lifecycle policy</td>
      </tr>
      <tr>
          <td>容器執行平台</td>
          <td>ECS / EKS / Fargate</td>
          <td>GKE / Cloud Run</td>
          <td>AKS / Container Apps</td>
          <td>managed 程度、cold start、計價單位</td>
      </tr>
      <tr>
          <td>Serverless 函式</td>
          <td>Lambda</td>
          <td>Cloud Functions / Cloud Run</td>
          <td>Azure Functions</td>
          <td>最大執行時間、cold start、整合方式</td>
      </tr>
      <tr>
          <td>Load Balancer</td>
          <td>ELB（ALB / NLB / CLB）</td>
          <td>Cloud Load Balancing</td>
          <td>Azure Load Balancer / App Gateway</td>
          <td>L4 vs L7、跨區 LB、TLS termination</td>
      </tr>
      <tr>
          <td>API Gateway</td>
          <td>API Gateway</td>
          <td>API Gateway / Apigee</td>
          <td>API Management</td>
          <td>rate limit、auth 整合、計價</td>
      </tr>
      <tr>
          <td>CDN / 邊緣</td>
          <td>CloudFront</td>
          <td>Cloud CDN / Media CDN</td>
          <td>Azure Front Door / CDN</td>
          <td>edge POP 數、purge API、cache key 彈性</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>CloudWatch</td>
          <td>Cloud Monitoring</td>
          <td>Azure Monitor</td>
          <td>metric retention、dashboard 表達力、整合範圍</td>
      </tr>
      <tr>
          <td>Log 聚合</td>
          <td>CloudWatch Logs</td>
          <td>Cloud Logging</td>
          <td>Log Analytics</td>
          <td>ingestion 成本、query 語言、retention</td>
      </tr>
      <tr>
          <td>Tracing</td>
          <td>X-Ray</td>
          <td>Cloud Trace</td>
          <td>Application Insights</td>
          <td>sampling 策略、跨服務 trace、整合 SDK</td>
      </tr>
      <tr>
          <td>Secret Management</td>
          <td>Secrets Manager / SSM Parameter</td>
          <td>Secret Manager</td>
          <td>Key Vault</td>
          <td>旋轉支援、整合 IAM、稽核 log</td>
      </tr>
      <tr>
          <td>Identity / IAM</td>
          <td>IAM</td>
          <td>IAM</td>
          <td>Entra ID（前 AAD） + Azure RBAC</td>
          <td>跨服務 policy、token lifetime、federation</td>
      </tr>
      <tr>
          <td>CI/CD</td>
          <td>CodePipeline / CodeBuild</td>
          <td>Cloud Build / Cloud Deploy</td>
          <td>Azure Pipelines</td>
          <td>整合 Git 平台、執行環境彈性、計價單位</td>
      </tr>
  </tbody>
</table>
<p>這張表以全球 hyperscaler 三巨頭為主、不是市場全貌。<strong>Oracle Cloud (OCI)</strong> 在 enterprise / Java workload 跟金融受監管環境有顯著市佔；<strong>Alibaba Cloud</strong> 在亞太 / 跨境電商是主流；<strong>IBM Cloud</strong> 在金融 / 受監管環境仍存在；<strong>Hetzner / DigitalOcean / Vultr</strong> 在 cost-leader 區段提供完全不同的計價模型；<strong>Sovereign cloud</strong>（GDPR Schrems II 後在歐洲、JEDI / JWCC 在美國政府）是另一條獨立軸、跟資料主權合規綁定、比較對象不在這張表內。對照判讀邏輯（「對應 ≠ 等價」）可以同樣套用、但具體 vendor 名稱與差異維度要按目標廠商各自查證。</p>
<h2 id="三家雲共同缺的能力分類">三家雲共同缺的能力分類</h2>
<p>對照表覆蓋的能力都有 vendor 直接對應，但有兩類能力三家雲廠商都沒有提供等價的原生服務，要靠第三方工具補完。把這兩類獨立成段，避免在對照表中用「（無原生）」填空造成模板化。</p>
<p><strong>壓測 / 流量重放</strong>：三家雲都沒有像 RDS 對 PostgreSQL 那樣的「managed 壓測服務」。團隊要從 k6、JMeter、Gatling、Locust、Vegeta、AWS Distributed Load Testing（這是 reference architecture 而非 managed service）這類第三方工具選擇。選型考量在於：是否支援該團隊熟悉的腳本語言（k6 用 JS / Gatling 用 Scala / Locust 用 Python）、能否分散執行、能否在 CI 整合、能否重放 production traffic（GoReplay、AWS VPC Traffic Mirroring）。各工具的選型細節見 <a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a>。</p>
<p><strong>事故管理 / on-call 通知</strong>：三家雲都沒有原生的 incident management 平台。CloudWatch / Cloud Monitoring / Azure Monitor 只到 alert 層、不負責 escalation、on-call rotation、incident timeline 與 retrospective。這層責任目前由 PagerDuty、Opsgenie、Splunk On-Call（前 VictorOps）、Grafana OnCall 等第三方平台承擔。三家雲提供的 alert 可以 webhook 到這些平台，但 incident workflow 本身不在 cloud vendor scope 內。事故管理流程見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理模組</a>。</p>
<p>辨識這兩類「跨雲共缺」能力的價值在於：跨雲遷移時這兩層不會增加 vendor lock-in，可以保留現有第三方工具直接接到新雲；反之，cloud-native incident management 或 cloud-native 壓測這類規劃要在採購前確認是否真實存在，避免被命名類似的工具誤導。</p>
<h2 id="對應--等價的具體差異範例">「對應 ≠ 等價」的具體差異範例</h2>
<p>對照表只給名稱對應，實際選型要看差異細節。下面四個常見的差異維度示範如何把名稱對應翻成選型判讀。</p>
<h3 id="失效切換時間差異rds-vs-cloud-sql-vs-azure-sql">失效切換時間差異（RDS vs Cloud SQL vs Azure SQL）</h3>
<p>同樣是 managed PostgreSQL，三家 vendor 文件給的 failover 時間參考值差距明顯。下列數字以各雲廠商公開文件為基準、實測長尾可能拖到更長：</p>
<ul>
<li>AWS RDS Multi-AZ：vendor 文件寫「typically 60–120 seconds」、P99 實測可達數分鐘</li>
<li>AWS Aurora：vendor 文件寫「typically less than 30 seconds」、實測 30–90 秒常見</li>
<li>GCP Cloud SQL HA：vendor 文件寫「1–2 minutes」</li>
<li>Azure SQL Business Critical：vendor 文件寫「around 30 seconds」、實測 30–60 秒</li>
</ul>
<p>選擇關鍵不是「哪個快」、而是「業務能容忍多少 downtime」。30 秒對 banking、ticketing 是不能接受的；對內部後台是無感的。失效切換時間直接影響 SLO 設定跟業務連續性 — 數字以 vendor 公開文件為參考、實際決策時要用該 vendor 自己的 SLA 條款跟 incident report 驗證。</p>
<h3 id="一致性模型差異dynamodb-vs-firestore-vs-cosmos-db">一致性模型差異（DynamoDB vs Firestore vs Cosmos DB）</h3>
<p>三家的 NoSQL 在一致性語意上分歧：</p>
<ul>
<li>DynamoDB：預設 eventual consistent read、可選 strongly consistent read（成本 2 倍）</li>
<li>Firestore：strongly consistent read 是預設、跨 region 用 multi-region 配置</li>
<li>Cosmos DB：五種一致性等級可選（strong / bounded staleness / session / consistent prefix / eventual）</li>
</ul>
<p>如果應用程式假設「寫完馬上能讀到」（read-after-write），在 DynamoDB 預設模式下會撞牆。在 Cosmos DB 選 session consistency 可以保證單一 client 內 read-after-write、跨 client 仍是 eventual。這類差異要在選型階段對齊，不是事後改 code。</p>
<h3 id="計價模型差異lambda-vs-cloud-functions-vs-azure-functions">計價模型差異（Lambda vs Cloud Functions vs Azure Functions）</h3>
<p>三家的 <a href="/blog/backend/knowledge-cards/serverless/" data-link-title="Serverless" data-link-desc="說明按請求 / 按用量計費、由平台管理執行環境與擴縮的運算交付模型、與其冷啟動與計價邊界">serverless</a> 在計價單位有差異：</p>
<ul>
<li>Lambda：請求數 + 執行時間 (GB-秒)</li>
<li>Cloud Functions：請求數 + 執行時間 + 網路流量</li>
<li>Azure Functions：執行次數 + 執行時間 + 記憶體（Consumption Plan）或固定費用（Premium / Dedicated Plan）</li>
</ul>
<p>對於低流量服務、三家差異不大；對於高頻率短時間函式、計價差異可能放大數倍（具體倍數視 memory size / 執行時間 / 流量分布、用 vendor calculator 算）。選型時要用實際 workload 估算、不能看單位價格表面數字。</p>
<h3 id="跨服務整合差異消息佇列-vs-觸發器">跨服務整合差異（消息佇列 vs 觸發器）</h3>
<p>AWS SQS + Lambda 整合非常成熟、有 native trigger；GCP Pub/Sub + Cloud Functions 同樣 native；Azure Service Bus + Functions 也有 trigger，但細節（dead-letter 處理、retry 策略、batch size）跟前兩家有差異。</p>
<p>跨服務的整合成熟度通常會在事故時放大差異。同樣的事件處理流程，在 AWS 上 90% 用 native 路徑、在另一家可能需要 30% 自己寫 glue code。</p>
<h2 id="跨雲遷移的判讀重點">跨雲遷移的判讀重點</h2>
<p>把這張對照表反過來讀，就是跨雲遷移的 gap 分析起點。但實際遷移要看四類風險：</p>
<table>
  <thead>
      <tr>
          <th>風險類型</th>
          <th>判讀重點</th>
          <th>對應緩解</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>語意差異</td>
          <td>兩家「對應」服務的一致性 / 失效 / 順序語意是否一致</td>
          <td>在抽象層（repository、queue adapter）封裝差異</td>
      </tr>
      <tr>
          <td>配額差異</td>
          <td>限制（每秒請求數、partition 上限、batch size）是否相當</td>
          <td>對照新平台配額重新設計批次大小</td>
      </tr>
      <tr>
          <td>計價差異</td>
          <td>計價單位不同，舊有 cost model 在新平台失準</td>
          <td>用新平台計價重做 cost engineering</td>
      </tr>
      <tr>
          <td>生態差異</td>
          <td>周邊工具（監控、log、IAM）整合不對等</td>
          <td>預估遷移成本要含「重建 observability / IAM」</td>
      </tr>
      <tr>
          <td>Data gravity / egress lock-in</td>
          <td>PB 級資料的 egress fee 跟一致性轉移時程</td>
          <td>決定資料「同步轉移 / 漸進複製 / 保留在原雲、運算跨雲」</td>
      </tr>
  </tbody>
</table>
<p>第五類風險常被低估：以 AWS S3 為例、egress 約 $0.09/GB、PB 級資料即 $90k 帶寬費；GCP / Azure 同等級。跨雲遷移最大單筆成本經常是 data gravity、需要先決策資料拓樸再算其他三類風險。</p>
<p>跨雲遷移不是把服務名稱換掉就完成。每一個對應都要做 deep audit，這是 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">01 大規模 DB 遷移實戰</a> 等模組的責任。</p>
<h2 id="混合雲與多雲的情境">混合雲與多雲的情境</h2>
<p>常見的混合或多雲組合：</p>
<ul>
<li><strong>資料留 AWS、ML 跑 GCP</strong>：因為 BigQuery、Vertex AI 在資料分析優勢</li>
<li><strong>主要 Azure、ML 跑 AWS</strong>：因為 SageMaker 跟 Bedrock 提供的選項</li>
<li><strong>DR 在另一家雲</strong>：主要在 AWS、DR 站在 Azure 避免單一雲廠商故障</li>
</ul>
<p>混合 / 多雲要解的核心問題是跨雲流量成本（egress）跟身分聯邦（cross-cloud IAM）。這兩個成本通常被低估，要在規劃階段就做進 cost model。</p>
<h2 id="對照表使用的判讀順序">對照表使用的判讀順序</h2>
<p>讀這張表時，避免以下兩種誤用：</p>
<p>第一是「看完表格就決定 vendor」。表格只給名稱對應，沒給選型理由。先確認自己的能力需求（容量、一致性、failover 時間、計價型態），再用表格找候選 vendor，再進該 vendor 的 deep article 驗證細節。</p>
<p>第二是「把『對應』當作可互換」。已經提到的失效時間、一致性語意、計價模型差異會直接影響業務。在做技術選型時不能假設「換家雲就行」，要驗證每一條差異。</p>
<p>正確的使用順序：能力需求 → 對照表找候選 → vendor deep article 驗證 → cost / failure / consistency 驗算 → 決策。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同樣 workload 在新雲上 cost 翻倍</td>
          <td>計價模型差異未被估到</td>
          <td>重做 cost engineering、用實際 traffic 估算</td>
      </tr>
      <tr>
          <td>遷移後 latency 升高</td>
          <td>區域、跨服務整合或一致性模式不同</td>
          <td>確認 region 選擇、跨服務整合方式</td>
      </tr>
      <tr>
          <td>跨雲 egress 成本失控</td>
          <td>流量設計沒考慮 inter-cloud transfer</td>
          <td>重新設計流量拓樸、考慮 cache 或聚合</td>
      </tr>
      <tr>
          <td>跨雲 IAM 設定爆炸</td>
          <td>身分聯邦設計不足、每個服務各管各的</td>
          <td>引入統一身分平台或 federation</td>
      </tr>
      <tr>
          <td>新雲服務功能對應不上</td>
          <td>「對應 ≠ 等價」的 gap 出現</td>
          <td>抽象層封裝差異、或評估是否值得換</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 vendor 對照表當「採購清單」，看完直接照表選。選型必須回到需求，不是看哪家有對應名稱就選。</p>
<p>把雲廠商當「commodity 商品」，假設換家就好。三家的整合生態、配額限制、計價單位都有差異、遷移成本經常被嚴重低估（特別是 data gravity / IAM / 監控重建這三類隱性成本）。</p>
<p>把單一雲廠商當「永遠不會變」。雲廠商會調整定價、棄用服務、改 API。設計時要有抽象邊界，避免直接綁定 vendor SDK 到業務邏輯，方便未來換家或多雲。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章預設「自建於雲端基礎設施」已成立；讀者若在對照表看到 Firestore 而想問「乾脆整個用 Firebase？」、那是 BaaS / 託管平台層的交付形態判斷、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
<p>本章專注「能力分類到 vendor 名稱的翻譯與對應差異」。當問題進入具體 vendor 配置（例如 RDS 怎麼設 backup）、跨 vendor 遷移流程（例如從 MySQL 遷到 Aurora），分別交給各模組的 <code>vendors/</code> 目錄跟 migration playbook。當問題進入需求分類（這個業務需要強一致還是最終一致？）回到 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>雲端服務選型可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a> — 0.14 收錄不同產業、不同規模階段企業的雲端選型決策；對照本章「跨雲遷移的判讀重點」段：合規、計價、IAM 整合是三家雲決策的主要分歧軸。</li>
<li><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：TiDB 遷到 DynamoDB</a> — Zomato 把 SQL 介面（TiDB）換成 KV 介面（DynamoDB）、用一致性語意差異換取 4 倍吞吐 + 50% 成本；對照本章「對應 ≠ 等價」段中的一致性模型差異子段。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：Aurora consolidation</a> — 案例是 AWS 內 DB 種類整併（多 RDB → Aurora），可對照本章「對應 ≠ 等價」段中的計價模型與整合成熟度差異。雖然不涉及跨雲，但在同一家雲廠商內整併服務、跟跨雲整併共用同一條決策邏輯：權衡 vendor lock-in 代價 vs 運維碎片化代價。</li>
<li><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s → EKS</a> — Tradeshift 從自管 K8s control plane 遷到 EKS managed control plane、運維責任邊界從「整套 cluster」收斂到「workload + worker node」。對照本章「容器執行平台」對照行：managed 程度是同一能力分類下的主要分歧軸。</li>
</ul>
<p>這些案例回答的是不同問題、不是同一個問題的不同切面。對照表本身只回答「叫什麼名字」；Zomato / Tradeshift 補「換掉名字後實際差多少」（介面 / 計價 / 一致性差異）；Netflix Aurora 補「同一雲內怎麼收斂」；0.14 補「真實企業在什麼壓力下選什麼」。讀者按手邊的問題進入對應案例、不需要也不適合串成同一條 narrative。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1 後端服務能力地圖</a> 的交接：先確認能力分類，再用本章找 vendor 對應。</li>
<li>與 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a> 的交接：cost model 是 vendor 選型的關鍵維度。</li>
<li>與各模組的 <code>vendors/</code> 目錄的交接：對照表只給名稱、deep article 給配置與運維。</li>
<li>與 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">01 大規模 DB 遷移實戰</a> 的交接：跨 vendor 遷移的具體流程。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>對照表是查 vendor 名稱的第一層、進入細節要走 deep article：</p>
<ul>
<li>實際企業選型案例 → <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a></li>
<li>資料庫 vendor 細節對比 → <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01 模組 vendors/</a></li>
<li>部署平台 vendor 細節對比 → <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">05 模組 vendors/</a></li>
</ul>
<p>本章不在規模成長路線上、是 sibling 工具型入口。要進規模成長路線、從 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 或 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 開始。</p>
]]></content:encoded></item><item><title>MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/modern-sql-features/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/modern-sql-features/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>8.0 modern SQL 特性&lt;/em> — 5 個關鍵能力 + 跟 PostgreSQL 對應特性的對比。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>「MySQL 是 SQL 簡單版」是個過時觀念。&lt;/p>
&lt;p>這個觀念的來源很合理：MySQL 5.x 時代沒 CTE、window function 要嗑 hack、recursive query 寫不出來、JSON 處理是字串 substring 拼接、複雜分析 query 只能丟去 PostgreSQL 或 Snowflake。整整 10 年 SQL 進階特性 MySQL 全缺、PostgreSQL 全有。&lt;/p>
&lt;p>MySQL 8.0（2018 推出）改變這件事。CTE / window function / lateral derived table / JSON_TABLE / hash join / atomic DDL / role-based authentication / common table expression 全部進來。&lt;strong>這不是「終於跟上 PG」、是 MySQL 第一次有資格進入 SQL 工程深度討論&lt;/strong>。但有 caveats：每個特性的 &lt;em>行為實現&lt;/em> 跟 PostgreSQL 對應特性都有 &lt;em>微妙差異&lt;/em>、不能假設 PG 經驗直接套用。&lt;/p>
&lt;p>對從 PostgreSQL 過來評估 MySQL 的讀者：本文是 &lt;em>特性對等驗證&lt;/em> — 哪些 8.0 特性真的可以 production 用、哪些是 marketing 但實作有 gap。對既有 MySQL 5.7 user：本文是 &lt;em>upgrade 5.7 → 8.0 的具體 ROI&lt;/em> — 從 SQL feature 角度看升級值不值得。&lt;/p>
&lt;h2 id="5-個關鍵特性--pg-對比">5 個關鍵特性 + PG 對比&lt;/h2>
&lt;h3 id="特性-1ctecommon-table-expression">特性 1：CTE（Common Table Expression）&lt;/h3>
&lt;p>MySQL 8.0 / PG 8.4+ 都支援。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- MySQL 8.0 + PG 都 OK
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order_summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SUM&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">total&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-01-01&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">total&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order_summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">total&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>行為差異&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>8.0 modern SQL 特性</em> — 5 個關鍵能力 + 跟 PostgreSQL 對應特性的對比。</p></blockquote>
<hr>
<p>「MySQL 是 SQL 簡單版」是個過時觀念。</p>
<p>這個觀念的來源很合理：MySQL 5.x 時代沒 CTE、window function 要嗑 hack、recursive query 寫不出來、JSON 處理是字串 substring 拼接、複雜分析 query 只能丟去 PostgreSQL 或 Snowflake。整整 10 年 SQL 進階特性 MySQL 全缺、PostgreSQL 全有。</p>
<p>MySQL 8.0（2018 推出）改變這件事。CTE / window function / lateral derived table / JSON_TABLE / hash join / atomic DDL / role-based authentication / common table expression 全部進來。<strong>這不是「終於跟上 PG」、是 MySQL 第一次有資格進入 SQL 工程深度討論</strong>。但有 caveats：每個特性的 <em>行為實現</em> 跟 PostgreSQL 對應特性都有 <em>微妙差異</em>、不能假設 PG 經驗直接套用。</p>
<p>對從 PostgreSQL 過來評估 MySQL 的讀者：本文是 <em>特性對等驗證</em> — 哪些 8.0 特性真的可以 production 用、哪些是 marketing 但實作有 gap。對既有 MySQL 5.7 user：本文是 <em>upgrade 5.7 → 8.0 的具體 ROI</em> — 從 SQL feature 角度看升級值不值得。</p>
<h2 id="5-個關鍵特性--pg-對比">5 個關鍵特性 + PG 對比</h2>
<h3 id="特性-1ctecommon-table-expression">特性 1：CTE（Common Table Expression）</h3>
<p>MySQL 8.0 / PG 8.4+ 都支援。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- MySQL 8.0 + PG 都 OK
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">WITH</span><span class="w"> </span><span class="n">order_summary</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2026-01-01&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">os</span><span class="p">.</span><span class="n">total</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">order_summary</span><span class="w"> </span><span class="n">os</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">os</span><span class="p">.</span><span class="n">user_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">os</span><span class="p">.</span><span class="n">total</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span></span></span></code></pre></div><p><strong>行為差異</strong>：</p>
<ul>
<li><strong>MySQL 8.0</strong>：CTE <em>不 materialize 為預設</em>、optimizer 把 CTE 視為 <em>inlined subquery</em>、CTE 引用兩次以上會 <em>重複計算</em></li>
<li><strong>PostgreSQL（&lt; 12）</strong>：CTE <em>fence by default</em>（materialize barrier）、optimizer 不 push predicate 進 CTE</li>
<li><strong>PostgreSQL（12+）</strong>：CTE 行為跟 MySQL 接近、有 <code>MATERIALIZED</code> / <code>NOT MATERIALIZED</code> keyword 明示</li>
</ul>
<p>對 PG 12+ user：可以套 MySQL 經驗。對 PG 11 以下 user：CTE 行為跟 MySQL 不一樣、要重看 query plan。</p>
<p><strong>Recursive CTE</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="k">RECURSIVE</span><span class="w"> </span><span class="n">org_chart</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">manager_id</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">depth</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">manager_id</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="k">UNION</span><span class="w"> </span><span class="k">ALL</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">manager_id</span><span class="p">,</span><span class="w"> </span><span class="n">oc</span><span class="p">.</span><span class="n">depth</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="w"> </span><span class="n">e</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">org_chart</span><span class="w"> </span><span class="n">oc</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">manager_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">oc</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">org_chart</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">depth</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>兩家都支援、但 MySQL 8.0 有 <em>深度上限</em>（<code>cte_max_recursion_depth=1000</code>、預設 1000、PG 預設 unlimited）。複雜 hierarchical query（深度 &gt; 1000）MySQL 需要顯式提高 limit。</p>
<h3 id="特性-2window-function">特性 2：Window Function</h3>
<p>MySQL 8.0 / PG 8.4+ 都支援、語法同 SQL standard。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">order_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">running_total</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="n">RANK</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">amount</span><span class="w"> </span><span class="k">DESC</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">rank_in_user</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p><strong>行為差異</strong>：</p>
<ul>
<li><strong>執行 plan</strong>：MySQL 8.0 用 <em>window iterator</em>、單 partition 內 sort、外加 in-memory window buffer。PostgreSQL 有更成熟的 <em>WindowAgg node</em>、複雜 frame spec 處理更好</li>
<li><strong>Frame spec 支援度</strong>：兩家都支援 ROWS / RANGE / GROUPS、但 <em>GROUPS frame</em> MySQL 是 8.0.16+ 才補進、PG 11+ 才補</li>
<li><strong>大資料量 spill behavior</strong>：MySQL window function 超過 <code>sort_buffer_size</code>（預設 256K）會 spill 到 disk、Performance 雪崩。PG 用 <code>work_mem</code>（預設 4MB）、寬裕些但也會 spill</li>
</ul>
<p>對長期用 PG window function 寫複雜 reporting query 的 user：MySQL 8.0 可以做、但 <em>效能 tune</em> 工作量大、不是 drop-in。</p>
<h3 id="特性-3json_tablepg-主要賣點對比">特性 3：JSON_TABLE（PG 主要賣點對比）</h3>
<p>這是 user 點到的對比重點。</p>
<p><strong>MySQL 8.0 的 JSON_TABLE</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">t</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">j</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">j</span><span class="p">.</span><span class="n">price</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="n">t</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">     </span><span class="n">JSON_TABLE</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">         </span><span class="n">t</span><span class="p">.</span><span class="n">metadata</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">         </span><span class="s1">&#39;$.variants[*]&#39;</span><span class="w"> </span><span class="n">COLUMNS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">             </span><span class="n">name</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span><span class="w"> </span><span class="n">PATH</span><span class="w"> </span><span class="s1">&#39;$.name&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">             </span><span class="n">price</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="n">PATH</span><span class="w"> </span><span class="s1">&#39;$.price&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">         </span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">     </span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">j</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">t</span><span class="p">.</span><span class="n">category</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shoes&#39;</span><span class="p">;</span></span></span></code></pre></div><p>JSON_TABLE 把 JSON document 內的 array element 展開成 <em>relational rows</em>、然後可以 JOIN / WHERE / GROUP BY。SQL:2016 standard 規範。</p>
<p><strong>PostgreSQL 對應</strong>：</p>
<p>PG 17+ 有 <code>JSON_TABLE</code>（SQL:2016 standard、跟 MySQL 同語法）、但歷史上 PG user 用兩條不同路線：</p>
<ol>
<li>
<p><strong>JSONB operator</strong>（PG 9.4+）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">metadata</span><span class="o">-&gt;</span><span class="s1">&#39;variants&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">variants</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p><strong>jsonb_path_query</strong>（PG 12+）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">t</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">.</span><span class="n">price</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="n">t</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">     </span><span class="n">jsonb_path_query</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*]&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">v</span><span class="p">;</span></span></span></code></pre></div></li>
</ol>
<p><strong>核心差異</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL JSON_TABLE</th>
          <th>PG JSONB operator</th>
          <th>PG jsonb_path_query</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Index</td>
          <td>必須對 JSON column 建 <em>generated column + 一般 index</em>、不能直接 GIN index JSON path</td>
          <td><strong>GIN index 直接 over JSONB</strong>（業界唯一）</td>
          <td>可以走 GIN expression index</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>JSON column = LONGTEXT 包裝</td>
          <td>JSONB = binary、壓縮、index 友善</td>
          <td>同左</td>
      </tr>
      <tr>
          <td>Query 效率（複雜 path）</td>
          <td>中等（需要 generated column 加速）</td>
          <td>高（GIN index 直接）</td>
          <td>高</td>
      </tr>
      <tr>
          <td>SQL standard 對齊</td>
          <td>高（JSON_TABLE 是 standard）</td>
          <td>低（JSONB operator 是 PG 專有）</td>
          <td>中（jsonpath 是 standard）</td>
      </tr>
      <tr>
          <td>大 JSON（&gt; 1 MB）</td>
          <td>LONGTEXT 仍可、但 query 慢</td>
          <td>JSONB 壓縮 + 部分 read</td>
          <td>同左</td>
      </tr>
  </tbody>
</table>
<p><strong>選型結論</strong>：</p>
<ul>
<li><strong>MySQL 是 JSON-storage 角色</strong>（document 順手存進關聯 DB）：JSON_TABLE 夠用、配 generated column + index、production-ready</li>
<li><strong>MySQL 是 document-heavy workload</strong>（大量 JSON-driven query / 複雜 path / 高 selectivity）：PG JSONB GIN index 仍是 <em>clearly winner</em>、或直接用 MongoDB</li>
<li><strong>MySQL 8.0 JSON 不是 PG JSONB 替代</strong>：JSON_TABLE 是 <em>SQL standard 對齊</em>、好 portable、但 <em>index 跟 storage 仍弱</em></li>
</ul>
<p>對「JSON 是 PG 主要賣點」的判斷：JSONB binary storage + GIN index 是 PG 在 JSON workload 的 <em>結構性優勢</em>、MySQL 8.0 補了 SQL_TABLE 但 <em>index 那層沒補</em>。8.0 後 JSON 議題 <em>不是 deal-breaker for MySQL</em>（不像 5.7 時代直接 disqualify）、但仍不是 MySQL 主場。</p>
<h3 id="特性-4lateral-derived-table">特性 4：Lateral Derived Table</h3>
<p>MySQL 8.0.14+ / PG 9.3+ 都支援。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 對每個 user、找他最近 5 個 order
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">recent</span><span class="p">.</span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="n">u</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="k">LATERAL</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">order_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">recent</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="k">true</span><span class="p">;</span></span></span></code></pre></div><p>Lateral 讓 subquery 可以 <em>引用外部 reference column</em>（<code>u.id</code>）、不可能用 plain subquery 寫出來。</p>
<p><strong>行為差異</strong>：</p>
<ul>
<li>MySQL 8.0：lateral 後加、optimizer plan 仍在演進、複雜 lateral query 可能 plan 次優</li>
<li>PostgreSQL：lateral 早就成熟、plan 跟 join 直接 fuse、效率高</li>
</ul>
<p>對 PG-experienced 使用 lateral 寫 reporting query 的 user：MySQL 8.0 可以、但有時候要 hint optimizer 達到最佳 plan。</p>
<h3 id="特性-5hash-join">特性 5：Hash Join</h3>
<p>MySQL 8.0.18+ / PG 早已有。</p>
<p><strong>MySQL 8.0 之前</strong>：只有 <em>nested loop join</em>、大表 JOIN 完全失控（n × m row scan）。8.0.18 加 hash join、optimizer 在預估 row count 大時自動切。</p>
<p><strong>注意</strong>：MySQL 8.0 hash join 預設 <em>不對所有 join 開</em>、只在 <code>optimizer_switch='hash_join=on'</code> 且 join condition 是 <em>equality on indexed column</em> 時觸發。常見錯估：複雜 join 條件不觸發 hash join、optimizer fallback nested loop、query 永遠跑不完。</p>
<p><strong>PG 對應</strong>：PG 一直有 hash join、optimizer 預設 cover 廣、且有 <em>parallel hash join</em>（PG 11+）大表 JOIN 並行加速。</p>
<p>MySQL hash join 是 <em>補洞</em>、不是 <em>並肩特性</em>。複雜 OLAP query MySQL 仍弱於 PG。</p>
<h2 id="其他-80-特性一句話帶過">其他 8.0 特性（一句話帶過）</h2>
<ul>
<li><strong>Atomic DDL</strong>：CREATE TABLE / DROP / ALTER 變 transactional、crash recovery 不會留 orphan table（PG 早就 atomic）</li>
<li><strong>Role-based authentication</strong>：role 取代 group-level grant、user 可繼承 role（PG 早就 role 系統）</li>
<li><strong>CHECK constraint enforcement</strong>：5.7 可寫但不執行、8.0 真的 enforce（PG 一直執行）</li>
<li><strong>invisible index</strong>：建 index 但 optimizer 暫不用、適合 staging query plan 測試（PG 沒原生對應）</li>
<li><strong>Resource Group</strong>：query 跑時可分配 CPU thread 給特定 user group（PG 沒原生對應）</li>
<li><strong>Generated column</strong>：MySQL 5.7 已有、8.0 強化、可作為 JSON path 加速的 workaround</li>
</ul>
<h2 id="配置-step-by-step從-57--80-sql-feature-升級">配置 step-by-step（從 5.7 → 8.0 SQL feature 升級）</h2>
<p>如果已經是 8.0、所有特性都可以用、不必額外配置。如果是 5.7 → 8.0、需要：</p>
<ol>
<li><strong><code>character_set_server=utf8mb4</code></strong>：8.0 預設 utf8mb4（5.7 預設 latin1）、character set 不一致導致 query 行為微差</li>
<li><strong><code>default_authentication_plugin=mysql_native_password</code></strong>：8.0 預設 caching_sha2_password、舊 client 連不上、cluster upgrade 期間用 native_password 保兼容</li>
<li><strong><code>optimizer_switch='hash_join=on'</code></strong>：確認 hash join 啟用、預設應該已 ON</li>
<li><strong><code>cte_max_recursion_depth=10000</code></strong>：複雜 recursive CTE 需要時提高</li>
<li><strong>重新 review 所有 ORM-generated SQL</strong>：8.0 keywords 變多（WINDOW、RANK、LATERAL 等變成 reserved word）、5.7 識別碼可能變 syntax error</li>
</ol>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-cte-引用兩次--跑兩次">1. CTE 引用兩次 = 跑兩次</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">expensive</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="n">heavy</span><span class="w"> </span><span class="n">aggregation</span><span class="w"> </span><span class="p">...)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">expensive</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">UNION</span><span class="w"> </span><span class="k">ALL</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">expensive</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">other_condition</span><span class="p">;</span></span></span></code></pre></div><p>預期 CTE 跑一次、實際 MySQL 跑兩次。Query 時間 doubled。</p>
<p>修法：</p>
<ul>
<li>把 CTE 結果先 INSERT 進 <em>temporary table</em>、SELECT 兩次走 temp table（手動 materialize）</li>
<li>或 PG 用 <code>MATERIALIZED</code> keyword（MySQL 沒對應 hint、要手動 temp table）</li>
</ul>
<h3 id="2-window-function-大-partition-spill-到-disk">2. Window function 大 partition spill 到 disk</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">order_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 1 億 row</span></span></span></code></pre></div><p><code>sort_buffer_size=256K</code> 預設、單 partition &gt; 256K row 開始 spill disk、執行從秒級變分鐘級。</p>
<p>修法：</p>
<ul>
<li>提高 <code>sort_buffer_size</code>（per-connection、不要設太大、connection × buffer 會吃 RAM）</li>
<li>加 INDEX 包含 <code>user_id, created_at</code>、optimizer 可直接用 sorted index、不必額外 sort</li>
</ul>
<h3 id="3-json_table-跟-generated-column-取捨錯誤">3. JSON_TABLE 跟 generated column 取捨錯誤</h3>
<p>直接 JSON_TABLE on every query：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">JSON_TABLE</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*]&#39;</span><span class="w"> </span><span class="n">COLUMNS</span><span class="w"> </span><span class="p">(...));</span></span></span></code></pre></div><p>每次 query 跑 JSON parse、無 index 加速、大表 query 慢。</p>
<p>修法：</p>
<ul>
<li>
<p>對 <em>常 query 的 JSON path</em> 建 generated column：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">products</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">category</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="n">JSON_UNQUOTE</span><span class="p">(</span><span class="n">metadata</span><span class="o">-&gt;</span><span class="s1">&#39;$.category&#39;</span><span class="p">))</span><span class="w"> </span><span class="n">STORED</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_category</span><span class="w"> </span><span class="p">(</span><span class="n">category</span><span class="p">);</span></span></span></code></pre></div></li>
<li>
<p>JSON_TABLE 用於 <em>ad-hoc query</em>、不要當熱 path</p>
</li>
<li>
<p>跟 PG JSONB GIN 對比：PG 不必預先建 generated column、GIN index 直接 over JSONB</p>
</li>
</ul>
<h3 id="4-hash-join-沒觸發--optimizer-預估錯-row-count">4. Hash join 沒觸發 — Optimizer 預估錯 row count</h3>
<p>JOIN 大表預期 hash join、實際 MySQL 跑 nested loop、query 跑不完。常見原因：</p>
<ul>
<li>Table statistics 過時（沒跑 <code>ANALYZE TABLE</code>）</li>
<li>Join condition 不是 pure equality（<code>a.id = b.id + 1</code> 等）</li>
<li>一邊有 LIMIT、optimizer 估 small set、選 nested loop</li>
</ul>
<p>修法：</p>
<ul>
<li>跑 <code>ANALYZE TABLE</code> 更新 statistics</li>
<li>用 <code>EXPLAIN ANALYZE</code> 看實際 row count vs 估計</li>
<li>用 <code>optimizer_hint</code>（如 <code>/*+ HASH_JOIN(t1 t2) */</code>）強制</li>
</ul>
<h3 id="5-recursive-cte-深度上限--production-query-突然-fail">5. Recursive CTE 深度上限 — Production query 突然 fail</h3>
<p><code>cte_max_recursion_depth=1000</code> 預設、organization hierarchy / tree query 超過 1000 層直接 fail（<code>ER_CTE_MAX_RECURSION_DEPTH_EXCEEDED</code>）。</p>
<p>修法：</p>
<ul>
<li>評估真實 hierarchy 深度、設 <code>cte_max_recursion_depth=10000</code> 或更高</li>
<li>或 query 加 <code>WHERE depth &lt; N</code> 提前停（不依賴 implicit limit）</li>
<li>對極大 hierarchy（社群 follow graph 等）改用 <em>graph DB</em>（Neo4j）— MySQL recursive CTE 不是 graph workload 主場</li>
</ul>
<h2 id="mysql-80-vs-pg-sql-特性-cross-reference">MySQL 8.0 vs PG SQL 特性 cross-reference</h2>
<table>
  <thead>
      <tr>
          <th>特性</th>
          <th>MySQL 8.0</th>
          <th>PostgreSQL</th>
          <th>差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CTE</td>
          <td>8.0+</td>
          <td>8.4+</td>
          <td>PG 2009 即支援、MySQL 2018 才支援、約晚 9 年</td>
      </tr>
      <tr>
          <td>Recursive CTE</td>
          <td>8.0+（depth 限）</td>
          <td>8.4+（unlimited）</td>
          <td>PG 無深度上限</td>
      </tr>
      <tr>
          <td>Window function</td>
          <td>8.0+</td>
          <td>8.4+</td>
          <td>Frame spec 兩家略不同（GROUPS frame 推出時點）</td>
      </tr>
      <tr>
          <td>Lateral</td>
          <td>8.0.14+</td>
          <td>9.3+</td>
          <td>PG plan 較成熟</td>
      </tr>
      <tr>
          <td>JSON_TABLE</td>
          <td>8.0+</td>
          <td>17+</td>
          <td>MySQL 早 6 年（SQL:2016 standard）</td>
      </tr>
      <tr>
          <td>JSONB index</td>
          <td>無原生</td>
          <td>GIN index over JSONB</td>
          <td><strong>PG 結構優勢</strong></td>
      </tr>
      <tr>
          <td>Hash join</td>
          <td>8.0.18+</td>
          <td>早</td>
          <td>PG parallel hash join</td>
      </tr>
      <tr>
          <td>Atomic DDL</td>
          <td>8.0+</td>
          <td>早</td>
          <td>PG 一直 atomic</td>
      </tr>
      <tr>
          <td>Common keyword</td>
          <td>補齊</td>
          <td>完整</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Role-based auth</td>
          <td>8.0+</td>
          <td>早</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Materialized view</td>
          <td>無原生</td>
          <td>9.3+</td>
          <td><strong>PG 結構優勢</strong>（MySQL 用 trigger / scheduled refresh 模擬）</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>無</td>
          <td>早</td>
          <td><strong>PG 結構優勢</strong></td>
      </tr>
      <tr>
          <td>Expression index</td>
          <td>8.0.13+</td>
          <td>早</td>
          <td>MySQL 後加</td>
      </tr>
      <tr>
          <td>Full-text search</td>
          <td>內建（InnoDB 5.6+）</td>
          <td>內建（tsvector）</td>
          <td>PG full-text 更成熟</td>
      </tr>
      <tr>
          <td>Foreign data wrapper</td>
          <td>無原生</td>
          <td>早（FDW）</td>
          <td><strong>PG 結構優勢</strong></td>
      </tr>
  </tbody>
</table>
<p>8.0 補了 <em>語法層</em> 大部分缺漏、<em>storage / index / extensibility 層</em> 仍是 PG 結構優勢。對「先選 SQL 工程深度」的 org、PG 仍領先；對「先選 ecosystem / replication / sharding」的 org、MySQL 已不是 disqualifier。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p>JSON column 在 InnoDB 是 LONGTEXT 包裝、大 JSON 進 off-page storage（<code>innodb_default_row_format=DYNAMIC</code> 才行、Antelope format 不支援）。Buffer pool 對 LONGTEXT 較不友善、大 JSON workload 可能要更大 buffer pool。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>8.0 新 hash join + lateral derived 讓 <em>EXPLAIN ANALYZE</em> 結果更複雜。優化複雜 query 需要熟 <em>新 plan node 類型</em>。詳見 <em>Query Optimization deep dive</em> 篇（待寫）。</p>
<h3 id="跟-online-schema-change">跟 Online Schema Change</h3>
<p>JSON column 跟 generated column 的 schema change 走 gh-ost / pt-osc 沒問題、但 JSON 大表 ALTER 速度比一般 column 慢（每 row 重 serialize）。詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-replication">跟 Replication</h3>
<p>Window function / CTE / JSON_TABLE 的 query <em>結果</em> replicate（row-level binlog 紀錄結果）、不 replicate <em>query 本身</em>。所以 replica apply 不會重新跑 window function、效率 OK。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h2 id="何時-sql-特性是-mysql-選型-driver">何時 SQL 特性是 MySQL 選型 driver</h2>
<ul>
<li><strong>想要 SQL standard 對齊跨 vendor portable</strong>：MySQL 8.0 JSON_TABLE / window 都對齊 standard、PG 部分能力（JSONB operator）是 PG-only、portability MySQL 略好</li>
<li><strong>JSON workload &lt; 20% query</strong>：MySQL 8.0 + generated column 夠用、不必為 JSON 換 PG</li>
<li><strong>JSON workload &gt; 50% query + 複雜 path / aggregation</strong>：PG JSONB GIN 仍 winner、考慮 PG 或 MongoDB</li>
<li><strong>需要 materialized view / FDW / partial index</strong>：PG 仍領先、不要因為 SQL feature parity 假設 MySQL 全 cover</li>
<li><strong>既有 MySQL 投資 + SQL 工程深度上升</strong>：升 8.0 + 訓練團隊用新特性、不是換 vendor</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>（JSON column 對 buffer pool 影響）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>（JSON column 大表 ALTER）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>（ROW-format binlog 對 window function）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">PostgreSQL SQL Features Baseline</a>（PG 反向視角、哪些特性 PG 早 5-15 年、MySQL 8.0 補齊後 PG 仍領先）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">PostgreSQL JSONB Deep Dive</a>（PG sibling、binary storage + GIN index 跟 MySQL JSON_TABLE 對比）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a>（JSON / SQL feature 對比 source）</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a>（document-heavy workload 替代）</li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html">MySQL 8.0 What&rsquo;s New</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/sql-features-baseline/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/sql-features-baseline/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>SQL features baseline&lt;/em> — PG 早期就有的、MySQL 8.0 才補的、PG 仍領先的、給從 MySQL 評估 PG 的讀者 reference。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-sql-工程深度的歷史錨點">PG SQL 工程深度的歷史錨點&lt;/h2>
&lt;p>PG 在 SQL feature 上長期領先 MySQL：&lt;/p>
&lt;ul>
&lt;li>2009 (PG 8.4)：CTE / window function / recursive query&lt;/li>
&lt;li>2013 (PG 9.3)：lateral derived table / materialized view&lt;/li>
&lt;li>2014 (PG 9.4)：JSONB / partial index 早就有 / GIN index&lt;/li>
&lt;li>2015 (PG 9.5)：UPSERT (&lt;code>ON CONFLICT&lt;/code>)&lt;/li>
&lt;li>2017 (PG 10)：declarative partitioning / logical replication / multi-column statistics&lt;/li>
&lt;/ul>
&lt;p>MySQL 8.0（2018）才補 CTE / window / lateral / JSON_TABLE / hash join — &lt;em>PG 早 9 年起步&lt;/em>。&lt;/p>
&lt;p>對 &lt;em>從 MySQL 評估 PG&lt;/em> 的讀者來說、PG 的 SQL 工程深度不只是「該有的都有」、更多是「PG 結構性領先的特性 + MySQL 8.0 補了哪些 + PG 仍領先哪些」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features&lt;/a> 對比視角：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>SQL features baseline</em> — PG 早期就有的、MySQL 8.0 才補的、PG 仍領先的、給從 MySQL 評估 PG 的讀者 reference。</p></blockquote>
<hr>
<h2 id="pg-sql-工程深度的歷史錨點">PG SQL 工程深度的歷史錨點</h2>
<p>PG 在 SQL feature 上長期領先 MySQL：</p>
<ul>
<li>2009 (PG 8.4)：CTE / window function / recursive query</li>
<li>2013 (PG 9.3)：lateral derived table / materialized view</li>
<li>2014 (PG 9.4)：JSONB / partial index 早就有 / GIN index</li>
<li>2015 (PG 9.5)：UPSERT (<code>ON CONFLICT</code>)</li>
<li>2017 (PG 10)：declarative partitioning / logical replication / multi-column statistics</li>
</ul>
<p>MySQL 8.0（2018）才補 CTE / window / lateral / JSON_TABLE / hash join — <em>PG 早 9 年起步</em>。</p>
<p>對 <em>從 MySQL 評估 PG</em> 的讀者來說、PG 的 SQL 工程深度不只是「該有的都有」、更多是「PG 結構性領先的特性 + MySQL 8.0 補了哪些 + PG 仍領先哪些」。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a> 對比視角：</p>
<ul>
<li>MySQL 8.0 視角：「我終於補齊 + 跟 PG 對比」</li>
<li>PG 視角：「我長期領先 + MySQL 8.0 才追上某些、其他我仍領先」</li>
</ul>
<h2 id="pg-結構性領先特性mysql-沒對應--弱對應">PG 結構性領先特性（MySQL 沒對應 / 弱對應）</h2>
<h3 id="1-materialized-view">1. Materialized View</h3>
<p>PG 9.3+ 內建 materialized view：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">orders_summary</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">order_count</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 手動 refresh
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">REFRESH</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">orders_summary</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 或 concurrent refresh（PG 9.4+、不 lock read）
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="n">REFRESH</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">orders_summary</span><span class="p">;</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li>預計算複雜 aggregation、查詢時極快</li>
<li>Concurrent refresh 不 lock read</li>
<li>可建 index on materialized view</li>
</ul>
<p><strong>MySQL 對應</strong>：沒原生 materialized view。常見替代：</p>
<ul>
<li>Trigger + summary table（手動維護）</li>
<li>Application 層 caching layer</li>
<li>用 view + cache layer（不是 materialization）</li>
</ul>
<p>MySQL 8.0+ 仍無原生 materialized view。</p>
<h3 id="2-partial-index">2. Partial Index</h3>
<p>PG 預設支援 partial index — 對 <em>滿足條件的 row</em> 才建 index：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 只對 active user 建 index
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_active_email</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="p">(</span><span class="n">email</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Index size 比 full index 小很多、query 性能跟 full index 一樣
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;x@y.com&#39;</span><span class="p">;</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li><em>Soft-delete</em> 場景：對 <code>deleted_at IS NULL</code> 建 partial index</li>
<li><em>Hot subset</em> 場景：對 <code>status = 'pending'</code> 等熱資料建 partial</li>
<li>Index 大小 / 寫入成本大降</li>
</ul>
<p><strong>MySQL 對應</strong>：MySQL 沒原生 partial index。MySQL 8.0+ 有 <em>functional index</em> 但跟 partial 不同。MySQL 替代：</p>
<ul>
<li>Generated column + index（接近、但維護複雜）</li>
<li>或接受 full index cost</li>
</ul>
<h3 id="3-foreign-data-wrapper-fdw">3. Foreign Data Wrapper (FDW)</h3>
<p>PG FDW 讓 query 跨外部資料源：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">postgres_fdw</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</span><span class="w"> </span><span class="k">FOREIGN</span><span class="w"> </span><span class="k">DATA</span><span class="w"> </span><span class="n">WRAPPER</span><span class="w"> </span><span class="n">postgres_fdw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">host</span><span class="w"> </span><span class="s1">&#39;remote.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">dbname</span><span class="w"> </span><span class="s1">&#39;analytics&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="n">MAPPING</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="n">localuser</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">user</span><span class="w"> </span><span class="s1">&#39;remoteuser&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">FOREIGN</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">remote_orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="p">...)</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</span><span class="w"> </span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">table_name</span><span class="w"> </span><span class="s1">&#39;orders&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 在 local PG query remote table
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">remote_orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span></span></span></code></pre></div><p>支援 FDW：<code>postgres_fdw</code> / <code>mysql_fdw</code> / <code>oracle_fdw</code> / <code>mongo_fdw</code> / <code>file_fdw</code> / <code>redis_fdw</code> 等。</p>
<p><strong>MySQL 對應</strong>：MySQL 8.0+ 有 FEDERATED engine（受限、不推薦）。實務上 MySQL 跨 DB query 用 application 層處理。</p>
<h3 id="4-jsonb--gin-indexpg-結構性優勢">4. JSONB + GIN Index（PG 結構性優勢）</h3>
<p>PG JSONB 是 <em>binary 儲存</em> + 可 <em>直接 GIN index</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">metadata</span><span class="w"> </span><span class="n">JSONB</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- GIN index over JSONB
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- 快 query
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@?</span><span class="w"> </span><span class="s1">&#39;$.variants[*].price &gt; 100&#39;</span><span class="p">;</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：MySQL 8.0 JSON_TABLE 是 SQL standard、但 <em>index 必須 generated column workaround</em>（不能 GIN index over JSON）。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a> JSON_TABLE vs PG JSONB 對比段。</p>
<h3 id="5-range-types--exclusion-constraints">5. Range Types + Exclusion Constraints</h3>
<p>PG range types + exclusion constraints 防止 <em>時間範圍重疊</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">reservations</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">room_id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">during</span><span class="w"> </span><span class="n">TSRANGE</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">EXCLUDE</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIST</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">=</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- INSERT 重疊 booking 自動 reject
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">reservations</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;[2026-05-19 10:00, 2026-05-19 12:00)&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">reservations</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;[2026-05-19 11:00, 2026-05-19 13:00)&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- ERROR: conflicting key value violates exclusion constraint</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：完全沒對應、必須 application 層 enforce。</p>
<h3 id="6-check-constraint--domain-type">6. CHECK Constraint + Domain Type</h3>
<p>PG <code>CHECK</code> constraint 真執行（MySQL 8.0 才補）+ user-defined <code>DOMAIN</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">DOMAIN</span><span class="w"> </span><span class="n">positive_int</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nb">INT</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">VALUE</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">quantity</span><span class="w"> </span><span class="n">positive_int</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">amount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：8.0+ 有 CHECK constraint enforcement（5.7 可寫但不執行）。沒 user-defined DOMAIN。</p>
<h3 id="7-extension-ecosystem">7. Extension Ecosystem</h3>
<p>PG extension 是 <em>結構優勢</em>：</p>
<ul>
<li><code>pg_partman</code>：自動 partition lifecycle</li>
<li><code>pg_repack</code>：online table rewrite</li>
<li><code>pg_stat_statements</code>：query stats</li>
<li><code>pgvector</code>：vector similarity search</li>
<li><code>pg_cron</code>：scheduled job</li>
<li><code>PostGIS</code>：GIS</li>
<li><code>TimescaleDB</code>：time-series</li>
<li><code>Citus</code>：sharding</li>
</ul>
<p><strong>MySQL 對應</strong>：MySQL plugin 機制有、生態遠遠不如。詳見 <em>PG Extension Ecosystem</em> 篇（待寫）。</p>
<h2 id="mysql-80-補齊的-pg-既有特性">MySQL 8.0 補齊的 PG 既有特性</h2>
<table>
  <thead>
      <tr>
          <th>特性</th>
          <th>PG 推出</th>
          <th>MySQL 推出</th>
          <th>差異後說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CTE</td>
          <td>8.4 (2009)</td>
          <td>8.0 (2018)</td>
          <td>MySQL 補語法、行為 PG 12+ 跟 MySQL 接近</td>
      </tr>
      <tr>
          <td>Window function</td>
          <td>8.4 (2009)</td>
          <td>8.0 (2018)</td>
          <td>兩家都標準、frame spec 細節有差</td>
      </tr>
      <tr>
          <td>Lateral derived table</td>
          <td>9.3 (2013)</td>
          <td>8.0.14 (2019)</td>
          <td>MySQL 後加、planner 不如 PG 成熟</td>
      </tr>
      <tr>
          <td>Hash join</td>
          <td>早就有</td>
          <td>8.0.18 (2019)</td>
          <td>MySQL 受限（equality on indexed column）</td>
      </tr>
      <tr>
          <td>JSON_TABLE</td>
          <td>17 (2024)</td>
          <td>8.0 (2018)</td>
          <td>MySQL 較早、PG 17+ 補進、PG 自己有 JSONB 路線</td>
      </tr>
      <tr>
          <td>CHECK constraint</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>MySQL 5.7 可寫但不執行</td>
      </tr>
      <tr>
          <td>Role-based auth</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Atomic DDL</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Common keyword</td>
          <td>完整</td>
          <td>8.0 補</td>
          <td>MySQL 5.7 缺很多 (window/rank/lateral 等)</td>
      </tr>
  </tbody>
</table>
<p>MySQL 8.0 是 <em>補齊 9 年 SQL standard 落後</em>、不是 <em>新領先 PG</em>。</p>
<h2 id="pg-仍領先的特性">PG 仍領先的特性</h2>
<p>對應「MySQL 8.0 補了 → PG 仍沒輸」的視角。以下 14 條中、<em>production 影響最大</em> 的是 Materialized view / Partial index / JSONB GIN / Full-text search 跟 Range / Exclusion constraints（schema-level expressiveness）；<em>次要但常用</em> 的是 Multi-column statistics 跟 Procedural language；<em>非典型但 niche 重要</em> 的是 User-defined DOMAIN / Generic table inheritance（讀者不必然知道、但 ORM 跟 schema migration 工具會用）：</p>
<table>
  <thead>
      <tr>
          <th>PG 領先特性</th>
          <th>MySQL 對應狀態</th>
          <th>補充</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Materialized view</td>
          <td>無原生</td>
          <td>application-side 重算成本高</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>無（functional index 不等同）</td>
          <td>對 boolean / status column 救 storage</td>
      </tr>
      <tr>
          <td>FDW</td>
          <td>弱（FEDERATED engine 不推薦）</td>
          <td>跨 DB query escape hatch</td>
      </tr>
      <tr>
          <td>JSONB GIN index</td>
          <td>無（generated column workaround）</td>
          <td>JSON workload 結構性差</td>
      </tr>
      <tr>
          <td>Range types</td>
          <td>無</td>
          <td>booking / availability schema 救命</td>
      </tr>
      <tr>
          <td>Exclusion constraints</td>
          <td>無</td>
          <td>range overlap 防護</td>
      </tr>
      <tr>
          <td>User-defined DOMAIN</td>
          <td>無</td>
          <td>column-level type constraint</td>
      </tr>
      <tr>
          <td>Extension ecosystem</td>
          <td>弱</td>
          <td>pgvector / TimescaleDB / PostGIS</td>
      </tr>
      <tr>
          <td>Full-text search 成熟</td>
          <td>InnoDB FTS 較弱</td>
          <td>tsvector + GIN + pg_trgm 三層</td>
      </tr>
      <tr>
          <td>Multi-column statistics</td>
          <td>8.0 histograms 部分對應、PG 更廣</td>
          <td>planner 更準</td>
      </tr>
      <tr>
          <td>Procedural language</td>
          <td>PL/pgSQL + 多語言（PL/Python / PL/Perl 等）</td>
          <td>Stored procedure（不擴語言）</td>
      </tr>
      <tr>
          <td>Recursive CTE 深度</td>
          <td>Unlimited</td>
          <td>1000（cte_max_recursion_depth）</td>
      </tr>
      <tr>
          <td>LSN-based replication</td>
          <td>簡潔</td>
          <td>binlog file+position（GTID 緩解）</td>
      </tr>
      <tr>
          <td>Generic table inheritance</td>
          <td>早就有</td>
          <td>無（multi-tenant schema 結構用）</td>
      </tr>
  </tbody>
</table>
<h2 id="對從-mysql-評估-pg的讀者">對「從 MySQL 評估 PG」的讀者</h2>
<p>讀者通常從 MySQL 8.0 過來、問題是 <em>「PG 比 MySQL 強在哪、弱在哪」</em>：</p>
<h3 id="pg-比-mysql-強">PG 比 MySQL 強</h3>
<ul>
<li><em>SQL 工程深度</em>：上面列的 7 個結構優勢</li>
<li><em>Extension ecosystem</em>：pgvector / TimescaleDB / Citus / pg_partman 等</li>
<li><em>Optimizer</em>：planner 對複雜 query 更成熟</li>
<li><em>Concurrency model</em>：MVCC + 少 lock（<a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>）</li>
</ul>
<h3 id="pg-比-mysql-弱">PG 比 MySQL 弱</h3>
<ul>
<li><em>Replication 機制簡潔度</em>：MySQL GTID 比 PG WAL + replication slot 配置簡單（<a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>）</li>
<li><em>Sharding ecosystem</em>：Vitess / PlanetScale 比 Citus 規模驗證高</li>
<li><em>Operational tooling 廣度</em>：pt-toolkit / gh-ost / Orchestrator 等</li>
<li><em>VACUUM 維護</em>：PG MVCC 必須 VACUUM、autovacuum 配錯議題多（<a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>）</li>
</ul>
<h3 id="選-pg-的核心-driver">選 PG 的核心 driver</h3>
<p>對 SQL 工程深度、extension、複雜 query / OLAP-style workload 的場景、PG 仍是首選。對純簡單 OLTP + 大規模 sharding、MySQL + Vitess 仍 competitive。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>：PG MVCC 是 SQL feature 的並行控制基礎</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>：PG planner 對 window / CTE / hash join 成熟</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">Citus Distributed</a>：extension 之一、體現 extension 生態</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>：MVCC 代價、跟 SQL feature 並行控制相關</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PG MVCC + Lock Model</a>（concurrency 基礎）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（planner 成熟度）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">PG Citus Distributed</a>（extension example）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（MVCC 維護）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>（sibling、反向視角）</li>
<li>官方：<a href="https://www.postgresql.org/about/featurematrix/">PostgreSQL Features</a></li>
</ul>
]]></content:encoded></item><item><title>3.C19 Wix：Multi-cluster Kafka zero-downtime 遷移</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-multi-cluster-migration/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-multi-cluster-migration/</guid><description>&lt;p>這個案例的核心責任是說明 single mega-cluster 的 metadata scaling ceiling 與分群策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Wix cluster metadata 從 2019 年 5K topic / 45K partition 漲到 20K topic / 200K partition、每日 record 從 450M 漲到 2.5B、controller startup 與 broker stability 受 metadata 量壓垮。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不用 MirrorMaker、自建 Replicator service + Migration Orchestrator、用 Kafka topic 當控制平面協調 consumer 切換 + offset mapping；按 SLA 切多 cluster。揭露「topic / partition 數量」是 broker 級別的物理上限、不能無限擴張。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：cross-region MirrorMaker / topic 生命週期 / 分層叢集策略。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn TopicGC&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/wix-engineering/migrating-to-a-multi-cluster-managed-kafka-with-0-downtime-b936655f888e">Migrating to a Multi-Cluster Managed Kafka with 0 Downtime&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 single mega-cluster 的 metadata scaling ceiling 與分群策略。</p>
<h2 id="觀察">觀察</h2>
<p>Wix cluster metadata 從 2019 年 5K topic / 45K partition 漲到 20K topic / 200K partition、每日 record 從 450M 漲到 2.5B、controller startup 與 broker stability 受 metadata 量壓垮。</p>
<h2 id="判讀">判讀</h2>
<p>不用 MirrorMaker、自建 Replicator service + Migration Orchestrator、用 Kafka topic 當控制平面協調 consumer 切換 + offset mapping；按 SLA 切多 cluster。揭露「topic / partition 數量」是 broker 級別的物理上限、不能無限擴張。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：cross-region MirrorMaker / topic 生命週期 / 分層叢集策略。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn TopicGC</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/wix-engineering/migrating-to-a-multi-cluster-managed-kafka-with-0-downtime-b936655f888e">Migrating to a Multi-Cluster Managed Kafka with 0 Downtime</a></li>
</ul>
]]></content:encoded></item><item><title>CrowdStrike Falcon Cloud Security</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/</guid><description>&lt;p>CrowdStrike Falcon Cloud Security 是 CrowdStrike 在 Falcon endpoint EDR 平台之上擴張出來的 CNAPP（Cloud-Native Application Protection Platform）產品線。它的核心邏輯是把已經跑在 endpoint 上的 &lt;em>Falcon agent&lt;/em> 同時拿來收 cloud workload / container / Kubernetes node 的 telemetry、再把 CrowdStrike Intelligence 的 threat actor profile 直接餵進 detection rule。對已是 CrowdStrike endpoint 客戶來說、邊際 onboarding cost 接近 0；對非 CrowdStrike 環境、選它的訴求應該是 &lt;em>threat intel + EDR 同 console&lt;/em> 而不是 CSPM 本身。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Falcon Cloud Security 的定位是 &lt;em>agent-first 的 CNAPP&lt;/em>、設計重心在「endpoint EDR agent 順便收 cloud workload 訊號」這條路徑、agentless CSPM 是補位、不是主軸。產品線靠多次收購整合：&lt;em>Bionic&lt;/em>（2023 收購、現為 Falcon ASPM、application security posture management）負責 application architecture + runtime risk map；&lt;em>Flow Security&lt;/em>（2024 收購、現為 Falcon Data Protection / DSPM）負責 sensitive data 發現與 access path；endpoint / workload / container runtime 偵測由 Falcon agent 自家補。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &amp;#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz&lt;/a> 比、Falcon CS 走 &lt;em>agent-first + EDR 整合&lt;/em>、Wiz 走 &lt;em>agentless-first + cloud workload graph&lt;/em>。已部署 Falcon endpoint 的客戶上 Falcon CS 邊際成本 0；純 cloud-native 沒 endpoint workload 的環境、Falcon 的 agent 紅利不存在、Wiz 更快出價值。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &amp;#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud&lt;/a> 比、兩者都走 agent + agentless 雙軌、Prisma 強項是 &lt;em>compliance pack 跟 IaC scanning 模板&lt;/em>、Falcon 強項是 &lt;em>CrowdStrike Intelligence threat actor profile + Counter Adversary Operations 提供的 hunting 服務&lt;/em>。跟 Lacework 比、Lacework 走 &lt;em>behavioral baseline / anomaly detection&lt;/em>、Falcon 走 &lt;em>signature + threat intel&lt;/em>、兩種偵測哲學。&lt;/p></description><content:encoded><![CDATA[<p>CrowdStrike Falcon Cloud Security 是 CrowdStrike 在 Falcon endpoint EDR 平台之上擴張出來的 CNAPP（Cloud-Native Application Protection Platform）產品線。它的核心邏輯是把已經跑在 endpoint 上的 <em>Falcon agent</em> 同時拿來收 cloud workload / container / Kubernetes node 的 telemetry、再把 CrowdStrike Intelligence 的 threat actor profile 直接餵進 detection rule。對已是 CrowdStrike endpoint 客戶來說、邊際 onboarding cost 接近 0；對非 CrowdStrike 環境、選它的訴求應該是 <em>threat intel + EDR 同 console</em> 而不是 CSPM 本身。</p>
<h2 id="服務定位">服務定位</h2>
<p>Falcon Cloud Security 的定位是 <em>agent-first 的 CNAPP</em>、設計重心在「endpoint EDR agent 順便收 cloud workload 訊號」這條路徑、agentless CSPM 是補位、不是主軸。產品線靠多次收購整合：<em>Bionic</em>（2023 收購、現為 Falcon ASPM、application security posture management）負責 application architecture + runtime risk map；<em>Flow Security</em>（2024 收購、現為 Falcon Data Protection / DSPM）負責 sensitive data 發現與 access path；endpoint / workload / container runtime 偵測由 Falcon agent 自家補。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> 比、Falcon CS 走 <em>agent-first + EDR 整合</em>、Wiz 走 <em>agentless-first + cloud workload graph</em>。已部署 Falcon endpoint 的客戶上 Falcon CS 邊際成本 0；純 cloud-native 沒 endpoint workload 的環境、Falcon 的 agent 紅利不存在、Wiz 更快出價值。跟 <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a> 比、兩者都走 agent + agentless 雙軌、Prisma 強項是 <em>compliance pack 跟 IaC scanning 模板</em>、Falcon 強項是 <em>CrowdStrike Intelligence threat actor profile + Counter Adversary Operations 提供的 hunting 服務</em>。跟 Lacework 比、Lacework 走 <em>behavioral baseline / anomaly detection</em>、Falcon 走 <em>signature + threat intel</em>、兩種偵測哲學。</p>
<p>關鍵張力：<em>agent 是 single point of compromise</em> 是 Falcon agent-first 路線的長期信任成本。2024-07 Falcon sensor 推 bad content update 導致全球 Windows host BSOD 的事件、把 <em>kernel-level agent 一改全炸</em> 的風險具象化、對 agent-first vendor 是長期教訓。選 Falcon CS 等於買 agent 在 host kernel 的存取權、要把 <em>agent 自身的供應鏈</em> 當成風險來源納入評估。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Falcon Cloud Security 在 cloud security stack 中承擔哪一段（CSPM / CWPP / CIEM / ASPM / DSPM）、哪些靠 Falcon agent、哪些靠 agentless connector</li>
<li>已有 CrowdStrike endpoint 跟沒有 CrowdStrike endpoint 兩種起點下、Falcon CS 的判讀是否一樣</li>
<li>CrowdStrike Intelligence 跟 Counter Adversary Operations 在 detection lifecycle 的位置</li>
<li>何時用 Falcon CS、何時走 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> / <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a> / Lacework 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Falcon Cloud Security deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Agent coverage 跟版本治理</strong>：哪些 host / workload / container 跑 Falcon agent、是否跨 endpoint + cloud workload + Kubernetes node 一致、agent version 跟 sensor content channel 是否走 staging tenant + canary rollout（2024-07 incident 後的硬性要求）</li>
<li><strong>Agentless connector 覆蓋</strong>：CSPM 連到哪些 cloud account（AWS / GCP / Azure / OCI）、CIEM 是否拉 IAM identity graph、ASPM 連到哪些 application code repo</li>
<li><strong>Threat intel 是否接進 detection lifecycle</strong>：CrowdStrike Intelligence 的 IoC / threat actor TTP 是否餵進 Falcon detection rule、Counter Adversary Operations（MDR / threat hunting 服務）是否訂閱</li>
<li><strong>跟 Falcon EDR 同 console / IR handoff</strong>：cloud finding 跟 endpoint finding 是否在同一個 Incident view、SOC team 跟 cloud team 的 routing 是否定義、跟 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> 是否對齊</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Falcon agent 統一</strong>：endpoint EDR 用的 Falcon sensor 同時收 cloud workload 上的 process / file / network telemetry、不需要再裝第二支 agent。對已用 Falcon endpoint 的組織意義最大 — VM / container host 上裝 Falcon 就同時是 EDR + CWPP + container runtime detection。新環境要評估 <em>agent 的 kernel 存取權</em> 是否可接受、container 內是否能或需要部署 agent（Falcon Container Sensor 走 sidecar / DaemonSet）。</p>
<p><strong>CSPM</strong>：agentless 連 cloud account（AWS / GCP / Azure / OCI）、掃 misconfiguration（public S3 / over-privileged role / unencrypted disk）、對照 CIS Benchmark / NIST / PCI 模板。CSPM 是 <em>配置面</em> 訊號、補 agent 看不到的 cloud control plane 行為（例如 IAM policy change、S3 bucket policy 改變）。</p>
<p><strong>CWPP — workload + container + Kubernetes</strong>：Falcon agent 在 VM host / container host / Kubernetes node 上做 runtime detection、看 process spawn、file integrity、network connection、container escape attempt。比 agentless snapshot scan 強的是 <em>runtime behavior</em>（看到實際發生的 process tree），比 Wiz agentless 弱的是 <em>初始 coverage 速度</em>（要先部署 agent）。</p>
<p><strong>CIEM</strong>：把 cloud IAM identity 跟 access 畫成 graph、識別 over-privileged role / unused permission / cross-account trust risk。跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> Access Analyzer / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> Policy Intelligence 是補位、不是替代 — CIEM 給的是 <em>跨雲 + 跨 identity provider</em> 的 risk view。</p>
<p><strong>ASPM（前 Bionic）</strong>：application security posture management、把 application architecture（service graph / data flow / external dependency / vulnerability）畫成 map、識別哪個 vulnerability 真的可達 production attack surface。跟 Wiz Code / Snyk 的訴求重疊、但 Bionic 強項是 <em>runtime + architecture</em> 而不是 pure SAST/SCA。導入需要拉 application telemetry、不是裝完就有結果。</p>
<p><strong>DSPM（前 Flow Security）</strong>：data security posture management、掃 cloud storage / database / SaaS 裡的 sensitive data 位置、誰能存取、access path 是什麼。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 不同層 — DSPM 是 <em>posture 層</em>（who can access what）、DLP 是 <em>runtime 層</em>（actual data egress event）、兩者互補。</p>
<p><strong>CrowdStrike Intelligence 整合</strong>：CrowdStrike Intelligence 是 CrowdStrike 自家的 threat intel team、定期發 threat actor profile（COZY BEAR / FANCY BEAR / Scattered Spider 等命名來自 CrowdStrike）、IoC、TTP。Falcon CS detection rule 直接吃這層、不用 SOC team 自己訂閱外部 threat feed。這是 Falcon CS 跟 <em>純 CNAPP 競品</em>（Wiz / Prisma）最大差異 — 競品要再買 Mandiant / Recorded Future 才能補。</p>
<p><strong>Charlotte AI</strong>：CrowdStrike 的 LLM-assisted investigation 介面、SOC analyst 用自然語言問 incident（「過去 24hr 有哪些 process 是 first-seen across fleet」）、Charlotte 翻成 Falcon query 跑。屬 SOC productivity 補位、不是 detection logic 本身。</p>
<p><strong>跟 Falcon LogScale / Identity Protection 同 plane</strong>：完整 CrowdStrike stack 客戶可以把 Falcon LogScale（前 Humio 收購、SIEM）+ Falcon Identity Protection（identity threat detection）跟 Falcon CS 整合在同一個 console。Single pane of glass 強、但 vendor lock-in 也最深、退場成本是業界最高。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>CrowdStrike Falcon CS</th>
          <th>Wiz</th>
          <th>Prisma Cloud</th>
          <th>Lacework</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Agent 策略</td>
          <td>Agent-first（Falcon sensor）+ agentless 補位</td>
          <td>Agentless-first（snapshot scan）+ runtime sensor</td>
          <td>Agent + agentless 雙軌（Defender agent）</td>
          <td>Agent-based（Lacework agent + Polygraph）</td>
      </tr>
      <tr>
          <td>強項</td>
          <td>EDR 整合、threat intel、Counter Adversary Ops</td>
          <td>Cloud workload graph、快速 onboarding、無 agent</td>
          <td>Compliance pack、IaC scanning、廣覆蓋</td>
          <td>Behavioral baseline、anomaly detection</td>
      </tr>
      <tr>
          <td>Threat intel</td>
          <td>CrowdStrike Intelligence 內建</td>
          <td>外部 feed integration</td>
          <td>Unit 42 threat intel 內建</td>
          <td>外部 feed integration</td>
      </tr>
      <tr>
          <td>ASPM / app layer</td>
          <td>Falcon ASPM（前 Bionic、runtime + architecture）</td>
          <td>Wiz Code（SAST / SCA / IaC）</td>
          <td>Prisma Code Security（前 Bridgecrew）</td>
          <td>有限</td>
      </tr>
      <tr>
          <td>DSPM</td>
          <td>Falcon Data Protection（前 Flow Security）</td>
          <td>Wiz DSPM</td>
          <td>Data Security Posture Management</td>
          <td>有限</td>
      </tr>
      <tr>
          <td>MDR / hunting</td>
          <td>Counter Adversary Operations（業界先驅）</td>
          <td>無 first-party MDR</td>
          <td>Cortex MDR（Palo Alto）</td>
          <td>有限</td>
      </tr>
      <tr>
          <td>跟 EDR 同 console</td>
          <td>內建（Falcon EDR / Identity Protection / LogScale）</td>
          <td>需外接</td>
          <td>Cortex XDR（同 Palo Alto stack）</td>
          <td>需外接</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>已用 Falcon endpoint、看重 threat intel + MDR</td>
          <td>Cloud-native、無 endpoint workload、要快</td>
          <td>Palo Alto stack 客戶、compliance-heavy 產業</td>
          <td>中等規模、behavioral detection 為主</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>最高（agent + console + threat intel + MDR 綁定）</td>
          <td>中（agentless 退出較快）</td>
          <td>高（Palo Alto stack 整合深）</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<p>選 Falcon CS 的核心訴求：<em>已是 CrowdStrike endpoint 客戶 / SOC team 已熟 Falcon console + 看重 CrowdStrike Intelligence threat intel + 願意接受 agent-first 的供應鏈風險</em>。純 cloud-only 沒 endpoint workload、agent 紅利不存在、走 <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> 更划算。非 CrowdStrike 環境想要 compliance + IaC、走 <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Counter Adversary Operations（MDR + threat hunting）</strong>：CrowdStrike 的 managed detection and response 服務、24x7 SOC team + threat hunter 主動掃客戶環境裡的 adversary 跡象。跟一般 MDR 不同的是、它直接接 CrowdStrike Intelligence 的 threat actor profile、看到 TTP 匹配就主動 hunt 而不是等 alert。對 SOC team 規模有限但要面對 nation-state actor 的組織、是補 SOC capability 的快路。</p>
<p><strong>CrowdStrike Intelligence threat actor profile</strong>：CrowdStrike 把 threat actor 用命名規則（BEAR = Russian state、PANDA = Chinese state、KITTEN = Iranian state、SPIDER = eCrime）+ 編號管理、每個 actor 有 TTP、tooling、target sector 的 profile。Detection rule 不再只看 IoC（hash / IP）而是看 <em>actor 的 behavioral pattern</em>、IoC 變了也能抓。配對 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a> 的 nation-state actor lesson。</p>
<p><strong>Falcon LogScale 整合</strong>：Falcon LogScale（前 Humio）是 CrowdStrike 自家的 SIEM、可以把 Falcon agent telemetry + cloud log + 自家 app log 全收。跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 比、LogScale 強在 <em>跟 Falcon detection 同 plane</em>、計費也不是 ingestion-based；弱在 <em>detection content 跟 ecosystem 比 Splunk 淺</em>。</p>
<p><strong>Charlotte AI + LLM-assisted investigation</strong>：SOC analyst triage 時間長是普遍痛點、Charlotte AI 用 LLM 把自然語言問題翻成 Falcon query、補出 incident timeline summary。屬 SOC productivity 工具、不取代 detection rule、也不取代 analyst judgement。</p>
<p><strong>Falcon Identity Protection 補位</strong>：identity-layer threat detection（pass-the-hash、Kerberoasting、AD enumeration）、跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> ITDR 訊號互補。完整 stack 客戶可把 endpoint + cloud + identity 三層 telemetry 一起 correlate。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Agent rollout 一改全炸</strong>：sensor content channel 沒有 staging tenant、prod 直接吃 vendor push 的 update — 2024-07 incident 後 CrowdStrike 推 Sensor Update Policy 允許客戶設 canary ring、所有 prod 都該開、不開等於把 fleet 命交給 vendor QA</li>
<li><strong>Cloud workload coverage 不全 / 偵測盲點</strong>：只有部分 VM 部署 Falcon agent、container / Kubernetes 沒覆蓋 — 補 Falcon Container Sensor（DaemonSet）+ CSPM agentless 連 cloud account 補配置面</li>
<li><strong>Threat intel 沒接進 detection lifecycle</strong>：訂了 CrowdStrike Intelligence 但 SOC team 沒把 actor TTP 對應到自家 detection rule — 走 <a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a>、定期 review intel report + rule coverage gap</li>
<li><strong>CIEM finding 太多 / SOC 看不完</strong>：cloud IAM 累積 over-permission 沒清、CIEM 一掃幾千條 finding — 走 risk prioritization（哪些 identity 真的可達 sensitive resource）+ 跟 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Cloud IAM</a> ownership 對齊、不是 dump 給 SOC</li>
<li><strong>ASPM 拉不出 application graph</strong>：Bionic 需要 application telemetry + repo integration、只裝 Falcon agent 不會有 application architecture map — 補 ASPM 的 application onboarding（repo / CI / runtime telemetry）</li>
<li><strong>DSPM 找到 sensitive data 但沒 follow-up</strong>：DSPM 是 <em>posture 層</em>、發現問題後要走 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Classification</a> lifecycle、不是只把 finding 丟到 dashboard</li>
<li><strong>Vendor lock-in 過深、退場時 SOC 工作流崩潰</strong>：所有 detection content / IR playbook / Charlotte query / LogScale dashboard 都綁 Falcon — 關鍵 detection rule 同步 export 成 Sigma format（中性 format）、IR playbook 寫成 vendor-neutral 文件、不全押在 Falcon console</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloud-native / 無 endpoint workload</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a></td>
      </tr>
      <tr>
          <td>Palo Alto stack + compliance-heavy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a></td>
      </tr>
      <tr>
          <td>Behavioral baseline / anomaly 為主</td>
          <td>Lacework</td>
      </tr>
      <tr>
          <td>Runtime container syscall 深度偵測</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">Falco</a> / Cilium Tetragon</td>
      </tr>
      <tr>
          <td>DLP / sensitive data egress event</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>純 SIEM / log aggregation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>Incident response routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Falcon agent 內部 architecture（kernel module / sensor content channel 細節）</li>
<li>CrowdStrike Intelligence 完整 threat actor 名單與 TTP reference</li>
<li>Falcon LogScale 完整 SIEM 操作（屬獨立 SIEM 章節範圍、跟 Splunk 對照）</li>
<li>2024-07 Falcon update incident 的完整 root cause（屬 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a> 範圍）</li>
<li>Falcon Identity Protection 的 AD-specific detection rule（屬 identity-access 範圍）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Falcon Cloud Security 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>CrowdStrike 是 SolarWinds incident response 主導 vendor、Falcon endpoint + CrowdStrike Intelligence 整合在事件期間是強項、agent + threat intel 同 plane 的價值具象案例</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>CrowdStrike 公開 attribution（指向 LABYRINTH CHOLLIMA）與 detection、Falcon agent runtime 偵測異常 process spawn、signed binary 也要看 behavior</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Falcon agent runtime 偵測 JNDI lookup process tree、CrowdStrike Intelligence push IoC + TTP、漏洞披露 → fleet-wide detection deployment 是時間競賽</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>對照啟示：endpoint agent vendor 自己也是 supply chain target、2024-07 Falcon bad sensor update 全球 Windows BSOD 是這個 risk 的具體表現、agent-first 路線的長期信任成本</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.12 雲端控制面安全與 CNAPP</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a>、<a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a>、Lacework</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（SIEM 對照）、<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP 補位）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>（CIEM 訊號來源）、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（identity threat 對照）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（cloud finding → IR routing）、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.4 release gate</a>（ASPM finding → release gate）</li>
<li>官方：<a href="https://www.crowdstrike.com/platform/cloud-security/">CrowdStrike Falcon Cloud Security</a></li>
</ul>
]]></content:encoded></item><item><title>9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB + EKS 上的遊戲後端</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/</guid><description>&lt;p>這個案例的核心責任是說明「遊戲後端 KV」跟「廣告 KV」「電商 KV」的業務語意差異。遊戲後端的 KV 工作負載特性是：玩家狀態（角色、裝備、戰績）必須次秒讀寫、跨 region 同步、防作弊 — 這層需求跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 的「廣告量測」或 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a> 的「AR 玩家位置」都不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Capcom 在 AWS 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/capcom/">Capcom Case Study&lt;/a> 與 &lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers&lt;/a>）：&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>遊戲 IP&lt;/td>
 &lt;td>Resident Evil、Street Fighter、Monster Hunter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>後端請求量&lt;/td>
 &lt;td>billions of requests&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>響應時間&lt;/td>
 &lt;td>single-digit millisecond&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>營運成本下降&lt;/td>
 &lt;td>30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>Amazon DynamoDB + Amazon EKS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程資源再配置&lt;/td>
 &lt;td>從 DB 運維轉到遊戲品質與開發週期&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵敘述：「Capcom uses Amazon DynamoDB to meet this demand with single-digit millisecond response times」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Capcom 案例揭露三個遊戲後端 KV 的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>遊戲後端 KV = 跨遊戲共用基礎設施&lt;/strong>：Resident Evil / Street Fighter / Monster Hunter 是不同類型遊戲（單機+多人 / 對戰 / 合作打怪）、卻共用 &lt;em>同一套後端 KV&lt;/em>。這個共用降低了單一遊戲的維運成本、也讓新遊戲上線時不用重做基礎設施。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 multi-tenant platform。&lt;/li>
&lt;li>&lt;strong>single-digit ms response time = 玩家體感「即時」的底線&lt;/strong>：戰鬥動作、技能釋放、玩家對戰都要次秒級反應、超過 10ms 就「卡」。這個延遲門檻反推 Capcom 必須用 sub-region cache（ElastiCache / 本地 game server）+ DynamoDB DAX、不能單靠 DynamoDB。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase&lt;/a> 的延遲反推。&lt;/li>
&lt;li>&lt;strong>「工程資源從 DB 運維轉到遊戲品質」是 managed 服務的真實價值&lt;/strong>：Capcom 不是 IT 公司、是遊戲公司。把 DBA 時間從「Postgres patching、replication 設定、backup 排程」釋放到「遊戲機制設計、玩家行為分析」、才是 30% 成本下降的本質。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的人力成本工程化。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「billions of requests」沒指明時間單位（每秒、每天、每月）。讀案例時要找具體單位、不要直接套用到自家。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>遊戲後端 KV 用 DynamoDB / Cosmos DB / Bigtable&lt;/strong>：partition key 用 player_id 天然均勻、不會 hot partition。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 schema 設計。&lt;/li>
&lt;li>&lt;strong>EKS 跑 game server、不直接連 DynamoDB&lt;/strong>：game server 處理遊戲邏輯（戰鬥、配對、防作弊）、DynamoDB 處理持久狀態。中間用 DAX 或 ElastiCache 減少 DynamoDB 呼叫。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a>。&lt;/li>
&lt;li>&lt;strong>多 IP / 多遊戲共用平台是降本核心&lt;/strong>：每個新遊戲不重做基礎設施、共用同一套 DynamoDB + EKS。跟 &lt;a href="https://tarrragon.github.io/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&lt;/a> 的「single-tenant per game」對照 — 不同 IP 公司有不同取捨。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP Bigtable + GKE + Memorystore、Azure Cosmos DB + AKS + Cache for Redis 都可實作對等架構。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「遊戲後端 KV」跟「廣告 KV」「電商 KV」的業務語意差異。遊戲後端的 KV 工作負載特性是：玩家狀態（角色、裝備、戰績）必須次秒讀寫、跨 region 同步、防作弊 — 這層需求跟 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 的「廣告量測」或 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> 的「AR 玩家位置」都不同。</p>
<h2 id="觀察">觀察</h2>
<p>Capcom 在 AWS 的關鍵敘述（引自 <a href="https://aws.amazon.com/solutions/case-studies/capcom/">Capcom Case Study</a> 與 <a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>遊戲 IP</td>
          <td>Resident Evil、Street Fighter、Monster Hunter</td>
      </tr>
      <tr>
          <td>後端請求量</td>
          <td>billions of requests</td>
      </tr>
      <tr>
          <td>響應時間</td>
          <td>single-digit millisecond</td>
      </tr>
      <tr>
          <td>營運成本下降</td>
          <td>30%</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>Amazon DynamoDB + Amazon EKS</td>
      </tr>
      <tr>
          <td>工程資源再配置</td>
          <td>從 DB 運維轉到遊戲品質與開發週期</td>
      </tr>
  </tbody>
</table>
<p>關鍵敘述：「Capcom uses Amazon DynamoDB to meet this demand with single-digit millisecond response times」。</p>
<h2 id="判讀">判讀</h2>
<p>Capcom 案例揭露三個遊戲後端 KV 的工程重點。</p>
<ol>
<li><strong>遊戲後端 KV = 跨遊戲共用基礎設施</strong>：Resident Evil / Street Fighter / Monster Hunter 是不同類型遊戲（單機+多人 / 對戰 / 合作打怪）、卻共用 <em>同一套後端 KV</em>。這個共用降低了單一遊戲的維運成本、也讓新遊戲上線時不用重做基礎設施。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 multi-tenant platform。</li>
<li><strong>single-digit ms response time = 玩家體感「即時」的底線</strong>：戰鬥動作、技能釋放、玩家對戰都要次秒級反應、超過 10ms 就「卡」。這個延遲門檻反推 Capcom 必須用 sub-region cache（ElastiCache / 本地 game server）+ DynamoDB DAX、不能單靠 DynamoDB。對應 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> 的延遲反推。</li>
<li><strong>「工程資源從 DB 運維轉到遊戲品質」是 managed 服務的真實價值</strong>：Capcom 不是 IT 公司、是遊戲公司。把 DBA 時間從「Postgres patching、replication 設定、backup 排程」釋放到「遊戲機制設計、玩家行為分析」、才是 30% 成本下降的本質。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的人力成本工程化。</li>
</ol>
<p>需要警惕：「billions of requests」沒指明時間單位（每秒、每天、每月）。讀案例時要找具體單位、不要直接套用到自家。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>遊戲後端 KV 用 DynamoDB / Cosmos DB / Bigtable</strong>：partition key 用 player_id 天然均勻、不會 hot partition。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema 設計。</li>
<li><strong>EKS 跑 game server、不直接連 DynamoDB</strong>：game server 處理遊戲邏輯（戰鬥、配對、防作弊）、DynamoDB 處理持久狀態。中間用 DAX 或 ElastiCache 減少 DynamoDB 呼叫。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a>。</li>
<li><strong>多 IP / 多遊戲共用平台是降本核心</strong>：每個新遊戲不重做基礎設施、共用同一套 DynamoDB + EKS。跟 <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> 的「single-tenant per game」對照 — 不同 IP 公司有不同取捨。</li>
</ol>
<p>跨平台等效：GCP Bigtable + GKE + Memorystore、Azure Cosmos DB + AKS + Cache for Redis 都可實作對等架構。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他遊戲後端 → <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games EKS</a>（cluster 隔離 vs 共用）</li>
<li>想設計遊戲 KV → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></li>
<li>想理解 sub-ms latency 反推 → <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a></li>
<li>想規劃遊戲 KV access pattern 與 single-table design → <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a></li>
<li>想評估遊戲流量的 on-demand vs provisioned → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/capcom/">CAPCOM Case Study</a></li>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/big-data/powering-gaming-applications-with-amazon-dynamodb/">Powering Gaming Applications with Amazon DynamoDB</a></li>
</ul>
]]></content:encoded></item><item><title>4.19 Debuggability by Design</title><link>https://tarrragon.github.io/blog/backend/04-observability/debuggability-by-design/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/debuggability-by-design/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>debuggability by design 的責任：讓系統設計本身支援定位、重現與證據收集&lt;/li>
&lt;li>API 設計：request id、error code、idempotency key、semantic status&lt;/li>
&lt;li>async workflow：message id、correlation id、retry count、dead-letter reason&lt;/li>
&lt;li>dependency call：timeout、fallback、upstream response、circuit state&lt;/li>
&lt;li>error model：可分類錯誤、可追蹤錯誤鏈、可對應使用者影響&lt;/li>
&lt;li>診斷入口：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">diagnostic endpoint&lt;/a>、health check、probe&lt;/li>
&lt;li>跟語言教材的分工：語言處理 logger / error chain，04 處理跨服務診斷能力&lt;/li>
&lt;li>反模式：事後補 log；錯誤只回 500；async 任務缺 correlation id；依賴失敗無上下文&lt;/li>
&lt;/ul>
&lt;p>Debuggability by design 的核心是讓系統在設計時就暴露足夠上下文。事故時需要的資訊若沒有在 API、message、dependency call 與 error model 層留下來，後端平台再完整也只能收集到片段訊號。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Debuggability by design 是把可診斷性當成服務設計輸入的做法，責任是讓系統在出問題時自然留下定位所需的脈絡。&lt;/p>
&lt;p>這一頁處理的是設計前移。觀測工具只能收集系統吐出的訊號；如果 API、async workflow、dependency call 與 error model 沒有診斷欄位，事後補平台也只能看到破碎片段。&lt;/p>
&lt;p>這層與可觀測平台互補：平台負責收、存、查，設計負責產生可判讀語意。兩者任一缺失，都會讓事故定位時間呈倍數增加。&lt;/p>
&lt;h2 id="設計輸入">設計輸入&lt;/h2>
&lt;p>Debuggability by design 的設計輸入是「未來出問題時需要回答什麼問題」。系統設計時先列出這些問題，才能決定 API、message、dependency call 與 error model 要留下哪些欄位。&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>這次失敗影響哪個請求或用戶&lt;/td>
 &lt;td>request id、tenant、user journey&lt;/td>
 &lt;td>API、log schema、trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個 async 任務從哪裡來&lt;/td>
 &lt;td>correlation id、message id、causation id&lt;/td>
 &lt;td>queue、worker、event log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗來自本服務還是外部依賴&lt;/td>
 &lt;td>upstream name、timeout、response class&lt;/td>
 &lt;td>HTTP client、adapter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個錯誤能否重試或回放&lt;/td>
 &lt;td>retry count、idempotency key、DLQ reason&lt;/td>
 &lt;td>worker、consumer、DLQ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故時能否安全查系統狀態&lt;/td>
 &lt;td>diagnostic endpoint、probe、read-only view&lt;/td>
 &lt;td>admin / diagnostic surface&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Request id 與 trace id 的責任不同。request id 通常對應對外請求與支援查詢，trace id 對應跨服務路徑；兩者互相連結時，支援查詢與工程診斷都會有穩定入口。&lt;/p>
&lt;p>Correlation id 與 causation id 能讓 async workflow 保留因果。事件進入 queue、fan-out、retry、DLQ 或 replay 後，團隊需要知道它從哪個 request 或上游事件來，並且知道目前是哪一次處理嘗試。&lt;/p>
&lt;p>Diagnostic endpoint 的責任是提供低風險查詢入口。它是受權限、速率、遮罩與審計保護的操作面，讓 on-call 能查健康、依賴、queue、cache 或 feature flag 狀態。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 debuggability 時，先看關鍵流程是否保留 correlation，再看錯誤是否能路由到下一步。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>API request 是否有穩定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 與錯誤分類&lt;/li>
&lt;li>async message 是否有 correlation id、retry count 與 DLQ reason&lt;/li>
&lt;li>dependency call 是否記錄 upstream、timeout、fallback 與 response class&lt;/li>
&lt;li>error chain 是否能連到 trace、log 與 user impact&lt;/li>
&lt;li>diagnostic endpoint 是否能支援 on-call 的低風險查詢&lt;/li>
&lt;/ul>
&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>API&lt;/td>
 &lt;td>request id、error code、idempotency key&lt;/td>
 &lt;td>快速對齊請求與結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Async / Queue&lt;/td>
 &lt;td>message id、correlation id、retry reason&lt;/td>
 &lt;td>還原跨流程事件鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency&lt;/td>
 &lt;td>upstream、timeout、fallback state&lt;/td>
 &lt;td>分辨本地問題與外部依賴問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error Model&lt;/td>
 &lt;td>error class、context、impact hint&lt;/td>
 &lt;td>路由到正確處理流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="api-可診斷性">API 可診斷性&lt;/h2>
&lt;p>API 可診斷性的責任是讓每一次 request 都能被支援、工程與事故流程共同定位。API 不只回傳成功或失敗，也要留下足夠語意讓團隊知道錯在哪個層級。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>debuggability by design 的責任：讓系統設計本身支援定位、重現與證據收集</li>
<li>API 設計：request id、error code、idempotency key、semantic status</li>
<li>async workflow：message id、correlation id、retry count、dead-letter reason</li>
<li>dependency call：timeout、fallback、upstream response、circuit state</li>
<li>error model：可分類錯誤、可追蹤錯誤鏈、可對應使用者影響</li>
<li>診斷入口：<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">diagnostic endpoint</a>、health check、probe</li>
<li>跟語言教材的分工：語言處理 logger / error chain，04 處理跨服務診斷能力</li>
<li>反模式：事後補 log；錯誤只回 500；async 任務缺 correlation id；依賴失敗無上下文</li>
</ul>
<p>Debuggability by design 的核心是讓系統在設計時就暴露足夠上下文。事故時需要的資訊若沒有在 API、message、dependency call 與 error model 層留下來，後端平台再完整也只能收集到片段訊號。</p>
<h2 id="概念定位">概念定位</h2>
<p>Debuggability by design 是把可診斷性當成服務設計輸入的做法，責任是讓系統在出問題時自然留下定位所需的脈絡。</p>
<p>這一頁處理的是設計前移。觀測工具只能收集系統吐出的訊號；如果 API、async workflow、dependency call 與 error model 沒有診斷欄位，事後補平台也只能看到破碎片段。</p>
<p>這層與可觀測平台互補：平台負責收、存、查，設計負責產生可判讀語意。兩者任一缺失，都會讓事故定位時間呈倍數增加。</p>
<h2 id="設計輸入">設計輸入</h2>
<p>Debuggability by design 的設計輸入是「未來出問題時需要回答什麼問題」。系統設計時先列出這些問題，才能決定 API、message、dependency call 與 error model 要留下哪些欄位。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>需要的設計輸入</th>
          <th>常見位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這次失敗影響哪個請求或用戶</td>
          <td>request id、tenant、user journey</td>
          <td>API、log schema、trace</td>
      </tr>
      <tr>
          <td>這個 async 任務從哪裡來</td>
          <td>correlation id、message id、causation id</td>
          <td>queue、worker、event log</td>
      </tr>
      <tr>
          <td>失敗來自本服務還是外部依賴</td>
          <td>upstream name、timeout、response class</td>
          <td>HTTP client、adapter</td>
      </tr>
      <tr>
          <td>這個錯誤能否重試或回放</td>
          <td>retry count、idempotency key、DLQ reason</td>
          <td>worker、consumer、DLQ</td>
      </tr>
      <tr>
          <td>事故時能否安全查系統狀態</td>
          <td>diagnostic endpoint、probe、read-only view</td>
          <td>admin / diagnostic surface</td>
      </tr>
  </tbody>
</table>
<p>Request id 與 trace id 的責任不同。request id 通常對應對外請求與支援查詢，trace id 對應跨服務路徑；兩者互相連結時，支援查詢與工程診斷都會有穩定入口。</p>
<p>Correlation id 與 causation id 能讓 async workflow 保留因果。事件進入 queue、fan-out、retry、DLQ 或 replay 後，團隊需要知道它從哪個 request 或上游事件來，並且知道目前是哪一次處理嘗試。</p>
<p>Diagnostic endpoint 的責任是提供低風險查詢入口。它是受權限、速率、遮罩與審計保護的操作面，讓 on-call 能查健康、依賴、queue、cache 或 feature flag 狀態。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 debuggability 時，先看關鍵流程是否保留 correlation，再看錯誤是否能路由到下一步。</p>
<p>重點訊號包括：</p>
<ul>
<li>API request 是否有穩定 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 與錯誤分類</li>
<li>async message 是否有 correlation id、retry count 與 DLQ reason</li>
<li>dependency call 是否記錄 upstream、timeout、fallback 與 response class</li>
<li>error chain 是否能連到 trace、log 與 user impact</li>
<li>diagnostic endpoint 是否能支援 on-call 的低風險查詢</li>
</ul>
<table>
  <thead>
      <tr>
          <th>設計層</th>
          <th>最小可診斷欄位</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API</td>
          <td>request id、error code、idempotency key</td>
          <td>快速對齊請求與結果</td>
      </tr>
      <tr>
          <td>Async / Queue</td>
          <td>message id、correlation id、retry reason</td>
          <td>還原跨流程事件鏈</td>
      </tr>
      <tr>
          <td>Dependency</td>
          <td>upstream、timeout、fallback state</td>
          <td>分辨本地問題與外部依賴問題</td>
      </tr>
      <tr>
          <td>Error Model</td>
          <td>error class、context、impact hint</td>
          <td>路由到正確處理流程</td>
      </tr>
  </tbody>
</table>
<h2 id="api-可診斷性">API 可診斷性</h2>
<p>API 可診斷性的責任是讓每一次 request 都能被支援、工程與事故流程共同定位。API 不只回傳成功或失敗，也要留下足夠語意讓團隊知道錯在哪個層級。</p>
<table>
  <thead>
      <tr>
          <th>API 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Request ID</td>
          <td>對齊客訴、log、trace 與支援查詢</td>
          <td>從用戶回報回到後端事件</td>
      </tr>
      <tr>
          <td>Error code</td>
          <td>穩定分類錯誤語意</td>
          <td>分辨 validation、auth、quota</td>
      </tr>
      <tr>
          <td>Idempotency key</td>
          <td>保護重試與重播</td>
          <td>避免 recovery 時重複副作用</td>
      </tr>
      <tr>
          <td>Semantic status</td>
          <td>表達可重試、已接受、部分完成</td>
          <td>支援客戶端與後端一致處置</td>
      </tr>
      <tr>
          <td>Impact hint</td>
          <td>標示 user-facing 或 internal-only</td>
          <td>支援 severity 初判</td>
      </tr>
  </tbody>
</table>
<p>Request ID 是支援與工程之間的共同鑰匙。客戶只知道某次操作失敗，支援需要 request id 或可查詢等價欄位，才能把客訴轉成 incident intake evidence。</p>
<p>Error code 應該表達穩定語意，並保持內部實作封裝。<code>PAYMENT_PROVIDER_TIMEOUT</code>、<code>QUOTA_EXCEEDED</code>、<code>TOKEN_EXPIRED</code> 這類分類能支援路由；隨程式碼結構變動的錯誤字串則會讓查詢與客戶端處置不穩定。</p>
<p>Idempotency key 是 recovery 的診斷欄位。當 retry、rollback、replay 或補償流程啟動時，團隊需要知道哪些請求已被接受、哪些副作用已完成、哪些可以安全重送。</p>
<h2 id="async-workflow-可診斷性">Async Workflow 可診斷性</h2>
<p>Async workflow 可診斷性的責任是讓事件離開同步 request 後仍保留因果鏈。queue、worker、event handler 與 scheduled job 會把時間拉長、路徑拉開，欄位不足時最容易形成診斷斷點。</p>
<table>
  <thead>
      <tr>
          <th>Async 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Message ID</td>
          <td>標識單一訊息</td>
          <td>查詢 delivery、ack、redelivery</td>
      </tr>
      <tr>
          <td>Correlation ID</td>
          <td>串回原始 request 或 workflow</td>
          <td>還原跨流程事件鏈</td>
      </tr>
      <tr>
          <td>Retry count</td>
          <td>記錄處理嘗試次數</td>
          <td>分辨 transient 與 poison case</td>
      </tr>
      <tr>
          <td>DLQ reason</td>
          <td>記錄進入 dead-letter queue 原因</td>
          <td>支援 replay 與修復排序</td>
      </tr>
      <tr>
          <td>Consumer version</td>
          <td>標示處理程式版本</td>
          <td>追查 rollout 或 schema 相容性</td>
      </tr>
  </tbody>
</table>
<p>Message ID 讓團隊能看見單一訊息的生命週期。它應該能串到 publish、broker delivery、consumer ack、redelivery、DLQ 與 replay。</p>
<p>Correlation ID 讓 async 任務保留業務脈絡。缺少 correlation id 時，DLQ dashboard 只能顯示失敗數量，tenant、request 與 user journey 影響範圍會留在人工追查階段。</p>
<p>Retry count 與 DLQ reason 讓回復路徑可排序。高 retry count 可能代表下游依賴失效，也可能代表 poison message；兩者需要不同處置。</p>
<h2 id="dependency-call-可診斷性">Dependency Call 可診斷性</h2>
<p>Dependency call 可診斷性的責任是讓團隊分辨本地問題、下游問題與保護機制啟動。每一次外部依賴呼叫都應留下足夠上下文，支援等待、降級、切換或升級 vendor incident 的判斷。</p>
<table>
  <thead>
      <tr>
          <th>Dependency 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Upstream name</td>
          <td>穩定標示依賴服務</td>
          <td>分辨哪個下游失效</td>
      </tr>
      <tr>
          <td>Deadline</td>
          <td>標示呼叫預算</td>
          <td>判斷 timeout 設計是否合理</td>
      </tr>
      <tr>
          <td>Response class</td>
          <td>聚合成功、4xx、5xx、timeout</td>
          <td>支援 error rate 與 vendor triage</td>
      </tr>
      <tr>
          <td>Fallback state</td>
          <td>記錄是否進入降級</td>
          <td>判斷用戶影響是否被吸收</td>
      </tr>
      <tr>
          <td>Circuit state</td>
          <td>記錄 circuit breaker 狀態</td>
          <td>分辨保護機制或真實恢復</td>
      </tr>
  </tbody>
</table>
<p>Upstream name 需要是穩定維度。若每個 adapter 使用不同名稱，dashboard 與 trace 很難把同一個供應商或內部依賴聚合在一起。</p>
<p>Deadline 是 dependency call 的診斷欄位。timeout 發生時，團隊需要知道是下游慢、呼叫預算過短、queue backlog 導致開始太晚，還是 retry policy 放大壓力。</p>
<p>Fallback state 讓事故團隊知道保護是否生效。服務錯誤率可能沒上升，是因為 fallback 吸收了下游失敗；若沒有 fallback 訊號，團隊會低估風險。</p>
<h2 id="error-model-可診斷性">Error Model 可診斷性</h2>
<p>Error model 可診斷性的責任是把錯誤轉成可分類、可路由、可復盤的語意。錯誤不只服務於程式控制流，也服務於事故判讀與使用者影響評估。</p>
<table>
  <thead>
      <tr>
          <th>錯誤層級</th>
          <th>設計責任</th>
          <th>路由方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Validation error</td>
          <td>輸入不符合契約</td>
          <td>API contract / client 修正</td>
      </tr>
      <tr>
          <td>Authorization error</td>
          <td>身分或權限不足</td>
          <td>IAM / security triage</td>
      </tr>
      <tr>
          <td>Dependency error</td>
          <td>外部依賴回應失敗或超時</td>
          <td>vendor / downstream triage</td>
      </tr>
      <tr>
          <td>Capacity error</td>
          <td>資源、queue 或 quota 不足</td>
          <td>capacity / load shedding</td>
      </tr>
      <tr>
          <td>Data consistency error</td>
          <td>寫入、讀取或 migration 不一致</td>
          <td>reliability / migration gate</td>
      </tr>
  </tbody>
</table>
<p>錯誤分類應該讓下一步明確。<code>internal error</code> 適合作為最後防線；主要分類需要支援 on-call 判斷是重試、降級、rollback、升級資安，還是進入資料修復。</p>
<p>Error chain 需要保留上下文。過度包裝錯誤會讓原始 dependency、timeout、request id 或 schema version 消失；完全不包裝則會把底層細節直接丟給外部使用者。好的 error model 會分開內部診斷語意與外部穩定契約。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時只能看到「500」，需要重跑才能定位原因</li>
<li>queue message 進 DLQ 後缺少原始 request 脈絡</li>
<li>外部 API timeout 無 upstream 名稱、耗時與 fallback 狀態</li>
<li>錯誤被包裝後 trace 與 error chain 斷裂</li>
<li>health check 顯示 healthy，但核心旅程已經失效</li>
</ul>
<p>典型情境是 queue 任務在三次重試後進 DLQ，但缺少 request 與 tenant 脈絡。工程師可以看到「失敗很多」，後續需要先補「誰受影響、哪個流程壞、該先修哪一段」的判讀資訊。這就是設計期缺欄位造成的診斷斷點。</p>
<h2 id="控制面">控制面</h2>
<p>Debuggability by design 的控制面是把診斷欄位納入設計審查與契約驗證。可診斷性若只靠事後補 log，會在每次新 API、新 workflow 或新 dependency 上重複遺漏。</p>
<ol>
<li>在 API design review 中檢查 request id、error code、idempotency 與 impact hint。</li>
<li>在 async workflow review 中檢查 message id、correlation、retry 與 DLQ reason。</li>
<li>在 dependency review 中檢查 timeout、deadline、fallback 與 upstream naming。</li>
<li>在 error model review 中檢查分類、內外部語意與 error chain。</li>
<li>在 contract testing 中驗證關鍵診斷欄位與錯誤語意。</li>
</ol>
<p>設計審查需要明確區分必填欄位與情境欄位。request id、trace context、error class 與 owner 通常是跨服務必填；idempotency key、DLQ reason、circuit state 則依 workflow 與依賴類型決定。</p>
<p>Contract testing 可以保護可診斷性。若 API 或 event schema 調整後移除了 correlation id、error code 或 retry metadata，測試應該阻擋這類破壞，因為它會讓事故判讀退回人工拼接。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Debuggability by design 的反模式是把診斷能力推遲到事故後。事故後補 log 可以修下一次，已發生事件的證據缺口則會留在復盤限制中。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事後補 log</td>
          <td>每次事故才知道缺哪個欄位</td>
          <td>設計審查納入診斷欄位</td>
      </tr>
      <tr>
          <td>錯誤只回 500</td>
          <td>客戶、支援與 on-call 缺少分類</td>
          <td>建立穩定 error code 與 error class</td>
      </tr>
      <tr>
          <td>Async 缺 correlation</td>
          <td>DLQ 只有失敗數量，無業務脈絡</td>
          <td>message schema 保留因果欄位</td>
      </tr>
      <tr>
          <td>Dependency 黑箱</td>
          <td>timeout 只顯示本地錯誤</td>
          <td>adapter 統一 upstream 與 response class</td>
      </tr>
      <tr>
          <td>Diagnostic endpoint 無治理</td>
          <td>查詢有用但風險過高或無審計</td>
          <td>權限、遮罩、速率與 audit log</td>
      </tr>
  </tbody>
</table>
<p>事後補 log 的代價是已發生事故會留下復盤缺口。若缺少原始 request、tenant、message 或 dependency 欄位，工程師只能用間接推論重建時間線。</p>
<p>錯誤只回 500 會把所有問題導向同一條路由。validation、authorization、dependency、capacity 與 data consistency 的處置完全不同，錯誤模型應該支援這些分流。</p>
<p>Diagnostic endpoint 無治理會把可診斷性變成資安風險。診斷入口需要最小權限、資料遮罩、速率限制與 audit log，並且只提供事故判讀需要的 read-only 資訊。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>Debuggability by design 位在 Backend 服務設計層。語言教材負責如何在特定 runtime 中傳遞 context、包裝 error、實作 middleware、處理 async local storage 或 goroutine context；本章負責定義跨語言都需要保留的診斷語意。</p>
<p>同步 runtime 的重點是 thread-local、connection pool 與 blocking dependency call 是否能保留 request context。async runtime 的重點是 task、promise、callback 與 queue boundary 是否能保留 trace context。goroutine 或 lightweight task runtime 的重點是廉價並發是否放大下游壓力，並且是否保留 deadline 與 cancellation。</p>
<p>不同語言可以用不同實作方式，但 API、async workflow、dependency call 與 error model 的診斷責任相同。這也是 Backend 章節保留跨語言抽象的理由。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.1 log schema：定義診斷欄位</li>
<li>04.3 tracing：保留跨服務 context</li>
<li>04.11 telemetry pipeline：確保診斷訊號能被採集</li>
<li>06.10 contract testing：把錯誤模型與外部契約納入驗證</li>
<li>08.18 incident intake：把設計期留下的診斷欄位轉成 evidence</li>
</ul>
]]></content:encoded></item><item><title>6.19 Reliability Readiness Review</title><link>https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>reliability readiness 的責任：確認服務能承受預期流量、依賴失效、資料變更與回復壓力&lt;/li>
&lt;li>檢查面向：SLO、capacity、dependency、rollback、data migration、on-call、runbook&lt;/li>
&lt;li>上線前門檻：核心路徑有 SLI、load test、rollback path、owner 與 alert&lt;/li>
&lt;li>重大變更門檻：migration、feature flag、dependency change、config rollout 的風險判讀&lt;/li>
&lt;li>高風險操作門檻：手動修資料、批次任務、backfill、區域切換&lt;/li>
&lt;li>跟 04 的交接：缺少訊號時回到 observability readiness&lt;/li>
&lt;li>跟 08 的交接：缺少事故節奏時回到 drills / runbook lifecycle&lt;/li>
&lt;li>反模式：release gate 只看 CI 綠燈；沒有 rollback rehearsal；容量假設沒有驗證&lt;/li>
&lt;/ul>
&lt;p>Reliability readiness review 的核心價值是把「上線前風險」前移成可討論的工程語言。只靠測試通過不代表服務可在真實流量與依賴波動下維持穩定，readiness 讓團隊在變更前先明確回答容量、回復、資料與值班四個問題。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Reliability readiness review 是把可靠性準備度轉成可檢查門檻的流程，責任是在服務承受 production 壓力前先找出可預期失效。&lt;/p>
&lt;p>這一頁處理的是準備度。readiness 要把訊號、容量、依賴、回復、資料與值班能力放在同一張檢查表中判讀。&lt;/p>
&lt;p>readiness 的目標是提高發布品質。當缺口被提前看見，團隊可以選擇補驗證、縮小範圍、延後發布或先加保護措施，避免把不確定性直接帶進 production。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 reliability readiness 時，先看服務的核心失敗模式是否已被驗證，再看回復路徑是否可執行。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>核心 user journey 是否有 SLO、load baseline 與 alert&lt;/li>
&lt;li>主要 dependency 是否有 timeout、fallback 與 degradation plan&lt;/li>
&lt;li>rollback / failover 是否有演練紀錄&lt;/li>
&lt;li>migration / backfill 是否有停止條件與資料校驗&lt;/li>
&lt;li>on-call 是否有 runbook、owner 與 escalation policy&lt;/li>
&lt;/ul>
&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>服務健康&lt;/td>
 &lt;td>核心旅程有 SLO 與 alert&lt;/td>
 &lt;td>只看系統資源，忽略用戶結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量邊界&lt;/td>
 &lt;td>有 load baseline 與容量餘裕&lt;/td>
 &lt;td>流量上升時才發現瓶頸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回復路徑&lt;/td>
 &lt;td>rollback / failover 有演練紀錄&lt;/td>
 &lt;td>事故現場才第一次走流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料操作&lt;/td>
 &lt;td>migration 有校驗與停止條件&lt;/td>
 &lt;td>補資料操作擴大影響面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>值班準備&lt;/td>
 &lt;td>on-call 有 runbook 與 escalation&lt;/td>
 &lt;td>事故當下才建立協作節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="readiness-範圍">Readiness 範圍&lt;/h2>
&lt;p>Reliability readiness review 的範圍是服務進入 production 壓力前需要具備的最低可靠性條件。它不取代 CI、load test、release gate 或 incident drill，而是把這些控制面接成同一個放行判斷。&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>服務健康&lt;/td>
 &lt;td>核心旅程是否有可靠性目標&lt;/td>
 &lt;td>SLO、SLI、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量&lt;/td>
 &lt;td>預期流量與尖峰是否被驗證&lt;/td>
 &lt;td>load test、capacity model&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>依賴&lt;/td>
 &lt;td>下游失效是否有 timeout 與降級&lt;/td>
 &lt;td>dependency budget、fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料&lt;/td>
 &lt;td>migration、backfill 是否可校驗&lt;/td>
 &lt;td>migration safety、test data&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回復&lt;/td>
 &lt;td>rollback、failover 是否可執行&lt;/td>
 &lt;td>DR rehearsal、rollback rehearsal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作&lt;/td>
 &lt;td>on-call 是否知道如何接住事故&lt;/td>
 &lt;td>runbook、escalation、drill&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務健康是 readiness 的第一層。核心 user journey 需要有 SLO、dashboard、alert 與 owner，讓團隊知道「服務是否仍在承諾範圍內」。&lt;/p>
&lt;p>容量是 readiness 的第二層。load baseline、throughput ceiling、queue lag、dependency saturation 與 cost threshold 都需要在上線前被看見，避免第一個尖峰才揭露瓶頸。&lt;/p>
&lt;p>依賴是 readiness 的第三層。每個關鍵 downstream 都需要 timeout、deadline、retry、fallback、circuit breaker 或 degradation plan，讓局部失效維持在可控範圍。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>reliability readiness 的責任：確認服務能承受預期流量、依賴失效、資料變更與回復壓力</li>
<li>檢查面向：SLO、capacity、dependency、rollback、data migration、on-call、runbook</li>
<li>上線前門檻：核心路徑有 SLI、load test、rollback path、owner 與 alert</li>
<li>重大變更門檻：migration、feature flag、dependency change、config rollout 的風險判讀</li>
<li>高風險操作門檻：手動修資料、批次任務、backfill、區域切換</li>
<li>跟 04 的交接：缺少訊號時回到 observability readiness</li>
<li>跟 08 的交接：缺少事故節奏時回到 drills / runbook lifecycle</li>
<li>反模式：release gate 只看 CI 綠燈；沒有 rollback rehearsal；容量假設沒有驗證</li>
</ul>
<p>Reliability readiness review 的核心價值是把「上線前風險」前移成可討論的工程語言。只靠測試通過不代表服務可在真實流量與依賴波動下維持穩定，readiness 讓團隊在變更前先明確回答容量、回復、資料與值班四個問題。</p>
<h2 id="概念定位">概念定位</h2>
<p>Reliability readiness review 是把可靠性準備度轉成可檢查門檻的流程，責任是在服務承受 production 壓力前先找出可預期失效。</p>
<p>這一頁處理的是準備度。readiness 要把訊號、容量、依賴、回復、資料與值班能力放在同一張檢查表中判讀。</p>
<p>readiness 的目標是提高發布品質。當缺口被提前看見，團隊可以選擇補驗證、縮小範圍、延後發布或先加保護措施，避免把不確定性直接帶進 production。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 reliability readiness 時，先看服務的核心失敗模式是否已被驗證，再看回復路徑是否可執行。</p>
<p>重點訊號包括：</p>
<ul>
<li>核心 user journey 是否有 SLO、load baseline 與 alert</li>
<li>主要 dependency 是否有 timeout、fallback 與 degradation plan</li>
<li>rollback / failover 是否有演練紀錄</li>
<li>migration / backfill 是否有停止條件與資料校驗</li>
<li>on-call 是否有 runbook、owner 與 escalation policy</li>
</ul>
<table>
  <thead>
      <tr>
          <th>檢查面向</th>
          <th>最小可用判準</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務健康</td>
          <td>核心旅程有 SLO 與 alert</td>
          <td>只看系統資源，忽略用戶結果</td>
      </tr>
      <tr>
          <td>容量邊界</td>
          <td>有 load baseline 與容量餘裕</td>
          <td>流量上升時才發現瓶頸</td>
      </tr>
      <tr>
          <td>回復路徑</td>
          <td>rollback / failover 有演練紀錄</td>
          <td>事故現場才第一次走流程</td>
      </tr>
      <tr>
          <td>資料操作</td>
          <td>migration 有校驗與停止條件</td>
          <td>補資料操作擴大影響面</td>
      </tr>
      <tr>
          <td>值班準備</td>
          <td>on-call 有 runbook 與 escalation</td>
          <td>事故當下才建立協作節奏</td>
      </tr>
  </tbody>
</table>
<h2 id="readiness-範圍">Readiness 範圍</h2>
<p>Reliability readiness review 的範圍是服務進入 production 壓力前需要具備的最低可靠性條件。它不取代 CI、load test、release gate 或 incident drill，而是把這些控制面接成同一個放行判斷。</p>
<table>
  <thead>
      <tr>
          <th>範圍</th>
          <th>核心問題</th>
          <th>對應控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務健康</td>
          <td>核心旅程是否有可靠性目標</td>
          <td>SLO、SLI、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a></td>
      </tr>
      <tr>
          <td>容量</td>
          <td>預期流量與尖峰是否被驗證</td>
          <td>load test、capacity model</td>
      </tr>
      <tr>
          <td>依賴</td>
          <td>下游失效是否有 timeout 與降級</td>
          <td>dependency budget、fallback</td>
      </tr>
      <tr>
          <td>資料</td>
          <td>migration、backfill 是否可校驗</td>
          <td>migration safety、test data</td>
      </tr>
      <tr>
          <td>回復</td>
          <td>rollback、failover 是否可執行</td>
          <td>DR rehearsal、rollback rehearsal</td>
      </tr>
      <tr>
          <td>操作</td>
          <td>on-call 是否知道如何接住事故</td>
          <td>runbook、escalation、drill</td>
      </tr>
  </tbody>
</table>
<p>服務健康是 readiness 的第一層。核心 user journey 需要有 SLO、dashboard、alert 與 owner，讓團隊知道「服務是否仍在承諾範圍內」。</p>
<p>容量是 readiness 的第二層。load baseline、throughput ceiling、queue lag、dependency saturation 與 cost threshold 都需要在上線前被看見，避免第一個尖峰才揭露瓶頸。</p>
<p>依賴是 readiness 的第三層。每個關鍵 downstream 都需要 timeout、deadline、retry、fallback、circuit breaker 或 degradation plan，讓局部失效維持在可控範圍。</p>
<p>資料是 readiness 的第四層。schema migration、backfill、online migration 與資料修復需要校驗、停止條件、rollback 或補償流程，讓資料風險能被事前判讀。</p>
<p>操作是 readiness 的最後一層。runbook、owner、escalation policy、incident intake 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a> 讓服務在失效時能被團隊接住。</p>
<h2 id="review-流程">Review 流程</h2>
<p>Reliability readiness review 的流程是從風險清單走向放行判斷。每個缺口都要被分類為阻擋、降級接受或後續改善，讓發布決策有清楚路由。</p>
<ol>
<li>定義本次上線或變更的服務承諾。</li>
<li>列出核心 failure mode、dependency、資料操作與回復路徑。</li>
<li>檢查 04 訊號是否足以支援判讀。</li>
<li>檢查 06 驗證是否足以支援放行。</li>
<li>檢查 08 值班與事故流程是否能接住失效。</li>
<li>對每個缺口指定 owner、處理路由與重新評估條件。</li>
</ol>
<p>服務承諾是 readiness review 的錨點。若本次變更影響 checkout、payment、message delivery 或 tenant migration，review 就要圍繞這些旅程的可靠性承諾，並把程式碼合併狀態視為其中一個輸入。</p>
<p>Failure mode 清單需要具體。依賴 timeout、queue lag、cache stampede、migration lock、feature flag misrouting、region failover 與 data reconciliation 都是不同失效模式，對應不同驗證與回復路由。</p>
<p>04 訊號是 readiness 的前提。若缺少 SLI、trace、log correlation 或 telemetry data quality，可靠性 review 只能停在推測；這類缺口應先回到 04.16 與 04.17。</p>
<p>08 流程是 readiness 的接手面。若 on-call 沒有 runbook、incident commander 不清楚啟動條件、status update 沒有節奏，可靠性缺口會在事故時轉成協作壓力。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>上線前只看 unit / integration test，沒有容量與回復判準</li>
<li>依賴失效時只能現場討論 fallback</li>
<li>migration 執行前沒有 rollback rehearsal</li>
<li>服務 owner 需要臨場補 RTO / RPO 或核心 SLO</li>
<li>on-call 第一次接觸 runbook 是事故當下</li>
</ul>
<p>典型情境是服務通過 CI 與 integration test 就上線，結果在流量尖峰時 dependency timeout 連鎖放大。若前一輪 readiness 已要求 load baseline、fallback 驗證與 rollback rehearsal，這類事故通常會降級成可控風險，維持在局部範圍。</p>
<h2 id="放行判斷">放行判斷</h2>
<p>Reliability readiness 的放行判斷需要區分「阻擋上線」與「帶限制上線」。這個區分讓團隊既能控制風險，也能在低風險缺口存在時保持交付節奏。</p>
<table>
  <thead>
      <tr>
          <th>結果</th>
          <th>判斷條件</th>
          <th>常見動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pass</td>
          <td>核心路徑、容量、回復與值班皆達標</td>
          <td>正常進入 release gate</td>
      </tr>
      <tr>
          <td>Conditional pass</td>
          <td>缺口可被降級、人工查證或短期 runbook 承接</td>
          <td>記錄限制、owner 與補齊期限</td>
      </tr>
      <tr>
          <td>Block</td>
          <td>核心旅程、資料或回復路徑缺少判讀</td>
          <td>暫停發布，補驗證或縮小範圍</td>
      </tr>
      <tr>
          <td>Defer</td>
          <td>需求價值低於可靠性風險</td>
          <td>延後變更，先處理 reliability debt</td>
      </tr>
  </tbody>
</table>
<p>Pass 代表核心風險已有證據支撐。這不代表系統完美，而是代表本次發布或操作有足夠訊號、驗證與回復路由。</p>
<p>Conditional pass 適合處理可控缺口。例如某個低風險 batch job 缺少完整 trace，但已有 log query、manual replay 與 on-call owner，可以帶著明確限制上線。</p>
<p>Block 適合處理核心旅程與資料風險。payment migration 缺少 rollback rehearsal、tenant backfill 缺少校驗、核心 API 缺少 SLO alert，這些缺口會讓事故處理沒有可靠入口。</p>
<p>Defer 適合處理價值與風險不對稱的變更。若本次變更只是次要優化，但會暴露高風險 migration 或 dependency change，延後是合理的 reliability decision。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Reliability readiness 的反模式通常來自把測試通過視為 production 準備度。測試通過證明某些功能路徑可執行，readiness 則要證明服務能在真實壓力、依賴波動與事故流程下被接住。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI 綠燈即上線</td>
          <td>只看 test pass</td>
          <td>加入 SLO、capacity、rollback 判準</td>
      </tr>
      <tr>
          <td>容量假設無驗證</td>
          <td>靠估算決定尖峰承載</td>
          <td>補 load baseline 與容量餘裕</td>
      </tr>
      <tr>
          <td>Rollback 只寫文件</td>
          <td>回復流程沒有演練紀錄</td>
          <td>補 rollback rehearsal</td>
      </tr>
      <tr>
          <td>Migration 缺停止條件</td>
          <td>執行中才判斷是否暫停</td>
          <td>事前定義校驗、pause、fallback</td>
      </tr>
      <tr>
          <td>On-call 臨場接手</td>
          <td>事故時才找 owner 與 runbook</td>
          <td>補 drill 與 escalation route</td>
      </tr>
  </tbody>
</table>
<p>CI 綠燈即上線會讓可靠性停在程式正確性層。production 可靠性還包含容量、依賴、資料、回復與協作，這些條件需要各自的證據。</p>
<p>Rollback 只寫文件會在事故現場暴露落差。回復流程需要在類 production 條件下演練過，才能知道權限、資料、流量、相容性與通訊是否接得上。</p>
<h2 id="產業情境醫療系統">產業情境：醫療系統</h2>
<p>醫療系統上線前的 readiness review 需要額外的合規維度。可靠性準備度跟醫療法規準備度是同一個放行判斷的兩個面向，缺任何一個都應 block。</p>
<p>Readiness checklist 需要包含合規項目：PHI（受保護健康資訊）加密狀態、存取控制驗證、audit trail 完整性、backup encryption 驗證。這些項目跟可靠性項目（SLO、load baseline、rollback path）平行檢查，合規缺口的阻擋權重跟核心旅程缺口相同。</p>
<p>合規驗證跟可靠性驗證有時存在張力。為了 HIPAA compliance 加密所有 backup 會增加 restore 時間，<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> 可能不符合臨床需求。為了最小資料揭露限制 staging 資料量會降低環境 parity。這類 trade-off 需要在 readiness review 中明確記錄，包含選擇理由與風險接受者。</p>
<p>醫療系統的 readiness review 需要臨床代表參與。技術 readiness 回答的是「系統能否穩定運作」，臨床 readiness 回答的是「臨床 workflow 能否安全繼續」。EMR 升級後的畫面配置變更、醫囑流程的步驟調整、報告格式的差異，這些在技術指標上可能正常，但在臨床操作上可能造成用藥錯誤或判讀延遲。</p>
<p>高風險變更（EMR 升級、PACS 遷移、醫囑系統切換）需要 go-live support window。變更後的前 24-72 小時維持加強值班，因為臨床問題的反饋延遲通常比技術指標長 — 護理站的操作異常可能在換班時才被回報，藥局的處方錯誤可能在調劑時才被發現。support window 的長度由臨床回饋延遲決定，技術團隊單獨設定容易低估。</p>
<h2 id="與-release-gate-的關係">與 Release Gate 的關係</h2>
<p>Reliability readiness review 是 release gate 的上游資料。readiness 負責整理風險與證據，release gate 負責根據政策做放行、暫停、縮小範圍或例外核准。</p>
<p>Readiness 結果應包含三種資訊：已驗證條件、已接受限制與阻擋缺口。Release gate 只看「通過 / 失敗」會遺失判讀脈絡；保留這三類資訊才能讓發布決策可復盤。</p>
<p>Readiness 也應回寫 reliability debt。每次 conditional pass 都代表團隊暫時接受一個缺口；若缺口反覆被接受，就應進入 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a>。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.16 observability readiness：確認訊號可支援 readiness 判讀</li>
<li>06.2 load test：補容量與吞吐驗證</li>
<li>06.7 DR / rollback rehearsal：補回復路徑演練</li>
<li>06.8 release gate：把 readiness 結果變成放行條件</li>
<li>08.6 drills / on-call readiness：補值班與事故演練</li>
</ul>
]]></content:encoded></item><item><title>8.19 Incident Decision Log</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>decision log 的責任：保留事故期間的關鍵假設、決策、證據與責任人&lt;/li>
&lt;li>欄位：timestamp、decision、context、evidence、owner、expected effect、rollback condition&lt;/li>
&lt;li>決策類型：severity change、containment、rollback、degradation、customer communication、vendor escalation&lt;/li>
&lt;li>evidence 連結：dashboard、log query、trace、status page、customer report、audit log&lt;/li>
&lt;li>事中使用：支援 handoff、multi-incident coordination、stakeholder update&lt;/li>
&lt;li>事後使用：支援 post-incident review、action item、runbook update&lt;/li>
&lt;li>跟 scribe 的關係：scribe 記錄事實，decision log 強調決策與證據鏈&lt;/li>
&lt;li>反模式：Slack 討論就是紀錄；事後才補決策理由；rollback 條件沒寫清楚&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">Incident decision log&lt;/a> 的核心價值是讓事故決策可回放。事故現場的關鍵是每次都能說清楚「為何這樣選、基於什麼證據、何時該回退」。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Incident decision log 是事故期間的決策紀錄，責任是讓團隊能回看當時基於哪些證據做了哪些取捨。&lt;/p>
&lt;p>這一頁處理的是事中決策可追溯性。事故期間的資訊通常不完整；decision log 的責任是保留每個決策的時間、證據、owner 與回退條件。&lt;/p>
&lt;p>decision log 也是交班工具。當事故跨班次或跨時區，新的 IC 只要接上決策序列與證據鏈，就能在幾分鐘內接手，而不需要重建整段背景。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 decision log 時，先看決策是否有 evidence，再看決策是否有預期效果與回退條件。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>severity 變更是否留下理由與 impact scope&lt;/li>
&lt;li>containment / rollback 是否有 owner 與 rollback condition&lt;/li>
&lt;li>customer communication 是否連到當時已知事實&lt;/li>
&lt;li>handoff 是否能靠 decision log 接上脈絡&lt;/li>
&lt;li>post-incident review 是否能直接引用決策紀錄&lt;/li>
&lt;/ul>
&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>Decision / Time&lt;/td>
 &lt;td>有清楚決策內容與時間&lt;/td>
 &lt;td>建立決策先後與節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Context / Evidence&lt;/td>
 &lt;td>有對應證據與限制&lt;/td>
 &lt;td>避免事後合理化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>有責任人可追蹤&lt;/td>
 &lt;td>提升執行一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expected Effect&lt;/td>
 &lt;td>有預期影響描述&lt;/td>
 &lt;td>判斷決策是否有效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback Condition&lt;/td>
 &lt;td>有回退門檻&lt;/td>
 &lt;td>控制次生風險&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="欄位模型">欄位模型&lt;/h2>
&lt;p>Incident decision log 的欄位模型要同時支援事中交班與事後復盤。欄位過少會失去證據鏈，欄位過多會讓事故現場寫不下去。&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>Timestamp&lt;/td>
 &lt;td>記錄決策時間&lt;/td>
 &lt;td>2026-05-02T10:15Z&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision&lt;/td>
 &lt;td>寫清楚採取或暫緩的動作&lt;/td>
 &lt;td>rollback API v42&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Context&lt;/td>
 &lt;td>說明當時問題與限制&lt;/td>
 &lt;td>p95 latency 超 SLO，trace sample 低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence&lt;/td>
 &lt;td>連到 dashboard、query、ticket&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> chart、support case&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>指定執行或追蹤責任人&lt;/td>
 &lt;td>IC、service owner、comms lead&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expected effect&lt;/td>
 &lt;td>說明預期改善或風險&lt;/td>
 &lt;td>10 分鐘內 error rate 下降&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">Rollback condition&lt;/a>&lt;/td>
 &lt;td>說明何時回退這個決策&lt;/td>
 &lt;td>queue lag 超門檻即停止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Follow-up&lt;/td>
 &lt;td>標記後續查證或復盤項目&lt;/td>
 &lt;td>補 runbook、補 alert&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Timestamp 要使用一致時間基準。事故跨工具、跨時區、跨 vendor 時，decision log 應保留標準化時間，必要時也保留來源原始時間。&lt;/p>
&lt;p>Decision 欄位要寫具體動作。&lt;code>處理中&lt;/code>、&lt;code>觀察一下&lt;/code> 這類描述難以支援復盤；&lt;code>rollback API v42&lt;/code>、&lt;code>disable feature flag checkout_new_route&lt;/code>、&lt;code>escalate to vendor support&lt;/code> 才能回放。&lt;/p>
&lt;p>Context 欄位要保留限制。事故期間的資料常有缺口，decision log 應寫出 evidence 的 completeness、freshness、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a> 與已知盲區。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>decision log 的責任：保留事故期間的關鍵假設、決策、證據與責任人</li>
<li>欄位：timestamp、decision、context、evidence、owner、expected effect、rollback condition</li>
<li>決策類型：severity change、containment、rollback、degradation、customer communication、vendor escalation</li>
<li>evidence 連結：dashboard、log query、trace、status page、customer report、audit log</li>
<li>事中使用：支援 handoff、multi-incident coordination、stakeholder update</li>
<li>事後使用：支援 post-incident review、action item、runbook update</li>
<li>跟 scribe 的關係：scribe 記錄事實，decision log 強調決策與證據鏈</li>
<li>反模式：Slack 討論就是紀錄；事後才補決策理由；rollback 條件沒寫清楚</li>
</ul>
<p><a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">Incident decision log</a> 的核心價值是讓事故決策可回放。事故現場的關鍵是每次都能說清楚「為何這樣選、基於什麼證據、何時該回退」。</p>
<h2 id="概念定位">概念定位</h2>
<p>Incident decision log 是事故期間的決策紀錄，責任是讓團隊能回看當時基於哪些證據做了哪些取捨。</p>
<p>這一頁處理的是事中決策可追溯性。事故期間的資訊通常不完整；decision log 的責任是保留每個決策的時間、證據、owner 與回退條件。</p>
<p>decision log 也是交班工具。當事故跨班次或跨時區，新的 IC 只要接上決策序列與證據鏈，就能在幾分鐘內接手，而不需要重建整段背景。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 decision log 時，先看決策是否有 evidence，再看決策是否有預期效果與回退條件。</p>
<p>重點訊號包括：</p>
<ul>
<li>severity 變更是否留下理由與 impact scope</li>
<li>containment / rollback 是否有 owner 與 rollback condition</li>
<li>customer communication 是否連到當時已知事實</li>
<li>handoff 是否能靠 decision log 接上脈絡</li>
<li>post-incident review 是否能直接引用決策紀錄</li>
</ul>
<table>
  <thead>
      <tr>
          <th>決策欄位</th>
          <th>最小可用判準</th>
          <th>判讀價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decision / Time</td>
          <td>有清楚決策內容與時間</td>
          <td>建立決策先後與節奏</td>
      </tr>
      <tr>
          <td>Context / Evidence</td>
          <td>有對應證據與限制</td>
          <td>避免事後合理化</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>有責任人可追蹤</td>
          <td>提升執行一致性</td>
      </tr>
      <tr>
          <td>Expected Effect</td>
          <td>有預期影響描述</td>
          <td>判斷決策是否有效</td>
      </tr>
      <tr>
          <td>Rollback Condition</td>
          <td>有回退門檻</td>
          <td>控制次生風險</td>
      </tr>
  </tbody>
</table>
<h2 id="欄位模型">欄位模型</h2>
<p>Incident decision log 的欄位模型要同時支援事中交班與事後復盤。欄位過少會失去證據鏈，欄位過多會讓事故現場寫不下去。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Timestamp</td>
          <td>記錄決策時間</td>
          <td>2026-05-02T10:15Z</td>
      </tr>
      <tr>
          <td>Decision</td>
          <td>寫清楚採取或暫緩的動作</td>
          <td>rollback API v42</td>
      </tr>
      <tr>
          <td>Context</td>
          <td>說明當時問題與限制</td>
          <td>p95 latency 超 SLO，trace sample 低</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>連到 dashboard、query、ticket</td>
          <td><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> chart、support case</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定執行或追蹤責任人</td>
          <td>IC、service owner、comms lead</td>
      </tr>
      <tr>
          <td>Expected effect</td>
          <td>說明預期改善或風險</td>
          <td>10 分鐘內 error rate 下降</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">Rollback condition</a></td>
          <td>說明何時回退這個決策</td>
          <td>queue lag 超門檻即停止</td>
      </tr>
      <tr>
          <td>Follow-up</td>
          <td>標記後續查證或復盤項目</td>
          <td>補 runbook、補 alert</td>
      </tr>
  </tbody>
</table>
<p>Timestamp 要使用一致時間基準。事故跨工具、跨時區、跨 vendor 時，decision log 應保留標準化時間，必要時也保留來源原始時間。</p>
<p>Decision 欄位要寫具體動作。<code>處理中</code>、<code>觀察一下</code> 這類描述難以支援復盤；<code>rollback API v42</code>、<code>disable feature flag checkout_new_route</code>、<code>escalate to vendor support</code> 才能回放。</p>
<p>Context 欄位要保留限制。事故期間的資料常有缺口，decision log 應寫出 evidence 的 completeness、freshness、<a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a> 與已知盲區。</p>
<p>Expected effect 與 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a> 是控制次生風險的核心。每個止血或回復決策都應說明預期看到什麼改善，以及看到什麼訊號時要撤回或改路線。</p>
<h2 id="決策類型">決策類型</h2>
<p>Incident decision log 需要覆蓋事故期間會改變路由的決策。聊天可以保留在原頻道；每個會影響分級、止血、回復、通訊或責任的動作都應進 log。</p>
<table>
  <thead>
      <tr>
          <th>決策類型</th>
          <th>記錄重點</th>
          <th>下游用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Severity change</td>
          <td>impact scope、customer pain、SLO</td>
          <td>對齊分級與通訊節奏</td>
      </tr>
      <tr>
          <td>Containment</td>
          <td>降級、限流、隔離、停用功能</td>
          <td>判斷止血是否有效</td>
      </tr>
      <tr>
          <td>Rollback / failover</td>
          <td>版本、流量、資料相容性</td>
          <td>支援回復與復盤</td>
      </tr>
      <tr>
          <td>Customer communication</td>
          <td>對外說法、已知事實、限制</td>
          <td>保持內外部訊息一致</td>
      </tr>
      <tr>
          <td>Vendor escalation</td>
          <td>vendor、ticket、ETA、替代方案</td>
          <td>管理外部依賴事故</td>
      </tr>
      <tr>
          <td>Security split</td>
          <td>資安 evidence、access、disclosure</td>
          <td>分流到 security IR</td>
      </tr>
  </tbody>
</table>
<p>Severity change 需要留下 impact scope。升級或降級事故等級時，decision log 應能回答哪些客戶、功能、區域、SLO 或商業風險支撐這個決策。</p>
<p>Containment 決策需要留下副作用。限流、降級、停用功能或隔離 tenant 都會改變使用者體驗，decision log 應記錄預期影響與解除條件。</p>
<p>Rollback / failover 決策需要留下資料相容性。版本回退、流量切換與資料 migration 可能互相影響，log 應記錄當時對資料風險的判斷。</p>
<p>Customer communication 決策需要與 evidence 對齊。對外說法應引用當時已確認事實，並標示仍在查證的範圍，避免內外部敘事分裂。</p>
<p>資料 migration 決策需要留下 rollout 階段。暫停 backfill、回到 fallback read、停止 contract 或選擇 fail-forward 時，decision log 應連到 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、mismatch sample、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 與 owner；完整範例可接到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故結束後沒人記得為何選擇 rollback 而非 degradation</li>
<li>IC handoff 後，新 IC 需要重問所有背景</li>
<li>對外通訊內容與內部決策依據對不起來</li>
<li>復盤時只能翻聊天紀錄拼時間線</li>
<li>同一決策被重複討論，因為缺少已決事項紀錄</li>
</ul>
<p>常見場景是 containment 與 rollback 在不同頻道同步進行，事後很難重建為什麼先做 A 再做 B。decision log 若能同步記錄選項、證據與回退條件，PIR 可以直接把差異轉成改進項目。</p>
<h2 id="事中使用">事中使用</h2>
<p>Decision log 的事中責任是支援 handoff、multi-incident coordination 與 stakeholder update。它讓事故團隊在壓力下維持共同記憶。</p>
<p>IC handoff 時，decision log 應提供最近決策、未完成動作、回退條件與目前 evidence 限制。新 IC 不需要重新翻整段聊天，就能接上決策脈絡。</p>
<p>Multi-incident coordination 時，decision log 能避免資源衝突。若兩個事故都需要同一組 database owner、comms lead 或 rollback window，決策紀錄能幫 IC pool 排序。</p>
<p>Stakeholder update 時，decision log 能保護對外敘事。status page、客戶通知與管理層更新應引用同一組已確認事實，並同步更新 impact assessment。</p>
<h2 id="事後使用">事後使用</h2>
<p>Decision log 的事後責任是支援 post-incident review。復盤需要理解當時的資訊條件，再用事後結果評估判讀品質與流程缺口。</p>
<p>Post-incident review 應從 decision log 取出三種材料：正確決策、錯誤假設與缺少 evidence 的決策。三者對應不同改善方向。</p>
<p>正確決策可以變成 runbook。若某次降級、rollback 或 vendor escalation 路線有效，應把 decision log 中的條件與步驟回寫到 runbook。</p>
<p>錯誤假設可以變成 readiness 或 experiment 題目。若當時相信 fallback 會吸收失敗但實際沒有，這個假設應回寫到 06 的 chaos 或 DR drill。</p>
<p>缺少 evidence 的決策可以回寫到 04。若團隊因 telemetry data quality、trace 斷鏈或 impact scope 不清而延遲決策，缺口應回到 observability readiness 與 data quality。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Incident decision log 的反模式通常來自把聊天紀錄當作決策紀錄。聊天紀錄保存討論，decision log 保存「已決事項與證據鏈」。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack 討論即紀錄</td>
          <td>復盤時翻聊天拼脈絡</td>
          <td>獨立 decision log 欄位</td>
      </tr>
      <tr>
          <td>事後補決策理由</td>
          <td>PIR 才重建當時為何這樣做</td>
          <td>事中記錄 context / evidence</td>
      </tr>
      <tr>
          <td>回退條件缺失</td>
          <td>rollback 後不知道何時改路線</td>
          <td>每個高風險決策寫 rollback condition</td>
      </tr>
      <tr>
          <td>Evidence 不連結</td>
          <td>決策只寫結論</td>
          <td>連到 dashboard / query / ticket</td>
      </tr>
      <tr>
          <td>Owner 不明</td>
          <td>決策已定但無人追蹤</td>
          <td>每筆決策指定 owner</td>
      </tr>
  </tbody>
</table>
<p>Slack 討論即紀錄會讓復盤成本升高。聊天頻道保留的是互動過程，decision log 應抽出可回放的決策摘要。</p>
<p>事後補決策理由容易產生 hindsight bias。事中記錄當時的 evidence 與限制，才能讓 PIR 同時評估判讀品質、流程品質與結果。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.2 incident command roles：定義誰維護 decision log</li>
<li>08.3 containment / recovery：記錄止血與回復決策</li>
<li>08.4 incident communication：對外更新引用同一組事實</li>
<li>08.12 IC handoff：交班時使用 decision log</li>
<li>08.5 post-incident review：把決策紀錄轉成復盤材料</li>
<li>04.17 telemetry data quality：標示 evidence 限制與偏誤</li>
<li>01.7 Schema Migration Rollout 證據：記錄 migration pause、fallback read、資料修補與 fail-forward 的決策鏈</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>：事故時調用驗證證據支撐決策</li>
</ul>
]]></content:encoded></item><item><title>MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/group-replication/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/group-replication/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Group Replication + InnoDB Cluster&lt;/em> — synchronous multi-primary 的 transaction model + 部署模型。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>把「Group Replication multi-primary mode」當成「multi-primary 直接線性 scale write」是常見誤解。&lt;/p>
&lt;p>Single-primary 跟 multi-primary 共用同一套 GR 機制（GCE atomic broadcast + certification + applier）— 切換 mode 是 &lt;em>配置變更&lt;/em>。但 &lt;em>性能效果&lt;/em> 經常跟讀者預期不同：在 single-primary cluster 上加開 &lt;code>group_replication_single_primary_mode=OFF&lt;/code>、預期 &lt;em>3 個 instance 都可以接受 write&lt;/em> 帶來吞吐倍增、實際上每個寫入仍要全 cluster GCE broadcast + certification、寫吞吐沒爆增 / latency 飆高 / certification 衝突回退增加。&lt;/p>
&lt;p>這篇 deep article 把 GR 的 &lt;em>certification 流程&lt;/em> 講清楚 — 為什麼「multi-primary」聽起來像「線性 scale」、實際是「保 strong consistency 的 multi-entry」。然後展開 InnoDB Cluster（GR + MySQL Shell + MySQL Router）作為 production deployment 工具。&lt;/p>
&lt;h2 id="group-replication-的-transaction-model">Group Replication 的 transaction model&lt;/h2>
&lt;p>GR 用 &lt;em>Group Communication Engine (GCE)&lt;/em>（Paxos 變種）達成 &lt;em>atomic broadcast&lt;/em> — 任何 write transaction 必須先 broadcast 到所有 member、所有 member 確認 &lt;em>certification pass&lt;/em> 才 commit。&lt;/p>
&lt;p>每個 transaction 的 GR lifecycle：&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">1. Client → Member A: BEGIN; UPDATE ...; COMMIT;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. Member A: 先 local execute、收集 write_set（被改的 row + PK + transaction GTID）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. Member A: write_set + binlog event → GCE broadcast to all members
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. GCE: Paxos consensus、所有 member 收到 broadcast、按 *相同順序*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">5. Each Member: certification phase — 看 write_set 跟 *尚未 apply 的 incoming transactions* 是否有 PK 衝突
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">6. 若無衝突 → apply 該 transaction（local + remote member 都 apply）、回 client COMMIT OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">7. 若衝突 → certification fail、Member A 對 client 回 ERR_LOCK_DEADLOCK / GR_CONFLICT、application 必須 retry&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>核心結論&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>Group Replication + InnoDB Cluster</em> — synchronous multi-primary 的 transaction model + 部署模型。</p></blockquote>
<hr>
<p>把「Group Replication multi-primary mode」當成「multi-primary 直接線性 scale write」是常見誤解。</p>
<p>Single-primary 跟 multi-primary 共用同一套 GR 機制（GCE atomic broadcast + certification + applier）— 切換 mode 是 <em>配置變更</em>。但 <em>性能效果</em> 經常跟讀者預期不同：在 single-primary cluster 上加開 <code>group_replication_single_primary_mode=OFF</code>、預期 <em>3 個 instance 都可以接受 write</em> 帶來吞吐倍增、實際上每個寫入仍要全 cluster GCE broadcast + certification、寫吞吐沒爆增 / latency 飆高 / certification 衝突回退增加。</p>
<p>這篇 deep article 把 GR 的 <em>certification 流程</em> 講清楚 — 為什麼「multi-primary」聽起來像「線性 scale」、實際是「保 strong consistency 的 multi-entry」。然後展開 InnoDB Cluster（GR + MySQL Shell + MySQL Router）作為 production deployment 工具。</p>
<h2 id="group-replication-的-transaction-model">Group Replication 的 transaction model</h2>
<p>GR 用 <em>Group Communication Engine (GCE)</em>（Paxos 變種）達成 <em>atomic broadcast</em> — 任何 write transaction 必須先 broadcast 到所有 member、所有 member 確認 <em>certification pass</em> 才 commit。</p>
<p>每個 transaction 的 GR lifecycle：</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. Client → Member A: BEGIN; UPDATE ...; COMMIT;
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Member A: 先 local execute、收集 write_set（被改的 row + PK + transaction GTID）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Member A: write_set + binlog event → GCE broadcast to all members
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. GCE: Paxos consensus、所有 member 收到 broadcast、按 *相同順序*
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Each Member: certification phase — 看 write_set 跟 *尚未 apply 的 incoming transactions* 是否有 PK 衝突
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 若無衝突 → apply 該 transaction（local + remote member 都 apply）、回 client COMMIT OK
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. 若衝突 → certification fail、Member A 對 client 回 ERR_LOCK_DEADLOCK / GR_CONFLICT、application 必須 retry</span></span></code></pre></div><p><strong>核心結論</strong>：</p>
<ul>
<li><em>Single-primary mode</em>：只有指定 member 接受 write、其他 member 純 apply、certification 仍跑（但衝突極少、因只有一個寫入源）</li>
<li><em>Multi-primary mode</em>：所有 member 都接受 write、certification 衝突常見、application 必須處理 conflict retry</li>
</ul>
<p><strong>「multi-primary 不會線性 scale write」的原因</strong>：</p>
<ul>
<li>每個 write 仍要全 cluster GCE broadcast + certification</li>
<li>寫吞吐 ceiling 受 <em>最慢 member + 網路延遲</em> 限制（不是「N members × M throughput」）</li>
<li>多寫入源增加 certification 衝突機率、衝突 retry 反而拖 throughput</li>
</ul>
<p><strong>「multi-primary 真實價值」</strong>：</p>
<ul>
<li><em>跨 region multi-active deploy</em>（每個 region local member 接受 local write、無 cross-region write latency）— 但需求極少、多數場景 single-primary + Aurora DSQL / Spanner 更實際</li>
<li><em>零停機 maintenance</em>（任一 member 下線、其他繼續接 write、不必 failover）— 但 single-primary mode 也提供同等 HA</li>
</ul>
<p>對 99% production case：<strong>single-primary mode</strong> 才是正確選擇。Multi-primary 是 <em>特殊 use case 工具</em>、不是 <em>預設 mode</em>。</p>
<h2 id="group-communication-enginegce">Group Communication Engine（GCE）</h2>
<p>GR 內建 GCE、基於 <em>XCom</em> protocol（Paxos 變種）。GCE 責任：</p>
<ul>
<li>Atomic broadcast：保證 message 到所有 member、按相同順序</li>
<li>Group membership：偵測 member join / leave / fail、reconfigure consensus</li>
<li>Network partition handling：minority partition 自動 fence（read-only）、majority 繼續服務</li>
</ul>
<p><strong>GCE 跟 Raft 對比</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>GR XCom (Paxos-like)</th>
          <th>Raft</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Leader</td>
          <td>沒固定 leader、每個 message 選一個 sender</td>
          <td>固定 leader、其他 follower</td>
      </tr>
      <tr>
          <td>配置複雜度</td>
          <td>高（cluster member 列表 + IP allowlist）</td>
          <td>中（更易理解）</td>
      </tr>
      <tr>
          <td>Member 數量</td>
          <td>預設 3 (max 9)</td>
          <td>預設 3-5</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td>高吞吐、低延遲（不必每次選 leader）</td>
          <td>Leader bottleneck 偶有</td>
      </tr>
      <tr>
          <td>工程實作</td>
          <td>XCom 在 MySQL 內部、不暴露 API</td>
          <td>etcd / Consul / TiKV 等獨立工具</td>
      </tr>
  </tbody>
</table>
<p>GR 的設計取捨：<em>緊耦合 MySQL</em>（不必外部 DCS）、<em>Paxos-like consensus</em>（不像 Raft 那麼簡單但效率更高）。trade-off 是 <em>對 ops 的 transparency 較低</em> — XCom 內部行為對 DBA 是 black box。</p>
<h2 id="innodb-clustergr--mysql-shell--mysql-router">InnoDB Cluster：GR + MySQL Shell + MySQL Router</h2>
<p>純 GR 是 <em>底層 replication mechanism</em>、要組成 production deployment 需要：</p>
<ul>
<li><em>MySQL Shell</em> (<code>mysqlsh</code>)：CLI 工具、提供 <code>dba.createCluster()</code> / <code>cluster.addInstance()</code> 等 cluster 管理 API</li>
<li><em>MySQL Router</em>：connection routing layer、自動發現 cluster topology、寫入 routing 給 primary、讀取 routing replica</li>
<li><em>MySQL Group Replication plugin</em>：在每個 MySQL instance 啟用</li>
</ul>
<p><strong>InnoDB Cluster = GR + Shell + Router</strong>、是 Oracle 推薦的 production GR deployment 方式。</p>
<h3 id="起始部署3-member-single-primary-cluster">起始部署（3 member single-primary cluster）</h3>





<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"># Step 1: 在每個 instance 啟 GR plugin + 配 my.cnf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="o">[</span>mysqld<span class="o">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">server_id</span> <span class="o">=</span> <span class="m">1</span>                          <span class="c1"># 各 instance 不同</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">gtid_mode</span> <span class="o">=</span> ON
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">enforce_gtid_consistency</span> <span class="o">=</span> ON
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">log_bin</span> <span class="o">=</span> mysql-bin
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">binlog_format</span> <span class="o">=</span> ROW
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">master_info_repository</span> <span class="o">=</span> TABLE
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">relay_log_info_repository</span> <span class="o">=</span> TABLE
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">transaction_write_set_extraction</span> <span class="o">=</span> XXHASH64
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nv">plugin_load_add</span> <span class="o">=</span> <span class="s1">&#39;group_replication.so&#39;</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="nv">group_replication_group_name</span> <span class="o">=</span> <span class="s2">&#34;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nv">group_replication_start_on_boot</span> <span class="o">=</span> OFF
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nv">group_replication_local_address</span> <span class="o">=</span> <span class="s2">&#34;node1.example.com:33061&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nv">group_replication_group_seeds</span> <span class="o">=</span> <span class="s2">&#34;node1:33061,node2:33061,node3:33061&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nv">group_replication_bootstrap_group</span> <span class="o">=</span> OFF
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nv">group_replication_single_primary_mode</span> <span class="o">=</span> ON       <span class="c1"># 99% 場景用 ON</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nv">group_replication_enforce_update_everywhere_checks</span> <span class="o">=</span> OFF
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># Step 2: 用 MySQL Shell 從第一個 member bootstrap cluster</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">mysqlsh --user<span class="o">=</span>root --host<span class="o">=</span>node1.example.com
</span></span><span class="line"><span class="ln">23</span><span class="cl">&gt; dba.configureInstance<span class="o">(</span><span class="s1">&#39;root@node1:3306&#39;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">&gt; var <span class="nv">cluster</span> <span class="o">=</span> dba.createCluster<span class="o">(</span><span class="s1">&#39;prodCluster&#39;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">&gt; cluster.addInstance<span class="o">(</span><span class="s1">&#39;root@node2:3306&#39;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">&gt; cluster.addInstance<span class="o">(</span><span class="s1">&#39;root@node3:3306&#39;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">&gt; cluster.status<span class="o">()</span>  <span class="c1"># 應該顯示 3 member、1 PRIMARY + 2 SECONDARY</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"># Step 3: 部署 MySQL Router</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">mysqlrouter --bootstrap root@node1:3306 --directory /etc/mysql-router --user<span class="o">=</span>mysqlrouter
</span></span><span class="line"><span class="ln">31</span><span class="cl">systemctl start mysql-router
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="c1"># 完成 — application 連 mysql-router:6446 (R/W) 或 :6447 (R/O)</span></span></span></code></pre></div><p>Application 連 Router、Router 自動發現 cluster topology + 自動 failover routing。Application 不必知道哪個 instance 是 primary。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-certification-lag--multi-primary-模式-retry-storm">1. Certification lag — Multi-primary 模式 retry storm</h3>
<p>Multi-primary mode 下、3 個 instance 同時收到 <em>相同 row</em> 的 conflicting write、certification 階段必有 N-1 個 transaction 被退回。Application 看到 <code>ER_GR_CONFLICT_TRANSACTION_ABORTED</code>、retry、若不智能 retry（exponential backoff）會 retry storm、整個 cluster 寫吞吐暴降。</p>
<p>修法：</p>
<ul>
<li>99% 場景用 <em>single-primary mode</em>、避開 conflict</li>
<li>真的需要 multi-primary：application 必須 sharding-aware（不同 entry 寫不同 row range）、本質上跟 Vitess sharding 同概念但用 GR 機制</li>
<li>Application retry 用 <em>jitter exponential backoff</em>、不直接 retry</li>
</ul>
<h3 id="2-certification-queue-爆炸--single-primary-mode-仍受-cert-backlog-影響">2. Certification queue 爆炸 — Single-primary mode 仍受 cert backlog 影響</h3>
<p>Single-primary mode 下 primary 接受 write、broadcast 到 secondary。Secondary 跟 primary network latency / 處理速度差時、cert queue 累積。Cert queue 滿 → primary write 也被卡（GR 設計：所有 member 同步前不接受新 write、保 consistency）。</p>
<p>修法：</p>
<ul>
<li>監控 <code>group_replication_member_stats</code> view：<code>COUNT_TRANSACTIONS_IN_QUEUE</code> 持續 &gt; 0 是警訊</li>
<li>提高 <code>group_replication_message_cache_size</code>（預設 1 GB）給 large transaction 緩衝</li>
<li>確認 <em>所有 member 同 instance class</em>、不要混 spec</li>
<li>跨 region GR：完全不推薦（network latency 殺 cert throughput）</li>
</ul>
<h3 id="3-large-transaction--全-cluster-卡住">3. Large transaction — 全 cluster 卡住</h3>
<p>GR 必須把整個 transaction（含所有 write_set）一次 broadcast。10 GB transaction（大批量 UPDATE）必須一次塞滿 GCE buffer、cluster 內所有 member 都暫停接受新 transaction 直到 broadcast / apply 完成。常見場景：批次 archive / 大 backfill / <code>INSERT ... SELECT 1 億 row</code>。</p>
<p>修法：</p>
<ul>
<li><code>group_replication_transaction_size_limit</code>（預設 150 MB）超過直接 reject、不要設 unlimited</li>
<li>大批量寫入拆 chunk（每 chunk &lt; 100 MB）、用 application 層 loop</li>
<li>對 archive / backfill 用 <code>INSERT INTO archive SELECT ... LIMIT 10000</code> chunked、不是一個 transaction</li>
</ul>
<h3 id="4-network-partition--minority-partition-自動-read-only">4. Network partition — Minority partition 自動 read-only</h3>
<p>3 member cluster、network partition 把 1 個 member 隔離。被隔離 member 是 <em>minority</em>、自動進入 <em>read-only mode</em>（不接受 write）、防 split-brain。Application 連到 minority member 寫入會失敗。</p>
<p>修法：</p>
<ul>
<li>MySQL Router 自動發現 cluster topology、自動 route write 到 majority partition primary</li>
<li>Application 必須處理 connection error + retry（甚至 connection string 改成 <em>Router endpoint</em> 而非個別 instance）</li>
<li>監控 <code>group_replication_primary_member</code> UDF、確認哪個是真 primary</li>
</ul>
<h3 id="5-member-加入-catch-up--大量-binlog-阻擋-cluster-service">5. Member 加入 catch-up — 大量 binlog 阻擋 cluster service</h3>
<p>新 member 加入 cluster（new instance / 復原 failed member）必須 <em>catch-up</em> — apply 從 GR cluster start 到當前所有 binlog 才能 join consensus。如果 cluster 已運作 1 個月、binlog 累積 100 GB、catch-up 可能 6-12 小時、catch-up 期間 <em>該 member 不投票、其他 member 仍 service</em>、但 majority 安全邊界縮小（3 → 2 member working）。</p>
<p>修法：</p>
<ul>
<li>
<p>用 <em>MySQL Shell clone plugin</em> 直接 physical-snapshot 一個 existing member、跳過 binlog replay：</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">&gt; cluster.addInstance<span class="o">(</span><span class="s1">&#39;root@node4:3306&#39;</span>, <span class="o">{</span>recoveryMethod: <span class="s1">&#39;clone&#39;</span><span class="o">})</span></span></span></code></pre></div></li>
<li>
<p>Clone 期間原 member 暫不接 write traffic（用 Router temporarily 排除）</p>
</li>
<li>
<p>規劃 maintenance window 加 member、不要在 peak load 期間</p>
</li>
</ul>
<h2 id="何時用-gr--innodb-cluster">何時用 GR / InnoDB Cluster</h2>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 <em>zero-data-loss HA</em>（不容忍任何 binlog gap）</td>
          <td>GR single-primary</td>
      </tr>
      <tr>
          <td>需要 <em>自動 failover 而不必 Orchestrator + fence script</em></td>
          <td>GR / InnoDB Cluster</td>
      </tr>
      <tr>
          <td>需要 <em>跨 region multi-active</em>（且 conflict 可接受 / sharding-aware）</td>
          <td>GR multi-primary</td>
      </tr>
      <tr>
          <td>流量 &lt; 50K WPS、無嚴格 zero-loss 需求</td>
          <td>傳統 Orchestrator + Semi-sync 更簡單</td>
      </tr>
      <tr>
          <td>已用 Aurora / Cloud SQL 等 managed</td>
          <td>不用 GR、用 managed offering</td>
      </tr>
      <tr>
          <td>需要分散式 SQL（跨 region linearizable）</td>
          <td>Spanner / CockroachDB / Aurora DSQL（GR 不解決這個）</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>GR 取代傳統 async / semi-sync replication、不是 <em>加在上面</em>。啟用 GR 後不要再配 <code>master-slave</code> style replication。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-orchestrator">跟 Orchestrator</h3>
<p>Orchestrator 跟 InnoDB Cluster 不該 <em>同時用</em> — 兩者都會 trigger failover、會打架。GR / InnoDB Cluster 內建 failover、不需要 Orchestrator。詳見 <a href="/blog/backend/01-database/vendors/mysql/orchestrator-failover/" data-link-title="MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster &#43; GTID-based promotion 的兩段 paradox" data-link-desc="Orchestrator 是 MySQL HA 自動 failover 的 de facto standard、但讀者第一個問題往往是「HA 工具自己會壞嗎」。本文走 Orchestrator 的雙層架構（管 MySQL 的 raft cluster &#43; 被 raft 管的 orchestrator instance）→ topology discovery → failure detection → failover decision tree → promote action → 5 production 踩雷（split-brain 跟 fencing / pre-failover hook 失敗 / anti-flapping window / GTID errant transaction / VIP 跟 ProxySQL 整合斷層）→ 跟 ProxySQL / Patroni / RDS 對比">Orchestrator Failover</a>。</p>
<h3 id="跟-proxysql--mysql-router">跟 ProxySQL / MySQL Router</h3>
<p>ProxySQL 可以連 GR cluster（自動偵測 read_only flag）、但 <em>MySQL Router</em> 是 GR 原生的 routing layer、跟 InnoDB Cluster 緊耦合（透過 MySQL Shell metadata）。</p>
<p>選擇邏輯：</p>
<ul>
<li><em>純 MySQL stack, 想 Oracle-supported 整套</em> → MySQL Router</li>
<li><em>已用 ProxySQL（包含其他非 GR cluster）+ 統一 routing</em> → 仍用 ProxySQL</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL 配置</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p>GR 對 <code>innodb_flush_log_at_trx_commit</code> / <code>sync_binlog</code> 行為更敏感 — GR 要求 binlog 必須 <em>fsync to disk</em>（<code>sync_binlog=1</code>）保 zero-loss、不能用 <code>sync_binlog=0</code> 換速度。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-postgresql-patroni-對比">跟 PostgreSQL Patroni 對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>InnoDB Cluster</th>
          <th>Patroni + PostgreSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Consensus</td>
          <td>GCE (Paxos-like) 內建</td>
          <td>依賴外部 DCS (etcd / Consul)</td>
      </tr>
      <tr>
          <td>Multi-primary</td>
          <td>支援（但少用）</td>
          <td>不支援（PG single-primary）</td>
      </tr>
      <tr>
          <td>HA tooling</td>
          <td>MySQL Shell + Router 整套</td>
          <td>Patroni + HAProxy + pgBouncer</td>
      </tr>
      <tr>
          <td>Setup 複雜度</td>
          <td>中（MySQL Shell 帶很多 abstraction）</td>
          <td>中（Patroni config + DCS）</td>
      </tr>
      <tr>
          <td>5-year production maturity</td>
          <td>Oracle-backed</td>
          <td>community-driven、廣用</td>
      </tr>
  </tbody>
</table>
<p>兩者角色相同、設計取捨不同。詳見 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni HA</a>。</p>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>配置建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Member 數量</td>
          <td>3 (預設、容忍 1 failure)、5 (容忍 2 failure)</td>
      </tr>
      <tr>
          <td>Member 間 network latency</td>
          <td>&lt; 5ms（同 region 同 AZ 或跨 AZ）</td>
      </tr>
      <tr>
          <td>Network bandwidth</td>
          <td>至少 1 Gbps、broadcast traffic 重</td>
      </tr>
      <tr>
          <td>Transaction size limit</td>
          <td><code>group_replication_transaction_size_limit=150M</code></td>
      </tr>
      <tr>
          <td>Message cache</td>
          <td><code>group_replication_message_cache_size=1G</code>（預設）+ 看 lag 調</td>
      </tr>
      <tr>
          <td>MySQL Router instance</td>
          <td>至少 2 個（HA）、放 application 同 LB 後</td>
      </tr>
  </tbody>
</table>
<p>Member 跨 region：<em>不推薦</em>。GR 對 latency 敏感、跨 region 50-200ms RTT 嚴重影響 cert throughput。multi-region 需求用 Aurora Global Database / Spanner 等專為跨 region 設計的方案。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（GR 取代傳統 replication）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/orchestrator-failover/" data-link-title="MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster &#43; GTID-based promotion 的兩段 paradox" data-link-desc="Orchestrator 是 MySQL HA 自動 failover 的 de facto standard、但讀者第一個問題往往是「HA 工具自己會壞嗎」。本文走 Orchestrator 的雙層架構（管 MySQL 的 raft cluster &#43; 被 raft 管的 orchestrator instance）→ topology discovery → failure detection → failover decision tree → promote action → 5 production 踩雷（split-brain 跟 fencing / pre-failover hook 失敗 / anti-flapping window / GTID errant transaction / VIP 跟 ProxySQL 整合斷層）→ 跟 ProxySQL / Patroni / RDS 對比">MySQL Orchestrator Failover</a>（GR / InnoDB Cluster 不必 Orchestrator）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（routing layer 對比）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（GR durability 需求）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/bdr-multi-master/" data-link-title="PostgreSQL BDR / Multi-Master：active-active 寫入的 3 種路徑跟 conflict 治理" data-link-desc="PG 預設是 single-primary、active-active 多寫入入口需要 *BDR (EDB)* / *pgEdge* / *Bucardo* 等 extension。本文走 3 種 multi-master 方案對比、conflict detection &#43; resolution model、async vs sync 取捨、配置 step-by-step（pgEdge 為主）、5 production 踩雷（last-write-wins data loss / sequence collision / DDL replication / conflict log 治理 / failover 後 timeline 分歧）、跟 MySQL Group Replication sibling 對比">PostgreSQL BDR / Multi-Master</a>（PG sibling、active-active 寫入 3 種路徑跟 conflict 治理）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni HA</a>（PG sibling、不同 consensus）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡片</a> / <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">Paxos / Raft 對比</a></li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication.html">MySQL Group Replication</a> / <a href="https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-innodb-cluster.html">InnoDB Cluster</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL BDR / Multi-Master：active-active 寫入的 3 種路徑跟 conflict 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/bdr-multi-master/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/bdr-multi-master/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>multi-master / active-active replication&lt;/em> — 不是 PG 預設、需要 extension。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-預設沒-multi-master得用-extension">PG 預設沒 multi-master、得用 extension&lt;/h2>
&lt;p>PG core 是 &lt;em>single-primary streaming replication&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>寫入只能進 primary&lt;/li>
&lt;li>Standby 接受 read（hot_standby）但拒絕 write&lt;/li>
&lt;li>Failover 後新 primary 接管、不能多入口&lt;/li>
&lt;/ul>
&lt;p>對需要 &lt;em>active-active&lt;/em>（多 region 各自接受 local write）的場景、PG 提供 3 條 extension 路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>方案&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>機制&lt;/th>
 &lt;th>License&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>BDR&lt;/strong>&lt;/td>
 &lt;td>EDB（Enterprise）&lt;/td>
 &lt;td>Logical replication-based、雙向&lt;/td>
 &lt;td>商業（EDB 訂閱）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>pgEdge&lt;/strong>&lt;/td>
 &lt;td>pgEdge Inc.&lt;/td>
 &lt;td>基於 BDR、開源、加 Spock extension&lt;/td>
 &lt;td>開源（Spock）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Bucardo&lt;/strong>&lt;/td>
 &lt;td>community&lt;/td>
 &lt;td>Trigger-based、async、Perl 寫&lt;/td>
 &lt;td>開源（BSD）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每條路徑有不同 trade-off。對 99% PG production case、&lt;em>不需要 multi-master&lt;/em> — single-primary streaming replication + read replica scaling 已夠。Multi-master 是 &lt;em>特殊需求&lt;/em>（跨 region active-active write / 不可中斷 maintenance）才上。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &amp;#43; certification* 整個機制不同。本文走 GR 機制（GCE &amp;#43; certification &amp;#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">MySQL Group Replication&lt;/a> 對比：MySQL GR 是 &lt;em>官方內建&lt;/em>（5.7+）、PG 沒對應內建選項。MySQL 用戶 GR / InnoDB Cluster 直接套、PG 用戶要選 extension + license trade-off。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>multi-master / active-active replication</em> — 不是 PG 預設、需要 extension。</p></blockquote>
<hr>
<h2 id="pg-預設沒-multi-master得用-extension">PG 預設沒 multi-master、得用 extension</h2>
<p>PG core 是 <em>single-primary streaming replication</em>：</p>
<ul>
<li>寫入只能進 primary</li>
<li>Standby 接受 read（hot_standby）但拒絕 write</li>
<li>Failover 後新 primary 接管、不能多入口</li>
</ul>
<p>對需要 <em>active-active</em>（多 region 各自接受 local write）的場景、PG 提供 3 條 extension 路徑：</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>來源</th>
          <th>機制</th>
          <th>License</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>BDR</strong></td>
          <td>EDB（Enterprise）</td>
          <td>Logical replication-based、雙向</td>
          <td>商業（EDB 訂閱）</td>
      </tr>
      <tr>
          <td><strong>pgEdge</strong></td>
          <td>pgEdge Inc.</td>
          <td>基於 BDR、開源、加 Spock extension</td>
          <td>開源（Spock）</td>
      </tr>
      <tr>
          <td><strong>Bucardo</strong></td>
          <td>community</td>
          <td>Trigger-based、async、Perl 寫</td>
          <td>開源（BSD）</td>
      </tr>
  </tbody>
</table>
<p>每條路徑有不同 trade-off。對 99% PG production case、<em>不需要 multi-master</em> — single-primary streaming replication + read replica scaling 已夠。Multi-master 是 <em>特殊需求</em>（跨 region active-active write / 不可中斷 maintenance）才上。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">MySQL Group Replication</a> 對比：MySQL GR 是 <em>官方內建</em>（5.7+）、PG 沒對應內建選項。MySQL 用戶 GR / InnoDB Cluster 直接套、PG 用戶要選 extension + license trade-off。</p>
<h2 id="multi-master-三方案對比">Multi-master 三方案對比</h2>
<h3 id="方案-1bdr-edb-postgres-distributed">方案 1：BDR (EDB Postgres Distributed)</h3>
<p>EDB 商業 distributed 方案、跑在 EDB Postgres Advanced Server 或 PG community 上。</p>
<p><strong>特性</strong>：</p>
<ul>
<li>雙向 logical replication、N-way active-active</li>
<li>Built-in conflict detection + resolution（LWW / column-level / user-defined）</li>
<li>Eager（sync）跟 async 兩種 mode</li>
<li>Tightly integrated with EDB tooling</li>
</ul>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>商業 license、EDB 訂閱</li>
<li>對 cross-region multi-master 成熟（北美 enterprise 廣用）</li>
<li>對 <em>新 PG version</em> 通常滯後幾個月</li>
</ul>
<h3 id="方案-2pgedge基於-spock-extension">方案 2：pgEdge（基於 Spock extension）</h3>
<p>pgEdge 開源 multi-master、基於 <em>Spock</em> extension（從 BDR 衍生）：</p>
<p><strong>特性</strong>：</p>
<ul>
<li>開源、可自管</li>
<li>跟 BDR 架構接近、無 license fee</li>
<li>Conflict resolution 用 LWW + column-level</li>
<li>對 <em>edge / 地理分散</em> 場景設計</li>
</ul>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>較新（2023+）、社群驗證度低於 BDR</li>
<li>Conflict resolution policy 比 BDR 簡單</li>
<li>部分 EDB 商業 feature 沒對應</li>
</ul>
<h3 id="方案-3bucardo">方案 3：Bucardo</h3>
<p>PG community async multi-master、Perl 寫、trigger-based：</p>
<p><strong>特性</strong>：</p>
<ul>
<li>完全開源</li>
<li>Trigger-based（不依賴 logical replication）</li>
<li>支援 multi-source replication（fan-in / fan-out）</li>
</ul>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>Async only — <em>higher latency conflict</em></li>
<li>Trigger overhead（影響 primary 寫吞吐）</li>
<li>維護 Perl + tools chain 不普及</li>
<li>對 <em>Sync 一致性</em> 需求不適用</li>
</ul>
<h2 id="multi-master-conflict-model">Multi-Master Conflict Model</h2>
<p>任何 multi-master 方案都要解決 <em>同一 row 兩地同時改</em> 的 conflict：</p>
<h3 id="conflict-來源">Conflict 來源</h3>





<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">Region A (primary 1)          Region B (primary 2)
</span></span><span class="line"><span class="ln">2</span><span class="cl">UPDATE orders                 UPDATE orders
</span></span><span class="line"><span class="ln">3</span><span class="cl">SET status=&#39;shipped&#39;          SET status=&#39;cancelled&#39;
</span></span><span class="line"><span class="ln">4</span><span class="cl">WHERE id=100                  WHERE id=100
</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></code></pre></div><p>跨 region 兩地各自 commit、replication lag 期間發現 conflict、必須 <em>自動 resolve</em>（不能丟給 application）。</p>
<h3 id="conflict-resolution-strategies">Conflict Resolution Strategies</h3>
<p><strong>1. Last-Write-Wins (LWW)</strong> — 最常見：</p>
<ul>
<li>比較 transaction commit timestamp、晚的贏</li>
<li>簡單但 <em>data loss</em>（前一個 commit 的變更被覆蓋）</li>
<li>需要 <em>clock 同步</em>（NTP）—  clock skew 造成不可預測</li>
</ul>
<p><strong>2. Column-level conflict resolution</strong>：</p>
<ul>
<li>不同 column 各自 LWW（status column 跟 amount column 獨立解）</li>
<li>比 row-level LWW 細、但需 application semantics 配合</li>
</ul>
<p><strong>3. User-defined trigger</strong>：</p>
<ul>
<li>寫 PG function 解 conflict</li>
<li>對 <em>特殊 business logic</em>（如：金額相加、不是覆蓋）有用</li>
<li>維護成本高</li>
</ul>
<p><strong>4. Manual reconciliation</strong>：</p>
<ul>
<li>Conflict 寫進 log table、application / DBA 手動處理</li>
<li>對 <em>無法自動 resolve</em> 場景（如金融）</li>
<li>高 ops cost</li>
</ul>
<p>對 99% case 用 LWW、接受 small data loss、application 設計 <em>idempotent / commutative</em> 操作避免衝突。</p>
<h3 id="conflict-機率取決於-application-pattern">Conflict 機率取決於 application pattern</h3>
<ul>
<li><em>Tenant-isolated</em> application（user_id 各自寫自己的 row）：基本無 conflict</li>
<li><em>Shared counter / inventory</em> application：高 conflict、multi-master 不適合</li>
<li><em>Append-only event log</em>：conflict 低、適合 multi-master</li>
</ul>
<h2 id="配置-step-by-steppgedge-為主">配置 step-by-step（pgEdge 為主）</h2>
<p>pgEdge 開源、最常見的 self-hosted 選擇。</p>
<h3 id="step-1在每個-region-node-裝-pgedge">Step 1：在每個 region node 裝 pgEdge</h3>





<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"># Install pgEdge CLI</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl -fsSL https://pgedge-upstream.s3.amazonaws.com/REPO/install.py <span class="p">|</span> python3
</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"># Setup PG + Spock + pgEdge</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">./pgedge install pg16
</span></span><span class="line"><span class="ln">6</span><span class="cl">./pgedge install spock</span></span></code></pre></div><h3 id="step-2配置每個-node">Step 2：配置每個 node</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 在 node1（us-east） 跑
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">node_create</span><span class="p">(</span><span class="n">node_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;node1&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">dsn</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;host=node1.example.com port=5432 dbname=production&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 在 node2（eu-west）跑
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">node_create</span><span class="p">(</span><span class="n">node_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;node2&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">dsn</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;host=node2.example.com port=5432 dbname=production&#39;</span><span class="p">);</span></span></span></code></pre></div><h3 id="step-3建-replication-set--subscribe">Step 3：建 replication set + subscribe</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 在 node1 建 default replication set + 加 tables
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">repset_add_all_tables</span><span class="p">(</span><span class="s1">&#39;default&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 在 node1 subscribe node2
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">sub_create</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">subscription_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;sub_n1_n2&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="n">provider_dsn</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;host=node2.example.com port=5432 dbname=production&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 在 node2 subscribe node1（雙向）
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">sub_create</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="n">subscription_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;sub_n2_n1&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="n">provider_dsn</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;host=node1.example.com port=5432 dbname=production&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><h3 id="step-4設-conflict-resolution">Step 4：設 conflict resolution</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 設 LWW（預設）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">conflict_resolution_setting_set</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">conflict_type</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;update_origin_change&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">resolution_setting</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;apply_remote&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><h3 id="step-5驗證">Step 5：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 subscription 狀態
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">spock</span><span class="p">.</span><span class="n">subscription</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看 replication lag
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_replication</span><span class="p">;</span></span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-lww-data-loss--application-沒設計-commutative">1. LWW data loss — Application 沒設計 commutative</h3>
<p>LWW 預設、兩 region 同時 UPDATE 同 row → 晚的 commit 贏、早的丟失。Application 看不到「我寫的不見了」、debug 困難。</p>
<p>修法：</p>
<ul>
<li>Application schema 設計 <em>tenant-isolated</em>（user_id 各自寫自己 row）</li>
<li>對 <em>shared counter / inventory</em> 用 <em>commutative operation</em>（INCREMENT not SET）</li>
<li>重要寫入加 <em>audit log</em> — conflict 仍寫到 audit、application 看 audit 知道發生過</li>
<li>真的需要 strict consistency 別用 multi-master、用 single-primary + reader 或 distributed SQL</li>
</ul>
<h3 id="2-sequence-collision--two-region-各自-next-同號">2. Sequence collision — Two region 各自 next 同號</h3>
<p><code>SERIAL</code> / <code>IDENTITY</code> 用 sequence、兩 region 各自 nextval 可能拿到同 number、INSERT 衝突（PK duplicate）。</p>
<p>修法：</p>
<ul>
<li>用 <em>staggered sequence range</em>：node1 用 1-1M、node2 用 1M+1 到 2M（用 <code>setval</code>）</li>
<li>或用 <em>UUID</em>（v4 / v7）作 PK、跨 node 無 collision</li>
<li>或 <em>sequence per-node namespace</em>：<code>CREATE SEQUENCE orders_id_node1 START 1 INCREMENT 2</code>（odd vs even）</li>
</ul>
<h3 id="3-ddl-replication-不自動">3. DDL replication 不自動</h3>
<p>PG logical replication（pgEdge / BDR 基礎）<em>不自動 replicate DDL</em>。每 node <code>CREATE TABLE</code> / <code>ALTER TABLE</code> 必須 <em>分別跑</em>。</p>
<p>修法：</p>
<ul>
<li>用 <em>deployment automation</em>（Ansible / Terraform）對所有 node 同時跑 DDL</li>
<li>pgEdge 提供 <code>spock.replicate_ddl(...)</code> 把 DDL 轉成可 replicate event</li>
<li>BDR Enterprise 有 <em>DDL replication</em>（商業 feature）</li>
<li>DDL 變更前確認 <em>所有 node 都健康</em>、減少 partial state</li>
</ul>
<h3 id="4-conflict-log-治理--log-table-爆滿">4. Conflict log 治理 — Log table 爆滿</h3>
<p>每個 conflict 寫進 <code>spock.conflict_log</code> / <code>bdr.conflict_history</code> 等 table、log 累積 disk 爆。</p>
<p>修法：</p>
<ul>
<li>設 <em>log retention</em>：cron 定期 archive + delete 老 conflict log</li>
<li>監控 conflict rate — 高 conflict rate 是 application 設計問題（不是 ops 問題）</li>
<li>對 <em>strict business</em> conflict 寫進 application-level audit table、不只 system log</li>
</ul>
<h3 id="5-failover-後-timeline-分歧">5. Failover 後 timeline 分歧</h3>
<p>Multi-master 設計上 <em>每 region 是 primary</em>、Region A 掛了 Region B 接管 — 但 Region A 復活後 <em>仍認為自己是 primary</em>。如果 Region A 復活前已有寫入沒 replicate 出去、resolution 跟 LWW 衝突。</p>
<p>修法：</p>
<ul>
<li><em>Fence Region A 復活</em>：物理 fence（network firewall）+ 手動 unfence 流程</li>
<li>用 <em>etcd / Consul</em> 跟 BDR / Spock 整合 leader election（避免 split-brain）</li>
<li>對 cross-region multi-master、必須有 <em>runbook</em> 處理 region 復活流程、不靠自動</li>
</ul>
<h2 id="何時用-multi-master-vs-不用">何時用 multi-master vs 不用</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>真正 cross-region active-active write 需求</td>
          <td>BDR / pgEdge</td>
      </tr>
      <tr>
          <td>不可中斷 maintenance（zero downtime upgrade）</td>
          <td>BDR / pgEdge</td>
      </tr>
      <tr>
          <td>高 conflict rate（shared counter / inventory）</td>
          <td>不要 multi-master、用 distributed SQL</td>
      </tr>
      <tr>
          <td>Read scaling 為主、可接受 stale read</td>
          <td>streaming replication + read replica（更簡單）</td>
      </tr>
      <tr>
          <td>Strict consistency 需求</td>
          <td>single-primary + sync replication 或 Aurora DSQL / Spanner</td>
      </tr>
      <tr>
          <td>預算敏感 + 不想養 BDR / pgEdge ops</td>
          <td>不要 multi-master、用 managed distributed SQL</td>
      </tr>
  </tbody>
</table>
<h2 id="跟-mysql-group-replication-對比">跟 MySQL Group Replication 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG Multi-Master</th>
          <th>MySQL Group Replication</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內建？</td>
          <td>否、需 extension</td>
          <td>是、5.7+ 內建</td>
      </tr>
      <tr>
          <td>商業 vs 開源</td>
          <td>BDR 商業 / pgEdge 開源</td>
          <td>Oracle 商業 / community 都行</td>
      </tr>
      <tr>
          <td>Sync mode</td>
          <td>可（BDR eager）</td>
          <td>是（certification-based）</td>
      </tr>
      <tr>
          <td>Conflict resolution</td>
          <td>LWW / column / user-defined</td>
          <td>Certification-based（distributed transaction）</td>
      </tr>
      <tr>
          <td>Production maturity</td>
          <td>BDR 高、pgEdge 中</td>
          <td>高（Oracle 推）</td>
      </tr>
      <tr>
          <td>Use case 比例</td>
          <td>少（PG 多用 single-primary）</td>
          <td>較多（MySQL 推 InnoDB Cluster）</td>
      </tr>
  </tbody>
</table>
<p>MySQL GR 內建 + Oracle 推、PG 沒對應內建。對 multi-master 需求重的 org、MySQL 走 GR 路徑更直接。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication Topology</h3>
<p>Multi-master 是 <em>streaming replication 之上的 logical replication 加雙向</em>、不取代 streaming。Streaming 仍給 standby / failover、multi-master 給 active-active write。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-logical-replication">跟 Logical Replication</h3>
<p>pgEdge / BDR 都基於 logical replication slot、跟 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a> 共用 PG logical decoding infrastructure、但 <em>配置 + tooling</em> 不同。</p>
<h3 id="跟-mvcc">跟 MVCC</h3>
<p>Multi-master 的 conflict 在 <em>commit 後</em> 偵測（async）、不在 transaction 內。跟單機 MVCC（同 cluster 內 transaction snapshot）不同層。詳見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（streaming + multi-master 共存）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PG Logical Replication + Debezium</a>（logical decoding 基礎）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PG MVCC + Lock Model</a>（multi-master conflict vs 單機 MVCC）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PG Patroni HA</a>（single-primary HA 替代方案）</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（multi-master vs distributed SQL）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">MySQL Group Replication</a>（sibling、不同實作）</li>
<li>官方：<a href="https://www.enterprisedb.com/products/edb-postgres-distributed-bdr">EDB BDR</a> / <a href="https://www.pgedge.com/">pgEdge</a> / <a href="https://github.com/pgEdge/spock">Spock GitHub</a> / <a href="https://bucardo.org/">Bucardo</a></li>
</ul>
]]></content:encoded></item><item><title>3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/</guid><description>&lt;p>Spotify 從 Kafka 遷出到 GCP Pub/Sub 的決策揭露了兩件事：broker 的可靠性保證是版本特性而非 Kafka 的不變量；以及「升級到新版」跟「換到另一個系統」之間的決策判準。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Spotify 的事件傳遞系統（Event Delivery）負責把使用者行為事件（播放、搜尋、推薦互動）從客戶端送到資料管線。系統跨 5 個 datacenter 運行 Kafka 0.7，production peak 700K events/sec、pressure test 達到 2M events/sec。事件資料是推薦系統、analytics 跟廣告計費的輸入，遺失事件直接影響商業決策的準確性。&lt;/p>
&lt;p>2016 年，Spotify 決定把 Event Delivery 從 Kafka 遷移到 GCP Pub/Sub，而非升級到當時已發布的 Kafka 0.8+。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="mirrormaker-的-best-effort-語意">MirrorMaker 的 best-effort 語意&lt;/h3>
&lt;p>Kafka 0.7 的跨 datacenter replication 工具 MirrorMaker 在 best-effort mode 下會丟失資料但向 producer 回報成功。對 Spotify 的場景，producer 端認為事件已送達，但跨 datacenter 的 mirror 實際上丟了一部分。丟失比例在正常情況下很低，但在 broker restart 或網路抖動時可以升高到影響 analytics 準確性的程度。&lt;/p>
&lt;p>這個問題的根源是 Kafka 0.7 的 producer 沒有 idempotent 保證，MirrorMaker 的 consumer offset commit 跟 producer ack 之間有 gap。&lt;/p>
&lt;h3 id="broker-restart-後-producer-無法自動恢復">Broker restart 後 producer 無法自動恢復&lt;/h3>
&lt;p>Kafka 0.7 的 producer 在 broker restart 後可能進入無法自動恢復的狀態 — 需要人工重啟 producer process。在 5 個 datacenter、數百個 producer instance 的規模下，每次 broker 維護操作都需要人工介入恢復 producer，運維成本跟 broker 數量成正比。&lt;/p>
&lt;h3 id="為什麼不升級到-kafka-08">為什麼不升級到 Kafka 0.8+&lt;/h3>
&lt;p>Kafka 0.8 引入了 replication、新的 consumer API 跟更可靠的 producer。但 Spotify 評估後認為升級的成本接近重新部署：&lt;/p>
&lt;ul>
&lt;li>Kafka 0.7 到 0.8 的 wire protocol 不相容，需要全量遷移而非滾動升級&lt;/li>
&lt;li>所有 producer / consumer 的 client library 都要更換&lt;/li>
&lt;li>Spotify 同時在向 GCP 遷移基礎設施，Kafka 的自管運維模式跟 GCP 的託管方向不一致&lt;/li>
&lt;/ul>
&lt;p>相比之下，GCP Pub/Sub 提供了託管的 exactly-once 語意、跨 region replication、零運維。遷移成本跟升級 Kafka 版本的成本相當，但遷移後的長期運維成本低得多。&lt;/p>
&lt;h2 id="解法與取捨">解法與取捨&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>留在 Kafka（升級 0.8+）&lt;/th>
 &lt;th>遷到 GCP Pub/Sub&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一次性遷移成本&lt;/td>
 &lt;td>中（全量遷移、不可滾動升級）&lt;/td>
 &lt;td>中（同樣需要改所有 client）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>長期運維成本&lt;/td>
 &lt;td>高（自管 broker × 5 DC）&lt;/td>
 &lt;td>低（託管、零 broker 維護）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可靠性保證&lt;/td>
 &lt;td>0.8+ 有 replication、改善大&lt;/td>
 &lt;td>Pub/Sub 原生 exactly-once&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 region replication&lt;/td>
 &lt;td>需要自建 MirrorMaker 2.0&lt;/td>
 &lt;td>原生支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生態鎖定&lt;/td>
 &lt;td>Kafka 生態成熟&lt;/td>
 &lt;td>GCP 鎖定、跨雲成本高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Spotify 的判斷是：在同時進行 GCP 遷移的背景下，維護自管 Kafka 的投資回報比不上切換到託管方案。這個判斷跟 Kafka 本身的能力無關 — Kafka 0.8+ 的可靠性已經解決了 0.7 的問題。決策的關鍵變數是「組織正在往哪走」，不只是「技術上哪個更好」。&lt;/p></description><content:encoded><![CDATA[<p>Spotify 從 Kafka 遷出到 GCP Pub/Sub 的決策揭露了兩件事：broker 的可靠性保證是版本特性而非 Kafka 的不變量；以及「升級到新版」跟「換到另一個系統」之間的決策判準。</p>
<h2 id="業務背景">業務背景</h2>
<p>Spotify 的事件傳遞系統（Event Delivery）負責把使用者行為事件（播放、搜尋、推薦互動）從客戶端送到資料管線。系統跨 5 個 datacenter 運行 Kafka 0.7，production peak 700K events/sec、pressure test 達到 2M events/sec。事件資料是推薦系統、analytics 跟廣告計費的輸入，遺失事件直接影響商業決策的準確性。</p>
<p>2016 年，Spotify 決定把 Event Delivery 從 Kafka 遷移到 GCP Pub/Sub，而非升級到當時已發布的 Kafka 0.8+。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="mirrormaker-的-best-effort-語意">MirrorMaker 的 best-effort 語意</h3>
<p>Kafka 0.7 的跨 datacenter replication 工具 MirrorMaker 在 best-effort mode 下會丟失資料但向 producer 回報成功。對 Spotify 的場景，producer 端認為事件已送達，但跨 datacenter 的 mirror 實際上丟了一部分。丟失比例在正常情況下很低，但在 broker restart 或網路抖動時可以升高到影響 analytics 準確性的程度。</p>
<p>這個問題的根源是 Kafka 0.7 的 producer 沒有 idempotent 保證，MirrorMaker 的 consumer offset commit 跟 producer ack 之間有 gap。</p>
<h3 id="broker-restart-後-producer-無法自動恢復">Broker restart 後 producer 無法自動恢復</h3>
<p>Kafka 0.7 的 producer 在 broker restart 後可能進入無法自動恢復的狀態 — 需要人工重啟 producer process。在 5 個 datacenter、數百個 producer instance 的規模下，每次 broker 維護操作都需要人工介入恢復 producer，運維成本跟 broker 數量成正比。</p>
<h3 id="為什麼不升級到-kafka-08">為什麼不升級到 Kafka 0.8+</h3>
<p>Kafka 0.8 引入了 replication、新的 consumer API 跟更可靠的 producer。但 Spotify 評估後認為升級的成本接近重新部署：</p>
<ul>
<li>Kafka 0.7 到 0.8 的 wire protocol 不相容，需要全量遷移而非滾動升級</li>
<li>所有 producer / consumer 的 client library 都要更換</li>
<li>Spotify 同時在向 GCP 遷移基礎設施，Kafka 的自管運維模式跟 GCP 的託管方向不一致</li>
</ul>
<p>相比之下，GCP Pub/Sub 提供了託管的 exactly-once 語意、跨 region replication、零運維。遷移成本跟升級 Kafka 版本的成本相當，但遷移後的長期運維成本低得多。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>留在 Kafka（升級 0.8+）</th>
          <th>遷到 GCP Pub/Sub</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一次性遷移成本</td>
          <td>中（全量遷移、不可滾動升級）</td>
          <td>中（同樣需要改所有 client）</td>
      </tr>
      <tr>
          <td>長期運維成本</td>
          <td>高（自管 broker × 5 DC）</td>
          <td>低（託管、零 broker 維護）</td>
      </tr>
      <tr>
          <td>可靠性保證</td>
          <td>0.8+ 有 replication、改善大</td>
          <td>Pub/Sub 原生 exactly-once</td>
      </tr>
      <tr>
          <td>跨 region replication</td>
          <td>需要自建 MirrorMaker 2.0</td>
          <td>原生支援</td>
      </tr>
      <tr>
          <td>生態鎖定</td>
          <td>Kafka 生態成熟</td>
          <td>GCP 鎖定、跨雲成本高</td>
      </tr>
  </tbody>
</table>
<p>Spotify 的判斷是：在同時進行 GCP 遷移的背景下，維護自管 Kafka 的投資回報比不上切換到託管方案。這個判斷跟 Kafka 本身的能力無關 — Kafka 0.8+ 的可靠性已經解決了 0.7 的問題。決策的關鍵變數是「組織正在往哪走」，不只是「技術上哪個更好」。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>：cross-region replication 跟 MirrorMaker 的進階主題。Spotify 的案例是「早期版本限制」的歷史教訓，Kafka 3.x 的 KRaft + idempotent producer 已解決這些問題。</li>
<li><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">Pub/Sub vendor 頁</a>：託管 MQ 的定位跟適用場景。</li>
<li><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a>：exactly-once 語意的工程實踐。Spotify 案例揭露 exactly-once 在早期 Kafka 版本不成立。</li>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 版本跟可靠性保證的關係。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>使用舊版 Kafka（&lt; 2.0）且跨 region replication 的資料完整性無法驗證</li>
<li>Broker restart 後需要人工重啟 producer、運維成本跟 broker 數量成正比</li>
<li>組織正在做基礎設施遷移（on-prem → cloud），考慮是否同步切換 MQ</li>
<li>評估「升級現有系統 vs 遷移到新系統」的決策框架</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.atspotify.com/2017/03/spotifys-event-delivery-the-road-to-the-cloud-part-ii">Spotify&rsquo;s Event Delivery — The Road to the Cloud (Part II)</a></li>
</ul>
]]></content:encoded></item><item><title>Open Policy Agent (OPA)</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/</guid><description>&lt;p>Open Policy Agent (OPA) 是 CNCF graduated 的 &lt;em>general-purpose policy engine&lt;/em>、設計目的是把「誰能做什麼、什麼 config 才合法」從 application code 抽到外部 policy decision layer。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> 的差別是：後兩者鎖在 K8s admission controller 領域、OPA 是 &lt;em>跨 enforcement point&lt;/em> 的 unified policy framework — 同一份 policy 可以同時管 K8s admission、API authz、Terraform plan、SQL row-level filter。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &amp;#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest&lt;/a> 的差別是：Conftest 是 OPA 的 &lt;em>CLI wrapper for static config&lt;/em>（在 CI 跑 Terraform / Dockerfile / K8s YAML 檢查）、OPA 本體是 &lt;em>runtime evaluation engine&lt;/em>（線上服務查詢決策）。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>OPA 的核心抽象是 &lt;em>decoupled decision + enforcement&lt;/em> — OPA 只負責 &lt;em>decide&lt;/em>（&lt;code>input&lt;/code> 進來、&lt;code>allow&lt;/code> / &lt;code>deny&lt;/code> + decision metadata 出去）、application 負責 &lt;em>enforce&lt;/em>（拿到 decision 後實際 reject request / block deploy / mask data）。這個解耦讓同一份 policy 跨 K8s admission（透過 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> 或 kube-mgmt sidecar）、Envoy authz filter、API gateway、Terraform pre-plan、SQL row-level filter、Kafka topic ACL 都能重用。&lt;/p>
&lt;p>OPA 的查詢語言是 &lt;em>Rego&lt;/em>、Datalog-like declarative language、設計上適合表達「給定一組 fact，這個動作合法嗎」。Rego 跟一般 imperative programming（Python / Go / YAML rules）差距大、team 要投入 1-2 週才能寫出 production-grade policy；換回的是 &lt;em>表達力 + 跨情境一致性&lt;/em> — Kyverno 的 YAML policy 易上手、但跨 K8s 邊界後沒辦法用。&lt;/p></description><content:encoded><![CDATA[<p>Open Policy Agent (OPA) 是 CNCF graduated 的 <em>general-purpose policy engine</em>、設計目的是把「誰能做什麼、什麼 config 才合法」從 application code 抽到外部 policy decision layer。它跟 <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> / <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 的差別是：後兩者鎖在 K8s admission controller 領域、OPA 是 <em>跨 enforcement point</em> 的 unified policy framework — 同一份 policy 可以同時管 K8s admission、API authz、Terraform plan、SQL row-level filter。跟 <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 的差別是：Conftest 是 OPA 的 <em>CLI wrapper for static config</em>（在 CI 跑 Terraform / Dockerfile / K8s YAML 檢查）、OPA 本體是 <em>runtime evaluation engine</em>（線上服務查詢決策）。</p>
<h2 id="服務定位">服務定位</h2>
<p>OPA 的核心抽象是 <em>decoupled decision + enforcement</em> — OPA 只負責 <em>decide</em>（<code>input</code> 進來、<code>allow</code> / <code>deny</code> + decision metadata 出去）、application 負責 <em>enforce</em>（拿到 decision 後實際 reject request / block deploy / mask data）。這個解耦讓同一份 policy 跨 K8s admission（透過 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 或 kube-mgmt sidecar）、Envoy authz filter、API gateway、Terraform pre-plan、SQL row-level filter、Kafka topic ACL 都能重用。</p>
<p>OPA 的查詢語言是 <em>Rego</em>、Datalog-like declarative language、設計上適合表達「給定一組 fact，這個動作合法嗎」。Rego 跟一般 imperative programming（Python / Go / YAML rules）差距大、team 要投入 1-2 週才能寫出 production-grade policy；換回的是 <em>表達力 + 跨情境一致性</em> — Kyverno 的 YAML policy 易上手、但跨 K8s 邊界後沒辦法用。</p>
<p>關鍵張力：<em>Rego 學習曲線</em> ↔ <em>unified policy 的長期價值</em>。只跑 K8s 的團隊用 Kyverno YAML 更直覺；只跑 CI policy 的用 Conftest 更輕；要在 K8s + API + Terraform + DB 跨層統一 policy、或要 audit-grade decision log、或預期 policy 會長期累積成資產的、才值得投資 OPA + Rego。</p>
<p>商業模型：核心 OPA 是 Apache 2.0 OSS、免費。Styra DAS（OPA 創辦人公司）是 enterprise SKU、提供 policy library + impact analysis + multi-cluster management + audit dashboard、適合大型團隊。OPAL（Permit.io 維護的 OSS）是 GitOps-style policy distribution layer、補 OSS OPA 缺的 bundle server。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>OPA 在 policy stack 中承擔哪一段（decision engine） vs enforcement point 各自的 ownership</li>
<li>Rego 投資門檻是否值得（K8s-only vs 跨 enforcement point）</li>
<li>Policy bundle / Decision log / Partial evaluation 三個 first-class concept 在 production 的設計形狀</li>
<li>何時用 OPA、何時走 <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> / <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> / <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 OPA deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Policy ownership</strong>：誰能寫 / 改 Rego policy（platform team / security team / SRE）、policy 是否進 Git、change 是否經 PR review + staging tenant 跑 24-48hr 觀察</li>
<li><strong>Bundle distribution</strong>：policy 是否 build 成 bundle（tar.gz）、是否簽章、OPA agent 是否定期 pull、bundle server 在哪（自管 nginx / S3 / OPAL / Styra DAS）</li>
<li><strong>Decision log governance</strong>：每個 decision 是否進 audit log（input + output + policy version + timestamp）、log 是否進 SIEM（<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / Elastic）、retention 多久</li>
<li><strong>Enforcement coverage</strong>：哪些 enforcement point 接 OPA（K8s admission / API / Envoy / Terraform）、policy 是否 share 還是各 point 各寫一份、跨 point 的一致性怎麼驗</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/security-as-risk-routing-system/" data-link-title="7.15 資安作為風險路由系統" data-link-desc="建立資安作為風險路由系統的導讀大綱，串接問題節點、控制面與跨模組交接">Policy as Code Foundations</a> 的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Rego policy 形狀</strong>：Rego 是 Datalog-like declarative language、policy 寫成 <code>allow { ... }</code> rule、所有條件成立才 evaluate 為 true。實務 idiom：底層寫 <em>base policy</em>（如 <code>policies/k8s/required_labels.rego</code>）、上層寫 <em>policy library</em>（共用 helper 如 <code>policies/lib/registry.rego</code>）、application 端傳 <code>input</code>（K8s admission request / API request / Terraform plan JSON）查詢。Rego 鼓勵 <em>small composable rule</em>、不寫長 imperative function。</p>
<p><strong>Policy bundle</strong>：OPA 不從 Git 直接讀 policy、而是讀 <em>bundle</em>（tar.gz、含 <code>.rego</code> + data JSON、optional 簽章）。Bundle 從 <em>bundle server</em> pull（自管 nginx / S3 / OPAL / Styra DAS）、OPA agent 定期 polling（預設 60s）。Bundle 的核心價值是 <em>versioned + signed + atomically swap</em> — policy 更新不會 partial apply、簽章確保中間沒被改、版本 metadata 讓 decision log 可追到當時用哪版 policy。</p>
<p><strong>Decision log</strong>：每個 OPA query 都可開 decision logging、log entry 含 <code>input</code> + <code>result</code> + <code>policy_version</code> + <code>timestamp</code> + <code>decision_id</code>。意義是 <em>audit-grade reconstruction</em> — 事後可以重跑 <code>opa eval --bundle &lt;version&gt; --input &lt;log_input&gt;</code> 驗證當時 decision 是否正確。Decision log 進 SIEM 後可做 <em>over-permission analysis</em>（哪些 user 拿到 allow 但實際從不該被 allow）跟 <em>policy coverage check</em>（哪些 rule 從沒被觸發過、可能是 dead code）。</p>
<p><strong>Integration pattern</strong>：production OPA 主要四種 enforcement integration — <em>K8s admission</em>（走 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 是 OPA 官方 K8s integration、或 kube-mgmt 把 OPA 當 sidecar 跑、admission webhook 把 request 送進 OPA decide）；<em>API authz</em>（application 在 request handler 開頭 query OPA、拿 allow/deny 後 enforce）；<em>Envoy / service mesh</em>（Envoy 的 ext_authz filter 接 OPA、L7 authz decision）；<em>Infrastructure as Code</em>（CI 跑 <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 對 Terraform plan / K8s YAML 做 OPA 評估）。</p>
<p><strong>Partial evaluation</strong>：OPA 進階 feature、把一份 policy 對某個 <em>partial input</em>（如 <code>user=&quot;alice&quot;</code>）pre-evaluate、產出 <em>殘餘 query</em>（如 SQL <code>WHERE tenant_id IN (...)</code> 或 regex），下發給 enforcement point 直接用。意義是 <em>把 policy decision 推到 enforcement point 內部</em>、減少每次 query 都要過 OPA 的 latency；常用於 row-level data filter（policy 寫一次、partial eval 出 SQL WHERE clause、application 直接拼進 query）。</p>
<p><strong>OPAL（GitOps for OPA）</strong>：OSS、Permit.io 維護、解決「policy 從 Git push 到所有 OPA agent」的 distribution 問題。Git → OPAL Server → OPA Agent 的 push model、policy commit 到 main branch 後幾秒內所有 OPA 更新。對應 OSS-only 的 production setup；Styra DAS 提供同等功能 + 管理 UI。</p>
<p><strong>Styra DAS（商業 management）</strong>：Styra 是 OPA 創辦人公司、DAS 是 enterprise SKU。核心價值：<em>policy library</em>（pre-built policy for K8s / Terraform / Kafka）、<em>impact analysis</em>（policy 上 production 前 dry-run 看會 deny 多少現有 resource）、<em>multi-cluster policy distribution</em>、<em>audit dashboard</em>。OSS-only 自己拼 OPAL + decision log + SIEM 也能做、但團隊 &gt; 50 個 cluster / 多 BU 時 DAS 划算。</p>
<p><strong>Constraint Framework</strong>：<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 在 OPA 之上加的 K8s-specific 抽象、用 <code>ConstraintTemplate</code>（Rego policy 模板）+ <code>Constraint</code>（K8s CRD instance、實際 enforce）。對純 K8s 場景比直接寫 Rego 更 K8s-native；但這個抽象只在 K8s 領域有意義、不會跨到 API / Terraform。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>OPA</th>
          <th>Kyverno</th>
          <th>Gatekeeper</th>
          <th>Conftest</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>定位</td>
          <td>General-purpose policy engine</td>
          <td>K8s-native admission controller</td>
          <td>OPA 的 K8s admission integration（官方）</td>
          <td>OPA 的 CLI wrapper for static config</td>
      </tr>
      <tr>
          <td>語言</td>
          <td>Rego（Datalog-like declarative）</td>
          <td>YAML（K8s-native）</td>
          <td>Rego（透過 ConstraintTemplate）</td>
          <td>Rego</td>
      </tr>
      <tr>
          <td>Enforcement</td>
          <td>K8s / API / Envoy / Terraform / SQL / Kafka 跨層</td>
          <td>K8s admission only</td>
          <td>K8s admission only</td>
          <td>CI / pre-commit（不在 runtime）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>陡 — Rego 1-2 週</td>
          <td>緩 — YAML 1-2 天</td>
          <td>中 — ConstraintTemplate 抽象 + Rego</td>
          <td>中 — Rego 1-2 週、但 scope 小</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>OPA agent（sidecar / daemon / library embed）</td>
          <td>K8s controller + webhook</td>
          <td>K8s controller + webhook</td>
          <td>CLI（CI / 本地）</td>
      </tr>
      <tr>
          <td>Mutation</td>
          <td>透過 Gatekeeper Mutation 或 application enforce 補上</td>
          <td>原生 mutate webhook（強項）</td>
          <td>Mutation 是 v3.10+ beta、功能不及 Kyverno</td>
          <td>無（static check only）</td>
      </tr>
      <tr>
          <td>Bundle / 分發</td>
          <td>Bundle server + sign + OPA agent pull / OPAL push</td>
          <td>K8s CRD apply（kubectl）</td>
          <td>K8s CRD apply</td>
          <td>Git repo（CI 直接 clone）</td>
      </tr>
      <tr>
          <td>Decision log</td>
          <td>First-class、audit-grade</td>
          <td>K8s event + audit log</td>
          <td>K8s event + audit log</td>
          <td>CI build log</td>
      </tr>
      <tr>
          <td>商業 SKU</td>
          <td>Styra DAS（management + impact analysis）</td>
          <td>Nirmata Kyverno Enterprise</td>
          <td>無（純 OSS）</td>
          <td>無（純 OSS）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>跨 enforcement point + long-term policy investment</td>
          <td>K8s-only + 快速上手 + YAML-friendly team</td>
          <td>K8s-only + 已用 OPA / Rego、要 OPA 官方整合</td>
          <td>CI pre-deploy check + Terraform / K8s YAML / Dockerfile</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — Rego policy 可移到其他 OPA-compatible engine</td>
          <td>高 — YAML policy 僅 Kyverno 認</td>
          <td>中 — Rego 可重用、Constraint 抽象要重寫</td>
          <td>低 — CLI tool、policy 可移到 OPA runtime</td>
      </tr>
  </tbody>
</table>
<p>選 OPA 的核心訴求：<em>跨 enforcement point 的 unified policy</em> + <em>long-term policy 資產化</em> + <em>audit-grade decision log</em> + 團隊願意投資 Rego。純 K8s + 想快速上手用 <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>；K8s + 已決定走 OPA 生態用 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a>；只跑 CI 不跑 runtime 用 <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Rego idioms（policy library + base policy）</strong>：production Rego 走分層結構 — <code>lib/</code>（utility function、registry whitelist、CIDR check）、<code>base/</code>（concrete policy、引用 lib）、<code>tests/</code>（用 <code>opa test</code> 跑 unit test）。Policy 也是 code、走 PR review + CI test + staging tenant、不是 console 直改。</p>
<p><strong>Partial evaluation for SQL row-level filter</strong>：把 policy 寫成「user 能看哪些 row」、用 <code>opa eval --partial</code> 把 <code>user=&quot;alice&quot;</code> 部分 pre-evaluate、output 殘餘 query 變 SQL <code>WHERE tenant_id IN ('a', 'b', 'c')</code>、application 拼進 query。意義是 <em>policy 不在 query path latency 上</em>、policy 規則仍是 SSoT。對應 RLS（row-level security）的工程化作法。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> workload identity 整合 authz</strong>：service-to-service authz 場景、SPIRE 簽 SVID（SPIFFE ID + mTLS cert）證明 caller 身份、OPA 拿到 SPIFFE ID 後 decide「這個 service 能呼叫這個 API 嗎」。SPIRE 解 <em>who</em>、OPA 解 <em>can they do this</em>、職責清楚分離。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 整合 dynamic credential policy</strong>：Vault 簽 dynamic credential（DB password / cloud STS token）的 issue 決定可以走 OPA — Vault 收到 issue request、轉 OPA decide「這個 caller 在這個 context 能不能拿這個 scope 的 token」。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的 lesson：scope 判斷不分散在應用層、集中到 policy engine。</p>
<p><strong>Decision log 進 SIEM</strong>：OPA decision log 設成 push 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> HEC / Elastic / Datadog、進 SIEM 後可做三件事 — over-permission analysis（哪些 allow 從沒被合法理由觸發）、dead policy detection（哪些 rule 從沒被 evaluate）、anomalous decision pattern（某 service 突然大量 allow 不在 baseline）。</p>
<p><strong>跟 K8s admission 的兩條路</strong>：純 K8s admission 場景、走 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a>（OPA 官方 K8s integration、有 Constraint Framework 抽象、社群活躍）比直接跑 OPA + kube-mgmt sidecar 更 production-ready。kube-mgmt 路線適合 already-running OPA 想加 K8s admission 而不引入 Gatekeeper 抽象。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Rego policy review 卡 SRE</strong>：policy 都得 SRE 寫、security team 看不懂 — 拆 <code>lib/</code> 給 SRE 維護、<code>base/</code> 給 security review、用 <code>opa test</code> unit test 保持迭代速度</li>
<li><strong>Bundle distribution 慢 / policy 不一致</strong>：自管 nginx bundle server 沒高可用、agent pull 失敗 fallback 用舊版 — 換 OPAL push model 或 S3 + CloudFront、bundle pull 失敗時 OPA <code>--set status.console=true</code> 直接 alert</li>
<li><strong>Decision log 沒進 SIEM</strong>：OPA 開了 decision log 但只進本地 file、沒人看 — 設 decision log plugin push 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> HEC / Kafka、不是寫本地 disk</li>
<li><strong>Policy 改完 production 大量 deny</strong>：新 policy 沒在 staging dry-run、上 production 後合法 traffic 被擋 — Styra DAS 的 impact analysis 或自己跑 <code>opa eval --partial</code> 對歷史 decision log replay、看 deny 數量再 promote</li>
<li><strong>OPA latency 高 / API 卡</strong>：每個 request 都 round-trip OPA、policy 複雜 evaluation 慢 — embed OPA as library（Go SDK / WASM）跑 in-process、或用 partial evaluation 把 policy compile 進 SQL / regex</li>
<li><strong>Rego policy bug 線上才發現</strong>：沒 unit test、staging 沒 cover edge case — 強制 PR 要含 <code>opa test</code> case、staging 跑 shadow mode（log only 不 enforce）24hr 再切 enforce</li>
<li><strong>跨 cluster policy drift</strong>：多 cluster 各自 apply、版本不同步 — OPAL 或 Styra DAS multi-cluster sync、不靠 kubectl apply 人工同步</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s admission only + YAML-friendly</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a></td>
      </tr>
      <tr>
          <td>K8s admission + 已選 OPA 生態</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a></td>
      </tr>
      <tr>
          <td>CI pre-deploy check（Terraform / K8s YAML / Dockerfile）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a></td>
      </tr>
      <tr>
          <td>Runtime container behavior（不是 admission）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">Falco</a></td>
      </tr>
      <tr>
          <td>Image scan + vulnerability policy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（scan）+ OPA（gate）</td>
      </tr>
      <tr>
          <td>Workload identity / mTLS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> + OPA（identity → authz 分工）</td>
      </tr>
      <tr>
          <td>Cloud IAM policy（AWS / GCP / Azure 本體）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>Decision log → SIEM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Rego 完整語法 reference（rule / function / built-in / <code>with</code> / <code>some</code>）</li>
<li>Gatekeeper Constraint Framework 的 ConstraintTemplate / Constraint CRD 設計細節（屬 Gatekeeper 頁）</li>
<li>Conftest CLI 用法跟 <code>conftest test</code> / <code>conftest verify</code> 流程（屬 Conftest 頁）</li>
<li>Kyverno YAML policy 語法跟 mutate / generate / verifyImages（屬 Kyverno 頁）</li>
<li>Styra DAS 商業 license / SKU 對照、Nirmata Enterprise 對照</li>
<li>WASM-compiled Rego policy 的 edge deployment 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 OPA 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>OPA admission policy 在 K8s 擋住未簽章 image deploy、配合 cosign signature verify 補 supply chain 信任鏈、policy 集中不分散到各 deployment</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>OPA admission 配合 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> scan result 擋住已知 vulnerable image deploy、policy 走「critical CVE = deny」</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>OPA 控制 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> dynamic credential issuance policy、scope 判斷集中不分散到應用層各自 if-else</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性 (section)</a></td>
          <td>OPA 是 admission gate 的核心工具、跟 SLSA provenance / cosign signature 組合做 policy enforcement、不是看一兩個欄位放行</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/security-as-risk-routing-system/" data-link-title="7.15 資安作為風險路由系統" data-link-desc="建立資安作為風險路由系統的導讀大綱，串接問題節點、控制面與跨模組交接">Policy as Code Foundations (section)</a></td>
          <td>OPA 對應 policy-as-code 的 <em>decoupled decision + enforcement</em>、跨 enforcement point 共用 policy 是設計核心、不是「再寫一份 K8s policy」</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/security-as-risk-routing-system/" data-link-title="7.15 資安作為風險路由系統" data-link-desc="建立資安作為風險路由系統的導讀大綱，串接問題節點、控制面與跨模組交接">7 章 policy-as-code foundations</a>、<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a></li>
<li>平行（Policy-as-Code 批次）：<a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a>（CI static check）、<a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>（K8s YAML-native）、<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a>（OPA K8s integration）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity → OPA authz）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>（dynamic credential policy）、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（scan → OPA gate）、<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（decision log → SIEM）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6 reliability</a>（CI pre-deploy gate 接 Conftest）、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 incident response</a>（policy violation alert → IR routing）</li>
<li>官方：<a href="https://www.openpolicyagent.org/">Open Policy Agent</a>、<a href="https://www.openpolicyagent.org/docs/latest/policy-language/">Rego Policy Language</a>、<a href="https://www.styra.com/">Styra DAS</a>、<a href="https://github.com/permitio/opal">OPAL</a></li>
</ul>
]]></content:encoded></item><item><title>Akamas</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/akamas/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/akamas/</guid><description>&lt;p>Akamas 的核心責任是把 workload、SLO constraint、runtime configuration 與雲端成本放進同一個最佳化迴圈。它適合 Kubernetes、VM、database、runtime 與雲端資源調校，重點在用實驗與約束條件產生 rightsizing、configuration tuning 與 capacity efficiency 建議。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Akamas 適合已經有可量測 workload 與成本壓力的服務。當團隊能說清楚 request rate、latency SLO、error budget、CPU / memory headroom、replica policy 與雲端費用目標，Akamas 可以把這些條件轉成 optimization objective，找出更好的配置組合。&lt;/p>
&lt;p>這個定位讓 Akamas 接到三個主章。它從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 接收 headroom 與 growth curve，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 接收 cost per request 與 cost curve，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop&lt;/a> 接收 test、profile、fix、re-test 的閉環。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Akamas 的核心定位是 &lt;em>AI-driven autonomous optimization&lt;/em>、不是 monitoring、不是 cost reporting、也不是手動 rightsizing 工具。它用 ML 在 &lt;em>parameter space&lt;/em> 中找出可同時降 cost 並達到 SLO 的配置組合、目標是把 &lt;em>效能調校&lt;/em> 從 expert-driven 手工活、轉成可重跑的工程實驗。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth&lt;/a> 這類 FinOps cost tool 的差異是 &lt;em>動作面&lt;/em>。FinOps tool 看到 &lt;em>cost 已經發生&lt;/em>、把帳單拆 tag、推薦保留方案；Akamas 看 workload 在 SLO 邊界下能不能跑得更便宜、輸出的是 &lt;em>configuration change&lt;/em>、不是 invoice 切片。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Datadog APM&lt;/a> / Prometheus 這類 observability stack 的差異是 &lt;em>決策面&lt;/em>。APM 告訴你 &lt;em>哪裡慢、哪個 endpoint p99 飆&lt;/em>；Akamas 接 APM / metrics 訊號當輸入、輸出 &lt;em>該怎麼改 JVM heap、HPA target、connection pool&lt;/em> 的 recommendation。Observability 是 &lt;em>看&lt;/em>、Akamas 是 &lt;em>動&lt;/em>。&lt;/p>
&lt;p>跟手動 tuning（SRE 拍腦袋、grid search、A/B configuration test）的差異是 &lt;em>參數空間規模&lt;/em>。Manual tuning 在 3-5 個參數還可控；JVM + container limit + HPA + DB pool + node packing 同時轉動時、組合爆炸、ML-driven search 才能在合理 budget 內收斂。&lt;/p></description><content:encoded><![CDATA[<p>Akamas 的核心責任是把 workload、SLO constraint、runtime configuration 與雲端成本放進同一個最佳化迴圈。它適合 Kubernetes、VM、database、runtime 與雲端資源調校，重點在用實驗與約束條件產生 rightsizing、configuration tuning 與 capacity efficiency 建議。</p>
<h2 id="定位">定位</h2>
<p>Akamas 適合已經有可量測 workload 與成本壓力的服務。當團隊能說清楚 request rate、latency SLO、error budget、CPU / memory headroom、replica policy 與雲端費用目標，Akamas 可以把這些條件轉成 optimization objective，找出更好的配置組合。</p>
<p>這個定位讓 Akamas 接到三個主章。它從 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 接收 headroom 與 growth curve，從 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 接收 cost per request 與 cost curve，從 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 接收 test、profile、fix、re-test 的閉環。</p>
<h2 id="服務定位">服務定位</h2>
<p>Akamas 的核心定位是 <em>AI-driven autonomous optimization</em>、不是 monitoring、不是 cost reporting、也不是手動 rightsizing 工具。它用 ML 在 <em>parameter space</em> 中找出可同時降 cost 並達到 SLO 的配置組合、目標是把 <em>效能調校</em> 從 expert-driven 手工活、轉成可重跑的工程實驗。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a> / <a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a> 這類 FinOps cost tool 的差異是 <em>動作面</em>。FinOps tool 看到 <em>cost 已經發生</em>、把帳單拆 tag、推薦保留方案；Akamas 看 workload 在 SLO 邊界下能不能跑得更便宜、輸出的是 <em>configuration change</em>、不是 invoice 切片。</p>
<p>跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Datadog APM</a> / Prometheus 這類 observability stack 的差異是 <em>決策面</em>。APM 告訴你 <em>哪裡慢、哪個 endpoint p99 飆</em>；Akamas 接 APM / metrics 訊號當輸入、輸出 <em>該怎麼改 JVM heap、HPA target、connection pool</em> 的 recommendation。Observability 是 <em>看</em>、Akamas 是 <em>動</em>。</p>
<p>跟手動 tuning（SRE 拍腦袋、grid search、A/B configuration test）的差異是 <em>參數空間規模</em>。Manual tuning 在 3-5 個參數還可控；JVM + container limit + HPA + DB pool + node packing 同時轉動時、組合爆炸、ML-driven search 才能在合理 budget 內收斂。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Akamas optimization study 是否健康、最少看四件事：</p>
<ul>
<li><strong>Agent / collector 部署完整度</strong>：哪些 target（JVM / container / K8s / DB）裝了 Akamas agent 或接到 metrics source、metrics window 是否涵蓋 representative peak、是否漏 tail latency 與 GC pause</li>
<li><strong>Target system 邊界定義</strong>：optimization 是針對單一 service / 一組 microservice / 整個 K8s cluster、tunable parameter list 是否經 service owner 審核、不在 list 內的參數是否會被間接影響</li>
<li><strong>Optimization goal 對得上 business outcome</strong>：goal 是「降 cost 30%」還是「同 SLO 下 cost minimize」、是否同時聲明 latency / error budget / throughput 的下界、避免 ML 為達 cost target 把 latency 推到邊緣</li>
<li><strong>Safety bound 緊 / 鬆的取捨</strong>：bound 太緊收斂不到方案、bound 太鬆 production validation 會出事、是否有 staging tenant 跑完再 promote、autopilot 範圍是否限定 non-critical workload</li>
</ul>
<p>四項任一缺、就是 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 邊界的待補項目、不是 Akamas 設定問題。</p>
<h2 id="適用場景">適用場景</h2>
<p>Kubernetes rightsizing 是 Akamas 的主要入口。多服務平台常見問題是 requests / limits、HPA target、replica floor、node pool 與 runtime 參數互相牽動；Akamas 的價值是把這些參數放進同一個優化空間，而非逐項手動調整。</p>
<p>Runtime 與 database tuning 適合需要穩定 SLO 的服務。JVM heap、Go runtime、PostgreSQL、MongoDB、Elasticsearch 或 Spark workload 會同時受配置、資料形狀與流量尖峰影響；optimization tool 可以用可重跑實驗保留調校證據。</p>
<p>FinOps 與 SRE 協作適合用 Akamas 建立共同語言。FinOps 關心浪費與預算，SRE 關心 latency、error rate 與可靠性；Akamas 類工具把節省幅度、性能風險與回退條件放在同一份 recommendation 裡，降低跨團隊溝通成本。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Akamas 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>優化目標</td>
          <td>把 cost、latency、throughput 與 SLO 一起建模</td>
          <td>明確 business objective 與風險上限</td>
      </tr>
      <tr>
          <td>參數空間</td>
          <td>支援 runtime、container、database 與雲端配置</td>
          <td>服務 owner 對參數語意的審核</td>
      </tr>
      <tr>
          <td>執行模式</td>
          <td>支援 human approval、pipeline 與自動化調校</td>
          <td>rollout guardrail、變更紀錄與回退</td>
      </tr>
      <tr>
          <td>證據保存</td>
          <td>recommendation 可以回寫實驗、約束與預期效益</td>
          <td>production validation 與長期 drift 追蹤</td>
      </tr>
  </tbody>
</table>
<p>優化目標價值來自約束透明。成本降低只有在 latency、availability 與 error budget 邊界內才成立，因此 Akamas 頁面要先問目標函數與 guardrail，再談節省幅度。</p>
<p>參數空間價值來自跨層調校。單看 CPU request 可能會誤判，因為 GC、DB connection、thread pool、replica policy 與 node packing 會一起改變 cost per request。</p>
<p>執行模式價值來自可控自動化。Human-in-the-loop 適合早期導入，pipeline mode 適合 release gate，autopilot 適合 guardrail、rollback 與 owner model 已成熟的環境。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Akamas 和 Vantage 的主要差異是控制面。Vantage 偏 cost visibility、allocation、forecast 與報表；Akamas 偏把效能約束放進 configuration optimization，適合需要直接調整 capacity 與 runtime 參數的場景。</p>
<p>Akamas 和 CloudHealth 的主要差異是操作層級。CloudHealth 偏 enterprise FinOps governance、policy、showback / chargeback 與多雲管理；Akamas 偏 service-level optimization 與工程調校閉環。</p>
<p>Akamas 和 AWS Cost Explorer 的主要差異是範圍與自動化。Cost Explorer 是 AWS-native 成本分析入口；Akamas 可以把成本訊號跟 workload、SLO 與配置實驗接起來，適合需要跨層優化的服務。</p>
<h2 id="操作成本">操作成本</h2>
<p>Akamas 的主要成本是 optimization model 建立。團隊要定義目標、約束、可調參數、測試窗口、流量代表性與成功門檻，並讓 service owner 審核每個 recommendation 的業務風險。</p>
<p>導入成本會隨自動化程度上升。早期可以用 approval workflow 接 recommendation；進入 pipeline 或 autopilot 後，要補 change window、deploy marker、rollback、SLO guardrail、audit log 與 incident handoff。</p>
<p>資料品質會直接影響結果可信度。Metric 延遲、缺少 tail latency、成本 tag 錯誤、workload window 偏差或測試環境差異，都會讓 recommendation 的 confidence 下降。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Akamas 結果應回寫到 optimization evidence package。最小欄位包括 optimization goal、constraint、tunable parameters、workload window、baseline cost、baseline performance、recommended configuration、expected saving、risk note、validation result 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Akamas 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>optimization report、experiment result、recommendation</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>workload sample、test window、production validation</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / metrics / cost dashboard / Akamas report</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>workload representativeness、metric freshness、tag coverage</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>SLO guardrail、repeatability、rollback readiness</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 cohort、未納入下游 quota、測試環境差異</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓成本調校可以被審查。Akamas recommendation 要能回答「節省來自哪個配置變更、哪個 SLO 保護這次變更、哪個訊號觸發回退」。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Akamas（AI optimization）</th>
          <th>FinOps tool（Vantage / CloudHealth）</th>
          <th>APM（Datadog / Prometheus）</th>
          <th>Manual tuning（SRE / 性能工程師）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要動作</td>
          <td>產出 configuration change recommend</td>
          <td>拆帳單、報表、保留方案推薦</td>
          <td>顯示瓶頸位置與 metric</td>
          <td>拍腦袋 / grid search / A/B test</td>
      </tr>
      <tr>
          <td>決策訊號</td>
          <td>workload + SLO + cost 同模型</td>
          <td>帳單 + tag</td>
          <td>latency / saturation / error metric</td>
          <td>經驗 + ad-hoc benchmark</td>
      </tr>
      <tr>
          <td>適用參數空間</td>
          <td>多參數（JVM + container + HPA + DB）</td>
          <td>N/A（不動參數）</td>
          <td>N/A（不動參數）</td>
          <td>3-5 個參數還可控</td>
      </tr>
      <tr>
          <td>自動化程度</td>
          <td>human approval / pipeline / autopilot</td>
          <td>recommendation + dashboard、不自動執行</td>
          <td>alert + dashboard</td>
          <td>全人工</td>
      </tr>
      <tr>
          <td>風險邊界</td>
          <td>靠 safety bound + staging validation</td>
          <td>低（只動 commitment、不動 runtime）</td>
          <td>低（觀察、不動）</td>
          <td>靠人盯、容易遺漏 cross-parameter</td>
      </tr>
      <tr>
          <td>何時不適用</td>
          <td>參數空間小 / SLO 未明確 / metric 不全</td>
          <td>需要動 runtime 才能省的場景</td>
          <td>不解決「改什麼」、只解決「在哪裡」</td>
          <td>參數爆炸時 ROI 太差</td>
      </tr>
  </tbody>
</table>
<p>選 Akamas 的核心訴求是 <em>參數空間大 + workload 可重跑 + cost 壓力夠高、值得投入 optimization study setup 成本</em>。小規模 / 參數少 / SLO 不明、直接走 manual tuning 更快；只想看帳單拆解、走 FinOps tool；只想知道哪裡慢、走 APM。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Optimization study 的三要素</strong>：goal（目標函數、常見 <code>minimize cost subject to p99 latency &lt; X, error rate &lt; Y</code>）、parameter list（哪些 knob 可動、各自合法區間）、safety bound（哪些 metric 不能越界、越界即 reject candidate）。study setup 是 Akamas 最重的人力投入、value 來自 <em>把隱性調校 know-how 寫成可重跑配置</em>、不是 ML 本身。</p>
<p><strong>Live experiment vs offline study</strong>：offline study 用 staging 環境跑代表性 workload、安全但與 production 流量結構有偏差；live experiment 在 production 上小範圍試 candidate（例如 single canary pod）、訊號真實但需要嚴格 safety bound 與 rollback。多數團隊先 offline 找候選 region、再 live 收斂 — 不要一開始就 production autopilot。</p>
<p><strong>跟 K8s VPA / HPA 互補不互斥</strong>：HPA 處理 <em>replica 數量</em>、VPA 處理 <em>單 pod request / limit</em>、Akamas 處理 <em>參數組合 + 跨層協同</em>（含 JVM heap、HPA target、replica floor、node pool selection）。三者並用時要明確分工 — Akamas 不該跟 VPA 同時調 request，否則彼此推翻；常見作法是 Akamas 設 <em>baseline configuration</em>、VPA / HPA 在 baseline 上做即時微調。</p>
<p><strong>跟 observability stack integration</strong>：Akamas 接 Datadog / Prometheus / New Relic / Dynatrace 取 metrics、接 Kubernetes API 取 workload state、接 cloud billing API 取 cost。integration 品質直接決定 recommendation 信度 — metric 缺 tail latency 或 cost tag 不準、ML 會找到 <em>看起來省、實際出事</em> 的配置。對應 <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.4 Performance Observability</a> 的訊號治理。</p>
<p><strong>安全邊界 — 不該全 autopilot production</strong>：critical workload（payment / auth / DB primary）即使 SLO bound 寫清楚也不該 autopilot、recommendation 要走 human approval + change window；non-critical workload（batch job / dev cluster / internal tool）autopilot 可接受。ML black-box 是 production safety 的本質風險、不是設定問題。</p>
<p><strong>ML 黑箱可解釋性</strong>：Akamas recommendation 給出 <em>why this configuration</em> 的 sensitivity analysis（哪個參數影響最大、哪個參數對 cost / latency 是 trade-off curve），但根因解釋仍弱於人類性能工程師的 mental model。Production 採用前、service owner 要能用自己的 domain knowledge 對 recommendation 做 sanity check、不是純靠 ML score 拍板。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Optimization goal 對不上 business outcome</strong>：goal 寫「降 cost 30%」但沒寫 latency / error budget 下界 — ML 把 cost 壓到 SLO 邊緣、production 上線就 incident、回頭補 safety bound + business KPI alignment</li>
<li><strong>Safety bound 太鬆 / 太緊</strong>：太鬆 candidate 過 staging 但 production validation 出事、太緊 study 跑不出有意義方案 — bound 應綁 production-observed p99 / error rate baseline + 20% 緩衝、不是拍數字</li>
<li><strong>ML black-box 沒辦法解釋</strong>：service owner 看不懂為何 recommendation 改某個 obscure JVM flag — 跑 sensitivity analysis、不接受 <em>無 domain rationale</em> 的 recommendation、視為 candidate 而非 final</li>
<li><strong>參數空間 leak 到 list 外</strong>：Akamas 改 JVM heap 但間接讓 GC 行為變、撞到沒納入的 thread pool — 補 cross-parameter dependency 到 list、或縮小 study scope</li>
<li><strong>Workload window 不代表 production</strong>：staging 跑 50% 流量、ML 找到的方案在 100% peak hour 出事 — workload sample 必須涵蓋 representative peak、不是平均值</li>
<li><strong>Autopilot 推到 critical service</strong>：non-critical workload 試出甜頭、團隊把 autopilot 推到 payment service、incident 後 rollback 困難 — autopilot 範圍要寫進政策、critical service 永遠 human approval</li>
<li><strong>Recommendation 跟 VPA 互推</strong>：Akamas 設 request = X、VPA 立刻調回 Y、循環 — Akamas baseline 跟 VPA scope 要分層、不要在同一個 dimension 兩個 controller 同時動</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Akamas 目前在 09 案例庫中適合作為 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的工具承接點。它可回寫到 <a href="/blog/backend/09-performance-capacity/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 TiDB → DynamoDB 遷移</a> 的成本下降 50% 取捨、<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 EKS cluster</a> 的年省 1000 萬美金的 Kubernetes capacity 調校、<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> 的營運成本下降 30%、以及 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech 體育博彩</a> 的需求降低時成本下降 25% 彈性曲線。</p>
<p>這些案例的重點是優化條件。Akamas 頁引用案例時，應把「某公司節省成本」轉成 workload window、SLO constraint、調整參數、驗證方式與回退條件 — 例如 Zomato 的 4x throughput / 90% latency 改善是同時優化目標、不是只看成本欄位。</p>
<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/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a></li>
<li>官方：<a href="https://docs.akamas.io/akamas-docs/getting-started/introduction-to-akamas">Akamas documentation</a></li>
</ul>
]]></content:encoded></item><item><title>GoReplay</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/goreplay/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/goreplay/</guid><description>&lt;p>GoReplay 的核心責任是捕捉 production HTTP traffic，並把真實請求形狀重播到 staging、shadow environment 或新版本。它適合驗證 synthetic load 難以建模的 endpoint mix、header、payload size、burst pattern 與 long-tail 行為，重點在把 production reality 轉成可控 replay artifact。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>GoReplay 適合在 synthetic workload 可信度偏低時使用。當 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 很難準確描述使用者路徑、payload 分布或 endpoint mix，GoReplay 可以從 production traffic 擷取真實樣本，再用 rate limit、filter、rewrite 與 output target 控制重播範圍。&lt;/p>
&lt;p>這個定位讓 GoReplay 接到 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 的 shadow traffic。它的價值在於保留 production 請求形狀；它的風險在於 PII、credential、side effect、下游容量與 capture host overhead 都要被治理。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter&lt;/a> 的 synthetic load 設計 mindset 完全不同。Scripted load 假設 &lt;em>測試者能描述使用者行為&lt;/em> — 寫 script、設 rate、跑 scenario；GoReplay 假設 &lt;em>production 才是 source of truth&lt;/em> — endpoint mix、header 分布、payload size、burst pattern 都從真實 traffic 抽樣、不靠人為建模。對 long-tail 行為（少見 endpoint、巨大 payload、特殊 header 組合）這個差異決定了 capacity 規劃的真實度。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 GoReplay deployment 是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Capture mode&lt;/strong>：用 &lt;code>raw&lt;/code> (libpcap-based)、&lt;code>pcap-file&lt;/code>（離線 replay 已存檔的 pcap）、&lt;code>file&lt;/code>（GoReplay 原生 gor format）哪一種？raw 對 production host 有 CPU / network overhead、pcap-file 適合事後 replay、file 適合 long-running capture buffer&lt;/li>
&lt;li>&lt;strong>Replay target&lt;/strong>：打到 staging full-stack、shadow service、還是 isolated sandbox？POST / PUT / DELETE 是否導到 dry-run path 或 idempotent mock？webhook / payment / notification 是否被攔截？&lt;/li>
&lt;li>&lt;strong>Rate adjustment&lt;/strong>：用原始 production rate replay，還是 2x / 10x / 0.1x？capacity 規劃通常需要 &lt;em>speed up&lt;/em> 來測未來流量、debug 通常需要 &lt;em>slow down&lt;/em> 跟單一請求追查&lt;/li>
&lt;li>&lt;strong>Middleware filter&lt;/strong>：PII / token / cookie / credential redaction 在哪一段做（capture 前、capture 後、replay 前）？是否走 GoReplay middleware binary（stdin / stdout pipeline）統一處理&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>架構遷移驗證適合 GoReplay。DB、cache、search、API gateway 或 framework 重寫時，可以把真實 HTTP traffic replay 到新路徑，觀察 latency、error、resource saturation 與 response diff。&lt;/p></description><content:encoded><![CDATA[<p>GoReplay 的核心責任是捕捉 production HTTP traffic，並把真實請求形狀重播到 staging、shadow environment 或新版本。它適合驗證 synthetic load 難以建模的 endpoint mix、header、payload size、burst pattern 與 long-tail 行為，重點在把 production reality 轉成可控 replay artifact。</p>
<h2 id="定位">定位</h2>
<p>GoReplay 適合在 synthetic workload 可信度偏低時使用。當 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 很難準確描述使用者路徑、payload 分布或 endpoint mix，GoReplay 可以從 production traffic 擷取真實樣本，再用 rate limit、filter、rewrite 與 output target 控制重播範圍。</p>
<p>這個定位讓 GoReplay 接到 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 的 shadow traffic。它的價值在於保留 production 請求形狀；它的風險在於 PII、credential、side effect、下游容量與 capture host overhead 都要被治理。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> / <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a> 的 synthetic load 設計 mindset 完全不同。Scripted load 假設 <em>測試者能描述使用者行為</em> — 寫 script、設 rate、跑 scenario；GoReplay 假設 <em>production 才是 source of truth</em> — endpoint mix、header 分布、payload size、burst pattern 都從真實 traffic 抽樣、不靠人為建模。對 long-tail 行為（少見 endpoint、巨大 payload、特殊 header 組合）這個差異決定了 capacity 規劃的真實度。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 GoReplay deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Capture mode</strong>：用 <code>raw</code> (libpcap-based)、<code>pcap-file</code>（離線 replay 已存檔的 pcap）、<code>file</code>（GoReplay 原生 gor format）哪一種？raw 對 production host 有 CPU / network overhead、pcap-file 適合事後 replay、file 適合 long-running capture buffer</li>
<li><strong>Replay target</strong>：打到 staging full-stack、shadow service、還是 isolated sandbox？POST / PUT / DELETE 是否導到 dry-run path 或 idempotent mock？webhook / payment / notification 是否被攔截？</li>
<li><strong>Rate adjustment</strong>：用原始 production rate replay，還是 2x / 10x / 0.1x？capacity 規劃通常需要 <em>speed up</em> 來測未來流量、debug 通常需要 <em>slow down</em> 跟單一請求追查</li>
<li><strong>Middleware filter</strong>：PII / token / cookie / credential redaction 在哪一段做（capture 前、capture 後、replay 前）？是否走 GoReplay middleware binary（stdin / stdout pipeline）統一處理</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p>架構遷移驗證適合 GoReplay。DB、cache、search、API gateway 或 framework 重寫時，可以把真實 HTTP traffic replay 到新路徑，觀察 latency、error、resource saturation 與 response diff。</p>
<p>Long-tail workload 校正適合 GoReplay。Synthetic scenario 通常覆蓋主路徑，GoReplay 可以揭露少見 endpoint、特殊 header、巨大 payload、冷門 tenant 與尖峰 cohort。</p>
<p>事故後修補驗證適合 GoReplay。若事故由特定請求形狀觸發，capture sample 可以在修補環境重播，確認 latency、error 或 resource usage 是否回到可接受範圍。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>GoReplay 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>真實 traffic</td>
          <td>endpoint mix、payload、header 分布接近 production</td>
          <td>PII / credential 遮罩與權限治理</td>
      </tr>
      <tr>
          <td>HTTP replay</td>
          <td>對 HTTP API 路徑直接有效</td>
          <td>非 HTTP protocol 與加密流量處理</td>
      </tr>
      <tr>
          <td>Filter / rewrite</td>
          <td>可控制 host、path、header、rate</td>
          <td>side effect 隔離與 sandbox target</td>
      </tr>
      <tr>
          <td>Capture artifact</td>
          <td>可保存樣本做回歸驗證</td>
          <td>retention、存取控制與樣本代表性</td>
      </tr>
  </tbody>
</table>
<p>真實 traffic 價值來自分布保真。它能捕捉 synthetic script 容易漏掉的 query parameter、header、payload size 與 endpoint mix，但 capture sample 也會帶入 production 資料治理責任。</p>
<p>Filter / rewrite 價值來自安全邊界。Replay 前要改寫 target、移除 credential、遮罩 PII、限制 rate，並把寫入類請求導到 sandbox 或 dry-run path。</p>
<h2 id="跟其他方式的取捨">跟其他方式的取捨</h2>
<p>GoReplay 和 k6 / Gatling / Locust 的主要差異是流量來源。GoReplay 取 production sample，保真度高；scripted load test 取人工模型，可控性高。</p>
<p>GoReplay 和 service mesh mirroring 的主要差異是部署位置。GoReplay 在 host / network capture 層工作，適合沒有 mesh 的服務；service mesh mirroring 在 sidecar / proxy 層工作，適合已經落地 mesh 的平台。</p>
<p>GoReplay 和 AWS VPC Traffic Mirroring 的主要差異是應用語意。GoReplay 對 HTTP replay 更直接；VPC Traffic Mirroring 在網路層複製封包，侵入性低但應用層 rewrite、遮罩與 replay 控制需要額外處理。</p>
<h3 id="核心取捨表">核心取捨表</h3>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>GoReplay</th>
          <th>k6 / JMeter (synthetic)</th>
          <th>AWS VPC Traffic Mirroring</th>
          <th>Service Mesh Mirroring</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>流量來源</td>
          <td>Production sniff（real shape）</td>
          <td>Scripted scenario（builder&rsquo;s model）</td>
          <td>VPC 網路層封包複製</td>
          <td>Sidecar / proxy 層複製</td>
      </tr>
      <tr>
          <td>工作層級</td>
          <td>HTTP / L7（capture host）</td>
          <td>HTTP / L7（client-side script）</td>
          <td>L3-L4（packet level）</td>
          <td>L7（sidecar in-mesh）</td>
      </tr>
      <tr>
          <td>Rate adjust</td>
          <td>原生支援（0.1x - 10x）</td>
          <td>scenario 內 ramp / arrival rate</td>
          <td>全量、無 rate control</td>
          <td>mesh policy 控制</td>
      </tr>
      <tr>
          <td>Replay 控制</td>
          <td>filter / rewrite / middleware binary</td>
          <td>程式內 logic 完整可控</td>
          <td>需自寫 application-level rewriter</td>
          <td>mesh-level routing rule</td>
      </tr>
      <tr>
          <td>Long-tail 覆蓋</td>
          <td>強（real distribution）</td>
          <td>弱（取決於 scenario design）</td>
          <td>強（real distribution）但需後處理</td>
          <td>強（in-mesh real traffic）</td>
      </tr>
      <tr>
          <td>PII / 安全成本</td>
          <td>高（middleware 自己寫 redaction）</td>
          <td>低（fixture 由人控制）</td>
          <td>高（packet-level 難語意化遮罩）</td>
          <td>中（mesh policy 可協助）</td>
      </tr>
      <tr>
          <td>部署條件</td>
          <td>host agent + libpcap，需有權限 sniff interface</td>
          <td>無（client / load generator 機台即可）</td>
          <td>AWS-only、ENI mirroring 配額</td>
          <td>已落地 mesh（Istio / Linkerd）</td>
      </tr>
  </tbody>
</table>
<p>選 GoReplay 的核心訴求：<em>HTTP 應用層 replay + production shape 保真 + 沒落地 mesh</em>；若已用 mesh、優先看 mesh 內建 mirroring；若要跨 protocol（gRPC / 自家 binary）GoReplay 開源版受限、需考慮 Pro 版或 mesh 方案。</p>
<h2 id="操作成本">操作成本</h2>
<p>GoReplay 的主要成本是資料安全。Production request 可能包含 token、cookie、PII、payment payload、internal IDs 與 tenant 資料，capture、保存、重播與刪除都要有明確 owner。</p>
<p>Replay 成本來自下游副作用。POST、PUT、DELETE、webhook、email、payment、notification 與 queue publish 都要導到 sandbox、mock 或 idempotent dry-run，避免 replay 造成重複交易或通知。</p>
<p>Capture 成本來自主機資源。高流量服務上的 capture agent 會消耗 CPU、network 與 disk，正式啟用前要先量測 overhead，並設定 sampling、rate limit 與 stop condition。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>GoReplay 結果應回寫到 evidence package。最小欄位包括 capture source、capture time range、filter / rewrite rule、sample size、replay rate、target environment、data masking status、p95 / p99、error rate、resource saturation、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>GoReplay 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>capture command、sample hash、replay command</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>capture start / end、replay start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>APM / metrics / logs / diff 查詢連結</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>sample representativeness、masking status</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>replay rate、target parity、capture coverage</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未捕捉 protocol、資料遮罩限制、sandbox 差異</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 replay 結論可審查。Reviewer 要能知道樣本來自哪段 production、經過哪些 filter、打到哪個 target，以及哪些 side effect 被 mock 或隔離。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Capture to file（pcap-like artifact）</strong>：用 <code>--output-file</code> 把 capture 寫成 GoReplay 原生 gor file（或讀 pcap）、之後用 <code>--input-file</code> 重複 replay。這個模式讓 <em>capture window</em> 跟 <em>replay run</em> 解耦 — capture 一次，可在不同 staging branch / 不同 rate / 不同 target 重播多次。對 regression 驗證跟「事故當時的 traffic shape」回放特別關鍵、但 file artifact 也成為 PII 儲存物、retention 跟存取控制要跟 production log 同級。</p>
<p><strong>Replay with rate adjustment（10x speed）</strong>：<code>--input-file-replay-speed 10</code>（gor format）或加 <code>--input-file-loop</code> 反覆播放。10x speed 對 capacity headroom 驗證直接有用 — 用真實 traffic shape 模擬「未來流量翻 10 倍」、避開 scripted scenario 自帶的人為偏差。反向用法 0.1x 跟 isolated request replay 適合排錯特定 endpoint 的 long-tail latency。注意 10x 會把下游 DB / cache / external API 同樣放大，sandbox target 容量要先評估。</p>
<p><strong>Middleware filter（PII redaction）</strong>：GoReplay middleware 是獨立 binary、用 stdin / stdout 跟 GoReplay process 串接、可寫任何語言。典型責任：JSON body 解析、Authorization / Cookie / Set-Cookie header strip、Email / phone / card number regex 遮罩、cross-request session ID rewriting（讓 staging 不撞 production session）。middleware 邏輯本身需要 code review、寫進版控、staging 測過再放到 production capture host。</p>
<p><strong>Pro version（GoReplay Pro - binary protocols）</strong>：開源版聚焦 HTTP/1.x；GoReplay Pro 支援 binary protocol（自家 protocol、protobuf-over-TCP、部分 gRPC pattern）跟 enterprise 維護 SLA。判斷點：若服務是純 HTTP REST 開源版夠用、若有 gRPC 或自家 binary 且不在 mesh 內、要評估 Pro 或改走 service mesh mirroring。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Capture loss / sample 不完整</strong>：libpcap 在高流量下會 drop packet、<code>gor stat</code> 的 capture stats 顯示 drop &gt; 1% 就不可信 — 加 capture host CPU、改用 PF_RING / AF_PACKET、或縮 capture filter 範圍（只 capture target port + sampling）</li>
<li><strong>TCP reassembly 失敗 / replay 結果亂碼</strong>：跨 packet 的 HTTP body 沒被正確組裝、常見於 MTU / TCP segment offload 設定異常 — 確認 capture interface 沒開 TSO / GRO、或用 application-level capture（HEC-style sidecar）取代 packet capture</li>
<li><strong>PII / secret 漏 redact 進 staging</strong>：middleware 規則沒覆蓋新加的 header / 新的 body schema — 建立 redaction allowlist（只放行已知 schema）而非 denylist、每次 schema 變更同步更新 middleware、staging 入口加 secret scanner 做 last-mile 攔截</li>
<li><strong>Replay 觸發下游真實副作用</strong>：POST / PUT 沒導 sandbox、webhook 真的打出去、payment 真的扣款 — replay target 預設 <em>deny all write</em>、白名單放行特定 idempotent endpoint、其餘走 mock 或 dry-run flag</li>
<li><strong>Replay rate 拖垮 capture host</strong>：同機 capture + replay、CPU / NIC 互相搶 — capture host 只負責 sniff + write to file、replay 機器獨立、用 gor file 解耦</li>
<li><strong>長時間 capture 寫爆 disk</strong>：未設 rotation 或 size limit — <code>--output-file</code> 加 size / time rotation、定期 archive 到 S3 + 過期刪除</li>
<li><strong>Staging 容量比 production 小、放大流量打爆</strong>：10x replay 沒先估下游 — capacity 規劃前先用 1x 暖機、觀察下游 saturation、再 ramp 到目標倍率</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>GoReplay 適合回寫 migration 與 production validation 案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 售票壓測</a> 的 production-shaped load、<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek waiting room</a> 的 cutover 前 replay、<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 consolidation</a> 這類資料庫整併前的 query pattern 驗證、<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 TiDB → DynamoDB</a> 跨 DB 遷移的請求 pattern 重播，以及 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365 MongoDB → Cosmos DB</a> 的全球分析平台遷移 query 驗證。</p>
<p>這些案例的重點是 production request shape。GoReplay 頁引用案例時，要把 case 轉成 capture window、filter、rewrite、target isolation、rate limit 與 diff / saturation metric — 例如 Zomato 遷 DB 時、replay 必須先 mask PII + 改寫 SQL 方言、不能直接把 TiDB query 打進 DynamoDB SDK。</p>
<p>Capacity 規劃用 real workload model 是這些案例的共通對照啟示。Tixcraft 的售票 spike、SeatGeek 的 waiting room cutover、Netflix 的 Aurora 整併、Microsoft 365 的全球 query 分布 — 共通點是 <em>scripted scenario 無法事先列舉所有 endpoint 跟 payload 組合</em>。GoReplay 的回應是把「使用者行為建模」這個工作丟回給 production traffic 本身、規劃者只負責決定 capture window、replay rate 跟 target boundary，不再試圖窮舉 scenario。這個 mindset 才是 GoReplay 跟 <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> / <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a> 在 capacity 規劃流程中的真正分工點。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring</a></li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li>官方：<a href="https://docs.goreplay.org/">GoReplay documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/</guid><description>&lt;p>這個案例的核心責任是提供「同樣業務需求、不同 DB 技術」的具體對照數字。Zomato 帳單系統從 TiDB 遷移到 DynamoDB、留下三個關鍵改善百分比、是 DB 選型決策的少見 &lt;em>可量化&lt;/em> 對照樣本。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Zomato 帳單系統遷移的關鍵數字（引自 &lt;a href="https://aws.amazon.com/blogs/database/unlocking-performance-scalability-and-cost-efficiency-of-zomatos-billing-platform-by-switching-from-tidb-to-dynamodb/">AWS Database Blog&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>TiDB（遷移前）&lt;/th>
 &lt;th>DynamoDB（遷移後）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>微服務吞吐&lt;/td>
 &lt;td>2,000 RPM&lt;/td>
 &lt;td>8,000 RPM（4x）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲降幅&lt;/td>
 &lt;td>baseline&lt;/td>
 &lt;td>-90%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本降幅&lt;/td>
 &lt;td>baseline&lt;/td>
 &lt;td>-50%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>每日事件量&lt;/td>
 &lt;td>10M（共用）&lt;/td>
 &lt;td>10M&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>餐廳合作夥伴&lt;/td>
 &lt;td>350,000+&lt;/td>
 &lt;td>350,000+&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵動機：TiDB 必須為「突發流量峰值」提前 over-provision、付出常態成本；DynamoDB on-demand 模式「pay only for what we use」、避免 over-provisioning。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Zomato 遷移揭露三個 DB 選型決策的判讀重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>NewSQL vs NoSQL 的取捨不只是 schema&lt;/strong>：TiDB 提供 SQL 介面跟 ACID、DynamoDB 提供 KV 介面跟最終一致性。Zomato 選 DynamoDB 是判斷「帳單事件本身可以接受 eventually consistent」、用一致性換性能跟成本。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a> 的一致性取捨。&lt;/li>
&lt;li>&lt;strong>TiDB 必須 over-provision 是分散式 SQL 的常態&lt;/strong>：分散式 SQL 為了支援跨節點交易、必須有預留容量、否則峰值會出現 leader election storm 或 follower lag。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner&lt;/a> 的「節點數即容量」是同類取捨、Spanner 也必須預先 scale 節點。&lt;/li>
&lt;li>&lt;strong>2K → 8K RPM 是 4 倍、但延遲降 90% 才是真關鍵&lt;/strong>：吞吐改善可能來自架構優化、延遲改善才是 DB 本質差。從 baseline → 10% 通常代表少了 1-2 個 hop（例如 cross-region replication、coordinator round-trip）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為&lt;/a> 的 Little&amp;rsquo;s Law。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>「成本降 50%」是 &lt;em>當下流量下的對照&lt;/em>。如果未來流量繼續成長、DynamoDB 的 cost-per-request 成長率比 TiDB 自管 cluster 高 — 達到某規模後 TiDB 反而更便宜。讀遷移案例要看「在當下流量下划算」、不等於「永遠划算」。&lt;/li>
&lt;li>「90% 延遲降」可能只是 p50、p99 / p999 改善幅度通常較小。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>DB 遷移前先確認業務一致性需求&lt;/strong>：能接受 eventually consistent 的工作負載適合 KV / NoSQL；必須 strong consistency 的工作負載必須 SQL / NewSQL。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遷移評估要看「總成本曲線」、不是「當下 snapshot」&lt;/strong>：算未來 12-24 個月在預期流量下的成本對照、不是只算現在。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遷移過程要 dual-write + shadow read 驗證&lt;/strong>：避免新舊系統行為不一致導致業務問題。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence&lt;/a>。&lt;/li>
&lt;li>&lt;strong>on-demand vs provisioned 的選擇與業務流量形狀對應&lt;/strong>：突發流量適合 on-demand、可預測流量適合 provisioned。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a> 的 on-demand 應用。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：MongoDB Atlas → DynamoDB、Cassandra → DynamoDB、PostgreSQL → Aurora、CockroachDB → Spanner 都是常見遷移路徑。每條路徑的取捨類似。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「同樣業務需求、不同 DB 技術」的具體對照數字。Zomato 帳單系統從 TiDB 遷移到 DynamoDB、留下三個關鍵改善百分比、是 DB 選型決策的少見 <em>可量化</em> 對照樣本。</p>
<h2 id="觀察">觀察</h2>
<p>Zomato 帳單系統遷移的關鍵數字（引自 <a href="https://aws.amazon.com/blogs/database/unlocking-performance-scalability-and-cost-efficiency-of-zomatos-billing-platform-by-switching-from-tidb-to-dynamodb/">AWS Database Blog</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>TiDB（遷移前）</th>
          <th>DynamoDB（遷移後）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>微服務吞吐</td>
          <td>2,000 RPM</td>
          <td>8,000 RPM（4x）</td>
      </tr>
      <tr>
          <td>延遲降幅</td>
          <td>baseline</td>
          <td>-90%</td>
      </tr>
      <tr>
          <td>成本降幅</td>
          <td>baseline</td>
          <td>-50%</td>
      </tr>
      <tr>
          <td>每日事件量</td>
          <td>10M（共用）</td>
          <td>10M</td>
      </tr>
      <tr>
          <td>餐廳合作夥伴</td>
          <td>350,000+</td>
          <td>350,000+</td>
      </tr>
  </tbody>
</table>
<p>關鍵動機：TiDB 必須為「突發流量峰值」提前 over-provision、付出常態成本；DynamoDB on-demand 模式「pay only for what we use」、避免 over-provisioning。</p>
<h2 id="判讀">判讀</h2>
<p>Zomato 遷移揭露三個 DB 選型決策的判讀重點。</p>
<ol>
<li><strong>NewSQL vs NoSQL 的取捨不只是 schema</strong>：TiDB 提供 SQL 介面跟 ACID、DynamoDB 提供 KV 介面跟最終一致性。Zomato 選 DynamoDB 是判斷「帳單事件本身可以接受 eventually consistent」、用一致性換性能跟成本。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的一致性取捨。</li>
<li><strong>TiDB 必須 over-provision 是分散式 SQL 的常態</strong>：分散式 SQL 為了支援跨節點交易、必須有預留容量、否則峰值會出現 leader election storm 或 follower lag。這跟 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> 的「節點數即容量」是同類取捨、Spanner 也必須預先 scale 節點。</li>
<li><strong>2K → 8K RPM 是 4 倍、但延遲降 90% 才是真關鍵</strong>：吞吐改善可能來自架構優化、延遲改善才是 DB 本質差。從 baseline → 10% 通常代表少了 1-2 個 hop（例如 cross-region replication、coordinator round-trip）。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為</a> 的 Little&rsquo;s Law。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「成本降 50%」是 <em>當下流量下的對照</em>。如果未來流量繼續成長、DynamoDB 的 cost-per-request 成長率比 TiDB 自管 cluster 高 — 達到某規模後 TiDB 反而更便宜。讀遷移案例要看「在當下流量下划算」、不等於「永遠划算」。</li>
<li>「90% 延遲降」可能只是 p50、p99 / p999 改善幅度通常較小。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>DB 遷移前先確認業務一致性需求</strong>：能接受 eventually consistent 的工作負載適合 KV / NoSQL；必須 strong consistency 的工作負載必須 SQL / NewSQL。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a>。</li>
<li><strong>遷移評估要看「總成本曲線」、不是「當下 snapshot」</strong>：算未來 12-24 個月在預期流量下的成本對照、不是只算現在。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>。</li>
<li><strong>遷移過程要 dual-write + shadow read 驗證</strong>：避免新舊系統行為不一致導致業務問題。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence</a>。</li>
<li><strong>on-demand vs provisioned 的選擇與業務流量形狀對應</strong>：突發流量適合 on-demand、可預測流量適合 provisioned。對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的 on-demand 應用。</li>
</ol>
<p>跨平台等效：MongoDB Atlas → DynamoDB、Cassandra → DynamoDB、PostgreSQL → Aurora、CockroachDB → Spanner 都是常見遷移路徑。每條路徑的取捨類似。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想做 DB 遷移評估 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a></li>
<li>想理解一致性取捨 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> + <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想做總成本評估 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
<li>對照其他 DB 遷移 → <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 的容量規劃成本">9.C9 Spotify Kafka→Pub/Sub</a></li>
<li>想拆 access pattern 對應的 DynamoDB schema → <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a> + <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a></li>
<li>想評估搬遷後的 capacity mode → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/database/unlocking-performance-scalability-and-cost-efficiency-of-zomatos-billing-platform-by-switching-from-tidb-to-dynamodb/">Unlocking performance, scalability, and cost-efficiency of Zomato&rsquo;s Billing Platform by switching from TiDB to DynamoDB</a></li>
<li><a href="https://aws.amazon.com/blogs/opensource/how-zomato-boosted-performance-25-and-cut-compute-cost-30-migrating-trino-and-druid-workloads-to-aws-graviton/">How Zomato Boosted Performance 25% and Cut Compute Cost 30% Migrating Trino and Druid Workloads to AWS Graviton</a></li>
</ul>
]]></content:encoded></item><item><title>4.20 Observability Evidence Package</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>evidence package 的責任：把分散的 observability 資料包成可交給 reliability 與 incident response 的證據&lt;/li>
&lt;li>資料來源：log、metric、trace、audit log、dashboard、query、client-side signal、deployment event&lt;/li>
&lt;li>欄位：source、time range、owner、query link、data quality、confidence、known gap、retention&lt;/li>
&lt;li>跟 4.17 的關係：telemetry data quality 提供資料限制，evidence package 提供交接格式&lt;/li>
&lt;li>跟 6.23 的關係：可靠性驗證使用同一格式保存 experiment evidence&lt;/li>
&lt;li>跟 8.18 / 8.19 的關係：事故 intake 與 decision log 使用同一組 evidence link&lt;/li>
&lt;li>反模式：只貼 dashboard 截圖；query 沒有時間窗；evidence 沒標示 sampling / freshness 限制&lt;/li>
&lt;/ul>
&lt;p>Observability &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 的核心是把可觀測資料從「查詢結果」升級成「可交接證據」。事故與驗證需要一組能說明來源、時間窗、可信度、限制與 owner 的 evidence。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability evidence package 是可觀測性模組交給可靠性驗證與事故處理的證據包，責任是讓 log、metric、trace 與 audit log 能被重用、回放與復盤。&lt;/p>
&lt;p>這一頁處理的是交接格式。4.17 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a> 說明資料是否可信；evidence package 說明如何把可信度、查詢入口與限制一起交給下游流程。&lt;/p>
&lt;p>證據包的價值在於保存判讀上下文。只有截圖時，讀者看不到 query、時間窗、sampling、資料延遲與 owner；有 evidence package 時，後續 release gate、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 與 post-incident review 才能回放同一組事實。&lt;/p>
&lt;h2 id="evidence-欄位">Evidence 欄位&lt;/h2>
&lt;p>Evidence 欄位的責任是讓每個觀測證據都可查、可解釋、可追蹤。欄位不需要複雜，但要覆蓋事中判讀與事後復盤的最小需求。&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>Source&lt;/td>
 &lt;td>標示資料來源&lt;/td>
 &lt;td>區分 log、metric、trace、audit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range&lt;/a>&lt;/td>
 &lt;td>標示查詢時間窗&lt;/td>
 &lt;td>對齊 incident timeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link&lt;/a>&lt;/td>
 &lt;td>保留可重跑查詢&lt;/td>
 &lt;td>支援 handoff 與復盤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>指定可解釋資料的人&lt;/td>
 &lt;td>避免 evidence 失去語意&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality&lt;/a>&lt;/td>
 &lt;td>標示 completeness / freshness&lt;/td>
 &lt;td>防止資料限制被誤讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence&lt;/a>&lt;/td>
 &lt;td>標示 confirmed / suspected&lt;/td>
 &lt;td>支援分級與決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap&lt;/a>&lt;/td>
 &lt;td>標示 missing signal 或 drift&lt;/td>
 &lt;td>回寫 04 readiness 與 data quality&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>標示保存期限&lt;/td>
 &lt;td>支援 audit、PIR 與長事故&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Source 欄位讓讀者知道 evidence 的能力邊界。Metric 適合看趨勢，log 適合看事件細節，trace 適合看路徑，audit log 適合看責任鏈。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>evidence package 的責任：把分散的 observability 資料包成可交給 reliability 與 incident response 的證據</li>
<li>資料來源：log、metric、trace、audit log、dashboard、query、client-side signal、deployment event</li>
<li>欄位：source、time range、owner、query link、data quality、confidence、known gap、retention</li>
<li>跟 4.17 的關係：telemetry data quality 提供資料限制，evidence package 提供交接格式</li>
<li>跟 6.23 的關係：可靠性驗證使用同一格式保存 experiment evidence</li>
<li>跟 8.18 / 8.19 的關係：事故 intake 與 decision log 使用同一組 evidence link</li>
<li>反模式：只貼 dashboard 截圖；query 沒有時間窗；evidence 沒標示 sampling / freshness 限制</li>
</ul>
<p>Observability <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 的核心是把可觀測資料從「查詢結果」升級成「可交接證據」。事故與驗證需要一組能說明來源、時間窗、可信度、限制與 owner 的 evidence。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability evidence package 是可觀測性模組交給可靠性驗證與事故處理的證據包，責任是讓 log、metric、trace 與 audit log 能被重用、回放與復盤。</p>
<p>這一頁處理的是交接格式。4.17 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a> 說明資料是否可信；evidence package 說明如何把可信度、查詢入口與限制一起交給下游流程。</p>
<p>證據包的價值在於保存判讀上下文。只有截圖時，讀者看不到 query、時間窗、sampling、資料延遲與 owner；有 evidence package 時，後續 release gate、<a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 與 post-incident review 才能回放同一組事實。</p>
<h2 id="evidence-欄位">Evidence 欄位</h2>
<p>Evidence 欄位的責任是讓每個觀測證據都可查、可解釋、可追蹤。欄位不需要複雜，但要覆蓋事中判讀與事後復盤的最小需求。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>標示資料來源</td>
          <td>區分 log、metric、trace、audit</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>標示查詢時間窗</td>
          <td>對齊 incident timeline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>保留可重跑查詢</td>
          <td>支援 handoff 與復盤</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定可解釋資料的人</td>
          <td>避免 evidence 失去語意</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>標示 completeness / freshness</td>
          <td>防止資料限制被誤讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>標示 confirmed / suspected</td>
          <td>支援分級與決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>標示 missing signal 或 drift</td>
          <td>回寫 04 readiness 與 data quality</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>標示保存期限</td>
          <td>支援 audit、PIR 與長事故</td>
      </tr>
  </tbody>
</table>
<p>Source 欄位讓讀者知道 evidence 的能力邊界。Metric 適合看趨勢，log 適合看事件細節，trace 適合看路徑，audit log 適合看責任鏈。</p>
<p><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a> 是 evidence package 的基本欄位。事故前後 30 分鐘、部署期間、DR drill 時窗、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 短窗與長窗都需要明確，否則同一張圖可能被不同人解讀成不同結論。</p>
<p><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a> 比截圖更重要。截圖適合溝通當下狀態，query link 才能讓下一班 on-call、可靠性 owner 或 PIR reviewer 重跑同一個判讀。</p>
<p><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a> 欄位讓 evidence 保留限制。sampling ratio、ingest delay、schema drift、log drop、cardinality truncation 與 timestamp skew 都應直接出現在證據包中。</p>
<h2 id="資料來源">資料來源</h2>
<p>Evidence package 的資料來源要按判讀責任分層。每一層回答的問題不同，下游使用時也要保留這個差異。</p>
<table>
  <thead>
      <tr>
          <th>資料來源</th>
          <th>回答問題</th>
          <th>常見限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Log</td>
          <td>單一事件發生了什麼</td>
          <td>schema drift、drop、PII masking</td>
      </tr>
      <tr>
          <td>Metric</td>
          <td>趨勢是否偏離穩態</td>
          <td>聚合粒度、cardinality、延遲</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>失效卡在哪個服務或依賴邊界</td>
          <td>sampling、async 斷鏈</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>高風險操作與責任鏈如何形成</td>
          <td>權限限制、retention、法規要求</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>操作視角如何快速判讀</td>
          <td>面板版本、查詢成本、owner</td>
      </tr>
      <tr>
          <td>Client-side signal</td>
          <td>使用者感知是否和 server 一致</td>
          <td>browser / region / device bias</td>
      </tr>
      <tr>
          <td>Deployment event</td>
          <td>近期變更是否與異常時間線重疊</td>
          <td>rollout 粒度、feature flag owner</td>
      </tr>
  </tbody>
</table>
<p>Log evidence 適合進入 incident intake。它要保留 request id、tenant、region、error class 與 trace id，讓事故候選能被查證。</p>
<p>Metric evidence 適合進入 SLO、release gate 與 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 判讀。它要保留時間窗、分母分子、聚合粒度與資料延遲，讓 burn rate 與容量判斷可回放。</p>
<p>Trace evidence 適合支援 dependency 與 async workflow 判讀。它要標示 sampling policy 與缺失 span，讓下游知道 trace 能支持到哪個邊界。</p>
<p>Audit log evidence 適合支援資安、資料修復與高風險操作。它要保留 access path、retention、masking 與 chain of custody 限制。</p>
<h2 id="打包流程">打包流程</h2>
<p>Evidence package 的打包流程是從問題開始。先問下游要做什麼決策，再選擇足以支援該決策的資料與工具入口。</p>
<ol>
<li>定義 evidence 要支援的決策：readiness、release gate、incident intake、decision log 或 PIR。</li>
<li>選擇最小資料集合：metric 看趨勢、log 看事件、trace 看路徑、audit 看責任。</li>
<li>補上 time range、query link、owner 與 data quality。</li>
<li>標示 confidence 與 known gap。</li>
<li>把缺口回寫到 4.16 readiness、4.17 data quality 或 4.18 operating model。</li>
</ol>
<p>Readiness 用的 evidence package 要回答「服務是否能被判讀」。它重視核心旅程、依賴、dashboard、alert、trace 與 owner。</p>
<p>Reliability 用的 evidence package 要回答「驗證是否有結果」。它重視 steady state、stop condition、experiment timeline、SLO burn 與回復訊號。</p>
<p>Incident 用的 evidence package 要回答「事故是否需要啟動、升級或回退」。它重視 source、impact scope、confidence、decision log 與 stakeholder update。</p>
<p>資料庫 migration 用的 evidence package 要回答「資料語意是否能進入下一階段」。它重視 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、row count、mismatch sample、replication lag、slow query 與資料限制；完整服務路徑可接到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
<h2 id="案例中的證據包判讀">案例中的證據包判讀</h2>
<p>證據包的價值要放回真實事故才看得清楚。Cloudflare 2019 與 AWS S3 2017 都不是「缺資料」，而是「資料若沒被包成可交接證據，決策會慢、通訊會亂、回寫會斷」。</p>
<p>Cloudflare 2019 的第一波判讀來自跨區 CPU、5xx 與 latency 同步惡化。這組訊號如果只有圖表截圖，團隊只能知道「全網變慢」；把 query link、time range、rule rollout event 與 confidence 一起交接，才能快速形成「先回滾規則」的決策。</p>
<p>AWS S3 2017 的關鍵是恢復分層：GET/LIST/DELETE 與 PUT 回線時間不同，且狀態頁通訊入口也受依賴影響。證據包若保留 subsystem 狀態、操作類型影響範圍與已知限制，對外更新才不會把「部分恢復」誤寫成「全面恢復」。</p>
<p>兩個案例共同指向同一個判讀原則：證據包要保留「能支持當下決策」的最小閉環，蒐集越多越好的思路反而製造噪音，至少包含事件時間窗、跨訊號對位、資料限制與決策責任人。</p>
<h2 id="誤判風險與修正路徑">誤判風險與修正路徑</h2>
<p>事故中的誤判多半源自證據包缺少判讀上下文，演算法本身很少是問題。當 evidence 只有結論沒有限制，下游就會把暫時訊號當成穩定事實。</p>
<table>
  <thead>
      <tr>
          <th>誤判場景</th>
          <th>為何會誤判</th>
          <th>修正路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>圖表短暫回穩就宣告恢復</td>
          <td>缺少時間窗與回線連續性門檻</td>
          <td>在 evidence 補 recovery window 與 steady state 對位</td>
      </tr>
      <tr>
          <td>trace 看起來正常</td>
          <td>缺 sampling ratio 與 missing span</td>
          <td>在 evidence 補 data quality 與 known gap</td>
      </tr>
      <tr>
          <td>對外說法過度樂觀</td>
          <td>缺 subsystem 分層狀態與限制說明</td>
          <td>在 evidence 補 scope / limitation / next update</td>
      </tr>
      <tr>
          <td>回滾決策反覆</td>
          <td>缺 deployment event 與影響範圍對位</td>
          <td>在 evidence 補 rollout event、impact scope 與 owner</td>
      </tr>
      <tr>
          <td>復盤找不到依據</td>
          <td>只留截圖，沒有 query 與時間窗</td>
          <td>在 evidence 補 query link 與 retention</td>
      </tr>
  </tbody>
</table>
<p>修正路徑的核心是把 evidence package 當成事故中的工作物，而不是事故後整理物。當下有完整欄位，後續 8.19 決策紀錄才有可回放證據，8.22 回寫才有可追蹤缺口。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Evidence package 的反模式通常來自把資料貼出來就當作證據交接。證據需要上下文，否則只是一段輸出。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只貼 dashboard 截圖</td>
          <td>事後缺少可重跑查詢</td>
          <td>保留 query link 與 time range</td>
      </tr>
      <tr>
          <td>Query 無時間窗</td>
          <td>同一查詢不同時間跑出不同結論</td>
          <td>標準化 time range</td>
      </tr>
      <tr>
          <td>缺資料品質限制</td>
          <td>sampling / drop / delay 被忽略</td>
          <td>引用 4.17 data quality 欄位</td>
      </tr>
      <tr>
          <td>Evidence 無 owner</td>
          <td>下游無人能解釋欄位語意</td>
          <td>指定 service / platform owner</td>
      </tr>
      <tr>
          <td>Retention 未標示</td>
          <td>PIR 或 audit 時證據已過期</td>
          <td>標示 retention 與保存責任</td>
      </tr>
  </tbody>
</table>
<p>只貼 dashboard 截圖會讓 evidence 失去可回放性。截圖可以當摘要，query、時間窗與資料限制則提供復盤與交接能力。</p>
<p>缺資料品質限制會讓下游高估證據。若 trace sampling 只保留 10%、log pipeline 有 drop、metric 有 ingest delay，這些限制要跟證據一起交接。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>4.16 observability readiness：補 evidence package 所需的訊號入口</li>
<li>4.17 telemetry data quality：標示 completeness、freshness、drift 與 sampling 限制</li>
<li>4.18 operating model：指定 evidence owner、retention 與 review cadence</li>
<li>1.7 Schema Migration Rollout 證據：把 validation query 與資料限制包成 migration gate 可用的證據</li>
<li>6.23 verification evidence handoff：把驗證結果包成同一格式</li>
<li>8.18 incident intake：把 evidence package 轉成事故候選</li>
<li>8.19 incident decision log：把 evidence package 連到事中決策</li>
</ul>
]]></content:encoded></item><item><title>6.20 Experiment Safety Boundary</title><link>https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>experiment safety boundary 的責任：讓可靠性實驗可控、可停、可回復&lt;/li>
&lt;li>實驗類型：chaos test、load test、failover drill、rollback rehearsal、DR drill&lt;/li>
&lt;li>blast radius：服務、tenant、region、dependency、資料範圍&lt;/li>
&lt;li>停止條件：SLO burn、error rate、latency、queue lag、customer impact、cost threshold&lt;/li>
&lt;li>權限約束：誰能啟動、誰能停止、誰能擴大範圍&lt;/li>
&lt;li>evidence 要求：假設、步驟、觀測訊號、結果、回復時間、action item&lt;/li>
&lt;li>跟 07 的交接：高風險演練需要權限與稽核約束&lt;/li>
&lt;li>反模式：直接在 production 打 chaos；缺停止條件；實驗 owner 與 incident commander 不清楚&lt;/li>
&lt;/ul>
&lt;p>Experiment safety boundary 的價值在於讓失敗驗證可重播、可停止、可回復。實驗越接近真實失效，對團隊越有學習價值；同時也越需要清楚邊界，避免「為了驗證韌性」而產生額外事故。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Experiment safety boundary 是定義可靠性實驗安全範圍的控制面，責任是讓團隊能主動驗證失敗，同時控制實驗造成的實際風險。&lt;/p>
&lt;p>這一頁處理的是實驗邊界。可靠性實驗的價值來自接近真實失效，但越接近真實，越需要明確 blast radius、停止條件與回復路徑。&lt;/p>
&lt;p>安全邊界是一組事前契約：誰能啟動、誰有停止權、觸發什麼門檻必須終止、終止後怎麼回復。契約存在時，團隊才能在實驗中保持速度，同時控制風險成本。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 experiment safety 時，先看實驗假設是否明確，再看實驗失控時是否能立刻停止與回復。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>experiment hypothesis 是否連到具體 failure mode&lt;/li>
&lt;li>blast radius 是否限制 service、tenant、region 或 traffic percentage&lt;/li>
&lt;li>stop condition 是否連到 SLO / customer impact / cost&lt;/li>
&lt;li>rollback / failover 是否在實驗前準備好&lt;/li>
&lt;li>observer、executor、approver 是否分工清楚&lt;/li>
&lt;/ul>
&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>範圍控制&lt;/td>
 &lt;td>blast radius 限在服務 / 區域 / 流量百分比&lt;/td>
 &lt;td>影響擴散到非目標服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>停止條件&lt;/td>
 &lt;td>stop condition 連到 SLO / impact / cost&lt;/td>
 &lt;td>超門檻仍持續實驗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>權限治理&lt;/td>
 &lt;td>啟動者、停止者、核准者分離&lt;/td>
 &lt;td>需要額外查證誰在操作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回復能力&lt;/td>
 &lt;td>rollback / failover 已預演&lt;/td>
 &lt;td>終止後回復時間失控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證據留存&lt;/td>
 &lt;td>hypothesis 與結果可回放&lt;/td>
 &lt;td>成功與失敗都不可重現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="實驗類型">實驗類型&lt;/h2>
&lt;p>Experiment safety boundary 需要依實驗類型調整邊界。不同實驗打到的系統層不同，學習價值與實際風險也不同。&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>Chaos test&lt;/td>
 &lt;td>依賴、節點、網路失效是否可承受&lt;/td>
 &lt;td>service、region、dependency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load test&lt;/td>
 &lt;td>流量與資料量是否超過容量模型&lt;/td>
 &lt;td>traffic percentage、cost、quota&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failover drill&lt;/td>
 &lt;td>切換流程是否可執行&lt;/td>
 &lt;td>region、data replication、routing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback rehearsal&lt;/td>
 &lt;td>回復到前一版本是否安全&lt;/td>
 &lt;td>version、migration、feature flag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DR drill&lt;/td>
 &lt;td>災難恢復是否符合 RTO / RPO&lt;/td>
 &lt;td>data scope、region、access&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Chaos test 的風險在於故障注入接近真實失效。它需要明確 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>、觀測訊號與停止條件，讓團隊知道實驗如何驗證韌性。&lt;/p>
&lt;p>Load test 的風險在於放大共享依賴。測試流量可能壓到 database、cache、broker、third-party API 或 observability pipeline，因此邊界要包含共享資源與成本上限。&lt;/p>
&lt;p>Failover drill 的風險在於切換後的長尾狀態。流量切過去只是第一步，團隊還需要看資料同步、cache warmup、queue drain、DNS / routing propagation 與客戶端行為。&lt;/p>
&lt;p>Rollback rehearsal 的風險在於資料與版本相容性。程式可回滾不代表 schema、message、cache、feature flag 與 client contract 都能同步回到安全狀態。&lt;/p>
&lt;p>DR drill 的風險在於權限、資料與外部通訊。災難恢復通常涉及高權限操作、備份還原與跨團隊協作，因此需要額外 audit trail 與 incident communication 準備。&lt;/p>
&lt;h2 id="boundary-契約">Boundary 契約&lt;/h2>
&lt;p>Experiment boundary 契約的責任是讓實驗在開始前就具備可停止、可回復與可復盤條件。契約應被寫成實驗 artifact，並納入可回查的操作紀錄。&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>Hypothesis&lt;/td>
 &lt;td>說明要驗證的 failure mode&lt;/td>
 &lt;td>避免實驗變成任意故障注入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Blast radius&lt;/td>
 &lt;td>限制服務、tenant、region 範圍&lt;/td>
 &lt;td>控制實際影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Steady state&lt;/td>
 &lt;td>定義實驗期間應維持的狀態&lt;/td>
 &lt;td>判斷實驗是否成功&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stop condition&lt;/td>
 &lt;td>定義終止門檻&lt;/td>
 &lt;td>讓失控時能立刻停手&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback path&lt;/td>
 &lt;td>定義回復步驟&lt;/td>
 &lt;td>降低終止後的恢復成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Authority&lt;/td>
 &lt;td>定義啟動、停止與擴大權限&lt;/td>
 &lt;td>避免事中權責不清&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence&lt;/td>
 &lt;td>定義要收集的觀測與決策紀錄&lt;/td>
 &lt;td>支援復盤與可重播&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Hypothesis 是實驗的錨點。好的假設會說明「當 dependency timeout 發生時，checkout 應進入 degraded mode，SLO &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 應維持在門檻內」，而不只是「關掉某個服務」。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>experiment safety boundary 的責任：讓可靠性實驗可控、可停、可回復</li>
<li>實驗類型：chaos test、load test、failover drill、rollback rehearsal、DR drill</li>
<li>blast radius：服務、tenant、region、dependency、資料範圍</li>
<li>停止條件：SLO burn、error rate、latency、queue lag、customer impact、cost threshold</li>
<li>權限約束：誰能啟動、誰能停止、誰能擴大範圍</li>
<li>evidence 要求：假設、步驟、觀測訊號、結果、回復時間、action item</li>
<li>跟 07 的交接：高風險演練需要權限與稽核約束</li>
<li>反模式：直接在 production 打 chaos；缺停止條件；實驗 owner 與 incident commander 不清楚</li>
</ul>
<p>Experiment safety boundary 的價值在於讓失敗驗證可重播、可停止、可回復。實驗越接近真實失效，對團隊越有學習價值；同時也越需要清楚邊界，避免「為了驗證韌性」而產生額外事故。</p>
<h2 id="概念定位">概念定位</h2>
<p>Experiment safety boundary 是定義可靠性實驗安全範圍的控制面，責任是讓團隊能主動驗證失敗，同時控制實驗造成的實際風險。</p>
<p>這一頁處理的是實驗邊界。可靠性實驗的價值來自接近真實失效，但越接近真實，越需要明確 blast radius、停止條件與回復路徑。</p>
<p>安全邊界是一組事前契約：誰能啟動、誰有停止權、觸發什麼門檻必須終止、終止後怎麼回復。契約存在時，團隊才能在實驗中保持速度，同時控制風險成本。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 experiment safety 時，先看實驗假設是否明確，再看實驗失控時是否能立刻停止與回復。</p>
<p>重點訊號包括：</p>
<ul>
<li>experiment hypothesis 是否連到具體 failure mode</li>
<li>blast radius 是否限制 service、tenant、region 或 traffic percentage</li>
<li>stop condition 是否連到 SLO / customer impact / cost</li>
<li>rollback / failover 是否在實驗前準備好</li>
<li>observer、executor、approver 是否分工清楚</li>
</ul>
<table>
  <thead>
      <tr>
          <th>控制面</th>
          <th>最小可用判準</th>
          <th>失控信號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍控制</td>
          <td>blast radius 限在服務 / 區域 / 流量百分比</td>
          <td>影響擴散到非目標服務</td>
      </tr>
      <tr>
          <td>停止條件</td>
          <td>stop condition 連到 SLO / impact / cost</td>
          <td>超門檻仍持續實驗</td>
      </tr>
      <tr>
          <td>權限治理</td>
          <td>啟動者、停止者、核准者分離</td>
          <td>需要額外查證誰在操作</td>
      </tr>
      <tr>
          <td>回復能力</td>
          <td>rollback / failover 已預演</td>
          <td>終止後回復時間失控</td>
      </tr>
      <tr>
          <td>證據留存</td>
          <td>hypothesis 與結果可回放</td>
          <td>成功與失敗都不可重現</td>
      </tr>
  </tbody>
</table>
<h2 id="實驗類型">實驗類型</h2>
<p>Experiment safety boundary 需要依實驗類型調整邊界。不同實驗打到的系統層不同，學習價值與實際風險也不同。</p>
<table>
  <thead>
      <tr>
          <th>實驗類型</th>
          <th>驗證問題</th>
          <th>主要邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chaos test</td>
          <td>依賴、節點、網路失效是否可承受</td>
          <td>service、region、dependency</td>
      </tr>
      <tr>
          <td>Load test</td>
          <td>流量與資料量是否超過容量模型</td>
          <td>traffic percentage、cost、quota</td>
      </tr>
      <tr>
          <td>Failover drill</td>
          <td>切換流程是否可執行</td>
          <td>region、data replication、routing</td>
      </tr>
      <tr>
          <td>Rollback rehearsal</td>
          <td>回復到前一版本是否安全</td>
          <td>version、migration、feature flag</td>
      </tr>
      <tr>
          <td>DR drill</td>
          <td>災難恢復是否符合 RTO / RPO</td>
          <td>data scope、region、access</td>
      </tr>
  </tbody>
</table>
<p>Chaos test 的風險在於故障注入接近真實失效。它需要明確 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>、觀測訊號與停止條件，讓團隊知道實驗如何驗證韌性。</p>
<p>Load test 的風險在於放大共享依賴。測試流量可能壓到 database、cache、broker、third-party API 或 observability pipeline，因此邊界要包含共享資源與成本上限。</p>
<p>Failover drill 的風險在於切換後的長尾狀態。流量切過去只是第一步，團隊還需要看資料同步、cache warmup、queue drain、DNS / routing propagation 與客戶端行為。</p>
<p>Rollback rehearsal 的風險在於資料與版本相容性。程式可回滾不代表 schema、message、cache、feature flag 與 client contract 都能同步回到安全狀態。</p>
<p>DR drill 的風險在於權限、資料與外部通訊。災難恢復通常涉及高權限操作、備份還原與跨團隊協作，因此需要額外 audit trail 與 incident communication 準備。</p>
<h2 id="boundary-契約">Boundary 契約</h2>
<p>Experiment boundary 契約的責任是讓實驗在開始前就具備可停止、可回復與可復盤條件。契約應被寫成實驗 artifact，並納入可回查的操作紀錄。</p>
<table>
  <thead>
      <tr>
          <th>契約欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hypothesis</td>
          <td>說明要驗證的 failure mode</td>
          <td>避免實驗變成任意故障注入</td>
      </tr>
      <tr>
          <td>Blast radius</td>
          <td>限制服務、tenant、region 範圍</td>
          <td>控制實際影響</td>
      </tr>
      <tr>
          <td>Steady state</td>
          <td>定義實驗期間應維持的狀態</td>
          <td>判斷實驗是否成功</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>定義終止門檻</td>
          <td>讓失控時能立刻停手</td>
      </tr>
      <tr>
          <td>Rollback path</td>
          <td>定義回復步驟</td>
          <td>降低終止後的恢復成本</td>
      </tr>
      <tr>
          <td>Authority</td>
          <td>定義啟動、停止與擴大權限</td>
          <td>避免事中權責不清</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>定義要收集的觀測與決策紀錄</td>
          <td>支援復盤與可重播</td>
      </tr>
  </tbody>
</table>
<p>Hypothesis 是實驗的錨點。好的假設會說明「當 dependency timeout 發生時，checkout 應進入 degraded mode，SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 應維持在門檻內」，而不只是「關掉某個服務」。</p>
<p>Blast radius 需要同時包含技術範圍與客戶範圍。技術範圍是 service、region、cluster、dependency；客戶範圍是 tenant、plan、traffic percentage 或 internal-only cohort。</p>
<p>Stop condition 需要對應使用者影響。CPU 上升可以作為輔助訊號，但停止條件更應包含 SLO burn、error rate、latency、queue lag、customer ticket、成本與安全事件。</p>
<p>Authority 需要事前分清。executor 可以啟動實驗，observer 可以判讀訊號，incident commander 或 designated stop owner 必須有權直接終止實驗。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>chaos 實驗描述只有「打掉節點」，沒有 steady state 與停止條件</li>
<li>load test 影響共享 dependency，其他服務被連帶拖垮</li>
<li>DR drill 的停止擴大條件需要臨場討論</li>
<li>實驗成功但沒有 evidence，可重播性不足</li>
<li>實驗權限過寬，值班人員不知道誰在操作</li>
</ul>
<p>常見事故型場景是 load test 誤傷共享依賴，導致無關服務一起退化。若實驗前有 boundary 契約，至少會先限制流量比例、設定跨服務告警與 stop condition，讓問題停留在演練範圍內。</p>
<h2 id="stop-condition-設計">Stop Condition 設計</h2>
<p>Stop condition 的責任是把「什麼時候停」變成可觀測門檻。實驗期間不應靠臨場感覺判斷是否繼續，應根據預先同意的訊號停止或縮小範圍。</p>
<table>
  <thead>
      <tr>
          <th>停止條件</th>
          <th>常見門檻</th>
          <th>路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO burn</td>
          <td>短窗 burn rate 超過 policy</td>
          <td>終止實驗，進 incident intake</td>
      </tr>
      <tr>
          <td>Customer impact</td>
          <td>ticket、RUM、synthetic probe 異常</td>
          <td>終止或降到 internal cohort</td>
      </tr>
      <tr>
          <td>Queue lag</td>
          <td>lag 超過 drain 能力</td>
          <td>暫停流量，啟動 drain plan</td>
      </tr>
      <tr>
          <td>Error rate</td>
          <td>目標服務或相鄰服務錯誤率上升</td>
          <td>縮小 blast radius</td>
      </tr>
      <tr>
          <td>Cost threshold</td>
          <td>cloud cost 或 observability cost 暴增</td>
          <td>終止 load / trace 擴張</td>
      </tr>
      <tr>
          <td>Security signal</td>
          <td>audit、WAF、IAM 異常</td>
          <td>停止實驗，轉 07 / 08 分流</td>
      </tr>
  </tbody>
</table>
<p>SLO burn 是最適合作為 stop condition 的可靠性訊號。它能把多個低層訊號聚合成使用者影響，並且直接接到 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 與 release policy。</p>
<p>Customer impact 是停止條件的高優先訊號。即使 backend 指標尚未超標，只要 RUM、synthetic probe、support ticket 或 status page evidence 顯示客戶受影響，實驗就應縮小或終止。</p>
<p>Security signal 需要獨立路由。若實驗觸發異常權限、audit log gap、WAF event 或資料外送風險，應停止 reliability experiment，改由 security / incident response 流程判讀。</p>
<h2 id="evidence-與復盤">Evidence 與復盤</h2>
<p>Experiment evidence 的責任是讓實驗結果可被重播、比較與回寫。一次實驗不論成功或失敗，都應產出可被後續 readiness、release gate 與 incident drill 使用的證據。</p>
<table>
  <thead>
      <tr>
          <th>Evidence 欄位</th>
          <th>責任</th>
          <th>後續用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hypothesis</td>
          <td>保留原始假設</td>
          <td>判斷成功或失敗</td>
      </tr>
      <tr>
          <td>Timeline</td>
          <td>記錄開始、注入、停止、回復</td>
          <td>產生 incident / drill 時間線</td>
      </tr>
      <tr>
          <td>Signal set</td>
          <td>保存 dashboard、query、alert</td>
          <td>回寫 04 observability readiness</td>
      </tr>
      <tr>
          <td>Decision log</td>
          <td>保存停止、擴大、回復決策</td>
          <td>支援 08 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a></td>
      </tr>
      <tr>
          <td>Action items</td>
          <td>保存缺口與 owner</td>
          <td>進入 reliability debt backlog</td>
      </tr>
  </tbody>
</table>
<p>成功實驗也需要 evidence。成功代表某個假設在某個範圍內成立，未必代表所有流量、region、tenant 或依賴都安全；evidence 能保留適用範圍。</p>
<p>失敗實驗需要分清系統缺口與實驗缺口。系統缺口可能是 fallback 沒生效；實驗缺口可能是 stop condition 不清、dashboard 缺訊號或 owner 權限不足。兩者回寫路由不同。</p>
<h2 id="案例對照chaos--fit-的安全邊界設計">案例對照：Chaos / FIT 的安全邊界設計</h2>
<p>本章的 boundary 跟 stop condition 框架在 Netflix 三個 case 中各對應不同子問題：N1 給出單輪 chaos 的四元素、N2 給出時段選擇 guardrails、N3 給出實驗輸出的結構化欄位。三者連起來、安全邊界從「實驗執行階段」延伸到「證據交接階段」。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">N1 Netflix Steady State、Chaos 與 FIT</a>：揭露一輪有效 chaos 驗證的四元素 — Steady state（服務正常時應維持什麼行為）、Hypothesis（失效發生後仍應維持什麼）、Blast radius（實驗範圍怎麼限制）、Abort condition（何時立即停止）。</p>
<p>四元素中 Blast radius + Abort condition 直接對應本章的 boundary 契約跟 stop condition。Steady state 對應 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady-state-definition</a>、Hypothesis 對應實驗設計層。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">N2 Netflix Business-Hours Chaos 與 Guardrails</a>：揭露「business-hours chaos 跟 off-hours chaos 的選擇」— 工作時間執行能驗證即時應變能力跟通訊鏈條、但要在 guardrails 內（時段限制、實驗範圍限制、明確 abort trigger、事後回寫）。</p>
<p>Business-hours chaos 的核心價值是在 guardrails 內接近真實情境：人員在線可即時應變、依賴流量真實、通訊鏈條被測到。Off-hours 雖然短期風險低、但測到的多是「工具可執行」、不等於「服務可承受」。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">N3 Netflix FIT 證據交接</a>：揭露實驗輸出要結構化成四個決策欄位。四欄位分屬不同 release gate 階段 — rollout 決策類（steady-state impact、dependency drift）回答「能否繼續 rollout / blast radius 是否可接受」、事故處置類（abort trigger record、fallback result）回答「是否進入凍結與回退 / 事故時能否安全止血」。這四欄位讓 FIT 結果直接對應 release gate 的具體決策 — 不再倚賴主觀討論回到放行 / 凍結判斷。詳見 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification-evidence-handoff</a> 跟 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 rule-rollout-safety-gate</a>。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Experiment safety 的反模式通常來自把可靠性實驗當成勇敢行為。可靠性實驗的價值在設計、控制與學習，風險承受只是需要被管理的成本。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>直接打 production chaos</td>
          <td>真實但邊界不清</td>
          <td>先定義 cohort、stop condition</td>
      </tr>
      <tr>
          <td>無 steady state</td>
          <td>只知道打壞了什麼</td>
          <td>補 6.22 穩態定義</td>
      </tr>
      <tr>
          <td>無 stop owner</td>
          <td>超門檻後仍等會議決定</td>
          <td>指定有權停止的人</td>
      </tr>
      <tr>
          <td>缺 evidence</td>
          <td>實驗做過但缺少重播材料</td>
          <td>保存 hypothesis、timeline、signal</td>
      </tr>
      <tr>
          <td>權限過寬</td>
          <td>任意工程師可擴大 blast radius</td>
          <td>啟動、停止、擴大權限分離</td>
      </tr>
  </tbody>
</table>
<p>直接打 production chaos 的問題是風險與學習常被混在一起。production 實驗可以有價值，但需要從小 cohort、清楚 stop condition 與完整 rollback path 開始。</p>
<p>缺 evidence 會讓實驗只留下口頭記憶。可靠性能力需要累積，實驗結果應能回寫到 readiness、release gate、runbook 與 incident drill。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.16 observability readiness：確認實驗可被觀測</li>
<li>06.4 chaos testing：定義故障注入場景</li>
<li>06.7 DR / rollback rehearsal：定義回復路徑</li>
<li>06.22 steady state definition：定義實驗前 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a></li>
<li>07.23 shared controls：接 containment、rollback、degradation 共用控制面</li>
<li>08.6 drills / on-call readiness：把實驗轉成值班演練</li>
</ul>
]]></content:encoded></item><item><title>8.20 Customer Impact Assessment</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>customer impact assessment 的責任：把技術症狀轉成用戶與業務影響&lt;/li>
&lt;li>影響維度：user count、tenant、region、feature、duration、data correctness、financial impact&lt;/li>
&lt;li>服務維度：availability、latency、data loss、duplicate action、partial degradation&lt;/li>
&lt;li>證據來源：SLI / SLO、RUM、support ticket、billing event、audit log、status page&lt;/li>
&lt;li>分級用途：severity、stakeholder update、補償政策、PIR prioritization&lt;/li>
&lt;li>跟 04 的交接：client-side / synthetic / audit log 提供 impact evidence&lt;/li>
&lt;li>跟 07 的交接：資料外洩、授權錯誤與合規影響需要分流&lt;/li>
&lt;li>反模式：只用 server error rate 代表用戶影響；所有客戶用同一句 status update；補償判斷事後人工拼帳&lt;/li>
&lt;/ul>
&lt;p>Customer impact assessment 的價值是把工程語言翻成決策語言。事故期間若只看技術指標，團隊容易低估商業影響或高估通訊範圍；impact model 讓分級、通訊與補償使用同一組事實。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Customer impact assessment 是把事故影響轉成用戶、產品與業務語言的模型，責任是支援分級、通訊、補償與復盤排序。&lt;/p>
&lt;p>這一頁處理的是影響量化。事故指標可以從 server 開始，但對外決策需要知道誰受影響、影響多久、影響哪個功能、是否造成資料或金錢後果。&lt;/p>
&lt;p>影響量化的重點是可追蹤更新。初版估算可以粗，但要明確標記 confidence 與更新節點，讓 stakeholder 知道哪些是已確認影響、哪些仍在查證。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 customer impact 時，先看影響對象與功能，再看影響是否可量化到通訊與補償所需精度。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>affected users / tenants / regions 是否可估算&lt;/li>
&lt;li>affected feature 是否能對應 customer journey&lt;/li>
&lt;li>duration 是否能用 incident timeline 與 SLO 對齊&lt;/li>
&lt;li>data correctness / financial impact 是否需要獨立調查&lt;/li>
&lt;li>status update 是否能反映不同客群的實際影響&lt;/li>
&lt;/ul>
&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>對象&lt;/td>
 &lt;td>users / tenants / regions 可估算&lt;/td>
 &lt;td>分級與客戶通知範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>功能&lt;/td>
 &lt;td>對應具體 customer journey&lt;/td>
 &lt;td>狀態頁與客服話術&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間&lt;/td>
 &lt;td>可對齊 timeline 與 SLO&lt;/td>
 &lt;td>影響期間與恢復宣告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>正確性&lt;/td>
 &lt;td>資料 / 交易是否受損可判定&lt;/td>
 &lt;td>補償與法規通報&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>金額&lt;/td>
 &lt;td>financial impact 可分層估算&lt;/td>
 &lt;td>補償與商務決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="影響維度">影響維度&lt;/h2>
&lt;p>Customer impact assessment 的影響維度要同時描述誰受影響、哪個功能受影響、影響多久，以及是否形成資料或金錢後果。&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>User / Tenant&lt;/td>
 &lt;td>哪些用戶、租戶、客群受影響&lt;/td>
 &lt;td>account metadata、support ticket&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Region / Channel&lt;/td>
 &lt;td>哪些區域、裝置、入口受影響&lt;/td>
 &lt;td>RUM、CDN、mobile crash、region tag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Feature / Journey&lt;/td>
 &lt;td>哪個 customer journey 受影響&lt;/td>
 &lt;td>SLI、product analytics、trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Duration&lt;/td>
 &lt;td>影響從何時開始、何時結束&lt;/td>
 &lt;td>incident timeline、SLO window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data correctness&lt;/td>
 &lt;td>資料是否遺失、重複、錯誤或延遲&lt;/td>
 &lt;td>audit log、reconciliation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Financial impact&lt;/td>
 &lt;td>是否影響交易、收費、補償或 SLA&lt;/td>
 &lt;td>billing event、order system&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User / tenant 維度能避免平均值誤導。低比例錯誤若集中在高價值 tenant、企業客戶或關鍵市場，severity 與 stakeholder update 都需要提升精度。&lt;/p>
&lt;p>Region / channel 維度能定位擴散範圍。單一区域、mobile-only、browser-specific、CDN edge 或 VPN / enterprise network 問題，對通訊與修復路由有不同影響。&lt;/p>
&lt;p>Feature / journey 維度能把技術症狀轉成產品語言。&lt;code>API 5xx&lt;/code> 對外仍需要翻成 login、checkout、upload、search、report export 或 webhook delivery 等使用者旅程。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>customer impact assessment 的責任：把技術症狀轉成用戶與業務影響</li>
<li>影響維度：user count、tenant、region、feature、duration、data correctness、financial impact</li>
<li>服務維度：availability、latency、data loss、duplicate action、partial degradation</li>
<li>證據來源：SLI / SLO、RUM、support ticket、billing event、audit log、status page</li>
<li>分級用途：severity、stakeholder update、補償政策、PIR prioritization</li>
<li>跟 04 的交接：client-side / synthetic / audit log 提供 impact evidence</li>
<li>跟 07 的交接：資料外洩、授權錯誤與合規影響需要分流</li>
<li>反模式：只用 server error rate 代表用戶影響；所有客戶用同一句 status update；補償判斷事後人工拼帳</li>
</ul>
<p>Customer impact assessment 的價值是把工程語言翻成決策語言。事故期間若只看技術指標，團隊容易低估商業影響或高估通訊範圍；impact model 讓分級、通訊與補償使用同一組事實。</p>
<h2 id="概念定位">概念定位</h2>
<p>Customer impact assessment 是把事故影響轉成用戶、產品與業務語言的模型，責任是支援分級、通訊、補償與復盤排序。</p>
<p>這一頁處理的是影響量化。事故指標可以從 server 開始，但對外決策需要知道誰受影響、影響多久、影響哪個功能、是否造成資料或金錢後果。</p>
<p>影響量化的重點是可追蹤更新。初版估算可以粗，但要明確標記 confidence 與更新節點，讓 stakeholder 知道哪些是已確認影響、哪些仍在查證。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 customer impact 時，先看影響對象與功能，再看影響是否可量化到通訊與補償所需精度。</p>
<p>重點訊號包括：</p>
<ul>
<li>affected users / tenants / regions 是否可估算</li>
<li>affected feature 是否能對應 customer journey</li>
<li>duration 是否能用 incident timeline 與 SLO 對齊</li>
<li>data correctness / financial impact 是否需要獨立調查</li>
<li>status update 是否能反映不同客群的實際影響</li>
</ul>
<table>
  <thead>
      <tr>
          <th>影響面向</th>
          <th>最小可用判準</th>
          <th>對外決策用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對象</td>
          <td>users / tenants / regions 可估算</td>
          <td>分級與客戶通知範圍</td>
      </tr>
      <tr>
          <td>功能</td>
          <td>對應具體 customer journey</td>
          <td>狀態頁與客服話術</td>
      </tr>
      <tr>
          <td>時間</td>
          <td>可對齊 timeline 與 SLO</td>
          <td>影響期間與恢復宣告</td>
      </tr>
      <tr>
          <td>正確性</td>
          <td>資料 / 交易是否受損可判定</td>
          <td>補償與法規通報</td>
      </tr>
      <tr>
          <td>金額</td>
          <td>financial impact 可分層估算</td>
          <td>補償與商務決策</td>
      </tr>
  </tbody>
</table>
<h2 id="影響維度">影響維度</h2>
<p>Customer impact assessment 的影響維度要同時描述誰受影響、哪個功能受影響、影響多久，以及是否形成資料或金錢後果。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>核心問題</th>
          <th>常見資料來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>User / Tenant</td>
          <td>哪些用戶、租戶、客群受影響</td>
          <td>account metadata、support ticket</td>
      </tr>
      <tr>
          <td>Region / Channel</td>
          <td>哪些區域、裝置、入口受影響</td>
          <td>RUM、CDN、mobile crash、region tag</td>
      </tr>
      <tr>
          <td>Feature / Journey</td>
          <td>哪個 customer journey 受影響</td>
          <td>SLI、product analytics、trace</td>
      </tr>
      <tr>
          <td>Duration</td>
          <td>影響從何時開始、何時結束</td>
          <td>incident timeline、SLO window</td>
      </tr>
      <tr>
          <td>Data correctness</td>
          <td>資料是否遺失、重複、錯誤或延遲</td>
          <td>audit log、reconciliation</td>
      </tr>
      <tr>
          <td>Financial impact</td>
          <td>是否影響交易、收費、補償或 SLA</td>
          <td>billing event、order system</td>
      </tr>
  </tbody>
</table>
<p>User / tenant 維度能避免平均值誤導。低比例錯誤若集中在高價值 tenant、企業客戶或關鍵市場，severity 與 stakeholder update 都需要提升精度。</p>
<p>Region / channel 維度能定位擴散範圍。單一区域、mobile-only、browser-specific、CDN edge 或 VPN / enterprise network 問題，對通訊與修復路由有不同影響。</p>
<p>Feature / journey 維度能把技術症狀轉成產品語言。<code>API 5xx</code> 對外仍需要翻成 login、checkout、upload、search、report export 或 webhook delivery 等使用者旅程。</p>
<p>Data correctness 維度需要獨立於 availability 判讀。服務可用但資料重複、漏寫、錯帳或延遲時，customer impact 通常比 error rate 更嚴重。</p>
<p>Financial impact 維度需要和商務與法務協作。交易失敗、重複扣款、SLA credit、補償政策與合約通知，都需要更嚴謹的 evidence chain。</p>
<h2 id="服務影響類型">服務影響類型</h2>
<p>Customer impact assessment 需要把技術症狀映射到服務影響類型。這個映射能讓 severity、communication 與 compensation 使用一致語言。</p>
<table>
  <thead>
      <tr>
          <th>服務影響類型</th>
          <th>技術樣貌</th>
          <th>對外語言</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Availability loss</td>
          <td>5xx、timeout、login failure</td>
          <td>用戶功能不可用</td>
      </tr>
      <tr>
          <td>Latency degradation</td>
          <td>p95 / p99 上升、queue lag</td>
          <td>功能變慢或處理延遲</td>
      </tr>
      <tr>
          <td>Data delay</td>
          <td>replication lag、index stale</td>
          <td>顯示資料較舊或更新延遲</td>
      </tr>
      <tr>
          <td>Data inconsistency</td>
          <td>duplicate、missing、wrong value</td>
          <td>資料可能不正確，需要校驗</td>
      </tr>
      <tr>
          <td>Duplicate action</td>
          <td>retry / replay 造成重複副作用</td>
          <td>可能重複通知、重複交易或重複任務</td>
      </tr>
      <tr>
          <td>Partial degradation</td>
          <td>fallback、read-only、load shedding</td>
          <td>部分功能暫停或降級</td>
      </tr>
  </tbody>
</table>
<p>Availability loss 是最容易分級的影響類型。它通常可以直接對應 SLO、status page 與客服話術。</p>
<p>Latency degradation 需要時間窗與使用者旅程。短時間 p99 上升可能只影響少數操作，也可能造成交易超時或 queue backlog，因此需要搭配 customer journey 判讀。</p>
<p>Data delay 常被低估。search index、reporting、notification、read model 或 cache projection 延遲時，用戶看到的是資料更新延遲。</p>
<p>Data inconsistency 需要更高 evidence 標準。它可能牽涉合規、金額、客戶信任與後續修復，因此要接 audit log、reconciliation 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a>。</p>
<p>Duplicate action 需要補償視角。retry、replay 或 idempotency 缺口造成的重複副作用，可能需要退款、撤銷通知、資料修復或客戶通知。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>error rate 很低，但集中在高價值客戶或核心功能</li>
<li>server-side 指標正常，但 RUM / support ticket 顯示用戶受影響</li>
<li>事故結束後才開始計算受影響帳戶</li>
<li>status page 寫「部分用戶」，內部需要臨場估算部分的範圍</li>
<li>補償判斷需要工程臨時產出查詢</li>
</ul>
<p>實務場景常是 server error rate 不高，但問題集中在高價值客戶或關鍵流程。若 impact assessment 只看平均值，會錯配通訊與補償；若同時看 tenant / feature / value 分佈，決策會更精準。</p>
<h2 id="assessment-流程">Assessment 流程</h2>
<p>Customer impact assessment 的流程是從技術證據走向對外決策。第一版可以粗，後續要隨 evidence 更新。</p>
<ol>
<li>從 incident intake 取得 source、time、feature 與初始 impact。</li>
<li>用 SLI / SLO、RUM、support ticket 與 product analytics 估算 affected scope。</li>
<li>標示 confidence：estimated、confirmed、reconciled。</li>
<li>把 impact 分層：internal-only、limited customers、broad customer impact、regulated / financial impact。</li>
<li>輸出 severity、status update、stakeholder update 與 compensation input。</li>
</ol>
<p>Estimated 代表初估。事故早期可以使用 error rate、ticket 數、synthetic probe 或抽樣資料先估範圍，但要標示限制。</p>
<p>Confirmed 代表已有多來源證據對齊。當 server-side、client-side、support 與 product data 指向同一範圍，impact assessment 就能支援對外通訊。</p>
<p>Reconciled 代表事後完成精算。補償、SLA credit、資料修復與 PIR 通常需要 reconciled impact，並把事中估算作為對照。</p>
<h2 id="通訊與補償">通訊與補償</h2>
<p>Customer impact assessment 是 stakeholder communication 與補償判斷的輸入。通訊需要足夠早，補償需要足夠準。</p>
<p>Status update 應描述使用者可理解的功能影響。<code>database CPU high</code> 應翻成 <code>部分用戶建立報表延遲</code> 或 <code>部分 API request 回應變慢</code>。</p>
<p>Stakeholder update 應描述範圍、信心與下一次更新時間。若影響仍在估算，應明確說明目前 confidence 與正在補的 evidence。</p>
<p>Compensation input 應接到可重算資料。affected users、duration、transaction amount、SLA tier、data correctness 與 customer segment 都應能被查詢與復核。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Customer impact assessment 的反模式通常來自用單一技術指標代表所有影響。技術指標是 evidence，完整影響模型還需要客戶、功能、時間、正確性與金額維度。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Server error rate 即影響</td>
          <td>低 error rate 就低估事故</td>
          <td>加入 tenant、feature、client signal</td>
      </tr>
      <tr>
          <td>所有客戶同一句更新</td>
          <td>狀態頁過粗或過度廣泛</td>
          <td>依 region / feature / segment 分層</td>
      </tr>
      <tr>
          <td>補償事後拼帳</td>
          <td>工程臨時查 billing 與 usage</td>
          <td>事前定義補償資料欄位</td>
      </tr>
      <tr>
          <td>只算人數</td>
          <td>忽略金額、合約、資料正確性</td>
          <td>加入 financial / compliance impact</td>
      </tr>
      <tr>
          <td>Confidence 不標示</td>
          <td>估算與確認混在一起</td>
          <td>標示 estimated / confirmed</td>
      </tr>
  </tbody>
</table>
<p>Server error rate 即影響會讓事故分級失真。低錯誤率集中在核心客戶、金流流程或資料正確性時，實際 impact 可能高於平均值。</p>
<p>補償事後拼帳會拖慢收尾。若 billing、usage、audit 與 incident timeline 在平時就能對齊，補償與客戶回覆會更快進入可驗證狀態。</p>
<h2 id="與資安分流的關係">與資安分流的關係</h2>
<p>Customer impact assessment 需要在資料外洩、授權錯誤與合規影響出現時啟動資安分流。這類事故的影響不只看可用性，也看資料類型、責任鏈、通知義務與證據保存。</p>
<p>若 impact assessment 發現 PII、credential、audit log gap、cross-tenant access 或資料匯出異常，應交給 07 的資料保護與事故分流流程，並在 8.19 decision log 中標示 evidence handling 限制。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.10 client-side / synthetic / RUM：補用戶感知訊號</li>
<li>04.12 audit log：補資料與責任證據</li>
<li>08.1 severity trigger：把 impact assessment 接入分級</li>
<li>08.4 incident communication：提供對外更新內容</li>
<li>08.10 stakeholder communication：接 status page 與補償政策</li>
<li>07.4 data protection：資料外洩或資料正確性影響分流</li>
</ul>
]]></content:encoded></item><item><title>Memcached → Redis：不搬資料、搬存取層的能力升級遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/migrate-to-redis/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（target）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 Schema/API + application change High、但 &lt;strong>data topology Low（cache 可重建）&lt;/strong>——本文是「能力升級 + 資料層免遷」的 dogfood，跟反向的 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &amp;#43; 資料結構 &amp;#43; pub/sub &amp;#43; Lua &amp;#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &amp;#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）&lt;/a> 對位。&lt;/p>&lt;/blockquote>
&lt;h2 id="cache-遷移不搬資料搬存取層">cache 遷移不搬資料、搬存取層&lt;/h2>
&lt;p>一般 migration 最重、最危險的部分是搬資料——schema 要對、一致性要保、cutover 要不丟。Memcached → Redis 把這塊幾乎拿掉了，因為 &lt;strong>cache 的資料本來就是可重建的副本&lt;/strong>。遷移不需要把 Memcached 裡的東西搬到 Redis；讓 Redis 空著上線、cache miss 自然回源、命中率慢慢 warm 起來即可。Memcached 在 warm-up 期間繼續服務，等 Redis 命中率追上來再切。&lt;/p>
&lt;p>這個性質讓 Memcached → Redis 的工作重心完全不同：不在資料層，在&lt;strong>存取層&lt;/strong>（換 client library、換協定）跟&lt;strong>可選的能力升級&lt;/strong>。觸發這個遷移的通常是「outgrew pure KV」——本來只用 Memcached 存 string，後來需要 counter（用 application 層讀-改-寫硬湊、有 race）、需要 session 物件（serialize 整包 JSON、改一個欄位要全寫）、需要 leaderboard（在 app 排序、慢）。這些 Redis 用 INCR / Hash / Sorted Set 原生解，把 application 層硬湊的邏輯收回 cache 層。&lt;/p>
&lt;p>本文跑 diff audit 確認這個形狀、用兩階段結構（先 drop-in、再升級能力）展開遷移與踩坑。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>Memcached 協定 → Redis RESP、純 string → 可選 data types&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>Redis 多了 eviction policy / persistence / cluster 決策&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>pure cache → data structure store（但可先維持 pure KV 用法）&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>client library 換、可選改用 data types&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Data topology&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>cache 可重建、不搬資料、re-warm&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>Low&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主導維度是 Schema/API + application change（存取層），但這個 migration 的特徵是 &lt;strong>data topology Low&lt;/strong>——這是 cache 類遷移獨有的性質。對映 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論&lt;/a> 的 type：本文是 &lt;strong>cache 類 Type A 的簡化變體&lt;/strong>（phased translation 的存取層翻譯，但因 data topology Low 省掉了資料遷移階段）。結構上採兩階段：&lt;strong>Phase 1 drop-in 替換（維持 pure KV 用法、先把 client 換掉）&lt;/strong>，&lt;strong>Phase 2 漸進採用 data types（把 application 層硬湊的邏輯收回 Redis）&lt;/strong>。Phase 2 是可選的、可以慢慢來。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（target）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 Schema/API + application change High、但 <strong>data topology Low（cache 可重建）</strong>——本文是「能力升級 + 資料層免遷」的 dogfood，跟反向的 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）</a> 對位。</p></blockquote>
<h2 id="cache-遷移不搬資料搬存取層">cache 遷移不搬資料、搬存取層</h2>
<p>一般 migration 最重、最危險的部分是搬資料——schema 要對、一致性要保、cutover 要不丟。Memcached → Redis 把這塊幾乎拿掉了，因為 <strong>cache 的資料本來就是可重建的副本</strong>。遷移不需要把 Memcached 裡的東西搬到 Redis；讓 Redis 空著上線、cache miss 自然回源、命中率慢慢 warm 起來即可。Memcached 在 warm-up 期間繼續服務，等 Redis 命中率追上來再切。</p>
<p>這個性質讓 Memcached → Redis 的工作重心完全不同：不在資料層，在<strong>存取層</strong>（換 client library、換協定）跟<strong>可選的能力升級</strong>。觸發這個遷移的通常是「outgrew pure KV」——本來只用 Memcached 存 string，後來需要 counter（用 application 層讀-改-寫硬湊、有 race）、需要 session 物件（serialize 整包 JSON、改一個欄位要全寫）、需要 leaderboard（在 app 排序、慢）。這些 Redis 用 INCR / Hash / Sorted Set 原生解，把 application 層硬湊的邏輯收回 cache 層。</p>
<p>本文跑 diff audit 確認這個形狀、用兩階段結構（先 drop-in、再升級能力）展開遷移與踩坑。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Memcached 協定 → Redis RESP、純 string → 可選 data types</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Redis 多了 eviction policy / persistence / cluster 決策</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>pure cache → data structure store（但可先維持 pure KV 用法）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>client library 換、可選改用 data types</td>
          <td>High</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>cache 可重建、不搬資料、re-warm</strong></td>
          <td><strong>Low</strong></td>
      </tr>
  </tbody>
</table>
<p>主導維度是 Schema/API + application change（存取層），但這個 migration 的特徵是 <strong>data topology Low</strong>——這是 cache 類遷移獨有的性質。對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的 type：本文是 <strong>cache 類 Type A 的簡化變體</strong>（phased translation 的存取層翻譯，但因 data topology Low 省掉了資料遷移階段）。結構上採兩階段：<strong>Phase 1 drop-in 替換（維持 pure KV 用法、先把 client 換掉）</strong>，<strong>Phase 2 漸進採用 data types（把 application 層硬湊的邏輯收回 Redis）</strong>。Phase 2 是可選的、可以慢慢來。</p>
<h2 id="phase-1drop-in-替換pure-kv不搬資料">Phase 1：drop-in 替換（pure KV、不搬資料）</h2>
<p>第一階段把 Memcached 換成 Redis，但<strong>只用 Redis 當 pure KV</strong>（GET / SET / DEL + TTL），存取行為跟 Memcached 一樣。這一步風險最低，因為不碰 data model、不搬資料。</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">存取層對應（Phase 1 維持 pure KV 語意）：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Memcached set(key, val, ttl)   →  Redis SET key val EX ttl
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Memcached get(key)             →  Redis GET key
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Memcached delete(key)          →  Redis DEL key
</span></span><span class="line"><span class="ln">5</span><span class="cl">  Memcached incr/decr            →  Redis INCR/DECR（Redis 原生原子、比 Memcached 更穩）</span></span></code></pre></div><p>cutover 流程（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">1. 部署 Redis（空的）、設 maxmemory + eviction policy（見記憶體調校）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. application 改用 Redis client（雙寫期：同時寫 Memcached + Redis，讀仍走 Memcached）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 讀切到 Redis（cache miss 回源 + 寫回 Redis、命中率逐步 warm up）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 觀察 Redis 命中率追上 Memcached、origin 壓力無異常
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 停止寫 Memcached、下線 Memcached</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>不需要資料遷移工具——Redis 空上線、靠 cache-aside 自然 warm（見 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a>）</li>
<li>warm-up 期 origin 壓力會短暫上升（命中率從 0 爬升），低流量時段切、或預熱熱 key</li>
<li>Phase 1 完成後 application 行為跟用 Memcached 時一致，只是底層換 Redis</li>
<li>想保留開源 OSI 授權，target 直接選 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Redis 相容、BSD）</li>
</ul>
<h2 id="phase-2漸進採用-data-types可選">Phase 2：漸進採用 data types（可選）</h2>
<p>Phase 1 上線穩定後，再把 application 層硬湊的邏輯逐步收回 Redis 的原生 data types。這一階段是能力升級、不是遷移必需，可以一個場景一個場景來。</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">application 硬湊 → Redis 原生：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  讀 JSON → 改欄位 → 寫回整包    →  Redis Hash（HSET/HGET 單欄位、免全寫）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  app 端計數 + CAS 重試           →  Redis INCR（原子、無 race）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  app 端排序 leaderboard          →  Redis Sorted Set（ZADD/ZRANGE）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  app 端 set 去重                 →  Redis Set（SADD/SISMEMBER）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  多 key 操作要原子               →  Redis MULTI / Lua（Memcached 只有 CAS）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>Phase 2 每個改動是獨立的小重構，不必一次到位</li>
<li>收回 data types 的收益是「消除 application 層的 read-modify-write race + 減少網路往返」</li>
<li>不是所有東西都要升級——純 string cache 留在 GET/SET 就好，別為了用而用</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1warm-up-期-origin-被打爆">Case 1：warm-up 期 origin 被打爆</h3>
<p><strong>徵兆</strong>：切讀到 Redis 的瞬間，origin（DB）QPS 暴增、延遲升高，因為 Redis 還是空的、大量 cache miss 同時回源。</p>
<p><strong>根因</strong>：Redis 空上線、命中率從 0 開始，warm-up 期所有讀都 miss 回源。沒有控制就是一次 origin 衝擊（類似冷啟動 stampede）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>低流量時段切讀、讓命中率平緩爬升</li>
<li>預熱熱 key（migration 前先把已知熱 key 灌進 Redis）</li>
<li>cache miss 回源加 singleflight / jitter，避免同 key 並發回源（見 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 stampede rollback</a>）</li>
<li>雙寫期先讓 Redis 被寫入 warm 一段時間，再切讀</li>
</ol>
<h3 id="case-2把-memcached-的-multi-get-行為直接搬效能不如預期">Case 2：把 Memcached 的 multi-get 行為直接搬、效能不如預期</h3>
<p><strong>徵兆</strong>：Memcached 的 batch get（一次拿多 key）搬到 Redis 後延遲沒改善甚至更差。</p>
<p><strong>根因</strong>：Memcached client 的 multi-get 跟 Redis 的 MGET / pipeline 行為不同。直接一個 key 一個 GET（N 次往返）會比 Memcached 的 batch 慢——Redis 要用 MGET 或 pipeline 才能合併往返（見 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a>）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Memcached multi-get → Redis MGET（同 slot）或 pipeline</li>
<li>不要把「N 次獨立 GET」當成 multi-get 的等價</li>
<li>cluster 模式下 MGET 跨 slot 會失敗，用 hash tag 或 pipeline 分組</li>
<li>量測往返次數，存取層遷移要保持「一次互動的往返數」不退化</li>
</ol>
<h3 id="case-3ttl-精度與-eviction-行為差異造成命中率變化">Case 3：TTL 精度與 eviction 行為差異造成命中率變化</h3>
<p><strong>徵兆</strong>：遷到 Redis 後命中率跟 Memcached 時期不一樣（可能更高或更低），cache 行為不如預期。</p>
<p><strong>根因</strong>：Memcached 是 LRU + 秒級 lazy expiration + slab 限制；Redis 有 8 種 eviction policy + ms 級 TTL + 不同記憶體模型。沿用 Memcached 的 TTL 與容量設定不會得到一樣的淘汰行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>明確設 Redis 的 <code>maxmemory-policy</code>（純 cache 用 allkeys-lru / allkeys-lfu，見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>）</li>
<li>不要假設 Memcached 的容量設定直接套用——Redis 記憶體模型不同（無 slab calcification、但有自己的 fragmentation）</li>
<li>觀察 <code>evicted_keys</code> 與命中率，對齊預期 working set</li>
<li>Memcached 的 slab 浪費 vs Redis 的編碼，記憶體佔用會不同，重新算容量</li>
</ol>
<h3 id="case-4以為-redis-一定比-memcached-快--省">Case 4：以為 Redis 一定比 Memcached 快 / 省</h3>
<p><strong>徵兆</strong>：遷到 Redis 後純 string cache 的記憶體佔用或延遲沒有改善，甚至 Redis 單執行緒在高並發純 GET 下不如 Memcached 多執行緒。</p>
<p><strong>根因</strong>：對「純 string KV、高並發」這個 Memcached 的本場，Memcached 的多執行緒可能比 Redis 單執行緒（命令層）更適合。遷 Redis 的收益在 data types / persistence / 生態，不是純 KV 效能。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>釐清遷移動機——是要 data types / persistence（Redis 解）還是純 KV 效能（Memcached 可能更好）</li>
<li>純 KV 高並發要 Redis 的多核走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> / <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> 或 Redis I/O threads</li>
<li>純 cache 紀律本來就是 Memcached 的優勢，遷 Redis 要小心別把 cache 用成 database</li>
<li>沒有 data types / persistence 需求的純 KV，留 Memcached 可能更對</li>
</ol>
<h3 id="case-5把可重建的-cache-當成要搬的資料白做遷移工具">Case 5：把可重建的 cache 當成要搬的資料、白做遷移工具</h3>
<p><strong>徵兆</strong>：團隊花時間寫 Memcached → Redis 的資料遷移腳本、做一致性校驗，結果發現 cache 切換後這些資料本來就會被新值覆蓋。</p>
<p><strong>根因</strong>：用一般 migration 的思維（搬資料 + 校驗）處理 cache 遷移，沒意識到 cache 是可重建副本——搬過去的舊值很快被回源的新值取代，搬資料是白工且可能搬到 stale 值。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cache 遷移預設不搬資料、靠 re-warm（這是 cache 類遷移的核心簡化）</li>
<li>只有「重建成本極高的 cache」（昂貴計算結果）才考慮搬，且要評估 stale 風險</li>
<li>把精力放在存取層正確性與 warm-up 控制，不是資料搬遷</li>
<li>對照 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a>：cache 是副本、不是 source-of-truth</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Memcached（source）</th>
          <th>Redis / Valkey（target）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料遷移</td>
          <td>—</td>
          <td>不需要（cache 可重建、re-warm）</td>
      </tr>
      <tr>
          <td>data types</td>
          <td>純 string KV</td>
          <td>6 大 + Stream / Geo</td>
      </tr>
      <tr>
          <td>原子操作</td>
          <td>INCR / DECR / CAS</td>
          <td>100+（INCR / HSET / ZADD / Lua）</td>
      </tr>
      <tr>
          <td>persistence</td>
          <td>無</td>
          <td>RDB / AOF（可選）</td>
      </tr>
      <tr>
          <td>多執行緒</td>
          <td>原生多執行緒</td>
          <td>單執行緒命令 + I/O threads</td>
      </tr>
      <tr>
          <td>eviction</td>
          <td>LRU only</td>
          <td>8 種 policy</td>
      </tr>
      <tr>
          <td>純 KV 高並發效能</td>
          <td>多執行緒、本場強</td>
          <td>單執行緒命令可能略遜（要多核走 fork）</td>
      </tr>
      <tr>
          <td>遷移風險</td>
          <td>—</td>
          <td>低（無資料遷移、存取層 + warm-up）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：要 data types / persistence / 原子操作 → 遷 Redis（兩階段、低風險）；純 KV + 高並發 + 嚴格 cache 紀律 → 留 Memcached。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Memcached → Redis 是能力升級，它跟 Redis 的調校與選型交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a></strong>：遷過去要設對 maxmemory-policy，Redis 記憶體模型跟 Memcached slab 不同。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a></strong>：Memcached multi-get → Redis MGET / pipeline，存取層遷移要保持往返數。</li>
<li><strong>跟反向 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></strong>：反向是 Type E paradigm reduction（downgrade）；本文是能力升級（upgrade），兩者對位看 cache paradigm 的兩個方向。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：要開源 OSI 授權，target 選 Valkey（Redis 相容、BSD），遷移流程一致。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>反向 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>0.21 交付形態選型：從全託管到自建的光譜與邊界</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/</guid><description>&lt;p>交付形態選型的核心原則是先判斷「這個服務值得自建嗎」、再進入自建世界的服務選型。提供線上服務的途徑是一個光譜：全託管平台（Wix、Shopify、Google Sites）、辦公生態自動化（Google Apps Script）、BaaS（Firebase、Supabase）、半託管 CMS（WordPress）、到自建程式 — 本模組其餘章節討論的資料庫、快取、queue、部署選型、全部屬於光譜最右端的自建世界。落在光譜其他位置的服務、那些章節的問題暫時與它無關；判斷自己落在哪、以及什麼訊號出現時該往右移、是比「選哪個資料庫」更早的決策。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、讀者能夠：&lt;/p>
&lt;ol>
&lt;li>用差異化位置與業務量判斷服務該落在交付形態光譜的哪一段&lt;/li>
&lt;li>看懂全託管平台、辦公生態自動化、BaaS、半託管 CMS 與自建各自的能力邊界與遷出代價&lt;/li>
&lt;li>在選擇託管形態的同時、保留日後遷往自建的可遷出路徑&lt;/li>
&lt;li>把「該升級自建了」的判斷從感覺轉成可觀察的 tripwire&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察自建的合理性來自差異化位置">【觀察】自建的合理性來自差異化位置&lt;/h2>
&lt;p>自建的合理性來自一個前提：&lt;strong>這個產品的差異化在軟體本身&lt;/strong>。差異化在商品、內容、社群或服務品質的生意、軟體只是通路 — 通路用現成的、把工程資源留給差異化所在的位置、是成本上更誠實的選擇。&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>需求能用「型錄 + 結帳」「表單 + 通知」「文章 + 頁面」描述完&lt;/td>
 &lt;td>託管平台的標準域、先不自建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>流程是把幾個現成服務串起來（表單進試算表、定時寄報表）&lt;/td>
 &lt;td>辦公生態自動化（Apps Script 類）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>產品是行動 app 或 SPA、後端需求是認證 + 資料同步 + 推播&lt;/td>
 &lt;td>BaaS（Firebase 類）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內容為主、但要客製版型、SEO、外掛功能&lt;/td>
 &lt;td>半託管 CMS（WordPress 類）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務流程落在某個垂直行業 SaaS 的標準域（預約、課表、POS、訂位）&lt;/td>
 &lt;td>垂直 SaaS — 行業專用的託管形態、先進候選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>產品本身就是軟體（SaaS 工具、API 服務、平台）&lt;/td>
 &lt;td>自建 — 本模組其餘章節的世界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>核心流程在任何現成平台都要大量 workaround 才能表達&lt;/td>
 &lt;td>自建、或重新檢視流程是否過度客製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務只有自己 / 家人用、跑在自有主機或私有網路、無對外使用者&lt;/td>
 &lt;td>個人自架工具 — 自建但走極縮減流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一列的判讀方式值得展開：把產品的核心流程用一句話描述、再問「這句話是不是某個託管平台的官方首頁文案」。「賣手作飾品、信用卡結帳、出貨通知」就是 Shopify 的首頁；「活動報名、自動寄確認信、報名滿額關閉」就是表單工具加自動化的範圍。描述不出落在任何平台標準域的流程 — 例如「客戶上傳檔案後跑客製演算法、依結果動態計價」— 才是自建訊號。&lt;/p>
&lt;p>「產品本身就是軟體」這一列要先過一個澄清：&lt;strong>這個軟體是要賣的產品、還是經營業務的工具&lt;/strong>。「給健身教練用的課表系統」有兩種身分 — 開發者要賣給眾多教練的產品（市場上的垂直 SaaS 是競爭對手、交付形態走自建）、或教練管理自己學員的工具（同一批垂直 SaaS — 課表、預約、POS — 正是該優先評估的託管形態）。同一句需求描述、兩種身分的結論相反、先拆身分再進訊號表。&lt;/p>
&lt;h2 id="判讀交付形態光譜">【判讀】交付形態光譜&lt;/h2>
&lt;p>光譜從左到右、控制力遞增、維運責任同步遞增。每一段先看它解什麼、再看邊界訊號與遷出代價。&lt;/p>
&lt;h3 id="全託管平台wixshopifysquarespacegoogle-sites">全託管平台：Wix、Shopify、Squarespace、Google Sites&lt;/h3>
&lt;p>平台承擔整條技術棧：主機、憑證、防護、金流、版面。使用者操作的對象是「網站內容」、不是「程式碼」。電商走 Shopify、形象站與簡介站走 Wix / Google Sites、訂閱內容走 Substack 類 — 各平台的標準域內、上線時間以天計、且本系列 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8 資安與資料保護需求&lt;/a> 裡多數伺服器側的功課由平台承擔。&lt;/p>
&lt;p>邊界訊號：客製需求開始對抗平台 — 結帳流程要插入平台不支援的步驟、資料要跟外部系統雙向同步、頁面效能撞到平台的模板天花板。在平台內 workaround 的程式碼（Shopify 的 liquid hack、Wix 的 Velo 腳本）累積越多、等於在用最差的開發環境自建。&lt;/p>
&lt;p>遷出代價：資料匯出通常有官方管道（商品、訂單 CSV）、但 URL 結構、SEO 累積、會員密碼（雜湊不可攜、遷移等於全體重設密碼）、訂閱金流的扣款授權（綁在平台的金流帳戶上）都要重建。&lt;/p>
&lt;h3 id="辦公生態自動化google-apps-script--sites--forms--sheets">辦公生態自動化：Google Apps Script + Sites / Forms / Sheets&lt;/h3>
&lt;p>這一段解的是「流程自動化」、不是「產品」。表單收件進試算表、定時整理寄報表、收到 email 觸發動作 — Apps Script 把 Google Workspace 的元件串成工作流、零主機、零部署、權限直接掛在 Google 帳號上。同段位的還有 no-code 資料庫工具（Airtable、Notion 當輕後台）— 串現成元件、零部署的角色相同。內部工具與小規模對外流程（報名、登記、排班）在這一段的成本接近零。&lt;/p>
&lt;p>邊界訊號：第一個出現的通常是配額牆 — 某天報名表單停止收件、log 裡躺著超額錯誤、而且已經靜默丟了一個下午的提交。再來是併發：兩個人同時送出、Sheets 用最後寫入蓋掉前一筆。最後是工程紀律的渴望、腳本長到想要版本控制與測試時、它已經是一個沒有工程基礎設施的程式專案。&lt;/p>
&lt;p>遷出代價：低 — 資料在 Sheets / Drive 裡天然可匯出、流程邏輯通常短到可以重寫。這一段的風險是「忘記遷」、不是「遷不動」：業務量上來後配額錯誤靜默發生、報名表單少收一批人才發現。&lt;/p>
&lt;h3 id="baasfirebasesupabase">BaaS：Firebase、Supabase&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS&lt;/a> 把後端拆成現成模組：認證、資料庫、檔案儲存、推播、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/serverless/" data-link-title="Serverless" data-link-desc="說明按請求 / 按用量計費、由平台管理執行環境與擴縮的運算交付模型、與其冷啟動與計價邊界">serverless&lt;/a> function。前端（app / SPA）自己寫、後端用平台的 SDK 直連 — 本系列 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 狀態與資料儲存選型&lt;/a> 討論的多數問題、在這一段被平台的預設答案取代。行動 app MVP 與快速驗證期的產品、BaaS 把「後端工程師」這個角色延後了。&lt;/p>
&lt;p>BaaS 的邊界牆通常分三面依序出現。第一面是報表 — 老闆要一張跨集合的月報、Firestore 查不出來、工程師開始把資料複製第二份、複製管線本身變成要維護的系統。第二面是帳單：讀寫計費隨流量線性成長、某個月的發票讓人重新打開計算機比對自建。第三面最安靜：client 直連資料庫的模型把授權邏輯全部塞進 security rules、規則檔長到沒人敢改時、&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8&lt;/a> 的整個控制面已經壓在一個難以測試的規則語言（DSL）裡。&lt;/p></description><content:encoded><![CDATA[<p>交付形態選型的核心原則是先判斷「這個服務值得自建嗎」、再進入自建世界的服務選型。提供線上服務的途徑是一個光譜：全託管平台（Wix、Shopify、Google Sites）、辦公生態自動化（Google Apps Script）、BaaS（Firebase、Supabase）、半託管 CMS（WordPress）、到自建程式 — 本模組其餘章節討論的資料庫、快取、queue、部署選型、全部屬於光譜最右端的自建世界。落在光譜其他位置的服務、那些章節的問題暫時與它無關；判斷自己落在哪、以及什麼訊號出現時該往右移、是比「選哪個資料庫」更早的決策。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、讀者能夠：</p>
<ol>
<li>用差異化位置與業務量判斷服務該落在交付形態光譜的哪一段</li>
<li>看懂全託管平台、辦公生態自動化、BaaS、半託管 CMS 與自建各自的能力邊界與遷出代價</li>
<li>在選擇託管形態的同時、保留日後遷往自建的可遷出路徑</li>
<li>把「該升級自建了」的判斷從感覺轉成可觀察的 tripwire</li>
</ol>
<hr>
<h2 id="觀察自建的合理性來自差異化位置">【觀察】自建的合理性來自差異化位置</h2>
<p>自建的合理性來自一個前提：<strong>這個產品的差異化在軟體本身</strong>。差異化在商品、內容、社群或服務品質的生意、軟體只是通路 — 通路用現成的、把工程資源留給差異化所在的位置、是成本上更誠實的選擇。</p>
<table>
  <thead>
      <tr>
          <th>可觀察訊號</th>
          <th>指向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需求能用「型錄 + 結帳」「表單 + 通知」「文章 + 頁面」描述完</td>
          <td>託管平台的標準域、先不自建</td>
      </tr>
      <tr>
          <td>流程是把幾個現成服務串起來（表單進試算表、定時寄報表）</td>
          <td>辦公生態自動化（Apps Script 類）</td>
      </tr>
      <tr>
          <td>產品是行動 app 或 SPA、後端需求是認證 + 資料同步 + 推播</td>
          <td>BaaS（Firebase 類）</td>
      </tr>
      <tr>
          <td>內容為主、但要客製版型、SEO、外掛功能</td>
          <td>半託管 CMS（WordPress 類）</td>
      </tr>
      <tr>
          <td>業務流程落在某個垂直行業 SaaS 的標準域（預約、課表、POS、訂位）</td>
          <td>垂直 SaaS — 行業專用的託管形態、先進候選</td>
      </tr>
      <tr>
          <td>產品本身就是軟體（SaaS 工具、API 服務、平台）</td>
          <td>自建 — 本模組其餘章節的世界</td>
      </tr>
      <tr>
          <td>核心流程在任何現成平台都要大量 workaround 才能表達</td>
          <td>自建、或重新檢視流程是否過度客製</td>
      </tr>
      <tr>
          <td>服務只有自己 / 家人用、跑在自有主機或私有網路、無對外使用者</td>
          <td>個人自架工具 — 自建但走極縮減流程</td>
      </tr>
  </tbody>
</table>
<p>第一列的判讀方式值得展開：把產品的核心流程用一句話描述、再問「這句話是不是某個託管平台的官方首頁文案」。「賣手作飾品、信用卡結帳、出貨通知」就是 Shopify 的首頁；「活動報名、自動寄確認信、報名滿額關閉」就是表單工具加自動化的範圍。描述不出落在任何平台標準域的流程 — 例如「客戶上傳檔案後跑客製演算法、依結果動態計價」— 才是自建訊號。</p>
<p>「產品本身就是軟體」這一列要先過一個澄清：<strong>這個軟體是要賣的產品、還是經營業務的工具</strong>。「給健身教練用的課表系統」有兩種身分 — 開發者要賣給眾多教練的產品（市場上的垂直 SaaS 是競爭對手、交付形態走自建）、或教練管理自己學員的工具（同一批垂直 SaaS — 課表、預約、POS — 正是該優先評估的託管形態）。同一句需求描述、兩種身分的結論相反、先拆身分再進訊號表。</p>
<h2 id="判讀交付形態光譜">【判讀】交付形態光譜</h2>
<p>光譜從左到右、控制力遞增、維運責任同步遞增。每一段先看它解什麼、再看邊界訊號與遷出代價。</p>
<h3 id="全託管平台wixshopifysquarespacegoogle-sites">全託管平台：Wix、Shopify、Squarespace、Google Sites</h3>
<p>平台承擔整條技術棧：主機、憑證、防護、金流、版面。使用者操作的對象是「網站內容」、不是「程式碼」。電商走 Shopify、形象站與簡介站走 Wix / Google Sites、訂閱內容走 Substack 類 — 各平台的標準域內、上線時間以天計、且本系列 <a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8 資安與資料保護需求</a> 裡多數伺服器側的功課由平台承擔。</p>
<p>邊界訊號：客製需求開始對抗平台 — 結帳流程要插入平台不支援的步驟、資料要跟外部系統雙向同步、頁面效能撞到平台的模板天花板。在平台內 workaround 的程式碼（Shopify 的 liquid hack、Wix 的 Velo 腳本）累積越多、等於在用最差的開發環境自建。</p>
<p>遷出代價：資料匯出通常有官方管道（商品、訂單 CSV）、但 URL 結構、SEO 累積、會員密碼（雜湊不可攜、遷移等於全體重設密碼）、訂閱金流的扣款授權（綁在平台的金流帳戶上）都要重建。</p>
<h3 id="辦公生態自動化google-apps-script--sites--forms--sheets">辦公生態自動化：Google Apps Script + Sites / Forms / Sheets</h3>
<p>這一段解的是「流程自動化」、不是「產品」。表單收件進試算表、定時整理寄報表、收到 email 觸發動作 — Apps Script 把 Google Workspace 的元件串成工作流、零主機、零部署、權限直接掛在 Google 帳號上。同段位的還有 no-code 資料庫工具（Airtable、Notion 當輕後台）— 串現成元件、零部署的角色相同。內部工具與小規模對外流程（報名、登記、排班）在這一段的成本接近零。</p>
<p>邊界訊號：第一個出現的通常是配額牆 — 某天報名表單停止收件、log 裡躺著超額錯誤、而且已經靜默丟了一個下午的提交。再來是併發：兩個人同時送出、Sheets 用最後寫入蓋掉前一筆。最後是工程紀律的渴望、腳本長到想要版本控制與測試時、它已經是一個沒有工程基礎設施的程式專案。</p>
<p>遷出代價：低 — 資料在 Sheets / Drive 裡天然可匯出、流程邏輯通常短到可以重寫。這一段的風險是「忘記遷」、不是「遷不動」：業務量上來後配額錯誤靜默發生、報名表單少收一批人才發現。</p>
<h3 id="baasfirebasesupabase">BaaS：Firebase、Supabase</h3>
<p><a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a> 把後端拆成現成模組：認證、資料庫、檔案儲存、推播、<a href="/blog/backend/knowledge-cards/serverless/" data-link-title="Serverless" data-link-desc="說明按請求 / 按用量計費、由平台管理執行環境與擴縮的運算交付模型、與其冷啟動與計價邊界">serverless</a> function。前端（app / SPA）自己寫、後端用平台的 SDK 直連 — 本系列 <a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 狀態與資料儲存選型</a> 討論的多數問題、在這一段被平台的預設答案取代。行動 app MVP 與快速驗證期的產品、BaaS 把「後端工程師」這個角色延後了。</p>
<p>BaaS 的邊界牆通常分三面依序出現。第一面是報表 — 老闆要一張跨集合的月報、Firestore 查不出來、工程師開始把資料複製第二份、複製管線本身變成要維護的系統。第二面是帳單：讀寫計費隨流量線性成長、某個月的發票讓人重新打開計算機比對自建。第三面最安靜：client 直連資料庫的模型把授權邏輯全部塞進 security rules、規則檔長到沒人敢改時、<a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a> 的整個控制面已經壓在一個難以測試的規則語言（DSL）裡。</p>
<p>遷出的代價集中在資料層：資料本身可匯出、但資料模型沿平台特性設計（為遷就查詢限制、同一份資料複製存放多處的反正規化結構、加上平台專屬的即時同步語意）、遷到關聯式資料庫等於重做資料層。認證體系可攜性視平台而定（Firebase Auth 可匯出密碼雜湊、是少數友善案例）。</p>
<h3 id="半託管-cmswordpress-與同類">半託管 CMS：WordPress 與同類</h3>
<p>WordPress 代表「半手動自定義」這一段：核心由開源專案提供、功能靠外掛拼裝、版型可以改到面目全非、主機可以託管也可以自架。內容為主、客製需求中等的站（媒體、部落格、預約、小型電商加 WooCommerce）長期住在這一段。控制力比全託管平台高一級：資料庫是自己的 MySQL、檔案是自己的目錄、想改什麼理論上都改得動。</p>
<p>邊界訊號：每次外掛更新前先全站備份、更新完手動點一輪主要頁面 — 這個儀式固定下來時、外掛堆疊已經超出任何人的全盤理解。效能問題跟著來：頁面變慢、但慢在哪一層查詢沒人說得清。資安面則是時間問題：WordPress 外掛漏洞是攻擊者的固定狩獵場、patch 責任在自己身上、沒人 patch 的站是 <a href="/blog/backend/00-service-selection/red-team-cross-service-weaknesses/" data-link-title="0.11 攻擊者視角（紅隊）：跨服務弱點判讀總表" data-link-desc="用攻擊面、可觀察訊號與失敗代價，建立 backend 選型前的弱點盤點框架">0.11 攻擊者視角</a> 裡最便宜的目標。</p>
<p>遷出代價：真正遷不動的是外掛私有表裡的業務邏輯 — 會員等級、預約規則、客製欄位散在各外掛自己的資料表、遷移時要逐個考古；內容本身（文章、媒體）反而是最成熟的匯出路徑。</p>
<h3 id="垂直-saas行業專用的託管形態">垂直 SaaS：行業專用的託管形態</h3>
<p>垂直 SaaS（預約系統、課表排班、POS、訂位平台）是全託管平台的行業特化分支。平台已經把該行業的標準流程做成產品、使用者設定開通而非寫程式碼。業務流程落在平台標準域內時、效率跟全託管平台相同；差異在於當需求偏離行業標準域（例如預約系統要加客製的動態定價邏輯）、平台的 API 與 webhook 是延伸天花板 — 超出就是自建訊號。遷出代價集中在客戶資料與業務規則的可攜性：客戶名單、歷史交易紀錄的匯出格式、以及在平台 UI 裡長出來的行業特定規則（排班邏輯、會員等級、優惠券組合）能否帶走。</p>
<h3 id="個人自架工具常駐本機無對外服務">個人自架工具：常駐本機、無對外服務</h3>
<p>這一段跟前面所有形態的本質差異在於：沒有對外使用者。遠端操控自己的主機、家庭自動化、個人備份同步這類工具、使用者就是所有者（單人或家人）。它在光譜上是自建的一個特例 — 自建成立、但本模組其餘章節的多數問題（租戶、使用者資料庫、對外服務）不適用。常駐在自有主機（launchd / systemd）或私有網路上、本模組裡真正展開的只剩三條：入口怎麼安全進來（部署平台）、誰能存取（資安）、密鑰怎麼保管（secret）;資料庫、快取、queue、多租戶隔離多半 N/A。</p>
<p>認證也離開 web-auth 光譜。沒有帳號系統、沒有 SSO、主體就是持有裝置的所有者：一層裝置原生生物辨識（Face ID / 指紋）認「人」、一層 app 與主機共享的密鑰認「連線」。入口形態常是 outbound tunnel（cloudflared / Tailscale）而非公網 IP — 本機主動外連、路由器零開 port。詳見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> 的單人裝置認證段與 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>。</p>
<p>這個模型的邊界在使用者數。使用者從「自己」變成「自己 + 幾個朋友」時、第一個多出來的使用者就打破整個模型 — 共享密鑰無法分辨是誰、生物辨識綁在單一裝置、沒有帳號就無法個別撤銷。這時回到完整自建訪談、把認證升級成帳號系統。</p>
<p>遷出方向也跟其他形態相反 — 方向是「長成服務」、離開平台只是副產物。工具沒有累積對外使用者資料、遷移成本低;真正要重做的是認證與授權層（從單人共享密鑰換成多使用者帳號系統）、以及入口（從個人 tunnel 換成能承載多人的公開入口）。</p>
<h3 id="自建本模組其餘章節的世界">自建：本模組其餘章節的世界</h3>
<p>差異化在軟體本身、或所有託管形態的邊界都已撞到、就進入自建。自建的真正成本由本模組其餘章節展開：<a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 需求分類</a> 開始、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a> 把人力與維運成本攤開。自建換到的是：資料模型自己定、流程任意客製、成本曲線在規模化後由自己控制、以及所有控制面（資安、觀測、備援）可以做到合規要求的深度。</p>
<h2 id="判讀混合形態是常態不是過渡期的妥協">【判讀】混合形態是常態、不是過渡期的妥協</h2>
<p>光譜上的位置不必全站一致。常見的健康組合：行銷頁與內容放託管平台（Wix / WordPress）、核心產品自建、兩者用子網域分流；電商主站走 Shopify、客製的批發報價系統自建接 Shopify API；內部流程跑 Apps Script、對外產品自建。判讀單位是「每條業務流程」、不是「整間公司」— 把不是差異化的流程硬塞進自建 codebase、跟把差異化流程硬塞進託管平台、是同一個錯誤的兩個方向。拆分軸除了逐流程、還有逐層：headless 形態（託管平台當後端引擎、自建前端體驗層）是同一條流程內的層級混合、判讀方式相同 — 每一層各自問「差異化在這層嗎」。</p>
<p>光譜上還有兩個停靠點值得知道：靜態網站生成器（Hugo / Next.js export）搭配 hosting（Netlify / Vercel）介於全託管與半託管之間，適合文件站與行銷頁；Edge Functions（Cloudflare Workers / Vercel Edge）介於 BaaS 與自建之間，寫程式但不管基礎設施，適合輕量 API 與邊緣邏輯。兩者的邊界訊號與遷出代價跟相鄰形態類似，需求超出時回到各自相鄰段落的判讀。</p>
<h2 id="權衡託管形態的成本與資安帳">【權衡】託管形態的成本與資安帳</h2>
<p>託管形態把伺服器帳單換成平台帳單、把維運人力換成平台依賴。權衡時五個方向都要看：</p>
<ol>
<li><strong>資安限制</strong>：平台扛掉 patch、憑證、DDoS 防護 — 這對沒有資安人力的團隊是淨收益。代價是資料主權與稽核深度受限：資料落在平台的儲存裡、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 細度由平台決定、有資料落地或合規要求（金融、醫療、政府標案）的業務、託管形態可能直接出局。</li>
<li><strong>流量與穩定性</strong>：平台的彈性通常優於小團隊自建（Shopify 扛 Black Friday 是它的本業）、但天花板不可協商 — API rate limit、配額、模板效能、撞到就是撞到。</li>
<li><strong>平台費用</strong>：月費 + 抽成（電商平台的交易抽成在量大後是實質稅率）。自建與託管的成本曲線會交叉、交叉點要算：平台月費 + 抽成 vs 自建的工程薪資 + 雲端帳單、在目前與三倍業務量下各是多少。</li>
<li><strong>人力與操作</strong>：託管形態讓非工程角色能直接維護（改文案、上商品、調流程）、這個能力在小團隊值很多錢；自建後每個改動都過工程隊列。</li>
<li><strong>機會成本</strong>：選託管、省下的工程時間投到差異化；選自建、買到的控制力要有明確用途。「以後可能要客製」是最常見的偽自建理由 — 客製需求出現時再遷、總成本通常低於提前自建養一套用不滿的基礎設施。</li>
</ol>
<h2 id="檢查可遷出保險選託管的同時保留往右走的路">【檢查】可遷出保險：選託管的同時保留往右走的路</h2>
<p>託管形態的真正風險是 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 的具體形：遷不出去。保險在進場時最便宜。選擇任何託管形態的同時、確認下列事項：</p>
<table>
  <thead>
      <tr>
          <th>保險項</th>
          <th>做法</th>
          <th>缺了會發生什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自有網域</td>
          <td>網域註冊在自己名下、DNS 自己控制、不用平台贈送的子網域</td>
          <td>遷移等於換址、SEO 與既有連結歸零</td>
      </tr>
      <tr>
          <td>資料定期匯出</td>
          <td>排程匯出商品 / 訂單 / 會員資料、確認格式可被重新匯入</td>
          <td>遷移當天才發現匯出殘缺、或平台限制匯出頻率</td>
      </tr>
      <tr>
          <td>客戶聯絡管道自有</td>
          <td>email 名單同步到自有系統、不只活在平台的行銷模組裡</td>
          <td>客戶關係綁死在平台、遷移等於重新獲客</td>
      </tr>
      <tr>
          <td>金流可攜性</td>
          <td>評估金流商是否平台綁定；訂閱制確認扣款授權能否轉移</td>
          <td>訂閱客戶遷移時全體重新授權、流失率直接體現在營收</td>
      </tr>
      <tr>
          <td>密碼不可攜的預案</td>
          <td>接受會員密碼雜湊多半遷不走、預先設計重設密碼的遷移體驗</td>
          <td>遷移日全體會員被迫重設、無預案時體驗等於資安事故</td>
      </tr>
      <tr>
          <td>業務邏輯文件化</td>
          <td>在平台設定裡長出來的規則（折扣邏輯、會員等級）寫成文件</td>
          <td>規則只存在平台 UI 裡、遷移時靠回憶重建</td>
      </tr>
  </tbody>
</table>
<p>每項保險在遷出日如何兌現 — 保險與理賠流程的對應 — 見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a> 的資產線盤點。</p>
<h2 id="檢查升級自建的-tripwire">【檢查】升級自建的 tripwire</h2>
<p>「日後可能需要自建」要轉成可觀察訊號、寫進選型記錄、而不是留在感覺裡：</p>
<table>
  <thead>
      <tr>
          <th>Tripwire 訊號</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台內 workaround 程式碼持續成長（liquid hack、Velo、外掛魔改）</td>
          <td>已經在用最差的環境自建、把工程投入轉到正式自建更划算</td>
      </tr>
      <tr>
          <td>平台年費用超過半個工程師全載年薪（含招聘與管理成本）</td>
          <td>成本曲線交叉、用自己團隊的數字錨定後重算自建總帳；三倍業務量下再算一次</td>
      </tr>
      <tr>
          <td>核心流程的客製需求連續被平台能力擋下</td>
          <td>差異化開始長在軟體上、自建的前提成立了</td>
      </tr>
      <tr>
          <td>API rate limit / 配額錯誤開始影響業務</td>
          <td>天花板撞到、額度調整權在平台手上</td>
      </tr>
      <tr>
          <td>合規或客戶要求資料落地、稽核細度、滲透測試</td>
          <td>控制面深度超出託管形態能給的範圍</td>
      </tr>
      <tr>
          <td>平台政策變更（費率、功能下架）直接衝擊營收</td>
          <td>平台風險具體化、依賴單一平台的代價浮現</td>
      </tr>
      <tr>
          <td>平台被收購、停止維護或公告 EOL</td>
          <td>帶死線的續存風險 — 問題從「該不該遷」變成「遷移窗口多長」、立即啟動評估</td>
      </tr>
  </tbody>
</table>
<p>tripwire 是重評承諾、不是遷移保證 — 觸發後每拖一季、資料量與整合深度都在墊高遷移成本。任一 tripwire 觸發時、回到本模組從 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 需求分類</a> 走完整的自建選型；評估成立後的執行劇本 — 資產線盤點、並行期、回切窗口 — 見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>判斷落在自建：從 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖</a> 開始走本模組的選型順序。</li>
<li>判斷落在自建、但周邊能力仍想逐塊外包（認證、搜尋、金流、表單、後台）：見 <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>。</li>
<li>判斷落在個人自架工具（單人自用、無對外服務）：跳過資料庫 / 快取 / queue 章節、只看 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 單人裝置認證</a> 與 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>;多人化時再回完整自建選型。</li>
<li>判斷落在託管形態：完成上方可遷出保險清單、把 tripwire 寫進選型記錄、定期回看。</li>
<li>成本曲線的算法：見 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a>。</li>
<li>託管形態下仍需要的資安底線（帳號安全、權限、資料匯出管控）：見 <a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8 資安與資料保護需求</a>。</li>
<li>從託管遷往自建的執行劇本：見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>；模組總覽見 <a href="/blog/backend/10-system-evolution/" data-link-title="模組十：系統演進與遷移" data-link-desc="處理服務拆分、跨服務重構、大型遷移與雲端切換的執行紀律 — 設計階段的選型判斷見模組零、執行階段的高風險變更收斂在本模組">模組十：系統演進與遷移</a>。</li>
</ul>
]]></content:encoded></item><item><title>MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/query-optimization/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/query-optimization/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>query optimization&lt;/em> — EXPLAIN / optimizer trace / hint 三層工具跟 5 個實際 case。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="5-個常見-production-case">5 個常見 production case&lt;/h2>
&lt;p>production 上 query 慢、root cause 幾乎都是 &lt;em>optimizer 選錯 plan&lt;/em>。從以下 5 個 case 進入 query optimization：&lt;/p>
&lt;h3 id="case-15-秒--50ms--join-順序選錯">Case 1：5 秒 → 50ms — JOIN 順序選錯&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 慢 (5 秒)：optimizer 選 customers 為 outer table、scan 全 1M row
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-05-01&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;TW&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EXPLAIN 顯示：&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">+----+-------------+-------+------+---------------+--------+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">| id | select_type | table | type | possible_keys | rows |
&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">| 1 | SIMPLE | c | ALL | NULL | 1000000|
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| 1 | SIMPLE | o | ref | idx_cust_id | 100 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">+----+-------------+-------+------+---------------+--------+&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>c&lt;/code> table type=ALL（full scan）、rows=1M。問題：&lt;code>customers&lt;/code> 沒在 &lt;code>region&lt;/code> 上的 index、optimizer 預估「region=TW filter 沒效率、就 full scan」、但 region=TW 只佔 10% row（100K row）。&lt;/p>
&lt;p>修法：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">region&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ANALYZE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 更新 statistics&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加 index 後 optimizer 切 plan：先 scan &lt;code>customers&lt;/code> 用 &lt;code>idx_region&lt;/code> 篩 100K row、再 join &lt;code>orders&lt;/code>。從 5 秒降到 50ms。&lt;/p>
&lt;h3 id="case-230-秒--200ms--range-scan-退化-all">Case 2：30 秒 → 200ms — Range scan 退化 ALL&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BETWEEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-05-01&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-05-02&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">12345&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>events&lt;/code> 有 &lt;code>idx_user_id&lt;/code> 跟 &lt;code>idx_created_at&lt;/code> 兩個 index、optimizer 應該選一個 + 二級 filter、但實際 &lt;code>type=ALL&lt;/code>（full scan）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>query optimization</em> — EXPLAIN / optimizer trace / hint 三層工具跟 5 個實際 case。</p></blockquote>
<hr>
<h2 id="5-個常見-production-case">5 個常見 production case</h2>
<p>production 上 query 慢、root cause 幾乎都是 <em>optimizer 選錯 plan</em>。從以下 5 個 case 進入 query optimization：</p>
<h3 id="case-15-秒--50ms--join-順序選錯">Case 1：5 秒 → 50ms — JOIN 順序選錯</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 慢 (5 秒)：optimizer 選 customers 為 outer table、scan 全 1M row
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="p">;</span></span></span></code></pre></div><p>EXPLAIN 顯示：</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">+----+-------------+-------+------+---------------+--------+
</span></span><span class="line"><span class="ln">2</span><span class="cl">| id | select_type | table | type | possible_keys | rows   |
</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">|  1 | SIMPLE      | c     | ALL  | NULL          | 1000000|
</span></span><span class="line"><span class="ln">5</span><span class="cl">|  1 | SIMPLE      | o     | ref  | idx_cust_id   | 100    |
</span></span><span class="line"><span class="ln">6</span><span class="cl">+----+-------------+-------+------+---------------+--------+</span></span></code></pre></div><p><code>c</code> table type=ALL（full scan）、rows=1M。問題：<code>customers</code> 沒在 <code>region</code> 上的 index、optimizer 預估「region=TW filter 沒效率、就 full scan」、但 region=TW 只佔 10% row（100K row）。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_region</span><span class="w"> </span><span class="p">(</span><span class="n">region</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">customers</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 更新 statistics</span></span></span></code></pre></div><p>加 index 後 optimizer 切 plan：先 scan <code>customers</code> 用 <code>idx_region</code> 篩 100K row、再 join <code>orders</code>。從 5 秒降到 50ms。</p>
<h3 id="case-230-秒--200ms--range-scan-退化-all">Case 2：30 秒 → 200ms — Range scan 退化 ALL</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;2026-05-02&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">AND</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">12345</span><span class="p">;</span></span></span></code></pre></div><p><code>events</code> 有 <code>idx_user_id</code> 跟 <code>idx_created_at</code> 兩個 index、optimizer 應該選一個 + 二級 filter、但實際 <code>type=ALL</code>（full scan）。</p>
<p>EXPLAIN ANALYZE 顯示：</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">-&gt; Filter: ((events.user_id = 12345) and (events.created_at between ...))  (cost=2M rows=100)
</span></span><span class="line"><span class="ln">2</span><span class="cl">    -&gt; Table scan on events  (cost=2M rows=10000000)  (actual time=0.1..30s ...)</span></span></code></pre></div><p>問題：optimizer estimated rows=100、實際 <em>cardinality estimation</em> 失準（distribution skew）、選了 ALL。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 用 composite index 直接 cover 兩個條件
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_user_created</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">);</span></span></span></code></pre></div><p>Composite index 讓 optimizer 看到 <em>單一 index 直接 satisfy 兩個 predicate</em>、走 range scan + index condition pushdown。30 秒降到 200ms。</p>
<h3 id="case-38-秒--30ms--subquery-沒-unnest">Case 3：8 秒 → 30ms — Subquery 沒 unnest</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">customer_id</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">vip_level</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">3</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>5.6 之前 MySQL 把 <code>IN (subquery)</code> 寫成 <em>correlated subquery</em>、外表每 row 都 re-run subquery、極慢。5.6+ 加 subquery unnesting、轉換成 JOIN，但某些情況 unnest 失敗。</p>
<p>EXPLAIN 顯示：</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">+----+--------------------+-----------+-------+
</span></span><span class="line"><span class="ln">2</span><span class="cl">| id | select_type        | table     | type  |
</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">|  1 | PRIMARY            | orders    | ALL   |
</span></span><span class="line"><span class="ln">5</span><span class="cl">|  2 | DEPENDENT SUBQUERY | customers | unique_subquery |
</span></span><span class="line"><span class="ln">6</span><span class="cl">+----+--------------------+-----------+-------+</span></span></code></pre></div><p><code>DEPENDENT SUBQUERY</code> 是危險訊號。修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 手動改寫成 JOIN
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">vip_level</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span></span></span></code></pre></div><p>或用 <code>EXISTS</code>（部分 case 比 <code>IN</code> plan 好）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">c</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">vip_level</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">3</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>不同寫法 plan 差異需用 EXPLAIN 驗證、不能假設「JOIN 一定比 IN 快」。</p>
<h3 id="case-42-秒--100ms--derived-table-沒-materialize">Case 4：2 秒 → 100ms — Derived table 沒 materialize</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">order_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">counts</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">order_count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>5.6 之前 derived table（FROM subquery）每次 query 都 re-run、慢。5.7+ 有 <em>derived table materialization</em>、但 optimizer 有時不觸發。</p>
<p>EXPLAIN 顯示：</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">+----+-------------+-------+------+
</span></span><span class="line"><span class="ln">2</span><span class="cl">| id | select_type | table | type |
</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">|  1 | PRIMARY     | o     | ALL  |
</span></span><span class="line"><span class="ln">5</span><span class="cl">|  2 | DERIVED     | orders| ALL  |  -- 沒 materialize、每次 join 都跑
</span></span><span class="line"><span class="ln">6</span><span class="cl">+----+-------------+-------+------+</span></span></code></pre></div><p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 顯式用 CTE + 改寫
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">WITH</span><span class="w"> </span><span class="n">counts</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">order_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">counts</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">order_count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>但記得 MySQL CTE 也不 materialize 預設、可能要 <em>temporary table</em> 才強制 cache：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TEMPORARY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">counts</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">order_count</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">customer_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">counts</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">counts</span><span class="p">.</span><span class="n">order_count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TEMPORARY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">counts</span><span class="p">;</span></span></span></code></pre></div><h3 id="case-510-秒--100ms--optimizer-選-index-不對">Case 5：10 秒 → 100ms — Optimizer 選 index 不對</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">active</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p><code>users</code> 有 <code>idx_active</code> (selectivity 高) 跟 <code>idx_age</code> (selectivity 低)。Optimizer 選 <code>idx_age</code>、scan 60% rows、慢。</p>
<p>EXPLAIN：<code>key: idx_age</code> — 但 active=1 filter 後 row 量 &lt; 5%。</p>
<p>修法選一：</p>
<ol>
<li>
<p><strong>Index hint 強制</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="n">USE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">(</span><span class="n">idx_active</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">active</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p><strong>Composite index 取代</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_active_age</span><span class="w"> </span><span class="p">(</span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">age</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_age</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p><strong>Optimizer hint (8.0+)</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ INDEX(users idx_active) */</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">active</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div></li>
</ol>
<p>Composite index 是最持久解（不依賴 hint）。Index hint 是 quick fix、但對 future schema change 脆弱。</p>
<h2 id="explain-三層工具">EXPLAIN 三層工具</h2>
<h3 id="tool-1explain--query-plan-preview">Tool 1：EXPLAIN — query plan preview</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><p>輸出每個 step 的 <em>估計</em> cost / row count / key used。<strong>用於 quick check plan 形狀</strong>。</p>
<p>關鍵欄位：</p>
<ul>
<li><code>type</code>：access type（ALL &lt; index &lt; range &lt; ref &lt; eq_ref &lt; const）、ALL / index 是警訊</li>
<li><code>key</code>：實際選的 index、可能跟 <code>possible_keys</code> 不同</li>
<li><code>rows</code>：估計 scan row 數</li>
<li><code>Extra</code>：<code>Using filesort</code> / <code>Using temporary</code> / <code>Using index condition</code> 等行為標記</li>
</ul>
<h3 id="tool-2explain-analyze--實際執行統計">Tool 2：EXPLAIN ANALYZE — 實際執行統計</h3>
<p>8.0+ 加的。差別：實際 run query、回實際 row count / time、跟 estimate 對比。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><p>輸出格式（tree format）：</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">-&gt; Nested loop inner join  (cost=2.4e6 rows=100000) (actual time=0.05..3.2 rows=10000 loops=1)
</span></span><span class="line"><span class="ln">2</span><span class="cl">    -&gt; Index range scan on orders using idx_created (cost=2.4e6 rows=10000) (actual time=0.04..3.0 rows=10000 loops=1)
</span></span><span class="line"><span class="ln">3</span><span class="cl">    -&gt; Single-row index lookup on customers using PRIMARY (cost=1 rows=1) (actual time=0.0001..0.0001 rows=1 loops=10000)</span></span></code></pre></div><p>關鍵：對比 <code>cost / rows</code>（estimate） vs <code>actual time / rows</code>。如果 estimate=100K / actual=10M、optimizer 嚴重低估、可能選錯 plan。</p>
<h3 id="tool-3optimizer-trace--看-optimizer-為何選這個-plan">Tool 3：Optimizer Trace — 看 optimizer 為何選這個 plan</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">optimizer_trace</span><span class="o">=</span><span class="s1">&#39;enabled=on&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">optimizer_trace</span><span class="p">;</span></span></span></code></pre></div><p>輸出 JSON、列每個 step optimizer 考慮過的 plan + cost estimate + 為什麼選最終 plan。<strong>用於：optimizer 行為跟你預期不符時、debug 為什麼</strong>。</p>
<p>複雜 query 的 optimizer trace 可能 100+ KB、要熟讀 JSON 結構。production debug tool、不是常規 tool。</p>
<h2 id="optimizer-hint-vs-index-hint">Optimizer hint vs Index hint</h2>
<p>兩種 hint、語法不同、行為不同：</p>
<h3 id="index-hint5x-就有">Index hint（5.x 就有）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="n">USE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">(</span><span class="n">idx_name</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="k">FORCE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">(</span><span class="n">idx_name</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="k">IGNORE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">(</span><span class="n">idx_name</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><ul>
<li><code>USE INDEX</code>：建議 optimizer 用這 index、但 optimizer 仍可拒絕</li>
<li><code>FORCE INDEX</code>：強制用、optimizer 不能拒絕</li>
<li><code>IGNORE INDEX</code>：禁止用</li>
</ul>
<p><strong>問題</strong>：</p>
<ul>
<li>對 table name 寫死、refactor / partition 時容易斷</li>
<li><code>FORCE</code> 太強、可能讓 optimizer 跑得比沒 hint 更慢（forced index 不是最佳 plan）</li>
</ul>
<h3 id="optimizer-hint80">Optimizer hint（8.0+）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ INDEX(table_name idx_name) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ JOIN_ORDER(t1, t2, t3) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t1</span><span class="p">,</span><span class="w"> </span><span class="n">t2</span><span class="p">,</span><span class="w"> </span><span class="n">t3</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ HASH_JOIN(t1 t2) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t1</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">t2</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ NO_INDEX_MERGE(table) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><ul>
<li>更細粒度（join order / join method / index 選擇分開）</li>
<li>注入 query comment 內、不污染 SQL syntax</li>
<li>比 index hint 安全：optimizer 看 hint 但仍走 plan space search</li>
</ul>
<p><strong>推薦</strong>：</p>
<ul>
<li>8.0+ 用 optimizer hint</li>
<li>5.7 仍用 index hint、但謹慎 — 觀察 hint 加上去後 <em>實際 plan</em> 是否真的好</li>
</ul>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-statistics-過時--optimizer-估錯-row-count">1. Statistics 過時 — optimizer 估錯 row count</h3>
<p><code>information_schema.STATISTICS</code> 紀錄每個 index 的 cardinality。如果 <em>過 1 個月沒 ANALYZE</em>、statistics 跟實際資料 distribution 嚴重偏差、optimizer 估計錯。</p>
<p>修法：</p>
<ul>
<li>定期跑 <code>ANALYZE TABLE</code>（大表改 nightly cron）</li>
<li>8.0+ <code>innodb_stats_auto_recalc=ON</code> 預設、但變更超過 10% row 才觸發</li>
<li>設 <code>innodb_stats_persistent=ON</code>（預設、把 statistics 存 disk）+ <code>innodb_stats_persistent_sample_pages=20</code>（提高 sample 精度）</li>
</ul>
<h3 id="2-forced-index-用錯--hint-比沒-hint-還慢">2. Forced index 用錯 — Hint 比沒 hint 還慢</h3>
<p><code>FORCE INDEX (idx)</code> 強制 optimizer 用、但 <em>idx 不是最佳</em> 時、query 變慢。常見：開發 staging 試出 <code>FORCE INDEX</code> 有效、production 資料 distribution 不同、forced index 反而慢。</p>
<p>修法：</p>
<ul>
<li>用 <code>USE INDEX</code> 而不是 <code>FORCE INDEX</code>（optimizer 仍可換）</li>
<li>不依賴 hint、用 composite index / 重寫 query 達到目的</li>
<li>已用 hint 的 query 進 <em>staging review 機制</em>、確認 plan 仍合理</li>
</ul>
<h3 id="3-hash-join-沒觸發--equality-是-expression">3. Hash join 沒觸發 — Equality 是 expression</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">b</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">a</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">b</span><span class="p">.</span><span class="n">parent_id</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p><code>b.parent_id + 1</code> 是 expression、不是 raw column、optimizer 不選 hash join、用 nested loop。</p>
<p>修法：</p>
<ul>
<li>Schema 改：把 <code>parent_id + 1</code> 變成 <em>generated column</em></li>
<li>Query 改：JOIN 之前 <em>預計算 expression</em> 存 temp table</li>
<li>或 <code>/*+ HASH_JOIN(a b) */</code> 顯式（但 plan 仍可能拒絕）</li>
</ul>
<h3 id="4-range-scan-退化-all--cardinality-估計太低">4. Range scan 退化 ALL — Cardinality 估計太低</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">col</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span></span></span></code></pre></div><p><code>IN</code> 1000 value、optimizer 預估「range scan 太多 lookup、不如 ALL」、選 full table scan。對 <em>中型表</em>（1M row）通常 IN 仍快、但 optimizer 估錯。</p>
<p>修法：</p>
<ul>
<li>
<p><code>IN</code> 拆成 <em>temp table JOIN</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TEMPORARY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">in_values</span><span class="w"> </span><span class="p">(</span><span class="n">val</span><span class="w"> </span><span class="nb">INT</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">in_values</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="mi">2</span><span class="p">),</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">t</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">in_values</span><span class="w"> </span><span class="n">iv</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">t</span><span class="p">.</span><span class="n">col</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">iv</span><span class="p">.</span><span class="n">val</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>或 <code>optimizer_switch='index_merge=on'</code>（multi-value IN 可能走 index merge）</p>
</li>
<li>
<p>或大 <code>IN</code> 改 application 層拆批 query</p>
</li>
</ul>
<h3 id="5-derived-table-materialization-off--重複-scan">5. Derived table materialization off — 重複 scan</h3>
<p><code>optimizer_switch='derived_merge=on'</code>（預設 ON、derived table 自動 inline merge）某些 query 反而慢（merge 後 plan 變複雜）。或 <em>反向問題</em>：derived table <em>沒</em> materialize、每次都 re-run。</p>
<p>修法：</p>
<ul>
<li>看 EXPLAIN 是否有 <code>DERIVED</code> row、確認 materialization 行為</li>
<li>可 <code>optimizer_switch='derived_merge=off'</code> 強制 materialize（影響整個 connection、謹慎用）</li>
<li>大 derived table 改 explicit <em>temporary table</em> 完全控制</li>
</ul>
<h2 id="跟-postgresql-explain-對比">跟 PostgreSQL EXPLAIN 對比</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>MySQL</th>
          <th>PostgreSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query plan preview</td>
          <td><code>EXPLAIN</code></td>
          <td><code>EXPLAIN</code></td>
      </tr>
      <tr>
          <td>實際執行統計</td>
          <td><code>EXPLAIN ANALYZE</code> (8.0+)</td>
          <td><code>EXPLAIN ANALYZE</code></td>
      </tr>
      <tr>
          <td>Optimizer 內部 trace</td>
          <td>optimizer_trace (JSON)</td>
          <td><code>auto_explain</code> extension</td>
      </tr>
      <tr>
          <td>Format</td>
          <td>TABLE / JSON / TREE</td>
          <td>TEXT / JSON / XML / YAML</td>
      </tr>
      <tr>
          <td>Parallel query plan</td>
          <td>受限（8.0 限 hash join）</td>
          <td>Full（PG 10+ parallel scan / aggregate / join）</td>
      </tr>
      <tr>
          <td>Index merge</td>
          <td>有</td>
          <td>有 (<code>bitmap index scan</code>)</td>
      </tr>
      <tr>
          <td>Genetic Query Optimizer</td>
          <td>無</td>
          <td>PG 有（適合 &gt; 12 table JOIN）</td>
      </tr>
      <tr>
          <td>Cost estimate accuracy</td>
          <td>中（histograms 8.0+）</td>
          <td>高（成熟 statistics）</td>
      </tr>
  </tbody>
</table>
<p>PG optimizer 整體更成熟、複雜 OLAP-style query plan 更穩定。MySQL 8.0 補了不少（histograms、hash join、derived table merge）、簡單 OLTP query 已 OK、複雜 query 仍弱。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-modern-sql-features">跟 Modern SQL Features</h3>
<p>CTE / window function / lateral / hash join 都改變 query plan space、optimizer 跟著要識別新 pattern。8.0 optimizer 對新 SQL feature plan 仍有改進空間。詳見 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">Modern SQL Features</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p>Query plan 受 <em>buffer pool hit rate</em> 影響 — optimizer 假設 random IO cost、實際資料在 buffer pool 內讀取快。Buffer pool 不夠時 plan estimate 失真。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-proxysql">跟 ProxySQL</h3>
<p>ProxySQL query rule 不影響 optimizer plan、但可以 <em>rewrite query</em>（rule engine 的 <code>replace_pattern</code>）— 用於把 application 寫不好的 query 改成 optimizer-friendly 形式、application 不必改。詳見 <a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL 配置</a>。</p>
<h3 id="跟-lock-contention">跟 Lock Contention</h3>
<p>Slow query 持有 lock 久、其他 query wait、整個 cluster lock contention 爆。Query optimization 不只是 latency 問題、也是 <em>lock 影響範圍</em> 問題。詳見 <em>Lock Contention deep dive</em> 篇（待寫）。</p>
<h3 id="跟-partitioning">跟 Partitioning</h3>
<p>Partition pruning 是 optimizer 決定的、<code>EXPLAIN PARTITIONS</code> 看 partition 命中。partition + index 組合可能比 single big table + index 慢（cross-partition query overhead）。詳見 <em>Partitioning</em> 篇（待寫）。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<p>Production 持續 monitor：</p>
<ul>
<li><code>Performance_schema.events_statements_summary_by_digest</code>：每個 query digest 的累計 time / row examined / row sent</li>
<li><code>slow_query_log</code>：slow query 進 log 檔（<code>long_query_time=1</code>）</li>
<li><code>sys.statements_with_full_table_scans</code>：列 query 用 full scan 的歷史</li>
<li><code>sys.schema_unused_indexes</code>：列從未用過的 index、可以 drop 省 write cost</li>
</ul>
<p>把這些丟進 Datadog / Percona Monitoring &amp; Management 做 trend analysis。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>（hash join / window / CTE 的 plan 議題）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（buffer pool 對 plan estimate）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（query rewrite 整合）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（add index 走 OSC）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PostgreSQL Query Optimization</a>（PG sibling、EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/index-selection/" data-link-title="PostgreSQL Index Selection：B-tree / GIN / GiST / BRIN / Hash 對應 workload 的決策樹" data-link-desc="PG 有 6 種 index method（B-tree / Hash / GIN / GiST / SP-GiST / BRIN）跟 partial / expression / covering 三種變體、不是「都用 B-tree 就好」。每種 index 有自己的 query pattern、儲存代價、write amplification 跟 maintenance 成本。本文走 6 種 index 的適用 workload 對照、決策樹、partial / expression / covering / multi-column 變體、5 production 踩雷（過度 index / partial 條件不對 / B-tree 對 JSON 無效 / BRIN 對非 correlated 資料無效 / multi-column 順序錯）、跟 query-optimization 的 EXPLAIN 互補">PostgreSQL Index Selection</a>（B-tree / GIN / GiST / BRIN 決策樹 vs MySQL B-tree only）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a>（EXPLAIN ANALYZE 對比）</li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/optimization.html">MySQL Optimization</a> / <a href="https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html">Optimizer Hints</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/query-optimization/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/query-optimization/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>query optimization&lt;/em> — EXPLAIN ANALYZE / auto_explain / pg_hint_plan 三層工具跟 4 個實際 case。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="4-個常見-production-case">4 個常見 production case&lt;/h2>
&lt;p>PG query 慢的 root cause 多數是 &lt;em>planner 選錯 plan&lt;/em>。從以下 4 個 case 進入 query optimization：&lt;/p>
&lt;h3 id="case-15-秒--50ms--seq-scan-vs-index">Case 1：5 秒 → 50ms — Seq scan vs index&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 慢 (5 秒)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;TW&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-05-01&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EXPLAIN (ANALYZE, BUFFERS)&lt;/code>：&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">Hash Join (cost=20000..50000 rows=100 width=...) (actual time=4900..5000 rows=10000)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> -&amp;gt; Seq Scan on customers c (cost=0..20000 rows=1000000 width=...)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> Filter: (region = &amp;#39;TW&amp;#39;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> Rows Removed by Filter: 900000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> -&amp;gt; Hash (cost=...)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> -&amp;gt; Index Scan on orders_created_idx&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>問題：&lt;code>customers.region&lt;/code> 沒 index、planner 選 seq scan、實際 region=TW 只 10% row。修法：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CONCURRENTLY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_customers_region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">region&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ANALYZE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 更新 statistics、讓 planner 看到新 index&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完 5 秒降 50ms。&lt;/p>
&lt;h3 id="case-230-秒--200ms--hash-join-沒觸發用-nested-loop">Case 2：30 秒 → 200ms — Hash join 沒觸發、用 nested loop&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">o&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EXPLAIN ANALYZE 顯示 &lt;em>Nested Loop&lt;/em> 跑 1M 次 inner loop、執行 30 秒。Planner 估錯 row count、選 nested loop。Hash join 應該 &amp;lt; 200ms。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>query optimization</em> — EXPLAIN ANALYZE / auto_explain / pg_hint_plan 三層工具跟 4 個實際 case。</p></blockquote>
<hr>
<h2 id="4-個常見-production-case">4 個常見 production case</h2>
<p>PG query 慢的 root cause 多數是 <em>planner 選錯 plan</em>。從以下 4 個 case 進入 query optimization：</p>
<h3 id="case-15-秒--50ms--seq-scan-vs-index">Case 1：5 秒 → 50ms — Seq scan vs index</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 慢 (5 秒)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="p">;</span></span></span></code></pre></div><p><code>EXPLAIN (ANALYZE, BUFFERS)</code>：</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">Hash Join  (cost=20000..50000 rows=100 width=...) (actual time=4900..5000 rows=10000)
</span></span><span class="line"><span class="ln">2</span><span class="cl">  -&gt;  Seq Scan on customers c  (cost=0..20000 rows=1000000 width=...)
</span></span><span class="line"><span class="ln">3</span><span class="cl">      Filter: (region = &#39;TW&#39;)
</span></span><span class="line"><span class="ln">4</span><span class="cl">      Rows Removed by Filter: 900000
</span></span><span class="line"><span class="ln">5</span><span class="cl">  -&gt;  Hash  (cost=...)
</span></span><span class="line"><span class="ln">6</span><span class="cl">      -&gt;  Index Scan on orders_created_idx</span></span></code></pre></div><p>問題：<code>customers.region</code> 沒 index、planner 選 seq scan、實際 region=TW 只 10% row。修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">idx_customers_region</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">customers</span><span class="p">(</span><span class="n">region</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ANALYZE</span><span class="w"> </span><span class="n">customers</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 更新 statistics、讓 planner 看到新 index</span></span></span></code></pre></div><p>加完 5 秒降 50ms。</p>
<h3 id="case-230-秒--200ms--hash-join-沒觸發用-nested-loop">Case 2：30 秒 → 200ms — Hash join 沒觸發、用 nested loop</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">name</span><span class="p">;</span></span></span></code></pre></div><p>EXPLAIN ANALYZE 顯示 <em>Nested Loop</em> 跑 1M 次 inner loop、執行 30 秒。Planner 估錯 row count、選 nested loop。Hash join 應該 &lt; 200ms。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ANALYZE</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ANALYZE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 提高 default_statistics_target 對 critical column
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="k">STATISTICS</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ANALYZE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>統計精度提升、planner 估 row count 準、自動切 hash join。</p>
<h3 id="case-38-秒--100ms--multi-column-統計缺">Case 3：8 秒 → 100ms — Multi-column 統計缺</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TW&#39;</span><span class="p">;</span></span></span></code></pre></div><p><code>status = 'pending'</code> 5% row、<code>region = 'TW'</code> 10% row。Planner 假設兩 column 獨立、估 0.5% (5K row)。實際 status=&lsquo;pending&rsquo; 跟 region=&lsquo;TW&rsquo; 強相關（TW 訂單多 pending）、實際 4% (40K row)。Planner 估錯 8x、選錯 plan。</p>
<p>修法（PG 10+）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">STATISTICS</span><span class="w"> </span><span class="n">stats_orders_status_region</span><span class="w"> </span><span class="p">(</span><span class="n">dependencies</span><span class="p">,</span><span class="w"> </span><span class="n">ndistinct</span><span class="p">,</span><span class="w"> </span><span class="n">mcv</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ANALYZE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 之後 planner 知道 status+region 相關度、估準</span></span></span></code></pre></div><h3 id="case-420-秒--5-秒--parallel-query-沒觸發">Case 4：20 秒 → 5 秒 — Parallel query 沒觸發</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">),</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">region</span><span class="p">;</span></span></span></code></pre></div><p><code>orders</code> 100M row、預期 PG parallel scan + parallel aggregate、實際 single worker 跑 20 秒。</p>
<p>EXPLAIN：<code>Workers Planned: 0</code>。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">max_parallel_workers_per_gather</span> <span class="o">=</span> <span class="s">4</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">max_parallel_workers</span> <span class="o">=</span> <span class="s">8</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">max_worker_processes</span> <span class="o">=</span> <span class="s">16</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">parallel_setup_cost</span> <span class="o">=</span> <span class="s">100        # 預設 1000、降低讓 planner 更敢 parallel</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">parallel_tuple_cost</span> <span class="o">=</span> <span class="s">0.01       # 預設 0.1</span></span></span></code></pre></div><p>並行後 5 秒。</p>
<h2 id="explain-三層工具">EXPLAIN 三層工具</h2>
<h3 id="tool-1explain--plan-preview">Tool 1：EXPLAIN — Plan preview</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><p>輸出每個 node 的 <em>估計</em> cost / row count / width。<strong>用於 quick plan check</strong>。</p>
<p>關鍵欄位：</p>
<ul>
<li><code>Plan node 類型</code>：<code>Seq Scan</code> &lt; <code>Index Scan</code> &lt; <code>Index Only Scan</code>、警訊看 <em>unexpected</em> node type</li>
<li><code>cost=START..END</code>：planner 估的 cost、START 是 startup cost、END 是 total</li>
<li><code>rows</code>：估計 output row 數</li>
<li><code>width</code>：每 row average byte（影響 sort / hash memory）</li>
</ul>
<h3 id="tool-2explain-analyze--實際執行--對比-estimate">Tool 2：EXPLAIN ANALYZE — 實際執行 + 對比 estimate</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="p">(</span><span class="k">ANALYZE</span><span class="p">,</span><span class="w"> </span><span class="n">BUFFERS</span><span class="p">,</span><span class="w"> </span><span class="k">VERBOSE</span><span class="p">)</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><p>差別：實際 <em>跑 query</em>、輸出實際 row count / time、跟 estimate 對比：</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">Hash Join  (cost=20000..50000 rows=100) (actual time=400..500 rows=10000 loops=1)</span></span></code></pre></div><p><code>rows=100 (estimate)</code> vs <code>rows=10000 (actual)</code> — 估錯 100x、planner 可能選錯 plan。<code>BUFFERS</code> 顯示 disk read vs buffer cache hit。</p>
<p><strong>注意</strong>：EXPLAIN ANALYZE <em>實際跑 query</em>、修改性 query（UPDATE / DELETE）會真的改 data。讀 query 安全。修改性 query 包 transaction：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;x&#39;</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ROLLBACK</span><span class="p">;</span></span></span></code></pre></div><h3 id="tool-3auto_explain--production-query-自動-capture">Tool 3：auto_explain — Production query 自動 capture</h3>
<p><code>auto_explain</code> extension 自動 log slow query 的 plan：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">shared_preload_libraries</span> <span class="o">=</span> <span class="s">&#39;auto_explain&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">auto_explain.log_min_duration</span> <span class="o">=</span> <span class="s">&#39;1s&#39;    # 超過 1 秒 log plan</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">auto_explain.log_analyze</span> <span class="o">=</span> <span class="s">on            # 含 ANALYZE 統計</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">auto_explain.log_buffers</span> <span class="o">=</span> <span class="s">on</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">auto_explain.log_format</span> <span class="o">=</span> <span class="s">&#39;json&#39;         # JSON 格式給工具消費</span></span></span></code></pre></div><p>Production slow query 自動進 log、不必手動 EXPLAIN。組合 pg_stat_statements + auto_explain 是 PG 標準 query observability。</p>
<h2 id="pg_hint_plan-vs-planner-guc">pg_hint_plan vs Planner GUC</h2>
<p>PG 兩種方式 nudge planner：</p>
<h3 id="planner-gucglobal">Planner GUC（global）</h3>
<p><code>postgresql.conf</code> 內：</p>
<ul>
<li><code>enable_seqscan = off</code> — 禁用 seq scan（force index）</li>
<li><code>enable_nestloop = off</code> — 禁用 nested loop（force hash/merge join）</li>
<li><code>random_page_cost = 1.1</code> — SSD 設低（預設 4 是 HDD assumption）</li>
<li><code>effective_cache_size = '16GB'</code> — buffer pool + OS cache 估、影響 planner</li>
</ul>
<p>GUC 是 <em>global</em> — 影響所有 query。對 <em>單一 query 用 hint</em>：</p>
<h3 id="pg_hint_plan-extensionper-query-hint">pg_hint_plan extension（per-query hint）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 強制特定 plan
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="cm">/*+ IndexScan(orders idx_orders_status) NestLoop(orders customers) */</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="p">...;</span></span></span></code></pre></div><p>Hint 形態：</p>
<ul>
<li><code>IndexScan(t1 idx_name)</code> — 強制 index scan</li>
<li><code>SeqScan(t1)</code> — 強制 seq scan</li>
<li><code>HashJoin(t1 t2)</code> / <code>NestLoop(t1 t2)</code> / <code>MergeJoin(t1 t2)</code></li>
<li><code>Leading(t1 t2 t3)</code> — 強制 join order</li>
<li><code>Rows(t1 t2 #100)</code> — 強制 row 估計</li>
</ul>
<p><strong>推薦</strong>：</p>
<ul>
<li>全 cluster 行為：用 GUC（如 <code>random_page_cost</code>）</li>
<li>單 query 行為：用 pg_hint_plan（不污染其他 query）</li>
<li>不要過度 hint — planner 多數時候 <em>是對的</em>、hint 是 last resort</li>
</ul>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-statistics-過時--planner-估錯-row-count">1. Statistics 過時 — Planner 估錯 row count</h3>
<p><code>ANALYZE</code> 是 autovacuum 一部分、預設 <em>autovacuum_analyze_scale_factor=0.1</em>（10% row 變動才 analyze）。對 <em>快速 grow 的表</em>（log / event）、ANALYZE 跟不上、planner 用過時 statistics。</p>
<p>修法：</p>
<ul>
<li>
<p>對 critical table 設 <em>較 aggressive autovacuum_analyze_scale_factor</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="n">autovacuum_analyze_scale_factor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">02</span><span class="p">);</span></span></span></code></pre></div></li>
<li>
<p>對 <em>大批量寫入後</em>、手動 <code>ANALYZE events;</code></p>
</li>
<li>
<p>監控 <code>pg_stat_user_tables.last_analyze</code> — 跟 row count 比、判定是否需手動 trigger</p>
</li>
</ul>
<h3 id="2-multi-column-statistics--planner-假設-column-獨立">2. Multi-column statistics — Planner 假設 column 獨立</h3>
<p>如 Case 3、單 column statistics 對 <em>相關 column</em> 估錯。</p>
<p>修法：</p>
<ul>
<li>對 <em>常一起 query 的 column 組合</em>、建 <code>CREATE STATISTICS</code>（PG 10+）</li>
<li>3 種 type：<code>dependencies</code>（functional dependency）、<code>ndistinct</code>（multi-column distinct count）、<code>mcv</code>（most common value combinations）</li>
<li>設完 <em>必須跑 ANALYZE</em> 才生效</li>
</ul>
<h3 id="3-cost-base-setting-不對齊硬體--planner-偏-seq-scan">3. Cost-base setting 不對齊硬體 — Planner 偏 seq scan</h3>
<p>預設 <code>random_page_cost = 4</code>、<code>seq_page_cost = 1</code> 是 <em>HDD assumption</em>（random IO 比 sequential 慢 4x）。SSD / NVMe random / seq IO 差別小、planner 不該 4x penalty random。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SSD
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">random_page_cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">.</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- NVMe
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">random_page_cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_reload_conf</span><span class="p">();</span></span></span></code></pre></div><p><code>random_page_cost</code> 改了 planner 對 index scan 的 cost 估計更準、自動選 index 更積極。</p>
<h3 id="4-effective_cache_size-不對齊實際-ram">4. <code>effective_cache_size</code> 不對齊實際 RAM</h3>
<p><code>effective_cache_size</code> 預設 4 GB、planner 假設 buffer pool + OS cache 共 4 GB。實際 server 64 GB RAM、<code>shared_buffers = 16GB</code>、OS page cache ~30 GB、實際可用 cache 46 GB。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">effective_cache_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;46GB&#39;</span><span class="p">;</span><span class="w">  </span><span class="c1">-- shared_buffers + OS cache 估</span></span></span></code></pre></div><p>提升後 planner 估 query 多數 page 在 cache、降低 <em>估計 random IO cost</em>、選 index 更積極。</p>
<h3 id="5-parallel-query-不觸發">5. Parallel query 不觸發</h3>
<p>預設 <code>max_parallel_workers_per_gather = 2</code>、有些 workload 不夠。或 <em>table size 太小</em>、<code>min_parallel_table_scan_size = 8MB</code> 預設、小表不 parallel。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">max_parallel_workers_per_gather</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">4</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">parallel_setup_cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">parallel_tuple_cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">01</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">min_parallel_table_scan_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;0&#39;</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 任何 size 都 parallel</span></span></span></code></pre></div><p>監控 <code>EXPLAIN</code> 的 <code>Workers Planned</code> 數量、看是否真 parallel。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<p>Production 持續 monitor：</p>
<ul>
<li><code>pg_stat_statements</code>：每個 query digest 累計 calls / time / rows / IO</li>
<li><code>auto_explain</code> log：slow query 的實際 plan + ANALYZE 統計</li>
<li><code>pg_stat_user_tables.last_analyze</code> / <code>last_autoanalyze</code>：statistics 新鮮度</li>
<li><code>pg_stat_user_indexes.idx_scan</code>：每個 index 使用次數 — 0 表示沒用、可考慮 drop</li>
</ul>
<p>把這些丟進 Datadog / Prometheus（用 <code>postgres_exporter</code> / <code>pg_exporter</code>）做 trend analysis。</p>
<h2 id="跟-mysql-query-optimization-對照">跟 MySQL Query Optimization 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG</th>
          <th>MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query plan preview</td>
          <td><code>EXPLAIN</code></td>
          <td><code>EXPLAIN</code></td>
      </tr>
      <tr>
          <td>實際執行統計</td>
          <td><code>EXPLAIN ANALYZE</code></td>
          <td><code>EXPLAIN ANALYZE</code> (8.0+)</td>
      </tr>
      <tr>
          <td>Auto-capture</td>
          <td><code>auto_explain</code> extension</td>
          <td><code>slow_query_log</code> + <code>pt-query-digest</code></td>
      </tr>
      <tr>
          <td>Optimizer trace</td>
          <td>log_planner_stats / log_executor_stats</td>
          <td><code>optimizer_trace</code> (JSON)</td>
      </tr>
      <tr>
          <td>Per-query hint</td>
          <td><code>pg_hint_plan</code> extension</td>
          <td>optimizer hint comment (<code>/*+ */</code>)</td>
      </tr>
      <tr>
          <td>Multi-column statistics</td>
          <td><code>CREATE STATISTICS</code></td>
          <td>無原生（依賴 index 統計）</td>
      </tr>
      <tr>
          <td>Parallel query</td>
          <td>Full (scan / agg / join, PG 9.6+)</td>
          <td>受限 (8.0 hash join)</td>
      </tr>
      <tr>
          <td>Cost-base setting</td>
          <td>random_page_cost / effective_cache_size</td>
          <td>隱性、optimizer 預設</td>
      </tr>
  </tbody>
</table>
<p>PG planner 整體成熟、複雜 OLAP-style query 處理較好。MySQL 8.0 補了不少（histograms / hash join）但複雜 query 仍弱於 PG。詳見 <a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">MySQL Query Optimization</a>。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-autovacuum-tuning">跟 Autovacuum Tuning</h3>
<p>ANALYZE 是 autovacuum 一部分、autovacuum 跟不上 → statistics 過時 → planner 估錯。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h3 id="跟-replication-topology">跟 Replication Topology</h3>
<p>Standby 上跑 query 用同 statistics（streaming replication copy 整個 system catalog）、planner 行為一致。但 <em>standby 有 hot_standby_feedback</em> 影響 primary autovacuum / ANALYZE 行為。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-partitioning">跟 Partitioning</h3>
<p>Partition pruning 跟 query plan 緊密 — <code>EXPLAIN</code> 看是否 prune 對的 partition。詳見 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">Declarative Partitioning</a>。</p>
<h2 id="何時用-pg_hint_plan-vs-guc">何時用 pg_hint_plan vs GUC</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全 cluster 行為（如 SSD random_page_cost）</td>
          <td>GUC</td>
      </tr>
      <tr>
          <td>單一 critical query 強制特定 plan</td>
          <td>pg_hint_plan</td>
      </tr>
      <tr>
          <td>暫時 disable 某類 plan 給 debug</td>
          <td><code>SET enable_xxx=off</code> per-session</td>
      </tr>
      <tr>
          <td>Production stable use</td>
          <td>GUC + multi-column statistics 為主、hint 為 last resort</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（ANALYZE 跟 statistics 新鮮度）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（standby planner 行為）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">PG Declarative Partitioning</a>（partition pruning）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">MySQL Query Optimization</a>（sibling、不同 optimizer 成熟度）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/sql-explain.html">EXPLAIN</a> / <a href="https://github.com/ossc-db/pg_hint_plan">pg_hint_plan</a> / <a href="https://www.postgresql.org/docs/current/auto-explain.html">auto_explain</a></li>
</ul>
]]></content:encoded></item><item><title>3.C21 Goldman Sachs：MSK 遷移 with MirrorMaker 2</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/</guid><description>&lt;p>這個案例的核心責任是說明 MM2 在 production cutover 的真實 tuning 與 LB 整合 pitfall。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Global Investment Research 把 ~12 microservice / 30 instance 從 on-prem Kafka 遷到 MSK；用 MM2 同步 topic / ACL / consumer group / offset、選擇 atomic cutover、整體耗時 ~7 小時。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>把 MM2 預設的 prefixed topic 改成 identical name；遇到 flush timeout（5s → 30s）、request size、NLB idle timeout 350s vs client 540s 衝突。揭露 managed 服務遷移的細節風險集中在「LB / timeout / topic naming」這些 client 端配置、不在 broker 本身。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：cross-region MirrorMaker / managed broker 遷移 / ACL 設計。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware → MSK&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/big-data/how-goldman-sachs-migrated-from-their-on-premises-apache-kafka-cluster-to-amazon-msk/">How Goldman Sachs Migrated from On-Premises Apache Kafka to Amazon MSK&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 MM2 在 production cutover 的真實 tuning 與 LB 整合 pitfall。</p>
<h2 id="觀察">觀察</h2>
<p>Global Investment Research 把 ~12 microservice / 30 instance 從 on-prem Kafka 遷到 MSK；用 MM2 同步 topic / ACL / consumer group / offset、選擇 atomic cutover、整體耗時 ~7 小時。</p>
<h2 id="判讀">判讀</h2>
<p>把 MM2 預設的 prefixed topic 改成 identical name；遇到 flush timeout（5s → 30s）、request size、NLB idle timeout 350s vs client 540s 衝突。揭露 managed 服務遷移的細節風險集中在「LB / timeout / topic naming」這些 client 端配置、不在 broker 本身。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：cross-region MirrorMaker / managed broker 遷移 / ACL 設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware → MSK</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/big-data/how-goldman-sachs-migrated-from-their-on-premises-apache-kafka-cluster-to-amazon-msk/">How Goldman Sachs Migrated from On-Premises Apache Kafka to Amazon MSK</a></li>
</ul>
]]></content:encoded></item><item><title>Kyverno</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/</guid><description>&lt;p>Kyverno 是 K8s-native 的 policy engine、CNCF Incubating（2024 升級）、設計 mindset 把 &lt;em>policy 寫成 YAML&lt;/em> 而不是引入新語言（vs &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a> 的 Rego、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> 也用 Rego）。它的核心不是「更輕量的 OPA」、而是 &lt;em>K8s 專用 policy engine&lt;/em> — 把 Validate / Mutate / Generate / Verify Images / Cleanup 五類動作做成 first-class rule type、跟 K8s admission webhook + GitOps + cosign / Sigstore ecosystem 深度整合。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Kyverno 的定位是 &lt;em>K8s admission controller-shaped policy engine、policy 用 YAML 表達&lt;/em>。底層是 dynamic admission webhook + background controller、頂層 CRD 包含 &lt;em>ClusterPolicy&lt;/em>（cluster 範圍）/ &lt;em>Policy&lt;/em>（namespace 範圍）/ &lt;em>PolicyException&lt;/em>（明確例外）/ &lt;em>ClusterCleanupPolicy&lt;/em>（過期 resource 清理）/ &lt;em>PolicyReport&lt;/em>（CIS / NIST 等審計輸出）。Nirmata 是 Kyverno 商業版、補 policy library / multi-cluster management / audit dashboard / 24x7 support。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a> 比、Kyverno 走 &lt;em>narrow + opinionated&lt;/em> — OPA 是 general-purpose policy engine（K8s / API gateway / Terraform / 自家服務都能用、語言是 Rego）、Kyverno &lt;em>K8s-only + YAML&lt;/em>、學習成本對 K8s admin 接近零。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> 比、Gatekeeper 也是 K8s admission controller 但底層用 OPA + Rego、ConstraintTemplate / Constraint 兩層 CRD；Kyverno 不用 Rego、policy 就是 YAML rule list。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> 的 misconfig scan 比、Trivy 是 &lt;em>scan static manifest&lt;/em>、Kyverno 是 &lt;em>admission gate + background scan&lt;/em>、定位互補不衝突。&lt;/p></description><content:encoded><![CDATA[<p>Kyverno 是 K8s-native 的 policy engine、CNCF Incubating（2024 升級）、設計 mindset 把 <em>policy 寫成 YAML</em> 而不是引入新語言（vs <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> 的 Rego、<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 也用 Rego）。它的核心不是「更輕量的 OPA」、而是 <em>K8s 專用 policy engine</em> — 把 Validate / Mutate / Generate / Verify Images / Cleanup 五類動作做成 first-class rule type、跟 K8s admission webhook + GitOps + cosign / Sigstore ecosystem 深度整合。</p>
<h2 id="服務定位">服務定位</h2>
<p>Kyverno 的定位是 <em>K8s admission controller-shaped policy engine、policy 用 YAML 表達</em>。底層是 dynamic admission webhook + background controller、頂層 CRD 包含 <em>ClusterPolicy</em>（cluster 範圍）/ <em>Policy</em>（namespace 範圍）/ <em>PolicyException</em>（明確例外）/ <em>ClusterCleanupPolicy</em>（過期 resource 清理）/ <em>PolicyReport</em>（CIS / NIST 等審計輸出）。Nirmata 是 Kyverno 商業版、補 policy library / multi-cluster management / audit dashboard / 24x7 support。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> 比、Kyverno 走 <em>narrow + opinionated</em> — OPA 是 general-purpose policy engine（K8s / API gateway / Terraform / 自家服務都能用、語言是 Rego）、Kyverno <em>K8s-only + YAML</em>、學習成本對 K8s admin 接近零。跟 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 比、Gatekeeper 也是 K8s admission controller 但底層用 OPA + Rego、ConstraintTemplate / Constraint 兩層 CRD；Kyverno 不用 Rego、policy 就是 YAML rule list。跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 的 misconfig scan 比、Trivy 是 <em>scan static manifest</em>、Kyverno 是 <em>admission gate + background scan</em>、定位互補不衝突。</p>
<p>關鍵張力：<em>YAML policy 的表達力上限</em> ↔ <em>跨平台統一 policy 的訴求</em>。Kyverno YAML rule 對 90% K8s 場景夠用、但需要跨 K8s / API gateway / Terraform 統一 policy decision 時、Rego 的表達力跟可移植性勝出。要看清楚 policy <em>邊界是否就在 K8s 內</em>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Kyverno 在 K8s 治理 stack 中承擔哪一段（admission gate / mutation / generation / image verify / cleanup）、跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> scan / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">SBOM Tools</a> / Sigstore cosign 怎麼分工</li>
<li>ClusterPolicy / Policy 的 ownership 設計（platform team 還是 app team 寫、誰 review、PolicyException 怎麼治理）</li>
<li>Validate / Mutate / Generate / Verify Images / Cleanup 五類 rule 的使用邊界跟陷阱</li>
<li>何時用 Kyverno、何時走 OPA / Gatekeeper / K8s native ValidatingAdmissionPolicy 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Kyverno deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Policy 是否走 GitOps</strong>：ClusterPolicy / Policy 是否在 Git 版控、走 ArgoCD / Flux sync、policy change 是否經 PR review、staging cluster 跑過 audit mode 才 promote 到 enforce</li>
<li><strong>Mode 配置</strong>：每條 policy 是 <em>Audit</em>（只記、不擋）還是 <em>Enforce</em>（擋 admission）、新規則是否先 audit 觀察 24-48hr 再 enforce、Background scan 是否開（補 admission 不到的 historical drift）</li>
<li><strong>Verify Images 啟用度</strong>：production cluster 是否要求 image 必須通過 cosign signature verify、SBOM attestation 是否驗、policy 是否包含 keyless verify（Fulcio + Rekor）</li>
<li><strong>PolicyException 治理</strong>：例外是否走 PR 申請 + 到期日 + owner、跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 的 exception governance 對齊</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>ClusterPolicy / Policy 結構</strong>：Kyverno policy 是 K8s CRD、結構 <code>spec.rules[]</code> 一條條 rule、每條 rule 有 <code>match</code>（套用對象、kind / namespace / label / name）+ <code>exclude</code>（明確排除）+ rule body（<code>validate</code> / <code>mutate</code> / <code>generate</code> / <code>verifyImages</code> / <code>cleanup</code> 五選一）。ClusterPolicy 套整個 cluster、Policy 套單一 namespace、app team 通常只能改自家 namespace 的 Policy、平台 team 控 ClusterPolicy。</p>
<p><strong>Validate rule</strong>：admission 階段檢查 manifest 是否符合條件、不符合就拒絕。最常見場景 — 禁止 <code>latest</code> tag、要求所有 pod 有 resource limit、禁止 privileged container、要求 specific label。寫法是 <code>validate.pattern</code> 或 <code>validate.deny</code>（後者支援更複雜的 boolean expression）、output 是 admission webhook reject。Validate 是 <em>K8s policy as code</em> 的入門場景、80% 的 ClusterPolicy 都是 Validate rule。</p>
<p><strong>Mutate rule</strong>：admission 階段修改 manifest、把缺的欄位補上或改成符合的值。常見場景 — 自動注入 sidecar（service mesh proxy / log forwarder）、自動加 resource limit default、自動加 label（cost center / owner）、自動把 imagePullPolicy 改成 <code>Always</code>。Mutate 是 OPA / Gatekeeper 做不到的（兩者都偏 Validate-only）、是 Kyverno 的 <em>K8s-specific 強項</em>。陷阱是 mutate 變更後 GitOps diff 會永遠不一致、要在 ArgoCD ignoreDifferences 上對齊。</p>
<p><strong>Generate rule</strong>：cluster event（namespace 建立、resource 變動）觸發、自動建立 <em>關聯 resource</em>。最常見場景 — 新 namespace 自動建 default NetworkPolicy（deny-all egress 起手）、自動建 ResourceQuota / LimitRange、自動 copy ConfigMap / Secret 到新 namespace。Generate 是把 <em>security default</em> 從文件層落到 runtime layer、避免 app team 忘記設 NetworkPolicy 就把整個 cluster 暴露。Generate 也是 OPA / Gatekeeper 做不到、Kyverno 獨有。</p>
<p><strong>Verify Images rule</strong>：admission 階段驗證 container image 的簽章 / SBOM attestation / in-toto provenance。實作底層 <a href="https://docs.sigstore.dev/">Sigstore</a> cosign — keyless 簽章驗 Fulcio CA + Rekor transparency log、key-based 驗 public key、attestation 驗 SLSA provenance / SBOM。production 場景 — internal registry image 必須 cosign 簽 + 來自 trusted CI runner、external image 必須在 allowlist。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a> 的 supply chain attack 防禦邊界。</p>
<p><strong>Cleanup policy</strong>：ClusterCleanupPolicy / CleanupPolicy 是 K8s 1.27+ 引入、Kyverno 1.10+ 支援、按 cron 跑、清掉符合條件的 resource。常見場景 — 過 30 天的 completed Job、過 7 天的 failed Pod、ephemeral namespace（PR preview env）超過 TTL 自動刪。Cleanup 補的是 K8s 沒有 <em>resource lifecycle policy</em> 的洞、TTL controller 只覆蓋 Job / Pod 子集。</p>
<p><strong>Background scan</strong>：除了 admission 攔截 <em>新 resource</em>、Kyverno 定期掃描 <em>已存在 resource</em> 是否違反 policy、結果寫入 PolicyReport CRD。意義是補 <em>歷史 drift</em> — policy 是後來加的、已 deploy 的 resource 不會被 admission 攔到、background scan 才會找出來。production 一定要開、不開等於 policy 只防新犯不抓舊案。</p>
<p><strong>ValidatingAdmissionPolicy (VAP) 整合</strong>：K8s 1.30+ 內建 CEL-based admission policy、不需要 admission webhook（VAP 由 kube-apiserver 直接 enforce、延遲低、不會因為 Kyverno pod 掛掉就讓 admission 失敗）。Kyverno 1.11+ 可以從 ClusterPolicy <em>生成</em> VAP、把簡單 Validate rule 卸載給 K8s native engine、複雜 rule（Mutate / Generate / Verify Images）留在 Kyverno。長期趨勢 — K8s native VAP 會吃掉 Kyverno <em>Validate-only</em> 的場景、Mutate / Generate / Verify Images 仍是 Kyverno 護城河。</p>
<p><strong>GitOps 整合</strong>：ClusterPolicy / Policy 是普通 K8s CRD、走 ArgoCD / Flux sync 沒任何特殊性。staging cluster 跑 Audit mode 24-48hr 看 PolicyReport 有多少違規 → tune rule 或加 PolicyException → 確認沒誤殺再 promote 到 production cluster 的 Enforce mode。對應 <a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a> 的 propose → staging → promote pattern。</p>
<p><strong>Policy Reporter</strong>：OSS dashboard（不是 Kyverno 內建、是社群專案）、把 PolicyReport CRD 視覺化、給 platform team / app team 看 cluster 違規概況。Nirmata 商業版有更完整的 multi-cluster dashboard + 歷史 trend + compliance mapping（CIS / NIST / PCI）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Kyverno</th>
          <th>OPA + Gatekeeper</th>
          <th>OPA standalone</th>
          <th>Conftest</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Policy 語言</td>
          <td>YAML（patterns / deny / preconditions）</td>
          <td>Rego（DSL、表達力強）</td>
          <td>Rego</td>
          <td>Rego</td>
      </tr>
      <tr>
          <td>覆蓋範圍</td>
          <td>K8s only</td>
          <td>K8s only</td>
          <td>K8s / API / Terraform / 任意 JSON 輸入</td>
          <td>CI-time static file（Terraform / Docker）</td>
      </tr>
      <tr>
          <td>Rule 類型</td>
          <td>Validate / Mutate / Generate / Verify Images / Cleanup</td>
          <td>Validate-only（Mutate 是 experimental）</td>
          <td>由 host application 決定</td>
          <td>Validate（CI-time）</td>
      </tr>
      <tr>
          <td>部署形態</td>
          <td>K8s admission webhook + controller</td>
          <td>K8s admission webhook（Gatekeeper 是 OPA 包）</td>
          <td>sidecar / library / standalone server</td>
          <td>CLI（CI pipeline）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>緩 — K8s admin 已熟 YAML</td>
          <td>陡 — 要學 Rego</td>
          <td>陡 — 要學 Rego + host integration</td>
          <td>中 — Rego 但範圍小</td>
      </tr>
      <tr>
          <td>Image signature</td>
          <td>內建 Verify Images（cosign + Sigstore）</td>
          <td>需自己接 cosign CLI</td>
          <td>需自己接</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Background scan</td>
          <td>內建</td>
          <td>gator audit（弱）</td>
          <td>不適用</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>跨 platform 一致</td>
          <td>弱 — K8s only</td>
          <td>弱 — K8s only</td>
          <td>強 — 同份 Rego 跑 K8s / API / Terraform</td>
          <td>強 — CI 跑同份 Rego</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>K8s-heavy + 想用 YAML + 需 Mutate / Generate / Image</td>
          <td>K8s + 已有 Rego 投資 + Validate-only</td>
          <td>跨 K8s / API / Terraform 統一 policy</td>
          <td>CI-time pre-merge 檢查</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — YAML rule 跟 K8s CRD 綁</td>
          <td>中 — Rego 可移植到 OPA standalone</td>
          <td>低 — Rego 跨平台</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 Kyverno 的核心訴求：<em>K8s-only 場景 + 不想學 Rego + 需要 Mutate / Generate / Verify Images 的 K8s-specific 能力</em>。團隊已投資 Rego ecosystem、或 policy 邊界跨 K8s + Terraform + API gateway、走 OPA / Gatekeeper 更合適。CI-time pre-merge 檢查走 Conftest 補位。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Verify Images 進階 — cosign keyless + SBOM attestation</strong>：production-grade image trust 不只驗 signature、要驗 <em>who signed it from where with what build process</em>。keyless 模式驗 Fulcio CA-issued 短期憑證 + Rekor transparency log entry、確認簽章來自 trusted CI runner 的 OIDC identity（例如 <code>https://github.com/myorg/myrepo/.github/workflows/release.yaml@refs/tags/v*</code>）。SBOM attestation 用 <code>verifyImages.attestations</code> 驗 in-toto envelope、確認 image 帶 SLSA provenance + SBOM（<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">CycloneDX / SPDX</a>）。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a> 的 lesson：maintainer takeover 也能簽 image、要靠 build provenance attestation 看出 build process 跟過去不一致。</p>
<p><strong>Mutate policy 跟 GitOps 的張力</strong>：Mutate 自動補欄位、ArgoCD / Flux 會永遠看到 live state 跟 Git state diff。處理方式有三 — <em>ignoreDifferences</em> on specific fields（ArgoCD <code>spec.ignoreDifferences</code>、Flux <code>spec.patches</code>）、<em>把 mutate 改成 validate + 在 PR template 補預設</em>（成本高但 GitOps diff 乾淨）、<em>Mutate at create only</em>（用 <code>mutate.mutateExistingOnPolicyUpdate: false</code>、只在 admission 動、不重複 mutate existing resource）。</p>
<p><strong>Generate policy 跟 multi-tenant security default</strong>：新 namespace 一建立、Generate rule 自動建 default-deny NetworkPolicy + ResourceQuota + LimitRange + 必要 RoleBinding。意義是 <em>security default 從 README 落到 runtime</em>、app team 開新 namespace 不會忘記設安全邊界。陷阱是 generated resource 的 ownership — 預設 Kyverno owns、app team 修改會被 reconcile 回去；要讓 app team 改、用 <code>synchronize: false</code>。</p>
<p><strong>Nirmata Enterprise</strong>：商業版補三件事 — <em>Policy Library</em>（CIS / NIST / PCI / SOC 2 預製 policy pack）、<em>Multi-cluster Management</em>（中央 console 推 policy 到多 cluster + audit dashboard + drift detection）、<em>Policy Reporter Plus</em>（trend + compliance mapping + JIRA / Slack integration）。對大企業多 cluster + 合規驅動的場景值得評估、中小 deployment OSS Kyverno + 社群 Policy Reporter 夠用。</p>
<p><strong>PolicyException 治理</strong>：Kyverno 1.9+ 引入 PolicyException CRD、讓特定 resource 明確繞過特定 policy、避免「app team 為了 deploy 直接把 policy 改寬」。Exception 走 PR + 到期日 + owner、跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 的 exception lifecycle 對齊 — 例外不是黑箱、是 <em>暫時性、有 owner、有 review 日期</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Policy 改了沒生效</strong>：admission webhook 沒 ready、或 policy 寫在錯的 namespace（Policy CRD 是 namespace-scoped、放錯 namespace 不會作用）— <code>kubectl get clusterpolicies</code> 看 ready 狀態、<code>kubectl describe</code> 看 events</li>
<li><strong>Admission 卡住 / Pod 起不來</strong>：Kyverno webhook 掛掉、failurePolicy 設 <code>Fail</code> 結果整個 cluster 不能 deploy — production 對 critical workload 設 <code>failurePolicy: Ignore</code> + 監控 Kyverno controller availability、不要讓 policy engine 變成 cluster-wide SPOF</li>
<li><strong>Mutate 後 ArgoCD 永遠 OutOfSync</strong>：mutate 改的欄位沒在 ArgoCD <code>ignoreDifferences</code> 排除 — 對應加 <code>spec.ignoreDifferences[*].jsonPointers</code> 或 <code>.jqPathExpressions</code>、不然每次 sync 都跳 diff</li>
<li><strong>Verify Images 全部失敗</strong>：cluster 沒對外網路、Fulcio / Rekor 拉不到、或 image 真的沒簽 — 先 audit mode 跑 + 看 PolicyReport 統計 unsigned image 比例、確認預期路徑（內部 image 簽 / 外部 image allowlist）後才 enforce</li>
<li><strong>Background scan 跑爆 controller</strong>：cluster 太大、scan interval 太短 — 調整 <code>backgroundScan: false</code> for 高頻變動 policy、或拉長 scan interval、或 Nirmata 用分散式 scan</li>
<li><strong>PolicyException 變成漏洞</strong>：例外沒到期日、owner 離職、規則永久繞過 — Exception CRD 補 metadata（owner / expiry / ticket）+ 定期 audit 過期 Exception</li>
<li><strong>VAP migration 不一致</strong>：Kyverno 生成的 VAP 跟原 ClusterPolicy 行為有差（CEL 不支援部分 Kyverno feature）— 對 critical rule 保留 Kyverno 不 migrate、只把簡單 Validate 卸載</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨 K8s + API gateway + Terraform 統一 policy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA standalone</a></td>
      </tr>
      <tr>
          <td>K8s only 但團隊已投資 Rego</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a></td>
      </tr>
      <tr>
          <td>CI-time pre-merge 檢查 Terraform / Dockerfile</td>
          <td>Conftest（OPA 系列、CLI-based）</td>
      </tr>
      <tr>
          <td>Image 漏洞 / misconfig scan（scan, not gate）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>SBOM 生成 / 管理</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">SBOM Tools</a></td>
      </tr>
      <tr>
          <td>Image signing pipeline</td>
          <td>Sigstore cosign（CI 簽、Kyverno 驗）</td>
      </tr>
      <tr>
          <td>K8s 1.30+ 簡單 Validate-only 場景</td>
          <td>K8s native ValidatingAdmissionPolicy（CEL、kube-apiserver 內建）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Kyverno policy 完整 YAML reference、JMESPath 進階用法</li>
<li>Sigstore cosign CLI 操作、Fulcio / Rekor 部署</li>
<li>Nirmata Enterprise 詳細功能跟 pricing</li>
<li>K8s ValidatingAdmissionPolicy CEL 語法 reference</li>
<li>跟 service mesh（Istio / Linkerd）整合的 sidecar injection 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Kyverno 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>Kyverno Verify Images policy 強制 production cluster 只 deploy 已 cosign 簽章 + Rekor transparency log entry 的 image、未簽 / 來源異常 image 在 admission 階段擋掉</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Kyverno admission policy 配 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> scan 結果 — image 帶 vulnerability label 超過閾值就擋 deploy、補 CI scan 沒攔到的舊 image</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>Kyverno Verify Images + SBOM attestation 補位 — maintainer takeover 也能簽 image、但缺乏 SLSA build provenance attestation 會被 Kyverno admission 擋住</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性 (section)</a></td>
          <td>Kyverno 是 <em>K8s admission gate</em> 的 K8s-specific 落實工具、跟 CI-time SBOM 生成 + cosign 簽章 + Rekor transparency log 組成 supply chain trust chain 的 runtime enforcement 段</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>ClusterPolicy / Policy 走 propose → staging audit mode → tune → promote enforce mode 的工程 lifecycle、PolicyException 是 lifecycle 一部分、不是黑箱繞過</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>、<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（scan + label）、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>（vuln 資訊源）、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">SBOM Tools</a>（attestation 來源）</li>
<li>跨類：Sigstore cosign（CI 簽、Kyverno 驗）、ArgoCD / Flux（GitOps sync policy 本身）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（policy violation → IR routing）</li>
<li>官方：<a href="https://kyverno.io/docs/">Kyverno Documentation</a>、<a href="https://docs.sigstore.dev/">Sigstore Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Service Mesh Mirroring</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/</guid><description>&lt;p>Service mesh mirroring 的核心責任是在 proxy 層複製 production traffic 到 shadow service，讓新版本接受真實請求形狀，同時把使用者回應留在原本路徑。它適合已經落地 Istio、Linkerd 或類似 mesh 的平台，重點在用 routing policy 控制 mirror ratio、target、隔離與觀測。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay&lt;/a> 比、Service Mesh Mirroring 在 &lt;em>proxy / sidecar&lt;/em> 層、是 K8s mesh-native 的 L7 HTTP request mirror、不需要 application 或 host 端 capture binary；GoReplay 在 &lt;em>application host&lt;/em> 層、適合無 mesh 的環境或要 capture artifact 離線 replay。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring&lt;/a> 比、Service Mesh Mirroring 在 L7（HTTP route / header / subset 可控）、VPC Traffic Mirroring 在 L3-L4 packet 層、見度更底層但缺 application 語意。三者組合常見於 K8s + 多 cloud 混合環境。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Service Mesh Mirroring 部署是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mesh implementation 對齊&lt;/strong>：用哪套 mesh（Istio / Linkerd / Envoy gateway / Consul Connect）、control plane 版本、sidecar injection coverage、跨 namespace policy 邊界是否清楚&lt;/li>
&lt;li>&lt;strong>VirtualService mirror config&lt;/strong>：mirror destination 是否限制在同 namespace / 同 cluster、mirror_percent 是否從 1% 漸進、route / header filter 是否排除 write-heavy 或 PII path&lt;/li>
&lt;li>&lt;strong>Target service capacity&lt;/strong>：shadow target deployment 是否有獨立 HPA、跟 primary 同 node pool 還是隔離、DB / cache / external API 是否導 mock 或 sandbox、不會 share connection pool 造成 primary 飽和&lt;/li>
&lt;li>&lt;strong>Response handling&lt;/strong>：mirrored response 是 fire-and-forget（Istio 預設）還是有 logging、shadow 端是否能辨識 mirrored request（&lt;code>X-Envoy-Internal&lt;/code> / custom header）、side effect（payment / notification / webhook）是否走 dry-run&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> shadow traffic 治理的待補項目。&lt;/p></description><content:encoded><![CDATA[<p>Service mesh mirroring 的核心責任是在 proxy 層複製 production traffic 到 shadow service，讓新版本接受真實請求形狀，同時把使用者回應留在原本路徑。它適合已經落地 Istio、Linkerd 或類似 mesh 的平台，重點在用 routing policy 控制 mirror ratio、target、隔離與觀測。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a> 比、Service Mesh Mirroring 在 <em>proxy / sidecar</em> 層、是 K8s mesh-native 的 L7 HTTP request mirror、不需要 application 或 host 端 capture binary；GoReplay 在 <em>application host</em> 層、適合無 mesh 的環境或要 capture artifact 離線 replay。跟 <a href="/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring</a> 比、Service Mesh Mirroring 在 L7（HTTP route / header / subset 可控）、VPC Traffic Mirroring 在 L3-L4 packet 層、見度更底層但缺 application 語意。三者組合常見於 K8s + 多 cloud 混合環境。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Service Mesh Mirroring 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>Mesh implementation 對齊</strong>：用哪套 mesh（Istio / Linkerd / Envoy gateway / Consul Connect）、control plane 版本、sidecar injection coverage、跨 namespace policy 邊界是否清楚</li>
<li><strong>VirtualService mirror config</strong>：mirror destination 是否限制在同 namespace / 同 cluster、mirror_percent 是否從 1% 漸進、route / header filter 是否排除 write-heavy 或 PII path</li>
<li><strong>Target service capacity</strong>：shadow target deployment 是否有獨立 HPA、跟 primary 同 node pool 還是隔離、DB / cache / external API 是否導 mock 或 sandbox、不會 share connection pool 造成 primary 飽和</li>
<li><strong>Response handling</strong>：mirrored response 是 fire-and-forget（Istio 預設）還是有 logging、shadow 端是否能辨識 mirrored request（<code>X-Envoy-Internal</code> / custom header）、side effect（payment / notification / webhook）是否走 dry-run</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> shadow traffic 治理的待補項目。</p>
<h2 id="定位">定位</h2>
<p>Service mesh mirroring 適合平台已經有 proxy control plane 的團隊。當 service-to-service traffic 都經過 sidecar 或 gateway，mirror policy 可以把部分 production request 複製到新版本，不需要在 application code 中加 capture / replay 邏輯。</p>
<p>這個定位讓 service mesh mirroring 接到 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 的 shadow traffic 與 canary perf check。它比 host capture 更貼近 service routing，但也依賴 mesh 的觀測、policy、資源隔離與治理能力。</p>
<h2 id="適用場景">適用場景</h2>
<p>新版本 shadow validation 適合 service mesh mirroring。平台可以把 1%、5% 或特定 route 的流量 mirror 到 shadow deployment，觀察新版本 CPU、memory、latency、DB read 與 error。</p>
<p>Service-to-service migration 適合 service mesh mirroring。當下游服務準備換 runtime、framework、DB client 或 cache client，mirror 可以讓新路徑吃到 production upstream pattern。</p>
<p>多 region / 多 version 對照適合 service mesh mirroring。Mesh policy 能按 namespace、host、route、header 或 subset 控制 mirror target，讓平台在小 blast radius 下收集 production-shaped evidence。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Service mesh mirroring 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Proxy 層控制</td>
          <td>mirror policy 不侵入 application code</td>
          <td>mesh control plane 治理與變更審核</td>
      </tr>
      <tr>
          <td>Service routing</td>
          <td>可按 host、route、subset 控制 target</td>
          <td>route 命名、ownership、policy drift</td>
      </tr>
      <tr>
          <td>Mesh observability</td>
          <td>request metric、trace、service graph 可對照</td>
          <td>shadow target 的獨立 dashboard</td>
      </tr>
      <tr>
          <td>漸進比例</td>
          <td>mirror ratio 可逐步放大</td>
          <td>下游容量與 stop condition</td>
      </tr>
  </tbody>
</table>
<p>Proxy 層控制價值來自一致性。當所有 service 都走 mesh，mirror policy 可以用同一套控制面管理，避免每個 application 自行實作 replay。</p>
<p>Mesh observability 價值來自對照能力。Shadow service 的 latency、error、resource saturation 與 dependency call 可以直接跟 primary path 對比，但 dashboard 要清楚標記 mirrored traffic，避免混入正式 SLO。</p>
<h2 id="跟其他方式的取捨">跟其他方式的取捨</h2>
<p>Service mesh mirroring 和 GoReplay 的主要差異是控制面。Service mesh mirroring 依賴既有 proxy / mesh，適合服務間流量；GoReplay 適合 HTTP capture artifact、離線 replay 與沒有 mesh 的環境。</p>
<p>Service mesh mirroring 和 AWS VPC Traffic Mirroring 的主要差異是語意層級。Mesh 在 L7 routing 層，能按 route、host、header 與 subset 控制；VPC mirroring 在網路層，能見度更底層但應用語意控制較少。</p>
<p>Service mesh mirroring 和 canary 的主要差異是使用者影響。Mirrored request 的回應不回給使用者，適合 capacity / correctness observation；canary 會讓真實使用者走新版本，適合最終放量。</p>
<h2 id="操作成本">操作成本</h2>
<p>Service mesh mirroring 的主要成本是下游容量。Shadow traffic 雖然不回應使用者，但仍會消耗 shadow service、DB、cache、third-party mock、queue 與 observability pipeline 的資源。</p>
<p>Policy 成本來自控制面治理。Mirror rule、route、subset、namespace、owner 與 rollout window 都要可審查；錯誤的 mirror policy 可能把過大比例流量導到未準備好的 target。</p>
<p>Side effect 成本來自 application 行為。Shadow service 要能辨識 mirrored request，並把 write、external API call、notification、payment 與 queue publish 導到 sandbox、mock 或 dry-run。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Service mesh mirroring 結果應回寫到 evidence package。最小欄位包括 mesh policy version、source service、route、mirror ratio、target subset、time range、shadow target resource、data / side effect isolation、p95 / p99、error rate、dependency saturation、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Service mesh mirroring 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>mesh policy、route config、deployment version</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>mirror start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>service graph、metrics、trace、logs</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>mirror ratio、route coverage、header filter</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>target parity、dependency isolation</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未 mirror route、side effect mock、mesh overhead</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 mirror 實驗可關閉。Reviewer 要能看到 mirror policy 何時啟動、何時停止、覆蓋哪些 route、消耗哪些下游資源，以及 shadow target 是否接近 production。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Istio VirtualService mirror / mirror_percent</strong>：Istio 用 <code>VirtualService</code> 的 <code>mirror</code> 欄位指定 shadow destination、<code>mirrorPercentage</code>（v1.7+；舊版 <code>mirror_percent</code>）控制比例。production 操作慣例是從 1% 起步、每 30-60min 觀察 shadow target latency / error / saturation 再放大、達到 100% 後維持一週收 evidence 才 promote。route-level config 比 mesh-wide policy 安全、blast radius 限定在指定 host / path。</p>
<p><strong>Linkerd traffic split</strong>：Linkerd 用 SMI <code>TrafficSplit</code> CRD 或 native <code>HTTPRoute</code> 分流、走 <em>active-active</em> shadow 模式而非 fire-and-forget。Linkerd mirror 預設較輕量、proxy overhead 比 Istio 低、適合資源敏感的 K8s cluster；但 L7 policy 表達力不如 Istio EnvoyFilter。</p>
<p><strong>Envoy MirrorPolicy</strong>：直接寫 Envoy config（不透過 Istio control plane）時、<code>route.RouteAction.request_mirror_policies</code> 是底層 primitive。多 cluster 邊緣 gateway（Contour / Emissary-Ingress / Gloo）都是這層的 abstraction、適合不想引入 full Istio 但要 mirror 能力的場景。</p>
<p><strong>跟 Argo Rollouts canary 整合 — shadow deployment</strong>：Argo Rollouts 的 <code>analysis</code> step 可以接 mesh mirror — <em>shadow stage</em> 先用 mirror 收 evidence、<em>canary stage</em> 才放真實流量。對應 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 的「shadow 先於 canary」原則、避免把使用者當小白鼠。</p>
<p><strong>跟 <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> APM trace correlation</strong>：mirrored request 應該有獨立的 trace tag（<code>env:shadow</code> 或 <code>traffic.mirror:true</code>）、讓 Datadog APM / <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">observability stack</a> 能 filter 出 shadow path 的 p95 / error rate、不混入 primary SLO dashboard。trace propagation header 要保留、否則 distributed trace 斷在 mesh 邊界。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Mirror target capacity 不足 / shadow service OOM</strong>：shadow deployment 沒獨立 HPA、跟 primary 共用 node pool — 拆 node pool、shadow 設獨立 resource request、mirror_percent 從 1% 起步</li>
<li><strong>Mirrored response 漏處理（fire-and-forget 副作用）</strong>：Istio 預設丟棄 mirrored response、shadow 端的 error 沒被 collect — shadow service 自己 emit metric / log、不依賴 mirror response、加 <code>X-Shadow-Request</code> header 讓 shadow 端可辨識並走 dry-run 路徑</li>
<li><strong>PII / sensitive data 進 staging</strong>：mirrored request 帶真實 user token / payment info 打到 staging — header / body filter 走 EnvoyFilter 做 PII redaction、或在 mesh 邊界跑 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">data masking proxy</a> 再 mirror</li>
<li><strong>Side effect 真的發生（payment double charge / notification 真寄）</strong>：shadow service 沒辨識 mirrored request 就走正式邏輯 — 強制 shadow 端用 sandbox credential、external API client 走 mock / dry-run mode、write 改 read-only replica</li>
<li><strong>Mesh control plane 飽和 / mirror policy drift</strong>：mirror rule 散落各 namespace 沒 owner、policy version 不一致 — 走 GitOps（Argo CD / Flux）+ policy as code、定期 audit <code>kubectl get virtualservice -A</code></li>
<li><strong>Cross-cluster mirror blast radius 失控</strong>：mirror destination 指向其他 cluster 導致跨 cluster 流量爆增 — mirror destination 限 same-cluster、跨 cluster 要走獨立的 gateway 並設 quota</li>
<li><strong>Shadow trace 混進 SLO dashboard</strong>：APM 沒分 primary / shadow tag、p95 看起來變差但其實是 shadow 拖累 — trace tag <code>env:shadow</code> 強制、observability dashboard filter</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>無 mesh 環境 / 要 capture artifact 離線重播</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a></td>
      </tr>
      <tr>
          <td>L3-L4 packet 層分析（IDS / network forensic）</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring</a></td>
      </tr>
      <tr>
          <td>合成負載 / load test 而非 production mirror</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> / <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a></td>
      </tr>
      <tr>
          <td>Production-side 整體治理</td>
          <td><a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Istio / Linkerd / Envoy 完整 install / 升級 / control plane HA 細節</li>
<li>Service mesh 安全模型（mTLS / SPIFFE / authorization policy）— 屬 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security</a> 邊界</li>
<li>Mesh-level retry / timeout / circuit breaker 等 resilience pattern</li>
<li>Multi-cluster mesh federation（Istio multi-primary、Linkerd multicluster）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Service mesh mirroring 適合回寫平台遷移與新版本 shadow validation 案例。它可接 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">Miro managed EKS migration</a>、<a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">Tradeshift self-managed K8s to EKS</a>、<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 雙峰 workload</a> 的逐步驗證需求、<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 EKS cluster</a> 的 single-tenant per game 跨 cluster 流量 shadow，以及 <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ 微服務</a> 跨服務的 mirror 範圍治理。</p>
<p>這些案例的重點是 routing policy 與 blast radius。Service mesh mirroring 頁引用案例時，要把 case 轉成 route、mirror ratio、target subset、dependency isolation 與 abort condition — 例如 Riot Games 的 single-tenant 模式下、mirror policy 必須限制在 <em>同遊戲</em> cluster 內、不能跨 game 否則 blast radius 失控。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>上游：<a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.6 Traffic, Config and Control Plane Boundary</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring</a></li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
</ul>
]]></content:encoded></item><item><title>Vantage</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vantage/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vantage/</guid><description>&lt;p>Vantage 是 &lt;em>modern multi-cloud FinOps SaaS&lt;/em>、2020 年由 Heroku ex-founder 創立。它的核心責任是把雲端帳單轉成工程團隊能追蹤的 cost report、allocation、forecast 與 efficiency metric。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth&lt;/a>、Apptio Cloudability、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer&lt;/a> 同層、但賣點是 &lt;em>developer-friendly UI + 直覺定價 + 多雲 connector 一鍵啟用&lt;/em> — 適合工程團隊自助而非走 FinOps 部門申請的組織。&lt;/p>
&lt;p>它適合多 account、多 provider、Kubernetes 與 shared infrastructure 成本需要分攤到 service、team、namespace、label 或 resource 的組織。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Vantage 的差異在 &lt;em>使用者體驗與切入角度&lt;/em>、指標本身跟同類工具相近。CloudHealth / Apptio 是傳統 enterprise FinOps platform、面向 procurement、CFO、FinOps governance team；Vantage 把入口換成工程團隊 — 報表能直接 share URL、UI 接近 observability dashboard、connector 走 self-service onboarding 而非 SOW + professional service。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth&lt;/a> 比、Vantage &lt;em>淺但快上手&lt;/em>、適合 100 - 1000 人工程組織自助 FinOps；CloudHealth 走 enterprise governance、policy engine、approval workflow 更深、適合 5000+ 員工跨 BU 治理。跟 Apptio Cloudability 比、定位類似 CloudHealth、但 Apptio 把成本接到 TBM（Technology Business Management）frame、適合需要把 IT 成本對到 business service / product P&amp;amp;L 的組織。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer&lt;/a> 比、Cost Explorer 是 AWS-only 入口、免費但只有 AWS、跨 provider / Kubernetes / SaaS spend 看不到；Vantage 把 AWS + GCP + Azure + Snowflake + Databricks + Datadog + Fastly 等串成單一視圖。&lt;/p>
&lt;p>關鍵張力：&lt;em>modern SaaS 速度&lt;/em> ↔ &lt;em>enterprise governance 深度&lt;/em> 是 Vantage 的核心定位 trade-off。要 procurement-grade workflow、approval chain、custom data warehouse export 走 CloudHealth / Apptio；要工程 owner 直接打開 dashboard 看 cost trend、5 分鐘加新 connector 走 Vantage。&lt;/p></description><content:encoded><![CDATA[<p>Vantage 是 <em>modern multi-cloud FinOps SaaS</em>、2020 年由 Heroku ex-founder 創立。它的核心責任是把雲端帳單轉成工程團隊能追蹤的 cost report、allocation、forecast 與 efficiency metric。它跟 <a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a>、Apptio Cloudability、<a href="/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer</a> 同層、但賣點是 <em>developer-friendly UI + 直覺定價 + 多雲 connector 一鍵啟用</em> — 適合工程團隊自助而非走 FinOps 部門申請的組織。</p>
<p>它適合多 account、多 provider、Kubernetes 與 shared infrastructure 成本需要分攤到 service、team、namespace、label 或 resource 的組織。</p>
<h2 id="服務定位">服務定位</h2>
<p>Vantage 的差異在 <em>使用者體驗與切入角度</em>、指標本身跟同類工具相近。CloudHealth / Apptio 是傳統 enterprise FinOps platform、面向 procurement、CFO、FinOps governance team；Vantage 把入口換成工程團隊 — 報表能直接 share URL、UI 接近 observability dashboard、connector 走 self-service onboarding 而非 SOW + professional service。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a> 比、Vantage <em>淺但快上手</em>、適合 100 - 1000 人工程組織自助 FinOps；CloudHealth 走 enterprise governance、policy engine、approval workflow 更深、適合 5000+ 員工跨 BU 治理。跟 Apptio Cloudability 比、定位類似 CloudHealth、但 Apptio 把成本接到 TBM（Technology Business Management）frame、適合需要把 IT 成本對到 business service / product P&amp;L 的組織。跟 <a href="/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer</a> 比、Cost Explorer 是 AWS-only 入口、免費但只有 AWS、跨 provider / Kubernetes / SaaS spend 看不到；Vantage 把 AWS + GCP + Azure + Snowflake + Databricks + Datadog + Fastly 等串成單一視圖。</p>
<p>關鍵張力：<em>modern SaaS 速度</em> ↔ <em>enterprise governance 深度</em> 是 Vantage 的核心定位 trade-off。要 procurement-grade workflow、approval chain、custom data warehouse export 走 CloudHealth / Apptio；要工程 owner 直接打開 dashboard 看 cost trend、5 分鐘加新 connector 走 Vantage。</p>
<h2 id="定位">定位</h2>
<p>Vantage 適合把 cost attribution 帶進容量規劃流程。當團隊已經能用 workload model 描述流量，下一步要知道每個 workload、namespace、database、cache、region 與 account 對成本曲線的影響，Vantage 可以把雲端費用整理成可查詢、可分組、可預測的報表。</p>
<p>這個定位讓 Vantage 接到三個主章。它從 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 接收 cost per request 與 over-provision waste，從 <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> 接收 dashboard 與 ownership 訊號，從 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a> 接收 tag、label 與 attribution vocabulary。</p>
<h2 id="適用場景">適用場景</h2>
<p>Showback 與 chargeback 是 Vantage 的主要入口。當平台成本散在 shared Kubernetes cluster、managed database、network egress、storage 與 support plan 裡，Cost Reports 可以把費用依 team、service、environment 或 business unit 切開，讓討論從總帳單轉成 owner action。</p>
<p>Kubernetes 成本分析適合用 Vantage 補足平台可見性。Namespace、label、service、pod、CPU、RAM、storage 與 GPU 維度能讓團隊看到 idle cost、resource efficiency 與 rightsizing recommendation，特別適合多租戶平台。</p>
<p>Forecast 與 anomaly review 適合日常成本治理。每月 forecast、cost trend、unexpected spike 與 budget drift 可以接到 engineering review，讓容量調整、release、marketing event 與成本變化在同一個時間軸上被討論。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Vantage 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cost allocation</td>
          <td>依 provider、account、resource、Kubernetes label 分攤</td>
          <td>tag / label policy、owner taxonomy</td>
      </tr>
      <tr>
          <td>Kubernetes 成本</td>
          <td>namespace、service、label 與 pod-level efficiency</td>
          <td>agent rollout、cluster mapping</td>
      </tr>
      <tr>
          <td>Forecast</td>
          <td>成本趨勢與月末預測可接 review 節奏</td>
          <td>事件註記、release marker、業務日曆</td>
      </tr>
      <tr>
          <td>工程入口</td>
          <td>報表可讓 service owner 直接查詢與追蹤</td>
          <td>action workflow、remediation ownership</td>
      </tr>
  </tbody>
</table>
<p>Cost allocation 價值來自 owner 明確。總帳單只能告訴組織花了多少錢；service-level report 才能讓工程團隊知道哪個 workload、region、database 或 network path 改變了成本。</p>
<p>Kubernetes 成本價值來自 shared cluster 拆分。多租戶平台常把多個服務塞進同一組 node pool；Vantage 類工具把 pod lifecycle 與底層基礎設施成本接起來，讓 namespace 或 label 變成成本討論單位。</p>
<p>Forecast 價值來自提前介入。成本 review 如果只看月底結果，容量浪費和異常用量已經發生；forecast 和 anomaly 讓團隊在月中就能調整 resource request、replica、reserved capacity 或 release plan。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Vantage deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Multi-cloud connector coverage</strong>：AWS / GCP / Azure / Snowflake / Datadog / Fastly 等 connector 是否都接上 — 缺一個就有成本盲區、缺了 Snowflake 反而比缺了 AWS 痛（query cost 沒人看）</li>
<li><strong>Cost Report 設計</strong>：是否依 service / team / environment / business unit 切出可 share 的 saved report、URL 是否進 wiki / Slack canonical 位置、誰每週看</li>
<li><strong>Anomaly Detection 設定</strong>：threshold 跟 baseline 是否 tune 過、false positive rate、anomaly 出現後是否有 owner 接、不是只進 email spam</li>
<li><strong>Report sharing 機制</strong>：cost report 是否走 read-only URL share 給工程 owner、不是把每個工程師都拉進 Vantage account；team 是否有 cost retrospective 節奏</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 邊界的待補項目。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Vantage 和 Akamas 的主要差異是決策深度。Vantage 讓團隊看清成本、分攤責任與找出浪費；Akamas 更進一步把 workload constraint 與 configuration tuning 接成 optimization loop。</p>
<p>Vantage 和 CloudHealth 的主要差異是組織重心。Vantage 偏工程團隊可直接使用的 cost reports、Kubernetes 成本與 resource-level 分析；CloudHealth 偏 enterprise FinOps governance、policy 與大組織流程。</p>
<p>Vantage 和 AWS Cost Explorer 的主要差異是範圍。AWS Cost Explorer 是 AWS-native 入口；Vantage 適合跨 provider、Kubernetes 與多 workspace 的成本視圖。</p>
<h3 id="核心取捨表">核心取捨表</h3>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Vantage</th>
          <th>CloudHealth</th>
          <th>Apptio Cloudability</th>
          <th>AWS Cost Explorer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者重心</td>
          <td>工程 owner 自助</td>
          <td>FinOps / procurement team</td>
          <td>FinOps + business / product owner</td>
          <td>AWS account holder</td>
      </tr>
      <tr>
          <td>多雲覆蓋</td>
          <td>AWS + GCP + Azure + 主要 SaaS connector</td>
          <td>AWS + GCP + Azure 完整 + policy engine</td>
          <td>AWS + GCP + Azure + on-prem (TBM frame)</td>
          <td>AWS only</td>
      </tr>
      <tr>
          <td>Onboarding 速度</td>
          <td>快 — connector self-service、分鐘級</td>
          <td>慢 — SOW + professional service</td>
          <td>慢 — TBM mapping + implementation</td>
          <td>即用（AWS-native）</td>
      </tr>
      <tr>
          <td>報表分享</td>
          <td>強 — URL share、read-only viewer 免費</td>
          <td>中 — 走 RBAC、外部分享受限</td>
          <td>中 — 走 TBM portal</td>
          <td>弱 — 限 AWS console viewer</td>
      </tr>
      <tr>
          <td>Kubernetes cost</td>
          <td>強 — namespace / label / pod-level 內建</td>
          <td>中 — 整合需配置</td>
          <td>中</td>
          <td>弱</td>
      </tr>
      <tr>
          <td>Anomaly detection</td>
          <td>內建、threshold 可調</td>
          <td>內建 + policy 觸發</td>
          <td>內建</td>
          <td>基本（AWS Cost Anomaly Detection）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>100-1000 人工程組織、cloud-native</td>
          <td>5000+ 員工跨 BU enterprise governance</td>
          <td>把 IT cost 對到 product P&amp;L 的組織</td>
          <td>純 AWS、預算敏感、初期治理</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低-中 — report 為主、無深度 lock-in</td>
          <td>高 — policy / approval workflow 量多</td>
          <td>高 — TBM mapping 跟 business 整合</td>
          <td>零 — 本就免費內建</td>
      </tr>
  </tbody>
</table>
<p>選 Vantage 的核心訴求：<em>工程團隊自助 FinOps + 跨雲跨 SaaS 一張視圖 + UI / 報表 share 走 modern observability 體驗</em>、且不需要 enterprise approval workflow / TBM business mapping。需要重 governance 走 CloudHealth、需要 IT-to-business cost mapping 走 Apptio、純 AWS 預算敏感先用 Cost Explorer。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Cost Report builder</strong>：Vantage 的核心 primitive、走 <em>filter + group by + time range</em> 的 declarative model — 例如 <code>provider:aws AND service:ec2 AND tag:team=payments group by region</code>。Saved report 變團隊 canonical view、URL 可貼 wiki / Slack；scheduled report 走 email / Slack notification。實務上 <em>每個 service owner 都該有一張 saved report</em>、不是 FinOps team 中央集中看。</p>
<p><strong>Anomaly Detection</strong>：依 cost trend 統計 baseline、超過 threshold 觸發 anomaly。痛點是 <em>false positive</em>：deploy 新 service、月底 invoice timing、provider 計費延遲都會觸發。Tune 方向是 <em>排除 known event</em>（new connector 接入後 7 天 grace period）+ <em>調 sensitivity per service</em>（payment 可容忍 5% drift、ML training cluster 容忍 50%）。對應 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的 anomaly governance frame。</p>
<p><strong>Resource ROI / efficiency metric</strong>：Vantage 把 cost 跟 utilization metric 對齊、算 <em>cost per unit</em>（cost / request、cost / GB stored、cost / GPU-hour）。意義是把 cost report 從 <em>absolute spend</em> 升級到 <em>efficiency frontier</em>、能識別 overprovision 跟 underutilization。需要 metric source 接上（Datadog / Prometheus / CloudWatch）、純帳單 data 算不出 ROI。</p>
<p><strong>Datadog / Slack integration</strong>：cost anomaly + scheduled report 推到 Slack channel、跟 incident channel 共用；Datadog 接成 metric source 後可在 Datadog dashboard 看 cost trend 跟 latency / error rate side-by-side、適合做 <em>cost-aware SLO review</em>。</p>
<p><strong>Vantage Network（vendor benchmark）</strong>：匿名化彙整 Vantage 客戶的 unit cost benchmark（每 GB S3 storage、每 RDS instance hour、每 Snowflake credit）、讓客戶看自己跟同產業比是貴是便宜。價值在 <em>negotiation leverage</em> — 跟 AWS / Snowflake 談 EDP / 多年合約時、benchmark 是議價素材。注意是匿名 aggregate、不是 vendor 個別揭露。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Multi-cloud tag drift</strong>：AWS 用 <code>team</code>、GCP 用 <code>Team</code>、Azure 用 <code>Team-Name</code>、Vantage report group by 後出現大量 <code>untagged</code> — 在 Vantage <em>Virtual Tag</em>（rule-based tag normalization）統一 mapping、或源頭走 tag policy enforcement（<a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html">AWS Organizations tag policy</a>、GCP organization policy）</li>
<li><strong>Anomaly false positive 過多 / SOC-like alert fatigue</strong>：threshold 設太緊、month-end billing delay 沒排除 — 拉大 baseline window、加 grace period for new resource、per-service tune sensitivity</li>
<li><strong>Cost spike root cause 不明</strong>：總帳單漲了但 group by service / region / tag 都看不出來 — 切到 <em>Resource Report</em>（最細粒度、看 instance / volume / snapshot 個別 cost）找 outlier、或開 Vantage <em>Cost Diffs</em>（兩個 time window 對比 delta breakdown）</li>
<li><strong>Kubernetes cost agent 資料缺</strong>：agent 沒裝 / cluster role 權限不足 / metric server 沒啟用、namespace breakdown 全空 — 走 Vantage Kubernetes onboarding checklist 補 agent + RBAC + metric server、確認資料 24hr 內出現</li>
<li><strong>Connector 接上但資料沒進來</strong>：跨 account assume role 失敗、CUR（Cost and Usage Report）export 沒開、Snowflake account usage 權限缺 — 在 Vantage connector page 看 sync status 跟 error log、不是盲猜</li>
<li><strong>Report share URL 被外人猜到</strong>：read-only URL 預設 <em>unauthenticated</em>、share 給 contractor 後沒 revoke — 改用 <em>Authentication-required share</em> 或定期 rotate URL、敏感成本數字（payment processor cost / customer-specific dedicated infra）走 internal-only</li>
<li><strong>Forecast 不準 / 跟實際差太多</strong>：base period 太短 / 有 one-off event（migration backfill、disaster recovery test）、forecast model 抓不到 seasonality — 拉長 base period、標記 one-off event 排除、或改走 manual override forecast 給特定 service</li>
</ul>
<h2 id="操作成本">操作成本</h2>
<p>Vantage 的主要成本是 cost taxonomy 維護。Tag、label、account、workspace、cluster、namespace 與 service owner 要有穩定規則，Cost Reports 才能被工程團隊信任。</p>
<p>Kubernetes agent 導入需要平台協作。Cluster 權限、資料上傳、node / pod mapping、provider cost delay 與 double counting 防護，都需要平台團隊與 FinOps 團隊一起定義。</p>
<p>Remediation 成本在報表之後才開始。找到 idle cost、overprovisioned workload 或 unexpected egress 只是第一步，後續要有 ticket、owner、驗證、rollback 與 saving confirmation。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Vantage 結果應回寫到 cost attribution evidence package。最小欄位包括 report name、filter、grouping、time range、provider、owner dimension、baseline cost、forecast、anomaly、efficiency metric、action item 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Vantage 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>Cost Report、Kubernetes Efficiency Report、Resource Report</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>report window、billing period、forecast period</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>Vantage report URL、cloud billing query、dashboard</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>tag coverage、agent freshness、provider data delay</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>owner mapping、double counting check、trend repeatability</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未標記 resource、shared cost allocation rule、資料延遲</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是把成本問題交給正確 owner。Vantage report 要能回答「誰的 workload 產生成本、成本從何時開始改變、哪個維度最能解釋變化」。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>Vantage 目前適合作為 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 與 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 cost attribution</a> 的工具承接點。它可回寫到 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 EKS cluster</a> 的多 cluster 成本歸屬與年省 1000 萬美金驗證、<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 consolidation</a> 的 28% 成本下降跨 DB 整併、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow modern data architecture</a> 的儲存 90% / 分析 80% 成本下降，以及 <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> 的 on-demand cost model 50% 降幅。</p>
<p>這些案例的重點是成本歸屬。Vantage 頁引用案例時，要把 report filter、owner dimension、成本變化、action item 與驗證結果寫清楚 — 例如 Netflix 的 28% 下降需要拆到 DB tier、replication topology 與 read replica 比例，避免停在帳單 dashboard 截圖。</p>
<p>Vantage 的客戶輪廓偏 <em>modern startup 與 mid-market</em> — 工程組織 100-1000 人、cloud-native first、沒有獨立 FinOps team、由 platform / SRE 兼任成本治理。這類組織的痛點是 <em>誰看 cost report、誰調 anomaly、誰負責 saving validation</em> 的工程節奏沒建立、governance policy 本身反而不缺。引用 Riot Games / Netflix / BookMyShow / Zomato 案例時、重點是把這些 enterprise-scale 的 attribution 機制轉譯成 mid-market 可執行的 weekly review 節奏、而非照搬全部 governance overhead。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a>、<a href="/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer</a></li>
<li>官方：<a href="https://docs.vantage.sh/cost_reports">Vantage Cost Reports</a></li>
</ul>
]]></content:encoded></item><item><title>9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/</guid><description>&lt;p>這個案例的核心責任是補強 Azure 案例庫深度。Cosmos DB 過往只有 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a> 一篇、ASOS 提供 &lt;em>傳統零售場景 + 全球分散 + 季節性峰值&lt;/em> 的對照、跟 Minecraft Earth 的 &lt;em>AR 遊戲 + 玩家位置&lt;/em> 完全不同業務語意。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>ASOS 在 Azure 的關鍵數字（引自 &lt;a href="https://www.microsoft.com/en/customers/story/718983-asos-retail-and-consumer-goods-azure">ASOS Microsoft Customer Story&lt;/a>）：&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>客戶數&lt;/td>
 &lt;td>1540 萬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Black Friday 24 小時請求量&lt;/td>
 &lt;td>1.67 億&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Black Friday 請求峰值&lt;/td>
 &lt;td>3,500 req/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Black Friday 訂單峰值&lt;/td>
 &lt;td>33 orders/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>平均響應時間&lt;/td>
 &lt;td>48 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品 SKU&lt;/td>
 &lt;td>85,000、每週新增 5,000 件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>架構轉變&lt;/td>
 &lt;td>2016 年遷移到 microservices&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>Azure Cosmos DB + microservices&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵業務驅動：「ASOS chose Azure Cosmos DB because of its global distribution and ability to handle heavy seasonal bursts like Black Friday」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>ASOS 案例揭露三個全球零售 KV 容量規劃重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Black Friday 24h 1.67 億 = 平均 1,930 req/sec、峰值 3,500 req/sec&lt;/strong>：峰值 / 平均 = 1.81 倍。這個比例顯示 Black Friday 「持續高峰」、不是「瞬間爆量」 — 24 小時內流量曲線相對平緩、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a> 的「5 分鐘賣完」是完全不同形狀。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling&lt;/a> 的負載形狀識別。&lt;/li>
&lt;li>&lt;strong>48ms 平均響應 = 全球分散下 Cosmos DB 的代表性數字&lt;/strong>：英國時尚電商、客戶遍及全球、Cosmos DB 在每個地區複製、讀取在最近 region 完成。這個 48ms 包含網路、DB、應用層 — DB 本身可能只佔 5-10ms、其他是網路與應用層。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 分解。&lt;/li>
&lt;li>&lt;strong>85K SKU + 每週新增 5K = 高更新頻率 catalog&lt;/strong>：商品資料不只是讀、還有頻繁更新（價格、庫存、推薦排序）。這層 write throughput 對 Cosmos DB partition key 設計（通常用 category_id 或 brand_id）至關重要。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 hot partition 識別。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：這是 2016 年的數字、過去 10 年 ASOS 應該成長很多。但 1.67 億 req/24h 跟 33 orders/sec 對許多新興電商仍是天花板級數字、可作為「中大型零售」對標。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 Azure 案例庫深度。Cosmos DB 過往只有 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> 一篇、ASOS 提供 <em>傳統零售場景 + 全球分散 + 季節性峰值</em> 的對照、跟 Minecraft Earth 的 <em>AR 遊戲 + 玩家位置</em> 完全不同業務語意。</p>
<h2 id="觀察">觀察</h2>
<p>ASOS 在 Azure 的關鍵數字（引自 <a href="https://www.microsoft.com/en/customers/story/718983-asos-retail-and-consumer-goods-azure">ASOS Microsoft Customer Story</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶數</td>
          <td>1540 萬</td>
      </tr>
      <tr>
          <td>Black Friday 24 小時請求量</td>
          <td>1.67 億</td>
      </tr>
      <tr>
          <td>Black Friday 請求峰值</td>
          <td>3,500 req/sec</td>
      </tr>
      <tr>
          <td>Black Friday 訂單峰值</td>
          <td>33 orders/sec</td>
      </tr>
      <tr>
          <td>平均響應時間</td>
          <td>48 ms</td>
      </tr>
      <tr>
          <td>商品 SKU</td>
          <td>85,000、每週新增 5,000 件</td>
      </tr>
      <tr>
          <td>架構轉變</td>
          <td>2016 年遷移到 microservices</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>Azure Cosmos DB + microservices</td>
      </tr>
  </tbody>
</table>
<p>關鍵業務驅動：「ASOS chose Azure Cosmos DB because of its global distribution and ability to handle heavy seasonal bursts like Black Friday」。</p>
<h2 id="判讀">判讀</h2>
<p>ASOS 案例揭露三個全球零售 KV 容量規劃重點。</p>
<ol>
<li><strong>Black Friday 24h 1.67 億 = 平均 1,930 req/sec、峰值 3,500 req/sec</strong>：峰值 / 平均 = 1.81 倍。這個比例顯示 Black Friday 「持續高峰」、不是「瞬間爆量」 — 24 小時內流量曲線相對平緩、跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的「5 分鐘賣完」是完全不同形狀。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a> 的負載形狀識別。</li>
<li><strong>48ms 平均響應 = 全球分散下 Cosmos DB 的代表性數字</strong>：英國時尚電商、客戶遍及全球、Cosmos DB 在每個地區複製、讀取在最近 region 完成。這個 48ms 包含網路、DB、應用層 — DB 本身可能只佔 5-10ms、其他是網路與應用層。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency budget 分解。</li>
<li><strong>85K SKU + 每週新增 5K = 高更新頻率 catalog</strong>：商品資料不只是讀、還有頻繁更新（價格、庫存、推薦排序）。這層 write throughput 對 Cosmos DB partition key 設計（通常用 category_id 或 brand_id）至關重要。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 hot partition 識別。</li>
</ol>
<p>需要警惕：這是 2016 年的數字、過去 10 年 ASOS 應該成長很多。但 1.67 億 req/24h 跟 33 orders/sec 對許多新興電商仍是天花板級數字、可作為「中大型零售」對標。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>Black Friday 類「持續高峰」適合 provisioned + scheduled scaling</strong>：跟 flash-sale 的「on-demand 吃彈性」不同、Black Friday 整天高、用 provisioned 比較划算。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 的可預期峰值準備。</li>
<li><strong>全球零售用 Cosmos DB / DynamoDB Global Tables</strong>：客戶在哪、讀取就在哪、避免跨洲 latency。對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> 的全球分散取捨。</li>
<li><strong>微服務 + Cosmos DB 是電商現代化典型路徑</strong>：從單體 → 微服務、從關聯式 DB → multi-model NoSQL、是 2016 後零售業常見遷移。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 與 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a>。</li>
</ol>
<p>跨平台等效：AWS DynamoDB Global Tables + Lambda、GCP Firestore + Cloud Run 都可以實作對等架構。差異是 Cosmos DB 的 multi-model（同一服務支援 SQL、Mongo、Cassandra、Gremlin、Table API）、AWS 對應有 DynamoDB（KV/Document）+ Neptune（Graph）+ Keyspaces（Cassandra）等多個服務。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他可預期峰值 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a> / <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a></li>
<li>對照 flash-sale-spike → <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></li>
<li>想對照其他 Cosmos DB 使用 → <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></li>
<li>想規劃全球電商 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想拆 Black Friday 容量背後的 RU 成本與 sizing → <a href="/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/" data-link-title="Cosmos DB RU/s 成本模型 &#43; 容量規劃：RU 思維、payload、index、provisioned vs autoscale vs serverless" data-link-desc="從 CPU&#43;IOPS 思維轉到 RU 思維的學習曲線、依負載形狀選容量模式、payload &#43; index policy 對 RU 的影響、autoscale reactive 限制 — 從 ASOS Black Friday &#43; Minecraft Earth 1M RU/s 壓測切入">Cosmos DB RU 成本模型與 sizing</a></li>
<li>想做電商 partition key 設計 → <a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">Cosmos DB partition key 設計</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.microsoft.com/en/customers/story/718983-asos-retail-and-consumer-goods-azure">ASOS – Online retailer uses cloud database to deliver world-class shopping experiences</a></li>
<li><a href="https://azure.microsoft.com/en-us/products/cosmos-db/">Azure Cosmos DB</a></li>
</ul>
]]></content:encoded></item><item><title>4.21 Rule-level CPU Signal Governance</title><link>https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/</guid><description>&lt;p>Rule-level CPU signal governance 的核心責任是讓規則與策略執行成本可被提前判讀，避免高成本規則在全域 rollout 後才以 5xx 與 latency 形式被動暴露。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Rule-level CPU signal governance 是把「哪一條規則在吃 CPU」變成可量測、可回退、可治理的觀測能力，責任是補上服務級 CPU 指標看不到的規則層風險。&lt;/p>
&lt;p>服務級 CPU 只告訴團隊「系統變慢了」，rule-level 訊號才告訴團隊「是哪個規則讓系統變慢」。兩者一起存在，事故才能從症狀快速收斂到可操作原因。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀順序是先看服務級異常，再下鑽到規則層成本分佈。若 CPU、latency、5xx 同步惡化，且 rule hit 分佈在短時間發生偏移，通常代表規則層出現新的成本熱點。&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>Rule hit rate 突增&lt;/td>
 &lt;td>某規則命中流量異常放大&lt;/td>
 &lt;td>先核對最近規則推送與 traffic pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule-level CPU p95 / p99 上升&lt;/td>
 &lt;td>規則執行成本惡化&lt;/td>
 &lt;td>先降級或回退高成本規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CPU hotspot 只集中在少數規則&lt;/td>
 &lt;td>問題可收斂到有限規則集合&lt;/td>
 &lt;td>優先處理 top-N 規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後 rule-level 成本快速回穩&lt;/td>
 &lt;td>異常與新規則高度關聯&lt;/td>
 &lt;td>凍結同批 rollout，進入 replay 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule trace 缺失&lt;/td>
 &lt;td>無法確認成本來自哪個分支與 payload&lt;/td>
 &lt;td>先補埋點再擴大 rollout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="訊號模型">訊號模型&lt;/h2>
&lt;p>Rule-level CPU 訊號模型的重點是同時保留成本、命中與上下文。只有成本沒有命中，無法判斷影響面；只有命中沒有成本，無法判斷風險等級。&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>rule_id / rule_version&lt;/td>
 &lt;td>對應具體規則版本&lt;/td>
 &lt;td>規則改版未更新版本標記&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>match_count&lt;/td>
 &lt;td>量測命中流量&lt;/td>
 &lt;td>未按 tenant / region 分層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>exec_cpu_ms&lt;/td>
 &lt;td>量測規則執行成本&lt;/td>
 &lt;td>只看平均值，忽略長尾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>input_class&lt;/td>
 &lt;td>區分 payload 類型與風險來源&lt;/td>
 &lt;td>缺少分類導致 replay 不可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollout_stage&lt;/td>
 &lt;td>對齊分批 rollout 狀態&lt;/td>
 &lt;td>觀測資料無法對應 rollout 階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback_action&lt;/td>
 &lt;td>記錄降級、旁路或阻擋策略是否觸發&lt;/td>
 &lt;td>事故後難以回放決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="控制面">控制面&lt;/h2>
&lt;p>Rule-level CPU signal governance 的控制面是把「測到異常後要怎麼停」直接接到 rollout 流程，而不是只做監控展示。&lt;/p>
&lt;ol>
&lt;li>對高風險規則建立 rule-level CPU baseline 與異常門檻。&lt;/li>
&lt;li>把 rule-level 訊號接到 staged rollout gate。&lt;/li>
&lt;li>對 top-N 高成本規則建立自動降級或回退條件。&lt;/li>
&lt;li>在 evidence package 記錄當次 rollout 的 rule-level 成本分佈與限制。&lt;/li>
&lt;li>在 post-incident review 回寫新 payload 類型與新風險樣式。&lt;/li>
&lt;/ol>
&lt;h2 id="常見反模式">常見反模式&lt;/h2>
&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>只看服務級 CPU&lt;/td>
 &lt;td>知道有問題但找不到高成本規則&lt;/td>
 &lt;td>補 rule_id / version / cost 埋點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則測試只跑功能正確&lt;/td>
 &lt;td>事故時才看見計算成本爆點&lt;/td>
 &lt;td>增加 representative payload replay&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollout 與觀測脫鉤&lt;/td>
 &lt;td>分批推送但缺乏階段判讀依據&lt;/td>
 &lt;td>把 rollout_stage 變成必填訊號欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退無證據包&lt;/td>
 &lt;td>復盤只剩結論，缺成本時間線&lt;/td>
 &lt;td>接 4.20 evidence package&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例回扣">案例回扣&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Cloudflare 2019 事故顯示高成本 regex 可以在全網同步推送下快速放大。Rule-level CPU 訊號治理的價值是把這類風險前移到 rollout 過程，而不是等到全球 5xx 才回頭排查。&lt;/p></description><content:encoded><![CDATA[<p>Rule-level CPU signal governance 的核心責任是讓規則與策略執行成本可被提前判讀，避免高成本規則在全域 rollout 後才以 5xx 與 latency 形式被動暴露。</p>
<h2 id="概念定位">概念定位</h2>
<p>Rule-level CPU signal governance 是把「哪一條規則在吃 CPU」變成可量測、可回退、可治理的觀測能力，責任是補上服務級 CPU 指標看不到的規則層風險。</p>
<p>服務級 CPU 只告訴團隊「系統變慢了」，rule-level 訊號才告訴團隊「是哪個規則讓系統變慢」。兩者一起存在，事故才能從症狀快速收斂到可操作原因。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀順序是先看服務級異常，再下鑽到規則層成本分佈。若 CPU、latency、5xx 同步惡化，且 rule hit 分佈在短時間發生偏移，通常代表規則層出現新的成本熱點。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>代表意義</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rule hit rate 突增</td>
          <td>某規則命中流量異常放大</td>
          <td>先核對最近規則推送與 traffic pattern</td>
      </tr>
      <tr>
          <td>Rule-level CPU p95 / p99 上升</td>
          <td>規則執行成本惡化</td>
          <td>先降級或回退高成本規則</td>
      </tr>
      <tr>
          <td>CPU hotspot 只集中在少數規則</td>
          <td>問題可收斂到有限規則集合</td>
          <td>優先處理 top-N 規則</td>
      </tr>
      <tr>
          <td>回退後 rule-level 成本快速回穩</td>
          <td>異常與新規則高度關聯</td>
          <td>凍結同批 rollout，進入 replay 驗證</td>
      </tr>
      <tr>
          <td>Rule trace 缺失</td>
          <td>無法確認成本來自哪個分支與 payload</td>
          <td>先補埋點再擴大 rollout</td>
      </tr>
  </tbody>
</table>
<h2 id="訊號模型">訊號模型</h2>
<p>Rule-level CPU 訊號模型的重點是同時保留成本、命中與上下文。只有成本沒有命中，無法判斷影響面；只有命中沒有成本，無法判斷風險等級。</p>
<table>
  <thead>
      <tr>
          <th>訊號欄位</th>
          <th>用途</th>
          <th>常見陷阱</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rule_id / rule_version</td>
          <td>對應具體規則版本</td>
          <td>規則改版未更新版本標記</td>
      </tr>
      <tr>
          <td>match_count</td>
          <td>量測命中流量</td>
          <td>未按 tenant / region 分層</td>
      </tr>
      <tr>
          <td>exec_cpu_ms</td>
          <td>量測規則執行成本</td>
          <td>只看平均值，忽略長尾</td>
      </tr>
      <tr>
          <td>input_class</td>
          <td>區分 payload 類型與風險來源</td>
          <td>缺少分類導致 replay 不可重現</td>
      </tr>
      <tr>
          <td>rollout_stage</td>
          <td>對齊分批 rollout 狀態</td>
          <td>觀測資料無法對應 rollout 階段</td>
      </tr>
      <tr>
          <td>fallback_action</td>
          <td>記錄降級、旁路或阻擋策略是否觸發</td>
          <td>事故後難以回放決策</td>
      </tr>
  </tbody>
</table>
<h2 id="控制面">控制面</h2>
<p>Rule-level CPU signal governance 的控制面是把「測到異常後要怎麼停」直接接到 rollout 流程，而不是只做監控展示。</p>
<ol>
<li>對高風險規則建立 rule-level CPU baseline 與異常門檻。</li>
<li>把 rule-level 訊號接到 staged rollout gate。</li>
<li>對 top-N 高成本規則建立自動降級或回退條件。</li>
<li>在 evidence package 記錄當次 rollout 的 rule-level 成本分佈與限制。</li>
<li>在 post-incident review 回寫新 payload 類型與新風險樣式。</li>
</ol>
<h2 id="常見反模式">常見反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只看服務級 CPU</td>
          <td>知道有問題但找不到高成本規則</td>
          <td>補 rule_id / version / cost 埋點</td>
      </tr>
      <tr>
          <td>規則測試只跑功能正確</td>
          <td>事故時才看見計算成本爆點</td>
          <td>增加 representative payload replay</td>
      </tr>
      <tr>
          <td>rollout 與觀測脫鉤</td>
          <td>分批推送但缺乏階段判讀依據</td>
          <td>把 rollout_stage 變成必填訊號欄位</td>
      </tr>
      <tr>
          <td>回退無證據包</td>
          <td>復盤只剩結論，缺成本時間線</td>
          <td>接 4.20 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回扣">案例回扣</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage</a></li>
<li><a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門</a></li>
</ul>
<p>Cloudflare 2019 事故顯示高成本 regex 可以在全網同步推送下快速放大。Rule-level CPU 訊號治理的價值是把這類風險前移到 rollout 過程，而不是等到全球 5xx 才回頭排查。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.17： <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a></li>
<li>04.20： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
<li>06.24： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">Rule Rollout Safety Gate</a></li>
<li>08.19： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
</ul>
]]></content:encoded></item><item><title>Datadog：2023 多區觀測中斷事件</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/</guid><description>&lt;p>這起案例的核心責任是處理「監控系統本身失效」的盲區。當觀測平台中斷，事故判讀需要立即切換備援證據來源。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>telemetry gap&lt;/td>
 &lt;td>缺失是否影響決策&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp;amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>customer-side false normal&lt;/td>
 &lt;td>客戶是否誤以為服務正常&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback evidence readiness&lt;/td>
 &lt;td>備援證據能否即時接手&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「觀測資料缺失時的事故判讀」。主要風險是把缺失資料誤判為服務恢復，導致決策建立在錯誤安全感上。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>事故流程要預留「觀測失明」分支，並在復盤回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>。同時補 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a> 的備援證據來源。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是處理「監控系統本身失效」的盲區。當觀測平台中斷，事故判讀需要立即切換備援證據來源。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>telemetry gap</td>
          <td>缺失是否影響決策</td>
          <td><a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18</a></td>
      </tr>
      <tr>
          <td>customer-side false normal</td>
          <td>客戶是否誤以為服務正常</td>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10</a></td>
      </tr>
      <tr>
          <td>fallback evidence readiness</td>
          <td>備援證據能否即時接手</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「觀測資料缺失時的事故判讀」。主要風險是把缺失資料誤判為服務恢復，導致決策建立在錯誤安全感上。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>事故流程要預留「觀測失明」分支，並在復盤回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a>。同時補 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 的備援證據來源。</p>
]]></content:encoded></item><item><title>Honeycomb：以 Burn Rate 驅動的可靠性操作</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/</guid><description>&lt;p>Honeycomb 案例的核心責任是把可觀測訊號直接轉成可靠性決策。當團隊面對大量告警時，burn rate 提供比固定閾值更接近使用者體感的判讀方式。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>固定閾值告警在高變化流量下容易失真。團隊可能長時間處於告警疲勞，卻看不出真正侵蝕 SLO 的事件。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Burn rate 警示&lt;/td>
 &lt;td>可靠性消耗速度是否異常&lt;/td>
 &lt;td>優先序判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLO 驅動值班&lt;/td>
 &lt;td>哪些事件需要立即接手&lt;/td>
 &lt;td>響應節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tracing-first 分析&lt;/td>
 &lt;td>事件路徑如何定位&lt;/td>
 &lt;td>可追溯證據&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>fast burn&lt;/td>
 &lt;td>短期消耗是否超過容忍帶&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>slow burn&lt;/td>
 &lt;td>長期趨勢是否持續惡化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>trace outlier path&lt;/td>
 &lt;td>關鍵路徑是否集中退化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先用 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a> 組證據，再在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a> 回寫驗證條件。&lt;/p></description><content:encoded><![CDATA[<p>Honeycomb 案例的核心責任是把可觀測訊號直接轉成可靠性決策。當團隊面對大量告警時，burn rate 提供比固定閾值更接近使用者體感的判讀方式。</p>
<h2 id="問題場景">問題場景</h2>
<p>固定閾值告警在高變化流量下容易失真。團隊可能長時間處於告警疲勞，卻看不出真正侵蝕 SLO 的事件。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Burn rate 警示</td>
          <td>可靠性消耗速度是否異常</td>
          <td>優先序判讀</td>
      </tr>
      <tr>
          <td>SLO 驅動值班</td>
          <td>哪些事件需要立即接手</td>
          <td>響應節奏</td>
      </tr>
      <tr>
          <td>Tracing-first 分析</td>
          <td>事件路徑如何定位</td>
          <td>可追溯證據</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fast burn</td>
          <td>短期消耗是否超過容忍帶</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a></td>
      </tr>
      <tr>
          <td>slow burn</td>
          <td>長期趨勢是否持續惡化</td>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a></td>
      </tr>
      <tr>
          <td>trace outlier path</td>
          <td>關鍵路徑是否集中退化</td>
          <td><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</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>先用 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 組證據，再在 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 回寫驗證條件。</p>
]]></content:encoded></item><item><title>Netflix：Steady State、Chaos 與 FIT 的驗證路徑</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/</guid><description>&lt;p>Netflix chaos 實踐的核心責任是驗證「服務在失效條件下是否仍維持 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>」。重點是注入後能否用明確訊號證明系統仍可服務，故障注入數量是次要考量。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>許多團隊會做壓測與演練，但演練設計常停在工具層：kill instance、斷連線、延遲注入。這些動作本身不會自動產生可靠性結論。若沒有 steady state 與停止條件，演練只會留下「有做過 chaos」的紀錄。&lt;/p>
&lt;p>Netflix 的價值在於把 chaos 轉成科學化驗證循環：先定義穩態，再設計可證偽的假設。&lt;/p>
&lt;h2 id="決策機制">決策機制&lt;/h2>
&lt;p>一輪有效的 chaos 驗證要同時具備四個元素。&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>Steady state&lt;/td>
 &lt;td>服務正常時應維持什麼行為&lt;/td>
 &lt;td>穩態指標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hypothesis&lt;/td>
 &lt;td>失效發生後仍應維持什麼&lt;/td>
 &lt;td>可證偽假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Blast radius&lt;/td>
 &lt;td>實驗範圍怎麼限制&lt;/td>
 &lt;td>實驗邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abort condition&lt;/td>
 &lt;td>何時立即停止&lt;/td>
 &lt;td>風險切斷條件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>FIT（Failure Injection Testing）把注入粒度推進到 request path，讓測試更接近真實依賴路徑。這讓團隊能在不擴大範圍的前提下，驗證高價值路徑的容錯能力。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>steady-state SLI&lt;/td>
 &lt;td>注入後是否維持服務承諾&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>abort trigger count&lt;/td>
 &lt;td>停止條件是否可執行&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback success ratio&lt;/td>
 &lt;td>降級與替代路徑是否有效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>trace degradation pattern&lt;/td>
 &lt;td>退化是否集中於預期依賴&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>最常見錯誤是把 chaos 視為「故障越大越好」。這會把演練從驗證流程變成壓力展示，增加真實風險卻不提升可學習性。有效做法是用最小 blast radius 驗證最高價值假設，然後逐步放大。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>若要把本案例落地，先寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a> 的穩態欄位，再在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a> 定義停止條件。案例輸出的證據交給 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Netflix chaos 實踐的核心責任是驗證「服務在失效條件下是否仍維持 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>」。重點是注入後能否用明確訊號證明系統仍可服務，故障注入數量是次要考量。</p>
<h2 id="問題場景">問題場景</h2>
<p>許多團隊會做壓測與演練，但演練設計常停在工具層：kill instance、斷連線、延遲注入。這些動作本身不會自動產生可靠性結論。若沒有 steady state 與停止條件，演練只會留下「有做過 chaos」的紀錄。</p>
<p>Netflix 的價值在於把 chaos 轉成科學化驗證循環：先定義穩態，再設計可證偽的假設。</p>
<h2 id="決策機制">決策機制</h2>
<p>一輪有效的 chaos 驗證要同時具備四個元素。</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Steady state</td>
          <td>服務正常時應維持什麼行為</td>
          <td>穩態指標</td>
      </tr>
      <tr>
          <td>Hypothesis</td>
          <td>失效發生後仍應維持什麼</td>
          <td>可證偽假設</td>
      </tr>
      <tr>
          <td>Blast radius</td>
          <td>實驗範圍怎麼限制</td>
          <td>實驗邊界</td>
      </tr>
      <tr>
          <td>Abort condition</td>
          <td>何時立即停止</td>
          <td>風險切斷條件</td>
      </tr>
  </tbody>
</table>
<p>FIT（Failure Injection Testing）把注入粒度推進到 request path，讓測試更接近真實依賴路徑。這讓團隊能在不擴大範圍的前提下，驗證高價值路徑的容錯能力。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>steady-state SLI</td>
          <td>注入後是否維持服務承諾</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>abort trigger count</td>
          <td>停止條件是否可執行</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>fallback success ratio</td>
          <td>降級與替代路徑是否有效</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>trace degradation pattern</td>
          <td>退化是否集中於預期依賴</td>
          <td><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</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>最常見錯誤是把 chaos 視為「故障越大越好」。這會把演練從驗證流程變成壓力展示，增加真實風險卻不提升可學習性。有效做法是用最小 blast radius 驗證最高價值假設，然後逐步放大。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>若要把本案例落地，先寫 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a> 的穩態欄位，再在 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> 定義停止條件。案例輸出的證據交給 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 與 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a>。</p>
]]></content:encoded></item><item><title>6.21 Reliability Debt Backlog</title><link>https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>reliability debt 的責任：把可靠性缺口從口頭風險變成可管理 backlog&lt;/li>
&lt;li>來源：post-incident review、game day、load test、chaos、on-call toil、customer ticket&lt;/li>
&lt;li>debt 類型：missing automation、weak rollback、manual recovery、fragile dependency、observability gap&lt;/li>
&lt;li>欄位：impact、frequency、owner、evidence、mitigation、target state、closure signal&lt;/li>
&lt;li>排序方式：SLO 影響、事故重複率、toil 成本、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>、修復成本&lt;/li>
&lt;li>關閉條件：測試、演練、runbook 更新、alert 改善、manual step 移除&lt;/li>
&lt;li>跟 08 的交接：PIR action item 進 reliability debt，集中成可追蹤工作&lt;/li>
&lt;li>反模式：每次復盤都列改善，三個月後仍 open；toil 沒有量化；debt 無 owner&lt;/li>
&lt;/ul>
&lt;p>Reliability debt backlog 的重點是把「事故教訓」轉成「可交付工作」。沒有 backlog，團隊每次復盤都會得到相似結論；有 backlog，才有辦法把缺口排序、分派、驗收並逐步關閉。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Reliability debt backlog 是管理可靠性缺口的工作佇列，責任是把反覆事故、演練缺口與手動修復轉成可排序、可驗證、可關閉的工程工作。&lt;/p>
&lt;p>這一頁處理的是債務治理。可靠性問題常以事故、值班疲勞與手動操作出現；backlog 讓這些訊號進入產品與工程排程。&lt;/p>
&lt;p>debt backlog 也提供跨團隊溝通語言。平台、服務、SRE 與產品可以用同一組欄位討論優先序，讓決策建立在同一批證據與欄位定義上。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 reliability debt 時，先看缺口是否有 evidence，再看關閉條件是否可驗證。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>debt 是否連到事故、演練或 toil 證據&lt;/li>
&lt;li>owner 是否能決定修復方案與排程&lt;/li>
&lt;li>impact 是否能對應 SLO、customer impact 或 on-call cost&lt;/li>
&lt;li>mitigation 是否只降低風險，或真正移除根因&lt;/li>
&lt;li>closure signal 是否能由測試、演練或監控證明&lt;/li>
&lt;/ul>
&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>Impact / Frequency&lt;/td>
 &lt;td>定義業務與技術代價&lt;/td>
 &lt;td>是否可量化到 SLO / toil / 客訴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner / Due&lt;/td>
 &lt;td>明確責任與時程&lt;/td>
 &lt;td>是否有人可決策與執行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence&lt;/td>
 &lt;td>連回事故或演練證據&lt;/td>
 &lt;td>是否能追溯原始問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mitigation / Target&lt;/td>
 &lt;td>區分短期止血與長期修法&lt;/td>
 &lt;td>是否避免只補 workaround&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Closure Signal&lt;/td>
 &lt;td>定義完成條件&lt;/td>
 &lt;td>是否可由測試或演練驗證&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同類事故重複發生，但每次 action item 都重新命名&lt;/li>
&lt;li>on-call 反覆手動修同一個問題&lt;/li>
&lt;li>runbook 記錄 workaround，但沒有工程化任務&lt;/li>
&lt;li>debt backlog 只有優先級，缺少 impact / evidence / closure&lt;/li>
&lt;li>reliability 工作永遠輸給 feature，但事故成本持續上升&lt;/li>
&lt;/ul>
&lt;p>實務上最常見的失敗模式是 action item 全留在會議筆記。三個月後同類事故再發生，團隊才重新開同一張單。把 PIR 直接轉進 debt backlog，才能讓「是否真的改善」變成可驗證事實。&lt;/p>
&lt;h2 id="action-item-分級跟-release-gate-綁定">Action Item 分級跟 Release Gate 綁定&lt;/h2>
&lt;p>Action item 分級的核心責任是給每個改進項匹配的強制力：高風險者進 release gate 綁定、中風險者進 backlog 落地節點、低風險者保留追蹤節點。三類風險（重複事故、影響面放大、診斷效率）各需不同強制力、沒有分級時所有改進項並列競爭資源、強制力被攤平。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2 Google Postmortem Action Item Closure 治理&lt;/a>：揭露三層機制對應上述三類風險 — action item 分級（P0/P1/P2）、可驗證完成條件（不是「優化」「強化」抽象字）、closure 進固定 review cadence。&lt;/p>
&lt;p>P0/P1/P2 分級的核心價值是「給高風險 action item 強制力」：&lt;/p>
&lt;ul>
&lt;li>P0 重複事故高機率再發生：下個 release 週期前完成並驗證&lt;/li>
&lt;li>P1 會放大事故影響面：要有落地日期跟 gate 條件&lt;/li>
&lt;li>P2 提升診斷或操作效率：可排 backlog、但保留追蹤節點&lt;/li>
&lt;/ul>
&lt;p>最關鍵的綁定是 &lt;strong>P0/P1 直接掛到 release gate&lt;/strong>：未完成時不得放行關聯變更。這層綁定才讓分級從「backlog 優先序」升級為「工程強制力」 — P0/P1 直接決定 release 是否放行、未完成的 action item 直接是放行條件缺口。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/#%e8%ae%8a%e6%9b%b4%e5%88%86%e5%b1%a4%e8%b7%9f-gate-%e6%94%bf%e7%ad%96" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate 變更分層&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>reliability debt 的責任：把可靠性缺口從口頭風險變成可管理 backlog</li>
<li>來源：post-incident review、game day、load test、chaos、on-call toil、customer ticket</li>
<li>debt 類型：missing automation、weak rollback、manual recovery、fragile dependency、observability gap</li>
<li>欄位：impact、frequency、owner、evidence、mitigation、target state、closure signal</li>
<li>排序方式：SLO 影響、事故重複率、toil 成本、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、修復成本</li>
<li>關閉條件：測試、演練、runbook 更新、alert 改善、manual step 移除</li>
<li>跟 08 的交接：PIR action item 進 reliability debt，集中成可追蹤工作</li>
<li>反模式：每次復盤都列改善，三個月後仍 open；toil 沒有量化；debt 無 owner</li>
</ul>
<p>Reliability debt backlog 的重點是把「事故教訓」轉成「可交付工作」。沒有 backlog，團隊每次復盤都會得到相似結論；有 backlog，才有辦法把缺口排序、分派、驗收並逐步關閉。</p>
<h2 id="概念定位">概念定位</h2>
<p>Reliability debt backlog 是管理可靠性缺口的工作佇列，責任是把反覆事故、演練缺口與手動修復轉成可排序、可驗證、可關閉的工程工作。</p>
<p>這一頁處理的是債務治理。可靠性問題常以事故、值班疲勞與手動操作出現；backlog 讓這些訊號進入產品與工程排程。</p>
<p>debt backlog 也提供跨團隊溝通語言。平台、服務、SRE 與產品可以用同一組欄位討論優先序，讓決策建立在同一批證據與欄位定義上。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 reliability debt 時，先看缺口是否有 evidence，再看關閉條件是否可驗證。</p>
<p>重點訊號包括：</p>
<ul>
<li>debt 是否連到事故、演練或 toil 證據</li>
<li>owner 是否能決定修復方案與排程</li>
<li>impact 是否能對應 SLO、customer impact 或 on-call cost</li>
<li>mitigation 是否只降低風險，或真正移除根因</li>
<li>closure signal 是否能由測試、演練或監控證明</li>
</ul>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>目的</th>
          <th>驗收重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Impact / Frequency</td>
          <td>定義業務與技術代價</td>
          <td>是否可量化到 SLO / toil / 客訴</td>
      </tr>
      <tr>
          <td>Owner / Due</td>
          <td>明確責任與時程</td>
          <td>是否有人可決策與執行</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>連回事故或演練證據</td>
          <td>是否能追溯原始問題</td>
      </tr>
      <tr>
          <td>Mitigation / Target</td>
          <td>區分短期止血與長期修法</td>
          <td>是否避免只補 workaround</td>
      </tr>
      <tr>
          <td>Closure Signal</td>
          <td>定義完成條件</td>
          <td>是否可由測試或演練驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同類事故重複發生，但每次 action item 都重新命名</li>
<li>on-call 反覆手動修同一個問題</li>
<li>runbook 記錄 workaround，但沒有工程化任務</li>
<li>debt backlog 只有優先級，缺少 impact / evidence / closure</li>
<li>reliability 工作永遠輸給 feature，但事故成本持續上升</li>
</ul>
<p>實務上最常見的失敗模式是 action item 全留在會議筆記。三個月後同類事故再發生，團隊才重新開同一張單。把 PIR 直接轉進 debt backlog，才能讓「是否真的改善」變成可驗證事實。</p>
<h2 id="action-item-分級跟-release-gate-綁定">Action Item 分級跟 Release Gate 綁定</h2>
<p>Action item 分級的核心責任是給每個改進項匹配的強制力：高風險者進 release gate 綁定、中風險者進 backlog 落地節點、低風險者保留追蹤節點。三類風險（重複事故、影響面放大、診斷效率）各需不同強制力、沒有分級時所有改進項並列競爭資源、強制力被攤平。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2 Google Postmortem Action Item Closure 治理</a>：揭露三層機制對應上述三類風險 — action item 分級（P0/P1/P2）、可驗證完成條件（不是「優化」「強化」抽象字）、closure 進固定 review cadence。</p>
<p>P0/P1/P2 分級的核心價值是「給高風險 action item 強制力」：</p>
<ul>
<li>P0 重複事故高機率再發生：下個 release 週期前完成並驗證</li>
<li>P1 會放大事故影響面：要有落地日期跟 gate 條件</li>
<li>P2 提升診斷或操作效率：可排 backlog、但保留追蹤節點</li>
</ul>
<p>最關鍵的綁定是 <strong>P0/P1 直接掛到 release gate</strong>：未完成時不得放行關聯變更。這層綁定才讓分級從「backlog 優先序」升級為「工程強制力」 — P0/P1 直接決定 release 是否放行、未完成的 action item 直接是放行條件缺口。詳見 <a href="/blog/backend/06-reliability/release-gate/#%e8%ae%8a%e6%9b%b4%e5%88%86%e5%b1%a4%e8%b7%9f-gate-%e6%94%bf%e7%ad%96" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate 變更分層</a>。</p>
<p>整體 reliability 訊號量化（含 toil ratio、closure rate、debt 趨勢）由 <a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 reliability-metrics-governance</a> 處理。</p>
<h2 id="toil-budget把重複手動工作變成預算問題">Toil Budget：把重複手動工作變成預算問題</h2>
<p>Toil budget 是把重複手動工作量化成預算、用 closure 機制強制超標部分轉投自動化的治理工具。Toil 沒被當預算治理時、會吸收 SRE 時間、把可靠性改進工作擠掉。</p>
<p>對應 <a href="/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">G3 Google Toil Budget 與 Automation 投資政策</a>：揭露四個機制 — toil 分類（哪些屬可自動化）、時間配比（Google SRE 經驗值 50%、組織應依自身 toil 性質校準、不是普世門檻）、超標處理（凍結部分 feature、轉投自動化）、改善驗證（closure 指標跟 evidence）。前兩項屬「測量設計」（toil 是什麼 + 多少算超標）、後兩項屬「治理動作」（超標後做什麼 + 改善如何驗證）。</p>
<p>Toil budget 跟 reliability debt backlog 是兩個面向：</p>
<ul>
<li>Reliability debt backlog：管「失效缺口」（事故 / 演練揭露的工程化任務）</li>
<li>Toil budget：管「日常壓力」（on-call 反覆手動工作的時間成本）</li>
</ul>
<p>兩者要共用同一個 closure 機制：toil 超標時、超標部分強制轉投自動化、進 debt backlog 排序、按完成條件驗收。這層綁定讓 toil 超標自動觸發改善排程：超標 ratio 進日常輸入信號、相關 feature 凍結、自動化工作進 debt backlog 排序、按完成條件驗收。把 toil ratio 當日常治理輸入、而非 on-call burnout 後的事後指標。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.8 signal governance loop：把觀測缺口變成 debt</li>
<li>06.8 release gate：高風險 debt 可成為 freeze 條件</li>
<li>06.18 reliability metrics governance：量化 debt 趨勢</li>
<li>08.5 post-incident review：PIR action items 的上游來源</li>
<li>08.13 repeated incident / toil：反覆事故與 toil 的事故端入口</li>
</ul>
]]></content:encoded></item><item><title>8.21 Incident Workflow Automation Boundary</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-workflow-automation-boundary/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-workflow-automation-boundary/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>automation boundary 的責任：把可自動化的事故工作與需要人工判斷的決策分開&lt;/li>
&lt;li>適合自動化：channel creation、role reminder、template update、status sync、evidence collection、ticket creation&lt;/li>
&lt;li>需要人工確認：severity upgrade、customer impact statement、rollback execution、security disclosure、compensation&lt;/li>
&lt;li>guardrail：approval、dry run、rollback condition、audit log、rate limit&lt;/li>
&lt;li>風險：自動化誤升級、誤通知、錯誤 rollback、過度信任 enrichment&lt;/li>
&lt;li>跟 vendor / IR platform 的關係：工具支援流程，決策邊界仍需由團隊定義&lt;/li>
&lt;li>跟 07 的交接：高風險自動化需要權限、稽核與安全例外治理&lt;/li>
&lt;li>反模式：把所有 incident workflow 都交給 bot；bot 產生錯誤 status update；自動化沒有停止條件&lt;/li>
&lt;/ul>
&lt;p>Incident workflow automation boundary 的價值是把速度與責任同時保住。事故流程中有大量可標準化動作，適合自動化；但分級、回退、對外說法與資安披露仍需要情境判斷，必須保留人類決策責任。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Incident workflow automation boundary 是事故流程自動化的決策邊界，責任是讓工具減少手動摩擦，同時保留高風險決策的人類確認。&lt;/p>
&lt;p>這一頁處理的是自動化取捨。事故流程有大量可預期動作，但 severity、rollback、對外說法與資安披露都帶有情境判斷與責任風險。&lt;/p>
&lt;p>邊界定義越清楚，工具越有價值。當團隊先定義好「可自動化動作」與「需人工確認動作」，bot 才能專注減少摩擦，而不會擴大決策風險。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 automation boundary 時，先看動作是否可逆，再看錯誤自動化的影響範圍。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>自動化動作是否只建立容器、收集資料或提醒角色&lt;/li>
&lt;li>高風險動作是否有 approval 與 audit log&lt;/li>
&lt;li>bot 產出的資訊是否標示 confidence 與來源&lt;/li>
&lt;li>workflow 是否有 stop condition 與 manual override&lt;/li>
&lt;li>自動化是否支援 IC，並保留 IC 的決策責任&lt;/li>
&lt;/ul>
&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>流程容器建立&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>頻道命名規範、角色模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證據彙整與同步&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>來源標示、信心標示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分級與回退決策&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>人工核准、雙重確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外狀態更新&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>審核流程、回退機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高風險操作觸發&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>權限隔離、audit log&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="自動化分層">自動化分層&lt;/h2>
&lt;p>Incident workflow automation boundary 的分層責任是把「節省摩擦」和「替人決策」分開。越接近容器建立與資料彙整，越適合自動化；越接近分級、回復、對外聲明與資安披露，越需要人工確認。&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>Workflow setup&lt;/td>
 &lt;td>建頻道、建 ticket、套模板、提醒角色&lt;/td>
 &lt;td>命名錯誤、重複建立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence collection&lt;/td>
 &lt;td>拉 dashboard、query、status、deploy&lt;/td>
 &lt;td>資料過期、來源誤解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Enrichment&lt;/td>
 &lt;td>加 owner、service map、recent change&lt;/td>
 &lt;td>關聯錯誤、信心未標示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recommendation&lt;/td>
 &lt;td>建議 severity、runbook、next action&lt;/td>
 &lt;td>建議被誤當決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Execution&lt;/td>
 &lt;td>rollback、traffic shift、customer update&lt;/td>
 &lt;td>次生事故、法務或資安風險&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Workflow setup 適合高度自動化。這層動作可逆、低風險，能讓 IC 省下開頻道、拉人、建文件與貼模板的時間。&lt;/p>
&lt;p>Evidence collection 適合自動化，但要標示來源與時間。bot 可以貼 dashboard、query、vendor status、recent deploy 與 support ticket，但應標示 timestamp、source 與 confidence。&lt;/p>
&lt;p>Enrichment 適合輔助判讀。service owner、dependency map、runbook、recent change 與 feature flag 狀態可以自動補上，但要允許 IC 修正。&lt;/p>
&lt;p>Recommendation 應保持建議語氣。bot 可以建議 severity、runbook 或 next action，但 IC 需要確認，並把採納或拒絕寫進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log&lt;/a>。&lt;/p>
&lt;p>Execution 是高風險層。rollback、traffic shift、status page publish、customer email、security disclosure 與 compensation 都應有人工確認、權限隔離與 audit log。&lt;/p>
&lt;h2 id="人工確認邊界">人工確認邊界&lt;/h2>
&lt;p>人工確認邊界的責任是保留責任判斷。自動化可以加速準備與整理，但高風險決策需要有人確認情境、證據與後果。&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>Severity upgrade&lt;/td>
 &lt;td>影響通訊、值班與 stakeholder&lt;/td>
 &lt;td>IC 確認、impact evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Customer impact statement&lt;/td>
 &lt;td>影響外部信任與合約&lt;/td>
 &lt;td>Comms / IC review、confidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback execution&lt;/td>
 &lt;td>可能影響資料、版本與流量&lt;/td>
 &lt;td>service owner approval、dry run&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security disclosure&lt;/td>
 &lt;td>涉及法規、證據與對外責任&lt;/td>
 &lt;td>security lead、legal route&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compensation&lt;/td>
 &lt;td>涉及金額與商務政策&lt;/td>
 &lt;td>business owner、reconciled impact&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Severity upgrade 需要 IC 確認。bot 可以根據 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a>、ticket 數與 status page 建議升級，但 severity 會改變通訊節奏與資源分配，需要保留人類責任。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>automation boundary 的責任：把可自動化的事故工作與需要人工判斷的決策分開</li>
<li>適合自動化：channel creation、role reminder、template update、status sync、evidence collection、ticket creation</li>
<li>需要人工確認：severity upgrade、customer impact statement、rollback execution、security disclosure、compensation</li>
<li>guardrail：approval、dry run、rollback condition、audit log、rate limit</li>
<li>風險：自動化誤升級、誤通知、錯誤 rollback、過度信任 enrichment</li>
<li>跟 vendor / IR platform 的關係：工具支援流程，決策邊界仍需由團隊定義</li>
<li>跟 07 的交接：高風險自動化需要權限、稽核與安全例外治理</li>
<li>反模式：把所有 incident workflow 都交給 bot；bot 產生錯誤 status update；自動化沒有停止條件</li>
</ul>
<p>Incident workflow automation boundary 的價值是把速度與責任同時保住。事故流程中有大量可標準化動作，適合自動化；但分級、回退、對外說法與資安披露仍需要情境判斷，必須保留人類決策責任。</p>
<h2 id="概念定位">概念定位</h2>
<p>Incident workflow automation boundary 是事故流程自動化的決策邊界，責任是讓工具減少手動摩擦，同時保留高風險決策的人類確認。</p>
<p>這一頁處理的是自動化取捨。事故流程有大量可預期動作，但 severity、rollback、對外說法與資安披露都帶有情境判斷與責任風險。</p>
<p>邊界定義越清楚，工具越有價值。當團隊先定義好「可自動化動作」與「需人工確認動作」，bot 才能專注減少摩擦，而不會擴大決策風險。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 automation boundary 時，先看動作是否可逆，再看錯誤自動化的影響範圍。</p>
<p>重點訊號包括：</p>
<ul>
<li>自動化動作是否只建立容器、收集資料或提醒角色</li>
<li>高風險動作是否有 approval 與 audit log</li>
<li>bot 產出的資訊是否標示 confidence 與來源</li>
<li>workflow 是否有 stop condition 與 manual override</li>
<li>自動化是否支援 IC，並保留 IC 的決策責任</li>
</ul>
<table>
  <thead>
      <tr>
          <th>動作類型</th>
          <th>自動化適配</th>
          <th>安全護欄</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>流程容器建立</td>
          <td>高</td>
          <td>頻道命名規範、角色模板</td>
      </tr>
      <tr>
          <td>證據彙整與同步</td>
          <td>高</td>
          <td>來源標示、信心標示</td>
      </tr>
      <tr>
          <td>分級與回退決策</td>
          <td>低</td>
          <td>人工核准、雙重確認</td>
      </tr>
      <tr>
          <td>對外狀態更新</td>
          <td>中</td>
          <td>審核流程、回退機制</td>
      </tr>
      <tr>
          <td>高風險操作觸發</td>
          <td>低</td>
          <td>權限隔離、audit log</td>
      </tr>
  </tbody>
</table>
<h2 id="自動化分層">自動化分層</h2>
<p>Incident workflow automation boundary 的分層責任是把「節省摩擦」和「替人決策」分開。越接近容器建立與資料彙整，越適合自動化；越接近分級、回復、對外聲明與資安披露，越需要人工確認。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>適合自動化內容</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workflow setup</td>
          <td>建頻道、建 ticket、套模板、提醒角色</td>
          <td>命名錯誤、重複建立</td>
      </tr>
      <tr>
          <td>Evidence collection</td>
          <td>拉 dashboard、query、status、deploy</td>
          <td>資料過期、來源誤解</td>
      </tr>
      <tr>
          <td>Enrichment</td>
          <td>加 owner、service map、recent change</td>
          <td>關聯錯誤、信心未標示</td>
      </tr>
      <tr>
          <td>Recommendation</td>
          <td>建議 severity、runbook、next action</td>
          <td>建議被誤當決策</td>
      </tr>
      <tr>
          <td>Execution</td>
          <td>rollback、traffic shift、customer update</td>
          <td>次生事故、法務或資安風險</td>
      </tr>
  </tbody>
</table>
<p>Workflow setup 適合高度自動化。這層動作可逆、低風險，能讓 IC 省下開頻道、拉人、建文件與貼模板的時間。</p>
<p>Evidence collection 適合自動化，但要標示來源與時間。bot 可以貼 dashboard、query、vendor status、recent deploy 與 support ticket，但應標示 timestamp、source 與 confidence。</p>
<p>Enrichment 適合輔助判讀。service owner、dependency map、runbook、recent change 與 feature flag 狀態可以自動補上，但要允許 IC 修正。</p>
<p>Recommendation 應保持建議語氣。bot 可以建議 severity、runbook 或 next action，但 IC 需要確認，並把採納或拒絕寫進 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a>。</p>
<p>Execution 是高風險層。rollback、traffic shift、status page publish、customer email、security disclosure 與 compensation 都應有人工確認、權限隔離與 audit log。</p>
<h2 id="人工確認邊界">人工確認邊界</h2>
<p>人工確認邊界的責任是保留責任判斷。自動化可以加速準備與整理，但高風險決策需要有人確認情境、證據與後果。</p>
<table>
  <thead>
      <tr>
          <th>需要確認的動作</th>
          <th>原因</th>
          <th>最小護欄</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Severity upgrade</td>
          <td>影響通訊、值班與 stakeholder</td>
          <td>IC 確認、impact evidence</td>
      </tr>
      <tr>
          <td>Customer impact statement</td>
          <td>影響外部信任與合約</td>
          <td>Comms / IC review、confidence</td>
      </tr>
      <tr>
          <td>Rollback execution</td>
          <td>可能影響資料、版本與流量</td>
          <td>service owner approval、dry run</td>
      </tr>
      <tr>
          <td>Security disclosure</td>
          <td>涉及法規、證據與對外責任</td>
          <td>security lead、legal route</td>
      </tr>
      <tr>
          <td>Compensation</td>
          <td>涉及金額與商務政策</td>
          <td>business owner、reconciled impact</td>
      </tr>
  </tbody>
</table>
<p>Severity upgrade 需要 IC 確認。bot 可以根據 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、ticket 數與 status page 建議升級，但 severity 會改變通訊節奏與資源分配，需要保留人類責任。</p>
<p>Customer impact statement 需要 comms 與 IC 協作。自動化可以產生初稿，但對外文字要反映已確認事實、confidence 與下一次更新時間。</p>
<p>Rollback execution 需要 service owner 確認。回滾可能受到 migration、feature flag、cache、client contract 與資料相容性影響，錯誤率只是判斷輸入之一。</p>
<p>Security disclosure 需要資安與法務路由。涉及資料外洩、權限濫用或合規通知時，自動化只能建立容器與 evidence checklist，披露決策需要專責角色確認。</p>
<h2 id="guardrail-設計">Guardrail 設計</h2>
<p>Automation guardrail 的責任是讓自動化行為可控、可停、可審計。每個 bot action 都應有範圍、權限、回退與紀錄。</p>
<table>
  <thead>
      <tr>
          <th>Guardrail</th>
          <th>責任</th>
          <th>適用動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Approval</td>
          <td>高風險動作前取得確認</td>
          <td>rollback、status update、severity</td>
      </tr>
      <tr>
          <td>Dry run</td>
          <td>先展示將要做的改變</td>
          <td>rollback、ticket bulk update</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>保存誰觸發、何時、做了什麼</td>
          <td>所有自動化</td>
      </tr>
      <tr>
          <td>Rate limit</td>
          <td>限制通知、查詢與變更頻率</td>
          <td>paging、ticket、status sync</td>
      </tr>
      <tr>
          <td>Manual override</td>
          <td>允許 IC 停用或接管 bot</td>
          <td>所有事中自動化</td>
      </tr>
      <tr>
          <td>Confidence label</td>
          <td>標示資料來源與可信度</td>
          <td>enrichment、recommendation</td>
      </tr>
      <tr>
          <td>Rollback condition</td>
          <td>定義自動化後如何撤回</td>
          <td>workflow update、routing change</td>
      </tr>
  </tbody>
</table>
<p>Approval 適合高風險動作。批准者應是對後果有責任的人，例如 IC、service owner、security lead、comms lead 或 business owner。</p>
<p>Dry run 能降低自動化黑箱感。bot 在執行前顯示即將改動的 status page、rollback target、ticket list 或 notification recipient，讓人類能快速檢查。</p>
<p>Manual override 是事故流程的基本安全閥。IC 需要能暫停 bot、停用自動更新、切換到手動流程，並留下 decision log。</p>
<p>Confidence label 能避免 enrichment 被誤當事實。自動補出的 owner、recent deploy、vendor status 或 impact estimate 都應顯示來源與時間。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>bot 自動開 incident，但沒有人確認 severity</li>
<li>status page 被 template 自動更新，內容與實際影響不一致</li>
<li>rollback 被自動觸發後，團隊才發現資料 migration 還在進行</li>
<li>enrichment 資料來源過期，但被當成事實使用</li>
<li>自動化成功率高，但事故期間沒有人知道如何停用</li>
</ul>
<p>典型場景是 bot 能快速建立 incident channel、拉齊角色與初版模板，這些都能穩定節省時間；但若 bot 直接執行 rollback 或發布對外影響描述，錯誤成本會急遽上升。邊界的責任就是把這條線畫清楚。</p>
<h2 id="vendor--ir-platform-關係">Vendor / IR Platform 關係</h2>
<p>IR platform 的責任是支援流程，決策邊界仍由團隊定義。Pager、incident channel、status page、postmortem template 與 workflow engine 都需要由團隊配置 owner、approval、field schema 與 audit route。</p>
<p>On-call 與 IR 工具適合自動化流程容器。它們可以建立 incident、指派角色、同步 status、建立 ticket、提醒 handoff 與收集 evidence。</p>
<p>Status page 工具適合自動化草稿與同步。公開發布前仍需要 IC 或 comms lead 確認，因為影響描述、confidence 與補償語氣都會影響客戶信任。</p>
<p>Postmortem 工具適合自動收集 timeline、decision log 與 action item。復盤結論仍需要人類判讀，把事故教訓回寫到 04、06、07 與產品流程。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Incident workflow automation 的反模式通常來自把工具速度當成流程成熟度。速度有價值，但責任邊界、資料可信度與人工確認才決定事故流程是否可靠。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bot 接管所有流程</td>
          <td>分級、通訊、rollback 都自動執行</td>
          <td>分層 automation boundary</td>
      </tr>
      <tr>
          <td>Status update 自動發布</td>
          <td>對外文字與實際 impact 不一致</td>
          <td>草稿自動化，發布人工確認</td>
      </tr>
      <tr>
          <td>Enrichment 無來源</td>
          <td>bot 補的 owner / impact 被當事實</td>
          <td>標示 source、timestamp、confidence</td>
      </tr>
      <tr>
          <td>無 stop condition</td>
          <td>自動化錯誤後持續擴散</td>
          <td>manual override、rate limit</td>
      </tr>
      <tr>
          <td>無 audit log</td>
          <td>事後不知道誰觸發了什麼</td>
          <td>所有 bot action 留紀錄</td>
      </tr>
  </tbody>
</table>
<p>Bot 接管所有流程會讓事故責任模糊。工具可以準備資料、提示角色與建議下一步，但 IC 仍要負責分級、優先序與高風險決策。</p>
<p>Enrichment 無來源會製造錯誤安全感。自動補充的 owner、recent deploy 或 customer impact 若沒有 timestamp 與來源，團隊容易把推測當成事實。</p>
<p>無 audit log 會破壞復盤。自動化動作也是事故事件的一部分，應能被 decision log 與 post-incident review 回放。</p>
<h2 id="與資安治理的關係">與資安治理的關係</h2>
<p>Incident workflow automation 需要接到資安權限與例外治理。自動化越靠近 rollback、traffic shift、status publish、customer data 或 security disclosure，越需要 least privilege、approval、audit log 與 exception review。</p>
<p>高風險自動化應使用分離權限。建立 incident channel 與讀 dashboard 可以是低權限；執行 rollback、讀 audit log、匯出客戶資料或發布對外聲明，需要更高權限與明確核准。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>08.1 severity trigger：定義哪些升級可自動建議、哪些需人工確認</li>
<li>08.2 incident command roles：讓 bot 支援角色提醒與交接</li>
<li>08.4 incident communication：保護對外通訊的人類確認邊界</li>
<li>08.19 incident decision log：自動化動作也要留下決策紀錄</li>
<li>07.14 security exception / <a href="/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire</a>：高風險自動化接安全例外治理</li>
<li>05 deployment platform：rollback / rollout automation 的實作邊界</li>
</ul>
]]></content:encoded></item><item><title>Heroku</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/</guid><description>&lt;p>Heroku 是早期 PaaS 的代表、router 層事故揭露 multi-tenant 路由的失敗模式。Heroku status 與工程文章累積多年事故敘事。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Router 層失效：多租戶 PaaS 的入口失效擴散&lt;/li>
&lt;li>Dyno scheduling：背景排程系統的 failure mode&lt;/li>
&lt;li>Add-on dependency：第三方服務嵌入 PaaS 後的責任邊界&lt;/li>
&lt;li>Salesforce 收購後的 IR 演化&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2021&lt;/td>
 &lt;td>Router incidents&lt;/td>
 &lt;td>多租戶 PaaS 的入口失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2022&lt;/td>
 &lt;td>DB add-on 事故&lt;/td>
 &lt;td>第三方依賴的責任歸屬&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Heroku 這個案例在講的是 PaaS 入口路由如何成為多租戶事故的第一個放大點。讀者先抓 router、dyno scheduling 與 add-on dependency 的責任，再把 status 通訊視為事故管理的一部分。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 router 或 keepalive 機制出現問題時，事故不只影響單一應用，而會直接影響入口流量與租戶隔離。當第三方 add-on 失效時，責任邊界也要一起說清楚，否則客戶會把平台與外部依賴視為同一個故障面。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否區分 router 層與應用層問題&lt;/li>
&lt;li>能否說明 add-on 依賴的責任邊界&lt;/li>
&lt;li>能否把 incident 通訊路由到正確的 status channel&lt;/li>
&lt;li>能否把多租戶入口失效視為平台級風險&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Heroku 比較像是 PaaS 世界裡的 AWS S3 或 Cloudflare，因為入口路由一出問題，很多 tenant 會一起受影響。它也能和 Datadog、Slack 對照，幫讀者理解平台本身與平台上的應用該怎麼切責任邊界。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>router incidents 顯示入口層是多租戶 PaaS 的第一個放大器。&lt;/li>
&lt;li>DB add-on 事故則讓第三方依賴的責任邊界變得很清楚。&lt;/li>
&lt;li>keepalive 與 internal routing 會直接影響租戶體感。&lt;/li>
&lt;li>status channel 的選擇也是事故管理的一部分。&lt;/li>
&lt;li>dyno scheduling 的問題會把平台內部失衡直接變成租戶可見故障。&lt;/li>
&lt;li>Salesforce Trust 作為主通路，改變了 Heroku 事故通訊的路由方式。&lt;/li>
&lt;li>multi-tenant routing 讓入口層成為最敏感的擴散點。&lt;/li>
&lt;li>third-party add-on 事故提醒平台必須清楚切出責任邊界。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">HR1&lt;/a>&lt;/td>
 &lt;td>Routing 控制事件&lt;/td>
 &lt;td>在 PaaS 多租戶入口層限制擴散與分批回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://devcenter.heroku.com/articles/heroku-status">Heroku Status&lt;/a>：Heroku incident 通訊與歷史紀錄的官方說明。&lt;/li>
&lt;li>&lt;a href="https://devcenter.heroku.com/changelog-items/3422">Salesforce Trust is now the primary channel for all Heroku incident and maintenance communications&lt;/a>：Heroku status 通訊的最新主通路。&lt;/li>
&lt;li>&lt;a href="https://devcenter.heroku.com/articles/heroku-labs-disabling-keepalives-to-dyno-for-router-2-0">Heroku Labs: Disabling Keepalives to Dyno for the Common Runtime Router&lt;/a>：Router / keepalive 的官方設計說明。&lt;/li>
&lt;li>&lt;a href="https://devcenter.heroku.com/articles/internal-routing">Internal Routing&lt;/a>：PaaS 內部路由與多租戶邊界。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Heroku 是早期 PaaS 的代表、router 層事故揭露 multi-tenant 路由的失敗模式。Heroku status 與工程文章累積多年事故敘事。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Router 層失效：多租戶 PaaS 的入口失效擴散</li>
<li>Dyno scheduling：背景排程系統的 failure mode</li>
<li>Add-on dependency：第三方服務嵌入 PaaS 後的責任邊界</li>
<li>Salesforce 收購後的 IR 演化</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2021</td>
          <td>Router incidents</td>
          <td>多租戶 PaaS 的入口失效</td>
      </tr>
      <tr>
          <td>2022</td>
          <td>DB add-on 事故</td>
          <td>第三方依賴的責任歸屬</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Heroku 這個案例在講的是 PaaS 入口路由如何成為多租戶事故的第一個放大點。讀者先抓 router、dyno scheduling 與 add-on dependency 的責任，再把 status 通訊視為事故管理的一部分。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 router 或 keepalive 機制出現問題時，事故不只影響單一應用，而會直接影響入口流量與租戶隔離。當第三方 add-on 失效時，責任邊界也要一起說清楚，否則客戶會把平台與外部依賴視為同一個故障面。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否區分 router 層與應用層問題</li>
<li>能否說明 add-on 依賴的責任邊界</li>
<li>能否把 incident 通訊路由到正確的 status channel</li>
<li>能否把多租戶入口失效視為平台級風險</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Heroku 比較像是 PaaS 世界裡的 AWS S3 或 Cloudflare，因為入口路由一出問題，很多 tenant 會一起受影響。它也能和 Datadog、Slack 對照，幫讀者理解平台本身與平台上的應用該怎麼切責任邊界。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>router incidents 顯示入口層是多租戶 PaaS 的第一個放大器。</li>
<li>DB add-on 事故則讓第三方依賴的責任邊界變得很清楚。</li>
<li>keepalive 與 internal routing 會直接影響租戶體感。</li>
<li>status channel 的選擇也是事故管理的一部分。</li>
<li>dyno scheduling 的問題會把平台內部失衡直接變成租戶可見故障。</li>
<li>Salesforce Trust 作為主通路，改變了 Heroku 事故通訊的路由方式。</li>
<li>multi-tenant routing 讓入口層成為最敏感的擴散點。</li>
<li>third-party add-on 事故提醒平台必須清楚切出責任邊界。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">HR1</a></td>
          <td>Routing 控制事件</td>
          <td>在 PaaS 多租戶入口層限制擴散與分批回復</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://devcenter.heroku.com/articles/heroku-status">Heroku Status</a>：Heroku incident 通訊與歷史紀錄的官方說明。</li>
<li><a href="https://devcenter.heroku.com/changelog-items/3422">Salesforce Trust is now the primary channel for all Heroku incident and maintenance communications</a>：Heroku status 通訊的最新主通路。</li>
<li><a href="https://devcenter.heroku.com/articles/heroku-labs-disabling-keepalives-to-dyno-for-router-2-0">Heroku Labs: Disabling Keepalives to Dyno for the Common Runtime Router</a>：Router / keepalive 的官方設計說明。</li>
<li><a href="https://devcenter.heroku.com/articles/internal-routing">Internal Routing</a>：PaaS 內部路由與多租戶邊界。</li>
</ul>
]]></content:encoded></item><item><title>Spotify</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/</guid><description>&lt;p>Spotify 是音樂串流平台、squad-based 組織模型對 SRE 實踐有特殊影響、chaos engineering 文章是 mid-size company 採用 chaos 的代表。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Squad-based SRE：分散式組織下的可靠性責任分配&lt;/li>
&lt;li>Backstage：開源開發者平台的可靠性整合&lt;/li>
&lt;li>Chaos engineering 採用過程：從 zero 到 mature 的實踐軌跡&lt;/li>
&lt;li>Streaming infrastructure：高頻寬媒體的可靠性挑戰&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Backstage&lt;/td>
 &lt;td>service catalog + reliability metadata&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Squad SRE&lt;/td>
 &lt;td>分散組織的可靠性責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Chaos Engineering Adoption&lt;/td>
 &lt;td>Spotify 的 chaos 起步歷程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN / Streaming Resilience&lt;/td>
 &lt;td>媒體串流的失敗模式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Spotify 這個案例在講的是平台工程如何把可靠性散到每個 squad，又把共同能力集中到 Backstage 這類基礎設施。讀者先抓 squad-based SRE、service catalog 與 declarative infrastructure 的關係，再看它們怎麼支撐大型串流平台。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當組織採用分散責任模型時，可靠性不再只靠中央團隊，而是靠平台把常見能力做成共同元件。當 fleet 或 streaming 基礎設施需要治理時，重點是 catalog 與 control plane 是否讓團隊看得到、管得動。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否把 service catalog 跟 reliability metadata 接起來&lt;/li>
&lt;li>能否說清楚 squad 與平台各自負責什麼&lt;/li>
&lt;li>能否用 declarative infrastructure 管 fleet 變化&lt;/li>
&lt;li>能否在 chaos 採用時保住平台一致性&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Spotify 的重點是把可靠性做成平台能力，這和 LinkedIn 的 operability、Honeycomb 的 observability、Meta 的 control plane 治理屬於相近抽象層。不同的是 Spotify 更強調組織分工，所以很適合拿來說明平台如何支撐分散團隊。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>Backstage 將 service catalog 與 reliability metadata 整合成平台入口。&lt;/li>
&lt;li>declarative infrastructure 讓 fleet 管理變成可重現的控制流程。&lt;/li>
&lt;li>squad-based SRE 讓責任分散到服務團隊。&lt;/li>
&lt;li>chaos engineering adoption 讓平台能力和演練節奏一起成熟。&lt;/li>
&lt;li>streaming resilience 讓高頻寬服務的失敗模式能被平台化管理。&lt;/li>
&lt;li>service catalog 讓可靠性資訊跟服務拓撲一起被看見。&lt;/li>
&lt;li>fleet management 讓大規模機器與服務狀態保持一致。&lt;/li>
&lt;li>catalog-driven ops 讓平台資訊成為日常營運入口。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">SP1&lt;/a>&lt;/td>
 &lt;td>平台工程與可靠性契約&lt;/td>
 &lt;td>讓分散團隊共用可靠性基線與交付契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/" data-link-title="Spotify：Backstage Service Catalog 與 Reliability Metadata" data-link-desc="用 service catalog 治理分散團隊的可靠性資訊：ownership、SLO 狀態、依賴圖與 runbook 的單一入口。">SP2&lt;/a>&lt;/td>
 &lt;td>Backstage Service Catalog 與 Reliability Metadata&lt;/td>
 &lt;td>用 service catalog 治理分散團隊的可靠性資訊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.atspotify.com/about/">About | Spotify Engineering&lt;/a>：Spotify Engineering 與 Backstage 的官方入口。&lt;/li>
&lt;li>&lt;a href="https://backstage.io/blog/2020/03/16/announcing-backstage">Announcing Backstage&lt;/a>：Backstage 的開源宣布與背景。&lt;/li>
&lt;li>&lt;a href="https://backstage.io/docs/overview/technical-overview">Technical overview&lt;/a>：Backstage 的技術總覽與 catalog/portal 說明。&lt;/li>
&lt;li>&lt;a href="https://engineering.atspotify.com/2023/05/fleet-management-at-spotify-part-2-the-path-to-declarative-infrastructure/">Fleet Management at Spotify (Part 2): The Path to Declarative Infrastructure&lt;/a>：大規模 fleet 與控制面的治理。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Spotify 是音樂串流平台、squad-based 組織模型對 SRE 實踐有特殊影響、chaos engineering 文章是 mid-size company 採用 chaos 的代表。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Squad-based SRE：分散式組織下的可靠性責任分配</li>
<li>Backstage：開源開發者平台的可靠性整合</li>
<li>Chaos engineering 採用過程：從 zero 到 mature 的實踐軌跡</li>
<li>Streaming infrastructure：高頻寬媒體的可靠性挑戰</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backstage</td>
          <td>service catalog + reliability metadata</td>
      </tr>
      <tr>
          <td>Squad SRE</td>
          <td>分散組織的可靠性責任</td>
      </tr>
      <tr>
          <td>Chaos Engineering Adoption</td>
          <td>Spotify 的 chaos 起步歷程</td>
      </tr>
      <tr>
          <td>CDN / Streaming Resilience</td>
          <td>媒體串流的失敗模式</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Spotify 這個案例在講的是平台工程如何把可靠性散到每個 squad，又把共同能力集中到 Backstage 這類基礎設施。讀者先抓 squad-based SRE、service catalog 與 declarative infrastructure 的關係，再看它們怎麼支撐大型串流平台。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當組織採用分散責任模型時，可靠性不再只靠中央團隊，而是靠平台把常見能力做成共同元件。當 fleet 或 streaming 基礎設施需要治理時，重點是 catalog 與 control plane 是否讓團隊看得到、管得動。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否把 service catalog 跟 reliability metadata 接起來</li>
<li>能否說清楚 squad 與平台各自負責什麼</li>
<li>能否用 declarative infrastructure 管 fleet 變化</li>
<li>能否在 chaos 採用時保住平台一致性</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Spotify 的重點是把可靠性做成平台能力，這和 LinkedIn 的 operability、Honeycomb 的 observability、Meta 的 control plane 治理屬於相近抽象層。不同的是 Spotify 更強調組織分工，所以很適合拿來說明平台如何支撐分散團隊。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>Backstage 將 service catalog 與 reliability metadata 整合成平台入口。</li>
<li>declarative infrastructure 讓 fleet 管理變成可重現的控制流程。</li>
<li>squad-based SRE 讓責任分散到服務團隊。</li>
<li>chaos engineering adoption 讓平台能力和演練節奏一起成熟。</li>
<li>streaming resilience 讓高頻寬服務的失敗模式能被平台化管理。</li>
<li>service catalog 讓可靠性資訊跟服務拓撲一起被看見。</li>
<li>fleet management 讓大規模機器與服務狀態保持一致。</li>
<li>catalog-driven ops 讓平台資訊成為日常營運入口。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">SP1</a></td>
          <td>平台工程與可靠性契約</td>
          <td>讓分散團隊共用可靠性基線與交付契約</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/" data-link-title="Spotify：Backstage Service Catalog 與 Reliability Metadata" data-link-desc="用 service catalog 治理分散團隊的可靠性資訊：ownership、SLO 狀態、依賴圖與 runbook 的單一入口。">SP2</a></td>
          <td>Backstage Service Catalog 與 Reliability Metadata</td>
          <td>用 service catalog 治理分散團隊的可靠性資訊</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.atspotify.com/about/">About | Spotify Engineering</a>：Spotify Engineering 與 Backstage 的官方入口。</li>
<li><a href="https://backstage.io/blog/2020/03/16/announcing-backstage">Announcing Backstage</a>：Backstage 的開源宣布與背景。</li>
<li><a href="https://backstage.io/docs/overview/technical-overview">Technical overview</a>：Backstage 的技術總覽與 catalog/portal 說明。</li>
<li><a href="https://engineering.atspotify.com/2023/05/fleet-management-at-spotify-part-2-the-path-to-declarative-infrastructure/">Fleet Management at Spotify (Part 2): The Path to Declarative Infrastructure</a>：大規模 fleet 與控制面的治理。</li>
</ul>
]]></content:encoded></item><item><title>自管 Redis / Valkey → AWS ElastiCache：engine 不變、變的是誰運維</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（source、自管）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>（target、managed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 對映 &lt;strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid&lt;/strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維&lt;/h2>
&lt;p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。&lt;/p>
&lt;p>那遷的是什麼？&lt;strong>運維責任的歸屬&lt;/strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚&lt;strong>交出運維的同時、交出了哪些控制權&lt;/strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。&lt;/p>
&lt;p>這對映 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論&lt;/a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&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>Schema / API&lt;/td>
 &lt;td>同 engine（Redis/Valkey）、RESP 一致、命令一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Operational model&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>自管 → AWS managed（failover/patch/snapshot）&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同 engine）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client 加 reconnect / TLS、其餘不動&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>cache 可重建（re-warm）或 RDB seed / online 複製&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>唯一 High 是 operational model，對映 &lt;strong>Type C operational hybrid&lt;/strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（source、自管）跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（target、managed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 對映 <strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid</strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維</h2>
<p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。</p>
<p>那遷的是什麼？<strong>運維責任的歸屬</strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚<strong>交出運維的同時、交出了哪些控制權</strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。</p>
<p>這對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 engine（Redis/Valkey）、RESP 一致、命令一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Operational model</strong></td>
          <td><strong>自管 → AWS managed（failover/patch/snapshot）</strong></td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同 engine）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client 加 reconnect / TLS、其餘不動</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>cache 可重建（re-warm）或 RDB seed / online 複製</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>唯一 High 是 operational model，對映 <strong>Type C operational hybrid</strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。</p>
<h2 id="operational-auditcutover-前要盤點的">operational audit：cutover 前要盤點的</h2>
<p>ElastiCache 把運維接走，但也劃下新的邊界。cutover 前必盤：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>自管時的負責項</th>
          <th>ElastiCache 後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署 / patch</td>
          <td>自己裝、自己升級</td>
          <td>AWS 管（失去任意版本控制、跟 AWS 的 engine 版本走）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自己設 Sentinel / 手動切</td>
          <td>Multi-AZ 自動（需確保 client 會重連）</td>
      </tr>
      <tr>
          <td>config</td>
          <td>改任意 redis.conf</td>
          <td>只能改 parameter group 開放的項（部分鎖死）</td>
      </tr>
      <tr>
          <td>網路存取</td>
          <td>自己的網路</td>
          <td>只在 VPC 內可達、要設 subnet group / Security Group</td>
      </tr>
      <tr>
          <td>認證</td>
          <td>AUTH password / 自管 TLS</td>
          <td>IAM auth（Redis 7+）/ ElastiCache 管的 TLS</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>自己的 Prometheus 等</td>
          <td>CloudWatch（指標名與自管不同、dashboard 要改）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：(1) 目前改了哪些 redis.conf 項、ElastiCache parameter group 是否支援；(2) client 是否有 failover reconnect 邏輯（managed failover 不會代為重連）；(3) 監控要從自管工具搬到 CloudWatch。這三項是 Type C 的核心工作。詳細的 managed 責任邊界見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a>。</p>
<h2 id="cutover資料連續性的兩條路">cutover：資料連續性的兩條路</h2>
<p>因為 engine/API 不變，cutover 接近 drop-in（換 endpoint）。資料連續性有兩條路：</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">路徑 A：re-warm（cache 可重建、最簡單）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  1. 建 ElastiCache cluster（空的、選 Valkey / Redis engine、設 parameter group）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  2. application 雙寫（自管 + ElastiCache）、讀仍走自管
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  3. 讀切到 ElastiCache endpoint、cache miss 回源 warm up
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  4. 命中率追上 → 停寫自管 → 下線自管
</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">路徑 B：RDB seed（要 cache 連續性、避免 warm-up origin 衝擊）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  1. 自管端 BGSAVE 產生 RDB
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  2. RDB 上傳 S3、ElastiCache 從 S3 seed 建 cluster（依官方 restore 流程）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  3. application 換 endpoint cutover
</span></span><span class="line"><span class="ln">11</span><span class="cl">  （ElastiCache 也提供 self-managed Redis online migration、見官方文件）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>純 cache、能接受短暫 warm-up → 路徑 A（最簡單、無資料遷移）</li>
<li>大 dataset、warm-up 會打爆 origin → 路徑 B（RDB seed 保連續性）</li>
<li>AWS CLI 建 cluster 與 restore 細節依 <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/">ElastiCache 官方文件</a>（未本機驗證）</li>
<li>engine 選 Valkey（AWS default、約低 Redis 20%）除非有 Redis 商業 module 依賴</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1parameter-group-不支援自管時改的-config">Case 1：parameter group 不支援自管時改的 config</h3>
<p><strong>徵兆</strong>：自管時改了某個 redis.conf 項（例如特定 <code>client-output-buffer-limit</code> 或某個進階參數），遷到 ElastiCache 後該設定無法套用或行為不同。</p>
<p><strong>根因</strong>：ElastiCache 只允許改 parameter group 開放的項，部分 config 被 AWS 鎖死（為了 managed 穩定性）。自管時的任意 config 自由度在 managed 後收窄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 列出自管端所有非預設 config，逐項對照 ElastiCache parameter group 支援度</li>
<li>不支援的項要評估影響——有些是 AWS 已用更好的方式處理、有些要調整 application 適應</li>
<li>把這個盤點放在 operational audit（cutover 前），不要遷完才發現</li>
<li>高度依賴特殊 config 調校的場景，managed 可能不適合、留自管</li>
</ol>
<h3 id="case-2failover-後-client-不重連managed-不代為重連">Case 2：failover 後 client 不重連（managed 不代為重連）</h3>
<p><strong>徵兆</strong>：ElastiCache Multi-AZ failover 完成，但 application 持續連舊 primary、寫入失敗。</p>
<p><strong>根因</strong>：ElastiCache 接走了 failover（自動晉升 replica），但 application 的 client 重連仍是 application 端的責任——這是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a> 的核心：AWS 換 primary，client 要自己跟上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 連 primary endpoint（會跟著 failover 更新 DNS）、不寫死 node IP</li>
<li>client 設合理 socket timeout + retry + 縮短 DNS 快取</li>
<li>遷移前就驗證 client 有 failover reconnect 行為（自管 Sentinel 時可能靠不同機制）</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover 時序</a>：自管與 managed 的 failover 機制不同、client 處理要重驗</li>
</ol>
<h3 id="case-3endpoint-只在-vpc-內cutover-後連不上">Case 3：endpoint 只在 VPC 內、cutover 後連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 完全連不上 ElastiCache、連線逾時。</p>
<p><strong>根因</strong>：ElastiCache endpoint 只在 VPC 內可達、不對公網開放。Security Group 沒開 6379、subnet group 配置錯、或 application 不在同 VPC / 沒有 VPC peering，就連不上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前確認 Security Group 開 6379 給 application 的來源、subnet group 正確</li>
<li>application 不在同 VPC 要設 peering / Transit Gateway</li>
<li>從 VPC 內 EC2 先 <code>redis-cli -h &lt;endpoint&gt; ping</code> 驗證連通，再切 application</li>
<li>這是自管（自己的網路）→ managed（AWS VPC 模型）最常見的卡點</li>
</ol>
<h3 id="case-4監控斷層自管工具--cloudwatch">Case 4：監控斷層（自管工具 → CloudWatch）</h3>
<p><strong>徵兆</strong>：cutover 後原本的 Prometheus / Grafana dashboard 全空、告警失效。</p>
<p><strong>根因</strong>：自管時用 redis_exporter + Prometheus，ElastiCache 的指標在 CloudWatch、指標名與維度不同。直接搬 dashboard 不會動。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前把關鍵告警在 CloudWatch 重建（<code>DatabaseMemoryUsagePercentage</code> / <code>ReplicationLag</code> / <code>CurrConnections</code> 等）</li>
<li>要保留 Grafana 可用 CloudWatch data source 接</li>
<li>把監控遷移納入 operational audit、不要遷完才發現沒監控</li>
<li>核心指標語意相同（記憶體 / 命中 / 連線 / 複製延遲）、只是來源與命名變了</li>
</ol>
<h3 id="case-5以為-managed-就不會-oom--stampede--熱-key">Case 5：以為 managed 就不會 OOM / stampede / 熱 key</h3>
<p><strong>徵兆</strong>：遷到 ElastiCache 後仍然 OOM、cache stampede、熱 key 打爆單 shard。</p>
<p><strong>根因</strong>：ElastiCache 接走的是運維（failover/patch/snapshot），不是 cache 使用方式的問題。記憶體淘汰、stampede、熱 key、key 設計仍是 application 端的責任——managed 不等於 hands-off。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / eviction 調校仍要做（透過 parameter group 設 maxmemory-policy），見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></li>
<li>stampede / 熱 key 的 application 端防護（jitter / singleflight / 兩層 cache）照舊</li>
<li>釐清 managed 的責任邊界——左欄 AWS 管、右欄 application 端管，見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">責任邊界 deep article</a></li>
<li>遷 managed 是減運維、不是免設計</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自管 Redis / Valkey</th>
          <th>ElastiCache（managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>engine / API</td>
          <td>同（Redis / Valkey）</td>
          <td>同（Redis / Valkey engine）</td>
      </tr>
      <tr>
          <td>運維責任</td>
          <td>全部自己扛</td>
          <td>failover / patch / snapshot 交 AWS</td>
      </tr>
      <tr>
          <td>config 自由度</td>
          <td>任意 redis.conf</td>
          <td>parameter group 開放項（部分鎖死）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自設 Sentinel / Cluster</td>
          <td>Multi-AZ 自動（client 要會重連）</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>機器 + 人力運維</td>
          <td>node 費 + managed premium（省人力）</td>
      </tr>
      <tr>
          <td>控制權</td>
          <td>完全</td>
          <td>受 AWS 邊界限制</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>要極致控制 / 跨雲 / 特殊 config</td>
          <td>AWS 生態 / 要減運維 / 可預測 SLA</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：在 AWS 生態、要把運維交出去、能接受 config 自由度收窄 → 遷 ElastiCache（engine 不變、Type C 低風險）；要極致控制 / 跨雲 / 依賴特殊 config → 留自管。engine 選 Valkey 省約 20%。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>self-managed → ElastiCache 是運維轉移，它跟 managed 邊界與 engine 調校交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a></strong>：遷過去後哪些 AWS 管、哪些仍 application 端管，是這個遷移的核心後果。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover</a></strong>：自管 failover（Sentinel）→ managed failover（Multi-AZ），client 重連邏輯要重驗。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：ElastiCache default engine 是 Valkey，自管 Redis 遷 ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位（見 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey 遷移</a>）。</li>
<li><strong>跟<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 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建</a></strong>：自管 vs managed 的上層取捨見該章，本文是「決定買（managed）之後」的遷移執行。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（自管）</li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>對應 deep article：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey</a>（換授權 + 可同時轉 managed）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type C operational hybrid）</li>
</ul>
]]></content:encoded></item><item><title>0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/</guid><description>&lt;p>能力級買 vs 建的核心原則是：交付形態 gate 決定整個系統要不要自建之後、每一塊非核心能力仍各自是一次獨立的買 vs 建決策。&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 把「整包該不該自建」篩過一輪、留下決定自建核心的團隊；但自建核心不等於每塊能力都要自己寫 — 認證、搜尋、金流、Email、表單蒐集、檔案儲存、後台操作介面這些非差異化能力、預設先問「能不能買現成的」。決策單位是能力、不是系統；真實系統是逐能力混搭、核心自建、周邊外包。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、讀者能夠：&lt;/p>
&lt;ol>
&lt;li>把「買 vs 建」的判斷單位從整個系統下降到單一能力&lt;/li>
&lt;li>辨識三種外包深度（managed 基礎設施、feature SaaS、BaaS bundle）與 no-code 到 dev-tool 的服務光譜&lt;/li>
&lt;li>用 commodity、合規、長尾成本與團隊規模判斷一塊能力該買還是該建&lt;/li>
&lt;li>算清外包的隱性帳：整合接縫、鎖定、遷出代價、以及權重如何隨情境浮動&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="判讀先確認該不該讀這章">【判讀】先確認該不該讀這章&lt;/h2>
&lt;p>本章預設讀者已經過了 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21&lt;/a> 的 whole-system gate、決定自建核心。在那之前有一種讀者該先被擋下來：&lt;strong>非工程師、目的是解自己的生活痛點或做第一個 MVP 的人&lt;/strong>。對這種讀者、0.21 已經把答案給完了 — 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS&lt;/a>（Supabase、Firebase）就是對的起點、本章的逐能力拆解反而是過度工程。免費額度對個人專案通常夠用、BaaS 連後端與資料庫的串接、效能調教、資源調配一起省掉、把這些當成「之後真的撞牆再說」的問題、是這個尺度最誠實的選擇。&lt;/p>
&lt;p>常見的誤判是把選型問題問錯層。「我該選什麼資料庫」這個提問、對非工程師多半真正的答案在 0.21（這個尺度根本不必自己管資料庫）、不在本章。下表把提問者的身分對應到該回的章節：&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>非工程師、解個人痛點、第一個 MVP&lt;/td>
 &lt;td>0.21 — 用 BaaS、本章不必細讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已決定自建核心、要逐塊判斷哪些能力外包&lt;/td>
 &lt;td>本章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已長期自建、某塊外包能力撐不住要搬回自建&lt;/td>
 &lt;td>本章 §「什麼訊號指向『自建或搬離』」+ &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一列展開說明：判斷自己是不是這種讀者、看「撞牆之後誰來修」。個人專案撞到 Supabase 免費額度上限時、升級付費方案或匯出資料換平台都是幾小時的事、不需要先把架構拆乾淨。把工程資源花在「現在還沒發生的擴展問題」上、是把 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨&lt;/a> 講的機會成本花在錯的地方。確定自己是要自建核心、且周邊有多塊能力要逐一決定買或建、再往下讀。&lt;/p>
&lt;h2 id="判讀三種外包深度與-no-code-到-dev-tool-光譜">【判讀】三種外包深度與 no-code 到 dev-tool 光譜&lt;/h2>
&lt;p>外包一塊能力有深度之分、不是非黑即白的二元動作（見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">Capability Outsourcing Depth&lt;/a> 卡片）。同樣是「不自己寫」、把維運交出去跟把整塊能力連業務邏輯一起交出去、控制權與遷出代價差一個量級。下面這三層講的是把能力交給雲端託管側時、交出多少 — 自架 OSS 或買 on-prem 授權、只租控制平面的自管形態是另一條平行軸（鎖定點在運維 know-how 與授權、不在 vendor API）、不收在這三層裡。下表把三種深度分開、每種附代表服務與遷出代價：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>外包深度&lt;/th>
 &lt;th>你外包什麼、還擁有什麼&lt;/th>
 &lt;th>dev-tool 代表&lt;/th>
 &lt;th>no-code / low-code 代表&lt;/th>
 &lt;th>遷出代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>managed 基礎設施&lt;/td>
 &lt;td>外包維運、仍擁有 schema、query 與架構&lt;/td>
 &lt;td>Aurora、ElastiCache、Neon&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>低–中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>feature SaaS（能力即服務）&lt;/td>
 &lt;td>外包整塊能力與內部業務邏輯、只消費 API&lt;/td>
 &lt;td>Auth0、Algolia、Stripe、SendGrid&lt;/td>
 &lt;td>Ragic、SurveyCake、Airtable、Typeform&lt;/td>
 &lt;td>中–高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨能力 BaaS bundle&lt;/td>
 &lt;td>一個 vendor 一次給多塊能力&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>Supabase、Firebase、Amplify&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>managed 基礎設施是最淺的外包：vendor 接手備份、修補、failover、擴容、跨區複製、但 schema、query、index、資料模型還是你的。遷出時資料是標準格式、架構是自己畫的、換一家 managed PostgreSQL 主要是搬資料與改連線字串。撞牆時你改得動的邊界很寬 — 慢查詢自己優化、index 自己加、只有底層硬體與維運動作在 vendor 手上。不過「邊界很寬」有前提：受限的 serverless 或內嵌型 managed（沒給 superuser、裝不了 extension 的那種）邊界其實更窄、選之前要確認需要的控制權它給不給。&lt;/p>
&lt;p>feature SaaS 把整塊能力連同它的內部業務邏輯一起交出去、你消費的是一組 API 而不是一台機器。買 Auth0 不只是省下跑一台認證伺服器、是把「密碼雜湊策略、MFA、SSO、social login、session 管理」整套交給 vendor、你只接它的 SDK。代價是你改得動的邊界縮到 vendor 開放的擴展點為止 — 它沒給的客製、你做不到、繞過去就是在 vendor 之外再搭一層。遷出代價中到高、因為資料模型與業務規則都沿 vendor 的特性長出來。&lt;/p>
&lt;p>選 feature SaaS 真正在賭的、是它的擴展點夠不夠你長出差異化。commodity 能力的多數需求跟同業一樣、買現成就解決；會區分產品的是少數、而那少數得疊在 vendor 之上自己長 — 但要先確認那塊差異化存不存在：有些 commodity（收個款、寄封信）差異化趨近零、整塊就是純買、這條擴展點判準根本不啟動。判準只在「確實有一塊差異化要疊上去」時才是選型核心。能不能疊、看 vendor 把控制權開到哪 — 開 API 讓你改它的排序、規則、把自己的資料接進它的邏輯、差異化長得出來；只開一面設定面板、核心邏輯動不了、一撞到差異化需求就得繞到 vendor 外另建一塊補。所以選的時候除了問「它做不做這塊能力」、更關鍵的是「它讓不讓我在上面長出獨有的那部分」 — 這一題決定它能陪產品走多遠。&lt;/p></description><content:encoded><![CDATA[<p>能力級買 vs 建的核心原則是：交付形態 gate 決定整個系統要不要自建之後、每一塊非核心能力仍各自是一次獨立的買 vs 建決策。<a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 把「整包該不該自建」篩過一輪、留下決定自建核心的團隊；但自建核心不等於每塊能力都要自己寫 — 認證、搜尋、金流、Email、表單蒐集、檔案儲存、後台操作介面這些非差異化能力、預設先問「能不能買現成的」。決策單位是能力、不是系統；真實系統是逐能力混搭、核心自建、周邊外包。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、讀者能夠：</p>
<ol>
<li>把「買 vs 建」的判斷單位從整個系統下降到單一能力</li>
<li>辨識三種外包深度（managed 基礎設施、feature SaaS、BaaS bundle）與 no-code 到 dev-tool 的服務光譜</li>
<li>用 commodity、合規、長尾成本與團隊規模判斷一塊能力該買還是該建</li>
<li>算清外包的隱性帳：整合接縫、鎖定、遷出代價、以及權重如何隨情境浮動</li>
</ol>
<hr>
<h2 id="判讀先確認該不該讀這章">【判讀】先確認該不該讀這章</h2>
<p>本章預設讀者已經過了 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a> 的 whole-system gate、決定自建核心。在那之前有一種讀者該先被擋下來：<strong>非工程師、目的是解自己的生活痛點或做第一個 MVP 的人</strong>。對這種讀者、0.21 已經把答案給完了 — 用 <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a>（Supabase、Firebase）就是對的起點、本章的逐能力拆解反而是過度工程。免費額度對個人專案通常夠用、BaaS 連後端與資料庫的串接、效能調教、資源調配一起省掉、把這些當成「之後真的撞牆再說」的問題、是這個尺度最誠實的選擇。</p>
<p>常見的誤判是把選型問題問錯層。「我該選什麼資料庫」這個提問、對非工程師多半真正的答案在 0.21（這個尺度根本不必自己管資料庫）、不在本章。下表把提問者的身分對應到該回的章節：</p>
<table>
  <thead>
      <tr>
          <th>提問者情境</th>
          <th>真正該回的章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>非工程師、解個人痛點、第一個 MVP</td>
          <td>0.21 — 用 BaaS、本章不必細讀</td>
      </tr>
      <tr>
          <td>已決定自建核心、要逐塊判斷哪些能力外包</td>
          <td>本章</td>
      </tr>
      <tr>
          <td>已長期自建、某塊外包能力撐不住要搬回自建</td>
          <td>本章 §「什麼訊號指向『自建或搬離』」+ <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出</a></td>
      </tr>
  </tbody>
</table>
<p>第一列展開說明：判斷自己是不是這種讀者、看「撞牆之後誰來修」。個人專案撞到 Supabase 免費額度上限時、升級付費方案或匯出資料換平台都是幾小時的事、不需要先把架構拆乾淨。把工程資源花在「現在還沒發生的擴展問題」上、是把 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a> 講的機會成本花在錯的地方。確定自己是要自建核心、且周邊有多塊能力要逐一決定買或建、再往下讀。</p>
<h2 id="判讀三種外包深度與-no-code-到-dev-tool-光譜">【判讀】三種外包深度與 no-code 到 dev-tool 光譜</h2>
<p>外包一塊能力有深度之分、不是非黑即白的二元動作（見 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">Capability Outsourcing Depth</a> 卡片）。同樣是「不自己寫」、把維運交出去跟把整塊能力連業務邏輯一起交出去、控制權與遷出代價差一個量級。下面這三層講的是把能力交給雲端託管側時、交出多少 — 自架 OSS 或買 on-prem 授權、只租控制平面的自管形態是另一條平行軸（鎖定點在運維 know-how 與授權、不在 vendor API）、不收在這三層裡。下表把三種深度分開、每種附代表服務與遷出代價：</p>
<table>
  <thead>
      <tr>
          <th>外包深度</th>
          <th>你外包什麼、還擁有什麼</th>
          <th>dev-tool 代表</th>
          <th>no-code / low-code 代表</th>
          <th>遷出代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>managed 基礎設施</td>
          <td>外包維運、仍擁有 schema、query 與架構</td>
          <td>Aurora、ElastiCache、Neon</td>
          <td>—</td>
          <td>低–中</td>
      </tr>
      <tr>
          <td>feature SaaS（能力即服務）</td>
          <td>外包整塊能力與內部業務邏輯、只消費 API</td>
          <td>Auth0、Algolia、Stripe、SendGrid</td>
          <td>Ragic、SurveyCake、Airtable、Typeform</td>
          <td>中–高</td>
      </tr>
      <tr>
          <td>跨能力 BaaS bundle</td>
          <td>一個 vendor 一次給多塊能力</td>
          <td>—</td>
          <td>Supabase、Firebase、Amplify</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<p>managed 基礎設施是最淺的外包：vendor 接手備份、修補、failover、擴容、跨區複製、但 schema、query、index、資料模型還是你的。遷出時資料是標準格式、架構是自己畫的、換一家 managed PostgreSQL 主要是搬資料與改連線字串。撞牆時你改得動的邊界很寬 — 慢查詢自己優化、index 自己加、只有底層硬體與維運動作在 vendor 手上。不過「邊界很寬」有前提：受限的 serverless 或內嵌型 managed（沒給 superuser、裝不了 extension 的那種）邊界其實更窄、選之前要確認需要的控制權它給不給。</p>
<p>feature SaaS 把整塊能力連同它的內部業務邏輯一起交出去、你消費的是一組 API 而不是一台機器。買 Auth0 不只是省下跑一台認證伺服器、是把「密碼雜湊策略、MFA、SSO、social login、session 管理」整套交給 vendor、你只接它的 SDK。代價是你改得動的邊界縮到 vendor 開放的擴展點為止 — 它沒給的客製、你做不到、繞過去就是在 vendor 之外再搭一層。遷出代價中到高、因為資料模型與業務規則都沿 vendor 的特性長出來。</p>
<p>選 feature SaaS 真正在賭的、是它的擴展點夠不夠你長出差異化。commodity 能力的多數需求跟同業一樣、買現成就解決；會區分產品的是少數、而那少數得疊在 vendor 之上自己長 — 但要先確認那塊差異化存不存在：有些 commodity（收個款、寄封信）差異化趨近零、整塊就是純買、這條擴展點判準根本不啟動。判準只在「確實有一塊差異化要疊上去」時才是選型核心。能不能疊、看 vendor 把控制權開到哪 — 開 API 讓你改它的排序、規則、把自己的資料接進它的邏輯、差異化長得出來；只開一面設定面板、核心邏輯動不了、一撞到差異化需求就得繞到 vendor 外另建一塊補。所以選的時候除了問「它做不做這塊能力」、更關鍵的是「它讓不讓我在上面長出獨有的那部分」 — 這一題決定它能陪產品走多遠。</p>
<p>跨能力 BaaS bundle 是一個 vendor 同時提供多塊能力、用整合當賣點。它的遷出代價最高、來自多塊能力被同一套整合綁在一起、不在任何單一能力 —— 例如同一組登入身分同時管資料庫、檔案與即時訂閱的權限、搬走其中一塊就要拆開這層共用整合（見下方 bundle 專段）。</p>
<p>這三種深度橫切一條 no-code 到 dev-tool 的光譜、而光譜的兩端服務不同的人。feature SaaS 這一層特別明顯：Auth0、Algolia 是 dev-tool、要寫 code 接 API、客製空間大、但需要工程整合能力；Ragic、SurveyCake、Airtable、Typeform 是 no-code、連 schema 都不必碰、填表就能用、客製天花板通常較低、但起步門檻也低到非工程師能獨立操作。選哪一端不只看「需要哪塊能力」、更看「誰來維護它」。一個沒有工程隊的小團隊、把會員資料放 Ragic、滿意度調查放 SurveyCake、是把維護權留在能自己改的人手上；同樣的能力換成 Auth0 + 自建問卷服務、每次調整都回到工程隊列、對這種團隊反而更貴。</p>
<h2 id="觀察什麼訊號指向買這塊能力">【觀察】什麼訊號指向「買這塊能力」</h2>
<p>一塊能力該優先評估外包、訊號集中在「自建不產生競爭優勢、卻要承擔沒有上限的長尾成本」。下表列可觀察訊號與它指向的判斷：</p>
<table>
  <thead>
      <tr>
          <th>可觀察訊號</th>
          <th>指向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這塊能力同業要的都一樣、做得再好也不構成差異化</td>
          <td>commodity 能力、預設先買</td>
      </tr>
      <tr>
          <td>合規負擔重且標準化（金流的 PCI、認證的 SSO / SOC2）</td>
          <td>把合規面交給專做這件事的 vendor</td>
      </tr>
      <tr>
          <td>自建後維護成本沒有上限（投遞率、反詐欺、登入方式矩陣）</td>
          <td>長尾成本型能力、買掉長尾</td>
      </tr>
      <tr>
          <td>團隊缺這個領域的專才、或時間壓力不允許自建</td>
          <td>用 SaaS 換時間、把人力留給核心</td>
      </tr>
  </tbody>
</table>
<p>commodity 這一列是最常見的買訊號。認證、Email 投遞、金流處理、問卷蒐集、物件儲存 / 檔案 / CDN、後台操作介面（internal tooling）都落在這裡 — 每個產品要的功能幾乎一樣、自己寫一套不會讓產品更有競爭力。後台介面值得特別點出：很多團隊把「完整後台可操作」當成自建理由、但 admin panel 本身是 commodity、Supabase Studio、Retool、Appsmith 這類工具讓你連著資料庫就生出可操作的後台、把工程時間留給真正客製的業務流程。</p>
<p>自己架一台 SMTP 寄 email 看起來簡單、真正的成本藏在 deliverability — SPF、DKIM、DMARC、IP 信譽、退信處理、進垃圾桶的排查、是一條沒有終點的維護線、而 SendGrid 這類服務把這條線變成它的本業。這就是長尾成本最容易被低估的地方：金流的反詐欺、認證的 MFA 與 social login 矩陣同理 — 第一版很快、長期維護吃掉的人力沒有上限。</p>
<h2 id="觀察什麼訊號指向自建或搬離">【觀察】什麼訊號指向「自建或搬離」</h2>
<p>外包不是預設終點、有四種訊號會把一塊能力從「買」翻回「建」。這一段是對照、判斷時跟上一段的買訊號一起看、不是讓否定句主導決策：</p>
<table>
  <thead>
      <tr>
          <th>可觀察訊號</th>
          <th>指向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這塊能力正是產品的差異化核心</td>
          <td>自建、控制權要握在自己手上</td>
      </tr>
      <tr>
          <td>客製需求持續撞到 SaaS 的擴展點上限</td>
          <td>外包換來的天花板開始擋路、評估自建</td>
      </tr>
      <tr>
          <td>計費隨規模線性成長、自建的 TCO 出現交叉點</td>
          <td>成本曲線翻轉、重算自建總帳</td>
      </tr>
      <tr>
          <td>資料主權或合規要求 SaaS 無法滿足</td>
          <td>控制面深度超出外包能給的範圍</td>
      </tr>
  </tbody>
</table>
<p>會把一塊能力翻回自建的訊號裡、差異化核心是最硬的一條。一塊能力如果就是產品賣點 — 推薦引擎之於內容平台、媒合演算法之於媒合服務 — 把它外包等於把競爭力外包、再貴也該自建。但「差異化核心」是最容易拿來自我說服的標籤 — 下手自建前先用買訊號表的 commodity 判準反向驗一次：同業是不是也都這樣做、做得再好客戶會不會無感？會、它其實是偽核心、「再貴也建」不適用。其餘三列是「原本買得對、條件變了該重評」：客製撞牆、成本交叉、合規不滿足、都是把選型結論拿出來重算的 tripwire、而不是一次定生死。觸發後的搬離執行 — 並行期、回切窗口、資產盤點 — 見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>。</p>
<h2 id="判讀四個真實例子">【判讀】四個真實例子</h2>
<p>例子的責任是建判斷錨點。下面四個刻意走四種不同方向 — 黏合型、買到部分搬離、永遠買、建到改買 — 避免把「逐能力外包」講成單向的故事。</p>
<p>一個親子活動 app 的形狀最能展示「決策單位是能力」。需求包含親子活動預約、室內空間遊玩預約、親職文章分享、會員資料、滿意度調查、線上諮詢。拆開來看：會員資料與空間預約接 Ragic、滿意度調查接 SurveyCake、文章只是連結跳轉、真正需要自建的差異化只有「線上諮詢內容匯整成固定格式檔」這一塊。這個 app 的本質是整合層 — 把幾個 no-code SaaS 黏起來、自己寫的部分極小。資料庫選 Supabase 還是 Neon 在這裡幾乎是次要問題、真正的工程量在「會員資料同時存在 Ragic 跟 app、要不要同步、諮詢內容怎麼流到固定格式檔」這些接縫上。逐能力看、它的決策是「五塊買、一塊建」、不是「選一個資料庫」。</p>
<p>一個成長期的 SaaS 走的是相反路徑的前半段。它早期用 Supabase 全包上線 — Postgres、Auth、Storage、Realtime 一次到位、團隊不必碰後端。業務量上來後、資料層的 query 複雜度與成本超過 Supabase 託管 Postgres 的舒適區、團隊把資料層搬到自管 PostgreSQL、但認證留在 Supabase Auth。這是逐能力遷出的典型 — 只把撞牆的那一塊（資料層）搬走、沒撞牆的（認證）留在原地、整包搬離反而是錯誤思路。</p>
<p>一個自建電商展示「永遠買」的判斷。核心交易流程、商品、訂單、庫存全部自建、因為那是差異化所在。金流則永遠接 Stripe — PCI 合規、反詐欺、各國支付方式是 Stripe 的本業、自建金流要承擔的合規與長尾成本沒有任何回報、因為「能收錢」不構成競爭優勢。這個「永遠買」是對絕大多數團隊的預設、不是無條件鐵律 — 例外要先攔在前面：做受監管清算、金融牌照或資金存管業務的團隊、接 vendor 不會把這些合規責任接走、得回到自己業態的前提判斷、別照抄「金流永遠買」。要分清這裡「買」涵蓋的是哪一層：收單（把一筆卡片交易實際跑完、完成扣款）、卡片資料、PCI 合規、各國支付方式這層、對絕大多數團隊從第一天到規模化都是買、收單就是終點。會翻回自建的是更上層的支付編排（orchestration）。當一家公司同時接多個 PSP（payment service provider，實際完成扣款的金流商、如 Stripe、Adyen）、就需要一層編排決定每筆交易走哪家、哪家失敗換哪家重試、月底跟各家對帳。這層協調的複雜度跨多業務線後超過任何單一 vendor 能給、超大規模下才會把它拿回自己手上；但拿回的是收單之上的協調邏輯、底層的 PCI 與卡片處理仍然外包。對本章設定的讀者、金流買到收單這層就是答案、編排層的自建是規模到了才會出現的 tripwire —— 而多數產品永遠到不了那個規模、orchestration 對它們是不會觸發的分支、不是必經路徑；少數例外是高度監管或特殊清算需求的團隊、小規模就可能直接 direct acquiring（跳過 PSP 直接對接收單行）。</p>
<p>站內搜尋走的是反方向 —— 建到改買。一個內容站初期用自建 Elasticsearch、隨內容成長、維運這套搜尋（叢集調校、相關性排序、同義詞、中文斷詞）吃掉的人力越來越多、而搜尋品質始終追不上專做這件事的服務。團隊把搜尋換成 Algolia — 一塊原本自建的能力、因為長尾運維成本翻轉、改成外包。方向跟前三個都不同、但判準一致：這塊能力的維護成本有沒有回報。</p>
<h2 id="判讀跨能力-bundle-的特殊判讀">【判讀】跨能力 bundle 的特殊判讀</h2>
<p>跨能力 BaaS bundle 難放進以能力分章的結構、因為它一次交付多塊能力。Supabase 同時是 Postgres、Auth、Storage、Realtime 與 Edge Functions、橫跨本系列的 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫</a>、<a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a>、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a> 與 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a>。以能力分章的教材、放不下「一個 vendor 給五塊能力」這種形狀、所以 Supabase 在資料庫章節只能當 managed PostgreSQL 比較表裡的一行 — 本章是它在選型層的上層錨點。</p>
<p>bundle 的價值與鎖定同源、都來自整合。同一套認證身分貫穿資料庫的 row-level security、Storage 的存取控制與 Realtime 的訂閱權限、是 bundle 最大的賣點 — 一次設定、多塊能力一致生效、省掉自己接縫的工作。但這份整合反過來就是遷出阻力：搬走任何一塊、都要把它跟其他幾塊的整合關係拆開重接。bundle 的高遷出代價不在資料量、在這些被同一套整合綁住的能力之間。</p>
<p>判讀 bundle 的單位仍然是逐能力。Supabase 不是一個必須整包接受或整包拒絕的決定 — 你可以只用它的 Postgres 當 managed 基礎設施、認證自建或接 Auth0；也可以反過來、資料庫自管、只用 Supabase Auth。問「我需要哪幾塊」「這幾塊的整合值不值得換取遷出代價」、比問「要不要用 Supabase」更準。</p>
<p>這也澄清一個常被混為一談的並列：Supabase 跟 Neon 不在同一個外包深度。Neon 是 managed 基礎設施、純 serverless PostgreSQL、給你一個會自動擴縮的資料庫、其餘能力自理；Supabase 是 BaaS bundle、資料庫只是它的一塊。只需要一個資料庫、兩者都行、Neon 更輕、遷出代價更低；需要認證、儲存、即時同步一起到位、才是 Supabase 的賣點。把它們當同級選項比較、會錯估各自真正交付的範圍。</p>
<h2 id="權衡六方向成本與權重隨情境浮動">【權衡】六方向成本與權重隨情境浮動</h2>
<p>外包一塊能力的成本帳有六個方向、但這六個方向不是等權的 — 權重隨讀者與規模浮動、用同一張等權表套所有情境會把真正主導的軸攤平。先定權重、再看方向。</p>
<p>MVP 與個人專案主導的是三個方向：免費額度天花板、整合接縫工作量、長大後的鎖定。金流 / PCI 合規與流量穩定性對一個親子活動預約 MVP 近乎無關 — 沒有信用卡資料、沒有尖峰流量 — 但「合規」不能整類劃掉：這個 app 蒐集兒少個資、在多數司法管轄區（COPPA、GDPR-K、台灣個資法）反而是高敏感類別、同意機制與資料存放地點要照規矩走。把金流合規與流量這兩個方向跟其他四個並重討論、只會稀釋真正要看的「免費額度夠不夠、SaaS 黏起來累不累、以後搬不搬得動」。企業與規模化則相反、主導的是合規、流量穩定與計費的線性成長、免費額度天花板根本不在視野裡。</p>
<p>六個方向：</p>
<ol>
<li><strong>資安與合規</strong>：外包認證把身分攻擊面交給 vendor、對沒有資安人力的團隊是淨收益；代價是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 細度、資料存放地點（落在哪個國家 / 區域）與稽核深度受 vendor 限制、有合規要求的業務可能直接出局。</li>
<li><strong>流量與穩定性</strong>：SaaS 的 rate limit 與 SLA 變成你的天花板、不可協商；vendor 故障時你跟著故障 — 買一塊能力等於接受一個外部單點依賴。這個依賴的極端是 vendor 自己 sunset、倒閉或被併後關停 — 跟「你想走的鎖定」相反、是 vendor 先走、後果不可逆、選有長期生存力的 vendor 是這條的隱性成本。</li>
<li><strong>伺服器與雲端成本</strong>：計費形狀（per-MAU、per-request、per-seat、免費額度上限）決定成本曲線。個人專案看免費額度夠不夠、規模化看線性成長何時跟自建的固定成本加人力出現交叉點。</li>
<li><strong>人力與操作</strong>：外包省下維運、換來 vendor 管理 — SDK 升級、API 變更追蹤、定價政策變動的因應、都是新的操作成本、只是從機房移到供應商關係。</li>
<li><strong>機會成本</strong>：買對、省下的工程時間投到差異化；買錯、付出遷出代價加 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a>。「以後可能要客製」是最常見的偽自建理由、客製需求真的出現再遷、總成本通常低於提前自建。</li>
<li><strong>整合接縫成本</strong>：每多買一塊 feature SaaS、就多一道接縫 — 資料跨 SaaS 重複（會員同時在 Ragic 跟 app）、跨來源一致性、整合維護。買越多塊、系統的真正複雜度從「每塊能力內部」移到「能力之間的縫」。這是外包換來的隱性帳、跟「省下維運」是同一個決策的反面、評估買 vs 建時要跟省下的成本一起算。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>還沒過 whole-system gate：先讀 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>、判斷整個系統該不該自建。</li>
<li>成本曲線交叉點的算法：見 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本、風險與選型取捨</a>。</li>
<li>逐能力的 managed 選項：資料庫見 <a href="/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/" data-link-title="Managed PostgreSQL Comparison" data-link-desc="RDS PostgreSQL、Aurora PostgreSQL、Cloud SQL、Azure Database for PostgreSQL、Neon、Supabase、Crunchy Bridge 的責任邊界比較">Managed PostgreSQL 比較</a>、認證見 <a href="/blog/backend/07-security-data-protection/vendors/" data-link-title="資安與資料保護 Vendor 清單" data-link-desc="規劃身份、秘密、金鑰、入口防護、供應鏈與偵測工具的服務頁撰寫順序與教學大綱">07 資安 vendor 清單</a>、佇列見 <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>。</li>
<li>外包能力撐不住要搬回自建：執行劇本見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>。</li>
<li>BaaS 的概念背景：見 <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS 知識卡片</a>。</li>
</ul>
]]></content:encoded></item><item><title>MySQL Partitioning：partition lifecycle 五段、跟 Vitess sharding 不同的「同 instance 內水平切割」</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/partitioning/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/partitioning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>native partitioning&lt;/em> — 5 段 lifecycle + 4 種 type + 跟 Vitess sharding / PG partitioning 對比。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="partition-lifecycle-五段">Partition lifecycle 五段&lt;/h2>
&lt;p>MySQL native partitioning 是 &lt;em>同 instance 內把一個邏輯 table 拆成多個 physical sub-table&lt;/em>、optimizer 可選擇只 scan 相關 partition。整個 partition lifecycle 5 段：&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">Design 決定 partition key / type / 數量
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Create CREATE TABLE ... PARTITION BY ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">Query WHERE clause + partition pruning
&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">Maintenance ADD / DROP / REORGANIZE / EXCHANGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">Drop 整個 partition 一次刪（比 DELETE FROM 快 1000x）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每段都有獨立工程決策。設計階段選錯 partition key、後續 maintenance + query 全部 broken。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding&lt;/a> 對比：&lt;/p>
&lt;ul>
&lt;li>&lt;em>MySQL partitioning&lt;/em>：同 instance、optimizer 自動 pruning、無 cross-instance network cost&lt;/li>
&lt;li>&lt;em>Vitess sharding&lt;/em>：跨 instance、application 透過 VTGate routing、可線性 scale&lt;/li>
&lt;/ul>
&lt;p>兩者不衝突、可組合：Vitess shard 內部 &lt;em>再&lt;/em> 用 MySQL partition（例如：shard 切 16 個、每個 shard 的 table 再按月份 partition）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>native partitioning</em> — 5 段 lifecycle + 4 種 type + 跟 Vitess sharding / PG partitioning 對比。</p></blockquote>
<hr>
<h2 id="partition-lifecycle-五段">Partition lifecycle 五段</h2>
<p>MySQL native partitioning 是 <em>同 instance 內把一個邏輯 table 拆成多個 physical sub-table</em>、optimizer 可選擇只 scan 相關 partition。整個 partition lifecycle 5 段：</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">Design       決定 partition key / type / 數量
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">Create       CREATE TABLE ... PARTITION BY ...
</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">Query        WHERE clause + partition pruning
</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">Maintenance  ADD / DROP / REORGANIZE / EXCHANGE
</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">Drop         整個 partition 一次刪（比 DELETE FROM 快 1000x）</span></span></code></pre></div><p>每段都有獨立工程決策。設計階段選錯 partition key、後續 maintenance + query 全部 broken。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding</a> 對比：</p>
<ul>
<li><em>MySQL partitioning</em>：同 instance、optimizer 自動 pruning、無 cross-instance network cost</li>
<li><em>Vitess sharding</em>：跨 instance、application 透過 VTGate routing、可線性 scale</li>
</ul>
<p>兩者不衝突、可組合：Vitess shard 內部 <em>再</em> 用 MySQL partition（例如：shard 切 16 個、每個 shard 的 table 再按月份 partition）。</p>
<h2 id="4-種-partition-type">4 種 partition type</h2>
<h3 id="range-partitioning--連續區間切割">RANGE partitioning — 連續區間切割</h3>
<p>最常見、適合 time-series / 連續數字：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="n">DATETIME</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">              </span><span class="c1">-- PK 必須含 partition key
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="n">created_at</span><span class="p">))</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202601</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-02-01&#39;</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202602</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-03-01&#39;</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202603</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-04-01&#39;</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_future</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="k">MAXVALUE</span><span class="w">  </span><span class="c1">-- 未來資料 fallback
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre></div><p>優點：</p>
<ul>
<li>Partition pruning 高效（時間 range query）</li>
<li>整個月 archive 直接 <code>ALTER TABLE orders DROP PARTITION p202601</code>、毫秒級</li>
</ul>
<p>缺點：</p>
<ul>
<li>必須 <em>預先建</em> 未來 partition（或用 <code>p_future</code> fallback、但 fallback partition 變大就失去 pruning 意義）</li>
<li><em>Hot partition</em> — 最新 partition 接收所有 INSERT、其他 partition 純歷史</li>
</ul>
<h3 id="list-partitioning--離散值切割">LIST partitioning — 離散值切割</h3>
<p>適合 enum-like value：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">name</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">100</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">region</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">LIST</span><span class="w"> </span><span class="n">COLUMNS</span><span class="w"> </span><span class="p">(</span><span class="n">region</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_asia</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;TW&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;JP&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;KR&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;CN&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_americas</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;US&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;CA&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;BR&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_emea</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;GB&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;DE&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;FR&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;IT&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>優點：對 enum-like value 直接命中、pruning 簡單。</p>
<p>缺點：value list 不能變更（不 supported <code>ALTER PARTITION ADD VALUE</code>）、新國家代碼必須 REORGANIZE。</p>
<h3 id="hash-partitioning--均勻分布">HASH partitioning — 均勻分布</h3>
<p>對 numeric / string column 取 hash、均勻分布：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">event_type</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">50</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">HASH</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="n">PARTITIONS</span><span class="w"> </span><span class="mi">8</span><span class="p">;</span></span></span></code></pre></div><p>優點：均勻分布、沒有 hot partition。</p>
<p>缺點：</p>
<ul>
<li><em>Range query 沒效</em> — <code>WHERE user_id BETWEEN 100 AND 200</code> 不能 pruning、scan 全部 partition</li>
<li>Partition 數量改變需要 REORGANIZE 整張表</li>
</ul>
<h3 id="key-partitioning--mysql-內部-hash">KEY partitioning — MySQL 內部 hash</h3>
<p>跟 HASH 類似、但用 MySQL 內部 hash function（不依賴 column 是否 integer）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sessions</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">session_id</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="k">data</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="n">PARTITIONS</span><span class="w"> </span><span class="mi">16</span><span class="p">;</span></span></span></code></pre></div><p>用於 <em>string column 或 composite column</em> 的均勻分布。一般場景跟 HASH 效果接近。</p>
<h3 id="sub-partitioning--兩層切割">Sub-partitioning — 兩層切割</h3>
<p>RANGE + HASH 組合、深化分隔：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">big_events</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="n">DATETIME</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="n">created_at</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="n">SUBPARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">HASH</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="n">SUBPARTITIONS</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202601</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-02-01&#39;</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202602</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-03-01&#39;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>每個 RANGE partition 又拆 4 個 HASH sub-partition、共 8 個 physical storage location。適合 <em>時間 range + user_id hash</em> 兩維度。</p>
<p>實務罕用、複雜性高、調 query plan 困難。多數 case 用 single-level partition 即可。</p>
<h2 id="partition-pruning--optimizer-怎麼選-partition">Partition Pruning — Optimizer 怎麼選 partition</h2>
<p><code>EXPLAIN PARTITIONS SELECT ...</code> 顯示 query 命中哪些 partition：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">PARTITIONS</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;2026-02-15&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;2026-02-20&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w">  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">p202602</span><span class="w">    </span><span class="o">|</span><span class="w"> </span><span class="n">range</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+</span></span></span></code></pre></div><p>只命中 <code>p202602</code>、其他 partition 不 scan。</p>
<p><strong>Pruning 失效場景</strong>：</p>
<ol>
<li>
<p><strong>Function on partition key</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">YEAR</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2026</span><span class="w">  </span><span class="c1">-- 沒 pruning、scan 全部</span></span></span></code></pre></div><p>應該寫成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-01-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2027-01-01&#39;</span></span></span></code></pre></div></li>
<li>
<p><strong>Implicit conversion</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;2026-02-15&#39;</span><span class="w">  </span><span class="c1">-- 字串 vs DATETIME、可能失效</span></span></span></code></pre></div><p>應該：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="w"> </span><span class="s1">&#39;2026-02-15 00:00:00&#39;</span></span></span></code></pre></div></li>
<li>
<p><strong>OR 跨 partition</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;2026-02-15&#39;</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w">  </span><span class="c1">-- partition + non-partition column OR、scan 全部</span></span></span></code></pre></div></li>
<li>
<p><strong>JOIN 不直接 filter partition key</strong>：JOIN 條件不含 partition key、optimizer 估計無法 pruning。</p>
</li>
</ol>
<h2 id="partition-maintenance--add--drop--reorganize--exchange">Partition Maintenance — ADD / DROP / REORGANIZE / EXCHANGE</h2>
<h3 id="add-partition">ADD partition</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202604</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-05-01&#39;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>對 RANGE 簡單、但要 <em>排在 MAXVALUE partition 之前</em>（如果有 <code>p_future</code>、要先 REORGANIZE）。</p>
<h3 id="drop-partition">DROP partition</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">DROP</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202601</span><span class="p">;</span></span></span></code></pre></div><p>直接刪 partition file、毫秒級完成。是 <em>time-series archive 的最大優勢</em> — 對比 <code>DELETE FROM orders WHERE created_at &lt; '...'</code> 跑 hours。</p>
<h3 id="reorganize-partition">REORGANIZE partition</h3>
<p>切分 / 合併 partition：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 切：把 p_future 切成 p202604 + new p_future
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">REORGANIZE</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_future</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202604</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="s1">&#39;2026-05-01&#39;</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p_future</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">LESS</span><span class="w"> </span><span class="k">THAN</span><span class="w"> </span><span class="k">MAXVALUE</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>REORGANIZE <em>rewrites partition data</em>、跟 OSC 一樣慢、大 partition 走 gh-ost / pt-osc 模擬（用 ghost table）。</p>
<h3 id="exchange-partition">EXCHANGE partition</h3>
<p>把 partition 跟 <em>獨立 table</em> swap（不複製資料）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 建一個 staging table 跟 partition 同 schema
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_staging</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_staging</span><span class="w"> </span><span class="n">REMOVE</span><span class="w"> </span><span class="n">PARTITIONING</span><span class="p">;</span><span class="w">  </span><span class="c1">-- staging 必須是 non-partitioned
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 把 archive partition 的資料 atomic swap 給 staging
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">EXCHANGE</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">p202601</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_staging</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- 現在 orders_staging 有 p202601 的資料、orders 的 p202601 變空
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">-- 可以 dump staging 到 S3、或 INSERT 進 archive DB</span></span></span></code></pre></div><p><code>EXCHANGE PARTITION</code> 是 <em>metadata operation</em>、毫秒級完成、不複製資料。Time-series archive 工作流的核心工具。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-pk-必須含-partition-key--schema-設計受限">1. PK 必須含 partition key — Schema 設計受限</h3>
<p>MySQL partition 規則：<strong>PK 必須包含所有 partition key column</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 錯：PK 沒包含 partition key
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">  </span><span class="c1">-- 只有 id
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="n">DATETIME</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="n">created_at</span><span class="p">))</span><span class="w"> </span><span class="p">(...);</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- ERROR 1503: A PRIMARY KEY must include all columns in the table&#39;s partitioning function</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 對：PK 包含 partition key
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="n">DATETIME</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">  </span><span class="c1">-- 兩 column 都進 PK
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">TO_DAYS</span><span class="p">(</span><span class="n">created_at</span><span class="p">))</span><span class="w"> </span><span class="p">(...);</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>接受 PK 是 composite（id + partition_key column）</li>
<li>AUTO_INCREMENT 仍 work、但 INSERT 必須給定 created_at</li>
<li><em>Unique constraint 也受影響</em> — 所有 UNIQUE index 必須含 partition key</li>
</ul>
<p>對 application：原本 <code>WHERE id = X</code> 仍 work、但慢（沒 partition pruning）、必須 <code>WHERE id = X AND created_at &gt;= ...</code> 才高效。</p>
<h3 id="2-global-index-沒原生支援">2. Global index 沒原生支援</h3>
<p>MySQL partitioning <em>沒 global secondary index</em>（PG 有）。每個 partition 各自有自己的 local index、跨 partition 的 unique constraint 必須 <em>包含 partition key</em>。</p>
<p>例：希望 <code>user_id</code> 全表 unique、但 partition by <code>created_at</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- MySQL 不允許這樣 — UNIQUE 必須含 created_at
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="w"> </span><span class="nb">BIGINT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="n">DATETIME</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="k">UNIQUE</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">  </span><span class="c1">-- 必須含 created_at、不是純 user_id
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre></div><p>對 application：跨 partition 的 unique 需要 <em>application 層處理</em>（INSERT 前 SELECT 檢查）或改用 Vitess <code>lookup_hash</code> Vindex。</p>
<h3 id="3-exchange-partition--schema-必須完全一致">3. EXCHANGE partition — schema 必須完全一致</h3>
<p>EXCHANGE 失敗常見：staging table 跟 partition 的 <em>index / column 順序差一個</em>、<code>ERROR 1736: Tables have different definitions</code>。</p>
<p>修法：</p>
<ul>
<li>建 staging 用 <code>CREATE TABLE staging LIKE orders</code> 而非手寫</li>
<li><code>REMOVE PARTITIONING</code> 後立即 verify schema</li>
<li>跑 OSC 改 schema 時、partition + staging table 同時改、不能漏一個</li>
</ul>
<h3 id="4-orphan-partition--future-partition-預先建忘記延展">4. Orphan partition — Future partition 預先建忘記延展</h3>
<p>部署 cron 每月建下個月 partition、cron 失敗 / pause、下個月 INSERT 無對應 partition、寫入 <code>p_future</code>。<code>p_future</code> 一年累積後變超大、partition pruning 沒效、查最近資料 scan 全表。</p>
<p>修法：</p>
<ul>
<li>監控 <code>p_future</code> partition size、超過 threshold alert</li>
<li>Cron 失敗 alert（不是 silent fail）</li>
<li>不依賴 cron、改成 <em>application 層在 INSERT 前 ensure partition exists</em>（lazy create）</li>
</ul>
<h3 id="5-cross-partition-query-慢">5. Cross-partition query 慢</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</span><span class="p">;</span></span></span></code></pre></div><p>沒 partition key filter、optimizer 不能 pruning、scan 全部 partition。比 <em>single big table without partition</em> 還慢（因為跨 partition aggregation overhead）。</p>
<p>修法：</p>
<ul>
<li>接受 partition 不是 <em>讀效能</em> 工具、是 <em>write + archive 效能</em> 工具</li>
<li>跨 partition aggregation 改 <em>materialized aggregation table</em>（trigger / scheduled job 維護）</li>
<li>跨 partition reporting 改丟 OLAP DB（BigQuery / Snowflake / ClickHouse）</li>
</ul>
<h2 id="跟-vitess-sharding-對比">跟 Vitess sharding 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL partitioning</th>
          <th>Vitess sharding</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>切割範圍</td>
          <td>同 instance 內</td>
          <td>跨 instance（無上限）</td>
      </tr>
      <tr>
          <td>Cross-shard query</td>
          <td>不適用</td>
          <td>VTGate 自動 split + aggregate</td>
      </tr>
      <tr>
          <td>Resharding</td>
          <td>REORGANIZE（rewrite data）</td>
          <td>VReplication 自動</td>
      </tr>
      <tr>
          <td>Operational cost</td>
          <td>低（單 instance 內）</td>
          <td>高（4 component Vitess stack）</td>
      </tr>
      <tr>
          <td>可線性 scale write</td>
          <td>否（單 instance 寫吞吐限）</td>
          <td>是（加 shard）</td>
      </tr>
      <tr>
          <td>Archive 效率</td>
          <td>DROP PARTITION 毫秒級</td>
          <td>不是 archive 工具</td>
      </tr>
  </tbody>
</table>
<p>兩者不衝突、適用不同問題。Partitioning 解決 <em>單 instance archive + write 集中</em>、sharding 解決 <em>跨 instance scale</em>。</p>
<h2 id="跟-postgresql-declarative-partitioning-對比">跟 PostgreSQL declarative-partitioning 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL partitioning</th>
          <th>PostgreSQL declarative-partitioning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition type</td>
          <td>RANGE / LIST / HASH / KEY</td>
          <td>RANGE / LIST / HASH</td>
      </tr>
      <tr>
          <td>Sub-partitioning</td>
          <td>RANGE + HASH</td>
          <td>多層 nested 支援更廣</td>
      </tr>
      <tr>
          <td>Global index</td>
          <td>無</td>
          <td>PG 11+ 有</td>
      </tr>
      <tr>
          <td>Partition wise join</td>
          <td>受限</td>
          <td>PG 11+ 強</td>
      </tr>
      <tr>
          <td>Cross-partition unique</td>
          <td>必須含 partition key</td>
          <td>PG 11+ 同限制、但 PG 17+ 部分解除</td>
      </tr>
      <tr>
          <td>Partition attach</td>
          <td>EXCHANGE PARTITION</td>
          <td>ATTACH PARTITION</td>
      </tr>
      <tr>
          <td>操作工具</td>
          <td>gh-ost / pt-osc 對 partition</td>
          <td>pg_partman（成熟）</td>
      </tr>
      <tr>
          <td>Production maturity</td>
          <td>中（5.x 開始有、8.0 強化）</td>
          <td>高（11+ declarative 後成熟）</td>
      </tr>
  </tbody>
</table>
<p>PG partitioning 對 <em>跨 partition unique</em> 跟 <em>partition-wise join</em> 處理較好、是 reporting workload 的優勢。MySQL partitioning 對 <em>archive workflow</em>（DROP / EXCHANGE）較成熟。詳見 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">PostgreSQL Declarative Partitioning</a>。</p>
<h2 id="何時用-native-partitioning">何時用 native partitioning</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Time-series workload + archive needs（log / event / order history）</td>
          <td>用 RANGE</td>
      </tr>
      <tr>
          <td>大表 &gt; 1 TB 且 query 多有 time filter</td>
          <td>用 RANGE 加速 prune</td>
      </tr>
      <tr>
          <td>跨 region / 跨業務切分</td>
          <td>用 LIST</td>
      </tr>
      <tr>
          <td>需要 <em>線性 scale write throughput</em></td>
          <td>不用 partition、用 Vitess sharding</td>
      </tr>
      <tr>
          <td>需要 <em>全表 unique constraint</em></td>
          <td>不用 partition、影響太大</td>
      </tr>
      <tr>
          <td>主要做 ad-hoc analytical query</td>
          <td>不用 partition、OLAP DB（ClickHouse / BigQuery）</td>
      </tr>
      <tr>
          <td>小表 &lt; 100 GB</td>
          <td>不必 partition、index 夠用</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-online-schema-change">跟 Online Schema Change</h3>
<p>對 partitioned table 的 schema change（ALTER COLUMN）必須 <em>每個 partition 都改</em>。gh-ost / pt-osc 對 partitioned table 仍 work、但複雜性增加。詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-vitess">跟 Vitess</h3>
<p>Vitess shard 內部可再 partition、單 shard 對應一個 MySQL instance、partition 是 instance 內優化。Vitess <code>vtctldclient PartitionTablet</code> 命令處理 shard-aware partition 操作。詳見 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p>每個 partition 是獨立 InnoDB tablespace（<code>innodb_file_per_table=ON</code> 預設）、buffer pool 內 cache 行為跟 single big table 不同。Partition 多時 buffer pool warm-up 時間更長。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-replication">跟 Replication</h3>
<p>Partition operation（ADD / DROP / EXCHANGE）是 DDL、走 binlog、replica apply 時可能 <em>locking issue</em>（特別是 EXCHANGE 跟 replica running query 衝突）。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p><code>EXPLAIN PARTITIONS</code> 是 partition-aware query optimization 的關鍵工具、看 query 真的命中哪些 partition。詳見 <a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">Query Optimization</a>。</p>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition 數量上限</td>
          <td>8.0 預設 8192、實務建議 &lt; 1000（管理成本上升）</td>
      </tr>
      <tr>
          <td>單 partition 大小</td>
          <td>10 GB - 100 GB（太小無 partition value、太大 prune 沒效）</td>
      </tr>
      <tr>
          <td>RANGE 時間 partition</td>
          <td>月 / 週 / 日（依資料量）</td>
      </tr>
      <tr>
          <td>HASH partition 數量</td>
          <td>通常 power of 2（8 / 16 / 32 / 64）</td>
      </tr>
      <tr>
          <td>Future partition pre-create</td>
          <td>至少 6 個月 buffer、cron 每月 add 1 個</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">MySQL Vitess sharding</a>（跨 instance 切割對比）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change</a>（partition table 的 schema change）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">MySQL Query Optimization</a>（EXPLAIN PARTITIONS）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（partition + buffer pool 互動）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">PostgreSQL Declarative Partitioning</a>（PG sibling 對比）</li>
<li><a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">Partition 卡片</a></li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/partitioning.html">MySQL Partitioning</a></li>
</ul>
]]></content:encoded></item><item><title>3.C22 Trivago：KEDA scale-to-zero by Kafka lag</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/</guid><description>&lt;p>這個案例的核心責任是說明 event-driven workload 該按 backlog 而非 resource usage scale 的設計判準。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Trivago 跨 3 個 region 跑 50+ Kafka sink service、每個 always-on 用 1 CPU + 1 GB；CPU/mem-based autoscaling 無效（sink 多為 I/O bottleneck、CPU 平坦）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>KEDA 以 consumer lag 為 scaling signal、minReplicaCount=0 達到 scale-to-zero、daily replica-hour 從 50 降到 1-2。揭露「resource usage 不等於工作量」、event-driven 場景該看 backlog signal。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Kafka 進階主題：consumer lag / autoscaling / multi-tenant 配額。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tech.trivago.com/post/2026-02-18-from-always-on-to-on-demand-scaling-kafka-sinks-with-keda">From Always-On to On-Demand: Scaling Kafka Sinks with KEDA&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 event-driven workload 該按 backlog 而非 resource usage scale 的設計判準。</p>
<h2 id="觀察">觀察</h2>
<p>Trivago 跨 3 個 region 跑 50+ Kafka sink service、每個 always-on 用 1 CPU + 1 GB；CPU/mem-based autoscaling 無效（sink 多為 I/O bottleneck、CPU 平坦）。</p>
<h2 id="判讀">判讀</h2>
<p>KEDA 以 consumer lag 為 scaling signal、minReplicaCount=0 達到 scale-to-zero、daily replica-hour 從 50 降到 1-2。揭露「resource usage 不等於工作量」、event-driven 場景該看 backlog signal。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Kafka 進階主題：consumer lag / autoscaling / multi-tenant 配額。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://tech.trivago.com/post/2026-02-18-from-always-on-to-on-demand-scaling-kafka-sinks-with-keda">From Always-On to On-Demand: Scaling Kafka Sinks with KEDA</a></li>
</ul>
]]></content:encoded></item><item><title>OPA Gatekeeper</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/</guid><description>&lt;p>OPA Gatekeeper 是 OPA 官方在 Kubernetes admission 層的落實、把 OPA 的 general-purpose policy engine 適配成 K8s-native admission controller。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &amp;#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest&lt;/a> 的差異不在「policy 能不能寫」、而在 &lt;em>對接面 + 抽象層次 + 工具鏈定位&lt;/em> — Gatekeeper 是 OPA 在 K8s admission 的 first-class 落實、ConstraintTemplate + Constraint 兩層抽象把 Rego policy 變成 K8s CRD、Audit 補位 background scan、Mutation 2024 起進 stable。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Gatekeeper 的核心定位是 &lt;em>Rego policy 在 K8s admission 層的 K8s-native 包裝&lt;/em>、不是另一個 policy engine。底層仍是 OPA、Rego 是同一套語言；上層加了兩個 K8s-specific 抽象 — &lt;em>ConstraintTemplate&lt;/em>（Rego policy + parameter schema 的 CRD 定義）跟 &lt;em>Constraint&lt;/em>（Template 的 instance、指定 match scope 與 parameter）。意義是同一份 Rego policy 寫一次、在不同 cluster / 不同 namespace 給不同 Constraint instance、不用改 Rego 本體。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a>（純 sidecar）比、Gatekeeper 走 &lt;em>K8s-native + 兩層抽象&lt;/em>、犧牲 OPA 純 sidecar 的跨平台彈性（OPA 可同時管 K8s admission + API gateway + Terraform plan）、換來 K8s 內部 CRD + RBAC + GitOps 的一致體驗。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno&lt;/a> 比、Gatekeeper 走 &lt;em>Rego DSL&lt;/em>、Kyverno 走 &lt;em>YAML pattern matching&lt;/em> — team 已投資 OPA / Rego（API gateway / Terraform 已用 Rego）就走 Gatekeeper、純 K8s shop + 沒 Rego 包袱直接用 Kyverno 較省學習成本。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &amp;#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest&lt;/a> 比、Conftest 是 &lt;em>CI-time static config check&lt;/em>、Gatekeeper 是 &lt;em>runtime admission + audit&lt;/em>、兩者互補不互斥（CI 用 Conftest 擋 PR、admission 用 Gatekeeper 擋 deploy）。&lt;/p></description><content:encoded><![CDATA[<p>OPA Gatekeeper 是 OPA 官方在 Kubernetes admission 層的落實、把 OPA 的 general-purpose policy engine 適配成 K8s-native admission controller。它跟 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> / <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> / <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 的差異不在「policy 能不能寫」、而在 <em>對接面 + 抽象層次 + 工具鏈定位</em> — Gatekeeper 是 OPA 在 K8s admission 的 first-class 落實、ConstraintTemplate + Constraint 兩層抽象把 Rego policy 變成 K8s CRD、Audit 補位 background scan、Mutation 2024 起進 stable。</p>
<h2 id="服務定位">服務定位</h2>
<p>Gatekeeper 的核心定位是 <em>Rego policy 在 K8s admission 層的 K8s-native 包裝</em>、不是另一個 policy engine。底層仍是 OPA、Rego 是同一套語言；上層加了兩個 K8s-specific 抽象 — <em>ConstraintTemplate</em>（Rego policy + parameter schema 的 CRD 定義）跟 <em>Constraint</em>（Template 的 instance、指定 match scope 與 parameter）。意義是同一份 Rego policy 寫一次、在不同 cluster / 不同 namespace 給不同 Constraint instance、不用改 Rego 本體。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>（純 sidecar）比、Gatekeeper 走 <em>K8s-native + 兩層抽象</em>、犧牲 OPA 純 sidecar 的跨平台彈性（OPA 可同時管 K8s admission + API gateway + Terraform plan）、換來 K8s 內部 CRD + RBAC + GitOps 的一致體驗。跟 <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> 比、Gatekeeper 走 <em>Rego DSL</em>、Kyverno 走 <em>YAML pattern matching</em> — team 已投資 OPA / Rego（API gateway / Terraform 已用 Rego）就走 Gatekeeper、純 K8s shop + 沒 Rego 包袱直接用 Kyverno 較省學習成本。跟 <a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 比、Conftest 是 <em>CI-time static config check</em>、Gatekeeper 是 <em>runtime admission + audit</em>、兩者互補不互斥（CI 用 Conftest 擋 PR、admission 用 Gatekeeper 擋 deploy）。</p>
<p>關鍵張力：<em>Rego 學習曲線</em> ↔ <em>跨平台 policy 一致性</em> 是 Gatekeeper 跟 Kyverno 最大的選擇分水嶺。純 K8s 場景 Kyverno YAML 寫起來快、但同樣的 image signature 規則若要在 Terraform plan / CI / admission 三處 enforce、Rego 寫一次跨三處比 YAML / Cue / Sentinel 多種語言混用乾淨。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Gatekeeper 在 cluster policy stack 中承擔哪一段（admission validation / audit / mutation）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> 純 sidecar 管非 K8s 對象、<a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a> 補 CI-time）</li>
<li>ConstraintTemplate 跟 Constraint 兩層怎麼切（Template 由 platform team 維護、Constraint 給 app team 在 namespace 內 instantiate）</li>
<li>Audit / Mutation / External Data Provider 何時開、開了之後 cost 與 failure mode</li>
<li>何時用 Gatekeeper、何時改 Kyverno 或退回純 OPA 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Gatekeeper deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>ConstraintTemplate 的 ownership</strong>：誰寫 Rego、誰 review、Template 是否走 Git（PR review + Gator CLI unit test）、是否有共用 library 避免每個 Template 重寫 K8s helper</li>
<li><strong>Audit coverage</strong>：除了 admission 攔截、Audit 是否定期 scan 已存在 resource（pre-Gatekeeper 部署的 legacy resource 違規）、<code>auditFromCache</code> 是否開、audit interval 是否合理（預設 60s、production 通常拉到 5-10min 避 API server 壓力）</li>
<li><strong>Failure mode 治理</strong>：Constraint <code>enforcementAction</code> 是 <code>deny</code> / <code>warn</code> / <code>dryrun</code>、Webhook failurePolicy 是 <code>Fail</code> / <code>Ignore</code>、<code>Fail</code> + Gatekeeper pod down 會擋全 cluster deploy</li>
<li><strong>跟 GitOps 的對接</strong>：ConstraintTemplate / Constraint 是否走 ArgoCD / Flux 部署、policy change 是否經 staging cluster 驗證、emergency exception 流程是否定義</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 在 admission 層的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>ConstraintTemplate（CT）— Rego policy + CRD 定義</strong>：CT 是 Gatekeeper 的核心抽象、由 Rego policy + parameter schema（OpenAPI v3）兩段組成。Template 寫好 apply 到 cluster 後、Gatekeeper 會生成同名 CRD（例 <code>K8sRequiredLabels</code>）、app team 就能用該 CRD 寫 Constraint。Template 由 platform team 維護、不該每個 app team 自己寫 Rego — 集中維護才能保證 helper / convention / unit test 一致。</p>
<p><strong>Constraint — Template 的 instance + match scope</strong>：Constraint 指定三件事 — <em>該套用哪個 Template</em>（kind）、<em>套用範圍</em>（match：kinds / namespaces / labelSelector / excludedNamespaces）、<em>parameter 值</em>（spec.parameters、對應 Template 的 schema）。同一個 Template 可以有多個 Constraint instance（production / staging 不同 threshold、不同 namespace 不同 required label set）。這層抽象的意義是 <em>policy logic 跟 environment-specific configuration 分開</em>。</p>
<p><strong>Audit — background scan 已存在 resource</strong>：除了 admission webhook 在 create / update 時攔、Audit controller 定期（預設 60s）掃整個 cluster 找違規 resource、結果寫到 Constraint status 的 <code>violations</code> 欄位。意義是 <em>legacy resource 在你 install Gatekeeper 之前就在那、admission 不會觸發、Audit 才會抓到</em>。<code>auditFromCache: true</code> 用 Gatekeeper 自己的 informer cache 不打 API server、適合大 cluster。</p>
<p><strong>Mutation — 2024+ stable</strong>：早期 Gatekeeper 只有 Validation、Mutation 在 v3.10+ 進 beta、2024 隨 v3.14+ 進 stable。Mutation 走獨立 CRD（<code>Assign</code> / <code>AssignMetadata</code> / <code>ModifySet</code>）、不走 ConstraintTemplate。常見用法：注入 <code>securityContext.runAsNonRoot: true</code>、補 default resource limit、加 organization label。Mutation 跟 Validation 都開的話、Mutation 先跑、Validation 看 mutated 後的結果。</p>
<p><strong>Sync Resources — cross-resource lookup</strong>：Rego policy 若要查 <em>別的 resource</em>（例：擋 Service 用了不存在的 Namespace）、要先 declare <code>Config</code> CRD 把該 resource type 加進 Gatekeeper 的 sync list、Gatekeeper 才會在 cache 裡有那個 resource 供 Rego 查。沒 sync 的 resource 不能跨 reference、是常見踩雷點。</p>
<p><strong>External Data Provider — query 外部 API 做 decision</strong>：Gatekeeper v3.10+ 引入 External Data Provider、Rego 可以 call 外部 HTTPS endpoint 取 runtime data 做 policy decision。典型用法：query image scan service（例 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> server）確認 image 沒 CVE、query SBOM attestation service 確認 supply chain 完整、query custom IAM 確認 namespace owner 有權建立該 resource。要設 timeout + cache、外部 service down 不能擋全 cluster admission。</p>
<p><strong>Gator CLI — policy unit test</strong>：Gator 是 Gatekeeper 官方 CLI、本機跑 Template + Constraint 對 mock K8s manifest、不需 cluster。CI pipeline 跑 <code>gator test</code> 對每個 Template 跑 fixture、policy change 出 PR 時自動驗證 — 避免 production deploy 才發現 Template Rego bug 擋全 cluster。</p>
<p><strong>跟 GitOps 整合</strong>：ConstraintTemplate / Constraint / Mutation / Config CRD 都是純 YAML、走 ArgoCD / Flux 部署是標準作法。實務 layout：<code>gatekeeper-system</code> namespace 裝 Gatekeeper、<code>gatekeeper-policies</code> repo 放 Template 跟 baseline Constraint（platform team owned）、各 app namespace 的 Constraint instance 可以由 app team 在自己 repo 管理（透過 ArgoCD AppProject 限制 Constraint kind）。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>OPA Gatekeeper</th>
          <th>Kyverno</th>
          <th>OPA 純 sidecar</th>
          <th>Conftest</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對接面</td>
          <td>K8s admission + Audit（K8s-only）</td>
          <td>K8s admission + Audit（K8s-only）</td>
          <td>任意 — API gateway / Terraform / K8s</td>
          <td>CI-time（static config check）</td>
      </tr>
      <tr>
          <td>Policy 語言</td>
          <td>Rego（OPA 同一套）</td>
          <td>YAML pattern matching（K8s-native）</td>
          <td>Rego</td>
          <td>Rego（OPA 同一套）</td>
      </tr>
      <tr>
          <td>抽象層次</td>
          <td>ConstraintTemplate + Constraint 兩層</td>
          <td>ClusterPolicy / Policy（單層）</td>
          <td>OPA policy bundle（無 K8s-specific 抽象）</td>
          <td>conftest test file（無 cluster 概念）</td>
      </tr>
      <tr>
          <td>Mutation</td>
          <td>支援（v3.14+ stable）</td>
          <td>支援（first-class、Kyverno 強項）</td>
          <td>不支援（需自寫 admission webhook）</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Cross-resource</td>
          <td>Sync Resources（要 declare）</td>
          <td>Context API（內建）</td>
          <td>看自己 sidecar 怎麼寫</td>
          <td>看 CI 怎麼 load</td>
      </tr>
      <tr>
          <td>外部 data</td>
          <td>External Data Provider（v3.10+）</td>
          <td>Context API（image registry / ConfigMap）</td>
          <td>看自己 sidecar 怎麼寫</td>
          <td>不適用（純 static）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>Rego 陡 + 兩層抽象多概念</td>
          <td>YAML 直觀、K8s-native idiom</td>
          <td>Rego 陡 + 自管 deployment</td>
          <td>Rego 陡 + CI integration</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>team 已投資 Rego / OPA、跨 K8s + 其他平台一致</td>
          <td>純 K8s shop、無 Rego 包袱、Mutation 是重點</td>
          <td>跨 K8s + API + Terraform 一致 policy 管理面</td>
          <td>PR 階段擋 manifest / IaC config</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — Template / Constraint / Rego 量多</td>
          <td>中 — YAML 較可移植</td>
          <td>中 — Rego 可搬到 Gatekeeper</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 Gatekeeper 的核心訴求：<em>team 已用 Rego（API gateway / Terraform plan / CI 已 OPA）+ 想把 same policy 延伸到 K8s admission + 看重 OPA ecosystem 一致性</em>。純 K8s shop 沒 Rego 包袱、又特別需要 Mutation 場景密集（PSP 廢除後重建、跨 namespace 統一 sidecar 注入）直接走 Kyverno 更省學習成本。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Rego idioms for K8s admission</strong>：K8s admission review 物件結構是 <code>input.review.object</code>、Template 的 <code>violation</code> rule 走 <code>violation[{&quot;msg&quot;: msg}] { ... }</code> 形式。常見 idiom：<code>match.kinds</code> 跟 <code>match.namespaceSelector</code> 在 Constraint 層處理 scope、Rego 內只寫 <em>policy logic</em>；K8s helper（label 取值、container loop、init container 排除）抽到 shared library Template；錯誤訊息要帶 <code>input.review.object.metadata.name</code> 幫 app team 定位是哪個 resource 被擋。</p>
<p><strong>External Data Provider 的 production 治理</strong>：Provider 是獨立 service、Gatekeeper webhook 透過 HTTPS call、cache 在 Gatekeeper 內。要設 timeout（預設 3s、過時 ConstraintTemplate <code>failurePolicy</code> 決定 fail-open / fail-closed）、cache TTL、Provider 自身的 readiness / liveness。Provider down 不該擋全 cluster — 用 <code>failurePolicy: Ignore</code> 對 External Data Provider 例外、但記錄 metric alert。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a> 的 SBOM attestation 查詢場景。</p>
<p><strong>Gator CLI 在 CI 的 pipeline 設計</strong>：<code>gator test</code> 對 fixture 跑、<code>gator verify</code> 跑 Template 自帶 test suite、<code>gator expand</code> 預覽 Mutation 結果。PR 流程：Template change → <code>gator verify</code> 跑 unit test → kind cluster 起 Gatekeeper apply Template + sample violation manifest → confirm 擋下來才 merge。</p>
<p><strong>跟 Styra DAS / Nirmata 整合</strong>：Gatekeeper OSS 本身沒 central management UI、多 cluster deployment 看 violation status 要自己拼。Styra DAS 是 OPA 商業 control plane、可以 push Template / Constraint 到多 cluster Gatekeeper、彙整 audit violation、做 policy impact analysis。Nirmata 走類似路線。OSS-only deployment 通常用 ArgoCD ApplicationSet + Prometheus exporter（gatekeeper-policy-manager / Open Policy Agent metrics）拼。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Gatekeeper webhook timeout / 擋全 cluster admission</strong>：Rego policy 寫了 expensive operation（大量 cross-resource lookup、External Data Provider call without cache）— webhook timeout 預設 3s、超過就走 failurePolicy；改寫 Rego 用 indexed lookup、External Data Provider 加 cache、<code>failurePolicy: Ignore</code> for non-critical Template</li>
<li><strong>新 Template apply 後 admission 整個壞</strong>：Rego syntax / logic bug、production 才發現 — PR 必跑 <code>gator verify</code> + staging cluster 24-48hr soak、Constraint 先用 <code>enforcementAction: dryrun</code> 觀察 violation count 才切 <code>deny</code></li>
<li><strong>Audit 跑很慢 / API server 壓力大</strong>：cluster resource 量大、Audit interval 預設 60s 太頻繁 — 拉長到 5-10min、<code>auditFromCache: true</code> 用 informer 不打 API server、大 cluster 開 <code>auditChunkSize</code> 分批處理</li>
<li><strong>legacy resource 不擋</strong>：admission webhook 只攔 create / update、<code>kubectl apply</code> 沒改動 spec 不觸發 — 用 Audit 抓 violation、配合手動 migration plan、不要期待 admission 自動修</li>
<li><strong>Mutation 跟 Validation 衝突</strong>：Mutation 加了 label、Validation 又擋說 label 不該存在 — Mutation 先跑、Validation 看 mutated 結果；設計 policy 時要對齊兩端、不能各自寫</li>
<li><strong>Sync 沒 declare、cross-resource policy 看不到對象</strong>：Rego <code>data.inventory.namespace[&quot;foo&quot;].v1.Pod</code> 回 undefined — <code>Config</code> CRD 加 sync targets、確認 Gatekeeper pod restart 後 cache 載入</li>
<li><strong>External Data Provider down 擋全 cluster</strong>：Provider service 自己掛、<code>failurePolicy: Fail</code> 整個 admission 壞 — Provider 走 <code>failurePolicy: Ignore</code> + metric alert、Provider 自身 HA 部署、cache TTL 拉長</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 K8s + 無 Rego 包袱 + Mutation 重點</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a></td>
      </tr>
      <tr>
          <td>跨 K8s + API gateway + Terraform</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>（純 sidecar）</td>
      </tr>
      <tr>
          <td>CI-time / PR 階段擋 manifest</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a></td>
      </tr>
      <tr>
          <td>Image scan 結果作為 policy 來源</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（feed External Data Provider）</td>
      </tr>
      <tr>
          <td>Runtime threat detection（syscall）</td>
          <td>Falco / Cilium Tetragon（屬 runtime detection、不在 admission 層）</td>
      </tr>
      <tr>
          <td>Multi-cluster policy 集中管理</td>
          <td>Styra DAS / Nirmata（OPA / Gatekeeper 商業 control plane）</td>
      </tr>
      <tr>
          <td>偵測 / SIEM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> 或同類 SIEM</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Rego 完整語法 reference（unification、comprehension、partial evaluation）</li>
<li>Gatekeeper helm chart / installation 細節（看官方 docs）</li>
<li>Open Policy Agent 在 service mesh / API gateway 的 sidecar 部署模式（看 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> 頁）</li>
<li>Pod Security Admission（K8s 內建、跟 Gatekeeper 互補但不是 Gatekeeper 一部分）</li>
<li>Multi-cluster policy bundle 的 OCI registry 分發（屬 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a> 邊界）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Gatekeeper 在 07 案例庫沒有直接 vendor-level 事件、但 supply chain 跟 admission policy 相關 case 都是 Gatekeeper 落實位置的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Gatekeeper 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>ConstraintTemplate 配 cosign image signature verify、擋未簽 / 簽章不符 image 進 cluster；Audit 補位掃既有 deployment 找未簽 image</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Gatekeeper External Data Provider 接 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> server、admission 階段查 image 是否有 critical CVE 直接擋</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></td>
          <td>External Data Provider 可 query SBOM attestation 服務做 policy decision、不只看 image hash 而看 component provenance 鏈</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性 (section)</a></td>
          <td>Gatekeeper 是 OPA ecosystem 在 K8s admission 的官方落實、artifact trust gate 從 CI（Conftest）延伸到 runtime（Gatekeeper）的閉環</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact Trust</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>、<a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>、<a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（image scan 結果 feed External Data Provider）、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload identity 跟 admission policy 互補）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（admission violation event 進 SIEM correlation）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（policy violation → IR routing）</li>
<li>官方：<a href="https://open-policy-agent.github.io/gatekeeper/">OPA Gatekeeper Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>AWS VPC Traffic Mirroring</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/</guid><description>&lt;p>AWS VPC Traffic Mirroring 的核心責任是在 VPC 網路層複製 ENI traffic，讓團隊用低 application 侵入方式觀察 production flow。它適合封包級診斷、網路安全分析、流量樣本收集與部分 replay 前置資料蒐集，重點在明確定義 mirror source、filter、target、加密邊界與保存責任。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>AWS VPC Traffic Mirroring 適合需要網路層能見度的 AWS workload。當 application code、service mesh 或 host capture 都不適合改動時，VPC 層 mirror 可以從 ENI 複製封包到 analysis appliance、IDS、packet capture 或自管處理服務。&lt;/p>
&lt;p>這個定位讓 AWS VPC Traffic Mirroring 接到 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 的 shadow traffic 前置觀測。它偏封包觀察與樣本收集，若要做應用層 replay、filter、rewrite 或 side effect 隔離，通常還需要 GoReplay、proxy、custom processor 或測試環境配合。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay&lt;/a> 比、VPC Traffic Mirroring 走 &lt;em>無侵入 L3 packet copy&lt;/em>、GoReplay 走 &lt;em>application-level HTTP capture / rewrite&lt;/em>；跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring&lt;/a> 比、VPC Mirror 在 ENI 層、Mesh Mirror 在 K8s pod 層；跟 AWS Network Firewall 比、Firewall 是 &lt;em>inline 阻擋&lt;/em>、Mirror 是 &lt;em>side-channel 觀察&lt;/em>、兩者目的不同但 packet path 相近。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 VPC Traffic Mirroring deployment 是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Source ENI selection&lt;/strong>：哪些 ENI 被 mirror（per-instance / per-subnet / 用 tag 自動選）、是否覆蓋瓶頸路徑上的關鍵節點（ALB target / NAT Gateway / RDS proxy / cross-AZ ENI）、漏掉哪個 ENI 就是 evidence 盲區&lt;/li>
&lt;li>&lt;strong>Filter rule 收斂&lt;/strong>：mirror filter 用 protocol / port / CIDR / direction 限定、避免「全 ENI 全 traffic」這種失控設定；filter 太寬會把 cross-AZ cost + target 處理量直接炸上去&lt;/li>
&lt;li>&lt;strong>Target NLB capacity&lt;/strong>：mirror target 是 ENI 或 NLB、target capacity（NLB flow / bandwidth）跟 source 流量比例要對得起來、target overload 會 drop 封包讓 evidence 失真&lt;/li>
&lt;li>&lt;strong>Sampling rate / packet length truncation&lt;/strong>：高流量服務不必 1:1 mirror、要設 &lt;code>packet_length&lt;/code> 截斷（只取 header）跟 mirror session ratio；忘設 sampling 等於整條 production 流量複製兩份、AWS bill 月底會出事&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 邊界的待補項目。&lt;/p></description><content:encoded><![CDATA[<p>AWS VPC Traffic Mirroring 的核心責任是在 VPC 網路層複製 ENI traffic，讓團隊用低 application 侵入方式觀察 production flow。它適合封包級診斷、網路安全分析、流量樣本收集與部分 replay 前置資料蒐集，重點在明確定義 mirror source、filter、target、加密邊界與保存責任。</p>
<h2 id="定位">定位</h2>
<p>AWS VPC Traffic Mirroring 適合需要網路層能見度的 AWS workload。當 application code、service mesh 或 host capture 都不適合改動時，VPC 層 mirror 可以從 ENI 複製封包到 analysis appliance、IDS、packet capture 或自管處理服務。</p>
<p>這個定位讓 AWS VPC Traffic Mirroring 接到 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 的 shadow traffic 前置觀測。它偏封包觀察與樣本收集，若要做應用層 replay、filter、rewrite 或 side effect 隔離，通常還需要 GoReplay、proxy、custom processor 或測試環境配合。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a> 比、VPC Traffic Mirroring 走 <em>無侵入 L3 packet copy</em>、GoReplay 走 <em>application-level HTTP capture / rewrite</em>；跟 <a href="/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring</a> 比、VPC Mirror 在 ENI 層、Mesh Mirror 在 K8s pod 層；跟 AWS Network Firewall 比、Firewall 是 <em>inline 阻擋</em>、Mirror 是 <em>side-channel 觀察</em>、兩者目的不同但 packet path 相近。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 VPC Traffic Mirroring deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Source ENI selection</strong>：哪些 ENI 被 mirror（per-instance / per-subnet / 用 tag 自動選）、是否覆蓋瓶頸路徑上的關鍵節點（ALB target / NAT Gateway / RDS proxy / cross-AZ ENI）、漏掉哪個 ENI 就是 evidence 盲區</li>
<li><strong>Filter rule 收斂</strong>：mirror filter 用 protocol / port / CIDR / direction 限定、避免「全 ENI 全 traffic」這種失控設定；filter 太寬會把 cross-AZ cost + target 處理量直接炸上去</li>
<li><strong>Target NLB capacity</strong>：mirror target 是 ENI 或 NLB、target capacity（NLB flow / bandwidth）跟 source 流量比例要對得起來、target overload 會 drop 封包讓 evidence 失真</li>
<li><strong>Sampling rate / packet length truncation</strong>：高流量服務不必 1:1 mirror、要設 <code>packet_length</code> 截斷（只取 header）跟 mirror session ratio；忘設 sampling 等於整條 production 流量複製兩份、AWS bill 月底會出事</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 邊界的待補項目。</p>
<h2 id="適用場景">適用場景</h2>
<p>網路層瓶頸定位適合 VPC Traffic Mirroring。當 latency、packet loss、TLS handshake、connection reset、NAT、load balancer 或 cross-AZ traffic 是疑點時，封包 mirror 能提供 application metrics 看不到的證據。</p>
<p>低侵入 traffic sampling 適合 VPC Traffic Mirroring。團隊可以在不改 application code 的情況下收集 production flow，作為 workload model、security analysis 或 replay pipeline 的輸入。</p>
<p>受管 AWS 網路環境適合 VPC Traffic Mirroring。當服務主要跑在 EC2 / ENI 可 mirror 的環境中，VPC 原生能力可以讓網路團隊用既有安全與觀測流程管理。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>AWS VPC Traffic Mirroring 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>網路層鏡像</td>
          <td>application 無侵入、封包級可見</td>
          <td>L7 解碼、filter、rewrite 與 replay</td>
      </tr>
      <tr>
          <td>AWS 原生</td>
          <td>VPC / ENI / filter / target 整合</td>
          <td>AWS 約束、跨帳號與跨 VPC 設計</td>
      </tr>
      <tr>
          <td>安全分析</td>
          <td>可接 IDS、packet analyzer、forensics</td>
          <td>PII / payload 保存與存取控制</td>
      </tr>
      <tr>
          <td>流量樣本</td>
          <td>可支援 workload model 校正</td>
          <td>加密 traffic 處理與樣本代表性</td>
      </tr>
  </tbody>
</table>
<p>網路層鏡像價值來自低侵入。團隊可以在不調整 application 或 service mesh 的情況下取得 flow evidence，但也要承擔 L7 語意不足的限制。</p>
<p>安全分析價值來自封包細節。對容量工程而言，封包證據能幫忙確認 connection、TLS、NAT、load balancer 與跨區流量成本；對資安而言，則能支援 IDS 與 forensic workflow。</p>
<h2 id="跟其他方式的取捨">跟其他方式的取捨</h2>
<p>AWS VPC Traffic Mirroring 和 GoReplay 的主要差異是層級。VPC mirroring 在 L3 / L4 觀察封包；GoReplay 更接近 HTTP application replay，對 request rewrite 與 target control 更直接。</p>
<p>AWS VPC Traffic Mirroring 和 service mesh mirroring 的主要差異是控制範圍。VPC mirroring 由網路層控制，適合低侵入封包觀察；service mesh mirroring 由 L7 route policy 控制，適合服務版本與 route 對照。</p>
<p>AWS VPC Traffic Mirroring 和 synthetic load test 的主要差異是用途。VPC mirroring 提供 production traffic evidence；synthetic load test 提供可控壓力。兩者常搭配：先用 mirror 校正 workload model，再用 k6 / Gatling / Locust 產生可控負載。</p>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS VPC Traffic Mirroring</th>
          <th>GoReplay</th>
          <th>Service Mesh Mirroring</th>
          <th>AWS Network Firewall</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鏡像層級</td>
          <td>L3 / L4 packet copy</td>
          <td>L7 HTTP capture + replay</td>
          <td>L7 pod-level（Istio / Linkerd）</td>
          <td>L3-L7 inline filter（非 mirror）</td>
      </tr>
      <tr>
          <td>Application 侵入</td>
          <td>無 — ENI 層、code 不改</td>
          <td>中 — 需 sidecar / capture host</td>
          <td>中 — service mesh 必須先佈</td>
          <td>無 — VPC gateway 層</td>
      </tr>
      <tr>
          <td>Replay 能力</td>
          <td>弱 — 需自接 packet replayer</td>
          <td>強 — 內建 request rewrite</td>
          <td>中 — mirror to shadow service</td>
          <td>無</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>network forensics / IDS / 容量分析</td>
          <td>HTTP regression / load replay</td>
          <td>K8s service-level shadow test</td>
          <td>inline 阻擋 / IDS / IPS</td>
      </tr>
      <tr>
          <td>加密 payload</td>
          <td>看不到 — TLS 仍密</td>
          <td>看得到 — application 解密後</td>
          <td>看得到 — mesh sidecar 已 TLS terminate</td>
          <td>partial — TLS inspection 需另設</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>per-ENI / cross-AZ traffic</td>
          <td>計算 + 儲存</td>
          <td>mesh overhead + shadow service</td>
          <td>per-GB processed</td>
      </tr>
  </tbody>
</table>
<h2 id="操作成本">操作成本</h2>
<p>AWS VPC Traffic Mirroring 的主要成本是資料治理。Mirror target 可能收到 payload、token、cookie、internal identifiers 與敏感資料，因此保存、查詢、保留期限、存取權與刪除責任要先定義。</p>
<p>網路成本來自複製 traffic。Mirror session 會增加網路流量與 target processing 成本，高流量服務要先估算 mirror ratio、filter、target capacity 與跨 AZ 費用。</p>
<p>加密成本來自 L7 可讀性。TLS traffic 在網路層 mirror 後通常仍是加密封包；若需要 application payload，要搭配解密點、proxy、key 管理或 application-level capture。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>AWS VPC Traffic Mirroring 結果應回寫到 evidence package。最小欄位包括 mirror source ENI、filter rule、mirror target、session number、time range、sampling / truncation、target capacity、payload handling、packet metrics、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>AWS VPC Traffic Mirroring 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>mirror session、filter、target config</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>mirror start / end</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>packet analyzer、flow logs、metrics link</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>filter coverage、sampling、encryption status</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>target capacity、source coverage</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>加密 payload、未 mirror ENI、L7 語意不足</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是把網路層觀察接回效能判斷。Reviewer 要能知道 mirror 覆蓋哪些 ENI、哪些封包被 filter、target 是否有 capacity，以及封包證據如何對應到 application latency 或 saturation。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Filter rule 設計</strong>：mirror filter 支援 source CIDR / dest CIDR / protocol / port range / direction（ingress / egress）、rule number 決定 evaluation 順序。production 慣例是 <em>最小覆蓋原則</em> — 先用 <code>port 443 + dest CIDR = ALB target group</code> 限定到關鍵 path、再依需要擴張。filter 寫太寬會把 control-plane heartbeat、health check、internal RPC 全部 mirror 進來、target 處理量瞬間爆掉。</p>
<p><strong>跟 IDS / packet analyzer 整合</strong>：mirror target 接 ENI 後常見的下游堆疊是 <em>Zeek</em>（前 Bro、生成 connection log / protocol log）、<em>Suricata</em>（rule-based IDS / IPS 偵測）、<em>Wireshark / tshark</em>（離線封包分析）。實務上 mirror → NLB → 自管 EC2 跑 Zeek 產 JSON log → 進 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog</a> / Splunk 做 correlation。容量工程關心 connection reset 跟 retransmit、資安關心 protocol anomaly、共用同一份 mirror feed。</p>
<p><strong>Replay 到 staging cluster</strong>：mirror feed 不能直接 replay（沒有 stateful 重組），但可以接 packet replayer（tcpreplay / GoReplay packet mode）把樣本送到 staging。要注意 <em>side effect 隔離</em> — staging 的 DB / external API 不應該真的執行寫入、否則 mirror 變成 production fanout。</p>
<p><strong>Traffic analysis platform 整合</strong>：mirror 取得的 packet evidence 通常進 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Network Performance Monitoring</a> 做 NPM dashboard、或進 Splunk Stream app 做 SIEM correlation。整合的關鍵是 <em>時間軸對齊</em> — packet timestamp、application log、metrics 三者要同步、否則 root cause 拼不回去。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Target NLB capacity 不夠 / drop packet</strong>：mirror traffic 量超過 NLB flow limit、packet 被 silently drop — 拆 mirror session 到多個 target、開 NLB flow log 看 drop reason、必要時改用 Gateway Load Balancer</li>
<li><strong>Filter rule 太寬導致流量爆</strong>：「mirror 所有 traffic」設定上線後 target ENI 跟 cross-AZ bandwidth 雙重炸 — 立刻關掉 session、改用 dest CIDR / port 收斂、加 <code>packet_length</code> 截斷只取 header</li>
<li><strong>Cross-AZ mirror cost 暴增</strong>：source ENI 跟 target 在不同 AZ、每個封包複製都收 cross-AZ traffic 費 — target NLB 部署到每個 AZ、用 AZ-affinity routing、或把 mirror target 限定在 source 同 AZ</li>
<li><strong>TLS payload 看不到</strong>：mirror 拿到加密封包、L7 內容無法分析 — 把解密點移到 ALB / NLB-TLS termination、或在 application 層加 capture（不再用 VPC mirror）</li>
<li><strong>Mirror session 漏掉新 instance</strong>：autoscaling 起新 instance 沒自動加入 mirror — 用 mirror target by tag、Terraform / CloudFormation 把 mirror session 寫進 ASC launch template</li>
<li><strong>Packet timestamp 不對齊 application log</strong>：mirror packet 時間是 source ENI capture 時間、不是 application processing 時間、做 latency 分析會偏差 — 用 packet 5-tuple + request ID 對齊 application log、不要直接相減 timestamp</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>AWS VPC Traffic Mirroring 適合回寫網路與平台層效能案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130K node GKE cluster</a> 的大規模網路觀測需求（雖在 GCP、但網路證據的層次拆解可類比）、<a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair GCP burst capacity</a> 的跨雲容量觀測、<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day readiness</a> 的 pre-event network evidence、<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 EKS cluster</a> 跨 cluster 的網路流量觀測、以及 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys DynamoDB 15-region</a> 的 99.999% 可用性下封包層 evidence 補強。</p>
<p>這些案例的重點是網路層 evidence。VPC Traffic Mirroring 頁引用案例時，要把 case 轉成 mirror source、filter、target capacity、packet metric、cross-AZ cost 與 L7 correlation — 例如 Riot Games 35ms 延遲門檻下、cross-AZ traffic mirror 本身會增加成本、必須先用 filter 收斂到關鍵 ENI。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring</a></li>
<li>知識卡：<a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li>官方：<a href="https://docs.aws.amazon.com/vpc/latest/mirroring/what-is-traffic-mirroring.html">AWS VPC Traffic Mirroring documentation</a></li>
</ul>
]]></content:encoded></item><item><title>CloudHealth</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/</guid><description>&lt;p>CloudHealth 的核心責任是把大型組織的 cloud spend、governance、policy、allocation 與 optimization workflow 放進同一個 FinOps 管理平面。它適合 account、team、business unit、provider 與採購流程複雜的組織，重點在讓成本治理、合規要求與工程 owner 能共用同一套成本事實。2018 年被 VMware 收購、2023 年隨 VMware 進入 Broadcom 旗下；現屬 Broadcom 的 enterprise FinOps 旗艦產品。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>CloudHealth 跟 AWS Cost Explorer / Azure Cost Management 那種單雲原生工具的差異在 &lt;em>跨雲一致 schema + enterprise FinOps operating model&lt;/em>、單雲帳單細節反而是原生工具更深。Cost Explorer 在 AWS-only 場景的 granularity 更深、但跨 Azure / GCP 帳單對齊、成本中心 chargeback、policy 治理就需要 CloudHealth 這類 multi-cloud platform。&lt;/p>
&lt;p>跟 Vantage 比、CloudHealth 走 &lt;em>enterprise governance-first&lt;/em>、Vantage 走 &lt;em>engineering-friendly dashboard-first&lt;/em>。Vantage 對小到中型 cloud-native 團隊更快上手、但 chargeback 流程、policy violation queue、approval workflow 都不是它的主場。跟 Apptio Cloudability（IBM 收購）比、兩者定位最接近、都吃 large enterprise FinOps 市場；CloudHealth 的差異是 VMware / Broadcom ecosystem 整合（vCenter / Tanzu / on-prem hybrid），Cloudability 強在 TBM（Technology Business Management）財務分攤模型成熟度。&lt;/p>
&lt;p>關鍵張力：&lt;em>Broadcom 收購後的 product roadmap 不確定性&lt;/em> ↔ &lt;em>enterprise FinOps ecosystem 深度&lt;/em>。Broadcom 對 VMware portfolio 的價格調整、partner 縮編、support tier 變動 2024-2025 持續發生；客戶要評估 &lt;em>退場成本（chargeback rule + tag taxonomy 量大）vs 短期 license 漲幅&lt;/em>、不是只看當下功能。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>CloudHealth 適合 enterprise FinOps 與 cloud governance。當組織需要跨 AWS、Azure、Google Cloud、Kubernetes、shared services 與成本中心建立 showback、chargeback、policy 與 optimization workflow，CloudHealth 類平台可以提供集中式成本管理與治理視角。&lt;/p>
&lt;p>這個定位讓 CloudHealth 接到三個主章。它從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 接收 cost curve 與 over-provision waste，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性&lt;/a> 接收成本 dashboard 需求，從 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因&lt;/a> 接收 owner、tag 與 attribution 規則。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>多雲成本治理是 CloudHealth 的主要入口。大型企業常有不同 cloud provider、不同採購合約、不同 account 結構與不同團隊成熟度；CloudHealth 可以把成本、資產、policy 與權限治理收斂到 FinOps 工作流程。&lt;/p></description><content:encoded><![CDATA[<p>CloudHealth 的核心責任是把大型組織的 cloud spend、governance、policy、allocation 與 optimization workflow 放進同一個 FinOps 管理平面。它適合 account、team、business unit、provider 與採購流程複雜的組織，重點在讓成本治理、合規要求與工程 owner 能共用同一套成本事實。2018 年被 VMware 收購、2023 年隨 VMware 進入 Broadcom 旗下；現屬 Broadcom 的 enterprise FinOps 旗艦產品。</p>
<h2 id="服務定位">服務定位</h2>
<p>CloudHealth 跟 AWS Cost Explorer / Azure Cost Management 那種單雲原生工具的差異在 <em>跨雲一致 schema + enterprise FinOps operating model</em>、單雲帳單細節反而是原生工具更深。Cost Explorer 在 AWS-only 場景的 granularity 更深、但跨 Azure / GCP 帳單對齊、成本中心 chargeback、policy 治理就需要 CloudHealth 這類 multi-cloud platform。</p>
<p>跟 Vantage 比、CloudHealth 走 <em>enterprise governance-first</em>、Vantage 走 <em>engineering-friendly dashboard-first</em>。Vantage 對小到中型 cloud-native 團隊更快上手、但 chargeback 流程、policy violation queue、approval workflow 都不是它的主場。跟 Apptio Cloudability（IBM 收購）比、兩者定位最接近、都吃 large enterprise FinOps 市場；CloudHealth 的差異是 VMware / Broadcom ecosystem 整合（vCenter / Tanzu / on-prem hybrid），Cloudability 強在 TBM（Technology Business Management）財務分攤模型成熟度。</p>
<p>關鍵張力：<em>Broadcom 收購後的 product roadmap 不確定性</em> ↔ <em>enterprise FinOps ecosystem 深度</em>。Broadcom 對 VMware portfolio 的價格調整、partner 縮編、support tier 變動 2024-2025 持續發生；客戶要評估 <em>退場成本（chargeback rule + tag taxonomy 量大）vs 短期 license 漲幅</em>、不是只看當下功能。</p>
<h2 id="定位">定位</h2>
<p>CloudHealth 適合 enterprise FinOps 與 cloud governance。當組織需要跨 AWS、Azure、Google Cloud、Kubernetes、shared services 與成本中心建立 showback、chargeback、policy 與 optimization workflow，CloudHealth 類平台可以提供集中式成本管理與治理視角。</p>
<p>這個定位讓 CloudHealth 接到三個主章。它從 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 接收 cost curve 與 over-provision waste，從 <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> 接收成本 dashboard 需求，從 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a> 接收 owner、tag 與 attribution 規則。</p>
<h2 id="適用場景">適用場景</h2>
<p>多雲成本治理是 CloudHealth 的主要入口。大型企業常有不同 cloud provider、不同採購合約、不同 account 結構與不同團隊成熟度；CloudHealth 可以把成本、資產、policy 與權限治理收斂到 FinOps 工作流程。</p>
<p>Showback / chargeback 適合用 CloudHealth 建立財務語言。成本中心、部門、產品線、環境與專案需要穩定分攤規則，才能讓工程決策接到預算管理、採購承諾與年度規劃。</p>
<p>Optimization workflow 適合用 CloudHealth 管理組織節奏。Rightsizing、reserved capacity、idle resource、tag compliance 與 policy violation 都需要 owner、例外、核准、驗證與追蹤，enterprise 平台的價值在於流程一致。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>CloudHealth 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組織治理</td>
          <td>支援多 account、多團隊、成本中心與 policy</td>
          <td>FinOps operating model、owner taxonomy</td>
      </tr>
      <tr>
          <td>成本分攤</td>
          <td>支援 showback / chargeback 與 shared cost rule</td>
          <td>tag hygiene、成本中心對照表</td>
      </tr>
      <tr>
          <td>最佳化流程</td>
          <td>支援 rightsizing、commitment 與 policy action</td>
          <td>工程驗證、變更排程、saving confirmation</td>
      </tr>
      <tr>
          <td>Enterprise 整合</td>
          <td>適合採購、財務、平台與工程共同使用</td>
          <td>權限模型、報表治理、例外處理</td>
      </tr>
  </tbody>
</table>
<p>組織治理價值來自一致流程。單一工程團隊可以靠雲端原生工具追成本；大型組織需要 policy、role、approval、exception 與 audit trail 才能讓成本治理長期運作。</p>
<p>成本分攤價值來自可對帳。Showback / chargeback 要能讓財務、平台與服務 owner 對同一筆費用得到相同解釋，shared platform cost、discount、support fee 與 commitment benefit 都要有分攤規則。</p>
<p>最佳化流程價值來自閉環管理。Rightsizing recommendation 只有在 owner 接手、服務驗證、變更落地與 saving confirmation 完成後，才會變成實際成本改善。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 CloudHealth deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Multi-cloud connector 完整性</strong>：AWS（CUR / billing role）、Azure（EA / MCA billing role）、GCP（BigQuery billing export）、Kubernetes（kube-state-metrics + Prometheus）連接器是否都接通、是否有 daily ingestion lag、是否漏 account / subscription</li>
<li><strong>FinOps team workflow 落地</strong>：policy queue、recommendation queue、approval flow 是否有實際 owner（不只是 dashboard 看一看）、weekly / monthly FinOps cadence 是否進到工程 sprint 跟財務 close cycle</li>
<li><strong>Chargeback 規則可對帳</strong>：business unit / cost center / application / environment 的分攤公式是否文件化、shared service（platform team / CI runner / observability stack）的 split rule 是否被各 BU 接受、月底財務 close 對得起來</li>
<li><strong>Reserved Instance / Savings Plan 管理</strong>：commitment coverage（已 commit 比例）、utilization（已用比例）、expiration alert、跨 account 的 commitment sharing 是否有 owner 主動經營、不是買完就放著</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 邊界的待補項目。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>CloudHealth</th>
          <th>Vantage</th>
          <th>AWS Cost Explorer</th>
          <th>Apptio Cloudability</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-cloud</td>
          <td>強 — AWS / Azure / GCP / K8s</td>
          <td>強 — 加 Snowflake / Datadog 整合</td>
          <td>弱 — AWS-only</td>
          <td>強 — 三大雲 + on-prem</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>陡 — enterprise model 複雜</td>
          <td>緩 — engineer 友善 dashboard</td>
          <td>緩 — AWS console 內建</td>
          <td>陡 — TBM 模型門檻高</td>
      </tr>
      <tr>
          <td>Chargeback</td>
          <td>強 — policy + approval flow 完整</td>
          <td>中 — report-driven、流程靠外掛</td>
          <td>弱 — 報表為主、無 workflow</td>
          <td>強 — TBM 財務分攤是主場</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>SaaS only</td>
          <td>SaaS only</td>
          <td>AWS console 內建</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>適合規模</td>
          <td>Enterprise（多 BU + 多雲）</td>
          <td>Startup ~ Mid（cloud-native）</td>
          <td>AWS single-account ~ Org</td>
          <td>Enterprise（重財務治理）</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>% of cloud spend + minimum</td>
          <td>Per-cloud-account tier</td>
          <td>Free（AWS 內建）</td>
          <td>% of cloud spend + minimum</td>
      </tr>
      <tr>
          <td>Roadmap 風險</td>
          <td>Broadcom 收購後不確定</td>
          <td>獨立公司、roadmap 穩定</td>
          <td>AWS 自家、roadmap 跟雲同步</td>
          <td>IBM 收購後整合中</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — chargeback rule + tag 量大</td>
          <td>低 — report 可重建</td>
          <td>無 — AWS-native 切換無痛</td>
          <td>高 — TBM 模型重 migrate</td>
      </tr>
  </tbody>
</table>
<p>選 CloudHealth 的核心訴求：<em>enterprise scale + 多雲 + 已有 VMware / Broadcom ecosystem</em>、且能投入 FinOps team 維護 chargeback rule、policy queue、commitment management lifecycle。中小型 cloud-native 走 Vantage 更快；AWS-only 直接用 Cost Explorer + Cost Anomaly Detection；重財務 TBM 整合走 Apptio Cloudability。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>CloudHealth 和 Vantage 的主要差異是治理深度。Vantage 偏工程友善報表與 Kubernetes cost visibility；CloudHealth 偏 enterprise FinOps operating model、policy 與大組織分攤流程。</p>
<p>CloudHealth 和 Akamas 的主要差異是最佳化方式。CloudHealth 偏成本治理與推薦流程；Akamas 偏把 SLO 約束與 configuration tuning 放進 optimization engine。</p>
<p>CloudHealth 和 AWS Cost Explorer 的主要差異是多雲與流程。Cost Explorer 適合 AWS-native 成本分析；CloudHealth 適合跨 provider、跨成本中心與跨團隊治理。</p>
<h2 id="操作成本">操作成本</h2>
<p>CloudHealth 的主要成本是組織模型維護。Business unit、cost center、application、environment、owner、account 與 tag policy 需要持續治理，平台才能提供穩定報表。</p>
<p>流程成本會高於單純報表工具。Recommendation 需要進入 approval、exception、change management、validation 與 financial close process；這些流程讓工具適合大型組織，也要求更高維運紀律。</p>
<p>資料品質成本會集中在標籤與 shared cost。未標記資源、跨團隊 shared service、commitment benefit 分攤與 marketplace charge 都會影響成本歸屬信任度。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Reserved Instance 與 Savings Plan management</strong>：CloudHealth 把 commitment 視為 portfolio、不是單筆採購。Coverage（已 commit 比例）、utilization（已用比例）、break-even（攤平時間）三個指標要持續追、跟業務 roadmap 對齊；新服務上線前先 model 預期用量、commit 太多反而 lock-in 浪費、太少又付 on-demand 溢價。跨 account / linked account 的 commitment sharing 要明確 owner、不然 platform team 買的 RI 被 product team 吃掉、財務分攤回不去。</p>
<p><strong>Chargeback / showback 流程</strong>：showback 是 <em>讓 BU 看到自己花多少</em>、chargeback 是 <em>讓 BU 帳本上真的扣這筆</em>。chargeback 需要財務簽核、需要每月 close cycle、需要 dispute 機制；CloudHealth 的 chargeback rule 改動要走 approval、不能 admin 自己改完就上線、會直接影響 BU 月結。</p>
<p><strong>Multi-cloud asset inventory</strong>：CloudHealth 不只是帳單工具、也作 asset inventory — EC2 / RDS / VM / GKE node / Azure SQL 等資源的 owner、tag、environment、policy state 在同一視角。這個能力是 enterprise CMDB integration 的入口、也能反向支援 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security posture</a> 的 untagged / unauthorized resource 偵測。</p>
<p><strong>跟 Datadog / SIEM integration</strong>：CloudHealth 的 cost data 可以 export 到 <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> 作 SRE cost-aware alert（service 突然花費暴衝 → 通常是 retry storm / runaway job），也可送 SIEM 作 untagged resource / cross-account spend anomaly 偵測。整合的價值不是把 CloudHealth 當另一個 observability tool、而是讓 cost signal 進到工程值班的視野。</p>
<p><strong>Broadcom 收購後 product roadmap 變動風險</strong>：2023 Broadcom 完成 VMware 收購後、CloudHealth 經歷 license model 調整、partner program 變動、support tier 重整。對既有大客戶來說 license 漲幅、SLA 條款、roadmap 透明度都進入再評估期；新客戶選型時 <em>退場成本評估</em> 要先做、不能假設 platform 五年不變。Broadcom 對 enterprise 客戶仍會維持產品線、但中小客戶可能感受到 support 縮減。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Multi-cloud tag 不一致</strong>：AWS 用 <code>Environment=prod</code>、Azure 用 <code>env=production</code>、GCP 用 <code>env-tier=prod</code> — CloudHealth 報表看起來三套不同 — 統一 tag taxonomy（cost center / application / environment / owner）寫進 cloud governance policy、用 cloud-native enforcement（AWS Tag Policy / Azure Policy / GCP Org Policy）擋未標記資源</li>
<li><strong>Chargeback 對不上帳</strong>：BU 看到的金額 ≠ 財務 close 的金額 — shared service split rule 沒被簽核、commitment benefit attribution 跑掉、marketplace charge 沒分攤 — 走 monthly close reconciliation、把 rule 鎖定後才開 dispute window</li>
<li><strong>Reserved Instance 浪費</strong>：commit 買了沒用滿（utilization &lt; 80%）— 跨 account share 沒開、或業務 roadmap 改了沒同步 commitment team — 開 cross-account RI sharing、commitment review 進 monthly FinOps cadence</li>
<li><strong>新雲帳號接不進來</strong>：connector 一直 ingestion failure — IAM role / EA permission / BigQuery export 沒設好、或 organization 結構改了 CloudHealth 沒同步 — 走 onboarding checklist、新 account 自動化納管</li>
<li><strong>Recommendation 一直沒人 action</strong>：rightsizing queue 累積幾百筆沒處理 — 沒有 owner、或 recommendation 沒對應到實際 service team — 用 tag 反查 owner、把 recommendation 進 sprint backlog 而非 FinOps 自己追</li>
<li><strong>Broadcom 收購後 support / price 變動</strong>：renewal 漲幅突然 30-50%、support tier 被降級 — 早一年開始評估替代方案（Vantage / Apptio / 雲原生組合）、把 chargeback rule 跟 tag taxonomy 抽象到不綁 vendor 的格式</li>
</ul>
<h2 id="evidence-package">Evidence Package</h2>
<p>CloudHealth 結果應回寫到 FinOps governance evidence package。最小欄位包括 business unit、cost center、application、provider、account、policy、recommendation、expected saving、approval state、implementation state、verified saving 與 exception。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>CloudHealth 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>cost report、policy report、recommendation queue</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>billing period、review cycle、saving validation window</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>CloudHealth report、cloud billing query、policy detail</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>tag compliance、account coverage、allocation rule</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>owner mapping、approval status、verified saving</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>shared service rule、manual exception、provider delay</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是支援治理審查。CloudHealth report 要能回答「這筆成本屬於誰、哪條 policy 觸發、誰核准例外、變更是否真的帶來 savings」。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>CloudHealth 目前適合作為 enterprise FinOps 與多雲治理案例的工具承接點。它可回寫到 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 的 7 個受監管市場跨地區治理與成本中心分攤需求、<a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch on Azure AKS</a> 的傳統產業多 BU 治理一致性、<a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair hybrid burst</a> 的 on-prem + GCP 雙來源帳單合併、以及 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap multi-cloud</a> 的 GCP + AWS 跨雲成本對照。</p>
<p>這些案例的重點是組織能力。CloudHealth 頁引用案例時，要把案例拆成 governance model、owner taxonomy、policy action、engineering validation 與 financial reporting — 例如 Standard Chartered 的 7 市場分割要回到 per-market policy + 合規 tag、不是單一全球 report、而非停在雲端帳單下降。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a></li>
<li>官方：<a href="https://news.broadcom.com/apj/releases/broadcom-announces-new-cloudhealth-user-experience-for-greater-cloud-spend-management-across-enterprise-teams">Broadcom CloudHealth announcement</a></li>
</ul>
]]></content:encoded></item><item><title>9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/</guid><description>&lt;p>這個案例的核心責任是說明「hybrid cloud burst」模式 — 平日跑自家 data center、峰值事件靠雲端補容量。這跟全部上雲（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a>）或全部自管的兩種極端都不同、是大企業常見的折衷路徑。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Wayfair 在 GCP 的關鍵敘述（引自 &lt;a href="https://cloud.google.com/customers/wayfair">Wayfair Case Study&lt;/a>）：&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>商品數量&lt;/td>
 &lt;td>22 M+ 個 SKU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應商數量&lt;/td>
 &lt;td>16,000+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>員工數&lt;/td>
 &lt;td>17,000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>北美 + 歐洲&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>峰值事件&lt;/td>
 &lt;td>Way Day（年度大促）、Black Friday、Cyber Monday&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>COVID Q2 2020 業績&lt;/td>
 &lt;td>美國淨營收成長 +82.5%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>架構模式&lt;/td>
 &lt;td>Hybrid（on-prem + GCP burst）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：BigQuery（資料倉儲）、Cloud Dataproc（資料處理）、Cloud Pub/Sub（資料注入）、Looker（dashboard）、Cloud DLP（合規）、C2 processors（高性能 compute）。&lt;/p>
&lt;p>關鍵敘述：「Our automation systems signal the cloud to scale on demand」「We were able to reduce and eventually eliminate the need for change freezes leading up to big events」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Wayfair 揭露三個 hybrid cloud burst 模式的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Hybrid burst 是「容量規劃成本平衡」的折衷&lt;/strong>：自家 data center 平日跑得便宜、峰值事件不夠用；全部上雲峰值好辦但平日成本高。Hybrid 模式讓 baseline 用便宜的、峰值用彈性的、總成本曲線最平。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的長期 TCO 規劃。&lt;/li>
&lt;li>&lt;strong>「Change freeze 不再需要」是 burst 模式的真正價值&lt;/strong>：傳統零售 IT 為了 Black Friday 通常 2-3 個月前就 freeze code change、確保穩定。Wayfair 在 GCP burst 上線後、能在峰值前繼續正常 release — 因為新功能可以單獨 deploy 到 GCP、不影響 on-prem 主系統。對應 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate&lt;/a> 的非凍結式變更管理。&lt;/li>
&lt;li>&lt;strong>資料平面（BigQuery / Dataproc）是 hybrid 的主場、交易平面仍在 on-prem&lt;/strong>：Wayfair 把「分析、報表、推薦模型」放 GCP、「核心交易、訂單處理、庫存」仍在自家。這個切分是 hybrid 的常見做法 — 計算密集的工作上雲、業務核心保留自管。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的核心 OLTP 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 的分析資料層分離。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>Wayfair 案例 &lt;em>沒有&lt;/em> 提具體 TPS、latency、capacity scale 數字 — 行銷敘述居多、工程細節較少。讀此類案例要對 &lt;em>策略&lt;/em> 做學習、不要套用具體數字。&lt;/li>
&lt;li>「82.5% 美國淨營收成長」是 &lt;em>業績&lt;/em>、不是 &lt;em>系統指標&lt;/em>。系統能撐業績、但兩者不是同一件事。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「hybrid cloud burst」模式 — 平日跑自家 data center、峰值事件靠雲端補容量。這跟全部上雲（<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>）或全部自管的兩種極端都不同、是大企業常見的折衷路徑。</p>
<h2 id="觀察">觀察</h2>
<p>Wayfair 在 GCP 的關鍵敘述（引自 <a href="https://cloud.google.com/customers/wayfair">Wayfair Case Study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商品數量</td>
          <td>22 M+ 個 SKU</td>
      </tr>
      <tr>
          <td>供應商數量</td>
          <td>16,000+</td>
      </tr>
      <tr>
          <td>員工數</td>
          <td>17,000</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>北美 + 歐洲</td>
      </tr>
      <tr>
          <td>峰值事件</td>
          <td>Way Day（年度大促）、Black Friday、Cyber Monday</td>
      </tr>
      <tr>
          <td>COVID Q2 2020 業績</td>
          <td>美國淨營收成長 +82.5%</td>
      </tr>
      <tr>
          <td>架構模式</td>
          <td>Hybrid（on-prem + GCP burst）</td>
      </tr>
  </tbody>
</table>
<p>服務組合：BigQuery（資料倉儲）、Cloud Dataproc（資料處理）、Cloud Pub/Sub（資料注入）、Looker（dashboard）、Cloud DLP（合規）、C2 processors（高性能 compute）。</p>
<p>關鍵敘述：「Our automation systems signal the cloud to scale on demand」「We were able to reduce and eventually eliminate the need for change freezes leading up to big events」。</p>
<h2 id="判讀">判讀</h2>
<p>Wayfair 揭露三個 hybrid cloud burst 模式的工程重點。</p>
<ol>
<li><strong>Hybrid burst 是「容量規劃成本平衡」的折衷</strong>：自家 data center 平日跑得便宜、峰值事件不夠用；全部上雲峰值好辦但平日成本高。Hybrid 模式讓 baseline 用便宜的、峰值用彈性的、總成本曲線最平。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的長期 TCO 規劃。</li>
<li><strong>「Change freeze 不再需要」是 burst 模式的真正價值</strong>：傳統零售 IT 為了 Black Friday 通常 2-3 個月前就 freeze code change、確保穩定。Wayfair 在 GCP burst 上線後、能在峰值前繼續正常 release — 因為新功能可以單獨 deploy 到 GCP、不影響 on-prem 主系統。對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a> 的非凍結式變更管理。</li>
<li><strong>資料平面（BigQuery / Dataproc）是 hybrid 的主場、交易平面仍在 on-prem</strong>：Wayfair 把「分析、報表、推薦模型」放 GCP、「核心交易、訂單處理、庫存」仍在自家。這個切分是 hybrid 的常見做法 — 計算密集的工作上雲、業務核心保留自管。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的核心 OLTP 跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 的分析資料層分離。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>Wayfair 案例 <em>沒有</em> 提具體 TPS、latency、capacity scale 數字 — 行銷敘述居多、工程細節較少。讀此類案例要對 <em>策略</em> 做學習、不要套用具體數字。</li>
<li>「82.5% 美國淨營收成長」是 <em>業績</em>、不是 <em>系統指標</em>。系統能撐業績、但兩者不是同一件事。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>Hybrid burst 適合「業務核心 on-prem 已穩定 + 季節性 / 事件型峰值」的企業</strong>：對於全新雲原生 startup、直接全上雲更簡單；對於有 15-20 年自建系統的大企業、hybrid 是穩妥路徑。</li>
<li><strong>資料平面先上雲、交易平面後上</strong>：BI、ML、推薦這類「計算密集 + 資料量大 + 容忍延遲」適合先上 GCP / AWS / Azure；OLTP 後續再評估。對應 <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> 的資料層先行模式。</li>
<li><strong>automation signal + 雲端 burst 是「change freeze」的解法</strong>：監控訊號 → 自動 trigger 雲端容量 → 平滑釋放 → 不影響 on-prem 主系統的部署節奏。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a>。</li>
</ol>
<p>跨平台等效：AWS Outposts + AWS Direct Connect、Azure Arc + ExpressRoute、Equinix + 各雲商 PrivateLink 都是 hybrid burst 的基礎設施。差異是各家 hybrid 策略成熟度。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 hybrid cloud burst → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a></li>
<li>想做資料平面遷移 → <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>對照全雲原生 → <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></li>
<li>想取消 change freeze → <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a> + <a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">06.17 feature flag governance</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/customers/wayfair">Wayfair Case Study (Google Cloud)</a></li>
<li><a href="https://cloud.google.com/blog/topics/customers">Way Day 2019 burst capacity</a></li>
</ul>
]]></content:encoded></item><item><title>4.22 Checkout API Evidence Package 實作示範</title><link>https://tarrragon.github.io/blog/backend/04-observability/checkout-api-evidence-package/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/checkout-api-evidence-package/</guid><description>&lt;p>Checkout API evidence package 的核心責任是把同一條交易路徑的訊號整理成可交接證據，讓放行與事故判斷用到同一組事實。&lt;/p>
&lt;h2 id="服務路徑與邊界">服務路徑與邊界&lt;/h2>
&lt;p>本篇服務路徑是 &lt;code>client -&amp;gt; checkout-api -&amp;gt; payment-adapter -&amp;gt; order-db&lt;/code>。觀測邊界只處理「這條路徑目前是否可判讀」，不處理重試策略與回退決策本身；後者交給 06 與 08。&lt;/p>
&lt;p>要先定義 evidence package 的最小欄位：&lt;code>Source&lt;/code>、&lt;code>Time range&lt;/code>、&lt;code>Query link&lt;/code>、&lt;code>Owner&lt;/code>、&lt;code>Data quality&lt;/code>、&lt;code>Confidence&lt;/code>、&lt;code>Known gap&lt;/code>。這些欄位在事故期與放行期共用，避免兩套語言。&lt;/p>
&lt;h2 id="實作步驟">實作步驟&lt;/h2>
&lt;ol>
&lt;li>固定交易路徑的觀測主鍵：&lt;code>trace_id&lt;/code>、&lt;code>order_id&lt;/code>、&lt;code>tenant_id&lt;/code>、&lt;code>region&lt;/code>。&lt;/li>
&lt;li>建立三組查詢入口：延遲分布（p50/p95/p99）、錯誤率與錯誤類別、下游 payment dependency timeout。&lt;/li>
&lt;li>為每組查詢補欄位：時間窗、資料延遲、採樣比例、目前 owner。&lt;/li>
&lt;li>在 deploy 前把同一份 evidence package 連到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>。&lt;/li>
&lt;li>事故期間把同一份 evidence package 連到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&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>p95 latency 升高但 error rate 無明顯變化&lt;/td>
 &lt;td>可能是下游慢查詢或連線池飽和&lt;/td>
 &lt;td>先查 dependency span 與 DB wait&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>payment timeout 增加且 trace 斷在 adapter&lt;/td>
 &lt;td>下游依賴退化，不是本地 CPU 飽和&lt;/td>
 &lt;td>進 6.8 依賴風險 gate，限制放行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>log 有錯誤但 metric 沒反映&lt;/td>
 &lt;td>訊號覆蓋不一致或聚合粒度不對&lt;/td>
 &lt;td>回寫 data quality，補 query 與聚合維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dashboard 正常但客訴增加&lt;/td>
 &lt;td>可觀測性盲區或取樣偏差&lt;/td>
 &lt;td>提升 client-side signal 權重並標示 known gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同版不同區域行為差異大&lt;/td>
 &lt;td>區域配置或依賴拓樸差異，非單點程式回歸&lt;/td>
 &lt;td>補 region 維度 evidence，進 8.18 分流 triage&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 evidence package 寫成 dashboard 截圖集合，會失去可重跑性。沒有 query link 與時間窗，事故交班時很難重建判讀脈絡。&lt;/p>
&lt;p>把 confidence 省略也會導致誤判。事故前期資料常不完整，若不標示 &lt;code>suspected&lt;/code> 與 &lt;code>known gap&lt;/code>，下游決策容易把猜測當成結論。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>這條路徑可用 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident&lt;/a> 回寫。先看跨服務訊號如何失真，再回到本章檢查欄位是否能支撐「先分流、再判斷」。&lt;/p>
&lt;p>這個案例主要支撐的是「證據欄位完整度」判讀，不直接支撐 release gate 停損門檻設計；停損規則要回到 6.8。&lt;/p>
&lt;h2 id="跨模組路由">跨模組路由&lt;/h2>
&lt;ol>
&lt;li>與 4.17 的交接：資料限制與偏差回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a>。&lt;/li>
&lt;li>與 6.8 的交接：放行判斷使用同一份 evidence package。&lt;/li>
&lt;li>與 6.23 的交接：驗證證據欄位對齊 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">Verification Evidence Handoff&lt;/a>。&lt;/li>
&lt;li>與 8.19 的交接：事故決策直接引用 evidence link 與 confidence。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要把證據轉成放行條件，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Checkout API evidence package 的核心責任是把同一條交易路徑的訊號整理成可交接證據，讓放行與事故判斷用到同一組事實。</p>
<h2 id="服務路徑與邊界">服務路徑與邊界</h2>
<p>本篇服務路徑是 <code>client -&gt; checkout-api -&gt; payment-adapter -&gt; order-db</code>。觀測邊界只處理「這條路徑目前是否可判讀」，不處理重試策略與回退決策本身；後者交給 06 與 08。</p>
<p>要先定義 evidence package 的最小欄位：<code>Source</code>、<code>Time range</code>、<code>Query link</code>、<code>Owner</code>、<code>Data quality</code>、<code>Confidence</code>、<code>Known gap</code>。這些欄位在事故期與放行期共用，避免兩套語言。</p>
<h2 id="實作步驟">實作步驟</h2>
<ol>
<li>固定交易路徑的觀測主鍵：<code>trace_id</code>、<code>order_id</code>、<code>tenant_id</code>、<code>region</code>。</li>
<li>建立三組查詢入口：延遲分布（p50/p95/p99）、錯誤率與錯誤類別、下游 payment dependency timeout。</li>
<li>為每組查詢補欄位：時間窗、資料延遲、採樣比例、目前 owner。</li>
<li>在 deploy 前把同一份 evidence package 連到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</li>
<li>事故期間把同一份 evidence package 連到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>p95 latency 升高但 error rate 無明顯變化</td>
          <td>可能是下游慢查詢或連線池飽和</td>
          <td>先查 dependency span 與 DB wait</td>
      </tr>
      <tr>
          <td>payment timeout 增加且 trace 斷在 adapter</td>
          <td>下游依賴退化，不是本地 CPU 飽和</td>
          <td>進 6.8 依賴風險 gate，限制放行</td>
      </tr>
      <tr>
          <td>log 有錯誤但 metric 沒反映</td>
          <td>訊號覆蓋不一致或聚合粒度不對</td>
          <td>回寫 data quality，補 query 與聚合維度</td>
      </tr>
      <tr>
          <td>dashboard 正常但客訴增加</td>
          <td>可觀測性盲區或取樣偏差</td>
          <td>提升 client-side signal 權重並標示 known gap</td>
      </tr>
      <tr>
          <td>同版不同區域行為差異大</td>
          <td>區域配置或依賴拓樸差異，非單點程式回歸</td>
          <td>補 region 維度 evidence，進 8.18 分流 triage</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 evidence package 寫成 dashboard 截圖集合，會失去可重跑性。沒有 query link 與時間窗，事故交班時很難重建判讀脈絡。</p>
<p>把 confidence 省略也會導致誤判。事故前期資料常不完整，若不標示 <code>suspected</code> 與 <code>known gap</code>，下游決策容易把猜測當成結論。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>這條路徑可用 <a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident</a> 回寫。先看跨服務訊號如何失真，再回到本章檢查欄位是否能支撐「先分流、再判斷」。</p>
<p>這個案例主要支撐的是「證據欄位完整度」判讀，不直接支撐 release gate 停損門檻設計；停損規則要回到 6.8。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 4.17 的交接：資料限制與偏差回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.8 的交接：放行判斷使用同一份 evidence package。</li>
<li>與 6.23 的交接：驗證證據欄位對齊 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">Verification Evidence Handoff</a>。</li>
<li>與 8.19 的交接：事故決策直接引用 evidence link 與 confidence。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把證據轉成放行條件，接著讀 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a>。</p>
]]></content:encoded></item><item><title>Netflix：Business-Hours Chaos 與 Guardrails</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/</guid><description>&lt;p>Netflix 把 Chaos Monkey 放在 business hours 執行，核心責任是同時驗證系統韌性與團隊反應能力。若只在離峰或隔離環境跑故障注入，很多真實依賴與協作問題不會被看見。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>團隊常把 chaos 排在低流量時段，理由是比較安全。這種做法雖然降低短期風險，但也降低驗證價值：人員不在位、依賴流量特徵不同、通訊鏈條沒被真正測到。最後得到的是工具可執行，不是服務可承受。&lt;/p>
&lt;h2 id="驗證機制">驗證機制&lt;/h2>
&lt;p>Business-hours chaos 是把風險放進 guardrails 內驗證，風險範圍是收斂的。&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>時段限制&lt;/td>
 &lt;td>事故處理人力是否在線&lt;/td>
 &lt;td>僅在可支援時段啟動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實驗範圍限制&lt;/td>
 &lt;td>是否影響過大 blast radius&lt;/td>
 &lt;td>先從小範圍服務群組啟動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>停止條件&lt;/td>
 &lt;td>何時立即結束實驗&lt;/td>
 &lt;td>明確 abort trigger 與 rollback 路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事後回寫&lt;/td>
 &lt;td>是否有把結果回寫到工程控制面&lt;/td>
 &lt;td>固定接 [8.22 evidence write-back]&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個機制的本質是「在可控邊界內接近真實情境」，而不是追求更大故障。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>abort trigger latency&lt;/td>
 &lt;td>停止條件是否能即時生效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>on-call handoff quality&lt;/td>
 &lt;td>值班與指揮鏈條是否順暢&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>steady-state drift&lt;/td>
 &lt;td>實驗期間是否偏離穩態&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>communication lag&lt;/td>
 &lt;td>內外部更新是否跟上變化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>常見誤解是「business hours chaos 比較危險，所以應該避免」。真正風險在於沒有 guardrails，而不是時段本身。若有明確範圍、停止條件與值班協調，business-hours 測到的結果反而更接近真實事故。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 Reliability Readiness Review&lt;/a> 檢查實驗前置條件，再到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a> 寫 guardrails 與 abort 條件。實驗結果回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6 Drills and On-call Readiness&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey">Netflix/SimianArmy Wiki: Chaos Monkey&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Netflix 把 Chaos Monkey 放在 business hours 執行，核心責任是同時驗證系統韌性與團隊反應能力。若只在離峰或隔離環境跑故障注入，很多真實依賴與協作問題不會被看見。</p>
<h2 id="問題場景">問題場景</h2>
<p>團隊常把 chaos 排在低流量時段，理由是比較安全。這種做法雖然降低短期風險，但也降低驗證價值：人員不在位、依賴流量特徵不同、通訊鏈條沒被真正測到。最後得到的是工具可執行，不是服務可承受。</p>
<h2 id="驗證機制">驗證機制</h2>
<p>Business-hours chaos 是把風險放進 guardrails 內驗證，風險範圍是收斂的。</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時段限制</td>
          <td>事故處理人力是否在線</td>
          <td>僅在可支援時段啟動</td>
      </tr>
      <tr>
          <td>實驗範圍限制</td>
          <td>是否影響過大 blast radius</td>
          <td>先從小範圍服務群組啟動</td>
      </tr>
      <tr>
          <td>停止條件</td>
          <td>何時立即結束實驗</td>
          <td>明確 abort trigger 與 rollback 路徑</td>
      </tr>
      <tr>
          <td>事後回寫</td>
          <td>是否有把結果回寫到工程控制面</td>
          <td>固定接 [8.22 evidence write-back]</td>
      </tr>
  </tbody>
</table>
<p>這個機制的本質是「在可控邊界內接近真實情境」，而不是追求更大故障。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>abort trigger latency</td>
          <td>停止條件是否能即時生效</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>on-call handoff quality</td>
          <td>值班與指揮鏈條是否順暢</td>
          <td><a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2</a></td>
      </tr>
      <tr>
          <td>steady-state drift</td>
          <td>實驗期間是否偏離穩態</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>communication lag</td>
          <td>內外部更新是否跟上變化</td>
          <td><a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>常見誤解是「business hours chaos 比較危險，所以應該避免」。真正風險在於沒有 guardrails，而不是時段本身。若有明確範圍、停止條件與值班協調，business-hours 測到的結果反而更接近真實事故。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先在 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 Reliability Readiness Review</a> 檢查實驗前置條件，再到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> 寫 guardrails 與 abort 條件。實驗結果回寫 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6 Drills and On-call Readiness</a> 與 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey">Netflix/SimianArmy Wiki: Chaos Monkey</a></li>
<li><a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey</a></li>
</ul>
]]></content:encoded></item><item><title>6.22 Steady State Definition</title><link>https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>steady state 的責任：定義實驗期間系統應維持的可接受狀態&lt;/li>
&lt;li>穩態來源：SLO、business KPI、queue lag、error rate、latency、throughput、customer impact&lt;/li>
&lt;li>可接受退化：degradation mode、fallback、load shedding、partial outage&lt;/li>
&lt;li>實驗假設：故障注入後哪些訊號應保持穩定，哪些訊號可暫時退化&lt;/li>
&lt;li>觀測要求：dashboard、alert、trace、synthetic probe、client-side signal&lt;/li>
&lt;li>跟 chaos 的關係：沒有 steady state，chaos 只能證明系統被打壞&lt;/li>
&lt;li>跟 incident response 的關係：steady state 也定義事故恢復完成條件&lt;/li>
&lt;li>反模式：只定義故障動作，不定義成功條件；只看 server 指標，不看使用者影響&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">Steady state&lt;/a> definition 的價值是讓實驗與事故有共同終點。穩態定義建立後，團隊可以同時回答「壞到什麼程度可接受」與「什麼時候算恢復」。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Steady state definition 是可靠性實驗的成功條件，責任是讓團隊知道故障發生後系統應該維持什麼服務能力。&lt;/p>
&lt;p>這一頁處理的是穩態定義。Chaos、failover 與 DR drill 都需要先定義系統的可接受狀態，才能判斷實驗是在驗證韌性，還是在製造混亂。&lt;/p>
&lt;p>穩態是一組服務承諾，通常同時包含成功率、延遲、資料正確性與使用者影響，並對應不同故障情境下的可接受退化範圍。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 steady state 時，先看穩態是否貼近使用者，再看退化是否有明確邊界。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>steady state 是否包含 success rate、latency、queue lag 與 user impact&lt;/li>
&lt;li>degraded mode 是否說明哪些功能保留、哪些功能暫停&lt;/li>
&lt;li>stop condition 是否連到 steady state breach&lt;/li>
&lt;li>dashboard 是否能同時呈現系統指標與使用者旅程&lt;/li>
&lt;li>recovery complete 是否有可量測門檻&lt;/li>
&lt;/ul>
&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>服務成功&lt;/td>
 &lt;td>success rate / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 在可接受範圍&lt;/td>
 &lt;td>判斷是否需要升級事故&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>體驗延遲&lt;/td>
 &lt;td>latency 與 queue lag 在門檻內&lt;/td>
 &lt;td>判斷是否進入 degraded mode&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料正確&lt;/td>
 &lt;td>無資料遺失或可接受補償策略&lt;/td>
 &lt;td>判斷是否可宣告恢復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>恢復條件&lt;/td>
 &lt;td>recovery complete 有量測閾值&lt;/td>
 &lt;td>判斷事故何時可關閉&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="穩態來源">穩態來源&lt;/h2>
&lt;p>Steady state 的來源是服務承諾與操作訊號。它需要把 SLO、business KPI、系統指標與客戶感知訊號放在同一個判讀模型中。&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>SLO / SLI&lt;/td>
 &lt;td>定義可靠性承諾&lt;/td>
 &lt;td>success rate、latency、freshness&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Business KPI&lt;/td>
 &lt;td>定義業務結果是否維持&lt;/td>
 &lt;td>checkout success、order volume&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Queue / async&lt;/td>
 &lt;td>定義背景流程是否可追上&lt;/td>
 &lt;td>queue lag、DLQ、retry rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client signal&lt;/td>
 &lt;td>定義使用者感知是否正常&lt;/td>
 &lt;td>RUM、synthetic probe、mobile error&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data signal&lt;/td>
 &lt;td>定義資料是否正確且可回復&lt;/td>
 &lt;td>reconciliation、replication lag&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>SLO / SLI 是 steady state 的主要來源。它們讓實驗與事故判讀有共同基準，避免每次演練都重新討論什麼算可接受狀態。&lt;/p>
&lt;p>Business KPI 能補足純技術指標的盲區。checkout success、payment authorization、message delivery、document publish 與 invoice generation 這些業務結果，能直接反映使用者旅程是否維持。&lt;/p>
&lt;p>Queue / async 訊號能保護延遲性風險。同步 API 可能恢復，但 queue lag、DLQ、retry storm 或 backfill backlog 仍在累積；steady state 應包含這些後段壓力。&lt;/p>
&lt;p>Client signal 能補 server-side 盲區。CDN、mobile network、browser runtime、third-party script 與 regional routing 可能讓 server 看起來健康，但使用者仍感知到失敗。&lt;/p>
&lt;p>Data signal 能保護正確性。failover、migration、replay 與 DR drill 都需要確認資料沒有遺失，或至少有明確補償與 reconciliation 路徑。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>steady state 的責任：定義實驗期間系統應維持的可接受狀態</li>
<li>穩態來源：SLO、business KPI、queue lag、error rate、latency、throughput、customer impact</li>
<li>可接受退化：degradation mode、fallback、load shedding、partial outage</li>
<li>實驗假設：故障注入後哪些訊號應保持穩定，哪些訊號可暫時退化</li>
<li>觀測要求：dashboard、alert、trace、synthetic probe、client-side signal</li>
<li>跟 chaos 的關係：沒有 steady state，chaos 只能證明系統被打壞</li>
<li>跟 incident response 的關係：steady state 也定義事故恢復完成條件</li>
<li>反模式：只定義故障動作，不定義成功條件；只看 server 指標，不看使用者影響</li>
</ul>
<p><a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">Steady state</a> definition 的價值是讓實驗與事故有共同終點。穩態定義建立後，團隊可以同時回答「壞到什麼程度可接受」與「什麼時候算恢復」。</p>
<h2 id="概念定位">概念定位</h2>
<p>Steady state definition 是可靠性實驗的成功條件，責任是讓團隊知道故障發生後系統應該維持什麼服務能力。</p>
<p>這一頁處理的是穩態定義。Chaos、failover 與 DR drill 都需要先定義系統的可接受狀態，才能判斷實驗是在驗證韌性，還是在製造混亂。</p>
<p>穩態是一組服務承諾，通常同時包含成功率、延遲、資料正確性與使用者影響，並對應不同故障情境下的可接受退化範圍。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 steady state 時，先看穩態是否貼近使用者，再看退化是否有明確邊界。</p>
<p>重點訊號包括：</p>
<ul>
<li>steady state 是否包含 success rate、latency、queue lag 與 user impact</li>
<li>degraded mode 是否說明哪些功能保留、哪些功能暫停</li>
<li>stop condition 是否連到 steady state breach</li>
<li>dashboard 是否能同時呈現系統指標與使用者旅程</li>
<li>recovery complete 是否有可量測門檻</li>
</ul>
<table>
  <thead>
      <tr>
          <th>穩態元素</th>
          <th>最小可用判準</th>
          <th>判讀價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務成功</td>
          <td>success rate / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 在可接受範圍</td>
          <td>判斷是否需要升級事故</td>
      </tr>
      <tr>
          <td>體驗延遲</td>
          <td>latency 與 queue lag 在門檻內</td>
          <td>判斷是否進入 degraded mode</td>
      </tr>
      <tr>
          <td>資料正確</td>
          <td>無資料遺失或可接受補償策略</td>
          <td>判斷是否可宣告恢復</td>
      </tr>
      <tr>
          <td>恢復條件</td>
          <td>recovery complete 有量測閾值</td>
          <td>判斷事故何時可關閉</td>
      </tr>
  </tbody>
</table>
<h2 id="穩態來源">穩態來源</h2>
<p>Steady state 的來源是服務承諾與操作訊號。它需要把 SLO、business KPI、系統指標與客戶感知訊號放在同一個判讀模型中。</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>責任</th>
          <th>常見訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO / SLI</td>
          <td>定義可靠性承諾</td>
          <td>success rate、latency、freshness</td>
      </tr>
      <tr>
          <td>Business KPI</td>
          <td>定義業務結果是否維持</td>
          <td>checkout success、order volume</td>
      </tr>
      <tr>
          <td>Queue / async</td>
          <td>定義背景流程是否可追上</td>
          <td>queue lag、DLQ、retry rate</td>
      </tr>
      <tr>
          <td>Client signal</td>
          <td>定義使用者感知是否正常</td>
          <td>RUM、synthetic probe、mobile error</td>
      </tr>
      <tr>
          <td>Data signal</td>
          <td>定義資料是否正確且可回復</td>
          <td>reconciliation、replication lag</td>
      </tr>
  </tbody>
</table>
<p>SLO / SLI 是 steady state 的主要來源。它們讓實驗與事故判讀有共同基準，避免每次演練都重新討論什麼算可接受狀態。</p>
<p>Business KPI 能補足純技術指標的盲區。checkout success、payment authorization、message delivery、document publish 與 invoice generation 這些業務結果，能直接反映使用者旅程是否維持。</p>
<p>Queue / async 訊號能保護延遲性風險。同步 API 可能恢復，但 queue lag、DLQ、retry storm 或 backfill backlog 仍在累積；steady state 應包含這些後段壓力。</p>
<p>Client signal 能補 server-side 盲區。CDN、mobile network、browser runtime、third-party script 與 regional routing 可能讓 server 看起來健康，但使用者仍感知到失敗。</p>
<p>Data signal 能保護正確性。failover、migration、replay 與 DR drill 都需要確認資料沒有遺失，或至少有明確補償與 reconciliation 路徑。</p>
<h2 id="產業情境遊戲服務的穩態定義">產業情境：遊戲服務的穩態定義</h2>
<p>遊戲伺服器的穩態指標跟一般 web service 有結構性差異。即時互動遊戲的關鍵衡量是 tick rate stability（伺服器每秒處理的遊戲邏輯循環數）和 player session continuity（玩家連線不中斷），HTTP success rate 只能反映 API 層健康，無法代表 gameplay 品質。</p>
<p>穩態訊號需要覆蓋四個面向：tick rate 維持在目標頻率（如 64 tick/s 的射擊遊戲降到 32 就會被感知）、matchmaking latency 在可接受範圍、session persistence 不因後端變更掉線、state synchronization lag 不讓玩家看到不一致的遊戲狀態。</p>
<p>遊戲的高峰型態跟電商不同。峰值可能是新版本上線首日、季賽開始或限時活動開放，持續時間通常以天計（而非 BFCM 的數小時），流量曲線有明顯的日週期（每日晚間尖峰）。workload model 需要反映這種「多日高原 + 日內尖峰」的形狀，而非單一爆量。</p>
<p>Degraded mode 的定義需要區分核心 gameplay loop 與周邊系統。排行榜、成就系統、社交功能、商城可以暫停或降級，但核心對戰邏輯必須維持。玩家對 gameplay 中斷的容忍度遠低於周邊功能 — 排行榜延遲更新是可接受的退化，比賽中角色動作不同步則直接導致玩家離開。</p>
<p>穩態 breach 的判準對應兩個升級門檻：tick rate 低於感知門檻時，遊戲體驗開始劣化，需要啟動 load shedding 或關閉新 match 入口；session drop rate 超過門檻時，代表大量玩家掉線，需要升級事故等級並啟動 rollback。</p>
<h2 id="產業情境saas-與-b2b-服務的穩態定義">產業情境：SaaS 與 B2B 服務的穩態定義</h2>
<p>SaaS 服務的穩態需要按租戶層級定義。全域指標健康但特定租戶劣化的情境在多租戶系統很常見 — 全域 error rate 正常但某個大客戶的 latency 已超出其 SLA 承諾，只用全域穩態定義會讓這類局部退化被平均值隱藏。</p>
<p>租戶級 SLI 是 SaaS 穩態定義的核心擴充。按 tenant_id label 拆分 SLI（success rate / latency / queue lag），讓穩態判讀能對齊個別客戶的 SLA 承諾。enterprise 客戶的穩態門檻通常比 self-serve 更嚴格，拆分後才能分別判讀。拆分的成本是 cardinality 上升（每個 SLI × 租戶數），需要搭配 recording rule 或 rollup 控制 Prometheus / metrics backend 的壓力。</p>
<p>Noisy neighbor 是 SaaS 穩態的特有威脅。一個租戶的流量爆增或異常 query pattern 會拖垮共享資源（DB connection pool / cache throughput / queue depth），其他租戶的穩態被連帶破壞。穩態定義需要包含「單租戶資源消耗不超過共享資源配額的 X%」的條件，X 的值取決於隔離策略的強度 — <a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">Amazon A1 的 shuffle sharding</a> 讓租戶間擴散受限於 shard 重疊機率，<a href="/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">Shopify H2 的 pod 隔離</a> 讓租戶群組有獨立 pod 的穩態邊界。</p>
<p>Chaos 實驗在 SaaS 場景需要同時驗證全域穩態與租戶穩態。注入 DB latency 後，全域 success rate 可能只掉 0.1%（被其他健康租戶稀釋），但受影響的租戶群組可能已經 breach SLA。實驗的 steady state probe 需要同時查詢全域 SLI 和 top-N 租戶 SLI，才能判斷退化是否在可接受範圍。</p>
<h2 id="可接受退化">可接受退化</h2>
<p>可接受退化的責任是定義故障期間哪些能力要維持、哪些能力可以暫停、哪些能力需要補償。它讓團隊在壓力下有一致的降級語言。</p>
<table>
  <thead>
      <tr>
          <th>退化模式</th>
          <th>適用情境</th>
          <th>穩態判準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Read-only mode</td>
          <td>寫入風險高、讀取仍可服務</td>
          <td>讀取成功率維持，寫入明確暫停</td>
      </tr>
      <tr>
          <td>Fallback</td>
          <td>下游依賴失效</td>
          <td>使用替代資料，標示 freshness 限制</td>
      </tr>
      <tr>
          <td>Load shedding</td>
          <td>流量超過容量</td>
          <td>保核心旅程，拒絕低優先請求</td>
      </tr>
      <tr>
          <td>Partial outage</td>
          <td>區域、tenant 或功能局部受影響</td>
          <td>影響範圍可界定且持續收斂</td>
      </tr>
      <tr>
          <td>Manual recovery</td>
          <td>自動回復不足</td>
          <td>人工步驟有 owner、timeline、證據</td>
      </tr>
  </tbody>
</table>
<p>Read-only mode 適合保護資料正確性。若寫入路徑風險高，暫停寫入但保留查詢，可以讓服務維持部分價值，同時避免資料修復成本擴大。</p>
<p>Fallback 適合吸收下游失效。fallback 需要明確資料新鮮度、適用功能與使用者提示，讓服務承諾暫時降到可接受範圍。</p>
<p>Load shedding 適合處理容量壓力。它需要先定義核心旅程與低優先請求，讓系統在高壓下保住最重要的使用者結果。</p>
<p>Partial outage 適合處理 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 已被限制的事故。穩態定義應說明受影響 region、tenant、功能與預期恢復路徑，避免把局部可控誤讀成全域恢復。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>chaos 實驗只記錄「節點被關掉」，沒有記錄服務是否維持</li>
<li>failover 後 server healthy，但用戶核心流程仍失敗</li>
<li>degraded mode 啟動後，團隊不知道何時能解除</li>
<li>recovery 宣告依賴人工感覺，而非 SLO / synthetic probe / queue drain</li>
<li>事故與演練使用不同的恢復完成定義</li>
</ul>
<p>典型場景是 failover 後基礎 health check 全綠，但核心交易成功率仍低於承諾。若 steady state 只看系統健康，團隊會過早宣告恢復；若 steady state 包含 user journey，則會持續修復直到服務承諾回線。</p>
<h2 id="實驗假設">實驗假設</h2>
<p>Steady state 是 experiment hypothesis 的成功條件。故障注入前，團隊要先寫清楚哪些訊號應維持、哪些訊號可退化、退化多久仍可接受。</p>
<table>
  <thead>
      <tr>
          <th>假設欄位</th>
          <th>責任</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Injected failure</td>
          <td>說明要注入的失效</td>
          <td>關閉一個 cache node</td>
      </tr>
      <tr>
          <td>Expected behavior</td>
          <td>說明系統應如何吸收</td>
          <td>request latency 短暫上升</td>
      </tr>
      <tr>
          <td>Stable signals</td>
          <td>說明應維持穩定的訊號</td>
          <td>checkout success rate 維持門檻</td>
      </tr>
      <tr>
          <td>Allowed degradation</td>
          <td>說明可接受退化</td>
          <td>p99 latency 10 分鐘內回線</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>說明何時終止</td>
          <td>error budget burn 超門檻</td>
      </tr>
      <tr>
          <td>Recovery complete</td>
          <td>說明何時算恢復</td>
          <td>queue lag drain 到基準線</td>
      </tr>
  </tbody>
</table>
<p>Injected failure 只是實驗輸入。可靠性實驗真正要驗證的是 expected behavior，也就是系統面對失效時是否維持約定服務能力。</p>
<p>Stable signals 需要同時包含 server-side 與 user-facing 訊號。pod healthy、CPU 正常、database 可連線都很有用，但最後仍要回到核心旅程是否成功。</p>
<p>Allowed degradation 能避免過度反應。某些實驗預期會造成短暫 latency 上升或 fallback 啟動，只要在可接受時間窗內回線，就代表系統符合預期。</p>
<p>Recovery complete 應該可量測。queue lag drain、error rate 回到 baseline、synthetic probe 連續通過、reconciliation 完成，都比「看起來好了」更適合作為關閉條件。</p>
<h2 id="事故恢復">事故恢復</h2>
<p>Steady state 也是事故恢復宣告的共同基準。事故處理需要知道服務何時從 containment 進入 recovery，何時可以對內外部宣告恢復，何時進入 post-incident review。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>Steady state 責任</th>
          <th>事故決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Triage</td>
          <td>判斷是否已偏離穩態</td>
          <td>啟動或升級 incident</td>
      </tr>
      <tr>
          <td>Containment</td>
          <td>判斷退化是否維持在可接受範圍</td>
          <td>降級、限流、切換</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>判斷核心旅程是否回到門檻</td>
          <td>宣告服務恢復</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>判斷穩態定義是否足以支援判讀</td>
          <td>回寫 SLO、dashboard、runbook</td>
      </tr>
  </tbody>
</table>
<p>Triage 階段，steady state 幫助團隊把異常轉成事故門檻。若 success rate、latency、queue lag 或 customer impact 偏離穩態，就有足夠理由啟動分級。</p>
<p>Containment 階段，steady state 幫助團隊判斷退化策略是否有效。fallback、load shedding 或 read-only mode 啟動後，團隊要看核心旅程是否回到可接受範圍。</p>
<p>Recovery 階段，steady state 幫助團隊避免過早關閉事故。基礎 health check 回綠只是其中一個訊號，核心旅程、資料正確性與長尾 backlog 都要回到門檻。</p>
<p>Review 階段，steady state 會回寫到 04 與 06。若事故期間發現穩態指標缺失、門檻過鬆或 dashboard 不支援判讀，就要回到 SLO、observability readiness 或 reliability readiness。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Steady state 的反模式通常來自只定義故障動作，缺少成功條件。成功條件能讓 chaos、failover 與 DR drill 證明系統如何承受失效，而不只證明系統被打壞。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只定義故障動作</td>
          <td>實驗說明只有關機、斷線、切流量</td>
          <td>補 stable signals 與成功條件</td>
      </tr>
      <tr>
          <td>只看 server 指標</td>
          <td>health check 綠燈就宣告恢復</td>
          <td>加入 user journey 與 client signal</td>
      </tr>
      <tr>
          <td>退化模式無邊界</td>
          <td>fallback 啟動後無時間窗與限制</td>
          <td>定義 allowed degradation</td>
      </tr>
      <tr>
          <td>恢復完成靠感覺</td>
          <td>IC 以主觀判斷關閉事故</td>
          <td>定義 recovery complete metric</td>
      </tr>
      <tr>
          <td>實驗與事故標準不同</td>
          <td>drill 通過但事故時用另一套門檻</td>
          <td>共用 steady state 與 runbook</td>
      </tr>
  </tbody>
</table>
<p>只看 server 指標會讓恢復宣告偏早。服務健康需要同時看基礎設施、後端旅程、client-side signal 與資料正確性，才能支援對外通訊。</p>
<p>退化模式無邊界會讓 fallback 變成隱性事故。fallback 可用時，團隊仍需要知道資料新鮮度、功能限制、時間窗與客戶影響。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.6 SLI/SLO signal：把穩態轉成可量測訊號</li>
<li>04.10 client-side / synthetic / RUM：補使用者感知訊號</li>
<li>06.4 chaos testing：把 steady state 作為實驗前提</li>
<li>06.7 DR / rollback rehearsal：把 steady state 作為恢復完成條件</li>
<li>08.3 containment / recovery：事故恢復宣告使用同一組穩態門檻</li>
</ul>
]]></content:encoded></item><item><title>8.22 Incident Evidence Write-back</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>evidence write-back 的責任：把事故中產生的證據、決策與學習轉成上游改善&lt;/li>
&lt;li>輸入：incident intake、decision log、customer impact、timeline、PIR action item&lt;/li>
&lt;li>回寫面向：observability signal、telemetry data quality、verification scenario、runbook、automation boundary&lt;/li>
&lt;li>欄位：finding、evidence、owner、target artifact、closure signal、review date&lt;/li>
&lt;li>跟 4.20 的關係：事故證據缺口回寫成 evidence package 與資料品質改善&lt;/li>
&lt;li>跟 6.23 的關係：事故學習回寫成新的驗證題目與 handoff evidence&lt;/li>
&lt;li>反模式：PIR action item 停在待辦；事故證據沒有回到 dashboard / runbook；同類事故重複發生&lt;/li>
&lt;/ul>
&lt;p>Incident evidence write-back 的核心是把事故學習轉成上游 artifact。事故是流程回寫點，會產生新的訊號需求、驗證題目、runbook 修訂與自動化邊界。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Incident evidence write-back 是事故處理回寫到可觀測性、可靠性驗證與操作流程的閉環，責任是讓事故學習變成可驗證改善。&lt;/p>
&lt;p>這一頁處理的是事故後的交接。8.18 產生 intake evidence，8.19 保留 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log&lt;/a>，8.20 量化 customer impact；本章把這些材料轉成 04、06、08 內部可追蹤的改善 artifact。&lt;/p>
&lt;p>Write-back 的價值在於避免同類事故只被記錄一次。PIR action item 若只停在待辦，下一次事故仍會遇到相同缺口；write-back 要把缺口落到 dashboard、alert、SLO、experiment、runbook 或 automation guardrail。&lt;/p>
&lt;h2 id="案例中的回寫路徑">案例中的回寫路徑&lt;/h2>
&lt;p>回寫不是抽象流程，必須能對應到具體事故。Cloudflare 2019 與 AWS S3 2017 提供了兩種常見回寫場景：快速擴散型事故與共享依賴型事故。&lt;/p>
&lt;p>Cloudflare 2019 的關鍵缺口是規則成本在上線前不可見。回寫不是只寫「加強測試」，而是把 evidence 落到可執行控制面：04 的 rule-level CPU 訊號、06 的 rollout safety gate、08 的 decision log 與 write-back 閉環。這樣下次同類變更才會在推送前被攔下。&lt;/p>
&lt;p>AWS S3 2017 的關鍵缺口是共享子系統恢復順序與通訊入口依賴。回寫重點是操作與通訊控制面，單一 bug 修復遠遠不夠：內部操作 guardrail、恢復順序驗證、主通道失效切換，以及對外敘事的證據對位。這些回寫會直接改變下次事故的可見性與節奏。&lt;/p>
&lt;p>這兩個案例共同說明：好的回寫不是「多做一點」，而是把事故中的決策痛點轉成下一次能提早判讀的控制面。&lt;/p>
&lt;h2 id="輸入材料">輸入材料&lt;/h2>
&lt;p>Evidence write-back 的輸入來自事故期間已經建立的 artifact。每個 artifact 對應不同回寫方向。&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>Incident intake&lt;/td>
 &lt;td>source、confidence、impact scope&lt;/td>
 &lt;td>04 readiness、8.1 severity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision log&lt;/td>
 &lt;td>hypothesis、evidence、rollback condition&lt;/td>
 &lt;td>06 experiment、8 runbook&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Customer impact&lt;/td>
 &lt;td>user、tenant、feature、financial impact&lt;/td>
 &lt;td>8.10 stakeholder、SLO policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident timeline&lt;/td>
 &lt;td>發生、判讀、止血、恢復順序&lt;/td>
 &lt;td>runbook、handoff、PIR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PIR action item&lt;/td>
 &lt;td>缺口、owner、target state&lt;/td>
 &lt;td>reliability debt、signal governance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Automation log&lt;/td>
 &lt;td>bot action、approval、manual override&lt;/td>
 &lt;td>automation boundary、audit&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Incident intake 能揭露入口缺口。若客訴早於告警，回寫方向可能是 client-side monitoring、synthetic probe 或 support-to-incident workflow。&lt;/p>
&lt;p>Decision log 能揭露判讀缺口。若 IC 做決策時缺少 trace、data quality 或 rollback condition，回寫方向可能是 04 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、06 rollback rehearsal 或 runbook lifecycle。&lt;/p>
&lt;p>Customer impact 能揭露通訊與補償缺口。若影響範圍在事故後才算清楚，回寫方向可能是 impact assessment query、billing evidence 或 status page template。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>evidence write-back 的責任：把事故中產生的證據、決策與學習轉成上游改善</li>
<li>輸入：incident intake、decision log、customer impact、timeline、PIR action item</li>
<li>回寫面向：observability signal、telemetry data quality、verification scenario、runbook、automation boundary</li>
<li>欄位：finding、evidence、owner、target artifact、closure signal、review date</li>
<li>跟 4.20 的關係：事故證據缺口回寫成 evidence package 與資料品質改善</li>
<li>跟 6.23 的關係：事故學習回寫成新的驗證題目與 handoff evidence</li>
<li>反模式：PIR action item 停在待辦；事故證據沒有回到 dashboard / runbook；同類事故重複發生</li>
</ul>
<p>Incident evidence write-back 的核心是把事故學習轉成上游 artifact。事故是流程回寫點，會產生新的訊號需求、驗證題目、runbook 修訂與自動化邊界。</p>
<h2 id="概念定位">概念定位</h2>
<p>Incident evidence write-back 是事故處理回寫到可觀測性、可靠性驗證與操作流程的閉環，責任是讓事故學習變成可驗證改善。</p>
<p>這一頁處理的是事故後的交接。8.18 產生 intake evidence，8.19 保留 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a>，8.20 量化 customer impact；本章把這些材料轉成 04、06、08 內部可追蹤的改善 artifact。</p>
<p>Write-back 的價值在於避免同類事故只被記錄一次。PIR action item 若只停在待辦，下一次事故仍會遇到相同缺口；write-back 要把缺口落到 dashboard、alert、SLO、experiment、runbook 或 automation guardrail。</p>
<h2 id="案例中的回寫路徑">案例中的回寫路徑</h2>
<p>回寫不是抽象流程，必須能對應到具體事故。Cloudflare 2019 與 AWS S3 2017 提供了兩種常見回寫場景：快速擴散型事故與共享依賴型事故。</p>
<p>Cloudflare 2019 的關鍵缺口是規則成本在上線前不可見。回寫不是只寫「加強測試」，而是把 evidence 落到可執行控制面：04 的 rule-level CPU 訊號、06 的 rollout safety gate、08 的 decision log 與 write-back 閉環。這樣下次同類變更才會在推送前被攔下。</p>
<p>AWS S3 2017 的關鍵缺口是共享子系統恢復順序與通訊入口依賴。回寫重點是操作與通訊控制面，單一 bug 修復遠遠不夠：內部操作 guardrail、恢復順序驗證、主通道失效切換，以及對外敘事的證據對位。這些回寫會直接改變下次事故的可見性與節奏。</p>
<p>這兩個案例共同說明：好的回寫不是「多做一點」，而是把事故中的決策痛點轉成下一次能提早判讀的控制面。</p>
<h2 id="輸入材料">輸入材料</h2>
<p>Evidence write-back 的輸入來自事故期間已經建立的 artifact。每個 artifact 對應不同回寫方向。</p>
<table>
  <thead>
      <tr>
          <th>輸入</th>
          <th>提供內容</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident intake</td>
          <td>source、confidence、impact scope</td>
          <td>04 readiness、8.1 severity</td>
      </tr>
      <tr>
          <td>Decision log</td>
          <td>hypothesis、evidence、rollback condition</td>
          <td>06 experiment、8 runbook</td>
      </tr>
      <tr>
          <td>Customer impact</td>
          <td>user、tenant、feature、financial impact</td>
          <td>8.10 stakeholder、SLO policy</td>
      </tr>
      <tr>
          <td>Incident timeline</td>
          <td>發生、判讀、止血、恢復順序</td>
          <td>runbook、handoff、PIR</td>
      </tr>
      <tr>
          <td>PIR action item</td>
          <td>缺口、owner、target state</td>
          <td>reliability debt、signal governance</td>
      </tr>
      <tr>
          <td>Automation log</td>
          <td>bot action、approval、manual override</td>
          <td>automation boundary、audit</td>
      </tr>
  </tbody>
</table>
<p>Incident intake 能揭露入口缺口。若客訴早於告警，回寫方向可能是 client-side monitoring、synthetic probe 或 support-to-incident workflow。</p>
<p>Decision log 能揭露判讀缺口。若 IC 做決策時缺少 trace、data quality 或 rollback condition，回寫方向可能是 04 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、06 rollback rehearsal 或 runbook lifecycle。</p>
<p>Customer impact 能揭露通訊與補償缺口。若影響範圍在事故後才算清楚，回寫方向可能是 impact assessment query、billing evidence 或 status page template。</p>
<p>Incident timeline 能揭露節奏缺口。若 handoff、escalation 或 containment 花太久，回寫方向可能是 on-call drill、IC handoff 或 automation setup。</p>
<h2 id="失敗回寫的判讀訊號">失敗回寫的判讀訊號</h2>
<p>回寫最常失敗在「有 action item，沒有控制面」。當回寫只停在任務清單，下次事故通常會重演同樣判讀遲滯。</p>
<table>
  <thead>
      <tr>
          <th>判讀訊號</th>
          <th>失敗原因</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>下次事故仍從客訴才發現</td>
          <td>訊號缺口未回寫到 04</td>
          <td>把缺口落到 readiness / evidence package</td>
      </tr>
      <tr>
          <td>對外更新仍反覆改口</td>
          <td>決策與通訊未對位</td>
          <td>對外敘事變更強制連到 decision log</td>
      </tr>
      <tr>
          <td>同類 rollback 仍無門檻</td>
          <td>驗證缺口未回寫到 06</td>
          <td>把缺口轉成 experiment safety 與 steady state</td>
      </tr>
      <tr>
          <td>PIR 提到缺口但無追蹤結果</td>
          <td>action item 缺 closure signal</td>
          <td>補 closure signal 與 review date</td>
      </tr>
      <tr>
          <td>有修程式碼但流程沒變</td>
          <td>回寫停在實作層</td>
          <td>同步回寫 runbook、演練與 incident 路由</td>
      </tr>
  </tbody>
</table>
<p>這組訊號的用途是幫團隊辨識「回寫是否真的發生」。如果半年後同類事故的判讀速度沒有變快，代表回寫仍停在文件層，還沒進到控制面層。</p>
<h2 id="回寫欄位">回寫欄位</h2>
<p>Write-back 欄位的責任是把學習轉成可關閉工作。每個回寫項都要有目標 artifact 與 closure signal。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Finding</td>
          <td>說明事故揭露的缺口</td>
          <td>burn alert 缺少 tenant 維度</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>連到 decision log / query</td>
          <td>8.19 decision log #12</td>
      </tr>
      <tr>
          <td>Target artifact</td>
          <td>指定要修改的上游 artifact</td>
          <td>4.4 alert、6.20 experiment</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定負責角色</td>
          <td>service owner、platform owner</td>
      </tr>
      <tr>
          <td>Closure signal</td>
          <td>定義完成後如何驗證</td>
          <td>drill 通過、alert 在 game day 觸發</td>
      </tr>
      <tr>
          <td>Review date</td>
          <td>定義何時重新檢查</td>
          <td>下一次 release readiness</td>
      </tr>
  </tbody>
</table>
<p>Finding 欄位要描述控制面缺口。<code>checkout timeout</code> 是現象；<code>dependency timeout alert 缺少 tenant / region 維度</code> 才是可回寫缺口。</p>
<p>Target artifact 讓回寫有落點。缺口可以落到 04 dashboard、04 data quality、06 experiment、06 readiness、08 runbook、08 automation boundary 或 07 security control。</p>
<p>Closure signal 讓 action item 可驗證。<code>補監控</code> 不夠具體；<code>game day 中 vendor timeout 能在 5 分鐘內觸發 tenant-scoped alert</code> 才能關閉。</p>
<h2 id="回寫路由">回寫路由</h2>
<p>Evidence write-back 的路由要依缺口性質選擇上游。不同缺口需要不同 owner 與驗證方式。</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>回寫位置</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號缺口</td>
          <td>4.16 readiness、4.20 evidence package</td>
          <td>下次 intake 可直接引用 evidence</td>
      </tr>
      <tr>
          <td>資料品質缺口</td>
          <td>4.17 telemetry data quality</td>
          <td>dashboard 標示 freshness / gap</td>
      </tr>
      <tr>
          <td>驗證缺口</td>
          <td>6.20 experiment、6.23 handoff</td>
          <td>新 experiment evidence 通過</td>
      </tr>
      <tr>
          <td>穩態缺口</td>
          <td>6.22 steady state definition</td>
          <td>recovery complete 可量測</td>
      </tr>
      <tr>
          <td>Runbook 缺口</td>
          <td>8.16 runbook lifecycle</td>
          <td>drill 中 runbook 可執行</td>
      </tr>
      <tr>
          <td>自動化缺口</td>
          <td>8.21 automation boundary</td>
          <td>bot action 有 approval / audit</td>
      </tr>
      <tr>
          <td>資安證據缺口</td>
          <td>07 audit / security workflow</td>
          <td>chain of custody 可追蹤</td>
      </tr>
  </tbody>
</table>
<p>訊號缺口要回到 04。若事故證據需要人工跨三個系統拼接，應補 evidence package、dashboard、query、log schema 或 trace context。</p>
<p>驗證缺口要回到 06。若事故中某個失效模式從未演練，應新增 chaos、DR drill、rollback rehearsal 或 readiness review 題目。</p>
<p>Runbook 缺口要回到 08。若事故處置依賴臨場記憶，應更新 runbook lifecycle，並透過 game day 或 on-call drill 驗證。</p>
<p>資安證據缺口要回到 07。若事故涉及 audit log、PII、credential 或 authorization，write-back 需要保存證據鏈與權限治理。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Evidence write-back 的反模式通常來自把 PIR 當成結案文件。PIR 是輸入，write-back 才是讓系統變好的交付。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Action item 停在待辦</td>
          <td>有清單但沒有 target artifact</td>
          <td>指定 dashboard / runbook / experiment</td>
      </tr>
      <tr>
          <td>缺 closure signal</td>
          <td>完成與否靠主觀判斷</td>
          <td>定義可驗證門檻</td>
      </tr>
      <tr>
          <td>只修程式碼</td>
          <td>訊號、runbook、演練沒有同步更新</td>
          <td>同步回寫 04 / 06 / 08</td>
      </tr>
      <tr>
          <td>同類事故重複</td>
          <td>PIR 未轉成 shared pattern</td>
          <td>回寫 incident pattern library</td>
      </tr>
      <tr>
          <td>自動化無復盤</td>
          <td>bot 錯誤只被人工記住</td>
          <td>回寫 automation guardrail</td>
      </tr>
  </tbody>
</table>
<p>Action item 停在待辦會讓改善失去落點。每個 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a> 都需要 target artifact，否則 owner 很難知道要改哪個系統面。</p>
<p>只修程式碼會留下流程缺口。事故通常同時暴露 product bug、signal gap、verification gap 與 runbook gap；修程式碼只是其中一條路由。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>4.16 observability readiness：回寫事故中缺少的訊號</li>
<li>4.17 telemetry data quality：回寫資料品質限制</li>
<li>4.20 observability evidence package：補 evidence 欄位與保存格式</li>
<li>6.20 experiment safety：把事故型態轉成安全驗證題目</li>
<li>6.23 verification evidence handoff：保存新驗證題目的輸出格式</li>
<li>8.16 runbook lifecycle：把有效決策與缺口回寫 runbook</li>
<li>8.21 automation boundary：把 bot 行為與人工確認缺口回寫 guardrail</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 Reliability Debt Backlog</a>：事故教訓回寫成 reliability debt</li>
<li><a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">6.4 Chaos Testing</a>：事故暴露的弱點變成 chaos 演練新題目</li>
</ul>
]]></content:encoded></item><item><title>Pinterest</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/</guid><description>&lt;p>Pinterest 是視覺探索平台、capacity planning 與儲存架構的工程文章揭露大規模 data-heavy service 的可靠性挑戰。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Storage Capacity：HBase / TiDB 等 stateful 系統的 capacity model&lt;/li>
&lt;li>Cache Strategies：Memcache / Redis 大規模部署的 failure mode&lt;/li>
&lt;li>Scaling Patterns：visual search 等高運算服務的可靠性&lt;/li>
&lt;li>Migration Reliability：跨 storage backend migration 的零事故設計&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Storage Migration&lt;/td>
 &lt;td>HBase → TiDB 等大規模 migration 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache Reliability&lt;/td>
 &lt;td>hot key、thundering herd 的工程處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity Planning&lt;/td>
 &lt;td>data-heavy service 的容量預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ML Serving Resilience&lt;/td>
 &lt;td>推薦系統的可靠性需求&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Pinterest 這個案例在講的是資料密集型服務如何透過 storage migration 與容量規劃維持可用性。讀者先抓 HBase、TiDB、zero downtime migration 與 RocksDB 這些原語，再把它們視為資料平台演進的路徑。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當儲存後端需要退役或升級時，重點是如何在搬移過程中維持服務穩定，把資料搬過去只是其中一環。當推薦或搜尋系統吃到熱點流量時，cache 與 capacity 的設計要先保住查詢路徑，再處理最佳化。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否把 storage migration 拆成不中斷的階段&lt;/li>
&lt;li>能否指出 hot key 與 thundering herd 的風險位置&lt;/li>
&lt;li>能否讓 data platform 的容量模型跟業務成長對齊&lt;/li>
&lt;li>能否把 migration 成果寫成可重複的工程模式&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Pinterest 把資料平台演進和可靠性綁在一起，和 Shopify 的峰值準備、GitHub 的資料一致性、Meta 的大規模 storage 實踐都有對照價值。這頁最重要的訊息是：migration 是維持服務語義的持續變更，用搬家的心態做會忽略穩定性。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>HBase → TiDB migration 展示零停機遷移如何保住線上讀寫。&lt;/li>
&lt;li>RocksDB wide column database 代表新 storage backend 如何接手舊系統的壓力。&lt;/li>
&lt;li>cache strategies 讓熱點流量不直接壓垮主存儲。&lt;/li>
&lt;li>capacity planning 把資料密集型服務的擴容節奏固定下來。&lt;/li>
&lt;li>ML serving resilience 讓推薦系統在資料平台變動時仍能維持體感。&lt;/li>
&lt;li>zero-downtime migration 讓線上變更從一次性事件變成可管理流程。&lt;/li>
&lt;li>hot key mitigation 讓快取與查詢壓力不會一起炸開。&lt;/li>
&lt;li>storage backend migration 讓資料平台可以分階段換血。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">P1&lt;/a>&lt;/td>
 &lt;td>快取可靠性與容量驚奇&lt;/td>
 &lt;td>在命中率崩落時維持可回復節奏與容量緩衝&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/" data-link-title="Pinterest：Storage Migration 與 Data Infrastructure Reliability" data-link-desc="大規模儲存遷移的可靠性設計：用 dual-write、shadow read 與 staged cutover 讓 PB 級資料基礎設施變更可漸進、可驗證、可回退。">P2&lt;/a>&lt;/td>
 &lt;td>Storage Migration 與 Data Infrastructure&lt;/td>
 &lt;td>大規模儲存遷移的漸進驗證與 dual-write / shadow read&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/hbase-deprecation-at-pinterest-8a99e6c8e6b7">HBase Deprecation at Pinterest&lt;/a>：HBase 退役與新 storage 方向。&lt;/li>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/tidb-adoption-at-pinterest-1130ab787a10">TiDB Adoption at Pinterest&lt;/a>：TiDB 選型與 migration 脈絡。&lt;/li>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/online-data-migration-from-hbase-to-tidb-with-zero-downtime-43f0fb474b84">Online Data Migration from HBase to TiDB with Zero Downtime&lt;/a>：零停機遷移的具體實作。&lt;/li>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/building-pinterests-new-wide-column-database-using-rocksdb-f5277ee4e3d2">Building Pinterest’s new wide column database using RocksDB&lt;/a>：新 wide column database 的工程脈絡。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Pinterest 是視覺探索平台、capacity planning 與儲存架構的工程文章揭露大規模 data-heavy service 的可靠性挑戰。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Storage Capacity：HBase / TiDB 等 stateful 系統的 capacity model</li>
<li>Cache Strategies：Memcache / Redis 大規模部署的 failure mode</li>
<li>Scaling Patterns：visual search 等高運算服務的可靠性</li>
<li>Migration Reliability：跨 storage backend migration 的零事故設計</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage Migration</td>
          <td>HBase → TiDB 等大規模 migration 設計</td>
      </tr>
      <tr>
          <td>Cache Reliability</td>
          <td>hot key、thundering herd 的工程處理</td>
      </tr>
      <tr>
          <td>Capacity Planning</td>
          <td>data-heavy service 的容量預測</td>
      </tr>
      <tr>
          <td>ML Serving Resilience</td>
          <td>推薦系統的可靠性需求</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Pinterest 這個案例在講的是資料密集型服務如何透過 storage migration 與容量規劃維持可用性。讀者先抓 HBase、TiDB、zero downtime migration 與 RocksDB 這些原語，再把它們視為資料平台演進的路徑。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當儲存後端需要退役或升級時，重點是如何在搬移過程中維持服務穩定，把資料搬過去只是其中一環。當推薦或搜尋系統吃到熱點流量時，cache 與 capacity 的設計要先保住查詢路徑，再處理最佳化。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否把 storage migration 拆成不中斷的階段</li>
<li>能否指出 hot key 與 thundering herd 的風險位置</li>
<li>能否讓 data platform 的容量模型跟業務成長對齊</li>
<li>能否把 migration 成果寫成可重複的工程模式</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Pinterest 把資料平台演進和可靠性綁在一起，和 Shopify 的峰值準備、GitHub 的資料一致性、Meta 的大規模 storage 實踐都有對照價值。這頁最重要的訊息是：migration 是維持服務語義的持續變更，用搬家的心態做會忽略穩定性。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>HBase → TiDB migration 展示零停機遷移如何保住線上讀寫。</li>
<li>RocksDB wide column database 代表新 storage backend 如何接手舊系統的壓力。</li>
<li>cache strategies 讓熱點流量不直接壓垮主存儲。</li>
<li>capacity planning 把資料密集型服務的擴容節奏固定下來。</li>
<li>ML serving resilience 讓推薦系統在資料平台變動時仍能維持體感。</li>
<li>zero-downtime migration 讓線上變更從一次性事件變成可管理流程。</li>
<li>hot key mitigation 讓快取與查詢壓力不會一起炸開。</li>
<li>storage backend migration 讓資料平台可以分階段換血。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">P1</a></td>
          <td>快取可靠性與容量驚奇</td>
          <td>在命中率崩落時維持可回復節奏與容量緩衝</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/" data-link-title="Pinterest：Storage Migration 與 Data Infrastructure Reliability" data-link-desc="大規模儲存遷移的可靠性設計：用 dual-write、shadow read 與 staged cutover 讓 PB 級資料基礎設施變更可漸進、可驗證、可回退。">P2</a></td>
          <td>Storage Migration 與 Data Infrastructure</td>
          <td>大規模儲存遷移的漸進驗證與 dual-write / shadow read</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/pinterest-engineering/hbase-deprecation-at-pinterest-8a99e6c8e6b7">HBase Deprecation at Pinterest</a>：HBase 退役與新 storage 方向。</li>
<li><a href="https://medium.com/pinterest-engineering/tidb-adoption-at-pinterest-1130ab787a10">TiDB Adoption at Pinterest</a>：TiDB 選型與 migration 脈絡。</li>
<li><a href="https://medium.com/pinterest-engineering/online-data-migration-from-hbase-to-tidb-with-zero-downtime-43f0fb474b84">Online Data Migration from HBase to TiDB with Zero Downtime</a>：零停機遷移的具體實作。</li>
<li><a href="https://medium.com/pinterest-engineering/building-pinterests-new-wide-column-database-using-rocksdb-f5277ee4e3d2">Building Pinterest’s new wide column database using RocksDB</a>：新 wide column database 的工程脈絡。</li>
</ul>
]]></content:encoded></item><item><title>Reddit</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/</guid><description>&lt;p>Reddit 2023 Pi Day（3/14）的 314 分鐘事故是 Kubernetes 升級導致的事故、揭露 k8s 升級在大規模生產環境的隱性風險。Reddit engineering blog 公開 post-mortem 細節豐富。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>Kubernetes 升級風險：minor version 升級的 breaking change&lt;/li>
&lt;li>升級回滾困境：為何 k8s control plane 不能直接降版&lt;/li>
&lt;li>大規模 stateful workload 的特殊性：pod 重排對狀態服務的衝擊&lt;/li>
&lt;li>內部 IR 流程：Reddit 的 IR commander / scribe 結構公開度&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2023-03&lt;/td>
 &lt;td>Pi Day k8s 升級 314 分鐘&lt;/td>
 &lt;td>k8s upgrade、control plane 回滾困境&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Reddit 這個案例在講的是 Kubernetes 升級如何在大規模 stateful 工作負載上拉長事故。讀者先看懂控制平面升級、回滾限制與狀態服務的特性，再把 Pi Day outage 當成升級風險的具體樣本。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 control plane 進行升級時，最先要保住的是回滾空間與資料完整性。當 pod 重排碰到 stateful workload 時，恢復節奏就不能只看節點健康，而要看整個狀態層是否真的穩回來。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否判斷問題是在 k8s 升級還是 workload 本身&lt;/li>
&lt;li>能否把回滾限制與控制平面風險講清楚&lt;/li>
&lt;li>能否辨識 stateful workload 的額外恢復成本&lt;/li>
&lt;li>能否把 IR commander / scribe 的流程用在對外說明&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Reddit 和 GitHub、Heroku 的交集在於，它們都會把平台層變更直接反映成使用者可見的 outage。這頁最值得和 GCP 一起看，因為 Kubernetes 升級與 control plane 回滾問題，能很好地補足「服務自己沒有寫錯，但平台還是會出事」這個視角。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2023-03 Pi Day 314 分鐘事故是 k8s 升級與 stateful workload 互相放大的樣本。&lt;/li>
&lt;li>這類事件特別能看出 control plane 回滾為何比一般服務回滾更麻煩。&lt;/li>
&lt;li>IR commander / scribe 讓對外資訊流有固定節奏。&lt;/li>
&lt;li>k8s 升級風險和其他平台事故頁可以互相對照。&lt;/li>
&lt;li>stateful workload 的 pod 重排會把效能恢復拉長。&lt;/li>
&lt;li>control plane rollback 的限制讓升級決策必須更早做完。&lt;/li>
&lt;li>kube upgrade 是整個平台控制面的變更，用版本更新的心態處理會低估風險。&lt;/li>
&lt;li>stateful service 的 cold start 會把恢復時間拉長到使用者可感知的程度。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">RD1&lt;/a>&lt;/td>
 &lt;td>Kubernetes 升級事故&lt;/td>
 &lt;td>將平台升級變更納入事故分級與回退節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.redditstatus.com/">Reddit Status&lt;/a>：Reddit 狀態頁與 incident history。&lt;/li>
&lt;li>&lt;a href="https://www.redditstatus.com/history">Reddit Status - Incident History&lt;/a>：歷史事故與 uptime 檢視。&lt;/li>
&lt;li>&lt;a href="https://www.redditstatus.com/api">Reddit Status - API&lt;/a>：status page API 文件。&lt;/li>
&lt;li>&lt;a href="https://redditinc.com/blog/the-search-for-better-search-at-reddit">The Search for Better Search at Reddit&lt;/a>：Reddit 工程內容總入口之一，補基礎工程脈絡。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Reddit 2023 Pi Day（3/14）的 314 分鐘事故是 Kubernetes 升級導致的事故、揭露 k8s 升級在大規模生產環境的隱性風險。Reddit engineering blog 公開 post-mortem 細節豐富。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>Kubernetes 升級風險：minor version 升級的 breaking change</li>
<li>升級回滾困境：為何 k8s control plane 不能直接降版</li>
<li>大規模 stateful workload 的特殊性：pod 重排對狀態服務的衝擊</li>
<li>內部 IR 流程：Reddit 的 IR commander / scribe 結構公開度</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2023-03</td>
          <td>Pi Day k8s 升級 314 分鐘</td>
          <td>k8s upgrade、control plane 回滾困境</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Reddit 這個案例在講的是 Kubernetes 升級如何在大規模 stateful 工作負載上拉長事故。讀者先看懂控制平面升級、回滾限制與狀態服務的特性，再把 Pi Day outage 當成升級風險的具體樣本。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 control plane 進行升級時，最先要保住的是回滾空間與資料完整性。當 pod 重排碰到 stateful workload 時，恢復節奏就不能只看節點健康，而要看整個狀態層是否真的穩回來。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否判斷問題是在 k8s 升級還是 workload 本身</li>
<li>能否把回滾限制與控制平面風險講清楚</li>
<li>能否辨識 stateful workload 的額外恢復成本</li>
<li>能否把 IR commander / scribe 的流程用在對外說明</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Reddit 和 GitHub、Heroku 的交集在於，它們都會把平台層變更直接反映成使用者可見的 outage。這頁最值得和 GCP 一起看，因為 Kubernetes 升級與 control plane 回滾問題，能很好地補足「服務自己沒有寫錯，但平台還是會出事」這個視角。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2023-03 Pi Day 314 分鐘事故是 k8s 升級與 stateful workload 互相放大的樣本。</li>
<li>這類事件特別能看出 control plane 回滾為何比一般服務回滾更麻煩。</li>
<li>IR commander / scribe 讓對外資訊流有固定節奏。</li>
<li>k8s 升級風險和其他平台事故頁可以互相對照。</li>
<li>stateful workload 的 pod 重排會把效能恢復拉長。</li>
<li>control plane rollback 的限制讓升級決策必須更早做完。</li>
<li>kube upgrade 是整個平台控制面的變更，用版本更新的心態處理會低估風險。</li>
<li>stateful service 的 cold start 會把恢復時間拉長到使用者可感知的程度。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">RD1</a></td>
          <td>Kubernetes 升級事故</td>
          <td>將平台升級變更納入事故分級與回退節奏</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.redditstatus.com/">Reddit Status</a>：Reddit 狀態頁與 incident history。</li>
<li><a href="https://www.redditstatus.com/history">Reddit Status - Incident History</a>：歷史事故與 uptime 檢視。</li>
<li><a href="https://www.redditstatus.com/api">Reddit Status - API</a>：status page API 文件。</li>
<li><a href="https://redditinc.com/blog/the-search-for-better-search-at-reddit">The Search for Better Search at Reddit</a>：Reddit 工程內容總入口之一，補基礎工程脈絡。</li>
</ul>
]]></content:encoded></item><item><title>4.23 觀測查詢設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>觀測資料的讀寫不對稱：一種寫入路徑對應多種讀取路徑&lt;/li>
&lt;li>三種查詢模式：即席診斷、聚合趨勢、鑑識回溯&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering&lt;/a> 與查詢路由：hot / warm / cold 不只是成本分層、是查詢能力分層&lt;/li>
&lt;li>Pre-aggregation 策略：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 的使用情境與維護成本&lt;/li>
&lt;li>Query 資源治理：priority、queue 分離、timeout 差異化、cost estimation&lt;/li>
&lt;li>觀測領域的讀寫分離：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的特化應用&lt;/li>
&lt;li>反模式：把 raw log 當 OLAP 查、dashboard 查詢直打 raw storage 無 pre-aggregation、recording rule 跟 raw query 重複計算&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>觀測查詢設計是把「產生訊號之後怎麼被讀取」當成獨立的系統設計問題。觀測資料的寫入路徑（agent → collector → ingest → storage）在 &lt;a href="https://tarrragon.github.io/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&lt;/a> 處理；本章處理的是讀取路徑 — 從 storage 經 query engine 到 dashboard、alert 與即席查詢的資料流。&lt;/p>
&lt;p>寫入路徑的設計目標是吞吐穩定、schema 一致、成本可控；讀取路徑的設計目標是在不同的時間壓力下，用對的精度取回對的切面。兩者的效能瓶頸不同、擴展方向不同、治理責任也不同。把讀取當寫入的附屬處理，會在流量成長後遇到「寫入正常但查詢崩潰」的局面。&lt;/p>
&lt;h2 id="觀測資料的讀寫不對稱">觀測資料的讀寫不對稱&lt;/h2>
&lt;p>觀測資料有一個 application data 不常見的特性：同一份資料被多種完全不同的查詢形狀讀取，每種查詢的時間壓力、精度需求、結果形狀差距可以到三個數量級。&lt;/p>
&lt;p>寫入面相對單純。不管是 log、metric 還是 trace，寫入都是 append-only、schema 由產生端定義、吞吐由流量決定。寫入路徑的設計問題集中在 cardinality 控制（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）、pipeline 可靠性（&lt;a href="https://tarrragon.github.io/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&lt;/a>）與 sampling 策略。&lt;/p>
&lt;p>讀取面則至少有三種模式，各自有獨立的 SLA、索引需求與資源消耗模型。把三種模式混在同一個未分化的 query engine 裡，會在任何一種模式的負載增長時拖累其他模式。&lt;/p>
&lt;h2 id="三種查詢模式">三種查詢模式&lt;/h2>
&lt;h3 id="即席診斷">即席診斷&lt;/h3>
&lt;p>事故中的查詢，責任是在秒級內定位問題。&lt;/p>
&lt;p>查詢形狀是精確 filter + 短時間範圍：拿一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 查關聯事件、拿一個 error code 加 time window 撈錯誤樣本、拿一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 展開完整 span tree。&lt;/p>
&lt;p>對儲存的要求：需要 hot tier 的完整索引、完整精度、毫秒到秒級回應。即席查詢幾乎不命中 warm 或 cold tier — 事故通常發生在「現在」或「剛才」。&lt;/p>
&lt;p>資源特性：低頻（事故時才有）、單次掃描量小、但延遲要求最嚴格。事故中的每一秒等待都在消耗 MTTR。&lt;/p>
&lt;h3 id="聚合趨勢">聚合趨勢&lt;/h3>
&lt;p>Dashboard 跟 alert rule 的查詢，責任是提供持續的服務健康視圖。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>觀測資料的讀寫不對稱：一種寫入路徑對應多種讀取路徑</li>
<li>三種查詢模式：即席診斷、聚合趨勢、鑑識回溯</li>
<li><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 與查詢路由：hot / warm / cold 不只是成本分層、是查詢能力分層</li>
<li>Pre-aggregation 策略：<a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a>、<a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a>、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的使用情境與維護成本</li>
<li>Query 資源治理：priority、queue 分離、timeout 差異化、cost estimation</li>
<li>觀測領域的讀寫分離：<a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的特化應用</li>
<li>反模式：把 raw log 當 OLAP 查、dashboard 查詢直打 raw storage 無 pre-aggregation、recording rule 跟 raw query 重複計算</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>觀測查詢設計是把「產生訊號之後怎麼被讀取」當成獨立的系統設計問題。觀測資料的寫入路徑（agent → collector → ingest → storage）在 <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> 處理；本章處理的是讀取路徑 — 從 storage 經 query engine 到 dashboard、alert 與即席查詢的資料流。</p>
<p>寫入路徑的設計目標是吞吐穩定、schema 一致、成本可控；讀取路徑的設計目標是在不同的時間壓力下，用對的精度取回對的切面。兩者的效能瓶頸不同、擴展方向不同、治理責任也不同。把讀取當寫入的附屬處理，會在流量成長後遇到「寫入正常但查詢崩潰」的局面。</p>
<h2 id="觀測資料的讀寫不對稱">觀測資料的讀寫不對稱</h2>
<p>觀測資料有一個 application data 不常見的特性：同一份資料被多種完全不同的查詢形狀讀取，每種查詢的時間壓力、精度需求、結果形狀差距可以到三個數量級。</p>
<p>寫入面相對單純。不管是 log、metric 還是 trace，寫入都是 append-only、schema 由產生端定義、吞吐由流量決定。寫入路徑的設計問題集中在 cardinality 控制（<a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）、pipeline 可靠性（<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</a>）與 sampling 策略。</p>
<p>讀取面則至少有三種模式，各自有獨立的 SLA、索引需求與資源消耗模型。把三種模式混在同一個未分化的 query engine 裡，會在任何一種模式的負載增長時拖累其他模式。</p>
<h2 id="三種查詢模式">三種查詢模式</h2>
<h3 id="即席診斷">即席診斷</h3>
<p>事故中的查詢，責任是在秒級內定位問題。</p>
<p>查詢形狀是精確 filter + 短時間範圍：拿一個 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 查關聯事件、拿一個 error code 加 time window 撈錯誤樣本、拿一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 展開完整 span tree。</p>
<p>對儲存的要求：需要 hot tier 的完整索引、完整精度、毫秒到秒級回應。即席查詢幾乎不命中 warm 或 cold tier — 事故通常發生在「現在」或「剛才」。</p>
<p>資源特性：低頻（事故時才有）、單次掃描量小、但延遲要求最嚴格。事故中的每一秒等待都在消耗 MTTR。</p>
<h3 id="聚合趨勢">聚合趨勢</h3>
<p>Dashboard 跟 alert rule 的查詢，責任是提供持續的服務健康視圖。</p>
<p>查詢形狀是 group by + aggregation + 中等時間範圍：過去 5 分鐘的 error rate by service、過去 1 小時的 latency p99 by endpoint、過去 24 小時的 log volume by level。Dashboard 每 30 秒到 1 分鐘刷新，alert rule 每 1 到 5 分鐘 evaluate。</p>
<p>對儲存的要求：可以讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的預聚合資料，不需要完整精度。延遲容忍比即席查詢寬（秒級到十秒級），但查詢頻率比即席查詢高兩到三個數量級。</p>
<p>資源特性：高頻、穩定、佔 query engine 的常態負載大頭。一個 Grafana dashboard 有 20 個 panel、每 30 秒刷新一次 = 每分鐘 40 個查詢；十個團隊各自有 dashboard = 每分鐘 400 個背景查詢。</p>
<h3 id="鑑識回溯">鑑識回溯</h3>
<p>事後分析、合規稽核與根因調查的查詢，責任是在大時間範圍內還原完整脈絡。</p>
<p>查詢形狀是寬時間範圍 + 條件掃描：過去 30 天某 tenant 的所有 authentication failure、過去 90 天某 API 的 error 分布演變、某次事故前後 48 小時的完整 log 流。</p>
<p>對儲存的要求：會命中 warm 甚至 cold tier。完整性比延遲重要 — 漏掉一筆 audit log 比多等 30 秒更嚴重。可能需要 rehydrate（把 cold tier 歸檔資料暫時載回可查詢狀態）。</p>
<p>資源特性：低頻但單次掃描量極大。一個 cold tier 的全量掃描可能佔用 query engine 數分鐘的計算資源。</p>
<h3 id="三種模式的設計衝突">三種模式的設計衝突</h3>
<p>三種模式搶同一個 query engine 時，聚合趨勢的穩定高頻負載會佔滿常態資源、擠壓即席診斷的突發需求；鑑識回溯的大範圍掃描會吃掉臨時資源、拖慢同時進行的即席查詢。</p>
<p>事故中是衝突最嚴重的時刻：incident commander 在做即席診斷、dashboard 在高頻刷新聚合趨勢、事後調查團隊可能同時在做鑑識回溯。三種負載同時打在同一個 query engine 上，誰先退讓取決於 query 資源治理的設計。</p>
<h2 id="storage-tiering-與查詢路由">Storage tiering 與查詢路由</h2>
<p><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 在讀取路徑上的責任不只是降低儲存成本，而是為不同時間範圍的查詢提供對應的查詢能力。每一層的儲存介質、索引密度、資料精度共同決定該層能回答什麼問題。</p>
<h3 id="每一層的查詢能力">每一層的查詢能力</h3>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>查詢延遲</th>
          <th>可用索引</th>
          <th>資料精度</th>
          <th>適合的查詢模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>毫秒到秒</td>
          <td>完整結構化索引 + 全文索引</td>
          <td>原始精度</td>
          <td>即席診斷</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>秒到十秒</td>
          <td>結構化索引（可能移除低價值欄位索引）</td>
          <td>原始或輕度 rollup</td>
          <td>聚合趨勢</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>十秒到分鐘</td>
          <td>最小索引（timestamp + service + tenant）</td>
          <td>rollup 或歸檔</td>
          <td>鑑識回溯</td>
      </tr>
  </tbody>
</table>
<p>查詢跨越 tier 邊界時，回應時間由最慢的 tier 決定。Dashboard 時間範圍從「最近 1 小時」（全部 hot）拉到「最近 30 天」（hot + warm + cold），查詢延遲可能從毫秒跳到分鐘。這個延遲跳變需要在 dashboard UI 上提示使用者。</p>
<h3 id="查詢路由的設計">查詢路由的設計</h3>
<p>查詢路由的責任是根據查詢的時間範圍跟精度需求，自動選擇最合適的 tier 跟資料精度。</p>
<ul>
<li>時間範圍在 hot tier 內：直接查 raw data，完整精度。</li>
<li>時間範圍跨越 hot 跟 warm：hot 部分查 raw data、warm 部分查 rollup series，query engine 負責拼接。</li>
<li>時間範圍延伸到 cold tier：cold 部分需要 rehydrate 或走 object storage 查詢路徑，延遲大幅增加。</li>
</ul>
<p>查詢路由的透明度影響使用者信任。使用者需要知道目前看到的資料是什麼精度、來自哪一層、是否有 freshness lag。Grafana 的 annotation 機制可以在 dashboard 上標示 tier 邊界跟精度切換點，避免使用者把精度變化誤讀成服務異常。</p>
<h3 id="rehydrate-的操作成本">Rehydrate 的操作成本</h3>
<p>Cold tier 的資料通常儲存在 object storage（S3、GCS、Azure Blob），查詢前需要 rehydrate — 把資料從歸檔格式解壓、重建索引、載入到可查詢狀態。這個操作有時間成本（分鐘到小時）、儲存成本（臨時佔用 hot/warm 空間）跟計算成本（CPU 用在解壓跟索引重建）。</p>
<p>Rehydrate 是事故事後分析跟合規稽核的常見操作。設計 tiering 時要把 rehydrate 的 SLA（多久可以完成）、容量（同時可以 rehydrate 多少資料）跟觸發方式（手動 / API / 自動 policy）納入規劃。</p>
<h2 id="pre-aggregation-策略">Pre-aggregation 策略</h2>
<p>Pre-aggregation 是把讀取時的計算成本轉移到寫入時的策略。觀測領域有三種常見的 pre-aggregation 機制，適用場景跟維護成本不同。</p>
<h3 id="recording-rule">Recording rule</h3>
<p><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a> 在 TSDB 層定期執行 query expression，把聚合結果寫成新 series。適合 metrics 的高頻聚合查詢（SLO burn rate、error ratio、跨服務 latency summary）。</p>
<p>Recording rule 的維護成本集中在規則增長後的管理。數百條 recording rule 需要命名慣例、版本控制、執行時間監控（rule evaluation duration）與定期審計（是否有 rule 不再被 dashboard 或 alert 引用）。</p>
<h3 id="log-to-metric-轉換">Log-to-metric 轉換</h3>
<p>在 collector 端把高頻 log pattern 轉成 metric。適合「從 log 衍生的聚合查詢」— 例如把 <code>level=error</code> 的 log 計數轉成 error_log_total counter，把 specific exception 的出現率轉成 gauge。</p>
<p>Log-to-metric 的好處是讓 dashboard 讀 metric 而非重掃 log volume。維護成本在於 collector 配置要跟 log schema 保持同步 — log 的 field name 改了，轉換規則沒跟著改，metric 會靜默歸零。</p>
<h3 id="rollup--downsampling">Rollup / downsampling</h3>
<p><a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">Rollup</a> 把高精度時間序列聚合成低精度版本。適合長時間範圍的趨勢查詢（90 天 error rate 趨勢、capacity planning 的年度成長曲線）。</p>
<p>Rollup 的設計關鍵是聚合函數必須按 metric type 選擇。Counter 用 sum、gauge 用 average（或 min/max 保留極端值）、histogram 需要保留 bucket boundary 而非做 average（否則 percentile 計算會失真）。混用聚合函數是 rollup 最常見的 silent data corruption。</p>
<h3 id="pre-aggregation-的維護成本">Pre-aggregation 的維護成本</h3>
<p>Pre-aggregation 不是免費的。每一條 recording rule、每一個 log-to-metric 轉換、每一層 rollup 都需要：</p>
<ul>
<li><strong>儲存空間</strong>：預聚合結果本身佔用 series 或 index 空間，增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> 負擔。</li>
<li><strong>計算資源</strong>：定期執行聚合需要 CPU，rule evaluation lag 會讓 dashboard 看到過期資料。</li>
<li><strong>配置維護</strong>：規則需要跟 schema、label、service 保持同步，漂移會靜默產生錯誤資料。</li>
<li><strong>除錯成本</strong>：dashboard 讀的是 recording rule 輸出，事故時可能需要同時查 raw data 驗證 recording rule 是否正確。</li>
</ul>
<p>設計時的判準是：預聚合的讀取節省是否大於維護成本。高頻讀取（dashboard auto-refresh、alert evaluation）的聚合計算值得 pre-aggregation；低頻讀取（月度報表、偶發 ad-hoc query）直接查 raw data 更簡單。</p>
<h2 id="query-資源治理">Query 資源治理</h2>
<p>觀測平台的 query engine 是共用資源，需要顯式的治理機制避免單一查詢類型或單一使用者耗盡資源。</p>
<h3 id="query-priority-與排程">Query priority 與排程</h3>
<p>Query engine 需要知道每個查詢的優先級，在資源不足時讓高優先查詢先執行。</p>
<table>
  <thead>
      <tr>
          <th>查詢類型</th>
          <th>建議優先級</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert evaluate</td>
          <td>最高</td>
          <td>告警延遲直接影響 MTTD，不可因其他查詢排隊而漏發</td>
      </tr>
      <tr>
          <td>即席診斷</td>
          <td>高</td>
          <td>事故中的查詢，每秒延遲消耗 MTTR</td>
      </tr>
      <tr>
          <td>Dashboard 刷新</td>
          <td>中</td>
          <td>穩定背景負載，短暫延遲不影響決策品質</td>
      </tr>
      <tr>
          <td>鑑識回溯</td>
          <td>低</td>
          <td>延遲容忍高，可排程到低負載時段執行</td>
      </tr>
      <tr>
          <td>Ad-hoc 探索</td>
          <td>最低</td>
          <td>非事故的探索性查詢，可被其他類型搶佔</td>
      </tr>
  </tbody>
</table>
<h3 id="query-timeout-差異化">Query timeout 差異化</h3>
<p>不同查詢類型設不同的 timeout：alert evaluation 設短 timeout（30 秒到 1 分鐘，跑不完說明 query 有問題）、即席診斷設中等 timeout（1 到 5 分鐘）、鑑識回溯允許較長 timeout（10 到 30 分鐘）。統一 timeout 會讓鑑識查詢被過早截斷、或讓 alert evaluation 等太久。</p>
<h3 id="query-cost-estimation">Query cost estimation</h3>
<p>在查詢執行前估算掃描量（掃描的 series 數、time range、shard 數），超過閾值的查詢被拒絕或降級。避免單一 heavy query（例：跨所有 service 的 90 天 full-resolution 聚合）拖垮 query engine。</p>
<p>Query cost estimation 對使用者的回饋要足夠清楚。拒絕查詢時要說明「這個查詢預計掃描 N 條 series × M 天，超過單次查詢上限；請縮小時間範圍或增加 filter 條件」，而不是只回 timeout 或 500 error。</p>
<h3 id="query-cache">Query cache</h3>
<p>聚合趨勢查詢的特徵是高頻重複 — 同一個 dashboard panel 每 30 秒查一次，查詢的時間範圍大部分重疊。Query cache 在 query-frontend 層快取最近的聯合結果，下一次刷新只需要增量計算新進的資料區間。</p>
<p>Thanos Query Frontend、Mimir Query Frontend、Grafana Cloud 的 query splitting + caching 都實作這個模式。Cache 的命中率直接影響 query engine 負載 — 高命中率讓 query engine 的常態負載下降、留更多資源給即席查詢。</p>
<h2 id="觀測領域的讀寫分離cqrs-的特化應用">觀測領域的讀寫分離：CQRS 的特化應用</h2>
<p>觀測查詢設計的底層問題是讀寫不對稱 — 寫入跟讀取的形狀、頻率、SLA 都不同，單一模型無法同時服務。這個問題在 application data 層有成熟的設計框架：<a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a>。觀測領域面對的是同一類不對稱，但不對稱的程度更極端，實作層級也不同。</p>
<h3 id="觀測場景的不對稱比-application-更極端">觀測場景的不對稱比 application 更極端</h3>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS 知識卡</a>描述了讀寫不對稱的三個維度（形狀、頻率、SLA）。觀測場景在這三個維度上都比典型 application 更極端：</p>
<p><strong>形狀不對稱</strong>：application 的 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 通常是一到兩種（列表頁、報表）。觀測的讀取面至少三種：即席診斷要精確 filter + 完整精度、聚合趨勢要 group by + pre-aggregated、鑑識回溯要寬範圍 + 完整性優先。三種形狀對索引、精度、儲存層的需求互斥。</p>
<p><strong>頻率不對稱</strong>：application 的讀寫比通常在 10:1 到 100:1 之間。觀測的 dashboard 每 30 秒刷新一次、alert 每分鐘 evaluate、十個團隊各自有 dashboard — 讀取頻率可以到寫入的千倍以上，而且是持續穩定的背景負載而非突發。</p>
<p><strong>SLA 不對稱</strong>：application CQRS 的讀寫 SLA 差距通常在同一個數量級（毫秒 vs 數百毫秒）。觀測的三種讀取模式 SLA 跨三個數量級 — 即席診斷要求毫秒到秒級、聚合趨勢容忍秒到十秒級、鑑識回溯容忍分鐘級。</p>
<h3 id="觀測領域怎麼實作讀寫分離">觀測領域怎麼實作讀寫分離</h3>
<p>CQRS 在 application 層透過 event handler、projector、read store 實作。觀測領域用自己的 first-class 機制做同樣的事：</p>
<table>
  <thead>
      <tr>
          <th>CQRS 概念</th>
          <th>觀測領域的對應</th>
          <th>設計責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Write model</td>
          <td>Raw series / log / span — append-only 寫入</td>
          <td>Schema 穩定、吞吐</td>
      </tr>
      <tr>
          <td>Read model</td>
          <td><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a>、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a>、log-to-metric 轉換</td>
          <td>讀取最佳化</td>
      </tr>
      <tr>
          <td>Projection</td>
          <td>Collector 端的 aggregation / enrichment / routing</td>
          <td>寫入到讀取模型的轉換</td>
      </tr>
      <tr>
          <td>Event 同步延遲</td>
          <td>Recording rule evaluation lag、rollup delay、buffer freshness lag</td>
          <td>最終一致性的延遲窗口</td>
      </tr>
      <tr>
          <td>多 read store</td>
          <td><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a>（hot / warm / cold 各自支援不同查詢模式）</td>
          <td>不同 SLA 的讀取走不同儲存層</td>
      </tr>
  </tbody>
</table>
<h3 id="cqrs-的代價在觀測領域同樣存在">CQRS 的代價在觀測領域同樣存在</h3>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS 知識卡</a>列出的三項代價（最終一致性、同步可靠性、多模型維護）在觀測場景都找得到對應：</p>
<p><strong>最終一致性</strong>：Recording rule 每 N 秒 evaluate 一次，dashboard 看到的聚合結果落後 raw data。Rollup 的延遲更長。事故中 incident commander 看 dashboard 做決策時，需要知道資料的 freshness — 這就是 CQRS 的 read model 延遲在觀測領域的具體表現。</p>
<p><strong>同步可靠性</strong>：Recording rule evaluation 本身可能失敗（expression 太重跑不完、TSDB 暫時不可用）。Log-to-metric 轉換可能因 schema 漂移而靜默歸零。這些同步失敗跟 application CQRS 的 projector 失敗是同一類問題 — read model 看起來有資料但其實是過期的。</p>
<p><strong>多模型維護</strong>：Metric schema 變更後，raw series、recording rule、rollup、dashboard query 都需要同步更新。Recording rule 引用的 label name 改了沒跟著改，aggregation 結果會靜默錯誤。這跟 application 的「schema migration 要同時更新 write model 跟所有 read model」是同一個維護負擔。</p>
<h3 id="術語邊界">術語邊界</h3>
<p>觀測領域的讀寫分離跟 CQRS 概念對應，但在業界溝通中直接說「log 的 CQRS」或「metrics 的 CQRS」會造成混淆。觀測領域有自己的 first-class 術語（recording rule、rollup、tiering、query routing），跟 application CQRS 的術語（command、query、projection、read model）平行但不互通。</p>
<p>理解 CQRS 的讀者可以把觀測查詢設計視為「infrastructure-level 的讀寫分離」，同樣的設計原則（分離的動機、最終一致性的代價、多模型維護的負擔）在不同層級重複出現。但設計決策時要用觀測領域的術語，把 recording rule 跟 rollup 當第一等公民，而非 CQRS 的衍生品。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀觀測查詢設計時，先看三種查詢模式是否有對應的資源與資料形狀，再看 pre-aggregation 跟 tiering 是否對齊實際查詢負載。</p>
<p>重點訊號包括：</p>
<ul>
<li>即席查詢在事故中的延遲是否在秒級以內</li>
<li>Dashboard 刷新是否佔用過多 query engine 資源</li>
<li>長時間範圍查詢是否有 rollup / recording rule 支撐</li>
<li>Storage tiering 的查詢路由是否對使用者透明</li>
<li>Alert evaluation 是否有最高 query priority</li>
<li>Pre-aggregation 規則是否跟 schema 保持同步</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Dashboard 載入時間持續退化、panel timeout 增加</li>
<li>Alert rule evaluation duration 成長、偶發 missed evaluation</li>
<li>事故中即席查詢被 dashboard 背景負載擠壓</li>
<li>長時間範圍的查詢精度突變但使用者不知道</li>
<li>Recording rule 輸出跟 raw query 結果不一致</li>
<li>Rehydrate 需求頻繁但沒有預設流程</li>
<li>Query engine CPU 被少數 heavy query 佔滿</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Raw log 當 OLAP 查</td>
          <td>聚合查詢掃 TB 級 log、timeout</td>
          <td>用 log-to-metric 轉換把常用聚合推到 metric 層</td>
      </tr>
      <tr>
          <td>Dashboard 直打 raw storage</td>
          <td>Panel 載入慢、query engine 過載</td>
          <td>用 recording rule / rollup 支撐高頻 panel</td>
      </tr>
      <tr>
          <td>Recording rule 跟 raw query 重複</td>
          <td>同一個指標有兩條查詢路徑、數值不一致</td>
          <td>統一入口：dashboard 讀 recording rule、ad-hoc 讀 raw</td>
      </tr>
      <tr>
          <td>所有查詢同一個 priority</td>
          <td>Alert 被 dashboard 查詢排隊延遲</td>
          <td>Query priority 分級、alert evaluation 最高</td>
      </tr>
      <tr>
          <td>Tier 邊界對使用者不透明</td>
          <td>拉長時間範圍時數值突變但不知為何</td>
          <td>Dashboard 標示 tier 邊界跟精度切換</td>
      </tr>
      <tr>
          <td>Rollup 聚合函數混用</td>
          <td>Histogram percentile 在長時間視圖被壓平</td>
          <td>按 metric type 指定聚合函數、histogram 保留 bucket</td>
      </tr>
      <tr>
          <td>所有訊號同一個 tier 邊界</td>
          <td>高價值訊號過早退化、低價值訊號佔 hot</td>
          <td>依訊號優先級設差異化 tier 邊界</td>
      </tr>
  </tbody>
</table>
<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>：log 的即席 / 聚合 / 鑑識三種查詢模式細節</li>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：metrics 的 recording rule 與 rollup 設計</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 / cost</a>：storage tiering 對查詢能力的影響</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 的延伸</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>：query 資源的成本歸屬</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：pre-aggregation 與 raw data 的一致性驗證</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：query 資源治理的 ownership</li>
<li><a href="/blog/monitoring/04-collector/read-write-separation/" data-link-title="讀寫分離與查詢擴展" data-link-desc="Monitor 在 PostgreSQL 層之後的讀寫競爭問題、Read Replica 分離策略、CQRS 判讀訊號">Monitoring 讀寫分離</a>：Monitor 專案的讀寫分離具體應用</li>
</ul>
]]></content:encoded></item><item><title>MySQL PITR + Backup Strategy：備份不是「拷貝資料」、是 N 點任意 restore 的能力</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/pitr-backup/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/pitr-backup/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>backup + PITR&lt;/em> — 不是「拷貝資料」、是「N 點任意 restore 的能力」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>「我們每天 mysqldump 一次、放 S3、沒問題吧」是個常見錯誤。問「能不能 restore 到 5 分鐘前」、答案會是 &lt;em>不能&lt;/em>。Dump-based backup 只能 restore 到 &lt;em>dump 那個瞬間&lt;/em>、5 分鐘前的事故無法 recover、必須等下次 dump。&lt;/p>
&lt;p>&lt;strong>真正的 backup strategy 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/point-in-time-recovery/" data-link-title="Point-in-Time Recovery" data-link-desc="說明如何用完整備份加上後續變更日誌，把資料庫還原到任意時間點">PITR（point-in-time recovery）&lt;/a>&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;em>能 restore 到任意過去時間點&lt;/em>（RPO 取決於 binlog flush 頻率、可接近 0）&lt;/li>
&lt;li>由 &lt;em>full backup 基線&lt;/em> + &lt;em>binlog 連續流&lt;/em>（從 backup 點到目標時間點的 incremental delta）組成&lt;/li>
&lt;li>Restore 過程：先 restore full backup → 再 apply binlog 到目標 timestamp 或 GTID&lt;/li>
&lt;/ul>
&lt;p>這篇 deep article 把 backup &lt;em>拆解成能力&lt;/em>、然後展開達到此能力需要的工具鏈跟工程紀律。&lt;/p>
&lt;h2 id="backup-三層責任">Backup 三層責任&lt;/h2>
&lt;p>PITR 的 &lt;em>能力&lt;/em> 由三層工程責任達成、任一層失效則 PITR 不成立：&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">Layer 1: Full Backup（基線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ (mysqldump / XtraBackup / MyDumper / LVM snapshot / EBS snapshot)
&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">Layer 2: Binlog Stream（incremental）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> ↓ (sync_binlog=1 + binlog 持續流到 backup storage)
&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">Layer 3: Restore + Replay 流程
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> (能 restore full + 能 apply binlog 到目標時間點)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層的 &lt;em>backup&lt;/em> 不夠 — 必須有 &lt;em>測試 restore 流程&lt;/em> 才算真的有 backup。「dump 在 S3」加「沒有 verified restore」= no backup。&lt;/p>
&lt;h2 id="tool-1mysqldump--邏輯備份最廣容最慢">Tool 1：mysqldump — 邏輯備份、最廣容、最慢&lt;/h2>





&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">mysqldump --single-transaction --master-data&lt;span class="o">=&lt;/span>&lt;span class="m">2&lt;/span> --gtid-purged&lt;span class="o">=&lt;/span>ON &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --triggers --routines --events &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> --all-databases &amp;gt; full-backup.sql&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>輸出&lt;/strong>：SQL statement、純文字、可 grep / 編輯。&lt;/p>
&lt;p>&lt;strong>Trade-off&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>優點：跨 MySQL 版本（5.7 → 8.0 也讀）、跨 cloud / 跨 OS、可選 dump 部分 table&lt;/li>
&lt;li>缺點：&lt;em>極慢&lt;/em>（rebuild 整 DB 從 SQL execute）、大 DB（&amp;gt; 100 GB）不適用、restore 時長 hours+&lt;/li>
&lt;li>&lt;code>--single-transaction&lt;/code>：InnoDB only、用 REPEATABLE READ 拿 consistent snapshot、不 lock 表&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>適合&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>backup + PITR</em> — 不是「拷貝資料」、是「N 點任意 restore 的能力」。</p></blockquote>
<hr>
<p>「我們每天 mysqldump 一次、放 S3、沒問題吧」是個常見錯誤。問「能不能 restore 到 5 分鐘前」、答案會是 <em>不能</em>。Dump-based backup 只能 restore 到 <em>dump 那個瞬間</em>、5 分鐘前的事故無法 recover、必須等下次 dump。</p>
<p><strong>真正的 backup strategy 是 <a href="/blog/backend/knowledge-cards/point-in-time-recovery/" data-link-title="Point-in-Time Recovery" data-link-desc="說明如何用完整備份加上後續變更日誌，把資料庫還原到任意時間點">PITR（point-in-time recovery）</a></strong>：</p>
<ul>
<li><em>能 restore 到任意過去時間點</em>（RPO 取決於 binlog flush 頻率、可接近 0）</li>
<li>由 <em>full backup 基線</em> + <em>binlog 連續流</em>（從 backup 點到目標時間點的 incremental delta）組成</li>
<li>Restore 過程：先 restore full backup → 再 apply binlog 到目標 timestamp 或 GTID</li>
</ul>
<p>這篇 deep article 把 backup <em>拆解成能力</em>、然後展開達到此能力需要的工具鏈跟工程紀律。</p>
<h2 id="backup-三層責任">Backup 三層責任</h2>
<p>PITR 的 <em>能力</em> 由三層工程責任達成、任一層失效則 PITR 不成立：</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">Layer 1: Full Backup（基線）
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓     (mysqldump / XtraBackup / MyDumper / LVM snapshot / EBS snapshot)
</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">Layer 2: Binlog Stream（incremental）
</span></span><span class="line"><span class="ln">5</span><span class="cl">   ↓     (sync_binlog=1 + binlog 持續流到 backup storage)
</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">Layer 3: Restore + Replay 流程
</span></span><span class="line"><span class="ln">8</span><span class="cl">         (能 restore full + 能 apply binlog 到目標時間點)</span></span></code></pre></div><p>每層的 <em>backup</em> 不夠 — 必須有 <em>測試 restore 流程</em> 才算真的有 backup。「dump 在 S3」加「沒有 verified restore」= no backup。</p>
<h2 id="tool-1mysqldump--邏輯備份最廣容最慢">Tool 1：mysqldump — 邏輯備份、最廣容、最慢</h2>





<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">mysqldump --single-transaction --master-data<span class="o">=</span><span class="m">2</span> --gtid-purged<span class="o">=</span>ON <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --triggers --routines --events <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --all-databases &gt; full-backup.sql</span></span></code></pre></div><p><strong>輸出</strong>：SQL statement、純文字、可 grep / 編輯。</p>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>優點：跨 MySQL 版本（5.7 → 8.0 也讀）、跨 cloud / 跨 OS、可選 dump 部分 table</li>
<li>缺點：<em>極慢</em>（rebuild 整 DB 從 SQL execute）、大 DB（&gt; 100 GB）不適用、restore 時長 hours+</li>
<li><code>--single-transaction</code>：InnoDB only、用 REPEATABLE READ 拿 consistent snapshot、不 lock 表</li>
</ul>
<p><strong>適合</strong>：</p>
<ul>
<li>&lt; 100 GB DB</li>
<li>Schema dump（migration / 給 dev clone DB）</li>
<li>跨版本 migrate</li>
<li>配 binlog 做 PITR baseline</li>
</ul>
<p><strong>不適合</strong>：</p>
<ul>
<li>
<blockquote>
<p>500 GB DB（restore 跑 days）</p></blockquote>
</li>
<li>高吞吐 production（dump 跑時 hold MVCC read view、bloat）</li>
</ul>
<h2 id="tool-2percona-xtrabackup--物理備份快production-標準">Tool 2：Percona XtraBackup — 物理備份、快、production 標準</h2>





<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">xtrabackup --backup --target-dir<span class="o">=</span>/backup/full-2026-05-19 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --user<span class="o">=</span>backup --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --slave-info --safe-slave-backup
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Prepare（apply 內部 redo log、變成可 restore 狀態）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">xtrabackup --prepare --target-dir<span class="o">=</span>/backup/full-2026-05-19</span></span></code></pre></div><p><strong>輸出</strong>：InnoDB 資料檔案的 binary copy。</p>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>優點：<em>極快</em>（直接 copy file、無 SQL execute）、適合 TB-scale DB、restore 跑時間跟 copy file 同</li>
<li>缺點：MySQL 版本綁定（XtraBackup 8.0 不能 restore 5.7 backup）、有 storage engine 限制（只 InnoDB）</li>
<li><em>Incremental backup</em> 支援：基於 LSN（log sequence number）只 copy 變更 page</li>
</ul>
<p><strong>Incremental flow</strong>：</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"># Day 1: Full backup</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">xtrabackup --backup --target-dir<span class="o">=</span>/backup/full-day1
</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"># Day 2: Incremental（only changes since day 1）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">xtrabackup --backup --target-dir<span class="o">=</span>/backup/inc-day2 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --incremental-basedir<span class="o">=</span>/backup/full-day1
</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"># Restore: Apply incremental on top of full</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">xtrabackup --prepare --apply-log-only --target-dir<span class="o">=</span>/backup/full-day1
</span></span><span class="line"><span class="ln">10</span><span class="cl">xtrabackup --prepare --apply-log-only --target-dir<span class="o">=</span>/backup/full-day1 <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --incremental-dir<span class="o">=</span>/backup/inc-day2
</span></span><span class="line"><span class="ln">12</span><span class="cl">xtrabackup --prepare --target-dir<span class="o">=</span>/backup/full-day1</span></span></code></pre></div><p><strong>適合</strong>：</p>
<ul>
<li>
<blockquote>
<p>100 GB production DB</p></blockquote>
</li>
<li>每日 incremental + 週一次 full（典型 enterprise schedule）</li>
<li>從自管 MySQL 遷 cloud（XtraBackup + rsync 到 cloud restore）</li>
</ul>
<p><strong>不適合</strong>：</p>
<ul>
<li>Schema-only dump（用 mysqldump 更簡單）</li>
<li>跨 major version restore</li>
</ul>
<h2 id="tool-3mydumper--並行邏輯備份">Tool 3：MyDumper — 並行邏輯備份</h2>





<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">mydumper --user<span class="o">=</span>backup --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --threads<span class="o">=</span><span class="m">8</span> --rows<span class="o">=</span><span class="m">100000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --outputdir<span class="o">=</span>/backup/mydumper-2026-05-19 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --less-locking</span></span></code></pre></div><p><strong>輸出</strong>：每張 table 一個 <code>.sql</code> file（schema） + 多個 chunked <code>.dat</code> file（資料）。</p>
<p><strong>Trade-off</strong>：</p>
<ul>
<li>優點：<em>並行 dump</em>（per-table thread）、比 mysqldump 快 5-10x、可恢復斷點（resume）</li>
<li>缺點：tooling 不如 mysqldump 普及、需要單獨裝</li>
<li>對應的 <code>myloader</code> restore：也並行、比 mysqldump restore 快 5-10x</li>
</ul>
<p><strong>適合</strong>：</p>
<ul>
<li>100 GB - 1 TB 範圍</li>
<li>中型 production、想要邏輯備份的可讀性 + 並行加速</li>
</ul>
<h2 id="tool-4lvm--ebs-snapshot--物理-file-system-層">Tool 4：LVM / EBS Snapshot — 物理 file system 層</h2>





<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. Freeze MySQL（讓 write 暫停）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysql&gt; FLUSH TABLES WITH READ LOCK<span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 2. Trigger snapshot（EBS / LVM）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">aws ec2 create-snapshot --volume-id vol-xxx --description <span class="s2">&#34;mysql-2026-05-19&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 3. Unfreeze</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">mysql&gt; UNLOCK TABLES<span class="p">;</span></span></span></code></pre></div><p><strong>Trade-off</strong>：</p>
<ul>
<li>優點：超快（file system 層）、適合 <em>VM-based MySQL</em>（EC2 / on-prem）</li>
<li>缺點：必須 <em>暫停 write</em>（短時間 lock）、不能跨 OS / cloud 移植</li>
<li>AWS RDS / Aurora 全部走這條路（自動 snapshot）</li>
</ul>
<p><strong>適合</strong>：</p>
<ul>
<li>AWS RDS / Aurora（自動）</li>
<li>自管 MySQL on EC2 with EBS（EBS snapshot 結合 mysql freeze）</li>
<li>大 DB 想要 fast backup + fast restore</li>
</ul>
<h2 id="binlog-based-pitr">Binlog-based PITR</h2>
<p>Full backup 加上 binlog 才能達到 PITR。Binlog 是 MySQL replication / CDC / PITR 共用的 source。</p>
<p><strong>配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[mysqld]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">log_bin</span> <span class="o">=</span> <span class="s">mysql-bin</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">binlog_format</span> <span class="o">=</span> <span class="s">ROW                  # ROW 必須</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">binlog_row_image</span> <span class="o">=</span> <span class="s">FULL              # 完整 row image</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">sync_binlog</span> <span class="o">=</span> <span class="s">1                      # 每次 commit fsync binlog（zero loss）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">binlog_expire_logs_seconds</span> <span class="o">=</span> <span class="s">1209600 # 14 天 retention（依需求調）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">gtid_mode</span> <span class="o">=</span> <span class="s">ON                       # GTID 必須、PITR 用 GTID 識別 transaction</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="na">enforce_gtid_consistency</span> <span class="o">=</span> <span class="s">ON</span></span></span></code></pre></div><p><strong>Binlog backup</strong>：</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"># 持續 stream binlog 到 backup storage</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysqlbinlog --read-from-remote-server --raw --stop-never <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --user<span class="o">=</span>replication --password<span class="o">=</span>... <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --host<span class="o">=</span>primary.example.com <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --result-file<span class="o">=</span>/backup/binlog/ mysql-bin.000001 <span class="p">&amp;</span></span></span></code></pre></div><p><code>--read-from-remote-server</code> + <code>--stop-never</code> 持續從 primary tail binlog、不間斷 stream 到 backup directory。每個 binlog file 寫滿後 close + 開新 file。</p>
<h2 id="restore--pitr-流程">Restore + PITR 流程</h2>
<p>完整 PITR 流程（restore 到 2026-05-19 14:30:00）：</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"># Step 1: Restore full backup</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">xtrabackup --copy-back --target-dir<span class="o">=</span>/backup/full-2026-05-18  <span class="c1"># 前一天 full</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="c1"># Step 2: 啟動 MySQL（會看到 backup 拿那刻的 GTID set）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">systemctl start mysqld
</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="c1"># Step 3: 查 full backup 結束時的 GTID</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">mysql&gt; SHOW MASTER STATUS<span class="p">;</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="p">|</span> File             <span class="p">|</span> Position <span class="p">|</span> Executed_Gtid_Set                        <span class="p">|</span>
</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 class="p">|</span> mysql-bin.000150 <span class="p">|</span>     <span class="m">1234</span> <span class="p">|</span> server-uuid:1-12345                      <span class="p">|</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">+------------------+----------+------------------------------------------+
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Step 4: Apply binlog 從 backup 之後到目標時間</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">mysqlbinlog --start-datetime<span class="o">=</span><span class="s2">&#34;2026-05-18 03:00:00&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>            --stop-datetime<span class="o">=</span><span class="s2">&#34;2026-05-19 14:30:00&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>            /backup/binlog/mysql-bin.000150 <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>            /backup/binlog/mysql-bin.000151 <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>            ...                                <span class="c1"># 列所有需要的 binlog</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="p">|</span> mysql -u root -p
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># Step 5: 驗證 GTID set 到目標時間點對應的位置</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">mysql&gt; SHOW MASTER STATUS<span class="p">;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># Executed_Gtid_Set 應包含到目標時間點的 transaction</span></span></span></code></pre></div><p>對 <em>精確 GTID-based PITR</em>（停在特定 transaction、不是 timestamp）：</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">mysqlbinlog --include-gtids<span class="o">=</span><span class="s1">&#39;server-uuid:1-50000&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>            /backup/binlog/mysql-bin.000150 ... <span class="p">|</span> mysql -u root -p</span></span></code></pre></div><h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-gtid-處理不一致--restore-後-replication-broken">1. GTID 處理不一致 — Restore 後 replication broken</h3>
<p>XtraBackup restore 時 <code>--slave-info</code> 紀錄 GTID purged set、mysqldump 用 <code>--gtid-purged=ON</code>。如果 restore 後沒正確 set <code>gtid_purged</code>、replica re-attach 時 GTID gap error。</p>
<p>修法：</p>
<ul>
<li>XtraBackup restore：用 <code>xtrabackup_binlog_info</code> 內的 GTID set 設 <code>SET GLOBAL gtid_purged='...';</code></li>
<li>mysqldump：dump file 內已有 <code>SET @@GLOBAL.GTID_PURGED='...';</code>、執行 dump 自動 set</li>
<li>Restore 後 <em>先驗證 <code>Executed_Gtid_Set</code></em> 跟 source 預期對齊、再 START SLAVE</li>
</ul>
<h3 id="2-binlog-gap--中間遺漏-file-直接-restore-fail">2. Binlog gap — 中間遺漏 file 直接 restore fail</h3>
<p>Binlog stream 失聯（network blip / disk full）+ binlog rotate、<code>mysql-bin.000156</code> 不在 backup storage 內。PITR 試圖跨過該 file restore、跳過已 commit transaction、結果 <em>資料不一致</em>（不是錯誤、是 <em>silently incorrect</em>）。</p>
<p>修法：</p>
<ul>
<li><em>Binlog stream 必須持續</em>、失聯 → alert</li>
<li>監控 backup storage 內 binlog 連續性（file name 連號、無 gap）</li>
<li>Restore 前 <em>先驗證 binlog 完整性</em>：<code>mysqlbinlog --verify-binlog-checksum *.bin &gt; /dev/null</code></li>
<li>對 missing binlog <em>中止 PITR</em>、不繼續 partial restore</li>
</ul>
<h3 id="3-backup-沒-verify--真事故時才發現-restore-broken">3. Backup 沒 verify — 真事故時才發現 restore broken</h3>
<p>每天備份成功、storage 用了 5 TB、實際 <em>從未 restore 過</em>。事故發生 restore 才知道 backup file corrupt / GTID 錯 / binlog gap、整套無用。</p>
<p>修法：</p>
<ul>
<li><em>自動化 restore test</em>：每週 / 每月在 staging server 跑完整 restore + PITR、跑完 SELECT 比對 production</li>
<li>驗證 restore 後 row count 跟 production 接近、<code>CHECKSUM TABLE</code> 比對主要 table</li>
<li>真的事故時 RTO 才不會 surprise</li>
</ul>
<h3 id="4-rpo-不到-1-分鐘的代價">4. RPO 不到 1 分鐘的代價</h3>
<p>「我要 RPO &lt; 1 分鐘」聽起來合理、但實現需要：</p>
<ul>
<li><code>sync_binlog=1</code>（每 commit fsync、寫吞吐降 10-30%）</li>
<li>Binlog stream 到 <em>獨立 storage</em>（不只是 primary local disk）、cross-region replication（額外 network cost）</li>
<li>Replica 也用 semi-sync 配合（zero binlog loss）</li>
<li>監控 + alert RPO 違反（&lt; 1 分鐘 stream lag）</li>
</ul>
<p><strong>TCO</strong>：~30% 寫吞吐 penalty + 額外 storage / network cost + 7x24 on-call。考慮 <em>real RPO requirement</em> — 多數 application 5 分鐘 RPO 已足夠、追求 1 分鐘 RPO 不划算。</p>
<p>修法：</p>
<ul>
<li>跟 product / business 確認 <em>真 RPO 要求</em></li>
<li><em>RPO budget = 寫吞吐 trade-off + ops cost</em>、不是 free</li>
<li>用 <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> / managed offering 把 RPO 議題 outsource（Aurora &lt; 1 秒 RPO + 自動 cross-AZ）</li>
</ul>
<h3 id="5-encryption-key-沒備份--restore-後解不開資料">5. Encryption key 沒備份 — Restore 後解不開資料</h3>
<p>啟用 <em>encryption at rest</em>（MySQL 8.0+ <code>default_table_encryption=ON</code> + keyring plugin / component；MariaDB 用 <code>innodb_encrypt_tables</code>）後、所有 InnoDB tablespace 都加密。Master key 在 <em>keyring file</em> 或 KMS-backed component。如果 backup 只 backup MySQL data file、沒備 keyring、restore 後資料 <em>encrypted 但無 key、無法讀</em>。</p>
<p>修法：</p>
<ul>
<li><em>Keyring file 跟 data file 分開儲存</em>、但兩者 <em>都要 backup</em></li>
<li>用 <em>KMS-based keyring</em>（AWS KMS / HashiCorp Vault）取代 file-based、key 不在 MySQL server 上</li>
<li>Disaster recovery runbook 紀錄 <em>key recovery 流程</em>、不要假設「重 install MySQL」就能解</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Full backup 頻率</td>
          <td>週一次（XtraBackup）或日一次（小 DB）</td>
      </tr>
      <tr>
          <td>Incremental 頻率</td>
          <td>每日（XtraBackup incremental）</td>
      </tr>
      <tr>
          <td>Binlog retention</td>
          <td>14 天（給 PITR window）</td>
      </tr>
      <tr>
          <td>Backup retention</td>
          <td>Full × 4 週 + 月度 archive × 12 個月</td>
      </tr>
      <tr>
          <td>Storage cost</td>
          <td>約 2-3x DB size（full + incremental + binlog）</td>
      </tr>
      <tr>
          <td>Cross-region copy</td>
          <td>必要（local backup 失效時還有 disaster recovery）</td>
      </tr>
      <tr>
          <td>Restore test 頻率</td>
          <td>每週 staging 上跑、每月 production-like 跑</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>Replication replica 不能取代 backup — replica 上的 DROP TABLE 也會被 replicate、replica 上資料同樣消失。Backup 是 <em>獨立保險</em>。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p><code>innodb_flush_log_at_trx_commit=1</code> + <code>sync_binlog=1</code> 是 backup-friendly 的設定（zero loss）、但寫吞吐降。如果為了寫吞吐放寬 durability、必須接受 <em>PITR window</em> 也 widening。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-aurora-mysql">跟 Aurora MySQL</h3>
<p>Aurora 完全 outsource backup — automatic continuous backup + PITR &lt; 1 秒、不必管 mysqldump / XtraBackup / binlog stream。從 Aurora 遷出時、需要重新建 self-managed backup chain。詳見 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">migrate-to-aurora</a>。</p>
<h3 id="跟-postgresql-pitr">跟 PostgreSQL PITR</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL PITR</th>
          <th>PostgreSQL PITR</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Logical backup</td>
          <td>mysqldump / MyDumper</td>
          <td>pg_dump / pg_dumpall</td>
      </tr>
      <tr>
          <td>Physical backup</td>
          <td>XtraBackup</td>
          <td>pg_basebackup / pgBackRest</td>
      </tr>
      <tr>
          <td>Incremental log</td>
          <td>Binary log（binlog）</td>
          <td>WAL (Write-Ahead Log)</td>
      </tr>
      <tr>
          <td>Stream tool</td>
          <td>mysqlbinlog &ndash;read-from-remote-server</td>
          <td>pg_receivewal</td>
      </tr>
      <tr>
          <td>PITR command</td>
          <td>mysqlbinlog &ndash;stop-datetime</td>
          <td>pg_ctl + recovery.conf / standby.signal</td>
      </tr>
      <tr>
          <td>Identifier</td>
          <td>GTID 或 file:position</td>
          <td>LSN（Log Sequence Number）</td>
      </tr>
      <tr>
          <td>Cross-version</td>
          <td>mysqldump（廣容）</td>
          <td>pg_dump（廣容）</td>
      </tr>
  </tbody>
</table>
<p>兩家 PITR 概念類似（full + log replay）、tool name 不同、概念對等。詳見 <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PostgreSQL PITR + WAL Archiving</a>。</p>
<h2 id="何時-outsource-backup">何時 outsource backup</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS 生態 + 不想管 backup ops</td>
          <td>Aurora MySQL（內建 PITR）</td>
      </tr>
      <tr>
          <td>GCP 生態</td>
          <td>Cloud SQL（內建 PITR）</td>
      </tr>
      <tr>
          <td>Azure 生態</td>
          <td>Azure DB for MySQL</td>
      </tr>
      <tr>
          <td>跨雲 + 想自管</td>
          <td>XtraBackup + binlog stream + S3</td>
      </tr>
      <tr>
          <td>規模小、可接受 mysqldump</td>
          <td>mysqldump cron + S3</td>
      </tr>
      <tr>
          <td>規模大、無 cloud</td>
          <td>Percona XtraBackup Enterprise + tape archive</td>
      </tr>
      <tr>
          <td>強合規（HIPAA / PCI-DSS）</td>
          <td>自管 + air-gap backup + audit trail</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（binlog 跟 PITR 共用 source）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（durability + backup 互動）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">migrate-to-aurora</a>（backup outsource）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PostgreSQL PITR + WAL Archiving</a>（PG sibling）</li>
<li>官方：<a href="https://docs.percona.com/percona-xtrabackup/8.0/">Percona XtraBackup</a> / <a href="https://github.com/mydumper/mydumper">MyDumper</a> / <a href="https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html">mysqldump</a></li>
</ul>
]]></content:encoded></item><item><title>3.C23 Bloomberg：多租戶 vhost + 自助平台化</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/</guid><description>&lt;p>Bloomberg 的 RabbitMQ 平台化案例揭露了 broker 從幾個團隊的工具演變成上百個團隊的共享基礎設施時，治理責任邊界應該前置設計，而非在規模化之後補救。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Bloomberg 有 5000+ 工程師，內部系統涵蓋金融資料處理、交易系統、新聞分發與分析平台。RabbitMQ 的使用從最初幾個團隊的 microservice 解耦開始，逐步擴展到上百個團隊。到 2019 年，Bloomberg 的 RabbitMQ 基礎設施每週處理超過 2 億條訊息，尖峰每秒數萬條。&lt;/p>
&lt;p>這個規模下，原本由平台團隊手動配置的 queue / exchange / binding 模式無法持續 — 上百個團隊各自有不同的 queue 需求，平台團隊成為所有變更的人工瓶頸。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="多租戶隔離">多租戶隔離&lt;/h3>
&lt;p>多個團隊共用同一個 RabbitMQ cluster 時，一個團隊的 queue 爆量或 consumer 故障可能影響其他團隊的訊息處理。RabbitMQ 的 Erlang scheduler 是共用的 — 一個 queue 的 message accumulation 會消耗 broker 的記憶體跟 CPU，影響同 cluster 上所有 queue 的效能。&lt;/p>
&lt;p>隔離需要在 broker 層實作，client 端的 best practice（限制 message size、設定 TTL）只能降低風險但無法保證隔離。&lt;/p>
&lt;h3 id="自助配置的安全邊界">自助配置的安全邊界&lt;/h3>
&lt;p>讓上百個團隊自助建立 queue / exchange / binding 需要明確的安全邊界 — 團隊 A 能在自己的 namespace 建立資源，但不能存取團隊 B 的 queue。RabbitMQ 的 vhost 機制提供了這個隔離單位，但 vhost 的建立跟權限配置本身需要自動化。&lt;/p>
&lt;h3 id="容量規劃與配額">容量規劃與配額&lt;/h3>
&lt;p>共享 cluster 的容量被所有租戶分攤。沒有配額機制時，一個團隊的 queue 可以無限增長直到 broker 記憶體告警、觸發 flow control、影響全部租戶。配額需要在 queue 層面設定上限（max-length、max-length-bytes），同時提供超出配額時的降級策略而非直接拒絕。&lt;/p>
&lt;h2 id="解法vhost-分層--自助平台">解法：vhost 分層 + 自助平台&lt;/h2>
&lt;h3 id="vhost-作為租戶邊界">Vhost 作為租戶邊界&lt;/h3>
&lt;p>Bloomberg 把 vhost 作為多租戶隔離的基本單位。每個團隊（或每個應用）分配一個 vhost，vhost 內的 queue / exchange / binding 只對該團隊可見。跨 vhost 的訊息傳遞透過 shovel 或 federation plugin，需要顯式配置，預設不互通。&lt;/p>
&lt;p>Vhost 的隔離粒度是「資源可見性 + 權限」而非「硬體資源」。同 cluster 上的 vhost 仍然共用 Erlang runtime 跟記憶體。完全的硬體隔離需要獨立 cluster — Bloomberg 對高敏感度的工作負載（交易相關）使用專用 cluster，一般業務共用大 cluster + vhost 隔離。&lt;/p>
&lt;h3 id="自助-vhost-註冊">自助 vhost 註冊&lt;/h3>
&lt;p>Bloomberg 建立了內部自助平台，團隊透過 API 或內部 portal 申請 vhost。申請時需要提供：應用名稱、預期的 message rate、保留期限、是否需要 HA（mirrored / quorum queue）。平台自動建立 vhost、設定權限、分配連線端點。&lt;/p>
&lt;p>自助流程的價值是去除平台團隊的人工瓶頸。新團隊從申請到拿到可用的 RabbitMQ 端點，時間從「提 ticket 等平台團隊排程」縮短到「填表 → 自動配置 → 立即可用」。&lt;/p>
&lt;h3 id="配額與監控">配額與監控&lt;/h3>
&lt;p>每個 vhost 有預設配額（max-length、max-connections）。超出配額時 broker 行為可配 — drop-head（丟最舊的訊息）或 reject-publish（拒絕新訊息）。配額不是懲罰機制，是保護共享 cluster 的防線。&lt;/p>
&lt;p>監控用 RabbitMQ 的 management plugin + Prometheus exporter，按 vhost 維度匯出 queue depth、message rate、connection count。每個 vhost 的 dashboard 對應到 owner 團隊，讓團隊自行判讀自己的使用狀況。&lt;/p></description><content:encoded><![CDATA[<p>Bloomberg 的 RabbitMQ 平台化案例揭露了 broker 從幾個團隊的工具演變成上百個團隊的共享基礎設施時，治理責任邊界應該前置設計，而非在規模化之後補救。</p>
<h2 id="業務背景">業務背景</h2>
<p>Bloomberg 有 5000+ 工程師，內部系統涵蓋金融資料處理、交易系統、新聞分發與分析平台。RabbitMQ 的使用從最初幾個團隊的 microservice 解耦開始，逐步擴展到上百個團隊。到 2019 年，Bloomberg 的 RabbitMQ 基礎設施每週處理超過 2 億條訊息，尖峰每秒數萬條。</p>
<p>這個規模下，原本由平台團隊手動配置的 queue / exchange / binding 模式無法持續 — 上百個團隊各自有不同的 queue 需求，平台團隊成為所有變更的人工瓶頸。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="多租戶隔離">多租戶隔離</h3>
<p>多個團隊共用同一個 RabbitMQ cluster 時，一個團隊的 queue 爆量或 consumer 故障可能影響其他團隊的訊息處理。RabbitMQ 的 Erlang scheduler 是共用的 — 一個 queue 的 message accumulation 會消耗 broker 的記憶體跟 CPU，影響同 cluster 上所有 queue 的效能。</p>
<p>隔離需要在 broker 層實作，client 端的 best practice（限制 message size、設定 TTL）只能降低風險但無法保證隔離。</p>
<h3 id="自助配置的安全邊界">自助配置的安全邊界</h3>
<p>讓上百個團隊自助建立 queue / exchange / binding 需要明確的安全邊界 — 團隊 A 能在自己的 namespace 建立資源，但不能存取團隊 B 的 queue。RabbitMQ 的 vhost 機制提供了這個隔離單位，但 vhost 的建立跟權限配置本身需要自動化。</p>
<h3 id="容量規劃與配額">容量規劃與配額</h3>
<p>共享 cluster 的容量被所有租戶分攤。沒有配額機制時，一個團隊的 queue 可以無限增長直到 broker 記憶體告警、觸發 flow control、影響全部租戶。配額需要在 queue 層面設定上限（max-length、max-length-bytes），同時提供超出配額時的降級策略而非直接拒絕。</p>
<h2 id="解法vhost-分層--自助平台">解法：vhost 分層 + 自助平台</h2>
<h3 id="vhost-作為租戶邊界">Vhost 作為租戶邊界</h3>
<p>Bloomberg 把 vhost 作為多租戶隔離的基本單位。每個團隊（或每個應用）分配一個 vhost，vhost 內的 queue / exchange / binding 只對該團隊可見。跨 vhost 的訊息傳遞透過 shovel 或 federation plugin，需要顯式配置，預設不互通。</p>
<p>Vhost 的隔離粒度是「資源可見性 + 權限」而非「硬體資源」。同 cluster 上的 vhost 仍然共用 Erlang runtime 跟記憶體。完全的硬體隔離需要獨立 cluster — Bloomberg 對高敏感度的工作負載（交易相關）使用專用 cluster，一般業務共用大 cluster + vhost 隔離。</p>
<h3 id="自助-vhost-註冊">自助 vhost 註冊</h3>
<p>Bloomberg 建立了內部自助平台，團隊透過 API 或內部 portal 申請 vhost。申請時需要提供：應用名稱、預期的 message rate、保留期限、是否需要 HA（mirrored / quorum queue）。平台自動建立 vhost、設定權限、分配連線端點。</p>
<p>自助流程的價值是去除平台團隊的人工瓶頸。新團隊從申請到拿到可用的 RabbitMQ 端點，時間從「提 ticket 等平台團隊排程」縮短到「填表 → 自動配置 → 立即可用」。</p>
<h3 id="配額與監控">配額與監控</h3>
<p>每個 vhost 有預設配額（max-length、max-connections）。超出配額時 broker 行為可配 — drop-head（丟最舊的訊息）或 reject-publish（拒絕新訊息）。配額不是懲罰機制，是保護共享 cluster 的防線。</p>
<p>監控用 RabbitMQ 的 management plugin + Prometheus exporter，按 vhost 維度匯出 queue depth、message rate、connection count。每個 vhost 的 dashboard 對應到 owner 團隊，讓團隊自行判讀自己的使用狀況。</p>
<h2 id="取捨">取捨</h2>
<p><strong>Vhost 隔離 vs 硬體隔離</strong>：vhost 隔離成本低（不需要額外 cluster），但隔離程度有限 — Erlang scheduler 跟記憶體仍然共用。Bloomberg 的做法是多數團隊用 vhost 隔離、高敏感度工作負載用專用 cluster，兩者共存。</p>
<p><strong>自助配置 vs 中央管控</strong>：自助配置加速團隊迭代，但也增加了 configuration drift 的風險。Bloomberg 透過配額跟自動化審計（定期掃描 vhost 的 queue 狀態、alert 異常 pattern）平衡自助跟管控。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 的多租戶治理責任</li>
<li><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka 平台</a>：Kafka 生態的多租戶治理比較 — Kafka 用 topic-level ACL + quota，RabbitMQ 用 vhost</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：平台團隊跟服務團隊的 ownership 邊界</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>以下訊號出現時，應該回讀本案例：</p>
<ul>
<li>RabbitMQ 的使用團隊數從個位數增長到雙位數、平台團隊成為配置瓶頸</li>
<li>單一 cluster 上的 queue 數量超過數百個、owner 不明</li>
<li>某個團隊的 queue 爆量影響了其他團隊的 consumer 效能</li>
<li>新團隊要用 RabbitMQ 但平台團隊的 ticket 要排隊數天</li>
<li>沒有 per-team 的 message rate 或 queue depth 監控</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.cloudamqp.com/blog/growing-a-farm-of-rabbits.html">Growing a Farm of Rabbits at Bloomberg</a></li>
</ul>
]]></content:encoded></item><item><title>Conftest</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/conftest/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/conftest/</guid><description>&lt;p>Conftest 是 &lt;em>OPA CLI wrapper for static config policy check&lt;/em>、Open Policy Agent project 旗下的 CLI 工具、Apache 2.0 OSS、無商業版。它的核心定位是 &lt;em>CI-time policy gate&lt;/em>、有別於 admission runtime：在 git commit / PR / merge 階段、用 Rego policy 對 config file（Terraform HCL / K8s YAML / Dockerfile / JSON / TOML / INI / serverless.yml）做 static check、把 misconfiguration 攔在 deploy 之前。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> Config 的差異在 &lt;em>執行時機 + 客製化方式&lt;/em>、規則表達力反而相近。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Conftest 是 OPA 生態中 &lt;em>最輕量的 CI-time tool&lt;/em> — 拿一份 Rego policy + 一份 config file、跑 &lt;code>conftest test&lt;/code> 就出 violation report。它不需要 cluster、不需要 daemon、不接 admission webhook、只是個 binary。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &amp;#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA&lt;/a> 比、OPA 是 &lt;em>runtime decision engine&lt;/em>（HTTP server / library / sidecar 提供 policy decision）、Conftest 只是 &lt;em>CLI 跑 once、結束即關&lt;/em>。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &amp;#43; Constraint 兩層、Rego policy &amp;#43; Audit &amp;#43; Mutation">Gatekeeper&lt;/a> 比、Gatekeeper 是 &lt;em>K8s admission controller runtime&lt;/em>、會在 kubectl apply 時攔下違規；Conftest 是在 PR 階段就攔下、deploy 前就 fail CI。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno&lt;/a> 比、Kyverno 是 K8s-only 的 admission policy（YAML 語法）、Conftest 跨多 config format（不只 K8s）且用 Rego。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a> Config 比、Trivy Config 是 &lt;em>built-in misconfig rule&lt;/em>（開箱即用、預定義常見 anti-pattern）、Conftest 是 &lt;em>自己寫 Rego policy&lt;/em>（客製化彈性大但要寫 rule）。&lt;/p></description><content:encoded><![CDATA[<p>Conftest 是 <em>OPA CLI wrapper for static config policy check</em>、Open Policy Agent project 旗下的 CLI 工具、Apache 2.0 OSS、無商業版。它的核心定位是 <em>CI-time policy gate</em>、有別於 admission runtime：在 git commit / PR / merge 階段、用 Rego policy 對 config file（Terraform HCL / K8s YAML / Dockerfile / JSON / TOML / INI / serverless.yml）做 static check、把 misconfiguration 攔在 deploy 之前。跟 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> / <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> / <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> Config 的差異在 <em>執行時機 + 客製化方式</em>、規則表達力反而相近。</p>
<h2 id="服務定位">服務定位</h2>
<p>Conftest 是 OPA 生態中 <em>最輕量的 CI-time tool</em> — 拿一份 Rego policy + 一份 config file、跑 <code>conftest test</code> 就出 violation report。它不需要 cluster、不需要 daemon、不接 admission webhook、只是個 binary。跟 <a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a> 比、OPA 是 <em>runtime decision engine</em>（HTTP server / library / sidecar 提供 policy decision）、Conftest 只是 <em>CLI 跑 once、結束即關</em>。跟 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 比、Gatekeeper 是 <em>K8s admission controller runtime</em>、會在 kubectl apply 時攔下違規；Conftest 是在 PR 階段就攔下、deploy 前就 fail CI。跟 <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> 比、Kyverno 是 K8s-only 的 admission policy（YAML 語法）、Conftest 跨多 config format（不只 K8s）且用 Rego。跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> Config 比、Trivy Config 是 <em>built-in misconfig rule</em>（開箱即用、預定義常見 anti-pattern）、Conftest 是 <em>自己寫 Rego policy</em>（客製化彈性大但要寫 rule）。</p>
<p>關鍵張力：<em>CI-time static check</em> ↔ <em>runtime admission enforcement</em> 是兩種互補機制、不是替代。CI 抓在 deploy 之前、但 manifest 不一定都走 PR（kubectl apply 直接打 cluster 就漏接）；admission 抓 runtime 寫入、但 deploy 後才 fail 已經慢。production 通常 CI（Conftest / Trivy Config）+ admission（Gatekeeper / Kyverno）雙層覆蓋。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Conftest 在 policy-as-code stack 中承擔哪一段（CI gate）、跟 admission runtime 怎麼分工</li>
<li>Rego policy directory / <code>conftest test</code> / <code>conftest verify</code> / Bundle / Combine flag 的 ownership 跟工程化做法</li>
<li>Conftest vs Trivy Config vs Checkov vs OPA + custom CI wrapper 的取捨</li>
<li>何時用 Conftest、何時走 Trivy Config（不想學 Rego）或 Gatekeeper（runtime enforcement）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Conftest 導入是否健康、最少看四件事：</p>
<ul>
<li><strong>Policy directory 走版控</strong>：Rego files（<code>policy/*.rego</code>）跟 application code 同 repo、或抽到中央 policy repo + Bundle 拉取、PR review 才能改 policy</li>
<li><strong><code>conftest verify</code> 是否跑</strong>：Rego policy 本身有單元測試（<code>*_test.rego</code>）、policy 改動有 test coverage、不是寫完就上 production CI</li>
<li><strong>CI integration 點</strong>：跑在 PR check / merge gate、fail 阻斷 merge、不是只跑 warning 沒人看</li>
<li><strong>跟 admission 是否雙層</strong>：CI fail 之外、cluster 也裝 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> / <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a> 接 runtime；否則 kubectl apply 繞過 CI 就破口</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Policy directory（Rego files）</strong>：Conftest 預設讀 <code>./policy/</code> 目錄下所有 <code>*.rego</code> 檔。Policy 用 <code>deny[msg]</code> / <code>warn[msg]</code> / <code>violation[msg]</code> rule 表達 — <code>deny</code> 失敗整個 test、<code>warn</code> 只 print 不 fail、<code>violation</code> 給結構化輸出（含 metadata 給後續 SOAR 處理）。慣例是一個 policy 檔對一個 anti-pattern（<code>policy/k8s_privileged.rego</code> / <code>policy/terraform_public_s3.rego</code>）、不混寫。</p>
<p><strong><code>conftest test</code> command</strong>：<code>conftest test deployment.yaml</code> / <code>conftest test --policy ./custom-policy terraform.plan.json</code> 是日常入口。支援 <code>--all-namespaces</code>（K8s 多 manifest）、<code>--input</code>（強制 parser 類型）、<code>--combine</code>（跨檔 check）、<code>--output json|tap|table</code>（CI 報表格式）。CI integration 通常 <code>conftest test infra/**/*.yaml --output github</code> 直接 emit GitHub Actions annotation。</p>
<p><strong>Parser（多 format 支援）</strong>：Conftest 原生支援 HCL（Terraform）/ YAML / JSON / TOML / INI / Dockerfile / CUE / Jsonnet / EDN / XML / VCL（Fastly）/ Cyclonedx SBOM 等。同一份 Rego 可跑多 format — parser 把不同 format normalize 成 Rego input JSON、policy 寫 <code>input.spec.containers[_].securityContext.privileged == true</code> 不管原本是 YAML 還是 JSON。這是 Conftest 比 Checkov / Trivy Config 客製化彈性更大的關鍵：同一個 policy 引擎處理跨 format misconfig。</p>
<p><strong>Combine flag（跨檔 check）</strong>：<code>conftest test --combine *.yaml</code> 把多檔合併成單一 input array、policy 可跨檔 reference。實務場景：K8s deployment 必須有對應 service + configmap + networkpolicy、缺一就 fail；Terraform module 跨檔 reference（VPC + subnet + security group）必須一致。沒有 Combine 就只能單檔檢查、跨檔 invariant 抓不到。</p>
<p><strong><code>conftest verify</code>（policy unit test）</strong>：Policy 本身要有測試 — <code>policy/k8s_privileged_test.rego</code> 寫 <code>test_privileged_denied</code> / <code>test_non_privileged_allowed</code>、<code>conftest verify</code> 跑這些測試。Policy 改動先跑 verify、再 merge policy 到 production CI。沒做 verify 的 policy 是「policy 自己 broken 沒人發現」的常見破口。</p>
<p><strong>Bundle（OPA bundle 拉 policy）</strong>：<code>conftest pull</code> 從 OCI registry / HTTP / git / S3 拉 policy bundle、policy 集中在 central repo、各 service repo 不重複維護。Bundle 包含 Rego files + data files + manifest、可簽章驗證（配 <a href="https://docs.sigstore.dev/">Sigstore cosign</a>）。大組織通常 platform team 維護 policy bundle、application team 在 CI 拉最新版本跑。</p>
<p><strong>CI integration</strong>：GitHub Actions（<code>open-policy-agent/conftest-action</code>）/ GitLab CI / Jenkins / CircleCI 都有現成 step。跑點通常在 PR check 階段（review 前 fail fast）+ merge gate（防止繞過）。失敗訊息含 file / line / policy name、SOC / SRE 看 annotation 就知道哪行違規。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Conftest</th>
          <th>Trivy Config</th>
          <th>Checkov</th>
          <th>OPA + custom CI wrapper</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規則來源</td>
          <td>自己寫 Rego（彈性大、要學 Rego）</td>
          <td>內建 misconfig rule（開箱即用）</td>
          <td>內建 + 自訂 Python rule</td>
          <td>自己寫 Rego + 自己包 CI</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — Rego 語法 + Conftest CLI</td>
          <td>緩 — <code>trivy config</code> 一個指令</td>
          <td>緩 — 內建 rule、自訂 Python 稍重</td>
          <td>陡 — Rego + 自己組 CI 跑點</td>
      </tr>
      <tr>
          <td>Format 支援</td>
          <td>廣 — Terraform / K8s / Dockerfile 等</td>
          <td>中 — Terraform / K8s / CloudFormation</td>
          <td>中 — Terraform / K8s / Serverless</td>
          <td>看自己包</td>
      </tr>
      <tr>
          <td>客製彈性</td>
          <td>高 — 任意 Rego policy</td>
          <td>低 — 內建 rule、客製要寫 plugin</td>
          <td>中 — Python custom rule</td>
          <td>高</td>
      </tr>
      <tr>
          <td>跨檔 check</td>
          <td>強 — <code>--combine</code> flag</td>
          <td>弱 — 主要單檔</td>
          <td>中</td>
          <td>看自己寫</td>
      </tr>
      <tr>
          <td>Policy 共享</td>
          <td>OPA Bundle（OCI / git / HTTP）</td>
          <td>Trivy DB（中央更新）</td>
          <td>Checkov rule pack</td>
          <td>自己管</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>OSS Apache 2.0</td>
          <td>OSS（Aqua 商業版有加值）</td>
          <td>OSS（Bridgecrew 商業版）</td>
          <td>OSS（OPA）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>客製化 policy、Rego 已用、跨 format</td>
          <td>開箱即用、團隊不想學 Rego</td>
          <td>Terraform-heavy、Python team 熟</td>
          <td>OPA 已是 runtime、CI 想複用 policy</td>
      </tr>
  </tbody>
</table>
<p>選 Conftest 的核心訴求：<em>組織有 custom policy 需求</em> + <em>已用 OPA / Rego（admission 走 Gatekeeper、CI 走 Conftest 統一語言）</em> + <em>跨多 config format 需要同一個 policy 引擎</em>。如果只是要 K8s privileged container / Terraform public S3 這類常見 anti-pattern 攔截、直接 Trivy Config 開箱即用更划算。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong><code>conftest verify</code>（policy unit test lifecycle）</strong>：Policy 是 code、code 要有測試。<code>policy/k8s_privileged_test.rego</code> 寫 <code>test_privileged_denied { count(deny) &gt; 0 with input as {...} }</code>、CI 跑 <code>conftest verify ./policy</code> 把 policy test 當 unit test。Policy change 走 PR → verify pass → 部署到 central bundle → application CI 拉新版本。沒 verify 的 policy 是「沒人知道 policy 自己壞掉、所有 application CI 都 silently pass」的 systemic 風險。</p>
<p><strong>Bundle 從 OCI registry pull + 簽章驗證</strong>：<code>conftest pull oci://registry.example.com/policy-bundle:v1.2.3</code> 從 OCI registry 拉 policy bundle、policy distribution 走 container image 同一套 supply chain。配 <a href="https://docs.sigstore.dev/">Sigstore cosign</a> 簽章驗證、policy bundle 也走 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性</a> 的 release gate 邏輯 — policy 本身就是 artifact、需要 signing + verification。</p>
<p><strong>跟 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> Config 混用</strong>：實務上 <em>Trivy Config 跑預設 rule</em>（CIS / NSA / OWASP baseline、開箱即用）+ <em>Conftest 跑客製 rule</em>（organization-specific：必須有特定 label、必須走特定 namespace、必須引用特定 ConfigMap）。兩者 CI 階段都跑、報表合併。不是二選一、是 baseline + custom 的分工。</p>
<p><strong>跟 admission 雙層</strong>：CI 階段 Conftest fail 之外、cluster 也裝 <a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> 接 admission。Gatekeeper 用 ConstraintTemplate（也是 Rego）、同一份 Rego policy 理論上 CI / runtime 共用 — 但實務上 admission 場景跟 static check 場景的 input shape 不同（admission 拿 AdmissionReview object、static 拿 raw manifest）、policy 通常分兩份維護或寫 abstraction layer 共用。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Policy pass 但 production 還是 misconfig</strong>：CI 沒卡在 merge gate、或有 <code>kubectl apply</code> 繞過 PR — 加 admission controller（<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> / <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>）做 runtime 雙層</li>
<li><strong>Rego policy 自己 broken / silently pass</strong>：沒寫 <code>*_test.rego</code> + <code>conftest verify</code> — 補 policy unit test、CI 跑 verify 才 promote</li>
<li><strong><code>conftest test</code> 跑出 0 violations 但 manifest 有問題</strong>：policy directory 沒讀對、或 parser 自動偵測選錯 — 顯式 <code>--policy ./policy --input yaml</code></li>
<li><strong>跨檔 invariant 抓不到</strong>（deployment 沒對應 service）：忘記用 <code>--combine</code> flag — 改 <code>conftest test --combine *.yaml</code></li>
<li><strong>Bundle 拉到舊版本 / policy drift</strong>：沒固定 bundle tag、用 <code>latest</code> 漂移 — bundle reference 用 digest（<code>sha256:...</code>）或 immutable tag</li>
<li><strong>False positive 多 / team 開始 ignore CI</strong>：policy 寫得太寬、沒考慮合理例外 — Rego 加 exception list（<code>data.exceptions</code>）、走 <a href="/blog/backend/07-security-data-protection/blue-team/" data-link-title="7.B 防守者視角（藍隊）與控制面驗證" data-link-desc="從防守者角度整理控制面、偵測路由、驗證策略與演練回寫">Exception Workflow</a> lifecycle</li>
<li><strong>Policy 散落各 application repo / 維護不一致</strong>：沒走 central bundle — 抽 policy 到中央 repo、各 application 拉 bundle</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開箱即用、不想學 Rego</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy Config</a></td>
      </tr>
      <tr>
          <td>K8s admission runtime</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a> / <a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a></td>
      </tr>
      <tr>
          <td>Runtime application policy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a></td>
      </tr>
      <tr>
          <td>Terraform-heavy + Python team</td>
          <td>Checkov / tfsec</td>
      </tr>
      <tr>
          <td>Cloud-native CNAPP</td>
          <td>Wiz / Prisma Cloud</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Rego 完整語法 reference、<code>every</code> / <code>walk</code> / built-in function 進階用法</li>
<li>OPA Bundle 的 server-side 實作（policy publish pipeline）</li>
<li>Conftest 跟 Open Policy Agent runtime 的內部架構差異</li>
<li>Sigstore cosign 的 keyless signing flow 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Conftest 在 07 案例庫沒有直接 vendor-level 事件、但所有 supply chain case 都是 CI-time policy gate 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Conftest 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>Conftest 在 CI 階段檢查 Terraform / K8s manifest 是否符合 image signing policy（image 必須來自 signed registry、必須有 cosign attestation）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Conftest 配 SBOM 檔案做 CI-time vulnerable component check、補 admission 之前的 gate（image 含 log4j-core &lt;2.17 直接 PR fail）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性 (section)</a></td>
          <td>Conftest 是 release gate 在 CI 階段的 policy enforcement 工具、跟 admission 雙層覆蓋、policy bundle 本身也走 cosign 簽章 supply chain</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 artifact 信任</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>、<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a>、<a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>（CI security check pipeline 共用）</li>
<li>官方：<a href="https://www.conftest.dev/">Conftest Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>AWS Cost Explorer</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/</guid><description>&lt;p>AWS Cost Explorer 的核心責任是提供 AWS-native 的成本、用量、forecast、reservation 與 rightsizing 分析入口。它適合 AWS-first 團隊把帳單變化拆到 account、service、region、tag、usage type 與 time range，並把成本訊號接回容量規劃與服務 owner review。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>AWS Cost Explorer 適合做 AWS 成本分析的 baseline。當團隊需要回答「哪個服務、帳號、tag 或 usage type 造成成本變化」，Cost Explorer 可以直接使用 AWS billing data 產生圖表、report、forecast 與 API 查詢。&lt;/p>
&lt;p>這個定位讓 AWS Cost Explorer 接到三個主章。它從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 接收 cost per request 與 cost curve，從 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性&lt;/a> 接收成本 dashboard 需求，從 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因&lt;/a> 接收 tag 與 ownership 規則。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage&lt;/a> 等 multi-cloud FinOps 平台比、Cost Explorer 走 &lt;em>AWS-native + free&lt;/em>：不另收費（API 查詢按 request 收 USD 0.01）、跟 Billing Console + CUR + Budgets + Anomaly Detection 同一 IAM 邊界、tag 與 Cost Category 設定直接從 billing data 拉。換來的限制是 &lt;em>只看 AWS&lt;/em>、跨雲 / Kubernetes pod-level / SaaS license 都要外接。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Cost Explorer 是否健康發揮、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cost Explorer view 是否有 saved report&lt;/strong>：team-level saved report（依 service / linked account / tag 拆）、月度 review checklist、有沒有人定期看 trend、view 是否進 dashboard share&lt;/li>
&lt;li>&lt;strong>CUR（Cost &amp;amp; Usage Report）設定&lt;/strong>：是否啟用 CUR 2.0 / Data Exports、S3 bucket 是否打開 Athena / QuickSight 查詢、hourly granularity 是否開、resource ID 是否開（沒開的話 tag-based allocation 拆不到 instance level）&lt;/li>
&lt;li>&lt;strong>Budgets + Anomaly Detection alert routing&lt;/strong>：service-level / account-level budget threshold、Cost Anomaly Detection monitor 是否分 service / linked account 設定、alert 接到 Slack / PagerDuty / email、誰負責 triage&lt;/li>
&lt;li>&lt;strong>Tag policy + Cost Category 治理&lt;/strong>：哪些 cost allocation tag 已啟用（在 Billing Console activate 才會進 CUR）、untagged resource 比例、Cost Category rule 是否覆蓋多帳號合併、誰維護 rule lifecycle&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失就是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 邊界的待補項目 — CUR 沒開就只能看 console aggregated view、CUR 開了沒接 Athena / QuickSight 就只能看 Console 介面、不能跟 release / capacity 資料 join。&lt;/p></description><content:encoded><![CDATA[<p>AWS Cost Explorer 的核心責任是提供 AWS-native 的成本、用量、forecast、reservation 與 rightsizing 分析入口。它適合 AWS-first 團隊把帳單變化拆到 account、service、region、tag、usage type 與 time range，並把成本訊號接回容量規劃與服務 owner review。</p>
<h2 id="定位">定位</h2>
<p>AWS Cost Explorer 適合做 AWS 成本分析的 baseline。當團隊需要回答「哪個服務、帳號、tag 或 usage type 造成成本變化」，Cost Explorer 可以直接使用 AWS billing data 產生圖表、report、forecast 與 API 查詢。</p>
<p>這個定位讓 AWS Cost Explorer 接到三個主章。它從 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 接收 cost per request 與 cost curve，從 <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> 接收成本 dashboard 需求，從 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a> 接收 tag 與 ownership 規則。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a> / <a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a> 等 multi-cloud FinOps 平台比、Cost Explorer 走 <em>AWS-native + free</em>：不另收費（API 查詢按 request 收 USD 0.01）、跟 Billing Console + CUR + Budgets + Anomaly Detection 同一 IAM 邊界、tag 與 Cost Category 設定直接從 billing data 拉。換來的限制是 <em>只看 AWS</em>、跨雲 / Kubernetes pod-level / SaaS license 都要外接。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Cost Explorer 是否健康發揮、最少看四件事：</p>
<ul>
<li><strong>Cost Explorer view 是否有 saved report</strong>：team-level saved report（依 service / linked account / tag 拆）、月度 review checklist、有沒有人定期看 trend、view 是否進 dashboard share</li>
<li><strong>CUR（Cost &amp; Usage Report）設定</strong>：是否啟用 CUR 2.0 / Data Exports、S3 bucket 是否打開 Athena / QuickSight 查詢、hourly granularity 是否開、resource ID 是否開（沒開的話 tag-based allocation 拆不到 instance level）</li>
<li><strong>Budgets + Anomaly Detection alert routing</strong>：service-level / account-level budget threshold、Cost Anomaly Detection monitor 是否分 service / linked account 設定、alert 接到 Slack / PagerDuty / email、誰負責 triage</li>
<li><strong>Tag policy + Cost Category 治理</strong>：哪些 cost allocation tag 已啟用（在 Billing Console activate 才會進 CUR）、untagged resource 比例、Cost Category rule 是否覆蓋多帳號合併、誰維護 rule lifecycle</li>
</ul>
<p>四件事任一缺失就是 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 邊界的待補項目 — CUR 沒開就只能看 console aggregated view、CUR 開了沒接 Athena / QuickSight 就只能看 Console 介面、不能跟 release / capacity 資料 join。</p>
<h2 id="適用場景">適用場景</h2>
<p>AWS 月度成本 review 是 Cost Explorer 的主要入口。團隊可以依 service、linked account、region、tag、cost category、purchase option 或 usage type 檢視趨勢，找出 EC2、RDS、S3、NAT Gateway、Data Transfer 或 managed service 的成本變化。</p>
<p>Forecast 與 trend review 適合用 Cost Explorer 連到容量規劃。月中 forecast、daily cost trend、commitment utilization 與 reservation recommendation 可以讓平台團隊提前調整 autoscaling、instance family、reserved capacity 或 service 配置。</p>
<p>Programmatic cost query 適合接內部 dashboard。Cost Explorer API 可以把成本與用量資料拉到 release dashboard、capacity review、service scorecard 或 FinOps workflow，讓工程團隊在自己熟悉的介面看成本訊號。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>AWS Cost Explorer 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS baseline</td>
          <td>直接使用 AWS billing data 與 Cost Management 入口</td>
          <td>Tag policy、Cost Category 設計</td>
      </tr>
      <tr>
          <td>Report</td>
          <td>支援 service、account、region、tag、usage type 分析</td>
          <td>owner mapping、business context</td>
      </tr>
      <tr>
          <td>Forecast</td>
          <td>支援成本預測與趨勢判讀</td>
          <td>release marker、event calendar</td>
      </tr>
      <tr>
          <td>API</td>
          <td>支援把 cost query 接到內部工具</td>
          <td>cache、權限控管、查詢成本治理</td>
      </tr>
  </tbody>
</table>
<p>AWS baseline 價值來自資料來源直接。Cost Explorer 使用 AWS 成本與用量資料，適合作為其他 FinOps 工具導入前的共同對帳入口。</p>
<p>Report 價值來自快速拆解。當某月成本上升，工程團隊可以先用 service、usage type、region 與 tag 找出最大變動，再決定是否需要更細的 workload-level 或 Kubernetes-level 工具。</p>
<p>API 價值來自流程整合。把 cost query 接到 release note、incident review 或 capacity planning dashboard，能讓成本變化跟部署、流量與容量決策同時被檢視。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>AWS Cost Explorer 和 Vantage 的主要差異是範圍。Cost Explorer 是 AWS-native 成本入口；Vantage 適合跨 provider、Kubernetes 成本與工程團隊自助報表。</p>
<p>AWS Cost Explorer 和 CloudHealth 的主要差異是治理層級。Cost Explorer 適合 AWS account 與 service-level 分析；CloudHealth 適合 enterprise FinOps policy、showback / chargeback 與多雲治理。</p>
<p>AWS Cost Explorer 和 Akamas 的主要差異是行動模型。Cost Explorer 提供成本與用量事實；Akamas 把成本、SLO 與配置調校接成 optimization loop。</p>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS Cost Explorer</th>
          <th>CloudHealth</th>
          <th>Vantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍</td>
          <td>AWS-only</td>
          <td>Multi-cloud（AWS / Azure / GCP / SaaS）</td>
          <td>Multi-cloud + Kubernetes pod-level + SaaS</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Free（API 按 request 微收）</td>
          <td>Per-cloud-spend % 或 fixed tier</td>
          <td>Per-cloud-spend % 或 fixed tier</td>
      </tr>
      <tr>
          <td>治理層級</td>
          <td>Account / service / tag / usage type</td>
          <td>Enterprise FinOps policy、showback chargeback</td>
          <td>Engineering self-serve、業務團隊自助查詢</td>
      </tr>
      <tr>
          <td>Kubernetes</td>
          <td>EKS service-level、不到 pod / namespace</td>
          <td>Container module 補位</td>
          <td>內建 Kubernetes cost allocation</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — 跟 AWS billing 同源、隨時可切</td>
          <td>中 — policy / showback rule 量多</td>
          <td>中 — query 跟 dashboard 量多</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>AWS-first、預算敏感、團隊小</td>
          <td>Enterprise、多雲、需要 chargeback</td>
          <td>Cloud-native、跨雲、engineering 自助 FinOps</td>
      </tr>
  </tbody>
</table>
<p>選 Cost Explorer 的核心訴求：<em>AWS-only + free + 跟 Billing / Budgets / Anomaly Detection 同 IAM 邊界</em>。當需求出現 <em>跨雲對帳</em> / <em>Kubernetes pod-level chargeback</em> / <em>SaaS license 整合</em>、就改走 CloudHealth / Vantage。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Cost Anomaly Detection</strong>：基於 ML 的 cost spike 偵測、按 service / linked account / cost category / tag 建 monitor、anomaly score 超 threshold 就 alert。實務治理：先用 <em>AWS services</em> monitor 全 service 跑 2-4 週看 baseline、再針對高變動 service（EC2 / Data Transfer / S3）建 dedicated monitor 拉緊 threshold、alert 接 SNS → Slack / PagerDuty。false positive 主要來自 release event 或 batch job、用 dimensional filter（exclude 特定 usage type / region）+ subscribe threshold 調 absolute USD + percentage 雙條件。</p>
<p><strong>Budgets + Forecast</strong>：Budget 可設 monthly / quarterly / annual、threshold 走 actual 跟 forecast 兩條 — forecast 達 80% 先 warn、actual 達 100% 才 page。Forecast 基於過去 historical pattern + linear extrapolation、新 workload / peak event 前要手動調整或關 forecast alert 避免噪音。Budget action 可以自動執行 IAM policy / SCP（例如 dev account 超預算自動 detach attach role）、但 production 別開、誤殺風險高。</p>
<p><strong>CUR (Cost &amp; Usage Report) + S3 + Athena / QuickSight</strong>：CUR 是 hourly granularity、含 resource ID、reserved instance / savings plan attribution、cost allocation tag 全欄位的 raw billing data、寫到 S3 bucket（Parquet 格式）。標準 pipeline：CUR → S3 → Glue Crawler → Athena → QuickSight dashboard、或直接拉到 BigQuery / Snowflake 跟其他維度 join（release calendar / SLO / traffic）。CUR 2.0 / Data Exports 是新版、欄位 schema 穩定、recommend 新部署直接走 CUR 2.0。</p>
<p><strong>Reserved Instance + Savings Plan recommendation</strong>：Cost Explorer 內建 RI / SP recommendation engine、看 past 7 / 30 / 60 day usage、推薦 commitment term（1yr / 3yr）+ payment option（All Upfront / Partial / No Upfront）+ break-even point。實務做法：先看 <em>Compute Savings Plan</em>（覆蓋 EC2 / Fargate / Lambda）的 baseline、再看 <em>EC2 Instance Savings Plan</em>（鎖 family + region）加深、最後看 RI 鎖 specific instance type — 三層疊加可達 60-70% saving、但 commitment 風險也疊加、要對齊 capacity planning。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Tag-based allocation 拆不到 instance / 比例異常</strong>：cost allocation tag 沒在 Billing Console activate（即使 EC2 tag 有設、billing 沒看到）— 進 Billing Console → Cost Allocation Tags → activate、要等 24hr CUR 才回填。Untagged resource 比例 &gt; 10% 直接代表 tag policy 沒落地、補 AWS Config rule 或 SCP 強制 tag。</li>
<li><strong>CUR delivery lag / 資料對不上 Console</strong>：CUR delivery 是 daily、月底結算後 finalized 還要等 1-3 天、月中看 CUR 跟 Console 有 % 差是正常 — 月中 review 用 Console、月底結算用 CUR finalized。如果 CUR 過了 48hr 還沒 delivery、檢查 S3 bucket policy 跟 CUR report status。</li>
<li><strong>Anomaly Detection false positive 多</strong>：threshold 設太嚴（absolute USD 太低 / percentage 太敏感）、或 monitor scope 太寬（包含 dev / sandbox account）— 拆 monitor 按 environment 分、production 抓 absolute USD + percentage 雙條件、dev 降低敏感度或關。</li>
<li><strong>Forecast 跳水 / 跳漲不合理</strong>：forecast 用 linear extrapolation、月中 spike / drop 會被放大、release 前 / peak event 前 forecast 不準 — 用 actual + Budget threshold 校正、別只看 forecast 決策。</li>
<li><strong>API rate limit / 查詢費用爆增</strong>：內部 dashboard 沒 cache 直接打 Cost Explorer API、每 request USD 0.01 月底結算 USD 數千 — cache 層 1hr TTL、time range 對齊 daily granularity、別 per-minute polling。</li>
<li><strong>Cost Category rule 衝突 / unallocated 過多</strong>：rule 設有 overlap 但 priority 沒設、或 rule 沒覆蓋新 service — Cost Category 走 explicit priority + default rule、新 service launch 進 owner checklist。</li>
</ul>
<h2 id="操作成本">操作成本</h2>
<p>Cost Explorer 的主要成本是資料治理。Tag、Cost Category、account structure、reservation sharing 與 owner mapping 要先整理，報表才會對工程團隊有行動意義。</p>
<p>API 整合需要查詢治理。程式化查詢要控制權限、頻率、cache、time range 與 paginated request 成本，避免內部 dashboard 造成額外查詢浪費。</p>
<p>成本解釋需要補業務 context。Cost Explorer 可以指出哪個 service 或 usage type 變貴；真正的工程判斷還要接 release、traffic、peak event、data retention、capacity policy 與 SLO 變化。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>AWS Cost Explorer 結果應回寫到 AWS cost evidence package。最小欄位包括 report name、group by、filter、time range、account、service、region、tag、usage type、forecast、recommendation、owner 與 action item。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>AWS Cost Explorer 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>Cost Explorer report、Cost Explorer API、RI / rightsizing recommendation</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>billing period、daily trend、forecast period</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>AWS Console report、API query、internal dashboard</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>tag coverage、Cost Category rule、data freshness</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>owner mapping、trend repeatability、billing delay</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>shared cost rule、multi-cloud gap、Kubernetes pod-level gap</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 AWS 成本 review 可以重跑。Cost Explorer report 要能回答「查詢條件是什麼、成本變化在哪個維度、誰負責處理、下次如何確認改善」。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>AWS Cost Explorer 目前適合作為 AWS-first 成本案例的 baseline 工具。它可回寫到 <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 consolidation</a> 的跨 DB 整併與 28% 成本下降驗證、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow modern data architecture</a> 的 80 TB 多副本 → 單一 source of truth + 80% 分析成本下降、<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> 的 on-demand vs over-provisioned 對照、以及 <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair GCP burst</a> 的 hybrid 模式 AWS-side baseline 釐清（即使是跨雲案例、AWS 側的 review 仍可用 Cost Explorer 跑）。</p>
<p>這些案例的重點是成本訊號到工程行動的轉換。Cost Explorer 頁引用案例時，要把 report 維度、變化原因、服務 owner、容量調整與驗證方式寫成可重跑流程 — Netflix 28% 下降要對應 Aurora cluster 數、IO-Optimized 切換時機與 reader replica 配比。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04 可觀測性成本歸因</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a></li>
<li>官方：<a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ce-what-is.html">AWS Cost Explorer documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 +75%、成本 -28%</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/</guid><description>&lt;p>這個案例的核心責任是說明 Netflix 在 AWS 上的「資料庫統一」決策、跟 &lt;a href="https://tarrragon.github.io/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 EKS 多集群&lt;/a> 形成對照。Riot 走「single-tenant per workload、246 個 cluster」、Netflix 走「跨 application 統一 Aurora、減少 DB 種類」 — 兩條路徑都是大規模平台的 &lt;em>合理&lt;/em> 選擇、但工程哲學完全不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Netflix 在 Aurora 整合的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/blogs/database/netflix-consolidates-relational-database-infrastructure-on-amazon-aurora-achieving-up-to-75-improved-performance/">Netflix consolidates relational database infrastructure on Amazon Aurora&lt;/a>）：&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>效能提升&lt;/td>
 &lt;td>up to 75%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本下降&lt;/td>
 &lt;td>28%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>月串流時數&lt;/td>
 &lt;td>billions of hours&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>global&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>整合範圍&lt;/td>
 &lt;td>多套 relational DB → Aurora&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>微服務架構&lt;/td>
 &lt;td>全球分散式 microservices&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容器編排&lt;/td>
 &lt;td>Amazon EKS&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Netflix 整體 AWS 使用：「Netflix uses AWS to deliver billions of hours of content monthly and runs its analytics platform for optimum performance of its global service. AWS enables Netflix to quickly deploy thousands of servers and terabytes of storage within minutes.」&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Netflix Aurora 整合揭露三個大規模平台 DB 治理重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>「DB 種類太多」本身是規模化的成本&lt;/strong>：Netflix 過往用 PostgreSQL、MySQL、Oracle 等不同 RDB、每個都需要不同 DBA 知識、不同備份、不同 monitoring 流程。整合到 Aurora 不只是「換 DB」、是「降低運維 surface area」、釋放工程資源。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的人力成本工程化、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &amp;#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &amp;#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom&lt;/a> 同類訴求。&lt;/li>
&lt;li>&lt;strong>75% performance improvement 是 Aurora storage layer 的本質優勢&lt;/strong>：Aurora 把 storage 跟 compute 分離、storage 用分散式 log-based 設計、replication 在 storage 層處理、不在 compute 層 — 這讓 read replica 不會受 master 寫入壓力影響、性能曲線比傳統 RDB 平滑。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 的儲存層 vs 計算層分離。&lt;/li>
&lt;li>&lt;strong>Netflix 的 DB 工作負載大多是「微服務私有 store」&lt;/strong>：Netflix 微服務各自有自己的 Aurora cluster、不共用 — 跟 monolith 「一個大 DB 撐全部」相反。這層架構讓「DB 容量規劃」變成「每個微服務的容量規劃」、複雜度分散。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 service decomposition、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&amp;#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&amp;#43; 個微服務承載 8 倍峰值流量、跨 200&amp;#43; 城市">9.C7 Lyft 微服務&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Netflix 在 AWS 上的「資料庫統一」決策、跟 <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 EKS 多集群</a> 形成對照。Riot 走「single-tenant per workload、246 個 cluster」、Netflix 走「跨 application 統一 Aurora、減少 DB 種類」 — 兩條路徑都是大規模平台的 <em>合理</em> 選擇、但工程哲學完全不同。</p>
<h2 id="觀察">觀察</h2>
<p>Netflix 在 Aurora 整合的關鍵敘述（引自 <a href="https://aws.amazon.com/blogs/database/netflix-consolidates-relational-database-infrastructure-on-amazon-aurora-achieving-up-to-75-improved-performance/">Netflix consolidates relational database infrastructure on Amazon Aurora</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>效能提升</td>
          <td>up to 75%</td>
      </tr>
      <tr>
          <td>成本下降</td>
          <td>28%</td>
      </tr>
      <tr>
          <td>月串流時數</td>
          <td>billions of hours</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>global</td>
      </tr>
      <tr>
          <td>整合範圍</td>
          <td>多套 relational DB → Aurora</td>
      </tr>
      <tr>
          <td>微服務架構</td>
          <td>全球分散式 microservices</td>
      </tr>
      <tr>
          <td>容器編排</td>
          <td>Amazon EKS</td>
      </tr>
  </tbody>
</table>
<p>Netflix 整體 AWS 使用：「Netflix uses AWS to deliver billions of hours of content monthly and runs its analytics platform for optimum performance of its global service. AWS enables Netflix to quickly deploy thousands of servers and terabytes of storage within minutes.」</p>
<h2 id="判讀">判讀</h2>
<p>Netflix Aurora 整合揭露三個大規模平台 DB 治理重點。</p>
<ol>
<li><strong>「DB 種類太多」本身是規模化的成本</strong>：Netflix 過往用 PostgreSQL、MySQL、Oracle 等不同 RDB、每個都需要不同 DBA 知識、不同備份、不同 monitoring 流程。整合到 Aurora 不只是「換 DB」、是「降低運維 surface area」、釋放工程資源。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的人力成本工程化、跟 <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> 同類訴求。</li>
<li><strong>75% performance improvement 是 Aurora storage layer 的本質優勢</strong>：Aurora 把 storage 跟 compute 分離、storage 用分散式 log-based 設計、replication 在 storage 層處理、不在 compute 層 — 這讓 read replica 不會受 master 寫入壓力影響、性能曲線比傳統 RDB 平滑。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的儲存層 vs 計算層分離。</li>
<li><strong>Netflix 的 DB 工作負載大多是「微服務私有 store」</strong>：Netflix 微服務各自有自己的 Aurora cluster、不共用 — 跟 monolith 「一個大 DB 撐全部」相反。這層架構讓「DB 容量規劃」變成「每個微服務的容量規劃」、複雜度分散。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 service decomposition、跟 <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 微服務</a>。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「effective 75% improvement」是 <em>跨多個 workload 的最大改善幅度</em>、不是「每個 workload 都 +75%」。實際每個 workload 改善幅度從 10% 到 75% 不等。</li>
<li>Netflix 數據層遠不止 Aurora — 還有 Cassandra（playback metadata）、EVCache（cache layer）、Iceberg（data warehouse）。Aurora 主要是「需要 ACID 的 OLTP 工作負載」、不是「all-purpose store」。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>DB 種類整合是規模化的必要工程</strong>：每多一種 DB 就多一套運維 surface。在能合理 consolidate 的時候整合、降低 ops 複雜度。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 vendor diversity 取捨。</li>
<li><strong>storage / compute 分離是 OLTP 擴容的關鍵</strong>：Aurora、Spanner、TiDB 都採類似設計、是現代 cloud DB 的共同特徵。對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> 的 storage layer 設計。</li>
<li><strong>微服務私有 store 比共用 DB 容量規劃簡單</strong>：每個服務各自管 DB 容量、跨服務 contention 變成 <em>network 議題</em> 而非 <em>DB lock 議題</em>。</li>
<li><strong>大規模平台必須區分「OLTP 用 Aurora」「analytics 用 data lake」「KV 用 DynamoDB」「cache 用 EVCache」</strong>：Netflix 用各種 DB、不是一招打天下。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 polyglot persistence。</li>
</ol>
<p>跨平台等效：GCP Spanner（替代 OLTP）+ Bigtable（替代 KV）+ BigQuery（替代 analytics）；Azure Cosmos DB（替代多 model）+ SQL Hyperscale + Synapse — 各雲商提供類似 stack。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他大規模平台 → <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games EKS</a>（不同 consolidation 策略）</li>
<li>想理解 Aurora 設計 → <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> + <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a></li>
<li>想做 polyglot persistence 選型 → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
<li>想做 DB consolidation 規劃 → <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a></li>
<li>想理解 +75% 的 storage / compute 解耦根因 → <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">Aurora 儲存層架構</a></li>
<li>想規劃自管 PostgreSQL / MySQL 遷入 Aurora 的步驟 → <a href="/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/" data-link-title="從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook" data-link-desc="PostgreSQL / MySQL → Aurora 的 Type C operational redesign hybrid playbook、6 規格面（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、Standard Chartered 合規 lead time 模型、Netflix 非 all-purpose store 邊界">從自管 PostgreSQL/MySQL 遷入 Aurora</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/database/netflix-consolidates-relational-database-infrastructure-on-amazon-aurora-achieving-up-to-75-improved-performance/">Netflix consolidates relational database infrastructure on Amazon Aurora, achieving up to 75% improved performance</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/innovators/netflix/">Netflix on AWS</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/netflix-case-study/">Netflix Case Study</a></li>
</ul>
]]></content:encoded></item><item><title>8.23 Control Plane Decision Log and Write-back 實作示範</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/control-plane-decision-log-write-back/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/control-plane-decision-log-write-back/</guid><description>&lt;p>Control plane decision log and write-back 的核心責任是讓規則或配置事故的事中判斷可回放、事後修正可追蹤。&lt;/p>
&lt;h2 id="服務路徑與事件邊界">服務路徑與事件邊界&lt;/h2>
&lt;p>示範事件是全域 rule rollout 後 CPU 激增與錯誤率上升。這類事故的難點在決策序列是否清楚、偵測本身反而容易：先限流、先回退、還是先分區隔離。&lt;/p>
&lt;p>事中決策欄位固定用 &lt;code>Timestamp&lt;/code>、&lt;code>Decision&lt;/code>、&lt;code>Context&lt;/code>、&lt;code>Evidence&lt;/code>、&lt;code>Owner&lt;/code>、&lt;code>Expected effect&lt;/code>、&lt;code>Rollback condition&lt;/code>。write-back 再補 &lt;code>target artifact&lt;/code>、&lt;code>closure signal&lt;/code>、&lt;code>review date&lt;/code>。&lt;/p>
&lt;h2 id="實作步驟">實作步驟&lt;/h2>
&lt;ol>
&lt;li>建立 incident intake：彙整告警、dashboard、客訴與 deploy event。&lt;/li>
&lt;li>啟動 decision log：每個會改變路由的動作都記錄欄位。&lt;/li>
&lt;li>每 10-15 分鐘更新一次 expected effect 是否達成。&lt;/li>
&lt;li>事故收斂後建立 write-back 條目：對應到 runbook、gate、signal 或 ownership 缺口。&lt;/li>
&lt;li>在下一次 readiness review 檢查 closure signal 是否達成。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&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>事故頻道討論很多但決策記錄很少&lt;/td>
 &lt;td>已決事項與討論事項混在一起&lt;/td>
 &lt;td>強制 decision log 欄位化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後暫時恢復但再次抖動&lt;/td>
 &lt;td>rollback condition 不完整&lt;/td>
 &lt;td>補充次級門檻與觀察窗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通訊內容與內部判斷不一致&lt;/td>
 &lt;td>evidence 版本不同步&lt;/td>
 &lt;td>以 decision log 為唯一對外事實來源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>write-back 列很多但無人關閉&lt;/td>
 &lt;td>owner 與 review date 缺失&lt;/td>
 &lt;td>補責任人與 closure signal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同類事故重複發生&lt;/td>
 &lt;td>回寫只寫故事，沒進入上游控制面&lt;/td>
 &lt;td>把項目映射到 4.20/6.8/6.23&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 decision log 當成事後整理會失去事故價值。事故當下不記，事後只能用記憶補洞，容易產生 hindsight 偏差。&lt;/p>
&lt;p>把 write-back 當成待辦清單也會失效。沒有 &lt;code>closure signal&lt;/code> 的改善項目很快會退化成長期債務。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>這條路徑可用 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">Cloudflare 2023 Workers KV Deployment Tool Misconfiguration&lt;/a> 回寫。先看控制面變更如何擴散，再回到本章檢查決策欄位與回寫欄位是否能完整重放事故節奏。&lt;/p>
&lt;p>這個案例主要支撐的是「控制面決策可回放」判讀，不直接支撐 provider dependency gate 門檻；放行策略回到 6.25/6.8。&lt;/p>
&lt;h2 id="跨模組路由">跨模組路由&lt;/h2>
&lt;ol>
&lt;li>與 8.19 的交接：欄位語言與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log&lt;/a> 對齊。&lt;/li>
&lt;li>與 8.22 的交接：回寫欄位與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back&lt;/a> 對齊。&lt;/li>
&lt;li>與 6.24 的交接：控制面事故停損條件回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">Rule Rollout Safety Gate&lt;/a>。&lt;/li>
&lt;li>與 4.20 的交接：證據來源統一到 observability evidence package。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要把控制面事故前移到資安治理，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence 實作示範&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Control plane decision log and write-back 的核心責任是讓規則或配置事故的事中判斷可回放、事後修正可追蹤。</p>
<h2 id="服務路徑與事件邊界">服務路徑與事件邊界</h2>
<p>示範事件是全域 rule rollout 後 CPU 激增與錯誤率上升。這類事故的難點在決策序列是否清楚、偵測本身反而容易：先限流、先回退、還是先分區隔離。</p>
<p>事中決策欄位固定用 <code>Timestamp</code>、<code>Decision</code>、<code>Context</code>、<code>Evidence</code>、<code>Owner</code>、<code>Expected effect</code>、<code>Rollback condition</code>。write-back 再補 <code>target artifact</code>、<code>closure signal</code>、<code>review date</code>。</p>
<h2 id="實作步驟">實作步驟</h2>
<ol>
<li>建立 incident intake：彙整告警、dashboard、客訴與 deploy event。</li>
<li>啟動 decision log：每個會改變路由的動作都記錄欄位。</li>
<li>每 10-15 分鐘更新一次 expected effect 是否達成。</li>
<li>事故收斂後建立 write-back 條目：對應到 runbook、gate、signal 或 ownership 缺口。</li>
<li>在下一次 readiness review 檢查 closure signal 是否達成。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事故頻道討論很多但決策記錄很少</td>
          <td>已決事項與討論事項混在一起</td>
          <td>強制 decision log 欄位化</td>
      </tr>
      <tr>
          <td>回退後暫時恢復但再次抖動</td>
          <td>rollback condition 不完整</td>
          <td>補充次級門檻與觀察窗</td>
      </tr>
      <tr>
          <td>通訊內容與內部判斷不一致</td>
          <td>evidence 版本不同步</td>
          <td>以 decision log 為唯一對外事實來源</td>
      </tr>
      <tr>
          <td>write-back 列很多但無人關閉</td>
          <td>owner 與 review date 缺失</td>
          <td>補責任人與 closure signal</td>
      </tr>
      <tr>
          <td>同類事故重複發生</td>
          <td>回寫只寫故事，沒進入上游控制面</td>
          <td>把項目映射到 4.20/6.8/6.23</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 decision log 當成事後整理會失去事故價值。事故當下不記，事後只能用記憶補洞，容易產生 hindsight 偏差。</p>
<p>把 write-back 當成待辦清單也會失效。沒有 <code>closure signal</code> 的改善項目很快會退化成長期債務。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>這條路徑可用 <a href="/blog/backend/08-incident-response/cases/cloudflare/2023-workers-kv-deployment-tool-misconfiguration/" data-link-title="Cloudflare 2023 Workers KV Deployment Tool Misconfiguration" data-link-desc="2023-10-30 Cloudflare 控制面事故：deployment tool 設定錯誤造成 Workers KV 連鎖影響，重點在變更範圍限制與決策回寫。">Cloudflare 2023 Workers KV Deployment Tool Misconfiguration</a> 回寫。先看控制面變更如何擴散，再回到本章檢查決策欄位與回寫欄位是否能完整重放事故節奏。</p>
<p>這個案例主要支撐的是「控制面決策可回放」判讀，不直接支撐 provider dependency gate 門檻；放行策略回到 6.25/6.8。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 8.19 的交接：欄位語言與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a> 對齊。</li>
<li>與 8.22 的交接：回寫欄位與 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a> 對齊。</li>
<li>與 6.24 的交接：控制面事故停損條件回到 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">Rule Rollout Safety Gate</a>。</li>
<li>與 4.20 的交接：證據來源統一到 observability evidence package。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把控制面事故前移到資安治理，接著讀 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence 實作示範</a>。</p>
]]></content:encoded></item><item><title>Netflix：FIT 證據交接與 Release Gate 回寫</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/</guid><description>&lt;p>FIT（Failure Injection Testing）的核心責任是產生可決策的證據，故障演示只是過程。當實驗結果無法直接回答「能不能放行」，FIT 就只是測試活動，不是可靠性控制面。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>團隊常在故障注入後留下 dashboard 截圖與結論摘要，但 release decision 仍靠主觀討論。這種斷裂會讓同類風險反覆出現，因為每次都在重新辯論，而不是沿用同一套 evidence 欄位。&lt;/p>
&lt;h2 id="決策機制">決策機制&lt;/h2>
&lt;p>要讓 FIT 成為 release gate 輸入，必須把實驗輸出結構化成決策欄位。&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>steady-state impact&lt;/td>
 &lt;td>注入後是否仍維持服務承諾&lt;/td>
 &lt;td>判斷能否繼續 rollout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>abort trigger record&lt;/td>
 &lt;td>停止條件是否被觸發、何時觸發&lt;/td>
 &lt;td>判斷是否進入凍結與回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback result&lt;/td>
 &lt;td>降級路徑是否可用、恢復是否收斂&lt;/td>
 &lt;td>判斷事故時能否安全止血&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dependency drift&lt;/td>
 &lt;td>受影響依賴是否落在預期範圍&lt;/td>
 &lt;td>判斷 blast radius 是否可接受&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>verification evidence&lt;/td>
 &lt;td>證據是否足以支持 release&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rule rollout anomaly&lt;/td>
 &lt;td>規則推送後是否偏離預期&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>incident decision lag&lt;/td>
 &lt;td>事故時是否可快速調用證據&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>evidence write-back&lt;/td>
 &lt;td>教訓是否回寫成下次驗證輸入&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>最常見錯誤是把 FIT 報告寫成敘事文件，沒有決策欄位，導致放行時無法直接引用。另一個錯誤是只記錄成功路徑，忽略 abort trigger 與 fallback 失敗，讓風險被低估。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先把 FIT 輸出整理到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff&lt;/a>，再接到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate&lt;/a> 做放行判斷。事故發生時由 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a> 快速提取決策證據，最後回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://netflixtechblog.com/fit-failure-injection-testing-35d8e2a9bb2e">FIT: Failure Injection Testing&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>FIT（Failure Injection Testing）的核心責任是產生可決策的證據，故障演示只是過程。當實驗結果無法直接回答「能不能放行」，FIT 就只是測試活動，不是可靠性控制面。</p>
<h2 id="問題場景">問題場景</h2>
<p>團隊常在故障注入後留下 dashboard 截圖與結論摘要，但 release decision 仍靠主觀討論。這種斷裂會讓同類風險反覆出現，因為每次都在重新辯論，而不是沿用同一套 evidence 欄位。</p>
<h2 id="決策機制">決策機制</h2>
<p>要讓 FIT 成為 release gate 輸入，必須把實驗輸出結構化成決策欄位。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>核心問題</th>
          <th>決策用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>steady-state impact</td>
          <td>注入後是否仍維持服務承諾</td>
          <td>判斷能否繼續 rollout</td>
      </tr>
      <tr>
          <td>abort trigger record</td>
          <td>停止條件是否被觸發、何時觸發</td>
          <td>判斷是否進入凍結與回退</td>
      </tr>
      <tr>
          <td>fallback result</td>
          <td>降級路徑是否可用、恢復是否收斂</td>
          <td>判斷事故時能否安全止血</td>
      </tr>
      <tr>
          <td>dependency drift</td>
          <td>受影響依賴是否落在預期範圍</td>
          <td>判斷 blast radius 是否可接受</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>verification evidence</td>
          <td>證據是否足以支持 release</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
      <tr>
          <td>rule rollout anomaly</td>
          <td>規則推送後是否偏離預期</td>
          <td><a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a></td>
      </tr>
      <tr>
          <td>incident decision lag</td>
          <td>事故時是否可快速調用證據</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>evidence write-back</td>
          <td>教訓是否回寫成下次驗證輸入</td>
          <td><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>最常見錯誤是把 FIT 報告寫成敘事文件，沒有決策欄位，導致放行時無法直接引用。另一個錯誤是只記錄成功路徑，忽略 abort trigger 與 fallback 失敗，讓風險被低估。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先把 FIT 輸出整理到 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>，再接到 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 Rule Rollout Safety Gate</a> 做放行判斷。事故發生時由 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 快速提取決策證據，最後回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://netflixtechblog.com/fit-failure-injection-testing-35d8e2a9bb2e">FIT: Failure Injection Testing</a></li>
<li><a href="https://github.com/Netflix/chaosmonkey">Netflix/chaosmonkey</a></li>
</ul>
]]></content:encoded></item><item><title>6.23 Verification Evidence Handoff</title><link>https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>verification evidence handoff 的責任：把可靠性驗證結果交給 release gate、runbook 與 incident response&lt;/li>
&lt;li>來源：SLO policy、load test、chaos experiment、DR drill、rollback rehearsal、readiness review&lt;/li>
&lt;li>欄位：hypothesis、steady state、result、scope、evidence package、decision、owner、next route&lt;/li>
&lt;li>跟 4.20 的關係：使用同一 evidence package 格式承接 observability 證據&lt;/li>
&lt;li>跟 8.22 的關係：事故復盤會回寫新的驗證題目與證據缺口&lt;/li>
&lt;li>反模式：驗證做完只留結論；load test 圖表沒有 workload；chaos 成功但沒有 runbook 回寫&lt;/li>
&lt;/ul>
&lt;p>Verification evidence handoff 的核心是把可靠性驗證從「做過測試」升級成「留下可用證據」。驗證結果需要能進 release gate、runbook、incident drill 與 post-incident review，才會形成跨模組閉環。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Verification evidence handoff 是可靠性模組交給發布與事故流程的證據交接，責任是讓 SLO、load、chaos、DR 與 readiness 結果能被決策使用。&lt;/p>
&lt;p>這一頁處理的是驗證結果的交付格式。6.20 定義實驗安全邊界，6.22 定義 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>；本章把這些驗證輸出整理成可以被 05 release、08 incident response 與 04 observability 回寫使用的 artifact。&lt;/p>
&lt;p>驗證證據的價值在於支援未來決策。一次 load test 的圖表、一次 chaos 成功、一次 DR drill 通過，如果沒有 hypothesis、scope、steady state、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 action item，後續團隊很難知道它證明了什麼。&lt;/p>
&lt;h2 id="handoff-欄位">Handoff 欄位&lt;/h2>
&lt;p>Verification evidence handoff 的欄位要同時保存驗證前提、觀測證據與決策結果。欄位的目標是讓下游能判斷「這個驗證能支持哪個決策」。&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>Hypothesis&lt;/td>
 &lt;td>說明要驗證的 failure mode&lt;/td>
 &lt;td>判斷實驗是否回答原問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope&lt;/td>
 &lt;td>標示服務、tenant、region 範圍&lt;/td>
 &lt;td>防止把局部結果外推&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Steady state&lt;/td>
 &lt;td>定義成功條件&lt;/td>
 &lt;td>判斷是否通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workload / Fault&lt;/td>
 &lt;td>記錄流量模型或故障注入&lt;/td>
 &lt;td>支援重播&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence package&lt;/td>
 &lt;td>連到 log、metric、trace&lt;/td>
 &lt;td>支援查證與 handoff&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Result&lt;/td>
 &lt;td>pass、conditional、fail&lt;/td>
 &lt;td>接 release gate 與 readiness&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision&lt;/td>
 &lt;td>放行、阻擋、補驗證、補 runbook&lt;/td>
 &lt;td>把結果轉成動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>指定後續責任人&lt;/td>
 &lt;td>支援 action item closure&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Hypothesis 欄位讓驗證聚焦。&lt;code>打掉 node&lt;/code> 只是操作；&lt;code>打掉一個 worker 後 queue lag 應在 10 分鐘內回到 baseline&lt;/code> 才是可判讀假設。&lt;/p>
&lt;p>Scope 欄位保護結論邊界。internal traffic、single tenant、one region、10% production traffic 與 full production 都是不同證據強度，handoff 需要明確標示。&lt;/p>
&lt;p>Evidence package 欄位接 4.20。驗證結果應保存 dashboard、query、trace、log、client-side signal、time range 與 data quality 限制，讓 release gate 或 incident response 可以回放。&lt;/p>
&lt;p>Result 欄位需要分層。Pass 代表在指定 scope 內符合 steady state；conditional 代表可接受但有缺口；fail 代表需要補設計、補訊號、補 runbook 或阻擋 release。&lt;/p>
&lt;h2 id="驗證來源">驗證來源&lt;/h2>
&lt;p>Verification evidence 的來源分成政策、容量、故障、回復與準備度。不同來源回答的決策問題不同。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>verification evidence handoff 的責任：把可靠性驗證結果交給 release gate、runbook 與 incident response</li>
<li>來源：SLO policy、load test、chaos experiment、DR drill、rollback rehearsal、readiness review</li>
<li>欄位：hypothesis、steady state、result、scope、evidence package、decision、owner、next route</li>
<li>跟 4.20 的關係：使用同一 evidence package 格式承接 observability 證據</li>
<li>跟 8.22 的關係：事故復盤會回寫新的驗證題目與證據缺口</li>
<li>反模式：驗證做完只留結論；load test 圖表沒有 workload；chaos 成功但沒有 runbook 回寫</li>
</ul>
<p>Verification evidence handoff 的核心是把可靠性驗證從「做過測試」升級成「留下可用證據」。驗證結果需要能進 release gate、runbook、incident drill 與 post-incident review，才會形成跨模組閉環。</p>
<h2 id="概念定位">概念定位</h2>
<p>Verification evidence handoff 是可靠性模組交給發布與事故流程的證據交接，責任是讓 SLO、load、chaos、DR 與 readiness 結果能被決策使用。</p>
<p>這一頁處理的是驗證結果的交付格式。6.20 定義實驗安全邊界，6.22 定義 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>；本章把這些驗證輸出整理成可以被 05 release、08 incident response 與 04 observability 回寫使用的 artifact。</p>
<p>驗證證據的價值在於支援未來決策。一次 load test 的圖表、一次 chaos 成功、一次 DR drill 通過，如果沒有 hypothesis、scope、steady state、<a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 action item，後續團隊很難知道它證明了什麼。</p>
<h2 id="handoff-欄位">Handoff 欄位</h2>
<p>Verification evidence handoff 的欄位要同時保存驗證前提、觀測證據與決策結果。欄位的目標是讓下游能判斷「這個驗證能支持哪個決策」。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hypothesis</td>
          <td>說明要驗證的 failure mode</td>
          <td>判斷實驗是否回答原問題</td>
      </tr>
      <tr>
          <td>Scope</td>
          <td>標示服務、tenant、region 範圍</td>
          <td>防止把局部結果外推</td>
      </tr>
      <tr>
          <td>Steady state</td>
          <td>定義成功條件</td>
          <td>判斷是否通過</td>
      </tr>
      <tr>
          <td>Workload / Fault</td>
          <td>記錄流量模型或故障注入</td>
          <td>支援重播</td>
      </tr>
      <tr>
          <td>Evidence package</td>
          <td>連到 log、metric、trace</td>
          <td>支援查證與 handoff</td>
      </tr>
      <tr>
          <td>Result</td>
          <td>pass、conditional、fail</td>
          <td>接 release gate 與 readiness</td>
      </tr>
      <tr>
          <td>Decision</td>
          <td>放行、阻擋、補驗證、補 runbook</td>
          <td>把結果轉成動作</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定後續責任人</td>
          <td>支援 action item closure</td>
      </tr>
  </tbody>
</table>
<p>Hypothesis 欄位讓驗證聚焦。<code>打掉 node</code> 只是操作；<code>打掉一個 worker 後 queue lag 應在 10 分鐘內回到 baseline</code> 才是可判讀假設。</p>
<p>Scope 欄位保護結論邊界。internal traffic、single tenant、one region、10% production traffic 與 full production 都是不同證據強度，handoff 需要明確標示。</p>
<p>Evidence package 欄位接 4.20。驗證結果應保存 dashboard、query、trace、log、client-side signal、time range 與 data quality 限制，讓 release gate 或 incident response 可以回放。</p>
<p>Result 欄位需要分層。Pass 代表在指定 scope 內符合 steady state；conditional 代表可接受但有缺口；fail 代表需要補設計、補訊號、補 runbook 或阻擋 release。</p>
<h2 id="驗證來源">驗證來源</h2>
<p>Verification evidence 的來源分成政策、容量、故障、回復與準備度。不同來源回答的決策問題不同。</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>回答問題</th>
          <th>交接對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO / Error Budget</td>
          <td>可靠性目標是否仍有風險餘額</td>
          <td>release gate、severity trigger</td>
      </tr>
      <tr>
          <td>Load test</td>
          <td>workload 是否覆蓋容量與成本壓力</td>
          <td>capacity plan、release gate</td>
      </tr>
      <tr>
          <td>Chaos experiment</td>
          <td>failure mode 是否可被吸收</td>
          <td>runbook、incident drill</td>
      </tr>
      <tr>
          <td>DR drill</td>
          <td>RTO / RPO 是否可達</td>
          <td>business continuity、IR</td>
      </tr>
      <tr>
          <td>Rollback rehearsal</td>
          <td>版本或資料回復是否可執行</td>
          <td>deployment platform、incident</td>
      </tr>
      <tr>
          <td>Readiness review</td>
          <td>上線前風險是否已被判讀</td>
          <td>release gate、service owner</td>
      </tr>
  </tbody>
</table>
<p>SLO evidence 適合支援變更節奏。當 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 上升或 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 緊張，release gate 需要知道哪些 user journey 受影響、資料品質是否可信、freeze 是否觸發。</p>
<p>Load test evidence 適合支援容量與成本決策。它要保留 workload model、traffic shape、data volume、dependency saturation、cost threshold 與觀測限制。</p>
<p>Chaos evidence 適合支援 incident drill。它要保留 injected failure、steady state、stop condition、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、<a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a> 與 action item。</p>
<p>DR evidence 適合支援恢復承諾。它要保留切換步驟、資料同步、RTO / RPO、權限、通訊節奏與回復完成條件。</p>
<p>Rollback evidence 適合支援事故止血。它要保留版本、migration、feature flag、client compatibility、cache 與資料相容性。</p>
<h2 id="交接流程">交接流程</h2>
<p>Verification handoff 的流程是從驗證結果走向下游決策。每個結果都要明確路由，讓測試報告轉成 release、runbook 或 incident drill 的輸入。</p>
<ol>
<li>把驗證結果整理成 handoff 欄位。</li>
<li>附上 4.20 evidence package 與 data quality 限制。</li>
<li>判斷 result：pass、conditional、fail。</li>
<li>把 pass 送入 release gate 或 runbook。</li>
<li>把 conditional 送入 reliability debt 或 follow-up。</li>
<li>把 fail 送入 block、補驗證、補 observability 或 incident drill。</li>
</ol>
<p>Pass 的責任是支持後續放行。Pass 需要同時保留 scope，避免「小範圍通過」被誤用成「全域安全」。</p>
<p>Conditional 的責任是保留風險借款。若驗證結果可接受但缺 trace、runbook、owner 或資料校驗，應進入 reliability debt backlog，並設定 closure signal。</p>
<p>Fail 的責任是阻止風險下流。Fail 不只代表測試失敗，也可能代表 steady state 定義錯誤、evidence 不足、blast radius 過大或 stop condition 不清。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Verification evidence handoff 的反模式通常來自把驗證結果寫成結論，而沒有保留判讀過程。下游需要知道結論成立的條件。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只寫 pass / fail</td>
          <td>release gate 看不到證據</td>
          <td>補 hypothesis、scope、evidence</td>
      </tr>
      <tr>
          <td>Load 圖表缺 workload</td>
          <td>圖表存在但缺少重播條件</td>
          <td>保存 traffic shape 與 data volume</td>
      </tr>
      <tr>
          <td>Chaos 成功無 runbook</td>
          <td>實驗有效但事故時用不上</td>
          <td>回寫 runbook 與 drill</td>
      </tr>
      <tr>
          <td>DR 通過缺 RTO / RPO</td>
          <td>切換完成但缺少承諾對齊</td>
          <td>保存 recovery timeline</td>
      </tr>
      <tr>
          <td>Conditional 無關閉條件</td>
          <td>風險借款長期存在</td>
          <td>設定 owner 與 closure signal</td>
      </tr>
  </tbody>
</table>
<p>只寫 pass / fail 會讓驗證證據失去工程價值。Pass 要說明在什麼範圍、什麼假設、什麼資料品質下成立；fail 要說明哪個控制面失效。</p>
<p>Conditional 無關閉條件會讓可靠性債累積。每個 conditional handoff 都需要 owner、期限、closure signal 與重新評估條件。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>4.20 observability evidence package：承接 log、metric、trace 與 data quality</li>
<li>6.8 release gate：把驗證結果轉成放行、阻擋或例外</li>
<li>6.20 experiment safety：保存 blast radius、stop condition 與權限</li>
<li>6.21 reliability debt backlog：承接 conditional 與 follow-up</li>
<li>8.6 drills / on-call readiness：把驗證結果轉成值班演練</li>
<li>8.22 incident evidence write-back：承接事故後新增的驗證題目</li>
</ul>
]]></content:encoded></item><item><title>Meta / Facebook</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/</guid><description>&lt;p>Meta（前 Facebook）是超大規模分散式系統的代表、2021-10 BGP 全球失效事故是大規模事故敘事的教學標竿。Engineering blog 公開的 reliability 文章涵蓋 region failover、cell architecture 等深度實踐。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>BGP 與 DNS 自我封鎖：2021-10 事故揭露的內部依賴鎖死&lt;/li>
&lt;li>Region Failover：超大規模服務的跨區切換挑戰&lt;/li>
&lt;li>Cell Architecture：Facebook 規模下的 cell 設計&lt;/li>
&lt;li>Storm：Internal incident management 系統公開的設計&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄實踐">預計收錄實踐&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>教學重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>2021-10 BGP 事故&lt;/td>
 &lt;td>配置變更鎖死自己、recovery 工具失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Region Failover&lt;/td>
 &lt;td>超大規模 traffic shift 的設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storm IM System&lt;/td>
 &lt;td>內部 IR 工具的揭露&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reliability Reviews&lt;/td>
 &lt;td>服務級可靠性審查制度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Meta 這個案例在講的是超大規模系統如何面對全球級網路與控制面事故。讀者先抓 BGP、自我封鎖、region failover 與 MySQL Raft 這些原語，再把它們當成超大規模恢復能力的組件。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當外部路由或內部配置互相牽制時，事故會把恢復工具一起拖進失效狀態。當服務開始做更快的 failover 投資時，真正要看的是它是否能縮短恢復時間並降低手動介入成本，單點工具層面的評估遠遠不夠。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否分辨事故是路由、配置還是服務層問題&lt;/li>
&lt;li>能否說明 region failover 的前置條件&lt;/li>
&lt;li>能否把 IR 工具與對外說明串成一致時間線&lt;/li>
&lt;li>能否把資料庫 failover 投資對應到恢復時間縮短&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Meta 的價值在於把超大規模網路事故和恢復工具放在一起看，這和 AWS S3、GCP、Cloudflare 都是在談「控制面出事時會擴散多遠」。如果先讀 Meta，再回看其他案例，會更容易看出 region failover 和 route propagation 的真正成本。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>2021-10 BGP 事故顯示一個控制面變更可以讓整個公司失去對外可見性。&lt;/li>
&lt;li>MySQL Raft 代表的是把資料庫 failover 工具化，縮短人工介入時間。&lt;/li>
&lt;li>region failover 顯示超大規模 traffic shift 的成本。&lt;/li>
&lt;li>reliability reviews 讓服務級風險在變更前先被看見。&lt;/li>
&lt;li>cell architecture 讓大規模服務把故障切成可管理的單位。&lt;/li>
&lt;li>Storm 代表內部 incident management 工具如何支撐跨團隊協作。&lt;/li>
&lt;li>DNS 自我封鎖讓內外部控制面一起失效。&lt;/li>
&lt;li>traffic shift 讓恢復不只是切流量，而是管理整個依賴網。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">M1&lt;/a>&lt;/td>
 &lt;td>Region Failover 邊界治理&lt;/td>
 &lt;td>把跨區故障擴散限制在可回復邊界內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">M2&lt;/a>&lt;/td>
 &lt;td>BGP 事故與控制面恢復順序&lt;/td>
 &lt;td>恢復工具依賴已故障系統時的恢復順序與 out-of-band 設計&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/">More details about the October 4 outage&lt;/a>：Meta 2021-10 outage 的技術回顧。&lt;/li>
&lt;li>&lt;a href="https://engineering.fb.com/2021/10/04/networking-traffic/outage/">Update about the October 4th outage&lt;/a>：事故初始公開說明。&lt;/li>
&lt;li>&lt;a href="https://engineering.fb.com/2023/05/16/data-infrastructure/mysql-raft-meta/">Building and deploying MySQL Raft at Meta&lt;/a>：更快 failover 與可靠性投資。&lt;/li>
&lt;li>&lt;a href="https://engineering.fb.com/2014/06/05/core-infra/hydrabase-the-evolution-of-hbase-facebook/">HydraBase – The evolution of HBase@Facebook&lt;/a>：分散式儲存與 failover 的早期實踐。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Meta（前 Facebook）是超大規模分散式系統的代表、2021-10 BGP 全球失效事故是大規模事故敘事的教學標竿。Engineering blog 公開的 reliability 文章涵蓋 region failover、cell architecture 等深度實踐。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>BGP 與 DNS 自我封鎖：2021-10 事故揭露的內部依賴鎖死</li>
<li>Region Failover：超大規模服務的跨區切換挑戰</li>
<li>Cell Architecture：Facebook 規模下的 cell 設計</li>
<li>Storm：Internal incident management 系統公開的設計</li>
</ul>
<h2 id="預計收錄實踐">預計收錄實踐</h2>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2021-10 BGP 事故</td>
          <td>配置變更鎖死自己、recovery 工具失效</td>
      </tr>
      <tr>
          <td>Region Failover</td>
          <td>超大規模 traffic shift 的設計</td>
      </tr>
      <tr>
          <td>Storm IM System</td>
          <td>內部 IR 工具的揭露</td>
      </tr>
      <tr>
          <td>Reliability Reviews</td>
          <td>服務級可靠性審查制度</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Meta 這個案例在講的是超大規模系統如何面對全球級網路與控制面事故。讀者先抓 BGP、自我封鎖、region failover 與 MySQL Raft 這些原語，再把它們當成超大規模恢復能力的組件。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當外部路由或內部配置互相牽制時，事故會把恢復工具一起拖進失效狀態。當服務開始做更快的 failover 投資時，真正要看的是它是否能縮短恢復時間並降低手動介入成本，單點工具層面的評估遠遠不夠。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否分辨事故是路由、配置還是服務層問題</li>
<li>能否說明 region failover 的前置條件</li>
<li>能否把 IR 工具與對外說明串成一致時間線</li>
<li>能否把資料庫 failover 投資對應到恢復時間縮短</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Meta 的價值在於把超大規模網路事故和恢復工具放在一起看，這和 AWS S3、GCP、Cloudflare 都是在談「控制面出事時會擴散多遠」。如果先讀 Meta，再回看其他案例，會更容易看出 region failover 和 route propagation 的真正成本。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>2021-10 BGP 事故顯示一個控制面變更可以讓整個公司失去對外可見性。</li>
<li>MySQL Raft 代表的是把資料庫 failover 工具化，縮短人工介入時間。</li>
<li>region failover 顯示超大規模 traffic shift 的成本。</li>
<li>reliability reviews 讓服務級風險在變更前先被看見。</li>
<li>cell architecture 讓大規模服務把故障切成可管理的單位。</li>
<li>Storm 代表內部 incident management 工具如何支撐跨團隊協作。</li>
<li>DNS 自我封鎖讓內外部控制面一起失效。</li>
<li>traffic shift 讓恢復不只是切流量，而是管理整個依賴網。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">M1</a></td>
          <td>Region Failover 邊界治理</td>
          <td>把跨區故障擴散限制在可回復邊界內</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">M2</a></td>
          <td>BGP 事故與控制面恢復順序</td>
          <td>恢復工具依賴已故障系統時的恢復順序與 out-of-band 設計</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/">More details about the October 4 outage</a>：Meta 2021-10 outage 的技術回顧。</li>
<li><a href="https://engineering.fb.com/2021/10/04/networking-traffic/outage/">Update about the October 4th outage</a>：事故初始公開說明。</li>
<li><a href="https://engineering.fb.com/2023/05/16/data-infrastructure/mysql-raft-meta/">Building and deploying MySQL Raft at Meta</a>：更快 failover 與可靠性投資。</li>
<li><a href="https://engineering.fb.com/2014/06/05/core-infra/hydrabase-the-evolution-of-hbase-facebook/">HydraBase – The evolution of HBase@Facebook</a>：分散式儲存與 failover 的早期實踐。</li>
</ul>
]]></content:encoded></item><item><title>Microsoft 365</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/</guid><description>&lt;p>Microsoft 365（Exchange Online / Teams / SharePoint）是企業 SaaS 套件的代表、事故影響企業生產力、Microsoft 的 PIR 揭露格式具有教學價值。&lt;/p>
&lt;h2 id="規劃重點">規劃重點&lt;/h2>
&lt;ul>
&lt;li>企業 SaaS 套件的 blast radius：跨產品事故對企業客戶的影響&lt;/li>
&lt;li>跟 Azure AD 的依賴：Identity 失效 vs M365 服務失效的分層&lt;/li>
&lt;li>Tenant-level vs region-level 影響：多租戶 SaaS 的部分事故揭露&lt;/li>
&lt;li>PIR 格式：Microsoft 的 Public Incident Report 結構&lt;/li>
&lt;/ul>
&lt;h2 id="預計收錄事故">預計收錄事故&lt;/h2>
&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>2023&lt;/td>
 &lt;td>Exchange Online 大規模失效&lt;/td>
 &lt;td>跨企業客戶通訊影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2024&lt;/td>
 &lt;td>Teams 全球失效&lt;/td>
 &lt;td>同步通訊工具失效的 IR 通訊困境&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例定位">案例定位&lt;/h2>
&lt;p>Microsoft 365 這個案例在講的是一組共享 productivity 服務如何把單點事故變成廣域通訊問題。讀者先看懂 service health、PIR 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 的責任，再把 M365 視為企業客戶的協作底層。&lt;/p>
&lt;h2 id="判讀重點">判讀重點&lt;/h2>
&lt;p>當 Exchange Online 或 Teams 失效時，復原不只是在服務本身恢復，還要讓客戶知道通訊與協作功能何時能回來。這類事故的關鍵在於可見性與一致的對外更新，讓企業能決定是否切換替代流程。&lt;/p>
&lt;h2 id="可操作判準">可操作判準&lt;/h2>
&lt;ul>
&lt;li>能否快速判斷影響的是哪個 M365 子服務&lt;/li>
&lt;li>能否從 service health 看出恢復順序&lt;/li>
&lt;li>能否把 PIR 的資訊轉成客戶能執行的替代路徑&lt;/li>
&lt;li>能否把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與實際 outage 對齊&lt;/li>
&lt;/ul>
&lt;h2 id="與其他案例的關係">與其他案例的關係&lt;/h2>
&lt;p>Microsoft 365 和 Azure AD 是一組必讀對照，前者看協作服務層的影響，後者看 identity 基礎層的失效。它也能和 Slack 一起讀，因為兩者都在說明當通訊平台出事時，客戶需要的是清楚的狀態與替代流程，而不是只有技術術語。&lt;/p>
&lt;h2 id="代表樣本">代表樣本&lt;/h2>
&lt;ul>
&lt;li>Exchange Online 大規模失效代表企業通訊與協作服務的廣域影響。&lt;/li>
&lt;li>Teams 全球失效則顯示 IR 通訊本身也會受到通訊工具失效的影響。&lt;/li>
&lt;li>service health 與 PIR 的公開格式會影響客戶判讀速度。&lt;/li>
&lt;li>tenant-level 與 region-level 失效要分開看。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 讓 Microsoft 能把復原流程標準化。&lt;/li>
&lt;li>built-in service resiliency 是企業 SaaS 的預設期待。&lt;/li>
&lt;li>shared productivity suite 讓一個服務失效就能放大成企業生產力問題。&lt;/li>
&lt;li>customer communication 與技術復原並行，才能避免恢復過程的資訊落差。&lt;/li>
&lt;/ul>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/" data-link-title="Microsoft 365：套件級身分驗證事故" data-link-desc="企業套件在身份依賴失效時，如何同步處理跨產品影響與對外揭露。">M365-1&lt;/a>&lt;/td>
 &lt;td>套件級身份事故&lt;/td>
 &lt;td>將跨產品影響分層並同步對外通訊與回復順序&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/service-health-and-continuity?country=au&amp;amp;culture=en-au">Service health and continuity&lt;/a>：M365 服務健康、PIR 與通訊政策。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/microsoft-365/enterprise/view-service-health?view=o365-worldwide">How to check Microsoft 365 service health&lt;/a>：Service health 的使用方式。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/services-hub/unified/health/ir-m365">Microsoft 365 incident readiness - Unified&lt;/a>：Microsoft 的 incident readiness / PIR 流程。&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/compliance/assurance/assurance-m365-service-resiliency?source=recommendations">Built-in service resiliency in Microsoft 365&lt;/a>：M365 服務韌性與 downtime 定義。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Microsoft 365（Exchange Online / Teams / SharePoint）是企業 SaaS 套件的代表、事故影響企業生產力、Microsoft 的 PIR 揭露格式具有教學價值。</p>
<h2 id="規劃重點">規劃重點</h2>
<ul>
<li>企業 SaaS 套件的 blast radius：跨產品事故對企業客戶的影響</li>
<li>跟 Azure AD 的依賴：Identity 失效 vs M365 服務失效的分層</li>
<li>Tenant-level vs region-level 影響：多租戶 SaaS 的部分事故揭露</li>
<li>PIR 格式：Microsoft 的 Public Incident Report 結構</li>
</ul>
<h2 id="預計收錄事故">預計收錄事故</h2>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>事故</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2023</td>
          <td>Exchange Online 大規模失效</td>
          <td>跨企業客戶通訊影響</td>
      </tr>
      <tr>
          <td>2024</td>
          <td>Teams 全球失效</td>
          <td>同步通訊工具失效的 IR 通訊困境</td>
      </tr>
  </tbody>
</table>
<h2 id="案例定位">案例定位</h2>
<p>Microsoft 365 這個案例在講的是一組共享 productivity 服務如何把單點事故變成廣域通訊問題。讀者先看懂 service health、PIR 與 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 的責任，再把 M365 視為企業客戶的協作底層。</p>
<h2 id="判讀重點">判讀重點</h2>
<p>當 Exchange Online 或 Teams 失效時，復原不只是在服務本身恢復，還要讓客戶知道通訊與協作功能何時能回來。這類事故的關鍵在於可見性與一致的對外更新，讓企業能決定是否切換替代流程。</p>
<h2 id="可操作判準">可操作判準</h2>
<ul>
<li>能否快速判斷影響的是哪個 M365 子服務</li>
<li>能否從 service health 看出恢復順序</li>
<li>能否把 PIR 的資訊轉成客戶能執行的替代路徑</li>
<li>能否把 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與實際 outage 對齊</li>
</ul>
<h2 id="與其他案例的關係">與其他案例的關係</h2>
<p>Microsoft 365 和 Azure AD 是一組必讀對照，前者看協作服務層的影響，後者看 identity 基礎層的失效。它也能和 Slack 一起讀，因為兩者都在說明當通訊平台出事時，客戶需要的是清楚的狀態與替代流程，而不是只有技術術語。</p>
<h2 id="代表樣本">代表樣本</h2>
<ul>
<li>Exchange Online 大規模失效代表企業通訊與協作服務的廣域影響。</li>
<li>Teams 全球失效則顯示 IR 通訊本身也會受到通訊工具失效的影響。</li>
<li>service health 與 PIR 的公開格式會影響客戶判讀速度。</li>
<li>tenant-level 與 region-level 失效要分開看。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 讓 Microsoft 能把復原流程標準化。</li>
<li>built-in service resiliency 是企業 SaaS 的預設期待。</li>
<li>shared productivity suite 讓一個服務失效就能放大成企業生產力問題。</li>
<li>customer communication 與技術復原並行，才能避免恢復過程的資訊落差。</li>
</ul>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/" data-link-title="Microsoft 365：套件級身分驗證事故" data-link-desc="企業套件在身份依賴失效時，如何同步處理跨產品影響與對外揭露。">M365-1</a></td>
          <td>套件級身份事故</td>
          <td>將跨產品影響分層並同步對外通訊與回復順序</td>
      </tr>
  </tbody>
</table>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/service-health-and-continuity?country=au&amp;culture=en-au">Service health and continuity</a>：M365 服務健康、PIR 與通訊政策。</li>
<li><a href="https://learn.microsoft.com/microsoft-365/enterprise/view-service-health?view=o365-worldwide">How to check Microsoft 365 service health</a>：Service health 的使用方式。</li>
<li><a href="https://learn.microsoft.com/en-us/services-hub/unified/health/ir-m365">Microsoft 365 incident readiness - Unified</a>：Microsoft 的 incident readiness / PIR 流程。</li>
<li><a href="https://learn.microsoft.com/en-us/compliance/assurance/assurance-m365-service-resiliency?source=recommendations">Built-in service resiliency in Microsoft 365</a>：M365 服務韌性與 downtime 定義。</li>
</ul>
]]></content:encoded></item><item><title>4.24 Client-to-Server 端到端觀測串接</title><link>https://tarrragon.github.io/blog/backend/04-observability/client-server-trace-integration/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/client-server-trace-integration/</guid><description>&lt;p>Client-to-server 端到端觀測串接的核心責任是讓一次使用者操作的完整路徑 — 從 browser click 到 server 處理到 response rendering — 可以用同一個 trace ID 串起來。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM&lt;/a> 講的是概念和 vendor 定位；本篇走完一個具體場景的實作鏈路。&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 模組 03 SDK 設計&lt;/a> 講的是 client 端怎麼埋點；本篇講 server 端怎麼接收和整合。&lt;/p>
&lt;h2 id="完整鏈路">完整鏈路&lt;/h2>
&lt;p>以使用者在 web app 點擊「結帳」為例，一次操作產生的觀測鏈路：&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">Browser: user clicks &amp;#34;checkout&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> → RUM SDK 建立 client span（type: resource / xhr）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> → HTTP POST /api/checkout + W3C traceparent header
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> → Server middleware 提取 trace context
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> → Server 建立 child span（checkout-handler）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> → DB query span（order insert）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> → Cache span（inventory check）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> → Queue span（event publish）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> → Server 回 200 + response body
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> → Browser 收到 response → resource timing 結束
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> → RUM SDK 關閉 client span（記錄 duration + status）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> → 統一 trace waterfall：client span 是 root、server spans 是 children&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>鏈路的每一段都需要 trace context 正確傳遞。任何一段斷掉，trace waterfall 就會出現孤立的 span — server 端看到的 trace 跟 client 端看到的 trace 是兩條不相關的紀錄。&lt;/p>
&lt;h2 id="trace-context-propagation">Trace context propagation&lt;/h2>
&lt;h3 id="w3c-traceparent-header">W3C traceparent header&lt;/h3>
&lt;p>W3C Trace Context 是跨 vendor 的標準 propagation 格式。Header 長這樣：&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">traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> │ trace-id (32 hex) parent-id (16 hex) flags
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> version&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>RUM SDK 在發起 XHR / fetch 時把 &lt;code>traceparent&lt;/code> 注入 request header。Server 的 trace SDK 從 header 提取 trace-id 和 parent-id，建立 child span。&lt;/p></description><content:encoded><![CDATA[<p>Client-to-server 端到端觀測串接的核心責任是讓一次使用者操作的完整路徑 — 從 browser click 到 server 處理到 response rendering — 可以用同一個 trace ID 串起來。<a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a> 講的是概念和 vendor 定位；本篇走完一個具體場景的實作鏈路。<a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 模組 03 SDK 設計</a> 講的是 client 端怎麼埋點；本篇講 server 端怎麼接收和整合。</p>
<h2 id="完整鏈路">完整鏈路</h2>
<p>以使用者在 web app 點擊「結帳」為例，一次操作產生的觀測鏈路：</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">Browser: user clicks &#34;checkout&#34;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  → RUM SDK 建立 client span（type: resource / xhr）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  → HTTP POST /api/checkout + W3C traceparent header
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    → Server middleware 提取 trace context
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    → Server 建立 child span（checkout-handler）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      → DB query span（order insert）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      → Cache span（inventory check）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      → Queue span（event publish）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    → Server 回 200 + response body
</span></span><span class="line"><span class="ln">10</span><span class="cl">  → Browser 收到 response → resource timing 結束
</span></span><span class="line"><span class="ln">11</span><span class="cl">  → RUM SDK 關閉 client span（記錄 duration + status）
</span></span><span class="line"><span class="ln">12</span><span class="cl">  → 統一 trace waterfall：client span 是 root、server spans 是 children</span></span></code></pre></div><p>鏈路的每一段都需要 trace context 正確傳遞。任何一段斷掉，trace waterfall 就會出現孤立的 span — server 端看到的 trace 跟 client 端看到的 trace 是兩條不相關的紀錄。</p>
<h2 id="trace-context-propagation">Trace context propagation</h2>
<h3 id="w3c-traceparent-header">W3C traceparent header</h3>
<p>W3C Trace Context 是跨 vendor 的標準 propagation 格式。Header 長這樣：</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">traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
</span></span><span class="line"><span class="ln">2</span><span class="cl">              │  │                                │                  │
</span></span><span class="line"><span class="ln">3</span><span class="cl">              │  trace-id (32 hex)                 parent-id (16 hex) flags
</span></span><span class="line"><span class="ln">4</span><span class="cl">              version</span></span></code></pre></div><p>RUM SDK 在發起 XHR / fetch 時把 <code>traceparent</code> 注入 request header。Server 的 trace SDK 從 header 提取 trace-id 和 parent-id，建立 child span。</p>
<h3 id="client-端注入">Client 端注入</h3>
<p>各 RUM SDK 的注入方式：</p>
<table>
  <thead>
      <tr>
          <th>SDK</th>
          <th>注入機制</th>
          <th>配置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM</td>
          <td>自動 patch XHR / fetch，注入 <code>x-datadog-*</code> + 可選 <code>traceparent</code></td>
          <td><code>allowedTracingUrls</code> 設定允許注入的 domain</td>
      </tr>
      <tr>
          <td>Sentry browser</td>
          <td>自動 patch fetch / XHR，注入 <code>sentry-trace</code> + <code>baggage</code> + 可選 <code>traceparent</code></td>
          <td><code>tracePropagationTargets</code> 設定目標 URL</td>
      </tr>
      <tr>
          <td>OTel browser SDK</td>
          <td>透過 <code>XMLHttpRequestInstrumentation</code> / <code>FetchInstrumentation</code> 注入 <code>traceparent</code></td>
          <td><code>propagateTraceHeaderCorsUrls</code> 設定 CORS 允許的 URL</td>
      </tr>
  </tbody>
</table>
<p>三者的共同模式：只對設定的 domain 注入 trace header。不設定白名單時，header 不會被注入到第三方 API（避免 information leakage）。</p>
<h3 id="server-端提取">Server 端提取</h3>
<p>Server 端的 trace SDK（OTel auto-instrumentation 或 vendor agent）從 incoming request 的 header 提取 trace context：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># OTel Python 範例 — auto-instrumentation 自動處理</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 不需要手動提取，middleware 自動讀 traceparent header</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 建立的 span 會繼承 client 傳來的 trace-id 和 parent-id</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="c1"># 手動提取（不用 auto-instrumentation 時）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.propagate</span> <span class="kn">import</span> <span class="n">extract</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">ctx</span> <span class="o">=</span> <span class="n">extract</span><span class="p">(</span><span class="n">carrier</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;checkout-handler&#34;</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">ctx</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1"># server logic</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">pass</span></span></span></code></pre></div><h3 id="cors-限制">CORS 限制</h3>
<p>跨域請求時，browser 的 CORS preflight 會阻止非標準 header。Server 需要明確允許 trace header：</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">Access-Control-Allow-Headers: traceparent, tracestate, sentry-trace, baggage</span></span></code></pre></div><p>CORS 是 client-server trace 串接最常見的斷裂原因。Server 沒有回 <code>Access-Control-Allow-Headers: traceparent</code> 時，browser 會 strip 掉 trace header，server 端收到的 request 沒有 trace context，建立的 span 成為新的 root — 跟 client span 斷裂。</p>
<h2 id="跨層-correlation-設計">跨層 correlation 設計</h2>
<h3 id="trace-id-串接">Trace ID 串接</h3>
<p>統一 trace-id 是最基本的 correlation。同一個 trace-id 下的所有 span（client + server）可以在 trace backend 的 waterfall view 裡按時間排列，看到完整的 request 路徑。</p>
<h3 id="session-跟-transaction-的-mapping">Session 跟 transaction 的 mapping</h3>
<p>RUM SDK 的 session（使用者的一次造訪）包含多個 user action，每個 action 可能觸發多個 HTTP request。Mapping 關係：</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">RUM session
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └── user action (click &#34;checkout&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">        ├── HTTP request /api/checkout  →  server transaction (trace)
</span></span><span class="line"><span class="ln">4</span><span class="cl">        ├── HTTP request /api/inventory →  server transaction (trace)
</span></span><span class="line"><span class="ln">5</span><span class="cl">        └── client-side rendering time</span></span></code></pre></div><p>Datadog RUM 和 Sentry 都支援從 session replay 點進去看對應的 server trace。這個 mapping 靠的是 RUM event 裡記錄的 trace-id，跟 server trace backend 裡的同一個 trace-id 做 join。</p>
<h3 id="breadcrumbs-跟-server-log-的時間對齊">Breadcrumbs 跟 server log 的時間對齊</h3>
<p>RUM SDK 收集的 breadcrumbs（使用者操作序列：page view → button click → form submit）跟 server-side log 的 timestamp 需要可比對。時間對齊的前提是 client 和 server 的 clock 差距在可接受範圍（通常 &lt; 1s）。</p>
<p>NTP 同步的 server 端 clock 通常精準。Client 端（browser）依賴使用者裝置的系統時間，可能偏差數秒到數分鐘。RUM SDK 通常會記錄 relative timing（相對於 session 開始的 offset），而非絕對 timestamp，來降低 clock skew 的影響。</p>
<h3 id="error-correlation">Error correlation</h3>
<p>Client-side JS error 跟 server-side 5xx 可能是同一個問題的兩面。Correlation 方式：</p>
<ul>
<li><strong>同一 trace-id</strong>：client error 發生在某個 HTTP request 的 response 處理中，該 request 的 trace-id 跟 server-side 500 的 trace-id 相同 — 直接 correlation</li>
<li><strong>時間窗 + endpoint</strong>：client error 沒有 trace-id（例如 CORS block 導致 request 沒發出），用時間窗 + endpoint 模式做 fuzzy correlation</li>
<li><strong>Server 無異常但 client 報錯</strong>：client-side rendering error（JSON parse failure、type error），server 端看不到 — 需要 RUM 獨立分析</li>
</ul>
<h2 id="evidence-package-整合">Evidence package 整合</h2>
<p>把 client-side 訊號納入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 時，需要額外記錄：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Client-side 補充</th>
          <th>為什麼需要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>標註 &ldquo;RUM&rdquo; 或 &ldquo;Synthetic&rdquo;</td>
          <td>區分 server-side metrics 和 client-side metrics</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>Client perceived latency（含 DNS + network + server + rendering）</td>
          <td>跟 server-side latency 差異是 network + rendering 時間</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>Trace sampling 不一致</td>
          <td>Client 和 server 可能各自取樣，同一個 request 不一定兩邊都有</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>Client clock skew 可能影響 timestamp precision</td>
          <td>標注 client timestamp 的精確度限制</td>
      </tr>
  </tbody>
</table>
<p>Client perceived latency 跟 server-side latency 的差異本身就是一個觀測訊號。差異穩定在 50ms 是正常的 network overhead；差異突然從 50ms 跳到 500ms 代表網路或 CDN 出了問題 — 而這個問題 server-side dashboard 完全看不到。</p>
<h2 id="失敗場景判讀">失敗場景判讀</h2>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client span 存在但 server span 缺失</td>
          <td>Trace context header 沒被 propagate — 最常見原因是 CORS block</td>
          <td>檢查 <code>Access-Control-Allow-Headers</code> 是否包含 <code>traceparent</code>；檢查 RUM SDK 的 <code>allowedTracingUrls</code> 設定</td>
      </tr>
      <tr>
          <td>Server 正常但 client perceived latency 高</td>
          <td>網路延遲或 client rendering 慢</td>
          <td>看 RUM 的 resource timing breakdown（DNS / TCP / TLS / TTFB / download / render）</td>
      </tr>
      <tr>
          <td>Client error 但 server 無對應 request</td>
          <td>Request 沒發出 — client-side validation 擋掉或 network offline</td>
          <td>看 RUM breadcrumbs 確認 request 是否有送出；檢查 navigator.onLine 狀態</td>
      </tr>
      <tr>
          <td>Trace sampling 不一致</td>
          <td>Client 取樣到但 server 沒取樣到同一個 request</td>
          <td>統一 sampling decision — 用 head-based sampling（decision 在 trace 起點做、propagate 到下游）</td>
      </tr>
      <tr>
          <td>Client 和 server 的 error count 對不上</td>
          <td>Client 包含 JS rendering error（server 看不到）；server 包含非 user-facing 的背景 job error</td>
          <td>分開看：API error 用 trace correlation 比對、non-API error 各自歸類</td>
      </tr>
  </tbody>
</table>
<h2 id="vendor-整合模式">Vendor 整合模式</h2>
<table>
  <thead>
      <tr>
          <th>組合</th>
          <th>串接方式</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM + Datadog APM</td>
          <td>原生 — 同一個 Datadog org 裡 client 跟 server trace 自動關聯</td>
          <td>兩邊都要 Datadog plan</td>
      </tr>
      <tr>
          <td>Sentry browser + Sentry server</td>
          <td>原生 — <code>sentry-trace</code> header propagation</td>
          <td>Performance monitoring 需要 Sentry paid plan</td>
      </tr>
      <tr>
          <td>OTel browser SDK + OTel server SDK</td>
          <td>W3C <code>traceparent</code> — vendor-neutral 標準</td>
          <td>Browser SDK 較新、instrumentation 覆蓋度不如 server 端成熟</td>
      </tr>
      <tr>
          <td>混合（Sentry browser + Datadog server）</td>
          <td>手動橋接 — 確保雙方都支援 W3C <code>traceparent</code></td>
          <td>Trace context format 要一致；session-level correlation 需自建</td>
      </tr>
  </tbody>
</table>
<p>同 vendor 組合的串接最自然。跨 vendor 組合只要雙方都支援 W3C Trace Context，trace-level correlation 可以通；但 session-level 的功能（session replay → server trace）需要同 vendor 才有。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a>：概念定位和 vendor 選型</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>：server-side trace context 設計</li>
<li><a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package</a>：evidence 整合到 release gate</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：evidence 欄位標準</li>
<li><a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 03 SDK 設計</a>：client-side SDK 埋點設計</li>
<li><a href="/blog/monitoring/06-commercial-comparison/" data-link-title="模組六：商業方案對照" data-link-desc="Sentry / Crashlytics / Datadog RUM / Mixpanel — 自架 vs 商業的功能和成本取捨">Monitoring 06 商業方案</a>：Sentry / Datadog RUM 的 client-side 能力比較</li>
<li><a href="/blog/monitoring/telemetry-data-dual-use/" data-link-title="監控資料的雙重用途：行為分析與訊號治理" data-link-desc="同一份 event data 如何同時服務行為分析（funnel / cohort / attribution）和訊號治理（cardinality / cost / signal governance）— 格式交叉、治理衝突與分流架構">監控資料的雙重用途</a>：同一份 event data 如何同時服務行為分析與訊號治理</li>
</ul>
]]></content:encoded></item><item><title>Retry Policy</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/</guid><description>&lt;p>Retry policy 的核心概念是「定義失敗後何時再試、試幾次、用什麼間隔、何時停止」。重試可以吸收暫時性故障（網路抖動、下游短暫不可用），但也可能放大下游壓力或重複造成副作用，因此跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a> 是成對設計。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Retry policy 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a> 構成錯誤處理的兩層機制 — retry 處理暫時性失敗，DLQ 承接 retry 耗盡後仍無法處理的訊息。Retry 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 是成對的設計 — 有 retry 就要有 idempotent consumer，否則重試可能造成重複扣款、重複發通知。&lt;/p>
&lt;p>Retry 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">retry storm&lt;/a> 的關係是：大量 consumer 同時 retry 失敗的訊息會形成下游的流量尖峰，把暫時性故障放大成全系統問題。Exponential backoff + jitter 是緩解 retry storm 的標準做法。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 retry policy 的訊號是下游偶發失敗影響成功率。付款查詢 API 短暫 timeout 可以重試；已送出的扣款請求則需要先查詢結果或用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> key，避免重試造成重複扣款。&lt;/p>
&lt;p>Retry 的判斷分類：暫時性錯誤（5xx、timeout、connection refused）適合 retry；永久性錯誤（4xx、schema validation failure、business rule violation）不應該 retry，直接送 DLQ 或 reject。分類錯誤是 retry policy 最常見的 bug — 對永久性錯誤 retry 只會消耗 quota、延遲問題被發現的時間。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Retry policy 要包含最大重試次數、backoff 策略（fixed / exponential / exponential + jitter）、每次 retry 的 timeout、錯誤分類規則（哪些 error code 算暫時性）、觀測欄位（retry count、最終結果）與停止條件（超過 N 次進 DLQ）。高流量系統還要設定 retry budget — 限制 retry 流量佔總流量的比例，避免 retry 自身成為負載來源。&lt;/p></description><content:encoded><![CDATA[<p>Retry policy 的核心概念是「定義失敗後何時再試、試幾次、用什麼間隔、何時停止」。重試可以吸收暫時性故障（網路抖動、下游短暫不可用），但也可能放大下游壓力或重複造成副作用，因此跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a> 是成對設計。</p>
<h2 id="概念位置">概念位置</h2>
<p>Retry policy 跟 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a> 構成錯誤處理的兩層機制 — retry 處理暫時性失敗，DLQ 承接 retry 耗盡後仍無法處理的訊息。Retry 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 是成對的設計 — 有 retry 就要有 idempotent consumer，否則重試可能造成重複扣款、重複發通知。</p>
<p>Retry 跟 <a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">retry storm</a> 的關係是：大量 consumer 同時 retry 失敗的訊息會形成下游的流量尖峰，把暫時性故障放大成全系統問題。Exponential backoff + jitter 是緩解 retry storm 的標準做法。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 retry policy 的訊號是下游偶發失敗影響成功率。付款查詢 API 短暫 timeout 可以重試；已送出的扣款請求則需要先查詢結果或用 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> key，避免重試造成重複扣款。</p>
<p>Retry 的判斷分類：暫時性錯誤（5xx、timeout、connection refused）適合 retry；永久性錯誤（4xx、schema validation failure、business rule violation）不應該 retry，直接送 DLQ 或 reject。分類錯誤是 retry policy 最常見的 bug — 對永久性錯誤 retry 只會消耗 quota、延遲問題被發現的時間。</p>
<h2 id="設計責任">設計責任</h2>
<p>Retry policy 要包含最大重試次數、backoff 策略（fixed / exponential / exponential + jitter）、每次 retry 的 timeout、錯誤分類規則（哪些 error code 算暫時性）、觀測欄位（retry count、最終結果）與停止條件（超過 N 次進 DLQ）。高流量系統還要設定 retry budget — 限制 retry 流量佔總流量的比例，避免 retry 自身成為負載來源。</p>
]]></content:encoded></item><item><title>MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/lock-contention/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/lock-contention/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>lock contention&lt;/em> — 5 種 lock type + isolation level 互動 + production debug。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="開場案例">開場案例&lt;/h2>
&lt;p>Application 跑了 6 個月、staging 100% 重現過的 deadlock 從來沒在 production 出現。某天 traffic 上升 30%、production 開始爆 &lt;code>ER_LOCK_DEADLOCK&lt;/code>、application retry 不夠快、order 大量失敗。&lt;/p>
&lt;p>&lt;code>SHOW ENGINE INNODB STATUS\G&lt;/code> 拉出 deadlock：&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">*** (1) TRANSACTION:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">TRANSACTION 12345, ACTIVE 1 sec starting index read
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">mysql tables in use 1, locked 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">MySQL thread id 100, query id 5000 update orders
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">UPDATE orders SET status = &amp;#39;shipped&amp;#39; WHERE id = 500
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">trx id 12345 lock_mode X locks rec but not gap waiting
&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">*** (2) TRANSACTION:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">TRANSACTION 12346, ACTIVE 1 sec starting index read
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">mysql tables in use 1, locked 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">4 lock struct(s), heap size 1136, 4 row lock(s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">MySQL thread id 101, query id 5001 update payments
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">UPDATE payments SET captured = 1 WHERE order_id = 500
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">*** (2) HOLDS THE LOCK(S):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">trx id 12346 lock_mode X locks rec but not gap
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">RECORD LOCKS space id 51 page no 10 n bits 80 index idx_order_id of table `production`.`payments`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">trx id 12346 lock_mode X waiting
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">*** WE ROLL BACK TRANSACTION (1)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個 transaction 各自拿了一邊 lock、互相等對方的、deadlock。為什麼 staging 重現過、production 6 個月才爆？因為 &lt;strong>lock contention 是 &lt;em>可能性&lt;/em> 不是 &lt;em>確定性&lt;/em>&lt;/strong> — staging 重現等於確認「程式邏輯有 deadlock risk」、production 6 個月平安等於「concurrency 還沒撞到」。Traffic 上升把 &lt;em>機率乘以 N&lt;/em>、原本每天 0 次變每分鐘 5 次。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>lock contention</em> — 5 種 lock type + isolation level 互動 + production debug。</p></blockquote>
<hr>
<h2 id="開場案例">開場案例</h2>
<p>Application 跑了 6 個月、staging 100% 重現過的 deadlock 從來沒在 production 出現。某天 traffic 上升 30%、production 開始爆 <code>ER_LOCK_DEADLOCK</code>、application retry 不夠快、order 大量失敗。</p>
<p><code>SHOW ENGINE INNODB STATUS\G</code> 拉出 deadlock：</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) TRANSACTION:
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">TRANSACTION 12345, ACTIVE 1 sec starting index read
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">mysql tables in use 1, locked 1
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">MySQL thread id 100, query id 5000 update orders
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">UPDATE orders SET status = &#39;shipped&#39; WHERE id = 500
</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">*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
</span></span><span class="line"><span class="ln">10</span><span class="cl">trx id 12345 lock_mode X locks rec but not gap waiting
</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">*** (2) TRANSACTION:
</span></span><span class="line"><span class="ln">13</span><span class="cl">TRANSACTION 12346, ACTIVE 1 sec starting index read
</span></span><span class="line"><span class="ln">14</span><span class="cl">mysql tables in use 1, locked 1
</span></span><span class="line"><span class="ln">15</span><span class="cl">4 lock struct(s), heap size 1136, 4 row lock(s)
</span></span><span class="line"><span class="ln">16</span><span class="cl">MySQL thread id 101, query id 5001 update payments
</span></span><span class="line"><span class="ln">17</span><span class="cl">UPDATE payments SET captured = 1 WHERE order_id = 500
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">*** (2) HOLDS THE LOCK(S):
</span></span><span class="line"><span class="ln">20</span><span class="cl">RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
</span></span><span class="line"><span class="ln">21</span><span class="cl">trx id 12346 lock_mode X locks rec but not gap
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
</span></span><span class="line"><span class="ln">24</span><span class="cl">RECORD LOCKS space id 51 page no 10 n bits 80 index idx_order_id of table `production`.`payments`
</span></span><span class="line"><span class="ln">25</span><span class="cl">trx id 12346 lock_mode X waiting
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl">*** WE ROLL BACK TRANSACTION (1)</span></span></code></pre></div><p>兩個 transaction 各自拿了一邊 lock、互相等對方的、deadlock。為什麼 staging 重現過、production 6 個月才爆？因為 <strong>lock contention 是 <em>可能性</em> 不是 <em>確定性</em></strong> — staging 重現等於確認「程式邏輯有 deadlock risk」、production 6 個月平安等於「concurrency 還沒撞到」。Traffic 上升把 <em>機率乘以 N</em>、原本每天 0 次變每分鐘 5 次。</p>
<p>這個 case 揭露 MySQL lock 教學的核心：理解 lock 不只是 <em>debug 跑 deadlock 報錯</em> 的能力、是 <em>讀 query 預測 lock pattern</em> 的能力。</p>
<h2 id="innodb-5-種-lock-類型">InnoDB 5 種 Lock 類型</h2>
<p>InnoDB 不是 <em>簡單 row lock</em>、有 5 個獨立 lock concept：</p>
<h3 id="1-record-lock--鎖-row">1. Record Lock — 鎖 row</h3>
<p><code>SELECT ... FOR UPDATE</code> / UPDATE / DELETE 對 <em>被 match 的 row</em> 加 record lock。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Transaction 1
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 對 id=100 的 row 加 record lock</span></span></span></code></pre></div><p>Transaction 2 試 <code>UPDATE orders WHERE id = 100</code> 必須等。</p>
<h3 id="2-gap-lock--鎖-row-之間的空隙">2. Gap Lock — 鎖 row 之間的「空隙」</h3>
<p>InnoDB 在 <em>REPEATABLE READ</em> (預設) 下、<code>SELECT ... FOR UPDATE WHERE col &gt; 100</code> 不只 lock 符合的 row、<em>也 lock 該 range 內的「空隙」</em>、防其他 transaction INSERT 進這個 range。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 已存在 orders: id=100, 200, 300
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">300</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Lock id=200 + gap lock (100, 200) + gap lock (200, 300)</span></span></span></code></pre></div><p>Transaction 2 試 <code>INSERT INTO orders (id) VALUES (150)</code> 必須等 — 即使 id=150 不存在、gap lock 阻擋 INSERT。</p>
<p><strong>Gap lock 是 deadlock 最常見來源</strong> — application logic 看 row、但 lock 卻 cover row 之外的空隙、難預測。</p>
<h3 id="3-next-key-lock--record--gap-組合">3. Next-Key Lock — Record + Gap 組合</h3>
<p>預設 lock 行為。<code>SELECT ... FOR UPDATE WHERE col = 100</code> 對 id=100 的 record lock + id=100 之前的 gap lock。</p>
<p>Lock 的範圍實際是 <em>半開區間</em> (previous_id, current_id]：</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">Records: 100, 200, 300
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">WHERE id = 100 FOR UPDATE → next-key lock (-inf, 100]
</span></span><span class="line"><span class="ln">4</span><span class="cl">WHERE id = 200 FOR UPDATE → next-key lock (100, 200]
</span></span><span class="line"><span class="ln">5</span><span class="cl">WHERE id = 300 FOR UPDATE → next-key lock (200, 300]
</span></span><span class="line"><span class="ln">6</span><span class="cl">WHERE id BETWEEN 150 AND 250 FOR UPDATE → next-key lock (100, 200] + (200, 300]</span></span></code></pre></div><h3 id="4-insert-intention-lock--insert-之前的-gap-lock">4. Insert Intention Lock — INSERT 之前的 gap lock</h3>
<p><code>INSERT</code> 不直接 lock 整個 gap、而是 <em>insert intention lock</em> — 比 gap lock 弱、允許多個 INSERT 同 gap 並行（不同 id）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Transaction 1
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">150</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Transaction 2
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">175</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 同 gap (100, 200)、兩個 INSERT 並行、不阻塞</span></span></span></code></pre></div><p>但如果 Transaction 1 已 hold gap lock（through SELECT FOR UPDATE）、Transaction 2 INSERT 必須等。</p>
<h3 id="5-auto-inc-lock--auto-increment-column-專用">5. Auto-Inc Lock — Auto-Increment column 專用</h3>
<p><code>INSERT INTO orders (id) VALUES (DEFAULT)</code> 取得 auto-increment value 時 lock。Mode：</p>
<ul>
<li><code>innodb_autoinc_lock_mode=0</code>（traditional）：lock 整個 INSERT statement 期間、其他 INSERT 必須等</li>
<li><code>innodb_autoinc_lock_mode=1</code>（consecutive）：lock 短時間（取值期間）、INSERT 1 row 不會阻塞其他</li>
<li><code>innodb_autoinc_lock_mode=2</code>（interleaved、8.0+ 預設（5.7 預設仍是 1））：完全並行、auto-inc value 不保證連續但可並行</li>
</ul>
<p>8.0+ 預設 mode=2、性能高、但 <em>binlog format 必須 ROW</em>（STATEMENT 行為錯）。</p>
<h2 id="isolation-level-對-lock-的決定性影響">Isolation Level 對 Lock 的決定性影響</h2>
<p>InnoDB 4 個 isolation level、lock 行為完全不同：</p>
<table>
  <thead>
      <tr>
          <th>Isolation</th>
          <th>Read 行為</th>
          <th>Lock 範圍</th>
          <th>Default?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>READ UNCOMMITTED</td>
          <td>可讀 dirty data</td>
          <td>純 record lock、無 gap</td>
          <td>否</td>
      </tr>
      <tr>
          <td>READ COMMITTED</td>
          <td>每個 statement 看當下 committed</td>
          <td>純 record lock、無 gap</td>
          <td>否</td>
      </tr>
      <tr>
          <td>REPEATABLE READ</td>
          <td>Transaction 內 snapshot consistent</td>
          <td>Record + gap + next-key</td>
          <td><strong>是</strong></td>
      </tr>
      <tr>
          <td>SERIALIZABLE</td>
          <td>強制 SELECT 變 SELECT &hellip; FOR SHARE</td>
          <td>Record + gap + next-key 加重</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p><strong>REPEATABLE READ + Gap lock 是 deadlock 主要來源</strong>：</p>
<ul>
<li>預設 isolation level</li>
<li>為了 <em>保證 repeatable read</em>（同 transaction 內讀同樣資料）、強制 gap lock 防 phantom row</li>
<li>但 gap lock 經常 lock 比預期廣的範圍、deadlock 機率上升</li>
</ul>
<p><strong>改成 READ COMMITTED 的取捨</strong>：</p>
<ul>
<li>優點：無 gap lock、deadlock 大降、寫吞吐上升</li>
<li>缺點：transaction 內讀同 query 結果可能不同（non-repeatable read）</li>
<li>重要：<em>binlog format 必須 ROW</em>（STATEMENT 在 READ COMMITTED 下 replication 行為不一致）</li>
<li>多數 MySQL production 用 READ COMMITTED 跑 OLTP、REPEATABLE READ 留給特殊 case</li>
</ul>
<p><strong>對比 PostgreSQL</strong>：</p>
<ul>
<li>PG 預設 isolation 是 <em>READ COMMITTED</em>（不是 RR）</li>
<li>PG 的 RR 用 <em>snapshot isolation</em>（不靠 gap lock）、deadlock 少</li>
<li>這是 MySQL 跟 PG 在 <em>並行控制 model</em> 的根本差異 — MySQL 用 lock-based、PG 用 MVCC-heavy</li>
</ul>
<h2 id="用-show-engine-innodb-status-讀-lock-狀態">用 SHOW ENGINE INNODB STATUS 讀 lock 狀態</h2>
<p><code>SHOW ENGINE INNODB STATUS\G</code> 是 production debug lock contention 的主要工具：</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">------------
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">TRANSACTIONS
</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">Trx id counter 12350
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Purge done for trx&#39;s n:o &lt; 12340 undo n:o &lt; 0 state: running but idle
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">History list length 5
</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">---TRANSACTION 12345, ACTIVE 30 sec  -- 長 transaction、警訊
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">3 lock struct(s), heap size 1136, 5 row lock(s)
</span></span><span class="line"><span class="ln">10</span><span class="cl">MySQL thread id 100, OS thread handle ..., query id ...
</span></span><span class="line"><span class="ln">11</span><span class="cl">SELECT * FROM orders WHERE id &gt; 100 FOR UPDATE
</span></span><span class="line"><span class="ln">12</span><span class="cl">------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK:
</span></span><span class="line"><span class="ln">13</span><span class="cl">RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
</span></span><span class="line"><span class="ln">14</span><span class="cl">trx id 12345 lock_mode X locks gap before rec  -- gap lock</span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>ACTIVE N sec</code>：transaction 跑多久（長 transaction 嫌疑）</li>
<li><code>lock_mode X / S</code>：exclusive / shared lock</li>
<li><code>locks rec but not gap</code> / <code>locks gap before rec</code> / <code>locks rec</code>：是 record / gap / next-key</li>
<li><code>TRX HAS BEEN WAITING N SEC FOR THIS LOCK</code>：等多久、超過幾秒就是 lock contention</li>
</ul>
<p><code>SELECT * FROM information_schema.INNODB_TRX</code> / <code>INNODB_LOCKS</code> (5.7) / <code>performance_schema.data_locks</code> (8.0) 給 <em>structured</em> lock 視圖。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-gap-lock-阻塞-insert--lock-不存在的-row">1. Gap lock 阻塞 INSERT — 「Lock 不存在的 row」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Transaction 1
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 假設 user_id=100 沒任何 order、預期沒 lock 任何 row
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Transaction 2
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- 等！為什麼？</span></span></span></code></pre></div><p>問題：<code>WHERE user_id = 100</code> <em>沒有 record</em> 時、InnoDB 仍 lock <em>user_id=100 應該在的 gap</em>（防 phantom）、Transaction 2 INSERT 進這個 gap 被阻擋。</p>
<p>修法：</p>
<ul>
<li>改 READ COMMITTED isolation</li>
<li>或不用 <code>SELECT ... FOR UPDATE</code> on empty result、改 <em>application 層 check + INSERT</em> pattern</li>
<li>用 <code>INSERT ... ON DUPLICATE KEY UPDATE</code> 或 <code>INSERT IGNORE</code> 避免 SELECT FOR UPDATE</li>
</ul>
<h3 id="2-auto-inc-lock-contention--大量並行-insert">2. Auto-Inc Lock Contention — 大量並行 INSERT</h3>
<p><code>innodb_autoinc_lock_mode=0</code> 或 <code>=1</code> 模式下、大量並行 INSERT 撞 auto-inc lock、寫吞吐 cap。</p>
<p>修法：</p>
<ul>
<li>設 <code>innodb_autoinc_lock_mode=2</code>（interleaved、8.0+ 預設（5.7 預設仍是 1））</li>
<li>確認 <code>binlog_format=ROW</code>（mode=2 必須）</li>
<li>接受 auto-inc value 不連續（id 可能跳號）</li>
</ul>
<h3 id="3-fk-lock-cascading--父子-transaction-互鎖">3. FK Lock Cascading — 父子 transaction 互鎖</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- orders 表有 customer_id FK → customers.id
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">-- Transaction 1
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">  </span><span class="c1">-- lock customers row
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- Transaction 2
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- FK check 需要 lock customers row id=100、等 Transaction 1</span></span></span></code></pre></div><p>FK 強制 <em>每個 INSERT child 都要 shared lock parent</em>、parent 的任何 UPDATE 都會 lock 所有 child INSERT。</p>
<p>修法：</p>
<ul>
<li>評估 FK 是否真的需要（high-write 場景考慮 application-level enforcement）</li>
<li>短 transaction 縮短 lock 時間</li>
<li>FK 設計時讓 <em>parent UPDATE 少</em> / <em>child INSERT 多</em>（parent 是穩定資料）</li>
</ul>
<h3 id="4-large-transaction-lock-holding--1-個-transaction-拖全-cluster">4. Large Transaction Lock Holding — 1 個 transaction 拖全 cluster</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 100K row 的 batch UPDATE
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;archived&#39;</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2024-01-01&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 跑 5 分鐘、持 100K row 的 lock
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 其他 transaction 撞到任何被 lock 的 row 都等 5 分鐘
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">COMMIT</span><span class="p">;</span></span></span></code></pre></div><p>長 transaction 是 <em>lock contention 災難</em>。</p>
<p>修法：</p>
<ul>
<li>
<p>把 batch operation <em>拆 chunk</em>（每 chunk 1000 row、commit、繼續）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">DO</span><span class="w"> </span><span class="err">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">START</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;archived&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2024-01-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s1">&#39;archived&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="err">}</span><span class="w"> </span><span class="n">WHILE</span><span class="w"> </span><span class="n">rows_affected</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>用 <em>pt-archiver</em> tool（Percona）對 batch UPDATE / DELETE 自動 chunked</p>
</li>
<li>
<p>監控 <code>information_schema.innodb_trx</code> 找出 long-running transaction</p>
</li>
</ul>
<h3 id="5-read-committed--binlog-row-interaction">5. READ COMMITTED + Binlog ROW Interaction</h3>
<p>READ COMMITTED isolation 改善 deadlock、但對 <em>binlog format</em> 有要求：</p>
<ul>
<li><code>binlog_format=STATEMENT</code>：READ COMMITTED 下 transaction 看到不同 snapshot、replicate 後 replica 結果可能 <em>不同於 primary</em>（broken replication semantically）</li>
<li><code>binlog_format=ROW</code>：每個 row event 都 explicit、READ COMMITTED 跟 ROW 兼容、replica 結果一致</li>
<li><code>binlog_format=MIXED</code>：部分 case 仍可能 fall back STATEMENT、不推薦</li>
</ul>
<p>修法：</p>
<ul>
<li>用 READ COMMITTED 時、強制 <code>binlog_format=ROW</code></li>
<li>全 cluster server（primary + replica + Group Replication members）統一 binlog_format</li>
<li>Migration 5.7 STATEMENT → 8.0 ROW 時、isolation 跟 binlog format 一起 review</li>
</ul>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication">跟 Replication</h3>
<p><code>binlog_format=ROW</code> 跟 isolation level 互動已述。Replica apply ROW binlog 時、replica 上 <em>也 acquire 同樣 lock</em>、replica 上的 long query 跟 replication lag 互動。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-group-replication">跟 Group Replication</h3>
<p>GR certification phase 跟 row lock 衝突 — write conflict 檢測在 certification、不是 lock。但 <em>local row lock</em> 仍存在、影響 single-instance write throughput。詳見 <a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">Group Replication</a>。</p>
<h3 id="跟-online-schema-change">跟 Online Schema Change</h3>
<p>gh-ost / pt-osc 在 cut-over 階段需要 metadata lock、跟 long-running transaction 衝突。Lock contention deep dive 跟 OSC cut-over 議題密切。詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>Slow query 持 lock 久、放大 contention。<code>EXPLAIN ANALYZE</code> 看實際執行時間、跟 lock holding time 直接相關。詳見 <a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">Query Optimization</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p><code>innodb_lock_wait_timeout=50</code>（預設 50 秒）— lock wait 超時 transaction 自動 rollback、避免無限等。production 建議調短（10-20 秒）、快 fail 給 application retry。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h2 id="跟-postgresql-lock-model-對比">跟 PostgreSQL Lock model 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL InnoDB</th>
          <th>PostgreSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Concurrency model</td>
          <td>Lock-based（rec / gap / next-key）</td>
          <td>MVCC-heavy（few explicit lock）</td>
      </tr>
      <tr>
          <td>預設 isolation</td>
          <td>REPEATABLE READ</td>
          <td>READ COMMITTED</td>
      </tr>
      <tr>
          <td>Gap lock</td>
          <td>有</td>
          <td>無對應（PG 用 predicate lock for SERIALIZABLE）</td>
      </tr>
      <tr>
          <td>Deadlock 機率</td>
          <td>中-高</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Auto-inc</td>
          <td>內建 + auto-inc lock</td>
          <td>SEQUENCE（無對應 lock 議題）</td>
      </tr>
      <tr>
          <td>Snapshot isolation</td>
          <td>部分（RR 內）</td>
          <td>完整（MVCC 跑全 stack）</td>
      </tr>
  </tbody>
</table>
<p>PG 用 MVCC 跑大部分並行 control、少數 case 才用 explicit lock、整體 deadlock 機率低。MySQL 用 lock-based + MVCC mixed、production 必須懂 lock pattern。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<p>Production 持續 monitor：</p>
<ul>
<li><code>Innodb_row_lock_waits</code> / <code>_time</code> → lock wait 累計</li>
<li><code>Innodb_deadlocks</code> → deadlock 次數（5.7+ 有、之前要 parse SHOW ENGINE）</li>
<li><code>performance_schema.data_lock_waits</code> → 即時 lock wait 視圖（8.0+）</li>
<li><code>information_schema.innodb_trx</code> → long-running transaction</li>
<li><code>slow_query_log</code> → 看 query 是否花太多 time 在 lock wait</li>
</ul>
<p>對 deadlock：把 <code>innodb_print_all_deadlocks=ON</code>、所有 deadlock 寫 error log、不用 <code>SHOW ENGINE</code> 才看到。</p>
<h2 id="何時改-isolation-level">何時改 isolation level</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 isolation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>典型 web OLTP、低-中寫吞吐</td>
          <td>REPEATABLE READ（預設）</td>
      </tr>
      <tr>
          <td>高寫吞吐、deadlock 頻繁</td>
          <td>READ COMMITTED</td>
      </tr>
      <tr>
          <td>金融 transaction、需要 strict isolation</td>
          <td>REPEATABLE READ + 仔細 review</td>
      </tr>
      <tr>
          <td>嚴格 serializable（小 case）</td>
          <td>SERIALIZABLE（performance penalty）</td>
      </tr>
      <tr>
          <td>跨 region replication + 強一致</td>
          <td>用 Group Replication / Spanner 而不是 isolation level</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（binlog format + isolation 互動）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/query-optimization/" data-link-title="MySQL Query Optimization：從 EXPLAIN 看到實際執行、5 條 query 從 5 秒變 50ms 的 anatomy" data-link-desc="MySQL query 慢的根因不在「SQL 寫法」、在「optimizer 選錯 plan」。本文從 5 個常見 production case 開場（5 秒 → 50ms / 30 秒 → 200ms / 8 秒 → 30ms 等）、走 EXPLAIN / EXPLAIN ANALYZE / optimizer trace 三層分析工具、index hint vs optimizer hint 取捨、cardinality estimation 失效時的修法、5 production 踩雷（statistics 過時 / forced index 用錯 / hash join 沒觸發 / range scan 退化 ALL / derived table materialization）">MySQL Query Optimization</a>（slow query → lock contention）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（lock_wait_timeout）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">MySQL Group Replication</a>（cert vs lock）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change</a>（metadata lock）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a>（PG sibling、為什麼 PG 比 MySQL 少 deadlock — pure MVCC vs MVCC + gap lock）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor page</a>（MVCC vs lock model）</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level 卡片</a></li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html">InnoDB Locking</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL MVCC + Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>MVCC + lock model&lt;/em> — PG 並行控制機制跟跟 MySQL lock-based 不同。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-mvcc每次更新都-新增-tuple不改舊版">PG MVCC：每次更新都 &lt;em>新增 tuple&lt;/em>、不改舊版&lt;/h2>
&lt;p>PG 的並行控制核心是 &lt;em>Multi-Version Concurrency Control&lt;/em> — UPDATE 不修改原 row、是 &lt;em>新增&lt;/em> 一個 tuple version、舊 version 留在 table 直到 VACUUM 清理：&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">原 row: (id=1, status=&amp;#39;pending&amp;#39;, xmin=100, xmax=NULL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ UPDATE status=&amp;#39;shipped&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">新 tuple: (id=1, status=&amp;#39;shipped&amp;#39;, xmin=200, xmax=NULL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">舊 tuple 標 xmax=200（不刪、給其他 transaction 看舊 version）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>xmin&lt;/code> / &lt;code>xmax&lt;/code> 是 &lt;em>creator transaction id&lt;/em> / &lt;em>destroyer transaction id&lt;/em>。每個 SELECT 用 &lt;em>snapshot&lt;/em>（含當下 active transaction list）判斷哪些 tuple 對自己可見：&lt;/p>
&lt;ul>
&lt;li>自己 transaction id &amp;gt; tuple.xmin 且 (tuple.xmax = NULL 或自己 transaction id &amp;lt; tuple.xmax) → 可見&lt;/li>
&lt;li>否則 → 看不到（過去 / 未來版本）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>結果&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;em>Readers 不 lock writers&lt;/em>：SELECT 看 snapshot、不 block UPDATE&lt;/li>
&lt;li>&lt;em>Writers 不 lock readers&lt;/em>：UPDATE 寫新 tuple、不影響正在跑的 SELECT snapshot&lt;/li>
&lt;li>&lt;em>Writers 只 lock 同一 row 的 writers&lt;/em>：兩個 UPDATE 同 row 才 conflict&lt;/li>
&lt;/ul>
&lt;p>跟 MySQL InnoDB &lt;em>lock-based&lt;/em>（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">Lock Contention&lt;/a>）對比：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>MVCC + lock model</em> — PG 並行控制機制跟跟 MySQL lock-based 不同。</p></blockquote>
<hr>
<h2 id="pg-mvcc每次更新都-新增-tuple不改舊版">PG MVCC：每次更新都 <em>新增 tuple</em>、不改舊版</h2>
<p>PG 的並行控制核心是 <em>Multi-Version Concurrency Control</em> — UPDATE 不修改原 row、是 <em>新增</em> 一個 tuple version、舊 version 留在 table 直到 VACUUM 清理：</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">原 row:    (id=1, status=&#39;pending&#39;, xmin=100, xmax=NULL)
</span></span><span class="line"><span class="ln">2</span><span class="cl">                 ↓ UPDATE status=&#39;shipped&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">新 tuple:  (id=1, status=&#39;shipped&#39;, xmin=200, xmax=NULL)
</span></span><span class="line"><span class="ln">4</span><span class="cl">舊 tuple 標 xmax=200（不刪、給其他 transaction 看舊 version）</span></span></code></pre></div><p><code>xmin</code> / <code>xmax</code> 是 <em>creator transaction id</em> / <em>destroyer transaction id</em>。每個 SELECT 用 <em>snapshot</em>（含當下 active transaction list）判斷哪些 tuple 對自己可見：</p>
<ul>
<li>自己 transaction id &gt; tuple.xmin 且 (tuple.xmax = NULL 或自己 transaction id &lt; tuple.xmax) → 可見</li>
<li>否則 → 看不到（過去 / 未來版本）</li>
</ul>
<p><strong>結果</strong>：</p>
<ul>
<li><em>Readers 不 lock writers</em>：SELECT 看 snapshot、不 block UPDATE</li>
<li><em>Writers 不 lock readers</em>：UPDATE 寫新 tuple、不影響正在跑的 SELECT snapshot</li>
<li><em>Writers 只 lock 同一 row 的 writers</em>：兩個 UPDATE 同 row 才 conflict</li>
</ul>
<p>跟 MySQL InnoDB <em>lock-based</em>（<a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">Lock Contention</a>）對比：</p>
<ul>
<li>MySQL：SELECT FOR UPDATE 用 gap lock 防 phantom、deadlock 機率高</li>
<li>PG：MVCC + snapshot 自然防 phantom（read 看 snapshot）、deadlock 少</li>
</ul>
<p>但 PG 代價是 <em>VACUUM 治理</em> — dead tuple 不清理會佔 disk + 影響 query 效率。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h2 id="pg-4-種-lock">PG 4 種 lock</h2>
<p>PG 仍有 lock、但場景跟 MySQL 不同：</p>
<h3 id="1-row-level-lock--主要由-update--delete--select-for-update-取">1. Row-level lock — 主要由 UPDATE / DELETE / SELECT FOR UPDATE 取</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 對 id=100 row 加 ROW EXCLUSIVE lock
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 其他 transaction 試 UPDATE / DELETE id=100 必須等</span></span></span></code></pre></div><p>Row-level lock <em>不 block reader</em>（SELECT 看 snapshot、不檢查 lock）。</p>
<h3 id="2-table-level-lock--ddl-跟少數-select-for-場景">2. Table-level lock — DDL 跟少數 SELECT FOR 場景</h3>
<p>PG 有 8 種 table lock mode、嚴重程度遞增：</p>
<table>
  <thead>
      <tr>
          <th>Mode</th>
          <th>行為</th>
          <th>衝突</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ACCESS SHARE</td>
          <td>SELECT 跑</td>
          <td>跟 ACCESS EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>ROW SHARE</td>
          <td>SELECT FOR UPDATE / FOR SHARE</td>
          <td>跟 EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>ROW EXCLUSIVE</td>
          <td>UPDATE / DELETE / INSERT</td>
          <td>跟 SHARE 衝突</td>
      </tr>
      <tr>
          <td>SHARE UPDATE EXCLUSIVE</td>
          <td>VACUUM / ANALYZE / CREATE INDEX CONCURRENTLY</td>
          <td>跟同 mode + 高 mode 衝突</td>
      </tr>
      <tr>
          <td>SHARE</td>
          <td>CREATE INDEX（non-concurrent）</td>
          <td>跟 ROW EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>SHARE ROW EXCLUSIVE</td>
          <td>CREATE TRIGGER / 某些 ALTER</td>
          <td>跟 ROW EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>EXCLUSIVE</td>
          <td>REFRESH MATERIALIZED VIEW</td>
          <td>跟所有 + 自身衝突</td>
      </tr>
      <tr>
          <td>ACCESS EXCLUSIVE</td>
          <td>DROP / ALTER TABLE / VACUUM FULL</td>
          <td>跟所有衝突</td>
      </tr>
  </tbody>
</table>
<p>DDL（ALTER / DROP）拿 ACCESS EXCLUSIVE、跟所有衝突。Production 跑 ALTER 必須短時間或走 <a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">Online Schema Change</a>。</p>
<h3 id="3-advisory-lock--application-自己控">3. Advisory lock — Application 自己控</h3>
<p>PG 提供 <em>advisory lock</em> 給 application 用、不關 row / table 結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Session 1
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_advisory_lock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 跑 critical section
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_advisory_unlock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Session 2
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_try_advisory_lock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 試取、不阻塞、返回 false</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li>Application-level 互斥（如：cron job 同時只跑一個）</li>
<li>跨 connection 同步（PG-managed mutex）</li>
<li>Distributed transaction coordinator（lightweight）</li>
</ul>
<p>跟 row lock 不同：advisory lock 不關 row、application 自定義 lock ID 語義。</p>
<h3 id="4-predicate-lock--serializable-isolation-才用">4. Predicate lock — SERIALIZABLE isolation 才用</h3>
<p>PG SERIALIZABLE 用 <em>Serializable Snapshot Isolation (SSI)</em>、追蹤 <em>predicate</em>（query 條件）而不是 <em>row</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SERIALIZABLE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Predicate lock 紀錄這個 query 看了哪些 predicate
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 其他 transaction INSERT pending order
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- 提交時：PG 偵測 anomaly、rollback 之一
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">COMMIT</span><span class="p">;</span></span></span></code></pre></div><p>跟 MySQL gap lock 不同：</p>
<ul>
<li>MySQL gap lock：<em>pre-lock</em>、防 phantom 在 query 期間</li>
<li>PG predicate lock：<em>post-detect</em>、commit 時偵測 anomaly、退回 transaction</li>
</ul>
<p>PG SSI 對 <em>寫入吞吐影響低</em>（不 pre-lock）、但 <em>transaction rollback 機率高</em>（要 application retry）。</p>
<h2 id="pg-預設-isolationread-committed">PG 預設 isolation：READ COMMITTED</h2>
<p>PG 預設 READ COMMITTED、跟 MySQL InnoDB 預設 REPEATABLE READ 不同：</p>
<table>
  <thead>
      <tr>
          <th>Isolation</th>
          <th>PG 行為</th>
          <th>MySQL InnoDB 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>READ UNCOMMITTED</td>
          <td>PG 視為 READ COMMITTED（不真的支援 dirty read）</td>
          <td>MySQL 真支援</td>
      </tr>
      <tr>
          <td>READ COMMITTED</td>
          <td>每 statement 看當下 committed snapshot（PG 預設）</td>
          <td>一致</td>
      </tr>
      <tr>
          <td>REPEATABLE READ</td>
          <td>Transaction 內 fixed snapshot（純 MVCC）</td>
          <td>MVCC snapshot + gap lock 防 phantom（兩者都 MVCC、差在 phantom 防護機制：PG 靠 snapshot version visibility、InnoDB 加 gap lock pre-lock 範圍）</td>
      </tr>
      <tr>
          <td>SERIALIZABLE</td>
          <td>SSI、commit 時偵測 anomaly</td>
          <td>強 lock + gap</td>
      </tr>
  </tbody>
</table>
<p><strong>對 application code 含意</strong>：</p>
<ul>
<li>PG REPEATABLE READ 對 <em>寫入吞吐</em> 影響低（不 pre-lock、只 retry）</li>
<li>沒 gap lock → INSERT 不被 lock-induced 阻塞</li>
<li>Deadlock 機率比 MySQL 低數量級</li>
</ul>
<p>實務 PG production：用預設 READ COMMITTED 即可、SERIALIZABLE 留給 <em>strict consistency 需求</em>（金融 / 訂單）但接受 retry。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-idle-transaction-卡-vacuum--bloat-暴增">1. Idle transaction 卡 vacuum — Bloat 暴增</h3>
<p>PG MVCC 仰賴 <em>VACUUM 清理 dead tuple</em>。VACUUM 只清理 <em>沒 active transaction 看得到的 dead tuple</em>。如果有 <em>idle in transaction</em> session 持續開著（application connection pool 連線忘關 transaction）、VACUUM 看不到 <em>該 transaction snapshot 之後的 dead tuple</em>、累積 bloat。</p>
<p>修法：</p>
<ul>
<li>監控 <code>pg_stat_activity</code> 看 <code>state = 'idle in transaction'</code> 持續時間</li>
<li>設 <code>idle_in_transaction_session_timeout = '5min'</code> — 超時 PG 自動 kill 該 session</li>
<li>Application connection pool 配置 <em>不留 transaction 開著</em>（如：pgBouncer transaction pool 自動 commit / rollback）</li>
</ul>
<h3 id="2-select-for-update-跨-transaction--application-retry-麻煩">2. SELECT FOR UPDATE 跨 transaction — Application retry 麻煩</h3>
<p>跟 MySQL 不同：PG SELECT FOR UPDATE 不會 <em>block 其他 SELECT</em>（讀仍可繼續）、但 <em>block 其他 UPDATE / FOR UPDATE</em>。若 application 在 transaction 內 SELECT FOR UPDATE、其他 transaction 等。</p>
<p>如果 application 設計 <em>跨 transaction 持 lock</em>（如：取 lock + return UI + 等用戶操作 + commit）、容易撞 idle in transaction 跟其他 transaction wait。</p>
<p>修法：</p>
<ul>
<li><em>Transaction 短</em>：取 FOR UPDATE → 立刻處理 → commit、不跨 user interaction</li>
<li>跨 user interaction 用 <em>advisory lock</em> 或 application-level state machine、不依賴 row lock</li>
</ul>
<h3 id="3-advisory-lock-沒釋放--session-結束才自動釋放">3. Advisory lock 沒釋放 — Session 結束才自動釋放</h3>
<p><code>pg_advisory_lock()</code> 拿了、沒 <code>pg_advisory_unlock()</code>、lock 直到 <em>session 結束</em> 才自動釋放。Connection pool 重複使用同 connection、可能繼承前面留的 lock。</p>
<p>修法：</p>
<ul>
<li>用 <code>pg_advisory_lock</code> 必 <code>try/finally pg_advisory_unlock</code></li>
<li>或用 <em>session-level</em> 用 transaction-scoped：<code>pg_advisory_xact_lock()</code> — commit / rollback 自動釋放</li>
<li>監控 <code>pg_locks</code> 看 advisory lock count、長期累積是警訊</li>
</ul>
<h3 id="4-bloat-不只是-vacuum-沒跑是-active-transaction-阻擋-vacuum">4. Bloat 不只是 vacuum 沒跑、是 <em>active transaction 阻擋 vacuum</em></h3>
<p>第 #1 點延伸：vacuum 已跑、但 bloat 仍持續成長、原因不是 vacuum 不夠、是 <em>active transaction 阻擋 vacuum 看 dead tuple</em>。</p>
<p>修法：</p>
<ul>
<li>不只看 <code>last_vacuum</code>、看 <em>VACUUM 跑了但沒收回多少</em></li>
<li><code>SELECT * FROM pg_stat_progress_vacuum</code> 看 VACUUM 進度</li>
<li><code>SELECT * FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY backend_xmin</code> — 看誰阻擋 vacuum</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a></li>
</ul>
<h3 id="5-serializable-下-transaction-rollback--application-必須-retry">5. SERIALIZABLE 下 transaction rollback — Application 必須 retry</h3>
<p><code>SET TRANSACTION ISOLATION LEVEL SERIALIZABLE</code> 後、PG SSI 偵測到 anomaly 會 <em>rollback transaction</em>、application 看到 <code>serialization failure</code>、必須 retry。</p>
<p>對 <em>不知道要 retry</em> 的 application、SERIALIZABLE 變 production bug。</p>
<p>修法：</p>
<ul>
<li>Application code 加 <em>retry middleware</em>：catch <code>SQLSTATE 40001 (serialization_failure)</code> → exponential backoff retry</li>
<li>不必所有 transaction 走 SERIALIZABLE — 只對 <em>strict consistency 需求</em> 場景 set</li>
<li>高並發 SERIALIZABLE workload 容易 rollback storm、考慮拆 transaction 縮短時間</li>
</ul>
<h2 id="觀測-metric">觀測 metric</h2>
<p>Production 監控：</p>
<ul>
<li><code>pg_stat_activity</code>：active session / idle in transaction / wait_event</li>
<li><code>pg_locks</code>：當前 lock 列表、用 join 看誰 block 誰</li>
<li><code>pg_stat_database.deadlocks</code>：deadlock 計數（PG 較低、但仍要監控）</li>
<li><code>pg_stat_user_tables.n_dead_tup</code> / <code>n_live_tup</code>：dead tuple 比例 — bloat 指標</li>
<li><code>pg_stat_progress_vacuum</code>：VACUUM 進度</li>
</ul>
<h2 id="跟-mysql-lock-model-對比">跟 MySQL Lock Model 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG MVCC</th>
          <th>MySQL InnoDB Lock</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要機制</td>
          <td>MVCC + snapshot</td>
          <td>Lock-based + MVCC mixed</td>
      </tr>
      <tr>
          <td>Readers vs Writers</td>
          <td>不互 block</td>
          <td>預設 RR 下 gap lock 影響</td>
      </tr>
      <tr>
          <td>Deadlock 機率</td>
          <td>低（無 gap lock）</td>
          <td>中-高（gap lock 主要來源）</td>
      </tr>
      <tr>
          <td>Phantom 防護</td>
          <td>Snapshot 自然防 + SSI predicate lock</td>
          <td>Gap lock 預先 lock</td>
      </tr>
      <tr>
          <td>預設 isolation</td>
          <td>READ COMMITTED</td>
          <td>REPEATABLE READ</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>Dead tuple + VACUUM 治理</td>
          <td>Lock contention 治理</td>
      </tr>
      <tr>
          <td>Application code</td>
          <td>SERIALIZABLE 需 retry</td>
          <td>寫得不錯多數時 OK</td>
      </tr>
  </tbody>
</table>
<p>兩者解決同一問題（並行控制）、用不同策略。PG 用 <em>空間換時間</em>（保留多版本 tuple、讀寫不互鎖、但需 VACUUM 清理）、MySQL 用 <em>時間換空間</em>（lock 等待、但不必清舊版本）。</p>
<p><strong>選擇判讀</strong>：</p>
<ul>
<li>High 並發 OLTP、寫 / 讀都重：PG MVCC 通常更好（讀不 block 寫）</li>
<li>簡單 OLTP + 不想管 VACUUM：MySQL InnoDB 對 ops 簡單</li>
<li>需要 SERIALIZABLE 強一致：PG SSI 對寫吞吐影響低</li>
<li>已有 MySQL 生態 / 工具鏈：MySQL Lock 知識可繼續用</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">MySQL Lock Contention</a> — 完整 MySQL lock 機制。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-autovacuum-tuning">跟 Autovacuum Tuning</h3>
<p>MVCC 仰賴 VACUUM、autovacuum 是 PG 並行控制的 <em>維護成本</em>。VACUUM 跑慢 / 沒跑 → bloat → query 慢。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h3 id="跟-replication-topology">跟 Replication Topology</h3>
<p><code>hot_standby_feedback = on</code> 讓 standby 上 long-running query 不被 vacuum 取消、但 <em>standby 把 oldest xmin 推回 primary</em>、primary autovacuum 變保守、增加 bloat。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-connection-pool">跟 Connection Pool</h3>
<p>pgBouncer transaction pooling 模式下、advisory lock / SELECT FOR UPDATE 跨 transaction 行為 <em>broken</em>（不同 transaction 可能進不同 backend connection）。詳見 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer Config</a>。</p>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>長 transaction 跑慢 query 期間、其他 transaction 看到 snapshot bloat、planner 估錯 dead tuple ratio。詳見 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（VACUUM 是 MVCC 必要成本）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（hot_standby_feedback 影響）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PG pgBouncer</a>（transaction pooling 跟 lock 互動）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PG Online Schema Change</a>（DDL lock 議題）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（snapshot bloat 影響 planner）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">MySQL Lock Contention</a>（sibling、不同模型）</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level 卡片</a></li>
<li>官方：<a href="https://www.postgresql.org/docs/current/mvcc.html">PG MVCC</a> / <a href="https://www.postgresql.org/docs/current/transaction-iso.html">PG Concurrency Control</a> / <a href="https://www.postgresql.org/docs/current/explicit-locking.html">Explicit Locking</a></li>
</ul>
]]></content:encoded></item><item><title>3.C24 SoundCloud：AMQP fan-out 音訊處理 pipeline</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-soundcloud-fanout-audio/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-soundcloud-fanout-audio/</guid><description>&lt;p>這個案例的核心責任是說明 fan-out 處理 pipeline 該按處理類型拆隊列、不該共用 queue。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>上傳音訊後用 RabbitMQ 觸發 transcode + 波形圖 + follower 通知。當 Skrillex 等大號上傳時、要避免同步寫 Cassandra 千萬次。每秒 20-30,000 條 persistent message。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不同處理類型分開隊列、各自獨立 scale。揭露 fan-out 不是「broadcast 同一份工作」、而是「同事件觸發多種獨立 pipeline」、每種 pipeline 的 throughput / latency 要求不同。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Prefetch + consumer 併發 / classic queue vs Streams（log fan-out 場景）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blogs.vmware.com/tanzu/scaling-with-rabbitmq-soundcloud">Scaling with RabbitMQ at SoundCloud (VMware Tanzu)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.infoq.com/presentations/amqp-soundcloud/">AMQP at SoundCloud (InfoQ)&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 fan-out 處理 pipeline 該按處理類型拆隊列、不該共用 queue。</p>
<h2 id="觀察">觀察</h2>
<p>上傳音訊後用 RabbitMQ 觸發 transcode + 波形圖 + follower 通知。當 Skrillex 等大號上傳時、要避免同步寫 Cassandra 千萬次。每秒 20-30,000 條 persistent message。</p>
<h2 id="判讀">判讀</h2>
<p>不同處理類型分開隊列、各自獨立 scale。揭露 fan-out 不是「broadcast 同一份工作」、而是「同事件觸發多種獨立 pipeline」、每種 pipeline 的 throughput / latency 要求不同。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Prefetch + consumer 併發 / classic queue vs Streams（log fan-out 場景）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blogs.vmware.com/tanzu/scaling-with-rabbitmq-soundcloud">Scaling with RabbitMQ at SoundCloud (VMware Tanzu)</a></li>
<li><a href="https://www.infoq.com/presentations/amqp-soundcloud/">AMQP at SoundCloud (InfoQ)</a></li>
</ul>
]]></content:encoded></item><item><title>Falco</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/falco/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/falco/</guid><description>&lt;p>Falco 是 CNCF Graduated 的 runtime cloud-native threat detection engine、原 Sysdig 開源、Apache 2.0 license。它在 host / container 上用 eBPF（或 kernel module / userspace fallback）攔截 syscall、跟 Plugin 拉到的 audit log 串成同一條 event stream、丟給 Rule engine 比對 YAML rule、命中後 alert 到 stdout / Falcosidekick / SIEM。它跟商業 CNAPP runtime 模組（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog CWS&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework Polygraph&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &amp;#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud Defender&lt;/a>）的差異在 &lt;em>OSS rule-based vs SaaS ML-based + 平台廣度 + 自動 response 的工程責任歸屬&lt;/em>、偵測技術本身相近。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Falco 的核心定位是 &lt;em>K8s container runtime detection engine 的 OSS 基準&lt;/em>、不是 full CNAPP、也不是 inline enforcement。底層 Driver 分三層：&lt;em>modern eBPF&lt;/em>（Linux 5.8+、預設）、&lt;em>legacy kernel module (kmod)&lt;/em>（舊 kernel fallback）、&lt;em>pdig userspace probe&lt;/em>（沒 root 或非 Linux）；Driver 抓 syscall 跟 K8s audit / cloud audit event、送進 Falco engine；engine 用 Sysdig filter syntax 比對 YAML rule、命中後吐 alert。Plugin 系統讓 Falco 看到非 syscall event（K8s audit log、Okta event、GitHub audit、AWS CloudTrail）— 變成 &lt;em>general detection engine&lt;/em>、不只 host runtime。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cilium-tetragon/" data-link-title="Cilium Tetragon" data-link-desc="eBPF-based runtime security &amp;#43; inline enforcement、跟 Cilium CNI 同生態、TracingPolicy CRD、process credentials tracking &amp;#43; KillerAction">Cilium Tetragon&lt;/a> 比、Falco 走 &lt;em>rule engine + alert-only&lt;/em>、Tetragon 走 &lt;em>eBPF + 可 enforce kill action&lt;/em>；Falco 偵測為主、Tetragon 偵測 + 防護。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a>（CWS）比、Datadog 是 SaaS observability 上加 runtime security view、ML-based behavioral baseline 內建、但 vendor lock + per-host 計費；Falco 是 OSS 自管、rule 完全可寫、但 ML baseline / threat intel / cross-source correlation 要自己接 SIEM。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework&lt;/a> Polygraph 比、Lacework 走 behavior graph 自動建 baseline、Falco 走 rule-explicit、邊界看得到也好 audit。&lt;/p></description><content:encoded><![CDATA[<p>Falco 是 CNCF Graduated 的 runtime cloud-native threat detection engine、原 Sysdig 開源、Apache 2.0 license。它在 host / container 上用 eBPF（或 kernel module / userspace fallback）攔截 syscall、跟 Plugin 拉到的 audit log 串成同一條 event stream、丟給 Rule engine 比對 YAML rule、命中後 alert 到 stdout / Falcosidekick / SIEM。它跟商業 CNAPP runtime 模組（<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog CWS</a> / <a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework Polygraph</a> / <a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud Defender</a>）的差異在 <em>OSS rule-based vs SaaS ML-based + 平台廣度 + 自動 response 的工程責任歸屬</em>、偵測技術本身相近。</p>
<h2 id="服務定位">服務定位</h2>
<p>Falco 的核心定位是 <em>K8s container runtime detection engine 的 OSS 基準</em>、不是 full CNAPP、也不是 inline enforcement。底層 Driver 分三層：<em>modern eBPF</em>（Linux 5.8+、預設）、<em>legacy kernel module (kmod)</em>（舊 kernel fallback）、<em>pdig userspace probe</em>（沒 root 或非 Linux）；Driver 抓 syscall 跟 K8s audit / cloud audit event、送進 Falco engine；engine 用 Sysdig filter syntax 比對 YAML rule、命中後吐 alert。Plugin 系統讓 Falco 看到非 syscall event（K8s audit log、Okta event、GitHub audit、AWS CloudTrail）— 變成 <em>general detection engine</em>、不只 host runtime。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/cilium-tetragon/" data-link-title="Cilium Tetragon" data-link-desc="eBPF-based runtime security &#43; inline enforcement、跟 Cilium CNI 同生態、TracingPolicy CRD、process credentials tracking &#43; KillerAction">Cilium Tetragon</a> 比、Falco 走 <em>rule engine + alert-only</em>、Tetragon 走 <em>eBPF + 可 enforce kill action</em>；Falco 偵測為主、Tetragon 偵測 + 防護。跟 <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>（CWS）比、Datadog 是 SaaS observability 上加 runtime security view、ML-based behavioral baseline 內建、但 vendor lock + per-host 計費；Falco 是 OSS 自管、rule 完全可寫、但 ML baseline / threat intel / cross-source correlation 要自己接 SIEM。跟 <a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a> Polygraph 比、Lacework 走 behavior graph 自動建 baseline、Falco 走 rule-explicit、邊界看得到也好 audit。</p>
<p>關鍵張力：<em>偵測 vs 防護</em> 跟 <em>rule-explicit vs ML-baseline</em> 是兩條取捨軸。Falco 預設只發 alert、要 inline kill / cordon 要靠 Falco Talon 或外接 SOAR；rule 完全可寫是優點也是負擔 — 自家 anti-pattern 要自己寫成 condition、不像 SaaS CNAPP 預設有 ML baseline。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Falco 在 K8s runtime security stack 中承擔哪一段（syscall detection / audit log detection / alert forwarding）、哪些要外接（Talon / SIEM / SOAR）</li>
<li>Driver 選擇（modern eBPF / kmod / pdig）跟 kernel 環境 / 部署模型 的對應、選錯會 silent miss event</li>
<li>Rule 寫作的 ownership 設計（誰寫、誰 review、staging 怎麼觀察 false positive）</li>
<li>何時用 Falco、何時改走 Tetragon（要 enforcement）或商業 CNAPP（要 ML baseline + 跨雲 posture）</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Falco deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Driver 是否符合 kernel 環境</strong>：modern eBPF on 5.8+ / kmod on legacy / pdig on serverless 或 non-root container；driver 跟 kernel 不對等於 silent miss，要看 <code>falco --version</code> 跟啟動 log 確認 driver 載入成功</li>
<li><strong>Rule ownership 跟 lifecycle</strong>：Falco 內建 rule（<code>falco_rules.yaml</code> / <code>k8s_audit_rules.yaml</code>）+ 自家 custom rule 是否走 Git PR review、staging tenant 跑幾小時觀察 false positive、再 promote production</li>
<li><strong>Alert sink + downstream routing</strong>：Falco 預設輸出 stdout / file / syslog、production 幾乎都接 Falcosidekick 做 fan-out（Slack / SIEM / S3 / Webhook），跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 的接點明確</li>
<li><strong>Response 是 alert-only 還是有 enforcement</strong>：純 alert 走 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 事故處理</a> routing；要自動 kill pod / cordon node 需 Falco Talon 或 SOAR、且 high-impact action 走 approval gate</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Driver layer</strong>：Falco 三種 driver — <em>modern eBPF</em>（CO-RE、Linux 5.8+、預設、不需 kernel header）、<em>legacy kernel module</em>（kmod、舊 kernel 唯一選、要 DKMS build）、<em>pdig</em>（userspace、ptrace-based、非 root container 或 macOS dev 環境用、效能差）。production K8s deployment 幾乎都走 modern eBPF、DaemonSet 部署到每個 node、kernel 版本不夠才走 kmod；不要混用 driver、否則 alert source 難對齊。</p>
<p><strong>Rule YAML 結構</strong>：Falco rule 由 <code>condition</code>（Sysdig filter syntax、類 SQL where）、<code>output</code>（alert template、含 field interpolation）、<code>priority</code>（emergency / alert / critical / error / warning / notice / informational / debug）、<code>tags</code>（mitre / cis / NIST 對應）組成。<code>condition</code> 寫法跟 Linux syscall 緊耦合（<code>evt.type=execve</code>、<code>fd.name=/etc/passwd</code>、<code>proc.name=nc</code>）— rule engineer 要對 syscall 跟 process tree 熟悉。<code>macro</code> 跟 <code>list</code> 讓 rule 可重用（<code>macro: container_started</code> / <code>list: shell_binaries</code>）、production rule 庫應該 macro-first、不是每條 rule 重寫 condition。</p>
<p><strong>Plugin ecosystem</strong>：Plugin 把 Falco 從 host syscall 擴張到任意 event source — <em>k8saudit plugin</em> 接 K8s API server audit log（看 RBAC change / Secret access）、<em>cloudtrail plugin</em> 接 AWS CloudTrail、<em>okta plugin</em> 接 Okta system log、<em>github plugin</em> 接 GitHub audit log。Plugin 讓 Falco 成為 <em>general detection engine</em>、不只 container runtime；但 plugin event source 跟 SIEM 重疊、要清楚 ownership — <em>Falco 做近 host 即時偵測、SIEM 做跨來源歷史 correlation</em>、別兩邊都跑同一條 rule。</p>
<p><strong>Falcosidekick + alert fan-out</strong>：Falco engine 預設輸出 stdout / file / gRPC、production 接 Falcosidekick（DaemonSet 旁邊或單獨 Deployment）做 fan-out — 同一個 alert 同時 forward 到 Slack（SOC chat）、Splunk HEC / Elastic / Loki（SIEM 持久化）、S3（合規 archive）、Webhook（自家 dashboard）、Prometheus（metrics）。Sidekick 是 stateless forwarder、不做 dedup / aggregation、那層要在 SIEM 處理。</p>
<p><strong>Falco Talon + 自動 response</strong>：Talon 是 response orchestrator、訂閱 Falcosidekick 的 webhook output、依照 rule action 自動執行 — kill pod、cordon node、加 NetworkPolicy、call webhook 通知 SOAR。Talon 把 <em>偵測 → 處置</em> 從手動 SOC playbook 變 declarative YAML、但 high-impact action（kill prod pod、cordon node）必須走 approval gate 或限制在 staging namespace、不能黑箱 fire-and-forget。對應 <a href="/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">Detection to Response Routing</a> 的章節原則。</p>
<p><strong>Helm chart 部署 + GitOps</strong>：Falco 官方 Helm chart 把 DaemonSet（Falco engine + driver）、Deployment（Falcosidekick）、ConfigMap（rule YAML）、ServiceAccount + RBAC 包成一組。生產 deployment 走 Argo CD / Flux 同步 Helm value、rule YAML 進 Git PR review、merge 觸發 staging tenant deploy、人工觀察 24-48hr false positive、再 promote production。Rule 直接改 ConfigMap、不走版控等於 detection drift、後續審計接不上。</p>
<p><strong>跟 SIEM / 8 事故處理整合</strong>：Falco alert 經 Falcosidekick 進 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a> 後、走跟其他 detection signal 同一條 correlation + triage 管線、不獨立 channel。Notable / high-priority alert 進 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8 事故處理</a> 的 IR queue、走 incident commander handoff。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Falco</th>
          <th>Cilium Tetragon</th>
          <th>Datadog CWS</th>
          <th>Lacework Polygraph</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>License</td>
          <td>Apache 2.0 OSS</td>
          <td>Apache 2.0 OSS</td>
          <td>Commercial SaaS</td>
          <td>Commercial SaaS</td>
      </tr>
      <tr>
          <td>Detection 模型</td>
          <td>Rule-explicit（YAML + Sysdig filter）</td>
          <td>Rule-explicit（YAML + TracingPolicy）</td>
          <td>ML-based behavioral baseline + rule</td>
          <td>Behavior graph 自動 baseline</td>
      </tr>
      <tr>
          <td>Enforcement</td>
          <td>Alert-only（Talon 補 response）</td>
          <td>Inline enforce（kill / signal、可阻擋）</td>
          <td>Inline enforce（Datadog Agent）</td>
          <td>Alert + workload baseline drift</td>
      </tr>
      <tr>
          <td>Driver</td>
          <td>modern eBPF / kmod / pdig</td>
          <td>eBPF only（cilium ecosystem）</td>
          <td>eBPF（Datadog Agent）</td>
          <td>eBPF（Lacework Agent）</td>
      </tr>
      <tr>
          <td>涵蓋面</td>
          <td>Container + host + plugin (audit log)</td>
          <td>Container + host（cilium 整合 network）</td>
          <td>Container + host + cloud + app</td>
          <td>Cloud + container + workload + IaC posture</td>
      </tr>
      <tr>
          <td>Cross-source</td>
          <td>靠 Plugin + Falcosidekick → SIEM</td>
          <td>靠 Cilium Hubble + 外接 SIEM</td>
          <td>內建（Datadog observability plane）</td>
          <td>內建（Polygraph graph）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>中 — Sysdig filter + macro</td>
          <td>中 — TracingPolicy + cilium 知識</td>
          <td>緩 — 沿用 Datadog UI / Workload Security</td>
          <td>緩 — SaaS console</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>OSS-first、SIEM 已部署、rule 想完全可寫</td>
          <td>要 inline enforcement、cilium CNI 已用</td>
          <td>Datadog 已用、cloud-native、預算允許</td>
          <td>CNAPP + posture 一站、跨雲</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — rule 是 YAML、可移植 Sigma</td>
          <td>中 — TracingPolicy 跟 cilium 綁定</td>
          <td>高 — Workload Security rule 跟 platform 綁</td>
          <td>高 — Polygraph data 跟 platform 綁</td>
      </tr>
  </tbody>
</table>
<p>選 Falco 的核心訴求：<em>K8s container runtime detection、OSS + rule-customizable、SIEM 已部署、SOC 有 detection engineer 寫得了 Sysdig filter rule</em>。要 inline enforcement 直接走 Tetragon；要 ML baseline + 跨雲 posture + 不想自管 rule lifecycle 直接走 Datadog CWS / Lacework / <a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> + <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Custom rule 設計</strong>：production rule 庫應該 <em>macro-first</em>、把可重用條件抽成 macro（<code>container_started</code> / <code>sensitive_mount</code> / <code>shell_in_container</code>）跟 list（<code>shell_binaries</code> / <code>sensitive_files</code>）；rule 引用 macro 而非重寫 condition、修改 macro 等於同時更新所有引用 rule。Rule 反例是 <em>single-event noisy rule</em>（看到一個 shell exec 就 alert）— production rule 應該 <em>context-bounded</em>（shell exec <strong>in container</strong> + parent <strong>不在 allowlist</strong> + image <strong>非 trusted registry</strong>）+ priority 階梯（生產 Notice、staging Warning、新規則先 Informational 觀察）。</p>
<p><strong>eBPF driver vs kmod 取捨</strong>：modern eBPF 用 CO-RE（Compile Once, Run Everywhere）、不需 per-kernel build、運行時動態 attach；kmod 需要 DKMS 在 host build、跟 kernel version 強耦合、升級 kernel 要重 build。所有現代 Linux distro 預設都該走 modern eBPF；只有 RHEL 7 / 老 Ubuntu LTS（kernel &lt; 5.8）才有理由用 kmod。pdig 給沒 root / 沒 eBPF 的環境（某些 serverless container、macOS dev）、效能差不適合 production。</p>
<p><strong>Falco Talon 自動 response 設計</strong>：Talon 把「Falco alert → 自動處置」變 declarative — rule action 可以是 <em>kubernetes:terminate-pod</em>、<em>kubernetes:label-pod</em>、<em>kubernetes:cordon-node</em>、<em>aws:disable-iam-user</em>、<em>calico:add-networkpolicy</em>。production 用 Talon 的關鍵原則：<em>high-impact action 走 approval gate</em>（PagerDuty incident → human approve → execute）、<em>containment-first not deletion</em>（先 cordon + label、再人工決定是否 terminate）、<em>blast radius 限制</em>（只能影響特定 namespace / label selector）、<em>audit trail</em>（每個 action 進 Splunk + IR queue）。</p>
<p><strong>Plugin ecosystem 邊界</strong>：Plugin 把 Falco 變 general detection engine、但要明確 plugin event 跟 SIEM 重疊處的 ownership。建議：<em>host syscall + container runtime → Falco rule</em>（即時 + low latency）、<em>K8s audit + cloud audit + IdP audit → 同時跑 Falco plugin（近即時 alert） + SIEM（歷史 correlation）</em>、<em>純跨來源 correlation（多 user 多 source 多時段）→ SIEM 為主</em>。別讓 Falco plugin 跟 SIEM rule 跑重複條件、會 double-alert 也 double-cost。</p>
<p><strong>Sigstore + SBOM 整合的位置</strong>：Falco 不做 image scan / SBOM 驗證（那是 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft &amp; Grype</a> 的位置）、但 runtime detection 是 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a> 縱深防禦的最後一層 — image scan 過、簽章驗證過、但 runtime 出現異常 syscall（log4shell 觸發 outbound LDAP、SolarWinds 合法簽章但行為異常）、Falco rule 是最後抓的點。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Falco 啟動成功但完全沒 event</strong>：driver 沒載入（modern eBPF 在舊 kernel fallback 失敗）— 看啟動 log 確認 <code>driver loaded successfully</code>、<code>falco --version</code> 對 driver 版本、舊 kernel 改 kmod</li>
<li><strong>大量 false positive 淹沒 SOC</strong>：rule 寫太寬（<code>shell in container</code> 但合法 debug shell 也 trigger）— staging tenant 跑 48hr 統計 FP、加 exception list 或改 macro 排除已知合法 source、新 rule 先 Informational priority 觀察</li>
<li><strong>Alert 沒進 SIEM</strong>：Falcosidekick 沒接、或 output channel 設錯 — 確認 Falcosidekick Deployment up、output webhook 對、SIEM HEC token 沒過期；Falco engine 本身的 stdout / file output 仍會留、不會 silent miss</li>
<li><strong>Rule update 後 detection drift</strong>：直接改 ConfigMap、沒走 Git PR + staging 觀察 — 強制 GitOps（Argo CD / Flux）、ConfigMap immutable、rule change 必須走 PR review + staging promote</li>
<li><strong>Plugin event lag / 漏抓</strong>：plugin polling cloud audit log（CloudTrail / Okta）的 latency 跟 API rate limit、不是即時 — 純即時偵測別靠 plugin、改靠 SIEM streaming ingest；plugin 適合補 syscall 看不到的層</li>
<li><strong>Talon 自動 response 誤殺 prod</strong>：rule action 直接 kill pod、沒 approval gate — 高影響 action 拆成兩步（先 label + cordon、再人工 approve terminate）、blast radius 限 namespace / label selector、audit trail 全進 SIEM</li>
<li><strong>eBPF driver 跟 kernel 升級不對齊</strong>：node kernel 升級後 modern eBPF 仍 CO-RE 自動適配、但 Falco 版本太舊不支援新 syscall — Falco engine 跟著定期升級、別 pin 在兩年前的 version</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>要 inline kill / enforcement</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cilium-tetragon/" data-link-title="Cilium Tetragon" data-link-desc="eBPF-based runtime security &#43; inline enforcement、跟 Cilium CNI 同生態、TracingPolicy CRD、process credentials tracking &#43; KillerAction">Cilium Tetragon</a></td>
      </tr>
      <tr>
          <td>ML behavioral baseline + 跨雲</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a>、<a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a></td>
      </tr>
      <tr>
          <td>Full CNAPP + posture + runtime</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a>、<a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS</a></td>
      </tr>
      <tr>
          <td>Image scan / SBOM / SCA</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft &amp; Grype</a>、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
      </tr>
      <tr>
          <td>Cross-source SIEM correlation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>、<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Sysdig filter syntax 完整 reference、syscall field 細目</li>
<li>Falco source code 內部架構（libsinsp / libscap）</li>
<li>Sysdig Secure（Falco 的商業版、Sysdig Inc. 維護、含 ML baseline + cloud posture）的功能對照細節</li>
<li>Container image scan / SBOM 驗證（屬 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft &amp; Grype</a> 的位置）</li>
<li>Kubernetes RBAC / Pod Security Standards / NetworkPolicy 的設計（屬 K8s 平台層、不在 runtime detection 範圍）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Falco 在 07 案例庫沒有直接 vendor-level 事件、但多個 runtime / supply chain case 都是 Falco rule 第一線該抓的場景：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Falco 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>Falco rule 偵測 desktop app process spawn 異常子程序 + outbound callback、補簽章驗證之外的 runtime 行為層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>Falco rule 偵測 JNDI lookup 觸發的 outbound LDAP / DNS、補 <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> image scan 之外的 runtime detection</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>合法簽章 binary 但 runtime 行為異常（process tree / outbound C2 / 異常 file access）、Falco rule + Talon containment 是最後一層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>對照啟示：Falco 主場是 host / container runtime、cloud-native data warehouse 行為偵測要走 SIEM + 平台層 audit、非 Falco 範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>Falco rule + macro + list 走 propose → staging tune → promote → review 的工程 lifecycle、不是 ConfigMap 直改</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>Falco rule priority 階梯（新規則先 Informational、staging 觀察 48hr、再 promote Warning / Critical）是 alert fatigue 的工程化解法</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">Detection to Response Routing</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cilium-tetragon/" data-link-title="Cilium Tetragon" data-link-desc="eBPF-based runtime security &#43; inline enforcement、跟 Cilium CNI 同生態、TracingPolicy CRD、process credentials tracking &#43; KillerAction">Cilium Tetragon</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a>、<a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（Falco alert 進 SIEM 做 cross-source correlation）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> / <a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft &amp; Grype</a>（image scan + SBOM、跟 runtime detection 構成 supply chain 縱深）、<a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a> / <a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS</a>（商業 CNAPP runtime 對照）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Falco notable alert → IR routing）、<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">Supply Chain Integrity</a>（artifact trust 跟 runtime detection 的縱深關係）</li>
<li>官方：<a href="https://falco.org/docs/">Falco Documentation</a>、<a href="https://github.com/falcosecurity/rules">Falco Rules Repository</a></li>
</ul>
]]></content:encoded></item><item><title>9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/</guid><description>&lt;p>這個案例的核心責任是說明 B2B SaaS 平台的容量規劃跟 C2C 案例的本質差異。Genesys 服務的是 &lt;em>客戶服務中心&lt;/em> — 客戶停線 = 全終端使用者打不通電話、客戶會失去信任。99.999% 可用性（年停機 5 分鐘）對 B2B 客服 SaaS 是合約義務、不是行銷敘述。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Genesys Cloud 在 DynamoDB 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/genesys-dynamodb-case-study/">Genesys DynamoDB Case Study&lt;/a>）：&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>客戶組織&lt;/td>
 &lt;td>8,000+ 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務國家&lt;/td>
 &lt;td>100+ 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主 region&lt;/td>
 &lt;td>15 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>衛星 region&lt;/td>
 &lt;td>5 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性&lt;/td>
 &lt;td>99.999%（截至 2024-07-31 的 12 個月）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>微服務數&lt;/td>
 &lt;td>數百個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料層&lt;/td>
 &lt;td>DynamoDB 為預設、用其他要 justify&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵架構決策（引述 Chief Architect Rob Gevers）：「Amazon DynamoDB is our primary data layer by default, and teams have to justify the use of something else.」&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Genesys 案例揭露三個 B2B SaaS 平台容量規劃重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>B2B 可用性目標跟 C2C 不同&lt;/strong>：B2C 大型網站可能接受 99.9%（年停機 8.76 小時）、B2B SaaS 經常合約規定 99.95% 或 99.99%、客服平台類甚至要 99.999%（年停機 5 分鐘）。每多一個 9、容量規劃跟運維成本指數成長。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 SLO 等級設計。&lt;/li>
&lt;li>&lt;strong>「DynamoDB 為預設、用其他要 justify」是規模化平台的工程治理&lt;/strong>：跟 &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&lt;/a> 整合到 Aurora 是同樣訴求、不同實作 — Genesys 選 DynamoDB 為基準是因為「Multi-region active-active」+「自動 scaling」+「99.999% SLA」的組合最容易達成 5 個 9 目標。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 DB 預設選型。&lt;/li>
&lt;li>&lt;strong>15 主 region + 5 衛星 region = 全球客戶就近接入&lt;/strong>：客戶服務有強烈延遲敏感（agent 操作介面卡 1 秒、客服效率掉一半）、必須在客戶所在地有 region。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster&lt;/a> 的延遲驅動 region 部署同類思維。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的地理分散規劃。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>「99.999% over 12 months」是 &lt;em>截至特定時間點的歷史值&lt;/em>、不代表「未來持續達成」。可用性是滾動指標、不是恆久承諾。&lt;/li>
&lt;li>案例 &lt;em>沒有&lt;/em> 提具體 QPS / RPS、訊息量、延遲分布。讀者要對 &lt;em>策略&lt;/em> 學習、具體數字需要自己壓測。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 B2B SaaS 平台的容量規劃跟 C2C 案例的本質差異。Genesys 服務的是 <em>客戶服務中心</em> — 客戶停線 = 全終端使用者打不通電話、客戶會失去信任。99.999% 可用性（年停機 5 分鐘）對 B2B 客服 SaaS 是合約義務、不是行銷敘述。</p>
<h2 id="觀察">觀察</h2>
<p>Genesys Cloud 在 DynamoDB 的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/genesys-dynamodb-case-study/">Genesys DynamoDB Case Study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶組織</td>
          <td>8,000+ 個</td>
      </tr>
      <tr>
          <td>服務國家</td>
          <td>100+ 個</td>
      </tr>
      <tr>
          <td>主 region</td>
          <td>15 個</td>
      </tr>
      <tr>
          <td>衛星 region</td>
          <td>5 個</td>
      </tr>
      <tr>
          <td>可用性</td>
          <td>99.999%（截至 2024-07-31 的 12 個月）</td>
      </tr>
      <tr>
          <td>微服務數</td>
          <td>數百個</td>
      </tr>
      <tr>
          <td>資料層</td>
          <td>DynamoDB 為預設、用其他要 justify</td>
      </tr>
  </tbody>
</table>
<p>關鍵架構決策（引述 Chief Architect Rob Gevers）：「Amazon DynamoDB is our primary data layer by default, and teams have to justify the use of something else.」</p>
<h2 id="判讀">判讀</h2>
<p>Genesys 案例揭露三個 B2B SaaS 平台容量規劃重點。</p>
<ol>
<li><strong>B2B 可用性目標跟 C2C 不同</strong>：B2C 大型網站可能接受 99.9%（年停機 8.76 小時）、B2B SaaS 經常合約規定 99.95% 或 99.99%、客服平台類甚至要 99.999%（年停機 5 分鐘）。每多一個 9、容量規劃跟運維成本指數成長。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 SLO 等級設計。</li>
<li><strong>「DynamoDB 為預設、用其他要 justify」是規模化平台的工程治理</strong>：跟 <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> 整合到 Aurora 是同樣訴求、不同實作 — Genesys 選 DynamoDB 為基準是因為「Multi-region active-active」+「自動 scaling」+「99.999% SLA」的組合最容易達成 5 個 9 目標。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 DB 預設選型。</li>
<li><strong>15 主 region + 5 衛星 region = 全球客戶就近接入</strong>：客戶服務有強烈延遲敏感（agent 操作介面卡 1 秒、客服效率掉一半）、必須在客戶所在地有 region。跟 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster</a> 的延遲驅動 region 部署同類思維。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的地理分散規劃。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「99.999% over 12 months」是 <em>截至特定時間點的歷史值</em>、不代表「未來持續達成」。可用性是滾動指標、不是恆久承諾。</li>
<li>案例 <em>沒有</em> 提具體 QPS / RPS、訊息量、延遲分布。讀者要對 <em>策略</em> 學習、具體數字需要自己壓測。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>B2B SaaS 平台優先選 multi-region active-active 資料層</strong>：DynamoDB Global Tables、Cosmos DB Multi-Region Write、Spanner multi-region 都是候選。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的全球一致性取捨。</li>
<li><strong>「預設 DB」原則簡化 onboarding</strong>：新團隊不用評估十種 DB、預設用 X、特殊需求再 justify。減少團隊認知負擔、加速產品開發。對應 <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> 的 DB 整合。</li>
<li><strong>99.999% 必須有 redundancy 在每一層</strong>：DNS、load balancer、application、database、storage 都要跨 region active-active。任何一層 single-region 就破壞整體 SLO。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 跟 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性驗證模組</a>。</li>
<li><strong>多 region 是成本 vs 可用性的硬取捨</strong>：15 個 region 的成本約是 1 個 region 的 15 倍 — 對 B2B SaaS 是合理投資、對 B2C 通常不划算。</li>
</ol>
<p>跨平台等效：Azure Cosmos DB Multi-Region Write、GCP Spanner multi-region、Cassandra multi-DC 都可實作對等架構。差異是 region 數量、SLA 承諾、跨 region 延遲。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 B2B SaaS 可用性 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget 政策</a></li>
<li>想設計多 region 資料層 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想做 DB 統一治理 → <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 consolidation</a> + <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a></li>
<li>想規劃跨 region 容量 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <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></li>
<li>想理解 DynamoDB 99.999% 背後的 partition / GSI 設計 → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a> + <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">DynamoDB GSI / LSI 設計</a></li>
<li>想對應 global tables 多 region 寫衝突 → <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">DynamoDB global tables 寫衝突</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/genesys-dynamodb-case-study/">Genesys Achieves 99.999% Availability Using Amazon DynamoDB</a></li>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
</ul>
]]></content:encoded></item><item><title>6.24 規則推送安全閘門</title><link>https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>規則推送安全閘門（rule rollout safety gate）的核心責任是防止控制面錯誤快速擴散到資料面。這個閘門是補上「規則與配置類變更」特有風險，跟既有 release gate 互補而非取代：變更體積小、覆蓋範圍大、擴散速度快。&lt;/p>
&lt;p>當變更屬於 WAF rule、routing policy、token/policy、或 Addressing API 相關設定時，判讀重點從程式碼正確性轉為擴散控制。這類變更即使 diff 很短，也可能在數十秒內影響跨區域流量與多產品控制面。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>典型風險&lt;/th>
 &lt;th>為何需要獨立 gate&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>WAF / regex 規則更新&lt;/td>
 &lt;td>高計算成本規則拖垮 edge runtime&lt;/td>
 &lt;td>CI 綠燈無法代表 runtime 成本安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Routing / BYOIP 相關設定變更&lt;/td>
 &lt;td>prefix withdrawal 造成服務不可達&lt;/td>
 &lt;td>單一 API 查詢語意錯誤可全網擴散&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Token / policy 控制面變更&lt;/td>
 &lt;td>多產品授權連鎖失效&lt;/td>
 &lt;td>shared dependency 失效會誤導排障路徑&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;h2 id="產業情境遊戲服務的規則推送安全">產業情境：遊戲服務的規則推送安全&lt;/h2>
&lt;p>遊戲的規則推送（平衡性調整、反作弊規則、賽季設定、經濟系統參數）有特殊的擴散特性：影響面是全體玩家、生效時機即時、且玩家行為會立刻適應規則變更。&lt;/p>
&lt;p>規則推送的 blast radius 預設是全體在線玩家。一次平衡性調整會立刻改變所有正在進行的比賽的角色強度、裝備數值或技能效果。跟一般 feature flag 的 percentage rollout 不同，遊戲平衡性需要所有玩家看到相同規則，否則同場比賽的公平性會被破壞。&lt;/p>
&lt;p>推送時機需要跟 match lifecycle 對齊。在進行中的比賽套用新規則會造成不公平 — 某隊在舊規則下建立的優勢可能在新規則下消失。安全做法是在 match boundary（比賽開始或結束時）切換，讓新規則只套用到新開始的比賽。這要求規則推送系統能區分「已開始的 match」和「即將開始的 match」。&lt;/p>
&lt;p>回退條件需要綁定遊戲特有的 KPI。active player count 下降超過門檻、match completion rate 異常降低（玩家中途離開）、player report rate 上升（玩家回報異常體驗）、in-game economy anomaly（虛擬貨幣或道具流通量異常）都是規則推送出問題的訊號。這些 KPI 的 feedback loop 比一般服務長 — 玩家行為的適應需要數小時到數天才會穩定，短窗觀察可能漏掉延遲暴露的問題。&lt;/p>
&lt;p>反作弊規則的推送有額外約束：規則內容本身是機密的，推送失敗後不能在 log 或 alert 中暴露規則細節，回退也必須在不洩漏偵測邏輯的前提下進行。&lt;/p>
&lt;h2 id="gate-檢查層">Gate 檢查層&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>Gate 問題&lt;/th>
 &lt;th>不通過訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Query / API 語意&lt;/td>
 &lt;td>查詢參數有沒有安全預設與錯誤拒絕策略&lt;/td>
 &lt;td>空參數回傳全量、模糊布林語意&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule 計算成本&lt;/td>
 &lt;td>規則是否通過代表性 payload 成本檢查&lt;/td>
 &lt;td>單一規則可造成 CPU 熱點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推送策略&lt;/td>
 &lt;td>是否採分群 rollout 並設即時回退條件&lt;/td>
 &lt;td>一次推全域、無分區觀測閘門&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Withdrawal 限速&lt;/td>
 &lt;td>批次撤告 / 刪除是否有數量與速率限制&lt;/td>
 &lt;td>單次操作可影響大量 prefixes 或 bindings&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared dependency&lt;/td>
 &lt;td>是否識別跨產品共享控制點&lt;/td>
 &lt;td>多產品同時異常卻無 shared root 判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence 與回寫&lt;/td>
 &lt;td>事故後是否可回放決策、查證恢復路徑與狀態差異&lt;/td>
 &lt;td>決策只留結論，缺假設與驗證證據&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>控制面變更後，多區域同時出現 5xx / timeout / auth 失敗&lt;/li>
&lt;li>指標在秒級惡化，且與最新規則或 policy 變更高度同時&lt;/li>
&lt;li>回退後短時間明顯回穩，顯示變更與故障高度關聯&lt;/li>
&lt;li>部分服務可自助恢復、部分服務需人工修復，代表狀態損壞分層&lt;/li>
&lt;li>事故中出現「每個產品都在修自己的症狀」，代表 shared dependency 沒被先識別&lt;/li>
&lt;/ul>
&lt;h2 id="最低可執行-gate">最低可執行 Gate&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>變更分類&lt;/strong>：將規則/配置/控制面 API 變更標為 &lt;code>high-blast-radius change&lt;/code>。&lt;/li>
&lt;li>&lt;strong>語意檢查&lt;/strong>：對 query 參數、空值與預設行為做拒絕式驗證。&lt;/li>
&lt;li>&lt;strong>成本檢查&lt;/strong>：用代表性 payload 跑 rule-level CPU / latency 測試。&lt;/li>
&lt;li>&lt;strong>分批推送&lt;/strong>：至少分成小流量群組與全量兩階段，且每階段有回退條件。&lt;/li>
&lt;li>&lt;strong>撤告保護&lt;/strong>：對 withdrawal / delete 設速率與數量上限，超限自動中止。&lt;/li>
&lt;li>&lt;strong>決策紀錄&lt;/strong>：事故期間保留假設、證據、回退門檻、owner 與狀態差異。&lt;/li>
&lt;/ol>
&lt;h2 id="反模式">反模式&lt;/h2>
&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>把規則推送當一般配置&lt;/td>
 &lt;td>低估擴散速度與影響面&lt;/td>
 &lt;td>強制走高風險變更 gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只看 CI / lint&lt;/td>
 &lt;td>無法捕捉 runtime 計算成本風險&lt;/td>
 &lt;td>補 rule replay 與成本基線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全域一次推送&lt;/td>
 &lt;td>擴散太快，回退窗口太短&lt;/td>
 &lt;td>改 staged rollout + 即時回退閘門&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故後只寫事後摘要&lt;/td>
 &lt;td>無法復盤決策與恢復路徑&lt;/td>
 &lt;td>補 decision log + evidence package&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>未分離 desired/actual state&lt;/td>
 &lt;td>壞狀態難以回到已知穩定點&lt;/td>
 &lt;td>引入 snapshot 與 staged state restore&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例回扣">案例回扣&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">Cloudflare 2023 Control Plane Token Incident&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">Cloudflare 2026 BYOIP BGP Withdrawal&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>這三個案例對應同一個上位問題：控制面小變更如何在短時間擴散成全網事故。不同事故只是擴散路徑不同，gate 核心都是「先限制擴散、再修復功能」。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>規則推送安全閘門（rule rollout safety gate）的核心責任是防止控制面錯誤快速擴散到資料面。這個閘門是補上「規則與配置類變更」特有風險，跟既有 release gate 互補而非取代：變更體積小、覆蓋範圍大、擴散速度快。</p>
<p>當變更屬於 WAF rule、routing policy、token/policy、或 Addressing API 相關設定時，判讀重點從程式碼正確性轉為擴散控制。這類變更即使 diff 很短，也可能在數十秒內影響跨區域流量與多產品控制面。</p>
<h2 id="適用場景">適用場景</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>典型風險</th>
          <th>為何需要獨立 gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>WAF / regex 規則更新</td>
          <td>高計算成本規則拖垮 edge runtime</td>
          <td>CI 綠燈無法代表 runtime 成本安全</td>
      </tr>
      <tr>
          <td>Routing / BYOIP 相關設定變更</td>
          <td>prefix withdrawal 造成服務不可達</td>
          <td>單一 API 查詢語意錯誤可全網擴散</td>
      </tr>
      <tr>
          <td>Token / policy 控制面變更</td>
          <td>多產品授權連鎖失效</td>
          <td>shared dependency 失效會誤導排障路徑</td>
      </tr>
      <tr>
          <td>共享控制面批次清理任務</td>
          <td>批量誤刪或批量撤告</td>
          <td>需要數量閘門與緊急停機機制</td>
      </tr>
  </tbody>
</table>
<h2 id="產業情境遊戲服務的規則推送安全">產業情境：遊戲服務的規則推送安全</h2>
<p>遊戲的規則推送（平衡性調整、反作弊規則、賽季設定、經濟系統參數）有特殊的擴散特性：影響面是全體玩家、生效時機即時、且玩家行為會立刻適應規則變更。</p>
<p>規則推送的 blast radius 預設是全體在線玩家。一次平衡性調整會立刻改變所有正在進行的比賽的角色強度、裝備數值或技能效果。跟一般 feature flag 的 percentage rollout 不同，遊戲平衡性需要所有玩家看到相同規則，否則同場比賽的公平性會被破壞。</p>
<p>推送時機需要跟 match lifecycle 對齊。在進行中的比賽套用新規則會造成不公平 — 某隊在舊規則下建立的優勢可能在新規則下消失。安全做法是在 match boundary（比賽開始或結束時）切換，讓新規則只套用到新開始的比賽。這要求規則推送系統能區分「已開始的 match」和「即將開始的 match」。</p>
<p>回退條件需要綁定遊戲特有的 KPI。active player count 下降超過門檻、match completion rate 異常降低（玩家中途離開）、player report rate 上升（玩家回報異常體驗）、in-game economy anomaly（虛擬貨幣或道具流通量異常）都是規則推送出問題的訊號。這些 KPI 的 feedback loop 比一般服務長 — 玩家行為的適應需要數小時到數天才會穩定，短窗觀察可能漏掉延遲暴露的問題。</p>
<p>反作弊規則的推送有額外約束：規則內容本身是機密的，推送失敗後不能在 log 或 alert 中暴露規則細節，回退也必須在不洩漏偵測邏輯的前提下進行。</p>
<h2 id="gate-檢查層">Gate 檢查層</h2>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>Gate 問題</th>
          <th>不通過訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query / API 語意</td>
          <td>查詢參數有沒有安全預設與錯誤拒絕策略</td>
          <td>空參數回傳全量、模糊布林語意</td>
      </tr>
      <tr>
          <td>Rule 計算成本</td>
          <td>規則是否通過代表性 payload 成本檢查</td>
          <td>單一規則可造成 CPU 熱點</td>
      </tr>
      <tr>
          <td>推送策略</td>
          <td>是否採分群 rollout 並設即時回退條件</td>
          <td>一次推全域、無分區觀測閘門</td>
      </tr>
      <tr>
          <td>Withdrawal 限速</td>
          <td>批次撤告 / 刪除是否有數量與速率限制</td>
          <td>單次操作可影響大量 prefixes 或 bindings</td>
      </tr>
      <tr>
          <td>Shared dependency</td>
          <td>是否識別跨產品共享控制點</td>
          <td>多產品同時異常卻無 shared root 判讀</td>
      </tr>
      <tr>
          <td>Evidence 與回寫</td>
          <td>事故後是否可回放決策、查證恢復路徑與狀態差異</td>
          <td>決策只留結論，缺假設與驗證證據</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>控制面變更後，多區域同時出現 5xx / timeout / auth 失敗</li>
<li>指標在秒級惡化，且與最新規則或 policy 變更高度同時</li>
<li>回退後短時間明顯回穩，顯示變更與故障高度關聯</li>
<li>部分服務可自助恢復、部分服務需人工修復，代表狀態損壞分層</li>
<li>事故中出現「每個產品都在修自己的症狀」，代表 shared dependency 沒被先識別</li>
</ul>
<h2 id="最低可執行-gate">最低可執行 Gate</h2>
<ol>
<li><strong>變更分類</strong>：將規則/配置/控制面 API 變更標為 <code>high-blast-radius change</code>。</li>
<li><strong>語意檢查</strong>：對 query 參數、空值與預設行為做拒絕式驗證。</li>
<li><strong>成本檢查</strong>：用代表性 payload 跑 rule-level CPU / latency 測試。</li>
<li><strong>分批推送</strong>：至少分成小流量群組與全量兩階段，且每階段有回退條件。</li>
<li><strong>撤告保護</strong>：對 withdrawal / delete 設速率與數量上限，超限自動中止。</li>
<li><strong>決策紀錄</strong>：事故期間保留假設、證據、回退門檻、owner 與狀態差異。</li>
</ol>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險結果</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把規則推送當一般配置</td>
          <td>低估擴散速度與影響面</td>
          <td>強制走高風險變更 gate</td>
      </tr>
      <tr>
          <td>只看 CI / lint</td>
          <td>無法捕捉 runtime 計算成本風險</td>
          <td>補 rule replay 與成本基線</td>
      </tr>
      <tr>
          <td>全域一次推送</td>
          <td>擴散太快，回退窗口太短</td>
          <td>改 staged rollout + 即時回退閘門</td>
      </tr>
      <tr>
          <td>事故後只寫事後摘要</td>
          <td>無法復盤決策與恢復路徑</td>
          <td>補 decision log + evidence package</td>
      </tr>
      <tr>
          <td>未分離 desired/actual state</td>
          <td>壞狀態難以回到已知穩定點</td>
          <td>引入 snapshot 與 staged state restore</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回扣">案例回扣</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">Cloudflare 2023 Control Plane Token Incident</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">Cloudflare 2026 BYOIP BGP Withdrawal</a></li>
</ul>
<p>這三個案例對應同一個上位問題：控制面小變更如何在短時間擴散成全網事故。不同事故只是擴散路徑不同，gate 核心都是「先限制擴散、再修復功能」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><code>04</code>： <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li><code>06</code>： <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
<li><code>06</code>： <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li><code>06</code>： <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li><code>08</code>： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li><code>08</code>： <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL 5.7 → 8.0 Major Version Upgrade：character set / authentication / atomic DDL 三條 paradigm 同時換軌</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/major-version-upgrade/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/major-version-upgrade/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> 內 version upgrade migration playbook、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type E paradigm shift 結構。&lt;/p>&lt;/blockquote>
&lt;p>5.7 → 8.0 看起來是 &lt;em>minor bump&lt;/em>（從 5.7.40 升到 8.0.36）、但不是。Oracle 把這個 release boundary 當成 &lt;em>清庫存的機會&lt;/em> — 同時推出 3 個 &lt;em>behavioral paradigm shift&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Paradigm&lt;/th>
 &lt;th>5.7 default&lt;/th>
 &lt;th>8.0 default&lt;/th>
 &lt;th>影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Character set&lt;/td>
 &lt;td>latin1 / utf8（=utf8mb3）&lt;/td>
 &lt;td>utf8mb4&lt;/td>
 &lt;td>string column 儲存 + emoji / 4-byte UTF-8&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Authentication plugin&lt;/td>
 &lt;td>mysql_native_password&lt;/td>
 &lt;td>caching_sha2_password&lt;/td>
 &lt;td>client / library 需要支援新 plugin&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DDL atomicity&lt;/td>
 &lt;td>Non-atomic（crash 留 orphan）&lt;/td>
 &lt;td>Atomic（crash recovery 乾淨）&lt;/td>
 &lt;td>開發信心、crash recovery 行為&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對應 &lt;em>任意一個&lt;/em> paradigm 升級失誤、production 都會 down。三條同時換、必須 &lt;em>三條都規劃&lt;/em>。&lt;/p>
&lt;p>這條 upgrade 比 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &amp;#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major-version-upgrade&lt;/a> 工作量大 — PG major upgrade 主要是 &lt;em>pg_upgrade&lt;/em> 工具流程、MySQL 是 &lt;em>behavioral compatibility audit + ecosystem 全 review&lt;/em>。&lt;/p>
&lt;h2 id="為什麼是-type-e不是-minor-upgrade">為什麼是 Type E（不是 minor upgrade）&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#%e5%af%ab%e5%89%8d%e7%9a%84-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> 內 version upgrade migration playbook、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type E paradigm shift 結構。</p></blockquote>
<p>5.7 → 8.0 看起來是 <em>minor bump</em>（從 5.7.40 升到 8.0.36）、但不是。Oracle 把這個 release boundary 當成 <em>清庫存的機會</em> — 同時推出 3 個 <em>behavioral paradigm shift</em>：</p>
<table>
  <thead>
      <tr>
          <th>Paradigm</th>
          <th>5.7 default</th>
          <th>8.0 default</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Character set</td>
          <td>latin1 / utf8（=utf8mb3）</td>
          <td>utf8mb4</td>
          <td>string column 儲存 + emoji / 4-byte UTF-8</td>
      </tr>
      <tr>
          <td>Authentication plugin</td>
          <td>mysql_native_password</td>
          <td>caching_sha2_password</td>
          <td>client / library 需要支援新 plugin</td>
      </tr>
      <tr>
          <td>DDL atomicity</td>
          <td>Non-atomic（crash 留 orphan）</td>
          <td>Atomic（crash recovery 乾淨）</td>
          <td>開發信心、crash recovery 行為</td>
      </tr>
  </tbody>
</table>
<p>對應 <em>任意一個</em> paradigm 升級失誤、production 都會 down。三條同時換、必須 <em>三條都規劃</em>。</p>
<p>這條 upgrade 比 <a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major-version-upgrade</a> 工作量大 — PG major upgrade 主要是 <em>pg_upgrade</em> 工具流程、MySQL 是 <em>behavioral compatibility audit + ecosystem 全 review</em>。</p>
<h2 id="為什麼是-type-e不是-minor-upgrade">為什麼是 Type E（不是 minor upgrade）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#%e5%af%ab%e5%89%8d%e7%9a%84-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Medium</td>
          <td>SQL 一致、reserved keyword 新增、collation 預設變</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Medium-High</td>
          <td>binary upgrade flow 簡單、但 ecosystem 工具兼容性 audit 工作量大</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>High</td>
          <td>3 條 default paradigm shift（charset / auth / atomic DDL）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
          <td>同 MySQL 引擎、不引新 component</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Medium-High</td>
          <td>client library / driver / connection string 都可能要改</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>部署 topology 不變</td>
      </tr>
  </tbody>
</table>
<p>Paradigm = High + App change = Medium-High → <strong>Type E paradigm shift</strong>。</p>
<p>雖然是 <em>同一個 vendor 的 major version</em>、實際的 <em>application 行為差異</em> 跨越多個 paradigm、6 type 框架仍適用、結構走 partial migration 收斂。</p>
<h2 id="4-phase-upgrade">4-phase upgrade</h2>
<h3 id="phase-1pre-check-audit">Phase 1：Pre-check audit</h3>
<p>8.0 升級前用 <em>MySQL Shell upgrade checker</em> + 手動 audit：</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">mysqlsh root@5.7-primary.example.com -- util check-for-server-upgrade</span></span></code></pre></div><p>Upgrade checker 報告：</p>
<ul>
<li><em>Reserved keyword</em> 衝突（5.7 不是 keyword 但 8.0 是、例如 <code>WINDOW</code> / <code>RANK</code> / <code>LATERAL</code>）</li>
<li>舊 character set / collation 使用點（latin1 / utf8mb3）</li>
<li>Deprecated feature 使用（GROUP BY 隱含 ORDER BY 等）</li>
<li>Datatype 變動（DATETIME 行為微差）</li>
</ul>
<p>手動 audit：</p>
<ul>
<li>Application driver / library 版本是否支援 caching_sha2_password</li>
<li>Connection string 內 <code>default-authentication-plugin</code> 設定</li>
<li>ORM / framework 是否假設 utf8 而非 utf8mb4</li>
</ul>
<p>完成標準：寫出 <em>blocker list</em>（必須在升級前修） + <em>warning list</em>（可在升級後處理）。</p>
<h3 id="phase-2shadow-upgrade--replica-升-80">Phase 2：Shadow upgrade — Replica 升 8.0</h3>
<p>從 <em>non-critical replica</em> 升起。先升一個 replica、跑 production traffic（read-only）2-4 週：</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. Stop replica</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">systemctl stop mysql
</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"># 2. Backup（XtraBackup）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">xtrabackup --backup --target-dir<span class="o">=</span>/backup/pre-upgrade
</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="c1"># 3. Install MySQL 8.0 binary（apt / yum 升級）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">apt-get install mysql-server-8.0
</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="c1"># 4. 啟動 8.0、自動 upgrade data dictionary</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">systemctl start mysql
</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="c1"># 5. 8.0 自動跑 server-upgrade（8.0.16+ 內建、mysql_upgrade utility 已 deprecated）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 若 5.7 升 8.0.16 之前 server、才需要手動跑 mysql_upgrade -u root -p</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 6. 重新 attach 為 5.7 primary 的 replica（8.0 replica 可 attach 5.7 primary）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">CHANGE MASTER TO <span class="nv">MASTER_AUTO_POSITION</span><span class="o">=</span>1<span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">START SLAVE<span class="p">;</span></span></span></code></pre></div><p>跑 production read traffic 觀察：</p>
<ul>
<li>Query result 是否跟 5.7 一致（特別 character set 相關）</li>
<li>Replication lag 是否在 baseline 範圍</li>
<li>8.0-specific feature 是否需要（hash join / window function 等）</li>
</ul>
<h3 id="phase-3promote-80-為-primary">Phase 3：Promote 8.0 為 primary</h3>
<p>確認 shadow replica 穩定後：</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. 升其他 replica 到 8.0</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># （per-replica 跑 Phase 2 流程）</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="c1"># 2. Application application 改用 8.0-compatible driver</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 把 connection string 加 default-authentication-plugin=caching_sha2_password</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 或仍用 mysql_native_password（user 端設定）</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"># 3. Failover：promote 8.0 replica 為 primary</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 用 Orchestrator / 自管 failover 流程</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="c1"># 4. 5.7 primary 變成 8.0 replica、升 5.7 → 8.0</span></span></span></code></pre></div><p>完成標準：所有 server 都是 8.0、application 連 8.0 endpoint 無 error。</p>
<h3 id="phase-4decommission-57--套用-80-paradigm">Phase 4：Decommission 5.7 + 套用 8.0 paradigm</h3>
<p>完成 binary upgrade 不是真正完成 — 還要逐步遷移 paradigm：</p>
<ul>
<li>
<p><strong>Character set 升級</strong>：歷史 latin1 / utf8 table 改 utf8mb4</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">CONVERT</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="nb">CHARACTER</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">utf8mb4</span><span class="w"> </span><span class="k">COLLATE</span><span class="w"> </span><span class="n">utf8mb4_0900_ai_ci</span><span class="p">;</span></span></span></code></pre></div><p>每張 table 走 gh-ost / pt-osc（避免 production 阻塞）</p>
</li>
<li>
<p><strong>Authentication 升級</strong>：逐步把 user 從 <code>mysql_native_password</code> 改 <code>caching_sha2_password</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;app&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">caching_sha2_password</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;new_password&#39;</span><span class="p">;</span></span></span></code></pre></div><p>需確認 application driver 已支援新 plugin（多數 modern driver OK、legacy 可能要升級）</p>
</li>
<li>
<p><strong>Reserved keyword 處理</strong>：column / table 名稱跟新 reserved word 衝突的、改名</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">RENAME</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">window</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">event_window</span><span class="p">;</span></span></span></code></pre></div></li>
</ul>
<p>多數 org 在 Phase 3 停留更久 — paradigm 升級不是一次 big bang、是漸進。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-authentication-plugin--application-突然連不上">1. Authentication plugin — Application 突然連不上</h3>
<p>升 8.0 後 <em>new user</em> 預設用 caching_sha2_password、舊 application driver（&lt; 5 年版本）不支援、connect error: <code>Authentication plugin 'caching_sha2_password' cannot be loaded</code>。</p>
<p>修法：</p>
<ul>
<li><em>先升 driver</em>：每個 application 升級 mysql-connector-* 到支援 caching_sha2 的版本（多數 modern release 已支援）</li>
<li>短期 workaround：用 <code>mysql_native_password</code>（new user 顯式 create with <code>IDENTIFIED WITH mysql_native_password</code>）</li>
<li>設 <code>default_authentication_plugin=mysql_native_password</code>、強制保留舊 default</li>
</ul>
<h3 id="2-character-set-4-byte-utf-8--emoji-進不去">2. Character set 4-byte UTF-8 — Emoji 進不去</h3>
<p>5.7 latin1 / utf8（=utf8mb3）column 升 8.0 後 <em>仍是 utf8mb3</em>、不會自動升 utf8mb4。Application 寫入 emoji（4-byte UTF-8）會被 <em>truncate / 拒絕</em>。</p>
<p>修法：</p>
<ul>
<li><em>逐 table CONVERT</em>：gh-ost / pt-osc 跑 <code>ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4</code></li>
<li>新建 table 預設用 utf8mb4（<code>character_set_server=utf8mb4</code> 設定）</li>
<li>Application 連線 charset 設定一致（<code>character_set_client / connection / results</code>）</li>
</ul>
<h3 id="3-reserved-keyword--application-query-突然-syntax-error">3. Reserved keyword — Application query 突然 syntax error</h3>
<p>5.7 跑得好的 query：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">window</span><span class="p">,</span><span class="w"> </span><span class="n">rank</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="p">;</span></span></span></code></pre></div><p>8.0 報錯：<code>window</code> 跟 <code>rank</code> 都是 reserved keyword、必須 backtick：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">`</span><span class="n">window</span><span class="o">`</span><span class="p">,</span><span class="w"> </span><span class="o">`</span><span class="n">rank</span><span class="o">`</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="p">;</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>Phase 1 upgrade checker 已抓出來、Application code review 改 SQL</li>
<li>推薦 <em>predefer table / column 名 backtick</em> policy（一律加 backtick、避免未來 reserved word 衝突）</li>
<li>ORM 多數會自動 backtick、raw SQL 容易踩</li>
</ul>
<h3 id="4-group-replication--新-feature-開了就不能-rollback">4. Group Replication / 新 feature 開了就不能 rollback</h3>
<p>8.0 升級後 <em>誘惑使用 8.0-only feature</em>：</p>
<ul>
<li>Group Replication（5.7 也有但 8.0 更穩）</li>
<li>Resource Group（5.7 沒有）</li>
<li>Histograms（5.7 沒有）</li>
<li>CTE / window function（5.7 沒有）</li>
</ul>
<p>一旦 application 用了這些 feature、不能 rollback 5.7（feature 不存在、query 失敗）。</p>
<p>修法：</p>
<ul>
<li><em>Phase 1-3 期間禁用 8.0-only feature</em>、保留 rollback option</li>
<li><em>Phase 4 完成</em> 且穩定運作 30+ 天後、才開始 evaluate 8.0-only feature</li>
<li>加 8.0-only feature 時 <em>明確記錄不可 rollback</em></li>
</ul>
<h3 id="5-collation-default-變動--sort-order-跟-unique-行為改變">5. Collation default 變動 — Sort order 跟 unique 行為改變</h3>
<p>5.7 utf8mb4 預設 collation = <code>utf8mb4_general_ci</code>、8.0 預設 = <code>utf8mb4_0900_ai_ci</code>。兩者排序行為不一致：</p>
<ul>
<li><code>utf8mb4_general_ci</code>：簡化 collation、不嚴格遵循 Unicode</li>
<li><code>utf8mb4_0900_ai_ci</code>：Unicode 9.0 compliance、accent-insensitive</li>
</ul>
<p>對 <em>已存在的 table</em>、collation 不會被 8.0 升級改變（保留 5.7 設定）。但 <em>新建 table</em> 預設用 0900_ai_ci、UNION / JOIN 跨不同 collation 的 column 可能 error: <code>Illegal mix of collations</code>。</p>
<p>修法：</p>
<ul>
<li>統一 collation：要麼 <em>所有 table 改 0900_ai_ci</em>、要麼 <em>所有 table 保留 general_ci</em></li>
<li>Schema migration 走 OSC 工具</li>
<li>Application 內 sort-dependent logic（leaderboard / search ranking）要驗證新 collation 結果</li>
</ul>
<h2 id="capability-gap57-有但-80-沒有">Capability gap：5.7 有但 8.0 沒有</h2>
<p>少數 8.0 <em>拿走</em> 的能力：</p>
<ul>
<li><strong>Query Cache</strong>：5.7 內建（但已 deprecated）、8.0 <em>完全移除</em>。Query cache 在高並發場景 actually slowing down、移除是好事</li>
<li><strong>InnoDB MEMORY engine</strong>：5.7 部分支援、8.0 限制更多</li>
<li><strong>Some MyISAM optimizations</strong>：8.0 強制 InnoDB-first、MyISAM-specific 工作流 broken</li>
</ul>
<p>對 Query Cache user：升 8.0 前評估是否依賴、考慮改 application-side cache（Redis）。</p>
<h2 id="容量與成本對照">容量與成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>5.7</th>
          <th>8.0</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cost</td>
          <td>Free (CE) / Enterprise</td>
          <td>Free (CE) / Enterprise</td>
      </tr>
      <tr>
          <td>升級 hosts × 時間</td>
          <td>-</td>
          <td>per-instance ~30 分鐘 binary upgrade</td>
      </tr>
      <tr>
          <td>Application 改動</td>
          <td>-</td>
          <td>driver upgrade + SQL review</td>
      </tr>
      <tr>
          <td>Character set conversion</td>
          <td>-</td>
          <td>per-table OSC、大表小時級</td>
      </tr>
      <tr>
          <td>Ops headcount</td>
          <td>-</td>
          <td>1-2 個 DBA × 2-4 週</td>
      </tr>
      <tr>
          <td>對 production 影響</td>
          <td>-</td>
          <td>Phase 2-3 漸進升級、無大 downtime</td>
      </tr>
  </tbody>
</table>
<p>5.7 → 8.0 upgrade 整體成本是 <em>1-2 個 FTE 月</em> 規模。對中型 deployment（100+ DB）可能更多。</p>
<h2 id="何時不升">何時不升</h2>
<ul>
<li><strong>App 用 Query Cache 重度</strong>：8.0 沒了、要 application 改造</li>
<li><strong>Old driver 不能升</strong>：legacy enterprise application 用 10 年前 driver、driver vendor 已倒、無法升 8.0-compatible</li>
<li><strong>Compliance freeze</strong>：某些金融 / 醫療場景 freeze technology 多年、升級需要重 audit + recertification</li>
<li><strong>5.7 已 EOL（2023-10）後仍堅持不升</strong>：security risk 高、應該 <em>優先升</em></li>
</ul>
<h2 id="跟-postgresql-major-version-upgrade-對比">跟 PostgreSQL Major Version Upgrade 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>MySQL 5.7 → 8.0</th>
          <th>PostgreSQL N → N+1</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tool</td>
          <td>binary upgrade + 自動 server-upgrade（8.0.16+；舊版用 mysql_upgrade）</td>
          <td>pg_upgrade（in-place）</td>
      </tr>
      <tr>
          <td>Downtime</td>
          <td>&lt; 5 分鐘 per instance（binary + DD upgrade）</td>
          <td>&lt; 1 分鐘 per instance（pg_upgrade）</td>
      </tr>
      <tr>
          <td>Paradigm shift</td>
          <td>3 條（charset / auth / atomic DDL）</td>
          <td>一般 0-1 條（PG major 多保 compat）</td>
      </tr>
      <tr>
          <td>App 必須改</td>
          <td>多（driver + query）</td>
          <td>少（多數 query 兼容）</td>
      </tr>
      <tr>
          <td>Risk</td>
          <td>高（paradigm 多）</td>
          <td>中-低</td>
      </tr>
      <tr>
          <td>Rollback</td>
          <td>不可（一旦 atomic DDL data 寫入、5.7 不認）</td>
          <td>不可（pg_upgrade 不可逆）</td>
      </tr>
  </tbody>
</table>
<p>PG major upgrade 比 MySQL 簡單。MySQL 5.7 → 8.0 是 <em>特例</em> — Oracle 把多年 deprecated 一次清。8.0 → 8.4 / 9.x 預期更平順。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>8.0 replica 可 attach 5.7 primary（向下兼容）、但 5.7 replica <em>不能 attach 8.0 primary</em>（向上不兼容）。Upgrade 順序必須 <em>replica 先升、primary 後升</em>。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-innodb-tuning">跟 InnoDB Tuning</h3>
<p>8.0 InnoDB 改寫了 redo log（atomic、可動態調整）、<code>innodb_log_file_size</code> 升級後可以 <em>online 改</em>、不必停機。詳見 <a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">InnoDB Tuning</a>。</p>
<h3 id="跟-modern-sql-features">跟 Modern SQL Features</h3>
<p>8.0 補 CTE / window / JSON_TABLE / hash join — 是 <em>為什麼要升 8.0</em> 的 driver。詳見 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">Modern SQL Features</a>。</p>
<h3 id="跟-group-replication">跟 Group Replication</h3>
<p>GR 在 5.7 有、但 8.0 才成熟。Group Replication 的 <em>MySQL Shell + Router</em> 整套 stack 主要在 8.0 才完整。詳見 <a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">Group Replication</a>。</p>
<h3 id="跟-aurora--planetscale-等-managed">跟 Aurora / PlanetScale 等 managed</h3>
<p>從 5.7 升 8.0 是個好時機 <em>同時評估</em> 是否要遷 Aurora / PlanetScale — 既然要做 paradigm shift、不如一次到位。詳見 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">migrate-to-aurora</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">migrate-to-planetscale</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（升級順序 replica-first）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>（升 8.0 的主要 driver）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/group-replication/" data-link-title="MySQL Group Replication / InnoDB Cluster：single-primary vs multi-primary mode 對 transaction certification 的影響" data-link-desc="MySQL Group Replication 提供 synchronous multi-primary replication、用 Paxos-like Group Communication Engine（GCE）達成 quorum-based commit。但「multi-primary」不是「single-primary 多開幾個 write 入口」、是 *transaction conflict detection &#43; certification* 整個機制不同。本文走 GR 機制（GCE &#43; certification &#43; applier）、single-primary vs multi-primary mode、InnoDB Cluster 跟 MySQL Shell / Router 整合、5 production 踩雷（cert lag / write conflict / large transaction / network partition / member 加入 catch-up）、何時用 GR 何時用傳統 replication">MySQL Group Replication</a>（8.0 成熟）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/innodb-tuning/" data-link-title="MySQL InnoDB Tuning：為什麼一個 100 GB DB 在 64 GB RAM server 上 query 慢 5 倍" data-link-desc="InnoDB 是 MySQL 預設 storage engine、預設值給 256 MB buffer pool（早期 default）。本文從一個常見痛點開場（DB &gt; RAM 但 server 仍 swap）、走 4 個 critical knob（buffer pool / redo log / flush method / IO capacity）、各自如何影響讀寫吞吐、配置 step-by-step、5 production 踩雷（buffer pool warm-up / log file 大小 / 設 sync_binlog=0 換速度 / IO scheduler / undo log 膨脹）、跟 SSD / NVMe / EBS 的 IO 假設">MySQL InnoDB Tuning</a>（8.0 redo log 改寫）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">migrate-to-aurora</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">migrate-to-planetscale</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL Major Version Upgrade</a>（PG sibling）</li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type E paradigm shift）</li>
<li>官方：<a href="https://dev.mysql.com/doc/refman/8.0/en/upgrading.html">MySQL 8.0 Upgrade Guide</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL JSONB Deep Dive：Binary Storage + GIN Index 為什麼是結構性優勢</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>JSONB deep dive&lt;/em> — binary storage + GIN index 的結構性優勢。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="json-vs-jsonb選-jsonb">JSON vs JSONB：選 JSONB&lt;/h2>
&lt;p>PG 9.2 加 &lt;code>JSON&lt;/code> type、9.4 加 &lt;code>JSONB&lt;/code>。99% 場景用 JSONB：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>JSON&lt;/th>
 &lt;th>JSONB&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>儲存&lt;/td>
 &lt;td>純文字（原樣保存）&lt;/td>
 &lt;td>Binary decomposed format&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Parse cost&lt;/td>
 &lt;td>每次 query parse&lt;/td>
 &lt;td>Insert 時 parse 一次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 支援&lt;/td>
 &lt;td>Limited（functional index）&lt;/td>
 &lt;td>GIN / functional / partial 都行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operator 支援&lt;/td>
 &lt;td>有限（→ / →&amp;gt;）&lt;/td>
 &lt;td>完整（@&amp;gt; / ? / @? / ? 等）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Duplicate key&lt;/td>
 &lt;td>保留（原樣）&lt;/td>
 &lt;td>只保留最後一個（normalize）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Key order&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>不保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Whitespace&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>不保留&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>JSONB 唯一缺點是 &lt;em>binary 儲存（不保留 key order / whitespace / duplicate）&lt;/em>。99% application 不在意這些。&lt;/p>
&lt;p>從 &lt;em>application semantics&lt;/em> 視角、JSONB 是 PG JSON 的 &lt;em>the right type&lt;/em>、JSON 是 &lt;em>legacy / niche&lt;/em>。&lt;/p>
&lt;h2 id="jsonb-gin-index核心結構性優勢">JSONB GIN Index：核心結構性優勢&lt;/h2>
&lt;p>PG GIN（Generalized Inverted Index）可以對 JSONB 內所有 key/value pair 建 inverted index：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SERIAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- GIN index
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_products_metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">GIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完後、JSONB query 用 GIN index 加速：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- @&amp;gt; (contains) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">@&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{&amp;#34;category&amp;#34;: &amp;#34;shoes&amp;#34;}&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ? (has key) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;discount&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ?| (has any of these keys) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">array&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;discount&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;promotion&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟 MongoDB index 對比、PG 不必 &lt;em>預先 define&lt;/em> JSON path index、&lt;code>USING GIN (metadata)&lt;/code> 對 &lt;em>整個 JSONB document 任意 path&lt;/em> 都有效。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>JSONB deep dive</em> — binary storage + GIN index 的結構性優勢。</p></blockquote>
<hr>
<h2 id="json-vs-jsonb選-jsonb">JSON vs JSONB：選 JSONB</h2>
<p>PG 9.2 加 <code>JSON</code> type、9.4 加 <code>JSONB</code>。99% 場景用 JSONB：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>JSON</th>
          <th>JSONB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>儲存</td>
          <td>純文字（原樣保存）</td>
          <td>Binary decomposed format</td>
      </tr>
      <tr>
          <td>Parse cost</td>
          <td>每次 query parse</td>
          <td>Insert 時 parse 一次</td>
      </tr>
      <tr>
          <td>Index 支援</td>
          <td>Limited（functional index）</td>
          <td>GIN / functional / partial 都行</td>
      </tr>
      <tr>
          <td>Operator 支援</td>
          <td>有限（→ / →&gt;）</td>
          <td>完整（@&gt; / ? / @? / ? 等）</td>
      </tr>
      <tr>
          <td>Duplicate key</td>
          <td>保留（原樣）</td>
          <td>只保留最後一個（normalize）</td>
      </tr>
      <tr>
          <td>Key order</td>
          <td>保留</td>
          <td>不保留</td>
      </tr>
      <tr>
          <td>Whitespace</td>
          <td>保留</td>
          <td>不保留</td>
      </tr>
  </tbody>
</table>
<p>JSONB 唯一缺點是 <em>binary 儲存（不保留 key order / whitespace / duplicate）</em>。99% application 不在意這些。</p>
<p>從 <em>application semantics</em> 視角、JSONB 是 PG JSON 的 <em>the right type</em>、JSON 是 <em>legacy / niche</em>。</p>
<h2 id="jsonb-gin-index核心結構性優勢">JSONB GIN Index：核心結構性優勢</h2>
<p>PG GIN（Generalized Inverted Index）可以對 JSONB 內所有 key/value pair 建 inverted index：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">metadata</span><span class="w"> </span><span class="n">JSONB</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- GIN index
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">);</span></span></span></code></pre></div><p>加完後、JSONB query 用 GIN index 加速：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- @&gt; (contains) 用 GIN
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- ? (has key) 用 GIN
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s1">&#39;discount&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- ?| (has any of these keys) 用 GIN
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?|</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;discount&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;promotion&#39;</span><span class="p">];</span></span></span></code></pre></div><p>跟 MongoDB index 對比、PG 不必 <em>預先 define</em> JSON path index、<code>USING GIN (metadata)</code> 對 <em>整個 JSONB document 任意 path</em> 都有效。</p>
<h3 id="jsonb_ops-vs-jsonb_path_ops"><code>jsonb_ops</code> vs <code>jsonb_path_ops</code></h3>
<p>PG GIN 對 JSONB 有兩種 <em>operator class</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><code>jsonb_ops</code>（預設）</th>
          <th><code>jsonb_path_ops</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引內容</td>
          <td>Key + value 都索引</td>
          <td>只索引 path → value pair</td>
      </tr>
      <tr>
          <td>Index size</td>
          <td>大</td>
          <td>小（約一半）</td>
      </tr>
      <tr>
          <td>支援 operator</td>
          <td><code>@&gt; / ? / ?| / ?&amp;</code></td>
          <td>只 <code>@&gt;</code> (containment)</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>多種 query pattern</td>
          <td>只用 <code>@&gt;</code> 的場景</td>
      </tr>
  </tbody>
</table>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- jsonb_ops（預設）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_meta_default</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- jsonb_path_ops（小、快、但只支援 @&gt;）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_meta_path</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="w"> </span><span class="n">jsonb_path_ops</span><span class="p">);</span></span></span></code></pre></div><p><strong>選擇</strong>：</p>
<ul>
<li>只跑 <code>@&gt;</code> containment query → <code>jsonb_path_ops</code>（index 小、快）</li>
<li>跑 <code>?</code> / <code>?|</code> / <code>?&amp;</code> key existence query → <code>jsonb_ops</code>（預設）</li>
</ul>
<h2 id="operator--path-query">Operator + Path Query</h2>
<p>JSONB 提供豐富 operator + jsonpath：</p>
<h3 id="operator">Operator</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Extract value（returns jsonb）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- Extract text（returns text）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">-&gt;&gt;</span><span class="w"> </span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- Path extract
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">#&gt;</span><span class="w"> </span><span class="s1">&#39;{variants, 0, price}&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">#&gt;&gt;</span><span class="w"> </span><span class="s1">&#39;{variants, 0, price}&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 返回 text
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- Containment（用 GIN index）
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;, &#34;active&#34;: true}&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- Reverse containment
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="s1">&#39;{&#34;sub&#34;: &#34;value&#34;}&#39;</span><span class="w"> </span><span class="o">&lt;@</span><span class="w"> </span><span class="n">metadata</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="c1">-- Key existence
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s1">&#39;discount&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?|</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;b&#39;</span><span class="p">];</span><span class="w">  </span><span class="c1">-- 任一 key
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?&amp;</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;b&#39;</span><span class="p">];</span><span class="w">  </span><span class="c1">-- 全部 key</span></span></span></code></pre></div><h3 id="jsonpathpg-12">jsonpath（PG 12+）</h3>
<p>SQL/JSON jsonpath 是 SQL standard、PG 12+ 支援：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- jsonb_path_query：展開 path 結果
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">jsonb_path_query</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*].price&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- jsonb_path_exists：返 boolean
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">jsonb_path_exists</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*] ? (@.price &gt; 100)&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- jsonb_path_query_array：返 array of result
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">jsonb_path_query_array</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.tags[*]&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span></span></span></code></pre></div><p>jsonpath 比 PG-specific operator 標準化、跨 vendor portable。</p>
<h2 id="partial-jsonb-index">Partial JSONB Index</h2>
<p>對 <em>只 query subset row</em> 的場景、建 partial index：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 只對 active product 建 metadata index
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_active_products_metadata</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Query active products + JSONB filter
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="c1">-- → planner 用 partial GIN index</span></span></span></code></pre></div><p>Partial index 比 full GIN 小很多、write cost 低、index hit rate 高。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-大-jsonb--toast--性能崩潰">1. 大 JSONB + TOAST — 性能崩潰</h3>
<p>JSONB &gt; 2 KB 自動進 TOAST（PG 內外部 storage）、每次 query read 該 row 都要 <em>de-TOAST</em>（拉外部 storage 再合併）。大 JSONB（&gt; 50 KB）每次 query 慢 10-100x。</p>
<p>修法：</p>
<ul>
<li>把 <em>大 attribute 拆獨立 column</em>（如 <code>description TEXT</code> 不放 metadata）</li>
<li>用 <em>JSON path index</em> 對 hot path 加速、不必每次讀整個 JSONB</li>
<li>用 <code>pg_column_size(metadata)</code> 監控 JSONB size 分布、找 outlier</li>
<li>對 truly 大 document（&gt; 1 MB）考慮 separate table 或 object storage</li>
</ul>
<h3 id="2-nested-update--整個-jsonb-重寫">2. Nested update — 整個 JSONB 重寫</h3>
<p>PG 沒 <em>atomic partial update</em>。修改 nested key 必須讀整個 JSONB → 修改 → 寫回：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">products</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">jsonb_set</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;{discount}&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;0.2&#39;</span><span class="p">::</span><span class="n">jsonb</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 等同於：讀 metadata、改 discount、寫回整個 metadata</span></span></span></code></pre></div><p>對 <em>大 JSONB + 高頻 update</em> 場景、寫吞吐受限。跟 MongoDB <code>$set</code> operator 對應 <em>partial document update</em> 不同。</p>
<p>修法：</p>
<ul>
<li>對 <em>high-update nested key</em> 拆獨立 column</li>
<li>Application 層 batch update（攢一批一次 update）</li>
<li>接受 PG JSONB <em>是 immutable-replace</em> 心智模型、不是 <em>mutable in-place</em></li>
</ul>
<h3 id="3-index-選錯-op-class---query-走-full-scan">3. Index 選錯 op class — <code>?</code> query 走 full scan</h3>
<p>對 <code>jsonb_path_ops</code> index、<code>?</code> key existence query 走 <em>full scan</em>（不用 index）。Application 看 query 慢、查 EXPLAIN 才發現 index 沒用。</p>
<p>修法：</p>
<ul>
<li>設計階段確認 <em>application query pattern</em>：只用 <code>@&gt;</code> 還是會用 <code>?</code></li>
<li>多 query pattern → <code>jsonb_ops</code>（預設）</li>
<li>純 containment → <code>jsonb_path_ops</code>（省 index size）</li>
<li>不確定先用預設、production 觀察後再優化</li>
</ul>
<h3 id="4-jsonb_path_query-跟-jsonb_path_exists-行為差">4. <code>jsonb_path_query</code> 跟 <code>jsonb_path_exists</code> 行為差</h3>
<ul>
<li><code>jsonb_path_query(metadata, '$.variants[*].price')</code> — 展開、每個 match return 一 row</li>
<li><code>jsonb_path_exists(metadata, '$.variants[*]')</code> — return boolean（true if any match）</li>
</ul>
<p>Application 想要「過濾 row」用前者寫成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 錯：返多 row 給每個 product、結果 row count 暴增
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">jsonb_path_query</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*].price&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span></span></span></code></pre></div><p>應該：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 對：只過濾 product
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">jsonb_path_exists</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*] ? (@.price &gt; 100)&#39;</span><span class="p">);</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>區分 <em>exists 過濾 row</em> vs <em>query 展開 row</em></li>
<li>過濾用 <code>jsonb_path_exists</code> 或 <code>@&gt;</code> operator</li>
<li>展開用 <code>jsonb_path_query</code> + 配合 <code>LATERAL</code> 或 subquery</li>
</ul>
<h3 id="5-partial-index-條件不對齊-query">5. Partial index 條件不對齊 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_active_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Application query 但 status 沒 explicit
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- → 不用 partial index（planner 不知道 status=&#39;active&#39; 條件）</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>
<p>Application query <em>必須包含 partial index 的 WHERE 條件</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;active&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>確認 planner 用 partial index：<code>EXPLAIN</code> 看 <code>Index Scan using idx_active_metadata</code></p>
</li>
<li>
<p>不對齊 query pattern 的 partial index = waste</p>
</li>
</ul>
<h2 id="何時用-jsonb-vs-拆-column">何時用 JSONB vs 拆 column</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不規則 schema（user-generated metadata / customization）</td>
          <td>JSONB</td>
      </tr>
      <tr>
          <td>半結構化 + 5-10 個常 query key</td>
          <td>JSONB + GIN partial index</td>
      </tr>
      <tr>
          <td>規則 schema、column 數量穩定</td>
          <td>拆 column（更快 / index 易）</td>
      </tr>
      <tr>
          <td>Nested 結構 + 經常需要展開 query</td>
          <td>JSONB + jsonb_path_query</td>
      </tr>
      <tr>
          <td>大 document（&gt; 1 KB）+ 高頻 update</td>
          <td>拆 column 或 separate table</td>
      </tr>
      <tr>
          <td>完全 schemaless workload</td>
          <td>考慮 MongoDB 而非 PG</td>
      </tr>
  </tbody>
</table>
<p>JSONB 是 <em>PG 適合 semi-structured data</em> 的工具、不是 <em>MongoDB 替代品</em>。對 <em>主要結構化 + 少量 JSON</em> 場景 JSONB 完美；對 <em>主要 JSON / 複雜 nested aggregation</em> 場景 MongoDB 仍是專業選擇。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>JSONB query 的 planner 行為：</p>
<ul>
<li><code>@&gt;</code> containment 對 jsonb_ops / jsonb_path_ops 都用 GIN</li>
<li><code>?</code> 只對 jsonb_ops 用 GIN</li>
<li>jsonb_path_exists 用 <em>functional index</em>（不是 GIN）</li>
<li>看 EXPLAIN 確認用對 index、詳見 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a></li>
</ul>
<h3 id="跟-sql-features-baseline">跟 SQL Features Baseline</h3>
<p>JSONB 是 PG 結構性領先特性之一、詳見 <a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">SQL Features Baseline</a>。</p>
<h3 id="跟-mvcc--lock-model">跟 MVCC + Lock Model</h3>
<p>JSONB UPDATE 整個 column 重寫、每次 update 創新 tuple、跟 row update 相同 MVCC behavior。詳見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>。</p>
<h3 id="跟-mysql-json_table">跟 MySQL JSON_TABLE</h3>
<p>MySQL 8.0 JSON_TABLE 跟 PG jsonpath 類似（都 SQL standard）、但 <em>index 機制</em> 完全不同：</p>
<ul>
<li>PG：JSONB + GIN index over 整個 column</li>
<li>MySQL：JSON column + generated column + index over generated</li>
</ul>
<p>PG JSONB GIN 是 <em>結構性領先</em>、MySQL 短期內難對應。詳見 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<ul>
<li><code>pg_column_size(metadata)</code> — 每 row JSONB size 分布</li>
<li><code>pg_relation_size('idx_name')</code> — JSONB GIN index 大小</li>
<li><code>pg_stat_user_indexes.idx_scan</code> — JSONB index 使用次數</li>
<li>TOAST table size：<code>SELECT pg_relation_size(reltoastrelid) FROM pg_class WHERE relname='products'</code></li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">PG SQL Features Baseline</a>（JSONB 是 PG 結構領先之一）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（JSONB index 用對）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PG MVCC + Lock Model</a>（JSONB update 跟 MVCC）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>（JSON_TABLE vs JSONB 對比）</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a>（純 document workload 替代）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/functions-json.html">PG JSON Functions</a> / <a href="https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING">JSONB Indexing</a></li>
</ul>
]]></content:encoded></item><item><title>3.C25 Indeed：Delay queue + DLQ 三層 escalation</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/</guid><description>&lt;p>這個案例的核心責任是說明 retry 策略要跟 queue 拓樸結合設計，分層延遲 + &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 的三層 escalation 能避免 head-of-line blocking。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Indeed 是全球最大的求職搜尋引擎之一，每天處理 35M+ 筆職缺資料的索引、更新與推送。職缺資料從雇主端進入系統後，需要經過解析、標準化、索引、推送到搜尋引擎等多個處理步驟，每個步驟由 RabbitMQ 串接的 consumer 處理。&lt;/p>
&lt;p>這個規模下，任何一個處理步驟的暫時失敗（downstream service timeout、資料格式異常、外部 API rate limit）都會產生需要 retry 的訊息。每天有數十萬筆訊息需要至少一次 retry。&lt;/p>
&lt;h2 id="技術挑戰head-of-line-blocking">技術挑戰：Head-of-line blocking&lt;/h2>
&lt;p>Indeed 原本的 retry 策略是 consumer 處理失敗時把訊息 requeue（&lt;code>basic.nack&lt;/code> with &lt;code>requeue=true&lt;/code>）。RabbitMQ 的 requeue 行為是把訊息放回 queue 的 head — 下一次 consumer 拿到的還是這條失敗的訊息。&lt;/p>
&lt;p>當一條訊息因為 downstream timeout 反覆失敗時，它會持續佔住 queue head，阻塞後面所有等待處理的訊息。單一 consumer 的時間被一條失敗訊息反覆消耗，其他正常的訊息延遲累積。在 35M+ 筆/天的吞吐量下，一條 head-of-line blocking 訊息就能讓整個 pipeline 的 processing lag 從秒級升到分鐘級。&lt;/p>
&lt;p>這個問題的根源是 retry 策略跟 queue 拓樸耦合在一起 — requeue 把 retry 決策留在同一個 queue 裡，讓失敗訊息跟正常訊息搶同一條通道。&lt;/p>
&lt;h2 id="解法三層-escalation">解法：三層 escalation&lt;/h2>
&lt;p>Indeed 設計了一個三層 escalation 模型，把失敗訊息依嚴重程度逐層隔離：&lt;/p>
&lt;h3 id="第一層immediate-retry同-queue">第一層：Immediate retry（同 queue）&lt;/h3>
&lt;p>Consumer 處理失敗時，先在 client 端做短暫 backoff（數百毫秒到數秒），然後 ack 原訊息、重新 publish 到同一個 queue 的 tail（而非 requeue 到 head）。&lt;/p>
&lt;p>這層處理的是暫態錯誤 — downstream 偶發的 500、短暫的 network hiccup。多數訊息在第一層就能恢復。重新 publish 到 tail 確保失敗訊息排在正常訊息後面，不阻塞其他訊息。&lt;/p>
&lt;h3 id="第二層delay-queue">第二層：Delay queue&lt;/h3>
&lt;p>第一層 retry N 次仍然失敗的訊息，透過 RabbitMQ 的 Dead Letter Exchange（DLX）路由到 delay queue。Delay queue 用 &lt;code>x-message-ttl&lt;/code> 設定延遲時間（例如 30 秒、1 分鐘、5 分鐘），TTL 到期後訊息透過另一個 DLX 路由回原始 queue 的 tail。&lt;/p>
&lt;p>Indeed 用多個不同 TTL 的 delay queue 實作 exponential backoff — 第一次進 delay 等 30 秒、第二次等 1 分鐘、第三次等 5 分鐘。這個做法利用 RabbitMQ 原生的 DLX + TTL 機制，不需要額外的 scheduler 或 cron job。&lt;/p>
&lt;p>這層處理的是持續性錯誤 — downstream 在做 deployment、外部 API 在做 maintenance。延遲重試讓 downstream 有時間恢復，同時失敗訊息完全離開主 queue、不影響正常處理。&lt;/p>
&lt;h3 id="第三層dead-letter-queue">第三層：Dead Letter Queue&lt;/h3>
&lt;p>Delay queue retry M 次後仍然失敗的訊息進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a>。DLQ 中的訊息不再自動重試，需要人工審視或批次 replay。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 retry 策略要跟 queue 拓樸結合設計，分層延遲 + <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 的三層 escalation 能避免 head-of-line blocking。</p>
<h2 id="業務背景">業務背景</h2>
<p>Indeed 是全球最大的求職搜尋引擎之一，每天處理 35M+ 筆職缺資料的索引、更新與推送。職缺資料從雇主端進入系統後，需要經過解析、標準化、索引、推送到搜尋引擎等多個處理步驟，每個步驟由 RabbitMQ 串接的 consumer 處理。</p>
<p>這個規模下，任何一個處理步驟的暫時失敗（downstream service timeout、資料格式異常、外部 API rate limit）都會產生需要 retry 的訊息。每天有數十萬筆訊息需要至少一次 retry。</p>
<h2 id="技術挑戰head-of-line-blocking">技術挑戰：Head-of-line blocking</h2>
<p>Indeed 原本的 retry 策略是 consumer 處理失敗時把訊息 requeue（<code>basic.nack</code> with <code>requeue=true</code>）。RabbitMQ 的 requeue 行為是把訊息放回 queue 的 head — 下一次 consumer 拿到的還是這條失敗的訊息。</p>
<p>當一條訊息因為 downstream timeout 反覆失敗時，它會持續佔住 queue head，阻塞後面所有等待處理的訊息。單一 consumer 的時間被一條失敗訊息反覆消耗，其他正常的訊息延遲累積。在 35M+ 筆/天的吞吐量下，一條 head-of-line blocking 訊息就能讓整個 pipeline 的 processing lag 從秒級升到分鐘級。</p>
<p>這個問題的根源是 retry 策略跟 queue 拓樸耦合在一起 — requeue 把 retry 決策留在同一個 queue 裡，讓失敗訊息跟正常訊息搶同一條通道。</p>
<h2 id="解法三層-escalation">解法：三層 escalation</h2>
<p>Indeed 設計了一個三層 escalation 模型，把失敗訊息依嚴重程度逐層隔離：</p>
<h3 id="第一層immediate-retry同-queue">第一層：Immediate retry（同 queue）</h3>
<p>Consumer 處理失敗時，先在 client 端做短暫 backoff（數百毫秒到數秒），然後 ack 原訊息、重新 publish 到同一個 queue 的 tail（而非 requeue 到 head）。</p>
<p>這層處理的是暫態錯誤 — downstream 偶發的 500、短暫的 network hiccup。多數訊息在第一層就能恢復。重新 publish 到 tail 確保失敗訊息排在正常訊息後面，不阻塞其他訊息。</p>
<h3 id="第二層delay-queue">第二層：Delay queue</h3>
<p>第一層 retry N 次仍然失敗的訊息，透過 RabbitMQ 的 Dead Letter Exchange（DLX）路由到 delay queue。Delay queue 用 <code>x-message-ttl</code> 設定延遲時間（例如 30 秒、1 分鐘、5 分鐘），TTL 到期後訊息透過另一個 DLX 路由回原始 queue 的 tail。</p>
<p>Indeed 用多個不同 TTL 的 delay queue 實作 exponential backoff — 第一次進 delay 等 30 秒、第二次等 1 分鐘、第三次等 5 分鐘。這個做法利用 RabbitMQ 原生的 DLX + TTL 機制，不需要額外的 scheduler 或 cron job。</p>
<p>這層處理的是持續性錯誤 — downstream 在做 deployment、外部 API 在做 maintenance。延遲重試讓 downstream 有時間恢復，同時失敗訊息完全離開主 queue、不影響正常處理。</p>
<h3 id="第三層dead-letter-queue">第三層：Dead Letter Queue</h3>
<p>Delay queue retry M 次後仍然失敗的訊息進入 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a>。DLQ 中的訊息不再自動重試，需要人工審視或批次 replay。</p>
<p>DLQ 的價值是把「目前無法處理」的訊息安全保存，不讓它們無限消耗 retry 資源。Indeed 的維運團隊定期檢查 DLQ 中的訊息 — 按 error type 分群、判斷是 bug（需要修 code 再 replay）還是資料問題（需要修正資料再 replay）。</p>
<h2 id="取捨">取捨</h2>
<p><strong>犧牲的是 delivery order</strong>。訊息從 delay queue 回到主 queue tail 時，已經不在原始的位置。對 Indeed 的職缺處理來說，order 不影響正確性 — 職缺更新是 idempotent 的，最終狀態正確即可。對 order-sensitive 的場景，這個模型需要額外的 ordering 機制。</p>
<p><strong>增加的是拓樸複雜度</strong>。三層 escalation 涉及主 queue + 多個 delay queue + DLQ + 多個 DLX 的 binding。RabbitMQ 的 exchange / queue / binding 組合需要明確規劃跟文件化，否則維運時搞不清楚訊息的路由路徑。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>：DLX + TTL 是 RabbitMQ 原生的 durable 機制</li>
<li><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a>：retry 策略跟 consumer 的 ack/nack 行為</li>
<li><a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ retry escalation</a>：DLX 配置的實作細節</li>
<li><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6 Uber Kafka 平台</a>：Kafka 生態的 retry topic 跟 DLQ 設計比較</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>以下訊號出現時，應該回讀本案例：</p>
<ul>
<li>Consumer 的 processing lag 在特定時段突然升高、但訊息產生速率沒變</li>
<li>同一條訊息的 retry 佔據 consumer 的大部分處理時間</li>
<li>Requeue 後訊息立刻又被同一個 consumer 取到、進入 retry 迴圈</li>
<li>DLQ 中的訊息堆積、沒有定期審視跟 replay 的機制</li>
<li>Retry 策略只有 client 端 backoff、沒有 queue 拓樸層面的隔離</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.indeedblog.com/blog/2017/06/delaying-messages/">Delaying Messages with RabbitMQ at Indeed</a></li>
<li><a href="https://engineering.indeedblog.com/talks/get-job-35-million-times-day-using-rabbitmq/">Get a Job 35 Million Times a Day Using RabbitMQ (talk)</a></li>
</ul>
]]></content:encoded></item><item><title>Cilium Tetragon</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cilium-tetragon/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cilium-tetragon/</guid><description>&lt;p>Tetragon 是 Cilium 旗下的 &lt;em>eBPF-based runtime security + enforcement&lt;/em> 元件、Isovalent 主導、2024 年起在 CNCF 屬 Incubating 階段。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &amp;#43; Falcosidekick &amp;#43; Talon、K8s container runtime 偵測為主">Falco&lt;/a> 的核心差異在於 &lt;em>偵測 vs 偵測 + 可 enforce&lt;/em> — Falco 預設 alert-only、Tetragon 設計支援 &lt;em>kernel-level inline enforcement&lt;/em>（直接 kill process、override syscall return value）；對 K8s heavy + 已用 Cilium CNI 的環境、Tetragon 把 &lt;em>network policy + process policy&lt;/em> 收進同一個 eBPF 生態。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Tetragon 的核心定位是 &lt;em>eBPF 為基底的 runtime observability + enforcement&lt;/em>、TracingPolicy CRD 是 first-class concept — 一份 YAML 同時描述 &lt;em>要觀察什麼 syscall / kprobe / tracepoint&lt;/em> 跟 &lt;em>觀察到後要不要 enforce&lt;/em>。底層 hook 點包括 syscall entry/exit、kprobe（任意 kernel function）、tracepoint（穩定 kernel event）、uprobe（user-space function），enforcement action 包括 &lt;code>Sigkill&lt;/code>（kill process）、&lt;code>Override&lt;/code>（override syscall return value）、&lt;code>NotifyEnforcer&lt;/code>、&lt;code>Post&lt;/code>（送 event 出 plane）。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &amp;#43; Falcosidekick &amp;#43; Talon、K8s container runtime 偵測為主">Falco&lt;/a> 比、Falco rule 用 Sysdig filter syntax、Tetragon 用 K8s CRD + JSON schema、對 K8s native 模型更貼近；Falco 主走 &lt;em>alert&lt;/em>、Tetragon 主走 &lt;em>alert + enforce&lt;/em>；Falco 對非 K8s VM-heavy 場景更 mature。跟 &lt;em>Datadog Cloud Workload Security&lt;/em> 比、Datadog 是 SaaS-only + per-host 計費、Tetragon 是 OSS Apache 2.0 + 自管 + Isovalent Enterprise 付費版可選。跟 &lt;em>Prisma Cloud Defender&lt;/em> 比、Prisma 是 CSPM/CWPP 一體化平台、Tetragon 專注 runtime + 跟 Cilium L3-L7 network policy 同 plane。&lt;/p>
&lt;p>關鍵張力：&lt;em>eBPF inline enforcement 的爆炸半徑&lt;/em> ↔ &lt;em>偵測即時性&lt;/em>。在 kernel-level 直接 kill process 比 userspace agent 更難 bypass、但 TracingPolicy 寫錯（match 太寬）可能誤殺合法 workload、且回退路徑只能改 CRD 再 reload。要看清楚自己 &lt;em>能不能承擔 enforcement 規則錯誤的 blast radius&lt;/em>、再決定哪些 policy 進 enforce、哪些只 observe。&lt;/p></description><content:encoded><![CDATA[<p>Tetragon 是 Cilium 旗下的 <em>eBPF-based runtime security + enforcement</em> 元件、Isovalent 主導、2024 年起在 CNCF 屬 Incubating 階段。跟 <a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a> 的核心差異在於 <em>偵測 vs 偵測 + 可 enforce</em> — Falco 預設 alert-only、Tetragon 設計支援 <em>kernel-level inline enforcement</em>（直接 kill process、override syscall return value）；對 K8s heavy + 已用 Cilium CNI 的環境、Tetragon 把 <em>network policy + process policy</em> 收進同一個 eBPF 生態。</p>
<h2 id="服務定位">服務定位</h2>
<p>Tetragon 的核心定位是 <em>eBPF 為基底的 runtime observability + enforcement</em>、TracingPolicy CRD 是 first-class concept — 一份 YAML 同時描述 <em>要觀察什麼 syscall / kprobe / tracepoint</em> 跟 <em>觀察到後要不要 enforce</em>。底層 hook 點包括 syscall entry/exit、kprobe（任意 kernel function）、tracepoint（穩定 kernel event）、uprobe（user-space function），enforcement action 包括 <code>Sigkill</code>（kill process）、<code>Override</code>（override syscall return value）、<code>NotifyEnforcer</code>、<code>Post</code>（送 event 出 plane）。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a> 比、Falco rule 用 Sysdig filter syntax、Tetragon 用 K8s CRD + JSON schema、對 K8s native 模型更貼近；Falco 主走 <em>alert</em>、Tetragon 主走 <em>alert + enforce</em>；Falco 對非 K8s VM-heavy 場景更 mature。跟 <em>Datadog Cloud Workload Security</em> 比、Datadog 是 SaaS-only + per-host 計費、Tetragon 是 OSS Apache 2.0 + 自管 + Isovalent Enterprise 付費版可選。跟 <em>Prisma Cloud Defender</em> 比、Prisma 是 CSPM/CWPP 一體化平台、Tetragon 專注 runtime + 跟 Cilium L3-L7 network policy 同 plane。</p>
<p>關鍵張力：<em>eBPF inline enforcement 的爆炸半徑</em> ↔ <em>偵測即時性</em>。在 kernel-level 直接 kill process 比 userspace agent 更難 bypass、但 TracingPolicy 寫錯（match 太寬）可能誤殺合法 workload、且回退路徑只能改 CRD 再 reload。要看清楚自己 <em>能不能承擔 enforcement 規則錯誤的 blast radius</em>、再決定哪些 policy 進 enforce、哪些只 observe。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Tetragon 在 K8s runtime stack 中承擔哪一段（process visibility / file access / network syscall / enforcement）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a> for VM-heavy、SIEM for log aggregation）</li>
<li>TracingPolicy 的 ownership 設計（誰寫 CRD、enforcement action 誰簽核、staging vs production rollout）</li>
<li><em>Observe</em> vs <em>Enforce</em> 的階段化決策、什麼樣的 policy 適合 inline kill、什麼樣的應該停在 alert</li>
<li>何時用 Tetragon、何時走 Falco / Datadog CWS / Prisma Defender 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Tetragon deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>TracingPolicy 治理</strong>：CRD 是否走 Git + PR review、enforcement action（Sigkill / Override）是否需額外簽核、staging cluster 是否先跑 24-48hr 觀察 false positive 才 promote production</li>
<li><strong>跟 Cilium 整合深度</strong>：Hubble flow + Tetragon process event 是否同 plane export、Pod identity 是否在 process event 自動 enrich、跟 Cilium NetworkPolicy 是否雙層 enforcement 設計</li>
<li><strong>Enforcement coverage 分層</strong>：哪些 policy 處於 observe-only（log JNDI lookup / setuid abuse / unexpected outbound）、哪些升到 enforce（kill known exploit pattern）、升級條件是什麼</li>
<li><strong>Event export pipeline</strong>：Tetragon event 是否進 SIEM（OpenTelemetry / JSON log → Splunk / Elastic）、是否跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界一致</li>
</ul>
<p>四件事任一缺失、就是 runtime security 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>TracingPolicy CRD</strong>：Tetragon 的 first-class concept、一份 YAML 描述 hook 點 + match selector + enforcement action。Hook 點包含 <em>syscall</em>（最穩定但 surface 廣）、<em>kprobe</em>（任意 kernel function、版本相依）、<em>tracepoint</em>（穩定 kernel event、首選）、<em>uprobe</em>（user-space function、低層用）。Match selector 支援 K8s namespace / pod label / container image、process credentials（UID / GID / capabilities）、parent process。Production rule 用 <em>pod label selector + 具體 syscall name + 額外 process credentials 條件</em>、避免 cluster-wide 寬鬆 match 誤殺。</p>
<p><strong>kprobe / tracepoint / syscall hook 的選擇</strong>：tracepoint 是 kernel 公開穩定介面、跨版本不變、首選；kprobe 可 hook 任意 kernel function 但跟 kernel build 緊綁、kernel upgrade 後可能要重寫；raw syscall 適合 audit 整類 syscall（如全部 <code>execve</code>）但量大、需要 in-kernel filter 控成本。</p>
<p><strong>Process credentials tracking</strong>：Tetragon 從 process exec 開始 track UID / GID / capabilities / namespace、偵測 <em>privilege escalation</em>（setuid abuse、capabilities drift、container escape）是 first-class use case。跟 audit log 比、credentials drift 是 <em>狀態變遷</em>、不是單一事件、更能 surface lateral movement 早期訊號（process 開始時 UID 1000、跑到一半變 0 是異常）。</p>
<p><strong>Pod identity correlation</strong>：Tetragon 在 K8s 環境會自動把 process event enrich K8s metadata（namespace / pod name / container image / service account）、不用後處理 join；event schema 跟 Hubble flow 同根、可在 Hubble UI 看 <em>某 Pod 的 network flow + process event</em> 同 timeline。</p>
<p><strong>跟 Cilium NetworkPolicy 雙層 enforcement</strong>：Cilium 控 <em>network ingress / egress / L7 HTTP</em>、Tetragon 控 <em>process / syscall / file access</em>。雙層設計的意義是 — network layer 擋不住的（如 process 內部 lateral movement、container escape syscall）由 process layer 補上；process layer 漏的（如合法 process 突然 outbound 異常 destination）由 network layer 補上。對 supply chain 攻擊特別有效、攻擊鏈通常跨 <em>malicious process spawn + outbound C2</em>。</p>
<p><strong>Event export 跟 SIEM 整合</strong>：Tetragon event 預設走 JSON log 到 stdout、可走 OpenTelemetry exporter 進 collector pipeline、再 fanout 到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>。在 SIEM 端做跨來源 correlation（process event + IdP audit + cloud control plane）是 production 標配、不可只看 Tetragon 自家視圖。</p>
<p><strong>Observe → Enforce 階段化</strong>：TracingPolicy 通常 <em>先進 observe-only</em>、跑 1-2 週收 baseline、確認 false positive 可控、再加 enforcement action 進 staging cluster、staging 觀察 24-48hr 才 promote production。對應 <a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a> 的章節原則 — runtime enforcement 不是 console 直改、是 detection content lifecycle。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Cilium Tetragon</th>
          <th>Falco</th>
          <th>Datadog CWS</th>
          <th>Prisma Cloud Defender</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偵測技術</td>
          <td>eBPF（kprobe / tracepoint / syscall / uprobe）</td>
          <td>eBPF + kernel module 兩種 driver</td>
          <td>eBPF agent</td>
          <td>eBPF + kernel module</td>
      </tr>
      <tr>
          <td>Enforcement</td>
          <td>內建（Sigkill / Override syscall return）</td>
          <td>預設 alert-only（plugin 可擴 response）</td>
          <td>自動 response（kill / isolate、SaaS 控）</td>
          <td>內建（block process / file / network）</td>
      </tr>
      <tr>
          <td>規則語言</td>
          <td>K8s CRD（TracingPolicy YAML）</td>
          <td>Sysdig filter syntax（YAML rule）</td>
          <td>Datadog Security Rules（JSON / UI）</td>
          <td>Prisma Runtime Rules（UI / JSON）</td>
      </tr>
      <tr>
          <td>計費 / 授權</td>
          <td>OSS Apache 2.0、Isovalent Enterprise 付費</td>
          <td>OSS Apache 2.0、Sysdig Secure 付費</td>
          <td>SaaS per-host</td>
          <td>商業 per-defender</td>
      </tr>
      <tr>
          <td>K8s native</td>
          <td>強 — Pod identity 自動 enrich、跟 Cilium 同源</td>
          <td>中 — K8s metadata 需 audit endpoint</td>
          <td>強 — Datadog Agent 已熟</td>
          <td>強 — Prisma 平台一體</td>
      </tr>
      <tr>
          <td>Network policy</td>
          <td>跟 Cilium L3-L7 雙層（同 plane）</td>
          <td>無 — 純 process / file</td>
          <td>無 — 跟 Datadog Network 分離</td>
          <td>內建 micro-segmentation</td>
      </tr>
      <tr>
          <td>VM / 非 K8s</td>
          <td>弱 — Linux only、K8s-first</td>
          <td>強 — VM / bare metal mature</td>
          <td>中 — 跨環境同 agent</td>
          <td>強 — VM / serverless / container 全覆蓋</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted DaemonSet（K8s）</td>
          <td>Self-hosted DaemonSet / VM agent</td>
          <td>SaaS</td>
          <td>商業 self-hosted + SaaS console</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>K8s heavy + 已用 Cilium + 要 inline enforce</td>
          <td>VM-heavy / K8s 混合、需要 mature alert ecosystem</td>
          <td>Datadog 已用、要 unified observability</td>
          <td>多雲 CSPM/CWPP 一體化、合規驅動</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — TracingPolicy CRD 跨 cluster 可移植</td>
          <td>中 — Falco rule 跟 Sigma 可互轉</td>
          <td>高 — SaaS lock-in</td>
          <td>高 — 商業平台 lock-in</td>
      </tr>
  </tbody>
</table>
<p>選 Tetragon 的核心訴求：<em>K8s heavy + 已用 Cilium CNI + 想要 kernel-level inline enforcement + OSS 免授權成本</em>、且有 SRE / security team 能維護 TracingPolicy CRD lifecycle。VM-heavy 或 K8s 但用其他 CNI 走 Falco 更划算。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Inline enforcement 的 blast radius 設計</strong>：<code>Sigkill</code> 直接 kill 觸發 process、<code>Override</code> 改寫 syscall return value（讓 process 以為成功但實際沒做）— 兩者都在 kernel-level、攻擊者很難 bypass、但寫錯規則的 blast radius 是 <em>整個 cluster 內 match 到的 process 全死</em>。實務治理：enforcement action 規則進 GitOps、PR 需 security + SRE 雙簽、staging cluster 跑 namespace-scoped 規則先驗證、production rollout 走 canary namespace 再擴散。</p>
<p><strong>Process credentials drift detection</strong>：track UID / GID / capabilities 變遷、偵測 setuid abuse（process 從 uid 1000 變 0）、capabilities 突然新增（特別是 CAP_SYS_ADMIN / CAP_NET_ADMIN）。對 lateral movement 早期警報是 first-class signal — 攻擊者拿到初始 access 後通常要 escalate privilege、credentials drift 是必經訊號。配對 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a> 的 lesson：簽章驗證通過但 runtime 行為異常需 <em>runtime credentials + process behavior</em> 雙重 baseline。</p>
<p><strong>跟 Cilium L3-L7 雙層 enforcement</strong>：典型 supply chain 攻擊鏈 — <em>malicious dependency loaded → process spawn → C2 outbound</em>、network layer 擋 outbound（Cilium NetworkPolicy 限制 egress destination）、process layer 擋 process（Tetragon KillerAction kill 異常 spawn）。雙層任一通則攻擊鏈中斷。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a> 的 case shape。</p>
<p><strong>跟 SBOM / image signing 整合 baseline</strong>：Tetragon 偵測 runtime 行為偏離 baseline、SBOM / image signing 控 build-time 信任、合在一起是 <em>trusted artifact + verified runtime behavior</em> 雙重保障。runtime 行為 baseline 通常從 SBOM 列出的合法 process / syscall set 出發、deviation 進 alert。</p>
<p><strong>Isovalent Enterprise</strong>：商業版加值在 multi-cluster management、policy 集中下發、support SLA、跟 Isovalent Hubble Enterprise / Cilium Service Mesh Enterprise 整合。OSS 版本核心功能完整、Enterprise 主要解 <em>多 cluster 大規模管理</em> 跟 <em>企業 support</em>、不是 feature gating。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>TracingPolicy 誤殺合法 workload</strong>：match selector 太寬、cluster-wide 沒加 namespace / pod label 條件 — 改 namespace-scoped + 加 process credentials 額外條件、staging 跑 48hr 再 promote</li>
<li><strong>kprobe rule kernel upgrade 後壞</strong>：hook 的 kernel function 改名或 signature 變 — 改用 tracepoint（穩定介面）、kprobe 進 staging 版本相依測試</li>
<li><strong>Event volume 爆炸 / SIEM ingestion cost 飆</strong>：raw syscall hook 沒做 in-kernel filter、所有 <code>execve</code> 都進 event — 加 in-kernel filter（按 pod label / process name），讓 filter 在 eBPF 端做、不要事後 drop</li>
<li><strong>Inline enforcement 規則錯誤 blast radius 太大</strong>：production 直接上 <code>Sigkill</code> 沒走 staging — enforcement action 規則一律先 observe-only 1 週、staging cluster 24-48hr、canary namespace、才 production</li>
<li><strong>跟 Cilium NetworkPolicy 重疊或衝突</strong>：同一個 attack pattern 被 network + process 同時阻擋、log 重複、誤判 — 設計時雙層各管 <em>互補面</em>（network 管 destination、process 管 process spawn）、不重複管同一面</li>
<li><strong>non-K8s workload 進不來</strong>：Tetragon DaemonSet 只在 K8s 跑、VM / bare metal 不支援 — VM-heavy 環境改走 <a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a>、K8s + VM 混合走雙 stack</li>
<li><strong>Pod identity enrich 不全</strong>：某些 process event 缺 namespace / pod name — 通常是 process 在 pod sandbox 啟動前 spawn、或 short-lived process 太快結束、調 Tetragon 的 process cache lifetime + K8s API server 連線健康</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VM-heavy / 非 K8s 為主</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a></td>
      </tr>
      <tr>
          <td>Datadog observability 已用</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>（Cloud Workload Security）</td>
      </tr>
      <tr>
          <td>多雲 CSPM/CWPP 一體化、合規驅動</td>
          <td>Prisma Cloud Defender（商業）</td>
      </tr>
      <tr>
          <td>SIEM 偵測為主、不需 inline kill</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>Endpoint EDR（user laptop / VDI）</td>
          <td>CrowdStrike Falcon / Microsoft Defender for Endpoint</td>
      </tr>
      <tr>
          <td>偵測覆蓋率治理</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>TracingPolicy CRD 完整欄位 reference 跟 kprobe / tracepoint 寫法 cookbook</li>
<li>Cilium NetworkPolicy 寫法（屬 network 治理、跨章節）</li>
<li>eBPF kernel programming 內部原理跟 verifier 限制</li>
<li>Isovalent Enterprise 跟 Cilium Service Mesh 商業整合細節</li>
<li>Hubble UI 操作（屬 observability 視角、跨章節）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Tetragon 在 07 案例庫沒有直接 vendor-level 事件、但所有 runtime detection + supply chain case 都是 eBPF inline enforcement 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Tetragon 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell CVE-2021-44228</a></td>
          <td>TracingPolicy 可 hook JNDI lookup 相關 syscall、配 <code>Sigkill</code> 直接 kill exploit process、比 userspace WAF 更難 bypass</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>process credentials drift detection 對 lateral movement 早期警報、簽章驗證通過但 runtime 行為異常需 runtime baseline 補位</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023 Desktop App Supply Chain</a></td>
          <td>偵測 desktop app 異常 outbound、Tetragon 抓 process + Cilium NetworkPolicy 同層擋 destination、雙層 enforcement 中斷攻擊鏈</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle (section)</a></td>
          <td>TracingPolicy CRD 走 GitOps + PR review + staging tune + canary rollout、inline enforcement 不可 console 直改</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/alert-fatigue-and-signal-quality/" data-link-title="7.B10 Alert Fatigue and Signal Quality" data-link-desc="建立告警疲勞治理方法，讓訊號品質、分級一致性與處置效率同步提升">Alert Fatigue and Signal Quality (section)</a></td>
          <td>observe-only 階段先收 baseline、in-kernel filter 控 event volume、enforcement 只升給高 confidence pattern、避免 alert / log 雙重 fatigue</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a>、<a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">Detection Engineering Lifecycle</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（Tetragon event 進 SIEM 做跨來源 correlation）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>（network edge 擋 + process 層補位）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（credentials drift 配 secret rotation）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（runtime alert → IR routing）、<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 observability</a>（Hubble + Tetragon event pipeline 共用）</li>
<li>官方：<a href="https://tetragon.io/">Tetragon Documentation</a>、<a href="https://cilium.io/">Cilium Project</a></li>
</ul>
]]></content:encoded></item><item><title>9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/</guid><description>&lt;p>這個案例的核心責任是說明「ML feature store 的延遲敏感層」工程選型。即時推薦（首頁 carousel、播放後下一支）需要在 100ms 內生成、ML inference 之前的 feature lookup 通常吃 30-50ms — 把 lookup 壓到 10ms 以下、整個推薦延遲才有預算空間。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tubi 在 ElastiCache 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/elasticache/customers/">ElastiCache Customers&lt;/a>）：&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>工作負載&lt;/td>
 &lt;td>ML inference feature store&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>p99 延遲&lt;/td>
 &lt;td>&amp;lt; 10 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移路徑&lt;/td>
 &lt;td>ScyllaDB → ElastiCache for Redis&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務場景&lt;/td>
 &lt;td>串流推薦（free streaming service）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Tubi 案例揭露三個 ML feature store 容量設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>feature store 是 ML inference 的 critical path&lt;/strong>：每個推薦請求都要查 N 個 feature（user_profile、item_metadata、recent_interactions、similar_users 等）、每個 feature 查詢都吃 latency budget。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的多 stage budget 分解。&lt;/li>
&lt;li>&lt;strong>ScyllaDB → ElastiCache 是「持久 KV → 純 cache」的權衡&lt;/strong>：ScyllaDB 是 Cassandra-compatible 高吞吐 KV、提供 durability；ElastiCache 是 in-memory cache、可以 cache miss。Tubi 選 cache 是判斷「feature 可以重新計算」、durability 不必、純 in-memory 更快。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache vs durable store 選型。&lt;/li>
&lt;li>&lt;strong>p99 才是 ML 系統的容量門檻&lt;/strong>：ML 系統的 user-perceived latency 是 &lt;em>最後完成的 inference&lt;/em>、不是平均。p50 快沒用、p99 慢用戶就看到 loading spinner。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 latency percentile 分析、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase&lt;/a> 的長尾延遲議題同類。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>「sub-10ms p99」沒指明 &lt;em>p999 / p9999&lt;/em>。p9999 通常比 p99 高一個量級、會出現在實際 user-perceived 體驗。&lt;/li>
&lt;li>ElastiCache 的 sub-10ms 是 &lt;em>cache hit 路徑&lt;/em> — cache miss 路徑會回到 ScyllaDB 或重新計算、延遲可能 100ms+。容量規劃要考慮 cache hit rate 跟 miss recovery 兩條路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ML feature store 用「兩層 cache」設計&lt;/strong>：L1 是 in-process cache（最熱的 features）、L2 是 ElastiCache / Memcached（次熱）、L3 才是持久 store（ScyllaDB / DynamoDB / S3 + Parquet）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache hierarchy。&lt;/li>
&lt;li>&lt;strong>feature 可重算 → 用 cache、feature 必須持久 → 用 store&lt;/strong>：判斷依據是「重算成本」跟「資料一致性需求」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary&lt;/a>。&lt;/li>
&lt;li>&lt;strong>p99 / p999 反推單個 stage latency 上限&lt;/strong>：每個 stage（network、cache lookup、feature aggregation、model inference、response serialization）給一個 latency budget、總和等於整體 SLO。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a>、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase&lt;/a> 同樣的反推思維。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS ElastiCache for Redis / Valkey / MemoryDB、GCP Memorystore for Redis、Azure Cache for Redis 都可實作對等架構。專為 ML feature store 設計的還有 Feast / Tecton / Hopsworks 等開源 + 商業方案、底層常用 Redis-compatible store。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「ML feature store 的延遲敏感層」工程選型。即時推薦（首頁 carousel、播放後下一支）需要在 100ms 內生成、ML inference 之前的 feature lookup 通常吃 30-50ms — 把 lookup 壓到 10ms 以下、整個推薦延遲才有預算空間。</p>
<h2 id="觀察">觀察</h2>
<p>Tubi 在 ElastiCache 的關鍵敘述（引自 <a href="https://aws.amazon.com/elasticache/customers/">ElastiCache Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工作負載</td>
          <td>ML inference feature store</td>
      </tr>
      <tr>
          <td>p99 延遲</td>
          <td>&lt; 10 ms</td>
      </tr>
      <tr>
          <td>遷移路徑</td>
          <td>ScyllaDB → ElastiCache for Redis</td>
      </tr>
      <tr>
          <td>業務場景</td>
          <td>串流推薦（free streaming service）</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<p>Tubi 案例揭露三個 ML feature store 容量設計重點。</p>
<ol>
<li><strong>feature store 是 ML inference 的 critical path</strong>：每個推薦請求都要查 N 個 feature（user_profile、item_metadata、recent_interactions、similar_users 等）、每個 feature 查詢都吃 latency budget。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的多 stage budget 分解。</li>
<li><strong>ScyllaDB → ElastiCache 是「持久 KV → 純 cache」的權衡</strong>：ScyllaDB 是 Cassandra-compatible 高吞吐 KV、提供 durability；ElastiCache 是 in-memory cache、可以 cache miss。Tubi 選 cache 是判斷「feature 可以重新計算」、durability 不必、純 in-memory 更快。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache vs durable store 選型。</li>
<li><strong>p99 才是 ML 系統的容量門檻</strong>：ML 系統的 user-perceived latency 是 <em>最後完成的 inference</em>、不是平均。p50 快沒用、p99 慢用戶就看到 loading spinner。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 latency percentile 分析、跟 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> 的長尾延遲議題同類。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「sub-10ms p99」沒指明 <em>p999 / p9999</em>。p9999 通常比 p99 高一個量級、會出現在實際 user-perceived 體驗。</li>
<li>ElastiCache 的 sub-10ms 是 <em>cache hit 路徑</em> — cache miss 路徑會回到 ScyllaDB 或重新計算、延遲可能 100ms+。容量規劃要考慮 cache hit rate 跟 miss recovery 兩條路徑。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>ML feature store 用「兩層 cache」設計</strong>：L1 是 in-process cache（最熱的 features）、L2 是 ElastiCache / Memcached（次熱）、L3 才是持久 store（ScyllaDB / DynamoDB / S3 + Parquet）。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache hierarchy。</li>
<li><strong>feature 可重算 → 用 cache、feature 必須持久 → 用 store</strong>：判斷依據是「重算成本」跟「資料一致性需求」。對應 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a>。</li>
<li><strong>p99 / p999 反推單個 stage latency 上限</strong>：每個 stage（network、cache lookup、feature aggregation、model inference、response serialization）給一個 latency budget、總和等於整體 SLO。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a>、跟 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> 同樣的反推思維。</li>
</ol>
<p>跨平台等效：AWS ElastiCache for Redis / Valkey / MemoryDB、GCP Memorystore for Redis、Azure Cache for Redis 都可實作對等架構。專為 ML feature store 設計的還有 Feast / Tecton / Hopsworks 等開源 + 商業方案、底層常用 Redis-compatible store。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 ML feature store → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a></li>
<li>想做 p99 / p999 反推 → <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a></li>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a>（配對引擎）</li>
<li>想理解 cache hierarchy → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/elasticache/customers/">Amazon ElastiCache Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/build-an-ultra-low-latency-online-feature-store-for-real-time-inferencing-using-amazon-elasticache-for-redis/">Build an ultra-low latency online feature store for real-time inferencing using Amazon ElastiCache for Redis</a></li>
</ul>
]]></content:encoded></item><item><title>6.25 Provider Dependency Release Gate 實作示範</title><link>https://tarrragon.github.io/blog/backend/06-reliability/provider-dependency-release-gate/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/provider-dependency-release-gate/</guid><description>&lt;p>Provider dependency release gate 的核心責任是把第三方依賴風險轉成可驗證放行條件，避免變更在高不確定性下直接擴散。&lt;/p>
&lt;h2 id="服務路徑與風險模型">服務路徑與風險模型&lt;/h2>
&lt;p>示範路徑是 checkout API 切換 payment provider timeout/retry 設定。這類變更看起來只改 config，但會直接影響交易成功率、延遲與重試風暴。&lt;/p>
&lt;p>gate 應固定五欄：&lt;code>Gate decision&lt;/code>、&lt;code>Checks&lt;/code>、&lt;code>Stop condition&lt;/code>、&lt;code>Rollback window&lt;/code>、&lt;code>Owner&lt;/code>。欄位先成立，再討論工具落地。&lt;/p>
&lt;p>以 payment provider timeout 調整為例，五欄的具體內容：&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>Gate decision&lt;/td>
 &lt;td>proceed / hold / rollback — 每批 canary 結束時做一次判定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Checks&lt;/td>
 &lt;td>checkout success rate &amp;gt; 99.5%、provider timeout ratio &amp;lt; 2%、duplicate charge = 0、error budget remaining &amp;gt; 30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stop condition&lt;/td>
 &lt;td>error rate 超門檻、latency p99 超過基線 2 倍、provider timeout ratio &amp;gt; 5%，任一觸發即停止擴批&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback window&lt;/td>
 &lt;td>15 min — config-only 變更無 schema 衝突，超過 15 min 後交易資料可能依賴新設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>checkout team lead，負責每批 go/no-go 與 rollback 決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Checks 欄位的數值來自歷史 baseline，每次變更前從 production 最近 7 天取值。baseline 偏移超過 10% 時，先校準再啟動 canary。&lt;/p>
&lt;h2 id="實作步驟">實作步驟&lt;/h2>
&lt;ol>
&lt;li>定義放行前檢查：checkout 成功率、provider timeout 比率、duplicate charge 監控、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 餘量。&lt;/li>
&lt;li>設定 canary 節奏：1% -&amp;gt; 5% -&amp;gt; 25% -&amp;gt; 100%，每批觀察固定時間窗。&lt;/li>
&lt;li>為每批設定 stop condition：error rate、latency、provider timeout 任一超門檻即停止擴大。&lt;/li>
&lt;li>設定 rollback window：例如 15 分鐘內可無資料格式衝突地回退設定。&lt;/li>
&lt;li>把每批結果寫入 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h3 id="canary-節奏與觀察窗">Canary 節奏與觀察窗&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>批次&lt;/th>
 &lt;th>流量比例&lt;/th>
 &lt;th>觀察窗&lt;/th>
 &lt;th>Go/no-go 判斷依據&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>B1&lt;/td>
 &lt;td>1%&lt;/td>
 &lt;td>30 min&lt;/td>
 &lt;td>checks 全過、stop condition 未觸發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B2&lt;/td>
 &lt;td>5%&lt;/td>
 &lt;td>1 h&lt;/td>
 &lt;td>B1 指標持平、無 duplicate charge、無客訴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B3&lt;/td>
 &lt;td>25%&lt;/td>
 &lt;td>2 h&lt;/td>
 &lt;td>B2 指標持平、error budget 消耗速度未加快&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B4&lt;/td>
 &lt;td>100%&lt;/td>
 &lt;td>持續觀測&lt;/td>
 &lt;td>B3 指標持平、跨區結果一致，進入持續觀測而非一次性放行&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Payment 類變更的觀察窗比一般 config 變更長，原因有兩個。第一，交易確認有延遲 — provider 回傳 settlement 結果可能在數分鐘到數小時後，短觀察窗無法看到完整的交易結果分佈。第二，退款與爭議申請通常在交易後數小時甚至數天才出現，B3 階段需要持續追蹤退款率趨勢，確認新設定沒有引發 provider 層的異常判定。&lt;/p></description><content:encoded><![CDATA[<p>Provider dependency release gate 的核心責任是把第三方依賴風險轉成可驗證放行條件，避免變更在高不確定性下直接擴散。</p>
<h2 id="服務路徑與風險模型">服務路徑與風險模型</h2>
<p>示範路徑是 checkout API 切換 payment provider timeout/retry 設定。這類變更看起來只改 config，但會直接影響交易成功率、延遲與重試風暴。</p>
<p>gate 應固定五欄：<code>Gate decision</code>、<code>Checks</code>、<code>Stop condition</code>、<code>Rollback window</code>、<code>Owner</code>。欄位先成立，再討論工具落地。</p>
<p>以 payment provider timeout 調整為例，五欄的具體內容：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>範例值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gate decision</td>
          <td>proceed / hold / rollback — 每批 canary 結束時做一次判定</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>checkout success rate &gt; 99.5%、provider timeout ratio &lt; 2%、duplicate charge = 0、error budget remaining &gt; 30%</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>error rate 超門檻、latency p99 超過基線 2 倍、provider timeout ratio &gt; 5%，任一觸發即停止擴批</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>15 min — config-only 變更無 schema 衝突，超過 15 min 後交易資料可能依賴新設定</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>checkout team lead，負責每批 go/no-go 與 rollback 決策</td>
      </tr>
  </tbody>
</table>
<p>Checks 欄位的數值來自歷史 baseline，每次變更前從 production 最近 7 天取值。baseline 偏移超過 10% 時，先校準再啟動 canary。</p>
<h2 id="實作步驟">實作步驟</h2>
<ol>
<li>定義放行前檢查：checkout 成功率、provider timeout 比率、duplicate charge 監控、<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 餘量。</li>
<li>設定 canary 節奏：1% -&gt; 5% -&gt; 25% -&gt; 100%，每批觀察固定時間窗。</li>
<li>為每批設定 stop condition：error rate、latency、provider timeout 任一超門檻即停止擴大。</li>
<li>設定 rollback window：例如 15 分鐘內可無資料格式衝突地回退設定。</li>
<li>把每批結果寫入 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</li>
</ol>
<h3 id="canary-節奏與觀察窗">Canary 節奏與觀察窗</h3>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>流量比例</th>
          <th>觀察窗</th>
          <th>Go/no-go 判斷依據</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B1</td>
          <td>1%</td>
          <td>30 min</td>
          <td>checks 全過、stop condition 未觸發</td>
      </tr>
      <tr>
          <td>B2</td>
          <td>5%</td>
          <td>1 h</td>
          <td>B1 指標持平、無 duplicate charge、無客訴</td>
      </tr>
      <tr>
          <td>B3</td>
          <td>25%</td>
          <td>2 h</td>
          <td>B2 指標持平、error budget 消耗速度未加快</td>
      </tr>
      <tr>
          <td>B4</td>
          <td>100%</td>
          <td>持續觀測</td>
          <td>B3 指標持平、跨區結果一致，進入持續觀測而非一次性放行</td>
      </tr>
  </tbody>
</table>
<p>Payment 類變更的觀察窗比一般 config 變更長，原因有兩個。第一，交易確認有延遲 — provider 回傳 settlement 結果可能在數分鐘到數小時後，短觀察窗無法看到完整的交易結果分佈。第二，退款與爭議申請通常在交易後數小時甚至數天才出現，B3 階段需要持續追蹤退款率趨勢，確認新設定沒有引發 provider 層的異常判定。</p>
<h3 id="證據留存格式">證據留存格式</h3>
<p>每批 canary 結束時留存一筆結構化證據，供 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 調用。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>batch</td>
          <td>B1 / B2 / B3 / B4</td>
      </tr>
      <tr>
          <td>timestamp</td>
          <td>批次開始與結束時間</td>
      </tr>
      <tr>
          <td>traffic %</td>
          <td>該批實際流量比例</td>
      </tr>
      <tr>
          <td>metrics snapshot</td>
          <td>checkout success rate、latency p99、provider timeout ratio</td>
      </tr>
      <tr>
          <td>decision</td>
          <td>proceed / hold / rollback</td>
      </tr>
      <tr>
          <td>decider</td>
          <td>做出該決策的人與角色</td>
      </tr>
  </tbody>
</table>
<p>這個格式讓事故發生時，<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a> 可以直接調用每批的 metrics 與決策紀錄，不需要回溯 dashboard 截圖。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>canary 成功率正常但 timeout 升高</td>
          <td>交易完成但成本與延遲風險在累積</td>
          <td>暫停擴批，先調 provider timeout 策略</td>
      </tr>
      <tr>
          <td>error budget 快速消耗</td>
          <td>變更風險超過目前可承受範圍</td>
          <td>觸發 freeze，回到上一批設定</td>
      </tr>
      <tr>
          <td>rollback 成功但客訴仍上升</td>
          <td>影響可能來自非同步補償或下游延遲</td>
          <td>補 replay/對帳證據，再決定是否二次回退</td>
      </tr>
      <tr>
          <td>不同區域結果分歧</td>
          <td>provider 區域品質差異或路由策略不一致</td>
          <td>分區 gate，禁止全域同批放行</td>
      </tr>
      <tr>
          <td>告警只反映症狀無法定位變更關聯</td>
          <td>evidence 與 deploy event 沒對位</td>
          <td>補 deployment event link 與 owner 欄位</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 gate 當成 CI 綠燈會漏掉依賴風險。依賴類變更需要觀測窗與停損條件，單靠測試通過不足以放行。</p>
<p>把 rollback window 寫成「可回退」但沒有時限也會失效。沒有時間邊界的回退通常意味著資料與行為已經漂移。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>這條路徑可用 <a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">Stripe Idempotency and Zero-downtime Migration</a> 回寫。先看交易正確性與變更節奏如何綁定，再回到本章對齊 gate 欄位與停損邏輯。</p>
<p>這個案例主要支撐的是「交易依賴變更放行節奏」判讀，不直接支撐 incident 通訊節奏；對外更新要回到 8.4。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 4.22 的交接：證據來源使用 <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">Checkout API Evidence Package</a>。</li>
<li>與 6.8 的交接：策略與制度回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate 與變更節奏</a>。</li>
<li>與 6.23 的交接：每批驗證證據進 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">Verification Evidence Handoff</a>。</li>
<li>與 8.19 的交接：停損與回退決策同步到 incident decision log。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看控制面事故如何用 decision log 與 write-back 關閉迴圈，接著讀 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back 實作示範</a>。</p>
]]></content:encoded></item><item><title>MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-aurora/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-aurora/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> 跟 &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&lt;/a>。走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type C operational hybrid 結構。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Ops 責任&lt;/th>
 &lt;th>自管 MySQL&lt;/th>
 &lt;th>Aurora MySQL&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>EBS / local SSD、自己選 + 監控&lt;/td>
 &lt;td>Aurora distributed storage（自動 6 份跨 3 AZ）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication setup&lt;/td>
 &lt;td>binlog + semi-sync 自己配&lt;/td>
 &lt;td>Storage layer 自動、無 binlog replication&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failover&lt;/td>
 &lt;td>Orchestrator + VIP + fence script&lt;/td>
 &lt;td>Aurora 內建、&amp;lt; 30 秒 RTO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup&lt;/td>
 &lt;td>mysqldump / Percona XtraBackup&lt;/td>
 &lt;td>自動 continuous backup、PITR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Parameter tuning&lt;/td>
 &lt;td>my.cnf 自己改&lt;/td>
 &lt;td>Parameter group（部分 knob 鎖）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection limit&lt;/td>
 &lt;td>max_connections 自己設&lt;/td>
 &lt;td>看 instance class、有上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auto scaling&lt;/td>
 &lt;td>不適用&lt;/td>
 &lt;td>Aurora Serverless v2 + read replica auto-scaling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-region&lt;/td>
 &lt;td>自己配 chained replication&lt;/td>
 &lt;td>Aurora Global Database&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Per-month cost&lt;/td>
 &lt;td>EC2 + EBS + 自己管 ops&lt;/td>
 &lt;td>Higher per-GB / per-IOPS、但 ops headcount saving&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>從 &lt;em>MySQL 角度&lt;/em> 看 Aurora MySQL：wire protocol 一致、SQL 一致、ORM 不必改、application 連 endpoint 字串以外幾乎不必動。從 &lt;em>Ops 角度&lt;/em> 看 Aurora MySQL：所有 storage / replication / failover knob 都 &lt;em>看不到也改不了&lt;/em>、整個 ops 心智模型重寫。&lt;/p>
&lt;p>這是 Type C operational hybrid 的典型 signature — &lt;em>schema / paradigm 接近、operational 完全不同&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</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>。走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type C operational hybrid 結構。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>Ops 責任</th>
          <th>自管 MySQL</th>
          <th>Aurora MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage</td>
          <td>EBS / local SSD、自己選 + 監控</td>
          <td>Aurora distributed storage（自動 6 份跨 3 AZ）</td>
      </tr>
      <tr>
          <td>Replication setup</td>
          <td>binlog + semi-sync 自己配</td>
          <td>Storage layer 自動、無 binlog replication</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>Orchestrator + VIP + fence script</td>
          <td>Aurora 內建、&lt; 30 秒 RTO</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>mysqldump / Percona XtraBackup</td>
          <td>自動 continuous backup、PITR</td>
      </tr>
      <tr>
          <td>Parameter tuning</td>
          <td>my.cnf 自己改</td>
          <td>Parameter group（部分 knob 鎖）</td>
      </tr>
      <tr>
          <td>Connection limit</td>
          <td>max_connections 自己設</td>
          <td>看 instance class、有上限</td>
      </tr>
      <tr>
          <td>Auto scaling</td>
          <td>不適用</td>
          <td>Aurora Serverless v2 + read replica auto-scaling</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>自己配 chained replication</td>
          <td>Aurora Global Database</td>
      </tr>
      <tr>
          <td>Per-month cost</td>
          <td>EC2 + EBS + 自己管 ops</td>
          <td>Higher per-GB / per-IOPS、但 ops headcount saving</td>
      </tr>
  </tbody>
</table>
<p>從 <em>MySQL 角度</em> 看 Aurora MySQL：wire protocol 一致、SQL 一致、ORM 不必改、application 連 endpoint 字串以外幾乎不必動。從 <em>Ops 角度</em> 看 Aurora MySQL：所有 storage / replication / failover knob 都 <em>看不到也改不了</em>、整個 ops 心智模型重寫。</p>
<p>這是 Type C operational hybrid 的典型 signature — <em>schema / paradigm 接近、operational 完全不同</em>。</p>
<h2 id="為什麼是-type-coperational-為主">為什麼是 Type C（operational 為主）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#%e5%af%ab%e5%89%8d%e7%9a%84-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low</td>
          <td>MySQL wire protocol + SQL 完全一致</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High</td>
          <td>storage / replication / failover / backup ops 全部轉到 AWS</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>同 OLTP relational paradigm</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Medium</td>
          <td>Aurora 加 storage layer / cluster endpoint / reader endpoint</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Low</td>
          <td>主要 connection string + connection pool 設定</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low-Medium</td>
          <td>single-region scaling、跨 region 走 Global Database</td>
      </tr>
  </tbody>
</table>
<p>Operational = High（其他 Low） → <strong>Type C operational hybrid</strong>。Migration 路徑用 <em>4-phase drop-in cutover</em> + <em>operational re-onboarding</em>。</p>
<h2 id="drivertco--multi-az-ha--aws-integration">Driver：TCO + Multi-AZ HA + AWS integration</h2>
<p>從自管 MySQL 遷到 Aurora MySQL 的核心 driver：</p>
<ul>
<li><strong>TCO</strong>：自管 MySQL 真實 cost = EC2 + EBS + ops headcount（1-3 個 FTE 撐大 MySQL deployment）。Aurora per-GB / per-IOPS 比 EC2+EBS 貴 30-50%、但省 ops headcount、總帳通常 break-even 或更便宜</li>
<li><strong>Multi-AZ HA</strong>：Aurora storage 自動 6 份跨 3 AZ、failover &lt; 30 秒、不需要自管 Orchestrator + VIP + fence script</li>
<li><strong>AWS ecosystem integration</strong>：跟 Lambda / SAM / CloudFormation / IAM / Secrets Manager 整合、給 cloud-native architecture 加分</li>
<li><strong>Read scaling</strong>：Aurora 最多 15 個 read replica、storage layer 共享（不 replicate data、僅 replicate page cache）、read latency &lt; 10ms inter-replica</li>
</ul>
<p>不適合 <em>已用 Percona Server fork</em> 或 <em>需要 cross-cloud portability</em> 的 org — Aurora MySQL 是 AWS-only、且 fork 自 MySQL 5.7/8.0、跟 Percona 特性不完全一致。</p>
<h2 id="4-phase-migration">4-phase migration</h2>
<h3 id="phase-1aurora-cluster-起來作為-read-replica">Phase 1：Aurora cluster 起來作為 read replica</h3>
<p>最低風險入口：建 Aurora cluster、用 MySQL binlog 把 production 資料 stream 進 Aurora。Application 仍寫自管 MySQL primary、Aurora 作為 <em>external read replica</em>。</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. 在 AWS 建 Aurora MySQL cluster</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 prod-aurora <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --engine aurora-mysql <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine-version 8.0.mysql_aurora.3.04.0 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --master-username admin <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --master-user-password ... <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --database-name production <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --vpc-security-group-ids sg-xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --db-subnet-group-name prod-subnet
</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 class="c1"># 2. 用 mysqldump 或 Percona XtraBackup 拿 baseline</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">mysqldump --single-transaction --master-data<span class="o">=</span><span class="m">2</span> --triggers --routines --events <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --all-databases &gt; baseline.sql
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 3. Restore 到 Aurora</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">mysql -h prod-aurora.cluster-xxx.us-east-1.rds.amazonaws.com -u admin -p &lt; baseline.sql
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 4. 設定 Aurora 從自管 MySQL 接 binlog</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">CALL mysql.rds_set_external_master<span class="o">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="s1">&#39;self-managed-primary.example.com&#39;</span>, 3306,
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="s1">&#39;replication_user&#39;</span>, <span class="s1">&#39;password&#39;</span>,
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="s1">&#39;mysql-bin.000123&#39;</span>, 12345, <span class="m">0</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">CALL mysql.rds_start_replication<span class="p">;</span></span></span></code></pre></div><p>完成標準：Aurora replica lag &lt; 1 秒、跟 production primary 同步。</p>
<h3 id="phase-2application-read-切到-aurora-reader-endpoint">Phase 2：Application read 切到 Aurora reader endpoint</h3>
<p>Application 仍寫自管 primary、但讀 query 切到 Aurora reader endpoint：</p>
<ul>
<li>Aurora reader endpoint：<code>prod-aurora.cluster-ro-xxx.us-east-1.rds.amazonaws.com</code></li>
<li>自動 round-robin 多個 read replica</li>
<li>ProxySQL 或 application config 改 read connection string</li>
</ul>
<p>跑 1-2 週、確認：</p>
<ul>
<li>Aurora read latency 跟自管 replica latency 接近（通常 Aurora 略好）</li>
<li>Aurora replication lag 穩定 &lt; 1 秒</li>
<li>Aurora query 結果跟自管 primary 一致（spot-check critical query）</li>
</ul>
<p>完成標準：所有 read traffic 都進 Aurora、no application bug。</p>
<h3 id="phase-3cutover--promote-aurora-primary">Phase 3：Cutover — promote Aurora primary</h3>
<p>Cutover window 內：</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. 停 application 寫入（feature flag / scheduled maintenance）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 2. 等自管 primary 跟 Aurora 同步完成（檢查 Aurora replica lag = 0）</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="c1"># 3. 把 Aurora 從 external replica 提升為獨立 primary</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">CALL mysql.rds_stop_replication<span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">CALL mysql.rds_reset_external_master<span class="p">;</span>
</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="c1"># 4. Application 寫 connection string 切到 Aurora writer endpoint</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># prod-aurora.cluster-xxx.us-east-1.rds.amazonaws.com</span>
</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 class="c1"># 5. 開始 application traffic</span></span></span></code></pre></div><p>完成標準：寫入流量 100% 進 Aurora、自管 primary 變 idle。Cutover 通常需要 30-60 分鐘 maintenance window。</p>
<h3 id="phase-4decommission-自管-mysql">Phase 4：Decommission 自管 MySQL</h3>
<p>跑 1-2 週確認 Aurora 穩定後 <em>慢慢退役自管</em>：</p>
<ul>
<li>自管 primary 保留作 <em>cold backup</em>（1-3 個月）、不接 traffic、可隨時 rollback</li>
<li>Replica 一個一個關掉</li>
<li>監控 Aurora cost vs 預估、確認 break-even</li>
</ul>
<p>完成標準：自管 EC2 instance terminate、EBS volume snapshot 後 delete、cost 對比驗證符合預期。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-parameter-group-沒對齊--innodb_flush_log_at_trx_commit-等行為差">1. Parameter group 沒對齊 — <code>innodb_flush_log_at_trx_commit</code> 等行為差</h3>
<p>Aurora 的 <em>parameter group</em> 取代 my.cnf。預設 parameter group 不一定跟自管 MySQL 一致：</p>
<ul>
<li><code>innodb_flush_log_at_trx_commit</code>：自管常設 1（zero loss）、Aurora 預設仍 1 但走 <em>Aurora storage durability</em>（行為等價但不同 mechanism）</li>
<li><code>sync_binlog</code>：自管 1、Aurora <em>沒有 binlog 寫 disk</em> 概念（Aurora 不用 binlog 做 replication、binlog 是 <em>optional output</em>）</li>
<li><code>time_zone</code>：Aurora 預設 UTC、自管常設 local time、TIMESTAMP query 行為可能不同</li>
<li><code>character_set_*</code>：自管常設 utf8mb4、Aurora 預設可能是 latin1（看 cluster create 命令）</li>
</ul>
<p>修法：</p>
<ul>
<li>
<p>Phase 1 完成後 <em>逐 row 對比 parameter group</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="k">global</span><span class="p">.</span><span class="n">variable_name</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="p">...</span></span></span></code></pre></div></li>
<li>
<p>建 <em>custom DB cluster parameter group</em>、匹配自管設定</p>
</li>
<li>
<p>重啟 Aurora primary 套 parameter group 改變（部分 parameter 需要重啟）</p>
</li>
</ul>
<h3 id="2-iam-authentication--application-沒準備">2. IAM authentication — application 沒準備</h3>
<p>Aurora 提供 <em>IAM authentication</em>（不用 password、用 AWS IAM role + temporary token）。Application 用 IAM auth 不必管 password rotation、但程式碼必須 <em>call AWS SDK 取 token、放 connection 設定</em>。</p>
<p>如果 Phase 2-3 期間沒 reverse engineer application connection logic、cutover 後 application 仍試用 password auth、Aurora 拒絕、production down。</p>
<p>修法：</p>
<ul>
<li>評估是否啟用 IAM auth — <em>簡單情況保留 password</em>、整合 AWS Secrets Manager 自動 rotation</li>
<li>啟用 IAM 必須 application code 改：
<ul>
<li>Java：<code>com.amazonaws.services.rds.auth.RdsIamAuthTokenGenerator</code></li>
<li>Python：<code>boto3.client('rds').generate_db_auth_token(...)</code></li>
<li>Go：<code>aws-sdk-go-v2/feature/rds/auth</code></li>
</ul>
</li>
<li>Phase 2 期間 application 對 Aurora 用 IAM token、self-managed 仍 password — 雙 path code</li>
</ul>
<h3 id="3-aurora-only-feature-寫進-applicationrollback-成本升高">3. Aurora-only feature 寫進 application、rollback 成本升高</h3>
<p>Migration 過程開發發現 Aurora 有 <em>Aurora-only feature</em>（Backtrack、Performance Insights、Aurora Global Database）、誘惑使用。一旦 application 用了 Aurora-only feature、要 rollback 自管 MySQL 變不可能（feature 不存在、query 失敗）。</p>
<p>常見 Aurora-only feature：</p>
<ul>
<li><em>Backtrack</em>：72 小時內 in-place rollback 整個 DB（不同於 PITR）</li>
<li><em>Aurora ML</em>：SQL function 內接 SageMaker / Comprehend</li>
<li><em>Aurora Parallel Query</em>：analytical query 跨 storage node 並行</li>
<li><em>Aurora Auto Scaling</em>：read replica 數量按 CPU 自動加減</li>
</ul>
<p>修法：</p>
<ul>
<li><em>Phase 1-3 期間禁用 Aurora-only feature</em>、保留 rollback option</li>
<li><em>Phase 4 完成後</em> 才開始 evaluate Aurora-only feature、加進來時 <em>明確記錄不可 rollback decision</em></li>
<li>把 Aurora-only feature 跟 <em>Aurora 特定 cluster</em> 綁定，避免 application 邏輯依賴 Aurora-only</li>
</ul>
<h3 id="4-read-replica-endpoint-behavior--application-不知道-reader-endpoint-round-robin">4. Read replica endpoint behavior — Application 不知道 reader endpoint round-robin</h3>
<p>Aurora reader endpoint（<code>prod-aurora.cluster-ro-xxx</code>）是 <em>DNS-based load balancer</em>、每次 DNS query 給不同 replica IP。Application connection pool 連續開 10 個 connection、可能全部連同一個 replica（DNS cache）、不均勻。</p>
<p>修法：</p>
<ul>
<li>Application connection pool 強制 <em>DNS re-resolve</em>（避免長時間 cache）</li>
<li>或用 <em>RDS Proxy</em>（managed connection pool）放在前面、不直接連 reader endpoint</li>
<li>或用 <em>Route 53 latency-based routing</em> 配 Aurora reader endpoint per AZ、application 連最近 AZ</li>
</ul>
<h3 id="5-region-failover--aurora-global-database-vs-自管-chained-replication">5. Region failover — Aurora Global Database vs 自管 chained replication</h3>
<p>自管 cross-region replication 是 <em>chained replication</em>（primary → region2 replica → region2 cascading replica）。Aurora Global Database 是 <em>storage-level replication</em>（storage page 直接 ship，而非 binlog）、跨 region &lt; 1 秒 lag、failover &lt; 1 分鐘。</p>
<p>但 Aurora Global Database 是 <em>active-passive</em>（primary region 可寫、secondary region 只讀）。如果原本自管已經 cross-region active-active write（用 multi-master 或應用層 sharding）、Aurora Global Database 的寫入模型會成為限制。</p>
<p>修法：</p>
<ul>
<li>評估 cross-region 是 <em>DR</em> 用途還是 <em>active write</em> 用途</li>
<li>純 DR + read scaling：Aurora Global Database 直接 cover</li>
<li>Active-active write：要 <em>Aurora DSQL</em>（2024 新推出、跟 Aurora 不同 product）或 distributed SQL（CockroachDB / Spanner）</li>
</ul>
<h2 id="capability-gap自管-mysql-有但-aurora-沒有">Capability gap：自管 MySQL 有但 Aurora 沒有</h2>
<table>
  <thead>
      <tr>
          <th>能力</th>
          <th>自管 MySQL</th>
          <th>Aurora MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plugin 自己裝</td>
          <td>任意</td>
          <td>受限（Aurora 只允許官方支援）</td>
      </tr>
      <tr>
          <td>OS-level access</td>
          <td>完整 SSH access</td>
          <td>managed service，無 SSH access</td>
      </tr>
      <tr>
          <td>MySQL 8.0 latest patch</td>
          <td>你決定</td>
          <td>跟 Aurora major version 對應、有滯後</td>
      </tr>
      <tr>
          <td>InnoDB log_file_size</td>
          <td>自己改</td>
          <td>Aurora 內建 storage path</td>
      </tr>
      <tr>
          <td>Custom storage engine</td>
          <td>可（MyRocks / TokuDB）</td>
          <td>只 InnoDB（Aurora optimized）</td>
      </tr>
      <tr>
          <td>Cross-cloud DR</td>
          <td>自配 binlog ship</td>
          <td>Aurora-only (AWS region)</td>
      </tr>
  </tbody>
</table>
<p>評估時必須確認 <em>當前自管功能</em> 沒用到 Aurora 不支援的能力。如果在用 MyRocks 等 storage engine、Aurora migration 不可行。</p>
<h2 id="容量與成本對照">容量與成本對照</h2>
<p>對 100 GB DB、5K WPS、20 個 application instance 的 deployment：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>自管 MySQL（EC2）</th>
          <th>Aurora MySQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Primary instance</td>
          <td>r5.2xlarge（$0.50/hr）</td>
          <td>db.r6g.2xlarge（$0.83/hr）</td>
      </tr>
      <tr>
          <td>EBS / Aurora storage</td>
          <td>io2 100 GB + 5000 IOPS = ~$70/mo</td>
          <td>Aurora storage 100 GB = ~$10/mo + I/O $0.20/M</td>
      </tr>
      <tr>
          <td>Replica × 3</td>
          <td>3 × r5.2xlarge = $1080/mo</td>
          <td>3 × db.r6g.large = $540/mo</td>
      </tr>
      <tr>
          <td>Backup storage</td>
          <td>S3 + 自己 cron mysqldump ~$50/mo</td>
          <td>Aurora backup 100 GB 免費 + 額外 $0.021/GB</td>
      </tr>
      <tr>
          <td>Ops headcount</td>
          <td>1-2 FTE × $150K = $300-500K/yr</td>
          <td>&lt; 0.5 FTE × $150K = $75K/yr</td>
      </tr>
      <tr>
          <td><strong>Total infra</strong></td>
          <td>~$1500/mo + 大 ops cost</td>
          <td>~$2000-3000/mo + 小 ops cost</td>
      </tr>
  </tbody>
</table>
<p>Pure infra cost Aurora 貴 30-50%、但 <em>ops cost 降幅大過 infra increase</em> — 200 人 eng team 養 1.5 FTE DBA 是 $300K-400K/yr、Aurora 換成 0.3 FTE 是 $60K-100K/yr、差距 $200K+ 抵 infra increase。</p>
<p>小團隊 / 小 deployment Aurora 不一定划算 — 50 人 eng team 沒有 dedicated DBA、自管 MySQL 也只佔某人 20% 時間、Aurora migration 的 ops saving 不存在。</p>
<h2 id="production-casenetflix-aurora-consolidation">Production case：Netflix Aurora consolidation</h2>
<p>MySQL → Aurora migration 的 production 責任是把自管 database operation 轉移成 managed SQL 的契約，而非只搬 schema 與資料。<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 consolidation</a> 提供的工程訊號是多套 RDBMS 整併到 Aurora 後，效能、成本與操作責任一起改變。</p>
<p>這個案例要回收到三個操作判準。第一，migration driver 應寫成 operation transfer，例如 backup、failover、storage growth、patching 與 observability 由誰承擔。第二，效能與成本要一起看，因為 Aurora 的 storage / compute / I/O 計費會把原本藏在 DBA 操作裡的成本攤開。第三，整併多套 RDBMS 時要先做 feature inventory，確認 plugin、storage engine、charset、replication topology 與 SQL mode 都能落到 Aurora MySQL 支援範圍。</p>
<p>Netflix case 的 sibling 路由是 <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 page</a> 與 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a>。若 migration 目標從 managed SQL 變成 multi-region active-active write，應改接 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。</p>
<h2 id="何時維持原路線">何時維持原路線</h2>
<ul>
<li><strong>Cross-cloud portability 是 requirement</strong>：Aurora AWS-only、要 cross-cloud 用 PlanetScale 或 自管</li>
<li><strong>用 Percona Server fork / MyRocks 等非標準 engine</strong>：Aurora 不支援</li>
<li><strong>需要 OS-level customization</strong>：Aurora 完全 managed、無 SSH</li>
<li><strong>規模太小</strong>：&lt; 100 GB / &lt; 1K WPS、自管 MySQL EC2 spot instance 已經夠便宜</li>
<li><strong>規模太大</strong>：&gt; 50 TB single DB / &gt; 100K WPS、Aurora single-instance 仍是 ceiling、考慮 Vitess 或 Aurora DSQL</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>平行 batch：→ PlanetScale migration playbook（同 MySQL backlog、不同 target paradigm）</li>
<li>上游：<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</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 vendor page</a></li>
<li>跨章節：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> — Aurora cost forecast</li>
<li>既有 case：<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 consolidation</a> — Netflix 從多套 RDBMS 統一到 Aurora 的 migration evidence</li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type C operational hybrid 結構說明）</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.html">Aurora MySQL Migration Guide</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>extension ecosystem&lt;/em> — PG 結構性產品線擴張的機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="extension-不只是-plugin是產品線擴張">Extension 不只是 plugin、是產品線擴張&lt;/h2>
&lt;p>PG extension 機制讓 &lt;em>第三方加新 type / function / operator / index access method / planner hook&lt;/em>、深度整合到 PG core。對比其他 DB 的 plugin model（MySQL plugin / MongoDB plugin）、PG extension 是 &lt;em>更深的 SPI&lt;/em>。&lt;/p>
&lt;p>結果：&lt;/p>
&lt;ul>
&lt;li>pgvector → PG 變 vector similarity search DB（取代 Pinecone / Weaviate）&lt;/li>
&lt;li>TimescaleDB → PG 變 time-series DB（取代 InfluxDB）&lt;/li>
&lt;li>Citus → PG 變 sharded cluster&lt;/li>
&lt;li>PostGIS → PG 變 GIS DB&lt;/li>
&lt;li>pg_cron → PG 變 scheduled job runner&lt;/li>
&lt;li>pgvectorscale → 大規模 vector index&lt;/li>
&lt;/ul>
&lt;p>對 &lt;em>vendor lock-in 敏感&lt;/em> / &lt;em>想統一 stack&lt;/em> 的 org、PG extension 提供 &lt;em>用 PG 取代多個 specialized DB&lt;/em> 的可能。&lt;/p>
&lt;p>但 &lt;em>統一 stack 的代價&lt;/em>：PG 主庫 ops 風險集中（一個 PG 掛 = vector / time-series / GIS / cron 全掛）、extension 跟 PG version 對齊矩陣多一道升級顧慮、規模上限通常比專業 DB 低（pgvector 100M+ vs Pinecone 10B+ / TimescaleDB 100K rows/s vs InfluxDB 500K+）。決策框架：&lt;em>中小規模 + 已用 PG + 不想多管系統&lt;/em> → extension；&lt;em>大規模 + 純該 workload + 有專業 team&lt;/em> → specialized DB。&lt;/p>
&lt;h2 id="extension-lifecycle">Extension Lifecycle&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 看可用 extension
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_available_extensions&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 安裝（在 OS 層、要有對應 package）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">-- apt install postgresql-14-pg-stat-statements
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Enable in DB
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_stat_statements&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 確認
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_extension&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 升級 extension
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_stat_statements&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UPDATE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 移除
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">DROP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_stat_statements&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 extension 有：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>extension ecosystem</em> — PG 結構性產品線擴張的機制。</p></blockquote>
<hr>
<h2 id="extension-不只是-plugin是產品線擴張">Extension 不只是 plugin、是產品線擴張</h2>
<p>PG extension 機制讓 <em>第三方加新 type / function / operator / index access method / planner hook</em>、深度整合到 PG core。對比其他 DB 的 plugin model（MySQL plugin / MongoDB plugin）、PG extension 是 <em>更深的 SPI</em>。</p>
<p>結果：</p>
<ul>
<li>pgvector → PG 變 vector similarity search DB（取代 Pinecone / Weaviate）</li>
<li>TimescaleDB → PG 變 time-series DB（取代 InfluxDB）</li>
<li>Citus → PG 變 sharded cluster</li>
<li>PostGIS → PG 變 GIS DB</li>
<li>pg_cron → PG 變 scheduled job runner</li>
<li>pgvectorscale → 大規模 vector index</li>
</ul>
<p>對 <em>vendor lock-in 敏感</em> / <em>想統一 stack</em> 的 org、PG extension 提供 <em>用 PG 取代多個 specialized DB</em> 的可能。</p>
<p>但 <em>統一 stack 的代價</em>：PG 主庫 ops 風險集中（一個 PG 掛 = vector / time-series / GIS / cron 全掛）、extension 跟 PG version 對齊矩陣多一道升級顧慮、規模上限通常比專業 DB 低（pgvector 100M+ vs Pinecone 10B+ / TimescaleDB 100K rows/s vs InfluxDB 500K+）。決策框架：<em>中小規模 + 已用 PG + 不想多管系統</em> → extension；<em>大規模 + 純該 workload + 有專業 team</em> → specialized DB。</p>
<h2 id="extension-lifecycle">Extension Lifecycle</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 看可用 extension
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_available_extensions</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 安裝（在 OS 層、要有對應 package）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">-- apt install postgresql-14-pg-stat-statements
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- Enable in DB
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 確認
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_extension</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 升級 extension
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="c1">-- 移除
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span><span class="k">DROP</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="p">;</span></span></span></code></pre></div><p>每個 extension 有：</p>
<ul>
<li><em>Version</em> — 跟 PG version 綁定（如 pg_stat_statements 14 / 15 / 16）</li>
<li><em>Schema</em> — 安裝到 <code>public</code> 或專屬 schema</li>
<li><em>Dependencies</em> — 部分 extension 依賴其他（如 PostGIS 依賴 pg_trgm）</li>
<li><em>Trusted vs untrusted</em> — trusted 可以 non-superuser 安裝（PG 13+）</li>
</ul>
<h2 id="6-個-production-critical-extension">6 個 Production-Critical Extension</h2>
<h3 id="1-pg_stat_statements--query-stats必裝">1. pg_stat_statements — Query stats（必裝）</h3>
<p>任何 production PG cluster 都該裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">shared_preload_libraries</span> <span class="o">=</span> <span class="s">&#39;pg_stat_statements&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">pg_stat_statements.max</span> <span class="o">=</span> <span class="s">5000</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">pg_stat_statements.track</span> <span class="o">=</span> <span class="s">all</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Top 10 query by total time
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">calls</span><span class="p">,</span><span class="w"> </span><span class="n">total_exec_time</span><span class="p">,</span><span class="w"> </span><span class="n">mean_exec_time</span><span class="p">,</span><span class="w"> </span><span class="k">rows</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">total_exec_time</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>對應 MySQL <code>events_statements_summary_by_digest</code>。詳見 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>。</p>
<h3 id="2-pg_partman--自動-partition-lifecycle">2. pg_partman — 自動 partition lifecycle</h3>
<p>PG declarative partitioning 需要 <em>手動建 / drop partition</em>。pg_partman 自動化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_partman</span><span class="w"> </span><span class="k">SCHEMA</span><span class="w"> </span><span class="n">partman</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="c1">-- 設 events 表自動 monthly partition
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">partman</span><span class="p">.</span><span class="n">create_parent</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">p_parent_table</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;public.events&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">p_control</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="n">p_type</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;range&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="n">p_interval</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;1 month&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">p_premake</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="mi">6</span><span class="w">  </span><span class="c1">-- 預先建 6 個未來 partition
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 跑 maintenance（建未來 partition + drop 老 partition）
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">partman</span><span class="p">.</span><span class="n">run_maintenance</span><span class="p">(</span><span class="n">p_analyze</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="k">false</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- 預設用 pg_cron 排程</span></span></span></code></pre></div><p>對 <em>time-series partition</em> workload 必裝。詳見 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">Declarative Partitioning</a>。</p>
<h3 id="3-pg_repack--online-table-rewrite">3. pg_repack — Online table rewrite</h3>
<p>詳見 <a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">Online Schema Change</a>。</p>
<h3 id="4-pgvector--vector-similarity-search">4. pgvector — Vector similarity search</h3>
<p>LLM embedding / semantic search 場景必裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">vector</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">content</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">embedding</span><span class="w"> </span><span class="n">VECTOR</span><span class="p">(</span><span class="mi">1536</span><span class="p">)</span><span class="w">  </span><span class="c1">-- OpenAI text-embedding-3-small 1536-dim
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- HNSW index（pgvector 0.5+）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">HNSW</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">vector_cosine_ops</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 找最相似的 5 個
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;=&gt;</span><span class="w"> </span><span class="s1">&#39;[0.1, 0.2, ...]&#39;</span><span class="p">::</span><span class="n">vector</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span></span></span></code></pre></div><p>對 <em>中小規模 RAG / semantic search</em> workload、pgvector 在 PG 內跑、不必跨 Pinecone / Weaviate / Qdrant 等獨立服務。</p>
<p>對 <em>超大規模</em> vector workload（&gt; 1 億 vector）考慮 pgvectorscale（pgvector 的 streaming variant）或專業 vector DB。</p>
<h3 id="5-timescaledb--time-series-擴展">5. TimescaleDB — Time-series 擴展</h3>
<p>把 PG 變 time-series DB：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">timescaledb</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">metrics</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">time</span><span class="w"> </span><span class="n">TIMESTAMPTZ</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">device_id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">value</span><span class="w"> </span><span class="n">DOUBLE</span><span class="w"> </span><span class="k">PRECISION</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- 轉成 hypertable（auto-partition by time）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_hypertable</span><span class="p">(</span><span class="s1">&#39;metrics&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;time&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- Continuous aggregate（materialized view 自動 refresh）
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">metrics_5min</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">continuous</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">time_bucket</span><span class="p">(</span><span class="s1">&#39;5 minutes&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">bucket</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">       </span><span class="n">device_id</span><span class="p">,</span><span class="w"> </span><span class="k">avg</span><span class="p">(</span><span class="n">value</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">metrics</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">bucket</span><span class="p">,</span><span class="w"> </span><span class="n">device_id</span><span class="p">;</span></span></span></code></pre></div><p>對 IoT / monitoring / financial tick data 場景、TimescaleDB 比純 PG 寫吞吐高 10x+。</p>
<h3 id="6-postgis--gis-extension">6. PostGIS — GIS extension</h3>
<p>地理 / 空間 query 業界標準：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">postgis</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">stores</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">name</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="k">location</span><span class="w"> </span><span class="n">GEOGRAPHY</span><span class="p">(</span><span class="n">POINT</span><span class="p">,</span><span class="w"> </span><span class="mi">4326</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">stores</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIST</span><span class="w"> </span><span class="p">(</span><span class="k">location</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 找 1 km 內的 store
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">stores</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">ST_DWithin</span><span class="p">(</span><span class="k">location</span><span class="p">,</span><span class="w"> </span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">05</span><span class="p">)::</span><span class="n">geography</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span></span></span></code></pre></div><p>PostGIS 是 GIS workload 業界標準、其他 DB GIS 能力都對標 PostGIS。</p>
<h2 id="其他常用-extension">其他常用 extension</h2>
<p>除 6 個 production-critical 之外、以下是 <em>特定場景常用</em> 的 extension — 分四類：排程跟 utility（<code>pg_cron</code> / <code>pg_trgm</code> / <code>uuid-ossp</code>）、type 擴展（<code>hstore</code> / <code>citext</code> / <code>pgcrypto</code>）、跨 DB 整合（<code>postgres_fdw</code> / <code>mysql_fdw</code>）、observability / debug 工具（<code>pg_buffercache</code> / <code>pg_visibility</code> / <code>auto_explain</code>）：</p>
<table>
  <thead>
      <tr>
          <th>Extension</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pg_cron</code></td>
          <td>排程 SQL job（不必外部 cron）</td>
      </tr>
      <tr>
          <td><code>pg_trgm</code></td>
          <td>Fuzzy string match / similarity</td>
      </tr>
      <tr>
          <td><code>uuid-ossp</code></td>
          <td>UUID 產生</td>
      </tr>
      <tr>
          <td><code>hstore</code></td>
          <td>Key-value pair type</td>
      </tr>
      <tr>
          <td><code>citext</code></td>
          <td>Case-insensitive text type</td>
      </tr>
      <tr>
          <td><code>pgcrypto</code></td>
          <td>加密 / hash function</td>
      </tr>
      <tr>
          <td><code>postgres_fdw</code></td>
          <td>PG → PG foreign table</td>
      </tr>
      <tr>
          <td><code>mysql_fdw</code></td>
          <td>PG → MySQL foreign table</td>
      </tr>
      <tr>
          <td><code>pg_buffercache</code></td>
          <td>Buffer pool 內容檢視</td>
      </tr>
      <tr>
          <td><code>pg_visibility</code></td>
          <td>Visibility map 檢視（debug bloat）</td>
      </tr>
      <tr>
          <td><code>auto_explain</code></td>
          <td>Slow query 自動 log plan</td>
      </tr>
      <tr>
          <td><code>wal2json</code></td>
          <td>Logical decoding output 為 JSON</td>
      </tr>
      <tr>
          <td><code>Citus</code></td>
          <td>Distributed PG</td>
      </tr>
      <tr>
          <td><code>pgvector</code></td>
          <td>Vector similarity</td>
      </tr>
      <tr>
          <td><code>pglogical</code></td>
          <td>Logical replication（功能比 native 強）</td>
      </tr>
      <tr>
          <td><code>pg_squeeze</code></td>
          <td>pg_repack 替代</td>
      </tr>
  </tbody>
</table>
<p>實務組合：observability 三件套（<code>pg_stat_statements</code> + <code>auto_explain</code> + <code>pg_buffercache</code>）幾乎是 production 標配；FDW 是「跨 DB query」的 escape hatch、但 cross-DB query 效能差、適合 reporting 不適合 OLTP。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-extension-version-跟-pg-version-對齊">1. Extension version 跟 PG version 對齊</h3>
<p>PG cluster 升 14 → 15 後、extension（pg_stat_statements / pg_partman / pgvector 等）必須有對應 15 版本。早期升級 / niche extension 可能還沒釋出。</p>
<p>修法：</p>
<ul>
<li>升 PG cluster 前 <em>先確認所有 extension 都有對應 PG version 釋出版本</em></li>
<li>升完 PG cluster <em>立即跑 <code>ALTER EXTENSION xxx UPDATE</code></em></li>
<li>Upgrade runbook 紀錄每個 extension 的版本兼容狀態</li>
</ul>
<h3 id="2-managed-pg-限制-extension-列表">2. Managed PG 限制 extension 列表</h3>
<p>AWS RDS / Aurora PG / Cloud SQL / Azure DB for PostgreSQL 各自有 <em>支援 extension 白名單</em>：</p>
<ul>
<li>不在白名單的 extension 不能 install</li>
<li>部分 extension 限定特定 PG version</li>
<li>Untrusted extension 通常不允許</li>
</ul>
<p>常見 <em>managed 不支援</em> 的 extension：</p>
<ul>
<li><code>pg_repack</code>（Aurora 有限支援、RDS 部分 version 支援）</li>
<li><code>pglogical</code>（部分 cloud 不支援）</li>
<li><code>pg_cron</code>（cloud 通常用 managed scheduler 取代）</li>
<li>Custom extension（自寫 .so）</li>
</ul>
<p>修法：</p>
<ul>
<li>評估 managed PG 之前、先查 <em>vendor 支援 extension 列表</em></li>
<li>Self-hosted vs managed 的 <em>跨雲 portability</em> 議題：extension 是 lock-in source</li>
<li>如果 application 強依賴某 extension（如 PostGIS），確認 cloud 支援</li>
</ul>
<h3 id="3-extension-upgrade-order">3. Extension upgrade order</h3>
<p><code>pg_upgrade</code> 升 PG major version 後、extension 也要升。順序：</p>
<ol>
<li><em>pg_upgrade</em> PG binary + cluster</li>
<li>對每個 DB 跑 <code>ALTER EXTENSION xxx UPDATE</code></li>
<li>部分 extension（如 PostGIS）需要 <em>特殊升級程序</em>（<code>SELECT postgis_extensions_upgrade()</code>）</li>
</ol>
<p>修法：</p>
<ul>
<li>升 PG 後 <em>先測 staging cluster</em> 確認 extension upgrade 流程</li>
<li>PostGIS / TimescaleDB / Citus 有自己 upgrade 程序、必須遵循 vendor doc</li>
<li>升完跑 <code>\dx</code> 看每個 extension 版本</li>
</ul>
<h3 id="4-shared_preload_libraries-衝突">4. <code>shared_preload_libraries</code> 衝突</h3>
<p>部分 extension（pg_stat_statements / auto_explain / TimescaleDB / Citus / pg_cron）必須在 <code>shared_preload_libraries</code> 加進去、需要 <em>重啟 PG</em>。</p>
<p>衝突情境：</p>
<ul>
<li>pg_partman + TimescaleDB 都用 background worker、worker 上限不夠</li>
<li><code>max_worker_processes</code> 預設 8、不夠時某些 extension 起不起來</li>
</ul>
<p>修法：</p>
<ul>
<li>列出所有 shared_preload extension、確認 order（部分有 dependency）</li>
<li>提高 <code>max_worker_processes = 16</code> / <code>max_parallel_workers = 8</code> 等</li>
<li>重啟 PG 才生效、計入 maintenance window</li>
</ul>
<h3 id="5-extension-跟-logical-replication-互動">5. Extension 跟 logical replication 互動</h3>
<p>Logical replication（pglogical / native）不自動 replicate extension state（function / type definition）。Subscriber 沒裝對應 extension、replicate event 失敗。</p>
<p>修法：</p>
<ul>
<li>Subscriber 必須 <em>先安裝</em> publisher 用的 extension</li>
<li>Extension 版本 <em>publisher / subscriber 對齊</em></li>
<li>對 extension-heavy schema、考慮用 <em>streaming replication</em>（physical）而非 logical</li>
</ul>
<h2 id="cloud-vendor-對-extension-的支援">Cloud Vendor 對 Extension 的支援</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>常見 extension 支援</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS RDS PostgreSQL</td>
          <td>pg_stat_statements / pg_partman / pgvector / pg_repack</td>
          <td>部分 version 限制 / 不能 install custom</td>
      </tr>
      <tr>
          <td>AWS Aurora PostgreSQL</td>
          <td>同 RDS、加 Aurora-specific</td>
          <td>pg_repack 限版本</td>
      </tr>
      <tr>
          <td>GCP Cloud SQL</td>
          <td>標準 extension 廣支援</td>
          <td>pg_cron / pgvector OK</td>
      </tr>
      <tr>
          <td>Azure DB for PostgreSQL</td>
          <td>廣泛支援 + Azure 整合</td>
          <td>Citus（managed 即 Cosmos DB for PG）</td>
      </tr>
      <tr>
          <td>Self-hosted</td>
          <td>全部</td>
          <td>自己維護</td>
      </tr>
  </tbody>
</table>
<p>對 <em>extension-heavy</em> application、self-hosted PG 仍是必要選擇。Managed PG 適合 <em>標準 extension</em> workload。</p>
<h2 id="何時用-pg-extension-取代專業-db">何時用 PG extension 取代專業 DB</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>用 extension 還是專業 DB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 100M vector + RAG / semantic search</td>
          <td>pgvector（單一 stack 省 ops）</td>
      </tr>
      <tr>
          <td>大規模 vector search &gt; 10M with high QPS</td>
          <td>專業 vector DB（Pinecone / Qdrant）</td>
      </tr>
      <tr>
          <td>Time-series &lt; 100 TB</td>
          <td>TimescaleDB</td>
      </tr>
      <tr>
          <td>Time-series &gt; 100 TB + high cardinality</td>
          <td>專業 TS DB（InfluxDB / VictoriaMetrics）</td>
      </tr>
      <tr>
          <td>GIS</td>
          <td>PostGIS（業界標準）</td>
      </tr>
      <tr>
          <td>Sharded &lt; 10 TB + multi-tenant</td>
          <td>Citus</td>
      </tr>
      <tr>
          <td>Sharded &gt; 100 TB</td>
          <td>distributed SQL（CockroachDB / TiDB）</td>
      </tr>
      <tr>
          <td>Scheduled job</td>
          <td>pg_cron（簡單）/ Airflow（複雜）</td>
      </tr>
  </tbody>
</table>
<p>對中小規模、PG + extension 是 <em>簡化 stack</em> 的有效路徑。規模超過時、專業 DB 仍是首選。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">Citus Distributed</a>：extension 一例、可看 extension model</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>：pg_stat_statements + auto_explain 必用</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">Online Schema Change</a>：pg_repack 是 extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">Declarative Partitioning</a>：pg_partman 是 extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">SQL Features Baseline</a>：extension 是 PG 結構性領先之一</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">PG SQL Features Baseline</a>（extension 是結構優勢）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">PG Citus Distributed</a>（extension example）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PG Online Schema Change</a>（pg_repack）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">PG Declarative Partitioning</a>（pg_partman）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（pg_stat_statements + auto_explain）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/extend-extensions.html">PG Extensions</a> / <a href="https://github.com/pgvector/pgvector">pgvector</a> / <a href="https://docs.timescale.com/">TimescaleDB</a> / <a href="https://postgis.net/">PostGIS</a></li>
</ul>
]]></content:encoded></item><item><title>3.C26 GoCardless：Hutch + 單一 topic exchange service mesh</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/</guid><description>&lt;p>這個案例的核心責任是說明小規模時單 vhost + 統一 routing key 規範可作為 service mesh 基礎。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>單一 RabbitMQ cluster 作為所有服務之間的通訊中樞、自家 Hutch（Ruby lib）2013 從 production 抽出開源。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>routing key 格式 &lt;code>service.subject.action&lt;/code>（如 &lt;code>paysvc.payment.chargedback&lt;/code>）、單一 topic exchange、JSON 序列化（多語言可讀）。揭露小規模單 cluster 可以用「routing key 命名規範」取代複雜 exchange 拓樸。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Exchange types 與 routing 設計 / 多 vhost（單 vhost 服務 mesh 的反向案例）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &amp;#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23 Bloomberg&lt;/a>（規模化後的對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://gocardless.com/blog/hutch-inter-service-communication-with-rabbitmq/">Hutch: Inter-Service Communication with RabbitMQ&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明小規模時單 vhost + 統一 routing key 規範可作為 service mesh 基礎。</p>
<h2 id="觀察">觀察</h2>
<p>單一 RabbitMQ cluster 作為所有服務之間的通訊中樞、自家 Hutch（Ruby lib）2013 從 production 抽出開源。</p>
<h2 id="判讀">判讀</h2>
<p>routing key 格式 <code>service.subject.action</code>（如 <code>paysvc.payment.chargedback</code>）、單一 topic exchange、JSON 序列化（多語言可讀）。揭露小規模單 cluster 可以用「routing key 命名規範」取代複雜 exchange 拓樸。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Exchange types 與 routing 設計 / 多 vhost（單 vhost 服務 mesh 的反向案例）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23 Bloomberg</a>（規模化後的對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://gocardless.com/blog/hutch-inter-service-communication-with-rabbitmq/">Hutch: Inter-Service Communication with RabbitMQ</a></li>
</ul>
]]></content:encoded></item><item><title>GitGuardian</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitguardian/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitguardian/</guid><description>&lt;p>GitGuardian 是 &lt;em>secret scanning + remediation&lt;/em> SaaS、起家於 GitHub public repo scan、現延伸到 internal SCM、CI 系統與 collaboration / chat 工具。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning&lt;/a> 的根本差異不在「能不能抓到 secret」、而在 &lt;em>偵測邊界跟 remediation workflow 的 shape&lt;/em> — GHAS 是 GitHub-only、partner pattern 強但 push protection 鎖在 GitHub repo；GitGuardian 把 detection 邊界擴到跨 SCM 跟 SaaS workspace、然後用 &lt;em>Incident&lt;/em> 物件管整個生命週期。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>GitGuardian 的核心定位是 &lt;em>跨工具的 secret leak detection + incident workflow&lt;/em>、不只是「pre-commit grep」。底層是一組 &lt;em>Detector&lt;/em>（350+ specific detector、覆蓋 AWS / GCP / Stripe / Slack / 自家 token 等）+ &lt;em>Validation endpoint&lt;/em>（call 該 service 確認 secret live 中），上層是 &lt;em>Incident&lt;/em> 物件（assign / resolve / ignore / share with developer）跟 &lt;em>Source&lt;/em> 抽象（GitHub / GitLab / Bitbucket / Azure DevOps / Slack / Jira / Confluence / Notion）。本機側用 &lt;em>ggshield&lt;/em> CLI 做 pre-commit hook 跟 CI scan。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning&lt;/a> 比、GitGuardian 走 &lt;em>cross-tool detection + remediation workflow&lt;/em>、GHAS 走 &lt;em>deep integration in GitHub&lt;/em>：GHAS 的 push protection 在 GitHub server-side 直接攔 push、partner pattern（AWS / Stripe / Slack）廣度高；但只要組織有 GitLab self-hosted、Bitbucket、或 developer 習慣把 token 貼 Slack / Confluence，GHAS 看不到的就是 GitGuardian 的場域。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &amp;#43; regex &amp;#43; entropy、SARIF output、跨 SCM、pre-commit &amp;#43; CI 友善">Gitleaks&lt;/a> / TruffleHog OSS 比、GitGuardian 走 &lt;em>managed SaaS + validation + workflow&lt;/em>、OSS 走 &lt;em>self-hosted + 你自己接 incident pipeline&lt;/em>；OSS 適合預算敏感 + 已有 SOC / IR tooling、GitGuardian 適合直接買 incident workflow 不想自建。&lt;/p></description><content:encoded><![CDATA[<p>GitGuardian 是 <em>secret scanning + remediation</em> SaaS、起家於 GitHub public repo scan、現延伸到 internal SCM、CI 系統與 collaboration / chat 工具。它跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> 的根本差異不在「能不能抓到 secret」、而在 <em>偵測邊界跟 remediation workflow 的 shape</em> — GHAS 是 GitHub-only、partner pattern 強但 push protection 鎖在 GitHub repo；GitGuardian 把 detection 邊界擴到跨 SCM 跟 SaaS workspace、然後用 <em>Incident</em> 物件管整個生命週期。</p>
<h2 id="服務定位">服務定位</h2>
<p>GitGuardian 的核心定位是 <em>跨工具的 secret leak detection + incident workflow</em>、不只是「pre-commit grep」。底層是一組 <em>Detector</em>（350+ specific detector、覆蓋 AWS / GCP / Stripe / Slack / 自家 token 等）+ <em>Validation endpoint</em>（call 該 service 確認 secret live 中），上層是 <em>Incident</em> 物件（assign / resolve / ignore / share with developer）跟 <em>Source</em> 抽象（GitHub / GitLab / Bitbucket / Azure DevOps / Slack / Jira / Confluence / Notion）。本機側用 <em>ggshield</em> CLI 做 pre-commit hook 跟 CI scan。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> 比、GitGuardian 走 <em>cross-tool detection + remediation workflow</em>、GHAS 走 <em>deep integration in GitHub</em>：GHAS 的 push protection 在 GitHub server-side 直接攔 push、partner pattern（AWS / Stripe / Slack）廣度高；但只要組織有 GitLab self-hosted、Bitbucket、或 developer 習慣把 token 貼 Slack / Confluence，GHAS 看不到的就是 GitGuardian 的場域。跟 <a href="/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &#43; regex &#43; entropy、SARIF output、跨 SCM、pre-commit &#43; CI 友善">Gitleaks</a> / TruffleHog OSS 比、GitGuardian 走 <em>managed SaaS + validation + workflow</em>、OSS 走 <em>self-hosted + 你自己接 incident pipeline</em>；OSS 適合預算敏感 + 已有 SOC / IR tooling、GitGuardian 適合直接買 incident workflow 不想自建。</p>
<p>關鍵張力：<em>validation endpoint 是 FP 降噪核心</em>、但也是 <em>vendor 風險點</em>。Detector 抓到字串後 GitGuardian <em>call 該 service API live verify</em>（AWS access key 試 <code>sts:GetCallerIdentity</code>、Stripe key 試 retrieve event）、活躍 secret 才升 Incident。意義是 noise 從 OSS gitleaks 的 70-80% FP 降到 個位數 FP；風險是 GitGuardian <em>本身會 call 你的 cloud account</em> — vendor trust 跟 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的 scope map 需要在 onboarding 就釐清。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>GitGuardian 在 secret scanning stack 中承擔哪一段（pre-commit / SCM scan / SaaS scan / honeytoken）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 管 rotation、IR tool 接 incident）</li>
<li>Detector / Validation / Incident / Source 的 ownership 設計（誰 assign、誰 resolve、developer 怎麼參與）</li>
<li>跨 SCM + SaaS coverage 該開哪些 source、historical scan 多久跑一次</li>
<li>何時用 GitGuardian、何時走 GHAS / Gitleaks / TruffleHog 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 GitGuardian deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Source coverage 廣度</strong>：除 GitHub / GitLab 外、Slack / Jira / Confluence / Notion 是否也納入掃 — developer 把 token 貼 Slack DM 是常見 leak vector</li>
<li><strong>Validation endpoint 是否開</strong>：FP 降噪靠 live verify、未開等於回到 OSS gitleaks 的 noise 水位</li>
<li><strong>Incident remediation SLA</strong>：valid incident 從偵測到 rotation 完成的時間、是否串 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 自動 rotation、是否進 PagerDuty / Slack alert</li>
<li><strong>ggshield 在 CI 跟 pre-commit 的覆蓋率</strong>：是否所有 repo 走 pre-commit hook、CI step 是否阻擋 commit-with-secret merge</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets Management at Scale</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Detector library</strong>：350+ specific detector（AWS Access Key / Stripe Live Key / Slack Bot Token / GitHub PAT / 自家 API key pattern 等）+ generic high-entropy detector。Specific detector 走 vendor-specific pattern + validation endpoint、FP 個位數；generic detector 抓 unknown pattern、FP 較高、通常 routing 到 manual triage queue。Production tenant 應該全開 specific、generic 視 noise budget 開。</p>
<p><strong>Validation endpoint</strong>：detector 抓到字串後、GitGuardian backend <em>call 該 service API live verify</em>。AWS key 試 <code>sts:GetCallerIdentity</code>、Stripe key 試 retrieve test event、GitHub PAT 試 <code>GET /user</code>。verify 結果 <em>Valid</em> / <em>Invalid</em> / <em>Unknown</em> 三態、決定 incident severity。意義是 <em>只有 active secret 升 incident</em>、已 revoke 的舊 commit history 不再 noise。</p>
<p><strong>Incident workflow</strong>：偵測命中後會建 <em>Incident</em> 物件（而非直接 alert）、含 source location / detector / validation status / suggested remediation。Incident 可 <em>assign 給 developer</em>（developer 在 GitGuardian dashboard 自助 acknowledge / rotate / mark FP）、SecOps 只 review escalated case。對應 <a href="/blog/backend/07-security-data-protection/security-as-risk-routing-system/" data-link-title="7.15 資安作為風險路由系統" data-link-desc="建立資安作為風險路由系統的導讀大綱，串接問題節點、控制面與跨模組交接">Security Workflow as Code</a> 的 shift-left 模式 — developer 是 first responder、不是 SecOps 全包。</p>
<p><strong>Source coverage</strong>：GitGuardian 預設掃 SCM（GitHub / GitLab / Bitbucket / Azure DevOps / 自管 Git），但 <em>差異化價值在 SaaS scan</em> — Slack workspace（message / DM / file upload）、Jira issue / comment、Confluence page、Notion workspace、Microsoft Teams 都可接 source connector。Developer 在 Slack 貼 prod DB password 是真實常見 case、SCM-only 工具看不到。</p>
<p><strong>ggshield CLI</strong>：本地 / CI 端的 detection engine。<em>pre-commit hook</em> 攔住 push 前 leak（developer 機器、可被 bypass 但成本提高）、<em>CI step</em> 在 PR 跑 historical scan（不可 bypass、阻擋 merge）。跟 GHAS Push Protection 同類、但跨 SCM、且 detector pool 來自同一個 GitGuardian backend、跟 dashboard incident 走同一條 lineage。</p>
<p><strong>Historical scan</strong>：onboarding 第一次跑 <em>full git history scan</em>、回填過去所有 commit 的 leak。意義是 <em>已 leaked 多年的 secret 被找出來、強制 rotation</em>。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation</a> 的場景：CI 環境 compromise 後、historical scan 在數小時內找出 CI 環境曾接觸過的所有 secret、配合 secret store API 自動 rotation。</p>
<p><strong>Honeytokens</strong>：散佈假 AWS / Stripe / GitHub token 到 repo 角落 / Confluence page / internal doc、attacker 拿到後試用會觸發 alert。是 <em>早期偵測 unauthorized access</em> 的工程化做法、不依賴 detection model 抓 attacker behavior、而是讓 attacker 自己 trigger 自己。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain</a> — attacker 拿到 OAuth token 後試 GitHub API、honeytoken 在 attacker map 環境時就 trigger。</p>
<p><strong>Rotation 整合</strong>：detect 完不是工作結束、要 <em>rotate the secret</em>。GitGuardian 自身不存 secret，rotation 走 webhook / API 拉 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / AWS Secrets Manager / Azure Key Vault 觸發 rotation workflow。意義是 <em>偵測跟 remediation 解耦</em>、但需要組織側先把 secret store 接好、否則 incident 只能 manual rotation。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>GitGuardian</th>
          <th>GHAS Secret Scanning</th>
          <th>Gitleaks (OSS)</th>
          <th>TruffleHog</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>SaaS（self-hosted 有 enterprise tier）</td>
          <td>GitHub-only（SaaS / GHES）</td>
          <td>Self-hosted CLI / CI</td>
          <td>Self-hosted CLI / CI / SaaS tier</td>
      </tr>
      <tr>
          <td>Source 範圍</td>
          <td>GitHub / GitLab / Bitbucket / ADO / SaaS</td>
          <td>GitHub repo only</td>
          <td>Git repo（任何 host）</td>
          <td>Git / S3 / Docker / 多 source</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>內建、350+ detector live verify</td>
          <td>Partner pattern validation（部分）</td>
          <td>無（regex match only）</td>
          <td>有（verified mode）</td>
      </tr>
      <tr>
          <td>Push 攔截</td>
          <td>ggshield pre-commit + CI</td>
          <td>Push Protection（server-side、強制）</td>
          <td>pre-commit hook</td>
          <td>pre-commit hook</td>
      </tr>
      <tr>
          <td>Incident workflow</td>
          <td>內建 Incident + assign + dashboard</td>
          <td>GitHub Alert + Dependabot-like UI</td>
          <td>無（自接 SIEM）</td>
          <td>SaaS tier 有、OSS 無</td>
      </tr>
      <tr>
          <td>Honeytokens</td>
          <td>內建</td>
          <td>無</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Per developer / contributor（年訂）</td>
          <td>Per active committer（GitHub Enterprise）</td>
          <td>免費</td>
          <td>OSS 免費、SaaS 按 contributor</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>跨 SCM + SaaS、要 workflow + honeytoken</td>
          <td>GitHub-only + 已買 GHAS</td>
          <td>預算敏感 + 自建 IR pipeline</td>
          <td>多 source（含 S3 / Docker）、OSS-friendly</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中 — Incident 歷史在 vendor、detector 通用</td>
          <td>中 — 綁 GitHub UI、export 有限</td>
          <td>低 — 規則自有</td>
          <td>低 — 規則自有</td>
      </tr>
  </tbody>
</table>
<p>選 GitGuardian 的核心訴求：<em>跨 SCM + SaaS coverage + 內建 incident workflow + honeytoken</em>、且能投入 per-developer 訂閱（大型公司 contributor 數會放大成本）+ 有 SecOps 跟 developer 分工承接 incident。GitHub-only 環境且已買 GHAS、重疊不必要、直接用 GHAS；預算敏感且自家有 IR pipeline、走 <a href="/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &#43; regex &#43; entropy、SARIF output、跨 SCM、pre-commit &#43; CI 友善">Gitleaks</a> 或 TruffleHog OSS。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Honeytokens 散佈策略</strong>：honeytoken 的效果取決於 <em>放在哪裡 + 看起來多真</em>。放 repo README、Confluence runbook、Slack <code>#engineering</code> 過期附件、舊 backup script — attacker reconnaissance 會優先看的地方。token 的命名要跟組織 naming convention 一致（<code>prod-db-readonly-2024</code>）、避免一看就是假的。每個 honeytoken 配 unique ID、trigger 時能定位 <em>attacker 從哪個位置拿到</em>、反推 leak surface。</p>
<p><strong>Validation endpoint 的 trade-off</strong>：validation 是 FP 降噪核心、但代價是 <em>GitGuardian 會 call 你的 cloud account</em>。AWS key 命中時 GitGuardian 從自家 IP call <code>sts:GetCallerIdentity</code>、log 留在你的 CloudTrail。Onboarding 要 <em>把 GitGuardian IP range allowlist 進 SIEM whitelist</em>、避免被自家 detection 誤判為 unauthorized access；同時要評估 vendor trust — 2020 年 GitGuardian 自家 source code 透過第三方 SaaS leak、提醒 vendor 不是 detection-only 的零信任邊界。</p>
<p><strong>IR workflow 整合</strong>：Incident 不應該停在 GitGuardian dashboard、要 routing 到組織既有 IR tooling — PagerDuty for on-call、Slack channel for SecOps、Jira ticket for tracking。Webhook 是標準做法、payload 含 incident metadata + validation status、由組織側決定升級邏輯（valid + prod scope → PagerDuty page；invalid + dev scope → Slack info）。</p>
<p><strong>Historical scan + scope map</strong>：偵測到 leaked secret 後、要回答 <em>這個 secret 還在哪裡用</em>。GitGuardian 的 historical scan 找出 <em>所有 commit 提到該 pattern 的位置</em>、配合組織側 secret store 的 <em>who uses this secret</em> metadata、形成 scope map。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> — rotation 不能只換一個地方、要全 scope 一起換、否則舊 secret 還在某個 service 用、rotation 沒生效。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Incident volume 爆炸 / developer 看不完</strong>：generic high-entropy detector 全開 + 沒 assign 到 developer — 縮 generic detector scope、incident 走 assign-to-author、SecOps 只 review escalated</li>
<li><strong>Valid incident rotation 慢 / SLA 跑掉</strong>：沒接 secret store rotation API、停在 manual rotation — 接 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / Secrets Manager webhook、自動觸發 rotation workflow</li>
<li><strong>Slack / Confluence 沒掃進來</strong>：以為 SCM-only 就夠 — 接 SaaS source connector、developer 貼 token 在 Slack DM 是常見 leak vector</li>
<li><strong>ggshield 被 bypass</strong>：pre-commit 在 developer 機器、可 <code>--no-verify</code> — 同步在 CI step 跑 ggshield、CI 不可 bypass、阻擋 merge</li>
<li><strong>Validation FP 不降</strong>：validation endpoint 沒開、或被 firewall 擋 — 確認 GitGuardian IP range 在 cloud account allowlist、validation status 是 <em>Valid</em> 不是 <em>Unknown</em></li>
<li><strong>Honeytoken 沒 trigger / 假警報</strong>：token 放錯位置（attacker 不會看的 deep nested folder）或命名一看就假 — 散佈到 reconnaissance hot spot、命名跟組織 convention 一致</li>
<li><strong>Per-developer 計費暴衝</strong>：contractor / bot account 也算 developer — review billing report、排除 service account / read-only viewer、跟 vendor 談 contributor 定義</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitHub-only + 已買 GHAS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a></td>
      </tr>
      <tr>
          <td>預算敏感 + 自建 IR pipeline</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &#43; regex &#43; entropy、SARIF output、跨 SCM、pre-commit &#43; CI 友善">Gitleaks</a> OSS / TruffleHog OSS</td>
      </tr>
      <tr>
          <td>多 source（S3 / Docker image）</td>
          <td>TruffleHog（覆蓋更多 non-Git source）</td>
      </tr>
      <tr>
          <td>Secret store / rotation</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> / AWS Secrets Manager / Azure Key Vault</td>
      </tr>
      <tr>
          <td>SIEM correlation / cross-source</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a></td>
      </tr>
      <tr>
          <td>Supply chain build provenance</td>
          <td><a href="https://docs.sigstore.dev/">Sigstore / SLSA vendor 群</a>（同 vendor 章）</td>
      </tr>
      <tr>
          <td>Incident routing</td>
          <td><a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ggshield 完整 CLI flag reference 跟 custom detector YAML schema</li>
<li>GitGuardian Internal Monitoring (self-hosted enterprise) 的部署架構細節</li>
<li>Honeytoken 在 active deception / canary token 廣義生態的位置（屬 deception engineering、不在本頁）</li>
<li>Detector pattern 的 regex / entropy 細節（屬 detection engineering）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>GitGuardian 在 07 案例庫沒有直接 vendor-level 事件、但所有 secret leak / supply chain case 都是它的偵測對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 GitGuardian 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation</a></td>
          <td>Historical scan 在 CI compromise 後數小時內找出 CI 環境曾接觸過的所有 secret、配合 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 自動 rotation、不是 console manual rotation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain</a></td>
          <td>Honeytokens 散佈在 repo 跟 Confluence、attacker 拿到 OAuth token 後試 GitHub API 時 trigger、不靠 detection model 抓 attacker behavior</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>GitGuardian historical scan 找出 leaked secret 的 <em>scope map</em>（哪些 service 共用同一個 secret）、配合 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 分域 rotation 才完整</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020 Sunburst</a></td>
          <td>secret scanning 抓不到 build pipeline 內 malicious code 注入、要靠 <a href="https://docs.sigstore.dev/">Sigstore / SLSA</a> provenance；secret scanning 是覆蓋一段、不是全部</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.x Secrets Management at Scale</a>、<a href="/blog/backend/07-security-data-protection/security-as-risk-routing-system/" data-link-title="7.15 資安作為風險路由系統" data-link-desc="建立資安作為風險路由系統的導讀大綱，串接問題節點、控制面與跨模組交接">Security Workflow as Code</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a>、<a href="/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &#43; regex &#43; entropy、SARIF output、跨 SCM、pre-commit &#43; CI 友善">Gitleaks</a>、TruffleHog</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（rotation 接點）、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>（Incident webhook → SIEM）、<a href="https://docs.sigstore.dev/">Sigstore</a>（build provenance 覆蓋 secret scanning 抓不到的段）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Incident → IR routing）</li>
<li>官方：<a href="https://docs.gitguardian.com/">GitGuardian Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/</guid><description>&lt;p>這個案例的核心責任是說明「行動支付類 SaaS」的訊息工作負載特性。PayPay 是日本最大行動支付（pre-IPO 估值 70 億美金級）、訊息功能需要在每筆交易後即時通知（付款成功、收款、優惠券）、單一用戶每天可能收到數十條訊息、加總到平台級別就是每日上億訊息。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>PayPay 在 DynamoDB 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers&lt;/a>）：&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>每日訊息量&lt;/td>
 &lt;td>3 億訊息&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要工作負載&lt;/td>
 &lt;td>行動支付通知 + 訊息功能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可靠性敘述&lt;/td>
 &lt;td>「Super reliable and performed consistently」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>Amazon DynamoDB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>日本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>PayPay 案例揭露三個行動支付訊息系統的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>支付通知是「不可丟失 + 不可延遲」雙重需求&lt;/strong>：用戶付完款 30 秒沒收到通知會懷疑系統壞了、會打客服 / 重複扣款。這層需求比 OTA 推播嚴格、必須有 durable queue + retry + 重複偵測。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> 的 idempotency 設計。&lt;/li>
&lt;li>&lt;strong>DynamoDB 在「訊息事件」這類負載特別適合&lt;/strong>：每則訊息有獨立 message_id（partition key 天然均勻）、TTL 機制可以自動清理過期訊息（避免 storage 爆炸）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 的 partition 均勻優勢、跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary&lt;/a> 的 TTL 議題。&lt;/li>
&lt;li>&lt;strong>3 億 / 天 ≈ 3,500 訊息 / 秒平均&lt;/strong>：聽起來不大、但這是 &lt;em>平均&lt;/em>。月底、雙 11 類大促、新年紅包等場景、單秒峰值可能達 10x-50x。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling&lt;/a> 的峰均比評估。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「super reliable」是行銷語言、不是工程承諾。讀此類短篇案例要把行銷敘述折扣、重點看 &lt;em>服務組合&lt;/em> 與 &lt;em>規模量級&lt;/em>。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>訊息系統設計區分「通知」跟「訊息」&lt;/strong>：通知（payment received）是 transactional、不可丟失；訊息（marketing）可以丟失部分、重點是 throughput。兩者用不同 SLO、不同 storage。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> 的訊息分類。&lt;/li>
&lt;li>&lt;strong>TTL 自動清理避免 storage 成本爆炸&lt;/strong>：3 億 / 天 × 30 天 = 90 億筆記錄、不清理會撐死 storage 預算。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 TTL 設計。&lt;/li>
&lt;li>&lt;strong>訊息推送的下游（APNs、FCM、SMS gateway）是隱性瓶頸&lt;/strong>：DynamoDB 寫入可以撐 3K msg/sec、但 APNs 一天的 quota 是有限的。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a> 的依賴鏈分析。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP Firestore + Cloud Messaging、Azure Cosmos DB + Notification Hubs 都是對等架構。差異是 vendor 整合度跟全球分發能力。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計行動支付訊息 → &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a>&lt;/li>
&lt;li>對照其他 KV 高吞吐 → &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom&lt;/a>&lt;/li>
&lt;li>想做訊息系統容量規劃 → &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling&lt;/a>&lt;/li>
&lt;li>想避免訊息熱點打爆單一 partition → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &amp;#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式&lt;/a>&lt;/li>
&lt;li>想評估訊息系統的 capacity mode → &lt;a href="https://tarrragon.github.io/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 on-demand vs provisioned&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/solutions/case-studies/paypay/">PayPay on AWS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「行動支付類 SaaS」的訊息工作負載特性。PayPay 是日本最大行動支付（pre-IPO 估值 70 億美金級）、訊息功能需要在每筆交易後即時通知（付款成功、收款、優惠券）、單一用戶每天可能收到數十條訊息、加總到平台級別就是每日上億訊息。</p>
<h2 id="觀察">觀察</h2>
<p>PayPay 在 DynamoDB 的關鍵敘述（引自 <a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每日訊息量</td>
          <td>3 億訊息</td>
      </tr>
      <tr>
          <td>主要工作負載</td>
          <td>行動支付通知 + 訊息功能</td>
      </tr>
      <tr>
          <td>可靠性敘述</td>
          <td>「Super reliable and performed consistently」</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>Amazon DynamoDB</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>日本</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<p>PayPay 案例揭露三個行動支付訊息系統的工程重點。</p>
<ol>
<li><strong>支付通知是「不可丟失 + 不可延遲」雙重需求</strong>：用戶付完款 30 秒沒收到通知會懷疑系統壞了、會打客服 / 重複扣款。這層需求比 OTA 推播嚴格、必須有 durable queue + retry + 重複偵測。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 idempotency 設計。</li>
<li><strong>DynamoDB 在「訊息事件」這類負載特別適合</strong>：每則訊息有獨立 message_id（partition key 天然均勻）、TTL 機制可以自動清理過期訊息（避免 storage 爆炸）。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 的 partition 均勻優勢、跟 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a> 的 TTL 議題。</li>
<li><strong>3 億 / 天 ≈ 3,500 訊息 / 秒平均</strong>：聽起來不大、但這是 <em>平均</em>。月底、雙 11 類大促、新年紅包等場景、單秒峰值可能達 10x-50x。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a> 的峰均比評估。</li>
</ol>
<p>需要警惕：「super reliable」是行銷語言、不是工程承諾。讀此類短篇案例要把行銷敘述折扣、重點看 <em>服務組合</em> 與 <em>規模量級</em>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>訊息系統設計區分「通知」跟「訊息」</strong>：通知（payment received）是 transactional、不可丟失；訊息（marketing）可以丟失部分、重點是 throughput。兩者用不同 SLO、不同 storage。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的訊息分類。</li>
<li><strong>TTL 自動清理避免 storage 成本爆炸</strong>：3 億 / 天 × 30 天 = 90 億筆記錄、不清理會撐死 storage 預算。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 TTL 設計。</li>
<li><strong>訊息推送的下游（APNs、FCM、SMS gateway）是隱性瓶頸</strong>：DynamoDB 寫入可以撐 3K msg/sec、但 APNs 一天的 quota 是有限的。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的依賴鏈分析。</li>
</ol>
<p>跨平台等效：GCP Firestore + Cloud Messaging、Azure Cosmos DB + Notification Hubs 都是對等架構。差異是 vendor 整合度跟全球分發能力。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計行動支付訊息 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
<li>對照其他 KV 高吞吐 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> / <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></li>
<li>想做訊息系統容量規劃 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.2 Workload Modeling</a></li>
<li>想避免訊息熱點打爆單一 partition → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a></li>
<li>想評估訊息系統的 capacity mode → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/paypay/">PayPay on AWS</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL → PlanetScale：managed Vitess + branch-based schema workflow 的 hybrid shift</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> 跟 PlanetScale。走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type E paradigm shift 結構。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>自管 MySQL&lt;/th>
 &lt;th>PlanetScale&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Sharding&lt;/td>
 &lt;td>自己配 Vitess 或不 shard&lt;/td>
 &lt;td>Vitess 透明（即使單 keyspace 也走 Vitess）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema migration&lt;/td>
 &lt;td>gh-ost / pt-osc 跑 ALTER&lt;/td>
 &lt;td>&lt;strong>Branch + Deploy Request&lt;/strong> workflow&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failover&lt;/td>
 &lt;td>Orchestrator 自管&lt;/td>
 &lt;td>PlanetScale 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Branching&lt;/td>
 &lt;td>不存在概念&lt;/td>
 &lt;td>&lt;strong>DB branch（git-like）+ revert&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection limit&lt;/td>
 &lt;td>max_connections 自己設&lt;/td>
 &lt;td>PlanetScale connection pool / per-plan limit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Foreign key&lt;/td>
 &lt;td>支援&lt;/td>
 &lt;td>有限支援（Vitess 18+ / 2023 起、需明確啟用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>SUPER&lt;/code> privilege&lt;/td>
 &lt;td>自己有&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-region&lt;/td>
 &lt;td>自己配 binlog ship&lt;/td>
 &lt;td>PlanetScale 內建（Boost feature）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Per-month cost&lt;/td>
 &lt;td>EC2 + EBS + ops&lt;/td>
 &lt;td>per-row-read + per-row-written + storage&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>從 &lt;em>application 連線&lt;/em> 視角：跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">Aurora MySQL migration&lt;/a> 一樣低、connection string 換就完事。從 &lt;em>schema management&lt;/em> 視角：PlanetScale 強推 &lt;em>branch-based workflow&lt;/em> — 改 schema 不再是「跑 gh-ost」、是「開 branch → Deploy Request → review → merge」。整個 schema change 工作流跟 git 同型、跟 application code review 同 workflow。&lt;/p>
&lt;p>這是 &lt;em>workflow + schema-tooling shift&lt;/em> — Aurora 是「同 workflow + managed」、PlanetScale 是「同 protocol + 不同 schema workflow + branch tooling」。Database paradigm（OLTP relational）跟 application change 都 Low、主要 shift 在 DBA / dev 操作介面。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> 跟 PlanetScale。走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type E paradigm shift 結構。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自管 MySQL</th>
          <th>PlanetScale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sharding</td>
          <td>自己配 Vitess 或不 shard</td>
          <td>Vitess 透明（即使單 keyspace 也走 Vitess）</td>
      </tr>
      <tr>
          <td>Schema migration</td>
          <td>gh-ost / pt-osc 跑 ALTER</td>
          <td><strong>Branch + Deploy Request</strong> workflow</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>Orchestrator 自管</td>
          <td>PlanetScale 自動</td>
      </tr>
      <tr>
          <td>Branching</td>
          <td>不存在概念</td>
          <td><strong>DB branch（git-like）+ revert</strong></td>
      </tr>
      <tr>
          <td>Connection limit</td>
          <td>max_connections 自己設</td>
          <td>PlanetScale connection pool / per-plan limit</td>
      </tr>
      <tr>
          <td>Foreign key</td>
          <td>支援</td>
          <td>有限支援（Vitess 18+ / 2023 起、需明確啟用）</td>
      </tr>
      <tr>
          <td><code>SUPER</code> privilege</td>
          <td>自己有</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>自己配 binlog ship</td>
          <td>PlanetScale 內建（Boost feature）</td>
      </tr>
      <tr>
          <td>Per-month cost</td>
          <td>EC2 + EBS + ops</td>
          <td>per-row-read + per-row-written + storage</td>
      </tr>
  </tbody>
</table>
<p>從 <em>application 連線</em> 視角：跟 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">Aurora MySQL migration</a> 一樣低、connection string 換就完事。從 <em>schema management</em> 視角：PlanetScale 強推 <em>branch-based workflow</em> — 改 schema 不再是「跑 gh-ost」、是「開 branch → Deploy Request → review → merge」。整個 schema change 工作流跟 git 同型、跟 application code review 同 workflow。</p>
<p>這是 <em>workflow + schema-tooling shift</em> — Aurora 是「同 workflow + managed」、PlanetScale 是「同 protocol + 不同 schema workflow + branch tooling」。Database paradigm（OLTP relational）跟 application change 都 Low、主要 shift 在 DBA / dev 操作介面。</p>
<h2 id="為什麼是-type-eparadigm--operational--schema-多軸">為什麼是 Type E（Paradigm + Operational + Schema 多軸）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#%e5%af%ab%e5%89%8d%e7%9a%84-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Medium-High</td>
          <td>MySQL wire protocol 一致、FK 有限支援（Vitess 18+）、部分 INSTANT DDL 行為差</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High</td>
          <td>branch lifecycle、Deploy Request workflow、connection pooler 不同</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>High</td>
          <td>branch-based schema management、跟自管 gh-ost / pt-osc 思維完全不同</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Medium</td>
          <td>PlanetScale CLI / Console / API / connection pooler 都進團隊工具</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Low</td>
          <td>connection string + 移除 FK 約束</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low-Medium</td>
          <td>Vitess 透明 sharding 即使單 keyspace</td>
      </tr>
  </tbody>
</table>
<p>Paradigm + Operational + Schema 三軸 High。按優先序 Schema &gt; Paradigm &gt; Operational、預設選 Type A。但 <em>讀者最關心</em> 的是 schema workflow paradigm 轉變、不是 schema field translation — Type E 結構更貼合「不收斂、部分 adopt」的真實 migration 流程。</p>
<p>→ <strong>Type E paradigm shift</strong>、4-phase partial migration（多數 org 停 Phase 2-3 hybrid）。</p>
<h2 id="driverbranch-based-workflow--vitess-transparent-sharding--zero-dba">Driver：Branch-based workflow + Vitess transparent sharding + zero DBA</h2>
<p>從自管 MySQL 遷 PlanetScale 的核心 driver 有三條：</p>
<p><strong>Branch-based schema workflow</strong>：</p>
<ul>
<li>改 schema 開 branch（<code>pscale branch create</code>）、在 branch 上跑 ALTER、跑 application code 改、merge 進 main 前 Deploy Request review</li>
<li>Deploy Request 顯示 schema diff、跟 GitHub PR 同概念</li>
<li>Merge 後 PlanetScale 自動跑 <em>no-downtime schema migration</em>（內部 VReplication）</li>
<li>出問題可 <em>revert</em>（48 小時內、用 Vitess VReplication 反向 ship 資料）</li>
</ul>
<p>這條 workflow 對 <em>developer ergonomic</em> 拉力大 — schema change 不再是「DBA 工作」、是「dev 自己處理、跟 code review 同流程」。</p>
<p><strong>Vitess transparent sharding</strong>：</p>
<ul>
<li>PlanetScale 強制每個 cluster 走 Vitess（即使單 keyspace 看似 unsharded）</li>
<li>寫吞吐成長到需要 shard 時、加 shard 是 PlanetScale internal 操作、application 看不到</li>
<li>不用養 Vitess SRE 團隊</li>
</ul>
<p><strong>Zero DBA</strong>：</p>
<ul>
<li>PlanetScale 接管所有 ops（failover / backup / parameter / scaling）</li>
<li>跟 Aurora 同等級「managed」、加上 branch workflow</li>
</ul>
<p>FK 處理：早期 Vitess（&lt; 18）不支援 FK、PlanetScale 對應期間建議全 drop FK + 改 application enforcement。Vitess 18（2023 末）後加 FK 支援、PlanetScale 在合適 plan 內可啟用、但 <em>cross-shard FK</em> 仍受限。Phase 1 audit 重點不再是「全 drop FK」、而是「驗證 FK 行為（特別 cascade / cross-shard）跟自管 MySQL 預期一致」。</p>
<h2 id="4-phase-partial-migration不收斂">4-phase partial migration（不收斂）</h2>
<h3 id="phase-1fk-行為驗證--schema-auditplanetscale-shadow-cluster-起來">Phase 1：FK 行為驗證 + schema audit、PlanetScale shadow cluster 起來</h3>
<p>第一步是 <em>FK 行為驗證</em> + schema layout audit。Vitess 18+ / PlanetScale 已支援 FK、但行為跟自管 MySQL 有差異：</p>
<ul>
<li>列所有 FK：<code>SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME IS NOT NULL</code></li>
<li>對每個 FK 評估：
<ul>
<li><em>Cross-shard FK</em>：PlanetScale 不允許 FK 跨 shard、parent 跟 child 必須同 shard（透過 Vindex 設計）</li>
<li><em>Cascade 行為</em>：cross-shard DELETE cascade 在 PlanetScale 不執行、改 application 層處理</li>
<li><em>Native FK 啟用 vs application enforcement</em>：依 Vitess 18+ 行為決定保留 FK 或改 app-level</li>
</ul>
</li>
<li><em>PlanetScale shadow cluster</em> 起來、跑 application schema、用 Vitess Connector 從自管 binlog ship 資料</li>
</ul>
<p>工作主要塊：</p>
<ul>
<li>FK 行為 audit + 改 cross-shard cascade（依 FK 數量、weeks 工作量）</li>
<li>Schema dump → PlanetScale import（用 <code>pscale shell</code>）</li>
<li>Vitess Connector 設定 binlog stream</li>
</ul>
<p>完成標準：PlanetScale shadow cluster 有完整 production schema、cross-shard FK 已處理、binlog stream lag &lt; 1 秒。</p>
<h3 id="phase-2read-traffic-切-planetscale">Phase 2：Read traffic 切 PlanetScale</h3>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">Aurora migration</a> Phase 2 同概念：read query 切 PlanetScale connection string、寫入仍自管 MySQL。</p>
<p>差異：</p>
<ul>
<li>PlanetScale connection 有 <em>per-plan rate limit</em>（Scaler Plan: 10K connections、Enterprise: 100K）</li>
<li>必須走 <em>PlanetScale connection pool</em>（不是直接連、有 SSL handshake overhead）</li>
<li>監控 <code>pscale_io_read_query_throttled_total</code> 確認沒撞 plan limit</li>
</ul>
<p>跑 2-4 週、確認：</p>
<ul>
<li>PlanetScale read latency 跟自管 replica latency 接近（PlanetScale Boost cache 可能比自管快）</li>
<li>Vitess Connector stream 穩定</li>
<li>Application 對 PlanetScale row read 量符合 cost forecast</li>
</ul>
<h3 id="phase-3schema-workflow-切-planetscale--write-cutover">Phase 3：Schema workflow 切 PlanetScale + write cutover</h3>
<p>關鍵 paradigm shift：<em>停 gh-ost / pt-osc</em>、改用 PlanetScale branch workflow。</p>
<p>訓練步驟：</p>
<ol>
<li><em>第一個 small schema change</em> 用 PlanetScale branch + Deploy Request 跑</li>
<li>開發團隊熟悉 <code>pscale branch create</code> / <code>pscale deploy-request create</code> CLI</li>
<li>CI integration：把 PlanetScale CLI 加進 deploy pipeline</li>
<li>退役 gh-ost / pt-osc CI integration</li>
</ol>
<p>完成 schema workflow 訓練後 write cutover：</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. PlanetScale 把 shadow cluster promote 為 primary（用 PlanetScale console / API）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 透過 PlanetScale Console 啟用 production write 或用 `pscale` CLI 對應 promotion 命令</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># （CLI 命令名稱隨 pscale 版本變動、以 pscale --help 為準）</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="c1"># 2. Application connection string 切 PlanetScale writer</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 自管 → mysql://primary.example.com:3306/production</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># PlanetScale → mysql://...@xxx.connect.psdb.cloud/production?sslaccept=strict</span>
</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="c1"># 3. Vitess Connector 反向（PlanetScale → 自管）作為 rollback insurance</span></span></span></code></pre></div><p>完成標準：寫入流量 100% 進 PlanetScale、自管 MySQL 接 PlanetScale binlog（rollback buffer）。</p>
<h3 id="phase-4自管-mysql-退役--保留作-rollback-buffer">Phase 4：自管 MySQL 退役 / 保留作 rollback buffer</h3>
<p>跟 Aurora migration Phase 4 同模式：</p>
<ul>
<li>自管保留 30-90 天作 cold buffer</li>
<li>確認 PlanetScale cost forecast 跟 actual 一致（per-row read / write 計費可能超預期）</li>
<li>確認 branch workflow 在 production team 內 adopt（不是「PlanetScale 在用、但團隊還是用 gh-ost on staging」這種 stuck 狀態）</li>
</ul>
<p>多數 org 在 <em>Phase 3</em> 停留更久（半年-一年）— Vitess Connector 反向 binlog ship 是穩定 rollback path、Phase 4 不急。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-cross-shard-fk--planetscale-跟-native-mysql-行為不同">1. Cross-shard FK — PlanetScale 跟 native MySQL 行為不同</h3>
<p>Vitess 18+ / PlanetScale 已支援 FK、但 <em>cross-shard cascade</em> 不執行。同 shard 內 FK 跟 native MySQL 一致；parent 跟 child 跨 shard 時、<code>ON DELETE CASCADE</code> 在 PlanetScale 不會跨 shard 觸發 child delete、結果 application 看到 <em>orphan row</em>。</p>
<p>修法：</p>
<ul>
<li>Phase 1 audit 出哪些 FK 跨 shard（Vindex 設計決定 parent / child 是否同 shard）</li>
<li>同 shard FK：直接保留、行為跟自管 MySQL 一致</li>
<li>Cross-shard cascade：改 application 層 transaction 內 explicit DELETE child、或 <em>background reconciliation job</em>（定期掃 orphan）</li>
<li>把 <em>parent / child 強制同 shard</em>（用相同 Vindex column）是預防 cross-shard FK 議題的根本解</li>
</ul>
<h3 id="2-deploy-request-思維轉換不到位--團隊仍用跑-alter心智模型">2. Deploy Request 思維轉換不到位 — 團隊仍用「跑 ALTER」心智模型</h3>
<p>DBA / SRE 習慣 <em>直接連 PlanetScale 跑 ALTER</em> —但 PlanetScale 在 production branch 上 <em>禁止 DDL</em>（必須走 Deploy Request）。失敗訊息 <em>not actionable</em>（ERROR: not authorized）、DBA 找不到原因、production maintenance 卡住。</p>
<p>修法：</p>
<ul>
<li>Phase 3 <em>訓練步驟</em> 不能跳：找一個 small schema change 在 staging 走完整 branch workflow、團隊每個 DBA / SRE 都 hands-on 過</li>
<li>在 ops runbook 寫明 <em>production schema change must go through Deploy Request</em>、列 CLI 命令模板</li>
<li>緊急 schema change（事故中）也走 branch + Deploy Request、PlanetScale 可加速 Deploy（不能 bypass workflow）</li>
</ul>
<h3 id="3-schema-diff-邊界--planetscale-看不到-application-level-insert-changes">3. Schema diff 邊界 — PlanetScale 看不到 application-level INSERT changes</h3>
<p>Deploy Request 顯示 <em>schema-level diff</em>（CREATE / ALTER / DROP）、不顯示 <em>data diff</em>。如果 branch 上有 INSERT 進去（測試資料 / seed data）、merge 進 main 時 <em>資料不會搬</em>（只搬 schema）、application 預期有資料但 production 沒。</p>
<p>修法：</p>
<ul>
<li>把 <em>seed data INSERT</em> 放 application migration / fixture、不在 PlanetScale branch 內</li>
<li>用 PlanetScale CLI <em>export branch data</em> 跟 <em>import to main</em>（手動操作）作為 escape hatch</li>
<li>教育團隊：PlanetScale branch = <em>schema branch</em>、不是 git-like <em>data branch</em></li>
</ul>
<h3 id="4-branch-lifecycle-ops-cost--100-個-stale-branch">4. Branch lifecycle ops cost — 100 個 stale branch</h3>
<p>每個 PR 都開一個 PlanetScale branch、PR merge 後忘記刪、累積 100 個 stale branch。每個 branch 佔 storage cost、PlanetScale plan limit 也限制 branch 數量。</p>
<p>修法：</p>
<ul>
<li>CI integration：PR close 自動 <code>pscale branch delete &lt;branch-name&gt;</code></li>
<li>設 <em>branch retention policy</em>（30 天無活動自動刪）</li>
<li>監控 <code>pscale branch list | wc -l</code> 數量、超 threshold alert</li>
<li>把 branch lifecycle 寫進 <em>team playbook</em>（不是 PlanetScale 教、是團隊內部規範）</li>
</ul>
<h3 id="5-無-super-privilege--部分操作不可行">5. 無 <code>SUPER</code> privilege — 部分操作不可行</h3>
<p>PlanetScale connection 拿到的 MySQL user 沒有 <code>SUPER</code> privilege。需要 <code>SUPER</code> 的操作直接失敗：</p>
<ul>
<li><code>SET GLOBAL</code>（不能改 runtime variable）</li>
<li><code>KILL</code> 別人的 query（PlanetScale console 提供 alt 介面）</li>
<li><code>SHOW MASTER STATUS</code> / <code>SHOW SLAVE STATUS</code>（PlanetScale 抽象掉、不暴露）</li>
<li><code>INSTALL PLUGIN</code>（managed、不允許）</li>
<li><code>STOP SLAVE</code> / <code>START SLAVE</code>（Vitess 內部）</li>
</ul>
<p>修法：</p>
<ul>
<li>評估 application 跟 ops tool 是否依賴 <code>SUPER</code> privilege</li>
<li>改用 PlanetScale console / API 等價操作</li>
<li>部分監控 query（<code>SHOW SLAVE STATUS</code>）用 <em>PlanetScale 內建 dashboard</em> 代替</li>
</ul>
<h2 id="schema-translation-主要工作量塊">Schema translation 主要工作量塊</h2>
<p>雖然 Type E 結構不以 schema translation 為主、但 schema diff 在 Phase 1 仍佔多數時間：</p>
<table>
  <thead>
      <tr>
          <th>自管 MySQL</th>
          <th>PlanetScale (Vitess)</th>
          <th>翻譯難度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FOREIGN KEY constraint</td>
          <td>（無）+ application enforcement</td>
          <td>高</td>
      </tr>
      <tr>
          <td>INSTANT DDL</td>
          <td>部分支援、其他走 Vitess online DDL</td>
          <td>低-中</td>
      </tr>
      <tr>
          <td>Stored procedure</td>
          <td>支援</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Trigger</td>
          <td>支援</td>
          <td>低</td>
      </tr>
      <tr>
          <td>User-defined function</td>
          <td>受限</td>
          <td>中</td>
      </tr>
      <tr>
          <td>INSERT 跨表（CTE）</td>
          <td>支援</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Cross-shard JOIN</td>
          <td>必須用 Vindex（user_id 等 shard key 同表）</td>
          <td>中-高</td>
      </tr>
      <tr>
          <td><code>SUPER</code> 行為</td>
          <td>不支援</td>
          <td>中（ops tool 改）</td>
      </tr>
      <tr>
          <td><code>RELOAD</code> privilege</td>
          <td>不支援</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
<h2 id="容量與成本對照">容量與成本對照</h2>
<p>PlanetScale 計費 <em>很不同</em>：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>自管 MySQL（EC2）</th>
          <th>PlanetScale Scaler Pro</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Per-row read</td>
          <td>不計費</td>
          <td>按量計費、$1 per 1B row read</td>
      </tr>
      <tr>
          <td>Per-row written</td>
          <td>不計費</td>
          <td>按量計費、$1.50 per 1M row write</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>EBS、$0.10/GB-month</td>
          <td>$1.50/GB-month + replication overhead</td>
      </tr>
      <tr>
          <td>Connection limit</td>
          <td>max_connections 自己設</td>
          <td>per-plan limit、可加 Connection pooler</td>
      </tr>
      <tr>
          <td>Branch</td>
          <td>不適用</td>
          <td>每 branch 含 storage cost</td>
      </tr>
      <tr>
          <td>Boost cache</td>
          <td>不適用</td>
          <td>additional cost</td>
      </tr>
      <tr>
          <td>Ops headcount</td>
          <td>1-2 FTE</td>
          <td>&lt; 0.2 FTE</td>
      </tr>
  </tbody>
</table>
<p>PlanetScale 適合 <em>小-中規模 + high developer productivity priority</em>：</p>
<ul>
<li>流量 &lt; 10K WPS：cost 接近自管、developer productivity 顯著提升</li>
<li>流量 10-50K WPS：cost 開始貴、但 ops saving 仍大於 cost increase</li>
<li>流量 &gt; 100K WPS：PlanetScale Enterprise 議價、要 commit pricing</li>
</ul>
<p>對 high-traffic 場景 cost forecast 必須跑 <em>真實 workload trace</em> — PlanetScale 提供 <code>pscale analytics</code> 預估 read / write 量、用 production binlog replay 在 staging 跑、估算 row read / write 計費。</p>
<h2 id="何時不要遷">何時不要遷</h2>
<ul>
<li><strong>FK 是 application core constraint</strong>：cascade DELETE / SET NULL 廣泛使用、application 改不動</li>
<li><strong>大量 <code>SUPER</code>-required ops 自動化</strong>：DBA tools / monitoring 寫死 <code>SUPER</code>、改不動</li>
<li><strong>OS-level customization 需求</strong>：跟 Aurora 一樣、PlanetScale 完全 managed</li>
<li><strong>流量極大 + 預算敏感</strong>：&gt; 100K WPS row read 計費可能比 EC2 貴 5x、需要 Enterprise commit pricing</li>
<li><strong>跨雲 portability 是 requirement</strong>：PlanetScale 跑在自家 cloud（背後 AWS / GCP）、不像自管 Vitess 可跨雲</li>
</ul>
<h2 id="跟-aurora-mysql-對比同-batch-的選擇">跟 Aurora MySQL 對比（同 batch 的選擇）</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Aurora MySQL</th>
          <th>PlanetScale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type</td>
          <td>C operational hybrid</td>
          <td>E paradigm shift</td>
      </tr>
      <tr>
          <td>工作量主軸</td>
          <td>parameter group + IAM + endpoint</td>
          <td>FK audit + branch workflow</td>
      </tr>
      <tr>
          <td>Sharding</td>
          <td>不 shard、single-region scaling</td>
          <td>Vitess 透明 sharding</td>
      </tr>
      <tr>
          <td>Schema workflow</td>
          <td>仍用 gh-ost / pt-osc</td>
          <td>Branch + Deploy Request</td>
      </tr>
      <tr>
          <td>FK</td>
          <td>支援</td>
          <td>不支援</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>per-hour instance + per-GB storage</td>
          <td>per-row read / write + per-GB storage</td>
      </tr>
      <tr>
          <td>適合規模</td>
          <td>100 GB - 50 TB</td>
          <td>100 GB - 1 PB</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>AWS-only</td>
          <td>PlanetScale 背後 AWS / GCP</td>
      </tr>
  </tbody>
</table>
<p>選擇邏輯：</p>
<ul>
<li><em>AWS-heavy ecosystem + 不想 schema workflow paradigm shift</em> → Aurora</li>
<li><em>Developer-first culture + 想 branch-based schema workflow + 接受 FK 限制</em> → PlanetScale</li>
</ul>
<p>兩者不互斥、有 org 用 Aurora 給 OLTP core、PlanetScale 給 newer microservices（branch workflow 帶價值）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>平行 batch：→ Aurora MySQL migration playbook（同 batch、不同 paradigm）</li>
<li>上游：<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a> / <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding 設計</a></li>
<li>跨章節：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a> — Deploy Request workflow 對 release gate 的影響</li>
<li>既有 vendor 對照：<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 page</a> / <a href="https://planetscale.com/">PlanetScale 官方</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type E paradigm shift 結構說明）</li>
<li>官方：<a href="https://planetscale.com/docs/imports/migrate-from-mysql">PlanetScale Migration Guide</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Full-Text Search：tsvector / tsquery / GIN index 跟 pg_trgm fuzzy 三層搜尋</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/full-text-search/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/full-text-search/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>full-text search&lt;/em> — 內建 tsvector / tsquery + pg_trgm fuzzy match。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-fts-機制tsvector--tsquery--gin-index">PG FTS 機制：tsvector + tsquery + GIN index&lt;/h2>
&lt;p>PG 內建 full-text search 三件組：&lt;/p>
&lt;ul>
&lt;li>&lt;code>tsvector&lt;/code>：document 轉成 &lt;em>lexeme&lt;/em>（字根 + position）vector、normalized 後存&lt;/li>
&lt;li>&lt;code>tsquery&lt;/code>：搜尋字串 parse 成 query 形式&lt;/li>
&lt;li>GIN index：對 tsvector 加 inverted index&lt;/li>
&lt;/ul>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- Document
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsvector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;The quick brown fox jumps over the lazy dog&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 結果：&amp;#39;brown&amp;#39;:3 &amp;#39;dog&amp;#39;:9 &amp;#39;fox&amp;#39;:4 &amp;#39;jump&amp;#39;:5 &amp;#39;lazi&amp;#39;:8 &amp;#39;quick&amp;#39;:2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">-- The/over 是 stop word 被過濾、jumps/lazy 轉字根、保留 position
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsquery&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;fox &amp;amp; dog&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 結果：&amp;#39;fox&amp;#39; &amp;amp; &amp;#39;dog&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Match
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsvector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;The quick brown fox&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">@@&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsquery&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;fox &amp;amp; quick&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- → true&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Index&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">articles&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SERIAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- GIN index over tsvector (動態 cast)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_articles_fts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">articles&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">GIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">to_tsvector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Query 用 index
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">articles&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsvector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">@@&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">to_tsquery&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;english&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;postgres &amp;amp; index&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &amp;#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&amp;#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &amp;#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">JSONB GIN index&lt;/a> 同 GIN access method、不同 indexed expression。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>full-text search</em> — 內建 tsvector / tsquery + pg_trgm fuzzy match。</p></blockquote>
<hr>
<h2 id="pg-fts-機制tsvector--tsquery--gin-index">PG FTS 機制：tsvector + tsquery + GIN index</h2>
<p>PG 內建 full-text search 三件組：</p>
<ul>
<li><code>tsvector</code>：document 轉成 <em>lexeme</em>（字根 + position）vector、normalized 後存</li>
<li><code>tsquery</code>：搜尋字串 parse 成 query 形式</li>
<li>GIN index：對 tsvector 加 inverted index</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Document
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;The quick brown fox jumps over the lazy dog&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="c1">-- 結果：&#39;brown&#39;:3 &#39;dog&#39;:9 &#39;fox&#39;:4 &#39;jump&#39;:5 &#39;lazi&#39;:8 &#39;quick&#39;:2
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">-- The/over 是 stop word 被過濾、jumps/lazy 轉字根、保留 position
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- Query
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;fox &amp; dog&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- 結果：&#39;fox&#39; &amp; &#39;dog&#39;
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- Match
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;The quick brown fox&#39;</span><span class="p">)</span><span class="w"> </span><span class="o">@@</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;fox &amp; quick&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- → true</span></span></span></code></pre></div><p><strong>Index</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">title</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">body</span><span class="w"> </span><span class="nb">TEXT</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- GIN index over tsvector (動態 cast)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_articles_fts</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">articles</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">body</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- Query 用 index
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">articles</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">body</span><span class="p">)</span><span class="w"> </span><span class="o">@@</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;postgres &amp; index&#39;</span><span class="p">);</span></span></span></code></pre></div><p>跟 <a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">JSONB GIN index</a> 同 GIN access method、不同 indexed expression。</p>
<h2 id="generated-column-加速">Generated column 加速</h2>
<p>每次 query 都跑 <code>to_tsvector(...)</code> 浪費 CPU。用 <em>generated column</em> 預存：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="n">tsvector</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">coalesce</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">coalesce</span><span class="p">(</span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)))</span><span class="w"> </span><span class="n">STORED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_articles_fts</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">fts</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Query 簡化
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="o">@@</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;postgres&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Stored generated column 是 PG 12+、自動跟 row update 同步。</p>
<h2 id="ranking--加權">Ranking + 加權</h2>
<p>PG FTS 提供 <code>ts_rank</code> / <code>ts_rank_cd</code> 給結果排序：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 簡單 ranking
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">ts_rank</span><span class="p">(</span><span class="n">fts</span><span class="p">,</span><span class="w"> </span><span class="n">query</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">rank</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">articles</span><span class="p">,</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;postgres &amp; index&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="o">@@</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">rank</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>加權（A &gt; B &gt; C &gt; D）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Title 比 body 重要
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="o">=</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">coalesce</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)),</span><span class="w"> </span><span class="s1">&#39;A&#39;</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">coalesce</span><span class="p">(</span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)),</span><span class="w"> </span><span class="s1">&#39;B&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- Query 用加權 ranking
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">       </span><span class="n">ts_rank</span><span class="p">(</span><span class="n">fts</span><span class="p">,</span><span class="w"> </span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="mi">32</span><span class="w"> </span><span class="cm">/* normalize by document length */</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">rank</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">articles</span><span class="p">,</span><span class="w"> </span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">&#39;english&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;postgres&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="o">@@</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">rank</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><p><code>ts_rank</code> 第三 parameter 是 normalization flag：</p>
<ul>
<li>0：no normalization</li>
<li>1：divide by document length</li>
<li>32：divide by uniqueness（避免短 doc 一律 rank 高）</li>
</ul>
<h2 id="multi-language-support">Multi-language Support</h2>
<p>PG 內建多種語言 dictionary：<code>english</code> / <code>french</code> / <code>german</code> / <code>spanish</code> / <code>simple</code>（不做 stemming）等。</p>
<p>對 <em>中文 / 日文 / 韓文</em>、PG 預設無支援、需要 extension：</p>
<ul>
<li><code>zhparser</code>（中文、用 SCWS 分詞）</li>
<li><code>pgroonga</code>（多語言、支援中日韓）</li>
<li><code>RUM index</code>（PG 自己 + 可選 dictionary）</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 中文用 zhparser
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">zhparser</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">SEARCH</span><span class="w"> </span><span class="n">CONFIGURATION</span><span class="w"> </span><span class="n">chinese</span><span class="w"> </span><span class="p">(</span><span class="n">PARSER</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">zhparser</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">SEARCH</span><span class="w"> </span><span class="n">CONFIGURATION</span><span class="w"> </span><span class="n">chinese</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ADD</span><span class="w"> </span><span class="n">MAPPING</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="n">n</span><span class="p">,</span><span class="n">v</span><span class="p">,</span><span class="n">a</span><span class="p">,</span><span class="n">i</span><span class="p">,</span><span class="n">e</span><span class="p">,</span><span class="n">l</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="k">simple</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 使用
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">&#39;chinese&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;我愛 PostgreSQL 資料庫&#39;</span><span class="p">);</span></span></span></code></pre></div><p>對 <em>主要英文 search</em> 場景 PG built-in 夠用、對 <em>主要 CJK search</em> 需要 extension。</p>
<h2 id="pg_trgm--fuzzy-string-match">pg_trgm — Fuzzy String Match</h2>
<p>PG FTS 對 <em>精確字根 match</em> 強、對 <em>拼錯 / similar string</em> 弱。<code>pg_trgm</code> extension 提供 trigram-based fuzzy match：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_trgm</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="c1">-- 對 column 建 GIN trigram index
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_name_trgm</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">name</span><span class="w"> </span><span class="n">gin_trgm_ops</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- Fuzzy match（similarity threshold 預設 0.3）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="s1">&#39;jhon&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- → 找到 &#39;John&#39;、&#39;Johan&#39;、&#39;Johnny&#39; 等 similar string
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 顯式 similarity score
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">similarity</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;jhon&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">similarity</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;jhon&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li>Autocomplete / typeahead suggestion</li>
<li>拼錯容錯（user 輸入 typo）</li>
<li>ILIKE 加速（<code>name ILIKE '%jhon%'</code> 走 GIN trigram index）</li>
</ul>
<p>跟 FTS 互補：</p>
<ul>
<li>FTS：full document search、tokenize / stemming / ranking</li>
<li>pg_trgm：short string similarity、typo tolerance</li>
</ul>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-dictionary-選錯--中文搜不到">1. Dictionary 選錯 — 中文搜不到</h3>
<p>對中文 column 用 <code>to_tsvector('english', text)</code>、不分詞、整段當一個 token、搜不到任何結果。</p>
<p>修法：</p>
<ul>
<li>中文用 <code>zhparser</code> / <code>pgroonga</code></li>
<li>多語言 column 拆 <em>per-language column</em> 或用 <code>simple</code> dictionary（不 stemming、字元級 match）</li>
<li>確認 dictionary 選對：<code>SELECT to_tsvector('chinese', '...')</code> 看分詞結果</li>
</ul>
<h3 id="2-gin-vs-gist-取捨選錯">2. GIN vs GiST 取捨選錯</h3>
<p>PG FTS 有兩種 index access method：</p>
<ul>
<li><em>GIN</em>：read fast、write slow、size 大、適合 <em>read-heavy</em></li>
<li><em>GiST</em>：read 慢、write fast、size 小、適合 <em>write-heavy 或 small doc</em></li>
</ul>
<p>預設選 GIN、適合 90% search workload。對 <em>寫入頻繁 + 文件小</em> 場景 GiST。</p>
<p>修法：</p>
<ul>
<li>預設 GIN</li>
<li>寫吞吐 &gt; 10K WPS 場景考慮 GiST 或 <em>bulk index</em>（先 disable index、bulk insert、重建 index）</li>
<li>GIN 有 <code>fastupdate</code> option、buffering 加速寫入（trade-off：read 慢）</li>
</ul>
<h3 id="3-ranking-評分權重不對齊-business">3. Ranking 評分權重不對齊 business</h3>
<p><code>ts_rank</code> 預設不考慮 <em>field weight</em>、<code>ts_rank_cd</code> 考慮 cover density、兩者結果不同。Application 不知道 <em>自己 query 對應哪個 rank function</em>、結果隨機。</p>
<p>修法：</p>
<ul>
<li>顯式選 ranking function：<code>ts_rank</code> 一般用、<code>ts_rank_cd</code> 對 <em>proximity 重要</em> 場景</li>
<li>設 <em>field weight</em>（A &gt; B &gt; C &gt; D）反映 business priority（title &gt; body &gt; tags）</li>
<li>對 <em>搜尋結果</em> 用 A/B test 評估 ranking 質量、不靠直覺</li>
</ul>
<h3 id="4-multi-language-column-處理">4. Multi-language column 處理</h3>
<p>Application 同表存多語言 row（user-generated content、不同 language）、用單一 <code>to_tsvector('english', ...)</code> 對中文 row 搜不到、對 french row 也 stem 錯。</p>
<p>修法：</p>
<ul>
<li>
<p>加 <code>language</code> column 標每 row 語言</p>
</li>
<li>
<p>用 dynamic dictionary：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">articles</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">fts</span><span class="w"> </span><span class="n">tsvector</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">to_tsvector</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="k">language</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;zh&#39;</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="s1">&#39;chinese&#39;</span><span class="p">::</span><span class="n">regconfig</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">             </span><span class="k">WHEN</span><span class="w"> </span><span class="k">language</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;fr&#39;</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="s1">&#39;french&#39;</span><span class="p">::</span><span class="n">regconfig</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">             </span><span class="k">ELSE</span><span class="w"> </span><span class="s1">&#39;english&#39;</span><span class="p">::</span><span class="n">regconfig</span><span class="w"> </span><span class="k">END</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">        </span><span class="n">coalesce</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; &#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">coalesce</span><span class="p">(</span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">STORED</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>Query 時用對應語言 <code>to_tsquery</code></p>
</li>
</ul>
<h3 id="5-何時不該用-pg-fts--應該換-elasticsearch--opensearch">5. 何時不該用 PG FTS — 應該換 Elasticsearch / OpenSearch</h3>
<p>PG FTS 適合 <em>中小規模搜尋</em>、不適合：</p>
<ul>
<li><em>&gt; 100M document</em> high-QPS search</li>
<li>需要 <em>complex aggregation</em>（faceted search）</li>
<li>需要 <em>advanced ranking</em>（BM25 / learning to rank）</li>
<li>需要 <em>分散式 search</em>（PG FTS 是 single-node）</li>
<li>需要 <em>near-real-time indexing</em>（PG GIN update 較慢）</li>
</ul>
<p>對這些場景、用 Elasticsearch / OpenSearch / Meilisearch / Typesense 等專業 search engine。</p>
<p>PG FTS <em>優勢</em> 是 <em>跟 OLTP data 同 transaction</em> — 不需要 ETL 同步 search index、application 寫 PG 立即 searchable。對 application data + search 是 <em>同源</em> 的場景 PG FTS 比較適合。</p>
<h2 id="何時用-pg-fts">何時用 PG FTS</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application internal search（admin / dashboard）</td>
          <td>PG FTS</td>
      </tr>
      <tr>
          <td>&lt; 10M document、低 QPS（&lt; 100/s）</td>
          <td>PG FTS</td>
      </tr>
      <tr>
          <td>Search 跟 OLTP data 同 transaction needed</td>
          <td>PG FTS</td>
      </tr>
      <tr>
          <td>Fuzzy / typo tolerance</td>
          <td>PG FTS + pg_trgm</td>
      </tr>
      <tr>
          <td>&gt; 100M document + high QPS</td>
          <td>Elasticsearch / OpenSearch</td>
      </tr>
      <tr>
          <td>Faceted aggregation</td>
          <td>Elasticsearch / OpenSearch</td>
      </tr>
      <tr>
          <td>Vector similarity（semantic search）</td>
          <td>pgvector（同 PG）</td>
      </tr>
  </tbody>
</table>
<p>PG FTS + pgvector 組合對 <em>中小規模 hybrid keyword + semantic search</em> 是強選擇。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">JSONB Deep Dive</a>：JSONB 跟 FTS 都用 GIN</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">Extension Ecosystem</a>：pg_trgm / pgroonga / zhparser 都是 extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>：FTS query 的 EXPLAIN</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>：FTS GIN index 在 standby 自動 replicate</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">PG Extension Ecosystem</a>（pg_trgm / pgroonga）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">PG JSONB Deep Dive</a>（共用 GIN）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（FTS query plan）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/textsearch.html">PG Full-Text Search</a> / <a href="https://www.postgresql.org/docs/current/pgtrgm.html">pg_trgm</a></li>
</ul>
]]></content:encoded></item><item><title>3.C27 Zalando：RabbitMQ on AWS 自動化 master selection</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/</guid><description>&lt;p>這個案例的核心責任是說明雲端 cluster 治理在 K8s 之前的工程模式。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Communication platform 用 RabbitMQ cluster、跑在 EC2 / Docker container 上、用 supervisord 並行 sidekick + RabbitMQ。AWS 帳號限制每 region 5 個 Elastic IP。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>自建 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、master 死後晉升下一個最老 node。跨版本升級用 federation 上游接到新 cluster 過渡。揭露「cluster master selection」跟「IP 限制」是雲端部署的早期關鍵限制。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Erlang clustering + network partition / Federation + Shovel / RabbitMQ Cluster Operator（K8s 之前的雲端 cluster 治理）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.zalando.com/posts/2018/02/rabbit-in-the-cloud.html">Rabbit in the Cloud (Zalando Engineering)&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明雲端 cluster 治理在 K8s 之前的工程模式。</p>
<h2 id="觀察">觀察</h2>
<p>Communication platform 用 RabbitMQ cluster、跑在 EC2 / Docker container 上、用 supervisord 並行 sidekick + RabbitMQ。AWS 帳號限制每 region 5 個 Elastic IP。</p>
<h2 id="判讀">判讀</h2>
<p>自建 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、master 死後晉升下一個最老 node。跨版本升級用 federation 上游接到新 cluster 過渡。揭露「cluster master selection」跟「IP 限制」是雲端部署的早期關鍵限制。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Erlang clustering + network partition / Federation + Shovel / RabbitMQ Cluster Operator（K8s 之前的雲端 cluster 治理）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.zalando.com/posts/2018/02/rabbit-in-the-cloud.html">Rabbit in the Cloud (Zalando Engineering)</a></li>
</ul>
]]></content:encoded></item><item><title>Gitleaks</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitleaks/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitleaks/</guid><description>&lt;p>Gitleaks 是 &lt;em>純 CLI 的 OSS secret scanner&lt;/em>、MIT License、Go 寫、單一 binary 跑遍 macOS / Linux / Windows。它只做一件事 — 對 git history、working tree 或 staged changes 跑 regex + entropy + path filter 找 secret、輸出 JSON / SARIF / CSV 給下游消化。它沒有 dashboard、沒有 SaaS、沒有 cross-platform scan、也沒有 incident workflow — 這些刻意拿掉的東西是它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &amp;#43; remediation SaaS、350&amp;#43; Detector &amp;#43; Validation endpoint、跨 SCM &amp;#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning&lt;/a> 的核心分界。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Gitleaks 的核心定位是 &lt;em>git-aware secret scan 的 CLI 原語&lt;/em>、不是 secret 治理平台。Rule 寫在 &lt;code>.gitleaks.toml&lt;/code>、輸出走標準格式（SARIF / JSON / CSV）、跟下游 pipeline（CI、SIEM、GHAS Code Scanning）解耦。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &amp;#43; remediation SaaS、350&amp;#43; Detector &amp;#43; Validation endpoint、跨 SCM &amp;#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian&lt;/a> 比、GitGuardian 是 SaaS + dashboard + remediation workflow + validation endpoint（呼叫真實 API 驗證 secret 是否有效降 FP）+ honeytoken decoy、Gitleaks 沒有任一項 — 它只回答「這個 string 看起來像不像 secret」。GitGuardian 適合大型組織 + 預算允許 + 要跨 Slack / Jira / Notion 等 SaaS scan；Gitleaks 適合預算敏感 + 只需要 git scope + 內部已有 CI / SIEM 接 SARIF 的場景。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning&lt;/a> 比、GHAS 限 GitHub 平台、提供 push protection（partner pattern 在 push 前直接擋）跟 partner 自動 revoke 等深度整合、但只覆蓋 GitHub repo；Gitleaks 跨 GitHub / GitLab / Bitbucket / 自架 Gitea、CLI 跑哪都行、缺點是沒有 partner revoke 跟 push protection 要自己用 hook 接。&lt;/p></description><content:encoded><![CDATA[<p>Gitleaks 是 <em>純 CLI 的 OSS secret scanner</em>、MIT License、Go 寫、單一 binary 跑遍 macOS / Linux / Windows。它只做一件事 — 對 git history、working tree 或 staged changes 跑 regex + entropy + path filter 找 secret、輸出 JSON / SARIF / CSV 給下游消化。它沒有 dashboard、沒有 SaaS、沒有 cross-platform scan、也沒有 incident workflow — 這些刻意拿掉的東西是它跟 <a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a> / <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> 的核心分界。</p>
<h2 id="服務定位">服務定位</h2>
<p>Gitleaks 的核心定位是 <em>git-aware secret scan 的 CLI 原語</em>、不是 secret 治理平台。Rule 寫在 <code>.gitleaks.toml</code>、輸出走標準格式（SARIF / JSON / CSV）、跟下游 pipeline（CI、SIEM、GHAS Code Scanning）解耦。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a> 比、GitGuardian 是 SaaS + dashboard + remediation workflow + validation endpoint（呼叫真實 API 驗證 secret 是否有效降 FP）+ honeytoken decoy、Gitleaks 沒有任一項 — 它只回答「這個 string 看起來像不像 secret」。GitGuardian 適合大型組織 + 預算允許 + 要跨 Slack / Jira / Notion 等 SaaS scan；Gitleaks 適合預算敏感 + 只需要 git scope + 內部已有 CI / SIEM 接 SARIF 的場景。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> 比、GHAS 限 GitHub 平台、提供 push protection（partner pattern 在 push 前直接擋）跟 partner 自動 revoke 等深度整合、但只覆蓋 GitHub repo；Gitleaks 跨 GitHub / GitLab / Bitbucket / 自架 Gitea、CLI 跑哪都行、缺點是沒有 partner revoke 跟 push protection 要自己用 hook 接。</p>
<p>跟 TruffleHog OSS 比、兩者都是 OSS CLI secret scanner、TruffleHog 強在 <em>verifier</em>（對 200+ secret type 呼叫對應 API 驗證真偽）、Gitleaks 強在 <em>rule TOML 表達力跟 SARIF output 成熟度</em>。實務上很多組織兩個一起跑、用不同的 rule 覆蓋面互補。</p>
<p>關鍵張力：<em>Allowlist 治理</em> ↔ <em>FP 噪音</em> 是 Gitleaks 客戶最大的長期問題。OSS 沒有 validation endpoint、entropy + path filter 一定會誤判 test fixture / mock token / sample config、allowlist 不持續 review 會膨脹成「全部都白名單」最後 rule 失效。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Gitleaks 在 secret scan stack 中承擔哪一段（pre-commit / CI scan / historical audit）、哪些要外接（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> rotate、<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Code Scanning</a> 收 SARIF dashboard）</li>
<li>Custom rule 跟 allowlist 的 ownership 設計（誰寫 rule、誰核准 allowlist、定期 review 週期）</li>
<li><code>detect</code> vs <code>protect</code> 兩個子命令的職責切分、跟 pre-commit framework / CI 整合的位置</li>
<li>何時用 Gitleaks、何時升級到 <a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a> / <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a> 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Gitleaks 部署是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能改 <code>.gitleaks.toml</code></strong>：rule 跟 allowlist 是否走 Git PR review、commit message 是否帶 allowlist 原因、是否有 owner 簽核</li>
<li><strong><code>detect</code> vs <code>protect</code> 是否都接</strong>：CI 跑 <code>gitleaks detect</code>（掃 history + working tree）、pre-commit hook 跑 <code>gitleaks protect</code>（只掃 staged changes）— 缺 protect 等於 leak 進 history 才知道、缺 detect 等於既有 leak 永遠不發現</li>
<li><strong>SARIF 是否上傳 dashboard</strong>：CI output 是否 upload 到 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Code Scanning</a> 或內部 SIEM、不然 finding 散在 CI log 沒人看</li>
<li><strong>Allowlist 是否定期 review</strong>：allowlist entry 是否帶 expire date / reason / owner、每季是否 revisit 把過期項目刪掉、不然 allowlist 會膨脹到掩蓋真實 leak</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Rule TOML / JSON</strong>：rule 結構是 <code>id</code> + <code>regex pattern</code> + 可選 <code>entropy threshold</code>（高熵字串通常是 secret、避開 lorem ipsum FP）+ 可選 <code>path filter</code>（限定 / 排除路徑）。預設 rule library 涵蓋 AWS / GCP / Azure / Stripe / Slack token 等 100+ pattern；organization 通常 <em>先 import 預設、再加自家 token format custom rule</em>。Custom rule 必須給 valid + invalid sample 跑 unit test、不然 regex 寫錯會大量 FP。</p>
<p><strong><code>gitleaks detect</code>（historical scan）</strong>：掃整個 git history + working tree、CI 跑、適合 <em>發現既有 leak</em>。預設掃 HEAD 到根、可用 <code>--log-opts</code> 限定 commit range 加速。實務上 PR scan 限定 <code>--log-opts=&quot;--since=...$(git merge-base origin/main HEAD)&quot;</code> 只看本 PR 新增 commit、避免每次跑整個 history 慢死。</p>
<p><strong><code>gitleaks protect</code>（pre-commit）</strong>：只掃 staged changes、pre-commit hook 跑、適合 <em>攔住未來 leak</em>。它不掃 history、意義是 <em>commit 前的最後一道閘</em>；配合 pre-commit framework（<code>pre-commit-hooks</code> 或 <a href="https://pre-commit.com/">pre-commit.com</a>）的 <code>repos: gitleaks</code> 配置直接接入。</p>
<p><strong>Report 格式（JSON / SARIF / CSV）</strong>：JSON 是 raw 結構、適合 script 處理；SARIF 是 OASIS 標準、跟 GHAS Code Scanning / 商業 SAST dashboard 共用；CSV 適合人讀 / Excel 二次處理。Production 通常 <em>CI 輸出 SARIF + 上傳 GHAS Code Scanning</em>、把 OSS scanner 的 finding 跟商業 SAST 同 dashboard、SOC 不用切多工具。</p>
<p><strong>跟 CI 整合</strong>：GitHub Actions 用 <code>gitleaks/gitleaks-action</code>、GitLab CI 用 official Docker image、Jenkins 用 binary download + shell step。CI 失敗策略要決定 — <em>block PR</em> 還是 <em>warn only</em>：嚴格組織 block PR、寬鬆組織 warn + 上 SARIF 讓 SOC 自行 triage、避免初期高 FP 阻塞所有 merge。</p>
<p><strong>跟 pre-commit framework 整合</strong>：<code>.pre-commit-config.yaml</code> 加 <code>- repo: https://github.com/gitleaks/gitleaks</code> 條目、<code>pre-commit install</code> 後每次 commit 自動跑。注意 <em>pre-commit 只在開發者 machine 跑</em>、繞過很簡單（<code>git commit --no-verify</code>）、所以一定要配 CI scan 兜底、不能只信 pre-commit。</p>
<p><strong>Allowlist 治理</strong>：<code>.gitleaks.toml</code> 裡 <code>[allowlist]</code> section 寫 <code>paths</code> / <code>regexes</code> / <code>commits</code> / <code>stopwords</code>。每個 entry 應該帶 reason（<code># allowlist reason: test fixture for OAuth flow, expire 2026-Q4</code>）、PR review 時要問「為什麼這個是 FP、什麼時候會過期」。Quarterly 跑 audit 把過期項目刪掉、避免 allowlist 變成「全部都白名單」。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Gitleaks</th>
          <th>GitGuardian</th>
          <th>GHAS Secret Scanning</th>
          <th>TruffleHog OSS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>License</td>
          <td>MIT OSS</td>
          <td>Proprietary SaaS（free tier 限個人）</td>
          <td>GitHub Enterprise add-on</td>
          <td>AGPL OSS（Enterprise 商業）</td>
      </tr>
      <tr>
          <td>Scope</td>
          <td>Git only（history + tree + staged）</td>
          <td>Git + Slack + Jira + Notion + 自訂 source</td>
          <td>GitHub repo only</td>
          <td>Git + S3 + filesystem + more</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>無、輸出 SARIF / JSON 自己接</td>
          <td>內建 incident workflow + remediation</td>
          <td>GitHub Security tab</td>
          <td>無（CLI / API）</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>無（只看 regex + entropy）</td>
          <td>有（呼叫 API 驗證真偽）</td>
          <td>Partner pattern 自動 revoke</td>
          <td>有（200+ verifier）</td>
      </tr>
      <tr>
          <td>Push protection</td>
          <td>無、要自己 wire pre-commit</td>
          <td>有（透過 ggshield）</td>
          <td>有（partner pattern、push 前擋）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>CLI binary、跑哪都行</td>
          <td>SaaS only</td>
          <td>GitHub SaaS / Enterprise Server</td>
          <td>CLI binary</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>免費</td>
          <td>Per-developer / per-repo</td>
          <td>Per-committer</td>
          <td>免費（OSS） / 商業另計</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>OSS-friendly、預算敏感、CI / SARIF 已有</td>
          <td>跨 SaaS scan + remediation workflow</td>
          <td>GitHub-only + push protection 為主</td>
          <td>多 source + verifier 為主</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>低 — rule TOML 可移植到 GitGuardian</td>
          <td>高 — incident history / workflow 綁定</td>
          <td>中 — SARIF 可移植但 push protection 限 GHAS</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選 Gitleaks 的核心訴求：<em>OSS + 預算敏感 + 只需要 git scope + 內部 CI / SIEM 已能消化 SARIF</em>、且願意自己投入 rule / allowlist 治理。要跨 SaaS scan + incident workflow 升 GitGuardian、要 push protection + partner revoke 走 GHAS Secret Scanning。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Custom rule 寫法（regex + entropy + path）</strong>：自家 internal token 通常有特定 prefix（<code>xy_live_</code> / <code>int_token_</code>）、寫 custom rule 就是 <code>regex = '''xy_live_[A-Za-z0-9]{32}'''</code> + <code>entropy = 4.0</code> + <code>path = '''.*\.go$'''</code>。Entropy threshold 越高 FP 越少但 FN 越多、實務值 3.5–4.5 之間 tune。每個 rule 一定要在 repo 加 unit test fixture（valid + invalid sample）、CI 跑 rule 自我驗證、避免 regex 寫錯後 silent break。</p>
<p><strong>跟 SARIF + GHAS Code Scanning 整合補位</strong>：Gitleaks CI 跑完輸出 SARIF、用 <code>github/codeql-action/upload-sarif</code> 上傳到 GHAS Code Scanning。GHAS Code Scanning 不限 CodeQL 來源、任何 SARIF tool 都收。意義是 <em>OSS scanner + GHAS dashboard</em> 是預算友善組合 — 不買 GHAS Secret Scanning license、但 finding 集中在 Security tab 跟 SAST 共看。對應 <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Advanced Security</a> 的 Code Scanning section。</p>
<p><strong>跟 Vault 自動 rotation pipeline</strong>：Gitleaks 找到 leak 不是終點、是 <em>rotation trigger</em>。CI 把 finding 推 SOAR（或自家 webhook）、SOAR 呼叫 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> API 對該 credential type rotate（dynamic credential 直接 revoke、static secret 換新版本）、再 broadcast 給依賴該 secret 的 service rolling restart。沒這條 pipeline、Gitleaks 只是 finding 列表沒實際治理價值。</p>
<p><strong>Allowlist 治理（FP 不能無限）</strong>：OSS 沒 validation endpoint、test fixture / mock token / sample config 一定觸發 FP。allowlist 治理三原則 — <em>每個 entry 帶 reason + owner + expire date</em>、<em>PR review 必問「為什麼 FP」</em>、<em>quarterly audit 刪過期項目</em>。沒這個治理 allowlist 會在 6–12 個月內膨脹到「半個 repo 都在白名單」、那時候 rule 已經沒用、refactor 成本比一開始嚴格更高。</p>
<p><strong>跟 Trivy secret scan 重疊</strong>：<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> 內建 secret scanner（用同樣的 regex pattern）、container image / filesystem 都掃。Gitleaks 跟 Trivy secret scan 在 <em>container build pipeline</em> 階段會重疊 — 實務分工：Gitleaks 掃 source repo（git history + working tree）、Trivy 掃 built artifact（image layer + filesystem）。兩者覆蓋不同階段、不衝突。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>FP 太多、開發者開始忽略 Gitleaks finding</strong>：rule 沒 tune entropy threshold 或 path filter — 對 high-FP rule 加 <code>entropy = 4.0</code> 跟 <code>paths = ['''!test/.*''']</code>、staging branch 跑 1 週統計 FP 再 promote</li>
<li><strong>Pre-commit 被繞過（<code>--no-verify</code>）</strong>：開發者本機跑不過直接 bypass — pre-commit 不能當唯一防線、CI <code>gitleaks detect</code> block PR 才是真實 gate</li>
<li><strong>Historical scan 太慢、CI timeout</strong>：每次掃整個 git history — PR scan 限定 <code>--log-opts=&quot;$(git merge-base origin/main HEAD)..HEAD&quot;</code> 只看本 PR commit、nightly job 才跑 full history</li>
<li><strong>SARIF 上傳失敗 / GHAS dashboard 沒 finding</strong>：<code>github/codeql-action/upload-sarif</code> 權限不足或 <code>security-events: write</code> permission 沒給 — 補 GitHub Actions permission、或改 upload 內部 SIEM</li>
<li><strong>Allowlist 膨脹、規則失效</strong>：FP 全部塞 allowlist、沒 reason 沒 expire — quarterly audit、刪過期項目、把高 FP rule 改寫成更窄的 regex 而不是 allowlist 蓋過</li>
<li><strong>既有 leak 沒被發現、新 commit 攔得很乾淨</strong>：只接 <code>protect</code> 沒接 <code>detect</code> — CI 加 <code>detect</code> job、找出 history 中已 leak 的 secret 一次性 rotate（<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> 自動化）</li>
<li><strong>Custom rule 寫錯、silent skip 真 leak</strong>：rule regex 沒 unit test fixture、production 才發現 — 強制 custom rule 加 valid + invalid sample、CI 跑 rule 自驗</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨 Slack / Jira / Notion / 自架 SaaS scan</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a></td>
      </tr>
      <tr>
          <td>Push protection + partner auto-revoke</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a></td>
      </tr>
      <tr>
          <td>Validation endpoint（驗證 secret 真偽）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a> 或 TruffleHog Enterprise</td>
      </tr>
      <tr>
          <td>Honeytoken decoy 主動防禦</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a>（內建 honeytoken）</td>
      </tr>
      <tr>
          <td>Container image secret scan</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>（內建 secret scanner）</td>
      </tr>
      <tr>
          <td>Secret 找到後自動 rotate</td>
          <td>配 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> dynamic credential</td>
      </tr>
      <tr>
          <td>SAST / SCA dashboard 整合</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Code Scanning</a>（收 SARIF）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Gitleaks v8 跟 v7 的 rule 格式遷移細節</li>
<li>Gitleaks 內部 git odb 解析跟性能 tuning（large monorepo 加速）</li>
<li>Pre-commit framework 本身的安裝跟治理（屬開發者工作流、不在資安範圍）</li>
<li>Rotation playbook 完整實作（屬 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> / <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> 章節）</li>
<li>Secret 治理整體政策（屬 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets Management section</a> 上層原則）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Gitleaks 在 07 案例庫沒有直接 vendor-level 事件、所有 secret leak case 都是 git history scan + rotation pipeline 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Gitleaks 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023 Secrets Rotation</a></td>
          <td>Gitleaks <code>detect</code> 跑整個 git history 找出已 leaked secret、配合 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> rotation 流程清乾淨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022 Token Supply Chain</a></td>
          <td>Pre-commit <code>protect</code> 攔未來 leak、但對既有 leak 要 historical scan 補位、單一防線不夠</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Gitleaks 找出 leaked static secret 是第一步、長期解是 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a> dynamic credential 取代 long-lived secret</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7 章 Secrets Management section</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a>、<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Secret Scanning</a>、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（找到 leak 後 rotate）、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS Code Scanning</a>（收 SARIF dashboard）、<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（finding 進 SIEM）</li>
<li>官方：<a href="https://github.com/gitleaks/gitleaks">Gitleaks GitHub</a>、<a href="https://github.com/gitleaks/gitleaks/blob/master/README.md">Gitleaks Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C27 Disney+：DynamoDB 撐每日數十億動作的觀看歷史</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/</guid><description>&lt;p>這個案例的核心責任是說明「串流平台 metadata 層」的工作負載 — 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&amp;#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL&lt;/a> 的「live streaming 直播容量」是同產業不同議題。Disney+ 的 metadata 層處理「播了什麼、看到哪、下次推薦什麼」、是串流平台的「control plane」、不是「data plane」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Disney+ 在 DynamoDB 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers&lt;/a>）：&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>每日動作量&lt;/td>
 &lt;td>billions of actions daily&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要工作負載&lt;/td>
 &lt;td>content metadata + watch list management&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>Amazon DynamoDB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>global&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個用戶動作（播放、暫停、跳過、加入 watchlist、評分）都是一次 DynamoDB 寫入。每次打開 app 又是多次讀（自己的 watchlist、最近播放、繼續觀看）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Disney+ 案例揭露三個串流平台 metadata 層的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>「每日數十億動作」= read + write 都要撐&lt;/strong>：跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a> 的 18:1 讀寫比不同、串流 metadata 通常接近 5:1 read-heavy（每動作 1 寫、每 session 5 讀）。partition key 設計通常用 user_id、天然均勻、不會 hot partition。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 schema design。&lt;/li>
&lt;li>&lt;strong>新片發布是 predictable-peak&lt;/strong>：Marvel / Star Wars / Disney 動畫 新片上線首日、metadata 流量可衝 3-5 倍 — 因為「全平台用戶同時打開該片頁面」。這比一般 Black Friday 集中、像 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&amp;#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL&lt;/a> 的集中型流量。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a> 的內容發布事件容量規劃。&lt;/li>
&lt;li>&lt;strong>watchlist + 播放進度需要跨裝置即時同步&lt;/strong>：用戶在手機看到一半、晚上回家用電視繼續、進度必須跨裝置同步。這層需求對 DynamoDB Global Tables（multi-region active-active）特別適合。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a> 的最終一致性可接受場景。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「billions of actions daily」沒指明具體數字（10 億、100 億 還是 數十億？）。讀此類短篇案例只能取「量級對標」、不能套用具體數字。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>串流平台分「metadata 層」「content delivery 層」&lt;/strong>：metadata（watchlist、播放進度、推薦）用 DynamoDB / Cosmos DB；content（video file）用 CDN + S3 / object storage。兩者完全分開、互不影響。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 control plane vs data plane、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom&lt;/a> 的同類思維。&lt;/li>
&lt;li>&lt;strong>新片發布像 mini Black Friday、要 pre-scaling&lt;/strong>：發布時間已知、流量倍數可預估（根據前幾部）、可以提前 1-2 天 pre-scale DynamoDB capacity。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備&lt;/a>。&lt;/li>
&lt;li>&lt;strong>DynamoDB Global Tables 是跨裝置同步的有效方案&lt;/strong>：用戶在不同 region 登入同帳號、寫入會自動同步到其他 region。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &amp;#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys&lt;/a> 的 multi-region active-active。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：Netflix 同類 metadata 用 Cassandra + EVCache（&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&lt;/a> 提及）、HBO Max 用 Aurora、Apple TV+ 用 FoundationDB + Cassandra — 各家串流的 metadata 技術棧不同、但「分層解耦」的工程哲學一致。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「串流平台 metadata 層」的工作負載 — 跟 <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a> 的「live streaming 直播容量」是同產業不同議題。Disney+ 的 metadata 層處理「播了什麼、看到哪、下次推薦什麼」、是串流平台的「control plane」、不是「data plane」。</p>
<h2 id="觀察">觀察</h2>
<p>Disney+ 在 DynamoDB 的關鍵敘述（引自 <a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每日動作量</td>
          <td>billions of actions daily</td>
      </tr>
      <tr>
          <td>主要工作負載</td>
          <td>content metadata + watch list management</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>Amazon DynamoDB</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>global</td>
      </tr>
  </tbody>
</table>
<p>每個用戶動作（播放、暫停、跳過、加入 watchlist、評分）都是一次 DynamoDB 寫入。每次打開 app 又是多次讀（自己的 watchlist、最近播放、繼續觀看）。</p>
<h2 id="判讀">判讀</h2>
<p>Disney+ 案例揭露三個串流平台 metadata 層的工程重點。</p>
<ol>
<li><strong>「每日數十億動作」= read + write 都要撐</strong>：跟 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 的 18:1 讀寫比不同、串流 metadata 通常接近 5:1 read-heavy（每動作 1 寫、每 session 5 讀）。partition key 設計通常用 user_id、天然均勻、不會 hot partition。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema design。</li>
<li><strong>新片發布是 predictable-peak</strong>：Marvel / Star Wars / Disney 動畫 新片上線首日、metadata 流量可衝 3-5 倍 — 因為「全平台用戶同時打開該片頁面」。這比一般 Black Friday 集中、像 <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a> 的集中型流量。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 的內容發布事件容量規劃。</li>
<li><strong>watchlist + 播放進度需要跨裝置即時同步</strong>：用戶在手機看到一半、晚上回家用電視繼續、進度必須跨裝置同步。這層需求對 DynamoDB Global Tables（multi-region active-active）特別適合。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的最終一致性可接受場景。</li>
</ol>
<p>需要警惕：「billions of actions daily」沒指明具體數字（10 億、100 億 還是 數十億？）。讀此類短篇案例只能取「量級對標」、不能套用具體數字。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>串流平台分「metadata 層」「content delivery 層」</strong>：metadata（watchlist、播放進度、推薦）用 DynamoDB / Cosmos DB；content（video file）用 CDN + S3 / object storage。兩者完全分開、互不影響。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 control plane vs data plane、跟 <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a> 的同類思維。</li>
<li><strong>新片發布像 mini Black Friday、要 pre-scaling</strong>：發布時間已知、流量倍數可預估（根據前幾部）、可以提前 1-2 天 pre-scale DynamoDB capacity。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a>。</li>
<li><strong>DynamoDB Global Tables 是跨裝置同步的有效方案</strong>：用戶在不同 region 登入同帳號、寫入會自動同步到其他 region。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> 的 multi-region active-active。</li>
</ol>
<p>跨平台等效：Netflix 同類 metadata 用 Cassandra + EVCache（<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> 提及）、HBO Max 用 Aurora、Apple TV+ 用 FoundationDB + Cassandra — 各家串流的 metadata 技術棧不同、但「分層解耦」的工程哲學一致。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他串流案例 → <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a>（live）/ <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 NTT DOCOMO Lemino</a></li>
<li>想理解 metadata 層 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
<li>想做內容發布 pre-scaling → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> + <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a></li>
<li>想做跨裝置同步設計 → <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys multi-region</a></li>
<li>想拆 metadata 的 single-table 與 GSI 設計 → <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a> + <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">DynamoDB GSI / LSI 設計</a></li>
<li>想做跨 region metadata 一致性 → <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">DynamoDB global tables 寫衝突</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/dynamodb/customers/">Amazon DynamoDB Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/amazon-dynamodb-use-cases-for-media-and-entertainment-customers/">Amazon DynamoDB use cases for media and entertainment customers</a></li>
</ul>
]]></content:encoded></item><item><title>7.27 Credential Rotation with Scoped Evidence 實作示範</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/</guid><description>&lt;p>Credential rotation with scoped evidence 的核心責任是把憑證輪替從一次性操作改成分域、可驗證、可回退的控制流程。&lt;/p>
&lt;h2 id="服務路徑與控制範圍">服務路徑與控制範圍&lt;/h2>
&lt;p>示範路徑是 webhook secret 與 service-to-service API token 輪替。這類變更常見錯誤是全域同批切換，導致無法快速定位失效範圍。&lt;/p>
&lt;p>第一步先建 &lt;code>scope map&lt;/code>：哪些服務、哪些環境、哪些第三方端點共用同一組 credential。再定義證據欄位：輪替前健康度、輪替中錯誤率、輪替後驗證結果與撤銷狀態。&lt;/p>
&lt;h2 id="實作步驟">實作步驟&lt;/h2>
&lt;ol>
&lt;li>盤點 scope map：服務、環境、憑證用途、到期日、owner。&lt;/li>
&lt;li>設計輪替批次：先低風險租戶與非核心流量，再核心路徑。&lt;/li>
&lt;li>建立雙軌驗證窗口：新舊 credential 並行期間記錄命中比例。&lt;/li>
&lt;li>設定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a>：若驗證失敗可在時限內回退到舊憑證。&lt;/li>
&lt;li>輪替後執行撤銷與稽核：確認舊 credential 不再可用並保留 audit evidence。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&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>輪替後 webhook 驗簽失敗集中在單區域&lt;/td>
 &lt;td>scope map 與部署批次不一致&lt;/td>
 &lt;td>暫停擴批，先修區域映射&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>新舊 credential 命中比例長期雙高&lt;/td>
 &lt;td>撤銷步驟未完成或有隱藏呼叫方&lt;/td>
 &lt;td>延長觀察並追來源，禁止結案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輪替成功率高但稽核鏈缺欄位&lt;/td>
 &lt;td>證據不完整，事後不可追蹤&lt;/td>
 &lt;td>補 audit 欄位再進 release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後仍有驗簽錯誤&lt;/td>
 &lt;td>客戶端快取或第三方同步延遲&lt;/td>
 &lt;td>補回退窗口策略與客戶端同步公告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一 key 在多服務超範圍使用&lt;/td>
 &lt;td>credential scope 漂移&lt;/td>
 &lt;td>重新分域並建立到期輪替節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把輪替看成單次安全動作，會忽略它其實是跨服務變更管理。沒有 scope map 的輪替，出問題時只能全域停損。&lt;/p>
&lt;p>把撤銷延後也會累積風險。舊 credential 長時間保留，會讓攻擊面與誤用窗口同時存在。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>這條路徑可用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">7.C9 反例：憑證輪替失敗&lt;/a> 回寫。先看失敗是發生在分域、驗證還是撤銷，再回到本章補齊 scope map 與回退窗口。&lt;/p>
&lt;p>這個案例主要支撐的是「輪替分域與證據鏈完整度」判讀，不直接支撐 incident 通訊節奏；外部通報回到 8.4/8.20。&lt;/p>
&lt;h2 id="控制面-token-事件的分域輪替壓力">控制面 token 事件的分域輪替壓力&lt;/h2>
&lt;p>控制面 token 事件的分域輪替壓力是 scope map 的最強壓測場景。當高權限 token 跨多個服務、多個 tenant、多個第三方端點共用、事件期間要回答「哪些必須先輪、哪些可以後輪、哪些必須同步輪」、缺 scope map 時這個排序只能靠 ad-hoc 判斷。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2 Cloudflare 控制面 token 2023&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023 follow-through&lt;/a>：揭露控制面 token 事件的處置壓力 — 主 case 揭露三個策略方向（工作負載身份替代長期共享 token、強制 rotation 與細粒度 scope、把憑證事件寫入 release gate）、紅隊 case 補的具體 mechanism 是「分批恢復必要權限、前提是事先有 token 範圍 inventory」。&lt;/p>
&lt;p>以下基於通用工程知識補充：分批恢復的工程意義是讓事件期間的可用性風險可控 — 用三個維度排序：業務優先序（核心交易 vs 內部工具）、依賴方向（上游 service 先恢復 / 下游後恢復）、權限等級（低權先恢復 / 高權後恢復）。三維度衝突時、業務優先序勝過權限等級、是常見的工程取捨點。粗粒度的「全部凍結再全部解封」是 fallback 選項、會把可用性代價拉滿。&lt;/p>
&lt;h2 id="ci-平台事件的輪替壓力">CI 平台事件的輪替壓力&lt;/h2>
&lt;p>CI 平台事件的輪替壓力跟控制面 token 不同 — 範圍 &lt;em>已知&lt;/em> 但 &lt;em>量大&lt;/em>。CI 平台被入侵時、所有客戶端 secrets 都進入 &lt;em>可能洩漏&lt;/em> 狀態、實際是否被使用要靠後續行為佐證；scoped rotation 要在「全部輪太貴」跟「分層輪會漏」之間找平衡。&lt;/p>
&lt;p>CircleCI 2023 案例的範圍量級壓力 governance frame 在 [7.6 § CI secrets 集中化跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>](/backend/07-security-data-protection/secrets-and-machine-credential-governance/#ci-secrets-集中化跟-blast-radius)；本節聚焦 scoped rotation 視角下的實作示範 — 拿到 inventory 後如何排序 batch、用什麼 metadata 支撐分批決策。&lt;/p></description><content:encoded><![CDATA[<p>Credential rotation with scoped evidence 的核心責任是把憑證輪替從一次性操作改成分域、可驗證、可回退的控制流程。</p>
<h2 id="服務路徑與控制範圍">服務路徑與控制範圍</h2>
<p>示範路徑是 webhook secret 與 service-to-service API token 輪替。這類變更常見錯誤是全域同批切換，導致無法快速定位失效範圍。</p>
<p>第一步先建 <code>scope map</code>：哪些服務、哪些環境、哪些第三方端點共用同一組 credential。再定義證據欄位：輪替前健康度、輪替中錯誤率、輪替後驗證結果與撤銷狀態。</p>
<h2 id="實作步驟">實作步驟</h2>
<ol>
<li>盤點 scope map：服務、環境、憑證用途、到期日、owner。</li>
<li>設計輪替批次：先低風險租戶與非核心流量，再核心路徑。</li>
<li>建立雙軌驗證窗口：新舊 credential 並行期間記錄命中比例。</li>
<li>設定 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>：若驗證失敗可在時限內回退到舊憑證。</li>
<li>輪替後執行撤銷與稽核：確認舊 credential 不再可用並保留 audit evidence。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>輪替後 webhook 驗簽失敗集中在單區域</td>
          <td>scope map 與部署批次不一致</td>
          <td>暫停擴批，先修區域映射</td>
      </tr>
      <tr>
          <td>新舊 credential 命中比例長期雙高</td>
          <td>撤銷步驟未完成或有隱藏呼叫方</td>
          <td>延長觀察並追來源，禁止結案</td>
      </tr>
      <tr>
          <td>輪替成功率高但稽核鏈缺欄位</td>
          <td>證據不完整，事後不可追蹤</td>
          <td>補 audit 欄位再進 release gate</td>
      </tr>
      <tr>
          <td>回退後仍有驗簽錯誤</td>
          <td>客戶端快取或第三方同步延遲</td>
          <td>補回退窗口策略與客戶端同步公告</td>
      </tr>
      <tr>
          <td>同一 key 在多服務超範圍使用</td>
          <td>credential scope 漂移</td>
          <td>重新分域並建立到期輪替節奏</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把輪替看成單次安全動作，會忽略它其實是跨服務變更管理。沒有 scope map 的輪替，出問題時只能全域停損。</p>
<p>把撤銷延後也會累積風險。舊 credential 長時間保留，會讓攻擊面與誤用窗口同時存在。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>這條路徑可用 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">7.C9 反例：憑證輪替失敗</a> 回寫。先看失敗是發生在分域、驗證還是撤銷，再回到本章補齊 scope map 與回退窗口。</p>
<p>這個案例主要支撐的是「輪替分域與證據鏈完整度」判讀，不直接支撐 incident 通訊節奏；外部通報回到 8.4/8.20。</p>
<h2 id="控制面-token-事件的分域輪替壓力">控制面 token 事件的分域輪替壓力</h2>
<p>控制面 token 事件的分域輪替壓力是 scope map 的最強壓測場景。當高權限 token 跨多個服務、多個 tenant、多個第三方端點共用、事件期間要回答「哪些必須先輪、哪些可以後輪、哪些必須同步輪」、缺 scope map 時這個排序只能靠 ad-hoc 判斷。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2 Cloudflare 控制面 token 2023</a> 跟 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023 follow-through</a>：揭露控制面 token 事件的處置壓力 — 主 case 揭露三個策略方向（工作負載身份替代長期共享 token、強制 rotation 與細粒度 scope、把憑證事件寫入 release gate）、紅隊 case 補的具體 mechanism 是「分批恢復必要權限、前提是事先有 token 範圍 inventory」。</p>
<p>以下基於通用工程知識補充：分批恢復的工程意義是讓事件期間的可用性風險可控 — 用三個維度排序：業務優先序（核心交易 vs 內部工具）、依賴方向（上游 service 先恢復 / 下游後恢復）、權限等級（低權先恢復 / 高權後恢復）。三維度衝突時、業務優先序勝過權限等級、是常見的工程取捨點。粗粒度的「全部凍結再全部解封」是 fallback 選項、會把可用性代價拉滿。</p>
<h2 id="ci-平台事件的輪替壓力">CI 平台事件的輪替壓力</h2>
<p>CI 平台事件的輪替壓力跟控制面 token 不同 — 範圍 <em>已知</em> 但 <em>量大</em>。CI 平台被入侵時、所有客戶端 secrets 都進入 <em>可能洩漏</em> 狀態、實際是否被使用要靠後續行為佐證；scoped rotation 要在「全部輪太貴」跟「分層輪會漏」之間找平衡。</p>
<p>CircleCI 2023 案例的範圍量級壓力 governance frame 在 [7.6 § CI secrets 集中化跟 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>](/backend/07-security-data-protection/secrets-and-machine-credential-governance/#ci-secrets-集中化跟-blast-radius)；本節聚焦 scoped rotation 視角下的實作示範 — 拿到 inventory 後如何排序 batch、用什麼 metadata 支撐分批決策。</p>
<p><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023</a> 案例「可落地檢查點」標明事故中 mechanism 為「按分級快速輪替、並記錄 MTTR」，前提是「事先有 secrets inventory 跟 owner mapping」。實作示範視角的補充是：分級要落到具體 metadata schema、不只是規範性說法。</p>
<p>以下基於通用工程知識補充：tag 是事件期間的輪替排序前提 — metadata 完整時可從「high blast radius + critical tier」直接抽 subset 優先輪、再依資源展開。每個 secret 在 vault 裡帶 blast radius tag（local / shared / global）、business tier（critical / standard / experimental）、rotation cost（low / high）三維度。metadata 不足時排序退回全域輪替（成本高）或部分輪替（覆蓋風險）兩個 fallback。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 7.6 的交接：治理原則回到 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets and Machine Credential Governance</a>。</li>
<li>與 7.7 的交接：稽核欄位與責任鏈回到 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a>。</li>
<li>與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 的交接：高風險輪替變更進 release gate。</li>
<li>與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a> 的交接：輪替中止與回退判斷進 incident decision log。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要回到全模組實作串接，接著讀 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
]]></content:encoded></item><item><title>PostgreSQL Replication Slot Management：Physical / Logical / Failover Slot 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/replication-slot-management/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/replication-slot-management/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>replication slot management&lt;/em> — physical / logical / failover slot 三類治理。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="replication-slot-兩大類">Replication Slot 兩大類&lt;/h2>
&lt;p>PG 兩種 replication slot：&lt;/p>
&lt;h3 id="physical-replication-slot">Physical Replication Slot&lt;/h3>
&lt;p>對應 &lt;em>streaming replication&lt;/em>（physical WAL byte-level）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_create_physical_replication_slot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;standby1_slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用於：&lt;/p>
&lt;ul>
&lt;li>Streaming replication standby（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &amp;#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &amp;#43; LSN-based 進度追蹤 &amp;#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &amp;#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &amp;#43; logical replication 整合">Replication Topology&lt;/a>）&lt;/li>
&lt;li>pg_basebackup 用 slot 防 WAL 清理&lt;/li>
&lt;li>高 lag standby 防 WAL premature deletion&lt;/li>
&lt;/ul>
&lt;h3 id="logical-replication-slot">Logical Replication Slot&lt;/h3>
&lt;p>對應 &lt;em>logical replication / logical decoding&lt;/em>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_create_logical_replication_slot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;my_slot&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;pgoutput&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 或用 wal2json plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_create_logical_replication_slot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;debezium_slot&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;wal2json&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用於：&lt;/p>
&lt;ul>
&lt;li>PG-to-PG logical replication（publication / subscription）&lt;/li>
&lt;li>CDC（Debezium / Maxwell / pg_logical_emitter）&lt;/li>
&lt;li>Multi-master replication（BDR / pgEdge / Spock）&lt;/li>
&lt;/ul>
&lt;p>logical slot 跟 physical slot 共存、各自獨立 retention。&lt;/p>
&lt;h2 id="slot-lifecycle">Slot Lifecycle&lt;/h2>





&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">建立 → active（有 consumer）→ inactive（consumer 失聯）→ drop
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> WAL 持續累積（直到推進 LSN 或 drop）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>狀態查詢&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>replication slot management</em> — physical / logical / failover slot 三類治理。</p></blockquote>
<hr>
<h2 id="replication-slot-兩大類">Replication Slot 兩大類</h2>
<p>PG 兩種 replication slot：</p>
<h3 id="physical-replication-slot">Physical Replication Slot</h3>
<p>對應 <em>streaming replication</em>（physical WAL byte-level）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_create_physical_replication_slot</span><span class="p">(</span><span class="s1">&#39;standby1_slot&#39;</span><span class="p">);</span></span></span></code></pre></div><p>用於：</p>
<ul>
<li>Streaming replication standby（<a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>）</li>
<li>pg_basebackup 用 slot 防 WAL 清理</li>
<li>高 lag standby 防 WAL premature deletion</li>
</ul>
<h3 id="logical-replication-slot">Logical Replication Slot</h3>
<p>對應 <em>logical replication / logical decoding</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_create_logical_replication_slot</span><span class="p">(</span><span class="s1">&#39;my_slot&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;pgoutput&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 或用 wal2json plugin
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_create_logical_replication_slot</span><span class="p">(</span><span class="s1">&#39;debezium_slot&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;wal2json&#39;</span><span class="p">);</span></span></span></code></pre></div><p>用於：</p>
<ul>
<li>PG-to-PG logical replication（publication / subscription）</li>
<li>CDC（Debezium / Maxwell / pg_logical_emitter）</li>
<li>Multi-master replication（BDR / pgEdge / Spock）</li>
</ul>
<p>logical slot 跟 physical slot 共存、各自獨立 retention。</p>
<h2 id="slot-lifecycle">Slot Lifecycle</h2>





<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">建立 → active（有 consumer）→ inactive（consumer 失聯）→ drop
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                    ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">                              WAL 持續累積（直到推進 LSN 或 drop）</span></span></code></pre></div><p><strong>狀態查詢</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">slot_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="n">slot_type</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">       </span><span class="n">active</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">       </span><span class="n">restart_lsn</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">       </span><span class="n">confirmed_flush_lsn</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_wal_lsn_diff</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">retained_wal</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="p">;</span></span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>slot_type</code>：<code>physical</code> / <code>logical</code></li>
<li><code>active</code>：true / false（consumer 是否連著）</li>
<li><code>restart_lsn</code>：slot 起點 LSN、primary 必須保留這以後的 WAL</li>
<li><code>confirmed_flush_lsn</code>：logical slot 已 confirm flush 的 LSN</li>
<li><code>retained_wal</code>：當前因 slot 累積的 WAL</li>
</ul>
<h2 id="failover-slot-synchronization-pg-17">Failover Slot Synchronization (PG 17+)</h2>
<p>PG 17 之前的 <em>痛點</em>：logical replication slot 是 <em>primary 上的 state</em>、failover 後 <em>新 primary 沒這個 slot</em>、CDC consumer 失聯、需要重建（大工程）。</p>
<p>PG 17 加 <em>failover slot synchronization</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- PG 17+：標 slot 為 failover-tracked
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">-- signature: pg_create_logical_replication_slot(slot_name, plugin, temporary, two_phase, failover)
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_create_logical_replication_slot</span><span class="p">(</span><span class="s1">&#39;my_slot&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;pgoutput&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">false</span><span class="p">,</span><span class="w"> </span><span class="k">false</span><span class="p">,</span><span class="w"> </span><span class="k">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">--                                                                          ↑
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">--                                                                     failover=true（第 5 個參數）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">-- 注意：第 4 個參數是 two_phase（這裡 false）、第 5 個才是 failover
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- Standby 上 enable sync_replication_slots
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">sync_replication_slots</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">on</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_reload_conf</span><span class="p">();</span></span></span></code></pre></div><p><code>sync_replication_slots = on</code> 後、physical replication 同步 slot state 到 standby。Failover promote standby 後、logical slot 仍可用、CDC consumer 重連即可。</p>
<p>PG 17 之前用 <a href="https://www.pgedge.com/">pgEdge</a> / <em>pglogical</em> 等 extension 提供類似功能、現在 PG core 內建。</p>
<h2 id="orphan-slot-治理">Orphan Slot 治理</h2>
<p><code>active = false</code> 的 slot 持續累積 WAL、disk 爆是 PG production 經典事故。</p>
<h3 id="監控-orphan-slot">監控 orphan slot</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 找 inactive 太久的 slot
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">slot_name</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_wal_lsn_diff</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">retained_wal</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="n">active</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">pg_wal_lsn_diff</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">restart_lsn</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1024</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1024</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1024</span><span class="p">;</span><span class="w">  </span><span class="c1">-- &gt; 1 GB</span></span></span></code></pre></div><h3 id="自動-invalidate-slotpg-13">自動 invalidate slot（PG 13+）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- postgresql.conf
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">max_slot_wal_keep_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;50GB&#39;</span><span class="p">;</span><span class="w">  </span><span class="c1">-- slot 累積 &gt; 50GB 自動 invalidate</span></span></span></code></pre></div><p>當 slot 累積 WAL 超過 <code>max_slot_wal_keep_size</code>、PG 自動 invalidate slot（<code>active=false</code> 且不再保留 WAL）。Consumer 重連會 fail、必須重建（base backup + new slot）。</p>
<p>這是 <em>trade-off</em>：</p>
<ul>
<li>設 limit → 保護 disk、但 consumer 失聯 → 大重建工作</li>
<li>不設 limit → consumer 失聯 OK、但 disk 爆</li>
</ul>
<p>實務多數設 <code>max_slot_wal_keep_size</code> 給 <em>disk capacity 50%</em>、避免徹底 disk full。</p>
<h3 id="手動-drop-orphan-slot">手動 drop orphan slot</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 確認 slot 真的不需要
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">slot_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;old_standby_slot&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Drop
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_drop_replication_slot</span><span class="p">(</span><span class="s1">&#39;old_standby_slot&#39;</span><span class="p">);</span></span></span></code></pre></div><p>DR runbook 必須包含 <em>standby 退役流程</em>：先 standby fence、再 primary drop slot。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-orphan-slot-disk-爆">1. Orphan slot disk 爆</h3>
<p>最經典 PG 事故：standby decomission 沒 drop slot、primary 持續保留 WAL、<code>pg_wal/</code> 累積到 disk full、primary 也掛。</p>
<p>修法：</p>
<ul>
<li>監控 <code>pg_replication_slots</code> + <code>pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn))</code> retained_wal</li>
<li>設 <code>max_slot_wal_keep_size</code>（PG 13+）— hard limit</li>
<li>Standby 退役 runbook 強制 <em>先 fence、再 drop slot</em></li>
<li>Cron job 自動 alert orphan slot</li>
</ul>
<h3 id="2-logical-slot-lag--cdc-consumer-跟不上">2. Logical slot lag — CDC consumer 跟不上</h3>
<p>Logical decoding 比 physical replication 慢（per-transaction logical event 重組）。CDC consumer（Debezium）跟不上 → slot lag 累積。</p>
<p>修法：</p>
<ul>
<li>監控 <code>pg_replication_slots.confirmed_flush_lsn</code> 跟 primary <code>pg_current_wal_lsn()</code> 對比</li>
<li>CDC consumer 性能調整（throughput / batch size）</li>
<li>Throttle source writes（如果不能升 consumer）</li>
<li>對 hot table 拆 publication / subscription、避免單 slot 處理所有變更</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a>。</p>
<h3 id="3-failover-後-logical-slot-丟pg-16-之前">3. Failover 後 logical slot 丟（PG 16 之前）</h3>
<p>PG 16 之前、failover promote standby、新 primary 沒有原 logical slot。CDC consumer 試連、ERROR: <code>replication slot &quot;xxx&quot; does not exist</code>。</p>
<p>修法（PG 17+）：</p>
<ul>
<li>用 <em>failover slot synchronization</em>（如上）</li>
<li><code>pg_create_logical_replication_slot(...,  failover := true)</code></li>
<li>Standby <code>sync_replication_slots = on</code></li>
</ul>
<p>修法（PG 16-）：</p>
<ul>
<li>用 <a href="https://www.2ndquadrant.com/en/resources/pglogical/">pglogical</a> 或 <a href="https://www.pgedge.com/">pgEdge</a> extension</li>
<li>Failover runbook 包含 <em>新 primary 重建 logical slot</em>（CDC consumer 重 snapshot）</li>
<li>Pre-create slot on standby + manual sync（早期 workaround）</li>
</ul>
<h3 id="4-wal_keep_size-跟-slot-衝突">4. <code>wal_keep_size</code> 跟 slot 衝突</h3>
<p><code>wal_keep_size</code>（PG 13+）/ <code>wal_keep_segments</code>（&lt; 13）跟 slot 都會保留 WAL：</p>
<ul>
<li><code>wal_keep_size</code>：固定 minimum WAL 保留量</li>
<li>Slot：動態保留直到 consumer 推進</li>
</ul>
<p>兩者一起 set 時：實際保留 WAL = <code>max(wal_keep_size, slot 需要的量)</code>。</p>
<p>修法：</p>
<ul>
<li><code>wal_keep_size</code> 設小（如 1-2 GB）作 <em>minimum backup</em></li>
<li>主要靠 slot 動態保留 — 給 active consumer</li>
<li>監控 <code>pg_wal/</code> 大小 + 拆解 retention source（<code>wal_keep_size</code> vs slot 各佔多少）</li>
</ul>
<h3 id="5-slot-數量上限">5. Slot 數量上限</h3>
<p><code>max_replication_slots</code> 預設 10、不夠時新 slot 建不出來、報錯。</p>
<p>修法：</p>
<ul>
<li>Production 大 cluster 設 <code>max_replication_slots = 50</code> 或更多</li>
<li>對 <em>standby + logical replication + CDC consumer</em> 同時跑、計算需要的 slot 數</li>
<li>監控 <code>SELECT count(*) FROM pg_replication_slots</code> 接近 limit 時告警</li>
</ul>
<h2 id="slot-naming-convention">Slot Naming Convention</h2>
<p>Production 大 cluster 多 slot、命名 convention 重要：</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">&lt;consumer-type&gt;_&lt;consumer-name&gt;_&lt;purpose&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">例：
</span></span><span class="line"><span class="ln">3</span><span class="cl">- physical_standby1_replication
</span></span><span class="line"><span class="ln">4</span><span class="cl">- physical_standby2_replication
</span></span><span class="line"><span class="ln">5</span><span class="cl">- logical_debezium_orders_cdc
</span></span><span class="line"><span class="ln">6</span><span class="cl">- logical_pgedge_node2_subscription
</span></span><span class="line"><span class="ln">7</span><span class="cl">- physical_pgbasebackup_temp（base backup 用、completed 後 drop）</span></span></code></pre></div><p>清楚命名讓 <em>看 slot 名</em> 就知道用途、誰負責、能不能 drop。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>：physical slot 給 streaming replication 用</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a>：logical slot 給 CDC</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/bdr-multi-master/" data-link-title="PostgreSQL BDR / Multi-Master：active-active 寫入的 3 種路徑跟 conflict 治理" data-link-desc="PG 預設是 single-primary、active-active 多寫入入口需要 *BDR (EDB)* / *pgEdge* / *Bucardo* 等 extension。本文走 3 種 multi-master 方案對比、conflict detection &#43; resolution model、async vs sync 取捨、配置 step-by-step（pgEdge 為主）、5 production 踩雷（last-write-wins data loss / sequence collision / DDL replication / conflict log 治理 / failover 後 timeline 分歧）、跟 MySQL Group Replication sibling 對比">BDR / Multi-Master</a>：multi-master 大量用 logical slot</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a>：WAL archive 跟 slot 是兩種 WAL retention 機制、可並行</li>
</ul>
<h2 id="監控-metric">監控 metric</h2>
<p>Production 持續監控：</p>
<ul>
<li><code>pg_replication_slots.active</code> — 失聯 slot</li>
<li><code>pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)</code> — slot 累積 WAL</li>
<li><code>pg_replication_slots.confirmed_flush_lsn</code> vs <code>pg_current_wal_lsn()</code> — logical slot lag</li>
<li><code>pg_ls_waldir()</code> 看 <code>pg_wal/</code> 目錄大小</li>
<li><code>count(*) FROM pg_replication_slots</code> 對 <code>max_replication_slots</code> 比例</li>
</ul>
<p>把這些丟進 Datadog / Prometheus + alert。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（physical slot 用途）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">PG Logical Replication + Debezium</a>（logical slot 用途）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/bdr-multi-master/" data-link-title="PostgreSQL BDR / Multi-Master：active-active 寫入的 3 種路徑跟 conflict 治理" data-link-desc="PG 預設是 single-primary、active-active 多寫入入口需要 *BDR (EDB)* / *pgEdge* / *Bucardo* 等 extension。本文走 3 種 multi-master 方案對比、conflict detection &#43; resolution model、async vs sync 取捨、配置 step-by-step（pgEdge 為主）、5 production 踩雷（last-write-wins data loss / sequence collision / DDL replication / conflict log 治理 / failover 後 timeline 分歧）、跟 MySQL Group Replication sibling 對比">PG BDR / Multi-Master</a>（multi-master 大量 slot）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PG PITR + WAL Archiving</a>（WAL retention 兩種機制）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS">PG Replication Slots</a> / <a href="https://www.postgresql.org/docs/current/logicaldecoding.html">Logical Replication Slot</a></li>
</ul>
]]></content:encoded></item><item><title>自管 Vitess → PlanetScale：Vitess component ops outsource、加 schema workflow shift</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-vitess-to-planetscale/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-vitess-to-planetscale/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding&lt;/a> 跟 PlanetScale。走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type C operational hybrid 結構。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元件&lt;/th>
 &lt;th>自管 Vitess&lt;/th>
 &lt;th>PlanetScale&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>VTGate&lt;/td>
 &lt;td>自己部署 + LB&lt;/td>
 &lt;td>Managed、隱藏在 PlanetScale endpoint 後&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VTTablet&lt;/td>
 &lt;td>自己 per-MySQL deploy&lt;/td>
 &lt;td>Managed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VReplication&lt;/td>
 &lt;td>自己 trigger workflow&lt;/td>
 &lt;td>Managed、透過 Console / API&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VSchema&lt;/td>
 &lt;td>自己維護（YAML / API）&lt;/td>
 &lt;td>Managed、Console UI 編輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MySQL backend&lt;/td>
 &lt;td>自己 EC2 / on-prem&lt;/td>
 &lt;td>Managed (Aurora-like underlying)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema migration&lt;/td>
 &lt;td>gh-ost / pt-osc 或 Vitess online DDL&lt;/td>
 &lt;td>&lt;strong>Branch + Deploy Request workflow&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failover&lt;/td>
 &lt;td>自己用 VTOrc&lt;/td>
 &lt;td>Managed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-region&lt;/td>
 &lt;td>自己配 VReplication 跨 region&lt;/td>
 &lt;td>Boost / per-region cluster&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost model&lt;/td>
 &lt;td>EC2 + EBS + ops headcount&lt;/td>
 &lt;td>Per-row read / write + storage&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這條 migration 跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">→ Aurora MySQL&lt;/a> 相似（self-managed → managed），但 &lt;em>target 是 Vitess-native managed&lt;/em>、保留 sharding 能力。同時加上 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &amp;#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &amp;#43; Operational &amp;#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">→ PlanetScale from self-managed MySQL&lt;/a> 的 branch workflow paradigm。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding</a> 跟 PlanetScale。走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type C operational hybrid 結構。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>自管 Vitess</th>
          <th>PlanetScale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VTGate</td>
          <td>自己部署 + LB</td>
          <td>Managed、隱藏在 PlanetScale endpoint 後</td>
      </tr>
      <tr>
          <td>VTTablet</td>
          <td>自己 per-MySQL deploy</td>
          <td>Managed</td>
      </tr>
      <tr>
          <td>VReplication</td>
          <td>自己 trigger workflow</td>
          <td>Managed、透過 Console / API</td>
      </tr>
      <tr>
          <td>VSchema</td>
          <td>自己維護（YAML / API）</td>
          <td>Managed、Console UI 編輯</td>
      </tr>
      <tr>
          <td>MySQL backend</td>
          <td>自己 EC2 / on-prem</td>
          <td>Managed (Aurora-like underlying)</td>
      </tr>
      <tr>
          <td>Schema migration</td>
          <td>gh-ost / pt-osc 或 Vitess online DDL</td>
          <td><strong>Branch + Deploy Request workflow</strong></td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>自己用 VTOrc</td>
          <td>Managed</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>自己配 VReplication 跨 region</td>
          <td>Boost / per-region cluster</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>EC2 + EBS + ops headcount</td>
          <td>Per-row read / write + storage</td>
      </tr>
  </tbody>
</table>
<p>這條 migration 跟 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">→ Aurora MySQL</a> 相似（self-managed → managed），但 <em>target 是 Vitess-native managed</em>、保留 sharding 能力。同時加上 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">→ PlanetScale from self-managed MySQL</a> 的 branch workflow paradigm。</p>
<p>對 <em>已花心力建 Vitess team 但 ops cost 太大</em> 的 org 來說、這條 migration 比 <em>Vitess → distributed SQL</em> 風險低、保留 sharding investment。</p>
<h2 id="為什麼是-type-c不是-type-a-或-type-e">為什麼是 Type C（不是 Type A 或 Type E）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#%e5%af%ab%e5%89%8d%e7%9a%84-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low</td>
          <td>Vitess wire protocol + VSchema 概念一致</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High</td>
          <td>4 個 component 的 ops 全部 outsource、branch workflow 是新 paradigm</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Medium</td>
          <td>Vitess paradigm 不變、但加 branch workflow</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
          <td>同 Vitess engine</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Low</td>
          <td>Connection string 改、無 schema rewrite</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>Vitess sharding 結構保留</td>
      </tr>
  </tbody>
</table>
<p>Operational = High（其他 Low / Medium） → <strong>Type C operational hybrid</strong>。Branch workflow 是 <em>Medium paradigm shift</em> 但不是 dominant — 主要工作量在 <em>operational ownership 轉移</em>。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">自管 MySQL → PlanetScale</a>（Type E paradigm shift）對比：那條 path 是 <em>no-Vitess → Vitess + branch</em>、要學 Vitess 概念 + branch；本條是 <em>已有 Vitess + 加 branch</em>、只學 branch、複雜度低很多。</p>
<h2 id="driverops-headcount--branch-workflow--vitess-feature-加速">Driver：Ops headcount + Branch workflow + Vitess feature 加速</h2>
<p>從自管 Vitess 遷 PlanetScale 的核心 driver：</p>
<p><strong>Ops headcount 削減</strong>：</p>
<ul>
<li>自管 Vitess 通常需要 <em>2-5 個 SRE/DBA 撐 production</em> —VTGate / VTTablet / VReplication / VSchema 各有議題</li>
<li>PlanetScale 把這層全部 outsource、團隊 ops headcount 可降到 &lt; 1 FTE</li>
<li>對 50-200 人 eng team、ops cost saving 是顯著 driver</li>
</ul>
<p><strong>Branch workflow paradigm</strong>：</p>
<ul>
<li>自管 Vitess 仍用 gh-ost / pt-osc 或 Vitess online DDL 跑 schema migration、是 DBA 主導</li>
<li>PlanetScale branch workflow 把 schema migration 變 <em>developer self-service</em>、開 branch / Deploy Request / merge、跟 git workflow 同節奏</li>
<li>對 <em>high-velocity engineering culture</em> 是文化升級</li>
</ul>
<p><strong>Vitess upstream feature</strong>：</p>
<ul>
<li>PlanetScale team 是 Vitess 的主要 contributor、新 feature 通常 PlanetScale 先 ship</li>
<li>自管 Vitess 升級慢、PlanetScale 用戶看到新 feature 早 3-6 個月</li>
</ul>
<p>不適合 <em>跨雲 portability priority high</em> 或 <em>strict on-prem deployment</em> 的 org — PlanetScale 是 cloud-only。</p>
<h2 id="4-phase-migration">4-phase migration</h2>
<h3 id="phase-1topology--vschema-audit">Phase 1：Topology + VSchema audit</h3>
<p>把當前自管 Vitess cluster 完整盤點：</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"># Vitess cluster topology</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vtctldclient GetKeyspaces
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">vtctldclient GetShards &lt;keyspace&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">vtctldclient GetTablets
</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"># VSchema</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">vtctldclient GetVSchema &lt;keyspace&gt;
</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="c1"># 跨 keyspace VReplication workflow</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">vtctldclient GetWorkflows</span></span></code></pre></div><p>對每個 keyspace 檢查：</p>
<ul>
<li><em>Shard 數量</em>：PlanetScale plan 對 shard 數量有 limit（Enterprise 才能超大規模）</li>
<li><em>VSchema features</em>：自管可能用 <em>PlanetScale 不支援的 Vindex</em>（custom Vindex）</li>
<li><em>Foreign key</em>：Vitess 18+（2023 末）才支援 FK、自管 Vitess 大多 &lt; 18、cluster 內已 application-enforced；遷 PlanetScale 後可選擇啟用 native FK（同 shard 內）或繼續 application enforcement</li>
<li><em>Stored procedure / trigger</em>：PlanetScale 受限、確認是否 application 依賴</li>
</ul>
<p>完成標準：寫 <em>blocker list</em>（PlanetScale 不支援的功能）+ <em>compatibility list</em>（功能對應）。</p>
<h3 id="phase-2dual-cluster--binlog-stream">Phase 2：Dual cluster + binlog stream</h3>
<p>PlanetScale 內建 <em>Vitess Connector</em>、從外部 MySQL（包括其他 Vitess cluster）binlog stream import：</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. 用 PlanetScale CLI 建 cluster</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pscale database create production --region us-east
</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"># 2. Import schema（從自管 Vitess export）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pscale shell production main &lt; schema.sql
</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="c1"># 3. 設 Vitess Connector 從自管 cluster import 資料</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># （透過 PlanetScale Console）</span></span></span></code></pre></div><p>Vitess Connector 從自管 VTTablet 的 MySQL primary 讀 binlog、寫進 PlanetScale。Lag 通常 &lt; 1 秒。</p>
<p>跑 1-2 週、確認：</p>
<ul>
<li>Schema 完整 migrate</li>
<li>VSchema 對應正確（Vindex 行為一致）</li>
<li>Lag 穩定</li>
</ul>
<h3 id="phase-3application-read-切-planetscale">Phase 3：Application read 切 PlanetScale</h3>
<p>跟 Aurora migration Phase 2 同概念。Application read query 切 PlanetScale endpoint：</p>
<ul>
<li>連 PlanetScale connection string（<code>xxx.connect.psdb.cloud</code>）</li>
<li>仍寫自管 Vitess、Vitess Connector 同步 PlanetScale</li>
</ul>
<p>跑 2-4 週、驗證：</p>
<ul>
<li>Query result 一致</li>
<li>PlanetScale read latency 接近自管（PlanetScale Boost cache 可能加速）</li>
<li>PlanetScale row read 計費跟預估一致</li>
</ul>
<h3 id="phase-4write-cutover--自管-vitess-退役">Phase 4：Write cutover + 自管 Vitess 退役</h3>





<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. PlanetScale 把 cluster promote 為 primary（透過 Console）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 透過 PlanetScale Console 啟用 production write 或用 `pscale` CLI 對應 promotion 命令</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># （CLI 命令名稱隨 pscale 版本變動、以 pscale --help 為準）</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="c1"># 2. Application 寫 connection string 切 PlanetScale</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 自管 Vitess → PlanetScale</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"># 3. Vitess Connector 反向（PlanetScale → 自管）作為 rollback buffer</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="c1"># 4. 跑 1-2 週確認、開始 decommission 自管 Vitess</span></span></span></code></pre></div><p>Decommission 自管 Vitess 是大工程：</p>
<ul>
<li>VTGate / VTTablet pods 一個個關</li>
<li>VReplication workflow 停掉</li>
<li>MySQL backend 保留作 cold backup 1-3 月、然後 EBS snapshot + terminate</li>
</ul>
<p>完成標準：所有 traffic 在 PlanetScale、自管 Vitess 資源全 release、ops headcount confirm 下降。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-vschema-不完全兼容--custom-vindex-必須改">1. VSchema 不完全兼容 — Custom Vindex 必須改</h3>
<p>自管 Vitess 可能用了 <em>custom Vindex</em>（自寫 Go plugin）、PlanetScale 不支援 custom Vindex（只支援 built-in：hash / lookup_hash / unicode 等）。</p>
<p>修法：</p>
<ul>
<li>Phase 1 audit 出所有 custom Vindex</li>
<li>對每個 custom Vindex 評估能否用 built-in 替代</li>
<li>不能替代的、考慮 <em>application 層 logic 取代 Vindex</em>（application 自己算 shard key）</li>
<li>或 <em>暫不遷該 keyspace</em>、保留自管 Vitess 跑 custom Vindex keyspace、其他遷 PlanetScale</li>
</ul>
<h3 id="2-branch-workflow-訓練不到位--dba-仍用vitess-online-ddl心智模型">2. Branch workflow 訓練不到位 — DBA 仍用「Vitess online DDL」心智模型</h3>
<p>自管 Vitess team 習慣 <code>vtctldclient ApplySchema --strategy=vitess</code> 跑 online DDL、遷 PlanetScale 後仍想直接這樣 — 但 PlanetScale production branch 禁止 schema change、必須走 Deploy Request。</p>
<p>修法：</p>
<ul>
<li>Phase 3 <em>訓練步驟</em>：team 每個 DBA / SRE 都跑過完整 branch + Deploy Request workflow</li>
<li>寫 <em>team runbook</em>：production schema change must 走 branch</li>
<li>緊急 schema change（事故中）也走 branch、PlanetScale 可加速 Deploy</li>
</ul>
<h3 id="3-super-privilege-移除--自管-admin-tool-失效">3. SUPER privilege 移除 — 自管 admin tool 失效</h3>
<p>自管 Vitess 用 <code>SUPER</code> privilege 跑 admin script、PlanetScale 沒給 SUPER。常見失效：</p>
<ul>
<li>自寫 monitor script 跑 <code>SHOW SLAVE STATUS</code>、PlanetScale 抽象掉</li>
<li>自寫 backup script 跑 <code>FLUSH TABLES WITH READ LOCK</code>、PlanetScale 不允許</li>
<li>自寫 cleanup script 跑 <code>KILL QUERY</code>、PlanetScale 受限</li>
</ul>
<p>修法：</p>
<ul>
<li>Phase 1 audit 所有 admin script</li>
<li>改用 <em>PlanetScale Console / CLI / API</em> 等價操作</li>
<li>PlanetScale 提供的 monitoring 介面替代自管監控</li>
</ul>
<h3 id="4-connection-limit--planetscale-plan-比預期緊">4. Connection limit — PlanetScale plan 比預期緊</h3>
<p>PlanetScale Scaler Plan: 10K connection、Enterprise: 100K。自管 Vitess VTGate 通常設 50K-200K connection、遷 PlanetScale 後 hit limit。</p>
<p>修法：</p>
<ul>
<li>Phase 1 <em>connection forecast</em>：peak hour 多少 active connection</li>
<li>升 PlanetScale plan（Scaler Pro / Enterprise）</li>
<li>或在 application 端加 connection pool（HikariCP / pgBouncer 等價）降低 connection count</li>
</ul>
<h3 id="5-cost-model-翻盤--per-row-read-計費超預期">5. Cost model 翻盤 — Per-row read 計費超預期</h3>
<p>PlanetScale 計費是 <em>per row read / written</em>。自管 Vitess cost = EC2 + EBS（線性 with infrastructure scale）。遷 PlanetScale 後計費跟 <em>application access pattern</em> 直接相關。</p>
<p>常見 surprise：</p>
<ul>
<li>Heavy analytics query（COUNT *、aggregation）讀大量 row、計費高</li>
<li>N+1 query pattern（application 跑很多小 SELECT）讀很多 row、計費高</li>
<li>Read-heavy workload 沒 Boost cache、每次 query 都 hit billing</li>
</ul>
<p>修法：</p>
<ul>
<li>Phase 1 <em>cost forecast</em>：用 <code>pscale analytics</code> 預估 row read / write 量、估算月帳</li>
<li>Phase 2 期間實際對 PlanetScale 跑 traffic、看實際 billing</li>
<li>Heavy analytics 改 <em>材料化 view</em> / <em>async aggregation</em>、不是每次 query</li>
<li>高 read frequency 開 Boost cache（額外 cost、但比 row read 便宜）</li>
</ul>
<h2 id="capability-mapping">Capability mapping</h2>
<table>
  <thead>
      <tr>
          <th>自管 Vitess</th>
          <th>PlanetScale 對應</th>
          <th>兼容度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VTGate</td>
          <td>PlanetScale endpoint</td>
          <td>100%</td>
      </tr>
      <tr>
          <td>VTTablet</td>
          <td>PlanetScale managed</td>
          <td>100%</td>
      </tr>
      <tr>
          <td>VReplication</td>
          <td>PlanetScale Console + Deploy Request</td>
          <td>90%（內部使用更受限）</td>
      </tr>
      <tr>
          <td>VSchema</td>
          <td>PlanetScale Console / pscale CLI</td>
          <td>95%（custom Vindex 不支援）</td>
      </tr>
      <tr>
          <td>Vitess online DDL</td>
          <td>Deploy Request workflow</td>
          <td>不同 paradigm、功能等價</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>PlanetScale 自動</td>
          <td>100%（且更好）</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>PlanetScale 自動</td>
          <td>100%</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>PlanetScale Boost / per-region cluster</td>
          <td>90%</td>
      </tr>
      <tr>
          <td>Custom plugin</td>
          <td>不支援</td>
          <td>0%</td>
      </tr>
      <tr>
          <td>SUPER privilege</td>
          <td>不支援</td>
          <td>0%</td>
      </tr>
  </tbody>
</table>
<h2 id="容量與成本對照">容量與成本對照</h2>
<p>對 200 人 eng team 用自管 Vitess（10 shard、20 TB 資料、50K WPS）：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>自管 Vitess（自管 EC2）</th>
          <th>PlanetScale Scaler Pro</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure</td>
          <td>~$15K-25K / mo（EC2 + EBS + LB）</td>
          <td>Variable（per row read / write）</td>
      </tr>
      <tr>
          <td>Ops headcount</td>
          <td>2-3 FTE × $150K / yr = $300K-450K / yr</td>
          <td>&lt; 0.5 FTE × $150K = $75K / yr</td>
      </tr>
      <tr>
          <td>Vitess upgrade cost</td>
          <td>每年 1-2 個 SRE × 2 週</td>
          <td>自動</td>
      </tr>
      <tr>
          <td>Per-row read</td>
          <td>不計費</td>
          <td>$1 per 1B row read</td>
      </tr>
      <tr>
          <td>Per-row written</td>
          <td>不計費</td>
          <td>$1.50 per 1M row write</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>EBS $2K-5K / mo</td>
          <td>$1.50 / GB / mo</td>
      </tr>
      <tr>
          <td><strong>總帳</strong></td>
          <td>~$400K-550K / yr</td>
          <td>~$200K-350K / yr（看 traffic）</td>
      </tr>
  </tbody>
</table>
<p>對中型規模、PlanetScale 通常 break-even 或更便宜。對極大規模（&gt; 200K WPS / &gt; 100 TB）PlanetScale Enterprise 需要 commit pricing、不一定划算。</p>
<h2 id="何時不要遷">何時不要遷</h2>
<ul>
<li><strong>跨雲 / on-prem 是 requirement</strong>：PlanetScale cloud-only</li>
<li><strong>Custom Vindex / 特殊 plugin</strong> 大量使用：兼容度低、改造工作量大</li>
<li><strong>規模極大</strong> &gt; 500K WPS / &gt; 200 TB：PlanetScale plan 對應 Enterprise commit、議價辛苦</li>
<li><strong>強合規 / 資料主權限制</strong>：金融 / 政府 / 醫療場景、PlanetScale 不一定能 cover compliance</li>
<li><strong>既有 Vitess team 強 + ops cost 低</strong>：如果 ops 已經精實、不必為 outsource 而 outsource</li>
</ul>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-vitess-sharding">跟 <a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess sharding</a></h3>
<p>本 migration 保留 Vitess sharding 概念、application code 視角幾乎不變。Phase 1 audit 是 <em>Vitess concept 對應 PlanetScale concept</em>、不是 <em>拆 Vitess 換 distributed SQL</em>。</p>
<h3 id="跟--planetscale-from-self-managed-mysql">跟 <a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">→ PlanetScale (from self-managed MySQL)</a></h3>
<p>本 migration 是 <em>Vitess → PlanetScale</em>、前者是 <em>MySQL → PlanetScale</em>。差異：</p>
<ul>
<li><em>MySQL → PlanetScale</em> (Type E)：要學 Vitess 概念 + branch workflow + FK 處理</li>
<li><em>Vitess → PlanetScale</em> (Type C)：只學 branch workflow + ops outsource、保留所有 Vitess investment</li>
</ul>
<p>選哪條 path 取決於起點。</p>
<h3 id="跟-major-version-upgrade">跟 <a href="/blog/backend/01-database/vendors/mysql/major-version-upgrade/" data-link-title="MySQL 5.7 → 8.0 Major Version Upgrade：character set / authentication / atomic DDL 三條 paradigm 同時換軌" data-link-desc="MySQL 5.7 → 8.0 三條 default 同時改：charset utf8 → utf8mb4、auth plugin native_password → caching_sha2_password、DDL non-atomic → atomic。本文走 Type E paradigm shift 結構、6 維 audit、4-phase upgrade、5 production 踩雷、何時不要升級。">Major Version Upgrade</a></h3>
<p>從自管 Vitess 上 MySQL 5.7 遷 PlanetScale 也是 <em>同時跨 major version</em>（PlanetScale 跑 8.0+ Vitess）。Application 必須同時處理 5.7 → 8.0 paradigm shift（charset / auth）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess Sharding</a>（self-managed source）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-planetscale/" data-link-title="MySQL → PlanetScale：managed Vitess &#43; branch-based schema workflow 的 hybrid shift" data-link-desc="自管 MySQL → PlanetScale 加上 Vitess sharding 跟 branch-based schema workflow。本文走 6 維 audit（Paradigm &#43; Operational &#43; Schema 多軸）、4-phase migration、5 production 踩雷、何時不要遷。">→ PlanetScale from self-managed MySQL</a>（不同起點）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-aurora/" data-link-title="MySQL → Aurora MySQL：storage layer 轉手到 AWS、replication / HA / backup 全部 outsource" data-link-desc="自管 MySQL → Aurora MySQL 是 Type C operational hybrid migration — wire protocol 一致、ops 責任轉到 AWS。本文走 6 維 audit（Operational High）、Aurora storage architecture 衝擊、4-phase migration、5 production 踩雷、何時維持原路線。">→ Aurora MySQL</a>（另一條 self-managed → managed path）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/major-version-upgrade/" data-link-title="MySQL 5.7 → 8.0 Major Version Upgrade：character set / authentication / atomic DDL 三條 paradigm 同時換軌" data-link-desc="MySQL 5.7 → 8.0 三條 default 同時改：charset utf8 → utf8mb4、auth plugin native_password → caching_sha2_password、DDL non-atomic → atomic。本文走 Type E paradigm shift 結構、6 維 audit、4-phase upgrade、5 production 踩雷、何時不要升級。">Major Version Upgrade</a>（5.7 → 8.0 同期考量）</li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type C operational hybrid）</li>
<li>官方：<a href="https://planetscale.com/docs/imports">PlanetScale Migration Guide</a> / <a href="https://github.com/planetscale/vitess-operator">Vitess Operator</a></li>
</ul>
]]></content:encoded></item><item><title>3.C28 WeWork：Consistent hash exchange 保證帳戶順序</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/</guid><description>&lt;p>這個案例的核心責任是說明 RabbitMQ 也能做「per-key ordering」、用 consistent hash exchange 模擬 partition。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>訊息順序對某些業務流程關鍵、但全局排序代價高。WeWork 採固定數量 queue + 用 account ID hash 路由到特定 queue。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>每個 queue 一個 SideKiq worker + exclusive consumer 保證單帳戶順序。文後發現 RabbitMQ Consistent Hashing plugin 已內建類似機制（類似 Kafka 分區）。揭露 partition-level ordering 不是 Kafka 專屬、在 broker model 可用 hash exchange 達成。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Exchange types / Prefetch + consumer 併發（partition-level ordering 模式）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a>（partition + key 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.cloudamqp.com/blog/weworks-good-enough-order%20guarantee.html">WeWork&amp;rsquo;s &amp;ldquo;Good Enough&amp;rdquo; Order Guarantee&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 RabbitMQ 也能做「per-key ordering」、用 consistent hash exchange 模擬 partition。</p>
<h2 id="觀察">觀察</h2>
<p>訊息順序對某些業務流程關鍵、但全局排序代價高。WeWork 採固定數量 queue + 用 account ID hash 路由到特定 queue。</p>
<h2 id="判讀">判讀</h2>
<p>每個 queue 一個 SideKiq worker + exclusive consumer 保證單帳戶順序。文後發現 RabbitMQ Consistent Hashing plugin 已內建類似機制（類似 Kafka 分區）。揭露 partition-level ordering 不是 Kafka 專屬、在 broker model 可用 hash exchange 達成。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Exchange types / Prefetch + consumer 併發（partition-level ordering 模式）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>（partition + key 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.cloudamqp.com/blog/weworks-good-enough-order%20guarantee.html">WeWork&rsquo;s &ldquo;Good Enough&rdquo; Order Guarantee</a></li>
</ul>
]]></content:encoded></item><item><title>Immuta</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/immuta/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/immuta/</guid><description>&lt;p>Immuta 是 &lt;em>Universal Data Access Platform&lt;/em>、定位是 &lt;em>跨多 data warehouse 統一的 query-time access control + masking 抽象層&lt;/em>。它解的問題是 &lt;em>同一份 policy 要同時在 Snowflake、Databricks、BigQuery、Redshift、Synapse 上生效&lt;/em>、不必到每個 warehouse 內逐表寫 native RLS / masking。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a> 的差異在 &lt;em>policy abstraction layer + query-time enforcement + ABAC scale&lt;/em>、偵測或 classification 層面相近。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Immuta 核心定位是 &lt;em>data security platform&lt;/em>、以 &lt;em>Data Policy + Subject Policy&lt;/em> 為 first-class concept、走 &lt;em>Attribute-Based Access Control (ABAC)&lt;/em> 模型。底層機制是 &lt;em>Native Query Plan Rewriter&lt;/em> — analyst 寫 SQL 後 Immuta 攔截、解析 policy、把 row filter 跟 column mask &lt;em>translate 成各 warehouse native primitive&lt;/em>（Snowflake row access policy / dynamic masking、BigQuery RLS、Databricks Unity Catalog policy）後再交給 warehouse 執行。Performance 接近 native、不是 proxy 中轉。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy&lt;/a> 比、cloud-native（Snowflake Horizon / BigQuery column-level security / Redshift dynamic masking）限單一雲、政策語意散落在各 warehouse；Immuta 走 &lt;em>policy abstraction&lt;/em>、寫一次 policy 對多 warehouse 生效。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a> 比、Purview 強在 Office docs label + endpoint DLP、Immuta 強在 &lt;em>data warehouse query-time access control&lt;/em>、兩者場景不重疊。跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a> 比、DLP 是 &lt;em>classification / discovery / redaction service&lt;/em>、Immuta 是 &lt;em>access policy enforcement&lt;/em>、前者找敏感資料、後者管誰能看到。&lt;/p></description><content:encoded><![CDATA[<p>Immuta 是 <em>Universal Data Access Platform</em>、定位是 <em>跨多 data warehouse 統一的 query-time access control + masking 抽象層</em>。它解的問題是 <em>同一份 policy 要同時在 Snowflake、Databricks、BigQuery、Redshift、Synapse 上生效</em>、不必到每個 warehouse 內逐表寫 native RLS / masking。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 的差異在 <em>policy abstraction layer + query-time enforcement + ABAC scale</em>、偵測或 classification 層面相近。</p>
<h2 id="服務定位">服務定位</h2>
<p>Immuta 核心定位是 <em>data security platform</em>、以 <em>Data Policy + Subject Policy</em> 為 first-class concept、走 <em>Attribute-Based Access Control (ABAC)</em> 模型。底層機制是 <em>Native Query Plan Rewriter</em> — analyst 寫 SQL 後 Immuta 攔截、解析 policy、把 row filter 跟 column mask <em>translate 成各 warehouse native primitive</em>（Snowflake row access policy / dynamic masking、BigQuery RLS、Databricks Unity Catalog policy）後再交給 warehouse 執行。Performance 接近 native、不是 proxy 中轉。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> 比、cloud-native（Snowflake Horizon / BigQuery column-level security / Redshift dynamic masking）限單一雲、政策語意散落在各 warehouse；Immuta 走 <em>policy abstraction</em>、寫一次 policy 對多 warehouse 生效。跟 <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> 比、Purview 強在 Office docs label + endpoint DLP、Immuta 強在 <em>data warehouse query-time access control</em>、兩者場景不重疊。跟 <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> 比、DLP 是 <em>classification / discovery / redaction service</em>、Immuta 是 <em>access policy enforcement</em>、前者找敏感資料、後者管誰能看到。</p>
<p>關鍵張力：<em>多 warehouse 統一治理價值</em> ↔ <em>商業 SaaS 成本</em>。單一 warehouse（純 Snowflake）客戶 2024+ 用 Snowflake Horizon native 多半夠用、Immuta 進場理由是 <em>Snowflake + Databricks + BigQuery 並存</em>、且 analyst 數量大到 ABAC 比 RBAC 划算。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Immuta 在 data platform 承擔哪一段（query-time access control / masking / ABAC）、跟 <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> 的取捨</li>
<li>Data Policy / Subject Policy / ABAC 的 ownership 設計（Data steward / Compliance / Data engineering 各管什麼）</li>
<li>Query Plan Rewriter 的工作模式跟 native warehouse policy 的 fallback 邊界</li>
<li>何時用 Immuta、何時走 cloud-native policy / Privacera / Snowflake Horizon 的取捨</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Immuta deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Data Source registration coverage</strong>：哪些 warehouse / schema / table 已註冊到 Immuta、是否有 <em>uncovered shadow path</em>（analyst 還能繞過 Immuta 直連 warehouse 拿 raw data）— 沒覆蓋等於有 backdoor</li>
<li><strong>Subject Policy 跟 IdP attribute 對齊</strong>：user attribute（部門、地理、clearance）從哪個 IdP / HRIS pull、attribute 變更（離職 / 換部門）多快 propagate 到 Immuta、policy 是否真的用 attribute 而不是退化成「user A、user B」直接 grant</li>
<li><strong>Policy-as-code 跟 review flow</strong>：Data Policy 是 UI 改還是走 Git PR review、policy change 是否經 staging tenant 驗證、有沒有 <em>break-glass</em> 流程</li>
<li><strong>Audit log 串到 SIEM</strong>：Immuta query audit 是否進 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、query pattern 異常（同一 user 大量觸發 masking、跨 schema scan）有無 alert</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection by Design</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Data Source registration</strong>：把 warehouse 內的 schema / table 註冊成 Immuta <em>Data Source</em>、Immuta 透過 service account 連 warehouse、拉 metadata + 註冊到 policy plane。Snowflake / Databricks / BigQuery / Redshift / Synapse / Starburst 是 first-class、其他 warehouse 走 JDBC connector。註冊後 analyst 改透過 Immuta 取得的 <em>projected view</em> 查詢、不直連原始 table。</p>
<p><strong>Data Policy（row / column / masking）</strong>：policy 三類 — <em>Subscription Policy</em>（誰能訂閱 data source）、<em>Row-level Policy</em>（filter 哪些 row）、<em>Masking Policy</em>（column 值如何呈現：hash / null / regex redact / k-anonymity / differential privacy noise）。可走 UI 設定、也可走 Immuta CLI / API 寫成 YAML 進 Git PR review，後者是 mature deployment 的標配。</p>
<p><strong>Subject Policy + ABAC</strong>：policy 用 <em>user attribute</em> 寫（<code>department == 'finance' AND region == 'EU' AND clearance &gt;= 'restricted'</code>）、不是 user / role 直接 grant。Attribute 從 IdP（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD）/ HRIS（Workday）pull、Immuta Identity Manager 同步。ABAC 的價值在 scaling — 5000 個 analyst 用 RBAC 要管 hundreds of role、用 ABAC 寫 20 條 policy 涵蓋全部組合。</p>
<p><strong>Query Plan Rewriter</strong>：核心機制。analyst 對 Immuta data source 寫 SQL → Immuta 解析 query plan + 套用 user 對應 policy → 翻譯成 warehouse native primitive（Snowflake row access policy + dynamic masking function、BigQuery RLS、Databricks Unity Catalog policy）→ 交給 warehouse 執行。Performance 接近 native、不是 query proxy。意義是 <em>policy 抽象在 Immuta、執行在 warehouse</em>、不引入額外資料路徑。</p>
<p><strong>Identity Manager 跟 IdP integration</strong>：Immuta 串 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD / Keycloak、用 SCIM / SAML / OIDC sync user + attribute。注意 <em>attribute propagation lag</em> — 員工換部門、HRIS 更新後多久反映到 Immuta policy 決策、production deployment 常見 trap 是 propagation 不及時、離職員工 attribute 還在、Subject Policy 仍判通過。</p>
<p><strong>Audit log</strong>：每個 query 都產 audit event（user、attribute snapshot、data source、applied policy、masked column、row count）、串到 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> 做 detection。對應 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a> — query audit 是 <em>data warehouse layer 的 first-class signal</em>。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Immuta</th>
          <th>Privacera</th>
          <th>Cloud-native data policy</th>
          <th>Snowflake Horizon native</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>SaaS、按 data source / module / user</td>
          <td>SaaS、按 data source / user</td>
          <td>內含於 warehouse 計費</td>
          <td>內含於 Snowflake credit</td>
      </tr>
      <tr>
          <td>多 warehouse 統一</td>
          <td>強 — abstraction layer、policy 寫一次</td>
          <td>強 — 類似定位、Apache Ranger 血脈</td>
          <td>弱 — 各 warehouse 各寫各的</td>
          <td>無 — 限 Snowflake</td>
      </tr>
      <tr>
          <td>ABAC 成熟度</td>
          <td>強 — IdP / HRIS attribute 為一等公民</td>
          <td>強 — Ranger ABAC 模型</td>
          <td>中 — 各 warehouse 支援不一</td>
          <td>中 — Snowflake tag-based</td>
      </tr>
      <tr>
          <td>Query 執行模型</td>
          <td>Native Query Plan Rewrite（接近 native）</td>
          <td>類似 rewrite + proxy 混合</td>
          <td>Native（warehouse 內建）</td>
          <td>Native</td>
      </tr>
      <tr>
          <td>Differential privacy</td>
          <td>內建 aggregate noise / k-anonymity</td>
          <td>部分支援</td>
          <td>一般無</td>
          <td>一般無</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>多 warehouse + analyst 數量大 + 合規重</td>
          <td>多 warehouse + Hadoop 遺產 + Ranger 熟</td>
          <td>單一雲 / 預算敏感 / 中小規模</td>
          <td>純 Snowflake + 想避免額外 vendor</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>高 — policy / data source 數量多</td>
          <td>高 — 類似</td>
          <td>低 — policy 已在 warehouse 內</td>
          <td>低 — 不換 vendor</td>
      </tr>
  </tbody>
</table>
<p>選 Immuta 的核心訴求：<em>多 warehouse 並存 + ABAC 規模化 + 合規（HIPAA / GDPR / FedRAMP）要求 query-time enforcement + audit</em>、且能承擔商業 SaaS license 跟 policy-as-code lifecycle 投入。單一 Snowflake / 預算敏感 / 中小 data team 直接走 Snowflake Horizon 更划算。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>ABAC scaling beyond RBAC</strong>：RBAC 在 hundreds-of-analyst 規模會退化成 role explosion（finance-eu-restricted-q1、finance-eu-restricted-q2…）。ABAC 把 role 拆成 attribute 組合、policy 寫一次 <code>department == 'finance' AND region == 'EU'</code>、新 analyst 加入只要 attribute 對、自動繼承。實作 trap 是 attribute 設計 — 不能用 free-form string、要有 controlled vocabulary + HRIS 為 SSoT。</p>
<p><strong>Differential privacy 跟 aggregate query noise</strong>：Immuta 支援對 aggregate query（COUNT / SUM / AVG）注入 <em>Laplace / Gaussian noise</em> 避免重識別（re-identification）攻擊。場景是醫療 / 政府統計、analyst 看 aggregate 不該能逆推個人記錄。要決定 <em>epsilon</em>（privacy budget）— epsilon 小 noise 大、analyst 抱怨數字不準；epsilon 大 noise 小、privacy 保障弱。</p>
<p><strong>跟 dbt / Airflow 整合</strong>：data pipeline 內的 transform 也該受 policy 控制 — dbt 模型生成的 derived table 註冊回 Immuta、policy 自動繼承。Airflow DAG 用 service account 走 Immuta 的 <em>system account exemption</em> 路徑、跟 analyst query 區分 audit 來源。實務上是 <em>pipeline-aware policy</em> — 知道哪個 job 是 trusted ETL、哪個是 ad-hoc query。</p>
<p><strong>Native integration 細節</strong>：Snowflake 走 row access policy + dynamic masking function；Databricks 走 Unity Catalog row filter + column mask；BigQuery 走 authorized view + RLS；Redshift 走 RLS + dynamic data masking。Immuta 寫的 policy 翻譯成各 warehouse native object、可在 warehouse console 看到 generated artifact。Native integration 失效時（warehouse API rate limit / schema drift）會 fallback 到 <em>deny-by-default</em>、不是 silent allow。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Analyst 直連 warehouse 繞過 Immuta</strong>：service account 沒收緊、analyst 用 warehouse native credential 直查 — 收 warehouse user direct access、改強制走 Immuta projected view、用 warehouse network policy 鎖 IP</li>
<li><strong>Attribute propagation lag 導致離職員工仍能查</strong>：HRIS → Immuta sync 週期太長 — 縮 sync 頻率、配合 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> deprovisioning webhook 即時觸發 attribute revoke</li>
<li><strong>Policy 改完 production 出現 mass deny</strong>：UI 直改、沒走 staging tenant 驗證 — policy 進 Git、PR review、staging 跑代表性 query suite、roll-forward 監控 deny rate</li>
<li><strong>Query performance 退化</strong>：複雜 row filter + masking 翻譯後的 warehouse plan 沒命中 index — 用 Immuta query analyzer 看 generated SQL、調整 policy 寫法或加 warehouse-side optimization</li>
<li><strong>Audit log 沒進 SIEM</strong>：Immuta audit export 沒設、event sink 斷線 — 補 <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> HEC / Elastic ingest pipeline、加 lag alert</li>
<li><strong>計費暴衝</strong>：data source 數量爆炸（每張 table 註冊一次）、user count 估錯 — 用 Immuta usage dashboard 看 module-by-module、合併小 table 到 schema-level policy</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一雲 / 預算敏感 / 中小 data team</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a></td>
      </tr>
      <tr>
          <td>純 Snowflake、不想引額外 vendor</td>
          <td>Snowflake Horizon native（內建 row access policy + dynamic masking）</td>
      </tr>
      <tr>
          <td>Hadoop / Ranger 遺產重</td>
          <td>Privacera（Apache Ranger 商業化、跟 Hadoop ecosystem 整合）</td>
      </tr>
      <tr>
          <td>敏感資料 discovery / classification</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Office docs / endpoint DLP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>Object storage / file-level policy</td>
          <td>Cloud-native IAM + bucket policy（Immuta 不管 raw S3 / GCS）</td>
      </tr>
      <tr>
          <td>Query audit 後的 detection</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Immuta CLI / API 完整語法 reference、policy YAML schema 細節</li>
<li>各 warehouse 的 native policy primitive 對應細節（Snowflake row access policy / Databricks Unity Catalog policy 語法）</li>
<li>Differential privacy 數學（epsilon / delta / Laplace mechanism 證明）</li>
<li>Hadoop ecosystem 整合（HDFS / Hive / Impala — 屬 Privacera 主場）</li>
<li>Object storage / file-level access control（屬 cloud IAM）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Immuta 在 07 案例庫沒有直接 vendor-level 事件、但所有 data warehouse credential / access 相關 case 都是 query-time enforcement 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Immuta 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>Immuta query-time ABAC 在 credential 外洩後仍限制 attacker 看到的 row + masked column、減 blast radius；對照啟示是「multi-tenant data warehouse 必須有 query-time 層」、不能只靠 credential / network 層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>Immuta 對 support tool 連到 backend warehouse 的 query 套 attribute-based filter、限 support user 只看授權 tenant、避免 internal tool 變 cross-tenant 提權路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a></td>
          <td>對照啟示：Immuta 主要在 query-time layer、backup / cold storage 場景仍需 storage-layer policy + IAM 隔離、不要把 Immuta 當 storage encryption 替代</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護設計</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a>、<a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（query audit 進 SIEM）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP attribute 來源）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（warehouse service credential 管理）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（query audit anomaly → IR routing）</li>
<li>官方：<a href="https://documentation.immuta.com/">Immuta Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C28 FanDuel：體育直播 + 投注的雙重峰值</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/</guid><description>&lt;p>這個案例的核心責任是說明「雙重峰值對齊」的工程取捨。FanDuel 同時運營體育直播（live streaming）跟體育投注（betting）、兩個工作負載在 &lt;em>同一場 NFL Super Bowl&lt;/em> 同時達到峰值、但 SLO 完全不同 — 直播容忍 30 秒延遲、投注必須毫秒內成交。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>FanDuel 在 AWS 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/fanduel-case-study/">FanDuel Case Study&lt;/a>）：&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>月活客戶&lt;/td>
 &lt;td>3.5 M+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>美國 20+ 州 + 加拿大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>峰值擴容倍數&lt;/td>
 &lt;td>5-10x（NFL Super Bowl 等大型賽事）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>AWS Local Zones + Wavelength + Outposts&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>峰值類型&lt;/td>
 &lt;td>直播 + 投注雙峰&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵敘述：「seamlessly scale capacity 5–10 times as required for large sporting events, such as the NFL Super Bowl」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>FanDuel 案例揭露三個雙重峰值對齊的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>直播跟投注是兩種完全不同 SLO&lt;/strong>：直播容忍秒級延遲（用 CDN + ABR 串流）、投注必須毫秒級成交（Super Bowl 進球瞬間、賠率變動、用戶投注必須在賠率變化前完成）。兩個服務必須各自獨立擴容、各自獨立 SLO。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的多 SLO 對齊。&lt;/li>
&lt;li>&lt;strong>AWS Local Zones / Wavelength / Outposts 是地理 + 監管雙重需求&lt;/strong>：美國博彩受各州監管、資料必須留在州內 → 用 Local Zones 在每個州就近部署；4G/5G 用戶投注延遲敏感 → 用 Wavelength 在電信商機房內運算；on-prem 需求 → 用 Outposts。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 的受監管雙重需求、跟 &lt;a href="https://tarrragon.github.io/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&lt;/a> 的延遲反推 region。&lt;/li>
&lt;li>&lt;strong>5-10x 是「同類事件中的最高倍率」&lt;/strong>：Super Bowl 是 NFL 賽季最大事件、不是常態。平日 baseline → 季後賽 2-3x → 季冠軍賽 4-5x → Super Bowl 5-10x。容量規劃要按事件級別分段、不是一律 10x。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的事件型容量分級。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>AWS 案例 &lt;em>沒有&lt;/em> 提具體 betting transaction TPS、concurrent streams、延遲分布。讀者要對 &lt;em>策略&lt;/em> 學習、不要套用具體數字。&lt;/li>
&lt;li>「5-10x」是 &lt;em>峰值倍數&lt;/em>、不是 &lt;em>peak 持續時間&lt;/em>。Super Bowl 的關鍵 30 分鐘可能 8-10x、其他 3 小時可能 3-5x。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「雙重峰值對齊」的工程取捨。FanDuel 同時運營體育直播（live streaming）跟體育投注（betting）、兩個工作負載在 <em>同一場 NFL Super Bowl</em> 同時達到峰值、但 SLO 完全不同 — 直播容忍 30 秒延遲、投注必須毫秒內成交。</p>
<h2 id="觀察">觀察</h2>
<p>FanDuel 在 AWS 的關鍵敘述（引自 <a href="https://aws.amazon.com/solutions/case-studies/fanduel-case-study/">FanDuel Case Study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月活客戶</td>
          <td>3.5 M+</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>美國 20+ 州 + 加拿大</td>
      </tr>
      <tr>
          <td>峰值擴容倍數</td>
          <td>5-10x（NFL Super Bowl 等大型賽事）</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>AWS Local Zones + Wavelength + Outposts</td>
      </tr>
      <tr>
          <td>峰值類型</td>
          <td>直播 + 投注雙峰</td>
      </tr>
  </tbody>
</table>
<p>關鍵敘述：「seamlessly scale capacity 5–10 times as required for large sporting events, such as the NFL Super Bowl」。</p>
<h2 id="判讀">判讀</h2>
<p>FanDuel 案例揭露三個雙重峰值對齊的工程重點。</p>
<ol>
<li><strong>直播跟投注是兩種完全不同 SLO</strong>：直播容忍秒級延遲（用 CDN + ABR 串流）、投注必須毫秒級成交（Super Bowl 進球瞬間、賠率變動、用戶投注必須在賠率變化前完成）。兩個服務必須各自獨立擴容、各自獨立 SLO。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的多 SLO 對齊。</li>
<li><strong>AWS Local Zones / Wavelength / Outposts 是地理 + 監管雙重需求</strong>：美國博彩受各州監管、資料必須留在州內 → 用 Local Zones 在每個州就近部署；4G/5G 用戶投注延遲敏感 → 用 Wavelength 在電信商機房內運算；on-prem 需求 → 用 Outposts。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 的受監管雙重需求、跟 <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> 的延遲反推 region。</li>
<li><strong>5-10x 是「同類事件中的最高倍率」</strong>：Super Bowl 是 NFL 賽季最大事件、不是常態。平日 baseline → 季後賽 2-3x → 季冠軍賽 4-5x → Super Bowl 5-10x。容量規劃要按事件級別分段、不是一律 10x。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的事件型容量分級。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>AWS 案例 <em>沒有</em> 提具體 betting transaction TPS、concurrent streams、延遲分布。讀者要對 <em>策略</em> 學習、不要套用具體數字。</li>
<li>「5-10x」是 <em>峰值倍數</em>、不是 <em>peak 持續時間</em>。Super Bowl 的關鍵 30 分鐘可能 8-10x、其他 3 小時可能 3-5x。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>不同 SLO 的工作負載分開部署、不要混在同一 service</strong>：betting 跟 streaming 在 FanDuel 必然是兩個獨立微服務、各自有 dedicated infrastructure。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 service decomposition、跟 <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a> 同思維。</li>
<li><strong>多層 edge（Local Zone / Wavelength / Outposts）服務不同延遲需求</strong>：Local Zone 服務「州內合規」需求、Wavelength 服務「電信網內超低延遲」、Outposts 服務「on-prem 監管」需求。三者組合對應跨州博彩業務。</li>
<li><strong>事件型容量規劃分級</strong>：建立 event tier 體系（regular game / playoff / championship / super bowl），每 tier 對應不同 pre-scale 倍數。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 的容量分級。</li>
</ol>
<p>跨平台等效：Azure 提供類似 stack（Stack Edge + Edge Zones + Azure for Operators）、GCP 有 Network Edge + Distributed Cloud。差異是各家 edge 覆蓋深度跟電信商合作。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他事件型峰值 → <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a>（賽事高潮 AI 預測）/ <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></li>
<li>想設計多 SLO 對齊 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a></li>
<li>想做受監管多地區部署 → <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> + <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></li>
<li>想做 edge / Local Zone 規劃 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
<li>想理解雙峰下 Aurora storage / replica scaling → <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">Aurora 儲存層架構</a> + <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 / 合規）">Aurora read replica scaling</a></li>
<li>想評估 distributed SQL 在 betting 場景的 fit → <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/fanduel-case-study/">FanDuel Case Study</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/fanduel-cloudfront-case-study/">FanDuel CloudFront Case Study</a></li>
</ul>
]]></content:encoded></item><item><title>TimescaleDB Deep Dive：Hypertable / Continuous Aggregate / Compression 把 PG 變 Time-Series DB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/timescaledb-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/timescaledb-deep-dive/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>TimescaleDB extension&lt;/em> — 用 PG 解 time-series workload 的路徑、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem&lt;/a> 是 &lt;em>單一 extension 細節 vs ecosystem 全景&lt;/em> 的關係。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="timescaledb-是-pg-的-time-series-specialization">TimescaleDB 是 PG 的 &lt;em>Time-Series Specialization&lt;/em>&lt;/h2>
&lt;p>TimescaleDB 不是獨立 DB、是 PG extension：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">timescaledb&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完後、PG 多三個 time-series 專屬機制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Hypertable&lt;/strong>：對 time column 自動 partition、應用層看是一張表&lt;/li>
&lt;li>&lt;strong>Continuous aggregate&lt;/strong>：incremental refresh 的 materialized view&lt;/li>
&lt;li>&lt;strong>Compression&lt;/strong>：對舊 chunk 壓縮（columnar-like format）&lt;/li>
&lt;/ol>
&lt;p>跟專業 time-series DB（InfluxDB / Prometheus / VictoriaMetrics）對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>TimescaleDB&lt;/th>
 &lt;th>InfluxDB&lt;/th>
 &lt;th>Prometheus&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Query 語言&lt;/td>
 &lt;td>標準 SQL&lt;/td>
 &lt;td>InfluxQL / Flux&lt;/td>
 &lt;td>PromQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入效能&lt;/td>
 &lt;td>中（10-100K rows/s）&lt;/td>
 &lt;td>高（500K+ rows/s）&lt;/td>
 &lt;td>中（pull-based scrape）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>壓縮&lt;/td>
 &lt;td>90%+（columnar compression）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Join&lt;/td>
 &lt;td>完整 SQL join&lt;/td>
 &lt;td>弱&lt;/td>
 &lt;td>不支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跟既有 PG schema&lt;/td>
 &lt;td>同一個 DB、可 join&lt;/td>
 &lt;td>獨立&lt;/td>
 &lt;td>獨立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生態&lt;/td>
 &lt;td>完整 PG ecosystem&lt;/td>
 &lt;td>自家 ecosystem&lt;/td>
 &lt;td>自家 ecosystem&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Open source&lt;/td>
 &lt;td>Apache 2.0（部分功能 TSL license）&lt;/td>
 &lt;td>MIT&lt;/td>
 &lt;td>Apache 2.0&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>何時選 TimescaleDB&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>TimescaleDB extension</em> — 用 PG 解 time-series workload 的路徑、跟 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 是 <em>單一 extension 細節 vs ecosystem 全景</em> 的關係。</p></blockquote>
<hr>
<h2 id="timescaledb-是-pg-的-time-series-specialization">TimescaleDB 是 PG 的 <em>Time-Series Specialization</em></h2>
<p>TimescaleDB 不是獨立 DB、是 PG extension：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">timescaledb</span><span class="p">;</span></span></span></code></pre></div><p>加完後、PG 多三個 time-series 專屬機制：</p>
<ol>
<li><strong>Hypertable</strong>：對 time column 自動 partition、應用層看是一張表</li>
<li><strong>Continuous aggregate</strong>：incremental refresh 的 materialized view</li>
<li><strong>Compression</strong>：對舊 chunk 壓縮（columnar-like format）</li>
</ol>
<p>跟專業 time-series DB（InfluxDB / Prometheus / VictoriaMetrics）對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB</th>
          <th>InfluxDB</th>
          <th>Prometheus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 語言</td>
          <td>標準 SQL</td>
          <td>InfluxQL / Flux</td>
          <td>PromQL</td>
      </tr>
      <tr>
          <td>寫入效能</td>
          <td>中（10-100K rows/s）</td>
          <td>高（500K+ rows/s）</td>
          <td>中（pull-based scrape）</td>
      </tr>
      <tr>
          <td>壓縮</td>
          <td>90%+（columnar compression）</td>
          <td>高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Join</td>
          <td>完整 SQL join</td>
          <td>弱</td>
          <td>不支援</td>
      </tr>
      <tr>
          <td>跟既有 PG schema</td>
          <td>同一個 DB、可 join</td>
          <td>獨立</td>
          <td>獨立</td>
      </tr>
      <tr>
          <td>生態</td>
          <td>完整 PG ecosystem</td>
          <td>自家 ecosystem</td>
          <td>自家 ecosystem</td>
      </tr>
      <tr>
          <td>Open source</td>
          <td>Apache 2.0（部分功能 TSL license）</td>
          <td>MIT</td>
          <td>Apache 2.0</td>
      </tr>
  </tbody>
</table>
<p><strong>何時選 TimescaleDB</strong>：</p>
<ul>
<li>Application 已用 PG、不想多管一套 time-series DB</li>
<li>需要 join time-series 跟 application 表（user / device metadata）</li>
<li>不需 InfluxDB 級寫入速度（&lt; 100K rows/s）</li>
<li>Team SQL 熟、PromQL / Flux 學習成本不想付</li>
</ul>
<p><strong>何時選 InfluxDB / Prometheus（不選 TimescaleDB）</strong>：</p>
<ul>
<li>High-cardinality metric（10M+ unique series）— TSDB-purpose-built engine 在 cardinality 跟 retention 上比 hypertable 高效</li>
<li>Pull-based scrape model（Prometheus）跟 alerting / Grafana 生態深整合</li>
<li>PromQL operator（<code>rate()</code> / <code>histogram_quantile()</code>）對 metric query 比 SQL 直覺</li>
<li>TSL license 不能接受（TimescaleDB 部分功能在 Timescale License、不是純 Apache 2.0）</li>
<li>Operational team 已熟 InfluxDB / Prometheus、不想多學 PG 維運</li>
</ul>
<h2 id="hypertable自動-time-based-partitioning">Hypertable：自動 Time-based Partitioning</h2>
<p>普通 PG 表變 hypertable：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sensor_data</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">time</span><span class="w">        </span><span class="n">TIMESTAMPTZ</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">sensor_id</span><span class="w">   </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">temperature</span><span class="w"> </span><span class="n">DOUBLE</span><span class="w"> </span><span class="k">PRECISION</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">humidity</span><span class="w">    </span><span class="n">DOUBLE</span><span class="w"> </span><span class="k">PRECISION</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- 變 hypertable、按 time 自動 partition
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_hypertable</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;time&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Hypertable 機制：</p>
<ul>
<li>後台自動拆 <em>chunk</em>（child partition）by time interval（預設 7 天）</li>
<li>Application 看到的是 <code>sensor_data</code> 一張表、實際資料分散在 <code>_timescaledb_internal._hyper_*_chunk</code> 表</li>
<li>Query 自動 chunk pruning（只掃命中時間範圍的 chunk）</li>
</ul>
<p><strong>Chunk interval 選擇</strong>很關鍵：</p>
<table>
  <thead>
      <tr>
          <th>Chunk interval</th>
          <th>適用</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 小時</td>
          <td>高頻 metrics（每秒 100+ row）</td>
          <td>Chunk 太多、catalog 膨脹</td>
      </tr>
      <tr>
          <td>1 天</td>
          <td>中高頻（每秒 10-100 row）</td>
          <td>OK</td>
      </tr>
      <tr>
          <td>7 天（預設）</td>
          <td>中頻（每分鐘 row）</td>
          <td>OK</td>
      </tr>
      <tr>
          <td>30 天</td>
          <td>低頻（每小時 row）</td>
          <td>OK</td>
      </tr>
  </tbody>
</table>
<p>通用原則：<em>每個 chunk 25% RAM</em>、超過退化 disk IO。Production 監控 <code>chunk_size</code> 跟 <code>shared_buffers</code> ratio 自動調。</p>
<p><strong>Multi-dimensional hypertable</strong>（time + space partition）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 按 time + device_id 雙維 partition
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_hypertable</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;time&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">partitioning_column</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;sensor_id&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">number_partitions</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="mi">16</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>適用 sensor 數 1000+ 的 IoT workload、單 chunk 太大時用 space partition 拆。</p>
<h2 id="continuous-aggregatecaggincremental-materialized-view">Continuous Aggregate（CAGG）：Incremental Materialized View</h2>
<p>普通 PG materialized view 是 <em>全量重算</em>、TimescaleDB CAGG 是 <em>incremental refresh</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1 小時粒度聚合
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_hourly</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">continuous</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">time_bucket</span><span class="p">(</span><span class="s1">&#39;1 hour&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">hour</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">sensor_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="k">avg</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">avg_temp</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="k">max</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">max_temp</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="k">min</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">min_temp</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">sample_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">sensor_data</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="p">,</span><span class="w"> </span><span class="n">sensor_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- 加 refresh policy（每 30 分鐘 refresh 過去 1 天）
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_continuous_aggregate_policy</span><span class="p">(</span><span class="s1">&#39;sensor_hourly&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="n">start_offset</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="n">end_offset</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 minutes&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="n">schedule_interval</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 minutes&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>CAGG 機制：</p>
<ul>
<li>記錄哪些 time bucket 已 materialize、哪些 stale</li>
<li>Refresh 時只重算 stale bucket、不全量</li>
<li>Query CAGG 自動 fallback 到原 hypertable 補最新資料（real-time aggregation）</li>
</ul>
<p><strong>CAGG vs 普通 MV 對比</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB CAGG</th>
          <th>普通 PG MV</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Refresh 模式</td>
          <td>Incremental</td>
          <td>全量重算</td>
      </tr>
      <tr>
          <td>Refresh 時間</td>
          <td>秒級</td>
          <td>表大時數十分鐘</td>
      </tr>
      <tr>
          <td>Real-time fallback</td>
          <td>自動補最新</td>
          <td>不支援、需手動 union</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>多一份 aggregated</td>
          <td>多一份 aggregated</td>
      </tr>
      <tr>
          <td>Policy</td>
          <td>內建排程</td>
          <td>需 pg_cron / 外部排程</td>
      </tr>
  </tbody>
</table>
<p><strong>CAGG hierarchy</strong>（多層聚合）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 從 1 hour CAGG 再聚合到 1 day
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_daily</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">continuous</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">time_bucket</span><span class="p">(</span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">hour</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">day</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="n">sensor_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="k">avg</span><span class="p">(</span><span class="n">avg_temp</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">daily_avg</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">sensor_hourly</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="n">sensor_id</span><span class="p">;</span></span></span></code></pre></div><p>Application query 不同時間範圍時自動命中對應粒度、不必每次掃原始資料。</p>
<h2 id="compression把舊-chunk-壓-90">Compression：把舊 Chunk 壓 90%+</h2>
<p>舊 chunk 可以開啟 compression：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 開啟 compression（必須先設定 segment by）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sensor_data</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">timescaledb</span><span class="p">.</span><span class="n">compress</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">timescaledb</span><span class="p">.</span><span class="n">compress_segmentby</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;sensor_id&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">timescaledb</span><span class="p">.</span><span class="n">compress_orderby</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;time DESC&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- 自動壓縮 policy：7 天前 chunk 壓
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_compression_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Compression 機制：</p>
<ul>
<li>把 chunk 內 row 按 <code>segmentby</code> 分組</li>
<li>每組內按 <code>orderby</code> 排序後、把每 column 變成 <em>columnar array</em></li>
<li>對 array 用 type-specific 壓縮（Gorilla for float / delta-of-delta for timestamp / dictionary for string）</li>
</ul>
<p>實際壓縮率：</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>壓縮率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IoT sensor（重複值多）</td>
          <td>95-98%</td>
      </tr>
      <tr>
          <td>Application metrics</td>
          <td>90-95%</td>
      </tr>
      <tr>
          <td>Trade tick（隨機浮點）</td>
          <td>70-85%</td>
      </tr>
      <tr>
          <td>Log line（高 cardinality string）</td>
          <td>50-70%</td>
      </tr>
  </tbody>
</table>
<p><strong>Compression 限制</strong>（重要）：</p>
<ul>
<li>壓縮後 chunk <strong>不能 UPDATE / DELETE 單 row</strong>（要先 decompress）</li>
<li>壓縮後 chunk <strong>不能加 column</strong>（要 decompress 所有 chunk）</li>
<li>壓縮後 chunk 只能 <em>append new row</em>、不能改舊 row</li>
<li>DDL 變更（加 column / 改 index）需 decompress</li>
</ul>
<p>實務：compression 是 <em>write-once cold data</em> 的工具、active OLTP chunk 不開。</p>
<h2 id="retention-policy自動刪舊資料">Retention Policy：自動刪舊資料</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 1 年前 chunk 自動刪
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;1 year&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Retention drop 整個 chunk（不是 DELETE row）、O(1) 操作、不產生 bloat。</p>
<p>CAGG 有獨立 retention：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 原始資料只留 30 天、aggregated 留 5 年
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 days&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_hourly&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;5 years&#39;</span><span class="p">);</span></span></span></code></pre></div><p>這是 TimescaleDB 跟普通 PG partitioning 最大的價值差 — 普通 PG 要自己寫 cron drop partition、TimescaleDB policy 內建。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1chunk-size-不對catalog-膨脹">Case 1：Chunk size 不對、catalog 膨脹</h3>
<p><strong>情境</strong>：sensor 每秒寫 10 row、chunk_interval 設 1 小時、一年產 8760 chunk、<code>pg_class</code> 撐到 200 萬 row、planner 變慢。</p>
<p>修法：</p>
<ul>
<li>Chunk 數量上限 ~10000、超過 catalog overhead 出現</li>
<li>重設 chunk_interval：<code>SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 day');</code></li>
<li>已存在 chunk 不會自動 merge、要靠 retention drop 自然消化</li>
</ul>
<h3 id="case-2cagg-refresh-落後-real-time">Case 2：CAGG refresh 落後 real-time</h3>
<p><strong>情境</strong>：CAGG refresh policy 每 1 小時跑、application 期待「即時 dashboard」、看到的數字落後 1 小時。</p>
<p>修法：</p>
<ul>
<li>縮短 <code>schedule_interval</code>（5 分鐘）</li>
<li>用 <code>real-time aggregation</code>（預設 ON、CAGG 自動 union 原始資料）</li>
<li>確認 <code>materialized_only = false</code>（real-time aggregation 開啟）</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_hourly</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">materialized_only</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">false</span><span class="p">);</span></span></span></code></pre></div><h3 id="case-3compression-後想-update">Case 3：Compression 後想 UPDATE</h3>
<p><strong>情境</strong>：發現某個歷史 row 數值錯、想 UPDATE、報錯 <em>cannot update/delete from compressed chunk</em>。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 找到該 chunk 並 decompress
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">decompress_chunk</span><span class="p">(</span><span class="k">c</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">show_chunks</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">older_than</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="p">::</span><span class="nb">text</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;%_5_chunk&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- UPDATE 完再 compress 回去
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">sensor_data</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">temperature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">22</span><span class="p">.</span><span class="mi">5</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">...;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">compress_chunk</span><span class="p">(...);</span></span></span></code></pre></div><p>或設計階段就避免 — compression 用在 <em>immutable data</em>、有可能改的留未壓。</p>
<h3 id="case-4hypertable-不能加-fk-到-non-hypertable">Case 4：Hypertable 不能加 FK 到 non-hypertable</h3>
<p><strong>情境</strong>：想對 <code>sensor_data</code> 加 FK 到 <code>sensors</code> 表、報錯 <em>foreign key constraints with hypertables are not supported</em>。</p>
<p>修法：</p>
<ul>
<li>Application 層維護 referential integrity</li>
<li>或反過來：<code>sensors</code> 可以 FK 到 hypertable（特定方向支援）</li>
<li>TimescaleDB 2.11+ 部分支援 FK from hypertable、但限制多</li>
</ul>
<h3 id="case-5timescaledb-跟-pg-主版本對齊">Case 5：TimescaleDB 跟 PG 主版本對齊</h3>
<p><strong>情境</strong>：PG 升級 14 → 16、TimescaleDB extension 沒對應升級、PG 啟動 fail。</p>
<p>TimescaleDB 跟 PG 版本對齊矩陣：</p>
<table>
  <thead>
      <tr>
          <th>TimescaleDB</th>
          <th>支援 PG version</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2.11+</td>
          <td>13, 14, 15</td>
          <td></td>
      </tr>
      <tr>
          <td>2.13+</td>
          <td>13, 14, 15, 16</td>
          <td>加 PG 16 支援</td>
      </tr>
      <tr>
          <td>2.15.x</td>
          <td>13, 14, 15, 16</td>
          <td>最後支援 PG 13 的 minor</td>
      </tr>
      <tr>
          <td>2.16+</td>
          <td>14, 15, 16</td>
          <td>PG 13 drop</td>
      </tr>
      <tr>
          <td>2.17+</td>
          <td>14, 15, 16, 17</td>
          <td>PG 17 加入（需 17.2+ binary 對齊）</td>
      </tr>
      <tr>
          <td>2.18+</td>
          <td>14, 15, 16, 17</td>
          <td>PG 17 完整支援</td>
      </tr>
      <tr>
          <td>2.23+</td>
          <td>14, 15, 16, 17, 18</td>
          <td>PG 18 加入</td>
      </tr>
  </tbody>
</table>
<p>修法：</p>
<ul>
<li>升 PG 前先升 TimescaleDB 到支援目標 PG 版本的 extension</li>
<li>Production 升級順序：TimescaleDB minor upgrade → PG major upgrade → TimescaleDB final upgrade</li>
<li>Cloud managed（Timescale Cloud）自動處理</li>
</ul>
<h2 id="跟-pg-原生-partitioning-對比">跟 PG 原生 Partitioning 對比</h2>
<p>PG 10+ 有 declarative partitioning、不一定要 TimescaleDB：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB hypertable</th>
          <th>PG declarative partitioning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自動建 chunk</td>
          <td>是</td>
          <td>否（需手動或 pg_partman）</td>
      </tr>
      <tr>
          <td>Chunk pruning</td>
          <td>自動</td>
          <td>自動（需 partition key）</td>
      </tr>
      <tr>
          <td>Retention 內建</td>
          <td>是</td>
          <td>否（pg_partman 或自寫 cron）</td>
      </tr>
      <tr>
          <td>Compression</td>
          <td>內建 columnar</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Continuous aggregate</td>
          <td>內建</td>
          <td>否（自寫 incremental refresh）</td>
      </tr>
      <tr>
          <td>跨 chunk index</td>
          <td>統一 management</td>
          <td>Per-partition index</td>
      </tr>
      <tr>
          <td>Cardinality limit</td>
          <td>10000+ chunk OK</td>
          <td>1000+ partition 就慢</td>
      </tr>
  </tbody>
</table>
<p>何時用原生 partitioning（不用 TimescaleDB）：</p>
<ul>
<li>不需要 compression / CAGG</li>
<li>Partition 數 &lt; 1000</li>
<li>已用 pg_partman 不想換</li>
<li>公司禁用 TSL license（TimescaleDB 部分功能受限）</li>
</ul>
<p>何時用 TimescaleDB：</p>
<ul>
<li>高頻 time-series（compression 必要）</li>
<li>需要 CAGG（手寫 incremental MV 成本高）</li>
<li>Partition 數 &gt; 1000</li>
<li>IoT / metrics / observability workload</li>
</ul>
<p>詳細 partitioning 機制看 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative-partitioning</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a>：PG extension 全景</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative-partitioning</a>：原生 partitioning</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">jsonb-deep-dive</a>：IoT payload 用 JSONB 儲存</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum-tuning</a>：hypertable autovacuum 行為</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">major-version-upgrade</a>：TimescaleDB + PG 升級順序</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 了解其他 PG 擴展選項</li>
<li>回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 看全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C29 WeWork：Bunny + Puma 多執行緒 channel pool</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-bunny-channel-pool/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-bunny-channel-pool/</guid><description>&lt;p>這個案例的核心責任是說明 AMQP client 的 connection / channel 邊界跟執行緒模型緊密耦合。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>從 Unicorn 切到 Puma 後遇到 &lt;code>ConnectionClosedError&lt;/code>、根因是快取 Bunny channel 在多執行緒間共享。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>AMQP channel 不應跨執行緒共用、改用 &lt;code>connection_pool&lt;/code> gem 管理 channel pool。揭露 AMQP 不是 stateless HTTP-style client、channel 是 statefull 物件、多 thread 模型要特別處理。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Prefetch + consumer 併發（client library 層的 connection / channel 邊界）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://wework.github.io/ruby/rails/bunny/rabbitmq/threads/concurrency/puma/errors/2015/11/12/bunny-threads/">Bunny Threads in Puma at WeWork&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 AMQP client 的 connection / channel 邊界跟執行緒模型緊密耦合。</p>
<h2 id="觀察">觀察</h2>
<p>從 Unicorn 切到 Puma 後遇到 <code>ConnectionClosedError</code>、根因是快取 Bunny channel 在多執行緒間共享。</p>
<h2 id="判讀">判讀</h2>
<p>AMQP channel 不應跨執行緒共用、改用 <code>connection_pool</code> gem 管理 channel pool。揭露 AMQP 不是 stateless HTTP-style client、channel 是 statefull 物件、多 thread 模型要特別處理。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Prefetch + consumer 併發（client library 層的 connection / channel 邊界）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://wework.github.io/ruby/rails/bunny/rabbitmq/threads/concurrency/puma/errors/2015/11/12/bunny-threads/">Bunny Threads in Puma at WeWork</a></li>
</ul>
]]></content:encoded></item><item><title>Privacera</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/privacera/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/privacera/</guid><description>&lt;p>Privacera 是 &lt;em>data security + AI governance&lt;/em> SaaS 平台、由 Apache Ranger 核心 contributor 在 2016 創立、產品是 Ranger 的 commercial extension。核心定位是把 Hadoop / Hive / Trino ecosystem 慣用的 &lt;em>centralized policy + tag-based access control&lt;/em> 模式擴張到現代 cloud warehouse（Snowflake / Databricks / BigQuery / Redshift），並在 2023+ 加上 PAIG（Privacera AI Governance）處理 LLM application 的 prompt / response 治理。它跟 Immuta 是同類的 &lt;em>cross-warehouse data security platform&lt;/em>、但譜系跟強項不同 — Immuta 走 query rewriter + ABAC 原生、Privacera 走 Ranger heritage + AI governance。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Privacera 的 first-class concept 是 &lt;em>Policy Repository&lt;/em>（中央 policy store、所有 data source 共用一份規則）、底下接 &lt;em>Data Source Connector&lt;/em>（Snowflake / Databricks / Hive / Trino / Spark / S3 / BigQuery / Redshift）、上層產品包含：&lt;em>Access Manager&lt;/em>（Ranger-based、row / column / tag policy）、&lt;em>Data Discovery &amp;amp; Classification&lt;/em>（auto-scan + tag）、&lt;em>Encryption Gateway&lt;/em>（FPE + tokenization、在 query path 或 application 層 inline）、&lt;em>PAIG&lt;/em>（LLM prompt scan + response redaction、AI governance 子產品）。&lt;/p>
&lt;p>跟 Immuta 比、Privacera 走 &lt;em>Ranger heritage + AI governance 雙主軸&lt;/em> — 對既有 Apache Ranger 部署是天然 upgrade 路徑（policy schema / role model 接近）、PAIG 是少數把 LLM I/O 治理跟 data security policy 放同一個 platform 的選項；Immuta 走 &lt;em>query rewriter + ABAC 原生、cloud warehouse first&lt;/em>、現代 cloud-only 架構 onboarding 較快、但 LLM governance 需要外接。跟 &lt;em>Apache Ranger OSS&lt;/em> 比、Privacera 是 Ranger 的 SaaS 商業版 + 多 warehouse 擴張、不想付費可直接用 Ranger 但只覆蓋 Hadoop ecosystem、不含現代 warehouse connector / Discovery / PAIG。跟 &lt;em>cloud-native policy&lt;/em>（Snowflake row access policy / Databricks Unity Catalog / BigQuery column-level security）比、cloud-native 在單一 warehouse 內最便宜、但跨 warehouse + 跨 lake + LLM I/O 的 &lt;em>統一 policy 視圖&lt;/em> 需要 platform 層補位。&lt;/p></description><content:encoded><![CDATA[<p>Privacera 是 <em>data security + AI governance</em> SaaS 平台、由 Apache Ranger 核心 contributor 在 2016 創立、產品是 Ranger 的 commercial extension。核心定位是把 Hadoop / Hive / Trino ecosystem 慣用的 <em>centralized policy + tag-based access control</em> 模式擴張到現代 cloud warehouse（Snowflake / Databricks / BigQuery / Redshift），並在 2023+ 加上 PAIG（Privacera AI Governance）處理 LLM application 的 prompt / response 治理。它跟 Immuta 是同類的 <em>cross-warehouse data security platform</em>、但譜系跟強項不同 — Immuta 走 query rewriter + ABAC 原生、Privacera 走 Ranger heritage + AI governance。</p>
<h2 id="服務定位">服務定位</h2>
<p>Privacera 的 first-class concept 是 <em>Policy Repository</em>（中央 policy store、所有 data source 共用一份規則）、底下接 <em>Data Source Connector</em>（Snowflake / Databricks / Hive / Trino / Spark / S3 / BigQuery / Redshift）、上層產品包含：<em>Access Manager</em>（Ranger-based、row / column / tag policy）、<em>Data Discovery &amp; Classification</em>（auto-scan + tag）、<em>Encryption Gateway</em>（FPE + tokenization、在 query path 或 application 層 inline）、<em>PAIG</em>（LLM prompt scan + response redaction、AI governance 子產品）。</p>
<p>跟 Immuta 比、Privacera 走 <em>Ranger heritage + AI governance 雙主軸</em> — 對既有 Apache Ranger 部署是天然 upgrade 路徑（policy schema / role model 接近）、PAIG 是少數把 LLM I/O 治理跟 data security policy 放同一個 platform 的選項；Immuta 走 <em>query rewriter + ABAC 原生、cloud warehouse first</em>、現代 cloud-only 架構 onboarding 較快、但 LLM governance 需要外接。跟 <em>Apache Ranger OSS</em> 比、Privacera 是 Ranger 的 SaaS 商業版 + 多 warehouse 擴張、不想付費可直接用 Ranger 但只覆蓋 Hadoop ecosystem、不含現代 warehouse connector / Discovery / PAIG。跟 <em>cloud-native policy</em>（Snowflake row access policy / Databricks Unity Catalog / BigQuery column-level security）比、cloud-native 在單一 warehouse 內最便宜、但跨 warehouse + 跨 lake + LLM I/O 的 <em>統一 policy 視圖</em> 需要 platform 層補位。</p>
<p>關鍵張力：<em>Ranger heritage 的廣度</em> ↔ <em>現代 cloud-only 的部署速度</em> 是 Privacera vs Immuta 最常見的取捨。Hadoop / Hive / Trino 還在 production 又要管 Snowflake / Databricks，Privacera 的 connector 譜系比較貼；如果已經沒有 Hadoop 包袱、純 cloud warehouse + 不需 LLM governance，Immuta 或 cloud-native 是更輕的選擇。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Privacera 在 data security stack 中承擔哪一段（central policy / data source enforcement / discovery / LLM I/O governance）、跟 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">偵測覆蓋率與訊號治理</a> 的交界</li>
<li>Policy Repository / Data Source Connector / Encryption Gateway / PAIG 各自的 ownership 設計（誰寫 policy、誰 review、誰 own LLM prompt rule）</li>
<li>Apache Ranger OSS / Privacera SaaS / Immuta / cloud-native policy 的取捨</li>
<li>何時選 Privacera、何時走 Immuta / Ranger OSS / 純 cloud-native</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Privacera deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Policy Repository ownership</strong>：policy 是否走版控（Git → Privacera Policy API import）、誰能改 production policy、tag-based vs resource-based policy 比例（tag-based 是 sustainable 模式、resource-based 不適合長期維護）</li>
<li><strong>Data Source Connector coverage</strong>：哪些 warehouse / lake 接上 Privacera（Snowflake / Databricks / Hive / Trino / S3 / BigQuery / Redshift）、是否有 source 還沒接、unmanaged source 跟 managed source 比例</li>
<li><strong>Discovery &amp; Classification 跑得到位</strong>：sensitive data tag（PII / PHI / PCI）是否 auto-scan 自動掛在 column / file 上、tag freshness（多久重 scan 一次）、人工 review 流程</li>
<li><strong>PAIG / Encryption Gateway 使用範圍</strong>：LLM application 是否走 PAIG（prompt scan / response redaction）、sensitive table 是否走 Encryption Gateway 的 FPE / tokenization、application 是否還在用明文路徑繞過 gateway</li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Policy Repository（central policy store）</strong>：所有 data source 共用一份 <em>policy + tag</em> 定義、policy 不綁特定 source 而是綁 tag（<code>PII.email</code> 在 Snowflake / Hive / S3 對 finance role 都 mask）。Repository 走 Git 同步是 production 標準作法、不能讓 SRE 在 console 直接改 production policy。policy change 經 PR review + staging tenant 跑 24-48hr 觀察 query failure rate 才 promote。</p>
<p><strong>Data Source Connector</strong>：每個 warehouse / lake 一個 connector、connector 把 Privacera policy 翻譯成 source 原生機制（Snowflake row access policy + masking policy、Databricks Unity Catalog grant、Hive Ranger plugin、Trino access control plugin、S3 bucket policy）。意義是 <em>user 直接連 source</em> — query path 不走 Privacera proxy、Privacera 只負責 policy 推送 + audit pull。比 query rewriter / proxy 架構（Immuta 部分模式）latency 影響低、但 connector breakage 時可能 fail-open，需要 connector health monitoring。</p>
<p><strong>Access Manager（Ranger-based）</strong>：UI 跟 Apache Ranger 接近 — <em>resource-based policy</em>（指定 database / table / column）跟 <em>tag-based policy</em>（指定 tag、跨 source 套用）兩種模式。生產建議走 tag-based 為主、resource-based 只用在臨時例外。Row filter / column mask / deny rule 是核心三類 policy、配對 IdP（Okta / Azure AD / SAML）拉 user attribute 做 ABAC 決策。</p>
<p><strong>Data Discovery &amp; Classification</strong>：scanner 跑遍 data source、auto-detect column 內容（regex / dictionary / ML-based classifier）、自動掛 tag（<code>PII.email</code> / <code>PHI.diagnosis</code> / <code>PCI.card_number</code>）。tag freshness 是工程議題 — schema 變動後多久重 scan、scan cost 怎麼控、false positive tag 如何 review。Discovery 結果應該是 <em>建議 tag、人工 confirm</em>、不該全自動套 policy。</p>
<p><strong>PAIG（Privacera AI Governance）</strong>：2023+ 推、針對 LLM application 的 <em>prompt scan + response redaction</em> 子產品。流程是 application 在送 prompt 到 LLM endpoint 前先過 PAIG（檢查 prompt 內 PII / 機敏內容、決定 redact / block / log）、LLM 回 response 後再過 PAIG（redact 不該外洩的 token、檢查 response 是否含 sensitive 內容）。跟 OpenAI / Anthropic / Azure OpenAI 等 endpoint 整合走 SDK wrapper 或 proxy 模式。對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">AI / LLM governance</a> 章節的 data-side policy。</p>
<p><strong>Encryption Gateway（FPE + tokenization）</strong>：可在 <em>query path</em>（warehouse 內 column 存 token、query 時 decrypt）或 <em>application 層</em>（application 取資料前先過 gateway 換 token）做 inline encrypt / decrypt。FPE 保留資料 format（信用卡號加密後還是 16 碼數字）、application 不需改 schema。使用要看 <em>誰持有 key</em>（Privacera 託管 vs 自帶 KMS）、failure mode（gateway 掛掉時 application 行為）跟 latency 預算。</p>
<p><strong>跟 IdP integration</strong>：user / role / attribute 從 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / Azure AD / SAML IdP 拉、ABAC 決策依賴 IdP attribute（department、clearance level、project tag）。IdP attribute 治理品質直接決定 Privacera policy 品質 — IdP 內 attribute 亂、Privacera policy 不可能準。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Privacera</th>
          <th>Immuta</th>
          <th>Apache Ranger OSS</th>
          <th>Cloud-native policy（Snowflake / Unity Catalog / BigQuery）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>譜系</td>
          <td>Ranger commercial fork</td>
          <td>Cloud warehouse-first、原生 ABAC</td>
          <td>Hadoop ecosystem OSS</td>
          <td>單一 warehouse 廠商原生</td>
      </tr>
      <tr>
          <td>Source 覆蓋</td>
          <td>廣 — Hadoop + 多 cloud warehouse + LLM</td>
          <td>廣 — cloud warehouse + lake</td>
          <td>Hadoop ecosystem only</td>
          <td>單一 warehouse 內</td>
      </tr>
      <tr>
          <td>Policy 模式</td>
          <td>Tag-based + resource-based（Ranger 風）</td>
          <td>Query rewriter + ABAC attribute</td>
          <td>Resource-based + tag-based（基本版）</td>
          <td>Warehouse 原生 row / column policy</td>
      </tr>
      <tr>
          <td>LLM governance</td>
          <td>PAIG（內建）</td>
          <td>無原生、需外接</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Encryption</td>
          <td>Encryption Gateway（FPE + tokenization）</td>
          <td>Masking + format-preserving 部分</td>
          <td>基本 masking</td>
          <td>Warehouse 原生 dynamic masking</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>Enterprise SaaS（按 source / module）</td>
          <td>Enterprise SaaS（按 source / user）</td>
          <td>OSS（免費、自管成本高）</td>
          <td>通常含在 warehouse spend</td>
      </tr>
      <tr>
          <td>部署速度</td>
          <td>中 — Ranger 熟悉者快</td>
          <td>中 — cloud-only 快</td>
          <td>慢 — 自管 Ranger admin / KMS</td>
          <td>快 — 直接寫 warehouse SQL</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Hadoop + 現代 warehouse 混合 + AI 導入</td>
          <td>純 cloud warehouse + ABAC 重</td>
          <td>純 Hadoop ecosystem + 預算敏感</td>
          <td>單一 warehouse 內 + 跨 warehouse 不密</td>
      </tr>
      <tr>
          <td>退場成本</td>
          <td>中高 — policy 量 + connector + PAIG rule</td>
          <td>中高 — policy + ABAC attribute</td>
          <td>低</td>
          <td>低（policy 已在 warehouse）</td>
      </tr>
  </tbody>
</table>
<p>選 Privacera 的核心訴求：<em>Apache Ranger 已部署想 upgrade 到管理 platform</em>、或 <em>Hadoop / Hive / Trino + 現代 cloud warehouse 混合架構需要單一 policy 視圖</em>、或 <em>AI / LLM application 開始導入且資料治理要跟 LLM I/O policy 同 plane</em>。純 cloud-only + 不碰 LLM 走 Immuta 或 cloud-native 更輕。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>PAIG 的 prompt / response governance</strong>：LLM application 的 data security 問題在 <em>prompt 內帶 PII 進 LLM context</em>（資料外洩到第三方）跟 <em>response 含 sensitive 內容流回 user</em>（policy bypass）。PAIG 在這兩個邊界做 redact / block / log、把資料治理規則套到 LLM I/O。實作關鍵是 <em>latency 預算</em>（每個 prompt 過一次 scan）、<em>false positive 容忍度</em>（redact 太多 LLM 回答品質掉）、<em>audit log retention</em>（哪些 prompt 該保留多久）。</p>
<p><strong>Encryption Gateway 的 key ownership</strong>：FPE / tokenization 的安全性核心是 <em>誰持有 key</em>。Privacera 託管 key 是最快上線方案、但 vendor compromise 等於資料明文外洩風險；自帶 KMS（AWS KMS / Azure Key Vault / GCP KMS）grant Privacera 使用權限是 production 推薦、key rotation / revoke 自己掌握。Gateway down 時 fail-open（直通明文）vs fail-closed（application 報錯）要明確定義。</p>
<p><strong>Apache Ranger OSS 遷移路徑</strong>：Ranger OSS deployment 升級到 Privacera 通常走 <em>policy export → Privacera import</em> + <em>connector 改接 Privacera plugin</em> 的階段性遷移、不是 big-bang。Privacera Ranger plugin 跟 OSS Ranger plugin 行為兼容、可以混用一段時間。遷移期間 <em>policy schema 差異</em>（Privacera 加的 tag / Discovery 欄位 Ranger OSS 沒有）需要處理。</p>
<p><strong>Compliance template</strong>：GDPR / HIPAA / CCPA / PCI-DSS 的 compliance pack 提供 <em>預定義 tag 集 + policy 範本</em>（自動 mask EU resident 的 PII、PHI 只給特定 clearance role）。template 是起點不是終點 — organization 的實際 compliance 需求通常更細、template 只覆蓋通用條款。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Query 大量 fail / user 抱怨拿不到資料</strong>：新 policy promote 沒經 staging 觀察、tag 自動套到太廣範圍 — rollback policy、staging tenant 跑 query replay 找 affected query、tune tag scope</li>
<li><strong>Connector breakage 後 fail-open</strong>：Privacera policy 沒推到 source、source 還是用舊 policy 或全開 — connector health monitoring + alert、定期 audit policy sync diff</li>
<li><strong>Discovery scan 找不到敏感 column</strong>：classifier rule 沒涵蓋 organization-specific 格式（內部員工編號 / 客戶 ID 自訂格式）— 加 custom regex / dictionary classifier、人工 review tag 補漏</li>
<li><strong>PAIG redact 太兇 / LLM 回答品質掉</strong>：prompt scan rule 寫太寬、把無關 token 也 redact — staging 環境 replay LLM session 觀察 redact 比例、tune classifier threshold、加 allow-list</li>
<li><strong>Encryption Gateway latency 變高</strong>：gateway pod 不夠 / inline 模式擋在 hot path — scale gateway、評估 <em>application 側 cache token mapping</em> 或 <em>batch decrypt</em>、不是所有 query 都過 gateway</li>
<li><strong>Policy 版控漂移</strong>：SRE 在 console hotfix 沒回寫 Git、Git policy 跟 production 不同步 — disable console edit for production policy、policy change 強制走 Git PR</li>
<li><strong>IdP attribute 亂 / ABAC 決策不準</strong>：user department / clearance 在 IdP 沒人維護、Privacera 拉的 attribute 跟實際角色不符 — 修 IdP 側 attribute lifecycle（onboarding / role change / offboarding）、不是 Privacera 加更多 policy 補</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 cloud warehouse + ABAC 重</td>
          <td>Immuta（同類 platform、cloud-first）</td>
      </tr>
      <tr>
          <td>純 Hadoop ecosystem + 預算敏感</td>
          <td>Apache Ranger OSS（自管）</td>
      </tr>
      <tr>
          <td>單一 warehouse 內 policy 夠用</td>
          <td>Snowflake row access policy / Databricks Unity Catalog / BigQuery column-level security</td>
      </tr>
      <tr>
          <td>DLP / sensitive data discovery only</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
      </tr>
      <tr>
          <td>純 LLM I/O guardrail（不含 data security）</td>
          <td>LLM-specific guardrail（Lakera / Protect AI / cloud provider 原生 content safety）</td>
      </tr>
      <tr>
          <td>SIEM / detection</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
      </tr>
      <tr>
          <td>IdP / SSO 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Apache Ranger OSS 的 admin / plugin 自管細節（policy DB schema、ranger-admin tuning）</li>
<li>PAIG 的 LLM SDK wrapper / proxy 模式選擇（SDK 整合屬 application engineering）</li>
<li>Encryption Gateway 的 FPE 演算法選型（NIST FF1 / FF3-1 等 cryptographic primitive 細節）</li>
<li>Privacera vs Immuta 的逐 feature checklist（產品快速迭代、列了會很快過期）</li>
<li>Snowflake / Databricks / BigQuery 各自原生 policy 的完整 reference（屬 warehouse vendor 文件）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Privacera 在 07 案例庫沒有直接 vendor-level 事件、但跨 warehouse + 加密 / tokenization 相關 case 都是 platform-level data security 的對照：</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Privacera 的關係（對照啟示）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 Credential Abuse</a></td>
          <td>credential 外洩後仍要靠 query-time access control + tag-based masking 限制 query 範圍、Privacera Access Manager 跟 Immuta 同類補位、不能只靠 IdP MFA</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023 Support Tool Abuse</a></td>
          <td>support / 內部工具連 warehouse 必須走 Privacera policy gate、support role 看到的欄位該預設 mask、不是相信 application 層的 UI 隱藏</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 Backup Chain</a></td>
          <td>Privacera Encryption Gateway 對 backup data 做 FPE / tokenization、即使 backup 外洩攻擊者拿到的也是 token、key ownership 一定要自帶 KMS</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>（cross-warehouse mask / tokenization policy）、<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11 資料駐留、刪除與證據鏈</a>（資料分類 + 證據鏈跟 Discovery tag 對接）</li>
<li>平行：Immuta（同類 cross-warehouse data security platform、cloud-first）、Apache Ranger OSS（Hadoop ecosystem 自管）</li>
<li>下游：<a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> / <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>（DLP 跟 Discovery 互補、tag 來源可共用）</li>
<li>跨類：<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（IdP attribute 來源、ABAC policy 依賴）、<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>（Encryption Gateway 的 KMS / key broker 選項）</li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（Privacera audit log → SIEM correlation）</li>
<li>官方：<a href="https://docs.privacera.com/">Privacera Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/</guid><description>&lt;p>這個案例的核心責任是說明「電信商級新串流服務」如何用雲端服務快速 launch + scale。Lemino 是 NTT DOCOMO 在 2023-04 推出的串流服務、3 個月達 5M MAU、工程工時下降 90% — 這個「不用大量工程師」的營運模式靠的是 managed services 組合、不是自建。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>NTT DOCOMO Lemino 在 AWS 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/ntt-docomo-lemino/">Lemino Case Study&lt;/a>）：&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>3 個月 MAU&lt;/td>
 &lt;td>500 萬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同時直播頻道&lt;/td>
 &lt;td>30 channels（規劃擴到 50）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DynamoDB 請求峰值&lt;/td>
 &lt;td>tens of thousands req/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程工時下降&lt;/td>
 &lt;td>90%（vs 自建）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>啟動年份&lt;/td>
 &lt;td>2023-04&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：AWS Media Services（Elemental Link、MediaConnect、MediaLive、MediaPackage）、Amazon Aurora、Amazon DynamoDB、DynamoDB Accelerator (DAX)、Amazon OpenSearch Service。&lt;/p>
&lt;p>關鍵敘述：採用 DynamoDB 的原因 — 「connection limits became bottlenecks when experiencing a rapid increase in access」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Lemino 案例揭露三個現代串流服務啟動的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>「connection limit 是 RDB 的隱性 bottleneck」是 OLTP 在 surge 下的典型問題&lt;/strong>：傳統 RDB（PostgreSQL、MySQL）每個連線吃記憶體跟 process / thread、connection pool 上限通常 1K-5K 個。當突發流量湧入、第一個爆的不是 CPU 也不是 disk、是 &lt;em>連線數量&lt;/em>。DynamoDB 的 HTTP API 模型沒有 connection state、天然解決這個問題。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 connection pool 議題、跟 &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%">9.C20 Zomato&lt;/a> 遷移動機同類。&lt;/li>
&lt;li>&lt;strong>AWS Media Services 是「電視台級」串流基礎設施&lt;/strong>：Elemental Link（encoding）、MediaConnect（transport）、MediaLive（live encoding）、MediaPackage（packaging + DRM）— 這套 stack 過往是電視台才買得起的硬體設備、AWS 把它變成 pay-per-use 服務。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 vendor-specific 串流服務評估。&lt;/li>
&lt;li>&lt;strong>90% 工程工時下降 = 走 managed 路線的真正價值&lt;/strong>：傳統電信商 launch 串流服務、要養 50-100 個 SRE + DBA + network 工程師、Lemino 用 managed 服務只需 5-10 個。差距不在「能不能 launch」、在「launch 後的維運成本」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &amp;#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &amp;#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom&lt;/a> 的同類訴求。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「tens of thousands req/sec」可能指 2 萬或 8 萬、差距 4 倍。「3 個月 5M MAU」很亮眼、但 NTT DOCOMO 自身有 8000 萬+ 電信用戶可以推、不是純自然成長。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「電信商級新串流服務」如何用雲端服務快速 launch + scale。Lemino 是 NTT DOCOMO 在 2023-04 推出的串流服務、3 個月達 5M MAU、工程工時下降 90% — 這個「不用大量工程師」的營運模式靠的是 managed services 組合、不是自建。</p>
<h2 id="觀察">觀察</h2>
<p>NTT DOCOMO Lemino 在 AWS 的關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/ntt-docomo-lemino/">Lemino Case Study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3 個月 MAU</td>
          <td>500 萬</td>
      </tr>
      <tr>
          <td>同時直播頻道</td>
          <td>30 channels（規劃擴到 50）</td>
      </tr>
      <tr>
          <td>DynamoDB 請求峰值</td>
          <td>tens of thousands req/sec</td>
      </tr>
      <tr>
          <td>工程工時下降</td>
          <td>90%（vs 自建）</td>
      </tr>
      <tr>
          <td>啟動年份</td>
          <td>2023-04</td>
      </tr>
  </tbody>
</table>
<p>服務組合：AWS Media Services（Elemental Link、MediaConnect、MediaLive、MediaPackage）、Amazon Aurora、Amazon DynamoDB、DynamoDB Accelerator (DAX)、Amazon OpenSearch Service。</p>
<p>關鍵敘述：採用 DynamoDB 的原因 — 「connection limits became bottlenecks when experiencing a rapid increase in access」。</p>
<h2 id="判讀">判讀</h2>
<p>Lemino 案例揭露三個現代串流服務啟動的工程重點。</p>
<ol>
<li><strong>「connection limit 是 RDB 的隱性 bottleneck」是 OLTP 在 surge 下的典型問題</strong>：傳統 RDB（PostgreSQL、MySQL）每個連線吃記憶體跟 process / thread、connection pool 上限通常 1K-5K 個。當突發流量湧入、第一個爆的不是 CPU 也不是 disk、是 <em>連線數量</em>。DynamoDB 的 HTTP API 模型沒有 connection state、天然解決這個問題。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 connection pool 議題、跟 <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> 遷移動機同類。</li>
<li><strong>AWS Media Services 是「電視台級」串流基礎設施</strong>：Elemental Link（encoding）、MediaConnect（transport）、MediaLive（live encoding）、MediaPackage（packaging + DRM）— 這套 stack 過往是電視台才買得起的硬體設備、AWS 把它變成 pay-per-use 服務。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 vendor-specific 串流服務評估。</li>
<li><strong>90% 工程工時下降 = 走 managed 路線的真正價值</strong>：傳統電信商 launch 串流服務、要養 50-100 個 SRE + DBA + network 工程師、Lemino 用 managed 服務只需 5-10 個。差距不在「能不能 launch」、在「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%">9.C19 Capcom</a> 的同類訴求。</li>
</ol>
<p>需要警惕：「tens of thousands req/sec」可能指 2 萬或 8 萬、差距 4 倍。「3 個月 5M MAU」很亮眼、但 NTT DOCOMO 自身有 8000 萬+ 電信用戶可以推、不是純自然成長。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>新串流服務優先選 DynamoDB / Cosmos DB / Bigtable 撐 metadata 層</strong>：避免 connection limit、避免 schema migration、避免 DBA 維運成本。</li>
<li><strong>AWS Media Services / GCP Media CDN / Azure Media Services 是新進入者快速 launch 的捷徑</strong>：不要重造串流 stack、直接用 vendor 提供的。</li>
<li><strong>DAX 是 DynamoDB 讀 cache 的標準解法</strong>：當讀峰值持續高（例如熱門節目首播、Hotstar 等級）、加 DAX 減少 DynamoDB 讀次數、降低成本。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a>。</li>
<li><strong>小團隊 + managed services 是電信商雲端轉型的範本</strong>：傳統電信商過去靠人海戰術、現在改靠 managed + 工程紀律。</li>
</ol>
<p>跨平台等效：GCP 提供 Media CDN + Anvato，Azure 提供 Media Services + Azure Front Door — 各家都有完整串流 stack。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他串流案例 → <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a>（live 直播）/ <a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a>（VOD metadata）</li>
<li>想理解 connection limit 議題 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <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></li>
<li>想做 DAX / cache 加速 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a></li>
<li>想規劃 managed-only 串流 stack → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a></li>
<li>想做串流 metadata 的 partition / GSI 設計 → <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key 反模式</a> + <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">DynamoDB GSI / LSI 設計</a></li>
<li>想評估 on-demand vs provisioned 給直播 / VOD 用 → <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 on-demand vs provisioned</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/solutions/case-studies/ntt-docomo-lemino/">NTT Docomo Rebuilds Infrastructure for Lemino Streaming Service Launch</a></li>
<li><a href="https://aws.amazon.com/media/direct-to-consumer-d2c-streaming/">Direct to Consumer &amp; Streaming on AWS</a></li>
</ul>
]]></content:encoded></item><item><title>Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/storage-architecture/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/storage-architecture/</guid><description>&lt;p>Aurora 把 storage 從「block device + WAL on local disk」重寫成跨 AZ 分散式 log service、compute node 只負責 process query 跟 generate redo log records。這個設計直接決定 read replica、failover、backup 跟跨 AZ replication 的物理上限 — 不理解 storage layer 設計、就無法解釋為什麼 &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 consolidation&lt;/a> 拿到 +75% 效能、為什麼 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings&lt;/a> replication lag 從 30 秒降到 10-30ms、為什麼 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 能同時把韌性跟性能當成單一目標。&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>）— 而是 storage-level 設計的實作層教學。覆蓋 quorum-based replication 的工程含義、「韌性即性能」frame 為什麼成立、OLTP workload 在 storage 設計下的讀寫雙峰錯位、跟容量規劃的判讀槓桿。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：團隊從 RDS PostgreSQL / 自管 PostgreSQL 遷到 Aurora、看到「跨 AZ replication lag 從秒級降到毫秒級」、但讀文件「quorum」「4-of-6」「分散式 storage」訊息密集、不知道哪些設計決策要相信、哪些是 marketing 詞。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「為什麼 Aurora 寫入比 RDS 還低、不是該因為跨 AZ network round-trip 而變慢？」&lt;/li>
&lt;li>「Storage layer 跟 compute layer 分離具體怎麼影響 backup、failover 跟 read replica？」&lt;/li>
&lt;li>「6 個 storage node 失去 2 個還能寫嗎？失去 3 個呢？」&lt;/li>
&lt;li>「Aurora 文件講『韌性』跟『性能』都用 storage 設計解釋、是同一件事還是兩件事？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：傳統工程文化把可靠性跟性能視為對立 — HA 投資（跨 AZ replication、failover 演練）通常被當成性能成本、不被視為性能來源。Aurora 設計反這個直覺、但讀者需要看到具體機制才能信。Standard Chartered case 揭露這個 frame 在受監管銀行業務（要求兩者同時達標）的價值；DraftKings 揭露具體數字（讀 &amp;lt; 1ms、寫 6ms）。&lt;/p>
&lt;h2 id="核心機制quorum-based-分散式-log">核心機制：quorum-based 分散式 log&lt;/h2>
&lt;p>Aurora storage 的 first-class concept 是 &lt;em>quorum 寫入 + 6-way 跨 AZ replication&lt;/em>。傳統 PostgreSQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication 在 compute 層做（streaming replication、logical replication）。Aurora 把 storage 拉到分散式 log service、6 個 storage node 各自獨立、application 看到的仍是 single primary SQL。&lt;/p></description><content:encoded><![CDATA[<p>Aurora 把 storage 從「block device + WAL on local disk」重寫成跨 AZ 分散式 log service、compute node 只負責 process query 跟 generate redo log records。這個設計直接決定 read replica、failover、backup 跟跨 AZ replication 的物理上限 — 不理解 storage layer 設計、就無法解釋為什麼 <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 consolidation</a> 拿到 +75% 效能、為什麼 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> replication lag 從 30 秒降到 10-30ms、為什麼 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 能同時把韌性跟性能當成單一目標。</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>）— 而是 storage-level 設計的實作層教學。覆蓋 quorum-based replication 的工程含義、「韌性即性能」frame 為什麼成立、OLTP workload 在 storage 設計下的讀寫雙峰錯位、跟容量規劃的判讀槓桿。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：團隊從 RDS PostgreSQL / 自管 PostgreSQL 遷到 Aurora、看到「跨 AZ replication lag 從秒級降到毫秒級」、但讀文件「quorum」「4-of-6」「分散式 storage」訊息密集、不知道哪些設計決策要相信、哪些是 marketing 詞。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「為什麼 Aurora 寫入比 RDS 還低、不是該因為跨 AZ network round-trip 而變慢？」</li>
<li>「Storage layer 跟 compute layer 分離具體怎麼影響 backup、failover 跟 read replica？」</li>
<li>「6 個 storage node 失去 2 個還能寫嗎？失去 3 個呢？」</li>
<li>「Aurora 文件講『韌性』跟『性能』都用 storage 設計解釋、是同一件事還是兩件事？」</li>
</ul>
<p>進一步問題：傳統工程文化把可靠性跟性能視為對立 — HA 投資（跨 AZ replication、failover 演練）通常被當成性能成本、不被視為性能來源。Aurora 設計反這個直覺、但讀者需要看到具體機制才能信。Standard Chartered case 揭露這個 frame 在受監管銀行業務（要求兩者同時達標）的價值；DraftKings 揭露具體數字（讀 &lt; 1ms、寫 6ms）。</p>
<h2 id="核心機制quorum-based-分散式-log">核心機制：quorum-based 分散式 log</h2>
<p>Aurora storage 的 first-class concept 是 <em>quorum 寫入 + 6-way 跨 AZ replication</em>。傳統 PostgreSQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication 在 compute 層做（streaming replication、logical replication）。Aurora 把 storage 拉到分散式 log service、6 個 storage node 各自獨立、application 看到的仍是 single primary SQL。</p>
<p><strong>Storage layout</strong>：每個 storage segment 跨 3 AZ × 2 node、共 6 個 storage node。一個 cluster 的 storage 被切成多個 10GB segment、每個 segment 6-way 複製。</p>
<p><strong>Quorum 設定</strong>：</p>
<ul>
<li>Write quorum：4-of-6（4 個 storage node 確認寫入才算 commit）— 容忍 1 AZ 失效 + 1 node 失效仍能寫</li>
<li>Read quorum：3-of-6（讀 3 個 node 取最新版本）— 比 write 小、降低 read latency</li>
<li>算術不對稱：寫嚴讀鬆是設計選擇、不是 marketing — durability 由寫端保證、讀端可以放寬</li>
</ul>
<p><strong>Write path 跟傳統 PostgreSQL 的差異</strong>：</p>
<ul>
<li>PostgreSQL primary：寫 WAL 到 local disk + dirty page flush + 透過 streaming replication 推到 replica</li>
<li>Aurora compute node：只送 <em>redo log records</em> 到 storage、不送整個 page；storage node 自己 apply redo log 重建 page、自己 checkpoint、自己 backup</li>
<li>工程含義：compute node 寫量小、CPU 不被 dirty page flush 佔用、寫入路徑變短</li>
</ul>
<p><strong>「韌性即性能」frame</strong>（<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 揭露）：</p>
<p>Aurora 把 HA 從 application-level（Patroni promotion + WAL catch-up）下推到 storage-level。設計含義是：storage 投資（6-way 跨 AZ replication）自動成為 read replica 的容量基底 — read replica 不需要 catch-up WAL、直接從共享 storage 讀、HA 預算同步轉成讀分流預算。</p>
<p>對 Standard Chartered 受監管銀行業務這代表：合規要求的 RPO / RTO 不能放棄、但業務也要求每秒 4000 TPS、兩者必須同時達成。傳統路徑要分別投資 HA（複雜的 streaming replication topology）跟性能（read replica catch-up tuning）、且兩個投資互相干擾。Aurora 讓 <em>同一份 storage 投資</em> 同時提供兩件事 — case「判讀」段第 2 點原話：「Aurora 的多 AZ storage + replica 同時提供性能（讀分流）跟韌性（故障切換）、達成 <em>韌性即性能</em> 的目標」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>。</p>
<p><strong>跟通用 quorum 概念差在哪</strong>：Aurora quorum 是 <em>storage-level</em>（不是 application-level Cassandra 風格）、application 看到 single primary SQL、不用感知 quorum；vs Cassandra application 要選 consistency level（ONE / QUORUM / ALL）。</p>
<h2 id="oltp-workload-shape讀寫雙峰錯位">OLTP workload shape：讀寫雙峰錯位</h2>
<p>Aurora 設計的工程含義在 application 層落地時、要看 workload 形狀。<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 揭露一個 OLTP 容量規劃的典型 pattern。</p>
<p><strong>DraftKings 揭露的雙峰錯位</strong>（case「觀察」段最後一行原文）：「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時是讀爆量（balance query）、payout event 時是寫爆量（ledger write）、兩個峰不在同一時刻。</p>
<p><strong>工程含義</strong>：</p>
<ul>
<li>讀寫資源規劃要分開、不能用「峰值總 TPS」單一數字規劃容量</li>
<li>讀峰拉 read replica 容量、寫峰靠 primary instance class 跟 commit batching、兩條路徑獨立預配</li>
<li>預估 headroom 也要分開：讀的 headroom 可以靠 auto-scale replica 接、寫的 headroom 要靠 primary 提前升 instance class（不能 auto-scale）</li>
</ul>
<p><strong>Application-level boundary</strong>：雙峰錯位是 <em>application 層</em> 拆讀寫 datasource 的決策訊號、storage layer 本身不解。Aurora 共享 storage 提供 lag 上限可預測（10-30ms）— 這是 read replica 變成「production-grade 可用」的前提、但讀寫分流要 application 端拆 read / write data source 才能落地。Storage 設計給的是「可預測的 lag 上限」、不是「自動讀寫分離」。</p>
<p><strong>跨 case 對照</strong>：</p>
<p><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> 揭露另一種雙峰 — 直播 + 投注 <em>兩種服務</em> 同時峰、不是同服務讀寫錯位。這兩種雙峰類型要分清楚：</p>
<ul>
<li>同服務讀寫錯位（DraftKings）：解法是 read / write data source 拆分、共享 Aurora cluster</li>
<li>跨服務雙峰（FanDuel）：解法是不同服務各自獨立擴容、betting 走 Aurora、streaming 走 CDN</li>
</ul>
<p>雙峰類型不同、容量規劃策略不同。</p>
<h2 id="step-by-step-配置--觀測">Step-by-step 配置 / 觀測</h2>
<p>Aurora storage 是 cluster-level、不暴露 segment-level config。讀者能影響的維度是 instance class、storage type、backup retention 跟 monitoring。</p>
<p><strong>Cluster 建立</strong>：</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 rds create-db-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">2</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">3</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --engine-version 15.5 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --master-username admin <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --master-user-password <span class="s2">&#34;</span><span class="k">$(</span>aws secretsmanager get-secret-value --secret-id db-password --query SecretString --output text<span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --storage-type aurora-iopt1 <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --backup-retention-period <span class="m">7</span></span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>--storage-type aurora-iopt1</code>：Aurora I/O-Optimized、月費高 30% 但無 I/O 收費；write-heavy + scan-heavy workload 才划算</li>
<li><code>--storage-type aurora</code>（預設）：Standard storage、按 I/O 計費；read-light workload 划算</li>
<li><code>--backup-retention-period 7</code>：1-35 天、影響 PITR 範圍</li>
</ul>
<p><strong>觀測 storage 狀態</strong>：</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 rds describe-db-clusters <span class="se">\
</span></span></span><span class="line"><span class="ln">2</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">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBClusters[0].{StorageType:StorageType,AllocatedStorage:AllocatedStorage,Status:Status}&#39;</span></span></span></code></pre></div><p><strong>CloudWatch metric</strong>（cluster-level）：</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">VolumeBytesUsed           # 當前 storage 用量、接近 128 TB 上限要警告
</span></span><span class="line"><span class="ln">2</span><span class="cl">VolumeReadIOPs            # storage 層讀 IOPS、判斷 I/O-Optimized ROI
</span></span><span class="line"><span class="ln">3</span><span class="cl">VolumeWriteIOPs           # storage 層寫 IOPS、跟 compute 層 WriteIOPS 對照
</span></span><span class="line"><span class="ln">4</span><span class="cl">AuroraVolumeBytesLeftTotal # 剩餘可用 storage</span></span></code></pre></div><p><strong>Performance Insights wait event</strong>：</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">db.IO.aurora_redo_log_flush   # quorum write 等待訊號、p99 &gt; 10ms 要看
</span></span><span class="line"><span class="ln">2</span><span class="cl">db.IO.aurora_storage_xx       # storage layer I/O 細節</span></span></code></pre></div><p><strong>驗證點</strong>：</p>
<ul>
<li>寫入 latency p99：PostgreSQL primary 1-3ms vs Aurora 3-6ms、跨 AZ network round-trip 是物理下界</li>
<li>Read latency p99：Aurora &lt; 1ms（從共享 storage 讀、不跨 AZ）</li>
<li>Storage autoscale event：128 TB 上限前自動 grow per 10GB</li>
</ul>
<p><strong>Rollback boundary</strong>：Aurora storage 是 cluster-level、無法回滾 storage 設計；唯一 rollback 是切回 RDS / 自管（走 migration playbook、不是配置層 rollback）。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1誤以為-aurora-寫入一定比-postgresql-primary-快">Case 1：誤以為 Aurora 寫入一定比 PostgreSQL primary 快</h3>
<p>徵兆：團隊期待 Aurora 寫入比自管 PostgreSQL 快、實測 p99 寫入 latency 沒明顯改善、甚至小 row + 單筆 commit 場景 Aurora 反而慢。</p>
<p>原因：跨 AZ network round-trip 是 3-5ms 物理下界、4-of-6 quorum 至少要等 4 個 storage node ack、單筆小寫場景 local SSD primary 仍有 latency 優勢。Aurora 的寫入優勢在 <em>壓力下</em> 才顯現 — write throughput 高峰時 PostgreSQL primary 受限於 dirty page flush + WAL fsync + replica catch-up、Aurora 的 storage layer 各自獨立處理 redo log apply。</p>
<blockquote>
<p><strong>數字口徑</strong>：「跨 AZ round-trip 3-5ms」屬通用工程估算（光速下界 + AWS 區內 AZ 物理距離）、case 未直接量化、實際值依 region / AZ pair / instance 類型而異、要看 AWS 官方 latency table 或自家 benchmark 校正。下方 DraftKings 6ms 寫入是 case 揭露的 production reference、可作為對照基線。</p></blockquote>
<p>修：</p>
<ul>
<li>benchmark 要跑壓力測試、不能只測單筆 latency</li>
<li>寫入 latency 不是 Aurora 的核心賣點、是 <em>可預測的 read replica lag + 韌性</em> 才是</li>
<li>DraftKings 6ms 寫入是 production reference：跨 AZ quorum 的物理下界、不是 Aurora 慢</li>
</ul>
<h3 id="case-2az-level-outage-期間寫入-latency-spike">Case 2：AZ-level outage 期間寫入 latency spike</h3>
<p>徵兆：1 個 AZ 失效後、寫入 p99 從 6ms spike 到 30-50ms、application timeout 增加。</p>
<p>原因：失去 1 AZ 後 quorum 仍成立（4-of-6 → 用剩 4 個 node 寫）、但 storage node fault 期間需要等 timeout 才確認；單一 storage node 額外 fault 會把寫推到 timeout。Aurora 在 AZ outage 期間 <em>能寫</em>、但不是 <em>性能不變</em>。</p>
<p>修：</p>
<ul>
<li>監測 <code>AuroraVolumeBytesLeftTotal</code> 跟 storage IOPS 分布、AZ outage 期間自動切到剩餘 AZ</li>
<li>application 端做 retry + circuit breaker、不要假設寫入永遠 6ms</li>
<li>確認 cluster 至少跨 3 AZ deploy、單 AZ outage 才有 quorum 餘地</li>
</ul>
<h3 id="case-3io-optimized-費用誤判">Case 3：I/O-Optimized 費用誤判</h3>
<p>徵兆：team 看 Aurora I/O-Optimized「無 I/O 收費」直接切過去、月帳變高 25%、沒看到 ROI。</p>
<p>原因：Standard storage 按 I/O 收費、I/O-Optimized 月費比 Standard 高 30%。只有 <em>write-heavy + scan-heavy</em> workload（I/O 月費接近 instance 費用）才划算；read-light + write-light workload 反而吃虧。</p>
<p>修：</p>
<ul>
<li>先量測 baseline I/O：<code>VolumeReadIOPs + VolumeWriteIOPs × $0.20 per million I/O</code> vs Standard 月費</li>
<li>I/O 費用 &gt; instance 費用 30% 才切 I/O-Optimized</li>
<li>DraftKings 用 I/O-Optimized 是因為金融帳本 write-heavy + balance query scan-heavy、ROI 明顯</li>
</ul>
<h3 id="case-4storage-autoscale-假設">Case 4：Storage autoscale 假設</h3>
<p>徵兆：TRUNCATE / DROP 大表釋放 50% storage、但下月帳單沒回落。</p>
<p>原因：Aurora storage 自動 grow、但 <em>不自動 shrink</em>。已分配的 storage 持續計費、TRUNCATE / DROP 只釋放 logical space、physical storage 仍占用。要 shrink 必須走 logical migration（dump / restore 到新 cluster）。</p>
<p>修：</p>
<ul>
<li>大量 DROP 操作前先評估是否值得做 logical migration</li>
<li>用 partition + DETACH 而非 DROP TABLE、partition 可以單獨 archive</li>
<li>接受 storage 用量是 <em>peak watermark</em> 而非 <em>current usage</em></li>
</ul>
<h3 id="case-5replication-lag-誤解">Case 5：Replication lag 誤解</h3>
<p>徵兆：read replica lag 10-30ms 看起來夠快、application 假設 read-after-write consistency、用戶下注後立刻查 balance 偶發看到舊資料。</p>
<p>原因：10-30ms 是 <em>typical</em>、heavy write + slow query 期間可能秒級。Aurora 共享 storage 設計讓 lag <em>可預測</em>（不會像 PostgreSQL streaming replication unbounded）、但 <em>可預測</em> 不等於 <em>zero</em>。Read-after-write 場景仍需要 application 端處理。</p>
<p>修：</p>
<ul>
<li>用戶寫操作後 N 秒內走 primary（N 由 lag p99 決定、典型 100ms）</li>
<li>Aurora 提供 session pinning：寫完同 session 短期內走 primary</li>
<li>不能假設「Aurora replication lag 小到可以忽略」、要看 application 容忍度</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</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">VolumeBytesUsed           # storage 用量、128 TB 上限預警
</span></span><span class="line"><span class="ln">2</span><span class="cl">AuroraReplicaLag          # replica lag、判斷讀寫分流可行性
</span></span><span class="line"><span class="ln">3</span><span class="cl">db.IO.aurora_redo_log_flush # quorum write 等待、storage 瓶頸訊號</span></span></code></pre></div><p><strong>Production reference number</strong>（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 揭露、case「觀察」段表格）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>DraftKings 在 Aurora MySQL 的數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀延遲</td>
          <td>&lt; 1 ms</td>
      </tr>
      <tr>
          <td>寫延遲</td>
          <td>6 ms</td>
      </tr>
      <tr>
          <td>Replication lag</td>
          <td>從 30 秒降到 10-30 ms</td>
      </tr>
  </tbody>
</table>
<p>這個 production reference 取代用「typical 3-5ms」籠統說法。讀寫 6x 差距是 OLTP 容量規劃槓桿 baseline — 寫延遲是 quorum 4-of-6 + 跨 AZ network round-trip 的物理下界、不是 storage 設計能再壓低。引用時要明示是 DraftKings production reference、不是 Aurora marketing。</p>
<p><strong>容量上限</strong>：</p>
<ul>
<li>128 TB / cluster（超過要拆 cluster、見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet 治理 SSoT）</li>
<li>15 read replica / region（<a href="../read-replica-scaling/">Aurora read replica scaling</a> 展開）</li>
<li>Storage 自動 grow per 10GB</li>
</ul>
<p><strong>跨 region replication</strong>：<a href="../global-database-multi-region/">Aurora Global Database</a> 用 <code>AuroraGlobalDBReplicationLag</code> 監測、&lt; 1 秒 typical。</p>
<p><strong>回路徑</strong>：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 抽 CloudWatch evidence、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 判斷 storage-bound vs compute-bound。</p>
<h2 id="netflix-75-效能改善的根因">Netflix +75% 效能改善的根因</h2>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix consolidation</a> 案例揭露 storage 設計的具體效能含義。Netflix 把多套 RDBMS（PostgreSQL / MySQL / Oracle）統一到 Aurora、拿到 <em>up to 75%</em> 效能改善、-28% 成本。</p>
<p><strong>+75% 的根因</strong>：</p>
<ul>
<li>傳統 PostgreSQL primary 寫 WAL + dirty page flush + 透過 streaming replication 推到 replica</li>
<li>Compute 大量 CPU 用在 dirty page flush + replication encoding、不是用在 query processing</li>
<li>Aurora compute 只送 redo log records、storage 自己 apply page、自己 checkpoint</li>
<li>→ 同樣 instance class 下、Aurora compute 能處理更多 query</li>
</ul>
<p>這不是 marketing 的「分散式儲存讓效能提升」籠統說法、而是具體的 <em>compute 不再 flush dirty page</em>。</p>
<p><strong>scope warning（必明示、case 自帶警示原話）</strong>：</p>
<p>「effective 75% improvement 是跨多 workload 的最大改善幅度、不是『每個 workload 都 +75%』。實際每個 workload 改善幅度從 10% 到 75% 不等」（case「需要警惕」段第 1 點）。</p>
<p>引用 Netflix 時不能把 75% 套到單一 workload — 容量規劃要看自家 workload 形狀（write-heavy / read-heavy / scan-heavy）、預估改善幅度範圍而非單一數字。</p>
<h2 id="fleet-治理cross-link不展開">Fleet 治理（cross-link、不展開）</h2>
<p>Production scale 不是「單一巨型 Aurora cluster」而是 <em>fleet of clusters</em> — 5 case 揭露同一 frame：</p>
<ul>
<li>DraftKings 200 個獨立 cluster（按業務切分）</li>
<li>Netflix 多 cluster（微服務私有 store）</li>
<li>Standard Chartered 7 個 cluster（受監管市場 boundary）</li>
</ul>
<p>跨 case 合成的 fleet 拓樸 3 條 driver（business sharding / microservice ownership / 合規市場 boundary）跟「何時拆 cluster vs 加 replica」的判讀順序、SSoT 在 <a href="../read-replica-scaling/">Aurora read replica scaling</a> 邊界段。Storage 設計本身不解 fleet 邊界決策 — Aurora 解 single-cluster scaling（quorum / 共享 storage / 共享 backup）、但「拆幾個 cluster」是業務拓樸決策。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> — storage 設計如何加速 failover（replica 不需要 catch-up）</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — 共享 storage 為什麼能養 15 replica + fleet 治理 SSoT</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 跨 region storage replication 設計</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — storage 設計差是 operational redesign 的核心 driver</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> — quorum 寫入 vs single-primary transaction 邊界</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> — Aurora storage 是 single-region scaling、不是 distributed SQL</li>
</ul>
<p><strong>何時不用本文</strong>：single-region OLTP 用 RDS 仍足夠、storage architecture 細節不影響容量規劃時可跳過、看 <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 overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">Quorum 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag 卡片</a> — 對照通用 replication lag 模型</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html">Aurora storage architecture</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB HLC + Raft Consensus：軟體時鐘 + per-range 共識的 latency 與容量結構</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/hlc-raft-consensus/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/hlc-raft-consensus/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 在 distributed SQL 譜系的定位、本文聚焦 &lt;em>HLC + Raft + range + leaseholder 四層機制&lt;/em> — 解釋為什麼 distributed SQL 的 latency / 容量曲線跟 PostgreSQL single-primary 完全不同、以及怎麼從 production 訊號倒推它對團隊的成本結構。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼這篇先講-hlc--raft">為什麼這篇先講 HLC + Raft&lt;/h2>
&lt;p>團隊評估 CockroachDB 替代 PostgreSQL streaming replication 時、會同時看到兩個訊號：「跨 region 強一致」很吸引人、「每次寫都經過 Raft majority」又讓人害怕。前者是賣點、後者是成本結構 — 不先把 HLC / Raft / range / leaseholder 拆清楚、後面講 survival goal、locality、transaction retry 都會卡在「為什麼這個機制存在」這層。&lt;/p>
&lt;p>讀者最常問的三題：&lt;/p>
&lt;ul>
&lt;li>Spanner 用 TrueTime 原子鐘做線性化、CockroachDB 沒硬體時鐘怎麼保證 ordering？&lt;/li>
&lt;li>Raft 每次寫要等 majority ack、不是比 PostgreSQL 慢得多？&lt;/li>
&lt;li>HLC clock skew 超出容忍區間時會發生什麼？節點隨機 panic 嗎？&lt;/li>
&lt;/ul>
&lt;p>三題都不只是 spec 問題、而是 &lt;em>production 容量規劃跟 incident 訊號的根本前置&lt;/em>。&lt;/p>
&lt;p>問題情境最常見的 trigger：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash&lt;/a> 在 2020-04-17 高峰 Aurora Postgres 撞到 1.636 M QPS、multi-hour outage。&lt;strong>這個數字是 Aurora 在那個時間點撞牆的痛點、case 自己警示「不是 CockroachDB 撐到 1.636 M QPS 的 throughput claim」&lt;/strong>。case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster、alert volume 反而下降」。要把 CockroachDB 當寫入容量解法評估、就得先理解 Raft per range 怎麼把寫入從 single-primary 分散到多 node。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> 則提供另一條訊號：380+ cluster / 60+ multi-region、最大單區 cluster 60 nodes / 26.5 TB。這個規模證明 Raft 維運在 production 可承擔、但也揭露容量規劃顆粒不是「全公司一條容量曲線」、是「每 cluster 各自規劃」— artery of small DBs。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 在 distributed SQL 譜系的定位、本文聚焦 <em>HLC + Raft + range + leaseholder 四層機制</em> — 解釋為什麼 distributed SQL 的 latency / 容量曲線跟 PostgreSQL single-primary 完全不同、以及怎麼從 production 訊號倒推它對團隊的成本結構。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<hr>
<h2 id="為什麼這篇先講-hlc--raft">為什麼這篇先講 HLC + Raft</h2>
<p>團隊評估 CockroachDB 替代 PostgreSQL streaming replication 時、會同時看到兩個訊號：「跨 region 強一致」很吸引人、「每次寫都經過 Raft majority」又讓人害怕。前者是賣點、後者是成本結構 — 不先把 HLC / Raft / range / leaseholder 拆清楚、後面講 survival goal、locality、transaction retry 都會卡在「為什麼這個機制存在」這層。</p>
<p>讀者最常問的三題：</p>
<ul>
<li>Spanner 用 TrueTime 原子鐘做線性化、CockroachDB 沒硬體時鐘怎麼保證 ordering？</li>
<li>Raft 每次寫要等 majority ack、不是比 PostgreSQL 慢得多？</li>
<li>HLC clock skew 超出容忍區間時會發生什麼？節點隨機 panic 嗎？</li>
</ul>
<p>三題都不只是 spec 問題、而是 <em>production 容量規劃跟 incident 訊號的根本前置</em>。</p>
<p>問題情境最常見的 trigger：<a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> 在 2020-04-17 高峰 Aurora Postgres 撞到 1.636 M QPS、multi-hour outage。<strong>這個數字是 Aurora 在那個時間點撞牆的痛點、case 自己警示「不是 CockroachDB 撐到 1.636 M QPS 的 throughput claim」</strong>。case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster、alert volume 反而下降」。要把 CockroachDB 當寫入容量解法評估、就得先理解 Raft per range 怎麼把寫入從 single-primary 分散到多 node。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> 則提供另一條訊號：380+ cluster / 60+ multi-region、最大單區 cluster 60 nodes / 26.5 TB。這個規模證明 Raft 維運在 production 可承擔、但也揭露容量規劃顆粒不是「全公司一條容量曲線」、是「每 cluster 各自規劃」— artery of small DBs。</p>
<h2 id="核心機制hlc--raft--range--leaseholder-四層">核心機制：HLC + Raft + range + leaseholder 四層</h2>
<p>CockroachDB 的線性化保證來自四層機制疊加、缺一層都解釋不通實際 latency / failure 行為。</p>
<h3 id="hlc軟體時鐘把-wall-clock--logical-counter-混在一起">HLC：軟體時鐘把 wall clock + logical counter 混在一起</h3>
<p><a href="/blog/backend/knowledge-cards/hybrid-logical-clock/" data-link-title="Hybrid Logical Clock" data-link-desc="用 physical wall clock &#43; monotonic logical counter 給每個事件 timestamp、靠軟體 max-offset 保證跨節點時鐘差不超過上限、超過 panic 保護一致性">Hybrid Logical Clock</a> 結合 <em>physical time</em>（NTP 同步的牆鐘）跟 <em>logical counter</em>（單調遞增的事件序號）、給每個事件一個 <code>(physical, logical)</code> timestamp。對比 Spanner TrueTime 直接靠 GPS + atomic clock 給「時鐘 uncertainty bound」、CockroachDB HLC 不依賴硬體、用軟體保證「節點之間時鐘最多差 <code>max-offset</code>（default 500ms）、超過就 panic」。</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">Node A 收到 write at wall=12:00:00.123, last_seen=12:00:00.100
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → HLC = (12:00:00.123, 0)
</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">Node A 收到 RPC from B at wall=12:00:00.140, B.HLC=(12:00:00.200, 5)
</span></span><span class="line"><span class="ln">5</span><span class="cl">  → A 跳到 B 的 physical (12:00:00.200)、logical = 6
</span></span><span class="line"><span class="ln">6</span><span class="cl">  → HLC = (12:00:00.200, 6)</span></span></code></pre></div><p>HLC 的契約 <em>只要節點間時鐘差不超過 max-offset、所有 transaction 仍是 linearizable</em>。production 必跑 NTP / chronyd — 一旦本機時鐘飄超過 500ms、節點自動 panic 保護 cluster 一致性、不會發出錯誤 commit。</p>
<p>跟 Spanner TrueTime 對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>CockroachDB HLC</th>
          <th>Spanner TrueTime</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬體依賴</td>
          <td>無（純軟體 + NTP）</td>
          <td>GPS + atomic clock（每資料中心配）</td>
      </tr>
      <tr>
          <td>Uncertainty</td>
          <td>由 max-offset 上界、固定 500ms</td>
          <td>動態 uncertainty interval（通常 &lt; 7ms）</td>
      </tr>
      <tr>
          <td>Commit 等待</td>
          <td>不需要 wait out uncertainty</td>
          <td>需要 wait out（commit-wait）</td>
      </tr>
      <tr>
          <td>部署彈性</td>
          <td>任何雲 / on-prem 都可跑</td>
          <td>只在有 TrueTime infra 的 GCP region</td>
      </tr>
  </tbody>
</table>
<p>兩條路徑解同一個 <em>event ordering</em> 問題、用不同 trade-off。CockroachDB 把硬體成本換成軟體 max-offset 容忍度、結果是「可以跨雲跨 on-prem 跑、但 NTP 維運是必要條件」。</p>
<h3 id="raft每個-range-一個獨立的-majority-consensus-group">Raft：每個 range 一個獨立的 majority consensus group</h3>
<p>Raft 把寫入流程切成 <em>propose → replicate to majority → commit</em> 三段。每個 range 維護自己的 Raft group、預設 3 replica、寫入要至少 2 個 replica ack 才能 commit。</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">Client → Leaseholder (Raft leader)
</span></span><span class="line"><span class="ln">2</span><span class="cl">   1. Propose log entry (write intent)
</span></span><span class="line"><span class="ln">3</span><span class="cl">   2. Replicate to 2 follower replicas
</span></span><span class="line"><span class="ln">4</span><span class="cl">   3. Wait for majority ack (本身 + 1 個 follower)
</span></span><span class="line"><span class="ln">5</span><span class="cl">   4. Commit、apply to state machine
</span></span><span class="line"><span class="ln">6</span><span class="cl">   5. Reply to client</span></span></code></pre></div><p>關鍵差異跟 PostgreSQL streaming replication 比：</p>
<ul>
<li>PostgreSQL primary：1 個節點 ack 就 commit（async replication）、replica 可能落後</li>
<li>PostgreSQL sync replication：1 個 standby ack 才 commit、但仍是「primary 是 single point of write」</li>
<li>CockroachDB Raft：majority（2 of 3）ack 才 commit、任何 replica 都可以是 leaseholder、寫入分散到所有節點</li>
</ul>
<p>寫入 latency 因此 <em>結構性</em> 高於 PostgreSQL — 多了一次 cross-node round trip。但寫入 <em>吞吐</em> 可以線性擴展、因為不同 range 的 Raft group 跑在不同節點上。</p>
<h3 id="range把-key-space-切成-512-mb-的可分裂單位">Range：把 key space 切成 ~512 MB 的可分裂單位</h3>
<p>CockroachDB 用 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> 把整個 key space 切成 range、每個 range 預設上限 ~512 MB、超過自動 split。每個 range 是一個獨立的 Raft group、有自己的 3 replica 分佈。</p>
<p>對比其他 distributed DB 的等價概念：</p>
<ul>
<li>DynamoDB partition：固定 hash 分區、自動 split 但 hot partition 容易撞 ceiling</li>
<li>Spanner split：類似 range、但配置 / placement 語法不同</li>
<li>Vitess keyspace：application 端決定 shard key、不透明 split</li>
</ul>
<p>CockroachDB range 是 <em>系統內建透明</em> 的 — application 只看到 SQL table、不需要 shard key 設計。但 hot range 仍會發生（後面 failure mode 段展開）。</p>
<h3 id="leaseholder每個-range-的-read--write-entry-point">Leaseholder：每個 range 的 read / write entry point</h3>
<p>每個 range 在任一時間點有一個 <a href="/blog/backend/knowledge-cards/leaseholder/" data-link-title="Leaseholder" data-link-desc="分散式 SQL 每個 range 在任一時間點的 read / write entry point、通常等於 Raft leader、承擔該 range 的 coordination">Leaseholder</a>（通常等於 Raft leader）、承擔該 range 的所有 read / write coordination。leaseholder 也是 <em>follower read</em> 的 timestamp 邊界 holder。</p>
<p>leaseholder 概念對 production 訊號的影響：</p>
<ul>
<li>寫入 latency 主要來自 leaseholder → follower replicas 的 Raft round trip</li>
<li>leaseholder 集中在某節點 → 該節點 CPU 飽和（hot range 的根因之一）</li>
<li>leaseholder 換手（lease transfer）短期 p99 spike — rebalance 期間 / 節點 graceful drain 都會觸發</li>
</ul>
<h2 id="操作流程配置--驗證--rollback-邊界">操作流程：配置 + 驗證 + rollback 邊界</h2>
<h3 id="cluster-起手配置">Cluster 起手配置</h3>
<p>最小可運行配置是 3 節點（Raft quorum 下界）、production 通常 9 節點以上（3 region × 3 replica）。每個節點啟動時必須帶 locality tag、讓 Raft placement 知道副本怎麼分佈：</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">cockroach start --insecure <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --locality<span class="o">=</span><span class="nv">region</span><span class="o">=</span>us-east1,zone<span class="o">=</span>us-east1-a <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --max-offset<span class="o">=</span>500ms <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --join<span class="o">=</span>node1:26257,node2:26257,node3:26257</span></span></code></pre></div><p><code>--max-offset</code> 是 HLC 容忍上界、超過會 panic — 不要為了「避免 panic」加大這個值、會犧牲 linearizability 保證。</p>
<p>NTP / chronyd 是 <em>必要前置</em>、不是 nice-to-have。production 應該在每個節點配置：</p>
<ul>
<li>NTP server 至少 3 個獨立 source（避免單一 server drift）</li>
<li>監控 <code>chronyc tracking</code> 的 offset、超過 100ms 就應該 alert（遠在 500ms panic 邊界之前）</li>
</ul>
<h3 id="驗證點">驗證點</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 看每節點當前 clock offset 跟 cluster 其他節點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">node_id</span><span class="p">,</span><span class="w"> </span><span class="n">address</span><span class="p">,</span><span class="w"> </span><span class="n">offset_min_nanos</span><span class="p">,</span><span class="w"> </span><span class="n">offset_max_nanos</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">gossip_nodes</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 看 Raft 健康（每個 range 的 leaseholder 跟 replica 分佈）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">range_id</span><span class="p">,</span><span class="w"> </span><span class="n">lease_holder</span><span class="p">,</span><span class="w"> </span><span class="n">replicas</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">ranges</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">table_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;orders&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 看 cluster max-offset 設定
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">CLUSTER</span><span class="w"> </span><span class="n">SETTING</span><span class="w"> </span><span class="n">server</span><span class="p">.</span><span class="n">clock</span><span class="p">.</span><span class="n">persist_upper_bound_interval</span><span class="p">;</span></span></span></code></pre></div><h3 id="rollback-邊界">Rollback 邊界</h3>
<p>HLC + Raft 對 rollback 的態度跟 PostgreSQL 不同：</p>
<ul>
<li>HLC 時鐘前進不可回滾 — 不能「改一下 max-offset 後重啟試試看」</li>
<li>Raft commit 不可回滾 — 一旦 majority ack、log entry 持久化</li>
<li>想還原業務狀態 <em>只能新交易補償</em>、不能 reverse Raft log</li>
</ul>
<p>實務上的影響：incident 時不要嘗試「強制回到舊版本」、應該走 transaction-level rollback / compensation。對應 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a> 跟業務層補償設計。</p>
<h2 id="失敗模式clock-skew--majority-lost--hot-range--retry-storm">失敗模式：clock skew / majority lost / hot range / retry storm</h2>
<h3 id="clock-skew-panic">Clock skew panic</h3>
<p>最常見：NTP 服務掛、節點時鐘漂移超過 max-offset、節點自動 panic。production incident 訊號：</p>
<ul>
<li><code>chronyc tracking</code> 顯示 offset 持續成長</li>
<li>CockroachDB log 出現 <code>clock synchronization error</code></li>
<li>Prometheus metric <code>clock_offset_meannanos</code> 接近 max-offset</li>
</ul>
<p>修法：先恢復 NTP service、節點重啟前再次驗證時鐘已同步、不要動 <code>--max-offset</code>。對比 PostgreSQL primary 不關心 time skew、distributed SQL 把時鐘變成 first-class operational concern。</p>
<h3 id="raft-majority-lost">Raft majority lost</h3>
<p>3 節點 cluster 失去 2 個、剩 1 個無法 commit、cluster 全 read-only（甚至連 read 都可能受影響、因為 leaseholder 拿不到 valid lease）。對比 PostgreSQL primary 失效後 streaming replica 仍可 read、CockroachDB 的 fault tolerance 是 <em>quorum-based</em>、不是 <em>primary-replica</em>。</p>
<p>production 規劃要點：跨 AZ / region 分佈時、必須保證任何 <em>單一 failure domain</em> 失敗後仍有 majority 存活。3 節點配 1 AZ → AZ 失敗 = cluster down。最小 production 配置是 3 AZ × 1 node 或 3 region × 3 node。</p>
<h3 id="hot-rangeleaseholder-節點-cpu-飽和">Hot range：leaseholder 節點 CPU 飽和</h3>
<p>某個 range 寫流量集中（例：訂單 table primary key 是時間序 / 自增 ID）、leaseholder 節點變成熱點。徵兆：</p>
<ul>
<li>CockroachDB Console「Leaseholder count per node」分佈不均</li>
<li>某節點 CPU 飽和、其他節點閒置</li>
<li><code>crdb_internal.ranges</code> 顯示該 range 的 QPS 遠高於其他 range</li>
</ul>
<p>修法：</p>
<ul>
<li>手動 <code>ALTER TABLE ... SPLIT AT VALUES (...)</code> 強制 split hot range</li>
<li>改 primary key 設計、避免時間序 / 自增 ID（用 UUID / hash-prefixed key）</li>
<li>partition by region、把 hot range 切到不同 region 的 leaseholder</li>
</ul>
<h3 id="transaction-retry-storm">Transaction retry storm</h3>
<p>serializable contention 嚴重時 application 端 retry loop、CPU 雪崩。這個議題的 application contract 重塑屬獨立議題、見 <a href="../transaction-retry-pattern/">transaction retry pattern</a>。</p>
<h3 id="range-split--rebalance-期間-p99-spike">Range split / rebalance 期間 p99 spike</h3>
<p>自動 split 大 range、leaseholder 換手期間有 ~100ms 的 lease transfer 視窗、p99 短期 spike。production 訊號：CockroachDB Console「Rebalance queue size」非零 + p99 latency 同期波動。一般是良性 — rebalance 完就回穩。但連續波動代表 range 在「split → 寫熱 → 再 split」循環、要從 schema 層解。</p>
<h2 id="容量與觀測per-cluster-顆粒--來源分層">容量與觀測：per-cluster 顆粒 + 來源分層</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Raft log queue size</code>：Raft replication 延遲訊號、持續高代表 follower 跟不上</li>
<li><code>Range count per node</code>：range 分佈是否均勻、不均代表 placement 有偏</li>
<li><code>Leaseholder count per node</code>：leaseholder 分佈是否均勻、不均直接導致 CPU 熱點</li>
<li><code>HLC offset distribution</code>：時鐘同步健康</li>
<li><code>Transaction retry rate</code>：contention 訊號（細節在 <a href="../transaction-retry-pattern/">transaction retry pattern</a>）</li>
</ul>
<h3 id="per-cluster-容量規劃顆粒9c40-netflix-揭露f47">Per-cluster 容量規劃顆粒（9.C40 Netflix 揭露、F4.7）</h3>
<p>Netflix 的 380+ cluster 模型揭露一個反直覺結論：production scale 不是「全公司一條容量曲線」、而是 <em>artery of small DBs</em>。每個 cluster 對應一個 application boundary、cluster sizing 從幾個 node 到 60 nodes 不等、最大單區 60 nodes / 26.5 TB（case 觀察段表格揭露）。</p>
<p>容量規劃顆粒對齊 application boundary 的好處：</p>
<ul>
<li>每個 cluster 各自規劃 capacity、不必預測「全公司加總 QPS」</li>
<li>blast radius 限縮在單一 app — 某 cluster 撞 hot range / Raft majority lost、其他 cluster 不受影響</li>
<li>upgrade / backup 可分批跑、不必整廠 maintenance window</li>
</ul>
<p>但也帶來 ops 成本：380+ cluster 需要 <em>專屬 Database Platform Team</em>（含 backup、upgrade、incident response、capacity review）— Netflix case 直接揭露這個前置條件。沒這量級團隊就走 Cockroach Cloud managed、不要 self-host。</p>
<p>per-app cluster vs shared cluster 的決策軸主寫於 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>、本篇 cross-link 不展開。</p>
<h3 id="寫入-latency-預算屬通用工程估算case-未揭露具體數字">寫入 latency 預算（屬通用工程估算、case 未揭露具體數字）</h3>
<p>以下數字屬通用工程估算 / 物理光速下界推導、<strong>DoorDash / Netflix / Hard Rock 三個 direct case 都沒揭露單一 cluster p99 latency</strong>。引用時必須明示來源層次：</p>
<ul>
<li>single-region 3-replica write p99 3-5ms（通用估算、跨 AZ Raft round trip）</li>
<li>multi-region 跨洲 write p99 100-150ms（光速下界 — 跨洲 round trip 物理 ~70-80ms × 2）</li>
<li>單一 range 寫 throughput ~1000 QPS（通用估算、實際依 row size / contention 而定）</li>
<li>整 cluster scale-out 加 range、寫入吞吐近線性擴展（理論、實際依 hot range 分佈）</li>
</ul>
<p>這些是「合理的工程估算量級」、不是 case 揭露的 p99 數字。讀者用這些做容量規劃時、應該 <em>自己 benchmark</em> 而不是直接套。</p>
<h3 id="doordash-1636-m-qps-引用紀律f41case-自帶警示">DoorDash 1.636 M QPS 引用紀律（F4.1、case 自帶警示）</h3>
<p>DoorDash case 揭露的 1.636 M QPS 是 <em>Aurora Postgres single-primary 在 2020-04-17 高峰撞牆的痛點</em>（multi-hour outage）、<strong>不是 CockroachDB throughput claim</strong>。case 明確警告不要把這個數字當「CockroachDB 撐 1.636 M QPS 的證據」。case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster、alert volume 反而下降」。</p>
<p>引用這個數字時的口徑：</p>
<ul>
<li>寫成「Aurora 撞牆訊號」、不寫成「CockroachDB 容量證明」</li>
<li>single-primary 撞牆的轉折點是 <em>primary CPU + WAL flush rate</em>（DoorDash 策略段 1）、不是 IOPS</li>
<li>「換引擎」前先評估「兩階段紓壓」— DoorDash 路徑是先把 hot table 拆到獨立 Aurora cluster（紓壓）、再規劃 Aurora → CockroachDB 換引擎（<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a>）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 Raft-bound vs storage-bound</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> replication factor × latency budget</li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">latency budget 卡</a> cross-region quorum 預算</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../survival-goals/">CockroachDB survival goals</a>：Raft replica 怎麼分佈到 zone / region、決定 RTO / RPO</li>
<li><a href="../transaction-retry-pattern/">CockroachDB transaction retry pattern</a>：serializable default 對 application 契約的重塑</li>
<li><a href="../locality-aware-schema/">CockroachDB locality-aware schema</a>：range placement 控制 + locality 配置</li>
</ul>
<h3 id="跟-aurora-對照">跟 Aurora 對照</h3>
<p>Aurora 是 <em>storage-level quorum</em>（4 of 6 storage replica）、compute 仍是 single primary。CockroachDB 是 <em>range-level Raft</em>（每個 range 獨立 majority）、compute 跟 storage 在每節點。兩者解的是不同 layer 的 consensus、結果是 Aurora 寫入仍受 primary 限制、CockroachDB 寫入隨節點線性擴。</p>
<h3 id="aurora-dsql--spanner-對比">Aurora DSQL / Spanner 對比</h3>
<p>完整三家 distributed SQL 對比、撞牆訊號分型、PostgreSQL 相容性 audit、團隊規模 vs vendor sizing barrier 等議題在 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游選型</li>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> distributed transaction 邊界</li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region OLTP + 寫入未撞 PostgreSQL primary 天花板 → PostgreSQL 已足夠</li>
<li>對 cross-region quorum 100-150ms latency 預算無法接受 → 走 async replication 路線</li>
<li>沒 NTP 維運能力 → distributed SQL 把時鐘變 ops concern、沒準備好不要硬上</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>（Aurora 1.636 M QPS 撞牆訊號）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（380+ cluster artery of small DBs）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner planetary scale</a>（TrueTime 對照）</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/architecture/overview.html">CockroachDB Architecture</a> / <a href="https://cse.buffalo.edu/tech-reports/2014-04.pdf">Hybrid Logical Clocks paper</a> / <a href="https://raft.github.io/raft.pdf">Raft paper</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/</guid><description>&lt;p>Cosmos DB 提供 &lt;em>5 個 API&lt;/em>（SQL / MongoDB / Cassandra / Gremlin / Table）、底層是同一個分散式 document store。團隊從 MongoDB 來、第一個問題通常是「MongoDB API 跟 native SQL API 我選哪個」 — 但這個問題框架太窄。讀者真正在比的是 &lt;em>vendor selection&lt;/em>、不是兩個 API 的 syntax 差。本文把選型推到四層問題：(a) 你的遷移路徑屬於哪一型、(b) dogfood signal 怎麼讀、(c) multi-model 差異化是否真用上、(d) 跨雲 hedging 還是單雲 lock-in。先把四層 framing 講清楚、再進兩個 API 的機制差異、最後給 MongoDB → Cosmos DB MongoDB API 的 migration playbook。&lt;/p>
&lt;p>本文不是 Cosmos DB overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a>）— 而是 &lt;em>選型決策 + 遷移實作&lt;/em> 的深度展開。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> — Microsoft 自家 dogfood、MongoDB → Cosmos DB MongoDB API 的 planet-scale 分析平台、提供四層 framing 的證據錨點。&lt;/p>
&lt;h2 id="問題情境選型問題不是兩個-api-哪個快">問題情境：選型問題不是「兩個 API 哪個快」&lt;/h2>
&lt;p>典型觸發場景：團隊已用 MongoDB（自管 或 Atlas）、評估遷到 Azure；Cosmos DB 提供 MongoDB API（wire protocol 相容）跟 native SQL API 兩條路；文件講「MongoDB API 是 wire compat、SQL API 是 native」、但這個敘述沒回答真實決策問題。&lt;/p>
&lt;p>讀者實際在問：&lt;/p>
&lt;ul>
&lt;li>「MongoDB API 我們的 aggregation pipeline 跑得起來嗎」&lt;/li>
&lt;li>「&lt;code>$lookup&lt;/code> 在 Cosmos DB MongoDB API 支援嗎」&lt;/li>
&lt;li>「change stream 跟 Change Feed 是同一回事嗎」&lt;/li>
&lt;li>「為什麼有人說 MongoDB API 只是過渡、最終要遷 SQL API」&lt;/li>
&lt;li>「Microsoft 自己選了 MongoDB API、是不是代表 MongoDB API 才是對的選擇」&lt;/li>
&lt;/ul>
&lt;p>這些問題背後的 &lt;em>真實壓力&lt;/em> 是 vendor selection：團隊已選 Azure、要決定「留 Atlas 還是進 Cosmos DB、進了 Cosmos DB 用哪個 API」、選錯的成本是 &lt;em>年級的工程遷移&lt;/em> — 不是 &lt;em>config 改不改&lt;/em> 等級。Microsoft 365 案例（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30&lt;/a>）從 MongoDB 遷到 Cosmos DB MongoDB API 是 dogfood、但 case 自承「沒有提具體 throughput、latency、cost 數字」— 引用時不能拿這個案例的「成功」當 benchmark、只能取它的 framing。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 提供 <em>5 個 API</em>（SQL / MongoDB / Cassandra / Gremlin / Table）、底層是同一個分散式 document store。團隊從 MongoDB 來、第一個問題通常是「MongoDB API 跟 native SQL API 我選哪個」 — 但這個問題框架太窄。讀者真正在比的是 <em>vendor selection</em>、不是兩個 API 的 syntax 差。本文把選型推到四層問題：(a) 你的遷移路徑屬於哪一型、(b) dogfood signal 怎麼讀、(c) multi-model 差異化是否真用上、(d) 跨雲 hedging 還是單雲 lock-in。先把四層 framing 講清楚、再進兩個 API 的機制差異、最後給 MongoDB → Cosmos DB MongoDB API 的 migration playbook。</p>
<p>本文不是 Cosmos DB overview（請看 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a>）— 而是 <em>選型決策 + 遷移實作</em> 的深度展開。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — Microsoft 自家 dogfood、MongoDB → Cosmos DB MongoDB API 的 planet-scale 分析平台、提供四層 framing 的證據錨點。</p>
<h2 id="問題情境選型問題不是兩個-api-哪個快">問題情境：選型問題不是「兩個 API 哪個快」</h2>
<p>典型觸發場景：團隊已用 MongoDB（自管 或 Atlas）、評估遷到 Azure；Cosmos DB 提供 MongoDB API（wire protocol 相容）跟 native SQL API 兩條路；文件講「MongoDB API 是 wire compat、SQL API 是 native」、但這個敘述沒回答真實決策問題。</p>
<p>讀者實際在問：</p>
<ul>
<li>「MongoDB API 我們的 aggregation pipeline 跑得起來嗎」</li>
<li>「<code>$lookup</code> 在 Cosmos DB MongoDB API 支援嗎」</li>
<li>「change stream 跟 Change Feed 是同一回事嗎」</li>
<li>「為什麼有人說 MongoDB API 只是過渡、最終要遷 SQL API」</li>
<li>「Microsoft 自己選了 MongoDB API、是不是代表 MongoDB API 才是對的選擇」</li>
</ul>
<p>這些問題背後的 <em>真實壓力</em> 是 vendor selection：團隊已選 Azure、要決定「留 Atlas 還是進 Cosmos DB、進了 Cosmos DB 用哪個 API」、選錯的成本是 <em>年級的工程遷移</em> — 不是 <em>config 改不改</em> 等級。Microsoft 365 案例（<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30</a>）從 MongoDB 遷到 Cosmos DB MongoDB API 是 dogfood、但 case 自承「沒有提具體 throughput、latency、cost 數字」— 引用時不能拿這個案例的「成功」當 benchmark、只能取它的 framing。</p>
<h2 id="四層-framingvendor-selection-的真實決策軸">四層 framing：vendor selection 的真實決策軸</h2>
<h3 id="framing-1document-model-三型遷移路徑對照本章合成-frame">Framing 1：document model 三型遷移路徑對照（本章合成 frame）</h3>
<p>「MongoDB → Cosmos DB」是 <em>一種</em> 遷移、不是 <em>全部</em> 遷移。document model 的遷移路徑在 case 庫至少呈現三型、風險跟 ROI 完全不同：</p>
<table>
  <thead>
      <tr>
          <th>遷移型</th>
          <th>案例</th>
          <th>工程複雜度</th>
          <th>ROI</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保留 + 補周邊</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（mongobetween + freshness token + ML predictive scaling）</td>
          <td>低、漸進、保留 MongoDB 自管</td>
          <td>中、解 connection storm 等瓶頸</td>
      </tr>
      <tr>
          <td>同 DB 換託管</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas、6 個月）</td>
          <td>中、schema 跟 access pattern 保留</td>
          <td>高、釋放 ops 人力</td>
      </tr>
      <tr>
          <td>同 model 換 vendor</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（MongoDB → Cosmos DB MongoDB API）</td>
          <td>高、底層架構換、driver 保留</td>
          <td>高、planet-scale 擴展性</td>
      </tr>
  </tbody>
</table>
<p><strong>三型 frame 是本章合成、case 原文沒有此分類</strong>。引用時要明示：Forbes 6 個月遷移成功 <em>不代表</em> Microsoft 365 也是 6 個月、底層架構換的工程複雜度遠高於託管換。讀者開頭要先問「我屬於哪一型」、再進兩個 API 比較 — 「保留 + 補周邊」根本不需要進 Cosmos DB selection、「同 DB 換託管」的主要 trade-off 是 Atlas vs Cosmos DB 跨雲問題（Framing 4）、「同 model 換 vendor」才是本文聚焦的決策。</p>
<p>把三型混淆的後果是：拿 Forbes 6 個月時程當 baseline 估 Microsoft 365 型遷移、實際工程複雜度高 3-5 倍、project plan 從第一天就 over-commit。</p>
<h3 id="framing-2dogfood-是高權重-selection-signal但案例數字常不公開">Framing 2：dogfood 是高權重 selection signal、但案例數字常不公開</h3>
<p>Microsoft 365 案例揭露的核心 signal 是「Microsoft 自家旗艦產品 dogfood Cosmos DB」— 跟 Amazon Prime Day 用 DynamoDB、Google 自家用 Spanner 一樣、雲商旗艦 DB 都用在自家旗艦產品上、這個 signal 在 vendor selection 的權重高、因為「雲商自己賭身家」。讀者該把這當 <em>選型訊號</em>、不是當 <em>production benchmark</em>。</p>
<p>但 9.C30 case 自承的警示必須明示：</p>
<ul>
<li>「沒有提具體 throughput、latency、cost 數字。Microsoft 內部數字通常不公開、跟 AWS / GCP 案例的數字密度差很多」</li>
<li>「『MongoDB 不夠用』是行銷話術。實際是 <em>MongoDB 在某些 workload pattern 下不夠用</em>、不是普遍結論」</li>
</ul>
<p>兩條警示直接影響寫作紀律：</p>
<ul>
<li>不能拿「Microsoft 365 遷成功」當「我們也會成功」的證據 — 規模 / workload pattern / 團隊能力都不同</li>
<li>不能拿「Microsoft 從 MongoDB 遷出」當「MongoDB 不行」的結論 — Microsoft 自己也有大量 MongoDB / Cosmos DB / SQL Server 並用、不是全部遷出</li>
</ul>
<p>dogfood signal 的 <em>正確用法</em> 是當 frame 借鑑（multi-model 差異化、planet-scale 抽象單位、API compatibility 層）、不是當數字 benchmark。</p>
<h3 id="framing-3multi-model-是-cosmos-db-的差異化價值不總是真用上">Framing 3：multi-model 是 Cosmos DB 的差異化價值、不總是真用上</h3>
<p>Cosmos DB 的差異化價值不是「比 Atlas 更會跑 MongoDB」、是 <em>單一服務支援 5 個 API</em>（SQL / MongoDB / Cassandra / Gremlin / Table）。跨雲對照揭露這個差異化的稀有度：</p>
<ul>
<li>AWS：DynamoDB（KV）+ DocumentDB（MongoDB-compatible）+ Neptune（graph）+ Keyspaces（Cassandra）— 各 use case 一個產品</li>
<li>GCP：Firestore（document）+ Bigtable（KV）+ Spanner（SQL）— 各 use case 一個產品</li>
<li>Azure Cosmos DB：5 個 API 在 <em>同一個服務</em> 內、partition + RU + region 治理共用</li>
</ul>
<p>對 selection 的意義：若團隊預期同一系統會用 document + KV + graph 混合、Cosmos DB 的 multi-model 是 <em>運維單一服務</em> 的 unique value、不是只看「MongoDB 替代品」就能 ROI 評估。但 anti-pattern 也明確：<em>若團隊只用 MongoDB API、不會用其他 4 個 API</em>、multi-model 差異化價值對該團隊 <em>不成立</em>、不該變成 selection 理由。</p>
<p>判讀時要把 multi-model 當「條件性價值」、不是「普遍優勢」 — 條件是「現在或可預見未來會用到第二個 API」。9.C30 Microsoft 365 case 策略段直接揭露「Multi-model 是 Cosmos DB 的差異化價值」、但這個價值對「只用 MongoDB API」的團隊不成立、不能套到所有讀者。</p>
<h3 id="framing-4跨雲-hedging-vs-單雲-lock-in-的-trade-off">Framing 4：跨雲 hedging vs 單雲 lock-in 的 trade-off</h3>
<p>選 Cosmos DB（單雲、Azure-only）跟選 MongoDB Atlas（跨雲、AWS / GCP / Azure 都能跑）的核心 trade-off 不是「哪個技術更強」、是 <em>未來不確定性的對沖價值</em> — 對應 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 的退出成本評估：</p>
<ul>
<li>Atlas：跨雲部署能力、未來換雲商不用換 DB、9.C37 Forbes 用 GCP 但保留跨雲彈性</li>
<li>Cosmos DB / DynamoDB / Spanner：三大雲商各自的單雲 DB、選一個就綁該雲商生態</li>
</ul>
<p>對 <em>未來雲商策略尚未底定</em> 的團隊、Atlas 的 hedging 價值 <em>高</em>、即使當下單雲就夠用 — 因為 5 年後換雲商的工程成本可能遠高於每月多付的 hosting 費用。對 <em>已綁 Azure 生態</em> 的團隊（Microsoft 365 dogfood、企業 AAD / Office / Power Platform 整合）、Cosmos DB 的 Azure-only 是 <em>整合延伸</em>、不是 <em>lock-in 損失</em> — 雲商已綁、再加一個 lock-in 不增邊際成本。</p>
<p>引用時必須明示這是 <em>未來不確定性 vs 當下整合</em> 的 hedging trade-off、不是「跨雲一定比較好」。讀者該問自己：「我們未來 5 年雲商策略是已定還是未定」、答案會直接決定 Atlas vs Cosmos DB 的選擇方向。</p>
<h2 id="兩個-api-的機制差異">兩個 API 的機制差異</h2>
<p>四層 framing 講完、再進 API 機制 — 不是為了「哪個快」、是為了讓 selection 後的實作不踩坑。</p>
<p>兩個 API 的關係：底層是同一個 Cosmos DB 分散式 document store、API layer 翻譯不同 wire protocol。MongoDB API 把 MongoDB 操作翻譯成 Cosmos DB internal、實際跑 Cosmos DB 自身 engine、不執行 MongoDB engine；SQL API 直接操作 Cosmos DB native query language。</p>
<p><strong>MongoDB API</strong>：</p>
<ul>
<li>相容 MongoDB wire protocol（時間敏感 claim、查 <a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60">最新支援版本</a>、目前對齊 6.0 / 7.0 但仍落後 native MongoDB）</li>
<li>Driver 不變：直接用 mongo-go-driver / pymongo / mongoose</li>
<li>翻譯層有 overhead、相同 query 的 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a> 通常比 SQL API 多 10-20%（屬通用工程估算、Microsoft 公開文件未列具體比例、case 也未直接量化、實際 overhead 依 query shape / driver 版本 / region 而異、應該以自家 workload benchmark 校正）</li>
</ul>
<p><strong>SQL API</strong>：</p>
<ul>
<li>Cosmos DB native query language（SQL-like、不是標準 SQL、不支援 JOIN）</li>
<li>直接操作 JSON document、ARRAY / nested field native 支援</li>
<li>完整 Cosmos DB feature 支援（Change Feed、stored procedure、trigger）</li>
</ul>
<p><strong>關鍵差異點</strong>：</p>
<ul>
<li><code>$lookup</code>（join）：MongoDB API 支援度有限、跨 partition 性能差；SQL API 沒 JOIN（document model 哲學）</li>
<li>Aggregation pipeline：部分 stage 不支援或行為不同（時間敏感、查 <a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60#aggregation-pipeline">支援列表</a>）</li>
<li>Index：MongoDB API hint / explain 行為跟 native MongoDB 不同</li>
<li>Change stream：MongoDB API 提供 change stream wire compat、但底層是 Cosmos DB Change Feed（語義 / ordering / retention 有差）</li>
<li>Transaction：兩邊都限同 partition、跨 partition transaction 都要改 workflow</li>
</ul>
<p>API kind 是 <em>account 層設定</em>、<em>建 account 時選擇、無法事後切換</em>。MongoDB API → SQL API 的「升級」是 export → recreate account → import + 重寫 application 的全量遷移、不是 in-place 切換。</p>
<h2 id="migration-playbookmongodb--cosmos-db-mongodb-api">Migration playbook：MongoDB → Cosmos DB MongoDB API</h2>
<p>「同 model 換 vendor」型遷移（Framing 1 第三型）的 6 規格面 audit：</p>
<h3 id="規格面-1driver">規格面 1：Driver</h3>
<ul>
<li>主要 driver：Azure 生態整合、需要更好的 global distribution、Atlas 跨雲成本不必要（單雲團隊）</li>
<li>對應 Framing 4 的「已綁 Azure 生態」條件</li>
</ul>
<h3 id="規格面-2no-go-condition">規格面 2：No-go condition</h3>
<ul>
<li>跨雲需求（Framing 4、Atlas 仍是首選、Forbes 案例證據）</li>
<li>需要 native MongoDB latest feature（MongoDB API server version 落後 native MongoDB）</li>
<li>未來雲商策略未定（hedging 價值喪失）</li>
<li>純 MongoDB 投資、無 Azure 生態其他服務整合（Framing 3 multi-model 不成立）</li>
</ul>
<h3 id="規格面-3diff-audit6-維度">規格面 3：Diff audit（6 維度）</h3>
<ul>
<li><strong>Schema</strong>：document shape 不變（wire compat）；但 <code>_id</code> 行為跟 Cosmos DB partition key 綁定方式要審</li>
<li><strong>Operational</strong>：自管 MongoDB → managed Cosmos DB、replica set / sharding 變成 partition + region、備份 / monitoring 全換</li>
<li><strong>Paradigm</strong>：不變（仍 document model）</li>
<li><strong>Components</strong>：MongoDB driver 保留、aggregation pipeline 部分需重寫</li>
<li><strong>Application change</strong>：connection string、authentication mechanism（SCRAM → Azure key / AAD）、read preference 對應 consistency level</li>
<li><strong>Topology</strong>：replica set → multi-region replication、shard key → partition key</li>
</ul>
<p>遷移類型判定：<strong>Type B drop-in（partial）</strong>、wire compat 但有相容性 gap、必須 dual-write per query pattern 驗證、不是一次切換。</p>
<h3 id="規格面-4phase-plan">規格面 4：Phase plan</h3>
<ul>
<li>Phase 0：相容性 audit、列 unsupported aggregation stage、production query corpus 對齊</li>
<li>Phase 1：partition key 設計（從 shard key 翻譯）、見 <a href="../partition-key-design/">partition-key-design</a></li>
<li>Phase 2：bulk export-import（mongodump → Cosmos DB Data Migration Tool）</li>
<li>Phase 3：CDC sync（MongoDB oplog → Azure Data Factory / 自寫 connector）</li>
<li>Phase 4：shadow read 驗證 query 一致性、量 RU consumption baseline</li>
<li>Phase 5：read cutover（讀切 Cosmos、寫仍 MongoDB）</li>
<li>Phase 6：write cutover</li>
<li>Phase 7：cleanup、退役 MongoDB cluster、保留 dump 90 天</li>
</ul>
<h3 id="規格面-5evidence">規格面 5：Evidence</h3>
<ul>
<li>query 一致性 diff log、aggregation result checksum、RU consumption baseline、replication lag</li>
<li>對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">schema-migration-rollout-evidence</a> 的 dual-write 驗證</li>
</ul>
<h3 id="規格面-6cutover--cleanup">規格面 6：Cutover + cleanup</h3>
<ul>
<li>read-only window &lt; 10 min、aggregation result 對齊驗證</li>
<li>Rollback 條件：query error rate &gt; 1% 或 RU consumption 異常偏高（翻譯層 cost 高於估算）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1假設-wire-compat--100-行為相同">Failure 1：假設 wire compat = 100% 行為相同</h3>
<p>「100% wire compat」是 vendor 行銷話術、實際是「在某些 query pattern 下相容」— aggregation pipeline 跑出不同結果、上 production 才發現。9.C30 case 揭露的「『MongoDB 不夠用』是行銷話術。實際是 <em>MongoDB 在某些 workload pattern 下不夠用</em>」同模型反向適用 — <em>相容性</em> 也是「在某些 query pattern 下相容」、不是普遍相容。</p>
<p>修法：production query corpus dual-write 跑一遍、case-by-case 驗證每個 query pattern、不能假設 wire compat = 行為 100% 一致。Phase 4 shadow read 不是「跑一些 test」、是 <em>把所有 production query 跑一遍、對 checksum</em>。</p>
<h3 id="failure-2_id-當-partition-key">Failure 2：<code>_id</code> 當 partition key</h3>
<p>MongoDB 的 <code>_id</code> 預設 ObjectId、跟 Cosmos DB partition key 邏輯不同；直接拿 <code>_id</code> 當 partition key 容易在 high-cardinality 但低均勻度的 access pattern 下 hot partition（VIP 用戶、機器人帳號）。要審 application 的真實 query pattern、選會均勻散佈的欄位、見 <a href="../partition-key-design/">partition-key-design</a>。</p>
<h3 id="failure-3change-stream-resume-token-跨-api-不可用">Failure 3：Change stream resume token 跨 API 不可用</h3>
<p>MongoDB API 提供 change stream wire compat、但 resume token 格式跟 native MongoDB 不同、跨環境 resume 會失敗。CDC pipeline 在遷移期間需要分兩段：MongoDB 端用原生 resume token、Cosmos DB 端用 Change Feed continuation token、不能 <em>把 token 從 MongoDB 帶到 Cosmos DB 繼續</em>。</p>
<h3 id="failure-4評估時只測-happy-path">Failure 4：評估時只測 happy path</h3>
<p>unsupported aggregation stage 在 dev 環境的 sample data 看不出、production 才爆。常見漏的 stage：<code>$graphLookup</code> / <code>$facet</code> / <code>$bucket</code> / 部分 <code>$lookup</code> pattern / window function。Phase 0 audit 要把 production aggregation pipeline 拉出來、對照 <a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60">Cosmos DB MongoDB API feature support</a> 清單。</p>
<h3 id="failure-5把-dogfood-案例數字當-benchmark">Failure 5：把 dogfood 案例數字當 benchmark</h3>
<p>9.C30 Microsoft 365 case 自承沒提具體 throughput / latency / cost 數字、不能拿 dogfood 案例的「成功」推論「我們團隊遷過去也會成功」— 規模 / workload pattern / 團隊能力都不同。寫 sizing 計畫時要回到 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> 用自己的 query corpus 量、不是抄 dogfood case。</p>
<h3 id="failure-6選-mongodb-api-後想升級-native-mongodb-feature">Failure 6：選 MongoDB API 後想升級 native MongoDB feature</h3>
<p>MongoDB API server version 升級節奏跟 native MongoDB 不同步、新 feature 等待時間長。選 MongoDB API 等於放棄「拿到 native MongoDB 最新 feature」、若團隊 long-term commit Cosmos DB、SQL API 反而是更穩的選擇（feature 自己決定、不等翻譯層）。這條 trade-off 在 selection 階段就要決定、不能 phase 6 才發現。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：MongoDB API 特有 <code>MongoRequests</code> / <code>MongoRequestCharge</code>、diagnostic log 看 aggregation stage 是否被翻譯成 cross-partition query</li>
<li>容量規劃：MongoDB API 翻譯層有 overhead、相同 query SQL API 通常便宜 10-20% — 但這個差距通常不足以驅動 API 切換（切換成本太高、見 Failure 6）</li>
<li>RU baseline：Phase 4 shadow read 階段量每個 query pattern 的 <code>x-ms-request-charge</code>、進 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> 的 capacity forecast</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：API kind 選擇進 cost forecast、不是 sizing 後才補</li>
</ul>
<h2 id="cosmos-db-unique-selection-value-整合四層-framing-收束">Cosmos DB unique selection value 整合（四層 framing 收束）</h2>
<p>讀者讀完本篇要能回答：「我該選 Cosmos DB MongoDB API、Cosmos DB SQL API、還是留 Atlas」 — 答案的四層判讀（對應 Framing 1-4）：</p>
<ul>
<li><strong>遷移路徑（Framing 1）</strong>：你是要保留 + 補周邊、換託管、還是換 vendor？三型風險不同、Forbes 時程不代表 Microsoft 365 時程</li>
<li><strong>dogfood signal（Framing 2）</strong>：你能用 frame 借鑑 Microsoft 365、但避免拿 dogfood 數字當 benchmark</li>
<li><strong>multi-model 是否真用上（Framing 3）</strong>：你的系統未來會不會用 graph / Cassandra / Table API？只用一個 API 時 multi-model unique value 不成立</li>
<li><strong>跨雲 hedging vs Azure 整合（Framing 4）</strong>：你的雲商策略是已定還是未定？已綁 Azure 時 lock-in 是整合延伸、未定時 lock-in 是 hedging 損失</li>
</ul>
<p>四層回答完、selection 才能落地、不是「Azure 上要不要用 Cosmos DB」單一問題。</p>
<h2 id="anti-recommendation">Anti-recommendation</h2>
<ul>
<li>純 MongoDB 投資、未來不會綁 Azure、應留在 Atlas — 跨雲彈性的長期價值高於每月 hosting 差價</li>
<li>MongoDB API 是「Azure 上的 MongoDB 替代品」、<em>不是</em> MongoDB 升級版 — 想要 native MongoDB latest feature 應留在 Atlas / 自管 MongoDB</li>
<li>跨雲 hedging 是 selection 主 driver 時、Cosmos DB（單雲）+ DynamoDB（單雲）+ Spanner（單雲）都不該進候選名單</li>
<li>只用 document model、不用其他 4 個 API 時、multi-model 不該變成 selection 理由 — 此時 Atlas managed 服務的 MongoDB 原生行為通常更穩</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾「MongoDB API vs native SQL API trade-off」backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365 dogfood case</a> — 本文主案例、四層 framing 的證據錨點</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 三型遷移路徑「保留 + 補周邊」對照</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 三型遷移路徑「同 DB 換託管」對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> — Phase 1 partition key 從 shard key 翻譯</li>
<li><a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> — Phase 4 RU consumption baseline</li>
<li><a href="../consistency-levels-engineering/">consistency-levels-engineering</a> — read preference 對應 consistency level</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a> — Atlas 對照</li>
<li><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> — 跨 vendor 遷移共通模型</li>
<li><a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">Database 卡片</a> / <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60">Cosmos DB MongoDB API feature support</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>team 用 RDBMS 設計思維建多個 DynamoDB table（&lt;code>user&lt;/code> / &lt;code>order&lt;/code> / &lt;code>order_item&lt;/code>）跑了一季、第二季開始撞「每個 query 要打 2-3 個 table、application 端拼接邏輯爆炸、latency 跟 cost 線性上升」。最直覺的補救是再加 GSI、結果 GSI 數量超過 5 個還是抓不到 access pattern。這時 team 通常開始問「DynamoDB 怎麼 join」— 那是 &lt;em>誤問&lt;/em>。DynamoDB 不做 join，要嘛把相關 entity 放同 PK 用 SK 前綴區分（single-table design），要嘛這個 workload 根本不該用 DynamoDB。本文先回答後者（DynamoDB 適用度前置判讀），再展開前者（single-table 設計流程）。&lt;/p>
&lt;h2 id="dynamodb-適用度前置判讀4-軸">DynamoDB 適用度前置判讀（4 軸）&lt;/h2>
&lt;p>進到 single-table 設計細節之前要先判讀 workload 是否在 DynamoDB 適用區。下面 4 個維度同時成立、single-table 才有意義；任一條不成立、改回 SQL / 多 vendor 組合可能更便宜。9 個 production case（Zoom / Disney+ / Capcom / PayPay / Tixcraft / Lemino / Amazon Ads / Genesys / Zomato）跨 case 重複揭露這 4 軸是適用度的真實邊界。&lt;/p>
&lt;h3 id="軸-1partition-key-是否天然均勻">軸 1：Partition key 是否天然均勻&lt;/h3>
&lt;p>DynamoDB 容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 就是 workload 的天花板。&lt;code>meeting_id&lt;/code>（Zoom）/ &lt;code>player_id&lt;/code>（Capcom）/ &lt;code>message_id&lt;/code>（PayPay）/ &lt;code>user_id&lt;/code>（Disney+）這類 ID 天然散布、不會集中在少數 partition；反之 &lt;code>event_id&lt;/code>（Tixcraft 售票）/ &lt;code>date&lt;/code>（時間序）/ &lt;code>status&lt;/code>（少數枚舉值）這類 PK 天然不均勻、要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key&lt;/a> 修補才能 single-table。修補成本見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &amp;#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>team 用 RDBMS 設計思維建多個 DynamoDB table（<code>user</code> / <code>order</code> / <code>order_item</code>）跑了一季、第二季開始撞「每個 query 要打 2-3 個 table、application 端拼接邏輯爆炸、latency 跟 cost 線性上升」。最直覺的補救是再加 GSI、結果 GSI 數量超過 5 個還是抓不到 access pattern。這時 team 通常開始問「DynamoDB 怎麼 join」— 那是 <em>誤問</em>。DynamoDB 不做 join，要嘛把相關 entity 放同 PK 用 SK 前綴區分（single-table design），要嘛這個 workload 根本不該用 DynamoDB。本文先回答後者（DynamoDB 適用度前置判讀），再展開前者（single-table 設計流程）。</p>
<h2 id="dynamodb-適用度前置判讀4-軸">DynamoDB 適用度前置判讀（4 軸）</h2>
<p>進到 single-table 設計細節之前要先判讀 workload 是否在 DynamoDB 適用區。下面 4 個維度同時成立、single-table 才有意義；任一條不成立、改回 SQL / 多 vendor 組合可能更便宜。9 個 production case（Zoom / Disney+ / Capcom / PayPay / Tixcraft / Lemino / Amazon Ads / Genesys / Zomato）跨 case 重複揭露這 4 軸是適用度的真實邊界。</p>
<h3 id="軸-1partition-key-是否天然均勻">軸 1：Partition key 是否天然均勻</h3>
<p>DynamoDB 容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 就是 workload 的天花板。<code>meeting_id</code>（Zoom）/ <code>player_id</code>（Capcom）/ <code>message_id</code>（PayPay）/ <code>user_id</code>（Disney+）這類 ID 天然散布、不會集中在少數 partition；反之 <code>event_id</code>（Tixcraft 售票）/ <code>date</code>（時間序）/ <code>status</code>（少數枚舉值）這類 PK 天然不均勻、要 <a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 修補才能 single-table。修補成本見 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a>。</p>
<p><code>9.C18 Zoom</code>、<code>9.C19 Capcom</code>、<code>9.C26 PayPay</code>、<code>9.C27 Disney+</code> 4 個 case 都揭露 partition key 天然均勻是 DynamoDB 「能撐」的前提之一。</p>
<h3 id="軸-2workload-是-control-plane-還是-data-plane">軸 2：Workload 是 control plane 還是 data plane</h3>
<p>DynamoDB 適合存 metadata / state，實際大流量（影音串流 / 大型 BLOB / 全文搜尋）走 CDN / WebRTC / object store。<code>9.C18 Zoom</code> 把媒體串流放 P2P + edge servers、DynamoDB 只承擔會議 metadata；<code>9.C27 Disney+</code> 把 content 放 S3 + CDN、DynamoDB 只承擔 watchlist + 播放進度；<code>9.C19 Capcom</code> 把即時遊戲邏輯放 EKS、DynamoDB 處理持久狀態。讀者該問的不是「DynamoDB 能撐多大流量」、是「我的系統哪一層該放 DynamoDB」。</p>
<p>如果 workload 是 data plane（單筆 payload 上 MB、要做全文搜尋、要存 BLOB），用 DynamoDB 是反模式 — single item 上限 400KB 直接擋掉 BLOB 場景。</p>
<h3 id="軸-3consistency-需求是否可接受-eventual">軸 3：Consistency 需求是否可接受 eventual</h3>
<p>DynamoDB 預設 eventually consistent read、strong read 也只在同 region quorum 內成立。最終一致性可接受的 workload 才適合；strong consistency 必要（跨 entity 原子寫入 / 跨 region 強一致 / 全局單調遞增 ID）必須走 SQL / NewSQL。本軸屬通用工程判讀、case 沒有揭露具體 staleness 閾值；判讀工具是 <a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> 的 per-call site review。</p>
<h3 id="軸-4access-pattern-是否穩定">軸 4：Access pattern 是否穩定</h3>
<p>access pattern 數量穩定且窮舉可列（典型 10-30 個）single-table 才能精準設計 PK/SK 跟 GSI；查詢仍在探索期、pattern 頻繁變動，SQL 多 table 較容易演化、改 query 不用改 schema。本軸也屬通用工程判讀、case 沒明示 access pattern 數量閾值，但 9 個 case 寫進 production 的 access pattern 多半是 <em>業務契約已凍結</em> 的場景（會議 metadata、watchlist、玩家戰績、訊息推送）。</p>
<p>任一軸不成立、回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a> 或考慮多 vendor 組合。4 軸都成立、再進 single-table 設計。</p>
<h2 id="核心概念access-pattern-先於-schema">核心概念：access pattern 先於 schema</h2>
<p>Single-table design 的 first-class concept 是 <em>access pattern 先於 schema</em>：先列 15-30 個 query 才開始設 key、不是先設 schema 再想怎麼 query。</p>
<p>DynamoDB 的 key 結構：</p>
<ul>
<li><strong>PK（partition key）</strong>：決定資料散布到哪個 partition；同 PK 的 item 物理共置（item collection）</li>
<li><strong>SK（sort key）</strong>：決定同 partition 內排序與範圍查詢；composite SK 用 <code>#</code> 分隔層級（如 <code>ORDER#2026-05-27#001</code>）</li>
<li><strong>同 PK 不同 SK 前綴</strong>：把相關 entity 物理共置、用一次 <code>Query</code> 拿回多個 entity；對應 RDB 的 JOIN</li>
</ul>
<p>實際範例（Disney+ 9.C27 揭露的 access pattern）：</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">PK             SK                          Entity
</span></span><span class="line"><span class="ln">2</span><span class="cl">USER#u123      PROFILE                     用戶資料
</span></span><span class="line"><span class="ln">3</span><span class="cl">USER#u123      WATCHLIST#m456              觀看清單項目
</span></span><span class="line"><span class="ln">4</span><span class="cl">USER#u123      PROGRESS#device-iPad#m456   播放進度
</span></span><span class="line"><span class="ln">5</span><span class="cl">USER#u123      PROGRESS#device-TV#m456     播放進度（跨裝置）</span></span></code></pre></div><p>一次 <code>Query PK=USER#u123</code> 拿回該 user 的所有資料、不需要 join。SK 前綴 <code>PROFILE</code> / <code>WATCHLIST#</code> / <code>PROGRESS#</code> 區分 entity type、range query 還能限定「只取 watchlist」（<code>begins_with(SK, &quot;WATCHLIST#&quot;)</code>）。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、<a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">workload model</a>。</p>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 反推 PK/SK 跟 GSI 的 5 步流程。</p>
<h4 id="step-1access-pattern-表窮舉">Step 1：access pattern 表窮舉</h4>
<p>每個 user story 寫成一條 query：</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">| #  | User story                          | Query                                 | Latency | Consistency |
</span></span><span class="line"><span class="ln">2</span><span class="cl">| -- | ----------------------------------- | ------------------------------------- | ------- | ----------- |
</span></span><span class="line"><span class="ln">3</span><span class="cl">| 1  | 顯示用戶 profile                    | GetItem PK=USER#{id} SK=PROFILE       | p99 5ms | eventual    |
</span></span><span class="line"><span class="ln">4</span><span class="cl">| 2  | 取用戶所有觀看清單                  | Query PK=USER#{id} begins_with(SK, &#34;WATCHLIST#&#34;) | p99 10ms | eventual |
</span></span><span class="line"><span class="ln">5</span><span class="cl">| 3  | 跨裝置同步播放進度（最新）          | GetItem PK=USER#{id} SK=PROGRESS#{movie}#latest | p99 15ms | strong |</span></span></code></pre></div><p>15-30 條 query 全列出，這是 single-table 的契約。漏列等於設計時看不到、上線後撞。</p>
<h4 id="step-2entity-relationship--pksk-映射">Step 2：entity-relationship → PK/SK 映射</h4>
<p>常見模式：</p>
<ul>
<li>主 entity 用 <code>{ENTITY}#{id}</code> 當 PK（USER / ORDER / PRODUCT）</li>
<li>子 entity 用同 PK + 不同 SK 前綴（<code>PROFILE</code> / <code>ORDER#{timestamp}</code> / <code>ITEM#{id}</code>）</li>
<li>1-N 關係（user 有多個 watchlist）用同 PK + 不同 SK</li>
<li>N-N 關係（user 跟 friend）用兩條 item（A→B 與 B→A）或單獨 relationship entity</li>
</ul>
<h4 id="step-3gsi-補反向查詢">Step 3：GSI 補反向查詢</h4>
<p>主 PK 覆蓋不到的 access pattern 用 GSI 補：</p>
<ul>
<li>「依 status 查所有 order」→ GSI PK = <code>status</code>、SK = <code>created_at</code></li>
<li>「依 product 查所有買家」→ GSI PK = <code>product_id</code>、SK = <code>user_id</code></li>
</ul>
<p>GSI 數量上限 20、實務 &lt; 5；過多時表示主 PK 設計沒覆蓋夠多 access pattern、應重新設計。詳見 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a>。</p>
<h4 id="step-4cloudformation--terraform-ddl">Step 4：CloudFormation / Terraform DDL</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">Resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">SingleTable</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::DynamoDB::Table</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">BillingMode</span><span class="p">:</span><span class="w"> </span><span class="l">PAY_PER_REQUEST</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">AttributeDefinitions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">PK</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">KeySchema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">HASH</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">RANGE</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">GlobalSecondaryIndexes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span>- <span class="nt">IndexName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span><span class="nt">KeySchema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">              </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">HASH</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">            </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">              </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">RANGE</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">          </span><span class="nt">Projection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">            </span><span class="nt">ProjectionType</span><span class="p">:</span><span class="w"> </span><span class="l">INCLUDE</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">            </span><span class="nt">NonKeyAttributes</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status, created_at]</span></span></span></code></pre></div><h4 id="step-5驗證點">Step 5：驗證點</h4>
<ul>
<li>每個 access pattern 對應一個 <code>Query</code> / <code>GetItem</code>、沒有 <code>Scan</code>、沒有 application-side join</li>
<li>Contributor Insights 看 top-N PK 訪問是否均勻</li>
<li>CloudWatch <code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code> 按 partition 分布觀察</li>
</ul>
<p><strong>Rollback boundary</strong>：access pattern 改動可加 GSI 補；entity 拆 table 比合 table 容易，先合再拆。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>5 個 production 常見踩雷：</p>
<h4 id="case-1late-binding-access-pattern">Case 1：late-binding access pattern</h4>
<p>production 上線半年後 PM 要新 query「按地區列訂單」、PK 沒包 region、只能 <code>Scan</code> 或加 GSI。根因是 access pattern 沒在設計階段窮舉，這是 single-table design 的核心責任。修法：access pattern 表列完整、不可省略；新需求進來先回 access pattern 表 review、再決定加 GSI 還是重設計 PK。</p>
<h4 id="case-2sk-排序衝突">Case 2：SK 排序衝突</h4>
<p>同 PK 下兩種 entity（<code>ORDER#{timestamp}</code> 與 <code>PAYMENT#{timestamp}</code>）混用同 SK 空間、range query 拿 <code>BETWEEN '2026-01-01' AND '2026-12-31'</code> 時 entity 邊界錯亂。修法：SK 前綴必須能 <em>用 <code>begins_with</code> 完全區隔</em> entity（<code>ORDER#2026-...</code> vs <code>PAYMENT#2026-...</code>）。</p>
<h4 id="case-3item-collection-超過-10gb">Case 3：item collection 超過 10GB</h4>
<p>單 PK 下所有 item 加起來超過 10GB 上限、DynamoDB 拒絕新寫入。常見於「user 為 PK + user 有大量歷史 event」場景。修法：歷史 event 改用 <code>USER#{id}#YYYYMM</code> 當 PK 把時間 bucket 切開、或把歷史 event 寫進另一張 archive table（cold path）。</p>
<h4 id="case-4gsi-反向變主表">Case 4：GSI 反向變主表</h4>
<p>開始 GSI 只補 1-2 個 query，半年後 GSI 流量超過主表、cost 翻倍。根因是主 PK 沒設計好、GSI 變成 <em>實質的主存取路徑</em>。修法：重新設計 PK、把 GSI 流量主要的 access pattern 升為主表 query；GSI 從多到少要 application 端配合 cutover。</p>
<h4 id="case-5dynamodb-當-rdbms-用">Case 5：DynamoDB 當 RDBMS 用</h4>
<p>把 normalize 過的 schema 直接搬、每個 business query 要 2-3 個 <code>GetItem</code>、latency 從 5ms 變 30ms。修法：normalize 適合 SQL、不適合 KV；single-table 是把 normalize 拍平、用 denormalize 換 read latency。</p>
<p><strong>Anti-recommendation</strong>：access pattern &lt; 5 個、entity 間關聯弱、查詢仍在探索期 → 用 SQL 或 multi-table 先寫、access pattern 穩定再 single-table。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code>：按 partition 分布看是否均勻</li>
<li><code>ThrottledRequests</code>：早期 hot partition 訊號（provisioned 模式立即可見）</li>
<li><code>SuccessfulRequestLatency</code> p99：on-demand 模式下 hot partition 表現為 latency spike（見 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> 的 mode × partition 交叉判讀）</li>
</ul>
<p>Contributor Insights：top-N partition key 訪問頻率，揭露 single-table 設計後是否仍均勻；每月 cost ~$0.02 per million event、值得開。</p>
<p>GSI 觀測：每個 GSI 獨立 RCU/WCU、projection type（<code>KEYS_ONLY</code> / <code>INCLUDE</code> / <code>ALL</code>）決定 storage cost。</p>
<p>TTL 是 storage cost 防爆的標配（特別在 message-class workload）— PayPay <code>9.C26</code> 揭露 3 億 / 天 × 30 天 = 90 億筆記錄、不清理會撐死 storage 預算；設 TTL attribute 讓 DynamoDB 自動刪過期 item、消耗 0 WCU。</p>
<p>接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 跟 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 Bottleneck localization</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-3dynamodb-在-fleet-治理-frame-的退化">Frame 3：DynamoDB 在 fleet 治理 frame 的退化</h3>
<p>跨 vendor 共通 frame：production scale 走 <em>fleet of clusters</em>（Aurora 200 cluster / CockroachDB 380+ cluster / MongoDB Atlas 20 DB 都是這個 frame）。DynamoDB 在這 frame 退化得最徹底 — <em>不走 fleet of clusters</em>、是用 partition 內部自動切。</p>
<p>對照其他 vendor：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Scale-out 拓樸</th>
          <th>容量決策層</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DynamoDB</td>
          <td>單 table、partition 自動 split / merge</td>
          <td>mode 選擇 + PK 均勻 + GSI 補位</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>Fleet of clusters（business / microservice / 合規）</td>
          <td>Cluster boundary + replica 數量</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>Fleet of clusters or 邏輯一個 cluster + locality</td>
          <td>Per-app vs shared cluster 決策</td>
      </tr>
      <tr>
          <td>MongoDB</td>
          <td>Sharded cluster + 多 cluster（blast radius）</td>
          <td>Shard key + cluster ownership boundary</td>
      </tr>
  </tbody>
</table>
<p><strong>DynamoDB 退化點</strong>：partition 是 <em>vendor 內部物理層</em>、不暴露給應用 — application 看到的永遠是「一張 table」、不需要規劃 cluster boundary。代價是 <em>partition key 設計責任全壓在 schema 上</em>（<a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a>）、不能用「拆 cluster 解 blast radius」當逃避路徑。</p>
<p><strong>例外情境</strong>：DynamoDB 在 <em>合規場景</em> 仍可能走「多 table per market」拓樸（見 Frame 5 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> region-pinned 段）— 但動機是合規 boundary 而非 capacity scale、跟 Aurora fleet driver 結構不同。</p>
<h3 id="dynamodb-在系統中的角色control-plane--metadata--state">DynamoDB 在系統中的角色：control plane / metadata / state</h3>
<p>DynamoDB 不是 universal store、不是 SQL 替代品。3 個 case 重複揭露同一定位：</p>
<ul>
<li><strong>9.C18 Zoom</strong>：媒體串流走 P2P + edge servers、DynamoDB 只承擔會議 / 用戶 metadata。control plane 跟 data plane 分離是 30x DAU surge 能撐的工程前提（不是 DynamoDB 自己魔法）。</li>
<li><strong>9.C27 Disney+</strong>：content 走 S3 + CDN、DynamoDB 只承擔 metadata / watchlist / cross-device 進度。</li>
<li><strong>9.C19 Capcom</strong>：EKS 跑 game server / 處理即時遊戲邏輯、DynamoDB 處理持久狀態。</li>
</ul>
<h3 id="durable-queue--write-buffer-作為正向非-oltp-access-pattern">Durable queue / write-buffer 作為正向非 OLTP access pattern</h3>
<p><code>9.C15 Tixcraft</code> 揭露 DynamoDB 的另一種正向用法 — <em>寫入緩衝層</em>、不是 OLTP：</p>
<ul>
<li>拓元用 DynamoDB 接「訂單」寫入、不是即時生效、是讓 traditional server（金流 / 票庫）用自己能承受的速度消費</li>
<li>架構上 DynamoDB 扮演 durable queue、不是傳統 OLTP DB；這層解耦讓「前端可擴 130 倍、後端不用同步擴」</li>
<li>對比 RDBMS：RDB 寫入要即時可讀、即時索引、即時 transaction commit；DynamoDB 寫入可以「先 durable、之後處理」</li>
<li>寫進你的設計時要明示：這是 <em>非預設</em> access pattern、是 flash-sale / 高峰寫入解耦的工程選擇、不是 DynamoDB 預設定位</li>
</ul>
<p>這個 access pattern 跟 single-table 設計兼容 — PK 仍是 <code>event_id#shard</code>、SK 是 <code>ORDER#{user_id}#{timestamp}</code>、寫入時直接寫，後端傳統 server 慢消費；只是讀路徑是 <em>後端服務 batch 取</em> 而非 user-facing query。</p>
<h3 id="rdb-connection-limit-機制對照">RDB connection limit 機制對照</h3>
<p><code>9.C29 Lemino</code> 揭露為什麼 DynamoDB 在 surge 下不會踩 RDB 的隱性天花板：</p>
<ul>
<li>「connection limits became bottlenecks when experiencing a rapid increase in access」— PostgreSQL/MySQL 每連線吃記憶體 / process、pool 上限 1K-5K、connection 是 RDB 在 surge 下 <em>第一個爆點</em>（不是 CPU / disk）</li>
<li>DynamoDB 的 HTTP API（無 long-lived connection state）天然解這個問題；client 不需要維護 connection pool、AWS SDK 用 connection-less HTTP request</li>
</ul>
<p>選 DynamoDB 不只是 schema 選擇、是 connection model 選擇。single-table 設計 <em>外部</em> 的容量優勢、寫進邊界判讀條件。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — 軸 1 不天然均勻時的 composite key 補救</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — 主 PK 覆蓋不到的 access pattern 補位</li>
<li><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">on-demand-vs-provisioned</a> — access pattern 影響 capacity mode 選擇</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — 軸 3 的 per-call site review</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> — 跨 region 多寫入時 single-table 仍適用、但 conflict resolution 加一層</li>
<li>反向路由：access pattern 探索期 / strong consistency 必要 / data plane workload → 回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/schema-design-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/schema-design-pattern/</guid><description>&lt;p>MongoDB schema design 的初學討論常停在「embedded vs reference 二選一」。真實 production 議題遠不止此：document model 給的 schema flexibility 在第一年是紅利、跑半年後同 collection 開始混三代 schema、application code 三層 if-else 處理欄位缺失與型別漂移。這時候讀者要解的不是「embed 還是 reference」、是 &lt;strong>schema contract 該由誰守、守在哪一層&lt;/strong>。本文把這個議題拆成三條 contract layer 路徑（DB-layer validator / app-layer abstraction / 混合）、配合 embedded / reference / polymorphic 機制與 time-series collection 邊界一起討論。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 document model 適用條件 — 而是 production 部署 + schema governance + 失敗修復 的實作層教學。&lt;/p>
&lt;h2 id="問題情境document-自由的後座力">問題情境：document 自由的後座力&lt;/h2>
&lt;p>MongoDB 適用度的前置判讀有三件事要確認：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>document shape 是否主導資料&lt;/strong>：sensor signal / CMS article / order aggregate 這類「形狀本來就多型 + 隨產品演進」適合 document model；access pattern 固定 + 欄位定型的反而該回 KV 系統或 SQL&lt;/li>
&lt;li>&lt;strong>contract layer 該放哪&lt;/strong>：DB-layer validator 適合 schema 穩定 / 跨服務共用 collection 的場景；app-layer abstraction 適合 schema 演進快 / 微服務獨立 owner；混合適合大型 production&lt;/li>
&lt;li>&lt;strong>跨雲 hedging 是否需要&lt;/strong>：若團隊未來雲商策略不確定、Atlas 跨雲是 selection 訊號；只在單雲跑就不必為 hedging 多付代價&lt;/li>
&lt;/ul>
&lt;p>確認 MongoDB 該用之後，讀者真正在 production 撞到的徵兆：&lt;/p>
&lt;ul>
&lt;li>Document model 早期 schema-less 紅利、跑半年後 collection 同時混三代 schema、application 寫 if-else 處理欄位缺失與型別漂移&lt;/li>
&lt;li>子文件越塞越深、單 document 突破 1-2MB、partial update 仍要把整顆 document load + write、IO 跟 working set 雙重壓力&lt;/li>
&lt;li>反向過度 normalize：訂單跟訂單 item 拆兩個 collection、單一查詢得 N+1 &lt;code>$lookup&lt;/code>、aggregation cost 飆&lt;/li>
&lt;li>IoT / sensor / event log workload 寫進 regular collection、寫入吞吐撞牆但沒考慮 time-series collection&lt;/li>
&lt;li>&lt;code>$lookup&lt;/code> 出現在 hot path、document size warning（16MB 上限預警）、partial update 卻產生大量 disk write、schema validation 報錯比例突然爬升&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 揭露車載 sensor schema 隨車型 / 年份 / 規範演進、polymorphic document 與 schema governance 並存；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a> 揭露 CMS 50+ 微服務透過自建中介 abstraction layer 隔離 schema 變動；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> 揭露 document model 保留 + 跨 vendor 形狀治理。早期 startup MongoDB 三代 schema 並存的具體 incident 細節需未來 case 補完、本文先以「常見 failure pattern」處理。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB schema design 的初學討論常停在「embedded vs reference 二選一」。真實 production 議題遠不止此：document model 給的 schema flexibility 在第一年是紅利、跑半年後同 collection 開始混三代 schema、application code 三層 if-else 處理欄位缺失與型別漂移。這時候讀者要解的不是「embed 還是 reference」、是 <strong>schema contract 該由誰守、守在哪一層</strong>。本文把這個議題拆成三條 contract layer 路徑（DB-layer validator / app-layer abstraction / 混合）、配合 embedded / reference / polymorphic 機制與 time-series collection 邊界一起討論。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 document model 適用條件 — 而是 production 部署 + schema governance + 失敗修復 的實作層教學。</p>
<h2 id="問題情境document-自由的後座力">問題情境：document 自由的後座力</h2>
<p>MongoDB 適用度的前置判讀有三件事要確認：</p>
<ul>
<li><strong>document shape 是否主導資料</strong>：sensor signal / CMS article / order aggregate 這類「形狀本來就多型 + 隨產品演進」適合 document model；access pattern 固定 + 欄位定型的反而該回 KV 系統或 SQL</li>
<li><strong>contract layer 該放哪</strong>：DB-layer validator 適合 schema 穩定 / 跨服務共用 collection 的場景；app-layer abstraction 適合 schema 演進快 / 微服務獨立 owner；混合適合大型 production</li>
<li><strong>跨雲 hedging 是否需要</strong>：若團隊未來雲商策略不確定、Atlas 跨雲是 selection 訊號；只在單雲跑就不必為 hedging 多付代價</li>
</ul>
<p>確認 MongoDB 該用之後，讀者真正在 production 撞到的徵兆：</p>
<ul>
<li>Document model 早期 schema-less 紅利、跑半年後 collection 同時混三代 schema、application 寫 if-else 處理欄位缺失與型別漂移</li>
<li>子文件越塞越深、單 document 突破 1-2MB、partial update 仍要把整顆 document load + write、IO 跟 working set 雙重壓力</li>
<li>反向過度 normalize：訂單跟訂單 item 拆兩個 collection、單一查詢得 N+1 <code>$lookup</code>、aggregation cost 飆</li>
<li>IoT / sensor / event log workload 寫進 regular collection、寫入吞吐撞牆但沒考慮 time-series collection</li>
<li><code>$lookup</code> 出現在 hot path、document size warning（16MB 上限預警）、partial update 卻產生大量 disk write、schema validation 報錯比例突然爬升</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露車載 sensor schema 隨車型 / 年份 / 規範演進、polymorphic document 與 schema governance 並存；<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> 揭露 CMS 50+ 微服務透過自建中介 abstraction layer 隔離 schema 變動；<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 揭露 document model 保留 + 跨 vendor 形狀治理。早期 startup MongoDB 三代 schema 並存的具體 incident 細節需未來 case 補完、本文先以「常見 failure pattern」處理。</p>
<h2 id="核心機制aggregate-rootembeddedreferencepolymorphic">核心機制：aggregate root、embedded、reference、polymorphic</h2>
<p>MongoDB schema design 的第一層是 <em>aggregate root 決定 atomicity 邊界</em>。MongoDB 把寫入 atomicity 限制在「單 document 內」、跨 document 要 multi-document transaction（5.0+ 在 replica set / sharded cluster 都支援、但跨 shard 有性能成本）。aggregate root 是 DDD 概念落地到 MongoDB 的具體實作 — 把「一起讀、一起寫、一致性邊界一致」的資料塞同一個 document。</p>
<ul>
<li><strong>Embedded（subdocument / array）</strong>：寫入 atomic、讀取一次到位；代價是 update sub-element 仍要 rewrite 整顆 document，sub-element 寫頻很高時不適合</li>
<li><strong>Reference（手動 <code>_id</code> foreign key + <code>$lookup</code>）</strong>：document 大小可控，但 join 在 application 或 aggregation 階段做；JOIN-heavy workload 跑這條路徑會 N+1</li>
<li><strong>Polymorphic pattern</strong>：同 collection 用 <code>type</code> discriminator 存多型實體；MongoDB 沒 inheritance、靠 schema validator 與 partial index 維持邊界</li>
<li><strong>16MB document hard limit</strong>：是 MongoDB 機制邊界；working set 在 RAM 的隱性軟限制（單 doc 大小直接影響 page cache 效率）更早就會出問題</li>
</ul>
<h3 id="contract-layer-三條路徑">Contract layer 三條路徑</h3>
<p>跨 case 合成 frame（本章合成、Toyota + Forbes 共同揭露）：document model 的 schema flexibility 在 production 必須以 schema governance 對沖、否則「schema 自由」變「production data inconsistency」（Toyota case 明示）。讀者要選的不是「要不要做 schema governance」、是「contract 守在哪一層」。三條路徑：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>實作機制</th>
          <th>適用條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB-layer contract</td>
          <td>MongoDB <code>$jsonSchema</code> validator + <code>validationLevel</code> + <code>validationAction</code></td>
          <td>Schema 穩定、多服務共用 collection、要 DB 擋髒資料</td>
      </tr>
      <tr>
          <td>App-layer contract</td>
          <td>自建 API abstraction + middleware schema 驗證</td>
          <td>Schema 演進快、微服務獨立 owner、跨雲彈性需求</td>
      </tr>
      <tr>
          <td>混合</td>
          <td>DB 層擋型別 / 必填、app 層擋業務語意 / 版本</td>
          <td>大型 production、多 owner、跨團隊</td>
      </tr>
  </tbody>
</table>
<p><strong>DB-layer 路徑</strong>：<code>$jsonSchema</code> validator 在 production 是「契約 enforcement」工具、不是 dev-time linter。設 <code>validationAction: &quot;error&quot;</code> 寫入直接擋；設 <code>&quot;warn&quot;</code> 只記 log。<code>validationLevel: &quot;moderate&quot;</code> 對既有 doc 放行、對新寫入嚴格；<code>&quot;strict&quot;</code> 對所有寫入都嚴格。適合 schema 穩定到「跨服務共用 collection」的程度。</p>
<p><strong>App-layer 路徑</strong>：9.C37 Forbes 揭露的模式 — 50+ 微服務透過自建中介 abstraction layer 看到穩定的 contract API、DB schema 變動限制在 owner microservice 內。Forbes 跨雲彈性能用起來、核心原因是 abstraction layer 把 schema 治理收斂到單點、跨雲遷移時 abstraction layer 不變、微服務不知道底層 DB 換 cluster 換雲。</p>
<p><strong>混合路徑</strong>：Atlas Application Services、enterprise schema registry 屬此類。DB 層 validator 守底線（欄位型別、必填欄位）、app 層 abstraction 守業務（版本欄位 / 相容處理 / cross-document 一致性）。代價是兩層都要維護、版本同步成本高、適合 production 規模真的撐住這個複雜度的團隊。</p>
<p>讀者選哪條路徑要看：team 規模 / collection 跨服務程度 / schema 演進速度。</p>
<h3 id="time-series-collection60">Time-series collection（6.0+）</h3>
<p>Time-series collection 是 MongoDB 為 IoT / sensor / event log / metrics 設計的 vendor-specific 機制 — 比 regular collection 寫入吞吐高 3-5x、storage 壓縮率更好。資料形狀必須是 <code>{ timestamp, metadata, measurement }</code> 三段式、timestamp 主導。</p>
<p>適用情境：sensor signal 高頻寫入、metrics 系統的 time series、application event log。<strong>不適用情境</strong>：schema 不以 timestamp 為主、需要跨 document update、需要 polymorphic discriminator。</p>
<p>9.C38 Toyota Connected 自承「20 個 Atlas database 沒明確說有沒有用 time series collection — 對 IoT 案例這是重要區分、但 case study 沒揭露」。寫進 production 時必須明示：IoT / sensor 場景該考慮 time-series collection、Toyota case 未揭露實際使用情況、不可寫成「Toyota 使用 time-series collection」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction-boundary</a>（aggregate boundary = transaction boundary）、<a href="/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">data-inconsistency</a>。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：access pattern 盤點</strong>。列出 top 10 query / write、標 read together / write together 集合 — 這份清單決定 embedded vs reference vs polymorphic 的候選。</p>
<p><strong>Step 2：contract layer 決策</strong>。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collection 跨多服務 + schema 穩定</td>
          <td>DB-layer validator</td>
      </tr>
      <tr>
          <td>Schema 演進快 + 微服務獨立 owner</td>
          <td>App-layer abstraction</td>
      </tr>
      <tr>
          <td>大型 production + 多 owner + 跨團隊</td>
          <td>混合（兩者並用）</td>
      </tr>
      <tr>
          <td>IoT / sensor / event log + timestamp 主導</td>
          <td>Time-series collection（取代 regular collection）</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：embed 判準</strong> — 1:few、life-cycle 同步、&lt; 1MB 預期上限；<strong>reference 判準</strong> — 1:many 寫頻不對稱、跨 aggregate 引用。</p>
<p><strong>Step 4：DB-layer 路徑 validator 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">runCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">collMod</span><span class="o">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">validator</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">$jsonSchema</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="s2">&#34;tenantId&#34;</span><span class="p">,</span> <span class="s2">&#34;createdAt&#34;</span><span class="p">,</span> <span class="s2">&#34;items&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">tenantId</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">createdAt</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;date&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">items</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">          <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;array&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="nx">minItems</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="nx">items</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;sku&#34;</span><span class="p">,</span> <span class="s2">&#34;qty&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">              <span class="nx">sku</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">              <span class="nx">qty</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;int&#34;</span><span class="p">,</span> <span class="nx">minimum</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">validationLevel</span><span class="o">:</span> <span class="s2">&#34;moderate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nx">validationAction</span><span class="o">:</span> <span class="s2">&#34;warn&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>灰度策略：先 <code>validationLevel: &quot;moderate&quot;</code> + <code>validationAction: &quot;warn&quot;</code> 觀察兩週、確認 application 不寫違規 doc、再切 <code>&quot;strict&quot;</code> + <code>&quot;error&quot;</code> 封死。</p>
<p><strong>Step 5：App-layer 路徑 abstraction 介面</strong>。9.C37 Forbes 揭露的模式 — middleware 攔截 microservice 寫入、驗 schema、套版本欄位、把 owner microservice 的 schema 變動隔離在 abstraction 內。</p>
<p><strong>Step 6：Polymorphic + partial index</strong> — <code>partialFilterExpression</code> 避免冷分支吃 index 成本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">createIndex</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">timestamp</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span> <span class="nx">partialFilterExpression</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$in</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;click&#34;</span><span class="p">,</span> <span class="s2">&#34;purchase&#34;</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p><strong>Step 7：量測 doc 形狀</strong>。用 <code>bsondump</code> + <code>$bsonSize</code> + <code>collStats</code> 量測：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">coll</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">      <span class="nx">_id</span><span class="o">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      <span class="nx">avg</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$avg</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$bsonSize</span><span class="o">:</span> <span class="s2">&#34;$$ROOT&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="nx">max</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$max</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$bsonSize</span><span class="o">:</span> <span class="s2">&#34;$$ROOT&#34;</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>驗證點：avgObjSize 在預期範圍、validator failure rate &lt; SLO、abstraction layer schema mismatch rate 可追溯。</p>
<p><strong>Rollback boundary</strong>：validator 從 <code>strict</code> 退回 <code>moderate</code> 是 single-command、application code 不必改；abstraction layer 換版需 application code 灰度；已 embed 進去的 schema 變更要靠 backfill migration script、無法 in-place 還原。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Unbounded array growth</strong>：把「使用者所有訊息」embed 進 user document、document 撞 16MB → 寫入直接 reject。修法是改 reference、訊息獨立 collection、用 <code>userId</code> 索引。</p>
<p><strong>Hot subdocument update</strong>：所有寫都打同一個 nested field、wiredTiger document-level lock 退化成熱點，concurrency 看似多核卻被序列化。修法是把熱寫欄位拆 reference document、或改 sharded collection 把寫散開（見 <a href="../shard-key-selection/">shard key selection</a>）。</p>
<p><strong><code>$lookup</code> 在 hot path</strong>：reference 沒設好變 join、p99 latency 隨 collection 大小線性退化。修法是 schema design 階段 denormalize、把 read-together 資料 embed 回 aggregate root；或 <code>$merge</code> 寫 materialized view（見 <a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a>）。</p>
<p><strong>Schema 三代並存（缺 contract layer）</strong>：缺 validator 跟 abstraction layer、舊版欄位殘留、application code 三層 fallback、新 dev onboarding 看不懂哪個欄位是現役。9.C38 Toyota 揭露：document model 的彈性「成本是 production 必須做 schema governance」、否則「schema 自由」變「production data inconsistency」。</p>
<p><strong>Abstraction layer 變成 lock-in</strong>：app-layer contract 寫得太重、跨 vendor 遷移時 abstraction 本身要重寫。該層應該薄、只做 schema 隔離、不做業務邏輯。</p>
<p><strong>Polymorphic 全表掃描</strong>：discriminator 沒進 index、<code>type: &quot;rare&quot;</code> 查詢全表 scan。修法用 partial index 把熱類型蓋住、冷類型走全表也只是冷路徑。</p>
<p><strong>Time-series collection 用錯場景</strong>：把非 timestamp 主導資料塞進 time-series collection、失去 flexibility 又拿不到吞吐紅利。Time-series collection 是專屬優化、不是普適 collection 升級。</p>
<p>Anti-recommendation：</p>
<ul>
<li>access pattern 還沒穩定的早期 MVP 不需要鎖死 schema validator；先用 app-layer abstraction、production 穩定後再決定 DB 層該不該封死</li>
<li>JOIN-heavy / 強 normalize workload 一開始就該回 PostgreSQL JSONB 或 SQL、不是塞進 MongoDB 再 <code>$lookup</code></li>
<li>跨案合成 frame：「不是所有資料都該進 MongoDB」、document-shaped + 形狀變化頻繁的進、access pattern 固定的 KV 走 KV（9.C36 Coinbase 揭露 MongoDB + DynamoDB 按 workload 分流）</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Document 形狀</strong>：<code>collStats.avgObjSize</code>、<code>collStats.size</code> vs <code>storageSize</code>（壓縮比）</li>
<li><strong>Contract 健康</strong>：document validation failure rate、abstraction layer schema mismatch rate</li>
<li><strong>Working set 壓力</strong>：<code>wiredTiger.cache.bytes currently in the cache</code> 對比 working set 估算</li>
<li><strong>Aggregation 副作用</strong>：profiler slow op、<code>$lookup</code> / <code>$unwind</code> 在 hot path 出現位置</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.coll.stats()</code> 看 document 平均 / 最大 size、storage / index size</li>
<li><code>db.runCommand({collMod: ..., validator: ...})</code> 改 validator</li>
<li><code>db.setProfilingLevel(1, {slowms: 100})</code> 抓 slow op</li>
</ul>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：把 doc size 分布、validator failure rate、abstraction layer schema mismatch、<code>$lookup</code> 出現位置列為 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：working set 撐爆 RAM 時的 page fault 信號、跟 doc size 異常增長強相關。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — document 形狀決定 shard key 候選空間</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — <code>$lookup</code> 與 schema reference 互相牽動</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — abstraction layer 跟 cache 層協作</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>document 形狀走樣到無法治理時的 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">→ MongoDB → PostgreSQL 拆 normalize</a> 路徑</li>
<li>保留 document model 換 vendor 三型對照 — 保留主 DB 補周邊（Coinbase）/ 同 DB 換託管（Forbes Atlas）/ 同 model 換 vendor（<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Microsoft 365 Cosmos DB MongoDB API</a>）</li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a> 處理通用 schema 演進原則、本文是 MongoDB-specific 落地；<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a> 對齊 aggregate = atomic 邊界。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「schema design pattern」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — polymorphic + governance</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — abstraction layer 模式</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/core/data-modeling-introduction/">MongoDB Data Modeling</a>、<a href="https://www.mongodb.com/docs/manual/core/schema-validation/">Schema Validation</a>、<a href="https://www.mongodb.com/docs/manual/core/timeseries-collections/">Time Series Collections</a></li>
</ul>
]]></content:encoded></item><item><title>Spanner TrueTime API 深度：GPS + 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/truetime-api-depth/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/truetime-api-depth/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>TrueTime API&lt;/em> — Spanner 用來消滅 single coordinator bottleneck、換到 line-rate scaling 的核心機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="商業邏輯先行truetime-是手段line-rate-scaling-才是目的">商業邏輯先行：TrueTime 是手段、line-rate scaling 才是目的&lt;/h2>
&lt;p>TrueTime 的設計目的是消滅 single coordinator bottleneck、讓 OLTP 拿到 line-rate scaling — external consistency 只是這條路徑上拿到的副產品。讀者若把 TrueTime 當成「一個保證 external consistency 的精巧時間 trick」、會誤把工具當目標、後續所有 commit wait / Paxos / GPS 細節都解錯方向。&lt;/p>
&lt;p>傳統 OLTP（PostgreSQL、MySQL、Cloud SQL）跨節點交易要靠一個 coordinator 決定全局順序、coordinator 本身就是 bottleneck。&lt;code>1x node = 1x throughput&lt;/code> 的線性擴展在 single-primary 模型撞牆、想 scale 只能往應用層 sharding 走、付管理 shard key / 跨 shard query / resharding 的代價。Spanner 換掉這條路徑：TrueTime 把 wall-clock 變成跨 datacenter 可比較的 &lt;em>interval&lt;/em>、Paxos 把 coordinator 變成「拓樸感知的多 leader」（每個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding&lt;/a> split 自己的 Paxos group 各自前進）、commit timestamp 用 TrueTime 對齊到 real-time 順序、不再需要一個全局 coordinator 串行所有 transaction。&lt;/p>
&lt;p>9.C10 Cloud Spanner planetary scale case 揭露的線性擴展證據：「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」是 Spanner 設計目標的直接證據、不只是 marketing 數字。這條揭露 Spanner external consistency 不是「加強版 serializable isolation」、是「coordinator 換拓樸」的 paradigm shift。寫到這裡讀者該意識到一件事：選 Spanner 不是選一個更貴更強的 SQL、是選一條 &lt;em>把 coordinator 拆掉&lt;/em> 的 scaling 路徑。&lt;/p>
&lt;p>&lt;strong>Dogfood 邊界（本文反覆強調）&lt;/strong>：9.C10 是 Google internal dogfood case、不是 customer-facing capacity 參考。「10 億 req/sec」是 Google 全使用者加總、不是單一 instance 配額；「2 nodes → 45K reads / 4 nodes → 90K reads」是 Google internal benchmark 揭露的線性擴展 &lt;em>模式&lt;/em>、不是客戶 SLA 承諾。本文後續所有 9.C10 數字引用都會明示這條邊界、避免讀者誤把 dogfood 當配額。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>TrueTime API</em> — Spanner 用來消滅 single coordinator bottleneck、換到 line-rate scaling 的核心機制。</p></blockquote>
<hr>
<h2 id="商業邏輯先行truetime-是手段line-rate-scaling-才是目的">商業邏輯先行：TrueTime 是手段、line-rate scaling 才是目的</h2>
<p>TrueTime 的設計目的是消滅 single coordinator bottleneck、讓 OLTP 拿到 line-rate scaling — external consistency 只是這條路徑上拿到的副產品。讀者若把 TrueTime 當成「一個保證 external consistency 的精巧時間 trick」、會誤把工具當目標、後續所有 commit wait / Paxos / GPS 細節都解錯方向。</p>
<p>傳統 OLTP（PostgreSQL、MySQL、Cloud SQL）跨節點交易要靠一個 coordinator 決定全局順序、coordinator 本身就是 bottleneck。<code>1x node = 1x throughput</code> 的線性擴展在 single-primary 模型撞牆、想 scale 只能往應用層 sharding 走、付管理 shard key / 跨 shard query / resharding 的代價。Spanner 換掉這條路徑：TrueTime 把 wall-clock 變成跨 datacenter 可比較的 <em>interval</em>、Paxos 把 coordinator 變成「拓樸感知的多 leader」（每個 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> split 自己的 Paxos group 各自前進）、commit timestamp 用 TrueTime 對齊到 real-time 順序、不再需要一個全局 coordinator 串行所有 transaction。</p>
<p>9.C10 Cloud Spanner planetary scale case 揭露的線性擴展證據：「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」是 Spanner 設計目標的直接證據、不只是 marketing 數字。這條揭露 Spanner external consistency 不是「加強版 serializable isolation」、是「coordinator 換拓樸」的 paradigm shift。寫到這裡讀者該意識到一件事：選 Spanner 不是選一個更貴更強的 SQL、是選一條 <em>把 coordinator 拆掉</em> 的 scaling 路徑。</p>
<p><strong>Dogfood 邊界（本文反覆強調）</strong>：9.C10 是 Google internal dogfood case、不是 customer-facing capacity 參考。「10 億 req/sec」是 Google 全使用者加總、不是單一 instance 配額；「2 nodes → 45K reads / 4 nodes → 90K reads」是 Google internal benchmark 揭露的線性擴展 <em>模式</em>、不是客戶 SLA 承諾。本文後續所有 9.C10 數字引用都會明示這條邊界、避免讀者誤把 dogfood 當配額。</p>
<p><strong>Fact vs derive 分層警告</strong>：本段「coordinator bottleneck → TrueTime + Paxos」frame 是跨 Spanner 2012 OSDI 論文 + 公開文件（2024-2026）+ 9.C10 case 合成的工程 frame、不是 9.C10 case 直接展開實作層細節。9.C10 案例直接揭露的 fact 是線性擴展數字跟 dogfood 邊界；本文 derive 的 frame 是「為什麼傳統 OLTP coordinator 是 bottleneck」。引用時這條分層在每段引用具體數字時都會重申。</p>
<h2 id="問題情境跨-region-oltp-的順序漏洞">問題情境：跨 region OLTP 的順序漏洞</h2>
<p>跨 region OLTP 想保證「全球用戶看到的交易順序跟 wall clock 一致」、但 NTP 同步誤差動輒 10-100ms、足夠讓 region A 已 commit 的計費事件被 region B 看到一個更新的 timestamp 卻是舊狀態。讀者徵兆通常從這幾個地方浮現：分散式系統團隊在 Cloud SQL / Aurora 多 region 上做 read replica、發現「跨 region read 順序顛倒」、audit log timestamp 不可靠、reconcile 對帳對不上、業務以為自己用了 transaction 就有「強一致」、實際只有 single-node 的 serializable isolation。</p>
<p>真實壓力場景：Google Ads 計費需要把每筆扣款事件放進可驗證的 <em>外部</em> 順序、不只是 transaction 內部 serializable。讀者若把這套需求帶回自家系統、會發現一條共同訊號 — 「兩個 transaction 都 commit 成功、用戶體感卻違反順序」這種事故、不是 isolation level 的問題、是 <em>external consistency</em> 的問題。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale</a> — Google Ads / Play 訂閱 / Search 計費跟 TrueTime 綁定。<strong>dogfood 邊界明示</strong>：9.C10 是 Google 內部 dogfood case、不是 customer-facing capacity 參考；引用其揭露的線性 scaling 模式時要分清「設計目標證據」vs「客戶可獲得配額」。</p>
<h2 id="核心機制truetime-的-api-跟硬體基礎">核心機制：TrueTime 的 API 跟硬體基礎</h2>
<p>TrueTime 對外只有兩個 primitive — <code>TT.now()</code> 回傳一個 <em>interval</em> <code>[earliest, latest]</code>、不是單一時刻；<code>TT.after(t)</code> / <code>TT.before(t)</code> 判斷一個事件是否確定在 t 之後 / 之前。整個 external consistency 演算法都建立在「時間是一個 interval、不是一個點」這個 API 設計上。</p>
<h3 id="硬體基礎gps--原子鐘冗餘">硬體基礎：GPS + 原子鐘冗餘</h3>
<p>每個 datacenter 部署 GPS 接收器 + 原子鐘（armageddon master、用來防 GPS 全網干擾）、time master 之間互相比對排除離群值、TrueTime daemon 從多個 master 拉時間並算 worst-case bound。GPS 給 absolute time reference、原子鐘給 short-term stability（GPS 短暫失聯時仍能用 drift bound 撐過去）。雙來源是為了把 ε 的失敗模式限制在「絕大多數時間 ε ≤ 7ms、極端事件下 ε spike 但不會無限制漂移」。</p>
<h3 id="不確定性-εepsilon">不確定性 ε（epsilon）</h3>
<p>跨 datacenter 同步 + clock drift 估計、ε 目標維持在 1-7ms 區間。</p>
<p><strong>Fact source 分層警告</strong>：1-7ms 是 Google 2012 OSDI 論文 + Spanner 公開文件（2024-2026）引用的範圍、9.C10 dogfood case 未直接揭露 production ε 分布。引用時這組數字明標「來自 Spanner vendor docs / 2012 論文、不是 9.C10 case 直接揭露」、避免讀者把兩種來源混為一談。</p>
<h3 id="commit-wait-機制external-consistency-的核心">Commit wait 機制：external consistency 的核心</h3>
<p>read-write transaction 要拿 commit timestamp s 時、Spanner 設 <code>s = TT.now().latest</code>、然後 <em>等待</em> 直到 <code>TT.after(s)</code> 才回 ACK。這段「等」就是 <a href="/blog/backend/knowledge-cards/commit-wait/" data-link-title="Commit Wait" data-link-desc="Spanner external consistency 的核心機制 — read-write transaction 拿 commit timestamp s 後等到 TT.after(s) 才 ACK、wait ≈ 2ε、付 latency tax 換 commit 順序 = real-time 順序">Commit Wait</a> — Spanner 特有的物理延遲、由 TrueTime ε 主導、跟 <a href="/blog/backend/knowledge-cards/cross-region-quorum/" data-link-title="Cross-Region Quorum" data-link-desc="multi-region distributed SQL 強制 voting replica 跨 region、commit 等多 region quorum ack、跨洲 RTT 物理硬限">Cross-Region Quorum</a> 的網路 RTT 是兩個獨立的延遲來源、不能混算。</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">T1 開始 commit            T1 確定可回 ACK
</span></span><span class="line"><span class="ln">2</span><span class="cl">       |                          |
</span></span><span class="line"><span class="ln">3</span><span class="cl">       v                          v
</span></span><span class="line"><span class="ln">4</span><span class="cl">TT.now().earliest .... s = TT.now().latest .... TT.after(s)
</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">                            |---------- commit wait ≈ ε ----------|
</span></span><span class="line"><span class="ln">7</span><span class="cl">       |---------- total commit wait ≈ 2ε（從拿 s 那刻開始） ---------|</span></span></code></pre></div><p>commit wait ≈ 2ε 的數學保證了「下一個 transaction 拿到的 timestamp 一定 &gt; s」、external consistency 的全序性質就由這個 wait 撐住。<strong>Fact source 分層</strong>：commit wait ≈ 2ε 的推導來自 Spanner 2012 OSDI 論文 + 官方文件、不是 9.C10 case 直接展開實作層數學。引用這條數學要附「來源 vendor docs / paper」、避免讀者誤以為這是 case 揭露。</p>
<h3 id="跟通用-linearizability-卡片的差異">跟通用 linearizability 卡片的差異</h3>
<p><a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">Linearizability</a> 只要求「存在某個全序」、external consistency 進一步要求「全序跟 real-time 順序一致」。TrueTime 是把後者變可實作的關鍵 — 它把跨 datacenter 的「real-time 順序」變成可機械判定的 <code>TT.after(s)</code>、不需要全局 coordinator 來決定誰先誰後。對應的概念卡：<a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external-consistency</a>、<a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a>、<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>。</p>
<h2 id="操作流程怎麼觀測-ε-跟調用-truetime">操作流程：怎麼觀測 ε 跟調用 TrueTime</h2>
<p>TrueTime 本身不對外暴露給 application 操作、ε / commit wait 由 Spanner 內部執行。團隊能做的是 <em>觀測</em> ε 跟 <em>選擇</em> 不同強度的 read consistency。</p>
<h3 id="觀測-ε">觀測 ε</h3>
<p>Cloud Monitoring metric <code>spanner.googleapis.com/instance/clock_skew_ms</code> 是 ε 的對外指標、判讀正常 &lt; 7ms、異常 spike &gt; 50ms 代表 time master 失聯或 GPS 干擾。把這條 metric 跟 <code>commit_latencies</code> p99 配成 evidence pair：ε spike 時 commit latency heatmap 應該整層平移、若 commit latency 動但 ε 沒動、不是 TrueTime 的問題、是 quorum / network 的問題。</p>
<h3 id="跨-region-instance-配置時的-truetime-影響">跨 region instance 配置時的 TrueTime 影響</h3>
<p>voting region 越分散、ε 上限越高、commit wait 越長 → write latency 直接受 ε 影響。multi-region instance config 在做 region layout 決策時要把「voting region 散布範圍」當 latency budget 的固定支出、不是配完才補觀測。</p>
<h3 id="read-only-transaction-的-staleness-選項">read-only transaction 的 staleness 選項</h3>





<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">strong              → 等 TrueTime 確認可讀最新、付完整 commit wait + quorum cost
</span></span><span class="line"><span class="ln">2</span><span class="cl">exact_staleness(t)  → 讀 t 秒前快照、避開 commit wait、適合 reporting / analytics
</span></span><span class="line"><span class="ln">3</span><span class="cl">bounded_staleness(t)→ 容忍 t 秒、可讀最近的本地 replica 副本、不跨 region quorum</span></span></code></pre></div><p>stale / bounded staleness 走的是 Spanner 版的 <a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">Follower Read</a> — 本地 replica serve 不參與 commit 的 read、避開跨 region quorum 把 read latency 降到 single-region 等級。</p>
<p>三者 trade-off 在 SDK 層顯式設定、不是 isolation level：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Spanner Go SDK 範例（time-sensitive、查最新文件確認 API）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nf">Single</span><span class="p">().</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nf">WithTimestampBound</span><span class="p">(</span><span class="nx">spanner</span><span class="p">.</span><span class="nf">MaxStaleness</span><span class="p">(</span><span class="mi">10</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)).</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nf">Query</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">statement</span><span class="p">)</span></span></span></code></pre></div><h3 id="驗證點跟-rollback-boundary">驗證點跟 rollback boundary</h3>
<p>跑 cross-region write + cross-region read benchmark、量 p50 / p99 write latency、確認 ≈ 2ε + quorum RTT 的數量級。TrueTime 配置不由用戶調、commit wait 由 Spanner 自動執行；應用層 rollback boundary 在「改用 stale read / bounded staleness」而不是「關掉 TrueTime」 — TrueTime 是 Spanner 內部不可關的機制、不是 feature flag。</p>
<h2 id="失敗模式ε-暴衝跟誤用-strong-read">失敗模式：ε 暴衝跟誤用 strong read</h2>
<h3 id="ε-暴衝time-master-失聯">ε 暴衝（time master 失聯）</h3>
<p>GPS 干擾、datacenter time master 雙故障、ε 從 4ms 跳到 200ms → 所有 write 的 commit wait 暴增、p99 write latency 從 50ms 變 500ms。徵兆是 Cloud Monitoring <code>commit_latencies</code> heatmap 整層平移、<code>clock_skew_ms</code> 同步上升。根因不在 application、在 datacenter 物理層、修法是等 GCP 內部 time master 恢復、應用層只能臨時降到 bounded staleness 救 read path。</p>
<h3 id="把-strong-read-用在不需要的路徑">把 strong read 用在不需要的路徑</h3>
<p>報表、analytics、user profile fetch 全用 strong read、每次 read 都付 TrueTime 對齊代價、p99 read 跟 write 同步退化。徵兆是 <code>commit_latencies</code> 沒動、但 <code>api/request_latencies</code> for <code>ExecuteSql</code> 整體上升。修法是把 read path 分類、reporting / analytics 改 bounded staleness、保留 strong read 給「讀後決策再寫」的 critical path。</p>
<h3 id="在-client-側做自己的-timestamp">在 client 側做「自己的 timestamp」</h3>
<p>application 用 <code>time.Now()</code> 當業務 key、跨 region 寫入時 client clock skew 直接破壞順序 — Spanner 內部 external consistency 對、業務層卻錯。徵兆是對帳系統發現 timestamp 順序顛倒、但 Spanner audit log 都 OK。修法是業務層 timestamp 全改用 Spanner <code>PENDING_COMMIT_TIMESTAMP</code> sentinel、commit 時由 Spanner 填、不靠 client clock。</p>
<h3 id="把-spanner-當-single-region-sql-用卻配-multi-region-instance">把 Spanner 當 single-region SQL 用、卻配 multi-region instance</h3>
<p>每筆 write 都付跨洲 quorum + commit wait、cost 跟 latency 都浪費。徵兆是 instance config 是 multi-region 但實際 read 99% 來自單一 region、write 也是。修法是降到 regional instance、把跨 region 需求改用 read-only replica 或 export 到 BigQuery。</p>
<h3 id="ε-沒監控">ε 沒監控</h3>
<p>團隊直到事故才看 clock_skew metric、被動處理而非主動告警。建議 <code>clock_skew_ms &gt; 20ms</code> warn、<code>&gt; 50ms</code> page、跟 commit_latencies p99 偏離 baseline 2x 一起當 saturation discovery 訊號（回 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>）。</p>
<h2 id="容量與觀測truetime-ε-是-latency-budget-的固定支出">容量與觀測：TrueTime ε 是 latency budget 的固定支出</h2>
<p>必看 metric：</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">commit_latencies (p50 / p95 / p99)        → commit wait + quorum RTT 的總和
</span></span><span class="line"><span class="ln">2</span><span class="cl">api/request_count by method               → strong read vs stale read 的分布
</span></span><span class="line"><span class="ln">3</span><span class="cl">instance/cpu/utilization_by_priority      → high / low priority 分流
</span></span><span class="line"><span class="ln">4</span><span class="cl">clock_skew_ms                             → TrueTime ε 的對外指標</span></span></code></pre></div><p>用 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 框架把 TrueTime ε 跟 commit latency 配成 evidence pair。Capacity 規劃路由回 <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>、把「ε × write rate」當 latency budget 的固定支出 — 寫越多筆、commit wait 累積成本越高、不是 free。</p>
<p>Alert 建議：</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Warn</th>
          <th>Page</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>clock_skew_ms</code></td>
          <td>&gt; 20ms</td>
          <td>&gt; 50ms</td>
      </tr>
      <tr>
          <td><code>commit_latencies</code> p99</td>
          <td>baseline 1.5x</td>
          <td>baseline 2x</td>
      </tr>
      <tr>
          <td><code>low_priority_utilization</code></td>
          <td>&gt; 80%</td>
          <td>&gt; 90%</td>
      </tr>
  </tbody>
</table>
<h3 id="line-rate-scaling-驗證呼應商業邏輯先行段">Line-rate scaling 驗證（呼應商業邏輯先行段）</h3>
<p>擴 node 數時量「read throughput / node」是否維持線性 — 9.C10 揭露的 2 → 4 nodes = 45K → 90K reads/sec 是 Google internal dogfood 的線性模式、不是客戶 SLA 承諾。團隊在自己 instance 上要驗證的不是「能不能達到 90K reads」、是「擴 node 後 throughput / node 有沒有保持線性」。若曲線 sub-linear、檢查是否 hot split / hot range / Paxos group 不均、TrueTime 機制本身不解這層。</p>
<h2 id="邊界與整合何時不用-truetime或不用-spanner">邊界與整合：何時不用 TrueTime（或不用 Spanner）</h2>
<h3 id="何時改用-stale-read">何時改用 stale read</h3>
<p>reporting / analytics / dashboard 場景改用 bounded staleness 換 cost、不付 commit wait 的 latency tax。判準：若這個 read path 用 5 秒前的資料不會影響業務決策、改 stale read；若會、保留 strong read。</p>
<h3 id="何時不該升-spanner">何時不該升 Spanner</h3>
<p>單 region workload 不該為了 external consistency 升 Spanner、Cloud SQL + serializable isolation 已經夠。9.C10 dogfood 揭露的線性 scaling 是「跨 region + 大規模」場景的設計目標、單 region 用戶拿不到對應的 cost / latency benefit。詳見遷移判讀：<a href="../migrate-from-cloud-sql-pg/">Cloud SQL → Spanner Migration Playbook</a> 的 no-go condition 段。</p>
<h3 id="sibling-deep-articles-路由">Sibling deep articles 路由</h3>
<ul>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：為什麼 external consistency ≠ serializability ≠ linearizability、line-rate scaling 對照表、cross-region quorum 100-200ms 物理硬限</li>
<li><a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>：schema change 也用 TrueTime 保證 version 邊界、parent-child storage layout</li>
<li><a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a>：cutover 階段需要把 application 對 timestamp 的假設審一遍（特別是 client 端 <code>time.Now()</code> 那條失敗模式）</li>
</ul>
<h3 id="跟-1x-章節的互引">跟 1.x 章節的互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>：Spanner 是 PC 系統的代表、Cosmos DB AP 系統當對照</li>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>：external consistency 是 transaction boundary 的全球延伸</li>
</ul>
<h3 id="anti-recommendation">Anti-recommendation</h3>
<p>讀者讀完本文應該能判斷：TrueTime 不是「保證強一致」的功能、是「換 scaling 路徑」的核心；若團隊只想要「強一致」、不需要「跨節點線性擴展」、PostgreSQL serializable + 應用層補上 client-side ordering 就夠、不必為 TrueTime 付 GCP lock-in 的 cost。</p>
]]></content:encoded></item><item><title>pgvector Deep Dive：HNSW / IVFFlat 取捨跟跟專業 Vector DB 對比</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgvector-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgvector-deep-dive/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>pgvector extension&lt;/em> — 用 PG 解 vector search workload 的路徑、是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem&lt;/a> 內最受關注的 extension。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pgvector-是-pg-變-vector-db-的最短路徑">pgvector 是 PG 變 Vector DB 的最短路徑&lt;/h2>
&lt;p>pgvector 加兩件事：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 加 vector column（dimension 必須事先決定）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SERIAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1536&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- OpenAI ada-002 維度
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 三種 distance operator
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[0.1, 0.2, ...]&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- L2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;#&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[0.1, 0.2, ...]&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- inner product
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[0.1, 0.2, ...]&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- cosine&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Operator 對應：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>pgvector extension</em> — 用 PG 解 vector search workload 的路徑、是 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 內最受關注的 extension。</p></blockquote>
<hr>
<h2 id="pgvector-是-pg-變-vector-db-的最短路徑">pgvector 是 PG 變 Vector DB 的最短路徑</h2>
<p>pgvector 加兩件事：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">vector</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="c1">-- 加 vector column（dimension 必須事先決定）
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">content</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="n">embedding</span><span class="w"> </span><span class="n">vector</span><span class="p">(</span><span class="mi">1536</span><span class="p">)</span><span class="w">  </span><span class="c1">-- OpenAI ada-002 維度
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 三種 distance operator
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;-&gt;</span><span class="w"> </span><span class="s1">&#39;[0.1, 0.2, ...]&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">  </span><span class="c1">-- L2
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;#&gt;</span><span class="w"> </span><span class="s1">&#39;[0.1, 0.2, ...]&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">  </span><span class="c1">-- inner product
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;=&gt;</span><span class="w"> </span><span class="s1">&#39;[0.1, 0.2, ...]&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">  </span><span class="c1">-- cosine</span></span></span></code></pre></div><p>Operator 對應：</p>
<table>
  <thead>
      <tr>
          <th>Operator</th>
          <th>意義</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>&lt;-&gt;</code></td>
          <td>L2 distance</td>
          <td>通用、空間距離</td>
      </tr>
      <tr>
          <td><code>&lt;#&gt;</code></td>
          <td>Negative inner product</td>
          <td>normalized vector、cosine 等價</td>
      </tr>
      <tr>
          <td><code>&lt;=&gt;</code></td>
          <td>Cosine distance</td>
          <td>embedding 比較最常用</td>
      </tr>
  </tbody>
</table>
<p>對 OpenAI / Cohere / sentence-transformers embedding、通常用 <code>&lt;=&gt;</code>（cosine）— embedding model 訓練時是 cosine objective。</p>
<h2 id="ann-index-是-vector-search-的核心">ANN Index 是 Vector Search 的核心</h2>
<p>不加 index 的 <code>ORDER BY embedding &lt;=&gt; ?</code> 是 <em>full scan</em>：</p>
<ul>
<li>100K row、1536 dim、每 query ~2-5s（不可用）</li>
<li>1M row 直接超時</li>
</ul>
<p>pgvector 提供兩種 <em>Approximate Nearest Neighbor</em>（ANN）index：</p>
<table>
  <thead>
      <tr>
          <th>Index</th>
          <th>Build 時間</th>
          <th>Query 時間</th>
          <th>Recall@10</th>
          <th>Memory cost</th>
          <th>Update 行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IVFFlat</td>
          <td>快（分鐘級）</td>
          <td>中（10-100ms）</td>
          <td>90-95%</td>
          <td>中（lists 數量）</td>
          <td>Insert OK、需重建保持 recall</td>
      </tr>
      <tr>
          <td>HNSW</td>
          <td>慢（小時級）</td>
          <td>快（1-10ms）</td>
          <td>95-99%</td>
          <td>高（2-4x 資料）</td>
          <td>Insert OK、graph 漸進維護</td>
      </tr>
  </tbody>
</table>
<p><strong>選 IVFFlat 的場景</strong>：</p>
<ul>
<li>Embedding 量 &lt; 1M</li>
<li>Build 時間敏感（CI / batch 環境）</li>
<li>Memory 緊</li>
<li>接受重建 cost（每月 / 每季）</li>
</ul>
<p><strong>選 HNSW 的場景</strong>：</p>
<ul>
<li>Embedding 量 1M-100M</li>
<li>Query latency &lt; 50ms 要求</li>
<li>Memory 充足</li>
<li>Insert 量穩定（不會爆炸性增長）</li>
</ul>
<h2 id="ivfflat分-cluster-找鄰居">IVFFlat：分 Cluster 找鄰居</h2>
<p>IVFFlat 機制：</p>
<ol>
<li><strong>Build</strong>：跑 k-means 把所有 vector 分 <code>lists</code> 個 cluster</li>
<li><strong>Query</strong>：先找最近的 <code>probes</code> 個 cluster、再在這些 cluster 內找 nearest neighbor</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Build（lists 數量重要）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">ivfflat</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">vector_cosine_ops</span><span class="p">)</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">lists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Query 時調 probes 換 recall vs latency
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="n">ivfflat</span><span class="p">.</span><span class="n">probes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;=&gt;</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p><strong>Lists 跟 probes sizing 規則</strong>（pgvector 官方建議）：</p>
<table>
  <thead>
      <tr>
          <th>Row count</th>
          <th>lists 建議</th>
          <th>probes 建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 1M</td>
          <td><code>rows / 1000</code></td>
          <td><code>sqrt(lists)</code></td>
      </tr>
      <tr>
          <td>&gt; 1M</td>
          <td><code>sqrt(rows)</code></td>
          <td><code>sqrt(lists)</code></td>
      </tr>
  </tbody>
</table>
<p>實務：100K row → lists=100 / probes=10、1M row → lists=1000 / probes=32。</p>
<p><strong>IVFFlat 的 recall drift</strong>：cluster 是 build 時固定的、新 insert 的 vector 進入「最近 cluster」、但隨資料分布改變、cluster center 可能不再代表性、recall 隨時間下降。</p>
<p>修法：定期 <code>REINDEX INDEX CONCURRENTLY ...</code>（每月 / 每 100K 新 row）。</p>
<h2 id="hnswmulti-level-graph-找鄰居">HNSW：Multi-level Graph 找鄰居</h2>
<p>HNSW（Hierarchical Navigable Small World）機制：</p>
<ol>
<li>多層 graph、上層稀疏、下層密集</li>
<li>Query 從上層 entry point 開始、逐層找近鄰、最後在底層精細搜尋</li>
<li>Insert 漸進維護 graph、不必重建</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Build（兩個關鍵參數）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">hnsw</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">vector_cosine_ops</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">m</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">16</span><span class="p">,</span><span class="w"> </span><span class="n">ef_construction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- Query 時調 ef_search
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="n">hnsw</span><span class="p">.</span><span class="n">ef_search</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;=&gt;</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p><strong>參數含義</strong>：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>含義</th>
          <th>預設</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>m</code></td>
          <td>每 node 最多鄰居數</td>
          <td>16</td>
          <td>大 → recall 高、memory 多</td>
      </tr>
      <tr>
          <td><code>ef_construction</code></td>
          <td>Build 時 graph 質量參數</td>
          <td>64</td>
          <td>大 → build 慢、graph 質量好</td>
      </tr>
      <tr>
          <td><code>ef_search</code></td>
          <td>Query 時搜尋範圍</td>
          <td>40</td>
          <td>大 → recall 高、latency 高</td>
      </tr>
  </tbody>
</table>
<p><strong>Build cost 真實量級</strong>（1M vector × 1536 dim）：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>Build 時間</th>
          <th>Memory</th>
          <th>Recall@10</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>m=8, ef_construction=32</td>
          <td>30 min</td>
          <td>4GB</td>
          <td>92%</td>
      </tr>
      <tr>
          <td>m=16, ef_construction=64</td>
          <td>2 hour</td>
          <td>8GB</td>
          <td>96%</td>
      </tr>
      <tr>
          <td>m=32, ef_construction=200</td>
          <td>8 hour</td>
          <td>16GB</td>
          <td>98%</td>
      </tr>
  </tbody>
</table>
<p>Production 多數選中間 <code>m=16, ef_construction=64</code>、recall / cost 平衡。</p>
<h2 id="hybrid-searchvector--filter-一起">Hybrid Search：Vector + Filter 一起</h2>
<p>Vector search 加 SQL filter 是 pgvector 比專業 vector DB 強的場景：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Vector + metadata filter
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">category</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;tech&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2025-01-01&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;=&gt;</span><span class="w"> </span><span class="s1">&#39;[0.1, 0.2, ...]&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p>但這裡有個 <em>pgvector 的踩雷</em>：filter 跟 ANN index 互動有兩種模式：</p>
<ol>
<li><strong>Pre-filter</strong>（planner 選）：先 filter 出符合條件的 row、再對 subset 跑 vector ordering → 不用 ANN index、可能慢</li>
<li><strong>Post-filter</strong>：用 ANN index 找 top-N、再 filter、可能 N 不夠補</li>
</ol>
<p>pgvector 0.8+（2024-10 release）加入 <em>iterative index scan</em>：HNSW / IVFFlat 一邊掃 graph 一邊 filter、效能比 pre-filter 好 5-10x。0.7+（2024-07）加 halfvec / binary quantization / parallel HNSW build。</p>
<p>實務：filter selectivity 高（&lt; 10%）時、考慮對 filter column 加 index 走 pre-filter；selectivity 低（&gt; 50%）走 iterative scan。</p>
<h2 id="quantization-跟-dimension-reduction">Quantization 跟 Dimension Reduction</h2>
<p>1536 dim float32 vector 一筆 6KB、1M row 6GB、加 HNSW index 後 ~20GB。Memory 緊時的省法：</p>
<h3 id="half-precisionpgvector-07">Half-precision（pgvector 0.7+）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">embedding</span><span class="w"> </span><span class="n">halfvec</span><span class="p">(</span><span class="mi">1536</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p><code>halfvec</code> 是 float16、storage 減半、recall 損失通常 &lt; 1%。</p>
<h3 id="binary-quantization">Binary quantization</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 把每維壓成 1 bit
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">hnsw</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">bit_hamming_ops</span><span class="p">);</span></span></span></code></pre></div><p>Recall 下降明顯（85-90%）、但 storage 1/32、適合「先粗篩再 rerank」hybrid pipeline。</p>
<h3 id="dimension-reduction">Dimension reduction</h3>
<p>訓練 PCA / Matryoshka model 把 1536 dim 降到 256-512 dim、recall 通常損失 &lt; 3%、storage 1/3-1/6。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1dimension-超-2000-限制">Case 1：Dimension 超 2000 限制</h3>
<p><strong>情境</strong>：要用 OpenAI text-embedding-3-large（3072 dim）、<code>CREATE TABLE ... embedding vector(3072)</code> 報錯。</p>
<p>pgvector <code>vector</code> type 上限 2000 dim（IVFFlat / HNSW index 限制）。</p>
<p>修法：</p>
<ul>
<li>改用 <code>halfvec</code>（pgvector 0.7+ 支援 4000 dim）</li>
<li>用 Matryoshka 截斷到 2000 dim 以下</li>
<li>換 embedding model（OpenAI text-embedding-3-small 1536 dim / 可截斷到 256-1024）</li>
</ul>
<h3 id="case-2hnsw-build-太慢">Case 2：HNSW build 太慢</h3>
<p><strong>情境</strong>：1M row build HNSW、跑 8 小時、blocking production。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 用 CONCURRENTLY 不 block
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">documents</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">hnsw</span><span class="w"> </span><span class="p">(...);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 開 maintenance_work_mem
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="n">maintenance_work_mem</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;8GB&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 開 parallel
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="n">max_parallel_maintenance_workers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">7</span><span class="p">;</span></span></span></code></pre></div><p>仍慢的話、考慮：</p>
<ul>
<li>切分 batch insert + index（適合 read-heavy）</li>
<li>用 IVFFlat 短期上線、之後再切 HNSW</li>
<li>改用 cloud managed pgvector（提供更大 instance）</li>
</ul>
<h3 id="case-3ivfflat-不重建-recall-漂移">Case 3：IVFFlat 不重建 recall 漂移</h3>
<p><strong>情境</strong>：IVFFlat build 時資料 100K、現在 500K、新資料 recall 從 92% 降到 75%、user 抱怨「找不到相關文件」。</p>
<p>修法：</p>
<ul>
<li>Monitor recall：定期跑 ground-truth eval（brute-force 對比）</li>
<li>設定 reindex policy：每 100K 新 row 或每月 reindex</li>
<li>換 HNSW：insert 漸進維護、不需 reindex（trade-off：build 更慢）</li>
</ul>
<h3 id="case-4hybrid-search-filter-selectivity-沒設計">Case 4：Hybrid search filter selectivity 沒設計</h3>
<p><strong>情境</strong>：query <code>WHERE user_id = ? ORDER BY embedding &lt;=&gt; ?</code>、user_id 高選擇性（1/1M）、planner 選 vector index scan、掃到 top-K 全不符 user_id、補抓無止盡。</p>
<p>修法：</p>
<ul>
<li><code>EXPLAIN</code> 看 planner 選 pre-filter 還是 vector-first</li>
<li>對 <code>user_id</code> 加 B-tree index、強 planner pre-filter（hint 不容易、用 statistics）</li>
<li>pgvector 0.8+ 用 iterative scan、自動處理</li>
<li>設計 schema：高選擇性 filter（user_id）建議走 pre-filter；低選擇性（category）走 iterative</li>
</ul>
<h3 id="case-5memory-budget-沒抓">Case 5：Memory budget 沒抓</h3>
<p><strong>情境</strong>：1M vector × 1536 dim × HNSW（m=16）= ~12GB index、shared_buffers 8GB、index 不在 cache、每 query disk IO、latency 100ms+。</p>
<p>修法：</p>
<ul>
<li>算 vector + index memory：<code>row × dim × 4 bytes × (1 + index_overhead)</code></li>
<li><code>shared_buffers</code> 至少能放 hot index portion</li>
<li>不行就降 dim（halfvec）/ 升 instance / 拆 sharded</li>
</ul>
<h2 id="跟專業-vector-db-對比">跟專業 Vector DB 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>pgvector</th>
          <th>Pinecone</th>
          <th>Weaviate</th>
          <th>Milvus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 介面</td>
          <td>SQL</td>
          <td>REST/gRPC API</td>
          <td>GraphQL / REST</td>
          <td>gRPC</td>
      </tr>
      <tr>
          <td>Recall</td>
          <td>95-99%（HNSW）</td>
          <td>95-99%</td>
          <td>95-99%</td>
          <td>95-99%</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>中（PG 限制）</td>
          <td>高</td>
          <td>高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hybrid search</td>
          <td>強（完整 SQL）</td>
          <td>中（metadata filter）</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>跟既有 PG 整合</td>
          <td>完美（同 DB join）</td>
          <td>需 sync</td>
          <td>需 sync</td>
          <td>需 sync</td>
      </tr>
      <tr>
          <td>Multi-tenant</td>
          <td>row-level（PG 一致）</td>
          <td>內建</td>
          <td>內建</td>
          <td>partition</td>
      </tr>
      <tr>
          <td>Open source</td>
          <td>是</td>
          <td>否</td>
          <td>是</td>
          <td>是</td>
      </tr>
      <tr>
          <td>Operational cost</td>
          <td>跟 PG 一樣（管 PG 即可）</td>
          <td>Managed-only</td>
          <td>需自管或 cloud</td>
          <td>需自管或 cloud</td>
      </tr>
      <tr>
          <td>Scale 上限</td>
          <td>10M-100M vector</td>
          <td>10B+</td>
          <td>1B+</td>
          <td>10B+</td>
      </tr>
  </tbody>
</table>
<p><strong>選 pgvector 的場景</strong>：</p>
<ul>
<li>Application 已用 PG、不想多管系統</li>
<li>Vector 量 &lt; 100M</li>
<li>需要 join vector + relational</li>
<li>Team SQL 熟、不想學 API SDK</li>
<li>Cost 敏感（managed Pinecone 1M vector 月 ~$70+）</li>
</ul>
<p><strong>選專業 vector DB 的場景</strong>：</p>
<ul>
<li>Vector 量 &gt; 5-20M（依 dim / QPS / recall 要求、pgvector 在這個級別 + 高 QPS 已開始痛、不必撐到 100M 才換）</li>
<li>純 vector workload（沒 relational integration）</li>
<li>需要 multi-tenant SaaS</li>
<li>Throughput 要求極高（&gt; 10K QPS）</li>
<li>不想自管 HNSW build / memory budget / recall drift（managed Pinecone 把這層 ops 轉嫁、cost 換 ops 時間）</li>
<li>需要 dim &gt; 2000（pgvector vector type 限制、halfvec 可到 4000、再大需 dimension reduction）</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a>：其他 PG extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">jsonb-deep-dive</a>：embedding 通常配 metadata JSONB</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/index-selection/" data-link-title="PostgreSQL Index Selection：B-tree / GIN / GiST / BRIN / Hash 對應 workload 的決策樹" data-link-desc="PG 有 6 種 index method（B-tree / Hash / GIN / GiST / SP-GiST / BRIN）跟 partial / expression / covering 三種變體、不是「都用 B-tree 就好」。每種 index 有自己的 query pattern、儲存代價、write amplification 跟 maintenance 成本。本文走 6 種 index 的適用 workload 對照、決策樹、partial / expression / covering / multi-column 變體、5 production 踩雷（過度 index / partial 條件不對 / B-tree 對 JSON 無效 / BRIN 對非 correlated 資料無效 / multi-column 順序錯）、跟 query-optimization 的 EXPLAIN 互補">index-selection</a>：B-tree / GIN / HNSW 整體比較</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a>：vector query 的 EXPLAIN</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 探索其他 PG 擴展可能</li>
<li>回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 看全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C30 Runtastic：Mirrored queue 網路負載瓶頸</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/</guid><description>&lt;p>Runtastic 的案例暴露了 RabbitMQ mirrored queue 的網路成本被嚴重低估。Mirrored queue 的可靠性提升代價是 message 在 cluster 內的網路複製量跟 mirror 數成正比，而這個成本在日常流量下可能不可見、只在壓力測試或突發流量時才暴露。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Runtastic 是 Adidas 旗下的健身追蹤平台，使用者透過 app 記錄跑步、騎車、重訓等運動資料。2020 年 COVID-19 lockdown 期間，居家運動需求爆增，平台的 concurrent user 數量在數週內翻倍。&lt;/p>
&lt;p>Runtastic 的後端架構是 microservice 架構，RabbitMQ 是服務間訊息傳遞的核心。運動資料記錄、通知推送、社交功能（好友排行、挑戰）、analytics 事件都透過 RabbitMQ 的 queue 串接。&lt;/p>
&lt;h2 id="技術挑戰mirroring-的隱藏網路成本">技術挑戰：Mirroring 的隱藏網路成本&lt;/h2>
&lt;p>Runtastic 的 RabbitMQ cluster 使用 mirrored queue（&lt;code>ha-mode: all&lt;/code>）確保訊息在 broker 故障時不遺失。Mirrored queue 把每條訊息同步複製到 cluster 中所有 node — 3 node cluster 代表每條訊息的網路傳輸量是原始大小的 3 倍。&lt;/p>
&lt;p>日常流量下，mirroring 的額外網路負載在 cluster 的頻寬容量之內，效能影響不明顯。但 lockdown 後流量翻倍時，mirroring 的網路負載跟著翻倍 — 更準確地說是翻 2×N 倍（流量 2 倍 × mirror 數 N）。&lt;/p>
&lt;p>Runtastic 的 cluster 使用了共享的網路元件（network switch / load balancer），mirroring 的流量把共享網路元件的頻寬壓到極限。表現是 broker 間的 mirroring 延遲上升 → publisher confirm 延遲上升 → producer 端的 publish latency 從毫秒跳到秒級 → 上游服務開始 timeout。&lt;/p>
&lt;p>問題的隱蔽性在於：日常監控只看 broker 的 CPU、memory、disk，沒有把 inter-node network throughput 作為關鍵指標。網路瓶頸在 broker-level metric 上的表現是「publish confirm 變慢」，容易被誤判為 broker 過載而非網路飽和。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="performance-test-定位瓶頸">Performance test 定位瓶頸&lt;/h3>
&lt;p>Runtastic 在事件發生後用 performance test 重現問題。測試揭露了 mirroring 流量跟 broker 間網路頻寬的關係 — 把 message rate 從日常的 X 推到 2X 時，inter-node traffic 超過 switch 容量，publish confirm latency 開始非線性增長。&lt;/p>
&lt;p>Performance test 的關鍵是把 inter-node network throughput 加入監控維度。RabbitMQ 3.8 的 Prometheus integration 提供了 &lt;code>rabbitmq_raft_term_total&lt;/code>、&lt;code>rabbitmq_channel_messages_published_total&lt;/code> 等指標，但 inter-node bandwidth 需要從 OS 層（&lt;code>node_exporter&lt;/code> 的 network bytes）或 switch 層取得。&lt;/p>
&lt;h3 id="調整-mirroring-配置">調整 mirroring 配置&lt;/h3>
&lt;p>Runtastic 從 &lt;code>ha-mode: all&lt;/code>（所有 node 都 mirror）調整為 &lt;code>ha-mode: exactly, ha-params: 2&lt;/code>（只 mirror 到 2 個 node）。這把每條訊息的網路複製量從 N 倍降到 2 倍，在可靠性（2 個 copy 可以容忍 1 node failure）跟網路成本之間取得平衡。&lt;/p></description><content:encoded><![CDATA[<p>Runtastic 的案例暴露了 RabbitMQ mirrored queue 的網路成本被嚴重低估。Mirrored queue 的可靠性提升代價是 message 在 cluster 內的網路複製量跟 mirror 數成正比，而這個成本在日常流量下可能不可見、只在壓力測試或突發流量時才暴露。</p>
<h2 id="業務背景">業務背景</h2>
<p>Runtastic 是 Adidas 旗下的健身追蹤平台，使用者透過 app 記錄跑步、騎車、重訓等運動資料。2020 年 COVID-19 lockdown 期間，居家運動需求爆增，平台的 concurrent user 數量在數週內翻倍。</p>
<p>Runtastic 的後端架構是 microservice 架構，RabbitMQ 是服務間訊息傳遞的核心。運動資料記錄、通知推送、社交功能（好友排行、挑戰）、analytics 事件都透過 RabbitMQ 的 queue 串接。</p>
<h2 id="技術挑戰mirroring-的隱藏網路成本">技術挑戰：Mirroring 的隱藏網路成本</h2>
<p>Runtastic 的 RabbitMQ cluster 使用 mirrored queue（<code>ha-mode: all</code>）確保訊息在 broker 故障時不遺失。Mirrored queue 把每條訊息同步複製到 cluster 中所有 node — 3 node cluster 代表每條訊息的網路傳輸量是原始大小的 3 倍。</p>
<p>日常流量下，mirroring 的額外網路負載在 cluster 的頻寬容量之內，效能影響不明顯。但 lockdown 後流量翻倍時，mirroring 的網路負載跟著翻倍 — 更準確地說是翻 2×N 倍（流量 2 倍 × mirror 數 N）。</p>
<p>Runtastic 的 cluster 使用了共享的網路元件（network switch / load balancer），mirroring 的流量把共享網路元件的頻寬壓到極限。表現是 broker 間的 mirroring 延遲上升 → publisher confirm 延遲上升 → producer 端的 publish latency 從毫秒跳到秒級 → 上游服務開始 timeout。</p>
<p>問題的隱蔽性在於：日常監控只看 broker 的 CPU、memory、disk，沒有把 inter-node network throughput 作為關鍵指標。網路瓶頸在 broker-level metric 上的表現是「publish confirm 變慢」，容易被誤判為 broker 過載而非網路飽和。</p>
<h2 id="解法">解法</h2>
<h3 id="performance-test-定位瓶頸">Performance test 定位瓶頸</h3>
<p>Runtastic 在事件發生後用 performance test 重現問題。測試揭露了 mirroring 流量跟 broker 間網路頻寬的關係 — 把 message rate 從日常的 X 推到 2X 時，inter-node traffic 超過 switch 容量，publish confirm latency 開始非線性增長。</p>
<p>Performance test 的關鍵是把 inter-node network throughput 加入監控維度。RabbitMQ 3.8 的 Prometheus integration 提供了 <code>rabbitmq_raft_term_total</code>、<code>rabbitmq_channel_messages_published_total</code> 等指標，但 inter-node bandwidth 需要從 OS 層（<code>node_exporter</code> 的 network bytes）或 switch 層取得。</p>
<h3 id="調整-mirroring-配置">調整 mirroring 配置</h3>
<p>Runtastic 從 <code>ha-mode: all</code>（所有 node 都 mirror）調整為 <code>ha-mode: exactly, ha-params: 2</code>（只 mirror 到 2 個 node）。這把每條訊息的網路複製量從 N 倍降到 2 倍，在可靠性（2 個 copy 可以容忍 1 node failure）跟網路成本之間取得平衡。</p>
<p>對可靠性要求最高的 queue（交易相關），維持 <code>ha-mode: all</code> 但把這些 queue 移到頻寬更高的專屬 network segment。</p>
<h3 id="遷移到-quorum-queue-的動機">遷移到 Quorum queue 的動機</h3>
<p>Mirrored queue 的另一個問題是同步機制 — 新 mirror 加入時需要全量同步（sync），sync 期間 queue 可能暫停接受新訊息。RabbitMQ 3.8 引入的 Quorum queue 用 Raft consensus 取代 mirrored queue 的 GM（Guaranteed Multicast），在網路效率跟故障恢復上都有改進。</p>
<p>Runtastic 的案例是「為什麼應該評估從 mirrored queue 遷到 quorum queue」的典型動機 — mirrored queue 的網路成本跟同步行為在規模化時成為瓶頸。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>ha-mode: all</th>
          <th>ha-mode: exactly 2</th>
          <th>Quorum queue</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>網路成本</td>
          <td>每條訊息 × N node</td>
          <td>每條訊息 × 2 node</td>
          <td>每條訊息 × majority</td>
      </tr>
      <tr>
          <td>可容忍的故障</td>
          <td>N-1 node failure</td>
          <td>1 node failure</td>
          <td>minority node failure</td>
      </tr>
      <tr>
          <td>新 node 加入</td>
          <td>全量同步（可能暫停 queue）</td>
          <td>全量同步（影響面小）</td>
          <td>Raft log replay（漸進）</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>小 cluster、低流量</td>
          <td>中 cluster、中流量</td>
          <td>中大 cluster、推薦路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 的 replication 跟 network 成本的關係</li>
<li><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a>：mirrored queue vs quorum queue 的詳細比較</li>
<li><a href="/blog/backend/03-message-queue/vendors/rabbitmq/queue-types-classic-quorum-stream/" data-link-title="RabbitMQ Queue Type 選型：Classic、Quorum、Stream 的責任邊界與容量取捨" data-link-desc="RabbitMQ 3.x 三種 queue type 的選型 deep article — classic queue（mirrored 已 deprecated）、quorum queue（Raft 一致性、取代 mirrored）、stream（3.9&#43; append-only log、可重複消費）。涵蓋三種模型在 throughput / retention / replay / 記憶體成本的判讀、宣告語意差異（實機驗證）、4 個 production 故障演練（mirrored 網路放大 / quorum loss / stream retention 超量 / classic→quorum in-flight message），與容量規劃。">RabbitMQ queue types</a>：Classic / Mirrored / Quorum / Stream 四種 queue type 的取捨</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>：broker 的 inter-node 網路作為 pipeline 健康指標</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>以下訊號出現時，應該回讀本案例：</p>
<ul>
<li>RabbitMQ cluster 使用 <code>ha-mode: all</code> 且 node 數量 &gt; 3</li>
<li>Publish confirm latency 在流量上升時非線性增長</li>
<li>Broker 的 CPU / memory / disk 指標正常但 publish 變慢</li>
<li>Broker 間的 network traffic 佔比超過 cluster 總頻寬的 50%</li>
<li>新 mirror 加入時 queue 出現暫停或大量延遲</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://seventhstate.io/portfolio/portfolio-runtastic/">Runtastic RabbitMQ Performance Case Study</a></li>
</ul>
]]></content:encoded></item><item><title>Datadog Continuous Profiler</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/</guid><description>&lt;p>Datadog Continuous Profiler 的核心責任是把 production profile 接到 SaaS APM、deployment marker、service tag 與 release regression workflow。它適合已經使用 Datadog APM / metrics / logs 的團隊，重點在讓 slow request、resource saturation、deploy version 與 profile diff 能在同一個操作介面中對齊。&lt;/p>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Datadog Continuous Profiler 是 &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> APM 的 &lt;em>production profiling&lt;/em> add-on、跟 Datadog Logs / Metrics / Traces 同 plane、共用 service tag、env tag、version tag 與 query bar。它的核心責任是把 production profile 接到 SaaS APM、deployment marker、service tag 與 release regression workflow，讓 slow request、resource saturation、deploy version 與 profile diff 能在同一個操作介面中對齊。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca&lt;/a> 這類 OSS profiler 比、Datadog Continuous Profiler 走 &lt;em>ecosystem-bundled&lt;/em> 路線 — profiler 本身不獨立計費、跟 APM host 一起進 business unit 預算、profile data 直接跟 trace_id、deploy marker、log query 在同一介面 cross-link。OSS profiler 走 &lt;em>standalone deployment&lt;/em>、profile store 自管（ClickHouse / object storage）、跟 observability 其他 plane 要自己 wire（grafana correlation、自寫 trace_id mapping）。差異在 &lt;em>跨 signal 的 query continuity 跟組織計費歸屬&lt;/em>、flame graph 本身的視覺呈現相近。&lt;/p>
&lt;p>這個定位讓 Datadog Continuous Profiler 接到 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling&lt;/a>。它的價值在於降低 profile diff 的交接成本；它的代價在於 SaaS 成本、agent 設定、資料保留與 vendor 約束。&lt;/p></description><content:encoded><![CDATA[<p>Datadog Continuous Profiler 的核心責任是把 production profile 接到 SaaS APM、deployment marker、service tag 與 release regression workflow。它適合已經使用 Datadog APM / metrics / logs 的團隊，重點在讓 slow request、resource saturation、deploy version 與 profile diff 能在同一個操作介面中對齊。</p>
<h2 id="定位">定位</h2>
<p>Datadog Continuous Profiler 是 <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> APM 的 <em>production profiling</em> add-on、跟 Datadog Logs / Metrics / Traces 同 plane、共用 service tag、env tag、version tag 與 query bar。它的核心責任是把 production profile 接到 SaaS APM、deployment marker、service tag 與 release regression workflow，讓 slow request、resource saturation、deploy version 與 profile diff 能在同一個操作介面中對齊。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a> / <a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a> 這類 OSS profiler 比、Datadog Continuous Profiler 走 <em>ecosystem-bundled</em> 路線 — profiler 本身不獨立計費、跟 APM host 一起進 business unit 預算、profile data 直接跟 trace_id、deploy marker、log query 在同一介面 cross-link。OSS profiler 走 <em>standalone deployment</em>、profile store 自管（ClickHouse / object storage）、跟 observability 其他 plane 要自己 wire（grafana correlation、自寫 trace_id mapping）。差異在 <em>跨 signal 的 query continuity 跟組織計費歸屬</em>、flame graph 本身的視覺呈現相近。</p>
<p>這個定位讓 Datadog Continuous Profiler 接到 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 與 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a>。它的價值在於降低 profile diff 的交接成本；它的代價在於 SaaS 成本、agent 設定、資料保留與 vendor 約束。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Datadog Continuous Profiler deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Agent / SDK profiling 是否真的 enabled</strong>：Datadog Agent 跑著不等於 profiler 開了 — 各語言要在 SDK init 加 <code>profiling_enabled=true</code> 或環境變數 <code>DD_PROFILING_ENABLED=true</code>、Go / Java / Python / Node / Ruby / .NET 的開啟方式跟覆蓋的 profile type（CPU / heap / goroutine / lock / wall time）各不同</li>
<li><strong>Service / version / env tag 紀律</strong>：profile 沒有 <code>service</code> + <code>env</code> + <code>version</code> tag 就無法 diff、release marker 也對不上 — CI 要把 git SHA 或 release tag 注入 <code>DD_VERSION</code>、deploy pipeline 要打 deployment marker API</li>
<li><strong>Sampling rate 跟 production coverage</strong>：profiler 預設 60s 採一次、低流量服務或 short-lived 任務可能 sample 不到 hot path — 對 ultra-low latency / burst workload 要評估 sampling 是否還抓得到 regression signal</li>
<li><strong>Profile ingestion cost / retention</strong>：profile 是按 APM host 計費、但 profile event 量隨 service 數量 + sampling rate 漲、retention 預設 7 天（custom retention 另計）— 大型 deployment 要做 service-level enable/disable governance</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p>Release regression 定位適合 Datadog Continuous Profiler。當 canary 或 release candidate 的 p99、CPU、memory 或 cost per request 退化，團隊可以用 deployment marker 對比 release 前後 profile，找出變寬的 call stack。</p>
<p>APM-to-profile drilldown 適合 Datadog Continuous Profiler。慢 request 可以從 service、endpoint、trace 或 span 往下切到 profile，讓工程師知道 latency 是 DB、network、runtime、serialization、lock 還是 CPU hot path。</p>
<p>多語言 SaaS 團隊適合 Datadog Continuous Profiler。團隊如果同時維護 Go、Java、Python、Ruby、Node.js 或 .NET 服務，SaaS profiler 可以用統一 tag、dashboard 與權限模型管理。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Datadog 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>APM 整合</td>
          <td>trace、service、endpoint、profile 可串接</td>
          <td>service tag 與 deploy label 紀律</td>
      </tr>
      <tr>
          <td>Deployment marker</td>
          <td>release 前後 profile diff 容易建立</td>
          <td>release pipeline 與版本標記整合</td>
      </tr>
      <tr>
          <td>SaaS 操作</td>
          <td>低自管成本、跨團隊易查詢</td>
          <td>成本治理、資料保留與 vendor 約束</td>
      </tr>
      <tr>
          <td>多語言支援</td>
          <td>多 runtime 用同一套操作介面</td>
          <td>各語言 agent overhead 與覆蓋差異</td>
      </tr>
  </tbody>
</table>
<p>APM 整合價值來自上下文連續。Metrics 告訴你 CPU 上升，trace 告訴你 endpoint 變慢，profile 告訴你哪段 code path 變貴；Datadog 的優勢是把這些訊號放進同一個查詢與 dashboard 流程。</p>
<p>Deployment marker 價值來自 release gate。Profile diff 如果能對齊 commit、version、environment 與 canary cohort，就能成為 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a> 的 evidence。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Datadog Continuous Profiler</th>
          <th>Pyroscope</th>
          <th>Parca</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>SaaS only、跟 Datadog Agent / APM 綁</td>
          <td>OSS self-host / Grafana Cloud SaaS</td>
          <td>OSS self-host（Polar Signals SaaS 選）</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>跟 APM host 計費（profile 不獨立 metering）</td>
          <td>OSS 免費 / Grafana Cloud 按 ingestion</td>
          <td>OSS 免費 / SaaS 按 host</td>
      </tr>
      <tr>
          <td>Profile 採集方式</td>
          <td>Language SDK（pull 採樣）</td>
          <td>SDK + eBPF agent</td>
          <td>eBPF-first、language-agnostic</td>
      </tr>
      <tr>
          <td>Trace correlation</td>
          <td>強 — trace_id 自動 link 到 flame graph</td>
          <td>中 — 要自己 wire OTel trace_id</td>
          <td>弱 — 偏 eBPF profile、trace 整合較淺</td>
      </tr>
      <tr>
          <td>視覺 / Workflow</td>
          <td>APM service view + Profile diff + Code Hotspot in IDE</td>
          <td>Grafana flame graph + diff、跟 Loki / Tempo 同 UI</td>
          <td>Parca UI 簡潔、偏單純 profile 探索</td>
      </tr>
      <tr>
          <td>多語言支援</td>
          <td>Go / Java / Python / Node / Ruby / .NET / PHP 官方 SDK</td>
          <td>同 + 社群 SDK；eBPF 補 native binary</td>
          <td>eBPF-only、不挑語言但 symbol 解析較吃力</td>
      </tr>
      <tr>
          <td>Vendor lock-in</td>
          <td>高 — profile 跟 APM workflow 綁、退場要重建 dashboard</td>
          <td>低 — OSS、profile 格式相對開放</td>
          <td>低 — OSS、pprof 格式相容</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Datadog-heavy org、APM / log / metric 已用</td>
          <td>Grafana stack 已用、要省 license</td>
          <td>eBPF-first、low-overhead always-on</td>
      </tr>
  </tbody>
</table>
<p>選 Datadog Continuous Profiler 的核心訴求：<em>Datadog 已是 observability backbone</em> + 要 <em>APM trace ↔ profile drilldown 是 first-class workflow</em> + 接受 SaaS 計費 + 接受 SDK overhead trade-off。如果 Datadog 不是既有平台、單純為了 profiling 引入 Datadog 通常成本不划算、改走 Pyroscope / Parca。</p>
<p>跟一次性 runtime profiler（<code>pprof</code>、<code>async-profiler</code> 手動跑）的差異是時間維度。一次性 profiler 適合本機或 incident 當下調查；continuous profiler 適合 baseline、release diff 與長期退化治理 — 兩者互補、不互斥。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>APM trace ↔ profile correlation</strong>：Datadog SDK 把 <code>trace_id</code> 注入 profile sample 的 label、APM trace view 上每個 span 可以直接點到「執行這段 span 時的 flame graph」。意義是 <em>p99 latency 異常 trace 不只看 span 等待時間、能直接看到該 span 期間 CPU / lock / allocation 真正花在哪段 code</em>。需要 SDK 版本支援 + trace context propagation 正確接上、舊版 SDK 或自寫 instrumentation 容易斷鏈。</p>
<p><strong>Endpoint profiling</strong>：profile 按 HTTP endpoint / RPC method 切片、不只看 service 整體 hot path。意義是 <em>新加的 endpoint 即便 traffic 小、也能單獨看它的 CPU / allocation cost</em>、不會被 service 主流量稀釋。對 multi-tenant API、A/B test endpoint、internal admin endpoint 的退化偵測特別有用。</p>
<p><strong>Code Hotspot in IDE</strong>：Datadog IDE plugin（IntelliJ / VS Code）把 production profile 的 hot line 直接 overlay 到 source code、工程師 review PR 時能看到「這個 function 在 production 佔 service CPU 12%」。降低 <em>看 flame graph → 找 source 對應行</em> 的 cognitive cost。對應 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 中「production signal → code change」的 feedback loop 縮短。</p>
<p><strong>Profile diff（baseline vs candidate）</strong>：Datadog 內建 diff view、選兩個 time window 或兩個 version tag、直接看 flame graph 哪些 frame 變寬 / 變窄。是 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a> 的核心 evidence — canary 跑完 30min、自動拉 baseline vs candidate diff 報告、超過 threshold 阻擋 promote。</p>
<p><strong>Notebooks correlation</strong>：Datadog Notebooks 可以把 profile flame graph、APM trace、metric chart、log query 排在同一份文件。incident post-mortem 跟 release review 寫一份 notebook 比散落多個 dashboard tab 更可追溯、也接 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">evidence package</a> 規範。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>SDK overhead 在 production 過高</strong>：profiler 預設 overhead &lt; 2% CPU、但 wall-time profiling / allocation profiling 全開可能到 5%+ — canary 一台量測、按 profile type 分別 enable、不要全部一次開</li>
<li><strong>Sampling rate 太低 / false negative</strong>：short-lived job（&lt; 60s）或 low-traffic service 可能整個生命週期沒被 sample 到、看不到 hot path — 改成事件觸發 profile（on-demand profiling API）或拉高該 service 的 sampling rate</li>
<li><strong>Profile 沒有 version tag / 無法 diff</strong>：deploy pipeline 沒注入 <code>DD_VERSION</code>、release marker 對不上 — 補 CI 環境變數、用 <code>dd-trace</code> SDK 自動讀 git commit SHA、跑 staging 驗證 diff view 能顯示 version</li>
<li><strong>Trace ↔ profile drilldown 斷鏈</strong>：SDK 版本太舊、或 trace context 在非同步 / queue handler 沒 propagate — 升 SDK + 補 trace context propagation、用一條已知慢 trace 驗證能不能跳到 flame graph</li>
<li><strong>Profiling cost spike</strong>：新 service 開啟 profiling、或某 service profile event 暴增（exception 路徑反覆採樣）— 看 Datadog usage dashboard 的 profile host hour、對嫌疑 service 暫關 profiling 觀察 cost 曲線、再 tune sampling rate</li>
<li><strong>Flame graph symbol 解析失敗 / 顯示 <code>?</code> frame</strong>：缺 debug symbol、stripped binary、或語言 runtime 版本不支援 — 補 build 時保留 symbol、確認 SDK 版本 vs runtime 版本對應表</li>
<li><strong>Lock profile 看不出 contention</strong>：某些語言（Go / Java）的 lock profiling 需要額外 flag（<code>DD_PROFILING_BLOCK_ENABLED</code> / <code>DD_PROFILING_LOCK_ENABLED</code>）— 預設沒開、要明確 enable 才看得到 lock contention flame graph</li>
</ul>
<h2 id="操作成本">操作成本</h2>
<p>Datadog Continuous Profiler 的主要成本是資料量與保留。Profile sample、tag cardinality、service 數量、environment 數量與 retention 都會影響費用與查詢體驗。</p>
<p>Agent 成本來自 runtime 差異。不同語言的 profiler 支援、overhead、可觀測維度與限制不同，導入時要用 canary service 量測 CPU、memory、latency 與 profile completeness。</p>
<p>Vendor 成本來自資料與 workflow 綁定。當 profile diff、release marker、APM drilldown 與 incident workflow 都在 Datadog 中，後續切換平台需要重新建立 tag schema、dashboard、retention 與 gate integration。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Datadog Continuous Profiler 結果應回寫到 evidence package。最小欄位包括 service、version、environment、deploy marker、profile type、time range、comparison baseline、profile diff link、overhead estimate、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Datadog 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>profiler view、profile diff、APM link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>baseline / candidate profile window</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>Datadog profile、trace、dashboard link</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>service tag、version tag、sampling status</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>production coverage、agent overhead</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>runtime coverage、tag drift、retention limit</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 release regression 可追溯。Reviewer 要能從 failed gate 直接打開 profile diff，看出哪個 service、version、endpoint 或 call stack 造成資源成本變化。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>Datadog Continuous Profiler 適合回寫 release regression 與 APM 整合案例。它可接 <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 consolidation</a> 的 profile noise 降低、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a> 的 low-latency hot path 定位、<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase ultra-low latency exchange</a> 的 z1d 單執行緒 hot path 分析、<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ 微服務</a> 的 per-service profile diff，以及 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">Datadog OTel migration practice</a> 的 observability pipeline 整合。</p>
<p>這些案例的重點是上下文對齊。Datadog Profiler 頁引用案例時，要把 case 轉成 service tag、deploy marker、profile diff、trace drilldown 與 release gate evidence — 例如 Coinbase sub-ms 目標下、profile 必須對齊 RAFT consensus 跟 placement group 拓樸、才能解釋 hot path 為何在某些 epoch 才出現。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</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></li>
<li>跨模組：<a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a></li>
<li>官方：<a href="https://docs.datadoghq.com/profiler/">Datadog Continuous Profiler documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/</guid><description>&lt;p>這個案例的核心責任是填補 Azure data-architecture 維度缺口、並提供「MongoDB → Cosmos DB」這個跨產品遷移的官方範本。Microsoft 365 是全球最大 SaaS 之一（月活十億級）、其使用分析平台的容量需求是 planet-scale。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Microsoft 365 在 Cosmos DB 的關鍵敘述（引自 &lt;a href="https://azure.microsoft.com/en-us/blog/microsoft-365-boosts-usage-analytics-with-azure-cosmos-db/">Microsoft 365 boosts usage analytics with Azure Cosmos DB&lt;/a>）：&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>用戶規模&lt;/td>
 &lt;td>Microsoft 365 全球用戶（十億級 MAU）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工作負載&lt;/td>
 &lt;td>使用分析（usage analytics）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷出技術&lt;/td>
 &lt;td>MongoDB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷入技術&lt;/td>
 &lt;td>Azure Cosmos DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移動機&lt;/td>
 &lt;td>「globally-distributed, multi-model」「virtually unlimited elastic scalability」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵敘述：「The team decided to replace MongoDB with Azure Cosmos DB, a fully managed globally-distributed, multi-model database service designed for global distribution and virtually unlimited elastic scalability.」&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Microsoft 365 案例揭露三個全球 SaaS 分析平台的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>MongoDB → Cosmos DB 是「相容 API + 升級擴展性」的遷移路徑&lt;/strong>：Cosmos DB 提供 MongoDB API 相容、應用層程式幾乎不用改、但底層儲存改用 Cosmos DB 的分散式架構。這層遷移成本遠低於改寫 application 到 native Cosmos DB SQL API、適合大規模既有系統。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook&lt;/a>、跟 &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%">9.C20 Zomato&lt;/a> 形成對照。&lt;/li>
&lt;li>&lt;strong>分析平台 vs 交易平台的 DB 取捨不同&lt;/strong>：交易平台優先 latency + consistency（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner&lt;/a>）、分析平台優先 throughput + global distribution + cost。Cosmos DB 5 個 consistency level 讓分析場景可以選 weakest（eventual / session），換最大 throughput。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a> 同思維。&lt;/li>
&lt;li>&lt;strong>Microsoft 自家產品 dogfood Cosmos DB&lt;/strong>：跟 Amazon Prime Day 用自家 DynamoDB（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1&lt;/a>）、Google 自家用 Spanner（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10&lt;/a>）一樣 — 雲商旗艦 DB 都會用在自家旗艦產品。讀此類 dogfood 案例的權重應該高、因為「雲商自己賭身家」。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是填補 Azure data-architecture 維度缺口、並提供「MongoDB → Cosmos DB」這個跨產品遷移的官方範本。Microsoft 365 是全球最大 SaaS 之一（月活十億級）、其使用分析平台的容量需求是 planet-scale。</p>
<h2 id="觀察">觀察</h2>
<p>Microsoft 365 在 Cosmos DB 的關鍵敘述（引自 <a href="https://azure.microsoft.com/en-us/blog/microsoft-365-boosts-usage-analytics-with-azure-cosmos-db/">Microsoft 365 boosts usage analytics with Azure Cosmos DB</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶規模</td>
          <td>Microsoft 365 全球用戶（十億級 MAU）</td>
      </tr>
      <tr>
          <td>工作負載</td>
          <td>使用分析（usage analytics）</td>
      </tr>
      <tr>
          <td>遷出技術</td>
          <td>MongoDB</td>
      </tr>
      <tr>
          <td>遷入技術</td>
          <td>Azure Cosmos DB</td>
      </tr>
      <tr>
          <td>遷移動機</td>
          <td>「globally-distributed, multi-model」「virtually unlimited elastic scalability」</td>
      </tr>
  </tbody>
</table>
<p>關鍵敘述：「The team decided to replace MongoDB with Azure Cosmos DB, a fully managed globally-distributed, multi-model database service designed for global distribution and virtually unlimited elastic scalability.」</p>
<h2 id="判讀">判讀</h2>
<p>Microsoft 365 案例揭露三個全球 SaaS 分析平台的工程重點。</p>
<ol>
<li><strong>MongoDB → Cosmos DB 是「相容 API + 升級擴展性」的遷移路徑</strong>：Cosmos DB 提供 MongoDB API 相容、應用層程式幾乎不用改、但底層儲存改用 Cosmos DB 的分散式架構。這層遷移成本遠低於改寫 application 到 native Cosmos DB SQL API、適合大規模既有系統。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a>、跟 <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> 形成對照。</li>
<li><strong>分析平台 vs 交易平台的 DB 取捨不同</strong>：交易平台優先 latency + consistency（<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a>）、分析平台優先 throughput + global distribution + cost。Cosmos DB 5 個 consistency level 讓分析場景可以選 weakest（eventual / session），換最大 throughput。對應 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> 同思維。</li>
<li><strong>Microsoft 自家產品 dogfood Cosmos DB</strong>：跟 Amazon Prime Day 用自家 DynamoDB（<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1</a>）、Google 自家用 Spanner（<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10</a>）一樣 — 雲商旗艦 DB 都會用在自家旗艦產品。讀此類 dogfood 案例的權重應該高、因為「雲商自己賭身家」。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>案例 <em>沒有</em> 提具體 throughput、latency、cost 數字。Microsoft 內部數字通常不公開、跟 AWS / GCP 案例的數字密度差很多。</li>
<li>「MongoDB 不夠用」是行銷話術。實際是 <em>MongoDB 在某些 workload pattern 下不夠用</em>、不是普遍結論。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>MongoDB-compatible Cosmos DB 是大規模遷移的捷徑</strong>：應用層改動少、底層擴展性升級。但要驗證 <em>特定 query pattern</em> 在兩邊行為一致。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">01.3 schema migration rollout evidence</a> 的 dual-write 驗證。</li>
<li><strong>分析平台用 weakest acceptable consistency</strong>：session consistency 或 eventual consistency 通常夠用、能換到 3-10x throughput。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的一致性取捨。</li>
<li><strong>dogfood 是 vendor selection 的重要訊號</strong>：vendor 自家是否用在 production-critical workload、能告訴你「他們對自己服務的信任度」。</li>
<li><strong>Multi-model 是 Cosmos DB 的差異化價值</strong>：同一個服務可以用 SQL API / MongoDB API / Cassandra API / Gremlin / Table API、避免多個 DB 服務並存。</li>
</ol>
<p>跨平台等效：AWS DynamoDB（KV）+ DocumentDB（MongoDB-compatible）、GCP Firestore（document）+ Spanner（SQL）+ Bigtable（KV）— 各家用不同產品覆蓋 multi-model、Cosmos DB 是少數「單一產品支援多 model」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他 Cosmos DB 案例 → <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> / <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a></li>
<li>對照其他 dogfood 案例 → <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a> / <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想做 MongoDB-compatible 遷移 → <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a></li>
<li>想理解 multi-model 取捨 → <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> + <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a></li>
<li>想對比 Cosmos DB MongoDB API vs SQL API 的選型 → <a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a></li>
<li>想做 RU 成本模型與容量 sizing → <a href="/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/" data-link-title="Cosmos DB RU/s 成本模型 &#43; 容量規劃：RU 思維、payload、index、provisioned vs autoscale vs serverless" data-link-desc="從 CPU&#43;IOPS 思維轉到 RU 思維的學習曲線、依負載形狀選容量模式、payload &#43; index policy 對 RU 的影響、autoscale reactive 限制 — 從 ASOS Black Friday &#43; Minecraft Earth 1M RU/s 壓測切入">Cosmos DB RU 成本模型</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://azure.microsoft.com/en-us/blog/microsoft-365-boosts-usage-analytics-with-azure-cosmos-db/">Microsoft 365 boosts usage analytics with Azure Cosmos DB</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/a-technical-overview-of-azure-cosmos-db/">A technical overview of Azure Cosmos DB</a></li>
</ul>
]]></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>DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>售票網站開賣前一小時把 DynamoDB capacity 從 200 WCU 拉到 5000、心想「容量加 25 倍應該夠」。開賣瞬間還是看到 &lt;code>ThrottledRequests&lt;/code> 拉警報、CloudWatch 顯示總 capacity 才用了 1500 WCU。打開 partition-level metric 才看到某一個 partition 已經達到 1000 WCU 上限、其他 partition 閒置 — &lt;code>event_id&lt;/code> 當 PK、單一熱門場次把所有寫入集中到同一個 partition。Capacity 加再多都救不了，因為單 partition 上限是 1000 WCU / 3000 RCU、跟 table 總容量無關。這就是 hot partition 的本質：partition key 設計問題、不是 capacity 不夠。&lt;/p>
&lt;p>本文展開 partition key 反模式的識別、composite key / write sharding 兩種修法、mode × partition 在 provisioned / on-demand 下的不同表現、以及 9.C15 拓元 6750x IOPS 擴展案例的工程細節。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>、本篇不重複展開。Partition key 反模式是 &lt;em>已選 DynamoDB 後&lt;/em> 的 schema 修補議題；若 4 軸不成立、改回 SQL 比補 composite key 更合理。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>跨 vendor 可逆性對照 SSoT&lt;/strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（DynamoDB 走 backfill 到新 table、屬中度可逆）、跨 vendor 對照 SSoT 主寫位置在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &amp;#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸&lt;/a> + 對應的&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &amp;#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段&lt;/a>。本篇聚焦 DynamoDB 內部如何識別 partition key 反模式 + composite key / write sharding 修法、不重複跨 vendor 比較。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>售票網站開賣前一小時把 DynamoDB capacity 從 200 WCU 拉到 5000、心想「容量加 25 倍應該夠」。開賣瞬間還是看到 <code>ThrottledRequests</code> 拉警報、CloudWatch 顯示總 capacity 才用了 1500 WCU。打開 partition-level metric 才看到某一個 partition 已經達到 1000 WCU 上限、其他 partition 閒置 — <code>event_id</code> 當 PK、單一熱門場次把所有寫入集中到同一個 partition。Capacity 加再多都救不了，因為單 partition 上限是 1000 WCU / 3000 RCU、跟 table 總容量無關。這就是 hot partition 的本質：partition key 設計問題、不是 capacity 不夠。</p>
<p>本文展開 partition key 反模式的識別、composite key / write sharding 兩種修法、mode × partition 在 provisioned / on-demand 下的不同表現、以及 9.C15 拓元 6750x IOPS 擴展案例的工程細節。</p>
<blockquote>
<p><strong>DynamoDB 適用度前置判讀</strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>、本篇不重複展開。Partition key 反模式是 <em>已選 DynamoDB 後</em> 的 schema 修補議題；若 4 軸不成立、改回 SQL 比補 composite key 更合理。</p></blockquote>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（DynamoDB 走 backfill 到新 table、屬中度可逆）、跨 vendor 對照 SSoT 主寫位置在 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸</a> + 對應的<a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段</a>。本篇聚焦 DynamoDB 內部如何識別 partition key 反模式 + composite key / write sharding 修法、不重複跨 vendor 比較。</p></blockquote>
<h2 id="核心機制partition-上限是工程硬天花板">核心機制：partition 上限是工程硬天花板</h2>
<p>DynamoDB 把 capacity 抽象成 RCU / WCU、但底下仍是物理 partition。理解 partition 的 4 條硬規則：</p>
<ul>
<li><strong>單 partition 上限</strong>：3000 RCU、1000 WCU、10GB storage；超過任一個觸發 partition split</li>
<li><strong>總容量公式</strong>：<code>partition 數量 × 每 partition 上限</code>、partition 數量由 vendor 自動管理</li>
<li><strong>Adaptive Capacity</strong>：跨 partition 重新分配閒置容量、但 <em>單 partition 仍硬上限</em>；不解 single-key 集中</li>
<li><strong>Splitting on heat</strong>：vendor 偵測 hot partition 後自動 split、有分鐘級延遲；突發流量來不及 split 就先 throttle</li>
</ul>
<p><code>9.C5 Amazon Ads</code> 揭露同一 frame：「容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是工程天花板」。Amazon Ads 90M reads/sec 不是把單 partition 推到極限、是 <em>partition key 設計讓流量散到極多 partition</em>、每個 partition 都在合理區間。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a>。</p>
<h2 id="mode--partition-交叉判讀">Mode × Partition 交叉判讀</h2>
<p>Hot partition 在 capacity mode 不同下表現不同、但根因都是 schema。這是 single-table / partition-key / capacity-mode 三篇 deep article 的交叉軸 — mode 切換不解 partition 設計問題、partition 設計也不解 mode 選擇問題。</p>
<table>
  <thead>
      <tr>
          <th>表現面</th>
          <th>Provisioned 模式</th>
          <th>On-demand 模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Throttle 可見性</td>
          <td><code>WriteThrottleEvents</code> 立即可見、CloudWatch 直接抓</td>
          <td>不顯示 throttle event、表現為 <code>SuccessfulRequestLatency</code> p99 突然跳高</td>
      </tr>
      <tr>
          <td>Application 表現</td>
          <td><code>ProvisionedThroughputExceededException</code> 立即拋</td>
          <td>timeout / retry 加劇、看起來像「DynamoDB 變慢」</td>
      </tr>
      <tr>
          <td>工程誤判風險</td>
          <td>低（exception 明顯）</td>
          <td>高（latency spike 容易被誤判成網路 / 應用層 / 下游服務問題）</td>
      </tr>
      <tr>
          <td>解法</td>
          <td>改 PK schema（composite key / write sharding）</td>
          <td>改 PK schema（同左、不是切 mode）</td>
      </tr>
  </tbody>
</table>
<p><code>9.C15 Tixcraft</code> 警惕段明示這個 frame：「DynamoDB 寫入排隊本身就是隱性限流」— provisioned 看得到、on-demand 看不到，但都是同一個 schema 問題。</p>
<p><strong>核心 frame</strong>：on-demand 不是 partition key 設計的逃避路徑。看到 on-demand 模式 latency spike 但 throttle 為零，<em>第一個懷疑就是 hot partition</em>、不是網路或應用層。</p>
<p>跟 <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">on-demand-vs-provisioned</a> 共軸閱讀：本篇從 schema 視角切入、那篇從 mode 選擇視角切入、合起來才是完整判讀。</p>
<h2 id="修復流程">修復流程</h2>
<p>從 access pattern audit 到 composite key 設計的 5 步流程。</p>
<h4 id="step-1識別寫入集中的-logical-key">Step 1：識別寫入集中的 logical key</h4>
<p>審視 access pattern 表、抓出 <em>寫入集中</em> 的 key：</p>
<ul>
<li>單一 event / single user 寫入比例 &gt; 10%（如熱門場次售票、bot 帳號）</li>
<li>時間 bucket（<code>PK = date</code> / <code>PK = hour</code>）— 寫入永遠打當下 partition、舊 partition 閒置</li>
<li>少數枚舉值（<code>PK = status</code> / <code>PK = country</code> 但只有 5-10 個值）</li>
</ul>
<p><code>9.C15 Tixcraft</code> 揭露的具體場景：演唱會某一熱門場次的 <code>event_id</code> 為 PK、開賣瞬間 200K 用戶同時搶該場次、所有寫入集中到單一 partition。</p>
<h4 id="step-2選-shard-數">Step 2：選 shard 數</h4>
<p>把單一 logical key 切成 N 個物理 shard。N 的估算邏輯：</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">單 partition WCU 上限 = 1000
</span></span><span class="line"><span class="ln">2</span><span class="cl">留 20% buffer            = 800
</span></span><span class="line"><span class="ln">3</span><span class="cl">N = 單 logical key 預期峰值 WCU / 800（最小 shard 數）</span></span></code></pre></div><blockquote>
<p><strong>Scope warning</strong>：「shard 數 10-100」、「800 WCU 留 buffer」這些具體數字是通用工程估算、9.C15 case <em>沒有</em> 揭露 Tixcraft 用幾個 shard。case 揭露的是「composite key 分散」概念跟「IOPS 從 20 衝到 135K」的結果、不是具體 shard 數量。寫進你自己的設計時、shard 數依預期單 logical key 峰值估算、不要照搬本文數字。</p></blockquote>
<h4 id="step-3composite-key-設計random-shard">Step 3：composite key 設計（random shard）</h4>
<p><a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 把 logical key 加上 random suffix、把 hot logical 值分散到多個 partition：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">write_order</span><span class="p">(</span><span class="n">event_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">order_data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1"># 寫入端：random suffix 分散到 N shard</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">shard</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">N</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">event_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">sk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span><span class="n">Item</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="n">sk</span><span class="p">,</span> <span class="o">**</span><span class="n">order_data</span><span class="p">})</span></span></span></code></pre></div><p>讀取時 fan-out 到所有 shard：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">query_event_orders</span><span class="p">(</span><span class="n">event_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="n">shard</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">event_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">KeyConditionExpression</span><span class="o">=</span><span class="n">Key</span><span class="p">(</span><span class="s2">&#34;PK&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eq</span><span class="p">(</span><span class="n">pk</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">results</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">page</span><span class="p">[</span><span class="s2">&#34;Items&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="n">results</span></span></span></code></pre></div><h4 id="step-4calculated-shard讓同-user-仍可預測讀取">Step 4：calculated shard（讓同 user 仍可預測讀取）</h4>
<p>random shard 的代價是讀取要 fan-out N 次。當你需要「同 user 寫入分散、但讀取 <em>該 user</em> 自己的資料時不要 fan-out」、改用 calculated shard：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">hashlib</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">shard_for_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">h</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">md5</span><span class="p">(</span><span class="n">user_id</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span> <span class="o">%</span> <span class="n">n</span>
</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">def</span> <span class="nf">write_user_event</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">event_data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">shard</span> <span class="o">=</span> <span class="n">shard_for_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># 同一 user_id 永遠拿到同一 shard</span></span></span></code></pre></div><p>讀單一 user 只 query 一個 shard、讀全平台 user 才 fan-out N 個 shard。</p>
<p>選擇：</p>
<ul>
<li><strong>random shard</strong>：寫入完全均勻、但所有讀路徑都要 fan-out；適合 <em>flash-sale / 緩衝層</em>（讀路徑是後端慢消費、不在乎 fan-out latency）</li>
<li><strong>calculated shard</strong>：寫入按 hash 均勻、user-level 讀路徑單 shard；適合 <em>user-facing OLTP</em>（user 讀自己資料延遲敏感）</li>
</ul>
<h4 id="step-5驗證點">Step 5：驗證點</h4>
<ul>
<li>Contributor Insights 看 top-N PK 訪問是否平均分布</li>
<li>CloudWatch partition-level throttle = 0</li>
<li>Application 端 read fan-out latency 在預算內</li>
</ul>
<p><strong>Rollback boundary</strong>：composite key 寫入端可雙寫舊 + 新 key 一段時間（雙寫窗口）、application read 端 fallback 到舊 PK；不可逆動作只在「移除舊 key」階段。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production case 揭露的 5 個踩雷情境：</p>
<h4 id="case-1時間序-pk-集中">Case 1：時間序 PK 集中</h4>
<p><code>PK = date</code> 或 <code>PK = hour</code> — 寫入永遠打當下 partition、舊 partition 閒置。每日凌晨換 partition 時瞬間冷啟動、寫入 latency spike。修法：<code>date#shard</code> 把當下 partition 拆 N 個物理 shard、或改用 event-stream pattern（每個 event 獨立 ID 為 PK）。</p>
<h4 id="case-2bot-user-集中">Case 2：bot user 集中</h4>
<p>PK = <code>user_id</code>、某個 bot 帳號每秒寫 1000 次、單 user_id 達 1000 WCU 上限。修法：</p>
<ul>
<li>偵測高頻 user 後動態加 shard suffix（<code>user_id#shard0</code> … <code>user_id#shardN</code>）</li>
<li>或在 application 層 rate limit、不讓 bot 直接打 DynamoDB</li>
</ul>
<h4 id="case-3composite-key-但-read-端忘記-fan-out">Case 3：composite key 但 read 端忘記 fan-out</h4>
<p>寫入分散到 100 shard、讀取只 query 一個 shard、結果不完整。修法：讀取必須 N 次 query 並 application 端合併、或建反向 GSI（GSI PK = <code>event_id</code>、不加 shard suffix；但 GSI 自己也會 hot partition）。</p>
<h4 id="case-4shard-數選太多-read-fan-out-latency-爆">Case 4：shard 數選太多 read fan-out latency 爆</h4>
<p>N 過大時讀取 fan-out latency 從 5ms 變 200ms（具體數字隨網路延遲跟並行度變動、9.C15 case 未揭露 Tixcraft 用幾個 shard）。修法：shard 數依「單 logical key 預期峰值 / 800」估算、不是越多越好；read latency 跟寫入分散度是 trade-off。</p>
<h4 id="case-5on-demand-模式以為不會-hot-partition">Case 5：on-demand 模式以為不會 hot partition</h4>
<p>on-demand 仍受單 partition 1000 WCU 限制、只是 throttling 表現為 latency spike 而非 exception。team 看到「沒有 ThrottledRequests」就以為沒問題、實際 p99 已經從 5ms 跳到 50ms。修法：on-demand 不是 partition key 設計的逃避路徑、依然要做 composite key；觀測上看 <code>SuccessfulRequestLatency</code> p99 不只看 throttle。跟 <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">on-demand-vs-provisioned</a> 共軸閱讀。</p>
<p><strong>Anti-recommendation</strong>：access pattern 寫入分散自然均勻（如 UUID 為 PK、無 logical hot key），不要預先 sharding；增加 read 端 fan-out 複雜度沒帶來收益。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>WriteThrottleEvents</code> / <code>ReadThrottleEvents</code>：按 table 跟 GSI 分；provisioned 模式直接訊號</li>
<li><code>SuccessfulRequestLatency</code> p99：on-demand 模式下 hot partition 的訊號（throttle 為零但 latency 跳高）</li>
<li>partition-level metric 透過 Contributor Insights 看，不是 CloudWatch 預設 panel</li>
</ul>
<p><strong>Contributor Insights 必開</strong>：top-N partition key by access frequency；每月 cost ~$0.02 per million event、值得開。沒開 Contributor Insights 你看不到 partition-level 分布、只能從總 capacity 跟 throttle 反推。</p>
<p>DynamoDB Streams：可用來抓 hot key debugging — 寫入事件落 Lambda 後統計 PK 頻率。</p>
<p><strong>Mode × partition 觀測差異</strong>（重申交叉判讀）：</p>
<ul>
<li>Provisioned 模式：看 <code>WriteThrottleEvents</code>、立即可見</li>
<li>On-demand 模式：看 <code>SuccessfulRequestLatency</code> p99、看 partition-level Contributor Insights、看 application 端 timeout / retry trend</li>
</ul>
<p>接回 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的 partition 章節。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="9c15-tixcraft-6750x-擴展的工程拆解">9.C15 Tixcraft 6750x 擴展的工程拆解</h3>
<p><code>9.C15 Tixcraft</code> 揭露的數字：IOPS 從 20 衝到 135K（6750 倍）、6 servers 變 800 servers、總成本 $4200、throttle rate 0.26%。但「6750x 擴展」不是 DynamoDB 自己的魔法、是 <em>partition key 均勻分散 + 架構解耦</em> 的組合結果：</p>
<ul>
<li><strong>partition key 均勻</strong>：composite key（<code>event_id</code> 加分散 suffix）把單一熱門場次散到多個 partition、每個 partition 都在合理區間（case 揭露概念、未揭露具體 shard 數）</li>
<li><strong>架構解耦</strong>：DynamoDB 當 durable queue、後端傳統 server（金流 / 票庫）用自己節奏消費、不被前端 130x 流量拖垮（見 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> 的 durable queue 段）</li>
<li><strong>付款層獨立</strong>：付款不是 DynamoDB、是另一層獨立服務、避免搶票流量影響付款</li>
</ul>
<p>讀者該學的不是「DynamoDB 能撐 6750x」、是「composite key + 架構解耦 + 服務分層」三件事一起做才能撐。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — PK 設計上游、本篇是 PK 不天然均勻時的補救</li>
<li><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">on-demand-vs-provisioned</a> — capacity mode 對 hot partition 表現的影響、mode × partition 交叉判讀的另一視角</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — GSI 自己也會 hot partition、GSI PK 設計獨立 review</li>
<li>Migration playbook：composite key migration 屬「topology re-layout」、寫入需雙軌；對應 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook methodology</a></li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 9.C15</a> 互引：售票模式的 6750x 擴展細節、composite key 是工程選擇而非 vendor 魔法</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9.C5</a> 互引：容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是容量天花板</li>
<li>跟 <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 9.C29</a> 互引：connection-free scale 的另一面是 partition 設計責任</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-key-selection/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-key-selection/</guid><description>&lt;p>MongoDB shard key 是 sharded cluster 上線時最難回頭的決策。Shard key 一旦設定錯、5.0 之前完全不可逆、5.0+ 用 &lt;code>reshardCollection&lt;/code> 可改但仍是長時間運算 + 額外磁碟 + 寫入暫停窗口。但 shard key 不是 production 唯一的橫向擴展選項 — 還有「多 cluster」這條路徑（Toyota Connected 揭露），兩者解的問題完全不同。本文把 shard key 三特性（cardinality / frequency / monotonicity）跟「單 cluster vs 多 cluster」對照在一起、配合跨 vendor partition key 可逆性紀律一起討論。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 sharding 簡介 — 而是 production 設計 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>MongoDB 適用度前置判讀&lt;/strong>：進到 shard key 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>、本篇不重複展開。Sharded cluster 是 &lt;em>已選 MongoDB 後&lt;/em> 的容量決策、不是 vendor 選型決策。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境橫向擴展不是只有-sharded-cluster-一條路">問題情境：橫向擴展不是只有 sharded cluster 一條路&lt;/h2>
&lt;p>典型觸發場景：single replica set 撐到上限、writes 已經把 primary 推到 CPU 90% / disk IO 飽和、working set 超出 RAM。讀者下意識會想到「分 shard」、但同時還有「分 cluster」這條路徑、兩者 trigger 完全不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>單 cluster 切 shard&lt;/strong>：解的是 &lt;em>單一資料域寫入飽和&lt;/em>、collection 大到單 replica set 撐不住&lt;/li>
&lt;li>&lt;strong>多 cluster 切 DB&lt;/strong>：解的是 &lt;em>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> / ownership / 合規邊界&lt;/em>、不一定是吞吐問題&lt;/li>
&lt;/ul>
&lt;p>混淆兩者的後果：吞吐沒撞牆但 blast radius 是議題、強行分 shard → aggregation / transaction / &lt;code>$lookup&lt;/code> 成本全部跳一級、業務 ownership 仍混在一起。或反過來：吞吐撞牆但選了分 cluster → 跨 cluster transaction 不存在、單一 collection 跨多 cluster 要在 application 層拼。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>&lt;code>mongos&lt;/code> 的 &lt;code>targeted query / scatter-gather query&lt;/code> 比例失衡&lt;/li>
&lt;li>單一 shard CPU 遠高其他 shard、balancer 移 chunk 跟不上寫入速度&lt;/li>
&lt;li>&lt;code>chunkMigrated&lt;/code> 異常頻繁、&lt;code>sh.status()&lt;/code> 顯示 chunk 分布偏斜&lt;/li>
&lt;li>微服務 ownership 跟 collection 邊界不對齊、某 microservice 故障打到其他服務&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 揭露「20 個 Atlas database 是業務邊界切分、不是吞吐切分」（單 cluster vs 多 cluster 對照）；hot shard 在 e-commerce flash sale / 遊戲開新區 / B2B 大客戶獨佔 chunk 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理、不憑空編造 incident 數字。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB shard key 是 sharded cluster 上線時最難回頭的決策。Shard key 一旦設定錯、5.0 之前完全不可逆、5.0+ 用 <code>reshardCollection</code> 可改但仍是長時間運算 + 額外磁碟 + 寫入暫停窗口。但 shard key 不是 production 唯一的橫向擴展選項 — 還有「多 cluster」這條路徑（Toyota Connected 揭露），兩者解的問題完全不同。本文把 shard key 三特性（cardinality / frequency / monotonicity）跟「單 cluster vs 多 cluster」對照在一起、配合跨 vendor partition key 可逆性紀律一起討論。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 sharding 簡介 — 而是 production 設計 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>MongoDB 適用度前置判讀</strong>：進到 shard key 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>、本篇不重複展開。Sharded cluster 是 <em>已選 MongoDB 後</em> 的容量決策、不是 vendor 選型決策。</p></blockquote>
<h2 id="問題情境橫向擴展不是只有-sharded-cluster-一條路">問題情境：橫向擴展不是只有 sharded cluster 一條路</h2>
<p>典型觸發場景：single replica set 撐到上限、writes 已經把 primary 推到 CPU 90% / disk IO 飽和、working set 超出 RAM。讀者下意識會想到「分 shard」、但同時還有「分 cluster」這條路徑、兩者 trigger 完全不同：</p>
<ul>
<li><strong>單 cluster 切 shard</strong>：解的是 <em>單一資料域寫入飽和</em>、collection 大到單 replica set 撐不住</li>
<li><strong>多 cluster 切 DB</strong>：解的是 <em><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> / ownership / 合規邊界</em>、不一定是吞吐問題</li>
</ul>
<p>混淆兩者的後果：吞吐沒撞牆但 blast radius 是議題、強行分 shard → aggregation / transaction / <code>$lookup</code> 成本全部跳一級、業務 ownership 仍混在一起。或反過來：吞吐撞牆但選了分 cluster → 跨 cluster transaction 不存在、單一 collection 跨多 cluster 要在 application 層拼。</p>
<p>讀者徵兆：</p>
<ul>
<li><code>mongos</code> 的 <code>targeted query / scatter-gather query</code> 比例失衡</li>
<li>單一 shard CPU 遠高其他 shard、balancer 移 chunk 跟不上寫入速度</li>
<li><code>chunkMigrated</code> 異常頻繁、<code>sh.status()</code> 顯示 chunk 分布偏斜</li>
<li>微服務 ownership 跟 collection 邊界不對齊、某 microservice 故障打到其他服務</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露「20 個 Atlas database 是業務邊界切分、不是吞吐切分」（單 cluster vs 多 cluster 對照）；hot shard 在 e-commerce flash sale / 遊戲開新區 / B2B 大客戶獨佔 chunk 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理、不憑空編造 incident 數字。</p>
<h2 id="核心機制shard-keychunkbalancer">核心機制：shard key、chunk、balancer</h2>
<p>Shard key 三特性決定 sharded cluster 行為：</p>
<ul>
<li><strong>Cardinality（基數）</strong>：shard key 的不同值數量。<code>status: &quot;active&quot; | &quot;inactive&quot;</code> 只有兩個值、cardinality = 2、不能分到多 chunk</li>
<li><strong>Frequency（頻率分布）</strong>：值的分布是否平均。<code>country</code> 在全球流量中通常一兩個國家佔 80%</li>
<li><strong>Monotonicity（單調性）</strong>：值是否單調遞增。<code>_id</code>（ObjectId）/ 時間戳 / 自增 ID 都是單調</li>
</ul>
<p>三特性決定 shard key 行為：</p>
<ul>
<li><strong>Hashed shard key</strong>：hash function 把 key 打散、寫入分布均勻、但 range query 變 scatter-gather（每個 shard 都問）</li>
<li><strong>Ranged shard key</strong>：相同 key 相近 → 同 chunk → range query 高效；但單調 key + ranged → 所有寫打最後 chunk</li>
<li><strong>Compound shard key</strong>（5.0+ 是常用做法、對應 <a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 的 MongoDB 實作）：例如 <code>{ tenantId: 1, _id: &quot;hashed&quot; }</code> — 先 tenant 隔離、再 hash 避免 tenant 內熱點</li>
<li><strong>Zone sharding</strong>：把特定 chunk 釘到特定 shard（地域 / 合規 / 硬體分層）</li>
</ul>
<p>Chunk 是 MongoDB 在 collection 上劃出的 64MB（預設）邏輯區塊。Balancer 在 shard 間搬 chunk 達成均衡。<strong>Chunk 不可 split 的條件</strong>是 shard key 在該範圍只有一個值（low cardinality / 大 tenant 獨佔範圍）— chunk split 不了、balancer 也搬不開。</p>
<p><code>reshardCollection</code>（4.4+）：透過 temporary collection + chunk 重切 + 雙寫 + cutover、耗時等比於資料量、需額外 ~1.2x 磁碟。是「設計錯了還有補救機會」但不是 free lunch。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>。</p>
<h3 id="單-cluster-切-shard-vs-多-cluster-切-blast-radius">單 cluster 切 shard vs 多 cluster 切 blast radius</h3>
<p>跨案合成 frame（本章合成、9.C38 Toyota 揭露事實但 case 原文沒提這個 frame）：橫向擴展不是只有「sharded cluster 一條路」、多 cluster 是另一條路。</p>
<p>9.C38 Toyota Connected 揭露事實：</p>
<ul>
<li>18B transactions / 月 ÷ 30 天 ÷ 86400 秒 ≈ 7K txn/sec（口徑：月度滾動平均、非瞬時尖峰）</li>
<li>單一 MongoDB cluster 完全撐得下這個吞吐</li>
<li>Toyota 切 20 個 Atlas database <strong>不是吞吐切分</strong>、是 <em>microservice ownership</em> + <em>blast radius</em> 切分</li>
<li>「每個 microservice 擁有自己的 DB、單一 DB 故障不影響其他服務」</li>
</ul>
<p>兩條路徑的判讀條件不同：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>Trigger</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sharded cluster（分 shard）</td>
          <td>單一 collection 寫入飽和、storage 撐爆單 replica set、access pattern 在同一個資料域內</td>
          <td>aggregation / transaction / <code>$lookup</code> 成本全部跳一級</td>
      </tr>
      <tr>
          <td>多 cluster（分 DB）</td>
          <td>微服務 ownership 邊界、blast radius 隔離、合規 boundary、不同 workload shape 共處風險</td>
          <td>跨 cluster transaction 不存在、跨 DB join 必須在 application 層做</td>
      </tr>
  </tbody>
</table>
<p>兩者可以同時用：每個 microservice 有獨立 cluster、cluster 內部該分 shard 還是分。寫設計文件時要避免讓讀者以為「sharded cluster 是唯一橫向擴展選項」。</p>
<h3 id="partition-key-可逆性跨-vendor-對照">Partition key 可逆性跨 vendor 對照</h3>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家可逆性不在同一光譜、跨 vendor 對照的 SSoT 主寫位置在 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸</a> + 對應的<a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段</a>。本段聚焦 MongoDB 5.0+ <code>reshardCollection</code> 對 shard key 設計的影響、不重複展開三 vendor 全光譜比較。</p></blockquote>
<p>不同 vendor 對 partition key 可逆性紀律完全不在同一光譜：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>機制</th>
          <th>可逆性</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB</td>
          <td>Shard key（<code>shardCollection</code>）</td>
          <td>4.4+ <code>reshardCollection</code> 可改、5.0 前完全不可逆</td>
          <td>等比資料量、~1.2x 磁碟、雙寫 + cutover</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>Partition key</td>
          <td>可改（用 backfill 到新 table）</td>
          <td>重設計 access pattern、流量切換成本</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td>Partition key</td>
          <td>不可改（必須 export-recreate-import）</td>
          <td>全量重灌、雙寫驗證、最大遷移成本</td>
      </tr>
  </tbody>
</table>
<p>寫進設計文件時必須附 vendor + 版本、避免讓讀者把三家當「partition key 都不可改」、也避免把 MongoDB 5.0+ 的 <code>reshardCollection</code> 當「便宜遷移」。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：横向擴展路徑決策</strong>。先問「我要解的是 <em>單一資料域寫入飽和</em> 還是 <em>blast radius / ownership</em>」、選分 shard 或分 cluster。若兩者都要、決定 cluster 邊界後再在 cluster 內分 shard。</p>
<p><strong>Step 2：access pattern audit</strong>。列出所有讀寫 query、標出哪些 query 必須走 single shard（targeted），哪些 query 不在意 scatter-gather。</p>
<p><strong>Step 3：候選 key 評估表</strong>。對每個候選打 cardinality / frequency / monotonicity 三項評分：</p>
<table>
  <thead>
      <tr>
          <th>候選 key</th>
          <th>Cardinality</th>
          <th>Frequency</th>
          <th>Monotonicity</th>
          <th>適合？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_id</code>（ObjectId）</td>
          <td>極高</td>
          <td>均勻</td>
          <td>單調</td>
          <td>否（單調寫熱）</td>
      </tr>
      <tr>
          <td><code>tenantId</code></td>
          <td>中</td>
          <td>偏斜</td>
          <td>否</td>
          <td>視 tenant 分布</td>
      </tr>
      <tr>
          <td><code>{ tenantId: 1, _id: &quot;hashed&quot; }</code></td>
          <td>高</td>
          <td>均勻</td>
          <td>否</td>
          <td>通常合適</td>
      </tr>
      <tr>
          <td><code>country</code></td>
          <td>極低（~200）</td>
          <td>嚴重偏斜</td>
          <td>否</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 4：dry-run 採樣</strong>。對既有資料採樣，跑 <code>db.coll.aggregate([{$sample:{size:100000}}, {$group:{_id:&quot;$candidateKey&quot;, c:{$sum:1}}}, {$sort:{c:-1}}])</code> 看分布、確認沒有單一 key value 吃掉 &gt; 20% 流量。</p>
<p><strong>Step 5：shardCollection</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">enableSharding</span><span class="p">(</span><span class="s2">&#34;shop&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">shardCollection</span><span class="p">(</span><span class="s2">&#34;shop.orders&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">tenantId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;hashed&#34;</span> <span class="p">})</span></span></span></code></pre></div><p>先在 staging 跑流量重放、確認 chunk 分布平均、targeted query 比例 &gt; 90%。</p>
<p><strong>Step 6：監控</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">()</span>                              <span class="c1">// 看 cluster 狀態
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">getShardDistribution</span><span class="p">()</span>         <span class="c1">// 看 chunk 分布
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">adminCommand</span><span class="p">({</span> <span class="nx">balancerStatus</span><span class="o">:</span> <span class="mi">1</span> <span class="p">})</span>   <span class="c1">// 看 balancer 狀態
</span></span></span></code></pre></div><p><strong>Step 7：若已上錯 key</strong>。評估 <code>reshardCollection</code>（4.4+）vs application-level 雙寫遷移：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">adminCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">reshardCollection</span><span class="o">:</span> <span class="s2">&#34;shop.orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">key</span><span class="o">:</span> <span class="p">{</span> <span class="nx">tenantId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">region</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;hashed&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><code>reshardCollection</code> 進入 cutover 後不能回退、必須 dry-run 估完時間 + 磁碟 + IO 影響再上。</p>
<p>驗證點：targeted query 比例 &gt; 90%、單 shard QPS 變異係數 &lt; 20%、balancer migration 速率追上寫入速率。</p>
<p>Rollback boundary：<code>shardCollection</code> 是不可逆操作（5.0 前完全不可逆、5.0+ 透過 reshardCollection 可改但需重做）；<code>reshardCollection</code> 進入 cutover 後不能回退。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>單調 key 寫熱點</strong>：<code>_id</code>（ObjectId）/ 時間戳 / 自增 ID 當 ranged shard key → 所有寫進最後 chunk，scale-out 等於零。修法是 hashed key 或 compound key 把單調軸拌散。</p>
<p><strong>低 cardinality key</strong>：用 <code>country</code> 當 shard key、某個 country 佔 80% 流量、chunk 無法繼續 split、該 shard 永久熱。修法是加一個高 cardinality 軸（compound key）讓 chunk 可繼續分。</p>
<p><strong>Tenant skew</strong>：B2B 場景大客戶獨佔 chunk、且該 tenant 的 chunk 還會繼續長大、balancer 搬不走。修法 compound key <code>{ tenantId: 1, _id: &quot;hashed&quot; }</code> — tenant 隔離但 tenant 內 hash 散開。</p>
<p><strong>Scatter-gather 過多</strong>：選了 hashed <code>_id</code> 但業務查詢主要是 <code>tenantId</code> 範圍查、每筆 query 打所有 shard、p99 隨 shard 數線性退化。修法 compound key 把常用查詢軸放第一位、targeted query 才能對 single shard。</p>
<p><strong>Resharding 卡在 build 階段</strong>：磁碟不夠（需 1.2x source size）、IO 飽和影響線上 workload、預期 4 小時實際跑 14 小時。修法是先擴磁碟、staging 跑 dry-run 量實際耗時、production 在低峰期啟動。</p>
<p><strong>Zone sharding 規則打架</strong>：合規規則（資料必須留在某 region）跟負載平衡規則衝突、balancer 無法移動 chunk → 熱點固化。修法是 zone 規則 vs balancer 設計階段就劃清、不要事後加 zone。</p>
<p><strong>誤把多 cluster 當分 shard 解</strong>：blast radius 議題塞到 sharded cluster、單 cluster 故障仍打掉全部 microservice。該分 cluster 的就分 cluster、不是塞到 shard。9.C38 Toyota 揭露：7K txn/sec 仍切 20 DB 的 trigger 是 microservice ownership、不是吞吐。</p>
<p><strong>Cluster 擴容時間估計太樂觀</strong>：MongoDB cluster 擴容是天級議題、不是 console 點點就好。9.C36 Coinbase 揭露 cluster 擴容要 70 分鐘（口徑：Coinbase 特定環境 cluster tier / 資料量 / Atlas API 條件下、reactive scaling 起點到完成、非 MongoDB 普遍承諾）；預測性流量必須走 predictive / scheduled scaling、不能只靠 sharded cluster 動態橫向擴展接住 surge（見 <a href="../connection-management-and-cache-layer/">connection management and cache layer</a>）。</p>
<p>Anti-recommendation：</p>
<ul>
<li>寫入 &lt; 5K WPS、storage &lt; 1TB、single replica set 還能撐就不該分 shard；分了之後 aggregation、transaction、<code>$lookup</code>、index 成本全部跳一級</li>
<li><strong>shard vs 多 cluster 對照</strong>：吞吐沒撞牆但 blast radius / ownership 是議題、走多 cluster 不是強行分 shard（9.C38 Toyota 7K txn/sec 仍切 20 DB 的 trigger）</li>
<li>跨 case 合成 frame：「不是所有資料都該進同一個 MongoDB cluster」、按 microservice ownership / blast radius / 合規邊界切</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Shard 分布健康</strong>：每 shard QPS / CPU / disk usage 變異係數（&lt; 20% 合理）</li>
<li><strong>Query 路由</strong>：targeted vs scatter-gather query 比例（targeted &gt; 90% 合理）</li>
<li><strong>Balancer 健康</strong>：chunk migration rate、balancer round duration</li>
<li><strong>Cluster 邊界</strong>：cluster-to-cluster ownership 邊界、跨 cluster query 比例</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>sh.status()</code>：cluster 整體狀態</li>
<li><code>db.coll.getShardDistribution()</code>：collection 在各 shard 的分布</li>
<li><code>db.adminCommand({balancerStatus:1})</code>：balancer 狀態</li>
<li><code>db.serverStatus().sharding</code>：sharding metric</li>
</ul>
<p><code>mongos</code> profiler：每 query 帶 <code>executionStats.executionStages.shards[]</code>、看是否 single shard。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：把 shard distribution、targeted ratio、resharding 進度列為 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 saturation discovery</a>：hot shard 是 partition-level saturation 的典型例子。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：當整 cluster CPU 看似只用 25%、實際是 1/4 shard 在 100%。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../schema-design-pattern/">schema design pattern</a> — document 形狀決定 shard key 選擇空間</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — cross-shard aggregation 的 <code>$out</code> / <code>$merge</code> 限制</li>
<li><a href="../change-streams-kafka/">change streams + Kafka</a> — cluster-wide vs collection-level change stream 在 sharded cluster 的差異</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — cluster 擴容時間是天級議題、必須跟 predictive scaling / proxy 層配合</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>避免自管 sharding 走 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas</a> 用 managed shard tier</li>
<li>徹底重新分區走 <a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">shard expansion + multi-DC</a></li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 把 shard key 列為 capacity 決策；<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 收 resharding 失敗 retrospective。</p>
<p>跨 vendor 對照：<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 vendor page</a>（partition key + adaptive capacity + backfill 可改）、<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor page</a>（partition key 不可改）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「shard key 選型」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — 20 個 Atlas DB 切 blast radius</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — cluster 擴容 70 分鐘特定環境數字</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/sharding/">MongoDB Sharding</a>、<a href="https://www.mongodb.com/docs/manual/core/sharding-shard-key/">Choosing a Shard Key</a>、<a href="https://www.mongodb.com/docs/manual/core/sharding-reshard-a-collection/">Resharding</a></li>
</ul>
]]></content:encoded></item><item><title>Spanner Consistency Models 對照：external consistency vs serializability vs linearizability</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/consistency-models-comparison/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/consistency-models-comparison/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 concept-layer deep article。Overview 已說明 Spanner 在強一致 SQL 譜系的定位、本文聚焦 &lt;em>consistency model&lt;/em> — 三個常被混用的概念（external consistency / serializability / linearizability）的精確差異、line-rate scaling 對照、跟 cross-region quorum 的物理硬限。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境五個詞混用的選型困境">問題情境：五個詞混用的選型困境&lt;/h2>
&lt;p>團隊在 Spanner / CockroachDB / Aurora DSQL 之間選型、看文件講 strict serializability、external consistency、linearizable、snapshot isolation、serializable — 五個詞混用、不確定買的是哪一種保證。讀者徵兆通常是「我們需要強一致」但說不出強到哪、把 serializable transaction 跟 linearizable read 當同一件事、debug 對帳時發現「兩個 transaction 都 commit 成功、順序卻違反 user 體感」。&lt;/p>
&lt;p>真實壓力場景：金融帳本 — A 在台北轉帳給 B、B 在東京立即收到通知然後查餘額、結果查到「轉帳前」的餘額。serializable 允許這種行為（兩 transaction 可以排成任意順序、不要求跟 wall clock 一致）、external consistency 不允許（必須等 commit 後的順序符合 real-time）。混用兩個詞會讓選型結論在系統實作後才被推翻、那時候改架構成本已經高了。&lt;/p>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale&lt;/a> — Google Ads 計費需要 external consistency；對照 PostgreSQL SSI、CockroachDB HLC、Aurora DSQL。&lt;strong>dogfood 邊界明示&lt;/strong>：9.C10 是 Google 內部 dogfood case、不是 customer-facing capacity 參考；本文引用其 line-rate scaling 數字時要附「Google internal dogfood 揭露的設計目標、不是客戶 SLA」邊界。&lt;/p>
&lt;h2 id="三個概念的精確定義">三個概念的精確定義&lt;/h2>
&lt;h3 id="serializability">Serializability&lt;/h3>
&lt;p>transaction 的執行結果等同於 &lt;em>某個&lt;/em> 序列順序執行；不要求順序跟 real-time 一致。PostgreSQL SERIALIZABLE isolation level（SSI 實作）給的就是這個保證。它解決的問題是 &lt;em>concurrent transaction 之間互相干擾的 anomaly&lt;/em>（dirty read / lost update / write skew / G2-item）、不解決「跨 transaction 的 wall-clock 順序」。&lt;/p>
&lt;p>範例：A 在 10:00:00 commit T1（餘額 +100）、B 在 10:00:01 commit T2（查餘額）。serializable 允許系統把 T2 排在 T1 之前、B 看到舊餘額 — 兩 transaction 都成功、isolation 沒被破壞、但用戶體感違反順序。&lt;/p>
&lt;h3 id="linearizability">Linearizability&lt;/h3>
&lt;p>單一 object 操作有全序、且全序跟 real-time wall-clock 一致。只談 single-object、不談跨 object transaction。DynamoDB strongly consistent read 是 single-item linearizability、Redis &lt;code>INCR&lt;/code> 是 single-key linearizability。對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability&lt;/a> 卡。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 concept-layer deep article。Overview 已說明 Spanner 在強一致 SQL 譜系的定位、本文聚焦 <em>consistency model</em> — 三個常被混用的概念（external consistency / serializability / linearizability）的精確差異、line-rate scaling 對照、跟 cross-region quorum 的物理硬限。</p></blockquote>
<hr>
<h2 id="問題情境五個詞混用的選型困境">問題情境：五個詞混用的選型困境</h2>
<p>團隊在 Spanner / CockroachDB / Aurora DSQL 之間選型、看文件講 strict serializability、external consistency、linearizable、snapshot isolation、serializable — 五個詞混用、不確定買的是哪一種保證。讀者徵兆通常是「我們需要強一致」但說不出強到哪、把 serializable transaction 跟 linearizable read 當同一件事、debug 對帳時發現「兩個 transaction 都 commit 成功、順序卻違反 user 體感」。</p>
<p>真實壓力場景：金融帳本 — A 在台北轉帳給 B、B 在東京立即收到通知然後查餘額、結果查到「轉帳前」的餘額。serializable 允許這種行為（兩 transaction 可以排成任意順序、不要求跟 wall clock 一致）、external consistency 不允許（必須等 commit 後的順序符合 real-time）。混用兩個詞會讓選型結論在系統實作後才被推翻、那時候改架構成本已經高了。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale</a> — Google Ads 計費需要 external consistency；對照 PostgreSQL SSI、CockroachDB HLC、Aurora DSQL。<strong>dogfood 邊界明示</strong>：9.C10 是 Google 內部 dogfood case、不是 customer-facing capacity 參考；本文引用其 line-rate scaling 數字時要附「Google internal dogfood 揭露的設計目標、不是客戶 SLA」邊界。</p>
<h2 id="三個概念的精確定義">三個概念的精確定義</h2>
<h3 id="serializability">Serializability</h3>
<p>transaction 的執行結果等同於 <em>某個</em> 序列順序執行；不要求順序跟 real-time 一致。PostgreSQL SERIALIZABLE isolation level（SSI 實作）給的就是這個保證。它解決的問題是 <em>concurrent transaction 之間互相干擾的 anomaly</em>（dirty read / lost update / write skew / G2-item）、不解決「跨 transaction 的 wall-clock 順序」。</p>
<p>範例：A 在 10:00:00 commit T1（餘額 +100）、B 在 10:00:01 commit T2（查餘額）。serializable 允許系統把 T2 排在 T1 之前、B 看到舊餘額 — 兩 transaction 都成功、isolation 沒被破壞、但用戶體感違反順序。</p>
<h3 id="linearizability">Linearizability</h3>
<p>單一 object 操作有全序、且全序跟 real-time wall-clock 一致。只談 single-object、不談跨 object transaction。DynamoDB strongly consistent read 是 single-item linearizability、Redis <code>INCR</code> 是 single-key linearizability。對應 <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> 卡。</p>
<p>linearizability 跟 serializability 是 <em>正交</em> 的兩個概念 — linearizability 講「單一 object 的 real-time 順序」、serializability 講「transaction 的 anomaly-free 執行」。一個系統可以是 linearizable 但不 serializable（單 object 強保證、跨 object transaction 沒有）、也可以是 serializable 但不 linearizable（PostgreSQL SSI single-node 在 replica lag 後就不 linearizable）。</p>
<h3 id="external-consistency--strict-serializability">External consistency / Strict serializability</h3>
<p>transaction 層級的 serializability + 全序跟 real-time 一致 — 等同於把 linearizability 推廣到 multi-object transaction。Spanner 用 TrueTime + commit wait 實作、保證 commit timestamp 順序 = real-time 順序。對應 <a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external-consistency</a> 卡。</p>
<p>回到金融帳本例：external consistency 不允許 T2 排在 T1 之前、因為 T2 的 transaction timestamp 必須大於 T1 的 commit timestamp、用戶查餘額必看到 +100 後的金額。</p>
<h2 id="line-rate-scaling-對照為什麼-pg-serializable-在-multi-node-拿不到-line-rate">Line-rate scaling 對照：為什麼 PG serializable 在 multi-node 拿不到 line-rate</h2>
<p>這段的核心責任是回答「為什麼 Spanner 不只是『更強的 serializable』、是『coordinator 換拓樸』的 paradigm shift」、扣 <a href="../truetime-api-depth/">truetime-api-depth</a> 的商業邏輯先行 frame。讀者選 consistency 等級時、實際在選「系統的 scaling 路徑」、不只是「應用層 anomaly 哪些被排除」。</p>
<h3 id="9c10-揭露的線性擴展數字">9.C10 揭露的線性擴展數字</h3>
<p>「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」這條線性 scaling 揭露 Spanner external consistency 不是「加強版 serializable」、是把跨節點 coordinator 從 single-point 換成「拓樸感知的多 leader（每個 split 自己的 Paxos group）」、所以擴 node 數可以線性拿 throughput。</p>
<p><strong>Dogfood 邊界明示</strong>：9.C10 數字是 Google internal dogfood、不是 customer-facing capacity 承諾。客戶能拿到的 line-rate 受 instance config、region layout、workload shape 影響、不會自動複製 Google 內部曲線。</p>
<h3 id="對照表四個系統的-scaling-路徑">對照表：四個系統的 scaling 路徑</h3>
<table>
  <thead>
      <tr>
          <th>系統</th>
          <th>Isolation / Consistency 等級</th>
          <th>Multi-node scaling 路徑</th>
          <th>為什麼撞天花板（或不撞）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PostgreSQL SSI</td>
          <td>Serializable</td>
          <td>single-primary + read replica</td>
          <td>寫只能 single primary、跨節點交易要 2PC + coordinator、replica 寫不了；scaling 路徑停在 single-primary 容量上限</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>Serializable + per-key linearizable</td>
          <td>range-based + HLC</td>
          <td>range coordinator 仍存在、但 range 拆細了；retry contract 接住跨 range conflict、扣 serializable restart cost</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>External consistency</td>
          <td>split-based + Paxos + TrueTime</td>
          <td>coordinator 變多 leader、TrueTime 對齊 commit 順序、線性擴展是設計目標（9.C10 揭露 dogfood 線性模式）</td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>Strong consistency（2024 推出）</td>
          <td>文件未完全公開、查最新 docs</td>
          <td>時間敏感 claim、本文不擴寫；讀者實作前查官方文件確認最新 scaling 模型</td>
      </tr>
  </tbody>
</table>
<p>每個欄位都要回到具體的 scaling 機制讀。PostgreSQL SSI 跟「single-primary」綁定 — 想 scale write 只能 sharding；CockroachDB 把 range 拆細、coordinator 分布到 range 層、但跨 range conflict 還是會 trigger retry；Spanner 用 Paxos group per split、commit timestamp 用 TrueTime 對齊、不需要全局 coordinator 來決定順序；Aurora DSQL 是新系統、機制細節隨版本演進。</p>
<h3 id="為什麼這個對照寫進-consistency-文章不是純機制文章">為什麼這個對照寫進 consistency 文章、不是純機制文章</h3>
<p>讀者選 consistency 等級時、實際在選「系統的 scaling 路徑」、不只是「應用層 anomaly 哪些被排除」。external consistency 的 cost 包含 commit wait latency、但 benefit 包含 line-rate scaling — 兩者要一起講、不能拆開。把對照表放這裡、讓 consistency 跟 scaling 在同一段被讀者一起判讀、避免「我們需要強一致」這種需求被翻譯成「升級到 Spanner」這種跳號決策。</p>
<h2 id="cross-region-quorum-100-200ms-物理硬限強一致--全球不是免費">Cross-region quorum 100-200ms 物理硬限：強一致 + 全球不是免費</h2>
<p><a href="/blog/backend/knowledge-cards/cross-region-quorum/" data-link-title="Cross-Region Quorum" data-link-desc="multi-region distributed SQL 強制 voting replica 跨 region、commit 等多 region quorum ack、跨洲 RTT 物理硬限">Cross-Region Quorum</a> + external consistency + multi-region 不是「免費全球」、是「用 latency 換 consistency」。讀者若沒看到具體數量級、會誤把 Spanner 當作「強一致 + 全球 + 低延遲」的奇蹟、實際 cross-region write 在物理光速硬限下必須付跨洲 round-trip cost。</p>
<h3 id="9c10-揭露的數量級">9.C10 揭露的數量級</h3>
<p>「external consistency 必須等多區 quorum、跨洲交易延遲可達 100-200ms」 — 這是 9.C10 case 直接揭露的工程數字、不是本章 derive。<strong>Dogfood 邊界明示</strong>：9.C10 case 揭露的是 Google internal dogfood 觀察到的數量級、不是 SLA 承諾；實際客戶的 cross-region write latency 隨 voting region 配置、network path 變化。</p>
<h3 id="latency-拆解模型cross-region-write">Latency 拆解模型（cross-region write）</h3>





<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">total write latency ≈ 2ε（[Commit Wait](/backend/knowledge-cards/commit-wait/)、TrueTime ε 兩倍 ≈ 2-14ms）
</span></span><span class="line"><span class="ln">2</span><span class="cl">                    + quorum RTT across voting regions
</span></span><span class="line"><span class="ln">3</span><span class="cl">                       跨洲：50-100ms one-way、來回 100-200ms
</span></span><span class="line"><span class="ln">4</span><span class="cl">                       跨大陸內：10-30ms
</span></span><span class="line"><span class="ln">5</span><span class="cl">                       跨 zone（同 region）：&lt; 5ms
</span></span><span class="line"><span class="ln">6</span><span class="cl">                    + Spanner internal processing</span></span></code></pre></div><p>跨洲 quorum 在這個模型裡是 <em>dominant term</em>、不是 <a href="/blog/backend/knowledge-cards/commit-wait/" data-link-title="Commit Wait" data-link-desc="Spanner external consistency 的核心機制 — read-write transaction 拿 commit timestamp s 後等到 TT.after(s) 才 ACK、wait ≈ 2ε、付 latency tax 換 commit 順序 = real-time 順序">commit wait</a> — 判讀時要明示「commit wait 跟跨 region quorum 是兩個獨立的物理 cost、不能混用一個 latency 數字解釋兩者」。讀者常見的誤解是把 100-200ms 寫成「Spanner commit wait」、實際 commit wait 只是其中 2-14ms、剩下 100ms+ 是物理光速限定的 quorum RTT。</p>
<h3 id="scope-warning實際-latency-依-region-配置">Scope warning：實際 latency 依 region 配置</h3>
<p>100-200ms 是 9.C10 case 揭露的範圍、實際 latency 隨 voting region 配置變化：</p>
<table>
  <thead>
      <tr>
          <th>Instance config 類型</th>
          <th>Voting region 散布</th>
          <th>典型 write p99</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Regional（單 region 多 zone）</td>
          <td>同 region 內</td>
          <td>&lt; 10ms</td>
      </tr>
      <tr>
          <td>Dual-region（同大陸）</td>
          <td>跨大陸內</td>
          <td>20-50ms</td>
      </tr>
      <tr>
          <td>Multi-region（跨洲）</td>
          <td>跨大陸或跨洲</td>
          <td>100-200ms</td>
      </tr>
  </tbody>
</table>
<p>引用要附條件「跨洲多 region instance、實際數字依 region 配置」、不能寫成「Spanner cross-region write 一律 100-200ms」。讀者拿這條 latency anchor 做 capacity planning 時、必須先 audit 自家 instance 是哪種 config、不能套用 100-200ms 當基線。</p>
<h2 id="ssot-對齊strong--multi-region-互斥議題不在此處展開">SSoT 對齊：Strong + multi-region 互斥議題不在此處展開</h2>
<p>Strong consistency + multi-region 互斥議題（包含 Cosmos DB 5 levels 的 Strong + multi-region 限制）的 SSoT 是 <a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region-write-conflict</a>。本篇 cross-link 不展開、避免重複展開同議題。</p>
<p>本篇展開的子議題：</p>
<ul>
<li>external consistency / serializability / linearizability 的精確定義差異</li>
<li>Spanner external consistency 的 TrueTime 實作機制（細節在 <a href="../truetime-api-depth/">truetime-api-depth</a>）</li>
<li>cross-region quorum 的物理 cost 數量級</li>
<li>line-rate scaling 對照表（為什麼 single-primary 系統拿不到線性）</li>
</ul>
<p>兩個 SSoT 處理同一個讀者問題（強一致 vs multi-region）的不同切面 — 本篇從 <em>系統 scaling 路徑</em> 切入、Cosmos DB 文章從 <em>consistency level 選擇</em> 切入。讀者讀完本篇後若還在問「為什麼 Cosmos DB strong consistency 不能配 multi-region write」、跳 Cosmos DB SSoT。</p>
<h2 id="操作流程怎麼驗證-consistency-等級">操作流程：怎麼驗證 consistency 等級</h2>
<h3 id="決策樹">決策樹</h3>





<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">跨 multi-object transaction 嗎？
</span></span><span class="line"><span class="ln">2</span><span class="cl">├─ 否 → DynamoDB linearizable read / Redis single-key 足夠
</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">   跨 region 寫入嗎？
</span></span><span class="line"><span class="ln">5</span><span class="cl">   ├─ 否 → CockroachDB / PostgreSQL serializable 足夠
</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">      real-time 順序是產品契約嗎？
</span></span><span class="line"><span class="ln">8</span><span class="cl">      ├─ 否 → CockroachDB multi-region 可接受
</span></span><span class="line"><span class="ln">9</span><span class="cl">      └─ 是 → Spanner / Aurora DSQL</span></span></code></pre></div><h3 id="驗證-consistency-等級的方法">驗證 consistency 等級的方法</h3>
<p>跑 Jepsen-style test、寫 read-write workload 跑 anomaly checker、量 dirty write / lost update / write skew / G2 anomaly。production 系統若不能跑完整 Jepsen、至少要在 staging 跑 <em>對應 anomaly 的具體 test case</em> — 例如金融帳本跑「轉帳後立即跨 region 查餘額、能不能看到舊值」這個具體 case、不是只看 isolation level 設定文字。</p>
<h3 id="sdk-層的選擇點">SDK 層的選擇點</h3>





<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">Spanner          → 預設就是 external consistency、read 可降到 bounded staleness
</span></span><span class="line"><span class="ln">2</span><span class="cl">CockroachDB      → 預設 serializable、可選 AS OF SYSTEM TIME 換 stale read
</span></span><span class="line"><span class="ln">3</span><span class="cl">PostgreSQL       → 要顯式 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
</span></span><span class="line"><span class="ln">4</span><span class="cl">DynamoDB         → 預設 eventually consistent、ConsistentRead=true 換強一致</span></span></code></pre></div><p>每個 SDK 的 default 都不同、不能假設「沒設就是強的」。PostgreSQL default 是 READ COMMITTED、write skew 直接漏。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>若一致性等級從強降到弱、要審計應用層所有讀取點（特別是「讀後決策再寫」的 critical path）。降級不是 config 一行的事、是 audit 一遍應用層假設的事。</p>
<h2 id="失敗模式把-transaction-當強一致的五種誤用">失敗模式：把 transaction 當「強一致」的五種誤用</h2>
<h3 id="把我們用-transaction當強一致">把「我們用 transaction」當「強一致」</h3>
<p>transaction 只保證原子性、不保證 isolation level；預設 isolation 可能是 READ COMMITTED、write skew 直接漏。修法是顯式設定 isolation level、跑對應 anomaly test 驗證、不靠「我們用 transaction」這種口頭契約。</p>
<h3 id="假設-single-node-serializable--distributed-serializable">假設 single-node serializable = distributed serializable</h3>
<p>PostgreSQL SSI 跨 read replica 立刻失效（replica lag）、團隊以為加 replica 還是 serializable。實際 replica 的 read 是 eventually consistent、可能看到舊 snapshot。修法是區分 primary read vs replica read、replica read path 標 <code>bounded staleness</code>、不混用 isolation level 字眼。</p>
<h3 id="跨系統-timestamp-假設">跨系統 timestamp 假設</h3>
<p>service A 用 Spanner、service B 用 Redis、用各自 timestamp 重組事件順序 — service B 的 clock 沒 TrueTime 保證、跨系統 external consistency 不成立。修法是跨系統事件順序要走 <em>單一系統的 timestamp</em> 或 <em>event sequence number</em>、不靠各系統自己的 wall-clock 拼出順序。</p>
<h3 id="把-linearizability-跟-strong-consistency-混用忽略-multi-object-場景">把 linearizability 跟 strong consistency 混用、忽略 multi-object 場景</h3>
<p>DynamoDB strongly consistent read 是 single-item linearizability、不等於跨 item transaction 強一致。團隊以為「我用了 strongly consistent read 就 OK」、實際跨 item 的順序保證沒有。修法是區分 single-object vs multi-object、跨 item 邏輯如果有順序需求、要用 DynamoDB transaction API（付 2x WCU 的 cost）或換到 Spanner。</p>
<h3 id="過度承諾-external-consistency">過度承諾 external consistency</h3>
<p>dashboard / analytics 強寫 strong read、付不必要的 latency tax。修法是把 read path 分類、analytics / reporting 改 bounded staleness、保留 strong read 給 critical path。回 <a href="../truetime-api-depth/">truetime-api-depth</a> 的「把 strong read 用在不需要的路徑」失敗模式。</p>
<h2 id="容量與觀測一致性等級的-latency-量化">容量與觀測：一致性等級的 latency 量化</h2>
<table>
  <thead>
      <tr>
          <th>一致性等級</th>
          <th>latency 影響</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>External consistency（strong）</td>
          <td>baseline = 2ε + quorum RTT</td>
          <td>critical path、金融帳本、計費</td>
      </tr>
      <tr>
          <td>Bounded staleness（5-10s）</td>
          <td>省 commit wait（10-50ms）、可讀本地 replica</td>
          <td>dashboard、reporting</td>
      </tr>
      <tr>
          <td>Eventual</td>
          <td>砍 quorum RTT、只讀本地 replica</td>
          <td>analytics、推薦</td>
      </tr>
  </tbody>
</table>
<p>跨 region 延遲量化（finding F3.15、來源 9.C10）：external consistency + multi-region instance config、跨洲 quorum 把 write latency 推到 100-200ms 數量級；單 region instance 的 commit wait 是 baseline（≈ 2ε ≈ 2-14ms）、跨 region quorum 是額外 dominant cost。</p>
<p>Cloud Monitoring：<code>spanner.googleapis.com/instance/clock_skew_ms</code> 觀察 ε、<code>api/api_request_latencies</code> for <code>Commit</code> 觀察 commit latency 分布；CockroachDB 觀察 <code>sql.txn.restart.serializable</code> 計數（serializable restart 率）。回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 把一致性等級當 release gate 的一部分。</p>
<p>Capacity 觀點：external consistency 的 commit wait 是「無法 scale away 的 latency 支出」、capacity planning 要先扣這部分；跨 region instance 的 quorum RTT 也是物理硬限、不能透過加 node 解。</p>
<h2 id="邊界與整合sibling-路由跟-anti-recommendation">邊界與整合：sibling 路由跟 anti-recommendation</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../truetime-api-depth/">truetime-api-depth</a>：external consistency 的硬體基礎、TrueTime ε / commit wait 數學、商業邏輯先行 frame</li>
<li><a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>：schema change 的版本一致性也用 TrueTime</li>
<li><a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a>：Diff 階段要明確標示一致性等級從 SSI 升到 external consistency 的應用層影響</li>
</ul>
<h3 id="ssot-cross-link">SSoT cross-link</h3>
<p>Strong consistency + multi-region 互斥議題的 SSoT 在 <a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region-write-conflict</a>、本篇不重複展開。</p>
<h3 id="跟-1x-章節的互引">跟 1.x 章節的互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>：Spanner 是 PC 系統的代表</li>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>：跨 transaction 順序保證</li>
</ul>
<h3 id="knowledge-card-雙引用">Knowledge card 雙引用</h3>
<ul>
<li><a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> — 本文當這張卡的 vendor 應用範例</li>
<li><a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external-consistency</a> — 本文擴展這張卡的實作機制</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation-level</a> — 本文澄清 isolation level 跟 consistency model 的差異</li>
</ul>
<h3 id="anti-recommendation">Anti-recommendation</h3>
<p>讀者讀完本文應該能判斷：「我們需要強一致」不等於「升級到 Spanner」 — 先問是 single-object 還是 multi-object、是 single region 還是 multi region、real-time 順序是否是產品契約。多數 OLTP workload 用 PostgreSQL serializable 已經夠、為 external consistency 付 GCP lock-in + 跨 region quorum cost 的判準很高。</p>
]]></content:encoded></item><item><title>PostGIS Deep Dive：Geometry / Geography 型別、GiST 空間索引跟 ST_* 函式生態</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/postgis-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/postgis-deep-dive/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>PostGIS extension&lt;/em> — PG 變 GIS DB 的標配、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem&lt;/a> 是 &lt;em>單一 extension 細節 vs ecosystem 全景&lt;/em> 的關係。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="postgis-是-pg-的-gis-specialization">PostGIS 是 PG 的 &lt;em>GIS Specialization&lt;/em>&lt;/h2>
&lt;p>PostGIS 是 PG 最成熟的 extension 之一（2001 年起、25 年歷史）、產業地位等同 OracleSpatial / SQL Server geography：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">postgis&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完後 PG 多兩件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>空間型別&lt;/strong>：&lt;code>geometry&lt;/code>（平面）/ &lt;code>geography&lt;/code>（地球曲面）/ &lt;code>raster&lt;/code>（柵格）&lt;/li>
&lt;li>&lt;strong>1000+ 函式&lt;/strong>：&lt;code>ST_Distance&lt;/code> / &lt;code>ST_Within&lt;/code> / &lt;code>ST_Buffer&lt;/code> / &lt;code>ST_Intersects&lt;/code> 等&lt;/li>
&lt;/ol>
&lt;p>用 PostGIS 解的典型 workload：&lt;/p>
&lt;ul>
&lt;li>「離我最近的 N 家店」（k-NN）&lt;/li>
&lt;li>「半徑 1km 內的所有 POI」（radius query）&lt;/li>
&lt;li>「兩個 polygon 是否重疊」（intersection）&lt;/li>
&lt;li>「polyline 總長度」（measurement）&lt;/li>
&lt;li>「行政區包含哪些 point」（containment）&lt;/li>
&lt;/ul>
&lt;h2 id="geometry-vs-geography選錯付學費">Geometry vs Geography：選錯付學費&lt;/h2>
&lt;p>PostGIS 提供兩種空間型別、用途完全不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>&lt;code>geometry&lt;/code>&lt;/th>
 &lt;th>&lt;code>geography&lt;/code>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>座標系統&lt;/td>
 &lt;td>平面（笛卡兒）&lt;/td>
 &lt;td>地球曲面（spheroid）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>距離單位&lt;/td>
 &lt;td>座標系統決定（meter / degree）&lt;/td>
 &lt;td>永遠 meter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨經度 180°&lt;/td>
 &lt;td>不處理&lt;/td>
 &lt;td>自動處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用範圍&lt;/td>
 &lt;td>小區域（單一城市 / 國家）&lt;/td>
 &lt;td>全球&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>函式覆蓋&lt;/td>
 &lt;td>1000+ 函式&lt;/td>
 &lt;td>約 300 函式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>效能&lt;/td>
 &lt;td>快（平面計算）&lt;/td>
 &lt;td>慢 2-5x（球面計算）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 行為&lt;/td>
 &lt;td>GiST 直接&lt;/td>
 &lt;td>GiST 直接&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>選 &lt;code>geography&lt;/code> 的場景&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>PostGIS extension</em> — PG 變 GIS DB 的標配、跟 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 是 <em>單一 extension 細節 vs ecosystem 全景</em> 的關係。</p></blockquote>
<hr>
<h2 id="postgis-是-pg-的-gis-specialization">PostGIS 是 PG 的 <em>GIS Specialization</em></h2>
<p>PostGIS 是 PG 最成熟的 extension 之一（2001 年起、25 年歷史）、產業地位等同 OracleSpatial / SQL Server geography：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">postgis</span><span class="p">;</span></span></span></code></pre></div><p>加完後 PG 多兩件事：</p>
<ol>
<li><strong>空間型別</strong>：<code>geometry</code>（平面）/ <code>geography</code>（地球曲面）/ <code>raster</code>（柵格）</li>
<li><strong>1000+ 函式</strong>：<code>ST_Distance</code> / <code>ST_Within</code> / <code>ST_Buffer</code> / <code>ST_Intersects</code> 等</li>
</ol>
<p>用 PostGIS 解的典型 workload：</p>
<ul>
<li>「離我最近的 N 家店」（k-NN）</li>
<li>「半徑 1km 內的所有 POI」（radius query）</li>
<li>「兩個 polygon 是否重疊」（intersection）</li>
<li>「polyline 總長度」（measurement）</li>
<li>「行政區包含哪些 point」（containment）</li>
</ul>
<h2 id="geometry-vs-geography選錯付學費">Geometry vs Geography：選錯付學費</h2>
<p>PostGIS 提供兩種空間型別、用途完全不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><code>geometry</code></th>
          <th><code>geography</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>座標系統</td>
          <td>平面（笛卡兒）</td>
          <td>地球曲面（spheroid）</td>
      </tr>
      <tr>
          <td>距離單位</td>
          <td>座標系統決定（meter / degree）</td>
          <td>永遠 meter</td>
      </tr>
      <tr>
          <td>跨經度 180°</td>
          <td>不處理</td>
          <td>自動處理</td>
      </tr>
      <tr>
          <td>適用範圍</td>
          <td>小區域（單一城市 / 國家）</td>
          <td>全球</td>
      </tr>
      <tr>
          <td>函式覆蓋</td>
          <td>1000+ 函式</td>
          <td>約 300 函式</td>
      </tr>
      <tr>
          <td>效能</td>
          <td>快（平面計算）</td>
          <td>慢 2-5x（球面計算）</td>
      </tr>
      <tr>
          <td>Index 行為</td>
          <td>GiST 直接</td>
          <td>GiST 直接</td>
      </tr>
  </tbody>
</table>
<p><strong>選 <code>geography</code> 的場景</strong>：</p>
<ul>
<li>全球範圍 application（跨國 / 跨大陸）</li>
<li>距離精準度要求高（球面比平面誤差小）</li>
<li>不需要複雜空間運算（geography 函式較少）</li>
</ul>
<p><strong>選 <code>geometry</code> 的場景</strong>：</p>
<ul>
<li>單一城市 / 國家內 application</li>
<li>需要完整 ST_* 函式（90% 函式只支援 geometry）</li>
<li>效能敏感</li>
</ul>
<p>實務多數 production 選 <code>geometry</code> + 適合的 SRID（用 local projection）— 既快又精準。</p>
<h2 id="srid-跟-projection為什麼-4326-vs-3857-是-gis-第一課">SRID 跟 Projection：為什麼 4326 vs 3857 是 GIS 第一課</h2>
<p>SRID（Spatial Reference System Identifier）定義「座標數字怎麼解讀」：</p>
<table>
  <thead>
      <tr>
          <th>SRID</th>
          <th>名稱</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>4326</td>
          <td>WGS 84（GPS）</td>
          <td>經緯度、最常見、Google Maps API</td>
      </tr>
      <tr>
          <td>3857</td>
          <td>Web Mercator</td>
          <td>Web tile map（OpenStreetMap）</td>
      </tr>
      <tr>
          <td>3826</td>
          <td>TWD97 / TM2 zone 121</td>
          <td>台灣 local projection、米為單位</td>
      </tr>
      <tr>
          <td>2272</td>
          <td>NAD83 / Pennsylvania</td>
          <td>美國 state plane（各州不同）</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼選 local projection（3826）而不是經緯度（4326）</strong>：</p>
<ul>
<li>經緯度單位是 <em>度</em>、不是距離 — <code>ST_Distance</code> 直接算出來是「度」、不是「米」</li>
<li>距離計算需 <code>ST_DistanceSphere</code> 或 <code>geography</code> cast、計算 cost 高</li>
<li>Local projection 是「平面投影」、<code>ST_Distance</code> 直接是米、<code>ST_Area</code> 直接是平方米</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 4326 經緯度直接算 → 結果不是米
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_Distance</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5654</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0330</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">),</span><span class="w">  </span><span class="c1">-- 台北 101
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5170</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0478</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">)</span><span class="w">   </span><span class="c1">-- 台北車站
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">  </span><span class="c1">-- ~0.05（這是「度」）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- 轉 3826（台灣本地投影）才是米
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_Distance</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">ST_Transform</span><span class="p">(</span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5654</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0330</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">),</span><span class="w"> </span><span class="mi">3826</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">ST_Transform</span><span class="p">(</span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5170</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0478</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">),</span><span class="w"> </span><span class="mi">3826</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">  </span><span class="c1">-- ~5300（米）
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 或用 geography cast
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_Distance</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5654</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0330</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">)::</span><span class="n">geography</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">121</span><span class="p">.</span><span class="mi">5170</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="p">.</span><span class="mi">0478</span><span class="p">),</span><span class="w"> </span><span class="mi">4326</span><span class="p">)::</span><span class="n">geography</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">  </span><span class="c1">-- ~5300（米）</span></span></span></code></pre></div><p><strong>典型 schema 設計</strong>（台灣 application）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">name</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="c1">-- 儲存 4326（跟 Google Maps API 對齊）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">location_4326</span><span class="w"> </span><span class="n">geometry</span><span class="p">(</span><span class="n">Point</span><span class="p">,</span><span class="w"> </span><span class="mi">4326</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="c1">-- 預計算 3826（給距離 / 面積 query 用）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">location_3826</span><span class="w"> </span><span class="n">geometry</span><span class="p">(</span><span class="n">Point</span><span class="p">,</span><span class="w"> </span><span class="mi">3826</span><span class="p">)</span><span class="w"> </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="p">(</span><span class="n">ST_Transform</span><span class="p">(</span><span class="n">location_4326</span><span class="p">,</span><span class="w"> </span><span class="mi">3826</span><span class="p">))</span><span class="w"> </span><span class="n">STORED</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_pois_location_3826</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIST</span><span class="w"> </span><span class="p">(</span><span class="n">location_3826</span><span class="p">);</span></span></span></code></pre></div><h2 id="gist-空間索引r-tree-的-pg-實作">GiST 空間索引：R-tree 的 PG 實作</h2>
<p>PostGIS 用 PG 內建 GiST 做空間索引（內部是 R-tree 變體）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_pois_geom</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIST</span><span class="w"> </span><span class="p">(</span><span class="n">location_3826</span><span class="p">);</span></span></span></code></pre></div><p>GiST 對空間 query 加速的場景：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 範圍 query（box overlap）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">location_3826</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">ST_MakeEnvelope</span><span class="p">(</span><span class="mi">290000</span><span class="p">,</span><span class="w"> </span><span class="mi">2760000</span><span class="p">,</span><span class="w"> </span><span class="mi">305000</span><span class="p">,</span><span class="w"> </span><span class="mi">2775000</span><span class="p">,</span><span class="w"> </span><span class="mi">3826</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 半徑 query（用 ST_DWithin 才走 index）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">ST_DWithin</span><span class="p">(</span><span class="n">location_3826</span><span class="p">,</span><span class="w"> </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">300000</span><span class="p">,</span><span class="w"> </span><span class="mi">2770000</span><span class="p">),</span><span class="w"> </span><span class="mi">3826</span><span class="p">),</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- k-NN（PostGIS 2.0+ &lt;-&gt; operator）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">location_3826</span><span class="w"> </span><span class="o">&lt;-&gt;</span><span class="w"> </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">300000</span><span class="p">,</span><span class="w"> </span><span class="mi">2770000</span><span class="p">),</span><span class="w"> </span><span class="mi">3826</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">location_3826</span><span class="w"> </span><span class="o">&lt;-&gt;</span><span class="w"> </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="n">ST_MakePoint</span><span class="p">(</span><span class="mi">300000</span><span class="p">,</span><span class="w"> </span><span class="mi">2770000</span><span class="p">),</span><span class="w"> </span><span class="mi">3826</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><p><strong>index 用沒用到的關鍵</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Query 寫法</th>
          <th>走 index？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ST_DWithin(a, b, dist)</code></td>
          <td>是</td>
      </tr>
      <tr>
          <td><code>ST_Distance(a, b) &lt; dist</code></td>
          <td>否（必 full scan）</td>
      </tr>
      <tr>
          <td><code>a &amp;&amp; bbox</code></td>
          <td>是</td>
      </tr>
      <tr>
          <td><code>ST_Intersects(a, bbox)</code></td>
          <td>是</td>
      </tr>
      <tr>
          <td><code>a &lt;-&gt; b ORDER BY ... LIMIT n</code></td>
          <td>是（k-NN）</td>
      </tr>
      <tr>
          <td><code>ST_Equals(a, b)</code></td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p>Production 寫法守則：能用 <code>ST_DWithin</code> 就不用 <code>ST_Distance(...) &lt; ?</code>、語意一樣但 index 行為差很多。</p>
<h2 id="st_-函式生態產業級全套">ST_* 函式生態：產業級全套</h2>
<p>PostGIS 1000+ 函式分類（典型用到的）：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>代表函式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建構</td>
          <td><code>ST_MakePoint</code> / <code>ST_MakeLine</code> / <code>ST_MakePolygon</code></td>
      </tr>
      <tr>
          <td>關係判定</td>
          <td><code>ST_Intersects</code> / <code>ST_Within</code> / <code>ST_Contains</code> / <code>ST_Touches</code></td>
      </tr>
      <tr>
          <td>距離 / 大小</td>
          <td><code>ST_Distance</code> / <code>ST_DWithin</code> / <code>ST_Length</code> / <code>ST_Area</code></td>
      </tr>
      <tr>
          <td>變換</td>
          <td><code>ST_Buffer</code> / <code>ST_Union</code> / <code>ST_Difference</code> / <code>ST_Intersection</code></td>
      </tr>
      <tr>
          <td>投影</td>
          <td><code>ST_Transform</code> / <code>ST_SetSRID</code></td>
      </tr>
      <tr>
          <td>格式轉換</td>
          <td><code>ST_AsGeoJSON</code> / <code>ST_AsKML</code> / <code>ST_AsText</code> / <code>ST_GeomFromGeoJSON</code></td>
      </tr>
      <tr>
          <td>路徑 / 拓樸</td>
          <td><code>ST_ShortestLine</code> / <code>ST_LineMerge</code></td>
      </tr>
      <tr>
          <td>聚合</td>
          <td><code>ST_Collect</code> / <code>ST_ConvexHull</code> / <code>ST_Centroid</code></td>
      </tr>
      <tr>
          <td>簡化</td>
          <td><code>ST_Simplify</code> / <code>ST_SimplifyPreserveTopology</code></td>
      </tr>
  </tbody>
</table>
<p><strong>Web tile 場景</strong>典型 query：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 給定 z/x/y tile、找這個 tile 內的所有 POI
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">ST_AsMVTGeom</span><span class="p">(</span><span class="n">location_3857</span><span class="p">,</span><span class="w"> </span><span class="n">ST_TileEnvelope</span><span class="p">(</span><span class="n">z</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">geom</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">location_3857</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">ST_TileEnvelope</span><span class="p">(</span><span class="n">z</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">);</span></span></span></code></pre></div><p><code>ST_AsMVTGeom</code> + <code>ST_AsMVT</code> 直接產 Mapbox Vector Tile binary、給前端 Leaflet / Mapbox GL JS 用。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1geometry-用錯-srid">Case 1：Geometry 用錯 SRID</h3>
<p><strong>情境</strong>：app 寫入時用 4326、query 時用 3826 ST_Transform、忘記給某個 column 設 SRID、index 失效。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 確認 SRID
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_SRID</span><span class="p">(</span><span class="k">location</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 強 type 約束（column type 寫死 SRID）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="k">location</span><span class="w"> </span><span class="k">TYPE</span><span class="w"> </span><span class="n">geometry</span><span class="p">(</span><span class="n">Point</span><span class="p">,</span><span class="w"> </span><span class="mi">4326</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">USING</span><span class="w"> </span><span class="n">ST_SetSRID</span><span class="p">(</span><span class="k">location</span><span class="p">,</span><span class="w"> </span><span class="mi">4326</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- Check constraint 防錯
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">pois</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">CONSTRAINT</span><span class="w"> </span><span class="n">chk_location_srid</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">ST_SRID</span><span class="p">(</span><span class="k">location</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">4326</span><span class="p">);</span></span></span></code></pre></div><h3 id="case-2geography-不能用所有-st_-函式">Case 2：Geography 不能用所有 ST_* 函式</h3>
<p><strong>情境</strong>：用 <code>geography</code> 想跑 <code>ST_Buffer</code>、報錯或結果不對。</p>
<p><code>ST_Buffer</code> 對 geography 走 spheroid 近似、邊界 case 結果跟 geometry 不一致；很多函式（<code>ST_Voronoi</code> / <code>ST_Delaunay</code> 等）只支援 geometry。</p>
<p>修法：</p>
<ul>
<li>簡單距離 query 用 geography</li>
<li>複雜空間運算用 geometry + 適合 projection</li>
<li>不確定哪些函式支援 geography、看 PostGIS docs <em>Geography Support Functions</em> 清單</li>
</ul>
<h3 id="case-3gist-index-不對-st_distance-生效">Case 3：GiST index 不對 ST_Distance 生效</h3>
<p><strong>情境</strong>：query <code>ST_Distance(location, ?) &lt; 1000</code>、<code>EXPLAIN</code> 顯示 full scan、加 index 也沒用。</p>
<p><code>ST_Distance</code> 算完才 filter、planner 沒辦法用 GiST。</p>
<p>修法：</p>
<ul>
<li>改 <code>ST_DWithin(location, ?, 1000)</code> — 語意一樣、會走 GiST</li>
<li>確認 index 是對 <em>被 query 的 column</em> 建的（不是 transform 後的 expression）</li>
</ul>
<h3 id="case-4cluster-on-geom-後-brin-失效">Case 4：CLUSTER on geom 後 BRIN 失效</h3>
<p><strong>情境</strong>：對 <code>pois</code> 跑 <code>CLUSTER pois USING idx_pois_geom</code> 想加速空間查、但同時對 <code>created_at</code> 用 BRIN index、BRIN 完全失效。</p>
<p>CLUSTER 重組 physical order 跟 GiST 對齊、<code>created_at</code> physical order correlation 從 1.0 變 0.0、BRIN range 沒選擇性。</p>
<p>修法：</p>
<ul>
<li>不要 CLUSTER 大表（一次性、影響其他 column）</li>
<li>換 partition by time + GiST per-partition（取兩者）</li>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/index-selection/" data-link-title="PostgreSQL Index Selection：B-tree / GIN / GiST / BRIN / Hash 對應 workload 的決策樹" data-link-desc="PG 有 6 種 index method（B-tree / Hash / GIN / GiST / SP-GiST / BRIN）跟 partial / expression / covering 三種變體、不是「都用 B-tree 就好」。每種 index 有自己的 query pattern、儲存代價、write amplification 跟 maintenance 成本。本文走 6 種 index 的適用 workload 對照、決策樹、partial / expression / covering / multi-column 變體、5 production 踩雷（過度 index / partial 條件不對 / B-tree 對 JSON 無效 / BRIN 對非 correlated 資料無效 / multi-column 順序錯）、跟 query-optimization 的 EXPLAIN 互補">index-selection</a> 的 BRIN 段</li>
</ul>
<h3 id="case-5ewkb-vs-wkb-跨工具相容">Case 5：EWKB vs WKB 跨工具相容</h3>
<p><strong>情境</strong>：用 PostGIS export 給其他 GIS 工具（QGIS / Shapely / ogr2ogr）、resort 抱怨格式不對。</p>
<p>PostGIS 內部用 EWKB（Extended Well-Known Binary）— 多帶 SRID。多數 GIS 工具讀 WKB（標準）。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Export 標準 WKB
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_AsBinary</span><span class="p">(</span><span class="n">geom</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 或 GeoJSON（跨工具最相容）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_AsGeoJSON</span><span class="p">(</span><span class="n">geom</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pois</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 或 Shapefile via ogr2ogr
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">-- ogr2ogr -f &#34;ESRI Shapefile&#34; output.shp PG:&#34;...&#34; -sql &#34;SELECT * FROM pois&#34;</span></span></span></code></pre></div><h2 id="跟專業-gis-db-對比">跟專業 GIS DB 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostGIS</th>
          <th>Oracle Spatial</th>
          <th>SQL Server geography</th>
          <th>MongoDB GeoJSON</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>函式覆蓋</td>
          <td>1000+</td>
          <td>800+</td>
          <td>200+</td>
          <td>~20</td>
      </tr>
      <tr>
          <td>Raster 支援</td>
          <td>是</td>
          <td>是</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>是（PostGIS Topology）</td>
          <td>是</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>3D 支援</td>
          <td>是（PostGIS SFCGAL）</td>
          <td>是</td>
          <td>部分</td>
          <td>否</td>
      </tr>
      <tr>
          <td>License</td>
          <td>GPL</td>
          <td>商業</td>
          <td>商業</td>
          <td>開源</td>
      </tr>
      <tr>
          <td>Tile generation</td>
          <td>內建（ST_AsMVT）</td>
          <td>否</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>跟 PG 整合</td>
          <td>完美</td>
          <td>跟 Oracle 一體</td>
          <td>跟 SQL Server 一體</td>
          <td>獨立</td>
      </tr>
      <tr>
          <td>工業界使用</td>
          <td>OpenStreetMap / 各國國土測繪</td>
          <td>大型企業</td>
          <td>Microsoft 生態</td>
          <td>簡單 location app</td>
      </tr>
  </tbody>
</table>
<p><strong>選 PostGIS 的場景</strong>（90% GIS workload）：</p>
<ul>
<li>Application 已用 PG</li>
<li>需要完整 GIS 函式生態（路網 / 等高線 / 流域分析）</li>
<li>開源 / cost 敏感</li>
<li>跟 OGR / GDAL / QGIS 互通</li>
</ul>
<p><strong>選專業 GIS DB 的場景</strong>：</p>
<ul>
<li>已綁定 Oracle / SQL Server license</li>
<li>極專業 GIS（3D 城市模型 / LIDAR / GPU 加速）</li>
<li>純 location app 不需 relational（MongoDB GeoJSON 足夠）</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a>：其他 PG extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/index-selection/" data-link-title="PostgreSQL Index Selection：B-tree / GIN / GiST / BRIN / Hash 對應 workload 的決策樹" data-link-desc="PG 有 6 種 index method（B-tree / Hash / GIN / GiST / SP-GiST / BRIN）跟 partial / expression / covering 三種變體、不是「都用 B-tree 就好」。每種 index 有自己的 query pattern、儲存代價、write amplification 跟 maintenance 成本。本文走 6 種 index 的適用 workload 對照、決策樹、partial / expression / covering / multi-column 變體、5 production 踩雷（過度 index / partial 條件不對 / B-tree 對 JSON 無效 / BRIN 對非 correlated 資料無效 / multi-column 順序錯）、跟 query-optimization 的 EXPLAIN 互補">index-selection</a>：GiST 跟其他 index 對比</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">query-optimization</a>：空間 query 的 EXPLAIN</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">jsonb-deep-dive</a>：POI metadata 用 JSONB 儲存</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 探索其他 PG 擴展可能</li>
<li>回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 看全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C31 Mozilla Pulse：命名前綴 + ACL 取代 vhost 多租戶</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-mozilla-pulse-naming-isolation/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-mozilla-pulse-naming-isolation/</guid><description>&lt;p>這個案例的核心責任是說明多租戶隔離可用「ACL + naming convention」取代 vhost、適合社群協作場景。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Pulse 是 Mozilla 自動化 / 基礎設施工具間的 managed RabbitMQ cluster、用 AMQP 0-9-1 + RabbitMQ 擴充、由 CloudAMQP 託管於 pulse.mozilla.org:5671（AMQP over TLS）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>技術上不需 vhost、改用權限限制 + 命名前綴（&lt;code>exchange/&amp;lt;username&amp;gt;/*&lt;/code>、&lt;code>queue/&amp;lt;username&amp;gt;/*&lt;/code>）做隔離。PulseGuardian 跑在 Heroku 管理使用者 / queue / exchange。揭露多租戶隔離不一定要 vhost、權限粒度可以拉到 resource naming 層。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：多 vhost + 多租戶（反向案例：用 ACL + naming 取代 vhost）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &amp;#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23 Bloomberg vhost 多租戶&lt;/a>（對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://wiki.mozilla.org/Auto-tools/Projects/Pulse">Mozilla Pulse Wiki&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pulse.mozilla.org/api/">Pulse API&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明多租戶隔離可用「ACL + naming convention」取代 vhost、適合社群協作場景。</p>
<h2 id="觀察">觀察</h2>
<p>Pulse 是 Mozilla 自動化 / 基礎設施工具間的 managed RabbitMQ cluster、用 AMQP 0-9-1 + RabbitMQ 擴充、由 CloudAMQP 託管於 pulse.mozilla.org:5671（AMQP over TLS）。</p>
<h2 id="判讀">判讀</h2>
<p>技術上不需 vhost、改用權限限制 + 命名前綴（<code>exchange/&lt;username&gt;/*</code>、<code>queue/&lt;username&gt;/*</code>）做隔離。PulseGuardian 跑在 Heroku 管理使用者 / queue / exchange。揭露多租戶隔離不一定要 vhost、權限粒度可以拉到 resource naming 層。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：多 vhost + 多租戶（反向案例：用 ACL + naming 取代 vhost）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23 Bloomberg vhost 多租戶</a>（對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://wiki.mozilla.org/Auto-tools/Projects/Pulse">Mozilla Pulse Wiki</a></li>
<li><a href="https://pulse.mozilla.org/api/">Pulse API</a></li>
</ul>
]]></content:encoded></item><item><title>Pyroscope</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/</guid><description>&lt;p>Pyroscope 的核心責任是提供開源 continuous profiling backend，讓團隊用 Grafana 生態保存、查詢、比較與視覺化 production profile。它適合偏 OSS-first、已使用 Grafana / Prometheus / Tempo / Loki 的團隊，重點在把 CPU、memory、allocation 與 profile diff 放進可自管 observability stack。Pyroscope 原為獨立 OSS 專案、&lt;em>2023 年被 Grafana Labs 收購&lt;/em>、現分兩條產品線：&lt;em>Grafana Pyroscope&lt;/em>（OSS、Apache 2.0、self-host）與 &lt;em>Grafana Cloud Profiles&lt;/em>（商業 SaaS、走 Grafana Cloud 計費）。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Pyroscope 在 continuous profiling 賽道上的差異點是 &lt;em>Grafana Labs 整合 + 多語言 SDK 覆蓋&lt;/em>、而不是 profiling 演算法本身。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca&lt;/a> 比、Parca 走 &lt;em>pprof + Prometheus-style label&lt;/em> 的 CNCF / eBPF infrastructure profiling 路線、focus 在 system-wide 一次抓全機；Pyroscope 走 &lt;em>per-language SDK + Grafana stack 整合&lt;/em> 的 developer-facing 路線、focus 在 application-level flame graph 與 release diff。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler&lt;/a> 比、Datadog 走 &lt;em>SaaS all-in-one + APM 同 trace context&lt;/em>、profiling 自動跟 trace span 關聯；Pyroscope 走 &lt;em>self-host 可選 + Grafana 跨 signal&lt;/em>、整合靠 Grafana dashboard 跟 explore link 而非 product-level deep linking。&lt;/p>
&lt;p>這個定位讓 Pyroscope 接到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop&lt;/a>。它的價值在於 OSS / Grafana 整合與可自管；它的代價在於 storage、retention、agent rollout 與營運責任要由團隊承擔。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Pyroscope deployment 是否健康、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent / SDK setup&lt;/strong>：是用 &lt;em>language SDK&lt;/em>（in-process profiler、跟 application code 一起部署）還是 &lt;em>Grafana Alloy / Pyroscope agent&lt;/em>（out-of-process、適合 binary-only 或無法改 code 的 workload）— 兩條路 overhead、覆蓋率、tag 注入方式都不同&lt;/li>
&lt;li>&lt;strong>Push or pull model&lt;/strong>：SDK 預設 &lt;em>push&lt;/em>（application 主動把 profile sample 推到 Pyroscope server）、Alloy / agent 可走 &lt;em>pull&lt;/em>（scrape pprof endpoint、跟 Prometheus 同模型）— push 適合 short-lived job / serverless、pull 適合 long-running service + Kubernetes service discovery&lt;/li>
&lt;li>&lt;strong>Grafana integration&lt;/strong>：是否在 Grafana datasource 設好 Pyroscope、explore 是否能跨 trace / log / profile 跳轉（Tempo trace → Pyroscope profile by service+span）、dashboard 是否內嵌 flame graph panel&lt;/li>
&lt;li>&lt;strong>Tag schema discipline&lt;/strong>：service / version / region / environment / pod 是否一致命名、deploy event 是否打 label 讓 baseline / candidate 比較可成立&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、profile 就只是「能看 flame graph」而非「release gate evidence」、無法支撐 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop&lt;/a> 的 diff workflow。&lt;/p></description><content:encoded><![CDATA[<p>Pyroscope 的核心責任是提供開源 continuous profiling backend，讓團隊用 Grafana 生態保存、查詢、比較與視覺化 production profile。它適合偏 OSS-first、已使用 Grafana / Prometheus / Tempo / Loki 的團隊，重點在把 CPU、memory、allocation 與 profile diff 放進可自管 observability stack。Pyroscope 原為獨立 OSS 專案、<em>2023 年被 Grafana Labs 收購</em>、現分兩條產品線：<em>Grafana Pyroscope</em>（OSS、Apache 2.0、self-host）與 <em>Grafana Cloud Profiles</em>（商業 SaaS、走 Grafana Cloud 計費）。</p>
<h2 id="服務定位">服務定位</h2>
<p>Pyroscope 在 continuous profiling 賽道上的差異點是 <em>Grafana Labs 整合 + 多語言 SDK 覆蓋</em>、而不是 profiling 演算法本身。跟 <a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a> 比、Parca 走 <em>pprof + Prometheus-style label</em> 的 CNCF / eBPF infrastructure profiling 路線、focus 在 system-wide 一次抓全機；Pyroscope 走 <em>per-language SDK + Grafana stack 整合</em> 的 developer-facing 路線、focus 在 application-level flame graph 與 release diff。跟 <a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a> 比、Datadog 走 <em>SaaS all-in-one + APM 同 trace context</em>、profiling 自動跟 trace span 關聯；Pyroscope 走 <em>self-host 可選 + Grafana 跨 signal</em>、整合靠 Grafana dashboard 跟 explore link 而非 product-level deep linking。</p>
<p>這個定位讓 Pyroscope 接到 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a> 與 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a>。它的價值在於 OSS / Grafana 整合與可自管；它的代價在於 storage、retention、agent rollout 與營運責任要由團隊承擔。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Pyroscope deployment 是否健康、最少看四件事：</p>
<ul>
<li><strong>Agent / SDK setup</strong>：是用 <em>language SDK</em>（in-process profiler、跟 application code 一起部署）還是 <em>Grafana Alloy / Pyroscope agent</em>（out-of-process、適合 binary-only 或無法改 code 的 workload）— 兩條路 overhead、覆蓋率、tag 注入方式都不同</li>
<li><strong>Push or pull model</strong>：SDK 預設 <em>push</em>（application 主動把 profile sample 推到 Pyroscope server）、Alloy / agent 可走 <em>pull</em>（scrape pprof endpoint、跟 Prometheus 同模型）— push 適合 short-lived job / serverless、pull 適合 long-running service + Kubernetes service discovery</li>
<li><strong>Grafana integration</strong>：是否在 Grafana datasource 設好 Pyroscope、explore 是否能跨 trace / log / profile 跳轉（Tempo trace → Pyroscope profile by service+span）、dashboard 是否內嵌 flame graph panel</li>
<li><strong>Tag schema discipline</strong>：service / version / region / environment / pod 是否一致命名、deploy event 是否打 label 讓 baseline / candidate 比較可成立</li>
</ul>
<p>四件事任一缺失、profile 就只是「能看 flame graph」而非「release gate evidence」、無法支撐 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 的 diff workflow。</p>
<h2 id="適用場景">適用場景</h2>
<p>自管 profiling backend 適合 Pyroscope。團隊若有資料主權、成本控制、內網部署或 OSS-first 要求，可以用 Pyroscope 保存 profile，降低 profile sample 外送帶來的治理成本。</p>
<p>Profile diff workflow 適合 Pyroscope。Release candidate、canary、baseline review 或 incident after-action 都可以用時間區間比較，找出 CPU、memory 或 allocation 的相對變化。</p>
<p>Grafana stack 整合適合 Pyroscope。若服務已經有 Grafana dashboard，profile link 可以放進 latency、CPU、memory、cost 或 release dashboard，讓 SRE 從聚合訊號跳到 callstack。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Pyroscope 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OSS / self-host</td>
          <td>profile 資料可自管</td>
          <td>backend storage、retention、upgrade</td>
      </tr>
      <tr>
          <td>Grafana 整合</td>
          <td>dashboard、explore、profile link 容易串接</td>
          <td>tag schema 與 dashboard discipline</td>
      </tr>
      <tr>
          <td>Profile diff</td>
          <td>時間區間與版本對比直觀</td>
          <td>deploy label 與 baseline 管理</td>
      </tr>
      <tr>
          <td>多語言 agent</td>
          <td>常見 runtime 可導入</td>
          <td>agent overhead 與覆蓋差異量測</td>
      </tr>
  </tbody>
</table>
<p>OSS / self-host 價值來自控制權。Profile 可能包含 function name、package path、tenant-specific code path 或敏感 business logic，自管能讓資料保存與存取控制更貼近內部規範。</p>
<p>Grafana 整合價值來自操作連續性。當 CPU dashboard、latency dashboard 與 deploy annotation 都在 Grafana 中，Pyroscope 能讓工程師從圖表直接切到 flame graph。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Pyroscope 和 Datadog Continuous Profiler 的主要差異是平台責任。Pyroscope 偏 OSS / self-host / Grafana stack；Datadog 偏 SaaS all-in-one 與 APM product workflow。</p>
<p>Pyroscope 和 Parca 的主要差異是生態定位。Pyroscope 偏 Grafana profiling backend 與 developer-facing flame graph；Parca 偏 eBPF / infrastructure-wide profiling 與 CNCF 生態。</p>
<p>Pyroscope 和一次性 profiler 的主要差異是可比較性。一次性 profiler 擅長局部調查；Pyroscope 擅長讓 profile 成為 release baseline 與 incident evidence。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Pyroscope（Grafana）</th>
          <th>Parca</th>
          <th>Datadog Continuous Profiler</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署模型</td>
          <td>OSS self-host / Grafana Cloud Profiles SaaS</td>
          <td>OSS self-host（CNCF Sandbox）</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>Profile 來源</td>
          <td>language SDK + Alloy / agent（push 為主）</td>
          <td>pprof scrape（pull）+ Parca Agent（eBPF）</td>
          <td>Datadog Agent + language tracer 整合</td>
      </tr>
      <tr>
          <td>語言覆蓋</td>
          <td>Go / Python / Java / Ruby / .NET / Rust / Node</td>
          <td>任何能輸出 pprof 的 runtime + eBPF system-wide</td>
          <td>Go / Python / Java / Ruby / .NET / Node</td>
      </tr>
      <tr>
          <td>Tag / label</td>
          <td>Prometheus-style label + 自訂 tag</td>
          <td>Prometheus-style label</td>
          <td>Datadog tag（跟 APM 共用）</td>
      </tr>
      <tr>
          <td>Diff workflow</td>
          <td>時間區間 + label 對比 + flame graph diff UI</td>
          <td>時間區間 + label 對比</td>
          <td>自動跟 deploy event + trace span 關聯</td>
      </tr>
      <tr>
          <td>整合方向</td>
          <td>Grafana（Tempo / Loki / Mimir 互跳）</td>
          <td>Prometheus / Grafana（弱整合）</td>
          <td>Datadog APM / Logs / Metrics 同 plane</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Grafana-first、OSS-friendly、release diff 主流程</td>
          <td>infrastructure-wide eBPF profiling、CNCF 生態</td>
          <td>Datadog 已是主 observability、要 APM 連動</td>
      </tr>
  </tbody>
</table>
<p>選 Pyroscope 的核心訴求：<em>已用 Grafana stack + 多語言服務組合 + 要 OSS self-host 選項或預算敏感</em>、profile 主要用途是 release diff / incident hot-path 定位、不需要 APM-level 自動 trace 關聯。</p>
<h2 id="操作成本">操作成本</h2>
<p>Pyroscope 的主要成本是自管 backend。Profile ingest、storage、retention、compaction、backup、upgrade 與 dashboard ownership 都需要團隊負責。</p>
<p>Tag 成本來自查詢維度。service、version、region、environment、runtime、pod、tenant 這些 label 能提高定位能力，也會增加 cardinality、儲存與查詢成本。</p>
<p>Agent 成本來自 rollout 與 overhead。導入時要先選代表性服務，量測 profiler 對 CPU、memory、latency 的影響，再逐步擴大到 critical path。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Pyroscope 結果應回寫到 evidence package。最小欄位包括 service、version、environment、profile type、baseline window、candidate window、profile diff link、tag set、retention policy、overhead estimate、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Pyroscope 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>profile query、flame graph、diff link</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>baseline / candidate profile window</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>Grafana / Pyroscope explore link</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>tag completeness、sampling status</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>production coverage、agent overhead</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 runtime、tag drift、retention gap</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是讓 profile diff 成為 release artifact。Reviewer 要能從 release gate 打開 Pyroscope diff，確認變化來自 code path、runtime 行為、負載變化或 baseline drift。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Grafana Cloud Profiles</strong>：商業 SaaS 版本、走 Grafana Cloud 計費（per-series 或 per-profile bytes）、適合不想自管 storage / retention / compaction 的團隊。跟 OSS Pyroscope 共用 SDK 跟 query API、可在 OSS 起步、規模到一定程度再遷移到 Cloud、避免廠商一開始就鎖死。</p>
<p><strong>Flame graph diff</strong>：Pyroscope 的核心 release workflow — 選 baseline window（release 前 24hr）跟 candidate window（release 後 24hr）、UI 把兩張 flame graph 差異標紅綠、可直接看到哪個 function 變慢 / 變快。判讀要點是 <em>baseline window 要排除部署當下的 warm-up / cache miss spike</em>、否則 diff 噪音蓋過真實 regression。</p>
<p><strong>多語言 SDK 覆蓋</strong>：Pyroscope 官方 SDK 覆蓋 Go / Python / Java / Ruby / .NET / Rust / Node.js — Go SDK 用 <code>runtime/pprof</code> 包裝、Java 走 async-profiler、Python 走 <code>py-spy</code> 風格 sampling profiler、Node.js 走 V8 sampling。各 SDK overhead 不一致（Java async-profiler ~1%、Python py-spy ~3-5%）、選型時要看代表性服務量測再 rollout、不能假設「都很低」。</p>
<p><strong>Adhoc profiling</strong>：當 production SDK 沒裝、或想對 batch job / CLI tool 做一次性 profile、可用 Pyroscope CLI 上傳 <em>standalone pprof file</em>（<code>pyroscope adhoc</code> 或 <code>profilecli</code>）— 補位「標準 pprof endpoint 不夠用、但又不想長期 instrument」的情境。對 ad-hoc incident investigation 跟 batch job postmortem 特別有用。</p>
<p><strong>Grafana Alloy 整合</strong>：Grafana Alloy（前 Grafana Agent）內建 Pyroscope receiver、可同時 scrape Prometheus metrics + tail Loki log + push Tempo trace + scrape Pyroscope profile、單一 agent 跨 four signal、降低 sidecar 數量跟維運成本。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>SDK overhead 過高 / latency p99 上升</strong>：profile sample rate 太高、或 Java async-profiler 在低 CPU host 競爭 schedule — 降 sample rate、staging 量測 CPU / latency delta 確認 &lt; 3% 再 promote</li>
<li><strong>Push agent 跟 pull agent 取捨錯</strong>：short-lived job 用 pull 結果還沒被 scrape 就 exit、long-running service 用 push 結果 Pyroscope server 過載 — short-lived / serverless 走 SDK push、long-running + Kubernetes service discovery 走 Alloy pull</li>
<li><strong>Label cardinality 爆 / storage 跟查詢都慢</strong>：tag 加了 pod name / request ID / user ID 等高 cardinality 維度 — 限制 tag 為 service / version / region / environment / cluster 等低 cardinality、高基數維度走 trace / log 別放 profile</li>
<li><strong>Baseline / candidate diff 全是噪音</strong>：baseline window 沒對齊流量模式（off-peak vs peak）、或 deploy label 沒打 — 要求 release pipeline 自動寫 <code>version</code> / <code>deploy_id</code> label、diff window 跨完整流量週期（24hr or 7day）</li>
<li><strong>Grafana datasource 連不到 / explore 跳轉失敗</strong>：datasource URL 設錯、或 service / span tag 不一致 — Tempo trace 用的 <code>service.name</code> 要跟 Pyroscope <code>service</code> label 對齊、否則 cross-signal 跳轉斷裂</li>
<li><strong>Storage / retention 失控</strong>：profile 保留太久、SmartStore-like 冷儲存沒設 — Pyroscope OSS 支援 object storage（S3 / GCS）backend、長 retention 必開、不然 PV 會爆</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已用 Datadog APM、要 trace ↔ profile 自動關聯</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a></td>
      </tr>
      <tr>
          <td>要 eBPF system-wide / infrastructure profiling</td>
          <td><a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a></td>
      </tr>
      <tr>
          <td>不想自管 backend、但要 Grafana stack</td>
          <td>Grafana Cloud Profiles（商業 SaaS、同 SDK）</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>Pyroscope 適合回寫 OSS observability 與 release diff 案例。它可接 <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 consolidation</a> 的 profile noise 降低、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a> 的 hot path 定位、<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 EKS multi-cluster</a> 的 single-tenant per game profile 隔離、<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> 的 30% 成本下降 hot path 分析，以及 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 的 baseline / candidate profile diff。</p>
<p>這些案例的重點是可比較 profile。Pyroscope 頁引用案例時，要把 case 轉成 tag schema、baseline window、candidate window、flame graph diff 與 release gate evidence — 例如 Riot Games 246 cluster 的 tag schema 必須涵蓋 game / region / cluster 三維、才能避免「跨遊戲混合 profile」的歸因錯誤。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</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></li>
<li>跨模組：<a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a></li>
<li>官方：<a href="https://grafana.com/docs/pyroscope/latest/">Grafana Pyroscope documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/</guid><description>&lt;p>這個案例的核心責任是補強 GCP 案例庫的「商業應用」深度、並提供拉丁美洲電商規模對標。Mercado Libre 是拉丁美洲最大電商（市值 600 億美金級）、業務涵蓋 18 個國家、是區域型平台的容量規劃範本。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Mercado Libre 在 GCP 的關鍵敘述（引自 &lt;a href="https://cloud.google.com/customers/mercado-libre">Mercado Libre Customer Story&lt;/a>）：&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>客戶數&lt;/td>
 &lt;td>1 億&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品數&lt;/td>
 &lt;td>1.5 億（3 個試點國家）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務影響&lt;/td>
 &lt;td>數百萬美金 incremental revenue（Vertex AI Search）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要 GCP 服務&lt;/td>
 &lt;td>Vertex AI Search、BigQuery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料即時性&lt;/td>
 &lt;td>near real-time&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務地理&lt;/td>
 &lt;td>拉丁美洲&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵能力：「Vertex AI Search across 150 million items in three pilot countries that is helping its 100 million customers find the products they love faster」、「BigQuery to design a robust data architecture that ensures the availability of data in near real-time」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Mercado Libre 揭露三個區域電商容量規劃重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>區域電商 ≠ 全球電商&lt;/strong>：拉丁美洲 18 個國家、各自有獨立貨幣、稅務、物流、合規規則。容量規劃單位通常是「per country」、不是「per region」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 的市場分割、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow&lt;/a> 的跨國平台對照。&lt;/li>
&lt;li>&lt;strong>Vertex AI Search = 「搜尋」當作 ML 服務、不是 Elasticsearch&lt;/strong>：傳統電商搜尋靠 Elasticsearch / OpenSearch + 自訓 ranker、Mercado Libre 用 vendor managed Vertex AI Search、把「商品搜尋 + 推薦排序」當作 ML 黑盒。這個取捨用「不可調參」換「快速上線」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的 build vs buy、跟 &lt;a href="https://tarrragon.github.io/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 的容量規劃成本">9.C9 Spotify&lt;/a> 的 managed 轉向同類思維。&lt;/li>
&lt;li>&lt;strong>「數百萬美金 incremental revenue」是 ML 容量規劃的真實 ROI&lt;/strong>：搜尋改善 → 轉換率 → 訂單 → 收入、ML 投資的 cost 才能合理化。容量規劃不只看「能撐多大流量」、也要看「擴容能否帶業務 ROI」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的成本工程化。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 GCP 案例庫的「商業應用」深度、並提供拉丁美洲電商規模對標。Mercado Libre 是拉丁美洲最大電商（市值 600 億美金級）、業務涵蓋 18 個國家、是區域型平台的容量規劃範本。</p>
<h2 id="觀察">觀察</h2>
<p>Mercado Libre 在 GCP 的關鍵敘述（引自 <a href="https://cloud.google.com/customers/mercado-libre">Mercado Libre Customer Story</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶數</td>
          <td>1 億</td>
      </tr>
      <tr>
          <td>商品數</td>
          <td>1.5 億（3 個試點國家）</td>
      </tr>
      <tr>
          <td>業務影響</td>
          <td>數百萬美金 incremental revenue（Vertex AI Search）</td>
      </tr>
      <tr>
          <td>主要 GCP 服務</td>
          <td>Vertex AI Search、BigQuery</td>
      </tr>
      <tr>
          <td>資料即時性</td>
          <td>near real-time</td>
      </tr>
      <tr>
          <td>服務地理</td>
          <td>拉丁美洲</td>
      </tr>
  </tbody>
</table>
<p>關鍵能力：「Vertex AI Search across 150 million items in three pilot countries that is helping its 100 million customers find the products they love faster」、「BigQuery to design a robust data architecture that ensures the availability of data in near real-time」。</p>
<h2 id="判讀">判讀</h2>
<p>Mercado Libre 揭露三個區域電商容量規劃重點。</p>
<ol>
<li><strong>區域電商 ≠ 全球電商</strong>：拉丁美洲 18 個國家、各自有獨立貨幣、稅務、物流、合規規則。容量規劃單位通常是「per country」、不是「per region」。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 的市場分割、跟 <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> 的跨國平台對照。</li>
<li><strong>Vertex AI Search = 「搜尋」當作 ML 服務、不是 Elasticsearch</strong>：傳統電商搜尋靠 Elasticsearch / OpenSearch + 自訓 ranker、Mercado Libre 用 vendor managed Vertex AI Search、把「商品搜尋 + 推薦排序」當作 ML 黑盒。這個取捨用「不可調參」換「快速上線」。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 build vs buy、跟 <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 的容量規劃成本">9.C9 Spotify</a> 的 managed 轉向同類思維。</li>
<li><strong>「數百萬美金 incremental revenue」是 ML 容量規劃的真實 ROI</strong>：搜尋改善 → 轉換率 → 訂單 → 收入、ML 投資的 cost 才能合理化。容量規劃不只看「能撐多大流量」、也要看「擴容能否帶業務 ROI」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的成本工程化。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「1.5 億商品 in 3 pilot countries」是 <em>試點規模</em>、不是全平台。全平台商品總數應該更大、但案例沒揭露。</li>
<li>BigQuery「near real-time」沒指明 latency（秒級、分鐘級）。BigQuery 傳統是 minutes-level、不是 sub-second、對「即時」的定義要謹慎。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>區域電商的容量規劃是「per country × peak_factor」</strong>：不是「per region」聚合、要按國家分別規劃。每個國家自己的 Black Friday / Cyber Monday / 雙 11 / 6.18 等本地大促時間都不同。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a>。</li>
<li><strong>「商品搜尋」適合用 managed AI search</strong>：除非有自家強大的 ML team + 大量訓練資料、否則 Vertex AI Search / OpenSearch Service 等 managed 比自建 ranker 划算。</li>
<li><strong>BigQuery 是 LatAm / 新興市場數據平台的標配</strong>：能處理 PB 級資料、無需 cluster 管理、適合中等工程資源的團隊。對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 的 data 平台選型、跟 <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> 的 Redshift + Athena 對照。</li>
<li><strong>ML ROI 直接 ＝ 業務指標</strong>：transaction conversion rate、AOV、recommendation CTR 都是 ML 容量規劃的下游 KPI。</li>
</ol>
<p>跨平台等效：AWS Personalize + Redshift + Glue、Azure AI Search + Synapse 都是對等候選。差異是 vendor 整合度跟模型的可調參空間。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他大規模電商 → <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a> / <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair burst</a></li>
<li>想規劃跨國容量 → <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> + <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a></li>
<li>想做 ML feature serving → <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a></li>
<li>想做 build vs buy 決策 → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/customers/mercado-libre">Mercado Libre Customer Story</a></li>
<li><a href="https://cloud.google.com/blog/products/data-analytics/how-mercado-libre-uses-real-time-analytics-for-on-time-delivery">How Mercado Libre uses real-time analytics for on-time delivery</a></li>
</ul>
]]></content:encoded></item><item><title>Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/</guid><description>&lt;p>Amazon 可靠性設計的核心責任是把失效影響限制在局部邊界。當系統採用多租戶與大規模共享資源，隔離策略必須先於恢復策略被定義，否則任何回復動作都會變成全域風險。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>多租戶服務常見的放大路徑是「單租戶異常 → 共享資源飽和 → 全域退化」。若路由與容量都沒有明確邊界，團隊只能在事故後做整體降載，代價高且恢復慢。&lt;/p>
&lt;p>cell-based architecture 與 shuffle sharding 提供的是前置結構：先限制擴散，再談恢復。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cell 邊界&lt;/td>
 &lt;td>一個失效最多影響到哪裡&lt;/td>
 &lt;td>局部故障域&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shuffle sharding&lt;/td>
 &lt;td>熱點租戶如何避免重疊影響&lt;/td>
 &lt;td>隨機子集合隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Static stability&lt;/td>
 &lt;td>控制面失效時資料面如何維持&lt;/td>
 &lt;td>降級持續服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Constant work&lt;/td>
 &lt;td>失敗模式下是否維持固定工作量&lt;/td>
 &lt;td>防放大設計&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這組機制讓恢復策略從「全域搶救」轉為「分批收斂」。在可用性與成本取捨上，局部隔離通常比全域冗餘更可持續。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>shard contention&lt;/td>
 &lt;td>熱點是否跨 shard 擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cell error isolation ratio&lt;/td>
 &lt;td>錯誤是否被限制在局部&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recovery batch completion&lt;/td>
 &lt;td>分批恢復是否可預測&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>control-plane dependency lag&lt;/td>
 &lt;td>控制面異常是否拖累資料面&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把 sharding 當成純擴容手段會忽略隔離責任。當分片策略只服務容量，沒有對齊失效邊界，事故時仍會看到跨租戶共振。真正的設計重點是「隔離優先，擴容其次」。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要把案例轉成可執行設計，先定義 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a> 的依賴預算與共享邊界，再在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a> 驗證局部化假設。事故時的分批回復流程回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Amazon 可靠性設計的核心責任是把失效影響限制在局部邊界。當系統採用多租戶與大規模共享資源，隔離策略必須先於恢復策略被定義，否則任何回復動作都會變成全域風險。</p>
<h2 id="問題場景">問題場景</h2>
<p>多租戶服務常見的放大路徑是「單租戶異常 → 共享資源飽和 → 全域退化」。若路由與容量都沒有明確邊界，團隊只能在事故後做整體降載，代價高且恢復慢。</p>
<p>cell-based architecture 與 shuffle sharding 提供的是前置結構：先限制擴散，再談恢復。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cell 邊界</td>
          <td>一個失效最多影響到哪裡</td>
          <td>局部故障域</td>
      </tr>
      <tr>
          <td>Shuffle sharding</td>
          <td>熱點租戶如何避免重疊影響</td>
          <td>隨機子集合隔離</td>
      </tr>
      <tr>
          <td>Static stability</td>
          <td>控制面失效時資料面如何維持</td>
          <td>降級持續服務</td>
      </tr>
      <tr>
          <td>Constant work</td>
          <td>失敗模式下是否維持固定工作量</td>
          <td>防放大設計</td>
      </tr>
  </tbody>
</table>
<p>這組機制讓恢復策略從「全域搶救」轉為「分批收斂」。在可用性與成本取捨上，局部隔離通常比全域冗餘更可持續。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>shard contention</td>
          <td>熱點是否跨 shard 擴散</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>cell error isolation ratio</td>
          <td>錯誤是否被限制在局部</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>recovery batch completion</td>
          <td>分批恢復是否可預測</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>control-plane dependency lag</td>
          <td>控制面異常是否拖累資料面</td>
          <td><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把 sharding 當成純擴容手段會忽略隔離責任。當分片策略只服務容量，沒有對齊失效邊界，事故時仍會看到跨租戶共振。真正的設計重點是「隔離優先，擴容其次」。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要把案例轉成可執行設計，先定義 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a> 的依賴預算與共享邊界，再在 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> 驗證局部化假設。事故時的分批回復流程回到 <a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a>。</p>
]]></content:encoded></item><item><title>Discord：Gateway 容量事件與恢復節奏</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/</guid><description>&lt;p>這起案例的核心責任是把長連線流量恢復做成可分批節奏。容量事件若直接全量回復，容易觸發二次擁塞。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>gateway saturation&lt;/td>
 &lt;td>是否超出穩態邊界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reconnect queue growth&lt;/td>
 &lt;td>回復是否放大壓力&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>region imbalance&lt;/td>
 &lt;td>影響是否偏斜&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「長連線回復節奏」不能跨過穩態容量。主要風險是全量 reconnect 直接壓垮 gateway，讓恢復動作本身成為二次事故來源。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先定義分批回復門檻，再在 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14&lt;/a> 固化協調規則，並回寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a> 的穩態門檻。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是把長連線流量恢復做成可分批節奏。容量事件若直接全量回復，容易觸發二次擁塞。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gateway saturation</td>
          <td>是否超出穩態邊界</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>reconnect queue growth</td>
          <td>回復是否放大壓力</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>region imbalance</td>
          <td>影響是否偏斜</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「長連線回復節奏」不能跨過穩態容量。主要風險是全量 reconnect 直接壓垮 gateway，讓恢復動作本身成為二次事故來源。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先定義分批回復門檻，再在 <a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a> 固化協調規則，並回寫 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a> 的穩態門檻。</p>
]]></content:encoded></item><item><title>LinkedIn：Capacity Headroom 與 On-call 分層</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/</guid><description>&lt;p>LinkedIn 案例的核心責任是讓容量治理與 on-call 分工一起運作。高流量服務的穩定性不只靠擴容，還靠清楚的接手邏輯。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>當流量逼近上限時，技術瓶頸與協作瓶頸會同時出現。若只有容量模型，沒有分層值班，恢復節奏仍會失控。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Headroom 預算&lt;/td>
 &lt;td>何時進入風險區&lt;/td>
 &lt;td>擴容與限流門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary/Secondary/SME&lt;/td>
 &lt;td>何時由誰接手&lt;/td>
 &lt;td>升級路徑&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;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>replication latency&lt;/td>
 &lt;td>是否接近容量邊界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>on-call handoff latency&lt;/td>
 &lt;td>分層交接是否順暢&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>load-test drift&lt;/td>
 &lt;td>模型與真實壓力是否偏移&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>把容量假設寫進 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>，再把交接規則對齊 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>LinkedIn 案例的核心責任是讓容量治理與 on-call 分工一起運作。高流量服務的穩定性不只靠擴容，還靠清楚的接手邏輯。</p>
<h2 id="問題場景">問題場景</h2>
<p>當流量逼近上限時，技術瓶頸與協作瓶頸會同時出現。若只有容量模型，沒有分層值班，恢復節奏仍會失控。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Headroom 預算</td>
          <td>何時進入風險區</td>
          <td>擴容與限流門檻</td>
      </tr>
      <tr>
          <td>Primary/Secondary/SME</td>
          <td>何時由誰接手</td>
          <td>升級路徑</td>
      </tr>
      <tr>
          <td>自動化壓測</td>
          <td>模型是否貼近現況</td>
          <td>驗證循環</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>replication latency</td>
          <td>是否接近容量邊界</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>on-call handoff latency</td>
          <td>分層交接是否順暢</td>
          <td><a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12</a></td>
      </tr>
      <tr>
          <td>load-test drift</td>
          <td>模型與真實壓力是否偏移</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>把容量假設寫進 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a>，再把交接規則對齊 <a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2</a>。</p>
]]></content:encoded></item><item><title>Amazon：Static Stability 與 Constant Work Pattern</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/static-stability/" data-link-title="Static Stability" data-link-desc="控制面失效時資料面用快取的已知好配置繼續服務的設計模式">Static stability&lt;/a> 的責任是讓資料面在控制面故障時仍能維持服務。Constant work pattern 的責任是讓系統在失敗模式下的工作量與正常時相同。兩者共同保護系統在最需要穩定時不會因為自救動作而崩潰。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>控制面管理路由、配置推送、服務發現與 auto-scaling。當控制面本身失效，依賴控制面的資料面會同時進入未知狀態。最危險的放大路徑是：控制面掛掉後，資料面節點同時嘗試重新連線或重新取得配置，retry storm 把殘餘容量耗盡，資料面跟著崩潰。&lt;/p>
&lt;p>這個問題在大規模平台上尤其嚴重。節點越多，控制面恢復時的同時 pull 量越大，恢復本身就會變成新的負載來源。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Static stability&lt;/td>
 &lt;td>控制面不可用時資料面能否繼續服務&lt;/td>
 &lt;td>快取的配置必須是完整可用狀態，不能是 partial update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Constant work&lt;/td>
 &lt;td>失敗模式下的系統工作量是否跟正常時相同&lt;/td>
 &lt;td>push-based 優於 pull-based：定時推全量，不靠拉取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pre-computed fallback&lt;/td>
 &lt;td>控制面失效時是否有不需要即時計算的備援路徑&lt;/td>
 &lt;td>fallback 路徑預先建好，切換動作本身不依賴控制面&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Static stability 的實作核心是讓每個資料面節點持有控制面最後已知的好配置。當控制面恢復通訊時，節點用最新配置更新快取；當通訊中斷時，節點用快取繼續服務。這個設計要求配置快取是完整的（能獨立驅動服務），而不是差分的（需要跟控制面合併才能用）。&lt;/p>
&lt;p>Constant work pattern 的核心是讓系統無論在正常或故障狀態下都執行相同的工作量。push-based config distribution 在每個週期推送全量配置給所有節點，不管配置是否有變更。這樣在控制面恢復時，不會因為所有節點同時 pull 而產生 thundering herd。相比之下，pull-based 在正常時流量低，但控制面恢復瞬間流量暴增。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>control-plane health&lt;/td>
 &lt;td>控制面是否可用、是否在退化中&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache staleness&lt;/td>
 &lt;td>快取配置距離最後更新多久&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recovery work amplification&lt;/td>
 &lt;td>恢復過程中負載是否比正常時更高&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>data-plane autonomous duration&lt;/td>
 &lt;td>資料面在無控制面時能獨立運作多久&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>cache staleness 是 static stability 最關鍵的健康指標。當快取新鮮度超過預設門檻（取決於配置變更頻率），資料面仍能服務，但服務行為可能與最新意圖不一致。這個門檻決定了 degraded mode 的可接受時間窗。&lt;/p>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把控制面失效視為低概率事件而不做 static stability 設計，會在真正發生時暴露循環依賴。Meta 2021-10 事故中，BGP 配置變更導致控制面與資料面共用的網路路徑同時失效，而恢復工具本身也依賴這條路徑，恢復動作陷入循環等待。這個案例說明 static stability 的價值在事前設計，而非事後補救。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal&lt;/a>：static stability 讓資料面在災難期間自主運作，是 DR by design&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget&lt;/a>：控制面是最高風險的內部依賴，budget 設計要先處理控制面失效&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition&lt;/a>：degraded mode 下的穩態需要包含「控制面不可用但資料面仍服務」的定義&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/builders-library/static-stability-using-availability-zones/">Static stability using Availability Zones&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/builders-library/avoiding-insurmountable-queue-backlogs/">Avoiding insurmountable queue backlogs&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/static-stability/" data-link-title="Static Stability" data-link-desc="控制面失效時資料面用快取的已知好配置繼續服務的設計模式">Static stability</a> 的責任是讓資料面在控制面故障時仍能維持服務。Constant work pattern 的責任是讓系統在失敗模式下的工作量與正常時相同。兩者共同保護系統在最需要穩定時不會因為自救動作而崩潰。</p>
<h2 id="問題場景">問題場景</h2>
<p>控制面管理路由、配置推送、服務發現與 auto-scaling。當控制面本身失效，依賴控制面的資料面會同時進入未知狀態。最危險的放大路徑是：控制面掛掉後，資料面節點同時嘗試重新連線或重新取得配置，retry storm 把殘餘容量耗盡，資料面跟著崩潰。</p>
<p>這個問題在大規模平台上尤其嚴重。節點越多，控制面恢復時的同時 pull 量越大，恢復本身就會變成新的負載來源。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>設計約束</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Static stability</td>
          <td>控制面不可用時資料面能否繼續服務</td>
          <td>快取的配置必須是完整可用狀態，不能是 partial update</td>
      </tr>
      <tr>
          <td>Constant work</td>
          <td>失敗模式下的系統工作量是否跟正常時相同</td>
          <td>push-based 優於 pull-based：定時推全量，不靠拉取</td>
      </tr>
      <tr>
          <td>Pre-computed fallback</td>
          <td>控制面失效時是否有不需要即時計算的備援路徑</td>
          <td>fallback 路徑預先建好，切換動作本身不依賴控制面</td>
      </tr>
  </tbody>
</table>
<p>Static stability 的實作核心是讓每個資料面節點持有控制面最後已知的好配置。當控制面恢復通訊時，節點用最新配置更新快取；當通訊中斷時，節點用快取繼續服務。這個設計要求配置快取是完整的（能獨立驅動服務），而不是差分的（需要跟控制面合併才能用）。</p>
<p>Constant work pattern 的核心是讓系統無論在正常或故障狀態下都執行相同的工作量。push-based config distribution 在每個週期推送全量配置給所有節點，不管配置是否有變更。這樣在控制面恢復時，不會因為所有節點同時 pull 而產生 thundering herd。相比之下，pull-based 在正常時流量低，但控制面恢復瞬間流量暴增。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>control-plane health</td>
          <td>控制面是否可用、是否在退化中</td>
          <td><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a></td>
      </tr>
      <tr>
          <td>cache staleness</td>
          <td>快取配置距離最後更新多久</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>recovery work amplification</td>
          <td>恢復過程中負載是否比正常時更高</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>data-plane autonomous duration</td>
          <td>資料面在無控制面時能獨立運作多久</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a></td>
      </tr>
  </tbody>
</table>
<p>cache staleness 是 static stability 最關鍵的健康指標。當快取新鮮度超過預設門檻（取決於配置變更頻率），資料面仍能服務，但服務行為可能與最新意圖不一致。這個門檻決定了 degraded mode 的可接受時間窗。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把控制面失效視為低概率事件而不做 static stability 設計，會在真正發生時暴露循環依賴。Meta 2021-10 事故中，BGP 配置變更導致控制面與資料面共用的網路路徑同時失效，而恢復工具本身也依賴這條路徑，恢復動作陷入循環等待。這個案例說明 static stability 的價值在事前設計，而非事後補救。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal</a>：static stability 讓資料面在災難期間自主運作，是 DR by design</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>：控制面是最高風險的內部依賴，budget 設計要先處理控制面失效</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：degraded mode 下的穩態需要包含「控制面不可用但資料面仍服務」的定義</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/builders-library/static-stability-using-availability-zones/">Static stability using Availability Zones</a></li>
<li><a href="https://aws.amazon.com/builders-library/avoiding-insurmountable-queue-backlogs/">Avoiding insurmountable queue backlogs</a></li>
</ul>
]]></content:encoded></item><item><title>Honeycomb：Production Excellence 與 Test in Production</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/</guid><description>&lt;p>Honeycomb 團隊是 test in production 理念的主要推動者之一。Production excellence 的核心責任是把 production 觀測能力提升到可以安全驗證變更的水準。當觀測能力足夠細緻，團隊可以在真實流量下驗證行為，降低對 staging 環境的依賴。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>Staging 跟 production 之間的差異是結構性的 — 資料量不同、流量模式不同、依賴行為不同、cache 狀態不同。團隊投入大量精力維護 staging parity，但差異仍然存在，staging 通過但 production 失敗的事故反覆出現。&lt;/p>
&lt;p>Honeycomb 提出的替代思路是：與其追求 staging 完美複製 production，不如提升 production 的觀測能力，讓驗證可以安全地在 production 執行。這個思路的前提是三個能力同時到位：high-cardinality observability 能即時看見異常、feature flag 能控制變更的可見範圍、automated rollback 能在問題擴大前收回變更。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Observability readiness&lt;/td>
 &lt;td>觀測能否按 tenant / path / feature 切分&lt;/td>
 &lt;td>high-cardinality trace / structured event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Feature flag safety&lt;/td>
 &lt;td>變更可見範圍是否可控&lt;/td>
 &lt;td>dark launch + kill switch&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Progressive validation&lt;/td>
 &lt;td>每一步放量是否有即時回饋&lt;/td>
 &lt;td>canary → observe → expand 循環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rollback readiness&lt;/td>
 &lt;td>異常出現時能否自動收回&lt;/td>
 &lt;td>automated rollback on anomaly trigger&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Observability readiness 是整個流程的前提。high-cardinality tracing 讓團隊可以按 tenant、feature flag 狀態、request path 等維度切分觀測資料，在問題只影響少量使用者時就被發現。若觀測只有聚合指標（平均 latency、總 error rate），異常會被稀釋到看不見，等到聚合指標也惡化時影響已經擴大。&lt;/p>
&lt;p>Feature flag safety 控制變更的 blast radius。dark launch 讓新邏輯在 production 執行但結果不對外可見，用來驗證效能與正確性。kill switch 讓團隊在異常出現時立即關閉新邏輯，不需要等 redeploy。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>trace cardinality coverage&lt;/td>
 &lt;td>觀測維度是否足以切分異常&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>flag rollout anomaly&lt;/td>
 &lt;td>新 flag 開啟後行為是否偏離&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>production validation pass&lt;/td>
 &lt;td>驗證結果是否支持繼續放量&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback trigger count&lt;/td>
 &lt;td>自動回退是否被觸發&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把 test in production 當成「跳過 staging 測試」的簡稱會帶來嚴重風險。test in production 的安全性建立在三個前提上：觀測能力能即時看見異常、feature flag 能控制影響範圍、rollback 能在秒級生效。缺少任何一個前提就直接在 production 測試，只是把風險從 staging 搬到 production，而且 production 的失敗成本更高。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 Environment Parity&lt;/a> 評估 staging 差異的實際風險，再到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance&lt;/a> 建立 flag safety 機制。production validation 的證據回寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Honeycomb 團隊是 test in production 理念的主要推動者之一。Production excellence 的核心責任是把 production 觀測能力提升到可以安全驗證變更的水準。當觀測能力足夠細緻，團隊可以在真實流量下驗證行為，降低對 staging 環境的依賴。</p>
<h2 id="問題場景">問題場景</h2>
<p>Staging 跟 production 之間的差異是結構性的 — 資料量不同、流量模式不同、依賴行為不同、cache 狀態不同。團隊投入大量精力維護 staging parity，但差異仍然存在，staging 通過但 production 失敗的事故反覆出現。</p>
<p>Honeycomb 提出的替代思路是：與其追求 staging 完美複製 production，不如提升 production 的觀測能力，讓驗證可以安全地在 production 執行。這個思路的前提是三個能力同時到位：high-cardinality observability 能即時看見異常、feature flag 能控制變更的可見範圍、automated rollback 能在問題擴大前收回變更。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability readiness</td>
          <td>觀測能否按 tenant / path / feature 切分</td>
          <td>high-cardinality trace / structured event</td>
      </tr>
      <tr>
          <td>Feature flag safety</td>
          <td>變更可見範圍是否可控</td>
          <td>dark launch + kill switch</td>
      </tr>
      <tr>
          <td>Progressive validation</td>
          <td>每一步放量是否有即時回饋</td>
          <td>canary → observe → expand 循環</td>
      </tr>
      <tr>
          <td>Rollback readiness</td>
          <td>異常出現時能否自動收回</td>
          <td>automated rollback on anomaly trigger</td>
      </tr>
  </tbody>
</table>
<p>Observability readiness 是整個流程的前提。high-cardinality tracing 讓團隊可以按 tenant、feature flag 狀態、request path 等維度切分觀測資料，在問題只影響少量使用者時就被發現。若觀測只有聚合指標（平均 latency、總 error rate），異常會被稀釋到看不見，等到聚合指標也惡化時影響已經擴大。</p>
<p>Feature flag safety 控制變更的 blast radius。dark launch 讓新邏輯在 production 執行但結果不對外可見，用來驗證效能與正確性。kill switch 讓團隊在異常出現時立即關閉新邏輯，不需要等 redeploy。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>trace cardinality coverage</td>
          <td>觀測維度是否足以切分異常</td>
          <td><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</a></td>
      </tr>
      <tr>
          <td>flag rollout anomaly</td>
          <td>新 flag 開啟後行為是否偏離</td>
          <td><a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17</a></td>
      </tr>
      <tr>
          <td>production validation pass</td>
          <td>驗證結果是否支持繼續放量</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>rollback trigger count</td>
          <td>自動回退是否被觸發</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把 test in production 當成「跳過 staging 測試」的簡稱會帶來嚴重風險。test in production 的安全性建立在三個前提上：觀測能力能即時看見異常、feature flag 能控制影響範圍、rollback 能在秒級生效。缺少任何一個前提就直接在 production 測試，只是把風險從 staging 搬到 production，而且 production 的失敗成本更高。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先回到 <a href="/blog/backend/06-reliability/environment-parity/" data-link-title="6.15 Environment Parity 與漂移控制" data-link-desc="把 staging / preprod / prod 之間的差異視為一級風險，按漂移來源分類偵測與治理">6.15 Environment Parity</a> 評估 staging 差異的實際風險，再到 <a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance</a> 建立 flag safety 機制。production validation 的證據回寫 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.honeycomb.io/blog/observability-every-engineers-job-not-just-ops-problem">Observability: It&rsquo;s Every Engineer&rsquo;s Job, Not Just Ops&rsquo; Problem</a></li>
<li><a href="https://www.honeycomb.io/resources/getting-started/what-is-observability-engineering">What Is Observability Engineering?</a></li>
</ul>
]]></content:encoded></item><item><title>LinkedIn：Automated Load Testing 與 Capacity Forecasting</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/</guid><description>&lt;p>Automated load testing 的核心責任是把壓測從一次性活動變成持續回饋的工程流程。Capacity forecasting 的責任是用歷史流量趨勢加上壓測結果，預測什麼時候需要擴容、什麼時候可以縮減。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>大型社交平台的流量增長是漸進的，但容量不足是突然的。超過 saturation point 後 latency 會非線性惡化，從可接受的排隊延遲快速轉成級聯超時。若靠一次性壓測做容量規劃，規劃結論會隨時間漂移：流量結構改變、功能上線帶進新 workload、依賴服務的回應時間波動，都會讓上一次壓測的 saturation point 不再準確。&lt;/p>
&lt;p>LinkedIn 的做法是把壓測自動化並跑在定期排程中，讓容量預測的輸入持續更新。壓測結果直接餵給 forecasting 模型，forecasting 輸出接到 headroom alert，headroom alert 觸發擴容 review。這條鏈路讓容量決策從「每季做一次人工判斷」變成「每週自動更新、異常時才需要人介入」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Automated load test&lt;/td>
 &lt;td>saturation point 是否仍準確&lt;/td>
 &lt;td>更新後的容量基準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Traffic forecasting&lt;/td>
 &lt;td>未來 N 天的 peak load 是否會逼近上限&lt;/td>
 &lt;td>擴容時間窗預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Headroom alert&lt;/td>
 &lt;td>forecast / ceiling 比值是否超過門檻&lt;/td>
 &lt;td>自動擴容 review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity budget&lt;/td>
 &lt;td>每個服務的容量開銷是否在預算內&lt;/td>
 &lt;td>超支 justification&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Automated load test 用 production traffic replay 而非固定 scenario，讓壓測的 workload model 跟真實流量保持同步。Traffic forecasting 結合歷史流量趨勢與產品 launch 日曆，把可預期的流量事件（功能上線、促銷、季節性增長）納入預測。Headroom alert 在 forecast peak / capacity ceiling 比值超過 70-80% 時觸發，讓團隊在容量耗盡前有足夠反應窗口。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>saturation point drift&lt;/td>
 &lt;td>壓測結果是否隨時間漂移&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>headroom ratio&lt;/td>
 &lt;td>peak load 與 capacity ceiling 比值&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>forecast accuracy&lt;/td>
 &lt;td>預測與實際 peak 的偏差&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>capacity spend trend&lt;/td>
 &lt;td>容量成本是否超出預算&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>自動化壓測最常見的失真來源是 workload model 僵化。若自動化跑的是建立時的固定 scenario 而非持續更新的 traffic replay，時間一長模型就跟 production 脫鉤。脫鉤的訊號是壓測結果與 production 同時段的 latency distribution 開始偏離 — p50 / p95 / p99 的比率差異超過 20% 時，模型已需要校準。&lt;/p>
&lt;p>另一個陷阱是把 forecast 當成精確預測。Forecasting 的價值在於提早觸發 review，讓團隊有時間做擴容決策。若團隊把 forecast 當成精確數字做自動擴容，預測偏差會直接變成過度擴容或擴容不足。forecast 輸出應該驅動人工 review，而非直接觸發資源變更。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先把壓測結果接到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load testing&lt;/a> 的 workload model 校準流程，再用 headroom ratio 餵給 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界&lt;/a> 做容量預算。forecast 準確度的追蹤連到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate&lt;/a> 的 baseline 校準。&lt;/p></description><content:encoded><![CDATA[<p>Automated load testing 的核心責任是把壓測從一次性活動變成持續回饋的工程流程。Capacity forecasting 的責任是用歷史流量趨勢加上壓測結果，預測什麼時候需要擴容、什麼時候可以縮減。</p>
<h2 id="問題場景">問題場景</h2>
<p>大型社交平台的流量增長是漸進的，但容量不足是突然的。超過 saturation point 後 latency 會非線性惡化，從可接受的排隊延遲快速轉成級聯超時。若靠一次性壓測做容量規劃，規劃結論會隨時間漂移：流量結構改變、功能上線帶進新 workload、依賴服務的回應時間波動，都會讓上一次壓測的 saturation point 不再準確。</p>
<p>LinkedIn 的做法是把壓測自動化並跑在定期排程中，讓容量預測的輸入持續更新。壓測結果直接餵給 forecasting 模型，forecasting 輸出接到 headroom alert，headroom alert 觸發擴容 review。這條鏈路讓容量決策從「每季做一次人工判斷」變成「每週自動更新、異常時才需要人介入」。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Automated load test</td>
          <td>saturation point 是否仍準確</td>
          <td>更新後的容量基準</td>
      </tr>
      <tr>
          <td>Traffic forecasting</td>
          <td>未來 N 天的 peak load 是否會逼近上限</td>
          <td>擴容時間窗預測</td>
      </tr>
      <tr>
          <td>Headroom alert</td>
          <td>forecast / ceiling 比值是否超過門檻</td>
          <td>自動擴容 review</td>
      </tr>
      <tr>
          <td>Capacity budget</td>
          <td>每個服務的容量開銷是否在預算內</td>
          <td>超支 justification</td>
      </tr>
  </tbody>
</table>
<p>Automated load test 用 production traffic replay 而非固定 scenario，讓壓測的 workload model 跟真實流量保持同步。Traffic forecasting 結合歷史流量趨勢與產品 launch 日曆，把可預期的流量事件（功能上線、促銷、季節性增長）納入預測。Headroom alert 在 forecast peak / capacity ceiling 比值超過 70-80% 時觸發，讓團隊在容量耗盡前有足夠反應窗口。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>saturation point drift</td>
          <td>壓測結果是否隨時間漂移</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2</a></td>
      </tr>
      <tr>
          <td>headroom ratio</td>
          <td>peak load 與 capacity ceiling 比值</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>forecast accuracy</td>
          <td>預測與實際 peak 的偏差</td>
          <td><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13</a></td>
      </tr>
      <tr>
          <td>capacity spend trend</td>
          <td>容量成本是否超出預算</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>自動化壓測最常見的失真來源是 workload model 僵化。若自動化跑的是建立時的固定 scenario 而非持續更新的 traffic replay，時間一長模型就跟 production 脫鉤。脫鉤的訊號是壓測結果與 production 同時段的 latency distribution 開始偏離 — p50 / p95 / p99 的比率差異超過 20% 時，模型已需要校準。</p>
<p>另一個陷阱是把 forecast 當成精確預測。Forecasting 的價值在於提早觸發 review，讓團隊有時間做擴容決策。若團隊把 forecast 當成精確數字做自動擴容，預測偏差會直接變成過度擴容或擴容不足。forecast 輸出應該驅動人工 review，而非直接觸發資源變更。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先把壓測結果接到 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2 load testing</a> 的 workload model 校準流程，再用 headroom ratio 餵給 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量與成本邊界</a> 做容量預算。forecast 準確度的追蹤連到 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a> 的 baseline 校準。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.linkedin.com/content/engineering/en-us/blog/2019/eliminating-toil-with-fully-automated-load-testing">Eliminating toil with fully automated load testing</a></li>
<li>（背景脈絡）<a href="https://engineering.linkedin.com/performance/taming-database-replication-latency-capacity-planning">Taming Database Replication Latency by Capacity Planning</a></li>
</ul>
]]></content:encoded></item><item><title>Log Schema</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/</guid><description>&lt;p>Log schema 的核心概念是「用穩定欄位描述 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 事件」。結構化 log 應包含時間、等級、服務名稱、事件名稱、錯誤類型、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a>、tenant、資源 ID 與處理結果。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Log schema 是可觀測性的事件明細層。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics&lt;/a> 提供趨勢，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 提供跨服務路徑，log 提供單一事件的上下文與細節。三者透過共享欄位（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、correlation id）互相連結。&lt;/p>
&lt;p>Log schema 的穩定性決定了查詢的效率 — 跨服務使用不同的欄位名稱記錄同一概念時，查詢需要窮舉所有變體。見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema 與搜尋規劃&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 log schema 的訊號是事故時查詢依賴全文搜尋或逐台機器翻查。Checkout 失敗時，穩定欄位讓團隊用 order_id、payment_id、request_id 在秒級內追出同一流程的所有紀錄。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Log schema 要控制欄位名稱（跨服務統一）、錯誤分類（error type / error code 有界而非 free-form message）、敏感資料遮罩（API key / token / PII 在寫入前 redact）與索引成本（高 cardinality 欄位不全部建索引）。高流量服務還要管理 log level、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 與查詢成本。&lt;/p></description><content:encoded><![CDATA[<p>Log schema 的核心概念是「用穩定欄位描述 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 事件」。結構化 log 應包含時間、等級、服務名稱、事件名稱、錯誤類型、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a>、tenant、資源 ID 與處理結果。</p>
<h2 id="概念位置">概念位置</h2>
<p>Log schema 是可觀測性的事件明細層。<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics</a> 提供趨勢，<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 提供跨服務路徑，log 提供單一事件的上下文與細節。三者透過共享欄位（<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、correlation id）互相連結。</p>
<p>Log schema 的穩定性決定了查詢的效率 — 跨服務使用不同的欄位名稱記錄同一概念時，查詢需要窮舉所有變體。見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema 與搜尋規劃</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 log schema 的訊號是事故時查詢依賴全文搜尋或逐台機器翻查。Checkout 失敗時，穩定欄位讓團隊用 order_id、payment_id、request_id 在秒級內追出同一流程的所有紀錄。</p>
<h2 id="設計責任">設計責任</h2>
<p>Log schema 要控制欄位名稱（跨服務統一）、錯誤分類（error type / error code 有界而非 free-form message）、敏感資料遮罩（API key / token / PII 在寫入前 redact）與索引成本（高 cardinality 欄位不全部建索引）。高流量服務還要管理 log level、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a>、<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 與查詢成本。</p>
]]></content:encoded></item><item><title>Aurora 多 cluster 按業務切分：微服務私有 store、blast radius 隔離與 fleet 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/multi-cluster-business-split/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/multi-cluster-business-split/</guid><description>&lt;p>把所有服務的資料塞進一個大 Aurora cluster，平時運維最省事，直到某一天：報表服務跑了一個沒索引的聚合 query、佔滿 connection 與 IOPS、結帳服務跟著變慢、整個平台一起卡。問題的根源是「不相關的業務共用同一個 cluster、彼此沒有隔離」，那個 query 只是觸發點。多 cluster 按業務切分要回答的是：哪些業務該各自獨立 cluster、哪些可以共用、切分後 fleet 怎麼維持治理一致。&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>）— 而是 cluster 邊界劃分與多 cluster 治理的實作層教學。&lt;/p>
&lt;h2 id="共用大-cluster-的根本問題blast-radius">共用大 cluster 的根本問題：blast radius&lt;/h2>
&lt;p>單一大 cluster 把多個業務的失敗耦合在一起。一個業務的異常會透過共用資源外溢到其他業務：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>資源競爭&lt;/strong>：connection pool、CPU、IOPS、buffer cache 共用，一個業務的尖峰擠壓其他業務&lt;/li>
&lt;li>&lt;strong>failure blast radius&lt;/strong>：cluster 故障 / 升級 / schema 變更鎖表，影響所有掛在上面的業務&lt;/li>
&lt;li>&lt;strong>容量規劃糾纏&lt;/strong>：要為「所有業務尖峰的總和」規劃容量，無法針對單一業務調整&lt;/li>
&lt;li>&lt;strong>schema change 互相牽制&lt;/strong>：一個業務的 migration 鎖表、其他業務跟著受影響&lt;/li>
&lt;/ul>
&lt;p>按業務切 cluster 的核心價值是把這些耦合切開——每個 cluster 的故障、容量、變更只影響自己的業務範圍。&lt;/p>
&lt;h2 id="切分判斷維度">切分判斷維度&lt;/h2>
&lt;p>不是「每個服務都該有自己的 cluster」（那會走向另一個極端：cluster 數爆炸、運維 surface 失控）。切分依以下維度判斷：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>傾向獨立 cluster&lt;/th>
 &lt;th>可共用 cluster&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>負載差異大、尖峰時段錯開&lt;/td>
 &lt;td>負載相近、可一起規劃容量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障容忍&lt;/td>
 &lt;td>不能被別的業務拖垮&lt;/td>
 &lt;td>可接受共命運&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>schema 變更頻率&lt;/td>
 &lt;td>高頻 migration、不想牽制別人&lt;/td>
 &lt;td>低頻、變更少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>合規邊界&lt;/td>
 &lt;td>資料需獨立隔離（PCI / 個資分艙）&lt;/td>
 &lt;td>無特殊合規隔離需求&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>9.C23 Netflix&lt;/code> 是這個判斷的 case anchor：Netflix 把過往多套不同 &lt;em>種類&lt;/em> 的關聯式 DB（PostgreSQL / MySQL / Oracle）整合到 Aurora、效能提升最高 75%、成本下降 28%；但整合的是「DB 種類 / 運維 surface」，&lt;em>不是&lt;/em> 把所有資料塞進一個 cluster——Netflix 的微服務各自擁有自己的 Aurora cluster、彼此不共用。兩件事同時成立：減少 DB &lt;em>技術種類&lt;/em> 降低運維知識負擔、同時維持 &lt;em>per-service cluster&lt;/em> 隔離 blast radius。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：Netflix 的「+75% 效能 / -28% 成本」是跨多 workload 的最大改善幅度、非每個 workload 都 +75%（case 原文已標明）；且 Netflix 數據層遠不止 Aurora（還有 Cassandra / EVCache / Iceberg），Aurora 承擔的是需要 ACID 的 OLTP。引用時不可外推成「整合到 Aurora 就 +75%」。&lt;/p>&lt;/blockquote>
&lt;h2 id="兩種切分哲學的對照">兩種切分哲學的對照&lt;/h2>
&lt;p>大規模平台的 cluster 切分沒有單一正解，光譜兩端各有代表：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>per-service 私有 store（Netflix 式）&lt;/strong>：每個微服務一個 Aurora cluster、容量規劃變成「每個服務各自規劃」、跨服務 contention 變成 &lt;em>網路議題&lt;/em> 而非 &lt;em>DB lock 議題&lt;/em>&lt;/li>
&lt;li>&lt;strong>高度 consolidation&lt;/strong>：少數大 cluster 承載多業務、運維實例少、但 blast radius 大&lt;/li>
&lt;/ul>
&lt;p>實務多落在中間：核心 / 高關鍵 / 合規敏感業務各自獨立 cluster，低關鍵性的內部服務可數個共用一個 cluster。判斷的是「這群業務能不能接受共命運」。&lt;/p>
&lt;h2 id="fleet-治理切分後的一致性">Fleet 治理：切分後的一致性&lt;/h2>
&lt;p>切成多 cluster 後，運維 surface 從「一個 cluster」變成「N 個 cluster」。若沒有治理一致性，N 個 cluster 各自飄移會比一個大 cluster 更難維護。fleet 治理要把以下標準化：&lt;/p></description><content:encoded><![CDATA[<p>把所有服務的資料塞進一個大 Aurora cluster，平時運維最省事，直到某一天：報表服務跑了一個沒索引的聚合 query、佔滿 connection 與 IOPS、結帳服務跟著變慢、整個平台一起卡。問題的根源是「不相關的業務共用同一個 cluster、彼此沒有隔離」，那個 query 只是觸發點。多 cluster 按業務切分要回答的是：哪些業務該各自獨立 cluster、哪些可以共用、切分後 fleet 怎麼維持治理一致。</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>）— 而是 cluster 邊界劃分與多 cluster 治理的實作層教學。</p>
<h2 id="共用大-cluster-的根本問題blast-radius">共用大 cluster 的根本問題：blast radius</h2>
<p>單一大 cluster 把多個業務的失敗耦合在一起。一個業務的異常會透過共用資源外溢到其他業務：</p>
<ul>
<li><strong>資源競爭</strong>：connection pool、CPU、IOPS、buffer cache 共用，一個業務的尖峰擠壓其他業務</li>
<li><strong>failure blast radius</strong>：cluster 故障 / 升級 / schema 變更鎖表，影響所有掛在上面的業務</li>
<li><strong>容量規劃糾纏</strong>：要為「所有業務尖峰的總和」規劃容量，無法針對單一業務調整</li>
<li><strong>schema change 互相牽制</strong>：一個業務的 migration 鎖表、其他業務跟著受影響</li>
</ul>
<p>按業務切 cluster 的核心價值是把這些耦合切開——每個 cluster 的故障、容量、變更只影響自己的業務範圍。</p>
<h2 id="切分判斷維度">切分判斷維度</h2>
<p>不是「每個服務都該有自己的 cluster」（那會走向另一個極端：cluster 數爆炸、運維 surface 失控）。切分依以下維度判斷：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>傾向獨立 cluster</th>
          <th>可共用 cluster</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>業務關鍵性</td>
          <td>核心交易（結帳、帳本）需隔離保護</td>
          <td>內部工具、低關鍵性服務可共用</td>
      </tr>
      <tr>
          <td>負載形狀</td>
          <td>負載差異大、尖峰時段錯開</td>
          <td>負載相近、可一起規劃容量</td>
      </tr>
      <tr>
          <td>故障容忍</td>
          <td>不能被別的業務拖垮</td>
          <td>可接受共命運</td>
      </tr>
      <tr>
          <td>schema 變更頻率</td>
          <td>高頻 migration、不想牽制別人</td>
          <td>低頻、變更少</td>
      </tr>
      <tr>
          <td>合規邊界</td>
          <td>資料需獨立隔離（PCI / 個資分艙）</td>
          <td>無特殊合規隔離需求</td>
      </tr>
  </tbody>
</table>
<p><code>9.C23 Netflix</code> 是這個判斷的 case anchor：Netflix 把過往多套不同 <em>種類</em> 的關聯式 DB（PostgreSQL / MySQL / Oracle）整合到 Aurora、效能提升最高 75%、成本下降 28%；但整合的是「DB 種類 / 運維 surface」，<em>不是</em> 把所有資料塞進一個 cluster——Netflix 的微服務各自擁有自己的 Aurora cluster、彼此不共用。兩件事同時成立：減少 DB <em>技術種類</em> 降低運維知識負擔、同時維持 <em>per-service cluster</em> 隔離 blast radius。</p>
<blockquote>
<p><strong>Scope warning</strong>：Netflix 的「+75% 效能 / -28% 成本」是跨多 workload 的最大改善幅度、非每個 workload 都 +75%（case 原文已標明）；且 Netflix 數據層遠不止 Aurora（還有 Cassandra / EVCache / Iceberg），Aurora 承擔的是需要 ACID 的 OLTP。引用時不可外推成「整合到 Aurora 就 +75%」。</p></blockquote>
<h2 id="兩種切分哲學的對照">兩種切分哲學的對照</h2>
<p>大規模平台的 cluster 切分沒有單一正解，光譜兩端各有代表：</p>
<ul>
<li><strong>per-service 私有 store（Netflix 式）</strong>：每個微服務一個 Aurora cluster、容量規劃變成「每個服務各自規劃」、跨服務 contention 變成 <em>網路議題</em> 而非 <em>DB lock 議題</em></li>
<li><strong>高度 consolidation</strong>：少數大 cluster 承載多業務、運維實例少、但 blast radius 大</li>
</ul>
<p>實務多落在中間：核心 / 高關鍵 / 合規敏感業務各自獨立 cluster，低關鍵性的內部服務可數個共用一個 cluster。判斷的是「這群業務能不能接受共命運」。</p>
<h2 id="fleet-治理切分後的一致性">Fleet 治理：切分後的一致性</h2>
<p>切成多 cluster 後，運維 surface 從「一個 cluster」變成「N 個 cluster」。若沒有治理一致性，N 個 cluster 各自飄移會比一個大 cluster 更難維護。fleet 治理要把以下標準化：</p>
<ul>
<li><strong>配置一致</strong>：engine 版本、parameter group、backup 策略、加密設定用 IaC 統一管理，避免逐個手調漂移</li>
<li><strong>監控一致</strong>：每個 cluster 同一套 CloudWatch alarm 基線（connection / replication lag / CPU / IOPS），不是只盯總量</li>
<li><strong>升級協調</strong>：major version 升級分批跨 fleet，不是一次全升（也不是放任各 cluster 版本散落）</li>
<li><strong>成本歸屬</strong>：按 cluster / 業務 tag 切成本，讓每個業務看見自己的 DB 成本</li>
</ul>
<p>這層治理對應 <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 的 fleet 治理段</a>——讀副本 fleet 與多 cluster fleet 共用「N 個實例如何維持治理一致」的方法。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的踩雷：</p>
<h4 id="case-1共用大-cluster報表-query-拖垮交易">Case 1：共用大 cluster、報表 query 拖垮交易</h4>
<p>分析 / 報表 workload 跟核心交易共用 cluster、一個重 query 佔滿資源、交易延遲飆高。修法：分析類 workload 切到獨立 cluster 或獨立 read replica；核心交易的 cluster 不混入不可控的分析查詢。</p>
<h4 id="case-2cluster-切太細運維-surface-爆炸">Case 2：cluster 切太細、運維 surface 爆炸</h4>
<p>矯枉過正、每個小服務都獨立 cluster、結果幾十個 cluster 各自飄移、升級與監控成本失控。修法：低關鍵性、負載相近、可共命運的服務合併共用 cluster；切分以「blast radius 需求」為準，不是「每個服務都要」。</p>
<h4 id="case-3切分了-cluster-但沒切分-fleet-治理">Case 3：切分了 cluster 但沒切分 fleet 治理</h4>
<p>多 cluster 各自手調 parameter group、版本散落、backup 策略不一、出事才發現某個 cluster 設定漂移。修法：fleet 配置用 IaC 統一、監控基線一致、升級分批協調。</p>
<h4 id="case-4跨-cluster-交易需求才發現切錯邊界">Case 4：跨 cluster 交易需求才發現切錯邊界</h4>
<p>把本該強一致綁在一起的資料切到不同 cluster、結果需要跨 cluster 交易（Aurora 不提供跨 cluster transaction）、application 層自己補償、複雜又易錯。修法：cluster 邊界要對齊 transaction boundary——必須在同一個交易內一起成功失敗的資料，放同一 cluster（對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>）。這是切分前就要確認的邊界，切錯後重切成本高。</p>
<p><strong>Anti-recommendation</strong>：團隊規模小、服務少、無合規隔離需求、且負載總量單一 cluster 撐得住 → 不要預先切成多 cluster；多 cluster 的治理成本只在「blast radius 隔離 / 合規分艙 / 負載差異大」真正需要時才值得。從少到多容易，從多合併回少要資料遷移。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>每個 cluster 獨立的 CloudWatch 基線：<code>DatabaseConnections</code> / <code>CPUUtilization</code> / <code>AuroraReplicaLag</code> / IOPS</li>
<li>跨 fleet 的成本 dashboard：按 cluster / 業務 tag 歸屬，看哪個業務的 DB 成本成長最快</li>
<li>blast radius 演練：定期確認單一 cluster 故障不會外溢到其他業務（混沌測試）</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 cluster 數量 / 容量數字；切分維度與治理項屬通用平台工程 + Netflix consolidation 的架構訊號。</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/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 service decomposition。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="cluster-邊界-vs-微服務邊界">cluster 邊界 vs 微服務邊界</h3>
<p>多 cluster 切分常跟微服務拆分一起發生，但兩者不必一一對應。一個微服務可以擁有一個 cluster（Netflix 式私有 store），數個低關鍵微服務也可共用一個 cluster。判斷錨點是 transaction boundary 與 blast radius，不是「服務數 = cluster 數」。當切分壓力其實來自「不同資料模型」而非「隔離需求」，可能該考慮的是 polyglot persistence（OLTP 用 Aurora、KV 用 DynamoDB、analytics 用數倉），而非切更多 Aurora cluster。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<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> — fleet 治理方法共用、讀副本 fleet 與多 cluster fleet 同源</li>
<li><a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">cross-az-failover-rto</a> — 每個 cluster 的 failover 行為、blast radius 隔離後各自獨立</li>
<li><a href="/blog/backend/01-database/vendors/aurora/serverless-v2-scaling/" data-link-title="Aurora Serverless v2 適用判斷：ACU 自動擴縮、混合 cluster 與何時不該用" data-link-desc="Aurora Serverless v2 不是「比較便宜的 Aurora」；本文展開 ACU 計費粒度、秒級自動擴縮機制、min/max ACU 設定、serverless 與 provisioned 同 cluster 混用，以及穩定高負載下 serverless 反而更貴的成本 crossover 邊界">serverless-v2-scaling</a> — 低關鍵 / 間歇負載的 cluster 可用 serverless 降離峰成本</li>
<li><a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary</a> — cluster 邊界對齊狀態 ownership</li>
<li>替代路由：切分壓力來自資料模型差異 → polyglot persistence、回 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</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> 互引：DB 種類 consolidation + per-service cluster 隔離雙重成立的架構</li>
</ul>
]]></content:encoded></item><item><title>DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 &lt;em>誤判&lt;/em>。GSI 多到 cost 超過 base table 通常是 &lt;em>主 PK 沒設計好&lt;/em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB workload 適配判讀（基本 4 軸）&lt;/strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。本文聚焦 GSI / LSI 補位操作層、是 &lt;em>已選 DynamoDB + access pattern 已穩定&lt;/em> 的 schema 設計議題。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異&lt;/h2>
&lt;p>DynamoDB 的兩種 secondary index 解的問題不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>GSI（Global Secondary Index）&lt;/th>
 &lt;th>LSI（Local Secondary Index）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Partition&lt;/td>
 &lt;td>獨立 partition、可選新 PK + SK&lt;/td>
 &lt;td>同 base table partition、同 PK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>建立時機&lt;/td>
 &lt;td>隨時可加 / 移除&lt;/td>
 &lt;td>只能在 create table 時定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency&lt;/td>
 &lt;td>只支援 eventual read&lt;/td>
 &lt;td>支援 strongly consistent read&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity&lt;/td>
 &lt;td>獨立 RCU/WCU、按 base 主表 write 同步收&lt;/td>
 &lt;td>共享 base table capacity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數量上限&lt;/td>
 &lt;td>vendor 規格、需 cross-verify AWS doc&lt;/td>
 &lt;td>vendor 規格、需 cross-verify&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用場景&lt;/td>
 &lt;td>跨 PK 查詢、需求變動&lt;/td>
 &lt;td>同 PK 內不同 SK + 需 strong read&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 <em>誤判</em>。GSI 多到 cost 超過 base table 通常是 <em>主 PK 沒設計好</em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。</p>
<blockquote>
<p><strong>DynamoDB workload 適配判讀（基本 4 軸）</strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。本文聚焦 GSI / LSI 補位操作層、是 <em>已選 DynamoDB + access pattern 已穩定</em> 的 schema 設計議題。</p></blockquote>
<h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異</h2>
<p>DynamoDB 的兩種 secondary index 解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>GSI（Global Secondary Index）</th>
          <th>LSI（Local Secondary Index）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition</td>
          <td>獨立 partition、可選新 PK + SK</td>
          <td>同 base table partition、同 PK</td>
      </tr>
      <tr>
          <td>建立時機</td>
          <td>隨時可加 / 移除</td>
          <td>只能在 create table 時定義</td>
      </tr>
      <tr>
          <td>Consistency</td>
          <td>只支援 eventual read</td>
          <td>支援 strongly consistent read</td>
      </tr>
      <tr>
          <td>Capacity</td>
          <td>獨立 RCU/WCU、按 base 主表 write 同步收</td>
          <td>共享 base table capacity</td>
      </tr>
      <tr>
          <td>數量上限</td>
          <td>vendor 規格、需 cross-verify AWS doc</td>
          <td>vendor 規格、需 cross-verify</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>跨 PK 查詢、需求變動</td>
          <td>同 PK 內不同 SK + 需 strong read</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>Scope warning</strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。</p></blockquote>
<p><strong>Projection type</strong> 決定 GSI 儲存哪些 attribute：</p>
<ul>
<li><code>KEYS_ONLY</code>：只存 PK + SK + base key、最省 storage、但讀取後通常還要回 base table 撈 attribute</li>
<li><code>INCLUDE</code>：除了 key、再存指定的 attribute；常用 sweet spot、storage 跟 query 效率平衡</li>
<li><code>ALL</code>：複製 base table 所有 attribute；最方便、最貴</li>
</ul>
<p>讀路徑差異：</p>
<ul>
<li>GSI eventual read：跨 partition、不支援 strong；base table write → GSI replication 通常 &lt; 1s 但無 SLA</li>
<li>LSI strong read：同 partition quorum 內成立、read-your-write 場景適用</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>。</p>
<h2 id="dax-作為讀峰值補位">DAX 作為讀峰值補位</h2>
<p>DAX（DynamoDB Accelerator）不是 GSI / LSI 同層方案、不是 DynamoDB 預設配置、是「讀峰值持續高時的補位」。寫進你的設計前先看觸發條件：</p>
<p><strong><code>9.C29 Lemino</code> 揭露</strong>（case fact）：「DAX 是 DynamoDB 讀 cache 的標準解法」、觸發條件是「當讀峰值持續高、加 DAX 減少 DynamoDB 讀次數、降低成本」（熱門節目首播時段、共用 metadata）。Lemino 是 case 直接揭露使用 DAX。</p>
<p><strong><code>9.C19 Capcom</code> 是判讀層 derive、不是 case fact</strong>：原 finding 從「single-digit ms」latency 反推 Capcom 必須用 sub-region cache + DynamoDB DAX、不能單靠 DynamoDB；但 <code>9.C19</code> case <em>沒有公開揭露</em> 使用 DAX。引用 Capcom 時要明示「DAX 是作者判讀層推論、Capcom 沒公開使用」、避免把推論寫成 case 揭露。</p>
<p><strong>跟 GSI / LSI 的職責分離</strong>：</p>
<ul>
<li>GSI / LSI 解「無法用主 PK 查」的問題（access pattern 補位）</li>
<li>DAX 解「同 query 重複打 DynamoDB 太貴或太慢」的問題（讀路徑加速）</li>
<li>兩者不互斥、但解不同問題；不要把 DAX 當 GSI 替代品</li>
</ul>
<p><strong>DAX 適用觸發條件</strong>：</p>
<ul>
<li>讀峰值持續高（熱門節目 / 共用 leaderboard / 全平台共享 metadata / read:write ratio &gt; 10:1）</li>
<li>cache 命中率可預期高（重複讀同一組 key）</li>
</ul>
<p><strong>DAX 不適用情境</strong>：</p>
<ul>
<li>寫密集 workload（cache invalidation 開銷 &gt; cache 收益）</li>
<li>每次讀都不同 key（cache hit rate &lt; 30%、加 DAX 等於白花錢）</li>
<li>read-your-write 場景（DAX 仍是 eventual cache、staleness 視 cache TTL 而定）</li>
</ul>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 補位到 DAX 評估的 6 步流程。</p>
<h4 id="step-1標記最小成本路徑">Step 1：標記最小成本路徑</h4>
<p>每個 access pattern 標記能用最便宜路徑解：</p>
<ul>
<li>能用主表 PK/SK 直接 <code>GetItem</code> / <code>Query</code> → 主表（最便宜）</li>
<li>同 PK 內不同 SK 排序 + 需要 strong read → LSI（同 partition、strong）</li>
<li>跨 PK 或 base table 已建好 → GSI（額外 storage + WCU）</li>
</ul>
<h4 id="step-2選-lsi-還是-gsi">Step 2：選 LSI 還是 GSI</h4>
<p>LSI 只能在 create table 時定義、不能後加。team 經常踩雷：上線後想加 strongly consistent 索引、發現只能重建 table。建 table 前列完 access pattern、不確定走 GSI 不走 LSI 是保守選擇（GSI 隨時可加可移）。</p>
<h4 id="step-3projection-設計">Step 3：projection 設計</h4>
<p>每個 GSI 單獨設 projection、不要全用 <code>ALL</code>：</p>
<ul>
<li>query 只要回 key → <code>KEYS_ONLY</code></li>
<li>query 需要常見 3-5 個欄位 → <code>INCLUDE</code>（列出實際 column、storage 跟 query 效率平衡）</li>
<li>用 GSI 直接顯示資料（不回 base table） → <code>ALL</code>（storage 跟 WCU 都翻倍、慎用）</li>
</ul>
<h4 id="step-4sparse-index-pattern">Step 4：sparse index pattern</h4>
<p>GSI PK 只在某 attribute 存在時填、自動「只索引子集」、節省 storage：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">write_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">status</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">item</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span> <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="n">status</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># sparse index: 只有 active order 進 GSI</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;active&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;STATUS#active&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1SK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">order_id</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span><span class="n">Item</span><span class="o">=</span><span class="n">item</span><span class="p">)</span></span></span></code></pre></div><p>GSI1 只索引 active order、archive order 不進 GSI。當 active order 是 10%、storage 節省約 90%。</p>
<blockquote>
<p><strong>Scope warning</strong>：「50-90% storage 節省」具體節省比例屬通用工程估算、依 active subset 比例變動、case 未揭露 sparse index 具體數字。</p></blockquote>
<h4 id="step-5驗證點">Step 5：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">query</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">KeyConditionExpression</span><span class="o">=</span><span class="n">Key</span><span class="p">(</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eq</span><span class="p">(</span><span class="s2">&#34;STATUS#active&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">IndexName</span><span class="o">=</span><span class="s2">&#34;GSI1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ReturnConsumedCapacity</span><span class="o">=</span><span class="s2">&#34;INDEXES&#34;</span>  <span class="c1"># 看每個 query 走 GSI 還是主表</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;ConsumedCapacity&#34;</span><span class="p">])</span></span></span></code></pre></div><p>CloudWatch GSI metric：看每個 GSI 的 WCU usage 跟主表的比例；GSI WCU &gt; base table WCU 通常是設計訊號。</p>
<h4 id="step-6dax-評估">Step 6：DAX 評估</h4>
<p>讀峰值持續高 + cache hit rate 可預期、才加 DAX；不要把 DAX 當預設配置（Lemino 揭露的觸發條件）。先觀察 base 路徑的 read pattern、判斷 cache hit rate 預期值、再決定加 DAX。</p>
<p><strong>Rollback boundary</strong>：GSI 可隨時刪、但 deletion 是 async 且不可逆；建議先 application 切回 base table query、觀察 1 週再刪 GSI。DAX 可隨時 detach、application 端把 DAX endpoint 換回 DynamoDB endpoint 即可。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>7 個 production 常見踩雷：</p>
<h4 id="case-1gsi-寫入-throttle-拖累主表-write">Case 1：GSI 寫入 throttle 拖累主表 write</h4>
<p>GSI 用了集中型 PK（如 <code>STATUS#active</code> 所有 active order 集中）、單 partition 上限 1000 WCU 撞牆、GSI replication 失敗、主表 write retry、整體 latency 上升。修法：GSI PK 設計獨立 review、不可繼承主表 PK 的均勻假設（base PK 均勻 ≠ GSI PK 均勻）；GSI PK 也要做 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition key 均勻度判讀</a>。</p>
<h4 id="case-2gsi-eventual-read-餵錯資料">Case 2：GSI eventual read 餵錯資料</h4>
<p>application 用 GSI 讀「user 最新 status」、code 假設 strong 一致；實際 100-500ms staleness 導致 UI 顯示舊狀態。修法：read-your-write 場景改回主表 query（主表支援 strong）、或加 application-side write-through cache。</p>
<blockquote>
<p><strong>Scope warning</strong>：「100-500ms staleness」具體數字屬通用工程估算、case 未揭露 GSI replication latency 具體 p99 數字。</p></blockquote>
<h4 id="case-3projection-all-把-cost-翻倍">Case 3：projection ALL 把 cost 翻倍</h4>
<p>圖省事所有 GSI 用 <code>ALL</code>、實際 query 只需要 3 個 column；storage + WCU 都浪費。修法：每個 GSI 單獨設 projection、<code>INCLUDE</code> 列出實際 column；只在「用 GSI 直接顯示資料、不回主表」場景才用 <code>ALL</code>。</p>
<blockquote>
<p><strong>Scope warning</strong>：「cost 翻 3 倍」具體數字屬通用工程估算、case 未揭露具體 cost ratio。</p></blockquote>
<h4 id="case-4lsi-用完了才發現要的是-gsi">Case 4：LSI 用完了才發現要的是 GSI</h4>
<p>LSI 上限受 vendor 規格限制（建議 cross-verify AWS doc 當前數字）且建 table 時定、半年後想加 strongly consistent 索引發現要重建 table。修法：建 table 前列完 access pattern、不確定就走 GSI（隨時可加可移）；LSI 留給「明確需要同 PK + strong read」場景。</p>
<h4 id="case-5gsi-反向-scan-取代-query">Case 5：GSI 反向 scan 取代 query</h4>
<p>application 用 GSI 做 <code>Scan</code> 而非 <code>Query</code>、全 GSI 掃過去、cost 跟 latency 都炸。修法：<code>Scan</code> 是 <em>程式碼錯誤訊號</em>、不是 capacity 不夠；review code 看 GSI 為什麼沒被當 query 路徑用、通常是 GSI PK 設計沒對齊 access pattern。</p>
<h4 id="case-6把-dax-當預設配置">Case 6：把 DAX 當預設配置</h4>
<p>寫密集 workload / cache hit rate 低的場景加 DAX、cache invalidation 成本超過 cache 收益、cost 上升 latency 沒降。修法：DAX 是「讀峰值持續高」的補位、不是預設（Lemino 揭露的觸發條件、Capcom 是 derive 不是 case fact）；先觀察 read pattern + 評估 cache hit rate 預期、再決定。</p>
<h4 id="case-7gsi-capacity-mode-跟-base-table-不一致">Case 7：GSI capacity mode 跟 base table 不一致</h4>
<p>GSI 的 capacity mode 跟 base table 是 <em>獨立</em> 設定、不會自動繼承 — base table 是 provisioned + auto-scaling、開新 GSI 預設仍是 provisioned 但 WCU / RCU 預設值跟 base table 不同步、或誤把某個 GSI 切 on-demand 而 base table 維持 provisioned、實際 production 寫入 throttle / 成本失衡都會出現。屬通用工程議題、case 未直接揭露具體 mode 錯配狀況。</p>
<p>徵兆：</p>
<ul>
<li>Base table <code>ConsumedWriteCapacityUnits</code> 健康、卻看到 GSI <code>WriteThrottleEvents</code> 持續觸發、application 端寫入 latency p99 拉高</li>
<li>GSI 切 on-demand 後成本「不知為何」翻 X 倍、查 Cost Explorer 才發現 GSI WCU 計費跟 base table 的 provisioned 是完全不同帳單路徑</li>
<li>Auto-scaling policy 只設了 base table、GSI 沒設、流量上來時 base table 自動擴、GSI 卻 throttle</li>
</ul>
<p>修法：</p>
<ul>
<li>建 GSI 時把 capacity mode 當成獨立決策、不要假設「base 怎麼設、GSI 跟著走」</li>
<li>流量穩定 workload 同時把 base + GSI 都設 provisioned + auto-scaling、auto-scaling target 對齊</li>
<li>Spiky workload 改 on-demand 時整批切（base table + 全部 GSI 同時切）、避免單側切換造成 partial throttle</li>
<li>CloudWatch alarm 對每個 GSI 獨立設 <code>WriteThrottleEvents</code> / <code>ReadThrottleEvents</code>、不要只盯 base table</li>
<li>詳細 mode 切換時機看 sibling <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">on-demand vs provisioned</a></li>
</ul>
<p><strong>Anti-recommendation</strong>：access pattern &lt; 3 個、主表 PK 已能覆蓋 → 不要預先建 GSI；GSI 從少到多容易、從多到少要 application 端配合 cutover。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li>每個 GSI 獨立 <code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code></li>
<li><code>ReplicationLatency</code>：GSI async replication 延遲、p99 通常 &lt; 1s（無 SLA）</li>
<li>DAX：<code>CacheHits</code> / <code>CacheMisses</code> / <code>CacheHitRate</code>、<code>ItemCacheHits</code> / <code>QueryCacheHits</code></li>
</ul>
<p><code>ReturnConsumedCapacity</code> flag：query 時帶 <code>INDEXES</code> 看 GSI consumption；<code>TOTAL</code> 看 base + GSI 合計、debug 時切換用。</p>
<p><strong>Cost monitoring</strong>：</p>
<ul>
<li>每個 GSI 都重複收 storage + WCU；GSI 多時 cost 容易超過 base table</li>
<li>用 AWS Cost Explorer 按 GSI 維度看、不是只看 table-level 總 cost</li>
<li>DAX cost 是 instance-hour 計、不是 per-request；只在 read peak 持續高才划算</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「GSI 多時 cost 超過 base table」屬通用工程知識、<code>9.C27 Disney+</code> / <code>9.C19 Capcom</code> case 沒揭露具體 GSI cost ratio。</p></blockquote>
<p><strong>DAX 觀測重點</strong>（新增）：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% 應重新評估 DAX 是否該存在</li>
<li>cache size utilization 看 DAX instance class 是否足夠</li>
<li>觀察 cache miss 後 fallback 到 DynamoDB 的 latency、確認 DAX 真的減少 base 路徑壓力</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、case 未揭露具體閾值。</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> 的 NoSQL index cost section、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="disney--capcom-的-access-pattern-對照">Disney+ / Capcom 的 access pattern 對照</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C19 Capcom</code> 是兩種 GSI 用法：</p>
<ul>
<li>Disney+ watchlist + 播放進度 + cross-device sync 全用主表 + 少量 GSI、避免 GSI 爆炸；cross-device sync 透過 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">Global Tables</a> 處理、不是 GSI</li>
<li>Capcom 玩家 leaderboard / 戰績用 GSI 反向查詢（跨遊戲共用平台、player_id 為 base PK、game_id 為 GSI PK）；leaderboard 是否該走 GSI 還是 Redis sorted set 是另一個取捨</li>
</ul>
<p>兩個 case 都 <em>沒有公開揭露</em> 具體 GSI 數量、projection 配置、DAX 是否使用。引用 case 時要分層 — 概念是 case 揭露、實作數字是通用工程估算。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — GSI 是 single-table 沒覆蓋的 access pattern 補位</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — GSI 自己也會 hot partition、GSI PK 設計獨立 review</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — GSI 強制 eventual、對應 consistency 軸</li>
<li><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">on-demand-vs-provisioned</a> — GSI 多時 cost 跟 mode 互動</li>
<li>替代路由：access pattern 變動頻繁 → 考慮 OpenSearch / Aurora、單純 search 不要拿 GSI 當 inverted index</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 9.C19</a> 互引：leaderboard 用 GSI vs Redis sorted set 的選擇；DAX 是 derive 不是 case fact、引用要明示</li>
<li>跟 <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 9.C29</a> 互引：DAX 作為讀峰值補位的 case 揭露</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Replica Set Read Preference：DB 層 causal session vs cache 層 freshness token</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/</guid><description>&lt;p>MongoDB replica set 在小規模時 read preference 五擇一就夠用、&lt;code>primary&lt;/code> 走預設、想分擔 primary 改 &lt;code>secondary&lt;/code> — 直觀但會在 production 反噬。讀者真正撞到的議題分兩層：DB 層的 read-your-own-write（同 client 寫完馬上讀讀不到）跟跨層的 read-after-write（write 進 MongoDB、cache 還是舊資料）。前者用 causal consistency session 解、後者要走 freshness token 跨層協議。Coinbase 1.5M reads/sec 不是純 MongoDB 撐出來、是 DB + cache 跨層合成。本文把 read preference 機制 + 跨層協作講清楚。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 replica set 簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>進本文前先確認 MongoDB 已通過適配判讀&lt;/strong>：workload 是否落在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 判讀軸見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>。Read scaling 是 &lt;em>已選 MongoDB 後&lt;/em> 的容量決策、判讀通不過時 read preference 修補無法救回 vendor 選錯。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境read-scaling-撞牆的兩種長相">問題情境：read scaling 撞牆的兩種長相&lt;/h2>
&lt;p>典型觸發場景：primary 寫入飽和、TL 提議「讀都打 secondary」想橫向擴容。改完後幾個 production 徵兆連環出現：&lt;/p>
&lt;ul>
&lt;li>User 看到「我剛下的訂單怎麼還沒出現」— write 進 primary、立刻 read 打 secondary、secondary 還沒 apply 該寫入、user 看到 stale data&lt;/li>
&lt;li>跨 region replica set：app server 在 Tokyo、primary 在 Singapore、每筆讀走 70ms 跨海 RTT；改 &lt;code>nearest&lt;/code> 後 latency 降但 stale read 出現&lt;/li>
&lt;li>Replication lag 在 backup 期間飆到分鐘級、&lt;code>secondary&lt;/code> read 拿到幾分鐘前的資料、前端報表時間軸對不上&lt;/li>
&lt;li>Failover 期間 read preference 沒寫好、client 一直連舊 primary、&lt;code>SocketTimeout&lt;/code> 直到 driver retry 邏輯介入&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>第二類議題、規模更大&lt;/strong>：把所有 read 打 secondary、replica 數量加到 5-7 仍撐不住 sustained 高 read（&amp;gt;500K reads/sec）；replication lag 升 + secondary CPU 飽和。這時 read preference 已不夠、必須加 cache + 跨層 freshness 機制。&lt;/p>
&lt;p>讀者徵兆：&lt;code>rs.printSecondaryReplicationInfo()&lt;/code> 顯示 lag 分鐘級、application log 出現「我剛寫的資料讀不到」客訴、failover 演練後 connection error 持續 30s+、cache hit rate 跟 read latency 反向相關。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB replica set 在小規模時 read preference 五擇一就夠用、<code>primary</code> 走預設、想分擔 primary 改 <code>secondary</code> — 直觀但會在 production 反噬。讀者真正撞到的議題分兩層：DB 層的 read-your-own-write（同 client 寫完馬上讀讀不到）跟跨層的 read-after-write（write 進 MongoDB、cache 還是舊資料）。前者用 causal consistency session 解、後者要走 freshness token 跨層協議。Coinbase 1.5M reads/sec 不是純 MongoDB 撐出來、是 DB + cache 跨層合成。本文把 read preference 機制 + 跨層協作講清楚。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 replica set 簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>進本文前先確認 MongoDB 已通過適配判讀</strong>：workload 是否落在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 判讀軸見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>。Read scaling 是 <em>已選 MongoDB 後</em> 的容量決策、判讀通不過時 read preference 修補無法救回 vendor 選錯。</p></blockquote>
<h2 id="問題情境read-scaling-撞牆的兩種長相">問題情境：read scaling 撞牆的兩種長相</h2>
<p>典型觸發場景：primary 寫入飽和、TL 提議「讀都打 secondary」想橫向擴容。改完後幾個 production 徵兆連環出現：</p>
<ul>
<li>User 看到「我剛下的訂單怎麼還沒出現」— write 進 primary、立刻 read 打 secondary、secondary 還沒 apply 該寫入、user 看到 stale data</li>
<li>跨 region replica set：app server 在 Tokyo、primary 在 Singapore、每筆讀走 70ms 跨海 RTT；改 <code>nearest</code> 後 latency 降但 stale read 出現</li>
<li>Replication lag 在 backup 期間飆到分鐘級、<code>secondary</code> read 拿到幾分鐘前的資料、前端報表時間軸對不上</li>
<li>Failover 期間 read preference 沒寫好、client 一直連舊 primary、<code>SocketTimeout</code> 直到 driver retry 邏輯介入</li>
</ul>
<p><strong>第二類議題、規模更大</strong>：把所有 read 打 secondary、replica 數量加到 5-7 仍撐不住 sustained 高 read（&gt;500K reads/sec）；replication lag 升 + secondary CPU 飽和。這時 read preference 已不夠、必須加 cache + 跨層 freshness 機制。</p>
<p>讀者徵兆：<code>rs.printSecondaryReplicationInfo()</code> 顯示 lag 分鐘級、application log 出現「我剛寫的資料讀不到」客訴、failover 演練後 connection error 持續 30s+、cache hit rate 跟 read latency 反向相關。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> 揭露「document model 撐 1.5M reads/sec 靠 cache + freshness token」、含警示「1.5M reads/sec 是 users 服務 <em>加上 cache</em> 的數字、不是 MongoDB cluster 純讀取數字」。跨 region read preference 改 <code>nearest</code> 後 stale read 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="mongodb-read-preference--read-concern-兩軸">MongoDB read preference + read concern 兩軸</h3>
<p>Read preference 五種：</p>
<ul>
<li><strong><code>primary</code></strong>（預設）：只打 primary、強一致、primary 飽和時無路可走</li>
<li><strong><code>primaryPreferred</code></strong>：先 primary、primary 不可用 fallback secondary</li>
<li><strong><code>secondary</code></strong>：只打 secondary、永遠拒 primary、failover 期間若所有 secondary 都不行就拋錯</li>
<li><strong><code>secondaryPreferred</code></strong>：先 secondary、secondary 不可用 fallback primary</li>
<li><strong><code>nearest</code></strong>：不是「最近的 secondary」、是「ping latency 最低的 member」（可能是 primary）；driver 用 latency window（預設 15ms）內隨機挑</li>
</ul>
<p>Read concern 是另一軸：</p>
<ul>
<li><strong><code>local</code></strong>：讀本地最新（含未確認）、效能最佳、可能讀到後來 rollback 的資料</li>
<li><strong><code>available</code></strong>：跟 <code>local</code> 類似但對 sharded cluster 有差異</li>
<li><strong><code>majority</code></strong>：讀到「已寫到多數 member」的資料、寫入 commit 後在多數 member 確認後才看得到</li>
<li><strong><code>linearizable</code></strong>：強制最新、必須打 primary、最高 latency</li>
</ul>
<p>Write concern <code>w: &quot;majority&quot;</code> 保證寫入確認後在多數 member 上、但不保證 secondary 馬上 visible — 兩個概念分開。</p>
<h3 id="causal-consistency-sessiondb-層機制">Causal consistency session（DB 層機制）</h3>
<p>Causal consistency session 解的是 <em>單 client</em> 在 <em>MongoDB cluster 內部</em> 的因果一致：</p>
<ul>
<li>Client session 帶 <code>clusterTime</code> + <code>operationTime</code></li>
<li>Driver 把 read 路由到「已 apply 該 operationTime」的 member</li>
<li>實現 read-your-own-write（自己剛寫的、自己讀得到）</li>
</ul>
<p>機制只在「同一 client session」內生效。跨 client 的因果一致（A 寫 → B 讀）不在範圍內。</p>
<p>其他輔助機制：</p>
<ul>
<li><strong>Tag set</strong>：member 標 <code>{region: &quot;ap-tokyo&quot;, role: &quot;analytics&quot;}</code>、read preference 帶 tag 把流量路由到特定 member</li>
<li><strong>Hidden / delayed secondary</strong>：不參與 election、不接 client read、做 backup / DR 用</li>
<li><strong>Election</strong>：primary 失聯後 majority 投票選新 primary、預設 10s 內完成；election 期間所有 primary read 失敗</li>
</ul>
<h3 id="freshness-tokencache-層機制">Freshness token（cache 層機制）</h3>
<p>9.C36 Coinbase 揭露的 <em>跨層</em> 機制 — 解的是 <em>MongoDB + cache 跨層</em> 的 read-after-write、不是 cluster 內部。對應 <a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 卡片的 application-level 版本協議定義：</p>
<p><strong>觸發條件</strong>：直接打 MongoDB 不可能撐 1.5M reads/sec（口徑：users 服務應用層觀察、含 cache、非 MongoDB cluster 純讀取）。Coinbase 在 users 服務前加 Memcached query cache、單 document query 先查 cache。</p>
<p><strong>跨層一致性問題</strong>：write 進 MongoDB primary、cache 還是舊資料、client 下次 read 從 cache 拿到舊版。</p>
<p><strong>freshness token 機制</strong>：</p>
<ol>
<li>Write 成功後、server 給 client 一個 token（包含 OCC version / clusterTime）</li>
<li>Client 之後 read 帶這個 token</li>
<li>Server 保證返回的資料版本 ≥ token</li>
<li>若 cache 的版本 &lt; token、bypass cache 直接打 DB</li>
</ol>
<p><strong>跟 causal consistency session 的關係</strong>：兩者解決同一類問題（read-after-write）但作用範圍不同。Causal session 是 DB 層、保證在同一 cluster 內 read-your-own-write；freshness token 是 <em>DB + cache 兩層共用的版本協議</em>、保證跨層 read-your-own-write。</p>
<h3 id="跨層協作三選一">跨層協作三選一</h3>
<p>讀者真實系統的 read 一致性需求要選哪層處理：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只用 DB 層（causal session）</td>
          <td>無 cache 層、讀寫都直接打 MongoDB cluster</td>
          <td>replica scaling 上限約幾十萬 reads/sec</td>
      </tr>
      <tr>
          <td>只用 cache 層（freshness token）</td>
          <td>有 cache、跨層一致性要求高、application 願改</td>
          <td>需設計 token 協議 + cache bypass 邏輯</td>
      </tr>
      <tr>
          <td>兩層並用</td>
          <td>大規模 OLTP、cluster 內也要 causal、跨 cache 也要 freshness</td>
          <td>複雜度最高、但 Coinbase 規模必走此路</td>
      </tr>
  </tbody>
</table>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>、<a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">session-consistency</a>、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual-consistency</a>。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：read shape 分類</strong>。把所有 read 分成四類：</p>
<ul>
<li>(a) 強一致必須 read-your-own-write（訂單詳情、帳戶餘額）</li>
<li>(b) 容忍秒級 lag（個人資料、商品詳情）</li>
<li>(c) 容忍分鐘級 lag（報表、analytics）</li>
<li>(d) 大規模 read scaling 需 cache + freshness token（用戶資料 / 高頻 product query）</li>
</ul>
<p><strong>Step 2：依分類對映機制</strong>。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>Read preference</th>
          <th>Read concern</th>
          <th>跨層機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>(a)</td>
          <td>primary</td>
          <td>majority</td>
          <td>causal consistency session</td>
      </tr>
      <tr>
          <td>(b)</td>
          <td>secondaryPreferred</td>
          <td>local</td>
          <td>monitoring lag alarm</td>
      </tr>
      <tr>
          <td>(c)</td>
          <td>secondary（tag set）</td>
          <td>available</td>
          <td>無</td>
      </tr>
      <tr>
          <td>(d)</td>
          <td>secondaryPreferred</td>
          <td>majority</td>
          <td>cache + freshness token + bypass</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：driver config</strong>（Node.js / Java / Python 都類似）：</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">mongodb://host1:27017,host2:27017,host3:27017/db?
</span></span><span class="line"><span class="ln">2</span><span class="cl">  replicaSet=rs0&amp;
</span></span><span class="line"><span class="ln">3</span><span class="cl">  readPreference=secondaryPreferred&amp;
</span></span><span class="line"><span class="ln">4</span><span class="cl">  readPreferenceTags=region:ap-tokyo&amp;
</span></span><span class="line"><span class="ln">5</span><span class="cl">  readPreferenceTags=&amp;
</span></span><span class="line"><span class="ln">6</span><span class="cl">  maxStalenessSeconds=90&amp;
</span></span><span class="line"><span class="ln">7</span><span class="cl">  readConcernLevel=majority</span></span></code></pre></div><p><code>readPreferenceTags</code> 寫多個 = fallback chain（先 tokyo 失敗 fallback 任意）。<code>maxStalenessSeconds=90</code> 拒絕 lag &gt; 90s 的 secondary。</p>
<p><strong>Step 4：causal consistency session</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">with</span> <span class="n">client</span><span class="o">.</span><span class="n">start_session</span><span class="p">(</span><span class="n">causal_consistency</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="k">as</span> <span class="n">s</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">coll</span><span class="o">.</span><span class="n">insert_one</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="n">s</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># 下面這個 find 自動路由到能讀到剛才寫的 member</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">coll</span><span class="o">.</span><span class="n">find_one</span><span class="p">({</span><span class="s2">&#34;_id&#34;</span><span class="p">:</span> <span class="n">doc</span><span class="p">[</span><span class="s2">&#34;_id&#34;</span><span class="p">]},</span> <span class="n">session</span><span class="o">=</span><span class="n">s</span><span class="p">)</span></span></span></code></pre></div><p>Session 結束後因果關係結束、下個 session 不繼承。</p>
<p><strong>Step 5：freshness token 設計</strong>（9.C36 Coinbase 模式）：</p>
<ul>
<li>Write API 返回 <code>{result, version_token}</code> — token 含 OCC version 或 MongoDB clusterTime</li>
<li>Read API 接受 optional <code>If-Version-≥</code> header / parameter</li>
<li>Cache lookup 比對 cache entry version 跟 token、低於 token 就 invalidate + bypass 到 MongoDB</li>
<li>DB 層 read 用 <code>readConcern: &quot;majority&quot;</code> 保證返回的 version ≥ token</li>
</ul>
<p><strong>Step 6：staging 驗證</strong>。灌入 replication lag（暫停 secondary apply）驗證 application 行為；灌入 stale cache 驗證 token bypass 邏輯；模擬 failover 驗證 driver retry。</p>
<p>驗證點：</p>
<ul>
<li><code>rs.printSecondaryReplicationInfo()</code> lag &lt; SLO</li>
<li>driver metric <code>readPreferenceUsageCount</code> 分布符合預期</li>
<li>failover drill 後 read recovery &lt; 15s</li>
<li>cache hit rate vs freshness bypass rate 比例監控</li>
</ul>
<p>Rollback boundary：read preference 是 driver-side config、可以 hot-swap；causal consistency session 需 application code 改、需灰度；freshness token 是 application + cache + DB 三方協議、回退需協調。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Read-after-write 不一致（DB 層）</strong>：寫 primary → 立刻 secondary read、應用 race condition 顯示「資料消失」。修法是 causal consistency session、driver 自動路由到已 apply 該寫入的 member。</p>
<p><strong>Read-after-write 不一致（跨層）</strong>：寫 primary → cache 還是舊資料 → user 看到舊資料。causal session 解不了（cache 在 MongoDB 外）、必須走 freshness token 跨層協議。</p>
<p><strong>Stale read 在 lag 高峰</strong>：backup / DDL / 大量寫入導致 secondary lag 分鐘級、<code>secondary</code> read 拿到舊資料。修法設 <code>maxStalenessSeconds</code> 拒舊 member、driver 自動轉到較新的 member 或 primary。</p>
<p><strong><code>nearest</code> 在跨 region 不穩</strong>：latency 抖動讓 driver 在 primary / secondary 跳、寫一致性與 read latency 同時惡化。修法是不要用 <code>nearest</code> 解跨 region 議題、應該用 tag set 明確路由。</p>
<p><strong>Failover 期間 primary read 全失敗</strong>：election 10s 內所有 primary read 拋錯。修法改 <code>primaryPreferred</code> + driver retry 邏輯吃掉短暫失敗、application 端配 retry policy。</p>
<p><strong>Tag set 失準</strong>：把 <code>region: &quot;ap-tokyo&quot;</code> 的流量路由到 tag 為 tokyo 的 member、但該 member 故障時沒 fallback、流量直接停。修法是 tag 設多層 fallback chain、最後一層留空 tag 表示「任意 member」。</p>
<p><strong>Analytical query 跑 OLTP secondary</strong>：<code>secondaryPreferred</code> 把報表打 OLTP secondary、報表 query 拖垮 OLTP read latency。修法是 analytical workload 用 tag set 路由到專屬 analytics secondary、跟 OLTP read 隔離。</p>
<p><strong>Freshness token 漏寫</strong>：write 沒帶 token 給 client / client 沒帶 token、token 機制 silently 失效、read 走 cache 拿舊資料。修法 token 必須 e2e 強制（middleware 自動帶 / 自動驗證）、不能靠 application 自覺。</p>
<p><strong>Cache bypass 比例失控</strong>：所有 read 都 bypass cache、cache 等於沒裝。修法是 token 失敗率要監控、過高表示 cache invalidation 設計有問題（cache 沒在 write 後 update / invalidate）。</p>
<p>Anti-recommendation：</p>
<ul>
<li>read-heavy 但有強一致需求的場景不要為了 scale 改 secondary read；該換 SQL + read replica 加 application-level cache、或加 sharding 把 primary 寫散開</li>
<li>大規模 OLTP（&gt;500K reads/sec）想單靠 MongoDB read preference 撐 = 拿不到那個量級。Coinbase 案明示「直接打 MongoDB 不可能撐 1.5M reads/sec」、必須 cache + freshness token</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Replica health</strong>：每個 member 的 <code>opcounters</code> 分布、<code>rs.status().members[].optimeDate</code> 推算 lag</li>
<li><strong>Read preference 命中</strong>：driver-side <code>readPreferenceTags</code> 命中率</li>
<li><strong>一致性 SLO</strong>：stale read 比例（causal consistency 拒絕重試次數）</li>
<li><strong>跨層 freshness</strong>：cache hit rate vs freshness bypass rate</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>rs.status()</code>：replica set 整體</li>
<li><code>rs.printSecondaryReplicationInfo()</code>：lag 概況</li>
<li><code>db.serverStatus().repl</code>：詳細 replication metric</li>
<li><code>db.adminCommand({replSetGetStatus:1})</code>：完整 status</li>
</ul>
<p>Application observability：APM 看「同一 session 內 write + read 順序對 latency / error 的影響」、SLO 是 read-your-own-write 命中率；跨層還要看 freshness token 流動完整性（write 是否發 token、read 是否帶 token、cache 是否驗 token）。</p>
<p>Lag alarm：lag &gt; 30s 預警、&gt; 90s 觸發 driver <code>maxStalenessSeconds</code> 自動拒讀。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：把 read preference 命中分布、replication lag time series、failover drill recovery time、freshness token bypass rate 列為 evidence。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：read latency 異常時要區分 (a) primary 飽和 (b) secondary lag 高 (c) tag routing 把流量集中到單一 member (d) cache hit rate 下降 / bypass 率上升。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-5合規邊界--mongodb-用-cluster-per-region-吸收">Frame 5：合規邊界 — MongoDB 用 cluster-per-region 吸收</h3>
<p>MongoDB / Atlas 沒有 <em>row-level locality</em> 機制（不像 CockroachDB 可把單 row pin 在合規 region）— 跨境合規必須以 <em>cluster-per-region</em> 拓樸吸收：每個合規市場開獨立 cluster、application 層做 routing、不靠 replica set / sharded cluster 機制跨 region。</p>
<p>跨 vendor 對照：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>合規吸收機制</th>
          <th>拓樸特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB / Cosmos DB</td>
          <td>cluster-per-region（無 row-level locality 等價物）</td>
          <td>各 region 獨立 cluster、application 層做市場 routing</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>fleet 拓樸（每市場獨立 cluster、Global Database 在合規場景反指標）</td>
          <td>active-passive per market、跨市場不複製</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>locality + placement（邏輯一個 cluster + region pinning + Outposts）</td>
          <td>單 logical cluster、physical row 鎖在合規 region</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>region-pinned Global Tables（按 region 開關 replication、各市場可分離）</td>
          <td>仍 active-active、但 replication 範圍可控</td>
      </tr>
  </tbody>
</table>
<p><strong>MongoDB 在這 frame 的退化點</strong>：read preference 機制本身不解合規 — 即使 <code>readPreferenceTags={region:eu}</code> 把流量路由到歐洲 secondary、但 primary 在亞洲時跨境 replication 仍在跑、合規 audit 不會放行 <em>路由層</em> 控制當作 <em>資料邊界</em> 控制。合規市場必須整 cluster 分離、再用 application 層 routing 把 user 帶到對應 cluster。</p>
<p><strong>Atlas 在合規場景的 fit</strong>：Atlas global cluster（zone sharding 把 shard 鎖在 region）是「跨 region 但 <em>資料 pin 在 zone</em>」的中介選項、適合 GDPR 軟條款（資料在歐洲 EEA 內可流動）；strict 條款（資料不能離開單一國家）仍須走 cluster-per-region。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — read preference 解決不了 write 飽和、要切 shard</li>
<li><a href="../change-streams-kafka/">change streams + Kafka</a> — change stream 預設打 primary、放 secondary 的 trade-off</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — 把 analytical aggregation 路由到專屬 secondary</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — freshness token 是該篇的核心議題之一、本文聚焦 DB 層 vs cache 層機制對照、不展開 cache 部署架構</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>跨 region 強 consistency 需求 → <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">→ Cosmos DB MongoDB API</a>（5 consistency level）</li>
<li>跨 region 想保留原生 MongoDB → <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas global cluster</a></li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 處理 read scaling pattern；<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 處理跨 region 一致性升級路徑。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「replica set + read preference」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — freshness token + 1.5M reads/sec（含 cache）</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/core/read-preference/">MongoDB Read Preference</a>、<a href="https://www.mongodb.com/docs/manual/reference/read-concern/">Read Concern</a>、<a href="https://www.mongodb.com/docs/manual/core/causal-consistency-read-write-concerns/">Causal Consistency</a></li>
</ul>
]]></content:encoded></item><item><title>Spanner Schema Migration Without Downtime + Interleaved Tables</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>schema migration without downtime + interleaved tables&lt;/em> — Spanner 兩個跟傳統 SQL 差異最大的 schema 機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境ddl-不停機跟-parent-child-物理-layout-的兩個疑問">問題情境：DDL 不停機跟 parent-child 物理 layout 的兩個疑問&lt;/h2>
&lt;p>傳統 PostgreSQL / MySQL DDL 拿 ACCESS EXCLUSIVE / metadata lock、線上跑 ALTER TABLE 動輒鎖表幾分鐘、大型 schema change 要 pt-osc / gh-ost / pg_repack 等外掛工具。Spanner 宣稱「schema change 不停機」、但團隊不知道實際機制跟邊界。讀者徵兆通常從這幾個地方浮現：「Spanner ALTER 真的不卡寫入嗎」「INDEX backfill 跑了 12 小時是正常嗎」「parent-child 的 INTERLEAVE IN PARENT 是什麼黑魔法」「ON DELETE CASCADE 在 interleaved table 為什麼是 storage-level 而不是 application-level」。&lt;/p>
&lt;p>真實壓力：multi-tenant SaaS 要對 100 億 row 的 orders 表加 column + 加 index、不能停機、不能讓 p99 write latency 超過 SLA。團隊以為「Spanner schema change 不停機」等同於「DDL 瞬間完成」、實際 ALTER 是 long-running operation、index backfill 在大表上跑數小時到數天、capacity 規劃要把 backfill 期間的 CPU 升幅算進去。&lt;/p>
&lt;p>Case anchor：&lt;strong>缺案例&lt;/strong>。9.C10 是 Google internal dogfood case、未展開 schema migration 細節、且 9.C10 不是 customer-facing capacity reference。本文用通用 pattern + 官方文件 + 反向回 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PostgreSQL Online Schema Change&lt;/a> 對照、待後續 customer case audit 補強。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>schema migration without downtime + interleaved tables</em> — Spanner 兩個跟傳統 SQL 差異最大的 schema 機制。</p></blockquote>
<hr>
<h2 id="問題情境ddl-不停機跟-parent-child-物理-layout-的兩個疑問">問題情境：DDL 不停機跟 parent-child 物理 layout 的兩個疑問</h2>
<p>傳統 PostgreSQL / MySQL DDL 拿 ACCESS EXCLUSIVE / metadata lock、線上跑 ALTER TABLE 動輒鎖表幾分鐘、大型 schema change 要 pt-osc / gh-ost / pg_repack 等外掛工具。Spanner 宣稱「schema change 不停機」、但團隊不知道實際機制跟邊界。讀者徵兆通常從這幾個地方浮現：「Spanner ALTER 真的不卡寫入嗎」「INDEX backfill 跑了 12 小時是正常嗎」「parent-child 的 INTERLEAVE IN PARENT 是什麼黑魔法」「ON DELETE CASCADE 在 interleaved table 為什麼是 storage-level 而不是 application-level」。</p>
<p>真實壓力：multi-tenant SaaS 要對 100 億 row 的 orders 表加 column + 加 index、不能停機、不能讓 p99 write latency 超過 SLA。團隊以為「Spanner schema change 不停機」等同於「DDL 瞬間完成」、實際 ALTER 是 long-running operation、index backfill 在大表上跑數小時到數天、capacity 規劃要把 backfill 期間的 CPU 升幅算進去。</p>
<p>Case anchor：<strong>缺案例</strong>。9.C10 是 Google internal dogfood case、未展開 schema migration 細節、且 9.C10 不是 customer-facing capacity reference。本文用通用 pattern + 官方文件 + 反向回 <a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PostgreSQL Online Schema Change</a> 對照、待後續 customer case audit 補強。</p>
<h2 id="核心機制ddl-是-long-runningtruetime-對齊-schema-version">核心機制：DDL 是 long-running、TrueTime 對齊 schema version</h2>
<h3 id="schema-change-的-lifecycle">Schema change 的 lifecycle</h3>
<p>Spanner DDL 不是同步 ALTER、是 <em>long-running operation</em>。TrueTime 給每次 schema change 分配一個 version timestamp、所有 read / write 用各自 transaction timestamp 對應「當下看到哪個 schema version」。讀者要理解的核心是：DDL 不是「鎖表→改→解鎖」、是「廣播新 schema version、讓現有 transaction 用舊 schema、新 transaction 用新 schema、背景 backfill 物理資料」。</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">時間軸：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">T0 (DDL 開始)
</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">     | ──── 舊 schema 仍可用、新 schema metadata 廣播 ────
</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">T1 (metadata 完成)
</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">     | ──── 新 transaction 用新 schema、舊 transaction 完成自己 ────
</span></span><span class="line"><span class="ln">10</span><span class="cl">     | ──── backfill 開始（背景）────
</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">T2 (backfill 完成)
</span></span><span class="line"><span class="ln">13</span><span class="cl">     |
</span></span><span class="line"><span class="ln">14</span><span class="cl">     | ──── 新 schema fully serve ────</span></span></code></pre></div><p>DDL 本身瞬間完成的部分是 <em>metadata 廣播</em>（毫秒到秒級）、慢的部分是 <em>backfill</em>（依資料量、可能數小時到數天）。讀者常見誤解是把 metadata 完成當「DDL 完成」、實際 query 還沒走新 index 因為 backfill 沒跑完。</p>
<h3 id="不停機的關鍵不同-ddl-的兩階段行為">不停機的關鍵：不同 DDL 的兩階段行為</h3>
<table>
  <thead>
      <tr>
          <th>DDL 類型</th>
          <th>metadata 行為</th>
          <th>backfill 行為</th>
          <th>阻塞？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ADD COLUMN</code>（無 NOT NULL）</td>
          <td>metadata-only、瞬間生效</td>
          <td>不需 backfill（新 column 預設 NULL）</td>
          <td>不阻塞 write</td>
      </tr>
      <tr>
          <td><code>ADD COLUMN</code>（NOT NULL）</td>
          <td>必須兩階段：先 ADD COLUMN with default、後 ADD CONSTRAINT</td>
          <td>兩階段間需 backfill default</td>
          <td>不阻塞 write、但兩階段不能合</td>
      </tr>
      <tr>
          <td><code>CREATE INDEX</code></td>
          <td>metadata 立即</td>
          <td>背景 backfill、不阻塞 write；backfill 完才 serve query</td>
          <td>不阻塞 write、阻塞「該 index 的 query」</td>
      </tr>
      <tr>
          <td><code>DROP COLUMN</code></td>
          <td>metadata 立即</td>
          <td>背景 GC dead column</td>
          <td>不阻塞</td>
      </tr>
      <tr>
          <td><code>ALTER COLUMN TYPE</code></td>
          <td>限制多、查最新文件</td>
          <td>-</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>讀者要記的是：<strong>index backfill 完成前、query 該 index 會 fallback 到 table scan</strong>、用 <code>EXPLAIN</code> 確認 query plan 走新 index 才算真正完成。沒做這層驗證、團隊會以為 CREATE INDEX 已經成功、實際 p99 query latency 還在表掃描的數量級。</p>
<h3 id="interleaved-table-的設計">Interleaved table 的設計</h3>
<p><a href="/blog/backend/knowledge-cards/interleaved-table/" data-link-title="Interleaved Table" data-link-desc="Spanner 把 parent / child table row 物理交錯儲存、parent &#43; child JOIN 不跨 split">Interleaved Table</a> 把 parent table（如 <code>Customer</code>）跟 child table（如 <code>Order</code>）的 row 在 storage 層 <em>物理上交錯儲存</em> — child row 跟對應 parent row 在同一個 split。不是純 foreign key、是 storage layout：</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">傳統 PostgreSQL FK 設計（兩張獨立表）：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Customer table:  [c1, c2, c3, ...]  → 一張表、一段 storage range
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Order table:     [o1, o2, o3, ...]  → 另一張表、另一段 storage range
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">FK 由 planner 在 JOIN 時拼接、可能跨 page / 跨 segment
</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">Spanner Interleaved 設計（物理交錯）：
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Storage layout: [c1, c1.o1, c1.o2, c2, c2.o1, c2.o2, c2.o3, c3, ...]
</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">                  c1 + 其 child           c2 + 其 child
</span></span><span class="line"><span class="ln">10</span><span class="cl">                  在同一個 split          在同一個 split</span></span></code></pre></div><p>Interleaved 的效果：parent + child JOIN 在同一個 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> split 完成、不跨 split = 不跨 Paxos group = 低延遲 transaction。這條設計把「FK 是 logical constraint」翻成「parent-child access pattern 是 physical co-location」、對 access pattern 固定的 workload（customer → orders、user → posts、tenant → records）是巨大 latency benefit。</p>
<h3 id="interleaved-的硬限">Interleaved 的硬限</h3>
<table>
  <thead>
      <tr>
          <th>限制</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>必須以 parent primary key 為 prefix</td>
          <td>child PK 第一段必須是 parent PK、不能完全自由</td>
      </tr>
      <tr>
          <td>最深 7 層</td>
          <td>深巢狀關係要選層級</td>
      </tr>
      <tr>
          <td><code>ON DELETE</code> 只能 CASCADE 或 NO ACTION</td>
          <td>不像 PG FK 有 SET NULL / SET DEFAULT</td>
      </tr>
      <tr>
          <td>一旦建立、無法直接 ALTER 改 interleave</td>
          <td>要改 → export + recreate + import、不是 ALTER</td>
      </tr>
  </tbody>
</table>
<p>最後一條是讀者最容易踩的雷 — 一開始沒設 interleaved、後悔時要 export-import 100 億 row、是大工程、不是 ALTER。Schema 設計階段要先 audit access pattern、決定哪些 parent-child 該 interleave。</p>
<h3 id="跟通用-fk-概念的差異">跟通用 FK 概念的差異</h3>
<p>PostgreSQL FK 是 logical constraint、JOIN 由 planner 處理；Spanner interleaved 是 physical layout、JOIN cost 跟 single-table access 接近。對應 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction-boundary</a> 卡 — interleaved 讓 transaction boundary 跟 storage boundary 對齊、跨 split transaction 變少、commit wait + Paxos round-trip 也省。</p>
<h2 id="操作流程ddl-跟-interleaved-table-的具體步驟">操作流程：DDL 跟 interleaved table 的具體步驟</h2>
<h3 id="加-column">加 column</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">Orders</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">tax_amount</span><span class="w"> </span><span class="n">FLOAT64</span><span class="p">;</span></span></span></code></pre></div><p>執行後拿 long-running operation id、用 <code>gcloud spanner operations list</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">gcloud spanner operations list --instance<span class="o">=</span>prod --database<span class="o">=</span>app
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud spanner operations describe projects/.../operations/&lt;op-id&gt;</span></span></code></pre></div><p>驗證點：operation 顯示 <code>done: true</code> 後、跑 <code>SELECT tax_amount FROM Orders LIMIT 1</code> 確認 column 可查。</p>
<h3 id="加-index">加 index</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">OrdersByCustomer</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">Orders</span><span class="p">(</span><span class="n">customer_id</span><span class="p">);</span></span></span></code></pre></div><p>拿 operation id → 用 Monitoring metric <code>spanner.googleapis.com/instance/indexes/backfill_progress</code>（或對應的最新 metric、查官方文件）追蹤進度。Backfill 完成前 query 不會走新 index、要用 <code>EXPLAIN</code> 確認：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">Orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;c123&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 應看到 plan 用 OrdersByCustomer index、不是 table scan</span></span></span></code></pre></div><h3 id="創建-interleaved-table">創建 interleaved table</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="o">`</span><span class="k">Order</span><span class="o">`</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="n">customer_id</span><span class="w"> </span><span class="n">INT64</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">order_id</span><span class="w"> </span><span class="n">INT64</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="n">FLOAT64</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="n">order_id</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="n">INTERLEAVE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="n">PARENT</span><span class="w"> </span><span class="n">Customer</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="k">DELETE</span><span class="w"> </span><span class="k">CASCADE</span><span class="p">;</span></span></span></code></pre></div><p>關鍵約束：</p>
<ul>
<li>child PK <code>(customer_id, order_id)</code> 第一段是 parent PK</li>
<li><code>ON DELETE CASCADE</code> 是 storage-level — 刪 parent row 自動刪 child row、Spanner 內部處理、不是 trigger</li>
</ul>
<h3 id="從-non-interleaved-改成-interleaved">從 non-interleaved 改成 interleaved</h3>
<p><em>無法直接 ALTER</em>、要走 export-recreate-import：</p>
<ol>
<li>用 Dataflow / <code>gcloud spanner databases export</code> 把舊表 export 到 GCS</li>
<li>建新表（interleaved schema）</li>
<li>用 Dataflow / <code>gcloud spanner databases import</code> 把資料倒回</li>
<li>應用層 cutover（feature flag / dual write）</li>
</ol>
<p>這個流程是 mini-migration、要走完整 <a href="../migrate-from-cloud-sql-pg/">migration playbook</a> 的 phase plan。Schema 設計階段就決定好 interleave、避免後悔成本。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>DDL 完成前可 <code>gcloud spanner operations cancel</code> 取消；完成後加 index 要 DROP、加 column 要 DROP COLUMN（同樣是 long-running）。讀者要先確認自己在 DDL 哪個階段、cancel 跟 reverse DDL 是兩條不同路徑。</p>
<h2 id="失敗模式5-個-production-踩雷">失敗模式：5 個 production 踩雷</h2>
<h3 id="backfill-時間沒估event-window-撞牆">Backfill 時間沒估、event window 撞牆</h3>
<p>100 億 row 加 index、預期 1 小時、實際 12 小時 — 沒先用 <code>cost</code> 估 + 沒監控進度 metric。事故場景：團隊在 black friday 前一週開 CREATE INDEX、以為週末跑完、實際週末仍在 backfill、event 期間 CPU 升、query latency 退化。</p>
<p>修法：</p>
<ul>
<li>DDL 前用小表 benchmark backfill 速度（rows/sec）、推估大表時間</li>
<li>DDL 期間監控 <code>instance/cpu/smoothed_utilization</code>、若 &gt; 80% 暫停或降流量</li>
<li>大 DDL 排在 capacity headroom 充足的時段、避開 event window</li>
</ul>
<h3 id="interleaved-table-一開始沒設後悔時要-recreate">Interleaved table 一開始沒設、後悔時要 recreate</h3>
<p>100 億 row export-import + cutover 是大工程、不是 ALTER。事故場景：團隊一開始把 Customer / Order 設成獨立表、上線一年後發現 customer → orders access pattern 是 99% 的 query、JOIN 跨 split 付 commit wait + Paxos cost、想改 interleaved、發現要 mini-migration。</p>
<p>修法：</p>
<ul>
<li>Schema 設計階段就 audit access pattern、決定哪些 parent-child 該 interleave</li>
<li>寫 ADR 把 interleave 決策跟業務 access pattern 綁定、避免後悔成本</li>
</ul>
<h3 id="把-interleaved-跟-fk-混為一談">把 interleaved 跟 FK 混為一談</h3>
<p>interleaved 的 <code>ON DELETE CASCADE</code> 是 storage-level、刪 parent 自動刪 child；非 interleaved FK 要 application 或 trigger 處理。事故場景：團隊以為「我加了 FK 就會 CASCADE」、實際非 interleaved table 只是 constraint check、刪 parent 時 child orphan、對帳爆炸。</p>
<p>修法：</p>
<ul>
<li>Schema 設計時明確分類：interleaved（storage-level CASCADE）vs FK constraint（只檢查、不 CASCADE）</li>
<li>非 interleaved 的 parent-child 刪除邏輯放應用層、寫入對帳測試</li>
</ul>
<h3 id="加-not-null-一步到位">加 NOT NULL 一步到位</h3>
<p>直接 <code>ALTER ADD COLUMN x INT64 NOT NULL</code> 會失敗、必須兩階段。事故場景：開發環境 schema 是新建空表、<code>ADD COLUMN NOT NULL</code> OK；production 表有資料、ADD 失敗、團隊以為 Spanner 不支援、回退。</p>
<p>修法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Phase 1: ADD with default
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">Orders</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">tax_amount</span><span class="w"> </span><span class="n">FLOAT64</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 等 backfill 完成
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- Phase 2: ADD CONSTRAINT
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">Orders</span><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">tax_amount</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span></span></span></code></pre></div><h3 id="schema-change-期間舊-client-還在用舊-schema">Schema change 期間舊 client 還在用舊 schema</h3>
<p>TrueTime 保證 read 看到自己 timestamp 對應的 schema version、但 client SDK cache schema 過期會 retry — 沒處理會看到 transient error。事故場景：DDL 完成後、舊 client session 看到 transient <code>FAILED_PRECONDITION</code>、團隊以為 DDL 失敗、回退。</p>
<p>修法：</p>
<ul>
<li>應用層處理 transient retry（指數退避）</li>
<li>DDL 完成後重新 deploy app instance、避免長期 stale schema cache</li>
</ul>
<h2 id="容量與觀測backfill-是-cpu--io-的額外負載">容量與觀測：Backfill 是 CPU + I/O 的額外負載</h2>
<p>必看 metric：</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">spanner.googleapis.com/instance/cpu/smoothed_utilization
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → backfill 期間 CPU 升幅、判讀是否撞 headroom
</span></span><span class="line"><span class="ln">3</span><span class="cl">api/api_request_count for ExecuteSql
</span></span><span class="line"><span class="ln">4</span><span class="cl">   → application traffic 是否受 backfill 影響
</span></span><span class="line"><span class="ln">5</span><span class="cl">long-running operation API progress
</span></span><span class="line"><span class="ln">6</span><span class="cl">   → DDL 自身進度（不是 query 進度）</span></span></code></pre></div><p>Backfill 期間的 capacity impact：DDL 跑在 background priority、但仍佔 CPU、需要在 instance 有足夠 headroom（建議 &lt; 65% CPU baseline 才開大 backfill）。capacity 規劃要把 schema migration 列入 buffer、回 <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>。</p>
<p>Observability evidence：backfill 開始 timestamp、operation id、predicted duration、實際 duration、CPU peak — 全進 incident decision log、回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<p>監控盲點：DDL operation 失敗 silent fail 在 <code>gcloud operations describe</code> 才能看到、Cloud Monitoring 沒有直接 alert。團隊要寫自己的 polling script、operation 失敗時主動 alert、不靠 Cloud Monitoring default。</p>
<h2 id="邊界與整合何時不用-interleaved怎麼跟-pg-對照">邊界與整合：何時不用 interleaved、怎麼跟 PG 對照</h2>
<h3 id="何時不用-interleaved">何時不用 interleaved</h3>
<ul>
<li>小 table（&lt; 1M row、單機可放）：不需要 interleave、用 standard FK 就好</li>
<li>過度 interleave 7 層：把 split 變窄、反而 hot、得不償失</li>
<li>access pattern 不是 parent-child JOIN：interleave 沒 benefit、純粹給 schema 加複雜度</li>
</ul>
<h3 id="跟-postgresql-的對照">跟 PostgreSQL 的對照</h3>
<p><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PostgreSQL Online Schema Change</a> 用 pg_repack / pt-osc workflow 模擬「不停機」 — 實際是用 trigger + 影子表 + cutover 把 lock 時間壓到秒級、不是真正瞬間。Spanner 是原生支援 DDL long-running operation、不需要外掛工具、但 backfill 時間在大表上仍長、跟 pg_repack 在大表上的執行時間量級接近。</p>
<p>差異點：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL（pg_repack / pt-osc）</th>
          <th>Spanner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lock 時間</td>
          <td>秒級（cutover 時短鎖）</td>
          <td>毫秒（metadata 廣播）</td>
      </tr>
      <tr>
          <td>Backfill 時間</td>
          <td>數小時</td>
          <td>數小時</td>
      </tr>
      <tr>
          <td>工具</td>
          <td>外掛</td>
          <td>原生</td>
      </tr>
      <tr>
          <td>Schema version</td>
          <td>單版</td>
          <td>TrueTime timestamp 對齊多版並存</td>
      </tr>
      <tr>
          <td>大表加 NOT NULL</td>
          <td>一步到位（搭配 default）</td>
          <td>必須兩階段</td>
      </tr>
  </tbody>
</table>
<p>讀者選 Spanner 不是為了「DDL 更快」、是為了「不依賴外掛 + 多版本並存」。實際在大表上的耗時兩邊差不多。</p>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../truetime-api-depth/">truetime-api-depth</a>：schema version 也是 TrueTime timestamp、跟 transaction timestamp 同層機制</li>
<li><a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a>：target schema 設計含 interleaved、Phase 1 必讀本文</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：schema change 期間多版本並存的一致性保證</li>
</ul>
<h3 id="跟-1x-章節">跟 1.x 章節</h3>
<p><a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">Schema Design</a> — interleaved 是 schema 設計的物理層決策、不是純 logical design。對照 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">schema-migration-rollout-evidence</a> 看 schema rollout 的 evidence 收集模式。</p>
<h3 id="anti-recommendation">Anti-recommendation</h3>
<p>讀者讀完本文應該能判斷：interleaved 不是「強制使用」的 feature、是「access pattern 固定時的 latency benefit」。小規模 OLTP、access pattern 不確定的 workload、用 standard PostgreSQL FK 就好、為 interleaved 付 schema 後悔成本的判準很高。</p>
]]></content:encoded></item><item><title>3.C32 LoyaltyLion：監控數千 RabbitMQ queue</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-loyaltylion-monitoring-thousands/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-loyaltylion-monitoring-thousands/</guid><description>&lt;p>這個案例的核心責任是說明大規模 queue topology 的監控議題超出 Management plugin 能力範圍。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>LoyaltyLion 跑數千個 RabbitMQ queue、用 rabbitmqctl 跑 recurring script 抓 queue 資訊、透過 statsd 送到 Datadog。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>大規模 queue 拓撲下管理 plugin API 不夠用、需自寫採集腳本。揭露 queue 數量上萬時、原生 monitoring 介面（HTTP API、Management UI）會變成瓶頸、需要 metrics agent 模式。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Prefetch + consumer 併發（大規模 queue topology 的監控議題）/ RabbitMQ Cluster Operator（運維邊界）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 觀測模組&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.loyaltylion.com/monitoring-thousands-of-rabbitmq-queues-with-datadog-d3168c088ea6">Monitoring Thousands of RabbitMQ Queues with Datadog&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明大規模 queue topology 的監控議題超出 Management plugin 能力範圍。</p>
<h2 id="觀察">觀察</h2>
<p>LoyaltyLion 跑數千個 RabbitMQ queue、用 rabbitmqctl 跑 recurring script 抓 queue 資訊、透過 statsd 送到 Datadog。</p>
<h2 id="判讀">判讀</h2>
<p>大規模 queue 拓撲下管理 plugin API 不夠用、需自寫採集腳本。揭露 queue 數量上萬時、原生 monitoring 介面（HTTP API、Management UI）會變成瓶頸、需要 metrics agent 模式。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Prefetch + consumer 併發（大規模 queue topology 的監控議題）/ RabbitMQ Cluster Operator（運維邊界）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 觀測模組</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.loyaltylion.com/monitoring-thousands-of-rabbitmq-queues-with-datadog-d3168c088ea6">Monitoring Thousands of RabbitMQ Queues with Datadog</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL MVCC 的 vacuum 必要性、本文聚焦 &lt;em>autovacuum 在 production write-heavy workload 為什麼追不上&lt;/em> 的根因 + 各維度 tuning。&lt;/p>&lt;/blockquote>
&lt;h2 id="你的-autovacuum-永遠追不上-bloat--為什麼">你的 autovacuum 永遠追不上 bloat — 為什麼&lt;/h2>
&lt;p>write-heavy table 的常見故事：上線時表 10GB、3 個月後 30GB、6 個月 80GB；DBA 看 &lt;code>pg_stat_user_tables&lt;/code> 發現 &lt;code>n_dead_tup&lt;/code> 比 &lt;code>n_live_tup&lt;/code> 還多、&lt;code>pg_stat_progress_vacuum&lt;/code> 顯示 autovacuum 一直在跑、但 dead tuple 從沒清乾淨。表本身才 5M row、實際磁碟卻佔 80GB。&lt;/p>
&lt;p>這不是 PostgreSQL bug、是 autovacuum &lt;em>cost-based throttling 預設保守&lt;/em> 的設計意圖 — autovacuum 不該影響 OLTP query 性能、所以每跑一段就 sleep。預設 &lt;code>autovacuum_vacuum_cost_limit=200&lt;/code> + &lt;code>autovacuum_vacuum_cost_delay=2ms&lt;/code> 在 write-heavy 表（每秒幾千 UPDATE）下、清理速度 &lt;em>永遠慢於&lt;/em> dead tuple 產生速度。預設配置適合 read-heavy / write-light workload；OLTP write-heavy 必須調。&lt;/p>
&lt;h2 id="mvcc-跟-dead-tuplevacuum-在解什麼">MVCC 跟 dead tuple：vacuum 在解什麼&lt;/h2>
&lt;p>PostgreSQL MVCC：每次 UPDATE 都是 &lt;em>insert new row + mark old row as deleted&lt;/em>；DELETE 是 &lt;em>mark as deleted、不立刻釋放空間&lt;/em>。dead tuple 在 disk 上佔位、但不能被 query 讀到。autovacuum 的責任：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>回收 dead tuple 空間&lt;/strong> 供新 row reuse（不縮 table 大小、是 free space map）&lt;/li>
&lt;li>&lt;strong>更新 visibility map&lt;/strong> 讓 index-only scan 跳過 heap fetch&lt;/li>
&lt;li>&lt;strong>凍結老 row 的 xid&lt;/strong>（freeze）避免 xid wraparound 災難&lt;/li>
&lt;li>&lt;strong>重整 index B-tree&lt;/strong> 標記 dead pointer（不刪 index page）&lt;/li>
&lt;/ol>
&lt;p>Vacuum 不縮表 — 真要縮要跑 &lt;code>VACUUM FULL&lt;/code>（全表 exclusive lock、production 不能跑）或 &lt;code>pg_repack&lt;/code>（online repack tool）。預期 vacuum 只能 &lt;em>讓表停止長大&lt;/em>、不能 &lt;em>讓表變小&lt;/em>。&lt;/p>
&lt;h2 id="tuningcost-based-throttle-跟-trigger-threshold">Tuning：cost-based throttle 跟 trigger threshold&lt;/h2>
&lt;h3 id="cost-based-throttle全-instance">Cost-based throttle（全 instance）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># postgresql.conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">autovacuum_vacuum_cost_limit&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">2000 # 預設 200、production 拉 5-10 倍&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">autovacuum_vacuum_cost_delay&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">2ms # 預設 2ms、不太需要動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="na">autovacuum_max_workers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">6 # 預設 3、CPU 多時拉到 6-10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">maintenance_work_mem&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">1GB # 預設 64MB、單一 vacuum 用的記憶體&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直覺：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL MVCC 的 vacuum 必要性、本文聚焦 <em>autovacuum 在 production write-heavy workload 為什麼追不上</em> 的根因 + 各維度 tuning。</p></blockquote>
<h2 id="你的-autovacuum-永遠追不上-bloat--為什麼">你的 autovacuum 永遠追不上 bloat — 為什麼</h2>
<p>write-heavy table 的常見故事：上線時表 10GB、3 個月後 30GB、6 個月 80GB；DBA 看 <code>pg_stat_user_tables</code> 發現 <code>n_dead_tup</code> 比 <code>n_live_tup</code> 還多、<code>pg_stat_progress_vacuum</code> 顯示 autovacuum 一直在跑、但 dead tuple 從沒清乾淨。表本身才 5M row、實際磁碟卻佔 80GB。</p>
<p>這不是 PostgreSQL bug、是 autovacuum <em>cost-based throttling 預設保守</em> 的設計意圖 — autovacuum 不該影響 OLTP query 性能、所以每跑一段就 sleep。預設 <code>autovacuum_vacuum_cost_limit=200</code> + <code>autovacuum_vacuum_cost_delay=2ms</code> 在 write-heavy 表（每秒幾千 UPDATE）下、清理速度 <em>永遠慢於</em> dead tuple 產生速度。預設配置適合 read-heavy / write-light workload；OLTP write-heavy 必須調。</p>
<h2 id="mvcc-跟-dead-tuplevacuum-在解什麼">MVCC 跟 dead tuple：vacuum 在解什麼</h2>
<p>PostgreSQL MVCC：每次 UPDATE 都是 <em>insert new row + mark old row as deleted</em>；DELETE 是 <em>mark as deleted、不立刻釋放空間</em>。dead tuple 在 disk 上佔位、但不能被 query 讀到。autovacuum 的責任：</p>
<ol>
<li><strong>回收 dead tuple 空間</strong> 供新 row reuse（不縮 table 大小、是 free space map）</li>
<li><strong>更新 visibility map</strong> 讓 index-only scan 跳過 heap fetch</li>
<li><strong>凍結老 row 的 xid</strong>（freeze）避免 xid wraparound 災難</li>
<li><strong>重整 index B-tree</strong> 標記 dead pointer（不刪 index page）</li>
</ol>
<p>Vacuum 不縮表 — 真要縮要跑 <code>VACUUM FULL</code>（全表 exclusive lock、production 不能跑）或 <code>pg_repack</code>（online repack tool）。預期 vacuum 只能 <em>讓表停止長大</em>、不能 <em>讓表變小</em>。</p>
<h2 id="tuningcost-based-throttle-跟-trigger-threshold">Tuning：cost-based throttle 跟 trigger threshold</h2>
<h3 id="cost-based-throttle全-instance">Cost-based throttle（全 instance）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">autovacuum_vacuum_cost_limit</span> <span class="o">=</span> <span class="s">2000          # 預設 200、production 拉 5-10 倍</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">autovacuum_vacuum_cost_delay</span> <span class="o">=</span> <span class="s">2ms            # 預設 2ms、不太需要動</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">autovacuum_max_workers</span> <span class="o">=</span> <span class="s">6                    # 預設 3、CPU 多時拉到 6-10</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">maintenance_work_mem</span> <span class="o">=</span> <span class="s">1GB                    # 預設 64MB、單一 vacuum 用的記憶體</span></span></span></code></pre></div><p>直覺：</p>
<ul>
<li><code>cost_limit</code> 是每個 cycle 能消費多少「cost」、cost 由 page read / dirty / hit 加總；拉高 = 每次 cycle 處理更多 page</li>
<li>拉 <code>cost_limit</code> 比 <code>cost_delay</code> 直接 — delay 太低（&lt; 1ms）OS scheduler 抖動就無效</li>
<li><code>max_workers</code> 限同時跑的 vacuum；partition 多時容易爆滿、要拉</li>
<li><code>maintenance_work_mem</code> 影響 index vacuum 速度、SSD 環境 1-2GB 是 sweet spot</li>
</ul>
<h3 id="per-table-override精準到-hot-table">Per-table override（精準到 hot table）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 對 hot write-heavy 表加強
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">autovacuum_vacuum_scale_factor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">05</span><span class="p">,</span><span class="w">      </span><span class="c1">-- 預設 0.2、5% dead 就觸發
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="n">autovacuum_vacuum_threshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w">          </span><span class="c1">-- 預設 50、絕對值底線
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="n">autovacuum_vacuum_cost_limit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">,</span><span class="w">         </span><span class="c1">-- 該表獨立 cost_limit
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="n">autovacuum_analyze_scale_factor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">05</span><span class="p">,</span><span class="w">      </span><span class="c1">-- analyze 也跟著
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="n">autovacuum_freeze_max_age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100000000</span><span class="w">        </span><span class="c1">-- anti-wraparound 提前
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 對 append-only 表（log table）降頻
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">audit_log</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="n">autovacuum_vacuum_scale_factor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">,</span><span class="w">        </span><span class="c1">-- 50% dead 才觸發（極少 UPDATE / DELETE）
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="n">autovacuum_freeze_max_age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000000000</span><span class="w">       </span><span class="c1">-- freeze 延後
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre></div><p>關鍵：<em>hot table 比 default 緊、cold table 比 default 鬆</em>、不要把所有表用同套配置。Production cluster 通常 5-20 個 hot table 需要 per-table tuning。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1write-heavy-hot-tableautovacuum-永遠跑不完">Case 1：write-heavy hot table，autovacuum 永遠跑不完</h3>
<p><strong>徵兆</strong>：<code>pg_stat_user_tables.n_dead_tup</code> 持續高於 <code>n_live_tup</code>、<code>pg_stat_progress_vacuum</code> 顯示某表 vacuum 跑了 6+ 小時還在 <code>scanning heap</code>、表 size 持續長大。</p>
<p><strong>根因</strong>：default <code>cost_limit=200</code> 對該表 write rate（~5000 UPDATE/s）下、vacuum 處理速度 &lt; dead tuple 產生速度；單次 autovacuum 跑完整表要 12 小時、但表 5% bloat 觸發又啟動下一輪。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>對該表 <code>ALTER TABLE ... SET (autovacuum_vacuum_cost_limit = 10000)</code> — 該表 vacuum 不受全 instance 限制</li>
<li><code>maintenance_work_mem</code> 拉到 2GB（單 vacuum）</li>
<li>短期：手動 <code>VACUUM (VERBOSE, ANALYZE) events;</code> 在 maintenance window 跑、catch up</li>
<li>長期：考慮 partitioning — partition 後 vacuum 只動最近 partition、不掃整表</li>
</ol>
<h3 id="case-2長-transaction-卡住-vacuum-的-xmin-horizon">Case 2：長 transaction 卡住 vacuum 的 xmin horizon</h3>
<p><strong>徵兆</strong>：autovacuum 看似有跑、但 <code>n_dead_tup</code> 不降；<code>pg_stat_activity</code> 看到一個跑了 8 小時的 SELECT（report query 或 idle in transaction）。</p>
<p><strong>根因</strong>：vacuum 只能回收「不會被任何 active transaction 看到」的 dead tuple；長 transaction 的 xmin 鎖死 vacuum 能回收的範圍、即使 autovacuum 不停跑、能回收的 row 數為 0。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：application 端用 <code>statement_timeout</code> + <code>idle_in_transaction_session_timeout</code>（30 分鐘）強制終止 long transaction</li>
<li><strong>偵測</strong>：<code>SELECT pid, now() - xact_start FROM pg_stat_activity WHERE state = 'idle in transaction'</code> 定期掃</li>
<li><strong>臨時</strong>：kill 長 transaction（<code>pg_cancel_backend(pid)</code> / <code>pg_terminate_backend(pid)</code>）、autovacuum 下次跑就能回收</li>
<li><strong>架構</strong>：報表 query 跑在 standby、不要在 primary 開 long transaction</li>
</ol>
<h3 id="case-3anti-wraparound-vacuum-在-peak-觸發">Case 3：Anti-wraparound vacuum 在 peak 觸發</h3>
<p><strong>徵兆</strong>：production 流量高峰時 PostgreSQL CPU 100%、<code>pg_stat_progress_vacuum</code> 顯示 anti-wraparound vacuum 正在跑、application latency 暴漲；log 出現 <code>database &quot;myapp&quot; must be vacuumed within X transactions</code>。</p>
<p><strong>根因</strong>：autovacuum_freeze_max_age（預設 200M）到了、PostgreSQL <em>強制</em> 跑 anti-wraparound vacuum（即使在 peak）；這個 vacuum <em>不受 cost_limit 限制</em>、跑到完才停、表大時要幾小時、跟 OLTP query 搶 IO。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：<code>autovacuum_freeze_max_age</code> 拉到 1B（10 億）、給 freeze 更多時間在 off-peak 自然發生</li>
<li><strong>per-table freeze</strong>：hot table 設 <code>autovacuum_freeze_max_age = 100M</code>（提前在 off-peak freeze）、cold table 設 800M（避免不必要 freeze）</li>
<li><strong>緊急</strong>：手動跑 <code>VACUUM (FREEZE, VERBOSE) table_name;</code> 在 maintenance window 預先 freeze</li>
<li><strong>監測</strong>：<code>SELECT relname, age(relfrozenxid) FROM pg_class WHERE relkind = 'r' ORDER BY age(relfrozenxid) DESC LIMIT 20;</code> 看哪些表逼近 wraparound</li>
</ol>
<h3 id="case-4partition-table-把-autovacuum_max_workers-跑滿">Case 4：Partition table 把 autovacuum_max_workers 跑滿</h3>
<p><strong>徵兆</strong>：partition 後（時間 partition、12 個月分區）、autovacuum 跑很慢、<code>pg_stat_activity</code> 看到 3 個 autovacuum worker 都在跑 partition 表、其他 hot table queue 等很久。</p>
<p><strong>根因</strong>：<code>autovacuum_max_workers=3</code> 預設、每個 partition 算獨立 table；100 個 partition 中 50 個都需要 vacuum、worker 滿、其他 table 排隊。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>拉 <code>autovacuum_max_workers</code> 到 6-10（依 CPU core 數）</li>
<li>cold partition 設 <code>autovacuum_enabled = false</code>（已不寫的舊 partition）、減少 worker 競爭</li>
<li>partition 數量本身要克制 — 100+ partition 是訊號該重新評估 partition strategy</li>
</ol>
<h3 id="case-5index-bloat-沒被-vacuum-處理">Case 5：Index bloat 沒被 vacuum 處理</h3>
<p><strong>徵兆</strong>：表 vacuum 跑完了、<code>n_dead_tup</code> 為 0、但 index size 持續長大；query 用該 index 越來越慢、跟 sequential scan 差不多。</p>
<p><strong>根因</strong>：autovacuum 只處理 <em>heap</em>（table data）跟 <em>index leaf pages</em>；index B-tree 內部結構 fragmentation 不被 vacuum 處理。dead pointer 留在 index leaf page、查詢仍 traverse 過、IO 多。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>REINDEX CONCURRENTLY</code> 線上重建 index（PG 12+）、不鎖表</li>
<li>監測 index bloat：<code>pgstattuple_approx</code> extension 或 <code>pg_repack</code></li>
<li>預防：B-tree index 設計避免 high cardinality + 大量 UPDATE 同欄位（typical 場景：status column update）；考慮 <em>partial index</em> 或 <em>hash index</em>（PG 10+ logged）</li>
<li>大量 bloat index 用 <code>pg_repack</code> 重建（不需要 superuser、不鎖表）</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<p>vacuum capacity 用 <em>跟得上 dead tuple 產生速度</em> 衡量：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算方式</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dead tuple 產生 rate</td>
          <td><code>UPDATE/s + DELETE/s + ~10% INSERT/s（HOT update miss）</code></td>
          <td>跟 vacuum rate 對比</td>
      </tr>
      <tr>
          <td>vacuum 處理 rate</td>
          <td><code>cost_limit / cost_delay × page_size</code>、~MB/s 數量級</td>
          <td>跟 dead tuple rate 對比</td>
      </tr>
      <tr>
          <td>autovacuum_max_workers</td>
          <td>partition 數 + hot table 數 / 3-5</td>
          <td>100+ partition 必須拉 worker</td>
      </tr>
      <tr>
          <td>maintenance_work_mem</td>
          <td>1-2GB / vacuum worker</td>
          <td>全 worker 跑時的記憶體上限要 sizing</td>
      </tr>
      <tr>
          <td>anti-wraparound 觸發頻率</td>
          <td>預設 200M xid、write-heavy ~ 1-2 週觸發一次</td>
          <td>拉到 1B 後 ~ 2-3 月一次</td>
      </tr>
      <tr>
          <td>Bloat ratio</td>
          <td><code>pg_stat_user_tables.n_dead_tup / n_live_tup</code></td>
          <td>&gt; 50% 表示 vacuum 追不上</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>OLTP write-heavy（事件 / 訂單）：cost_limit 2000-5000、scale_factor 0.05、freeze_max_age 100M</li>
<li>OLTP read-heavy（user / config）：default 即可</li>
<li>Append-only log：scale_factor 0.5、freeze_max_age 800M、<code>autovacuum_enabled = false</code> for cold partition</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-partitioning-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">partitioning</a> 整合</h3>
<p>partitioning 是 vacuum 問題的長期解：</p>
<ul>
<li>大表（&gt; 100GB）vacuum 時間隨 size 線性、partition 後 vacuum 只動最近 partition</li>
<li>Cold partition <code>autovacuum_enabled = false</code> 完全停掉、新數據只在 hot partition</li>
<li>缺點：partition 數量爆炸時、autovacuum_max_workers 也要拉</li>
</ul>
<h3 id="跟-monitoring-整合">跟 monitoring 整合</h3>
<p>關鍵 metric：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- bloat 比例
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">relname</span><span class="p">,</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="p">,</span><span class="w"> </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">       </span><span class="n">round</span><span class="p">(</span><span class="n">n_dead_tup</span><span class="p">::</span><span class="nb">numeric</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">nullif</span><span class="p">(</span><span class="n">n_live_tup</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">dead_pct</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">n_live_tup</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- vacuum 進度
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_progress_vacuum</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- xid wraparound 距離
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">datname</span><span class="p">,</span><span class="w"> </span><span class="n">age</span><span class="p">(</span><span class="n">datfrozenxid</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_database</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><p>Prometheus alert 三條：<code>dead_pct &gt; 30</code>、<code>vacuum_running_seconds &gt; 3600</code>、<code>xid_age &gt; 500000000</code>。</p>
<h3 id="跟-backup-window">跟 backup window</h3>
<p>VACUUM FREEZE 在 backup 前跑能減少 backup size（freeze tuple 不需要 special handling）：</p>
<ol>
<li>每週 maintenance window 跑 <code>VACUUM (FREEZE, ANALYZE) hot_table</code> — 預先 freeze + 更新 stats</li>
<li>backup 前避免長 transaction、確保 vacuum 能跑</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>HOT update 跟 fillfactor</strong>：UPDATE 同頁可重用空間、fillfactor 80 為 hot table 留 20% buffer</li>
<li><strong><code>pg_repack</code> vs <code>VACUUM FULL</code></strong>：online vs offline、長期維護工具選擇</li>
<li><strong>PostgreSQL 14+ parallel vacuum</strong>：index vacuum 平行化、大表受益明顯</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">High Concurrency Access</a> — vacuum 是 concurrency 治理一環</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">Declarative Partitioning</a> / <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>（為什麼會有 dead tuple、跟 lock 互動）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Parca</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/parca/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/parca/</guid><description>&lt;p>Parca 的核心責任是用開源 continuous profiling 與 eBPF 路線建立 infrastructure-wide profile evidence。它適合需要低侵入、跨 process、跨 service、偏平台層的 profiling 團隊，重點在用 always-on profile 找出 CPU、memory、runtime 與 kernel / user space 的資源熱點。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Parca 是 Polar Signals 主導的 OSS continuous profiling、特色是 &lt;em>eBPF-based 採集 + pprof 標準格式 + Prometheus-style 拉取與 label 模型&lt;/em>。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope&lt;/a> 是 OSS 同類、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler&lt;/a> 則是 OSS / 自管 vs SaaS / APM 整合的差異。eBPF agent 直接從 kernel 採 stack trace、不需要 application 改 code 或注入 runtime agent；pprof 格式讓既有 Go / Java / Python 工具鏈可以直接讀；Prometheus-style scrape 讓 Parca server 跟 metrics 用同一套 service discovery 與 label。&lt;/p>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Parca 部署是否能撐起 platform-wide profiling、最少看四件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>eBPF agent deploy&lt;/strong>：Parca Agent 走 DaemonSet 跑在每個 node、需要 kernel ≥ 4.18（CO-RE / BTF）、&lt;code>SYS_ADMIN&lt;/code> 或 &lt;code>PERF_EVENT&lt;/code> capability、host PID namespace。受管 Kubernetes（GKE / EKS / AKS）的 worker node 是否允許這個權限是第一個判讀點&lt;/li>
&lt;li>&lt;strong>Parca server scrape&lt;/strong>：server 跟 agent 走 pull-based、Prometheus-style ServiceMonitor / scrape config、label 跟 metrics 同模型（namespace / pod / container / node）。scrape interval、retention、storage backend（FrostDB 內建 / object storage）要明確&lt;/li>
&lt;li>&lt;strong>pprof query&lt;/strong>：profile 以 pprof format 存、Parca UI 提供 flame graph 與 compare view、也可 export pprof file 給 &lt;code>go tool pprof&lt;/code> 或其他既有工具離線分析&lt;/li>
&lt;li>&lt;strong>Grafana integration&lt;/strong>：Parca 提供 datasource plugin、profile 可以跟 metrics / log / trace 在 Grafana 同一頁 correlate、配 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope&lt;/a> 或 Tempo 形成 observability 對齊&lt;/li>
&lt;/ul>
&lt;p>四件事任一缺失、就是 profiling control plane 還沒上線的待補項目。&lt;/p></description><content:encoded><![CDATA[<p>Parca 的核心責任是用開源 continuous profiling 與 eBPF 路線建立 infrastructure-wide profile evidence。它適合需要低侵入、跨 process、跨 service、偏平台層的 profiling 團隊，重點在用 always-on profile 找出 CPU、memory、runtime 與 kernel / user space 的資源熱點。</p>
<h2 id="服務定位">服務定位</h2>
<p>Parca 是 Polar Signals 主導的 OSS continuous profiling、特色是 <em>eBPF-based 採集 + pprof 標準格式 + Prometheus-style 拉取與 label 模型</em>。它跟 <a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a> 是 OSS 同類、跟 <a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a> 則是 OSS / 自管 vs SaaS / APM 整合的差異。eBPF agent 直接從 kernel 採 stack trace、不需要 application 改 code 或注入 runtime agent；pprof 格式讓既有 Go / Java / Python 工具鏈可以直接讀；Prometheus-style scrape 讓 Parca server 跟 metrics 用同一套 service discovery 與 label。</p>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Parca 部署是否能撐起 platform-wide profiling、最少看四件事：</p>
<ul>
<li><strong>eBPF agent deploy</strong>：Parca Agent 走 DaemonSet 跑在每個 node、需要 kernel ≥ 4.18（CO-RE / BTF）、<code>SYS_ADMIN</code> 或 <code>PERF_EVENT</code> capability、host PID namespace。受管 Kubernetes（GKE / EKS / AKS）的 worker node 是否允許這個權限是第一個判讀點</li>
<li><strong>Parca server scrape</strong>：server 跟 agent 走 pull-based、Prometheus-style ServiceMonitor / scrape config、label 跟 metrics 同模型（namespace / pod / container / node）。scrape interval、retention、storage backend（FrostDB 內建 / object storage）要明確</li>
<li><strong>pprof query</strong>：profile 以 pprof format 存、Parca UI 提供 flame graph 與 compare view、也可 export pprof file 給 <code>go tool pprof</code> 或其他既有工具離線分析</li>
<li><strong>Grafana integration</strong>：Parca 提供 datasource plugin、profile 可以跟 metrics / log / trace 在 Grafana 同一頁 correlate、配 <a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a> 或 Tempo 形成 observability 對齊</li>
</ul>
<p>四件事任一缺失、就是 profiling control plane 還沒上線的待補項目。</p>
<h2 id="定位">定位</h2>
<p>Parca 適合平台團隊建立 profiling control plane。當問題橫跨 Kubernetes cluster、node pool、multi-service path 或 shared runtime 成本，Parca 能從更接近 infrastructure 的角度收集 profile。</p>
<p>這個定位讓 Parca 接到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 與 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a>。它的價值在於低侵入與平台廣度；它的代價在於 eBPF 支援、symbolization、storage、權限與平台維運責任。</p>
<h2 id="適用場景">適用場景</h2>
<p>Infrastructure-wide profiling 適合 Parca。平台團隊可以觀察 cluster、node、namespace、service 與 process 的 CPU 熱點，找出共同 library、runtime、sidecar、agent 或 kernel path 的成本。</p>
<p>Kubernetes 平台適合 Parca。當服務在多 namespace、多 workload、多 node pool 上運作，Parca 可以把 profile 維度接到 pod、container、node、namespace 與 label。</p>
<p>低侵入 profiling 適合 Parca。eBPF-based profiling 可以降低 application instrumentation 成本，讓團隊先取得廣域視角，再對特定服務加更細的 runtime profiler 或 APM 整合。</p>
<h2 id="選型判準">選型判準</h2>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>Parca 的價值</th>
          <th>需要補的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>eBPF / low overhead</td>
          <td>低侵入取得廣域 profile</td>
          <td>kernel / runtime 支援與權限治理</td>
      </tr>
      <tr>
          <td>Platform-wide</td>
          <td>node、namespace、service 維度可對照</td>
          <td>Kubernetes label 與 ownership discipline</td>
      </tr>
      <tr>
          <td>Open source</td>
          <td>profiling platform 可自管</td>
          <td>storage、retention、upgrade</td>
      </tr>
      <tr>
          <td>Compare / diff</td>
          <td>profile compare 支援退化定位</td>
          <td>deploy label、baseline 與 symbolization</td>
      </tr>
  </tbody>
</table>
<p>eBPF / low overhead 價值來自平台廣度。團隊可以先觀察整個基礎設施的 CPU 熱點，再決定哪些服務需要更深入的 application-level profiling。</p>
<p>Platform-wide 價值來自共同成本治理。Sidecar、agent、logging library、serialization library 或 runtime upgrade 的成本可能散在多個服務中，Parca 這類工具能把分散成本聚合回平台決策。</p>
<h2 id="跟其他工具的取捨">跟其他工具的取捨</h2>
<p>Parca 和 Datadog Continuous Profiler 的主要差異是平台模型。Parca 偏開源、自管、eBPF 與 infra-wide profiling；Datadog 偏 SaaS、APM drilldown、deployment marker 與產品化 workflow。</p>
<p>Parca 和 Pyroscope 的主要差異是視角。Pyroscope 偏 Grafana / application profiling backend；Parca 偏 eBPF、Kubernetes / infrastructure-level profiling 與平台團隊治理。</p>
<p>Parca 和 language runtime profiler 的主要差異是導入方式。Runtime profiler 能提供語言特定維度；Parca 能先提供低侵入廣域 profile，但 symbolization 與語言細節需要額外治理。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Parca</th>
          <th>Pyroscope</th>
          <th>Datadog Continuous Profiler</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>採集方式</td>
          <td>eBPF agent（kernel-level、unwound）</td>
          <td>eBPF + SDK 雙路、語言 SDK 較豐富</td>
          <td>APM agent 內建、語言 SDK 整合</td>
      </tr>
      <tr>
          <td>Profile format</td>
          <td>pprof（Google 標準）</td>
          <td>自家 + pprof export</td>
          <td>Datadog proprietary、可 export pprof</td>
      </tr>
      <tr>
          <td>採集模型</td>
          <td>Pull-based、Prometheus-style scrape</td>
          <td>Push or pull（Grafana Agent）</td>
          <td>Push to Datadog backend</td>
      </tr>
      <tr>
          <td>Label 模型</td>
          <td>Prometheus label（namespace / pod）</td>
          <td>Grafana label</td>
          <td>Datadog tag</td>
      </tr>
      <tr>
          <td>部署模型</td>
          <td>Self-hosted OSS + Polar Signals SaaS</td>
          <td>Self-hosted OSS + Grafana Cloud SaaS</td>
          <td>SaaS only</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>FrostDB 內建 / object storage</td>
          <td>自家 storage / Grafana backend</td>
          <td>Datadog managed</td>
      </tr>
      <tr>
          <td>APM 整合</td>
          <td>弱 — 走 Grafana correlation</td>
          <td>中 — Grafana stack 整合</td>
          <td>強 — trace ↔ profile drilldown 內建</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Platform team 自管、Prometheus stack</td>
          <td>Grafana stack 已用、應用層 profiling</td>
          <td>已用 Datadog、APM-first、SaaS-only 可</td>
      </tr>
  </tbody>
</table>
<h2 id="進階主題">進階主題</h2>
<p><strong>Polar Signals Cloud</strong>：Parca 上游公司 Polar Signals 提供 managed SaaS — agent 一樣走 OSS、server / storage / UI 託管。適合不想養 Parca server 又要 OSS agent 路線的團隊。差異點是 ingestion cost 跟 retention 由 SaaS 計費、license / data residency 要看合約。</p>
<p><strong>Prometheus 同 label model</strong>：Parca 的 service discovery、scrape config 跟 label 跟 Prometheus 幾乎同形 — 既有 ServiceMonitor、relabel rule、Kubernetes SD 可以直接複用。意義是 profile 維度跟 metric 維度天然對齊、<code>namespace=foo, service=bar</code> 在兩邊都成立、cross-signal correlation 不需要再 mapping。</p>
<p><strong>Compare profiles（diff before/after deploy）</strong>：Parca UI 支援選 baseline window 跟 candidate window 做 flame graph diff、顏色標示哪個 stack frame 變胖變瘦。配 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a> 的 deploy marker、可以把「這次發版讓 CPU +15%」直接歸因到具體 frame。</p>
<p><strong>Continuous profiling vs sampling-only</strong>：傳統 profiler 是「出問題時手動跑 30 秒」、Parca 是「always-on、低頻率持續採」。差異是 <em>事後回溯能力</em> — incident 發生時直接拉時間區間的 profile、不用重現問題；sampling-only 工具在偶發 spike 時抓不到現場。代價是 storage 跟 agent overhead 要長期治理。</p>
<h2 id="操作成本">操作成本</h2>
<p>Parca 的主要成本是平台維運。Agent / scraper、server、storage、retention、symbolization、upgrade 與 Kubernetes 權限都需要平台團隊負責。</p>
<p>Symbolization 成本來自可讀性。Profile 如果缺 symbol、debug info、build ID 或 source mapping，flame graph 會變成難以行動的 address / binary offset，因此 build pipeline 要保留符號資訊策略。</p>
<p>權限成本來自 eBPF 與 node visibility。低層 profiling 需要足夠 host / kernel 權限，受管 Kubernetes、security policy、multi-tenant cluster 與 compliance 要先評估。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Parca 結果應回寫到 evidence package。最小欄位包括 cluster、namespace、service、node pool、profile type、baseline window、candidate window、compare link、symbolization status、agent overhead、known gap 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Parca 證據來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>Parca query、compare view、flame graph</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>baseline / candidate profile window</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>Parca UI / dashboard / metrics link</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>label completeness、symbolization status</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>cluster coverage、agent overhead</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>未覆蓋 node、symbol 缺失、kernel 限制</td>
      </tr>
  </tbody>
</table>
<p>Evidence package 的核心用途是把平台層 profile 變成容量決策。Reviewer 要能看到成本來自 application code、runtime、sidecar、kernel path 還是 shared library，並把結果回寫到 owner。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>eBPF agent 起不來 / kernel 不支援</strong>：舊 kernel（&lt; 4.18）或缺 BTF / CO-RE 支援、受管 Kubernetes 不開 <code>SYS_ADMIN</code> — 先確認 node OS image、必要時換 distribution 或升級 worker node pool</li>
<li><strong>Profile storage 暴增</strong>：scrape interval 太密 + retention 沒設 + label cardinality 爆炸（把 request-id 放進 label）— 降頻、限 retention window、把高 cardinality 維度移出 profile label</li>
<li><strong>Symbol resolution 失敗 / flame graph 全是 address</strong>：build pipeline 沒保留 debug info、stripped binary、容器 image 不含符號 — 在 build 階段保留 debug symbol、用 separate debuginfo 上傳 Parca debuginfod、或在 image 保留 unstripped binary</li>
<li><strong>JIT 語言（Java / Node.js）stack 不完整</strong>：eBPF 看到的是 native frame、JIT-compiled frame 需要額外 perf map / JVMTI agent — 補語言層 profiler 或開 JIT symbol dump</li>
<li><strong>Agent overhead 影響 production</strong>：sample rate 預設 19 Hz、特定 workload 可能仍敏感 — 在 noisy neighbor 敏感的 node pool 降頻或排除特定 namespace</li>
<li><strong>多 cluster scrape 中心化太重</strong>：單一 Parca server 拉 N 個 cluster 變瓶頸 — 改 federation 模型、每 cluster 一個 Parca server、上層做 query aggregation</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>Parca 適合回寫平台層與 multi-service 成本案例。它可接 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130K node GKE cluster</a> 的 cluster-scale profiling 需求、<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 EKS multi-cluster</a> 的 246 cluster 平台成本治理、<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 consolidation</a> 的 shared platform noise 降低、<a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch Azure AKS</a> 的傳統產業多 BU 平台層歸因，以及 <a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom DynamoDB + EKS</a> 跨遊戲共用後端的 profile 切分。</p>
<p>這些案例的重點是平台視角。Parca 頁引用案例時，要把 case 轉成 cluster / namespace / service label、compare window、symbolization、shared library cost 與 owner routing — 例如 GCP 130K-node 規模下，Parca 自身的 storage / scrape capacity 也成為 profile target、不只是觀測 application。</p>
<p>兩個典型用途值得單獨點名：</p>
<ul>
<li><strong>Performance regression detection</strong>：發版前後拉 compare profile、把「這次 release 讓 P99 CPU +18%」歸因到具體 stack frame。配 <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 EKS multi-cluster</a> 的 246 cluster 規模、單一 service rollout 在 always-on profile 下可秒級看出 hot path 變化、不需要等 SRE 跑手動 pprof</li>
<li><strong>Cost engineering</strong>：把 CPU profile 折算成 node 成本、找出 shared library / runtime / sidecar 的 hidden cost。配 <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 consolidation</a> 的 platform consolidation 思路、profile 證據可以決定要不要重寫熱點、換 library、還是接受成本</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a></li>
<li>官方：<a href="https://www.parca.dev/docs/">Parca documentation</a></li>
</ul>
]]></content:encoded></item><item><title>9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/</guid><description>&lt;p>這個案例的核心責任是補強 Azure DB-OLTP 維度缺口。Clearent 是美國的中型支付處理商、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered 跨市場銀行 OLTP&lt;/a> 形成對照 — 一個是合規驅動的跨市場分割、一個是單一規模的高吞吐處理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Clearent 在 Azure SQL Hyperscale 的關鍵敘述（引自 &lt;a href="https://www.microsoft.com/en/customers/story/774969-clearent-banking-capital-markets-azure">Clearent Customer Story&lt;/a>）：&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>年交易量&lt;/td>
 &lt;td>5 億筆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客戶基礎&lt;/td>
 &lt;td>各種規模 merchants（中小型為主）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>Azure SQL Database Hyperscale 服務級&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>架構模式&lt;/td>
 &lt;td>modern microservices architecture&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>擴展能力&lt;/td>
 &lt;td>「scale automatically and almost infinitely」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>並發特性&lt;/td>
 &lt;td>「tens of thousands of users 同時存取」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務驅動&lt;/td>
 &lt;td>「unite all its information in one place」+ 「faster insights」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵特性：Azure SQL Hyperscale 把 storage 跟 compute 分離、跟 &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> 的 Aurora 是同類設計。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Clearent 案例揭露三個 Hyperscale 設計的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>5 億筆 / 年 ≈ 1500 筆 / 秒平均、但 peak 可能 10-50x&lt;/strong>：支付交易有日內 / 月內 / 季內節律。早上 9-11 點商家對帳高峰、下午 12-1 點消費高峰、晚上 6-8 點消費高峰、月底結算高峰。容量規劃必須按 peak 訂、不是平均。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 的 peak/avg ratio 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a>。&lt;/li>
&lt;li>&lt;strong>Hyperscale = storage / compute 解耦&lt;/strong>：傳統 SQL Server primary 對 storage 跟 CPU / RAM 綁定、擴 storage 就要換更大 instance、不便。Hyperscale 把 storage 拉到分散式 log service、可以獨立擴 storage（最高 100 TB）、compute 獨立擴。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner&lt;/a> 的同類分離思維、跟 &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>。&lt;/li>
&lt;li>&lt;strong>「unite all information in one place」是支付業的特殊需求&lt;/strong>：merchants 需要對帳、退款、清算、稅務報表都即時可查、不能 OLAP 分開。Hyperscale 的 read scale-out（最多 4 個 secondary replica）讓即時報表跑在 OLTP DB 上不影響交易吞吐。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：「scale automatically and almost infinitely」是行銷敘述。實際 Hyperscale 有上限（100 TB storage、Gen5 series 80 vCore）、超過要 sharding 應用層分散。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 Azure DB-OLTP 維度缺口。Clearent 是美國的中型支付處理商、跟 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered 跨市場銀行 OLTP</a> 形成對照 — 一個是合規驅動的跨市場分割、一個是單一規模的高吞吐處理。</p>
<h2 id="觀察">觀察</h2>
<p>Clearent 在 Azure SQL Hyperscale 的關鍵敘述（引自 <a href="https://www.microsoft.com/en/customers/story/774969-clearent-banking-capital-markets-azure">Clearent Customer Story</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>年交易量</td>
          <td>5 億筆</td>
      </tr>
      <tr>
          <td>客戶基礎</td>
          <td>各種規模 merchants（中小型為主）</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>Azure SQL Database Hyperscale 服務級</td>
      </tr>
      <tr>
          <td>架構模式</td>
          <td>modern microservices architecture</td>
      </tr>
      <tr>
          <td>擴展能力</td>
          <td>「scale automatically and almost infinitely」</td>
      </tr>
      <tr>
          <td>並發特性</td>
          <td>「tens of thousands of users 同時存取」</td>
      </tr>
      <tr>
          <td>業務驅動</td>
          <td>「unite all its information in one place」+ 「faster insights」</td>
      </tr>
  </tbody>
</table>
<p>關鍵特性：Azure SQL Hyperscale 把 storage 跟 compute 分離、跟 <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> 的 Aurora 是同類設計。</p>
<h2 id="判讀">判讀</h2>
<p>Clearent 案例揭露三個 Hyperscale 設計的工程重點。</p>
<ol>
<li><strong>5 億筆 / 年 ≈ 1500 筆 / 秒平均、但 peak 可能 10-50x</strong>：支付交易有日內 / 月內 / 季內節律。早上 9-11 點商家對帳高峰、下午 12-1 點消費高峰、晚上 6-8 點消費高峰、月底結算高峰。容量規劃必須按 peak 訂、不是平均。對應 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 的 peak/avg ratio 跟 <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><strong>Hyperscale = storage / compute 解耦</strong>：傳統 SQL Server primary 對 storage 跟 CPU / RAM 綁定、擴 storage 就要換更大 instance、不便。Hyperscale 把 storage 拉到分散式 log service、可以獨立擴 storage（最高 100 TB）、compute 獨立擴。對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> 的同類分離思維、跟 <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>。</li>
<li><strong>「unite all information in one place」是支付業的特殊需求</strong>：merchants 需要對帳、退款、清算、稅務報表都即時可查、不能 OLAP 分開。Hyperscale 的 read scale-out（最多 4 個 secondary replica）讓即時報表跑在 OLTP DB 上不影響交易吞吐。</li>
</ol>
<p>需要警惕：「scale automatically and almost infinitely」是行銷敘述。實際 Hyperscale 有上限（100 TB storage、Gen5 series 80 vCore）、超過要 sharding 應用層分散。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>Hyperscale 跟 Aurora 是同類設計、選型按生態</strong>：Azure 生態用 Hyperscale、AWS 生態用 Aurora、GCP 用 AlloyDB / Spanner。三家底層工程哲學一致（log-structured storage、storage / compute 分離）、選哪家取決於 application 已在哪個 cloud。</li>
<li><strong>微服務 + 共用 OLTP 是支付業常見架構</strong>：服務拆細、但 OLTP 仍是 single source of truth、共用一個 Hyperscale cluster。這跟 <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 microservice 各自 Aurora</a> 不同 — Netflix 每微服務 <em>自己</em> Aurora、Clearent 微服務共用 Hyperscale。取捨：Clearent 的「對帳一致性」需求讓共用更划算。</li>
<li><strong>支付業容量規劃以 peak 為主</strong>：不能用平均 RPS 規劃、要按單日 / 單秒 peak。歷史 peak × 預期成長 × headroom 是基本公式（<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>）。</li>
</ol>
<p>跨平台等效：AWS Aurora Serverless v2、GCP AlloyDB、Spanner、PostgreSQL 自管 + Patroni 都可實作對等架構。差異是 vendor managed 程度跟 OLAP / OLTP 統一視覺。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他 OLTP 案例 → <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> / <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> / <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></li>
<li>想設計支付業容量 → <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> + <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>想理解 storage / compute 分離 → <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.microsoft.com/en/customers/story/774969-clearent-banking-capital-markets-azure">Clearent scales its modern microservices architecture to handle 500 million payment transactions a year</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/announcing-azure-sql-database-hyperscale-public-preview/">Announcing Azure SQL Database Hyperscale</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/get-high-performance-scaling-for-your-azure-database-workloads-with-hyperscale/">Get high-performance scaling for your Azure database workloads with Hyperscale</a></li>
</ul>
]]></content:encoded></item><item><title>Metrics</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/</guid><description>&lt;p>Metrics 的核心概念是「用可聚合數值描述系統行為的時間序列」。常見指標包括 request count、error rate、latency、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、CPU、memory、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 使用量與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Metrics 是趨勢觀測跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的基礎。跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>（事件明細）跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>（跨服務路徑）互補：log 適合查單一事件的細節，trace 適合看一次 request 的路徑，metrics 適合回答「服務是否在變慢、錯誤是否在增加、容量是否接近上限」。&lt;/p>
&lt;p>Metrics 有三種基本型別：counter（累積計數、只增不減）、gauge（瞬間值、可增可減）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>（分布、支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> 計算）。選錯型別會讓後面的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 跟 alert 建立在錯誤訊號上。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 metrics 的訊號是團隊需要在使用者回報前知道服務異常。Checkout p95 latency 上升、Redis &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 增加、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> lag 擴大，都應先從 metrics 看見。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Metrics 設計要選擇正確的型別（latency 用 histogram、request count 用 counter、connection pool size 用 gauge）跟有界的 label（service、method、status_code，排除 user_id / request_id）。重要指標要能對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>；高 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a> label 會推高儲存跟查詢成本。Metrics 的聚合查詢跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Metrics 的核心概念是「用可聚合數值描述系統行為的時間序列」。常見指標包括 request count、error rate、latency、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、CPU、memory、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 使用量與 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Metrics 是趨勢觀測跟 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的基礎。跟 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>（事件明細）跟 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>（跨服務路徑）互補：log 適合查單一事件的細節，trace 適合看一次 request 的路徑，metrics 適合回答「服務是否在變慢、錯誤是否在增加、容量是否接近上限」。</p>
<p>Metrics 有三種基本型別：counter（累積計數、只增不減）、gauge（瞬間值、可增可減）、<a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>（分布、支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 計算）。選錯型別會讓後面的 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 跟 alert 建立在錯誤訊號上。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 metrics 的訊號是團隊需要在使用者回報前知道服務異常。Checkout p95 latency 上升、Redis <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 增加、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> lag 擴大，都應先從 metrics 看見。</p>
<h2 id="設計責任">設計責任</h2>
<p>Metrics 設計要選擇正確的型別（latency 用 histogram、request count 用 counter、connection pool size 用 gauge）跟有界的 label（service、method、status_code，排除 user_id / request_id）。重要指標要能對應 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a> 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>；高 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> label 會推高儲存跟查詢成本。Metrics 的聚合查詢跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 設計見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>。</p>
]]></content:encoded></item><item><title>Aurora RDS Proxy 與連線管理：connection multiplexing、pinning 陷阱與 failover 加速</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/rds-proxy-connection-pooling/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/rds-proxy-connection-pooling/</guid><description>&lt;p>Lambda 函式在流量尖峰被同時拉起幾百個實例、每個各自開一條到 Aurora 的連線、Aurora 的 connection 上限瞬間被打爆、新請求拿不到連線、整批失敗。根因是 &lt;em>連線管理&lt;/em> 缺位、Aurora 容量本身夠用——serverless 與高並發短連線 workload 製造的連線數遠超過資料庫該同時維持的後端連線。RDS Proxy 在 application 與 Aurora 之間做 connection multiplexing，把大量 client 連線收斂成少量後端連線。但它不是「連上去就自動省」——某些 session 操作會讓連線被 pin 住、multiplexing 失效。&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>）— 而是 RDS Proxy 連線管理機制與陷阱的實作層教學。&lt;/p>
&lt;h2 id="核心機制connection-multiplexing">核心機制：connection multiplexing&lt;/h2>
&lt;p>RDS Proxy 維護一個到 Aurora 的後端連線池，多個 client 連線共享這些後端連線。當 client 連線閒置（交易之間沒有活動），proxy 可以把對應的後端連線釋放回池子給其他 client 用：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>沒有 proxy&lt;/th>
 &lt;th>有 RDS Proxy&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>每個 client 連線 = 一條後端連線&lt;/td>
 &lt;td>多個 client 連線共享少量後端連線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lambda 並發 N → 後端 N 條連線&lt;/td>
 &lt;td>Lambda 並發 N → 後端遠少於 N 條&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>failover 時 client 連線斷、要重連&lt;/td>
 &lt;td>proxy 保持 client 連線、後端切換對 client 透明&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>連線建立開銷由 application 承擔&lt;/td>
 &lt;td>proxy 維持暖連線池、省去反覆建立&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>multiplexing 生效的前提是 client 連線「閒置時可以被借走」。這只在連線處於 &lt;em>交易之間&lt;/em> 的乾淨狀態時成立——一旦連線帶了交易內狀態，proxy 不能把它借給別人，這就是 pinning。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「RDS Proxy 支援的 engine / 連線數上限 / IAM 認證細節」屬 AWS vendor 規格、實作時 cross-verify 官方 doc 當前值。本文不含 production case 揭露的 proxy 配置數字。&lt;/p>&lt;/blockquote>
&lt;p>對應 knowledge card：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>。&lt;/p>
&lt;h2 id="pinningmultiplexing-失效的主因">Pinning：multiplexing 失效的主因&lt;/h2>
&lt;p>Pinning 是 RDS Proxy 最常被忽略、卻直接決定省連線效果的機制。當 client 在連線上做了「跨交易持續的 session 狀態」操作，proxy 無法安全地把這條後端連線借給其他 client，於是把它 &lt;em>pin&lt;/em>（綁定）到該 client 直到連線關閉——這條後端連線在 pin 期間不參與 multiplexing。&lt;/p>
&lt;p>常見觸發 pinning 的操作：&lt;/p>
&lt;ul>
&lt;li>session 層級的變數設定（&lt;code>SET&lt;/code> 某些 session variable）&lt;/li>
&lt;li>建立 temp table&lt;/li>
&lt;li>prepared statement（某些情況）&lt;/li>
&lt;li>advisory lock、保持開啟的交易&lt;/li>
&lt;li>部分 session 層級的設定語句&lt;/li>
&lt;/ul>
&lt;p>pinning 的後果是「明明裝了 RDS Proxy、後端連線數卻沒降下來」。若大量 client 都觸發 pinning，等於退化回「一個 client 一條後端連線」、proxy 白裝。&lt;/p>
&lt;p>&lt;strong>判讀與修法方向&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>監控 &lt;code>DatabaseConnectionsCurrentlySessionPinned&lt;/code>，看 pinning 比例&lt;/li>
&lt;li>application 端避免不必要的 session 狀態（少用 session variable、temp table；改用交易內可清理的方式）&lt;/li>
&lt;li>真的需要 session 狀態的 workload，接受該連線會 pin、或評估這類 workload 是否適合走 proxy&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「哪些具體語句觸發 pinning」隨 RDS Proxy 版本與 engine 演進、實作時以 AWS doc 當前清單為準；本段列舉是常見類型、非完整或固定清單。&lt;/p></description><content:encoded><![CDATA[<p>Lambda 函式在流量尖峰被同時拉起幾百個實例、每個各自開一條到 Aurora 的連線、Aurora 的 connection 上限瞬間被打爆、新請求拿不到連線、整批失敗。根因是 <em>連線管理</em> 缺位、Aurora 容量本身夠用——serverless 與高並發短連線 workload 製造的連線數遠超過資料庫該同時維持的後端連線。RDS Proxy 在 application 與 Aurora 之間做 connection multiplexing，把大量 client 連線收斂成少量後端連線。但它不是「連上去就自動省」——某些 session 操作會讓連線被 pin 住、multiplexing 失效。</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>）— 而是 RDS Proxy 連線管理機制與陷阱的實作層教學。</p>
<h2 id="核心機制connection-multiplexing">核心機制：connection multiplexing</h2>
<p>RDS Proxy 維護一個到 Aurora 的後端連線池，多個 client 連線共享這些後端連線。當 client 連線閒置（交易之間沒有活動），proxy 可以把對應的後端連線釋放回池子給其他 client 用：</p>
<table>
  <thead>
      <tr>
          <th>沒有 proxy</th>
          <th>有 RDS Proxy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個 client 連線 = 一條後端連線</td>
          <td>多個 client 連線共享少量後端連線</td>
      </tr>
      <tr>
          <td>Lambda 並發 N → 後端 N 條連線</td>
          <td>Lambda 並發 N → 後端遠少於 N 條</td>
      </tr>
      <tr>
          <td>failover 時 client 連線斷、要重連</td>
          <td>proxy 保持 client 連線、後端切換對 client 透明</td>
      </tr>
      <tr>
          <td>連線建立開銷由 application 承擔</td>
          <td>proxy 維持暖連線池、省去反覆建立</td>
      </tr>
  </tbody>
</table>
<p>multiplexing 生效的前提是 client 連線「閒置時可以被借走」。這只在連線處於 <em>交易之間</em> 的乾淨狀態時成立——一旦連線帶了交易內狀態，proxy 不能把它借給別人，這就是 pinning。</p>
<blockquote>
<p><strong>Scope warning</strong>：「RDS Proxy 支援的 engine / 連線數上限 / IAM 認證細節」屬 AWS vendor 規格、實作時 cross-verify 官方 doc 當前值。本文不含 production case 揭露的 proxy 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a>。</p>
<h2 id="pinningmultiplexing-失效的主因">Pinning：multiplexing 失效的主因</h2>
<p>Pinning 是 RDS Proxy 最常被忽略、卻直接決定省連線效果的機制。當 client 在連線上做了「跨交易持續的 session 狀態」操作，proxy 無法安全地把這條後端連線借給其他 client，於是把它 <em>pin</em>（綁定）到該 client 直到連線關閉——這條後端連線在 pin 期間不參與 multiplexing。</p>
<p>常見觸發 pinning 的操作：</p>
<ul>
<li>session 層級的變數設定（<code>SET</code> 某些 session variable）</li>
<li>建立 temp table</li>
<li>prepared statement（某些情況）</li>
<li>advisory lock、保持開啟的交易</li>
<li>部分 session 層級的設定語句</li>
</ul>
<p>pinning 的後果是「明明裝了 RDS Proxy、後端連線數卻沒降下來」。若大量 client 都觸發 pinning，等於退化回「一個 client 一條後端連線」、proxy 白裝。</p>
<p><strong>判讀與修法方向</strong>：</p>
<ul>
<li>監控 <code>DatabaseConnectionsCurrentlySessionPinned</code>，看 pinning 比例</li>
<li>application 端避免不必要的 session 狀態（少用 session variable、temp table；改用交易內可清理的方式）</li>
<li>真的需要 session 狀態的 workload，接受該連線會 pin、或評估這類 workload 是否適合走 proxy</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「哪些具體語句觸發 pinning」隨 RDS Proxy 版本與 engine 演進、實作時以 AWS doc 當前清單為準；本段列舉是常見類型、非完整或固定清單。</p></blockquote>
<h2 id="failover-加速">Failover 加速</h2>
<p>RDS Proxy 的第二個價值是縮短 failover 對 application 的中斷。沒有 proxy 時，writer failover 會讓所有 client 連線斷掉、application 要偵測、重連、重建連線池；有 proxy 時，proxy 保持與 client 的連線、在後端把流量切到新 writer，client 端感知到的中斷時間縮短。</p>
<p>這對連線建立成本高、或 failover 期間不能大量重連的 workload 特別有價值。但 proxy 不消除 failover 本身——in-flight 的交易仍會失敗、application 仍要有 retry；proxy 縮短的是「重建連線」這段，不是「交易不中斷」。</p>
<h2 id="操作流程">操作流程</h2>
<p>從連線壓力判讀到上線的 6 步流程。</p>
<h4 id="step-1確認是不是連線問題">Step 1：確認是不是連線問題</h4>
<p>先區分「Aurora 容量不夠」vs「連線管理問題」。看 <code>DatabaseConnections</code> 是否逼近上限、且 CPU/IOPS 還有餘量——後者是典型的連線數問題、proxy 能解；若是 CPU/IOPS 飽和，proxy 不解。</p>
<h4 id="step-2判斷-workload-是否適合-proxy">Step 2：判斷 workload 是否適合 proxy</h4>
<ul>
<li>serverless / Lambda / 高並發短連線 → 適合（連線爆炸是主問題）</li>
<li>少量長連線、穩定的 application server → proxy 效益有限（連線數本就可控）</li>
<li>大量 session 狀態 workload → pinning 會吃掉 multiplexing 效益、要先評估</li>
</ul>
<h4 id="step-3建立-proxy">Step 3：建立 proxy</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">aws rds create-db-proxy <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --db-proxy-name my-aurora-proxy <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --engine-family POSTGRESQL <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --auth ... <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --role-arn ... <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --vpc-subnet-ids ...</span></span></code></pre></div><p>application 連到 proxy endpoint 而非直連 cluster endpoint。</p>
<h4 id="step-4減少-pinning">Step 4：減少 pinning</h4>
<p>review application 的 session 狀態使用、移除不必要的 <code>SET</code> / temp table；連線池設定避免長時間持有閒置連線。</p>
<h4 id="step-5驗證-multiplexing-生效">Step 5：驗證 multiplexing 生效</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"># 對照後端連線數：裝 proxy 後 Aurora 的 DatabaseConnections 應顯著低於 client 並發數
</span></span><span class="line"><span class="ln">2</span><span class="cl"># 看 DatabaseConnectionsCurrentlySessionPinned：pinning 比例高代表 multiplexing 沒發揮</span></span></code></pre></div><h4 id="step-6驗證-failover-行為">Step 6：驗證 failover 行為</h4>
<p>主動觸發一次 failover、測量 application 感知到的中斷時間、確認 retry 邏輯能吸收 in-flight 交易失敗。</p>
<p><strong>Rollback boundary</strong>：application 可在 proxy endpoint 與直連 cluster endpoint 間切換、proxy 出問題時改回直連（但直連會回到連線爆炸風險，要先確認後端撐得住）。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1裝了-proxy-但-pinning-比例高連線沒降">Case 1：裝了 proxy 但 pinning 比例高、連線沒降</h4>
<p>application 大量用 session variable / temp table、多數連線被 pin、後端連線數沒降、proxy 白裝。修法：監控 pinning 比例、減少 session 狀態；理解 proxy 的省連線前提是連線可被借走。</p>
<h4 id="case-2把-proxy-當aurora-容量擴充">Case 2：把 proxy 當「Aurora 容量擴充」</h4>
<p>連線數沒問題、是 CPU/IOPS 飽和、卻裝 proxy 期待變快。修法：proxy 解連線管理、不解運算容量；容量問題要擴 instance / 加 replica。</p>
<h4 id="case-3以為-proxy-讓-failover-零中斷">Case 3：以為 proxy 讓 failover 零中斷</h4>
<p>裝了 proxy 就拿掉 application 的 retry、failover 時 in-flight 交易失敗沒處理。修法：proxy 縮短重連時間、不保證交易不中斷；application 仍要 retry in-flight 交易。</p>
<h4 id="case-4少量長連線-workload-強裝-proxy">Case 4：少量長連線 workload 強裝 proxy</h4>
<p>穩定的 application server 連線數本就可控、裝 proxy 多一跳延遲、效益有限。修法：proxy 的價值在連線爆炸場景（serverless / 高並發短連線）；連線可控的 workload 不必加。</p>
<h4 id="case-5proxy-與自管-pooler-疊加未理清責任">Case 5：proxy 與自管 pooler 疊加未理清責任</h4>
<p>application 已有自管連線池（如語言層 pool）、又加 RDS Proxy、兩層 pool 互相打架、連線數行為難預測。修法：理清兩層職責——application 層 pool 管「app 到 proxy」、proxy 管「proxy 到 Aurora」；兩層設定要協調、不是各設各的。</p>
<p><strong>Anti-recommendation</strong>：連線數本就可控的少量長連線 workload、或 workload 大量依賴 session 狀態（pinning 會吃掉效益）→ 不必上 RDS Proxy；它的價值集中在 serverless / Lambda / 高並發短連線的連線爆炸場景。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>DatabaseConnections</code>（Aurora 端）：裝 proxy 後應顯著低於 client 並發數</li>
<li><code>DatabaseConnectionsCurrentlySessionPinned</code>：pinning 數、判斷 multiplexing 效益</li>
<li><code>ClientConnections</code>（proxy 端）：client 側連線數、對照後端收斂比例</li>
<li><code>QueryDatabaseResponseLatency</code>：proxy 多一跳的延遲影響</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li>後端連線數沒因 proxy 下降 → pinning 比例高或 workload 不適合</li>
<li>pinning 數持續高 → application session 狀態過多、需 review</li>
<li>proxy 延遲明顯 → 評估這一跳對延遲敏感路徑是否值得</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 proxy metric 數字；上述指標與判讀屬 vendor 規格 + 通用連線管理工程。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>、<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="rds-proxy-vs-自管-pgbouncer">RDS Proxy vs 自管 pgbouncer</h3>
<p>兩者都是 connection pooler，責任切分在「managed vs 自管」：</p>
<ul>
<li><strong>RDS Proxy</strong>：AWS managed、跟 Aurora / IAM / Secrets Manager 整合、零運維、含 failover 加速；綁 AWS</li>
<li><strong>自管 pgbouncer / pgcat</strong>：自己部署運維、pooling 模式（session / transaction / statement）可細調、跨雲可攜；運維責任自負</li>
</ul>
<p>PostgreSQL 的通用連線池機制與 pgbouncer 細節主寫於 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a> 與 <a href="/blog/backend/01-database/vendors/postgresql/connection-pooler-comparison/" data-link-title="PostgreSQL Connection Pooler Comparison" data-link-desc="PostgreSQL PgBouncer、Odyssey、RDS Proxy、application pool 與 transaction pooling 的選型比較">connection-pooler-comparison</a>；本篇聚焦 RDS Proxy 這個 AWS managed 方案的機制與 pinning 陷阱。要細調 pooling 模式、或需要跨雲可攜 → 評估自管 pooler；要零運維 + Aurora 原生整合 + failover 加速 → RDS Proxy。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/aurora/serverless-v2-scaling/" data-link-title="Aurora Serverless v2 適用判斷：ACU 自動擴縮、混合 cluster 與何時不該用" data-link-desc="Aurora Serverless v2 不是「比較便宜的 Aurora」；本文展開 ACU 計費粒度、秒級自動擴縮機制、min/max ACU 設定、serverless 與 provisioned 同 cluster 混用，以及穩定高負載下 serverless 反而更貴的成本 crossover 邊界">serverless-v2-scaling</a> — serverless + Lambda 場景的連線管理常與 RDS Proxy 一起出現</li>
<li><a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">cross-az-failover-rto</a> — proxy 縮短 failover 重連時間、與 RTO 目標結合</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgbouncer-config</a> / <a href="/blog/backend/01-database/vendors/postgresql/connection-pooler-comparison/" data-link-title="PostgreSQL Connection Pooler Comparison" data-link-desc="PostgreSQL PgBouncer、Odyssey、RDS Proxy、application pool 與 transaction pooling 的選型比較">connection-pooler-comparison</a> — 通用連線池 SSoT、自管方案對照</li>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a> — 連線池與 transaction 範圍控制</li>
<li>替代路由：需要細調 pooling 模式 / 跨雲 → 自管 pgbouncer</li>
</ul>
]]></content:encoded></item><item><title>DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>quarterly review 看 DynamoDB bill 突然漲 80%、追查發現是 dev team 把所有 table 切 on-demand「省 capacity 管理」。finance 反問「於是省了多少 SRE 工時、又多花多少 cost」、team 答不出來。反向情境：Black Friday 前一週 provisioned table auto-scaling 上限是日常 5 倍、但開賣瞬間流量是 50 倍、auto-scaling 反應週期 5 分鐘、前 10 分鐘大量 throttle。兩個 production 痛點指向同一件事 — capacity mode 選擇不能只看「peak/avg ratio &amp;gt; 5x」單軸閾值。&lt;/p>
&lt;p>本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 性質 / 事件分級 / DBA 工時釋放 / vendor crossover），把單軸決策樹擴成完整判讀框架。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>、本篇不重複展開。Capacity mode 選擇是 &lt;em>已選 DynamoDB 後&lt;/em> 的成本決策；若 workload 不適用 DynamoDB、mode 選擇無法救回 vendor 選錯的成本。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制兩種-mode-的工程差異">核心機制：兩種 mode 的工程差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>Provisioned&lt;/th>
 &lt;th>On-demand&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>計費方式&lt;/td>
 &lt;td>預先買 RCU/WCU、按 hour 計&lt;/td>
 &lt;td>按 request 計、無 capacity 預設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auto-scaling&lt;/td>
 &lt;td>動態調整、target utilization 70%、min / max&lt;/td>
 &lt;td>自動 scale、仍受單 partition 1000 WCU / 3000 RCU 上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Throttle 表現&lt;/td>
 &lt;td>&lt;code>WriteThrottleEvents&lt;/code> 立即可見、exception 拋出&lt;/td>
 &lt;td>不顯示 throttle、表現為 latency spike（hot partition 隱藏）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost 模型&lt;/td>
 &lt;td>可預測、低基礎 rate&lt;/td>
 &lt;td>按用量、cost-per-request 約 provisioned base rate 的 6-7 倍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mode 切換限制&lt;/td>
 &lt;td>24 小時內只能切一次&lt;/td>
 &lt;td>同左&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Auto-scaling 內部機制&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>CloudWatch alarm 觸發 → scaling activity → 1-5 分鐘調整 capacity&lt;/li>
&lt;li>target utilization 70%（建議值、留 buffer 給 scale latency）&lt;/li>
&lt;li>連續 spike 仍可能 throttle（auto-scaling 反應週期 &amp;gt; spike 速度）&lt;/li>
&lt;/ul>
&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>quarterly review 看 DynamoDB bill 突然漲 80%、追查發現是 dev team 把所有 table 切 on-demand「省 capacity 管理」。finance 反問「於是省了多少 SRE 工時、又多花多少 cost」、team 答不出來。反向情境：Black Friday 前一週 provisioned table auto-scaling 上限是日常 5 倍、但開賣瞬間流量是 50 倍、auto-scaling 反應週期 5 分鐘、前 10 分鐘大量 throttle。兩個 production 痛點指向同一件事 — capacity mode 選擇不能只看「peak/avg ratio &gt; 5x」單軸閾值。</p>
<p>本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 性質 / 事件分級 / DBA 工時釋放 / vendor crossover），把單軸決策樹擴成完整判讀框架。</p>
<blockquote>
<p><strong>DynamoDB 適用度前置判讀</strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>、本篇不重複展開。Capacity mode 選擇是 <em>已選 DynamoDB 後</em> 的成本決策；若 workload 不適用 DynamoDB、mode 選擇無法救回 vendor 選錯的成本。</p></blockquote>
<h2 id="核心機制兩種-mode-的工程差異">核心機制：兩種 mode 的工程差異</h2>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>Provisioned</th>
          <th>On-demand</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費方式</td>
          <td>預先買 RCU/WCU、按 hour 計</td>
          <td>按 request 計、無 capacity 預設</td>
      </tr>
      <tr>
          <td>Auto-scaling</td>
          <td>動態調整、target utilization 70%、min / max</td>
          <td>自動 scale、仍受單 partition 1000 WCU / 3000 RCU 上限</td>
      </tr>
      <tr>
          <td>Throttle 表現</td>
          <td><code>WriteThrottleEvents</code> 立即可見、exception 拋出</td>
          <td>不顯示 throttle、表現為 latency spike（hot partition 隱藏）</td>
      </tr>
      <tr>
          <td>Cost 模型</td>
          <td>可預測、低基礎 rate</td>
          <td>按用量、cost-per-request 約 provisioned base rate 的 6-7 倍</td>
      </tr>
      <tr>
          <td>Mode 切換限制</td>
          <td>24 小時內只能切一次</td>
          <td>同左</td>
      </tr>
  </tbody>
</table>
<p><strong>Auto-scaling 內部機制</strong>：</p>
<ul>
<li>CloudWatch alarm 觸發 → scaling activity → 1-5 分鐘調整 capacity</li>
<li>target utilization 70%（建議值、留 buffer 給 scale latency）</li>
<li>連續 spike 仍可能 throttle（auto-scaling 反應週期 &gt; spike 速度）</li>
</ul>
<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>、<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a>。</p>
<h2 id="6-軸決策框架">6 軸決策框架</h2>
<p>mode 選擇不是單軸 peak/avg ratio。下面 6 軸是 9 個 production case（Zomato / Zoom / Amazon Ads / Disney+ / Tixcraft / Capcom / Lemino / Genesys / PayPay）跨 case 揭露的真實決策維度。</p>
<h3 id="軸-1peak--average-流量-ratio">軸 1：peak / average 流量 ratio</h3>
<p>最直覺的軸、但是單軸誤判的根源。基本判讀：</p>
<ul>
<li>高 ratio（spiky / flash-sale）傾向 on-demand</li>
<li>穩定 ratio（sustained / 平緩）傾向 provisioned + auto-scaling</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「peak/avg &gt; 5x → on-demand」、「provisioned base rate × 6-7 = on-demand rate」這些具體閾值是經驗值 / 通用工程估算、<code>9.C5</code> / <code>9.C20</code> case 都沒給具體 ratio 數字。實際 crossover 點隨 region pricing + workload shape 變動、不要照搬本文數字。</p></blockquote>
<p>軸 1 單獨不夠用、要跟軸 2-6 合成判讀。</p>
<h3 id="軸-2讀寫比-trend-變化">軸 2：讀寫比 trend 變化</h3>
<p><code>9.C5 Amazon Ads</code> 揭露的觀測軸：「讀寫比 <em>變化</em> 比讀寫比本身更重要」。</p>
<ul>
<li>絕對讀寫比對容量規劃不是最重要（C5 是 18:1、C27 推估 5:1、絕對值各家不同）</li>
<li>業務邏輯改變（新增即時報表 / 新增推播 / 新增分析 query）會讓讀寫比跳一個量級</li>
<li>觀測上加 metric：read / write ratio 7-day rolling average、超過 ±30% 偏移觸發 review</li>
</ul>
<p>把 trend 變化當 capacity mode 重新評估的訊號 — 不是固定週期 review、是 <em>trend 偏移</em> 觸發 review。</p>
<h3 id="軸-3surge-是-暫時-還是-永久-baseline-上移">軸 3：surge 是 <em>暫時</em> 還是 <em>永久 baseline 上移</em></h3>
<p><code>9.C18 Zoom</code> COVID 30x DAU surge 揭露的軸：surge 後 baseline 永久上移、不會回去。</p>
<ul>
<li>暫時 surge（單日活動 / 季節高峰）：on-demand 划算、活動結束 mode 不用調</li>
<li>永久上移後（Zoom COVID、社會行為改變）：原 on-demand 設計會持續燒錢、要重新算 crossover、考慮切回 provisioned</li>
</ul>
<p><strong>Tripwire</strong>：surge 結束後 4-8 週仍維持 surge 期間 baseline 的 70%+、判定為「永久 baseline 上移」、重評 mode。</p>
<blockquote>
<p><strong>Scope warning</strong>：「4-8 週 / 70% 閾值」屬通用工程估算、9.C18 Zoom case 揭露「surge 後 baseline 不會回去」概念、未揭露具體閾值。</p></blockquote>
<h3 id="軸-4predictable-peak-vs-flash-sale">軸 4：predictable-peak vs flash-sale</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C15 Tixcraft</code> 對比揭露的軸：兩種 event-driven peak 不是同一類。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>predictable-peak（Disney+ 新片發布）</th>
          <th>flash-sale（拓元售票）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時間 lead</td>
          <td>已知日期、提前 1-2 天可預備</td>
          <td>已知時刻、提前 1-5 分鐘有效</td>
      </tr>
      <tr>
          <td>峰值倍數</td>
          <td>metadata 3-5x、持續數小時</td>
          <td>6750x in seconds、t=0 起跳 / t=300 結束</td>
      </tr>
      <tr>
          <td>Scale 方式</td>
          <td>scheduled scaling 預先升 baseline</td>
          <td>scheduled scaling 太慢、必須 pre-provision + composite PK</td>
      </tr>
      <tr>
          <td>Auto-scaling</td>
          <td>跟得上（事件持續時間長）</td>
          <td>完全跟不上（事件時間 &lt; scaling 反應週期）</td>
      </tr>
      <tr>
          <td>後續調回</td>
          <td>事件結束後 scheduled scaling 降回</td>
          <td>結束後立即降回、避免燒錢</td>
      </tr>
  </tbody>
</table>
<p><code>9.C27 Disney+</code>（Marvel / Star Wars 首日 metadata 流量 3-5 倍、持續時段較長）可以提前 1-2 天 pre-scale、scheduled scaling 合適。<code>9.C15 Tixcraft</code> 6750x in seconds，scheduled scaling 太慢、必須事前 pre-provision baseline 拉到極高、或用 on-demand + composite partition key 雙保險。</p>
<p>兩者都不是「peak/avg &gt; 5x → on-demand」單軸決策能解。</p>
<blockquote>
<p><strong>Scope warning</strong>：「scheduled scaling 30-60 分鐘前升 capacity」這個具體 lead time 是經驗值、case 未揭露具體時間。pre-scale 的 lead time 依事件性質決定、不是固定 30-60 分鐘。</p></blockquote>
<h3 id="軸-5dba--sre-工時釋放">軸 5：DBA / SRE 工時釋放</h3>
<p><code>9.C19 Capcom</code> 跟 <code>9.C29 Lemino</code> 揭露的成本軸：DynamoDB 真實成本不只看 monthly bill。</p>
<ul>
<li><code>9.C19 Capcom</code>：30% 成本下降的本質是「工程資源從 DB 運維轉到遊戲品質」、Capcom 是遊戲公司不是 IT 公司、把 DBA 時間從 Postgres patching / replication 設定 / backup 排程釋放到遊戲機制設計</li>
<li><code>9.C29 Lemino</code>：90% 工程工時下降（DBA + connection management + capacity planning 統包）</li>
</ul>
<p><strong>評估公式</strong>：</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">總成本 = direct cost (monthly bill)
</span></span><span class="line"><span class="ln">2</span><span class="cl">       + 工程工時機會成本 (DBA 從 patch/replication/backup 釋放出來做的事)</span></span></code></pre></div><p>on-demand 的 6-7x base rate 在 DBA 工時釋放下、實質 ROI 可能仍正向（特別在小團隊 / 非 IT 主業公司）。但要算總成本、不是只看 bill。</p>
<h3 id="軸-6dynamodb-vs-自管-cluster-cost-crossover">軸 6：DynamoDB vs 自管 cluster cost crossover</h3>
<p><code>9.C20 Zomato</code> 警惕段揭露的最上層決策軸：mode 選擇之上還有 vendor 選擇。</p>
<ul>
<li><code>9.C20 Zomato</code>：「成本降 50% 是 <em>當下流量</em> 的對照」、未來流量繼續成長、DynamoDB cost-per-request 成長率比 TiDB 自管 cluster 高、某流量規模後 crossover、自管 cluster 反而便宜</li>
<li>不是只在 on-demand vs provisioned 之間挑、是要算「未來 12-24 個月在預期流量下、DynamoDB（不論 mode）vs 自管 cluster 的成本曲線」</li>
</ul>
<p>判讀分層：</p>
<ul>
<li><strong>小 / 中流量 startup</strong>：DynamoDB on-demand 簡單划算、不用糾結</li>
<li><strong>大流量 + 流量可預測 + DBA 團隊已存在</strong>：自管 cluster crossover 點可能成立、值得算</li>
<li><strong>大流量 + 流量不可預測 + 小團隊</strong>：DynamoDB managed 仍划算（軸 5 加成）</li>
</ul>
<p>本軸是 mode 選擇之上的更上層決策、不是每次都展開、但寫進邊界判讀條件。</p>
<h2 id="操作流程">操作流程</h2>
<p>從 workload profiling 到 mode 切換的 8 步流程。</p>
<h4 id="step-1workload-profiling">Step 1：workload profiling</h4>
<p>用 CloudWatch 過去 30 天 RCU/WCU、算 p50 / p95 / p99 peak、求 peak/avg ratio（軸 1 輸入）+ read/write ratio rolling avg（軸 2 輸入）。</p>
<h4 id="step-2surge-性質判讀">Step 2：surge 性質判讀</h4>
<ul>
<li>是暫時 surge 還是永久 baseline 上移（軸 3）— 看 surge 結束後 4-8 週的 baseline trend</li>
<li>是 predictable-peak 還是 flash-sale（軸 4）— 看事件時間跟 auto-scaling 反應週期的比例</li>
</ul>
<h4 id="step-36-軸合成決策">Step 3：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">軸 1（peak/avg）+ 軸 2（讀寫比 trend）+ 軸 3（surge 性質）
</span></span><span class="line"><span class="ln">2</span><span class="cl">+ 軸 4（事件分級）+ 軸 5（工時機會成本）+ 軸 6（vendor crossover）
</span></span><span class="line"><span class="ln">3</span><span class="cl">→ provisioned + auto-scaling / on-demand / scheduled scaling 三選一</span></span></code></pre></div><p>不是任一軸獨自決定、是 6 軸合成；軸間衝突時優先序：軸 6（vendor）&gt; 軸 5（工時）&gt; 軸 3（surge 永久 vs 暫時）&gt; 軸 4（事件分級）&gt; 軸 1（peak/avg）&gt; 軸 2（讀寫比 trend）。</p>
<h4 id="step-4provisioned-配-auto-scaling">Step 4：provisioned 配 auto-scaling</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">BillingMode</span><span class="p">:</span><span class="w"> </span><span class="l">PROVISIONED</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">ProvisionedThroughput</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">ReadCapacityUnits</span><span class="p">:</span><span class="w"> </span><span class="m">100</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">WriteCapacityUnits</span><span class="p">:</span><span class="w"> </span><span class="m">50</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">AutoScalingSettings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">TargetTrackingScalingPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">TargetValue</span><span class="p">:</span><span class="w"> </span><span class="m">70.0</span><span class="w">  </span><span class="c"># target utilization</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">ScaleOutCooldown</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">ScaleInCooldown</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">MinCapacity</span><span class="p">:</span><span class="w"> </span><span class="m">50</span><span class="w">      </span><span class="c"># baseline</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">MaxCapacity</span><span class="p">:</span><span class="w"> </span><span class="m">1000</span><span class="w">    </span><span class="c"># baseline × 預期 surge multiplier</span></span></span></code></pre></div><p>target utilization 70% 留 buffer 給 scale latency；alarm 設 5 分鐘觀察窗。</p>
<h4 id="step-5scheduled-scaling">Step 5：scheduled scaling</h4>
<p>已知大事件（黑五、開票、新片發布）前預先提升 min capacity、事件後回原值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 黑五前 24 小時把 min capacity 拉到日常 10 倍</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">put_scheduled_action</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">ResourceId</span><span class="o">=</span><span class="s2">&#34;table/orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ScheduledActionName</span><span class="o">=</span><span class="s2">&#34;black-friday-pre-scale&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">Schedule</span><span class="o">=</span><span class="s2">&#34;cron(0 0 * * ? *)&#34;</span><span class="p">,</span>  <span class="c1"># 時間 lead 依事件性質決定、非固定 30-60 分鐘</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">ScalableTargetAction</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;MinCapacity&#34;</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span> <span class="s2">&#34;MaxCapacity&#34;</span><span class="p">:</span> <span class="mi">50000</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><h4 id="step-6mode-switch">Step 6：mode switch</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">aws dynamodb update-table <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table-name orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --billing-mode-summary <span class="nv">BillingMode</span><span class="o">=</span>PAY_PER_REQUEST</span></span></code></pre></div><p>每張 table 24 小時內只能切一次、要計畫 maintenance window。</p>
<h4 id="step-7驗證點">Step 7：驗證點</h4>
<p>切換後第一週對比 cost + throttle metric、確認方向正確：</p>
<ul>
<li>cost 變化方向跟預期一致（on-demand 應該變貴 / provisioned 應該變便宜）</li>
<li>throttle rate 沒上升</li>
<li>latency p99 沒退化</li>
</ul>
<h4 id="step-8總成本評估軸-5--軸-6">Step 8：總成本評估（軸 5 + 軸 6）</h4>
<p>直接 cost + 工時機會成本 + 對照自管 cluster 的 cost crossover 曲線。Quarterly review 用這個公式、不是只看 monthly bill。</p>
<p><strong>Rollback boundary</strong>：on-demand → provisioned 隨時可切、但 baseline 要先 sized 好；切錯方向第一個月可逆、長期累積 cost 不可逆。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 觀察到的 6 個典型 anti-pattern：</p>
<h4 id="case-1on-demand-後-cost-翻-3-倍">Case 1：on-demand 後 cost 翻 3 倍</h4>
<p>dev team 切 on-demand「不用管 capacity」、但 workload 是 sustained constant、on-demand 6-7x base rate 全付出來。<code>9.C5 Amazon Ads</code> 明示「sustained workload 用 provisioned + auto-scaling」。修法：穩定 workload 用 provisioned + auto-scaling（軸 1 + 軸 2）。</p>
<h4 id="case-2auto-scaling-跟不上-spike">Case 2：auto-scaling 跟不上 spike</h4>
<p>流量 1 分鐘內 10x、auto-scaling alarm 5 分鐘才觸發、前 4 分鐘全 throttle。修法：peak/avg 高且 spike 突然 → on-demand、或 scheduled scaling 預先升配（軸 1 + 軸 4）；flash-sale 場景 auto-scaling 不夠快、必須 pre-provision。</p>
<h4 id="case-3on-demand-hot-partition-隱藏">Case 3：on-demand hot partition 隱藏</h4>
<p>on-demand 不顯示 throttle、latency 從 5ms 變 50ms、application timeout retry 加劇問題。修法：on-demand 仍要看 partition-level metric（Contributor Insights）、不能假設 mode 解決設計問題（跟 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> cross-link）；mode × partition 交叉判讀。</p>
<h4 id="case-4provisioned-target-utilization-設太高">Case 4：provisioned target utilization 設太高</h4>
<p>target = 90% 看似省、實際每次 spike 都先 throttle 再 scale。修法：70% buffer 給 scale latency、不要為了省 cost 把 utilization 推到極限。</p>
<h4 id="case-5頻繁切-mode-撞-24h-限制">Case 5：頻繁切 mode 撞 24h 限制</h4>
<p>team 想「白天 provisioned 晚上 on-demand」省 cost、但 mode 切換 24h 一次、計畫破產。修法：白天 provisioned + 晚上把 capacity 設低、不切 mode；用 scheduled scaling 處理日週期、不用 mode switch。</p>
<h4 id="case-6surge-後沒重評-mode長期燒錢軸-3-對應">Case 6：surge 後沒重評 mode、長期燒錢（軸 3 對應）</h4>
<p>Zoom 式 30x permanent baseline 上移後、原 on-demand 設計成本爆炸。修法：surge 結束 4-8 週後重評、若 baseline 維持 70%+ 改 provisioned；把「surge 後 mode review」寫進 runbook、不是 ad-hoc 才想到。</p>
<p><strong>Anti-recommendation</strong>：流量 &lt; 100 RPS、cost &lt; $50/月的小 table 不用糾結 mode、on-demand 簡單；workload 穩定且 cost 高才值得做 provisioned + auto-scaling 的工程投入。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code>：基本用量</li>
<li><code>ProvisionedReadCapacityUnits</code> / <code>ProvisionedWriteCapacityUnits</code>：provisioned 預設值</li>
<li><code>ThrottledRequests</code>：provisioned mode 直接訊號、on-demand 為零不代表沒問題</li>
<li><code>SuccessfulRequestLatency</code> p99：on-demand mode 下 hot partition 訊號</li>
</ul>
<p><strong>新增的觀測軸</strong>（軸 2 / 軸 3 對應）：</p>
<ul>
<li>read/write ratio 7-day rolling avg、超過 ±30% 偏移觸發 review</li>
<li>surge baseline 4-week rolling avg、判斷 surge 是暫時還是永久</li>
<li>AWS Cost Explorer 按 table + mode 切 cost trend、月對比</li>
</ul>
<p>Auto-scaling activity log：CloudWatch alarm history + scaling activity，觀察 scaling 是否頻繁但 utilization 仍低（表示 alarm 設太敏感）。</p>
<p><strong>指標口徑紀律</strong>：引用 case 數字時明示口徑 — <code>9.C5</code> 90M reads/sec 是「年度峰值最高一秒、非平均」、<code>9.C20</code> 90% latency 降可能只 p50 不是 p99/p999、<code>9.C18</code> 30x DAU 是「permanent baseline 上移」非單日 peak。讀 vendor case 數字要分「最大瞬時 / 99 百分位 / 常態 / 滾動」四個口徑、不是混用。</p>
<p>Cost gate：每月 finance review 把 DynamoDB cost 對齊 access pattern volume、不只看絕對數字；軸 5 工時釋放跟軸 6 vendor crossover 也納入。</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>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-8-event-driven-scaling-5-種模式">Frame 8 event-driven scaling 5 種模式</h3>
<p><code>9.C5</code> / <code>9.C15</code> / <code>9.C18</code> / <code>9.C24</code> / <code>9.C27</code> 跨 case 揭露 event-driven scaling 至少 5 種形狀：</p>
<ul>
<li><strong>flash-sale spike</strong>：拓元 6750x in seconds（軸 4 走 pre-provision + composite PK）</li>
<li><strong>predictable peak</strong>：Disney+ 新片首發（軸 4 走 scheduled scaling）</li>
<li><strong>sustained growth</strong>：Amazon Ads / Capcom（軸 1 + 軸 5 → provisioned + auto-scaling）</li>
<li><strong>surge baseline permanent shift</strong>：Zoom 30x DAU 不會回去（軸 3 → 重評 mode）</li>
<li><strong>B2B sustained + 高可用</strong>：Genesys 99.999%（軸 5 + 軸 6 → managed 工時釋放比 cost 重要）</li>
</ul>
<p>不是用「peak/avg &gt; 5x」單一閾值決策、是事件型分類 × 軸合成。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — capacity mode 不解 hot partition、mode × partition 交叉判讀</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — access pattern 影響 peak/avg ratio 跟 read/write ratio</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — GSI 多時 cost 跟 mode 互動</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> — 多 region capacity 規劃放大、軸 5 工時釋放在 multi-region 更顯著</li>
<li>Migration playbook：跨 vendor cost optimization（如 <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 → DynamoDB</a>）對應 type C operational hybrid</li>
<li>替代路由：cost 極度敏感 + 流量穩定 + DBA 團隊已存在 → 自管 PostgreSQL / MySQL 可能更便宜（軸 6 crossover）、回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 9.C18</a> 互引：30x permanent surge 後的 mode 重評（軸 3 主案例）</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 9.C19</a> + <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 9.C29</a> 互引：DBA 工時釋放（軸 5 主案例）</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 / 合規）">Aurora read-replica-scaling</a> 共軸 cross-link：本篇從 KV 層 mode 選擇切入、5 模式分類在本篇主寫；Aurora 從 SQL 讀副本視角切入、事件分級表（FanDuel 平日 / playoff / championship / Super Bowl）跟雙 SLO 並行（DraftKings 讀寫雙峰錯位）+ fleet 治理在 Aurora 端主寫、本篇不重複展開</li>
</ul>
]]></content:encoded></item><item><title>Migration Playbook：Cloud SQL for PostgreSQL → Cloud Spanner</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/migrate-from-cloud-sql-pg/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/migrate-from-cloud-sql-pg/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook。走 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">vendor-article-spec&lt;/a> Migration Playbook 規格 + &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology&lt;/a> Type E（paradigm shift）。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關 — Evidence 段列的證據是 gate 通過條件、不是 nice-to-have。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="driver為什麼遷什麼條件不該遷">Driver：為什麼遷、什麼條件不該遷&lt;/h2>
&lt;h3 id="啟動壓力">啟動壓力&lt;/h3>
&lt;p>single-region Cloud SQL PostgreSQL primary 觸到容量上限（connection、write throughput、storage IOPS、region 故障風險）、產品要求跨 region active-active write、external consistency 是契約而非 nice-to-have。讀者要先確認自己面對的是「real 跨 region write residency」、不是「想用更強的技術」 — driver 段的核心責任是排除空泛動機。&lt;/p>
&lt;h3 id="主要-driver-候選">主要 driver 候選&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Global write residency&lt;/strong>：用戶分散全球、各地寫入本地 region、跨 region 一致性是產品要求&lt;/li>
&lt;li>&lt;strong>External consistency 對帳契約&lt;/strong>：跨 region 交易順序錯誤會導致對帳爆炸（金融、計費、ticketing）&lt;/li>
&lt;li>&lt;strong>單 primary 容量天花板&lt;/strong>：Cloud SQL 最大 instance 仍撐不住、應用層 sharding 是大工程&lt;/li>
&lt;li>&lt;strong>跨 region read latency&lt;/strong>：read 從各地直接打本地 replica、Cloud SQL read replica 受 single-primary 寫入 throughput 限制&lt;/li>
&lt;/ul>
&lt;h3 id="no-go-condition基礎">No-go condition（基礎）&lt;/h3>
&lt;p>流量集中單 region、跨 region 只是 DR 需求 → 維持 Cloud SQL + read replica + cross-region async DR 更便宜。這條 no-go 不複雜、但團隊常被 marketing 推著跳過 — 在自家 traffic dashboard 上 audit 一遍「write 來自哪些 region、各占比多少」、若 90%+ 來自單 region、Spanner 沒有 benefit。&lt;/p>
&lt;h3 id="no-go-conditionsizing-barrier">No-go condition（sizing barrier）&lt;/h3>
&lt;p>小 / 中型 PostgreSQL workload 的成本門檻 — Spanner 早期最小單位 100 processing units（≈ 1 node）對中小負載偏貴、過去是 sizing barrier；2021+ 推出 100 pu 起跳的 granular sizing 後雖然可從小開始、但 100 pu × per-pu monthly cost 加上跨 region replication 仍可能比 Cloud SQL HA 設定貴數倍。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook。走 <a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">vendor-article-spec</a> Migration Playbook 規格 + <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology</a> Type E（paradigm shift）。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關 — Evidence 段列的證據是 gate 通過條件、不是 nice-to-have。</p></blockquote>
<hr>
<h2 id="driver為什麼遷什麼條件不該遷">Driver：為什麼遷、什麼條件不該遷</h2>
<h3 id="啟動壓力">啟動壓力</h3>
<p>single-region Cloud SQL PostgreSQL primary 觸到容量上限（connection、write throughput、storage IOPS、region 故障風險）、產品要求跨 region active-active write、external consistency 是契約而非 nice-to-have。讀者要先確認自己面對的是「real 跨 region write residency」、不是「想用更強的技術」 — driver 段的核心責任是排除空泛動機。</p>
<h3 id="主要-driver-候選">主要 driver 候選</h3>
<ul>
<li><strong>Global write residency</strong>：用戶分散全球、各地寫入本地 region、跨 region 一致性是產品要求</li>
<li><strong>External consistency 對帳契約</strong>：跨 region 交易順序錯誤會導致對帳爆炸（金融、計費、ticketing）</li>
<li><strong>單 primary 容量天花板</strong>：Cloud SQL 最大 instance 仍撐不住、應用層 sharding 是大工程</li>
<li><strong>跨 region read latency</strong>：read 從各地直接打本地 replica、Cloud SQL read replica 受 single-primary 寫入 throughput 限制</li>
</ul>
<h3 id="no-go-condition基礎">No-go condition（基礎）</h3>
<p>流量集中單 region、跨 region 只是 DR 需求 → 維持 Cloud SQL + read replica + cross-region async DR 更便宜。這條 no-go 不複雜、但團隊常被 marketing 推著跳過 — 在自家 traffic dashboard 上 audit 一遍「write 來自哪些 region、各占比多少」、若 90%+ 來自單 region、Spanner 沒有 benefit。</p>
<h3 id="no-go-conditionsizing-barrier">No-go condition（sizing barrier）</h3>
<p>小 / 中型 PostgreSQL workload 的成本門檻 — Spanner 早期最小單位 100 processing units（≈ 1 node）對中小負載偏貴、過去是 sizing barrier；2021+ 推出 100 pu 起跳的 granular sizing 後雖然可從小開始、但 100 pu × per-pu monthly cost 加上跨 region replication 仍可能比 Cloud SQL HA 設定貴數倍。</p>
<p><strong>來源 9.C10「判讀」段第 3 點</strong>：Spanner 早期 100 pu 起跳是 sizing barrier、後來推出 granular sizing 才讓中小負載可從小開始。<strong>Dogfood 邊界明示</strong>：9.C10 case 揭露的 sizing 結構是 Google 內部 dogfood 的 capacity 規劃語言、不是 customer-facing pricing 承諾；客戶實際成本要看當期 Spanner pricing + region + replication config。</p>
<p>觸發 sizing no-go 的條件：</p>
<table>
  <thead>
      <tr>
          <th>信號</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>workload row count &lt; 數百萬</td>
          <td>100 pu 對這個資料量過 over-provision</td>
      </tr>
      <tr>
          <td>QPS &lt; 1000</td>
          <td>100 pu 容量遠超實際 traffic、cost / QPS ratio 高</td>
      </tr>
      <tr>
          <td>單 region 即可滿足合規</td>
          <td>跨 region replication cost 是純浪費</td>
      </tr>
      <tr>
          <td>Cloud SQL HA 設定已 cover SLA</td>
          <td>升 Spanner 沒 marginal benefit</td>
      </tr>
  </tbody>
</table>
<p>觸發任一條 → 強烈建議走 Cloud SQL HA、不升 Spanner。判讀時要把 Cloud SQL HA cost vs Spanner 100 pu cost 對比清楚、避免讀者「想用新技術」而升級。</p>
<h3 id="no-go-condition應用層延遲容忍">No-go condition（應用層延遲容忍）</h3>
<p>應用層延遲容忍 &lt; 50ms write 的 workload 不該升 Spanner — 跨 region Spanner write 在物理光速硬限下達 100-200ms（<a href="../consistency-models-comparison/">consistency-models-comparison</a> 的 cross-region quorum 段）。延遲敏感 workload 升級後會在 p99 直接撞牆、回退時資料已經寫進 Spanner、roll back 成本巨大。</p>
<p><strong>來源 9.C10「判讀」段第 2 點 + 「策略」段第 3 點</strong>：「external consistency 必須等多區 quorum、跨洲交易延遲可達 100-200ms」。<strong>Dogfood 邊界明示</strong>：9.C10 揭露的數量級是 Google internal observation、客戶實際 latency 隨 voting region 配置變化、引用時要附條件。</p>
<p>觸發 latency no-go 的場景：</p>
<ul>
<li>實時報價系統（毫秒級回應）</li>
<li>高頻交易（HFT）</li>
<li>遊戲 leaderboard 寫入</li>
<li>低延遲 OLTP（金融下單、支付路由）</li>
</ul>
<p>觸發任一條 → 強烈建議走 Cloud SQL 單 region、或考慮把 <em>跨 region 一致性需求</em> 重新審視（是否真的需要強一致、能不能改 event-driven async reconcile）。</p>
<h3 id="替代方案排除">替代方案排除</h3>
<ul>
<li><strong>Aurora DSQL</strong>：AWS 生態、若團隊在 GCP、跨雲不合</li>
<li><strong>CockroachDB</strong>：要自管或想 PostgreSQL wire 但不選 GCP 託管時可考慮、本 playbook 不對照</li>
<li><strong>Citus on Cloud SQL</strong>：multi-region write 不是強項、不解 cross-region external consistency 需求</li>
</ul>
<h3 id="case-anchor--dogfood-邊界">Case anchor + dogfood 邊界</h3>
<p><strong>無強 customer case</strong>。9.C10 是 Google 內部 dogfood、不是公開遷移 case；本 playbook 用 Spanner overview 的 PostgreSQL dialect 路徑 + 官方 migration guide + 通用 pattern。引用時必須明示「9.C10 揭露的線性 scaling / line-rate 設計目標是 Spanner 設計依據、不等於客戶遷移後可獲得的 capacity」。</p>
<p>對照 case：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered Aurora 受監管 banking</a> — 雖然是 Aurora、不是 Spanner、但揭露「受監管 OLTP 遷移要算合規 lead time」「資料駐留限制 = 容量規劃 per-市場」這兩條結論在 Spanner 遷移同樣適用。讀者若是受監管產業、跨 region instance config 還要疊上 voting region 是否落在合規市場的 audit。</p>
<h2 id="diff-audit6-規格面--sizing--cost-第-7-面">Diff Audit（6 規格面 + sizing / cost 第 7 面）</h2>
<h3 id="schema-diff">Schema diff</h3>
<p>PostgreSQL DDL → Spanner PostgreSQL dialect 對照：</p>
<table>
  <thead>
      <tr>
          <th>PostgreSQL 特性</th>
          <th>Spanner 對應</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SERIAL</code></td>
          <td>bit-reversed sequence</td>
          <td>改 primary key 策略、避免 hot split</td>
      </tr>
      <tr>
          <td><code>JSONB</code></td>
          <td><code>JSON</code> type</td>
          <td>大部分相容、複雜 path query 重寫</td>
      </tr>
      <tr>
          <td><code>ARRAY</code></td>
          <td><code>ARRAY</code></td>
          <td>OK</td>
      </tr>
      <tr>
          <td><code>PARTITION BY</code></td>
          <td>不直接支援</td>
          <td>改成 interleaved table 或單表</td>
      </tr>
      <tr>
          <td><code>FOREIGN KEY</code></td>
          <td>保留 FK constraint + 考慮 <a href="/blog/backend/knowledge-cards/interleaved-table/" data-link-title="Interleaved Table" data-link-desc="Spanner 把 parent / child table row 物理交錯儲存、parent &#43; child JOIN 不跨 split">Interleaved Table</a></td>
          <td>parent-child access pattern 改 interleaved</td>
      </tr>
      <tr>
          <td><code>B-tree INDEX</code></td>
          <td>OK</td>
          <td>直接遷</td>
      </tr>
      <tr>
          <td><code>GIN / GiST INDEX</code></td>
          <td>不支援</td>
          <td>用 <code>STORING</code> column 取代部分需求、其餘改應用層</td>
      </tr>
      <tr>
          <td><code>CHECK constraint</code></td>
          <td>部分支援（time-sensitive、查最新文件）</td>
          <td>audit 每條 constraint</td>
      </tr>
      <tr>
          <td><code>UDF / stored procedure</code></td>
          <td>少數支援</td>
          <td>改應用層或 client-side compute</td>
      </tr>
      <tr>
          <td><code>TRIGGER</code></td>
          <td>不支援</td>
          <td>改 application 層或 Spanner change streams</td>
      </tr>
  </tbody>
</table>
<p>interleaved table 設計參考 <a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>。讀者要在 schema audit 階段就決定哪些 parent-child 該 interleave、避免後悔成本。</p>
<h3 id="operational-diff">Operational diff</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Cloud SQL</th>
          <th>Spanner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>基礎架構</td>
          <td>VM-based</td>
          <td>API-based</td>
      </tr>
      <tr>
          <td>認證</td>
          <td>postgres user / role</td>
          <td>IAM role / service account</td>
      </tr>
      <tr>
          <td>備份</td>
          <td>pg_dump / pgBackRest</td>
          <td>point-in-time backup（PITR）</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>postgres-flavor（pg_stat_*）</td>
          <td>Cloud Monitoring <code>spanner.*</code></td>
      </tr>
      <tr>
          <td>Connection pool</td>
          <td>PgBouncer</td>
          <td>SDK 內 gRPC pool</td>
      </tr>
      <tr>
          <td>Vacuum</td>
          <td>必要</td>
          <td>不存在（MVCC 機制不同）</td>
      </tr>
      <tr>
          <td>Replication lag</td>
          <td>需監控</td>
          <td>不存在 single-primary 概念</td>
      </tr>
  </tbody>
</table>
<p>不再需要的 Cloud SQL 責任：vacuum、autovacuum tuning、connection pool（PgBouncer）、replication lag 監控、Patroni HA。</p>
<p>新增 Spanner 責任：processing unit capacity 預測、TrueTime ε 觀測（<a href="../truetime-api-depth/">truetime-api-depth</a>）、long-running schema operation 跟蹤、IAM 細粒度權限。</p>
<h3 id="paradigm-diff">Paradigm diff</h3>
<p>從 single-primary OLTP → 跨 region distributed SQL：</p>
<ul>
<li>transaction commit latency：&lt; 5ms → 50-200ms（跨洲、含 <a href="/blog/backend/knowledge-cards/commit-wait/" data-link-title="Commit Wait" data-link-desc="Spanner external consistency 的核心機制 — read-write transaction 拿 commit timestamp s 後等到 TT.after(s) 才 ACK、wait ≈ 2ε、付 latency tax 換 commit 順序 = real-time 順序">Commit Wait</a> + cross-region quorum）</li>
<li>external consistency 是 default（不再是 isolation level 選擇題）</li>
<li>transaction 上限：Cloud SQL 無硬限 → Spanner 10s timeout、要重構成短交易</li>
<li>read consistency：default eventual → default strong、需顯式選 bounded staleness</li>
</ul>
<p>詳細 consistency model 差異看 <a href="../consistency-models-comparison/">consistency-models-comparison</a>。</p>
<h3 id="component-diff">Component diff</h3>
<p>退役：</p>
<ul>
<li>PgBouncer / pgcat（connection pool）</li>
<li>Cloud SQL HA / Patroni cluster</li>
<li>pgBackRest（備份外掛）</li>
<li>Citus extension（若有用）</li>
<li>各種 postgres extension（時間敏感、逐個 audit 是否 Spanner 支援等效）</li>
</ul>
<p>新增：</p>
<ul>
<li>Spanner client library（Go / Java / Node / Python）</li>
<li>Dataflow（用於 bulk export-import）</li>
<li>Datastream / Database Migration Service（用於 CDC catch-up）</li>
<li>Spanner Studio（query UI）</li>
</ul>
<h3 id="application-diff">Application diff</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Cloud SQL（PostgreSQL client）</th>
          <th>Spanner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ORM</td>
          <td>全 PG ORM 相容</td>
          <td>PostgreSQL dialect 相容部分 ORM、查最新 dialect 支援列表</td>
      </tr>
      <tr>
          <td>Connection model</td>
          <td>process-per-connection（postgres）</td>
          <td>stateless gRPC client（SDK 內 pool）</td>
      </tr>
      <tr>
          <td>Transaction model</td>
          <td>可長交易</td>
          <td>10s timeout、需短交易</td>
      </tr>
      <tr>
          <td>Timestamp 使用</td>
          <td>app 內 <code>now()</code> / <code>CURRENT_TIMESTAMP</code></td>
          <td>改用 <code>PENDING_COMMIT_TIMESTAMP</code> sentinel</td>
      </tr>
      <tr>
          <td>Cursor / prepared statement</td>
          <td>全支援</td>
          <td>部分支援、查 SDK 文件</td>
      </tr>
      <tr>
          <td>Stored procedure</td>
          <td>全支援</td>
          <td>少數支援、業務邏輯改應用層</td>
      </tr>
  </tbody>
</table>
<p>ORM 兼容性是 time-sensitive claim — JPA / Hibernate / SQLAlchemy 在 Spanner PostgreSQL dialect 上的行為隨 dialect 版本演進、實作前查最新 vendor docs。讀者要把 ORM 兼容測試放 Phase 0、不能假設「PostgreSQL ORM 直接搬到 Spanner」。</p>
<h3 id="data-topology-diff">Data topology diff</h3>
<ul>
<li>Single primary（write）+ read replica → multi-region voting + read-only replica</li>
<li>Primary key 設計：避免單調遞增（SERIAL）造成 hot split、改 UUID 或 bit-reversed</li>
<li>Partition：PostgreSQL declarative partition → Spanner 不需要顯式 partition（自動 split）</li>
</ul>
<h3 id="sizing--cost-diff第-7-規格面">Sizing / cost diff（第 7 規格面）</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Cloud SQL</th>
          <th>Spanner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費單位</td>
          <td>instance class（vCPU / RAM）+ storage IOPS + HA add-on</td>
          <td>100 processing units 起跳 ≈ 1 node</td>
      </tr>
      <tr>
          <td>起跳成本</td>
          <td>小型 instance 月成本可控（小型 HA $50-200/月）</td>
          <td>100 pu × per-pu monthly rate、月成本是 Cloud SQL 小型 HA 的數倍</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>獨立計費（GB / month）</td>
          <td>含在 node count 內、無單獨 storage charge</td>
      </tr>
      <tr>
          <td>Throughput cap</td>
          <td>隨 instance class</td>
          <td>隨 pu 線性擴展</td>
      </tr>
      <tr>
          <td>跨 region replication</td>
          <td>額外 read replica cost</td>
          <td>含在 multi-region instance config 內</td>
      </tr>
      <tr>
          <td>Egress</td>
          <td>跨 region 額外</td>
          <td>跨 region 額外</td>
      </tr>
  </tbody>
</table>
<p>觸發 sizing audit 的時機：workload 行數、QPS、跨 region 需求都明確後、把「Cloud SQL HA monthly bill」對「Spanner 100 pu × monthly rate + egress」做 cost crossover 分析、無法 cost crossover 證明 → 不升。</p>
<p>Cost crossover 不是「Spanner 成本必須低於 Cloud SQL」、是「Spanner 多付的成本要對應到具體 benefit」：</p>
<ul>
<li>若 benefit 是 multi-region write residency、Spanner 多付的 cost 換得跨 region 一致性 — 對齊</li>
<li>若 benefit 只是「更新的技術」、Spanner 多付的 cost 沒對應產品價值 — 不升</li>
</ul>
<h3 id="type-判定">Type 判定</h3>
<p><strong>Type E（paradigm shift）</strong>、不是 drop-in。schema / app / operation / data topology / cost 五軸都動、不能用 Type B（drop-in）思路規劃 phase。詳細 type 判定方法看 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology</a>。</p>
<h2 id="phase-plan9-段每段有驗證門檻">Phase Plan：9 段、每段有驗證門檻</h2>
<h3 id="phase-0--compatibility-audit--sizing-audit">Phase 0 — Compatibility audit + sizing audit</h3>
<p>跑 schema-converter（pgloader / Spanner migration tool）、列出 incompatible feature、決定哪些改 schema、哪些改 app。hot key 風險評估（SERIAL primary key、單調遞增 timestamp）。</p>
<p>同時跑 sizing audit：</p>
<ul>
<li>估 target Spanner pu 數（基於 QPS、storage size、cross-region replication factor）</li>
<li>做 Cloud SQL HA cost vs Spanner cost crossover 分析</li>
<li>若 cost crossover 證明不出來 → halt migration、回到 driver 段重審</li>
</ul>
<p>Phase 0 是 migration 的決策閘門 — 不過閘門就停、不浪費 Phase 1+ 的 engineering effort。</p>
<h3 id="phase-1--target-schema-design">Phase 1 — Target schema design</h3>
<ul>
<li>interleaved table 設計（base on Phase 0 access pattern audit）</li>
<li>Index 重寫（GIN / GiST 用 STORING column 替代、其他用 B-tree）</li>
<li>Primary key 反序（避免 hot split）</li>
<li>Storing column 選擇（trade-off：query latency vs index size）</li>
</ul>
<p>Output 是 target DDL、跟原 PostgreSQL schema 並排 diff 文件、給 application 團隊審。</p>
<h3 id="phase-2--application-dual-target-preparation">Phase 2 — Application dual-target preparation</h3>
<ul>
<li>抽象 DB layer（repository pattern、避免直接呼 SQL）</li>
<li>SDK 並存（go-pg + Spanner client）</li>
<li>Feature flag 控制讀寫路徑（read-from-pg / read-from-spanner / dual-write）</li>
<li>Transaction 模式 audit（長交易拆短）</li>
</ul>
<h3 id="phase-3--bulk-initial-load">Phase 3 — Bulk initial load</h3>
<p>Cloud SQL → Cloud Storage（CSV / Avro）→ Dataflow → Spanner。Row count + checksum 驗證、column-level diff sample。</p>
<h3 id="phase-4--cdc-catch-up">Phase 4 — CDC catch-up</h3>
<p>Datastream from Cloud SQL → Dataflow → Spanner。Replication lag &lt; 1s 為前進門檻、sustained 24h。</p>
<h3 id="phase-5--shadow-read">Phase 5 — Shadow read</h3>
<p>Production read 同時打 Cloud SQL 跟 Spanner、diff log 異常。至少 7 天觀察、divergence rate &lt; 0.1%、p99 latency Spanner &lt; 1.5x Cloud SQL。</p>
<h3 id="phase-6--dual-write">Phase 6 — Dual write</h3>
<p>Cloud SQL 為 source-of-truth、Spanner 為 mirror。偵測 dual write divergence、評估是否提早升 source-of-truth。</p>
<h3 id="phase-7--cutover">Phase 7 — Cutover</h3>
<p>read-only window（&lt; 5 min）→ 最後 catch-up → switch source-of-truth → cutover application write。</p>
<h3 id="phase-8--cleanup">Phase 8 — Cleanup</h3>
<p>退役 Cloud SQL primary、保留 backup、清 PgBouncer / Patroni / 監控 dashboard。</p>
<h3 id="stage-0-variant-規劃">Stage 0 variant 規劃</h3>
<p>若 read-only window 不可接受（24/7 不能停機的金融 / 醫療系統）、Phase 6 dual write 期間做 conflict resolution（last-writer-wins + manual reconcile）、進入 fail-forward 模式、不走 read-only cutover。</p>
<h2 id="evidence每階段驗證材料">Evidence：每階段驗證材料</h2>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Phase 0</td>
          <td>incompatible feature 清單、預估改動 SP、hot key 風險 row count、<strong>sizing audit 報告</strong>（target pu 數估算 + Cloud SQL HA vs Spanner cost crossover 月 / 年成本對比）</td>
      </tr>
      <tr>
          <td>Phase 1</td>
          <td>DDL diff report、預估 backfill 時間（基於 row count + Spanner 文件）</td>
      </tr>
      <tr>
          <td>Phase 3</td>
          <td>row count 對齊、column-level checksum、payload sample diff</td>
      </tr>
      <tr>
          <td>Phase 4</td>
          <td>CDC lag &lt; 1s sustained 24h、error rate &lt; 0.01%</td>
      </tr>
      <tr>
          <td>Phase 5</td>
          <td>shadow read divergence rate &lt; 0.1%、p99 latency Spanner &lt; 1.5x Cloud SQL</td>
      </tr>
      <tr>
          <td>Phase 6</td>
          <td>dual write divergence &lt; 0.01%、reconcile queue 不積壓</td>
      </tr>
      <tr>
          <td>Phase 7</td>
          <td>cutover window 內 write 一致性、回到 Phase 6 的條件（rollback path）</td>
      </tr>
  </tbody>
</table>
<p><strong>Cost crossover 報告</strong>（Phase 0 必交付）：</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">Item                          | Cloud SQL HA | Spanner 100 pu | Delta
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">------------------------------|--------------|----------------|------
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Compute monthly               | $X           | $Y             | $Y-X
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Storage monthly               | $A           | (included)     | -$A
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Cross-region replication      | $B           | (included)     | -$B
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Egress (est)                  | $C           | $C             | $0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Total monthly                 | $X+A+B+C     | $Y+C           | $Y-X-A-B
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Annual                        | 12*above     | 12*above       | -
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Benefit (qualitative)         | -            | multi-region write residency / external consistency | -
</span></span><span class="line"><span class="ln">10</span><span class="cl">Crossover verdict             | -            | proceed / halt | -</span></span></code></pre></div><p>Verdict = <code>proceed</code> 才進 Phase 1；<code>halt</code> → 回到 Driver 段重審 driver 是否成立。</p>
<p>所有 evidence 進 incident decision log、回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="cutover決策與-rollback">Cutover：決策與 rollback</h2>
<h3 id="cutover-window">Cutover window</h3>
<p>選用戶最低流量時段、&lt; 5 min read-only freeze、預先通知。受監管產業（對照 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>）要算合規 lead time、每市場各自審。</p>
<h3 id="decision-owner">Decision owner</h3>
<p>DB lead + product lead + on-call SRE 共同 sign-off。受監管產業多加合規 owner。</p>
<h3 id="rollback-condition">Rollback condition</h3>
<ul>
<li>cutover 後 30 min 內 p99 write latency 持續 &gt; SLA 2x → rollback</li>
<li>error rate &gt; 1% sustained 5 min → rollback</li>
<li>對帳系統發現 divergence &gt; 0.1% → rollback</li>
</ul>
<h3 id="rollback-機制">Rollback 機制</h3>
<p>保留 Cloud SQL 為 read-only mirror 14 天、Spanner 改 read-only、reverse CDC（Spanner → Cloud SQL）需事先準備。Reverse CDC 在 Phase 4-6 期間就要 dry-run 過、不能 cutover 才第一次試。</p>
<p>連結 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window</a>、<a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback-condition</a>。</p>
<h2 id="cleanup退役清單跟保留責任">Cleanup：退役清單跟保留責任</h2>
<h3 id="退役清單">退役清單</h3>
<ul>
<li>Cloud SQL primary instance</li>
<li>PgBouncer 配置</li>
<li>Patroni cluster</li>
<li>pgBackRest backup job（保留歸檔 90 天、依產業合規）</li>
<li>Datastream pipeline</li>
<li>Dataflow job</li>
</ul>
<h3 id="監控清理">監控清理</h3>
<p>postgres-specific dashboard（exporter / wal lag / autovacuum）改成 Spanner dashboard（commit_latencies / clock_skew_ms / cpu_utilization_by_priority）。</p>
<h3 id="文件--runbook-更新">文件 / runbook 更新</h3>
<p>postgres operation runbook 標記 deprecated、Spanner runbook 上線。新 runbook 含：</p>
<ul>
<li>DDL long-running operation 監控</li>
<li>TrueTime ε 異常處理</li>
<li>Cross-region instance failover drill</li>
<li>Cost monitoring alert</li>
</ul>
<h3 id="稽核--合規">稽核 / 合規</h3>
<p>保留 final pg_dump 7 年（依產業）、incident write-back 完成、合規市場各自留檔（對照 Standard Chartered case 的 per-市場合規 lead time）。</p>
<h2 id="邊界與整合sibling對照anti-recommendation">邊界與整合：sibling、對照、anti-recommendation</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../truetime-api-depth/">truetime-api-depth</a>：app 對 timestamp 假設審計（Phase 2 必讀）</li>
<li><a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>：Phase 1 target schema 設計</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：Phase 0 應用層一致性要求釐清、Driver 段 latency no-go 的物理硬限</li>
</ul>
<h3 id="跟其他-migration-對照">跟其他 migration 對照</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PostgreSQL → Aurora DSQL Migration</a>：兩者都是 PostgreSQL → distributed SQL paradigm shift、選 GCP / AWS 看生態</li>
<li><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>：通用大規模遷移方法論</li>
</ul>
<h3 id="跟-case-對照">跟 case 對照</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale</a>：dogfood case、揭露 Spanner 設計目標、不是 customer-facing capacity reference</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered Aurora banking</a>：受監管產業遷移要算合規 lead time、per-市場容量規劃</li>
</ul>
<h3 id="anti-recommendation">Anti-recommendation</h3>
<p>讀者讀完本文應該能判斷：</p>
<ul>
<li>若 driver 只是「想用新技術」→ 回 Cloud SQL</li>
<li>若 workload 小（QPS &lt; 1000、行數 &lt; 數百萬）→ Cloud SQL HA 更划算</li>
<li>若應用層延遲容忍 &lt; 50ms write → Cloud SQL 單 region</li>
<li>若 cost crossover 證明不出來 → halt migration、不升</li>
</ul>
<p>Driver 是真正跨 region write residency / external consistency 對帳契約 / 單 primary 容量天花板 → 才升。Migration playbook 的目標不是把所有 Cloud SQL workload 升到 Spanner、是把「適合升」的部分用低風險路徑遷過去。</p>
]]></content:encoded></item><item><title>MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</guid><description>&lt;p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。&lt;/p>
&lt;h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆&lt;/h2>
&lt;p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：&lt;/p>
&lt;p>&lt;strong>Connection ceiling&lt;/strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。&lt;/p>
&lt;p>&lt;strong>Read scaling ceiling&lt;/strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&amp;gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。&lt;/p>
&lt;p>&lt;strong>Scaling reaction lag&lt;/strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。&lt;/p>
&lt;p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限&lt;/li>
&lt;li>p99 read latency 在事件時段集體爬&lt;/li>
&lt;li>Atlas auto-scaling event log 顯示 &lt;em>triggered too late&lt;/em>&lt;/li>
&lt;li>Cache hit rate 跟 read latency 反向相關&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a> 是 rich case，含具體數字（deploy 尖峰 &lt;em>connection event rate&lt;/em> ~60K connections / 分鐘 / mongobetween 後 &lt;em>steady-state concurrent connections&lt;/em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 雙模式負載敘事（持續 sensor + 緊急事件）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a> 媒體爆量形狀。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。</p>
<h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆</h2>
<p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：</p>
<p><strong>Connection ceiling</strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。</p>
<p><strong>Read scaling ceiling</strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。</p>
<p><strong>Scaling reaction lag</strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。</p>
<p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。</p>
<p>讀者徵兆：</p>
<ul>
<li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限</li>
<li>p99 read latency 在事件時段集體爬</li>
<li>Atlas auto-scaling event log 顯示 <em>triggered too late</em></li>
<li>Cache hit rate 跟 read latency 反向相關</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> 是 rich case，含具體數字（deploy 尖峰 <em>connection event rate</em> ~60K connections / 分鐘 / mongobetween 後 <em>steady-state concurrent connections</em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 雙模式負載敘事（持續 sensor + 緊急事件）、<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> 媒體爆量形狀。</p>
<h2 id="核心機制三層合成-frame">核心機制：三層合成 frame</h2>
<p>跨案合成 frame（本章合成、case 原文沒這個 frame）：應用層連 MongoDB cluster 在大規模 production 是 <em>三層協作</em>、不是 driver 一個元件：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>角色</th>
          <th>9.C36 Coinbase 對應元件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Driver / Proxy</td>
          <td>連線多工、應用 process 跟 cluster 的橋接</td>
          <td>MongoDB driver + mongobetween proxy</td>
      </tr>
      <tr>
          <td>Cache + freshness token</td>
          <td>read scaling 主路、跨層一致性協議</td>
          <td>Memcached + freshness token + OCC version</td>
      </tr>
      <tr>
          <td>Scaling trigger</td>
          <td>cluster 擴容啟動時機</td>
          <td>ML predictive scaling + reactive fallback</td>
      </tr>
  </tbody>
</table>
<p>三層缺一都會在大規模時撞牆。本文聚焦這三層如何協作、單一層的深度議題（read preference 機制、schema 治理、aggregation pipeline）推到 sibling。</p>
<h3 id="driver--proxy-層">Driver / Proxy 層</h3>
<p>MongoDB driver 原生 connection 模式：driver 在 application process 內維護 connection pool、每個 process 跟 MongoDB cluster 開固定數量 socket。但 driver <strong>沒跨 process pool</strong> — 多個 process 共用同一台機器、每個 process 自己一份 pool、cluster 看到的是 N 倍 connection。跟 PostgreSQL 走 pgbouncer 是同樣需求。</p>
<p>Connection storm 的具體 trigger：</p>
<ul>
<li><strong>部署模型放大 process 數</strong>：CRuby + GVL 強制每 CPU core 一 process、blue-green 部署 instance 數 ×2、連線數隨之 ×2（9.C36 Coinbase 揭露：單 cluster 看到 60K connections/min）</li>
<li><strong>微服務數量多</strong>：50+ microservice 各自連 cluster、每服務 connection 加總後撞上限（9.C37 Forbes 50+ 微服務情境對照）</li>
</ul>
<p>mongobetween proxy（Coinbase 自建）：把多 application process 的連線合成少量到 MongoDB cluster 的連線。9.C36 揭露兩個獨立口徑、不是同一數字的連續變化：deploy 尖峰時 <em>connection event rate</em> 是 ~60K connections / 分鐘（unique connection 事件量、rate）；mongobetween 介入後 <em>steady-state concurrent connection 數</em> 由 ~30K 降到 ~2K（瞬時量、前後對比、一個量級）。引用時把 rate 跟瞬時 concurrent count 分開、不要壓成「60K 收斂到 2K」。</p>
<p><strong>Scope warning（必明示）</strong>：mongobetween 是 Coinbase 為 Ruby + GVL 需求自建、case 自承「Go / Java / Node.js 應用因原生支援連線多工、通常不需要這層 proxy」。寫進設計文件時不可寫成「MongoDB 在大規模都需要 mongobetween」、要寫成「特定部署模型才需要」。</p>
<h3 id="cache--freshness-token-層">Cache + freshness token 層</h3>
<p>直接打 MongoDB 不可能撐 1.5M reads/sec（口徑：users 服務應用層觀察、含 cache、非 MongoDB cluster 純讀取）。Coinbase 在 users 服務前面加 Memcached query cache、單 document query 先查 cache。</p>
<p>跨層一致性問題：write 進 MongoDB primary、cache 還是舊版、user 下次 read 拿到舊資料。</p>
<p><a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 機制：</p>
<ol>
<li>Write 成功後給 client token（含 OCC version / clusterTime）</li>
<li>Client read 帶 token</li>
<li>Server 保證返回的資料版本 ≥ token</li>
<li>必要時 bypass cache 直接打 DB</li>
</ol>
<p>跟 DB 層 causal consistency session 對照：causal session 解 MongoDB 內 read-your-own-write、freshness token 解 <em>DB + cache 跨層</em> read-your-own-write。機制細節見 <a href="../replica-set-read-preference/">replica set read preference</a>、本文不重複展開。</p>
<p><strong>Scope warning（必明示）</strong>：1.5M reads/sec 是 <em>users 服務 + cache</em> 合成數字、不是 MongoDB cluster 純讀取 benchmark。寫進設計文件必須明示口徑、避免讀者把 1.5M reads/sec 當成「MongoDB 單獨能撐」。</p>
<h3 id="scaling-trigger-層">Scaling trigger 層</h3>
<p>MongoDB cluster 擴容時間：傳統 reactive scaling 起點到完成 ~70 分鐘（9.C36 Coinbase 揭露口徑：含 instance provisioning + 資料 sync + balancer rebalance、特定 Atlas tier / 資料量條件）。</p>
<p>Reactive 為主撐不住快變流量：CPU / queue 觸發 reactive scaling 在 surge 開始時才動、來不及；surge 已經結束擴容才到位。</p>
<p>Predictive scaling 機制（Coinbase 揭露）：</p>
<ul>
<li>用外部訊號（加密貨幣價格、賽事行程、票務開賣時間）訓練 ML 模型</li>
<li>提前 60 分鐘預測流量</li>
<li>預先擴容</li>
<li>把擴容啟動時間從 70 分鐘壓到 25 分鐘（口徑：trigger 提前、不是擴容本身變快）</li>
</ul>
<p><strong>Scope warning（必明示）</strong>：case 警示「ML 預測有 false positive / false negative、Coinbase 沒揭露準確率、所以仍保留 reactive scaling 作為 safety net」。寫進設計文件要明示兩段式設計、不可寫成「Predictive scaling 取代 reactive scaling」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection-pool</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">session-consistency</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>（cache 失效時打穿 DB 的 hot key）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：connection ceiling audit</strong>。量測現有 deploy 在 peak 的 connection count、推算 deploy ×2 / 微服務新增時 connection 走勢；對照 MongoDB cluster 的 hard limit（Atlas tier 決定、典型 1500-32000）。</p>
<p><strong>Step 2：部署模型判讀</strong>。</p>
<table>
  <thead>
      <tr>
          <th>部署模型</th>
          <th>是否需 proxy 層</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CRuby + GVL（process-per-core）</td>
          <td>需要</td>
          <td>每 core 一 process、連線隨 process 線性升</td>
      </tr>
      <tr>
          <td>大量微服務（50+）+ 各自 deploy</td>
          <td>需要</td>
          <td>微服務 connection 加總撞 cluster limit</td>
      </tr>
      <tr>
          <td>Blue-green 部署（雙環境並存）</td>
          <td>需要</td>
          <td>部署期間連線 ×2、容易撞 cluster ceiling</td>
      </tr>
      <tr>
          <td>Go / Java / Node.js 單一 binary + 多 thread</td>
          <td>通常不需要</td>
          <td>原生 driver pool 跨 thread 共用、收斂效率高</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：proxy 選型</strong>。Coinbase mongobetween 是參考實作、社群還有 mongoproxy / DocumentDB 內建 connection multiplexer。自建 proxy 是 Coinbase 規模才合理、中型團隊先評估 Atlas tier 升級。</p>
<p><strong>Step 4：cache layer 設計</strong>（read scaling 主路）：</p>
<ul>
<li>前置 Memcached / Redis、cache key = collection + document id + version</li>
<li>Write API 返回 <code>{result, version_token}</code> — token 含 OCC version 或 MongoDB clusterTime</li>
<li>Read API 接受 optional version token、cache lookup 比對 entry version 跟 token、低於就 invalidate + bypass</li>
<li>DB 層 fallback <code>readConcern: &quot;majority&quot;</code> 保證返回 version ≥ token</li>
</ul>
<p><strong>Step 5：predictive scaling 設計</strong>（適用「外部訊號可預測流量」）：</p>
<ul>
<li><strong>識別 driver 訊號</strong>：加密貨幣價格 / 賽事行程 / 票務開賣 / 促銷活動 / IoT 緊急事件預警</li>
<li><strong>訓練 ML</strong>：用歷史流量 vs 訊號 correlation 訓練、輸出未來 30-60 分鐘流量預測</li>
<li><strong>觸發擴容</strong>：預測超 threshold 時主動 trigger Atlas scaling API、不等 reactive metric</li>
<li><strong>保留 reactive safety net</strong>：ML failure 時 reactive scaling 仍會接、不可拿掉</li>
</ul>
<p><strong>Step 6：全鏈路驗證</strong>。Staging 灌入 deploy ×2 模擬 connection storm、灌入 stale cache 驗證 freshness token bypass、放假流量驗證 predictive scaling trigger。</p>
<p>驗證點：</p>
<ul>
<li>Connection count 在 deploy 後不爆 cluster limit</li>
<li>Cache hit rate vs freshness bypass rate 比例正常（cache hit &gt; 90% + bypass &lt; 5% 屬通用工程估算、case 未揭露具體數字）</li>
<li>Predictive scaling 領先窗 ≥ 30 分鐘</li>
<li>Reactive scaling 仍保留作 safety</li>
</ul>
<p>Rollback boundary：</p>
<ul>
<li>Proxy 層可下線（流量改直連 cluster、但短時 connection storm 風險回來）</li>
<li>Cache 層可下線（read 全部打 DB、需 cluster 容量能撐）</li>
<li>Predictive scaling 可下線（退回純 reactive、但快變 surge 接不住）</li>
<li>三層都要設計 graceful degradation、不是全有全無</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Connection storm during deploy</strong>：blue-green 部署 instance 數 ×2、connection 隨之爆、新 deploy 連不上 cluster、cascade 失敗。修法是 proxy 層 + cluster connection limit 預留 headroom（典型留 30% buffer、屬通用工程估算）。</p>
<p><strong>Proxy 變成單點瓶頸</strong>：mongobetween / pgbouncer 風格 proxy 自己變熱點、proxy 故障時下游全死。修法是 proxy 叢集 + health check + 客戶端 retry、跟 application 同 region 共部署降低 proxy ↔ application 的網路 RTT。</p>
<p><strong>Cache hit rate 崩塌</strong>：cache 失效 + 大量 read bypass、DB 突然吃 100% 流量、cluster 飽和。修法是 freshness token 設計時要監控 bypass rate、過高表示 cache invalidation 邏輯有問題、cache 沒在 write 後 update / invalidate。</p>
<p><strong>Freshness token 漏寫</strong>：write 沒帶 token / client 沒帶 token、token silently 失效、user 拿到舊資料。修法是 protocol 強制（middleware 攔截 write / read、自動帶 token）、不能靠 application 自覺。</p>
<p><strong>Predictive scaling false positive 浪費容量</strong>：ML 預測 surge 但實際沒來、cluster 預先擴容後閒置。接受成本、保留 ML model retraining、定期評估 precision / recall。</p>
<p><strong>Predictive scaling false negative 漏接 surge</strong>：ML 沒預測到、cluster 沒提前擴、surge 來時 reactive scaling 開始動但 70 分鐘來不及。修法是 reactive safety net + 服務降級（限流 / 部分 read 降級拿舊資料 + freshness token 告警）。</p>
<p><strong>三層協作脫節</strong>：proxy 擋住 connection storm 但 cluster 內部 read scaling 沒設計、application 仍打爆。三層必須一起設計、不是各自獨立。</p>
<p>Anti-recommendation：</p>
<ul>
<li>中小流量（&lt; 100K reads/sec、單 deploy &lt; 50 instance）不需要這三層；Atlas tier 升級 + cluster 內 replica + 簡單 cache 就夠</li>
<li>mongobetween 風格 proxy 只在 Ruby + GVL / 類似部署模型才必要、Go / Java / Node.js 通常不需要（case 自承）</li>
<li>Predictive scaling 只在外部訊號可預測時有效；無預測訊號的純隨機 surge 還是回 reactive + headroom</li>
<li>大規模 OLTP 不該為了省成本拿掉 cache 層；read scaling 主路就是 cache、單靠 MongoDB cluster 拿不到 1.5M reads/sec 量級</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Connection 層</strong>：cluster connection count / Atlas tier limit / proxy 到 cluster 的 connection multiplex 比、deploy 前後 connection 走勢</li>
<li><strong>Cache 層</strong>：cache hit rate、freshness token bypass rate、cache key collision rate</li>
<li><strong>Scaling 層</strong>：predictive scaling trigger event count / 領先窗、reactive scaling fallback 觸發頻率、實際擴容啟動到完成時間、ML 預測準確率（precision / recall）</li>
</ul>
<p>Mongo / Atlas command：</p>
<ul>
<li><code>db.serverStatus().connections</code>：cluster 當前 connection 統計</li>
<li><code>db.currentOp({})</code>：看 connection 使用</li>
<li>Atlas API：cluster scaling event log</li>
<li>Proxy admin metric：connection multiplex 比、上下游 latency</li>
</ul>
<p>Application observability：APM 看 connection acquire latency、cache hit rate time series、freshness token 流動完整性（write 是否發 token、read 是否帶 token、cache 是否驗 token）。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：把 connection storm event、cache hit rate / bypass rate、scaling trigger leadtime 列為跨層 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：大規模 OLTP 撞牆時要區分 (a) connection ceiling (b) cache hit rate 下降 (c) cluster 內 replica 飽和 (d) scaling 跟不上。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — DB 層 causal session 機制、freshness token 跨層協議；本文聚焦三層協作、那篇聚焦 DB 層機制</li>
<li><a href="../shard-key-selection/">shard key selection</a> — cluster 擴容是天級議題、是 scaling layer 的 trigger；單 cluster vs 多 cluster 切分</li>
<li><a href="../schema-design-pattern/">schema design pattern</a> — app-layer abstraction 跟本文 cache + freshness token 同層協作、contract layer 三選一</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — report dashboard 跑爆 primary 的補位路徑是本文的 cache + read scaling、不是讓 aggregation 自己優化</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li><strong>Federated DB 模式</strong>（9.C36 Coinbase 揭露：MongoDB + DynamoDB）— 不是「全用 MongoDB」、document-shaped 用 MongoDB、access pattern 固定的 KV 用 DynamoDB；對應 <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 vendor page</a> 跨 vendor 對照</li>
<li><strong>跨雲 hedging</strong>（9.C37 Forbes 跨雲彈性）— Atlas 跨 AWS / GCP / Azure 是規避未來雲商鎖定的 selection 訊號</li>
</ul>
<p>跟 1.x 互引：</p>
<ul>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> — connection storm 通用模式（pgbouncer / mongobetween 對應）</li>
<li><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> — 三層架構列為大規模 OLTP 容量規劃必看點</li>
<li><a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> — predictive scaling 的 ML 訓練紀律</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「connection management + Atlas scaling」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 三層合成 rich case</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 媒體爆量形狀</li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — IoT 雙模式負載</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/reference/connection-string-options/">MongoDB Connection Pool Options</a>、<a href="https://www.mongodb.com/docs/atlas/cluster-autoscaling/">Atlas Auto-Scaling</a>、<a href="https://github.com/coinbase/mongobetween">mongobetween GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>3.C33 Wargaming：World of Tanks 戰後 dossier 解耦</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wargaming-game-portal-decoupling/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wargaming-game-portal-decoupling/</guid><description>&lt;p>這個案例的核心責任是說明 game server / web portal 異步解耦、queue 吸收戰後事件 burst。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>World of Tanks server 全 Linux、用 RabbitMQ 作為 web service stack 核心。每場戰鬥結束後玩家 tank dossier 寫入 message queue、讓 game portal 顯示最新統計而不增加 game server load。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Queue 是 game server 與 portal 的解耦邊界、subscription 也走 RabbitMQ。揭露遊戲場景的「戰後事件 burst」適合用 queue 吸收、不該打到 game server 內部狀態。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>RabbitMQ 進階主題：Federation + Shovel（多 region game server 同步）/ 多 vhost + 多租戶（多遊戲共用 broker）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.linuxfoundation.org/blog/blog/wargaming-mobilizes-with-linux-and-open-source">Wargaming Mobilizes with Linux and Open Source (Linux Foundation)&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://ftr.wot-news.com/2014/07/17/wargaming-public-api-part-2/">Wargaming Public API&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 game server / web portal 異步解耦、queue 吸收戰後事件 burst。</p>
<h2 id="觀察">觀察</h2>
<p>World of Tanks server 全 Linux、用 RabbitMQ 作為 web service stack 核心。每場戰鬥結束後玩家 tank dossier 寫入 message queue、讓 game portal 顯示最新統計而不增加 game server load。</p>
<h2 id="判讀">判讀</h2>
<p>Queue 是 game server 與 portal 的解耦邊界、subscription 也走 RabbitMQ。揭露遊戲場景的「戰後事件 burst」適合用 queue 吸收、不該打到 game server 內部狀態。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>RabbitMQ 進階主題：Federation + Shovel（多 region game server 同步）/ 多 vhost + 多租戶（多遊戲共用 broker）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.linuxfoundation.org/blog/blog/wargaming-mobilizes-with-linux-and-open-source">Wargaming Mobilizes with Linux and Open Source (Linux Foundation)</a></li>
<li><a href="http://ftr.wot-news.com/2014/07/17/wargaming-public-api-part-2/">Wargaming Public API</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/declarative-partitioning/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/declarative-partitioning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明大表（&amp;gt; 1TB）需要 partitioning、本文聚焦 &lt;em>partition 真實價值在哪、為什麼多數人第一次 partition 都做錯&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="partition-不是把大表切小是讓-planner-pruning--縮小-maintenance-scope">Partition 不是「把大表切小」、是「讓 planner pruning + 縮小 maintenance scope」&lt;/h2>
&lt;p>剛開始學 partitioning 的人多半從「表太大、切小一點」直覺出發；切了之後發現 — &lt;em>query 變慢&lt;/em>（planner 還在看所有 partition）、&lt;em>INSERT 變慢&lt;/em>（trigger / partition routing overhead）、&lt;em>backup 沒變短&lt;/em>（總資料量沒變）。直覺錯了：partition 的工程價值來自兩個機制、跟「切小」沒直接關係：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Query planner pruning&lt;/strong>：planner 在 planning 階段 &lt;em>跳過&lt;/em> 不可能命中 partition key 的 partition、查詢只 scan 相關 partition；前提是 &lt;em>WHERE 條件含 partition key&lt;/em>、否則 planner 看完所有 partition、效能反而比單表差&lt;/li>
&lt;li>&lt;strong>Maintenance scope 縮小&lt;/strong>：vacuum / index rebuild / DROP / archive 只動單一 partition、不掃整表；vacuum 12 小時變 30 分鐘 / DROP 老資料 0.01 秒、是 partition 真正回本的地方&lt;/li>
&lt;/ol>
&lt;p>partition 是 &lt;em>為了 maintenance 跟 planner pruning&lt;/em> 設計、不是「表變小」設計。漏掉這個 framing、partition 配置會錯。&lt;/p>
&lt;h2 id="range--list--hashpartition-策略對應業務形狀">RANGE / LIST / HASH：partition 策略對應業務形狀&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- RANGE: 時間序列、log、event（最常見）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">bigint&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">event_time&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">timestamptz&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RANGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">event_time&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events_2026_05&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-05-01&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-06-01&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- LIST: tenant ID / region / status enum
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">bigint&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LIST&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders_tenant_premium&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1001&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1002&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1003&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- HASH: 均勻散落（無自然 partition key）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">bigint&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HASH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users_0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">users&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">MODULUS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REMAINDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>策略選擇關鍵：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明大表（&gt; 1TB）需要 partitioning、本文聚焦 <em>partition 真實價值在哪、為什麼多數人第一次 partition 都做錯</em>。</p></blockquote>
<h2 id="partition-不是把大表切小是讓-planner-pruning--縮小-maintenance-scope">Partition 不是「把大表切小」、是「讓 planner pruning + 縮小 maintenance scope」</h2>
<p>剛開始學 partitioning 的人多半從「表太大、切小一點」直覺出發；切了之後發現 — <em>query 變慢</em>（planner 還在看所有 partition）、<em>INSERT 變慢</em>（trigger / partition routing overhead）、<em>backup 沒變短</em>（總資料量沒變）。直覺錯了：partition 的工程價值來自兩個機制、跟「切小」沒直接關係：</p>
<ol>
<li><strong>Query planner pruning</strong>：planner 在 planning 階段 <em>跳過</em> 不可能命中 partition key 的 partition、查詢只 scan 相關 partition；前提是 <em>WHERE 條件含 partition key</em>、否則 planner 看完所有 partition、效能反而比單表差</li>
<li><strong>Maintenance scope 縮小</strong>：vacuum / index rebuild / DROP / archive 只動單一 partition、不掃整表；vacuum 12 小時變 30 分鐘 / DROP 老資料 0.01 秒、是 partition 真正回本的地方</li>
</ol>
<p>partition 是 <em>為了 maintenance 跟 planner pruning</em> 設計、不是「表變小」設計。漏掉這個 framing、partition 配置會錯。</p>
<h2 id="range--list--hashpartition-策略對應業務形狀">RANGE / LIST / HASH：partition 策略對應業務形狀</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- RANGE: 時間序列、log、event（最常見）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">bigint</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">event_time</span><span class="w"> </span><span class="n">timestamptz</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="n">payload</span><span class="w"> </span><span class="n">jsonb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">event_time</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_2026_05</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">OF</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;2026-05-01&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;2026-06-01&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- LIST: tenant ID / region / status enum
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">bigint</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="n">tenant_id</span><span class="w"> </span><span class="nb">int</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">LIST</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_tenant_premium</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">OF</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="mi">1001</span><span class="p">,</span><span class="w"> </span><span class="mi">1002</span><span class="p">,</span><span class="w"> </span><span class="mi">1003</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span><span class="c1">-- HASH: 均勻散落（無自然 partition key）
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="n">user_id</span><span class="w"> </span><span class="nb">bigint</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">HASH</span><span class="w"> </span><span class="p">(</span><span class="n">user_id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users_0</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">OF</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">MODULUS</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="n">REMAINDER</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span></span></span></code></pre></div><p>策略選擇關鍵：</p>
<ul>
<li><strong>RANGE</strong> 適合 <em>時間 / 有序值</em> — query 多半帶 <code>WHERE event_time &gt;= X</code>、prune 效率最高；archive / drop 老資料是 <code>DROP PARTITION</code> 0.01 秒</li>
<li><strong>LIST</strong> 適合 <em>離散 enum / tenant</em> — query 帶 <code>WHERE tenant_id = X</code> prune；缺點是 tenant 增長要手動 ALTER ADD PARTITION</li>
<li><strong>HASH</strong> 適合 <em>均勻分散、沒自然 key</em> — query 多半 by-PK lookup、HASH 讓單 partition 大小均勻；prune 只在 <code>WHERE hash_key = X</code> 等值查詢觸發</li>
</ul>
<h3 id="選錯-partition-key-是最常見的錯誤">選錯 partition key 是最常見的錯誤</h3>
<p>例：events 表用 <code>user_id</code> HASH partition、但 query 多半 <code>WHERE event_time BETWEEN ...</code>、<code>user_id</code> 不在 WHERE — planner 沒法 prune、掃所有 partition、效能比單表更差（多了 partition routing overhead）。</p>
<p>partition key <em>必須</em> 對應 query 最常用的 WHERE filter；錯了就退化成 <em>維護面有好處、查詢面有壞處</em> 的尷尬狀態。</p>
<h2 id="partition-pruningplanner-怎麼決定跳過">Partition pruning：planner 怎麼決定跳過</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="p">(</span><span class="k">ANALYZE</span><span class="p">,</span><span class="w"> </span><span class="n">BUFFERS</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2026-05-15&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 期望輸出包含：
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">--  Append (cost=...)
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">--    -&gt; Seq Scan on events_2026_05  (cost=...)
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">-- (只 scan 一個 partition、其他 partition pruned)</span></span></span></code></pre></div><p>pruning 觸發條件：</p>
<ol>
<li>WHERE 含 partition key 的 <em>constant expression</em>（<code>WHERE x = 5</code> 觸發；<code>WHERE x = some_function()</code> 不觸發 planning-time prune、但 PG 11+ execution-time prune 可救）</li>
<li>PG 11+ 支援 <em>execution-time pruning</em> — query plan 內含 partition key、runtime 才知道值（prepared statement / NestedLoop join）</li>
<li>partition key 不在 WHERE 時 — <em>全部 partition 掃</em>、是反指標、表示 partition strategy 不對</li>
</ol>
<h3 id="partition-wise-join--aggregate-pg-11">Partition-wise join / aggregate (PG 11+)</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">enable_partitionwise_join</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">on</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">enable_partitionwise_aggregate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">on</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 兩個同 partition 策略的表 JOIN 時、planner 可 partition-wise 平行做
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="n">e</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">events_metadata</span><span class="w"> </span><span class="n">m</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">event_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">m</span><span class="p">.</span><span class="n">event_time</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">event_time</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="p">;</span></span></span></code></pre></div><p>需要兩個表 <em>partition strategy 完全一致</em>（同 partition key + 同 partition boundary）— 設計時對齊、後期不容易調整。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1partition-key-選錯query-變慢">Case 1：partition key 選錯，query 變慢</h3>
<p><strong>徵兆</strong>：partition 後特定查詢從 200ms 變成 2000ms；EXPLAIN 顯示 <code>Append</code> 下面所有 partition 都被 scan、沒 partition 被 prune。</p>
<p><strong>根因</strong>：partition by <code>user_id</code> HASH、但 query 多用 <code>WHERE created_at BETWEEN X AND Y</code>；planner 不知道 user 在哪個 partition、必須掃全部。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>驗證 step</strong>：partition 前先 <code>pg_stat_statements</code> 看 top 10 query 的 WHERE pattern、partition key 必須對應其中 80% 流量的 filter</li>
<li><strong>修正</strong>：DROP partition strategy、改 partition by <code>created_at</code> RANGE；遷移用 <code>pg_dump --section=data</code> per-partition 重灌</li>
<li><strong>避免</strong>：partitioning 不可逆、設計階段 query pattern 沒看清楚不要動</li>
</ol>
<h3 id="case-2cross-partition-unique-constraint-不-enforce">Case 2：cross-partition unique constraint 不 enforce</h3>
<p><strong>徵兆</strong>：partition 後發現 application code 寫死 duplicate user_email、但 unique constraint 沒擋；DB 內有同 email 多筆。</p>
<p><strong>根因</strong>：PostgreSQL partition table 的 <code>UNIQUE</code> constraint <em>必須包含 partition key</em> — <code>UNIQUE (email)</code> 在 partition by <code>tenant_id</code> 的表上 <em>無法 enforce</em>（PostgreSQL 拒建）；workaround 用 <code>UNIQUE (email, tenant_id)</code>、但業務語意是「email 全域唯一」、PG 無法保證。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：跨 partition 唯一性必須在 <em>application 層</em> enforce（lock + check 模式）</li>
<li><strong>替代</strong>：用 <em>non-partitioned</em> 表存唯一性目標（user_email_registry）、做寫入前 lookup</li>
<li><strong>設計階段檢查</strong>：partition by X、unique constraint 必須含 X；若業務要求 unique 不含 X、partition strategy 錯</li>
</ol>
<h3 id="case-3attach-partition-鎖表太久">Case 3：ATTACH PARTITION 鎖表太久</h3>
<p><strong>徵兆</strong>：新 month partition <code>ATTACH PARTITION</code> 跑 30 秒、期間整個 events 表 read 阻塞、application timeout 大量。</p>
<p><strong>根因</strong>：<code>ATTACH PARTITION</code> 預設加 <code>ACCESS EXCLUSIVE</code> lock 在 parent table、scan 整個新 partition 驗證 CHECK constraint；大 partition + 沒 CHECK constraint 預先驗證 → 鎖時間爆。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1. 先把要 attach 的 partition 加 CHECK constraint，用 NOT VALID 不掃描
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_2026_06</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">CONSTRAINT</span><span class="w"> </span><span class="n">events_2026_06_range</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">event_time</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-06-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2026-07-01&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">VALID</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 2. VALIDATE 用 SHARE UPDATE EXCLUSIVE lock、允許讀寫
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_2026_06</span><span class="w"> </span><span class="n">VALIDATE</span><span class="w"> </span><span class="k">CONSTRAINT</span><span class="w"> </span><span class="n">events_2026_06_range</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- 3. ATTACH 不再需要 scan（CHECK 已 VALIDATE 過）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="n">ATTACH</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">events_2026_06</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;2026-06-01&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;2026-07-01&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- ATTACH 變 instant</span></span></span></code></pre></div><h3 id="case-4partition-數爆炸planner-planning-time-爆">Case 4：partition 數爆炸，planner planning time 爆</h3>
<p><strong>徵兆</strong>：partition 累積到 500+（daily partition 跑 1-2 年）、簡單 query EXPLAIN 顯示 planning_time 從 1ms 漲到 200ms、application response 變慢。</p>
<p><strong>根因</strong>：partition 越多 planner 要評估的 partition 越多、即使有 pruning、planning 階段也要 walk 全部 partition table；500+ partition 是 planning overhead 明顯的閾值。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：partition granularity 對應 retention — 不要 daily partition 留 2 年（→ weekly / monthly）</li>
<li><strong>archive 老 partition</strong>：DETACH 老 partition、轉成 cold storage 表、planner 不再看</li>
<li><strong><code>enable_partition_pruning</code></strong> 預設 on、確保啟用</li>
<li><strong>PG 12+</strong>：planner 對 partition table 的 list 處理優化、planning time 上限拉高、但仍要控</li>
</ol>
<h3 id="case-5detach-後磁碟空間沒回收">Case 5：DETACH 後磁碟空間沒回收</h3>
<p><strong>徵兆</strong>：DETACH PARTITION 後 <code>pg_database_size</code> 沒下降、預期釋放 50GB；磁碟仍滿。</p>
<p><strong>根因</strong>：DETACH 只是把 partition 從 parent table <em>分離</em>、partition 自己仍是獨立表存在；要真釋放需要 <code>DROP TABLE detached_partition</code>。SRE 以為 DETACH = 刪掉。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 完整流程
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="n">DETACH</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">events_2024_01</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- events_2024_01 仍存在、佔磁碟
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 確認沒 query 在用後
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_2024_01</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 才釋放磁碟</span></span></span></code></pre></div><h3 id="routinearchive-workflow">Routine：archive workflow</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 月底跑：
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">-- 1. detach 13 個月前的 partition
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="n">DETACH</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="n">events_2025_04</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 2. dump 到 cold storage
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="err">\</span><span class="k">COPY</span><span class="w"> </span><span class="n">events_2025_04</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;/cold/events_2025_04.csv&#39;</span><span class="w"> </span><span class="p">(</span><span class="n">FORMAT</span><span class="w"> </span><span class="n">CSV</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- 3. drop 釋放磁碟
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_2025_04</span><span class="p">;</span></span></span></code></pre></div><h2 id="容量規劃">容量規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 partition size</td>
          <td>跟單表 vacuum 上限對齊（10-100GB sweet spot）</td>
          <td>&gt; 200GB 時考慮 sub-partition 或細化 granularity</td>
      </tr>
      <tr>
          <td>Partition 數量</td>
          <td>對應 retention × granularity</td>
          <td>&gt; 200 partition 時 planning time 開始浮現</td>
      </tr>
      <tr>
          <td>Partition key cardinality</td>
          <td>LIST：&lt; 100 / HASH：自定 modulus / RANGE：時間 + 維度</td>
          <td>太多獨立 partition value 用 HASH</td>
      </tr>
      <tr>
          <td>Cross-partition query 比例</td>
          <td>EXPLAIN 看 partition scan 數</td>
          <td>&gt; 30% query 掃 &gt; 50% partition 表示 key 選錯</td>
      </tr>
      <tr>
          <td>Maintenance window</td>
          <td>DROP / DETACH / ATTACH 各 partition 各自管</td>
          <td>hot partition 維護仍在 maintenance window</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>時間序列（events / log）：monthly RANGE partition、retention 12-24 個月</li>
<li>Multi-tenant（orders / records）：tenant_id LIST partition + 大 tenant 各自獨立 partition</li>
<li>均勻散落（user / metric）：8-16 個 HASH partition、單 partition 50-100GB</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-autovacuum-tuning-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a> 整合</h3>
<p>partitioning 是 autovacuum 問題的長期解：</p>
<ol>
<li>Hot partition autovacuum 緊（scale_factor 0.05、cost_limit 5000）</li>
<li>Cold partition <code>autovacuum_enabled = false</code></li>
<li>但 partition 數爆會把 <code>autovacuum_max_workers</code> 跑滿、需要拉</li>
</ol>
<h3 id="跟-index-設計整合">跟 index 設計整合</h3>
<p>partition table 的 index 處理：</p>
<ol>
<li>PG 11+ 全域 index：<code>CREATE INDEX ON partitioned_table (...)</code> 自動在每 partition 建 local index</li>
<li><strong>不存在跨 partition unique</strong> — 只能 partition-local</li>
<li><strong>partition-wise index scan</strong>：PG 11+ 跟 partition-wise join 一起、index lookup 平行</li>
</ol>
<h3 id="跟-backup--pitr">跟 backup / PITR</h3>
<p>partition 不是 backup 替代品 — 但能加速 <em>partial restore</em>：</p>
<ol>
<li>只 restore 特定時段的 partition、不用 restore 整個表</li>
<li>對應 <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL archiving</a> 的 partial recovery scenario</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Sub-partitioning</strong>：partition 內再 partition（時間 + tenant）、適合 multi-tenant + 時間序列</li>
<li><strong>pg_partman extension</strong>：自動建月 partition、不用 cron</li>
<li><strong>Foreign key to partitioned table</strong> (PG 12+)：跨 partition FK enforce、但 cascade 限制多</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">Schema Design</a> — partition 是 schema 決策</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a> / <a href="/blog/backend/01-database/vendors/postgresql/timescaledb-deep-dive/" data-link-title="TimescaleDB Deep Dive：Hypertable / Continuous Aggregate / Compression 把 PG 變 Time-Series DB" data-link-desc="TimescaleDB 是 PG extension（不是 fork）、用 *hypertable* 自動 partition by time、加 *continuous aggregate* 做 incremental materialized view、加 *compression* 對舊 chunk 壓 90%&#43;、把 PG 變成 InfluxDB / Prometheus 級 time-series DB。本文走 hypertable 機制、continuous aggregate 跟普通 MV 差異、compression policy、retention policy、5 production 踩雷（chunk size 不對 / CAGG refresh 落後 / compression 後 update 限制 / hypertable 不能加 FK / TimescaleDB 跟 PG 主版本對齊）、跟 PG 原生 partitioning 對比">TimescaleDB Deep Dive</a>（hypertable 是 partition 自動化）</li>
<li>後續路由：<a href="/blog/backend/01-database/vendors/postgresql/partition-redesign/" data-link-title="PostgreSQL Partition Redesign：當 monthly partition 越跑越慢" data-link-desc="PostgreSQL partition redesign 是 Type F「topology re-layout」第 2 個 dogfood — 從 monthly partition 改 daily / 從 range 改 list / 從單軸改 sub-partition；6 維 audit 皆 Low &#43; topology 軸 High；涵蓋 partition 不平衡偵測、ATTACH/DETACH 線上重劃、5 個 production 踩雷、跟 partition_pruning &#43; autovacuum 整合">Partition Redesign</a>（重排 partition strategy 的 migration playbook）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>9.C33 Maersk + Bosch：傳統產業在 Azure AKS 上的微服務治理</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/</guid><description>&lt;p>這個案例的核心責任是補強 Azure compute / K8s 維度缺口。Maersk（全球最大貨櫃航運公司、每天處理百萬級貨櫃移動）跟 Bosch（德國工業集團、智慧建築 IoT）是 &lt;em>傳統產業上雲&lt;/em> 的代表 — 跟 &lt;a href="https://tarrragon.github.io/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 雲原生 EKS&lt;/a> 形成對比、傳統產業的 K8s 採用動機跟雲原生公司不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Maersk + Bosch 在 Azure AKS 的關鍵敘述（引自 &lt;a href="https://azure.microsoft.com/en-us/products/kubernetes-service/">AKS Customer Stories&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Maersk&lt;/th>
 &lt;th>Bosch Software Innovations&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>行業&lt;/td>
 &lt;td>全球海運&lt;/td>
 &lt;td>工業 IoT（Connected Building Solution）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要 workload&lt;/td>
 &lt;td>貨櫃追蹤、港口物流、行程規劃&lt;/td>
 &lt;td>樓宇感測、能源管理、設備運維&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AKS 用途&lt;/td>
 &lt;td>deployment + 運維 + 管理 Kubernetes API&lt;/td>
 &lt;td>microservices 監控、不同 release cycle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程訴求&lt;/td>
 &lt;td>「focus on things that makes the most business impact」&lt;/td>
 &lt;td>「simplify management of microservices released on different cycles」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務組合&lt;/td>
 &lt;td>AKS + Azure 管理工具&lt;/td>
 &lt;td>AKS + monitoring capabilities&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>其他常見 AKS 大客戶：Siemens Healthineers（醫療設備）、Finastra（金融軟體）、Hafslund（能源）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Maersk 跟 Bosch 案例揭露三個傳統產業 K8s 治理的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>傳統產業上 K8s 的動機是「治理一致性」、不是「成長彈性」&lt;/strong>：
&lt;ul>
&lt;li>雲原生公司（Riot、Netflix）上 K8s 是為了 &lt;em>快速擴容&lt;/em> 跟 &lt;em>跨 region 部署&lt;/em>&lt;/li>
&lt;li>傳統產業上 K8s 是為了 &lt;em>統一 50+ 個應用團隊的部署流程&lt;/em>、降低 ops 複雜度&lt;/li>
&lt;li>訴求不同、配置不同 — 傳統產業可能用 &lt;em>較大 node、較少 cluster&lt;/em>、不是 &lt;a href="https://tarrragon.github.io/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 246 cluster&lt;/a> 那種多 cluster 策略&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>微服務 release cycle 多元化是傳統產業上 K8s 的核心需求&lt;/strong>：Bosch Connected Building 有「樓宇感測 daily release、能源計費 weekly release、設備運維 monthly release」、每個 release cycle 不同。K8s + GitOps（Argo CD、Flux）讓不同 cycle 共存於同一 cluster。對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組&lt;/a> 的 release governance。&lt;/li>
&lt;li>&lt;strong>「focus on business impact」是 managed K8s 的真正價值&lt;/strong>：Maersk 不是科技公司、是航運公司。工程資源從 &lt;em>維持 K8s 運維&lt;/em> 釋放到 &lt;em>貨櫃追蹤演算法、港口物流優化&lt;/em>、是商業 ROI 的關鍵。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">9.C29 Lemino 90% 工程工時下降&lt;/a> 的同類訴求、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 的人力成本工程化。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：Azure 官方對 Maersk / Bosch 的描述偏行銷、缺具體 throughput / latency 數字。讀此類案例要對 &lt;em>策略&lt;/em> 學習、不要套用數字。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 Azure compute / K8s 維度缺口。Maersk（全球最大貨櫃航運公司、每天處理百萬級貨櫃移動）跟 Bosch（德國工業集團、智慧建築 IoT）是 <em>傳統產業上雲</em> 的代表 — 跟 <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 雲原生 EKS</a> 形成對比、傳統產業的 K8s 採用動機跟雲原生公司不同。</p>
<h2 id="觀察">觀察</h2>
<p>Maersk + Bosch 在 Azure AKS 的關鍵敘述（引自 <a href="https://azure.microsoft.com/en-us/products/kubernetes-service/">AKS Customer Stories</a>）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Maersk</th>
          <th>Bosch Software Innovations</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行業</td>
          <td>全球海運</td>
          <td>工業 IoT（Connected Building Solution）</td>
      </tr>
      <tr>
          <td>主要 workload</td>
          <td>貨櫃追蹤、港口物流、行程規劃</td>
          <td>樓宇感測、能源管理、設備運維</td>
      </tr>
      <tr>
          <td>AKS 用途</td>
          <td>deployment + 運維 + 管理 Kubernetes API</td>
          <td>microservices 監控、不同 release cycle</td>
      </tr>
      <tr>
          <td>工程訴求</td>
          <td>「focus on things that makes the most business impact」</td>
          <td>「simplify management of microservices released on different cycles」</td>
      </tr>
      <tr>
          <td>服務組合</td>
          <td>AKS + Azure 管理工具</td>
          <td>AKS + monitoring capabilities</td>
      </tr>
  </tbody>
</table>
<p>其他常見 AKS 大客戶：Siemens Healthineers（醫療設備）、Finastra（金融軟體）、Hafslund（能源）。</p>
<h2 id="判讀">判讀</h2>
<p>Maersk 跟 Bosch 案例揭露三個傳統產業 K8s 治理的工程重點。</p>
<ol>
<li><strong>傳統產業上 K8s 的動機是「治理一致性」、不是「成長彈性」</strong>：
<ul>
<li>雲原生公司（Riot、Netflix）上 K8s 是為了 <em>快速擴容</em> 跟 <em>跨 region 部署</em></li>
<li>傳統產業上 K8s 是為了 <em>統一 50+ 個應用團隊的部署流程</em>、降低 ops 複雜度</li>
<li>訴求不同、配置不同 — 傳統產業可能用 <em>較大 node、較少 cluster</em>、不是 <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 246 cluster</a> 那種多 cluster 策略</li>
</ul>
</li>
<li><strong>微服務 release cycle 多元化是傳統產業上 K8s 的核心需求</strong>：Bosch Connected Building 有「樓宇感測 daily release、能源計費 weekly release、設備運維 monthly release」、每個 release cycle 不同。K8s + GitOps（Argo CD、Flux）讓不同 cycle 共存於同一 cluster。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 release governance。</li>
<li><strong>「focus on business impact」是 managed K8s 的真正價值</strong>：Maersk 不是科技公司、是航運公司。工程資源從 <em>維持 K8s 運維</em> 釋放到 <em>貨櫃追蹤演算法、港口物流優化</em>、是商業 ROI 的關鍵。對應 <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 90% 工程工時下降</a> 的同類訴求、跟 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的人力成本工程化。</li>
</ol>
<p>需要警惕：Azure 官方對 Maersk / Bosch 的描述偏行銷、缺具體 throughput / latency 數字。讀此類案例要對 <em>策略</em> 學習、不要套用數字。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>傳統產業 K8s 採用先做「單一 cluster 多 namespace」、再考慮多 cluster</strong>：管理 1 個大 cluster 比管理 246 個小 cluster 容易。除非有 <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>、否則 single-cluster-multi-namespace 是 sane default。</li>
<li><strong>不同 release cycle 用 GitOps + namespace 隔離</strong>：每個團隊 own 自己的 namespace、配合 Argo CD / Flux 各自 release。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a>。</li>
<li><strong>AKS / EKS / GKE 的差異對傳統產業不關鍵</strong>：選哪家通常取決於企業已用哪家 cloud、不是 K8s feature 本身。重點是 <em>managed K8s ops 比自管划算</em>、不是哪家 managed 最好。</li>
<li><strong>監控訊號設計按業務 cycle</strong>：每天 release 的服務跟每月 release 的服務 monitoring 策略不同、alert 敏感度不同。對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>。</li>
</ol>
<p>跨平台等效：AWS EKS、GCP GKE、自管 Kubernetes + Rancher 都可實作對等架構。Azure 在 enterprise 整合（Active Directory、Azure DevOps）有優勢、特別適合 Microsoft 生態企業。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照雲原生 K8s 策略 → <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster</a></li>
<li>對照其他 managed 服務釋放工程資源 → <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> / <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></li>
<li>想設計 K8s 治理 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://azure.microsoft.com/en-us/products/kubernetes-service/">Azure Kubernetes Service customer stories</a></li>
<li><a href="https://customers.microsoft.com/en-us/story/maersk-travel-transportation-azure">Maersk Azure case</a></li>
<li><a href="https://azure.microsoft.com/en-us/blog/product/azure-kubernetes-service-aks/">Bosch Software Innovations</a></li>
<li><a href="https://azure.microsoft.com/en-us/solutions/kubernetes-on-azure">Kubernetes on Azure - Enterprise Expertise</a></li>
</ul>
]]></content:encoded></item><item><title>SLI / SLO</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/</guid><description>&lt;p>SLI / SLO 的核心概念是「用可量測訊號表達服務承諾」。SLI（Service Level Indicator）是服務品質指標 — 成功率、延遲、可用性；SLO（Service Level Objective）是這些指標的目標 — 99.9% request 在 300ms 內成功回應。SLO 的執行力來自 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> — 預算耗盡就暫停發版。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>SLI / SLO 把觀測資料轉成決策語言。單純看到 error rate 上升只能說明症狀；對照 SLO 後，團隊才能判斷是否需要暫停發版、啟動 incident、擴容或降級。SLO 不是「越高越好」— 99.999% 的 SLO 意味著幾乎沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 做變更，反而限制了功能交付速度。&lt;/p>
&lt;p>SLI 的設計起點是使用者旅程（checkout 是否成功、搜尋是否夠快），量測點選擇（edge / gateway / service）決定了 SLI 反映的是「使用者體驗」還是「基礎設施健康」。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 SLI / SLO 的訊號是服務重要性已經影響收入、合約或使用者信任。付款、登入、訂單建立與訊息送達通常需要不同 SLO，因為失敗代價不同。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>SLI 需要定義「什麼算 good request」的邊界（5xx 算 bad、4xx 通常不算）。SLO 需要定義目標值、量測窗口（30 天 rolling）跟 owner。SLO 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> alerting 搭配使用，讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 反映使用者影響而非基礎設施噪音。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>SLI / SLO 的核心概念是「用可量測訊號表達服務承諾」。SLI（Service Level Indicator）是服務品質指標 — 成功率、延遲、可用性；SLO（Service Level Objective）是這些指標的目標 — 99.9% request 在 300ms 內成功回應。SLO 的執行力來自 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> — 預算耗盡就暫停發版。</p>
<h2 id="概念位置">概念位置</h2>
<p>SLI / SLO 把觀測資料轉成決策語言。單純看到 error rate 上升只能說明症狀；對照 SLO 後，團隊才能判斷是否需要暫停發版、啟動 incident、擴容或降級。SLO 不是「越高越好」— 99.999% 的 SLO 意味著幾乎沒有 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 做變更，反而限制了功能交付速度。</p>
<p>SLI 的設計起點是使用者旅程（checkout 是否成功、搜尋是否夠快），量測點選擇（edge / gateway / service）決定了 SLI 反映的是「使用者體驗」還是「基礎設施健康」。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 SLI / SLO 的訊號是服務重要性已經影響收入、合約或使用者信任。付款、登入、訂單建立與訊息送達通常需要不同 SLO，因為失敗代價不同。</p>
<h2 id="設計責任">設計責任</h2>
<p>SLI 需要定義「什麼算 good request」的邊界（5xx 算 bad、4xx 通常不算）。SLO 需要定義目標值、量測窗口（30 天 rolling）跟 owner。SLO 跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alerting 搭配使用，讓 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 反映使用者影響而非基礎設施噪音。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Aurora PG/MySQL vs Aurora DSQL 取捨：何時 single-region managed 夠用、何時跨到 distributed</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/aurora-vs-dsql-tradeoff/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/aurora-vs-dsql-tradeoff/</guid><description>&lt;blockquote>
&lt;p>本文是 Aurora family 內的決策取捨文章。聚焦 &lt;em>standard Aurora（Aurora PostgreSQL / MySQL，single-region managed SQL）&lt;/em> 跟 &lt;em>Aurora DSQL（active-active distributed SQL）&lt;/em> 之間的升級門檻判斷。兩個既有 SSoT 不在本篇重複：「PG → DSQL 怎麼遷」見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &amp;#43; snapshot isolation &amp;#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &amp;#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql&lt;/a>；「DSQL vs Spanner vs CockroachDB 三方 distributed SQL 選型」見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &amp;#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree&lt;/a>。本篇只回答「standard Aurora 夠不夠、要不要跨過去」。&lt;/p>&lt;/blockquote>
&lt;p>多數團隊不需要 Aurora DSQL。Aurora PostgreSQL / MySQL 已經是 managed SQL、storage / compute 分離、跨 AZ 高可用、read replica 擴讀——絕大多數 OLTP workload 在這層就解決了。Aurora DSQL 是 2024-12 re:Invent preview、2025-05 GA 的 &lt;em>不同 paradigm&lt;/em> 產品：PG wire-compatible 但底層是 active-active distributed、OCC + snapshot isolation、multi-region strong consistency。它解的是 standard Aurora &lt;em>解不了&lt;/em> 的特定問題，代價是放棄一部分 PostgreSQL 相容性與交易自由度。要不要跨過去，看 workload 是否真的撞到 standard Aurora 的結構上限。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 Aurora family 內的決策取捨文章。聚焦 <em>standard Aurora（Aurora PostgreSQL / MySQL，single-region managed SQL）</em> 跟 <em>Aurora DSQL（active-active distributed SQL）</em> 之間的升級門檻判斷。兩個既有 SSoT 不在本篇重複：「PG → DSQL 怎麼遷」見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a>；「DSQL vs Spanner vs CockroachDB 三方 distributed SQL 選型」見 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a>。本篇只回答「standard Aurora 夠不夠、要不要跨過去」。</p></blockquote>
<p>多數團隊不需要 Aurora DSQL。Aurora PostgreSQL / MySQL 已經是 managed SQL、storage / compute 分離、跨 AZ 高可用、read replica 擴讀——絕大多數 OLTP workload 在這層就解決了。Aurora DSQL 是 2024-12 re:Invent preview、2025-05 GA 的 <em>不同 paradigm</em> 產品：PG wire-compatible 但底層是 active-active distributed、OCC + snapshot isolation、multi-region strong consistency。它解的是 standard Aurora <em>解不了</em> 的特定問題，代價是放棄一部分 PostgreSQL 相容性與交易自由度。要不要跨過去，看 workload 是否真的撞到 standard Aurora 的結構上限。</p>
<blockquote>
<p><strong>時間錨點</strong>：Aurora DSQL 2024-12 preview、2025-05 GA。vendor 能力持續演進、實際決策前以 AWS docs 當前狀態為準。</p></blockquote>
<h2 id="核心差異single-writer-vs-active-active">核心差異：single-writer vs active-active</h2>
<p>兩者的根本差異在寫入架構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Aurora PG / MySQL（standard）</th>
          <th>Aurora DSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入架構</td>
          <td>single writer（一個 region 一個 writer）</td>
          <td>active-active（多 region 同時可寫）</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>單 region 強一致、跨 region 非同步</td>
          <td>multi-region strong consistency</td>
      </tr>
      <tr>
          <td>SQL 相容</td>
          <td>完整 PostgreSQL / MySQL</td>
          <td>PG wire-compatible <em>子集</em>、無多數 extension</td>
      </tr>
      <tr>
          <td>交易模型</td>
          <td>標準 PG/MySQL transaction、長交易</td>
          <td>OCC + snapshot isolation、需處理 retry</td>
      </tr>
      <tr>
          <td>寫入擴展</td>
          <td>受 single writer instance 上限約束</td>
          <td>水平擴展、無 single writer 瓶頸</td>
      </tr>
      <tr>
          <td>運維</td>
          <td>managed、但仍要管 instance / failover</td>
          <td>serverless、zero-touch、無 instance 概念</td>
      </tr>
  </tbody>
</table>
<p>standard Aurora 的 storage 層雖然分散，<em>compute 寫入仍是 single writer</em>——這是它的結構上限。DSQL 把寫入也分散，代價是 SQL 相容性縮窄（PG 子集、extension 缺位）與交易語意改變（OCC，衝突要 application retry）。</p>
<h2 id="該跨到-dsql-的訊號">該跨到 DSQL 的訊號</h2>
<p>只有撞到 standard Aurora 結構上限的特定需求，才值得跨 paradigm：</p>
<ul>
<li><strong>global write（多 region 都要低延遲寫入）</strong>：standard Aurora 跨 region 只有非同步副本、寫入要回到單一 writer region；真正需要多 region active-active 寫入 → DSQL</li>
<li><strong>single-writer 寫入上限撞牆</strong>：寫入量大到單一 writer instance（即使最大 instance class）撐不住、且無法用 sharding 簡單解 → DSQL 的水平寫入擴展</li>
<li><strong>region resiliency（單 region 失效仍要可寫）</strong>：standard Aurora 的跨 region failover 有 RPO/RTO 與寫入中斷；要求單 region 失效時其他 region 仍持續接受寫入 → DSQL active-active</li>
<li><strong>operational zero-touch</strong>：不想管 instance / failover / 容量 → DSQL serverless 模型（但這單項不足以跨 paradigm、要搭配上面的結構需求）</li>
</ul>
<h2 id="不該跨的訊號standard-aurora-夠用">不該跨的訊號（standard Aurora 夠用）</h2>
<p>以下情況跨 DSQL 是過度工程、且會付出相容性代價：</p>
<ul>
<li><strong>single-region 夠用</strong>：寫入集中在一個 region、跨 region 只需要讀副本或 DR → standard Aurora</li>
<li><strong>需要 PostgreSQL extension</strong>：依賴 PostGIS / pgvector / 特定 extension → DSQL 子集不支援、留 standard Aurora</li>
<li><strong>複雜 / 長交易</strong>：依賴長交易、複雜多語句交易、特定 isolation 行為 → standard Aurora 的完整交易模型</li>
<li><strong>寫入量 standard Aurora 撐得住</strong>：single writer 還有餘量 → 不必為「未來可能」預先跨 paradigm</li>
</ul>
<p><code>9.C14 Standard Chartered</code> 與 <code>9.C4 DraftKings</code> 是反向佐證：金融帳本 / 博彩這類高一致性、高關鍵 OLTP workload，在 <em>standard Aurora</em> 上就能同時拿到韌性與性能（DraftKings replication lag 降到 10-30ms 級、Standard Chartered 把韌性與性能當單一目標）。它們沒有跨到 distributed SQL——因為 single-region 強一致 + 跨 AZ 高可用已滿足需求。多數金融 OLTP 不需要 active-active multi-region write。</p>
<blockquote>
<p><strong>Scope warning</strong>：Standard Chartered / DraftKings 的 case 揭露其用 standard Aurora 達成韌性 + 性能（見 <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>）；「它們不需要 DSQL」是本文基於其 single-region 強一致需求的推論、非 case 明文比較 DSQL。引用為「standard Aurora 已足夠多數高一致 OLTP」的訊號、不當 DSQL 對比的 case fact。</p></blockquote>
<h2 id="升級門檻決策流程">升級門檻決策流程</h2>
<p>從需求判讀到路徑選擇的流程：</p>
<h4 id="step-1確認是不是-global-write-需求">Step 1：確認是不是 global write 需求</h4>
<p>寫入是否真的需要多 region 同時低延遲？還是只需要多 region 讀 + 單 region 寫？後者 standard Aurora（+ Global Database 讀副本）就解。</p>
<h4 id="step-2確認-single-writer-是否真的撞牆">Step 2：確認 single-writer 是否真的撞牆</h4>
<p>當前寫入量 vs 最大 instance class 上限、是否已嘗試過 read/write 分離、是否能用 application 層 sharding。撞牆才考慮 DSQL；沒撞牆是過早優化。</p>
<h4 id="step-3檢查相容性代價">Step 3：檢查相容性代價</h4>
<p>清點對 PG extension、長交易、特定 SQL 功能的依賴。依賴重 → DSQL 相容性子集會擋路、留 standard Aurora。</p>
<h4 id="step-4若決定跨走既有-ssot">Step 4：若決定跨，走既有 SSoT</h4>
<ul>
<li>「PG → DSQL 怎麼遷」（protocol drop-in + paradigm shift、transaction retry 處理、extension 缺位）→ <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a></li>
<li>「DSQL vs Spanner vs CockroachDB 哪個 distributed SQL」→ <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a></li>
</ul>
<p><strong>Rollback boundary</strong>：跨 paradigm 是高成本決策——DSQL 子集相容性與 OCC 交易模型改變了 application 契約，回退到 standard Aurora 不是改 connection string 就好。決策前用一個非關鍵 workload 試點、確認相容性與 retry 行為，再擴大。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="為什麼這是升級門檻而非遷移">為什麼這是「升級門檻」而非「遷移」</h3>
<p>standard Aurora → DSQL 不是版本升級、是 paradigm 切換。Aurora PG/MySQL 用得好好的，不代表「升級到 DSQL 會更好」——多數情況會更差（失去 extension、交易要改、相容性縮窄）。只有 workload 真的需要 active-active multi-region write 或撞到 single-writer 上限，跨過去才划算。這跟「PostgreSQL major version upgrade」（同 paradigm、向後相容）是完全不同性質的決策。</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> — standard Aurora 的 storage 分散但 compute single-writer 的結構上限根源</li>
<li><a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">global-database-multi-region</a> — standard Aurora 的多 region 方案（非同步副本）、global write 需求前先確認這層夠不夠</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a> — 決定跨之後的遷移 playbook（SSoT）</li>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a> — 三方 distributed SQL 選型（SSoT）</li>
<li>替代路由：single-region 夠 → 留 standard Aurora；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/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">Standard Chartered 9.C14</a> / <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">DraftKings 9.C4</a> 互引：高一致 OLTP 在 standard Aurora 已足夠的訊號</li>
</ul>
]]></content:encoded></item><item><title>Spanner Change Streams (CDC)：捕捉資料變更、watch partition、下游整合與 DynamoDB Streams 對照</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/change-streams-cdc/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/change-streams-cdc/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>Change Streams&lt;/em> — Spanner 把 commit 後的 row mutation 變成下游可消費事件流的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">CDC&lt;/a> 機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境oltp-的變更要餵給搜尋快取分析三個下游">問題情境：OLTP 的變更要餵給搜尋、快取、分析三個下游&lt;/h2>
&lt;p>Change Streams 的責任是把 Spanner 內已 commit 的 row mutation 變成有序、可重放、攜帶 commit timestamp 的事件流、讓搜尋索引、快取、分析倉儲三類下游不用反覆 full-table scan 就能跟上主資料庫。OLTP 主庫負責正確寫入、下游各自負責自己的 query shape、兩邊之間需要一條「只送變更、不送全表」的管線、這條管線就是 CDC 的職責。&lt;/p>
&lt;p>讀者徵兆通常從這幾個地方浮現：搜尋團隊每 5 分鐘跑一次 full scan 把 orders 重灌進 Elasticsearch、Spanner CPU 被掃表打到 70%；快取層靠 TTL 過期被動失效、使用者看到舊價格;分析團隊想做近即時 dashboard、卻只有每日 batch export。共同壓力是「主庫的變更沒有一條乾淨的出口」、每個下游各自發明輪子去 poll 主庫。&lt;/p>
&lt;p>真實壓力場景：全球電商把訂單寫進 Spanner multi-region instance、需要把每筆訂單狀態變更同時推給 (1) 搜尋索引更新庫存可售性、(2) Pub/Sub 通知履約系統、(3) BigQuery 做近即時營收儀表板。三個下游對延遲、順序、retention 的要求不同、但都需要從同一條變更流取得資料。&lt;/p>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale&lt;/a> 提供「全球大規模 OLTP 寫入」的壓力 anchor — Google Ads / Play 計費的寫入量級說明為什麼下游不能靠 full scan 跟上。&lt;strong>dogfood 邊界明示&lt;/strong>：9.C10 是 Google 內部 dogfood case、未展開 change streams 實作細節；本文 change stream 的物件模型、partition 行為與 retention 上限均來自 GCP vendor 規格、不是 9.C10 case 揭露。&lt;/p>
&lt;h2 id="核心機制data-change-recordpartition-tokencommit-timestamp">核心機制：data change record、partition token、commit timestamp&lt;/h2>
&lt;p>Change Stream 是一個用 DDL 建立、綁定到特定 table / column 集合的 schema 物件、commit 後 Spanner 把對應 row 的 mutation 寫成 &lt;em>data change record&lt;/em> 供消費。它跟「在 application 層自己寫 outbox table」最大的差異是：change record 由 Spanner 內部跟 transaction commit 綁定產生、攜帶該 mutation 的 commit timestamp、繼承 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external consistency&lt;/a> 的全序性質、不需要 application 額外保證原子性。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>Change Streams</em> — Spanner 把 commit 後的 row mutation 變成下游可消費事件流的 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">CDC</a> 機制。</p></blockquote>
<hr>
<h2 id="問題情境oltp-的變更要餵給搜尋快取分析三個下游">問題情境：OLTP 的變更要餵給搜尋、快取、分析三個下游</h2>
<p>Change Streams 的責任是把 Spanner 內已 commit 的 row mutation 變成有序、可重放、攜帶 commit timestamp 的事件流、讓搜尋索引、快取、分析倉儲三類下游不用反覆 full-table scan 就能跟上主資料庫。OLTP 主庫負責正確寫入、下游各自負責自己的 query shape、兩邊之間需要一條「只送變更、不送全表」的管線、這條管線就是 CDC 的職責。</p>
<p>讀者徵兆通常從這幾個地方浮現：搜尋團隊每 5 分鐘跑一次 full scan 把 orders 重灌進 Elasticsearch、Spanner CPU 被掃表打到 70%；快取層靠 TTL 過期被動失效、使用者看到舊價格;分析團隊想做近即時 dashboard、卻只有每日 batch export。共同壓力是「主庫的變更沒有一條乾淨的出口」、每個下游各自發明輪子去 poll 主庫。</p>
<p>真實壓力場景：全球電商把訂單寫進 Spanner multi-region instance、需要把每筆訂單狀態變更同時推給 (1) 搜尋索引更新庫存可售性、(2) Pub/Sub 通知履約系統、(3) BigQuery 做近即時營收儀表板。三個下游對延遲、順序、retention 的要求不同、但都需要從同一條變更流取得資料。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale</a> 提供「全球大規模 OLTP 寫入」的壓力 anchor — Google Ads / Play 計費的寫入量級說明為什麼下游不能靠 full scan 跟上。<strong>dogfood 邊界明示</strong>：9.C10 是 Google 內部 dogfood case、未展開 change streams 實作細節；本文 change stream 的物件模型、partition 行為與 retention 上限均來自 GCP vendor 規格、不是 9.C10 case 揭露。</p>
<h2 id="核心機制data-change-recordpartition-tokencommit-timestamp">核心機制：data change record、partition token、commit timestamp</h2>
<p>Change Stream 是一個用 DDL 建立、綁定到特定 table / column 集合的 schema 物件、commit 後 Spanner 把對應 row 的 mutation 寫成 <em>data change record</em> 供消費。它跟「在 application 層自己寫 outbox table」最大的差異是：change record 由 Spanner 內部跟 transaction commit 綁定產生、攜帶該 mutation 的 commit timestamp、繼承 <a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external consistency</a> 的全序性質、不需要 application 額外保證原子性。</p>
<p>建立語法是 DDL：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 監看整個資料庫
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">CHANGE</span><span class="w"> </span><span class="n">STREAM</span><span class="w"> </span><span class="n">everything_stream</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">ALL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 只監看特定 table 的特定欄位
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">CHANGE</span><span class="w"> </span><span class="n">STREAM</span><span class="w"> </span><span class="n">orders_stream</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="n">orders</span><span class="p">(</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">total_amount</span><span class="p">),</span><span class="w"> </span><span class="n">inventory</span><span class="p">(</span><span class="n">available_qty</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="n">retention_period</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;7d&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">value_capture_type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;NEW_AND_OLD_VALUES&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="p">);</span></span></span></code></pre></div><p><code>value_capture_type</code> 決定 record 攜帶多少資料、三個選項對下游的意義不同：</p>
<table>
  <thead>
      <tr>
          <th>value_capture_type</th>
          <th>record 攜帶內容</th>
          <th>適合下游</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>OLD_AND_NEW_VALUES</code></td>
          <td>變更前後完整 row</td>
          <td>需要 diff / 審計 / 反向補償的下游</td>
      </tr>
      <tr>
          <td><code>NEW_VALUES</code></td>
          <td>變更後的值 + key</td>
          <td>搜尋索引、快取 upsert（只要最新狀態）</td>
      </tr>
      <tr>
          <td><code>NEW_ROW</code></td>
          <td>變更後完整 row（含未改欄位）</td>
          <td>不想自己拼 row 的下游、犧牲 record 體積</td>
      </tr>
  </tbody>
</table>
<h3 id="data-change-record-的關鍵欄位">Data change record 的關鍵欄位</h3>
<p>每筆 data change record 攜帶 commit timestamp、record sequence、transaction tag、mod type（INSERT / UPDATE / DELETE）、以及 primary key 與依 capture type 決定的 value payload。下游靠 commit timestamp + record sequence 在同一個 transaction 內重建變更順序、跨 transaction 則靠 commit timestamp 的全序。這條順序保證是 Spanner CDC 跟「自己 poll updated_at column」的根本差異：poll updated_at 在 clock skew 下會漏序、change stream 的順序由 TrueTime 撐住。</p>
<h3 id="watch-partitionchange-stream-的-partition-模型">Watch partition：change stream 的 partition 模型</h3>
<p>Change stream 的讀取單位是 <em>partition</em>、不是整條流。Spanner 把 change stream 依底層 key range 切成多個 partition、每個 partition 用一個 <em>partition token</em> 標識、消費者對每個 token 各開一個 <code>read</code> 呼叫並行讀。當底層資料 split 或 merge（Spanner 自動 re-balance key range）、partition 會產生 <em>child partition</em> — 父 partition 的 record 讀到結束時回傳 child partition token、消費者要接著去讀 child token、才不會漏掉 split 後的變更。</p>
<p>這個 child partition 的接力機制是 change stream 消費的核心複雜度。手刻消費者必須維護一張 partition token 的 watermark 表、處理 parent 結束 → child 開始的交棒、保證每個 token 只被一個 worker 讀。多數團隊不該手刻這層、應走 Dataflow connector（下節）讓它代管 partition 生命週期。</p>
<blockquote>
<p><strong>Scope warning</strong>：本節 data change record 欄位、value_capture_type 選項、child partition 接力語意均屬 GCP Spanner change streams 規格、實作前 cross-verify <a href="https://cloud.google.com/spanner/docs/change-streams">Spanner change streams 官方文件</a>。retention_period、partition 切分行為隨版本演進、非 9.C10 case 揭露。</p></blockquote>
<h2 id="操作流程建立-change-stream-到-dataflow-下游">操作流程：建立 change stream 到 Dataflow 下游</h2>
<h3 id="step-1建立-change-stream-並驗證">Step 1：建立 change stream 並驗證</h3>
<p>用 DDL 建立 change stream 後、用 information schema 確認它存在、並用 metadata 查詢確認監看範圍正確。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">CHANGE</span><span class="w"> </span><span class="n">STREAM</span><span class="w"> </span><span class="n">orders_stream</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">FOR</span><span class="w"> </span><span class="n">orders</span><span class="p">,</span><span class="w"> </span><span class="n">inventory</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="n">retention_period</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;7d&#39;</span><span class="p">);</span></span></span></code></pre></div><p>驗證：查 <code>INFORMATION_SCHEMA.CHANGE_STREAMS</code> 確認 stream 已建立、查 <code>CHANGE_STREAM_TABLES</code> 確認監看的 table 集合符合預期。若監看範圍寫錯（漏了某 table）、下游會靜默漏掉那張表的變更、這是高代價的靜默失敗、必須在這步驗證。</p>
<h3 id="step-2選消費路徑--dataflow-connector-為預設">Step 2：選消費路徑 — Dataflow connector 為預設</h3>
<p>消費 change stream 有三條路徑、對應不同的下游能力與運維成本：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>partition 管理</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dataflow + Apache Beam SpannerIO connector</td>
          <td>connector 代管</td>
          <td>串到 BigQuery / GCS / Pub/Sub、需 exactly-once</td>
      </tr>
      <tr>
          <td>Pub/Sub via Dataflow template</td>
          <td>template 代管</td>
          <td>fan-out 給多個事件驅動下游</td>
      </tr>
      <tr>
          <td>直接用 client library 讀 partition</td>
          <td>自己維護 token watermark</td>
          <td>客製化邏輯、能承擔 partition 生命週期工程</td>
      </tr>
  </tbody>
</table>
<p>Dataflow connector 是預設路徑、因為它代管 partition token 的 split / merge 接力、提供 checkpoint 與 exactly-once 到下游 sink。</p>
<h3 id="step-3部署-dataflow-pipeline-並驗證-end-to-end">Step 3：部署 Dataflow pipeline 並驗證 end-to-end</h3>
<p>用官方 Spanner-to-BigQuery 或 Spanner-to-PubSub Dataflow template 部署。驗證 end-to-end：在 Spanner 寫一筆變更、量它多久出現在下游、確認 commit timestamp 在下游被保留、確認 INSERT / UPDATE / DELETE 三種 mod type 都被正確處理（DELETE 特別容易在下游被漏掉、要專門測）。</p>
<h3 id="step-4rollback-boundary">Step 4：rollback boundary</h3>
<p>Change stream 是可加可刪的 schema 物件、<code>DROP CHANGE STREAM orders_stream</code> 即停止捕捉、不影響主表寫入。rollback boundary 在「停掉 Dataflow pipeline + 標記下游資料為 stale」、不是「改主庫 schema」 — change stream 本身對 OLTP write path 的影響極小、刪除它不需要 cutover window。</p>
<h2 id="失敗模式retention-過期下游慢於-retentiondelete-漏處理">失敗模式：retention 過期、下游慢於 retention、DELETE 漏處理</h2>
<h3 id="retention-窗口過期導致-partition-不可讀">Retention 窗口過期導致 partition 不可讀</h3>
<p>change stream 的 record 只保留 retention_period（預設 1 天、上限數天、查官方文件確認當前上限）。若下游消費者停機超過 retention 窗口、過期 partition 的 record 被 GC、消費者重啟後讀到 partition token 已失效的錯誤、那段變更永久漏掉。徵兆是消費者重啟後報 partition not found、下游資料出現一段空洞。修法是 retention_period 設成大於「最壞情況下游停機 + 重啟趕上」的時間、並對 change stream 的 consumer lag 設告警、lag 接近 retention 一半就 page。</p>
<blockquote>
<p><strong>Scope warning</strong>：retention_period 的預設值與上限屬 GCP 規格、隨版本變動、cross-verify 官方文件。本段 lag 告警閾值（retention 一半）是通用工程估算、不是 9.C10 case 揭露的數字。</p></blockquote>
<h3 id="下游消費吞吐慢於主庫寫入速率">下游消費吞吐慢於主庫寫入速率</h3>
<p>主庫 write rate 持續高於下游消費速率、consumer lag 單調上升、最終撞 retention 窗口漏資料。這在全球大規模 OLTP 寫入下是真實壓力 — 對應 9.C10 揭露的 Google internal dogfood 寫入量級（<strong>dogfood 邊界</strong>：該量級是 Google 全使用者加總、不是單一 instance 配額）。修法是擴 Dataflow worker、確認 partition 數足夠讓消費並行、必要時把單一 change stream 依 table 拆成多條降低單條負載。判讀訊號是 Dataflow backlog metric 持續成長、不是偶發 spike。</p>
<h3 id="delete-變更在下游被漏處理">DELETE 變更在下游被漏處理</h3>
<p>下游 pipeline 只處理 INSERT / UPDATE 的 upsert、忘了處理 DELETE 的 tombstone、導致下游索引 / 快取殘留已刪除的資料。徵兆是搜尋結果出現主庫已不存在的項目、對帳發現下游 row count 高於主庫。修法是 pipeline 顯式 handle mod type = DELETE、依 capture type 決定能否拿到 old values 來反向補償；若用 <code>NEW_VALUES</code> capture、DELETE record 只攜帶 key、下游必須靠 key 刪除、不能假設拿得到完整 old row。</p>
<h3 id="把-change-stream-當可靠-message-queue-用">把 change stream 當可靠 message queue 用</h3>
<p>change stream 是 <em>變更捕捉</em>、不是 general-purpose message bus。團隊若把它當成「任意事件都塞進來」的 queue、會發現它只能攜帶 row mutation、不能攜帶 application 自定義事件、且 retention 比專用 message broker 短。<strong>Anti-recommendation（何時不用）</strong>：需要長期保留、任意 payload、複雜 routing 的事件流、用 Pub/Sub 或 Kafka 當 SSoT、change stream 只負責「資料庫變更」這一類來源；把 application 業務事件硬塞進 change stream 是把 CDC 機制誤用成 event bus。</p>
<h2 id="容量與觀測consumer-lag-是核心健康訊號">容量與觀測：consumer lag 是核心健康訊號</h2>
<p>Change stream 的容量壓力集中在「下游能不能跟上主庫寫入」、核心 metric 是 consumer lag 與 partition 並行度。</p>
<p>必看 metric：</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">Dataflow data freshness / system lag   → 下游落後主庫 commit 的時間
</span></span><span class="line"><span class="ln">2</span><span class="cl">Dataflow backlog bytes / elements      → 未消費的 record 積壓量
</span></span><span class="line"><span class="ln">3</span><span class="cl">Spanner change stream partition count  → 並行讀取單位、隨底層 split 變化
</span></span><span class="line"><span class="ln">4</span><span class="cl">Spanner CPU utilization                → change stream 讀取也消耗主 instance CPU</span></span></code></pre></div><p>Change stream 的讀取消耗主 instance 的 CPU 與 read capacity、不是免費旁路。容量規劃要把「change stream 消費」當成額外 read workload 算進 instance sizing、回 <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/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 把 consumer lag 跟 Spanner CPU 配成 evidence pair：lag 上升且 CPU 飽和、是 instance 容量不足；lag 上升但 CPU 有餘、是 Dataflow worker 不足。</p>
<p>Alert 建議：</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Warn</th>
          <th>Page</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dataflow data freshness</td>
          <td>&gt; retention 的 1/4</td>
          <td>&gt; retention 的 1/2</td>
      </tr>
      <tr>
          <td>Dataflow backlog 成長趨勢</td>
          <td>持續成長 30 分鐘</td>
          <td>持續成長 2 小時</td>
      </tr>
      <tr>
          <td>Spanner CPU（含 stream 讀取）</td>
          <td>&gt; 65%</td>
          <td>&gt; 80%</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>Scope warning</strong>：上述閾值為通用工程估算、依各團隊 retention 設定與 SLA 調整、非 9.C10 case 揭露的 production 數字。</p></blockquote>
<h2 id="邊界與整合跟-dynamodb-streams-對照何時不用-change-streams">邊界與整合：跟 DynamoDB Streams 對照、何時不用 change streams</h2>
<h3 id="跟-dynamodb-streams-的對照">跟 DynamoDB Streams 的對照</h3>
<p>Change Streams 跟 DynamoDB Streams 都是 managed CDC、但 partition 模型、ordering 範圍、retention 的設計取捨不同、選型時這三軸最關鍵。</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>Spanner Change Streams</th>
          <th>DynamoDB Streams</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ordering 範圍</td>
          <td>commit timestamp 全序（繼承 external consistency）</td>
          <td>每個 shard / partition key 內有序、跨 partition 無全序</td>
      </tr>
      <tr>
          <td>Partition 模型</td>
          <td>隨底層 key range split / merge、child partition 接力</td>
          <td>對應 DynamoDB partition、shard 隨 partition 變化</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>retention_period 可設（天級、查官方上限）</td>
          <td>固定 24 小時</td>
      </tr>
      <tr>
          <td>消費路徑</td>
          <td>Dataflow / Pub/Sub / client library</td>
          <td>Lambda trigger / Kinesis Adapter</td>
      </tr>
      <tr>
          <td>Payload 控制</td>
          <td>value_capture_type 三選</td>
          <td>StreamViewType 四選（KEYS_ONLY / NEW / OLD / BOTH）</td>
      </tr>
  </tbody>
</table>
<p>關鍵差異在 ordering：Spanner change stream 繼承 external consistency、跨 partition 的 record 可用 commit timestamp 排出全序;DynamoDB Streams 只保證單 partition key 內有序、跨 partition 重組需要下游自己處理。retention 上 DynamoDB Streams 固定 24 小時、Spanner 可設更長、對「下游可能長時間停機」的場景 Spanner 較有彈性。消費模型上 DynamoDB Streams 跟 Lambda 整合最順、Spanner 跟 Dataflow / BigQuery 生態整合最順。</p>
<blockquote>
<p><strong>Scope warning</strong>：DynamoDB Streams 24 小時 retention 與 StreamViewType 屬 AWS 規格、Spanner retention 上限屬 GCP 規格、兩者均隨版本演進、cross-verify 各自官方文件。</p></blockquote>
<h3 id="何時不用-change-streams">何時不用 change streams</h3>
<p>單純需要「下游讀到最新狀態、不在意中間每筆變更」、且主庫變更率低、定期 batch export 反而更簡單、不必引入 change stream + Dataflow 的運維成本。對延遲不敏感的分析、走 BigQuery federation 直接查 Spanner（見 sibling）比建 CDC 管線更省。Anti-recommendation 的判準是：若下游不需要「每一筆變更的順序」、只需要「定期最新快照」、CDC 是過度工程。</p>
<h3 id="sibling-deep-articles-路由">Sibling deep articles 路由</h3>
<ul>
<li><a href="../bigquery-federation/">bigquery-federation</a>：不想建 CDC 管線、直接 federated query 查 Spanner 的 OLAP 路徑、跟 change stream → BigQuery 是兩條互補的整合方式</li>
<li><a href="../truetime-api-depth/">truetime-api-depth</a>：change stream 的 commit timestamp 全序來自 TrueTime、理解順序保證的物理基礎</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：change stream 繼承 external consistency、跟 DynamoDB Streams 的 per-partition ordering 對照回 linearizability 定義</li>
</ul>
<h3 id="跟-knowledge-card-的互引">跟 knowledge card 的互引</h3>
<ul>
<li><a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change-data-capture</a> — 本文是這張卡的 Spanner 實作範例</li>
<li><a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external-consistency</a> — change stream 的全序保證來源</li>
</ul>
<h3 id="跟-04--09-章節的互引">跟 04 / 09 章節的互引</h3>
<ul>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：consumer lag × Spanner CPU 的 evidence pair</li>
<li><a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：change stream 讀取當額外 read workload 算進 sizing</li>
</ul>
]]></content:encoded></item><item><title>DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>B2B SaaS 跟客戶 SLA 寫 99.99%、單 region 跑了一年遇過兩次 region-level outage、合計 downtime 已逼近 SLA 上限。team 要把核心 table 改 Global Tables active-active、首問是「multi-region write 之後資料還會一致嗎」。這個問題的答案是：&lt;em>不會、但有工程解法&lt;/em>；DynamoDB Global Tables 用 LWW（Last Writer Wins）跨 region async 同步、conflict 偵測跟 reconciliation 要 application 自己加。&lt;/p>
&lt;p>但 Global Tables 不只是 conflict 痛點。Disney+ 用同一個機制處理 cross-device sync（手機看一半回家用電視繼續）、Genesys 用同一個機制做 15 region B2B 客服平台的 99.999% 可用性。本文先講正向 access pattern（避免讓讀者誤以為 Global Tables 只是「跨 region 寫入會 conflict、所以痛苦」）、再展開 conflict resolution 跟 reconciliation 設計。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Workload 適配本 vendor 才繼續&lt;/strong>：DynamoDB 4 軸判讀（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）軸見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。Global Tables 是 &lt;em>已選 DynamoDB 後&lt;/em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner / Cosmos DB strong consistency level、不是用 LWW 補。&lt;/p>&lt;/blockquote>
&lt;h2 id="b2b-saas-vs-b2c-業務-driver-對比">B2B SaaS vs B2C 業務 driver 對比&lt;/h2>
&lt;p>Global Tables 不是預設選擇、是 &lt;em>業務性質&lt;/em> 決定的工程投資。&lt;code>9.C24 Genesys&lt;/code> 揭露兩條關鍵 frame — 可用性目標的業務 driver、跟每多一個 9 的 cost 指數成長。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>業務性質&lt;/th>
 &lt;th>典型可用性目標&lt;/th>
 &lt;th>年停機容忍&lt;/th>
 &lt;th>Multi-region 投資邏輯&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>B2C 大型網站&lt;/td>
 &lt;td>99.9%&lt;/td>
 &lt;td>8.76 小時&lt;/td>
 &lt;td>通常單 region + PITR / cross-region backup 划算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B2B SaaS&lt;/td>
 &lt;td>99.95% 或 99.99%（合約）&lt;/td>
 &lt;td>4.4 小時 / 52.6 分鐘&lt;/td>
 &lt;td>合約義務、客戶 SLA 違約有金錢損失、ROI 正向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客服平台類&lt;/td>
 &lt;td>99.999%（合約客戶）&lt;/td>
 &lt;td>5.26 分鐘&lt;/td>
 &lt;td>客戶停線損失極大、15 region 投資合理（Genesys）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>B2C 大型網站&lt;/strong>通常 99.9% SLA、年停機 8.76 小時可接受、單 region + PITR + cross-region backup 是常見配置；改 Global Tables 邊際成本高、ROI 通常不正向。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>B2B SaaS 跟客戶 SLA 寫 99.99%、單 region 跑了一年遇過兩次 region-level outage、合計 downtime 已逼近 SLA 上限。team 要把核心 table 改 Global Tables active-active、首問是「multi-region write 之後資料還會一致嗎」。這個問題的答案是：<em>不會、但有工程解法</em>；DynamoDB Global Tables 用 LWW（Last Writer Wins）跨 region async 同步、conflict 偵測跟 reconciliation 要 application 自己加。</p>
<p>但 Global Tables 不只是 conflict 痛點。Disney+ 用同一個機制處理 cross-device sync（手機看一半回家用電視繼續）、Genesys 用同一個機制做 15 region B2B 客服平台的 99.999% 可用性。本文先講正向 access pattern（避免讓讀者誤以為 Global Tables 只是「跨 region 寫入會 conflict、所以痛苦」）、再展開 conflict resolution 跟 reconciliation 設計。</p>
<blockquote>
<p><strong>Workload 適配本 vendor 才繼續</strong>：DynamoDB 4 軸判讀（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）軸見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。Global Tables 是 <em>已選 DynamoDB 後</em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner / Cosmos DB strong consistency level、不是用 LWW 補。</p></blockquote>
<h2 id="b2b-saas-vs-b2c-業務-driver-對比">B2B SaaS vs B2C 業務 driver 對比</h2>
<p>Global Tables 不是預設選擇、是 <em>業務性質</em> 決定的工程投資。<code>9.C24 Genesys</code> 揭露兩條關鍵 frame — 可用性目標的業務 driver、跟每多一個 9 的 cost 指數成長。</p>
<table>
  <thead>
      <tr>
          <th>業務性質</th>
          <th>典型可用性目標</th>
          <th>年停機容忍</th>
          <th>Multi-region 投資邏輯</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B2C 大型網站</td>
          <td>99.9%</td>
          <td>8.76 小時</td>
          <td>通常單 region + PITR / cross-region backup 划算</td>
      </tr>
      <tr>
          <td>B2B SaaS</td>
          <td>99.95% 或 99.99%（合約）</td>
          <td>4.4 小時 / 52.6 分鐘</td>
          <td>合約義務、客戶 SLA 違約有金錢損失、ROI 正向</td>
      </tr>
      <tr>
          <td>客服平台類</td>
          <td>99.999%（合約客戶）</td>
          <td>5.26 分鐘</td>
          <td>客戶停線損失極大、15 region 投資合理（Genesys）</td>
      </tr>
  </tbody>
</table>
<p><strong>B2C 大型網站</strong>通常 99.9% SLA、年停機 8.76 小時可接受、單 region + PITR + cross-region backup 是常見配置；改 Global Tables 邊際成本高、ROI 通常不正向。</p>
<p><strong>B2B SaaS</strong> 99.95% 或 99.99% SLA 多半寫進合約、違約有具體金錢損失；Global Tables 的 N region cost 對比 SLA 違約成本通常 ROI 正向。critical 的是 <em>合約義務</em> 不是 <em>技術完美</em>。</p>
<p><strong>客服平台類</strong> 99.999% 是極端可用性目標、年停機 5.26 分鐘、Genesys 撐 8000+ orgs 的客服平台、客戶停線損失極大、跨 15 region 的 active-active 是合理投資。但 <em>不是每個 SaaS 都該追 99.999%</em>、是 <em>業務性質決定下限</em>。</p>
<p><strong>成本對比</strong>（<code>9.C24</code> 揭露）：15 region 成本約 = 1 region 的 15x（base table cost）+ 跨 region replication WCU。每多一個 9、容量規劃跟運維成本指數成長。</p>
<blockquote>
<p><strong>Scope warning（指標口徑紀律）</strong>：99.999% 是「12 個月滾動歷史值、不代表未來持續達成」（<code>9.C24</code> 警惕段第 1 條）。可用性是滾動指標、不是恆久承諾。引用 Genesys 99.999% 數字時要明示口徑（滾動 / customer-facing），不要寫成「DynamoDB 保證 99.999%」。</p></blockquote>
<h2 id="正向-access-pattern不只-conflict-議題">正向 access pattern：不只 conflict 議題</h2>
<p>Global Tables 不只是 DR / availability、也是正向 access pattern 的工程方案。先建立正向用例的判讀、再進 conflict 細節。</p>
<p><strong>Cross-device sync</strong>（<code>9.C27 Disney+</code> 揭露）：用戶在手機看到一半、晚上回家用電視繼續、播放進度跨裝置同步。Global Tables 自然解這個 access pattern — 用戶在不同 region 登入同帳號、寫入自動同步、最終一致性可接受場景。</p>
<p><strong>Global read（latency 優化）</strong>：跨地域用戶讀取就近 region 副本、latency 從 200ms 降到 &lt; 10ms。read 比 write 多很多倍的 workload（feed / catalog / user profile）受益最大。</p>
<p><strong>DR failover</strong>：region-level outage 時 application 切到 secondary region 繼續服務、RTO 通常 &lt; 5 分鐘（DNS / routing 切換時間、不含 application 端 reconnect）。</p>
<p><strong>B2C 也可能划算的場景</strong>：cross-device sync 是 <em>user-facing experience</em>、不是合規 / SLA driver。B2C 大規模平台（Disney+ / Spotify 類）也可能投資 Global Tables。判讀軸是「sync 體驗是否核心 UX」、不只「合約 SLA」。</p>
<h2 id="核心機制lww-conflict-resolution">核心機制：LWW conflict resolution</h2>
<p>Global Tables 的 first-class concept：</p>
<ul>
<li><strong>Multi-region active-active</strong>：每個 region 都能寫、async replication；typical replication latency &lt; 1s 但 <em>無 SLA</em></li>
<li><strong>LWW by wall clock</strong>：conflict 由 attribute <code>aws:rep:updatetime</code> 決定、純物理時間；不是 logical clock、不是 vector clock</li>
<li><strong>同 region read-your-write</strong>：本 region 寫立即可讀（同 region quorum 內）、其他 region 看到要等 replication</li>
<li><strong>Capacity 獨立</strong>：每個 region 自己的 RCU/WCU、<code>ReplicatedWriteCapacityUnits</code> 是跨 region replication 額外 WCU、按 region 數倍計</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>。</p>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 分類到 reconciliation pipeline 的 6 步流程。</p>
<h4 id="step-1access-pattern-分類">Step 1：access pattern 分類</h4>
<p>把 table 中的資料分兩類：</p>
<ul>
<li><strong>region-pinned data</strong>：user 主要 region（合規 / 地理 affinity）；不啟用 Global Tables、用 region-pinned cluster</li>
<li><strong>global data</strong>：跨 region read / cross-device sync；啟用 Global Tables</li>
</ul>
<p>不是所有 table 都該上 Global Tables；user profile 跨 region 同步、但用戶交易紀錄可能該 pin 在合規 region。</p>
<h4 id="step-2啟用-global-tables">Step 2：啟用 Global Tables</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">aws dynamodb update-table <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table-name orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --replica-updates <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;[{&#34;Create&#34;: {&#34;RegionName&#34;: &#34;us-east-1&#34;}}]&#39;</span></span></span></code></pre></div><p>加 region 後 vendor 自動 backfill；backfill 期間 capacity 雙倍（原 region + 新 region 同步流量）、要預留 capacity buffer。</p>
<h4 id="step-3application-寫入策略">Step 3：application 寫入策略</h4>
<p>兩種寫入策略：</p>
<ul>
<li><strong>home region write</strong>：每 user 固定一個 home region 寫、避免 conflict；user 跨 region 漫遊時透過 routing 仍寫 home</li>
<li><strong>nearest region write</strong>：latency 優先、user 寫就近 region；conflict 機率高、必須加 idempotency 跟 reconciliation</li>
</ul>
<p>選擇：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>寫入策略</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>user profile / 設定</td>
          <td>home region write</td>
          <td>conflict 少、簡單</td>
      </tr>
      <tr>
          <td>cross-device sync</td>
          <td>nearest region write</td>
          <td>用戶在不同裝置同時操作、容忍 LWW</td>
      </tr>
      <tr>
          <td>訂單 / 金流</td>
          <td>home region write</td>
          <td>業務不容許 conflict 損失</td>
      </tr>
  </tbody>
</table>
<h4 id="step-4idempotency-設計">Step 4：idempotency 設計</h4>
<p>每筆 write 加 <code>request_id</code> 或 <code>client_timestamp</code>、application 端去重：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">write_with_idempotency</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">request_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">Item</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ACTION#</span><span class="si">{</span><span class="n">action</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="s2">&#34;ts&#34;</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;request_id&#34;</span><span class="p">:</span> <span class="n">request_id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;attribute_not_exists(request_id)&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">)</span></span></span></code></pre></div><p><code>ConditionExpression</code> 在同一 region 內擋重複；跨 region eventual 仍可能 race，conflict 落到 LWW + reconciliation。</p>
<blockquote>
<p><strong>Scope warning（重要）</strong>：「加 request_id 或 client_timestamp」具體實作屬通用工程知識、<code>9.C26 PayPay</code> case 揭露「通知不可丟失」的需求分層、<em>沒有</em> 揭露具體 idempotency 實作。引用 PayPay 時要降溫成「PayPay 揭露需求分層（通知 vs 訊息）、idempotency 為通用工程實作」、不寫成「PayPay 使用 request_id」（陷阱 4：把通用工程實作寫成 case 揭露）。</p></blockquote>
<h4 id="step-5conflict-detection">Step 5：conflict detection</h4>
<p>DynamoDB Streams 訂閱、Lambda 比較 <code>aws:rep:updatetime</code> 跟 application timestamp、抓出可疑 conflict 進 reconciliation queue：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">detect_conflict</span><span class="p">(</span><span class="n">stream_event</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">new_image</span> <span class="o">=</span> <span class="n">stream_event</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;NewImage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">repl_time</span> <span class="o">=</span> <span class="n">new_image</span><span class="p">[</span><span class="s2">&#34;aws:rep:updatetime&#34;</span><span class="p">][</span><span class="s2">&#34;S&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">app_time</span> <span class="o">=</span> <span class="n">new_image</span><span class="p">[</span><span class="s2">&#34;client_timestamp&#34;</span><span class="p">][</span><span class="s2">&#34;S&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="n">parse</span><span class="p">(</span><span class="n">repl_time</span><span class="p">)</span> <span class="o">-</span> <span class="n">parse</span><span class="p">(</span><span class="n">app_time</span><span class="p">))</span> <span class="o">&gt;</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">seconds</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1"># 可疑 conflict、進 reconciliation</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">sqs</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">QueueUrl</span><span class="o">=</span><span class="n">RECONCILIATION_QUEUE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="n">MessageBody</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">stream_event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">)</span></span></span></code></pre></div><blockquote>
<p><strong>Scope warning</strong>：DynamoDB Streams 用法屬通用工程實作、<code>9.C26 PayPay</code> case <em>沒有</em> 明示用 Streams、引用時要分層（PayPay 揭露需求、Streams 是工程實作的標準解）。</p></blockquote>
<h4 id="step-6reconciliation-pipeline">Step 6：reconciliation pipeline</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">Conflict event → SQS queue → Lambda / human review → merge logic → write back</span></span></code></pre></div><p>merge logic 視業務而定：</p>
<ul>
<li>訂單金額 conflict：抓最大值（避免少收）</li>
<li>用戶設定 conflict：抓最新（user-facing 行為一致）</li>
<li>watchlist conflict：union（兩裝置加的都保留）</li>
</ul>
<p><strong>驗證點</strong>：DR drill 演 region outage、確認 secondary region 接管後 read / write 都正常；<code>ReplicationLatency</code> p99 &lt; 1s。</p>
<p><strong>Rollback boundary</strong>：region 可逐個移除、但 active-active 改 active-passive 期間 application 需配合路由切換；先 application 切再移 region、不可同時做。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>實際部署常見的 5 種失敗：</p>
<h4 id="case-1lww-默默吃掉-write">Case 1：LWW 默默吃掉 write</h4>
<p>跨 region 同一 record concurrent update、後到的 write 因 timestamp 較大蓋過先到的；business 看到「我送出的更新沒了」、稽核 log 才發現 conflict。修法：critical write 加 <code>ConditionExpression</code> 比較 <code>version</code> attribute、conflict 時 application 端 retry + merge；不要依賴 LWW 作為 conflict 解。</p>
<h4 id="case-2clock-skew-讓-lww-倒置">Case 2：Clock skew 讓 LWW 倒置</h4>
<p>region A 寫入 timestamp 因 NTP skew 比 region B 後寫快 200ms、結果舊資料贏。修法：依靠 application timestamp + monotonic counter、不依賴 server wall clock；critical write 用 conditional version + retry。</p>
<blockquote>
<p><strong>Scope warning</strong>：「200ms NTP skew」具體數字屬通用工程估算、case 未揭露具體 skew 範圍。</p></blockquote>
<h4 id="case-3replication-lag-撞-slo">Case 3：Replication lag 撞 SLO</h4>
<p>大 batch write 期間 replication lag 從 1s 變 30s、跨 region read 看到 30s 前資料、application 端 user 操作異常。修法：偵測 <code>ReplicationLatency</code> 升高時 application 端切 home region read、避免跨 region eventual read；把 replication lag 加進 SLO 監控、設 alarm。</p>
<h4 id="case-4dr-切換後-stale-data-持續-propagate">Case 4：DR 切換後 stale data 持續 propagate</h4>
<p>primary region outage 切到 secondary、舊 primary 恢復後仍把 outdated data 推回去、覆蓋 secondary 期間的新寫入。修法：DR runbook 含「舊 primary 恢復後人工 reconciliation 或重建」step、不可全自動 catch-up；舊 primary 恢復前先確認 replication 方向是「從 secondary catch up」而非「推舊資料回 secondary」。</p>
<h4 id="case-5跨-region-transaction-失敗">Case 5：跨 region transaction 失敗</h4>
<p>application 試圖跨 region <code>TransactWriteItems</code>、API 不支援跨 region transaction、原子性破裂。修法：transaction 限同 region 內、跨 region 用 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a> + idempotent + reconciliation；不要把同 region 的 transaction 假設搬到跨 region。</p>
<p><strong>Anti-recommendation</strong>：single-region availability 已達 99.95% + RTO 可接受 1 小時 + 預算敏感（特別 B2C 場景）→ 用 PITR + 跨 region backup 而非 Global Tables；Global Tables cost = N × single region cost 不止（對應 B2B vs B2C driver 對比）。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ReplicationLatency</code>：p99 通常 &lt; 1s、建議 SLO 設 5s alarm</li>
<li><code>PendingReplicationCount</code>：積壓量、batch write 期間會升高</li>
<li><code>ReplicatedWriteCapacityUnits</code>：跨 region replication 額外 WCU、按 region 數倍計</li>
</ul>
<p>DynamoDB Streams + Lambda：抓 conflict event、寫進獨立 audit table；reconciliation job 從 audit table 跑、不直接動 base table。</p>
<p><strong>Region-level dashboard</strong>：每個 region 獨立 capacity / latency / error rate panel；DR drill 看是否能在 RTO 內切換。</p>
<p><strong>Cost monitoring</strong>：</p>
<ul>
<li>Global Tables cost ≈ N region × base cost + replication WCU</li>
<li>4 region 成本約 4.5x single region；15 region（Genesys 規模）約 15x</li>
<li>每多一個 region 都要重新算 ROI（軸 6 vendor crossover 的延伸）</li>
</ul>
<p><strong>指標口徑紀律</strong>（重要）：99.99% / 99.999% SLA 是 <em>滾動指標 + 歷史值</em>、不是永久承諾；引用 Genesys 99.999% 時明示「12 個月滾動 / customer-facing」、不寫成「DynamoDB 保證 99.999%」。</p>
<p>接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>、<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-5region-pinned-global-tables-吸收合規邊界">Frame 5：region-pinned Global Tables 吸收合規邊界</h3>
<p>Global Tables 不只是高可用工具、也是 <em>合規邊界</em>（<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 拓樸）的吸收層。DynamoDB 在 vendor capability 層級支援 <em>region-pinned replication</em> — 每張 table 可獨立決定哪些 region 參與 replication group、部分 region 可不加入。這個 capability 同時服務三類場景：合規分離（受監管市場資料不跨境）、cost / latency 取捨（資料只在主要服務 region 同步）、災備拓樸（少數 region 純讀備援）。<code>9.C24 Genesys</code> 15 region 揭露的是 <em>延遲就近接入</em> 的 B2B SaaS 拓樸（客戶服務延遲敏感、必須在客戶所在地有 region）— case 原文沒明示合規應用、但 region-pinned capability 在 Genesys 規模下天然能容納合規市場分離、是同 capability 的 <em>可能應用維度</em>、不是 case 已驗證的具體實踐。</p>
<p>跨 vendor 對照：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>合規吸收機制</th>
          <th>拓樸特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DynamoDB</td>
          <td>region-pinned Global Tables（按 region 開關 replication、各市場可分離）</td>
          <td>仍是 active-active、但 replication 範圍可控</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>fleet 拓樸（每市場獨立 cluster、合規禁止跨境 = Global Database 反指標）</td>
          <td>active-passive per market、跨市場不複製</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>locality + placement（邏輯一個 cluster + region pinning + Outposts）</td>
          <td>單 logical cluster、physical row 鎖在合規 region</td>
      </tr>
      <tr>
          <td>MongoDB / Cosmos DB</td>
          <td>cluster-per-region（無 row-level locality 等價物、整 cluster 切割）</td>
          <td>各 region 獨立 cluster、application 層做市場 routing</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼 DynamoDB 在這個 frame 退化得最輕</strong>：Global Tables 的 region 開關是 <em>attribute 級</em> 設計（每張 table 可獨立決定哪些 region 參與）、不像 Aurora 必須整 cluster 拆。讀者要把「跨境合規 + 高可用」雙重需求兼顧時、DynamoDB 是最少結構性改造的路徑 — 但代價是 LWW conflict 跟 reconciliation 設計仍要自己做。</p>
<p><strong>何時 region-pinned 而非 active-active</strong>：受監管金融 / 個資跨境禁止的市場（如 GDPR strict 條款區、中國個資法 PIPL、巴西 LGPD）— 該 region 仍開 DynamoDB table、但 <em>不加入 Global Tables replication group</em>、跟其他 region 完全切割。capability 設計上支援這種按 region 開關 replication 的拓樸；具體是否套用、要看 <em>讀者自己的市場合規清單</em>、不是把 Genesys 規模當必然證據（Genesys case 揭露的是延遲就近接入、未明示合規分離實踐）。</p>
<h3 id="disney-vs-genesys兩種-global-tables-工程動機">Disney+ vs Genesys：兩種 Global Tables 工程動機</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C24 Genesys</code> 是 Global Tables 兩種不同的工程動機：</p>
<ul>
<li><strong>Disney+</strong>：cross-device sync 是 user-facing UX、watchlist + 播放進度跨裝置同步、B2C 但 sync 是 core experience</li>
<li><strong>Genesys</strong>：99.999% B2B SaaS 合約義務、15 region active-active、客服平台停線損失極大</li>
</ul>
<p>兩個 case 都用 Global Tables、但動機完全不同 — Disney+ 是 UX driver、Genesys 是合約 driver。寫進你自己的設計時要明示自己屬哪一型，因為兩種型別的 cost 容忍度跟 conflict 容忍度完全不同。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — 同 region eventual / strong 取捨、本篇是跨 region 延伸</li>
<li><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">on-demand-vs-provisioned</a> — 多 region capacity 規劃放大、軸 5 工時釋放在 multi-region 更顯著</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — hot partition 跨 region 同樣存在、每個 region 的 partition 都要均勻</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — single-table 設計在 multi-region 仍適用、access pattern 反推 PK/SK 不變</li>
<li>替代路由：global strong consistency 必要 → Spanner / Cosmos DB strong consistency level</li>
<li>Migration playbook：single-region → Global Tables 屬 topology re-layout、對應 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook methodology</a> Type F</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 9.C24</a> 互引：15 region 5 個 9 可用性的工程實踐 + B2B SaaS 業務 driver</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 9.C27</a> 互引：cross-device sync 作為正向 access pattern</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 9.C26</a> 互引：揭露需求分層（通知 vs 訊息）、idempotency / Streams 為通用工程實作、PayPay 未公開揭露具體實作</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Aggregation Pipeline Optimization：stage 順序、index 配合與 memory 邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/aggregation-pipeline-optimization/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/aggregation-pipeline-optimization/</guid><description>&lt;p>MongoDB aggregation pipeline 是 document model 做 analytical query 的主要介面、stage stream 設計直觀但 production 容易踩雷 — 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。Aggregation pipeline 的最佳化跟 RDBMS 的 SQL planner 完全不同邏輯 — RDBMS 靠 planner 自動重排 join / filter、MongoDB 靠寫 query 的人手動排 stage 順序。本文把 stage 機制、index 配合、memory 邊界、cross-shard 限制講清楚、並對「report dashboard 跑爆 primary」這個常見 anti-pattern 給治理路徑。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 aggregation 簡介 — 而是 production tuning + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>前置閱讀&lt;/strong>：MongoDB workload 適配判讀（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>。本文聚焦 aggregation pipeline 操作層、是 &lt;em>已選 MongoDB 後&lt;/em> 的 query 層工程議題、不重複前置判讀。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境aggregation-是-hot-path-的反模式">問題情境：aggregation 是 hot path 的反模式&lt;/h2>
&lt;p>典型觸發場景：報表 pipeline 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。&lt;/p>
&lt;p>進一步徵兆：&lt;/p>
&lt;ul>
&lt;li>「OLTP collection 上跑 analytical query」的混合 workload：把 &lt;code>$group + $lookup + $sort&lt;/code> 接成長 pipeline、aggregation 把整個 working set 從 cache 擠走&lt;/li>
&lt;li>Sharded cluster 上跑 cross-shard aggregation：&lt;code>$group&lt;/code> / &lt;code>$sort&lt;/code> 必須在 mongos 合併、mongos 變單點瓶頸&lt;/li>
&lt;li>&lt;code>$lookup&lt;/code> 出現在 hot path：每筆 input doc 都要去另一個 collection 查、嚴格意義上是 N+1&lt;/li>
&lt;li>&lt;code>db.serverStatus().metrics.aggStageCounters&lt;/code> 飆、&lt;code>executionStats.executionTimeMillis&lt;/code> 跟 doc 數線性增長&lt;/li>
&lt;li>Profiler 報 &lt;code>usedDisk: true&lt;/code>、aggregation OOM kill &lt;code>QueryExceededMemoryLimitNoDiskUseAllowed&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Case anchor：report dashboard 跑爆 primary 的具體 incident 細節需未來 case 補完、本文以「常見 anti-pattern」處理、不憑空編造 incident 數字。側面引用 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> — 從 MongoDB 把 analytics 分離出來的 driver。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB aggregation pipeline 是 document model 做 analytical query 的主要介面、stage stream 設計直觀但 production 容易踩雷 — 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。Aggregation pipeline 的最佳化跟 RDBMS 的 SQL planner 完全不同邏輯 — RDBMS 靠 planner 自動重排 join / filter、MongoDB 靠寫 query 的人手動排 stage 順序。本文把 stage 機制、index 配合、memory 邊界、cross-shard 限制講清楚、並對「report dashboard 跑爆 primary」這個常見 anti-pattern 給治理路徑。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 aggregation 簡介 — 而是 production tuning + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>前置閱讀</strong>：MongoDB workload 適配判讀（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>。本文聚焦 aggregation pipeline 操作層、是 <em>已選 MongoDB 後</em> 的 query 層工程議題、不重複前置判讀。</p></blockquote>
<h2 id="問題情境aggregation-是-hot-path-的反模式">問題情境：aggregation 是 hot path 的反模式</h2>
<p>典型觸發場景：報表 pipeline 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。</p>
<p>進一步徵兆：</p>
<ul>
<li>「OLTP collection 上跑 analytical query」的混合 workload：把 <code>$group + $lookup + $sort</code> 接成長 pipeline、aggregation 把整個 working set 從 cache 擠走</li>
<li>Sharded cluster 上跑 cross-shard aggregation：<code>$group</code> / <code>$sort</code> 必須在 mongos 合併、mongos 變單點瓶頸</li>
<li><code>$lookup</code> 出現在 hot path：每筆 input doc 都要去另一個 collection 查、嚴格意義上是 N+1</li>
<li><code>db.serverStatus().metrics.aggStageCounters</code> 飆、<code>executionStats.executionTimeMillis</code> 跟 doc 數線性增長</li>
<li>Profiler 報 <code>usedDisk: true</code>、aggregation OOM kill <code>QueryExceededMemoryLimitNoDiskUseAllowed</code></li>
</ul>
<p>Case anchor：report dashboard 跑爆 primary 的具體 incident 細節需未來 case 補完、本文以「常見 anti-pattern」處理、不憑空編造 incident 數字。側面引用 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 把 analytics 分離出來的 driver。</p>
<h2 id="核心機制">核心機制</h2>
<p>Aggregation pipeline 是 stage 序列：每個 stage 接 stream of document、產出 stream of document。Stage 順序直接決定後續 stage 處理量 — 第一個 stage 是 IXSCAN 還是 COLLSCAN、<code>$match</code> 推到前面還是後面、<code>$project</code> 早 drop 還是晚 drop、都會放大或縮小後續 cost。</p>
<p><strong>Optimizer rewrite</strong>：MongoDB 會自動把 <code>$match</code> / <code>$project</code> 往前推、把 <code>$sort + $limit</code> 合併成 top-K、但不保證所有 case。用 <code>explain(&quot;executionStats&quot;)</code> 看 rewrite 後的 effective pipeline、不要靠原始 pipeline 推斷實際執行順序。</p>
<p><strong>Index 配合</strong>：pipeline 的 <em>第一個 stage</em> 若是 <code>$match</code> 或 <code>$sort</code>、且能對到 index、就走 IXSCAN。中間 stage 都是 in-memory stream、沒 index 概念。所以 <code>$match</code> 永遠該排第一、配合對應 index。</p>
<p><strong>Memory 邊界</strong>：每個 aggregation stage 預設 100MB memory 上限、超過要 <code>allowDiskUse: true</code>（4.2+ 是預設）。Disk spill 啟動後 IO 嚴重拖慢、aggregation 變慢 50-100x。</p>
<p><strong><code>$lookup</code> 在 sharded cluster</strong>：foreign collection 不能 sharded（5.0 前完全不行、5.0+ 有限放寬）；<code>$lookup</code> 本質是 nested loop join、沒 hash join / merge join — 對大 collection 不可用。</p>
<p><strong><code>$facet</code> 平行多 pipeline</strong>：但所有 facet 共享同一個 100MB 限制、複雜 facet 容易撞 memory ceiling。</p>
<p><strong><code>$merge</code> / <code>$out</code></strong>：把結果寫回 collection（pre-computed view / materialized view）— 把 hot analytical query 移出 read path、是治理 anti-pattern 的主要工具。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>（aggregation 集中讀單 shard 的副作用）、<a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>（從 secondary 跑 aggregation 的 trade-off）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 0：把壞 pipeline 跟好 pipeline 並排</strong>。看一個簡化但典型的優化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 壞：lookup 在 match 前、sort 沒 limit、project 在最後
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">{</span> <span class="nx">$lookup</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;users&#34;</span><span class="p">,</span> <span class="nx">localField</span><span class="o">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span> <span class="nx">foreignField</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="nx">as</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="s2">&#34;completed&#34;</span><span class="p">,</span> <span class="s2">&#34;user.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">{</span> <span class="nx">$sort</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">{</span> <span class="nx">$project</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">])</span>
</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="c1">// 好：可推前的 match 寫前面、sort + limit 配對、project 早寫
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="s2">&#34;completed&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">{</span> <span class="nx">$sort</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">{</span> <span class="nx">$limit</span><span class="o">:</span> <span class="mi">100</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">{</span> <span class="nx">$lookup</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;users&#34;</span><span class="p">,</span> <span class="nx">localField</span><span class="o">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span> <span class="nx">foreignField</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="nx">as</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;user.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">{</span> <span class="nx">$project</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;user.name&#34;</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>差別：壞 pipeline 對整個 orders 做 lookup、然後才過濾；好 pipeline 先過濾 + top-100、只對 100 筆做 lookup、再過濾 lookup 結果。實際 collection 大時兩者差 50-100x。</p>
<p><strong>Step 1：拿 explain plan</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">coll</span><span class="p">.</span><span class="nx">explain</span><span class="p">(</span><span class="s2">&#34;executionStats&#34;</span><span class="p">).</span><span class="nx">aggregate</span><span class="p">([...])</span></span></span></code></pre></div><p>看 <code>stages[]</code> 顯示 rewrite 後的 effective pipeline、<code>executionTimeMillis</code>、<code>totalDocsExamined / totalDocsReturned</code> 比值、是否 <code>usedDisk</code>。</p>
<p><strong>Step 2：把 <code>$match</code> 推到最前</strong>。越早過濾、後續 stage 處理量越小。Optimizer 通常自己會推、但 <code>$lookup</code> 之後的 <code>$match</code> 不會自動推到 <code>$lookup</code> 之前 — 因為 lookup 出的欄位才能被那個 match 用、邏輯依賴。寫 query 時就把能推前的 <code>$match</code> 寫前面。</p>
<p><strong>Step 3：對 <code>$match</code> 欄位建 compound index</strong>。確保 <code>executionStages</code> 顯示 <code>IXSCAN</code> 而不是 <code>COLLSCAN</code>。Compound index 順序敏感 — <code>{ status: 1, createdAt: -1 }</code> 對 <code>{ status: ..., createdAt: $gte: ... }</code> 高效、對 <code>{ createdAt: $gte: ... }</code> 走不到 index。</p>
<p><strong>Step 4：<code>$sort + $limit</code> 寫在一起</strong>。Optimizer 才會推 top-K（不需要 full sort、只需要 heap）。單 <code>$sort</code> 不限 limit 會做 full sort、容易撞 memory。</p>
<p><strong>Step 5：<code>$project</code> 早寫</strong>。把不需要的欄位早期 drop、減少後續 stage 處理 doc size。對大 document 特別有效。</p>
<p><strong>Step 6：把 hot analytical pipeline 寫成 materialized view</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$gte</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&#34;2026-05-01&#34;</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">{</span> <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;$customerId&#34;</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="s2">&#34;$amount&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">{</span> <span class="nx">$merge</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">into</span><span class="o">:</span> <span class="s2">&#34;monthly_customer_summary&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">on</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">whenMatched</span><span class="o">:</span> <span class="s2">&#34;merge&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">whenNotMatched</span><span class="o">:</span> <span class="s2">&#34;insert&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>定時更新（cron / 5 分鐘一次）、application 讀 materialized view 而不是即時跑 aggregation。</p>
<p><strong>Step 7：sharded cluster 處理</strong>。避免在 hot path 用 cross-shard <code>$lookup</code> / <code>$group</code>、或把這類 query 路由到 analytical replica（用 tag set + read preference）、見 <a href="../replica-set-read-preference/">replica set read preference</a>。</p>
<p>驗證點：</p>
<ul>
<li><code>executionTimeMillis</code> 在預期 budget 內</li>
<li><code>totalDocsExamined / totalDocsReturned</code> 比值接近 1（過濾效率高）</li>
<li>無 <code>usedDisk: true</code></li>
<li>無 stage 看到 <code>inMemory &gt; 50MB</code></li>
</ul>
<p>Rollback boundary：pipeline 改寫是 application code 變更、可以灰度；materialized view（<code>$merge</code>）需備份 target collection 才能還原。</p>
<h3 id="典型-tuning-過程200ms--8s--250ms">典型 tuning 過程（200ms → 8s → 250ms）</h3>
<p>一個常見的 production pipeline 演化路徑：</p>
<ol>
<li><strong>上線時 200ms</strong>：collection 100K doc、<code>$match</code> 過濾 95%、<code>$lookup</code> 只跑 5K 次、in-memory <code>$sort</code> 處理 5K row 在 100MB 內</li>
<li><strong>半年後 8s</strong>：collection 長到 2M doc、<code>$match</code> 仍過濾 95% 但變 100K row、<code>$lookup</code> 跑 100K 次（5K → 100K 是 20x）、<code>$sort</code> 在 in-memory 撞 100MB 開始 disk spill、IO 100x 退化</li>
<li><strong>加 compound index 沒用</strong>：index 是給 <code>$match</code> 用的、但 <code>$match</code> 之後的 stage（<code>$lookup</code> / <code>$sort</code>）走的是 in-memory pipeline、index 救不了</li>
<li><strong>修法到 250ms</strong>：(a) <code>$sort + $limit</code> 配對讓 optimizer 走 top-K、避免 full sort (b) 改 schema embed 把 <code>$lookup</code> 拿掉（見 <a href="../schema-design-pattern/">schema design pattern</a>）(c) hot pipeline 寫成 <code>$merge</code> materialized view、application 讀 view 不跑 aggregation</li>
</ol>
<p>關鍵教訓：aggregation 慢的原因不在 query 本身、在 <em>資料形狀演進</em>。Index 是 hot path 的第一個槓桿、但只對 <code>$match</code> / <code>$sort</code> 第一 stage 有效；後續 stage 要靠 stage 順序、materialized view、schema denormalize 來救。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong><code>$lookup</code> 在 hot path</strong>：list page 每行去另一 collection 查、p99 隨 page size 線性增。應在 schema design 階段 denormalize、把 read-together 資料 embed 回 aggregate root（見 <a href="../schema-design-pattern/">schema design pattern</a>）。</p>
<p><strong><code>$sort</code> 不帶 limit + 沒 index</strong>：全表 in-memory sort、撞 100MB 限制 → OOM 或 disk spill。<code>allowDiskUse: true</code> 解 OOM 但 IO 100x 退化。修法是建對應 index 走 IXSCAN sort、或限 limit 走 top-K。</p>
<p><strong>Sharded cluster cross-shard aggregation</strong>：<code>$group</code> 階段所有 partial result 跑到 mongos 合併、mongos memory + CPU 爆。修法是 group key 包含 shard key prefix（讓 group 在 shard 內完成）、或路由到 analytical replica 跑。</p>
<p><strong>Stage 順序錯</strong>：<code>$lookup</code> 放在 <code>$match</code> 前、等於對全表都做 lookup 再過濾、每個 input doc 都觸發 lookup。<code>$match</code> 永遠該排第一。</p>
<p><strong>Aggregation 把 working set 擠走</strong>：OLTP 的 hot page 被 aggregation 的 cold scan 擠出 cache、整體 query latency 一起退化。修法是 analytical workload 跟 OLTP read 隔離（read preference tag）、或搬走 analytical（見下面 anti-recommendation）。</p>
<p><strong><code>$facet</code> 滿載</strong>：四個 facet 各跑大 pipeline、共享 100MB 限制立刻爆。修法是拆成獨立 query、不要硬塞 facet。</p>
<p>Anti-recommendation：</p>
<ul>
<li><strong>報表 / BI / analytics workload 跑 MongoDB primary 是反模式</strong>：應該 (a) 設定 analytical secondary + read preference tag (b) 用 <code>$merge</code> 寫到 reporting collection (c) 進階用 BI Connector / data lake / 把 analytical workload 整批搬到 <a href="https://clickhouse.com">ClickHouse</a> / BigQuery</li>
<li><strong>「report dashboard 跑爆 primary」典型 anti-pattern</strong>：BI 工具直連 MongoDB primary 跑長 pipeline、cache eviction 把 OLTP working set 擠走、p99 latency 在報表時段集體升。沒拿到具體 incident 數字、不在本文編造、改寫成「常見 anti-pattern」並推到治理路徑</li>
<li><strong>Aggregation 不能解 read scaling</strong>：aggregation 是 OLTP 的補位、不是 read scaling 的主路。Read scaling 在大規模 OLTP 走 cache + freshness token（見 <a href="../connection-management-and-cache-layer/">connection management and cache layer</a>）、不是把 aggregation 跑爆 secondary</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li>Aggregation operation time 分布</li>
<li>Disk spill 次數</li>
<li><code>opcounters.command</code> 中 aggregate 比例</li>
<li>Cache eviction rate 在 aggregation 高峰時的變化</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.currentOp({ &quot;command.aggregate&quot;: { $exists: true } })</code>：當前 aggregation 在跑</li>
<li><code>db.serverStatus().metrics.aggStageCounters</code>：stage 級別 counter</li>
<li><code>explain(&quot;executionStats&quot;)</code>：單 query 詳細分析</li>
</ul>
<p>Profiler：<code>db.setProfilingLevel(1, {slowms: 200})</code>、看 <code>usedDisk</code> flag 跟 <code>numYield</code>。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：aggregation slow log + cache hit ratio + disk spill rate 是「analytical 壓力」的 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：用 explain executionStats 把 pipeline stage 對到瓶頸（IXSCAN 還是 COLLSCAN、in-memory 還是 disk spill、shard-local 還是 mongos merge）。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../schema-design-pattern/">schema design pattern</a> — embedded 設計可消除大部分 <code>$lookup</code></li>
<li><a href="../shard-key-selection/">shard key selection</a> — 決定 aggregation 是 shard-local 還是 cross-shard</li>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — aggregation 跑 secondary 的 stale read trade-off</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — report dashboard 跑爆 primary 時的 cache + read scaling 主路</li>
</ul>
<p>Migration playbook：analytical workload 大到不能繼續混在 MongoDB → split 出 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">→ Cosmos DB MongoDB API + Synapse</a> 或 <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 + Athena/Glue</a>（access pattern 重設計）。</p>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 把 aggregation 列為 read-shape 的成本維度；<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 處理「OLTP + analytical 同 cluster」的反模式。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「aggregation pipeline optimization」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/aggregation/">Aggregation Pipeline</a>、<a href="https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/">Optimize Pipelines</a>、<a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/">$merge</a></li>
</ul>
]]></content:encoded></item><item><title>3.C34 Netlify：NATS 當全球 metrics/logs 統一資料平面</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-netlify-data-plane-fanout/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-netlify-data-plane-fanout/</guid><description>&lt;p>Netlify 的 NATS 選型示範了 subject-based fan-out 在跨雲觀測資料平面的優勢 — 協議極簡帶來的是部署簡單跟 client 整合成本低，代價是放棄持久化保證。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Netlify 是靜態網站跟 serverless function 的部署平台，服務 70,000+ 網站、近月 10 億 page view。基礎設施橫跨 Rackspace、AWS、GCP、Digital Ocean 四個雲端供應商。每個服務節點都會產生 metrics 跟 logs，需要一條統一的資料路徑把這些訊號從各地收集到中央觀測系統。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="跨雲統一資料平面">跨雲統一資料平面&lt;/h3>
&lt;p>四個雲的服務各自有不同的網路拓樸跟存取方式。觀測資料需要跨雲收集到同一個目的地（Elasticsearch），但直接讓每個服務 HTTP POST 到 Elasticsearch 會有連線管理、背壓、格式轉換的問題分散在每個服務裡。&lt;/p>
&lt;p>Netlify 需要一個中介層 — 各服務把 metrics / logs 推到中介層，中介層負責 fan-out 到下游消費者（Elasticsearch、即時 dashboard、告警系統）。&lt;/p>
&lt;h3 id="選型nats-vs-rabbitmq">選型：NATS vs RabbitMQ&lt;/h3>
&lt;p>Netlify 評估了 RabbitMQ 跟 NATS。RabbitMQ 在功能上更完整（持久化 queue、DLQ、ack 機制），但 Netlify 的觀測資料場景有三個特性讓 NATS 更合適：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>資料可丟&lt;/strong>：metrics 跟 logs 是 best-effort 的觀測資料，遺失幾秒的資料不影響業務 — 持久化保證帶來的運維成本大於收益&lt;/li>
&lt;li>&lt;strong>Fan-out 是主要模式&lt;/strong>：同一份資料要被多個消費者訂閱（Elasticsearch、即時 tail、告警），NATS 的 subject-based pub/sub 天然支援，RabbitMQ 需要設 exchange + 多個 binding&lt;/li>
&lt;li>&lt;strong>部署極簡&lt;/strong>：NATS server 是單一 binary、零依賴、幾秒鐘啟動，跨四個雲部署時每個雲跑一個 NATS node 的運維成本遠低於 RabbitMQ cluster&lt;/li>
&lt;/ul>
&lt;h2 id="解法與取捨">解法與取捨&lt;/h2>
&lt;h3 id="架構">架構&lt;/h3>
&lt;p>Netlify 用 Core NATS（非 JetStream）搭建觀測資料平面：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Producer 端&lt;/strong>：用 logrus 的 NATS hook 讓所有 Go 服務的 structured log 自動推到 NATS subject；另用 log-tail 工具從 file-based log 讀取推送&lt;/li>
&lt;li>&lt;strong>Consumer 端&lt;/strong>：一個 elastinats 消費者訂閱 NATS subject、批次寫入 Elasticsearch；其他消費者可以各自訂閱同一個 subject 做即時處理&lt;/li>
&lt;/ul>
&lt;p>Subject 的命名用階層式結構（例如 &lt;code>logs.production.api&lt;/code>），讓消費者可以用 wildcard 訂閱整個子樹（&lt;code>logs.production.*&lt;/code>）或特定服務。&lt;/p>
&lt;h3 id="取捨">取捨&lt;/h3>
&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>持久化&lt;/td>
 &lt;td>放棄（Core NATS）&lt;/td>
 &lt;td>NATS server 重啟時 in-flight 的訊息遺失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ack 機制&lt;/td>
 &lt;td>放棄（fire-and-forget）&lt;/td>
 &lt;td>Consumer 處理失敗的訊息不會被重送&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨雲連接&lt;/td>
 &lt;td>NATS cluster&lt;/td>
 &lt;td>需要跨雲的網路連線、延遲影響 cluster 一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consumer 擴展&lt;/td>
 &lt;td>多個訂閱者各自訂閱&lt;/td>
 &lt;td>每個消費者收到全量資料、沒有 consumer group 的分攤機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Core NATS 的 fire-and-forget 語意在觀測資料場景是有意的選擇 — 觀測資料的價值隨時間快速衰減，遺失一秒鐘的 metrics 不影響趨勢判讀。如果場景需要持久化（例：audit log、交易事件），Core NATS 就不適合，需要 JetStream 或其他有持久化保證的 broker。&lt;/p>
&lt;h2 id="回寫教材的連結">回寫教材的連結&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a>：Core NATS 的 fire-and-forget 是 broker 可靠性光譜的一端（at-most-once），Kafka 跟 RabbitMQ 在另一端（at-least-once / durable）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a>：Core NATS vs JetStream 的選型判準 — 本案例是純 Core NATS 的代表場景&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/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&lt;/a>：Netlify 的 NATS 資料平面在觀測 pipeline 架構中扮演 collector 跟 storage 之間的 transport 層&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>讀者在自己的系統看到以下訊號時，應該回讀本案例：&lt;/p></description><content:encoded><![CDATA[<p>Netlify 的 NATS 選型示範了 subject-based fan-out 在跨雲觀測資料平面的優勢 — 協議極簡帶來的是部署簡單跟 client 整合成本低，代價是放棄持久化保證。</p>
<h2 id="業務背景">業務背景</h2>
<p>Netlify 是靜態網站跟 serverless function 的部署平台，服務 70,000+ 網站、近月 10 億 page view。基礎設施橫跨 Rackspace、AWS、GCP、Digital Ocean 四個雲端供應商。每個服務節點都會產生 metrics 跟 logs，需要一條統一的資料路徑把這些訊號從各地收集到中央觀測系統。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="跨雲統一資料平面">跨雲統一資料平面</h3>
<p>四個雲的服務各自有不同的網路拓樸跟存取方式。觀測資料需要跨雲收集到同一個目的地（Elasticsearch），但直接讓每個服務 HTTP POST 到 Elasticsearch 會有連線管理、背壓、格式轉換的問題分散在每個服務裡。</p>
<p>Netlify 需要一個中介層 — 各服務把 metrics / logs 推到中介層，中介層負責 fan-out 到下游消費者（Elasticsearch、即時 dashboard、告警系統）。</p>
<h3 id="選型nats-vs-rabbitmq">選型：NATS vs RabbitMQ</h3>
<p>Netlify 評估了 RabbitMQ 跟 NATS。RabbitMQ 在功能上更完整（持久化 queue、DLQ、ack 機制），但 Netlify 的觀測資料場景有三個特性讓 NATS 更合適：</p>
<ul>
<li><strong>資料可丟</strong>：metrics 跟 logs 是 best-effort 的觀測資料，遺失幾秒的資料不影響業務 — 持久化保證帶來的運維成本大於收益</li>
<li><strong>Fan-out 是主要模式</strong>：同一份資料要被多個消費者訂閱（Elasticsearch、即時 tail、告警），NATS 的 subject-based pub/sub 天然支援，RabbitMQ 需要設 exchange + 多個 binding</li>
<li><strong>部署極簡</strong>：NATS server 是單一 binary、零依賴、幾秒鐘啟動，跨四個雲部署時每個雲跑一個 NATS node 的運維成本遠低於 RabbitMQ cluster</li>
</ul>
<h2 id="解法與取捨">解法與取捨</h2>
<h3 id="架構">架構</h3>
<p>Netlify 用 Core NATS（非 JetStream）搭建觀測資料平面：</p>
<ul>
<li><strong>Producer 端</strong>：用 logrus 的 NATS hook 讓所有 Go 服務的 structured log 自動推到 NATS subject；另用 log-tail 工具從 file-based log 讀取推送</li>
<li><strong>Consumer 端</strong>：一個 elastinats 消費者訂閱 NATS subject、批次寫入 Elasticsearch；其他消費者可以各自訂閱同一個 subject 做即時處理</li>
</ul>
<p>Subject 的命名用階層式結構（例如 <code>logs.production.api</code>），讓消費者可以用 wildcard 訂閱整個子樹（<code>logs.production.*</code>）或特定服務。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>選擇</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>持久化</td>
          <td>放棄（Core NATS）</td>
          <td>NATS server 重啟時 in-flight 的訊息遺失</td>
      </tr>
      <tr>
          <td>Ack 機制</td>
          <td>放棄（fire-and-forget）</td>
          <td>Consumer 處理失敗的訊息不會被重送</td>
      </tr>
      <tr>
          <td>跨雲連接</td>
          <td>NATS cluster</td>
          <td>需要跨雲的網路連線、延遲影響 cluster 一致性</td>
      </tr>
      <tr>
          <td>Consumer 擴展</td>
          <td>多個訂閱者各自訂閱</td>
          <td>每個消費者收到全量資料、沒有 consumer group 的分攤機制</td>
      </tr>
  </tbody>
</table>
<p>Core NATS 的 fire-and-forget 語意在觀測資料場景是有意的選擇 — 觀測資料的價值隨時間快速衰減，遺失一秒鐘的 metrics 不影響趨勢判讀。如果場景需要持久化（例：audit log、交易事件），Core NATS 就不適合，需要 JetStream 或其他有持久化保證的 broker。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：Core NATS 的 fire-and-forget 是 broker 可靠性光譜的一端（at-most-once），Kafka 跟 RabbitMQ 在另一端（at-least-once / durable）</li>
<li><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a>：Core NATS vs JetStream 的選型判準 — 本案例是純 Core NATS 的代表場景</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>：Netlify 的 NATS 資料平面在觀測 pipeline 架構中扮演 collector 跟 storage 之間的 transport 層</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>觀測資料（metrics / logs）需要跨多個雲或多個 datacenter 收集到中央系統</li>
<li>現有的 broker（RabbitMQ / Kafka）在觀測資料場景的運維成本跟資料價值不成比例</li>
<li>Fan-out 是主要消費模式 — 同一份資料需要被多個下游系統訂閱</li>
<li>對 message delivery 的可靠性要求是 best-effort 而非 at-least-once</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://nats.io/blog/netlify-nats-blog/">Why Netlify chose NATS</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Logical Replication + Debezium CDC：replication slot × failure × recovery 對照</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 提到 logical decoding / Debezium CDC、本文聚焦 &lt;em>replication slot 生命週期 + 5 個 production failure mode 跟 recovery&lt;/em> 的對照。&lt;/p>&lt;/blockquote>
&lt;h2 id="replication-slot--failure--recovery-對照">Replication slot × Failure × Recovery 對照&lt;/h2>
&lt;p>Logical replication 跟 Debezium CDC 的 production 議題集中在 &lt;em>replication slot&lt;/em> — 它是 PostgreSQL 內保證 WAL 不被回收的 anchor point；slot 設不對、整個 CDC pipeline 失效。各 failure mode 對 slot 的影響跟 recovery 路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Failure mode&lt;/th>
 &lt;th>對 slot 影響&lt;/th>
 &lt;th>Primary 端徵兆&lt;/th>
 &lt;th>Recovery 路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Consumer 卡住 / lag&lt;/td>
 &lt;td>slot LSN 不前進、WAL 留著&lt;/td>
 &lt;td>&lt;code>pg_wal&lt;/code> 目錄持續長大、disk 撐爆&lt;/td>
 &lt;td>修 consumer / 加 throttle / 必要時 drop slot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consumer crash 無 restart&lt;/td>
 &lt;td>slot 留在 active state&lt;/td>
 &lt;td>跟 lag 同、不會自動清&lt;/td>
 &lt;td>手動 &lt;code>SELECT pg_drop_replication_slot('name')&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema change（ADD COLUMN）&lt;/td>
 &lt;td>多數 plugin 自動處理、無感&lt;/td>
 &lt;td>通常無感&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema change（DROP / RENAME COLUMN）&lt;/td>
 &lt;td>多數 plugin 直接斷&lt;/td>
 &lt;td>Consumer log 報錯、slot active 卻不前進&lt;/td>
 &lt;td>重建 publication / 重 init load&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Initial COPY&lt;/td>
 &lt;td>slot 建立時跑 snapshot、long-running tx&lt;/td>
 &lt;td>大表 COPY 期間鎖跟 WAL 都受影響&lt;/td>
 &lt;td>用 &lt;code>CREATE_REPLICATION_SLOT ... NOEXPORT_SNAPSHOT&lt;/code> 分階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Promotion (failover)&lt;/td>
 &lt;td>physical slot 跟 logical slot 處理不同&lt;/td>
 &lt;td>logical slot 在 PG 16- 不跨 failover&lt;/td>
 &lt;td>PG 16+ logical slot 持久化、或 consumer 重 init load&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replay storm（offset 重置）&lt;/td>
 &lt;td>slot 不變、consumer 重讀&lt;/td>
 &lt;td>Kafka 端流量爆、application 看 duplicate&lt;/td>
 &lt;td>Idempotent consumer 設計、或 transactional outbox&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 failure mode 對應的詳細配置 + recovery 步驟、下面分段展開。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 提到 logical decoding / Debezium CDC、本文聚焦 <em>replication slot 生命週期 + 5 個 production failure mode 跟 recovery</em> 的對照。</p></blockquote>
<h2 id="replication-slot--failure--recovery-對照">Replication slot × Failure × Recovery 對照</h2>
<p>Logical replication 跟 Debezium CDC 的 production 議題集中在 <em>replication slot</em> — 它是 PostgreSQL 內保證 WAL 不被回收的 anchor point；slot 設不對、整個 CDC pipeline 失效。各 failure mode 對 slot 的影響跟 recovery 路徑：</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>對 slot 影響</th>
          <th>Primary 端徵兆</th>
          <th>Recovery 路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Consumer 卡住 / lag</td>
          <td>slot LSN 不前進、WAL 留著</td>
          <td><code>pg_wal</code> 目錄持續長大、disk 撐爆</td>
          <td>修 consumer / 加 throttle / 必要時 drop slot</td>
      </tr>
      <tr>
          <td>Consumer crash 無 restart</td>
          <td>slot 留在 active state</td>
          <td>跟 lag 同、不會自動清</td>
          <td>手動 <code>SELECT pg_drop_replication_slot('name')</code></td>
      </tr>
      <tr>
          <td>Schema change（ADD COLUMN）</td>
          <td>多數 plugin 自動處理、無感</td>
          <td>通常無感</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Schema change（DROP / RENAME COLUMN）</td>
          <td>多數 plugin 直接斷</td>
          <td>Consumer log 報錯、slot active 卻不前進</td>
          <td>重建 publication / 重 init load</td>
      </tr>
      <tr>
          <td>Initial COPY</td>
          <td>slot 建立時跑 snapshot、long-running tx</td>
          <td>大表 COPY 期間鎖跟 WAL 都受影響</td>
          <td>用 <code>CREATE_REPLICATION_SLOT ... NOEXPORT_SNAPSHOT</code> 分階段</td>
      </tr>
      <tr>
          <td>Promotion (failover)</td>
          <td>physical slot 跟 logical slot 處理不同</td>
          <td>logical slot 在 PG 16- 不跨 failover</td>
          <td>PG 16+ logical slot 持久化、或 consumer 重 init load</td>
      </tr>
      <tr>
          <td>Replay storm（offset 重置）</td>
          <td>slot 不變、consumer 重讀</td>
          <td>Kafka 端流量爆、application 看 duplicate</td>
          <td>Idempotent consumer 設計、或 transactional outbox</td>
      </tr>
  </tbody>
</table>
<p>每個 failure mode 對應的詳細配置 + recovery 步驟、下面分段展開。</p>
<h2 id="logical-replication-基礎publication--subscription--slot">Logical replication 基礎：publication + subscription + slot</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Primary：建 publication
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">app_changes</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="p">,</span><span class="w"> </span><span class="n">events</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Subscriber：建 subscription（自動建 replication slot）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">SUBSCRIPTION</span><span class="w"> </span><span class="n">app_sub</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">CONNECTION</span><span class="w"> </span><span class="s1">&#39;host=primary user=replicator dbname=app&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">app_changes</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">slot_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;app_sub_slot&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">copy_data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">true</span><span class="p">);</span></span></span></code></pre></div><p>關鍵物件：</p>
<ul>
<li><strong>publication</strong>（primary 端）：宣告 <em>哪些表 + 哪些操作（INSERT/UPDATE/DELETE/TRUNCATE）</em> 對外暴露</li>
<li><strong>subscription</strong>（subscriber 端、若是 PG-to-PG）：訂閱 + 自動建 slot + 自動 initial COPY</li>
<li><strong>replication slot</strong>：primary 端、保證 <em>consumer 還沒消費的 WAL</em> 不被回收</li>
</ul>
<p><code>copy_data = true</code> 觸發 initial COPY（snapshot）+ 後續 streaming；<code>copy_data = false</code> 只 streaming、適合 already-in-sync 場景。</p>
<h2 id="debezium-cdc用-logical-replication-slot-但繞過-subscription">Debezium CDC：用 logical replication slot 但繞過 subscription</h2>
<p>Debezium 不是 PostgreSQL subscriber、是 <em>直接讀 replication slot</em> 的外部 consumer：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-properties" data-lang="properties"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Debezium PostgreSQL connector</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">connector.class</span><span class="o">=</span><span class="s">io.debezium.connector.postgresql.PostgresConnector</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">database.hostname</span><span class="o">=</span><span class="s">primary</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">database.dbname</span><span class="o">=</span><span class="s">app</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">plugin.name</span><span class="o">=</span><span class="s">pgoutput                            # 內建、PG 10+ 推薦</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">slot.name</span><span class="o">=</span><span class="s">debezium_app</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">publication.name</span><span class="o">=</span><span class="s">app_changes</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">publication.autocreate.mode</span><span class="o">=</span><span class="s">filtered            # debezium 自動建 publication</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">table.include.list</span><span class="o">=</span><span class="s">public.orders,public.events</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">snapshot.mode</span><span class="o">=</span><span class="s">initial                            # 起始 snapshot 後 streaming</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>Debezium 用 <code>pgoutput</code>（PG 10+ 內建）或 <code>wal2json</code>（外掛 plugin）解 WAL、轉成結構化事件送 Kafka</li>
<li>不像 PG-to-PG subscription、Debezium 沒 subscription object、是 <em>外部 consumer 自管</em> replication slot</li>
<li>Failure mode 上 <em>consumer 端是 Debezium 自己</em>、所以 lag 來源是 Debezium 處理速度 / Kafka 寫入速度</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1consumer-lagslot-lsn-不前進primary-disk-爆">Case 1：consumer lag、slot LSN 不前進、primary disk 爆</h3>
<p><strong>徵兆</strong>：primary <code>pg_wal</code> 目錄持續長大、<code>df -h</code> 看磁碟 90%+；<code>pg_replication_slots</code> 看 <code>confirmed_flush_lsn</code> 卡在某 LSN、<code>pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)</code> 數十 GB。</p>
<p><strong>根因</strong>：consumer（Debezium / subscriber）處理慢於 primary 寫入；replication slot <em>保證 WAL 不回收</em>、但 consumer 沒消費 → WAL 堆積。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>監測</strong>：Prometheus alert <code>pg_replication_slot_lag_bytes &gt; 5GB</code> 觸發前 catch</li>
<li><strong>修 consumer</strong>：throttle primary 寫入 OR scale Debezium / subscriber 處理能力</li>
<li><strong>緊急</strong>：<code>SELECT pg_drop_replication_slot('debezium_app')</code> 釋放 WAL — 但 consumer 必須重 init load（資料缺一塊）</li>
<li><strong>架構</strong>：用 <em>max_slot_wal_keep_size</em>（PG 13+）設 slot 能保留 WAL 上限、超出自動 invalidate slot、保護 primary disk</li>
</ol>
<h3 id="case-2consumer-crash-後-slot-變-zombie">Case 2：consumer crash 後 slot 變 zombie</h3>
<p><strong>徵兆</strong>：Debezium pod OOM crash、新 pod 起來時報 <code>slot is active for PID X</code>、無法 attach；primary 端 <code>pg_replication_slots.active = true</code>、<code>active_pid</code> 指向已經死掉的 process。</p>
<p><strong>根因</strong>：PostgreSQL 把 slot 標 active 是基於 <em>當下有 connection</em>；consumer crash 但 connection 沒被 server 端發現（network 沒 RST）、slot 留在 active state。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 手動清 zombie slot
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_terminate_backend</span><span class="p">(</span><span class="n">active_pid</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">slot_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;debezium_app&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">active</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 或直接 drop（會丟資料、consumer 要重 init）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_drop_replication_slot</span><span class="p">(</span><span class="s1">&#39;debezium_app&#39;</span><span class="p">);</span></span></span></code></pre></div><p>預防：</p>
<ol>
<li>PostgreSQL <code>tcp_keepalives_idle / interval / count</code> 設較短（300 / 60 / 6）、network drop 較快被發現</li>
<li>Consumer 端用 <em>graceful shutdown</em> + <code>pg_terminate_backend(active_pid)</code> 在 startup 前主動清 stale connection</li>
</ol>
<h3 id="case-3schema-changedrop--rename-column斷流">Case 3：schema change（DROP / RENAME COLUMN）斷流</h3>
<p><strong>徵兆</strong>：Debezium consumer 突然停 produce 訊息、log 報 <code>column XYZ does not exist</code>；primary 端 slot 還 active、但 <code>confirmed_flush_lsn</code> 不前進。</p>
<p><strong>根因</strong>：pgoutput plugin 把 WAL 解成 row event 時、用的 schema 是 <em>當下 catalog</em>；如果中間 DROP COLUMN、之前 WAL 內的 row event 含已不存在欄位、解析失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：schema change 走 <em>expand-contract pattern</em>
<ul>
<li>Phase 1: ADD COLUMN new_col（不影響 logical replication）</li>
<li>Phase 2: application 雙寫 old + new</li>
<li>Phase 3: 等 consumer catch up old column 訊息</li>
<li>Phase 4: DROP COLUMN old_col（此時無 in-flight WAL 帶 old_col）</li>
</ul>
</li>
<li><strong>緊急</strong>：DROP existing slot、重建 publication 跟 slot、consumer 重 init load</li>
<li><strong>長期</strong>：用 Debezium <em>snapshot.mode=schema_only_recovery</em> 在 schema 變動時不重灌資料、只 reset schema</li>
</ol>
<h3 id="case-4initial-copy-大表鎖太久">Case 4：initial COPY 大表鎖太久</h3>
<p><strong>徵兆</strong>：對 1TB 表跑 <code>CREATE SUBSCRIPTION ... WITH (copy_data=true)</code> 後、application 對該表 query / write 阻塞 30+ 分鐘；application timeout 大量。</p>
<p><strong>根因</strong>：initial COPY 默認跑在 <em>single transaction</em>、整個 snapshot LSN 鎖住、長 transaction 跟 vacuum 衝突；同時對 subscriber 端鎖表寫入。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>分階段 init</strong>：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Primary：建 publication 不 copy
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">app_changes</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">big_table</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- Subscriber：建 subscription 不 copy
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">SUBSCRIPTION</span><span class="w"> </span><span class="n">app_sub</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="k">CONNECTION</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">app_changes</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">copy_data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">false</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 手動跑 partition-by-partition COPY（若是 partition table）
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">-- 或用 pg_dump / pg_basebackup 拿 snapshot</span></span></span></code></pre></div><ol start="2">
<li><strong>PG 16+ parallel init</strong>：<code>max_sync_workers_per_subscription = 4</code> 平行 COPY 多個表</li>
<li><strong>Debezium replacement</strong>：用 incremental snapshot（Debezium 1.6+）、background trickle copy、不鎖長 transaction</li>
</ol>
<h3 id="case-5replay-storm-後-consumer-offset-reset">Case 5：replay storm 後 consumer offset reset</h3>
<p><strong>徵兆</strong>：Debezium 修 bug / 重 deploy 後、<code>snapshot.mode=initial</code> 觸發整個資料重灌；Kafka topic 流量爆 10x、下游 application 看到大量 duplicate event。</p>
<p><strong>根因</strong>：Debezium offset store（Kafka topic 或 file）被誤刪 / corruption；重啟時不知道從哪 LSN 開始、預設 fall back 到 initial snapshot。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：Debezium offset store 跟 Kafka cluster <em>backup 一起做</em>、不要單獨依賴 Kafka topic</li>
<li><strong>架構</strong>：consumer side 設計 <em>idempotent</em> — 用 event 自帶的 (source LSN + transaction ID) 當 dedupe key</li>
<li><strong>transactional outbox pattern</strong>：CDC 只 capture outbox 表、application 主動寫 outbox + business data 在同 transaction；duplicate 由 application 自己 dedupe</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replication slot lag</td>
          <td><code>pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)</code></td>
          <td>&gt; 1GB lag 訊號 consumer 跟不上</td>
      </tr>
      <tr>
          <td>Primary <code>pg_wal</code> size</td>
          <td>retention × peak WAL rate</td>
          <td>預留 disk 容量 = max_slot_wal_keep_size + 30% buffer</td>
      </tr>
      <tr>
          <td>Debezium throughput</td>
          <td>~5-10K row/s 單 connector、多表平行可拉</td>
          <td>跟 primary write rate 對比</td>
      </tr>
      <tr>
          <td>Initial COPY time</td>
          <td>100GB ~ 10-30 分鐘（看 network + subscriber IO）</td>
          <td>TB 級必須分階段</td>
      </tr>
      <tr>
          <td>Slot 數量</td>
          <td>每 slot 佔 primary 一份 WAL 保留 buffer</td>
          <td>5+ slot 同時跑 disk 壓力倍增</td>
      </tr>
      <tr>
          <td>max_replication_slots</td>
          <td>預設 10、production 跑 CDC + standby 各佔 slot 要拉到 20-50</td>
          <td>達上限會拒新 slot 建立</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>Debezium production：1 connector per source schema、不要 1 connector 跨 50 個表</li>
<li>Slot retention：<code>max_slot_wal_keep_size = 100GB</code>、超出 invalidate slot 保護 primary</li>
<li>Monitor cadence：1 分鐘 sample lag + 5 分鐘 alert threshold</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-patroni-ha-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> 整合</h3>
<p>logical slot 在 PG 16- 不跨 failover、是長期痛點：</p>
<ol>
<li><strong>PG 16-</strong>：failover 後 logical consumer 必須重 init（slot 在新 leader 上不存在）</li>
<li><strong>PG 16+</strong>：<code>failover</code> parameter 讓 logical slot 在 standby 同步、failover 後 consumer 直接接</li>
<li>Patroni 16+ 支援 logical slot persistence 配置、配合用</li>
</ol>
<h3 id="跟-kafka-outbox-pattern">跟 Kafka outbox pattern</h3>
<p>production-grade CDC 不直接 read business table、是 read <em>outbox table</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Application transaction
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(...)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">outbox</span><span class="w"> </span><span class="p">(</span><span class="n">event_type</span><span class="p">,</span><span class="w"> </span><span class="n">payload</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;order_created&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">now</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span></span></span></code></pre></div><p>Debezium 只 capture outbox table、event payload 已是 application-shaped JSON、不用解 row event。好處：</p>
<ol>
<li>Schema change 不影響 CDC（outbox table schema 穩定）</li>
<li>跨表 transaction 對應到單 event（outbox 是業務語意層）</li>
<li>Replay 可靠 — outbox 是 append-only、可重讀</li>
</ol>
<h3 id="跟-partitioning-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">partitioning</a> 整合</h3>
<p>partitioned table 的 logical replication：</p>
<ol>
<li>PG 13+ <code>publish_via_partition_root = true</code> — publication 從 parent 角度看、不是 per-partition</li>
<li>Subscriber 端可 partition 不同 strategy（甚至不 partition）</li>
<li>Schema change 對 partition table 更複雜、走 expand-contract 嚴格</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Logical replication conflict</strong>：subscriber 端寫衝突的處理（PG 17+ 加 conflict resolution）</li>
<li><strong>bi-directional replication（pg_active）</strong>：多 region active-active、衝突解決設計</li>
<li><strong>Decoder plugin 對比</strong>：pgoutput / wal2json / decoderbufs 效能跟易用性</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a> — schema change × CDC 對應</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a> / <a href="/blog/backend/01-database/vendors/postgresql/replication-slot-management/" data-link-title="PostgreSQL Replication Slot Management：Physical / Logical / Failover Slot 治理" data-link-desc="PG replication slot 是 *primary 端的 standby 進度紀錄*、防 WAL premature deletion。但 orphan slot 會吃 disk、failover 後 logical slot 不會自動跟新 primary、是 PG 操作的 hidden complexity。本文走 physical / logical slot 差異、slot lifecycle、failover slot synchronization（PG 17&#43; 新特性）、orphan slot 治理、5 production 踩雷（orphan slot disk 爆 / logical slot lag / failover 後 slot 丟 / wal_keep_size 跟 slot 衝突 / connection 同時打 slot 數量限制）">Replication Slot Management</a>（slot lifecycle / orphan / failover sync）/ <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>（streaming + LSN 基礎）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>9.C34 GCP：130,000-node GKE cluster 的工程極限</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/</guid><description>&lt;p>這個案例的核心責任是揭示「現代 AI workload 對 Kubernetes 規模極限的拉扯」。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster&lt;/a> 走「多小 cluster 隔離」相反 — GCP 內部驗證的是「單一巨大 cluster 集中管理」、為前沿 LLM 訓練的萬卡叢集需求設計。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>GCP 130K-node GKE cluster 實驗（引自 &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/how-we-built-a-130000-node-gke-cluster">How we built a 130,000-node GKE cluster&lt;/a>）：&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>實驗節點數&lt;/td>
 &lt;td>130,000（vs 官方支援 65,000）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pod 創建峰值&lt;/td>
 &lt;td>1,000 Pods / 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Phase 1 deploy 時間&lt;/td>
 &lt;td>130,000 Pods in 3 分 40 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Phase 2 batch 創建&lt;/td>
 &lt;td>65,000 Pods in 81 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Preemption 峰值&lt;/td>
 &lt;td>39,000 Pods preempted in 93 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pod startup p99&lt;/td>
 &lt;td>~10 秒（inference workload）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API server LIST p99&lt;/td>
 &lt;td>「well below defined thresholds」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database objects&lt;/td>
 &lt;td>100 萬 +&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lease 更新 QPS&lt;/td>
 &lt;td>13,000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客戶當前範圍&lt;/td>
 &lt;td>20-65K node range&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預期 cluster size 穩定&lt;/td>
 &lt;td>100K node mark&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>工作負載類型：AI / ML 平台、三個 priority class：&lt;/p>
&lt;ul>
&lt;li>Low：preemptible batch（data prep）&lt;/li>
&lt;li>Medium：core model training（tolerant to queuing）&lt;/li>
&lt;li>High：latency-sensitive inference&lt;/li>
&lt;/ul>
&lt;p>關鍵 control plane 設計：&lt;/p>
&lt;ul>
&lt;li>Consistent Reads from Cache（KEP-2340）— 強一致 read 從 in-memory cache、不打 storage&lt;/li>
&lt;li>Snapshottable API Server Cache（KEP-4988）— B-tree snapshot 處理 LIST 請求&lt;/li>
&lt;li>Spanner-based key-value store 作為 K8s storage backend（撐 13K QPS lease 更新）&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>130K-node 案例揭露三個 hyperscale K8s 設計的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>單一 control plane 的極限取決於 storage backend、不是 nodes&lt;/strong>：130K node 不是「機器跑不動」、是「API server 跟 etcd 撐不撐住」。GCP 用 Spanner 替換 etcd、配上 cache-first read 設計、把 storage 從瓶頸變成「showed no signs of not being able to support higher scales」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a> 的「真實 bottleneck 在哪一層」。&lt;/li>
&lt;li>&lt;strong>AI workload 顛覆了 K8s 容量規劃&lt;/strong>：傳統 web workload 的 K8s 多在 1K-10K node、節點生命週期長。AI workload 短時間爆量創建跟銷毀 Pods（13 萬個 in 3 分 40 秒）、preempt 跟 schedule 頻繁、對 control plane 是完全不同壓力模式。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> — workload 形狀完全不同、容量規劃也完全不同。&lt;/li>
&lt;li>&lt;strong>「power constraint &amp;gt; chip supply」是新瓶頸&lt;/strong>：單顆 NVIDIA GB200 GPU 吃 2700W、萬卡叢集 = 27MW 用電量。未來 mega cluster 必須跨多個 data center（一個 DC 電力撐不住）、需要 &lt;em>robust multi-cluster solutions&lt;/em>。這層瓶頸跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界&lt;/a> 對接 — 電力成本變成主要 cost driver。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是揭示「現代 AI workload 對 Kubernetes 規模極限的拉扯」。跟 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster</a> 走「多小 cluster 隔離」相反 — GCP 內部驗證的是「單一巨大 cluster 集中管理」、為前沿 LLM 訓練的萬卡叢集需求設計。</p>
<h2 id="觀察">觀察</h2>
<p>GCP 130K-node GKE cluster 實驗（引自 <a href="https://cloud.google.com/blog/products/containers-kubernetes/how-we-built-a-130000-node-gke-cluster">How we built a 130,000-node GKE cluster</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實驗節點數</td>
          <td>130,000（vs 官方支援 65,000）</td>
      </tr>
      <tr>
          <td>Pod 創建峰值</td>
          <td>1,000 Pods / 秒</td>
      </tr>
      <tr>
          <td>Phase 1 deploy 時間</td>
          <td>130,000 Pods in 3 分 40 秒</td>
      </tr>
      <tr>
          <td>Phase 2 batch 創建</td>
          <td>65,000 Pods in 81 秒</td>
      </tr>
      <tr>
          <td>Preemption 峰值</td>
          <td>39,000 Pods preempted in 93 秒</td>
      </tr>
      <tr>
          <td>Pod startup p99</td>
          <td>~10 秒（inference workload）</td>
      </tr>
      <tr>
          <td>API server LIST p99</td>
          <td>「well below defined thresholds」</td>
      </tr>
      <tr>
          <td>Database objects</td>
          <td>100 萬 +</td>
      </tr>
      <tr>
          <td>Lease 更新 QPS</td>
          <td>13,000</td>
      </tr>
      <tr>
          <td>客戶當前範圍</td>
          <td>20-65K node range</td>
      </tr>
      <tr>
          <td>預期 cluster size 穩定</td>
          <td>100K node mark</td>
      </tr>
  </tbody>
</table>
<p>工作負載類型：AI / ML 平台、三個 priority class：</p>
<ul>
<li>Low：preemptible batch（data prep）</li>
<li>Medium：core model training（tolerant to queuing）</li>
<li>High：latency-sensitive inference</li>
</ul>
<p>關鍵 control plane 設計：</p>
<ul>
<li>Consistent Reads from Cache（KEP-2340）— 強一致 read 從 in-memory cache、不打 storage</li>
<li>Snapshottable API Server Cache（KEP-4988）— B-tree snapshot 處理 LIST 請求</li>
<li>Spanner-based key-value store 作為 K8s storage backend（撐 13K QPS lease 更新）</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>130K-node 案例揭露三個 hyperscale K8s 設計的工程重點。</p>
<ol>
<li><strong>單一 control plane 的極限取決於 storage backend、不是 nodes</strong>：130K node 不是「機器跑不動」、是「API server 跟 etcd 撐不撐住」。GCP 用 Spanner 替換 etcd、配上 cache-first read 設計、把 storage 從瓶頸變成「showed no signs of not being able to support higher scales」。對應 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 的「真實 bottleneck 在哪一層」。</li>
<li><strong>AI workload 顛覆了 K8s 容量規劃</strong>：傳統 web workload 的 K8s 多在 1K-10K node、節點生命週期長。AI workload 短時間爆量創建跟銷毀 Pods（13 萬個 in 3 分 40 秒）、preempt 跟 schedule 頻繁、對 control plane 是完全不同壓力模式。對應 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> — workload 形狀完全不同、容量規劃也完全不同。</li>
<li><strong>「power constraint &gt; chip supply」是新瓶頸</strong>：單顆 NVIDIA GB200 GPU 吃 2700W、萬卡叢集 = 27MW 用電量。未來 mega cluster 必須跨多個 data center（一個 DC 電力撐不住）、需要 <em>robust multi-cluster solutions</em>。這層瓶頸跟 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界</a> 對接 — 電力成本變成主要 cost driver。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>130K-node 是 <em>Google 內部實驗</em>、不是 <em>客戶能用的 production</em> 配置。目前 GKE 官方支援 65K node、客戶用到 100K+ 還很遠。</li>
<li>AI workload 跟 web workload 完全不同、把 AI 經驗套用到 web service 容量規劃是錯誤類比。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>K8s control plane 跟 data plane 分開規劃容量</strong>：data plane（worker nodes）擴容容易、control plane（API server、etcd / storage）擴容難。瓶頸通常在 control plane、不是 worker。</li>
<li><strong>storage backend 是 K8s 規模極限的關鍵</strong>：etcd 撐 5K-10K node 後開始吃力、要用 PostgreSQL / Spanner / 自家 KV 替換、才能擴到萬級節點。一般客戶用不到、但要知道「為什麼到某個規模 etcd 不夠」。</li>
<li><strong>AI workload 用 specialized scheduler</strong>（Kueue、Volcano）：默認 K8s scheduler 為 web workload 設計、AI 的 gang scheduling、fair-sharing、preemption 都不太適合。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 scheduler 選型。</li>
<li><strong>power-aware capacity planning 是未來方向</strong>：傳統按 CPU / RAM 規劃容量、未來要加上 <em>power budget</em>。data center 用電量是硬上限、不是錢的問題。</li>
<li><strong>multi-cluster 是萬卡訓練的必然</strong>：單一 cluster 撐不住、要 MultiKueue 等跨 cluster 排程方案。對應 <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 multi-cluster</a> 但目的完全不同。</li>
</ol>
<p>跨平台等效：AWS EKS 官方支援單 cluster 多至 100K pod / cluster、Azure AKS 支援 5K node / cluster。GCP 用 Spanner 替換 etcd 是最深的工程投資、目前其他兩家還沒到這個規模。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他大規模 K8s → <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games 246 cluster</a>（多 cluster 策略）</li>
<li>對照 AI workload → <a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO 50x surge</a>（非 AI 但同 GCP K8s）</li>
<li>想理解 control plane vs data plane → <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a> + <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>想設計 K8s 容量上限 → <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/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/containers-kubernetes/how-we-built-a-130000-node-gke-cluster">How we built a 130,000-node GKE cluster</a></li>
<li><a href="https://cloud.google.com/blog/products/containers-kubernetes/gke-and-kubernetes-at-kubecon-2025">GKE and Kubernetes at KubeCon 2025</a></li>
<li><a href="https://cloud.google.com/blog/products/containers-kubernetes/whats-new-in-gke-at-next26">What&rsquo;s new in GKE at Next 26</a></li>
</ul>
]]></content:encoded></item><item><title>Backend 服務實務指南</title><link>https://tarrragon.github.io/blog/backend/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/</guid><description>&lt;p>Backend 教材的核心目標是教讀者理解後端服務如何共同支撐一個 production system。資料庫、快取、訊息佇列、觀測平台、部署平台、可靠性驗證、資安資料保護、事故處理與容量規劃，各自承擔一段服務責任；本教材把這些責任整理成可學習、可操作、可演進的跨語言知識路線。&lt;/p>
&lt;p>服務能力、風險、成本與決策是理解後端服務的必要概念框架。讀者學資料庫時需要知道 transaction、schema migration、replication lag 與資料修復；學快取時需要知道 freshness、origin protection、eviction 與 hot key；學 queue 時需要知道 delivery、processing、replay 與 idempotency。這些判準服務教學目標：讓讀者能看懂一個後端問題該交給哪類能力處理，並理解多個能力如何串接。&lt;/p>
&lt;p>語言教材負責各自的語法、標準庫、並發或非同步模型、測試方法與 interface / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol&lt;/a> 邊界；Backend 教材負責「應該被 application interface 隔離」的外部服務能力。Go、Python 或其他後端語言可以各自說明如何定義抽象邊界、處理取消與逾時、回傳錯誤、寫 fake 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test；Backend 章節則說明 SQLite、PostgreSQL、Redis、RabbitMQ、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、tracing、Kubernetes、identity、permission、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log&lt;/a> 等具體技術如何運作。&lt;/p>
&lt;p>Backend 是多個後端語言系列共用的教學層。未來若新增 frontend、data engineering、machine learning、mobile 或其他非後端主題，也可以用同樣方式把共用實作知識抽成獨立資料夾，讓特定語言教材保留在語言本身的能力邊界。&lt;/p>
&lt;h2 id="總體教學設計">總體教學設計&lt;/h2>
&lt;p>Backend 教材用三層學習結構組織內容。第一層是心智模型，先讓讀者理解資料、快取、事件、觀測、部署、驗證、資安、事故與容量分別承擔什麼責任；第二層是服務路徑，用一條具體業務流程串起多個模組的 artifact 與交接；第三層是具體服務與工具，討論 PostgreSQL、Redis、Kafka、Kubernetes、PagerDuty、k6 等服務如何落到真實操作。&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>心智模型&lt;/td>
 &lt;td>建立服務分類、責任邊界與共同術語&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">前置知識卡片&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務路徑&lt;/td>
 &lt;td>用同一條業務流程演練資料、快取、事件、觀測、部署、驗證、資安與事故交接&lt;/td>
 &lt;td>各模組主章（01-09）+ 本頁下方「貫穿式案例：Checkout 服務演進」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>真實服務&lt;/td>
 &lt;td>把分類語言落到 vendor / platform / tool 的能力、成本與遷移&lt;/td>
 &lt;td>各模組 &lt;code>vendors/&lt;/code> 子目錄、案例庫見 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 紅隊案例&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 效能案例&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這三層對應三種讀者問題。讀者想知道「這是什麼問題」時，先讀心智模型；想知道「一個服務流程怎麼把多個能力接起來」時，讀服務路徑；想知道「具體要選哪個工具、怎麼遷移」時，再進入真實服務與 vendor 頁。&lt;/p>
&lt;h3 id="學習路線">學習路線&lt;/h3>
&lt;p>Backend 教材可以依讀者目的分成六條路線。每條路線都有起點、主要順序與完成判準，讓讀者不用從能力地圖自行推導閱讀順序。&lt;/p>
&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>想理解後端服務分工&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態&lt;/a> → 00 → knowledge cards → 01 / 02 / 03 的共同觀念 → &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務邊界&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端對照&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸&lt;/a>&lt;/td>
 &lt;td>能把需求分類成正式狀態、暫存副本、非同步交接、觀測、部署或可靠性問題、並用服務邊界與擴展軸描述系統走向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API 到資料流&lt;/td>
 &lt;td>想設計 API 背後的資料、快取與事件流程&lt;/td>
 &lt;td>01 Database → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式&lt;/a> → 02 Cache → 03 Queue → 04 Evidence → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &amp;#43; PR review 整合 &amp;#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14 slow log 閉環&lt;/a>&lt;/td>
 &lt;td>能說明一次 checkout 如何跨 DB、cache、queue 與 evidence package、並在進 production 前用 query 預算與 slow log 攔下查詢反模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production 操作&lt;/td>
 &lt;td>想學上線、觀測、驗證與事故閉環&lt;/td>
 &lt;td>04 Observability → 05 Deployment（含 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發&lt;/a>）→ 06 Reliability → 08 Incident&lt;/td>
 &lt;td>能把 release、alert、gate、邊緣層 origin protection、incident decision log 與 write-back 串成操作閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security / Data Protection&lt;/td>
 &lt;td>想理解權限、秘密、資料、偵測與回應&lt;/td>
 &lt;td>07 Security → 04 audit evidence → 06 control validation → 08 security incident&lt;/td>
 &lt;td>能從身份、資料、入口、秘密與 audit evidence 判讀控制面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vendor / Migration&lt;/td>
 &lt;td>已懂分類、要比較工具或遷移&lt;/td>
 &lt;td>對應模組主章 → &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端對照地圖&lt;/a> → &lt;code>vendors/&lt;/code> → migration playbook（含 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 拆分執行 runbook&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出&lt;/a>）→ cases（含 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &amp;#43; PR review 整合 &amp;#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14 slow log 閉環&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 連線池放大&lt;/a>）&lt;/td>
 &lt;td>能先判斷分類責任，再對照雲端能力、比較具體服務、操作成本與遷移風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規模成長&lt;/td>
 &lt;td>已能做出「能跑」的 SaaS、要學「能撐」&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 升級 tripwire&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出&lt;/a>（託管出身者適用）→ &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 Queue&lt;/a> → &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11&lt;/a>&lt;/td>
 &lt;td>能描述一個服務從一臺機器演進到多區域多服務的時序、各階段該撞哪面牆、要先補哪一塊能力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些路線共享同一組後端語言。讀者可以只走一條，也可以從「系統心智模型」開始，再按工作需求轉入 API、production、security、vendor 或規模成長專題。&lt;/p></description><content:encoded><![CDATA[<p>Backend 教材的核心目標是教讀者理解後端服務如何共同支撐一個 production system。資料庫、快取、訊息佇列、觀測平台、部署平台、可靠性驗證、資安資料保護、事故處理與容量規劃，各自承擔一段服務責任；本教材把這些責任整理成可學習、可操作、可演進的跨語言知識路線。</p>
<p>服務能力、風險、成本與決策是理解後端服務的必要概念框架。讀者學資料庫時需要知道 transaction、schema migration、replication lag 與資料修復；學快取時需要知道 freshness、origin protection、eviction 與 hot key；學 queue 時需要知道 delivery、processing、replay 與 idempotency。這些判準服務教學目標：讓讀者能看懂一個後端問題該交給哪類能力處理，並理解多個能力如何串接。</p>
<p>語言教材負責各自的語法、標準庫、並發或非同步模型、測試方法與 interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a> 邊界；Backend 教材負責「應該被 application interface 隔離」的外部服務能力。Go、Python 或其他後端語言可以各自說明如何定義抽象邊界、處理取消與逾時、回傳錯誤、寫 fake 或 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test；Backend 章節則說明 SQLite、PostgreSQL、Redis、RabbitMQ、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、<a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、tracing、Kubernetes、identity、permission、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 等具體技術如何運作。</p>
<p>Backend 是多個後端語言系列共用的教學層。未來若新增 frontend、data engineering、machine learning、mobile 或其他非後端主題，也可以用同樣方式把共用實作知識抽成獨立資料夾，讓特定語言教材保留在語言本身的能力邊界。</p>
<h2 id="總體教學設計">總體教學設計</h2>
<p>Backend 教材用三層學習結構組織內容。第一層是心智模型，先讓讀者理解資料、快取、事件、觀測、部署、驗證、資安、事故與容量分別承擔什麼責任；第二層是服務路徑，用一條具體業務流程串起多個模組的 artifact 與交接；第三層是具體服務與工具，討論 PostgreSQL、Redis、Kafka、Kubernetes、PagerDuty、k6 等服務如何落到真實操作。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>教學責任</th>
          <th>主要入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>心智模型</td>
          <td>建立服務分類、責任邊界與共同術語</td>
          <td><a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零</a> + <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">前置知識卡片</a></td>
      </tr>
      <tr>
          <td>服務路徑</td>
          <td>用同一條業務流程演練資料、快取、事件、觀測、部署、驗證、資安與事故交接</td>
          <td>各模組主章（01-09）+ 本頁下方「貫穿式案例：Checkout 服務演進」</td>
      </tr>
      <tr>
          <td>真實服務</td>
          <td>把分類語言落到 vendor / platform / tool 的能力、成本與遷移</td>
          <td>各模組 <code>vendors/</code> 子目錄、案例庫見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 紅隊案例</a> 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 效能案例</a></td>
      </tr>
  </tbody>
</table>
<p>這三層對應三種讀者問題。讀者想知道「這是什麼問題」時，先讀心智模型；想知道「一個服務流程怎麼把多個能力接起來」時，讀服務路徑；想知道「具體要選哪個工具、怎麼遷移」時，再進入真實服務與 vendor 頁。</p>
<h3 id="學習路線">學習路線</h3>
<p>Backend 教材可以依讀者目的分成六條路線。每條路線都有起點、主要順序與完成判準，讓讀者不用從能力地圖自行推導閱讀順序。</p>
<table>
  <thead>
      <tr>
          <th>路線</th>
          <th>適合讀者</th>
          <th>建議順序</th>
          <th>讀完能做什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>系統心智模型</td>
          <td>想理解後端服務分工</td>
          <td><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態</a> → 00 → knowledge cards → 01 / 02 / 03 的共同觀念 → <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務邊界</a> / <a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端對照</a> / <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a></td>
          <td>能把需求分類成正式狀態、暫存副本、非同步交接、觀測、部署或可靠性問題、並用服務邊界與擴展軸描述系統走向</td>
      </tr>
      <tr>
          <td>API 到資料流</td>
          <td>想設計 API 背後的資料、快取與事件流程</td>
          <td>01 Database → <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> → 02 Cache → 03 Queue → 04 Evidence → <a href="/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &#43; PR review 整合 &#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14 slow log 閉環</a></td>
          <td>能說明一次 checkout 如何跨 DB、cache、queue 與 evidence package、並在進 production 前用 query 預算與 slow log 攔下查詢反模式</td>
      </tr>
      <tr>
          <td>Production 操作</td>
          <td>想學上線、觀測、驗證與事故閉環</td>
          <td>04 Observability → 05 Deployment（含 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發</a>）→ 06 Reliability → 08 Incident</td>
          <td>能把 release、alert、gate、邊緣層 origin protection、incident decision log 與 write-back 串成操作閉環</td>
      </tr>
      <tr>
          <td>Security / Data Protection</td>
          <td>想理解權限、秘密、資料、偵測與回應</td>
          <td>07 Security → 04 audit evidence → 06 control validation → 08 security incident</td>
          <td>能從身份、資料、入口、秘密與 audit evidence 判讀控制面</td>
      </tr>
      <tr>
          <td>Vendor / Migration</td>
          <td>已懂分類、要比較工具或遷移</td>
          <td>對應模組主章 → <a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端對照地圖</a> → <code>vendors/</code> → migration playbook（含 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 拆分執行 runbook</a> / <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出</a>）→ cases（含 <a href="/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &#43; PR review 整合 &#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14 slow log 閉環</a> / <a href="/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 連線池放大</a>）</td>
          <td>能先判斷分類責任，再對照雲端能力、比較具體服務、操作成本與遷移風險</td>
      </tr>
      <tr>
          <td>規模成長</td>
          <td>已能做出「能跑」的 SaaS、要學「能撐」</td>
          <td><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 升級 tripwire</a> → <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0</a> → <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管遷出</a>（託管出身者適用）→ <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1</a> → <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13</a> → <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13</a> → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a> → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a> → <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9</a> → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 Queue</a> → <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11</a></td>
          <td>能描述一個服務從一臺機器演進到多區域多服務的時序、各階段該撞哪面牆、要先補哪一塊能力</td>
      </tr>
  </tbody>
</table>
<p>這些路線共享同一組後端語言。讀者可以只走一條，也可以從「系統心智模型」開始，再按工作需求轉入 API、production、security、vendor 或規模成長專題。</p>
<h4 id="規模成長路線的閱讀重點">規模成長路線的閱讀重點</h4>
<p>規模成長路線回答的核心問題是「為什麼系統會被迫長出新的架構」。每一階段的轉折都來自一個具體的撞牆訊號：單一服務承載不了業務分化（拆服務）、單機規格撞天花板（水平擴展）、應用層 query 變慢（反模式優化）、單一資料庫成為瓶頸（高併發策略 + 讀寫分離）、origin 被流量打爆（邊緣分發）、同步呼叫卡住事務流（非同步化）、可預期高峰需要事前準備（peak event readiness）。</p>
<p>這條路線跟「API 到資料流」路線的差別：API 路線教讀者「怎麼把功能做出來」，規模路線教讀者「做出來之後怎麼撐住」。兩條路線可以分開讀、也可以順序讀，建議寫完第一個 MVP 後就回到規模路線盤點。MVP 是用託管平台或 BaaS 做的話、先用 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的升級自建 tripwire 判斷該不該啟動自建評估；評估成立、遷出執行見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>、遷入後再走規模成長階段。</p>
<h4 id="從影片觀眾的詞彙進入">從影片觀眾的詞彙進入</h4>
<p>若讀者剛從「用 Claude Code 做 SaaS 最常撞到的那堵牆」這類入門影片進來、可以用以下對照表把影片名詞橋接到本系列章節：</p>
<table>
  <thead>
      <tr>
          <th>影片名詞</th>
          <th>Blog 對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scale up / Scale out</td>
          <td><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a></td>
      </tr>
      <tr>
          <td>CDN / 邊緣節點 / 物料配送</td>
          <td><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a></td>
      </tr>
      <tr>
          <td>微服務 / 拆分丁丁茶業</td>
          <td><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a></td>
      </tr>
      <tr>
          <td>Read replica / 主從複製</td>
          <td><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a> + <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 stateless 前提中的 stateful 分支</a></td>
      </tr>
      <tr>
          <td>快取 / 保溫桶</td>
          <td><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組 快取與 Redis</a></td>
      </tr>
      <tr>
          <td>Queue + Rate limiter</td>
          <td><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組 訊息佇列</a> + <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></td>
      </tr>
      <tr>
          <td>索引 / N+1 / 倉庫整理</td>
          <td><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a></td>
      </tr>
      <tr>
          <td>雲端服務（RDS / ECS / S3）</td>
          <td><a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖</a></td>
      </tr>
  </tbody>
</table>
<p>影片用具象比喻幫助理解、blog 用工程術語幫助操作；兩者承擔的目標不同、把詞彙橋接起來後可以兩邊互相印證。</p>
<h4 id="規模成長階段對照">規模成長階段對照</h4>
<p>每個影片觀眾最想要的答案是「現在系統長這樣、下一步該做什麼」。下表把規模成長路線拆成 6 個典型階段、每階段對應一個撞牆訊號跟該做的下一件事：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>撞牆訊號</th>
          <th>該做的事</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A</td>
          <td>一臺機器 CPU / 記憶體吃滿</td>
          <td>評估垂直 vs 水平擴展、確認 stateless 前提</td>
          <td><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a></td>
      </tr>
      <tr>
          <td>B</td>
          <td>DB 變慢、加機器看起來只是 workaround</td>
          <td>先用反模式清單收回單機容量、再考慮 vendor 升級</td>
          <td><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a></td>
      </tr>
      <tr>
          <td>C</td>
          <td>DB 寫入 OK 但讀取壓爆 primary</td>
          <td>開 read replica、應用層配讀寫路由</td>
          <td><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL</a> + <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a></td>
      </tr>
      <tr>
          <td>D</td>
          <td>活動 / KOL 引流時 origin 被靜態資源請求打爆</td>
          <td>把靜態 / 半靜態內容放到邊緣層、保護 origin</td>
          <td><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發 + Origin Protection</a></td>
      </tr>
      <tr>
          <td>E</td>
          <td>同步呼叫卡住事務流、付款 / 通知互相阻塞</td>
          <td>拆事件、非同步化、加 idempotency</td>
          <td><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列</a></td>
      </tr>
      <tr>
          <td>F</td>
          <td>業務分化、團隊發版互相阻擋</td>
          <td>沿真實邊界拆服務（資料 / 團隊 / 部署 / 流量四選一）</td>
          <td><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a></td>
      </tr>
  </tbody>
</table>
<p>階段不是嚴格時序、可能跳階段或反覆。階段 F（服務拆分）有些團隊會在 A-E 之前先做、有些團隊永遠不會做。階段 D 之後若要應對可預期高峰活動（雙 11、新片上線、活動推廣）、加 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a>；平日的容量規劃靠 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>。</p>
<h3 id="貫穿式案例checkout-服務演進">貫穿式案例：Checkout 服務演進</h3>
<p>貫穿式案例的責任是把平行模組串成同一個服務演進路徑。本系列使用簡化的 checkout / order / payment / notification 流程作為主案例：使用者建立訂單、付款服務回應、商品或價格資料被快取、事件送到下游通知與報表、服務上線時產出觀測與驗證證據，事故發生時留下決策紀錄並回寫改善。</p>
<table>
  <thead>
      <tr>
          <th>Episode</th>
          <th>問題</th>
          <th>主要模組</th>
          <th>主要產物</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>E1</td>
          <td>新增付款狀態欄位</td>
          <td>01 + 04 + 08</td>
          <td>migration plan、validation query、incident decision route</td>
      </tr>
      <tr>
          <td>E2</td>
          <td>商品價格快取失效與回源保護</td>
          <td>02 + 04 + 06</td>
          <td>cache evidence、origin protection gate、warmup plan</td>
      </tr>
      <tr>
          <td>E3</td>
          <td>訂單事件 consumer 失敗與 replay</td>
          <td>03 + 06 + 08</td>
          <td>idempotency evidence、DLQ handling、replay runbook</td>
      </tr>
      <tr>
          <td>E4</td>
          <td>Checkout service rollout</td>
          <td>05 + 04 + 08</td>
          <td>rollout plan、canary evidence、drain signal、rollback condition</td>
      </tr>
      <tr>
          <td>E5</td>
          <td>Payment provider timeout 變更</td>
          <td>06 + 04 + 09</td>
          <td>release gate、SLO evidence、capacity baseline</td>
      </tr>
      <tr>
          <td>E6</td>
          <td>Webhook secret rotation</td>
          <td>07 + 04 + 08</td>
          <td>scope map、audit evidence、rollback window</td>
      </tr>
      <tr>
          <td>E7</td>
          <td>Flash-sale peak readiness</td>
          <td>09 + 02 + 03 + 06</td>
          <td>workload model、saturation evidence、queue / cache capacity gate</td>
      </tr>
  </tbody>
</table>
<p>每個模組可以獨立閱讀；貫穿式案例提供跨模組記憶。讀者看到資料庫章節時，知道它處理 E1 的正式狀態演進；看到 queue 章節時，知道它處理 E3 的跨程序交接；看到事故章節時，知道它收斂 E1、E3、E4、E6 的決策與回寫。</p>
<h3 id="教材完成判準">教材完成判準</h3>
<p>Backend 教材的完成判準是讀者能沿著一條路線走完並做出可操作判斷。內容覆蓋率、案例數與 vendor 數量只是素材面進度；教學面要看讀者能否取得起點、順序、案例與下一步路由。</p>
<table>
  <thead>
      <tr>
          <th>判準</th>
          <th>具體訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>學習路線成立</td>
          <td>入口頁能依目的給出閱讀順序與完成判準</td>
      </tr>
      <tr>
          <td>概念梯度成立</td>
          <td>讀者先建立分類語言，再進入服務路徑，最後看 vendor / migration</td>
      </tr>
      <tr>
          <td>貫穿案例成立</td>
          <td>多個模組能回到同一條 checkout 服務演進路徑</td>
      </tr>
      <tr>
          <td>Artifact 可演練</td>
          <td>Evidence package、release gate、decision log、rollback condition 能被讀者填寫</td>
      </tr>
      <tr>
          <td>案例能回寫</td>
          <td>07 / 09 案例庫能支撐主章判讀，也能揭露缺口</td>
      </tr>
      <tr>
          <td>Vendor 回到分類</td>
          <td>具體服務頁先回扣分類責任，再比較功能、成本與遷移</td>
      </tr>
  </tbody>
</table>
<p>這套設計來自三張寫作反省卡：<a href="/blog/report/teaching-goal-before-decision-frame/" data-link-title="教材目標先於決策框架" data-link-desc="教材的核心目標是讓讀者學會某個領域的心智模型、操作語意與演進路徑；服務能力、風險、成本與決策是教學中的必要概念框架。當決策框架取代教材目標，文章會變成選型分析或治理文件，讀者知道怎麼判斷，卻仍缺少領域學習路線。">教材目標先於決策框架</a>、<a href="/blog/report/teaching-completeness-by-learner-journey/" data-link-title="教材完整性要用讀者旅程驗證" data-link-desc="教材完整性要用讀者旅程驗證；章節數、案例數或 vendor 覆蓋度只能判斷素材量。成熟教材要能回答不同讀者從哪裡開始、按什麼順序讀、讀完能做什麼。LLM 與 Go 目錄顯示，讀者旅程、學習梯度與主題導讀是教學設計完成度的核心訊號。">教材完整性要用讀者旅程驗證</a> 與 <a href="/blog/report/throughline-case-as-teaching-spine/" data-link-title="貫穿式案例是服務教材的教學骨架" data-link-desc="服務型教材需要一條可重播的貫穿式案例，把資料庫、快取、queue、觀測、部署、可靠性、資安、事故與容量串成同一個服務演進路徑。沒有貫穿式案例時，章節會各自正確但讀者難以理解能力之間如何交接。">貫穿式案例是服務教材的教學骨架</a>。</p>
<h2 id="教材邊界">教材邊界</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>放在語言教材</th>
          <th>放在 Backend 教材</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料存取</td>
          <td>repository port、interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a>、context / cancellation、error、<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test</td>
          <td>SQLite、PostgreSQL、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a>、<a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、index、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a></td>
      </tr>
      <tr>
          <td>快取</td>
          <td>cache port、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 概念、資料複製邊界、失效策略的程式邊界</td>
          <td>Redis 資料型別、<a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a>、distributed lock、<a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a>、<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a></td>
      </tr>
      <tr>
          <td>訊息傳遞</td>
          <td>channel / <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> abstraction、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>、publisher port、processor、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> interface</td>
          <td>RabbitMQ、NATS、Kafka、Redis Streams、<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>、<a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a></td>
      </tr>
      <tr>
          <td>可觀測性</td>
          <td>標準 logger、執行環境訊號、diagnostics endpoint、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 邊界、錯誤分類欄位</td>
          <td><a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> aggregation、Prometheus、OpenTelemetry、trace、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a></td>
      </tr>
      <tr>
          <td>部署平台</td>
          <td><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>、health/<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、signal handling、resource limit、<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a> hook 的程式設計</td>
          <td>Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> image、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a></td>
      </tr>
      <tr>
          <td>可靠性驗證</td>
          <td>unit test、table-driven / parameterized test、race / async test、integration test、故障路徑測試</td>
          <td><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a>、<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a>、fuzz campaign、chaos testing、環境治理</td>
      </tr>
      <tr>
          <td>資安保護</td>
          <td><a href="/blog/backend/knowledge-cards/middleware/" data-link-title="Request Middleware" data-link-desc="說明請求處理鏈中的共通攔截與前後置處理">Request Middleware</a>、policy interface、error mapping、redaction helper、security test</td>
          <td>identity、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a>、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a></td>
      </tr>
      <tr>
          <td>效能容量</td>
          <td>algorithm、hot path、micro benchmark、runtime profiler 解讀、並發程式邊界</td>
          <td>workload model、production traffic replay、k6 / JMeter / Gatling / Locust、saturation metric、headroom budget、capacity planning、cost per request</td>
      </tr>
  </tbody>
</table>
<h2 id="教學模組">教學模組</h2>
<h3 id="前置知識卡片"><a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">前置知識卡片</a></h3>
<p>用原子化卡片整理 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、transaction、migration、CDC、<a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill</a>、<a href="/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover</a>、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、backoff、<a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a>、<a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">retry storm</a>、<a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">load shedding</a>、<a href="/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">bulkhead</a>、<a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a>、TTL、<a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a>、<a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight</a>、broker、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/prefetch/" data-link-title="Prefetch" data-link-desc="說明 consumer 一次取得多少未完成訊息，以及它如何影響吞吐與公平性">prefetch</a>、<a href="/blog/backend/knowledge-cards/redelivery/" data-link-title="Redelivery" data-link-desc="說明 broker 重新投遞訊息時 consumer 需要承擔的重入責任">redelivery</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>、<a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a>、idempotency、outbox、backpressure、<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、metrics、trace、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a>、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA</a>、<a href="/blog/backend/knowledge-cards/mass-assignment/" data-link-title="Mass Assignment" data-link-desc="說明自動綁定 request 欄位如何造成未授權欄位被修改">mass assignment</a>、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">Website Certificate Lifecycle</a> 等後端 domain knowhow。這些卡片負責補足服務選型文章中的先備知識，讓章節可以專注在需求判讀與服務取捨。</p>
<h3 id="模組零後端需求分析與服務選型"><a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零：後端需求分析與服務選型</a></h3>
<p>整理後端需求分類、流量形狀、資料量、失敗代價、成本模型、錯誤定位、觀測訊號、備援切換與服務能力地圖，再從需求類型判斷資料庫、快取、訊息佇列、觀測平台與部署平台的選型方向。</p>
<h3 id="模組一資料庫與持久化"><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">模組一：資料庫與持久化</a></h3>
<p>整理 relational <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、embedded database、transaction、migration、repository <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">adapter</a> 與資料一致性。</p>
<h3 id="模組二快取與-redis"><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組二：快取與 Redis</a></h3>
<p>整理 cache aside、TTL、eviction、Redis data structure、distributed lock、pub/sub 與快取一致性。</p>
<h3 id="模組三訊息佇列與事件傳遞"><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三：訊息佇列與事件傳遞</a></h3>
<p>整理 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>、broker、ack/nack、retry、dead-letter queue、outbox、idempotency 與 consumer 設計。</p>
<h3 id="模組四可觀測性平台"><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">模組四：可觀測性平台</a></h3>
<p>整理 structured log aggregation、metrics、tracing、dashboard、alert 與操作診斷流程。</p>
<h3 id="模組五部署平台與網路入口"><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口</a></h3>
<p>整理 Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a> 與平台合約。</p>
<h3 id="模組六可靠性驗證流程"><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a></h3>
<p>整理 CI、load test、fuzz、chaos testing、測試環境與回歸驗證策略。</p>
<h3 id="模組七資安與資料保護"><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">模組七：資安與資料保護</a></h3>
<p>整理權限分級、伺服器防護、資料遮罩、傳輸保護、密鑰管理、稽核追蹤與資料匯出安全。</p>
<h3 id="模組八事故處理與復盤"><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">模組八：事故處理與復盤</a></h3>
<p>整理事故分級、指揮流程、止血回復、通訊節奏、復盤閉環與演練機制。</p>
<h3 id="模組九效能工程與容量規劃"><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">模組九：效能工程與容量規劃</a></h3>
<p>整理壓測理論、workload modeling、壓測工具選型、saturation discovery、瓶頸定位、容量規劃、成本邊界、效能可觀測性與改進閉環。跟模組六是 sibling 工程紀律：06 看「失敗模式如何被驗證」、09 看「正常負載如何被量化與規劃」。附 31 個 AWS / GCP / Azure 雲端公開實戰案例庫、覆蓋售票 flash-sale、極端可預期峰值、事件型峰值、surge、低延遲持續、持續成長六種負載形狀。</p>
<h3 id="模組十系統演進與遷移"><a href="/blog/backend/10-system-evolution/" data-link-title="模組十：系統演進與遷移" data-link-desc="處理服務拆分、跨服務重構、大型遷移與雲端切換的執行紀律 — 設計階段的選型判斷見模組零、執行階段的高風險變更收斂在本模組">模組十：系統演進與遷移</a></h3>
<p>整理服務拆分、跨服務重構、大型遷移與雲端切換的執行紀律。跟模組零是 sibling 設計／執行紀律：00 看「該選哪個服務」、10 看「決定要動之後、怎麼安全動手」。當前收三章：服務拆分的 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 邊界判讀</a> 與 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 執行 Runbook</a>、加上 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>；未來擴充跨服務 schema 演進、大型雲端遷移、基礎設施替換與容量重平衡。</p>
<h2 id="與語言教材的關係">與語言教材的關係</h2>
<p>Backend 教材提供跨語言的服務概念與操作語意。語言教材可以回連 Backend，說明特定語言如何實作 repository port、publisher port、cache interface、<a href="/blog/backend/knowledge-cards/middleware/" data-link-title="Request Middleware" data-link-desc="說明請求處理鏈中的共通攔截與前後置處理">Request Middleware</a>、async worker 或 observability boundary；Backend 章節本身應保持獨立，讓 Go、Python、Node.js、Java、C#、PHP、Rust 或其他後端語言都能使用同一套服務判斷。</p>
<p>Backend 章節討論具體服務時，應加入跨語言適配評估。這個評估讓讀者從執行環境、語言生態與抽象邊界理解服務使用方式，並取代特定語言教材作為前置依賴。</p>
<ol>
<li>這個服務需要語言端提供哪些抽象邊界，例如 interface、protocol、<a href="/blog/backend/knowledge-cards/adapter/" data-link-title="Integration Adapter" data-link-desc="說明外部系統接入層如何轉換介面與隔離差異">Integration Adapter</a>、<a href="/blog/backend/knowledge-cards/middleware/" data-link-title="Request Middleware" data-link-desc="說明請求處理鏈中的共通攔截與前後置處理">Request Middleware</a>、worker 或 client wrapper。</li>
<li>哪些執行環境特性會影響服務使用方式，例如 thread model、event loop、async/await、goroutine、process model、GC、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 或 cancellation。</li>
<li>哪些語言特性適合這個服務，例如明確 context 傳遞、型別化錯誤、成熟 ORM、生態套件、背景 worker 框架或 observability SDK。</li>
<li>哪些語言特性會形成風險，例如隱式全域狀態、阻塞 I/O 混入 event loop、連線池生命週期不清楚、例外處理邊界模糊或套件抽象過厚。</li>
<li>語言教材若需要示範實作，應由語言教材回連 Backend；Backend 則保持跨語言概念完整。</li>
</ol>
<p>這個方向讓 Backend 保持可重用：它先說明服務如何選型、如何使用、如何操作、如何承擔成本；各語言系列再各自說明如何接上這些服務。</p>
<h2 id="與案例的關係">與案例的關係</h2>
<p>Backend 案例應從服務需求出發。高併發、長連線、事件處理、資料庫、雲端基礎設施、資安與可觀測性都可以用跨語言案例說明；案例的責任是提供需求情境，服務章節的責任是說明後端能力本身。</p>
<h3 id="案例庫承接策略用-07--09-案例庫擴展模組內容">案例庫承接策略：用 07 + 09 案例庫擴展模組內容</h3>
<p>Backend 各模組（01-08）的章節寫作、把 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安資料保護</a> 跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">09 效能容量</a> 兩個案例庫當成 <em>跨模組材料庫</em> 使用、不是各自獨立寫案例。寫每篇技術章節時、先盤點 07/09 哪些案例能 <em>承接</em> 該章節的主題、再決定要 link 哪些、要重述哪些、要新建案例補哪些缺口。</p>
<p>兩個案例庫的責任分工：</p>
<ul>
<li><strong>09 案例庫</strong>：以 <em>效能 / 容量 / 規模</em> 為主軸的真實工程案例（35+ 個）、涵蓋 DynamoDB 億級 RPS、Aurora 跨 region、Cosmos DB Black Friday、Spanner planetary scale、KeyDB cross-cloud、Tixcraft flash sale 等。給技術章節提供「規模上限參考」、「設計取捨對照」、「失敗模式對照」。</li>
<li><strong>07 案例庫</strong>：分主案例（10+ 個）跟紅隊案例（49+ 個、分 data-exfiltration / edge-exposure / identity-access / supply-chain 四類）。給技術章節提供「攻擊鏈到該技術層的路徑」、「事故代價對照」、「控制面失效模式」。</li>
</ul>
<p>承接的最小要求：</p>
<ol>
<li><strong>每篇技術章節都要有「案例對照」段</strong>：表格 + 每案的「跟本章關係」一句話、不是純列連結。</li>
<li><strong>案例的引用要在敘事中分散</strong>：不只在文末列表、文中討論特定模式時就 inline link、讓讀者邊讀邊看到實證。</li>
<li><strong>盤點未承接案例是 backlog 項目</strong>：寫完一個模組、跑一次 grep 看 09 / 07 紅隊案例庫剩下哪些沒 link、判斷是否該補。</li>
</ol>
<p>判斷案例是否該承接的條件：</p>
<ul>
<li><strong>直接相關</strong>：案例的核心議題就是本章技術（例：DynamoDB partition key → 1.10 KV 容量）→ 必引</li>
<li><strong>對照相關</strong>：案例展示了 <em>對立</em> 設計取捨（例：SeatGeek 明確排隊 vs Tixcraft 隱性緩衝）→ 強建議引、能展示判讀</li>
<li><strong>失敗模式相關</strong>：案例揭露了本章技術的失敗代價（例：MOVEit SQL injection → 1.5 紅隊）→ 必引</li>
<li><strong>間接相關</strong>：案例提到本章技術但不是核心（例：Niantic 提到 DB 但主要是 K8s scaling）→ 可不引、避免模板化</li>
</ul>
<p>不承接的條件：</p>
<ul>
<li>案例本質屬於別的模組（例：Snap KeyDB 屬 02 cache、不屬 01 DB）</li>
<li>案例對本章只是 passing reference、沒 substantive 對照價值</li>
<li>引用會把表格塞爆但每個只有一句話的形式化承接（情境優先於模板原則、見上）</li>
</ul>
<p>實例：<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 已承接 27/35 個 09 案例 + 6/10 個 07 主案例 + 6 個 07 紅隊案例。剩下未承接的多屬其他模組（K8s 屬 05、cache 屬 02、edge VPN CVE 屬 07 自身）。各模組寫作完成後做同類盤點、能確認案例庫被當成共享資源、不重複勞動、也不漏接。</p>
<h2 id="教學寫作方向">教學寫作方向</h2>
<p>Backend 教學文章以敘事說明為主。每篇先回答「這個能力在真實服務承擔什麼責任」，再展開判讀訊號、風險擴散、決策順序與回寫路由，讓讀者能沿著情境推導出操作判斷。</p>
<p>檢查清單與欄位表在本系列是輔助工具，不是文章主體。它們的責任是收斂判讀，不是取代推理；每一個條目都應回到案例脈絡，說明為何成立、失效時會發生什麼，以及下一步要交給哪個模組。</p>
<p>寫作時優先保留因果鏈：觀測到什麼、如何判讀、做了什麼決策、承擔什麼代價、後續如何修正。當文章只剩條列而缺少因果，讀者會知道要檢查什麼，卻不知道為什麼要這樣檢查。</p>
<h2 id="補充知識卡片入口">補充知識卡片入口</h2>
<p>下列卡片目前尚未從教學文章直接引用，先放在這裡作為補充入口。</p>
<table>
  <thead>
      <tr>
          <th>卡片</th>
          <th>入口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/authentication-middleware/" data-link-title="Authentication Middleware" data-link-desc="說明請求進入 handler 前如何完成身份驗證">Authentication Middleware</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/authorization-middleware/" data-link-title="Authorization Middleware" data-link-desc="說明請求進入 handler 前如何完成權限判斷">Authorization Middleware</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/bopla/" data-link-title="BOPLA" data-link-desc="說明屬性層授權缺失如何讓使用者讀寫不該暴露的欄位">BOPLA</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/bucket/" data-link-title="Bucket" data-link-desc="說明 histogram 分桶如何決定觀測解析度與成本">Bucket</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/cache-prefetching/" data-link-title="Cache Prefetching" data-link-desc="說明系統如何在資料被需要前預先載入快取">Cache Prefetching</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/cold-start/" data-link-title="Cold Start" data-link-desc="說明服務或快取剛啟動時尚未累積狀態造成的延遲與壓力">Cold Start</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/competing-consumers/" data-link-title="Competing Consumers" data-link-desc="說明多個 consumer 共同處理同一個 queue 如何提高吞吐與影響順序">Competing Consumers</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/consumer-capacity/" data-link-title="Consumer Capacity" data-link-desc="說明 consumer 群組每秒能穩定處理多少工作">Consumer Capacity</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">Correctness Check</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-completeness/" data-link-title="Data Completeness" data-link-desc="說明資料是否完整到足以支持查詢、遷移與決策">Data Completeness</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">Dual Write</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/endpoint/" data-link-title="Service Endpoint" data-link-desc="說明服務如何對外暴露可被路由與存取的入口">Service Endpoint</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/http-client/" data-link-title="HTTP Client" data-link-desc="說明服務呼叫外部 HTTP 依賴時需要管理 timeout、連線與重試">HTTP Client</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/in-flight-message/" data-link-title="In-Flight Message" data-link-desc="說明已交給 consumer 但尚未完成確認的訊息狀態">In-Flight Message</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">Migration Gate</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/notification-adapter/" data-link-title="Notification Adapter" data-link-desc="說明通知通道如何把 domain event 轉成外部傳遞格式">Notification Adapter</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/observability-middleware/" data-link-title="Observability Middleware" data-link-desc="說明請求進入 handler 前後如何補上觀測欄位">Observability Middleware</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/online-migration/" data-link-title="Online Migration" data-link-desc="說明服務持續接流量時如何完成資料或 schema 遷移">Online Migration</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">Projection</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/publisher-confirm/" data-link-title="Publisher Confirm" data-link-desc="說明 producer 如何確認 broker 已接收並承擔訊息">Publisher Confirm</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/queue-contract/" data-link-title="Queue Contract" data-link-desc="說明佇列工作在重試、確認與重複投遞上的約定">Queue Contract</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/redelivery-loop/" data-link-title="Redelivery Loop" data-link-desc="說明同一訊息反覆投遞失敗如何消耗 consumer 容量">Redelivery Loop</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/request-response-protocol/" data-link-title="Request/Response Protocol" data-link-desc="說明同步請求如何在 client 與 service 之間對齊互動規則">Request/Response Protocol</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/rollback-rehearsal/" data-link-title="Rollback Rehearsal" data-link-desc="說明如何在正式事故前演練回滾流程">Rollback Rehearsal</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/routing-rule/" data-link-title="Routing Rule" data-link-desc="說明訊息系統如何依規則把訊息送到不同處理路徑">Routing Rule</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/security-misconfiguration/" data-link-title="Security Misconfiguration" data-link-desc="說明錯誤設定如何讓安全控制失效或暴露內部能力">Security Misconfiguration</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">Shadow Read</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/socket/" data-link-title="Socket" data-link-desc="說明 network socket 如何成為 application 與網路之間的資料傳輸邊界">Socket</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/soft-ttl/" data-link-title="Soft TTL" data-link-desc="說明資料進入刷新期後仍可短暫使用以降低 stampede">Soft TTL</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/ssrf/" data-link-title="SSRF" data-link-desc="說明伺服器端請求被濫用時如何存取內部網路或 metadata 服務">SSRF</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/stream-pipeline/" data-link-title="Stream Pipeline" data-link-desc="說明連續資料流經多個處理階段時如何管理吞吐、順序與 backpressure ">Stream Pipeline</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/unacked-message/" data-link-title="Unacked Message" data-link-desc="說明 broker 已投遞但尚未收到 consumer 確認的訊息">Unacked Message</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/unrestricted-resource-consumption/" data-link-title="Unrestricted Resource Consumption" data-link-desc="說明缺少資源限制如何讓 API 被濫用或拖垮">Unrestricted Resource Consumption</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/validation-middleware/" data-link-title="Validation Middleware" data-link-desc="說明請求進入 handler 前如何完成共通驗證">Validation Middleware</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/webhook-protocol/" data-link-title="Webhook Protocol" data-link-desc="說明外部回呼如何對齊簽章、重試與 payload 語意">Webhook Protocol</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/write-behind-cache/" data-link-title="Write-Behind Cache" data-link-desc="說明先寫快取再非同步寫入正式來源的風險與用途">Write-Behind Cache</a></td>
          <td>補充入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/write-through-cache/" data-link-title="Write-Through Cache" data-link-desc="說明寫入時同步更新快取與正式來源的策略">Write-Through Cache</a></td>
          <td>補充入口</td>
      </tr>
  </tbody>
</table>
<h2 id="前置知識卡片規範">前置知識卡片規範</h2>
<p>Backend 文章中的術語只要會影響讀者理解或實作判斷，就應優先抽成前置知識卡片，不以「是否高頻出現」作為必要條件。Source of truth、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、schema migration、timeout、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a>、<a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">exponential backoff</a>、jitter、retry storm、<a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a>、<a href="/blog/backend/knowledge-cards/transient-failure/" data-link-title="Transient Failure" data-link-desc="說明暫時性故障如何影響重試、告警與使用者回應">transient failure</a>、<a href="/blog/backend/knowledge-cards/partial-failure/" data-link-title="Partial Failure" data-link-desc="說明分散式系統中部分依賴失效時如何保留整體可用性">partial failure</a>、<a href="/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">cascading failure</a>、load shedding、<a href="/blog/backend/knowledge-cards/token-bucket/" data-link-title="Token Bucket" data-link-desc="說明 token bucket 如何用配額與補充速率控制流量">token bucket</a>、<a href="/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency isolation</a>、bulkhead、fallback、<a href="/blog/backend/knowledge-cards/fail-fast/" data-link-title="Fail Fast" data-link-desc="說明已知無法完成時快速回應如何保護資源與上游判斷">fail fast</a>、<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a>、TTL、eviction、broker、consumer lag、dead-letter queue、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a>、<a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">重複投遞</a>、idempotency、outbox、backpressure、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a>、log schema、metrics、trace context、SLO、authorization、data masking、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a>、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">website certificate lifecycle</a>、<a href="/blog/backend/knowledge-cards/certificate-rotation-renewal/" data-link-title="Certificate Rotation and Renewal" data-link-desc="說明網站憑證如何安全續期與輪替以避免停機">certificate rotation and renewal</a>、<a href="/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">certificate revocation</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>、<a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機</a>、readiness 與 graceful shutdown 都是 domain knowhow；它們需要說明系統責任、產品後果、操作訊號與排障方式。</p>
<p>每張卡片應維持一個概念，並至少包含概念位置、可觀察訊號、接近真實網路服務的例子與設計責任。卡片內容要能獨立閱讀；定義只是一個入口，完整卡片要讓讀者理解這個概念在事故、擴容、部署或資料修復時會如何影響決策。</p>
<p>卡片與技術文章必須分離。卡片負責名詞與共同語言；技術文章負責情境判讀、設計取捨與決策順序。章節文章使用「情境、判讀流程、風險代價、設計取捨、最低控制面」結構，並以卡片連結補術語背景，不在文章區重寫卡片格式。</p>
<h2 id="暫定章節來源">暫定章節來源</h2>
<p>目前 Backend 目錄先承接多個後端語言都會遇到的外部實作議題：</p>
<ul>
<li>資料庫 transaction、schema migration、isolation level</li>
<li>durable queue、outbox、idempotency store</li>
<li>Redis、distributed cache、presence store</li>
<li>metrics、tracing、log aggregation、OpenTelemetry</li>
<li>Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> runtime</li>
<li>CI、load test、fuzz、chaos testing</li>
<li>identity、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a>、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a>、<a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">website certificate lifecycle</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
<li><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a>、command model、<a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a>、<a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a>、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a></li>
<li>workload modeling、<a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">saturation point</a>、<a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method</a>、<a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a>、<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/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">headroom budget</a>、<a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>、<a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">performance budget</a>、<a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">latency budget</a></li>
</ul>
<p>後續撰寫任何語言教材時，凡是涉及具體外部服務操作、部署平台設定或產品選型，都應優先放到 Backend；語言章節只保留足夠說明抽象邊界的最小背景。</p>
<h2 id="服務分類開頭規範">服務分類開頭規範</h2>
<p>每個 Backend 服務分類的 <code>_index.md</code> 必須先回答選型問題，再進入實作細節。服務分類與服務實體章節都要把成本權衡視為固定段落；資安限制、流量穩定性、伺服器成本、人力成本與機會成本會共同決定某個服務是否值得引入。</p>
<ol>
<li>說明這類服務解決哪一種工程問題</li>
<li>列出可觀察需求訊號</li>
<li>舉接近真實網路服務的例子</li>
<li>比較同質服務或相近能力的差異</li>
<li>討論資安限制下的成本權衡</li>
<li>討論選擇此方案的機會成本與替代方案</li>
<li>指向後續實作章節</li>
</ol>
<p>表格只能作為索引。只要表格列出分類、工具或服務能力，後面就要補對應段落，說明如何辨識、何時選擇、與其他同質服務差在哪裡。</p>
<h2 id="服務實體章節規範">服務實體章節規範</h2>
<p>每篇討論具體服務實體的章節，例如 PostgreSQL、Redis、RabbitMQ、Kafka、Prometheus、OpenTelemetry、Kubernetes、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 或 <a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM</a>，都必須包含「成本權衡與機會成本」段落。這個段落至少回答：</p>
<ol>
<li>這個服務降低哪一種風險</li>
<li>在資安限制下會增加哪些設計、審核、遮罩、加密、稽核或權限成本</li>
<li>在高流量、尖峰、長連線或大量資料下會增加哪些伺服器與雲端成本</li>
<li>團隊需要承擔哪些維護、監控、升級、備份、演練與事故處理成本</li>
<li>若選擇更簡單方案，會承擔哪些風險；若選擇更完整方案，會延後哪些產品或工程工作</li>
<li>什麼條件出現時，原本的選型結論應該被重新評估</li>
</ol>
<h2 id="跨語言適配規範">跨語言適配規範</h2>
<p>每篇具體服務實體章節都必須包含「跨語言適配評估」段落。這個段落不連到特定語言教材，而是從語言特性評估使用風險：</p>
<ol>
<li>同步 thread-based runtime 如何管理 connection pool、blocking I/O 與 timeout。</li>
<li>async/event-loop runtime 如何避免 blocking client、長時間 CPU work 與 backpressure 失控。</li>
<li>goroutine 或 lightweight task runtime 如何限制下游資源，避免把廉價並發轉成昂貴連線壓力。</li>
<li>強型別語言如何表達 schema、錯誤分類與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test。</li>
<li>動態語言如何用 protocol、typing、fixture、runtime validation 或 framework convention 保護邊界。</li>
<li>語言生態的 ORM、broker client、observability SDK、security <a href="/blog/backend/knowledge-cards/security-middleware/" data-link-title="Security Middleware" data-link-desc="說明請求進入 handler 前如何完成共通安全控制">Security Middleware</a> 是否成熟，是否會隱藏重要操作語意。</li>
</ol>
<hr>
<p><em>文件版本：v0.1.0</em>
<em>最後更新：2026-04-22</em>
<em>系列狀態：分類索引建立中</em></p>
]]></content:encoded></item><item><title>Trace Context</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/</guid><description>&lt;p>Trace context 的核心概念是「讓同一個 request 在跨服務呼叫中保持同一條追蹤線」。它包含 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>（標識整條 trace）、span id（標識上游 span）與 trace flags（sampling 決策），讓下游服務建立的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 能歸屬同一條 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace context 是跨服務診斷的關聯層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a> 互補 — correlation id 關聯業務流程、trace context 關聯技術呼叫路徑。它的傳遞機制決定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 能不能完整串起 — context 斷掉的地方，trace 就從「完整路徑」退化成需要人工拼接的局部紀錄。&lt;/p>
&lt;p>W3C Trace Context 標準定義了 HTTP 的傳遞格式：&lt;code>traceparent&lt;/code> header 帶 version + trace id + parent span id + trace flags，&lt;code>tracestate&lt;/code> header 帶 vendor-specific 附加資訊。OpenTelemetry SDK 預設使用 W3C 格式。部分 vendor 有自己的 header（Datadog 用 &lt;code>x-datadog-trace-id&lt;/code>、AWS X-Ray 用 &lt;code>X-Amzn-Trace-Id&lt;/code>），跨 vendor 時需要在 collector 層轉換。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 trace context 的訊號是延遲或錯誤跨越多個服務。Checkout 變慢時，trace context 讓 tracing 系統把 API gateway、order service、payment service、database query 的 span 串成一條路徑，在 waterfall view 中直接看到時間花在哪。&lt;/p>
&lt;p>Context 在 HTTP call、gRPC metadata、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> message header 上傳遞。Queue 邊界的 propagation 比 HTTP 複雜 — consumer 可能在 producer 之後很久才消費，context 的時間跨度從毫秒擴大到分鐘。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace context 設計要處理四個邊界的傳遞：HTTP / gRPC（SDK auto-instrumentation 自動處理）、queue（需要 instrumented client 注入 message header）、thread pool（需要語言級的 context 傳播機制）、background job（需要在 job 啟動時建立 root span）。&lt;/p>
&lt;p>斷鏈的常見原因和修復策略見 &lt;a href="https://tarrragon.github.io/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 link&lt;/a>。Sampling 決策跟 trace context 的關係見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Trace context 的核心概念是「讓同一個 request 在跨服務呼叫中保持同一條追蹤線」。它包含 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>（標識整條 trace）、span id（標識上游 span）與 trace flags（sampling 決策），讓下游服務建立的 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 能歸屬同一條 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace context 是跨服務診斷的關聯層，跟 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> 互補 — correlation id 關聯業務流程、trace context 關聯技術呼叫路徑。它的傳遞機制決定 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 能不能完整串起 — context 斷掉的地方，trace 就從「完整路徑」退化成需要人工拼接的局部紀錄。</p>
<p>W3C Trace Context 標準定義了 HTTP 的傳遞格式：<code>traceparent</code> header 帶 version + trace id + parent span id + trace flags，<code>tracestate</code> header 帶 vendor-specific 附加資訊。OpenTelemetry SDK 預設使用 W3C 格式。部分 vendor 有自己的 header（Datadog 用 <code>x-datadog-trace-id</code>、AWS X-Ray 用 <code>X-Amzn-Trace-Id</code>），跨 vendor 時需要在 collector 層轉換。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 trace context 的訊號是延遲或錯誤跨越多個服務。Checkout 變慢時，trace context 讓 tracing 系統把 API gateway、order service、payment service、database query 的 span 串成一條路徑，在 waterfall view 中直接看到時間花在哪。</p>
<p>Context 在 HTTP call、gRPC metadata、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> message header 上傳遞。Queue 邊界的 propagation 比 HTTP 複雜 — consumer 可能在 producer 之後很久才消費，context 的時間跨度從毫秒擴大到分鐘。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace context 設計要處理四個邊界的傳遞：HTTP / gRPC（SDK auto-instrumentation 自動處理）、queue（需要 instrumented client 注入 message header）、thread pool（需要語言級的 context 傳播機制）、background job（需要在 job 啟動時建立 root span）。</p>
<p>斷鏈的常見原因和修復策略見 <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 link</a>。Sampling 決策跟 trace context 的關係見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略</a>。</p>
]]></content:encoded></item><item><title>DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>對帳跑出一筆異常：用戶錢包餘額扣了 100 元、但對應訂單沒建立。追 log 發現 application 先 &lt;code>PutItem&lt;/code> 扣餘額、再 &lt;code>PutItem&lt;/code> 建訂單、兩步之間 process 被 OOM kill、第二步沒跑完。另一個系統反向情境：秒殺活動庫存剩 1、兩個請求同時讀到「剩 1」、各自 &lt;code>PutItem&lt;/code> 扣成 0、實際賣出 2 件。兩個 production 痛點指向同一件事 — DynamoDB 預設的單筆寫入沒有跨 item 原子性、也沒有「讀到的值寫回時還沒被改」的保證。本文展開 DynamoDB 提供的三層寫保護：跨 item transaction、單 item conditional write、version-based optimistic locking。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>寫一致性前提：先確認 workload 適配 DynamoDB&lt;/strong>：本篇假設 workload 已通過 DynamoDB 適配 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。寫一致性是 &lt;em>已選 DynamoDB&lt;/em> 後的操作層議題；若 workload 需要頻繁跨多表多列複雜交易、那是 relational 的主場、應先回頭問 DynamoDB 是否選錯。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制三層寫保護">核心機制：三層寫保護&lt;/h2>
&lt;p>DynamoDB 的寫一致性由三種粒度不同的工具組成 — 單 item 寫、conditional write、跨 item transaction，三者解的問題與成本各異，不是單一 ACID 開關：&lt;/p>
&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>單 item 寫&lt;/td>
 &lt;td>一筆 item 的 put / update / delete&lt;/td>
 &lt;td>單 item&lt;/td>
 &lt;td>1x WCU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Conditional write&lt;/td>
 &lt;td>只在條件成立時才寫（防覆蓋、防重複）&lt;/td>
 &lt;td>單 item + 前置條件&lt;/td>
 &lt;td>1x WCU（條件不成立也計費）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TransactWriteItems&lt;/td>
 &lt;td>多筆 item 一起成功或一起失敗&lt;/td>
 &lt;td>跨 item（同 region / account）&lt;/td>
 &lt;td>2x WCU（prepare + commit 兩階段）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>TransactWriteItems 的工程語意&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一次 transaction 最多含若干個 action（put / update / delete / condition check）— 上限屬 vendor 規格、實作時 cross-verify AWS doc 當前數字&lt;/li>
&lt;li>全成功或全失敗：任一 action 的 condition 不成立、整個 transaction roll back、拋 &lt;code>TransactionCanceledException&lt;/code> 帶 &lt;code>CancellationReasons&lt;/code>&lt;/li>
&lt;li>不跨 region、不跨 account：transaction 只在單一 region 單一 account 內成立、Global Tables 多 region 寫不享有跨 region transaction（對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&amp;#43; 跨裝置同步的對照">global-tables-conflict&lt;/a>）&lt;/li>
&lt;li>兩階段（prepare + commit）導致 2x capacity 消耗 — 這是 transaction 不能濫用的成本根源&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「TransactWriteItems 100 action 上限」、「transaction 2x WCU」這些具體數字屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc 當前值。本文不含對應 production case 揭露的 transaction 規模數字。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>對帳跑出一筆異常：用戶錢包餘額扣了 100 元、但對應訂單沒建立。追 log 發現 application 先 <code>PutItem</code> 扣餘額、再 <code>PutItem</code> 建訂單、兩步之間 process 被 OOM kill、第二步沒跑完。另一個系統反向情境：秒殺活動庫存剩 1、兩個請求同時讀到「剩 1」、各自 <code>PutItem</code> 扣成 0、實際賣出 2 件。兩個 production 痛點指向同一件事 — DynamoDB 預設的單筆寫入沒有跨 item 原子性、也沒有「讀到的值寫回時還沒被改」的保證。本文展開 DynamoDB 提供的三層寫保護：跨 item transaction、單 item conditional write、version-based optimistic locking。</p>
<blockquote>
<p><strong>寫一致性前提：先確認 workload 適配 DynamoDB</strong>：本篇假設 workload 已通過 DynamoDB 適配 4 軸（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）— 判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。寫一致性是 <em>已選 DynamoDB</em> 後的操作層議題；若 workload 需要頻繁跨多表多列複雜交易、那是 relational 的主場、應先回頭問 DynamoDB 是否選錯。</p></blockquote>
<h2 id="核心機制三層寫保護">核心機制：三層寫保護</h2>
<p>DynamoDB 的寫一致性由三種粒度不同的工具組成 — 單 item 寫、conditional write、跨 item transaction，三者解的問題與成本各異，不是單一 ACID 開關：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>解的問題</th>
          <th>原子性範圍</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 item 寫</td>
          <td>一筆 item 的 put / update / delete</td>
          <td>單 item</td>
          <td>1x WCU</td>
      </tr>
      <tr>
          <td>Conditional write</td>
          <td>只在條件成立時才寫（防覆蓋、防重複）</td>
          <td>單 item + 前置條件</td>
          <td>1x WCU（條件不成立也計費）</td>
      </tr>
      <tr>
          <td>TransactWriteItems</td>
          <td>多筆 item 一起成功或一起失敗</td>
          <td>跨 item（同 region / account）</td>
          <td>2x WCU（prepare + commit 兩階段）</td>
      </tr>
  </tbody>
</table>
<p><strong>TransactWriteItems 的工程語意</strong>：</p>
<ul>
<li>一次 transaction 最多含若干個 action（put / update / delete / condition check）— 上限屬 vendor 規格、實作時 cross-verify AWS doc 當前數字</li>
<li>全成功或全失敗：任一 action 的 condition 不成立、整個 transaction roll back、拋 <code>TransactionCanceledException</code> 帶 <code>CancellationReasons</code></li>
<li>不跨 region、不跨 account：transaction 只在單一 region 單一 account 內成立、Global Tables 多 region 寫不享有跨 region transaction（對應 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a>）</li>
<li>兩階段（prepare + commit）導致 2x capacity 消耗 — 這是 transaction 不能濫用的成本根源</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「TransactWriteItems 100 action 上限」、「transaction 2x WCU」這些具體數字屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc 當前值。本文不含對應 production case 揭露的 transaction 規模數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>。</p>
<h2 id="conditional-write最便宜的一致性工具">Conditional Write：最便宜的一致性工具</h2>
<p>跨 item transaction 之前、先看單 item conditional write 能不能解。多數「race condition」其實是單 item 問題、不需要 transaction 的 2x 成本。</p>
<p>ConditionExpression 在寫入前檢查條件、條件不成立則拒絕寫入並拋 <code>ConditionalCheckFailedException</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 防重複建立：只有 item 不存在時才寫</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">Item</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span> <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;created&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;attribute_not_exists(PK)&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 防超賣：只有庫存 &gt; 0 時才扣</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">table</span><span class="o">.</span><span class="n">update_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;SKU#</span><span class="si">{</span><span class="n">sku</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;STOCK&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">UpdateExpression</span><span class="o">=</span><span class="s2">&#34;SET stock = stock - :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;stock &gt;= :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;:one&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>第二個例子是關鍵：<code>update_item</code> 帶 condition 是 <em>原子的 read-modify-write</em>。DynamoDB 在單 item 上保證「條件檢查 + 寫入」不會被其他寫入插隊。前述「兩個請求同時讀到剩 1」的超賣問題、用單 item conditional update 即可解、不需要 transaction。</p>
<h2 id="optimistic-locking跨讀寫週期的保護">Optimistic Locking：跨讀寫週期的保護</h2>
<p>Conditional write 解單次寫的 race；當 application 需要「讀出來、業務邏輯運算、再寫回」、且運算期間不能被別人改、用 version-based optimistic locking。</p>
<p>機制是在 item 上維護一個 <code>version</code> attribute、寫回時用 condition 確認 version 沒被改過：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">update_with_optimistic_lock</span><span class="p">(</span><span class="n">pk</span><span class="p">,</span> <span class="n">new_balance</span><span class="p">,</span> <span class="n">expected_version</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">update_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;WALLET&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">UpdateExpression</span><span class="o">=</span><span class="s2">&#34;SET balance = :b, version = version + :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;version = :expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;:b&#34;</span><span class="p">:</span> <span class="n">new_balance</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="s2">&#34;:one&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="s2">&#34;:expected&#34;</span><span class="p">:</span> <span class="n">expected_version</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">)</span></span></span></code></pre></div><p>讀出時拿到 <code>version=5</code>、運算後寫回時 condition 是 <code>version = 5</code>；若期間別人已寫成 <code>version=6</code>、condition 失敗、application 收到 <code>ConditionalCheckFailedException</code>、retry 整個讀-算-寫週期。</p>
<p>optimistic 的代價是衝突時要重試、不是阻塞等待。高衝突 workload（同一 item 大量並發寫）optimistic locking 會 retry 風暴、這時要回頭問資料模型 — 把熱點 item 拆開、或改用單 item atomic counter（<code>ADD</code>）避免 read-modify-write。</p>
<blockquote>
<p><strong>Scope warning</strong>：optimistic locking 是通用並發控制 pattern、DynamoDB 用 ConditionExpression 實作；本段機制描述屬 vendor 規格 + 通用工程知識、非 production case 揭露。</p></blockquote>
<h2 id="idempotencytransaction-的重複提交保護">Idempotency：transaction 的重複提交保護</h2>
<p>分散式系統的寫入會重試（network timeout、client retry）。同一筆 transaction 重送兩次、不能扣兩次款。DynamoDB transaction 提供 <code>ClientRequestToken</code> 做 dedup：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">transact_write_items</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">ClientRequestToken</span><span class="o">=</span><span class="n">request_id</span><span class="p">,</span>  <span class="c1"># 同 token 在 dedup window 內視為同一次</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">TransactItems</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;Update&#34;</span><span class="p">:</span> <span class="p">{</span>  <span class="c1"># 扣錢包</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="s2">&#34;TableName&#34;</span><span class="p">:</span> <span class="s2">&#34;wallet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="s2">&#34;Key&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;S&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">uid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;UpdateExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;SET balance = balance - :amt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="s2">&#34;ConditionExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;balance &gt;= :amt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="s2">&#34;ExpressionAttributeValues&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;:amt&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;N&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">amount</span><span class="p">)}},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;Put&#34;</span><span class="p">:</span> <span class="p">{</span>  <span class="c1"># 建訂單</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="s2">&#34;TableName&#34;</span><span class="p">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="s2">&#34;Item&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;S&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">},</span> <span class="s2">&#34;amount&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;N&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">amount</span><span class="p">)}},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s2">&#34;ConditionExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;attribute_not_exists(PK)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">}},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>同一個 <code>ClientRequestToken</code> 在 dedup window 內重送、DynamoDB 視為同一次、不會重複執行。這解掉開場的「扣款成功但訂單沒建」問題：兩個 action 在同一 transaction、要嘛都成、要嘛都不成；client 重試帶同 token、不會重複扣款。</p>
<blockquote>
<p><strong>Scope warning</strong>：「ClientRequestToken dedup window 約 10 分鐘」屬 AWS vendor 規格、實作時 cross-verify 官方 doc；application 層仍應有自己的 idempotency key 設計、不依賴 vendor dedup window 當唯一防線（對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 卡）。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從一致性需求判讀到工具選擇的 6 步流程。</p>
<h4 id="step-1分類寫入的一致性需求">Step 1：分類寫入的一致性需求</h4>
<p>每個寫入路徑標記它真正需要的保護：</p>
<ul>
<li>單筆獨立寫、無前置條件 → 單 item put / update（最便宜）</li>
<li>單筆寫但要防覆蓋 / 防重複 / 防超賣 → 單 item conditional write</li>
<li>讀-算-寫週期、期間不能被改 → version optimistic locking</li>
<li>多筆 item 必須一起成功或失敗 → TransactWriteItems</li>
</ul>
<h4 id="step-2先用-conditional-write-解單-item-race">Step 2：先用 conditional write 解單 item race</h4>
<p>把「需要 transaction」當成最後選項。多數 race condition 是單 item 問題、conditional update 的 atomic read-modify-write 已足夠、成本 1x 而非 2x。</p>
<h4 id="step-3跨-item-才上-transaction">Step 3：跨 item 才上 transaction</h4>
<p>只有「多筆 item 的修改必須綁在一起」才用 TransactWriteItems。例：扣錢包 + 建訂單 + 寫流水帳三筆綁定。寫進 transaction 的 item 數量越少越好、每多一個 item 多一份 2x 成本。</p>
<h4 id="step-4加-idempotency-token">Step 4：加 idempotency token</h4>
<p>所有會被 client 重試的 transaction 帶 <code>ClientRequestToken</code>；token 用業務層的唯一鍵（order_id / request_id）、不要用隨機值（隨機值每次重試都不同、dedup 失效）。</p>
<h4 id="step-5處理失敗例外">Step 5：處理失敗例外</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">botocore.exceptions</span> <span class="kn">import</span> <span class="n">ClientError</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">client</span><span class="o">.</span><span class="n">transact_write_items</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">except</span> <span class="n">ClientError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">code</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;Error&#34;</span><span class="p">][</span><span class="s2">&#34;Code&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="n">code</span> <span class="o">==</span> <span class="s2">&#34;TransactionCanceledException&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">reasons</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;CancellationReasons&#34;</span><span class="p">]</span>  <span class="c1"># 逐 action 失敗原因</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># 區分 ConditionalCheckFailed（業務拒絕、不重試）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="c1"># vs TransactionConflict / ThrottlingError（可重試）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">elif</span> <span class="n">code</span> <span class="o">==</span> <span class="s2">&#34;ConditionalCheckFailedException&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">pass</span>  <span class="c1"># 單 item condition 失敗、業務層決定</span></span></span></code></pre></div><p>關鍵：<code>ConditionalCheckFailed</code> 是 <em>業務拒絕</em>（庫存不足、訂單已存在）、不該不分原因一律重試；<code>TransactionConflict</code> / <code>ThrottlingError</code> 才是可重試的 transient error。混為一談會把「庫存真的不夠」當成 transient 一直重試。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 驗證 conditional write 真的擋住併發</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 啟兩個並發 update 扣同一庫存、確認只有一個成功、另一個拋 ConditionalCheckFailed</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">update_item</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">ReturnValues</span><span class="o">=</span><span class="s2">&#34;UPDATED_NEW&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;Attributes&#34;</span><span class="p">])</span>  <span class="c1"># 確認 version / stock 變化符合預期</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：transaction 本身全成全敗、無 partial state 需要 rollback；但 application 層若在 transaction 外還有副作用（送通知、呼叫外部 API）、那些不在 transaction 保護內、要另行設計補償。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1用-transaction-取代本該單-item-的寫">Case 1：用 transaction 取代本該單 item 的寫</h4>
<p>team 把所有寫入都包進 TransactWriteItems「保險」、cost 翻倍、且 transaction 有 throughput 上限比單寫低。修法：transaction 只用於真正跨 item 綁定的場景；單 item 用 conditional write。</p>
<h4 id="case-2optimistic-lock-在高衝突-item-上-retry-風暴">Case 2：optimistic lock 在高衝突 item 上 retry 風暴</h4>
<p>熱點 item（如全站唯一的計數器）大量並發寫、version condition 不斷失敗、application retry 風暴、latency 爆炸。修法：高衝突計數改用 atomic <code>ADD</code>（單 item 原子累加、不需 read-modify-write）；或把計數 shard 成多個 item 分散寫入。</p>
<h4 id="case-3idempotency-token-用隨機值">Case 3：idempotency token 用隨機值</h4>
<p>這個 case 的失敗代價跟其他踩雷不同層級。Case 1（cost 翻倍）、Case 2（retry 風暴）、Case 5（跨 region 誤解）都可以在發現後調整設定或改資料模型補救；idempotency token 用隨機值導致的重複扣款是 <em>財務不可逆</em> — 每次 client retry 產生新 token、dedup 完全失效、同一筆付款被執行多次、錢已經從用戶帳戶扣走、要靠對帳發現後人工退款，且退款流程本身又是另一條容易出錯的補償路徑。修法：token 綁業務唯一鍵（order_id / payment_id）、同一筆業務操作的所有重試共用同一 token；且不只依賴 DynamoDB 的 dedup window（有時效上限），application 層自己也維護 idempotency 記錄當第二道防線（對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 卡）。涉及金流的寫入，這道防線要在上線前用「同一 token 重送 N 次只執行一次」的測試明確驗證。</p>
<h4 id="case-4把-conditionalcheckfailed-當-transient-error-重試">Case 4：把 ConditionalCheckFailed 當 transient error 重試</h4>
<p>庫存真的為 0、condition 永遠失敗、application 無限重試打爆 capacity。修法：例外分流 — 業務拒絕（ConditionalCheckFailed）回報給呼叫端、transient error（throttle / conflict）才 backoff retry。</p>
<h4 id="case-5以為-transaction-跨-region-有效">Case 5：以為 transaction 跨 region 有效</h4>
<p>Global Tables 多 region 部署、誤以為 TransactWriteItems 在跨 region 也原子。實際 transaction 只在單 region 成立、跨 region 是 last-writer-wins（對應 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a>）。修法：跨 region 一致性需求不能靠 transaction、要重新設計資料 ownership（單一 region 為 write authority）。</p>
<p><strong>Anti-recommendation</strong>：寫入無併發競爭、或業務本身可接受最終一致（各 message_id 獨立的訊息事件即屬此類）→ 不要為了求保險而加 transaction；transaction 的 2x 成本只在真正需要跨 item 原子性時才值得。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>TransactionConflict</code>：transaction 因併發衝突取消的次數、持續高代表熱點 item 競爭</li>
<li><code>ConditionalCheckFailedRequests</code>：condition 失敗次數、區分業務拒絕 vs 設計問題</li>
<li><code>ThrottledRequests</code>：transaction 因 capacity 不足被限流、transaction 的 2x 消耗更容易撞上限</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>TransactionConflict</code> 持續上升 → 資料模型有熱點、考慮拆 item 或改 atomic counter</li>
<li><code>ConditionalCheckFailed</code> 突然飆高 → 可能是業務異常（大量重複請求 / 攻擊）、也可能是 application 邏輯把 version 算錯</li>
<li>transaction 的 capacity 用量按 2x 計、容量規劃要把 transaction 比例算進去</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 transaction metric 數字；上述 metric 名稱與判讀屬 vendor 規格 + 通用觀測工程。</p></blockquote>
<p>接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="跟-relational-transaction-的責任差異">跟 relational transaction 的責任差異</h3>
<p>DynamoDB transaction 跟 relational transaction 不是同一個東西。Relational transaction 支援任意複雜的多表多列交易、長交易、isolation level 調整；DynamoDB transaction 是「一次性提交一組有限 action、全成全敗、無互動式 transaction、無 SELECT FOR UPDATE」。當 application 需要長交易、複雜 join 內的一致性、或多步互動式 transaction、那是 relational 的場景、不該硬塞進 DynamoDB（回頭看 single-table 4 軸前置判讀）。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — 該篇主寫 <em>讀</em> 一致性（eventual vs strong read）、本篇主寫 <em>寫</em> 原子性、兩篇互補</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — 跨 item transaction 常用於 single-table 內多 entity 綁定寫</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> — transaction 不跨 region、多 region 寫衝突另有處理</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/" data-link-title="DynamoDB Streams 與 Lambda 事件驅動：CDC、shard 順序保證、消費模式與失敗處理" data-link-desc="DynamoDB Streams 不是免費的可靠事件流；本文展開 stream record 的四種 view type、shard 對應 partition 的順序保證邊界、Lambda event source mapping vs Kinesis 消費模式、at-least-once 下游冪等需求，以及 batch 失敗時的 bisect / DLQ 處理">streams-lambda-event-driven</a> — transaction 寫入會觸發 stream、下游 event 處理要 idempotent</li>
<li>替代路由：頻繁複雜交易需求 → 回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</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>、relational transaction 是主場</li>
<li>對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a> — 寫一致性失守後的對帳與修復</li>
</ul>
]]></content:encoded></item><item><title>Spanner PostgreSQL dialect：PG-compatible interface vs GoogleSQL、相容子集邊界、何時選 PG dialect</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/postgresql-dialect/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/postgresql-dialect/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>PostgreSQL dialect&lt;/em> — Spanner 為降低 PostgreSQL 生態遷入門檻提供的 PG-compatible 介面、跟原生 GoogleSQL dialect 的差異與邊界。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="核心定位pg-dialect-是介面層不是換引擎">核心定位：PG dialect 是介面層、不是換引擎&lt;/h2>
&lt;p>Spanner PostgreSQL dialect 的責任是讓 PostgreSQL 生態的語法、型別系統與 wire protocol 能跑在 Spanner 的分散式引擎之上、降低團隊既有 PostgreSQL 知識與工具的遷移成本。它改變的是 &lt;em>query 語言與 client 介面&lt;/em>、不改變底層的 split-based 儲存、Paxos 複製、TrueTime commit 與 external consistency — 這些 Spanner 的分散式語意在兩種 dialect 下完全一致。&lt;/p>
&lt;p>把這條定位放在最前面、是因為最常見的誤解是「選了 PG dialect 就等於用 PostgreSQL」。實際上 PG dialect 是「用 PostgreSQL 的方言跟 Spanner 對話」、不是「在 Spanner 裡裝一個 PostgreSQL」。team 帶著 PostgreSQL 的 &lt;code>psql&lt;/code>、libpq driver、PG 語法進來、但要寫的仍是 Spanner — 一個沒有 single-primary、沒有本地 sequence、partition 由系統管理的分散式 SQL。&lt;/p>
&lt;p>GoogleSQL dialect 是 Spanner 原生方言、語法接近 BigQuery 的 GoogleSQL、攜帶 Spanner-specific 的 &lt;code>INTERLEAVE IN PARENT&lt;/code>、array 型別、&lt;code>PENDING_COMMIT_TIMESTAMP&lt;/code> 等原生概念。兩種 dialect 是 instance / database 建立時就固定的選擇、之後不可變更。&lt;/p>
&lt;h2 id="問題情境postgresql-團隊想遷入-spanner但不想重寫所有-sql">問題情境：PostgreSQL 團隊想遷入 Spanner、但不想重寫所有 SQL&lt;/h2>
&lt;p>PostgreSQL dialect 的存在價值、在「既有 PostgreSQL 應用要拿到 Spanner 的全球強一致與線性擴展、但團隊的 SQL、ORM、tooling、人員技能都綁在 PostgreSQL」這個壓力下浮現。讀者徵兆：團隊評估 Spanner 時發現 GoogleSQL 語法陌生、ORM（如 SQLAlchemy、Hibernate）的 PostgreSQL dialect 已深度整合、DBA 熟悉 &lt;code>psql&lt;/code> 與 PG 工具鏈、不想為了遷移把整套 SQL 知識重學。&lt;/p>
&lt;p>真實壓力場景：一個建在 Cloud SQL for PostgreSQL 上的金融 ledger、撞到 single-primary 寫入上限、需要遷到 Spanner 拿跨 region 強一致;團隊有數萬行 PostgreSQL SQL、用 libpq-based driver、若 target 是 GoogleSQL、application 層改動範圍會大到讓遷移 ROI 不成立。PG dialect 把這個改動範圍縮小到「相容子集邊界內的 SQL 多數可沿用、邊界外的功能需要改寫」。&lt;/p>
&lt;p>Case anchor：本主題在 case 庫覆蓋稀薄。9.C10 是 Google internal dogfood case、未展開 dialect 選擇細節、且不是 customer-facing 參考。本文 dialect 機制、相容子集邊界、wire protocol 行為均以 GCP vendor 規格 + 通用遷移工程展開、case 僅作「為什麼 PostgreSQL 團隊要遷 Spanner」的壓力 anchor。延伸的遷移流程在 sibling &lt;a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>PostgreSQL dialect</em> — Spanner 為降低 PostgreSQL 生態遷入門檻提供的 PG-compatible 介面、跟原生 GoogleSQL dialect 的差異與邊界。</p></blockquote>
<hr>
<h2 id="核心定位pg-dialect-是介面層不是換引擎">核心定位：PG dialect 是介面層、不是換引擎</h2>
<p>Spanner PostgreSQL dialect 的責任是讓 PostgreSQL 生態的語法、型別系統與 wire protocol 能跑在 Spanner 的分散式引擎之上、降低團隊既有 PostgreSQL 知識與工具的遷移成本。它改變的是 <em>query 語言與 client 介面</em>、不改變底層的 split-based 儲存、Paxos 複製、TrueTime commit 與 external consistency — 這些 Spanner 的分散式語意在兩種 dialect 下完全一致。</p>
<p>把這條定位放在最前面、是因為最常見的誤解是「選了 PG dialect 就等於用 PostgreSQL」。實際上 PG dialect 是「用 PostgreSQL 的方言跟 Spanner 對話」、不是「在 Spanner 裡裝一個 PostgreSQL」。team 帶著 PostgreSQL 的 <code>psql</code>、libpq driver、PG 語法進來、但要寫的仍是 Spanner — 一個沒有 single-primary、沒有本地 sequence、partition 由系統管理的分散式 SQL。</p>
<p>GoogleSQL dialect 是 Spanner 原生方言、語法接近 BigQuery 的 GoogleSQL、攜帶 Spanner-specific 的 <code>INTERLEAVE IN PARENT</code>、array 型別、<code>PENDING_COMMIT_TIMESTAMP</code> 等原生概念。兩種 dialect 是 instance / database 建立時就固定的選擇、之後不可變更。</p>
<h2 id="問題情境postgresql-團隊想遷入-spanner但不想重寫所有-sql">問題情境：PostgreSQL 團隊想遷入 Spanner、但不想重寫所有 SQL</h2>
<p>PostgreSQL dialect 的存在價值、在「既有 PostgreSQL 應用要拿到 Spanner 的全球強一致與線性擴展、但團隊的 SQL、ORM、tooling、人員技能都綁在 PostgreSQL」這個壓力下浮現。讀者徵兆：團隊評估 Spanner 時發現 GoogleSQL 語法陌生、ORM（如 SQLAlchemy、Hibernate）的 PostgreSQL dialect 已深度整合、DBA 熟悉 <code>psql</code> 與 PG 工具鏈、不想為了遷移把整套 SQL 知識重學。</p>
<p>真實壓力場景：一個建在 Cloud SQL for PostgreSQL 上的金融 ledger、撞到 single-primary 寫入上限、需要遷到 Spanner 拿跨 region 強一致;團隊有數萬行 PostgreSQL SQL、用 libpq-based driver、若 target 是 GoogleSQL、application 層改動範圍會大到讓遷移 ROI 不成立。PG dialect 把這個改動範圍縮小到「相容子集邊界內的 SQL 多數可沿用、邊界外的功能需要改寫」。</p>
<p>Case anchor：本主題在 case 庫覆蓋稀薄。9.C10 是 Google internal dogfood case、未展開 dialect 選擇細節、且不是 customer-facing 參考。本文 dialect 機制、相容子集邊界、wire protocol 行為均以 GCP vendor 規格 + 通用遷移工程展開、case 僅作「為什麼 PostgreSQL 團隊要遷 Spanner」的壓力 anchor。延伸的遷移流程在 sibling <a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a>。</p>
<h2 id="相容子集邊界哪些-postgresql-功能不在範圍內">相容子集邊界：哪些 PostgreSQL 功能不在範圍內</h2>
<p>PG dialect 提供 PostgreSQL 語法、型別、function 與 wire protocol 的 <em>一個子集</em>、邊界由「Spanner 分散式引擎能不能支援該語意」決定、不是 PostgreSQL 有什麼就有什麼。理解邊界的關鍵是分清三類：相容沿用的、Spanner 用不同方式達成的、根本不存在的。</p>
<h3 id="相容沿用多數標準-sql">相容沿用：多數標準 SQL</h3>
<p>標準 DML（<code>SELECT</code> / <code>INSERT</code> / <code>UPDATE</code> / <code>DELETE</code>）、多數 JOIN、聚合、CTE、常見型別（<code>bigint</code> / <code>text</code> / <code>numeric</code> / <code>timestamptz</code> / <code>bool</code> / <code>jsonb</code>）、prepared statement、parameterized query 在 PG dialect 下沿用 PostgreSQL 語法。libpq-based driver 與 <code>psql</code> 可直接連、wire protocol 相容讓 PostgreSQL client 工具多數可用。</p>
<h3 id="spanner-用不同方式達成sequenceschema-changepk">Spanner 用不同方式達成：sequence、schema change、PK</h3>
<p>PostgreSQL 的 <code>SERIAL</code> / <code>bigserial</code> 在分散式系統下會製造熱點（單調遞增的 PK 集中寫到同一個 split）、Spanner 引導用 UUID 或 bit-reversed sequence 分散寫入。schema change 在 PG dialect 下仍是 Spanner 的 long-running operation、不是 PostgreSQL 的同步 DDL — DDL 語法是 PG 風格、但執行語意是 Spanner 的（見 <a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>）。primary key 設計直接決定資料分布、跟 PostgreSQL 把 PK 當邏輯約束的心智不同。</p>
<h3 id="根本不存在postgresql-重度功能">根本不存在：PostgreSQL 重度功能</h3>
<p>部分 PostgreSQL 的進階功能不在 PG dialect 範圍內、團隊若依賴它們、遷移要先找替代路徑。常見的缺口包含：自訂 extension（PostGIS、pgvector 等需另尋路徑）、stored procedure / 觸發器生態、部分 window function 與進階型別、<code>LISTEN</code> / <code>NOTIFY</code>、以及 PostgreSQL 特有的 lock 與 vacuum 心智。這些缺口不是 bug、是「Spanner 不是 PostgreSQL」的直接後果。</p>
<blockquote>
<p><strong>Scope warning</strong>：PG dialect 的具體支援清單（支援哪些型別、function、語法）逐版本擴充、本文列舉的相容子集邊界屬 GCP 規格、實作前必須 cross-verify <a href="https://cloud.google.com/spanner/docs/postgresql-interface">Spanner PostgreSQL dialect 官方文件</a> 的當前支援矩陣、不能依本文清單當最終依據。</p></blockquote>
<h2 id="操作流程建立-pg-dialect-database連線驗證相容性">操作流程：建立 PG dialect database、連線、驗證相容性</h2>
<h3 id="step-1建立-pg-dialect-database">Step 1：建立 PG dialect database</h3>
<p>dialect 在建立 database 時指定、不可事後變更。建立時明確選 PostgreSQL dialect：</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">gcloud spanner databases create my-pg-db <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --instance<span class="o">=</span>my-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --database-dialect<span class="o">=</span>POSTGRESQL</span></span></code></pre></div><p>驗證：查 database metadata 確認 dialect 是 POSTGRESQL。這步若選錯、唯一修法是建新 database 重遷、沒有 in-place 轉換 — 這是本文反覆強調的不可逆決策。</p>
<h3 id="step-2用-postgresql-client-連線">Step 2：用 PostgreSQL client 連線</h3>
<p>PG dialect 接受 PostgreSQL wire protocol、可用 <code>psql</code> 或 libpq-based driver 連線（透過 PGAdapter proxy 或支援的 client library）。</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"># 透過 PGAdapter 用 psql 連線</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">psql -h localhost -p <span class="m">5432</span> -d my-pg-db</span></span></code></pre></div><p>驗證：跑一個簡單 <code>SELECT 1</code>、確認 wire protocol 通;再跑一個帶 PG 型別的 query、確認型別映射正確。</p>
<h3 id="step-3相容性-audit--跑既有-sql-測邊界">Step 3：相容性 audit — 跑既有 SQL 測邊界</h3>
<p>把既有 PostgreSQL application 的 SQL 集合在 PG dialect database 上跑一遍、標出哪些直接通過、哪些報不支援。這步是遷移評估的核心 evidence — 它把「相容子集邊界」從文件文字變成「我的 SQL 有多少落在邊界內」的具體數字。</p>
<p>驗證點：統計通過率、把不通過的 SQL 分類（用 different way 達成 vs 根本不支援）、對「根本不支援」的部分評估改寫成本。若改寫成本過高、這是 PG dialect 路徑的 no-go 訊號。</p>
<h3 id="step-4rollback-boundary">Step 4：rollback boundary</h3>
<p>dialect 不可變更、所以 rollback boundary 在「遷移評估階段」、不在「上線後」。決策樹是：相容性 audit 通過率高 + 改寫成本可控 → 選 PG dialect;通過率低 + 大量 Spanner-only 優化需求 → 直接學 GoogleSQL。一旦 database 建好、dialect 就鎖定、rollback 等於重建 database 重遷。</p>
<h2 id="失敗模式把-pg-dialect-當完整-postgresql與-dialect-鎖定">失敗模式：把 PG dialect 當完整 PostgreSQL、與 dialect 鎖定</h2>
<h3 id="把-pg-dialect-當完整-postgresql-用">把 PG dialect 當完整 PostgreSQL 用</h3>
<p>團隊假設「PG dialect = PostgreSQL」、直接把依賴 extension、stored procedure、<code>SERIAL</code> PK 的應用搬過來、上線後發現 extension 不存在、<code>SERIAL</code> 製造熱點、p99 write latency 因 PK 集中而退化。徵兆是特定 PK range 的 split CPU 飆高、其餘 split 閒置。修法是審查 PK 設計改用分散式友善的 key（UUID / bit-reversed sequence）、把 extension 依賴改成 application 層或外部服務。這個失敗的根因是心智模型錯位、不是 bug。</p>
<h3 id="dialect-鎖定後才發現需要另一種-dialect">Dialect 鎖定後才發現需要另一種 dialect</h3>
<p>dialect 是 database 建立時的不可逆選擇、團隊選了 PG dialect、後續發現需要 GoogleSQL 才有的某個原生能力（或反之）、唯一路徑是建新 database 重遷全部資料。這個失敗的代價遠高於一般 config 錯誤 — 它不是改一行設定、是一次完整的資料遷移 + application cutover + 驗證 + rollback 規劃。回退路徑是把它當成一次 Type E migration（見 <a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a> 的 paradigm shift 結構）、不能當成 hotfix。預防勝於回退：在 Step 3 的相容性 audit 階段就要把「未來可能需要哪種 dialect 的能力」一起評估、而不是只看當下的 SQL 通過率。</p>
<h3 id="以為換了-pg-dialect-就不用懂-spanner-分散式語意">以為換了 PG dialect 就不用懂 Spanner 分散式語意</h3>
<p>PG dialect 降低語法門檻、但 Spanner 的 split、hot range、interleaved table、commit wait、cross-region quorum 在 PG dialect 下完全一樣。團隊若以為「用 PG 語法就能當 PostgreSQL 維運」、會在 hot partition、跨 region latency、schema change 是 long-running operation 這些 Spanner-specific 議題上踩雷。修法是不論選哪種 dialect、Spanner 的分散式機制都要懂 — dialect 是介面、不是引擎。</p>
<h2 id="容量與觀測dialect-不改變容量模型">容量與觀測：dialect 不改變容量模型</h2>
<p>PG dialect 跟 GoogleSQL 共用同一個 Spanner 引擎、容量模型、metric、sizing 完全一致 — 選 dialect 不影響容量規劃。核心觀測仍是 Spanner instance 的 CPU、split distribution、commit latency、跟原生 GoogleSQL database 沒有差別。</p>
<p>需要額外觀測的是 PG dialect 特有的接入層：若透過 PGAdapter proxy 連線、proxy 本身是一跳、要監控 proxy 的延遲與可用性、避免它成為單點。</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">Spanner CPU utilization        → 跟 dialect 無關、共用引擎指標
</span></span><span class="line"><span class="ln">2</span><span class="cl">split / hot range distribution → PK 設計（含 SERIAL 熱點）直接反映在這
</span></span><span class="line"><span class="ln">3</span><span class="cl">PGAdapter proxy latency        → PG dialect 接入層的額外一跳（若使用）
</span></span><span class="line"><span class="ln">4</span><span class="cl">commit_latencies               → external consistency 的 commit wait、兩 dialect 一致</span></span></code></pre></div><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> — sizing 邏輯跟 dialect 無關。觀測接 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<blockquote>
<p><strong>Scope warning</strong>：PGAdapter 的部署模型（sidecar / standalone proxy）與其延遲特性屬 GCP 規格、cross-verify 官方文件、非 9.C10 case 揭露。</p></blockquote>
<h2 id="邊界與整合何時選-pg-dialect何時選-googlesql">邊界與整合：何時選 PG dialect、何時選 GoogleSQL</h2>
<h3 id="選-pg-dialect-的條件">選 PG dialect 的條件</h3>
<p>既有 PostgreSQL 應用要遷入、SQL / ORM / tooling 深度綁 PostgreSQL、相容性 audit 通過率高、且不需要大量 Spanner-only 原生優化 — 這是 PG dialect 的適用條件。它讓遷移的 application 層改動最小化、保留團隊既有 PostgreSQL 技能。</p>
<h3 id="選-googlesql-的條件">選 GoogleSQL 的條件</h3>
<p>全新專案、團隊願意學 Spanner 原生方言、需要深度用 interleaved table、array 型別、Spanner-specific 優化、或想跟 BigQuery 的 GoogleSQL 生態對齊 — 選 GoogleSQL。它是 Spanner 的一等公民方言、新功能通常先在 GoogleSQL 落地。</p>
<h3 id="何時兩者都不選不該升-spanner">何時兩者都不選（不該升 Spanner）</h3>
<p>若 workload 是單 region、不需要全球強一致、PostgreSQL dialect 的相容性吸引力不該成為升 Spanner 的理由 — Cloud SQL for PostgreSQL 是真正的 PostgreSQL、相容性 100%、成本更低。Anti-recommendation 的判準是：PG dialect 的價值在「已經要遷 Spanner、想降低遷移成本」、不在「因為它像 PostgreSQL 所以選 Spanner」。把 dialect 相容性當升級理由是把次要因素當主要決策。</p>
<h3 id="sibling-deep-articles-路由">Sibling deep articles 路由</h3>
<ul>
<li><a href="../migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a>：PG dialect 是 Cloud SQL → Spanner 遷移降低改動成本的關鍵、本文的相容子集邊界對應該 playbook 的 diff audit</li>
<li><a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>：PG dialect 下 DDL 仍是 Spanner long-running operation、interleaved table 在兩 dialect 都要懂</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：兩 dialect 共用 external consistency、dialect 不改變一致性語意</li>
</ul>
<h3 id="跟-knowledge-card-的互引">跟 knowledge card 的互引</h3>
<ul>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed-sql</a> — PG dialect 是 distributed SQL 上的相容介面、不改變 distributed SQL 的本質</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation-level</a> — 兩 dialect 共用 Spanner 的 external consistency、isolation 語意一致</li>
</ul>
<h3 id="跟其他-vendor-的對照路由">跟其他 vendor 的對照路由</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a>：CockroachDB 走 PostgreSQL wire 相容是其核心策略、跟 Spanner PG dialect 是兩種「PostgreSQL 相容的 distributed SQL」路線、相容程度與邊界不同</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Change Streams + Kafka 整合：resume token、scope 選擇與 connector 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/change-streams-kafka/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/change-streams-kafka/</guid><description>&lt;p>MongoDB change streams 是 3.6+ 原生 CDC 介面、本質上是 oplog tail 包裝成 cursor API。Application 從 dual-write 模式（自己寫 MongoDB 又寫 Elasticsearch / Redis / data warehouse）換成 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現「downstream 漏 event」或「duplicate event」；最痛的是 connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌。本文把 change stream 機制、Kafka Connector 配置、resume token 治理、sharded cluster scope 選擇講清楚。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 change streams 簡介 — 而是 production CDC pipeline 部署 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>MongoDB 適用度前置判讀&lt;/strong>：進到 CDC pipeline 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>、本篇不重複展開。Change streams 是 &lt;em>已選 MongoDB 後&lt;/em> 的 event-driven 整合議題。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境第一版-cdc-pipeline-跑幾週的踩雷">問題情境：第一版 CDC pipeline 跑幾週的踩雷&lt;/h2>
&lt;p>典型觸發場景：application 寫 MongoDB 後還要 dual-write Elasticsearch / Redis / data warehouse、application code 越塞越多 hook、寫入失敗的補償邏輯散落各處。改用 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現：&lt;/p>
&lt;ul>
&lt;li>Downstream 漏 event 或 duplicate event&lt;/li>
&lt;li>Connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌&lt;/li>
&lt;li>Sharded cluster 上 collection-level change stream 跟 cluster-wide change stream 行為不同、application 連 mongos 跟連 single shard 拿到不同 event&lt;/li>
&lt;/ul>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>MongoDB Kafka Connector log &lt;code>ChangeStreamHistoryLost&lt;/code> 或 &lt;code>ResumeTokenChanged&lt;/code>&lt;/li>
&lt;li>Downstream Kafka topic event count vs source collection write count 不平&lt;/li>
&lt;li>Replication oplog 跟 change stream consumer 的 lag 同時升&lt;/li>
&lt;/ul>
&lt;p>Case anchor：CDC pipeline resume token 過期導致全量重灌的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」+ 容量公式處理、不憑空編造 incident 數字。側面引用 &lt;a href="https://tarrragon.github.io/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 → PubSub migration&lt;/a>（pipeline-level migration 經驗對照）。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB change streams 是 3.6+ 原生 CDC 介面、本質上是 oplog tail 包裝成 cursor API。Application 從 dual-write 模式（自己寫 MongoDB 又寫 Elasticsearch / Redis / data warehouse）換成 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現「downstream 漏 event」或「duplicate event」；最痛的是 connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌。本文把 change stream 機制、Kafka Connector 配置、resume token 治理、sharded cluster scope 選擇講清楚。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 change streams 簡介 — 而是 production CDC pipeline 部署 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>MongoDB 適用度前置判讀</strong>：進到 CDC pipeline 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>、本篇不重複展開。Change streams 是 <em>已選 MongoDB 後</em> 的 event-driven 整合議題。</p></blockquote>
<h2 id="問題情境第一版-cdc-pipeline-跑幾週的踩雷">問題情境：第一版 CDC pipeline 跑幾週的踩雷</h2>
<p>典型觸發場景：application 寫 MongoDB 後還要 dual-write Elasticsearch / Redis / data warehouse、application code 越塞越多 hook、寫入失敗的補償邏輯散落各處。改用 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現：</p>
<ul>
<li>Downstream 漏 event 或 duplicate event</li>
<li>Connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌</li>
<li>Sharded cluster 上 collection-level change stream 跟 cluster-wide change stream 行為不同、application 連 mongos 跟連 single shard 拿到不同 event</li>
</ul>
<p>讀者徵兆：</p>
<ul>
<li>MongoDB Kafka Connector log <code>ChangeStreamHistoryLost</code> 或 <code>ResumeTokenChanged</code></li>
<li>Downstream Kafka topic event count vs source collection write count 不平</li>
<li>Replication oplog 跟 change stream consumer 的 lag 同時升</li>
</ul>
<p>Case anchor：CDC pipeline resume token 過期導致全量重灌的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」+ 容量公式處理、不憑空編造 incident 數字。側面引用 <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 → PubSub migration</a>（pipeline-level migration 經驗對照）。</p>
<h2 id="核心機制">核心機制</h2>
<p>Change stream 是 MongoDB 3.6+ 原生 CDC、本質上是 oplog tail 包裝成 cursor API。可以從 collection / database / cluster 三個 scope 開：</p>
<ul>
<li><strong>Collection-level</strong>：監看單一 collection 的變更</li>
<li><strong>Database-level</strong>：監看整個 database 的所有 collection</li>
<li><strong>Cluster-wide</strong>：監看整個 cluster 的所有 database</li>
</ul>
<p>Oplog 是 capped collection、預設 size = disk 5% 或 50GB（取較小）。Resume token 對應 oplog entry 的 timestamp + UUID + documentKey。Token 必須對應仍在 oplog 內的 entry — oplog 滾掉就拿不到 token 對應的位置、<code>ChangeStreamHistoryLost</code>。</p>
<p><strong>Resume token 兩種用法</strong>：</p>
<ul>
<li><code>_id</code>：每個 event 都帶、application 自己存</li>
<li><code>startAfter</code> / <code>resumeAfter</code> parameter：重啟 cursor 時帶上</li>
</ul>
<p><strong><code>fullDocument: &quot;updateLookup&quot;</code></strong>：update event 預設只給 delta、加這個 option 會額外 query 一次 primary 拿完整 doc；高頻 update 下成本顯著（primary 負擔翻倍）。</p>
<p><strong>Pre-image / post-image（6.0+）</strong>：可以拿到 update 前的 doc 狀態、需 collection-level option <code>changeStreamPreAndPostImages: true</code>。</p>
<p><strong>Cluster-wide vs collection-level change stream</strong>：</p>
<ul>
<li>Cluster-wide 必須打 mongos、event ordering 是 global</li>
<li>Collection-level 可直接打單 shard、ordering 只在該 shard 內</li>
<li>Sharded cluster 上 cluster-wide stream 容易把 mongos 變單點瓶頸（所有 shard 的 event 都收斂到 mongos）</li>
</ul>
<p><strong>MongoDB Kafka Connector</strong>（Confluent / MongoDB 官方）：</p>
<ul>
<li>Source connector：把 change stream → Kafka topic</li>
<li>Sink connector：把 Kafka topic → MongoDB</li>
<li>At-least-once 語義、需 application 處理 idempotency</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change-data-capture</a>、<a href="/blog/backend/knowledge-cards/replication-channel/" data-link-title="Replication Channel" data-link-desc="說明多來源複製中，每個來源對應的獨立複製通道如何成為隔離單位">replication-channel</a>、<a href="/blog/backend/knowledge-cards/replication-slot/" data-link-title="Replication Slot" data-link-desc="說明邏輯複製如何用 slot 追蹤消費進度，並對來源端造成保留壓力">replication-slot</a>（MongoDB 沒 slot、概念對照）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：scope 決策樹</strong>。</p>
<table>
  <thead>
      <tr>
          <th>Scope</th>
          <th>適用條件</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collection-level</td>
          <td>單一 collection 的下游 sink、ordering 需求單一</td>
          <td>多 collection 要多 connector</td>
      </tr>
      <tr>
          <td>Database-level</td>
          <td>多 collection 共享 sink、ordering 跨 collection</td>
          <td>filter cost 在 connector 端</td>
      </tr>
      <tr>
          <td>Cluster-wide</td>
          <td>整個 cluster 統一 audit / replay</td>
          <td>mongos 單點瓶頸風險、event 量大</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 2：oplog sizing</strong>。容量公式：</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">oplog size &gt;= peak write rate × max acceptable consumer downtime</span></span></code></pre></div><p>典型設 24-72 小時可恢復窗口。例：peak 5K WPS、想容忍 48 小時 connector down、oplog 至少 5K × 86400 × 2 ÷ docs_per_GB ≈ 看實際 doc size 決定。在 Atlas 上 oplog size 可直接調、自管 cluster 改 <code>replSetResizeOplog</code>。</p>
<p><strong>Step 3：Kafka Connector 配置</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;connector.class&#34;</span><span class="p">:</span> <span class="s2">&#34;com.mongodb.kafka.connect.MongoSourceConnector&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;connection.uri&#34;</span><span class="p">:</span> <span class="s2">&#34;mongodb://...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;database&#34;</span><span class="p">:</span> <span class="s2">&#34;shop&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;collection&#34;</span><span class="p">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;publish.full.document.only&#34;</span><span class="p">:</span> <span class="s2">&#34;true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;change.stream.full.document&#34;</span><span class="p">:</span> <span class="s2">&#34;updateLookup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;copy.existing&#34;</span><span class="p">:</span> <span class="s2">&#34;true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&#34;copy.existing.namespace.regex&#34;</span><span class="p">:</span> <span class="s2">&#34;shop\\.orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nt">&#34;errors.tolerance&#34;</span><span class="p">:</span> <span class="s2">&#34;none&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nt">&#34;offset.flush.interval.ms&#34;</span><span class="p">:</span> <span class="s2">&#34;10000&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>change.stream.full.document: &quot;updateLookup&quot;</code>：每 update 額外 query primary 拿完整 doc（成本意識）</li>
<li><code>copy.existing: &quot;true&quot;</code>：connector 啟動時先把現有 collection 全量複製、再切到 change stream — 適合初次部署</li>
<li><code>errors.tolerance: &quot;none&quot;</code>：sink 失敗時 batch 停在 dead-letter queue、不 silently drop</li>
</ul>
<p><strong>Step 4：resume token persistence</strong>。Connector 把 token 寫 Kafka <code>__consumer_offsets</code> 或外部 store；application 自管 change stream 時要寫到 durable store（不是 in-memory）。</p>
<p><strong>Step 5：filter pipeline</strong>。Change stream 支援 aggregation pipeline 把過濾下推到 MongoDB：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">pipeline</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;operationType&#34;</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$in</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;insert&#34;</span><span class="p">,</span> <span class="s2">&#34;update&#34;</span><span class="p">,</span> <span class="s2">&#34;delete&#34;</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;fullDocument.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">const</span> <span class="nx">changeStream</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">watch</span><span class="p">(</span><span class="nx">pipeline</span><span class="p">)</span></span></span></code></pre></div><p>把過濾下推減少 connector 處理量、特別是高頻 collection 上。</p>
<p><strong>Step 6：downstream idempotency</strong>。Sink 收 Kafka event 時用 <code>documentKey._id + clusterTime</code> 做 dedup key — at-least-once 語義意味著 connector restart 後幾分鐘 event 會重發。</p>
<p>驗證點：</p>
<ul>
<li>Source collection write count vs Kafka topic event count 差異 &lt; 0.1%</li>
<li>Resume token age &lt; oplog retention 的 50%（健康狀態）</li>
<li>Connector restart drill 能 5 分鐘內接回</li>
</ul>
<p>Rollback boundary：source connector 是 read-only 對 MongoDB 無傷；sink connector 要備份 target 才能還原；resume token 寫錯 → 從 <code>startAtOperationTime</code> 回退到時間點重跑。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Resume token 過期（oplog 滾掉）</strong>：connector down 太久、oplog 已超出 retention、<code>ChangeStreamHistoryLost</code> → 必須 <code>copy.existing</code> 全量重灌、期間 downstream 看不到新資料。預防是 oplog sizing 留 buffer + connector lag alarm + token age 監控（age &gt; oplog retention 的 50% 預警）。</p>
<p><strong>updateLookup 在高頻 update 下打爆 primary</strong>：每筆 update event 都觸發一次 primary query、primary 負擔翻倍。修法是改 collection-level pre/post image（6.0+）、由 MongoDB 自己在寫入時記錄、或在 application 補完整 doc 後再寫 Kafka、不用 updateLookup。</p>
<p><strong>Sharded cluster cluster-wide stream 打爆 mongos</strong>：所有 shard 的 event 都收斂到 mongos、mongos 變單點瓶頸。修法是改 collection-level stream 多 connector 並行、每 connector 連 mongos 但只訂單一 collection。</p>
<p><strong>At-least-once 變 duplicate flood</strong>：connector restart 點之後幾分鐘 event 重發、downstream 沒做 idempotency → 重複 side effect（重複發 email、重複扣款）。修法是 sink 端強制 idempotency（dedup key 寫 Redis / DB）、不能假設「我用 at-least-once 但實際不會 duplicate」。</p>
<p><strong>Schema drift 突然 break sink</strong>：MongoDB 寫了新欄位 / 改型別、sink connector 的 JSON schema 不認、batch 停在 dead-letter queue。修法是 schema 變動有 validation gate（見 <a href="../schema-design-pattern/">schema design pattern</a>）、sink schema 設 <code>lenient</code> 模式吃 unknown field、或加 schema registry 統一版本。</p>
<p><strong>Backup / DDL 期間 change stream 異常</strong>：<code>reIndex</code> / <code>compact</code> / <code>dropCollection</code> 觸發特殊 event、connector 沒處理 → consumer 停。修法是 connector 處理特殊 event 邏輯要明確、不認得的 operation type 至少 log warning 而不是 silently stuck。</p>
<p>Anti-recommendation：</p>
<ul>
<li>簡單的 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> + application transactional write 對於低吞吐 / 單 sink 的場景比 change stream + Kafka 簡單；不是所有「需要 event 通知」的場景都要 CDC pipeline</li>
<li>若 downstream 只是同一 region 同團隊的 Elasticsearch index、<code>$merge</code> 寫進中介 collection 或 application 雙寫 + 對賬可能成本更低</li>
<li>Resume token 過期是這條路徑最痛的事故、oplog sizing 是 <em>投資而不是成本</em> — 不要為了省 storage 把 oplog 設太小</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Oplog 健康</strong>：oplog 寫入速率與保留時間</li>
<li><strong>Change stream 健康</strong>：cursor age、resume token 距 oplog 頭尾的距離</li>
<li><strong>Connector 健康</strong>：connector lag（Kafka offset 對比 source write）</li>
<li><strong>下游健康</strong>：event count diff（source write count vs sink apply count）、event time → arrival time lag 分布</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.getReplicationInfo()</code>：oplog 大小 / 時間範圍</li>
<li><code>db.printReplicationInfo()</code>：oplog 摘要</li>
<li><code>db.currentOp({ &quot;op&quot;: &quot;getmore&quot;, &quot;ns&quot;: &quot;local.oplog.rs&quot; })</code>：看 change stream consumer 連線</li>
</ul>
<p>Connector metric（Kafka Connect JMX）：<code>source-record-poll-rate</code>、<code>source-record-write-rate</code>、<code>offset-commit-success-rate</code>。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：oplog retention + connector lag + dedup rate 是 CDC pipeline 健康狀態 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：CDC lag 升高時區分 (a) source oplog 寫太快 (b) connector 處理慢 (c) downstream sink 慢。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — cluster-wide vs collection-level change stream 在 sharded cluster 的選擇</li>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — change stream 對 primary load 的影響、能否走 secondary</li>
<li><a href="../schema-design-pattern/">schema design pattern</a> — schema validator 對下游 sink 的契約意義</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — CDC sink 在 production 跨層架構裡的角色（cache invalidation / federated DB 同步）</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>MongoDB → 其他 sink 的 bulk migration 走 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas Migration Service</a></li>
<li>遷出 MongoDB 時 change stream 是 catch-up 機制（先 bulk export、再 change stream 補增量）</li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a> 處理 schema drift 時 CDC pipeline 的對賬；<a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation data repair</a> 處理 CDC 失準後的對賬流程。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「change streams + Kafka」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/changeStreams/">Change Streams</a>、<a href="https://www.mongodb.com/docs/kafka-connector/current/">MongoDB Kafka Connector</a>、<a href="https://www.mongodb.com/docs/manual/core/replica-set-oplog/">Oplog</a></li>
</ul>
]]></content:encoded></item><item><title>3.C35 Form3：NATS JetStream 多雲低延遲支付</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/</guid><description>&lt;p>這個案例的核心責任是說明 JetStream Leaf Node 在跨地理 / 跨雲 durability 拓樸的關鍵角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Form3 服務 Tier-1 銀行（含 Mastercard、Square 等）、要求 500ms 端到端 SLA、AWS SNS/SQS 約 300ms 延遲吃掉預算。在 Faster Payments 機房資源受限下、用 NATS + JetStream 替換 legacy pub/sub bus、達到約 6× 延遲改善並做到「AWS 整個 region 掛掉時不喪失處理能力」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 JetStream 的 Leaf Node 做跨雲橋接、把 on-prem Faster Payments 機房跟雲端 cluster 連起來。揭露金融支付對端到端 latency 預算的硬要求逼出特定 broker 選型、不是「Kafka / SQS 通用化」。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：Cluster + Supercluster + Leaf node / JetStream stream 設計。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS&lt;/a>（跨區對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.synadia.com/blog/how-form3-built-a-multi-cloud-low-latency-payments-service-with-nats-io-jetstream">How Form3 Built a Multi-Cloud Low-Latency Payments Service with NATS JetStream (Synadia blog)&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 JetStream Leaf Node 在跨地理 / 跨雲 durability 拓樸的關鍵角色。</p>
<h2 id="觀察">觀察</h2>
<p>Form3 服務 Tier-1 銀行（含 Mastercard、Square 等）、要求 500ms 端到端 SLA、AWS SNS/SQS 約 300ms 延遲吃掉預算。在 Faster Payments 機房資源受限下、用 NATS + JetStream 替換 legacy pub/sub bus、達到約 6× 延遲改善並做到「AWS 整個 region 掛掉時不喪失處理能力」。</p>
<h2 id="判讀">判讀</h2>
<p>用 JetStream 的 Leaf Node 做跨雲橋接、把 on-prem Faster Payments 機房跟雲端 cluster 連起來。揭露金融支付對端到端 latency 預算的硬要求逼出特定 broker 選型、不是「Kafka / SQS 通用化」。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：Cluster + Supercluster + Leaf node / JetStream stream 設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1 Meta FOQS</a>（跨區對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.synadia.com/blog/how-form3-built-a-multi-cloud-low-latency-payments-service-with-nats-io-jetstream">How Form3 Built a Multi-Cloud Low-Latency Payments Service with NATS JetStream (Synadia blog)</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL PITR + WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 backup / recovery 是 OLTP 必備能力、本文聚焦 &lt;em>PITR（Point-In-Time Recovery）的雙軌資料設計 + production 5 個 failure mode&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Logical bug 在 production 部署、執行 6 小時後才發現 — 某個 batch job 把 50 萬筆 user.email 改成 NULL。此時：&lt;/p>
&lt;ul>
&lt;li>還原最新 daily backup（昨晚）→ 丟掉今天所有正常寫入（訂單、註冊）&lt;/li>
&lt;li>從 standby promote → standby 已同步 bug、跟 primary 同狀態&lt;/li>
&lt;li>從 application log 重建 → 部分操作不可逆（已寄出 email）&lt;/li>
&lt;/ul>
&lt;p>PITR 是這類 &lt;em>logical disaster&lt;/em> 的標準解 — 不還原到 backup 時間點、而是 &lt;em>還原到 bug 發生前一刻&lt;/em>（例：1 分鐘前）。需要 &lt;em>base backup + WAL archive&lt;/em> 雙軌資料：base backup 是 snapshot、WAL archive 是 snapshot 之後的所有寫入；recovery 時 replay WAL 到指定 timestamp / LSN / transaction ID。&lt;/p>
&lt;h2 id="核心概念base-backup--wal-archive-的雙軌設計">核心概念：base backup + WAL archive 的雙軌設計&lt;/h2>





&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">[Base backup t0] + [WAL archive t0 → now]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 全量 snapshot incremental log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> └────── recover to t_target ──→ [restored cluster at t_target]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個軌道各自獨立但必須對齊：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Base backup&lt;/strong>：某時刻整個 data dir 的 snapshot。&lt;code>pg_basebackup&lt;/code> / &lt;code>pgBackRest&lt;/code> / &lt;code>WAL-G&lt;/code> 都產這個；通常 &lt;em>每天 / 每週&lt;/em> 跑一次&lt;/li>
&lt;li>&lt;strong>WAL archive&lt;/strong>：base backup 之後每段 WAL 都 push 到外部 storage（S3 / GCS / NFS）。&lt;code>archive_command&lt;/code> 觸發、PostgreSQL 等到 archive 成功才 &lt;em>回收&lt;/em> 那段 WAL&lt;/li>
&lt;/ol>
&lt;p>兩者組合決定 RPO（recovery point objective）：&lt;/p>
&lt;ul>
&lt;li>RPO ≈ WAL archive frequency（streaming 即時、&lt;code>archive_timeout&lt;/code> 預設 1 分鐘）&lt;/li>
&lt;li>RPO 不是 base backup frequency — daily base backup + 每分鐘 archive WAL → RPO 1 分鐘&lt;/li>
&lt;/ul>
&lt;p>RTO（recovery time objective）跟 &lt;em>base backup size + WAL replay 量&lt;/em> 相關：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 backup / recovery 是 OLTP 必備能力、本文聚焦 <em>PITR（Point-In-Time Recovery）的雙軌資料設計 + production 5 個 failure mode</em>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Logical bug 在 production 部署、執行 6 小時後才發現 — 某個 batch job 把 50 萬筆 user.email 改成 NULL。此時：</p>
<ul>
<li>還原最新 daily backup（昨晚）→ 丟掉今天所有正常寫入（訂單、註冊）</li>
<li>從 standby promote → standby 已同步 bug、跟 primary 同狀態</li>
<li>從 application log 重建 → 部分操作不可逆（已寄出 email）</li>
</ul>
<p>PITR 是這類 <em>logical disaster</em> 的標準解 — 不還原到 backup 時間點、而是 <em>還原到 bug 發生前一刻</em>（例：1 分鐘前）。需要 <em>base backup + WAL archive</em> 雙軌資料：base backup 是 snapshot、WAL archive 是 snapshot 之後的所有寫入；recovery 時 replay WAL 到指定 timestamp / LSN / transaction ID。</p>
<h2 id="核心概念base-backup--wal-archive-的雙軌設計">核心概念：base backup + WAL archive 的雙軌設計</h2>





<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">[Base backup t0]  +  [WAL archive t0 → now]
</span></span><span class="line"><span class="ln">2</span><span class="cl">     ↓                       ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">  全量 snapshot          incremental log
</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">     └────── recover to t_target ──→ [restored cluster at t_target]</span></span></code></pre></div><p>兩個軌道各自獨立但必須對齊：</p>
<ol>
<li><strong>Base backup</strong>：某時刻整個 data dir 的 snapshot。<code>pg_basebackup</code> / <code>pgBackRest</code> / <code>WAL-G</code> 都產這個；通常 <em>每天 / 每週</em> 跑一次</li>
<li><strong>WAL archive</strong>：base backup 之後每段 WAL 都 push 到外部 storage（S3 / GCS / NFS）。<code>archive_command</code> 觸發、PostgreSQL 等到 archive 成功才 <em>回收</em> 那段 WAL</li>
</ol>
<p>兩者組合決定 RPO（recovery point objective）：</p>
<ul>
<li>RPO ≈ WAL archive frequency（streaming 即時、<code>archive_timeout</code> 預設 1 分鐘）</li>
<li>RPO 不是 base backup frequency — daily base backup + 每分鐘 archive WAL → RPO 1 分鐘</li>
</ul>
<p>RTO（recovery time objective）跟 <em>base backup size + WAL replay 量</em> 相關：</p>
<ul>
<li>Restore base backup ~ 1-4 小時（TB 級）</li>
<li>WAL replay 時間 ~ archive 累積量 / replay throughput</li>
</ul>
<h2 id="step-by-step-配置">Step-by-step 配置</h2>
<h3 id="primaryarchive_command-設好">Primary：archive_command 設好</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># postgresql.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">wal_level</span> <span class="o">=</span> <span class="s">replica                          # 預設 replica、PITR 需要</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">archive_mode</span> <span class="o">=</span> <span class="s">on                            # 啟用 archive</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">archive_command</span> <span class="o">=</span> <span class="s">&#39;wal-g wal-push %p&#39;        # 或 pgBackRest / 自寫 script</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">archive_timeout</span> <span class="o">=</span> <span class="s">60                         # 60s 無 WAL 時強制切 segment</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">max_wal_size</span> <span class="o">=</span> <span class="s">4GB</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">checkpoint_timeout</span> <span class="o">=</span> <span class="s">15min</span></span></span></code></pre></div><p><code>archive_command</code> 必須 <em>回 exit code 0 才算成功</em>；非 0 PostgreSQL retry、retry 失敗會在 <code>pg_wal</code> 堆積 WAL 直到 disk 滿。<strong>critical：archive_command 不能寫成 silent-fail</strong>。</p>
<h3 id="用-pgbackrest-取代手寫-script">用 pgBackRest 取代手寫 script</h3>
<p>production 強烈不建議自寫 archive script — pgBackRest / WAL-G / Barman 處理過所有 edge case：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># pgbackrest.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">[global]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">repo1-type</span><span class="o">=</span><span class="s">s3</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">repo1-s3-bucket</span><span class="o">=</span><span class="s">mybucket</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">repo1-s3-region</span><span class="o">=</span><span class="s">us-east-1</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">repo1-retention-full</span><span class="o">=</span><span class="s">4                       # 留 4 個 full backup</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">repo1-retention-diff</span><span class="o">=</span><span class="s">8                       # 留 8 個 differential</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">repo1-cipher-type</span><span class="o">=</span><span class="s">aes-256-cbc                # encrypt at rest</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">process-max</span><span class="o">=</span><span class="s">8                                # parallel restore</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="k">[main]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">pg1-path</span><span class="o">=</span><span class="s">/var/lib/postgresql/16/main</span></span></span></code></pre></div>




<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"># 跑 full backup</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgbackrest --stanza<span class="o">=</span>main backup --type<span class="o">=</span>full
</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"># archive_command 用 pgbackrest 內建</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">archive_command</span> <span class="o">=</span> <span class="s1">&#39;pgbackrest --stanza=main archive-push %p&#39;</span></span></span></code></pre></div><p>pgBackRest 處理：parallel push、compression、encryption、checksum、archive replay timing、backup catalog、retention 自動清理。</p>
<h3 id="restorerecovery_target_time">Restore：recovery_target_time</h3>





<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. 從 S3 / repo 拉 base backup</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgbackrest --stanza<span class="o">=</span>main --type<span class="o">=</span><span class="nb">time</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --target<span class="o">=</span><span class="s2">&#34;2026-05-18 14:30:00+00&#34;</span> restore
</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="c1"># 2. PostgreSQL 進 recovery mode、自動 replay WAL 到 target time</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># (pgBackRest 寫好 recovery.signal + postgresql.auto.conf)</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"># 3. 確認到目標 timestamp 後、promote</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">pg_ctl promote</span></span></code></pre></div><p>Recovery target 三種：</p>
<ul>
<li><strong><code>recovery_target_time</code></strong>：到某 timestamp</li>
<li><strong><code>recovery_target_xid</code></strong>：到某 transaction ID（log 有 xid 才好定位）</li>
<li><strong><code>recovery_target_lsn</code></strong>：到某 WAL LSN（最精確、但需要事先記下 LSN）</li>
</ul>
<p>production 多用 timestamp、application log 有時間戳容易定位。</p>
<h2 id="故障演練--邊界-case">故障演練 / 邊界 case</h2>
<h3 id="case-1archive_command-靜默失敗">Case 1：archive_command 靜默失敗</h3>
<p><strong>徵兆</strong>：DBA 發現某 PITR test 時、最近 3 天的 WAL 在 S3 上沒有；但 PostgreSQL 沒 alert、<code>pg_wal</code> 也沒堆積（早就被回收？）。</p>
<p><strong>根因</strong>：archive_command 寫成 <code>aws s3 cp %p s3://bucket/... 2&gt;/dev/null</code> — 錯誤訊息被吞、exit code 卻是 0（cp 失敗但 redirect 後 shell wrapper 不傳 fail code）；PostgreSQL 以為成功、繼續 advance WAL pointer、舊 WAL 已回收、archive 上實際沒有。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>絕對不要靜默 exit code</strong>：archive_command 必須 <em>fail loud</em>、exit code 非 0</li>
<li><strong>用 pgBackRest / WAL-G</strong>、不自寫 shell 腳本</li>
<li><strong>monitoring</strong>：對 archive lag 寫 alert</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_last_archived_xact_time</span><span class="p">(),</span><span class="w"> </span><span class="n">now</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">pg_last_archived_xact_time</span><span class="p">()</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">lag</span><span class="p">;</span></span></span></code></pre></div><p>alert if lag &gt; 5 minutes</p>
<ol start="4">
<li><strong>定期測試 restore</strong>：每月跑一次 PITR drill、實際從 archive restore + 驗證 timestamp</li>
</ol>
<h3 id="case-2wal-archive-lagprimary-disk-壓力">Case 2：WAL archive lag、primary disk 壓力</h3>
<p><strong>徵兆</strong>：<code>pg_wal</code> 目錄持續長大、<code>df -h</code> 90%+；<code>pg_stat_archiver</code> 顯示 <code>failed_count</code> 累積、<code>last_failed_time</code> 是 30 分鐘前；archive_command 寫不出去（S3 throttle / network 慢）。</p>
<p><strong>根因</strong>：archive_command 寫到 S3、但 S3 rate limit / connection timeout、PostgreSQL retry；WAL 一直在 <code>pg_wal</code> 不能回收、disk 持續長。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：<code>archive_command</code> 內部 retry + parallel push（pgBackRest 自帶 <code>process-max</code>）</li>
<li><strong>alert</strong>：<code>pg_stat_archiver.failed_count</code> 增長 + primary disk usage &gt; 80%</li>
<li><strong>緊急</strong>：暫時改 archive_command 寫 local NFS / 其他 storage、等 S3 恢復再同步；不要直接 disable archive（會丟資料）</li>
<li><strong>架構</strong>：archive storage 至少跨 region 兩份、單一 storage 故障不影響 archive</li>
</ol>
<h3 id="case-3recovery-跑到-wrong-target-time">Case 3：recovery 跑到 wrong target time</h3>
<p><strong>徵兆</strong>：PITR 還原後資料看起來 <em>缺一塊</em>；DBA 後悔 — target time 設早了 30 分鐘、recovery 已 promote、後續 WAL 在新 timeline 上、回不去。</p>
<p><strong>根因</strong>：recovery 過程不可逆 — 一旦 promote 開新 timeline、舊 WAL 在新 timeline 上不會被 replay；想還原到更晚 timestamp 必須 <em>重新 restore base backup + WAL</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>recovery_target_action = pause</code></strong>（PG 13+）：到 target time 後 <em>暫停</em>、不自動 promote；DBA 手動 query 確認資料對才 promote</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">recovery_target_time</span> <span class="o">=</span> <span class="s">&#39;2026-05-18 14:30:00+00&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">recovery_target_action</span> <span class="o">=</span> <span class="s">pause</span></span></span></code></pre></div><ol start="2">
<li><strong>多次 PITR 試錯</strong>：用 <em>獨立 staging cluster</em> restore、驗證 target time 對、再對 production 跑</li>
<li><strong>記錄 target time 來源</strong>：application log / event timestamp 多比對、避免時區錯亂（<code>+00</code> UTC 跟 local time 差）</li>
</ol>
<h3 id="case-4base-backup-過期未清storage-爆">Case 4：base backup 過期未清、storage 爆</h3>
<p><strong>徵兆</strong>：S3 backup bucket size 半年內從 200GB 漲到 5TB；DBA 才發現 retention 沒設、daily base backup 留 180 天。</p>
<p><strong>根因</strong>：archive_command 自寫腳本沒 retention 邏輯、或 pgBackRest 設了 <code>repo1-retention-full=180</code> 漏看；DB 容量本來就成長 + 每日 full backup 累積。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># pgBackRest retention：4 full + auto-expire archive</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">repo1-retention-full</span><span class="o">=</span><span class="s">4                         # 留 4 個 full backup</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">repo1-retention-diff</span><span class="o">=</span><span class="s">8                         # 留 8 個 differential</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">repo1-retention-archive</span><span class="o">=</span><span class="s">4                      # WAL archive 跟 full 對齊</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">repo1-retention-archive-type</span><span class="o">=</span><span class="s">full</span></span></span></code></pre></div><p>storage budgeting：</p>
<ul>
<li>daily full + diff + WAL archive ≈ 1-2x DB size / day</li>
<li>4-week retention → ~30-60x DB size storage</li>
<li>跨 region replication → 2-3x</li>
</ul>
<h3 id="case-5timeline-分歧後-recovery-模糊">Case 5：timeline 分歧後 recovery 模糊</h3>
<p><strong>徵兆</strong>：production 經歷一次 failover（Patroni promote）+ 之後又 PITR 一次；現在要再 PITR 到 failover 前一刻、archive 上有兩個 timeline、recovery target 搞不清要哪個。</p>
<p><strong>根因</strong>：每次 promote 開新 timeline ID（<code>.history</code> 檔）；archive storage 上同 LSN 可能對應不同 timeline；recovery target time 在分歧點附近、ambiguous。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>recovery_target_timeline</code></strong> 明示要 follow 哪個 timeline</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">recovery_target_time</span> <span class="o">=</span> <span class="s">&#39;2026-05-15 10:00:00+00&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">recovery_target_timeline</span> <span class="o">=</span> <span class="s">&#39;3&#39;                 # 要 follow timeline 3</span></span></span></code></pre></div><ol start="2">
<li><strong>熟悉 <code>.history</code> 檔</strong>：<code>/wal_archive/000000XX.history</code> 記錄 timeline 切換點、PITR 前先看</li>
<li><strong>預防</strong>：每次 promote 後 <em>立刻</em> 跑新的 base backup、簡化未來 PITR 流程（不用跨 timeline）</li>
</ol>
<h2 id="容量--cost-規劃">容量 / cost 規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Base backup size</td>
          <td>跟 DB data dir 大小成正比（PostgreSQL 內部 compression 後）</td>
          <td>每 backup ~ 0.5-1x DB size</td>
      </tr>
      <tr>
          <td>WAL archive size</td>
          <td>~5-50GB / day depending on write volume</td>
          <td>1TB DB / write-heavy 可能 100GB+ / day</td>
      </tr>
      <tr>
          <td>Storage retention</td>
          <td>4-12 weeks 典型</td>
          <td>30-60x DB size budget</td>
      </tr>
      <tr>
          <td>Base backup time</td>
          <td>TB 級 1-4 小時</td>
          <td>跑在 maintenance window</td>
      </tr>
      <tr>
          <td>Restore time</td>
          <td>base backup restore + WAL replay</td>
          <td>TB 級 PITR 通常 2-6 小時</td>
      </tr>
      <tr>
          <td>Network bandwidth</td>
          <td>full backup 期間 100-500 Mbps</td>
          <td>跨 region 注意 egress cost</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>Daily full backup + 4 weeks retention</li>
<li>WAL archive every 60s（<code>archive_timeout = 60</code>）</li>
<li>跨 region replication（S3 → S3 cross-region）</li>
<li>月度 restore drill 驗證可用</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-patroni-ha-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> 整合</h3>
<p>Patroni 不管 backup，但 promotion 後 timeline 切換影響 archive：</p>
<ol>
<li>archive_command 用 <code>%t</code>（timeline）+ <code>%f</code>（filename）路徑、避免不同 timeline WAL 覆蓋</li>
<li>Patroni <code>recovery_conf</code> 包含 <code>restore_command</code>、standby clone 從 archive 拉</li>
<li>每次 Patroni failover 後跑 <em>full backup</em>、簡化未來 PITR</li>
</ol>
<h3 id="跟-logical-replication-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">logical replication</a> 對位</h3>
<p>PITR 跟 logical replication 服務不同 use case：</p>
<ul>
<li>PITR 是 <em>災難恢復</em>（logical bug / corruption）— 全量還原到某時刻</li>
<li>Logical replication 是 <em>連續 sync</em> — Kafka / 跨 DB 即時複製</li>
</ul>
<p>兩者 <em>都依賴 WAL</em>、但目標不同；同 PostgreSQL 可同時跑、互不衝突。</p>
<h3 id="跟-monitoring--alert">跟 monitoring + alert</h3>
<p>關鍵 metric：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- archive 健康度
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_archiver</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- archived_count, failed_count, last_archived_wal, last_archived_time
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- WAL 在 pg_wal 等待 archive 量
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_ls_waldir</span><span class="p">()</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">~</span><span class="w"> </span><span class="s1">&#39;^[0-9A-F]{24}$&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="c1">-- base backup 上次跑時間
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">-- (pgBackRest API 或 backup catalog)</span></span></span></code></pre></div><p>Prometheus alert 三條：archive failed_count 增、archive lag &gt; 5min、base backup &gt; 25h 沒跑。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Incremental backup（PG 17+）</strong>：base backup 不全量、只 base + incremental</li>
<li><strong>Block-level differential</strong>：pgBackRest 已支援</li>
<li><strong>Cloud-native 替代</strong>：RDS / Aurora 用 storage-layer snapshot、不走 PITR 鏈</li>
<li><strong><code>pg_dump</code> vs PITR</strong>：pg_dump 是 logical backup（resume to different schema OK）、PITR 是 physical（必須同 version + same arch）</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a> — PITR 是 migration 的失敗回退</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a> / <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>9.C35 Snap：GCP + KeyDB 在 multi-cloud 架構下的低延遲快取</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/</guid><description>&lt;p>這個案例的核心責任是補強 GCP cache 維度、並揭示 multi-cloud 架構的隱性 latency 議題。Snap（Snapchat 母公司、日活 4 億 +）2011 年從零起就在 GCP 上、是雲原生最早期客戶之一、但近年走 multi-cloud（GCP + AWS）。這個架構引出「跨 cloud cache latency 怎麼處理」的工程議題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Snap 在 GCP 的關鍵敘述（引自 &lt;a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud&lt;/a>、&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap TPU recommendation&lt;/a>）：&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>用戶基礎&lt;/td>
 &lt;td>4 億 + DAU、年增 18% YoY&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開始在 GCP 時間&lt;/td>
 &lt;td>2011 年（產品早期）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-cloud cache 方案&lt;/td>
 &lt;td>GCP 上部署 KeyDB cluster 減少 cross-cloud latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ML training&lt;/td>
 &lt;td>TPU（vs GPU 吞吐高 67%、成本低 52%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>安全框架&lt;/td>
 &lt;td>BeyondCorp Enterprise（Zero Trust）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵架構決策：在 &lt;em>GCP&lt;/em> 上部署 KeyDB（Redis fork、multi-threaded）作為 cache layer、減少 cross-cloud latency。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Snap 案例揭露三個 multi-cloud 容量設計的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>跨 cloud latency 是隱性容量瓶頸&lt;/strong>：當 application 在 AWS、cache 在 GCP（或反之）、每個 cache lookup 都吃跨 cloud 網路 latency（通常 5-30ms、視 region pair 而定）。對 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Snap 這類「每次互動查多個 cache」&lt;/a> 的服務、5ms × 10 cache lookup = 50ms 額外 latency、用戶感受明顯。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 反推。&lt;/li>
&lt;li>&lt;strong>KeyDB 是 Redis 的 multi-threaded 替代&lt;/strong>：Redis 7+ 之前是 single-threaded、單實例吞吐受限。KeyDB（Snap 等大型用戶採用）改成 multi-threaded、單實例 throughput 提升 5-10x、適合超高吞吐 cache 需求。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache&lt;/a> 的 cache layer 設計、但 Snap 規模更大要走專業 fork。&lt;/li>
&lt;li>&lt;strong>TPU vs GPU 是 ML training 的容量成本決策&lt;/strong>：Snap 算過 GPU 的「throughput -67% + cost +52%」就是 TPU 的反向 — TPU 的 throughput 高 67%、cost 低 52% — 對 ML-heavy 公司是巨大決策。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 的雲端硬體選型、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &amp;#43; 1.5 億商品、用 GCP Vertex AI Search &amp;#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre Vertex AI&lt;/a> 的 ML 容量規劃同類。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 GCP cache 維度、並揭示 multi-cloud 架構的隱性 latency 議題。Snap（Snapchat 母公司、日活 4 億 +）2011 年從零起就在 GCP 上、是雲原生最早期客戶之一、但近年走 multi-cloud（GCP + AWS）。這個架構引出「跨 cloud cache latency 怎麼處理」的工程議題。</p>
<h2 id="觀察">觀察</h2>
<p>Snap 在 GCP 的關鍵敘述（引自 <a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud</a>、<a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap TPU recommendation</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶基礎</td>
          <td>4 億 + DAU、年增 18% YoY</td>
      </tr>
      <tr>
          <td>開始在 GCP 時間</td>
          <td>2011 年（產品早期）</td>
      </tr>
      <tr>
          <td>Multi-cloud cache 方案</td>
          <td>GCP 上部署 KeyDB cluster 減少 cross-cloud latency</td>
      </tr>
      <tr>
          <td>ML training</td>
          <td>TPU（vs GPU 吞吐高 67%、成本低 52%）</td>
      </tr>
      <tr>
          <td>安全框架</td>
          <td>BeyondCorp Enterprise（Zero Trust）</td>
      </tr>
  </tbody>
</table>
<p>關鍵架構決策：在 <em>GCP</em> 上部署 KeyDB（Redis fork、multi-threaded）作為 cache layer、減少 cross-cloud latency。</p>
<h2 id="判讀">判讀</h2>
<p>Snap 案例揭露三個 multi-cloud 容量設計的工程重點。</p>
<ol>
<li><strong>跨 cloud latency 是隱性容量瓶頸</strong>：當 application 在 AWS、cache 在 GCP（或反之）、每個 cache lookup 都吃跨 cloud 網路 latency（通常 5-30ms、視 region pair 而定）。對 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Snap 這類「每次互動查多個 cache」</a> 的服務、5ms × 10 cache lookup = 50ms 額外 latency、用戶感受明顯。對應 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a> 的 latency budget 反推。</li>
<li><strong>KeyDB 是 Redis 的 multi-threaded 替代</strong>：Redis 7+ 之前是 single-threaded、單實例吞吐受限。KeyDB（Snap 等大型用戶採用）改成 multi-threaded、單實例 throughput 提升 5-10x、適合超高吞吐 cache 需求。對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 的 cache layer 設計、但 Snap 規模更大要走專業 fork。</li>
<li><strong>TPU vs GPU 是 ML training 的容量成本決策</strong>：Snap 算過 GPU 的「throughput -67% + cost +52%」就是 TPU 的反向 — TPU 的 throughput 高 67%、cost 低 52% — 對 ML-heavy 公司是巨大決策。對應 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的雲端硬體選型、跟 <a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre Vertex AI</a> 的 ML 容量規劃同類。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>KeyDB 是 <em>fork-based</em> 軟體、有 vendor lock-in 風險（Snap 大規模採用後、KeyDB 公司被收購、未來 fork 走向不確定）</li>
<li>TPU 是 <em>Google 專屬硬體</em>、不能在其他 cloud 用、是 vendor lock-in 來源</li>
<li>「年增 18%」是用戶數、不是流量。流量成長通常超過用戶成長（per-user engagement 上升）</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>Multi-cloud 架構優先把 cache 跟 application 放同一 cloud</strong>：跨 cloud 的不該是 cache lookup（高頻、低 latency 容忍）、應該是 batch sync（低頻、高 latency 容忍）。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的部署策略。</li>
<li><strong>Redis 規模化遇到 single-threaded 限制時的選項</strong>：
<ul>
<li>拆 cluster（多個 Redis instance）— 應用層分散 key</li>
<li>換 KeyDB / Dragonfly（multi-threaded fork）</li>
<li>換 Redis 7+ I/O thread（保留 protocol）</li>
<li>換 Memcached（multi-threaded、但功能少）</li>
</ul>
</li>
<li><strong>ML training infrastructure 選型按 throughput / cost 而非品牌</strong>：GPU vs TPU vs Trainium 不是「哪家好」、是「在 <em>本 workload</em> 上哪個划算」。要實測 benchmark、不是看 vendor marketing。</li>
<li><strong>跨 cloud 部署的「資料引力」</strong>：data 在哪、application 通常會被 data 吸過去。Snap 把 cache 放 GCP 是因為 production data 在 GCP — 想搬 cache 到 AWS 同時要搬 data、成本高。</li>
</ol>
<p>跨平台等效：AWS ElastiCache + Cassandra / DynamoDB Global Tables、Azure Cache for Redis + Cosmos DB 都可實作 multi-region cache 但 single-cloud 內。multi-cloud cache 通常要自管（自管 KeyDB / Dragonfly / Redis Cluster）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a></li>
<li>想設計 multi-cloud cache → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>想做 ML training 容量規劃 → <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界</a> + <a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre</a></li>
<li>想理解 cross-cloud latency → <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud to reduce cross-cloud latency</a></li>
<li><a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap Inc. uses Google Cloud TPU for deep learning recommendation models</a></li>
<li><a href="https://cloud.google.com/blog/products/gcp/snap-maintains-uptime-with-mcs-from-google-cloud/">Snap maintains uptime with MCS from Google Cloud</a></li>
<li><a href="https://cloud.google.com/blog/products/identity-security/why-snap-chose-beyondcorp-enterprise-to-build-a-durable-zero-trust-framework">Why Snap chose BeyondCorp Enterprise</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB DAX 快取策略：cluster 架構、item/query cache、write-through 與 invalidation 邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DAX 觸發條件 SSoT&lt;/strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段&lt;/a>、含 &lt;code>9.C29 Lemino&lt;/code> case fact 跟 &lt;code>9.C19 Capcom&lt;/code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取&lt;/h2>
&lt;p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。&lt;/p>
&lt;p>&lt;strong>cluster 拓樸&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica&lt;/li>
&lt;li>跨多 AZ 部署、primary 故障時 replica 接手&lt;/li>
&lt;li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>兩種快取、不同生命週期&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。</p>
<blockquote>
<p><strong>DAX 觸發條件 SSoT</strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段</a>、含 <code>9.C29 Lemino</code> case fact 跟 <code>9.C19 Capcom</code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。</p></blockquote>
<h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取</h2>
<p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。</p>
<p><strong>cluster 拓樸</strong>：</p>
<ul>
<li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica</li>
<li>跨多 AZ 部署、primary 故障時 replica 接手</li>
<li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica</li>
</ul>
<p><strong>兩種快取、不同生命週期</strong>：</p>
<table>
  <thead>
      <tr>
          <th>快取類型</th>
          <th>內容</th>
          <th>寫入如何影響</th>
          <th>失效方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Item cache</td>
          <td><code>GetItem</code> / <code>BatchGetItem</code> 的單筆結果</td>
          <td>write-through 寫入時同步更新對應 item</td>
          <td>item TTL + write-through</td>
      </tr>
      <tr>
          <td>Query cache</td>
          <td><code>Query</code> / <code>Scan</code> 的結果集</td>
          <td>單筆 write <em>不會</em> 失效對應 query 結果集</td>
          <td>只靠 query TTL</td>
      </tr>
  </tbody>
</table>
<p>這張表的第二列是 DAX 最常被誤解的點：<strong>query cache 不會因為底層某筆 item 被改而失效</strong>。item cache 走 write-through、寫入時會更新；但 query cache 存的是「整個結果集」、DAX 無法知道某筆新寫入是否該進某個已快取的 query 結果、所以 query cache 只靠 TTL 過期。這代表 query 結果可能 stale 到一個 TTL 週期。</p>
<blockquote>
<p><strong>Scope warning</strong>：「item cache 預設 TTL 5 分鐘」、「query cache 預設 TTL 5 分鐘」這些預設值屬 AWS vendor 規格、可在 cluster 設定調整、實作時 cross-verify 官方 doc。本文不含 production case 揭露的 DAX TTL 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache-invalidation</a>、<a href="/blog/backend/knowledge-cards/write-through-cache/" data-link-title="Write-Through Cache" data-link-desc="說明寫入時同步更新快取與正式來源的策略">write-through-cache</a>、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a>、<a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache-hit-rate</a>。</p>
<h2 id="一致性與-invalidation-邊界">一致性與 invalidation 邊界</h2>
<p>DAX 的一致性語意是它跟「一般 cache-aside」最大的差別、也是踩雷集中區。</p>
<p><strong>write-through 的保證範圍</strong>：</p>
<p>寫入經過 DAX 時、DAX 先寫 DynamoDB、成功後更新自己的 item cache。所以「寫完馬上用 <code>GetItem</code> 讀同一筆」、在 <em>同一個 DAX node</em> 上能讀到新值。但這不是 strong consistency — 多 node cluster 下、寫入只更新 primary 與被路由到的 node、其他 read replica 的 item cache 仍可能 stale 到 TTL。</p>
<p><strong>strongly consistent read 繞過 cache</strong>：</p>
<p>DAX 只服務 eventually consistent read。application 若要求 strongly consistent read（<code>ConsistentRead=True</code>）、DAX 直接 pass through 到 DynamoDB、不經 cache、也享受不到 microsecond latency。這是設計上的取捨 — DAX 換 latency 的代價是放棄 strong consistency。read-your-write 嚴格場景不能靠 DAX。</p>
<p><strong>query cache stale 的真實後果</strong>：</p>
<p>application 用 <code>Query</code> 列「某 user 的 active order」、結果被 query cache 快取；user 新建一筆 order、item cache 更新了該筆 item、但 <em>列表 query 的 cache 沒失效</em>、user 重整頁面在 TTL 內看不到新訂單。修法不是調 DAX、是判斷「這個 query 能不能接受 TTL 內 stale」— 不能接受的、該 query 不要走 DAX（直接打 DynamoDB）、或縮短該類 query 的 TTL。</p>
<blockquote>
<p><strong>Scope warning</strong>：上述一致性語意屬 DAX vendor 規格 + 通用 cache 工程知識、非 production case 揭露；實際 staleness 視 cluster node 數、TTL 配置與讀寫分布而定。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從 read pattern 評估到上線的 6 步流程。</p>
<h4 id="step-1確認-read-pattern-適配">Step 1：確認 read pattern 適配</h4>
<p>在加 DAX 前、用 CloudWatch 看目標 table 的 read:write ratio 跟 read 的 key 重複度：</p>
<ul>
<li>read:write 高（讀遠多於寫）+ 重複讀同一組 key → 適合</li>
<li>寫密集 / 每次讀不同 key / 大量 strongly consistent read → 不適合（回頭看 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design DAX 觸發條件</a>）</li>
</ul>
<h4 id="step-2cluster-sizing">Step 2：cluster sizing</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">node 數 = 讀峰值 throughput / 單 node 容量 + 1（容錯餘量）
</span></span><span class="line"><span class="ln">2</span><span class="cl">node class = 依 working set 大小選（cache 要能裝下熱資料）</span></span></code></pre></div><p>跨至少 2 個 AZ、確保 primary 故障有 replica 接手。</p>
<h4 id="step-3application-切換-client">Step 3：application 切換 client</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">amazondax</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 原本：dynamodb = boto3.resource(&#34;dynamodb&#34;)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">dax</span> <span class="o">=</span> <span class="n">amazondax</span><span class="o">.</span><span class="n">AmazonDaxClient</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="n">endpoint_url</span><span class="o">=</span><span class="s2">&#34;dax://my-cluster.xxx.dax-clusters.region.amazonaws.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">table</span> <span class="o">=</span> <span class="n">dax</span><span class="o">.</span><span class="n">Table</span><span class="p">(</span><span class="s2">&#34;orders&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># API 不變、讀寫自動經過 DAX</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="s2">&#34;ORDER#123&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">})</span></span></span></code></pre></div><h4 id="step-4分流-strongly-consistent-read">Step 4：分流 strongly consistent read</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 需要 strong 的讀直接走 DynamoDB、不要走 DAX</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">ddb_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">,</span> <span class="n">ConsistentRead</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>   <span class="c1"># 繞過 cache</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 可接受 eventual 的讀走 DAX</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">dax_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">)</span>                          <span class="c1"># 走 cache</span></span></span></code></pre></div><p>application 要明確區分哪些讀路徑能接受 stale、哪些不能；不能接受的不走 DAX。</p>
<h4 id="step-5設定-ttl-與監控-hit-rate">Step 5：設定 TTL 與監控 hit rate</h4>
<p>依資料變動頻率設 item / query cache TTL：變動慢的 metadata 可設長 TTL、變動快的設短或不快取。上線後盯 <code>CacheHitRate</code>。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 驗證 hit rate 達預期、確認 DAX 真的減少 DynamoDB 讀</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># CloudWatch: DAX CacheHits / (CacheHits + CacheMisses)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 同時看 DynamoDB ConsumedReadCapacityUnits 是否下降</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：DAX 可隨時 detach — application 端把 DAX endpoint 換回 DynamoDB endpoint 即可、無資料遷移；DAX 只是讀路徑加速層、不持有唯一資料。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1把-dax-當預設配置">Case 1：把 DAX 當預設配置</h4>
<p>寫密集 / 低 hit rate workload 加 DAX、invalidation 開銷 + cluster 成本 &gt; cache 收益。修法：先確認 read pattern 適配（Step 1）、DAX 是讀峰值補位不是預設（觸發條件 SSoT 在 gsi-lsi-design）。</p>
<h4 id="case-2以為-query-cache-會即時反映寫入">Case 2：以為 query cache 會即時反映寫入</h4>
<p>寫入後列表 query 在 TTL 內看不到新資料、被當成 bug 長時間誤查。修法：理解 query cache 只靠 TTL 失效（不是 bug 是設計）；強一致列表需求的 query 不走 DAX、或縮短 TTL。</p>
<h4 id="case-3strongly-consistent-read-全走-dax-還抱怨不快">Case 3：strongly consistent read 全走 DAX 還抱怨不快</h4>
<p>application 全程 <code>ConsistentRead=True</code>、DAX 全部 pass through、等於沒裝 DAX 還多付 cluster 錢。修法：分流 — strong read 直接打 DynamoDB、eventual read 才走 DAX。</p>
<h4 id="case-4cluster-單-az--單-node">Case 4：cluster 單 AZ / 單 node</h4>
<p>省成本只開單 node、primary 故障時讀路徑整個失效、回退到 DynamoDB 瞬間流量尖峰。修法：跨 2+ AZ、primary + replica；DAX 故障的 fallback 路徑（直連 DynamoDB）要先測過。這個 Case 的失敗代價跟其他 Case 不對稱 — 其餘 Case 多是成本浪費或延遲沒降、detach DAX 即可回復；單 AZ / 單 node 故障是讀路徑硬中斷、回退瞬間把原本被 cache 吸收的讀峰值全打回 DynamoDB、若 base table 的 RCU 或 on-demand burst 餘量沒預留、會引發 throttling 連鎖。回退路徑要按「DAX 全失效時的讀峰值」預估 DynamoDB 側容量、而非平時被 cache 削減後的讀量。</p>
<h4 id="case-5working-set-超過-cache-容量">Case 5：working set 超過 cache 容量</h4>
<p>熱資料超過 node memory、cache 不斷 evict、hit rate 掉到沒意義。修法：依 working set 選 node class、或縮小快取範圍（只快取真正熱的 access pattern）。</p>
<p><strong>Anti-recommendation</strong>：read:write ratio 低、或 cache hit rate 預期 &lt; 50% 的 workload、不要上 DAX；application 端的 request-level cache 或根本不快取可能更划算。DAX 是 cluster 常駐成本（instance-hour 計）、只在讀峰值持續高才回本。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>CacheHits</code> / <code>CacheMisses</code> / 算出 <code>CacheHitRate</code> — 核心健康指標</li>
<li><code>ItemCacheHits</code> / <code>QueryCacheHits</code> — 分辨兩種快取各自的命中</li>
<li><code>CPUUtilization</code> / <code>EvictedSize</code> — node 是否過載、cache 是否頻繁 evict</li>
<li>DynamoDB 端 <code>ConsumedReadCapacityUnits</code> — 確認 DAX 真的削減了 base 讀取</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% — 重新評估 DAX 是否該存在、或快取範圍是否該收窄</li>
<li><code>EvictedSize</code> 持續高 — working set 超過 cache 容量、要加大 node class</li>
<li>DynamoDB read capacity 沒因 DAX 下降 — read pattern 不適配、DAX 沒發揮作用</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、非 case 揭露；實際閾值依 cost 結構與 latency 目標調整。</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/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="dax-vs-application-side-cache-vs-elasticache">DAX vs application-side cache vs ElastiCache</h3>
<p>DAX 不是唯一的 DynamoDB 讀加速方案。三者責任不同：</p>
<ul>
<li><strong>DAX</strong>：DynamoDB 專屬、API 相容、write-through、零 application cache 邏輯；綁 DynamoDB</li>
<li><strong>application-side cache</strong>（如 in-process LRU）：最低延遲、但每個 instance 各自一份、一致性難管</li>
<li><strong>ElastiCache（Redis / Valkey）</strong>：通用 cache、可跨資料源、但要自己寫 cache-aside 邏輯與 invalidation</li>
</ul>
<p>當快取需求超出單一 DynamoDB table（跨資料源聚合 / 需要 Redis 資料結構如 sorted set leaderboard）、回 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 評估 ElastiCache；DAX 最適配的情境是「純 DynamoDB 讀加速、且不想自行維護 cache 邏輯」。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — DAX 觸發條件 SSoT（讀峰值補位 / Lemino case fact / Capcom derive）在該篇、本篇承接機制層</li>
<li><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">on-demand-vs-provisioned</a> — DAX 削減 base 讀取後、provisioned RCU 規劃要重算</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — strongly consistent read 繞過 DAX、對應 read 一致性軸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — DAX 不解 hot partition、寫熱點仍打到 DynamoDB</li>
<li>替代路由：跨資料源快取 / Redis 資料結構需求 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> ElastiCache</li>
<li>跟 <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 9.C29</a> 互引：DAX 讀峰值補位的 case fact</li>
</ul>
]]></content:encoded></item><item><title>Spanner Graph (2024)：property graph 能力、跟 relational 表共存、適用場景與邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/spanner-graph/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/spanner-graph/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>Spanner Graph&lt;/em>（2024 推出）— 建在 relational 引擎上的 property graph 能力、跟 SQL 表共用同一份資料與 transaction。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="核心定位graph-是-relational-表上的視圖不是另一個資料庫">核心定位：graph 是 relational 表上的視圖、不是另一個資料庫&lt;/h2>
&lt;p>Spanner Graph 的責任是讓「實體之間的多跳關係查詢」用 property graph 模型（node、edge、property）表達、底層仍儲存在 Spanner 的 relational table、graph 與 SQL 共用同一份資料、同一個 transaction、同一套 external consistency。它不是在 Spanner 旁邊掛一個獨立的 graph database、是在既有 relational 表之上定義一層 graph 映射、讓同一份資料能同時被 SQL query 與 GQL graph query 存取。&lt;/p>
&lt;p>把這條定位放最前面、是因為 graph database 常被想成「需要單獨的儲存引擎、單獨的資料同步管線」。Spanner Graph 的設計取捨相反：node table 跟 edge table 就是普通的 Spanner table、graph schema 定義它們之間的映射、查詢時引擎在 relational 儲存上執行圖遍歷。這帶來兩個直接後果 — graph 與 transactional 寫入天然強一致（同一份資料、同一個 commit）、不需要把資料從 OLTP 同步到專用 graph DB;但也意味著 graph 效能受 relational 引擎的特性約束、不是專用 graph engine 的記憶體圖結構。&lt;/p>
&lt;h2 id="問題情境關係查詢在-sql-裡變成難以維護的多層-self-join">問題情境：關係查詢在 SQL 裡變成難以維護的多層 self-JOIN&lt;/h2>
&lt;p>Graph 能力的價值、在「資料本質是關係網絡、但被迫用 relational JOIN 表達多跳查詢」的壓力下浮現。讀者徵兆：反詐欺團隊要查「跟某個可疑帳號在 3 跳內共用過裝置 / 地址 / 付款方式的所有帳號」、寫成 SQL 是 3-4 層 self-JOIN、query 既難寫又難優化;推薦團隊要查「買過 A 的人也買過什麼」的多跳關聯;權限團隊要查「使用者透過群組 / 角色繼承鏈能存取哪些資源」的傳遞閉包。這些查詢的共同形狀是「沿著關係邊走 N 跳」、用 JOIN 表達時跳數越多 SQL 越複雜、優化器越難處理。&lt;/p>
&lt;p>真實壓力場景：金融反詐欺系統把交易、帳號、裝置、地址存在 Spanner、需要即時查可疑帳號的關係網絡;這份資料同時要支援交易的強一致寫入。傳統做法是把資料從 OLTP ETL 到專用 graph DB（Neo4j 等）、付出資料同步延遲 + 兩套系統的運維成本 + graph DB 上的資料不是強一致快照。Spanner Graph 讓「強一致的交易資料」與「圖遍歷查詢」在同一個系統、避開同步管線。&lt;/p>
&lt;p>Case anchor：本主題在 case 庫覆蓋稀薄。9.C10 是 Google internal dogfood case、未展開 graph 能力、且不是 customer-facing 參考。本文 graph 物件模型、GQL 語意、relational 共存機制均以 GCP vendor 規格 + 通用 graph 工程展開、case 僅作「全球大規模 OLTP 之上要做關係查詢」的壓力 anchor。Spanner Graph 是 2024 推出的較新能力、所有能力 claim 屬時間敏感、實作前查官方文件。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>Spanner Graph</em>（2024 推出）— 建在 relational 引擎上的 property graph 能力、跟 SQL 表共用同一份資料與 transaction。</p></blockquote>
<hr>
<h2 id="核心定位graph-是-relational-表上的視圖不是另一個資料庫">核心定位：graph 是 relational 表上的視圖、不是另一個資料庫</h2>
<p>Spanner Graph 的責任是讓「實體之間的多跳關係查詢」用 property graph 模型（node、edge、property）表達、底層仍儲存在 Spanner 的 relational table、graph 與 SQL 共用同一份資料、同一個 transaction、同一套 external consistency。它不是在 Spanner 旁邊掛一個獨立的 graph database、是在既有 relational 表之上定義一層 graph 映射、讓同一份資料能同時被 SQL query 與 GQL graph query 存取。</p>
<p>把這條定位放最前面、是因為 graph database 常被想成「需要單獨的儲存引擎、單獨的資料同步管線」。Spanner Graph 的設計取捨相反：node table 跟 edge table 就是普通的 Spanner table、graph schema 定義它們之間的映射、查詢時引擎在 relational 儲存上執行圖遍歷。這帶來兩個直接後果 — graph 與 transactional 寫入天然強一致（同一份資料、同一個 commit）、不需要把資料從 OLTP 同步到專用 graph DB;但也意味著 graph 效能受 relational 引擎的特性約束、不是專用 graph engine 的記憶體圖結構。</p>
<h2 id="問題情境關係查詢在-sql-裡變成難以維護的多層-self-join">問題情境：關係查詢在 SQL 裡變成難以維護的多層 self-JOIN</h2>
<p>Graph 能力的價值、在「資料本質是關係網絡、但被迫用 relational JOIN 表達多跳查詢」的壓力下浮現。讀者徵兆：反詐欺團隊要查「跟某個可疑帳號在 3 跳內共用過裝置 / 地址 / 付款方式的所有帳號」、寫成 SQL 是 3-4 層 self-JOIN、query 既難寫又難優化;推薦團隊要查「買過 A 的人也買過什麼」的多跳關聯;權限團隊要查「使用者透過群組 / 角色繼承鏈能存取哪些資源」的傳遞閉包。這些查詢的共同形狀是「沿著關係邊走 N 跳」、用 JOIN 表達時跳數越多 SQL 越複雜、優化器越難處理。</p>
<p>真實壓力場景：金融反詐欺系統把交易、帳號、裝置、地址存在 Spanner、需要即時查可疑帳號的關係網絡;這份資料同時要支援交易的強一致寫入。傳統做法是把資料從 OLTP ETL 到專用 graph DB（Neo4j 等）、付出資料同步延遲 + 兩套系統的運維成本 + graph DB 上的資料不是強一致快照。Spanner Graph 讓「強一致的交易資料」與「圖遍歷查詢」在同一個系統、避開同步管線。</p>
<p>Case anchor：本主題在 case 庫覆蓋稀薄。9.C10 是 Google internal dogfood case、未展開 graph 能力、且不是 customer-facing 參考。本文 graph 物件模型、GQL 語意、relational 共存機制均以 GCP vendor 規格 + 通用 graph 工程展開、case 僅作「全球大規模 OLTP 之上要做關係查詢」的壓力 anchor。Spanner Graph 是 2024 推出的較新能力、所有能力 claim 屬時間敏感、實作前查官方文件。</p>
<h2 id="核心機制node-tableedge-tablegraph-schema-映射">核心機制：node table、edge table、graph schema 映射</h2>
<p>Spanner Graph 用 <em>property graph</em> 模型 — node 代表實體（帳號、裝置）、edge 代表關係（共用、轉帳）、兩者都可帶 property。底層每個 node 類型對應一張 relational table、每個 edge 類型對應一張記錄「來源 PK → 目標 PK」的 relational table、graph schema 用 DDL 把這些表宣告成 node / edge。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 底層仍是普通 relational table
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">Account</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="n">INT64</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">risk_score</span><span class="w"> </span><span class="n">FLOAT64</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">AccountTransfersAccount</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="n">src_id</span><span class="w"> </span><span class="n">INT64</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="n">dst_id</span><span class="w"> </span><span class="n">INT64</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="n">amount</span><span class="w"> </span><span class="nb">NUMERIC</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">src_id</span><span class="p">,</span><span class="w"> </span><span class="n">dst_id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- graph schema 把表映射成 node / edge
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">PROPERTY</span><span class="w"> </span><span class="n">GRAPH</span><span class="w"> </span><span class="n">FraudGraph</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="n">NODE</span><span class="w"> </span><span class="n">TABLES</span><span class="w"> </span><span class="p">(</span><span class="n">Account</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="n">EDGE</span><span class="w"> </span><span class="n">TABLES</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="n">AccountTransfersAccount</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">      </span><span class="k">SOURCE</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">src_id</span><span class="p">)</span><span class="w"> </span><span class="k">REFERENCES</span><span class="w"> </span><span class="n">Account</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="n">DESTINATION</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">dst_id</span><span class="p">)</span><span class="w"> </span><span class="k">REFERENCES</span><span class="w"> </span><span class="n">Account</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="p">);</span></span></span></code></pre></div><p>關鍵是 edge table 的 PK 設計直接決定圖遍歷效能。edge table 通常用 <code>(src_id, dst_id)</code> 當 PK、讓「從某 node 出發的所有 out-edge」落在相鄰的 key range、遍歷時是一次 range scan 而非散落查詢。這個物理 layout 跟 <a href="../schema-migration-interleaved-tables/">interleaved table</a> 的思路相通 — 把一起查的資料在 storage 上放近。</p>
<h3 id="gql-查詢用-pattern-matching-表達遍歷">GQL 查詢：用 pattern matching 表達遍歷</h3>
<p>graph 查詢用 GQL（ISO graph query language）的 pattern matching 語法、把多跳遍歷寫成 path pattern、比多層 SQL JOIN 直觀。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 查跟某帳號 1-3 跳內有轉帳關係的高風險帳號
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">GRAPH</span><span class="w"> </span><span class="n">FraudGraph</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">MATCH</span><span class="w"> </span><span class="p">(</span><span class="n">a</span><span class="p">:</span><span class="n">Account</span><span class="w"> </span><span class="err">{</span><span class="n">id</span><span class="p">:</span><span class="w"> </span><span class="mi">12345</span><span class="err">}</span><span class="p">)</span><span class="o">-</span><span class="p">[:</span><span class="n">AccountTransfersAccount</span><span class="p">]</span><span class="o">-&gt;</span><span class="err">{</span><span class="mi">1</span><span class="p">,</span><span class="mi">3</span><span class="err">}</span><span class="p">(</span><span class="n">b</span><span class="p">:</span><span class="n">Account</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">b</span><span class="p">.</span><span class="n">risk_score</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">8</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">RETURN</span><span class="w"> </span><span class="n">b</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">b</span><span class="p">.</span><span class="n">risk_score</span><span class="p">;</span></span></span></code></pre></div><p><code>-&gt;{1,3}</code> 表達 1 到 3 跳的可變長度路徑 — 這在 SQL 裡需要 recursive CTE 或多個 self-JOIN、在 GQL 裡是一個 pattern。引擎把 pattern 編譯成在底層 relational 表上的遍歷計劃。</p>
<blockquote>
<p><strong>Scope warning</strong>：Spanner Graph 是 2024 推出的能力、GQL 語法、支援的 pattern、graph schema DDL 均屬 GCP 規格且逐版本演進。本文語法為示意、實作前必須 cross-verify <a href="https://cloud.google.com/spanner/docs/graph/overview">Spanner Graph 官方文件</a> 的當前語法與支援範圍、不可依本文當最終依據。</p></blockquote>
<h3 id="graph-與-relational-共存的語意">graph 與 relational 共存的語意</h3>
<p>同一份資料能同時被 SQL 與 GQL 查 — 對 Account 表的 SQL UPDATE 立即反映在 graph 查詢、因為它們是同一份 storage。寫入走標準 Spanner transaction、graph 查詢看到的是 external-consistent 的快照。這個共存是 Spanner Graph 跟「ETL 到專用 graph DB」最根本的差異：沒有同步延遲、graph 看到的就是 OLTP 的當前一致狀態。</p>
<h2 id="操作流程定義-graph查詢驗證遍歷效能">操作流程：定義 graph、查詢、驗證遍歷效能</h2>
<h3 id="step-1設計-node--edge-table-與-pk-layout">Step 1：設計 node / edge table 與 PK layout</h3>
<p>先設計底層 relational table、edge table 的 PK 用 <code>(src, dst)</code> 讓 out-edge 連續。這步是 graph 效能的決定性步驟、也是最難回退的步驟（見失敗模式）。驗證：對「最高頻的遍歷方向」確認 edge table PK 讓該方向的 out-edge 落在連續 key range。</p>
<h3 id="step-2建立-property-graph-schema">Step 2：建立 property graph schema</h3>
<p>用 <code>CREATE PROPERTY GRAPH</code> 宣告 node / edge 映射。驗證：查 information schema 確認 graph 已建立、node / edge 映射符合預期、edge 的 source / destination key 正確 reference 到 node 的 PK。</p>
<h3 id="step-3跑代表性-gql-查詢並量遍歷成本">Step 3：跑代表性 GQL 查詢並量遍歷成本</h3>
<p>用真實業務的代表性遍歷（例如反詐欺的 3 跳查詢）跑 GQL、用 query plan 確認遍歷走 range scan 而非 full scan、量 latency 與掃描的 row 數。驗證點：跳數增加時 latency 的成長曲線 — 圖查詢的成本對「每跳的扇出（fan-out）」非常敏感、高扇出的 node（super node、例如被百萬帳號連到的熱門裝置）會讓遍歷成本急遽放大。</p>
<h3 id="step-4rollback-boundary">Step 4：rollback boundary</h3>
<p>graph schema 本身可加可改（在相容範圍內）、<code>DROP PROPERTY GRAPH</code> 不刪底層 relational 資料 — graph 是視圖層、刪 graph schema 不影響 SQL 存取。真正難回退的是底層 edge table 的 PK 設計（見失敗模式）。所以 rollback boundary 分兩層：graph schema 層可逆、底層 table layout 層接近不可逆。</p>
<h2 id="失敗模式edge-table-layout-設計錯誤的高代價">失敗模式：edge table layout 設計錯誤的高代價</h2>
<p>graph 的失敗模式跟前述機制型文章不同 — 它的核心風險是「資料模型的物理設計錯誤、且代價不可逆」、所以這節用更完整的代價與回退敘事處理、不壓成兩句式。</p>
<h3 id="edge-table-pk-方向選錯最高頻遍歷變成-full-scan">Edge table PK 方向選錯、最高頻遍歷變成 full scan</h3>
<p>這是 graph 設計最高代價、最難回退的失敗。edge table 的 PK 決定哪個遍歷方向是連續 range scan、哪個是散落查詢。若團隊把 PK 設成 <code>(dst_id, src_id)</code>、但 99% 的查詢是「從 src 出發找 dst」、那最高頻的遍歷變成對整張 edge table 的 scan、隨資料量線性退化。</p>
<p>代價之所以高、是因為它不在上線時暴露 — 小資料量下 full scan 也快、效能崩塌在資料長到一定規模、流量打到 production 之後才浮現。徵兆是特定遍歷的 latency 隨 edge table 成長而單調惡化、query plan 顯示 full scan 而非 range scan、Spanner CPU 被掃描打滿。</p>
<p>回退路徑的代價是這個失敗的關鍵：edge table 的 PK 不能 in-place 變更、修正需要建一張新的 edge table（正確 PK 方向）、backfill 全部 edge、更新 graph schema 指向新表、驗證遍歷走 range scan、再 drop 舊表。對 100 億 edge 的圖、backfill 是數小時到數天的 long-running operation、期間要管 capacity 升幅、要保證 graph 查詢在切換期間的正確性。這不是 hotfix、是一次完整的 schema migration。所以這個失敗的真正教訓是「在 Step 1 設計階段就把最高頻遍歷方向定死」、而不是「上線後再優化」 — 設計階段花一天想清楚遍歷方向、勝過上線後花一週重建 edge table。</p>
<h3 id="super-node-讓遍歷扇出急遽放大">Super node 讓遍歷扇出急遽放大</h3>
<p>某些 node 的 degree（連出的 edge 數）極高 — 例如一個被百萬帳號共用的熱門 IP、一個被千萬使用者關注的明星帳號。多跳遍歷經過 super node 時、單跳就扇出百萬條 edge、查詢成本急遽放大、可能拖垮整個 instance。徵兆是「多數遍歷快、少數遍歷極慢」、慢的那些都經過已知的高 degree node。修法不是純技術 — 要在業務層決定如何處理 super node：限制遍歷的 degree（只取前 N 條 edge）、把 super node 的關係單獨建模、或在應用層對經過 super node 的查詢設上限。這個失敗的代價在「它讓 tail latency 不可預測」、容量規劃要把 super node 的扇出當成 worst-case。</p>
<h3 id="把-graph-當專用-graph-db-的全功能替代">把 graph 當專用 graph DB 的全功能替代</h3>
<p>團隊把 Spanner Graph 當 Neo4j 用、期待專用 graph DB 的所有演算法（PageRank、community detection、複雜圖分析）與圖原生效能。Spanner Graph 的強項是「跟強一致 OLTP 共存的關係查詢」、不是「重度圖分析引擎」。徵兆是想跑的圖演算法不在支援範圍、或重度分析查詢效能不如專用引擎。<strong>Anti-recommendation（何時不用）</strong>：純圖分析、不需要跟 OLTP transaction 共用資料、需要豐富圖演算法庫的場景、用專用 graph DB 或圖分析框架;Spanner Graph 的定位是「OLTP 資料順便要做關係查詢」、不是「圖是核心工作負載」。</p>
<h2 id="容量與觀測遍歷扇出是核心容量訊號">容量與觀測：遍歷扇出是核心容量訊號</h2>
<p>graph 查詢的容量壓力不在「資料量」、在「遍歷的扇出與跳數」 — 同樣的資料量、低扇出的遍歷便宜、高扇出的急遽放大。核心觀測是 graph query 掃描的 row 數與 query plan 的遍歷形狀。</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">GQL query 掃描的 row / edge 數    → 遍歷扇出的直接指標
</span></span><span class="line"><span class="ln">2</span><span class="cl">query plan: range scan vs full scan → edge table PK layout 是否匹配遍歷方向
</span></span><span class="line"><span class="ln">3</span><span class="cl">Spanner CPU during graph query    → 高扇出遍歷會打滿 CPU
</span></span><span class="line"><span class="ln">4</span><span class="cl">特定遍歷的 p99 latency 隨資料成長  → edge layout 錯誤的早期訊號</span></span></code></pre></div><p>容量規劃要把「最壞情況遍歷」（經過 super node 的高扇出多跳）當 worst-case 算進 sizing、不能只用平均遍歷成本、回 <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/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 把「遍歷掃描 row 數」跟「Spanner CPU」配成 evidence pair：掃描 row 數突增且 CPU 飽和、是某個查詢撞到 super node 或 layout 退化。</p>
<blockquote>
<p><strong>Scope warning</strong>：Spanner Graph 的具體效能特性、query plan 工具、graph 相關 metric 屬 2024 後的新能力規格、隨版本演進、cross-verify 官方文件、非 9.C10 case 揭露。</p></blockquote>
<h2 id="邊界與整合何時用-graph何時用純-relational-或專用-graph-db">邊界與整合：何時用 graph、何時用純 relational 或專用 graph DB</h2>
<h3 id="選-spanner-graph-的條件">選 Spanner Graph 的條件</h3>
<p>資料已在 Spanner、本質是關係網絡、需要多跳遍歷查詢、且這份資料同時要支援強一致的 OLTP 寫入 — 這是 Spanner Graph 的適用條件。它的核心價值是「免去 OLTP → graph DB 的同步管線、graph 看到的就是強一致的當前資料」。反詐欺、權限傳遞、即時推薦這類「在交易資料上做關係查詢」的場景最適合。</p>
<h3 id="何時用純-relational">何時用純 relational</h3>
<p>關係查詢的跳數固定且淺（1-2 跳）、用標準 SQL JOIN 已足夠清晰、不值得引入 graph schema 的額外概念。graph 的價值隨跳數與遍歷複雜度上升、淺查詢用 relational 反而簡單。判準是：若查詢用 JOIN 寫起來不痛、就不需要 graph。</p>
<h3 id="何時用專用-graph-db">何時用專用 graph DB</h3>
<p>純圖工作負載、需要豐富圖演算法（PageRank、最短路徑、社群偵測）、不需要跟 OLTP transaction 共用強一致資料 — 用專用 graph DB 或圖分析框架。Spanner Graph 不是要取代專用 graph engine、是要服務「OLTP 順便要關係查詢」的場景。把重度圖分析硬塞 Spanner Graph 是用錯工具。</p>
<h3 id="sibling-deep-articles-路由">Sibling deep articles 路由</h3>
<ul>
<li><a href="../schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a>：edge table 的 PK layout 思路跟 interleaved table 相通、都是「把一起查的資料在 storage 上放近」、且 graph 的 edge layout 錯誤回退跟 schema migration 同代價</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：graph 查詢繼承 external consistency、graph 看到的快照跟 OLTP 一致</li>
<li><a href="../bigquery-federation/">bigquery-federation</a>：重度圖分析若超出 graph 即時查詢範圍、可考慮把資料分到分析層</li>
</ul>
<h3 id="跟-knowledge-card-的互引">跟 knowledge card 的互引</h3>
<ul>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed-sql</a> — Spanner Graph 是 distributed SQL 引擎上的 property graph 層、繼承其分散式語意</li>
</ul>
<h3 id="跟其他-vendor--章節的對照">跟其他 vendor / 章節的對照</h3>
<ul>
<li><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 vendor</a>：DynamoDB 的 adjacency list 設計是另一種「在 KV 上做關係查詢」的路線、跟 Spanner Graph 的 native graph 是不同取捨</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>：graph 是 Spanner 在 OLTP 之上擴展的查詢能力之一</li>
</ul>
]]></content:encoded></item><item><title>9.C36 Coinbase：MongoDB 撐 Ruby 單體 + 1.5M reads/sec identity 服務</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/</guid><description>&lt;p>這個案例的核心責任是說明「document database 在大規模 OLTP 場景如何撐住」。Coinbase 從 Ruby on Rails 單體 + MongoDB 起家、八年後仍保留 MongoDB 作為主資料層、並把 connection pooling、ML 預測擴容、cache + freshness token 都疊在 document model 上。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> 對照 — Microsoft 365 走「遷出 MongoDB、保留 document API」、Coinbase 走「保留 MongoDB、補周邊工具」。兩條路徑都揭露 MongoDB 在 production 主角位置會遇到什麼壓力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Coinbase MongoDB 平台的關鍵數字（引自 &lt;a href="https://www.coinbase.com/blog/scaling-connections-with-ruby-and-mongodb">Coinbase Engineering Blog&lt;/a> 與 &lt;a href="https://www.mongodb.com/solutions/customer-case-studies/coinbase">MongoDB customer case study&lt;/a>）：&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>Users 服務尖峰讀取&lt;/td>
 &lt;td>1.5M reads / sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy 時 MongoDB 連線尖峰&lt;/td>
 &lt;td>~60K connections / minute（單 cluster）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>mongobetween 後連線降幅&lt;/td>
 &lt;td>30K → ~2K（一個量級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MongoDB cluster 數量&lt;/td>
 &lt;td>many clusters（多服務 federated）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加密貨幣 surge 擴容時間&lt;/td>
 &lt;td>70 分鐘 → 25 分鐘（-64%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ML 預測擴容領先窗&lt;/td>
 &lt;td>60 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache 命中後跳過 DB&lt;/td>
 &lt;td>是（Memcached query-cache）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：MongoDB Atlas（主資料層）、DynamoDB（部分 workload 的 federated store）、Memcached（query result cache）、自研 mongobetween proxy（連線多工）、Ruby on Rails 單體 + 多個 Fragment APIs、ML 預測模型驅動 cluster auto-scaling。&lt;/p>
&lt;p>關鍵負載形狀：「加密貨幣價格突發 + 用戶交易需求湧入」雙峰疊加。價格 alert 觸發 read 爆量（users / portfolio 查詢）、下單觸發 write 爆量（order book / wallet 寫入）。兩種峰值不像 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings&lt;/a> 的 Super Bowl 事件型可預測、是隨外部市場波動的 &lt;em>low-latency-sustained 中夾雜 surge&lt;/em>。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Coinbase MongoDB 的工程選擇揭露三個 document database 在 production 主角位置的設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>MongoDB + Ruby 連線爆炸需要外部 connection pool&lt;/strong>：CRuby 因為 GVL 必須每 CPU core 起一個 process、blue-green 部署期間 instance 數量 ×2、連線數隨之 ×2、單一 cluster 看到 60K 連線/分鐘。原生 MongoDB driver 沒有跨 process 的 connection pool — 跟 PostgreSQL 走 pgbouncer 是同樣需求、所以 Coinbase 自建 &lt;a href="https://github.com/coinbase/mongobetween">mongobetween&lt;/a> 做多工。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取&lt;/a> 的 connection storm 問題、document database 不會自動解決、要主動補工具。&lt;/li>
&lt;li>&lt;strong>document model 撐 1.5M reads/sec 靠 cache + freshness token&lt;/strong>：直接打 MongoDB 不可能撐 1.5M reads/sec — Coinbase 在 users 服務前面加 Memcached query cache、單 document query 先查 cache。但 cache + write 會有一致性問題、所以引入 OCC version 跟 &lt;em>freshness token&lt;/em>：write 成功後給 client 一個 token、client 之後 read 帶 token、server 保證返回的資料版本 ≥ token、必要時 bypass cache 直接打 DB。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a> 的 read-after-write 設計。&lt;/li>
&lt;li>&lt;strong>加密貨幣 surge 用 ML 預測、不靠 reactive scaling&lt;/strong>：cluster 擴容要 70 分鐘、傳統 CPU / queue 觸發的 reactive scaling 在 surge 開始時才動、來不及。Coinbase 訓練 ML 模型分析價格資料、提前 60 分鐘預測流量、預先擴容。把擴容時間從 70 分鐘壓到 25 分鐘是 trigger 提前、不是擴容本身變快。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 的 predictive scaling。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「document database 在大規模 OLTP 場景如何撐住」。Coinbase 從 Ruby on Rails 單體 + MongoDB 起家、八年後仍保留 MongoDB 作為主資料層、並把 connection pooling、ML 預測擴容、cache + freshness token 都疊在 document model 上。跟 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 對照 — Microsoft 365 走「遷出 MongoDB、保留 document API」、Coinbase 走「保留 MongoDB、補周邊工具」。兩條路徑都揭露 MongoDB 在 production 主角位置會遇到什麼壓力。</p>
<h2 id="觀察">觀察</h2>
<p>Coinbase MongoDB 平台的關鍵數字（引自 <a href="https://www.coinbase.com/blog/scaling-connections-with-ruby-and-mongodb">Coinbase Engineering Blog</a> 與 <a href="https://www.mongodb.com/solutions/customer-case-studies/coinbase">MongoDB customer case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Users 服務尖峰讀取</td>
          <td>1.5M reads / sec</td>
      </tr>
      <tr>
          <td>Deploy 時 MongoDB 連線尖峰</td>
          <td>~60K connections / minute（單 cluster）</td>
      </tr>
      <tr>
          <td>mongobetween 後連線降幅</td>
          <td>30K → ~2K（一個量級）</td>
      </tr>
      <tr>
          <td>MongoDB cluster 數量</td>
          <td>many clusters（多服務 federated）</td>
      </tr>
      <tr>
          <td>加密貨幣 surge 擴容時間</td>
          <td>70 分鐘 → 25 分鐘（-64%）</td>
      </tr>
      <tr>
          <td>ML 預測擴容領先窗</td>
          <td>60 分鐘</td>
      </tr>
      <tr>
          <td>Cache 命中後跳過 DB</td>
          <td>是（Memcached query-cache）</td>
      </tr>
  </tbody>
</table>
<p>服務組合：MongoDB Atlas（主資料層）、DynamoDB（部分 workload 的 federated store）、Memcached（query result cache）、自研 mongobetween proxy（連線多工）、Ruby on Rails 單體 + 多個 Fragment APIs、ML 預測模型驅動 cluster auto-scaling。</p>
<p>關鍵負載形狀：「加密貨幣價格突發 + 用戶交易需求湧入」雙峰疊加。價格 alert 觸發 read 爆量（users / portfolio 查詢）、下單觸發 write 爆量（order book / wallet 寫入）。兩種峰值不像 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 的 Super Bowl 事件型可預測、是隨外部市場波動的 <em>low-latency-sustained 中夾雜 surge</em>。</p>
<h2 id="判讀">判讀</h2>
<p>Coinbase MongoDB 的工程選擇揭露三個 document database 在 production 主角位置的設計重點。</p>
<ol>
<li><strong>MongoDB + Ruby 連線爆炸需要外部 connection pool</strong>：CRuby 因為 GVL 必須每 CPU core 起一個 process、blue-green 部署期間 instance 數量 ×2、連線數隨之 ×2、單一 cluster 看到 60K 連線/分鐘。原生 MongoDB driver 沒有跨 process 的 connection pool — 跟 PostgreSQL 走 pgbouncer 是同樣需求、所以 Coinbase 自建 <a href="https://github.com/coinbase/mongobetween">mongobetween</a> 做多工。對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a> 的 connection storm 問題、document database 不會自動解決、要主動補工具。</li>
<li><strong>document model 撐 1.5M reads/sec 靠 cache + freshness token</strong>：直接打 MongoDB 不可能撐 1.5M reads/sec — Coinbase 在 users 服務前面加 Memcached query cache、單 document query 先查 cache。但 cache + write 會有一致性問題、所以引入 OCC version 跟 <em>freshness token</em>：write 成功後給 client 一個 token、client 之後 read 帶 token、server 保證返回的資料版本 ≥ token、必要時 bypass cache 直接打 DB。對應 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a> 的 read-after-write 設計。</li>
<li><strong>加密貨幣 surge 用 ML 預測、不靠 reactive scaling</strong>：cluster 擴容要 70 分鐘、傳統 CPU / queue 觸發的 reactive scaling 在 surge 開始時才動、來不及。Coinbase 訓練 ML 模型分析價格資料、提前 60 分鐘預測流量、預先擴容。把擴容時間從 70 分鐘壓到 25 分鐘是 trigger 提前、不是擴容本身變快。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的 predictive scaling。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「1.5M reads/sec」是 users 服務 <em>加上 cache</em> 的數字、不是 MongoDB cluster 純讀取數字。讀案例時要區分「應用層觀察到」跟「DB 層實際承擔」。</li>
<li>mongobetween 是 Coinbase 特殊環境（Ruby + GVL + blue-green）的產物。Go / Java / Node.js 應用因為原生支援連線多工、通常不需要這層 proxy。</li>
<li>ML 預測有 false positive / false negative — 預測錯時要嘛浪費容量、要嘛 surge 真來時擋不住。Coinbase 沒揭露準確率、所以仍保留 reactive scaling 作為 safety net。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>document database 撐大規模 OLTP 要主動補 connection pool</strong>：MongoDB 原生 connection 模式對「process 數多 + deploy 重」的環境會爆。應用層或 sidecar proxy 做多工是基線設計。對應 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">01.10 KV / Document DB 容量規劃</a>。</li>
<li><strong>freshness token 是 read-after-write 一致性的可重用模式</strong>：比 strong consistency（性能差）跟 eventually consistent（read 不到剛寫的）更精細的中間路徑。token 機制可以推廣到任何「主要 eventually consistent、少數 read 要求最新」的場景。</li>
<li><strong>predictive scaling 適用於「外部訊號可預測流量」的服務</strong>：加密貨幣價格、賽事行程、票務開賣時間都是外部訊號。比 reactive scaling 早一個擴容週期出手。對應 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> 的 AI 預測式擴容。</li>
<li><strong>federated DB（MongoDB + DynamoDB）按 workload 分流</strong>：document-shaped 用 MongoDB、access pattern 固定的 KV 用 DynamoDB。不是「全用 MongoDB」也不是「全遷 DynamoDB」、是按 workload 形狀分。對應 <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> 的多 DB 整合反例（Netflix 走整合方向、Coinbase 走 federated）。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>AWS：MongoDB Atlas + ElastiCache + DynamoDB（Coinbase 配置）</li>
<li>GCP：MongoDB Atlas on GCP + Memorystore + Firestore（document API）</li>
<li>Azure：Cosmos DB MongoDB API + Cache for Redis、不需要 Atlas</li>
<li>mongobetween 風格的 proxy：PostgreSQL 走 pgbouncer / pgcat、MongoDB 走 mongobetween / mongoproxy</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 MongoDB 大規模 production → <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a> + <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">01.10 KV / Document DB 容量規劃</a></li>
<li>想做 read-after-write 一致性設計 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a></li>
<li>想做 predictive scaling → <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li>想對照 MongoDB 遷出 / 保留決策 → <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（遷到 Cosmos DB MongoDB API）</li>
<li>想理解 connection storm 問題 → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a></li>
<li>想深入 connection / proxy 治理與 cache 層 → <a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">MongoDB connection 管理與 cache 層</a></li>
<li>想做 replica set 讀寫分離設計 → <a href="/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/" data-link-title="MongoDB Replica Set Read Preference：DB 層 causal session vs cache 層 freshness token" data-link-desc="MongoDB read preference 五擇一 &#43; read concern &#43; causal consistency session 機制；DB 層機制解 cluster 內 read-your-own-write、cache 層 freshness token 解跨層 read-after-write、大規模 OLTP 必須兩層合用">MongoDB replica set read preference</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.coinbase.com/blog/scaling-connections-with-ruby-and-mongodb">Coinbase：Scaling connections with Ruby and MongoDB</a></li>
<li><a href="https://www.coinbase.com/blog/scaling-identity-how-coinbase-serves-1.5M-reads-second">Coinbase：Scaling Identity - How Coinbase Serves 1.5M Reads/Second</a></li>
<li><a href="https://www.coinbase.com/blog/how-we-do-mongodb-migrations-at-coinbase">Coinbase：How We Do MongoDB Migrations at Coinbase</a></li>
<li><a href="https://www.mongodb.com/solutions/customer-case-studies/coinbase">MongoDB customer case study：Coinbase Decreases Scaling Time</a></li>
<li><a href="https://github.com/coinbase/mongobetween">mongobetween GitHub repository</a></li>
</ul>
]]></content:encoded></item><item><title>3.C36 Intelecy：工業 IoT 即時感測 + 多租戶</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/</guid><description>&lt;p>這個案例的核心責任是說明 edge gateway 從本地 KV 演進到 JetStream 的決策訊號。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Intelecy 在工廠端 gateway 接「數萬個 sensor」、要求 &amp;lt; 2 秒往返延遲做即時 ML 推論、需要多租戶安全隔離與雲端無鎖定方案。Gateway 把 process data 寫進 Synadia Cloud topic。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>從 BoltDB 本地快取 → JetStream 持久化的演進、揭露「無 durable layer 時 edge gateway 自己要做存儲、加 JetStream 後可放掉本地 BoltDB」的決策訊號。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：JetStream stream 設計 / Subject-based ACL + 多租戶（sensor 隔離）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &amp;#43; JetStream &amp;#43; KV &amp;#43; Object Store。">3.C37 MachineMetrics&lt;/a>（同類對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.synadia.com/blog/how-intelecy-optimizes-factory-processes-with-nats-ngs-and-jetstream">How Intelecy Optimizes Factory Processes with NATS, NGS and JetStream&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 edge gateway 從本地 KV 演進到 JetStream 的決策訊號。</p>
<h2 id="觀察">觀察</h2>
<p>Intelecy 在工廠端 gateway 接「數萬個 sensor」、要求 &lt; 2 秒往返延遲做即時 ML 推論、需要多租戶安全隔離與雲端無鎖定方案。Gateway 把 process data 寫進 Synadia Cloud topic。</p>
<h2 id="判讀">判讀</h2>
<p>從 BoltDB 本地快取 → JetStream 持久化的演進、揭露「無 durable layer 時 edge gateway 自己要做存儲、加 JetStream 後可放掉本地 BoltDB」的決策訊號。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：JetStream stream 設計 / Subject-based ACL + 多租戶（sensor 隔離）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37 MachineMetrics</a>（同類對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.synadia.com/blog/how-intelecy-optimizes-factory-processes-with-nats-ngs-and-jetstream">How Intelecy Optimizes Factory Processes with NATS, NGS and JetStream</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB Streams 與 Lambda 事件驅動：CDC、shard 順序保證、消費模式與失敗處理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>訂單寫進 DynamoDB 後、搜尋索引要更新、快取要失效、要推一筆通知、要寫一筆 audit。第一版 application 在寫訂單的同一段 code 裡同步做完這四件事、結果單一步驟（推通知的外部 API）變慢、整個寫訂單路徑被拖垮。第二版改成「另一個 service 每 10 秒輪詢 table 撈新資料」、輪詢既貴（全表 scan）又慢（最差 10 秒延遲）。兩個痛點都指向同一個缺口 — 資料變更需要一條可靠、低延遲、不污染寫路徑的下游通道。這正是 DynamoDB Streams 的責任。本文展開 Streams 的 record 結構、順序保證的真實邊界、消費模式選擇與失敗處理。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>事件機制前提：先確認 workload 適配 DynamoDB&lt;/strong>：事件驅動機制是已選 DynamoDB 後的議題；選型本身先過 workload 適配 4 軸 — PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定。判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。本文聚焦 &lt;em>已選 DynamoDB&lt;/em> 後、把資料變更導向下游的事件機制。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制stream-record-與-view-type">核心機制：Stream record 與 view type&lt;/h2>
&lt;p>DynamoDB Streams 是 table 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture&lt;/a> 通道 — 把 item 層級的 insert / modify / delete 變成一條時間排序的事件流。開啟後、每筆寫入產生一筆 stream record。&lt;/p>
&lt;p>&lt;strong>view type 決定 record 帶什麼&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>StreamViewType&lt;/th>
 &lt;th>record 內容&lt;/th>
 &lt;th>典型用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>KEYS_ONLY&lt;/code>&lt;/td>
 &lt;td>只有被改 item 的 key&lt;/td>
 &lt;td>下游自己回查、最省&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>NEW_IMAGE&lt;/code>&lt;/td>
 &lt;td>寫入後的完整新 item&lt;/td>
 &lt;td>同步到搜尋索引 / 快取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>OLD_IMAGE&lt;/code>&lt;/td>
 &lt;td>寫入前的舊 item&lt;/td>
 &lt;td>audit「改了什麼」、刪除留底&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>NEW_AND_OLD_IMAGES&lt;/code>&lt;/td>
 &lt;td>新舊都帶&lt;/td>
 &lt;td>算 diff、條件性下游處理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>view type 在開 stream 時定、改要重開 stream。選 &lt;code>NEW_AND_OLD_IMAGES&lt;/code> 最方便但 record 最大（影響 Lambda payload 與成本）；下游只需 key 就回查的、選 &lt;code>KEYS_ONLY&lt;/code>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「stream record 保留 24 小時」、「Lambda 單次 batch 上限」這些屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc。本文不含 production case 揭露的 stream 配置數字。&lt;/p>&lt;/blockquote>
&lt;p>對應 knowledge card：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change-data-capture&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a>。&lt;/p>
&lt;h2 id="順序保證的真實邊界">順序保證的真實邊界&lt;/h2>
&lt;p>這是 Streams 最常被誤解的點 — 「stream 是有序的」這句話只在特定範圍成立。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>訂單寫進 DynamoDB 後、搜尋索引要更新、快取要失效、要推一筆通知、要寫一筆 audit。第一版 application 在寫訂單的同一段 code 裡同步做完這四件事、結果單一步驟（推通知的外部 API）變慢、整個寫訂單路徑被拖垮。第二版改成「另一個 service 每 10 秒輪詢 table 撈新資料」、輪詢既貴（全表 scan）又慢（最差 10 秒延遲）。兩個痛點都指向同一個缺口 — 資料變更需要一條可靠、低延遲、不污染寫路徑的下游通道。這正是 DynamoDB Streams 的責任。本文展開 Streams 的 record 結構、順序保證的真實邊界、消費模式選擇與失敗處理。</p>
<blockquote>
<p><strong>事件機制前提：先確認 workload 適配 DynamoDB</strong>：事件驅動機制是已選 DynamoDB 後的議題；選型本身先過 workload 適配 4 軸 — PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定。判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。本文聚焦 <em>已選 DynamoDB</em> 後、把資料變更導向下游的事件機制。</p></blockquote>
<h2 id="核心機制stream-record-與-view-type">核心機制：Stream record 與 view type</h2>
<p>DynamoDB Streams 是 table 的 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture</a> 通道 — 把 item 層級的 insert / modify / delete 變成一條時間排序的事件流。開啟後、每筆寫入產生一筆 stream record。</p>
<p><strong>view type 決定 record 帶什麼</strong>：</p>
<table>
  <thead>
      <tr>
          <th>StreamViewType</th>
          <th>record 內容</th>
          <th>典型用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>KEYS_ONLY</code></td>
          <td>只有被改 item 的 key</td>
          <td>下游自己回查、最省</td>
      </tr>
      <tr>
          <td><code>NEW_IMAGE</code></td>
          <td>寫入後的完整新 item</td>
          <td>同步到搜尋索引 / 快取</td>
      </tr>
      <tr>
          <td><code>OLD_IMAGE</code></td>
          <td>寫入前的舊 item</td>
          <td>audit「改了什麼」、刪除留底</td>
      </tr>
      <tr>
          <td><code>NEW_AND_OLD_IMAGES</code></td>
          <td>新舊都帶</td>
          <td>算 diff、條件性下游處理</td>
      </tr>
  </tbody>
</table>
<p>view type 在開 stream 時定、改要重開 stream。選 <code>NEW_AND_OLD_IMAGES</code> 最方便但 record 最大（影響 Lambda payload 與成本）；下游只需 key 就回查的、選 <code>KEYS_ONLY</code>。</p>
<blockquote>
<p><strong>Scope warning</strong>：「stream record 保留 24 小時」、「Lambda 單次 batch 上限」這些屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc。本文不含 production case 揭露的 stream 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change-data-capture</a>、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>。</p>
<h2 id="順序保證的真實邊界">順序保證的真實邊界</h2>
<p>這是 Streams 最常被誤解的點 — 「stream 是有序的」這句話只在特定範圍成立。</p>
<p><strong>保證範圍</strong>：</p>
<ul>
<li>stream 切成多個 shard、每個 shard 對應 table 的一組 partition</li>
<li><strong>同一 partition key 的所有變更、進同一個 shard、在 shard 內嚴格時間排序</strong></li>
<li>跨 shard <em>沒有</em> 全域順序保證</li>
</ul>
<p>這代表：同一筆訂單（同 PK）的 create → update → delete 一定按序到下游；但訂單 A 跟訂單 B（不同 PK、可能不同 shard）的相對順序不保證。下游若依賴「跨實體的全域順序」、會踩雷。</p>
<p><strong>shard split / merge</strong>：</p>
<p>table partition 會隨資料量與流量 split、stream shard 跟著變動。消費端要能處理 shard 生命週期（Lambda event source mapping 自動處理；自己用 SDK 拉的要處理 shard iterator 的 parent-child 關係）。</p>
<p><strong>順序 + 冪等的組合</strong>：</p>
<p>Lambda 消費 stream 是 <em>at-least-once</em> — 同一筆 record 可能被送兩次（retry、shard 重平衡）。下游處理必須冪等：用 record 的 sequence number 或業務鍵去重、不能假設「每筆只處理一次」。每筆訊息帶獨立 message_id 的事件流天然適合 — message_id 當冪等鍵、重送不重複發。</p>
<blockquote>
<p><strong>Scope warning</strong>：上述順序與 at-least-once 語意屬 Streams vendor 規格 + 通用事件處理工程、非 production case 揭露。</p></blockquote>
<h2 id="消費模式lambda-vs-kinesis">消費模式：Lambda vs Kinesis</h2>
<p>兩條主要消費路徑、責任與運維成本不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Lambda event source mapping</th>
          <th>Kinesis Data Streams for DynamoDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模式</td>
          <td>push（DynamoDB 觸發 Lambda）</td>
          <td>pull（消費端自己拉）</td>
      </tr>
      <tr>
          <td>retention</td>
          <td>stream 原生較短</td>
          <td>較長（可重播更久）</td>
      </tr>
      <tr>
          <td>消費者數</td>
          <td>適合單一 / 少量消費者</td>
          <td>適合多消費者 fan-out</td>
      </tr>
      <tr>
          <td>運維</td>
          <td>幾乎零（managed trigger）</td>
          <td>要管 Kinesis consumer / KCL</td>
      </tr>
      <tr>
          <td>重播能力</td>
          <td>受 stream retention 限制</td>
          <td>retention 內可重播</td>
      </tr>
  </tbody>
</table>
<p>多數「寫入後觸發一個下游動作」用 Lambda event source mapping 最簡單。需要長 retention、多消費者 fan-out、或要重播歷史變更的、用 Kinesis Data Streams for DynamoDB。</p>
<p><strong>Lambda event source mapping 的關鍵旋鈕</strong>：</p>
<ul>
<li>batch size：一次給 Lambda 幾筆 record（吞吐 vs 延遲）</li>
<li>batch window：湊滿 batch 或等多久才觸發（低流量時的延遲控制）</li>
<li>parallelization factor：一個 shard 並行幾個 Lambda（提升單 shard 吞吐、但犧牲 shard 內嚴格順序）</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：parallelization factor &gt; 1 會在單 shard 內並行處理、放寬順序保證；需要嚴格順序的維持 factor = 1。具體上限屬 vendor 規格。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從開 stream 到下游上線的 6 步流程。</p>
<h4 id="step-1選-view-type">Step 1：選 view type</h4>
<p>依下游需要什麼決定。同步到搜尋索引要完整新 item → <code>NEW_IMAGE</code>；audit 要看改動 → <code>NEW_AND_OLD_IMAGES</code>；下游自己回查 → <code>KEYS_ONLY</code>。</p>
<h4 id="step-2開-stream">Step 2：開 stream</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">aws dynamodb update-table <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table-name orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --stream-specification <span class="nv">StreamEnabled</span><span class="o">=</span>true,StreamViewType<span class="o">=</span>NEW_AND_OLD_IMAGES</span></span></code></pre></div><h4 id="step-3接-lambda-event-source-mapping">Step 3：接 Lambda event source mapping</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">event</span><span class="p">[</span><span class="s2">&#34;Records&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">event_name</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s2">&#34;eventName&#34;</span><span class="p">]</span>      <span class="c1"># INSERT / MODIFY / REMOVE</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">if</span> <span class="n">event_name</span> <span class="o">==</span> <span class="s2">&#34;REMOVE&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="n">old</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;OldImage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="n">delete_from_search_index</span><span class="p">(</span><span class="n">old</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="n">new</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;NewImage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">upsert_to_search_index</span><span class="p">(</span><span class="n">new</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="c1"># 冪等：用 sequence number 或業務鍵去重</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">seq</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;SequenceNumber&#34;</span><span class="p">]</span></span></span></code></pre></div><h4 id="step-4設定-batch-與失敗處理">Step 4：設定 batch 與失敗處理</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">BatchSize: 依下游處理能力與延遲目標
</span></span><span class="line"><span class="ln">2</span><span class="cl">MaximumBatchingWindowInSeconds: 低流量湊批、控制延遲
</span></span><span class="line"><span class="ln">3</span><span class="cl">BisectBatchOnFunctionError: true   # 失敗時二分批、隔離壞 record
</span></span><span class="line"><span class="ln">4</span><span class="cl">MaximumRetryAttempts: 有限次       # 避免毒丸 record 無限重試
</span></span><span class="line"><span class="ln">5</span><span class="cl">DestinationConfig.OnFailure: DLQ   # 超過重試送 DLQ</span></span></code></pre></div><h4 id="step-5下游冪等設計">Step 5：下游冪等設計</h4>
<p>下游 upsert 用業務鍵（PK）做 idempotent write、刪除用「刪不存在不報錯」；確保同一 record 處理兩次結果相同。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 灌一筆寫入、確認下游在預期延遲內收到對應 record</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># CloudWatch: Lambda IteratorAge（消費落後程度）應接近 0</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 製造一筆會失敗的 record、確認進 DLQ 而非卡住整個 shard</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：關 stream 即停止產生新 record；已產生的 record 在 retention 內仍存在。下游邏輯出錯時、修好 Lambda 後可在 retention 內讓未處理 record 重新消費（或從 DLQ 重放）。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1下游非冪等重送導致重複副作用">Case 1：下游非冪等、重送導致重複副作用</h4>
<p>at-least-once 重送、下游每次都發一筆通知、用戶收到重複推播。修法：下游用業務鍵冪等、sequence number 去重；副作用（發通知 / 扣款）必須 idempotent。</p>
<h4 id="case-2依賴跨實體全域順序">Case 2：依賴跨實體全域順序</h4>
<p>下游假設「所有訂單事件按全域時間到達」、實際跨 shard 無此保證、算錯聚合。修法：只依賴「同 PK 內有序」；需要跨實體順序的、在下游用 event timestamp 重排、或重新設計不依賴全域順序。</p>
<h4 id="case-3毒丸-record-卡住整個-shard">Case 3：毒丸 record 卡住整個 shard</h4>
<p>某筆 record 讓 Lambda 永遠拋例外、預設行為是重試整個 batch、shard 卡死、IteratorAge 無限上升。修法：開 <code>BisectBatchOnFunctionError</code> + <code>MaximumRetryAttempts</code> + DLQ、隔離壞 record 讓其餘繼續。</p>
<h4 id="case-4consumer-落後record-過期遺失">Case 4：consumer 落後、record 過期遺失</h4>
<p>下游處理太慢、IteratorAge 超過 stream retention、未處理 record 被清掉。這個 Case 的代價跟前三個不同層級：前三個是「重複副作用 / 算錯聚合 / shard 卡住」、都還在 stream 裡留有 record、修好邏輯後可重新消費或從 DLQ 重放。Case 4 是 record 本身已被 retention 清除、那段時間的資料變更在 stream 這條通道上永久消失、沒有回退路徑。要補回只能反向比對 table 當前狀態跟下游狀態（若下游存得了），或在源頭重跑一次寫入觸發新 record — 兩者都是事故後的人工修復、成本遠高於前三個 Case 的設定旋鈕。</p>
<p>因為不可逆、防線要前置在「逼近 retention 之前」而非「過期之後」：IteratorAge alarm 的閾值設在遠低於 retention 的水位、留出擴容反應時間；吞吐不足時加 parallelization factor 或改 Kinesis（更長 retention、爭取更大的落後緩衝）；下游設計要能水平擴、讓落後可被快速追平。</p>
<h4 id="case-5parallelization-factor-開了還抱怨順序錯">Case 5：parallelization factor 開了還抱怨順序錯</h4>
<p>為提吞吐把 factor 開 &gt; 1、又依賴 shard 內嚴格順序、兩者矛盾。修法：需要嚴格順序維持 factor = 1；要並行吞吐就接受順序放寬、或把順序敏感的處理移到下游用 PK 分組。</p>
<p><strong>Anti-recommendation</strong>：只有單一同步下游、且寫路徑延遲容忍度高 → 直接在 application 寫入後同步處理可能更簡單、不必引入 stream 的運維與冪等複雜度。Streams 的價值在「多下游 / 解耦寫路徑 / 低延遲 CDC」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>IteratorAge</code>（Lambda）：消費落後程度、最關鍵指標、持續上升代表下游跟不上</li>
<li>Lambda <code>Errors</code> / <code>Throttles</code>：下游處理失敗 / 被限流</li>
<li>DLQ 訊息數：毒丸 record 累積、需要人工介入</li>
<li>stream <code>ReadProvisionedThroughputExceeded</code>（Kinesis 模式）：消費端讀超限</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>IteratorAge</code> 接近 retention 上限 → 資料變更即將遺失、緊急擴消費端</li>
<li>DLQ 持續累積 → 有系統性壞 record、查 Lambda 邏輯或上游資料</li>
<li>Errors 尖峰但 IteratorAge 正常 → transient 失敗、retry 有在吸收</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 stream metric 數字；上述指標與判讀屬 vendor 規格 + 通用事件處理觀測。</p></blockquote>
<p>接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="streams-跟-03-訊息佇列的責任切分">Streams 跟 03 訊息佇列的責任切分</h3>
<p>DynamoDB Streams 是 <em>資料庫變更</em> 的 CDC 通道、不是通用訊息佇列。兩者責任不同：</p>
<ul>
<li><strong>Streams</strong>：源頭是 table 寫入、record 由 DynamoDB 自動產生、生命週期綁 table、retention 短</li>
<li><strong>訊息佇列（SQS / SNS / Kafka）</strong>：源頭是 application 主動 publish、用於通用解耦、retention 與語意更彈性</li>
</ul>
<p>典型組合：Streams 捕捉 table 變更 → Lambda 處理 → 需要扇出到多個獨立服務時、再 publish 到 SNS / EventBridge。當事件來源不是「資料庫變更」而是「業務事件」、直接用 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 queue / topic、不要硬塞進 table 再用 stream。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/" data-link-title="DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency" data-link-desc="DynamoDB 的寫原子性不是免費 ACID；本文展開 TransactWriteItems 跨 item 原子性、ConditionExpression 條件寫、version-based optimistic locking、ClientRequestToken idempotency，以及 transaction 2x 成本邊界與何時用單 item conditional write 取代 transaction">transactions-conditional-writes</a> — transaction 寫入也觸發 stream、下游處理要冪等</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — single-table 下不同 entity 共用 stream、下游用 type 欄位分流</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> — Global Tables 跨 region 複製本身基於 stream 機制</li>
<li>替代路由：通用業務事件 / 多消費者扇出 / 長 retention → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a></li>
<li>搜尋索引同步下游 → OpenSearch / Elasticsearch（DynamoDB 不適合做全文檢索）</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 9.C26</a> 互引：訊息事件 message_id 天然冪等、適合 stream 下游處理</li>
</ul>
]]></content:encoded></item><item><title>Spanner ↔ BigQuery federation：OLTP/OLAP 分工、federated query、Data Boost、何時把分析 workload 分出去</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/bigquery-federation/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/bigquery-federation/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner&lt;/a> overview 的 implementation-layer deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 &lt;em>Spanner ↔ BigQuery federation&lt;/em> — OLTP 與 OLAP 的責任分工、以及讓分析查詢存取 OLTP 活資料的整合機制。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="核心定位oltp-與-olap-是兩種不同的資料責任">核心定位：OLTP 與 OLAP 是兩種不同的資料責任&lt;/h2>
&lt;p>Spanner ↔ BigQuery federation 的責任是讓「分析查詢」存取「交易資料」、同時把 OLTP 與 OLAP 兩種根本不同的工作負載分開、各自用適合的引擎與運算資源。Spanner 承擔交易責任 — 低延遲、高並發、行級讀寫、強一致;BigQuery 承擔分析責任 — 掃描大量資料、複雜聚合、欄式儲存、吞吐優先。federation 是讓這兩種責任協作的橋、不是讓一個引擎兼做兩件事。&lt;/p>
&lt;p>把這條分工放最前面、是因為最常見的反模式是「在 OLTP 庫上直接跑分析查詢」。一個掃描全表做月度營收聚合的查詢、跑在 Spanner 上會吃掉本該服務交易的 CPU、把 OLTP 的 p99 latency 拖垮。federation 的價值是讓分析查詢「邏輯上看得到 OLTP 資料、物理上不搶 OLTP 資源」。理解這點、才能正確判斷哪些查詢該留在 Spanner、哪些該推到 BigQuery。&lt;/p>
&lt;h2 id="問題情境分析查詢正在拖垮交易系統">問題情境：分析查詢正在拖垮交易系統&lt;/h2>
&lt;p>federation 的價值、在「分析需求與交易需求共用同一個 OLTP 庫、互相干擾」的壓力下浮現。讀者徵兆：BI 團隊的 dashboard 每小時跑全表聚合、每次跑都讓 Spanner CPU spike、交易 p99 跟著抖;資料團隊想做 ad-hoc 分析、卻被告知「不要在 production Spanner 上跑大查詢」;為了避免干擾、團隊每天 batch export 一次到 BigQuery、但分析師抱怨資料延遲一天、看不到當天的活資料。&lt;/p>
&lt;p>真實壓力場景：全球電商把訂單寫進 Spanner、營運團隊要即時看「過去一小時各區域的訂單趨勢」。這個查詢需要近即時的活資料（不能等隔日 batch）、又是掃描大量 row 的聚合（不該跑在 OLTP 上）。兩個需求拉扯：要新鮮就得查 Spanner 活資料、要不干擾交易就得分到分析引擎。federation + Data Boost 正是為了同時滿足這兩端 — 查 Spanner 的活資料、但用獨立運算資源。&lt;/p>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale&lt;/a> 提供「Spanner 定位在 OLTP、analytics workload 交給 BigQuery」的分工 anchor — overview 已指出 Spanner 的不適用場景包含「需要 OLAP 分析能力」、替代是跟 BigQuery 整合。&lt;strong>dogfood 邊界明示&lt;/strong>：9.C10 是 Google 內部 dogfood case、未展開 federation 實作細節;本文 federation 機制、Data Boost 行為均以 GCP vendor 規格 + 通用 OLTP/OLAP 工程展開、case 僅作分工壓力 anchor。&lt;/p>
&lt;h2 id="核心機制external-dataset-federated-query-與-data-boost">核心機制：external dataset federated query 與 Data Boost&lt;/h2>
&lt;p>federation 讓 BigQuery 把 Spanner database 註冊成 &lt;em>external dataset&lt;/em>、之後用標準 BigQuery SQL 直接查 Spanner 的表、查詢在執行時把資料從 Spanner 拉進 BigQuery 的執行引擎。資料不複製、查的是 Spanner 當前狀態 — 這是 federation 跟「定期 export 一份 copy 到 BigQuery」的根本差異:federated query 看到的是活資料、export 看到的是某個時間點的快照。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Cloud Spanner</a> overview 的 implementation-layer deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Overview 已說明 Spanner 在全球 OLTP 譜系的定位、本文聚焦 <em>Spanner ↔ BigQuery federation</em> — OLTP 與 OLAP 的責任分工、以及讓分析查詢存取 OLTP 活資料的整合機制。</p></blockquote>
<hr>
<h2 id="核心定位oltp-與-olap-是兩種不同的資料責任">核心定位：OLTP 與 OLAP 是兩種不同的資料責任</h2>
<p>Spanner ↔ BigQuery federation 的責任是讓「分析查詢」存取「交易資料」、同時把 OLTP 與 OLAP 兩種根本不同的工作負載分開、各自用適合的引擎與運算資源。Spanner 承擔交易責任 — 低延遲、高並發、行級讀寫、強一致;BigQuery 承擔分析責任 — 掃描大量資料、複雜聚合、欄式儲存、吞吐優先。federation 是讓這兩種責任協作的橋、不是讓一個引擎兼做兩件事。</p>
<p>把這條分工放最前面、是因為最常見的反模式是「在 OLTP 庫上直接跑分析查詢」。一個掃描全表做月度營收聚合的查詢、跑在 Spanner 上會吃掉本該服務交易的 CPU、把 OLTP 的 p99 latency 拖垮。federation 的價值是讓分析查詢「邏輯上看得到 OLTP 資料、物理上不搶 OLTP 資源」。理解這點、才能正確判斷哪些查詢該留在 Spanner、哪些該推到 BigQuery。</p>
<h2 id="問題情境分析查詢正在拖垮交易系統">問題情境：分析查詢正在拖垮交易系統</h2>
<p>federation 的價值、在「分析需求與交易需求共用同一個 OLTP 庫、互相干擾」的壓力下浮現。讀者徵兆：BI 團隊的 dashboard 每小時跑全表聚合、每次跑都讓 Spanner CPU spike、交易 p99 跟著抖;資料團隊想做 ad-hoc 分析、卻被告知「不要在 production Spanner 上跑大查詢」;為了避免干擾、團隊每天 batch export 一次到 BigQuery、但分析師抱怨資料延遲一天、看不到當天的活資料。</p>
<p>真實壓力場景：全球電商把訂單寫進 Spanner、營運團隊要即時看「過去一小時各區域的訂單趨勢」。這個查詢需要近即時的活資料（不能等隔日 batch）、又是掃描大量 row 的聚合（不該跑在 OLTP 上）。兩個需求拉扯：要新鮮就得查 Spanner 活資料、要不干擾交易就得分到分析引擎。federation + Data Boost 正是為了同時滿足這兩端 — 查 Spanner 的活資料、但用獨立運算資源。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Cloud Spanner planetary scale</a> 提供「Spanner 定位在 OLTP、analytics workload 交給 BigQuery」的分工 anchor — overview 已指出 Spanner 的不適用場景包含「需要 OLAP 分析能力」、替代是跟 BigQuery 整合。<strong>dogfood 邊界明示</strong>：9.C10 是 Google 內部 dogfood case、未展開 federation 實作細節;本文 federation 機制、Data Boost 行為均以 GCP vendor 規格 + 通用 OLTP/OLAP 工程展開、case 僅作分工壓力 anchor。</p>
<h2 id="核心機制external-dataset-federated-query-與-data-boost">核心機制：external dataset federated query 與 Data Boost</h2>
<p>federation 讓 BigQuery 把 Spanner database 註冊成 <em>external dataset</em>、之後用標準 BigQuery SQL 直接查 Spanner 的表、查詢在執行時把資料從 Spanner 拉進 BigQuery 的執行引擎。資料不複製、查的是 Spanner 當前狀態 — 這是 federation 跟「定期 export 一份 copy 到 BigQuery」的根本差異:federated query 看到的是活資料、export 看到的是某個時間點的快照。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- BigQuery 端：透過 external connection 查 Spanner 活資料
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">order_count</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">total</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">revenue</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">EXTERNAL_QUERY</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="s1">&#39;my-project.us-central1.spanner-conn&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="s1">&#39;SELECT region, total FROM orders WHERE created_at &gt; TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">region</span><span class="p">;</span></span></span></code></pre></div><h3 id="data-boost分析查詢的-workload-隔離">Data Boost：分析查詢的 workload 隔離</h3>
<p>federated query 直接查 Spanner、預設仍消耗 Spanner instance 的運算資源 — 大分析查詢還是會干擾 OLTP。Data Boost 解的就是這層:它讓分析查詢用 <em>獨立的、按需配置的運算資源</em> 讀 Spanner 資料、不消耗服務交易的 instance CPU。Data Boost 讀的是同一份 storage、但用獨立 compute、所以「分析查詢看活資料」與「不干擾 OLTP」可以同時成立。</p>
<p>這是 federation 整套機制的關鍵 — 沒有 Data Boost、federated query 只是把查詢入口換到 BigQuery、底層仍搶 Spanner CPU;有了 Data Boost、workload 隔離才真正成立。Data Boost 適合 batch / ad-hoc 的大型分析讀取、按使用量計費、不需要預先 provision。</p>
<blockquote>
<p><strong>Scope warning</strong>：external dataset / EXTERNAL_QUERY 的語法、Data Boost 的計費模型與資源隔離邊界屬 GCP 規格、逐版本演進。實作前 cross-verify <a href="https://cloud.google.com/bigquery/docs/spanner-federated-queries">BigQuery Spanner federation</a> 與 <a href="https://cloud.google.com/spanner/docs/databoost/databoost-overview">Data Boost 官方文件</a>、不可依本文當最終依據。</p></blockquote>
<h3 id="兩條整合路線federation-vs-change-stream-to-bigquery">兩條整合路線：federation vs change-stream-to-BigQuery</h3>
<p>把 Spanner 資料給 BigQuery 分析有兩條路線、取捨不同：</p>
<table>
  <thead>
      <tr>
          <th>路線</th>
          <th>資料新鮮度</th>
          <th>對 OLTP 影響</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Federated query + Data Boost</td>
          <td>查詢當下的活資料</td>
          <td>Data Boost 隔離、不搶 CPU</td>
          <td>ad-hoc 分析、即時 dashboard、低頻大查詢</td>
      </tr>
      <tr>
          <td>Change stream → BigQuery</td>
          <td>近即時持續同步</td>
          <td>change stream 讀取耗少量 CPU</td>
          <td>高頻分析、需要在 BigQuery 落地的歷史資料</td>
      </tr>
  </tbody>
</table>
<p>federation 是「需要時去查」、change stream 是「持續推一份到 BigQuery 落地」。federation 適合不需要把資料常駐 BigQuery、偶爾查活資料的場景;change stream（見 <a href="../change-streams-cdc/">change-streams-cdc</a>）適合要在 BigQuery 累積歷史、做高頻或需要 BigQuery 原生效能的分析。兩者不互斥 — 即時 ad-hoc 用 federation、長期歷史分析用 change stream 落地。</p>
<h2 id="操作流程建立-connectionfederated-query啟用-data-boost">操作流程：建立 connection、federated query、啟用 Data Boost</h2>
<h3 id="step-1建立-bigquery--spanner-external-connection">Step 1：建立 BigQuery → Spanner external connection</h3>
<p>在 BigQuery 建立指向 Spanner 的 external connection、設定 IAM 讓 BigQuery service account 有讀 Spanner 的權限。驗證：用 <code>EXTERNAL_QUERY</code> 跑一個簡單 <code>SELECT 1</code> 確認 connection 通、權限正確。</p>
<h3 id="step-2跑-federated-query-並確認查的是活資料">Step 2：跑 federated query 並確認查的是活資料</h3>
<p>跑一個帶時間條件的 federated query、在 Spanner 端寫一筆新資料、立即用 federated query 確認讀得到 — 驗證它查的是活資料、不是快照。這步確立 federation 的核心性質。</p>
<h3 id="step-3對大分析查詢啟用-data-boost-並驗證隔離">Step 3：對大分析查詢啟用 Data Boost 並驗證隔離</h3>
<p>對會掃描大量資料的分析查詢啟用 Data Boost、然後在跑分析查詢的同時觀測 Spanner OLTP 的 CPU 與 p99 latency。驗證點：開 Data Boost 後、大分析查詢執行期間 Spanner OLTP CPU 不應 spike、交易 p99 不應退化。這是 Data Boost 隔離是否生效的直接 evidence — 若 OLTP CPU 仍 spike、表示查詢沒走 Data Boost。</p>
<h3 id="step-4rollback-boundary">Step 4：rollback boundary</h3>
<p>federation 是讀取路徑、不改 Spanner 資料、rollback 成本低 — 停掉 federated query 即可、不影響 OLTP。決策的回退在「分析需求是否該用 federation」:若 federated query 即使開 Data Boost 仍無法滿足效能 / 成本、回退路徑是改用 change stream 把資料落地 BigQuery、用 BigQuery 原生效能查。</p>
<h2 id="失敗模式未隔離的查詢拖垮-oltp資料一致性誤解過度依賴-federation">失敗模式：未隔離的查詢拖垮 OLTP、資料一致性誤解、過度依賴 federation</h2>
<h3 id="federated-query-未開-data-boost拖垮-oltp">Federated query 未開 Data Boost、拖垮 OLTP</h3>
<p>團隊用 federated query 跑大分析查詢、但沒啟用 Data Boost、查詢直接吃 Spanner instance CPU、把交易 p99 拖垮。徵兆是「BI 查詢一跑、交易 latency 就抖」、Spanner CPU 在分析查詢期間 spike。修法是對所有大分析查詢啟用 Data Boost、把「federation = workload 隔離」這個假設明確驗證 — federation 本身不保證隔離、Data Boost 才保證。這個失敗的代價是它直接傷害 production 交易、不是只影響分析。</p>
<h3 id="把-federated-query-的快照當成跨系統強一致">把 federated query 的快照當成跨系統強一致</h3>
<p>federated query 讀的是 Spanner 的活資料、但這份分析結果是「查詢執行那一刻」的快照、不是跟某個 OLTP transaction 綁定的一致點。團隊若把 federated 分析結果當成「跟某筆交易嚴格對齊的數字」、會在對帳場景出錯 — 分析查詢跨多張表掃描時、不同表讀到的時間點可能略有差異、不像單一 OLTP transaction 有 external consistency 的全序保證。</p>
<p>這個失敗的代價在它的隱蔽性:多數分析場景對「秒級的時間點差異」不敏感、所以平時看不出問題;但在「分析數字被當成財務對帳依據」的場景、這個鬆散的一致性會讓對帳對不上、且很難 debug — 因為資料「看起來都對」、只是時間點不嚴格對齊。修法是分清分析查詢的一致性需求:近似趨勢分析、federation 的快照足夠;需要跟交易嚴格對齊的對帳、要用 Spanner 的 read-only transaction 配明確 timestamp bound、或在 OLTP 側生成對帳快照、不靠跨表 federated 掃描拼湊。回退路徑是把「需要強一致對帳」的查詢移回 Spanner read-only transaction、不要硬用 federation 省事。</p>
<h3 id="把所有分析都堆在-federation不評估落地-bigquery">把所有分析都堆在 federation、不評估落地 BigQuery</h3>
<p>團隊把所有分析都用 federated query 直查 Spanner、即使是高頻、重複、不需要活資料的查詢。federated query 每次都從 Spanner 拉資料、高頻重複查的成本與延遲都高於「資料已落地 BigQuery、用 BigQuery 原生欄式儲存查」。徵兆是同樣的分析查詢高頻跑、每次都付 federation 的拉取成本。<strong>Anti-recommendation（何時不該用 federation）</strong>:高頻、重複、可容忍近即時延遲的分析、用 change stream 把資料落地 BigQuery 更划算;federation 的適用範圍是低頻、ad-hoc、需要活資料的查詢。把高頻分析硬塞 federation 是用錯整合路線。</p>
<h2 id="容量與觀測oltp-cpu-隔離與-federation-拉取成本">容量與觀測：OLTP CPU 隔離與 federation 拉取成本</h2>
<p>federation 的容量壓力分兩端 — Spanner 側看「分析查詢有沒有被 Data Boost 隔離開」、BigQuery 側看「federated query 的拉取量與成本」。</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">Spanner OLTP CPU during analytics   → Data Boost 隔離是否生效的關鍵指標
</span></span><span class="line"><span class="ln">2</span><span class="cl">Spanner read capacity used by 分析   → 未隔離的 federated query 會吃這部分
</span></span><span class="line"><span class="ln">3</span><span class="cl">BigQuery federated query bytes 處理量 → federation 拉取成本的計費基礎
</span></span><span class="line"><span class="ln">4</span><span class="cl">分析查詢 latency vs OLTP p99 抖動相關性 → 隔離失效會讓兩者正相關</span></span></code></pre></div><p>核心容量判讀是「分析查詢執行期間、OLTP CPU 與 p99 是否穩定」 — 若穩定、Data Boost 隔離生效;若兩者正相關、隔離失效、分析查詢正在消耗本該服務 OLTP 的資源。用 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 把「分析查詢時段」跟「OLTP p99」配成 evidence pair。容量規劃上、若走 federation + Data Boost、OLTP sizing 不需為分析加碼（Data Boost 用獨立 compute）;若 federated query 未隔離、OLTP sizing 要把分析尖峰算進去、回 <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>。</p>
<blockquote>
<p><strong>Scope warning</strong>：Data Boost 的計費單位、federated query 的 bytes 計費、隔離的資源邊界屬 GCP 規格、隨版本演進、cross-verify 官方文件、非 9.C10 case 揭露的 production 數字。</p></blockquote>
<h2 id="邊界與整合何時把分析-workload-完全分出去">邊界與整合：何時把分析 workload 完全分出去</h2>
<h3 id="何時用-federation--data-boost">何時用 federation + Data Boost</h3>
<p>分析需要 Spanner 的活資料、查詢低頻或 ad-hoc、不想維護資料同步管線 — 這是 federation 的適用條件。Data Boost 讓它不干擾 OLTP、按需計費。即時營運 dashboard、臨時資料探索、不需要常駐 BigQuery 的分析都適合。</p>
<h3 id="何時把分析完全分到-bigquerychange-stream-落地">何時把分析完全分到 BigQuery（change stream 落地）</h3>
<p>分析是高頻、重複、需要 BigQuery 原生欄式效能、或需要在 BigQuery 累積跨年歷史 — 把資料用 change stream 持續同步到 BigQuery 落地、分析直接查 BigQuery、不再回 Spanner。判準是:當分析 workload 穩定且高頻、落地的一次性同步成本會被「不再每次 federated 拉取」攤平。這是「分析 workload 完全分出去」的訊號 — OLTP 與 OLAP 不只查詢入口分開、連儲存都分開。</p>
<h3 id="何時都不需要分析量小">何時都不需要（分析量小）</h3>
<p>若分析需求很小、Spanner 本身的 read capacity 有餘、偶爾在低峰跑個聚合不影響交易 — 不需要引入 federation 的額外設定。Anti-recommendation 的判準是:federation / Data Boost 的價值隨「分析與交易互相干擾的程度」上升;若兩者本來就不打架、保持簡單。</p>
<h3 id="sibling-deep-articles-路由">Sibling deep articles 路由</h3>
<ul>
<li><a href="../change-streams-cdc/">change-streams-cdc</a>：federation 的互補路線、高頻分析用 change stream 把資料落地 BigQuery、跟 federation 的「需要時去查」是兩種整合取捨</li>
<li><a href="../consistency-models-comparison/">consistency-models-comparison</a>：federated query 的快照一致性鬆於 OLTP transaction 的 external consistency、對帳場景的差異對應該文的一致性等級定義</li>
<li><a href="../truetime-api-depth/">truetime-api-depth</a>：需要嚴格時間點的分析要用 read-only transaction 配 timestamp bound、回該文的 staleness 選項</li>
</ul>
<h3 id="跟-knowledge-card-的互引">跟 knowledge card 的互引</h3>
<ul>
<li><a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> — 本文是這張卡在 Spanner ↔ BigQuery 的具體應用</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed-sql</a> — Spanner 作為 OLTP distributed SQL、跟 BigQuery 的 OLAP 分工</li>
</ul>
<h3 id="跟其他章節的對照路由">跟其他章節的對照路由</h3>
<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>：OLTP / OLAP 分工後各自的 sizing 不同、Data Boost 讓分析 sizing 跟 OLTP 解耦</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>：Spanner 定位在 OLTP、analytics 分到 BigQuery 是清楚的責任邊界</li>
</ul>
]]></content:encoded></item><item><title>9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/</guid><description>&lt;p>這個案例的核心責任是說明「從自管 MongoDB 遷到 Atlas managed」這條路徑的工程與成本對照。Forbes 自 2011 年起用 MongoDB 重寫 CMS、2020 年把 production 遷到 Atlas on Google Cloud、保留同一個 document model、轉移 DBA 責任跟跨雲彈性。跟 &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%">9.C20 Zomato&lt;/a> 的「跨 DB 種類遷移」對照 — Forbes 是 &lt;em>同 DB、換託管模式&lt;/em>、不需要重寫 schema 跟 access pattern。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Forbes 遷移到 MongoDB Atlas on Google Cloud 的關鍵數字（引自 &lt;a href="https://cloud.google.com/blog/products/databases/forbes-migrates-to-mongodb-atlas-on-google-cloud">Google Cloud Blog&lt;/a> 與 &lt;a href="https://www.mongodb.com/solutions/customer-case-studies/forbes">MongoDB customer case study&lt;/a>）：&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>單月不重複訪客&lt;/td>
 &lt;td>120M（2020 年 5 月）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build 時間&lt;/td>
 &lt;td>25 分鐘 → 9 分鐘（-64%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release 頻率提升&lt;/td>
 &lt;td>2x – 10x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>微服務數量&lt;/td>
 &lt;td>50+（GKE 上）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移耗時&lt;/td>
 &lt;td>6 個月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DB 總體擁有成本降幅&lt;/td>
 &lt;td>-25%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>電子報訂閱量&lt;/td>
 &lt;td>+92%（2020 全年）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atlas 可用 region&lt;/td>
 &lt;td>70+（跨 AWS / GCP / Azure）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CMS MongoDB 起用年&lt;/td>
 &lt;td>2011（首版 CMS 兩個月內交付）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：MongoDB Atlas（managed document DB）、Google Cloud Platform（基礎設施）、Google Kubernetes Engine（50+ 微服務編排）、Google App Engine（部分 serverless 應用）、自建中介 abstraction layer（API 隔離 schema 變動）。&lt;/p>
&lt;p>關鍵負載形狀：「文章 publish 後突然爆量」是新聞媒體常態 — 熱門報導、人物專訪、財經事件都會在分鐘內把單篇文章拉到百萬讀者。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&amp;#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL&lt;/a> 的「賽事時段預期峰值」不同、Forbes 的爆量是事件驅動、難以精確預測、需要 Atlas auto-scaling 撐住臨時讀爆。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Forbes 的遷移選擇揭露三個「自管 → managed」路徑的判讀重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>同 DB 換託管模式比換 DB 種類風險低、但 ROI 也較窄&lt;/strong>：Forbes 6 個月完成遷移、保留同 document model、schema 不動、application 改動只在 connection string 跟運維邊界。這跟 &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%">9.C20 Zomato 從 TiDB 遷到 DynamoDB&lt;/a> 對照、後者要重新設計 access pattern、ROI 大但風險高。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組&lt;/a> 的 schema migration playbook：「換 DB」跟「換託管」是兩個不同議題、不要混為一談。&lt;/li>
&lt;li>&lt;strong>跨雲彈性的價值在規避未來鎖定、不是當下省成本&lt;/strong>：Atlas 提供 AWS / GCP / Azure 跨雲部署。Forbes 選 GCP 是當下決策、但 Atlas 的跨雲能力讓未來雲商選型不再綁定特定 vendor。這跟 DynamoDB（AWS only）、Cosmos DB（Azure only）、Spanner（GCP only）的單雲鎖定形成對照。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的 vendor lock-in 評估。&lt;/li>
&lt;li>&lt;strong>Build 時間 25 → 9 分鐘 = 開發者效率改善、不是 DB 性能改善&lt;/strong>：Build 時間下降主因是 ephemeral test environment 用 Atlas API spin-up、不是 MongoDB query 變快。CMS 系統的 production read latency Atlas 跟自管 MongoDB 差距通常在 ±20% 內、真正贏的是「開發 / 部署 cycle 變短」。讀案例時要區分「開發者體驗 metric」跟「production 性能 metric」、兩者改善的杠桿完全不同。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「從自管 MongoDB 遷到 Atlas managed」這條路徑的工程與成本對照。Forbes 自 2011 年起用 MongoDB 重寫 CMS、2020 年把 production 遷到 Atlas on Google Cloud、保留同一個 document model、轉移 DBA 責任跟跨雲彈性。跟 <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> 的「跨 DB 種類遷移」對照 — Forbes 是 <em>同 DB、換託管模式</em>、不需要重寫 schema 跟 access pattern。</p>
<h2 id="觀察">觀察</h2>
<p>Forbes 遷移到 MongoDB Atlas on Google Cloud 的關鍵數字（引自 <a href="https://cloud.google.com/blog/products/databases/forbes-migrates-to-mongodb-atlas-on-google-cloud">Google Cloud Blog</a> 與 <a href="https://www.mongodb.com/solutions/customer-case-studies/forbes">MongoDB customer case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單月不重複訪客</td>
          <td>120M（2020 年 5 月）</td>
      </tr>
      <tr>
          <td>Build 時間</td>
          <td>25 分鐘 → 9 分鐘（-64%）</td>
      </tr>
      <tr>
          <td>Release 頻率提升</td>
          <td>2x – 10x</td>
      </tr>
      <tr>
          <td>微服務數量</td>
          <td>50+（GKE 上）</td>
      </tr>
      <tr>
          <td>遷移耗時</td>
          <td>6 個月</td>
      </tr>
      <tr>
          <td>DB 總體擁有成本降幅</td>
          <td>-25%</td>
      </tr>
      <tr>
          <td>電子報訂閱量</td>
          <td>+92%（2020 全年）</td>
      </tr>
      <tr>
          <td>Atlas 可用 region</td>
          <td>70+（跨 AWS / GCP / Azure）</td>
      </tr>
      <tr>
          <td>CMS MongoDB 起用年</td>
          <td>2011（首版 CMS 兩個月內交付）</td>
      </tr>
  </tbody>
</table>
<p>服務組合：MongoDB Atlas（managed document DB）、Google Cloud Platform（基礎設施）、Google Kubernetes Engine（50+ 微服務編排）、Google App Engine（部分 serverless 應用）、自建中介 abstraction layer（API 隔離 schema 變動）。</p>
<p>關鍵負載形狀：「文章 publish 後突然爆量」是新聞媒體常態 — 熱門報導、人物專訪、財經事件都會在分鐘內把單篇文章拉到百萬讀者。這跟 <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a> 的「賽事時段預期峰值」不同、Forbes 的爆量是事件驅動、難以精確預測、需要 Atlas auto-scaling 撐住臨時讀爆。</p>
<h2 id="判讀">判讀</h2>
<p>Forbes 的遷移選擇揭露三個「自管 → managed」路徑的判讀重點。</p>
<ol>
<li><strong>同 DB 換託管模式比換 DB 種類風險低、但 ROI 也較窄</strong>：Forbes 6 個月完成遷移、保留同 document model、schema 不動、application 改動只在 connection string 跟運維邊界。這跟 <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 從 TiDB 遷到 DynamoDB</a> 對照、後者要重新設計 access pattern、ROI 大但風險高。對應 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 的 schema migration playbook：「換 DB」跟「換託管」是兩個不同議題、不要混為一談。</li>
<li><strong>跨雲彈性的價值在規避未來鎖定、不是當下省成本</strong>：Atlas 提供 AWS / GCP / Azure 跨雲部署。Forbes 選 GCP 是當下決策、但 Atlas 的跨雲能力讓未來雲商選型不再綁定特定 vendor。這跟 DynamoDB（AWS only）、Cosmos DB（Azure only）、Spanner（GCP only）的單雲鎖定形成對照。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 vendor lock-in 評估。</li>
<li><strong>Build 時間 25 → 9 分鐘 = 開發者效率改善、不是 DB 性能改善</strong>：Build 時間下降主因是 ephemeral test environment 用 Atlas API spin-up、不是 MongoDB query 變快。CMS 系統的 production read latency Atlas 跟自管 MongoDB 差距通常在 ±20% 內、真正贏的是「開發 / 部署 cycle 變短」。讀案例時要區分「開發者體驗 metric」跟「production 性能 metric」、兩者改善的杠桿完全不同。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「25% TCO 降幅」是 <em>特定流量規模下</em> 的數字。Atlas managed 服務在小流量時 cost-per-GB 比自管低（不用養 DBA），但流量增長到一定規模後 self-hosted 反而便宜。Forbes 在 120M MAU 規模下選 managed 是合理判斷、但這個結論不是普適的。</li>
<li>「Build 25 → 9 分鐘」混合了「MongoDB Atlas API」、「GKE optimization」、「GCP CI/CD」三個變因。把全部歸功於 MongoDB Atlas 會誇大效益。</li>
<li>中介 abstraction layer 是 Forbes 主動加的設計、不是 Atlas 自帶。沒有這層 abstraction、schema 變動仍會直接打穿到所有 microservice、跨雲彈性也用不起來。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>自管 → managed 的遷移要先做 schema 跟 access pattern 盤點</strong>：確認沒有自管時的特殊 hack（自訂 plugin、特殊 storage engine、客製 oplog 處理）— 這些在 managed 服務上通常不支援。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a>。</li>
<li><strong>微服務 + abstraction layer 隔離 schema 變動</strong>：document database 的 schema flexibility 容易讓 production 出現 data inconsistency。中介 API 層把 schema 變動限制在 DB 邊界、microservice 看到的是穩定 API。對應 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor 的 schema governance 段</a>。</li>
<li><strong>跨雲 managed 服務比單雲服務更適合長期不確定的雲商策略</strong>：Atlas（跨 AWS / GCP / Azure）vs DynamoDB / Cosmos DB / Spanner（單雲）的取捨。當雲商選擇尚未底定、跨雲服務的選項保留價值高。對應 <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 vendor page</a> 跟 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor page</a> 對比。</li>
<li><strong>遷移時間表跟團隊規模耦合</strong>：Forbes 6 個月完成、團隊規模未揭露但顯然是中型團隊 + 多個 squad 並行。1-2 人團隊做同類遷移通常要 12+ 個月。對應 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">01.12 大規模 DB 遷移實戰</a> 的時間估計。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>自管 MongoDB → MongoDB Atlas（同 DB、換託管）：Forbes、SEGA HARDlight 路徑</li>
<li>自管 MongoDB → DocumentDB（AWS 自研、API 部分相容）：較多應用層改動、跨雲彈性失去</li>
<li>自管 MongoDB → Cosmos DB MongoDB API（Azure）：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 路徑、有 RU 模型差異</li>
<li>自管 PostgreSQL → Aurora / Cloud SQL：對等遷移、但 RDB 跟 document DB 的 schema 治理議題不同</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 MongoDB 遷移到 Atlas → <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a> + <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a></li>
<li>想評估跨雲 vs 單雲 DB 取捨 → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <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 vendor page</a> 對比段</li>
<li>想做 microservice + abstraction layer 設計 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
<li>想對照同類遷移 → <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（遷到 Cosmos DB MongoDB API）/ <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>（換 DB 種類）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/databases/forbes-migrates-to-mongodb-atlas-on-google-cloud">Forbes migrates from self-managed MongoDB to MongoDB Atlas on Google Cloud</a></li>
<li><a href="https://www.mongodb.com/solutions/customer-case-studies/forbes">New CMS and developer environment cuts build times to just nine minutes for Forbes</a></li>
<li><a href="https://www.mongodb.com/resources/solutions/use-cases/forbes-cloud-migration-helps-worlds-biggest-media-brand-continue-standard-digital-innovation">Forbes：MongoDB Cloud Migration Helps World&rsquo;s Biggest Media Brand</a></li>
</ul>
]]></content:encoded></item><item><title>3.C37 MachineMetrics：邊緣到雲端工廠資料管線</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/</guid><description>&lt;p>這個案例的核心責任是說明工業 IoT 完整的 edge-to-cloud NATS 整合（Leaf Node + JetStream + KV + Object Store + Auth）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>跨「數百個客戶廠區、數千台機台」的 Industrial IoT、單機產出最高 1000 Hz 採樣、工廠網路斷斷續續、Kinesis 等 cloud-only 工具無法跑在資源受限 edge 上。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 Leaf Node 做 hub-and-spoke 把邊緣設備串到雲端、Edge 端用 JetStream 做本地持久化（取代 SQLite）抵抗網路斷線、用 KV store 做 config / 短期 cache、Object Store 派發 WASM 模組、Decentralized Auth 隔離客戶。揭露「broker 的功能集合」決定它能不能取代多套 edge 工具。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：Cluster + Supercluster + Leaf node / JetStream KV + Object Store / Subject-based ACL + 多租戶。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/" data-link-title="3.C36 Intelecy：工業 IoT 即時感測 &amp;#43; 多租戶" data-link-desc="Intelecy 工廠 gateway 接數萬感測器、&amp;lt; 2 秒往返延遲做即時 ML、從 BoltDB 本地快取演進到 JetStream 持久化。">3.C36 Intelecy&lt;/a>（同類對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.synadia.com/customer-stories/machinemetrics">MachineMetrics Customer Story (Synadia)&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明工業 IoT 完整的 edge-to-cloud NATS 整合（Leaf Node + JetStream + KV + Object Store + Auth）。</p>
<h2 id="觀察">觀察</h2>
<p>跨「數百個客戶廠區、數千台機台」的 Industrial IoT、單機產出最高 1000 Hz 採樣、工廠網路斷斷續續、Kinesis 等 cloud-only 工具無法跑在資源受限 edge 上。</p>
<h2 id="判讀">判讀</h2>
<p>用 Leaf Node 做 hub-and-spoke 把邊緣設備串到雲端、Edge 端用 JetStream 做本地持久化（取代 SQLite）抵抗網路斷線、用 KV store 做 config / 短期 cache、Object Store 派發 WASM 模組、Decentralized Auth 隔離客戶。揭露「broker 的功能集合」決定它能不能取代多套 edge 工具。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：Cluster + Supercluster + Leaf node / JetStream KV + Object Store / Subject-based ACL + 多租戶。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/" data-link-title="3.C36 Intelecy：工業 IoT 即時感測 &#43; 多租戶" data-link-desc="Intelecy 工廠 gateway 接數萬感測器、&lt; 2 秒往返延遲做即時 ML、從 BoltDB 本地快取演進到 JetStream 持久化。">3.C36 Intelecy</a>（同類對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.synadia.com/customer-stories/machinemetrics">MachineMetrics Customer Story (Synadia)</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB TTL 資料生命週期：自動過期、48 小時刪除延遲、過期仍可讀與 storage 成本</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/ttl-data-lifecycle/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/ttl-data-lifecycle/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>訊息系統的 storage bill 每月穩定上漲、查 table 發現裡面堆了三年份的過期通知、沒人清。team 設了 TTL「自動清理」、結果兩個新問題冒出來：第一、設了 TTL 之後 storage 還是沒馬上降、過了好幾小時才開始掉；第二、有個報表 query 把「已過期但還沒被刪」的 item 也撈進來、算錯數字。兩個痛點揭露 DynamoDB TTL 的真實語意 — 它是 &lt;em>最終會刪除&lt;/em> 的背景機制、不是即時刪除、也不是查詢層的過濾器。本文展開 TTL 的 epoch 語意、刪除延遲特性、過期可讀陷阱與 storage 成本判讀。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>生命週期前提：先確認 workload 適配 DynamoDB&lt;/strong>：資料生命週期管理是 &lt;em>已選 DynamoDB&lt;/em> 之後才浮現的議題 — TTL 解的是「資料存進來之後怎麼自動退場」、而非「資料該不該存進 DynamoDB」。後者由 4 軸前置判讀決定：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定、判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。本文承接該前提、聚焦用 TTL 管理資料生命週期與 storage 成本。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制ttl-attribute-與背景刪除">核心機制：TTL attribute 與背景刪除&lt;/h2>
&lt;p>DynamoDB TTL 讓 item 在指定時間後自動被刪除、不消耗寫容量。機制很簡單但語意有三個容易踩的邊界。&lt;/p>
&lt;p>&lt;strong>設定方式&lt;/strong>：在 item 上放一個數值 attribute、值是 &lt;em>Unix epoch 秒數&lt;/em>（不是毫秒、不是 ISO 字串）、並在 table 啟用 TTL 指向該 attribute：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">time&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">table&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">put_item&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Item&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;PK&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;MSG#&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">msg_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;SK&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;META&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;body&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;expireAt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">30&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">86400&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># 30 天後過期、epoch 秒&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>三個關鍵語意&lt;/strong>：&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>刪除非即時&lt;/td>
 &lt;td>過期後由 AWS 背景程序刪除、通常 48 小時內、不保證準時&lt;/td>
 &lt;td>不能用 TTL 做即時失效邏輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>過期仍可讀&lt;/td>
 &lt;td>過期但尚未被刪的 item 仍出現在 GetItem / Query / Scan 結果&lt;/td>
 &lt;td>read 路徑要 application 端 filter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪除免 WCU&lt;/td>
 &lt;td>TTL 刪除不消耗 write capacity&lt;/td>
 &lt;td>大量過期清理不增寫成本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第二列是報表算錯的根因：&lt;strong>TTL 不是查詢過濾器&lt;/strong>。過期到實際刪除之間有一段窗口、這期間 item 還在、還會被讀到。需要「過期立刻不可見」的、application 必須在讀取後自己比對 &lt;code>expireAt&lt;/code> 過濾。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「TTL 通常 48 小時內刪除」屬 AWS vendor 規格描述、AWS 不保證準時、實際延遲視 table 大小與背景負載而定、實作時 cross-verify 官方 doc。&lt;code>9.C26 PayPay&lt;/code> case 揭露「TTL 機制可自動清理過期訊息」的 &lt;em>用途&lt;/em>、未揭露刪除延遲的具體數字。&lt;/p>&lt;/blockquote>
&lt;p>對應 knowledge card：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/soft-ttl/" data-link-title="Soft TTL" data-link-desc="說明資料進入刷新期後仍可短暫使用以降低 stampede">soft-ttl&lt;/a>。&lt;/p>
&lt;h2 id="刪除延遲與過期可讀兩個必須處理的窗口">刪除延遲與過期可讀：兩個必須處理的窗口&lt;/h2>
&lt;p>TTL 的「最終刪除」特性製造兩個 application 必須意識的窗口。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>訊息系統的 storage bill 每月穩定上漲、查 table 發現裡面堆了三年份的過期通知、沒人清。team 設了 TTL「自動清理」、結果兩個新問題冒出來：第一、設了 TTL 之後 storage 還是沒馬上降、過了好幾小時才開始掉；第二、有個報表 query 把「已過期但還沒被刪」的 item 也撈進來、算錯數字。兩個痛點揭露 DynamoDB TTL 的真實語意 — 它是 <em>最終會刪除</em> 的背景機制、不是即時刪除、也不是查詢層的過濾器。本文展開 TTL 的 epoch 語意、刪除延遲特性、過期可讀陷阱與 storage 成本判讀。</p>
<blockquote>
<p><strong>生命週期前提：先確認 workload 適配 DynamoDB</strong>：資料生命週期管理是 <em>已選 DynamoDB</em> 之後才浮現的議題 — TTL 解的是「資料存進來之後怎麼自動退場」、而非「資料該不該存進 DynamoDB」。後者由 4 軸前置判讀決定：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定、判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。本文承接該前提、聚焦用 TTL 管理資料生命週期與 storage 成本。</p></blockquote>
<h2 id="核心機制ttl-attribute-與背景刪除">核心機制：TTL attribute 與背景刪除</h2>
<p>DynamoDB TTL 讓 item 在指定時間後自動被刪除、不消耗寫容量。機制很簡單但語意有三個容易踩的邊界。</p>
<p><strong>設定方式</strong>：在 item 上放一個數值 attribute、值是 <em>Unix epoch 秒數</em>（不是毫秒、不是 ISO 字串）、並在 table 啟用 TTL 指向該 attribute：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span><span class="n">Item</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;MSG#</span><span class="si">{</span><span class="n">msg_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s2">&#34;body&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s2">&#34;expireAt&#34;</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">())</span> <span class="o">+</span> <span class="mi">30</span> <span class="o">*</span> <span class="mi">86400</span><span class="p">,</span>  <span class="c1"># 30 天後過期、epoch 秒</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><strong>三個關鍵語意</strong>：</p>
<table>
  <thead>
      <tr>
          <th>語意</th>
          <th>內容</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>刪除非即時</td>
          <td>過期後由 AWS 背景程序刪除、通常 48 小時內、不保證準時</td>
          <td>不能用 TTL 做即時失效邏輯</td>
      </tr>
      <tr>
          <td>過期仍可讀</td>
          <td>過期但尚未被刪的 item 仍出現在 GetItem / Query / Scan 結果</td>
          <td>read 路徑要 application 端 filter</td>
      </tr>
      <tr>
          <td>刪除免 WCU</td>
          <td>TTL 刪除不消耗 write capacity</td>
          <td>大量過期清理不增寫成本</td>
      </tr>
  </tbody>
</table>
<p>第二列是報表算錯的根因：<strong>TTL 不是查詢過濾器</strong>。過期到實際刪除之間有一段窗口、這期間 item 還在、還會被讀到。需要「過期立刻不可見」的、application 必須在讀取後自己比對 <code>expireAt</code> 過濾。</p>
<blockquote>
<p><strong>Scope warning</strong>：「TTL 通常 48 小時內刪除」屬 AWS vendor 規格描述、AWS 不保證準時、實際延遲視 table 大小與背景負載而定、實作時 cross-verify 官方 doc。<code>9.C26 PayPay</code> case 揭露「TTL 機制可自動清理過期訊息」的 <em>用途</em>、未揭露刪除延遲的具體數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a>、<a href="/blog/backend/knowledge-cards/soft-ttl/" data-link-title="Soft TTL" data-link-desc="說明資料進入刷新期後仍可短暫使用以降低 stampede">soft-ttl</a>。</p>
<h2 id="刪除延遲與過期可讀兩個必須處理的窗口">刪除延遲與過期可讀：兩個必須處理的窗口</h2>
<p>TTL 的「最終刪除」特性製造兩個 application 必須意識的窗口。</p>
<p><strong>窗口一：過期 → 實際刪除（可讀窗口）</strong>：</p>
<p>item 的 <code>expireAt</code> 已過、但背景程序還沒刪。這段時間 item：</p>
<ul>
<li>仍會被 <code>Query</code> / <code>Scan</code> / <code>GetItem</code> 撈到</li>
<li>仍佔 storage、仍計 storage 費</li>
<li>仍會被 secondary index 索引到</li>
</ul>
<p>application 若依賴「過期就消失」、會在這個窗口讀到 stale 資料。正確做法是 read 後 filter：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">now</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">items</span> <span class="o">=</span> <span class="p">[</span><span class="n">it</span> <span class="k">for</span> <span class="n">it</span> <span class="ow">in</span> <span class="n">response</span><span class="p">[</span><span class="s2">&#34;Items&#34;</span><span class="p">]</span> <span class="k">if</span> <span class="n">it</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;expireAt&#34;</span><span class="p">,</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">62</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">now</span><span class="p">]</span></span></span></code></pre></div><p>或在 query 加 <code>FilterExpression</code> 排除過期 item（注意 filter 在讀取後套用、仍消耗讀容量）。</p>
<p><strong>窗口二：TTL 刪除 → stream record</strong>：</p>
<p>TTL 刪除會在 stream 產生一筆 <code>REMOVE</code> record、且 <code>userIdentity</code> 標記為 DynamoDB 服務本身（principal <code>dynamodb.amazonaws.com</code>）。這讓「過期歸檔」成為可能 — 下游 Lambda 收到 TTL 刪除事件、把 item 寫進冷儲存（S3）再讓它從 hot table 消失：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">event</span><span class="p">[</span><span class="s2">&#34;Records&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">if</span> <span class="n">record</span><span class="p">[</span><span class="s2">&#34;eventName&#34;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&#34;REMOVE&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">            <span class="n">principal</span> <span class="o">=</span> <span class="n">record</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;userIdentity&#34;</span><span class="p">,</span> <span class="p">{})</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;principalId&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">            <span class="k">if</span> <span class="n">principal</span> <span class="o">==</span> <span class="s2">&#34;dynamodb.amazonaws.com&#34;</span><span class="p">:</span>  <span class="c1"># TTL 刪除、非 application 刪除</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">                <span class="n">archive_to_s3</span><span class="p">(</span><span class="n">record</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;OldImage&#34;</span><span class="p">])</span></span></span></code></pre></div><p>區分「TTL 自動刪除」vs「application 主動刪除」靠 <code>userIdentity</code> — 兩者都是 <code>REMOVE</code> record、但只有 TTL 刪除帶服務 principal。對應 <a href="/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/" data-link-title="DynamoDB Streams 與 Lambda 事件驅動：CDC、shard 順序保證、消費模式與失敗處理" data-link-desc="DynamoDB Streams 不是免費的可靠事件流；本文展開 stream record 的四種 view type、shard 對應 partition 的順序保證邊界、Lambda event source mapping vs Kinesis 消費模式、at-least-once 下游冪等需求，以及 batch 失敗時的 bisect / DLQ 處理">streams-lambda-event-driven</a>。</p>
<blockquote>
<p><strong>Scope warning</strong>：stream record 的 <code>userIdentity</code> 標記屬 vendor 規格、欄位細節 cross-verify 官方 doc；本段機制描述非 production case 揭露。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從生命週期需求到上線的 6 步流程。</p>
<h4 id="step-1判斷資料是否適合-ttl-管理">Step 1：判斷資料是否適合 TTL 管理</h4>
<p>適合 TTL 的資料有「自然過期時間」：session、訊息通知、暫存 token、event log、合規保留期到期的資料。不適合的：需要精確即時刪除的、需要刪除前審批的、永久保存的。</p>
<h4 id="step-2設計-expireat-計算">Step 2：設計 expireAt 計算</h4>
<p>寫入時算好 epoch 秒數的 <code>expireAt</code>；不同資料類型可不同保留期（通知 30 天、session 1 天、audit 依合規要求）。</p>
<h4 id="step-3啟用-table-ttl">Step 3：啟用 table TTL</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">aws dynamodb update-time-to-live <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table-name messages <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --time-to-live-specification <span class="s2">&#34;Enabled=true, AttributeName=expireAt&#34;</span></span></span></code></pre></div><h4 id="step-4read-路徑加過期過濾">Step 4：read 路徑加過期過濾</h4>
<p>所有面向用戶的讀取、在 application 端比對 <code>expireAt</code>（或加 FilterExpression）；不要假設過期 item 已消失。</p>
<h4 id="step-5可選接-ttl-刪除歸檔">Step 5：（可選）接 TTL 刪除歸檔</h4>
<p>需要保留過期資料的、接 stream Lambda、用 <code>userIdentity</code> 辨識 TTL 刪除、歸檔到 S3。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 寫一筆短 TTL item、等過期後確認：</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 1. 過期但未刪窗口內仍可讀到（驗證需要 filter）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 2. 數小時後背景刪除生效、storage 下降</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 3. 若接歸檔、確認 S3 收到對應 OldImage</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：關閉 TTL 即停止自動刪除、已刪除的 item 不可恢復（除非有歸檔）；啟用 TTL 前先確認 <code>expireAt</code> 計算正確、避免誤設過短把活躍資料刪掉。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1expireat-用毫秒或-iso-字串">Case 1：expireAt 用毫秒或 ISO 字串</h4>
<p>TTL 只認 Unix epoch 秒；填毫秒（多三位數）會讓過期時間落在遙遠未來、item 永不過期；填字串 TTL 直接不生效。修法：統一用 <code>int(time.time()) + seconds</code>、寫測試驗證 attribute 是秒級數值。</p>
<h4 id="case-2以為-ttl-是即時刪除做即時失效邏輯">Case 2：以為 TTL 是即時刪除、做即時失效邏輯</h4>
<p>用 TTL 當「到點立刻不可用」的開關（如優惠券到期）、實際過期後幾小時還能用。修法：即時失效靠 application 邏輯比對時間、TTL 只負責 <em>清理 storage</em>、兩者分開。</p>
<h4 id="case-3報表--對帳撈到過期未刪-item">Case 3：報表 / 對帳撈到過期未刪 item</h4>
<p>聚合 query 沒過濾過期 item、把可讀窗口內的殘留資料算進去。修法：所有讀取路徑一致地過濾 <code>expireAt</code>；對帳查詢明確排除過期。</p>
<h4 id="case-4誤設過短保留期刪掉活躍資料">Case 4：誤設過短保留期刪掉活躍資料</h4>
<p>這個 case 跟前三個的失敗代價層級不同。前面的踩雷多半可回復 — storage 緩漲可回填、過期未刪可在讀取路徑加 filter、index 殘留會隨背景刪除自然消退。誤設過短保留期則是 <em>不可逆</em> 的：<code>expireAt</code> 計算 bug（少乘 86400、用錯時區基準）把保留期算成幾小時、背景程序把仍在使用的活躍資料當成過期 item 刪除、而 TTL 刪除不寫 undo log、刪掉就沒有從 DynamoDB 端救回的途徑、只能靠外部備份（PITR / 另存的 stream archive）回灌、且回灌期間資料缺口已經對線上服務造成影響。</p>
<p>代價的關鍵在於計算錯誤的爆炸半徑：一個錯誤常數會同時套用到所有新寫入 item、刪除是持續發生的背景行為、發現時往往已刪掉大批資料。修法的重心因此放在 <em>上線前驗證</em> 而非事後補救：上線前在 staging 用短週期資料驗證 <code>expireAt</code> 算出的絕對時間點符合預期、TTL 啟用初期把 <code>TimeToLiveDeletedItemCount</code> 跟預估刪除量對照、刪除量明顯偏高就立即停用 TTL 並排查計算、不等 storage 趨勢確認。對保留期敏感的 table 先開 PITR 當不可逆操作的最後防線。</p>
<h4 id="case-5過期-item-仍被-gsi-索引推高-index-成本">Case 5：過期 item 仍被 GSI 索引、推高 index 成本</h4>
<p>過期未刪 item 仍佔 GSI storage；大量過期堆積時 GSI 成本沒因「邏輯過期」下降。修法：理解 GSI 跟著 base item 生命週期、storage 降要等實際刪除；對成本敏感的 sparse index 設計可讓過期 item 不進 GSI（對應 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design sparse index</a>）。</p>
<p><strong>Anti-recommendation</strong>：資料量小、storage 成本可忽略、或刪除需要審批/合規記錄 → 不必用 TTL；手動或排程刪除更可控。TTL 的價值在「大量有自然過期時間的資料、要低成本自動清理」（如 PayPay 式每日上億訊息）。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>TimeToLiveDeletedItemCount</code>：TTL 背景刪除的 item 數、確認 TTL 真的在運作</li>
<li>table <code>ItemCount</code> / storage size：長期趨勢、確認過期清理讓 storage 趨於穩態</li>
<li>過期未刪比例：自行用 <code>expireAt &lt; now</code> 的 item 數估算可讀窗口殘留量</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>TimeToLiveDeletedItemCount</code> 為零但有設過期資料 → TTL 沒生效（attribute 名稱錯 / 值格式錯）</li>
<li>storage 持續上漲且 TTL 刪除量遠小於寫入量 → 保留期設太長、或寫入遠超過期速度、要重估保留策略</li>
<li>大量過期未刪堆積 → 背景刪除跟不上寫入、storage 成本被殘留拉高</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：<code>9.C26 PayPay</code> 的「3 億/天 × 30 天 = 90 億筆」是 PayPay case 文章（9.C26）的策略段推算、非 PayPay 官方揭露的精確 item 數；引用時當量級壓力 anchor、不當精確數字。</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/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="ttl-vs-cache-ttl-vs-合規保留">TTL vs cache TTL vs 合規保留</h3>
<p>「TTL」這個詞在不同層意義不同、不要混用：</p>
<ul>
<li><strong>DynamoDB TTL</strong>：主資料的生命週期管理、最終刪除、本篇主寫</li>
<li><strong>cache TTL</strong>（如 DAX item / query cache、Redis TTL）：快取副本的新鮮度邊界、過期是「重新回源」不是「刪除主資料」、主寫於 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 與 <a href="/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/" data-link-title="DynamoDB DAX 快取策略：cluster 架構、item/query cache、write-through 與 invalidation 邊界" data-link-desc="DAX 不是「加上去就變快」的開關；本文展開 DAX cluster 架構、item cache vs query cache 兩種快取、write-through 一致性語意、query cache 只靠 TTL 失效的陷阱，以及 strongly consistent read 繞過 cache 的邊界，含 Lemino 讀峰值補位 case fact 與 gsi-lsi-design 的 SSoT 切分">dax-caching-strategy</a></li>
<li><strong>合規保留期</strong>：法規要求的最短/最長保存、可用 TTL 實作到期清理、但刪除前的稽核記錄要另外保留（對應 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit trail</a>）</li>
</ul>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/" data-link-title="DynamoDB Streams 與 Lambda 事件驅動：CDC、shard 順序保證、消費模式與失敗處理" data-link-desc="DynamoDB Streams 不是免費的可靠事件流；本文展開 stream record 的四種 view type、shard 對應 partition 的順序保證邊界、Lambda event source mapping vs Kinesis 消費模式、at-least-once 下游冪等需求，以及 batch 失敗時的 bisect / DLQ 處理">streams-lambda-event-driven</a> — TTL 刪除觸發 stream REMOVE record、用 userIdentity 辨識、可做過期歸檔</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — single-table 下不同 entity 用不同 expireAt 保留期</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — 過期未刪 item 仍佔 GSI、sparse index 可讓過期不進 GSI</li>
<li><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">on-demand-vs-provisioned</a> — TTL 刪除免 WCU、不影響寫容量規劃、但 storage 成本要靠 TTL 控制</li>
<li>替代路由：快取副本新鮮度 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a>；合規稽核 → <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit trail</a></li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 9.C26</a> 互引：每日上億訊息用 TTL 自動清理避免 storage 爆炸的 case anchor</li>
</ul>
]]></content:encoded></item><item><title>9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/</guid><description>&lt;p>這個案例的核心責任是說明「IoT / telematics 高頻 sensor 寫入」如何套在 document model 上、以及 MongoDB Atlas 在 mission-critical（生命安全）服務中的角色。Toyota Connected 把車輛 sensor、緊急通報（SOS / 撞擊偵測）、駕駛資料都寫進 20 個 MongoDB Atlas database、用 event-driven microservice 處理。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB&lt;/a> 對照 — Amazon Ads 用 KV 撐極高吞吐、Toyota 用 document model 撐「形狀變化頻繁的 sensor signal」、兩條路徑反映不同的工作負載決策。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Toyota Connected 平台關鍵數字（引自 &lt;a href="https://aws.amazon.com/solutions/case-studies/toyota-connected/">AWS case study&lt;/a> 與 &lt;a href="https://www.mongodb.com/solutions/customer-case-studies/toyota-connected">MongoDB customer case study&lt;/a>）：&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>服務涵蓋車輛數&lt;/td>
 &lt;td>9M+（Toyota / Lexus 北美 Safety Connect）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>每月平台 transaction&lt;/td>
 &lt;td>18 Billion&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>流量擴展能力&lt;/td>
 &lt;td>18x usual 流量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>緊急訊號處理延遲&lt;/td>
 &lt;td>3 秒內到 safety agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性目標&lt;/td>
 &lt;td>99.99%（target、實測 99% 月達成）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MongoDB Atlas DB 數&lt;/td>
 &lt;td>20&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS 用量成長&lt;/td>
 &lt;td>3x（自 2018 啟動以來）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自管成本降幅&lt;/td>
 &lt;td>70-80%（serverless 架構整體）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>車載 sensor 種類&lt;/td>
 &lt;td>數百個（occupant、seatbelt、fuel、air quality）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：MongoDB Atlas（document store，20 databases）、AWS Lambda（serverless 處理事件）、Amazon Kinesis Data Streams（即時資料攝取）、CloudAMQP（非同步訊息）、Redis（hot cache）、Kubernetes（microservice 編排）。&lt;/p>
&lt;p>關鍵負載形狀：「車輛 sensor 持續低頻 + 緊急事件高優先低延遲」雙模式並存。&lt;/p>
&lt;ul>
&lt;li>&lt;em>持續模式&lt;/em>：900 萬車輛、每車數百 sensor、定期上報遙測資料。這是「sustained-growth + 高 throughput」的形狀、document model 比 wide-column 更適合 — 因為不同車型 / 不同年份的 sensor schema 不一樣、document 自然演進、不需要每加 sensor 就 ALTER TABLE。&lt;/li>
&lt;li>&lt;em>緊急模式&lt;/em>：SOS 按鈕、自動撞擊通報、車輛安全異常。這是 &lt;em>life-critical low-latency&lt;/em> — 3 秒內 sensor 訊號要從車輛到 agent 螢幕、含網路傳輸、event routing、microservice 處理、agent UI rendering。這個 budget 倒推回 MongoDB 寫入要求是 sub-100ms。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Toyota Connected 的 MongoDB 選擇揭露三個 IoT / telematics 工程決策的判讀重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>document model 適合「sensor schema 隨產品演進」的場景&lt;/strong>：車載 sensor 種類隨車型、年份、地區規範變化。RDB 走「每加 sensor 加 column」會讓 schema migration 變成發行週期的卡點；document model 走「polymorphic document」、新 sensor 只是新欄位、舊文件不需要 backfill。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page&lt;/a> 的 document shape 教學段。但這個彈性的成本是：production 必須做 schema governance（validation、版本欄位、application 層相容處理），否則「schema 自由」會變「production data inconsistency」。&lt;/li>
&lt;li>&lt;strong>20 個 Atlas database 不是技術上限、是業務邊界切分&lt;/strong>：18 Billion transactions / 月 ÷ 30 天 ÷ 86400 秒 ≈ 7K transactions / sec。這個數字單一 MongoDB cluster 可以撐、不需要 20 個 DB。Toyota 切 20 個 DB 是按 &lt;em>microservice ownership&lt;/em> 跟 &lt;em>blast radius&lt;/em> — 每個 microservice 擁有自己的 DB、單一 DB 故障不會影響其他服務。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程&lt;/a>、把「總吞吐」拆成「per-DB 邊界」。&lt;/li>
&lt;li>&lt;strong>99.99% target vs 99% 實測差距揭露 telematics 的可用性挑戰&lt;/strong>：99.99% 是 4 分鐘 / 月停機、99% 是 7.2 小時 / 月停機。差兩個 9 不是 MongoDB 自身可用性問題、是 &lt;em>end-to-end&lt;/em> 鏈路問題 — 車輛無線網路、cellular tower、AWS network、event bus、microservice、Atlas cluster 任一環節掉都會打掉可用性。MongoDB Atlas 自身的 SLA 通常是 99.95%、達到 99.99% 必須 multi-region + 跨雲冗餘。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &amp;#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%&lt;/a> 的多 region active-active 設計。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「IoT / telematics 高頻 sensor 寫入」如何套在 document model 上、以及 MongoDB Atlas 在 mission-critical（生命安全）服務中的角色。Toyota Connected 把車輛 sensor、緊急通報（SOS / 撞擊偵測）、駕駛資料都寫進 20 個 MongoDB Atlas database、用 event-driven microservice 處理。跟 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a> 對照 — Amazon Ads 用 KV 撐極高吞吐、Toyota 用 document model 撐「形狀變化頻繁的 sensor signal」、兩條路徑反映不同的工作負載決策。</p>
<h2 id="觀察">觀察</h2>
<p>Toyota Connected 平台關鍵數字（引自 <a href="https://aws.amazon.com/solutions/case-studies/toyota-connected/">AWS case study</a> 與 <a href="https://www.mongodb.com/solutions/customer-case-studies/toyota-connected">MongoDB customer case study</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務涵蓋車輛數</td>
          <td>9M+（Toyota / Lexus 北美 Safety Connect）</td>
      </tr>
      <tr>
          <td>每月平台 transaction</td>
          <td>18 Billion</td>
      </tr>
      <tr>
          <td>流量擴展能力</td>
          <td>18x usual 流量</td>
      </tr>
      <tr>
          <td>緊急訊號處理延遲</td>
          <td>3 秒內到 safety agent</td>
      </tr>
      <tr>
          <td>可用性目標</td>
          <td>99.99%（target、實測 99% 月達成）</td>
      </tr>
      <tr>
          <td>MongoDB Atlas DB 數</td>
          <td>20</td>
      </tr>
      <tr>
          <td>AWS 用量成長</td>
          <td>3x（自 2018 啟動以來）</td>
      </tr>
      <tr>
          <td>自管成本降幅</td>
          <td>70-80%（serverless 架構整體）</td>
      </tr>
      <tr>
          <td>車載 sensor 種類</td>
          <td>數百個（occupant、seatbelt、fuel、air quality）</td>
      </tr>
  </tbody>
</table>
<p>服務組合：MongoDB Atlas（document store，20 databases）、AWS Lambda（serverless 處理事件）、Amazon Kinesis Data Streams（即時資料攝取）、CloudAMQP（非同步訊息）、Redis（hot cache）、Kubernetes（microservice 編排）。</p>
<p>關鍵負載形狀：「車輛 sensor 持續低頻 + 緊急事件高優先低延遲」雙模式並存。</p>
<ul>
<li><em>持續模式</em>：900 萬車輛、每車數百 sensor、定期上報遙測資料。這是「sustained-growth + 高 throughput」的形狀、document model 比 wide-column 更適合 — 因為不同車型 / 不同年份的 sensor schema 不一樣、document 自然演進、不需要每加 sensor 就 ALTER TABLE。</li>
<li><em>緊急模式</em>：SOS 按鈕、自動撞擊通報、車輛安全異常。這是 <em>life-critical low-latency</em> — 3 秒內 sensor 訊號要從車輛到 agent 螢幕、含網路傳輸、event routing、microservice 處理、agent UI rendering。這個 budget 倒推回 MongoDB 寫入要求是 sub-100ms。</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>Toyota Connected 的 MongoDB 選擇揭露三個 IoT / telematics 工程決策的判讀重點。</p>
<ol>
<li><strong>document model 適合「sensor schema 隨產品演進」的場景</strong>：車載 sensor 種類隨車型、年份、地區規範變化。RDB 走「每加 sensor 加 column」會讓 schema migration 變成發行週期的卡點；document model 走「polymorphic document」、新 sensor 只是新欄位、舊文件不需要 backfill。對應 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a> 的 document shape 教學段。但這個彈性的成本是：production 必須做 schema governance（validation、版本欄位、application 層相容處理），否則「schema 自由」會變「production data inconsistency」。</li>
<li><strong>20 個 Atlas database 不是技術上限、是業務邊界切分</strong>：18 Billion transactions / 月 ÷ 30 天 ÷ 86400 秒 ≈ 7K transactions / sec。這個數字單一 MongoDB cluster 可以撐、不需要 20 個 DB。Toyota 切 20 個 DB 是按 <em>microservice ownership</em> 跟 <em>blast radius</em> — 每個 microservice 擁有自己的 DB、單一 DB 故障不會影響其他服務。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a>、把「總吞吐」拆成「per-DB 邊界」。</li>
<li><strong>99.99% target vs 99% 實測差距揭露 telematics 的可用性挑戰</strong>：99.99% 是 4 分鐘 / 月停機、99% 是 7.2 小時 / 月停機。差兩個 9 不是 MongoDB 自身可用性問題、是 <em>end-to-end</em> 鏈路問題 — 車輛無線網路、cellular tower、AWS network、event bus、microservice、Atlas cluster 任一環節掉都會打掉可用性。MongoDB Atlas 自身的 SLA 通常是 99.95%、達到 99.99% 必須 multi-region + 跨雲冗餘。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a> 的多 region active-active 設計。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「18 Billion transactions / 月」是 <em>平台所有服務</em> 加總、不是 MongoDB 單一 cluster 數字。MongoDB 只承擔其中需要 document storage 的部分、其他走 Lambda 直接處理或寫到 Kinesis。</li>
<li>「3 秒延遲到 agent」包含車載、無線、雲端、UI、agent 操作多個環節。MongoDB 在這個延遲鏈裡通常分到 100-500ms 預算、不是整個 3 秒。</li>
<li>MongoDB 6.0+ 有 time series collection 對 IoT 寫入有專屬優化。Toyota 揭露的 20 個 DB 沒明確說有沒有用 time series collection — 對 IoT 案例這是重要區分、但 case study 沒揭露。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>IoT 高頻 sensor 寫入考慮 MongoDB time series collection（6.0+）</strong>：比 regular collection 寫入吞吐高 3-5x、storage 壓縮率更好。專為 timestamp + metadata + measurement 三段式資料優化。對應 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a> 的容量規劃要點段。</li>
<li><strong>mission-critical IoT 系統要做 multi-region 跟多供應商備援</strong>：99.99% 不能只靠 MongoDB Atlas 本身、要靠 region 冗餘 + 多條 cellular network + 多個 event bus 路徑。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> 的 multi-region active-active。</li>
<li><strong>按 microservice ownership 切 MongoDB cluster、不要單一巨型 cluster</strong>：blast radius 邊界 = 業務邊界、不是「能不能撐」的問題。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a>。</li>
<li><strong>event-driven 處理 IoT 資料、不用 request-response</strong>：sensor 寫到 Kinesis / Kafka / event bus、microservice 從 stream 消費、寫進 MongoDB。這條 path 避免「sensor 寫不進去 DB 就 retry storm」的問題。對應 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a>。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>AWS：MongoDB Atlas + Kinesis + Lambda（Toyota 配置）</li>
<li>GCP：MongoDB Atlas on GCP + Pub/Sub + Cloud Functions、或 Firestore + Pub/Sub（document API native）</li>
<li>Azure：Cosmos DB MongoDB API + Event Hubs + Azure Functions</li>
<li>跨雲：MongoDB Atlas 是 IoT 平台保留跨雲彈性的少數選項</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 IoT / telematics 資料層 → <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor page</a> + <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">01.10 KV / Document DB 容量規劃</a></li>
<li>想做 multi-region 高可用性 → <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a></li>
<li>想對照不同 IoT 資料層選擇 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a>（KV）/ <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a>（高頻訊息）</li>
<li>想理解 event-driven IoT 架構 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a></li>
<li>想做 IoT 寫入吞吐的 shard key 選型 → <a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">MongoDB shard key 選型</a></li>
<li>想規劃 telemetry schema design → <a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">MongoDB schema design pattern</a></li>
<li>想處理 IoT 高 client 數的 connection storm → <a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">MongoDB connection 管理與 cache 層</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.mongodb.com/solutions/customer-case-studies/toyota-connected">Toyota Connected Aims For At Least 99.99% Availability With MongoDB Assistance</a></li>
<li><a href="https://aws.amazon.com/solutions/case-studies/toyota-connected/">Toyota Connected Reimagines Mobility on AWS</a></li>
<li><a href="https://digitalcxo.com/article/mongodb-aws-help-toyota-connected-move-past-legacy-database-challenges/">MongoDB, AWS Help Toyota Connected Move Past Legacy Database Challenges</a></li>
<li><a href="https://www.just-auto.com/news/toyota-connected-hails-efficiencies-from-migration-of-data-services-to-mongodb-atlas/">Toyota Connected hails efficiencies from migration of data services to MongoDB Atlas</a></li>
<li><a href="https://www.mongodb.com/company/blog/innovation/data-modeling-strategies-connected-vehicle-signal-data-in-mongodb">Data Modeling Strategies For Connected Vehicle Signal Data In MongoDB</a></li>
</ul>
]]></content:encoded></item><item><title>3.C38 Clarifai：NATS Streaming ML 平台非同步任務</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/</guid><description>&lt;p>這個案例的核心責任是說明 NATS Streaming（JetStream 前身）的 queue group + at-least-once 在 ML worker pool 的角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Clarifai 做 custom model 訓練、任務從幾秒到幾分鐘、原本同步呼叫遇到 rolling deployment 會掉訊息。三週內把一個服務遷到 NATS、5 個月內擴展到 5 個服務、每日 100k+ 訊息、100% uptime。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 NATS Streaming 的 at-least-once delivery + queue subscription group 做 worker pool、每個微服務連到三個獨立 NATS Streaming 實例做 fanout 隔離。揭露 ML 任務的長尾處理時間特別需要 at-least-once + redelivery、不能容忍 rolling deploy 掉訊息。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：JetStream consumer 設計（NATS Streaming 是前身）/ Queue groups。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://nats.io/blog/how-clarifai-uses-nats-and-kubernetes-for-machine-learning/">How Clarifai Uses NATS and Kubernetes for Machine Learning&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 NATS Streaming（JetStream 前身）的 queue group + at-least-once 在 ML worker pool 的角色。</p>
<h2 id="觀察">觀察</h2>
<p>Clarifai 做 custom model 訓練、任務從幾秒到幾分鐘、原本同步呼叫遇到 rolling deployment 會掉訊息。三週內把一個服務遷到 NATS、5 個月內擴展到 5 個服務、每日 100k+ 訊息、100% uptime。</p>
<h2 id="判讀">判讀</h2>
<p>用 NATS Streaming 的 at-least-once delivery + queue subscription group 做 worker pool、每個微服務連到三個獨立 NATS Streaming 實例做 fanout 隔離。揭露 ML 任務的長尾處理時間特別需要 at-least-once + redelivery、不能容忍 rolling deploy 掉訊息。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：JetStream consumer 設計（NATS Streaming 是前身）/ Queue groups。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://nats.io/blog/how-clarifai-uses-nats-and-kubernetes-for-machine-learning/">How Clarifai Uses NATS and Kubernetes for Machine Learning</a></li>
</ul>
]]></content:encoded></item><item><title>從 RDS / MongoDB 遷移到 DynamoDB：access-pattern-first 重建模、混合架構與 cost crossover</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/migrate-rds-mongodb-to-dynamodb/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/migrate-rds-mongodb-to-dynamodb/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 migration playbook。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>「我們要把 RDS 整個搬到 DynamoDB。」這句話本身就藏著最大的誤解 — DynamoDB 遷移不是把 table schema 1:1 搬過去。RDS 的 normalized schema、JOIN、ad-hoc query 在 DynamoDB 沒有對應物；MongoDB 的彈性 document、二級索引、aggregation pipeline 也不能直接映射。字面意義的「遷移」不成立 — 遷移的動作是 &lt;em>從 access pattern 重新設計資料模型&lt;/em>、搬資料只是最後一步。能不能遷、該遷多少，取決於 workload 的查詢形狀是否固定、一致性需求是否能放寬。本文走 paradigm shift 結構：先講為何字面遷移不成立、再講哪些該遷哪些該留、最後才是階段化執行。&lt;/p>
&lt;h2 id="6-維-diff-audit主導維度是-paradigm">6 維 diff audit：主導維度是 paradigm&lt;/h2>
&lt;p>遷移前先盤點 source 跟 target 的差異落在哪幾維、決定 playbook 結構：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>RDS / MongoDB → DynamoDB&lt;/th>
 &lt;th>程度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>SQL / document query → KV &lt;code>GetItem&lt;/code> / &lt;code>Query&lt;/code>、無 JOIN&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>self-managed / RDS-managed → fully managed serverless&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>relational / document model → access-pattern-first KV&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components 數量&lt;/td>
 &lt;td>單 DB → 單 DB（不拆分）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>ORM / query layer 全改、access pattern 先行&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>partition key 設計、無跨 region transaction&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主導維度是 &lt;strong>paradigm&lt;/strong>（其次 schema / application change）。這定義了結構 — &lt;strong>Type E paradigm shift&lt;/strong>（排除 schema 翻譯 Type A 和 drop-in Type B）：部分遷移、長期混合架構、不收斂到「全部搬完」。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>No-go condition&lt;/strong>：workload 需要 ad-hoc 分析查詢、跨實體 JOIN、頻繁 schema 變動下的彈性查詢、或複雜多表交易 → 不該遷 DynamoDB。這些是 relational / document 的主場、硬遷會把複雜度推給 application 層（自己做 JOIN、自己維護冗餘）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼字面遷移不成立paradigm-gap">為什麼字面遷移不成立：paradigm gap&lt;/h2>
&lt;p>RDS / MongoDB 是 &lt;em>先有資料模型、再支援任意查詢&lt;/em>；DynamoDB 是 &lt;em>先有查詢、才設計資料模型&lt;/em>。這個順序顛倒是遷移的核心難點。&lt;/p>
&lt;p>&lt;strong>relational → DynamoDB 的斷層&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>JOIN 消失：relational 用 JOIN 組合多表、DynamoDB 要嘛預先反正規化（把關聯資料寫在同一 item / 同一 partition）、要嘛 application 多次查詢自己組&lt;/li>
&lt;li>ad-hoc query 消失：RDS 可以對任意欄位下 &lt;code>WHERE&lt;/code>、DynamoDB 只能用 PK/SK 或預建 GSI 查（對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design&lt;/a>）&lt;/li>
&lt;li>強一致交易縮窄：relational 任意多表交易 → DynamoDB 有限的 TransactWriteItems（對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/" data-link-title="DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency" data-link-desc="DynamoDB 的寫原子性不是免費 ACID；本文展開 TransactWriteItems 跨 item 原子性、ConditionExpression 條件寫、version-based optimistic locking、ClientRequestToken idempotency，以及 transaction 2x 成本邊界與何時用單 item conditional write 取代 transaction">transactions-conditional-writes&lt;/a>）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>document（MongoDB）→ DynamoDB 的斷層&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 migration playbook。寫作參照 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。</p></blockquote>
<p>「我們要把 RDS 整個搬到 DynamoDB。」這句話本身就藏著最大的誤解 — DynamoDB 遷移不是把 table schema 1:1 搬過去。RDS 的 normalized schema、JOIN、ad-hoc query 在 DynamoDB 沒有對應物；MongoDB 的彈性 document、二級索引、aggregation pipeline 也不能直接映射。字面意義的「遷移」不成立 — 遷移的動作是 <em>從 access pattern 重新設計資料模型</em>、搬資料只是最後一步。能不能遷、該遷多少，取決於 workload 的查詢形狀是否固定、一致性需求是否能放寬。本文走 paradigm shift 結構：先講為何字面遷移不成立、再講哪些該遷哪些該留、最後才是階段化執行。</p>
<h2 id="6-維-diff-audit主導維度是-paradigm">6 維 diff audit：主導維度是 paradigm</h2>
<p>遷移前先盤點 source 跟 target 的差異落在哪幾維、決定 playbook 結構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RDS / MongoDB → DynamoDB</th>
          <th>程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>SQL / document query → KV <code>GetItem</code> / <code>Query</code>、無 JOIN</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>self-managed / RDS-managed → fully managed serverless</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>relational / document model → access-pattern-first KV</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Components 數量</td>
          <td>單 DB → 單 DB（不拆分）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>ORM / query layer 全改、access pattern 先行</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>partition key 設計、無跨 region transaction</td>
          <td>Medium</td>
      </tr>
  </tbody>
</table>
<p>主導維度是 <strong>paradigm</strong>（其次 schema / application change）。這定義了結構 — <strong>Type E paradigm shift</strong>（排除 schema 翻譯 Type A 和 drop-in Type B）：部分遷移、長期混合架構、不收斂到「全部搬完」。</p>
<blockquote>
<p><strong>No-go condition</strong>：workload 需要 ad-hoc 分析查詢、跨實體 JOIN、頻繁 schema 變動下的彈性查詢、或複雜多表交易 → 不該遷 DynamoDB。這些是 relational / document 的主場、硬遷會把複雜度推給 application 層（自己做 JOIN、自己維護冗餘）。</p></blockquote>
<h2 id="為什麼字面遷移不成立paradigm-gap">為什麼字面遷移不成立：paradigm gap</h2>
<p>RDS / MongoDB 是 <em>先有資料模型、再支援任意查詢</em>；DynamoDB 是 <em>先有查詢、才設計資料模型</em>。這個順序顛倒是遷移的核心難點。</p>
<p><strong>relational → DynamoDB 的斷層</strong>：</p>
<ul>
<li>JOIN 消失：relational 用 JOIN 組合多表、DynamoDB 要嘛預先反正規化（把關聯資料寫在同一 item / 同一 partition）、要嘛 application 多次查詢自己組</li>
<li>ad-hoc query 消失：RDS 可以對任意欄位下 <code>WHERE</code>、DynamoDB 只能用 PK/SK 或預建 GSI 查（對應 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a>）</li>
<li>強一致交易縮窄：relational 任意多表交易 → DynamoDB 有限的 TransactWriteItems（對應 <a href="/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/" data-link-title="DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency" data-link-desc="DynamoDB 的寫原子性不是免費 ACID；本文展開 TransactWriteItems 跨 item 原子性、ConditionExpression 條件寫、version-based optimistic locking、ClientRequestToken idempotency，以及 transaction 2x 成本邊界與何時用單 item conditional write 取代 transaction">transactions-conditional-writes</a>）</li>
</ul>
<p><strong>document（MongoDB）→ DynamoDB 的斷層</strong>：</p>
<ul>
<li>看似接近（都是 NoSQL / document-ish）、實際 MongoDB 的二級索引彈性、aggregation pipeline、彈性 query 在 DynamoDB 都沒有對應</li>
<li>MongoDB 可以「先存進去、之後再想怎麼查」；DynamoDB 不行、access pattern 沒想清楚就建表、後面要重做</li>
</ul>
<p>所以遷移的第一步不是匯資料、是 <strong>窮舉 access pattern</strong>：列出 application 對這份資料的所有讀寫路徑、每條路徑對應 DynamoDB 的 PK/SK/GSI 設計。access pattern 列不完整、就還不能開始遷。</p>
<h2 id="哪些-workload-該遷哪些該留混合架構">哪些 workload 該遷、哪些該留（混合架構）</h2>
<p>Type E 的本質是 <em>不收斂</em> — 不是所有資料都該進 DynamoDB、混合架構會長期存在。判讀標準：</p>
<table>
  <thead>
      <tr>
          <th>Workload 特徵</th>
          <th>去向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>access pattern 固定、key-based 查詢、高吞吐</td>
          <td>遷 DynamoDB</td>
      </tr>
      <tr>
          <td>可接受 eventually consistent</td>
          <td>遷 DynamoDB</td>
      </tr>
      <tr>
          <td>需要 ad-hoc 分析 / 報表 / JOIN</td>
          <td>留 RDS / 或進 analytics 系統</td>
      </tr>
      <tr>
          <td>需要強一致複雜交易</td>
          <td>留 RDS</td>
      </tr>
      <tr>
          <td>schema 頻繁演進、查詢需求不穩</td>
          <td>留 MongoDB / RDS</td>
      </tr>
  </tbody>
</table>
<p><code>9.C20 Zomato</code> 是這個判讀的 case anchor：Zomato 遷的是 <em>billing platform</em>（帳單事件、access pattern 固定、可接受 eventually consistent）、不是把整家公司的資料庫都搬。帳單系統從 TiDB 遷到 DynamoDB 後吞吐 2,000 → 8,000 RPM（4x）、延遲降 90%、成本降 50%；動機是 TiDB 必須為突發流量峰值預先 over-provision、DynamoDB on-demand「pay only for what we use」避免常態浪費。</p>
<blockquote>
<p><strong>Scope warning</strong>：Zomato 的「成本降 50%」是 <em>當下流量</em> 下的對照、不是永久結論；「延遲降 90%」可能主要是 p50、p99/p999 改善幅度通常較小。這兩點 case 原文已標明、引用時不可升級成「DynamoDB 永遠更便宜更快」。crossover 判讀見下方容量段。</p></blockquote>
<h2 id="phase-planaccess-pattern-first-階段化">Phase plan：access-pattern-first 階段化</h2>
<p>paradigm shift 的階段化把不可逆動作放到最後、每階段有獨立驗證門檻：</p>
<h4 id="phase-1access-pattern-窮舉">Phase 1：access pattern 窮舉</h4>
<p>列出 application 對目標資料的所有讀寫路徑、標每條的頻率、一致性需求、是否可放寬。這份清單是後續所有設計的輸入、不完整不進下一階段。</p>
<h4 id="phase-2dynamodb-資料建模">Phase 2：DynamoDB 資料建模</h4>
<p>依 access pattern 設計 PK/SK、single-table 結構、需要的 GSI、capacity mode。對應 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a>、<a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a>。</p>
<h4 id="phase-3dual-write">Phase 3：dual-write</h4>
<p>application 同時寫舊（RDS / MongoDB）跟新（DynamoDB）。舊系統仍是 source of truth、DynamoDB 累積資料。dual-write 要處理寫入失敗一致性（其中一邊失敗如何補償）。</p>
<h4 id="phase-4backfill-歷史資料">Phase 4：backfill 歷史資料</h4>
<p>把舊系統既有資料按新模型轉換寫入 DynamoDB。backfill 跟 dual-write 並行時要處理覆蓋順序（backfill 不能覆蓋掉 dual-write 的新值）。</p>
<h4 id="phase-5shadow-read-驗證">Phase 5：shadow read 驗證</h4>
<p>讀路徑同時打舊跟新、比對結果、記錄差異但仍以舊系統回應用戶。shadow read 是 cutover 前的信心來源 — 差異率降到可接受才進 cutover。對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a> 的 evidence 方法。</p>
<h4 id="phase-6漸進-cutover">Phase 6：漸進 cutover</h4>
<p>讀流量逐步從舊切到新（按比例 / 按 user segment）、保留隨時切回的能力。cutover 完成後 DynamoDB 成為該 workload 的 source of truth；但其他未遷 workload 仍在 RDS / MongoDB — 混合架構成立。</p>
<h2 id="evidence每階段的前進依據">Evidence：每階段的前進依據</h2>
<p>每個階段用資料證明可前進、不靠感覺：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dual-write</td>
          <td>雙寫成功率、寫入失敗補償紀錄、兩邊 row count 差異</td>
      </tr>
      <tr>
          <td>backfill</td>
          <td>已 backfill 比例、轉換錯誤數、checksum 對照</td>
      </tr>
      <tr>
          <td>shadow read</td>
          <td>新舊結果差異率、差異分類（可接受的 eventual vs 真錯誤）</td>
      </tr>
      <tr>
          <td>cutover</td>
          <td>切流比例、新系統 latency p99、error rate、rollback 是否觸發</td>
      </tr>
  </tbody>
</table>
<p>這些 evidence 對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>（Source / Time range / Query link / Owner / Data quality）與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的 gate 決策。</p>
<h2 id="cutover-與-rollback-決策">Cutover 與 rollback 決策</h2>
<p>資料庫切流失敗代價高、決策權責要寫清楚：</p>
<ul>
<li><strong>cutover window</strong>：選低流量時段、明確切流比例階梯（如 1% → 10% → 50% → 100%）</li>
<li><strong>rollback condition</strong>：新系統 error rate / latency 超過閾值、或 shadow read 差異率異常 → 切回舊系統</li>
<li><strong>decision owner</strong>：誰有權喊停、依據什麼 evidence、記錄在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a>（Timestamp / Decision / Context / Evidence / Owner / Rollback condition）</li>
<li><strong>資料凍結策略</strong>：cutover 期間若需要凍結寫入、明確凍結範圍與時長</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、<a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<h2 id="cleanup-與長期混合">Cleanup 與長期混合</h2>
<p>Type E 的 cleanup 不一定是「退役舊系統」— 多數情況舊系統仍服務未遷 workload：</p>
<ul>
<li>已遷 workload 的舊 schema / 舊 writer / dual-write code path 退役</li>
<li>shadow read 比對 code 移除</li>
<li>但 RDS / MongoDB 本身保留（服務 analytics / 強一致 / 彈性查詢 workload）</li>
<li>明確標示哪條資料路徑的 source of truth 是 DynamoDB、哪條仍是 RDS / MongoDB、避免「到底哪個是真的」混亂</li>
</ul>
<p>混合架構不是過渡失敗、是 paradigm shift 的穩態 — 每個 workload 待在最適合它的儲存層。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1先匯資料才想-access-pattern">Case 1：先匯資料才想 access pattern</h4>
<p>把 RDS table 結構直接搬成 DynamoDB item、上線後發現查不出要的資料、要重建表。修法：access pattern 窮舉是 Phase 1、資料建模是 Phase 2；順序不能顛倒。</p>
<h4 id="case-2把-join-邏輯推給-application-卻沒評估成本">Case 2：把 JOIN 邏輯推給 application 卻沒評估成本</h4>
<p>遷了關聯資料、application 每次查詢做 N 次 DynamoDB 呼叫自己組 JOIN、latency 跟成本爆炸。修法：關聯資料在建模階段反正規化（同 partition / 同 item）；無法反正規化的關聯查詢、該 workload 可能不適合遷。</p>
<h4 id="case-3dual-write-一邊失敗沒補償">Case 3：dual-write 一邊失敗沒補償</h4>
<p>dual-write 時 DynamoDB 寫成功 RDS 失敗（或反之）、兩邊資料分歧、cutover 後發現新系統資料不完整。修法：dual-write 要有失敗補償（記錄失敗、重試、或標記該筆需人工對帳）；對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>。</p>
<h4 id="case-4跳過-shadow-read-直接-cutover">Case 4：跳過 shadow read 直接 cutover</h4>
<p>對自己的建模有信心、省掉 shadow read、cutover 後才發現 access pattern 漏了某個查詢路徑、生產出錯。修法：shadow read 是 cutover 前唯一能在真實流量下驗證新模型的階段、不能省。</p>
<h4 id="case-5只看當下成本忽略-crossover">Case 5：只看當下成本忽略 crossover</h4>
<p>遷移時算出成本降 50% 就下決策、未來流量成長後 DynamoDB cost-per-request 累積超過自管 cluster、反而更貴。修法：算 12-24 個月在預期流量下的成本曲線、不是當下 snapshot（見容量段）。</p>
<p><strong>Anti-recommendation</strong>：workload 查詢需求還在快速變化、或團隊對 access-pattern-first 建模沒經驗 → 先不要遷；用一個低風險、access pattern 已穩定的 workload 試點（如 Zomato 的 billing platform）、累積經驗再擴大。</p>
<h2 id="容量與成本crossover-判讀">容量與成本：crossover 判讀</h2>
<p>DynamoDB 成本判讀的關鍵是 <em>未來流量曲線</em>、不是遷移當下的 snapshot：</p>
<ul>
<li><strong>遷移當下</strong>：相對 over-provisioned 的自管 cluster、DynamoDB on-demand 常更便宜（Zomato -50%）</li>
<li><strong>流量成長後</strong>：DynamoDB cost-per-request 隨用量線性成長、自管 cluster 在高且可預測流量下有 crossover 點、可能反超便宜</li>
<li><strong>判讀分層</strong>：小/中流量或流量不可預測 → DynamoDB 划算；大且可預測流量 + 已有 DBA 團隊 → 算自管 crossover</li>
</ul>
<p>這條 vendor-level 成本軸主寫於 <a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/#%e8%bb%b8-6dynamodb-vs-%e8%87%aa%e7%ae%a1-cluster-cost-crossover" 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">on-demand-vs-provisioned 軸 6</a>；本篇從遷移決策角度引用、不重複展開 6 軸。</p>
<blockquote>
<p><strong>Scope warning</strong>：crossover 點隨 region pricing、workload shape、團隊成本結構變動、無通用閾值；Zomato 的具體百分比是單一 case 當下對照、不可外推。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="跟其他遷移路徑的關係">跟其他遷移路徑的關係</h3>
<ul>
<li><strong>DynamoDB → SQL / search / analytics split</strong>（遷出方向）：當 DynamoDB workload 長出 ad-hoc 查詢需求、把分析部分拆到 OpenSearch / 數倉、是反向路徑、屬另一篇 playbook scope</li>
<li><strong>MongoDB → Atlas</strong>：若只是要 managed MongoDB 而非換 paradigm、走 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a>、不必遷 DynamoDB（保留 document paradigm）</li>
<li><strong>跨平台等效</strong>：RDS → Aurora（保留 relational）、MongoDB → Cosmos DB（保留 document）、都比遷 DynamoDB 的 paradigm 跨度小；先確認真的需要換 paradigm</li>
</ul>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — 遷移 Phase 2 資料建模的核心</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — 建模時 PK 均勻度判讀</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/" data-link-title="DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency" data-link-desc="DynamoDB 的寫原子性不是免費 ACID；本文展開 TransactWriteItems 跨 item 原子性、ConditionExpression 條件寫、version-based optimistic locking、ClientRequestToken idempotency，以及 transaction 2x 成本邊界與何時用單 item conditional write 取代 transaction">transactions-conditional-writes</a> — 遷移後寫一致性如何在 DynamoDB 重建</li>
<li><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">on-demand-vs-provisioned</a> — cost crossover 軸 6 SSoT</li>
<li><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> — 通用 dual-write / shadow read / cutover 框架</li>
<li>跟 <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 9.C20</a> 互引：billing platform 遷移的可量化對照與 cost crossover 警示</li>
</ul>
]]></content:encoded></item><item><title>9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/</guid><description>&lt;p>這個案例的核心責任是說明「single-primary OLTP 撞到寫入天花板」如何用 distributed SQL 拆解。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings&lt;/a> 對比 — DraftKings 在 Aurora 上靠「業務切 200 個獨立 cluster」橫向擴展、DoorDash 是「保留 PostgreSQL wire 介面、但底層換成多主寫入的 CockroachDB」。兩條路徑都在解「Aurora 單主寫入容量上限」、走法不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>DoorDash 從 Aurora Postgres 遷到 CockroachDB 的關鍵敘述（引自 &lt;a href="https://www.cockroachlabs.com/blog/aurora-postgres-to-cockroachdb/">Why DoorDash migrated from Aurora Postgres to CockroachDB&lt;/a> / &lt;a href="https://thenewstack.io/how-doordash-migrated-from-aurora-postgres-to-cockroachdb/">The New Stack 報導&lt;/a>）：&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>2020-04-17 高峰 QPS&lt;/td>
 &lt;td>&amp;gt; 1.636 million QPS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件結果&lt;/td>
 &lt;td>multi-hour outage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件背景&lt;/td>
 &lt;td>疫情封鎖、外送需求暴增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移啟動&lt;/td>
 &lt;td>事件後幾週、先把 table 從主 cluster 拆出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第一階段移轉量&lt;/td>
 &lt;td>一個月內把 dozens of tables 拆到獨立 Aurora cluster&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第二階段&lt;/td>
 &lt;td>自動化工具把 Aurora Postgres → CockroachDB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>後續結果&lt;/td>
 &lt;td>跑更多 cluster、incident alert volume 反而下降&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：Aurora Postgres（遷移前主要 OLTP）、CockroachDB self-hosted、自製 table extraction tool、自製 lossless migration pipeline。&lt;/p>
&lt;p>關鍵負載形狀：DoorDash 是 &lt;em>規模化外送平台&lt;/em> — 訂單、Dasher 派遣、餐廳 menu、新業務（grocery / convenience）並存。寫入壓力來自訂單成立、status 變更、地圖位置更新等多種 hot write path。2020 疫情前流量已大、疫情後再翻倍、且高峰集中在週末晚餐 / 週日早午餐時段。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>DoorDash 的工程選擇揭露三個 OLTP 寫入容量設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Aurora 的「single-primary 寫入」是規模化的天花板&lt;/strong>：Aurora 把 storage 跟 compute 分離、read replica 容易擴、&lt;em>但寫入仍走唯一 primary&lt;/em>。1.636 M QPS 不是均勻分佈、是 hot table 集中寫爆。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取&lt;/a> 的寫入容量規劃。CockroachDB 改成 Raft per range、每個 node 都能服務寫入、容量隨節點線性擴。&lt;/li>
&lt;li>&lt;strong>Migration 工具自製是先決條件、不是 nice-to-have&lt;/strong>：DoorDash 沒「一次性遷整套」、而是先寫工具把 table 從主 cluster 拆到獨立 Aurora cluster（紓壓）、再寫第二套工具把 Aurora → CockroachDB（換引擎）。兩階段都要 &lt;em>lossless&lt;/em> + &lt;em>可回退&lt;/em>。對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook&lt;/a> 的「先建工具、再遷資料」原則。&lt;/li>
&lt;li>&lt;strong>Cluster 數量增加、alert volume 卻下降&lt;/strong>：直覺反過來、cluster 多 = 維運面變大、應該更多 alert。但每個 CockroachDB cluster 內建 Raft 自動容錯、單節點 fail 不會 page on-call、Aurora 時代的「primary failover alert」消失。對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 的「告警 surface 設計」與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06.x reliability&lt;/a> 的 graceful degradation。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：1.636 M QPS 是 &lt;em>主 cluster 峰值&lt;/em>、不是「DoorDash 全部寫入 QPS」。case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster」。讀案例時不要把這個數字當成「CockroachDB 撐 1.6 M QPS」的證據、它是 &lt;em>Aurora 在那個時間點撞牆的痛點&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「single-primary OLTP 撞到寫入天花板」如何用 distributed SQL 拆解。跟 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 對比 — DraftKings 在 Aurora 上靠「業務切 200 個獨立 cluster」橫向擴展、DoorDash 是「保留 PostgreSQL wire 介面、但底層換成多主寫入的 CockroachDB」。兩條路徑都在解「Aurora 單主寫入容量上限」、走法不同。</p>
<h2 id="觀察">觀察</h2>
<p>DoorDash 從 Aurora Postgres 遷到 CockroachDB 的關鍵敘述（引自 <a href="https://www.cockroachlabs.com/blog/aurora-postgres-to-cockroachdb/">Why DoorDash migrated from Aurora Postgres to CockroachDB</a> / <a href="https://thenewstack.io/how-doordash-migrated-from-aurora-postgres-to-cockroachdb/">The New Stack 報導</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2020-04-17 高峰 QPS</td>
          <td>&gt; 1.636 million QPS</td>
      </tr>
      <tr>
          <td>事件結果</td>
          <td>multi-hour outage</td>
      </tr>
      <tr>
          <td>事件背景</td>
          <td>疫情封鎖、外送需求暴增</td>
      </tr>
      <tr>
          <td>遷移啟動</td>
          <td>事件後幾週、先把 table 從主 cluster 拆出</td>
      </tr>
      <tr>
          <td>第一階段移轉量</td>
          <td>一個月內把 dozens of tables 拆到獨立 Aurora cluster</td>
      </tr>
      <tr>
          <td>第二階段</td>
          <td>自動化工具把 Aurora Postgres → CockroachDB</td>
      </tr>
      <tr>
          <td>後續結果</td>
          <td>跑更多 cluster、incident alert volume 反而下降</td>
      </tr>
  </tbody>
</table>
<p>服務組合：Aurora Postgres（遷移前主要 OLTP）、CockroachDB self-hosted、自製 table extraction tool、自製 lossless migration pipeline。</p>
<p>關鍵負載形狀：DoorDash 是 <em>規模化外送平台</em> — 訂單、Dasher 派遣、餐廳 menu、新業務（grocery / convenience）並存。寫入壓力來自訂單成立、status 變更、地圖位置更新等多種 hot write path。2020 疫情前流量已大、疫情後再翻倍、且高峰集中在週末晚餐 / 週日早午餐時段。</p>
<h2 id="判讀">判讀</h2>
<p>DoorDash 的工程選擇揭露三個 OLTP 寫入容量設計重點。</p>
<ol>
<li><strong>Aurora 的「single-primary 寫入」是規模化的天花板</strong>：Aurora 把 storage 跟 compute 分離、read replica 容易擴、<em>但寫入仍走唯一 primary</em>。1.636 M QPS 不是均勻分佈、是 hot table 集中寫爆。對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a> 的寫入容量規劃。CockroachDB 改成 Raft per range、每個 node 都能服務寫入、容量隨節點線性擴。</li>
<li><strong>Migration 工具自製是先決條件、不是 nice-to-have</strong>：DoorDash 沒「一次性遷整套」、而是先寫工具把 table 從主 cluster 拆到獨立 Aurora cluster（紓壓）、再寫第二套工具把 Aurora → CockroachDB（換引擎）。兩階段都要 <em>lossless</em> + <em>可回退</em>。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> 的「先建工具、再遷資料」原則。</li>
<li><strong>Cluster 數量增加、alert volume 卻下降</strong>：直覺反過來、cluster 多 = 維運面變大、應該更多 alert。但每個 CockroachDB cluster 內建 Raft 自動容錯、單節點 fail 不會 page on-call、Aurora 時代的「primary failover alert」消失。對應 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 的「告警 surface 設計」與 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06.x reliability</a> 的 graceful degradation。</li>
</ol>
<p>需要警惕：1.636 M QPS 是 <em>主 cluster 峰值</em>、不是「DoorDash 全部寫入 QPS」。case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster」。讀案例時不要把這個數字當成「CockroachDB 撐 1.6 M QPS」的證據、它是 <em>Aurora 在那個時間點撞牆的痛點</em>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>single-primary 撞牆前、先評估 multi-primary 選項</strong>：Aurora / RDS Postgres 是 single-primary 為主、寫入量持續成長最終會撞天花板。轉折點不是 IOPS、是 <em>primary CPU + WAL flush rate</em>。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的瓶頸辨識。</li>
<li><strong>遷 OLTP 引擎要走「兩階段紓壓」</strong>：先在原引擎內把 hot table 拆出（降低主 cluster 壓力、爭取時間）、再規劃換引擎（架構級改造）。直接「一次性換引擎」風險過高。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a>。</li>
<li><strong>PostgreSQL wire protocol 相容性是降低遷移成本的關鍵</strong>：DoorDash 保留 PostgreSQL driver / ORM、應用層改動小。CockroachDB 不是 PostgreSQL fork、是 <em>protocol-level 相容</em>、實際 SQL 行為（serializable default、retry semantics、partial index）仍要驗證。對應 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a> 的 PostgreSQL 相容性 audit 段。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>AWS Aurora DSQL（2024）解同類「multi-primary 寫入」問題、但 AWS-only</li>
<li>Spanner（GCP）同類設計、GCP-only</li>
<li>TiDB（MySQL wire）解同類問題、亞洲生態深</li>
<li>自管 PostgreSQL + Citus（sharded extension）走 application 層 sharding、operation burden 較高</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解 single-primary 寫入天花板訊號 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> + <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01.6 高併發資料存取</a></li>
<li>想規劃 PostgreSQL → CockroachDB migration → <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> + <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></li>
<li>對照其他 OLTP 規模化案例 → <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a>（按業務切 cluster）/ <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 consolidation</a>（DB 種類整合）</li>
<li>想對照其他 distributed SQL 案例 → <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix CockroachDB fleet</a> / <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a></li>
<li>想理解全球一致性 OLTP 選型 → <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>想拆 CockroachDB transaction retry 與 contention 模式 → <a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a></li>
<li>想對比 Aurora DSQL / Spanner / CockroachDB 的選型 → <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.cockroachlabs.com/blog/aurora-postgres-to-cockroachdb/">Why DoorDash migrated from Aurora Postgres to CockroachDB</a></li>
<li><a href="https://thenewstack.io/how-doordash-migrated-from-aurora-postgres-to-cockroachdb/">How DoorDash Migrated from Aurora Postgres to CockroachDB（The New Stack）</a></li>
<li><a href="https://careersatdoordash.com/blog/how-we-scaled-new-verticals-fulfillment-backend-with-cockroachdb/">How We Scaled New Verticals Fulfillment Backend with CockroachDB（DoorDash Engineering Blog）</a></li>
<li><a href="https://www.infoq.com/news/2024/02/doordash-config-cockroachdb/">DoorDash Uses CockroachDB to Create Config Management Platform for Microservices（InfoQ）</a></li>
</ul>
]]></content:encoded></item><item><title>3.C39 Choria：NATS 管 50 萬 server fleet</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-choria-orchestration-fleet/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-choria-orchestration-fleet/</guid><description>&lt;p>這個案例的核心責任是說明 fire-and-forget RPC + scatter-gather pattern 是 NATS Core 的典型場景。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Choria 是 Puppet MCollective 的現代化替代品、目標管理數萬到數十萬節點的 fleet 同時下指令。評估過多個 broker、選 NATS 因為「單 binary、無 Zookeeper 依賴、Ruby client 品質好」、實測「單 server 300MB RAM 管 2000+ 機器」、4GB 節點可達 50 萬 server。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>MCollective 的 fire-and-forget RPC 語意正好對應 NATS Core 的 stateless best-effort + request-reply pattern、用 wildcard subject + queue group 做 parallel scatter-gather RPC。揭露 server orchestration 場景不需要 persistence、Core NATS 已足夠。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：Request/Reply pattern / Queue groups / Cluster + Supercluster + Leaf node（Choria Federation Broker = 跨地理 federation）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://nats.io/blog/nats-for-the-marionette-collective/">NATS for the Marionette Collective (Choria)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://choria.io/docs/concepts/">Choria Architecture Docs&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 fire-and-forget RPC + scatter-gather pattern 是 NATS Core 的典型場景。</p>
<h2 id="觀察">觀察</h2>
<p>Choria 是 Puppet MCollective 的現代化替代品、目標管理數萬到數十萬節點的 fleet 同時下指令。評估過多個 broker、選 NATS 因為「單 binary、無 Zookeeper 依賴、Ruby client 品質好」、實測「單 server 300MB RAM 管 2000+ 機器」、4GB 節點可達 50 萬 server。</p>
<h2 id="判讀">判讀</h2>
<p>MCollective 的 fire-and-forget RPC 語意正好對應 NATS Core 的 stateless best-effort + request-reply pattern、用 wildcard subject + queue group 做 parallel scatter-gather RPC。揭露 server orchestration 場景不需要 persistence、Core NATS 已足夠。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：Request/Reply pattern / Queue groups / Cluster + Supercluster + Leaf node（Choria Federation Broker = 跨地理 federation）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://nats.io/blog/nats-for-the-marionette-collective/">NATS for the Marionette Collective (Choria)</a></li>
<li><a href="https://choria.io/docs/concepts/">Choria Architecture Docs</a></li>
</ul>
]]></content:encoded></item><item><title>Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/</guid><description>&lt;p>Aurora cross-AZ failover 的 RTO 文件數字是「&amp;lt; 30 秒」、但 application 端實測常常看到 60-120 秒 — 這個落差不是 Aurora 慢、是 &lt;em>DNS cache + connection pool + retry policy&lt;/em> 的對齊問題。本文展開 failover lifecycle 三段（detection / promotion / DNS update）、application 端 reconnect 契約、量測真實 RTO 的流程、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 受監管銀行業務為什麼選獨立 cluster 而非 Global Database failover 的合規 driver。&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>）— 而是 failover 流程的實作層教學。前置閱讀建議 &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a>（理解為什麼 Aurora failover 不需要 data catch-up）。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：DraftKings / Standard Chartered 等級的金融交易服務、AZ-level outage 期間用戶操作不能斷、RTO 預算 &amp;lt; 60 秒、但 application 端看到的 reconnect 行為跟 AWS 文件不一致。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「Failover trigger 後新 connection 還連到舊 primary、為什麼？」&lt;/li>
&lt;li>「Writer endpoint DNS 切換了、application 還沒重連、什麼時候會切？」&lt;/li>
&lt;li>「Failover 期間 in-flight transaction 是全 abort 還是部分 commit？」&lt;/li>
&lt;li>「我手動測 failover RTO 量出 90 秒、AWS 文件講 &amp;lt; 30 秒、誰錯？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：失敗模式分布在 &lt;em>application 端的 connection state&lt;/em>、不只是 Aurora 端的 promotion 流程。Aurora 端的 promotion 在 storage 共享下確實 &amp;lt; 30 秒（不需要等 data catch-up）、但 application reconnect 受 JVM DNS cache、connection pool validation、retry policy 影響、容易把總體 RTO 拉長到 2-3 倍。&lt;/p>
&lt;p>對 Standard Chartered 這種受監管銀行業務、failover 還有合規維度：受監管市場資料 &lt;em>不能跨境複製&lt;/em>、Global Database 在這種場景違反合規、必須用每市場獨立 cluster 的 cross-AZ failover 吸收 RTO 預算。這個 driver 跟一般工程「跨 region failover 更好」的直覺相反。&lt;/p>
&lt;h2 id="核心機制failover-lifecycle-三段">核心機制：failover lifecycle 三段&lt;/h2>
&lt;p>Aurora cross-AZ failover 的 first-class concept 是 &lt;em>failover lifecycle 三段&lt;/em>：detection → promotion → DNS update。每一段有自己的 SLA 跟可調維度。&lt;/p></description><content:encoded><![CDATA[<p>Aurora cross-AZ failover 的 RTO 文件數字是「&lt; 30 秒」、但 application 端實測常常看到 60-120 秒 — 這個落差不是 Aurora 慢、是 <em>DNS cache + connection pool + retry policy</em> 的對齊問題。本文展開 failover lifecycle 三段（detection / promotion / DNS update）、application 端 reconnect 契約、量測真實 RTO 的流程、跟 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 受監管銀行業務為什麼選獨立 cluster 而非 Global Database failover 的合規 driver。</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>）— 而是 failover 流程的實作層教學。前置閱讀建議 <a href="../storage-architecture/">Aurora storage architecture</a>（理解為什麼 Aurora failover 不需要 data catch-up）。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：DraftKings / Standard Chartered 等級的金融交易服務、AZ-level outage 期間用戶操作不能斷、RTO 預算 &lt; 60 秒、但 application 端看到的 reconnect 行為跟 AWS 文件不一致。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「Failover trigger 後新 connection 還連到舊 primary、為什麼？」</li>
<li>「Writer endpoint DNS 切換了、application 還沒重連、什麼時候會切？」</li>
<li>「Failover 期間 in-flight transaction 是全 abort 還是部分 commit？」</li>
<li>「我手動測 failover RTO 量出 90 秒、AWS 文件講 &lt; 30 秒、誰錯？」</li>
</ul>
<p>進一步問題：失敗模式分布在 <em>application 端的 connection state</em>、不只是 Aurora 端的 promotion 流程。Aurora 端的 promotion 在 storage 共享下確實 &lt; 30 秒（不需要等 data catch-up）、但 application reconnect 受 JVM DNS cache、connection pool validation、retry policy 影響、容易把總體 RTO 拉長到 2-3 倍。</p>
<p>對 Standard Chartered 這種受監管銀行業務、failover 還有合規維度：受監管市場資料 <em>不能跨境複製</em>、Global Database 在這種場景違反合規、必須用每市場獨立 cluster 的 cross-AZ failover 吸收 RTO 預算。這個 driver 跟一般工程「跨 region failover 更好」的直覺相反。</p>
<h2 id="核心機制failover-lifecycle-三段">核心機制：failover lifecycle 三段</h2>
<p>Aurora cross-AZ failover 的 first-class concept 是 <em>failover lifecycle 三段</em>：detection → promotion → DNS update。每一段有自己的 SLA 跟可調維度。</p>
<p><strong>Detection（10-15 秒）</strong>：</p>
<ul>
<li>AWS 內部 health check 每幾秒檢查 primary writer health</li>
<li>連續失敗到一定閾值才 trigger failover（避免 false positive）</li>
<li>讀者無法直接調 detection 閾值、是 AWS managed</li>
</ul>
<p><strong>Promotion（&lt; 5 秒）</strong>：</p>
<ul>
<li>選 PromotionTier 最低的 read replica 升 primary</li>
<li>Storage 跨 AZ 共享、replica 升 primary <em>不需要 data catch-up</em>（vs 傳統 PostgreSQL streaming replication 要等 WAL apply）</li>
<li>Promotion 本身極快、是 Aurora storage 設計的直接受益</li>
</ul>
<p><strong>DNS update（5-15 秒）</strong>：</p>
<ul>
<li>Cluster endpoint / writer endpoint DNS 切到新 primary</li>
<li>Aurora endpoint DNS TTL 是 5 秒、AWS DNS infrastructure 通常 5-15 秒 propagate 完</li>
<li>但 application 端的 DNS cache 可能 cache 更久 — JVM <code>networkaddress.cache.ttl</code> 預設 -1（cache forever）就會卡在這層</li>
</ul>
<p><strong>Endpoint 類型跟 failover 行為</strong>：</p>
<ul>
<li><strong>Writer endpoint</strong>：跟著 failover 走、DNS 切到新 primary、application 寫操作用這個</li>
<li><strong>Reader endpoint</strong>：load-balance 到所有 replica；failover 期間短暫包含 promoted replica（已升 primary）、reader query 可能打到 primary、引起寫鎖競爭</li>
<li><strong>Custom endpoint</strong>：用戶自定 routing rule、failover 期間行為要驗證、不能假設自動跟隨</li>
</ul>
<p><strong>跟通用 failover 差在哪</strong>：Aurora 不需要 data catch-up phase、failover 主要瓶頸是 DNS propagation + application reconnect、不是 promotion 本身。傳統 PostgreSQL streaming replication failover 要等 replica WAL catch-up（heavy write 期間可能秒級延遲）、Aurora 在 storage 設計下消除這段等待。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>。</p>
<h2 id="step-by-step-配置--量測">Step-by-step 配置 / 量測</h2>
<p><strong>Cluster failover 配置</strong>：</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"># 確認 cluster 至少有一個跨 AZ replica</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws rds describe-db-clusters <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>  --query <span class="s1">&#39;DBClusters[0].DBClusterMembers&#39;</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"># 設定 PromotionTier（0 最優先、15 最不優先）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">aws rds modify-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --db-instance-identifier my-replica-az-b <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --promotion-tier <span class="m">0</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="c1"># 跨 region replica 預設 tier 15（不優先升、避免 failover 跨 region）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">aws rds modify-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --db-instance-identifier my-cross-region-replica <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --promotion-tier <span class="m">15</span></span></span></code></pre></div><p><strong>Application 端 JVM 設定</strong>（最常踩雷的點）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-properties" data-lang="properties"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># JVM 系統 property、預設 -1 = cache forever、必改</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">networkaddress.cache.ttl</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">networkaddress.cache.negative.ttl</span><span class="o">=</span><span class="s">0</span></span></span></code></pre></div><p><strong>Connection pool 設定</strong>（HikariCP 範例）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">spring.datasource.hikari</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">maximum-pool-size</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">connection-test-query</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SELECT 1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">validation-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">max-lifetime</span><span class="p">:</span><span class="w"> </span><span class="m">1800000</span><span class="w">      </span><span class="c"># 30 分鐘、強制 recycle connection</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">keepalive-time</span><span class="p">:</span><span class="w"> </span><span class="m">30000</span><span class="w">      </span><span class="c"># 30 秒檢查 idle connection</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">connection-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">30000</span></span></span></code></pre></div><p><strong>Retry policy</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 簡化範例、實際用 Resilience4j 或 Failsafe</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">RetryPolicy</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span><span class="w"> </span><span class="n">retryPolicy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">RetryPolicy</span><span class="p">.</span><span class="na">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">handle</span><span class="p">(</span><span class="n">SQLTransientConnectionException</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="n">SQLNonTransientConnectionException</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">withBackoff</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">1</span><span class="p">),</span><span class="w"> </span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">30</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">withMaxAttempts</span><span class="p">(</span><span class="n">5</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span></span></span></code></pre></div><p><strong>手動觸發 failover 量測 RTO</strong>：</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"># 觸發 failover、記錄時間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">START</span><span class="o">=</span><span class="k">$(</span>date +%s%3N<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws rds failover-db-cluster --db-cluster-identifier my-cluster
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Failover triggered at </span><span class="nv">$START</span><span class="s2"> ms&#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 class="c1"># 用 application heartbeat 寫入時間戳</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># application 端跑 every-second insert、failover 後第一個成功 insert 的時間 - START = RTO</span></span></span></code></pre></div><p><strong>驗證點</strong>：</p>
<ul>
<li>CloudWatch <code>FailoverEvent</code> counter &gt; 0（failover 觸發訊號）</li>
<li><code>DatabaseConnections</code> 在 failover 期間 drop &gt; 50%、之後 spike（reconnect 風暴）</li>
<li>Application metric「first successful write after failover trigger」&lt; 30 秒</li>
</ul>
<p><strong>Rollback boundary</strong>：promotion 不可逆 — 原 primary 變 replica、不會自動 fallback。要切回原 AZ 必須再做一次 failover。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1dns-cache-把-rto-從-30-秒拉到-120-秒">Case 1：DNS cache 把 RTO 從 30 秒拉到 120 秒</h3>
<p>徵兆：手動 failover 後、CloudWatch <code>FailoverEvent</code> 1 秒內出現、但 application log 顯示寫操作 120 秒後才恢復。</p>
<p>原因：JVM <code>networkaddress.cache.ttl</code> 預設 <code>-1</code>（cache forever）、application JVM 把 writer endpoint DNS 永久 cache 到舊 primary IP；只有 connection pool eviction 或 application restart 才會重新 resolve。</p>
<p>修：</p>
<ul>
<li>JVM startup 加 <code>-Dnetworkaddress.cache.ttl=5</code></li>
<li>或在 <code>$JAVA_HOME/lib/security/java.security</code> 改 <code>networkaddress.cache.ttl=5</code></li>
<li>Python application 通常沒這問題（DNS resolve per connection）、但要確認 SQLAlchemy 用 <code>pool_pre_ping=True</code></li>
</ul>
<h3 id="case-2connection-pool-cached-connection-全-stale">Case 2：Connection pool cached connection 全 stale</h3>
<p>徵兆：DNS 切換 OK、但 application 寫操作 timeout 10-30 秒後才觸發 reconnect、p99 latency spike。</p>
<p>原因：connection pool 的 cached connection 還指向舊 primary IP、validation 沒開或 timeout 太長、application 拿到 stale connection 才發現 backend gone。</p>
<p>修：</p>
<ul>
<li>HikariCP：<code>connection-test-query: &quot;SELECT 1&quot;</code> + <code>validation-timeout: 5000</code> + <code>keepalive-time: 30000</code></li>
<li>SQLAlchemy：<code>pool_pre_ping=True</code> + <code>pool_recycle=1800</code></li>
<li>failover 演練後驗證 connection pool 在 30 秒內 evict 完所有 stale connection</li>
</ul>
<h3 id="case-3reader-endpoint-failover-期間打到新-primary">Case 3：Reader endpoint failover 期間打到新 primary</h3>
<p>徵兆：failover 期間 application read query 偶發出現 <code>cannot execute SELECT in a read-only transaction</code> 或寫鎖競爭、用戶看到 inconsistent state。</p>
<p>原因：reader endpoint 是 DNS-based load balance 到所有 replica、failover 期間 <em>短暫</em> 包含已升 primary 的 replica（DNS propagation 期間 reader 跟 writer endpoint 都指向同一台）。Read query 打到 primary 後、跟正在寫的 transaction 競爭。</p>
<p>修：</p>
<ul>
<li>Application 端 read 跟 write data source 拆分、不要假設 reader endpoint 永遠 read-only</li>
<li>Failover 期間 application 端做 SQL error type 偵測、<code>read-only transaction</code> 錯誤觸發 retry</li>
<li>用 custom endpoint group 特定 replica、failover 期間 custom endpoint 行為更可控</li>
</ul>
<h3 id="case-4in-flight-transaction-全-abort">Case 4：In-flight transaction 全 abort</h3>
<p>徵兆：failover 期間正在執行的 transaction <em>全部 abort</em>、application 看到 <code>connection reset</code> 或 <code>server closed connection</code>、commit 沒成功。</p>
<p>原因：Aurora failover 不保留 transaction 狀態、所有 in-flight transaction（包括已執行 BEGIN 但還沒 COMMIT 的）全 abort。Application 沒做 idempotent retry 就會丟失 commit。</p>
<p>修：</p>
<ul>
<li>寫操作必須 idempotent（用 idempotency key、application 端做 deduplication）</li>
<li>在 application 層做 transaction-level retry、不在 connection 層 retry</li>
<li>重要寫入做 <em>write-then-verify</em> 模式：commit 後立刻 SELECT 確認、失敗才 retry</li>
</ul>
<h3 id="case-5promotiontier-配置忽略">Case 5：PromotionTier 配置忽略</h3>
<p>徵兆：failover 後 application latency 暴漲、發現升 primary 的是 cross-region replica。</p>
<p>原因：cross-region replica 預設 PromotionTier 是 1（或忘記改）、failover 時優先升、application 跟新 primary 跨 region、latency 從 5ms 變 100ms+。</p>
<p>修：</p>
<ul>
<li>cross-region replica <code>--promotion-tier 15</code>（不優先升）</li>
<li>同 region 跨 AZ replica <code>--promotion-tier 0</code> 或 <code>1</code></li>
<li>Multi-AZ deployment 至少配 2 個 same-region replica、避免 cross-region 被升</li>
</ul>
<h2 id="standard-chartered-為什麼選獨立-cluster-而非-global-database">Standard Chartered 為什麼選獨立 cluster 而非 Global Database</h2>
<p><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 揭露受監管產業的 failover 設計選擇 — 案例「判讀」段第 1 點：「7 個受監管市場代表 7 個獨立 cluster（資料不能跨境）、容量規劃變成『7 個獨立規劃 × 各自合規門檻』」。</p>
<p><strong>合規 driver</strong>：</p>
<ul>
<li>受監管市場資料 <em>不能跨境複製</em></li>
<li>Aurora Global Database 是跨 region async replication、會把資料推到其他 region</li>
<li>→ Global Database 在這種場景 <em>違反合規</em>、不是 DR 選項</li>
<li>必須用每市場獨立 cluster、各自做 cross-AZ failover、各自吸收 RTO 預算</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>每市場 cross-AZ failover RTO &lt; 30 秒、滿足當地監管 RTO 要求</li>
<li>跨市場 DR 不靠 Global Database、靠應用層的 <em>市場切換</em>（用戶從 A 市場切到 B 市場是業務決策、不是技術 failover）</li>
<li>7 個 cluster 各自獨立、operational surface area × 7（parameter group / backup / IAM / observability fan-out）、但合規要求壓倒運維成本</li>
</ul>
<p><strong>Fleet 拓樸</strong>：合規驅動的 fleet 設計（7 個受監管市場 = 7 個獨立 cluster）詳見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet 治理 SSoT 邊界段。本篇只展開 <em>單 cluster cross-AZ failover</em> 流程、不展開跨 cluster 拓樸決策。</p>
<p><strong>scope warning（必明示、case 自承）</strong>：Standard Chartered case 未公開是 PostgreSQL 還是 MySQL、未公開具體 cost 數字、屬「相關 case study」匿名對照。引用時不能擴寫具體 engine。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</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">FailoverEvent           # failover 觸發 counter、&gt; 0 立即通知
</span></span><span class="line"><span class="ln">2</span><span class="cl">DatabaseConnections     # failover 期間 drop、之後 spike
</span></span><span class="line"><span class="ln">3</span><span class="cl">AuroraReplicaLag        # failover 前 replica 是否 caught up</span></span></code></pre></div><p><strong>Application 端 metric</strong>：</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">first_successful_write_after_failover  # 真實 RTO
</span></span><span class="line"><span class="ln">2</span><span class="cl">connection_pool_error_rate              # stale connection 訊號
</span></span><span class="line"><span class="ln">3</span><span class="cl">db_retry_count                          # retry policy 觸發頻率</span></span></code></pre></div><p><strong>量測 RTO 流程</strong>：</p>
<ol>
<li>跑 application 端 every-second heartbeat insert</li>
<li>手動觸發 failover、記錄 trigger 時間戳</li>
<li>從 heartbeat insert log 找 failover 後第一個成功 insert 的時間戳</li>
<li>差值 = 真實 RTO（包含 detection + promotion + DNS + reconnect）</li>
</ol>
<p><strong>Alert</strong>：</p>
<ul>
<li><code>FailoverEvent &gt; 0</code> 立即通知 on-call</li>
<li><code>DatabaseConnections</code> 5 分鐘內 drop &gt; 50% 警告 stale connection</li>
<li><code>db_retry_count</code> 短期內 spike 警告 reconnect 風暴</li>
</ul>
<p><strong>Failover 演練頻率</strong>：</p>
<ul>
<li>Non-critical workload：每季一次 planned failover drill</li>
<li>受監管產業（Standard Chartered 類）：每月一次、有合規 sign-off 記錄</li>
<li>重大版本升級前必跑一次</li>
</ul>
<p><strong>回路徑</strong>：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.x incident response</a> failover playbook、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 判斷 reconnect-bound vs query-bound。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../storage-architecture/">Aurora storage architecture</a> — 理解為什麼 Aurora failover 不需要 data catch-up（storage 跨 AZ 共享）</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — replica 升 primary 流程跟 fleet 治理 SSoT</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 跨 region failover RTO 不同數量級（2-15 分鐘 vs cross-AZ &lt; 30 秒）</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — HA redesign 是 operational redesign 主項、從 Patroni / Orchestrator 切到 Aurora cluster endpoint</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> — failover 期間 in-flight transaction abort 對 application 契約的影響</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.x incident response</a> — failover decision log</li>
</ul>
<p><strong>何時不用本文</strong>：non-critical workload、RTO 預算 &gt; 5 分鐘、Multi-AZ 預設配置足夠時可跳過、看 <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 overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO 卡片</a> — RTO 量測判讀</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html">Aurora high availability</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Survival Goals：zone 級 vs region 級配置與業務 SLO 倒推流程</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/survival-goals/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/survival-goals/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 &lt;em>survival goal 配置怎麼從業務 SLO 倒推、怎麼避開「cross-region = 更快」的動機誤判&lt;/em>。Raft replica 分佈機制屬前置、見 &lt;a href="../hlc-raft-consensus/">HLC + Raft consensus&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="multi-region-上線前的兩個錯誤期待">Multi-region 上線前的兩個錯誤期待&lt;/h2>
&lt;p>multi-region CockroachDB cluster 上線時、團隊最常踩的兩個錯誤期待：&lt;/p>
&lt;ul>
&lt;li>&lt;em>「default 配置應該就好、上線後再說」&lt;/em>：default 是 &lt;code>SURVIVE ZONE FAILURE&lt;/code>、一旦遇到 region failure 整 cluster 變 read-only、客訴湧入才發現要重新配&lt;/li>
&lt;li>&lt;em>「跨 region 應該會讓全球用戶都更快」&lt;/em>：跨 region quorum 物理上必然 &lt;em>增&lt;/em> 寫入 latency、把 multi-region 動機誤判成 latency 優化會在 production 撞牆&lt;/li>
&lt;/ul>
&lt;p>讀者進來最常問：&lt;/p>
&lt;ul>
&lt;li>&lt;code>SURVIVE ZONE FAILURE&lt;/code> 跟 &lt;code>SURVIVE REGION FAILURE&lt;/code> 差在哪？&lt;/li>
&lt;li>為什麼 region survival 寫入 latency 是 zone survival 的 3 倍？&lt;/li>
&lt;li>Default 配置是什麼、上線前該不該改？&lt;/li>
&lt;/ul>
&lt;p>要回答這三題、必須先把 survival goal 跟業務 SLO 的對應關係講清楚。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a> 提供最 concrete 的 SLO 倒推路徑：sportsbook 中 &lt;em>bet placement 不能 lose&lt;/em> — 玩家下注後系統 crash 沒紀錄、對博彩牌照是合規事故。CockroachDB Raft 3-replica + 跨 AZ + survival goal 配置是把這個業務不可丟事件翻譯成 DB 層保證。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> 則提供反直覺判讀：60+ multi-region cluster 主要動機是 &lt;em>region failure 0 downtime&lt;/em>、不是降 latency。Gaming cluster 48-node 跨 4 region 就是為了「region failover 不停服」、不是讓玩家延遲變低。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 <em>survival goal 配置怎麼從業務 SLO 倒推、怎麼避開「cross-region = 更快」的動機誤判</em>。Raft replica 分佈機制屬前置、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>。</p></blockquote>
<hr>
<h2 id="multi-region-上線前的兩個錯誤期待">Multi-region 上線前的兩個錯誤期待</h2>
<p>multi-region CockroachDB cluster 上線時、團隊最常踩的兩個錯誤期待：</p>
<ul>
<li><em>「default 配置應該就好、上線後再說」</em>：default 是 <code>SURVIVE ZONE FAILURE</code>、一旦遇到 region failure 整 cluster 變 read-only、客訴湧入才發現要重新配</li>
<li><em>「跨 region 應該會讓全球用戶都更快」</em>：跨 region quorum 物理上必然 <em>增</em> 寫入 latency、把 multi-region 動機誤判成 latency 優化會在 production 撞牆</li>
</ul>
<p>讀者進來最常問：</p>
<ul>
<li><code>SURVIVE ZONE FAILURE</code> 跟 <code>SURVIVE REGION FAILURE</code> 差在哪？</li>
<li>為什麼 region survival 寫入 latency 是 zone survival 的 3 倍？</li>
<li>Default 配置是什麼、上線前該不該改？</li>
</ul>
<p>要回答這三題、必須先把 survival goal 跟業務 SLO 的對應關係講清楚。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 提供最 concrete 的 SLO 倒推路徑：sportsbook 中 <em>bet placement 不能 lose</em> — 玩家下注後系統 crash 沒紀錄、對博彩牌照是合規事故。CockroachDB Raft 3-replica + 跨 AZ + survival goal 配置是把這個業務不可丟事件翻譯成 DB 層保證。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> 則提供反直覺判讀：60+ multi-region cluster 主要動機是 <em>region failure 0 downtime</em>、不是降 latency。Gaming cluster 48-node 跨 4 region 就是為了「region failover 不停服」、不是讓玩家延遲變低。</p>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 走另一條路：銀行受監管市場資料 <em>不能跨境</em>、不可用 region survival、必須拆每市場獨立 Aurora cluster + zone survival。這個 anti-recommendation 提醒「survival goal 不是越強越好、合規邊界優先於技術 HA 配置」。</p>
<h2 id="核心機制兩種-survival-goal--replica-placement">核心機制：兩種 survival goal + replica placement</h2>
<h3 id="兩種宣告式配置">兩種宣告式配置</h3>
<p>CockroachDB 把 HA 配置抽象成兩個 database-level（或 table-level）宣告：</p>
<ul>
<li><strong><code>SURVIVE ZONE FAILURE</code></strong>（default）：失去 1 個 AZ 仍能寫入。replica 跨 AZ 分佈、但可能集中在同一個 region 內。對應 RTO ~ 數秒（Raft + <a href="/blog/backend/knowledge-cards/leaseholder/" data-link-title="Leaseholder" data-link-desc="分散式 SQL 每個 range 在任一時間點的 read / write entry point、通常等於 Raft leader、承擔該 range 的 coordination">Leaseholder</a> 自動 failover）、RPO = 0（已 commit 資料不丟）</li>
<li><strong><code>SURVIVE REGION FAILURE</code></strong>：失去 1 個整個 region 仍能寫入。voting replica 強制跨 region、需要至少 3 個 region。對應 RTO ~ 數秒、RPO = 0、但寫入 latency 因跨 region quorum 結構性增加</li>
</ul>
<p>survival goal 是 <em>宣告式</em> 配置 — application 端不用手動指定 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> 的 replica placement、Raft 根據 survival goal + locality 自動分佈、用 <a href="/blog/backend/knowledge-cards/hybrid-logical-clock/" data-link-title="Hybrid Logical Clock" data-link-desc="用 physical wall clock &#43; monotonic logical counter 給每個事件 timestamp、靠軟體 max-offset 保證跨節點時鐘差不超過上限、超過 panic 保護一致性">Hybrid Logical Clock</a> 串接 commit ordering。對比通用 HA 設計（如 PostgreSQL streaming + Patroni manual failover）、CockroachDB 把這層邏輯壓進系統內。</p>
<h3 id="voting-vs-non-voting-replica">Voting vs non-voting replica</h3>
<p>region survival 模式下、CockroachDB 區分兩種 replica：</p>
<ul>
<li><strong>Voting replica</strong>：參與 Raft majority 決策、commit 必須等 voting majority ack。region survival 下 voting replica 強制跨 region — 這就是 <a href="/blog/backend/knowledge-cards/cross-region-quorum/" data-link-title="Cross-Region Quorum" data-link-desc="multi-region distributed SQL 強制 voting replica 跨 region、commit 等多 region quorum ack、跨洲 RTT 物理硬限">Cross-Region Quorum</a> 拓樸、commit latency 受跨洲 RTT 物理硬限主導</li>
<li><strong>Non-voting replica</strong>：只用來 serve <a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">Follower Read</a>、不參與 Raft commit。可以放在「不想列入 quorum 但希望本地 read 快」的 region</li>
</ul>
<p>實務影響：region survival 下、跨 3 region 配置最少 3 voting replica（每 region 1 個）、寫入要等其中 2 個 region 的 ack。若想讓第 4 個 region 也能本地 read、可以加 non-voting replica、不影響 commit latency 但增加 storage cost。</p>
<h3 id="配置語法">配置語法</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Database-level
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="w"> </span><span class="n">SURVIVE</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="n">FAILURE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Table-level（覆蓋 database 設定）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">SURVIVE</span><span class="w"> </span><span class="k">ZONE</span><span class="w"> </span><span class="n">FAILURE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 驗證
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">SURVIVAL</span><span class="w"> </span><span class="n">GOAL</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">ZONE</span><span class="w"> </span><span class="n">CONFIGURATION</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="p">;</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto 卡</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo 卡</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius 卡</a> 的具體機制實現。</p>
<h3 id="為什麼選-region-survival-是業務動機判讀不是技術-factf48">為什麼選 region survival 是業務動機判讀、不是技術 fact（F4.8）</h3>
<p>Netflix 60+ multi-region cluster 揭露的反直覺結論：<em>主要動機是 region failure 0 downtime、不是降 latency</em>。跨 region quorum 物理上必然增 latency — 跨洲 round trip 物理 ~70-80ms、Raft majority 需要 2 個 region ack、寫入 p99 因此被光速下界限制。</p>
<p>Gaming cluster 48-node 跨 4 region 就是為了「region failover 不停服」、不是讓玩家延遲變低。<strong>Scope warning</strong>：case 沒揭露 Gaming cluster 具體 p99 數字、只揭露「48-node、跨 4 region、region failure 不停服」這個拓樸 fact 跟業務動機釐清。</p>
<p>引用時若提到「region survival 怎麼提升用戶體驗」、要 <em>釐清成 survival、不是 latency 優化</em>。讓讀者誤把跨 region 當成 latency 解法、是這條決策最常見的源頭錯誤。</p>
<h2 id="操作流程從業務-slo-倒推-survival-goal">操作流程：從業務 SLO 倒推 survival goal</h2>
<h3 id="配置前置">配置前置</h3>
<p>region survival 的最小可運行配置：</p>
<ul>
<li>cluster 至少 3 個 region</li>
<li>每 region 至少 3 個節點（保證單一 region 內也能扛 AZ failure）</li>
<li>locality tag 配齊（region + zone）</li>
</ul>





<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"># Region us-east1 的節點</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cockroach start --locality<span class="o">=</span><span class="nv">region</span><span class="o">=</span>us-east1,zone<span class="o">=</span>us-east1-a ...
</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"># Region us-west2 的節點</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">cockroach start --locality<span class="o">=</span><span class="nv">region</span><span class="o">=</span>us-west2,zone<span class="o">=</span>us-west2-a ...
</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="c1"># Region eu-west1 的節點</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">cockroach start --locality<span class="o">=</span><span class="nv">region</span><span class="o">=</span>eu-west1,zone<span class="o">=</span>eu-west1-a ...</span></span></code></pre></div><h3 id="從業務-slo-倒推9c41-hard-rock-揭露f411">從業務 SLO 倒推（9.C41 Hard Rock 揭露、F4.11）</h3>
<p>Hard Rock Digital sportsbook 揭露的 5 步倒推流程：</p>
<ol>
<li><strong>列業務「不能丟」事件清單</strong>：bet placement、payment、order commit、settlement 等業務事件</li>
<li><strong>對每個事件決定 RPO</strong>：bet placement → RPO = 0（不可丟）、log audit → RPO = 1 分鐘（可接受 short-window 丟失）</li>
<li><strong>對 RPO = 0 事件決定故障域容忍</strong>：Hard Rock 案例 <em>Outpost 或 AZ 失敗不丟</em> 是業務要求、跨 region failure 不是 sportsbook 的硬需求（因為各州各自合規邊界）</li>
<li><strong>故障域容忍翻譯成 survival goal</strong>：
<ul>
<li>Outpost / AZ 失敗 → <code>SURVIVE ZONE FAILURE</code> 即可</li>
<li>region 失敗也不丟 → <code>SURVIVE REGION FAILURE</code></li>
</ul>
</li>
<li><strong>反過來驗 replica 分佈</strong>：survival goal 配置產出的 replica 分佈是否覆蓋業務故障域。Hard Rock CockroachDB Raft 3-replica + 跨 AZ → Outpost 失敗時其他 replica 在、自動 failover、滿足 bet placement RPO = 0</li>
</ol>
<h3 id="跟業務動機釐清的互補">跟業務動機釐清的互補</h3>
<p>Netflix 從技術配置 <em>反推</em>「為什麼選 region survival」（survival 動機、不是 latency）、Hard Rock 從業務不能丟事件 <em>正推</em> 該選哪個 survival goal。兩個方向是同一條路徑：</p>
<ul>
<li>正推（Hard Rock）：業務不能丟 → RPO → 故障域 → survival goal</li>
<li>反推（Netflix）：survival goal 配置 → 揭露的不是「會變快」而是「region failover 不停服」</li>
</ul>
<p>兩個方向互相驗證、避免把跨 region 配置誤解成 latency 工具。</p>
<h3 id="升級流程跟-rollback-邊界">升級流程跟 rollback 邊界</h3>
<p>zone survival → region survival 是 <em>非破壞性</em> 配置變更、Raft 自動 rebalance replica。但要注意：</p>
<ul>
<li>rebalance 期間 cross-region traffic 暴增、p99 短期波動</li>
<li>replication factor 增加 → storage 用量 × 新 RF</li>
<li>升級後 application 寫入 latency 結構性上升、要先在 staging 量過</li>
</ul>
<p>監控 rebalance：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 range 數量變化跟 rebalance queue
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">range_count</span><span class="p">,</span><span class="w"> </span><span class="n">used</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">kv_store_status</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- CockroachDB Console「Rebalance queue size」應該歸零</span></span></span></code></pre></div><p>Rollback：survival goal 可即時降級（region → zone）、replica 自動 rebalance、無不可逆動作。但 application 端如果已經依賴 region failover 0 downtime、降級回 zone survival 後 region failure 會讓 cluster 變 read-only — 配置 rollback 容易、業務 SLO rollback 不容易。</p>
<h2 id="失敗模式5-種典型錯配">失敗模式：5 種典型錯配</h2>
<h3 id="default-zone-survival-期待-region-survival">Default zone survival 期待 region survival</h3>
<p>最常見：上線後一個 region 掛、cluster 變 read-only、客訴。要在 production 前 <em>明確選</em> survival goal、不依賴 default。</p>
<h3 id="region-survival-但只配-2-region">Region survival 但只配 2 region</h3>
<p>Raft majority 需要 3 個獨立 fault domain。2 region 配置實際是 zone survival — 任一 region 失敗剩 1 region 拿不到 majority。要 region survival <em>至少</em> 3 region。</p>
<h3 id="cross-region-cost-暴漲">Cross-region cost 暴漲</h3>
<p>region survival 強制 voting replica 跨 region、每次 write 跨 region traffic × 3。AWS / GCP 的 cross-region data transfer 是高 markup、月費可能 2-3 倍。</p>
<p>production 前必須估：</p>
<ul>
<li>寫 QPS × row size × 3 = cross-region traffic GB/day</li>
<li>對應 cloud provider 定價（AWS 跨 region $0.02/GB、GCP 類似量級）</li>
<li>月度 traffic cost 加總、跟 single-region 配置比</li>
</ul>
<h3 id="locality-跟-survival-goal-衝突">Locality 跟 survival goal 衝突</h3>
<p>業務想把 user data partition by region 留 local（locality 配置）、但 survival goal 要求跨 region replica、結果 replica 仍跑遠端。這是 locality + survival 的互動議題、見 <a href="../locality-aware-schema/">locality-aware schema</a> 詳細展開。</p>
<h3 id="合規邊界-violation">合規邊界 violation</h3>
<p>受監管市場（金融 / 醫療 / 博彩）資料 <em>不能跨境</em>、但 region survival 強制 voting replica 跨 region — 這直接違反合規。對照 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 走的是「每市場獨立 Aurora cluster + zone survival」、不是 region survival。</p>
<p>合規邊界判讀：</p>
<ul>
<li>跨境合規 <em>禁止</em> 跨 region replica → 不可用 region survival、走 cluster-per-市場</li>
<li>跨州合規 <em>允許</em> 跨州但要求資料留國內 → 可用 region survival、選同國內的 region</li>
<li>業務邏輯要求跨 boundary（如 Hard Rock 跨州統一帳戶）→ 不可拆獨立 cluster、必須 locality + placement</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Raft replicas per node</code>：replica 分佈均勻度</li>
<li><code>Range count by survival mode</code>：region survival 配置的 range 數量</li>
<li><code>Cross-region write latency p99</code>：跨 region quorum 實測 latency</li>
<li><code>Rebalance queue size</code>：rebalance 是否完成</li>
<li><code>Network traffic by direction</code>：cross-region 流量、cost signal</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>region survival 最小：region count × 3 nodes</li>
<li>replica factor 預設 3、storage 用量 × replication factor</li>
<li>cross-region traffic = write QPS × row size × (region count - 1)</li>
</ul>
<h3 id="write-latency-預算屬通用工程估算case-未揭露具體-latency-數字">Write latency 預算（屬通用工程估算、case 未揭露具體 latency 數字）</h3>
<p><strong>Scope warning</strong>：以下數字屬通用工程估算（跨 region 物理光速下界推導）、<strong>Netflix / Hard Rock case 都沒揭露 zone / region survival 的 p99 latency 數字</strong>。引用時必須明示來源層次：</p>
<ul>
<li>zone survival single-region 寫入 p99 5-10ms（跨 AZ Raft round trip）</li>
<li>region survival 同洲跨 region p99 30-60ms（跨 region round trip × Raft majority）</li>
<li>region survival 跨洲 p99 100-150ms（跨洲光速下界 ~70-80ms × 2）</li>
</ul>
<p>數字屬「合理的工程估算量級」、不是 case 揭露的 p99。讀者用這些做容量規劃時應該自己 benchmark、不要直接套。</p>
<h3 id="賽季型容量擺盪9c41-hard-rock">賽季型容量擺盪（9.C41 Hard Rock）</h3>
<p>sportsbook 業務年度循環：NFL / NBA 季初季末流量結構性差異 — Hard Rock 100 nodes ↔ 33 nodes 擺盪是 <em>計畫內</em>、不是異常事件。CockroachDB 加減節點靠 range rebalance、不停服。</p>
<p>容量規劃要點：</p>
<ul>
<li>NFL / NBA / 國際賽事曆塞進預測模型、不要當 surprise</li>
<li>scale up 提前 1-2 週執行、留 rebalance 時間</li>
<li>scale down 在淡季低流量時段執行、避免 rebalance 期間 p99 spike</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> survival goal 對 replica count / cost 影響</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> event-driven scaling</li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">latency budget 卡</a> cross-region 預算</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：Raft 機制是 survival goal 的基礎</li>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：locality + survival 一起決定 placement</li>
<li><a href="../transaction-retry-pattern/">transaction retry pattern</a>：cross-region latency 加長 retry window</li>
</ul>
<h3 id="跟-aurora-對照">跟 Aurora 對照</h3>
<ul>
<li>Aurora cross-AZ failover：zone-level survival 等價、但只在 single-region 內</li>
<li>Aurora Global Database：跨 region async replication、不是 sync — region failure 仍會丟 last seconds</li>
<li>CockroachDB region survival：sync majority、region failure RPO = 0</li>
</ul>
<p>Aurora 沒有 row-level locality 配置、跨 region 強一致要走 Aurora DSQL（AWS 2024 GA）。</p>
<h3 id="aurora-dsql--spanner-對比">Aurora DSQL / Spanner 對比</h3>
<p>完整三家 distributed SQL 在 multi-region survival 的取捨、見 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游</li>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> distributed transaction</li>
</ul>
<h3 id="何時不用-region-survival">何時不用 region survival</h3>
<ul>
<li>single-region 已滿足業務 SLO → zone survival 即可</li>
<li>預算敏感、cross-region traffic cost 不划算</li>
<li>合規禁止跨境 → 必須拆每市場獨立 cluster + zone survival</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（bet placement RPO=0 倒推）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（Gaming 48-node 跨 4 region survival）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>（anti-recommendation、為何 <em>不用</em> region survival）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto 卡</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo 卡</a> / <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/multiregion-survival-goals.html">CockroachDB Multi-Region Survival Goals</a> / <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">Multi-Region Capabilities Overview</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB RU/s 成本模型 + 容量規劃：RU 思維、payload、index、provisioned vs autoscale vs serverless</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/</guid><description>&lt;p>Cosmos DB 用單一 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &amp;#43; memory &amp;#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit&lt;/a>（RU）抽象 read / write / query / replace 的成本。這個抽象 &lt;em>簡化&lt;/em> 容量規劃（不用拆 RCU/WCU、不用估 CPU + IOPS）、但也引入 &lt;em>團隊知識遷移&lt;/em> 成本 — 從 MongoDB / PostgreSQL 自管團隊轉過來、工程師要重新學「query 為什麼吃 200 RU」「payload 從 1KB 變 10KB cost 怎麼變」「index 改一個欄位 write RU 漲 30%」這些 RU 思維問題。本文先講 RU 思維的學習曲線、再進操作流程（依負載形狀選容量模式）、再進失敗模式（autoscale reactive 限制等）。&lt;/p>
&lt;p>本文不是 Cosmos DB overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a>）— 而是 &lt;em>RU 成本模型 + sizing&lt;/em> 的深度展開。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（24h 1.67 億 request、autoscale + RU budgeting）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（測試到 1M RU/s、RU 抽象單位定義）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>、本篇不重複展開。RU sizing + 容量模式選擇是 &lt;em>已選 Cosmos DB 後&lt;/em> 的成本決策；若 workload 不適用 Cosmos DB、RU sizing 無法救回 vendor 選錯的成本結構落差。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境ru-思維的學習曲線">問題情境：RU 思維的學習曲線&lt;/h2>
&lt;p>典型觸發場景：團隊原本用 MongoDB 自管 / PostgreSQL、把容量規劃成「CPU + IOPS + working set RAM」三軸；遷到 Cosmos DB 後第一個問題是「我們的 query 要設多少 RU/s」 — 文件回答「估每個操作的 RU × 操作頻率」、但工程師沒有 RU 的直覺、不知道「200 RU 是貴還是便宜」。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 用單一 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a>（RU）抽象 read / write / query / replace 的成本。這個抽象 <em>簡化</em> 容量規劃（不用拆 RCU/WCU、不用估 CPU + IOPS）、但也引入 <em>團隊知識遷移</em> 成本 — 從 MongoDB / PostgreSQL 自管團隊轉過來、工程師要重新學「query 為什麼吃 200 RU」「payload 從 1KB 變 10KB cost 怎麼變」「index 改一個欄位 write RU 漲 30%」這些 RU 思維問題。本文先講 RU 思維的學習曲線、再進操作流程（依負載形狀選容量模式）、再進失敗模式（autoscale reactive 限制等）。</p>
<p>本文不是 Cosmos DB overview（請看 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a>）— 而是 <em>RU 成本模型 + sizing</em> 的深度展開。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（24h 1.67 億 request、autoscale + RU budgeting）+ <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（測試到 1M RU/s、RU 抽象單位定義）。</p>
<blockquote>
<p><strong>Cosmos DB 適用度前置判讀</strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>、本篇不重複展開。RU sizing + 容量模式選擇是 <em>已選 Cosmos DB 後</em> 的成本決策；若 workload 不適用 Cosmos DB、RU sizing 無法救回 vendor 選錯的成本結構落差。</p></blockquote>
<h2 id="問題情境ru-思維的學習曲線">問題情境：RU 思維的學習曲線</h2>
<p>典型觸發場景：團隊原本用 MongoDB 自管 / PostgreSQL、把容量規劃成「CPU + IOPS + working set RAM」三軸；遷到 Cosmos DB 後第一個問題是「我們的 query 要設多少 RU/s」 — 文件回答「估每個操作的 RU × 操作頻率」、但工程師沒有 RU 的直覺、不知道「200 RU 是貴還是便宜」。</p>
<p>讀者徵兆：</p>
<ul>
<li>「為什麼這個 query 吃 200 RU」</li>
<li>「payload 從 1KB 變 10KB、cost 怎麼變」</li>
<li>「Autoscale vs Provisioned 怎麼選」</li>
<li>「Serverless 跟 Provisioned 的 break-even 在哪」</li>
<li>「Index policy 改了一個欄位、write RU 漲 30%」</li>
</ul>
<p>真實壓力：Black Friday 流量 10x、autoscale 跟不上 throttle；dev 環境 24/7 跑、付 provisioned 月費卻只用 1 小時；team 估 RU 估到一半發現「不知道怎麼估」、回去問 PM「我們的 access pattern 是什麼」、PM 給不出答案。</p>
<h3 id="從-cpu--iops-思維轉到-ru-思維">從 CPU + IOPS 思維轉到 RU 思維</h3>
<p>9.C11 Minecraft Earth 案例的平台特性段揭露的 RU 對照：</p>
<ul>
<li>1 RU = 1 KB document 的 strong-consistent read 成本</li>
<li>寫成本約 5 RU</li>
<li>複雜 query 可達數百 RU</li>
</ul>
<p>這個對照看起來簡單、但 <em>容量規劃變成「估每個操作多少 RU × 操作頻率」</em>、跟傳統 RDB「估 CPU / IOPS / working set RAM」是完全不同的思維。具體差異：</p>
<ul>
<li>用 RU 思考、不是用 CPU 思考 — 不需要估「query 跑多久」、要估「query 吃多少 RU」</li>
<li>量單一 query 的 <code>x-ms-request-charge</code> header、不是看 slow query log — 監控位置從 server 端移到 SDK response</li>
<li>拆 query 為 RU budget、不是調 indexing strategy — Cosmos DB index policy 影響 RU、但 <em>改 index 不改 query 速度</em>、改的是 cost</li>
</ul>
<p>跨 vendor 的 capacity 抽象差距（本章合成 frame、跨 vendor case 比對）：</p>
<ul>
<li>MongoDB 用 CPU + IOPS + working set 三軸</li>
<li>DynamoDB 用 WCU / RCU 二軸 + on-demand vs provisioned 模式選擇 + adaptive capacity</li>
<li>Cosmos DB 用 RU 單軸 + 5 consistency level</li>
</ul>
<p><em>思維遷移成本可能高過 vendor 廣告的價格差距</em> — 工程師需要 4-6 週才會建立 RU 直覺、selection 評估時不能只看 monthly bill 就做 ROI 結論。對中型團隊、這個學習曲線可能直接決定遷移成功率。</p>
<p><strong>Scope warning</strong>：9.C11 揭露「100 萬 RU/s 壓測通過」 — <em>壓測通過數字、不是 production 持續跑</em>（case 自己警示）。引用 1M RU/s 時必須帶 scope：壓測 vs 持續、case 明示「實際營運要看 partition key 設計是否均勻」。把壓測數字當 production capacity 推算的後果是 sizing 嚴重低估 hot partition 風險。</p>
<h2 id="ru-的核心機制">RU 的核心機制</h2>
<h3 id="ru-基準">RU 基準</h3>
<p>1 RU = strong-consistent read of 1KB document、用 CPU + memory + IOPS 綜合抽象。每個操作的 RU charge 從 SDK response 的 <code>x-ms-request-charge</code> header 拿、不是事後估算。</p>
<p>操作 RU 對照（rule of thumb、實際以 <code>x-ms-request-charge</code> 為準）：</p>
<ul>
<li>Read 1KB（point read）：1 RU（eventual / session 更便宜、strong / bounded staleness 約 2x）</li>
<li>Write 1KB：5-10 RU（含 index 更新）</li>
<li>Replace 1KB：10-15 RU</li>
<li>Query：跟 query plan + result count + index hit 強相關、可從 5 RU 到 1000+ RU</li>
</ul>
<h3 id="payload-size-的影響">Payload size 的影響</h3>
<p>每多 1 KB payload、write RU 線性增加；read 同 partition 多個 doc 用 query / feed 比多次 point read 更便宜。常見誤區是「拆小 doc 比較便宜」 — 不一定、要看 read pattern：若每次 read 都拿 10 個小 doc、不如合成一個大 doc 一次 read。</p>
<h3 id="index-policy-的影響">Index policy 的影響</h3>
<p>預設 indexing 全欄位（auto-indexing）、降 query cost 但提 write cost；customize index policy（exclude path / include path）可降 write RU 30-50%。判讀時：write-heavy collection 通常該 exclude 不查的欄位、read-heavy collection 通常該 include 常用 query 欄位。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;indexingMode&#34;</span><span class="p">:</span> <span class="s2">&#34;consistent&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;includedPaths&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/userId/?&#34;</span><span class="p">},</span> <span class="p">{</span><span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/orderDate/?&#34;</span><span class="p">}],</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;excludedPaths&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/*&#34;</span><span class="p">}]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="三種容量模式">三種容量模式</h3>
<ul>
<li><strong>Provisioned throughput</strong>：訂死 RU/s、不用也付、適合穩定流量</li>
<li><strong>Autoscale provisioned</strong>：訂 max、實際用多少算多少（10% min ceiling）、適合 unpredictable</li>
<li><strong>Serverless</strong>：完全按 request 計、小流量 / dev / 稀疏負載</li>
</ul>
<p>模式選擇不是「哪個便宜」、是「負載形狀適配哪個」— 下節展開。</p>
<h2 id="操作流程依負載形狀選容量模式">操作流程：依負載形狀選容量模式</h2>
<h3 id="量測單一-query-ru">量測單一 query RU</h3>
<p>SDK response header <code>x-ms-request-charge</code>、或 portal Query Stats。Phase 0 audit 一定要 <em>把 production query corpus 跑一遍量 RU</em>、不是估算 — 估算誤差通常 5-10x。</p>
<h3 id="量測-container-baseline-ru">量測 container baseline RU</h3>
<p><code>az cosmosdb sql container show-throughput</code>、portal Metrics &gt; Normalized RU Consumption。</p>
<h3 id="設定-autoscale">設定 autoscale</h3>





<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">az cosmosdb sql container update <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --max-throughput <span class="m">40000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --resource-group myrg --account-name mycosmos <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --database-name mydb --name mycontainer</span></span></code></pre></div><h3 id="依負載形狀對應容量模式">依負載形狀對應容量模式</h3>
<p>不同負載形狀的容量決策完全不同、不能用同一個模板：</p>
<p><strong>持續高峰（24h 整天高）</strong> — Provisioned + <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a></p>
<ul>
<li>Trigger 訊號：峰值 / 平均 &lt; 2x、預測性高</li>
<li>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a> — 24h 1.67 億 request、峰值 / 平均 = 1.81、整天高</li>
<li>為什麼選 provisioned：autoscale 的 reactive trigger 在持續高峰時仍會被拖累 p99、provisioned 鎖定 RU 反而平穩</li>
<li>Scheduled scaling 在 event 前 30-60 分鐘 pre-warm、避免事件開始 trigger autoscale</li>
</ul>
<p><strong>隨機 surge（不可預測 timing）</strong> — Autoscale + reactive safety net</p>
<ul>
<li>Trigger 訊號：不規則尖峰、預測訊號弱、流量曲線無規律</li>
<li>為什麼選 autoscale：成本不浪費（10% min ceiling）、reactive 雖然有延遲但比 over-provisioned 划算</li>
<li>Case anchor 屬本章合成 frame、case 庫未直接揭露純「隨機 surge」的 Cosmos DB 案例</li>
</ul>
<p><strong>預測性 surge（外部訊號可預測）</strong> — Pre-provision + scheduled scaling</p>
<ul>
<li>Trigger 訊號：賽事 / 上線 / 季節 peak、有外部訊號可學</li>
<li>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase predictive scaling</a> 模型對 KV / document 同適用 — ML 預測 60 分鐘領先窗、改善的是 <em>trigger 提前</em>、不是擴容本身變快</li>
<li>Coinbase case 是 MongoDB 場景、模型可借鑑、但 Cosmos DB 沒有直接對應 ML 預測整合、需要自建</li>
</ul>
<p><strong>稀疏 / dev / 低流量</strong> — Serverless</p>
<ul>
<li>Trigger 訊號：&lt; 1000 RU/s 預期、長時間閒置（如 dev / test / 內部工具）</li>
<li>Serverless 是建 account 時選、<em>不能事後轉 provisioned</em>、要在 Phase 0 決定</li>
<li>屬本章合成 frame、case 庫未直接揭露 serverless 場景（多數案例都是 production 流量）</li>
</ul>
<p><strong>本章合成 frame 警示</strong>：上表是跨 4 個 case 合成（9.C21 ASOS 提供「持續高峰」明確 anchor、9.C36 Coinbase 提供「預測性 surge」模型）、其他兩格屬 outline knowledge — 引用時必須明示「對照表是本章合成、case 原文沒有此分類」。</p>
<h3 id="切換-provisioned--autoscale">切換 provisioned ↔ autoscale</h3>
<p>portal / CLI 支援、不需停機；但 Serverless 是建 account 時選、<em>不能轉 provisioned</em>。Phase 0 決定 mode 後若要切 serverless ↔ provisioned 等於重建 account + 資料遷移。</p>
<h3 id="驗證點">驗證點</h3>
<ul>
<li>autoscale min ceiling = 10% max；若 traffic 預測 baseline &gt; 25% peak、autoscale 不划算（baseline 已經超過 min ceiling、autoscale 的彈性沒用上）</li>
<li>p99 query RU &lt; provisioned / 100（給 burst 留 100x buffer 是 rule of thumb、實際視 query 分布）</li>
<li>每個 query pattern 的 <code>x-ms-request-charge</code> &lt; SLA budget</li>
</ul>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>throughput 可即時改、index policy 改完背景 rebuild（rebuild 期間 query 用舊 index、性能可能下降但不中斷）；mode（serverless ↔ provisioned）不可改。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1用-point-read-取代-query">Failure 1：用 point read 取代 query</h3>
<p>要拿同 partition 100 個 doc、做 100 次 point read（100 RU）vs 一次 query（可能 10-20 RU）— point read 雖然每次便宜、總成本反高。這個 anti-pattern 在 application code 很常見 — 「每次 read 一個 doc 比較簡單」是 application 角度、不是 RU 角度。</p>
<p>修：拉 access pattern audit、把 N+1 read pattern 改 batch query；用 query 拿同 partition 多 doc、用 cross-partition query 拿不同 partition（成本高、但比 N+1 point read 通常還便宜）。</p>
<h3 id="failure-2index-全開不審">Failure 2：Index 全開不審</h3>
<p>所有欄位 auto-index、write 大表時 RU 暴漲；徵兆是 <code>Total RU consumption</code> 寫入路徑佔 80%、read 只佔 20%、但 application 明明 read-heavy。原因是 index 維護成本太高。</p>
<p>修：customize index policy、exclude 不查的欄位（特別是 array / nested object 等高成本欄位）、include 常用 query 路徑。改完背景 rebuild、不中斷服務。</p>
<h3 id="failure-3autoscale-min-沒考慮">Failure 3：Autoscale min 沒考慮</h3>
<p>max 40000、min 4000（10% max ceiling）、實際 baseline 是 500、付 8x baseline 費；應該降 max 或改 serverless。autoscale 的 <em>min ceiling</em> 是常見的隱性成本來源 — 訂太高 max 就被 min 綁住、autoscale 反而比 provisioned 貴。</p>
<p>修：先量 baseline 跟 peak、算 peak / baseline ratio；ratio &gt; 10x 用 autoscale 划算、ratio &lt; 4x 用 provisioned 划算（autoscale min ceiling 吃掉彈性）。</p>
<h3 id="failure-4autoscale-撐不住預測性流量必須-scheduled-scaling-或-pre-provision">Failure 4：Autoscale 撐不住預測性流量、必須 scheduled scaling 或 pre-provision</h3>
<p>autoscale 的 min ceiling = 10% max、實際擴容仍是 <em>reactive</em>（看到 throttle 才往上推）。對預測性流量（季節 peak / 賽事 / 上線日）、autoscale 跟不上、必須 scheduled scaling 或 pre-provision。</p>
<p>9.C21 ASOS Black Friday 是「持續高峰」、整天高 — 用 provisioned + scheduled 比 autoscale 划算（autoscale 仍會被 reactive trigger 拖累 p99）。9.C36 Coinbase 模型雖然是 MongoDB case、可借鑑：cluster 擴容 70 分鐘、reactive 來不及、ML 預測 60 分鐘領先窗、改善的是 <em>trigger 提前</em>、不是擴容本身變快 — Cosmos DB autoscale 的 10% ceiling 同樣是 reactive 限制。</p>
<p>修：預測性 event 前 30-60 分鐘 pre-warm RU/s、事件結束後降回；用 scheduled scaling pipeline（Azure Function trigger + ARM template）自動化。</p>
<h3 id="failure-5provisioned-沒退場">Failure 5：Provisioned 沒退場</h3>
<p>dev / staging container 全開 provisioned、月費 $300+ × N 個 environment；應切 serverless 或共用 shared throughput（多個 container 共享一個 RU pool）。dev 環境的 cost waste 是長尾、月底帳單才發現。</p>
<p>修：dev / staging 改 serverless、production 才 provisioned；或用 <em>shared database throughput</em>、多個 container 共用 400-1000 RU pool。</p>
<h3 id="failure-6跨-partition-query-浪費">Failure 6：跨 partition query 浪費</h3>
<p>query 沒包含 partition key 條件、fan-out 全 partition、RU × partition 數；徵兆是 <code>RetrievedDocumentCount</code> 跟 <code>OutputDocumentCount</code> 比例 &gt; 10（拿了 10x doc 才篩出要的）。</p>
<p>修：query 強制帶 partition key 條件、改 access pattern 讓 query 自然帶 partition key；若必須跨 partition、用 <a href="https://learn.microsoft.com/azure/cosmos-db/change-feed">Change Feed</a> 把投影預先寫到另一個 container 用單一 partition 查。</p>
<h3 id="failure-7沒設-budget-alert">Failure 7：沒設 budget alert</h3>
<p>cost 失控直到月底帳單才發現。Cosmos DB 的成本可以在幾天內飆 10x（hot partition + index 全開 + autoscale max 設太高 互相加乘）、月底才看是災難。</p>
<p>修：Azure Cost Management 設 daily budget alert（超預算 1.5x trigger）、portal Insights &gt; Cost insights 每週 review。</p>
<h3 id="failure-8ttl-自動刪除把-ru-偷走">Failure 8：TTL 自動刪除把 RU 偷走</h3>
<p>Cosmos DB 容器層的 TTL（<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/time-to-live">Time To Live</a>）會在 background 持續掃描過期文件、跑 delete 操作消耗 RU、但不會出現在 application driver 的 RU 統計、容易在 sizing 階段被忽略。屬通用工程議題、case 未直接量化 TTL 對 RU 的佔比。</p>
<p>徵兆：</p>
<ul>
<li>Provisioned RU 估算「query + write」流量明明很穩、實際 <code>NormalizedRUConsumption</code> 卻偏高、找不到對應 application call</li>
<li>高寫入率 container 開啟 TTL 後、<code>Total Request Units</code> 持續高於預期、portal Insights 「Background operations」段非零</li>
<li>TTL 設過短（例：分鐘級）、background delete 跟 application write 競爭同 partition、寫入 latency p99 變高</li>
</ul>
<p>修：</p>
<ul>
<li>估 RU 容量時把 TTL delete 當第三類流量（除了 user read / write 外）、用「過期 doc / 秒 × 平均 doc delete RU」估算</li>
<li>設定 TTL 不要過短、避免 delete 壓力跟 application write 撞 partition</li>
<li>對高 TTL volume 的 container 開啟 <a href="https://learn.microsoft.com/azure/cosmos-db/analytical-store-introduction">analytical store</a>、避免歷史資料保留在 transactional store 持續耗 RU</li>
<li>監控 <code>Background operations</code> 跟 <code>NormalizedRUConsumption</code> 的 ratio、把 TTL 對 RU 的影響可視化</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>NormalizedRUConsumption</code>（peak）、<code>TotalRequestUnits</code>（cumulative）、<code>MetadataRequests</code>、<code>UserErrors</code>（for <code>429 throttle</code>）</li>
<li>成本分析：Azure Cost Management 按 container / region tag；portal Insights &gt; Cost insights</li>
<li>容量公式：peak RPS × avg RU per request × peak duration factor = required RU/s</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 把 RU 當主要 capacity 軸（不只 storage / CPU）</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>：把 429 throttle 當 saturation 訊號</li>
<li>Alert：429 rate &gt; 0.1%、RU consumption &gt; 80% provisioned 持續 5 min、daily cost 超預算 1.5x</li>
</ul>
<h3 id="latency-budget-拆解vendor-sla-vs-end-to-end-實測">Latency budget 拆解：vendor SLA vs end-to-end 實測</h3>
<p><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a> 觀察「48ms 平均響應」段揭露：48ms 包含 <em>網路 + DB + 應用層</em>、DB 本身可能只佔 5-10ms。引用時不能把 vendor 廣告的 5-10ms p99 當「使用者體驗」 — 詳細拆解見 <a href="../partition-key-design/">partition-key-design</a> 的 latency budget 段。</p>
<h3 id="跟其他-vendor-capacity-抽象的對照">跟其他 vendor capacity 抽象的對照</h3>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Capacity 抽象</th>
          <th>思維重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB</td>
          <td>CPU + IOPS + working set RAM</td>
          <td>估資源、調 indexing</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>WCU / RCU + on-demand vs provisioned + adaptive</td>
          <td>mode 選擇 + PK 均勻度</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td>RU + 5 consistency level</td>
          <td>RU 預算、每 query 量 charge</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>instance class + replica count + storage IOPS</td>
          <td>provisioned</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>processing unit（100 pu 起跳）</td>
          <td>node count</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>range × replication factor × node count</td>
          <td>distributed</td>
      </tr>
  </tbody>
</table>
<p>對照表是本章合成 frame、case 庫沒有單一案例橫跨多 vendor。判讀時要明示「思維遷移成本是 selection 評估的隱性軸、不是只看 monthly bill」。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../partition-key-design/">partition-key-design</a>（partition skew 讓 RU 失效、hot partition 是 sizing 假設失敗的主因）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（Strong / Bounded 對 read RU 2x）、<a href="../multi-region-write-conflict/">multi-region-write-conflict</a>（multi-region RU × region 數）、<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a>（MongoDB API 翻譯層多 10-20% RU）</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>跟 9.x 章節：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（429 throttle 當 saturation 訊號）</li>
<li>Knowledge cards：<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/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li>Anti-recommendation：流量 &lt; 1000 RU/s 不需 autoscale tuning、用 serverless 或 400 RU/s shared throughput；過度 sizing 比 under-sizing 更常見、特別是 dev / staging</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 RU/s cost model backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday case</a> — 持續高峰 + RU budgeting 主案例</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — RU 抽象單位定義 + 1M RU/s 壓測（scope warning：壓測非持續）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase predictive scaling case</a> — 預測性 surge 模型借鑑（跨 vendor）</li>
<li><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/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/request-units">Cosmos DB Request Units</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/throughput-serverless">Provisioned throughput vs autoscale vs serverless</a></li>
</ul>
]]></content:encoded></item><item><title>9.C40 Netflix：380+ CockroachDB cluster 的 multi-active 拓樸艦隊</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/</guid><description>&lt;p>這個案例的核心責任是說明「Cassandra 撐不住 transactional 一致性」如何用 distributed SQL 補位。Netflix &lt;em>用 CockroachDB 補 Cassandra 缺的那塊&lt;/em>、全面替換從來不是策略：需要 rich transaction + global secondary index + multi-active 寫入的場景。跟 &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 consolidation&lt;/a> 對照 — Aurora 整合的是 OLTP single-region workload、CockroachDB 解的是「跨 region 強一致 + 跨 cluster 高彈性」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Netflix CockroachDB 艦隊的關鍵數字（引自 &lt;a href="https://www.cockroachlabs.com/customers/netflix/">Now Streaming: Why Netflix Runs a Fleet of 380+ CockroachDB Clusters&lt;/a> / &lt;a href="https://www.cockroachlabs.com/blog/netflix-at-cockroachdb/">The history of databases at Netflix&lt;/a>）：&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>總 cluster 數&lt;/td>
 &lt;td>380+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production cluster&lt;/td>
 &lt;td>160+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-region cluster&lt;/td>
 &lt;td>60+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最大單區 cluster&lt;/td>
 &lt;td>60 nodes / 26.5 TB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gaming 平台 cluster&lt;/td>
 &lt;td>48 nodes、跨 4 個 region&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>首個 prod cluster&lt;/td>
 &lt;td>2020 上線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production cluster&lt;/td>
 &lt;td>2022 已達 100、近年擴至 160+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署拓樸常態&lt;/td>
 &lt;td>多數 single-region、3 個 AZ&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：CockroachDB self-managed（Netflix Database Platform Team 自運維）、跨 AWS region、與 Cassandra / EVCache / RDS 並存（polyglot persistence）。&lt;/p>
&lt;p>關鍵 workload：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Studio Cloud Drive&lt;/strong>：影視製作資產的 file-system 風格服務、需要強一致 metadata + 全球可寫&lt;/li>
&lt;li>&lt;strong>Open Connect 控制平面&lt;/strong>：Netflix 自有 CDN、控制全球網路設備、需要跨 region 一致 control state&lt;/li>
&lt;li>&lt;strong>Spinnaker（持續交付平台）&lt;/strong>：deployment workflow state 需要 transactional 一致&lt;/li>
&lt;li>&lt;strong>Maestro（ML / 資料 workflow orchestration）&lt;/strong>：scheduling 與 state machine 不容許 eventual consistency&lt;/li>
&lt;li>&lt;strong>Gaming control plane&lt;/strong>：metadata 跨 4 region、region failure 不能 downtime&lt;/li>
&lt;/ul>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Netflix CockroachDB 艦隊揭露三個「補 Cassandra 缺口」的 OLTP 工程選擇。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Cassandra 不是 transactional 引擎、補位需求是工程現實&lt;/strong>：Netflix 2014 全面採用 Cassandra 解 global replication、但 &lt;em>lightweight transaction&lt;/em> 跟 unreliable secondary index 在 studio / control plane 等場景出問題。2019 評估後選 CockroachDB 是因為它同時滿足 multi-active topology、global consistent secondary index、global transaction、open source、SQL — 五個條件 Cassandra 在 transactional 場景下湊不齊。對應 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組&lt;/a> 的 polyglot persistence 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary&lt;/a>。&lt;/li>
&lt;li>&lt;strong>380+ cluster ≠ 「一個巨型 DB」&lt;/strong>：Netflix 是 &lt;em>artery of small DBs&lt;/em> 模型 — 每個微服務 / 應用配自己的 cluster、cluster sizing 從幾個 node 到 60 nodes 不等。容量規劃變成「每個 cluster 各自規劃」、不是「全公司一個容量曲線」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型&lt;/a> 跟 &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> 的「微服務私有 store」哲學。&lt;/li>
&lt;li>&lt;strong>Multi-region 是「region failure 0 downtime」、不是「更快」&lt;/strong>：Netflix 60+ multi-region cluster 主要動機是 region-level survival、不是降 latency（跨 region quorum 反而會增 latency）。Gaming cluster 48-node 跨 4 region 就是為了「region failover 不停服」、不是讓玩家延遲變低。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency vs availability 取捨。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「Cassandra 撐不住 transactional 一致性」如何用 distributed SQL 補位。Netflix <em>用 CockroachDB 補 Cassandra 缺的那塊</em>、全面替換從來不是策略：需要 rich transaction + global secondary index + multi-active 寫入的場景。跟 <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 consolidation</a> 對照 — Aurora 整合的是 OLTP single-region workload、CockroachDB 解的是「跨 region 強一致 + 跨 cluster 高彈性」。</p>
<h2 id="觀察">觀察</h2>
<p>Netflix CockroachDB 艦隊的關鍵數字（引自 <a href="https://www.cockroachlabs.com/customers/netflix/">Now Streaming: Why Netflix Runs a Fleet of 380+ CockroachDB Clusters</a> / <a href="https://www.cockroachlabs.com/blog/netflix-at-cockroachdb/">The history of databases at Netflix</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>總 cluster 數</td>
          <td>380+</td>
      </tr>
      <tr>
          <td>Production cluster</td>
          <td>160+</td>
      </tr>
      <tr>
          <td>Multi-region cluster</td>
          <td>60+</td>
      </tr>
      <tr>
          <td>最大單區 cluster</td>
          <td>60 nodes / 26.5 TB</td>
      </tr>
      <tr>
          <td>Gaming 平台 cluster</td>
          <td>48 nodes、跨 4 個 region</td>
      </tr>
      <tr>
          <td>首個 prod cluster</td>
          <td>2020 上線</td>
      </tr>
      <tr>
          <td>Production cluster</td>
          <td>2022 已達 100、近年擴至 160+</td>
      </tr>
      <tr>
          <td>部署拓樸常態</td>
          <td>多數 single-region、3 個 AZ</td>
      </tr>
  </tbody>
</table>
<p>服務組合：CockroachDB self-managed（Netflix Database Platform Team 自運維）、跨 AWS region、與 Cassandra / EVCache / RDS 並存（polyglot persistence）。</p>
<p>關鍵 workload：</p>
<ul>
<li><strong>Studio Cloud Drive</strong>：影視製作資產的 file-system 風格服務、需要強一致 metadata + 全球可寫</li>
<li><strong>Open Connect 控制平面</strong>：Netflix 自有 CDN、控制全球網路設備、需要跨 region 一致 control state</li>
<li><strong>Spinnaker（持續交付平台）</strong>：deployment workflow state 需要 transactional 一致</li>
<li><strong>Maestro（ML / 資料 workflow orchestration）</strong>：scheduling 與 state machine 不容許 eventual consistency</li>
<li><strong>Gaming control plane</strong>：metadata 跨 4 region、region failure 不能 downtime</li>
</ul>
<h2 id="判讀">判讀</h2>
<p>Netflix CockroachDB 艦隊揭露三個「補 Cassandra 缺口」的 OLTP 工程選擇。</p>
<ol>
<li><strong>Cassandra 不是 transactional 引擎、補位需求是工程現實</strong>：Netflix 2014 全面採用 Cassandra 解 global replication、但 <em>lightweight transaction</em> 跟 unreliable secondary index 在 studio / control plane 等場景出問題。2019 評估後選 CockroachDB 是因為它同時滿足 multi-active topology、global consistent secondary index、global transaction、open source、SQL — 五個條件 Cassandra 在 transactional 場景下湊不齊。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 polyglot persistence 與 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a>。</li>
<li><strong>380+ cluster ≠ 「一個巨型 DB」</strong>：Netflix 是 <em>artery of small DBs</em> 模型 — 每個微服務 / 應用配自己的 cluster、cluster sizing 從幾個 node 到 60 nodes 不等。容量規劃變成「每個 cluster 各自規劃」、不是「全公司一個容量曲線」。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 跟 <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> 的「微服務私有 store」哲學。</li>
<li><strong>Multi-region 是「region failure 0 downtime」、不是「更快」</strong>：Netflix 60+ multi-region cluster 主要動機是 region-level survival、不是降 latency（跨 region quorum 反而會增 latency）。Gaming cluster 48-node 跨 4 region 就是為了「region failover 不停服」、不是讓玩家延遲變低。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency vs availability 取捨。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>case study 沒揭露單一 cluster QPS / latency 具體數字、只揭露 <em>艦隊規模</em> 跟 <em>最大 cluster 容量</em>。讀案例時不要把「380 cluster」直接換算成「Netflix CockroachDB QPS 上限」。</li>
<li>Netflix 是 <em>self-managed</em>、不是 Cockroach Cloud — 需要專屬 Database Platform Team 養 380+ cluster。沒這量級團隊的組織直接 self-host 380 cluster 是 ops 自殺、Cockroach Cloud 才是合理路徑。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>不要試圖一個 DB 撐全部</strong>：Netflix 同時用 Cassandra（高吞吐 eventual）、CockroachDB（transactional + global）、Aurora（單區 ACID）、EVCache（cache）。每種 DB 對應不同 workload 類型、不混用。對應 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 的 polyglot persistence。</li>
<li><strong>每個 cluster 對應一個 application boundary</strong>：避免 multi-tenant 大 cluster、改用「per-app cluster」— 容量規劃顆粒對齊 application、爆掉時 blast radius 限縮在單一 app。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 的 blast radius 設計。</li>
<li><strong>Multi-region 用於 survival、不是 latency 優化</strong>：跨 region quorum 物理上必然增 latency。把 multi-region 動機釐清成 <em>region failure 容忍</em>、不要混淆「跨 region = 更快」。對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 survival goal vs latency budget 取捨。</li>
<li><strong>Self-managed 規模化需要專屬平台團隊</strong>：Netflix 有 Database Platform Team 養 380+ cluster — 包含 backup、upgrade、incident response、capacity review。沒這量級團隊就走 managed service。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的人力成本權衡。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>Spanner（GCP）解同類「global transaction + secondary index」、GCP-only</li>
<li>DynamoDB Global Tables 走 eventual consistency、不是 Netflix 想要的 strong consistency</li>
<li>Yugabyte / TiDB 是 distributed SQL 對等候選、生態深度與 PostgreSQL wire 相容度有差</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解 polyglot persistence 選型 → <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> + <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></li>
<li>想規劃 multi-region survival goal → <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> + <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></li>
<li>對照其他 distributed SQL 案例 → <a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> / <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> / <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想理解 transaction vs eventual consistency 邊界 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">01.5 transaction boundary</a></li>
<li>想深入 CockroachDB survival goal 與 region failure 取捨 → <a href="/blog/backend/01-database/vendors/cockroachdb/survival-goals/" data-link-title="CockroachDB Survival Goals：zone 級 vs region 級配置與業務 SLO 倒推流程" data-link-desc="CockroachDB 用 SURVIVE ZONE FAILURE / SURVIVE REGION FAILURE 兩種 survival goal 宣告式控制 Raft replica 分佈、決定 RTO / RPO。本文走 Hard Rock Digital bet placement RPO=0 倒推流程、Netflix Gaming 48-node 跨 4 region 「為求 survival 而非 latency」的反直覺判讀、配置語法、寫入 latency 暴漲跟 cost 暴漲兩條失敗模式、合規邊界對比">CockroachDB survival goals</a></li>
<li>想規劃跨 region schema 與資料本地化 → <a href="/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/" data-link-title="CockroachDB Locality-Aware Schema：跨州合規 &#43; 邏輯一個 cluster 的 region placement 策略" data-link-desc="Hard Rock Digital 跨 8 州 sportsbook、用 AWS Outposts &#43; region placement 把運算釘在州內、邏輯上仍是一個 CockroachDB cluster。本文走 REGIONAL BY ROW / REGIONAL BY TABLE / GLOBAL 三種 locality、Hard Rock 拓樸創新對比 Standard Chartered Aurora 7 cluster fleet、AWS Outposts 是合規工具不是 latency 工具的反直覺判讀">CockroachDB locality-aware schema</a></li>
<li>想對比 Aurora DSQL / Spanner / CockroachDB → <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://assets.ctfassets.net/00voh0j35590/7qBPsA0FKKTuAK4JhK27uu/1b30b2015f32878874bd0873a2a54361/CockroachLabs-NETFLIX-Case-Study.pdf">Now Streaming: Why Netflix Runs a Fleet of 380+ CockroachDB Clusters（PDF）</a></li>
<li><a href="https://www.cockroachlabs.com/customers/netflix/">Now Streaming: Why Netflix Runs a Fleet of 380+ CockroachDB Clusters（cockroachlabs.com Netflix customer page）</a></li>
<li><a href="https://www.cockroachlabs.com/blog/netflix-at-cockroachdb/">The history of databases at Netflix: From Cassandra to CockroachDB</a></li>
<li><a href="https://www.cockroachlabs.com/blog/netflix-dbaas-roachfest24-recap/">A Netflix RoachFest24 Original: The Case for Multi-Region Clusters</a></li>
<li><a href="https://www.cockroachlabs.com/blog/persistence-as-a-service-at-netflix/">How Netflix engineers choose their tech stack</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/major-version-upgrade/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/major-version-upgrade/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。寫作前判讀 &lt;em>不適用&lt;/em> &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> 的 5 type — 本文是該 methodology 「何時不該套」段的第 2 項實證（同 vendor major version upgrade）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼這篇不套-5-type-migration">為什麼這篇不套 5 type migration&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 對 PostgreSQL 14 → 17：&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>Schema / API&lt;/td>
 &lt;td>同 PostgreSQL wire protocol、SQL syntax 99%+ 相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>同 PostgreSQL operational stack、tooling 不變&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>同 OLTP RDBMS&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>同 1 個&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>多數 application 不改&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>5 維皆 Low — 對映 Type B drop-in。但 &lt;em>實際工作量&lt;/em> 跟 drop-in 完全不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Extension 相容性&lt;/strong>：pg14 的 extension 不一定能在 pg17 直接用（API 變動 / ABI break）&lt;/li>
&lt;li>&lt;strong>Breaking change&lt;/strong>：每個 major version 有 release-specific behavior change（pg17 移除 &lt;code>relation&lt;/code>/&lt;code>oid&lt;/code> 隱性 type、pg15 公開 &lt;code>pg_role&lt;/code> 規則變嚴）&lt;/li>
&lt;li>&lt;strong>Storage format&lt;/strong>：major version 之間 &lt;em>data dir 不向後相容&lt;/em>、必須 &lt;code>pg_upgrade&lt;/code> 或 dump-restore&lt;/li>
&lt;li>&lt;strong>Statistics 重建&lt;/strong>：upgrade 後 &lt;code>pg_statistic&lt;/code> 失效、必須跑 &lt;code>ANALYZE&lt;/code>、否則 query plan 退化&lt;/li>
&lt;li>&lt;strong>Replication slot&lt;/strong>：logical replication slot 不跨 major version&lt;/li>
&lt;/ul>
&lt;p>5 type 對映 &lt;em>跨 vendor process&lt;/em>、漏了 &lt;em>同 vendor 內升級&lt;/em> 的 upgrade-specific dimension。本文採用 &lt;em>deep article methodology 的 6-section + 額外 upgrade audit 段&lt;/em> 結構、不是 5 type 的任一個。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。寫作前判讀 <em>不適用</em> <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> 的 5 type — 本文是該 methodology 「何時不該套」段的第 2 項實證（同 vendor major version upgrade）。</p></blockquote>
<h2 id="為什麼這篇不套-5-type-migration">為什麼這篇不套 5 type migration</h2>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 對 PostgreSQL 14 → 17：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 PostgreSQL wire protocol、SQL syntax 99%+ 相容</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 PostgreSQL operational stack、tooling 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 OLTP RDBMS</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>多數 application 不改</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>5 維皆 Low — 對映 Type B drop-in。但 <em>實際工作量</em> 跟 drop-in 完全不同：</p>
<ul>
<li><strong>Extension 相容性</strong>：pg14 的 extension 不一定能在 pg17 直接用（API 變動 / ABI break）</li>
<li><strong>Breaking change</strong>：每個 major version 有 release-specific behavior change（pg17 移除 <code>relation</code>/<code>oid</code> 隱性 type、pg15 公開 <code>pg_role</code> 規則變嚴）</li>
<li><strong>Storage format</strong>：major version 之間 <em>data dir 不向後相容</em>、必須 <code>pg_upgrade</code> 或 dump-restore</li>
<li><strong>Statistics 重建</strong>：upgrade 後 <code>pg_statistic</code> 失效、必須跑 <code>ANALYZE</code>、否則 query plan 退化</li>
<li><strong>Replication slot</strong>：logical replication slot 不跨 major version</li>
</ul>
<p>5 type 對映 <em>跨 vendor process</em>、漏了 <em>同 vendor 內升級</em> 的 upgrade-specific dimension。本文採用 <em>deep article methodology 的 6-section + 額外 upgrade audit 段</em> 結構、不是 5 type 的任一個。</p>
<h2 id="結構-differentiatordeep-article--upgrade-audit">結構 differentiator：deep article + upgrade audit</h2>
<p>跟 single feature deep article（如 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer config</a> / <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a>）對照、本文多一段 <em>upgrade audit</em>；跟 migration playbook 對照、本文 <em>沒 phased translation / parallel run / cutover routing</em>：</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">問題情境（為什麼升）
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ Upgrade audit（extension / breaking change / dependency）
</span></span><span class="line"><span class="ln">3</span><span class="cl">→ 升級方法選擇（pg_upgrade / logical / blue-green）
</span></span><span class="line"><span class="ln">4</span><span class="cl">→ Step-by-step 執行
</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">→ Capacity / downtime trade-off
</span></span><span class="line"><span class="ln">7</span><span class="cl">→ 整合 / 下一步</span></span></code></pre></div><p>7 段、220-280 行。比 single feature deep article 多 1 段 audit、比 migration playbook 少 phased translation 章節。</p>
<h2 id="問題情境major-version-不只是-minor-bump">問題情境：major version 不只是 minor bump</h2>
<p>PostgreSQL major version（14 / 15 / 16 / 17）一年一版、每版含 <em>breaking change</em>、不是 minor bump。常見升級驅動：</p>
<ul>
<li><strong>EOL pressure</strong>：PostgreSQL 每版 maintained 5 年、pg14 EOL 2026-11；pg13 EOL 2025-11 已過、production 仍跑 pg13 是 risk</li>
<li><strong>新 feature 需求</strong>：pg15 MERGE / pg16 parallel hash join / pg17 incremental backup</li>
<li><strong>Cloud provider 強制</strong>：Aurora / RDS 對 EOL 版本停 minor patch、planned upgrade 不能拖</li>
</ul>
<p>不升級的代價：security patch 停發、新功能不能用、跟新 client / extension 漸增不相容。</p>
<h2 id="upgrade-audit">Upgrade audit</h2>
<p>升級前的硬閘門 audit、跳過任一個 production 必踩：</p>
<h3 id="audit-1extension-相容性">Audit 1：Extension 相容性</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">extname</span><span class="p">,</span><span class="w"> </span><span class="n">extversion</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_extension</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">extname</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s1">&#39;plpgsql&#39;</span><span class="p">;</span></span></span></code></pre></div><p>對每個 extension 跑：</p>
<ol>
<li>對應 target version (pg17) 是否有 release？</li>
<li>ABI break？（如 PostGIS major version 對應 PG major version）</li>
<li>是否有 maintainer 持續更新？（TimescaleDB 已不 cover pg17 部分 feature）</li>
</ol>
<p>常見 pg14 → pg17 需要 <em>先升 extension</em> 的：PostGIS / TimescaleDB / pgaudit / pg_partman / pg_repack。</p>
<h3 id="audit-2breaking-change-pull">Audit 2：Breaking change pull</h3>





<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"># 查 release note 累積 breaking change（pg14 → pg17 跨 3 個 major）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># pg15: deprecated public schema 預設 write 權限變嚴</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># pg16: regrole removed implicit casts</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># pg17: removed several deprecated columns from system catalogs</span></span></span></code></pre></div><p>對每個 breaking change：</p>
<ol>
<li>用 SQL grep / static analysis 找 application code 影響範圍</li>
<li>評估修改工作量（通常 50-95% 是 false alarm、5-10% 真實影響）</li>
<li>列出無法立刻修的、規劃 <em>逐 major 升</em> 而不是 <em>一次升 3 major</em></li>
</ol>
<h3 id="audit-3replication--logical-slot">Audit 3：Replication / logical slot</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">slot_name</span><span class="p">,</span><span class="w"> </span><span class="n">plugin</span><span class="p">,</span><span class="w"> </span><span class="n">slot_type</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_replication_slots</span><span class="p">;</span></span></span></code></pre></div><p>major version upgrade 後：</p>
<ul>
<li><strong>Physical replication slot</strong>：standby 必須先升級到 <em>相同 major version</em> 才能跟新 primary</li>
<li><strong>Logical replication slot</strong>：<strong>不跨 major version</strong>、必須在 upgrade 前 drop、之後重建（消費者重 init load）</li>
<li>對應 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Debezium CDC</a> consumer 必須重 init</li>
</ul>
<h3 id="audit-4config-參數變更">Audit 4：Config 參數變更</h3>





<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"># diff postgresql.conf default 14 vs 17</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 重點: shared_preload_libraries / autovacuum_* / wal_level / synchronous_commit</span></span></span></code></pre></div><p>新 major version 預設值常變（pg14 → 17：<code>max_worker_processes</code> 預設變 / <code>unix_socket_directories</code> 行為差異）；自定 config 需逐項 review。</p>
<h3 id="audit-5statistics-重建計畫">Audit 5：Statistics 重建計畫</h3>
<p><code>pg_upgrade</code> 後 <code>pg_statistic</code> 重置、第一次跑 query plan 用空 stats、production 性能會塌；upgrade 計畫必須含：</p>
<ul>
<li><code>ANALYZE</code> 跑全 DB（小 DB ~10 分鐘、大 DB 1-3 小時）</li>
<li>多 stage <code>vacuumdb --analyze-in-stages</code> 先快速跑 baseline、再跑 full</li>
<li>Maintenance window 內預留 statistics 重建時間</li>
</ul>
<h2 id="升級方法選擇">升級方法選擇</h2>
<p>三種主流方法、依 downtime 容忍跟 DB 大小：</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>Downtime</th>
          <th>風險</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pg_upgrade --link</code></td>
          <td>10-30 分鐘</td>
          <td>data dir 跟 OS package 同 host、回退複雜</td>
          <td>&lt; 500GB、可接受 30 分鐘 downtime</td>
      </tr>
      <tr>
          <td>Logical replication</td>
          <td>切換瞬間（&lt; 1 分鐘）</td>
          <td>設定複雜、long-running migration window</td>
          <td>TB 級、低 downtime 需求</td>
      </tr>
      <tr>
          <td>Blue-green deployment</td>
          <td>切換瞬間</td>
          <td>雙倍硬體、cutover 期間需嚴格 traffic shifting</td>
          <td>Cloud-managed（Aurora / RDS 內建）</td>
      </tr>
  </tbody>
</table>
<h3 id="pg_upgrade---link-流程"><code>pg_upgrade --link</code> 流程</h3>





<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. install pg17 binary（不啟動）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 2. stop pg14</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo systemctl stop postgresql@14
</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="c1"># 3. 跑 pg_upgrade（hard link、不複製資料）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sudo -u postgres /usr/lib/postgresql/17/bin/pg_upgrade <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --old-bindir<span class="o">=</span>/usr/lib/postgresql/14/bin <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --new-bindir<span class="o">=</span>/usr/lib/postgresql/17/bin <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --old-datadir<span class="o">=</span>/var/lib/postgresql/14/main <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --new-datadir<span class="o">=</span>/var/lib/postgresql/17/main <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --link <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --jobs<span class="o">=</span><span class="m">8</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 4. 啟動 pg17</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">sudo systemctl start postgresql@17
</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 class="c1"># 5. 跑 pg_upgrade 產出的 analyze script</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">sudo -u postgres /tmp/analyze_new_cluster.sh</span></span></code></pre></div><p><code>--link</code> 用 hard link、不複製 data dir、適合大 DB；缺點是 <em>回退到 pg14 不可能</em>（data dir 已被新 pg 修改）— 必須有完整 backup + tested restore。</p>
<h2 id="故障演練">故障演練</h2>
<h3 id="case-1extension-相容性沒先-auditupgrade-後啟動失敗">Case 1：Extension 相容性沒先 audit、upgrade 後啟動失敗</h3>
<p><strong>徵兆</strong>：pg_upgrade 跑完、<code>pg_ctl start</code> 失敗、log 顯示 <code>could not load library &quot;timescaledb-2.13.so&quot;</code>。</p>
<p><strong>根因</strong>：TimescaleDB 對應 pg14、pg17 需要 TimescaleDB 2.16+；pg_upgrade 階段沒 check、library path 找不到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-upgrade audit</strong>：每個 extension 列出 target version 對應、預先升 extension（在 pg14 上跑、用 <code>ALTER EXTENSION ... UPDATE</code>）</li>
<li><strong>回退</strong>：data dir 用 <code>--link</code> 已不可逆、必須從 backup restore + 重試</li>
<li><strong>預防</strong>：staging 環境完整 dry-run、production upgrade 前已知 path 都驗證過</li>
</ol>
<h3 id="case-2application-用-deprecated-sql跑壞">Case 2：Application 用 deprecated SQL、跑壞</h3>
<p><strong>徵兆</strong>：upgrade 後某些 application query 直接 error <code>ERROR: type &quot;regtype&quot; does not have a cast</code>。</p>
<p><strong>根因</strong>：pg16 移除了某些隱性 cast、application code 用了 implicit cast、現在 explicit cast 才能跑。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-upgrade</strong>：跑 application test suite 對 pg17 staging、catch 不相容 query</li>
<li><strong>緊急</strong>：staging 找到的 query 在 production 改 application code、deploy 後再 upgrade DB</li>
<li><strong>長期</strong>：application code 用 ORM / query builder、避免 raw SQL 對 PG version-specific behavior 依賴</li>
</ol>
<h3 id="case-3analyze-沒跑production-query-性能崩">Case 3：<code>ANALYZE</code> 沒跑、production query 性能崩</h3>
<p><strong>徵兆</strong>：upgrade 後 5 分鐘、application latency p99 從 50ms 衝到 5000ms；query plan 從 index scan 退化到 seq scan。</p>
<p><strong>根因</strong>：<code>pg_upgrade</code> 重置 <code>pg_statistic</code>、planner 用空 stats 跑 plan、無法估 selectivity、保守選 seq scan。</p>
<p><strong>修法</strong>：</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"># upgrade 完立刻跑 (順序)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vacuumdb --all --analyze-in-stages --jobs<span class="o">=</span><span class="m">4</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Stage 1: 最少 stats（快、~5 分鐘）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Stage 2: 中 stats（~30 分鐘）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># Stage 3: 完整 stats（1-3 小時）</span></span></span></code></pre></div><p><code>--analyze-in-stages</code> 分 3 階段、第 1 階段就能讓 planner 做大致正確的決策；可在 maintenance window 內接受 stage 3 仍在跑。</p>
<h3 id="case-4logical-replication-slot-漏-dropdebezium-卡死">Case 4：Logical replication slot 漏 drop、Debezium 卡死</h3>
<p><strong>徵兆</strong>：upgrade 完開機後、Debezium connector log 顯示 <code>slot not found</code>、消費停滯；Kafka downstream 訊息斷流。</p>
<p><strong>根因</strong>：logical replication slot 不跨 major version、<code>pg_upgrade</code> 不自動處理 logical slot；upgrade 前沒 drop、新 cluster 上 slot 不存在。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-upgrade</strong>：列所有 logical replication slot、Debezium 暫停 consumer + drop slot</li>
<li><strong>Upgrade 後重建</strong>：用新 LSN starting position 建 slot、Debezium snapshot.mode=schema_only_recovery 取代 initial（避免重 init load）</li>
<li><strong>架構</strong>：未來考慮用 <em>outbox pattern</em>、CDC 只追 outbox 表、降低 logical slot 重建成本</li>
</ol>
<h3 id="case-5standby-沒同步升replication-斷">Case 5：Standby 沒同步升、replication 斷</h3>
<p><strong>徵兆</strong>：primary 升 pg17 後、standby 仍 pg14、replication 不通；<code>pg_stat_replication</code> 沒 standby connection。</p>
<p><strong>根因</strong>：streaming replication 不跨 major version；standby 必須 <em>先升</em> 或 <em>upgrade 後重 base backup</em>。</p>
<p><strong>修法</strong>：</p>
<p>兩種策略：</p>
<ol>
<li><strong>In-place upgrade standby</strong>：standby 也跑 <code>pg_upgrade</code>、但要先 stop streaming、升完重接（standby 端 archive_command + restore_command 對齊）</li>
<li><strong>Rebuild standby</strong>：upgrade primary 完、standby 跑 <code>pg_basebackup</code> 重建（適合 standby 容量小、network 快）</li>
</ol>
<p>Patroni HA 環境：用 <em>rolling upgrade</em> — 先升 sync standby、failover 過去、再升舊 primary 變新 standby。複雜度高、需要 staging 演練。</p>
<h2 id="capacity--downtime-trade-off">Capacity / downtime trade-off</h2>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>Downtime 估算（500GB DB）</th>
          <th>硬體成本</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pg_upgrade --link</code></td>
          <td>15-30 分鐘（含 ANALYZE 1st stage）</td>
          <td>同當前</td>
          <td>高（不可逆）</td>
      </tr>
      <tr>
          <td><code>pg_upgrade --clone</code></td>
          <td>1-3 小時</td>
          <td>暫時 2x storage</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Logical replication</td>
          <td>&lt; 1 分鐘 cutover</td>
          <td>暫時 2x compute + storage</td>
          <td>中（複雜）</td>
      </tr>
      <tr>
          <td>Blue-green</td>
          <td>切換瞬間（&lt; 30 秒）</td>
          <td>持續 2x（cutover 後可拆）</td>
          <td>低（cloud managed）</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>&lt; 100GB、可接受 30 分鐘 downtime：<code>pg_upgrade --link</code></li>
<li>100GB - 1TB、要求 &lt; 5 分鐘 downtime：logical replication（標準 PostgreSQL）</li>
<li>1TB+ 或 SLA 嚴格：blue-green via Aurora / RDS（cloud managed）</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-patroni-ha-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> 整合</h3>
<p>HA cluster upgrade 流程：</p>
<ol>
<li>升新 standby（不在 cluster 中、physical / logical replicate 過去）</li>
<li>Promote 新 standby、舊 cluster failover 過去</li>
<li>重建剩餘 standby</li>
</ol>
<p>Patroni 17+ 支援 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">logical slot 跨 failover</a> — major version upgrade 期間 logical consumer 影響降低。</p>
<h3 id="跟-monitoring-整合">跟 monitoring 整合</h3>
<p>upgrade 期間特別關注的 metric：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Pre-upgrade baseline
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_database_size</span><span class="p">(</span><span class="s1">&#39;myapp&#39;</span><span class="p">),</span><span class="w"> </span><span class="k">version</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Post-upgrade verification
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_database_size</span><span class="p">(</span><span class="s1">&#39;myapp&#39;</span><span class="p">),</span><span class="w"> </span><span class="k">version</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">last_analyze</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 應該 = 0、若有未 analyze 表、ANALYZE 沒跑完</span></span></span></code></pre></div><p>Prometheus alert 三條：<code>pg_database_size</code> upgrade 後差異 &lt; 1%、<code>pg_stat_replication</code> lag &lt; 10s、<code>pg_query_p99_latency</code> 對 baseline &lt; 1.5x。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Aurora major version upgrade</strong>：blue-green deployment 是 default、流程跟 self-managed 完全不同、見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora migration</a> 對位段</li>
<li><strong>Cross-major version skip upgrade</strong>：pg13 → pg17 跨 4 major、breaking change 累積、建議 <em>逐 major 升</em> 而不是 <em>single hop</em></li>
<li><strong>Extension lifecycle 管理</strong>：自動 audit extension 跟 PG version compatibility、每 quarter 跑 dry-run</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a> / <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a></li>
<li>對位 migration：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a> / <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 <em>漏類</em>）</li>
</ul>
]]></content:encoded></item><item><title>3.C40 Resgate：WebSocket-to-NATS realtime API gateway</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-resgate-realtime-api-gateway/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-resgate-realtime-api-gateway/</guid><description>&lt;p>這個案例的核心責任是說明「subject hierarchy 即 access control 邊界」的設計範例。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Resgate 把 NATS subject 暴露成 REST + WebSocket、客戶端跨多 Resgate 實例自動同步狀態、事件延遲 &amp;lt; 1ms。需要同時支援 pub-sub 跟 request-reply、選 NATS 因為「performance、simplicity、兩種模式都原生支援」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>subject 設計遵循 &lt;code>get.{service}.{resource}&lt;/code> / &lt;code>event.{service}.{resource}.{event-type}&lt;/code> 的命名規約、是「subject 階層當 schema」的典型範例。揭露 subject 命名是 NATS 的 API contract 起點、不是隨意命名。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：Request/Reply pattern / Subject-based ACL + 多租戶（subject hierarchy 即 access control 邊界）/ Core NATS vs JetStream（純 Core）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &amp;#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26 GoCardless Hutch routing key&lt;/a>（命名規約對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://resgate.io/blog/introducing-resgate/">Introducing Resgate&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「subject hierarchy 即 access control 邊界」的設計範例。</p>
<h2 id="觀察">觀察</h2>
<p>Resgate 把 NATS subject 暴露成 REST + WebSocket、客戶端跨多 Resgate 實例自動同步狀態、事件延遲 &lt; 1ms。需要同時支援 pub-sub 跟 request-reply、選 NATS 因為「performance、simplicity、兩種模式都原生支援」。</p>
<h2 id="判讀">判讀</h2>
<p>subject 設計遵循 <code>get.{service}.{resource}</code> / <code>event.{service}.{resource}.{event-type}</code> 的命名規約、是「subject 階層當 schema」的典型範例。揭露 subject 命名是 NATS 的 API contract 起點、不是隨意命名。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：Request/Reply pattern / Subject-based ACL + 多租戶（subject hierarchy 即 access control 邊界）/ Core NATS vs JetStream（純 Core）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26 GoCardless Hutch routing key</a>（命名規約對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://resgate.io/blog/introducing-resgate/">Introducing Resgate</a></li>
</ul>
]]></content:encoded></item><item><title>9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 + 跨州單一邏輯 DB</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/</guid><description>&lt;p>這個案例的核心責任是說明「合規強制資料留地理邊界 + 想要單一邏輯 DB」如何用 distributed SQL + 邊緣硬體解。跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 對比 — Standard Chartered 走「Aurora 多 region、each region 一個 cluster」、Hard Rock Digital 走「跨 AWS Outposts + AWS region 一個邏輯 cluster」。兩條都解受監管金融類業務、結構差異反映法規顆粒不同：銀行是國家層級、美國運動博彩是 &lt;em>州&lt;/em> 層級。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Hard Rock Digital sportsbook 部署的關鍵數字（引自 &lt;a href="https://www.cockroachlabs.com/customers/hard-rock-digital/">Hard Rock Digital customer page&lt;/a> / &lt;a href="https://www.cockroachlabs.com/blog/highly-available-sports-betting-app/">How Hard Rock Digital built a highly available and compliant sports betting app&lt;/a>）：&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>營運州數&lt;/td>
 &lt;td>8（AZ / IN / TN / FL / OH / IL / NJ / VA）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高峰節點數&lt;/td>
 &lt;td>~100 nodes、each 32 vCPU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>淡季節點數&lt;/td>
 &lt;td>scales down ~33 nodes（約 1/3）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>基礎設施組合&lt;/td>
 &lt;td>AWS Regions + AWS Local Zones + AWS Outposts（按州合規要求布局）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料庫拓樸&lt;/td>
 &lt;td>跨所有 region 一個 logical database&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Survival goal&lt;/td>
 &lt;td>單一 Outpost 或 AWS AZ 失敗不丟資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>顯著測試失敗事件&lt;/td>
 &lt;td>node crash / EC2 instance fail / single state loss — 對使用者無感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重大事件流量&lt;/td>
 &lt;td>Super Bowl / World Cup 等高峰、無效能退化紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Engineering 團隊&lt;/td>
 &lt;td>tech team ~50 人；若用 PostgreSQL 估計需多加 10-20 工程師&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務組合：CockroachDB self-managed、AWS US-East-1（共用 control plane）、AWS Outposts（部分州合規要求設備位於州內）、AWS Local Zones（特定都會區延遲補強）。&lt;/p>
&lt;p>關鍵 workload：bet placement、bet settlement、account management、cache loading、sports metadata import。&lt;/p>
&lt;p>關鍵負載形狀：sports betting 是 &lt;em>event-driven peak&lt;/em> — Super Bowl / World Cup 等賽事是已知時間點、流量在開賽前 30-60 分鐘飆升、賽中持續高水位、賽後 settlement 集中爆發。「100 → 33 → 100」的 scale up / down 反映賽季 vs 淡季的容量需求差。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Hard Rock Digital 的工程選擇揭露三個受監管 OLTP 的設計重點。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「合規強制資料留地理邊界 + 想要單一邏輯 DB」如何用 distributed SQL + 邊緣硬體解。跟 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 對比 — Standard Chartered 走「Aurora 多 region、each region 一個 cluster」、Hard Rock Digital 走「跨 AWS Outposts + AWS region 一個邏輯 cluster」。兩條都解受監管金融類業務、結構差異反映法規顆粒不同：銀行是國家層級、美國運動博彩是 <em>州</em> 層級。</p>
<h2 id="觀察">觀察</h2>
<p>Hard Rock Digital sportsbook 部署的關鍵數字（引自 <a href="https://www.cockroachlabs.com/customers/hard-rock-digital/">Hard Rock Digital customer page</a> / <a href="https://www.cockroachlabs.com/blog/highly-available-sports-betting-app/">How Hard Rock Digital built a highly available and compliant sports betting app</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>營運州數</td>
          <td>8（AZ / IN / TN / FL / OH / IL / NJ / VA）</td>
      </tr>
      <tr>
          <td>高峰節點數</td>
          <td>~100 nodes、each 32 vCPU</td>
      </tr>
      <tr>
          <td>淡季節點數</td>
          <td>scales down ~33 nodes（約 1/3）</td>
      </tr>
      <tr>
          <td>基礎設施組合</td>
          <td>AWS Regions + AWS Local Zones + AWS Outposts（按州合規要求布局）</td>
      </tr>
      <tr>
          <td>資料庫拓樸</td>
          <td>跨所有 region 一個 logical database</td>
      </tr>
      <tr>
          <td>Survival goal</td>
          <td>單一 Outpost 或 AWS AZ 失敗不丟資料</td>
      </tr>
      <tr>
          <td>顯著測試失敗事件</td>
          <td>node crash / EC2 instance fail / single state loss — 對使用者無感</td>
      </tr>
      <tr>
          <td>重大事件流量</td>
          <td>Super Bowl / World Cup 等高峰、無效能退化紀錄</td>
      </tr>
      <tr>
          <td>Engineering 團隊</td>
          <td>tech team ~50 人；若用 PostgreSQL 估計需多加 10-20 工程師</td>
      </tr>
  </tbody>
</table>
<p>服務組合：CockroachDB self-managed、AWS US-East-1（共用 control plane）、AWS Outposts（部分州合規要求設備位於州內）、AWS Local Zones（特定都會區延遲補強）。</p>
<p>關鍵 workload：bet placement、bet settlement、account management、cache loading、sports metadata import。</p>
<p>關鍵負載形狀：sports betting 是 <em>event-driven peak</em> — Super Bowl / World Cup 等賽事是已知時間點、流量在開賽前 30-60 分鐘飆升、賽中持續高水位、賽後 settlement 集中爆發。「100 → 33 → 100」的 scale up / down 反映賽季 vs 淡季的容量需求差。</p>
<h2 id="判讀">判讀</h2>
<p>Hard Rock Digital 的工程選擇揭露三個受監管 OLTP 的設計重點。</p>
<ol>
<li><strong>法規顆粒決定基礎設施拓樸、不是反過來</strong>：美國 Wire Act 要求 <em>betting data 必須在下注州內處理</em>、所以每個營運州都要有州內運算資源。傳統路徑是「每州一個獨立 silo」— 但 silo 之間的玩家統一帳戶、跨州 reporting、欺詐偵測會撞牆。Hard Rock Digital 用 AWS Outposts 把運算放進州內、但邏輯上仍是 <em>一個</em> CockroachDB cluster — region placement 配置決定哪些 range 釘在哪個 Outpost、合規與單一邏輯 DB 同時成立。對應 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a> 的合規 boundary 設計與 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 region placement。</li>
<li><strong>Survival goal 「Outpost 或 AZ 失敗不丟」對應業務 SLO</strong>：sports betting 中 <em>bet placement</em> 不能 lose — 玩家下注後系統 crash 沒紀錄、對博彩牌照是合規事故。CockroachDB Raft 3-replica + 跨 AZ 配置讓 Outpost 失敗時其他 replica 還在、自動 failover。對應 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability</a> 的 RPO=0 設計與 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a> 的 Survival Goals。</li>
<li><strong>Scale up / down 是賽季常態、不是異常事件</strong>：100 → 33 → 100 的擺盪在 sportsbook 業務是 <em>年度循環</em> — NFL 季結束 / NBA 季初切換、流量結構性下降。CockroachDB 加減節點靠 range rebalance、不停服。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 的 seasonality 與 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 的 event-driven scaling。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>case study 沒揭露 QPS、p99 latency 具體數字。100 node × 32 vCPU 是硬體規模、不是 throughput。讀案例時要區分 <em>容量 sizing</em>（節點數）跟 <em>workload throughput</em>（每秒處理量）。</li>
<li>「省了 10-20 工程師」是 <em>估計差距</em>、不是已 hire 後解雇。對應的是「沒選 PostgreSQL 所以沒招那麼多 DBA」、是機會成本不是節省支出。</li>
<li>Wire Act 是 <em>美國聯邦法</em>、各州還有獨立法規（NJ DGE、NV NGC 等）。Hard Rock Digital 模型適合 <em>跨州</em> 合規、不是 <em>跨國</em> — 跨國牌照差異更大、不能直接套。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>合規 boundary 用 region placement 表達、不是 cluster fragmentation</strong>：當法規要求資料留某地理邊界、優先看 distributed SQL 的 region placement / pin-to-region 能力、不要直接開獨立 cluster。獨立 cluster 解了合規但破壞了業務邏輯（跨州統一帳戶、欺詐偵測、reporting）。對應 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a> 的 multi-region table 與 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 的 placement。</li>
<li><strong>邊緣硬體（AWS Outposts / Local Zones）是合規工具、不是 latency 工具</strong>：Outposts 主要為「資料留某地理邊界」而存在、latency 改善是副作用。決策時先看合規驅動力、latency 改善列為 bonus。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的 hybrid cloud 設計。</li>
<li><strong>賽季型擴縮容寫進 baseline 容量模型</strong>：Hard Rock Digital 100 ↔ 33 的擺盪不是「臨時 scale up」、是計畫內年度循環。容量規劃要直接把 NFL / NBA / 國際賽事曆塞進預測模型、不要當 surprise。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 與 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech 體育博彩 AI 預測</a>。</li>
<li><strong>distributed SQL 的 ops 槓桿：team 小、cluster 大</strong>：Hard Rock Digital 50 人 tech team 養全部運維、估省了 10-20 個 DBA。distributed SQL 把「DBA 養單區、跨區 sync 養運維」的工作量壓進 <em>系統內建</em> 的 Raft / placement、人月支出降。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的人力成本工程化。</li>
</ol>
<p>跨平台等效：</p>
<ul>
<li>Spanner（GCP）也支援 region placement、但 GCP-only、無 Outposts 等效</li>
<li>Aurora DSQL（AWS 2024）支援跨 region 強一致、但 Outpost 部署現階段未完整覆蓋</li>
<li>自管 PostgreSQL + application 層 sharding：理論可行、operation burden 跟人力需求大幅上升、Hard Rock Digital 評估後選 CockroachDB 的主因之一</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他受監管金融 / 博彩 OLTP → <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>（銀行國家層級）/ <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>（fantasy sports）</li>
<li>對照 event-driven peak 設計 → <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> / <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></li>
<li>想規劃 multi-region OLTP survival goal → <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> + <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a></li>
<li>對照其他 distributed SQL 案例 → <a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> / <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> / <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></li>
<li>想理解合規驅動的拓樸設計 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> + <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">01.4 database migration playbook</a></li>
<li>想拆 CockroachDB survival goal 與合規拓樸對齊 → <a href="/blog/backend/01-database/vendors/cockroachdb/survival-goals/" data-link-title="CockroachDB Survival Goals：zone 級 vs region 級配置與業務 SLO 倒推流程" data-link-desc="CockroachDB 用 SURVIVE ZONE FAILURE / SURVIVE REGION FAILURE 兩種 survival goal 宣告式控制 Raft replica 分佈、決定 RTO / RPO。本文走 Hard Rock Digital bet placement RPO=0 倒推流程、Netflix Gaming 48-node 跨 4 region 「為求 survival 而非 latency」的反直覺判讀、配置語法、寫入 latency 暴漲跟 cost 暴漲兩條失敗模式、合規邊界對比">CockroachDB survival goals</a></li>
<li>想做 region pinning 與在地化 schema → <a href="/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/" data-link-title="CockroachDB Locality-Aware Schema：跨州合規 &#43; 邏輯一個 cluster 的 region placement 策略" data-link-desc="Hard Rock Digital 跨 8 州 sportsbook、用 AWS Outposts &#43; region placement 把運算釘在州內、邏輯上仍是一個 CockroachDB cluster。本文走 REGIONAL BY ROW / REGIONAL BY TABLE / GLOBAL 三種 locality、Hard Rock 拓樸創新對比 Standard Chartered Aurora 7 cluster fleet、AWS Outposts 是合規工具不是 latency 工具的反直覺判讀">CockroachDB locality-aware schema</a></li>
<li>想對比 Aurora DSQL / Spanner / CockroachDB 給博彩 OLTP → <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.cockroachlabs.com/customers/hard-rock-digital/">Hard Rock Digital: scaling a performant sports betting platform（cockroachlabs.com customer page）</a></li>
<li><a href="https://downloads.ctfassets.net/00voh0j35590/7dKNWhsW4RjpUlFgzHB8qw/752a22c833c879bca503bbffb2b584c7/CockroachLabs-Hard-Rock-Digital-Case-Study-v2.pdf">Hard Rock, anytime, anywhere: scaling a performant sports betting platform（PDF case study）</a></li>
<li><a href="https://www.cockroachlabs.com/blog/highly-available-sports-betting-app/">How Hard Rock Digital built a highly available and compliant sports betting app</a></li>
<li><a href="https://www.cockroachlabs.com/blog/real-money-gaming-reference-architecture/">Building a sports betting application to handle &lsquo;Big Game&rsquo; traffic</a></li>
<li><a href="https://www.cockroachlabs.com/solutions/verticals/gambling/">CockroachDB for Gambling solutions page</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL → Aurora Migration：protocol 相容、operational 重設計</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a>（self-managed source）跟 &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&lt;/a>（cloud-managed target）。跟前兩篇 migration（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic&lt;/a> 高 schema 差 / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> drop-in）對照、本篇是 &lt;em>middle ground&lt;/em>：wire protocol drop-in、但 operational model 重設計。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷operational-cost--ha--dr-三條-driver">為什麼遷：operational cost / HA / DR 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Operational cost&lt;/strong>&lt;/td>
 &lt;td>self-managed PostgreSQL + Patroni HA + pgBackRest backup + monitoring 需 0.5-2 FTE；Aurora 把這層責任轉嫁 AWS、SRE 專注 application&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>HA reliability&lt;/strong>&lt;/td>
 &lt;td>Patroni split-brain / DCS quorum 偶爾踩雷、production failover 4-15s；Aurora 自動 multi-AZ failover &amp;lt; 30s、shared storage 不丟資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>DR / backup&lt;/strong>&lt;/td>
 &lt;td>自管 PITR + cross-region replication 複雜；Aurora 內建 PITR + global database + backup retention 簡化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（Aurora → self-managed）也存在 — 主要是 &lt;em>cost 在 10TB+ 規模時 Aurora 反而更貴&lt;/em>、或 &lt;em>需要 PostgreSQL extension Aurora 不支援&lt;/em>（pg_partman / pg_repack / TimescaleDB 等）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>（self-managed source）跟 <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>（cloud-managed target）。跟前兩篇 migration（<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a> 高 schema 差 / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> drop-in）對照、本篇是 <em>middle ground</em>：wire protocol drop-in、但 operational model 重設計。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關。</p></blockquote>
<h2 id="為什麼遷operational-cost--ha--dr-三條-driver">為什麼遷：operational cost / HA / DR 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Operational cost</strong></td>
          <td>self-managed PostgreSQL + Patroni HA + pgBackRest backup + monitoring 需 0.5-2 FTE；Aurora 把這層責任轉嫁 AWS、SRE 專注 application</td>
      </tr>
      <tr>
          <td><strong>HA reliability</strong></td>
          <td>Patroni split-brain / DCS quorum 偶爾踩雷、production failover 4-15s；Aurora 自動 multi-AZ failover &lt; 30s、shared storage 不丟資料</td>
      </tr>
      <tr>
          <td><strong>DR / backup</strong></td>
          <td>自管 PITR + cross-region replication 複雜；Aurora 內建 PITR + global database + backup retention 簡化</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Aurora → self-managed）也存在 — 主要是 <em>cost 在 10TB+ 規模時 Aurora 反而更貴</em>、或 <em>需要 PostgreSQL extension Aurora 不支援</em>（pg_partman / pg_repack / TimescaleDB 等）。</p>
<h2 id="結構protocol-相容--operational-phased-的混合">結構：protocol 相容 + operational phased 的混合</h2>
<p>跟前兩篇對照、Aurora migration 結構是 <em>protocol drop-in</em>（application 不改 SQL）+ <em>operational redesign</em>（HA / backup / monitoring 全換）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Splunk → Elastic（高 schema 差）</th>
          <th>Redis → DragonflyDB（drop-in）</th>
          <th>PostgreSQL → Aurora（middle）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Wire protocol</td>
          <td>完全不同（SPL vs KQL）</td>
          <td>完全相同（RESP）</td>
          <td>完全相同（PostgreSQL wire）</td>
      </tr>
      <tr>
          <td>Schema / data model</td>
          <td>高差異（CIM vs ECS）</td>
          <td>完全相同</td>
          <td>完全相同</td>
      </tr>
      <tr>
          <td>Application code</td>
          <td>必改</td>
          <td>不改</td>
          <td>不改</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>不同</td>
          <td>相似</td>
          <td><strong>大差</strong></td>
      </tr>
      <tr>
          <td>HA / replication</td>
          <td>不同</td>
          <td>相似</td>
          <td><strong>完全重設計</strong></td>
      </tr>
      <tr>
          <td>Backup model</td>
          <td>不同</td>
          <td>簡化</td>
          <td><strong>完全換 AWS-native</strong></td>
      </tr>
      <tr>
          <td>Migration 週期</td>
          <td>4-9 個月</td>
          <td>1-4 週</td>
          <td>6-12 週</td>
      </tr>
      <tr>
          <td>Phased 結構需要</td>
          <td>6-phase 明顯</td>
          <td>不需要</td>
          <td><strong>混合</strong>（3 operational phase + drop-in cutover）</td>
      </tr>
  </tbody>
</table>
<p><strong>Hypothesis 驗證</strong>：migration playbook 結構由 <em>最大差異維度</em> 決定 — Splunk → Elastic 是 schema 差導向 phased、Aurora migration 是 operational 差導向局部 phased。</p>
<h2 id="operational-redesign-對位">Operational redesign 對位</h2>
<p>跟 self-managed PostgreSQL 比、Aurora 的 operational 模型差異：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>Self-managed PostgreSQL</th>
          <th>Aurora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage</td>
          <td>Local disk / EBS、跟 compute 一體</td>
          <td>Shared storage 跨 AZ 6 副本、跟 compute 解耦</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Patroni + DCS quorum + watchdog</td>
          <td>Aurora 自家 failover、shared storage 不重 promote</td>
      </tr>
      <tr>
          <td>Read replica</td>
          <td>Streaming replication + Patroni 管理</td>
          <td>Aurora reader endpoint、cluster 自動 routing</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>pgBackRest / WAL-G + S3</td>
          <td>自動 continuous backup + PITR（內建）</td>
      </tr>
      <tr>
          <td>Failover time</td>
          <td>15-60s（Patroni）</td>
          <td>&lt; 30s（同 AZ）/ 1-2 min（跨 AZ）</td>
      </tr>
      <tr>
          <td>Connection management</td>
          <td>PgBouncer 必裝</td>
          <td>RDS Proxy 推薦、Aurora 自家 connection pool</td>
      </tr>
      <tr>
          <td>Major version upgrade</td>
          <td>手動 + 停機</td>
          <td>Aurora 自家 blue/green deployment</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Prometheus + grafana-postgresql</td>
          <td>CloudWatch + Performance Insights</td>
      </tr>
      <tr>
          <td>Extension support</td>
          <td>自由安裝</td>
          <td><strong>白名單</strong>、限 AWS 認可 extension</td>
      </tr>
      <tr>
          <td>Custom config</td>
          <td>postgresql.conf 全控</td>
          <td>Parameter Group（限制）</td>
      </tr>
      <tr>
          <td>OS / kernel access</td>
          <td>完全控</td>
          <td><strong>無</strong>（fully managed）</td>
      </tr>
  </tbody>
</table>
<p>每一條 operational concept 都需要 migration plan、application code 不變但 <em>運維知識體系全換</em>。</p>
<h2 id="migration-流程3-phase-operational--drop-in-cutover">Migration 流程：3 phase operational + drop-in cutover</h2>
<h3 id="phase-0pre-migration-audit1-2-週">Phase 0：Pre-migration audit（1-2 週）</h3>
<ol>
<li><strong>Extension 清單對位</strong>：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">extname</span><span class="p">,</span><span class="w"> </span><span class="n">extversion</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_extension</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 對照 Aurora supported extensions list
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- 不支援的（pg_repack / pg_partman 部分 / TimescaleDB / Citus）需替代方案</span></span></span></code></pre></div><ol start="2">
<li><strong>Custom config 清單</strong>：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">setting</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_settings</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">source</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s1">&#39;default&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 對照 Aurora Parameter Group 可調項目</span></span></span></code></pre></div><ol start="3">
<li><strong>Capacity 評估</strong>：</li>
</ol>
<ul>
<li>當前 IOPS / connection / storage / WAL rate</li>
<li>對應 Aurora instance class（db.r6g.large to db.r6g.32xlarge）</li>
<li>估算 cost（vCPU + IOPS + storage + backup retention）</li>
</ul>
<ol start="4">
<li><strong>Application connection pool audit</strong>：</li>
</ol>
<ul>
<li>PgBouncer 配置是否能直接搬到 RDS Proxy</li>
<li>Connection string + IAM 認證準備</li>
</ul>
<h3 id="phase-1operational-infrastructure-準備2-3-週">Phase 1：Operational infrastructure 準備（2-3 週）</h3>
<ol>
<li>建 Aurora cluster（Terraform / CloudFormation）</li>
<li>設 Parameter Group、對位 self-managed 配置</li>
<li>設 Security Group + IAM role</li>
<li>設 RDS Proxy（推薦、connection 集中管理）</li>
<li>CloudWatch alert + Performance Insights baseline</li>
<li>Backup retention + PITR window 設定</li>
</ol>
<h3 id="phase-2data-migration取決於-dataset-大小">Phase 2：Data migration（取決於 dataset 大小）</h3>
<p>兩條路：</p>
<h4 id="路線-aaws-dms推薦中等規模--5tb">路線 A：AWS DMS（推薦中等規模 &lt; 5TB）</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">self-managed Postgres ──(DMS)──→ Aurora
</span></span><span class="line"><span class="ln">2</span><span class="cl">                         |
</span></span><span class="line"><span class="ln">3</span><span class="cl">                  full load + CDC continuous</span></span></code></pre></div><ul>
<li>DMS task 設 <code>Full Load + Ongoing Replication</code></li>
<li>跑 full load 估算（100GB ~ 1-3 小時依 instance class）</li>
<li>CDC 持續直到 cutover</li>
</ul>
<h4 id="路線-blogical-replication推薦-5tb-或要精準控制">路線 B：Logical replication（推薦 5TB+ 或要精準控制）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Source：建 publication
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">migrate_pub</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="n">TABLES</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Aurora：建 subscription
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">SUBSCRIPTION</span><span class="w"> </span><span class="n">migrate_sub</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">CONNECTION</span><span class="w"> </span><span class="s1">&#39;host=&lt;source&gt; dbname=&lt;db&gt; user=&lt;replicator&gt;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="n">PUBLICATION</span><span class="w"> </span><span class="n">migrate_pub</span><span class="p">;</span></span></span></code></pre></div><ul>
<li>Initial COPY 跑完後 streaming</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a></li>
</ul>
<h3 id="phase-3cutover-跟-verification">Phase 3：Cutover 跟 verification</h3>





<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. Application 端設 maintenance mode（block writes）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 等 replication lag → 0
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 確認 Aurora 端 row count + checksum 對齊
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Application connection string 切到 Aurora endpoint
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 解除 maintenance mode
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Self-managed 端 read-only 保留 1-2 週 standby</span></span></code></pre></div><p>Cutover window 視 dataset 大小：</p>
<ul>
<li>&lt; 100GB：1-2 小時</li>
<li>100GB - 1TB：2-4 小時</li>
<li>1TB+：考慮 <em>zero-downtime cutover</em> via blue-green deployment</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1extension-不支援application-直接壞">Case 1：Extension 不支援、application 直接壞</h3>
<p><strong>徵兆</strong>：cutover 後 application 某些 query 報 <code>extension &quot;pg_repack&quot; not available</code>、batch job 壞。</p>
<p><strong>根因</strong>：Phase 0 audit 漏掉 application 用 pg_repack 做 maintenance；Aurora 不支援、self-managed 端的 cron job 改不過去。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit 必做</strong>：<code>SELECT extname FROM pg_extension</code> 對照 <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Extensions.html">Aurora extension whitelist</a></li>
<li><strong>替代方案</strong>：
<ul>
<li>pg_repack → Aurora 自家 vacuum + storage auto-resize</li>
<li>TimescaleDB → 改 declarative partitioning 或換 Timestream</li>
<li>Citus → 評估保留 self-managed 或重設計 schema</li>
</ul>
</li>
<li><strong>退役策略</strong>：Extension 是 application 必要的、評估暫不遷或選 alternative cloud（如 AlloyDB / Citus on Azure）</li>
</ol>
<h3 id="case-2replication-slot-不直通">Case 2：Replication slot 不直通</h3>
<p><strong>徵兆</strong>：self-managed 端有 Debezium CDC 接 application 事件、cutover 後 CDC pipeline 直接壞、Kafka 端訊息斷流。</p>
<p><strong>根因</strong>：Aurora 對 logical replication slot 有限制 — 不直接支援 external consumer（如 Debezium）讀 slot；要走 <em>RDS Database Events</em> 或 <em>DMS CDC</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit</strong>：列所有 logical consumer（Debezium / Kafka Connect / 自家 CDC）</li>
<li><strong>替代方案</strong>：
<ul>
<li>DMS CDC 取代 Debezium（Aurora 原生支援）</li>
<li>評估 RDS Database Activity Streams（newer feature）</li>
<li>重設計 CDC：application 寫 outbox 表、Aurora trigger 發 SNS → Lambda → Kafka</li>
</ul>
</li>
<li><strong>接受代價</strong>：CDC pipeline 重建是 2-4 週工作、納入 migration scope</li>
</ol>
<h3 id="case-3autovacuum-行為跟-self-managed-不同">Case 3：Autovacuum 行為跟 self-managed 不同</h3>
<p><strong>徵兆</strong>：cutover 後幾天、特定 hot table 的 bloat 數據異常、application 端 query latency p99 漲；CloudWatch Performance Insights 顯示 autovacuum 跑頻率比 self-managed 端高 3 倍。</p>
<p><strong>根因</strong>：Aurora 預設 Parameter Group 的 autovacuum 配置跟 self-managed 不同 — <code>autovacuum_vacuum_cost_limit</code> 預設更低、<code>vacuum_scale_factor</code> 更激進；shared storage 上 vacuum 行為不一樣。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Parameter Group 對位</strong>：把 self-managed <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a> 配置複製到 Aurora Parameter Group</li>
<li><strong>per-table tuning</strong>：hot table 的 <code>ALTER TABLE SET (autovacuum_*)</code> 可遷過去</li>
<li><strong>接受差異</strong>：Aurora storage 設計讓 vacuum 不一定要跟 self-managed 同 cadence、SRE 心智模型要調</li>
</ol>
<h3 id="case-4iam-認證強制application-端改-connection-logic">Case 4：IAM 認證強制、application 端改 connection logic</h3>
<p><strong>徵兆</strong>：production 切到 Aurora 後、application 仍用 password authentication、SOC team 要求改 IAM 認證（compliance）；application 連線 logic 大改、token rotation 邏輯也要加。</p>
<p><strong>根因</strong>：self-managed 端用固定 username/password、Aurora 推薦（部分情境強制）IAM authentication；token 15 分鐘輪換、application 必須改連線 SDK。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Migration scope 內包含</strong>：authentication migration 是必要工作、不能事後補</li>
<li><strong>SDK 整合</strong>：用 AWS SDK + RDS Proxy 抽象 token rotation、application 不直接管 token</li>
<li><strong>Hybrid 期間</strong>：保留 password auth 直到 application 全切 IAM、再 disable password auth</li>
</ol>
<h3 id="case-5cost-model-預估錯月底帳單炸">Case 5：Cost model 預估錯、月底帳單炸</h3>
<p><strong>徵兆</strong>：第一個月 Aurora 帳單比預估高 50-80%；IOPS / backup storage / I/O cost 都比預期多。</p>
<p><strong>根因</strong>：Aurora pricing 三層（compute instance / storage / I/O）—</p>
<ul>
<li>Storage：actual data + backup × retention</li>
<li>I/O：每個 read / write block 都計費（self-managed 不算）</li>
<li>Backup：超過 backup retention 部分 charged as snapshot storage</li>
</ul>
<p>self-managed 端習慣 <em>fixed EC2 + EBS</em> cost、Aurora I/O-based 計費對 high-IOPS workload 衝擊大。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration cost estimate</strong>：用 self-managed <code>pg_stat_database</code> 估 I/O 量、套 Aurora pricing calc</li>
<li><strong>I/O optimization</strong>：開 Aurora I/O-Optimized storage class（fixed monthly + 不算 I/O）、適合 high-IOPS workload</li>
<li><strong>Backup retention 控制</strong>：不要 default 35 天、依 compliance 調整（7-14 天通常夠）</li>
<li><strong>Reserved Instance</strong>：穩定 workload 預付 1-3 年、省 30-40%</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed PostgreSQL（EC2 + EBS）</th>
          <th>Aurora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Instance cost</td>
          <td>EC2 + EBS（compute + storage 自管）</td>
          <td>Aurora instance class + storage + I/O</td>
      </tr>
      <tr>
          <td>HA cost</td>
          <td>Patroni 跨 3 AZ + EBS 3 副本</td>
          <td>Aurora 跨 3 AZ shared storage（內建）</td>
      </tr>
      <tr>
          <td>Backup cost</td>
          <td>pgBackRest + S3 archive</td>
          <td>Aurora 自動 continuous backup（內建）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-2 FTE（HA / backup / patching）</td>
          <td>0.1-0.3 FTE（application 端 + Parameter Group）</td>
      </tr>
      <tr>
          <td>1TB / month cost</td>
          <td>$400-800（含 HA）</td>
          <td>$700-1500（含 HA）</td>
      </tr>
      <tr>
          <td>10TB / month cost</td>
          <td>$2K-4K</td>
          <td>$4K-8K（I/O cost 顯著）</td>
      </tr>
      <tr>
          <td>50TB+ cost</td>
          <td>$10K-20K</td>
          <td>$30K+（cost 反轉、self-managed 更便宜）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：&lt; 10TB workload Aurora 平攤 operational cost 後仍便宜；50TB+ workload Aurora cost 顯著高、要 reserved + I/O-Optimized 才有競爭力。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-patroni-ha-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> 對位</h3>
<p>Patroni 在 Aurora migration 後 <em>退役</em> — Aurora 自家 failover 取代；但 SRE 心智模型要調：</p>
<ul>
<li>Patroni 的 <code>pg_rewind</code> 概念不存在（shared storage）</li>
<li>Patroni 的 <code>synchronous_commit</code> 行為 Aurora 隱藏在 storage layer</li>
<li>Aurora 跨 region 用 <em>Global Database</em>、不是 Patroni cross-region setup</li>
</ul>
<h3 id="跟-pitr-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR</a> 對位</h3>
<p>self-managed PITR rebuild 工作量大、Aurora PITR 是 native API call：</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 rds restore-db-cluster-to-point-in-time <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --source-db-cluster-identifier myapp-prod <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --db-cluster-identifier myapp-prod-restored <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --restore-to-time 2026-05-19T14:30:00Z</span></span></code></pre></div><p>完全不需要 base backup + WAL replay 思維、storage layer 自動處理。</p>
<h3 id="跟-pgbouncer--rds-proxy">跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> → RDS Proxy</h3>
<p>PgBouncer 多數情境可換 RDS Proxy：</p>
<ul>
<li>transaction pooling 等效</li>
<li>IAM authentication 整合</li>
<li>Connection pinning（Lambda / serverless workload）</li>
<li><strong>限制</strong>：RDS Proxy 對某些 PG 14+ feature 仍 catching up、prepared statements 行為差異</li>
</ul>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Aurora Serverless v2 評估</strong>：variable workload 適合、steady workload 反而貴</li>
<li><strong>Babelfish 評估</strong>：跑 SQL Server protocol on Aurora（多 source 遷移到 Aurora）</li>
<li><strong>Cross-region DR</strong>：Aurora Global Database vs self-managed cross-region streaming + Patroni</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>Target vendor：<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></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>Aurora family 內進一步遷移：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">→ Aurora DSQL</a>（從 Aurora PG 升 DSQL active-active distributed、Type E paradigm shift）</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a> / <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/</guid><description>&lt;p>這個案例的核心責任是說明 OT/IT 整合場景的多工廠 leaf node 拓樸。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>i-flow 是工業數據整合平台、每日 4 億筆 data operation、提供 200+ OT/IT 系統 connector、客戶含 Fortune 500 工廠（Bosch、Sto、Lenze）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 NATS 當 OT/IT 跨層整合 bus、邊緣端負責 connect / harmonize / publish。揭露多工廠場景該用 leaf node hub-and-spoke、不該每工廠自管 cluster。&lt;strong>注意&lt;/strong>：此案例技術細節較淺、引用時要補其他案例的具體 stream / consumer 設計。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>NATS 進階主題：Cluster + Supercluster + Leaf node（多工廠 leaf node 連 central）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &amp;#43; JetStream &amp;#43; KV &amp;#43; Object Store。">3.C37 MachineMetrics&lt;/a>（技術細節更深的對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://nats.io/blog/i-flow-case-study/">i-flow Case Study&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 OT/IT 整合場景的多工廠 leaf node 拓樸。</p>
<h2 id="觀察">觀察</h2>
<p>i-flow 是工業數據整合平台、每日 4 億筆 data operation、提供 200+ OT/IT 系統 connector、客戶含 Fortune 500 工廠（Bosch、Sto、Lenze）。</p>
<h2 id="判讀">判讀</h2>
<p>用 NATS 當 OT/IT 跨層整合 bus、邊緣端負責 connect / harmonize / publish。揭露多工廠場景該用 leaf node hub-and-spoke、不該每工廠自管 cluster。<strong>注意</strong>：此案例技術細節較淺、引用時要補其他案例的具體 stream / consumer 設計。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>NATS 進階主題：Cluster + Supercluster + Leaf node（多工廠 leaf node 連 central）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37 MachineMetrics</a>（技術細節更深的對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://nats.io/blog/i-flow-case-study/">i-flow Case Study</a></li>
</ul>
]]></content:encoded></item><item><title>Azure AD：2021 身分控制面中斷事件</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/</guid><description>&lt;p>這起案例的核心責任是處理身份控制面故障對下游產品的連鎖影響。身份系統事故通常擴散快、影響廣，分級與通訊需要提前對齊。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>auth failure surge&lt;/td>
 &lt;td>影響是否跨產品擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>token issuance lag&lt;/td>
 &lt;td>控制面是否壅塞&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp;amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dependency blast radius&lt;/td>
 &lt;td>下游受影響範圍&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「身份控制面」對下游產品鏈的連鎖影響。主要風險是事件分級只看單一產品，忽略共用身份依賴的擴散速度。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先做影響分層，再同步外部通訊與回復節奏，並將判讀欄位回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是處理身份控制面故障對下游產品的連鎖影響。身份系統事故通常擴散快、影響廣，分級與通訊需要提前對齊。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>auth failure surge</td>
          <td>影響是否跨產品擴散</td>
          <td><a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1</a></td>
      </tr>
      <tr>
          <td>token issuance lag</td>
          <td>控制面是否壅塞</td>
          <td><a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18</a></td>
      </tr>
      <tr>
          <td>dependency blast radius</td>
          <td>下游受影響範圍</td>
          <td><a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「身份控制面」對下游產品鏈的連鎖影響。主要風險是事件分級只看單一產品，忽略共用身份依賴的擴散速度。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先做影響分層，再同步外部通訊與回復節奏，並將判讀欄位回寫 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a>。</p>
]]></content:encoded></item><item><title>Meta：Region Failover 與可靠性邊界</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/</guid><description>&lt;p>Meta 案例的核心責任是處理跨區故障時的邊界與回復順序。大規模平台的關鍵風險在跨區相依引發的連鎖退化，單點失效反而是較好處理的情況。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>當核心網路或控制面異常跨越區域邊界，若沒有預先定義故障域與回復順序，恢復動作本身會變成新的放大器。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Region fault domain&lt;/td>
 &lt;td>影響面最多到哪裡&lt;/td>
 &lt;td>故障邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ordered failover&lt;/td>
 &lt;td>先恢復哪條路徑&lt;/td>
 &lt;td>回復順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency isolation&lt;/td>
 &lt;td>共享相依如何降風險&lt;/td>
 &lt;td>局部化策略&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>cross-region error spread&lt;/td>
 &lt;td>擴散是否越界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>failover completion lag&lt;/td>
 &lt;td>回復批次是否收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>shared dependency saturation&lt;/td>
 &lt;td>共享依賴是否成瓶頸&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先定義 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a> 的演練範圍，再回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a> 的決策欄位。&lt;/p></description><content:encoded><![CDATA[<p>Meta 案例的核心責任是處理跨區故障時的邊界與回復順序。大規模平台的關鍵風險在跨區相依引發的連鎖退化，單點失效反而是較好處理的情況。</p>
<h2 id="問題場景">問題場景</h2>
<p>當核心網路或控制面異常跨越區域邊界，若沒有預先定義故障域與回復順序，恢復動作本身會變成新的放大器。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Region fault domain</td>
          <td>影響面最多到哪裡</td>
          <td>故障邊界</td>
      </tr>
      <tr>
          <td>Ordered failover</td>
          <td>先恢復哪條路徑</td>
          <td>回復順序</td>
      </tr>
      <tr>
          <td>Dependency isolation</td>
          <td>共享相依如何降風險</td>
          <td>局部化策略</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cross-region error spread</td>
          <td>擴散是否越界</td>
          <td><a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14</a></td>
      </tr>
      <tr>
          <td>failover completion lag</td>
          <td>回復批次是否收斂</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>shared dependency saturation</td>
          <td>共享依賴是否成瓶頸</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>先定義 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> 的演練範圍，再回寫 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 的決策欄位。</p>
]]></content:encoded></item><item><title>Stripe：Idempotency 與零停機遷移的交易安全設計</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/</guid><description>&lt;p>Stripe 案例的核心責任是確保交易語義在重試與變更中保持一致。支付系統的失效成本不只來自停機，還來自錯誤結果；因此可靠性設計要同時守住可用性與正確性。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>交易系統最常見的高風險組合是：客戶端重試、網路抖動、後端部署或資料遷移同時發生。若系統只處理單一失效，結果往往是可用但不一致，或者一致但無法持續交付。&lt;/p>
&lt;p>idempotency key 與 zero-downtime migration 的組合，目標是讓這些變更在同一套邊界下可判讀。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Idempotency key&lt;/td>
 &lt;td>同一交易重送如何得到同一結果&lt;/td>
 &lt;td>重試安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expand/contract migration&lt;/td>
 &lt;td>資料變更如何與新舊版本共存&lt;/td>
 &lt;td>漸進遷移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Canary + rollback gate&lt;/td>
 &lt;td>發版異常如何快速收斂&lt;/td>
 &lt;td>可回復交付&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction-path observability&lt;/td>
 &lt;td>交易路徑是否可追溯&lt;/td>
 &lt;td>一致性證據&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這組機制把「交易正確性」前移到 API 與遷移設計，而不是事後 reconciliation 才補救。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>duplicate request collapse ratio&lt;/td>
 &lt;td>重試是否被正確合併&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>migration phase error drift&lt;/td>
 &lt;td>遷移各階段錯誤是否收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>canary transaction anomaly&lt;/td>
 &lt;td>小流量交易是否出現偏差&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>payment trace consistency&lt;/td>
 &lt;td>trace 是否完整覆蓋交易關鍵欄位&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把 idempotency 實作成「只去重請求 ID」會漏掉交易語義。正確做法是讓 key 與業務操作邊界一致，並保留足夠證據以供重放與稽核判讀。另一個常見錯誤是把 migration 視為資料庫任務，沒有與 release gate 共同治理。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>實作層先從 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12&lt;/a> 定義重放語義，再到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a> 建立遷移節奏。發布控制對齊 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>；事故時的交易影響評估對齊 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Stripe 案例的核心責任是確保交易語義在重試與變更中保持一致。支付系統的失效成本不只來自停機，還來自錯誤結果；因此可靠性設計要同時守住可用性與正確性。</p>
<h2 id="問題場景">問題場景</h2>
<p>交易系統最常見的高風險組合是：客戶端重試、網路抖動、後端部署或資料遷移同時發生。若系統只處理單一失效，結果往往是可用但不一致，或者一致但無法持續交付。</p>
<p>idempotency key 與 zero-downtime migration 的組合，目標是讓這些變更在同一套邊界下可判讀。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Idempotency key</td>
          <td>同一交易重送如何得到同一結果</td>
          <td>重試安全</td>
      </tr>
      <tr>
          <td>Expand/contract migration</td>
          <td>資料變更如何與新舊版本共存</td>
          <td>漸進遷移</td>
      </tr>
      <tr>
          <td>Canary + rollback gate</td>
          <td>發版異常如何快速收斂</td>
          <td>可回復交付</td>
      </tr>
      <tr>
          <td>Transaction-path observability</td>
          <td>交易路徑是否可追溯</td>
          <td>一致性證據</td>
      </tr>
  </tbody>
</table>
<p>這組機制把「交易正確性」前移到 API 與遷移設計，而不是事後 reconciliation 才補救。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>duplicate request collapse ratio</td>
          <td>重試是否被正確合併</td>
          <td><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a></td>
      </tr>
      <tr>
          <td>migration phase error drift</td>
          <td>遷移各階段錯誤是否收斂</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td>canary transaction anomaly</td>
          <td>小流量交易是否出現偏差</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>payment trace consistency</td>
          <td>trace 是否完整覆蓋交易關鍵欄位</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把 idempotency 實作成「只去重請求 ID」會漏掉交易語義。正確做法是讓 key 與業務操作邊界一致，並保留足夠證據以供重放與稽核判讀。另一個常見錯誤是把 migration 視為資料庫任務，沒有與 release gate 共同治理。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>實作層先從 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a> 定義重放語義，再到 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a> 建立遷移節奏。發布控制對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a>；事故時的交易影響評估對齊 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a>。</p>
]]></content:encoded></item><item><title>Meta：BGP 事故與控制面恢復順序</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/</guid><description>&lt;p>控制面恢復順序的責任是確保回復路徑不依賴已故障的系統。當 DNS、BGP、遠端存取工具與內部通訊都跑在同一個網路上，網路故障會同時切斷服務和回復手段。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>2021-10-04，Meta 的一次 BGP 配置變更導致骨幹網路撤回所有 route announcement。影響的範圍不只是對外服務：DNS 因為無法到達權威 DNS server 而失效，內部工具（包含遠端管理、通訊與身份驗證）也依賴同一個內部網路，因此同步不可用。&lt;/p>
&lt;p>工程師無法透過遠端存取工具連線到設備，必須實體前往資料中心手動恢復 BGP 配置。資料中心的實體存取流程（門禁授權、安全人員協調、設備定位）進一步拉長恢復時間。整個事故從發生到服務恢復超過 6 小時。&lt;/p>
&lt;p>這個事故的核心教訓是恢復工具必須獨立於被恢復的系統。當 out-of-band 路徑在設計上或認證上依賴 production 網路，它就不是真正的 out-of-band。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Out-of-band management&lt;/td>
 &lt;td>恢復路徑是否獨立於 production 網路&lt;/td>
 &lt;td>獨立連線與管理通道&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery dependency mapping&lt;/td>
 &lt;td>每個回復步驟的依賴是否有循環&lt;/td>
 &lt;td>依賴圖與循環偵測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Staged recovery order&lt;/td>
 &lt;td>恢復順序是否先連通再服務&lt;/td>
 &lt;td>網路 → DNS → 控制面 → 資料面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Physical access readiness&lt;/td>
 &lt;td>remote 手段失效時實體存取是否可立即啟動&lt;/td>
 &lt;td>授權、存取卡、知識分佈&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Out-of-band management 的設計約束是完全獨立於 production 路徑。這包含網路連線（獨立 ISP 或 cellular）、認證（不依賴 production identity service）與通訊（獨立通訊工具或電話樹）。任何一環依賴 production 系統，就不算真正的 out-of-band。&lt;/p>
&lt;p>Recovery dependency mapping 的責任是在事故前畫出恢復步驟之間的依賴關係，找出循環依賴。Meta 事故中，DNS 恢復依賴網路連通，網路恢復依賴 BGP 設備存取，設備存取依賴 out-of-band 工具，而 out-of-band 工具的認證依賴 production identity service — 形成循環。事前的 dependency mapping 能暴露這類隱性路徑。&lt;/p>
&lt;p>Staged recovery order 把恢復拆成明確的階段：先恢復物理網路連通，再恢復 DNS 與名稱解析，接著恢復控制面服務（監控、部署、配置管理），最後恢復資料面流量。每個階段有明確的完成條件，下一階段才啟動。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>out-of-band reachability&lt;/td>
 &lt;td>獨立管理通道是否可連線&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>recovery dependency cycle count&lt;/td>
 &lt;td>恢復步驟之間是否存在循環依賴&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DNS propagation lag&lt;/td>
 &lt;td>名稱解析恢復後多久全域生效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>physical access activation time&lt;/td>
 &lt;td>從決策到實體接觸設備的時間&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>最常見的錯誤是把 out-of-band 存取當成「有設定就好」而不定期驗證。Meta 事故暴露的問題是 out-of-band 工具的 authentication 依賴 production identity service — 名義上路徑獨立，實際上認證路徑共享。DR rehearsal 必須包含「假設 production 網路完全不可用」的場景，驗證 out-of-band 路徑的每一環（連線、認證、通訊、操作權限）都能獨立運作。&lt;/p>
&lt;p>另一個常見問題是 recovery 知識集中在少數人。當實體恢復需要到場操作時，知識的地理分佈直接影響恢復時間。關鍵設備的恢復程序必須文件化，且分佈在多個地理位置的團隊成員手上。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal&lt;/a>：out-of-band 路徑的定期驗證&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget&lt;/a>：恢復路徑的隱性依賴治理&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition&lt;/a>：DNS 與控制面恢復完成的判準&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14 multi-incident coordination&lt;/a>：跨區域恢復的指揮協調&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/">More details about the October 4 outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://engineering.fb.com/2021/10/04/networking-traffic/outage/">Update about the October 4th outage&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>控制面恢復順序的責任是確保回復路徑不依賴已故障的系統。當 DNS、BGP、遠端存取工具與內部通訊都跑在同一個網路上，網路故障會同時切斷服務和回復手段。</p>
<h2 id="問題場景">問題場景</h2>
<p>2021-10-04，Meta 的一次 BGP 配置變更導致骨幹網路撤回所有 route announcement。影響的範圍不只是對外服務：DNS 因為無法到達權威 DNS server 而失效，內部工具（包含遠端管理、通訊與身份驗證）也依賴同一個內部網路，因此同步不可用。</p>
<p>工程師無法透過遠端存取工具連線到設備，必須實體前往資料中心手動恢復 BGP 配置。資料中心的實體存取流程（門禁授權、安全人員協調、設備定位）進一步拉長恢復時間。整個事故從發生到服務恢復超過 6 小時。</p>
<p>這個事故的核心教訓是恢復工具必須獨立於被恢復的系統。當 out-of-band 路徑在設計上或認證上依賴 production 網路，它就不是真正的 out-of-band。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Out-of-band management</td>
          <td>恢復路徑是否獨立於 production 網路</td>
          <td>獨立連線與管理通道</td>
      </tr>
      <tr>
          <td>Recovery dependency mapping</td>
          <td>每個回復步驟的依賴是否有循環</td>
          <td>依賴圖與循環偵測</td>
      </tr>
      <tr>
          <td>Staged recovery order</td>
          <td>恢復順序是否先連通再服務</td>
          <td>網路 → DNS → 控制面 → 資料面</td>
      </tr>
      <tr>
          <td>Physical access readiness</td>
          <td>remote 手段失效時實體存取是否可立即啟動</td>
          <td>授權、存取卡、知識分佈</td>
      </tr>
  </tbody>
</table>
<p>Out-of-band management 的設計約束是完全獨立於 production 路徑。這包含網路連線（獨立 ISP 或 cellular）、認證（不依賴 production identity service）與通訊（獨立通訊工具或電話樹）。任何一環依賴 production 系統，就不算真正的 out-of-band。</p>
<p>Recovery dependency mapping 的責任是在事故前畫出恢復步驟之間的依賴關係，找出循環依賴。Meta 事故中，DNS 恢復依賴網路連通，網路恢復依賴 BGP 設備存取，設備存取依賴 out-of-band 工具，而 out-of-band 工具的認證依賴 production identity service — 形成循環。事前的 dependency mapping 能暴露這類隱性路徑。</p>
<p>Staged recovery order 把恢復拆成明確的階段：先恢復物理網路連通，再恢復 DNS 與名稱解析，接著恢復控制面服務（監控、部署、配置管理），最後恢復資料面流量。每個階段有明確的完成條件，下一階段才啟動。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>out-of-band reachability</td>
          <td>獨立管理通道是否可連線</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a></td>
      </tr>
      <tr>
          <td>recovery dependency cycle count</td>
          <td>恢復步驟之間是否存在循環依賴</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>DNS propagation lag</td>
          <td>名稱解析恢復後多久全域生效</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>physical access activation time</td>
          <td>從決策到實體接觸設備的時間</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>最常見的錯誤是把 out-of-band 存取當成「有設定就好」而不定期驗證。Meta 事故暴露的問題是 out-of-band 工具的 authentication 依賴 production identity service — 名義上路徑獨立，實際上認證路徑共享。DR rehearsal 必須包含「假設 production 網路完全不可用」的場景，驗證 out-of-band 路徑的每一環（連線、認證、通訊、操作權限）都能獨立運作。</p>
<p>另一個常見問題是 recovery 知識集中在少數人。當實體恢復需要到場操作時，知識的地理分佈直接影響恢復時間。關鍵設備的恢復程序必須文件化，且分佈在多個地理位置的團隊成員手上。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal</a>：out-of-band 路徑的定期驗證</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>：恢復路徑的隱性依賴治理</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：DNS 與控制面恢復完成的判準</li>
<li><a href="/blog/backend/08-incident-response/multi-incident-coordination/" data-link-title="8.14 Multi-incident Coordination" data-link-desc="把同時多事故的優先序、資源分配與 incident command system pool 協調變成可執行流程">8.14 multi-incident coordination</a>：跨區域恢復的指揮協調</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/">More details about the October 4 outage</a></li>
<li><a href="https://engineering.fb.com/2021/10/04/networking-traffic/outage/">Update about the October 4th outage</a></li>
</ul>
]]></content:encoded></item><item><title>Pinterest：Storage Migration 與 Data Infrastructure Reliability</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/</guid><description>&lt;p>Storage migration 的可靠性責任是讓資料基礎設施的變更可漸進、可驗證、可回退。PB 級資料的儲存引擎遷移（如 HBase → TiDB）牽涉 schema mapping、query pattern 差異與 consistency model 變更，任何一處不相容都會在 production 流量下被放大。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>Pinterest 的資料基礎設施服務數十億 pin、推薦系統與搜尋索引。當儲存引擎需要退役或升級時，直接 cutover 的風險在於所有不相容同時暴露 — query 語意差異、pagination 行為、null handling、ordering 規則都可能在切換瞬間衝擊線上流量。&lt;/p>
&lt;p>漸進遷移的設計核心是把一次性 cutover 拆成可觀測的多階段流程，每個階段都有回退路徑。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Dual-write&lt;/td>
 &lt;td>新舊系統的寫入是否同步且完整&lt;/td>
 &lt;td>資料不遺失保證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shadow read&lt;/td>
 &lt;td>新舊系統的讀取結果是否一致&lt;/td>
 &lt;td>行為差異清單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reconciliation&lt;/td>
 &lt;td>兩套系統的資料是否持續一致&lt;/td>
 &lt;td>一致性報告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Staged cutover&lt;/td>
 &lt;td>何時可以把流量從舊系統切到新系統&lt;/td>
 &lt;td>漸進切換節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Dual-write 確保遷移期間每筆寫入同時進入新舊系統。寫入失敗的處理策略決定資料完整性 — 若新系統寫入失敗是否 block 舊系統的寫入，取決於遷移階段（早期容許新系統 fail-open、接近 cutover 時需要 fail-close）。&lt;/p>
&lt;p>Shadow read 在真實流量下比對新舊系統的查詢結果。比對維度包含回傳資料的完整性、排序、分頁邊界與 null 值處理。mismatch rate 是 cutover 可行性的核心判準 — rate 趨近零才能進入下一批切換。&lt;/p>
&lt;p>Staged cutover 按 traffic percentage、data partition 或 use case 漸進切換。每一批觀察 mismatch rate、latency overhead 與 error rate，任一指標超門檻即回退到舊系統。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>shadow read mismatch rate&lt;/td>
 &lt;td>新舊系統行為差異是否收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dual-write latency overhead&lt;/td>
 &lt;td>同步寫入是否拖累主路徑&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reconciliation gap&lt;/td>
 &lt;td>兩套系統資料是否持續一致&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cutover rollback count&lt;/td>
 &lt;td>切換過程是否穩定&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>Shadow read 比對容易只看最終結果是否相同，忽略中間狀態的差異。pagination 的邊界行為、null 欄位的回傳語意、排序在 tie-breaking 時的規則 — 這些差異在主流程不明顯，但在邊界情境會爆發。reconciliation 需要覆蓋 edge case，包含空集合回傳、大量資料分頁與 concurrent write 衝突。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety&lt;/a>：storage migration 的 schema 相容與 rollout 策略&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal&lt;/a>：cutover 失敗時的 rollback 路徑&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate&lt;/a>：dual-write latency 作為 regression 偵測&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff&lt;/a>：reconciliation 結果作為 cutover 決策證據&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/online-data-migration-from-hbase-to-tidb-with-zero-downtime-43f0fb474b84">Online Data Migration from HBase to TiDB with Zero Downtime&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://medium.com/pinterest-engineering/hbase-deprecation-at-pinterest-8a99e6c8e6b7">HBase Deprecation at Pinterest&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Storage migration 的可靠性責任是讓資料基礎設施的變更可漸進、可驗證、可回退。PB 級資料的儲存引擎遷移（如 HBase → TiDB）牽涉 schema mapping、query pattern 差異與 consistency model 變更，任何一處不相容都會在 production 流量下被放大。</p>
<h2 id="問題場景">問題場景</h2>
<p>Pinterest 的資料基礎設施服務數十億 pin、推薦系統與搜尋索引。當儲存引擎需要退役或升級時，直接 cutover 的風險在於所有不相容同時暴露 — query 語意差異、pagination 行為、null handling、ordering 規則都可能在切換瞬間衝擊線上流量。</p>
<p>漸進遷移的設計核心是把一次性 cutover 拆成可觀測的多階段流程，每個階段都有回退路徑。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dual-write</td>
          <td>新舊系統的寫入是否同步且完整</td>
          <td>資料不遺失保證</td>
      </tr>
      <tr>
          <td>Shadow read</td>
          <td>新舊系統的讀取結果是否一致</td>
          <td>行為差異清單</td>
      </tr>
      <tr>
          <td>Reconciliation</td>
          <td>兩套系統的資料是否持續一致</td>
          <td>一致性報告</td>
      </tr>
      <tr>
          <td>Staged cutover</td>
          <td>何時可以把流量從舊系統切到新系統</td>
          <td>漸進切換節奏</td>
      </tr>
  </tbody>
</table>
<p>Dual-write 確保遷移期間每筆寫入同時進入新舊系統。寫入失敗的處理策略決定資料完整性 — 若新系統寫入失敗是否 block 舊系統的寫入，取決於遷移階段（早期容許新系統 fail-open、接近 cutover 時需要 fail-close）。</p>
<p>Shadow read 在真實流量下比對新舊系統的查詢結果。比對維度包含回傳資料的完整性、排序、分頁邊界與 null 值處理。mismatch rate 是 cutover 可行性的核心判準 — rate 趨近零才能進入下一批切換。</p>
<p>Staged cutover 按 traffic percentage、data partition 或 use case 漸進切換。每一批觀察 mismatch rate、latency overhead 與 error rate，任一指標超門檻即回退到舊系統。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>shadow read mismatch rate</td>
          <td>新舊系統行為差異是否收斂</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a></td>
      </tr>
      <tr>
          <td>dual-write latency overhead</td>
          <td>同步寫入是否拖累主路徑</td>
          <td><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13</a></td>
      </tr>
      <tr>
          <td>reconciliation gap</td>
          <td>兩套系統資料是否持續一致</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
      <tr>
          <td>cutover rollback count</td>
          <td>切換過程是否穩定</td>
          <td><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>Shadow read 比對容易只看最終結果是否相同，忽略中間狀態的差異。pagination 的邊界行為、null 欄位的回傳語意、排序在 tie-breaking 時的規則 — 這些差異在主流程不明顯，但在邊界情境會爆發。reconciliation 需要覆蓋 edge case，包含空集合回傳、大量資料分頁與 concurrent write 衝突。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>：storage migration 的 schema 相容與 rollout 策略</li>
<li><a href="/blog/backend/06-reliability/dr-rollback-rehearsal/" data-link-title="6.7 DR 演練與 Rollback Rehearsal" data-link-desc="把回復路徑從紙面計畫變成定期可重播、可量測的驗證流程">6.7 DR rollback rehearsal</a>：cutover 失敗時的 rollback 路徑</li>
<li><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 performance regression gate</a>：dual-write latency 作為 regression 偵測</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>：reconciliation 結果作為 cutover 決策證據</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/pinterest-engineering/online-data-migration-from-hbase-to-tidb-with-zero-downtime-43f0fb474b84">Online Data Migration from HBase to TiDB with Zero Downtime</a></li>
<li><a href="https://medium.com/pinterest-engineering/hbase-deprecation-at-pinterest-8a99e6c8e6b7">HBase Deprecation at Pinterest</a></li>
</ul>
]]></content:encoded></item><item><title>Spotify：Backstage Service Catalog 與 Reliability Metadata</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/</guid><description>&lt;p>Service catalog 在可靠性工程中的責任是讓每個服務的 reliability metadata 有單一查詢入口。事故發生時，團隊能在同一個地方找到 owner、SLO 狀態、依賴圖與 runbook，而不是在 wiki、Slack 與個人筆記之間來回搜尋。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>Squad-based 組織結構讓團隊能獨立交付，但也讓服務數量快速增長。當服務超過數百個，metadata 開始散落在不同系統：ownership 記在 wiki、SLO 記在 monitoring 平台、runbook 記在文件庫、依賴關係靠口頭傳遞。事故時花時間找 owner 和 runbook 的成本直接拉長 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>。Spotify 用 Backstage 作為 service catalog，把這些 metadata 收攏到同一個入口。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Service ownership&lt;/td>
 &lt;td>這個服務歸誰管&lt;/td>
 &lt;td>強制 owner team&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLO metadata&lt;/td>
 &lt;td>這個服務的可靠性承諾是什麼&lt;/td>
 &lt;td>catalog 內嵌 SLO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency graph&lt;/td>
 &lt;td>這個服務依賴誰、誰依賴它&lt;/td>
 &lt;td>可查詢依賴圖&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runbook linkage&lt;/td>
 &lt;td>出事時該看哪份 runbook&lt;/td>
 &lt;td>一鍵連結&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metadata freshness&lt;/td>
 &lt;td>catalog 資料是否仍然準確&lt;/td>
 &lt;td>過期警告機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Service ownership 是最基礎的一層。每個服務在 catalog 中必須有明確的 owner team，沒有 owner 的服務標記為 orphan 並進入清理追蹤。ownership 不只是名義歸屬，而是事故時的第一接手責任。&lt;/p>
&lt;p>SLO metadata 讓 catalog 不只是目錄，而是可靠性狀態的即時入口。團隊能在 catalog 頁面直接看到服務目前的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 消耗狀態，判斷該服務的變更風險。&lt;/p>
&lt;p>Dependency graph 的價值在事故時最明顯。當一個服務異常時，catalog 能回答「還有誰會被影響」和「這個問題可能從哪裡傳過來」，讓事故指揮能快速判斷 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>。&lt;/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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Orphan service count&lt;/td>
 &lt;td>無 owner 服務是否持續增加&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metadata freshness&lt;/td>
 &lt;td>catalog 資料是否仍然準確&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency coverage&lt;/td>
 &lt;td>依賴圖是否涵蓋關鍵路徑&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MTTR vs catalog coverage&lt;/td>
 &lt;td>catalog 覆蓋率是否與恢復速度相關&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>Catalog 最常見的失效模式是變成靜態文件。若 metadata 靠人工維護但沒有 freshness check，catalog 會隨時間漂移 — owner 換了團隊但 catalog 沒更新、SLO 調整了但 catalog 還是舊值、依賴關係變了但 graph 沒有同步。事故時從 catalog 拿到過期資訊，比沒有 catalog 更危險，因為團隊會信任它。維持 catalog 價值的關鍵是自動化校驗：定期掃描 orphan service、比對 SLO metadata 與 monitoring 平台的實際值、用 runtime trace 驗證依賴圖的準確性。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget&lt;/a>：catalog 的依賴圖是 dependency budget 的資料來源&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 reliability metrics governance&lt;/a>：catalog coverage 與 metadata freshness 本身是可靠性指標&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review&lt;/a>：readiness checklist 可從 catalog 自動拉取&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog&lt;/a>：orphan service 與過期 metadata 是 reliability debt&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://backstage.io/">Backstage.io&lt;/a>：Spotify 開源的 developer portal 框架&lt;/li>
&lt;li>&lt;a href="https://backstage.spotify.com/">Spotify Engineering: What is Backstage?&lt;/a>：Backstage 的設計理念與架構&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Service catalog 在可靠性工程中的責任是讓每個服務的 reliability metadata 有單一查詢入口。事故發生時，團隊能在同一個地方找到 owner、SLO 狀態、依賴圖與 runbook，而不是在 wiki、Slack 與個人筆記之間來回搜尋。</p>
<h2 id="問題場景">問題場景</h2>
<p>Squad-based 組織結構讓團隊能獨立交付，但也讓服務數量快速增長。當服務超過數百個，metadata 開始散落在不同系統：ownership 記在 wiki、SLO 記在 monitoring 平台、runbook 記在文件庫、依賴關係靠口頭傳遞。事故時花時間找 owner 和 runbook 的成本直接拉長 <a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a>。Spotify 用 Backstage 作為 service catalog，把這些 metadata 收攏到同一個入口。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Service ownership</td>
          <td>這個服務歸誰管</td>
          <td>強制 owner team</td>
      </tr>
      <tr>
          <td>SLO metadata</td>
          <td>這個服務的可靠性承諾是什麼</td>
          <td>catalog 內嵌 SLO</td>
      </tr>
      <tr>
          <td>Dependency graph</td>
          <td>這個服務依賴誰、誰依賴它</td>
          <td>可查詢依賴圖</td>
      </tr>
      <tr>
          <td>Runbook linkage</td>
          <td>出事時該看哪份 runbook</td>
          <td>一鍵連結</td>
      </tr>
      <tr>
          <td>Metadata freshness</td>
          <td>catalog 資料是否仍然準確</td>
          <td>過期警告機制</td>
      </tr>
  </tbody>
</table>
<p>Service ownership 是最基礎的一層。每個服務在 catalog 中必須有明確的 owner team，沒有 owner 的服務標記為 orphan 並進入清理追蹤。ownership 不只是名義歸屬，而是事故時的第一接手責任。</p>
<p>SLO metadata 讓 catalog 不只是目錄，而是可靠性狀態的即時入口。團隊能在 catalog 頁面直接看到服務目前的 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 消耗狀態，判斷該服務的變更風險。</p>
<p>Dependency graph 的價值在事故時最明顯。當一個服務異常時，catalog 能回答「還有誰會被影響」和「這個問題可能從哪裡傳過來」，讓事故指揮能快速判斷 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Orphan service count</td>
          <td>無 owner 服務是否持續增加</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a></td>
      </tr>
      <tr>
          <td>Metadata freshness</td>
          <td>catalog 資料是否仍然準確</td>
          <td><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18</a></td>
      </tr>
      <tr>
          <td>Dependency coverage</td>
          <td>依賴圖是否涵蓋關鍵路徑</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>MTTR vs catalog coverage</td>
          <td>catalog 覆蓋率是否與恢復速度相關</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>Catalog 最常見的失效模式是變成靜態文件。若 metadata 靠人工維護但沒有 freshness check，catalog 會隨時間漂移 — owner 換了團隊但 catalog 沒更新、SLO 調整了但 catalog 還是舊值、依賴關係變了但 graph 沒有同步。事故時從 catalog 拿到過期資訊，比沒有 catalog 更危險，因為團隊會信任它。維持 catalog 價值的關鍵是自動化校驗：定期掃描 orphan service、比對 SLO metadata 與 monitoring 平台的實際值、用 runtime trace 驗證依賴圖的準確性。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency reliability budget</a>：catalog 的依賴圖是 dependency budget 的資料來源</li>
<li><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 reliability metrics governance</a>：catalog coverage 與 metadata freshness 本身是可靠性指標</li>
<li><a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19 reliability readiness review</a>：readiness checklist 可從 catalog 自動拉取</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt backlog</a>：orphan service 與過期 metadata 是 reliability debt</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://backstage.io/">Backstage.io</a>：Spotify 開源的 developer portal 框架</li>
<li><a href="https://backstage.spotify.com/">Spotify Engineering: What is Backstage?</a>：Backstage 的設計理念與架構</li>
</ul>
]]></content:encoded></item><item><title>Stripe：Canary Deploy 與 Progressive Rollout 治理</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/</guid><description>&lt;p>金流場景的 canary deploy 核心責任是讓每一批放量都能用交易指標判斷是否安全。progressive rollout 的節奏由交易成功率、duplicate charge 偵測與退款異常等金流特有指標驅動。本文從金流場景的通用壓力推導 progressive rollout 設計，以 Stripe 公開的 deploy 與 idempotency 實踐作為背景脈絡。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>金流變更的風險帶有延遲性。交易失敗可能在結帳時才被發現，退款申請可能在數天後才出現，對帳差異可能在日終結算才暴露。若 canary 只觀察幾分鐘的 error rate，延遲暴露的問題會在全量放行後才浮現。&lt;/p>
&lt;p>這種延遲特性讓金流場景需要比一般功能更長的觀察窗與更多元的判讀指標。放行決策要等交易生命週期的關鍵階段都走過，才能確認變更安全。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Canary traffic control&lt;/td>
 &lt;td>每批流量比例與觀察窗如何設定&lt;/td>
 &lt;td>1% → 5% → 25% → 100%，觀察窗依交易確認延遲調整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction-specific checks&lt;/td>
 &lt;td>交易指標是否涵蓋結帳到對帳的完整鏈路&lt;/td>
 &lt;td>checkout success rate、capture rate、duplicate、refund anomaly&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Automatic rollback trigger&lt;/td>
 &lt;td>交易異常時是否能即時回退&lt;/td>
 &lt;td>指標超門檻自動回退，不等人工判斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Staged config vs code&lt;/td>
 &lt;td>config 變更與 code 變更的風險是否相同&lt;/td>
 &lt;td>timeout / retry 等 config 變更走獨立且更短的 rollout 節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Canary traffic 的觀察窗設計是這個機制的關鍵。1% 階段至少觀察到一個完整的交易確認週期（通常 30 分鐘到數小時），5% 階段需要覆蓋一個對帳週期，25% 階段需要確認退款率無異常。每批之間的 go/no-go 判斷依據是全部交易指標都在 baseline 範圍內，任一指標偏離即暫停擴批。&lt;/p>
&lt;p>Config 變更（如 provider timeout 或 retry 次數）與 code 變更走不同 rollout 路線。config 變更影響面通常更可預測、回退更快（秒級生效），但風險在於小幅調整也可能放大 retry storm 或觸發 cascade timeout。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>checkout success rate&lt;/td>
 &lt;td>canary 批次是否維持交易承諾&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>canary vs baseline latency&lt;/td>
 &lt;td>延遲偏移是否超過可接受範圍&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>payment duplicate rate&lt;/td>
 &lt;td>重試是否產生重複扣款&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback trigger count&lt;/td>
 &lt;td>自動回退是否頻繁觸發&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>refund anomaly rate&lt;/td>
 &lt;td>退款比率是否偏離歷史 baseline&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把金流 canary 跟一般 feature rollout 用同一套觀察窗，會漏掉延遲暴露的問題。金流的 feedback loop 從結帳到退款可能跨越數天，短窗觀察拿到的 pass 訊號只代表即時指標正常，無法涵蓋對帳與退款階段的風險。&lt;/p>
&lt;p>另一個常見問題是 config 變更被視為低風險而跳過 canary。timeout 或 retry 設定的微幅調整看似無害，但在高流量下可能觸發 retry storm 或改變 provider 端的行為，影響幅度可能大於 code 變更。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a> 定義金流場景的放行政策，再到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance&lt;/a> 設計 progressive rollout 的 flag lifecycle。實作示範見 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>金流場景的 canary deploy 核心責任是讓每一批放量都能用交易指標判斷是否安全。progressive rollout 的節奏由交易成功率、duplicate charge 偵測與退款異常等金流特有指標驅動。本文從金流場景的通用壓力推導 progressive rollout 設計，以 Stripe 公開的 deploy 與 idempotency 實踐作為背景脈絡。</p>
<h2 id="問題場景">問題場景</h2>
<p>金流變更的風險帶有延遲性。交易失敗可能在結帳時才被發現，退款申請可能在數天後才出現，對帳差異可能在日終結算才暴露。若 canary 只觀察幾分鐘的 error rate，延遲暴露的問題會在全量放行後才浮現。</p>
<p>這種延遲特性讓金流場景需要比一般功能更長的觀察窗與更多元的判讀指標。放行決策要等交易生命週期的關鍵階段都走過，才能確認變更安全。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Canary traffic control</td>
          <td>每批流量比例與觀察窗如何設定</td>
          <td>1% → 5% → 25% → 100%，觀察窗依交易確認延遲調整</td>
      </tr>
      <tr>
          <td>Transaction-specific checks</td>
          <td>交易指標是否涵蓋結帳到對帳的完整鏈路</td>
          <td>checkout success rate、capture rate、duplicate、refund anomaly</td>
      </tr>
      <tr>
          <td>Automatic rollback trigger</td>
          <td>交易異常時是否能即時回退</td>
          <td>指標超門檻自動回退，不等人工判斷</td>
      </tr>
      <tr>
          <td>Staged config vs code</td>
          <td>config 變更與 code 變更的風險是否相同</td>
          <td>timeout / retry 等 config 變更走獨立且更短的 rollout 節奏</td>
      </tr>
  </tbody>
</table>
<p>Canary traffic 的觀察窗設計是這個機制的關鍵。1% 階段至少觀察到一個完整的交易確認週期（通常 30 分鐘到數小時），5% 階段需要覆蓋一個對帳週期，25% 階段需要確認退款率無異常。每批之間的 go/no-go 判斷依據是全部交易指標都在 baseline 範圍內，任一指標偏離即暫停擴批。</p>
<p>Config 變更（如 provider timeout 或 retry 次數）與 code 變更走不同 rollout 路線。config 變更影響面通常更可預測、回退更快（秒級生效），但風險在於小幅調整也可能放大 retry storm 或觸發 cascade timeout。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>checkout success rate</td>
          <td>canary 批次是否維持交易承諾</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>canary vs baseline latency</td>
          <td>延遲偏移是否超過可接受範圍</td>
          <td><a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13</a></td>
      </tr>
      <tr>
          <td>payment duplicate rate</td>
          <td>重試是否產生重複扣款</td>
          <td><a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12</a></td>
      </tr>
      <tr>
          <td>rollback trigger count</td>
          <td>自動回退是否頻繁觸發</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
      </tr>
      <tr>
          <td>refund anomaly rate</td>
          <td>退款比率是否偏離歷史 baseline</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把金流 canary 跟一般 feature rollout 用同一套觀察窗，會漏掉延遲暴露的問題。金流的 feedback loop 從結帳到退款可能跨越數天，短窗觀察拿到的 pass 訊號只代表即時指標正常，無法涵蓋對帳與退款階段的風險。</p>
<p>另一個常見問題是 config 變更被視為低風險而跳過 canary。timeout 或 retry 設定的微幅調整看似無害，但在高流量下可能觸發 retry storm 或改變 provider 端的行為，影響幅度可能大於 code 變更。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 定義金流場景的放行政策，再到 <a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance</a> 設計 progressive rollout 的 flag lifecycle。實作示範見 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://stripe.com/blog/idempotency">Designing robust and predictable APIs with idempotency</a>：idempotency key 設計，支撐 canary 回退後的重試安全</li>
<li><a href="https://stripe.com/blog/how-stripes-document-databases-supported-99.999-uptime-with-zero-downtime-data-migrations">How Stripe&rsquo;s document databases supported 99.999% uptime with zero-downtime data migrations</a>：zero-downtime migration 的 staged rollout 思路</li>
</ul>
<p>本文的 progressive rollout 機制（觀察窗設計、交易指標門檻、自動回退）從金流場景的通用壓力推導，並非 Stripe 公開的具體 deploy pipeline 描述。</p>
]]></content:encoded></item><item><title>PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a>（source）跟 &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&lt;/a>（DSQL 也屬 Aurora family、但 paradigm 不同）。跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">migrate-to-aurora&lt;/a>（PG → Aurora PG、protocol drop-in + operational redesign）跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &amp;#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">migrate-to-cockroachdb&lt;/a>（PG → CRDB、Type E paradigm shift）對照、本篇是 &lt;em>Aurora 內 PG → DSQL 的 paradigm shift&lt;/em>。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>時間錨點&lt;/strong>：Aurora DSQL 在 &lt;strong>2024-12 re:Invent preview&lt;/strong>、&lt;strong>2025-05-27 GA&lt;/strong>。本文 vendor claim 以 2025-2026 公開狀態為準、實際 migration 前請以 AWS docs 為準（feature 持續演進中）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷global-write--operational-zero-touch--region-resiliency-三條-driver">為什麼遷：Global Write / Operational Zero-touch / Region Resiliency 三條 driver&lt;/h2>
&lt;p>PG → DSQL 不是「自然演進」、是 &lt;em>application 需求超出 single-primary 模型&lt;/em> 時的 paradigm 換軌。三條典型 driver 各自對應一種 application 約束、不是「三選一」、而是「至少其中一條剛性、其他兩條是 bonus」：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>（source）跟 <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>（DSQL 也屬 Aurora family、但 paradigm 不同）。跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">migrate-to-aurora</a>（PG → Aurora PG、protocol drop-in + operational redesign）跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">migrate-to-cockroachdb</a>（PG → CRDB、Type E paradigm shift）對照、本篇是 <em>Aurora 內 PG → DSQL 的 paradigm shift</em>。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關。</p></blockquote>
<blockquote>
<p><strong>時間錨點</strong>：Aurora DSQL 在 <strong>2024-12 re:Invent preview</strong>、<strong>2025-05-27 GA</strong>。本文 vendor claim 以 2025-2026 公開狀態為準、實際 migration 前請以 AWS docs 為準（feature 持續演進中）。</p></blockquote>
<h2 id="為什麼遷global-write--operational-zero-touch--region-resiliency-三條-driver">為什麼遷：Global Write / Operational Zero-touch / Region Resiliency 三條 driver</h2>
<p>PG → DSQL 不是「自然演進」、是 <em>application 需求超出 single-primary 模型</em> 時的 paradigm 換軌。三條典型 driver 各自對應一種 application 約束、不是「三選一」、而是「至少其中一條剛性、其他兩條是 bonus」：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Global write</strong></td>
          <td>Application 需要多 region active-active write（不是 Aurora PG 的 single-writer + read replica）</td>
      </tr>
      <tr>
          <td><strong>Operational zero-touch</strong></td>
          <td>不想管 Patroni / PgBouncer / autovacuum / failover / backup retention、Aurora PG 已減一半、DSQL 進一步零接觸</td>
      </tr>
      <tr>
          <td><strong>Region resiliency</strong></td>
          <td>整 region 失效時應用無感切換（Aurora PG 是 cross-region replica 異步、DSQL 是 strong consistency 多 region）</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（DSQL → Aurora PG）也存在：</p>
<ul>
<li>需要 PG extension（pgvector / TimescaleDB / PostGIS / pg_repack）— DSQL 不支援</li>
<li>Cost：DSQL 比 Aurora PG 貴 2-5x（依 region 數量）</li>
<li>Single-region OLTP 不需 distributed transaction 的 overhead</li>
</ul>
<h2 id="結構protocol-drop-in--paradigm-shift">結構：Protocol Drop-in + Paradigm Shift</h2>
<p>DSQL 是 PG wire-compatible（用 <code>psql</code> 連得上）、但內部是 <em>distributed SQL engine</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>self-managed PG</th>
          <th>Aurora PG</th>
          <th>Aurora DSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Wire protocol</td>
          <td>PG</td>
          <td>PG</td>
          <td>PG（subset）</td>
      </tr>
      <tr>
          <td>Architecture</td>
          <td>Single primary</td>
          <td>Single primary + shared storage</td>
          <td><strong>Active-active distributed</strong></td>
      </tr>
      <tr>
          <td>Multi-region write</td>
          <td>不支援（async replica）</td>
          <td>不支援（async replica）</td>
          <td><strong>Strong consistency 多 region</strong></td>
      </tr>
      <tr>
          <td>Transaction model</td>
          <td>MVCC + snapshot isolation</td>
          <td>MVCC + snapshot isolation</td>
          <td><strong>OCC + strong snapshot isolation</strong></td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>任意</td>
          <td>AWS whitelist</td>
          <td><strong>無 extension 支援</strong></td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>全部自管</td>
          <td>AWS 管 storage / failover</td>
          <td>AWS 管全部、零接觸</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>Patroni 15-60s</td>
          <td>Aurora 30s</td>
          <td>N/A（永遠 active-active、無 failover 概念）</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>Self-managed instance</td>
          <td>Instance hour + storage</td>
          <td>Per-DPU + multi-AZ replication</td>
      </tr>
  </tbody>
</table>
<p><strong>Paradigm shift 的核心</strong>：</p>
<ol>
<li><strong>Transaction semantic</strong>：DSQL 用 OCC（Optimistic Concurrency Control）+ strong snapshot isolation、跟 PG 預設 read committed / repeatable read snapshot 不同 — 同 row 有 concurrent write 時、commit 階段才偵測衝突 + abort、application 要 handle <code>40001</code> serialization_failure</li>
<li><strong>No extension</strong>：PostGIS / pgvector / TimescaleDB / pg_partman 都不能用、依賴這些 feature 的 application 要拆出去</li>
<li><strong>No connection pool stateful</strong>：DSQL 內建 connection pool、application 不能依賴 session state（temp table / prepared statement / advisory lock）</li>
</ol>
<h2 id="schema-gappg-對-dsql-限制">Schema gap：PG 對 DSQL 限制</h2>
<p>DSQL 是 PG-compatible <em>subset</em>、有幾類功能不支援：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>PG 支援</th>
          <th>DSQL 支援</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Extension</td>
          <td>是</td>
          <td>否（沒 <code>CREATE EXTENSION</code>）</td>
      </tr>
      <tr>
          <td>Foreign key constraint</td>
          <td>是</td>
          <td>否（application 維護 referential integrity）</td>
      </tr>
      <tr>
          <td>View / Materialized view</td>
          <td>是</td>
          <td>View 部分 / Materialized view 否</td>
      </tr>
      <tr>
          <td>JSON / JSONB</td>
          <td>是</td>
          <td>部分（無 GIN index 加速）</td>
      </tr>
      <tr>
          <td>Foreign data wrapper</td>
          <td>是</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Stored procedure（PL/pgSQL）</td>
          <td>是</td>
          <td>部分（限制多）</td>
      </tr>
      <tr>
          <td>Trigger</td>
          <td>是</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>LISTEN / NOTIFY</td>
          <td>是</td>
          <td>否</td>
      </tr>
      <tr>
          <td><code>SELECT ... FOR UPDATE</code></td>
          <td>是</td>
          <td>部分（DSQL OCC semantic）</td>
      </tr>
      <tr>
          <td>Sequence（serial / identity）</td>
          <td>是</td>
          <td>支援、但高吞吐有 coordination overhead</td>
      </tr>
      <tr>
          <td>Table partition</td>
          <td>是</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>Logical replication slot</td>
          <td>是</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p><strong>Migration 必做 schema audit</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 找所有 extension 依賴
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_extension</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 找 materialized view
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">schemaname</span><span class="p">,</span><span class="w"> </span><span class="n">matviewname</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_matviews</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- 找 sequence
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_sequences</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 找 FDW
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_foreign_server</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 找 trigger
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_trigger</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="n">tgisinternal</span><span class="p">;</span></span></span></code></pre></div><p>任何項目命中、都是 migration blocker。</p>
<h2 id="operational-redesign">Operational Redesign</h2>
<p>跟 self-managed PG 或 Aurora PG 比、DSQL operational model 大幅簡化但語意不同：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>self-managed PG</th>
          <th>Aurora PG</th>
          <th>Aurora DSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage</td>
          <td>Local / EBS</td>
          <td>Shared 6 副本</td>
          <td>Distributed log + replicated state</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Patroni</td>
          <td>Aurora failover</td>
          <td>永遠 HA（無 failover 概念）</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>pgBackRest / WAL-G</td>
          <td>內建 continuous</td>
          <td>內建 continuous（更深整合）</td>
      </tr>
      <tr>
          <td>Connection pool</td>
          <td>PgBouncer / PgCat</td>
          <td>RDS Proxy 推薦</td>
          <td>內建（無需配置）</td>
      </tr>
      <tr>
          <td>Major version upgrade</td>
          <td>手動 + 停機</td>
          <td>Aurora blue/green</td>
          <td>完全 transparent（AWS 升）</td>
      </tr>
      <tr>
          <td>Read replica</td>
          <td>Streaming replication</td>
          <td>Reader endpoint</td>
          <td>無分（每 region 都讀寫）</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Prometheus / pg_stat_*</td>
          <td>CloudWatch + Performance Insights</td>
          <td>CloudWatch（簡化）</td>
      </tr>
      <tr>
          <td>預期 SRE FTE</td>
          <td>0.5-2</td>
          <td>0.2-0.5</td>
          <td>&lt; 0.1</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-流程type-e-phased-plan">Migration 流程：Type E Phased Plan</h2>
<p>Type E paradigm shift 的 phased plan、跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">migrate-to-cockroachdb</a> 結構類似：</p>
<h3 id="phase-1schema--application-audit">Phase 1：Schema / Application Audit</h3>
<ul>
<li>跑 schema audit（extension / MV / FDW / sequence / trigger）</li>
<li>識別 application 哪些 query / transaction pattern 需重設計</li>
<li>估算 <em>能直接遷的 % vs 需重寫的 %</em>、典型 60-80% / 20-40%</li>
</ul>
<h3 id="phase-2application-改造不上-dsql先在-pg-跑">Phase 2：Application 改造（不上 DSQL、先在 PG 跑）</h3>
<ul>
<li>加 transaction retry middleware（攔截 <code>40001</code>、exponential backoff）</li>
<li>用 UUID 替代 serial / bigserial</li>
<li>移除依賴 LISTEN/NOTIFY 的功能（改 SQS / EventBridge）</li>
<li>移除 materialized view（改 application-side cache 或 incremental ETL）</li>
<li>Stored procedure 改 application code</li>
<li>在 PG 上跑 staging、確認新 application code 還對</li>
</ul>
<h3 id="phase-3dsql-cluster-建立--schema-遷">Phase 3：DSQL Cluster 建立 + Schema 遷</h3>
<ul>
<li>DSQL cluster create</li>
<li>DDL apply（subset of PG schema、無 extension）</li>
<li>DMS（Database Migration Service）initial load + ongoing replication</li>
<li>兩邊跑 shadow traffic、比對 query 結果</li>
</ul>
<h3 id="phase-4cutover">Phase 4：Cutover</h3>
<ul>
<li>Application 切 connection string 到 DSQL</li>
<li>保留 PG read-only 一週、出狀況 rollback</li>
<li>Monitor <code>40001</code> retry rate、scaling event 行為</li>
</ul>
<h3 id="phase-5多-region-拓展如適用">Phase 5：多 region 拓展（如適用）</h3>
<ul>
<li>加第二 region endpoint</li>
<li>Application 改 multi-region routing（latency-based）</li>
<li>Test region failure / network partition 行為</li>
</ul>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1transaction-retry-沒處理">Case 1：Transaction Retry 沒處理</h3>
<p><strong>情境</strong>：PG 上「兩個 transaction 都 update 同 row」走 lock + wait；DSQL 同情境一個會收 <code>40001 serialization_failure</code>、application 沒 catch、user 看到 500 error。</p>
<p>修法：</p>
<ul>
<li>DAO 層加 retry middleware：catch <code>40001</code> + exponential backoff（jitter）</li>
<li>Retry 上限 3-5 次、超過回 4xx 給 user</li>
<li>Transaction 內不要做 side effect（API call / message send）、retry 會重做</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">with_retry</span><span class="p">(</span><span class="n">fn</span><span class="p">,</span> <span class="n">max_attempts</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">for</span> <span class="n">attempt</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">max_attempts</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">            <span class="k">return</span> <span class="n">fn</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">except</span> <span class="n">SerializationError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">            <span class="k">if</span> <span class="n">attempt</span> <span class="o">==</span> <span class="n">max_attempts</span> <span class="o">-</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">                <span class="k">raise</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">((</span><span class="mi">2</span> <span class="o">**</span> <span class="n">attempt</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.05</span> <span class="o">+</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">*</span> <span class="mf">0.05</span><span class="p">)</span></span></span></code></pre></div><h3 id="case-2extension-缺位feature-整段掉">Case 2：Extension 缺位、Feature 整段掉</h3>
<p><strong>情境</strong>：production PG 用 pgvector 做 RAG search、PostGIS 做 store locator、TimescaleDB 做 metrics — 切 DSQL 後三 feature 全沒。</p>
<p>修法：</p>
<ul>
<li>不要直接遷、評估 <em>which extension is load-bearing</em></li>
<li>pgvector → 外掛 Pinecone / Weaviate 或保留 PG 跑 vector workload</li>
<li>PostGIS → 保留 PG 跑 GIS workload</li>
<li>TimescaleDB → 切 Amazon Timestream 或保留 PG</li>
<li>DSQL 只放 <em>不依賴 extension</em> 的 transactional core</li>
</ul>
<p>實務常見拓撲：DSQL 跑 transactional core、附 PG（vector） + PG（GIS） + Timestream（metrics）。</p>
<h3 id="case-3sequence-高吞吐撞-coordination-overhead">Case 3：Sequence 高吞吐撞 Coordination Overhead</h3>
<p><strong>情境</strong>：<code>SERIAL</code> / <code>GENERATED AS IDENTITY</code> PK 在 DSQL 用、insert 量 1000+/s 時 sequence nextval 變成 bottleneck、insert latency 從 5ms 跳到 80-100ms+。</p>
<p>DSQL 有支援 sequence、但不是「local atomic counter」、是分散式 counter — 每次 nextval 需跨 region coordination 保證唯一性。低吞吐 OK、高吞吐撞牆。</p>
<p>修法：</p>
<ul>
<li>高吞吐表 PK 換 UUID v7（time-sortable、無 coordination）：<code>gen_random_uuid()</code> 或 application-side UUID v7 library</li>
<li>或 application-side ULID（time-sortable、12-byte 緊湊）</li>
<li>完全避免依賴「連續 integer PK」的 application 邏輯（reporting / paging 改用 <code>ORDER BY created_at, id</code>）</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 換 UUID PK
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="n">UUID</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">gen_random_uuid</span><span class="p">(),</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>低吞吐表（settings / config）保留 sequence OK；high-volume transactional 表（orders / events）建議 UUID。</p>
<h3 id="case-4aurora-pg-直升-dsql-想當-in-place">Case 4：Aurora PG 直升 DSQL 想當 in-place</h3>
<p><strong>情境</strong>：team 以為「Aurora PG 跟 Aurora DSQL 都是 Aurora、應該能直升」、申請 cluster modify、發現完全是兩個 service。</p>
<p>修法：</p>
<ul>
<li>不是 in-place upgrade、是 full migration（DMS + cutover）</li>
<li>把 DSQL 當完全新的 cluster type、走 Phase 1-4 完整流程</li>
<li>Aurora PG → Aurora DSQL 不比 PG → CRDB 容易、wire-compatible 只解 application connect 問題、不解 schema / paradigm 差異</li>
</ul>
<h3 id="case-5region-failover-semantic">Case 5：Region Failover Semantic</h3>
<p><strong>情境</strong>：team 以為「DSQL multi-region 等於高可用」、設計時假設「整 region 掛還是能寫」、實測發現「網絡分割時 DSQL 走 quorum、可能 reject write」。</p>
<p>DSQL 是 strong consistency 多 region、CAP 取 CP（不是 AP）—  network partition 時部分 region 會拒絕 write、不是「永遠可寫」。</p>
<p>修法：</p>
<ul>
<li>設計 application 要 handle write reject（partition recovery 後 retry）</li>
<li>不要把 DSQL 當「永遠可寫」的 cache 或 queue 用</li>
<li>真要 AP 行為、用 DynamoDB（global table）</li>
</ul>
<h2 id="capacity-規劃">Capacity 規劃</h2>
<p>DSQL 計費跟 Aurora PG 差很多：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>Aurora PG</th>
          <th>Aurora DSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Instance</td>
          <td>Per-instance hour</td>
          <td>無（serverless）</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>Per-GB-month</td>
          <td>Per-GB-month（多副本價）</td>
      </tr>
      <tr>
          <td>IO</td>
          <td>Per-million IO</td>
          <td>每 transaction 計費</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>Per-GB-month</td>
          <td>內建（無額外）</td>
      </tr>
      <tr>
          <td>Multi-region</td>
          <td>Cross-region replica（額外）</td>
          <td>每 region 全費 × N</td>
      </tr>
  </tbody>
</table>
<p>實務 cost：Aurora PG db.r6g.4xlarge multi-AZ 月 ~$2000 → DSQL 同 workload ~$5000-10000（依 region 數）。</p>
<p>何時 DSQL cost 划算：</p>
<ul>
<li>多 region active-active 需求剛性（不是 nice-to-have）</li>
<li>Operational FTE 節省超過 cost 差</li>
<li>Burst workload（DSQL 自動 scale、Aurora PG 預配置 idle 期浪費）</li>
</ul>
<h2 id="跟既有-migration-playbook-對比">跟既有 Migration Playbook 對比</h2>
<table>
  <thead>
      <tr>
          <th>Migration</th>
          <th>Type</th>
          <th>主結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">→ Aurora PG</a></td>
          <td>C</td>
          <td>Protocol drop-in + operational redesign</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">→ CockroachDB</a></td>
          <td>E</td>
          <td>Paradigm shift（distributed SQL）</td>
      </tr>
      <tr>
          <td>→ Aurora DSQL（本篇）</td>
          <td>E</td>
          <td>Paradigm shift（PG-compatible distributed）</td>
      </tr>
  </tbody>
</table>
<p><strong>Aurora DSQL vs CockroachDB 選擇</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Aurora DSQL</th>
          <th>CockroachDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PG compatibility</td>
          <td>Wire-compatible 較完整</td>
          <td>高、但有差異</td>
      </tr>
      <tr>
          <td>Vendor lock-in</td>
          <td>AWS only</td>
          <td>跨雲 / on-prem</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>AWS pricing</td>
          <td>自管或 CockroachDB Cloud</td>
      </tr>
      <tr>
          <td>Multi-region 模型</td>
          <td>Strong consistency 內建</td>
          <td>可配置（regional / global table）</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>完全沒</td>
          <td>部分（CDC / changefeed）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Zero-touch</td>
          <td>自管或 managed</td>
      </tr>
  </tbody>
</table>
<p>選 DSQL：已綁 AWS、不想管基礎設施、需 PG semantic。
選 CRDB：跨雲、有自管 SRE、需要 fine-grained control。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">migrate-to-aurora</a>：Aurora PG 對比（Type C）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">migrate-to-cockroachdb</a>：CRDB 對比（Type E）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a>：DSQL 不支援的 extension</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/connection-scaling/" data-link-title="PostgreSQL Connection Scaling：process-per-connection model 跟為什麼 pooler 是必裝" data-link-desc="PG 每個 client connection fork 一個 backend process（不是 thread）、RAM 成本 5-15MB/connection、context switch 跟 fork() cost 在 100&#43; connection 後線性放大、所以 pooler 不是 *optional optimization* 而是 *production prerequisite*。本文走 process-per-connection model 跟 MySQL thread-per-connection 對比、max_connections &#43; shared_buffers &#43; work_mem 三 GUC 互動、application-side pool vs middleware pool vs RDS Proxy 三層選擇、5 production 踩雷（connection storm / fork() cost 在 burst 流量 / shared_buffers 跟 connection 數壓縮 / double-pool 配置錯誤 / max_connections 設太大反而慢）、跟 PgBouncer config 互補不重複">connection-scaling</a>：DSQL 內建 pool 跟 PgBouncer 對比</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <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 overview</a> 認識 Aurora family</li>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">migrate-to-cockroachdb</a> 對比另一個 Type E migration</li>
<li>回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a> 看全圖</li>
</ul>
]]></content:encoded></item><item><title>3.C42 Bitso：Reliable Redis Streams 抽象 + 自建 DLQ</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 沒有原生 DLQ、要在 application 層自建抽象。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Bitso 的 Order Engine 微服務需要 thousands of messages/sec/stream + 亞毫秒延遲、撐住 BTC 價格暴動的流量尖峰；先後評估 Kafka（latency）跟 SQS（vendor lock-in + latency）後選 Redis Streams、團隊本來就熟 Redis、已在 mission-critical service 跑超過半年。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>自建 &amp;ldquo;Reliable Redis Streams&amp;rdquo; 抽象層（StreamRedisOperations adapter / ReliableStream interface / MessageReadingLoop）封裝 readMessages + readPendingMessages、加上 Redis Streams 沒有原生支援的 DLQ（N 次 retry 後路由）、走 idempotent processing 接受重複勝過遺失。揭露 Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Sentinel + Cluster 可靠性。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/bitso-engineering/the-redis-streams-we-have-known-and-loved-e9e596d49a22">The Redis Streams We Have Known and Loved&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 沒有原生 DLQ、要在 application 層自建抽象。</p>
<h2 id="觀察">觀察</h2>
<p>Bitso 的 Order Engine 微服務需要 thousands of messages/sec/stream + 亞毫秒延遲、撐住 BTC 價格暴動的流量尖峰；先後評估 Kafka（latency）跟 SQS（vendor lock-in + latency）後選 Redis Streams、團隊本來就熟 Redis、已在 mission-critical service 跑超過半年。</p>
<h2 id="判讀">判讀</h2>
<p>自建 &ldquo;Reliable Redis Streams&rdquo; 抽象層（StreamRedisOperations adapter / ReliableStream interface / MessageReadingLoop）封裝 readMessages + readPendingMessages、加上 Redis Streams 沒有原生支援的 DLQ（N 次 retry 後路由）、走 idempotent processing 接受重複勝過遺失。揭露 Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Sentinel + Cluster 可靠性。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/bitso-engineering/the-redis-streams-we-have-known-and-loved-e9e596d49a22">The Redis Streams We Have Known and Loved</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB&lt;/a>。本文是 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 多重歸類跟 tie-breaking&lt;/a> 規則的實證 — 三維皆 High 配對的處理方式不是「選 type A 或 type C 或 type E」、是 &lt;em>主導維度走 Type E、其他高維度獨立加段&lt;/em>。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關。&lt;/p>&lt;/blockquote>
&lt;h2 id="三維皆-high決策矩陣">三維皆 High：決策矩陣&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 對 PostgreSQL → CockroachDB：&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>Schema / API&lt;/td>
 &lt;td>PostgreSQL wire protocol 兼容、但 SQL feature set 部分缺（CTE recursive 部分 / window function 部分 / extension 完全缺）&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>Single-node + Patroni → distributed Raft + 自動 rebalance；HA / backup / topology 全換&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>Single-node MVCC + transaction → distributed Serializable Snapshot Isolation (SSI)&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>同 1 個 DB cluster&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Transaction retry pattern 必須改、ORM 可能需 patch&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>3 維 High + 1 維 Medium。按 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">methodology audit Step 5&lt;/a> 的多重歸類處理規則：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> 跟 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a>。本文是 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 多重歸類跟 tie-breaking</a> 規則的實證 — 三維皆 High 配對的處理方式不是「選 type A 或 type C 或 type E」、是 <em>主導維度走 Type E、其他高維度獨立加段</em>。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關。</p></blockquote>
<h2 id="三維皆-high決策矩陣">三維皆 High：決策矩陣</h2>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 對 PostgreSQL → CockroachDB：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>PostgreSQL wire protocol 兼容、但 SQL feature set 部分缺（CTE recursive 部分 / window function 部分 / extension 完全缺）</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Single-node + Patroni → distributed Raft + 自動 rebalance；HA / backup / topology 全換</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>Single-node MVCC + transaction → distributed Serializable Snapshot Isolation (SSI)</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個 DB cluster</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Transaction retry pattern 必須改、ORM 可能需 patch</td>
          <td>Medium</td>
      </tr>
  </tbody>
</table>
<p>3 維 High + 1 維 Medium。按 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">methodology audit Step 5</a> 的多重歸類處理規則：</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">主導維度判讀 (優先序): Schema &gt; Paradigm &gt; Operational &gt; Components
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">實際應用: Schema High + Paradigm High + Operational High
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Schema 是 High、但 CRDB 提供 PostgreSQL wire protocol 兼容
</span></span><span class="line"><span class="ln">5</span><span class="cl">- Paradigm 是 High、是 *單機 → 分散式* 的根本轉變、讀者最關心
</span></span><span class="line"><span class="ln">6</span><span class="cl">- Operational 是 High、但很大程度是 Paradigm 的 downstream
</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">→ 主結構選 Paradigm（Type E）、Schema + Operational 抽獨立段補充</span></span></code></pre></div><p>不強迫單一 type 標籤 — 本文是 <em>Type E 為主 + Type A / C 高維度增補</em> 的 multi-axis 形態。</p>
<h2 id="結構-differentiatortype-e-主結構--多軸增補段">結構 differentiator：Type E 主結構 + 多軸增補段</h2>
<p>跟前批 5 個 migration playbook 對照：</p>
<table>
  <thead>
      <tr>
          <th>結構元素</th>
          <th>Type A Splunk → Elastic</th>
          <th>Type B Redis → DragonflyDB</th>
          <th>Type C PostgreSQL → Aurora</th>
          <th>Type D Datadog → Grafana</th>
          <th>Type E Kafka ↔ NATS</th>
          <th><strong>本文（三維 High）</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Phased translation</td>
          <td>yes</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>partial</td>
      </tr>
      <tr>
          <td>Compatibility audit</td>
          <td>-</td>
          <td>yes</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Operational redesign 對位</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
          <td>-</td>
          <td>-</td>
          <td><strong>yes（獨立段）</strong></td>
      </tr>
      <tr>
          <td>Schema gap 對位</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td><strong>yes（獨立段）</strong></td>
      </tr>
      <tr>
          <td>Parallel streams</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
          <td>-</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Paradigm contrast</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Application 重設計</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>混合架構 long-term</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>yes</td>
          <td>partial（部分 workload）</td>
      </tr>
  </tbody>
</table>
<p>本文是「Type E 為主 + Type A schema gap 段 + Type C operational redesign 段」混合形態、9-10 章節、260-300 行。</p>
<h2 id="維度-1paradigm-shift主導">維度 1：Paradigm shift（主導）</h2>
<p>CRDB 是 <em>distributed SQL DB</em>、不是「PostgreSQL 多節點版」。核心差異：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>PostgreSQL</th>
          <th>CockroachDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Transaction isolation</td>
          <td>MVCC、Read Committed default</td>
          <td>Serializable Snapshot Isolation (SSI)、強一致</td>
      </tr>
      <tr>
          <td>Transaction conflict</td>
          <td>First writer wins</td>
          <td>Retry-on-conflict、application 必須處理 <code>40001</code> retry code</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Streaming replication + standby</td>
          <td>Raft consensus、每筆寫 quorum + 自動 rebalance</td>
      </tr>
      <tr>
          <td>Partition</td>
          <td>Declarative partitioning（手動）</td>
          <td>Automatic range-based + locality-aware</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>1-10ms（單 region）</td>
          <td>5-50ms（cross-AZ Raft quorum）</td>
      </tr>
      <tr>
          <td>Throughput limit</td>
          <td>單 primary 上限 ~10-50K TPS</td>
          <td>Linear scale by adding node、~5K TPS / node</td>
      </tr>
  </tbody>
</table>
<p>關鍵 paradigm 改變：<em>transaction 是 retry-able 操作、不是 atomic guaranteed</em>。所有 transaction code 需要包 retry loop（CRDB 提供 <code>cockroach_restart</code> savepoint）。</p>
<h2 id="維度-2schema-gappostgresql-features-crdb-不支援">維度 2：Schema gap（PostgreSQL features CRDB 不支援）</h2>
<p>CRDB 號稱 PostgreSQL-compatible、但 <em>covergence rate 80-90%</em>；常見 gap：</p>
<table>
  <thead>
      <tr>
          <th>PostgreSQL feature</th>
          <th>CRDB 狀態</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stored procedure / function (PL/pgSQL)</td>
          <td>Limited（CRDB 22.2+ 部分支援）</td>
          <td>Migration scope 內必須 audit + 改寫</td>
      </tr>
      <tr>
          <td>Common Table Expression (CTE) recursive</td>
          <td>Limited (depth + structure)</td>
          <td>複雜 CTE 可能跑不通、必須 query refactor</td>
      </tr>
      <tr>
          <td>Window function 全集</td>
          <td>Partial</td>
          <td>報表 query 需逐 case 驗證</td>
      </tr>
      <tr>
          <td>Extensions (pg_repack / pgaudit / TimescaleDB)</td>
          <td><strong>不支援</strong></td>
          <td>用 CRDB 自家 alternative 或自管 application 層</td>
      </tr>
      <tr>
          <td>Triggers</td>
          <td>Limited</td>
          <td>Audit / data integrity 邏輯遷到 application 層</td>
      </tr>
      <tr>
          <td>Custom types / domain</td>
          <td>Partial</td>
          <td>用 CHECK constraint 替代</td>
      </tr>
      <tr>
          <td>Geographic types (PostGIS)</td>
          <td>CRDB native geo support（語法不同）</td>
          <td>Spatial query 改寫</td>
      </tr>
      <tr>
          <td><code>SELECT FOR UPDATE</code> semantics</td>
          <td>對等但底層機制不同（distributed lock）</td>
          <td>注意 deadlock pattern 差異</td>
      </tr>
      <tr>
          <td>Advisory locks</td>
          <td><strong>不支援</strong></td>
          <td>Application 端用其他 distributed lock（Redis / Consul）</td>
      </tr>
  </tbody>
</table>
<p>Migration 必須 <em>先 audit 完整 SQL feature 使用</em>、列出 gap、評估解法或退役。</p>
<h2 id="維度-3operational-redesign">維度 3：Operational redesign</h2>
<p>CRDB operational model 完全不同：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>PostgreSQL self-managed</th>
          <th>CRDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>Patroni / Stolon + manual</td>
          <td><code>cockroach init</code> + 自動 Raft formation</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Patroni + DCS + watchdog</td>
          <td>內建 Raft、無 single primary</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>Patroni-managed、15-60s</td>
          <td>透明 Raft re-election、&lt; 5s</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>pgBackRest + WAL archive</td>
          <td><code>BACKUP TO</code> (incremental + full)</td>
      </tr>
      <tr>
          <td>Restore</td>
          <td><code>pgBackRest restore</code> + PITR</td>
          <td><code>RESTORE FROM</code></td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Streaming + logical</td>
          <td>Built-in、無 logical replication 對等概念</td>
      </tr>
      <tr>
          <td>Schema migration</td>
          <td><code>pg_dump</code> / Flyway / Liquibase</td>
          <td><code>cockroach sql</code> + online schema change（無 lock）</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>pg_stat_* views + Prometheus exporter</td>
          <td>CRDB admin UI + Prometheus（schema 不同）</td>
      </tr>
      <tr>
          <td>Sizing</td>
          <td>Vertical scale（單 node big spec）</td>
          <td>Horizontal scale（多 node 小 spec）</td>
      </tr>
  </tbody>
</table>
<p>SRE 心智模型完全重訓：<em>無 primary 概念 / 無 streaming lag 概念 / 無 standby promote 概念</em>。</p>
<h2 id="migration-流程混合形態">Migration 流程（混合形態）</h2>
<p>不是線性 phased、是 <em>phased + parallel + partial</em> 混合：</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">Phase 0: scope 判讀
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  - 列 application、區分「適合 CRDB」vs「保留 PostgreSQL」
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  - SQL feature audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  - Application transaction pattern audit
</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">Phase 1: schema port + application 改寫
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  - DDL 轉成 CRDB syntax
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  - 不支援 extension 找 alternative
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  - Application transaction code 加 retry loop
</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">Phase 2: 雙寫期（部分 application 開始走 CRDB）
</span></span><span class="line"><span class="ln">12</span><span class="cl">  - 新 application 走 CRDB
</span></span><span class="line"><span class="ln">13</span><span class="cl">  - 舊 application 持續 PostgreSQL
</span></span><span class="line"><span class="ln">14</span><span class="cl">  - CDC bridge（Debezium → Kafka → CRDB consumer）
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">Phase 3: cutover 適合的 application
</span></span><span class="line"><span class="ln">17</span><span class="cl">  - 每個 application 獨立 cutover
</span></span><span class="line"><span class="ln">18</span><span class="cl">  - 不是「全 DB 一次切」
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">Phase 4: 長期混合架構
</span></span><span class="line"><span class="ln">21</span><span class="cl">  - 某些 workload 永遠保留 PostgreSQL（不適合分散式）
</span></span><span class="line"><span class="ln">22</span><span class="cl">  - CRDB 跑 distributed 適配 workload</span></span></code></pre></div><p>整體 3-6 個月、不收斂到全 CRDB。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1transaction-retry-沒處理application-大量-40001-error">Case 1：Transaction retry 沒處理、application 大量 <code>40001</code> error</h3>
<p><strong>徵兆</strong>：cutover 後 application 5-10% transaction 報 <code>restart transaction: TransactionRetryWithProtoRefreshError</code>、業務 fail。</p>
<p><strong>根因</strong>：PostgreSQL Read Committed 不要求 application 處理 conflict、CRDB Serializable Isolation 必須 <em>retry-on-conflict</em>；application code 沒 retry loop。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// CRDB transaction with retry</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">for</span> <span class="nx">retries</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">retries</span> <span class="p">&lt;</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">retries</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1">// ... transaction logic ...</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="o">&amp;&amp;</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">(),</span> <span class="s">&#34;40001&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nf">backoff</span><span class="p">(</span><span class="nx">retries</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">break</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>framework-level：用 CRDB-provided client lib（go-cockroachdb / crdb-jdbc）有 retry helper。</p>
<h3 id="case-2extension-缺位application-feature-整段掉">Case 2：Extension 缺位、application feature 整段掉</h3>
<p><strong>徵兆</strong>：cutover 後 application 某個地理計算功能直接報錯、PostGIS 函數不存在；migrate 計畫漏看。</p>
<p><strong>根因</strong>：CRDB native geo 不同 syntax / API、PostGIS extension 不能直接搬。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 必跑 extension audit</strong>：列所有 <code>pg_extension</code>、找對應 CRDB feature 或退役</li>
<li><strong>PostGIS 替代</strong>：CRDB native ST_* functions、部分 syntax 對齊但 spatial index 不同</li>
<li><strong>退役不能換的 feature</strong>：評估保留 PostgreSQL（混合架構）</li>
</ol>
<h3 id="case-3sequential-pk-撞-raft-quorum-瓶頸">Case 3：Sequential PK 撞 Raft quorum 瓶頸</h3>
<p><strong>徵兆</strong>：cutover 後寫入吞吐量 / latency 不如預期、CRDB cluster CPU &lt; 30% 但 write latency p99 high。</p>
<p><strong>根因</strong>：application 用 <code>AUTO_INCREMENT</code> / <code>SERIAL</code> 連續 PK；CRDB 把連續 key 放 <em>同一 range</em> / 同一 Raft group、寫入串行化、無法平行 scale。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>改 UUID v7 / <code>unique_rowid()</code></strong>：時序排序但散佈跨 range、自動 partition by hash</li>
<li><strong><code>PRIMARY KEY (region, id)</code></strong>：multi-region 場景 multi-tenancy 自然拆分</li>
<li><strong>不適合的 workload 留 PostgreSQL</strong>：不是所有 schema 都適合 distributed</li>
</ol>
<h3 id="case-4long-transaction-對-raft-衝擊">Case 4：Long transaction 對 Raft 衝擊</h3>
<p><strong>徵兆</strong>：跨 1 分鐘+ 的 transaction（batch processing / 大 ETL）大量 retry、最後失敗；同期間其他短 transaction 也 retry rate 上升。</p>
<p><strong>根因</strong>：CRDB long transaction holds intent on touched ranges、阻塞其他 transaction；SSI conflict 機率隨 transaction 時間平方增長。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Long transaction 拆短</strong>：batch 用多個 short transaction、checkpoint 在 application 層</li>
<li><strong>Heavy ETL 不跑 CRDB</strong>：用 CRDB CDC export 到 OLAP（Snowflake / BigQuery）跑 batch</li>
<li><strong>Read-only long transaction 用 follower read</strong>：<code>AS OF SYSTEM TIME</code> 不 hold intent、適合 reporting</li>
</ol>
<h3 id="case-5backup--restore-行為跟-postgresql-不同sre-runbook-失效">Case 5：Backup / restore 行為跟 PostgreSQL 不同、SRE runbook 失效</h3>
<p><strong>徵兆</strong>：DBA 嘗試 <code>pg_restore</code> 失敗、CRDB 端 backup format 完全不同；incident response 卡關 1-2 小時。</p>
<p><strong>根因</strong>：CRDB backup 是 <em>cluster-internal format</em>、不能用 PostgreSQL tooling；SRE runbook 仍是 PostgreSQL world、應急時心智模型錯位。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Runbook 重寫</strong>：CRDB-specific backup / restore 流程、SRE training</li>
<li><strong>DR drill</strong>：cutover 前跑完整 DR drill、用 CRDB tooling 完成、不依賴 PostgreSQL 經驗</li>
<li><strong>Multi-region backup</strong>：CRDB 跨 region backup 配置、避免單 region 故障</li>
</ol>
<h2 id="capacity-規劃">Capacity 規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL self-managed</th>
          <th>CockroachDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-node 上限</td>
          <td>~10-50K TPS（vertical scale 到 32-128 vCPU）</td>
          <td>~5K TPS / node（horizontal scale by adding node）</td>
      </tr>
      <tr>
          <td>跨 region</td>
          <td>高 latency 跨區 streaming</td>
          <td>設計 native、Locality-aware queries</td>
      </tr>
      <tr>
          <td>Sharding</td>
          <td>手動 partition / pg_partman</td>
          <td>自動 range-based</td>
      </tr>
      <tr>
          <td>Storage / TPS ratio</td>
          <td>不變</td>
          <td>Storage 跨 node 3x（Raft quorum 3-replica default）</td>
      </tr>
      <tr>
          <td>Total cost (10TB)</td>
          <td>$2-4K USD / month（self-managed）</td>
          <td>$5-10K USD / month（CRDB Cloud + 3x storage）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：CRDB cost 顯著高、選 CRDB 必須是 <em>paradigm 需求</em>（distributed transaction / multi-region / linear scale）；單純成本 / availability 改善走 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">Aurora</a> 更划算。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql--aurora-migration-對比">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora migration</a> 對比</h3>
<p>兩條 PostgreSQL 出路：</p>
<ul>
<li><strong>Aurora</strong>：operational simplification、protocol drop-in、cost 中等漲；適合 <em>不需 distributed transaction</em> 的 production</li>
<li><strong>CRDB</strong>：distributed paradigm shift、application 必須改、cost 顯著漲；適合 <em>真的需要 distributed</em> 的 workload</li>
</ul>
<p>多數 application 不需要 distributed transaction、Aurora 更合理；真正需要 cross-region 強一致 / linear scale by adding node 才走 CRDB。</p>
<h3 id="跟-application-transaction-pattern-重設計">跟 application transaction pattern 重設計</h3>
<p>CRDB 強制 application 改 transaction code、retry loop 必加。團隊心智模型轉換是 migration 主要 effort、技術部分相對少。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>CRDB → PostgreSQL reverse migration</strong>：當業務 simplify 後 distributed 不必要、reverse migration cost 高、實務上 CRDB 是 <em>single-direction lock-in</em></li>
<li><strong>CRDB Serverless</strong>：cost 起點低、burst workload 適合；steady workload 仍是 dedicated cluster</li>
<li><strong>Multi-region active-active</strong>：CRDB 真正強項、但網路成本爆、僅金融 / 政府客戶 ROI 合理</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> / <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a></li>
<li>對位 migration：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a>（另一條 PostgreSQL 出路）</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> / <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定</a>（本文驗證 <em>多重歸類 multi-axis 處理</em>）</li>
</ul>
]]></content:encoded></item><item><title>3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/</guid><description>&lt;p>Arcjet 用 Redis Streams 取代 Kafka 的案例揭露了中小規模場景下「Kafka 的 managed 成本 vs Redis Streams 的運維成本」的具體取捨 — 省下六位數年費的代價是自寫 retention 治理跟監控工具。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Arcjet 是 security / bot detection 平台，處理每個 HTTP request 的安全判斷。核心需求是 low-latency 的請求處理 — 安全判斷要在幾毫秒內完成，不能拖慢使用者的 request。&lt;/p>
&lt;p>系統架構中有一段 event-driven pipeline 負責把安全事件從 detection layer 傳遞到 analytics 跟 alerting。原本評估用 Kafka 做這段 pipeline，但 managed Kafka 的年費落在六位數美金 — 對 Arcjet 的流量規模跟業務階段，這個成本不合理。&lt;/p>
&lt;p>Arcjet 的基礎設施已經有 Redis 做 cache。把 Redis 從純 cache 升級到 cache + Streams，利用既有的 Redis infrastructure 承擔 event pipeline，總成本約 $1k/year。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="redis-streams-沒有自動-retention">Redis Streams 沒有自動 retention&lt;/h3>
&lt;p>Kafka 的 retention 是內建功能 — 設定 &lt;code>log.retention.hours&lt;/code> 後 broker 自動刪除到期資料。Redis Streams 沒有內建的自動 retention — stream 資料會持續累積，直到手動 &lt;code>XTRIM&lt;/code> 或 &lt;code>XDEL&lt;/code>。&lt;/p>
&lt;p>在生產環境下，不處理 retention 意味著 Redis 的記憶體持續成長，最終觸發 eviction policy 或 OOM。對 Arcjet 來說 Redis 同時做 cache 跟 Streams，Streams 的記憶體成長會擠壓 cache 的可用空間。&lt;/p>
&lt;h3 id="consumer-group-進度追蹤">Consumer group 進度追蹤&lt;/h3>
&lt;p>Redis Streams 的 consumer group 會追蹤每個 consumer 的讀取進度（last delivered ID）。做 &lt;code>XTRIM&lt;/code> 時需要確保不刪除尚未被所有 consumer group 確認的訊息 — 否則 consumer 會丟失未處理的事件。&lt;/p>
&lt;p>Kafka 的 log compaction 跟 retention 自動處理這個問題（consumer offset 以前的 segment 才會被清理）。Redis Streams 需要 application 自己確認所有 consumer group 的進度，再決定 trim 的位置。&lt;/p>
&lt;h3 id="單機-redis-的可靠性邊界">單機 Redis 的可靠性邊界&lt;/h3>
&lt;p>Redis 的持久化機制（RDB snapshot + AOF）提供的是 best-effort 的持久性，跟 Kafka 的 replication-based 持久化保證不同。Redis crash + restart 時，AOF 的最後幾筆寫入可能遺失（取決於 &lt;code>appendfsync&lt;/code> 設定）。&lt;/p>
&lt;p>對 Arcjet 的安全事件場景，偶爾丟失幾筆事件可以接受（security detection 的結果是即時判斷，事後的 analytics 容忍小量遺失）。如果場景是金融交易或 audit log，這個可靠性邊界就不夠。&lt;/p>
&lt;h2 id="解法與取捨">解法與取捨&lt;/h2>
&lt;h3 id="自建-janitor-process">自建 Janitor process&lt;/h3>
&lt;p>Arcjet 自寫了一個 Janitor process 處理 Redis Streams 的 retention：&lt;/p></description><content:encoded><![CDATA[<p>Arcjet 用 Redis Streams 取代 Kafka 的案例揭露了中小規模場景下「Kafka 的 managed 成本 vs Redis Streams 的運維成本」的具體取捨 — 省下六位數年費的代價是自寫 retention 治理跟監控工具。</p>
<h2 id="業務背景">業務背景</h2>
<p>Arcjet 是 security / bot detection 平台，處理每個 HTTP request 的安全判斷。核心需求是 low-latency 的請求處理 — 安全判斷要在幾毫秒內完成，不能拖慢使用者的 request。</p>
<p>系統架構中有一段 event-driven pipeline 負責把安全事件從 detection layer 傳遞到 analytics 跟 alerting。原本評估用 Kafka 做這段 pipeline，但 managed Kafka 的年費落在六位數美金 — 對 Arcjet 的流量規模跟業務階段，這個成本不合理。</p>
<p>Arcjet 的基礎設施已經有 Redis 做 cache。把 Redis 從純 cache 升級到 cache + Streams，利用既有的 Redis infrastructure 承擔 event pipeline，總成本約 $1k/year。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="redis-streams-沒有自動-retention">Redis Streams 沒有自動 retention</h3>
<p>Kafka 的 retention 是內建功能 — 設定 <code>log.retention.hours</code> 後 broker 自動刪除到期資料。Redis Streams 沒有內建的自動 retention — stream 資料會持續累積，直到手動 <code>XTRIM</code> 或 <code>XDEL</code>。</p>
<p>在生產環境下，不處理 retention 意味著 Redis 的記憶體持續成長，最終觸發 eviction policy 或 OOM。對 Arcjet 來說 Redis 同時做 cache 跟 Streams，Streams 的記憶體成長會擠壓 cache 的可用空間。</p>
<h3 id="consumer-group-進度追蹤">Consumer group 進度追蹤</h3>
<p>Redis Streams 的 consumer group 會追蹤每個 consumer 的讀取進度（last delivered ID）。做 <code>XTRIM</code> 時需要確保不刪除尚未被所有 consumer group 確認的訊息 — 否則 consumer 會丟失未處理的事件。</p>
<p>Kafka 的 log compaction 跟 retention 自動處理這個問題（consumer offset 以前的 segment 才會被清理）。Redis Streams 需要 application 自己確認所有 consumer group 的進度，再決定 trim 的位置。</p>
<h3 id="單機-redis-的可靠性邊界">單機 Redis 的可靠性邊界</h3>
<p>Redis 的持久化機制（RDB snapshot + AOF）提供的是 best-effort 的持久性，跟 Kafka 的 replication-based 持久化保證不同。Redis crash + restart 時，AOF 的最後幾筆寫入可能遺失（取決於 <code>appendfsync</code> 設定）。</p>
<p>對 Arcjet 的安全事件場景，偶爾丟失幾筆事件可以接受（security detection 的結果是即時判斷，事後的 analytics 容忍小量遺失）。如果場景是金融交易或 audit log，這個可靠性邊界就不夠。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<h3 id="自建-janitor-process">自建 Janitor process</h3>
<p>Arcjet 自寫了一個 Janitor process 處理 Redis Streams 的 retention：</p>
<ol>
<li>定期檢查每個 stream 的長度（<code>XLEN</code>）</li>
<li>查詢所有 consumer group 的 pending entry list（PEL）跟最後確認位置</li>
<li>計算安全的 trim 位置（所有 consumer group 都已確認的最舊 ID）</li>
<li>執行 <code>XTRIM stream MINID &lt;safe-id&gt;</code> 刪除已確認的舊資料</li>
</ol>
<p>Janitor 的執行頻率根據實際處理速度（~100 msgs/min）設定 — 不需要非常頻繁，但不能完全不跑。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Managed Kafka</th>
          <th>Redis Streams + Janitor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>年成本</td>
          <td>六位數 USD</td>
          <td>~$1k USD</td>
      </tr>
      <tr>
          <td>Retention 管理</td>
          <td>內建自動</td>
          <td>自寫 Janitor</td>
      </tr>
      <tr>
          <td>持久化保證</td>
          <td>Replication-based（強）</td>
          <td>AOF/RDB（best-effort）</td>
      </tr>
      <tr>
          <td>Consumer group</td>
          <td>原生支援、offset commit 自動</td>
          <td>原生支援、但 trim 要手動協調</td>
      </tr>
      <tr>
          <td>生態工具</td>
          <td>Kafka Connect、Schema Registry</td>
          <td>無（自建）</td>
      </tr>
      <tr>
          <td>擴展性</td>
          <td>Partition 水平擴展</td>
          <td>單 Redis 受限、Cluster 模式複雜</td>
      </tr>
      <tr>
          <td>運維知識</td>
          <td>Kafka 運維（或交給 managed）</td>
          <td>Redis 運維 + 自建 Janitor 維護</td>
      </tr>
  </tbody>
</table>
<h3 id="適用邊界">適用邊界</h3>
<p>Redis Streams 取代 Kafka 的適用邊界：</p>
<ul>
<li><strong>流量規模</strong>：每分鐘數百到數千筆（超過每秒數萬筆需要 Redis Cluster 或多 stream）</li>
<li><strong>持久化要求</strong>：容忍偶爾丟失少量訊息（best-effort）</li>
<li><strong>已有 Redis</strong>：不需要額外部署 Redis、利用既有 infrastructure</li>
<li><strong>Kafka 功能不需要</strong>：不需要 Kafka Connect、Schema Registry、long-term retention、跨 region replication</li>
</ul>
<p>超過這些邊界時，Redis Streams 的自建成本（Janitor + 監控 + retention 治理 + 可靠性補償）會逐漸接近 managed Kafka 的費用，成本優勢消失。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a>：XCLAIM / PEL recovery 的進階主題</li>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>：成本對照 — Kafka 的固定成本高但功能完整</li>
<li><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>：Redis Streams 的持久化機制跟 Kafka 的 replication 在 durability 光譜上的位置</li>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 選型時成本是一級決策維度</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>Managed Kafka 的月帳單跟實際流量量級不成比例（低流量但高成本）</li>
<li>已有 Redis infrastructure、考慮把 event pipeline 合併到 Redis</li>
<li>Event pipeline 的流量在每秒數百筆以下、持久化要求是 best-effort</li>
<li>Redis 記憶體持續成長但不確定 Streams 的 retention 有沒有正確執行</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.arcjet.com/replacing-kafka-with-redis-streams/">Replacing Kafka with Redis Streams</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Partition Redesign：當 monthly partition 越跑越慢</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/partition-redesign/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/partition-redesign/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。對應 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Type F「Topology re-layout」&lt;/a> 第 2 個 dogfood（第 1 個是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis cluster re-sharding&lt;/a>）— 驗證 Type F anatomy 在不同 vendor 上的通用性。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼-monthly-partition-越跑越慢">為什麼 monthly partition 越跑越慢&lt;/h2>
&lt;p>上線時 monthly range partition 設計很合理 — 每月一個 partition、12 個月一年、partition_pruning 在 &lt;code>WHERE event_time &amp;gt;= '2026-05-01'&lt;/code> 時跑單 partition、查詢快。但業務跑了 18 個月後：&lt;/p>
&lt;ul>
&lt;li>每月 partition size 從 50GB 漲到 500GB（流量 10x）&lt;/li>
&lt;li>單月查詢 &lt;code>WHERE event_time BETWEEN '2026-05-01' AND '2026-05-15'&lt;/code> 仍掃整月 500GB（partition_pruning 粒度只到 month）&lt;/li>
&lt;li>Vacuum 一個月 partition 需要 6-8 小時、跑不進 maintenance window&lt;/li>
&lt;li>DROP 老 partition 釋放 storage 是 monthly cadence、但 retention policy 要求 daily granularity&lt;/li>
&lt;/ul>
&lt;p>partition 設計需要 &lt;em>redesign&lt;/em>、不是「optimize」 — 從 monthly range partition 改成 daily range partition、partition 數量從 36 個（3 年 retention）變 1095 個。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。對應 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Type F「Topology re-layout」</a> 第 2 個 dogfood（第 1 個是 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis cluster re-sharding</a>）— 驗證 Type F anatomy 在不同 vendor 上的通用性。</p></blockquote>
<h2 id="為什麼-monthly-partition-越跑越慢">為什麼 monthly partition 越跑越慢</h2>
<p>上線時 monthly range partition 設計很合理 — 每月一個 partition、12 個月一年、partition_pruning 在 <code>WHERE event_time &gt;= '2026-05-01'</code> 時跑單 partition、查詢快。但業務跑了 18 個月後：</p>
<ul>
<li>每月 partition size 從 50GB 漲到 500GB（流量 10x）</li>
<li>單月查詢 <code>WHERE event_time BETWEEN '2026-05-01' AND '2026-05-15'</code> 仍掃整月 500GB（partition_pruning 粒度只到 month）</li>
<li>Vacuum 一個月 partition 需要 6-8 小時、跑不進 maintenance window</li>
<li>DROP 老 partition 釋放 storage 是 monthly cadence、但 retention policy 要求 daily granularity</li>
</ul>
<p>partition 設計需要 <em>redesign</em>、不是「optimize」 — 從 monthly range partition 改成 daily range partition、partition 數量從 36 個（3 年 retention）變 1095 個。</p>
<p><a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 結果：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 PostgreSQL、同 table 定義、partition key 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 PostgreSQL operational stack</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 OLTP RDBMS</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 DB</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>不改（partition_pruning 透明）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>Partition strategy 從 monthly → daily</strong></td>
          <td><strong>High</strong></td>
      </tr>
  </tbody>
</table>
<p>6 維皆 Low + topology High = <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">Type F「Topology re-layout」</a>。</p>
<h2 id="pre-layout-analysispartition-不平衡偵測">Pre-layout analysis：partition 不平衡偵測</h2>
<p>執行 redesign 前必須先量化當前 topology：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1. 每 partition size + row count
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">child</span><span class="p">.</span><span class="n">relname</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">partition_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_relation_size</span><span class="p">(</span><span class="n">child</span><span class="p">.</span><span class="n">oid</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">size</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="n">child</span><span class="p">.</span><span class="n">reltuples</span><span class="p">::</span><span class="nb">bigint</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">estimated_rows</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="n">pg_stat_get_last_vacuum_time</span><span class="p">(</span><span class="n">child</span><span class="p">.</span><span class="n">oid</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">last_vacuum</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_inherits</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">pg_class</span><span class="w"> </span><span class="n">parent</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">pg_inherits</span><span class="p">.</span><span class="n">inhparent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">parent</span><span class="p">.</span><span class="n">oid</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">pg_class</span><span class="w"> </span><span class="n">child</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">pg_inherits</span><span class="p">.</span><span class="n">inhrelid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">child</span><span class="p">.</span><span class="n">oid</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">parent</span><span class="p">.</span><span class="n">relname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;events&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">pg_relation_size</span><span class="p">(</span><span class="n">child</span><span class="p">.</span><span class="n">oid</span><span class="p">)</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 2. partition_pruning 命中率
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="p">(</span><span class="k">ANALYZE</span><span class="p">,</span><span class="w"> </span><span class="n">BUFFERS</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;2026-05-15&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="c1">-- 期望: 只 scan 1 partition (target: daily) 或 1 partition (current: monthly)
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1">-- 觀察: monthly 設計下、即使 query 只跨 15 天、planner 仍 scan 整月 partition (~500GB)
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="c1">-- 3. 找 partition imbalance
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="n">to_char</span><span class="p">(</span><span class="n">event_time</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;YYYY-MM&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">month</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">row_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="c1">-- 找 hot month / cold month、判斷 redesign 後分佈</span></span></span></code></pre></div><p>Pre-layout 階段的 output：</p>
<ul>
<li><strong>當前 topology 量化</strong>：36 monthly partition、總 size 1.8TB、最大 partition 500GB、最小 50GB</li>
<li><strong>Hot key 分佈</strong>：80% 流量集中最近 3 個月</li>
<li><strong>Redesign 目標</strong>：daily partition、最近 3 個月 hot daily / 3 個月 + 之前 cold weekly / 1 年 + 之前 monthly（sub-partition strategy）</li>
<li><strong>Migration scope</strong>：1095 個 partition 不直接全建、按 retention policy 階段性</li>
</ul>
<h2 id="re-layout-機制attach--detach-線上重劃">Re-layout 機制：ATTACH / DETACH 線上重劃</h2>
<p>PostgreSQL 不支援「直接改 partition strategy」、必須走 <em>新 partition tree + 資料搬遷</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 1. 建新 daily partition table (parallel to events)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_daily</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">bigint</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">event_time</span><span class="w"> </span><span class="n">timestamptz</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="n">payload</span><span class="w"> </span><span class="n">jsonb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">event_time</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- 2. 預建未來 90 天 daily partition
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="n">format</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="s1">&#39;CREATE TABLE events_daily_%s PARTITION OF events_daily FOR VALUES FROM (%L) TO (%L)&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="n">to_char</span><span class="p">(</span><span class="n">d</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;YYYY_MM_DD&#39;</span><span class="p">),</span><span class="w"> </span><span class="n">d</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nb">interval</span><span class="w"> </span><span class="s1">&#39;1 day&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">generate_series</span><span class="p">(</span><span class="k">current_date</span><span class="p">,</span><span class="w"> </span><span class="k">current_date</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nb">interval</span><span class="w"> </span><span class="s1">&#39;90 days&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">interval</span><span class="w"> </span><span class="s1">&#39;1 day&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">d</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="c1">-- 3. dual-write phase: application 同寫 events + events_daily
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">-- (用 trigger 或 application-side)
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">dual_write_events</span><span class="p">()</span><span class="w"> </span><span class="k">RETURNS</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="err">$$</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">events_daily</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="k">NEW</span><span class="p">.</span><span class="o">*</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w"></span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="err">$$</span><span class="w"> </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="n">plpgsql</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="n">events_dual_write</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w"></span><span class="k">AFTER</span><span class="w"> </span><span class="k">INSERT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="k">FOR</span><span class="w"> </span><span class="k">EACH</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">EXECUTE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">dual_write_events</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w"></span><span class="c1">-- 4. backfill historical data per partition
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">events_daily</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-05-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">event_time</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;2026-05-02&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w"></span><span class="c1">-- ... 每天跑一個 day partition、avoid long transaction
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w"></span><span class="c1">-- 5. cutover: rename swap
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">RENAME</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">events_old</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events_daily</span><span class="w"> </span><span class="k">RENAME</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">events</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="n">events_dual_write</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">events_old</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w"></span><span class="c1">-- 6. 觀察 1-2 週、DROP events_old</span></span></span></code></pre></div><p>關鍵：rename swap 是 <em>single transaction</em>、cutover 瞬間發生；application connection 不需重連、但 prepared statement cache 可能要刷新。</p>
<h2 id="execution-flow-per-step">Execution flow per-step</h2>
<p>5 段、每段含 rollback boundary：</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>動作</th>
          <th>Rollback boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 預建 partition</td>
          <td>建 events_daily + 90 天 partition、不影響 production</td>
          <td>DROP events_daily、無 impact</td>
      </tr>
      <tr>
          <td>2 Dual-write</td>
          <td>加 trigger 同寫兩端、observe diff</td>
          <td>DROP trigger、events_daily 留作 cleanup</td>
      </tr>
      <tr>
          <td>3 Backfill</td>
          <td>逐日 backfill 歷史資料、用 CHECK constraint 確保完整性</td>
          <td>DROP backfilled partition、不影響 source events</td>
      </tr>
      <tr>
          <td>4 Verify</td>
          <td>對 sample query 跑 events vs events_daily、確認 row count 一致</td>
          <td>仍在 dual-write、發現 diff 可暫停 cutover</td>
      </tr>
      <tr>
          <td>5 Cutover</td>
          <td>Rename swap</td>
          <td><strong>不可逆</strong>、回退需 reverse rename + dual-write restart</td>
      </tr>
  </tbody>
</table>
<p>Step 5 是不可逆邊界、應該排在 <em>低流量 maintenance window</em> 跑、且 cutover 前必須有 backup checkpoint。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1backfill-期間-long-transaction-阻塞-vacuum">Case 1：Backfill 期間 long transaction 阻塞 vacuum</h3>
<p><strong>徵兆</strong>：backfill 跑 6 小時的 <code>INSERT INTO events_daily SELECT * FROM events WHERE ...</code>、期間 events 表的 autovacuum 完全不跑、dead tuple 累積、production query 變慢。</p>
<p><strong>根因</strong>：PostgreSQL transaction 期間 <em>xmin horizon 鎖死</em>、vacuum 只能回收「不會被任何 active transaction 看到」的 dead tuple；long backfill = long open transaction、vacuum 失效。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>拆 batch INSERT</strong>：每日 backfill 拆成 small batch（10 萬 row 一個 transaction）、每個 commit 釋放 xmin</li>
<li><strong>用 COPY 不用 INSERT</strong>：<code>COPY events_daily FROM (SELECT * FROM events WHERE ...)</code> 是 PG 對 batch 最快 + 對 vacuum 影響小</li>
<li><strong>Backfill 跑在 standby</strong>：用 logical replication 從 standby 拉資料、不在 primary 跑長 transaction</li>
</ol>
<h3 id="case-2trigger-dual-write-對-application-造成-latency">Case 2：Trigger dual-write 對 application 造成 latency</h3>
<p><strong>徵兆</strong>：加 trigger 後 application 寫入 latency p99 從 5ms 漲到 25-50ms；high-throughput batch job 直接 timeout。</p>
<p><strong>根因</strong>：每筆 INSERT 都觸發 trigger function 跑一次 INSERT 到 events_daily、IO 雙倍、index 也雙倍維護。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>改 application-side dual-write</strong>：application code 顯式寫兩端、用 connection pool batch 攤平 IO</li>
<li><strong>用 logical replication slot</strong>：events → events_daily 用 logical replication 取代 trigger、降 IO 衝擊</li>
<li><strong>dual-write 時間最小化</strong>：trigger 只在 backfill + verify 期間打開、cutover 前關掉</li>
</ol>
<h3 id="case-3partition_pruning-沒命中planner-仍掃所有-partition">Case 3：Partition_pruning 沒命中、planner 仍掃所有 partition</h3>
<p><strong>徵兆</strong>：cutover 完成後、application 端某些 query latency 從 200ms 跳到 5000ms；EXPLAIN 顯示 <code>Append</code> 下面所有 1095 個 partition 都被 scan。</p>
<p><strong>根因</strong>：partition 數量爆到 1000+、planner planning_time 對某些 query 變長（含 prepared statement 沒帶 partition key bound）；或 query 用了 <code>WHERE event_time = some_function(now())</code>、planning-time pruning 不觸發。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>enable_partition_pruning = on</code></strong> 預設、確認沒被 disable</li>
<li><strong>PG 11+ runtime pruning</strong>：prepared statement 用 generic plan、runtime pruning 補位</li>
<li><strong>Sub-partition strategy</strong>：1095 個 daily 太多、改 <em>最近 90 天 daily / 之前 monthly</em> 混合 strategy、減 partition count</li>
<li><strong>Planner statistics</strong>：跑 <code>ANALYZE</code> 重建 statistics、partition 樹太大時 planner 需新 stats</li>
</ol>
<h3 id="case-4constraint-exclusion-失敗跨-partition-unique-不-enforce">Case 4：Constraint exclusion 失敗、跨 partition unique 不 enforce</h3>
<p><strong>徵兆</strong>：cutover 後發現某 user 的 event 在多個 partition 都有、unique constraint <code>(user_id, event_id)</code> 沒 enforce；data audit 抓到 duplicate。</p>
<p><strong>根因</strong>：PostgreSQL partition table 的 <code>UNIQUE</code> constraint <em>必須包含 partition key</em>；本來 monthly partition 下 <code>UNIQUE (user_id, event_id)</code> 加上 <code>event_time</code>（partition key）變 <code>UNIQUE (user_id, event_id, event_time)</code>、實際語意是「同月同 user 同 event_id 唯一」；改 daily 後變「同日同 user 同 event_id 唯一」— unique scope 從月變天、原本月內跨日 dedup 失效。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-redesign</strong>：明示 unique constraint 的 <em>時間 scope</em>、redesign 後 scope 縮小是否可接受</li>
<li><strong>Application-side dedup</strong>：跨 partition 唯一性走 application 層 lookup（用 Redis SETEX 暫存 key）</li>
<li><strong>退到 non-partitioned dedup 表</strong>：建獨立 user_events_dedup 表、application 寫入前先 lookup</li>
</ol>
<h3 id="case-5drop-老-partition-太頻繁shared_buffers-cache-miss-爆">Case 5：DROP 老 partition 太頻繁、shared_buffers cache miss 爆</h3>
<p><strong>徵兆</strong>：daily partition 上線後、每天凌晨 cron DROP <code>events_2025_05_18</code>（90 天前）；DROP 後 shared_buffers 大量 invalidate、application 端 query latency p99 從 10ms 跳到 100-200ms 持續 30 分鐘。</p>
<p><strong>根因</strong>：PostgreSQL shared_buffers cache 對被 DROP 表的 page 全部 invalidate；DROP 大 partition（10GB+）後 cache hit rate 從 99% 掉到 60%、application 等 disk IO。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>DROP 跑在 off-peak</strong>：凌晨 3-4 點 cron、避開業務高峰</li>
<li><strong>預熱 next partition</strong>：DROP 前用 <code>pg_prewarm</code> 主動 load 熱 partition 進 cache</li>
<li><strong>改 DETACH + DROP TABLE delayed</strong>：DETACH 是 fast、DROP TABLE 排到 weekly batch、降頻率</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Monthly partition (current)</th>
          <th>Daily partition (target)</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition count</td>
          <td>36 (3 年 retention)</td>
          <td>1095 (3 年 retention)</td>
          <td>30x partition count、planner cost 略升</td>
      </tr>
      <tr>
          <td>Single partition size</td>
          <td>50-500GB</td>
          <td>1-20GB</td>
          <td>Daily 更易 vacuum</td>
      </tr>
      <tr>
          <td>DROP old data</td>
          <td>Monthly cadence</td>
          <td>Daily cadence</td>
          <td>更細 retention 控制</td>
      </tr>
      <tr>
          <td>Query latency</td>
          <td>跨 partition 多時 50-200ms</td>
          <td>跨 partition 少時 5-50ms</td>
          <td>Daily 多數 query 更快</td>
      </tr>
      <tr>
          <td>Planning time</td>
          <td>5-10ms</td>
          <td>50-100ms (對 generic plan)</td>
          <td>Planning overhead + 1 order</td>
      </tr>
      <tr>
          <td>Maintenance window</td>
          <td>Vacuum 1 partition 6 小時</td>
          <td>Vacuum 1 partition 5-30 分鐘</td>
          <td>維護視窗更小、可日跑</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：daily partition 適合 <em>高流量 + 跨日查詢多 + retention 細的場景</em>；超大 partition (TB 級單日) 仍要 sub-partition 拆。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-autovacuum-tuning-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a> 整合</h3>
<p>Daily partition 後 autovacuum 行為：</p>
<ul>
<li>每 daily partition 獨立 autovacuum、scale_factor + threshold per-partition tuning</li>
<li><code>autovacuum_max_workers</code> 要從 3 拉到 6-10（partition 數爆）</li>
<li>Cold partition (&gt; 30 天) <code>autovacuum_enabled = false</code>、不浪費 CPU</li>
</ul>
<h3 id="跟-patroni-ha-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a> 整合</h3>
<p>Failover 期間 partition migration 不能跑、必須在 stable cluster state 執行；Patroni promote 後重新評估 partition health。</p>
<h3 id="跟-logical-replication--debezium-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Logical Replication + Debezium</a> 整合</h3>
<p><code>publish_via_partition_root = true</code> 讓 publication 從 parent 角度看；CDC consumer 不需要對每個 partition 設 subscription。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>跨 daily partition 的 archive strategy</strong>：archive 到 S3 cold storage、daily granularity 給更細 retention 控制</li>
<li><strong>pg_partman extension</strong>：自動建 daily partition、不用 cron；但要先確認 Aurora / RDS 支援</li>
<li><strong>Sub-partitioning</strong>：未來流量爆時用「daily by time + list by tenant」雙軸 partition</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">Declarative Partitioning</a>（partition 基礎）/ <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a></li>
<li>平行 Type F dogfood：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Re-sharding</a>（dogfood #1）/ <a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">MongoDB Shard + Multi-DC</a>（dogfood #3、F-multi-region sub-type）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Data topology 是第 6 audit 維度</a></li>
</ul>
]]></content:encoded></item><item><title>3.C44 Harness：CD 微服務 async state transfer</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 在 production 落地的三類經常性議題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Harness 為 CD 微服務之間的 async state transfer 採用 Redis Streams、避開「每個 service 都要知道怎麼跟其他 service 講話」的 brittle HTTP 模式；初始規模 a few thousand msgs/min、Kafka 在此規模 overkill、又能複用已存在的 Redis 基建。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>落地後揭露三類問題：監控缺口（自寫 app 追 consumer lag）、需要主動 MAXLEN truncation、head-of-line blocking 要用 XAUTOCLAIM 重派並設計 redelivery 策略。揭露「Redis Streams 適合中小規模」這個聲明、實際包含三件 production work。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Memory + retention 取捨。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.harness.io/blog/event-driven-architecture-redis-streams">Event-Driven Architecture with Redis Streams&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 在 production 落地的三類經常性議題。</p>
<h2 id="觀察">觀察</h2>
<p>Harness 為 CD 微服務之間的 async state transfer 採用 Redis Streams、避開「每個 service 都要知道怎麼跟其他 service 講話」的 brittle HTTP 模式；初始規模 a few thousand msgs/min、Kafka 在此規模 overkill、又能複用已存在的 Redis 基建。</p>
<h2 id="判讀">判讀</h2>
<p>落地後揭露三類問題：監控缺口（自寫 app 追 consumer lag）、需要主動 MAXLEN truncation、head-of-line blocking 要用 XAUTOCLAIM 重派並設計 redelivery 策略。揭露「Redis Streams 適合中小規模」這個聲明、實際包含三件 production work。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Memory + retention 取捨。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.harness.io/blog/event-driven-architecture-redis-streams">Event-Driven Architecture with Redis Streams</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Multi-Region GDPR Rollout：政策驅動的 migration 屬本 methodology 嗎</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。同時是 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation&lt;/a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 &lt;em>residency 軸驗證&lt;/em>、跟 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook methodology「何時不該套」段&lt;/a> 對「政策合規驅動」是否在 methodology scope 的反思。&lt;/p>&lt;/blockquote>
&lt;h2 id="政策驅動的-migration-屬本-methodology-嗎">政策驅動的 migration 屬本 methodology 嗎&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> 「何時不該套」段曾把「compliance-driven migration」歸為排除情境、後來改寫為「不在排除範圍 — 法規驅動只是 driver、資料層仍走 type A-E 之一」。本文是該改寫的 &lt;em>正面實證&lt;/em> — GDPR EU residency 強制需求驅動 single-region → multi-region rollout、本文是 &lt;em>政策驅動但仍走 audit + type 對映流程&lt;/em> 的 case study。&lt;/p>
&lt;p>但 reviewer D 在第三輪 audit 提出：residency 不只是 &lt;em>driver&lt;/em>、本身是 &lt;em>cross-cutting constraint&lt;/em>、反向約束 topology + operational + schema；該不該升 &lt;em>獨立 audit 軸&lt;/em>？本文是該議題的 dogfood。&lt;/p>
&lt;h2 id="三層約束driver--topology--contract">三層約束：driver / topology / contract&lt;/h2>
&lt;p>GDPR 對 PostgreSQL multi-region rollout 的影響在三個層次：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。同時是 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation</a> 第 1 點「6 維仍可能漏類（identity / consistency / residency 三軸候選）」的 <em>residency 軸驗證</em>、跟 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook methodology「何時不該套」段</a> 對「政策合規驅動」是否在 methodology scope 的反思。</p></blockquote>
<h2 id="政策驅動的-migration-屬本-methodology-嗎">政策驅動的 migration 屬本 methodology 嗎</h2>
<p><a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> 「何時不該套」段曾把「compliance-driven migration」歸為排除情境、後來改寫為「不在排除範圍 — 法規驅動只是 driver、資料層仍走 type A-E 之一」。本文是該改寫的 <em>正面實證</em> — GDPR EU residency 強制需求驅動 single-region → multi-region rollout、本文是 <em>政策驅動但仍走 audit + type 對映流程</em> 的 case study。</p>
<p>但 reviewer D 在第三輪 audit 提出：residency 不只是 <em>driver</em>、本身是 <em>cross-cutting constraint</em>、反向約束 topology + operational + schema；該不該升 <em>獨立 audit 軸</em>？本文是該議題的 dogfood。</p>
<h2 id="三層約束driver--topology--contract">三層約束：driver / topology / contract</h2>
<p>GDPR 對 PostgreSQL multi-region rollout 的影響在三個層次：</p>
<ol>
<li><strong>Driver layer</strong>：EU 客戶資料必須 <em>物理上儲存在 EU</em>（GDPR Article 44-49）— 觸發 multi-region migration 的根本理由</li>
<li><strong>Topology layer</strong>：跨 region replication 不能 <em>自由跨 region 複製</em> EU 客戶資料、必須按 GDPR scope 分區；topology 設計受合規約束</li>
<li><strong>Contract layer</strong>：審計能 <em>demonstrate</em> 「EU 資料在 EU」、操作日誌 + replication evidence 必須可追溯；application + ops contract 多出合規 obligation</li>
</ol>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">6 維 diff dimension audit</a> 對「single us-east → us-east + eu-west」：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 PostgreSQL、可能加 region column</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>HA / backup / monitoring 跨 region 重設計</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>同 OLTP RDBMS</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 PostgreSQL instance + Patroni</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Routing logic by user region、必改</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Single → multi-region replication</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td><strong>Residency contract</strong></td>
          <td><strong>EU 資料禁止離開 EU、log + replication 範圍受約束</strong></td>
          <td><strong>High</strong></td>
      </tr>
  </tbody>
</table>
<p>6 維 audit 抓不到「Residency contract = High」這軸。用既有 6 維歸類、會走 Type F multi-axis（topology + operational + application change 多 High）+ 政策合規補強段；但這個歸類 <em>漏掉合規對 topology / operational / application 的反向約束</em>：</p>
<ul>
<li>Topology layer：6 維只 audit 「topology 是否變動」、漏 audit 「topology 範圍是否受合規約束」</li>
<li>Operational layer：6 維只 audit 「operational 是否重設計」、漏 audit 「audit log / encryption / access control 是否符合合規要求」</li>
<li>Application layer：6 維只 audit 「application code 是否改」、漏 audit 「資料 routing 是否符合 residency rule」</li>
</ul>
<p><strong>Residency 不只是 driver、是 cross-cutting constraint</strong>、會反向約束其他 3-4 維、且帶獨立工作量（合規 evidence collection / DPIA / audit prep）。</p>
<h2 id="residency-axis-是否獨立3-個論據">Residency axis 是否獨立：3 個論據</h2>
<p><strong>Yes、residency 是獨立軸</strong>：</p>
<ol>
<li><strong>可獨立發生</strong>：原本 multi-region setup、新增「PCI 強制信用卡資料只能 us-east」、是 <em>純 residency 變更</em>、其他 6 維皆 Low（topology 不重設計、operational 不重設計、application 加 routing rule 即可）；但 residency 約束 routing + log 範圍</li>
<li><strong>驅動工作量分佈</strong>：本文 multi-region GDPR rollout 工作量分佈：
<ul>
<li>Topology setup（logical replication / region setup）：~25%</li>
<li>Operational redesign（HA / backup / monitoring）：~20%</li>
<li>Application routing change（region detection / data filter）：~15%</li>
<li><strong>Residency compliance（DPIA / audit log / access control / encryption / evidence）：~40%</strong></li>
</ul>
</li>
<li><strong>Cross-cutting nature</strong>：residency 不只影響「資料放哪」、影響：
<ul>
<li>Backup 可不可以 cross-region store（多數 GDPR 不允許）</li>
<li>Audit log 是否包含 EU PII（需 EU 端 log + 跨 region log filter）</li>
<li>Encryption key 是否可 cross-region share（多數情境不允許）</li>
<li>Application access logs 是否含 EU IP / user ID</li>
</ul>
</li>
</ol>
<p><strong>No、residency 可塞 operational + driver</strong>：</p>
<ul>
<li>反論：residency 是 operational 子議題、加 audit + replication scope 規則就好</li>
<li>拒絕：residency 反向約束 topology / application / operational、且帶獨立合規工作量（DPIA / cross-border transfer agreement / data subject rights）；不是單純 operational 子議題</li>
</ul>
<p>實證：本文 migration 工作量 40% 在 compliance、確認 residency 是 <em>獨立工作量主軸</em>。</p>
<h2 id="結構type-f-multi-axis--residency-compliance-獨立段">結構：Type F multi-axis + residency compliance 獨立段</h2>
<p>本文結構是 <em>Type F 為主</em>（topology high + operational high）+ <em>residency compliance 獨立段</em>（不在 6 維任一個）：</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. 政策驅動的 migration 屬本 methodology 嗎（meta-reflection 開頭）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 三層約束：driver / topology / contract
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Residency axis 是否獨立的論據
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 結構 differentiator（Type F multi-axis + residency compliance 段）
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. EU residency 對 topology / operational / application 的反向約束
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Migration 流程（含 DPIA 跟 evidence collection 階段）
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. Production 故障演練
</span></span><span class="line"><span class="ln">8</span><span class="cl">8. Capacity / cost（含合規 audit cost）
</span></span><span class="line"><span class="ln">9</span><span class="cl">9. 整合 / 下一步</span></span></code></pre></div><p>9 章節、240-270 行。比標準 Type F 多 1 段（residency compliance）+ 1 段（meta-reflection）。</p>
<h2 id="eu-residency-對其他維度的反向約束">EU residency 對其他維度的反向約束</h2>





<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">Residency rule → Topology constraint:
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- EU customer data 不能 replicate to us-east
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- Backup of EU table 不能 store in non-EU region
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- Logical replication subscriber 在 us-east 必須 filter out EU data
</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">Residency rule → Operational constraint:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">- Cross-region monitoring 不能 export EU PII to global SaaS (Datadog)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">- Audit log 含 EU user_id 必須 store 在 EU
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">- Encryption key (KMS) 不能 share 跨 region（EU 端用 EU KMS）
</span></span><span class="line"><span class="ln">10</span><span class="cl">- DBA / SRE access EU data 必須 from EU jurisdiction + 記 audit trail
</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">Residency rule → Application constraint:
</span></span><span class="line"><span class="ln">13</span><span class="cl">- Application 必須 detect user region + route 對應 DB endpoint
</span></span><span class="line"><span class="ln">14</span><span class="cl">- Cross-region join / aggregate 對 EU user 必須走 EU 端 query
</span></span><span class="line"><span class="ln">15</span><span class="cl">- Data export feature 必須 reject 跨 region export request</span></span></code></pre></div><p>每條反向約束都是 <em>新工作量</em>、不在 6 維 audit 內。</p>
<h2 id="migration-流程含-dpia--evidence-collection">Migration 流程（含 DPIA + evidence collection）</h2>
<p>10 step、跨 5 個月：</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Step</th>
          <th>對應 6 維 / 合規</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0 Pre-migration</td>
          <td>1. DPIA（Data Protection Impact Assessment）</td>
          <td>Compliance pre-requisite</td>
      </tr>
      <tr>
          <td>0</td>
          <td>2. 法務 review 跨境傳輸 agreement</td>
          <td>Compliance</td>
      </tr>
      <tr>
          <td>1 Setup</td>
          <td>3. EU PostgreSQL cluster build + Patroni</td>
          <td>Operational + Topology</td>
      </tr>
      <tr>
          <td>1</td>
          <td>4. EU KMS + audit log + monitoring stack</td>
          <td>Operational + Residency</td>
      </tr>
      <tr>
          <td>2 Data</td>
          <td>5. Logical replication 設 filter（exclude EU table from us-east）</td>
          <td>Topology + Residency</td>
      </tr>
      <tr>
          <td>2</td>
          <td>6. Initial sync EU table 到 EU cluster</td>
          <td>Topology</td>
      </tr>
      <tr>
          <td>3 App</td>
          <td>7. Application 端加 region detection + routing</td>
          <td>Application change</td>
      </tr>
      <tr>
          <td>3</td>
          <td>8. Cross-region query banning（cross-region join 拒絕 EU table）</td>
          <td>Application + Residency</td>
      </tr>
      <tr>
          <td>4 Verify</td>
          <td>9. Compliance audit + evidence package</td>
          <td>Residency</td>
      </tr>
      <tr>
          <td>4</td>
          <td>10. DPO sign-off + DR drill</td>
          <td>Residency + Operational</td>
      </tr>
  </tbody>
</table>
<p>Step 1 + 9 + 10 是 <em>residency-specific</em>、不在既有 6 維內。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1replication-filter-漏-tableeu-資料-leak-到-us-east">Case 1：Replication filter 漏 table、EU 資料 leak 到 us-east</h3>
<p><strong>徵兆</strong>：6 個月後 internal audit 發現 us-east 端 <code>customers</code> table 含 EU 客戶資料；replication filter 設定漏改、新加的 <code>eu_customer_extensions</code> table 被自動 replicate 到 us-east。</p>
<p><strong>根因</strong>：PostgreSQL logical replication publication 預設 <code>FOR ALL TABLES</code>、新加的 table 自動納入；應該明示 <code>FOR TABLE list...</code> 並 GDPR review。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Publication 改 explicit table list</strong>：<code>CREATE PUBLICATION xxx FOR TABLE users, orders, ...</code>、不用 <code>FOR ALL TABLES</code></li>
<li><strong>Schema change review 加 GDPR check</strong>：每個 DDL PR 必須答「新 table 是否含 EU PII、是否該 filter」</li>
<li><strong>Replication monitor</strong>：定期跑 <code>SELECT * FROM pg_publication_tables</code> 對照 expected list、漂移立刻 alert</li>
<li><strong>Evidence collection</strong>：filter 配置 + audit log 留檔、出事 DPO 知道何時 leak</li>
</ol>
<h3 id="case-2backup-跨-region-store合規違規">Case 2：Backup 跨 region store、合規違規</h3>
<p><strong>徵兆</strong>：跑 1 年後 GDPR audit 抓到 EU table 的 backup 存在 us-west S3 bucket；違反 Article 44-49 限制。</p>
<p><strong>根因</strong>：pgBackRest 預設用 <em>global S3 bucket</em>（在 us-east-1）；EU PostgreSQL cluster backup 跑去 us-east、跨境傳輸無 transfer mechanism。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Per-region backup config</strong>：EU cluster 用 EU S3 bucket（eu-west-1）、寫進 pgBackRest config</li>
<li><strong>Backup test</strong>：每月跑一次 backup restore drill、validate backup 是 from EU region</li>
<li><strong>Bucket policy 強 enforce</strong>：EU bucket 加 <code>aws:RequestedRegion=eu-west-1</code> 強制 region match</li>
<li><strong>Audit log archive 同理</strong>：log shipping 也必須 region-respect</li>
</ol>
<h3 id="case-3monitor-saas-收集-eu-pii合規-alert">Case 3：Monitor SaaS 收集 EU PII、合規 alert</h3>
<p><strong>徵兆</strong>：Datadog APM 收集了 EU customer 端 request 含 user_email 在 trace、被 DPO catch、required to delete 過去 90 天的 Datadog data。</p>
<p><strong>根因</strong>：APM trace 預設收集 application context、含 PII；Datadog 是 us-east SaaS、PII 跨境到 Datadog us-east、違規。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>APM scrub PII</strong>：application 端在 trace 前 scrub user_email / user_id 替換成 hash</li>
<li><strong>EU-specific monitor stack</strong>：EU PostgreSQL + APM 用 Grafana on EU EKS、不送 Datadog</li>
<li><strong>跨 region SaaS use 必須 audit</strong>：所有外部 SaaS（Datadog / Sentry / NewRelic）必須 GDPR-friendly 配置</li>
<li><strong>Privacy by design</strong>：log / trace 預設 scrub PII、不是 opt-in</li>
</ol>
<h3 id="case-4cross-region-query-跑-eu--us-資料residency-違規">Case 4：Cross-region query 跑 EU + US 資料、residency 違規</h3>
<p><strong>徵兆</strong>：BI dashboard 跑跨 region aggregation query（EU sales + US sales）、PostgreSQL FDW 從 us-east cluster query EU cluster、EU 端 server log 顯示「PII export to us-east」。</p>
<p><strong>根因</strong>：開發者用 PostgreSQL Foreign Data Wrapper（FDW）方便跑跨 region query、不知道這在 GDPR 視為跨境 PII export。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Architecture: aggregate at edge</strong>：BI 跑 <em>per-region aggregate</em>、再在 BI layer compose（無 PII）；不直接跨 region join</li>
<li><strong>FDW 限制</strong>：disable FDW from us-east → EU cluster、enforce one-way data flow</li>
<li><strong>DBA access policy</strong>：DBA 不能直接 query EU cluster 從 us-east jumpbox</li>
<li><strong>Query audit</strong>：production query log 跑 PII detection（regex / NER）、發現跨境 export 立即 alert</li>
</ol>
<h3 id="case-5dr-drill-跨-region-failover暴露-residency-assumption-失敗">Case 5：DR drill 跨 region failover、暴露 residency assumption 失敗</h3>
<p><strong>徵兆</strong>：DR drill「EU 完全不可用、切到 us-east」執行後、發現 us-east 端 <em>沒 EU 資料</em> — 因為一直 strict residency filter；business 端 EU 客戶 24 小時無法服務。</p>
<p><strong>根因</strong>：strict GDPR residency 跟 strict DR availability 衝突 — 要 <em>跨 region DR</em> 就要 <em>跨 region 持有資料</em>、要 <em>strict residency</em> 就 <em>DR 範圍受限</em>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>DR strategy revision</strong>：EU 端 multi-AZ within EU、不靠跨 region；EU region 全不可用情境接受 longer RTO</li>
<li><strong>Compliance + DR negotiation</strong>：跟 DPO / 法務談 <em>DR 跨境 short-window 是否可接受</em>、簽 cross-border transfer agreement</li>
<li><strong>Backup recovery 在 EU 內</strong>：EU 端 backup 跨 AZ store、不跨 region；EU AZ 災難用 EU 另一個 AZ 重建</li>
<li><strong>明示 RTO trade-off</strong>：EU customer SLA 寫「regional DR 內 RTO 1 小時、global DR 24-48 小時」、residency 跟 DR 是 <em>互斥取捨</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Single region</th>
          <th>Multi-region GDPR-compliant</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure cost</td>
          <td>baseline</td>
          <td>+60-100%（雙 cluster + cross-region replication）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1</td>
          <td>1-2 FTE（雙 region SRE + compliance）</td>
      </tr>
      <tr>
          <td>Compliance cost</td>
          <td>0</td>
          <td>$50-200K USD setup（DPIA / audit / DPO time）+ ongoing</td>
      </tr>
      <tr>
          <td>Egress cost</td>
          <td>Low</td>
          <td>High（cross-region replication 流量）</td>
      </tr>
      <tr>
          <td>Application latency</td>
          <td>Single AZ</td>
          <td>EU customer 連 EU、低；US customer 連 US、低</td>
      </tr>
      <tr>
          <td>DR RTO</td>
          <td>30 分鐘 (single region)</td>
          <td>EU regional 1 小時 / global 24-48 小時</td>
      </tr>
      <tr>
          <td>Audit cost</td>
          <td>Minimal</td>
          <td>季度 DPIA + 年度 compliance audit</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：GDPR multi-region 成本 1.5-2.5x、但合規是 <em>必要 spend</em>、用 cost optimization 的框架看會誤判；多數歐洲業務 7+ 年回本（避免 4% revenue fine）。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql--aurora-對位">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 對位</h3>
<p>Aurora Global Database 可簡化跨 region setup、但 residency filter 仍需 application 端；不是「Aurora 就解決 GDPR」。</p>
<h3 id="跟-multi-dc-mongodb-對位">跟 <a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">Multi-DC MongoDB</a> 對位</h3>
<p>兩篇都是 multi-region rollout、但本文加合規維度；MongoDB 篇純 capacity + DR driver、本文加 residency constraint、結構不同。</p>
<h3 id="跟-128-self-aware-limitation-第-1-點對位">跟 #128 self-aware limitation 第 1 點對位</h3>
<p>本文驗證 <em>residency axis 候選</em>：</p>
<ul>
<li><strong>Yes 軸獨立</strong>：reverse-constrain topology + operational + application、且帶獨立 compliance 工作量（DPIA / evidence collection / DPO sign-off）</li>
<li><strong>作為 driver 不夠</strong>：methodology 把 residency 歸為 driver 太窄、忽略 cross-cutting constraint 性質</li>
</ul>
<p>未來 audit 可能擴 7 維（加 residency / compliance contract）；累積 PCI / HIPAA / SOX 等不同合規 case 後再評估。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Identity + Consistency + Residency 三軸候選統合</strong>：本批 3 篇分別驗證、未來累積 evidence 後考慮獨立 #129 卡 / 擴 audit 到 7-8 維</li>
<li><strong>Schrems II + new EU data transfer rules</strong>：跨大西洋資料傳輸法規變動快、playbook 半衰期短</li>
<li><strong>Data localization in China / Russia / India</strong>：類似 GDPR 但細節不同、未來 case 累積後評估</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>平行 multi-region case：<a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">MongoDB Shard + Multi-DC</a></li>
<li>平行 axis 候選驗證：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager</a>（identity 候選）/ <a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">DynamoDB Consistency Model</a>（consistency 候選）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation 第 1 點</a>（residency axis 候選驗證、本文是該驗證的 dogfood）</li>
</ul>
]]></content:encoded></item><item><title>3.C45 Klaxit：Rust + Redis Streams 處理 Heroku Logplex</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 在高吞吐 log ingestion 的 consumer group 分流。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Klaxit 用 Redis Streams 處理 Heroku Logplex 匯流的 log、自動偵測並修復 Heroku 平台層 perf 問題（在使用者察覺前）；正式 production 跑超過 6 個月、是團隊第一個 Rust project。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露 high-throughput log ingestion 對 Redis Streams 的壓力：用 consumer group 分流到多個 Rust worker、需要長時間穩定運轉。揭露 client library 品質決定 Redis Streams 在小眾語言（Rust）的可行性。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：XADD / XREAD / XREADGROUP 操作 / Consumer group + PEL。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dev.to/goodtouch/consuming-high-throughput-redis-streams-with-rust-580c">Consuming High-Throughput Redis Streams with Rust&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 在高吞吐 log ingestion 的 consumer group 分流。</p>
<h2 id="觀察">觀察</h2>
<p>Klaxit 用 Redis Streams 處理 Heroku Logplex 匯流的 log、自動偵測並修復 Heroku 平台層 perf 問題（在使用者察覺前）；正式 production 跑超過 6 個月、是團隊第一個 Rust project。</p>
<h2 id="判讀">判讀</h2>
<p>揭露 high-throughput log ingestion 對 Redis Streams 的壓力：用 consumer group 分流到多個 Rust worker、需要長時間穩定運轉。揭露 client library 品質決定 Redis Streams 在小眾語言（Rust）的可行性。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：XADD / XREAD / XREADGROUP 操作 / Consumer group + PEL。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://dev.to/goodtouch/consuming-high-throughput-redis-streams-with-rust-580c">Consuming High-Throughput Redis Streams with Rust</a></li>
</ul>
]]></content:encoded></item><item><title>3.C46 Learning.com：Redis 事件源退場（反例）</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/</guid><description>&lt;p>這個反例的核心責任是說明 Redis 不適合長期事件儲存、揭露「Redis-as-event-store」的退場路徑。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Learning.com 把 microservice 之間的 event store 放 Redis 上、一年內累積到 GB/週的 memory 成長、AOF fsync + EBS 磁碟 I/O 變成 latency 痛點。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露「Redis 不適合長期事件儲存」的退場路徑：event 移到 PostgreSQL、Redis 留做訊息佇列 + snapshot；中途靠 syncTimeout 調整、提升 IOPS、調整 AOF fsync 緩解。揭露 broker 選型要看「長期存儲是 source-of-truth 還是 transient」。&lt;strong>注意&lt;/strong>：此文討論的是 Redis-as-event-store 整體、Streams 是其中一塊、引用時要小心區分。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Memory + retention 取捨 / Sentinel + Cluster 可靠性（持久化選型）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/lcom-techblog/a-year-with-redis-event-sourcing-lessons-learned-6736068e17cc">A Year with Redis Event Sourcing - Lessons Learned&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 Redis 不適合長期事件儲存、揭露「Redis-as-event-store」的退場路徑。</p>
<h2 id="觀察">觀察</h2>
<p>Learning.com 把 microservice 之間的 event store 放 Redis 上、一年內累積到 GB/週的 memory 成長、AOF fsync + EBS 磁碟 I/O 變成 latency 痛點。</p>
<h2 id="判讀">判讀</h2>
<p>揭露「Redis 不適合長期事件儲存」的退場路徑：event 移到 PostgreSQL、Redis 留做訊息佇列 + snapshot；中途靠 syncTimeout 調整、提升 IOPS、調整 AOF fsync 緩解。揭露 broker 選型要看「長期存儲是 source-of-truth 還是 transient」。<strong>注意</strong>：此文討論的是 Redis-as-event-store 整體、Streams 是其中一塊、引用時要小心區分。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Memory + retention 取捨 / Sentinel + Cluster 可靠性（持久化選型）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/lcom-techblog/a-year-with-redis-event-sourcing-lessons-learned-6736068e17cc">A Year with Redis Event Sourcing - Lessons Learned</a></li>
</ul>
]]></content:encoded></item><item><title>3.C47 PHP 微服務：Redis Streams + S3 hybrid storage</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/</guid><description>&lt;p>這個案例的核心責任是說明 in-memory 訊息的 payload 限制要靠 hybrid storage 解決。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>PHP 雙微服務之間的可靠通訊、Kafka 在 PHP 生態工具薄弱、團隊無 Kafka 經驗、production 跑數月後寫此文；明確覆蓋 XADD / XREADGROUP / consumer group / MAXLEN / MINID / XDEL / XACK / XACKDEL（Redis 8.2+）/ XTRIM。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露 in-memory 訊息的 payload 限制：用 payload compression + S3 hybrid storage（大 payload 存 S3、stream 只放 reference）；用 MAXLEN/MINID 控制 stream 成長。揭露 broker 選型常被「語言生態 client 品質」主導、不是純技術 feature。&lt;strong>注意&lt;/strong>：作者是個人工程師、production 經驗但非知名公司。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：XADD/XREAD/XREADGROUP 操作 / Retention (MAXLEN/MINID) / Memory + retention 取捨。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16 Robinhood Faust&lt;/a>（語言生態對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dev.to/mtk3d/beyond-the-hype-why-we-chose-redis-streams-over-kafka-for-our-microservices-dmc">Beyond the Hype: Why We Chose Redis Streams Over Kafka for Our Microservices&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 in-memory 訊息的 payload 限制要靠 hybrid storage 解決。</p>
<h2 id="觀察">觀察</h2>
<p>PHP 雙微服務之間的可靠通訊、Kafka 在 PHP 生態工具薄弱、團隊無 Kafka 經驗、production 跑數月後寫此文；明確覆蓋 XADD / XREADGROUP / consumer group / MAXLEN / MINID / XDEL / XACK / XACKDEL（Redis 8.2+）/ XTRIM。</p>
<h2 id="判讀">判讀</h2>
<p>揭露 in-memory 訊息的 payload 限制：用 payload compression + S3 hybrid storage（大 payload 存 S3、stream 只放 reference）；用 MAXLEN/MINID 控制 stream 成長。揭露 broker 選型常被「語言生態 client 品質」主導、不是純技術 feature。<strong>注意</strong>：作者是個人工程師、production 經驗但非知名公司。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：XADD/XREAD/XREADGROUP 操作 / Retention (MAXLEN/MINID) / Memory + retention 取捨。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16 Robinhood Faust</a>（語言生態對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://dev.to/mtk3d/beyond-the-hype-why-we-chose-redis-streams-over-kafka-for-our-microservices-dmc">Beyond the Hype: Why We Chose Redis Streams Over Kafka for Our Microservices</a></li>
</ul>
]]></content:encoded></item><item><title>3.C48 Airbnb Dynein：SQS 分散式延遲任務排程</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/</guid><description>&lt;p>這個案例的核心責任是說明 SQS at-least-once + DLQ 模型在工作排程的對齊邏輯。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 構建 Dynein 分散式延遲任務排程系統取代 Resque（受限於單 Redis 實例）。明確選 SQS、利用 at-least-once delivery、dead letter queue、individual message acknowledgment、access control 與 encryption-at-rest。每個 scheduler instance 達 ~1000 QPS、可水平擴展。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>at-least-once 對工作排程「不丟資料」假設足夠、SQS wrap DynamoDB 處理 &amp;gt; 15 分鐘 delay、DLQ 分離「短暫失敗」與「永久毒訊息」。揭露 managed queue 在工作排程的取捨：trade ordering 換 scaling。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / DLQ 設計。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/airbnb-engineering/dynein-building-a-distributed-delayed-job-queueing-system-93ab10f05f99">Dynein: Building a Distributed Delayed Job Queueing System&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS at-least-once + DLQ 模型在工作排程的對齊邏輯。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 構建 Dynein 分散式延遲任務排程系統取代 Resque（受限於單 Redis 實例）。明確選 SQS、利用 at-least-once delivery、dead letter queue、individual message acknowledgment、access control 與 encryption-at-rest。每個 scheduler instance 達 ~1000 QPS、可水平擴展。</p>
<h2 id="判讀">判讀</h2>
<p>at-least-once 對工作排程「不丟資料」假設足夠、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay、DLQ 分離「短暫失敗」與「永久毒訊息」。揭露 managed queue 在工作排程的取捨：trade ordering 換 scaling。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / DLQ 設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/airbnb-engineering/dynein-building-a-distributed-delayed-job-queueing-system-93ab10f05f99">Dynein: Building a Distributed Delayed Job Queueing System</a></li>
</ul>
]]></content:encoded></item><item><title>3.C49 Airbnb Inspekt：Visibility timeout 當 retry budget</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/</guid><description>&lt;p>這個案例的核心責任是說明 visibility timeout 不只是「處理時間」、可當隱式的 retry 機制。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 的 Inspekt 隱私資料掃描系統用 SQS task queue 派發 scan task（每 table/object/app 一個 message）、Scanner nodes 水平 pull。&amp;ldquo;each message reappears N times back into the queue until a scanner node deletes it&amp;rdquo; 是 visibility timeout 在實戰的應用。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 message 重現次數做 retry budget、scanner 失敗時不用自管 retry table。揭露 SQS 的「不刪除即重現」是設計、不是 bug、可以當隱式 retry 機制用。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Visibility timeout + in-flight messages。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/airbnb-engineering/automating-data-protection-at-scale-part-2-c2b8d2068216">Automating Data Protection at Scale Part 2&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 visibility timeout 不只是「處理時間」、可當隱式的 retry 機制。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 的 Inspekt 隱私資料掃描系統用 SQS task queue 派發 scan task（每 table/object/app 一個 message）、Scanner nodes 水平 pull。&ldquo;each message reappears N times back into the queue until a scanner node deletes it&rdquo; 是 visibility timeout 在實戰的應用。</p>
<h2 id="判讀">判讀</h2>
<p>用 message 重現次數做 retry budget、scanner 失敗時不用自管 retry table。揭露 SQS 的「不刪除即重現」是設計、不是 bug、可以當隱式 retry 機制用。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Visibility timeout + in-flight messages。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/airbnb-engineering/automating-data-protection-at-scale-part-2-c2b8d2068216">Automating Data Protection at Scale Part 2</a></li>
</ul>
]]></content:encoded></item><item><title>Aurora Read Replica Scaling：15 replica 上限、lag profile、headroom 預留與 fleet 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/read-replica-scaling/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/read-replica-scaling/</guid><description>&lt;p>Aurora 「最多 15 read replica」是文件數字、實際 production 部署常常更早遇到拆 cluster 的決策點 — 不是 15 replica 不夠用、是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>、業務 sharding、微服務 ownership、合規 boundary 早在 15 replica 之前就推動拆 cluster。本文同時展開兩個議題：(1) 單 cluster 內 read replica 怎麼用、容量怎麼規劃、lag 怎麼管；(2) Aurora fleet 治理的 3 條 driver、什麼條件下拆 cluster vs 加 replica。後者是 Aurora 系列的 &lt;em>fleet 治理 SSoT&lt;/em> — &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a> / &lt;a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO&lt;/a> / &lt;a href="../global-database-multi-region/">Aurora Global Database&lt;/a> / &lt;a href="../migrate-from-self-managed-pg-mysql/">Aurora migration playbook&lt;/a> 都 cross-link 到本篇、不重複展開。&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>）— 而是 read replica 跟 fleet 拓樸的實作層教學。前置閱讀建議 &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a>（理解共享 storage 為什麼能養大量 replica）。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：FanDuel Super Bowl / DraftKings 比賽日、流量 5-10 倍尖峰、read query（用戶查 balance、投注紀錄、odds）打爆 primary、需要快速擴 read replica 但又怕 lag 把 stale read 推到 user-facing。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「加 read replica 後 primary CPU 沒降、為什麼？」&lt;/li>
&lt;li>「Auto-scaling 加 replica 要幾分鐘、來不及接尖峰怎麼辦？」&lt;/li>
&lt;li>「Reader endpoint round-robin 把 query 打到 lag 大的 replica、用戶看到舊 balance」&lt;/li>
&lt;li>「業務跨 200 個 cluster、單個 cluster 才 5-10 個 replica、為什麼不集中？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：讀寫雙峰錯位是 Aurora 讀寫分流的核心 driver。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings&lt;/a> 揭露「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時讀爆量、payout event 時寫爆量、兩個峰不在同一時刻。這代表 read replica 容量規劃不是「分散負載」、而是「為讀峰專門配置 capacity」。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &amp;#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &amp;#43; Wavelength &amp;#43; Outposts 處理 20&amp;#43; 州的雙重峰值">9.C28 FanDuel&lt;/a> 揭露事件型容量分級：平日 baseline → 季後賽 2-3x → 季冠軍賽 4-5x → Super Bowl 5-10x。容量規劃要按事件級別分段、不是一律 10x。&lt;/p></description><content:encoded><![CDATA[<p>Aurora 「最多 15 read replica」是文件數字、實際 production 部署常常更早遇到拆 cluster 的決策點 — 不是 15 replica 不夠用、是 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>、業務 sharding、微服務 ownership、合規 boundary 早在 15 replica 之前就推動拆 cluster。本文同時展開兩個議題：(1) 單 cluster 內 read replica 怎麼用、容量怎麼規劃、lag 怎麼管；(2) Aurora fleet 治理的 3 條 driver、什麼條件下拆 cluster vs 加 replica。後者是 Aurora 系列的 <em>fleet 治理 SSoT</em> — <a href="../storage-architecture/">Aurora storage architecture</a> / <a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> / <a href="../global-database-multi-region/">Aurora Global Database</a> / <a href="../migrate-from-self-managed-pg-mysql/">Aurora migration playbook</a> 都 cross-link 到本篇、不重複展開。</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>）— 而是 read replica 跟 fleet 拓樸的實作層教學。前置閱讀建議 <a href="../storage-architecture/">Aurora storage architecture</a>（理解共享 storage 為什麼能養大量 replica）。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：FanDuel Super Bowl / DraftKings 比賽日、流量 5-10 倍尖峰、read query（用戶查 balance、投注紀錄、odds）打爆 primary、需要快速擴 read replica 但又怕 lag 把 stale read 推到 user-facing。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「加 read replica 後 primary CPU 沒降、為什麼？」</li>
<li>「Auto-scaling 加 replica 要幾分鐘、來不及接尖峰怎麼辦？」</li>
<li>「Reader endpoint round-robin 把 query 打到 lag 大的 replica、用戶看到舊 balance」</li>
<li>「業務跨 200 個 cluster、單個 cluster 才 5-10 個 replica、為什麼不集中？」</li>
</ul>
<p>進一步問題：讀寫雙峰錯位是 Aurora 讀寫分流的核心 driver。<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 揭露「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時讀爆量、payout event 時寫爆量、兩個峰不在同一時刻。這代表 read replica 容量規劃不是「分散負載」、而是「為讀峰專門配置 capacity」。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> 揭露事件型容量分級：平日 baseline → 季後賽 2-3x → 季冠軍賽 4-5x → Super Bowl 5-10x。容量規劃要按事件級別分段、不是一律 10x。</p>
<p>對 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 這種受監管金融、不能用單一巨型 cluster — 7 個受監管市場 = 7 個獨立 cluster、合規 boundary 比運維成本優先。</p>
<h2 id="核心機制15-replica-上限共享-storagereader-endpoint">核心機制：15 replica 上限、共享 storage、reader endpoint</h2>
<p>Aurora read replica 的 first-class concept 是 <em>共享 storage + DNS-based reader endpoint</em>。傳統 PostgreSQL streaming replication 靠 primary push WAL 給 replica、replica 自己 apply；Aurora replica 直接從共享 storage 讀已 apply 的 page、不需要 catch-up。</p>
<p><strong>15 replica 上限</strong>：</p>
<ul>
<li>每個 Aurora cluster 最多 15 個 read replica（跨 AZ）</li>
<li>跨 region replica 走 <a href="../global-database-multi-region/">Aurora Global Database</a>（不算這 15 個）</li>
<li>文件上限不是 production 真實上限 — 多數 production 部署在 5-10 replica 之間遇到拆 cluster 訊號</li>
</ul>
<p><strong>共享 storage 對 lag 的影響</strong>：</p>
<ul>
<li>Replica 不靠 logical replication catch-up、直接從共享 storage 讀</li>
<li>Lag 來源是 <em>compute node 的 buffer cache 同步</em>、不是 WAL replay</li>
<li>Typical 10-30ms、heavy write 期間可能 100ms+、但 <em>不會像 PostgreSQL 那樣 unbounded</em></li>
</ul>
<p><strong>DraftKings 揭露的「lag 可預測」frame</strong>（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">case「判讀」段第 2 點</a>）：</p>
<p>「30 秒降到 10-30 ms」的工程意義不只是「快」、而是「讓 read-after-write 變得可預測」。30 秒 lag 的世界裡、application 端做 read-after-write 要 cache 用戶最後寫入 30 秒以上、實務上做不到；10-30ms lag 的世界裡、application 可以做「寫操作後 100ms 內走 primary、之後可走 replica」的可規劃策略。</p>
<p><strong>Reader endpoint 行為</strong>：</p>
<ul>
<li>DNS-based round-robin、不感知 replica 健康狀態</li>
<li>Application 想要 lag-aware routing 要自己實作或用 RDS Proxy</li>
<li>Failover 期間短暫包含 promoted replica（已升 primary）、見 <a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a></li>
</ul>
<p><strong>Auto-scaling policy</strong>：</p>
<ul>
<li>CloudWatch metric（CPU / connection）trigger</li>
<li>Replica creation 2-5 分鐘</li>
<li><em>無法用於秒級尖峰</em> — 是 DraftKings「+50% no sweat」誤讀的關鍵點</li>
</ul>
<p><strong>跟通用 read replica 差在哪</strong>：Aurora replica 不用 catch-up WAL、lag 上限可預測；vs PostgreSQL streaming replication lag 是 unbounded（取決於 primary 寫速度）。可預測 lag 是 read-after-write 場景變得可規劃的前提。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>。</p>
<h2 id="step-by-step-配置--reader-endpoint-設計">Step-by-step 配置 / Reader endpoint 設計</h2>
<p><strong>建 read replica</strong>：</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 rds create-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">2</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">3</span><span class="cl"><span class="se"></span>  --db-instance-identifier my-replica-01 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --db-instance-class db.r6g.4xlarge <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --availability-zone us-east-1b <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --promotion-tier <span class="m">1</span></span></span></code></pre></div><p><strong>Reader endpoint vs Custom endpoint</strong>：</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"># 預設 reader endpoint：所有 replica round-robin</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 訪問 url: my-cluster.cluster-ro-xxx.us-east-1.rds.amazonaws.com</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="c1"># Custom endpoint：group 特定 replica</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws rds create-db-cluster-endpoint <span class="se">\
</span></span></span><span class="line"><span class="ln">6</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">7</span><span class="cl"><span class="se"></span>  --db-cluster-endpoint-identifier my-cluster-analytics <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --endpoint-type READER <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --static-members my-replica-analytics-01 my-replica-analytics-02</span></span></code></pre></div><p>Custom endpoint 適用場景：</p>
<ul>
<li>分析 query 走獨立 endpoint、不影響 OLTP read replica</li>
<li>Read-after-write session 走 primary endpoint、其他 read 走 reader endpoint</li>
<li>不同 SLO 的 read traffic 分流（high-priority vs batch）</li>
</ul>
<p><strong>Auto-scaling policy</strong>：</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 application-autoscaling register-scalable-target <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --service-namespace rds <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --resource-id cluster:my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --scalable-dimension rds:cluster:ReadReplicaCount <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --min-capacity <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --max-capacity <span class="m">10</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">aws application-autoscaling put-scaling-policy <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --service-namespace rds <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --resource-id cluster:my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --scalable-dimension rds:cluster:ReadReplicaCount <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --policy-name my-cluster-cpu-scaling <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --policy-type TargetTrackingScaling <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --target-tracking-scaling-policy-configuration file://scaling-config.json</span></span></code></pre></div><p><strong>預配 vs auto-scale</strong>：</p>
<ul>
<li>Peak workload 預知（賽事、促銷、季節事件）→ 提前 1 小時預配</li>
<li>Unpredictable burst → auto-scale（接受 2-5 分鐘 lead time）</li>
<li>兩者混合：baseline 預配 + auto-scale 處理 baseline 之上的浮動</li>
</ul>
<p><strong>驗證點</strong>：</p>
<ul>
<li><code>AuroraReplicaLag</code> &lt; 100ms（per replica）</li>
<li>Reader endpoint CPU 分布均勻（不是某 replica 過熱）</li>
<li>Application stale-read error rate &lt; 0.1%</li>
</ul>
<p><strong>Rollback boundary</strong>：移除 replica 即時生效、無 data loss；但 reader endpoint DNS cache 仍可能短暫 routing 到已移除 replica（5-30 秒）。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1加-replica-後-primary-cpu-沒降">Case 1：加 replica 後 primary CPU 沒降</h3>
<p>徵兆：明明加了 3 個 read replica、primary CPU 仍然 90%、reader endpoint CPU 才 10%。</p>
<p>原因：application 沒把 read query routing 到 reader endpoint、所有 query 仍打 primary。Aurora reader endpoint 不會自動分流 — 必須 application 端拆 read / write data source。</p>
<p>修：</p>
<ul>
<li>Application 端 ORM / data source layer 拆 read / write connection pool</li>
<li>寫操作用 writer endpoint、純讀走 reader endpoint</li>
<li>雙峰錯位是這層拆分的 driver（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">DraftKings case 揭露</a> 讀寫資源規劃要分開）</li>
</ul>
<h3 id="case-2reader-endpoint-round-robin-推-stale-read">Case 2：Reader endpoint round-robin 推 stale read</h3>
<p>徵兆：read-after-write 場景（用戶下注後立刻查 balance）打到 lagging replica、看到舊 balance、客訴。</p>
<p>原因：reader endpoint DNS-based round-robin、不感知 lag。Application 假設 read 永遠 fresh、但 typical 10-30ms lag 期間用戶操作就會踩到。</p>
<p>修：</p>
<ul>
<li>Sticky session：寫操作後 N 秒內同 session 走 primary（N = lag p99、typical 100ms）</li>
<li>Application 端做「下注後 N 秒走 primary」邏輯（DraftKings「可預測 lag」frame 讓 N 秒可規劃）</li>
<li>或用 RDS Proxy 提供 lag-aware routing（managed alternative）</li>
</ul>
<h3 id="case-3auto-scaling-來不及接秒級尖峰--headroom-預留判讀">Case 3：Auto-scaling 來不及接秒級尖峰 — headroom 預留判讀</h3>
<p>徵兆：賽事開賽 30 秒內流量 +50%、auto-scaling 觸發但 2-5 分鐘後才有新 replica、開賽尖峰已過、用戶在最關鍵時段看到 timeout。</p>
<p>機制限制：replica creation 2-5 分鐘、秒級尖峰過去了 replica 才上線。</p>
<p><strong>DraftKings「Super Bowl +50% no sweat」的工程意義</strong>（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">case「判讀」段第 3 點原文</a>）：「這句話的工程意義是 <em>提前做好容量規劃</em>、不是『Aurora 神奇』。寫 workload 預期可能 +50%、整個 system headroom 預留至少 50%、加上 read replica 動態加減、才能讓 50% 增幅變成『不流汗』」。</p>
<p>工程含義：</p>
<ul>
<li>Peak workload 預知（賽事 / 促銷）用 <em>headroom 預留 + <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a> 提前預配</em>、不靠 auto-scale 接秒級</li>
<li>Auto-scale 是 unpredictable burst 才用（突發新聞、KOL 推廣、未預期事件）</li>
<li>DraftKings 的「不流汗」是 <em>系統設計</em> 結果、不是 Aurora 特殊能力</li>
</ul>
<p>修：</p>
<ul>
<li>賽事日曆建模：賽前 1 小時自動加 replica、賽後 2 小時減</li>
<li>Primary instance class 升級提前一週、不是賽前升（升級期間 failover 風險）</li>
<li>Headroom 預算：read replica 預留 50%、primary CPU baseline &lt; 50%</li>
</ul>
<h3 id="case-415-replica-上限--拆-cluster-訊號">Case 4：15 replica 上限 — 拆 cluster 訊號</h3>
<p>徵兆：read traffic 持續成長、加到 15 replica 仍接近 CPU 瓶頸、想加第 16 個被 API 拒絕。</p>
<p>原因：Aurora 硬上限 15 replica / cluster、超過要拆 cluster。但實務上更常在 5-10 replica 就遇到其他拆 cluster 訊號（blast radius、ownership boundary、業務 sharding）。</p>
<p>修：見下方「邊界與整合：fleet 治理 SSoT」段、按 3 條 driver 判讀拆 cluster vs 加 replica。</p>
<h3 id="case-5heavy-write-期間-replica-lag-spike">Case 5：Heavy write 期間 replica lag spike</h3>
<p>徵兆：bulk insert / DDL 期間 replica lag 從 10-30ms 跳到 100-500ms、application 假設 typical lag 永遠成立、stale read 比例大幅上升。</p>
<p>原因：heavy write 期間 replica buffer cache invalidate 速度跟不上、lag 暫時拉大。Aurora 的「可預測 lag」不等於「lag 永遠 10-30ms」。</p>
<p>修：</p>
<ul>
<li>bulk insert / DDL 期間 application 端切到全 primary 模式（避開 stale read 風險）</li>
<li>重要 DDL 用 <a href="https://github.com/reorg/pg_repack">pg_repack</a> 或 logical migration、避免長時間 table lock</li>
<li>監測 <code>AuroraReplicaLagMaximum</code>、spike 超過 p99 threshold trigger application 端 fallback</li>
</ul>
<h3 id="case-6fanduel-雙-slo-並行--不要壓成單一數字">Case 6：FanDuel 雙 SLO 並行 — 不要壓成單一數字</h3>
<p>徵兆：team 看 FanDuel「5-10x peak」直接套到自家 streaming workload、結果 Aurora 撐不住、發現 FanDuel streaming 根本不走 Aurora。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> case「判讀」段第 1 點原文：「直播跟投注是兩種完全不同 SLO：直播容忍秒級延遲（用 CDN + ABR 串流）、投注必須毫秒級成交。兩個服務必須各自獨立擴容、各自獨立 SLO」。</p>
<p><strong>scope warning（必明示）</strong>：</p>
<ul>
<li>FanDuel 5-10x 是 <em>betting 服務的 Aurora 擴容倍數</em>、不是 streaming</li>
<li>Streaming 走 CDN、不走 Aurora</li>
<li>不能把兩種 SLO 壓縮成「Aurora 撐 5-10x」單一數字</li>
</ul>
<p><strong>case 自承的進一步 scope warning</strong>：「AWS 案例 <em>沒有</em> 提具體 betting transaction TPS、concurrent streams、延遲分布」（case「需要警惕」段）。引用 FanDuel 時不能寫「Aurora 在 betting 路徑撐 X TPS」這類細節 — case 沒提的數字不能擴寫。</p>
<p>修：</p>
<ul>
<li>不同 SLO workload 拆獨立 cluster 或拆 read / write data source</li>
<li>容量規劃看自家 workload TPS、不要套用未公開的 case 數字</li>
</ul>
<h2 id="事件型容量分級表">事件型容量分級表</h2>
<p><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> 揭露事件型 scaling 不是一律 10x — <em>事件級別</em> 是容量分級單位：</p>
<table>
  <thead>
      <tr>
          <th>事件級別</th>
          <th>倍數</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平日 baseline</td>
          <td>1x</td>
          <td>FanDuel case「判讀」段第 3 點</td>
      </tr>
      <tr>
          <td>季後賽 playoff</td>
          <td>2-3x</td>
          <td>FanDuel case 揭露事件分級</td>
      </tr>
      <tr>
          <td>季冠軍賽 championship</td>
          <td>4-5x</td>
          <td>FanDuel case 揭露事件分級</td>
      </tr>
      <tr>
          <td>Super Bowl</td>
          <td>5-10x</td>
          <td>FanDuel case 揭露事件分級</td>
      </tr>
  </tbody>
</table>
<p><strong>Frame 8 event-driven scaling 5 模式（跨 vendor 共寫）</strong>：本表是 Aurora 端從讀峰視角切入的事件分級、跟 <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 on-demand-vs-provisioned</a> 的 5 模式分類（flash-sale spike / predictable peak / sustained growth / surge baseline permanent shift / B2B sustained + 高可用）共軸。Aurora 端的 FanDuel 季賽 cycle 在 5 模式分類中對應 <em>predictable peak</em> 的時間序列展開 — 事件 tier 已知（賽季 → 季後賽 → 季冠軍賽 → Super Bowl）、按 tier 預配 read replica 數量、本質是「峰值已知 + 重複出現」的 predictable peak 在多 tier 結構下的延伸。</p>
<p><strong>KV 層 vs SQL 層的 mode 決策差異</strong>：DynamoDB 端的 on-demand vs provisioned mode 是 KV vendor 的容量抽象（軸 1 peak/avg ratio / 軸 4 predictable-peak vs flash-sale）、詳見 <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 on-demand-vs-provisioned 6 軸決策</a>、本篇不展開。Aurora 端對應的決策是 <em>read replica 數量 + auto-scaling vs scheduled scaling vs headroom 預留</em>、靠的是 replica fleet size 而非 mode 切換。</p>
<p>兩 vendor 在 Frame 8 各自承擔：</p>
<ul>
<li><strong>DynamoDB on-demand-vs-provisioned</strong>：5 模式分類 SSoT、mode × 事件型分類的合成判讀</li>
<li><strong>Aurora read-replica-scaling（本篇）</strong>：read 峰值的 headroom 預留 + 雙 SLO 並行（FanDuel 分級 + DraftKings 讀寫雙峰錯位）+ fleet 治理</li>
</ul>
<p><strong>case 自帶警示（scope warning 必保留）</strong>：</p>
<ul>
<li>「5-10x」是 <em>峰值倍數</em>、不是 <em>peak 持續時間</em>。Super Bowl 的關鍵 30 分鐘可能 8-10x、其他 3 小時可能 3-5x（case「需要警惕」段）</li>
<li>分級 driver 是「同類事件中的最高倍率」、不是恆定數字 — 引用時要保留事件 tier 對應、不是一律「Super Bowl = 10x」單一閾值</li>
<li>跨業務 transfer 判讀：本表 <em>只代表體育博彩賽季 cycle</em>、不能直接套到 e-commerce flash-sale（後者倍數結構是「秒級數千倍」、跟事件 tier 結構不同）</li>
</ul>
<p><strong>容量規劃做法</strong>：</p>
<ul>
<li>建立 event tier 體系、每 tier 對應不同 pre-scale 倍數跟 lead time（賽前 N 小時預配）</li>
<li>事件型分級的關鍵是「峰值是已知的」、不是「峰值多大」</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.11 高峰事件準備</a> 的容量分級</li>
</ul>
<h2 id="邊界與整合fleet-治理-ssot--何時拆-cluster-vs-加-replica">邊界與整合：Fleet 治理 SSoT — 何時拆 cluster vs 加 replica</h2>
<p>本段是 Aurora fleet 治理軸 SSoT — <a href="../storage-architecture/">Aurora storage architecture</a> / <a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> / <a href="../global-database-multi-region/">Aurora Global Database</a> / <a href="../migrate-from-self-managed-pg-mysql/">Aurora migration playbook</a> cross-link 不重複展開。</p>
<p><strong>跨 case 合成 frame</strong>：production scale 不是「單一巨型 cluster」而是 <em>fleet of clusters</em>、但 <em>driver 各異</em>。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>Case anchor</th>
          <th>Fleet 規模</th>
          <th>拆分判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Business sharding</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>200 cluster</td>
          <td>業務本身可切分（每體育類別 / 每地理 / 每產品線各自 cluster）、blast radius 隔離</td>
      </tr>
      <tr>
          <td>Microservice ownership</td>
          <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>多 cluster</td>
          <td>每微服務私有 store、不共用 cluster — 容量規劃分散到 service owner</td>
      </tr>
      <tr>
          <td>合規市場 boundary</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>7 cluster</td>
          <td>受監管市場資料 <em>不能跨境複製</em>、每市場獨立 cluster — Global Database 在合規場景反指標</td>
      </tr>
  </tbody>
</table>
<h3 id="driver-1business-shardingdraftkings-200-cluster">Driver 1：Business sharding（DraftKings 200 cluster）</h3>
<p>DraftKings 不用一個巨型 cluster 撐 100 萬 ops/min、而是 <em>按業務切 200 cluster</em>。每體育類別、每地理、每產品線各自 cluster、blast radius 自然隔離。</p>
<p>工程含義：</p>
<ul>
<li>業務本身就有 sharding key（sport type / region / product line）— 拆 cluster 不需要 schema redesign</li>
<li>單 cluster 故障只影響該業務、不影響全平台</li>
<li>容量規劃變成「每 cluster 的容量規劃」、單機極限不重要</li>
</ul>
<p><strong>容易誤判的邊界</strong>：<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">DraftKings 100 萬 ops/min ≈ 17K ops/sec</a> 是 <em>200 cluster 加總</em>、平均每 cluster 約 80 ops/sec（case「需要警惕」段）— 不是「單一 cluster 撐 100 萬 ops」、案例對照不能擴寫成單 cluster 容量。</p>
<h3 id="driver-2microservice-ownershipnetflix">Driver 2：Microservice ownership（Netflix）</h3>
<p>Netflix 每微服務各自有 private Aurora cluster、不共用 — 跟 monolith「一個大 DB 撐全部」相反。</p>
<p>工程含義：</p>
<ul>
<li>DB 容量規劃變成「每微服務的容量規劃」、複雜度分散到 service owner</li>
<li>跨服務 contention 變成 <em>network 議題</em> 而非 <em>DB lock 議題</em></li>
<li>每多一個微服務就多一個 cluster、operational surface area × N</li>
</ul>
<p><strong>case 自帶 scope 警示</strong>：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix 數據層遠不止 Aurora</a> — 還有 Cassandra（playback metadata）、EVCache（cache layer）、Iceberg（data warehouse）。Aurora 主要是「需要 ACID 的 OLTP 工作負載」、不是「all-purpose store」（case「需要警惕」段第 2 點）。讀者引用 Netflix consolidation 時、不能誤推論「Aurora 可以替所有 store」。</p>
<h3 id="driver-3合規市場-boundarystandard-chartered-7-cluster">Driver 3：合規市場 boundary（Standard Chartered 7 cluster）</h3>
<p>Standard Chartered 7 個受監管市場 = 7 個獨立 cluster。<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 規範資料 <em>不能跨境複製</em>、<a href="../global-database-multi-region/">Aurora Global Database</a> 在這種場景違反合規。</p>
<p>工程含義：</p>
<ul>
<li>容量規劃變成「7 個獨立規劃 × 各自合規門檻」</li>
<li>跨市場 DR 不靠 Global Database、靠應用層市場切換</li>
<li>合規 lead time 是時程主項（見 <a href="../migrate-from-self-managed-pg-mysql/">migration playbook</a> 合規時程段）</li>
</ul>
<p><strong>case 自承 scope 警示</strong>：Standard Chartered case 未公開是 PostgreSQL 還是 MySQL、未公開具體 cost 數字、屬「相關 case study」匿名對照。</p>
<h3 id="何時拆-vs-加-replica-的判讀順序">何時拆 vs 加 replica 的判讀順序</h3>
<p>按以下順序判斷、第一個成立的就是拆 cluster 的訊號：</p>
<ol>
<li><strong>&gt; 15 replica 需求</strong> → 拆 cluster（Aurora 硬上限）</li>
<li><strong>Blast radius 隔離需求</strong> → 拆 cluster（單 cluster 故障影響範圍太大、業務不能接受）</li>
<li><strong>業務本身可切分</strong>（user shard / 產品線 / 地理）→ 拆 cluster（DraftKings 拓樸）</li>
<li><strong>微服務私有 store 拓樸</strong> → 拆 cluster（Netflix 拓樸、跟服務生命週期綁定）</li>
<li><strong>合規禁止跨境複製</strong> → 拆 cluster（Standard Chartered 拓樸、Global Database 反指標）</li>
<li><strong>以上都不成立</strong> → 加 replica（最便宜的容量槓桿）</li>
</ol>
<p><strong>容易誤判的邊界</strong>：</p>
<ul>
<li>Fleet 治理本身有 ops surface area 成本（parameter group / backup / IAM / observability fan-out × N cluster）— 不是免費；driver 不夠強時不該拆</li>
<li>「fleet 看起來大」不是 driver — driver 是業務本身有 boundary、不是運維美觀</li>
<li>拆 cluster 後再合併比拆更難（資料遷移成本高）— driver 不確定時先加 replica</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</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">AuroraReplicaLag           # per replica lag
</span></span><span class="line"><span class="ln">2</span><span class="cl">AuroraReplicaLagMaximum    # cluster max lag
</span></span><span class="line"><span class="ln">3</span><span class="cl">CPUUtilization             # per replica CPU
</span></span><span class="line"><span class="ln">4</span><span class="cl">DatabaseConnections        # per replica connection</span></span></code></pre></div><p><strong>Application 端 metric</strong>：</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">read_query_latency_p99       # per endpoint (writer vs reader)
</span></span><span class="line"><span class="ln">2</span><span class="cl">stale_read_error_count       # read-after-write 失敗訊號
</span></span><span class="line"><span class="ln">3</span><span class="cl">read_replica_routing_ratio   # writer vs reader 流量比例</span></span></code></pre></div><p><strong>容量上限</strong>：</p>
<ul>
<li>15 replica / cluster（硬上限）</li>
<li>Cross-region replica 走 <a href="../global-database-multi-region/">Aurora Global Database</a>（不算 15）</li>
</ul>
<p><strong>容量公式</strong>：</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">read replica count = (read QPS / replica throughput) × (1 + lag buffer) × (1 + event tier headroom)
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">lag buffer        = 30%（典型）
</span></span><span class="line"><span class="ln">4</span><span class="cl">event tier headroom = 0% (平日) / 50% (playoff) / 100% (championship) / 200% (Super Bowl)</span></span></code></pre></div><p><strong>回路徑</strong>：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 判斷 read-bound vs write-bound、<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> peak workload 預配 vs auto-scale 決策。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../storage-architecture/">Aurora storage architecture</a> — 共享 storage 為什麼能養 15 replica + 雙峰錯位 application 邊界</li>
<li><a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> — replica 升 primary 流程</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 跨 region replica 配置 + 合規 anti-pattern</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — fleet 拓樸是 migration 規劃的維度之一</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> — read replica 是 OLTP 擴容的基本槓桿</li>
</ul>
<p><strong>RDS Proxy 整合</strong>：lag-aware routing、connection pool 共享、Lambda 場景；managed alternative。</p>
<p><strong>何時不用本文</strong>：single replica + cross-AZ failover 已滿足、read traffic 不是 bottleneck 時可跳過、看 <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 overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read 卡片</a> — read-after-write 容忍度</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 cluster business sharding 跟 headroom 預留</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — 微服務私有 store + Aurora 非 all-purpose store 邊界</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 合規驅動 fleet 拓樸</li>
<li><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 雙 SLO 並行 + 事件型容量分級</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html">Aurora replication</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 &lt;em>serializable default 對 application transaction contract 的重塑&lt;/em>。&lt;/p>
&lt;p>&lt;strong>Scope warning（最高、F4 Frame 2）&lt;/strong>：&lt;strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露&lt;/strong>。3 個 CockroachDB direct case（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a>）對 application transaction retry contract 重塑的揭露 &lt;em>都偏弱&lt;/em> — DoorDash case 只寫 PostgreSQL wire &lt;em>protocol-level&lt;/em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、&lt;strong>沒&lt;/strong>直接寫 &lt;code>40001 serialization_failure&lt;/code> / &lt;code>SAVEPOINT cockroach_restart&lt;/code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 &lt;em>自己跑 application audit&lt;/em> 而不是直接套合成的 pattern。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊&lt;/h2>
&lt;p>團隊從 PostgreSQL（default &lt;code>READ COMMITTED&lt;/code>）遷到 CockroachDB（default &lt;code>SERIALIZABLE&lt;/code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 &lt;code>40001 serialization_failure&lt;/code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 <em>serializable default 對 application transaction contract 的重塑</em>。</p>
<p><strong>Scope warning（最高、F4 Frame 2）</strong>：<strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露</strong>。3 個 CockroachDB direct case（<a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> / <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> / <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>）對 application transaction retry contract 重塑的揭露 <em>都偏弱</em> — DoorDash case 只寫 PostgreSQL wire <em>protocol-level</em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、<strong>沒</strong>直接寫 <code>40001 serialization_failure</code> / <code>SAVEPOINT cockroach_restart</code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 <em>自己跑 application audit</em> 而不是直接套合成的 pattern。</p></blockquote>
<hr>
<h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊</h2>
<p>團隊從 PostgreSQL（default <code>READ COMMITTED</code>）遷到 CockroachDB（default <code>SERIALIZABLE</code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 <code>40001 serialization_failure</code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。</p>
<p>讀者常問：</p>
<ul>
<li>為什麼同樣的 transaction 在 CockroachDB 一直 retry、在 PostgreSQL 從來不會？</li>
<li><code>40001 serialization_failure</code> error 怎麼處理、能不能直接 swallow？</li>
<li>我要把所有 application transaction 都改成 retry loop 包起來嗎？</li>
<li>能不能改 isolation level 回 <code>READ COMMITTED</code>、放棄 serializable 保證？</li>
</ul>
<p>四題的回答都依賴一個前提：CockroachDB 的 application transaction contract 跟 PostgreSQL default 不一樣、必須重塑。</p>
<h3 id="scope-warning-explicit-labeldoordash-case-沒揭露-retry-pattern">Scope warning explicit label：DoorDash case 沒揭露 retry pattern</h3>
<p><strong>DoorDash case 沒直接揭露 serializable retry contract / 40001 / SAVEPOINT pattern / hot row contention</strong>。case 只寫「PostgreSQL wire protocol 相容、實際 SQL 行為（serializable default、retry semantics、partial index）<em>仍要驗證</em>」（DoorDash 觀察段 / 策略段 3、F4.4）。</p>
<p>本章 retry pattern 議題是從 PG → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露。引用 DoorDash 時應該用：</p>
<ul>
<li><strong>正確口徑</strong>：「DoorDash 揭露 Aurora Postgres 1.636 M QPS 撞牆 → 引出 distributed SQL retry contract 需求、本章 retry pattern 議題是從 PostgreSQL → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露」</li>
<li><strong>不要寫成</strong>：「DoorDash retry pattern」、「DoorDash 揭露 40001 處理」之類把合成包成 case fact 的語法</li>
</ul>
<h3 id="case-anchortrigger-context不是-ground-truth">Case anchor（trigger context、不是 ground truth）</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>：提供「PG wire 相容、SQL 行為仍要 audit」的 case 警語（F4.4）、作為本章 <em>為什麼 retry contract 要重塑</em> 的觸發訊號。retry pattern 本體走 standard-driven（Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs）</li>
</ul>
<p>Sibling 對照 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora financial ledger</a> 提供 <em>PostgreSQL READ COMMITTED + Aurora</em> 的另一條路徑 — 用 application-level sharding（200 個獨立 Aurora cluster）避開 retry、而不是處理 retry。<strong>Scope warning</strong>：DraftKings case <em>沒</em> 寫 PostgreSQL READ COMMITTED retry pattern、case 是 Aurora 內 business sharding 路徑。本章引用 DraftKings 為「假想若把 DraftKings 遷 CockroachDB 會撞到 retry contract 重塑」合成對照、不是 case 直接揭露。</p>
<h2 id="核心機制serializable-default-跟-postgresql-的差異">核心機制：serializable default 跟 PostgreSQL 的差異</h2>
<blockquote>
<p><strong>來源分層</strong>：本段機制來源是 Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs（standard-driven）、<em>不是</em> 從 case 抽取。3 個 direct case 都沒揭露這些機制細節。</p></blockquote>
<h3 id="serializable-是-cockroachdb-的-default">Serializable 是 CockroachDB 的 default</h3>
<p>CockroachDB 預設 <code>SERIALIZABLE</code> — 最強 isolation level、保證 transaction 結果等同某個 serial order（即所有 transaction 像逐個按順序執行）。對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL default</th>
          <th>CockroachDB default</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Isolation</td>
          <td>READ COMMITTED</td>
          <td>SERIALIZABLE</td>
      </tr>
      <tr>
          <td>衝突處理</td>
          <td>後 writer 等 lock</td>
          <td>衝突即 abort、丟 40001</td>
      </tr>
      <tr>
          <td>機制</td>
          <td>row lock + MVCC</td>
          <td>timestamp ordering + write intent</td>
      </tr>
      <tr>
          <td>Retry 必要性</td>
          <td>通常不需要</td>
          <td>application 必須有 retry loop</td>
      </tr>
      <tr>
          <td>SSI 對應</td>
          <td>PG SSI（opt-in）</td>
          <td>預設啟用</td>
      </tr>
  </tbody>
</table>
<h3 id="conflict-detectionread--write-set-衝突就-abort">Conflict detection：read / write set 衝突就 abort</h3>
<p>CockroachDB 追蹤每個 transaction 的 read set 跟 write set。當兩個並行 transaction 的 read / write set 衝突、CockroachDB abort 後到的那個、發 <a href="/blog/backend/knowledge-cards/serialization-failure/" data-link-title="Serialization Failure" data-link-desc="SERIALIZABLE isolation 衝突偵測後 abort 的協議、SQL state 40001、application 必須包 retry loop">Serialization Failure</a>（<code>40001 serialization_failure</code>）。</p>
<p>對比 PostgreSQL serializable（SSI）：兩者都是「post-detect」、commit 時偵測 anomaly、不是 pre-lock。差別在 <em>衝突偵測時機</em> 跟 <em>成本</em>：</p>
<ul>
<li>PostgreSQL SSI：用 predicate lock 追蹤 query 條件、commit 時偵測</li>
<li>CockroachDB：用 timestamp ordering + write intent、衝突 <em>當下</em> 就 abort</li>
</ul>
<p>CockroachDB 的成本在「衝突立刻 abort 不等 commit」、好處是「retry window 較短、不會跑完整個 transaction 才發現衝突」。</p>
<h3 id="application-端-retrydriver-不自動處理">Application 端 retry：driver 不自動處理</h3>
<p>關鍵：<strong>CockroachDB driver 不自動 retry</strong>。application 收到 <code>40001 serialization_failure</code> 必須自己決定怎麼處理 — exponential backoff retry、circuit break、或拋給上層。</p>
<p>對比 PostgreSQL：PostgreSQL READ COMMITTED 幾乎不會丟 serialization failure（後 writer 等 lock 不 abort）、SERIALIZABLE 才會、但多數 application 沒走 SERIALIZABLE。CockroachDB <em>預設</em> 就是 SERIALIZABLE、所以 retry loop 是 <em>必要</em>、不是 optional。</p>
<h3 id="savepoint-pattern官方推薦寫法">Savepoint pattern：官方推薦寫法</h3>
<p>Cockroach Labs 官方推薦的 retry pattern 用 <code>SAVEPOINT cockroach_restart</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 做正常 transaction 工作
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="n">RELEASE</span><span class="w"> </span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 如果中途 40001：
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">-- ROLLBACK TO SAVEPOINT cockroach_restart;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 重新跑 transaction body、再 RELEASE + COMMIT</span></span></span></code></pre></div><p><code>cockroach_restart</code> 是特殊保留 savepoint name — CockroachDB 認得這個名字、會把 <code>ROLLBACK TO SAVEPOINT cockroach_restart</code> 視為「重啟整個 transaction」而不是部分 rollback。</p>
<h3 id="read-committed-是-v232-可選降級">READ COMMITTED 是 v23.2+ 可選降級</h3>
<p>CockroachDB v23.2+ 新增 <code>READ COMMITTED</code> isolation level — application 可選擇用 weaker isolation 換少 retry。但這是「降級」、失去 serializable 保證 — 對應的反例段在失敗模式段展開（金融 ledger 走 READ COMMITTED 可能讓 balance 變負）。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> 跟 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a>。</p>
<h3 id="doordash-case-對接點trigger-context-only">DoorDash case 對接點（trigger context only）</h3>
<p>DoorDash case 揭露 PG wire <em>protocol-level</em> 相容、明示 SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」（F4.4）。本章機制段就是回答「audit 什麼」的具體展開 — 但 audit checklist 本體屬通用工程知識、case 沒 ground truth。</p>
<p>引用紀律：「DoorDash 揭露 PG wire 相容、SQL 行為仍要 audit、其中 serializable default 跟 retry semantics 是 application contract 重塑的核心議題」— 把 case 揭露的 fact 跟本章合成的 frame 分開講。</p>
<h2 id="操作流程retry-loop-設計">操作流程：retry loop 設計</h2>
<h3 id="retry-loop-偽碼">Retry loop 偽碼</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">for</span> <span class="nx">attempt</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempt</span> <span class="p">&lt;</span> <span class="nx">MAX_RETRIES</span><span class="p">;</span> <span class="nx">attempt</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</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="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">();</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</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">// ... 跑 transaction body ...</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="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;RELEASE SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="c1">// 成功</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nf">isSerializationFailure</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// SQLSTATE == &#34;40001&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">backoff</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="nx">math</span><span class="p">.</span><span class="nf">Pow</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">attempt</span><span class="p">)))</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">backoff</span> <span class="o">+</span> <span class="nf">jitter</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span> <span class="c1">// 非 retry-able error</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">return</span> <span class="nx">ErrMaxRetriesExceeded</span></span></span></code></pre></div><p>關鍵點：</p>
<ul>
<li>exponential backoff with jitter（避免 retry storm 同步）</li>
<li>max retry 上限（避免無限 loop、要有 circuit breaker）</li>
<li>只 retry serialization failure、其他 error 直接拋</li>
<li>transaction body 必須是 <em>冪等</em> 的（同樣 input 多次執行結果一致）</li>
</ul>
<h3 id="配置">配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 改 transaction isolation level（v23.2+ 才支援 READ COMMITTED）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看當前 session 預設
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="n">default_transaction_isolation</span><span class="p">;</span></span></span></code></pre></div><h3 id="驗證點">驗證點</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 transaction retry 統計
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">txn_stats</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看哪些 query / table 衝突最多
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">cluster_contention_events</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">count</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><h3 id="idempotency-設計transaction-body-必須冪等">Idempotency 設計：transaction body 必須冪等</h3>
<p>retry-safe transaction body 必須冪等 — 同樣 input 多次執行結果一致。這是 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 在 distributed SQL retry contract 下的具體展開、不是 optional：</p>
<table>
  <thead>
      <tr>
          <th>Transaction body</th>
          <th>是否冪等</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>UPDATE balance SET balance = balance - 100</code></td>
          <td>是</td>
          <td>同樣 input 每次都減 100</td>
      </tr>
      <tr>
          <td><code>UPDATE balance SET balance = 900</code></td>
          <td>是</td>
          <td>設成絕對值、retry 不影響</td>
      </tr>
      <tr>
          <td><code>INSERT INTO logs VALUES (...)</code></td>
          <td>否</td>
          <td>retry 後重複寫、要加 UNIQUE constraint</td>
      </tr>
      <tr>
          <td><code>INSERT ON CONFLICT (id) DO NOTHING</code></td>
          <td>是</td>
          <td>用 ON CONFLICT 處理重複</td>
      </tr>
      <tr>
          <td><code>UPDATE counter SET val = val + 1</code></td>
          <td>否（語意問題）</td>
          <td>retry 後加超過預期次數</td>
      </tr>
  </tbody>
</table>
<p>冪等性是 application 設計議題、不是 CockroachDB 配置可解的 — application contract 重塑的核心成本就在這。</p>
<h3 id="rollback-邊界">Rollback 邊界</h3>
<p>transaction 自身有 <code>SAVEPOINT cockroach_restart</code> 邊界、<code>ROLLBACK TO SAVEPOINT</code> 後可重試整個 transaction body。但：</p>
<ul>
<li>commit 後不可回滾 — 業務狀態還原只能新交易補償</li>
<li>application 端如果在 transaction <em>外</em> cache state、retry 後 state 不一致（見失敗模式段）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="retry-stormcontention-嚴重時-cpu-雪崩">Retry storm：contention 嚴重時 CPU 雪崩</h3>
<p>當高頻寫入撞同一 row（例：全局 counter、熱門商品 inventory）、serializable 衝突率可能 100%、application 端 retry loop 不斷重跑、CPU 雪崩。</p>
<p>修法：</p>
<ul>
<li>Max retry 上限 + circuit breaker：超過就放棄、回 5xx 給 client、避免 retry storm 拖垮 cluster</li>
<li>改 schema 避開 hot row（partition by region、shard counter、用 sequence 代替全局 counter）</li>
<li>監控 <code>crdb_internal.cluster_contention_events</code>、針對 top-N table 改設計</li>
</ul>
<h3 id="非冪等-transaction-重試double-count">非冪等 transaction 重試：double-count</h3>
<p>最危險的 production bug：transaction body 不是冪等的、retry 後資料重複寫。ledger double-count、payment 重複扣款、log 重複記錄。</p>
<p>修法：</p>
<ul>
<li>transaction body 寫成 <code>UPDATE balance SET balance = balance - X</code>（相對運算）、不寫 <code>UPDATE balance SET balance = Y</code>（絕對賦值依賴 read 結果）</li>
<li><code>INSERT</code> 加 UNIQUE constraint + <code>ON CONFLICT DO NOTHING</code></li>
<li>用 idempotency key（client 帶 UUID、server 端 dedupe）</li>
</ul>
<h3 id="cross-statement-state-假設">Cross-statement state 假設</h3>
<p>application 在 transaction <em>外</em> cache state（例：開 transaction 前 read 一個值、跑 transaction 期間用 cached 值）— retry 從 SAVEPOINT 重來時、cached state 不會重新讀、retry 後 state 不一致。</p>
<p>修法：</p>
<ul>
<li>把 cached state 改成在 transaction 內 read</li>
<li>retry loop 內 reset 所有 cached state</li>
<li>用 closure / scope 限制 cache 的生命週期到 transaction 內</li>
</ul>
<h3 id="hot-row-contention">Hot row contention</h3>
<p>高頻 update 同一 row（例：全局計數器、熱門商品庫存、世界冠軍直播觀眾數）— serializable 衝突率接近 100%、無論 retry 多少次都繼續衝突。</p>
<p>修法（schema-level、不是 application-level）：</p>
<ul>
<li>用 sequence 或 distributed counter（每節點本地 + 定期 aggregate）</li>
<li>partition by hash key、把單一 row 拆成 N 個 sub-row</li>
<li>改 <em>append-only</em> + 定期 aggregate（事件流 + materialized view）</li>
</ul>
<h3 id="改-read-committed-後忘了驗證業務語意">改 READ COMMITTED 後忘了驗證業務語意</h3>
<p>v23.2+ 可改 <code>READ COMMITTED</code>、少 retry 但失去 serializable 保證。對金融 ledger：READ COMMITTED 可能讓 balance 變負（兩個並行 withdraw 都看到 balance=100、都扣 50、結果 balance=-50）。</p>
<p>修法：</p>
<ul>
<li>金融 / 庫存 / 配額這類 <em>strict consistency</em> 場景必須留 SERIALIZABLE</li>
<li>READ COMMITTED 只用在 <em>容忍 stale read</em> 的場景（搜尋結果 / 分析 dashboard）</li>
<li>改 isolation level 前 <em>跑 application audit</em>、確認業務語意能容忍</li>
</ul>
<h3 id="long-running-transactionretry-機率隨時間線性上升">Long-running transaction：retry 機率隨時間線性上升</h3>
<p>transaction read 開始時間早、commit 時 conflict window 大、retry 機率隨 transaction duration 線性上升。</p>
<p>修法：</p>
<ul>
<li>transaction scope 縮小 — 只包必要 read / write、不要把 RPC call / external API 放 transaction 內</li>
<li>kill long-running query（<code>SHOW SESSIONS</code> + <code>CANCEL QUERY</code>）</li>
<li>把 batch update 拆成多個小 transaction、加 idempotency key</li>
</ul>
<h3 id="distributed-deadlock-跟-retry-互動">Distributed deadlock 跟 retry 互動</h3>
<p>CockroachDB 用 distributed deadlock detection（每個 node 維護 wait-for graph、定期跨 node 交換）跟 PostgreSQL local lock 表的 deadlock detection 不同。一般情況下、被 detector 選為 victim 的 transaction 會直接 abort、application retry loop 應該收到 <code>40001</code> 後重跑。但在三種 corner case 下會跟 retry loop 形成雪崩 pattern：</p>
<ul>
<li>多 transaction 同時撞同一組熱 row、deadlock detector 跨節點時間窗有 lag、多個 victim 同時 abort 後同時 retry、撞回同一個 deadlock window</li>
<li>跨節點的 distributed deadlock 偵測週期（預設 200ms+）放大 application retry latency、application 的 retry backoff 沒對齊偵測週期、形成「detect → abort → 快速 retry → 再 deadlock」迴圈</li>
<li>Application 把 deadlock victim 當 <code>40001</code> 直接 retry、不分流出來看、就難以從 metric 區分「serialization conflict retry」跟「distributed deadlock retry」、調 schema / contention 的策略會用錯方向</li>
</ul>
<p>修法（屬通用工程議題、case 未直接揭露）：</p>
<ul>
<li>Retry backoff 至少對齊 distributed deadlock 偵測週期、避免在偵測窗內快速 retry</li>
<li>加 jitter、不同 session 的 retry 不同步</li>
<li>Application metric 分桶記錄 <code>serialization_conflict_retry</code> vs <code>distributed_deadlock_retry</code>、避免 contention 改善方向判錯</li>
<li>Schema 設計階段避免「跨節點熱 row 環形依賴」（例：兩個服務交叉 update 對方的 counter row）</li>
</ul>
<h3 id="跨-case-合成-scope-warningdraftkings-對照">跨 case 合成 Scope warning：DraftKings 對照</h3>
<p>DraftKings ledger 對照 — <strong>DraftKings case 沒寫 PostgreSQL READ COMMITTED retry pattern</strong>、case 內容是「Aurora 內 business sharding 路徑」、用 200 個獨立 cluster 解 Aurora single-primary 撞牆。本章把 DraftKings 拿來當「假想若遷 CockroachDB 需改 SERIALIZABLE + retry loop」的合成對照、不是 case 揭露的 fact。</p>
<p>實際 DraftKings 走 Aurora + application sharding 而非 CockroachDB、所以「DraftKings retry pattern」這個說法本身就是合成 — 應該寫成「DraftKings 走 Aurora sharding 避開 retry contract 重塑、若改走 CockroachDB 則需處理本章描述的 application 改寫」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Transaction retry rate</code>：per table、per session</li>
<li><code>Serialization failure rate</code>：絕對值 + ratio</li>
<li><code>Transaction duration p99</code>：long-running 是 retry 的根因之一</li>
<li><code>Hot ranges by retry count</code>：top contention 來源</li>
<li>Application metric：retry count per request、retry-induced latency p99、circuit breaker trip count</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>基底 QPS × (1 + avg retry count) = 實際 transaction load</li>
<li>例：1000 QPS、avg retry = 0.3 → 實際 cluster 處理 1300 transaction/s</li>
</ul>
<p>retry rate 是 <em>容量規劃必納入</em> 的變數 — 沒算 retry 就會 underestimate 真實 load。</p>
<h3 id="tuning">Tuning</h3>
<ul>
<li>reduce transaction scope：transaction 越短、conflict window 越小</li>
<li>kill long-running query：transaction 過長要主動截斷</li>
<li>partition hot rows：schema-level 解 hot contention</li>
<li>改 isolation 到 READ COMMITTED（如果業務語意允許）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 retry-bound vs CPU-bound</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> retry rate × baseline QPS</li>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：為什麼 serializable 是 distributed SQL 的合理 default</li>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：partition 降低 hot row contention</li>
<li><a href="../survival-goals/">survival goals</a>：cross-region latency 加長 retry window</li>
</ul>
<h3 id="跟-postgresql-對照">跟 PostgreSQL 對照</h3>
<p>PostgreSQL READ COMMITTED 是 default、application 沒 retry loop 是 acceptable。遷 CockroachDB <em>必須</em> 重塑 application transaction contract — 這是 migration 階段最容易 underestimate 的成本。</p>
<p>對應 PostgreSQL MVCC + SSI 機制細節、見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a>。</p>
<h3 id="migration-playbook">Migration playbook</h3>
<p>PG → CockroachDB 的 application audit 必看 transaction shape：</p>
<ul>
<li>每個 transaction 的 read / write set 預估衝突率</li>
<li>是否冪等（retry-safe）</li>
<li>transaction duration（long-running 是 retry 放大器）</li>
<li>業務語意能否容忍 READ COMMITTED（避開 retry 的 fallback）</li>
</ul>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 上游 — distributed transaction 邊界</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>純 read-only workload、無 contention</li>
<li>已用 PostgreSQL serializable（application contract 相似、遷移衝擊小）</li>
<li>用 CockroachDB v23.2+ READ COMMITTED 且業務允許 stale read</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>（trigger context — PG wire 相容警語）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>（合成對照 — Aurora sharding 路徑）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/transactions.html">CockroachDB Transactions</a> / <a href="https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference.html">Transaction Retry Error Reference</a> / <a href="https://www.cockroachlabs.com/docs/stable/read-committed.html">READ COMMITTED v23.2 announcement</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong + multi-region 互斥的 AP 取捨</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/</guid><description>&lt;p>Cosmos DB 是 &lt;em>AP 系統&lt;/em>（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP&lt;/a> 三選二、放棄跨 region linearizability 換取 multi-region write 可用性）。跨 region 寫同一筆 document 必然有 conflict、Cosmos DB 提供三種 resolution policy 處理：LWW（Last-Writer-Wins）、custom merge stored procedure、conflict feed manual reconciliation。本文先講 AP 取捨的硬約束（為什麼 Strong consistency 跟 multi-region write 互斥）、再進三種 resolution 機制、再進廣告 SLA vs 實測可用性的鏈路拆解（DB 端 SLA 不等於使用者體驗）。&lt;/p>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a> 的深度展開、也是 &lt;em>Strong + multi-region 互斥&lt;/em> 議題的 SSoT 主寫位置（&lt;a href="../consistency-levels-engineering/">consistency-levels-engineering&lt;/a> cross-link 過來、不展開）。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（AR 遊戲跨 region 寫入、5 consistency level + multi-region SLA）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（Black Friday 全球零售）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a>（鏈路 SLA 拆解、跨 vendor 適用做 frame anchor）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>、本篇不重複展開。Multi-region write + conflict resolution 是 &lt;em>已選 Cosmos DB 後&lt;/em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner 或 Cosmos DB Strong（單一 write region）、不是用 LWW 補。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 是 <em>AP 系統</em>（<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 三選二、放棄跨 region linearizability 換取 multi-region write 可用性）。跨 region 寫同一筆 document 必然有 conflict、Cosmos DB 提供三種 resolution policy 處理：LWW（Last-Writer-Wins）、custom merge stored procedure、conflict feed manual reconciliation。本文先講 AP 取捨的硬約束（為什麼 Strong consistency 跟 multi-region write 互斥）、再進三種 resolution 機制、再進廣告 SLA vs 實測可用性的鏈路拆解（DB 端 SLA 不等於使用者體驗）。</p>
<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a> 的深度展開、也是 <em>Strong + multi-region 互斥</em> 議題的 SSoT 主寫位置（<a href="../consistency-levels-engineering/">consistency-levels-engineering</a> cross-link 過來、不展開）。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（AR 遊戲跨 region 寫入、5 consistency level + multi-region SLA）+ <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（Black Friday 全球零售）+ <a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a>（鏈路 SLA 拆解、跨 vendor 適用做 frame anchor）。</p>
<blockquote>
<p><strong>Cosmos DB 適用度前置判讀</strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>、本篇不重複展開。Multi-region write + conflict resolution 是 <em>已選 Cosmos DB 後</em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner 或 Cosmos DB Strong（單一 write region）、不是用 LWW 補。</p></blockquote>
<h2 id="問題情境active-active-的-conflict-是必然代價">問題情境：active-active 的 conflict 是必然代價</h2>
<p>典型觸發場景：產品要 global active-active（每個 region 都能寫、低延遲）、Cosmos DB 是 AP 系統、不像 Spanner 用 quorum 強一致；跨 region 寫同一筆 document 必然有 conflict、團隊不知道「conflict 真的發生時、誰贏 / 怎麼處理 / 業務語義保不保得住」。</p>
<p>讀者徵兆：</p>
<ul>
<li>「multi-region write 開了、user 在 A region 寫『加入購物車』、B region 寫『移除購物車』、最後哪個贏」</li>
<li>「LWW 用 timestamp 決定、client clock skew 不就破壞了嗎」</li>
<li>「conflict feed 是什麼、要不要消費」</li>
<li>「multi-region write 開了之後 consistency level 還能設 Strong 嗎」</li>
<li>「廣告寫 99.999%、為什麼實測只有 99%」</li>
</ul>
<p>真實壓力：購物車跨 region 寫入丟失、遊戲玩家狀態跨 region 衝突回滾、IoT device 跨 region 寫 telemetry 後消失。這些事故的根因不是 bug、是 multi-region write 的 <em>設計取捨</em>、需要在 selection 階段就決定 conflict resolution policy。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="ap-取捨的硬約束為什麼-strong--multi-region-write-互斥">AP 取捨的硬約束：為什麼 Strong + multi-region write 互斥</h3>
<p>Cosmos DB 是 AP 系統（在 partition 的情況下選 availability 跟 partition tolerance、放棄 cross-region linearizability）。multi-region write 的兩個前置條件：</p>
<ul>
<li>account 開啟 <code>enableMultipleWriteLocations = true</code></li>
<li>consistency level <em>不能設 Strong</em>（multi-region write 跟 Strong 互斥、時間敏感 claim、查 <a href="https://learn.microsoft.com/azure/cosmos-db/consistency-levels">最新文件</a>）</li>
</ul>
<p>為什麼互斥（CAP 三選二的硬約束）：</p>
<ul>
<li><strong>Strong consistency</strong> 在 Cosmos DB 的實作是 quorum-based linearizable read — 確保 read 拿到最新 commit、需要 <em>單一 write region</em> 來保證寫入順序</li>
<li><strong>Multi-region write</strong> 是 active-active、每個 region 都能寫 — 不存在「單一 write region」、寫入是 LWW-based eventual consistency</li>
<li>兩者在技術上 <em>不能同時成立</em> — 不是 Microsoft 工程選擇問題、是 distributed system 的基本限制（跟 Spanner 用 Paxos quorum + TrueTime 不同的設計路徑）</li>
</ul>
<p>對 selection 的意義：產品要「全球都能寫」就接受 eventual consistency；產品要「全球 linearizable」就轉 Spanner / Aurora DSQL、Cosmos DB 不是替代品。把 Cosmos DB Strong 跟 Spanner external consistency 等同視之是 <em>常見的選型誤判</em>。</p>
<p><a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 的 Strong 段只 cross-link 過來、不展開 conflict resolution 細節 — 本篇是 SSoT 主寫位置。</p>
<h3 id="conflict-偵測">Conflict 偵測</h3>
<p>同一 document（partition key + id）在多 region 並發寫入、Cosmos DB 偵測為 conflict。偵測機制基於 LSN（log sequence number）、不是 timestamp — 兩個 region 對同一 document 寫入時、replication 過程比對 LSN 發現分歧、進 resolution。</p>
<h3 id="三種-conflict-resolution-policy">三種 conflict resolution policy</h3>
<h4 id="lwwlast-writer-wins預設">LWW（Last-Writer-Wins、預設）</h4>
<ul>
<li>機制：用 <code>_ts</code>（system timestamp）或自訂 numeric property、value 大的贏</li>
<li>副作用：clock skew 在 ms 級就能讓「先寫的反而贏」、業務邏輯破洞</li>
<li>適合：純覆寫場景（如玩家位置最新值、IoT 最新讀數）— write 順序不影響業務語義</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;LastWriterWins&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionPath&#34;</span><span class="p">:</span> <span class="s2">&#34;/customTimestamp&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="custom-merge-stored-procedure">Custom merge stored procedure</h4>
<ul>
<li>機制：寫一個 JavaScript stored proc、conflict 時 Cosmos DB 呼叫、proc 回傳 merge 結果</li>
<li>適合：要保留業務語義的場景（購物車 merge = union 兩邊 items、計數器 merge = sum、status 機器 merge = 狀態圖規則）</li>
<li>風險：stored proc 在 Cosmos DB JavaScript runtime 跑、有 timeout / RU 限制；複雜 merge 邏輯難 debug</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionProcedure&#34;</span><span class="p">:</span> <span class="s2">&#34;dbs/mydb/colls/mycoll/sprocs/resolveCart&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="conflict-feed-manual-reconciliation">Conflict feed manual reconciliation</h4>
<ul>
<li>機制：Cosmos DB 把 conflict 寫入 conflict feed、不自動解決、app 自行消費並 reconcile</li>
<li>適合：conflict 需要人工 / 業務流程判斷、不能 auto-resolve（如金融交易、合規場景）</li>
<li>風險：feed 不消費就累積、後續分析失準；app 需要實作 reconcile 流程</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span> <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span> <span class="p">}</span></span></span></code></pre></div><p>（沒指 procedure、conflict 全進 feed、app 用 SDK <code>ReadConflictsAsync()</code> / Change Feed Processor pattern 消費）</p>
<h3 id="跟其他-vendor-對比">跟其他 vendor 對比</h3>
<ul>
<li><strong>DynamoDB Global Tables</strong>：也是 LWW、<em>無</em> custom merge、<em>無</em> conflict feed — 行為比 Cosmos DB 簡單但彈性少</li>
<li><strong>Spanner</strong>：用 Paxos quorum、<em>不會有 conflict</em>（CP 系統、可用性換一致性）— 跨 region write 需 quorum、latency 100-200ms</li>
<li><strong>Aurora Global Database</strong>：single-primary（一個 region 寫、其他 region 讀）、不是真 multi-region write、無 conflict</li>
</ul>
<p>對應 knowledge cards：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="開啟-multi-region-write">開啟 multi-region write</h3>





<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">az cosmosdb update --name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --enable-multiple-write-locations <span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --locations <span class="nv">regionName</span><span class="o">=</span>eastus <span class="nv">failoverPriority</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --locations <span class="nv">regionName</span><span class="o">=</span>westeurope <span class="nv">failoverPriority</span><span class="o">=</span><span class="m">1</span></span></span></code></pre></div><p>開啟後 <em>不能直接關回</em>、要 disable + 改 region 配置 + re-enable、有停機窗口。</p>
<h3 id="設定-lww-policycontainer-層">設定 LWW policy（container 層）</h3>
<p>建 container 時指定、可事後改但 conflict 行為以新 policy 為準（既有 conflict 不會重 resolve）。預設用 <code>_ts</code> 比較；改成 customTimestamp 時要保證 application 寫入時 <em>用單調遞增</em> 的 timestamp source（不能用 client clock）。</p>
<h3 id="設定-custom-merge">設定 custom merge</h3>
<p>建 stored proc：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">resolveCart</span><span class="p">(</span><span class="nx">incomingItem</span><span class="p">,</span> <span class="nx">existingItem</span><span class="p">,</span> <span class="nx">isTombstone</span><span class="p">,</span> <span class="nx">conflictingItems</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 範例：merge 購物車 items（取 union）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">merged</span> <span class="o">=</span> <span class="nx">existingItem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">merged</span><span class="p">.</span><span class="nx">items</span> <span class="o">=</span> <span class="nx">mergeArrays</span><span class="p">(</span><span class="nx">existingItem</span><span class="p">.</span><span class="nx">items</span><span class="p">,</span> <span class="nx">incomingItem</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">merged</span><span class="p">.</span><span class="nx">_ts</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">existingItem</span><span class="p">.</span><span class="nx">_ts</span><span class="p">,</span> <span class="nx">incomingItem</span><span class="p">.</span><span class="nx">_ts</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">__</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">setBody</span><span class="p">(</span><span class="nx">merged</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionProcedure&#34;</span><span class="p">:</span> <span class="s2">&#34;dbs/mydb/colls/mycoll/sprocs/resolveCart&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>驗證：proc 內處理 timeout / exception；測 edge case（空 array / null / 並發 3+ region 寫入）。</p>
<h3 id="消費-conflict-feed">消費 conflict feed</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// .NET SDK</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kt">var</span> <span class="n">iterator</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="n">GetItemQueryIterator</span><span class="p">&lt;</span><span class="n">ConflictProperties</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;SELECT * FROM c&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="n">iterator</span><span class="p">.</span><span class="n">HasMoreResults</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">iterator</span><span class="p">.</span><span class="n">ReadNextAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">conflict</span> <span class="k">in</span> <span class="n">response</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">await</span> <span class="n">ProcessConflict</span><span class="p">(</span><span class="n">conflict</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>用 Change Feed Processor pattern 把 conflict feed 當 stream 消費、寫到 reconcile queue、由業務流程處理。</p>
<h3 id="驗證點">驗證點</h3>
<ul>
<li>跨 region 並發寫測試（synthetic load）、觀察 conflict count / resolution result</li>
<li>Custom merge stored proc 跑過 edge case（exception / null / 並發 3+）</li>
<li>Conflict feed 不積壓（lag &lt; 5 min）</li>
<li>Region 故障時 application 仍能寫（active-active 設計、不需 manual failover）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1全用-lww--用-server-timestamp">Failure 1：全用 LWW + 用 server timestamp</h3>
<p>clock skew 在 ms 級可能讓「先寫的反而贏」、業務邏輯破洞。常見徵兆：使用者反映「我明明先按確認、後來改的反而是舊的」、debug 才發現是跨 region clock skew。</p>
<p>修：</p>
<ul>
<li>用 <code>customTimestamp</code> 從 application 端 monotonic source 取（如 Snowflake ID、HLC、Lamport clock）</li>
<li>或改用 custom merge stored proc、用業務邏輯而非 timestamp 決勝</li>
<li>或拆 collection、把 conflict 高的 collection 用 stored proc、低的用 LWW</li>
</ul>
<h3 id="failure-2業務語義不適合-lww">Failure 2：業務語義不適合 LWW</h3>
<p>購物車（要 union）、計數器（要 sum）、status 機器（要狀態圖）全用 LWW = <em>資料丟失</em>。LWW 的設計假設是「最新 write 就是正確答案」、但很多業務語義不是覆寫關係。</p>
<p>修：盤點 collection 的業務語義、選對應 resolution policy：</p>
<ul>
<li>覆寫關係 → LWW</li>
<li>累積關係 → custom merge stored proc（union / sum / set 合併）</li>
<li>狀態機 → custom merge stored proc（按狀態圖規則 resolve）</li>
<li>需要人工裁決 → conflict feed</li>
</ul>
<h3 id="failure-3custom-merge-stored-proc-沒測-edge-case">Failure 3：Custom merge stored proc 沒測 edge case</h3>
<p>proc throw exception 時 Cosmos DB 行為：conflict 留 feed、不會自動 retry。團隊以為 proc 跑了就沒事、實際 conflict 累積在 feed、後續分析失準。</p>
<p>修：proc 內部 try-catch、log exception、確保 <em>任何輸入都能 return 一個合理結果</em>（即使是 fallback 到 LWW）；定期掃 conflict feed 檢查積壓。</p>
<h3 id="failure-4不消費-conflict-feed">Failure 4：不消費 conflict feed</h3>
<p>選 manual mode 後忘記實作 feed consumer、conflict 累積、後續分析失準。常見徵兆：feed lag metric alert、或業務反映「資料對不上」、最後發現 conflict feed 裡躺著一堆未處理的 conflict。</p>
<p>修：選 conflict feed mode 前先實作 consumer pipeline（Azure Function trigger on Change Feed / 自建 worker）；設 alert：feed lag &gt; 5 min 通知。</p>
<h3 id="failure-5期待-multi-region-write-還有-strong-consistency">Failure 5：期待 multi-region write 還有 Strong consistency</h3>
<p>兩者互斥、開啟 multi-region write 後 Strong 自動 downgrade（或拒絕設定、時間敏感、查最新文件）。團隊以為「multi-region + Strong = 全球 linearizable」、底層是設計 incompatibility。</p>
<p>修：在 selection 階段就決定「要 active-active write 還是要 Strong」 — 兩者只能擇一。要全球 linearizable 轉 Spanner / Aurora DSQL、要 active-active 就接受 eventual / session / bounded staleness。</p>
<h3 id="failure-6跨-region-寫入後立即同-session-read-看不到">Failure 6：跨 region 寫入後立即同 session read 看不到</h3>
<p>session token 沒跨 region 傳遞、看似 inconsistency 其實是 session 沒對齊。典型 anti-pattern：service A 在 region 1 寫、用 region 1 session token；service B 在 region 2 讀、沒拿到 A 的 token、看不到 A 的寫。</p>
<p>修：session token 隨 request 傳遞（通常進 HTTP header）；或改 account 層 Bounded staleness（提供跨 session 的 K/T bound）；見 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 的 session token 管理段。</p>
<h3 id="failure-7region-故障時的-failover-邏輯誤判">Failure 7：Region 故障時的 failover 邏輯誤判</h3>
<p>multi-region write 已是 active-active、<em>不需要 manual failover</em> — 一個 region 掛、其他 region 自動承接寫入。但若用了 <code>failoverPriority</code> 配置、failover 邏輯仍要審 — priority 是 <em>當 multi-region read 切到哪個 region 為 primary</em>、不是 active-active 的 routing。</p>
<p>修：multi-region write 場景不用依賴 failoverPriority、用 Traffic Manager / Front Door 做 region routing；application 端 SDK 配置 <code>PreferredLocations</code> 讓 SDK 自己選 nearest region。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>ConflictCount</code>、<code>ReplicationLatency</code> per region pair、conflict feed lag</li>
<li>Conflict rate 監控：正常 &lt; 0.01%、突增代表 hot key 或 region 同步異常</li>
<li>Cost 影響：multi-region write 開啟後、寫入成本 × region 數（每個 region 都 replicate）— 3 region active-active = 3x write <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a> cost</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：multi-region write multiplier 進 sizing</li>
<li>對應 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：conflict rate 當 reliability evidence</li>
<li>Alert：conflict rate &gt; 0.1%、conflict feed lag &gt; 5 min、cross-region replication lag &gt; SLA</li>
</ul>
<h3 id="廣告-sla-vs-實測可用性鏈路拆解本章合成-frame">廣告 SLA vs 實測可用性鏈路拆解（本章合成 frame）</h3>
<p>9.C11 Minecraft Earth 平台揭露的 Cosmos DB SLA：</p>
<ul>
<li>single-region 99.99%</li>
<li>multi-region 99.999%</li>
</ul>
<p>這是 <em>DB 端 SLA</em>、不是 <em>端到端系統 SLA</em>。真實 production 系統的可用性是鏈路乘積：</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">實測可用性 = DB SLA × 網路 SLA × 應用層 SLA × 客戶端可達性</span></span></code></pre></div><p><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露「99.99% target vs 99% 實測」段的觀察：兩個 9 的差距 <em>不是</em> MongoDB / Atlas 自身問題、是 end-to-end 鏈路（車輛無線網路 / cellular tower / cloud network / event bus / microservice / DB cluster 任一環節掉都會打掉可用性）。Cosmos DB multi-region write 同模型：</p>
<ul>
<li>多 region active-active 可解 <em>DB 端可用性</em>、但網路 / 應用層任一掉、實測仍 &lt; 99.99%</li>
<li>廣告 99.999% 是 multi-region availability zone 級、<em>不是</em> 「使用者 request 成功率」</li>
</ul>
<p>引用時必須明示：Cosmos DB multi-region 廣告 99.999% 是 DB 端、要算實測可用性必須補網路 / 應用層 SLA 乘積、Toyota case 的「99% 實測」揭露的就是這個鏈路問題、跨 vendor 都適用。</p>
<p>跟 conflict resolution 的關係：多 region 高可用性 <em>買來</em> 的代價是 conflict、conflict rate 是 reliability 的暗稅 — 廣告 SLA 不計 conflict 處理成本。production 設計要把「conflict resolution 的工程成本」加進 multi-region write 的 ROI 評估。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（multi-region write 跟 Strong 互斥的 cross-link 來源）、<a href="../partition-key-design/">partition-key-design</a>（hot partition 會放大 conflict）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（multi-region cost × region 數）</li>
<li>跟 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 對比：CP vs AP、無 conflict vs LWW / custom</li>
<li>跟 DynamoDB Global Tables 對比：兩者都 LWW、Cosmos DB 多 custom merge + conflict feed</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 把 multi-region write 模式並陳</li>
<li>Knowledge cards：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a></li>
<li>Anti-recommendation：single-region write + cross-region read replica 在大多數情況更便宜、更易推理；只有 <em>write residency</em> 是產品契約（合規 / latency / 業務需求）時才升 multi-region write</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 multi-region write + conflict resolution backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — multi-region 99.999% / single-region 99.99% SLA 來源</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — 全球零售 multi-region 補充</li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected case</a> — 鏈路 SLA 拆解 frame anchor（跨 vendor 適用）</li>
<li><a href="../consistency-levels-engineering/">consistency-levels-engineering</a> — Strong + multi-region 互斥的 cross-link 目的地</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read 卡片</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO 卡片</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/conflict-resolution-policies">Cosmos DB conflict resolution</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/how-to-multi-master">Multi-region writes</a></li>
</ul>
]]></content:encoded></item><item><title>3.C50 Capital One：Visibility timeout 設計與 Lambda event source</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/</guid><description>&lt;p>Capital One 的 SQS + Lambda 實務揭露了 visibility timeout 的雙邊風險 — 太短導致重複處理、太長延遲 retry — 以及 Lambda event source mapping 的 scaling 行為跟直覺不同的地方。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Capital One 是美國大型金融機構，tech blog 公開分享了 SQS + Lambda 的 event-driven 架構實踐。金融場景的 message 處理對正確性要求極高 — 重複處理一筆交易跟遺失一筆交易的代價都是具體的金錢損失。&lt;/p>
&lt;p>SQS 是 AWS 原生的 managed queue，Lambda 是 serverless compute。兩者搭配的 event source mapping 是 AWS 上最常見的 event-driven 入門架構 — 看起來簡單（SQS → Lambda 自動觸發），但 visibility timeout 跟 Lambda scaling 的互動有不少實務細節。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="visibility-timeout-的雙邊風險">Visibility timeout 的雙邊風險&lt;/h3>
&lt;p>SQS 的 visibility timeout 定義了「consumer 取走訊息後，其他 consumer 多久之後才能再看到這筆訊息」。它是 SQS 的核心容錯機制 — consumer 處理失敗（crash、timeout）時，visibility timeout 到期後訊息重新出現在 queue 裡，讓其他 consumer 接手。&lt;/p>
&lt;p>&lt;strong>Timeout 太短&lt;/strong>：consumer 還在處理中、visibility timeout 已到期、另一個 consumer 取走同一筆訊息開始處理 — 重複處理。金融場景的重複處理可能導致重複扣款或重複退款。&lt;/p>
&lt;p>&lt;strong>Timeout 太長&lt;/strong>：consumer 處理失敗、需要等很久 visibility timeout 才到期、訊息才重新出現 — retry 延遲。原本幾秒就能被其他 consumer 接手的訊息，要等 15 分鐘才 retry。&lt;/p>
&lt;p>Capital One 的實務建議是 visibility timeout 設為「最大預期處理時間 + 少量緩衝」。例如：最大處理時間 30 秒 → visibility timeout 設 45 秒。&lt;/p>
&lt;h3 id="lambda-event-source-mapping-的-scaling-行為">Lambda event source mapping 的 scaling 行為&lt;/h3>
&lt;p>Lambda 跟 SQS 的整合透過 event source mapping — Lambda 服務自動從 SQS long polling 取訊息、觸發 Lambda function。使用者不需要自己寫 polling 邏輯。&lt;/p>
&lt;p>Capital One 揭露的 scaling 行為跟「Lambda 自動擴展」的直覺不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>初始狀態&lt;/strong>：Lambda 啟動 5 個 long polling connection（poller）&lt;/li>
&lt;li>&lt;strong>Scale up&lt;/strong>：每分鐘最多新增 60 個 poller instance（每個 instance 處理一批 message）&lt;/li>
&lt;li>&lt;strong>上限&lt;/strong>：最多 1000 個並行 batch&lt;/li>
&lt;/ul>
&lt;p>這意味著突發流量（queue 瞬間湧入大量訊息）的消化速度不是即時的 — Lambda 需要數分鐘才能 scale 到足夠的並行度。在這段 ramp-up 期間，queue depth 會持續增長。&lt;/p>
&lt;h3 id="batch-size-跟-visibility-timeout-的互動">Batch size 跟 visibility timeout 的互動&lt;/h3>
&lt;p>Lambda event source mapping 預設 batch size = 10 — 一次取 10 筆訊息、用一個 Lambda invocation 處理。如果 batch 中的某一筆處理特別慢，整個 batch 的處理時間會被拉長。&lt;/p></description><content:encoded><![CDATA[<p>Capital One 的 SQS + Lambda 實務揭露了 visibility timeout 的雙邊風險 — 太短導致重複處理、太長延遲 retry — 以及 Lambda event source mapping 的 scaling 行為跟直覺不同的地方。</p>
<h2 id="業務背景">業務背景</h2>
<p>Capital One 是美國大型金融機構，tech blog 公開分享了 SQS + Lambda 的 event-driven 架構實踐。金融場景的 message 處理對正確性要求極高 — 重複處理一筆交易跟遺失一筆交易的代價都是具體的金錢損失。</p>
<p>SQS 是 AWS 原生的 managed queue，Lambda 是 serverless compute。兩者搭配的 event source mapping 是 AWS 上最常見的 event-driven 入門架構 — 看起來簡單（SQS → Lambda 自動觸發），但 visibility timeout 跟 Lambda scaling 的互動有不少實務細節。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="visibility-timeout-的雙邊風險">Visibility timeout 的雙邊風險</h3>
<p>SQS 的 visibility timeout 定義了「consumer 取走訊息後，其他 consumer 多久之後才能再看到這筆訊息」。它是 SQS 的核心容錯機制 — consumer 處理失敗（crash、timeout）時，visibility timeout 到期後訊息重新出現在 queue 裡，讓其他 consumer 接手。</p>
<p><strong>Timeout 太短</strong>：consumer 還在處理中、visibility timeout 已到期、另一個 consumer 取走同一筆訊息開始處理 — 重複處理。金融場景的重複處理可能導致重複扣款或重複退款。</p>
<p><strong>Timeout 太長</strong>：consumer 處理失敗、需要等很久 visibility timeout 才到期、訊息才重新出現 — retry 延遲。原本幾秒就能被其他 consumer 接手的訊息，要等 15 分鐘才 retry。</p>
<p>Capital One 的實務建議是 visibility timeout 設為「最大預期處理時間 + 少量緩衝」。例如：最大處理時間 30 秒 → visibility timeout 設 45 秒。</p>
<h3 id="lambda-event-source-mapping-的-scaling-行為">Lambda event source mapping 的 scaling 行為</h3>
<p>Lambda 跟 SQS 的整合透過 event source mapping — Lambda 服務自動從 SQS long polling 取訊息、觸發 Lambda function。使用者不需要自己寫 polling 邏輯。</p>
<p>Capital One 揭露的 scaling 行為跟「Lambda 自動擴展」的直覺不同：</p>
<ul>
<li><strong>初始狀態</strong>：Lambda 啟動 5 個 long polling connection（poller）</li>
<li><strong>Scale up</strong>：每分鐘最多新增 60 個 poller instance（每個 instance 處理一批 message）</li>
<li><strong>上限</strong>：最多 1000 個並行 batch</li>
</ul>
<p>這意味著突發流量（queue 瞬間湧入大量訊息）的消化速度不是即時的 — Lambda 需要數分鐘才能 scale 到足夠的並行度。在這段 ramp-up 期間，queue depth 會持續增長。</p>
<h3 id="batch-size-跟-visibility-timeout-的互動">Batch size 跟 visibility timeout 的互動</h3>
<p>Lambda event source mapping 預設 batch size = 10 — 一次取 10 筆訊息、用一個 Lambda invocation 處理。如果 batch 中的某一筆處理特別慢，整個 batch 的處理時間會被拉長。</p>
<p>Visibility timeout 要覆蓋整個 batch 的處理時間（包含最慢的那一筆），否則 batch 還在處理中、早期取走的訊息 visibility timeout 到期、被其他 poller 重新取走 — 導致重複處理。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<table>
  <thead>
      <tr>
          <th>設計參數</th>
          <th>建議值</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visibility timeout</td>
          <td>最大處理時間 + 緩衝（例 45 秒）</td>
          <td>太短重複、太長延遲 retry</td>
      </tr>
      <tr>
          <td>Batch size</td>
          <td>依處理時間變異度調整</td>
          <td>Batch 大省 invocation 費用、但延長 visibility 需求</td>
      </tr>
      <tr>
          <td>DLQ</td>
          <td>設定 maxReceiveCount（例 3 次）</td>
          <td>避免 poison message 無限 retry</td>
      </tr>
      <tr>
          <td>Concurrency limit</td>
          <td>依下游承受能力設定</td>
          <td>避免 Lambda 爆量壓垮下游 DB</td>
      </tr>
  </tbody>
</table>
<h3 id="idempotency-作為安全網">Idempotency 作為安全網</h3>
<p>Visibility timeout 無法完全避免重複處理（網路分區、Lambda timeout 等邊界條件）。Capital One 的做法是在 Lambda function 內實作 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> — 用 message ID 做去重，確保同一筆訊息被多次處理時結果相同。</p>
<p>Idempotency 把 visibility timeout 的精確度要求降低 — 即使偶爾重複處理，業務結果仍然正確。Visibility timeout 仍然需要合理設定（降低不必要的重複 invocation 成本），但 idempotency 是「即使設錯也不會造成業務錯誤」的安全網。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a>：visibility timeout、in-flight limit、Lambda event source 的進階主題</li>
<li><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a>：at-least-once 語意下的 consumer 端 idempotency</li>
<li><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>：visibility timeout 是 SQS 的 delivery guarantee 機制</li>
<li><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。">3.8 queue consumer retry replay handoff</a>：DLQ + maxReceiveCount 的 retry 升級策略</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>SQS + Lambda 架構中出現訊息重複處理（CloudWatch 的 <code>ApproximateNumberOfMessagesNotVisible</code> 跟 <code>NumberOfMessagesReceived</code> 比例異常）</li>
<li>Lambda function 的 timeout 跟 SQS visibility timeout 的關係沒有明確設計</li>
<li>突發流量時 queue depth 持續增長、Lambda 的 concurrent execution 沒有立刻跟上</li>
<li>Batch processing 中的慢訊息拖慢整個 batch、造成 visibility timeout 到期</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.capitalone.com/tech/cloud/using-aws-solutions-for-event-driven-serverless-architectures/">Using AWS Solutions for Event-Driven Serverless Architectures</a></li>
</ul>
]]></content:encoded></item><item><title>3.C51 Atlassian JiRT：Kinesis + SQS subscription</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/</guid><description>&lt;p>這個案例的核心責任是說明 SQS 作為 streaming source 的 per-consumer subscription 模式。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Atlassian 內部 event bus StreamHub 底層用 Kinesis、但「每個 consumer 自己準備 SQS queue 接收 event」。JiRT 即時服務透過此模式把輪詢式（~1 min）改成 event-driven（秒級）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>在 Kinesis 上面疊 SQS 讓 consumer 各自設定 retention、各自獨立 visibility timeout。揭露「stream + per-consumer queue」是 fan-out 場景的常見複合 pattern、不是 streaming vs queue 二選一。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / SQS 作為 fan-out subscriber。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a>（streaming + queue 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/using-an-event-driven-architecture-to-improve-jira-software-responsiveness">Using an Event-Driven Architecture to Improve Jira Software Responsiveness&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS 作為 streaming source 的 per-consumer subscription 模式。</p>
<h2 id="觀察">觀察</h2>
<p>Atlassian 內部 event bus StreamHub 底層用 Kinesis、但「每個 consumer 自己準備 SQS queue 接收 event」。JiRT 即時服務透過此模式把輪詢式（~1 min）改成 event-driven（秒級）。</p>
<h2 id="判讀">判讀</h2>
<p>在 Kinesis 上面疊 SQS 讓 consumer 各自設定 retention、各自獨立 visibility timeout。揭露「stream + per-consumer queue」是 fan-out 場景的常見複合 pattern、不是 streaming vs queue 二選一。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / SQS 作為 fan-out subscriber。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>（streaming + queue 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/using-an-event-driven-architecture-to-improve-jira-software-responsiveness">Using an Event-Driven Architecture to Improve Jira Software Responsiveness</a></li>
</ul>
]]></content:encoded></item><item><title>Heroku：Routing 控制事件與多租戶影響</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/</guid><description>&lt;p>這起案例的核心責任是守住路由層故障的擴散邊界。PaaS 共享入口若失效，租戶影響會快速放大。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>router error spike&lt;/td>
 &lt;td>入口故障是否擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>tenant-level impact variance&lt;/td>
 &lt;td>影響是否呈現分區差異&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>status lag&lt;/td>
 &lt;td>對外更新是否落後&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「路由層共享入口」對多租戶的擴散影響。主要風險是未先切租戶影響就全量回復，導致二次壅塞。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>事故流程需先切分租戶影響，再做回復批次，並回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是守住路由層故障的擴散邊界。PaaS 共享入口若失效，租戶影響會快速放大。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>router error spike</td>
          <td>入口故障是否擴散</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
      <tr>
          <td>tenant-level impact variance</td>
          <td>影響是否呈現分區差異</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
      <tr>
          <td>status lag</td>
          <td>對外更新是否落後</td>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「路由層共享入口」對多租戶的擴散影響。主要風險是未先切租戶影響就全量回復，導致二次壅塞。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>事故流程需先切分租戶影響，再做回復批次，並回寫 <a href="/blog/backend/08-incident-response/incident-communication/" data-link-title="8.4 事故通訊與狀態更新" data-link-desc="建立內外部通報節奏與狀態更新格式">8.4</a> 與 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a>。</p>
]]></content:encoded></item><item><title>Microsoft：變更治理與可靠性門檻</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/</guid><description>&lt;p>Microsoft 案例的核心責任是把變更管理制度化。對大型 SaaS 而言，事故常由多個低風險變更疊加而成，治理重點在於發布節奏與風險分層。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>高頻變更環境中，單一變更看起來都可接受，但累積後會突破可靠性預算。若缺少一致 gate，團隊難以提早收斂。&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;/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>何時擴大、何時停止&lt;/td>
 &lt;td>放行節奏&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;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>release rollback frequency&lt;/td>
 &lt;td>變更品質是否退化&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>freeze trigger count&lt;/td>
 &lt;td>凍結是否過晚&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>incident recurrence&lt;/td>
 &lt;td>同型事件是否重複&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">8.13&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>把風險分層寫進 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19&lt;/a>，並將復盤項目回寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Microsoft 案例的核心責任是把變更管理制度化。對大型 SaaS 而言，事故常由多個低風險變更疊加而成，治理重點在於發布節奏與風險分層。</p>
<h2 id="問題場景">問題場景</h2>
<p>高頻變更環境中，單一變更看起來都可接受，但累積後會突破可靠性預算。若缺少一致 gate，團隊難以提早收斂。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>變更分層</td>
          <td>哪些變更需要高門檻</td>
          <td>風險分級</td>
      </tr>
      <tr>
          <td>漸進發布</td>
          <td>何時擴大、何時停止</td>
          <td>放行節奏</td>
      </tr>
      <tr>
          <td>復盤回寫</td>
          <td>事故教訓如何制度化</td>
          <td>持續改善</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>release rollback frequency</td>
          <td>變更品質是否退化</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>freeze trigger count</td>
          <td>凍結是否過晚</td>
          <td><a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">6.6</a></td>
      </tr>
      <tr>
          <td>incident recurrence</td>
          <td>同型事件是否重複</td>
          <td><a href="/blog/backend/08-incident-response/repeated-incident-toil/" data-link-title="8.13 Repeated Incident 與 Toil 治理" data-link-desc="把同型事故反覆發生與重複手動修復作為工程化治理對象">8.13</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>把風險分層寫進 <a href="/blog/backend/06-reliability/reliability-readiness-review/" data-link-title="6.19 Reliability Readiness Review" data-link-desc="把上線前、重大變更前與高風險操作前的可靠性準備度變成可檢查門檻">6.19</a>，並將復盤項目回寫 <a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a>。</p>
]]></content:encoded></item><item><title>Shopify：BFCM 容量治理與 Game Day 驗證節奏</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/</guid><description>&lt;p>Shopify 案例的核心責任是把可預期峰值轉成可預演流程。當流量高峰是年度固定事件，可靠性工作重點是提前把容量與失效路徑變成可驗證資產，臨場救火代表準備不足。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>BFCM 類型高峰會同時放大三種壓力：流量突增、資料層寫入壓力、跨服務依賴抖動。若只在活動前做單次壓測，團隊通常只能看到系統上限，無法看到恢復節奏與指揮負載。&lt;/p>
&lt;p>Shopify 的做法是把容量規劃、隔離邊界與演練節奏綁成同一條年度路線。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Capacity planning baseline&lt;/td>
 &lt;td>高峰前可承受上限是多少&lt;/td>
 &lt;td>容量預算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pod/isolation boundary&lt;/td>
 &lt;td>故障影響如何限制在局部&lt;/td>
 &lt;td>擴散邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Game Day&lt;/td>
 &lt;td>高峰前如何驗證假設&lt;/td>
 &lt;td>演練證據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resiliency matrix&lt;/td>
 &lt;td>服務與失效模式如何對齊&lt;/td>
 &lt;td>控制面清單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個機制的價值是讓高峰風險在活動前被分批消化，而不是在活動中一次承擔。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>peak-load headroom&lt;/td>
 &lt;td>高峰前安全緩衝是否充足&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>game-day action closure&lt;/td>
 &lt;td>演練缺口是否完成回寫&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>pod-level degradation&lt;/td>
 &lt;td>退化是否被限制在局部&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>command handoff latency&lt;/td>
 &lt;td>高峰日交接節奏是否穩定&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&amp;#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>把高峰準備當成一次性專案會讓知識斷層快速累積。可靠做法是把每輪活動輸出的缺口回寫成固定資產：runbook、matrix、驗證腳本與放行門檻。這讓下一輪準備從更高基準開始，而不是重來。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>若要落地本案例，先從 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a> 建容量模型，再在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a> 定義高峰穩態。演練證據回寫 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Shopify 案例的核心責任是把可預期峰值轉成可預演流程。當流量高峰是年度固定事件，可靠性工作重點是提前把容量與失效路徑變成可驗證資產，臨場救火代表準備不足。</p>
<h2 id="問題場景">問題場景</h2>
<p>BFCM 類型高峰會同時放大三種壓力：流量突增、資料層寫入壓力、跨服務依賴抖動。若只在活動前做單次壓測，團隊通常只能看到系統上限，無法看到恢復節奏與指揮負載。</p>
<p>Shopify 的做法是把容量規劃、隔離邊界與演練節奏綁成同一條年度路線。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Capacity planning baseline</td>
          <td>高峰前可承受上限是多少</td>
          <td>容量預算</td>
      </tr>
      <tr>
          <td>Pod/isolation boundary</td>
          <td>故障影響如何限制在局部</td>
          <td>擴散邊界</td>
      </tr>
      <tr>
          <td>Game Day</td>
          <td>高峰前如何驗證假設</td>
          <td>演練證據</td>
      </tr>
      <tr>
          <td>Resiliency matrix</td>
          <td>服務與失效模式如何對齊</td>
          <td>控制面清單</td>
      </tr>
  </tbody>
</table>
<p>這個機制的價值是讓高峰風險在活動前被分批消化，而不是在活動中一次承擔。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>peak-load headroom</td>
          <td>高峰前安全緩衝是否充足</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>game-day action closure</td>
          <td>演練缺口是否完成回寫</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a></td>
      </tr>
      <tr>
          <td>pod-level degradation</td>
          <td>退化是否被限制在局部</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>command handoff latency</td>
          <td>高峰日交接節奏是否穩定</td>
          <td><a href="/blog/backend/08-incident-response/ic-handoff-long-incident/" data-link-title="8.12 IC Handoff 與長事故跨班次協調" data-link-desc="把 24h&#43; / 跨 timezone 事故的接班節奏變成可重複流程">8.12</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>把高峰準備當成一次性專案會讓知識斷層快速累積。可靠做法是把每輪活動輸出的缺口回寫成固定資產：runbook、matrix、驗證腳本與放行門檻。這讓下一輪準備從更高基準開始，而不是重來。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>若要落地本案例，先從 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a> 建容量模型，再在 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a> 定義高峰穩態。演練證據回寫 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a> 與 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">8.6</a>。</p>
]]></content:encoded></item><item><title>Microsoft：Safe Deployment Practices 與 Resilience Patterns</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/</guid><description>&lt;p>Safe deployment practices 的核心責任是讓大規模服務的每次變更都經過漸進驗證。ring-based deployment 把影響面從小到大排列，每一層是下一層的安全網。resilience patterns 的責任是讓服務在依賴失效時有標準化的降級行為，降低臨場判斷的成本。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>Azure 與 M365 等大型 SaaS 每天部署數千次變更，單靠人工審核不可擴展。當部署速度超過人工審查能力，需要一套自動化的漸進驗證流程來控制每次變更的風險。同時，服務間的依賴關係複雜，任何一個依賴的劣化都可能影響多個下游服務，需要標準化的降級行為避免連鎖失效。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ring-based deployment&lt;/td>
 &lt;td>變更如何從小範圍漸進到全量&lt;/td>
 &lt;td>分層放行節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Automatic rollback&lt;/td>
 &lt;td>health signal 異常時如何自動退回&lt;/td>
 &lt;td>自動化回退條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resilience patterns&lt;/td>
 &lt;td>依賴失效時服務如何標準化降級&lt;/td>
 &lt;td>retry / breaker / bulkhead&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Blast radius control&lt;/td>
 &lt;td>ring boundary 如何限制影響範圍&lt;/td>
 &lt;td>每層的最大影響面&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Ring-based deployment 的標準路徑是 Ring 0（internal dogfood）→ Ring 1（canary）→ Ring 2（early adopters）→ Ring 3（broad）。每一層的 go/no-go 條件包含 health signal delta（跟前一版 baseline 比較）、error rate、latency percentile 與 customer impact signal。只有當前層的所有指標都在可接受範圍內，才進入下一層。&lt;/p>
&lt;p>Automatic rollback 是 ring progression 的安全網。當 health signal 超過預設門檻時，系統自動回退到前一版，不需要等人工判斷。自動回退的觸發條件要嚴格定義 — 過於敏感會造成頻繁 false positive rollback，過於寬鬆會讓問題擴散到下一個 ring。&lt;/p>
&lt;p>Resilience patterns 讓依賴失效時的行為可預測。retry with &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter&lt;/a> 避免重試風暴、circuit breaker 在依賴持續失效時停止發送請求、bulkhead isolation 把不同依賴的資源池隔開。這些 patterns 的價值在於標準化 — 團隊不需要每次都從頭設計降級邏輯，而是從已驗證的 pattern 庫中選擇。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>ring health delta&lt;/td>
 &lt;td>每層的品質是否維持&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>automatic rollback frequency&lt;/td>
 &lt;td>自動回退是否過於頻繁或過少&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>circuit breaker trip rate&lt;/td>
 &lt;td>依賴失效是否被及時隔離&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>deployment velocity&lt;/td>
 &lt;td>漸進部署是否拖慢交付速度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>Ring progression 的觀察窗長度需要跟服務的 feedback loop 對齊。通用服務可能幾分鐘內就能看到異常，但有延遲確認的服務（結算、對帳、非同步補償）可能需要數小時甚至數天才暴露問題。觀察窗太短會漏掉延遲暴露的問題；太長會拖慢所有變更的交付速度。分服務類型設定不同觀察窗，比用統一時長更有效。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先把 ring-based deployment 的 go/no-go 條件寫進 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>，再把 resilience patterns 的 circuit breaker 與 retry 設計接到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 Dependency Reliability Budget&lt;/a>。deployment velocity 的量測回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 Reliability Metrics&lt;/a>，CI 整合回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI Pipeline&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Safe deployment practices 的核心責任是讓大規模服務的每次變更都經過漸進驗證。ring-based deployment 把影響面從小到大排列，每一層是下一層的安全網。resilience patterns 的責任是讓服務在依賴失效時有標準化的降級行為，降低臨場判斷的成本。</p>
<h2 id="問題場景">問題場景</h2>
<p>Azure 與 M365 等大型 SaaS 每天部署數千次變更，單靠人工審核不可擴展。當部署速度超過人工審查能力，需要一套自動化的漸進驗證流程來控制每次變更的風險。同時，服務間的依賴關係複雜，任何一個依賴的劣化都可能影響多個下游服務，需要標準化的降級行為避免連鎖失效。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ring-based deployment</td>
          <td>變更如何從小範圍漸進到全量</td>
          <td>分層放行節奏</td>
      </tr>
      <tr>
          <td>Automatic rollback</td>
          <td>health signal 異常時如何自動退回</td>
          <td>自動化回退條件</td>
      </tr>
      <tr>
          <td>Resilience patterns</td>
          <td>依賴失效時服務如何標準化降級</td>
          <td>retry / breaker / bulkhead</td>
      </tr>
      <tr>
          <td>Blast radius control</td>
          <td>ring boundary 如何限制影響範圍</td>
          <td>每層的最大影響面</td>
      </tr>
  </tbody>
</table>
<p>Ring-based deployment 的標準路徑是 Ring 0（internal dogfood）→ Ring 1（canary）→ Ring 2（early adopters）→ Ring 3（broad）。每一層的 go/no-go 條件包含 health signal delta（跟前一版 baseline 比較）、error rate、latency percentile 與 customer impact signal。只有當前層的所有指標都在可接受範圍內，才進入下一層。</p>
<p>Automatic rollback 是 ring progression 的安全網。當 health signal 超過預設門檻時，系統自動回退到前一版，不需要等人工判斷。自動回退的觸發條件要嚴格定義 — 過於敏感會造成頻繁 false positive rollback，過於寬鬆會讓問題擴散到下一個 ring。</p>
<p>Resilience patterns 讓依賴失效時的行為可預測。retry with <a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a> 避免重試風暴、circuit breaker 在依賴持續失效時停止發送請求、bulkhead isolation 把不同依賴的資源池隔開。這些 patterns 的價值在於標準化 — 團隊不需要每次都從頭設計降級邏輯，而是從已驗證的 pattern 庫中選擇。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ring health delta</td>
          <td>每層的品質是否維持</td>
          <td><a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>automatic rollback frequency</td>
          <td>自動回退是否過於頻繁或過少</td>
          <td><a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18</a></td>
      </tr>
      <tr>
          <td>circuit breaker trip rate</td>
          <td>依賴失效是否被及時隔離</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>deployment velocity</td>
          <td>漸進部署是否拖慢交付速度</td>
          <td><a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>Ring progression 的觀察窗長度需要跟服務的 feedback loop 對齊。通用服務可能幾分鐘內就能看到異常，但有延遲確認的服務（結算、對帳、非同步補償）可能需要數小時甚至數天才暴露問題。觀察窗太短會漏掉延遲暴露的問題；太長會拖慢所有變更的交付速度。分服務類型設定不同觀察窗，比用統一時長更有效。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先把 ring-based deployment 的 go/no-go 條件寫進 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>，再把 resilience patterns 的 circuit breaker 與 retry 設計接到 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 Dependency Reliability Budget</a>。deployment velocity 的量測回到 <a href="/blog/backend/06-reliability/reliability-metrics-governance/" data-link-title="6.18 Reliability Metrics Governance" data-link-desc="DORA / SPACE 指標的選用、量測陷阱、anti-gaming 與團隊階段適配">6.18 Reliability Metrics</a>，CI 整合回到 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI Pipeline</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/devops/operate/safe-deployment-practices">Safe deployment practices</a></li>
<li><a href="https://learn.microsoft.com/en-gb/azure/well-architected/reliability/design-patterns">Architecture design patterns that support reliability</a></li>
</ul>
]]></content:encoded></item><item><title>Shopify：Pod Architecture 與 Resiliency Matrix</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/</guid><description>&lt;p>Shopify pod architecture 的核心責任是把多租戶流量限制在獨立的 pod 內，讓一個 pod 的故障不影響其他 pod 的商店。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resiliency-matrix/" data-link-title="Resiliency Matrix" data-link-desc="服務與失敗模式的交叉矩陣，標記每個交叉點的防護狀態與驗證覆蓋">resiliency matrix&lt;/a> 的核心責任是把每個服務的失敗模式與防護狀態列成可檢查的矩陣，讓 game day 有結構化的驗證清單。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>多租戶電商平台的流量分佈高度不均。大商店的促銷活動可能在短時間內吃掉共享資源的大部分容量，若缺少隔離機制，一個商店的流量爆增會拖垮同一基礎設施上的其他商店。&lt;/p>
&lt;p>隔離解決的是擴散問題，但隔離本身不回答「哪些失敗模式已經有防護、哪些還是缺口」。resiliency matrix 把這個問題結構化：每個服務列出已知的失敗模式，每種模式標註防護狀態，缺口直接成為下一輪演練的輸入。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Pod boundary&lt;/td>
 &lt;td>一個商店的故障最多影響到哪裡&lt;/td>
 &lt;td>獨立隔離單位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tenant routing&lt;/td>
 &lt;td>商店按什麼規則分配到 pod&lt;/td>
 &lt;td>映射策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resiliency matrix&lt;/td>
 &lt;td>每個服務的失敗模式是否都有對應防護&lt;/td>
 &lt;td>防護覆蓋狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Game Day 整合&lt;/td>
 &lt;td>matrix 的缺口如何轉成演練題目&lt;/td>
 &lt;td>演練驗證清單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Pod boundary 的設計是每個 pod 擁有獨立的 DB、cache 與 compute 資源。這讓 pod 之間在資源層完全隔離 — 一個 pod 的 DB 連線耗盡不會影響其他 pod 的查詢能力。隔離的代價是資源利用率降低，但在峰值場景下，隔離帶來的故障局部化價值遠高於利用率損失。&lt;/p>
&lt;p>Tenant routing 決定商店到 pod 的映射。映射規則通常考慮商店規模（大商店獨立 pod 或少量共用）、地理區域、與風險等級（新商店 vs 穩定商店）。映射一旦建立，變更需要 migration — 這是隔離架構的操作成本之一。&lt;/p>
&lt;p>Resiliency matrix 是 service × failure mode 的二維矩陣。每格填入三種狀態之一：covered（有防護且已驗證）、gap（已知缺口、尚未補齊）、in-progress（正在建設）。matrix 的維護責任跟服務 owner 綁定，每輪 game day 前 review 一次。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>pod-level error isolation&lt;/td>
 &lt;td>故障是否被限制在單一 pod 內&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>matrix gap count trend&lt;/td>
 &lt;td>缺口是否在收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cross-pod contamination&lt;/td>
 &lt;td>是否有故障穿越 pod 邊界&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>game-day action closure&lt;/td>
 &lt;td>演練暴露的缺口是否被關閉&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見陷阱">常見陷阱&lt;/h2>
&lt;p>resiliency matrix 最大的風險是退化為文件。若 matrix 只在年度 review 更新一次、gap 沒有 owner、action item 沒有 deadline，它就失去了驅動演練的功能。有效的 matrix 跟 game day 節奏綁定：每輪演練前 review gap、演練後更新狀態、新服務上線時補齊對應行列。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判&lt;/a>：resiliency matrix 是 FMEA 的落地工具&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget&lt;/a>：pod 隔離是依賴預算的實作手段&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety&lt;/a>：跨 pod 實驗的 blast radius 控制&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt&lt;/a>：matrix gap 回寫成 reliability debt&lt;/li>
&lt;/ul>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/four-steps-creating-effective-game-day-tests">Four Steps to Creating Effective Game Day Tests&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://shopify.engineering/resiliency-planning-for-high-traffic-events">Resiliency Planning for High-Traffic Events&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://shopify.engineering/a-pods-architecture-to-allow-shopify-to-scale">A Pods Architecture To Allow Shopify To Scale&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Shopify pod architecture 的核心責任是把多租戶流量限制在獨立的 pod 內，讓一個 pod 的故障不影響其他 pod 的商店。<a href="/blog/backend/knowledge-cards/resiliency-matrix/" data-link-title="Resiliency Matrix" data-link-desc="服務與失敗模式的交叉矩陣，標記每個交叉點的防護狀態與驗證覆蓋">resiliency matrix</a> 的核心責任是把每個服務的失敗模式與防護狀態列成可檢查的矩陣，讓 game day 有結構化的驗證清單。</p>
<h2 id="問題場景">問題場景</h2>
<p>多租戶電商平台的流量分佈高度不均。大商店的促銷活動可能在短時間內吃掉共享資源的大部分容量，若缺少隔離機制，一個商店的流量爆增會拖垮同一基礎設施上的其他商店。</p>
<p>隔離解決的是擴散問題，但隔離本身不回答「哪些失敗模式已經有防護、哪些還是缺口」。resiliency matrix 把這個問題結構化：每個服務列出已知的失敗模式，每種模式標註防護狀態，缺口直接成為下一輪演練的輸入。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pod boundary</td>
          <td>一個商店的故障最多影響到哪裡</td>
          <td>獨立隔離單位</td>
      </tr>
      <tr>
          <td>Tenant routing</td>
          <td>商店按什麼規則分配到 pod</td>
          <td>映射策略</td>
      </tr>
      <tr>
          <td>Resiliency matrix</td>
          <td>每個服務的失敗模式是否都有對應防護</td>
          <td>防護覆蓋狀態</td>
      </tr>
      <tr>
          <td>Game Day 整合</td>
          <td>matrix 的缺口如何轉成演練題目</td>
          <td>演練驗證清單</td>
      </tr>
  </tbody>
</table>
<p>Pod boundary 的設計是每個 pod 擁有獨立的 DB、cache 與 compute 資源。這讓 pod 之間在資源層完全隔離 — 一個 pod 的 DB 連線耗盡不會影響其他 pod 的查詢能力。隔離的代價是資源利用率降低，但在峰值場景下，隔離帶來的故障局部化價值遠高於利用率損失。</p>
<p>Tenant routing 決定商店到 pod 的映射。映射規則通常考慮商店規模（大商店獨立 pod 或少量共用）、地理區域、與風險等級（新商店 vs 穩定商店）。映射一旦建立，變更需要 migration — 這是隔離架構的操作成本之一。</p>
<p>Resiliency matrix 是 service × failure mode 的二維矩陣。每格填入三種狀態之一：covered（有防護且已驗證）、gap（已知缺口、尚未補齊）、in-progress（正在建設）。matrix 的維護責任跟服務 owner 綁定，每輪 game day 前 review 一次。</p>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>pod-level error isolation</td>
          <td>故障是否被限制在單一 pod 內</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>matrix gap count trend</td>
          <td>缺口是否在收斂</td>
          <td><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21</a></td>
      </tr>
      <tr>
          <td>cross-pod contamination</td>
          <td>是否有故障穿越 pod 邊界</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>game-day action closure</td>
          <td>演練暴露的缺口是否被關閉</td>
          <td><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見陷阱">常見陷阱</h2>
<p>resiliency matrix 最大的風險是退化為文件。若 matrix 只在年度 review 更新一次、gap 沒有 owner、action item 沒有 deadline，它就失去了驅動演練的功能。有效的 matrix 跟 game day 節奏綁定：每輪演練前 review gap、演練後更新狀態、新服務上線時補齊對應行列。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判</a>：resiliency matrix 是 FMEA 的落地工具</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a>：pod 隔離是依賴預算的實作手段</li>
<li><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 experiment safety</a>：跨 pod 實驗的 blast radius 控制</li>
<li><a href="/blog/backend/06-reliability/reliability-debt-backlog/" data-link-title="6.21 Reliability Debt Backlog" data-link-desc="把反覆事故、演練缺口與手動修復累積成可排序、可關閉的 reliability debt">6.21 reliability debt</a>：matrix gap 回寫成 reliability debt</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/four-steps-creating-effective-game-day-tests">Four Steps to Creating Effective Game Day Tests</a></li>
<li><a href="https://shopify.engineering/resiliency-planning-for-high-traffic-events">Resiliency Planning for High-Traffic Events</a></li>
<li><a href="https://shopify.engineering/a-pods-architecture-to-allow-shopify-to-scale">A Pods Architecture To Allow Shopify To Scale</a></li>
</ul>
]]></content:encoded></item><item><title>3.C52 Nielsen：Spark on EKS 雙 SQS 工作流</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/</guid><description>&lt;p>這個案例的核心責任是說明 SQS queue depth 作為 autoscale 訊號的真實案例。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Nielsen 每日處理 25 TB / 30 billion event。架構用兩個 SQS queue：work queue（待處理工作項）+ completion queue（回報完成）。Lambda 從 DB 拉檔案、組成 work item 推進 work queue、EKS pod 拉取處理、處理完寫 completion queue。基於 queue depth 自動擴 pod。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不用直接 Lambda invoke（pod 上跑長時間 Spark workload）、queue depth 當 backlog signal driving autoscale。揭露長 workload 場景該用 pod + queue depth、不是 Lambda function。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：CloudWatch metric + alarm / Standard queue / 長 workload autoscaling。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&amp;#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA&lt;/a>（lag-based autoscale 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/architecture/how-nielsen-uses-serverless-concepts-on-amazon-eks-for-big-data-processing-with-spark-workloads/">How Nielsen Uses Serverless Concepts on Amazon EKS for Big Data Spark Workloads&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS queue depth 作為 autoscale 訊號的真實案例。</p>
<h2 id="觀察">觀察</h2>
<p>Nielsen 每日處理 25 TB / 30 billion event。架構用兩個 SQS queue：work queue（待處理工作項）+ completion queue（回報完成）。Lambda 從 DB 拉檔案、組成 work item 推進 work queue、EKS pod 拉取處理、處理完寫 completion queue。基於 queue depth 自動擴 pod。</p>
<h2 id="判讀">判讀</h2>
<p>不用直接 Lambda invoke（pod 上跑長時間 Spark workload）、queue depth 當 backlog signal driving autoscale。揭露長 workload 場景該用 pod + queue depth、不是 Lambda function。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：CloudWatch metric + alarm / Standard queue / 長 workload autoscaling。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA</a>（lag-based autoscale 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/architecture/how-nielsen-uses-serverless-concepts-on-amazon-eks-for-big-data-processing-with-spark-workloads/">How Nielsen Uses Serverless Concepts on Amazon EKS for Big Data Spark Workloads</a></li>
</ul>
]]></content:encoded></item><item><title>3.C53 FINRA：S3 → SQS notification 大檔上傳</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/</guid><description>&lt;p>這個案例的核心責任是說明 S3 event notification 是 SQS 最經典 trigger、合規場景的 IAM 多層設定。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>FINRA 金融監管機構、處理 broker-dealer 上傳大檔。Large File Service 用 S3 → SQS 通知模式：使用者上傳完 loading dock bucket、S3 推 SQS message 給 LFS、移檔後再推 &amp;ldquo;file available&amp;rdquo; SQS message 給下游。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>S3 通知是 SQS 最經典 trigger、KMS + bucket policy + queue 權限的合規場景（金融業要保留稽核軌跡）。揭露金融場景的 IAM 設計不是一道權限、是多層稽核軌跡。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：SQS + Lambda event source / IAM + Cross-account。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security 模組&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.finra.org/about/how-we-operate/technology/blog/large-file-service-securely-uploading-large-files-to-s3">FINRA Large File Service&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 S3 event notification 是 SQS 最經典 trigger、合規場景的 IAM 多層設定。</p>
<h2 id="觀察">觀察</h2>
<p>FINRA 金融監管機構、處理 broker-dealer 上傳大檔。Large File Service 用 S3 → SQS 通知模式：使用者上傳完 loading dock bucket、S3 推 SQS message 給 LFS、移檔後再推 &ldquo;file available&rdquo; SQS message 給下游。</p>
<h2 id="判讀">判讀</h2>
<p>S3 通知是 SQS 最經典 trigger、KMS + bucket policy + queue 權限的合規場景（金融業要保留稽核軌跡）。揭露金融場景的 IAM 設計不是一道權限、是多層稽核軌跡。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：SQS + Lambda event source / IAM + Cross-account。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security 模組</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.finra.org/about/how-we-operate/technology/blog/large-file-service-securely-uploading-large-files-to-s3">FINRA Large File Service</a></li>
</ul>
]]></content:encoded></item><item><title>3.C54 Twitch EventSub：SNS+SQS fan-out 給第三方</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/</guid><description>&lt;p>這個案例的核心責任是說明 SNS-SQS fan-out + dispatcher pattern 的實戰。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Twitch 內部 Event Bus 發佈 ~1660 events/sec 到 SNS。EventSub（給第三方應用訂閱 Twitch 事件）用 SQS 接收 async notification、再由 Dispatcher fan-out 給各訂閱者。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>fan-out 後每個 consumer 要自己一個 queue。揭露 SNS → SQS 是 AWS 生態的 fan-out 標配、SQS 是第三方訂閱的 buffer 層、Dispatcher 是 application 級別的分發責任。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard queue + SQS + Lambda / SNS-SQS fan-out。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &amp;#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51 Atlassian JiRT&lt;/a>（subscription 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.twitch.tv/en/2023/09/28/twitch-state-of-engineering-2023/">Twitch State of Engineering 2023&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SNS-SQS fan-out + dispatcher pattern 的實戰。</p>
<h2 id="觀察">觀察</h2>
<p>Twitch 內部 Event Bus 發佈 ~1660 events/sec 到 SNS。EventSub（給第三方應用訂閱 Twitch 事件）用 SQS 接收 async notification、再由 Dispatcher fan-out 給各訂閱者。</p>
<h2 id="判讀">判讀</h2>
<p>fan-out 後每個 consumer 要自己一個 queue。揭露 SNS → SQS 是 AWS 生態的 fan-out 標配、SQS 是第三方訂閱的 buffer 層、Dispatcher 是 application 級別的分發責任。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard queue + SQS + Lambda / SNS-SQS fan-out。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51 Atlassian JiRT</a>（subscription 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.twitch.tv/en/2023/09/28/twitch-state-of-engineering-2023/">Twitch State of Engineering 2023</a></li>
</ul>
]]></content:encoded></item><item><title>3.C55 SmugMug：SQS 驅動可重放搜尋管線</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/</guid><description>&lt;p>這個案例的核心責任是說明 SQS 作為「workload generator」的分散式平行化角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>SmugMug 用 SQS 兩種模式：(1) backfill — script 推 DynamoDB scan-segment 指令進 SQS、Lambda 拉取做平行掃描寫 OpenSearch、(2) 鏡像查詢 — production query 推副本 SQS、Lambda replay 到 replica domain。每小時可 index &amp;gt; 1 billion document、不影響 production。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>SQS 作為「workload generator」分散式平行化、不需協調 worker 數量。揭露 SQS 不只是「事件 queue」、也是「並行任務分散」的協調基礎。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard queue / Long polling / SQS + Lambda event source。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/big-data/smugmugs-durable-search-pipelines-for-amazon-opensearch-service/">SmugMug&amp;rsquo;s Durable Search Pipelines for Amazon OpenSearch Service&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS 作為「workload generator」的分散式平行化角色。</p>
<h2 id="觀察">觀察</h2>
<p>SmugMug 用 SQS 兩種模式：(1) backfill — script 推 DynamoDB scan-segment 指令進 SQS、Lambda 拉取做平行掃描寫 OpenSearch、(2) 鏡像查詢 — production query 推副本 SQS、Lambda replay 到 replica domain。每小時可 index &gt; 1 billion document、不影響 production。</p>
<h2 id="判讀">判讀</h2>
<p>SQS 作為「workload generator」分散式平行化、不需協調 worker 數量。揭露 SQS 不只是「事件 queue」、也是「並行任務分散」的協調基礎。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard queue / Long polling / SQS + Lambda event source。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/big-data/smugmugs-durable-search-pipelines-for-amazon-opensearch-service/">SmugMug&rsquo;s Durable Search Pipelines for Amazon OpenSearch Service</a></li>
</ul>
]]></content:encoded></item><item><title>3.C56 PostNL EBE：完整 DLQ + retention + redrive 設計</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/</guid><description>&lt;p>這個案例的核心責任是業內真正完整的 DLQ + redrive + retention 設計案例、不是 demo 規模。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>PostNL（荷蘭最大物流商、每天 6.9M 信件 + 1.1M 包裹）的 Event Broker E-commerce 系統每天處理 ~10M message。完整列出 SQS 配置：每 producer/consumer 隔離 stack（最小爆炸半徑）、3 天 replay via EventBridge、exponential backoff with jitter、24 小時內最多 retry 100 次、final DLQ 允許 consumer 自己 redrive。max receive count 設 1 觸發 DLQ 告警。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「每 producer/consumer 隔離 stack」是 mission-critical 系統的 blast radius 設計、不只是 queue 配置。揭露 production-grade SQS 設計含三件事：隔離 + retry 政策 + redrive 流程。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：DLQ 設計 / CloudWatch alarm / Cost 模型。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/postnl-engineering/design-a-mission-critical-serverless-application-for-high-resilience-2858bf11360a">Designing a Mission-Critical Serverless Application for High Resilience&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是業內真正完整的 DLQ + redrive + retention 設計案例、不是 demo 規模。</p>
<h2 id="觀察">觀察</h2>
<p>PostNL（荷蘭最大物流商、每天 6.9M 信件 + 1.1M 包裹）的 Event Broker E-commerce 系統每天處理 ~10M message。完整列出 SQS 配置：每 producer/consumer 隔離 stack（最小爆炸半徑）、3 天 replay via EventBridge、exponential backoff with jitter、24 小時內最多 retry 100 次、final DLQ 允許 consumer 自己 redrive。max receive count 設 1 觸發 DLQ 告警。</p>
<h2 id="判讀">判讀</h2>
<p>「每 producer/consumer 隔離 stack」是 mission-critical 系統的 blast radius 設計、不只是 queue 配置。揭露 production-grade SQS 設計含三件事：隔離 + retry 政策 + redrive 流程。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：DLQ 設計 / CloudWatch alarm / Cost 模型。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/postnl-engineering/design-a-mission-critical-serverless-application-for-high-resilience-2858bf11360a">Designing a Mission-Critical Serverless Application for High Resilience</a></li>
</ul>
]]></content:encoded></item><item><title>3.C57 Lob：自家 fork @lob/sqs-consumer 修 FIFO bug</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/</guid><description>&lt;p>這個案例的核心責任是說明真實 production library 維護成本、FIFO consumer 的隱性 bug。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Lob（programmatic mail API）原本用 bbc/sqs-consumer 但被鎖在 AWS SDK v2。他們 fork 出 @lob/sqs-consumer：支援 SDK v3（模組化 import 縮 bundle、TypeScript 一級支援、async/await）、修正原 library 對 FIFO queue 的 bug。SQS 用在 Lob API 跟其他內部 service。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不能只靠 SDK 原生 API、SDK 升級會逼出 library 維護議題。揭露「FIFO queue 跟 standard queue 的 client 行為差異」是 library 層的隱性 bug 來源。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / Long polling / Client library 維護。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.lob.com/blog/lob-sqs-consumer">@lob/sqs-consumer&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明真實 production library 維護成本、FIFO consumer 的隱性 bug。</p>
<h2 id="觀察">觀察</h2>
<p>Lob（programmatic mail API）原本用 bbc/sqs-consumer 但被鎖在 AWS SDK v2。他們 fork 出 @lob/sqs-consumer：支援 SDK v3（模組化 import 縮 bundle、TypeScript 一級支援、async/await）、修正原 library 對 FIFO queue 的 bug。SQS 用在 Lob API 跟其他內部 service。</p>
<h2 id="判讀">判讀</h2>
<p>不能只靠 SDK 原生 API、SDK 升級會逼出 library 維護議題。揭露「FIFO queue 跟 standard queue 的 client 行為差異」是 library 層的隱性 bug 來源。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / Long polling / Client library 維護。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.lob.com/blog/lob-sqs-consumer">@lob/sqs-consumer</a></li>
</ul>
]]></content:encoded></item><item><title>3.C58 Twilio：SQS 緩衝高流量 webhook</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/</guid><description>&lt;p>這個案例的核心責任是說明 webhook → SQS buffer 是 Twilio 推薦的 pattern、FIFO TPS 上限的分片實務。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Twilio 自己 engineering blog 教使用者用 SQS 緩衝來自 Twilio 的高流量 SMS / status callback webhook（避免下游 app 來不及處理）。用 separate queue 區分 SMS vs status callback、long polling 減少空 API call、特別點出 FIFO 300 TPS 上限要分 queue。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Webhook 是 push、下游可能來不及、SQS 當 buffer 是常見 pattern。揭露 FIFO 的 300 TPS 上限是 hard limit、要設計分片才能擴張。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Long polling / Standard vs FIFO。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.twilio.com/en-us/blog/handling-high-volume-inbound-sms-and-webhooks-with-twilio-functions-and-amazon-sqs-html">Handling High Volume Inbound SMS and Webhooks with Twilio Functions and Amazon SQS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 webhook → SQS buffer 是 Twilio 推薦的 pattern、FIFO TPS 上限的分片實務。</p>
<h2 id="觀察">觀察</h2>
<p>Twilio 自己 engineering blog 教使用者用 SQS 緩衝來自 Twilio 的高流量 SMS / status callback webhook（避免下游 app 來不及處理）。用 separate queue 區分 SMS vs status callback、long polling 減少空 API call、特別點出 FIFO 300 TPS 上限要分 queue。</p>
<h2 id="判讀">判讀</h2>
<p>Webhook 是 push、下游可能來不及、SQS 當 buffer 是常見 pattern。揭露 FIFO 的 300 TPS 上限是 hard limit、要設計分片才能擴張。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Long polling / Standard vs FIFO。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.twilio.com/en-us/blog/handling-high-volume-inbound-sms-and-webhooks-with-twilio-functions-and-amazon-sqs-html">Handling High Volume Inbound SMS and Webhooks with Twilio Functions and Amazon SQS</a></li>
</ul>
]]></content:encoded></item><item><title>3.C59 Rapid7：SQS 100 億 message/day 規模</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/</guid><description>&lt;p>這個案例的核心責任是建立 SQS 在 10 billion+/day 規模下的成本結構與量級參考點。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Rapid7 Platform Software Architect 公開引述：「SQS 是我們架構的關鍵元件、讓我們 scale 到處理 10s of billions of messages per day。」是 AWS 官方文中具名客戶 quote、非 marketing 概括。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>SQS 在百億訊息/日規模下仍可用、是 scale 的具體量級參考點。揭露 SQS request-based 計費在這個規模下、cost 模型該被認真評估。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Cost 模型 / Standard queue。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&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）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/aws/amazon-sqs-15-years-and-still-queueing/">Amazon SQS — 15 Years and Still Queueing&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是建立 SQS 在 10 billion+/day 規模下的成本結構與量級參考點。</p>
<h2 id="觀察">觀察</h2>
<p>Rapid7 Platform Software Architect 公開引述：「SQS 是我們架構的關鍵元件、讓我們 scale 到處理 10s of billions of messages per day。」是 AWS 官方文中具名客戶 quote、非 marketing 概括。</p>
<h2 id="判讀">判讀</h2>
<p>SQS 在百億訊息/日規模下仍可用、是 scale 的具體量級參考點。揭露 SQS request-based 計費在這個規模下、cost 模型該被認真評估。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Cost 模型 / Standard queue。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/aws/amazon-sqs-15-years-and-still-queueing/">Amazon SQS — 15 Years and Still Queueing</a></li>
</ul>
]]></content:encoded></item><item><title>Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/global-database-multi-region/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/global-database-multi-region/</guid><description>&lt;p>Aurora Global Database 是 &lt;em>跨 region async replication&lt;/em>、&amp;lt; 1 秒 typical lag、最多 5 個 secondary region — 看起來是 multi-region OLTP 的標準解、但 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 揭露一個受監管產業的 anti-recommendation：合規禁止跨境複製場景下、Global Database &lt;em>違反合規&lt;/em>、要改用每市場獨立 cluster + 應用層市場切換。本文展開 Global Database 適用條件、跟 cross-AZ failover 的 RTO 數量級差、合規邊界、跟 Aurora DSQL / Spanner / CockroachDB 的決策樹。&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>）— 而是 Global Database 的實作層教學。前置閱讀建議 &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a>（理解 storage-level replication）、&lt;a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO&lt;/a>（對照單 region failover）。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：global SaaS / 跨地理金融服務、需要 region-level DR（us-east-1 整 region 失效時 &amp;lt; 5 分鐘恢復寫入）、或跨地理 read（歐洲用戶查美國 primary 延遲 100ms+ 不可接受）、但又不到「multi-region active-active write」需求。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「Global Database 是 sync 還是 async？lag 多少？」&lt;/li>
&lt;li>「Secondary region 可以寫嗎？」&lt;/li>
&lt;li>「Region failover 流程跟 cross-AZ 一樣嗎？」&lt;/li>
&lt;li>「跟 Aurora DSQL / Spanner / CockroachDB 怎麼選？」&lt;/li>
&lt;li>「合規場景一定要用 Global Database 嗎？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：Global Database 對一般 SaaS 是合理的 DR + 跨地理 read 工具、但對 &lt;em>受監管產業&lt;/em> 是反指標。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 7 個受監管市場、各自獨立 Aurora cluster、不用 Global Database — 不是技術不夠、是合規要求「資料不能跨境複製」。讀者規劃 multi-region 架構時、合規維度要在技術維度之前判斷。&lt;/p>
&lt;h2 id="核心機制跨-region-async-storage-replication">核心機制：跨 region async storage replication&lt;/h2>
&lt;p>Aurora Global Database 的 first-class concept 是 &lt;em>跨 region storage-level async replication&lt;/em>。跟 logical replication / streaming replication 不同、Global Database 在 storage layer 複製、lag 上限相對穩定。&lt;/p></description><content:encoded><![CDATA[<p>Aurora Global Database 是 <em>跨 region async replication</em>、&lt; 1 秒 typical lag、最多 5 個 secondary region — 看起來是 multi-region OLTP 的標準解、但 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 揭露一個受監管產業的 anti-recommendation：合規禁止跨境複製場景下、Global Database <em>違反合規</em>、要改用每市場獨立 cluster + 應用層市場切換。本文展開 Global Database 適用條件、跟 cross-AZ failover 的 RTO 數量級差、合規邊界、跟 Aurora DSQL / Spanner / CockroachDB 的決策樹。</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>）— 而是 Global Database 的實作層教學。前置閱讀建議 <a href="../storage-architecture/">Aurora storage architecture</a>（理解 storage-level replication）、<a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a>（對照單 region failover）。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：global SaaS / 跨地理金融服務、需要 region-level DR（us-east-1 整 region 失效時 &lt; 5 分鐘恢復寫入）、或跨地理 read（歐洲用戶查美國 primary 延遲 100ms+ 不可接受）、但又不到「multi-region active-active write」需求。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「Global Database 是 sync 還是 async？lag 多少？」</li>
<li>「Secondary region 可以寫嗎？」</li>
<li>「Region failover 流程跟 cross-AZ 一樣嗎？」</li>
<li>「跟 Aurora DSQL / Spanner / CockroachDB 怎麼選？」</li>
<li>「合規場景一定要用 Global Database 嗎？」</li>
</ul>
<p>進一步問題：Global Database 對一般 SaaS 是合理的 DR + 跨地理 read 工具、但對 <em>受監管產業</em> 是反指標。<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 7 個受監管市場、各自獨立 Aurora cluster、不用 Global Database — 不是技術不夠、是合規要求「資料不能跨境複製」。讀者規劃 multi-region 架構時、合規維度要在技術維度之前判斷。</p>
<h2 id="核心機制跨-region-async-storage-replication">核心機制：跨 region async storage replication</h2>
<p>Aurora Global Database 的 first-class concept 是 <em>跨 region storage-level async replication</em>。跟 logical replication / streaming replication 不同、Global Database 在 storage layer 複製、lag 上限相對穩定。</p>
<p><strong>Architecture</strong>：</p>
<ul>
<li>Primary region：1 個 writer cluster + N read replica</li>
<li>Secondary region：最多 5 個 secondary region、每 region N 個 reader-only cluster（最多 16 個 reader 含 1 個 headless）</li>
<li>Storage replication：primary region 寫 storage 後 <em>async</em> push 到 secondary region storage、不等 ack</li>
</ul>
<p><strong>Write path</strong>：</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">Application
</span></span><span class="line"><span class="ln">2</span><span class="cl">    ↓ writer endpoint (primary region only)
</span></span><span class="line"><span class="ln">3</span><span class="cl">Primary region compute
</span></span><span class="line"><span class="ln">4</span><span class="cl">    ↓ redo log
</span></span><span class="line"><span class="ln">5</span><span class="cl">Primary region storage (4-of-6 quorum)
</span></span><span class="line"><span class="ln">6</span><span class="cl">    ↓ async replication (typical &lt; 1 秒)
</span></span><span class="line"><span class="ln">7</span><span class="cl">Secondary region storage</span></span></code></pre></div><p><strong>Read path</strong>：</p>
<ul>
<li>Secondary region 直接從 local storage 讀、不需要跨 region 拉</li>
<li>Read latency 是 secondary region local latency、不是跨 region</li>
</ul>
<p><strong>DR 切換 RTO 跟 cross-AZ 對比</strong>：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>RTO</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cross-AZ failover</td>
          <td>&lt; 30 秒</td>
          <td>storage 跨 AZ 共享、replica 升 primary 即可</td>
      </tr>
      <tr>
          <td>Planned failover</td>
          <td>&lt; 2 分鐘</td>
          <td>managed graceful failover、無資料丟失</td>
      </tr>
      <tr>
          <td>Unplanned failover</td>
          <td>5-15 分鐘</td>
          <td>整 region 失效、手動 promote secondary</td>
      </tr>
  </tbody>
</table>
<p>數量級不同 — cross-AZ 是 <em>seconds</em>、cross-region planned 是 <em>minutes</em>、unplanned 是 <em>tens of minutes</em>。</p>
<p><strong>對應 knowledge card</strong>：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>。</p>
<p><strong>跟通用 cross-region replication 差在哪</strong>：Aurora 在 storage layer 複製、lag 上限更穩定；vs PostgreSQL logical replication lag 受寫速度影響大、heavy write 期間可能秒級到分鐘級。</p>
<h2 id="step-by-step-配置">Step-by-step 配置</h2>
<p><strong>建 global cluster</strong>：</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"># Step 1：在 primary region 建 global cluster</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws rds create-global-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --global-cluster-identifier myglobal <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --source-db-cluster-identifier arn:aws:rds:us-east-1:123:cluster:primary-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --region us-east-1
</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="c1"># Step 2：在 secondary region 加 reader cluster</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">aws rds create-db-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --db-cluster-identifier secondary-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --global-cluster-identifier myglobal <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --source-region us-east-1 <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --region eu-west-1
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Step 3：在 secondary region 建 db instance</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">aws rds create-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --db-cluster-identifier secondary-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  --db-instance-identifier secondary-reader-01 <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>  --db-instance-class db.r6g.4xlarge <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --region eu-west-1</span></span></code></pre></div><p><strong>Application routing</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># 寫永遠去 primary region writer endpoint</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">primary</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">jdbc:postgresql://primary-cluster.cluster-xxx.us-east-1.rds.amazonaws.com/mydb</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c"># read 可走 secondary region reader endpoint（靠近用戶的 region）</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="nt">secondary-eu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">jdbc:postgresql://secondary-cluster.cluster-ro-xxx.eu-west-1.rds.amazonaws.com/mydb</span></span></span></code></pre></div><p><strong>DR 切換（planned failover）</strong>：</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 rds failover-global-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --global-cluster-identifier myglobal <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --target-db-cluster-identifier arn:aws:rds:eu-west-1:123:cluster:secondary-cluster</span></span></code></pre></div><p>切換後 application 端要 <em>reconfigure connection string</em> — DNS 不自動切跨 region（vs cross-AZ failover writer endpoint 自動跟）。</p>
<p><strong>Application reconfiguration 模式</strong>：</p>
<ul>
<li>Connection string 用 service discovery（Consul / Route53 health check）動態解析</li>
<li>或在 application config 加入 region-aware logic、failover 後切換 active region</li>
<li>不能假設 application 自動 reconnect 到新 primary region</li>
</ul>
<p><strong>驗證點</strong>：</p>
<ul>
<li><code>AuroraGlobalDBReplicationLag</code> &lt; 1 秒</li>
<li>Planned failover RTO 量測（手動 trigger + heartbeat timestamp diff）</li>
<li>Application 跨 region read 路徑 latency 符合預期</li>
</ul>
<p><strong>Rollback boundary</strong>：promote secondary 後原 primary 變 secondary、不會自動 fallback；rollback 要再做一次 failover。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1期待-multi-region-active-active-write">Case 1：期待 multi-region active-active write</h3>
<p>徵兆：team 在 secondary region application 直連 secondary cluster 寫資料、收到 <code>cannot execute INSERT in a read-only transaction</code> 錯誤。</p>
<p>原因：Global Database secondary 是 <em>reader-only</em>、寫只能去 primary region。要 active-active write 必須改用其他服務（Aurora DSQL / Spanner / CockroachDB）。</p>
<p>修：</p>
<ul>
<li>Application 設計時明確區分 read region vs write region</li>
<li>寫操作永遠路由到 primary region、容忍跨 region write latency</li>
<li>真的需要 active-active write 才考慮 Aurora DSQL（2024-12 preview / 2025-05 GA）</li>
</ul>
<h3 id="case-2dns-不跨-region-自動切">Case 2：DNS 不跨 region 自動切</h3>
<p>徵兆：手動 failover trigger 後、application 端 connection string 仍指向舊 primary region、寫操作全失敗。</p>
<p>原因：cross-AZ failover writer endpoint DNS 自動跟、cross-region 不會 — Global Database 切換要 application 端管 region-specific connection string。</p>
<p>修：</p>
<ul>
<li>Application 用 service discovery（Route53 / Consul / etcd）解析 active primary region</li>
<li>部署 region-aware DNS（Route53 latency-based routing + health check）</li>
<li>Failover 演練要包含 application reconfiguration step、不只是 DB layer</li>
</ul>
<h3 id="case-3跨-region-read-假設-strong-consistency">Case 3：跨 region read 假設 strong consistency</h3>
<p>徵兆：用戶在 primary region 寫資料、隨即在 secondary region read、看到舊資料、客訴 inconsistency。</p>
<p>原因：Global Database 是 async replication、&lt; 1 秒 lag 不是 zero、read-after-write 場景仍會看到 stale data。</p>
<p>修：</p>
<ul>
<li>用戶寫操作後短期內 read 走 primary region（read-after-write window）</li>
<li>接受最終一致性、application 端做 versioning / timestamp 比對</li>
<li>強一致性需求改 Aurora DSQL / Spanner</li>
</ul>
<h3 id="case-4lag-spike-during-bulk-operation">Case 4：Lag spike during bulk operation</h3>
<p>徵兆：DDL 或 bulk insert 期間 cross-region lag 從 &lt; 1 秒跳到秒級到分鐘級、secondary region read 大量 stale。</p>
<p>原因：Global Database 「&lt; 1 秒」是 typical、heavy write 期間 lag 拉大。Storage-level replication 比 logical 穩定、但 <em>不是 zero variance</em>。</p>
<p>修：</p>
<ul>
<li>DDL 跟 bulk insert 在低峰期跑、避開跨 region read traffic</li>
<li>監測 <code>AuroraGlobalDBReplicationLag</code>、spike 超過閾值 trigger application 端 fallback（read 切回 primary region）</li>
<li>重要 DDL 用 <a href="https://github.com/reorg/pg_repack">pg_repack</a> 避免長時間 lag</li>
</ul>
<h3 id="case-5合規邊界誤用-global-database--standard-chartered-anti-pattern">Case 5：合規邊界誤用 Global Database — Standard Chartered anti-pattern</h3>
<p>徵兆：team 以為 Global Database 是受監管金融的標準 DR 解、配置完才發現監管機構不接受跨境資料複製、被迫拆掉 Global Database 重建獨立 cluster。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered case</a> 「判讀」段第 1 點原文：「7 個受監管市場代表 7 個獨立 cluster（資料不能跨境）、容量規劃變成『7 個獨立規劃 × 各自合規門檻』」。</p>
<p>原因：受監管市場資料 <em>不能跨境複製</em>（<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 硬約束）、Global Database 本質上就是跨 region storage replication、配置了就違反合規。Standard Chartered 的選擇是 <em>每市場獨立 cluster</em>、跨市場 DR 走應用層市場切換、不靠 Global Database。</p>
<p>修：</p>
<ul>
<li>規劃 multi-region 前先確認合規要求（資料駐留、跨境複製禁令、稽核要求）</li>
<li>合規禁止跨境複製場景：每市場獨立 cluster + cross-AZ failover 吸收 RTO（見 <a href="../cross-az-failover-rto/">cross-az-failover-rto</a>）</li>
<li>跨市場 DR 設計成 <em>市場切換</em>（用戶從 A 市場切到 B 市場）、不是 <em>資料切換</em></li>
<li>Fleet 拓樸（多市場 → 多 cluster）詳見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet 治理 SSoT</li>
</ul>
<p><strong>scope warning（必明示）</strong>：Standard Chartered case 未公開是 PostgreSQL 還是 MySQL、未公開具體 cost 數字、屬「相關 case study」匿名對照。引用時不能擴寫具體 engine。</p>
<h3 id="case-6cost-trap--cross-region-data-transfer">Case 6：Cost trap — cross-region data transfer</h3>
<p>徵兆：開了 Global Database 後月帳變高 50%、發現 cross-region data transfer 是主要費用、不是 instance。</p>
<p>原因：Aurora 跨 region replication 走 AWS 內部網路、但 <em>cross-region data transfer 仍計費</em>。Heavy write workload 月費可能 doubled。</p>
<p>修：</p>
<ul>
<li>用 <code>AuroraGlobalDBReplicatedWriteIO</code> × per-region transfer rate 估月費</li>
<li>Write-heavy workload 評估 Global Database ROI（保險、低費用版本是用 cross-region snapshot 做冷備）</li>
<li>Cost 跟 RTO 一起看 — 如果接受 hours RTO、cross-region snapshot 更便宜</li>
</ul>
<h3 id="case-7fanduel-雙峰-case-對照避免-over-extrapolate">Case 7：FanDuel 雙峰 case 對照（避免 over-extrapolate）</h3>
<p>如果 team 引用 <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> 規劃 multi-region 部署、要明示 scope warning。</p>
<p><strong>case「判讀」段第 1 點原文</strong>：「直播跟投注是兩種完全不同 SLO：直播容忍秒級延遲（用 CDN + ABR 串流）、投注必須毫秒級成交。兩個服務必須各自獨立擴容、各自獨立 SLO」。</p>
<p><strong>scope warning（必明示）</strong>：</p>
<ul>
<li>FanDuel 5-10x 是 <em>betting 服務的 Aurora 擴容倍數</em>、不是 streaming（streaming 走 CDN、不走 Aurora）</li>
<li>不能壓成「Aurora 撐 5-10x」單一數字</li>
<li>案例自承：betting transaction TPS 跟 concurrent streams 未公開、不能 over-extrapolate</li>
</ul>
<p>引用 FanDuel 規劃自家 multi-region betting workload 時、看 <em>策略</em>（事件型分級 + 雙 SLO 拆分 + 多層 edge）、不套用 <em>具體數字</em>。</p>
<h2 id="跟-aurora-dsql--spanner--cockroachdb-的決策樹">跟 Aurora DSQL / Spanner / CockroachDB 的決策樹</h2>
<p>Global Database 是 <em>async + reader-only secondary</em>、不是 multi-region active-active。當 active-active write 是核心需求時、要看 distributed SQL 方案。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Aurora Global Database</th>
          <th>Aurora DSQL</th>
          <th>Spanner</th>
          <th>CockroachDB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replication</td>
          <td>Async storage-level</td>
          <td>Sync distributed</td>
          <td>Sync TrueTime</td>
          <td>Sync Raft consensus</td>
      </tr>
      <tr>
          <td>Secondary</td>
          <td>Reader-only</td>
          <td>Active-active</td>
          <td>Active-active</td>
          <td>Active-active</td>
      </tr>
      <tr>
          <td>Lag</td>
          <td>&lt; 1 秒 typical</td>
          <td>None (sync)</td>
          <td>None (sync)</td>
          <td>None (sync)</td>
      </tr>
      <tr>
          <td>Write</td>
          <td>Primary region only</td>
          <td>Multi-region</td>
          <td>Multi-region</td>
          <td>Multi-region</td>
      </tr>
      <tr>
          <td>Strong consistency cross-region</td>
          <td>No</td>
          <td>Yes</td>
          <td>Yes</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>DR + 跨地理 read</td>
          <td>Multi-region OLTP</td>
          <td>Global scale OLTP</td>
          <td>Cross-cloud OLTP</td>
      </tr>
      <tr>
          <td>邊界</td>
          <td>active-active 不支援、合規 反指標</td>
          <td>AWS-only、新服務</td>
          <td>GCP-only、學習曲線</td>
          <td>跨雲、operational 複雜</td>
      </tr>
  </tbody>
</table>
<p><strong>何時選 Global Database</strong>：</p>
<ul>
<li>DR + 跨地理 read 是主要需求</li>
<li>寫流量集中在一個 region（單 region write 撐得住）</li>
<li>合規允許跨境複製（一般 SaaS、非受監管）</li>
<li>從 single-region Aurora 升級、不想換 engine</li>
</ul>
<p><strong>何時改 Aurora DSQL / Spanner / CockroachDB</strong>：</p>
<ul>
<li>Multi-region active-active write</li>
<li>跨 region strong consistency 是業務需求</li>
<li>跨雲 / on-prem 需求（CockroachDB）</li>
</ul>
<p><strong>何時不用 Global Database</strong>：</p>
<ul>
<li>合規禁止跨境複製（Standard Chartered case）→ 每市場獨立 cluster</li>
<li>Single-region 已滿足 DR / read 需求</li>
<li>跨 region cost 不划算（write-heavy workload）</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</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">AuroraGlobalDBReplicationLag       # secondary lag、&lt; 1 秒 typical
</span></span><span class="line"><span class="ln">2</span><span class="cl">AuroraGlobalDBReplicatedWriteIO    # cross-region data transfer 量
</span></span><span class="line"><span class="ln">3</span><span class="cl">AuroraGlobalDBProgressLag          # storage replication progress</span></span></code></pre></div><p><strong>容量上限</strong>：</p>
<ul>
<li>1 primary region + 5 secondary region</li>
<li>每 secondary region 16 個 reader 含 1 個 headless（可升 writer）</li>
</ul>
<p><strong>Cost signal</strong>：</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">月費 ≈ AuroraGlobalDBReplicatedWriteIO × per-region transfer rate
</span></span><span class="line"><span class="ln">2</span><span class="cl">     + secondary region instance + storage
</span></span><span class="line"><span class="ln">3</span><span class="cl">     + cross-region snapshot (optional)</span></span></code></pre></div><p>Write 量大的 workload 月費可能 doubled（primary region + secondary region 都計費）、要在規劃時估準。</p>
<p><strong>驗證 DR</strong>：</p>
<ul>
<li>Planned failover drill 每季一次、量測 RTO / RPO</li>
<li>受監管產業：每月一次、有合規 sign-off 記錄</li>
<li>重大版本升級前必跑一次</li>
</ul>
<p><strong>回路徑</strong>：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> cross-region cost、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.x DR playbook</a> region-level failover decision。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../storage-architecture/">Aurora storage architecture</a> — cross-region replication 是 storage-level 延伸</li>
<li><a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> — cross-AZ 跟 cross-region failover RTO 數量級對比</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — fleet 治理 SSoT、合規驅動 fleet 拓樸的展開</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — 從 PostgreSQL streaming replication 跨 region 升級的差異</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> — Global Database vs distributed SQL 對比</li>
</ul>
<p><strong>何時不用本文</strong>：single-region OLTP、無跨 region DR / read 需求時可跳過、看 <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 overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read 卡片</a> — read-after-write 容忍度</li>
<li><a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO 卡片</a> — DR RPO 判讀</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 合規驅動的 Global Database anti-pattern</li>
<li><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 雙 SLO 並行的 multi-region 策略對照</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html">Aurora Global Database</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Locality-Aware Schema：跨州合規 + 邏輯一個 cluster 的 region placement 策略</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 &lt;em>locality 配置怎麼解合規地理邊界 + 跨 boundary 業務邏輯需求&lt;/em> — 用 Hard Rock Digital 跨 8 州單一邏輯 cluster 作為 concrete framing。Replica placement 機制屬前置、見 &lt;a href="../hlc-raft-consensus/">HLC + Raft consensus&lt;/a>、survival goal 互動見 &lt;a href="../survival-goals/">survival goals&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境hard-rock-的跨州-sportsbook-拓樸創新">問題情境：Hard Rock 的跨州 sportsbook 拓樸創新&lt;/h2>
&lt;p>美國 sportsbook 受 &lt;em>Wire Act&lt;/em> 規範、betting data 必須在下注州內處理 → 每個營運州都要有州內運算資源。傳統路徑是「每州一個獨立 silo、each silo 一個獨立 DB cluster」、合規上沒問題、但撞牆於三個業務需求：&lt;/p>
&lt;ul>
&lt;li>&lt;em>跨州統一帳戶&lt;/em>：玩家在 NJ 跟 FL 兩州都有帳戶、登入要看到統一 portfolio&lt;/li>
&lt;li>&lt;em>跨州 reporting&lt;/em>：總公司 BI / 財務 reporting 要橫跨所有州、不能 query N 個 cluster 後再合&lt;/li>
&lt;li>&lt;em>跨州欺詐偵測&lt;/em>：同一張身分證在不同州 IP 同時下注 → 風控引擎要看 &lt;em>cross-state aggregated&lt;/em> 資料&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a> 跨 8 州（AZ / IN / TN / FL / OH / IL / NJ / VA）用 AWS Outposts 把運算放進州內、但邏輯上仍是 &lt;em>一個&lt;/em> CockroachDB cluster — region placement 配置決定哪些 range 釘在哪個 Outpost / AWS region。case 觀察段直接揭露「跨所有 region 一個 logical database」這個拓樸 fact。&lt;/p>
&lt;p>讀者常問：&lt;/p>
&lt;ul>
&lt;li>合規逼我每州一 cluster、但跨州帳戶 / 風控 / 欺詐偵測撞牆怎麼辦？&lt;/li>
&lt;li>&lt;code>REGIONAL BY ROW&lt;/code> 跟 &lt;code>REGIONAL BY TABLE&lt;/code> 怎麼選、&lt;code>GLOBAL&lt;/code> 又在什麼場景？&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code> table 為什麼讀快但寫慢、預設為什麼不全部用？&lt;/li>
&lt;li>AWS Outposts 是 latency 工具還是合規工具？&lt;/li>
&lt;/ul>
&lt;p>對照 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a>：60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region、locality 配置直接影響 cluster 規模治理。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 <em>locality 配置怎麼解合規地理邊界 + 跨 boundary 業務邏輯需求</em> — 用 Hard Rock Digital 跨 8 州單一邏輯 cluster 作為 concrete framing。Replica placement 機制屬前置、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>、survival goal 互動見 <a href="../survival-goals/">survival goals</a>。</p></blockquote>
<hr>
<h2 id="問題情境hard-rock-的跨州-sportsbook-拓樸創新">問題情境：Hard Rock 的跨州 sportsbook 拓樸創新</h2>
<p>美國 sportsbook 受 <em>Wire Act</em> 規範、betting data 必須在下注州內處理 → 每個營運州都要有州內運算資源。傳統路徑是「每州一個獨立 silo、each silo 一個獨立 DB cluster」、合規上沒問題、但撞牆於三個業務需求：</p>
<ul>
<li><em>跨州統一帳戶</em>：玩家在 NJ 跟 FL 兩州都有帳戶、登入要看到統一 portfolio</li>
<li><em>跨州 reporting</em>：總公司 BI / 財務 reporting 要橫跨所有州、不能 query N 個 cluster 後再合</li>
<li><em>跨州欺詐偵測</em>：同一張身分證在不同州 IP 同時下注 → 風控引擎要看 <em>cross-state aggregated</em> 資料</li>
</ul>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 跨 8 州（AZ / IN / TN / FL / OH / IL / NJ / VA）用 AWS Outposts 把運算放進州內、但邏輯上仍是 <em>一個</em> CockroachDB cluster — region placement 配置決定哪些 range 釘在哪個 Outpost / AWS region。case 觀察段直接揭露「跨所有 region 一個 logical database」這個拓樸 fact。</p>
<p>讀者常問：</p>
<ul>
<li>合規逼我每州一 cluster、但跨州帳戶 / 風控 / 欺詐偵測撞牆怎麼辦？</li>
<li><code>REGIONAL BY ROW</code> 跟 <code>REGIONAL BY TABLE</code> 怎麼選、<code>GLOBAL</code> 又在什麼場景？</li>
<li><code>GLOBAL</code> table 為什麼讀快但寫慢、預設為什麼不全部用？</li>
<li>AWS Outposts 是 latency 工具還是合規工具？</li>
</ul>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>：60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region、locality 配置直接影響 cluster 規模治理。</p>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> Aurora 7 cluster fleet：銀行業跨國合規邊界、走的是「每市場獨立 Aurora cluster」路徑 — 跟 Hard Rock 邏輯一個 cluster 的拓樸完全不同。兩條路徑沒有對錯、trigger 條件不同（合規顆粒 × 跨 boundary 業務邏輯需求）。</p>
<h2 id="核心機制三種-table-locality--row-level-region-標記">核心機制：三種 table locality + row-level region 標記</h2>
<h3 id="三種-locality-模式">三種 locality 模式</h3>
<p>CockroachDB 用 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> 把 multi-region table 抽象成三種 locality、配合 <a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 合規邊界決定 row 落在哪個 region：</p>
<table>
  <thead>
      <tr>
          <th>Locality</th>
          <th>Read 行為</th>
          <th>Write 行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>REGIONAL BY TABLE</code></td>
          <td>本 region 快、其他 region 走 follower read</td>
          <td>本 region 快、其他 region 慢</td>
          <td>整 table 服務單一 region（如：us-orders）</td>
      </tr>
      <tr>
          <td><code>REGIONAL BY ROW</code></td>
          <td>該 row 所在 region 快、其他 follower</td>
          <td>該 row 所在 region 快、其他慢</td>
          <td>用戶資料跟地理綁定（玩家 / 訂單 / 帳戶）</td>
      </tr>
      <tr>
          <td><code>GLOBAL</code></td>
          <td>每 region local（快）</td>
          <td>跨 region quorum（慢）</td>
          <td>reference data（國碼、貨幣、規則表）</td>
      </tr>
  </tbody>
</table>
<h3 id="regional-by-row每-row-帶-crdb_region-隱含欄位">REGIONAL BY ROW：每 row 帶 <code>crdb_region</code> 隱含欄位</h3>
<p><code>REGIONAL BY ROW</code> 是 Hard Rock 場景的主要選擇。每 row 自動帶一個 <code>crdb_region</code> 隱含欄位、根據這個欄位把 row 對應的 range 釘在指定 region：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-az&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-nj&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-fl&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 寫入時指定 row 屬哪個 region
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">crdb_region</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;us-east1-nj&#39;</span><span class="p">);</span></span></span></code></pre></div><p>CockroachDB planner 自動感知 <code>crdb_region</code>、把 read / write 路由到 row 所在 region 的 leaseholder。application 不用手動配 shard key、不用 application 端路由邏輯 — 這是 distributed SQL 的「宣告式 locality」優勢。</p>
<h3 id="global每-region-local-read跨-region-sync-write">GLOBAL：每 region local read、跨 region sync write</h3>
<p><code>GLOBAL</code> table 適合 <em>reference data</em> — 變更少、read 頻繁、需要全球 local read latency：</p>
<ul>
<li>read：每 region 都有 leaseholder、本地 read p99 跟 single-region 一樣</li>
<li>write：跨 region quorum、p99 100ms+</li>
</ul>
<p>實務上 <code>GLOBAL</code> 只放國家代碼、貨幣表、規則 lookup 等 <em>變更頻率低</em> 的 reference data。把 high-write workload 設成 <code>GLOBAL</code> 是典型錯配（見失敗模式段）。</p>
<h3 id="follower-readnon-voting-replica-提供本地-read">Follower read：non-voting replica 提供本地 read</h3>
<p>CockroachDB 區分 voting 跟 non-voting replica：</p>
<ul>
<li>voting replica 參與 Raft majority、決定 commit</li>
<li>non-voting replica 不參與 commit、只 serve <a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">Follower Read</a></li>
</ul>
<p><code>REGIONAL BY ROW</code> + <code>SURVIVE REGION FAILURE</code> 配合時：row 所在 region 是 voting + <a href="/blog/backend/knowledge-cards/leaseholder/" data-link-title="Leaseholder" data-link-desc="分散式 SQL 每個 range 在任一時間點的 read / write entry point、通常等於 Raft leader、承擔該 range 的 coordination">Leaseholder</a>、其他 region 有 voting replica（survival 需要）+ non-voting replica（本地 follower read）。</p>
<p>Follower read 讀到的是 <em>closed timestamp</em> 之前的資料 — strong consistency 場景不能用（read-after-write 會 stale）、但 dashboard / reporting / 風控分析等 <em>容忍 stale</em> 場景大幅降低 cross-region latency。</p>
<h3 id="配置語法跟驗證">配置語法跟驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 設 database 的 region
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;europe-west1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 設 table locality
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">country_codes</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_us</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;us-east1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 驗證
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">RANGES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 看 replica 分佈
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 看 query plan 是否 local</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a>、<a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a> 的具體機制實現。</p>
<h2 id="操作流程從合規-boundary-到-schema-配置">操作流程：從合規 boundary 到 schema 配置</h2>
<h3 id="配置-multi-region-database">配置 multi-region database</h3>
<p>第一步是把所有 region 加入 database：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 假設 cluster 已跨 8 個州（透過 AWS Outposts 在每州內）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-virginia&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-nj&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-fl&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-az&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- ...其他州</span></span></span></code></pre></div><p>每個「region」對應一個 Outpost / AWS region 的 locality tag、CockroachDB Raft 根據 locality 自動分佈 replica。</p>
<h3 id="table-level-locality-配置">Table-level locality 配置</h3>
<p>bet placement / settlement table 走 <code>REGIONAL BY ROW</code>（資料跟玩家所在州綁定）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">settlements</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span></span></span></code></pre></div><p>account / user profile 跨州統一帳戶 — 玩家可能在多州下注、但 <em>主檔</em> 留 single region：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;us-east1-virginia&#34;</span><span class="p">;</span></span></span></code></pre></div><p>reference data（運動類別、賽事 metadata）— 全球變更少、每州都要快速 read：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sports_metadata</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span></span></span></code></pre></div><h3 id="application-端寫入">Application 端寫入</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 顯式指定 row 所在 region（推薦、明確）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">crdb_region</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;NJ&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;us-east1-nj&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 或用 gateway_region() default（依 application 連到的 region）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;NJ&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="p">);</span><span class="w">  </span><span class="c1">-- crdb_region 自動填 gateway 端</span></span></span></code></pre></div><p><code>gateway_region()</code> 是便利但有風險的 default — 如果 application server 在 us-east1-fl 但 user 在 NJ 下注、row 會被放到 FL 而不是 NJ、違反 Wire Act 合規。Hard Rock 場景下顯式指定 <code>crdb_region</code> 是更安全的做法。</p>
<h3 id="rollback-邊界">Rollback 邊界</h3>
<p>locality 變更即時生效、Raft 自動 rebalance — 無不可逆動作。但 rebalance 期間 cross-region traffic 暴增、p99 短期 spike。production 環境改 locality 應該選低流量時段、並監控 rebalance queue。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="拆獨立-cluster-解合規但破壞業務邏輯反模式hard-rock-對比-standard-charteredf410">「拆獨立 cluster 解合規但破壞業務邏輯」反模式（Hard Rock 對比 Standard Chartered、F4.10）</h3>
<p>直覺路徑是「合規要求資料留某地理邊界 → 每邊界開一個獨立 cluster」、合規上沒問題。但獨立 cluster 之間：</p>
<ul>
<li>玩家統一帳戶撞牆 — 每 cluster 各自有 user table、跨 cluster query 麻煩</li>
<li>跨州 reporting 要 N 個 cluster + ETL pipeline</li>
<li>欺詐偵測要 <em>cross-state aggregated view</em> — 獨立 cluster 拼不出</li>
</ul>
<p>Hard Rock 選擇 <em>邏輯一個 cluster + 物理跨州 Outpost placement</em> — 合規 boundary 用 region placement 表達、不是 cluster fragmentation。對比 Standard Chartered：</p>
<ul>
<li><strong>Standard Chartered Aurora 7 cluster fleet</strong>：銀行業跨國合規邊界、<em>跨 cluster 業務邏輯需求弱</em>（每市場用戶獨立、跨境統一帳戶不是核心 driver）→ 用 fleet 拓樸吸收合規可行</li>
<li><strong>Hard Rock Wire Act 跨州</strong>：跨州統一帳戶 + 跨州 reporting + 欺詐偵測是 <em>核心業務需求</em> → 必須邏輯一個 cluster、用 locality + placement 吸收合規</li>
</ul>
<p>兩條路徑沒有對錯、trigger 條件不同。判讀軸線：</p>
<ul>
<li>合規顆粒（跨國 vs 跨州 vs 跨 AZ）</li>
<li>跨 boundary 業務邏輯需求強度（強 → CockroachDB locality / 弱 → 拆獨立 cluster 可行）</li>
<li>團隊運維能力（CockroachDB 邏輯一個 cluster vs Aurora 多 cluster fleet 的人月成本）</li>
</ul>
<h3 id="outposts-是-latency-工具動機誤判f413case-反直覺判讀">「Outposts 是 latency 工具」動機誤判（F4.13、case 反直覺判讀）</h3>
<p>AWS Outposts 主要為「資料留某地理邊界」存在、latency 改善是 <em>副作用</em>。Hard Rock 策略段 2 明確警告：「決策時先看合規驅動力、latency 改善列為 bonus」。</p>
<p>若把 Outposts 當跨州 latency 改善工具、會在沒合規驅動的場景過度投資 — Outposts 硬體成本 + 維運複雜度遠高於純 AWS region 部署。實務判讀：</p>
<ul>
<li>有合規驅動（Wire Act / GDPR / 各州博彩牌照）→ Outposts 是合理投資</li>
<li>純 latency 優化 → 用 AWS Local Zones、用 CDN、用 edge cache、不要碰 Outposts</li>
<li>兩者並存 → Outposts 投資按 <em>合規</em> 計算、latency 改善是 ROI 加分項</li>
</ul>
<h3 id="global-table-write-太慢"><code>GLOBAL</code> table write 太慢</h3>
<p><code>GLOBAL</code> table 每次 write 跨 region quorum、p99 100ms+。用在 high-write workload 是典型錯配 — 該用在 reference data（國家代碼、貨幣表、規則 lookup）。</p>
<p>判讀：</p>
<ul>
<li>write QPS &lt; 10 + read QPS 跨 region 高 → <code>GLOBAL</code> 合理</li>
<li>write QPS &gt; 100 → 不要用 <code>GLOBAL</code>、改 <code>REGIONAL BY ROW</code> + 接受 cross-region read 偶爾走 follower</li>
</ul>
<h3 id="regional-by-row-但-row-沒設-crdb_region"><code>REGIONAL BY ROW</code> 但 row 沒設 <code>crdb_region</code></h3>
<p>application 寫入時忘了設 <code>crdb_region</code>、default 走 <code>gateway_region()</code> — application server 所在 region 變成 row 的 region。常見後果：</p>
<ul>
<li>application server 集中部署 → 所有 row 跑同一 region、locality 失效</li>
<li>application server 跟 user 不同 region → 合規 violation（Wire Act 場景）</li>
</ul>
<p>修法：顯式指定 <code>crdb_region</code>、把 user 的合規區域當業務欄位明確管理。</p>
<h3 id="cross-region-join-跑爆-latency">Cross-region join 跑爆 latency</h3>
<p>兩個 <code>REGIONAL BY ROW</code> table join、planner 要跨 region 拉資料、p99 暴漲。</p>
<p>修法：</p>
<ul>
<li>兩個 table partition by <em>同樣</em> 的 key（如：user_id）、保證 join 對應 row 在同 region</li>
<li>不能保證 co-location 時、考慮用 follower read 接受 stale 資料</li>
<li>query 重寫成多步：先在各 region 算 local 結果、application 端 merge</li>
</ul>
<h3 id="follower-read-假設-strong-consistency">Follower read 假設 strong consistency</h3>
<p>non-voting replica 是 <em>closed timestamp</em> 之前的資料、read-after-write 場景仍會 stale。</p>
<p>修法：</p>
<ul>
<li>read-after-write critical（如：剛下注立刻顯示「下注成功」）→ 不能走 follower、要走 leaseholder</li>
<li>dashboard / 分析 / reporting 容忍 stale → follower read 安全、大幅降 latency</li>
</ul>
<h3 id="data-residency-違規">Data residency 違規</h3>
<p>受監管州 / 國資料應留 boundary 內、但 application 從別 region 寫入 row、沒設 <code>crdb_region</code>、資料跑出 boundary、合規 violation（Wire Act / GDPR / 各州博彩牌照都有類似條款）。</p>
<p>修法（schema-level + application-level 雙保險）：</p>
<ul>
<li>schema：<code>REGIONAL BY ROW</code> + <code>crdb_region</code> 是 NOT NULL + CHECK constraint 限制可選值</li>
<li>application：寫入前明確驗證 <code>crdb_region</code> 對應 user 所在合規區</li>
<li>監控：定期跑 <code>SELECT crdb_region, count(*) FROM bets GROUP BY crdb_region</code> 確認分佈符合預期</li>
</ul>
<h3 id="hard-rock-場景的組合配置9c41">Hard Rock 場景的組合配置（9.C41）</h3>
<p>bet placement / settlement / account management 都需要跨州資料存取 + 州內合規 placement。Hard Rock 案例揭露的具體組合：</p>
<ul>
<li><code>REGIONAL BY ROW</code> + <code>crdb_region</code> 標州別 + region placement pin Outpost</li>
<li>account 跨州統一 → <code>REGIONAL BY TABLE</code> IN primary region、其他州走 follower read</li>
<li>sports metadata → <code>GLOBAL</code>、reference data 全州 local read</li>
</ul>
<p>這是滿足 Wire Act + 跨州業務邏輯的組合、不是唯一解、但揭露了 schema 設計的 <em>判讀軸</em> — 不是「locality 越強越好」、是「locality 對應業務 + 合規邊界」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Range locality distribution</code>：range 分佈跟 locality 配置是否一致</li>
<li><code>Cross-region query count</code>：cross-region query 數量、locality 失效訊號</li>
<li><code>Follower read rate</code>：follower read 命中率、降 latency 效果</li>
<li><code>Leaseholder distribution by region</code>：leaseholder 在 region 間是否均勻</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>cross-region traffic = <code>GLOBAL</code> table write QPS × region count</li>
<li><code>REGIONAL BY ROW</code> 跨 region read = follower read rate × QPS</li>
<li>storage 用量 = base storage × replication factor × (voting + non-voting replica count)</li>
</ul>
<h3 id="容量上限">容量上限</h3>
<ul>
<li>region count：建議 ≤ 5（多 region 增加 quorum latency + 維運複雜度）</li>
<li><code>GLOBAL</code> table 數量：建議只放 reference data、總 row 數 &lt; 10 萬</li>
<li>single range 寫 throughput ~1000 QPS（通用估算、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 cross-region-bound vs CPU-bound</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游合規 / latency 取捨</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../survival-goals/">survival goals</a>：locality + survival goal 一起決定 replica placement</li>
<li><a href="../transaction-retry-pattern/">transaction retry pattern</a>：partition 降低 hot row contention 的 schema 路徑</li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：leaseholder 跟 locality 的關係</li>
</ul>
<h3 id="跟-aurora-global-database-對照">跟 Aurora Global Database 對照</h3>
<p>Aurora 不支援 row-level locality — 跨 region 只能 cluster-per-region + async replication。CockroachDB 在一個 cluster 內可以 fine-grained locality、application 不需要管 cross-cluster 路由。Aurora Global Database 適合 <em>async DR</em> 場景、不適合 <em>跨 region 強一致 + row-level locality</em> 需求。</p>
<h3 id="跟-spanner-interleaved-tables-對照">跟 Spanner interleaved tables 對照</h3>
<p>Spanner 的 <a href="/blog/backend/knowledge-cards/interleaved-table/" data-link-title="Interleaved Table" data-link-desc="Spanner 把 parent / child table row 物理交錯儲存、parent &#43; child JOIN 不跨 split">Interleaved Table</a> 跟 CockroachDB 的 <code>REGIONAL BY ROW</code> 概念類似（parent-child row co-location）、語法不同。Spanner 在 GCP region 內 placement、無 Outposts 等效 — Hard Rock 場景下 Spanner 不能直接套用。</p>
<h3 id="aurora-dsql--spanner-對比">Aurora DSQL / Spanner 對比</h3>
<p>完整三家 distributed SQL 在 locality / multi-region placement 的取捨、見 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region 部署、無 data residency 需求 → 用 default locality 即可</li>
<li>合規邊界 <em>禁止</em> 跨境 replica（如 Standard Chartered 模式）→ 拆 cluster-per-市場、不走本文 locality 路徑</li>
<li>純 latency 優化、無合規驅動 → 用 CDN / cache / Local Zones、不必動 schema</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（concrete framing — 跨 8 州 + Outposts + 邏輯一個 cluster）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（多 region locality 規模治理）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>（fleet 拓樸對照、不同合規邊界）</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a> / <a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">CockroachDB Multi-Region Capabilities</a> / <a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">Table Localities</a> / <a href="https://www.cockroachlabs.com/docs/stable/follower-reads.html">Follower Reads</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB Partition Key Design：synthetic / composite / hierarchical + 不可逆性硬約束</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/partition-key-design/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/partition-key-design/</guid><description>&lt;p>Cosmos DB 的 &lt;em>logical partition 上限是 10,000 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &amp;#43; memory &amp;#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit&lt;/a>/s + 20 GB storage&lt;/em>、partition key 一旦上 production &lt;em>改不了&lt;/em>（要 export → recreate container → import）。partition key 選錯的後果是 Black Friday / 上線日 / VIP 用戶把流量壓在少數 partition、p99 latency 從 50ms 飆到 5s、整體 container 還有 70% RU 剩餘卻全 throttle。Cosmos DB partition key 設計是 &lt;em>selection 階段就要決定的硬約束&lt;/em>、不是「先選錯再改」可承擔的風險 — 這個不可逆性跟 MongoDB（&lt;code>reshardCollection&lt;/code> 線上完成）跟 DynamoDB（建新 table backfill）形成關鍵對比。&lt;/p>
&lt;p>本文不是 Cosmos DB overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a>）— 而是 partition key 設計 + 故障演練的深度展開。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（synthetic partition key 強制分散、AR 遊戲玩家位置）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（Black Friday 流量分散 + latency budget 拆解）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>、本篇不重複展開。Partition key 設計是 &lt;em>已選 Cosmos DB 後&lt;/em> 的硬約束議題；若 workload 不適用 Cosmos DB、partition key 設計無法救回 vendor 選錯的不可逆性風險。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 的 <em>logical partition 上限是 10,000 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a>/s + 20 GB storage</em>、partition key 一旦上 production <em>改不了</em>（要 export → recreate container → import）。partition key 選錯的後果是 Black Friday / 上線日 / VIP 用戶把流量壓在少數 partition、p99 latency 從 50ms 飆到 5s、整體 container 還有 70% RU 剩餘卻全 throttle。Cosmos DB partition key 設計是 <em>selection 階段就要決定的硬約束</em>、不是「先選錯再改」可承擔的風險 — 這個不可逆性跟 MongoDB（<code>reshardCollection</code> 線上完成）跟 DynamoDB（建新 table backfill）形成關鍵對比。</p>
<p>本文不是 Cosmos DB overview（請看 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a>）— 而是 partition key 設計 + 故障演練的深度展開。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（synthetic partition key 強制分散、AR 遊戲玩家位置）+ <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（Black Friday 流量分散 + latency budget 拆解）。</p>
<blockquote>
<p><strong>Cosmos DB 適用度前置判讀</strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>、本篇不重複展開。Partition key 設計是 <em>已選 Cosmos DB 後</em> 的硬約束議題；若 workload 不適用 Cosmos DB、partition key 設計無法救回 vendor 選錯的不可逆性風險。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：團隊用 user_id 當 partition key 上 production、平常正常、Black Friday 或 VIP 大客戶上線當天 — application 收到 <code>429 TooManyRequests</code>、p99 從 50ms 飆到 5s；查 portal Metrics 發現 <em>整體 RU 使用率才 30%</em> 但少數 partition 100% 滿、其他 partition 閒置。Cosmos DB 設了 10000 RU/s、實際只能用 2000 就 throttle。</p>
<p>讀者徵兆：</p>
<ul>
<li>「Cosmos DB throughput 我設了 10000 RU、但寫入只有 2000 就 throttle」</li>
<li>「user_id 當 partition key 結果 VIP 用戶全卡在一個 partition」</li>
<li>「Hierarchical partition key 是 2023 後才有的、跟 composite 差在哪」</li>
<li>「partition key 選錯能改嗎」</li>
</ul>
<p>真實壓力：</p>
<ul>
<li>遊戲玩家位置（同伺服器集中同 partition、Minecraft Earth 場景）</li>
<li>IoT 裝置遙測（單一裝置高頻寫入、device_id 不均）</li>
<li>SaaS 多租戶（大客戶 vs 小客戶不均、tenant_id 直接當 partition key 會 hot）</li>
<li>零售商品 catalog（熱門 SKU vs 冷門 SKU 不均）</li>
</ul>
<p>partition key 選錯的隱性成本：要改就是 <em>export → recreate container with new partition key → import</em>、無 in-place migration、production 等於停機窗口 + 全量資料搬移。selection 階段就要決定、不能 phase 後補。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="partition-模型">Partition 模型</h3>
<p>每個 container 有 N 個 <em>physical partition</em>、每個 physical 上有多個 <em>logical partition</em>。同 partition key value 的所有 document 落到同一個 logical partition。Cosmos DB 動態調整 physical partition 數量（透明 split）、但 logical partition 的歸屬 <em>永遠不變</em>（同 PK value 永遠在同 logical）。</p>
<p>9.C11 Minecraft Earth 案例的平台特性段揭露「partition 動態分裂：透明」 — physical partition 的 split 對 application 透明、不需要 application 重連 / 重新 hash。但這個透明 <em>只解 physical partition 容量</em> 問題、<em>不解 logical partition 熱點</em> — logical partition 由 PK value 決定、application 必須自己均勻散佈 value。</p>
<h3 id="logical-partition-上限">Logical partition 上限</h3>
<p>10,000 RU/s + 20 GB storage、達 limit 後即使 container 還有總 RU、單一 partition key 一樣 throttle。這是 <em>硬上限</em>、不是 soft limit、不能調高。</p>
<p>20 GB storage 限制在小用戶通常碰不到、但對「以 tenant_id 為 PK 的大客戶」、storage 也可能先到上限（單一大客戶 50GB 資料、塞不進一個 logical partition）。</p>
<h3 id="partition-key-設計三種模式">Partition key 設計三種模式</h3>
<h4 id="synthetic人工合成-key">Synthetic（人工合成 key）</h4>
<p>機制：用 <code>{userId}_{random_0_to_99}</code> 把單一 user 的寫入散到 100 個 logical partition。application 端 hash userId + random suffix、寫入時組合成 partition key。</p>
<p>副作用：read 需 fan-out 100 個 partition、單一 query RU 暴漲 100x。適合 <em>write-heavy + 不需精準 read</em> 場景（如 IoT telemetry、log）。</p>
<p>9.C11 Minecraft Earth 用 synthetic partition key 強制分散 — AR 遊戲玩家位置寫入頻繁、partition 分散讓單一玩家不會打爆一個 partition。但 case 沒揭露具體 schema、synthetic 細節屬 outline knowledge 推論。</p>
<h4 id="composite多欄位合成">Composite（多欄位合成）</h4>
<p>機制：用 <code>{tenantId}_{deviceId}</code> 兩個欄位合成（<a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 通用樣式）、避免單一 high-cardinality 欄位 hot。適合 <em>多租戶 SaaS</em>、單一 tenant 內又有多個 device、避免大 tenant 把所有寫入集中。</p>
<p>副作用：read 必須帶兩個欄位、否則 cross-partition query；query API 設計要強制帶 tenant + device。</p>
<h4 id="hierarchical2023-原生支援">Hierarchical（2023+ 原生支援）</h4>
<p>機制：原生支援多層 key（最多 3 層、如 <code>tenantId / deviceId / sessionId</code>）、不用手動合成；query 可指定前綴做 partition scope query（如「拿 tenant X 的所有 device」單一 partition scope）。</p>
<p>適合：多層業務 hierarchy 場景（tenant → user → session、organization → team → project）。比 composite 優勢是 <em>支援 prefix query</em>、composite key 只能完整匹配。</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">az cosmosdb sql container create <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --partition-key-paths <span class="s2">&#34;/tenantId&#34;</span> <span class="s2">&#34;/deviceId&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --partition-key-kind <span class="s2">&#34;MultiHash&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  ...</span></span></code></pre></div><p>設計順序要從 <em>低 cardinality</em> 到 <em>高 cardinality</em>（tenant 少、device 多、session 最多）— 反序會讓 prefix query 無意義。</p>
<h3 id="跟其他-vendor-的可逆性對照本章合成-frame">跟其他 vendor 的可逆性對照（本章合成 frame）</h3>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（Cosmos DB 屬不可改、不可逆性最高）、跨 vendor 對照 SSoT 主寫位置在 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸</a> + 對應的<a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段</a>。本段聚焦 Cosmos DB 不可改特性對 selection 階段 access pattern audit 嚴格度的影響、不重複展開三 vendor 全光譜比較。</p></blockquote>
<p>partition / shard key 的可逆性在 vendor 間差異懸殊：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>可逆性</th>
          <th>機制</th>
          <th>工程成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB</td>
          <td>可改（4.4+ <code>reshardCollection</code>）</td>
          <td>線上完成、cluster 內搬移</td>
          <td>高、但 in-place</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>可改</td>
          <td>建新 table、backfill + dual-write 切換</td>
          <td>中、要 backfill</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td><em>不可改</em></td>
          <td>必須 export → recreate container → import</td>
          <td>最高、需停機窗口</td>
      </tr>
  </tbody>
</table>
<p><strong>對照表是本章合成 frame、9.C11 Minecraft Earth 沒直接揭露此對比、是從 outline knowledge 跟 MongoDB shard-key-selection 對照得出</strong>。引用時必須明示：Cosmos DB partition key 不可改是 <em>設計選型的硬約束</em>、不是「先選錯再改」可承擔的風險 — 這個約束直接決定 selection 階段的 partition key audit 嚴格度該多高。</p>
<p>對 selection 的意義：若團隊對 access pattern 不確定、不能用「先上 Cosmos DB 再說、不行再改」的心態、要先用 MongoDB / DynamoDB 試 access pattern、確定後再評估 Cosmos DB。</p>
<h3 id="跟-dynamodb-partition-key-對比">跟 DynamoDB partition key 對比</h3>
<ul>
<li><strong>DynamoDB</strong>：partition key + optional sort key、無 hierarchical key、adaptive capacity 自動補 hot partition（部分減緩、不完全解決）</li>
<li><strong>Cosmos DB</strong>：hierarchical key 是 <em>原生功能</em>、不靠 adaptive；單 logical partition 限制嚴格、必須前期設計</li>
</ul>
<p>Cosmos DB 的 <em>硬上限 + 不可逆性</em> 跟 DynamoDB 的 <em>adaptive + 可遷移</em> 是兩種設計哲學 — selection 時要評估團隊能不能負擔前期 design effort。</p>
<p>對應 knowledge cards：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a> / <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a>。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="設定-partition-key">設定 partition key</h3>
<p>建 container 時指定、<em>無法事後修改</em>：</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">az cosmosdb sql container create <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --account-name mycosmos --database-name mydb <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --name mycontainer --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --partition-key-path <span class="s2">&#34;/userId&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --partition-key-version <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --throughput <span class="m">10000</span></span></span></code></pre></div><h3 id="hierarchical-key-設定c-sdk-範例">Hierarchical key 設定（C# SDK 範例）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kt">var</span> <span class="n">properties</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ContainerProperties</span><span class="p">(</span><span class="s">&#34;mycontainer&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="s">&#34;/tenantId&#34;</span><span class="p">,</span> <span class="s">&#34;/deviceId&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">properties</span><span class="p">.</span><span class="n">PartitionKeyDefinitionVersion</span> <span class="p">=</span> <span class="n">PartitionKeyDefinitionVersion</span><span class="p">.</span><span class="n">V2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kt">var</span> <span class="n">container</span> <span class="p">=</span> <span class="k">await</span> <span class="n">database</span><span class="p">.</span><span class="n">CreateContainerAsync</span><span class="p">(</span><span class="n">properties</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 寫入時帶完整 hierarchical key</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kt">var</span> <span class="n">pk</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PartitionKeyBuilder</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;tenant-123&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;device-456&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">.</span><span class="n">Build</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">CreateItemAsync</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">pk</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="c1">// Prefix query：拿 tenant-123 的所有 device</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kt">var</span> <span class="n">prefixPk</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PartitionKeyBuilder</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;tenant-123&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">.</span><span class="n">Build</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kt">var</span> <span class="n">iterator</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="n">GetItemQueryIterator</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="s">&#34;SELECT * FROM c&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">requestOptions</span><span class="p">:</span> <span class="k">new</span> <span class="n">QueryRequestOptions</span> <span class="p">{</span> <span class="n">PartitionKey</span> <span class="p">=</span> <span class="n">prefixPk</span> <span class="p">});</span></span></span></code></pre></div><h3 id="synthetic-key-寫入">Synthetic key 寫入</h3>
<p>application 端 hash + random suffix、寫入時組合成 partition key：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">hashlib</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">random</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">def</span> <span class="nf">get_partition_key</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">fanout</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">suffix</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">fanout</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&#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"># Read 時 fan-out 所有可能 suffix</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">read_user_data</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">fanout</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">for</span> <span class="n">suffix</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">fanout</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">results</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">query_partition</span><span class="p">(</span><span class="n">pk</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="n">results</span></span></span></code></pre></div><p>注意 fanout 的 trade-off：fanout = 100 等於 read 成本 × 100；要在 <em>write 分散</em> 跟 <em>read 效率</em> 間平衡、通常 fanout 10-100 之間。</p>
<h3 id="查-partition-分布">查 partition 分布</h3>
<p>portal Metrics &gt; Storage by partition key、看分布是否均勻；或用 <code>SELECT * FROM c WHERE c.partitionKey = &quot;specific-value&quot;</code> query + diagnostic log 看 RU 分布。</p>
<h3 id="驗證點">驗證點</h3>
<ul>
<li>每個 logical partition 的 RU 消耗 &lt; 80% limit（給 burst 留 20% buffer）</li>
<li>單一 partition 的 storage &lt; 16 GB（給成長預留 4 GB buffer）</li>
<li>p99 latency 在 hot partition 不退化</li>
<li>跨 partition query 比例 &lt; 5%（多數 query 帶 partition key 條件）</li>
</ul>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>partition key 選錯只能 export → recreate container with new partition key → import；無 in-place migration、生產系統等於停機窗口 + dual-write cutover 流程。對應 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 的遷移模型。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1user_id-直接當-partition-key">Failure 1：user_id 直接當 partition key</h3>
<p>高活躍用戶（VIP / bot / 大客戶）超過 10,000 RU/s、全 container 被 throttle；徵兆是 <code>429 TooManyRequests</code> 集中在少數 partition、整體 RU 利用率才 30%。</p>
<p>修：</p>
<ul>
<li>短期：把 hot user 拉到獨立 container（合規上有時要這樣做、把 VIP / 企業客戶獨立治理）</li>
<li>長期：換 synthetic key（user_id + random suffix）或 composite key（tenant + user）</li>
<li>selection 階段 audit：access pattern 是否會有「少數 user 主導流量」現象（B2B SaaS、VIP 用戶都有）</li>
</ul>
<h3 id="failure-2時間當-partition-key">Failure 2：時間當 partition key</h3>
<p><code>/createdDate</code> 或 <code>/yyyyMM</code>、新資料全寫入最新 partition、舊 partition 冷掉浪費 — write hot + read 不均。徵兆：最新月份 partition throttle、其他月份 partition 閒置。</p>
<p>修：時間 + 業務維度組合（如 <code>/yyyyMM-userId</code>、<code>/userId-yyyy</code>）、避免純時間維度。time-series workload 該考慮 Azure Time Series Insights 或 Cosmos DB time-series 專屬模式。</p>
<h3 id="failure-3synthetic-key-沒考慮-read-路徑">Failure 3：Synthetic key 沒考慮 read 路徑</h3>
<p>寫入散開但 read 必須 fan-out 100 partition、單一 query RU 暴漲 100x。徵兆：read 成本遠高於估算、<code>RetrievedDocumentCount</code> 跟 <code>OutputDocumentCount</code> 比例 &gt; 50。</p>
<p>修：</p>
<ul>
<li>用 Change Feed 把投影預先寫到 read-optimized container（partition key 用 user_id）、read 走投影</li>
<li>或調 fanout（10 而非 100）、平衡 write 分散跟 read 成本</li>
<li>或重新評估「真的需要 synthetic key 嗎」 — 多數場景用 composite 就夠</li>
</ul>
<h3 id="failure-4hierarchical-key-設計順序顛倒">Failure 4：Hierarchical key 設計順序顛倒</h3>
<p>把 high-cardinality 放第一層、prefix query 變得無意義。如 <code>/userId/tenantId</code> 而非 <code>/tenantId/userId</code> — 想拿「tenant X 的所有 user」變成 cross-partition query、完全失去 hierarchical 優勢。</p>
<p>修：設計順序從 <em>低 cardinality</em> 到 <em>高 cardinality</em>、跟業務 query pattern 對齊。建 container 前畫 access pattern 表、列每個 query 的 hierarchy 順序、再決定 partition key path。</p>
<h3 id="failure-5不監控-partition-分布">Failure 5：不監控 partition 分布</h3>
<p>partition skew 累積幾個月、直到事故才發現。production 上線初期 access pattern 還不明顯、半年後 VIP 客戶開始用、partition 失衡 — 來不及改 partition key、只能在 throttle 中應急。</p>
<p>修：上線第一天就設 alert：</p>
<ul>
<li>單 partition RU 利用 &gt; 80% 持續 5 min</li>
<li>單 partition storage &gt; 16 GB</li>
<li>429 error rate 突增</li>
</ul>
<p>每週看 portal Insights &gt; Top contributors &gt; Partition key range、early detect skew。</p>
<h3 id="failure-6container-之間-partition-設計不一致">Failure 6：Container 之間 partition 設計不一致</h3>
<p>跨 container query 需要 fan-out、cross-partition query 成本爆炸。常見 anti-pattern：訂單 container 用 user_id、商品 container 用 product_id、join 訂單 + 商品時兩邊都 cross-partition。</p>
<p>修：跨 container 的 access pattern 在 selection 階段就要設計、不能各 container 各自決定 partition key。或者用 Change Feed 把跨 container 資料合成 single container 的 materialized view。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>PhysicalPartitionThroughputInfo</code>、<code>NormalizedRUConsumption</code> per partition、<code>StorageDistributionPerPartition</code></li>
<li>Hot partition 偵測：portal Insights &gt; Top contributors &gt; Partition key range</li>
<li>容量估算公式：peak RU per partition × partition 數 + 預留 buffer（一般 30%）= total RU/s</li>
<li>回 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>：把 partition skew 當 saturation signal</li>
<li>Alert：單 partition RU 利用 &gt; 80% 持續 5 min；429 error rate 突增</li>
</ul>
<h3 id="latency-budget-拆解vendor-sla-vs-end-to-end-實測">Latency budget 拆解：vendor SLA vs end-to-end 實測</h3>
<p>9.C21 ASOS 觀察「48ms 平均響應 = 全球分散下 Cosmos DB 的代表性數字」段揭露：48ms 包含 <em>網路 + DB + 應用層</em>、DB 本身可能只佔 5-10ms、其他是網路與應用層。引用時不能把 vendor 廣告的 5-10ms p99 當「使用者體驗」、要明示「48ms 是 9.C21 ASOS 案例的 end-to-end 觀察、Cosmos DB 自身可能只佔 5-10ms（case 揭露的拆解推論、不是 case fact）」。</p>
<p>操作上要把 end-to-end latency 拆 budget：</p>
<ul>
<li><strong>DB 端 latency</strong>（vendor SLA、p99 &lt; 10ms 地區內讀、9.C11 揭露）</li>
<li><strong>跨 region replication latency</strong>（multi-region read 從就近 region 拿、不會跨洲、但 cross-region write 不同、見 <a href="../multi-region-write-conflict/">multi-region-write-conflict</a>）</li>
<li><strong>應用層 latency</strong>（serialize / business logic / HTTP overhead）</li>
<li><strong>客戶端網路 latency</strong>（mobile / 跨洲）</li>
</ul>
<p>跟 partition skew 的關係：partition 失衡時即使 vendor 端 SLA 達標、實測 p99 仍會被 hot partition 拉高 — 單一 partition 的 RU consumption 飽和 → 429 retry → 應用層 latency 暴漲 → end-to-end 從 48ms 變 500ms。partition 設計直接影響 end-to-end SLA 鏈路。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（partition skew 直接影響 RU sizing）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（partition 失衡時即使設 Strong 也看到 throttle）、<a href="../multi-region-write-conflict/">multi-region-write-conflict</a>（partition key 影響 conflict 分布）、<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a>（MongoDB shard key → Cosmos DB partition key 翻譯）</li>
<li>跟 <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 vendor</a> 對比：partition key + adaptive capacity vs 不可逆 + hierarchical</li>
<li>跟 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a> 對比：<code>reshardCollection</code> 可逆 vs 不可逆</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>Knowledge cards：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a> / <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a></li>
<li>Anti-recommendation：小流量（&lt; 1000 RU/s 預期）不必過度設計 synthetic key、Cosmos DB autoscale + 簡單 partition key 即可；過度 design 比 under-design 更常見的成本浪費</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 partition key design backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — synthetic partition key 主案例</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — latency budget 拆解 + 全球零售流量分散</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> / <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/partitioning-overview">Cosmos DB partitioning</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/hierarchical-partition-keys">Hierarchical partition keys</a></li>
</ul>
]]></content:encoded></item><item><title>3.C60 Spotify：Event Delivery 從 Kafka 遷到 Pub/Sub</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-event-delivery-platform/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-event-delivery-platform/</guid><description>&lt;p>Spotify 把全球 event delivery 從 Kafka 遷到 Cloud Pub/Sub 的案例揭露了大規模 pull subscription 的工程現實 — at-least-once 語意意味著應用層去重不可省。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Spotify 的 Event Delivery 系統負責把所有使用者行為事件（播放、搜尋、推薦互動、廣告曝光）從客戶端經由資料管線送到下游消費者。事件是推薦引擎、A/B test、廣告計費跟 analytics 的核心輸入。&lt;/p>
&lt;p>遷移到 GCP Pub/Sub 後的系統規模：每個 event type 一個 topic、~15 個 microservice 跑在 ~2500 VM 上、Q1 2019 高峰 8M events/sec、每日 350 TB raw event 流量。遷出 Kafka 的動機跟技術評估見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20 Spotify 遷出 Kafka（反例）&lt;/a>。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="at-least-once-語意下的重複">At-least-once 語意下的重複&lt;/h3>
&lt;p>Cloud Pub/Sub（早期版本）提供 at-least-once delivery — 同一筆訊息可能被 deliver 多次。在每日 350 TB 的流量下，「偶爾重複」的頻率足以影響 analytics 數據跟廣告計費的準確性。&lt;/p>
&lt;p>Pub/Sub 的重複來源有兩個：ack deadline 到期前 consumer 還沒處理完、訊息被重新 deliver 給其他 consumer；以及 Pub/Sub backend 的內部 redelivery（罕見但非零）。&lt;/p>
&lt;h3 id="pull-subscription-的流控">Pull subscription 的流控&lt;/h3>
&lt;p>Pull subscription 讓 consumer 主動從 Pub/Sub 拉取訊息（vs push subscription 由 Pub/Sub 推送到 HTTP endpoint）。Pull 的好處是 consumer 可以控制自己的消費速度，避免被推送壓垮。&lt;/p>
&lt;p>大規模 pull subscription 的挑戰在於流控的精細度 — 每個 consumer VM 要設定合理的 maxOutstandingMessages 跟 maxOutstandingBytes，太大會讓 consumer 記憶體不足、太小會浪費 Pub/Sub 的吞吐能力。Spotify 的 2500 VM 各自獨立做 pull，需要在 fleet 級別保持流控的一致性。&lt;/p>
&lt;h3 id="每個-event-type-一個-topic-的治理">每個 event type 一個 topic 的治理&lt;/h3>
&lt;p>Spotify 按 event type 建立 topic（例如 &lt;code>play-event&lt;/code>、&lt;code>search-event&lt;/code>、&lt;code>ad-impression&lt;/code>）。Event type 數量成長後，topic 數量跟著增長。每個 topic 需要獨立的 subscription、monitoring、ack deadline 設定跟 retention policy。&lt;/p>
&lt;p>Topic 治理的工程問題是「誰 own 這個 topic、schema 變更怎麼協調、retention 該設多久」。Spotify 自建了 event delivery 平台層（Event Delivery Platform）來管理 topic lifecycle — 包括 topic 建立 / 刪除的 self-service API、schema registry、consumer group 管理。&lt;/p>
&lt;h2 id="解法與取捨">解法與取捨&lt;/h2>
&lt;h3 id="自建-deduplication-層">自建 deduplication 層&lt;/h3>
&lt;p>Spotify 在 consumer 端自建去重機制。每筆 event 帶 unique event ID，consumer 在處理前查 dedup store（記憶體 + 外部 cache）確認是否已處理過。已處理的 event 直接 ack、跳過處理邏輯。&lt;/p></description><content:encoded><![CDATA[<p>Spotify 把全球 event delivery 從 Kafka 遷到 Cloud Pub/Sub 的案例揭露了大規模 pull subscription 的工程現實 — at-least-once 語意意味著應用層去重不可省。</p>
<h2 id="業務背景">業務背景</h2>
<p>Spotify 的 Event Delivery 系統負責把所有使用者行為事件（播放、搜尋、推薦互動、廣告曝光）從客戶端經由資料管線送到下游消費者。事件是推薦引擎、A/B test、廣告計費跟 analytics 的核心輸入。</p>
<p>遷移到 GCP Pub/Sub 後的系統規模：每個 event type 一個 topic、~15 個 microservice 跑在 ~2500 VM 上、Q1 2019 高峰 8M events/sec、每日 350 TB raw event 流量。遷出 Kafka 的動機跟技術評估見 <a href="/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20 Spotify 遷出 Kafka（反例）</a>。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="at-least-once-語意下的重複">At-least-once 語意下的重複</h3>
<p>Cloud Pub/Sub（早期版本）提供 at-least-once delivery — 同一筆訊息可能被 deliver 多次。在每日 350 TB 的流量下，「偶爾重複」的頻率足以影響 analytics 數據跟廣告計費的準確性。</p>
<p>Pub/Sub 的重複來源有兩個：ack deadline 到期前 consumer 還沒處理完、訊息被重新 deliver 給其他 consumer；以及 Pub/Sub backend 的內部 redelivery（罕見但非零）。</p>
<h3 id="pull-subscription-的流控">Pull subscription 的流控</h3>
<p>Pull subscription 讓 consumer 主動從 Pub/Sub 拉取訊息（vs push subscription 由 Pub/Sub 推送到 HTTP endpoint）。Pull 的好處是 consumer 可以控制自己的消費速度，避免被推送壓垮。</p>
<p>大規模 pull subscription 的挑戰在於流控的精細度 — 每個 consumer VM 要設定合理的 maxOutstandingMessages 跟 maxOutstandingBytes，太大會讓 consumer 記憶體不足、太小會浪費 Pub/Sub 的吞吐能力。Spotify 的 2500 VM 各自獨立做 pull，需要在 fleet 級別保持流控的一致性。</p>
<h3 id="每個-event-type-一個-topic-的治理">每個 event type 一個 topic 的治理</h3>
<p>Spotify 按 event type 建立 topic（例如 <code>play-event</code>、<code>search-event</code>、<code>ad-impression</code>）。Event type 數量成長後，topic 數量跟著增長。每個 topic 需要獨立的 subscription、monitoring、ack deadline 設定跟 retention policy。</p>
<p>Topic 治理的工程問題是「誰 own 這個 topic、schema 變更怎麼協調、retention 該設多久」。Spotify 自建了 event delivery 平台層（Event Delivery Platform）來管理 topic lifecycle — 包括 topic 建立 / 刪除的 self-service API、schema registry、consumer group 管理。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<h3 id="自建-deduplication-層">自建 deduplication 層</h3>
<p>Spotify 在 consumer 端自建去重機制。每筆 event 帶 unique event ID，consumer 在處理前查 dedup store（記憶體 + 外部 cache）確認是否已處理過。已處理的 event 直接 ack、跳過處理邏輯。</p>
<p>Dedup store 的挑戰是大小跟 TTL — 要記住多久以前的 event ID 才夠。TTL 太短會漏掉 late redelivery（Pub/Sub 在 ack deadline 之後才重新 deliver）、TTL 太長 dedup store 太大。Spotify 用滑動視窗（retention 跟 ack deadline 的倍數）設定 TTL。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Pub/Sub + 自建 dedup</th>
          <th>自管 Kafka 0.8+</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>運維成本</td>
          <td>低（Pub/Sub 全託管）</td>
          <td>高（自管 broker × 多 region）</td>
      </tr>
      <tr>
          <td>語意保證</td>
          <td>At-least-once + 應用層 dedup</td>
          <td>At-least-once（idempotent 0.11+）</td>
      </tr>
      <tr>
          <td>跨 region replication</td>
          <td>原生支援</td>
          <td>需要 MirrorMaker 或自建</td>
      </tr>
      <tr>
          <td>流控精細度</td>
          <td>Pull subscription 可控</td>
          <td>Consumer group 自動分配</td>
      </tr>
      <tr>
          <td>Topic 治理</td>
          <td>需要自建平台層</td>
          <td>Kafka 生態工具（Confluent 等）</td>
      </tr>
      <tr>
          <td>Dedup 成本</td>
          <td>額外的 cache / store 成本</td>
          <td>Idempotent producer 減少需求</td>
      </tr>
  </tbody>
</table>
<p>自建 dedup 的成本是 Spotify 選 Pub/Sub 的額外付出。這個代價在託管方案的運維節省面前被接受 — 維護一個 dedup cache 的成本遠低於維護跨 5 個 datacenter 的 Kafka broker fleet。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><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">Pub/Sub vendor 頁</a>：push vs pull subscription、ack deadline、ordering 跟 DLT 的進階主題</li>
<li><a href="/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20 Spotify 遷出 Kafka</a>：遷出 Kafka 的動機跟決策判準</li>
<li><a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing recovery semantics</a>：at-least-once 語意下的 dedup 策略</li>
<li><a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract replay boundary</a>：event schema 跟 topic lifecycle 的治理</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>使用 GCP Pub/Sub 且下游消費者偶爾處理到重複事件</li>
<li>Pull subscription 的 consumer 記憶體使用不穩定、maxOutstandingMessages 設定不合理</li>
<li>Topic 數量持續增長但缺少統一的 lifecycle 管理</li>
<li>從自管 Kafka 遷移到 GCP Pub/Sub 的評估階段</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.atspotify.com/2019/11/spotifys-event-delivery-life-in-the-cloud">Spotify&rsquo;s Event Delivery — Life in the Cloud</a></li>
</ul>
]]></content:encoded></item><item><title>3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/</guid><description>&lt;p>這個案例的核心責任是說明「subscription backlog 不等於 consumer healthy」、autoscaling 跟 ack deadline 的耦合風險。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>下游 Cloud Storage export 失敗時、consumer 不 ack 仍持續消耗 CPU 處理同批訊息、造成 autoscaling 把 CPU 越拉越高的反效果；解法是 exponential backoff 抑制 CPU 消耗。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「Subscription backlog 不等於 consumer healthy」— 訊息未 ack 累積跟 autoscaling 的耦合風險。揭露 autoscale signal 該看「處理成功率」而非「CPU + backlog」。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Ack deadline / autoscaling signal 設計。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.atspotify.com/2017/11/autoscaling-pub-sub-consumers">Autoscaling Pub/Sub Consumers&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「subscription backlog 不等於 consumer healthy」、autoscaling 跟 ack deadline 的耦合風險。</p>
<h2 id="觀察">觀察</h2>
<p>下游 Cloud Storage export 失敗時、consumer 不 ack 仍持續消耗 CPU 處理同批訊息、造成 autoscaling 把 CPU 越拉越高的反效果；解法是 exponential backoff 抑制 CPU 消耗。</p>
<h2 id="判讀">判讀</h2>
<p>「Subscription backlog 不等於 consumer healthy」— 訊息未 ack 累積跟 autoscaling 的耦合風險。揭露 autoscale signal 該看「處理成功率」而非「CPU + backlog」。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Ack deadline / autoscaling signal 設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.atspotify.com/2017/11/autoscaling-pub-sub-consumers">Autoscaling Pub/Sub Consumers</a></li>
</ul>
]]></content:encoded></item><item><title>Pinterest：快取可靠性與容量驚奇治理</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/</guid><description>&lt;p>Pinterest 案例的核心責任是處理快取層造成的容量驚奇。快取命中率下滑會在短時間放大到資料層與下游依賴，因此需要預先設計退化與重建節奏。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>流量高峰或快取失溫時，回源壓力會瞬間上升。若沒有緩衝機制與重建策略，系統容易進入連鎖退化。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cache headroom&lt;/td>
 &lt;td>命中率下滑能承受多久&lt;/td>
 &lt;td>容量緩衝&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Graceful degradation&lt;/td>
 &lt;td>快取失效時如何降級&lt;/td>
 &lt;td>服務連續性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rewarm strategy&lt;/td>
 &lt;td>熱資料如何有序回填&lt;/td>
 &lt;td>恢復節奏&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>cache hit ratio drop&lt;/td>
 &lt;td>是否進入危險區&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback latency&lt;/td>
 &lt;td>降級路徑是否可接受&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rewarm backlog&lt;/td>
 &lt;td>回填是否可收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2&lt;/a> 模擬命中率崩落，再把恢復證據寫入 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Pinterest 案例的核心責任是處理快取層造成的容量驚奇。快取命中率下滑會在短時間放大到資料層與下游依賴，因此需要預先設計退化與重建節奏。</p>
<h2 id="問題場景">問題場景</h2>
<p>流量高峰或快取失溫時，回源壓力會瞬間上升。若沒有緩衝機制與重建策略，系統容易進入連鎖退化。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cache headroom</td>
          <td>命中率下滑能承受多久</td>
          <td>容量緩衝</td>
      </tr>
      <tr>
          <td>Graceful degradation</td>
          <td>快取失效時如何降級</td>
          <td>服務連續性</td>
      </tr>
      <tr>
          <td>Rewarm strategy</td>
          <td>熱資料如何有序回填</td>
          <td>恢復節奏</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cache hit ratio drop</td>
          <td>是否進入危險區</td>
          <td><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a></td>
      </tr>
      <tr>
          <td>fallback latency</td>
          <td>降級路徑是否可接受</td>
          <td><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22</a></td>
      </tr>
      <tr>
          <td>rewarm backlog</td>
          <td>回填是否可收斂</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>先在 <a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">6.2</a> 模擬命中率崩落，再把恢復證據寫入 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a>。</p>
]]></content:encoded></item><item><title>Reddit：2023 Kubernetes 升級事故</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/</guid><description>&lt;p>這起案例的核心責任是把平台升級納入事故流程。升級事件不是純部署問題，會直接影響事件分級、回退與通訊節奏。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>post-upgrade error burst&lt;/td>
 &lt;td>變更後退化是否快速擴散&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback decision delay&lt;/td>
 &lt;td>回退決策是否過慢&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>service recovery slope&lt;/td>
 &lt;td>恢復是否分批收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「平台升級變更」與「事故分級決策」要共用同一套欄位。主要風險是把升級當例行操作，延後回退判斷。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>把升級變更與事故決策共用欄位，並在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8&lt;/a> 加入升級專屬 gate。事故收斂後回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是把平台升級納入事故流程。升級事件不是純部署問題，會直接影響事件分級、回退與通訊節奏。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>post-upgrade error burst</td>
          <td>變更後退化是否快速擴散</td>
          <td><a href="/blog/backend/08-incident-response/incident-severity-trigger/" data-link-title="8.1 事故分級與啟動條件" data-link-desc="建立統一分級標準與事故啟動門檻">8.1</a></td>
      </tr>
      <tr>
          <td>rollback decision delay</td>
          <td>回退決策是否過慢</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>service recovery slope</td>
          <td>恢復是否分批收斂</td>
          <td><a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「平台升級變更」與「事故分級決策」要共用同一套欄位。主要風險是把升級當例行操作，延後回退判斷。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>把升級變更與事故決策共用欄位，並在 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 加入升級專屬 gate。事故收斂後回寫 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a>。</p>
]]></content:encoded></item><item><title>3.C62 Spotify：Pub/Sub → GCS reliable export</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-cloud-storage-export/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-cloud-storage-export/</guid><description>&lt;p>這個案例的核心責任是說明 ack 是 end-to-end commit 信號、不是 buffer-flush 信號。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Consumer 只在下游 Completionist 回 200 OK 才 ack 回 Pub/Sub、並用「Oldest Unacknowledged Message」metric 判斷 hourly bucket 何時可安全關閉；ack semantics 直接綁定下游 commit。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>ack 是 end-to-end commit 信號、不是 buffer-flush 信號。揭露為什麼後來原生 GCS subscription 有價值（Spotify 早期沒有原生、自建管線）。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Ack deadline / Cloud Storage subscription（早期無原生、自建對照）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.atspotify.com/2017/04/reliable-export-of-cloud-pubsub-streams-to-cloud-storage">Reliable Export of Cloud Pub/Sub Streams to Cloud Storage&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 ack 是 end-to-end commit 信號、不是 buffer-flush 信號。</p>
<h2 id="觀察">觀察</h2>
<p>Consumer 只在下游 Completionist 回 200 OK 才 ack 回 Pub/Sub、並用「Oldest Unacknowledged Message」metric 判斷 hourly bucket 何時可安全關閉；ack semantics 直接綁定下游 commit。</p>
<h2 id="判讀">判讀</h2>
<p>ack 是 end-to-end commit 信號、不是 buffer-flush 信號。揭露為什麼後來原生 GCS subscription 有價值（Spotify 早期沒有原生、自建管線）。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Ack deadline / Cloud Storage subscription（早期無原生、自建對照）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 event contract / replay boundary</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.atspotify.com/2017/04/reliable-export-of-cloud-pubsub-streams-to-cloud-storage">Reliable Export of Cloud Pub/Sub Streams to Cloud Storage</a></li>
</ul>
]]></content:encoded></item><item><title>3.C63 Mercari Actionable History：ack deadline 是 batch-level</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/</guid><description>&lt;p>這個案例的核心責任是揭露 Pub/Sub client lib 「ack deadline 是 batch-level」這個真實的工程陷阱。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Merpay 支付流水帳服務用 Pub/Sub 做 async messaging、靠 nack 控制處理順序；踩到「ack deadline 是整批 batch 而非單訊息」、acked 訊息會跟同 batch 其他 expired/nacked 訊息一起 redeliver 的設計細節。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「ack deadline 是 batch-level」是 Pub/Sub client lib 真實的工程陷阱；idempotency 是處理 duplicate 的必要設計、新出的 exactly-once delivery 才有機會降低重複量。揭露 client lib 的批次語意會「污染」單訊息 ack。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Ack deadline / Push vs Pull / Ordering key（exactly-once / ordering 章節）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.mercari.com/en/blog/entry/20221212-merpay-actionable-history-displaying-millions-of-payments-with-lightning-speed/">Merpay Actionable History: Displaying Millions of Payments with Lightning Speed&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是揭露 Pub/Sub client lib 「ack deadline 是 batch-level」這個真實的工程陷阱。</p>
<h2 id="觀察">觀察</h2>
<p>Merpay 支付流水帳服務用 Pub/Sub 做 async messaging、靠 nack 控制處理順序；踩到「ack deadline 是整批 batch 而非單訊息」、acked 訊息會跟同 batch 其他 expired/nacked 訊息一起 redeliver 的設計細節。</p>
<h2 id="判讀">判讀</h2>
<p>「ack deadline 是 batch-level」是 Pub/Sub client lib 真實的工程陷阱；idempotency 是處理 duplicate 的必要設計、新出的 exactly-once delivery 才有機會降低重複量。揭露 client lib 的批次語意會「污染」單訊息 ack。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Ack deadline / Push vs Pull / Ordering key（exactly-once / ordering 章節）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例：語義誤配</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.mercari.com/en/blog/entry/20221212-merpay-actionable-history-displaying-millions-of-payments-with-lightning-speed/">Merpay Actionable History: Displaying Millions of Payments with Lightning Speed</a></li>
</ul>
]]></content:encoded></item><item><title>3.C64 Mercari Item Feed：DLT 防 poison message 阻塞</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/</guid><description>&lt;p>這個案例的核心責任是說明 DLT 在防止 poison message 阻塞 pipeline 的角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>商品 feed 同步用 pull subscription + 自家 batch requester、成功時 ack 整批、失敗時 nack 讓 Pub/Sub 重送；重試多次仍失敗則送 Dead-letter topic、後續訊息優先處理；topic 同時當突發流量的緩衝。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>直接示範 DLT 在防止 poison message 阻塞 pipeline 的角色、以及把 topic 當 load-leveling queue 的設計。揭露「topic = buffer + dispatch」雙重角色。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Dead-letter topic / Push vs Pull subscription。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/" data-link-title="3.C56 PostNL EBE：完整 DLQ &amp;#43; retention &amp;#43; redrive 設計" data-link-desc="PostNL 物流每天 1000 萬訊息、每 producer/consumer 隔離 stack、24h 內 100 次 retry、final DLQ 可 consumer redrive。">3.C56 PostNL EBE&lt;/a>（DLQ 設計對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.mercari.com/en/blog/entry/20241212-mercaris-seamless-item-feed-integration-bridging-the-gap-between-systems/">Mercari&amp;rsquo;s Seamless Item Feed Integration&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 DLT 在防止 poison message 阻塞 pipeline 的角色。</p>
<h2 id="觀察">觀察</h2>
<p>商品 feed 同步用 pull subscription + 自家 batch requester、成功時 ack 整批、失敗時 nack 讓 Pub/Sub 重送；重試多次仍失敗則送 Dead-letter topic、後續訊息優先處理；topic 同時當突發流量的緩衝。</p>
<h2 id="判讀">判讀</h2>
<p>直接示範 DLT 在防止 poison message 阻塞 pipeline 的角色、以及把 topic 當 load-leveling queue 的設計。揭露「topic = buffer + dispatch」雙重角色。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Dead-letter topic / Push vs Pull subscription。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/" data-link-title="3.C56 PostNL EBE：完整 DLQ &#43; retention &#43; redrive 設計" data-link-desc="PostNL 物流每天 1000 萬訊息、每 producer/consumer 隔離 stack、24h 內 100 次 retry、final DLQ 可 consumer redrive。">3.C56 PostNL EBE</a>（DLQ 設計對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.mercari.com/en/blog/entry/20241212-mercaris-seamless-item-feed-integration-bridging-the-gap-between-systems/">Mercari&rsquo;s Seamless Item Feed Integration</a></li>
</ul>
]]></content:encoded></item><item><title>3.C65 Mercari LINE：Pull subscription 對齊外部 RPS</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/</guid><description>&lt;p>這個案例的核心責任是說明「下游有 RPS 限制」是 Pull subscription 勝過 push 的典型情境。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Braze webhook 進來後轉成 Pub/Sub event、下游 LINE worker pull subscription「精確控制每秒處理訊息數」、因為外部 LINE API 有 RPS 限制。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>push 會把流量瞬間打到 endpoint、pull 可由 consumer 自行 throttle。揭露 push vs pull 不是「實作偏好」、是「下游能否接受 push 衝擊」的判讀。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Push vs Pull subscription。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook buffer&lt;/a>（webhook + buffer 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.mercari.com/en/blog/entry/20231212-flow-control-challenges-in-mercaris-line-integration/">Flow Control Challenges in Mercari&amp;rsquo;s LINE Integration&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「下游有 RPS 限制」是 Pull subscription 勝過 push 的典型情境。</p>
<h2 id="觀察">觀察</h2>
<p>Braze webhook 進來後轉成 Pub/Sub event、下游 LINE worker pull subscription「精確控制每秒處理訊息數」、因為外部 LINE API 有 RPS 限制。</p>
<h2 id="判讀">判讀</h2>
<p>push 會把流量瞬間打到 endpoint、pull 可由 consumer 自行 throttle。揭露 push vs pull 不是「實作偏好」、是「下游能否接受 push 衝擊」的判讀。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Push vs Pull subscription。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook buffer</a>（webhook + buffer 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.mercari.com/en/blog/entry/20231212-flow-control-challenges-in-mercaris-line-integration/">Flow Control Challenges in Mercari&rsquo;s LINE Integration</a></li>
</ul>
]]></content:encoded></item><item><title>3.C66 Mercari B2C：自建 PubSub gRPC Pusher</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-b2c-grpc-pusher/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-b2c-grpc-pusher/</guid><description>&lt;p>這個案例的核心責任是說明原生 push subscription 在特定場景的限制、逼出自建層的工程選擇。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>全球商品同步系統、自建 in-house「PubSub gRPC Pusher」（Pub/Sub 的 gRPC 版 push subscription）解決高吞吐 / 長 job / 彈性 RPS；同時用 message ID 做去重、timestamp 驗證解決重複 + 亂序。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>原生 HTTP push subscription 在「長 job + 高吞吐 + 動態 rate」場景的限制、逼出自建層的工程選擇。揭露 managed broker 的「原生功能」不是所有場景的終點。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Push vs Pull subscription / Ordering key（亂序的 application-level 處理）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.mercari.com/en/blog/entry/20251009-from-local-to-global-building-seamless-b2c-product-integration-at-mercari/">From Local to Global: Building Seamless B2C Product Integration at Mercari&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明原生 push subscription 在特定場景的限制、逼出自建層的工程選擇。</p>
<h2 id="觀察">觀察</h2>
<p>全球商品同步系統、自建 in-house「PubSub gRPC Pusher」（Pub/Sub 的 gRPC 版 push subscription）解決高吞吐 / 長 job / 彈性 RPS；同時用 message ID 做去重、timestamp 驗證解決重複 + 亂序。</p>
<h2 id="判讀">判讀</h2>
<p>原生 HTTP push subscription 在「長 job + 高吞吐 + 動態 rate」場景的限制、逼出自建層的工程選擇。揭露 managed broker 的「原生功能」不是所有場景的終點。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Push vs Pull subscription / Ordering key（亂序的 application-level 處理）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.mercari.com/en/blog/entry/20251009-from-local-to-global-building-seamless-b2c-product-integration-at-mercari/">From Local to Global: Building Seamless B2C Product Integration at Mercari</a></li>
</ul>
]]></content:encoded></item><item><title>3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/</guid><description>&lt;p>這個案例的核心責任是說明大規模遊戲 telemetry 的 ingest backbone 設計。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Pokémon GO frontend 把玩家事件 publish 到 Pub/Sub topic 餵分析 pipeline、再進 BigQuery streaming；高峰 ~1M TPS、Pub/Sub 是 managed service 因此 SRE 維運成本低。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Pub/Sub 在 publisher 突發流量下作為 elastic buffer、下游 BigQuery streaming 是常見組合。揭露「managed service 的 SRE 成本」是大規模遊戲場景的關鍵選型理由。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：BigQuery subscription（原生 BQ subscription 出現前的 Dataflow pattern）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/" data-link-title="3.C68 Wix：Pub/Sub decouple &amp;#43; Dataflow &amp;#43; BQ archive" data-link-desc="Wix App Engine 收 clickstream 進 Pub/Sub、Dataflow 進 Datastore &amp;lt; 100ms、BigQuery 並行存 raw recovery。">3.C68 Wix clickstream&lt;/a>（同類組合）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/how-pok%C3%A9mon-go-scales-millions-requests">How Pokémon GO Scales to Millions of Requests&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明大規模遊戲 telemetry 的 ingest backbone 設計。</p>
<h2 id="觀察">觀察</h2>
<p>Pokémon GO frontend 把玩家事件 publish 到 Pub/Sub topic 餵分析 pipeline、再進 BigQuery streaming；高峰 ~1M TPS、Pub/Sub 是 managed service 因此 SRE 維運成本低。</p>
<h2 id="判讀">判讀</h2>
<p>Pub/Sub 在 publisher 突發流量下作為 elastic buffer、下游 BigQuery streaming 是常見組合。揭露「managed service 的 SRE 成本」是大規模遊戲場景的關鍵選型理由。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：BigQuery subscription（原生 BQ subscription 出現前的 Dataflow pattern）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/" data-link-title="3.C68 Wix：Pub/Sub decouple &#43; Dataflow &#43; BQ archive" data-link-desc="Wix App Engine 收 clickstream 進 Pub/Sub、Dataflow 進 Datastore &lt; 100ms、BigQuery 並行存 raw recovery。">3.C68 Wix clickstream</a>（同類組合）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/topics/developers-practitioners/how-pok%C3%A9mon-go-scales-millions-requests">How Pokémon GO Scales to Millions of Requests</a></li>
</ul>
]]></content:encoded></item><item><title>3.C68 Wix：Pub/Sub decouple + Dataflow + BQ archive</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/</guid><description>&lt;p>這個案例的核心責任是「Pub/Sub buffer + Dataflow stream processor + BQ archive」的教科書組合。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>App Engine 收 clickstream → 進 Cloud Pub/Sub queue、再由 Dataflow streaming 處理進 Datastore、dashboard 端到端 latency &amp;lt; 100ms；BigQuery 並行存 raw data 做 recovery。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「Pub/Sub 當 decouple buffer + Dataflow 當 stream processor + BigQuery 當 raw archive」的 textbook 組合、可作為 BigQuery subscription 出現前的對比 case（為什麼後來原生 BQ subscription 能省掉 Dataflow 中介層）。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：BigQuery subscription / Push vs Pull。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/" data-link-title="3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest" data-link-desc="Pokémon GO frontend publish 玩家事件、~1M TPS、Pub/Sub elastic buffer、下游 BigQuery streaming。">3.C67 Niantic Pokémon GO&lt;/a>（同類組合）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/customers/wix">Wix Customer Story&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是「Pub/Sub buffer + Dataflow stream processor + BQ archive」的教科書組合。</p>
<h2 id="觀察">觀察</h2>
<p>App Engine 收 clickstream → 進 Cloud Pub/Sub queue、再由 Dataflow streaming 處理進 Datastore、dashboard 端到端 latency &lt; 100ms；BigQuery 並行存 raw data 做 recovery。</p>
<h2 id="判讀">判讀</h2>
<p>「Pub/Sub 當 decouple buffer + Dataflow 當 stream processor + BigQuery 當 raw archive」的 textbook 組合、可作為 BigQuery subscription 出現前的對比 case（為什麼後來原生 BQ subscription 能省掉 Dataflow 中介層）。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：BigQuery subscription / Push vs Pull。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/" data-link-title="3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest" data-link-desc="Pokémon GO frontend publish 玩家事件、~1M TPS、Pub/Sub elastic buffer、下游 BigQuery streaming。">3.C67 Niantic Pokémon GO</a>（同類組合）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/customers/wix">Wix Customer Story</a></li>
</ul>
]]></content:encoded></item><item><title>3.C69 Twitter Ad Engagement：把 stream 切成多 topic 做 partition</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-twitter-ad-engagement/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-twitter-ad-engagement/</guid><description>&lt;p>這個案例的核心責任是說明 Pub/Sub 沒有 Kafka-style partition 概念下的應對策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Twitter 把 on-prem 服務的 Avro-formatted 訊息 push 到 Pub/Sub（兩條 stream、較不關鍵但量大的那條 ~80K msg/s 切成 6 個 topic）、下游用 Dataflow + Beam 處理進 Bigtable / BigQuery。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「把單一 high-volume stream 切成多 topic 做 partition」是 Pub/Sub 沒有 Kafka-style partition 概念下的應對策略。揭露 Pub/Sub 跟 Kafka 的選型差異不是 feature parity、是不同的擴張模型。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Pub/Sub 進階主題：Schema enforcement（Avro 是常見 schema 候選）/ Ordering key（topic 切分 vs ordering key 的取捨）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/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">Pub/Sub vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a>（partition 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/data-analytics/modernizing-twitters-ad-engagement-analytics-platform">Modernizing Twitter&amp;rsquo;s Ad Engagement Analytics Platform&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Pub/Sub 沒有 Kafka-style partition 概念下的應對策略。</p>
<h2 id="觀察">觀察</h2>
<p>Twitter 把 on-prem 服務的 Avro-formatted 訊息 push 到 Pub/Sub（兩條 stream、較不關鍵但量大的那條 ~80K msg/s 切成 6 個 topic）、下游用 Dataflow + Beam 處理進 Bigtable / BigQuery。</p>
<h2 id="判讀">判讀</h2>
<p>「把單一 high-volume stream 切成多 topic 做 partition」是 Pub/Sub 沒有 Kafka-style partition 概念下的應對策略。揭露 Pub/Sub 跟 Kafka 的選型差異不是 feature parity、是不同的擴張模型。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Pub/Sub 進階主題：Schema enforcement（Avro 是常見 schema 候選）/ Ordering key（topic 切分 vs ordering key 的取捨）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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">Pub/Sub vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>（partition 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/data-analytics/modernizing-twitters-ad-engagement-analytics-platform">Modernizing Twitter&rsquo;s Ad Engagement Analytics Platform</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Multi-region Table 配置：三種 table locality 的選擇與 latency / 一致性取捨</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/multi-region-table-config/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/multi-region-table-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。本文聚焦 &lt;em>三種 table locality 怎麼選、選錯的 latency / 一致性後果與重配代價&lt;/em>。Schema 怎麼配合 locality 設計（合規 boundary、跨州業務邏輯、Outposts 拓樸）主寫於 &lt;a href="../locality-aware-schema/">locality-aware schema&lt;/a>、survival goal 的存活機制主寫於 &lt;a href="../survival-goals/">survival goals&lt;/a>、本文兩者都 cross-link、不重複展開。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境multi-region-cluster-起來了每張-table-該設哪種-locality">問題情境：multi-region cluster 起來了、每張 table 該設哪種 locality&lt;/h2>
&lt;p>團隊把 CockroachDB 跨 region 拉起來、&lt;code>ALTER DATABASE ... ADD REGION&lt;/code> 也跑完了，接下來面對的是逐張 table 的 locality 決策。這個決策的成本結構很不對稱：設對了，read / write 走本地 leaseholder、latency 貼著單區水準；設錯了，每次寫入或讀取都吃一趟跨 region round trip，p99 從個位數毫秒跳到上百毫秒。&lt;/p>
&lt;p>multi-region table locality 是 &lt;em>把「資料的地理歸屬」跟「讀寫路徑」綁在一起&lt;/em> 的宣告。CockroachDB 提供三種 locality，對應三種「資料屬於誰、誰要快」的業務形狀：&lt;/p>
&lt;ul>
&lt;li>&lt;code>REGIONAL BY TABLE&lt;/code>：整張 table 歸屬單一 region，該 region 讀寫快、其他 region 慢。&lt;/li>
&lt;li>&lt;code>REGIONAL BY ROW&lt;/code>：每一 row 各自歸屬一個 region，row 所在 region 讀寫快。&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code>：資料屬於所有 region，每個 region 本地讀都快，但寫入要跨 region 達成共識。&lt;/li>
&lt;/ul>
&lt;p>讀者進來最常卡的三題：&lt;/p>
&lt;ul>
&lt;li>三種 locality 對應什麼業務形狀、判讀軸是什麼？&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code> 既然每區讀都快，為什麼不全部設 &lt;code>GLOBAL&lt;/code>？&lt;/li>
&lt;li>上線後發現 locality 設錯，重配的代價有多高、能不能無痛改？&lt;/li>
&lt;/ul>
&lt;p>這三題都是 &lt;em>把業務的資料歸屬與讀寫熱點，翻譯成副本拓樸&lt;/em> 的設計決策，語法層面反而簡單。&lt;/p>
&lt;p>問題情境最常見的 trigger：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> 的 60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region。case 揭露一個反直覺判讀 — multi-region 的主要動機是 &lt;em>region failure 0 downtime&lt;/em>、不是降 latency；跨 region quorum 物理上會 &lt;em>增&lt;/em> 寫入 latency。這條判讀直接決定 table locality 怎麼設：當 multi-region 的目的是 survival 而非 latency，把高寫入 table 設成 &lt;code>GLOBAL&lt;/code>（跨區同步寫）就是把成本花在錯的地方。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a> 則提供 row-level 歸屬的 concrete framing：跨 8 州 sportsbook、bet 資料按下注州歸屬、邏輯上仍是一個 cluster。case 觀察段揭露「跨所有 region 一個 logical database」這個拓樸 fact — 也就是 row-level locality 撐起了「合規分州 placement + 單一邏輯 DB」的組合。Hard Rock 的合規驅動與 schema 設計細節在 &lt;a href="../locality-aware-schema/">locality-aware schema&lt;/a> 展開，本文只取「row-level 歸屬」這個 locality 選擇本身。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。本文聚焦 <em>三種 table locality 怎麼選、選錯的 latency / 一致性後果與重配代價</em>。Schema 怎麼配合 locality 設計（合規 boundary、跨州業務邏輯、Outposts 拓樸）主寫於 <a href="../locality-aware-schema/">locality-aware schema</a>、survival goal 的存活機制主寫於 <a href="../survival-goals/">survival goals</a>、本文兩者都 cross-link、不重複展開。</p></blockquote>
<hr>
<h2 id="問題情境multi-region-cluster-起來了每張-table-該設哪種-locality">問題情境：multi-region cluster 起來了、每張 table 該設哪種 locality</h2>
<p>團隊把 CockroachDB 跨 region 拉起來、<code>ALTER DATABASE ... ADD REGION</code> 也跑完了，接下來面對的是逐張 table 的 locality 決策。這個決策的成本結構很不對稱：設對了，read / write 走本地 leaseholder、latency 貼著單區水準；設錯了，每次寫入或讀取都吃一趟跨 region round trip，p99 從個位數毫秒跳到上百毫秒。</p>
<p>multi-region table locality 是 <em>把「資料的地理歸屬」跟「讀寫路徑」綁在一起</em> 的宣告。CockroachDB 提供三種 locality，對應三種「資料屬於誰、誰要快」的業務形狀：</p>
<ul>
<li><code>REGIONAL BY TABLE</code>：整張 table 歸屬單一 region，該 region 讀寫快、其他 region 慢。</li>
<li><code>REGIONAL BY ROW</code>：每一 row 各自歸屬一個 region，row 所在 region 讀寫快。</li>
<li><code>GLOBAL</code>：資料屬於所有 region，每個 region 本地讀都快，但寫入要跨 region 達成共識。</li>
</ul>
<p>讀者進來最常卡的三題：</p>
<ul>
<li>三種 locality 對應什麼業務形狀、判讀軸是什麼？</li>
<li><code>GLOBAL</code> 既然每區讀都快，為什麼不全部設 <code>GLOBAL</code>？</li>
<li>上線後發現 locality 設錯，重配的代價有多高、能不能無痛改？</li>
</ul>
<p>這三題都是 <em>把業務的資料歸屬與讀寫熱點，翻譯成副本拓樸</em> 的設計決策，語法層面反而簡單。</p>
<p>問題情境最常見的 trigger：<a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> 的 60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region。case 揭露一個反直覺判讀 — multi-region 的主要動機是 <em>region failure 0 downtime</em>、不是降 latency；跨 region quorum 物理上會 <em>增</em> 寫入 latency。這條判讀直接決定 table locality 怎麼設：當 multi-region 的目的是 survival 而非 latency，把高寫入 table 設成 <code>GLOBAL</code>（跨區同步寫）就是把成本花在錯的地方。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 則提供 row-level 歸屬的 concrete framing：跨 8 州 sportsbook、bet 資料按下注州歸屬、邏輯上仍是一個 cluster。case 觀察段揭露「跨所有 region 一個 logical database」這個拓樸 fact — 也就是 row-level locality 撐起了「合規分州 placement + 單一邏輯 DB」的組合。Hard Rock 的合規驅動與 schema 設計細節在 <a href="../locality-aware-schema/">locality-aware schema</a> 展開，本文只取「row-level 歸屬」這個 locality 選擇本身。</p>
<h2 id="核心機制三種-locality-的判讀軸--survival-goal-互動">核心機制：三種 locality 的判讀軸 + survival goal 互動</h2>
<p>三種 table locality 的差異，本質是 <em>leaseholder（讀寫入口）跟資料歸屬 region 之間的關係</em>。leaseholder 機制屬前置、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>；本文聚焦三種 locality 把 leaseholder 放在哪、因此誰快誰慢。</p>
<h3 id="判讀軸資料歸屬的顆粒--讀寫熱點分佈">判讀軸：資料歸屬的顆粒 × 讀寫熱點分佈</h3>
<p>選 locality 的第一個判讀軸是 <em>資料歸屬的顆粒</em>：整張 table 屬於一個 region（table 級），還是每 row 各屬一個 region（row 級），還是屬於所有 region（global）。第二個判讀軸是 <em>讀寫熱點落在哪</em>：本地讀為主、本地寫為主、還是全球讀為主。</p>
<table>
  <thead>
      <tr>
          <th>Locality</th>
          <th>資料歸屬顆粒</th>
          <th>Read 快的條件</th>
          <th>Write 快的條件</th>
          <th>對應業務形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>REGIONAL BY TABLE</code></td>
          <td>整張 table 一個 region</td>
          <td>從歸屬 region 讀</td>
          <td>從歸屬 region 寫</td>
          <td>整張表服務單一市場（例：日本訂單表）</td>
      </tr>
      <tr>
          <td><code>REGIONAL BY ROW</code></td>
          <td>每 row 一個 region</td>
          <td>從 row 歸屬 region 讀</td>
          <td>從 row 歸屬 region 寫</td>
          <td>資料跟用戶地理綁定（玩家、帳戶、訂單）</td>
      </tr>
      <tr>
          <td><code>GLOBAL</code></td>
          <td>所有 region 共有</td>
          <td>任何 region 本地讀都快</td>
          <td>沒有「快」的寫（跨區共識）</td>
          <td>reference data（國碼、貨幣、規則表）</td>
      </tr>
  </tbody>
</table>
<p>每一格的判讀都要回到該情境，不能只看表。</p>
<p><code>REGIONAL BY TABLE</code> 適合 <em>整張表的讀寫熱點集中在單一 region</em> 的情況。例如一張只服務日本市場的訂單表，把整張表的 leaseholder 釘在 <code>asia-northeast1</code>，日本端的應用讀寫都走本地 leaseholder，跨區應用偶爾讀則走 follower read 接受 stale。判讀訊號：這張表的寫入請求是否 95% 以上來自同一 region。如果不是，table 級歸屬會讓多數寫入吃跨區延遲。</p>
<p><code>REGIONAL BY ROW</code> 適合 <em>每一 row 跟某個地理位置強綁定、但整張表跨多 region</em> 的情況。玩家帳戶、訂單、下注紀錄都屬於這類 — 每筆資料屬於某個用戶所在 region，但整張表服務所有 region 的用戶。row 透過隱含的 <code>crdb_region</code> 欄位決定歸屬，leaseholder 跟著 row 走。判讀訊號：同一張表的不同 row，讀寫熱點是否分散在不同 region。是的話，row 級歸屬讓每個 row 都貼著自己的用戶。</p>
<p><code>GLOBAL</code> 適合 <em>讀遠多於寫、且每個 region 都要本地快讀</em> 的 reference data。國家代碼、貨幣表、運動賽事 metadata 這類資料變更稀少、但每個 region 的每次查詢都要用到。<code>GLOBAL</code> 讓每個 region 都能本地讀（讀到 closed timestamp 前的一致快照），代價是寫入要跨 region 達成共識。判讀訊號：寫入頻率是否低到「跨區寫的慢可以忽略」。</p>
<h3 id="為什麼不全部設-global">為什麼不全部設 GLOBAL</h3>
<p><code>GLOBAL</code> 的「每區讀都快」看似適合全表套用，但它對 <em>寫入</em> 收取跨 region quorum 的全額成本。<code>GLOBAL</code> table 的讀之所以能本地完成，是因為 CockroachDB 維護一個全球同步的 closed timestamp，讓每個 region 都能安全地本地讀稍早的快照；維護這個 timestamp 的代價是每次寫入都要跟所有 region 協調。</p>
<blockquote>
<p><strong>Scope warning</strong>：<code>GLOBAL</code> table 的跨 region 寫入 p99、<code>REGIONAL BY ROW</code> 的本地寫入 p99、closed timestamp 的傳播間隔等具體數字，屬 vendor 規格與部署拓樸（region 距離、replica 數）的函數，三個 anchor case（DoorDash / Netflix / Hard Rock）都未揭露單一 table 的 latency 數字。本文只給量級判讀（本地 quorum vs 跨洲 quorum 差一到兩個數量級），具體值需 benchmark 自身拓樸並 cross-verify <a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">CockroachDB Table Localities 文件</a>。</p></blockquote>
<p>因此「全部設 <code>GLOBAL</code>」會把所有寫入推上跨 region 路徑，等於放棄了 distributed SQL 把寫入分散到各 region 的核心優勢。<code>GLOBAL</code> 的正確用法是限定在 <em>變更頻率低、全球都要快讀</em> 的 reference data。</p>
<h3 id="survival-goal-怎麼跟-locality-一起決定副本拓樸">Survival goal 怎麼跟 locality 一起決定副本拓樸</h3>
<p>table locality 決定 <em>leaseholder 放哪、讀寫走哪條路徑</em>；survival goal 決定 <em>副本要分佈到幾個 failure domain 才能在故障後存活</em>。兩者一起決定每張 table 的副本拓樸。</p>
<p>survival goal 的存活機制本身（<code>SURVIVE ZONE FAILURE</code> vs <code>SURVIVE REGION FAILURE</code>、怎麼從業務 SLO 倒推、RTO / RPO 怎麼算）是 <a href="../survival-goals/">survival goals</a> 的 SSoT，本文不重複展開。本文只取兩者 <em>互動</em> 的一個關鍵後果：把 <code>SURVIVE REGION FAILURE</code> 套到 <code>REGIONAL BY ROW</code> table 時，每個 region 的 row 不只需要本地 voting replica，還需要在 <em>其他 region</em> 放足夠的 voting replica 才能在整個 region 失效後仍達成 quorum。這會把跨 region 的 voting replica 數量推高，間接增加寫入要協調的範圍。</p>
<p>判讀路線：先依業務的資料歸屬與讀寫熱點選 locality（本文），再依業務的 region failure 容忍度選 survival goal（<a href="../survival-goals/">survival goals</a>），兩者疊加後才得到最終副本拓樸與 latency 結構。</p>
<h2 id="操作流程配置驗證每步檢查生效">操作流程：配置、驗證、每步檢查生效</h2>
<h3 id="第一步確認-database-已加入所有-region">第一步：確認 database 已加入所有 region</h3>
<p>table locality 的前提是 database 已宣告 region。先確認 region 列表正確，再設 table locality。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 database 已有哪些 region、哪個是 primary
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">REGIONS</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：輸出的 region 數量與名稱要對齊實際部署的 region。少一個 region，後面把 table 設成該 region 的 <code>REGIONAL BY TABLE</code> 會直接報錯。</p>
<h3 id="第二步依判讀軸設定每張-table-的-locality">第二步：依判讀軸設定每張 table 的 locality</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 整張表服務單一市場
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_jp</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;asia-northeast1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 資料跟用戶地理綁定
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 低寫入、全球本地讀的 reference data
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">currency_codes</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 確認每張 table 的 locality 設定符合預期
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">   </span><span class="c1">-- locality 子句會出現在輸出尾段</span></span></span></code></pre></div><h3 id="第三步驗證讀寫路徑真的走本地">第三步：驗證讀寫路徑真的走本地</h3>
<p>設了 locality 不代表查詢真的走本地路徑 — 寫入時 row 的 <code>crdb_region</code> 沒設對、或 query 沒帶上對應條件，仍會跨區。用 <code>EXPLAIN ANALYZE</code> 看實際 plan。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 query 是否在 row 歸屬 region 本地完成、有沒有跨 region 拉資料
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：plan 中不應出現大量跨 region 的 distributed scan；<code>REGIONAL BY ROW</code> 的點查應落在 row 歸屬 region 的單一 leaseholder。</p>
<h3 id="第四步驗證副本分佈符合-locality--survival-goal">第四步：驗證副本分佈符合 locality + survival goal</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看每張 table 的 range 副本實際分佈在哪些 region
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">RANGES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：副本分佈要同時滿足 locality（leaseholder 在歸屬 region）跟 survival goal（跨足夠 failure domain）。兩者衝突時，CockroachDB 以 survival goal 為硬約束調整副本數，這會反過來影響 latency — 對應 <a href="../survival-goals/">survival goals</a> 的 latency 暴漲失敗模式。</p>
<h2 id="失敗模式locality-選錯的高代價回退">失敗模式：locality 選錯的高代價回退</h2>
<h3 id="global-套到高寫入-table"><code>GLOBAL</code> 套到高寫入 table</h3>
<p>把高寫入 table（訂單、下注、status 變更）設成 <code>GLOBAL</code>，每筆寫入都跨 region 共識，寫入 p99 結構性暴漲、寫入吞吐被跨區協調卡死。徵兆：CockroachDB Console 的跨 region network traffic 隨寫入量線性成長、寫入 p99 跟 region 距離正相關。</p>
<p>修法：把 table 改成 <code>REGIONAL BY ROW</code>（按用戶歸屬）或 <code>REGIONAL BY TABLE</code>（按市場歸屬）。</p>
<p>Anti-recommendation：reference data 之外的任何 table，預設都不要設 <code>GLOBAL</code>。<code>GLOBAL</code> 的判準是「寫入頻率低到跨區寫的慢可以忽略」，高寫入 workload 直接排除。</p>
<h3 id="regional-by-row-但-row-沒帶正確-crdb_region"><code>REGIONAL BY ROW</code> 但 row 沒帶正確 <code>crdb_region</code></h3>
<p><code>REGIONAL BY ROW</code> 靠 <code>crdb_region</code> 決定 row 歸屬。寫入時沒顯式指定，default 走 <code>gateway_region()</code> — application server 所在 region 變成 row 歸屬。後果是 row 被釘在 application server 那一區，而非用戶所在區，locality 形同失效（甚至在合規場景違反 data residency，見 <a href="../locality-aware-schema/">locality-aware schema</a>）。</p>
<p>修法：寫入時顯式指定 <code>crdb_region</code> 為用戶所在 region，並用 NOT NULL + CHECK constraint 把可選值鎖死。</p>
<h3 id="選錯-locality-的重配代價高代價不可逆情境的回退敘事">選錯 locality 的重配代價（高代價不可逆情境的回退敘事）</h3>
<p>table locality 選錯，重配本身語法上一行就能改（<code>ALTER TABLE ... SET LOCALITY ...</code>），但 <em>資料層面的重配代價高且有持續影響</em>，需要專屬回退計畫，不能比照「改個 config 重啟」對待。</p>
<p>重配 locality 會觸發 CockroachDB 把受影響 range 的副本搬到新拓樸對應的位置。把一張大 table 從 <code>GLOBAL</code> 改成 <code>REGIONAL BY ROW</code>，或從 single region 改成 row-level 跨多 region，意味著大量 range 要 rebalance — 期間跨 region network 流量暴增、leaseholder 反覆換手、p99 持續波動，table 越大、region 越多，rebalance 窗口越長。這是隨資料量延長的背景過程，遠非秒級操作。</p>
<p>更關鍵的是 <code>REGIONAL BY ROW</code> 的 <code>crdb_region</code> 是 <em>資料內容</em>，不只是 metadata。如果原本 row 的歸屬區設錯（例如全部落到 application server 那一區），重配 locality 不會自動把 row 搬到正確的用戶 region — 還要 <em>回填 <code>crdb_region</code> 欄位</em>，這是一次 data migration，不是 schema 變更。合規場景下，錯誤歸屬期間寫入的資料可能已經違反 data residency，回退時要連同合規證據一起盤點。</p>
<p>回退計畫的要素：</p>
<ul>
<li>重配前估算受影響 range 數量與資料量，換算 rebalance 窗口，選低流量時段執行。</li>
<li>重配 <code>REGIONAL BY ROW</code> 時，分開處理「locality 宣告變更」與「<code>crdb_region</code> 回填」兩個動作，回填走分批 update 並監控 contention。</li>
<li>重配期間監控 rebalance queue 與跨 region traffic，設好「波動超過閾值就暫停 rebalance」的 tripwire。</li>
<li>合規場景下，先盤點錯誤歸屬期間的資料是否已違規，再決定回填策略與是否需要合規通報。</li>
</ul>
<p>Anti-recommendation：不要在 production 高峰時段直接對大 table 改 locality 試效果。locality 是「上線前依業務形狀想清楚再設」的決策，不是「線上 A/B 試」的旋鈕。</p>
<h3 id="cross-region-join-跑爆-latency">Cross-region join 跑爆 latency</h3>
<p>兩張 <code>REGIONAL BY ROW</code> table join，若 join key 不保證兩邊 row 在同 region，planner 要跨 region 拉資料，p99 暴漲。</p>
<p>修法：兩張 table 用同一個歸屬 key（如 user_id），讓 join 對應的 row co-locate 在同 region；無法 co-locate 時，對容忍 stale 的查詢改走 follower read。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Cross-region query count</code>：locality 是否生效的直接訊號，數值高代表查詢在跨區拉資料。</li>
<li><code>Leaseholder distribution by region</code>：leaseholder 是否落在資料歸屬 region，不均代表 locality 配置或 <code>crdb_region</code> 有偏。</li>
<li><code>Rebalance queue size</code>：locality 重配 / 副本搬遷期間的進度訊號，持續非零代表 rebalance 未收斂。</li>
<li><code>Cross-region network bytes</code>：<code>GLOBAL</code> table 寫入與 cross-region join 的成本訊號。</li>
</ul>
<h3 id="容量判讀">容量判讀</h3>
<ul>
<li><code>GLOBAL</code> table 的跨區寫入成本 ≈ 寫入 QPS × region 數，region 越多成本越高，所以 <code>GLOBAL</code> 只放低寫入 reference data。</li>
<li><code>REGIONAL BY ROW</code> 的跨區讀成本 ≈ 落到非歸屬 region 的讀 QPS，這部分若高，代表 <code>crdb_region</code> 歸屬與實際讀熱點不一致。</li>
<li>region 數量建議維持精簡 — 每多一個 region，跨區協調與重配窗口都變長。</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：region 數量上限建議、單 range 寫入吞吐量級、closed timestamp 傳播間隔等為 vendor 通用估算，非 case 揭露數字，容量規劃前以 <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">CockroachDB Multi-Region 文件</a> cross-verify 並 benchmark 自身拓樸。</p></blockquote>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 cross-region-bound vs CPU-bound。</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> region count × replica × latency budget。</li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">latency budget 卡</a> 跨 region quorum 預算。</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：schema 怎麼配合 locality 設計 — 合規 boundary、跨州業務邏輯、Outposts 拓樸、<code>crdb_region</code> 作為合規欄位的管理。本文是「三種 locality 怎麼選」、該文是「選好後 schema 怎麼配合」，兩者互補不重複。</li>
<li><a href="../survival-goals/">survival goals</a>：survival goal 的存活機制與 SLO 倒推 — 本文只取「survival goal 與 locality 互動如何影響副本拓樸」這一個交點，存活機制本身以該文為 SSoT。</li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：leaseholder 與 range 機制 — locality 決定 leaseholder 放哪，前置機制在該文。</li>
</ul>
<h3 id="跟-spanner--aurora-對照">跟 Spanner / Aurora 對照</h3>
<p>Spanner 在 GCP region 內做 placement，無 AWS Outposts 等效；Aurora 不支援 row-level locality，跨 region 只能 cluster-per-region + async replication。完整三家 distributed SQL 在 multi-region placement 的選型對比，是 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a> 的 SSoT，本文不重展三方對比。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游 latency / 一致性取捨。</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a>、<a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">follower read 卡</a> — <code>GLOBAL</code> 與跨區讀的一致性語意。</li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region 部署：用 default locality 即可，三種 locality 在單區無差異。</li>
<li>從 PostgreSQL 遷到 CockroachDB 的整體流程：見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB migration</a>，本文只處理遷移後的 table locality 配置。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（multi-region 動機是 survival 非 latency）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（row-level 歸屬 + 單一邏輯 cluster）</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">CockroachDB Table Localities</a> / <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">Multi-Region Overview</a> / <a href="https://www.cockroachlabs.com/docs/stable/follower-reads.html">Follower Reads</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/</guid><description>&lt;p>Cosmos DB 文件列 &lt;em>5 個 consistency level&lt;/em>（Strong / Bounded staleness / Session / Consistent prefix / Eventual）、用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC&lt;/a> 講概念、但沒給具體工程判準。team 啟動 Cosmos DB 第一個要決定的就是 account 預設 level、再決定哪些 query 要 per-request override。本文先講 5 個 level 的精確語義、再進 Session 為什麼是 production 預設、再進「同一 application 內不同操作選不同 level」的進階策略；&lt;em>Strong + multi-region write 互斥&lt;/em>議題 cross-link 到 &lt;a href="../multi-region-write-conflict/">multi-region-write-conflict&lt;/a>、本篇不展開。&lt;/p>
&lt;p>本文不是 Cosmos DB overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a>）— 而是 &lt;em>consistency level 工程選擇邏輯&lt;/em> 的深度展開。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（用 session consistency 撐 AR 全球同步、5 level 跨 collection 分流）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（Black Friday 用較弱 consistency 換 throughput）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB workload 適配判讀（四層 framing）&lt;/strong>：API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in — 判讀軸詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>。本文聚焦 consistency level 選擇操作層、是 &lt;em>已選 Cosmos DB 後&lt;/em> 的 read / write 語義決策；若 workload 不適用 Cosmos DB、level 選擇無法救回 vendor 選錯的取捨。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：team 啟動 Cosmos DB account、setup wizard 問「預設 consistency level」 — 5 個選項、文件講概念、不知道實際業務該選哪個。production 上線後使用者反映「加入購物車後立刻看『我的購物車』讀到舊狀態」、「跨 region 看到玩家瞬移回舊位置」 — debug 發現是 consistency level 沒選對。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 文件列 <em>5 個 consistency level</em>（Strong / Bounded staleness / Session / Consistent prefix / Eventual）、用 <a href="/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC</a> 講概念、但沒給具體工程判準。team 啟動 Cosmos DB 第一個要決定的就是 account 預設 level、再決定哪些 query 要 per-request override。本文先講 5 個 level 的精確語義、再進 Session 為什麼是 production 預設、再進「同一 application 內不同操作選不同 level」的進階策略；<em>Strong + multi-region write 互斥</em>議題 cross-link 到 <a href="../multi-region-write-conflict/">multi-region-write-conflict</a>、本篇不展開。</p>
<p>本文不是 Cosmos DB overview（請看 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a>）— 而是 <em>consistency level 工程選擇邏輯</em> 的深度展開。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（用 session consistency 撐 AR 全球同步、5 level 跨 collection 分流）+ <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（Black Friday 用較弱 consistency 換 throughput）。</p>
<blockquote>
<p><strong>Cosmos DB workload 適配判讀（四層 framing）</strong>：API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in — 判讀軸詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>。本文聚焦 consistency level 選擇操作層、是 <em>已選 Cosmos DB 後</em> 的 read / write 語義決策；若 workload 不適用 Cosmos DB、level 選擇無法救回 vendor 選錯的取捨。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：team 啟動 Cosmos DB account、setup wizard 問「預設 consistency level」 — 5 個選項、文件講概念、不知道實際業務該選哪個。production 上線後使用者反映「加入購物車後立刻看『我的購物車』讀到舊狀態」、「跨 region 看到玩家瞬移回舊位置」 — debug 發現是 consistency level 沒選對。</p>
<p>讀者徵兆：</p>
<ul>
<li>「Session 跟 Eventual 看起來差不多、為什麼 Session 是預設」</li>
<li>「Bounded staleness 的 K 跟 T 該設多少」</li>
<li>「Strong 在 multi-region account 為什麼有額外限制」</li>
<li>「跨 region read 拿到舊版本、是 consistency 設錯還是 partition key 問題」</li>
</ul>
<p>真實壓力：</p>
<ul>
<li>購物車場景：加入購物車後立刻看「我的購物車」、結果讀到舊狀態（user 體驗破洞）</li>
<li>遊戲場景：玩家位置同步、跨 region 看到「玩家瞬移」回舊位置（遊戲體驗 bug）</li>
<li>金融場景：跨服務寫入後立即 read confirm、看不到剛寫的 — 業務邏輯誤判「沒寫進去」、重試 / rollback</li>
</ul>
<p>consistency level 選錯不是 config 問題、是 <em>影響 user-facing 行為</em> 的 selection 決策、必須在 selection 階段釐清。</p>
<h2 id="核心機制5-個-level-的精確語義">核心機制：5 個 level 的精確語義</h2>
<h3 id="strong">Strong</h3>
<ul>
<li>機制：read 拿到最新 commit、提供 linearizable read</li>
<li>限制：<em>single-write region 限制</em>；multi-region write 不可同時用 Strong（時間敏感 claim、查 <a href="https://learn.microsoft.com/azure/cosmos-db/consistency-levels">最新文件</a>）；跨 region 配 Strong 還要付 <a href="/blog/backend/knowledge-cards/cross-region-quorum/" data-link-title="Cross-Region Quorum" data-link-desc="multi-region distributed SQL 強制 voting replica 跨 region、commit 等多 region quorum ack、跨洲 RTT 物理硬限">Cross-Region Quorum</a> 的物理 latency tax（跨洲 100-200ms）</li>
<li>適合：金融交易、庫存扣減、status 機器寫後 read confirm</li>
<li>為什麼互斥：詳見 <a href="../multi-region-write-conflict/">multi-region-write-conflict</a> 的 AP 取捨段、本篇不展開</li>
</ul>
<h3 id="bounded-staleness">Bounded staleness</h3>
<ul>
<li>機制：read 落後 <em>不超過 K 個 version 或 T 秒</em>（取較嚴格者）；單 region 內 linearizable、跨 region 有 bounded lag、跟 <a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 是兩種「跨層 read-after-write」協議的選擇（前者 vendor 內建、後者 application-level）</li>
<li>設定：K（version 上限）+ T（時間上限）兩個參數</li>
<li>適合：multi-region 但需要「有 bound 的 staleness 保證」、如 trading system 跨 region read with SLA</li>
</ul>
<h3 id="session預設最常用">Session（預設、最常用）</h3>
<ul>
<li>機制：同一 session token 內讀寫一致；session 之外 eventual</li>
<li>適合：<em>多數互動式產品的甜蜜點</em> — 使用者寫入後自己立刻讀得到、其他 session 可接受 eventual</li>
<li>為什麼是預設：cost 接近 eventual（不像 Strong 多 2x RU）、體驗接近 Strong（自己讀寫一致）— 是 trade-off 的甜蜜點</li>
</ul>
<h3 id="consistent-prefix">Consistent prefix</h3>
<ul>
<li>機制：read 不會看到亂序的寫入（看到 A→B→C、不會看到 A→C→B）、但可能落後</li>
<li>適合：時序敏感但可 stale 的場景（如新聞 feed 不能跳序、但可以晚幾秒）</li>
<li>風險：常被誤用為 Session 替代、跨 session 一樣 stale、但比 Eventual 多保證 <em>順序</em></li>
</ul>
<h3 id="eventual">Eventual</h3>
<ul>
<li>機制：最便宜、無順序保證</li>
<li>適合：完全可 stale + 不需順序的場景（分析、log 聚合、推薦系統）</li>
</ul>
<h3 id="跟-cosmos-db-account--container-的關係">跟 Cosmos DB account / container 的關係</h3>
<ul>
<li>account 預設一個 level</li>
<li>單一 request 可以 <em>降級</em>（讀更弱 level）、<em>不可升級</em>（讀更強）</li>
<li>container 層 <em>無法獨立設定 consistency level</em>（時間敏感、查最新文件）— 分流靠 <em>collection 切分</em> + <em>per-request override</em></li>
</ul>
<h3 id="ru-成本差異">RU 成本差異</h3>
<ul>
<li>Strong / Bounded read ≈ 2x Session / Eventual 的 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a></li>
<li>write 成本不直接受 read level 影響、但 multi-region replication 開銷會（每多一個 region、寫成本 ×N）</li>
<li>selection 階段要把 consistency level 當「RU 倍數」進入容量公式、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
</ul>
<h3 id="跟通用-consistency-卡片的對應">跟通用 consistency 卡片的對應</h3>
<p>Cosmos DB 是 <em>少數把 5 level 都商品化</em> 的服務、其他系統通常只給 2-3 級（MongoDB read concern majority / local / linearizable、DynamoDB strong / eventual）。對應 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency-level</a> 卡片的概念分層。</p>
<p>跟 <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> 的關係：Cosmos DB Strong = single-region linearizable、<em>不是</em> 跨 region external consistency（跟 Spanner 的 TrueTime + Paxos 不同）。這個區別是 selection 階段的常見誤判 — 別把 Cosmos DB Strong 當成 Spanner 替代品。</p>
<p>對應 knowledge cards：<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency-level</a> / <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>。</p>
<h2 id="進階設計策略同一-application-內不同操作選不同-level">進階設計策略：同一 application 內不同操作選不同 level</h2>
<p>9.C11 Minecraft Earth 案例的平台特性段揭露「一致性是 spectrum、不是 binary」 — AR 遊戲玩家位置稍 stale OK（用 session / eventual）、庫存交易需要 strong；<em>同一 application 內不同 collection / container 配不同 consistency 是進階策略</em>、不一定是 account 一刀切。</p>
<p>container 層無法獨立設定 consistency level（時間敏感、查最新文件）、所以分流靠：</p>
<ul>
<li><strong>Collection / container 切分</strong>：高一致需求的資料放獨立 account、預設 Strong；低一致需求放另一 account、預設 Session</li>
<li><strong>Per-request override</strong>：account 預設 Session、特定「寫入後立即讀」場景升 Bounded、批次分析降 Eventual；用 SDK 的 <code>RequestOptions.ConsistencyLevel</code></li>
</ul>
<h3 id="per-request-override-範例c-sdk">Per-request override 範例（C# SDK）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// account 預設 Session</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// 但這個 read 需要 Bounded staleness</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">ReadItemAsync</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">id</span><span class="p">:</span> <span class="s">&#34;item-123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">partitionKey</span><span class="p">:</span> <span class="k">new</span> <span class="n">PartitionKey</span><span class="p">(</span><span class="s">&#34;user-456&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">requestOptions</span><span class="p">:</span> <span class="k">new</span> <span class="n">ItemRequestOptions</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">ConsistencyLevel</span> <span class="p">=</span> <span class="n">ConsistencyLevel</span><span class="p">.</span><span class="n">BoundedStaleness</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 批次分析、降到 Eventual 換成本</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kt">var</span> <span class="n">queryOptions</span> <span class="p">=</span> <span class="k">new</span> <span class="n">QueryRequestOptions</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">ConsistencyLevel</span> <span class="p">=</span> <span class="n">ConsistencyLevel</span><span class="p">.</span><span class="n">Eventual</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kt">var</span> <span class="n">iterator</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="n">GetItemQueryIterator</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span><span class="n">query</span><span class="p">,</span> <span class="n">requestOptions</span><span class="p">:</span> <span class="n">queryOptions</span><span class="p">);</span></span></span></code></pre></div><p>注意 <em>不可升級</em> 的限制：account 預設 Eventual、per-request 不能升 Strong（會 error）。要保留升級彈性、account 預設應該是 <em>最強需要的 level</em>、再 per-request 降級。</p>
<h3 id="跟-partition-key-design-的關係">跟 partition-key-design 的關係</h3>
<p>partition 失衡時即使設 Strong consistency 也看到 throttle、application 看到的是 <em>429 retry 後的高 latency</em>、不是 stale data — consistency level 跟 partition key 共同決定 <em>真實一致性體驗</em>。partition skew 把 Strong 的 SLA 拉到比 Session 還差、見 <a href="../partition-key-design/">partition-key-design</a> 的 latency budget 拆解段。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="account-層設定">account 層設定</h3>





<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"># Portal / ARM template / CLI</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">az cosmosdb update --name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --default-consistency-level Session</span></span></code></pre></div><p>切換 level 是即時生效、但 production 切換需要 audit 所有 client 的 session 邏輯（特別是 Strong → Session 的降級會讓「跨 session read 變 stale」）。</p>
<h3 id="request-層-override">Request 層 override</h3>
<p>SDK 傳 <code>RequestOptions.ConsistencyLevel</code>（C# / Java / Node SDK 行為一致）。注意 <em>只能降級</em>、升級會 reject。</p>
<h3 id="session-token-管理">Session token 管理</h3>
<p>每個 read response 帶 session token、client 下次 read 帶回去；跨 service 共享 token 需要顯式傳遞（不然每個 service 自己一個 session）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 拿到 session token</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">ReadItemAsync</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span><span class="n">id</span><span class="p">,</span> <span class="n">pk</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kt">var</span> <span class="n">sessionToken</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">&#34;x-ms-session-token&#34;</span><span class="p">];</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="c1">// 跨 service 傳遞（如 HTTP header）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">httpClient</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;X-Cosmos-Session-Token&#34;</span><span class="p">,</span> <span class="n">sessionToken</span><span class="p">);</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">// 下游 service 取得 token、用在 SDK request</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kt">var</span> <span class="n">requestOptions</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ItemRequestOptions</span> <span class="p">{</span> <span class="n">SessionToken</span> <span class="p">=</span> <span class="n">sessionToken</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kt">var</span> <span class="n">downstreamResponse</span> <span class="p">=</span> <span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">ReadItemAsync</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span><span class="n">id</span><span class="p">,</span> <span class="n">pk</span><span class="p">,</span> <span class="n">requestOptions</span><span class="p">);</span></span></span></code></pre></div><h3 id="驗證-level-行為">驗證 level 行為</h3>
<p>寫入後立即 read 同 partition key、量 staleness window。用 Cosmos DB Diagnostic Log 看 request 的實際 consistency level；對照 SDK 設定確認沒被預設 override。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>account 預設可改、但 production 切換 level 需要 audit 所有 client 的 session 邏輯；container 層無法獨立設定（時間敏感、查最新文件）。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1全用-strong-consistency">Failure 1：全用 Strong consistency</h3>
<p>互動式產品 Session 即足夠、用 Strong 浪費 2x RU + 限制 multi-region write、cost 暴漲且 multi-region 配置受限。徵兆是「RU consumption 明顯偏高、且 multi-region write 開不起來」 — 才發現預設選 Strong。</p>
<p>修：</p>
<ul>
<li>盤點業務需求、絕大多數讀寫場景 Session 就夠</li>
<li>把需要 Strong 的少數 collection 拆獨立 account、其他 default Session</li>
<li>計算 cost：Session vs Strong 在多數 workload 差距 1.5-2x、長期成本顯著</li>
</ul>
<h3 id="failure-2session-token-沒回傳">Failure 2：Session token 沒回傳</h3>
<p>read 後拿 token、下次 read 沒帶、實際變 Eventual；徵兆是「自己的寫立刻 read 看不到」、debug 才發現 SDK 設定漏。SDK 預設會自動管理 session token、但跨 service 傳遞時容易漏。</p>
<p>修：</p>
<ul>
<li>同一 service 內用 SDK 預設行為、不要關 session token cache</li>
<li>跨 service 通信時把 session token 隨 HTTP header 傳遞</li>
<li>或改 account 層 Bounded staleness（提供跨 session 的 K/T bound、不依賴 token）</li>
</ul>
<h3 id="failure-3跨-service-共享-session-假設">Failure 3：跨 service 共享 session 假設</h3>
<p>service A 寫、service B 讀、B 沒拿到 A 的 session token → 看不到 A 的寫。常見場景：order service 寫訂單、notification service 立刻 read 訂單寄通知 — notification 沒拿到 order 的 token、讀到舊狀態（或讀不到）。</p>
<p>修：</p>
<ul>
<li>service A 寫完、把 session token 進 message（Kafka event / HTTP response）傳給 B</li>
<li>B 用 token 做 read、保證讀到 A 的寫</li>
<li>或業務上接受 eventual、design notification 有 retry / reconcile 機制</li>
</ul>
<h3 id="failure-4bounded-staleness-設太鬆">Failure 4：Bounded staleness 設太鬆</h3>
<p>K = 100,000、T = 1 hour、實際等於 Eventual、team 以為自己有保護。bounded staleness 的 K/T 要對應業務 SLA、不是 vendor 預設值。</p>
<p>修：</p>
<ul>
<li>根據業務 read-after-write SLA 設 T（如「5 秒內必須讀到」設 T=5）</li>
<li>K 通常設成「peak QPS × T」的合理倍數</li>
<li>量測：production 觀察實際 staleness 分布、調整 K/T</li>
</ul>
<h3 id="failure-5multi-region-write-配-strong">Failure 5：multi-region write 配 Strong</h3>
<p>文件不允許 / 行為退化（時間敏感、查最新）— 必須改 Bounded / Session。這是 <em>AP 取捨的硬約束</em>、不是 config 問題；詳見 <a href="../multi-region-write-conflict/">multi-region-write-conflict</a> 的 AP 取捨段。</p>
<p>修：在 selection 階段就決定「要 active-active write 還是要 Strong」、不能事後補；要全球 linearizable 轉 Spanner / Aurora DSQL、要 active-active 接受 eventual / session / bounded。</p>
<h3 id="failure-6consistent-prefix-誤用">Failure 6：Consistent prefix 誤用</h3>
<p>把它當 Session 用、跨 session read 還是 stale、但比 Eventual 多一個順序保證；用錯地方等於浪費。常見誤判：「我要『順序對』、所以選 Consistent prefix」 — 但實際業務需求是「自己讀到自己寫的」、應該是 Session 而非 Consistent prefix。</p>
<p>修：</p>
<ul>
<li>Consistent prefix 適合 <em>時序敏感但可跨 session stale</em> 場景（新聞 feed、event log）</li>
<li>「自己讀到自己寫的」場景用 Session</li>
<li>跨 session 也要強一致用 Bounded / Strong</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>NormalizedRUConsumption</code>、<code>TotalRequestUnits</code>、<code>ReplicationLatency</code>（跨 region lag）</li>
<li>Diagnostic Log：每個 request 的實際 consistency level、確認沒被預設 override</li>
<li>成本計算：Strong / Bounded read 算 2x RU；multi-region 開後寫入成本 × region 數；level 跟 region 數的 cost matrix 是規劃必算</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：consistency level 當「RU 倍數」進入容量公式</li>
<li>Alert：
<ul>
<li><code>ReplicationLatency</code> 突增（跨 region 同步異常）</li>
<li>Diagnostic log 偵測 Strong read 突增（成本失控）</li>
<li>跨 service session token 缺失導致 stale read 比例上升</li>
</ul>
</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../partition-key-design/">partition-key-design</a>（partition key 跟 consistency 共同決定真實一致性體驗）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（RU 倍數量化）、<a href="../multi-region-write-conflict/">multi-region-write-conflict</a>（multi-region 下 consistency 的特殊行為、Strong + multi-region 互斥的 SSoT 主寫位置）、<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a>（MongoDB read concern → Cosmos DB consistency level 對應）</li>
<li>跟 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 對比：external consistency vs Cosmos DB Strong 不是同一個 thing</li>
<li>跟 <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 vendor</a> 對比：DynamoDB 只 strong / eventual 兩級、Cosmos DB 5 級提供細粒度</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>（Cosmos DB 5 level 跟 Spanner external consistency 並陳）</li>
<li>Knowledge cards：<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency-level</a> / <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a> / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a></li>
<li>Anti-recommendation：別把 Cosmos DB Strong 跟 Spanner external consistency 等同視之；產品需要真正全球 linearizable transaction 時、Cosmos DB 不是替代品 — 轉 Spanner / Aurora DSQL</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 5 consistency levels backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — session consistency + 跨 collection 分流主案例</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — 高 throughput + 較弱 level 補充</li>
<li><a href="../multi-region-write-conflict/">multi-region-write-conflict</a> — Strong + multi-region 互斥的 SSoT 主寫位置</li>
<li><a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">Consistency Level 卡片</a> / <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">Linearizability 卡片</a> / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/consistency-levels">Cosmos DB consistency levels</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/how-to-manage-consistency">Consistency level overrides</a></li>
</ul>
]]></content:encoded></item><item><title>從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/</guid><description>&lt;p>從自管 PostgreSQL / MySQL 遷到 Aurora 是 &lt;em>operational redesign hybrid&lt;/em>（Type C &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>）— wire protocol 相容、application 不改、但 HA / backup / monitoring / capacity 模型完全不同。本 playbook 走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 6 規格面&lt;/a>（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、補三個 Aurora-specific 議題：(1) 合規禁止跨境複製的 no-go condition、(2) 合規驅動遷移的時程模型（市場數 × 平均審查月份）、(3) Aurora 不是 all-purpose store 邊界。每階段進入下一步前都要過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> — Evidence 段列出的證據是 gate 條件、不是 nice-to-have。&lt;/p>
&lt;p>本 playbook 不重複 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>）— 前置閱讀建議 &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a>（理解為什麼 operational redesign）、&lt;a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO&lt;/a>（HA redesign 主項）、&lt;a href="../read-replica-scaling/">Aurora read replica scaling&lt;/a>（fleet 治理 SSoT、含合規 driver）。&lt;/p>
&lt;h2 id="migration-type-判定">Migration type 判定&lt;/h2>
&lt;p>本 playbook 是 &lt;em>Type C：Operational redesign hybrid&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>PostgreSQL / MySQL → Aurora wire protocol 相容、application 多數不改&lt;/li>
&lt;li>但 operational model（HA / backup / monitoring / capacity）完全不同、需要 redesign&lt;/li>
&lt;li>跟 Type A schema translation 差：不需要翻譯 application SQL&lt;/li>
&lt;li>跟 Type B drop-in 差：HA / backup / monitoring / capacity 模型需要 redesign&lt;/li>
&lt;li>跟 Type E paradigm shift 差：保留 single-primary SQL 跟 ACID transaction 語意&lt;/li>
&lt;/ul>
&lt;p>對照其他 Aurora-related migration playbook：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &amp;#43; snapshot isolation &amp;#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &amp;#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL&lt;/a> 是 Type E paradigm shift（distributed SQL、multi-region active-active）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &amp;#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PG → CockroachDB&lt;/a> 是 Type E paradigm shift + cross-cloud&lt;/li>
&lt;/ul>
&lt;h2 id="driver為什麼遷">Driver：為什麼遷&lt;/h2>
&lt;h3 id="主要-driver">主要 driver&lt;/h3>
&lt;ul>
&lt;li>團隊規模成長、DBA bandwidth 飽和、backup / failover / patch 操作負擔超過產品價值&lt;/li>
&lt;li>Read replica scaling 需求（傳統 streaming replication lag 秒級、Aurora 10-30ms — 詳見 &lt;a href="../read-replica-scaling/">Aurora read replica scaling&lt;/a>）&lt;/li>
&lt;li>Storage growth 痛點（local SSD 上限、resize 要 downtime、Aurora 自動 grow 到 128 TB）&lt;/li>
&lt;/ul>
&lt;h3 id="次要-driver">次要 driver&lt;/h3>
&lt;ul>
&lt;li>HA model 簡化（Patroni / Orchestrator → Aurora cluster endpoint、見 &lt;a href="../cross-az-failover-rto/">cross-AZ failover RTO&lt;/a>）&lt;/li>
&lt;li>Backup 自動化（pgBackRest / xtrabackup → Aurora automated backup + PITR）&lt;/li>
&lt;li>Multi-region DR 需求（&lt;a href="../global-database-multi-region/">Aurora Global Database&lt;/a>、但合規場景例外）&lt;/li>
&lt;/ul>
&lt;h3 id="no-go-condition嚴格遵守">No-go condition（嚴格遵守）&lt;/h3>
&lt;p>跨雲 / on-prem 需求觸動 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in&lt;/a> — Aurora storage layer 是 AWS 專屬、wire protocol 相容不代表退出成本低、long-term 跨雲策略未定時 self-managed PG / MySQL 反而保留路徑。&lt;/p></description><content:encoded><![CDATA[<p>從自管 PostgreSQL / MySQL 遷到 Aurora 是 <em>operational redesign hybrid</em>（Type C <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>）— wire protocol 相容、application 不改、但 HA / backup / monitoring / capacity 模型完全不同。本 playbook 走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 6 規格面</a>（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、補三個 Aurora-specific 議題：(1) 合規禁止跨境複製的 no-go condition、(2) 合規驅動遷移的時程模型（市場數 × 平均審查月份）、(3) Aurora 不是 all-purpose store 邊界。每階段進入下一步前都要過 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> — Evidence 段列出的證據是 gate 條件、不是 nice-to-have。</p>
<p>本 playbook 不重複 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>）— 前置閱讀建議 <a href="../storage-architecture/">Aurora storage architecture</a>（理解為什麼 operational redesign）、<a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a>（HA redesign 主項）、<a href="../read-replica-scaling/">Aurora read replica scaling</a>（fleet 治理 SSoT、含合規 driver）。</p>
<h2 id="migration-type-判定">Migration type 判定</h2>
<p>本 playbook 是 <em>Type C：Operational redesign hybrid</em>：</p>
<ul>
<li>PostgreSQL / MySQL → Aurora wire protocol 相容、application 多數不改</li>
<li>但 operational model（HA / backup / monitoring / capacity）完全不同、需要 redesign</li>
<li>跟 Type A schema translation 差：不需要翻譯 application SQL</li>
<li>跟 Type B drop-in 差：HA / backup / monitoring / capacity 模型需要 redesign</li>
<li>跟 Type E paradigm shift 差：保留 single-primary SQL 跟 ACID transaction 語意</li>
</ul>
<p>對照其他 Aurora-related migration playbook：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL</a> 是 Type E paradigm shift（distributed SQL、multi-region active-active）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PG → CockroachDB</a> 是 Type E paradigm shift + cross-cloud</li>
</ul>
<h2 id="driver為什麼遷">Driver：為什麼遷</h2>
<h3 id="主要-driver">主要 driver</h3>
<ul>
<li>團隊規模成長、DBA bandwidth 飽和、backup / failover / patch 操作負擔超過產品價值</li>
<li>Read replica scaling 需求（傳統 streaming replication lag 秒級、Aurora 10-30ms — 詳見 <a href="../read-replica-scaling/">Aurora read replica scaling</a>）</li>
<li>Storage growth 痛點（local SSD 上限、resize 要 downtime、Aurora 自動 grow 到 128 TB）</li>
</ul>
<h3 id="次要-driver">次要 driver</h3>
<ul>
<li>HA model 簡化（Patroni / Orchestrator → Aurora cluster endpoint、見 <a href="../cross-az-failover-rto/">cross-AZ failover RTO</a>）</li>
<li>Backup 自動化（pgBackRest / xtrabackup → Aurora automated backup + PITR）</li>
<li>Multi-region DR 需求（<a href="../global-database-multi-region/">Aurora Global Database</a>、但合規場景例外）</li>
</ul>
<h3 id="no-go-condition嚴格遵守">No-go condition（嚴格遵守）</h3>
<p>跨雲 / on-prem 需求觸動 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> — Aurora storage layer 是 AWS 專屬、wire protocol 相容不代表退出成本低、long-term 跨雲策略未定時 self-managed PG / MySQL 反而保留路徑。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>為什麼是 no-go</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨雲 / on-prem 需求</td>
          <td>Aurora AWS-only、wire protocol 相容但 storage 是 AWS 專屬</td>
      </tr>
      <tr>
          <td>需要 latest upstream 特性</td>
          <td>Aurora 通常落後 upstream PostgreSQL / MySQL 1-2 major version</td>
      </tr>
      <tr>
          <td>預算極敏感</td>
          <td>Aurora 比 self-managed PostgreSQL / MySQL 貴 20-30%</td>
      </tr>
      <tr>
          <td>合規禁止跨境複製</td>
          <td>受監管市場 <a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> <em>禁止跨境複製</em>、Aurora Global Database 在這種場景 <em>違反合規</em> — 要改用每市場獨立 cluster</td>
      </tr>
      <tr>
          <td>客製化 storage / I/O</td>
          <td>Aurora storage 是 AWS managed、不能客製化（vs self-managed 可以做 cgroup / quota / 自訂 storage 配置）</td>
      </tr>
  </tbody>
</table>
<p><strong>合規禁止跨境複製 no-go</strong>（<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered 揭露</a>）：</p>
<p>受監管市場資料不能跨境複製、Aurora Global Database 在這種場景違反合規。讀者規劃 Aurora migration 時不能假設「Aurora 一定有 Global Database 選項」— 要改用每市場獨立 cluster（fleet 拓樸吸收合規邊界、見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet SSoT）。</p>
<h3 id="替代方案">替代方案</h3>
<ul>
<li><strong>RDS PostgreSQL / MySQL</strong>：更接近 upstream、單 AZ 便宜、不重寫 storage</li>
<li><strong>自管 + Patroni HA + pgBackRest</strong>：保留控制、跨雲可用</li>
<li><strong>CockroachDB / Aurora DSQL</strong>：multi-region active-active write 需求</li>
</ul>
<h3 id="case-anchor">Case anchor</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a>：多套 RDBMS 統一到 Aurora、driver 是 <em>operational consolidation</em>、不是純效能</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>：200 個 cluster、按業務切分（不是一個大 cluster + 200 schema）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>：受監管場景、合規 lead time 是時程主項</li>
</ul>
<p><strong>Netflix scope warning（必引用）</strong>：</p>
<ul>
<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%、串流數十億小時">case「需要警惕」段第 2 點原文</a>：「Netflix 數據層遠不止 Aurora — 還有 Cassandra（playback metadata）、EVCache（cache layer）、Iceberg（data warehouse）。Aurora 主要是『需要 ACID 的 OLTP 工作負載』、不是『all-purpose store』」</li>
<li>工程含義：consolidation 是 <em>ACID OLTP 整合到 Aurora</em>、不是 <em>所有 store 整合到 Aurora</em></li>
<li>讀者規劃整合範圍時要明示什麼 workload 不在範圍（cache、analytics、time-series、search、KV 高峰）</li>
<li>「+75% performance improvement 是跨多 workload 的最大改善幅度、不是『每個 workload 都 +75%』。實際每個 workload 改善幅度從 10% 到 75% 不等」（case「需要警惕」段第 1 點）</li>
</ul>
<h2 id="diff-audit6-維-source--target-差異盤點">Diff audit：6 維 source / target 差異盤點</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>差異</th>
          <th>主導程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>PostgreSQL extension 相容性（pg_cron 改 Lambda / Step Functions、pg_partman 改 manual / native partitioning、TimescaleDB 不支援、PostGIS 支援）；MySQL plugin（HandlerSocket 不支援、audit plugin 改 CloudTrail）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>HA model、backup、monitoring、parameter management（postgresql.conf → DB parameter group / cluster parameter group）</td>
          <td>高（主導）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>保留（single-primary SQL、ACID transaction、wire protocol）</td>
          <td>無變動</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>connection pool（PgBouncer → RDS Proxy 或保留 PgBouncer in front of Aurora）、logical replication（pglogical / Debezium → Aurora 原生支援、但有版本限制）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>保留（connection string 改 endpoint、SSL config 改 RDS CA、driver 不改）</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>保留（single-region scaling、若要 multi-region 走另一條 playbook to DSQL）；fleet 拓樸決策（拆幾個 cluster）詳見 <a href="../read-replica-scaling/">read replica scaling</a> fleet SSoT</td>
          <td>中-高</td>
      </tr>
  </tbody>
</table>
<p><strong>主導差異</strong>：Operational layer（HA / backup / monitoring）、不是 schema 或 application。</p>
<h3 id="schema-diff-細節">Schema diff 細節</h3>
<p><strong>PostgreSQL → Aurora PostgreSQL</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Extension</th>
          <th>Aurora 支援</th>
          <th>Migration 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>pg_cron</td>
          <td>不支援</td>
          <td>改 Lambda 排程 + RDS event 或 Step Functions</td>
      </tr>
      <tr>
          <td>pg_partman</td>
          <td>不支援</td>
          <td>改 native declarative partitioning（PostgreSQL 11+）</td>
      </tr>
      <tr>
          <td>TimescaleDB</td>
          <td>不支援</td>
          <td>改 native partition + materialized view、或保留 self-managed</td>
      </tr>
      <tr>
          <td>PostGIS</td>
          <td>支援</td>
          <td>直接遷</td>
      </tr>
      <tr>
          <td>pgvector</td>
          <td>支援（新版）</td>
          <td>確認 Aurora PostgreSQL version、可能需要升級</td>
      </tr>
      <tr>
          <td>pglogical</td>
          <td>不支援</td>
          <td>改 Aurora 原生 logical replication（有版本限制）</td>
      </tr>
  </tbody>
</table>
<p><strong>MySQL → Aurora MySQL</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Plugin</th>
          <th>Aurora 支援</th>
          <th>Migration 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HandlerSocket</td>
          <td>不支援</td>
          <td>改 SQL access 或 Aurora-specific KV cache</td>
      </tr>
      <tr>
          <td>Vault audit</td>
          <td>不支援</td>
          <td>改 AWS CloudTrail + RDS audit log</td>
      </tr>
      <tr>
          <td>MyRocks engine</td>
          <td>不支援</td>
          <td>改 InnoDB（Aurora 預設）、評估 storage 成本</td>
      </tr>
      <tr>
          <td>MaxScale</td>
          <td>不支援</td>
          <td>改 Aurora reader endpoint 或 RDS Proxy</td>
      </tr>
  </tbody>
</table>
<h3 id="operational-diff-細節">Operational diff 細節</h3>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>Self-managed</th>
          <th>Aurora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HA</td>
          <td>Patroni / Orchestrator + etcd / ZooKeeper</td>
          <td>Cluster endpoint + 自動 cross-AZ failover</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>pgBackRest / xtrabackup + S3 lifecycle</td>
          <td>Automated backup + manual snapshot + PITR</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Prometheus exporter + Grafana</td>
          <td>CloudWatch + Performance Insights</td>
      </tr>
      <tr>
          <td>Parameter</td>
          <td>postgresql.conf / my.cnf</td>
          <td>DB parameter group / cluster parameter group</td>
      </tr>
      <tr>
          <td>Failover testing</td>
          <td>Patroni <code>patronictl failover</code></td>
          <td><code>aws rds failover-db-cluster</code></td>
      </tr>
      <tr>
          <td>WAL / binlog 觀測</td>
          <td><code>pg_stat_wal</code> / <code>SHOW MASTER STATUS</code></td>
          <td>CloudWatch + Performance Insights wait events</td>
      </tr>
  </tbody>
</table>
<h3 id="application-diff-細節">Application diff 細節</h3>





<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"># Self-managed PostgreSQL
</span></span><span class="line"><span class="ln">2</span><span class="cl">jdbc:postgresql://primary.internal:5432/mydb?ssl=true&amp;sslmode=verify-full&amp;sslrootcert=/etc/ssl/postgresql.crt
</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"># Aurora PostgreSQL
</span></span><span class="line"><span class="ln">5</span><span class="cl">jdbc:postgresql://my-cluster.cluster-xxx.us-east-1.rds.amazonaws.com:5432/mydb?ssl=true&amp;sslmode=verify-full&amp;sslrootcert=rds-ca.pem</span></span></code></pre></div><p>Application 改動量小：connection string 換 endpoint、SSL CA 換 RDS CA、driver 不變。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>。</p>
<h2 id="phase-plan階段切換">Phase plan：階段切換</h2>
<h3 id="phase-0pre-migration-audit2-4-週">Phase 0：Pre-migration audit（2-4 週）</h3>
<p>工作：</p>
<ul>
<li>Extension audit：<code>SELECT * FROM pg_extension</code> / <code>SHOW PLUGINS</code>、列出 source 使用的 extension</li>
<li>Parameter audit：postgresql.conf vs Aurora parameter group、列差異</li>
<li>Application connection string audit：所有服務的 DB connection 點位</li>
<li>Benchmark baseline：write QPS / read QPS / p99 latency</li>
<li>Cost baseline：current self-managed monthly cost vs Aurora estimate</li>
</ul>
<p>Output：</p>
<ul>
<li>Migration feasibility report（含 no-go condition check）</li>
<li>Aurora cluster sizing 估算</li>
<li>Extension migration plan（each extension 對應的策略）</li>
</ul>
<h3 id="phase-1aurora-infra-準備1-2-週">Phase 1：Aurora infra 準備（1-2 週）</h3>
<p>工作：</p>
<ul>
<li>Aurora cluster 開設（dev / staging / prod）</li>
<li>Parameter group 對位（從 source postgresql.conf / my.cnf 翻譯到 Aurora parameter group）</li>
<li>SG / subnet / IAM 設定</li>
<li>RDS Proxy 配置（如需要）</li>
<li>CloudWatch dashboard + Performance Insights baseline</li>
<li>Backup retention 設定（1-35 天）</li>
</ul>
<p>Output：</p>
<ul>
<li>Aurora cluster 待 data load</li>
<li>Monitoring 已 ready、能對照 source 跟 target</li>
</ul>
<h3 id="phase-2data-migration2-8-週依資料量">Phase 2：Data migration（2-8 週、依資料量）</h3>
<p>三條 path、依場景選：</p>
<h4 id="path-aaws-dms-full-load--cdc">Path A：AWS DMS full load + CDC</h4>
<ul>
<li>適合：&lt; 1 TB、可接受 read-only 短窗口</li>
<li>流程：DMS full load → DMS CDC → application cutover</li>
<li>優點：managed、validation 工具齊全</li>
<li>缺點：CDC lag 受 DMS task config 影響、bulk DDL 不友善</li>
</ul>
<h4 id="path-bpg_dump--mysqldump--logical-replication-catch-up">Path B：pg_dump / mysqldump + logical replication catch-up</h4>
<ul>
<li>適合：&gt; 1 TB、要長 CDC 期、預算敏感</li>
<li>流程：snapshot → pg_dump / mysqldump → restore to Aurora → logical replication catch-up → application cutover</li>
<li>優點：成本低、可控性高</li>
<li>缺點：手動步驟多、要自己管 CDC lag</li>
</ul>
<h4 id="path-csnapshot-restore">Path C：Snapshot restore</h4>
<ul>
<li>適合：已在 RDS PostgreSQL / MySQL</li>
<li>流程：RDS snapshot → Aurora restore-from-snapshot → catch-up → application cutover</li>
<li>優點：最快、AWS-internal 操作</li>
<li>缺點：只適用 RDS source、不適用 self-managed</li>
</ul>
<h3 id="phase-3dual-read-validation1-2-週">Phase 3：Dual-read validation（1-2 週）</h3>
<p>工作：</p>
<ul>
<li>Application read 50/50 split source / target</li>
<li>比對 query 結果（per-table checksum + sampling）</li>
<li>量測 latency（Aurora p99 ≤ source × 1.2）</li>
<li>確認 stale read 比例 &lt; 0.01%</li>
</ul>
<p>Output：</p>
<ul>
<li>Validation report：query 結果差異、latency 對照</li>
<li>Go/no-go decision for cutover</li>
</ul>
<h3 id="phase-4cutover-1-小時-window">Phase 4：Cutover（&lt; 1 小時 window）</h3>
<p>工作：</p>
<ul>
<li>Source set read-only</li>
<li>CDC catch-up final（lag → 0）</li>
<li>Application switch endpoint（DNS / service discovery / config flag）</li>
<li>Smoke test（critical path query + write）</li>
<li>Monitor error rate + latency 1 小時</li>
</ul>
<p>Output：</p>
<ul>
<li>Cutover complete</li>
<li>Source 切到 read-only、保留作為 rollback 餘地</li>
</ul>
<h3 id="phase-5cleanup4-8-週">Phase 5：Cleanup（4-8 週）</h3>
<p>工作：</p>
<ul>
<li>Source 保留 1 個月 read-only（rollback window）</li>
<li>確認穩定後 snapshot → S3 archive → decommission</li>
<li>舊 monitoring / backup / runbook archive</li>
</ul>
<p>Output：</p>
<ul>
<li>Source decommissioned</li>
<li>新 runbook + monitoring 為 SSoT</li>
</ul>
<h3 id="本-phase-plan-適用範圍">本 phase plan 適用範圍</h3>
<p><strong>Non-regulated workload</strong>（一般 SaaS / e-commerce / 內部系統）。受監管場景（銀行 / 保險 / 醫療）請見下方「合規驅動遷移的時程模型」段、技術 phase 不變但 lead time 完全不同。</p>
<h2 id="合規驅動遷移的時程模型">合規驅動遷移的時程模型</h2>
<p>受監管產業遷移的關鍵時程是 <em>合規審查 lead time</em>、不是技術遷移時間 — 本段是補充給銀行 / 保險 / 醫療讀者、避免照本 playbook 走嚴重低估時程。</p>
<h3 id="standard-chartered-揭露的時程模型">Standard Chartered 揭露的時程模型</h3>
<p><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered case</a> 「判讀」段第 3 點 + 「策略」段第 3 點原文：「每個受監管市場的審查可能 3-12 個月、合計遷移時程是『市場數 × 平均審查月份』、不是『技術遷移月份』」。</p>
<p>工程含義：</p>
<ul>
<li>技術 phase plan 假設 2-8 週 data migration + &lt; 1 小時 cutover</li>
<li>合規 lead time 是 <em>獨立軸</em>、可能比技術時程長一個數量級</li>
<li>不同市場合規進度不同步、可能要分批上線</li>
</ul>
<h3 id="合規時程組合">合規時程組合</h3>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>時程估算</th>
          <th>不可壓縮原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>技術遷移</td>
          <td>2-8 週 data migration + &lt; 1 小時 cutover</td>
          <td>工程可控</td>
      </tr>
      <tr>
          <td>單市場合規審查</td>
          <td>3-12 個月（Standard Chartered case 揭露）</td>
          <td>監管機構 lead time、不是技術問題</td>
      </tr>
      <tr>
          <td>多市場合規 lead time</td>
          <td>市場數 × 平均審查月份（7 市場 × 6 個月 ≈ 3.5 年最壞情況）</td>
          <td>各市場各自審、平行度受監管機構文化影響</td>
      </tr>
      <tr>
          <td>跨境複製禁令審查</td>
          <td>包含在合規審查內、可能讓 Global Database 從候選變反指標</td>
          <td>監管要求 data residency、無 cross-region replication option</td>
      </tr>
  </tbody>
</table>
<h3 id="讀者判讀">讀者判讀</h3>
<ul>
<li>受監管場景 <em>不能</em> 用本 playbook 的「2-8 週 data migration + &lt; 1 小時 cutover」估時程交付給管理層 — 合規 lead time 是時程主項</li>
<li>受監管場景 <em>不能</em> 假設 Aurora Global Database 是 multi-region DR 選項 — 合規禁止跨境複製場景下 Global Database 違反合規（見 <a href="../global-database-multi-region/">global-database-multi-region</a>），要改用每市場獨立 cluster</li>
<li>合規場景的 phase plan 要把每市場當成獨立 mini-migration、用 <em>市場批次</em> 推進、不是一次 big bang</li>
</ul>
<p><strong>scope warning（必明示、case 自承）</strong>：Standard Chartered case 未公開是 PostgreSQL 還是 MySQL、未公開具體 cost 數字 — 引用時不能擴寫「Standard Chartered 用 Aurora PostgreSQL」這類細節（case 用「相關 case study」匿名標明）。</p>
<p><strong>合規時程 scope 警示</strong>：「3-12 個月、7 市場 × 6 個月 ≈ 3.5 年」是 Standard Chartered case 揭露範圍。實際合規 lead time 隨產業（銀行 / 保險 / 醫療）跟國家（東南亞 / 歐盟 / 北美 / 中東）差異大、不是恆定數字。讀者要把自家對應監管框架的實際 lead time 算進來、不是直接套 Standard Chartered 數字。</p>
<h2 id="evidence每階段驗證資料">Evidence：每階段驗證資料</h2>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Phase 0</td>
          <td>extension list、parameter diff、application SQL 抽樣 test on Aurora dev cluster</td>
      </tr>
      <tr>
          <td>Phase 1</td>
          <td>Aurora cluster ready、monitoring dashboard 跟 source 對照</td>
      </tr>
      <tr>
          <td>Phase 2</td>
          <td>DMS row count match、checksum（per-table MD5）、CDC replication lag &lt; 5 秒</td>
      </tr>
      <tr>
          <td>Phase 3</td>
          <td>query result diff &lt; 0.01%、p99 latency Aurora ≤ source × 1.2、application error rate baseline</td>
      </tr>
      <tr>
          <td>Phase 4</td>
          <td>cutover 完成後 1 小時內 error rate &lt; baseline × 2、write success rate 100%</td>
      </tr>
      <tr>
          <td>Phase 5</td>
          <td>30 天無 rollback trigger、cost 月帳對齊預估</td>
      </tr>
  </tbody>
</table>
<p><strong>受監管追加 evidence</strong>：</p>
<ul>
<li>每市場合規 sign-off 文件（central bank / 金融監管機關）</li>
<li>跨境複製禁令審查記錄</li>
<li>Data residency 驗證測試（資料未流出受監管市場 boundary）</li>
<li>Audit log 連續性驗證（source / target audit log 銜接）</li>
</ul>
<p><strong>回路徑</strong>：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 抽 CDC / latency evidence。</p>
<h2 id="cutover切流決策">Cutover：切流決策</h2>
<p><strong>Cutover window</strong>：</p>
<ul>
<li>建議 4 AM local time（lowest traffic）</li>
<li>預留 4 小時 buffer</li>
<li>受監管場景可能要在合規規定的 maintenance window（例如某些央行規定週日凌晨）</li>
</ul>
<p><strong>Rollback condition</strong>：</p>
<ul>
<li>error rate &gt; baseline × 5</li>
<li>write latency p99 &gt; baseline × 3 持續 10 分鐘</li>
<li>data corruption signal（checksum mismatch、unexpected row count drop）</li>
</ul>
<p><strong>Rollback path</strong>：</p>
<ul>
<li>Application connection string 切回 source</li>
<li>Source 仍 read-write（cutover 前留 read-write 路徑、若已 read-only 要先解凍）</li>
<li>CDC 反向同步（Aurora → source）catch-up</li>
</ul>
<p><strong>Decision owner</strong>：</p>
<ul>
<li>DBA lead + service owner + on-call SRE 三方 sign-off</li>
<li>受監管場景追加 compliance officer sign-off</li>
<li>Cutover decision log 記錄（<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> / <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a> 文件化）</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window</a>、<a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback-condition</a>。</p>
<h2 id="cleanup雙軌退役">Cleanup：雙軌退役</h2>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>Cleanup 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source database</td>
          <td>read-only 1 個月、確認穩定後 snapshot → S3 archive → decommission</td>
      </tr>
      <tr>
          <td>舊 monitoring</td>
          <td>Prometheus exporter 拆、Grafana dashboard archive、CloudWatch dashboard 為 SSoT</td>
      </tr>
      <tr>
          <td>舊 backup chain</td>
          <td>pgBackRest / xtrabackup retention 保留至合規邊界（金融 7 年、一般 90 天）</td>
      </tr>
      <tr>
          <td>舊 runbook</td>
          <td>Patroni / Orchestrator runbook archive、新 runbook 對 Aurora cluster endpoint</td>
      </tr>
      <tr>
          <td>舊 CDC connector</td>
          <td>DMS task 留 7 天觀察期 → delete；自管 Debezium / pglogical 在 source decommission 同時退役</td>
      </tr>
  </tbody>
</table>
<p><strong>不可逆 cleanup 邊界</strong>：</p>
<ul>
<li>Source decommission 後資料只能從 backup restore</li>
<li>確保 backup 可用性測試通過再 decommission</li>
<li>受監管場景要保留 source backup 到合規 retention（金融 7 年、可能更長）</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<h3 id="netflix-aurora-consolidationoperational-consolidation-的價值">Netflix Aurora consolidation：operational consolidation 的價值</h3>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> 多套 RDBMS（PostgreSQL / MySQL / Oracle）→ Aurora、+75% 效能 / -28% 成本。</p>
<p><strong>驗證的 driver</strong>：</p>
<ul>
<li>DB 種類太多本身是規模化的成本（每多一種 DB 多一套 DBA 知識 / backup / monitoring）</li>
<li>整合到 Aurora 釋放工程資源、不是純效能改善</li>
</ul>
<p><strong>case 自帶警示（必引用）</strong>：</p>
<ul>
<li>「+75% 是跨多 workload 最大改善幅度、不是每 workload 都 +75%」（case「需要警惕」段第 1 點）</li>
<li><strong>Aurora 非 all-purpose store 邊界</strong>：「Netflix 數據層遠不止 Aurora — 還有 Cassandra（playback metadata）、EVCache（cache layer）、Iceberg（data warehouse）。Aurora 主要是『需要 ACID 的 OLTP 工作負載』」（case「需要警惕」段第 2 點）</li>
</ul>
<p>工程含義：consolidation 是「ACID OLTP 整合到 Aurora」、不是「所有 store 整合到 Aurora」。讀者規劃整合範圍時要明示什麼 workload 不在範圍：</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>是否在 Aurora consolidation 範圍</th>
          <th>替代</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ACID OLTP</td>
          <td>是</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Playback metadata</td>
          <td>否（Netflix 用 Cassandra）</td>
          <td>Cassandra / ScyllaDB</td>
      </tr>
      <tr>
          <td>Cache layer</td>
          <td>否（Netflix 用 EVCache）</td>
          <td>EVCache / Redis / Memcached</td>
      </tr>
      <tr>
          <td>Data warehouse</td>
          <td>否（Netflix 用 Iceberg）</td>
          <td>Iceberg / Snowflake / Redshift</td>
      </tr>
      <tr>
          <td>Time-series</td>
          <td>否（性能不適合）</td>
          <td>InfluxDB / TimescaleDB self-managed</td>
      </tr>
      <tr>
          <td>Search</td>
          <td>否（無 inverted index 優化）</td>
          <td>Elasticsearch / OpenSearch</td>
      </tr>
  </tbody>
</table>
<h3 id="draftkingsfleet-拓樸-redesign">DraftKings：fleet 拓樸 redesign</h3>
<p><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 200 個獨立 Aurora cluster、按業務切分（不是一個大 cluster + 200 schema）。</p>
<p><strong>驗證的 driver</strong>：</p>
<ul>
<li>Migration 不只是技術切換、也是 cluster 拓樸 redesign</li>
<li>業務本身可切分（每體育類別 / 每地理 / 每產品線）就在 migration 時順便拆 cluster</li>
<li>Blast radius 隔離跟容量規劃分散一起獲得</li>
</ul>
<p><strong>Fleet 拓樸決策</strong>：詳見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> 邊界段 SSoT。本 playbook 提醒 <em>migration 是拆 cluster 的好時機</em>、不展開拓樸決策本身。</p>
<h3 id="standard-chartered合規-lead-time--跨境複製禁令">Standard Chartered：合規 lead time + 跨境複製禁令</h3>
<p><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 受監管場景揭露：</p>
<ul>
<li>合規 lead time 是時程主項（3-12 個月 / 市場）</li>
<li>跨境複製禁止讓 Global Database 變反指標</li>
<li>每市場獨立 cluster + cross-AZ failover 是合規場景的標準解</li>
</ul>
<h3 id="反例aurora-不適合的場景">反例：Aurora 不適合的場景</h3>
<ul>
<li>Multi-region active-active write：見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL Migration</a></li>
<li>跨雲：見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PG → CockroachDB Migration</a></li>
<li>極端寫入吞吐（&gt; 100K WPS）：考慮 sharding、CockroachDB、或 DynamoDB</li>
</ul>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling playbook</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL</a> — paradigm shift、Type E、multi-region active-active</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PG → CockroachDB</a> — cross-cloud、paradigm shift</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PG → Aurora</a> — 既有 PG-specific playbook、可對照本 playbook 的 vendor-neutral 版本</li>
</ul>
<p><strong>Sibling deep article</strong>：</p>
<ul>
<li><a href="../storage-architecture/">Aurora storage architecture</a> — 理解 storage 設計才知道為什麼 operational redesign</li>
<li><a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> — HA redesign 主項</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — fleet 治理 SSoT、含合規 driver</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 合規禁止跨境複製的 anti-recommendation</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> — migration 上游 framework</li>
</ul>
<p><strong>何時不用本 playbook</strong>：</p>
<ul>
<li>從 Aurora 遷到別處（反向、走對應的反向 playbook）</li>
<li>從 RDS PostgreSQL 升 Aurora PostgreSQL 是 in-place upgrade、用 RDS console「Convert to Aurora」即可、不需要這套 playbook</li>
<li>跨雲遷移：本 playbook 不涵蓋 GCP / Azure SQL → Aurora 流程</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag 卡片</a> — operational diff 主軸</li>
<li><a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window 卡片</a> — cutover decision</li>
<li><a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">Rollback Condition 卡片</a> — rollback trigger</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — operational consolidation 跟 Aurora 非 all-purpose store 邊界</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — fleet 拓樸 redesign</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 合規 lead time + 跨境複製禁令</li>
<li><a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.html">Aurora migration documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB Change Feed (CDC)：persistent change log、Azure Functions trigger、latest-version vs all-versions-and-deletes 與跟 DynamoDB Streams 對照</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/change-feed-cdc/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/change-feed-cdc/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Change Feed 是 Cosmos DB 把 container 內每次寫入按 logical partition 順序持久化成一條可重讀變更序列的能力、對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture&lt;/a> 的概念分層。它讓「寫入後要做的後續工作」（投影、cache 失效、事件發布、跨 store 同步）從 application 寫入路徑解耦出來、由獨立 consumer 按自己的進度消費。本文先講 Change Feed 的精確語義與兩種模式、再進 change feed processor 與 Azure Functions trigger 的操作流程、最後拆失敗模式與跟 DynamoDB Streams 的對照。&lt;/p>
&lt;p>Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（85,000 SKU、每週新增 5,000 件的高更新頻率 catalog、寫入後需要 search index / 推薦排序投影）。ASOS case 本身沒有揭露 Change Feed 的實作細節、本文只取它的 catalog 寫入投影壓力當情境 anchor、機制以 Azure vendor 規格與通用工程展開。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：catalog 寫入 Cosmos DB 後、下游還有一連串工作要做 — 把商品同步到 search index、刷新推薦排序、讓 cache 失效、發 event 給庫存服務。團隊一開始把這些工作塞進寫入 API 的同步路徑、寫一筆商品要等 search index 更新完才返回、寫入 latency 被下游拖垮；高峰時下游 search service 變慢、整條寫入鏈一起阻塞。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「寫入 API latency 被下游投影工作拖高、想把它非同步化」&lt;/li>
&lt;li>「下游 consumer 掛掉一段時間、重啟後要怎麼補回漏掉的變更」&lt;/li>
&lt;li>「同一筆 document 在短時間內改三次、下游只需要最終狀態還是每次都要」&lt;/li>
&lt;li>「要做 audit / 要知道刪除事件、但 Change Feed 預設讀不到 delete」&lt;/li>
&lt;/ul>
&lt;p>真實壓力：寫入路徑與下游處理耦合會讓寫入 SLA 受制於最不穩的 consumer；而把投影改成「掃全表」的 batch job 又有延遲與成本問題。Change Feed 提供的是 &lt;em>持久、可重讀、按 partition 有序&lt;/em> 的變更來源、讓下游用 pull 或 trigger 模式按自己的進度消費。&lt;/p>
&lt;h2 id="核心機制partition-scoped-persistent-change-log">核心機制：partition-scoped persistent change log&lt;/h2>
&lt;p>Change Feed 是 container 的內建能力、把每個 logical partition 內的寫入按發生順序記錄成一條持久序列。它的關鍵語義有幾個面向。&lt;/p>
&lt;p>順序保證是 &lt;em>per logical partition&lt;/em>、不是 container 全域。同一 partition key 內的變更嚴格有序、跨 partition 之間沒有全域順序 — 這跟 &lt;a href="../partition-key-design/">partition-key-design&lt;/a> 的設計直接相關、consumer 必須假設不同 partition 的事件可能交錯到達。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Change Feed 是 Cosmos DB 把 container 內每次寫入按 logical partition 順序持久化成一條可重讀變更序列的能力、對應 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture</a> 的概念分層。它讓「寫入後要做的後續工作」（投影、cache 失效、事件發布、跨 store 同步）從 application 寫入路徑解耦出來、由獨立 consumer 按自己的進度消費。本文先講 Change Feed 的精確語義與兩種模式、再進 change feed processor 與 Azure Functions trigger 的操作流程、最後拆失敗模式與跟 DynamoDB Streams 的對照。</p>
<p>Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（85,000 SKU、每週新增 5,000 件的高更新頻率 catalog、寫入後需要 search index / 推薦排序投影）。ASOS case 本身沒有揭露 Change Feed 的實作細節、本文只取它的 catalog 寫入投影壓力當情境 anchor、機制以 Azure vendor 規格與通用工程展開。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：catalog 寫入 Cosmos DB 後、下游還有一連串工作要做 — 把商品同步到 search index、刷新推薦排序、讓 cache 失效、發 event 給庫存服務。團隊一開始把這些工作塞進寫入 API 的同步路徑、寫一筆商品要等 search index 更新完才返回、寫入 latency 被下游拖垮；高峰時下游 search service 變慢、整條寫入鏈一起阻塞。</p>
<p>讀者徵兆：</p>
<ul>
<li>「寫入 API latency 被下游投影工作拖高、想把它非同步化」</li>
<li>「下游 consumer 掛掉一段時間、重啟後要怎麼補回漏掉的變更」</li>
<li>「同一筆 document 在短時間內改三次、下游只需要最終狀態還是每次都要」</li>
<li>「要做 audit / 要知道刪除事件、但 Change Feed 預設讀不到 delete」</li>
</ul>
<p>真實壓力：寫入路徑與下游處理耦合會讓寫入 SLA 受制於最不穩的 consumer；而把投影改成「掃全表」的 batch job 又有延遲與成本問題。Change Feed 提供的是 <em>持久、可重讀、按 partition 有序</em> 的變更來源、讓下游用 pull 或 trigger 模式按自己的進度消費。</p>
<h2 id="核心機制partition-scoped-persistent-change-log">核心機制：partition-scoped persistent change log</h2>
<p>Change Feed 是 container 的內建能力、把每個 logical partition 內的寫入按發生順序記錄成一條持久序列。它的關鍵語義有幾個面向。</p>
<p>順序保證是 <em>per logical partition</em>、不是 container 全域。同一 partition key 內的變更嚴格有序、跨 partition 之間沒有全域順序 — 這跟 <a href="../partition-key-design/">partition-key-design</a> 的設計直接相關、consumer 必須假設不同 partition 的事件可能交錯到達。</p>
<p>進度由 continuation token 表達。consumer 讀到哪裡、用一個 continuation token 標記；下次帶 token 回來、從上次的位置繼續。token 是 per partition range 的、container 做 partition split 時 token 要能跟著 range 拆分 — 這是 change feed processor 幫忙處理的部分。</p>
<p>讀取是 pull-based 持久來源、不是 push 通知。Change Feed 不主動推、是 consumer 主動拉。Azure Functions 的 Cosmos DB trigger 看起來像 push、底層仍是 trigger runtime 持續 poll Change Feed。</p>
<h3 id="兩種模式latest-version-vs-all-versions-and-deletes">兩種模式：latest-version vs all-versions-and-deletes</h3>
<p>Change Feed 有兩種模式、語義差很大、選錯會在 audit / 補償場景出問題（模式名稱與可用性屬時間敏感、查 <a href="https://learn.microsoft.com/azure/cosmos-db/change-feed">最新文件</a>）。</p>
<p>Latest-version 模式（過去稱 incremental feed）只給每個 document 的 <em>最新狀態</em>。同一 document 在兩次消費之間改了三次、consumer 只會看到最後一個版本、中間版本看不到；delete 也看不到（document 消失、feed 裡沒有對應的 tombstone）。這個模式適合「我只要把最終狀態投影到下游」的場景 — search index 同步、cache 刷新、物化視圖更新。</p>
<p>All-versions-and-deletes 模式給 <em>每一次</em> 變更、包含中間版本與 delete / TTL 過期事件。同一 document 改三次、feed 給三筆；刪掉給一筆刪除事件。這個模式適合需要完整變更歷史的場景 — audit log、event sourcing、需要對 delete 做反應的跨 store 同步。代價是事件量更大、且這個模式對 retention 與 partition 行為有額外約束（時間敏感、查文件）。</p>
<p>選擇判準：問「我需要中間版本與刪除事件嗎」。投影類工作（只要最終狀態）用 latest-version；audit 與需要對刪除反應的同步用 all-versions-and-deletes。預設選 latest-version、只有明確需要歷史與 delete 時才升級。</p>
<h3 id="change-feed-processor-的角色">change feed processor 的角色</h3>
<p>直接讀 Change Feed 要自己管 partition range、lease、continuation token、failover — 這些 plumbing 用 change feed processor library 處理。它的核心元件是 <em>lease container</em>：一個獨立的 Cosmos DB container、記錄每個 partition range 由哪個 consumer instance 處理、處理到哪個 continuation token。多個 consumer instance 共用同一個 lease container 時、processor 自動把 partition range 分配到不同 instance、達成水平擴展與 failover。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="啟用與確認">啟用與確認</h3>
<p>Change Feed 對 SQL API container 是預設啟用的、不需要額外開關（latest-version 模式）。all-versions-and-deletes 模式需要在 container 層設定、且要設 retention window。</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"># 確認 container 存在、Change Feed 自動可用（latest-version）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">az cosmosdb sql container show <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --account-name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --database-name catalog --name products <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;resource.id&#34;</span></span></span></code></pre></div><p>驗證：container 存在即可讀 latest-version feed。要用 all-versions-and-deletes、先確認 account / SDK 版本支援（時間敏感、查文件）並設好 retention。</p>
<h3 id="change-feed-processorc-sdk">change feed processor（C# SDK）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// lease container 獨立於 monitored container</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">Container</span> <span class="n">monitored</span> <span class="p">=</span> <span class="n">client</span><span class="p">.</span><span class="n">GetContainer</span><span class="p">(</span><span class="s">&#34;catalog&#34;</span><span class="p">,</span> <span class="s">&#34;products&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">Container</span> <span class="n">leases</span> <span class="p">=</span> <span class="n">client</span><span class="p">.</span><span class="n">GetContainer</span><span class="p">(</span><span class="s">&#34;catalog&#34;</span><span class="p">,</span> <span class="s">&#34;leases&#34;</span><span class="p">);</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">ChangeFeedProcessor</span> <span class="n">processor</span> <span class="p">=</span> <span class="n">monitored</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">.</span><span class="n">GetChangeFeedProcessorBuilder</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">processorName</span><span class="p">:</span> <span class="s">&#34;search-index-sync&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">onChangesDelegate</span><span class="p">:</span> <span class="n">HandleChangesAsync</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">.</span><span class="n">WithInstanceName</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="n">MachineName</span><span class="p">)</span>  <span class="c1">// 每個 instance 唯一</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">.</span><span class="n">WithLeaseContainer</span><span class="p">(</span><span class="n">leases</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">.</span><span class="n">Build</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">await</span> <span class="n">processor</span><span class="p">.</span><span class="n">StartAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kd">async</span> <span class="n">Task</span> <span class="n">HandleChangesAsync</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">IReadOnlyCollection</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span> <span class="n">changes</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">product</span> <span class="k">in</span> <span class="n">changes</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="c1">// 投影到 search index — 必須 idempotent</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">await</span> <span class="n">searchIndex</span><span class="p">.</span><span class="n">UpsertAsync</span><span class="p">(</span><span class="n">product</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="c1">// delegate 正常返回 = processor 自動推進 lease 的 continuation token</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>驗證：lease container 內會出現每個 partition range 的 lease document、<code>ContinuationToken</code> 欄位隨消費推進；多開一個 instance、觀察 lease 被重新分配到兩個 instance。失敗時 delegate 拋例外、processor 不推進該 range 的 token、下次重讀同一批（at-least-once、所以 handler 要 idempotent）。</p>
<h3 id="azure-functions-trigger消費端最省維運的形態">Azure Functions trigger（消費端最省維運的形態）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="na">[FunctionName(&#34;SyncSearchIndex&#34;)]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="kd">async</span> <span class="n">Task</span> <span class="n">Run</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">    [CosmosDBTrigger(
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">        databaseName: &#34;catalog&#34;,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">        containerName: &#34;products&#34;,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">        Connection = &#34;CosmosConnection&#34;,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">        LeaseContainerName = &#34;leases&#34;,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">        CreateLeaseContainerIfNotExists = true)]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span> <span class="n">changes</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">ILogger</span> <span class="n">log</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">p</span> <span class="k">in</span> <span class="n">changes</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">await</span> <span class="n">searchIndex</span><span class="p">.</span><span class="n">UpsertAsync</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>  <span class="c1">// idempotent</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Functions trigger 底層就是 change feed processor、lease 與 scale-out 由 Functions runtime 管。驗證：function 的 invocation count 隨寫入增加、Application Insights 看 <code>changes</code> batch size 與 lag。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>Change Feed 是讀取側機制、停掉 consumer 不影響寫入。要重放：刪掉 lease container 的對應 lease（或建新 processor name）會從 container 起點或指定時間點重讀。重放前確認下游投影是 idempotent、否則重放會重複寫。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="把-handler-寫成非-idempotent">把 handler 寫成非 idempotent</h3>
<p>Change Feed 是 at-least-once。consumer 在處理一批後、推進 token 前 crash、重啟會重讀同一批。handler 若是「append 一筆 audit row」這種非 idempotent 操作、重放會產生重複。徵兆是下游出現重複事件、且重複數對應 consumer 重啟次數。修法是讓投影用 upsert（以 document id + version 為 key）、audit 用 dedup key、發 event 帶 idempotency key 讓下游去重 — 對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 的設計。</p>
<h3 id="用-latest-version-模式卻期待看到-delete">用 latest-version 模式卻期待看到 delete</h3>
<p>team 用預設 latest-version feed 做跨 store 同步、上線後發現「source 刪掉的 document、target 還在」。latest-version 模式不發 delete 事件、刪除在 feed 裡是「該 document 不再出現」、consumer 無從得知。修法是 audit / 需要刪除反應的場景改 all-versions-and-deletes 模式；或在 application 層用 soft delete（寫一個 <code>deleted: true</code> 的版本、latest-version feed 就看得到這次寫入）。</p>
<h3 id="lease-container-配置不足成為瓶頸">lease container 配置不足成為瓶頸</h3>
<p>lease container 自己也吃 RU、且 processor 對它有頻繁讀寫。lease container RU 配太低、processor 推進 token 被 throttle、表現成 Change Feed 消費 lag 升高、但 monitored container 看起來健康。徵兆是消費 lag 持續增長、診斷發現 429 來自 lease container 而非 source。修法是給 lease container 足夠 RU、把它跟 source container 的容量分開規劃、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</p>
<h3 id="假設-change-feed-有跨-partition-全域順序">假設 Change Feed 有跨 partition 全域順序</h3>
<p>consumer 假設事件按全域時間到達、做了依賴順序的邏輯（例如「先建立帳號事件、後消費事件」）。Change Feed 只保證 per logical partition 有序、跨 partition 交錯。徵兆是偶發的「後續事件先到、依賴的前置事件後到」。修法是讓有順序依賴的 document 落在同一 partition key、或在 consumer 端用業務 timestamp / version 做排序與 buffer、不依賴 feed 到達順序。</p>
<h3 id="anti-recommendation不是所有寫入後工作都要-change-feed">Anti-recommendation：不是所有「寫入後工作」都要 Change Feed</h3>
<p>寫入後若只是同一 request 內、同一 partition 的小量同步工作、直接在 application 寫入路徑處理、或用 stored procedure 在 partition 內做（見 <a href="../stored-procedure-trigger/">stored-procedure-trigger</a>）更簡單。Change Feed 的價值在 <em>解耦下游、可重放、水平擴展</em> — 當下游處理慢、會失敗、需要重放、或要被多個獨立 consumer 各自消費時才成立。下游工作輕、不需要重放、強耦合在寫入語義內時、引入 Change Feed + lease container 是多一層維運成本。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：Change Feed 消費 lag（最新寫入時間 vs consumer 已處理位置）、processor 每批 <code>changes</code> 數量、lease container 的 <code>NormalizedRUConsumption</code></li>
<li>consumer 端 throughput 受 partition range 數限制 — 並行度上限約等於 physical partition 數；range 不夠多時加 consumer instance 不會更快</li>
<li>成本：Change Feed 讀取本身吃 RU、all-versions-and-deletes 模式事件量更大、lease container 額外 RU — 三項都進容量公式、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：把 Change Feed consumer 當獨立 throughput 單位、不要跟 OLTP 寫入共用同一個 RU budget 估算</li>
<li>Alert：消費 lag 持續增長（consumer 跟不上寫入）、lease container 429、handler 例外率上升</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../stored-procedure-trigger/">stored-procedure-trigger</a>（partition 內同步邏輯 vs Change Feed 的非同步解耦）、<a href="../synapse-link-federation/">synapse-link-federation</a>（分析 workload 用 analytical store、不要用 Change Feed 自己搭 analytics pipeline）、<a href="../partition-key-design/">partition-key-design</a>（per-partition 順序的來源）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（Change Feed + lease container 的 RU 成本）</li>
<li>跟 DynamoDB Streams 對照：兩者都是 partition-ordered 變更 log + at-least-once consumer。差異在 DynamoDB Streams 有固定 24 小時 retention、原生發 INSERT / MODIFY / REMOVE（含 delete）；Cosmos DB latest-version 模式預設不發 delete、要 all-versions-and-deletes 模式才有完整事件與 delete。從 DynamoDB Streams 思維過來的 team 容易假設「delete 一定看得到」、要先確認模式。對照 <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 vendor</a></li>
<li>Knowledge card：<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「忽略 Change Feed」常見陷阱</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 Change Feed backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — 高更新頻率 catalog 投影壓力的情境 anchor</li>
<li><a href="../stored-procedure-trigger/">stored-procedure-trigger</a> — partition 內同步邏輯的對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> — per-partition 順序的設計來源</li>
<li><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 vendor</a> — DynamoDB Streams 對照</li>
<li><a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture 卡片</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/change-feed">Change feed in Azure Cosmos DB</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/nosql/change-feed-processor">Change feed processor</a></li>
</ul>
]]></content:encoded></item><item><title>Microsoft 365：套件級身分驗證事故</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/</guid><description>&lt;p>這起案例的核心責任是處理跨產品套件的共同依賴風險。企業套件事故常同時影響 mail、collaboration 與 admin 能力，影響評估必須快速分層。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>cross-product auth errors&lt;/td>
 &lt;td>影響是否跨產品同步出現&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>admin-plane availability&lt;/td>
 &lt;td>管理平面是否可用&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>communication consistency&lt;/td>
 &lt;td>對外狀態是否一致&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這個案例的邊界是「套件級共同依賴失效」，不是單一產品缺陷。主要風險是把跨產品事件拆成局部事件，導致對外訊息與修復順序失焦。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先做產品分層影響盤點，再把指揮決策與外部更新同步回寫 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>。若影響評估不一致，先補 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20&lt;/a> 再更新對外節奏。&lt;/p></description><content:encoded><![CDATA[<p>這起案例的核心責任是處理跨產品套件的共同依賴風險。企業套件事故常同時影響 mail、collaboration 與 admin 能力，影響評估必須快速分層。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>回寫章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cross-product auth errors</td>
          <td>影響是否跨產品同步出現</td>
          <td><a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a></td>
      </tr>
      <tr>
          <td>admin-plane availability</td>
          <td>管理平面是否可用</td>
          <td><a href="/blog/backend/08-incident-response/vendor-dependency-incident/" data-link-title="8.15 Vendor / 第三方依賴事故處理" data-link-desc="依賴方掛掉、自己無 control 時的決策模型">8.15</a></td>
      </tr>
      <tr>
          <td>communication consistency</td>
          <td>對外狀態是否一致</td>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">8.10</a></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這個案例的邊界是「套件級共同依賴失效」，不是單一產品缺陷。主要風險是把跨產品事件拆成局部事件，導致對外訊息與修復順序失焦。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先做產品分層影響盤點，再把指揮決策與外部更新同步回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a>。若影響評估不一致，先補 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20</a> 再更新對外節奏。</p>
]]></content:encoded></item><item><title>Spotify：平台工程與可靠性契約</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/</guid><description>&lt;p>Spotify 案例的核心責任是把可靠性標準平台化。當團隊自治程度高，若沒有共同契約，跨服務風險會在整合時爆發。&lt;/p>
&lt;h2 id="問題場景">問題場景&lt;/h2>
&lt;p>不同團隊採用不同部署與觀測習慣，單隊看似穩定，但跨服務路徑會出現隱性斷點，導致事故時難以協同定位。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Reliability contract&lt;/td>
 &lt;td>每個服務最低要提供什麼&lt;/td>
 &lt;td>基線能力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Platform self-service&lt;/td>
 &lt;td>標準如何降低導入成本&lt;/td>
 &lt;td>擴散能力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-team evidence&lt;/td>
 &lt;td>證據如何跨團隊共享&lt;/td>
 &lt;td>協作效率&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="可觀測訊號">可觀測訊號&lt;/h2>
&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>contract compliance rate&lt;/td>
 &lt;td>契約覆蓋是否足夠&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>release dependency failures&lt;/td>
 &lt;td>依賴變更是否常破壞發布&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cross-team incident handoff latency&lt;/td>
 &lt;td>交接是否有共同語言&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先補 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10&lt;/a> 的契約欄位，再以 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18&lt;/a> 對齊 owner 與責任邊界。&lt;/p></description><content:encoded><![CDATA[<p>Spotify 案例的核心責任是把可靠性標準平台化。當團隊自治程度高，若沒有共同契約，跨服務風險會在整合時爆發。</p>
<h2 id="問題場景">問題場景</h2>
<p>不同團隊採用不同部署與觀測習慣，單隊看似穩定，但跨服務路徑會出現隱性斷點，導致事故時難以協同定位。</p>
<h2 id="決策機制">決策機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>核心問題</th>
          <th>交付結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reliability contract</td>
          <td>每個服務最低要提供什麼</td>
          <td>基線能力</td>
      </tr>
      <tr>
          <td>Platform self-service</td>
          <td>標準如何降低導入成本</td>
          <td>擴散能力</td>
      </tr>
      <tr>
          <td>Cross-team evidence</td>
          <td>證據如何跨團隊共享</td>
          <td>協作效率</td>
      </tr>
  </tbody>
</table>
<h2 id="可觀測訊號">可觀測訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>contract compliance rate</td>
          <td>契約覆蓋是否足夠</td>
          <td><a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10</a></td>
      </tr>
      <tr>
          <td>release dependency failures</td>
          <td>依賴變更是否常破壞發布</td>
          <td><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14</a></td>
      </tr>
      <tr>
          <td>cross-team incident handoff latency</td>
          <td>交接是否有共同語言</td>
          <td><a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>先補 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10</a> 的契約欄位，再以 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a> 對齊 owner 與責任邊界。</p>
]]></content:encoded></item><item><title>7.1 攻擊者視角（紅隊）與攻擊面驗證</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/</guid><description>&lt;p>紅隊子分類的核心目標是建立一條可操作的風險判讀路徑：先盤點攻擊面，再檢查流程濫用、資料外洩、資源濫用與設定風險。這裡的紅隊定位為攻擊者視角的風險檢查與設計驗證。章節內容使用技術文章格式，聚焦情境判讀、代價分析與設計取捨，名詞定義則統一放在 knowledge cards。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Attack surface&lt;/td>
 &lt;td>public API、admin route、webhook、diagnostic endpoint、upload&lt;/td>
 &lt;td>&lt;code>7.R1&lt;/code> + &lt;code>7.3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trust boundary&lt;/td>
 &lt;td>auth boundary、tenant boundary、network boundary、internal capability&lt;/td>
 &lt;td>&lt;code>7.R1&lt;/code> + &lt;code>7.2&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abuse case&lt;/td>
 &lt;td>export abuse、invite abuse、reset abuse、trial abuse&lt;/td>
 &lt;td>&lt;code>7.R2&lt;/code> + &lt;code>7.R11&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data exposure path&lt;/td>
 &lt;td>response、log、search index、support tool、backup&lt;/td>
 &lt;td>&lt;code>7.R3&lt;/code> + &lt;code>7.4&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resource abuse&lt;/td>
 &lt;td>rate limit bypass、bot traffic、expensive operation、queue saturation&lt;/td>
 &lt;td>&lt;code>7.R4&lt;/code> + &lt;code>06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Misconfiguration surface&lt;/td>
 &lt;td>debug flag、open CORS、default credential、cloud policy&lt;/td>
 &lt;td>&lt;code>7.R5&lt;/code> + &lt;code>7.3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Control failure pattern&lt;/td>
 &lt;td>邊界、身分、會話、資料、證據鏈控制面失效&lt;/td>
 &lt;td>&lt;code>7.R8&lt;/code> + &lt;code>7.8&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Adversary cost cadence&lt;/td>
 &lt;td>初始成本、維持成本、擴散成本、兌現成本&lt;/td>
 &lt;td>&lt;code>7.R9&lt;/code> + &lt;code>7.9&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Detection evasion&lt;/td>
 &lt;td>覆蓋缺口、關聯缺口、噪音、保留不足與復盤回寫&lt;/td>
 &lt;td>&lt;code>7.R10&lt;/code> + &lt;code>7.13&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>紅隊分析優先問「攻擊者最先會找哪裡」。如果一個功能能被枚舉、被猜測、被重放、被跨 tenant 存取、被帶出內網、被放大流量或被錯誤設定打開，這個功能就應該被優先放進攻擊者視角檢查清單。&lt;/p>
&lt;h2 id="與安全主模組的關係">與安全主模組的關係&lt;/h2>
&lt;p>本子分類與資安主模組形成互補，並從相反方向驗證防護是否成立。資安主模組從「應該如何保護」出發；紅隊子分類從「哪裡會被打穿」出發，兩者共用同一批卡片，只是觀察角度不同。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/red-team-basics-and-attack-flow/" data-link-title="7.R0 紅隊基礎：攻擊流程作為服務判讀語言" data-link-desc="建立紅隊共同詞彙與流程視角，讓案例分析回到服務環節的決策判讀">7.R0&lt;/a>&lt;/td>
 &lt;td>紅隊基礎與常見攻擊流程&lt;/td>
 &lt;td>建立共同詞彙與流程判讀框架&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/attack-surface-boundary/" data-link-title="7.R1 攻擊面與信任邊界" data-link-desc="從紅隊角度盤點系統暴露面，以及信任假設在哪裡開始失效">7.R1&lt;/a>&lt;/td>
 &lt;td>攻擊面與信任邊界&lt;/td>
 &lt;td>確認哪些入口與資源先被看見&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/abuse-paths/" data-link-title="7.R2 入口濫用與權限突破" data-link-desc="說明合法功能如何被惡意組合成權限突破或流程濫用">7.R2&lt;/a>&lt;/td>
 &lt;td>入口濫用與權限突破&lt;/td>
 &lt;td>確認合法功能是否能被惡意組合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/exposure-and-exfiltration/" data-link-title="7.R3 資料暴露與外洩路徑" data-link-desc="說明敏感資料會從哪些回應、紀錄或工具中流出">7.R3&lt;/a>&lt;/td>
 &lt;td>資料暴露與外洩路徑&lt;/td>
 &lt;td>確認資料會從哪些路徑流出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/resource-abuse/" data-link-title="7.R4 資源濫用與可用性破壞" data-link-desc="說明攻擊者如何把合法操作放大成容量壓力或服務退化">7.R4&lt;/a>&lt;/td>
 &lt;td>資源濫用與可用性破壞&lt;/td>
 &lt;td>確認哪些操作會被放大成壓力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/misconfiguration-and-hidden-entrypoints/" data-link-title="7.R5 設定錯誤與隱藏入口" data-link-desc="說明 debug、預設值與環境差異如何意外暴露能力">7.R5&lt;/a>&lt;/td>
 &lt;td>設定錯誤與隱藏入口&lt;/td>
 &lt;td>確認哪些預設值或 debug 面會暴露能力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/" data-link-title="7.R6 事故故事重構：服務環節問題與注意事項" data-link-desc="以統一模板整理案例：服務環節問題地圖、案例對照表與跨模組交接邊界">7.R6&lt;/a>&lt;/td>
 &lt;td>事故故事：按攻擊流程拆解弱點&lt;/td>
 &lt;td>用公開事故理解不同環節的失效模式與取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7&lt;/a>&lt;/td>
 &lt;td>事故案例庫（可引用）&lt;/td>
 &lt;td>讓服務章節可引用「缺少哪個 workflow 會重演事故」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/control-failure-patterns/" data-link-title="7.R8 控制面失效樣式" data-link-desc="把常見攻擊結果回推成控制面失效樣式">7.R8&lt;/a>&lt;/td>
 &lt;td>控制面失效樣式&lt;/td>
 &lt;td>把攻擊結果回推成可重用的失效樣式語言&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/adversary-cost-and-campaign-cadence/" data-link-title="7.R9 攻擊者成本與行動節奏" data-link-desc="用攻擊者成本模型判讀哪些環節最容易被優先利用">7.R9&lt;/a>&lt;/td>
 &lt;td>攻擊者成本與行動節奏&lt;/td>
 &lt;td>用成本與收益排序優先防守環節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/detection-evasion-and-observability-gaps/" data-link-title="7.R10 偵測迴避與觀測缺口" data-link-desc="從攻擊者角度盤點偵測盲區與觀測資料缺口">7.R10&lt;/a>&lt;/td>
 &lt;td>偵測迴避與觀測缺口&lt;/td>
 &lt;td>從攻擊者角度補強觀測盲區判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/" data-link-title="7.R11 流程濫用問題卡片" data-link-desc="以原子化卡片細化整體 red-team 知識網，承接金字塔結構往下生長的問題討論">7.R11&lt;/a>&lt;/td>
 &lt;td>流程濫用問題卡片&lt;/td>
 &lt;td>用原子卡片拆解高風險流程失效樣式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本子分類會先建立判讀順序與控制面，再往後延伸到具體驗證方式與實作策略。&lt;/p>
&lt;p>案例庫與 incident workflow 的雙向路由可參考 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&amp;gt; 案例 -&amp;gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>紅隊子分類的核心目標是建立一條可操作的風險判讀路徑：先盤點攻擊面，再檢查流程濫用、資料外洩、資源濫用與設定風險。這裡的紅隊定位為攻擊者視角的風險檢查與設計驗證。章節內容使用技術文章格式，聚焦情境判讀、代價分析與設計取捨，名詞定義則統一放在 knowledge cards。</p>
<h2 id="判讀分類">判讀分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
          <th>承接章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Attack surface</td>
          <td>public API、admin route、webhook、diagnostic endpoint、upload</td>
          <td><code>7.R1</code> + <code>7.3</code></td>
      </tr>
      <tr>
          <td>Trust boundary</td>
          <td>auth boundary、tenant boundary、network boundary、internal capability</td>
          <td><code>7.R1</code> + <code>7.2</code></td>
      </tr>
      <tr>
          <td>Abuse case</td>
          <td>export abuse、invite abuse、reset abuse、trial abuse</td>
          <td><code>7.R2</code> + <code>7.R11</code></td>
      </tr>
      <tr>
          <td>Data exposure path</td>
          <td>response、log、search index、support tool、backup</td>
          <td><code>7.R3</code> + <code>7.4</code></td>
      </tr>
      <tr>
          <td>Resource abuse</td>
          <td>rate limit bypass、bot traffic、expensive operation、queue saturation</td>
          <td><code>7.R4</code> + <code>06</code></td>
      </tr>
      <tr>
          <td>Misconfiguration surface</td>
          <td>debug flag、open CORS、default credential、cloud policy</td>
          <td><code>7.R5</code> + <code>7.3</code></td>
      </tr>
      <tr>
          <td>Control failure pattern</td>
          <td>邊界、身分、會話、資料、證據鏈控制面失效</td>
          <td><code>7.R8</code> + <code>7.8</code></td>
      </tr>
      <tr>
          <td>Adversary cost cadence</td>
          <td>初始成本、維持成本、擴散成本、兌現成本</td>
          <td><code>7.R9</code> + <code>7.9</code></td>
      </tr>
      <tr>
          <td>Detection evasion</td>
          <td>覆蓋缺口、關聯缺口、噪音、保留不足與復盤回寫</td>
          <td><code>7.R10</code> + <code>7.13</code></td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>紅隊分析優先問「攻擊者最先會找哪裡」。如果一個功能能被枚舉、被猜測、被重放、被跨 tenant 存取、被帶出內網、被放大流量或被錯誤設定打開，這個功能就應該被優先放進攻擊者視角檢查清單。</p>
<h2 id="與安全主模組的關係">與安全主模組的關係</h2>
<p>本子分類與資安主模組形成互補，並從相反方向驗證防護是否成立。資安主模組從「應該如何保護」出發；紅隊子分類從「哪裡會被打穿」出發，兩者共用同一批卡片，只是觀察角度不同。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/red-team-basics-and-attack-flow/" data-link-title="7.R0 紅隊基礎：攻擊流程作為服務判讀語言" data-link-desc="建立紅隊共同詞彙與流程視角，讓案例分析回到服務環節的決策判讀">7.R0</a></td>
          <td>紅隊基礎與常見攻擊流程</td>
          <td>建立共同詞彙與流程判讀框架</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/attack-surface-boundary/" data-link-title="7.R1 攻擊面與信任邊界" data-link-desc="從紅隊角度盤點系統暴露面，以及信任假設在哪裡開始失效">7.R1</a></td>
          <td>攻擊面與信任邊界</td>
          <td>確認哪些入口與資源先被看見</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/abuse-paths/" data-link-title="7.R2 入口濫用與權限突破" data-link-desc="說明合法功能如何被惡意組合成權限突破或流程濫用">7.R2</a></td>
          <td>入口濫用與權限突破</td>
          <td>確認合法功能是否能被惡意組合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/exposure-and-exfiltration/" data-link-title="7.R3 資料暴露與外洩路徑" data-link-desc="說明敏感資料會從哪些回應、紀錄或工具中流出">7.R3</a></td>
          <td>資料暴露與外洩路徑</td>
          <td>確認資料會從哪些路徑流出</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/resource-abuse/" data-link-title="7.R4 資源濫用與可用性破壞" data-link-desc="說明攻擊者如何把合法操作放大成容量壓力或服務退化">7.R4</a></td>
          <td>資源濫用與可用性破壞</td>
          <td>確認哪些操作會被放大成壓力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/misconfiguration-and-hidden-entrypoints/" data-link-title="7.R5 設定錯誤與隱藏入口" data-link-desc="說明 debug、預設值與環境差異如何意外暴露能力">7.R5</a></td>
          <td>設定錯誤與隱藏入口</td>
          <td>確認哪些預設值或 debug 面會暴露能力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/" data-link-title="7.R6 事故故事重構：服務環節問題與注意事項" data-link-desc="以統一模板整理案例：服務環節問題地圖、案例對照表與跨模組交接邊界">7.R6</a></td>
          <td>事故故事：按攻擊流程拆解弱點</td>
          <td>用公開事故理解不同環節的失效模式與取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7</a></td>
          <td>事故案例庫（可引用）</td>
          <td>讓服務章節可引用「缺少哪個 workflow 會重演事故」</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/control-failure-patterns/" data-link-title="7.R8 控制面失效樣式" data-link-desc="把常見攻擊結果回推成控制面失效樣式">7.R8</a></td>
          <td>控制面失效樣式</td>
          <td>把攻擊結果回推成可重用的失效樣式語言</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/adversary-cost-and-campaign-cadence/" data-link-title="7.R9 攻擊者成本與行動節奏" data-link-desc="用攻擊者成本模型判讀哪些環節最容易被優先利用">7.R9</a></td>
          <td>攻擊者成本與行動節奏</td>
          <td>用成本與收益排序優先防守環節</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/detection-evasion-and-observability-gaps/" data-link-title="7.R10 偵測迴避與觀測缺口" data-link-desc="從攻擊者角度盤點偵測盲區與觀測資料缺口">7.R10</a></td>
          <td>偵測迴避與觀測缺口</td>
          <td>從攻擊者角度補強觀測盲區判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/" data-link-title="7.R11 流程濫用問題卡片" data-link-desc="以原子化卡片細化整體 red-team 知識網，承接金字塔結構往下生長的問題討論">7.R11</a></td>
          <td>流程濫用問題卡片</td>
          <td>用原子卡片拆解高風險流程失效樣式</td>
      </tr>
  </tbody>
</table>
<p>本子分類會先建立判讀順序與控制面，再往後延伸到具體驗證方式與實作策略。</p>
<p>案例庫與 incident workflow 的雙向路由可參考 <a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow</a> 與 <a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖</a>。</p>
]]></content:encoded></item><item><title>Consumer Group</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/</guid><description>&lt;p>Consumer group 的核心概念是「一組 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 共同承擔某個 stream 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 的處理進度」。同一 group 內的 consumer 分攤工作（每筆訊息只被 group 內的一個 consumer 處理）；不同 group 可以各自獨立處理同一批事件，實現 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Consumer group 是事件流跟多服務訂閱的協調模型。分析服務、搜尋索引服務、通知服務可以用不同 group 讀同一 topic — 每個 group 有自己的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 進度跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>。&lt;/p>
&lt;p>在 Kafka 中，consumer group 是一級概念、由 group coordinator 管理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 分配（rebalance）。在 Redis Streams 中對應 consumer group（XREADGROUP）。在 RabbitMQ 中沒有原生 consumer group — 多個 consumer 連到同一個 queue 就是 competing consumers、不同 queue 綁到同一個 exchange 就是 fan-out。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 consumer group 的訊號是同一事件要被多個系統各自處理。訂單事件同時給出貨、通知與報表 — 三個 consumer group 各自有自己的處理速度、錯誤率跟重放流程。&lt;/p>
&lt;p>Consumer group 的 rebalance（partition 重新分配）是 Kafka 生態的常見運維議題。Consumer 加入或離開 group 時觸發 rebalance、rebalance 期間 partition 暫時無人消費、造成短暫的處理停頓。Rebalance 時間跟 partition 數量、consumer 數量有關。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Consumer group 要設計 group 名稱（跟服務名稱對齊、方便辨識）、offset / checkpoint 策略（auto-commit vs manual commit）、rebalance 行為（cooperative vs eager）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 告警閾值與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a> 權限。不同 group 的失敗應分開觀測跟處理 — 通知 group 落後不應影響出貨 group 的監控判讀。&lt;/p></description><content:encoded><![CDATA[<p>Consumer group 的核心概念是「一組 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 共同承擔某個 stream 或 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 的處理進度」。同一 group 內的 consumer 分攤工作（每筆訊息只被 group 內的一個 consumer 處理）；不同 group 可以各自獨立處理同一批事件，實現 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Consumer group 是事件流跟多服務訂閱的協調模型。分析服務、搜尋索引服務、通知服務可以用不同 group 讀同一 topic — 每個 group 有自己的 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 進度跟 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>。</p>
<p>在 Kafka 中，consumer group 是一級概念、由 group coordinator 管理 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 分配（rebalance）。在 Redis Streams 中對應 consumer group（XREADGROUP）。在 RabbitMQ 中沒有原生 consumer group — 多個 consumer 連到同一個 queue 就是 competing consumers、不同 queue 綁到同一個 exchange 就是 fan-out。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 consumer group 的訊號是同一事件要被多個系統各自處理。訂單事件同時給出貨、通知與報表 — 三個 consumer group 各自有自己的處理速度、錯誤率跟重放流程。</p>
<p>Consumer group 的 rebalance（partition 重新分配）是 Kafka 生態的常見運維議題。Consumer 加入或離開 group 時觸發 rebalance、rebalance 期間 partition 暫時無人消費、造成短暫的處理停頓。Rebalance 時間跟 partition 數量、consumer 數量有關。</p>
<h2 id="設計責任">設計責任</h2>
<p>Consumer group 要設計 group 名稱（跟服務名稱對齊、方便辨識）、offset / checkpoint 策略（auto-commit vs manual commit）、rebalance 行為（cooperative vs eager）、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 告警閾值與 <a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a> 權限。不同 group 的失敗應分開觀測跟處理 — 通知 group 落後不應影響出貨 group 的監控判讀。</p>
]]></content:encoded></item><item><title>Cosmos DB Stored Procedure / Trigger（JavaScript）：partition-scoped 交易、server-side 邏輯邊界、何時用何時讓 application 層處理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/stored-procedure-trigger/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/stored-procedure-trigger/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Cosmos DB 的 stored procedure、trigger 與 user-defined function 是用 JavaScript 寫、執行在 Cosmos DB engine 內的 server-side 邏輯。它最有價值的能力是把同一 logical partition 內的多個操作包成一個原子交易 — 這是 application 層無法用 SDK 單獨做到的。本文先講這層 server-side 邏輯的精確語義與限制、再進操作流程、最後重點放在「何時用、何時不用」的判準 — 因為多數應用邏輯放在 application 層更好維護、stored procedure 應該是少數有明確理由的場景。&lt;/p>
&lt;p>本文沒有專屬 production case anchor：stored procedure 的設計取捨在公開 case 庫覆蓋稀薄、機制以 Azure vendor 規格與通用工程展開、情境用 partition 內原子交易這個具體需求驅動。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：本文涉及的 script 大小上限、執行時間上限、bounded execution 行為等具體限制屬時間敏感、不同 account 配置可能不同、實作前以 &lt;a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Cosmos DB stored procedure 官方文件&lt;/a> cross-verify。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：業務需要「讀一筆庫存、檢查數量、扣減、寫一筆扣減記錄」這四步必須原子完成 — 中間不能被別的請求插入。用 application 層 SDK 做、四步是四個獨立 round-trip、中間有 race window；兩個請求同時扣同一筆庫存、可能都讀到 10、各扣 1、結果是 9 而非 8。這類 read-modify-write 在同一 partition 內、需要 server-side 原子性。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「同一 partition 內的 read-modify-write 有 race、想要原子交易」&lt;/li>
&lt;li>「想做批次 upsert、減少 round-trip 與 RU」&lt;/li>
&lt;li>「想在寫入時自動加 timestamp / 算衍生欄位、用 pre-trigger 行不行」&lt;/li>
&lt;li>「stored procedure 能不能跨 partition 做交易」（不行 — 這是常見誤解）&lt;/li>
&lt;/ul>
&lt;p>真實壓力：Cosmos DB 的 transaction 邊界是 &lt;em>single logical partition&lt;/em>、跨 partition 沒有原生 ACID 交易。partition 內需要原子性時、SDK 多次 round-trip 無法保證、stored procedure 是 vendor 提供的 partition-scoped transaction 機制。但這個能力有強約束、且容易被濫用成「把業務邏輯都搬進 DB」。&lt;/p>
&lt;h2 id="核心機制partition-scoped-javascript-execution">核心機制：partition-scoped JavaScript execution&lt;/h2>
&lt;p>Cosmos DB 的 server-side 邏輯有三類、責任不同。&lt;/p>
&lt;p>Stored procedure 是執行在單一 logical partition 內的 JavaScript 函式、它內部對該 partition 的所有 document 操作包在一個 &lt;em>隱式交易&lt;/em> 裡 — 全部成功 commit、任一失敗整個 rollback。呼叫時必須指定 partition key、procedure 的所有操作都限定在那個 partition。&lt;/p>
&lt;p>Trigger 分 pre-trigger 與 post-trigger、綁在 create / replace / delete 等操作上、但 &lt;em>不會自動觸發&lt;/em> — 必須在 request 明確指定要跑哪個 trigger（這跟關聯式 DB 的 trigger 自動執行不同）。pre-trigger 在操作前跑（常用來補欄位、驗證）、post-trigger 在操作後跑（常用來更新同 partition 的彙總 document）。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Cosmos DB 的 stored procedure、trigger 與 user-defined function 是用 JavaScript 寫、執行在 Cosmos DB engine 內的 server-side 邏輯。它最有價值的能力是把同一 logical partition 內的多個操作包成一個原子交易 — 這是 application 層無法用 SDK 單獨做到的。本文先講這層 server-side 邏輯的精確語義與限制、再進操作流程、最後重點放在「何時用、何時不用」的判準 — 因為多數應用邏輯放在 application 層更好維護、stored procedure 應該是少數有明確理由的場景。</p>
<p>本文沒有專屬 production case anchor：stored procedure 的設計取捨在公開 case 庫覆蓋稀薄、機制以 Azure vendor 規格與通用工程展開、情境用 partition 內原子交易這個具體需求驅動。</p>
<blockquote>
<p><strong>Scope warning</strong>：本文涉及的 script 大小上限、執行時間上限、bounded execution 行為等具體限制屬時間敏感、不同 account 配置可能不同、實作前以 <a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Cosmos DB stored procedure 官方文件</a> cross-verify。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：業務需要「讀一筆庫存、檢查數量、扣減、寫一筆扣減記錄」這四步必須原子完成 — 中間不能被別的請求插入。用 application 層 SDK 做、四步是四個獨立 round-trip、中間有 race window；兩個請求同時扣同一筆庫存、可能都讀到 10、各扣 1、結果是 9 而非 8。這類 read-modify-write 在同一 partition 內、需要 server-side 原子性。</p>
<p>讀者徵兆：</p>
<ul>
<li>「同一 partition 內的 read-modify-write 有 race、想要原子交易」</li>
<li>「想做批次 upsert、減少 round-trip 與 RU」</li>
<li>「想在寫入時自動加 timestamp / 算衍生欄位、用 pre-trigger 行不行」</li>
<li>「stored procedure 能不能跨 partition 做交易」（不行 — 這是常見誤解）</li>
</ul>
<p>真實壓力：Cosmos DB 的 transaction 邊界是 <em>single logical partition</em>、跨 partition 沒有原生 ACID 交易。partition 內需要原子性時、SDK 多次 round-trip 無法保證、stored procedure 是 vendor 提供的 partition-scoped transaction 機制。但這個能力有強約束、且容易被濫用成「把業務邏輯都搬進 DB」。</p>
<h2 id="核心機制partition-scoped-javascript-execution">核心機制：partition-scoped JavaScript execution</h2>
<p>Cosmos DB 的 server-side 邏輯有三類、責任不同。</p>
<p>Stored procedure 是執行在單一 logical partition 內的 JavaScript 函式、它內部對該 partition 的所有 document 操作包在一個 <em>隱式交易</em> 裡 — 全部成功 commit、任一失敗整個 rollback。呼叫時必須指定 partition key、procedure 的所有操作都限定在那個 partition。</p>
<p>Trigger 分 pre-trigger 與 post-trigger、綁在 create / replace / delete 等操作上、但 <em>不會自動觸發</em> — 必須在 request 明確指定要跑哪個 trigger（這跟關聯式 DB 的 trigger 自動執行不同）。pre-trigger 在操作前跑（常用來補欄位、驗證）、post-trigger 在操作後跑（常用來更新同 partition 的彙總 document）。</p>
<p>UDF（user-defined function）是 query 內可呼叫的純函式、用來在 query projection / filter 階段做自訂計算、沒有寫入能力。</p>
<h3 id="交易邊界與-bounded-execution">交易邊界與 bounded execution</h3>
<p>交易嚴格限 single logical partition。stored procedure 不能跨 partition 寫、傳不同 partition key 的操作會失敗。跨 partition 的原子需求要改 workflow（saga / 補償）或重新設計 partition key 讓相關資料同 partition、見 <a href="../partition-key-design/">partition-key-design</a>。</p>
<p>執行有 bounded execution 限制：每次呼叫有時間與 resource 上限（時間敏感、查文件）、跑太久 Cosmos DB 會中止。處理大量 document 的 stored procedure 必須自己檢查每個操作的回傳、發現「快到上限」時停下、回傳一個 continuation 標記、讓 client 帶著標記再呼叫一次 — 這個 continuation 模式是寫批次 stored procedure 的必備 pattern。</p>
<h3 id="ru-成本">RU 成本</h3>
<p>stored procedure 內每個 document 操作都吃 RU、整個 procedure 的 RU 是內部所有操作的總和、由 response header 回報。一個掃很多 document 的 procedure 可能很貴、且因為 bounded execution 要分多次呼叫、成本與複雜度都比想像高、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="寫一個-partition-scoped-原子扣減">寫一個 partition-scoped 原子扣減</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// deductStock.js — 在單一 partition 內原子扣減庫存
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">deductStock</span><span class="p">(</span><span class="nx">productId</span><span class="p">,</span> <span class="nx">qty</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">getContext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="nx">getCollection</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">response</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="nx">getResponse</span><span class="p">();</span>
</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="kd">var</span> <span class="nx">query</span> <span class="o">=</span> <span class="s2">&#34;SELECT * FROM c WHERE c.id = &#39;&#34;</span> <span class="o">+</span> <span class="nx">productId</span> <span class="o">+</span> <span class="s2">&#34;&#39;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">var</span> <span class="nx">accepted</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">queryDocuments</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">container</span><span class="p">.</span><span class="nx">getSelfLink</span><span class="p">(),</span> <span class="nx">query</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">docs</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">docs</span> <span class="o">||</span> <span class="nx">docs</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;product not found&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="kd">var</span> <span class="nx">product</span> <span class="o">=</span> <span class="nx">docs</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="o">&lt;</span> <span class="nx">qty</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">                <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;insufficient stock&#34;</span><span class="p">);</span>  <span class="c1">// 整個交易 rollback
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="o">-=</span> <span class="nx">qty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="kd">var</span> <span class="nx">ok</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">replaceDocument</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">                <span class="nx">product</span><span class="p">.</span><span class="nx">_self</span><span class="p">,</span> <span class="nx">product</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">                <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">e</span><span class="p">;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;replace not accepted&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="nx">response</span><span class="p">.</span><span class="nx">setBody</span><span class="p">({</span> <span class="nx">remaining</span><span class="o">:</span> <span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="p">});</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">accepted</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;query not accepted&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>註冊與呼叫（C# SDK）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">Scripts</span><span class="p">.</span><span class="n">CreateStoredProcedureAsync</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">new</span> <span class="n">StoredProcedureProperties</span><span class="p">(</span><span class="s">&#34;deductStock&#34;</span><span class="p">,</span> <span class="n">File</span><span class="p">.</span><span class="n">ReadAllText</span><span class="p">(</span><span class="s">&#34;deductStock.js&#34;</span><span class="p">)));</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="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">Scripts</span><span class="p">.</span><span class="n">ExecuteStoredProcedureAsync</span><span class="p">&lt;</span><span class="kt">dynamic</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;deductStock&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">new</span> <span class="n">PartitionKey</span><span class="p">(</span><span class="n">productId</span><span class="p">),</span>   <span class="c1">// 必須指定 partition key</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">new</span> <span class="kt">dynamic</span><span class="p">[]</span> <span class="p">{</span> <span class="n">productId</span><span class="p">,</span> <span class="m">1</span> <span class="p">});</span></span></span></code></pre></div><p>驗證：兩個並行請求扣同一筆、總扣減量等於兩次之和、不會 lost update（交易原子性）。庫存不足時拋例外、整個 procedure rollback、stock 不變。回傳 header 的 <code>x-ms-request-charge</code> 是這次交易的總 RU。</p>
<h3 id="批次操作的-continuation-模式">批次操作的 continuation 模式</h3>
<p>掃多筆 document 的 procedure 要在 callback 內檢查回傳的 <code>accepted</code>、為 false（快到上限）時停下並回傳已處理數量、由 client loop 呼叫直到全部處理完。驗證：對一個大 partition 跑、觀察需要多次呼叫、每次回傳的已處理數累加到總數。</p>
<h3 id="pre-trigger-補欄位">pre-trigger 補欄位</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">addTimestamp</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">doc</span> <span class="o">=</span> <span class="nx">getContext</span><span class="p">().</span><span class="nx">getRequest</span><span class="p">().</span><span class="nx">getBody</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">doc</span><span class="p">.</span><span class="nx">createdAt</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">getContext</span><span class="p">().</span><span class="nx">getRequest</span><span class="p">().</span><span class="nx">setBody</span><span class="p">(</span><span class="nx">doc</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫時要明確指定 trigger、否則不執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">CreateItemAsync</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="k">new</span> <span class="n">PartitionKey</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">pk</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">new</span> <span class="n">ItemRequestOptions</span> <span class="p">{</span> <span class="n">PreTriggers</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="s">&#34;addTimestamp&#34;</span> <span class="p">}</span> <span class="p">});</span></span></span></code></pre></div><p>驗證：帶 trigger 的寫入有 <code>createdAt</code>、不帶 trigger 的寫入沒有 — 確認 trigger 非自動。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>stored procedure 本身的交易是 all-or-nothing、procedure 內拋例外即整個 rollback。部署層面：stored procedure / trigger 是 container 內的 resource、replace 即更新、delete 即移除、不影響 data。</p>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<p>這是本文的主判讀段：多數應用邏輯放在 application 層更好、stored procedure 只有少數場景值得。</p>
<p>值得用 stored procedure 的條件：</p>
<ul>
<li><em>partition 內的多步原子交易</em> — read-modify-write、需要 all-or-nothing、且相關資料確實在同一 partition。這是 stored procedure 不可替代的能力。</li>
<li><em>省 round-trip 的批次操作</em> — 一次寫入幾百筆同 partition document、用 stored procedure 比幾百次 SDK 呼叫省 latency 與部分 RU overhead。</li>
</ul>
<p>讓 application 層處理的條件（多數情況）：</p>
<ul>
<li>業務邏輯複雜、會頻繁變動 — JavaScript stored procedure 的版本管理、測試、debug、observability 都比 application 層差；邏輯放 DB 內、CI / 單元測試 / log / APM 都接不上。</li>
<li>不需要原子性、或跨 partition — 跨 partition 的協調用 application 層 workflow 或 saga、stored procedure 做不到。</li>
<li>寫入後的非同步工作（投影、通知、同步）— 用 <a href="../change-feed-cdc/">Change Feed</a> 解耦、不要塞進 stored procedure 拖長寫入路徑。</li>
<li>衍生欄位 / 計算 — 簡單的放 application 層或 pre-trigger、複雜的不要進 DB 邏輯。</li>
</ul>
<p>判讀句：stored procedure 的正當理由幾乎只有「partition-scoped atomicity」與「批次 round-trip 縮減」。看到「想把業務規則集中到 DB」「想讓 DB 自動做某件事」這類動機、優先回 application 層 — server-side JavaScript 的維護成本長期高於它省下的東西。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="期待跨-partition-交易">期待跨 partition 交易</h3>
<p>team 把多個不同 partition key 的寫入放進一個 stored procedure、期待原子性。procedure 對非當前 partition 的操作會失敗。徵兆是「跨用戶 / 跨類別的原子操作報錯或部分寫入」。修法是重新設計 partition key 讓相關資料同 partition（若業務允許）、或改用 application 層補償 / saga workflow 處理跨 partition 一致性。</p>
<h3 id="沒處理-bounded-execution">沒處理 bounded execution</h3>
<p>批次 stored procedure 假設「一次呼叫處理完所有 document」、資料量大時被中止、只處理了一部分、client 以為全做完。徵兆是大 partition 上批次操作結果不完整、且沒有錯誤（procedure 被 bounded execution 截斷但回傳了部分成功）。修法是實作 continuation 模式、每個操作檢查 <code>accepted</code>、回傳已處理數、client loop 直到完成。</p>
<h3 id="把可變業務邏輯固化進-stored-procedure">把可變業務邏輯固化進 stored procedure</h3>
<p>把定價規則、折扣計算、狀態機這類會變的邏輯寫進 JavaScript stored procedure、之後每次改規則都要改 DB resource、無法走正常 application CI / code review / 測試流程、且 production debug 缺 log。徵兆是「改一個業務規則要動 DB、且改完不確定對不對」。修法是把邏輯搬回 application 層、stored procedure 只保留無法在 application 層做的 partition-scoped atomicity。</p>
<h3 id="依賴-trigger-自動執行">依賴 trigger 自動執行</h3>
<p>從關聯式 DB 過來的 team 假設 trigger 像 SQL trigger 一樣自動跑、寫了 audit / 補欄位的 trigger 卻發現大部分寫入沒觸發 — 因為 Cosmos DB trigger 必須 per-request 指定。徵兆是「trigger 有時跑有時不跑」、實際是只有明確帶 trigger 的 request 才跑。修法是確認所有相關寫入路徑都指定 trigger、或把「必須每次都做」的邏輯放 application 層 / pre-trigger 並在 SDK wrapper 統一帶上。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：stored procedure 執行的 <code>x-ms-request-charge</code>（整個交易的總 RU）、執行例外率、bounded execution 中止比例</li>
<li>成本：一個掃多 document 的 procedure 可能比等量單筆操作貴、且 continuation 多次呼叫累加 — 把它當「一個複合操作的總 RU」進容量公式、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>observability gap：stored procedure 內部沒有 application APM / structured log、debug 靠回傳 body 與例外訊息 — 這個 gap 本身是「邏輯不該放這裡」的訊號之一</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：partition-scoped transaction 的 RU 要算進該 partition 的 budget、熱門 partition 上跑重 procedure 會放大 hot partition、見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li>Alert：stored procedure 例外率上升、執行 RU 異常偏高、bounded execution 截斷比例升高</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../change-feed-cdc/">change-feed-cdc</a>（寫入後的非同步工作走 Change Feed、不要塞 stored procedure）、<a href="../partition-key-design/">partition-key-design</a>（transaction 邊界 = partition 邊界、跨 partition 原子需求要重設計 partition key）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（複合交易的 RU 估算）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（partition 內原子性 vs 跨 session consistency 是兩個不同議題）</li>
<li>跟 Spanner 對照：需要 <em>跨 partition / 全域</em> ACID 交易時、Cosmos DB stored procedure 做不到 — 轉 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 或 Aurora DSQL</li>
<li>跟 DynamoDB 對照：DynamoDB 的 TransactWriteItems 提供跨 item（含跨 partition、有上限）的交易、語義跟 Cosmos DB 的 single-partition stored procedure 不同 — 從 DynamoDB transaction 過來的 team 要注意 Cosmos DB 沒有等價的開箱跨 partition 交易、見 <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 vendor</a></li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「跨 partition transaction 要改 workflow / stored procedure 邊界」</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 stored procedure / trigger backlog 的深度展開</li>
<li><a href="../change-feed-cdc/">change-feed-cdc</a> — 寫入後非同步工作的對照路徑</li>
<li><a href="../partition-key-design/">partition-key-design</a> — transaction 邊界 = partition 邊界</li>
<li><a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> — 複合交易 RU 估算</li>
<li><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> / <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 vendor</a> — 跨 partition 交易能力對照</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> — 熱 partition 上的重交易放大效應</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Stored procedures, triggers, and UDFs</a></li>
</ul>
]]></content:encoded></item><item><title>7.2 身分與授權邊界</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/</guid><description>&lt;p>本章的責任是把「誰可以做什麼」拆成可驗證的邊界模型，讓團隊在功能上線前就能判讀身分擴散與授權濫用風險。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦概念層判讀，主體是問題節點、訊號、風險與路由條件。案例在問題被觸發時提供證據參考，不作章節主體。&lt;/p>
&lt;h2 id="讀者路由">讀者路由&lt;/h2>
&lt;p>個人自架工具場景（從 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 導過來）直接看&lt;a href="#%e5%96%ae%e4%ba%ba%e8%a3%9d%e7%bd%ae%e8%aa%8d%e8%ad%89%e6%a8%a1%e5%9e%8b">單人裝置認證模型&lt;/a>段。多人 SaaS 場景從&lt;a href="#%e8%ba%ab%e5%88%86%e8%88%87%e6%8e%88%e6%ac%8a%e9%82%8a%e7%95%8c%e6%a8%a1%e5%9e%8b">身分與授權邊界模型&lt;/a>段開始。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：credential brute force / credential stuffing / phishing 與 MFA fatigue / privilege escalation / session hijacking / 供應商身分鏈傳導 / insider abuse / 過寬授權範圍 / 單人裝置認證邊界轉移。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>入口暴露面 → &lt;a href="../entrypoint-and-server-protection/">7.3&lt;/a>&lt;/li>
&lt;li>資料外洩 → &lt;a href="../data-protection-and-masking-governance/">7.4&lt;/a>&lt;/li>
&lt;li>傳輸 / 憑證信任 → &lt;a href="../transport-trust-and-certificate-lifecycle/">7.5&lt;/a>&lt;/li>
&lt;li>機器憑證 → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.10&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../detection-coverage-and-signal-governance/">7.13&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[authentication]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="身分與授權邊界模型">身分與授權邊界模型&lt;/h2>
&lt;p>身分邊界的核心責任是定義「登入主體是否可信」，授權邊界的核心責任是定義「可信主體可以觸及哪些能力」。兩者需要分開治理，才能避免認證成功就直接等於高權限存取。&lt;/p>
&lt;ol>
&lt;li>身分層：驗證主體真實性與登入情境風險，重點是強認證、裝置信任、異常行為判讀。&lt;/li>
&lt;li>授權層：驗證操作是否符合最小權限，重點是 scope、角色、資源邊界與操作條件。&lt;/li>
&lt;li>授權有時間邊界 — 會話層驗證授權是否在有效時窗內，重點是 token 壽命、失效節奏與事件後收斂。&lt;/li>
&lt;li>信任不止內部 — 供應商層驗證第三方身分鏈是否可控，重點是外部事件後的內部權限收斂能力。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「身分異常」快速轉成「控制面動作」。&lt;/p>
&lt;ol>
&lt;li>先判斷異常發生在身分層、授權層、會話層或供應商層。&lt;/li>
&lt;li>再判斷是單點異常還是可擴散異常。&lt;/li>
&lt;li>接著啟動對應收斂動作：限制登入、縮權、失效會話、停用外部 token。&lt;/li>
&lt;li>最後交接到部署、可靠性與 incident workflow，讓處置可追蹤且可驗證。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>登入驗證節奏失衡&lt;/td>
 &lt;td>異常驗證密度、異常地理切換、連續高風險操作&lt;/td>
 &lt;td>身分擴散速度提升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 incident response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>授權範圍擴張過快&lt;/td>
 &lt;td>高權限操作集中、代理操作鏈過長&lt;/td>
 &lt;td>權限濫用影響面擴大&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">least-privilege&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 incident response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>會話失效節奏落後&lt;/td>
 &lt;td>修補後異常 session 持續、token 存續過久&lt;/td>
 &lt;td>事件關閉時間延長&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 + 05&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應商身分鏈傳導&lt;/td>
 &lt;td>外部事件後內部憑證存續比例偏高&lt;/td>
 &lt;td>內部信任邊界承受外部衝擊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 + 06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>單人裝置認證邊界轉移&lt;/td>
 &lt;td>device 失竊後生物辨識可繞過、共享密鑰存本機、無中央會話可遠端失效&lt;/td>
 &lt;td>認證邊界落在 device 層、單點失效即全失效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a>、裝置綁定 + 共享密鑰&lt;/td>
 &lt;td>&lt;code>05 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨章-ssot供應商身分鏈傳導">跨章 SSoT：供應商身分鏈傳導&lt;/h2>
&lt;p>本章「供應商身分鏈傳導」問題節點是跨章 SSoT——其他章節從不同 layer 補同議題的 specific 訊號：&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把「誰可以做什麼」拆成可驗證的邊界模型，讓團隊在功能上線前就能判讀身分擴散與授權濫用風險。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦概念層判讀，主體是問題節點、訊號、風險與路由條件。案例在問題被觸發時提供證據參考，不作章節主體。</p>
<h2 id="讀者路由">讀者路由</h2>
<p>個人自架工具場景（從 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 導過來）直接看<a href="#%e5%96%ae%e4%ba%ba%e8%a3%9d%e7%bd%ae%e8%aa%8d%e8%ad%89%e6%a8%a1%e5%9e%8b">單人裝置認證模型</a>段。多人 SaaS 場景從<a href="#%e8%ba%ab%e5%88%86%e8%88%87%e6%8e%88%e6%ac%8a%e9%82%8a%e7%95%8c%e6%a8%a1%e5%9e%8b">身分與授權邊界模型</a>段開始。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：credential brute force / credential stuffing / phishing 與 MFA fatigue / privilege escalation / session hijacking / 供應商身分鏈傳導 / insider abuse / 過寬授權範圍 / 單人裝置認證邊界轉移。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>入口暴露面 → <a href="../entrypoint-and-server-protection/">7.3</a></li>
<li>資料外洩 → <a href="../data-protection-and-masking-governance/">7.4</a></li>
<li>傳輸 / 憑證信任 → <a href="../transport-trust-and-certificate-lifecycle/">7.5</a></li>
<li>機器憑證 → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a> → <a href="../workload-identity-and-federated-trust/">7.10</a></li>
<li>偵測訊號 → <a href="../detection-coverage-and-signal-governance/">7.13</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[authentication]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="身分與授權邊界模型">身分與授權邊界模型</h2>
<p>身分邊界的核心責任是定義「登入主體是否可信」，授權邊界的核心責任是定義「可信主體可以觸及哪些能力」。兩者需要分開治理，才能避免認證成功就直接等於高權限存取。</p>
<ol>
<li>身分層：驗證主體真實性與登入情境風險，重點是強認證、裝置信任、異常行為判讀。</li>
<li>授權層：驗證操作是否符合最小權限，重點是 scope、角色、資源邊界與操作條件。</li>
<li>授權有時間邊界 — 會話層驗證授權是否在有效時窗內，重點是 token 壽命、失效節奏與事件後收斂。</li>
<li>信任不止內部 — 供應商層驗證第三方身分鏈是否可控，重點是外部事件後的內部權限收斂能力。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「身分異常」快速轉成「控制面動作」。</p>
<ol>
<li>先判斷異常發生在身分層、授權層、會話層或供應商層。</li>
<li>再判斷是單點異常還是可擴散異常。</li>
<li>接著啟動對應收斂動作：限制登入、縮權、失效會話、停用外部 token。</li>
<li>最後交接到部署、可靠性與 incident workflow，讓處置可追蹤且可驗證。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>登入驗證節奏失衡</td>
          <td>異常驗證密度、異常地理切換、連續高風險操作</td>
          <td>身分擴散速度提升</td>
          <td><a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a>、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity</a></td>
          <td><code>08 incident response</code></td>
      </tr>
      <tr>
          <td>授權範圍擴張過快</td>
          <td>高權限操作集中、代理操作鏈過長</td>
          <td>權限濫用影響面擴大</td>
          <td><a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">least-privilege</a></td>
          <td><code>08 incident response</code></td>
      </tr>
      <tr>
          <td>會話失效節奏落後</td>
          <td>修補後異常 session 持續、token 存續過久</td>
          <td>事件關閉時間延長</td>
          <td><a href="/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation</a>、<a href="/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation</a></td>
          <td><code>08 + 05</code></td>
      </tr>
      <tr>
          <td>供應商身分鏈傳導</td>
          <td>外部事件後內部憑證存續比例偏高</td>
          <td>內部信任邊界承受外部衝擊</td>
          <td><a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a>、<a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a></td>
          <td><code>08 + 06</code></td>
      </tr>
      <tr>
          <td>單人裝置認證邊界轉移</td>
          <td>device 失竊後生物辨識可繞過、共享密鑰存本機、無中央會話可遠端失效</td>
          <td>認證邊界落在 device 層、單點失效即全失效</td>
          <td><a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a>、裝置綁定 + 共享密鑰</td>
          <td><code>05 + 08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="跨章-ssot供應商身分鏈傳導">跨章 SSoT：供應商身分鏈傳導</h2>
<p>本章「供應商身分鏈傳導」問題節點是跨章 SSoT——其他章節從不同 layer 補同議題的 specific 訊號：</p>
<ul>
<li><a href="../transport-trust-and-certificate-lifecycle/">7.5 第三方信任重評估延遲</a>：傳輸層的 specific 訊號（憑證收斂滯後）</li>
<li><a href="../secrets-and-machine-credential-governance/">7.6 供應商事件傳導未收斂</a>：機器憑證層的 specific 訊號（憑證仍活躍）</li>
<li><a href="../workload-identity-and-federated-trust/#%e7%ac%ac%e4%b8%89%e6%96%b9%e6%8e%88%e6%ac%8a%e7%af%84%e5%9c%8d%e8%b7%9f%e4%ba%8b%e4%bb%b6%e5%82%b3%e5%b0%8e%e5%8d%8a%e5%be%91">7.10 第三方授權範圍跟事件傳導半徑</a>：workload identity 層的 specific 訊號（<a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> token scope 過寬）</li>
</ul>
<p>本章視角聚焦客戶側人類身分鏈收斂責任；workload identity 層的 federation token scope 視角見 7.10。跨章 audit 時、本條為 canonical 定義（threat scope / mitigation chain），其他章補 layer 視角差異。</p>
<h2 id="mfa-fatigue-與-step-up-驗證">MFA fatigue 與 step-up 驗證</h2>
<p>MFA fatigue 是身分層擴散風險的代表機制：登入挑戰可被使用者連續同意，攻擊者把「使用者誤點」當成唯一所需的人類動作。要解這個機制要拉開兩層判讀，登入層放強認證、操作層放 step-up 驗證，避免認證成功直接等於高權限存取。</p>
<p>對應 <a href="../red-team/cases/identity-access/uber-2022-mfa-fatigue/">Uber 2022</a>：揭露三個失效控制面 — 高風險登入路徑缺 step-up、內部工具授權邊界不足（初始落點可快速擴散）、身分異常事件與值班告警串接不足。案例的「可落地檢查點」段把對應 mechanism 標明為 phishing-resistant 強認證（WebAuthn / passkey）+ 裝置信任綁定（managed device / posture check）、屬於案例直接可引用範圍。</p>
<p>以下基於通用工程知識補充：強認證跟裝置綁定是 mechanism 雙軌、缺一不可。只做強認證不綁裝置、攻擊者仍可在受感染端點繼承會話；只綁裝置不強化認證、社交工程仍可繞過。判讀升級條件是「短時間 MFA 請求密度異常」要走 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 升級、不是當一般使用者支援處理。</p>
<h2 id="高權限工具的會話收斂節奏">高權限工具的會話收斂節奏</h2>
<p>身分被取得後、token 撤銷跟 session kill 的時間窗口直接決定攻擊者可觸及的資產面積、是初始落點橫向擴散的關鍵節流點。這層治理跟登入驗證是兩條獨立 chain，前者管「入場」、後者管「停留」。會話收斂節奏的 canonical 在 <a href="../transport-trust-and-certificate-lifecycle/#%e6%9c%83%e8%a9%b1%e9%87%8d%e6%94%be%e8%b7%9f%e5%85%a8%e5%9f%9f%e5%a4%b1%e6%95%88canonical">7.5 § 會話重放跟全域失效</a>、本節從身分層補 token 撤銷窗口的 specific 訊號。</p>
<p>對應 <a href="../red-team/cases/identity-access/slack-2022-token-compromise/">Slack 2022</a>：揭露三層失效控制面 — 員工身分遭濫用後的隔離速度不足、token 範圍與用途邊界定義不夠細緻、程式碼資產存取異常訊號未快速匯流。本段聚焦的會話收斂視角直接對應前兩層、訊號匯流層放 <a href="../audit-trail-and-accountability-boundary/">7.7 audit-trail</a> 處理。案例「可落地檢查點」列出 mechanism 為「管理 token 分域並限制到最小權限、依用途切 audience」，並標明前提是「token 有 inventory 可查 issuer / scope」。</p>
<p>以下基於通用工程知識補充：token 分域要看可達的 trust boundary、權限等級只是其中一個維度。同樣是「管理 token」、跨多敏感系統的單一 token 跟限定單一 audience 的 token、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 差兩個數量級。日常治理要建立 token inventory（issuer / scope / blast radius 標籤）、事件時可直接按 blast radius 降序撤銷；inventory 缺位時排序退回 ad-hoc 判斷、容易把可用性跟風險同時打斷。</p>
<h2 id="第三方身分鏈的內部收斂責任">第三方身分鏈的內部收斂責任</h2>
<p>第三方身分鏈傳導的控制責任由客戶側承擔。當供應商公開事件、內部要有獨立 runbook 讓「閱讀公告」直接 trigger「全域 token 盤點 + 分批輪替」、停留在資訊接收層會把外部風險變成內部事故。這個收斂節奏的快慢、決定供應商事件能維持在「外部新聞」、或升級成「內部事故」。</p>
<p>對應 <a href="../red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/">Okta + Cloudflare 2023</a>：揭露支援工作流層三層失效控制面 — 支援資料流沒被視為高敏感資產、憑證或會話資料生命周期管理不足、供應商事件到客戶內部輪替流程沒有強制觸發。同事件鏈的 <a href="../red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/">Cloudflare 2023 follow-through</a> 從客戶側補另外三層 — 供應商事件觸發條件與內部 runbook 連動不足、高權限 token 失效與輪替策略準備度不足、受影響資產盤點與證據保存流程分離。CF follow-through 案例「可落地檢查點」標明 mechanism 是讓供應商公告直接 trigger 內部盤點，並要求「輪替能力涵蓋第三方授權 token、不只內部 session」。</p>
<p>以下基於通用工程知識補充：第三方事件的判讀盲點是把控制責任當成廠商的事。廠商只能處理供應商側、客戶側的 token / session / 憑證仍是各組織自己的責任面。內部 runbook 要把「廠商公告」「客戶側盤點」「依範圍輪替」綁成一條 chain、不分先後執行；如果三件事都要等「下一步指引」、控制節奏會比攻擊節奏慢。</p>
<h2 id="單人裝置認證模型">單人裝置認證模型</h2>
<p>單人自用工具（遠端操控自己的主機、家庭自動化、個人備份）的認證不走 web-auth 光譜。沒有中央使用者資料庫、沒有 SSO、主體就是持有裝置的所有者，認證拆成兩層獨立 mechanism：</p>
<ol>
<li>裝置層：裝置原生生物辨識（Face ID / BiometricPrompt）認「人」、防的是裝置遺失後被他人直接操作。這一層沒有「異常驗證密度」「地理切換」的概念 — 判讀對象是裝置是否仍由所有者持有、不是 login anomaly。</li>
<li>連線層：app 與服務端共享密鑰認「連線」、防的是拿到入口位址的外人。密鑰存裝置安全儲存（Keychain / Keystore）、不硬寫進 app（反編譯可挖）、配對走實體隔離通道（不經網路、改用 QR 掃描等實體方式傳輸密鑰）。</li>
</ol>
<p>失效模型跟多人 SaaS 的「會話失效」不同。裝置失竊等於認證邊界整個失效（生物辨識可被繞過、共享密鑰就在本機）、且沒有中央會話可以遠端 kill;唯一的收斂手段是服務端輪替密鑰版本、讓舊裝置的密鑰失效（強迫重新配對）。所以前置控制面是「密鑰版本可遠端輪替」加「裝置清單」、而不是 session TTL。交接到 <code>05</code>（部署要支援密鑰版本變更的同步）與 <code>08</code>（事故時的裝置清查）。</p>
<p>這個模型的 tripwire 是使用者數從一變多。共享密鑰無法分辨是哪個使用者、生物辨識綁在單一裝置、沒有帳號就無法個別撤銷;第一個要分享存取的對象出現時、認證模型要升級回帳號系統。應用場景的判斷見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/#%e5%80%8b%e4%ba%ba%e8%87%aa%e6%9e%b6%e5%b7%a5%e5%85%b7%e5%b8%b8%e9%a7%90%e6%9c%ac%e6%a9%9f%e7%84%a1%e5%b0%8d%e5%a4%96%e6%9c%8d%e5%8b%99" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 個人自架工具</a>。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時需要從一般維運升級到事件處置。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>應視為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一身分在短時間跨區、跨裝置、跨高權限路徑操作</td>
          <td>可擴散事件</td>
      </tr>
      <tr>
          <td>高權限代理操作沒有獨立審核或時間限制</td>
          <td>授權模型失衡</td>
      </tr>
      <tr>
          <td>修補或公告後仍有舊 token 持續可用</td>
          <td>會話收斂失敗</td>
      </tr>
      <tr>
          <td>供應商事件後內部權限沒有分域回收</td>
          <td>外部風險傳導未隔離</td>
      </tr>
  </tbody>
</table>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是提供反向驗證，確認控制面是否足夠。</p>
<ul>
<li>MFA 疲勞與內部工具擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>第三方身分鏈事件： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a></li>
<li>token 事件後橫向擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>多人 SaaS 場景</strong>：</p>
<ul>
<li>入口與平台實體：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a></li>
<li>驗證與回復節奏：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性</a></li>
<li>事件分級與收斂：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故回應</a></li>
</ul>
<p><strong>個人自架工具場景</strong>：</p>
<ul>
<li>回 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a> 確認 tunnel 之後的認證疊法</li>
<li>進 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 做入口威脅建模</li>
<li>判斷服務形態：回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a></li>
</ul>
]]></content:encoded></item><item><title>Partition</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/partition/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/partition/</guid><description>&lt;p>Partition 的核心概念是「把事件流切分成多個可並行處理的片段」。同一 partition 內保留順序，不同 partition 可以平行處理。Partition 數量決定 consumer 的最大並行度 — 一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 中 consumer 數量不能超過 partition 數量。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Partition 是 throughput、ordering 與 hot key 之間的取捨核心。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 的關係是：topic 是邏輯分類（order events、payment events），partition 是 topic 內的物理分片。Partition key 決定同一類事件會落到哪個 partition；選錯 key 會造成 hot partition（單一 partition 過載）或讓需要順序的事件被拆散。&lt;/p>
&lt;p>在 Kafka 中 partition 是一級概念；RabbitMQ 沒有原生 partition（用多個 queue + consistent hash exchange 模擬）；SQS 沒有顯式 partition（內部自動分片）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 partition 設計的訊號是事件量大且需要水平擴展處理能力。訂單事件可以用 order_id 作為 partition key，讓同一訂單的事件保留順序；若所有高流量商家的訂單都 hash 到同一個 partition，會形成 hot partition。&lt;/p>
&lt;p>Partition 數量也影響 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 管理的複雜度 — 每個 partition 有獨立的 offset，consumer group 的 rebalance 要重新分配 partition ownership。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Partition 設計要定義 partition key（通常是業務實體 ID）、partition 數量（建議初期設多一點，Kafka partition 數量只能增加不能減少）、順序需求（同 key 保序 vs 全域保序）與 lag 觀測（per-partition lag 能定位 hot partition）。重新分 partition 可能影響順序、consumer group 配置與 replay 範圍。&lt;/p></description><content:encoded><![CDATA[<p>Partition 的核心概念是「把事件流切分成多個可並行處理的片段」。同一 partition 內保留順序，不同 partition 可以平行處理。Partition 數量決定 consumer 的最大並行度 — 一個 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 中 consumer 數量不能超過 partition 數量。</p>
<h2 id="概念位置">概念位置</h2>
<p>Partition 是 throughput、ordering 與 hot key 之間的取捨核心。它跟 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 的關係是：topic 是邏輯分類（order events、payment events），partition 是 topic 內的物理分片。Partition key 決定同一類事件會落到哪個 partition；選錯 key 會造成 hot partition（單一 partition 過載）或讓需要順序的事件被拆散。</p>
<p>在 Kafka 中 partition 是一級概念；RabbitMQ 沒有原生 partition（用多個 queue + consistent hash exchange 模擬）；SQS 沒有顯式 partition（內部自動分片）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 partition 設計的訊號是事件量大且需要水平擴展處理能力。訂單事件可以用 order_id 作為 partition key，讓同一訂單的事件保留順序；若所有高流量商家的訂單都 hash 到同一個 partition，會形成 hot partition。</p>
<p>Partition 數量也影響 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 管理的複雜度 — 每個 partition 有獨立的 offset，consumer group 的 rebalance 要重新分配 partition ownership。</p>
<h2 id="設計責任">設計責任</h2>
<p>Partition 設計要定義 partition key（通常是業務實體 ID）、partition 數量（建議初期設多一點，Kafka partition 數量只能增加不能減少）、順序需求（同 key 保序 vs 全域保序）與 lag 觀測（per-partition lag 能定位 hot partition）。重新分 partition 可能影響順序、consumer group 配置與 replay 範圍。</p>
]]></content:encoded></item><item><title>從 MongoDB / Cassandra 遷入 Cosmos DB：protocol-compat API drop-in vs native API paradigm shift、相容性邊界與 dual-write cutover</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/migrate-from-mongodb-cassandra/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/migrate-from-mongodb-cassandra/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 migration playbook、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。從 MongoDB 或 Cassandra 遷入 Cosmos DB 的核心決策是 &lt;em>選哪條路徑&lt;/em> — 用 Cosmos 的 protocol-compat API（MongoDB API / Cassandra API）做 wire-protocol drop-in、driver 與 query 大致不動；還是換 native SQL API、把 application 重寫成 Cosmos native paradigm。這兩條路的 diff 維度、風險、不可逆性都不同、是一個 multi-element 的 migration 規劃。本文先把 driver 與 no-go 講清楚、再做 6 維 diff audit 分出兩條路徑、再進各自的 phase plan、evidence 與 cutover。&lt;/p>
&lt;p>API &lt;em>選擇判斷&lt;/em> 本身（MongoDB API vs SQL API 的四層 framing、dogfood signal、multi-model、跨雲 hedging）由 &lt;a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api&lt;/a> 主寫、本文不重複展開那層對比；本文主寫 &lt;em>遷移流程&lt;/em> — 選定路徑後怎麼安全把資料與流量搬過去。&lt;/p>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>（MongoDB → Cosmos DB MongoDB API、planet-scale、dogfood）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a>（自管 → Atlas、6 個月、同 DB 換託管的時程對照）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a>（保留 MongoDB 補周邊、對照「不一定要遷」）。Microsoft 365 case 自承沒揭露 throughput / latency / cost 數字、本文不拿它當 benchmark、只取遷移路徑 frame。&lt;/p>
&lt;h2 id="driver為什麼遷什麼條件不遷">Driver：為什麼遷、什麼條件不遷&lt;/h2>
&lt;p>有效的遷移 driver 不是「Cosmos DB 比較好」、而是具體壓力：team 已綁 Azure 生態、需要 turnkey global distribution、自管 MongoDB / Cassandra cluster 的 ops 負擔要轉移、或需要 multi-model 把多個 NoSQL 集中治理。Microsoft 365 的 driver 是 planet-scale 全球分散 + Azure dogfood、不是 query 性能。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 migration playbook、寫作參照 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。從 MongoDB 或 Cassandra 遷入 Cosmos DB 的核心決策是 <em>選哪條路徑</em> — 用 Cosmos 的 protocol-compat API（MongoDB API / Cassandra API）做 wire-protocol drop-in、driver 與 query 大致不動；還是換 native SQL API、把 application 重寫成 Cosmos native paradigm。這兩條路的 diff 維度、風險、不可逆性都不同、是一個 multi-element 的 migration 規劃。本文先把 driver 與 no-go 講清楚、再做 6 維 diff audit 分出兩條路徑、再進各自的 phase plan、evidence 與 cutover。</p>
<p>API <em>選擇判斷</em> 本身（MongoDB API vs SQL API 的四層 framing、dogfood signal、multi-model、跨雲 hedging）由 <a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> 主寫、本文不重複展開那層對比；本文主寫 <em>遷移流程</em> — 選定路徑後怎麼安全把資料與流量搬過去。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（MongoDB → Cosmos DB MongoDB API、planet-scale、dogfood）、<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas、6 個月、同 DB 換託管的時程對照）、<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（保留 MongoDB 補周邊、對照「不一定要遷」）。Microsoft 365 case 自承沒揭露 throughput / latency / cost 數字、本文不拿它當 benchmark、只取遷移路徑 frame。</p>
<h2 id="driver為什麼遷什麼條件不遷">Driver：為什麼遷、什麼條件不遷</h2>
<p>有效的遷移 driver 不是「Cosmos DB 比較好」、而是具體壓力：team 已綁 Azure 生態、需要 turnkey global distribution、自管 MongoDB / Cassandra cluster 的 ops 負擔要轉移、或需要 multi-model 把多個 NoSQL 集中治理。Microsoft 365 的 driver 是 planet-scale 全球分散 + Azure dogfood、不是 query 性能。</p>
<p>No-go condition（這些情況不該遷入 Cosmos DB）：</p>
<ul>
<li>跨雲是核心需求 — Cosmos DB 只在 Azure；跨雲彈性高於 Azure 整合時、MongoDB 留 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">Atlas</a>（Forbes 路徑、跨 AWS / GCP / Azure）、Cassandra 留自管或 ScyllaDB。</li>
<li>需要 native MongoDB / Cassandra 最新 feature — Cosmos DB 的 protocol-compat API server version 落後原生、且部分 feature 行為不同。</li>
<li>未來雲商策略未定 — hedging 價值高於當下整合、見 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 的退出成本。</li>
<li>現有 cluster 補周邊就夠用 — Coinbase 保留 MongoDB 加 proxy / cache / predictive scaling、沒遷出。遷移成本高、先確認「補周邊」解不了問題再遷。</li>
</ul>
<h2 id="diff-audit6-維度分出兩條路徑">Diff audit：6 維度分出兩條路徑</h2>
<p>source（MongoDB / Cassandra）與 target（Cosmos DB）的差異按 6 維度盤點、兩條路徑的維度高低不同、這也是 type 判定的依據。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>protocol-compat API（MongoDB / Cassandra API）</th>
          <th>native SQL API</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low — document / table shape 大致保留</td>
          <td>Medium — 重新建模成 Cosmos native document</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High — 自管 cluster → managed RU/s + region</td>
          <td>High — 同左</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low — 仍 document / wide-column 語意</td>
          <td>High — 換 query 模型、index policy、RU 思維</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Medium — driver 保留、aggregation / CQL 部分要改</td>
          <td>High — driver、query layer、ORM 全換</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>Medium — connection string、auth、consistency 對應</td>
          <td>High — 整個 data access layer 重寫</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>High — replica set / ring → partition + multi-region</td>
          <td>High — 同左</td>
      </tr>
  </tbody>
</table>
<p>主導差異決定 type：</p>
<ul>
<li>protocol-compat 路徑 — 最大差異是 operational 與 data topology、paradigm 維持 Low、是 wire-compat 的 drop-in 但有相容 gap。對應 <strong>Type B drop-in（partial）</strong>：driver 不換、但每個 query pattern 要驗證相容性、不是無腦切換。</li>
<li>native API 路徑 — paradigm High + application High、是 <strong>Type E paradigm shift</strong>：不只搬資料、要重寫 application 的整個 data access layer。</li>
</ul>
<p>判讀句：protocol-compat 是「換底層儲存與運維、保留 query 介面」、native API 是「連 query 範式一起換」。多數遷移先走 protocol-compat 把資料與 ops 搬過去、native API 是後續若要拿完整 Cosmos feature（Change Feed、stored procedure 原生支援、SQL API query）才考慮的二次遷移 — 一次到位 native API 的工程複雜度與風險顯著更高。</p>
<h3 id="cassandra-路徑的專屬差異">Cassandra 路徑的專屬差異</h3>
<p>Cassandra → Cosmos DB Cassandra API 跟 MongoDB 路徑有一個關鍵不同：Cassandra 的資料建模是 <em>query-driven</em>（partition key + clustering key 對應 access pattern）、這套建模思維跟 Cosmos DB 的 logical partition 概念部分對齊、但 Cosmos DB 的 per-partition RU 上限（目前約 10,000 RU/s、vendor 規格、實作時 cross-verify Azure doc 當前值）與 RU 計費會讓原本 Cassandra 上「寬 partition + 大量 clustering row」的設計變成 hot partition 風險。CQL 的 consistency level（QUORUM / LOCAL_ONE 等）要對應到 Cosmos DB 的 5 個 consistency level、語義不是一對一、見 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a>。Cassandra 的 secondary index / materialized view 在 Cassandra API 的支援度要逐項驗證（時間敏感、查文件）。</p>
<h2 id="phase-plan">Phase plan</h2>
<p>兩條路徑共用大架構、protocol-compat 的相容 audit 較輕、native API 多一段 application 重寫。</p>
<h3 id="protocol-compat-路徑type-b-drop-in">protocol-compat 路徑（Type B drop-in）</h3>
<ul>
<li>Phase 0：相容性 audit — 把 production query / aggregation pipeline（MongoDB）或 CQL statement（Cassandra）拉出來、逐條對照 Cosmos DB 對應 API 的 <a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60">feature support</a> 清單、列出 unsupported 與行為不同的部分。</li>
<li>Phase 1：partition key 設計 — MongoDB shard key / Cassandra partition key 翻譯成 Cosmos logical partition key、檢查 10,000 RU/s 上限與 hot partition 風險、見 <a href="../partition-key-design/">partition-key-design</a>。</li>
<li>Phase 2：bulk export-import — 初始資料用 Data Migration Tool / mongodump / sstable export 灌入。</li>
<li>Phase 3：CDC sync — source 的持續變更（MongoDB oplog / Cassandra CDC）同步到 Cosmos DB、收斂初始 load 後的增量。</li>
<li>Phase 4：shadow read — production query 在兩邊各跑一遍、對 result checksum、量 Cosmos 端 RU baseline、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</li>
<li>Phase 5：read cutover — 讀切 Cosmos、寫仍 source（可回退）。</li>
<li>Phase 6：write cutover — 寫切 Cosmos。</li>
<li>Phase 7：cleanup — 退役 source cluster、保留 export 與最終 checksum。</li>
</ul>
<h3 id="native-api-路徑type-e-paradigm-shift多出的工作">native API 路徑（Type E paradigm shift）多出的工作</h3>
<p>native API 路徑在 Phase 0 與 Phase 1 之間插入 <em>application 重寫 stream</em>、與資料遷移 stream 並行：</p>
<ul>
<li>重新建模 document（從 MongoDB document / Cassandra table 設計 Cosmos native shape、決定 embed vs reference）</li>
<li>重寫 data access layer（換掉 MongoDB driver / CQL、改用 Cosmos SQL API SDK、重寫所有 query）</li>
<li>重寫 aggregation（Cosmos SQL API 沒有 JOIN、aggregation 模型不同、部分邏輯移到 application 或用 stored procedure / Change Feed 物化）</li>
</ul>
<p>這條 application stream 是 native API 路徑的主要風險與工期來源、必須跟資料遷移 stream 用獨立 owner 並行、shadow read 階段要對 <em>重寫後的 query</em> 與 <em>原 query</em> 的結果一致性、不只是資料一致性。</p>
<h3 id="時程現實">時程現實</h3>
<p>Forbes 同 DB 換託管（自管 → Atlas、paradigm 不變）用 6 個月、中型團隊多 squad 並行。protocol-compat 遷入 Cosmos DB 的工程複雜度高於 Forbes 型（多了 RU / partition / region 範式與相容 gap）、native API 路徑再高一個量級（加 application 重寫）。拿 Forbes 6 個月當 native API 路徑 baseline 會從第一天 over-commit。</p>
<h2 id="evidence">Evidence</h2>
<p>每個 phase 用資料證明可前進、不靠感覺：</p>
<ul>
<li>Phase 0：unsupported feature 清單已窮舉、每條有對應策略（改寫 / 移 application 層 / 接受降級）</li>
<li>Phase 2-3：row / document count 對齊、CDC replication lag 收斂到穩定</li>
<li>Phase 4：query result checksum 一致（protocol-compat 比原 query 結果；native API 比重寫 query 與原 query 結果）、RU baseline 量到、aggregation result 逐條對齊</li>
<li>Phase 5-6：error rate、p99 latency、RU consumption 在 cutover 後在預期範圍</li>
<li>對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">schema-migration-rollout-evidence</a> 的 dual-write 驗證</li>
</ul>
<h2 id="cutover">Cutover</h2>
<ul>
<li>read cutover window：先切讀、寫留 source、Cosmos 端 read error rate 與 latency 達標再進 write cutover</li>
<li>write cutover window：read-only freeze &lt; 10 分鐘、切寫、最終 checksum 對齊</li>
<li>Rollback condition：query error rate 超過閾值（如 &gt; 1%）、RU consumption 顯著高於估算（protocol-compat 翻譯層 overhead 比預期高）、或 result mismatch — 任一成立回退到 source、對應 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a></li>
<li>decision owner：cutover 期間誰有權回退要事前定、資料庫切流失敗代價高、不靠臨場判斷</li>
<li>不可逆點：API kind 是 account 層、建 account 時選定、無法事後切換 — protocol-compat 與 native API 是 <em>兩個不同 account</em>；選 protocol-compat 後想升 native API 是 export → 新 account → import + 重寫 application 的二次全量遷移、不是 in-place 升級。這個不可逆性要在 Phase 0 就決定方向、不能 cutover 後反悔</li>
</ul>
<h2 id="cleanup">Cleanup</h2>
<ul>
<li>退役 source cluster 前確認最終 checksum、保留 export dump 90 天作為 rollback 後路</li>
<li>移除 dual-write writer、CDC connector、shadow read harness</li>
<li>保留 RU baseline 與 partition 分布觀測進 production dashboard、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>incident write-back：把相容 gap 與翻譯層成本意外寫回 runbook、給未來同類遷移</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="假設-wire-compat--100-行為相同">假設 wire-compat = 100% 行為相同</h3>
<p>protocol-compat API 是「在某些 query pattern 下相容」、不是普遍相容。MongoDB 的部分 aggregation stage（<code>$graphLookup</code> / <code>$facet</code> 等）、Cassandra 的部分 CQL feature 在對應 API 行為不同或不支援、dev 環境 sample data 看不出、production 才爆。修法是 Phase 0 把 <em>所有</em> production query 拉出來逐條驗證、Phase 4 shadow read 對 checksum、不能假設相容。</p>
<h3 id="shard-key--partition-key-直接照搬">shard key / partition key 直接照搬</h3>
<p>MongoDB shard key 或 Cassandra partition key 直接當 Cosmos logical partition key、忽略 10,000 RU/s per partition 上限。原本 Cassandra 寬 partition 在 Cosmos 變 hot partition、throttle。修法是 Phase 1 按 Cosmos 的 partition 上限重新評估、必要時用 synthetic / composite key 強制分散、見 <a href="../partition-key-design/">partition-key-design</a> 與 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>。</p>
<h3 id="把-native-api-二次遷移當升級低估">把 native API 二次遷移當「升級」低估</h3>
<p>選 protocol-compat 上線後、想拿 Change Feed / SQL query 等 native 能力、以為「升級到 SQL API」是改設定。實際是新 account + 全量資料遷 + application 重寫的第二次完整遷移。修法是 Phase 0 就決定終態方向 — 若終態確定要 native feature 且團隊能承擔重寫、直接走 native API 路徑、不要兩段遷。</p>
<h3 id="consistency-level-對應錯">consistency level 對應錯</h3>
<p>CQL 的 QUORUM / MongoDB 的 read concern majority 直接假設等價於 Cosmos 某個 level、語義不是一對一。修法是按 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 把 read-after-write 與順序需求逐場景對應、不照字面翻譯 consistency 名稱。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>主對比 SSoT：<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> — API <em>選擇判斷</em> 與三型遷移路徑分類在它主寫、本文主寫選定後的 <em>遷移流程</em></li>
<li>Sibling deep articles：<a href="../partition-key-design/">partition-key-design</a>（shard / partition key 翻譯）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（翻譯層 RU overhead 與 baseline）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（read concern / CQL consistency 對應）、<a href="../change-feed-cdc/">change-feed-cdc</a>（native API 才有原生 Change Feed、是 native 路徑的 feature driver 之一）</li>
<li>不遷的對照：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">Coinbase</a> 保留 MongoDB 補周邊 — 確認「補周邊」解不了再遷</li>
<li>跨雲對照：<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">Forbes</a> 留 Atlas 跨雲 — 跨雲需求是 Cosmos DB 的 no-go</li>
<li>共通遷移模型：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>Knowledge card：<a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> / <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「從 MongoDB / Cassandra 遷入」backlog</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾遷入 backlog 的深度展開</li>
<li><a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> — API 選擇判斷與三型遷移路徑 SSoT</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB → Cosmos DB MongoDB API dogfood</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 同 DB 換託管時程對照</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 保留 MongoDB 不遷的對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> / <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> / <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> — 遷移各 phase 的 sibling</li>
<li><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> — 跨 vendor 共通模型</li>
<li><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor Lock-in 卡片</a> — 跨雲 no-go 判讀</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/">Migrate to Cosmos DB for MongoDB</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/cassandra/">Cosmos DB for Apache Cassandra</a></li>
</ul>
]]></content:encoded></item><item><title>7.3 入口治理與伺服器防護</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/</guid><description>&lt;p>本章的責任是把入口暴露風險拆成可操作的防護節點，讓外網可達面、管理平面與修補窗口能用同一套判讀語言治理。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦入口分級、管理平面邊界與修補窗口治理。案例在問題觸發時提供證據，不作固定列表。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：對外 attack surface 擴張 / public 與 admin 與 diagnostic endpoint 暴露失衡 / VPN 與遠端路徑利用 / 邊界設備漏洞 / 修補窗口暴露 / 管理平面暴露。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>身分授權 → &lt;a href="../identity-access-boundary/">7.2&lt;/a>&lt;/li>
&lt;li>資料外洩 → &lt;a href="../data-protection-and-masking-governance/">7.4&lt;/a>&lt;/li>
&lt;li>傳輸 / 憑證 → &lt;a href="../transport-trust-and-certificate-lifecycle/">7.5&lt;/a>&lt;/li>
&lt;li>機器憑證 → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../detection-coverage-and-signal-governance/">7.13&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[attack-surface]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="入口治理模型">入口治理模型&lt;/h2>
&lt;p>入口治理的核心責任是定義哪些流量可以進來、能觸及什麼能力、異常時如何收斂。&lt;/p>
&lt;ol>
&lt;li>入口分級：區分 public、admin、diagnostic、internal 端點責任。&lt;/li>
&lt;li>平面分層：把管理平面與業務平面隔離，避免單點突破橫向擴散。&lt;/li>
&lt;li>修補節奏：把隔離、修補、驗證綁成同一個交付鏈，不讓修補停在部署完成。&lt;/li>
&lt;li>會話收斂：把入口事件後的會話失效與權限回收納入標準流程。&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbound-tunnel/" data-link-title="Outbound Tunnel" data-link-desc="反向隧道把出站連線轉成可達入口、與傳統 port-forward 的責任倒轉">outbound tunnel&lt;/a>（cloudflared / Tailscale）作為入口形態的部署合約見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口&lt;/a>。&lt;/p>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「入口異常」快速轉成「防護動作」。&lt;/p>
&lt;ol>
&lt;li>先判讀異常發生在 public 面、admin 面或遠端接入路徑。&lt;/li>
&lt;li>再判讀是否已進入可擴散窗口（批量掃描、已利用、橫向跡象）。&lt;/li>
&lt;li>接著啟動暫時緩解、分區隔離與修補驗證。&lt;/li>
&lt;li>最後交接到 incident workflow，追蹤關閉條件與復盤回寫。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>對外入口可達面擴張&lt;/td>
 &lt;td>掃描流量上升、未知端點暴露、修補等待時間拉長&lt;/td>
 &lt;td>批量利用窗口擴大&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/public-api-endpoint/" data-link-title="Public API Endpoint" data-link-desc="說明面向外部 client 的穩定 API 入口如何被管理">public-api-endpoint&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>管理平面暴露失衡&lt;/td>
 &lt;td>管理入口異常登入、異常設定變更&lt;/td>
 &lt;td>高權限面成為事件起點&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">admin-endpoint&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VPN 與遠端路徑失控&lt;/td>
 &lt;td>異常 session 延續、跨區存取時序偏移&lt;/td>
 &lt;td>內網橋接風險增加&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky-session&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 + 06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修補與驗證節奏分離&lt;/td>
 &lt;td>修補完成後異常指標持續&lt;/td>
 &lt;td>事件處置成本上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback-strategy&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="邊界設備事件的三同步-mechanism">邊界設備事件的三同步 mechanism&lt;/h2>
&lt;p>邊界設備事件的核心治理是「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事 &lt;em>同步發生&lt;/em>、不分先後留下時間窗口。任一件先做完、其他兩件還在準備、攻擊者就能在窗口內把已取得的會話或內網落點轉成持續存取。會話失效層的 canonical 在 &lt;a href="../transport-trust-and-certificate-lifecycle/#%e6%9c%83%e8%a9%b1%e9%87%8d%e6%94%be%e8%b7%9f%e5%85%a8%e5%9f%9f%e5%a4%b1%e6%95%88canonical">7.5 § 會話重放跟全域失效&lt;/a>、本節聚焦邊界設備視角下三同步的並行需求。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把入口暴露風險拆成可操作的防護節點，讓外網可達面、管理平面與修補窗口能用同一套判讀語言治理。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦入口分級、管理平面邊界與修補窗口治理。案例在問題觸發時提供證據，不作固定列表。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：對外 attack surface 擴張 / public 與 admin 與 diagnostic endpoint 暴露失衡 / VPN 與遠端路徑利用 / 邊界設備漏洞 / 修補窗口暴露 / 管理平面暴露。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>身分授權 → <a href="../identity-access-boundary/">7.2</a></li>
<li>資料外洩 → <a href="../data-protection-and-masking-governance/">7.4</a></li>
<li>傳輸 / 憑證 → <a href="../transport-trust-and-certificate-lifecycle/">7.5</a></li>
<li>機器憑證 → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li>偵測訊號 → <a href="../detection-coverage-and-signal-governance/">7.13</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[attack-surface]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="入口治理模型">入口治理模型</h2>
<p>入口治理的核心責任是定義哪些流量可以進來、能觸及什麼能力、異常時如何收斂。</p>
<ol>
<li>入口分級：區分 public、admin、diagnostic、internal 端點責任。</li>
<li>平面分層：把管理平面與業務平面隔離，避免單點突破橫向擴散。</li>
<li>修補節奏：把隔離、修補、驗證綁成同一個交付鏈，不讓修補停在部署完成。</li>
<li>會話收斂：把入口事件後的會話失效與權限回收納入標準流程。</li>
</ol>
<p><a href="/blog/backend/knowledge-cards/outbound-tunnel/" data-link-title="Outbound Tunnel" data-link-desc="反向隧道把出站連線轉成可達入口、與傳統 port-forward 的責任倒轉">outbound tunnel</a>（cloudflared / Tailscale）作為入口形態的部署合約見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>。</p>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「入口異常」快速轉成「防護動作」。</p>
<ol>
<li>先判讀異常發生在 public 面、admin 面或遠端接入路徑。</li>
<li>再判讀是否已進入可擴散窗口（批量掃描、已利用、橫向跡象）。</li>
<li>接著啟動暫時緩解、分區隔離與修補驗證。</li>
<li>最後交接到 incident workflow，追蹤關閉條件與復盤回寫。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對外入口可達面擴張</td>
          <td>掃描流量上升、未知端點暴露、修補等待時間拉長</td>
          <td>批量利用窗口擴大</td>
          <td><a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface</a>、<a href="/blog/backend/knowledge-cards/public-api-endpoint/" data-link-title="Public API Endpoint" data-link-desc="說明面向外部 client 的穩定 API 入口如何被管理">public-api-endpoint</a></td>
          <td><code>05 + 08</code></td>
      </tr>
      <tr>
          <td>管理平面暴露失衡</td>
          <td>管理入口異常登入、異常設定變更</td>
          <td>高權限面成為事件起點</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane</a>、<a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">admin-endpoint</a></td>
          <td><code>05 + 08</code></td>
      </tr>
      <tr>
          <td>VPN 與遠端路徑失控</td>
          <td>異常 session 延續、跨區存取時序偏移</td>
          <td>內網橋接風險增加</td>
          <td><a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky-session</a>、<a href="/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation</a></td>
          <td><code>08 + 06</code></td>
      </tr>
      <tr>
          <td>修補與驗證節奏分離</td>
          <td>修補完成後異常指標持續</td>
          <td>事件處置成本上升</td>
          <td><a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a>、<a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback-strategy</a></td>
          <td><code>06 + 08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="邊界設備事件的三同步-mechanism">邊界設備事件的三同步 mechanism</h2>
<p>邊界設備事件的核心治理是「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事 <em>同步發生</em>、不分先後留下時間窗口。任一件先做完、其他兩件還在準備、攻擊者就能在窗口內把已取得的會話或內網落點轉成持續存取。會話失效層的 canonical 在 <a href="../transport-trust-and-certificate-lifecycle/#%e6%9c%83%e8%a9%b1%e9%87%8d%e6%94%be%e8%b7%9f%e5%85%a8%e5%9f%9f%e5%a4%b1%e6%95%88canonical">7.5 § 會話重放跟全域失效</a>、本節聚焦邊界設備視角下三同步的並行需求。</p>
<p><a href="../red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/">Citrix Bleed 2023</a> 跟 <a href="../red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/">PAN-OS 2024</a> 兩個案例的「mechanism 總綱」段共同標明這個三同步原則、並標明前提是「事先有 inventory + 自動化失效 / 清查能力」。兩 case 分別補不同層失效訊號 — Citrix Bleed 補會話被竊取後重放的視角、PAN-OS 補邊界設備暴露面集中且修補窗口內缺暫時緩解的視角。</p>
<p>以下基於通用工程知識補充：三同步是 mechanism 並行需求 — 三條 chain 共享同一個事件期間的時間窗口、不視為流程時序。inventory 缺位時、團隊在事件期間答不出「哪些 session 受影響」「哪些憑證該收斂」、只能先修補再事後追查 — 留下的時間窗口正是攻擊者持續存取的高機率窗口。日常修補演練的驗收標準要同時包含「修補完成」跟「修補同時完成會話失效」兩條軌、把 inventory 完整度當共同前提。</p>
<h2 id="修補窗口期內的暫時緩解">修補窗口期內的暫時緩解</h2>
<p>邊界設備的修補窗口從 CVE 公告到所有 fleet 完成 deploy 通常以天為單位、實際可利用窗口會超過廠商建議的修補時限。控制責任是定義 <em>修補前的暫時緩解策略</em>、讓窗口期內不暴露完整攻擊面。</p>
<p>對應 <a href="../red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/">PAN-OS 2024</a>：揭露三層失效控制面 — 邊界設備暴露面高且集中、修補窗口內缺少暫時緩解與替代路徑、攻擊偵測依賴單一訊號來源。案例「可落地檢查點」標明 mechanism 為「先套用緩解、再分區修補與驗證」，前提是「關鍵邊界設備有降級與備援計畫」。</p>
<p>以下基於通用工程知識補充：暫時緩解的選項要在 CVE 公告前就準備好。可選項包含關閉脆弱模組、收斂可達來源、加 WAF / IPS 規則、或臨時降級到備援路徑；每個選項都有可用性代價、要在日常演練中量化過、事件發生時才能快速取捨。「依賴單一訊號來源」是另一個常見盲點 — 邊界事件的早期信號常分散在 IDS、CDN log、應用層 audit、廠商情資、單一來源容易漏掉。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時需要從一般維運切換成高壓處置模式。</p>
<ul>
<li>外網可達入口在短期內被集中掃描且修補窗口過長時，代表利用風險已升高。</li>
<li>管理平面出現異常登入與設定漂移時，代表高權限入口已受壓。</li>
<li>遠端接入事件後 session 持續可用時，代表收斂節奏落後。</li>
<li>修補完成但異常訊號未下降時，代表控制面尚未真正恢復。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證入口治理是否足以對抗真實攻擊節奏。</p>
<ul>
<li>邊界設備高風險窗口： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></li>
<li>VPN 路徑被鏈式利用： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a></li>
<li>管理平面被快速接管： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/cisco-ios-xe-cve-2023-20198-webui-chain/" data-link-title="7.R7.3.7 Cisco IOS XE 2023：Web UI 管理面風險" data-link-desc="網通設備管理介面暴露時，攻擊可直接穿透邊界控制平面">Cisco IOS XE 2023</a></li>
<li>單人遠端 shell 的入口選型： <a href="/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &#43; Access 兩種存取模型的選型判讀。">7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平台入口與配置：<code>05-deployment-platform</code></li>
<li>壓力與回復驗證：<code>06-reliability</code></li>
<li>分級與收斂流程：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>Offset</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/offset/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/offset/</guid><description>&lt;p>Offset 的核心概念是「consumer 在事件流中的讀取位置」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 的進度記錄，讓 consumer 知道自己已經處理到哪裡，也讓系統可以從某個位置繼續或重放。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Offset 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 的進度記錄、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 的計算基準、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a> 的起點定位。在 Kafka 中，offset 是每個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 內的遞增整數；在 Redis Streams 中是 entry ID（timestamp-sequence）；在 SQS 中沒有顯式 offset，改用 visibility timeout 控制消費進度。&lt;/p>
&lt;p>Offset 提交太早（處理前就 commit）可能造成處理遺失 — consumer crash 後從已 commit 的位置繼續，跳過未完成的訊息。提交太晚（處理完成很久才 commit）可能造成重複處理 — consumer crash 後從舊 offset 重新開始，重複處理已完成的訊息。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要理解 offset 的訊號是 consumer 重啟後需要接續處理。報表 consumer 處理到某個 offset 後 crash，重啟時要從安全位置繼續，並用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 承受可能重複的事件。&lt;/p>
&lt;p>Offset 也是 replay 操作的控制參數。「重設 offset 到三天前」意味著 consumer group 會從三天前的位置重新處理所有事件 — 下游需要有 idempotent 設計才能承受重播。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Offset 提交策略要和業務處理完成條件對齊。Auto-commit（定期自動提交）實作簡單但在 crash 時有遺失風險；manual commit（處理完成後手動提交）更安全但程式碼更複雜。Runbook 應說明如何查 current offset、committed offset、lag、重設 offset 的操作步驟與 replay 對下游的影響。&lt;/p></description><content:encoded><![CDATA[<p>Offset 的核心概念是「consumer 在事件流中的讀取位置」。它是 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 的進度記錄，讓 consumer 知道自己已經處理到哪裡，也讓系統可以從某個位置繼續或重放。</p>
<h2 id="概念位置">概念位置</h2>
<p>Offset 是 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 的進度記錄、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 的計算基準、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a> 的起點定位。在 Kafka 中，offset 是每個 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 內的遞增整數；在 Redis Streams 中是 entry ID（timestamp-sequence）；在 SQS 中沒有顯式 offset，改用 visibility timeout 控制消費進度。</p>
<p>Offset 提交太早（處理前就 commit）可能造成處理遺失 — consumer crash 後從已 commit 的位置繼續，跳過未完成的訊息。提交太晚（處理完成很久才 commit）可能造成重複處理 — consumer crash 後從舊 offset 重新開始，重複處理已完成的訊息。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要理解 offset 的訊號是 consumer 重啟後需要接續處理。報表 consumer 處理到某個 offset 後 crash，重啟時要從安全位置繼續，並用 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 承受可能重複的事件。</p>
<p>Offset 也是 replay 操作的控制參數。「重設 offset 到三天前」意味著 consumer group 會從三天前的位置重新處理所有事件 — 下游需要有 idempotent 設計才能承受重播。</p>
<h2 id="設計責任">設計責任</h2>
<p>Offset 提交策略要和業務處理完成條件對齊。Auto-commit（定期自動提交）實作簡單但在 crash 時有遺失風險；manual commit（處理完成後手動提交）更安全但程式碼更複雜。Runbook 應說明如何查 current offset、committed offset、lag、重設 offset 的操作步驟與 replay 對下游的影響。</p>
]]></content:encoded></item><item><title>Cosmos DB for PostgreSQL：基於 Citus 的分散式 PostgreSQL、跟核心 Cosmos DB 是不同產品、何時選它而非核心 Cosmos 或一般 PG</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/cosmos-for-postgresql/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/cosmos-for-postgresql/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Cosmos DB for PostgreSQL 是 Azure 在 2022 把 Citus（PostgreSQL 的分散式 extension）納入後推出的 &lt;em>分散式 PostgreSQL&lt;/em> 託管服務 — 它跑真正的 PostgreSQL engine、支援標準 SQL / JOIN / ACID 交易、把單表水平分片到多個 worker node。它跟本 vendor 頁主講的核心 Cosmos DB（NoSQL、multi-model、RU/s 計費）是 &lt;em>兩個不同產品&lt;/em>、只是共用品牌名稱。本文的主責任是釐清這個定位混淆、再講它的架構與選型判準：何時選它、何時該回核心 Cosmos DB、何時一般 PostgreSQL 就夠。&lt;/p>
&lt;p>本文沒有專屬 production case anchor：Cosmos DB for PostgreSQL 的公開 case 覆蓋稀薄、機制以 Azure / Citus vendor 規格與分散式 PostgreSQL 通用工程展開、選型判準用「scale-out PG vs NoSQL vs single-node PG」這個具體決策驅動。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：本文涉及的服務命名、node 規格上限、Citus 版本、PostgreSQL major version 支援屬時間敏感、Azure 服務命名歷史上有變動、實作前以 &lt;a href="https://learn.microsoft.com/azure/cosmos-db/postgresql/">Cosmos DB for PostgreSQL 官方文件&lt;/a> cross-verify。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：team 在 Azure 上跑 PostgreSQL、單機 primary 撐到上限 — write throughput、資料量、或單表太大導致 index / vacuum / query 變慢。看到「Cosmos DB」以為是要把資料搬進 NoSQL、重寫 application 成 document model；或反過來、看到「Cosmos DB for PostgreSQL」以為它就是核心 Cosmos DB 的一個 PostgreSQL API、結果發現它是完全不同的東西。命名混淆讓選型從一開始就走偏。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「單機 PostgreSQL 撐不住、但 application 是 SQL / JOIN / 交易重、不想重寫成 NoSQL」&lt;/li>
&lt;li>「Cosmos DB for PostgreSQL 跟核心 Cosmos DB 是同一個東西嗎」&lt;/li>
&lt;li>「它跟一般 Azure Database for PostgreSQL 差在哪、什麼時候才需要它」&lt;/li>
&lt;li>「跟 CockroachDB / Aurora / Spanner 這些 distributed SQL 怎麼選」&lt;/li>
&lt;/ul>
&lt;p>真實壓力：SQL workload 撐到單機上限時、選錯方向的成本是年級的。誤以為要遷 NoSQL 而重寫 application 是浪費；誤以為核心 Cosmos DB 有「PostgreSQL 相容」而選錯產品也是浪費。正確的選型要先把這個服務放回它真正的分類 — &lt;em>分散式 SQL&lt;/em>、見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL&lt;/a>。&lt;/p>
&lt;h2 id="核心機制citus-based-coordinator-worker-分散式-postgresql">核心機制：Citus-based coordinator-worker 分散式 PostgreSQL&lt;/h2>
&lt;p>Cosmos DB for PostgreSQL 的底層是 Citus、把 PostgreSQL 從單機擴展成 coordinator + worker 的分散式叢集。它的關鍵概念有幾個。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Cosmos DB for PostgreSQL 是 Azure 在 2022 把 Citus（PostgreSQL 的分散式 extension）納入後推出的 <em>分散式 PostgreSQL</em> 託管服務 — 它跑真正的 PostgreSQL engine、支援標準 SQL / JOIN / ACID 交易、把單表水平分片到多個 worker node。它跟本 vendor 頁主講的核心 Cosmos DB（NoSQL、multi-model、RU/s 計費）是 <em>兩個不同產品</em>、只是共用品牌名稱。本文的主責任是釐清這個定位混淆、再講它的架構與選型判準：何時選它、何時該回核心 Cosmos DB、何時一般 PostgreSQL 就夠。</p>
<p>本文沒有專屬 production case anchor：Cosmos DB for PostgreSQL 的公開 case 覆蓋稀薄、機制以 Azure / Citus vendor 規格與分散式 PostgreSQL 通用工程展開、選型判準用「scale-out PG vs NoSQL vs single-node PG」這個具體決策驅動。</p>
<blockquote>
<p><strong>Scope warning</strong>：本文涉及的服務命名、node 規格上限、Citus 版本、PostgreSQL major version 支援屬時間敏感、Azure 服務命名歷史上有變動、實作前以 <a href="https://learn.microsoft.com/azure/cosmos-db/postgresql/">Cosmos DB for PostgreSQL 官方文件</a> cross-verify。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：team 在 Azure 上跑 PostgreSQL、單機 primary 撐到上限 — write throughput、資料量、或單表太大導致 index / vacuum / query 變慢。看到「Cosmos DB」以為是要把資料搬進 NoSQL、重寫 application 成 document model；或反過來、看到「Cosmos DB for PostgreSQL」以為它就是核心 Cosmos DB 的一個 PostgreSQL API、結果發現它是完全不同的東西。命名混淆讓選型從一開始就走偏。</p>
<p>讀者徵兆：</p>
<ul>
<li>「單機 PostgreSQL 撐不住、但 application 是 SQL / JOIN / 交易重、不想重寫成 NoSQL」</li>
<li>「Cosmos DB for PostgreSQL 跟核心 Cosmos DB 是同一個東西嗎」</li>
<li>「它跟一般 Azure Database for PostgreSQL 差在哪、什麼時候才需要它」</li>
<li>「跟 CockroachDB / Aurora / Spanner 這些 distributed SQL 怎麼選」</li>
</ul>
<p>真實壓力：SQL workload 撐到單機上限時、選錯方向的成本是年級的。誤以為要遷 NoSQL 而重寫 application 是浪費；誤以為核心 Cosmos DB 有「PostgreSQL 相容」而選錯產品也是浪費。正確的選型要先把這個服務放回它真正的分類 — <em>分散式 SQL</em>、見 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a>。</p>
<h2 id="核心機制citus-based-coordinator-worker-分散式-postgresql">核心機制：Citus-based coordinator-worker 分散式 PostgreSQL</h2>
<p>Cosmos DB for PostgreSQL 的底層是 Citus、把 PostgreSQL 從單機擴展成 coordinator + worker 的分散式叢集。它的關鍵概念有幾個。</p>
<p>它跑 <em>真正的 PostgreSQL</em>。不是 wire-compat、不是 PostgreSQL API on top of NoSQL — 是 PostgreSQL engine 加 Citus extension。標準 SQL、JOIN、ACID 交易、PostgreSQL extension 生態（含部分如 PostGIS）都在。這跟核心 Cosmos DB（自己的 query language、SQL-like 但無 JOIN、RU/s 計費）是根本不同的東西。</p>
<p>架構是 coordinator-worker。coordinator node 接 query、根據 distribution column 把 query 路由 / 拆分到 worker node、worker 存實際的 shard。application 連 coordinator、看起來像連一個 PostgreSQL。</p>
<p>distribution column 是核心設計決策、類比核心 Cosmos DB 的 partition key 之於 NoSQL、也類比 <a href="../partition-key-design/">partition-key-design</a> 講的分散原則。表按 distribution column 的值分片到 worker；同一 distribution column 值的 row 落在同一 shard。JOIN 與交易若在同一 distribution column 值內、可以下推到單一 worker 高效執行（co-location）；跨 distribution column 的 JOIN / 交易要跨 worker 協調、較貴。</p>
<p>表分三種：distributed table（按 distribution column 分片、大表用）、reference table（每個 worker 全複本、小的維度表用、讓 JOIN co-locate）、local table（只在 coordinator）。建模的關鍵是把常一起 JOIN 的大表用 <em>同一 distribution column</em> 分片、達成 co-location。</p>
<h2 id="選型判準三方對照">選型判準：三方對照</h2>
<p>這是本文主判讀段。Cosmos DB for PostgreSQL 的正確位置是「single-node PG 不夠、但 workload 仍是 SQL 範式」的中間地帶。</p>
<p>選 Cosmos DB for PostgreSQL 的條件：</p>
<ul>
<li>workload 是 SQL 範式（關聯 schema、JOIN、交易）、不想 / 不能重寫成 NoSQL</li>
<li>single-node PostgreSQL 已達上限（write throughput / 資料量 / 單表大小）、且資料有好的 distribution column（多租戶的 tenant_id、time-series 的某維度）</li>
<li>工作負載偏向多租戶 SaaS 或 real-time analytics over fresh data — Citus 的典型適配場景</li>
<li>想留在 PostgreSQL 生態（SQL、extension、既有 tooling）而非進 NoSQL</li>
</ul>
<p>回核心 Cosmos DB（NoSQL）的條件：</p>
<ul>
<li>資料形狀已是 document / KV、access pattern 固定、不需要 JOIN 與複雜 SQL</li>
<li>需要 multi-model（document + graph + KV）、5 個 consistency level、turnkey multi-region active-active write</li>
<li>RU/s 容量抽象與 serverless 計費更符合 workload — 見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
</ul>
<p>一般 Azure Database for PostgreSQL（single-node managed PG）就夠的條件：</p>
<ul>
<li>single-node 還沒到上限 — 多數 OLTP baseline 用 vertical scaling + read replica 就夠、不需要分散式</li>
<li>沒有好的 distribution column — 分散式 PostgreSQL 沒有均勻 distribution column 會 hot worker、好處拿不到、複雜度卻全付</li>
<li>不想承擔 distributed SQL 的複雜度（distribution column 設計、co-location 規劃、跨 shard query 成本）</li>
</ul>
<p>判讀句：先確認 single-node PG 真的到上限、再確認 workload 是 SQL 範式（否則考慮 NoSQL）、最後確認有好的 distribution column。三個都成立、Cosmos DB for PostgreSQL 才是對的；缺任一個、回 single-node PG 或核心 Cosmos DB。</p>
<h3 id="跟其他-distributed-sql-的位置">跟其他 distributed SQL 的位置</h3>
<p>Cosmos DB for PostgreSQL 是 Azure 上、PostgreSQL-native、scale-out（co-location 設計驅動）的 distributed SQL。跟 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner</a>（全球 external consistency、自己的 SQL 方言）、<a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a>（跨雲、PostgreSQL wire、自動 range 分散）、Aurora DSQL（AWS、全球 active-active）位置不同：Cosmos DB for PostgreSQL 強在「真 PostgreSQL engine + extension 生態 + co-location 控制」、弱在它的分散需要 distribution column 設計（不像 CockroachDB / Spanner 自動分 range）、且綁 Azure。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="建叢集與設定-distribution-column">建叢集與設定 distribution column</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 建 distributed table、按 tenant_id 分片（多租戶 SaaS 典型）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">tenant_id</span><span class="w">   </span><span class="nb">bigint</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="n">event_id</span><span class="w">    </span><span class="nb">bigint</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="n">payload</span><span class="w">     </span><span class="n">jsonb</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w">  </span><span class="n">timestamptz</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">now</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_distributed_table</span><span class="p">(</span><span class="s1">&#39;events&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;tenant_id&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 維度小表設 reference table、讓 JOIN co-locate
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">tenants</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="nb">bigint</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="nb">text</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_reference_table</span><span class="p">(</span><span class="s1">&#39;tenants&#39;</span><span class="p">);</span></span></span></code></pre></div><p>驗證：<code>SELECT * FROM citus_tables;</code> 看每張表的 distribution column 與 shard 分布；對 distributed table 的查詢若帶 distribution column filter、<code>EXPLAIN</code> 顯示下推到單一 shard、不帶則 fan-out 到所有 worker。</p>
<h3 id="驗證-co-location">驗證 co-location</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 同 distribution column 的兩張 distributed table JOIN 應 co-located
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">colocation_id</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">citus_tables</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">colocation_id</span><span class="p">;</span></span></span></code></pre></div><p>驗證：常一起 JOIN 的大表落在同一 colocation group、JOIN 在 worker 本地完成、不跨 worker shuffle。</p>
<h3 id="加-worker-擴容">加 worker 擴容</h3>
<p>加 worker node 後 rebalance shard。驗證：rebalance 後 shard 在新舊 worker 間分布均勻、單一 worker 不再是 hot spot。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>Cosmos DB for PostgreSQL 是叢集級服務、scale worker 是運維操作、可逆（縮回去）。但 <em>distribution column 一旦選定、改它要重建表 + 重灌資料</em> — 跟核心 Cosmos DB 的 partition key 不可改是同一類不可逆設計、見 <a href="../partition-key-design/">partition-key-design</a>。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="把它跟核心-cosmos-db-當同一產品選">把它跟核心 Cosmos DB 當同一產品選</h3>
<p>選型時把「Cosmos DB for PostgreSQL」當成「核心 Cosmos DB 的 PostgreSQL 介面」、規劃用 RU/s、找 consistency level 設定、結果整套 mental model 對不上 — 因為它是分散式 PostgreSQL、用 node 規格計費、用 PostgreSQL 的交易隔離級別。修法是選型第一步就確認「這是分散式 SQL、不是 NoSQL」、規劃按 PostgreSQL + Citus 的模型走、不要套核心 Cosmos DB 的概念。</p>
<h3 id="沒有好的-distribution-column-硬上分散式">沒有好的 distribution column 硬上分散式</h3>
<p>workload 沒有均勻的 distribution column（例如資料天然集中在少數 tenant）、硬分片後變 hot worker、分散式的好處拿不到、複雜度全付。徵兆是少數 worker CPU / IO 飽和、其他 worker 閒置。修法是選型階段就評估 distribution column 的 cardinality 與均勻度；不均勻時、要嘛留 single-node PG（垂直擴 + read replica）、要嘛重新設計 distribution column（如多租戶用 composite 或對 hot tenant 特殊處理）。</p>
<h3 id="大量跨-shard-query--非-co-located-join">大量跨 shard query / 非 co-located JOIN</h3>
<p>application query 大多不帶 distribution column filter、或常做跨 distribution column 的 JOIN、每個 query fan-out 到所有 worker + shuffle、latency 與成本都差。徵兆是 <code>EXPLAIN</code> 顯示 query 打所有 worker、p99 latency 高。修法是重新設計 schema 讓常一起查的表 co-located、把 distribution column 放進熱 query 的 filter；改不動時、這個 workload 可能不適合 scale-out PG、回 single-node 或考慮其他方案。</p>
<h3 id="該用-nosql-卻選了分散式-pg或反之">該用 NoSQL 卻選了分散式 PG（或反之）</h3>
<p>document / KV、固定 access pattern、不需要 JOIN 的 workload 選了 Cosmos DB for PostgreSQL、付了 SQL / distribution column 設計的複雜度卻沒用到關聯能力 — 這類 workload 核心 Cosmos DB（NoSQL）更自然。反過來、SQL / JOIN / 交易重的 workload 被推去核心 Cosmos DB（NoSQL）要重寫成 document model 也是錯。修法是回到「workload 是 SQL 範式還是 document / KV 範式」的根本判斷、見本文選型判準段與 <a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> 的範式判讀。</p>
<h3 id="anti-recommendationsingle-node-pg-沒到上限不要上">Anti-recommendation：single-node PG 沒到上限不要上</h3>
<p>分散式 PostgreSQL 帶來 distribution column 設計、co-location 規劃、跨 shard query 成本、rebalance 運維。single-node managed PostgreSQL 加 vertical scaling 與 read replica 能撐的 OLTP baseline 比多數團隊以為的大。沒有觸及 single-node 真實上限（write throughput 飽和、單表大到 maintenance 困難、資料量超出單機）就上分散式、是用複雜度換不存在的容量需求。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：各 worker node 的 CPU / IO / 連線（找 hot worker）、shard 在 worker 間的分布均勻度、跨 shard query 比例、coordinator 連線數</li>
<li>容量單位：node 規格（不是 RU/s）— 規劃是 coordinator + N worker 的 vCPU / memory / storage、跟核心 Cosmos DB 的 RU 思維完全不同、不要混用 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> 的 RU 模型來估這個服務</li>
<li>distribution column 均勻度是容量上限的真實決定因素 — 跟 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a> 同模型、hot worker 讓名義叢集容量達不到</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：scale-out 的有效容量 = node 數 × 單 node 容量 × distribution 均勻度</li>
<li>Alert：單一 worker 飽和（distribution skew）、跨 shard query 比例上升、rebalance 後仍不均</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>定位釐清：本服務是 <em>分散式 PostgreSQL</em>、不是核心 Cosmos DB（NoSQL）— 共用品牌名稱、產品不同、選型不要混淆</li>
<li>跟核心 Cosmos DB 的分界：SQL / JOIN / 交易 + 到單機上限 → 本服務；document / KV / multi-model / multi-region active-active → 核心 Cosmos DB、見 <a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a></li>
<li>跟 PostgreSQL vendor 的分界：single-node 沒到上限 → <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">Azure Database for PostgreSQL / 一般 PG</a>；PostgreSQL 既有的 <a href="/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/" data-link-title="Specialized PostgreSQL Variants" data-link-desc="pgvectorscale、Citus、TimescaleDB、PostGIS、AlloyDB、Cosmos DB for PostgreSQL、serverless PG 等 PostgreSQL 變體的選型邊界">Specialized PostgreSQL Variants</a> 段已把 Cosmos DB for PostgreSQL 列為 Citus-based 變體之一</li>
<li>跟其他 distributed SQL：<a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner</a>（全球強一致）、<a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a>（跨雲、自動 range）— 本服務強在真 PostgreSQL engine + co-location 控制、弱在需 distribution column 設計 + 綁 Azure</li>
<li>distribution column 不可改：跟 <a href="../partition-key-design/">partition-key-design</a> 的 partition key 不可改是同類不可逆設計</li>
<li>Knowledge card：<a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> / <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 Cosmos DB for PostgreSQL backlog 的深度展開</li>
<li><a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> — SQL 範式 vs document / KV 範式的根本判讀</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a> / <a href="/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/" data-link-title="Specialized PostgreSQL Variants" data-link-desc="pgvectorscale、Citus、TimescaleDB、PostGIS、AlloyDB、Cosmos DB for PostgreSQL、serverless PG 等 PostgreSQL 變體的選型邊界">Specialized PostgreSQL Variants</a> — single-node PG 與 Citus 變體定位</li>
<li><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> / <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor</a> — 其他 distributed SQL 對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> — distribution column 不可改的同類設計</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">Distributed SQL 卡片</a> / <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/postgresql/">Azure Cosmos DB for PostgreSQL</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/postgresql/concepts-distributed-data">Citus distributed tables</a></li>
</ul>
]]></content:encoded></item><item><title>7.4 資料保護與遮罩治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/</guid><description>&lt;p>本章的責任是把資料暴露風險拆成可治理的節點，讓資料分級、遮罩、匯出與備份在設計期就能對齊判準。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦資料語意、暴露路徑、責任鏈與通報節奏。案例在特定問題觸發時提供證據參考。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：過量回應欄位暴露 / 高風險匯出節奏 / 備份權限混層 / 跨組織交換責任鏈斷點 / 資料分級錯位 / 遮罩遺漏路徑。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>身分授權 → &lt;a href="../identity-access-boundary/">7.2&lt;/a>&lt;/li>
&lt;li>入口暴露 → &lt;a href="../entrypoint-and-server-protection/">7.3&lt;/a>&lt;/li>
&lt;li>傳輸保護 → &lt;a href="../transport-trust-and-certificate-lifecycle/">7.5&lt;/a>&lt;/li>
&lt;li>殘留與刪除證據 → &lt;a href="../data-residency-deletion-and-evidence-chain/">7.11&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../detection-coverage-and-signal-governance/">7.13&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[data-classification]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="資料保護治理模型">資料保護治理模型&lt;/h2>
&lt;p>資料治理的核心責任是讓每一條資料路徑都有明確語意、責任人與控制面。&lt;/p>
&lt;ol>
&lt;li>分級層：定義資料敏感度與最小揭露範圍。&lt;/li>
&lt;li>傳輸層：定義 API、檔案與分享鏈路的暴露邊界。&lt;/li>
&lt;li>儲存層：定義正式資料、快取資料、備份資料的權限隔離。&lt;/li>
&lt;li>匯出層：定義誰可匯出、何時可匯出、匯出後可存活多久。&lt;/li>
&lt;li>證據層：定義高風險操作的稽核與回查能力。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「資料使用需求」轉成「資料暴露風險」。&lt;/p>
&lt;ol>
&lt;li>先判讀資料分級與使用目的是否一致。&lt;/li>
&lt;li>再判讀資料是否跨越預期邊界（欄位、路徑、時窗、角色）。&lt;/li>
&lt;li>接著判讀是否有可追溯證據可回查。&lt;/li>
&lt;li>最後把問題路由到平台防護、回復節奏或事故處置。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>回應欄位超出必要範圍&lt;/td>
 &lt;td>欄位分級與 API 回應不一致&lt;/td>
 &lt;td>資料暴露面擴張&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data-classification&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">excessive-data-exposure&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高風險匯出節奏異常&lt;/td>
 &lt;td>批量匯出、異常角色、異常時段集中&lt;/td>
 &lt;td>外送風險提升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact-scope&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>備份資產權限混層&lt;/td>
 &lt;td>備份讀取與正式環境權限邊界重疊&lt;/td>
 &lt;td>回復鏈轉為外送鏈&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨組織交換責任鏈斷點&lt;/td>
 &lt;td>通知節奏與交易時序偏移&lt;/td>
 &lt;td>通報品質與處置速度下降&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-communication-channel/" data-link-title="Incident Communication Channel" data-link-desc="說明事故期間內外部溝通要使用哪些固定通道與節奏">incident-communication-channel&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是界定哪些資料行為需要立即升級治理等級。&lt;/p>
&lt;ul>
&lt;li>回應欄位持續出現分級外資料時，代表最小揭露模型已失效。&lt;/li>
&lt;li>匯出在異常時段由異常角色大量觸發時，代表資料外送風險已進入高壓區。&lt;/li>
&lt;li>備份帳號可直接取得正式環境資料時，代表復原邊界與外送邊界混層。&lt;/li>
&lt;li>跨組織資料交換沒有同步通知與責任鏈時，代表事件時序與證據鏈不可驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證資料路徑控制是否完整。&lt;/p>
&lt;ul>
&lt;li>支援工具被濫用導致資料外送： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023&lt;/a>&lt;/li>
&lt;li>憑證濫用導致資料平台外送： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>備份鏈被轉為外洩路徑： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>資料路徑與入口設計：&lt;code>05-deployment-platform&lt;/code>&lt;/li>
&lt;li>回復排序與演練：&lt;code>06-reliability&lt;/code>&lt;/li>
&lt;li>通報與事故節奏：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把資料暴露風險拆成可治理的節點，讓資料分級、遮罩、匯出與備份在設計期就能對齊判準。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦資料語意、暴露路徑、責任鏈與通報節奏。案例在特定問題觸發時提供證據參考。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：過量回應欄位暴露 / 高風險匯出節奏 / 備份權限混層 / 跨組織交換責任鏈斷點 / 資料分級錯位 / 遮罩遺漏路徑。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>身分授權 → <a href="../identity-access-boundary/">7.2</a></li>
<li>入口暴露 → <a href="../entrypoint-and-server-protection/">7.3</a></li>
<li>傳輸保護 → <a href="../transport-trust-and-certificate-lifecycle/">7.5</a></li>
<li>殘留與刪除證據 → <a href="../data-residency-deletion-and-evidence-chain/">7.11</a></li>
<li>偵測訊號 → <a href="../detection-coverage-and-signal-governance/">7.13</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[data-classification]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="資料保護治理模型">資料保護治理模型</h2>
<p>資料治理的核心責任是讓每一條資料路徑都有明確語意、責任人與控制面。</p>
<ol>
<li>分級層：定義資料敏感度與最小揭露範圍。</li>
<li>傳輸層：定義 API、檔案與分享鏈路的暴露邊界。</li>
<li>儲存層：定義正式資料、快取資料、備份資料的權限隔離。</li>
<li>匯出層：定義誰可匯出、何時可匯出、匯出後可存活多久。</li>
<li>證據層：定義高風險操作的稽核與回查能力。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「資料使用需求」轉成「資料暴露風險」。</p>
<ol>
<li>先判讀資料分級與使用目的是否一致。</li>
<li>再判讀資料是否跨越預期邊界（欄位、路徑、時窗、角色）。</li>
<li>接著判讀是否有可追溯證據可回查。</li>
<li>最後把問題路由到平台防護、回復節奏或事故處置。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>回應欄位超出必要範圍</td>
          <td>欄位分級與 API 回應不一致</td>
          <td>資料暴露面擴張</td>
          <td><a href="/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data-classification</a>、<a href="/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">excessive-data-exposure</a></td>
          <td><code>05 + 08</code></td>
      </tr>
      <tr>
          <td>高風險匯出節奏異常</td>
          <td>批量匯出、異常角色、異常時段集中</td>
          <td>外送風險提升</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a>、<a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact-scope</a></td>
          <td><code>08</code></td>
      </tr>
      <tr>
          <td>備份資產權限混層</td>
          <td>備份讀取與正式環境權限邊界重疊</td>
          <td>回復鏈轉為外送鏈</td>
          <td><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a>、<a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a></td>
          <td><code>06 + 08</code></td>
      </tr>
      <tr>
          <td>跨組織交換責任鏈斷點</td>
          <td>通知節奏與交易時序偏移</td>
          <td>通報品質與處置速度下降</td>
          <td><a href="/blog/backend/knowledge-cards/incident-communication-channel/" data-link-title="Incident Communication Channel" data-link-desc="說明事故期間內外部溝通要使用哪些固定通道與節奏">incident-communication-channel</a>、<a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline</a></td>
          <td><code>08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定哪些資料行為需要立即升級治理等級。</p>
<ul>
<li>回應欄位持續出現分級外資料時，代表最小揭露模型已失效。</li>
<li>匯出在異常時段由異常角色大量觸發時，代表資料外送風險已進入高壓區。</li>
<li>備份帳號可直接取得正式環境資料時，代表復原邊界與外送邊界混層。</li>
<li>跨組織資料交換沒有同步通知與責任鏈時，代表事件時序與證據鏈不可驗證。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證資料路徑控制是否完整。</p>
<ul>
<li>支援工具被濫用導致資料外送： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023</a></li>
<li>憑證濫用導致資料平台外送： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li>備份鏈被轉為外洩路徑： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>資料路徑與入口設計：<code>05-deployment-platform</code></li>
<li>回復排序與演練：<code>06-reliability</code></li>
<li>通報與事故節奏：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>Retention</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/retention/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/retention/</guid><description>&lt;p>Retention 的核心概念是「資料或事件在系統中保留多久」。它影響 storage cost、audit 能力、replay 能力、debug 時間窗口、合規義務與資料刪除責任，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 共同構成資料生命週期管理。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Retention 連接資料生命週期跟查詢能力。不同類型的資料需要不同保留期限 — log 的 debug 用途可能只需要 7 天、audit log 因合規要求可能需要 1 年以上、metrics 的 raw data 可能保留 15 天但 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 保留 90 天。&lt;/p>
&lt;p>Retention 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 搭配運作 — hot tier 保留最近的高精度資料、warm / cold tier 保留較舊的低精度或歸檔資料。保留期限的設定見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 與成本邊界&lt;/a> 的保留階梯段。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 retention 設計的訊號是事故排查或資料修復需要回看歷史。若 event stream 只保留 24 小時，三天前的錯誤就無法靠 replay 重建。反過來，無限保留會讓儲存成本持續成長。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Retention 要同時考慮成本（儲存 × 時間）、法規（合規要求的最短保留期跟 GDPR 要求的最長保留期可能衝突）、資安（高敏感資料保留越久風險越高）、replay 需求（MQ 的 retention 影響 consumer 的 catchup 能力）跟 debug 能力（retention 太短讓事後分析無資料可用）。不同訊號類型用不同 retention 是基本做法 — error log 保留比 debug log 長、audit log 保留比 operational log 長。&lt;/p></description><content:encoded><![CDATA[<p>Retention 的核心概念是「資料或事件在系統中保留多久」。它影響 storage cost、audit 能力、replay 能力、debug 時間窗口、合規義務與資料刪除責任，跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 與 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 共同構成資料生命週期管理。</p>
<h2 id="概念位置">概念位置</h2>
<p>Retention 連接資料生命週期跟查詢能力。不同類型的資料需要不同保留期限 — log 的 debug 用途可能只需要 7 天、audit log 因合規要求可能需要 1 年以上、metrics 的 raw data 可能保留 15 天但 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 保留 90 天。</p>
<p>Retention 跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 搭配運作 — hot tier 保留最近的高精度資料、warm / cold tier 保留較舊的低精度或歸檔資料。保留期限的設定見 <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> 的保留階梯段。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 retention 設計的訊號是事故排查或資料修復需要回看歷史。若 event stream 只保留 24 小時，三天前的錯誤就無法靠 replay 重建。反過來，無限保留會讓儲存成本持續成長。</p>
<h2 id="設計責任">設計責任</h2>
<p>Retention 要同時考慮成本（儲存 × 時間）、法規（合規要求的最短保留期跟 GDPR 要求的最長保留期可能衝突）、資安（高敏感資料保留越久風險越高）、replay 需求（MQ 的 retention 影響 consumer 的 catchup 能力）跟 debug 能力（retention 太短讓事後分析無資料可用）。不同訊號類型用不同 retention 是基本做法 — error log 保留比 debug log 長、audit log 保留比 operational log 長。</p>
]]></content:encoded></item><item><title>Cosmos DB ↔ Azure Synapse Link：analytical store、HTAP federation、何時把分析 workload 從 OLTP 分出去</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/synapse-link-federation/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/synapse-link-federation/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Azure Synapse Link 把 Cosmos DB 的交易型資料自動同步到一個 column-oriented 的 analytical store、讓 Synapse（或其他 analytics engine）直接查分析資料、而 &lt;em>不消耗 OLTP 的 RU、不打 transactional store&lt;/em>。它是一種 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> — 同一份資料的 OLTP 與 OLAP 存取被分到兩個各自最佳化的 store、由平台保持同步。本文先講 analytical store 與 HTAP federation 的精確語義、再進啟用流程、最後拆「何時把分析 workload 分出去、何時 federate 到專用 OLAP」的判準。&lt;/p>
&lt;p>Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> — Microsoft 自家把使用分析平台建在 Cosmos DB 上、planet-scale 全球分散式分析。case 自承沒揭露具體 throughput / latency / cost 數字、也沒明說用了 Synapse Link、本文只取「analytics workload 建在 Cosmos 上」這個情境 anchor、機制以 Azure vendor 規格與 HTAP / federation 通用工程展開。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：交易資料在 Cosmos DB、business 想跑分析 — 跨日期彙總、跨 partition 聚合、ad-hoc 報表、餵 ML。直接在 Cosmos OLTP container 上跑這些 query 有兩個問題：一是 NoSQL query 引擎不擅長大範圍掃描與聚合、二是 &lt;em>分析 query 吃掉 OLTP 的 RU&lt;/em>、跑一個全表聚合可能把線上交易的 RU budget 耗光、造成 OLTP throttle（429）。團隊被迫在「分析準確性」與「OLTP 穩定性」之間二選一。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「在 Cosmos OLTP container 跑分析 query、把線上交易的 RU 吃光、OLTP 開始 429」&lt;/li>
&lt;li>「想做 analytics 但不想自己搭 ETL pipeline 把資料抽到 data warehouse」&lt;/li>
&lt;li>「分析資料可以晚幾分鐘、但不想為了分析犧牲 OLTP 容量」&lt;/li>
&lt;li>「什麼時候 Synapse Link 夠、什麼時候要把資料 ETL 到專用 OLAP（BigQuery / Snowflake）」&lt;/li>
&lt;/ul>
&lt;p>真實壓力：OLTP store 為點查與小範圍寫入最佳化、分析 query 為大範圍掃描與聚合最佳化、兩者對 storage layout 與資源的需求衝突。在同一個 store 同時服務兩者、不是 RU 互搶就是 query 形狀不對。Synapse Link 的價值是用 federation 把這個衝突拆開 — OLTP 與 OLAP 各有最佳化的 store、平台自動同步。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Azure Synapse Link 把 Cosmos DB 的交易型資料自動同步到一個 column-oriented 的 analytical store、讓 Synapse（或其他 analytics engine）直接查分析資料、而 <em>不消耗 OLTP 的 RU、不打 transactional store</em>。它是一種 <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> — 同一份資料的 OLTP 與 OLAP 存取被分到兩個各自最佳化的 store、由平台保持同步。本文先講 analytical store 與 HTAP federation 的精確語義、再進啟用流程、最後拆「何時把分析 workload 分出去、何時 federate 到專用 OLAP」的判準。</p>
<p>Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — Microsoft 自家把使用分析平台建在 Cosmos DB 上、planet-scale 全球分散式分析。case 自承沒揭露具體 throughput / latency / cost 數字、也沒明說用了 Synapse Link、本文只取「analytics workload 建在 Cosmos 上」這個情境 anchor、機制以 Azure vendor 規格與 HTAP / federation 通用工程展開。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：交易資料在 Cosmos DB、business 想跑分析 — 跨日期彙總、跨 partition 聚合、ad-hoc 報表、餵 ML。直接在 Cosmos OLTP container 上跑這些 query 有兩個問題：一是 NoSQL query 引擎不擅長大範圍掃描與聚合、二是 <em>分析 query 吃掉 OLTP 的 RU</em>、跑一個全表聚合可能把線上交易的 RU budget 耗光、造成 OLTP throttle（429）。團隊被迫在「分析準確性」與「OLTP 穩定性」之間二選一。</p>
<p>讀者徵兆：</p>
<ul>
<li>「在 Cosmos OLTP container 跑分析 query、把線上交易的 RU 吃光、OLTP 開始 429」</li>
<li>「想做 analytics 但不想自己搭 ETL pipeline 把資料抽到 data warehouse」</li>
<li>「分析資料可以晚幾分鐘、但不想為了分析犧牲 OLTP 容量」</li>
<li>「什麼時候 Synapse Link 夠、什麼時候要把資料 ETL 到專用 OLAP（BigQuery / Snowflake）」</li>
</ul>
<p>真實壓力：OLTP store 為點查與小範圍寫入最佳化、分析 query 為大範圍掃描與聚合最佳化、兩者對 storage layout 與資源的需求衝突。在同一個 store 同時服務兩者、不是 RU 互搶就是 query 形狀不對。Synapse Link 的價值是用 federation 把這個衝突拆開 — OLTP 與 OLAP 各有最佳化的 store、平台自動同步。</p>
<h2 id="核心機制analytical-store--htap-federation">核心機制：analytical store + HTAP federation</h2>
<p>Synapse Link 的核心是 Cosmos DB container 的 <em>analytical store</em>。</p>
<p>analytical store 是 column-oriented 的自動複本。在 container 啟用 analytical store 後、Cosmos DB 把 transactional store（row / document、為 OLTP 最佳化）的資料自動同步到一份 column-oriented 表示（為大範圍掃描與聚合最佳化）。兩份共存、同一份資料兩種 layout。</p>
<p>同步是 no-ETL、auto-sync。寫入 transactional store 後、平台在背景把變更同步到 analytical store（通常分鐘級延遲、時間敏感、查文件）。team 不寫 ETL、不維護 pipeline。</p>
<p>關鍵隔離：analytical store query <em>不消耗 OLTP 的 RU</em>。Synapse engine 查 analytical store、走的是 analytical store 的計費與資源、跟 transactional store 的 provisioned RU 分離。這是 federation 對 OLTP 的核心保護 — 分析跑再重也不會 throttle 線上交易。</p>
<p>這是 HTAP（Hybrid Transactional/Analytical Processing）的一種實現：同一資料源、OLTP 與 OLAP 共存、不需要把資料搬到獨立 warehouse 就能做近即時分析。對應 <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> 的「同一份資料、多個各自最佳化的存取路徑」概念。</p>
<h3 id="跟自己搭-change-feed-pipeline-的差別">跟自己搭 Change Feed pipeline 的差別</h3>
<p><a href="../change-feed-cdc/">Change Feed</a> 也能把資料同步到別處做分析、但那要自己寫 consumer、自己維護 target store、自己處理 schema 演進與 backfill。Synapse Link 是平台託管的 analytical store + auto-sync、省掉這整條 pipeline。判準：需求是「Cosmos 資料的近即時 column-oriented 分析」、Synapse Link 直接給；需求是「自訂 transform、餵特定下游、複雜 routing」、Change Feed 提供控制權但要自己搭。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="在-container-啟用-analytical-store">在 container 啟用 analytical store</h3>





<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"># 建 container 時開 analytical store TTL（-1 = 跟 transactional 同壽命）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">az cosmosdb sql container create <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --account-name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --database-name catalog --name orders <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --partition-key-path <span class="s2">&#34;/customerId&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --analytical-storage-ttl -1</span></span></code></pre></div><p>驗證：container 的 <code>analyticalStorageTtl</code> 已設；account 層的 Synapse Link feature 已啟用（account 設定、時間敏感、查文件）。注意 analytical store 通常需要 <em>建 container 時</em> 啟用、既有 container 的開啟支援度要查文件。</p>
<h3 id="從-synapse-查-analytical-store">從 Synapse 查 analytical store</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Synapse serverless SQL pool 直接查 analytical store、不打 OLTP
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">customerId</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">orders</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">revenue</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">OPENROWSET</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="n">PROVIDER</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;CosmosDB&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">CONNECTION</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Account=mycosmos;Database=catalog&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="k">OBJECT</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;orders&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="n">SERVER_CREDENTIAL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;cosmos-cred&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">customerId</span><span class="w"> </span><span class="nb">varchar</span><span class="p">(</span><span class="mi">64</span><span class="p">),</span><span class="w"> </span><span class="n">amount</span><span class="w"> </span><span class="nb">float</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">customerId</span><span class="p">;</span></span></span></code></pre></div><p>驗證：query 跑大範圍聚合期間、Cosmos OLTP container 的 <code>NormalizedRUConsumption</code> <em>不受影響</em>（這是 federation 隔離生效的關鍵證據）。對照同樣 query 直接打 transactional store、會看到 RU 飆升甚至 429。</p>
<h3 id="驗證同步延遲">驗證同步延遲</h3>
<p>寫一筆到 transactional store、隔一段時間在 analytical store 查到 — 量同步延遲（分鐘級）。驗證：延遲在業務可接受的分析新鮮度範圍內；要秒級新鮮度的分析、Synapse Link 不是對的工具。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>Synapse Link 是讀取側 federation、停用不影響 transactional store 的 OLTP。analytical store 是衍生複本、刪掉重建可重新同步（從 transactional store）。OLTP 寫入路徑完全不受 analytical store 啟用與否影響。</p>
<h2 id="何時分出去何時-federate-到專用-olap">何時分出去、何時 federate 到專用 OLAP</h2>
<p>這是本文主判讀段。Synapse Link 在「OLTP 資料要近即時分析、但不想犧牲 OLTP 容量也不想搭 ETL」的場景成立；它不是所有分析需求的答案。</p>
<p>用 Synapse Link（在 Cosmos federation 內做分析）的條件：</p>
<ul>
<li>分析的主資料源就是 Cosmos OLTP container、且分析可接受分鐘級新鮮度</li>
<li>主要痛點是「分析 query 搶 OLTP 的 RU」— federation 的 RU 隔離直接解這個</li>
<li>不想維護 ETL pipeline — no-ETL auto-sync 省掉這條</li>
<li>分析 query 形狀適合 column-oriented 掃描聚合（多數 BI / 報表 / 彙總）</li>
</ul>
<p>把分析 workload federate 到專用 OLAP（BigQuery / Snowflake / 專用 warehouse）的條件：</p>
<ul>
<li>分析要 <em>跨多個資料源</em> join（Cosmos + 其他 DB + 外部資料）— 需要一個獨立的 warehouse 做集中、Synapse Link 只給 Cosmos 單源</li>
<li>分析是重型 data warehouse workload（複雜多表 join、長期歷史、大規模 transform）— 專用 OLAP 的引擎與成本模型更合適</li>
<li>已有成熟的 data platform（Snowflake / BigQuery / lakehouse）、Cosmos 只是其中一個 source — 把 Cosmos 資料用 Change Feed / connector 餵進既有 platform、不另起 Synapse Link</li>
</ul>
<p>判讀句：Synapse Link 是 <em>Cosmos 單源、近即時、column-oriented</em> 分析的省力路徑；分析需求一旦跨源、變重型 warehouse、或已有集中 data platform、就 federate 到專用 OLAP。Cosmos DB overview 已標明「純 OLAP 分析」交給 Synapse / BigQuery / Snowflake — Synapse Link 是兩者之間的橋、不是把 Cosmos 變成 data warehouse。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="不啟用-synapse-link直接在-oltp-跑分析">不啟用 Synapse Link、直接在 OLTP 跑分析</h3>
<p>team 在 OLTP container 直接跑全表聚合報表、分析 query 吃光 provisioned RU、線上交易 429。徵兆是「跑月報的時段、線上交易 latency 飆 / 出現 throttle」。修法是啟用 analytical store + Synapse Link、分析 query 改打 analytical store、RU 隔離後 OLTP 不再受影響；或退一步、把分析 query 移到離峰、但這只是緩解、根本解是 federation 隔離。</p>
<h3 id="期待-analytical-store-即時反映寫入">期待 analytical store 即時反映寫入</h3>
<p>把 Synapse Link 當即時分析用、寫入後立刻在 analytical store 查、查不到剛寫的。analytical store 同步是分鐘級、不是即時。徵兆是「剛下的訂單在分析報表看不到」。修法是接受分析的分鐘級新鮮度、需要即時數字的場景（如即時庫存）走 OLTP 點查、不走 analytical store。</p>
<h3 id="把-synapse-link-當跨源-data-warehouse">把 Synapse Link 當跨源 data warehouse</h3>
<p>分析需要 join Cosmos 資料與其他系統的資料、期待 Synapse Link 解決、發現 analytical store 只有 Cosmos 單一 container / account 的資料。徵兆是「分析做到一半發現缺其他系統的維度資料、Synapse Link 帶不進來」。修法是跨源分析用獨立 warehouse（BigQuery / Snowflake / Synapse dedicated pool）集中、Cosmos 資料用 Synapse Link 或 Change Feed 餵進去當其中一個 source、不期待 Synapse Link 自己做跨源 join。</p>
<h3 id="既有-container-才想開發現要重建">既有 container 才想開、發現要重建</h3>
<p>analytical store 通常要建 container 時啟用、production 跑一陣子才想開、發現既有 container 的開啟有限制（時間敏感、查文件）、可能要新建 container + 遷資料。徵兆是「想開 analytical store 但介面不讓開 / 要重建」。修法是新 container 規劃時就評估未來是否需要分析、預先開 analytical store TTL（不用時成本影響有限）；既有 container 要開時、按文件評估是否需建新 container 遷移。</p>
<h3 id="anti-recommendation分析需求很輕不要起-federation">Anti-recommendation：分析需求很輕不要起 federation</h3>
<p>分析只是偶爾跑、資料量小、OLTP RU 有餘裕扛、且新鮮度要求即時 — 這種場景直接在 OLTP 上 query 或加少量 read 容量更簡單、不需要 analytical store 的額外儲存與 Synapse 的接入。Synapse Link 的價值在「分析會搶 OLTP 容量」或「不想搭 ETL」這兩個痛點明確時才成立；痛點不存在就引入 federation 是多一層東西要管。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：OLTP container 的 <code>NormalizedRUConsumption</code>（驗證分析 query 沒污染它）、analytical store 同步延遲、Synapse 端 query 的掃描量與成本</li>
<li>成本模型分離：analytical store 有獨立的 storage + 寫入計費、Synapse query 有自己的計費（serverless 按掃描量、dedicated 按 pool）— 跟 OLTP 的 RU 完全分開、不要混進 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> 的 RU 公式、那篇主寫 transactional store 的 RU</li>
<li>federation 的隔離證據：跑重型分析時 OLTP RU 平穩、就是 federation 生效；若 OLTP RU 仍隨分析波動、表示分析 query 其實打到了 transactional store、要檢查 query 是否真的走 analytical store</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：OLTP 容量與 analytical 容量分兩條 budget 規劃、這正是 federation 的容量規劃價值 — 兩個 workload 不再互相競爭資源</li>
<li>Alert：analytical store 同步延遲異常增長、OLTP RU 出現非預期的分析時段波動（隔離失效）</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../change-feed-cdc/">change-feed-cdc</a>（自訂 transform / 跨源 routing 用 Change Feed、近即時 Cosmos 單源分析用 Synapse Link）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（analytical store 成本獨立於 OLTP RU、不混算）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（analytical store 是分鐘級延遲的衍生複本、不適用 OLTP 的 consistency level 語義）</li>
<li>federation 概念：<a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> — OLTP / OLAP 各自最佳化 store + 平台同步</li>
<li>跨源 / 重型分析的升級路由：Synapse dedicated pool / BigQuery / Snowflake — Cosmos DB overview「純 OLAP 分析」段已標明</li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「跟 Azure Synapse Link 整合（OLTP / OLAP federation）」backlog 與「純 OLAP 分析」不適用場景</li>
<li>Microsoft 365 analytics 主 anchor：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30</a> — analytics workload 建在 Cosmos 上的情境</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 Synapse Link backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365 case</a> — Cosmos 上的全球分析平台情境 anchor</li>
<li><a href="../change-feed-cdc/">change-feed-cdc</a> — 自訂 pipeline 的對照路徑</li>
<li><a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> — OLTP RU 與 analytical 成本的分離</li>
<li><a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">Federation 卡片</a> — OLTP / OLAP federation 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/synapse-link">Azure Synapse Link for Cosmos DB</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/analytical-store-introduction">Analytical store</a></li>
</ul>
]]></content:encoded></item><item><title>7.5 傳輸信任與憑證生命週期</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/</guid><description>&lt;p>本章的責任是把跨邊界通訊風險拆成信任鏈節點，讓連線完整性、會話收斂與憑證節奏可以一致治理。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦信任鏈治理、會話收斂、憑證生命周期與第三方傳導。案例在問題被觸發時提供佐證。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：會話收斂節奏落後 / 憑證輪替覆蓋不足 / 管理平面傳輸混層 / 第三方信任重評估延遲。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>身分授權 → &lt;a href="../identity-access-boundary/">7.2&lt;/a>&lt;/li>
&lt;li>入口暴露 → &lt;a href="../entrypoint-and-server-protection/">7.3&lt;/a>&lt;/li>
&lt;li>機器憑證 → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>workload &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.10&lt;/a>&lt;/li>
&lt;li>artifact 信任 → &lt;a href="../supply-chain-integrity-and-artifact-trust/">7.12&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[session-invalidation]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="傳輸信任模型">傳輸信任模型&lt;/h2>
&lt;p>傳輸信任的核心責任是定義連線兩端如何被驗證，以及信任失效時如何快速收斂。&lt;/p>
&lt;ol>
&lt;li>端點驗證：確認服務端與客戶端身份可驗證。&lt;/li>
&lt;li>會話完整性：確認連線與 token 不可被重放或跨情境復用。&lt;/li>
&lt;li>憑證節奏：確認簽發、輪替、撤銷與到期處置可追蹤。&lt;/li>
&lt;li>平面隔離：確認管理流量與業務流量使用不同信任邊界。&lt;/li>
&lt;li>第三方重評估：確認外部事件後內部信任關係可重建。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「連線可用」轉成「連線可信」。&lt;/p>
&lt;ol>
&lt;li>先判讀異常發生在握手、會話或憑證狀態。&lt;/li>
&lt;li>再判讀是否涉及管理平面或高價值資料路徑。&lt;/li>
&lt;li>接著啟動會話收斂、憑證撤銷與替代路徑切換。&lt;/li>
&lt;li>最後交接到可靠性驗證與 incident 收斂流程。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>會話收斂節奏落後&lt;/td>
 &lt;td>修補後異常 session 延續&lt;/td>
 &lt;td>事件關閉窗口延長&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 + 05&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>憑證輪替覆蓋不足&lt;/td>
 &lt;td>輪替完成率偏低、失效窗口過長&lt;/td>
 &lt;td>信任鏈可利用窗口維持&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">website-certificate-lifecycle&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">certificate-revocation&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>管理平面傳輸混層&lt;/td>
 &lt;td>管理流量與業務流量共用邊界&lt;/td>
 &lt;td>高權限邊界可被橫向利用&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方信任重評估延遲&lt;/td>
 &lt;td>外部事件後內部憑證收斂滯後&lt;/td>
 &lt;td>傳導風險停留在生產路徑&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨章議題交叉引用">跨章議題交叉引用&lt;/h2>
&lt;p>本章「第三方信任重評估延遲」是 &lt;a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導&lt;/a> 在傳輸層的展現；canonical SSoT 在 7.2、本條補憑證收斂滯後的 specific 訊號。&lt;/p>
&lt;h2 id="會話重放跟全域失效canonical">會話重放跟全域失效（canonical）&lt;/h2>
&lt;p>會話重放是傳輸層獨有的失效模式：攻擊者不需要重新驗證、只需要把 &lt;em>已通過驗證&lt;/em> 的會話資料拿到新環境播放。控制責任是讓會話的「可重放窗口」短於攻擊者的「重放準備時間」、這條 chain 跟登入層的強認證是不同責任。&lt;/p>
&lt;p>會話收斂節奏的 canonical 在本章；&lt;a href="../identity-access-boundary/#%e9%ab%98%e6%ac%8a%e9%99%90%e5%b7%a5%e5%85%b7%e7%9a%84%e6%9c%83%e8%a9%b1%e6%94%b6%e6%96%82%e7%af%80%e5%a5%8f">7.2 identity-access-boundary&lt;/a> 從身分視角補 token 撤銷時間窗口的 specific 訊號、&lt;a href="../entrypoint-and-server-protection/#%e9%82%8a%e7%95%8c%e8%a8%ad%e5%82%99%e4%ba%8b%e4%bb%b6%e7%9a%84%e4%b8%89%e5%90%8c%e6%ad%a5-mechanism">7.3 entrypoint&lt;/a> 從邊界設備視角補「修補 / 失效 / 清查」三同步並行需求。&lt;/p>
&lt;p>對應 &lt;a href="../red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/">Citrix Bleed 2023&lt;/a>：揭露三層失效控制面 — 會話機制缺少快速失效策略、邊界事件後憑證與會話輪替未即時執行、會話異常偵測與告警關聯不足。案例「可落地檢查點」標明事故中 mechanism 為「修補、全域失效、強制重新登入同步執行」，日常監控「異常地理位置與設備指紋切換」。&lt;/p>
&lt;p>以下基於通用工程知識補充：全域 session 失效的工程意義是讓重放窗口從「token 自然到期」縮成「事件確認後分鐘級」。失效路徑要在日常設計時就完成驗證、確保全域 kill switch 在事件當下可立即觸發；缺位時要在日常演練回頭補。使用者 session 走強制 re-auth 路徑、服務間 session 透過 issuer 端撤銷 — 兩條 lever 不同、事件期間需各自獨立準備。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把跨邊界通訊風險拆成信任鏈節點，讓連線完整性、會話收斂與憑證節奏可以一致治理。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦信任鏈治理、會話收斂、憑證生命周期與第三方傳導。案例在問題被觸發時提供佐證。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：會話收斂節奏落後 / 憑證輪替覆蓋不足 / 管理平面傳輸混層 / 第三方信任重評估延遲。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>身分授權 → <a href="../identity-access-boundary/">7.2</a></li>
<li>入口暴露 → <a href="../entrypoint-and-server-protection/">7.3</a></li>
<li>機器憑證 → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li>workload <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> → <a href="../workload-identity-and-federated-trust/">7.10</a></li>
<li>artifact 信任 → <a href="../supply-chain-integrity-and-artifact-trust/">7.12</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[session-invalidation]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="傳輸信任模型">傳輸信任模型</h2>
<p>傳輸信任的核心責任是定義連線兩端如何被驗證，以及信任失效時如何快速收斂。</p>
<ol>
<li>端點驗證：確認服務端與客戶端身份可驗證。</li>
<li>會話完整性：確認連線與 token 不可被重放或跨情境復用。</li>
<li>憑證節奏：確認簽發、輪替、撤銷與到期處置可追蹤。</li>
<li>平面隔離：確認管理流量與業務流量使用不同信任邊界。</li>
<li>第三方重評估：確認外部事件後內部信任關係可重建。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「連線可用」轉成「連線可信」。</p>
<ol>
<li>先判讀異常發生在握手、會話或憑證狀態。</li>
<li>再判讀是否涉及管理平面或高價值資料路徑。</li>
<li>接著啟動會話收斂、憑證撤銷與替代路徑切換。</li>
<li>最後交接到可靠性驗證與 incident 收斂流程。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>會話收斂節奏落後</td>
          <td>修補後異常 session 延續</td>
          <td>事件關閉窗口延長</td>
          <td><a href="/blog/backend/knowledge-cards/session-invalidation/" data-link-title="Session Invalidation" data-link-desc="說明事件後如何讓既有會話失效，避免被重放或延續利用">session-invalidation</a>、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a></td>
          <td><code>08 + 05</code></td>
      </tr>
      <tr>
          <td>憑證輪替覆蓋不足</td>
          <td>輪替完成率偏低、失效窗口過長</td>
          <td>信任鏈可利用窗口維持</td>
          <td><a href="/blog/backend/knowledge-cards/website-certificate-lifecycle/" data-link-title="Website Certificate Lifecycle" data-link-desc="說明網站 TLS 憑證從簽發到續期與撤銷的全流程責任">website-certificate-lifecycle</a>、<a href="/blog/backend/knowledge-cards/certificate-revocation/" data-link-title="Certificate Revocation" data-link-desc="說明憑證洩漏或誤發時如何撤銷並控制影響範圍">certificate-revocation</a></td>
          <td><code>05 + 06</code></td>
      </tr>
      <tr>
          <td>管理平面傳輸混層</td>
          <td>管理流量與業務流量共用邊界</td>
          <td>高權限邊界可被橫向利用</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane</a>、<a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary</a></td>
          <td><code>05 + 08</code></td>
      </tr>
      <tr>
          <td>第三方信任重評估延遲</td>
          <td>外部事件後內部憑證收斂滯後</td>
          <td>傳導風險停留在生產路徑</td>
          <td><a href="/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation</a>、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity</a></td>
          <td><code>08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="跨章議題交叉引用">跨章議題交叉引用</h2>
<p>本章「第三方信任重評估延遲」是 <a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導</a> 在傳輸層的展現；canonical SSoT 在 7.2、本條補憑證收斂滯後的 specific 訊號。</p>
<h2 id="會話重放跟全域失效canonical">會話重放跟全域失效（canonical）</h2>
<p>會話重放是傳輸層獨有的失效模式：攻擊者不需要重新驗證、只需要把 <em>已通過驗證</em> 的會話資料拿到新環境播放。控制責任是讓會話的「可重放窗口」短於攻擊者的「重放準備時間」、這條 chain 跟登入層的強認證是不同責任。</p>
<p>會話收斂節奏的 canonical 在本章；<a href="../identity-access-boundary/#%e9%ab%98%e6%ac%8a%e9%99%90%e5%b7%a5%e5%85%b7%e7%9a%84%e6%9c%83%e8%a9%b1%e6%94%b6%e6%96%82%e7%af%80%e5%a5%8f">7.2 identity-access-boundary</a> 從身分視角補 token 撤銷時間窗口的 specific 訊號、<a href="../entrypoint-and-server-protection/#%e9%82%8a%e7%95%8c%e8%a8%ad%e5%82%99%e4%ba%8b%e4%bb%b6%e7%9a%84%e4%b8%89%e5%90%8c%e6%ad%a5-mechanism">7.3 entrypoint</a> 從邊界設備視角補「修補 / 失效 / 清查」三同步並行需求。</p>
<p>對應 <a href="../red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/">Citrix Bleed 2023</a>：揭露三層失效控制面 — 會話機制缺少快速失效策略、邊界事件後憑證與會話輪替未即時執行、會話異常偵測與告警關聯不足。案例「可落地檢查點」標明事故中 mechanism 為「修補、全域失效、強制重新登入同步執行」，日常監控「異常地理位置與設備指紋切換」。</p>
<p>以下基於通用工程知識補充：全域 session 失效的工程意義是讓重放窗口從「token 自然到期」縮成「事件確認後分鐘級」。失效路徑要在日常設計時就完成驗證、確保全域 kill switch 在事件當下可立即觸發；缺位時要在日常演練回頭補。使用者 session 走強制 re-auth 路徑、服務間 session 透過 issuer 端撤銷 — 兩條 lever 不同、事件期間需各自獨立準備。</p>
<h2 id="簽章金鑰失效時的驗證路徑收斂">簽章金鑰失效時的驗證路徑收斂</h2>
<p>簽章金鑰治理的 canonical 在 <a href="../secrets-and-machine-credential-governance/#%e7%b0%bd%e7%ab%a0%e9%87%91%e9%91%b0%e8%b7%9f%e9%95%b7%e6%9c%9f%e4%bf%a1%e4%bb%bb%e6%a0%b9">7.6 secrets governance § 簽章金鑰跟長期信任根</a>（含 material 保護）。本節聚焦傳輸層的 specific 訊號 — 簽章金鑰失效時、驗證路徑能否在 fleet 層級熱抽換 issuer、決定信任鏈重建的速度。</p>
<p>對應 <a href="../red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/">Microsoft Storm-0558 2023</a>：揭露的「權杖驗證邊界缺少跨服務一致性檢查」屬本章傳輸層責任。案例「可落地檢查點」標明 mechanism 是「監控跨租戶 token 出現相同 issuer 但不應跨域的軌跡」、並標明前提是 token validation 路徑可在 fleet 層級熱抽換 issuer。</p>
<p>以下基於通用工程知識補充：fleet 層級熱抽換屬日常基礎設施的能力前提、要在日常設計階段內建、事件期間才補通常會把重建時間拉長到小時 / 天級。常見落差是 token validation 邏輯被嵌進個別 service 的 library、抽換 issuer 等於重 deploy 每個 service。傳輸層治理要把這個能力當前提條件、缺位時要在 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5.x deployment platform</a> 跟基礎設施團隊協作補上。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是判斷何時要升級信任鏈處置等級。</p>
<ul>
<li>修補後異常會話仍活躍時，代表會話收斂能力不足。</li>
<li>憑證輪替覆蓋率長期偏低時，代表信任鏈存在長窗口暴露。</li>
<li>管理平面與業務平面共用同一傳輸邊界時，代表高權限流量隔離不足。</li>
<li>外部公告後內部仍保留高風險憑證時，代表第三方信任重評估延遲。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證傳輸與憑證治理能否承受事件壓力。</p>
<ul>
<li>會話被竊取與重放壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023</a></li>
<li>VPN 通道漏洞與信任鏈衝擊： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/" data-link-title="7.R7.3.8 Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口" data-link-desc="VPN 邊界漏洞發生時，入口隔離與修補節奏需要同時啟動">Fortinet SSL VPN 2024</a></li>
<li>第三方身分鏈事件後收斂壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>連線與憑證配置：<code>05-deployment-platform</code></li>
<li>輪替與驗證節奏：<code>06-reliability</code></li>
<li>事件收斂流程：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.6 秘密管理與機器憑證治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/</guid><description>&lt;p>本章的責任是把機器身份與憑證風險拆成分域治理模型，讓 secret、token、key 的生命周期可以被一致驗證。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦分域策略、生命周期一致性與事件收斂節奏。案例在問題觸發時作為證據參考。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：token 分域不足 / CI secrets 集中 / 憑證生命週期失衡 / 供應商事件傳導未收斂。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>人類身分 → &lt;a href="../identity-access-boundary/">7.2&lt;/a>&lt;/li>
&lt;li>入口暴露 → &lt;a href="../entrypoint-and-server-protection/">7.3&lt;/a>&lt;/li>
&lt;li>傳輸 / 憑證輪替 → &lt;a href="../transport-trust-and-certificate-lifecycle/">7.5&lt;/a>&lt;/li>
&lt;li>workload &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.10&lt;/a>&lt;/li>
&lt;li>build provenance → &lt;a href="../supply-chain-integrity-and-artifact-trust/">7.12&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[token-revocation]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="憑證治理模型">憑證治理模型&lt;/h2>
&lt;p>憑證治理的核心責任是讓每一種機器憑證都有清楚的用途邊界與收斂節奏。&lt;/p>
&lt;ol>
&lt;li>類型分層：區分應用程式 secret、存取 token、簽章 key、部署憑證。&lt;/li>
&lt;li>用途分域：區分讀取、寫入、管理操作的權限邊界。&lt;/li>
&lt;li>環境分域：區分開發、測試、正式環境，避免跨環境共用憑證。&lt;/li>
&lt;li>生命周期：定義發放、輪替、撤銷、淘汰的責任與時窗。&lt;/li>
&lt;li>事件收斂：定義外部事件後的內部權限回收與驗證流程。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「可用憑證」轉成「可控憑證」。&lt;/p>
&lt;ol>
&lt;li>先盤點憑證是否與服務邊界一致。&lt;/li>
&lt;li>再判讀憑證是否存在過寬 scope、過長 TTL 或過多共享。&lt;/li>
&lt;li>接著判讀事件發生後是否能在時限內完成撤銷與替換。&lt;/li>
&lt;li>最後把缺口路由到部署面、可靠性演練與 incident workflow。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>token 分域不足&lt;/td>
 &lt;td>高權限 token 使用面過寬&lt;/td>
 &lt;td>外部事件可快速傳導&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI secrets 集中&lt;/td>
 &lt;td>單一節點承載大量憑證&lt;/td>
 &lt;td>輪替成本與中斷風險上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret-management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 + 06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>憑證生命周期失衡&lt;/td>
 &lt;td>發放、更新、撤銷節奏分離&lt;/td>
 &lt;td>可用憑證存量高於收斂速度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應商事件傳導未收斂&lt;/td>
 &lt;td>外部事件後內部憑證仍活躍&lt;/td>
 &lt;td>內部風險延長停留&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact-scope&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨章議題交叉引用">跨章議題交叉引用&lt;/h2>
&lt;p>本章「供應商事件傳導未收斂」是 &lt;a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導&lt;/a> 在機器憑證層的展現；canonical SSoT 在 7.2、本條補憑證仍活躍的 specific 訊號。&lt;/p>
&lt;h2 id="ci-secrets-集中化跟-blast-radius">CI secrets 集中化跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>&lt;/h2>
&lt;p>CI secrets 集中化的核心風險是把 &lt;em>單一節點承載的憑證數量&lt;/em> 跟 &lt;em>事件期間需要輪替的範圍&lt;/em> 綁在一起。當 CI 平台被入侵、可暴露的範圍就是該平台所有 secrets 的集合；治理層要在事件發生前把這個集合切小、不是事件後試圖縮範圍。&lt;/p>
&lt;p>&lt;a href="../red-team/cases/supply-chain/circleci-2023-secrets-rotation/">CircleCI 2023&lt;/a> 揭露三條互相強化的失效訊號 — CI secrets 集中化且缺少分域隔離、輪替流程成本高（導致執行延遲）、客戶端難以快速判斷最小必要輪替範圍。案例「可落地檢查點」直接列出 mechanism「定義 secrets 分級與依賴地圖、依 blast radius 分層、不只依名稱」屬可引用範圍、前提條件是事先有 secrets inventory 跟 owner mapping。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把機器身份與憑證風險拆成分域治理模型，讓 secret、token、key 的生命周期可以被一致驗證。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦分域策略、生命周期一致性與事件收斂節奏。案例在問題觸發時作為證據參考。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：token 分域不足 / CI secrets 集中 / 憑證生命週期失衡 / 供應商事件傳導未收斂。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>人類身分 → <a href="../identity-access-boundary/">7.2</a></li>
<li>入口暴露 → <a href="../entrypoint-and-server-protection/">7.3</a></li>
<li>傳輸 / 憑證輪替 → <a href="../transport-trust-and-certificate-lifecycle/">7.5</a></li>
<li>workload <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> → <a href="../workload-identity-and-federated-trust/">7.10</a></li>
<li>build provenance → <a href="../supply-chain-integrity-and-artifact-trust/">7.12</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[token-revocation]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="憑證治理模型">憑證治理模型</h2>
<p>憑證治理的核心責任是讓每一種機器憑證都有清楚的用途邊界與收斂節奏。</p>
<ol>
<li>類型分層：區分應用程式 secret、存取 token、簽章 key、部署憑證。</li>
<li>用途分域：區分讀取、寫入、管理操作的權限邊界。</li>
<li>環境分域：區分開發、測試、正式環境，避免跨環境共用憑證。</li>
<li>生命周期：定義發放、輪替、撤銷、淘汰的責任與時窗。</li>
<li>事件收斂：定義外部事件後的內部權限回收與驗證流程。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「可用憑證」轉成「可控憑證」。</p>
<ol>
<li>先盤點憑證是否與服務邊界一致。</li>
<li>再判讀憑證是否存在過寬 scope、過長 TTL 或過多共享。</li>
<li>接著判讀事件發生後是否能在時限內完成撤銷與替換。</li>
<li>最後把缺口路由到部署面、可靠性演練與 incident workflow。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>token 分域不足</td>
          <td>高權限 token 使用面過寬</td>
          <td>外部事件可快速傳導</td>
          <td><a href="/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation</a>、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a></td>
          <td><code>08</code></td>
      </tr>
      <tr>
          <td>CI secrets 集中</td>
          <td>單一節點承載大量憑證</td>
          <td>輪替成本與中斷風險上升</td>
          <td><a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret-management</a>、<a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline</a></td>
          <td><code>05 + 06</code></td>
      </tr>
      <tr>
          <td>憑證生命周期失衡</td>
          <td>發放、更新、撤銷節奏分離</td>
          <td>可用憑證存量高於收斂速度</td>
          <td><a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a>、<a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a></td>
          <td><code>06 + 08</code></td>
      </tr>
      <tr>
          <td>供應商事件傳導未收斂</td>
          <td>外部事件後內部憑證仍活躍</td>
          <td>內部風險延長停留</td>
          <td><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline</a>、<a href="/blog/backend/knowledge-cards/impact-scope/" data-link-title="Impact Scope" data-link-desc="說明事故中如何盤點受影響範圍，支持通報、回復與責任判讀">impact-scope</a></td>
          <td><code>08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="跨章議題交叉引用">跨章議題交叉引用</h2>
<p>本章「供應商事件傳導未收斂」是 <a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導</a> 在機器憑證層的展現；canonical SSoT 在 7.2、本條補憑證仍活躍的 specific 訊號。</p>
<h2 id="ci-secrets-集中化跟-blast-radius">CI secrets 集中化跟 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a></h2>
<p>CI secrets 集中化的核心風險是把 <em>單一節點承載的憑證數量</em> 跟 <em>事件期間需要輪替的範圍</em> 綁在一起。當 CI 平台被入侵、可暴露的範圍就是該平台所有 secrets 的集合；治理層要在事件發生前把這個集合切小、不是事件後試圖縮範圍。</p>
<p><a href="../red-team/cases/supply-chain/circleci-2023-secrets-rotation/">CircleCI 2023</a> 揭露三條互相強化的失效訊號 — CI secrets 集中化且缺少分域隔離、輪替流程成本高（導致執行延遲）、客戶端難以快速判斷最小必要輪替範圍。案例「可落地檢查點」直接列出 mechanism「定義 secrets 分級與依賴地圖、依 blast radius 分層、不只依名稱」屬可引用範圍、前提條件是事先有 secrets inventory 跟 owner mapping。</p>
<p>以下基於通用工程知識補充：secrets 分級的工程意義是讓事件期間的輪替能按風險排序、不靠 ad-hoc 判斷。缺分級時、組織要在壓力下做全面輪替、容易造成服務中斷或遺漏。日常演練要包含「假設整個 CI vendor 受損」的 fire drill、確認輪替路徑能在 vendor 失能時仍可執行，這是 7.6 跟 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.x reliability</a> 演練面的共同訴求。</p>
<h2 id="簽章金鑰跟長期信任根">簽章金鑰跟長期信任根</h2>
<p>簽章金鑰是憑證治理的最高層信任根、生命週期治理要跟一般 token 分開。簽章金鑰一旦失守、攻擊者能偽造 <em>可被驗證</em> 的 token、繞過所有依賴該 issuer 的下游驗證；這跟一般 token 洩漏（仍受 token 自身 scope 限制）是不同層級的失效。</p>
<p>本節是簽章金鑰治理的 canonical（含 material 保護跟 lifecycle 視角）；驗證路徑層的 specific 訊號（fleet 層級 issuer 熱抽換）見 <a href="../transport-trust-and-certificate-lifecycle/#%e7%b0%bd%e7%ab%a0%e9%87%91%e9%91%b0%e5%a4%b1%e6%95%88%e6%99%82%e7%9a%84%e9%a9%97%e8%ad%89%e8%b7%af%e5%be%91%e6%94%b6%e6%96%82">7.5 簽章金鑰失效時的驗證路徑收斂</a>。</p>
<p>對應 <a href="../red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/">Microsoft Storm-0558 2023</a>：揭露三層失效控制面 — 簽章金鑰生命週期治理與隔離策略不足、權杖驗證邊界缺少跨服務一致性檢查、高風險身分事件追查與升級節奏偏慢。本章聚焦第一層 material 保護、第二層 validation 路徑由 7.5 處理。案例「可落地檢查點」標明 mechanism 為「把簽章金鑰納入硬體保護與輪替節奏（HSM-bound、不可導出、強制輪替週期）」。</p>
<p>以下基於通用工程知識補充：簽章金鑰治理由材料保護跟驗證路徑兩條 chain 構成 — <em>材料保護</em> 用 HSM-bound（不可導出 + 強制輪替）處理金鑰本體（本章責任）、<em>驗證路徑</em> 用 fleet 層級熱抽換能力處理 issuer 切換（7.5 責任）。兩條 chain 構成單一信任根的雙重防線、任一邊失能會把另一邊的工程投資清零（材料外洩時若 issuer 無法熱抽換、攻擊窗口會延長到所有 fleet 完成 deploy；驗證路徑可熱抽換但金鑰可被導出時、攻擊者仍能離線濫用）。實作層的具體選型（HSM 廠商 / 雲託管 KMS）屬於 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">5.x deployment platform</a> 範圍、本章不展開。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是定義何時要把憑證管理從日常維運升級成事件處置。</p>
<ul>
<li>同一 token 在多服務、多環境長期可用時，代表分域策略已鬆動。</li>
<li>CI 節點可同時取得大量正式環境 secrets 時，代表供應鏈傳導半徑過大。</li>
<li>事件公告後舊憑證仍可持續使用時，代表撤銷節奏落後於攻擊節奏。</li>
<li>憑證輪替缺乏回退驗證時，代表可用性與安全性同時承壓。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是檢查憑證治理是否具備現實抗壓能力。</p>
<ul>
<li>CI secrets 事件與輪替壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023</a></li>
<li>第三方身分鏈導致內部風險傳導： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a></li>
<li>開源供應鏈長期滲透壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>交付與執行環境：<code>05-deployment-platform</code>（tunnel 憑證的保管與輪替見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>）</li>
<li>輪替與回退演練：<code>06-reliability</code></li>
<li>事件收斂與通報：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.7 稽核追蹤與責任邊界</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/</guid><description>&lt;p>本章的責任是把高風險操作轉成可回查的證據鏈，讓事故期間能快速界定責任、排序處置與回寫改進。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦證據模型、責任鏈與跨部門節奏。案例在問題節點被觸發時作為判讀佐證。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：稽核欄位結構缺漏 / 代理與批准節奏脫鉤 / 跨部門通報節奏失衡 / 平台級事件責任混層。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>資料分級與遮罩 → &lt;a href="../data-protection-and-masking-governance/">7.4&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../detection-coverage-and-signal-governance/">7.13&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[audit-log]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="稽核與責任模型">稽核與責任模型&lt;/h2>
&lt;p>稽核治理的核心責任是讓每一個關鍵操作都能回答「誰、為何、何時、在哪裡、對什麼資產做了什麼」。&lt;/p>
&lt;ol>
&lt;li>證據模型：主體、目的、資產、動作、結果、關聯事件 ID。&lt;/li>
&lt;li>責任鏈模型：提交者、批准者、執行者與值班決策者分層記錄。&lt;/li>
&lt;li>時序模型：技術時序與業務時序同時可回查，避免單一時間軸誤判。&lt;/li>
&lt;li>切分模型：平台責任與產品責任明確交界，降低指揮混亂。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「事件描述」轉成「可證明的責任判讀」。&lt;/p>
&lt;ol>
&lt;li>先檢查關鍵欄位是否完整並可關聯。&lt;/li>
&lt;li>再檢查批准與執行時序是否一致。&lt;/li>
&lt;li>接著檢查跨部門通報節奏是否同步。&lt;/li>
&lt;li>最後交接到 incident 指揮與復盤流程。&lt;/li>
&lt;/ol>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>稽核欄位結構缺漏&lt;/td>
 &lt;td>主體、目的、資產欄位不完整&lt;/td>
 &lt;td>事故回查效率下降&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>代理與批准節奏脫鉤&lt;/td>
 &lt;td>變更事件與批准事件時序偏移&lt;/td>
 &lt;td>責任邊界判讀成本上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident-command-system&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨部門通報節奏失衡&lt;/td>
 &lt;td>技術更新與對外訊息不同步&lt;/td>
 &lt;td>決策一致性下降&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-communication-channel/" data-link-title="Incident Communication Channel" data-link-desc="說明事故期間內外部溝通要使用哪些固定通道與節奏">incident-communication-channel&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident-review&lt;/a>&lt;/td>
 &lt;td>&lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>平台級事件責任混層&lt;/td>
 &lt;td>平台與產品責任切分不清&lt;/td>
 &lt;td>收斂順序與優先級混亂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="token-audit-跟跨工具回查壓力">Token audit 跟跨工具回查壓力&lt;/h2>
&lt;p>跨工具回查的稽核責任是把同一身分在多個工具的足跡 &lt;em>對齊重組&lt;/em> — 當 audit 欄位的主體 / 資產 / 操作 ID 在不同工具之間不對齊、回查時間會以小時或天計、超過攻擊者擴散的時間尺度。&lt;/p>
&lt;p>對應 &lt;a href="../red-team/cases/identity-access/uber-2022-mfa-fatigue/">Uber 2022&lt;/a> 跟 &lt;a href="../red-team/cases/identity-access/slack-2022-token-compromise/">Slack 2022&lt;/a>：兩個案例分別在身分監控層揭露同類失效訊號 — Uber 失效控制面標明「身分異常事件與值班告警串接不足」、Slack 標明「程式碼資產存取異常訊號未快速匯流」。本章把兩者抽象為「跨工具回查壓力」是稽核視角的合成 frame、非 case 原文框架。Slack 案例「可落地檢查點」直接列出 mechanism 為 detection 層「repo 異常 clone、token 跨 IP / 跨 device 序列」+ incident response 層「分層撤銷 token、以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 框定影響面」、前提是「token 有 inventory 可查 issuer / scope」。&lt;/p>
&lt;p>以下基於通用工程知識補充：跨工具回查的工程瓶頸通常在欄位 schema 不一致 — 同一個 user_id 在 SSO log / 應用 audit / Git 操作記錄裡用不同 key 表示、JOIN 不上時要靠人類 fuzzy match。事件期間的時間壓力下、這層 fuzzy match 是最常出錯的地方。日常治理要把「跨工具 audit 欄位對齊」內建到 schema 設計階段、屬基礎建設層的長期投資。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把高風險操作轉成可回查的證據鏈，讓事故期間能快速界定責任、排序處置與回寫改進。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦證據模型、責任鏈與跨部門節奏。案例在問題節點被觸發時作為判讀佐證。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：稽核欄位結構缺漏 / 代理與批准節奏脫鉤 / 跨部門通報節奏失衡 / 平台級事件責任混層。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>資料分級與遮罩 → <a href="../data-protection-and-masking-governance/">7.4</a></li>
<li>偵測訊號 → <a href="../detection-coverage-and-signal-governance/">7.13</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[audit-log]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="稽核與責任模型">稽核與責任模型</h2>
<p>稽核治理的核心責任是讓每一個關鍵操作都能回答「誰、為何、何時、在哪裡、對什麼資產做了什麼」。</p>
<ol>
<li>證據模型：主體、目的、資產、動作、結果、關聯事件 ID。</li>
<li>責任鏈模型：提交者、批准者、執行者與值班決策者分層記錄。</li>
<li>時序模型：技術時序與業務時序同時可回查，避免單一時間軸誤判。</li>
<li>切分模型：平台責任與產品責任明確交界，降低指揮混亂。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「事件描述」轉成「可證明的責任判讀」。</p>
<ol>
<li>先檢查關鍵欄位是否完整並可關聯。</li>
<li>再檢查批准與執行時序是否一致。</li>
<li>接著檢查跨部門通報節奏是否同步。</li>
<li>最後交接到 incident 指揮與復盤流程。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>稽核欄位結構缺漏</td>
          <td>主體、目的、資產欄位不完整</td>
          <td>事故回查效率下降</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a>、<a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline</a></td>
          <td><code>08</code></td>
      </tr>
      <tr>
          <td>代理與批准節奏脫鉤</td>
          <td>變更事件與批准事件時序偏移</td>
          <td>責任邊界判讀成本上升</td>
          <td><a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、<a href="/blog/backend/knowledge-cards/incident-command-system/" data-link-title="Incident Command System" data-link-desc="說明事故期間的指揮角色、決策邊界與協作方式">incident-command-system</a></td>
          <td><code>08</code></td>
      </tr>
      <tr>
          <td>跨部門通報節奏失衡</td>
          <td>技術更新與對外訊息不同步</td>
          <td>決策一致性下降</td>
          <td><a href="/blog/backend/knowledge-cards/incident-communication-channel/" data-link-title="Incident Communication Channel" data-link-desc="說明事故期間內外部溝通要使用哪些固定通道與節奏">incident-communication-channel</a>、<a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident-review</a></td>
          <td><code>08</code></td>
      </tr>
      <tr>
          <td>平台級事件責任混層</td>
          <td>平台與產品責任切分不清</td>
          <td>收斂順序與優先級混亂</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management-plane</a>、<a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a></td>
          <td><code>06 + 08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="token-audit-跟跨工具回查壓力">Token audit 跟跨工具回查壓力</h2>
<p>跨工具回查的稽核責任是把同一身分在多個工具的足跡 <em>對齊重組</em> — 當 audit 欄位的主體 / 資產 / 操作 ID 在不同工具之間不對齊、回查時間會以小時或天計、超過攻擊者擴散的時間尺度。</p>
<p>對應 <a href="../red-team/cases/identity-access/uber-2022-mfa-fatigue/">Uber 2022</a> 跟 <a href="../red-team/cases/identity-access/slack-2022-token-compromise/">Slack 2022</a>：兩個案例分別在身分監控層揭露同類失效訊號 — Uber 失效控制面標明「身分異常事件與值班告警串接不足」、Slack 標明「程式碼資產存取異常訊號未快速匯流」。本章把兩者抽象為「跨工具回查壓力」是稽核視角的合成 frame、非 case 原文框架。Slack 案例「可落地檢查點」直接列出 mechanism 為 detection 層「repo 異常 clone、token 跨 IP / 跨 device 序列」+ incident response 層「分層撤銷 token、以 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 框定影響面」、前提是「token 有 inventory 可查 issuer / scope」。</p>
<p>以下基於通用工程知識補充：跨工具回查的工程瓶頸通常在欄位 schema 不一致 — 同一個 user_id 在 SSO log / 應用 audit / Git 操作記錄裡用不同 key 表示、JOIN 不上時要靠人類 fuzzy match。事件期間的時間壓力下、這層 fuzzy match 是最常出錯的地方。日常治理要把「跨工具 audit 欄位對齊」內建到 schema 設計階段、屬基礎建設層的長期投資。</p>
<h2 id="資料外送事件的時序壓力">資料外送事件的時序壓力</h2>
<p>查詢行為的可回查性跟匯出活動的責任歸屬是資料外送類事件稽核責任的兩條 chain。當大量 query 跟匯出活動在事後追不到具體的觸發 session 跟業務目的、責任邊界判讀停在「不確定誰做的」階段 — 屬稽核能力不足的明確訊號。</p>
<p>對應 <a href="../red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/">Snowflake 2024</a>：揭露三層失效控制面 — 身分基線未強制 MFA 與條件式存取、查詢行為異常偵測門檻不足、高價值資料匯出控制較弱。案例「可落地檢查點」標明 mechanism 為「異常查詢與匯出告警 — query 體積 / 來源 IP / 跨 schema scan 模式」、並標明該證據鏈要支撐「分批停用可疑憑證、限制外送並啟動調查」的決策。</p>
<p>以下基於通用工程知識補充：資料平台的 audit 設計要把「查詢」升級為第一級事件（跟「登入」同層 schema）、事件期間查詢時序可直接從主 audit stream 抽出、避免從 slow query log / billing log 拼湊。匯出活動的責任歸屬要綁業務目的（ticket 編號 / approval ID）、單綁執行者身份不夠細。</p>
<h2 id="平台級事件的責任切分">平台級事件的責任切分</h2>
<p>兩層 audit 共同承擔平台級事件的責任切分 — 平台 audit 記錄 build pipeline / artifact 來源 / dependency 解析、產品 audit 記錄業務操作 / 資料存取 / 使用者行為。當供應鏈植入的 artifact 出現在產品 build pipeline、產品團隊看到 build 失敗、平台團隊看到 dependency 異常、責任歸屬需要兩邊視角 <em>同時</em> 可回查、才能切清「平台層該收斂什麼」「產品層該回應什麼」。</p>
<p>對應 <a href="../red-team/cases/supply-chain/solarwinds-2020-sunburst/">SolarWinds 2020</a>：案例的失效控制面標明「更新來源信任過於單點」「行為監測難以區分合法元件與惡意利用」「供應鏈異常事件缺少快速隔離流程」。本章把這幾條失效面從供應鏈信任視角延伸到稽核視角、抽象為「平台 vs 產品的責任邊界判讀壓力」— 此 frame 為本章合成、非 case 原文。</p>
<p>以下基於通用工程知識補充：平台跟產品的責任切分要在 audit schema 設計時就分層 — 平台 audit 記錄 build pipeline / artifact 來源 / dependency 解析、產品 audit 記錄業務操作 / 資料存取 / 使用者行為。兩層用 correlation ID 串連、事件期間可獨立查詢、責任歸屬會比 <em>把所有事件混在一個 log stream</em> 容易切清許多。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是判斷何時稽核能力已不足以支撐處置決策。</p>
<ul>
<li>高風險操作缺少主體或資產欄位時，代表證據鏈已斷裂。</li>
<li>批准紀錄與執行紀錄長期無法對齊時，代表責任分工不可驗證。</li>
<li>跨部門訊息更新時差過大時，代表決策節奏正在失衡。</li>
<li>平台事件中無法快速切分產品與平台責任時，代表指揮鏈風險升高。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證責任鏈與稽核模型是否足以支撐高壓情境。</p>
<ul>
<li>身分事件後的跨工具回查壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>資料外送事件的時序與責任壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li>供應鏈事件中的平台責任切分： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>演練與驗證：<code>06-reliability</code></li>
<li>分級、指揮、通報、復盤：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.8 模組路由：問題到服務實作</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/</guid><description>&lt;p>本章的責任是把問題節點轉成跨模組交接規則。核心輸出是交接條件與責任切分，讓概念層與實作層保持同一條決策路徑。&lt;/p>
&lt;h2 id="路由基線">路由基線&lt;/h2>
&lt;p>路由基線的責任是維持章節分工穩定。07 模組先完成問題判讀，再把實作交接到 05/06/08。&lt;/p>
&lt;ol>
&lt;li>先判斷問題節點與影響面。&lt;/li>
&lt;li>再確認判讀訊號與風險等級。&lt;/li>
&lt;li>接著建立收斂順序與責任鏈。&lt;/li>
&lt;li>最後交接到對應實作章節。&lt;/li>
&lt;/ol>
&lt;h2 id="主題路由表問題驅動">主題路由表（問題驅動）&lt;/h2>
&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>身分擴散與授權濫用&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>入口暴露與管理面風險&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料暴露與交換責任鏈&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>信任鏈與憑證節奏&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>06 reliability&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>秘密治理與機器身份&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>稽核證據與責任切分&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7&lt;/a>&lt;/td>
 &lt;td>&lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務生命週期風險節奏&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/" data-link-title="7.9 服務生命週期的資安風險節奏" data-link-desc="定義設計、上線、變更、事故、復盤五段中的資安問題節點">7.9&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workload 聯邦信任&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料駐留與刪除證據鏈&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應鏈與 artifact 信任&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12&lt;/a>&lt;/td>
 &lt;td>&lt;code>05 deployment-platform&lt;/code> + &lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偵測覆蓋與訊號治理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13&lt;/a>&lt;/td>
 &lt;td>&lt;code>04 observability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>例外治理與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14&lt;/a>&lt;/td>
 &lt;td>&lt;code>06 reliability&lt;/code> + &lt;code>08 incident-response&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="章節交接條件">章節交接條件&lt;/h2>
&lt;p>章節交接條件的責任是讓概念層輸出可以被實作層直接使用。&lt;/p>
&lt;ol>
&lt;li>交接前輸出：問題節點、判讀訊號、風險邊界、責任角色。&lt;/li>
&lt;li>交接中輸出：控制面優先序、驗證節奏、回退條件。&lt;/li>
&lt;li>交接後輸出：觀測指標、復盤入口、重新評估觸發器。&lt;/li>
&lt;/ol>
&lt;h2 id="路由決策流程">路由決策流程&lt;/h2>
&lt;p>路由流程的責任是避免章節重複、避免控制面遺漏。&lt;/p>
&lt;ol>
&lt;li>先確認問題是否已超過單一模組可處理範圍。&lt;/li>
&lt;li>再確認優先處理的是入口風險、驗證風險或事故節奏風險。&lt;/li>
&lt;li>接著把問題切成 &lt;code>05 platform&lt;/code>、&lt;code>06 reliability&lt;/code>、&lt;code>08 incident&lt;/code> 的可執行項。&lt;/li>
&lt;li>最後定義回寫點，確保 07 的判讀語言會被下一輪更新。&lt;/li>
&lt;/ol>
&lt;h2 id="交接模板">交接模板&lt;/h2>
&lt;p>交接模板的責任是讓不同章節用同一種輸入輸出格式合作。&lt;/p>
&lt;ul>
&lt;li>問題摘要：一句話描述失效樣式與影響面。&lt;/li>
&lt;li>判讀訊號：列出可觀測事件與觸發閾值。&lt;/li>
&lt;li>風險邊界：列出升級條件與停止條件。&lt;/li>
&lt;li>控制面優先序：列出先做、後做、可延後動作。&lt;/li>
&lt;li>驗證與回退：列出驗證指標、觀察時窗與回退條件。&lt;/li>
&lt;li>回寫規則：列出要更新的章節、卡片與案例索引。&lt;/li>
&lt;/ul>
&lt;h2 id="文件邊界">文件邊界&lt;/h2>
&lt;p>文件邊界的責任是維持模組分工穩定。&lt;/p>
&lt;ul>
&lt;li>&lt;code>07&lt;/code>：定義問題語言、判讀訊號、風險邊界與路由規則。&lt;/li>
&lt;li>&lt;code>05&lt;/code>：落地入口、網路、部署與平台控制面。&lt;/li>
&lt;li>&lt;code>06&lt;/code>：落地驗證、演練、回退與可靠性節奏。&lt;/li>
&lt;li>&lt;code>08&lt;/code>：落地分級、指揮、通報、收斂與復盤閉環。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把問題節點轉成跨模組交接規則。核心輸出是交接條件與責任切分，讓概念層與實作層保持同一條決策路徑。</p>
<h2 id="路由基線">路由基線</h2>
<p>路由基線的責任是維持章節分工穩定。07 模組先完成問題判讀，再把實作交接到 05/06/08。</p>
<ol>
<li>先判斷問題節點與影響面。</li>
<li>再確認判讀訊號與風險等級。</li>
<li>接著建立收斂順序與責任鏈。</li>
<li>最後交接到對應實作章節。</li>
</ol>
<h2 id="主題路由表問題驅動">主題路由表（問題驅動）</h2>
<table>
  <thead>
      <tr>
          <th>問題主題</th>
          <th>概念入口</th>
          <th>交接章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身分擴散與授權濫用</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2</a></td>
          <td><code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>入口暴露與管理面風險</td>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3</a></td>
          <td><code>05 deployment-platform</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>資料暴露與交換責任鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4</a></td>
          <td><code>05 deployment-platform</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>信任鏈與憑證節奏</td>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5</a></td>
          <td><code>05 deployment-platform</code> + <code>06 reliability</code></td>
      </tr>
      <tr>
          <td>秘密治理與機器身份</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6</a></td>
          <td><code>05 deployment-platform</code> + <code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>稽核證據與責任切分</td>
          <td><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7</a></td>
          <td><code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>服務生命週期風險節奏</td>
          <td><a href="/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/" data-link-title="7.9 服務生命週期的資安風險節奏" data-link-desc="定義設計、上線、變更、事故、復盤五段中的資安問題節點">7.9</a></td>
          <td><code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>Workload 聯邦信任</td>
          <td><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10</a></td>
          <td><code>05 deployment-platform</code> + <code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>資料駐留與刪除證據鏈</td>
          <td><a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11</a></td>
          <td><code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>供應鏈與 artifact 信任</td>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12</a></td>
          <td><code>05 deployment-platform</code> + <code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>偵測覆蓋與訊號治理</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13</a></td>
          <td><code>04 observability</code> + <code>08 incident-response</code></td>
      </tr>
      <tr>
          <td>例外治理與 <a href="/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire</a></td>
          <td><a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14</a></td>
          <td><code>06 reliability</code> + <code>08 incident-response</code></td>
      </tr>
  </tbody>
</table>
<h2 id="章節交接條件">章節交接條件</h2>
<p>章節交接條件的責任是讓概念層輸出可以被實作層直接使用。</p>
<ol>
<li>交接前輸出：問題節點、判讀訊號、風險邊界、責任角色。</li>
<li>交接中輸出：控制面優先序、驗證節奏、回退條件。</li>
<li>交接後輸出：觀測指標、復盤入口、重新評估觸發器。</li>
</ol>
<h2 id="路由決策流程">路由決策流程</h2>
<p>路由流程的責任是避免章節重複、避免控制面遺漏。</p>
<ol>
<li>先確認問題是否已超過單一模組可處理範圍。</li>
<li>再確認優先處理的是入口風險、驗證風險或事故節奏風險。</li>
<li>接著把問題切成 <code>05 platform</code>、<code>06 reliability</code>、<code>08 incident</code> 的可執行項。</li>
<li>最後定義回寫點，確保 07 的判讀語言會被下一輪更新。</li>
</ol>
<h2 id="交接模板">交接模板</h2>
<p>交接模板的責任是讓不同章節用同一種輸入輸出格式合作。</p>
<ul>
<li>問題摘要：一句話描述失效樣式與影響面。</li>
<li>判讀訊號：列出可觀測事件與觸發閾值。</li>
<li>風險邊界：列出升級條件與停止條件。</li>
<li>控制面優先序：列出先做、後做、可延後動作。</li>
<li>驗證與回退：列出驗證指標、觀察時窗與回退條件。</li>
<li>回寫規則：列出要更新的章節、卡片與案例索引。</li>
</ul>
<h2 id="文件邊界">文件邊界</h2>
<p>文件邊界的責任是維持模組分工穩定。</p>
<ul>
<li><code>07</code>：定義問題語言、判讀訊號、風險邊界與路由規則。</li>
<li><code>05</code>：落地入口、網路、部署與平台控制面。</li>
<li><code>06</code>：落地驗證、演練、回退與可靠性節奏。</li>
<li><code>08</code>：落地分級、指揮、通報、收斂與復盤閉環。</li>
</ul>
]]></content:encoded></item><item><title>7.9 服務生命週期的資安風險節奏</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/</guid><description>&lt;p>本章的責任是把資安問題放回服務生命週期節奏，讓團隊在不同階段使用一致的判讀與交接語言。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦階段責任、風險訊號與節奏銜接，不討論特定工具配置或平台實作細節。&lt;/p>
&lt;h2 id="生命週期治理模型">生命週期治理模型&lt;/h2>
&lt;p>生命週期治理的核心責任是讓每個階段都能回答「現在最可能失效的是哪一層控制面」。&lt;/p>
&lt;ol>
&lt;li>設計前：先定義信任邊界、攻擊面與資料責任。&lt;/li>
&lt;li>上線前：先驗證入口、身份、稽核與回退條件。&lt;/li>
&lt;li>變更中：先控制變更速度與驗證速度的平衡。&lt;/li>
&lt;li>事故中：先排序收斂優先序，再分配責任鏈。&lt;/li>
&lt;li>復盤後：先回寫控制面缺口，再設定重評估觸發器。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「階段進度」轉成「風險狀態」。&lt;/p>
&lt;ol>
&lt;li>先定位目前處於哪個生命週期階段。&lt;/li>
&lt;li>再檢查該階段必要控制面是否完整。&lt;/li>
&lt;li>接著檢查是否出現跨階段節奏脫鉤。&lt;/li>
&lt;li>最後把缺口交接到 05/06/08 的實作與處置流程。&lt;/li>
&lt;/ol>
&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>信任邊界假設過度樂觀&lt;/td>
 &lt;td>邊界條件缺乏可驗證描述&lt;/td>
 &lt;td>&lt;code>07 -&amp;gt; 05&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>上線前&lt;/td>
 &lt;td>控制面未形成最小閉環&lt;/td>
 &lt;td>入口、身份、稽核檢查點缺漏&lt;/td>
 &lt;td>&lt;code>07 -&amp;gt; 05/06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變更中&lt;/td>
 &lt;td>變更速度高於驗證速度&lt;/td>
 &lt;td>釋出節奏與驗證時序脫鉤&lt;/td>
 &lt;td>&lt;code>07 -&amp;gt; 06&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故中&lt;/td>
 &lt;td>收斂順序缺少共同語言&lt;/td>
 &lt;td>分級、止血、通報節奏偏移&lt;/td>
 &lt;td>&lt;code>07 -&amp;gt; 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>復盤後&lt;/td>
 &lt;td>改進項目缺乏追蹤條件&lt;/td>
 &lt;td>同類缺口重複出現&lt;/td>
 &lt;td>&lt;code>08 -&amp;gt; 07&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是判斷何時要從一般迭代切換成風險控制模式。&lt;/p>
&lt;ul>
&lt;li>設計假設沒有可驗證條件時，代表後續章節無法穩定承接。&lt;/li>
&lt;li>上線檢查只看功能可用不看控制可用時，代表安全閉環尚未形成。&lt;/li>
&lt;li>變更頻率高於驗證能力時，代表事故機率與回退成本同步上升。&lt;/li>
&lt;li>復盤結果未回寫到下一輪設計條件時，代表缺口將重複出現。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是用現實事件校正生命週期節奏設計。&lt;/p>
&lt;ul>
&lt;li>邊界設備高壓修補窗口： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024&lt;/a>&lt;/li>
&lt;li>供應鏈事件下的凍結與回退： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>&lt;/li>
&lt;li>身分事件下的收斂與復盤壓力： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把資安問題放回服務生命週期節奏，讓團隊在不同階段使用一致的判讀與交接語言。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦階段責任、風險訊號與節奏銜接，不討論特定工具配置或平台實作細節。</p>
<h2 id="生命週期治理模型">生命週期治理模型</h2>
<p>生命週期治理的核心責任是讓每個階段都能回答「現在最可能失效的是哪一層控制面」。</p>
<ol>
<li>設計前：先定義信任邊界、攻擊面與資料責任。</li>
<li>上線前：先驗證入口、身份、稽核與回退條件。</li>
<li>變更中：先控制變更速度與驗證速度的平衡。</li>
<li>事故中：先排序收斂優先序，再分配責任鏈。</li>
<li>復盤後：先回寫控制面缺口，再設定重評估觸發器。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「階段進度」轉成「風險狀態」。</p>
<ol>
<li>先定位目前處於哪個生命週期階段。</li>
<li>再檢查該階段必要控制面是否完整。</li>
<li>接著檢查是否出現跨階段節奏脫鉤。</li>
<li>最後把缺口交接到 05/06/08 的實作與處置流程。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計前</td>
          <td>信任邊界假設過度樂觀</td>
          <td>邊界條件缺乏可驗證描述</td>
          <td><code>07 -&gt; 05</code></td>
      </tr>
      <tr>
          <td>上線前</td>
          <td>控制面未形成最小閉環</td>
          <td>入口、身份、稽核檢查點缺漏</td>
          <td><code>07 -&gt; 05/06</code></td>
      </tr>
      <tr>
          <td>變更中</td>
          <td>變更速度高於驗證速度</td>
          <td>釋出節奏與驗證時序脫鉤</td>
          <td><code>07 -&gt; 06</code></td>
      </tr>
      <tr>
          <td>事故中</td>
          <td>收斂順序缺少共同語言</td>
          <td>分級、止血、通報節奏偏移</td>
          <td><code>07 -&gt; 08</code></td>
      </tr>
      <tr>
          <td>復盤後</td>
          <td>改進項目缺乏追蹤條件</td>
          <td>同類缺口重複出現</td>
          <td><code>08 -&gt; 07</code></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是判斷何時要從一般迭代切換成風險控制模式。</p>
<ul>
<li>設計假設沒有可驗證條件時，代表後續章節無法穩定承接。</li>
<li>上線檢查只看功能可用不看控制可用時，代表安全閉環尚未形成。</li>
<li>變更頻率高於驗證能力時，代表事故機率與回退成本同步上升。</li>
<li>復盤結果未回寫到下一輪設計條件時，代表缺口將重複出現。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是用現實事件校正生命週期節奏設計。</p>
<ul>
<li>邊界設備高壓修補窗口： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></li>
<li>供應鏈事件下的凍結與回退： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a></li>
<li>身分事件下的收斂與復盤壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Cloud Serverless 適用判斷：按用量 vs dedicated 的取捨與 RU 計費結構</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/cloud-serverless/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/cloud-serverless/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。本文聚焦 &lt;em>Cockroach Cloud serverless 與 dedicated 的取捨判讀、RU 計費結構、冷啟動 / scale 行為、何時用 serverless&lt;/em>。Self-managed 規模化的運維責任（Netflix Platform Team 養 380+ cluster）跟賽季型擴縮（Hard Rock 100 ↔ 33 node）作為 &lt;em>對照軸&lt;/em> 引用、不重展 self-host 運維細節。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境要-managed-cockroachdb但-serverless-跟-dedicated-該選哪個">問題情境：要 managed CockroachDB、但 serverless 跟 dedicated 該選哪個&lt;/h2>
&lt;p>團隊決定不自管 Raft / backup / upgrade，改走 Cockroach Cloud managed，接著面對的是 serverless 跟 dedicated 兩種 managed 形態的取捨。這個取捨不是「哪個比較好」，而是 &lt;em>容量壓力的形狀對應哪種計費與 scale 模型&lt;/em>。&lt;/p>
&lt;p>Cockroach Cloud serverless 是 &lt;em>把容量決策從「預先 provision 節點」換成「按實際用量計費 + 自動 scale」&lt;/em> 的 managed 形態。它消去了 cluster sizing 這個決策 — 沒有「要開幾個 node」的問題，資源隨 workload 自動伸縮，甚至閒置時 scale 到接近零。代價是計費單位變成抽象的 Request Unit（RU），用量暴衝時成本跟著暴衝，且共享底層資源帶來冷啟動與性能可預測性的取捨。&lt;/p>
&lt;p>dedicated 則保留 &lt;em>固定的 cluster 容量 + 可預測的計費&lt;/em>，由 vendor 代管運維但容量仍是團隊決策。&lt;/p>
&lt;p>讀者進來最常卡的三題：&lt;/p>
&lt;ul>
&lt;li>serverless 的 RU 計費到底計什麼、怎麼估自己的 workload 會花多少？&lt;/li>
&lt;li>serverless 閒置會 scale 到零，那冷啟動會不會讓第一個請求變慢？&lt;/li>
&lt;li>什麼 workload 適合 serverless、什麼時候該選 dedicated 或乾脆 self-managed？&lt;/li>
&lt;/ul>
&lt;p>這三題的共同核心是 &lt;em>把 workload 的流量形狀（穩定 vs 突發、可預測 vs 不可預測、高峰 vs 長尾）翻譯成計費與 scale 模型&lt;/em>。&lt;/p>
&lt;p>問題情境的對照 trigger 來自兩個 self-managed 規模的 case，它們界定了「什麼時候 serverless / dedicated 都不對、要 self-host」的邊界。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> 是 self-managed 380+ cluster（case 揭露 380+ 為含非 production 的總數、production cluster 160+），case 明確揭露這需要 &lt;em>專屬 Database Platform Team&lt;/em>（backup、upgrade、incident response、capacity review），並警示「沒這量級團隊就走 Cockroach Cloud managed、不要 self-host」。這條判讀的反向就是本文的入口 — 大多數團隊沒有 Platform Team，managed 才是合理起點，問題只剩 serverless 還是 dedicated。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。本文聚焦 <em>Cockroach Cloud serverless 與 dedicated 的取捨判讀、RU 計費結構、冷啟動 / scale 行為、何時用 serverless</em>。Self-managed 規模化的運維責任（Netflix Platform Team 養 380+ cluster）跟賽季型擴縮（Hard Rock 100 ↔ 33 node）作為 <em>對照軸</em> 引用、不重展 self-host 運維細節。</p></blockquote>
<hr>
<h2 id="問題情境要-managed-cockroachdb但-serverless-跟-dedicated-該選哪個">問題情境：要 managed CockroachDB、但 serverless 跟 dedicated 該選哪個</h2>
<p>團隊決定不自管 Raft / backup / upgrade，改走 Cockroach Cloud managed，接著面對的是 serverless 跟 dedicated 兩種 managed 形態的取捨。這個取捨不是「哪個比較好」，而是 <em>容量壓力的形狀對應哪種計費與 scale 模型</em>。</p>
<p>Cockroach Cloud serverless 是 <em>把容量決策從「預先 provision 節點」換成「按實際用量計費 + 自動 scale」</em> 的 managed 形態。它消去了 cluster sizing 這個決策 — 沒有「要開幾個 node」的問題，資源隨 workload 自動伸縮，甚至閒置時 scale 到接近零。代價是計費單位變成抽象的 Request Unit（RU），用量暴衝時成本跟著暴衝，且共享底層資源帶來冷啟動與性能可預測性的取捨。</p>
<p>dedicated 則保留 <em>固定的 cluster 容量 + 可預測的計費</em>，由 vendor 代管運維但容量仍是團隊決策。</p>
<p>讀者進來最常卡的三題：</p>
<ul>
<li>serverless 的 RU 計費到底計什麼、怎麼估自己的 workload 會花多少？</li>
<li>serverless 閒置會 scale 到零，那冷啟動會不會讓第一個請求變慢？</li>
<li>什麼 workload 適合 serverless、什麼時候該選 dedicated 或乾脆 self-managed？</li>
</ul>
<p>這三題的共同核心是 <em>把 workload 的流量形狀（穩定 vs 突發、可預測 vs 不可預測、高峰 vs 長尾）翻譯成計費與 scale 模型</em>。</p>
<p>問題情境的對照 trigger 來自兩個 self-managed 規模的 case，它們界定了「什麼時候 serverless / dedicated 都不對、要 self-host」的邊界。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> 是 self-managed 380+ cluster（case 揭露 380+ 為含非 production 的總數、production cluster 160+），case 明確揭露這需要 <em>專屬 Database Platform Team</em>（backup、upgrade、incident response、capacity review），並警示「沒這量級團隊就走 Cockroach Cloud managed、不要 self-host」。這條判讀的反向就是本文的入口 — 大多數團隊沒有 Platform Team，managed 才是合理起點，問題只剩 serverless 還是 dedicated。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 是 self-managed、賽季型擴縮（高峰 ~100 node、淡季 ~33 node，case 觀察段揭露）。這個 100 ↔ 33 的擺盪是 <em>已知時間點的年度循環</em>（NFL / NBA 賽季切換），不是不可預測的突發。case 還揭露合規驅動需要 AWS Outposts 把運算放進州內 — 這把它鎖死在 self-managed。Hard Rock 的形狀正好對照出 serverless 的適配範圍：serverless 擅長 <em>不可預測</em> 的突發與長尾閒置，而非 <em>可預測且需要特定部署位置</em> 的賽季擴縮。</p>
<h2 id="核心機制ru-計費--自動-scale--冷啟動">核心機制：RU 計費 + 自動 scale + 冷啟動</h2>
<h3 id="request-unit把多維資源用量折算成單一計費單位">Request Unit：把多維資源用量折算成單一計費單位</h3>
<p>serverless 的計費核心是 Request Unit（RU）— 一個把 <em>CPU、IO、network、storage 存取</em> 等多維資源用量折算成的抽象單位。每個 SQL 請求依其實際消耗的資源換算成若干 RU，帳單按 RU 總量計。這跟 dedicated「按 provision 的節點數 × 時間」計費是兩種不同的成本心智模型。</p>
<p>RU 模型的好處是 <em>用多少付多少</em> — 閒置時段不付運算費。風險是 RU 跟「人類直覺的請求數」不是線性對應：一個全表掃描的 query 可能吃掉相當於上千個點查的 RU。estimate workload 成本時，要以 <em>資源消耗</em> 為單位思考，不是以「請求數」。</p>
<blockquote>
<p><strong>Scope warning</strong>：RU 的具體換算係數、serverless 免費額度、scale-to-zero 的觸發閒置時間、冷啟動延遲量級、serverless 的 region / 一致性 / 規模上限，都屬 Cockroach Cloud 的計費與規格、且隨方案版本演進，三個 anchor case（DoorDash / Netflix / Hard Rock 全為 self-managed）都未揭露 serverless 計費數字。本文只給結構性判讀（RU = 多維資源折算、scale-to-zero 帶來冷啟動），具體數值與當前方案邊界需 cross-verify <a href="https://www.cockroachlabs.com/docs/cockroachcloud/plan-your-cluster">Cockroach Cloud Pricing 文件</a> 與官方計費頁。</p></blockquote>
<h3 id="自動-scale-與-scale-to-zero">自動 scale 與 scale-to-zero</h3>
<p>serverless 隨 workload 自動伸縮資源，無需團隊 provision。閒置時可 scale 到接近零，這正是「閒置不付運算費」的來源。對 <em>突發 + 長閒置</em> 的 workload（開發 / 測試環境、低流量 side project、流量極不均的早期產品），這個模型把成本壓到只反映實際活躍時段。</p>
<p>scale-to-zero 的代價是冷啟動 — 從近零狀態接到請求時，要先把資源拉起來，第一個請求的延遲高於 warm 狀態。對開發環境這通常可接受；對「閒置後第一個用戶請求就要快」的面向用戶 production 路徑，冷啟動是要先評估的取捨。</p>
<h3 id="serverless-vs-dedicated-的責任與成本對照">serverless vs dedicated 的責任與成本對照</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>serverless</th>
          <th>dedicated</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>容量決策</td>
          <td>自動 scale、無需 sizing</td>
          <td>團隊決定 cluster 規模</td>
      </tr>
      <tr>
          <td>計費單位</td>
          <td>RU（按實際資源用量）</td>
          <td>按 provision 的節點 × 時間</td>
      </tr>
      <tr>
          <td>閒置成本</td>
          <td>接近零（scale-to-zero）</td>
          <td>仍付 provisioned 容量費</td>
      </tr>
      <tr>
          <td>冷啟動</td>
          <td>閒置後第一請求有冷啟動延遲</td>
          <td>無（容量常駐）</td>
      </tr>
      <tr>
          <td>成本可預測性</td>
          <td>隨用量浮動、突發時可能暴衝</td>
          <td>固定、可預算</td>
      </tr>
      <tr>
          <td>性能可預測性</td>
          <td>共享底層、受鄰居影響</td>
          <td>專屬資源、更可預測</td>
      </tr>
  </tbody>
</table>
<p>每一行都要回到 workload 形狀判讀。</p>
<p>容量決策這一行是兩種模型的根本差異：serverless 把「要開幾個節點」這個決策從團隊手上拿走，對沒有容量規劃經驗或流量極不可預測的場景能降低團隊的容量規劃負擔；但對流量已知、需要性能可預測的 production，dedicated 的「自己定容量」反而是想要的控制權。</p>
<p>成本可預測性這一行是 serverless 的主要風險面。RU 隨用量浮動意味著 <em>一次失控的查詢模式、一波爬蟲、一個沒加 LIMIT 的全表掃描</em> 都會把帳單推高，而 dedicated 的成本上限就是 provisioned 容量。流量可預測的 production，dedicated 的可預算性往往比 serverless 的「用多少付多少」更重要。</p>
<h2 id="操作流程選型判讀配置用量驗證">操作流程：選型判讀、配置、用量驗證</h2>
<h3 id="第一步用流量形狀做-serverless--dedicated-初判">第一步：用流量形狀做 serverless / dedicated 初判</h3>
<p>選型的判讀軸是 workload 的 <em>流量形狀</em>，不是規模大小。</p>
<ul>
<li>流量突發 + 長閒置（dev / test、低流量產品、不可預測早期 workload）→ serverless 的 scale-to-zero 與按用量計費直接受益。</li>
<li>流量穩定 + 可預測 + 需要性能可預測 → dedicated 的固定容量與可預算成本更合適。</li>
<li>流量大 + 有專屬 Platform Team + 需要跨雲 / on-prem / 特定部署位置（如 Hard Rock 的合規 Outposts）→ 兩種 managed 都不對，走 self-managed（見 vendor overview 的容量規劃段）。</li>
</ul>
<p>判讀訊號：把過去一段時間的 QPS 畫成時間序列，看「活躍時段佔比」與「峰谷比」。活躍佔比低、峰谷比高 → serverless;活躍佔比高、波動平緩 → dedicated。</p>
<h3 id="第二步serverless-建立-cluster-並設成本上限">第二步：serverless 建立 cluster 並設成本上限</h3>
<p>serverless 的成本風險來自用量浮動，所以建立後第一件事是設 <em>消費上限</em>，把「用量暴衝 = 帳單暴衝」的尾部風險封住。</p>
<p>驗證點：cluster 建立後，確認消費上限已設、且設了接近上限的告警閾值（例如達上限 80% 告警）。沒設上限的 serverless cluster 等於把成本曝險完全交給 workload 行為。</p>
<h3 id="第三步驗證-ru-消耗與預期一致">第三步：驗證 RU 消耗與預期一致</h3>
<p>上線後監控 RU 消耗速率，對照第一步的流量形狀預估。</p>
<p>驗證點：RU 消耗速率若遠高於預估，通常是某類 query 的資源消耗被低估（全表掃描、缺索引、N+1 查詢）。這時要回到 query 層優化，而非直接加預算 — serverless 的計費把「低效 query」直接翻譯成「高帳單」，是一個比 dedicated 更直接的成本訊號。</p>
<h3 id="第四步評估冷啟動對-production-路徑的影響">第四步：評估冷啟動對 production 路徑的影響</h3>
<p>若 serverless cluster 服務面向用戶的 production 路徑，驗證閒置後第一個請求的延遲是否在 SLO 內。</p>
<p>驗證點：模擬閒置後的首請求延遲，對照面向用戶路徑的 latency SLO。超出 SLO 代表這條路徑不適合 scale-to-zero，要嘛保持一定 warm 流量、要嘛改 dedicated。</p>
<h2 id="失敗模式成本失控與選型誤判">失敗模式：成本失控與選型誤判</h2>
<h3 id="ru-用量暴衝帳單失控高代價情境的回退敘事">RU 用量暴衝、帳單失控（高代價情境的回退敘事）</h3>
<p>serverless 最常見的事故是 <em>帳單暴衝</em> — 一波非預期流量、一個低效查詢上線、一次爬蟲，把 RU 消耗推到遠超預算。跟 dedicated「成本上限 = provisioned 容量」不同，serverless 的成本上限要靠人為設定，沒設就沒有天花板。</p>
<p>這個情境的回退代價特殊之處在於 <em>成本已經發生</em>：rebalance 可以暫停、locality 可以改回，但已計的 RU 帳單不會退回。所以 serverless 成本失控的「回退」重點在 <em>事前封頂</em> 與 <em>事中熔斷</em>，而非事後補救。</p>
<p>回退與防護要素：</p>
<ul>
<li>事前一定設消費上限與分級告警（接近上限前就要收到訊號），把尾部風險封在可承受範圍。</li>
<li>事中發現 RU 暴衝，先定位來源 — 是流量真的漲（業務事件），還是某個 query 模式失控（缺索引、全表掃描、無 LIMIT）。前者考慮是否該轉 dedicated，後者回 query 層修。</li>
<li>設「RU 消耗速率超過閾值就告警 + 自動限流」的 tripwire，避免單一失控 query 在無人值守時段燒完整月預算。</li>
<li>若 workload 已穩定成長到「serverless 浮動成本 &gt; dedicated 固定成本」的交叉點，規劃轉 dedicated。</li>
</ul>
<h3 id="serverless--dedicated-遷移的代價">serverless → dedicated 遷移的代價</h3>
<p>當 workload 從「突發長尾」成長為「穩定高量」，serverless 的按用量成本會超過 dedicated 的固定成本，此時要遷移。這個遷移不是改個開關 — serverless 與 dedicated 是不同的 cluster 形態，遷移意味著資料搬遷與 cutover，要走 backup / restore 或資料複製流程，並承擔 cutover 窗口。</p>
<p>回退敘事：把 serverless → dedicated 當成一次小型 migration 規劃 — 估資料量與遷移窗口、雙寫或 backup/restore 路徑、cutover 條件與回退條件，而非「線上無痛切換」。提早在用量逼近成本交叉點時規劃，避免在帳單已經失控時倉促遷移。</p>
<p>Anti-recommendation：不要因為「serverless 聽起來更現代」就把已知穩定、可預測、高流量的 production workload 開在 serverless。這類 workload 的可預算性與性能可預測性，dedicated 給得更直接，serverless 反而引入成本浮動與冷啟動兩個非必要風險。</p>
<h3 id="把賽季型--可預測擴縮誤當-serverless-場景">把賽季型 / 可預測擴縮誤當 serverless 場景</h3>
<p>可預測的擴縮（如 Hard Rock 的 NFL / NBA 賽季 100 ↔ 33 node 年度循環）不是 serverless 的適配範圍。serverless 擅長 <em>不可預測</em> 的突發，而可預測的擴縮可以用 dedicated 的計畫內 scale 直接規劃容量、保留性能可預測性。把可預測擴縮交給 serverless，是用「成本浮動 + 冷啟動」換一個本來就能用排程解決的問題。</p>
<p>修法：可預測的容量循環，用 dedicated + 排程 scale；只有真正不可預測的突發長尾才用 serverless。</p>
<h3 id="冷啟動拖垮面向用戶路徑">冷啟動拖垮面向用戶路徑</h3>
<p>scale-to-zero 的 serverless cluster 服務面向用戶 production，閒置後首請求冷啟動延遲超出 SLO，用戶感受到第一次訪問特別慢。</p>
<p>修法：面向用戶且對首請求延遲敏感的路徑，要嘛維持低頻 warm 流量避免完全 scale-to-zero，要嘛改 dedicated；scale-to-zero 留給容忍冷啟動的 dev / test / 後台 batch 路徑。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>RU 消耗速率</code>：serverless 成本的直接訊號，速率異常上升要立刻定位 query 來源。</li>
<li><code>當期累計消費 vs 上限</code>：成本封頂的剩餘空間，逼近上限要告警。</li>
<li><code>冷啟動 / 首請求延遲</code>：scale-to-zero 對面向用戶路徑的影響。</li>
<li><code>query 資源消耗分佈</code>：哪些 query 吃掉最多 RU，是 serverless 成本優化的入口。</li>
</ul>
<h3 id="容量與成本判讀">容量與成本判讀</h3>
<ul>
<li>serverless 月成本 ≈ Σ(各 query RU × 頻率)，所以成本優化等於 query 效率優化 — 缺索引、全表掃描在 serverless 直接體現為帳單。</li>
<li>serverless / dedicated 成本交叉點 ≈ 「serverless 浮動成本」與「dedicated 固定容量成本」相等的用量水準，逼近交叉點是規劃遷移的訊號。</li>
<li>dedicated 的容量規劃回到節點數 × replica × latency budget（見 vendor overview 容量規劃段）。</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：RU 換算係數、免費額度、serverless 的規模 / region / 一致性上限、serverless ↔ dedicated 成本交叉點的具體用量水準，均為 Cockroach Cloud 計費與規格、隨方案版本變動，非 case 揭露數字，成本建模前以 <a href="https://www.cockroachlabs.com/docs/cockroachcloud/">Cockroach Cloud 文件</a> cross-verify。</p></blockquote>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> 流量形狀 → 計費模型對應。</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> managed vs self-managed 的人力 + 資源成本權衡。</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../survival-goals/">survival goals</a>：managed 形態下 survival goal 仍是團隊決策 — serverless / dedicated 都要對齊業務 RTO / RPO，存活機制以該文為 SSoT。</li>
<li><a href="../multi-region-table-config/">multi-region table config</a>：serverless 與 dedicated 對 multi-region table locality 的支援邊界不同，跨 region 強一致需求要先確認所選 managed 形態是否覆蓋。</li>
<li><a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>：Aurora DSQL 本身是 serverless distributed SQL，三家 managed distributed SQL 的選型對比以該文為 SSoT，本文不重展。</li>
</ul>
<h3 id="跟-aurora-dsql--spanner-serverless-對照">跟 Aurora DSQL / Spanner serverless 對照</h3>
<p>Aurora DSQL（AWS）以 serverless 為核心形態、AWS-only；Spanner 提供 managed 但計費與 scale 模型不同。三家在 serverless / managed 維度的完整對比是 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a> 的 SSoT，本文只處理 Cockroach Cloud 自身的 serverless / dedicated 取捨。</p>
<h3 id="跟-self-managed-對照">跟 self-managed 對照</h3>
<p>self-managed（如 Netflix 380+ cluster、Hard Rock 合規 Outposts）給最大控制權（跨雲 / on-prem / 特定部署位置），代價是專屬 Platform Team 的運維責任。判讀軸：沒有 Platform Team → managed（serverless / dedicated）；有 Platform Team + 需要特定部署位置或跨雲 → self-managed。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游選型。</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB migration</a> — 從 PostgreSQL 遷入後再選 managed 形態。</li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>已決定 self-managed（有 Platform Team 或需要 on-prem / 合規 Outposts）→ 看 vendor overview 容量規劃段與 self-host 運維，本文的 serverless / dedicated 取捨不適用。</li>
<li>single-region 小 workload 且 PostgreSQL 已夠用 → 先確認是否真需要 distributed SQL，見 vendor overview 不適用場景。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（self-managed 需 Platform Team 的反向 = managed 入口）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（可預測賽季擴縮 vs serverless 突發適配範圍的對照）</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/cockroachcloud/">Cockroach Cloud Documentation</a> / <a href="https://www.cockroachlabs.com/docs/cockroachcloud/plan-your-cluster">Plan Your Cluster</a></li>
</ul>
]]></content:encoded></item><item><title>可觀測性案例正文</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/</guid><description>&lt;p>這個資料夾的核心責任是把觀測案例變成可回寫章節。案例表格提供線索，正文負責輸出訊號邊界與路由。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1&lt;/a>&lt;/td>
 &lt;td>FinTech 審計證據觀測&lt;/td>
 &lt;td>把審計與證據鏈變成可觀測訊號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2&lt;/a>&lt;/td>
 &lt;td>Gaming 高峰訊號治理&lt;/td>
 &lt;td>把高峰流量下訊號失真風險前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3&lt;/a>&lt;/td>
 &lt;td>Healthcare 存取可追溯性&lt;/td>
 &lt;td>把資料主權場景的存取證據做成治理閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4&lt;/a>&lt;/td>
 &lt;td>X-Ray 到 OTel 轉換&lt;/td>
 &lt;td>把觀測遷移標準化成可分段執行流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5&lt;/a>&lt;/td>
 &lt;td>Cloud Trace OTLP 導入&lt;/td>
 &lt;td>把資料通道標準化納入觀測平台治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6&lt;/a>&lt;/td>
 &lt;td>ADOT on EKS 遷移&lt;/td>
 &lt;td>把 collector/agent 管線轉換成集中治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7&lt;/a>&lt;/td>
 &lt;td>Datadog OTel 遷移實務&lt;/td>
 &lt;td>把 APM 採集轉成 OTel-compatible 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8&lt;/a>&lt;/td>
 &lt;td>Airbnb K8s 規模化訊號&lt;/td>
 &lt;td>把叢集擴縮行為接回觀測與容量治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9&lt;/a>&lt;/td>
 &lt;td>反例：OTel 遷移訊號漂移&lt;/td>
 &lt;td>雙軌採集未對齊導致告警與 SLO 判讀失真&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下觀測遷移&lt;/td>
 &lt;td>不同規模團隊在觀測遷移的風險與流程差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11&lt;/a>&lt;/td>
 &lt;td>Uber M3 大規模 Metrics&lt;/td>
 &lt;td>從散落的 Prometheus 到統一 metrics 平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&amp;#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12&lt;/a>&lt;/td>
 &lt;td>Cloudflare 觀測三層能力&lt;/td>
 &lt;td>monitoring / analytics / forensics 拆分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13&lt;/a>&lt;/td>
 &lt;td>Discord 儲存→觀測缺口&lt;/td>
 &lt;td>每次遷移暴露觀測盲區的共同結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14&lt;/a>&lt;/td>
 &lt;td>觀測成本治理&lt;/td>
 &lt;td>attribution + cardinality budget + tiering&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把觀測案例變成可回寫章節。案例表格提供線索，正文負責輸出訊號邊界與路由。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1</a></td>
          <td>FinTech 審計證據觀測</td>
          <td>把審計與證據鏈變成可觀測訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2</a></td>
          <td>Gaming 高峰訊號治理</td>
          <td>把高峰流量下訊號失真風險前移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3</a></td>
          <td>Healthcare 存取可追溯性</td>
          <td>把資料主權場景的存取證據做成治理閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4</a></td>
          <td>X-Ray 到 OTel 轉換</td>
          <td>把觀測遷移標準化成可分段執行流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5</a></td>
          <td>Cloud Trace OTLP 導入</td>
          <td>把資料通道標準化納入觀測平台治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6</a></td>
          <td>ADOT on EKS 遷移</td>
          <td>把 collector/agent 管線轉換成集中治理</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>Datadog OTel 遷移實務</td>
          <td>把 APM 採集轉成 OTel-compatible 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8</a></td>
          <td>Airbnb K8s 規模化訊號</td>
          <td>把叢集擴縮行為接回觀測與容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9</a></td>
          <td>反例：OTel 遷移訊號漂移</td>
          <td>雙軌採集未對齊導致告警與 SLO 判讀失真</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10</a></td>
          <td>對照：規模差異下觀測遷移</td>
          <td>不同規模團隊在觀測遷移的風險與流程差異</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11</a></td>
          <td>Uber M3 大規模 Metrics</td>
          <td>從散落的 Prometheus 到統一 metrics 平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12</a></td>
          <td>Cloudflare 觀測三層能力</td>
          <td>monitoring / analytics / forensics 拆分</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13</a></td>
          <td>Discord 儲存→觀測缺口</td>
          <td>每次遷移暴露觀測盲區的共同結構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14</a></td>
          <td>觀測成本治理</td>
          <td>attribution + cardinality budget + tiering</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>模組二案例正文</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/</guid><description>&lt;p>這個資料夾的核心責任是把快取與 Redis 的轉換壓力寫成可回寫正文，而不是只列工具名稱。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1&lt;/a>&lt;/td>
 &lt;td>Meta cache 一致性升級&lt;/td>
 &lt;td>把 invalidation 不一致問題轉成可觀測與可治理流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2&lt;/a>&lt;/td>
 &lt;td>Meta mcrouter 快取路由&lt;/td>
 &lt;td>把單叢集快取演進到跨區路由與失效隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3&lt;/a>&lt;/td>
 &lt;td>Shopify 快取序列化遷移&lt;/td>
 &lt;td>把快取 payload 格式遷移做成雙軌相容與回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4&lt;/a>&lt;/td>
 &lt;td>Meta CacheLib 分層快取&lt;/td>
 &lt;td>把 DRAM-only 快取演進到記憶體/快閃分層架構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5&lt;/a>&lt;/td>
 &lt;td>Shopify write-through&lt;/td>
 &lt;td>把 read-heavy 路徑轉成寫入同步快取策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6&lt;/a>&lt;/td>
 &lt;td>Netflix EVCache&lt;/td>
 &lt;td>把本地快取演進成跨區分散式快取層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7&lt;/a>&lt;/td>
 &lt;td>Cloudflare Cache Reserve&lt;/td>
 &lt;td>把邊緣快取延伸到持久層降低回源壓力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8&lt;/a>&lt;/td>
 &lt;td>Meta TAO&lt;/td>
 &lt;td>把 graph cache 演進成可擴展的一致性資料層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9&lt;/a>&lt;/td>
 &lt;td>反例：快取切換失敗&lt;/td>
 &lt;td>快取策略切換若無防線會觸發 stampede 與回源雪崩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下快取策略&lt;/td>
 &lt;td>小中大型服務用同一快取策略會造成不同失敗型態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把快取與 Redis 的轉換壓力寫成可回寫正文，而不是只列工具名稱。</p>
<h2 id="案例列表">案例列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1</a></td>
          <td>Meta cache 一致性升級</td>
          <td>把 invalidation 不一致問題轉成可觀測與可治理流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2</a></td>
          <td>Meta mcrouter 快取路由</td>
          <td>把單叢集快取演進到跨區路由與失效隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a></td>
          <td>Shopify 快取序列化遷移</td>
          <td>把快取 payload 格式遷移做成雙軌相容與回退</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4</a></td>
          <td>Meta CacheLib 分層快取</td>
          <td>把 DRAM-only 快取演進到記憶體/快閃分層架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5</a></td>
          <td>Shopify write-through</td>
          <td>把 read-heavy 路徑轉成寫入同步快取策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6</a></td>
          <td>Netflix EVCache</td>
          <td>把本地快取演進成跨區分散式快取層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7</a></td>
          <td>Cloudflare Cache Reserve</td>
          <td>把邊緣快取延伸到持久層降低回源壓力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8</a></td>
          <td>Meta TAO</td>
          <td>把 graph cache 演進成可擴展的一致性資料層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9</a></td>
          <td>反例：快取切換失敗</td>
          <td>快取策略切換若無防線會觸發 stampede 與回源雪崩</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10</a></td>
          <td>對照：規模差異下快取策略</td>
          <td>小中大型服務用同一快取策略會造成不同失敗型態</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>模組三案例正文</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/</guid><description>&lt;p>這個資料夾的核心責任是把 broker、queue 與語義治理的轉換壓力落到可執行判讀、並提供各 vendor 的真實 production case 庫支撐撰寫。案例不是事後舉例、是寫作 finding 的 source — 章節該討論的議題從 case 反推、不是先寫章節再找案例填。&lt;/p>
&lt;h2 id="通用案例跨-vendor--反例--規模對照">通用案例（跨 vendor / 反例 / 規模對照）&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1&lt;/a>&lt;/td>
 &lt;td>Meta FOQS 全域遷移&lt;/td>
 &lt;td>區域佇列如何升級到 disaster-ready 架構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2&lt;/a>&lt;/td>
 &lt;td>VMware Kafka → MSK&lt;/td>
 &lt;td>自管 broker 轉 managed streaming 的治理重點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3&lt;/a>&lt;/td>
 &lt;td>LinkedIn TopicGC&lt;/td>
 &lt;td>topic 生命週期治理如何影響叢集可靠性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4&lt;/a>&lt;/td>
 &lt;td>LinkedIn Kafka 分層&lt;/td>
 &lt;td>把單叢集使用模式轉成分層叢集治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &amp;#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5&lt;/a>&lt;/td>
 &lt;td>Slack Job Queue&lt;/td>
 &lt;td>背景工作通道轉成 Kafka + Redis 組合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6&lt;/a>&lt;/td>
 &lt;td>Uber Kafka 基礎設施&lt;/td>
 &lt;td>把事件平台演進成多租戶共享能力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/" data-link-title="3.C7 LinkedIn：Kafka 自動修復治理" data-link-desc="Kafka 維運從人工處置轉向自動修復的案例。">3.C7&lt;/a>&lt;/td>
 &lt;td>LinkedIn Self-healing Kafka&lt;/td>
 &lt;td>把手動維運轉成自動修復治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8&lt;/a>&lt;/td>
 &lt;td>Cloudflare Queues&lt;/td>
 &lt;td>把全球佇列傳遞模型轉成可治理交付路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9&lt;/a>&lt;/td>
 &lt;td>反例：語義切換失敗&lt;/td>
 &lt;td>at-least-once / exactly-once 語義誤配造成資料錯亂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下佇列模型&lt;/td>
 &lt;td>同一佇列模型在不同規模下有不同治理與失敗邊界&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="kafka-案例">Kafka 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 Kafka 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">3.C11&lt;/a>&lt;/td>
 &lt;td>Pinterest Tiered Storage&lt;/td>
 &lt;td>Tiered storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/" data-link-title="3.C12 Pinterest：Shallow Mirror 優化 MirrorMaker" data-link-desc="Pinterest 跨 3 region MirrorMaker、原版解壓&amp;#43;重壓造成 CPU/memory 2-10x spike、改 RecordBatch 層淺迭代。">3.C12&lt;/a>&lt;/td>
 &lt;td>Pinterest Shallow Mirror&lt;/td>
 &lt;td>Cross-region MirrorMaker&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&amp;#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &amp;lt; 10s。">3.C13&lt;/a>&lt;/td>
 &lt;td>Shopify Debezium CDC&lt;/td>
 &lt;td>Kafka Connect / CDC&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">3.C14&lt;/a>&lt;/td>
 &lt;td>Yelp Schematizer&lt;/td>
 &lt;td>Schema Registry / Schema evolution&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15&lt;/a>&lt;/td>
 &lt;td>Airbnb Spark Streaming&lt;/td>
 &lt;td>Consumer 設計 / partition + consumer group&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16&lt;/a>&lt;/td>
 &lt;td>Robinhood Faust&lt;/td>
 &lt;td>跨語言 client / stream processing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&amp;#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17&lt;/a>&lt;/td>
 &lt;td>Walmart MPS&lt;/td>
 &lt;td>Rebalance storm / consumer lag / multi-tenant&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-greyhound-troubleshooting/" data-link-title="3.C18 Wix：Greyhound TLLSR 解 consumer 卡住" data-link-desc="Wix 2000&amp;#43; microservice 66B msg/day、自建 Greyhound 抽象、TLLSR 框架解 single-partition lag / poison pill / handler 卡住。">3.C18&lt;/a>&lt;/td>
 &lt;td>Wix Greyhound&lt;/td>
 &lt;td>Consumer lag / observability / poison message&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-wix-multi-cluster-migration/" data-link-title="3.C19 Wix：Multi-cluster Kafka zero-downtime 遷移" data-link-desc="Wix metadata 從 5K topic 漲到 20K topic / 200K partition、controller startup 跟 broker stability 受壓垮、分多 cluster 解決。">3.C19&lt;/a>&lt;/td>
 &lt;td>Wix Multi-cluster&lt;/td>
 &lt;td>Topic 生命週期 / 分層叢集&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20&lt;/a>&lt;/td>
 &lt;td>Spotify 遷出 Kafka（反例）&lt;/td>
 &lt;td>Replication 失敗模式 / producer 可靠性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/" data-link-title="3.C21 Goldman Sachs：MSK 遷移 with MirrorMaker 2" data-link-desc="Goldman Sachs Global Investment Research 從 on-prem Kafka 遷到 MSK、用 MM2 同步 topic/ACL/offset、atomic cutover 7 小時完成。">3.C21&lt;/a>&lt;/td>
 &lt;td>Goldman Sachs MSK&lt;/td>
 &lt;td>Cross-region MirrorMaker / managed broker 遷移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&amp;#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22&lt;/a>&lt;/td>
 &lt;td>Trivago KEDA&lt;/td>
 &lt;td>Consumer lag / autoscaling&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="rabbitmq-案例">RabbitMQ 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 RabbitMQ 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &amp;#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23&lt;/a>&lt;/td>
 &lt;td>Bloomberg vhost 多租戶&lt;/td>
 &lt;td>多 vhost + 多租戶 / Erlang clustering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-soundcloud-fanout-audio/" data-link-title="3.C24 SoundCloud：AMQP fan-out 音訊處理 pipeline" data-link-desc="SoundCloud 每秒 20-30K persistent message、不同處理類型分開隊列、各自獨立 scale。">3.C24&lt;/a>&lt;/td>
 &lt;td>SoundCloud fan-out 音訊&lt;/td>
 &lt;td>Prefetch + consumer 併發 / Streams&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &amp;#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&amp;#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25&lt;/a>&lt;/td>
 &lt;td>Indeed Delay + DLQ&lt;/td>
 &lt;td>Dead-letter exchange / retry 策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &amp;#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26&lt;/a>&lt;/td>
 &lt;td>GoCardless Hutch service mesh&lt;/td>
 &lt;td>Exchange types / 多 vhost（反向）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27&lt;/a>&lt;/td>
 &lt;td>Zalando AWS master selection&lt;/td>
 &lt;td>Erlang clustering / Federation / Operator&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/" data-link-title="3.C28 WeWork：Consistent hash exchange 保證帳戶順序" data-link-desc="WeWork 固定數量 queue &amp;#43; account ID hash 路由、每 queue 一個 worker &amp;#43; exclusive consumer 保 partition-level ordering。">3.C28&lt;/a>&lt;/td>
 &lt;td>WeWork consistent hash&lt;/td>
 &lt;td>Exchange types / partition-level ordering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wework-bunny-channel-pool/" data-link-title="3.C29 WeWork：Bunny &amp;#43; Puma 多執行緒 channel pool" data-link-desc="WeWork 從 Unicorn 切到 Puma 後遇 ConnectionClosedError、根因是 AMQP channel 跨執行緒共用、改用 connection_pool 管理。">3.C29&lt;/a>&lt;/td>
 &lt;td>WeWork Bunny channel pool&lt;/td>
 &lt;td>Prefetch + consumer 併發（client lib）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30&lt;/a>&lt;/td>
 &lt;td>Runtastic mirrored queue 瓶頸&lt;/td>
 &lt;td>Mirrored queue → Quorum queue 遷移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-mozilla-pulse-naming-isolation/" data-link-title="3.C31 Mozilla Pulse：命名前綴 &amp;#43; ACL 取代 vhost 多租戶" data-link-desc="Mozilla Pulse 不用 vhost、改用權限 &amp;#43; 命名前綴 (exchange/{user}/*) 做隔離、CloudAMQP 託管、PulseGuardian 管使用者。">3.C31&lt;/a>&lt;/td>
 &lt;td>Mozilla Pulse naming isolation&lt;/td>
 &lt;td>多 vhost + 多租戶（反向：用 ACL + naming）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-loyaltylion-monitoring-thousands/" data-link-title="3.C32 LoyaltyLion：監控數千 RabbitMQ queue" data-link-desc="LoyaltyLion 跑數千 queue、用 rabbitmqctl &amp;#43; statsd 推 Datadog、揭露大規模 queue 拓樸下原生 plugin API 不夠用。">3.C32&lt;/a>&lt;/td>
 &lt;td>LoyaltyLion 監控數千 queue&lt;/td>
 &lt;td>監控觀測 / Operator&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/rabbitmq-wargaming-game-portal-decoupling/" data-link-title="3.C33 Wargaming：World of Tanks 戰後 dossier 解耦" data-link-desc="Wargaming WoT server 全 Linux、戰後 dossier 寫 RabbitMQ、portal 顯示統計而不增 game server load。">3.C33&lt;/a>&lt;/td>
 &lt;td>Wargaming game portal 解耦&lt;/td>
 &lt;td>Federation + Shovel / 多 vhost&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="nats-案例">NATS 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 NATS 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-netlify-data-plane-fanout/" data-link-title="3.C34 Netlify：NATS 當全球 metrics/logs 統一資料平面" data-link-desc="Netlify 70K&amp;#43; 網站、10 億 PV/月、跨多雲、NATS 當 all-purpose data plane fan-out bus、超 RabbitMQ 評估。">3.C34&lt;/a>&lt;/td>
 &lt;td>Netlify 全球資料平面 fan-out&lt;/td>
 &lt;td>Core NATS vs JetStream / subject-based routing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/" data-link-title="3.C35 Form3：NATS JetStream 多雲低延遲支付" data-link-desc="Form3 服務 Tier-1 銀行、500ms SLA、SNS/SQS 吃 300ms 預算、改 NATS&amp;#43;JetStream 跨雲 6x 延遲改善。">3.C35&lt;/a>&lt;/td>
 &lt;td>Form3 多雲低延遲支付&lt;/td>
 &lt;td>Cluster + Supercluster + Leaf node / JetStream&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/" data-link-title="3.C36 Intelecy：工業 IoT 即時感測 &amp;#43; 多租戶" data-link-desc="Intelecy 工廠 gateway 接數萬感測器、&amp;lt; 2 秒往返延遲做即時 ML、從 BoltDB 本地快取演進到 JetStream 持久化。">3.C36&lt;/a>&lt;/td>
 &lt;td>Intelecy 工業 IoT&lt;/td>
 &lt;td>JetStream stream / Subject-based ACL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &amp;#43; JetStream &amp;#43; KV &amp;#43; Object Store。">3.C37&lt;/a>&lt;/td>
 &lt;td>MachineMetrics edge to cloud&lt;/td>
 &lt;td>Leaf node / KV + Object Store / 多租戶 ACL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&amp;#43; 訊息 100% uptime。">3.C38&lt;/a>&lt;/td>
 &lt;td>Clarifai NATS Streaming ML&lt;/td>
 &lt;td>JetStream consumer 設計 / Queue groups&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-choria-orchestration-fleet/" data-link-title="3.C39 Choria：NATS 管 50 萬 server fleet" data-link-desc="Choria 替代 Puppet MCollective、NATS 單 binary 無 Zookeeper、4GB node 可達 50 萬 server、wildcard &amp;#43; queue group 做 scatter-gather RPC。">3.C39&lt;/a>&lt;/td>
 &lt;td>Choria fleet orchestration&lt;/td>
 &lt;td>Request/Reply / Queue groups / Supercluster&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-resgate-realtime-api-gateway/" data-link-title="3.C40 Resgate：WebSocket-to-NATS realtime API gateway" data-link-desc="Resgate 把 NATS subject 暴露成 REST &amp;#43; WebSocket、subject 階層當 schema、event 延遲 &amp;lt; 1ms、純 Core NATS。">3.C40&lt;/a>&lt;/td>
 &lt;td>Resgate WebSocket-to-NATS&lt;/td>
 &lt;td>Request/Reply / Subject ACL / Core NATS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&amp;#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">3.C41&lt;/a>&lt;/td>
 &lt;td>i-flow OT/IT 整合&lt;/td>
 &lt;td>Cluster + Supercluster + Leaf node&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="redis-streams-案例">Redis Streams 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 Redis Streams 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &amp;#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &amp;#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &amp;#43; retry &amp;#43; DLQ、idempotent processing。">3.C42&lt;/a>&lt;/td>
 &lt;td>Bitso Reliable Streams + DLQ&lt;/td>
 &lt;td>Consumer group + PEL / XCLAIM / Sentinel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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&lt;/a>&lt;/td>
 &lt;td>Arcjet 取代 Kafka 省 6 位數 $&lt;/td>
 &lt;td>Retention / Memory 取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">3.C44&lt;/a>&lt;/td>
 &lt;td>Harness CD async state transfer&lt;/td>
 &lt;td>Consumer group + PEL / XCLAIM / Memory&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/" data-link-title="3.C45 Klaxit：Rust &amp;#43; Redis Streams 處理 Heroku Logplex" data-link-desc="Klaxit carpool 用 Redis Streams 處理 Heroku Logplex 匯流、自動偵測修復平台 perf 問題、6 個月 production Rust。">3.C45&lt;/a>&lt;/td>
 &lt;td>Klaxit Rust + Heroku Logplex&lt;/td>
 &lt;td>XADD / XREADGROUP / Consumer group&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&amp;#43;EBS 變 latency 痛點、退到 PostgreSQL。">3.C46&lt;/a>&lt;/td>
 &lt;td>Learning.com 退場（反例）&lt;/td>
 &lt;td>Memory + retention / Sentinel 可靠性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/" data-link-title="3.C47 PHP 微服務：Redis Streams &amp;#43; S3 hybrid storage" data-link-desc="PHP 雙微服務通訊、Kafka 在 PHP 生態工具薄弱、用 Redis Streams &amp;#43; payload compression &amp;#43; S3 hybrid 處理大訊息。">3.C47&lt;/a>&lt;/td>
 &lt;td>PHP 微服務 + S3 hybrid&lt;/td>
 &lt;td>XADD/XREAD / Retention / Memory&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="aws-sqs-案例">AWS SQS 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 SQS 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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 &amp;#43; DLQ 取代 Resque 單 Redis 限制、每 scheduler 1000 QPS、SQS wrap DynamoDB 處理 &amp;gt; 15 分鐘 delay。">3.C48&lt;/a>&lt;/td>
 &lt;td>Airbnb Dynein 延遲任務&lt;/td>
 &lt;td>Standard vs FIFO / DLQ 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/" data-link-title="3.C49 Airbnb Inspekt：Visibility timeout 當 retry budget" data-link-desc="Airbnb Inspekt 隱私掃描器、scanner pull message、visibility timeout 自然觸發重現、用重現次數當 retry budget。">3.C49&lt;/a>&lt;/td>
 &lt;td>Airbnb Inspekt visibility timeout&lt;/td>
 &lt;td>Visibility timeout + in-flight&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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 &amp;#43; Lambda：visibility timeout 應略高於最大處理時間、Lambda 初 5 個 long polling、可擴 60/min。">3.C50&lt;/a>&lt;/td>
 &lt;td>Capital One visibility timeout&lt;/td>
 &lt;td>Visibility timeout / SQS + Lambda&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &amp;#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51&lt;/a>&lt;/td>
 &lt;td>Atlassian JiRT Kinesis + SQS&lt;/td>
 &lt;td>Standard vs FIFO / fan-out subscription&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/" data-link-title="3.C52 Nielsen：Spark on EKS 雙 SQS 工作流" data-link-desc="Nielsen 每日 25TB / 30B event、work queue &amp;#43; completion queue 雙 SQS、queue depth autoscale EKS pod。">3.C52&lt;/a>&lt;/td>
 &lt;td>Nielsen Spark on EKS 雙 SQS&lt;/td>
 &lt;td>CloudWatch metric / autoscaling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/" data-link-title="3.C53 FINRA：S3 → SQS notification 大檔上傳" data-link-desc="FINRA 金融監管、broker 上傳大檔、S3 → SQS notification → LFS、KMS &amp;#43; bucket policy &amp;#43; queue policy 三層稽核。">3.C53&lt;/a>&lt;/td>
 &lt;td>FINRA S3 → SQS 合規&lt;/td>
 &lt;td>SQS + Lambda / IAM 多層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&amp;#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &amp;#43; Dispatcher fan-out 給訂閱者。">3.C54&lt;/a>&lt;/td>
 &lt;td>Twitch EventSub SNS+SQS&lt;/td>
 &lt;td>Standard queue / SNS-SQS fan-out&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/" data-link-title="3.C55 SmugMug：SQS 驅動可重放搜尋管線" data-link-desc="SmugMug 用 SQS 兩種模式：DynamoDB scan-segment 平行 backfill &amp;#43; production query 鏡像 replay 到 replica。">3.C55&lt;/a>&lt;/td>
 &lt;td>SmugMug 搜尋管線 backfill&lt;/td>
 &lt;td>Standard queue / Long polling / Lambda&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/" data-link-title="3.C56 PostNL EBE：完整 DLQ &amp;#43; retention &amp;#43; redrive 設計" data-link-desc="PostNL 物流每天 1000 萬訊息、每 producer/consumer 隔離 stack、24h 內 100 次 retry、final DLQ 可 consumer redrive。">3.C56&lt;/a>&lt;/td>
 &lt;td>PostNL EBE 完整 DLQ + redrive&lt;/td>
 &lt;td>DLQ 設計 / CloudWatch alarm / Cost&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/" data-link-title="3.C57 Lob：自家 fork @lob/sqs-consumer 修 FIFO bug" data-link-desc="Lob 原用 bbc/sqs-consumer 鎖 SDK v2、fork 出 @lob/sqs-consumer 支援 SDK v3 &amp;#43; TypeScript &amp;#43; 修 FIFO bug。">3.C57&lt;/a>&lt;/td>
 &lt;td>Lob @lob/sqs-consumer&lt;/td>
 &lt;td>Standard vs FIFO / Client library&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58&lt;/a>&lt;/td>
 &lt;td>Twilio SQS 緩衝 webhook&lt;/td>
 &lt;td>Long polling / Standard vs FIFO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/td>
 &lt;td>Rapid7 100 億 msg/day 規模&lt;/td>
 &lt;td>Cost 模型 / Standard queue&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="google-pubsub-案例">Google Pub/Sub 案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>公司 / 主題&lt;/th>
 &lt;th>對應 Pub/Sub 大綱章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-event-delivery-platform/" data-link-title="3.C60 Spotify：Event Delivery 從 Kafka 遷到 Pub/Sub" data-link-desc="Spotify 全球 event delivery 從 Kafka 遷到 Pub/Sub、~2500 VM、Q1 2019 8M events/s、350TB/day raw、自建 dedup。">3.C60&lt;/a>&lt;/td>
 &lt;td>Spotify Event Delivery 遷入&lt;/td>
 &lt;td>Pub/Sub vs Lite / Push vs Pull / Ack deadline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61&lt;/a>&lt;/td>
 &lt;td>Spotify Autoscaling 反效果&lt;/td>
 &lt;td>Ack deadline / autoscaling signal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-spotify-cloud-storage-export/" data-link-title="3.C62 Spotify：Pub/Sub → GCS reliable export" data-link-desc="Spotify 用 Oldest Unacknowledged Message metric 判斷 hourly bucket 何時可安全關閉、ack 綁定下游 commit。">3.C62&lt;/a>&lt;/td>
 &lt;td>Spotify reliable GCS export&lt;/td>
 &lt;td>Ack deadline / Cloud Storage subscription&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/" data-link-title="3.C63 Mercari Actionable History：ack deadline 是 batch-level" data-link-desc="Merpay 支付流水帳用 Pub/Sub、ack deadline 是整批 batch 而非單訊息、acked 訊息會跟同批 expired 一起 redeliver。">3.C63&lt;/a>&lt;/td>
 &lt;td>Mercari ack deadline batch-level&lt;/td>
 &lt;td>Ack deadline / Push vs Pull / Ordering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64&lt;/a>&lt;/td>
 &lt;td>Mercari Item Feed DLT&lt;/td>
 &lt;td>Dead-letter topic / Push vs Pull&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65&lt;/a>&lt;/td>
 &lt;td>Mercari LINE 對齊外部 RPS&lt;/td>
 &lt;td>Push vs Pull subscription&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-mercari-b2c-grpc-pusher/" data-link-title="3.C66 Mercari B2C：自建 PubSub gRPC Pusher" data-link-desc="Mercari 全球商品同步、原生 HTTP push 在「長 job &amp;#43; 高吞吐 &amp;#43; 動態 RPS」場景受限、自建 gRPC 版 push。">3.C66&lt;/a>&lt;/td>
 &lt;td>Mercari B2C 自建 gRPC pusher&lt;/td>
 &lt;td>Push vs Pull / Ordering 應用層處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/" data-link-title="3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest" data-link-desc="Pokémon GO frontend publish 玩家事件、~1M TPS、Pub/Sub elastic buffer、下游 BigQuery streaming。">3.C67&lt;/a>&lt;/td>
 &lt;td>Niantic Pokémon GO telemetry&lt;/td>
 &lt;td>BigQuery subscription（pattern 對照）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/" data-link-title="3.C68 Wix：Pub/Sub decouple &amp;#43; Dataflow &amp;#43; BQ archive" data-link-desc="Wix App Engine 收 clickstream 進 Pub/Sub、Dataflow 進 Datastore &amp;lt; 100ms、BigQuery 並行存 raw recovery。">3.C68&lt;/a>&lt;/td>
 &lt;td>Wix clickstream + Dataflow + BQ&lt;/td>
 &lt;td>BigQuery subscription / Push vs Pull&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/pubsub-twitter-ad-engagement/" data-link-title="3.C69 Twitter Ad Engagement：把 stream 切成多 topic 做 partition" data-link-desc="Twitter 把 80K msg/s stream 切成 6 個 topic 做 partition、Avro schema、Beam/Dataflow → Bigtable/BQ。">3.C69&lt;/a>&lt;/td>
 &lt;td>Twitter Ad Engagement topic 切分&lt;/td>
 &lt;td>Schema enforcement / Ordering key&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例覆蓋缺口待補">案例覆蓋缺口（待補）&lt;/h2>
&lt;p>下列大綱章節在本案例庫中&lt;strong>公開 customer-side case 偏弱或缺&lt;/strong>、撰寫正文時要明示「以下分析依官方文件 / KIP / 通用模式推導、非 case-driven」：&lt;/p></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把 broker、queue 與語義治理的轉換壓力落到可執行判讀、並提供各 vendor 的真實 production case 庫支撐撰寫。案例不是事後舉例、是寫作 finding 的 source — 章節該討論的議題從 case 反推、不是先寫章節再找案例填。</p>
<h2 id="通用案例跨-vendor--反例--規模對照">通用案例（跨 vendor / 反例 / 規模對照）</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/meta-foqs-global-migration/" data-link-title="3.C1 Meta：FOQS 從區域到全域佇列遷移" data-link-desc="佇列架構如何在不中斷下升級成 disaster-ready 模式。">3.C1</a></td>
          <td>Meta FOQS 全域遷移</td>
          <td>區域佇列如何升級到 disaster-ready 架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2</a></td>
          <td>VMware Kafka → MSK</td>
          <td>自管 broker 轉 managed streaming 的治理重點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3</a></td>
          <td>LinkedIn TopicGC</td>
          <td>topic 生命週期治理如何影響叢集可靠性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4</a></td>
          <td>LinkedIn Kafka 分層</td>
          <td>把單叢集使用模式轉成分層叢集治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5</a></td>
          <td>Slack Job Queue</td>
          <td>背景工作通道轉成 Kafka + Redis 組合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/uber-kafka-infrastructure-evolution/" data-link-title="3.C6 Uber：Kafka 事件平台演進" data-link-desc="事件平台從團隊自管走向多租戶共享基礎設施。">3.C6</a></td>
          <td>Uber Kafka 基礎設施</td>
          <td>把事件平台演進成多租戶共享能力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/linkedin-kafka-self-healing-automation/" data-link-title="3.C7 LinkedIn：Kafka 自動修復治理" data-link-desc="Kafka 維運從人工處置轉向自動修復的案例。">3.C7</a></td>
          <td>LinkedIn Self-healing Kafka</td>
          <td>把手動維運轉成自動修復治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/cloudflare-queues-global-delivery-model/" data-link-title="3.C8 Cloudflare：Queues 全球交付模型" data-link-desc="事件佇列服務在全球網路下的交付語義與治理案例。">3.C8</a></td>
          <td>Cloudflare Queues</td>
          <td>把全球佇列傳遞模型轉成可治理交付路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9</a></td>
          <td>反例：語義切換失敗</td>
          <td>at-least-once / exactly-once 語義誤配造成資料錯亂</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10</a></td>
          <td>對照：規模差異下佇列模型</td>
          <td>同一佇列模型在不同規模下有不同治理與失敗邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="kafka-案例">Kafka 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 Kafka 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-tiered-storage/" data-link-title="3.C11 Pinterest：Kafka tiered storage broker-decoupled" data-link-desc="Pinterest 採 broker-decoupled tiered storage、把 ~200 TB/day 熱資料卸到 S3、broker 不再是熱路徑。">3.C11</a></td>
          <td>Pinterest Tiered Storage</td>
          <td>Tiered storage</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-pinterest-shallow-mirror/" data-link-title="3.C12 Pinterest：Shallow Mirror 優化 MirrorMaker" data-link-desc="Pinterest 跨 3 region MirrorMaker、原版解壓&#43;重壓造成 CPU/memory 2-10x spike、改 RecordBatch 層淺迭代。">3.C12</a></td>
          <td>Pinterest Shallow Mirror</td>
          <td>Cross-region MirrorMaker</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-shopify-debezium-cdc/" data-link-title="3.C13 Shopify：Debezium CDC over sharded MySQL" data-link-desc="Shopify 100&#43; MySQL shard、150 Debezium connector、Black Friday 100K records/sec P99 &lt; 10s。">3.C13</a></td>
          <td>Shopify Debezium CDC</td>
          <td>Kafka Connect / CDC</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-yelp-schematizer/" data-link-title="3.C14 Yelp：Schematizer 自建 Schema Registry" data-link-desc="Yelp data pipeline 強制所有 message 走 Avro、自建 Schematizer 做 schema evolution 與 topic 自動分配。">3.C14</a></td>
          <td>Yelp Schematizer</td>
          <td>Schema Registry / Schema evolution</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-airbnb-spark-streaming-rebalance/" data-link-title="3.C15 Airbnb：Spark Streaming Kafka reader rebalance" data-link-desc="Airbnb logging pipeline 解 partition-task 1:1 造成的 data skew、catch-up 4 小時 lag 要再花 4 小時的反效率。">3.C15</a></td>
          <td>Airbnb Spark Streaming</td>
          <td>Consumer 設計 / partition + consumer group</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16</a></td>
          <td>Robinhood Faust</td>
          <td>跨語言 client / stream processing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-walmart-mps-rebalance/" data-link-title="3.C17 Walmart：Messaging Proxy Service 解 rebalance storm" data-link-desc="Walmart 每天 trillions of message、25K&#43; consumer 在 K8s、partition-consumer 1:1 模型撞到擴張極限。">3.C17</a></td>
          <td>Walmart MPS</td>
          <td>Rebalance storm / consumer lag / multi-tenant</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-wix-greyhound-troubleshooting/" data-link-title="3.C18 Wix：Greyhound TLLSR 解 consumer 卡住" data-link-desc="Wix 2000&#43; microservice 66B msg/day、自建 Greyhound 抽象、TLLSR 框架解 single-partition lag / poison pill / handler 卡住。">3.C18</a></td>
          <td>Wix Greyhound</td>
          <td>Consumer lag / observability / poison message</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-wix-multi-cluster-migration/" data-link-title="3.C19 Wix：Multi-cluster Kafka zero-downtime 遷移" data-link-desc="Wix metadata 從 5K topic 漲到 20K topic / 200K partition、controller startup 跟 broker stability 受壓垮、分多 cluster 解決。">3.C19</a></td>
          <td>Wix Multi-cluster</td>
          <td>Topic 生命週期 / 分層叢集</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-spotify-event-delivery-exodus/" data-link-title="3.C20 Spotify：Event Delivery 從 Kafka 遷出（反例）" data-link-desc="Spotify Kafka 0.7 MirrorMaker best-effort 會掉資料但回報成功、broker restart 後 producer 無法恢復、決定遷到 GCP Pub/Sub。">3.C20</a></td>
          <td>Spotify 遷出 Kafka（反例）</td>
          <td>Replication 失敗模式 / producer 可靠性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-goldman-sachs-msk-migration/" data-link-title="3.C21 Goldman Sachs：MSK 遷移 with MirrorMaker 2" data-link-desc="Goldman Sachs Global Investment Research 從 on-prem Kafka 遷到 MSK、用 MM2 同步 topic/ACL/offset、atomic cutover 7 小時完成。">3.C21</a></td>
          <td>Goldman Sachs MSK</td>
          <td>Cross-region MirrorMaker / managed broker 遷移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22</a></td>
          <td>Trivago KEDA</td>
          <td>Consumer lag / autoscaling</td>
      </tr>
  </tbody>
</table>
<h2 id="rabbitmq-案例">RabbitMQ 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 RabbitMQ 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-bloomberg-multi-tenant-vhost/" data-link-title="3.C23 Bloomberg：多租戶 vhost &#43; 自助平台化" data-link-desc="Bloomberg 從幾個團隊推到上百個團隊、靠自助 vhost 註冊跟專用叢集分離應用與 broker。">3.C23</a></td>
          <td>Bloomberg vhost 多租戶</td>
          <td>多 vhost + 多租戶 / Erlang clustering</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-soundcloud-fanout-audio/" data-link-title="3.C24 SoundCloud：AMQP fan-out 音訊處理 pipeline" data-link-desc="SoundCloud 每秒 20-30K persistent message、不同處理類型分開隊列、各自獨立 scale。">3.C24</a></td>
          <td>SoundCloud fan-out 音訊</td>
          <td>Prefetch + consumer 併發 / Streams</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25</a></td>
          <td>Indeed Delay + DLQ</td>
          <td>Dead-letter exchange / retry 策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26</a></td>
          <td>GoCardless Hutch service mesh</td>
          <td>Exchange types / 多 vhost（反向）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-zalando-aws-master-selection/" data-link-title="3.C27 Zalando：RabbitMQ on AWS 自動化 master selection" data-link-desc="Zalando 用 sidekick 服務查 AWS API 動態識別 cluster、指定最老 instance 當 master、跨版本升級用 federation 過渡。">3.C27</a></td>
          <td>Zalando AWS master selection</td>
          <td>Erlang clustering / Federation / Operator</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/" data-link-title="3.C28 WeWork：Consistent hash exchange 保證帳戶順序" data-link-desc="WeWork 固定數量 queue &#43; account ID hash 路由、每 queue 一個 worker &#43; exclusive consumer 保 partition-level ordering。">3.C28</a></td>
          <td>WeWork consistent hash</td>
          <td>Exchange types / partition-level ordering</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-bunny-channel-pool/" data-link-title="3.C29 WeWork：Bunny &#43; Puma 多執行緒 channel pool" data-link-desc="WeWork 從 Unicorn 切到 Puma 後遇 ConnectionClosedError、根因是 AMQP channel 跨執行緒共用、改用 connection_pool 管理。">3.C29</a></td>
          <td>WeWork Bunny channel pool</td>
          <td>Prefetch + consumer 併發（client lib）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-runtastic-mirrored-queue-bottleneck/" data-link-title="3.C30 Runtastic：Mirrored queue 網路負載瓶頸" data-link-desc="Runtastic 2020 lockdown 流量暴增、performance test 揭露 mirroring 邏輯把網路元件壓垮、調整 mirroring 配置消除瓶頸。">3.C30</a></td>
          <td>Runtastic mirrored queue 瓶頸</td>
          <td>Mirrored queue → Quorum queue 遷移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-mozilla-pulse-naming-isolation/" data-link-title="3.C31 Mozilla Pulse：命名前綴 &#43; ACL 取代 vhost 多租戶" data-link-desc="Mozilla Pulse 不用 vhost、改用權限 &#43; 命名前綴 (exchange/{user}/*) 做隔離、CloudAMQP 託管、PulseGuardian 管使用者。">3.C31</a></td>
          <td>Mozilla Pulse naming isolation</td>
          <td>多 vhost + 多租戶（反向：用 ACL + naming）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-loyaltylion-monitoring-thousands/" data-link-title="3.C32 LoyaltyLion：監控數千 RabbitMQ queue" data-link-desc="LoyaltyLion 跑數千 queue、用 rabbitmqctl &#43; statsd 推 Datadog、揭露大規模 queue 拓樸下原生 plugin API 不夠用。">3.C32</a></td>
          <td>LoyaltyLion 監控數千 queue</td>
          <td>監控觀測 / Operator</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/rabbitmq-wargaming-game-portal-decoupling/" data-link-title="3.C33 Wargaming：World of Tanks 戰後 dossier 解耦" data-link-desc="Wargaming WoT server 全 Linux、戰後 dossier 寫 RabbitMQ、portal 顯示統計而不增 game server load。">3.C33</a></td>
          <td>Wargaming game portal 解耦</td>
          <td>Federation + Shovel / 多 vhost</td>
      </tr>
  </tbody>
</table>
<h2 id="nats-案例">NATS 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 NATS 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-netlify-data-plane-fanout/" data-link-title="3.C34 Netlify：NATS 當全球 metrics/logs 統一資料平面" data-link-desc="Netlify 70K&#43; 網站、10 億 PV/月、跨多雲、NATS 當 all-purpose data plane fan-out bus、超 RabbitMQ 評估。">3.C34</a></td>
          <td>Netlify 全球資料平面 fan-out</td>
          <td>Core NATS vs JetStream / subject-based routing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-form3-multi-cloud-payments/" data-link-title="3.C35 Form3：NATS JetStream 多雲低延遲支付" data-link-desc="Form3 服務 Tier-1 銀行、500ms SLA、SNS/SQS 吃 300ms 預算、改 NATS&#43;JetStream 跨雲 6x 延遲改善。">3.C35</a></td>
          <td>Form3 多雲低延遲支付</td>
          <td>Cluster + Supercluster + Leaf node / JetStream</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-intelecy-industrial-iot/" data-link-title="3.C36 Intelecy：工業 IoT 即時感測 &#43; 多租戶" data-link-desc="Intelecy 工廠 gateway 接數萬感測器、&lt; 2 秒往返延遲做即時 ML、從 BoltDB 本地快取演進到 JetStream 持久化。">3.C36</a></td>
          <td>Intelecy 工業 IoT</td>
          <td>JetStream stream / Subject-based ACL</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-machinemetrics-edge-to-cloud/" data-link-title="3.C37 MachineMetrics：邊緣到雲端工廠資料管線" data-link-desc="MachineMetrics 跨數百工廠、數千機台、1000Hz 採樣、Kinesis 無法跑在 edge、改 NATS Leaf Node &#43; JetStream &#43; KV &#43; Object Store。">3.C37</a></td>
          <td>MachineMetrics edge to cloud</td>
          <td>Leaf node / KV + Object Store / 多租戶 ACL</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">3.C38</a></td>
          <td>Clarifai NATS Streaming ML</td>
          <td>JetStream consumer 設計 / Queue groups</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-choria-orchestration-fleet/" data-link-title="3.C39 Choria：NATS 管 50 萬 server fleet" data-link-desc="Choria 替代 Puppet MCollective、NATS 單 binary 無 Zookeeper、4GB node 可達 50 萬 server、wildcard &#43; queue group 做 scatter-gather RPC。">3.C39</a></td>
          <td>Choria fleet orchestration</td>
          <td>Request/Reply / Queue groups / Supercluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-resgate-realtime-api-gateway/" data-link-title="3.C40 Resgate：WebSocket-to-NATS realtime API gateway" data-link-desc="Resgate 把 NATS subject 暴露成 REST &#43; WebSocket、subject 階層當 schema、event 延遲 &lt; 1ms、純 Core NATS。">3.C40</a></td>
          <td>Resgate WebSocket-to-NATS</td>
          <td>Request/Reply / Subject ACL / Core NATS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/nats-iflow-ot-it-integration/" data-link-title="3.C41 i-flow：NATS 做 OT/IT 跨層整合 bus" data-link-desc="i-flow 每日 4 億筆 data operation、200&#43; OT/IT connector、客戶含 Bosch / Sto / Lenze、NATS 當邊緣到 central 整合 bus。">3.C41</a></td>
          <td>i-flow OT/IT 整合</td>
          <td>Cluster + Supercluster + Leaf node</td>
      </tr>
  </tbody>
</table>
<h2 id="redis-streams-案例">Redis Streams 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 Redis Streams 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">3.C42</a></td>
          <td>Bitso Reliable Streams + DLQ</td>
          <td>Consumer group + PEL / XCLAIM / Sentinel</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>Arcjet 取代 Kafka 省 6 位數 $</td>
          <td>Retention / Memory 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">3.C44</a></td>
          <td>Harness CD async state transfer</td>
          <td>Consumer group + PEL / XCLAIM / Memory</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/" data-link-title="3.C45 Klaxit：Rust &#43; Redis Streams 處理 Heroku Logplex" data-link-desc="Klaxit carpool 用 Redis Streams 處理 Heroku Logplex 匯流、自動偵測修復平台 perf 問題、6 個月 production Rust。">3.C45</a></td>
          <td>Klaxit Rust + Heroku Logplex</td>
          <td>XADD / XREADGROUP / Consumer group</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">3.C46</a></td>
          <td>Learning.com 退場（反例）</td>
          <td>Memory + retention / Sentinel 可靠性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/" data-link-title="3.C47 PHP 微服務：Redis Streams &#43; S3 hybrid storage" data-link-desc="PHP 雙微服務通訊、Kafka 在 PHP 生態工具薄弱、用 Redis Streams &#43; payload compression &#43; S3 hybrid 處理大訊息。">3.C47</a></td>
          <td>PHP 微服務 + S3 hybrid</td>
          <td>XADD/XREAD / Retention / Memory</td>
      </tr>
  </tbody>
</table>
<h2 id="aws-sqs-案例">AWS SQS 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 SQS 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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</a></td>
          <td>Airbnb Dynein 延遲任務</td>
          <td>Standard vs FIFO / DLQ 設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/" data-link-title="3.C49 Airbnb Inspekt：Visibility timeout 當 retry budget" data-link-desc="Airbnb Inspekt 隱私掃描器、scanner pull message、visibility timeout 自然觸發重現、用重現次數當 retry budget。">3.C49</a></td>
          <td>Airbnb Inspekt visibility timeout</td>
          <td>Visibility timeout + in-flight</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>Capital One visibility timeout</td>
          <td>Visibility timeout / SQS + Lambda</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51</a></td>
          <td>Atlassian JiRT Kinesis + SQS</td>
          <td>Standard vs FIFO / fan-out subscription</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/" data-link-title="3.C52 Nielsen：Spark on EKS 雙 SQS 工作流" data-link-desc="Nielsen 每日 25TB / 30B event、work queue &#43; completion queue 雙 SQS、queue depth autoscale EKS pod。">3.C52</a></td>
          <td>Nielsen Spark on EKS 雙 SQS</td>
          <td>CloudWatch metric / autoscaling</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/" data-link-title="3.C53 FINRA：S3 → SQS notification 大檔上傳" data-link-desc="FINRA 金融監管、broker 上傳大檔、S3 → SQS notification → LFS、KMS &#43; bucket policy &#43; queue policy 三層稽核。">3.C53</a></td>
          <td>FINRA S3 → SQS 合規</td>
          <td>SQS + Lambda / IAM 多層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54</a></td>
          <td>Twitch EventSub SNS+SQS</td>
          <td>Standard queue / SNS-SQS fan-out</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/" data-link-title="3.C55 SmugMug：SQS 驅動可重放搜尋管線" data-link-desc="SmugMug 用 SQS 兩種模式：DynamoDB scan-segment 平行 backfill &#43; production query 鏡像 replay 到 replica。">3.C55</a></td>
          <td>SmugMug 搜尋管線 backfill</td>
          <td>Standard queue / Long polling / Lambda</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/" data-link-title="3.C56 PostNL EBE：完整 DLQ &#43; retention &#43; redrive 設計" data-link-desc="PostNL 物流每天 1000 萬訊息、每 producer/consumer 隔離 stack、24h 內 100 次 retry、final DLQ 可 consumer redrive。">3.C56</a></td>
          <td>PostNL EBE 完整 DLQ + redrive</td>
          <td>DLQ 設計 / CloudWatch alarm / Cost</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/" data-link-title="3.C57 Lob：自家 fork @lob/sqs-consumer 修 FIFO bug" data-link-desc="Lob 原用 bbc/sqs-consumer 鎖 SDK v2、fork 出 @lob/sqs-consumer 支援 SDK v3 &#43; TypeScript &#43; 修 FIFO bug。">3.C57</a></td>
          <td>Lob @lob/sqs-consumer</td>
          <td>Standard vs FIFO / Client library</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58</a></td>
          <td>Twilio SQS 緩衝 webhook</td>
          <td>Long polling / Standard vs FIFO</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>Rapid7 100 億 msg/day 規模</td>
          <td>Cost 模型 / Standard queue</td>
      </tr>
  </tbody>
</table>
<h2 id="google-pubsub-案例">Google Pub/Sub 案例</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>公司 / 主題</th>
          <th>對應 Pub/Sub 大綱章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-event-delivery-platform/" data-link-title="3.C60 Spotify：Event Delivery 從 Kafka 遷到 Pub/Sub" data-link-desc="Spotify 全球 event delivery 從 Kafka 遷到 Pub/Sub、~2500 VM、Q1 2019 8M events/s、350TB/day raw、自建 dedup。">3.C60</a></td>
          <td>Spotify Event Delivery 遷入</td>
          <td>Pub/Sub vs Lite / Push vs Pull / Ack deadline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61</a></td>
          <td>Spotify Autoscaling 反效果</td>
          <td>Ack deadline / autoscaling signal</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-spotify-cloud-storage-export/" data-link-title="3.C62 Spotify：Pub/Sub → GCS reliable export" data-link-desc="Spotify 用 Oldest Unacknowledged Message metric 判斷 hourly bucket 何時可安全關閉、ack 綁定下游 commit。">3.C62</a></td>
          <td>Spotify reliable GCS export</td>
          <td>Ack deadline / Cloud Storage subscription</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-actionable-history/" data-link-title="3.C63 Mercari Actionable History：ack deadline 是 batch-level" data-link-desc="Merpay 支付流水帳用 Pub/Sub、ack deadline 是整批 batch 而非單訊息、acked 訊息會跟同批 expired 一起 redeliver。">3.C63</a></td>
          <td>Mercari ack deadline batch-level</td>
          <td>Ack deadline / Push vs Pull / Ordering</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64</a></td>
          <td>Mercari Item Feed DLT</td>
          <td>Dead-letter topic / Push vs Pull</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-line-flow-control/" data-link-title="3.C65 Mercari LINE：Pull subscription 對齊外部 RPS" data-link-desc="Mercari LINE webhook 轉 Pub/Sub、worker pull subscription 精確控制 RPS、應 LINE API 限制。">3.C65</a></td>
          <td>Mercari LINE 對齊外部 RPS</td>
          <td>Push vs Pull subscription</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-mercari-b2c-grpc-pusher/" data-link-title="3.C66 Mercari B2C：自建 PubSub gRPC Pusher" data-link-desc="Mercari 全球商品同步、原生 HTTP push 在「長 job &#43; 高吞吐 &#43; 動態 RPS」場景受限、自建 gRPC 版 push。">3.C66</a></td>
          <td>Mercari B2C 自建 gRPC pusher</td>
          <td>Push vs Pull / Ordering 應用層處理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-niantic-pokemon-go-telemetry/" data-link-title="3.C67 Niantic Pokémon GO：Pub/Sub 當 telemetry ingest" data-link-desc="Pokémon GO frontend publish 玩家事件、~1M TPS、Pub/Sub elastic buffer、下游 BigQuery streaming。">3.C67</a></td>
          <td>Niantic Pokémon GO telemetry</td>
          <td>BigQuery subscription（pattern 對照）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-wix-clickstream-dashboard/" data-link-title="3.C68 Wix：Pub/Sub decouple &#43; Dataflow &#43; BQ archive" data-link-desc="Wix App Engine 收 clickstream 進 Pub/Sub、Dataflow 進 Datastore &lt; 100ms、BigQuery 並行存 raw recovery。">3.C68</a></td>
          <td>Wix clickstream + Dataflow + BQ</td>
          <td>BigQuery subscription / Push vs Pull</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/cases/pubsub-twitter-ad-engagement/" data-link-title="3.C69 Twitter Ad Engagement：把 stream 切成多 topic 做 partition" data-link-desc="Twitter 把 80K msg/s stream 切成 6 個 topic 做 partition、Avro schema、Beam/Dataflow → Bigtable/BQ。">3.C69</a></td>
          <td>Twitter Ad Engagement topic 切分</td>
          <td>Schema enforcement / Ordering key</td>
      </tr>
  </tbody>
</table>
<h2 id="案例覆蓋缺口待補">案例覆蓋缺口（待補）</h2>
<p>下列大綱章節在本案例庫中<strong>公開 customer-side case 偏弱或缺</strong>、撰寫正文時要明示「以下分析依官方文件 / KIP / 通用模式推導、非 case-driven」：</p>
<ul>
<li><strong>Kafka KRaft</strong>：缺 customer-side 一手案例、目前依官方 KIP-833 / Confluent 公告為準</li>
<li><strong>RabbitMQ MQTT plugin + 多協議</strong>：缺 IoT 廠商 customer case、可補 RabbitMQ 官方 native MQTT blog</li>
<li><strong>RabbitMQ Cluster Operator（K8s）</strong>：缺直接案例、Zalando 案例是 pre-K8s 對照</li>
<li><strong>Redis Streams + Functions（Redis 7+）</strong>：公開 customer case 幾乎沒有</li>
<li><strong>Redis Cluster on Streams</strong>：公開 case 多在 single-instance / Sentinel 規模、Cluster 案例少</li>
<li><strong>Pub/Sub IAM + Service Account</strong>：customer engineering blog 著墨少、建議依 GCP 官方 IAM 文件 + 通用安全原則</li>
</ul>
<p>案例庫總計 69 個、其中 Kafka 17 個（含通用層 5）、RabbitMQ 11 個、NATS 8 個、Redis Streams 6 個、SQS 12 個（含通用層 1）、Pub/Sub 10 個、純通用 / 反例 4 個。</p>
]]></content:encoded></item><item><title>模組五案例正文</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/</guid><description>&lt;p>這個資料夾的核心責任是把平台遷移案例轉成部署策略、切流策略與回退策略的可操作內容。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1&lt;/a>&lt;/td>
 &lt;td>Tradeshift：self-managed K8s -&amp;gt; EKS&lt;/td>
 &lt;td>把零停機平台遷移拆成可執行階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2&lt;/a>&lt;/td>
 &lt;td>Condé Nast：EKS 平台整併&lt;/td>
 &lt;td>把多團隊異質集群整併成單一治理面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3&lt;/a>&lt;/td>
 &lt;td>Orbitera：遷移到 managed Kubernetes&lt;/td>
 &lt;td>把平台重置與產品不中斷目標對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4&lt;/a>&lt;/td>
 &lt;td>Mobileye：workloads -&amp;gt; EKS&lt;/td>
 &lt;td>把工作負載搬遷策略做成可回退階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5&lt;/a>&lt;/td>
 &lt;td>Miro：managed EKS 遷移&lt;/td>
 &lt;td>把平台託管化與團隊維運模型對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6&lt;/a>&lt;/td>
 &lt;td>Airbnb K8s 叢集演進&lt;/td>
 &lt;td>把手動擴縮轉成自動化容量治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7&lt;/a>&lt;/td>
 &lt;td>Airbnb Istio 升級治理&lt;/td>
 &lt;td>把 service mesh 升級變成可重播流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9&lt;/a>&lt;/td>
 &lt;td>反例：切流未先 drain&lt;/td>
 &lt;td>平台切換忽略連線清退造成錯誤暴增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下平台遷移&lt;/td>
 &lt;td>小中大型組織的平台遷移風險邊界不同&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把平台遷移案例轉成部署策略、切流策略與回退策略的可操作內容。</p>
<h2 id="案例列表">案例列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1</a></td>
          <td>Tradeshift：self-managed K8s -&gt; EKS</td>
          <td>把零停機平台遷移拆成可執行階段</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2</a></td>
          <td>Condé Nast：EKS 平台整併</td>
          <td>把多團隊異質集群整併成單一治理面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3</a></td>
          <td>Orbitera：遷移到 managed Kubernetes</td>
          <td>把平台重置與產品不中斷目標對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4</a></td>
          <td>Mobileye：workloads -&gt; EKS</td>
          <td>把工作負載搬遷策略做成可回退階段</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5</a></td>
          <td>Miro：managed EKS 遷移</td>
          <td>把平台託管化與團隊維運模型對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6</a></td>
          <td>Airbnb K8s 叢集演進</td>
          <td>把手動擴縮轉成自動化容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7</a></td>
          <td>Airbnb Istio 升級治理</td>
          <td>把 service mesh 升級變成可重播流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9</a></td>
          <td>反例：切流未先 drain</td>
          <td>平台切換忽略連線清退造成錯誤暴增</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10</a></td>
          <td>對照：規模差異下平台遷移</td>
          <td>小中大型組織的平台遷移風險邊界不同</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>模組零案例正文</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/cases/</guid><description>&lt;p>這個資料夾的核心責任是把案例圖譜中的產業壓力轉成可回寫正文。圖譜負責索引，正文負責判讀訊號、風險邊界與下一步路由。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cases/fintech-compliance-and-selection-pressure/" data-link-title="FinTech：合規壓力下的後端選型" data-link-desc="在審計、留存與交易正確性要求下，如何平衡成本、風險與交付速度。">0.C1&lt;/a>&lt;/td>
 &lt;td>FinTech 合規壓力&lt;/td>
 &lt;td>把合規、留存、審計對選型的影響變成可判讀條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cases/gaming-peak-traffic-and-isolation/" data-link-title="Gaming：高峰流量與隔離邊界選型" data-link-desc="大型活動流量下，如何在低延遲與穩定性之間做可持續取捨。">0.C2&lt;/a>&lt;/td>
 &lt;td>Gaming 高峰流量&lt;/td>
 &lt;td>把低延遲與高峰容量風險轉成分層決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cases/healthcare-data-sovereignty-and-recovery/" data-link-title="Healthcare：資料主權與回復順序選型" data-link-desc="醫療場景下，如何把資料主權、存取邊界與災難回復放進同一套決策。">0.C3&lt;/a>&lt;/td>
 &lt;td>Healthcare 資料主權&lt;/td>
 &lt;td>把資料主權與回復順序放進同一個選型模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4&lt;/a>&lt;/td>
 &lt;td>營運後技術轉換&lt;/td>
 &lt;td>營運成熟後何時要轉語言、工具或架構，以及轉換成本邏輯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把案例圖譜中的產業壓力轉成可回寫正文。圖譜負責索引，正文負責判讀訊號、風險邊界與下一步路由。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cases/fintech-compliance-and-selection-pressure/" data-link-title="FinTech：合規壓力下的後端選型" data-link-desc="在審計、留存與交易正確性要求下，如何平衡成本、風險與交付速度。">0.C1</a></td>
          <td>FinTech 合規壓力</td>
          <td>把合規、留存、審計對選型的影響變成可判讀條件</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cases/gaming-peak-traffic-and-isolation/" data-link-title="Gaming：高峰流量與隔離邊界選型" data-link-desc="大型活動流量下，如何在低延遲與穩定性之間做可持續取捨。">0.C2</a></td>
          <td>Gaming 高峰流量</td>
          <td>把低延遲與高峰容量風險轉成分層決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cases/healthcare-data-sovereignty-and-recovery/" data-link-title="Healthcare：資料主權與回復順序選型" data-link-desc="醫療場景下，如何把資料主權、存取邊界與災難回復放進同一套決策。">0.C3</a></td>
          <td>Healthcare 資料主權</td>
          <td>把資料主權與回復順序放進同一個選型模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4</a></td>
          <td>營運後技術轉換</td>
          <td>營運成熟後何時要轉語言、工具或架構，以及轉換成本邏輯</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.10 Workload Identity 與聯邦信任邊界</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/</guid><description>&lt;p>本章的責任是把機器到機器信任風險拆成可驗證邊界，讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> 不會把外部風險直接帶入內部高權限路徑。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 workload identity、federation、短時憑證與信任收斂，不討論雲廠商特定設定語法。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：workload 身份來源不清 / 跨平台信任擴張過快 / federation token scope 漂移 / 短時憑證策略不完整 / federation 回查不足 / 第三方授權範圍跟事件傳導半徑。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>人類身分 → &lt;a href="../identity-access-boundary/">7.2&lt;/a>&lt;/li>
&lt;li>機器憑證 lifecycle / 簽章金鑰 → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>傳輸層 validation 路徑 → &lt;a href="../transport-trust-and-certificate-lifecycle/">7.5&lt;/a>&lt;/li>
&lt;li>供應鏈 artifact 信任 → &lt;a href="../supply-chain-integrity-and-artifact-trust/">7.12&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[token-revocation]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「下一步路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="跨章議題交叉引用">跨章議題交叉引用&lt;/h2>
&lt;p>本章「第三方授權範圍跟事件傳導半徑」是 &lt;a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導 SSoT&lt;/a> 在 workload identity 層的展現；canonical SSoT 在 7.2、本章補 federation token scope 過寬的 specific 訊號。&lt;/p>
&lt;h2 id="workload-identity-治理模型">Workload Identity 治理模型&lt;/h2>
&lt;p>workload identity 的核心責任是把機器身份與人類身份分開治理，避免長期共享憑證形成不可控傳導。&lt;/p>
&lt;ol>
&lt;li>身分分離：把人類操作身份與機器執行身份拆分責任。&lt;/li>
&lt;li>邊界定義：把 workload 可觸及資源限制在最小業務範圍。&lt;/li>
&lt;li>聯邦信任：把跨平台 token 交換限制在可驗證來源與用途。&lt;/li>
&lt;li>短時憑證：把憑證有效時窗縮短，降低竊取後可利用時間。&lt;/li>
&lt;li>收斂節奏：把外部事件後的信任重評估納入固定流程。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「機器可用憑證」轉成「機器可控身份」。&lt;/p>
&lt;ol>
&lt;li>先盤點 workload 身份來源、簽發路徑與責任主體。&lt;/li>
&lt;li>再檢查 token scope、TTL 與可觸及資源是否超出用途。&lt;/li>
&lt;li>接著檢查 federation 來源與授權決策是否可回查。&lt;/li>
&lt;li>最後把缺口交接到平台、可靠性與事件收斂流程。&lt;/li>
&lt;/ol>
&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>credential 缺乏發放責任鏈&lt;/td>
 &lt;td>憑證可用窗口失控&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨平台信任擴張過快&lt;/td>
 &lt;td>token 使用面超出預期服務邊界&lt;/td>
 &lt;td>外部事件可快速傳導&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>短時憑證策略不完整&lt;/td>
 &lt;td>失效節奏與授權節奏分離&lt;/td>
 &lt;td>撤銷成本上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>federation 回查不足&lt;/td>
 &lt;td>信任來源與授權決策無法回串&lt;/td>
 &lt;td>事故判讀時間延長&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="federation-信任漂移跟跨平台-token-重評估">Federation 信任漂移跟跨平台 token 重評估&lt;/h2>
&lt;p>Federation 信任漂移是 workload identity 獨有的失效模式：信任關係建立後、token 的 &lt;em>來源&lt;/em> 跟 &lt;em>用途&lt;/em> 隨時間逐步脫鉤、攻擊者可在非預期服務持續使用同一個 federated token。控制責任是定期重評估信任關係的有效性、把 federation 視為長期演化中的信任配置、跟業務變動 cycle 同步。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把機器到機器信任風險拆成可驗證邊界，讓 <a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a> 與 <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> 不會把外部風險直接帶入內部高權限路徑。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 workload identity、federation、短時憑證與信任收斂，不討論雲廠商特定設定語法。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：workload 身份來源不清 / 跨平台信任擴張過快 / federation token scope 漂移 / 短時憑證策略不完整 / federation 回查不足 / 第三方授權範圍跟事件傳導半徑。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>人類身分 → <a href="../identity-access-boundary/">7.2</a></li>
<li>機器憑證 lifecycle / 簽章金鑰 → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li>傳輸層 validation 路徑 → <a href="../transport-trust-and-certificate-lifecycle/">7.5</a></li>
<li>供應鏈 artifact 信任 → <a href="../supply-chain-integrity-and-artifact-trust/">7.12</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[token-revocation]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「下一步路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="跨章議題交叉引用">跨章議題交叉引用</h2>
<p>本章「第三方授權範圍跟事件傳導半徑」是 <a href="../identity-access-boundary/#%e8%b7%a8%e7%ab%a0-ssot%e4%be%9b%e6%87%89%e5%95%86%e8%ba%ab%e5%88%86%e9%8f%88%e5%82%b3%e5%b0%8e">7.2 供應商身分鏈傳導 SSoT</a> 在 workload identity 層的展現；canonical SSoT 在 7.2、本章補 federation token scope 過寬的 specific 訊號。</p>
<h2 id="workload-identity-治理模型">Workload Identity 治理模型</h2>
<p>workload identity 的核心責任是把機器身份與人類身份分開治理，避免長期共享憑證形成不可控傳導。</p>
<ol>
<li>身分分離：把人類操作身份與機器執行身份拆分責任。</li>
<li>邊界定義：把 workload 可觸及資源限制在最小業務範圍。</li>
<li>聯邦信任：把跨平台 token 交換限制在可驗證來源與用途。</li>
<li>短時憑證：把憑證有效時窗縮短，降低竊取後可利用時間。</li>
<li>收斂節奏：把外部事件後的信任重評估納入固定流程。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「機器可用憑證」轉成「機器可控身份」。</p>
<ol>
<li>先盤點 workload 身份來源、簽發路徑與責任主體。</li>
<li>再檢查 token scope、TTL 與可觸及資源是否超出用途。</li>
<li>接著檢查 federation 來源與授權決策是否可回查。</li>
<li>最後把缺口交接到平台、可靠性與事件收斂流程。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>機器身份來源不清</td>
          <td>credential 缺乏發放責任鏈</td>
          <td>憑證可用窗口失控</td>
          <td><a href="/blog/backend/knowledge-cards/credential/" data-link-title="Credential" data-link-desc="整理身分驗證與系統存取用秘密資料">credential</a></td>
      </tr>
      <tr>
          <td>跨平台信任擴張過快</td>
          <td>token 使用面超出預期服務邊界</td>
          <td>外部事件可快速傳導</td>
          <td><a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary</a></td>
      </tr>
      <tr>
          <td>短時憑證策略不完整</td>
          <td>失效節奏與授權節奏分離</td>
          <td>撤銷成本上升</td>
          <td><a href="/blog/backend/knowledge-cards/token-revocation/" data-link-title="Token Revocation" data-link-desc="說明事件中如何撤銷 token，縮短可利用窗口">token-revocation</a></td>
      </tr>
      <tr>
          <td>federation 回查不足</td>
          <td>信任來源與授權決策無法回串</td>
          <td>事故判讀時間延長</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a></td>
      </tr>
  </tbody>
</table>
<h2 id="federation-信任漂移跟跨平台-token-重評估">Federation 信任漂移跟跨平台 token 重評估</h2>
<p>Federation 信任漂移是 workload identity 獨有的失效模式：信任關係建立後、token 的 <em>來源</em> 跟 <em>用途</em> 隨時間逐步脫鉤、攻擊者可在非預期服務持續使用同一個 federated token。控制責任是定期重評估信任關係的有效性、把 federation 視為長期演化中的信任配置、跟業務變動 cycle 同步。</p>
<p>對應失效樣式 <a href="../red-team/problem-cards/fp-federated-token-trust-drift/">Federated token trust drift</a>：揭露 federation 邊界失效的三個常見形成條件 — federation trust 建立後缺少定期重評估、token scope 與最小權限原則不一致、跨平台 token revocation 流程沒有同批收斂。Problem-card「判讀訊號」直接列出「同一聯邦 token 在非預期服務持續出現」「外部身分事件後高權限聯邦 token 存續比例偏高」。<a href="../red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/">Microsoft Storm-0558 2023</a> 作為背景 case 補上信任根失守時對 federation 漂移半徑的放大效應；該案例核心是簽章金鑰偽造、不是 federation drift 的代表 case、簽章金鑰治理見 <a href="../secrets-and-machine-credential-governance/#%e7%b0%bd%e7%ab%a0%e9%87%91%e9%91%b0%e8%b7%9f%e9%95%b7%e6%9c%9f%e4%bf%a1%e4%bb%bb%e6%a0%b9">7.6 canonical</a>。</p>
<p>以下基於通用工程知識補充：定期重評估的工程實作要包含 <em>使用模式 audit</em>（token 實際被用在哪些 service / 跨多少 audience）跟 <em>授權決策回查</em>（federation 端的授權邏輯是否仍對應目前的業務需求）。日常治理要把 federation 視為跟業務 cycle 共演化的長期配置 — 業務變動 trigger 重評估、避免 token scope 隨時間累積到遠超實際用途。重評估節奏綁兩個 cycle：業務變動 + 時間到期、任一觸發即啟動。</p>
<h2 id="第三方授權範圍跟事件傳導半徑">第三方授權範圍跟事件傳導半徑</h2>
<p>第三方授權的範圍直接決定供應商事件的內部傳導半徑。token scope 過寬時、供應商事件能影響的內部資源面積會超出原本授權的業務範圍；這層治理要在授權發起時就把 scope 收斂到最小必要、事件處置才能在已知範圍內快速分批收斂。</p>
<p>對應失效樣式 <a href="../red-team/problem-cards/fp-overscoped-third-party-token-grant/">Overscoped 第三方 token grant</a>：揭露 token scope 過寬的三個常見形成條件 — 第三方 token scope 與實際用途不一致、token 期限過長且回收節奏落後、供應商事件後缺少分域收斂流程。同 frame 在 <a href="../red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/">Okta + Cloudflare 2023</a> 案例落到事件實際處置面、可落地檢查點標明前提條件是「輪替能力涵蓋第三方授權 token、不只內部 session」。本章視角聚焦 workload identity 層 federation token scope；客戶側人類身分鏈收斂責任見 <a href="../identity-access-boundary/#%e7%ac%ac%e4%b8%89%e6%96%b9%e8%ba%ab%e5%88%86%e9%8f%88%e7%9a%84%e5%85%a7%e9%83%a8%e6%94%b6%e6%96%82%e8%b2%ac%e4%bb%bb">7.2 § 第三方身分鏈的內部收斂責任</a>。</p>
<p>以下基於通用工程知識補充：scope 收斂的工程瓶頸通常在第三方平台的權限粒度 — 廠商提供的 scope 選項可能比實際需求粗、組織要在「接受粗 scope」「自建中間層收斂」「換廠商」之間取捨。中間層收斂是常見折衷、把第三方 token 在內部 proxy 後降權再傳遞給下游 service。中間層存在時、第三方 scope 跟內部 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 解耦；中間層缺位時、兩者直接綁定。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是判斷何時機器身份治理需要升級處置。</p>
<ul>
<li>機器憑證來源無法對應到責任主體時，代表信任鏈不可驗證。</li>
<li>跨平台 token 在非預期服務長期可用時，代表 federation 邊界鬆動。</li>
<li>短時憑證實作退化成長時存活時，代表撤銷窗口擴大。</li>
<li>供應商事件後內部 workload 權限未收斂時，代表外部風險仍在傳導。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證機器身份模型是否能承受現實攻擊壓力。</p>
<ul>
<li>第三方身分鏈事件： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a></li>
<li>token 傳導與後續擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a></li>
<li>憑證濫用下的資料平台風險： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身份與平台邊界實作：<code>05-deployment-platform</code></li>
<li>憑證輪替與驗證節奏：<code>06-reliability</code></li>
<li>事件分級與收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.11 資料駐留、刪除與證據鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/</guid><description>&lt;p>本章的責任是把資料位置與刪除責任拆成可驗證閉環，讓資料治理在合規壓力與營運需求同時存在時仍可追蹤。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦資料位置責任、刪除流程閉環與證據保留語意，不展開法規條文逐條解釋。&lt;/p>
&lt;h2 id="資料駐留與刪除模型">資料駐留與刪除模型&lt;/h2>
&lt;p>資料駐留治理的核心責任是回答資料在哪裡，刪除治理的核心責任是證明資料已從所有可觸及路徑被收斂。&lt;/p>
&lt;ol>
&lt;li>位置責任：定義正式資料、衍生資料、備份資料的地理與服務邊界。&lt;/li>
&lt;li>刪除責任：定義請求受理、執行、驗證與回覆的責任鏈。&lt;/li>
&lt;li>一致性責任：定義主系統與衍生系統刪除節奏一致條件。&lt;/li>
&lt;li>證據責任：定義刪除證據與稽核證據的保留與可驗證性。&lt;/li>
&lt;li>通知責任：定義跨組織資料治理事件的通報與驗證時序。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「刪除完成」轉成「可驗證刪除完成」。&lt;/p>
&lt;ol>
&lt;li>先確認資料分類與駐留位置清單是否完整。&lt;/li>
&lt;li>再確認刪除是否覆蓋主路徑、衍生路徑與備份路徑。&lt;/li>
&lt;li>接著確認刪除證據是否可對應主體、時間與資產。&lt;/li>
&lt;li>最後把缺口交接到可靠性驗證與 incident 溝通流程。&lt;/li>
&lt;/ol>
&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>同一資料集跨區副本責任不清&lt;/td>
 &lt;td>通報與整改範圍擴大&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-lifecycle/" data-link-title="Data Lifecycle" data-link-desc="說明資料從建立、使用、保留到刪除的責任邊界">data-lifecycle&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪除流程缺乏閉環&lt;/td>
 &lt;td>主系統刪除完成但衍生系統仍存留&lt;/td>
 &lt;td>使用者承諾失效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>備份刪除節奏脫鉤&lt;/td>
 &lt;td>正式資料移除後備份仍長期可還原&lt;/td>
 &lt;td>長尾暴露風險升高&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object-storage&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪除證據不可驗證&lt;/td>
 &lt;td>時序與主體資訊無法回查&lt;/td>
 &lt;td>合規與事故回應成本上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是界定何時資料治理需要升級處置。&lt;/p>
&lt;ul>
&lt;li>同一資料集跨區副本沒有明確責任人時，代表駐留邊界不可控。&lt;/li>
&lt;li>主系統刪除完成但索引或快取仍可查到資料時，代表刪除閉環失效。&lt;/li>
&lt;li>備份保留策略與刪除承諾衝突時，代表長尾暴露窗口擴大。&lt;/li>
&lt;li>刪除回覆缺少可驗證證據時，代表合規與信任成本上升。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證資料位置與刪除證據是否能支撐現實事件回應。&lt;/p>
&lt;ul>
&lt;li>備份鏈與資料外送壓力： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022&lt;/a>&lt;/li>
&lt;li>憑證濫用下的大量資料外送： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>檔案服務暴露與資料治理壓力： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>資料與儲存邊界實作：&lt;code>05-deployment-platform&lt;/code>&lt;/li>
&lt;li>一致性驗證與演練：&lt;code>06-reliability&lt;/code>&lt;/li>
&lt;li>通報與事件收斂：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把資料位置與刪除責任拆成可驗證閉環，讓資料治理在合規壓力與營運需求同時存在時仍可追蹤。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦資料位置責任、刪除流程閉環與證據保留語意，不展開法規條文逐條解釋。</p>
<h2 id="資料駐留與刪除模型">資料駐留與刪除模型</h2>
<p>資料駐留治理的核心責任是回答資料在哪裡，刪除治理的核心責任是證明資料已從所有可觸及路徑被收斂。</p>
<ol>
<li>位置責任：定義正式資料、衍生資料、備份資料的地理與服務邊界。</li>
<li>刪除責任：定義請求受理、執行、驗證與回覆的責任鏈。</li>
<li>一致性責任：定義主系統與衍生系統刪除節奏一致條件。</li>
<li>證據責任：定義刪除證據與稽核證據的保留與可驗證性。</li>
<li>通知責任：定義跨組織資料治理事件的通報與驗證時序。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「刪除完成」轉成「可驗證刪除完成」。</p>
<ol>
<li>先確認資料分類與駐留位置清單是否完整。</li>
<li>再確認刪除是否覆蓋主路徑、衍生路徑與備份路徑。</li>
<li>接著確認刪除證據是否可對應主體、時間與資產。</li>
<li>最後把缺口交接到可靠性驗證與 incident 溝通流程。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料駐留邊界模糊</td>
          <td>同一資料集跨區副本責任不清</td>
          <td>通報與整改範圍擴大</td>
          <td><a href="/blog/backend/knowledge-cards/data-lifecycle/" data-link-title="Data Lifecycle" data-link-desc="說明資料從建立、使用、保留到刪除的責任邊界">data-lifecycle</a></td>
      </tr>
      <tr>
          <td>刪除流程缺乏閉環</td>
          <td>主系統刪除完成但衍生系統仍存留</td>
          <td>使用者承諾失效</td>
          <td><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a></td>
      </tr>
      <tr>
          <td>備份刪除節奏脫鉤</td>
          <td>正式資料移除後備份仍長期可還原</td>
          <td>長尾暴露風險升高</td>
          <td><a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object-storage</a></td>
      </tr>
      <tr>
          <td>刪除證據不可驗證</td>
          <td>時序與主體資訊無法回查</td>
          <td>合規與事故回應成本上升</td>
          <td><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時資料治理需要升級處置。</p>
<ul>
<li>同一資料集跨區副本沒有明確責任人時，代表駐留邊界不可控。</li>
<li>主系統刪除完成但索引或快取仍可查到資料時，代表刪除閉環失效。</li>
<li>備份保留策略與刪除承諾衝突時，代表長尾暴露窗口擴大。</li>
<li>刪除回覆缺少可驗證證據時，代表合規與信任成本上升。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證資料位置與刪除證據是否能支撐現實事件回應。</p>
<ul>
<li>備份鏈與資料外送壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a></li>
<li>憑證濫用下的大量資料外送： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li>檔案服務暴露與資料治理壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>資料與儲存邊界實作：<code>05-deployment-platform</code></li>
<li>一致性驗證與演練：<code>06-reliability</code></li>
<li>通報與事件收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.12 供應鏈完整性與 Artifact 信任</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/</guid><description>&lt;p>本章的責任是把交付鏈信任風險拆成可驗證節點，讓來源可信度、產物完整性與事件後收斂能被一致治理。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦來源可信度、組件邊界與發佈節奏治理，不討論單一 CI/CD 平台操作流程。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：來源可追溯性不足 / artifact 信任斷點 / 第三方依賴風險放大 / 事件後發佈節奏混亂。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>CI secrets → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.10&lt;/a>&lt;/li>
&lt;li>例外治理 → &lt;a href="../security-governance-exception-and-tripwire/">7.14&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[ci-pipeline]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="供應鏈信任模型">供應鏈信任模型&lt;/h2>
&lt;p>供應鏈治理的核心責任是讓每一個進入正式環境的產物都可追溯、可驗證、可回退。&lt;/p>
&lt;ol>
&lt;li>來源層：確認 build provenance 可對應到可驗證來源與責任主體。&lt;/li>
&lt;li>產物層：確認 artifact 在簽署、摘要與傳遞過程沒有完整性斷點。&lt;/li>
&lt;li>依賴層：確認第三方組件有隔離邊界與影響面地圖。&lt;/li>
&lt;li>節奏層：確認事件後可執行凍結、復原與再驗證流程。&lt;/li>
&lt;li>收斂層：確認供應鏈事件可路由到事件分級與回復驗證。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「可部署產物」轉成「可信產物」。&lt;/p>
&lt;ol>
&lt;li>先確認來源提交、build 環境與產物 metadata 是否可關聯。&lt;/li>
&lt;li>再確認產物簽署與完整性證據是否可驗證。&lt;/li>
&lt;li>接著確認依賴事件是否有快速切換與回退路徑。&lt;/li>
&lt;li>最後交接到可靠性與 incident 流程，追蹤收斂結果。&lt;/li>
&lt;/ol>
&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>build 與來源提交無法一致回查&lt;/td>
 &lt;td>發佈可信度下降&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>artifact 信任斷點&lt;/td>
 &lt;td>發佈產物缺乏簽署與完整性證據&lt;/td>
 &lt;td>受污染產物進入正式流程&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方依賴風險放大&lt;/td>
 &lt;td>同類組件事件波及多服務&lt;/td>
 &lt;td>修補與回退成本上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency-isolation&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件後發佈節奏混亂&lt;/td>
 &lt;td>凍結與恢復條件不一致&lt;/td>
 &lt;td>二次事故風險上升&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release-gate&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是界定何時供應鏈風險已進入高壓狀態。&lt;/p>
&lt;ul>
&lt;li>build 來源與產物長期無法一致回查時，代表 provenance 模型失效。&lt;/li>
&lt;li>產物沒有簽署或簽署驗證未納入發佈關卡時，代表完整性邊界不足。&lt;/li>
&lt;li>第三方事件發生後無法快速判斷受影響服務時，代表依賴隔離不足。&lt;/li>
&lt;li>事故期間凍結與恢復標準反覆變動時，代表交付節奏未收斂。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證交付鏈信任模型是否有現實抗壓能力。&lt;/p>
&lt;ul>
&lt;li>開源組件滲透與下游衝擊： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024&lt;/a>&lt;/li>
&lt;li>組件級漏洞造成大範圍傳導： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021&lt;/a>&lt;/li>
&lt;li>平台級供應鏈事件與回退壓力： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用標準">引用標準&lt;/h2>
&lt;p>供應鏈領域標準演化快、本章參考下列外部標準作為 mechanism 層 anchor。Reader 套用前 verify 版本仍是 current best practice：&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>SLSA（Supply-chain Levels for Software Artifacts）&lt;/td>
 &lt;td>v1.0 (2023)&lt;/td>
 &lt;td>build provenance 等級判讀（L1-L4）、來源可追溯模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NIST SSDF（Secure Software Development Framework）&lt;/td>
 &lt;td>SP 800-218 (2022)&lt;/td>
 &lt;td>開發流程安全控制 reference&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sigstore（cosign / Rekor / Fulcio）&lt;/td>
 &lt;td>continuous&lt;/td>
 &lt;td>artifact 簽署 / 透明度日誌 mechanism&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CycloneDX / SPDX&lt;/td>
 &lt;td>CycloneDX v1.6 / SPDX 3.0 (2024)&lt;/td>
 &lt;td>SBOM 格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OWASP Software Component Verification Standard (SCVS)&lt;/td>
 &lt;td>v1.0 (2020)&lt;/td>
 &lt;td>元件驗證控制 reference&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>引用版本與 cadence 規則見 &lt;a href="https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &amp;#43; sync owner（內部）。">security-citation-currency-and-precision&lt;/a>（每 12-24 月 re-check 主流標準是否有新版）。Last reviewed: 2026-05-01。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把交付鏈信任風險拆成可驗證節點，讓來源可信度、產物完整性與事件後收斂能被一致治理。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦來源可信度、組件邊界與發佈節奏治理，不討論單一 CI/CD 平台操作流程。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：來源可追溯性不足 / artifact 信任斷點 / 第三方依賴風險放大 / 事件後發佈節奏混亂。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>CI secrets → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a> → <a href="../workload-identity-and-federated-trust/">7.10</a></li>
<li>例外治理 → <a href="../security-governance-exception-and-tripwire/">7.14</a></li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[ci-pipeline]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="供應鏈信任模型">供應鏈信任模型</h2>
<p>供應鏈治理的核心責任是讓每一個進入正式環境的產物都可追溯、可驗證、可回退。</p>
<ol>
<li>來源層：確認 build provenance 可對應到可驗證來源與責任主體。</li>
<li>產物層：確認 artifact 在簽署、摘要與傳遞過程沒有完整性斷點。</li>
<li>依賴層：確認第三方組件有隔離邊界與影響面地圖。</li>
<li>節奏層：確認事件後可執行凍結、復原與再驗證流程。</li>
<li>收斂層：確認供應鏈事件可路由到事件分級與回復驗證。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「可部署產物」轉成「可信產物」。</p>
<ol>
<li>先確認來源提交、build 環境與產物 metadata 是否可關聯。</li>
<li>再確認產物簽署與完整性證據是否可驗證。</li>
<li>接著確認依賴事件是否有快速切換與回退路徑。</li>
<li>最後交接到可靠性與 incident 流程，追蹤收斂結果。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>來源可追溯性不足</td>
          <td>build 與來源提交無法一致回查</td>
          <td>發佈可信度下降</td>
          <td><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline</a></td>
      </tr>
      <tr>
          <td>artifact 信任斷點</td>
          <td>發佈產物缺乏簽署與完整性證據</td>
          <td>受污染產物進入正式流程</td>
          <td><a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract</a></td>
      </tr>
      <tr>
          <td>第三方依賴風險放大</td>
          <td>同類組件事件波及多服務</td>
          <td>修補與回退成本上升</td>
          <td><a href="/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency-isolation</a></td>
      </tr>
      <tr>
          <td>事件後發佈節奏混亂</td>
          <td>凍結與恢復條件不一致</td>
          <td>二次事故風險上升</td>
          <td><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release-gate</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時供應鏈風險已進入高壓狀態。</p>
<ul>
<li>build 來源與產物長期無法一致回查時，代表 provenance 模型失效。</li>
<li>產物沒有簽署或簽署驗證未納入發佈關卡時，代表完整性邊界不足。</li>
<li>第三方事件發生後無法快速判斷受影響服務時，代表依賴隔離不足。</li>
<li>事故期間凍結與恢復標準反覆變動時，代表交付節奏未收斂。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證交付鏈信任模型是否有現實抗壓能力。</p>
<ul>
<li>開源組件滲透與下游衝擊： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></li>
<li>組件級漏洞造成大範圍傳導： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021</a></li>
<li>平台級供應鏈事件與回退壓力： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a></li>
</ul>
<h2 id="引用標準">引用標準</h2>
<p>供應鏈領域標準演化快、本章參考下列外部標準作為 mechanism 層 anchor。Reader 套用前 verify 版本仍是 current best practice：</p>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLSA（Supply-chain Levels for Software Artifacts）</td>
          <td>v1.0 (2023)</td>
          <td>build provenance 等級判讀（L1-L4）、來源可追溯模型</td>
      </tr>
      <tr>
          <td>NIST SSDF（Secure Software Development Framework）</td>
          <td>SP 800-218 (2022)</td>
          <td>開發流程安全控制 reference</td>
      </tr>
      <tr>
          <td>Sigstore（cosign / Rekor / Fulcio）</td>
          <td>continuous</td>
          <td>artifact 簽署 / 透明度日誌 mechanism</td>
      </tr>
      <tr>
          <td>CycloneDX / SPDX</td>
          <td>CycloneDX v1.6 / SPDX 3.0 (2024)</td>
          <td>SBOM 格式</td>
      </tr>
      <tr>
          <td>OWASP Software Component Verification Standard (SCVS)</td>
          <td>v1.0 (2020)</td>
          <td>元件驗證控制 reference</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>（每 12-24 月 re-check 主流標準是否有新版）。Last reviewed: 2026-05-01。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>交付平台與部署治理：<code>05-deployment-platform</code></li>
<li>發佈驗證與回退演練：<code>06-reliability</code></li>
<li>分級與跨部門收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.13 偵測覆蓋率與訊號治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/</guid><description>&lt;p>本章的責任是把偵測能力轉成可決策的訊號系統，讓告警不只存在，而且能支撐分級、收斂與復盤。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦偵測覆蓋率語意、訊號品質分級與告警成本，不討論 SIEM 或監控產品配置細節。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：覆蓋率描述空泛 / 訊號品質不穩定 / 漏報風險無回饋迴路 / 事件分級與訊號脫鉤。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>各領域 specific threats → 7.2-7.12（各章領域）&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>、實作交付 → &lt;code>05&lt;/code> / &lt;code>06&lt;/code> / &lt;code>08&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 &lt;code>[alert]&lt;/code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>04-observability / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="偵測治理模型">偵測治理模型&lt;/h2>
&lt;p>偵測治理的核心責任是定義「哪些風險一定要看見、看見後要如何行動」。&lt;/p>
&lt;ol>
&lt;li>覆蓋率層：把攻擊面、關鍵流程與高風險資料路徑對應到偵測責任。&lt;/li>
&lt;li>品質層：把訊號分成可行動、待驗證、背景參考三類，避免單一噪音主導判讀。&lt;/li>
&lt;li>成本層：把誤報、漏報與疲勞成本納入日常治理，不只看告警數量。&lt;/li>
&lt;li>分級層：把偵測訊號與 incident severity 綁定，確保高風險事件有高信號來源。&lt;/li>
&lt;li>復盤層：把事件後缺口回寫到偵測策略，形成閉環改善節奏。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「觀測資料」轉成「處置動作」。&lt;/p>
&lt;ol>
&lt;li>先確認偵測對象是否對齊高風險路徑。&lt;/li>
&lt;li>再確認訊號能否支持分級與責任歸屬。&lt;/li>
&lt;li>接著確認誤報與漏報成本是否可控。&lt;/li>
&lt;li>最後把缺口交接到可靠性與 incident workflow。&lt;/li>
&lt;/ol>
&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>只定義監控存在，未定義判讀用途&lt;/td>
 &lt;td>事故期無法快速決策&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訊號品質不穩定&lt;/td>
 &lt;td>同類事件訊號噪音高、關聯性低&lt;/td>
 &lt;td>告警疲勞與延遲處置&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>漏報風險無回饋迴路&lt;/td>
 &lt;td>復盤未回寫偵測策略&lt;/td>
 &lt;td>缺口長期存留&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident-review&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件分級與訊號脫鉤&lt;/td>
 &lt;td>高嚴重度事件缺少高信號來源&lt;/td>
 &lt;td>分級品質下降&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是界定偵測能力何時已不足以支撐營運決策。&lt;/p>
&lt;ul>
&lt;li>高嚴重度事件需靠人工拼接多系統資料才能判讀時，代表訊號可用性不足。&lt;/li>
&lt;li>同類攻擊反覆發生但告警規則未演進時，代表復盤回寫機制失效。&lt;/li>
&lt;li>告警噪音長期高於值班承載能力時，代表偵測成本正在侵蝕處置品質。&lt;/li>
&lt;li>關鍵資料外送行為缺少即時訊號時，代表覆蓋率與風險路徑脫鉤。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證偵測策略是否足以應對現實攻擊節奏。&lt;/p>
&lt;ul>
&lt;li>身分異常訊號不足導致擴散： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>憑證濫用下的低噪音外送： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>邊界設備高壓窗口下的偵測需求： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="引用標準">引用標準&lt;/h2>
&lt;p>偵測領域標準演化快、本章參考下列外部標準作為 mechanism 層 anchor。Reader 套用前 verify 版本仍是 current best practice：&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>NIST SP 800-61 Computer Security Incident Handling Guide&lt;/td>
 &lt;td>Rev. 2 (2012)，Rev. 3 draft (2024)&lt;/td>
 &lt;td>偵測與事件處理流程 reference&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MITRE ATT&amp;amp;CK&lt;/td>
 &lt;td>continuous&lt;/td>
 &lt;td>攻擊技術 taxonomy / detection coverage 對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OWASP Logging Cheat Sheet&lt;/td>
 &lt;td>continuous&lt;/td>
 &lt;td>log / alert / detection 設計 reference&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sigma Rules&lt;/td>
 &lt;td>continuous&lt;/td>
 &lt;td>跨 SIEM 偵測規則 portable 格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ENISA Detection Engineering Guide&lt;/td>
 &lt;td>2023&lt;/td>
 &lt;td>detection 成熟度與訊號品質 reference（歐盟脈絡）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>引用版本與 cadence 規則見 &lt;a href="https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &amp;#43; sync owner（內部）。">security-citation-currency-and-precision&lt;/a>（每 12-24 月 re-check）。Last reviewed: 2026-05-01。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把偵測能力轉成可決策的訊號系統，讓告警不只存在，而且能支撐分級、收斂與復盤。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦偵測覆蓋率語意、訊號品質分級與告警成本，不討論 SIEM 或監控產品配置細節。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：覆蓋率描述空泛 / 訊號品質不穩定 / 漏報風險無回饋迴路 / 事件分級與訊號脫鉤。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>各領域 specific threats → 7.2-7.12（各章領域）</li>
<li>偵測平台 → <code>04-observability</code>、實作交付 → <code>05</code> / <code>06</code> / <code>08</code></li>
</ul>
<p>Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點；out-of-scope 議題請直接跳到對應章節、不在本章 audit 範圍。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 <code>[alert]</code> 等 control link 進 knowledge-card、看具體機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>04-observability / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="偵測治理模型">偵測治理模型</h2>
<p>偵測治理的核心責任是定義「哪些風險一定要看見、看見後要如何行動」。</p>
<ol>
<li>覆蓋率層：把攻擊面、關鍵流程與高風險資料路徑對應到偵測責任。</li>
<li>品質層：把訊號分成可行動、待驗證、背景參考三類，避免單一噪音主導判讀。</li>
<li>成本層：把誤報、漏報與疲勞成本納入日常治理，不只看告警數量。</li>
<li>分級層：把偵測訊號與 incident severity 綁定，確保高風險事件有高信號來源。</li>
<li>復盤層：把事件後缺口回寫到偵測策略，形成閉環改善節奏。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「觀測資料」轉成「處置動作」。</p>
<ol>
<li>先確認偵測對象是否對齊高風險路徑。</li>
<li>再確認訊號能否支持分級與責任歸屬。</li>
<li>接著確認誤報與漏報成本是否可控。</li>
<li>最後把缺口交接到可靠性與 incident workflow。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>覆蓋率描述空泛</td>
          <td>只定義監控存在，未定義判讀用途</td>
          <td>事故期無法快速決策</td>
          <td><a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a></td>
      </tr>
      <tr>
          <td>訊號品質不穩定</td>
          <td>同類事件訊號噪音高、關聯性低</td>
          <td>告警疲勞與延遲處置</td>
          <td><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert</a></td>
      </tr>
      <tr>
          <td>漏報風險無回饋迴路</td>
          <td>復盤未回寫偵測策略</td>
          <td>缺口長期存留</td>
          <td><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident-review</a></td>
      </tr>
      <tr>
          <td>事件分級與訊號脫鉤</td>
          <td>高嚴重度事件缺少高信號來源</td>
          <td>分級品質下降</td>
          <td><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定偵測能力何時已不足以支撐營運決策。</p>
<ul>
<li>高嚴重度事件需靠人工拼接多系統資料才能判讀時，代表訊號可用性不足。</li>
<li>同類攻擊反覆發生但告警規則未演進時，代表復盤回寫機制失效。</li>
<li>告警噪音長期高於值班承載能力時，代表偵測成本正在侵蝕處置品質。</li>
<li>關鍵資料外送行為缺少即時訊號時，代表覆蓋率與風險路徑脫鉤。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證偵測策略是否足以應對現實攻擊節奏。</p>
<ul>
<li>身分異常訊號不足導致擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>憑證濫用下的低噪音外送： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li>邊界設備高壓窗口下的偵測需求： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></li>
</ul>
<h2 id="引用標準">引用標準</h2>
<p>偵測領域標準演化快、本章參考下列外部標準作為 mechanism 層 anchor。Reader 套用前 verify 版本仍是 current best practice：</p>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NIST SP 800-61 Computer Security Incident Handling Guide</td>
          <td>Rev. 2 (2012)，Rev. 3 draft (2024)</td>
          <td>偵測與事件處理流程 reference</td>
      </tr>
      <tr>
          <td>MITRE ATT&amp;CK</td>
          <td>continuous</td>
          <td>攻擊技術 taxonomy / detection coverage 對照</td>
      </tr>
      <tr>
          <td>OWASP Logging Cheat Sheet</td>
          <td>continuous</td>
          <td>log / alert / detection 設計 reference</td>
      </tr>
      <tr>
          <td>Sigma Rules</td>
          <td>continuous</td>
          <td>跨 SIEM 偵測規則 portable 格式</td>
      </tr>
      <tr>
          <td>ENISA Detection Engineering Guide</td>
          <td>2023</td>
          <td>detection 成熟度與訊號品質 reference（歐盟脈絡）</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>（每 12-24 月 re-check）。Last reviewed: 2026-05-01。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>觀測資料與平台能力：<code>04-observability</code></li>
<li>驗證與演練節奏：<code>06-reliability</code></li>
<li>分級與事件收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.14 資安治理例外與 Tripwire</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/</guid><description>&lt;p>本章的責任是定義治理例外與重新評估問題節點，讓風險接受決策具備期限、條件與回收機制。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦決策治理與重新評估節奏，不討論單一審批系統流程細節。&lt;/p>
&lt;h2 id="例外治理模型">例外治理模型&lt;/h2>
&lt;p>例外治理的核心責任是把暫時接受風險的決策，限制在可追蹤、可回收、可重評估的範圍內。&lt;/p>
&lt;ol>
&lt;li>決策責任：記錄例外目的、邊界、批准者與受影響資產。&lt;/li>
&lt;li>期限責任：設定明確到期日與重評估條件。&lt;/li>
&lt;li>補償責任：例外期間加上額外監測、限制或人工檢查。&lt;/li>
&lt;li>觸發責任：定義 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire&lt;/a>，一旦觸發立即重審例外。&lt;/li>
&lt;li>關閉責任：例外結束後回寫知識與控制面改進。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「例外同意」轉成「例外可控」。&lt;/p>
&lt;ol>
&lt;li>先確認例外是否有清楚邊界與風險描述。&lt;/li>
&lt;li>再確認是否有到期日與量化關閉條件。&lt;/li>
&lt;li>接著確認補償控制面是否足以降低暴露窗口。&lt;/li>
&lt;li>最後確認 tripwire 與重評估流程是否可執行。&lt;/li>
&lt;/ol>
&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>只記錄同意結果，未記錄邊界條件&lt;/td>
 &lt;td>例外範圍持續擴張&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>風險接受缺乏期限&lt;/td>
 &lt;td>例外長期存續且無重評估節點&lt;/td>
 &lt;td>長期暴露風險累積&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補償控制面不足&lt;/td>
 &lt;td>例外期間缺少額外監測與限制&lt;/td>
 &lt;td>事件發生時缺乏止血槓桿&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>tripwire 未定義&lt;/td>
 &lt;td>重大變化出現時無自動重審機制&lt;/td>
 &lt;td>決策過期仍持續生效&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;p>風險邊界的責任是判斷例外決策何時已不可接受。&lt;/p>
&lt;ul>
&lt;li>例外文件只有結論沒有條件時，代表例外邊界不可驗證。&lt;/li>
&lt;li>例外到期後仍自動延長時，代表治理節奏失控。&lt;/li>
&lt;li>例外期間缺少補償監測時，代表事件來臨時無法提早止血。&lt;/li>
&lt;li>關鍵風險指標變化卻未觸發重審時，代表 tripwire 機制失效。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證例外治理是否能承受高壓情境。&lt;/p>
&lt;ul>
&lt;li>修補窗口中的暫時風險接受： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024&lt;/a>&lt;/li>
&lt;li>供應鏈事件中的凍結與恢復判準： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024&lt;/a>&lt;/li>
&lt;li>身分事件中的收斂與決策期限： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>平台控制面補償設計：&lt;code>05-deployment-platform&lt;/code>&lt;/li>
&lt;li>驗證與回退節奏：&lt;code>06-reliability&lt;/code>&lt;/li>
&lt;li>分級、通報與重評估：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是定義治理例外與重新評估問題節點，讓風險接受決策具備期限、條件與回收機制。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦決策治理與重新評估節奏，不討論單一審批系統流程細節。</p>
<h2 id="例外治理模型">例外治理模型</h2>
<p>例外治理的核心責任是把暫時接受風險的決策，限制在可追蹤、可回收、可重評估的範圍內。</p>
<ol>
<li>決策責任：記錄例外目的、邊界、批准者與受影響資產。</li>
<li>期限責任：設定明確到期日與重評估條件。</li>
<li>補償責任：例外期間加上額外監測、限制或人工檢查。</li>
<li>觸發責任：定義 <a href="/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire</a>，一旦觸發立即重審例外。</li>
<li>關閉責任：例外結束後回寫知識與控制面改進。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「例外同意」轉成「例外可控」。</p>
<ol>
<li>先確認例外是否有清楚邊界與風險描述。</li>
<li>再確認是否有到期日與量化關閉條件。</li>
<li>接著確認補償控制面是否足以降低暴露窗口。</li>
<li>最後確認 tripwire 與重評估流程是否可執行。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>例外條件描述不足</td>
          <td>只記錄同意結果，未記錄邊界條件</td>
          <td>例外範圍持續擴張</td>
          <td><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a></td>
      </tr>
      <tr>
          <td>風險接受缺乏期限</td>
          <td>例外長期存續且無重評估節點</td>
          <td>長期暴露風險累積</td>
          <td><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident-timeline</a></td>
      </tr>
      <tr>
          <td>補償控制面不足</td>
          <td>例外期間缺少額外監測與限制</td>
          <td>事件發生時缺乏止血槓桿</td>
          <td><a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a></td>
      </tr>
      <tr>
          <td>tripwire 未定義</td>
          <td>重大變化出現時無自動重審機制</td>
          <td>決策過期仍持續生效</td>
          <td><a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident-severity</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是判斷例外決策何時已不可接受。</p>
<ul>
<li>例外文件只有結論沒有條件時，代表例外邊界不可驗證。</li>
<li>例外到期後仍自動延長時，代表治理節奏失控。</li>
<li>例外期間缺少補償監測時，代表事件來臨時無法提早止血。</li>
<li>關鍵風險指標變化卻未觸發重審時，代表 tripwire 機制失效。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證例外治理是否能承受高壓情境。</p>
<ul>
<li>修補窗口中的暫時風險接受： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></li>
<li>供應鏈事件中的凍結與恢復判準： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></li>
<li>身分事件中的收斂與決策期限： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平台控制面補償設計：<code>05-deployment-platform</code></li>
<li>驗證與回退節奏：<code>06-reliability</code></li>
<li>分級、通報與重評估：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>模組七案例正文</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/</guid><description>&lt;p>這個資料夾的核心責任是把資安與控制平面事故轉成可回寫治理控制的案例正文。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">7.C1&lt;/a>&lt;/td>
 &lt;td>Cloudflare 2026 Route Leak&lt;/td>
 &lt;td>路由策略自動化失誤如何回寫治理與 tripwire&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2&lt;/a>&lt;/td>
 &lt;td>Cloudflare 2023 Token 事件&lt;/td>
 &lt;td>控制面 token 風險如何轉成機器憑證治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">7.C3&lt;/a>&lt;/td>
 &lt;td>Azure AD 2021 控制面事件&lt;/td>
 &lt;td>身分控制面事故如何影響多服務信任鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">7.C4&lt;/a>&lt;/td>
 &lt;td>Microsoft Storm-0558&lt;/td>
 &lt;td>簽章金鑰事件如何回寫 identity 信任邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">7.C5&lt;/a>&lt;/td>
 &lt;td>Okta support 系統事件&lt;/td>
 &lt;td>支援系統憑證風險如何擴散到客戶租戶&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">7.C6&lt;/a>&lt;/td>
 &lt;td>Okta cross-tenant 事件&lt;/td>
 &lt;td>跨租戶 impersonation 如何回寫防禦與偵測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/" data-link-title="7.C7 Okta：BYO Telephony 的身份安全責任轉換" data-link-desc="MFA 簡訊/語音路徑從平台托管轉向客戶自管的治理案例。">7.C7&lt;/a>&lt;/td>
 &lt;td>Okta BYO Telephony&lt;/td>
 &lt;td>MFA 供應鏈責任如何轉為客戶可控治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">7.C9&lt;/a>&lt;/td>
 &lt;td>反例：憑證輪替失敗&lt;/td>
 &lt;td>憑證輪替未分 scope 導致跨系統連鎖中斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/contrast-identity-governance-by-scale/" data-link-title="7.C10 對照：規模差異下的身份治理" data-link-desc="identity 控制面治理在不同規模服務下的失敗邊界差異。">7.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下身份治理&lt;/td>
 &lt;td>不同規模服務在 identity 控制面的風險差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &amp;#43; Access 兩種存取模型的選型判讀。">7.C11&lt;/a>&lt;/td>
 &lt;td>選型：單人遠端 Shell&lt;/td>
 &lt;td>單人遠端 Shell 情境下的 tunnel 選型判讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把資安與控制平面事故轉成可回寫治理控制的案例正文。</p>
<h2 id="案例列表">案例列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">7.C1</a></td>
          <td>Cloudflare 2026 Route Leak</td>
          <td>路由策略自動化失誤如何回寫治理與 tripwire</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2</a></td>
          <td>Cloudflare 2023 Token 事件</td>
          <td>控制面 token 風險如何轉成機器憑證治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">7.C3</a></td>
          <td>Azure AD 2021 控制面事件</td>
          <td>身分控制面事故如何影響多服務信任鏈</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">7.C4</a></td>
          <td>Microsoft Storm-0558</td>
          <td>簽章金鑰事件如何回寫 identity 信任邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">7.C5</a></td>
          <td>Okta support 系統事件</td>
          <td>支援系統憑證風險如何擴散到客戶租戶</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">7.C6</a></td>
          <td>Okta cross-tenant 事件</td>
          <td>跨租戶 impersonation 如何回寫防禦與偵測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-byo-telephony-security-shift/" data-link-title="7.C7 Okta：BYO Telephony 的身份安全責任轉換" data-link-desc="MFA 簡訊/語音路徑從平台托管轉向客戶自管的治理案例。">7.C7</a></td>
          <td>Okta BYO Telephony</td>
          <td>MFA 供應鏈責任如何轉為客戶可控治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">7.C9</a></td>
          <td>反例：憑證輪替失敗</td>
          <td>憑證輪替未分 scope 導致跨系統連鎖中斷</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/contrast-identity-governance-by-scale/" data-link-title="7.C10 對照：規模差異下的身份治理" data-link-desc="identity 控制面治理在不同規模服務下的失敗邊界差異。">7.C10</a></td>
          <td>對照：規模差異下身份治理</td>
          <td>不同規模服務在 identity 控制面的風險差異</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &#43; Access 兩種存取模型的選型判讀。">7.C11</a></td>
          <td>選型：單人遠端 Shell</td>
          <td>單人遠端 Shell 情境下的 tunnel 選型判讀</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>8.8 事故報告轉 workflow：從案例到日常流程</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/</guid><description>&lt;p>這一章的核心原則是把事故報告轉成可重複執行流程。每份報告都需要落地為 runbook、告警規則、演練腳本，並可回查到對應 red-team 案例。&lt;/p>
&lt;h2 id="轉換流程">轉換流程&lt;/h2>
&lt;ol>
&lt;li>事件切片：把事故拆成入口、擴散、外送、回復四段。&lt;/li>
&lt;li>控制面對應：每段映射到身份、邊界、資料、可觀測性控制面。&lt;/li>
&lt;li>失效步驟定位：明確指出缺少或延遲的流程步驟。&lt;/li>
&lt;li>動作落地：把缺口寫成 runbook、告警與演練任務。&lt;/li>
&lt;li>驗證關閉：用桌上推演與實際演練驗證關閉結果。&lt;/li>
&lt;/ol>
&lt;h2 id="常見輸出物">常見輸出物&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>：定義觸發條件、決策邊界與停止條件。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a>：建立跨團隊共用時間軸。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>：保留可追蹤 action items。&lt;/li>
&lt;li>量測指標：例如 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>、告警到升級時間、回復耗時。&lt;/li>
&lt;/ul>
&lt;h2 id="從案例到-workflow">從案例到 workflow&lt;/h2>
&lt;p>案例入口在 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7 事故案例庫（可引用）&lt;/a>。&lt;/p>
&lt;ol>
&lt;li>先在服務章節選同類型案例。&lt;/li>
&lt;li>引用案例中的「如果 workflow 少一步會發生什麼」。&lt;/li>
&lt;li>把該步驟落地為 runbook 與演練任務。&lt;/li>
&lt;/ol>
&lt;h2 id="從-workflow-回查案例">從 workflow 回查案例&lt;/h2>
&lt;p>workflow 設計完成後要反向驗證案例覆蓋是否充足。引用地圖在 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&amp;gt; 案例 -&amp;gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖&lt;/a>。&lt;/p>
&lt;ul>
&lt;li>身分或授權步驟：回查 &lt;code>identity-access&lt;/code> 案例。&lt;/li>
&lt;li>供應鏈或 CI/CD 步驟：回查 &lt;code>supply-chain&lt;/code> 案例。&lt;/li>
&lt;li>邊界設備或外網入口步驟：回查 &lt;code>edge-exposure&lt;/code> 案例。&lt;/li>
&lt;li>外送與回復步驟：回查 &lt;code>data-exfiltration&lt;/code> 案例。&lt;/li>
&lt;/ul>
&lt;h2 id="範例邊界漏洞案例轉-workflow">範例：邊界漏洞案例轉 workflow&lt;/h2>
&lt;ul>
&lt;li>觸發：外部公告高風險邊界漏洞。&lt;/li>
&lt;li>立即動作：入口隔離與臨時緩解。&lt;/li>
&lt;li>後續動作：分區修補、憑證輪替、狀態驗證。&lt;/li>
&lt;li>驗證：48 小時內完成抽樣復測與事件回顧。&lt;/li>
&lt;/ul>
&lt;p>這組流程可直接套用到 VPN、WAF、API Gateway 與對外管理介面。&lt;/p></description><content:encoded><![CDATA[<p>這一章的核心原則是把事故報告轉成可重複執行流程。每份報告都需要落地為 runbook、告警規則、演練腳本，並可回查到對應 red-team 案例。</p>
<h2 id="轉換流程">轉換流程</h2>
<ol>
<li>事件切片：把事故拆成入口、擴散、外送、回復四段。</li>
<li>控制面對應：每段映射到身份、邊界、資料、可觀測性控制面。</li>
<li>失效步驟定位：明確指出缺少或延遲的流程步驟。</li>
<li>動作落地：把缺口寫成 runbook、告警與演練任務。</li>
<li>驗證關閉：用桌上推演與實際演練驗證關閉結果。</li>
</ol>
<h2 id="常見輸出物">常見輸出物</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>：定義觸發條件、決策邊界與停止條件。</li>
<li><a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a>：建立跨團隊共用時間軸。</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>：保留可追蹤 action items。</li>
<li>量測指標：例如 <a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a>、告警到升級時間、回復耗時。</li>
</ul>
<h2 id="從案例到-workflow">從案例到 workflow</h2>
<p>案例入口在 <a href="/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7 事故案例庫（可引用）</a>。</p>
<ol>
<li>先在服務章節選同類型案例。</li>
<li>引用案例中的「如果 workflow 少一步會發生什麼」。</li>
<li>把該步驟落地為 runbook 與演練任務。</li>
</ol>
<h2 id="從-workflow-回查案例">從 workflow 回查案例</h2>
<p>workflow 設計完成後要反向驗證案例覆蓋是否充足。引用地圖在 <a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖</a>。</p>
<ul>
<li>身分或授權步驟：回查 <code>identity-access</code> 案例。</li>
<li>供應鏈或 CI/CD 步驟：回查 <code>supply-chain</code> 案例。</li>
<li>邊界設備或外網入口步驟：回查 <code>edge-exposure</code> 案例。</li>
<li>外送與回復步驟：回查 <code>data-exfiltration</code> 案例。</li>
</ul>
<h2 id="範例邊界漏洞案例轉-workflow">範例：邊界漏洞案例轉 workflow</h2>
<ul>
<li>觸發：外部公告高風險邊界漏洞。</li>
<li>立即動作：入口隔離與臨時緩解。</li>
<li>後續動作：分區修補、憑證輪替、狀態驗證。</li>
<li>驗證：48 小時內完成抽樣復測與事件回顧。</li>
</ul>
<p>這組流程可直接套用到 VPN、WAF、API Gateway 與對外管理介面。</p>
]]></content:encoded></item><item><title>效能與容量工具清單</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/</guid><description>&lt;p>效能與容量工具清單的核心責任是把工具名稱放回 workload model、saturation discovery、capacity planning 與 production validation 的服務責任。工具頁先回答它降低哪一種風險，再討論 scenario scripting、distributed load、結果保存、CI 整合、成本與案例回寫。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>效能工具要從問題節點進入。團隊如果缺 workload model，先讀 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a>；如果缺 saturation 邊界，先讀 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a>；如果缺 production 驗證，先讀 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a>。&lt;/p>
&lt;p>工具頁的任務是承接這些問題節點。k6、JMeter、Gatling、Locust 與 Vegeta 都能產生負載，但它們在腳本語言、protocol 覆蓋、分散式執行、CI integration、報表與團隊學習成本上不同；production replay、profiling 與 cost analysis 工具則承擔不同的證據責任。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>效能與容量工具頁的教學順序是先建立 load test，再進入 replay / mirroring、profiling、optimization 與 FinOps。這個順序對齊 checkout E7：讀者先理解 workload model、saturation evidence 與 capacity gate，再比較 production traffic evidence、profile evidence、rightsizing 建議與成本 owner 如何形成改善閉環。&lt;/p>
&lt;h2 id="t1-工具頁">T1 工具頁&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>用 scriptable scenario 建立 API / protocol 負載&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>用 GUI、plugin 與多 protocol sampler 承接企業測試資產&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>用 JVM DSL 與 injection profile 表達複雜 scenario&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>用 Python user behavior 與 distributed worker 表達高自訂負載&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vegeta/" data-link-title="Vegeta" data-link-desc="用簡潔 CLI 與固定 rate HTTP attack 快速探測 latency、throughput 與 saturation 的效能工程工具">Vegeta&lt;/a>&lt;/td>
 &lt;td>HTTP probe&lt;/td>
 &lt;td>用固定 rate HTTP attack 快速探測 endpoint saturation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay&lt;/a>&lt;/td>
 &lt;td>Traffic replay&lt;/td>
 &lt;td>捕捉 production HTTP traffic 並重播到 shadow target&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring&lt;/a>&lt;/td>
 &lt;td>Traffic mirror&lt;/td>
 &lt;td>用 proxy route policy mirror production traffic&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring&lt;/a>&lt;/td>
 &lt;td>Traffic mirror&lt;/td>
 &lt;td>用 VPC 網路層封包鏡像建立低侵入 production evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler&lt;/a>&lt;/td>
 &lt;td>Profiling&lt;/td>
 &lt;td>用 SaaS APM 整合與 deploy marker 支援 profile diff&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope&lt;/a>&lt;/td>
 &lt;td>Profiling&lt;/td>
 &lt;td>用 Grafana / OSS profiling backend 建立可自管 profile diff&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca&lt;/a>&lt;/td>
 &lt;td>Profiling&lt;/td>
 &lt;td>用 eBPF 與平台視角建立 infrastructure-wide profile evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/akamas/" data-link-title="Akamas" data-link-desc="用 AI-driven optimization 把效能、可靠性與雲端成本放進同一個容量調校閉環">Akamas&lt;/a>&lt;/td>
 &lt;td>Optimization&lt;/td>
 &lt;td>用 SLO constraint 與配置實驗建立 capacity / cost 調校閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage&lt;/a>&lt;/td>
 &lt;td>FinOps&lt;/td>
 &lt;td>用 cost reports、Kubernetes cost 與 forecast 建立成本可見性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth&lt;/a>&lt;/td>
 &lt;td>FinOps&lt;/td>
 &lt;td>用 enterprise governance、policy 與 allocation 管理雲端成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer&lt;/a>&lt;/td>
 &lt;td>AWS FinOps&lt;/td>
 &lt;td>用 AWS-native cost / usage report 建立成本分析 baseline&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這批工具頁已完成 load test、production traffic replay、continuous profiling 與 capacity / cost analysis 的主要分流。k6 承接 scriptable scenario，JMeter 承接企業測試資產，Gatling 承接 JVM simulation，Locust 承接 Python custom behavior，Vegeta 承接快速 HTTP probe；GoReplay、Service Mesh Mirroring 與 AWS VPC Traffic Mirroring 承接不同層級的 production traffic evidence；Datadog Continuous Profiler、Pyroscope 與 Parca 承接不同操作模型的 profile evidence；Akamas、Vantage、CloudHealth 與 AWS Cost Explorer 承接 cost visibility、optimization 與 FinOps governance。&lt;/p></description><content:encoded><![CDATA[<p>效能與容量工具清單的核心責任是把工具名稱放回 workload model、saturation discovery、capacity planning 與 production validation 的服務責任。工具頁先回答它降低哪一種風險，再討論 scenario scripting、distributed load、結果保存、CI 整合、成本與案例回寫。</p>
<h2 id="讀法">讀法</h2>
<p>效能工具要從問題節點進入。團隊如果缺 workload model，先讀 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a>；如果缺 saturation 邊界，先讀 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>；如果缺 production 驗證，先讀 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a>。</p>
<p>工具頁的任務是承接這些問題節點。k6、JMeter、Gatling、Locust 與 Vegeta 都能產生負載，但它們在腳本語言、protocol 覆蓋、分散式執行、CI integration、報表與團隊學習成本上不同；production replay、profiling 與 cost analysis 工具則承擔不同的證據責任。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>效能與容量工具頁的教學順序是先建立 load test，再進入 replay / mirroring、profiling、optimization 與 FinOps。這個順序對齊 checkout E7：讀者先理解 workload model、saturation evidence 與 capacity gate，再比較 production traffic evidence、profile evidence、rightsizing 建議與成本 owner 如何形成改善閉環。</p>
<h2 id="t1-工具頁">T1 工具頁</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>類型</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a></td>
          <td>Load test</td>
          <td>用 scriptable scenario 建立 API / protocol 負載</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a></td>
          <td>Load test</td>
          <td>用 GUI、plugin 與多 protocol sampler 承接企業測試資產</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a></td>
          <td>Load test</td>
          <td>用 JVM DSL 與 injection profile 表達複雜 scenario</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></td>
          <td>Load test</td>
          <td>用 Python user behavior 與 distributed worker 表達高自訂負載</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/vegeta/" data-link-title="Vegeta" data-link-desc="用簡潔 CLI 與固定 rate HTTP attack 快速探測 latency、throughput 與 saturation 的效能工程工具">Vegeta</a></td>
          <td>HTTP probe</td>
          <td>用固定 rate HTTP attack 快速探測 endpoint saturation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/goreplay/" data-link-title="GoReplay" data-link-desc="用 production HTTP traffic capture 與 replay 驗證真實請求形狀的效能工程工具">GoReplay</a></td>
          <td>Traffic replay</td>
          <td>捕捉 production HTTP traffic 並重播到 shadow target</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/service-mesh-mirroring/" data-link-title="Service Mesh Mirroring" data-link-desc="用 sidecar / proxy 層 mirror production traffic 到新版本或 shadow service 的 production validation 方式">Service Mesh Mirroring</a></td>
          <td>Traffic mirror</td>
          <td>用 proxy route policy mirror production traffic</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/aws-vpc-traffic-mirroring/" data-link-title="AWS VPC Traffic Mirroring" data-link-desc="用 VPC 網路層封包鏡像觀察 production traffic 的低侵入 production validation 方式">AWS VPC Traffic Mirroring</a></td>
          <td>Traffic mirror</td>
          <td>用 VPC 網路層封包鏡像建立低侵入 production evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a></td>
          <td>Profiling</td>
          <td>用 SaaS APM 整合與 deploy marker 支援 profile diff</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a></td>
          <td>Profiling</td>
          <td>用 Grafana / OSS profiling backend 建立可自管 profile diff</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a></td>
          <td>Profiling</td>
          <td>用 eBPF 與平台視角建立 infrastructure-wide profile evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/akamas/" data-link-title="Akamas" data-link-desc="用 AI-driven optimization 把效能、可靠性與雲端成本放進同一個容量調校閉環">Akamas</a></td>
          <td>Optimization</td>
          <td>用 SLO constraint 與配置實驗建立 capacity / cost 調校閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/vantage/" data-link-title="Vantage" data-link-desc="用 cloud cost reports、Kubernetes cost allocation 與 forecast 建立工程可用的成本可見性">Vantage</a></td>
          <td>FinOps</td>
          <td>用 cost reports、Kubernetes cost 與 forecast 建立成本可見性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/cloudhealth/" data-link-title="CloudHealth" data-link-desc="用 enterprise FinOps governance、policy 與多雲成本管理支援大型組織的容量成本治理">CloudHealth</a></td>
          <td>FinOps</td>
          <td>用 enterprise governance、policy 與 allocation 管理雲端成本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/vendors/aws-cost-explorer/" data-link-title="AWS Cost Explorer" data-link-desc="用 AWS-native 成本與用量分析建立 account、service、tag 與 usage type 的成本判讀入口">AWS Cost Explorer</a></td>
          <td>AWS FinOps</td>
          <td>用 AWS-native cost / usage report 建立成本分析 baseline</td>
      </tr>
  </tbody>
</table>
<p>這批工具頁已完成 load test、production traffic replay、continuous profiling 與 capacity / cost analysis 的主要分流。k6 承接 scriptable scenario，JMeter 承接企業測試資產，Gatling 承接 JVM simulation，Locust 承接 Python custom behavior，Vegeta 承接快速 HTTP probe；GoReplay、Service Mesh Mirroring 與 AWS VPC Traffic Mirroring 承接不同層級的 production traffic evidence；Datadog Continuous Profiler、Pyroscope 與 Parca 承接不同操作模型的 profile evidence；Akamas、Vantage、CloudHealth 與 AWS Cost Explorer 承接 cost visibility、optimization 與 FinOps governance。</p>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個工具頁下會擴充兩類文章：deep article（工具自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨工具遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「← X」代表從 X 遷入。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="k6/">k6</a></td>
          <td>—</td>
          <td><a href="k6/migrate-from-jmeter/">← JMeter (Type E)</a></td>
      </tr>
      <tr>
          <td><a href="datadog-continuous-profiler/">Datadog Continuous Profiler</a></td>
          <td>—</td>
          <td><a href="datadog-continuous-profiler/migrate-from-pyroscope/">← Pyroscope (Type C)</a></td>
      </tr>
  </tbody>
</table>
<p>其他 T1 工具（JMeter / Gatling / Locust / Vegeta / GoReplay / Service Mesh Mirroring / AWS VPC Traffic Mirroring / Pyroscope / Parca / Akamas / Vantage / CloudHealth / AWS Cost Explorer）尚未開始。跟 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">06 vendors</a> 共用部分工具（k6 / JMeter / Gatling / Locust），未來寫 deep article 時需明確區分「驗證流程的工具鏈」（06）跟「效能工程的工具鏈」（09）的角度。對應的 backlog 議題見上方「T1 工具頁」段每個工具頁要回答的核心責任、跟各工具 <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選工具</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Load test</td>
          <td>Artillery、wrk、hey、Grafana k6 Cloud、AWS Distributed Load Testing、BlazeMeter、LoadRunner</td>
          <td>managed runner、跨 region、報表與費用</td>
      </tr>
      <tr>
          <td>Production traffic replay</td>
          <td>shadow traffic pattern、Diffy 類 response diff、proxy mirror variants</td>
          <td>response diff、資料遮罩、side effect 邊界</td>
      </tr>
      <tr>
          <td>Profiling</td>
          <td>GCP Cloud Profiler、AWS CodeGuru Profiler、Azure Application Insights Profiler、New Relic Profiler、Dynatrace Profiling</td>
          <td>雲端整合、採樣成本、profile diff</td>
      </tr>
      <tr>
          <td>Capacity / cost analysis</td>
          <td>Kubecost / OpenCost、CloudZero、CAST AI、Infracost、Harness Cloud Cost Management</td>
          <td>workload-level 成本、rightsizing、IaC cost</td>
      </tr>
      <tr>
          <td>Benchmark / workload model</td>
          <td>YCSB、JMH、pgbench、sysbench</td>
          <td>component benchmark、DB workload、micro vs system boundary</td>
      </tr>
  </tbody>
</table>
<p>Load test 工具頁要保留 workload model 語言。JMeter 適合 protocol 覆蓋與 GUI 驅動團隊，Gatling 適合程式化 scenario 與 JVM 生態，Locust 適合 Python 團隊，Vegeta 適合簡單 HTTP 壓測與 CLI workflow。</p>
<p>Production replay 工具頁要保留安全與副作用邊界。Replay production traffic 會碰到 PII、credential、payment callback、idempotency 與下游配額，因此文章要先定義遮罩、隔離、rate limit 與 stop condition。</p>
<p>Profiling 工具頁要保留長期成本。Continuous profiling 能降低退化定位時間，但會增加採樣成本、儲存成本、敏感資訊治理、symbolization 與 baseline 維護責任。</p>
<p>Capacity / cost analysis 工具頁要保留 owner 與行動閉環。成本報表只有在 tag、label、cost center、service owner、release marker 與 action workflow 對齊後，才會變成容量規劃與成本改善的工程證據。</p>
<p>主流覆蓋檢查的重點是分開 scenario load、quick probe、managed runner、traffic replay、profiling、FinOps 與 component benchmark。k6 / Gatling / Locust 解 scenario；Vegeta / wrk / hey 解 quick HTTP probe；Grafana k6 Cloud / AWS Distributed Load Testing / BlazeMeter 解 managed runner；Pyroscope / Parca / Datadog / cloud profiler 解 profiling；Kubecost / CloudZero / CAST AI 解 workload cost。</p>
<h2 id="工具頁標準章節">工具頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>效能與容量工具頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工具定位</td>
          <td>它是 load test、replay、traffic mirror、profiler、optimizer 還是 FinOps 工具</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷它降低容量未知、production gap、瓶頸定位或成本歸因哪種風險</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「缺 workload、缺 saturation、缺 production evidence、缺 cost owner」快速定位</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>scenario、runner、threshold、sampling、dashboard、recommendation、owner</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>同類工具與相鄰工具的機會成本，例如 k6 vs JMeter、Vantage vs Cost Explorer</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>distributed runner、shadow traffic、continuous profiling、optimization guardrail</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>runner bottleneck、side effect、sampling bias、tag gap、forecast drift</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>驗證流程回 06、觀測資料回 04、部署控制回 05、事故處理回 08</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整工具 CLI 教學、供應商 pricing 細節、所有 dashboard 設定</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 09 cases、6.13 regression gate、4.20 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>本模組 15 個 vendor 跨 5 個 sub-category（load test / production replay / continuous profiling / optimization / FinOps）、解不同效能與容量工程問題、不是同類選一。</p>
<table>
  <thead>
      <tr>
          <th>Sub-category</th>
          <th>典型 vendor</th>
          <th>輸出證據</th>
          <th>Production 風險</th>
          <th>操作成本</th>
          <th>Owner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Load test</td>
          <td>k6 / JMeter / Gatling / Locust / Vegeta</td>
          <td>threshold pass/fail / p95 p99 / throughput</td>
          <td>低（測試環境）</td>
          <td>scenario 維護 / runner 規模 / 測試資料</td>
          <td>Engineering / QA</td>
      </tr>
      <tr>
          <td>Production replay</td>
          <td>GoReplay / Service Mesh Mirroring / AWS VPC</td>
          <td>response diff / shadow load</td>
          <td>高（PII / side effect / 配額）</td>
          <td>masking / isolation / rate limit</td>
          <td>SRE + Security</td>
      </tr>
      <tr>
          <td>Continuous profiling</td>
          <td>Datadog Profiler / Pyroscope / Parca</td>
          <td>flame graph diff / regression detection</td>
          <td>中（採樣 overhead）</td>
          <td>symbolization / storage / baseline 維護</td>
          <td>Engineering</td>
      </tr>
      <tr>
          <td>Optimization</td>
          <td>Akamas</td>
          <td>recommendation / SLO-constrained config</td>
          <td>中（autopilot rollout）</td>
          <td>objective model / approval workflow</td>
          <td>SRE + FinOps</td>
      </tr>
      <tr>
          <td>FinOps</td>
          <td>Vantage / CloudHealth / AWS Cost Explorer</td>
          <td>cost report / forecast / rightsizing</td>
          <td>無(reporting)</td>
          <td>tag governance / owner mapping / cadence</td>
          <td>FinOps + Eng lead</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>對齊 sub-category 跟問題節點：缺 saturation → load test；缺 production gap → replay；缺 瓶頸定位 → profiler；缺 capacity / cost 閉環 → optimizer + FinOps</li>
<li>評估 production 風險：load test 安全、replay / mirror 要明示 side effect 邊界、profiler 要看採樣 overhead、FinOps reporting 無風險</li>
<li>對齊 owner：load test 多 Engineering / QA、replay 多 SRE + Security、optimization + FinOps 跨團隊</li>
</ul>
<p>下面 5 段把對照表的 sub-category 展開、每段帶 vendor 選型判讀。</p>
<h3 id="load-testk6--jmeter--gatling--locust--vegeta">Load test（k6 / JMeter / Gatling / Locust / Vegeta）</h3>
<p>Load test 是 09 模組的主要 saturation 探測工具、跟 <a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">06 reliability load test 章節</a> 同 vendor 但角度不同 — 06 看 CI gate / regression evidence、09 看 capacity planning / saturation discovery / peak event readiness。</p>
<p>選型判讀：CI-first JS → <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a>；JVM + 複雜 scenario → <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a>；既有 .jmx 資產 → <a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a>；Python custom behavior → <a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a>；快速 HTTP probe / fixed rate → <a href="/blog/backend/09-performance-capacity/vendors/vegeta/" data-link-title="Vegeta" data-link-desc="用簡潔 CLI 與固定 rate HTTP attack 快速探測 latency、throughput 與 saturation 的效能工程工具">Vegeta</a>（單一 HTTP attack 模式、不適合多 step scenario）。</p>
<h3 id="production-replaygoreplay--service-mesh-mirroring--aws-vpc-traffic-mirroring">Production replay（GoReplay / Service Mesh Mirroring / AWS VPC Traffic Mirroring）</h3>
<p>Production replay 把實際流量重播到 shadow target、補 load test 的「人工 scenario 跟真實流量差距」缺口。<strong>GoReplay</strong> 應用層 HTTP traffic capture + replay；<strong>Service Mesh Mirroring</strong> 用 Envoy / Istio proxy mirror、適合 K8s 內部；<strong>AWS VPC Traffic Mirroring</strong> L4 封包鏡像、適合非 HTTP / 低侵入。</p>
<p>選型判讀：HTTP application 層 → GoReplay；K8s 內 service mesh → Service Mesh Mirroring；非 HTTP / 跨 VPC / 低侵入 → AWS VPC。共同議題：PII 遮罩、idempotency boundary、downstream 配額 — 不可省。</p>
<h3 id="continuous-profilingdatadog-continuous-profiler--pyroscope--parca">Continuous profiling（Datadog Continuous Profiler / Pyroscope / Parca）</h3>
<p>Continuous profiling 在 production 持續採樣、退化時可 profile diff 找瓶頸。<strong>Datadog Continuous Profiler</strong> SaaS APM 整合、deploy marker 自動關聯；<strong>Pyroscope</strong> OSS / Grafana 生態、可自管或 Grafana Cloud；<strong>Parca</strong> eBPF-based、infrastructure-wide profile（不需 application instrumentation）。</p>
<p>選型判讀：已用 Datadog APM → Datadog Profiler；Grafana 生態 / OSS → Pyroscope；不想 instrument application + eBPF 友善 → Parca。共同議題：採樣 overhead（CPU / memory）、symbolization、storage cost、敏感資訊。</p>
<h3 id="optimizationakamas">Optimization（Akamas）</h3>
<p>Optimization 把 workload + SLO + cost 放進同一閉環、產出 configuration recommendation。<strong>Akamas</strong> 是 09 模組唯一 optimizer vendor、適合已有可量測 workload 跟成本壓力的服務。</p>
<p>選型判讀：Kubernetes rightsizing + runtime tuning + cost target → Akamas；純 FinOps reporting 不夠（要主動建議）→ Akamas。Akamas 不替代 FinOps tool — Vantage / CloudHealth 看歷史成本、Akamas 提產出未來 recommendation。</p>
<h3 id="finopsvantage--cloudhealth--aws-cost-explorer">FinOps（Vantage / CloudHealth / AWS Cost Explorer）</h3>
<p>FinOps 提供 cost visibility + forecast + allocation。<strong>Vantage</strong> Kubernetes cost + forecast 友善的 startup-friendly 平台；<strong>CloudHealth</strong> enterprise FinOps governance + policy + chargeback；<strong>AWS Cost Explorer</strong> AWS-native cost analysis baseline（免費、限 AWS）。</p>
<p>選型判讀：純 AWS 啟動 → Cost Explorer；多雲 + startup / mid-size → Vantage；enterprise + 多 BU chargeback → CloudHealth；K8s workload cost → Kubecost / OpenCost（不在本表、後續候選）。共同議題：tag governance、cost center mapping、cadence。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>服務路徑：<a href="/blog/backend/#%e8%b2%ab%e7%a9%bf%e5%bc%8f%e6%a1%88%e4%be%8bcheckout-%e6%9c%8d%e5%8b%99%e6%bc%94%e9%80%b2" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Checkout 服務演進</a></li>
<li>平行：<a href="/blog/backend/06-reliability/vendors/" data-link-title="可靠性 Vendor 清單" data-link-desc="規劃 CI、壓測、chaos engineering 與 SLO 工具的服務頁撰寫順序與判準">06 Reliability vendors</a> — 06 從驗證流程看工具，09 從容量量化與效能工程看工具</li>
</ul>
]]></content:encoded></item><item><title>資安與資料保護 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/</guid><description>&lt;p>資安與資料保護 Vendor 清單的核心責任是把安全服務名稱放回控制面、信任邊界、證據鏈與交接路由的判斷。每個服務頁先回答它承擔身份、秘密、傳輸、入口、資料保護、供應鏈或偵測哪一段控制責任，再討論導入條件、操作成本、例外治理與事故回寫。多數控制面都有「自建 vs 買託管」的選擇：自建認證 vs Auth0 / Okta / Cognito、自管 secret store vs managed KMS / Vault、自架 WAF vs 雲端 WAF — 把整塊控制責任外包給專做合規的 vendor 是常見且合理的起點，逐控制面的買 vs 建判讀見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>資安服務要從控制問題進入。讀者如果要處理身份與授權，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>；如果要處理秘密與機器憑證，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理&lt;/a>；如果要處理入口與伺服器暴露，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>資安服務頁的教學順序是先建立 identity / IAM，再進入 secrets / KMS / PKI、edge、supply chain、detection / DLP。這個順序對齊 checkout E6：讀者先理解誰能做什麼、秘密與金鑰如何生命週期化，再比較入口防護、artifact trust、偵測訊號與資料控制如何接到 release gate、evidence package 與 incident handoff。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>Identity / IdP&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a>&lt;/td>
 &lt;td>人類身份、SSO、MFA、group、role 與 session 邊界如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloud IAM&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a>&lt;/td>
 &lt;td>cloud resource 權限、policy、role assumption 與 least privilege 如何落地&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Secrets / Vault&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &amp;#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &amp;#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager&lt;/a>&lt;/td>
 &lt;td>secret storage、rotation、lease、audit 與 application delivery 如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>KMS / HSM&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &amp;#43; Grant 雙軌授權">AWS KMS&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &amp;#43; Cloud HSM &amp;#43; External Key Manager">Google Cloud KMS&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &amp;#43; Key &amp;#43; Certificate）、整合 Managed Identity &amp;#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &amp;#43; 資料主權場景的 key custody">CloudHSM&lt;/a>&lt;/td>
 &lt;td>key lifecycle、envelope encryption、rotation 與權限分離如何成立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge / WAF&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &amp;#43; DDoS &amp;#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &amp;#43; Managed Rule Group &amp;#43; Rate-based Rule、Shield Standard 內含">AWS WAF&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &amp;#43; ATO &amp;#43; Bot 一體">Fastly Next-Gen WAF&lt;/a>&lt;/td>
 &lt;td>入口防護、bot、rate limit、managed rule 與 false positive 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Certificate / PKI&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&amp;#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &amp;#43; Challenge solver">cert-manager&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &amp;#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&amp;#39;s Encrypt" data-link-desc="免費 &amp;#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&amp;rsquo;s Encrypt&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &amp;#43; Trust Bundle、跨組織 federation">SPIRE&lt;/a>&lt;/td>
 &lt;td>TLS、mTLS、workload identity 與憑證生命週期如何自動化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Supply chain&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &amp;#43; Secret Scanning &amp;#43; Dependency Review &amp;#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &amp;#43; Code (SAST) &amp;#43; Container &amp;#43; IaC &amp;#43; Cloud (CSPM)、Reachability analysis">Snyk&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &amp;#43; Security Update &amp;#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &amp;#43; Secret &amp;#43; License &amp;#43; SBOM、Apache 2.0、CI 友善">Trivy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &amp;#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &amp;#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype&lt;/a>&lt;/td>
 &lt;td>SCA、container scan、SBOM、artifact trust 與 release gate 如何接軌&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SIEM / Detection&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &amp;#43; indexer &amp;#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &amp;#43; EDR &amp;#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &amp;#43; CSPM &amp;#43; CWS &amp;#43; AAP &amp;#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &amp;#43; SOAR &amp;#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &amp;#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations&lt;/a>&lt;/td>
 &lt;td>偵測訊號、log pipeline、alert quality 與 incident handoff 如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DLP / Data control&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &amp;#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &amp;#43; information protection &amp;#43; DLP &amp;#43; insider risk 統合平台、label-driven">Microsoft Purview&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &amp;#43; S3)" data-link-desc="BigQuery column / row-level security &amp;#43; S3 bucket policy &amp;#43; Access Points &amp;#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native Data Policy (BigQuery + S3)&lt;/a>&lt;/td>
 &lt;td>資料分類、遮罩、匯出、資料駐留與證據鏈如何落地&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook。&lt;/p></description><content:encoded><![CDATA[<p>資安與資料保護 Vendor 清單的核心責任是把安全服務名稱放回控制面、信任邊界、證據鏈與交接路由的判斷。每個服務頁先回答它承擔身份、秘密、傳輸、入口、資料保護、供應鏈或偵測哪一段控制責任，再討論導入條件、操作成本、例外治理與事故回寫。多數控制面都有「自建 vs 買託管」的選擇：自建認證 vs Auth0 / Okta / Cognito、自管 secret store vs managed KMS / Vault、自架 WAF vs 雲端 WAF — 把整塊控制責任外包給專做合規的 vendor 是常見且合理的起點，逐控制面的買 vs 建判讀見 <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>。</p>
<h2 id="讀法">讀法</h2>
<p>資安服務要從控制問題進入。讀者如果要處理身份與授權，先回到 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>；如果要處理秘密與機器憑證，先回到 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>；如果要處理入口與伺服器暴露，先回到 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>資安服務頁的教學順序是先建立 identity / IAM，再進入 secrets / KMS / PKI、edge、supply chain、detection / DLP。這個順序對齊 checkout E6：讀者先理解誰能做什麼、秘密與金鑰如何生命週期化，再比較入口防護、artifact trust、偵測訊號與資料控制如何接到 release gate、evidence package 與 incident handoff。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務群</th>
          <th>候選服務</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Identity / IdP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>、<a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a>、<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
          <td>人類身份、SSO、MFA、group、role 與 session 邊界如何治理</td>
      </tr>
      <tr>
          <td>Cloud IAM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
          <td>cloud resource 權限、policy、role assumption 與 least privilege 如何落地</td>
      </tr>
      <tr>
          <td>Secrets / Vault</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a></td>
          <td>secret storage、rotation、lease、audit 與 application delivery 如何治理</td>
      </tr>
      <tr>
          <td>KMS / HSM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-kms/" data-link-title="Google Cloud KMS" data-link-desc="GCP 原生 key management service、KeyRing / CryptoKey Version 設計、CMEK 整合 &#43; Cloud HSM &#43; External Key Manager">Google Cloud KMS</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a></td>
          <td>key lifecycle、envelope encryption、rotation 與權限分離如何成立</td>
      </tr>
      <tr>
          <td>Edge / WAF</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a>、<a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly Next-Gen WAF</a></td>
          <td>入口防護、bot、rate limit、managed rule 與 false positive 如何取捨</td>
      </tr>
      <tr>
          <td>Certificate / PKI</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-acm/" data-link-title="AWS ACM" data-link-desc="AWS-managed certificate provisioning、DNS validation &#43; auto-renewal、整合 ELB / CloudFront / API Gateway、Private CA 後端">AWS ACM</a>、<a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a>、<a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a></td>
          <td>TLS、mTLS、workload identity 與憑證生命週期如何自動化</td>
      </tr>
      <tr>
          <td>Supply chain</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a>、<a href="/blog/backend/07-security-data-protection/vendors/dependabot/" data-link-title="Dependabot" data-link-desc="GitHub 原生依賴更新自動化、Version Update &#43; Security Update &#43; Alerts、Grouped Updates 減 PR noise、Auto-merge 配 branch protection">Dependabot</a>、<a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a>、<a href="/blog/backend/07-security-data-protection/vendors/syft-grype/" data-link-title="Syft &#43; Grype" data-link-desc="Anchore 開源姐妹工具：Syft 產 SBOM (CycloneDX / SPDX) &#43; Grype scan 漏洞、Unix philosophy、cosign attestation 整合">Syft / Grype</a></td>
          <td>SCA、container scan、SBOM、artifact trust 與 release gate 如何接軌</td>
      </tr>
      <tr>
          <td>SIEM / Detection</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a>、<a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a></td>
          <td>偵測訊號、log pipeline、alert quality 與 incident handoff 如何治理</td>
      </tr>
      <tr>
          <td>DLP / Data control</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a>、<a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native Data Policy (BigQuery + S3)</a></td>
          <td>資料分類、遮罩、匯出、資料駐留與證據鏈如何落地</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="cloudflare-waf/">Cloudflare WAF</a></td>
          <td><a href="cloudflare-waf/page-shield-csp-sri/">page-shield-csp-sri</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="hashicorp-vault/">HashiCorp Vault</a></td>
          <td><a href="hashicorp-vault/dynamic-credential/">dynamic-credential</a></td>
          <td><a href="hashicorp-vault/migrate-to-aws-secrets-manager/">→ AWS Secrets Manager</a></td>
      </tr>
      <tr>
          <td><a href="splunk/">Splunk</a></td>
          <td><a href="splunk/risk-based-alerting/">risk-based-alerting</a></td>
          <td><a href="splunk/migrate-to-elastic-security/">→ Elastic Security</a></td>
      </tr>
  </tbody>
</table>
<p>本章節 vendor 服務頁覆蓋率高（51 個 vendor 服務頁、上方「T1 服務頁大綱」跟「後續候選」段已全部建立），但 deep article / migration playbook 還在早期階段。對應的 backlog 議題見上方「T1 服務頁大綱」段每個服務群要回答的核心問題、跟各 vendor <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>資安服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 identity、IAM、secret、KMS、WAF、PKI、supply chain、SIEM 還是 DLP</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷控制面責任、信任邊界、證據需求、例外與事故交接</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「誰能做什麼、憑證在哪裡、入口如何暴露、證據是否可回查」快速定位</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>onboarding、policy、rotation、rule update、exception、audit、handoff</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>managed service、self-hosted control、cloud-native、SaaS security tool 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>federation、workload identity、mTLS、SBOM、DLP、multi-cloud policy</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>over-permission、stale secret、broken rotation、WAF false positive、missing audit trail</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>觀測訊號回 04、release gate 回 06、入口部署回 05、事故處理回 08</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>合規逐條法規解讀、完整 SOC 2 / HIPAA 流程、所有攻擊技術細節</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>回到 7.C cases、7.B blue-team materials、8 incident write-back 連到對應 vendor 事件</td>
      </tr>
      <tr>
          <td>下一步路由</td>
          <td>上游 chapter（7.X）、平行 vendor、下游模組（04 / 05 / 06 / 08）的交接</td>
      </tr>
  </tbody>
</table>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務群</th>
          <th>撰寫目的</th>
          <th>狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>S1</td>
          <td>Identity / Cloud IAM</td>
          <td>建立人類身份、機器身份、role / policy baseline</td>
          <td><strong>完成（2026-05-18、7 個 vendor）</strong></td>
      </tr>
      <tr>
          <td>S2</td>
          <td>Secrets / KMS / PKI</td>
          <td>建立 secret、key、certificate lifecycle 與 rotation 判準</td>
          <td><strong>完成（2026-05-18、11 個 vendor）</strong></td>
      </tr>
      <tr>
          <td>S3</td>
          <td>Edge / WAF / Supply chain</td>
          <td>建立入口防護、artifact trust 與 release gate 對照</td>
          <td><strong>完成（2026-05-18、8 個 vendor）</strong></td>
      </tr>
      <tr>
          <td>S4</td>
          <td>SIEM / Detection / DLP</td>
          <td>建立偵測覆蓋、資料保護、證據鏈與事故 handoff</td>
          <td><strong>完成（2026-05-18、7 個 vendor）</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選c-批次完成">後續候選（C 批次完成）</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點 / 狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PAM / access</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/teleport/" data-link-title="Teleport" data-link-desc="Identity-Aware Proxy &#43; PAM、SSH / DB / K8s / Desktop session 統一 short-lived cert &#43; session recording &#43; JIT、跟 Okta / Vault 互補">Teleport</a>、<a href="/blog/backend/07-security-data-protection/vendors/boundary/" data-link-title="HashiCorp Boundary" data-link-desc="Identity-based access broker、跟 Vault 同生態組合（Boundary 控連線 / Vault 給 credential）、Multi-hop Worker 跨網路分段">Boundary</a>、<a href="/blog/backend/07-security-data-protection/vendors/tailscale-ssh/" data-link-title="Tailscale SSH" data-link-desc="WireGuard-based zero-trust mesh &#43; identity-bound SSH、ACL JSON policy、developer-friendly、跟 IdP integration 取代 SSH key">Tailscale SSH</a>、<a href="/blog/backend/07-security-data-protection/vendors/cloudflare-access/" data-link-title="Cloudflare Access" data-link-desc="Zero Trust Network Access (ZTNA)、取代 VPN 的 application-layer access、Argo Tunnel &#43; Device Posture &#43; IdP integration">Cloudflare Access</a></td>
          <td>完成（C1、4 vendor）— 管理面 access、session audit、JIT</td>
      </tr>
      <tr>
          <td>CSPM / CNAPP</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/wiz/" data-link-title="Wiz" data-link-desc="Agentless CNAPP、Security Graph &#43; Toxic Combination 風險優先級、API-only scan 不需 workload agent">Wiz</a>、<a href="/blog/backend/07-security-data-protection/vendors/prisma-cloud/" data-link-title="Prisma Cloud" data-link-desc="Palo Alto CNAPP、agent (Defender) &#43; agentless 雙軌、五模組（Compute / CSPM / Code / Data / CIEM）、Compliance template 強">Prisma Cloud</a>、<a href="/blog/backend/07-security-data-protection/vendors/lacework/" data-link-title="Lacework" data-link-desc="CNAPP 走 Polygraph ML behavioral baseline 路線、2024 跟 Fortinet 合併成 FortiCNAPP、自動學 normal、anomaly 自動 alert">Lacework</a>、<a href="/blog/backend/07-security-data-protection/vendors/crowdstrike-falcon-cs/" data-link-title="CrowdStrike Falcon Cloud Security" data-link-desc="CrowdStrike 在 Falcon endpoint EDR 之上的 CNAPP、agent 統一跨 endpoint &#43; workload &#43; container、CrowdStrike Intelligence 內建">CrowdStrike Falcon CS</a></td>
          <td>完成（C2、4 vendor）— cloud posture、asset inventory、risk prioritization</td>
      </tr>
      <tr>
          <td>Policy as code</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/opa/" data-link-title="Open Policy Agent (OPA)" data-link-desc="CNCF general-purpose policy engine、Rego Datalog-like 語言、decoupled decision &#43; enforcement、跨 K8s / API / Terraform / SQL 統一 policy">OPA</a>、<a href="/blog/backend/07-security-data-protection/vendors/conftest/" data-link-title="Conftest" data-link-desc="OPA CLI wrapper for static config policy check、Rego policy &#43; 多 parser（Terraform / K8s / Dockerfile / JSON）、CI-time gate">Conftest</a>、<a href="/blog/backend/07-security-data-protection/vendors/kyverno/" data-link-title="Kyverno" data-link-desc="K8s-native policy engine、YAML policy（非 Rego）、五類 rule（Validate / Mutate / Generate / Verify Images / Cleanup）、CNCF Incubating">Kyverno</a>、<a href="/blog/backend/07-security-data-protection/vendors/gatekeeper/" data-link-title="OPA Gatekeeper" data-link-desc="OPA 官方 K8s admission controller、ConstraintTemplate &#43; Constraint 兩層、Rego policy &#43; Audit &#43; Mutation">Gatekeeper</a></td>
          <td>完成（C3、4 vendor）— admission control、policy review、exception workflow</td>
      </tr>
      <tr>
          <td>Runtime detection</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/falco/" data-link-title="Falco" data-link-desc="CNCF Graduated runtime cloud-native threat detection、eBPF / kmod driver、Rule YAML &#43; Falcosidekick &#43; Talon、K8s container runtime 偵測為主">Falco</a>、<a href="/blog/backend/07-security-data-protection/vendors/cilium-tetragon/" data-link-title="Cilium Tetragon" data-link-desc="eBPF-based runtime security &#43; inline enforcement、跟 Cilium CNI 同生態、TracingPolicy CRD、process credentials tracking &#43; KillerAction">Cilium Tetragon</a></td>
          <td>完成（C4、2 vendor）— syscall / runtime signal、container threat detection</td>
      </tr>
      <tr>
          <td>Secret scanning</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/gitguardian/" data-link-title="GitGuardian" data-link-desc="Secret scanning &#43; remediation SaaS、350&#43; Detector &#43; Validation endpoint、跨 SCM &#43; SaaS（Slack / Notion）、Honeytokens decoy">GitGuardian</a>、<a href="/blog/backend/07-security-data-protection/vendors/gitleaks/" data-link-title="Gitleaks" data-link-desc="OSS CLI secret scanner、Go 寫、Rule TOML &#43; regex &#43; entropy、SARIF output、跨 SCM、pre-commit &#43; CI 友善">Gitleaks</a></td>
          <td>完成（C4、2 vendor）— leaked secret detection、developer workflow、rotation trigger</td>
      </tr>
      <tr>
          <td>Data security</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/immuta/" data-link-title="Immuta" data-link-desc="Data security platform、跨 Snowflake / Databricks / BigQuery / Redshift 統一 ABAC &#43; masking、Query Plan Rewriter、native execution">Immuta</a>、<a href="/blog/backend/07-security-data-protection/vendors/privacera/" data-link-title="Privacera" data-link-desc="Data security &#43; AI governance platform、Apache Ranger commercial fork、多 warehouse access control &#43; LLM I/O 治理（PAIG）">Privacera</a>、<a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a></td>
          <td>完成（C5 + S4、3 vendor）— data access policy、masking、lineage、governance</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 preventive control、detective control 與 response handoff。IAM / KMS / WAF / policy-as-code 是 preventive control；SIEM / runtime detection / secret scanning 是 detective control；PAM、incident channel 與 evidence write-back 連到 08 的 response handoff。</p>
<h2 id="reading-paths51-個-vendor-的進入順序建議">Reading paths（51 個 vendor 的進入順序建議）</h2>
<p>讀完 51 個 vendor 不是線性目的、是 <em>依組織當前的安全成熟度跳讀</em>。以下是四條常見路徑：</p>
<p><strong>Path A — Startup baseline（&lt; 50 人、cloud-native）</strong>：
<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> → <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> → <a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> → <a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> → <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GitHub Advanced Security</a> → <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a>。共 7-8 個 vendor、預算敏感、cloud-native、SaaS 優先。</p>
<p><strong>Path B — Enterprise multi-cloud（500+ 人、跨雲）</strong>：
<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> + <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> → 各雲 IAM（<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">GCP</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure</a>）→ <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> + <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a> → <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a> + <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> → <a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly NG-WAF</a> → <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a> + <a href="/blog/backend/07-security-data-protection/vendors/trivy/" data-link-title="Trivy" data-link-desc="Aqua Security 開源 all-in-one scanner：Container / Filesystem / K8s / IaC &#43; Secret &#43; License &#43; SBOM、Apache 2.0、CI 友善">Trivy</a> → <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> → <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> + <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a>。</p>
<p><strong>Path C — Compliance-heavy（金融 / 醫療 / 政府）</strong>：
<a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a>（資料主權）→ <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> + <a href="/blog/backend/07-security-data-protection/vendors/cloudhsm/" data-link-title="AWS CloudHSM" data-link-desc="Single-tenant dedicated HSM（FIPS 140-2 Level 3）、AWS 不持 Crypto User credential、合規 &#43; 資料主權場景的 key custody">CloudHSM</a>（FIPS 140-2 L3）→ <a href="/blog/backend/07-security-data-protection/vendors/cert-manager/" data-link-title="cert-manager" data-link-desc="K8s 原生 certificate lifecycle automation、支援 Let&#39;s Encrypt / Vault PKI / Venafi 等多 issuer、auto-renewal &#43; Challenge solver">cert-manager</a> + <a href="/blog/backend/07-security-data-protection/vendors/letsencrypt/" data-link-title="Let&#39;s Encrypt" data-link-desc="免費 &#43; 自動化的公共 ACME CA、90 天 TTL 強制自動化、跨雲跨平台 public TLS cert 的事實基礎">Let&rsquo;s Encrypt</a> → <a href="/blog/backend/07-security-data-protection/vendors/google-dlp/" data-link-title="Google DLP" data-link-desc="GCP 原生 Sensitive Data Protection：infoType discovery &#43; transformation (mask / FPE / tokenize / k-anonymity)、整合 BigQuery / GCS / Cloud SQL">Google DLP</a> + <a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> → <a href="/blog/backend/07-security-data-protection/vendors/cloud-data-policy/" data-link-title="Cloud-native Data Policy (BigQuery &#43; S3)" data-link-desc="BigQuery column / row-level security &#43; S3 bucket policy &#43; Access Points &#43; Macie、雲端原生資料層 access control、跟 DLP / Purview 互補">Cloud-native data policy</a> → <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a>（Mandiant + 大規模）。</p>
<p><strong>Path D — 事故驅動補洞</strong>：先讀 <a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">7.C 案例</a> + <a href="/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">紅隊案例</a> 對應自家事故、再回 vendor 頁找控制面缺口。例如 helpdesk social engineering 失效 → <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> callback workflow + <a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> anomaly detection；signing key 失控 → <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a> HSM-bound key + <a href="/blog/backend/07-security-data-protection/vendors/google-security-operations/" data-link-title="Google Security Operations" data-link-desc="Google 雲原生 SIEM &#43; SOAR &#43; Mandiant threat intel 三合一（前 Chronicle）、UDM &#43; YARA-L、fixed-price by data tier、PB-scale 友善">Google Security Operations</a> cross-tenant token forging detection。</p>
<h2 id="cross-category-整合-stack">Cross-category 整合 stack</h2>
<p>實務 stack 通常跨多個類別組合、不是單一 vendor。下表列三個典型 stack：</p>
<table>
  <thead>
      <tr>
          <th>Stack 場景</th>
          <th>Identity</th>
          <th>Cloud IAM</th>
          <th>Secrets / KMS</th>
          <th>WAF + Supply chain</th>
          <th>SIEM + DLP</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only SaaS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a> → <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a></td>
          <td>AWS IAM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> + <a href="/blog/backend/07-security-data-protection/vendors/aws-kms/" data-link-title="AWS KMS" data-link-desc="AWS 原生 key management service、envelope encryption / digital signing / Multi-Region Key、Key Policy &#43; Grant 雙軌授權">AWS KMS</a></td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-waf/" data-link-title="AWS WAF" data-link-desc="AWS-internal WAF、跟 ALB / CloudFront / API Gateway 直接整合、Web ACL &#43; Managed Rule Group &#43; Rate-based Rule、Shield Standard 內含">AWS WAF</a> + <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS</a></td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/" data-link-title="Splunk" data-link-desc="業界 SIEM 標準、forwarder &#43; indexer &#43; search head 架構、SPL 為核心查詢語言、ingestion-based 計費跟偵測覆蓋率的 trade-off">Splunk</a> / <a href="/blog/backend/07-security-data-protection/vendors/datadog-security/" data-link-title="Datadog Security" data-link-desc="Datadog observability platform 上的 security suite：Cloud SIEM &#43; CSPM &#43; CWS &#43; AAP &#43; Sensitive Data Scanner、跟 observability 同 plane">Datadog Security</a></td>
      </tr>
      <tr>
          <td>Multi-cloud + on-prem</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>（人類）+ <a href="/blog/backend/07-security-data-protection/vendors/spire/" data-link-title="SPIRE" data-link-desc="SPIFFE Runtime Environment、attested workload identity、short-lived SVID &#43; Trust Bundle、跨組織 federation">SPIRE</a>（workload）</td>
          <td>三家 cloud IAM</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">Vault</a>（跨雲統一）</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/" data-link-title="Cloudflare WAF" data-link-desc="Edge WAF &#43; DDoS &#43; Bot management 整合套件、global anycast 網路、控制面信任邊界跟客戶側補強的對照">Cloudflare WAF</a> + <a href="/blog/backend/07-security-data-protection/vendors/snyk/" data-link-title="Snyk" data-link-desc="跨 SCM 多模組 application security platform：Open Source (SCA) &#43; Code (SAST) &#43; Container &#43; IaC &#43; Cloud (CSPM)、Reachability analysis">Snyk</a></td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/elastic-security/" data-link-title="Elastic Security" data-link-desc="Elastic Stack 上的 SIEM &#43; EDR &#43; Cloud Security 套件、OSS 起源、KQL/EQL/Lucene/ES|QL 多查詢語言、resource-based pricing">Elastic Security</a>（OSS-friendly）</td>
      </tr>
      <tr>
          <td>Microsoft 365 + Azure heavy</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Entra ID</a></td>
          <td>Azure RBAC</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-key-vault/" data-link-title="Azure Key Vault" data-link-desc="Azure 三合一 service（Secret &#43; Key &#43; Certificate）、整合 Managed Identity &#43; Entra ID RBAC、Premium tier 走 HSM">Azure Key Vault</a></td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/fastly-ngwaf/" data-link-title="Fastly Next-Gen WAF" data-link-desc="Behavioral / 語意分析 WAF（前 Signal Sciences）、低 false positive、Edge / Agent / Cloud 三種部署模型、API &#43; ATO &#43; Bot 一體">Fastly NG-WAF</a> + <a href="/blog/backend/07-security-data-protection/vendors/github-advanced-security/" data-link-title="GitHub Advanced Security" data-link-desc="GitHub 內建 4 大模組：Code Scanning (CodeQL) &#43; Secret Scanning &#43; Dependency Review &#43; Dependabot、跟 PR / Security tab 深度整合">GHAS</a></td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/microsoft-purview/" data-link-title="Microsoft Purview" data-link-desc="Microsoft 跨 M365 / Azure / endpoint 的 data governance &#43; information protection &#43; DLP &#43; insider risk 統合平台、label-driven">Microsoft Purview</a> + Sentinel</td>
      </tr>
  </tbody>
</table>
<p>Stack 不是一次到位、按 <a href="#reading-paths33-%e5%80%8b-vendor-%e7%9a%84%e9%80%b2%e5%85%a5%e9%a0%86%e5%ba%8f%e5%bb%ba%e8%ad%b0">Path A → B → C</a> 的成熟度演進加 vendor。每加一個 vendor 都要對應一個 <em>已被 case 庫驗證</em> 的失效模式 — 不是「業界都用」就上。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li>上游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>上游：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
<li>案例：<a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">7.C 資安案例正文</a></li>
<li>服務路徑：<a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence 實作示範</a></li>
</ul>
]]></content:encoded></item><item><title>可靠性服務案例庫</title><link>https://tarrragon.github.io/blog/backend/06-reliability/cases/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/cases/</guid><description>&lt;p>本案例庫以服務為單位、收錄公開 SRE 實踐（SRE Book / 工程部落格 / 演講 / paper）。每個服務一個資料夾，累積該服務的可靠性工程文化、failure mode 與 chaos / DR 案例。&lt;/p>
&lt;p>服務分層依 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六 _index&lt;/a> 的 T1 / T2 / T3 規劃。重複出現於 06 / 08 的服務（stripe / cloudflare / linkedin）資料夾住在主要教學模組、跨模組以連結互通。&lt;/p>
&lt;h2 id="t1-服務">T1 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">google&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/" data-link-title="Netflix" data-link-desc="Netflix Chaos Engineering 起源：Simian Army / FIT / 規模化故障注入">netflix&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/" data-link-title="Amazon" data-link-desc="Amazon Cell-based Architecture / Shuffle Sharding / Blast Radius 設計">amazon&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">shopify&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="t1-第一批正文已完成">T1 第一批正文（已完成）&lt;/h2>
&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>Google&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">G1 Error Budget 與 Release Gating&lt;/a>&lt;/td>
 &lt;td>可靠性消耗如何直接決定發布節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Netflix&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">N1 Steady State、Chaos 與 FIT&lt;/a>&lt;/td>
 &lt;td>故障注入如何變成可證偽流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Amazon&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1 Shuffle Sharding 與 Cell 邊界&lt;/a>&lt;/td>
 &lt;td>多租戶故障如何被局部化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stripe&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Idempotency 與零停機遷移&lt;/a>&lt;/td>
 &lt;td>交易重試與遷移如何共用一致性模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1 BFCM 容量治理與 Game Day&lt;/a>&lt;/td>
 &lt;td>峰值風險如何在活動前被消化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t1-第二批正文已完成">T1 第二批正文（已完成）&lt;/h2>
&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>Amazon&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">A2 Static Stability 與 Constant Work&lt;/a>&lt;/td>
 &lt;td>控制面失效時資料面如何維持服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stripe&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">S2 Canary Deploy 與 Progressive Rollout&lt;/a>&lt;/td>
 &lt;td>金流場景的放行節奏與交易指標驅動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shopify&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">H2 Pod Architecture 與 Resiliency Matrix&lt;/a>&lt;/td>
 &lt;td>多租戶隔離與系統化失敗模式盤點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t1-深挖批次已完成">T1 深挖批次（已完成）&lt;/h2>
&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>Google&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2 Postmortem Action Item Closure 治理&lt;/a>&lt;/td>
 &lt;td>事故教訓如何轉成有 owner 的改進項&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Google&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">G3 Toil Budget 與 Automation 投資政策&lt;/a>&lt;/td>
 &lt;td>值班壓力如何轉成工程投資決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Netflix&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">N2 Business-Hours Chaos Guardrails&lt;/a>&lt;/td>
 &lt;td>business hours 故障注入的安全邊界設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Netflix&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">N3 FIT 證據交接與 Release Gate 回寫&lt;/a>&lt;/td>
 &lt;td>故障注入結果如何結構化驅動放行決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t2-服務">T2 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/" data-link-title="Honeycomb" data-link-desc="Honeycomb Observability-driven SRE 與 SLO 實作">honeycomb&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare（住於 08）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/" data-link-title="Microsoft / Azure SRE" data-link-desc="Microsoft Azure SRE Practices 與 Resilience Patterns">microsoft&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="t2t3-第一批正文已完成">T2/T3 第一批正文（已完成）&lt;/h2>
&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>LinkedIn&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1 Capacity 與 On-call 分層&lt;/a>&lt;/td>
 &lt;td>容量邊界與值班交接協同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Honeycomb&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">HC1 Burn Rate 驅動可靠性&lt;/a>&lt;/td>
 &lt;td>用 SLO 消耗速度驅動行動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Microsoft&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">MS1 變更治理與可靠性門檻&lt;/a>&lt;/td>
 &lt;td>變更分層與 release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Spotify&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">SP1 平台工程與可靠性契約&lt;/a>&lt;/td>
 &lt;td>分散團隊共用可靠性基線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pinterest&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">P1 快取可靠性與容量驚奇&lt;/a>&lt;/td>
 &lt;td>命中率崩落時的恢復節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">M1 Region Failover 邊界治理&lt;/a>&lt;/td>
 &lt;td>跨區擴散與回復順序治理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t2t3-第二批正文已完成">T2/T3 第二批正文（已完成）&lt;/h2>
&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>LinkedIn&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">L2 Automated Load Testing 與 Capacity Forecasting&lt;/a>&lt;/td>
 &lt;td>持續壓測驅動容量預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">M2 BGP 事故與控制面恢復順序&lt;/a>&lt;/td>
 &lt;td>回復工具依賴已故障系統的恢復困境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Honeycomb&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/" data-link-title="Honeycomb：Production Excellence 與 Test in Production" data-link-desc="用 high-cardinality observability 把 production 變成安全的驗證環境：feature flag、progressive rollout 與即時回饋的配合。">HC2 Production Excellence 與 Test in Production&lt;/a>&lt;/td>
 &lt;td>observability-driven 生產驗證文化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Microsoft&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/" data-link-title="Microsoft：Safe Deployment Practices 與 Resilience Patterns" data-link-desc="大型 SaaS 用 ring-based deployment 控制變更擴散，用標準化 resilience patterns 讓依賴失效時的降級行為可預測。">MS2 Safe Deployment Practices 與 Resilience Patterns&lt;/a>&lt;/td>
 &lt;td>ring-based deployment 與韌性設計模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Spotify&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/" data-link-title="Spotify：Backstage Service Catalog 與 Reliability Metadata" data-link-desc="用 service catalog 治理分散團隊的可靠性資訊：ownership、SLO 狀態、依賴圖與 runbook 的單一入口。">SP2 Backstage Service Catalog 與 Reliability Metadata&lt;/a>&lt;/td>
 &lt;td>service catalog 治理可靠性資訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pinterest&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/" data-link-title="Pinterest：Storage Migration 與 Data Infrastructure Reliability" data-link-desc="大規模儲存遷移的可靠性設計：用 dual-write、shadow read 與 staged cutover 讓 PB 級資料基礎設施變更可漸進、可驗證、可回退。">P2 Storage Migration 與 Data Infrastructure Reliability&lt;/a>&lt;/td>
 &lt;td>大規模儲存遷移的驗證流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t3-服務">T3 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/spotify/" data-link-title="Spotify" data-link-desc="Spotify Chaos Engineering 與 Squad-based SRE">spotify&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/pinterest/" data-link-title="Pinterest" data-link-desc="Pinterest Capacity Planning 與儲存架構可靠性">pinterest&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/meta/" data-link-title="Meta / Facebook" data-link-desc="Meta Reliability Engineering 與超大規模事故學習">meta&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本案例庫以服務為單位、收錄公開 SRE 實踐（SRE Book / 工程部落格 / 演講 / paper）。每個服務一個資料夾，累積該服務的可靠性工程文化、failure mode 與 chaos / DR 案例。</p>
<p>服務分層依 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六 _index</a> 的 T1 / T2 / T3 規劃。重複出現於 06 / 08 的服務（stripe / cloudflare / linkedin）資料夾住在主要教學模組、跨模組以連結互通。</p>
<h2 id="t1-服務">T1 服務</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/google/" data-link-title="Google" data-link-desc="Google SRE 實踐原典：SLI / SLO / Error Budget / Postmortem 文化">google</a></li>
<li><a href="/blog/backend/06-reliability/cases/netflix/" data-link-title="Netflix" data-link-desc="Netflix Chaos Engineering 起源：Simian Army / FIT / 規模化故障注入">netflix</a></li>
<li><a href="/blog/backend/06-reliability/cases/amazon/" data-link-title="Amazon" data-link-desc="Amazon Cell-based Architecture / Shuffle Sharding / Blast Radius 設計">amazon</a></li>
<li><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe</a></li>
<li><a href="/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">shopify</a></li>
</ul>
<h2 id="t1-第一批正文已完成">T1 第一批正文（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Google</td>
          <td><a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">G1 Error Budget 與 Release Gating</a></td>
          <td>可靠性消耗如何直接決定發布節奏</td>
      </tr>
      <tr>
          <td>Netflix</td>
          <td><a href="/blog/backend/06-reliability/cases/netflix/steady-state-chaos-and-fit/" data-link-title="Netflix：Steady State、Chaos 與 FIT 的驗證路徑" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再設計注入與回復條件。">N1 Steady State、Chaos 與 FIT</a></td>
          <td>故障注入如何變成可證偽流程</td>
      </tr>
      <tr>
          <td>Amazon</td>
          <td><a href="/blog/backend/06-reliability/cases/amazon/shuffle-sharding-and-cell-boundary/" data-link-title="Amazon：Shuffle Sharding 與 Cell 邊界的失效局部化" data-link-desc="用 cell 與 shuffle sharding 將多租戶故障限制在局部，讓恢復策略可分批執行。">A1 Shuffle Sharding 與 Cell 邊界</a></td>
          <td>多租戶故障如何被局部化</td>
      </tr>
      <tr>
          <td>Stripe</td>
          <td><a href="/blog/backend/06-reliability/cases/stripe/idempotency-and-zero-downtime-migration/" data-link-title="Stripe：Idempotency 與零停機遷移的交易安全設計" data-link-desc="把 API 重試與資料遷移放在同一套安全模型，維持支付交易的一致結果。">S1 Idempotency 與零停機遷移</a></td>
          <td>交易重試與遷移如何共用一致性模型</td>
      </tr>
      <tr>
          <td>Shopify</td>
          <td><a href="/blog/backend/06-reliability/cases/shopify/bfcm-capacity-and-game-day/" data-link-title="Shopify：BFCM 容量治理與 Game Day 驗證節奏" data-link-desc="把季節性流量峰值轉成年度可靠性流程，透過容量模型、演練與隔離策略提前吸收風險。">H1 BFCM 容量治理與 Game Day</a></td>
          <td>峰值風險如何在活動前被消化</td>
      </tr>
  </tbody>
</table>
<h2 id="t1-第二批正文已完成">T1 第二批正文（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Amazon</td>
          <td><a href="/blog/backend/06-reliability/cases/amazon/static-stability-and-constant-work/" data-link-title="Amazon：Static Stability 與 Constant Work Pattern" data-link-desc="控制面失效時資料面如何維持服務：用快取、預計算與固定工作量避免恢復放大。">A2 Static Stability 與 Constant Work</a></td>
          <td>控制面失效時資料面如何維持服務</td>
      </tr>
      <tr>
          <td>Stripe</td>
          <td><a href="/blog/backend/06-reliability/cases/stripe/canary-deploy-and-progressive-rollout/" data-link-title="Stripe：Canary Deploy 與 Progressive Rollout 治理" data-link-desc="金流場景如何用交易指標驅動放行節奏：延遲確認、duplicate 偵測與自動回退。">S2 Canary Deploy 與 Progressive Rollout</a></td>
          <td>金流場景的放行節奏與交易指標驅動</td>
      </tr>
      <tr>
          <td>Shopify</td>
          <td><a href="/blog/backend/06-reliability/cases/shopify/pod-architecture-and-resiliency-matrix/" data-link-title="Shopify：Pod Architecture 與 Resiliency Matrix" data-link-desc="多租戶隔離與系統化失敗模式盤點：pod 邊界控制擴散、resiliency matrix 驅動演練。">H2 Pod Architecture 與 Resiliency Matrix</a></td>
          <td>多租戶隔離與系統化失敗模式盤點</td>
      </tr>
  </tbody>
</table>
<h2 id="t1-深挖批次已完成">T1 深挖批次（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Google</td>
          <td><a href="/blog/backend/06-reliability/cases/google/postmortem-action-item-closure-governance/" data-link-title="Google：Postmortem Action Item Closure 治理" data-link-desc="把 blameless postmortem 從會議文件變成可追蹤的可靠性治理機制：action item 分級、完成條件與回寫節奏。">G2 Postmortem Action Item Closure 治理</a></td>
          <td>事故教訓如何轉成有 owner 的改進項</td>
      </tr>
      <tr>
          <td>Google</td>
          <td><a href="/blog/backend/06-reliability/cases/google/toil-budget-and-automation-investment-policy/" data-link-title="Google：Toil Budget 與 Automation 投資政策" data-link-desc="把 toil 從感受問題轉成預算問題：用時間配比與自動化回報機制，避免 on-call 壓力長期侵蝕可靠性工程。">G3 Toil Budget 與 Automation 投資政策</a></td>
          <td>值班壓力如何轉成工程投資決策</td>
      </tr>
      <tr>
          <td>Netflix</td>
          <td><a href="/blog/backend/06-reliability/cases/netflix/chaos-monkey-business-hours-guardrails/" data-link-title="Netflix：Business-Hours Chaos 與 Guardrails" data-link-desc="Chaos Monkey 為何刻意在 business hours 執行：把即時應變能力納入驗證，並用 guardrails 限制實驗風險。">N2 Business-Hours Chaos Guardrails</a></td>
          <td>business hours 故障注入的安全邊界設計</td>
      </tr>
      <tr>
          <td>Netflix</td>
          <td><a href="/blog/backend/06-reliability/cases/netflix/fit-failure-injection-evidence-handoff/" data-link-title="Netflix：FIT 證據交接與 Release Gate 回寫" data-link-desc="用 Failure Injection Testing 產出的證據直接驅動 release gate：把實驗結果轉成可放行、可凍結、可回退的決策欄位。">N3 FIT 證據交接與 Release Gate 回寫</a></td>
          <td>故障注入結果如何結構化驅動放行決策</td>
      </tr>
  </tbody>
</table>
<h2 id="t2-服務">T2 服務</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin</a></li>
<li><a href="/blog/backend/06-reliability/cases/honeycomb/" data-link-title="Honeycomb" data-link-desc="Honeycomb Observability-driven SRE 與 SLO 實作">honeycomb</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare（住於 08）</a></li>
<li><a href="/blog/backend/06-reliability/cases/microsoft/" data-link-title="Microsoft / Azure SRE" data-link-desc="Microsoft Azure SRE Practices 與 Resilience Patterns">microsoft</a></li>
</ul>
<h2 id="t2t3-第一批正文已完成">T2/T3 第一批正文（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LinkedIn</td>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/capacity-headroom-and-oncall-tiering/" data-link-title="LinkedIn：Capacity Headroom 與 On-call 分層" data-link-desc="把容量預測與值班分層綁在一起，降低高峰時段的升級混亂與恢復延遲。">L1 Capacity 與 On-call 分層</a></td>
          <td>容量邊界與值班交接協同</td>
      </tr>
      <tr>
          <td>Honeycomb</td>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/burn-rate-driven-reliability-operations/" data-link-title="Honeycomb：以 Burn Rate 驅動的可靠性操作" data-link-desc="把 SLO burn rate 直接連到值班決策與改善優先序，降低高噪音告警造成的判讀失真。">HC1 Burn Rate 驅動可靠性</a></td>
          <td>用 SLO 消耗速度驅動行動</td>
      </tr>
      <tr>
          <td>Microsoft</td>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">MS1 變更治理與可靠性門檻</a></td>
          <td>變更分層與 release gate</td>
      </tr>
      <tr>
          <td>Spotify</td>
          <td><a href="/blog/backend/06-reliability/cases/spotify/platform-engineering-and-reliability-contracts/" data-link-title="Spotify：平台工程與可靠性契約" data-link-desc="用平台契約統一服務團隊的可靠性最低標準，降低跨團隊變更造成的隱性風險。">SP1 平台工程與可靠性契約</a></td>
          <td>分散團隊共用可靠性基線</td>
      </tr>
      <tr>
          <td>Pinterest</td>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/cache-reliability-and-capacity-surprises/" data-link-title="Pinterest：快取可靠性與容量驚奇治理" data-link-desc="針對快取層失效與流量突增，建立容量緩衝、退化路徑與重建節奏。">P1 快取可靠性與容量驚奇</a></td>
          <td>命中率崩落時的恢復節奏</td>
      </tr>
      <tr>
          <td>Meta</td>
          <td><a href="/blog/backend/06-reliability/cases/meta/region-failover-and-reliability-boundaries/" data-link-title="Meta：Region Failover 與可靠性邊界" data-link-desc="把跨區故障視為邊界治理問題，透過分區隔離與回復順序控制失效擴散。">M1 Region Failover 邊界治理</a></td>
          <td>跨區擴散與回復順序治理</td>
      </tr>
  </tbody>
</table>
<h2 id="t2t3-第二批正文已完成">T2/T3 第二批正文（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LinkedIn</td>
          <td><a href="/blog/backend/06-reliability/cases/linkedin/automated-load-testing-and-capacity-forecasting/" data-link-title="LinkedIn：Automated Load Testing 與 Capacity Forecasting" data-link-desc="持續壓測驅動容量預測：用自動化回饋取代一次性壓測的容量規劃。">L2 Automated Load Testing 與 Capacity Forecasting</a></td>
          <td>持續壓測驅動容量預測</td>
      </tr>
      <tr>
          <td>Meta</td>
          <td><a href="/blog/backend/06-reliability/cases/meta/bgp-control-plane-recovery-ordering/" data-link-title="Meta：BGP 事故與控制面恢復順序" data-link-desc="當回復工具依賴已故障的系統：2021-10 事故揭露控制面恢復順序與 out-of-band 存取的設計約束。">M2 BGP 事故與控制面恢復順序</a></td>
          <td>回復工具依賴已故障系統的恢復困境</td>
      </tr>
      <tr>
          <td>Honeycomb</td>
          <td><a href="/blog/backend/06-reliability/cases/honeycomb/production-excellence-and-test-in-production/" data-link-title="Honeycomb：Production Excellence 與 Test in Production" data-link-desc="用 high-cardinality observability 把 production 變成安全的驗證環境：feature flag、progressive rollout 與即時回饋的配合。">HC2 Production Excellence 與 Test in Production</a></td>
          <td>observability-driven 生產驗證文化</td>
      </tr>
      <tr>
          <td>Microsoft</td>
          <td><a href="/blog/backend/06-reliability/cases/microsoft/safe-deployment-practices-and-resilience-patterns/" data-link-title="Microsoft：Safe Deployment Practices 與 Resilience Patterns" data-link-desc="大型 SaaS 用 ring-based deployment 控制變更擴散，用標準化 resilience patterns 讓依賴失效時的降級行為可預測。">MS2 Safe Deployment Practices 與 Resilience Patterns</a></td>
          <td>ring-based deployment 與韌性設計模式</td>
      </tr>
      <tr>
          <td>Spotify</td>
          <td><a href="/blog/backend/06-reliability/cases/spotify/backstage-service-catalog-and-reliability-metadata/" data-link-title="Spotify：Backstage Service Catalog 與 Reliability Metadata" data-link-desc="用 service catalog 治理分散團隊的可靠性資訊：ownership、SLO 狀態、依賴圖與 runbook 的單一入口。">SP2 Backstage Service Catalog 與 Reliability Metadata</a></td>
          <td>service catalog 治理可靠性資訊</td>
      </tr>
      <tr>
          <td>Pinterest</td>
          <td><a href="/blog/backend/06-reliability/cases/pinterest/storage-migration-and-data-infrastructure-reliability/" data-link-title="Pinterest：Storage Migration 與 Data Infrastructure Reliability" data-link-desc="大規模儲存遷移的可靠性設計：用 dual-write、shadow read 與 staged cutover 讓 PB 級資料基礎設施變更可漸進、可驗證、可回退。">P2 Storage Migration 與 Data Infrastructure Reliability</a></td>
          <td>大規模儲存遷移的驗證流程</td>
      </tr>
  </tbody>
</table>
<h2 id="t3-服務">T3 服務</h2>
<ul>
<li><a href="/blog/backend/06-reliability/cases/spotify/" data-link-title="Spotify" data-link-desc="Spotify Chaos Engineering 與 Squad-based SRE">spotify</a></li>
<li><a href="/blog/backend/06-reliability/cases/pinterest/" data-link-title="Pinterest" data-link-desc="Pinterest Capacity Planning 與儲存架構可靠性">pinterest</a></li>
<li><a href="/blog/backend/06-reliability/cases/meta/" data-link-title="Meta / Facebook" data-link-desc="Meta Reliability Engineering 與超大規模事故學習">meta</a></li>
</ul>
]]></content:encoded></item><item><title>可觀測性 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/</guid><description>&lt;p>可觀測性 Vendor 清單的核心責任是把工具名稱放回 telemetry contract、signal ownership、data quality、cardinality 與成本治理的判斷。每個服務頁先回答它承擔 metrics、logs、traces、errors、APM 或平台原生觀測的哪一段，再討論資料模型、查詢能力、成本與案例回寫。觀測這塊能力的買 vs 建特別現實：自建 telemetry stack（Prometheus、Grafana、Loki）、買 observability SaaS（Datadog、New Relic、Grafana Cloud），還是用雲端原生（CloudWatch、Cloud Monitoring）— 取捨與遷出代價見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>可觀測性服務要從訊號責任進入。讀者如果要建立 metrics baseline，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics&lt;/a>；如果要處理資料品質，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a>；如果要交付 evidence，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>可觀測性服務頁的教學順序是先建立 OpenTelemetry 標準入口，再比較 metrics / logs / traces backend、SaaS observability 與 cloud-native 工具。這個順序服務 E1-E7 所有 checkout episode：每個服務變更都要把訊號整理成 evidence package，讀者要先理解 signal quality，再進入 vendor 能力與成本模型。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry&lt;/a>&lt;/td>
 &lt;td>Standard / SDK&lt;/td>
 &lt;td>instrumentation、collector、semantic convention 如何降低 vendor lock-in&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a>&lt;/td>
 &lt;td>Metrics&lt;/td>
 &lt;td>pull model、PromQL、cardinality 與 retention 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>&lt;/td>
 &lt;td>OSS / Cloud stack&lt;/td>
 &lt;td>Grafana、Loki、Tempo、Mimir 如何組成可觀測性平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>SaaS APM&lt;/td>
 &lt;td>all-in-one APM、logs、traces、profiling 與成本治理如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a>&lt;/td>
 &lt;td>Search / logs&lt;/td>
 &lt;td>log search、index lifecycle、APM 與資料量成本如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a>&lt;/td>
 &lt;td>High-cardinality&lt;/td>
 &lt;td>event-based observability 與 high-cardinality 查詢如何支援除錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a>&lt;/td>
 &lt;td>AWS-native&lt;/td>
 &lt;td>AWS metrics、logs、alarms 與 account / region 邊界如何管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a>&lt;/td>
 &lt;td>GCP-native&lt;/td>
 &lt;td>Cloud Monitoring、Logging、Trace 與 GCP resource model 如何整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a>&lt;/td>
 &lt;td>Error tracking&lt;/td>
 &lt;td>error event、release、trace、session replay 如何連到 owner action&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性 Vendor 清單的核心責任是把工具名稱放回 telemetry contract、signal ownership、data quality、cardinality 與成本治理的判斷。每個服務頁先回答它承擔 metrics、logs、traces、errors、APM 或平台原生觀測的哪一段，再討論資料模型、查詢能力、成本與案例回寫。觀測這塊能力的買 vs 建特別現實：自建 telemetry stack（Prometheus、Grafana、Loki）、買 observability SaaS（Datadog、New Relic、Grafana Cloud），還是用雲端原生（CloudWatch、Cloud Monitoring）— 取捨與遷出代價見 <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>。</p>
<h2 id="讀法">讀法</h2>
<p>可觀測性服務要從訊號責任進入。讀者如果要建立 metrics baseline，先回到 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a>；如果要處理資料品質，先回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>；如果要交付 evidence，先回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>可觀測性服務頁的教學順序是先建立 OpenTelemetry 標準入口，再比較 metrics / logs / traces backend、SaaS observability 與 cloud-native 工具。這個順序服務 E1-E7 所有 checkout episode：每個服務變更都要把訊號整理成 evidence package，讀者要先理解 signal quality，再進入 vendor 能力與成本模型。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></td>
          <td>Standard / SDK</td>
          <td>instrumentation、collector、semantic convention 如何降低 vendor lock-in</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
          <td>Metrics</td>
          <td>pull model、PromQL、cardinality 與 retention 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></td>
          <td>OSS / Cloud stack</td>
          <td>Grafana、Loki、Tempo、Mimir 如何組成可觀測性平台</td>
      </tr>
      <tr>
          <td><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></td>
          <td>SaaS APM</td>
          <td>all-in-one APM、logs、traces、profiling 與成本治理如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
          <td>Search / logs</td>
          <td>log search、index lifecycle、APM 與資料量成本如何治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
          <td>High-cardinality</td>
          <td>event-based observability 與 high-cardinality 查詢如何支援除錯</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a></td>
          <td>AWS-native</td>
          <td>AWS metrics、logs、alarms 與 account / region 邊界如何管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a></td>
          <td>GCP-native</td>
          <td>Cloud Monitoring、Logging、Trace 與 GCP resource model 如何整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
          <td>Error tracking</td>
          <td>error event、release、trace、session replay 如何連到 owner action</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="aws-cloudwatch/">AWS CloudWatch</a></td>
          <td><a href="aws-cloudwatch/logs-insights-governance/">Logs Insights 治理</a> / <a href="aws-cloudwatch/alarms-composite-operations/">Alarms 與 Composite</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="datadog/">Datadog</a></td>
          <td><a href="datadog/cost-governance-agent-config/">成本治理與 Agent 配置</a> / <a href="datadog/otlp-ingestion-otel-integration/">OTLP Ingestion 與 OTel 整合</a></td>
          <td><a href="datadog/migrate-from-new-relic/">← New Relic</a> / <a href="datadog/migrate-to-grafana-stack/">→ Grafana Stack</a></td>
      </tr>
      <tr>
          <td><a href="elastic-stack/">Elastic Stack</a></td>
          <td><a href="elastic-stack/ilm-log-pipeline/">ILM 與 Log Pipeline</a></td>
          <td><a href="elastic-stack/migrate-to-elastic-cloud/">→ Elastic Cloud</a></td>
      </tr>
      <tr>
          <td><a href="gcp-cloud-operations/">GCP Cloud Ops</a></td>
          <td><a href="gcp-cloud-operations/cloud-monitoring-mql/">Monitoring MQL</a> / <a href="gcp-cloud-operations/cloud-logging-export-compliance/">Logging 匯出合規</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="grafana-stack/">Grafana Stack</a></td>
          <td><a href="grafana-stack/lgtm-stack-operations/">LGTM Stack Operations</a> / <a href="grafana-stack/loki-design-operational-limits/">Loki 設計與操作限制</a></td>
          <td><a href="grafana-stack/migrate-prometheus-to-cloud-metrics/">Prometheus → Cloud Metrics</a></td>
      </tr>
      <tr>
          <td><a href="honeycomb/">Honeycomb</a></td>
          <td><a href="honeycomb/high-cardinality-query-bubbleup/">High-Cardinality BubbleUp</a></td>
          <td><a href="honeycomb/migrate-from-sentry/">← Sentry</a></td>
      </tr>
      <tr>
          <td><a href="opentelemetry/">OpenTelemetry</a></td>
          <td><a href="opentelemetry/collector-deployment-patterns/">Collector 部署模式</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="prometheus/">Prometheus</a></td>
          <td><a href="prometheus/capacity-failure-modes/">容量規劃與故障模式</a> / <a href="prometheus/promql-recording-rules/">PromQL 與 Recording Rules</a> / <a href="prometheus/remote-write-long-term-storage/">Remote Write 與長期儲存</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="sentry/">Sentry</a></td>
          <td><a href="sentry/error-grouping-fingerprinting/">Error Grouping Fingerprinting</a> / <a href="sentry/release-tracking-session-replay/">Release Tracking Session Replay</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>進度（2026-06-23）：9 個 T1 vendor 全部有 deep article（共 21 篇）。OpenTelemetry 後續候選：Sampling 策略 / Auto-instrumentation。各 vendor 進階主題的更多 deep article 見各自 <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>可觀測性服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 signal standard、metrics、logs、traces、error tracking 還是 APM platform</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>cardinality、retention、debug speed、multi-cloud、compliance、成本哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>OSS stack、cloud-native、SaaS APM、specialized error tracking 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>instrumentation、agent、collector、index、retention、query cost、PII governance</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>dashboard、query link、trace sample、log sample、alert rule、data quality note</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>事故、capacity、release gate 與 cost attribution 如何回寫成 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>可觀測性服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 standard、metrics backend、log search、trace backend、APM 還是 error tracking</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷 signal ownership、data quality、cardinality、retention 與 cost</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「現在缺哪個訊號會阻止決策」快速判斷該看 metrics、logs、traces 或 errors</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>instrumentation、collector、agent、dashboard、alert、retention</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>OSS stack、SaaS APM、cloud-native、specialized tool 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>high-cardinality、sampling、multi-cloud、PII redaction、cost attribution</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>missing signal、label explosion、trace gap、log index cost、alert noise</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>標準化先用 OpenTelemetry、規模化 metrics 轉 managed backend、事故協作轉 08</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>每種語言 SDK 完整教學、dashboard 美術、所有 query cookbook</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 4.20 evidence package、9.8 performance observability、8 incident cases</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>橫向議題在不同 vendor 用不同 mechanism 達成。本表列同一議題在 9 個 vendor 的對應位置、確保大綱不缺漏、讀者跨 vendor 查找時有索引。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>OTel</th>
          <th>Prometheus</th>
          <th>Grafana Stack</th>
          <th>Datadog</th>
          <th>Elastic Stack</th>
          <th>Honeycomb</th>
          <th>CloudWatch</th>
          <th>Cloud Ops</th>
          <th>Sentry</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號類型</td>
          <td>全（標準）</td>
          <td>metrics</td>
          <td>全 stack</td>
          <td>全 + Security</td>
          <td>logs + APM</td>
          <td>events / traces</td>
          <td>全 AWS-native</td>
          <td>全 GCP-native</td>
          <td>errors + APM</td>
      </tr>
      <tr>
          <td>採集模式</td>
          <td>SDK + Collector</td>
          <td>Pull scrape</td>
          <td>mixed</td>
          <td>Agent push</td>
          <td>Beats / Agent</td>
          <td>SDK / OTLP</td>
          <td>Agent / native</td>
          <td>Agent / native</td>
          <td>SDK push</td>
      </tr>
      <tr>
          <td>查詢語言</td>
          <td>N/A</td>
          <td>PromQL</td>
          <td>PromQL/LogQL/TraceQL</td>
          <td>Datadog query</td>
          <td>KQL / ES DSL</td>
          <td>Honeycomb query</td>
          <td>Logs Insights</td>
          <td>Logs query</td>
          <td>Issue filter</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>由 backend 決定</td>
          <td>受限（series）</td>
          <td>Mimir / Loki 各自</td>
          <td>計費 per dim</td>
          <td>Mapping limit</td>
          <td>設計目標 (high)</td>
          <td>計費 per metric</td>
          <td>計費 per metric</td>
          <td>issue grouping</td>
      </tr>
      <tr>
          <td>部署模式</td>
          <td>OSS standard</td>
          <td>OSS self-host</td>
          <td>OSS / Cloud</td>
          <td>SaaS only</td>
          <td>OSS / Cloud</td>
          <td>SaaS only</td>
          <td>AWS managed</td>
          <td>GCP managed</td>
          <td>OSS / SaaS</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>取決 backend</td>
          <td>self-host CapEx</td>
          <td>self-host / Cloud</td>
          <td>hosts + signals</td>
          <td>self-host</td>
          <td>events volume</td>
          <td>ingestion + API</td>
          <td>ingestion + API</td>
          <td>events volume</td>
      </tr>
      <tr>
          <td>多雲 / 跨平台</td>
          <td>是（標準）</td>
          <td>是 (OSS)</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td>AWS-only</td>
          <td>GCP-only</td>
          <td>是</td>
      </tr>
      <tr>
          <td>OTel 相容度</td>
          <td>原生</td>
          <td>exporter</td>
          <td>OTLP receiver</td>
          <td>OTLP ingestion</td>
          <td>OTLP ES 7.16+</td>
          <td>OTLP 原生</td>
          <td>ADOT</td>
          <td>OTLP Trace 2.0+</td>
          <td>OTel context</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>C2/C3/C4/C5/C8</td>
          <td>C1/C6/C7</td>
          <td>C6/C11</td>
          <td>C5</td>
          <td>C5/C6</td>
          <td>C7</td>
          <td>C1/C8</td>
          <td>C3</td>
          <td>待補</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題是否有對應的進階主題子段</li>
<li>讀者選型時、知道對應 mechanism 在不同 vendor 的形態</li>
<li>評估遷移風險：訊號類型 + 部署模式 + OTel 相容度三維度合併判讀</li>
</ul>
<p>下面 8 段把對照表的每行展開、避免裸表格成為終點。</p>
<h3 id="訊號類型">訊號類型</h3>
<p>訊號類型決定 vendor 解決哪一段觀測問題。<strong>OpenTelemetry</strong> 是 standard、覆蓋 traces / metrics / logs；<strong>Prometheus</strong> 純 metrics；<strong>Grafana Stack</strong> 全 stack（各 backend 各司其職、Loki + Tempo + Mimir + Pyroscope）；<strong>Datadog</strong> 全 + Security + RUM + CI；<strong>Elastic Stack</strong> logs 為主 + APM；<strong>Honeycomb</strong> events-based（不是 metrics aggregation）；<strong>CloudWatch / Cloud Operations</strong> 雲原生全 stack（含 traces / profiler）；<strong>Sentry</strong> 專精 error tracking + 簡易 APM。</p>
<p>選型判讀：缺哪個訊號 → 補對應 vendor；想 turnkey 全棧 → Datadog / cloud-native；想 OSS 全棧 → Grafana Stack；error tracking 已有 → Sentry / Bugsnag 補強。</p>
<h3 id="採集模式">採集模式</h3>
<p>採集模式影響部署複雜度跟 instrumentation 工作量。<strong>OTel</strong> 是 SDK + Collector 兩層；<strong>Prometheus</strong> 是 pull scrape（service discovery）；<strong>Grafana Stack</strong> 各 backend 模式不同（Loki push / Tempo OTLP / Mimir remote write）；<strong>Datadog</strong> Agent push；<strong>Elastic</strong> Beats / Logstash / Agent；<strong>Honeycomb</strong> SDK push 或 OTLP；<strong>CloudWatch / Cloud Ops</strong> 雲服務內建 + Agent；<strong>Sentry</strong> SDK push。</p>
<p>選型判讀：服務在 K8s + 想自管 → Prometheus pull + Operator；應用層 push → OTel SDK + Collector；不想配 instrumentation → Datadog / cloud-native 自動。</p>
<h3 id="查詢語言">查詢語言</h3>
<p>查詢語言差異影響 dashboard / alert 設計成本。<strong>Prometheus PromQL</strong>（業界 metrics query 標準）；<strong>Grafana</strong> 支援 PromQL（Mimir）/ LogQL（Loki）/ TraceQL（Tempo）；<strong>Datadog</strong> 自家 query syntax；<strong>Elastic</strong> KQL / Lucene / ES DSL / ES|QL；<strong>Honeycomb</strong> point-and-click + 簡單 query；<strong>CloudWatch</strong> Logs Insights syntax；<strong>Cloud Ops</strong> 類似但 GCP-specific；<strong>Sentry</strong> 是 issue filter、不算 query language。</p>
<p>選型判讀：跨 vendor 統一 → 學 PromQL + LogQL（Grafana 通用）；vendor-specific → 依該 vendor 學；OTel 不解決 query 問題（純 instrumentation 標準）。</p>
<h3 id="cardinality-處理">Cardinality 處理</h3>
<p>Cardinality 是 observability 成本跟可用性的關鍵。<strong>Prometheus</strong> 受限（series 爆炸會 OOM）；<strong>Datadog</strong> custom metrics 計費 per dimension；<strong>CloudWatch / Cloud Ops</strong> metrics 計費 per metric；<strong>Elastic</strong> mapping field limit；<strong>Honeycomb</strong> 設計目標就是 high-cardinality（events-based）；<strong>Grafana Stack</strong> Mimir 多 tenant 各自 cardinality budget；<strong>Sentry</strong> 用 issue grouping 替代 cardinality 概念。</p>
<p>選型判讀：high-cardinality 是核心需求（per-user / per-request debug）→ Honeycomb；中等 cardinality + 成本敏感 → Prometheus + 設計謹慎；任意 cardinality + 計費承擔 → Datadog。</p>
<h3 id="部署模式">部署模式</h3>
<p>部署模式決定運維責任歸屬。<strong>OTel</strong> 是 standard、各 backend 各自部署；<strong>Prometheus</strong> OSS self-host；<strong>Grafana Stack</strong> OSS self-host / Grafana Cloud；<strong>Datadog / Honeycomb / Sentry</strong> SaaS（Sentry 有 self-host OSS）；<strong>Elastic</strong> OSS / Elastic Cloud / OpenSearch fork；<strong>CloudWatch / Cloud Ops</strong> 雲原生 managed。</p>
<p>選型判讀：要極致控制 → self-host OSS；不想運維 → SaaS（Datadog / Honeycomb / Sentry）；已在 AWS / GCP → 雲原生 + 補強；混合模式 → OTel 抽象層 + 多 backend。</p>
<h3 id="成本模型">成本模型</h3>
<p>成本模型差異大、容易誤判。<strong>OTel</strong> 本身無成本、取決下游 backend；<strong>Prometheus</strong> self-host CapEx（compute + storage）；<strong>Grafana Stack</strong> self-host CapEx 或 Grafana Cloud OpEx；<strong>Datadog</strong> hosts + signal 各自計費（容易堆疊）；<strong>Elastic</strong> self-host CapEx 或 Elastic Cloud；<strong>Honeycomb</strong> events volume；<strong>CloudWatch / Cloud Ops</strong> ingestion + API call；<strong>Sentry</strong> events / users / replays 計費。</p>
<p>選型判讀：可預期固定成本 → self-host（CapEx）；流量不穩 → SaaS（OpEx + 預警）；多訊號類型 → Datadog 容易爆、Honeycomb 計費單純；AWS / GCP-only 場景 → 雲原生通常 cheaper than 第三方 SaaS。</p>
<h3 id="多雲--跨平台">多雲 / 跨平台</h3>
<p>多雲決定 vendor 鎖定風險。<strong>OTel</strong> 是抽象層、最不 lock-in；<strong>Prometheus / Grafana Stack / Elastic / Datadog / Honeycomb / Sentry</strong> 都支援多雲；<strong>CloudWatch</strong> AWS-only；<strong>Cloud Ops</strong> GCP-only；<strong>Azure Monitor</strong> Azure-only（T2 候選）。</p>
<p>選型判讀：多雲 → 避免 AWS / GCP-only vendor、用 Datadog / Grafana Stack / OTel + multi-backend；單一雲 → 雲原生通常成本最低；既有混合 → OTel 標準化 + 漸進遷移。</p>
<h3 id="otel-相容度">OTel 相容度</h3>
<p>OTel 相容度影響 vendor 切換成本。各 vendor 接受程度：</p>
<ul>
<li>完全相容（drop-in）：Honeycomb / Grafana Tempo / Cloud Trace（2.0+）</li>
<li>接受但 feature 落後 vendor SDK：Datadog / CloudWatch（X-Ray 整合）/ Elastic APM</li>
<li>跟 OTel 互補但設計不同：Prometheus（exporter pattern）/ Sentry（OTel context）</li>
</ul>
<p>選型判讀：未來想換 vendor → 從 day 1 用 OTel SDK；不換 vendor → vendor SDK 較深；多 backend dual ship → OTel 幾乎是唯一可行路徑。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>O1</td>
          <td>OpenTelemetry</td>
          <td>建立 instrumentation standard、collector 與 vendor portability</td>
      </tr>
      <tr>
          <td>O2</td>
          <td>Prometheus / Grafana Stack</td>
          <td>建立 metrics baseline、cardinality 與 OSS platform 判準</td>
      </tr>
      <tr>
          <td>O3</td>
          <td>Elastic Stack / Datadog / Honeycomb / Sentry</td>
          <td>建立 logs / APM / high-cardinality / error tracking 對照</td>
      </tr>
      <tr>
          <td>O4</td>
          <td>AWS CloudWatch / GCP Cloud Operations</td>
          <td>建立 cloud-native observability 與 account / project 邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise APM</td>
          <td>New Relic、Dynatrace、Splunk Observability</td>
          <td>SaaS APM、enterprise workflow、成本治理</td>
      </tr>
      <tr>
          <td>OSS / Hybrid</td>
          <td>SigNoz、Chronosphere、VictoriaMetrics、Thanos、Cortex</td>
          <td>Prometheus scale、managed metrics、OpenTelemetry ingestion</td>
      </tr>
      <tr>
          <td>Tracing</td>
          <td>Jaeger、OpenSearch Observability</td>
          <td>trace backend、OpenTelemetry-native ingestion、log correlation</td>
      </tr>
      <tr>
          <td>Logs / pipeline</td>
          <td>Fluent Bit、Fluentd、Vector、OpenSearch</td>
          <td>log shipping、filtering、index lifecycle、cost</td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td>Bugsnag、Rollbar、Raygun</td>
          <td>release health、frontend / backend error ownership</td>
      </tr>
      <tr>
          <td>Cloud-native</td>
          <td>Azure Monitor</td>
          <td>Azure resource model、Log Analytics、cost boundary</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 instrumentation、metrics、logs、traces、APM 與 error tracking。OpenTelemetry 是標準入口，Prometheus / Thanos / Cortex / VictoriaMetrics 是 metrics 路線，Loki / OpenSearch / Elastic 是 logs / search 路線，Jaeger / Tempo 是 tracing 路線，Datadog / New Relic / Dynatrace / Splunk 是 SaaS APM 路線。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>上游：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>服務路徑：<a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package 實作示範</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></li>
</ul>
]]></content:encoded></item><item><title>快取 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/</guid><description>&lt;p>快取 Vendor 清單的核心責任是把 cache 服務名稱放回副本語意、資料新鮮度、回源保護與操作成本的判斷。每個服務頁先回答它承擔哪種暫存責任，再討論資料型別、失效策略、容量模型、HA / managed 邊界與案例回寫。在挑單一服務之前先有一個更上層的判斷：這塊快取能力該自管 Redis、用 managed cache（ElastiCache、MemoryDB）、還是用 serverless cache（Upstash）或含 cache 的 BaaS bundle — 逐能力的買 vs 建判讀見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>快取服務要從資料生命週期進入。讀者如果要保護資料庫讀取壓力，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside&lt;/a>；如果要判斷 TTL 與淘汰，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction&lt;/a>；如果服務已經把 cache 當主要 serving layer，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>快取服務頁的教學順序是先建立 Redis / Valkey baseline，再比較 Memcached、DragonflyDB 與 managed cache。這個順序對齊 checkout E2：讀者先理解可重建副本、新鮮度與回源保護，再比較同類服務如何改變相容性、memory model、failover 與 managed operation。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>&lt;/td>
 &lt;td>Data structure cache&lt;/td>
 &lt;td>data types、persistence、cluster 與授權變動如何影響選型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>&lt;/td>
 &lt;td>Redis-compatible&lt;/td>
 &lt;td>Redis 相容性、開源治理與 managed ecosystem 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>&lt;/td>
 &lt;td>Simple KV cache&lt;/td>
 &lt;td>純快取、低語意與水平擴張如何降低操作成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>&lt;/td>
 &lt;td>Redis-compatible&lt;/td>
 &lt;td>多核心架構、相容性與高吞吐 cache workload 如何評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>&lt;/td>
 &lt;td>Managed cache&lt;/td>
 &lt;td>managed Redis / Valkey / Memcached 如何轉移維運責任&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>快取 Vendor 清單的核心責任是把 cache 服務名稱放回副本語意、資料新鮮度、回源保護與操作成本的判斷。每個服務頁先回答它承擔哪種暫存責任，再討論資料型別、失效策略、容量模型、HA / managed 邊界與案例回寫。在挑單一服務之前先有一個更上層的判斷：這塊快取能力該自管 Redis、用 managed cache（ElastiCache、MemoryDB）、還是用 serverless cache（Upstash）或含 cache 的 BaaS bundle — 逐能力的買 vs 建判讀見 <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>。</p>
<h2 id="讀法">讀法</h2>
<p>快取服務要從資料生命週期進入。讀者如果要保護資料庫讀取壓力，先回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>；如果要判斷 TTL 與淘汰，先回到 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>；如果服務已經把 cache 當主要 serving layer，先回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>快取服務頁的教學順序是先建立 Redis / Valkey baseline，再比較 Memcached、DragonflyDB 與 managed cache。這個順序對齊 checkout E2：讀者先理解可重建副本、新鮮度與回源保護，再比較同類服務如何改變相容性、memory model、failover 與 managed operation。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></td>
          <td>Data structure cache</td>
          <td>data types、persistence、cluster 與授權變動如何影響選型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
          <td>Redis-compatible</td>
          <td>Redis 相容性、開源治理與 managed ecosystem 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
          <td>Simple KV cache</td>
          <td>純快取、低語意與水平擴張如何降低操作成本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
          <td>Redis-compatible</td>
          <td>多核心架構、相容性與高吞吐 cache workload 如何評估</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></td>
          <td>Managed cache</td>
          <td>managed Redis / Valkey / Memcached 如何轉移維運責任</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="redis/">Redis</a></td>
          <td><a href="redis/memory-eviction-tuning/">memory-eviction-tuning</a> / <a href="redis/persistence-fork-latency/">persistence-fork-latency</a> / <a href="redis/sentinel-ha-failover/">sentinel-ha-failover</a> / <a href="redis/connection-pipeline-latency/">connection-pipeline-latency</a> / <a href="redis/cluster-resharding/">cluster-resharding</a></td>
          <td><a href="redis/migrate-to-valkey/">→ Valkey</a> / <a href="redis/migrate-to-dragonflydb/">→ DragonflyDB</a> / <a href="redis/migrate-to-memcached/">→ Memcached</a> / <a href="redis/migrate-to-elasticache/">→ ElastiCache</a></td>
      </tr>
      <tr>
          <td><a href="valkey/">Valkey</a></td>
          <td><a href="valkey/redis-compatibility-and-io-threads/">redis-compatibility-and-io-threads</a></td>
          <td>（沿用 Redis → ElastiCache：自管 Valkey 同路徑）</td>
      </tr>
      <tr>
          <td><a href="memcached/">Memcached</a></td>
          <td><a href="memcached/slab-allocator-memory-economics/">slab-allocator-memory-economics</a></td>
          <td><a href="memcached/migrate-to-redis/">→ Redis</a></td>
      </tr>
      <tr>
          <td><a href="dragonflydb/">DragonflyDB</a></td>
          <td><a href="dragonflydb/shared-nothing-multicore-architecture/">shared-nothing-multicore-architecture</a></td>
          <td><a href="dragonflydb/migrate-to-redis/">→ Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="aws-elasticache/">AWS ElastiCache</a></td>
          <td><a href="aws-elasticache/managed-responsibility-boundary/">managed-responsibility-boundary</a></td>
          <td><a href="aws-elasticache/migrate-to-self-managed/">→ 自管 Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="keydb/">KeyDB</a></td>
          <td><a href="keydb/active-active-replication/">active-active-replication</a></td>
          <td><a href="keydb/migrate-to-redis/">→ Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="momento/">Momento</a></td>
          <td>overview-only（見下方註）</td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="caffeine/">Caffeine</a></td>
          <td><a href="caffeine/two-tier-cache-invalidation/">two-tier-cache-invalidation</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>備註：<a href="redis/cluster-resharding/">cluster-resharding</a> 是同 cluster 的 topology 重劃（5 type migration 漏類驗證、形式上歸在 deep article 欄、不是跨 vendor 遷移）。</p>
<p>Momento overview-only 的理由：Momento 是 serverless cache、實作面（無 server 參數、無容量規劃、無 cluster topology）相對薄；本 blog case 庫無 Momento production case、且 SaaS 無法本機驗證。依 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">deep article 方法論</a> 反向判準（無 production 經驗 / case 支撐的純 spec 復述不該寫 deep article），Momento 維持 overview-only、待有 case 或 serverless cost 實證再評估。</p>
<p>進度（2026-06-22）：8 個 vendor 的 deep-article 層收尾完成。Migration playbook 覆蓋更新：ElastiCache → 自管 Redis/Valkey、DragonflyDB → Redis/Valkey、KeyDB → Redis/Valkey 三條回退路徑已補齊。目前只剩 Momento（overview-only、無 migration 需求）跟 Caffeine（local cache、無跨 vendor migration 概念）的 migration 欄為空白、屬設計決策。剩餘獨立 track：各 T1 vendor 進階主題的更多 deep article（Redis distributed lock / modules、Memcached CAS、ElastiCache Global Datastore DR）、後續候選的 Garnet / Hazelcast / Aerospike / Varnish edge cache。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>快取服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 cache copy、data structure、presence、counter 還是 managed operation</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>hot key、read QPS、origin cost、latency、multi-region、memory cost 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>同類 Redis 相容服務、Memcached、managed cache、local cache 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>memory sizing、eviction、backup、failover、cluster upgrade、client compatibility</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>hit rate、miss rate、origin QPS、stale read、eviction、hot key、replication lag</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Meta、Shopify、Netflix、Cloudflare、Tinder、Tubi、Snap 案例如何提供判準</td>
      </tr>
  </tbody>
</table>
<p>服務責任段要先分辨副本與正式狀態。Redis、Valkey、DragonflyDB 與 ElastiCache 都可能承擔 cache serving layer，但資料是否可重建、stale window 多長、回源壓力是否受控，才是選型判斷的起點。</p>
<p>適用壓力段要保留 workload 語言。商品詳情、session、presence、rate limit、leaderboard、ML feature store 與 edge cache 的資料形狀不同，服務頁要用各自的 freshness、memory、QPS 與回退條件寫。</p>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>快取服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 data structure cache、simple KV、managed cache、local cache 還是 HTTP cache</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷資料是否可重建、stale window、origin protection 與 memory cost</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「miss 打回 origin 是否可承受」快速判斷是否能引入或擴大快取</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>key design、TTL、eviction、warmup、failover、client timeout</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>Redis 相容服務、Memcached、managed cache、local cache 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>cluster、persistence、multi-region、serverless cache、module / data type</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>hit rate、miss rate、origin QPS、hot key、eviction、replication lag</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>durable workflow 轉 queue、正式狀態轉 database、全文查詢轉 search</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>Redis command 百科、語言 client API 細節、完整調參手冊</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 2.C cases、9.C cache capacity cases、4.20 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>橫向議題在不同 vendor 用不同 mechanism 達成。本表列同一議題在 5 個 vendor 的對應位置、確保大綱不缺漏、讀者跨 vendor 查找時有索引。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>Redis</th>
          <th>Valkey</th>
          <th>Memcached</th>
          <th>DragonflyDB</th>
          <th>AWS ElastiCache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Redis API 相容度</td>
          <td>原生（最高）</td>
          <td>100%（fork 7.2.4）</td>
          <td>不相容（純 KV）</td>
          <td>高（少數 commands 不支援）</td>
          <td>Engine 決定（Redis/Valkey 100%、Memcached 不適用）</td>
      </tr>
      <tr>
          <td>Data types</td>
          <td>6 大 + Stream / Geo</td>
          <td>跟 Redis 一致</td>
          <td>純 string KV</td>
          <td>跟 Redis 一致</td>
          <td>跟 engine 一致</td>
      </tr>
      <tr>
          <td>多核 / 多執行緒</td>
          <td>I/O threads（main 仍單線）</td>
          <td>Valkey 8 強化 async I/O threading（超出 Redis）</td>
          <td>原生多執行緒</td>
          <td>完全 shared-nothing 多核</td>
          <td>跟 engine 一致</td>
      </tr>
      <tr>
          <td>Cluster mode</td>
          <td>Cluster + Sentinel</td>
          <td>跟 Redis 一致</td>
          <td>Client-side ketama hashing</td>
          <td>Single instance scale-up（無 Cluster）</td>
          <td>Cluster mode enabled/disabled</td>
      </tr>
      <tr>
          <td>持久化策略</td>
          <td>AOF + RDB</td>
          <td>跟 Redis 一致</td>
          <td>無持久化</td>
          <td>Fork-less snapshot</td>
          <td>Automatic + manual snapshot</td>
      </tr>
      <tr>
          <td>跨 AZ / 多 region</td>
          <td>Sentinel + replication / Cluster geo</td>
          <td>跟 Redis 一致</td>
          <td>需 Mcrouter / EVCache 等代理</td>
          <td>Replica 模式</td>
          <td>Multi-AZ + Global Datastore</td>
      </tr>
      <tr>
          <td>授權模式</td>
          <td>RSALv2 / SSPL（非 OSI）</td>
          <td>BSD 3-clause（OSI）</td>
          <td>BSD（OSI）</td>
          <td>BSL（4 年後轉 Apache 2.0）</td>
          <td>AWS managed pricing</td>
      </tr>
      <tr>
          <td>Managed level</td>
          <td>自管</td>
          <td>自管 / managed Valkey 可選</td>
          <td>自管</td>
          <td>自管（無 Dragonfly managed）</td>
          <td>Fully managed</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>2.C1-C8（跨 Meta / Netflix / Shopify）</td>
          <td>待補（fork 較新）</td>
          <td>2.C4 Mcrouter / 2.C5 EVCache</td>
          <td>待補（採用較新）</td>
          <td>2.C5 EVCache / 2.C8 Shopify</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題是否都有對應的進階主題子段</li>
<li>讀者在 vendor 間遷移時、知道對應 mechanism 在另一個 vendor 叫什麼</li>
<li>評估遷移風險：相容度 + 授權 + 生態三維度合併判讀</li>
</ul>
<p>下面 8 段把對照表的每行展開、避免裸表格成為終點。</p>
<h3 id="redis-api-相容度">Redis API 相容度</h3>
<p>API 相容度決定 client / 工具 / module 是否能直接遷移。<strong>Redis</strong> 是 reference 實作；<strong>Valkey</strong> 100% 相容（直接 drop-in、所有 client library 可用）；<strong>DragonflyDB</strong> 相容核心 commands 但部分 module / Lua 行為差異、不支援 Redis Cluster mode；<strong>Memcached</strong> 跟 Redis 完全不相容（protocol 不同、無 data types）；<strong>ElastiCache</strong> 取決於 engine（Redis / Valkey 100%、Memcached 是另一條線）。</p>
<p>選型判讀：既有 Redis 部署遷移 → Valkey 最低風險；要 scale up single instance → DragonflyDB 可評估但確認 module 跟 Cluster mode 影響；純 KV 從 Redis 改 Memcached → 等同重寫（不是相容問題、是 capability 差異）。</p>
<h3 id="data-types">Data types</h3>
<p>Data types 影響可用場景。<strong>Redis / Valkey</strong> 提供 string / hash / list / set / sorted set / stream / hyperloglog / geo — leaderboard / session / counter / distributed lock 等都有原生支援；<strong>Memcached</strong> 純 string KV — 任何複雜結構要在 application 層自己處理（serialize JSON 等）；<strong>DragonflyDB</strong> 跟 Redis 一致；<strong>ElastiCache</strong> 取決於 engine。</p>
<p>選型判讀：需要 sorted set / streams / hash → Redis 系列；純 cache GET/SET → Memcached 更輕；想用 Redis data types 但要極高 throughput → DragonflyDB。</p>
<h3 id="多核--多執行緒">多核 / 多執行緒</h3>
<p>多核利用度差異大。<strong>Redis</strong> 主執行緒 + I/O threads（Redis 6+）— main thread 仍處理所有 command；<strong>Valkey</strong> 8.x 強化 async I/O threading、把更多 I/O 路徑非同步化、多核吞吐超出 Redis（這是 Valkey fork 後第一個實質技術分歧、見 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey deep article</a>）；<strong>Memcached</strong> 原生 multi-threaded（<code>-t</code> 指定 thread 數）— 適合多核機器；<strong>DragonflyDB</strong> 完全 shared-nothing 多核 — 官方宣稱比 Redis 高 25× throughput（依 workload、以官方 benchmark 為準）；<strong>ElastiCache</strong> 取決於 engine、不能改變。</p>
<p>選型判讀：單 instance 想充分利用 16+ core → DragonflyDB / Memcached；4-8 core 中等場景 → Redis 加 I/O threads 已夠；需要 Redis API + 高 throughput → DragonflyDB 是 sweet spot。</p>
<h3 id="cluster-mode">Cluster mode</h3>
<p>擴展拓樸不同。<strong>Redis</strong> Cluster mode（16384 hash slot、可加減 shard）跟 Sentinel mode（HA 無 sharding）；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 沒有 server-side cluster、靠 client-side consistent hashing（ketama）；<strong>DragonflyDB</strong> 完全沒有 Cluster mode — 哲學是「single instance 撐到很大規模」；<strong>ElastiCache</strong> 提供 Cluster mode enabled / disabled 兩選項、disabled 上限 ~340GB。</p>
<p>選型判讀：超 single instance 容量 → Redis Cluster / ElastiCache Cluster enabled；HA 但容量在單 master → Redis Sentinel / ElastiCache disabled；scale up 機制 → DragonflyDB；極簡 client-side sharding → Memcached。</p>
<h3 id="持久化策略">持久化策略</h3>
<p>cache 是否需要持久化、view 不一。<strong>Redis</strong> AOF（append-only）+ RDB（snapshot）+ 混合模式；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 無持久化 — 重啟即 cold cache（嚴格 cache 哲學）；<strong>DragonflyDB</strong> fork-less snapshot（大記憶體場景比 Redis fork 高效）；<strong>ElastiCache</strong> 自動 snapshot + manual snapshot、跨 region 複製。</p>
<p>選型判讀：cache warmup 後不想全失 → Redis AOF / Valkey；純 cache 接受 cold start → Memcached；大記憶體 + snapshot 頻繁 → DragonflyDB fork-less；managed snapshot 不想處理 → ElastiCache。</p>
<h3 id="跨-az--多-region">跨 AZ / 多 region</h3>
<p>HA 拓樸三類。<strong>Redis</strong> Sentinel + replication（單 region 多 AZ）/ Cluster geo replication（規劃中）；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 沒有原生 — 靠 Mcrouter / EVCache 等代理做跨 AZ；<strong>DragonflyDB</strong> Replica 模式（primary-replica）跨 AZ 可行、跨 region 需自建；<strong>ElastiCache</strong> Multi-AZ replica（內建）+ Global Datastore（跨 region active-passive）。</p>
<p>選型判讀：自管跨 AZ → Redis Sentinel / Valkey；自管跨 region → Mcrouter 或自建；不想處理跨區 → ElastiCache Multi-AZ + Global Datastore。</p>
<h3 id="授權模式">授權模式</h3>
<p>授權直接影響商業使用權利。<strong>Redis</strong> 2024 起 RSALv2 / SSPL（非 OSI 認可）— SaaS 提供 Redis-as-service 受限；<strong>Valkey</strong> BSD 3-clause（OSI 認可）— 商業使用無限制；<strong>Memcached</strong> BSD（OSI）— 開源無限制；<strong>DragonflyDB</strong> BSL（Business Source License）— 4 年後轉 Apache 2.0、目前商業 managed service 提供受限；<strong>ElastiCache</strong> AWS managed pricing — 跟 license 無關（你付的是 AWS 服務費）。</p>
<p>選型判讀：開源合規敏感（公部門 / 企業政策）→ Valkey / Memcached；新部署不在乎 license → Redis / DragonflyDB；不想處理 license → ElastiCache（AWS 處理）。</p>
<h3 id="managed-level">Managed level</h3>
<p>運維責任轉移程度。<strong>Redis / Valkey</strong> 自管或選 managed（ElastiCache / Memorystore / Azure Cache）；<strong>Memcached</strong> 自管或 ElastiCache；<strong>DragonflyDB</strong> 目前只能自管（無 fully managed offering）；<strong>ElastiCache</strong> 完全 managed（auto failover / snapshot / patching）— 付 managed premium。</p>
<p>選型判讀：team 沒運維 Redis 經驗 → managed（ElastiCache / Memorystore）；要極致控制 → 自管；DragonflyDB 必自管（無 managed）。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>C1</td>
          <td>Redis / Valkey</td>
          <td>建立 Redis baseline、開源治理與相容性判準</td>
      </tr>
      <tr>
          <td>C2</td>
          <td>Memcached</td>
          <td>建立 simple KV cache、低語意副本與水平擴張邊界</td>
      </tr>
      <tr>
          <td>C3</td>
          <td>DragonflyDB / AWS ElastiCache</td>
          <td>建立高吞吐 Redis-compatible 與 managed cache 的操作取捨</td>
      </tr>
      <tr>
          <td>C4</td>
          <td>KeyDB / Momento / Caffeine</td>
          <td>補 multi-threaded fork、serverless cache、local cache 對照（overview 完成 2026-06-16）</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<p>C4 已建立 <a href="keydb/">KeyDB</a> / <a href="momento/">Momento</a> / <a href="caffeine/">Caffeine</a> overview。剩餘候選：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Redis fork / compat</td>
          <td>Garnet（Microsoft）</td>
          <td>相容性、multi-threading、client behavior</td>
      </tr>
      <tr>
          <td>Managed cache</td>
          <td>Azure Cache for Redis、GCP Memorystore</td>
          <td>managed SLA、vendor boundary</td>
      </tr>
      <tr>
          <td>Distributed cache</td>
          <td>Hazelcast、Aerospike</td>
          <td>cluster memory、near-cache、durability boundary</td>
      </tr>
      <tr>
          <td>Local cache</td>
          <td>Guava Cache、Ehcache（off-heap）</td>
          <td>process-local cache、invalidation、memory pressure</td>
      </tr>
      <tr>
          <td>HTTP / edge cache</td>
          <td>Varnish、Cloudflare Cache、Fastly、CloudFront</td>
          <td>edge TTL、origin protection、purge workflow</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是把 cache 分成 process-local、service-local、distributed 與 edge 四層。Redis 系列 / KeyDB / DragonflyDB 解 service-local / distributed data structure cache；<a href="caffeine/">Caffeine</a> 解 process-local、<a href="momento/">Momento</a> 解 serverless cache；Varnish、Cloudflare、Fastly、CloudFront 解 HTTP / edge cache；Hazelcast、Aerospike 解更重的 distributed data / cache 邊界。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
<li>上游：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary</a></li>
<li>案例：<a href="/blog/backend/02-cache-redis/cases/" data-link-title="模組二案例正文" data-link-desc="快取策略與快取平台演進案例入口。">2.C 快取案例正文</a></li>
<li>服務路徑：<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a></li>
</ul>
]]></content:encoded></item><item><title>事故處理服務案例庫</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/cases/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/cases/</guid><description>&lt;p>本案例庫以服務為單位、收錄公開事故報告（post-mortem / status page / 工程部落格）。每個服務一個資料夾，累積該服務的架構脈絡、事故時間線與共通失敗模式。&lt;/p>
&lt;p>服務分層依 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">模組八 _index&lt;/a> 的 T1 / T2 / T3 規劃。重複出現於 06 / 08 的服務（stripe / cloudflare / linkedin）資料夾住在主要教學模組、跨模組以連結互通。&lt;/p>
&lt;h2 id="完成狀態">完成狀態&lt;/h2>
&lt;p>案例庫的完成狀態以「可直接引用的事故頁」為準。服務資料夾只算索引，子案例頁才算可引用素材；每篇子案例至少要有事故摘要、判讀訊號、事故路徑、可回寫控制面、下一步路由與引用源。&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>Cloudflare&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal&lt;/a>&lt;/td>
 &lt;td>已回寫 4.21 / 6.24&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS S3&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption&lt;/a>&lt;/td>
 &lt;td>補 2021 多服務退化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitHub&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident&lt;/a>&lt;/td>
 &lt;td>補 2020 Actions 案例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GCP&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident&lt;/a>&lt;/td>
 &lt;td>補 IAM 控制面案例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atlassian&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage&lt;/a>&lt;/td>
 &lt;td>補次級事故對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Roblox&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage&lt;/a>&lt;/td>
 &lt;td>補恢復後優化案例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fastly&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage&lt;/a>&lt;/td>
 &lt;td>補後續改善案例&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t2t3-第一批正文已完成">T2/T3 第一批正文（已完成）&lt;/h2>
&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>Slack&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/" data-link-title="Slack：2022 連線恢復與狀態通訊節奏" data-link-desc="在通訊平台自身失效時，如何同步恢復節奏與對外狀態揭露。">SL1 連線恢復與狀態通訊&lt;/a>&lt;/td>
 &lt;td>通訊平台失效時的對外節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Datadog&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/" data-link-title="Datadog：2023 多區觀測中斷事件" data-link-desc="監控平台自身退化時，如何避免客戶誤判系統健康狀態。">DD1 多區觀測中斷&lt;/a>&lt;/td>
 &lt;td>監控平台失效的二階風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Discord&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">DC1 Gateway 容量事件&lt;/a>&lt;/td>
 &lt;td>長連線回復造成的二次擁塞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Azure AD&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">AZ1 身分控制面中斷&lt;/a>&lt;/td>
 &lt;td>跨產品身份依賴分級治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Heroku&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">HR1 Routing 控制事件&lt;/a>&lt;/td>
 &lt;td>多租戶入口故障的局部化回復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reddit&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">RD1 Kubernetes 升級事故&lt;/a>&lt;/td>
 &lt;td>平台升級與回退決策治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Microsoft 365&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/" data-link-title="Microsoft 365：套件級身分驗證事故" data-link-desc="企業套件在身份依賴失效時，如何同步處理跨產品影響與對外揭露。">M365-1 套件級身份事故&lt;/a>&lt;/td>
 &lt;td>企業套件跨產品影響盤點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="t1-服務">T1 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">aws-s3&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">github&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/" data-link-title="Google Cloud Platform" data-link-desc="GCP 重大事故時間線與架構脈絡">gcp&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">atlassian&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">roblox&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/fastly/" data-link-title="Fastly" data-link-desc="Fastly 全球配置 push 事故時間線">fastly&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="t2-服務">T2 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">slack&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">datadog&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe（住於 06）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">discord&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">azure-ad&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="t3-服務">T3 服務&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">heroku&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin（住於 06）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/reddit/" data-link-title="Reddit" data-link-desc="Reddit Pi Day 2023 k8s 升級事故">reddit&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">microsoft-365&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本案例庫以服務為單位、收錄公開事故報告（post-mortem / status page / 工程部落格）。每個服務一個資料夾，累積該服務的架構脈絡、事故時間線與共通失敗模式。</p>
<p>服務分層依 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">模組八 _index</a> 的 T1 / T2 / T3 規劃。重複出現於 06 / 08 的服務（stripe / cloudflare / linkedin）資料夾住在主要教學模組、跨模組以連結互通。</p>
<h2 id="完成狀態">完成狀態</h2>
<p>案例庫的完成狀態以「可直接引用的事故頁」為準。服務資料夾只算索引，子案例頁才算可引用素材；每篇子案例至少要有事故摘要、判讀訊號、事故路徑、可回寫控制面、下一步路由與引用源。</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>已完成案例</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloudflare</td>
          <td><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">2019 Regex CPU Outage</a>、<a href="/blog/backend/08-incident-response/cases/cloudflare/2023-control-plane-token-incident/" data-link-title="Cloudflare 2023 Control Plane Token Incident" data-link-desc="2023-01-24 Cloudflare service token 錯誤變更導致多產品連鎖影響的事故解析：信任邊界、擴散機制、止血策略與流程回寫。">2023 Control Plane Token Incident</a>、<a href="/blog/backend/08-incident-response/cases/cloudflare/2026-byoip-bgp-withdrawal/" data-link-title="Cloudflare 2026 BYOIP BGP Withdrawal" data-link-desc="2026-02-20 Cloudflare BYOIP prefixes 被非預期撤告的事故解析：Addressing API bug、BGP withdrawal、狀態恢復與控制面回寫。">2026 BYOIP BGP Withdrawal</a></td>
          <td>已回寫 4.21 / 6.24</td>
      </tr>
      <tr>
          <td>AWS S3</td>
          <td><a href="/blog/backend/08-incident-response/cases/aws-s3/2017-us-east-1-service-disruption/" data-link-title="AWS S3 2017 US-EAST-1 Service Disruption" data-link-desc="2017-02-28 AWS S3 us-east-1 事故解析：內部操作命令、index / placement 子系統重啟、區域依賴擴散與狀態頁依賴回寫。">2017 US-EAST-1 Service Disruption</a></td>
          <td>補 2021 多服務退化</td>
      </tr>
      <tr>
          <td>GitHub</td>
          <td><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">2018 Oct21 MySQL Topology Incident</a></td>
          <td>補 2020 Actions 案例</td>
      </tr>
      <tr>
          <td>GCP</td>
          <td><a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">2019 US Network Congestion Multi-service Incident</a></td>
          <td>補 IAM 控制面案例</td>
      </tr>
      <tr>
          <td>Atlassian</td>
          <td><a href="/blog/backend/08-incident-response/cases/atlassian/2022-april-multi-tenant-deletion-outage/" data-link-title="Atlassian 2022 April Multi-tenant Deletion Outage" data-link-desc="2022-04 Atlassian 因維運腳本誤刪多租戶站點造成長時間事故的解析：恢復分批、跨團隊指揮與對外通訊節奏。">2022 April Multi-tenant Deletion Outage</a></td>
          <td>補次級事故對照</td>
      </tr>
      <tr>
          <td>Roblox</td>
          <td><a href="/blog/backend/08-incident-response/cases/roblox/2021-oct-prolonged-core-infra-outage/" data-link-title="Roblox 2021 Oct Prolonged Core Infra Outage" data-link-desc="2021-10 Roblox 長時間平台中斷的事故解析：核心基礎設施壓力失衡、根因定位延遲與長尾恢復。">2021 Oct Prolonged Core Infra Outage</a></td>
          <td>補恢復後優化案例</td>
      </tr>
      <tr>
          <td>Fastly</td>
          <td><a href="/blog/backend/08-incident-response/cases/fastly/2021-june-global-edge-config-triggered-outage/" data-link-title="Fastly 2021 June Global Edge Config-triggered Outage" data-link-desc="2021-06-08 Fastly 全球 edge 事故解析：有效客戶配置觸發潛藏 bug、分鐘級擴散與快速隔離恢復。">2021 June Global Edge Config-triggered Outage</a></td>
          <td>補後續改善案例</td>
      </tr>
  </tbody>
</table>
<h2 id="t2t3-第一批正文已完成">T2/T3 第一批正文（已完成）</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>正文入口</th>
          <th>主題重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slack</td>
          <td><a href="/blog/backend/08-incident-response/cases/slack/2022-connection-recovery-and-status-communication/" data-link-title="Slack：2022 連線恢復與狀態通訊節奏" data-link-desc="在通訊平台自身失效時，如何同步恢復節奏與對外狀態揭露。">SL1 連線恢復與狀態通訊</a></td>
          <td>通訊平台失效時的對外節奏</td>
      </tr>
      <tr>
          <td>Datadog</td>
          <td><a href="/blog/backend/08-incident-response/cases/datadog/2023-multi-region-observability-disruption/" data-link-title="Datadog：2023 多區觀測中斷事件" data-link-desc="監控平台自身退化時，如何避免客戶誤判系統健康狀態。">DD1 多區觀測中斷</a></td>
          <td>監控平台失效的二階風險</td>
      </tr>
      <tr>
          <td>Discord</td>
          <td><a href="/blog/backend/08-incident-response/cases/discord/2022-gateway-capacity-event/" data-link-title="Discord：Gateway 容量事件與恢復節奏" data-link-desc="長連線平台在容量邊界被擊穿時，如何控制擴散並分批恢復。">DC1 Gateway 容量事件</a></td>
          <td>長連線回復造成的二次擁塞</td>
      </tr>
      <tr>
          <td>Azure AD</td>
          <td><a href="/blog/backend/08-incident-response/cases/azure-ad/2021-identity-control-plane-disruption/" data-link-title="Azure AD：2021 身分控制面中斷事件" data-link-desc="身分服務失效時，如何評估跨產品影響與收斂優先序。">AZ1 身分控制面中斷</a></td>
          <td>跨產品身份依賴分級治理</td>
      </tr>
      <tr>
          <td>Heroku</td>
          <td><a href="/blog/backend/08-incident-response/cases/heroku/2021-routing-control-event/" data-link-title="Heroku：Routing 控制事件與多租戶影響" data-link-desc="PaaS 路由層異常時，如何限制租戶擴散並維持可用通訊。">HR1 Routing 控制事件</a></td>
          <td>多租戶入口故障的局部化回復</td>
      </tr>
      <tr>
          <td>Reddit</td>
          <td><a href="/blog/backend/08-incident-response/cases/reddit/2023-kubernetes-upgrade-incident/" data-link-title="Reddit：2023 Kubernetes 升級事故" data-link-desc="平台升級變更如何觸發服務退化，以及如何設計可回退的升級策略。">RD1 Kubernetes 升級事故</a></td>
          <td>平台升級與回退決策治理</td>
      </tr>
      <tr>
          <td>Microsoft 365</td>
          <td><a href="/blog/backend/08-incident-response/cases/microsoft-365/2023-suite-wide-authentication-incident/" data-link-title="Microsoft 365：套件級身分驗證事故" data-link-desc="企業套件在身份依賴失效時，如何同步處理跨產品影響與對外揭露。">M365-1 套件級身份事故</a></td>
          <td>企業套件跨產品影響盤點</td>
      </tr>
  </tbody>
</table>
<h2 id="t1-服務">T1 服務</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/aws-s3/" data-link-title="AWS S3" data-link-desc="AWS S3 重大事故時間線與架構脈絡">aws-s3</a></li>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/" data-link-title="Cloudflare" data-link-desc="Cloudflare 全球 edge 事故時間線與架構脈絡">cloudflare</a></li>
<li><a href="/blog/backend/08-incident-response/cases/github/" data-link-title="GitHub" data-link-desc="GitHub 重大事故時間線與架構脈絡">github</a></li>
<li><a href="/blog/backend/08-incident-response/cases/gcp/" data-link-title="Google Cloud Platform" data-link-desc="GCP 重大事故時間線與架構脈絡">gcp</a></li>
<li><a href="/blog/backend/08-incident-response/cases/atlassian/" data-link-title="Atlassian" data-link-desc="Atlassian 多租戶事故時間線與架構脈絡">atlassian</a></li>
<li><a href="/blog/backend/08-incident-response/cases/roblox/" data-link-title="Roblox" data-link-desc="Roblox 73 小時事故時間線與架構脈絡">roblox</a></li>
<li><a href="/blog/backend/08-incident-response/cases/fastly/" data-link-title="Fastly" data-link-desc="Fastly 全球配置 push 事故時間線">fastly</a></li>
</ul>
<h2 id="t2-服務">T2 服務</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/slack/" data-link-title="Slack" data-link-desc="Slack 通訊服務事故與外部狀態頁設計">slack</a></li>
<li><a href="/blog/backend/08-incident-response/cases/datadog/" data-link-title="Datadog" data-link-desc="Datadog 監控服務事故、客戶觀測落差">datadog</a></li>
<li><a href="/blog/backend/06-reliability/cases/stripe/" data-link-title="Stripe" data-link-desc="Stripe Deploy Strategy / Game Day / Idempotency 實踐">stripe（住於 06）</a></li>
<li><a href="/blog/backend/08-incident-response/cases/discord/" data-link-title="Discord" data-link-desc="Discord Gateway scale-out 事故與容量驚奇">discord</a></li>
<li><a href="/blog/backend/08-incident-response/cases/azure-ad/" data-link-title="Azure AD / Entra ID" data-link-desc="Microsoft Identity 控制面失效與 cascading 影響">azure-ad</a></li>
</ul>
<h2 id="t3-服務">T3 服務</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/heroku/" data-link-title="Heroku" data-link-desc="Heroku PaaS 事故與 router 層架構脈絡">heroku</a></li>
<li><a href="/blog/backend/06-reliability/cases/linkedin/" data-link-title="LinkedIn" data-link-desc="LinkedIn Capacity Planning 與 On-call 結構">linkedin（住於 06）</a></li>
<li><a href="/blog/backend/08-incident-response/cases/reddit/" data-link-title="Reddit" data-link-desc="Reddit Pi Day 2023 k8s 升級事故">reddit</a></li>
<li><a href="/blog/backend/08-incident-response/cases/microsoft-365/" data-link-title="Microsoft 365" data-link-desc="Microsoft 365 SaaS 套件事故與企業客戶影響">microsoft-365</a></li>
</ul>
]]></content:encoded></item><item><title>訊息佇列 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/</guid><description>&lt;p>訊息佇列 Vendor 清單的核心責任是把 broker 名稱放回 delivery semantics、processing semantics、replay boundary 與操作治理的判斷。每個服務頁先回答它提供哪種投遞與消費模型，再討論 ordering、retention、consumer group、DLQ、managed 邊界與案例回寫。選 broker 之前、佇列這塊能力先過一次買 vs 建：自管 broker（RabbitMQ、Kafka）自己扛 ordering、retention、DLQ 的運維、managed（SQS、SNS、MSK、Confluent Cloud）把這層交出去、雲端原生事件匯流更省 — 逐能力的判讀見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>佇列服務要從處理語意進入。讀者如果要處理一般工作佇列，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>；如果要處理事件流與 replay，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer design&lt;/a>；如果問題是資料庫交易與事件發布一致性，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>佇列服務頁的教學順序是先建立 work queue baseline，再進入 event log、managed delivery、lightweight messaging 與 embedded stream。這個順序對齊 checkout E3：讀者先理解 delivery、processing、recovery 三層語意，再比較 broker、managed queue、pub/sub 與 stream 如何影響 retry、DLQ、ordering 與 replay。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a>&lt;/td>
 &lt;td>Classic broker&lt;/td>
 &lt;td>exchange、routing、ack/nack 與 DLQ 如何支援工作分派&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka&lt;/a>&lt;/td>
 &lt;td>Event streaming&lt;/td>
 &lt;td>partition、offset、retention 與 replay 如何支援事件流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS&lt;/a>&lt;/td>
 &lt;td>Messaging / stream&lt;/td>
 &lt;td>subject、JetStream、low-latency 與 durability 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams&lt;/a>&lt;/td>
 &lt;td>Embedded stream&lt;/td>
 &lt;td>Redis 生態中的 stream、consumer group 與 pending entry 邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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>&lt;/td>
 &lt;td>Managed queue&lt;/td>
 &lt;td>standard / FIFO、visibility timeout 與 DLQ 如何支援 managed delivery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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 Cloud Pub/Sub&lt;/a>&lt;/td>
 &lt;td>Managed pub/sub&lt;/td>
 &lt;td>topic / subscription、push / pull 與 global delivery 如何取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「↔ X」代表雙向遷移、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>訊息佇列 Vendor 清單的核心責任是把 broker 名稱放回 delivery semantics、processing semantics、replay boundary 與操作治理的判斷。每個服務頁先回答它提供哪種投遞與消費模型，再討論 ordering、retention、consumer group、DLQ、managed 邊界與案例回寫。選 broker 之前、佇列這塊能力先過一次買 vs 建：自管 broker（RabbitMQ、Kafka）自己扛 ordering、retention、DLQ 的運維、managed（SQS、SNS、MSK、Confluent Cloud）把這層交出去、雲端原生事件匯流更省 — 逐能力的判讀見 <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>。</p>
<h2 id="讀法">讀法</h2>
<p>佇列服務要從處理語意進入。讀者如果要處理一般工作佇列，先回到 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>；如果要處理事件流與 replay，先回到 <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 design</a>；如果問題是資料庫交易與事件發布一致性，先回到 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>佇列服務頁的教學順序是先建立 work queue baseline，再進入 event log、managed delivery、lightweight messaging 與 embedded stream。這個順序對齊 checkout E3：讀者先理解 delivery、processing、recovery 三層語意，再比較 broker、managed queue、pub/sub 與 stream 如何影響 retry、DLQ、ordering 與 replay。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a></td>
          <td>Classic broker</td>
          <td>exchange、routing、ack/nack 與 DLQ 如何支援工作分派</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Apache Kafka</a></td>
          <td>Event streaming</td>
          <td>partition、offset、retention 與 replay 如何支援事件流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></td>
          <td>Messaging / stream</td>
          <td>subject、JetStream、low-latency 與 durability 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
          <td>Embedded stream</td>
          <td>Redis 生態中的 stream、consumer group 與 pending entry 邊界</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Managed queue</td>
          <td>standard / FIFO、visibility timeout 與 DLQ 如何支援 managed delivery</td>
      </tr>
      <tr>
          <td><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 Cloud Pub/Sub</a></td>
          <td>Managed pub/sub</td>
          <td>topic / subscription、push / pull 與 global delivery 如何取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「↔ X」代表雙向遷移、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="kafka/">Kafka</a></td>
          <td><a href="kafka/consumer-rebalance-lag-diagnosis/">rebalance/lag</a> / <a href="kafka/replication-isr-exactly-once/">replication/ISR</a> / <a href="kafka/retention-tiered-storage/">retention/tiered</a> / <a href="kafka/schema-registry-evolution/">schema registry</a> / <a href="kafka/multi-tenant-quota-acl/">multi-tenant</a></td>
          <td><a href="kafka/migrate-from-to-nats/">↔ NATS</a> / <a href="kafka/migrate-to-msk/">→ MSK</a></td>
      </tr>
      <tr>
          <td><a href="rabbitmq/">RabbitMQ</a></td>
          <td><a href="rabbitmq/queue-types-classic-quorum-stream/">queue 模型選型</a> / <a href="rabbitmq/network-partition-clustering/">network partition</a> / <a href="rabbitmq/dlq-retry-escalation/">DLQ retry escalation</a></td>
          <td><a href="rabbitmq/migrate-to-kafka/">→ Kafka</a> / <a href="rabbitmq/migrate-to-aws-sqs/">→ AWS SQS</a></td>
      </tr>
      <tr>
          <td><a href="nats/">NATS</a></td>
          <td><a href="nats/jetstream-supercluster-design/">JetStream/supercluster</a> / <a href="nats/jetstream-durability-consumer/">JetStream durability/consumer</a></td>
          <td>↔ Kafka（見 Kafka 列）</td>
      </tr>
      <tr>
          <td><a href="redis-streams/">Redis Streams</a></td>
          <td><a href="redis-streams/xclaim-pel-recovery/">XCLAIM/PEL</a></td>
          <td><a href="redis-streams/migrate-to-kafka/">→ Kafka</a></td>
      </tr>
      <tr>
          <td><a href="aws-sqs/">AWS SQS</a></td>
          <td><a href="aws-sqs/visibility-polling-lambda-cost/">visibility/polling/Lambda</a></td>
          <td><a href="aws-sqs/migrate-to-google-pubsub/">→ Google Pub/Sub</a></td>
      </tr>
      <tr>
          <td><a href="google-pubsub/">Google Pub/Sub</a></td>
          <td><a href="google-pubsub/ordering-dlt-schema/">ordering/DLT/schema</a> / <a href="google-pubsub/push-pull-ack-flow-control/">push/pull/ack flow control</a></td>
          <td><a href="google-pubsub/migrate-from-kafka/">← Kafka</a></td>
      </tr>
  </tbody>
</table>
<p>deep article 走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>、指令均經 Docker / emulator 實機驗證（驗不了的標 caveat）；migration playbook 走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>。</p>
<p>main 與 feat/backend_03 兩批平行撰寫過 03 deep article、重疊主題已去重：RabbitMQ quorum、Redis Streams PEL、AWS SQS visibility 三組各保留涵蓋較完整、經實機驗證的一篇（主題框架已併入）。NATS 保留兩篇互補定位——<a href="nats/jetstream-durability-consumer/">core 到 JetStream 邊界</a> 是採用決策入口、<a href="nats/jetstream-supercluster-design/">JetStream 設計與 supercluster/leaf</a> 是完整實作。後續候選見上方「T1 服務頁大綱」段、各 vendor <code>_index.md</code> 進階主題段與下方「後續候選」表。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>佇列服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 work queue、event log、pub/sub、stream 還是 workflow handoff</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>throughput、ordering、fan-out、retention、replay、managed operation 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>broker、event streaming、managed queue、workflow engine 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>partition、consumer lag、DLQ drain、schema、ACL、upgrade、quota</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>publish rate、consume rate、lag、redelivery、DLQ depth、replay window</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Meta FOQS、VMware MSK、LinkedIn TopicGC 如何提供治理判準</td>
      </tr>
  </tbody>
</table>
<p>服務責任段要先分辨投遞成功與處理成功。Broker 可以保存訊息與重新投遞，但 consumer 的 idempotency、side effect、checkpoint 與補償流程才決定業務結果是否可恢復。</p>
<p>適用壓力段要保留副作用語言。寄信、轉檔、invoice、search index sync、webhook fan-out 與 audit event 的 retry、ordering、DLQ 與 replay 條件不同，服務頁要分別展開。</p>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>佇列服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 work queue、event log、pub/sub、embedded stream 還是 workflow engine</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷 delivery、processing、recovery、ordering 與 replay 邊界</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「是否需要 durable retry、fan-out、ordering、replay」快速定位工具類型</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>ack/nack、visibility timeout、DLQ、consumer group、schema、quota</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>RabbitMQ、Kafka、SQS、Pub/Sub、NATS、Redis Streams 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>partition、retention、exactly-once claims、multi-region、managed quota</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>lag、redelivery、DLQ depth、poison message、consumer pause、offset</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>human workflow 轉 workflow engine、同步查詢回 API、正式狀態回 database</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整 client API、framework adapter、所有 broker plugin</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 3.C cases、6.12 replay verification、8.19 decision log</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>橫向議題在不同 vendor 用不同旋鈕達成。本表把同一議題在 6 個 vendor 的對應位置列出、確保大綱不缺漏議題、且讀者跨 vendor 查找對照位置時有索引。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>Kafka</th>
          <th>RabbitMQ</th>
          <th>NATS</th>
          <th>Redis Streams</th>
          <th>AWS SQS</th>
          <th>Pub/Sub</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多租戶配額 / 隔離</td>
          <td>quota + ACL</td>
          <td>vhost + user permission</td>
          <td>account + subject ACL</td>
          <td>Redis ACL</td>
          <td>IAM policy</td>
          <td>IAM + Service Account</td>
      </tr>
      <tr>
          <td>跨區 / 全球交付</td>
          <td>MirrorMaker 2</td>
          <td>Federation / Shovel</td>
          <td>Supercluster + Leaf node</td>
          <td>Redis Cluster（受限）</td>
          <td>Cross-region replication</td>
          <td>內建 global routing</td>
      </tr>
      <tr>
          <td>Topic 生命週期治理</td>
          <td>TopicGC、auto-cleanup</td>
          <td>vhost / queue lifecycle</td>
          <td>Stream lifecycle</td>
          <td>MAXLEN / XTRIM</td>
          <td>DLQ + redrive policy</td>
          <td>Subscription expiration</td>
      </tr>
      <tr>
          <td>自動修復</td>
          <td>Self-healing automation</td>
          <td>cluster_partition_handling</td>
          <td>JetStream raft</td>
          <td>Sentinel / Cluster failover</td>
          <td>managed 內建</td>
          <td>managed 內建</td>
      </tr>
      <tr>
          <td>Delivery 機制</td>
          <td>acks + idempotence + ISR</td>
          <td>manual ack + DLX</td>
          <td>JetStream ack + AckWait</td>
          <td>XACK + XCLAIM + PEL</td>
          <td>visibility timeout + DLQ</td>
          <td>ack deadline + DLT</td>
      </tr>
      <tr>
          <td>路由模型</td>
          <td>partition + key</td>
          <td>exchange + routing key</td>
          <td>subject + wildcard</td>
          <td>stream key（無 partition）</td>
          <td>queue URL</td>
          <td>topic + subscription</td>
      </tr>
      <tr>
          <td>持久化模型</td>
          <td>log + retention policy</td>
          <td>durable queue + TTL</td>
          <td>JetStream storage</td>
          <td>append-only log（RAM）</td>
          <td>managed durable</td>
          <td>managed durable</td>
      </tr>
      <tr>
          <td>Schema 治理</td>
          <td>Schema Registry</td>
          <td>（無原生）</td>
          <td>（無原生、靠 JSON Schema 慣例）</td>
          <td>（無）</td>
          <td>（無）</td>
          <td>Schema enforcement</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>C1/C3-C7 + C11-C22</td>
          <td>C23-C33</td>
          <td>C34-C41</td>
          <td>C42-C47</td>
          <td>C48-C59 + C2 反面</td>
          <td>C60-C69</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題是否都有對應的進階主題子段、避免缺漏</li>
<li>讀者在 vendor 間遷移時、知道對應旋鈕在另一個 vendor 叫什麼</li>
<li>未來擴充案例時、依 <a href="/blog/backend/03-message-queue/cases/" data-link-title="模組三案例正文" data-link-desc="訊息佇列與事件傳遞的轉換案例入口、含通用案例與 6 個 vendor 的真實 production case 庫。">cases/_index 的「案例覆蓋缺口」段</a> 判定優先補的章節</li>
</ul>
<p>下面 8 段把對照表的每行展開、避免單純的表格成為「終點」。每段先解釋議題本質、再展開不同 vendor 的 mechanism 差異、最後給選型判讀。</p>
<h3 id="路由模型">路由模型</h3>
<p>路由模型決定「訊息怎麼送到對的 consumer」、不是同概念換名字。<strong>Kafka</strong> partition + key 透過 hash 把訊息落在固定 partition、consumer group 靠 rebalance 綁定 partition 跟 consumer；<strong>RabbitMQ</strong> exchange + routing key 透過 binding rule 比對、可 broadcast（fanout）/ 精準（direct）/ pattern（topic + <code>*</code> 單層 / <code>#</code> 多層）；<strong>NATS</strong> subject + wildcard（<code>*</code> 單層、<code>&gt;</code> 多層）讓 subscriber 用 pattern 訂閱主題層級；<strong>Redis Streams</strong> 是單一 stream key、無 partition、跨 shard 要靠 hash tag 強制分散；<strong>SQS</strong> queue URL 直接對應、無 routing 邏輯；<strong>Pub/Sub</strong> topic + subscription、subscription 是 first-class entity（跟 Kafka topic + consumer group 不同）。</p>
<p>選型判讀：需要 fan-out 多 subscriber → fanout exchange / subject pattern / multi-subscription；需要 per-key ordering → Kafka partition+key / RabbitMQ consistent hash exchange / NATS queue group；不需 routing 邏輯 → SQS 最簡單。</p>
<h3 id="delivery-機制">Delivery 機制</h3>
<p>Delivery 機制是「broker 怎麼保證訊息被處理」、不同 vendor 用不同協議層級達成同語意。詳見 <a href="/blog/backend/03-message-queue/broker-basics/#%e8%aa%9e%e6%84%8f%e4%bf%9d%e8%ad%89%e7%9a%84%e4%b8%8d%e5%90%8c%e5%af%a6%e4%bd%9c%e6%a9%9f%e5%88%b6" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker-basics 的「語意保證的不同實作機制」</a>。三層核心旋鈕：<strong>Kafka</strong> acks（0/1/all）+ idempotence + ISR（min.insync.replicas）；<strong>RabbitMQ</strong> manual ack + DLX + prefetch；<strong>NATS</strong> JetStream ack + AckWait + MaxDeliver；<strong>Redis Streams</strong> XACK + XCLAIM + PEL；<strong>SQS</strong> visibility timeout + DLQ + maxReceiveCount；<strong>Pub/Sub</strong> ack deadline + DLT + ack extension。</p>
<p>選型判讀：寫入即承諾（事件流）→ Kafka acks=all + ISR；處理即承諾（任務隊列）→ RabbitMQ manual ack / SQS visibility timeout / Pub/Sub ack deadline；wire-level handshake（device 端）→ MQTT QoS（透過 RabbitMQ MQTT plugin 或 EMQX）。</p>
<h3 id="持久化模型">持久化模型</h3>
<p>持久化模型決定「訊息能保留多久、能不能 replay」。<strong>Kafka</strong> log + retention policy（time / size、compact / delete）— 訊息保留到 retention 過期、consumer 可任意 offset replay；<strong>RabbitMQ</strong> durable queue + TTL — 訊息持久化但 ack 後即刪、不能 replay；<strong>NATS</strong> JetStream storage（file / memory、配 MaxMsgs / MaxBytes / MaxAge）— 介於 log 跟 queue 之間；<strong>Redis Streams</strong> append-only log 但受 RAM 限制 — retention 短期、replay 視 MAXLEN 設定；<strong>SQS / Pub/Sub</strong> managed durable — SQS 最長 14 天、Pub/Sub 7 天、不適合長期 archive。</p>
<p>選型判讀：需要事件 replay（多 consumer 各自進度、長期保留）→ Kafka / Pulsar / JetStream；任務處理即刪（worker pool）→ RabbitMQ / SQS / Pub/Sub；中期 stream 但已在 Redis 生態 → Redis Streams + MAXLEN。</p>
<h3 id="topic-生命週期治理">Topic 生命週期治理</h3>
<p>當 topic / queue 數量上萬、metadata 本身變成 broker 壓力。<strong>Kafka</strong> 早期靠人工管 topic、規模化後需 TopicGC（自動清理 unused topic）+ partition 數量上限；<strong>RabbitMQ</strong> vhost / queue lifecycle 通常手動、queue auto-delete + TTL 是常見 pattern；<strong>NATS</strong> JetStream stream 有 lifecycle policy（DiscardPolicy / MaxAge）；<strong>Redis Streams</strong> MAXLEN / XTRIM 手動修剪、無自動 GC；<strong>SQS</strong> DLQ + redrive policy 是 lifecycle 核心、queue 本身不自動刪；<strong>Pub/Sub</strong> subscription expiration policy（閒置 N 天自動刪）。</p>
<p>選型判讀：metadata 量大（topic 數 / partition 數）→ 需 Kafka TopicGC 模式；任務隊列 → 需 DLQ + redrive 規範；長期 stream → 需明示 retention policy。</p>
<h3 id="自動修復">自動修復</h3>
<p>自動修復把 SRE 從人工值班轉到自動化、但層次不同。<strong>Kafka</strong> Self-healing（disk full / broker offline / under-replicated partition 自動處理）；<strong>RabbitMQ</strong> cluster_partition_handling（ignore / autoheal / pause_minority）— 偏向「腦裂處理策略」、不是全自動 SRE；<strong>NATS</strong> JetStream raft 自動 leader election + replica sync；<strong>Redis Streams</strong> 靠 Sentinel / Cluster failover、failover 期間 PEL 可能不一致；<strong>SQS / Pub/Sub</strong> managed 內建、不需用戶管。</p>
<p>選型判讀：自管要 24/7 → Kafka self-healing 或 NATS raft；不要值班 → managed（SQS / Pub/Sub）；中等規模容忍人工 → RabbitMQ cluster_partition_handling。</p>
<h3 id="多租戶配額--隔離">多租戶配額 / 隔離</h3>
<p>隔離粒度跟 mechanism 不同。<strong>Kafka</strong> quota（byte rate / request rate）+ ACL（principal / resource / operation）— 流量級 + identity 級；<strong>RabbitMQ</strong> vhost + user permission — namespace 級隔離（最強）；<strong>NATS</strong> account + subject ACL — account 是 namespace、subject ACL 是細粒度權限；<strong>Redis Streams</strong> Redis ACL — command-level 權限；<strong>SQS / Pub/Sub</strong> IAM policy + Service Account — identity 級、無 namespace 概念。</p>
<p>選型判讀：跨 team 共用 cluster → 需 namespace 隔離（vhost / account）；多 client app → identity 隔離（IAM）；流量公平 → 需 quota（Kafka quota / 自建 rate limit）。</p>
<h3 id="跨區--全球交付">跨區 / 全球交付</h3>
<p>跨區拓樸三類：mesh（broker 自己同步）vs hub-spoke（單向轉發）vs managed global。<strong>Kafka</strong> MirrorMaker 2 是 mesh（active-active / active-passive）；<strong>RabbitMQ</strong> Federation 是 hub-spoke（upstream → downstream 鬆耦合）、Shovel 是點對點搬運；<strong>NATS</strong> Supercluster + Leaf node 是 mesh + edge（適合 IoT 廠區）；<strong>Redis Cluster</strong> 跨區受限（Cluster 是 shard、不是 region）；<strong>SQS</strong> Cross-region replication（managed）；<strong>Pub/Sub</strong> 內建 global routing — 無需設定。</p>
<p>選型判讀：自管要 mesh → MirrorMaker 2 / NATS Supercluster；hub-spoke 簡單 → Federation；不想處理跨區 → Pub/Sub global 或 SQS replication。</p>
<h3 id="schema-治理">Schema 治理</h3>
<p>Schema 強制度跨 vendor 差異最大。<strong>Kafka</strong> Schema Registry（Confluent / Apicurio）+ Avro / Protobuf — 強制 producer 帶 schema ID、enforce compatibility；<strong>RabbitMQ</strong> 無原生 schema 機制 — 靠 application 層約定；<strong>NATS</strong> 無原生、靠 JSON Schema 慣例；<strong>Redis Streams</strong> 無 schema 概念；<strong>SQS</strong> message attribute + body string — 無 enforce；<strong>Pub/Sub</strong> Schema enforcement（topic 綁 Avro / Protobuf schema）。</p>
<p>選型判讀：跨服務契約嚴 → Kafka + Schema Registry / Pub/Sub Schema enforcement；內部簡單通訊 → RabbitMQ / NATS 靠慣例；schema 演進頻繁 → 需 forward / backward / full compatibility 規範。</p>
<h2 id="服務頁大綱對齊">服務頁大綱對齊</h2>
<p>6 個 vendor 頁套同樣的章節結構、方便讀者跨 vendor 跳讀。對齊參考 <a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">LLM 模組 1.0 Ollama</a> 的「觀念 → 原理 → 操作指令」分層寫法：</p>
<ol>
<li><strong>服務定位</strong>（段首段、3 個責任 + 設計取捨）</li>
<li><strong>本章目標</strong>（5 條可驗證能力 checklist）</li>
<li><strong>最短路徑</strong>（5 分鐘可跑通的 install + verify、bash 範例 placeholder）</li>
<li><strong>日常操作與決策形狀</strong>（CLI / API、路由設計、ack 策略三個子段）</li>
<li><strong>進階主題</strong>（按需閱讀、每子段對應一個 case 或 vendor 專長議題）</li>
<li><strong>排錯快速判讀</strong>（每情境：操作原則 + 指令 + 解法）</li>
<li><strong>何時改走其他服務</strong>（對照表）</li>
<li><strong>不在本頁內的主題</strong>（明確邊界）</li>
<li><strong>案例回寫</strong>（cases/ 引用 + 主討論議題）</li>
<li><strong>下一步路由</strong>（上游概念 / 平行 vendor / 下游能力）</li>
</ol>
<p>每個章節「要回答的問題」「要包含的指令範例 placeholder」「對應 case」都已寫在各 vendor 頁的大綱、但未寫實際正文 — 等到撰寫批次（見下節）開始時才展開。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q1</td>
          <td>RabbitMQ</td>
          <td>建立 work queue、routing、ack/nack 與 DLQ baseline</td>
      </tr>
      <tr>
          <td>Q2</td>
          <td>Kafka</td>
          <td>建立 event log、partition、retention 與 replay 判準</td>
      </tr>
      <tr>
          <td>Q3</td>
          <td>AWS SQS / Google Pub/Sub</td>
          <td>建立 managed delivery、visibility timeout 與 cloud pub/sub 邊界</td>
      </tr>
      <tr>
          <td>Q4</td>
          <td>NATS / Redis Streams</td>
          <td>建立 lightweight messaging 與 embedded stream 的邊界</td>
      </tr>
      <tr>
          <td>Q5</td>
          <td>Pulsar / Kinesis / Temporal</td>
          <td>補 multi-tenant streaming、managed stream 與 workflow engine 對照</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Streaming</td>
          <td>Apache Pulsar、Redpanda、AWS Kinesis、Confluent Cloud / MSK</td>
          <td>retention、partition、managed Kafka、serverless stream</td>
      </tr>
      <tr>
          <td>Managed event bus</td>
          <td>AWS SNS、EventBridge、Azure Event Grid</td>
          <td>fan-out、event routing、schema、cloud-native integration</td>
      </tr>
      <tr>
          <td>Enterprise queue</td>
          <td>Azure Service Bus、ActiveMQ、IBM MQ</td>
          <td>enterprise integration、session、routing、DLQ</td>
      </tr>
      <tr>
          <td>Workflow engine</td>
          <td>Temporal、Cadence</td>
          <td>durable workflow、activity retry、human / machine workflow 邊界</td>
      </tr>
      <tr>
          <td>Lightweight</td>
          <td>NSQ、ZeroMQ</td>
          <td>simple broker、library messaging、durability trade-off</td>
      </tr>
      <tr>
          <td>IoT messaging</td>
          <td>MQTT、EMQX、HiveMQ、Mosquitto</td>
          <td>device connection、QoS、topic hierarchy、edge constraints</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 queue、stream、event bus、workflow 與 device messaging。Kafka / Pulsar / Kinesis 解 event stream；SQS / Service Bus 解 managed queue；SNS / EventBridge / Event Grid 解 cloud event routing；Temporal 解 workflow state；MQTT broker 解 IoT device delivery。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></li>
<li>上游：<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 design</a></li>
<li>案例：<a href="/blog/backend/03-message-queue/cases/" data-link-title="模組三案例正文" data-link-desc="訊息佇列與事件傳遞的轉換案例入口、含通用案例與 6 個 vendor 的真實 production case 庫。">3.C 佇列案例正文</a></li>
<li>服務路徑：<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。">3.8 Queue Consumer Retry 與 Replay Handoff</a></li>
</ul>
]]></content:encoded></item><item><title>部署平台 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/</guid><description>&lt;p>部署平台 Vendor 清單的核心責任是把平台名稱放回 runtime contract、lifecycle contract、traffic contract、control plane 與 rollout governance 的判斷。每個服務頁先回答它承擔啟動、調度、入口、設定、基礎設施狀態或 service discovery 的哪一段，再討論操作成本與案例回寫。部署這塊能力的買 vs 建是一條深度光譜：自管 Kubernetes、用 managed K8s（EKS、GKE、AKS）、用 PaaS（Fly、Render、Railway），到完全 serverless — 越往右維運越少、客製與可攜性也越受限，取捨見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>部署服務要從服務生命週期進入。讀者如果要處理 container 與 runtime，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime&lt;/a>；如果要處理 rollout 與 probe，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment&lt;/a>；如果要處理入口與 drain，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>部署平台服務頁的教學順序是先建立 workload runtime，再進入 orchestration、traffic entry、infra state 與 discovery。這個順序對齊 checkout E4：讀者先理解服務如何啟動、接流量、drain 與 rollback，再比較 Kubernetes、systemd、Docker、load balancer、proxy、Terraform 與 Consul 分別承擔哪一層平台責任。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes&lt;/a>&lt;/td>
 &lt;td>Orchestration&lt;/td>
 &lt;td>pod lifecycle、probe、rolling update 與 resource limit 如何成為平台契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker&lt;/a>&lt;/td>
 &lt;td>Container runtime&lt;/td>
 &lt;td>image、entrypoint、runtime config 與 local / prod parity 如何管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd&lt;/a>&lt;/td>
 &lt;td>Process supervisor&lt;/td>
 &lt;td>unit、restart policy、signal 與 journal 如何支援單機服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx&lt;/a>&lt;/td>
 &lt;td>Reverse proxy / LB&lt;/td>
 &lt;td>reverse proxy、timeout、buffering、TLS 與 ingress 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy&lt;/a>&lt;/td>
 &lt;td>Service proxy&lt;/td>
 &lt;td>xDS、dynamic config、mesh data plane 與 traffic policy 如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB&lt;/a>&lt;/td>
 &lt;td>Managed LB&lt;/td>
 &lt;td>ALB / NLB、health check、draining 與 target group 如何支援 AWS 入口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform / OpenTofu&lt;/a>&lt;/td>
 &lt;td>IaC&lt;/td>
 &lt;td>state、plan、provider、drift 與 review gate 如何管理 infra 變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik&lt;/a>&lt;/td>
 &lt;td>Ingress / proxy&lt;/td>
 &lt;td>auto-discovery、dynamic routing 與 cloud-native ingress 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul&lt;/a>&lt;/td>
 &lt;td>Registry / mesh&lt;/td>
 &lt;td>service registry、DNS、health check、KV 與 mesh 邊界如何取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。&lt;/p></description><content:encoded><![CDATA[<p>部署平台 Vendor 清單的核心責任是把平台名稱放回 runtime contract、lifecycle contract、traffic contract、control plane 與 rollout governance 的判斷。每個服務頁先回答它承擔啟動、調度、入口、設定、基礎設施狀態或 service discovery 的哪一段，再討論操作成本與案例回寫。部署這塊能力的買 vs 建是一條深度光譜：自管 Kubernetes、用 managed K8s（EKS、GKE、AKS）、用 PaaS（Fly、Render、Railway），到完全 serverless — 越往右維運越少、客製與可攜性也越受限，取捨見 <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>。</p>
<h2 id="讀法">讀法</h2>
<p>部署服務要從服務生命週期進入。讀者如果要處理 container 與 runtime，先回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container runtime</a>；如果要處理 rollout 與 probe，先回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment</a>；如果要處理入口與 drain，先回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>部署平台服務頁的教學順序是先建立 workload runtime，再進入 orchestration、traffic entry、infra state 與 discovery。這個順序對齊 checkout E4：讀者先理解服務如何啟動、接流量、drain 與 rollback，再比較 Kubernetes、systemd、Docker、load balancer、proxy、Terraform 與 Consul 分別承擔哪一層平台責任。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/" data-link-title="Kubernetes" data-link-desc="Container orchestration 主流、GKE / EKS / AKS / 自管">Kubernetes</a></td>
          <td>Orchestration</td>
          <td>pod lifecycle、probe、rolling update 與 resource limit 如何成為平台契約</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker</a></td>
          <td>Container runtime</td>
          <td>image、entrypoint、runtime config 與 local / prod parity 如何管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/systemd/" data-link-title="systemd" data-link-desc="Linux init system、VM / 單機 service lifecycle">systemd</a></td>
          <td>Process supervisor</td>
          <td>unit、restart policy、signal 與 journal 如何支援單機服務</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/nginx/" data-link-title="nginx" data-link-desc="HTTP server / reverse proxy / LB / ingress">nginx</a></td>
          <td>Reverse proxy / LB</td>
          <td>reverse proxy、timeout、buffering、TLS 與 ingress 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/envoy/" data-link-title="Envoy" data-link-desc="Cloud-native service proxy、xDS dynamic config、Istio / Gateway 底層">Envoy</a></td>
          <td>Service proxy</td>
          <td>xDS、dynamic config、mesh data plane 與 traffic policy 如何治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/aws-elb/" data-link-title="AWS ELB（ALB / NLB / CLB）" data-link-desc="AWS managed load balancer、ALB（L7）/ NLB（L4）/ CLB（legacy）">AWS ELB</a></td>
          <td>Managed LB</td>
          <td>ALB / NLB、health check、draining 與 target group 如何支援 AWS 入口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/terraform/" data-link-title="Terraform / OpenTofu" data-link-desc="Infrastructure as Code 主流工具">Terraform / OpenTofu</a></td>
          <td>IaC</td>
          <td>state、plan、provider、drift 與 review gate 如何管理 infra 變更</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/traefik/" data-link-title="Traefik" data-link-desc="Cloud-native ingress / reverse proxy、auto-discovery">Traefik</a></td>
          <td>Ingress / proxy</td>
          <td>auto-discovery、dynamic routing 與 cloud-native ingress 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/consul/" data-link-title="Consul" data-link-desc="Service registry / mesh / KV / DNS">Consul</a></td>
          <td>Registry / mesh</td>
          <td>service registry、DNS、health check、KV 與 mesh 邊界如何取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="kubernetes/">Kubernetes</a></td>
          <td><a href="kubernetes/graceful-shutdown/">graceful-shutdown</a></td>
          <td><a href="kubernetes/migrate-from-docker-swarm/">← Docker Swarm</a></td>
      </tr>
      <tr>
          <td><a href="terraform/">Terraform</a></td>
          <td>—</td>
          <td><a href="terraform/migrate-to-opentofu/">→ OpenTofu</a></td>
      </tr>
      <tr>
          <td><a href="consul/">Consul</a></td>
          <td>—</td>
          <td><a href="consul/migrate-from-etcd/">← etcd</a></td>
      </tr>
  </tbody>
</table>
<p>其他 T1 vendor（Docker / systemd / nginx / Envoy / AWS ELB / Traefik）尚未開始。對應的 backlog 議題見上方「T1 服務頁大綱」段每個服務頁要回答的核心問題、跟各 vendor <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>部署服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 runtime、orchestration、traffic entry、IaC、registry 還是 mesh</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>rollout frequency、instance count、long connection、multi-region、team ownership 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>VM、container、Kubernetes、managed platform、service mesh、simple proxy 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>upgrade、config drift、certificate、health check、drain、state、rollback</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>deploy marker、per-version SLI、health check、drain completion、plan diff、registry freshness</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Tradeshift、Condé Nast、Orbitera 與平台切換案例如何提供回退判準</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>部署服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 runtime、process supervisor、orchestrator、proxy、LB、IaC 還是 registry</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷 lifecycle、traffic、config、resource 與 rollback contract</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「服務如何啟動、接流量、擴容、停止、回退」快速定位平台層</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>image、unit、deployment、health check、drain、TLS、plan、registry</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>systemd、Docker、Kubernetes、managed runtime、proxy、service mesh 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>multi-cluster、service mesh、dynamic config、IaC drift、managed runtime</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>readiness、liveness、drain timeout、target health、config drift、state lock</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>單機服務回 systemd、多服務平台上 Kubernetes、簡單入口用 managed LB</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整 YAML / HCL 語法百科、雲端平台所有產品線、語言 framework deployment</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 5.C migration cases、6 release gate、8 decision log</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>本模組 9 個 vendor 跨 6 個 category（orchestrator / container / process / proxy / LB / IaC / registry）、不是同類產品的多個選項。對照表用「橫向工程議題」標明每個議題在哪些 vendor 是核心責任、哪些不適用。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>K8s</th>
          <th>Docker</th>
          <th>systemd</th>
          <th>nginx</th>
          <th>Envoy</th>
          <th>AWS ELB</th>
          <th>Terraform</th>
          <th>Traefik</th>
          <th>Consul</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主責任</td>
          <td>orchestration</td>
          <td>container build/run</td>
          <td>process supervisor</td>
          <td>reverse proxy</td>
          <td>service proxy</td>
          <td>managed LB</td>
          <td>IaC state</td>
          <td>ingress proxy</td>
          <td>registry / mesh</td>
      </tr>
      <tr>
          <td>服務生命週期</td>
          <td>pod lifecycle</td>
          <td>container run</td>
          <td>service unit</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>target health</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>health check</td>
      </tr>
      <tr>
          <td>流量入口</td>
          <td>Service/Ingress</td>
          <td>port mapping</td>
          <td>listen socket</td>
          <td>HTTP server</td>
          <td>listener</td>
          <td>listener</td>
          <td>N/A</td>
          <td>entrypoint</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>配置模式</td>
          <td>declarative</td>
          <td>imperative</td>
          <td>declarative</td>
          <td>static config</td>
          <td>xDS dynamic</td>
          <td>API / IaC</td>
          <td>declarative</td>
          <td>dynamic provider</td>
          <td>KV + watch</td>
      </tr>
      <tr>
          <td>Service discovery</td>
          <td>K8s DNS</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>manual upstream</td>
          <td>xDS EDS</td>
          <td>target group</td>
          <td>provider data</td>
          <td>provider 自動</td>
          <td>registry 原生</td>
      </tr>
      <tr>
          <td>Health check</td>
          <td>probe</td>
          <td>healthcheck</td>
          <td>restart policy</td>
          <td>upstream check</td>
          <td>active/passive</td>
          <td>health check</td>
          <td>N/A</td>
          <td>health check</td>
          <td>health check</td>
      </tr>
      <tr>
          <td>TLS / mTLS</td>
          <td>cert-manager</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>ssl module</td>
          <td>filter chain</td>
          <td>ACM</td>
          <td>provider data</td>
          <td>ACME 自動</td>
          <td>Connect mTLS</td>
      </tr>
      <tr>
          <td>Multi-cluster</td>
          <td>federation</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>manual</td>
          <td>mesh control plane</td>
          <td>cross-region</td>
          <td>provider chain</td>
          <td>per cluster</td>
          <td>DC federation</td>
      </tr>
      <tr>
          <td>授權模式</td>
          <td>Apache 2</td>
          <td>Apache 2 / Desktop license</td>
          <td>LGPL</td>
          <td>BSD-2 / Plus 商業</td>
          <td>Apache 2</td>
          <td>AWS managed</td>
          <td>BSL / OpenTofu MPL</td>
          <td>MIT / Hub 商業</td>
          <td>BSL</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>C1/C2/C3/C4/C8</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>C5</td>
          <td>C9</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題該怎麼定位（不該強塞跟它無關的議題）</li>
<li>讀者理解「9 vendor 不是同類選一個、是不同 layer 各自一個」</li>
<li>評估部署 stack：選 orchestrator + container + proxy + LB + IaC + registry 各 1-2 個組合</li>
</ul>
<p>下面 5 段把對照表的關鍵橫向議題展開（不是每行都展開 — 部分行如「主責任」「授權模式」直接看表即可）。</p>
<h3 id="配置模式">配置模式</h3>
<p>配置模式跨 vendor 差異大、影響 dev workflow 跟 GitOps 整合度。<strong>K8s</strong> declarative（kubectl apply / YAML）；<strong>Terraform</strong> declarative（HCL）；<strong>systemd</strong> declarative（unit file）；<strong>Docker</strong> imperative（CLI）+ Compose declarative；<strong>nginx</strong> static config + reload；<strong>Envoy</strong> xDS dynamic（control plane push）；<strong>Traefik</strong> dynamic（provider 自動 sync）；<strong>AWS ELB</strong> API + IaC；<strong>Consul</strong> KV + watch。</p>
<p>選型判讀：要 GitOps → declarative（K8s + Terraform + systemd unit）；要 zero-reload → dynamic config（Envoy / Traefik）；要 manual control → imperative（Docker / 純 CLI）。</p>
<h3 id="service-discovery--health-check">Service discovery + Health check</h3>
<p>Service discovery 是 5 模組多個 vendor 共同關心的議題、但實作差異大。<strong>K8s</strong> 內建（Service + DNS + kube-proxy）；<strong>Consul</strong> registry first + DNS interface + health check 內建；<strong>Envoy</strong> EDS（xDS endpoint discovery）；<strong>Traefik</strong> provider 自動發現；<strong>nginx / AWS ELB</strong> 配置 upstream target；<strong>Docker / systemd</strong> N/A（單機 / 不負責 discovery）。</p>
<p>選型判讀：K8s-only → 內建；非 K8s 多平台 → Consul；K8s + service mesh → Istio + Envoy；單機 → nginx + manual config。</p>
<h3 id="multi-cluster--跨-dc">Multi-cluster / 跨 DC</h3>
<p>跨多 cluster / DC 拓樸差異大。<strong>K8s</strong> federation（v2 / Cluster API multi-cluster）；<strong>Consul</strong> 一級公民跨 DC（WAN federation）；<strong>Envoy + Istio</strong> multi-cluster mesh；<strong>Terraform</strong> 用 provider chain 管多 cloud / 多 cluster；<strong>AWS ELB</strong> cross-region replication；<strong>nginx / Traefik</strong> 一般 per cluster；<strong>systemd / Docker</strong> N/A。</p>
<p>選型判讀：跨 DC 為核心需求 → Consul / Istio；單一 cluster + cross-region LB → ELB / Global LB；多 cluster K8s → Cluster API + federation。</p>
<h3 id="tls--mtls">TLS / mTLS</h3>
<p>TLS / mTLS 在不同 vendor 由不同 layer 負責。<strong>K8s</strong> cert-manager（Let&rsquo;s Encrypt / 內部 CA）；<strong>AWS ELB</strong> ACM 自動憑證；<strong>Traefik</strong> ACME 自動 TLS；<strong>nginx</strong> ssl module + manual cert / cert-manager；<strong>Envoy</strong> filter chain（SDS 動態 cert）；<strong>Consul Connect</strong> mTLS 自動 sidecar；<strong>Terraform</strong> 不負責 TLS、提供 provider；<strong>Docker / systemd</strong> 不負責（交給 application 或上游 proxy）。</p>
<p>選型判讀：cluster 內 mTLS → cert-manager / Consul Connect；外部 TLS → ACME（Traefik / 自管 cert-manager）；managed → AWS ELB / Cloudflare。</p>
<h3 id="授權模式2023-2024-bsl-變動">授權模式（2023-2024 BSL 變動）</h3>
<p>2023-2024 多個 HashiCorp 產品改 BSL（Terraform / Vault / Consul / Boundary / Vagrant）— 影響採用決策。<strong>Terraform</strong> → OpenTofu fork（Linux Foundation、MPL 2.0）；<strong>Consul</strong> → 暫無大型 fork；<strong>Docker Desktop</strong> → 商業 license（員工 &gt; 250 / 收入 &gt; $10M）→ Podman Desktop 替代；<strong>nginx</strong> → F5 後 OSS 不滿 → Freenginx / angie fork；<strong>K8s / Envoy / Traefik</strong> → 仍 OSI 開源。</p>
<p>選型判讀：商業 SaaS 提供類似服務 → 避 BSL（用 OpenTofu / 自評）；企業內部使用 → BSL 多數無影響；公部門 / 嚴格合規 → 仍要 OSI 認可 license。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>D1</td>
          <td>Docker / systemd</td>
          <td>建立 runtime、entrypoint、process supervisor 與單機服務 baseline</td>
      </tr>
      <tr>
          <td>D2</td>
          <td>Kubernetes</td>
          <td>建立 workload lifecycle、orchestration、probe 與 rollout contract</td>
      </tr>
      <tr>
          <td>D3</td>
          <td>nginx / AWS ELB / Envoy / Traefik</td>
          <td>建立 traffic entry、drain、timeout 與 proxy policy 對照</td>
      </tr>
      <tr>
          <td>D4</td>
          <td>Terraform / OpenTofu / Consul</td>
          <td>建立 infra state、service registry 與 control-plane boundary</td>
      </tr>
      <tr>
          <td>D5</td>
          <td>ECS / Fargate / Cloud Run / Nomad</td>
          <td>補 managed runtime、platform abstraction 與自管調度對照</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitOps / package</td>
          <td>Argo CD、Flux、Helm、Kustomize</td>
          <td>desired state、release review、config drift、environment promotion</td>
      </tr>
      <tr>
          <td>Ingress / Gateway</td>
          <td>ingress-nginx、Envoy Gateway、Gateway API、HAProxy</td>
          <td>routing contract、TLS、cross-namespace policy、drain</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>Istio、Linkerd、Cilium Service Mesh</td>
          <td>mTLS、traffic split、sidecar / ambient、control-plane cost</td>
      </tr>
      <tr>
          <td>Managed runtime</td>
          <td>ECS、Fargate、Cloud Run、Azure Container Apps、Fly.io</td>
          <td>managed scaling、deployment contract、platform limit</td>
      </tr>
      <tr>
          <td>Alternative orchestrator</td>
          <td>Nomad、OpenShift、Rancher</td>
          <td>operations model、multi-cluster governance、enterprise support</td>
      </tr>
      <tr>
          <td>IaC / PaaS</td>
          <td>Pulumi、Heroku、Railway、Vercel</td>
          <td>developer workflow、state ownership、backend suitability</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 runtime、orchestration、ingress / gateway、GitOps、IaC 與 mesh。Kubernetes 是 orchestration baseline；Argo CD / Flux / Helm / Kustomize 解 desired state delivery；ingress-nginx / Envoy Gateway / HAProxy 解 traffic entry；Istio / Linkerd / Cilium 解 service-to-service policy；ECS / Fargate / Cloud Run 解 managed runtime。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes deployment</a></li>
<li>上游：<a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 Load Balancer Contract</a></li>
<li>案例：<a href="/blog/backend/05-deployment-platform/cases/" data-link-title="模組五案例正文" data-link-desc="部署平台轉換案例入口。">5.C 部署平台案例正文</a></li>
<li>服務路徑：<a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback</a></li>
</ul>
]]></content:encoded></item><item><title>資料庫 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/</guid><description>&lt;p>資料庫 Vendor 清單的核心責任是把 database 服務名稱放回正式狀態、交易邊界、查詢模型、schema 演進、容量與資料治理的判斷。每個服務頁先說明它承擔的資料責任，再比較適用場景、容量邊界、替代服務、操作成本、案例對照與下一步路由。&lt;/p>
&lt;p>資料庫服務頁的共同讀法是先用 PostgreSQL / MySQL 建立 SQL baseline，再看 managed SQL、KV / document 與 global distributed SQL 如何改變團隊責任。&lt;/p>
&lt;p>資料庫 vendor 文章的撰寫規格見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">資料庫 Vendor 文章撰寫規格&lt;/a>。該規格把 PostgreSQL / MySQL batch 的經驗整理成三個 surface：vendor overview 負責第一輪服務判斷，deep article 負責單一機制的操作與除錯，migration playbook 負責跨 vendor、跨 topology 或跨 operational model 的階段化變更。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>資料庫服務頁的教學順序是先建立 SQL baseline，再處理 embedded / local、document / KV、managed SQL 與 global distributed SQL。這個順序對齊 checkout E1：讀者先理解正式狀態、transaction、schema migration 與 query boundary，再比較哪些服務把操作責任交給平台，哪些服務改變資料模型或一致性成本。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a>&lt;/td>
 &lt;td>SQL baseline&lt;/td>
 &lt;td>transaction、schema、query、extension 與操作成熟度如何成為比較基準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a>&lt;/td>
 &lt;td>SQL baseline&lt;/td>
 &lt;td>高併發 OLTP、replication、online schema change 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding&lt;/a> 生態如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a>&lt;/td>
 &lt;td>Embedded SQL&lt;/td>
 &lt;td>單機正式狀態、測試資料、edge / local DB 與低操作成本如何成立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a>&lt;/td>
 &lt;td>Document database&lt;/td>
 &lt;td>document shape、index、schema flexibility 與 transaction 邊界如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a>&lt;/td>
 &lt;td>Managed KV / document&lt;/td>
 &lt;td>partition key、access pattern、容量計費與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition&lt;/a> 如何設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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&lt;/a>&lt;/td>
 &lt;td>Managed SQL&lt;/td>
 &lt;td>storage / compute 分離、failover、replica 與 AWS operation model 如何轉移責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner&lt;/a>&lt;/td>
 &lt;td>Global SQL&lt;/td>
 &lt;td>TrueTime、strong consistency、multi-region latency 與成本如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a>&lt;/td>
 &lt;td>Global multi-model&lt;/td>
 &lt;td>consistency level、API model、partition 與 Azure 約束如何影響架構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB&lt;/a>&lt;/td>
 &lt;td>Distributed SQL&lt;/td>
 &lt;td>SQL 相容、range lease、multi-region 與自管 / managed 邊界如何判斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>資料庫 Vendor 清單的核心責任是把 database 服務名稱放回正式狀態、交易邊界、查詢模型、schema 演進、容量與資料治理的判斷。每個服務頁先說明它承擔的資料責任，再比較適用場景、容量邊界、替代服務、操作成本、案例對照與下一步路由。</p>
<p>資料庫服務頁的共同讀法是先用 PostgreSQL / MySQL 建立 SQL baseline，再看 managed SQL、KV / document 與 global distributed SQL 如何改變團隊責任。</p>
<p>資料庫 vendor 文章的撰寫規格見 <a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">資料庫 Vendor 文章撰寫規格</a>。該規格把 PostgreSQL / MySQL batch 的經驗整理成三個 surface：vendor overview 負責第一輪服務判斷，deep article 負責單一機制的操作與除錯，migration playbook 負責跨 vendor、跨 topology 或跨 operational model 的階段化變更。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>資料庫服務頁的教學順序是先建立 SQL baseline，再處理 embedded / local、document / KV、managed SQL 與 global distributed SQL。這個順序對齊 checkout E1：讀者先理解正式狀態、transaction、schema migration 與 query boundary，再比較哪些服務把操作責任交給平台，哪些服務改變資料模型或一致性成本。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></td>
          <td>SQL baseline</td>
          <td>transaction、schema、query、extension 與操作成熟度如何成為比較基準</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a></td>
          <td>SQL baseline</td>
          <td>高併發 OLTP、replication、online schema change 與 <a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding</a> 生態如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a></td>
          <td>Embedded SQL</td>
          <td>單機正式狀態、測試資料、edge / local DB 與低操作成本如何成立</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a></td>
          <td>Document database</td>
          <td>document shape、index、schema flexibility 與 transaction 邊界如何治理</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Managed KV / document</td>
          <td>partition key、access pattern、容量計費與 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a> 如何設計</td>
      </tr>
      <tr>
          <td><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></td>
          <td>Managed SQL</td>
          <td>storage / compute 分離、failover、replica 與 AWS operation model 如何轉移責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner</a></td>
          <td>Global SQL</td>
          <td>TrueTime、strong consistency、multi-region latency 與成本如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a></td>
          <td>Global multi-model</td>
          <td>consistency level、API model、partition 與 Azure 約束如何影響架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a></td>
          <td>Distributed SQL</td>
          <td>SQL 相容、range lease、multi-region 與自管 / managed 邊界如何判斷</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="postgresql/">PostgreSQL</a></td>
          <td><a href="postgresql/autovacuum-tuning/">autovacuum-tuning</a> / <a href="postgresql/connection-scaling/">connection-scaling</a> / <a href="postgresql/connection-pooler-comparison/">connection-pooler-comparison</a> / <a href="postgresql/pgbouncer-config/">pgbouncer-config</a> / <a href="postgresql/declarative-partitioning/">declarative-partitioning</a> / <a href="postgresql/pg-partman-advanced/">pg-partman-advanced</a> / <a href="postgresql/logical-replication-debezium/">logical-replication-debezium</a> / <a href="postgresql/logical-decoding-plugins/">logical-decoding-plugins</a> / <a href="postgresql/replication-topology/">replication-topology</a> / <a href="postgresql/replication-slot-management/">replication-slot-management</a> / <a href="postgresql/patroni-ha/">patroni-ha</a> / <a href="postgresql/pitr-wal-archiving/">pitr-wal-archiving</a> / <a href="postgresql/cross-region-dr/">cross-region-dr</a> / <a href="postgresql/online-schema-change/">online-schema-change</a> / <a href="postgresql/query-optimization/">query-optimization</a> / <a href="postgresql/index-selection/">index-selection</a> / <a href="postgresql/mvcc-lock-model/">mvcc-lock-model</a> / <a href="postgresql/citus-distributed/">citus-distributed</a> / <a href="postgresql/bdr-multi-master/">bdr-multi-master</a> / <a href="postgresql/sql-features-baseline/">sql-features-baseline</a> / <a href="postgresql/jsonb-deep-dive/">jsonb-deep-dive</a> / <a href="postgresql/extension-ecosystem/">extension-ecosystem</a> / <a href="postgresql/specialized-pg-variants/">specialized-pg-variants</a> / <a href="postgresql/timescaledb-deep-dive/">timescaledb-deep-dive</a> / <a href="postgresql/pgvector-deep-dive/">pgvector-deep-dive</a> / <a href="postgresql/postgis-deep-dive/">postgis-deep-dive</a> / <a href="postgresql/full-text-search/">full-text-search</a> / <a href="postgresql/security-rls-audit-logging/">security-rls-audit-logging</a> / <a href="postgresql/managed-pg-comparison/">managed-pg-comparison</a> / <a href="postgresql/aurora-io-optimized-cost/">aurora-io-optimized-cost</a> / <a href="postgresql/developer-dba-responsibility-split/">developer-dba-responsibility-split</a></td>
          <td><a href="postgresql/major-version-upgrade/">major-version-upgrade</a> / <a href="postgresql/migrate-to-aurora/">→ Aurora</a> / <a href="postgresql/migrate-to-aurora-dsql/">→ Aurora DSQL</a> / <a href="postgresql/migrate-to-cockroachdb/">→ CockroachDB</a> / <a href="postgresql/migrate-to-yugabytedb-tidb/">→ YugabyteDB / TiDB</a> / <a href="postgresql/multi-region-gdpr-rollout/">multi-region-gdpr-rollout</a> / <a href="postgresql/partition-redesign/">partition-redesign</a></td>
      </tr>
      <tr>
          <td><a href="mysql/">MySQL</a></td>
          <td><a href="mysql/replication-topology/">replication-topology</a> / <a href="mysql/multi-source-replication/">multi-source-replication</a> / <a href="mysql/group-replication/">group-replication</a> / <a href="mysql/orchestrator-failover/">orchestrator-failover</a> / <a href="mysql/online-schema-change-tools/">online-schema-change-tools</a> / <a href="mysql/proxysql-config/">proxysql-config</a> / <a href="mysql/innodb-tuning/">innodb-tuning</a> / <a href="mysql/cross-buffer-memory-contention/">cross-buffer-memory-contention</a> / <a href="mysql/metadata-lock-deep-dive/">metadata-lock-deep-dive</a> / <a href="mysql/lock-contention/">lock-contention</a> / <a href="mysql/binlog-cdc/">binlog-cdc</a> / <a href="mysql/pitr-backup/">pitr-backup</a> / <a href="mysql/vitess-sharding/">vitess-sharding</a> / <a href="mysql/partitioning/">partitioning</a> / <a href="mysql/query-optimization/">query-optimization</a> / <a href="mysql/modern-sql-features/">modern-sql-features</a> / <a href="mysql/document-store-x-protocol/">document-store-x-protocol</a> / <a href="mysql/heatwave-olap-addon/">heatwave-olap-addon</a> / <a href="mysql/encryption-tls-key-management/">encryption-tls-key-management</a> / <a href="mysql/audit-log-siem/">audit-log-siem</a></td>
          <td><a href="mysql/major-version-upgrade/">major-version-upgrade</a> / <a href="mysql/migrate-to-postgresql/">→ PostgreSQL</a> / <a href="mysql/migrate-to-aurora/">→ Aurora</a> / <a href="mysql/migrate-to-planetscale/">→ PlanetScale</a> / <a href="mysql/migrate-vitess-to-planetscale/">Vitess → PlanetScale</a></td>
      </tr>
      <tr>
          <td><a href="sqlite/">SQLite</a></td>
          <td><a href="sqlite/file-lifecycle-backup-boundary/">file-lifecycle-backup-boundary</a> / <a href="sqlite/wal-concurrency-locking/">wal-concurrency-locking</a> / <a href="sqlite/pragma-tuning-performance/">pragma-tuning-performance</a> / <a href="sqlite/schema-migration-versioning/">schema-migration-versioning</a> / <a href="sqlite/sql-dialect-index-limits/">sql-dialect-index-limits</a> / <a href="sqlite/observability-runbook/">observability-runbook</a> / <a href="sqlite/test-fixture-best-practice/">test-fixture-best-practice</a> / <a href="sqlite/mobile-desktop-embedded-store/">mobile-desktop-embedded-store</a> / <a href="sqlite/local-first-sync-boundary/">local-first-sync-boundary</a> / <a href="sqlite/litestream-litefs-replication/">litestream-litefs-replication</a> / <a href="sqlite/d1-turso-libsql-comparison/">d1-turso-libsql-comparison</a></td>
          <td><a href="sqlite/migrate-from-postgresql-simplification/">migrate-from-postgresql-simplification</a> / <a href="sqlite/migrate-to-postgresql/">→ PostgreSQL</a> / <a href="sqlite/migrate-to-d1-turso/">→ D1 / Turso</a></td>
      </tr>
      <tr>
          <td><a href="mongodb/">MongoDB</a></td>
          <td><a href="mongodb/schema-design-pattern/">schema-design-pattern</a> (SSoT Frame 1 MongoDB 適用度) / <a href="mongodb/shard-key-selection/">shard-key-selection</a> / <a href="mongodb/replica-set-read-preference/">replica-set-read-preference</a> / <a href="mongodb/aggregation-pipeline-optimization/">aggregation-pipeline-optimization</a> / <a href="mongodb/change-streams-kafka/">change-streams-kafka</a> / <a href="mongodb/connection-management-and-cache-layer/">connection-management-and-cache-layer</a></td>
          <td><a href="mongodb/migrate-to-atlas/">→ Atlas</a> / <a href="mongodb/shard-expansion-multi-dc/">shard-expansion-multi-dc</a></td>
      </tr>
      <tr>
          <td><a href="dynamodb/">DynamoDB</a></td>
          <td><a href="dynamodb/single-table-design-pattern/">single-table-design-pattern</a> (SSoT Frame 1 DynamoDB 適用度) / <a href="dynamodb/partition-key-antipatterns/">partition-key-antipatterns</a> / <a href="dynamodb/gsi-lsi-design/">gsi-lsi-design</a> / <a href="dynamodb/on-demand-vs-provisioned/">on-demand-vs-provisioned</a> (SSoT Frame 8 event-driven scaling) / <a href="dynamodb/global-tables-conflict/">global-tables-conflict</a> / <a href="dynamodb/consistency-model-optimization/">consistency-model-optimization</a> / <a href="dynamodb/transactions-conditional-writes/">transactions-conditional-writes</a> / <a href="dynamodb/dax-caching-strategy/">dax-caching-strategy</a> / <a href="dynamodb/streams-lambda-event-driven/">streams-lambda-event-driven</a> / <a href="dynamodb/ttl-data-lifecycle/">ttl-data-lifecycle</a></td>
          <td><a href="dynamodb/migrate-rds-mongodb-to-dynamodb/">migrate-rds-mongodb-to-dynamodb</a> (Type E paradigm shift)</td>
      </tr>
      <tr>
          <td><a href="aurora/">Aurora</a></td>
          <td><a href="aurora/storage-architecture/">storage-architecture</a> / <a href="aurora/cross-az-failover-rto/">cross-az-failover-rto</a> / <a href="aurora/read-replica-scaling/">read-replica-scaling</a> (SSoT Aurora fleet 治理 + Frame 8 共寫) / <a href="aurora/global-database-multi-region/">global-database-multi-region</a> / <a href="aurora/migrate-from-self-managed-pg-mysql/">migrate-from-self-managed-pg-mysql</a> / <a href="aurora/serverless-v2-scaling/">serverless-v2-scaling</a> / <a href="aurora/multi-cluster-business-split/">multi-cluster-business-split</a> / <a href="aurora/rds-proxy-connection-pooling/">rds-proxy-connection-pooling</a> / <a href="aurora/aurora-vs-dsql-tradeoff/">aurora-vs-dsql-tradeoff</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="spanner/">Spanner</a></td>
          <td><a href="spanner/truetime-api-depth/">truetime-api-depth</a> / <a href="spanner/consistency-models-comparison/">consistency-models-comparison</a> / <a href="spanner/schema-migration-interleaved-tables/">schema-migration-interleaved-tables</a> / <a href="spanner/migrate-from-cloud-sql-pg/">migrate-from-cloud-sql-pg</a> / <a href="spanner/change-streams-cdc/">change-streams-cdc</a> / <a href="spanner/postgresql-dialect/">postgresql-dialect</a> / <a href="spanner/spanner-graph/">spanner-graph</a> / <a href="spanner/bigquery-federation/">bigquery-federation</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="cosmosdb/">Cosmos DB</a></td>
          <td><a href="cosmosdb/consistency-levels-engineering/">consistency-levels-engineering</a> / <a href="cosmosdb/partition-key-design/">partition-key-design</a> / <a href="cosmosdb/ru-cost-model-sizing/">ru-cost-model-sizing</a> / <a href="cosmosdb/mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> (SSoT Document 三型遷移 + Cosmos Frame 1) / <a href="cosmosdb/multi-region-write-conflict/">multi-region-write-conflict</a> (SSoT Strong + multi-region 互斥) / <a href="cosmosdb/change-feed-cdc/">change-feed-cdc</a> / <a href="cosmosdb/stored-procedure-trigger/">stored-procedure-trigger</a> / <a href="cosmosdb/cosmos-for-postgresql/">cosmos-for-postgresql</a> / <a href="cosmosdb/synapse-link-federation/">synapse-link-federation</a></td>
          <td><a href="cosmosdb/migrate-from-mongodb-cassandra/">migrate-from-mongodb-cassandra</a> (Type B drop-in / Type E paradigm)</td>
      </tr>
      <tr>
          <td><a href="cockroachdb/">CockroachDB</a></td>
          <td><a href="cockroachdb/hlc-raft-consensus/">hlc-raft-consensus</a> / <a href="cockroachdb/survival-goals/">survival-goals</a> / <a href="cockroachdb/transaction-retry-pattern/">transaction-retry-pattern</a> / <a href="cockroachdb/locality-aware-schema/">locality-aware-schema</a> / <a href="cockroachdb/aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a> (SSoT DB4 entry + cluster boundary 顆粒) / <a href="cockroachdb/multi-region-table-config/">multi-region-table-config</a> / <a href="cockroachdb/cloud-serverless/">cloud-serverless</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>DB1（PostgreSQL / MySQL）/ DB2（SQLite）/ DB3（MongoDB / DynamoDB）/ DB4（Aurora / Spanner / Cosmos DB / CockroachDB）四批 vendor 的 deep article 都已鋪滿。DB3 跟 DB4 batch 共新增 31 篇 deep article + 1 篇 DB3 entry article（<a href="db3-vendor-selection/">db3-vendor-selection</a>）+ 1 篇 DB4 entry article（<a href="cockroachdb/aurora-dsql-spanner-decision-tree/">cockroachdb/aurora-dsql-spanner-decision-tree</a>）。PostgreSQL 與 MySQL 也保留 hands-on 子目錄，集中放可重現的 lab；hands-on 與覆蓋表互補、不重複列在這張表。SQLite 的 <a href="sqlite/teaching-structure/">teaching-structure</a> 是該服務章節群的大綱、不視為 deep article。後續批次 backlog 見下方各 vendor _index.md 的「後續擴充（仍待補）」段。</p>
<p>DB5 batch（BaaS 資料層）新增 <a href="firestore/">Firestore</a> overview（serverless document、client 直連 + Security Rules、查詢邊界、realtime / offline）+ <a href="firestore/migrate-to-relational/">→ 自建 relational</a> 遷移 playbook（Type E paradigm shift、存取模型反轉）+ 4 篇 deep article：<a href="firestore/security-rules-authz-modeling/">Security Rules 授權建模</a>、<a href="firestore/distributed-counter-high-frequency-write/">distributed counter 高頻寫入</a>、<a href="firestore/denormalization-fanout-consistency/">document 反正規化與一致性</a>、<a href="firestore/realtime-listener-fanout-cost/">realtime listener 扇出與成本</a>。這批的定位是把 BaaS（<a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">Firebase</a> / Firestore）的資料層面放回 vendor 視角：要不要採用 BaaS 這種交付形態本身是 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a> / <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</a> 的選型層決策、本模組只負責「Firestore 作為 document store 承擔什麼狀態責任、撞牆後如何遷往自建」。deep article 章節群讀法見 <a href="firestore/#deep-article-%e7%ab%a0%e7%af%80%e7%be%a4">Firestore overview 的 Deep article 章節群段</a>；<a href="firestore/hands-on/">hands-on 章節群</a> 提供 3 個 Firebase Emulator lab（emulator quickstart、Security Rules 測試、distributed counter）。Supabase 的資料層不另開 vendor 頁 — 它的 Postgres 面寫在 <a href="postgresql/managed-pg-comparison/">managed-pg-comparison</a> 的比較表一行、選型層錨點在 <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</a>（見該章「跨能力 bundle 的特殊判讀」段）、SSoT 不重複。</p>
<h2 id="cross-vendor-ssot-主寫位置">Cross-vendor SSoT 主寫位置</h2>
<p>跨 vendor 共寫 frame 在多篇 deep article 之間 cross-link、但每個 frame 都有 <em>單一 SSoT 主寫位置</em>、其他 article 只 cross-link 不重複展開。讀者從 entry article 或覆蓋表進來時、可以直接跳對應 SSoT 看完整推導：</p>
<ul>
<li><strong>Aurora fleet 治理（business sharding / microservice / 合規 driver）</strong>：<a href="aurora/read-replica-scaling/">aurora/read-replica-scaling</a> — DraftKings 200 cluster fleet、何時拆 cluster vs 加 replica 的 6 條判讀順序</li>
<li><strong>CockroachDB cluster boundary 顆粒（per-app cluster vs 邏輯一個 cluster）</strong>：<a href="cockroachdb/aurora-dsql-spanner-decision-tree/">cockroachdb/aurora-dsql-spanner-decision-tree</a> — 已選 CockroachDB 後的拓樸決策、跟 vendor 選擇分流</li>
<li><strong>Strong consistency + multi-region write 互斥（CAP 硬約束）</strong>：<a href="cosmosdb/multi-region-write-conflict/">cosmosdb/multi-region-write-conflict</a> — 跨 region active-active write 三家機制對比 + Cosmos DB Strong / multi-region 互斥根因</li>
<li><strong>Document model 三型遷移（保留 / 換託管 / 換 vendor 保留 model）</strong>：<a href="cosmosdb/mongodb-api-vs-sql-api/">cosmosdb/mongodb-api-vs-sql-api</a> — wire compat ≠ 100% 行為相同、dual-write per query pattern 驗證</li>
<li><strong>Frame 1 vendor 適用度判讀 — MongoDB</strong>：<a href="mongodb/schema-design-pattern/">mongodb/schema-design-pattern</a> — document workload 適配軸 + aggregate root 邊界</li>
<li><strong>Frame 1 vendor 適用度判讀 — DynamoDB</strong>：<a href="dynamodb/single-table-design-pattern/">dynamodb/single-table-design-pattern</a> — KV 4 軸前置判讀 + access pattern 穩定度</li>
<li><strong>Frame 1 vendor 適用度判讀 — Cosmos DB</strong>：<a href="cosmosdb/mongodb-api-vs-sql-api/">cosmosdb/mongodb-api-vs-sql-api</a> — Cosmos DB 跟 Document migration 同 SSoT、API model 四層 framing</li>
<li><strong>Frame 8 event-driven scaling 5 模式（surge / burst / sustained / scheduled / predictive）</strong>：<a href="dynamodb/on-demand-vs-provisioned/">dynamodb/on-demand-vs-provisioned</a> + <a href="aurora/read-replica-scaling/">aurora/read-replica-scaling</a> 共寫 — Tixcraft 6750x spike / Capcom predictive / DraftKings scheduled 三 case 對應</li>
<li><strong>Partition / shard key 可逆性跨 vendor 對照</strong>：<a href="db3-vendor-selection/">db3-vendor-selection</a> 三 vendor 對比 10 軸 + 軸的延伸子段「Partition / shard key 可逆性」 — MongoDB（4.4+ 可改）/ DynamoDB（backfill 可改）/ Cosmos DB（不可改）三家不在同一光譜、決定 selection 階段 access pattern audit 嚴格度。3 篇 deep article（mongodb/shard-key-selection、dynamodb/partition-key-antipatterns、cosmosdb/partition-key-design）各自展開本 vendor 內部設計、不重複跨 vendor 對照</li>
</ul>
<p>DB3 / DB4 entry article（<a href="db3-vendor-selection/">db3-vendor-selection</a> + <a href="cockroachdb/aurora-dsql-spanner-decision-tree/">cockroachdb/aurora-dsql-spanner-decision-tree</a>）承擔 <em>跨 vendor 選型 driver path</em> SSoT、上列 SSoT 是 <em>單一 frame 跨 vendor 共寫</em> 主寫位置、兩層 SSoT 不重疊。</p>
<h2 id="cross-vendor-entry-point">Cross-vendor entry point</h2>
<p>跨 vendor 選型不該直接讀單一 vendor overview — 先用 entry article 判斷 driver path、再進個別 vendor 深度：</p>
<ul>
<li><strong>DB3 入口</strong>（document / KV / multi-model 三家對比）：<a href="db3-vendor-selection/">db3-vendor-selection</a> — workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸</li>
<li><strong>DB4 入口</strong>（distributed SQL 三家對比）：<a href="cockroachdb/aurora-dsql-spanner-decision-tree/">cockroachdb/aurora-dsql-spanner-decision-tree</a> — 撞牆訊號分型（DoorDash 單主寫入 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）+ 七問題決策樹（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）</li>
</ul>
<p>兩個 entry 都用 case-driven driver 視角切入、不只是「特性對照表」— 讀者帶具體的 production 訊號（撞牆 case / 資料形狀變化 / 合規邊界）進來、entry 才會指向正確的 vendor。</p>
<h2 id="服務頁教學功能">服務頁教學功能</h2>
<p>資料庫服務頁的共同檢查軸是教學功能，而非固定章節順序。PostgreSQL、SQLite、MongoDB、DynamoDB 與 Spanner 的服務對象不同，頁面可以用不同標題展開，但都要讓讀者學會正式狀態、資料形狀、交易需求、查詢邊界、容量與操作責任的判斷。</p>
<table>
  <thead>
      <tr>
          <th>教學功能</th>
          <th>資料庫服務頁要交付的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是正式狀態、embedded store、managed SQL、KV/document 還是 distributed SQL</td>
      </tr>
      <tr>
          <td>學習目標</td>
          <td>讀者能判斷資料形狀、交易需求、查詢邊界、容量與操作責任</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用 transaction、ad-hoc query、local state 或 global consistency 快速定位</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>schema migration、backup、restore、replica、index、connection、quota</td>
      </tr>
      <tr>
          <td>核心取捨</td>
          <td>SQL baseline、managed SQL、KV/document、<a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding</a>、multi-region、online migration、CDC、global consistency</td>
      </tr>
      <tr>
          <td>失敗快速判讀</td>
          <td>connection exhaustion、slow query、lock、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication lag</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a></td>
      </tr>
      <tr>
          <td>替代服務路由</td>
          <td>query 變複雜時回 SQL、replay 需求轉 event log、全文搜尋轉 search</td>
      </tr>
      <tr>
          <td>Scope boundary</td>
          <td>ORM 語法、語言 driver 細節、完整 DBA 手冊</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 01 主章、09 capacity case、08 incident decision log</td>
      </tr>
  </tbody>
</table>
<h2 id="後續擴充">後續擴充</h2>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>候選服務</th>
          <th>補充理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>T2</td>
          <td>Oracle Database、Microsoft SQL Server、MariaDB</td>
          <td>enterprise / commercial SQL 與 MySQL 相鄰生態</td>
      </tr>
      <tr>
          <td>T2</td>
          <td>PlanetScale / Vitess、TiDB、YugabyteDB、Neon、Supabase、Azure SQL Hyperscale</td>
          <td><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">sharding</a>、<a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a>、serverless Postgres、managed SQL</td>
      </tr>
      <tr>
          <td>T2</td>
          <td>Apache Cassandra、ScyllaDB</td>
          <td>wide-column、high-write（mobile / serverless document 已由 <a href="firestore/">Firestore</a> 覆蓋）</td>
      </tr>
      <tr>
          <td>T2</td>
          <td>OpenSearch / Elasticsearch</td>
          <td>search engine 與 log / document search 邊界</td>
      </tr>
      <tr>
          <td>T3</td>
          <td>ClickHouse、BigQuery、Snowflake</td>
          <td>OLAP / analytics，先作相鄰路由</td>
      </tr>
      <tr>
          <td>T3</td>
          <td>CouchDB、Couchbase</td>
          <td>sync / document database 的特殊場景</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是區分 OLTP、search 與 analytics。Oracle、SQL Server、MariaDB 補 enterprise SQL；Cassandra / ScyllaDB 補 wide-column；OpenSearch / Elasticsearch 補 search；ClickHouse、BigQuery、Snowflake 先保留 analytics 路由，避免資料庫服務頁承擔整個數倉教材。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB1</td>
          <td>PostgreSQL / MySQL</td>
          <td>建立 SQL baseline、transaction、schema evolution 與 connection 判準</td>
      </tr>
      <tr>
          <td>DB2</td>
          <td>SQLite</td>
          <td>建立 embedded / local formal state 與低操作成本邊界</td>
      </tr>
      <tr>
          <td>DB3</td>
          <td>MongoDB / DynamoDB</td>
          <td>建立 document / KV、access pattern、partition 與資料形狀判準</td>
      </tr>
      <tr>
          <td>DB4</td>
          <td>Aurora / Spanner / Cosmos DB / CockroachDB</td>
          <td>建立 managed / global SQL、多 region、consistency 與 vendor 約束</td>
      </tr>
      <tr>
          <td>DB5</td>
          <td>Firestore</td>
          <td>建立 BaaS 資料層視角：client 直連 document store 與撞牆後遷往自建</td>
      </tr>
  </tbody>
</table>
<h2 id="db3--db4-batch-完成紀錄">DB3 / DB4 batch 完成紀錄</h2>
<p>DB3 / DB4 batch（MongoDB / DynamoDB / Cosmos DB / Aurora / Spanner / CockroachDB 共 6 vendor、31 篇新 deep article + 2 篇 cross-vendor entry article）已完成。完成順序如下：</p>
<ul>
<li><strong>DB3</strong>：MongoDB（schema design / shard key / read preference / aggregation / change streams / connection management 6 篇）+ DynamoDB（single-table / partition key 反模式 / GSI-LSI / capacity mode / global tables 5 篇）+ Cosmos DB（consistency / partition key / RU 成本 / MongoDB API / multi-region 5 篇）+ DB3 entry article（document / KV / multi-model 三方選型）</li>
<li><strong>DB4</strong>：Aurora（storage / failover / replica scaling / global database / migration 5 篇）+ Spanner（TrueTime / consistency models / schema migration / Cloud SQL migration 4 篇）+ CockroachDB（HLC-Raft / survival goals / retry pattern / locality / DB4 decision tree 5 篇、含 DB4 entry article）</li>
</ul>
<p>各 vendor 後續 backlog（Atlas 遷移、PITR restore drill、Serverless 等）見各 vendor _index.md「後續擴充（仍待補）」段。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a></li>
<li>上游：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a></li>
<li>服務路徑：<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a></li>
</ul>
]]></content:encoded></item><item><title>LLM Deployment 供應鏈完整性</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-deployment-supply-chain/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-deployment-supply-chain/</guid><description>&lt;p>本章的責任是把 LLM 服務的模型權重、推論伺服器、第三方 plugin / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> server 三條供應鏈、納入 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任&lt;/a> 的既有框架。模型來源信任的判讀依據見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">model card&lt;/a> 卡；通用 artifact 信任機制見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">artifact-provenance&lt;/a> 卡。LLM 場景的特殊性在於模型權重既是「資料」又是「程式邏輯」、第三方 MCP 是可執行程式碼、跟一般 software artifact 的信任模型有部分差異、但 build provenance / signature / dependency isolation 等控制原則沿用同一套。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 服務的供應鏈完整性問題節點。個人 dev 視角的模型來源信任見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 模型供應鏈與信任邊界&lt;/a>；本章不重複個人 dev 場景的判讀、聚焦 production 場景下的特殊議題（規模化下載、跨 region 鏡像、retry 策略、模型 release 流程）。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：模型權重 build provenance（HF organization / 量化者 / Ollama registry）、GGUF / safetensors artifact 完整性、production 下載與鏡像策略、第三方 MCP / plugin 的 deployment 供應鏈、模型版本回退機制。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>一般 software artifact 信任 → &lt;a href="../supply-chain-integrity-and-artifact-trust/">7.4 supply-chain-integrity-and-artifact-trust&lt;/a>&lt;/li>
&lt;li>機器憑證 → &lt;a href="../secrets-and-machine-credential-governance/">7.6 secrets-and-machine-credential-governance&lt;/a>&lt;/li>
&lt;li>入口治理 → &lt;a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection&lt;/a>&lt;/li>
&lt;li>個人 dev 模型來源信任 → &lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 model-supply-chain-trust&lt;/a>&lt;/li>
&lt;li>部署平台 → &lt;code>05-deployment-platform&lt;/code>、可靠性 → &lt;code>06-reliability&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer、沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表的 control link 進 knowledge-card、看具體機制與 LLM 場景的差異。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-供應鏈的三條-chain">LLM 供應鏈的三條 chain&lt;/h2>
&lt;p>LLM 服務的供應鏈跟一般 software 服務的差異在「同時管三條 chain」：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>模型權重 chain&lt;/strong>：原始作者 → 官方 release → 量化者 → registry → production 鏡像&lt;/li>
&lt;li>&lt;strong>推論伺服器 chain&lt;/strong>：llama.cpp / vLLM / Ollama 等 server software 的一般 software artifact chain&lt;/li>
&lt;li>&lt;strong>第三方 plugin / MCP chain&lt;/strong>：MCP server / Continue.dev 等的程式碼供應鏈&lt;/li>
&lt;/ol>
&lt;p>三條 chain 在 production 階段都需要 build provenance、簽署驗證、依賴隔離跟回退機制。差異主要在模型權重 chain 的特殊性：權重是大型 binary（GB 級）、難以靜態 audit、且權重本身會影響推論行為。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 服務的模型權重、推論伺服器、第三方 plugin / <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> server 三條供應鏈、納入 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任</a> 的既有框架。模型來源信任的判讀依據見 <a href="/blog/llm/knowledge-cards/model-card/" data-link-title="Model Card" data-link-desc="Hugging Face 等平台上模型的 metadata 文件、列出模型來源、訓練資料、能力、限制、授權">model card</a> 卡；通用 artifact 信任機制見 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">artifact-provenance</a> 卡。LLM 場景的特殊性在於模型權重既是「資料」又是「程式邏輯」、第三方 MCP 是可執行程式碼、跟一般 software artifact 的信任模型有部分差異、但 build provenance / signature / dependency isolation 等控制原則沿用同一套。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 服務的供應鏈完整性問題節點。個人 dev 視角的模型來源信任見 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 模型供應鏈與信任邊界</a>；本章不重複個人 dev 場景的判讀、聚焦 production 場景下的特殊議題（規模化下載、跨 region 鏡像、retry 策略、模型 release 流程）。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：模型權重 build provenance（HF organization / 量化者 / Ollama registry）、GGUF / safetensors artifact 完整性、production 下載與鏡像策略、第三方 MCP / plugin 的 deployment 供應鏈、模型版本回退機制。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>一般 software artifact 信任 → <a href="../supply-chain-integrity-and-artifact-trust/">7.4 supply-chain-integrity-and-artifact-trust</a></li>
<li>機器憑證 → <a href="../secrets-and-machine-credential-governance/">7.6 secrets-and-machine-credential-governance</a></li>
<li>入口治理 → <a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection</a></li>
<li>個人 dev 模型來源信任 → <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">llm/6.0 model-supply-chain-trust</a></li>
<li>部署平台 → <code>05-deployment-platform</code>、可靠性 → <code>06-reliability</code></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer、沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表的 control link 進 knowledge-card、看具體機制與 LLM 場景的差異。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <code>05-deployment-platform / 06-reliability / 08-incident-response</code>、接配置 / 驗證 / 處置交付。</li>
</ul>
<h2 id="llm-供應鏈的三條-chain">LLM 供應鏈的三條 chain</h2>
<p>LLM 服務的供應鏈跟一般 software 服務的差異在「同時管三條 chain」：</p>
<ol>
<li><strong>模型權重 chain</strong>：原始作者 → 官方 release → 量化者 → registry → production 鏡像</li>
<li><strong>推論伺服器 chain</strong>：llama.cpp / vLLM / Ollama 等 server software 的一般 software artifact chain</li>
<li><strong>第三方 plugin / MCP chain</strong>：MCP server / Continue.dev 等的程式碼供應鏈</li>
</ol>
<p>三條 chain 在 production 階段都需要 build provenance、簽署驗證、依賴隔離跟回退機制。差異主要在模型權重 chain 的特殊性：權重是大型 binary（GB 級）、難以靜態 audit、且權重本身會影響推論行為。</p>
<h2 id="分析模型">分析模型</h2>
<p>production LLM 供應鏈的分析依五個層次拆解、跟 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4</a> 的層次模型保持一致：</p>
<ol>
<li><strong>來源層</strong>：模型 build provenance 是否可回溯（哪個 base model、用哪個 dataset、由誰量化）。</li>
<li><strong>產物層</strong>：GGUF / safetensors 在傳遞過程的完整性（hash / 簽署）。</li>
<li><strong>依賴層</strong>：MCP server / inference framework / model 各自獨立信任、影響面隔離。</li>
<li><strong>節奏層</strong>：模型版本切換、回退、freeze 流程。</li>
<li><strong>收斂層</strong>：供應鏈事件能否路由到 IR 流程。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「可部署的 LLM 服務」轉成「可信的 LLM 服務」。</p>
<ol>
<li>先確認模型來源 organization、量化版本、build provenance 可關聯。</li>
<li>再確認 GGUF / safetensors 的完整性證據（hash、size、metadata）。</li>
<li>接著確認模型 + server + plugin 三條 chain 的依賴隔離。</li>
<li>最後交接到可靠性與 incident 流程、追蹤回退能力。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型來源不可追溯</td>
          <td>HF organization 不明、量化者沒公開 build script</td>
          <td>模型可信度下降、無法 audit、合規問題</td>
          <td><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">ci-pipeline</a></td>
      </tr>
      <tr>
          <td>GGUF artifact 完整性斷點</td>
          <td>缺 hash 比對、CDN 鏡像未驗證、未簽署</td>
          <td>模型權重被替換、影響推論行為</td>
          <td><a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract</a></td>
      </tr>
      <tr>
          <td>第三方 MCP / plugin 風險放大</td>
          <td>多服務共用同一 MCP server、依賴版本固定</td>
          <td>單一 MCP server 漏洞波及多 service</td>
          <td><a href="/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency-isolation</a></td>
      </tr>
      <tr>
          <td>模型版本切換節奏混亂</td>
          <td>版本切換條件不一致、回退測試缺失</td>
          <td>切換時行為差異未測、production incident</td>
          <td><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release-gate</a></td>
      </tr>
      <tr>
          <td>量化版本污染</td>
          <td>信任未知量化者、未做 behavior regression</td>
          <td>量化過程引入後門或非預期行為</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract-test</a></td>
      </tr>
      <tr>
          <td>跨 region 鏡像不一致</td>
          <td>不同 region 跑不同版本權重、cache 政策衝突</td>
          <td>一致性議題、debug 困難</td>
          <td><a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">deployment-contract</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM 供應鏈風險已進入高壓狀態。</p>
<ul>
<li>模型來源（base + dataset + 量化者）長期無法回溯時、代表 provenance 模型失效。</li>
<li>模型 artifact 在 CDN / 鏡像層沒有簽署驗證時、代表完整性邊界不足。</li>
<li>MCP server / plugin 跟 inference framework 共用單一信任域時、代表依賴隔離不足。</li>
<li>模型版本切換沒有 behavior regression test 時、代表 release 流程不收斂。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM 供應鏈相對一般 software 供應鏈有幾個特殊點：</p>
<ol>
<li><strong>權重是大型 binary、難以靜態 audit</strong>：跟 source code 不同、權重檔案無法用 grep / diff / linter 找後門；只能用 behavior testing 跟 hash 比對。</li>
<li><strong>量化過程可能改變推論行為</strong>：同一 base model 不同量化版本、回答品質有差；量化者的可信度影響整體可信度、需 case-by-case 信任。</li>
<li><strong>模型 supply chain 跟 production deployment 解耦</strong>：模型釋出方（如 Meta、Qwen 團隊）跟 production 部署方通常不同單位、責任邊界要明確。</li>
<li><strong>「license」議題</strong>：模型權重的 license（如 Llama Community License）跟一般 software license 不同、production 使用需 legal review、不只是技術議題。</li>
<li><strong>MCP server 多為 Node / Python 程式</strong>：跟一般 dependency 一樣有 supply chain 風險、但 LLM 場景下、MCP 對主機資源的副作用面比一般 dependency 大、需更嚴格的 isolation。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM 場景的供應鏈事件案例尚在累積中、本章先沿用 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4</a> 的通用案例。LLM-specific 案例累積後會補入 <code>red-team/cases/llm-supply-chain/</code>：</p>
<ul>
<li>開源組件滲透與下游衝擊：<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a>（同類威脅在 MCP server / inference framework 也適用）</li>
<li>平台級供應鏈事件：<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a>（模型釋出方平台級事件適用）</li>
</ul>
<blockquote>
<p><strong>事實查核註</strong>：LLM 供應鏈的公開事件案例累積還在早期、本章列舉的通用案例提供 mechanism 對照、不代表 LLM 場景已有等同規模的事件記錄。建議引用前以最新的 <a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP LLM Top 10</a> 跟社群 incident 報告為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<p>LLM 場景的供應鏈標準在發展中、本章沿用 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈與產物信任</a> 的標準作為 mechanism 層 anchor、補上 LLM-specific 參考：</p>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLSA</td>
          <td>v1.0 (2023)</td>
          <td>套用於 inference server + MCP build provenance</td>
      </tr>
      <tr>
          <td>Sigstore（cosign / Rekor / Fulcio）</td>
          <td>continuous</td>
          <td>模型 artifact 簽署實驗階段</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM application security 通用 reference</td>
      </tr>
      <tr>
          <td>Hugging Face Model Card spec</td>
          <td>continuous</td>
          <td>模型來源 metadata</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>交付平台與部署治理：<code>05-deployment-platform</code></li>
<li>發佈驗證與回退演練：<code>06-reliability</code></li>
<li>多租戶 LLM 推論隔離：<a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></li>
<li>偵測訊號設計：<a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></li>
<li>分級與跨部門收斂：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>可靠性 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/</guid><description>&lt;p>可靠性 Vendor 清單的核心責任是把工具名稱放回 verification loop、release gate、fault injection、SLO governance 與 evidence handoff 的判斷。每個服務頁先回答它承擔哪一種可靠性驗證責任，再討論整合成本、風險控制、artifact 與案例回寫。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">cases/&lt;/a> 是不同維度。Cases 是教學案例來源，vendors 是把驗證流程落地的工具入口。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>可靠性工具要從驗證流程進入。讀者如果要處理 release gate，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>；如果要處理 load test 與 regression，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate&lt;/a>；如果要處理 chaos，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>可靠性工具頁的教學順序是先建立 CI / release gate，再進入 load test、chaos / fault injection 與 SLO governance。這個順序對齊 checkout E5：讀者先理解變更如何被放行與停止，再比較哪些工具產生 regression evidence、experiment evidence 與 error budget evidence。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions&lt;/a>&lt;/td>
 &lt;td>CI/CD&lt;/td>
 &lt;td>workflow、environment、artifact 與 approval gate 如何支援 release evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI&lt;/a>&lt;/td>
 &lt;td>CI/CD&lt;/td>
 &lt;td>pipeline、orb、parallelism 與 context 權限如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>scenario、threshold 與 CI gate 如何支援可靠性驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>JVM simulation、injection profile 與 report 如何支援 regression gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &amp;#43; plugins">JMeter&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>GUI plan、protocol sampler 與既有測試資產如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust&lt;/a>&lt;/td>
 &lt;td>Load test&lt;/td>
 &lt;td>Python user behavior 與 distributed worker 如何支援自訂 workload&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh&lt;/a>&lt;/td>
 &lt;td>Chaos engineering&lt;/td>
 &lt;td>Kubernetes-native fault injection 與 experiment scope 如何控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos&lt;/a>&lt;/td>
 &lt;td>Chaos engineering&lt;/td>
 &lt;td>chaos workflow、hub 與 Kubernetes 實驗治理如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin&lt;/a>&lt;/td>
 &lt;td>Chaos platform&lt;/td>
 &lt;td>商業 chaos 平台、blast radius guardrail 與審計如何支援成熟團隊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy&lt;/a>&lt;/td>
 &lt;td>Fault injection&lt;/td>
 &lt;td>TCP fault、local integration test 與 dependency failure 如何模擬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9&lt;/a>&lt;/td>
 &lt;td>SLO platform&lt;/td>
 &lt;td>SLO、error budget、alerting 與 governance 如何整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/sloth/" data-link-title="Sloth" data-link-desc="OSS SLO generator for Prometheus">Sloth&lt;/a>&lt;/td>
 &lt;td>SLO generator&lt;/td>
 &lt;td>OpenSLO / Prometheus rule 生成如何降低 SLO 維護成本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「← X」代表從 X 遷入。&lt;/p></description><content:encoded><![CDATA[<p>可靠性 Vendor 清單的核心責任是把工具名稱放回 verification loop、release gate、fault injection、SLO governance 與 evidence handoff 的判斷。每個服務頁先回答它承擔哪一種可靠性驗證責任，再討論整合成本、風險控制、artifact 與案例回寫。</p>
<p>跟 <a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">cases/</a> 是不同維度。Cases 是教學案例來源，vendors 是把驗證流程落地的工具入口。</p>
<h2 id="讀法">讀法</h2>
<p>可靠性工具要從驗證流程進入。讀者如果要處理 release gate，先回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>；如果要處理 load test 與 regression，先回到 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a>；如果要處理 chaos，先回到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>可靠性工具頁的教學順序是先建立 CI / release gate，再進入 load test、chaos / fault injection 與 SLO governance。這個順序對齊 checkout E5：讀者先理解變更如何被放行與停止，再比較哪些工具產生 regression evidence、experiment evidence 與 error budget evidence。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a></td>
          <td>CI/CD</td>
          <td>workflow、environment、artifact 與 approval gate 如何支援 release evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a></td>
          <td>CI/CD</td>
          <td>pipeline、orb、parallelism 與 context 權限如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/k6/" data-link-title="k6" data-link-desc="現代 load test、JS scripting、Grafana Labs">k6</a></td>
          <td>Load test</td>
          <td>scenario、threshold 與 CI gate 如何支援可靠性驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/gatling/" data-link-title="Gatling" data-link-desc="JVM-based load test、Scala / Java / Kotlin DSL、強型別 scenario、HAR-driven recording">Gatling</a></td>
          <td>Load test</td>
          <td>JVM simulation、injection profile 與 report 如何支援 regression gate</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="老牌 load test 工具、GUI &#43; plugins">JMeter</a></td>
          <td>Load test</td>
          <td>GUI plan、protocol sampler 與既有測試資產如何治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/locust/" data-link-title="Locust" data-link-desc="Python-based load test、distributed、易擴展">Locust</a></td>
          <td>Load test</td>
          <td>Python user behavior 與 distributed worker 如何支援自訂 workload</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/chaos-mesh/" data-link-title="Chaos Mesh" data-link-desc="Kubernetes-native chaos engineering（CNCF incubating）">Chaos Mesh</a></td>
          <td>Chaos engineering</td>
          <td>Kubernetes-native fault injection 與 experiment scope 如何控制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/litmuschaos/" data-link-title="LitmusChaos" data-link-desc="Kubernetes chaos engineering 平台（CNCF graduated）">LitmusChaos</a></td>
          <td>Chaos engineering</td>
          <td>chaos workflow、hub 與 Kubernetes 實驗治理如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/gremlin/" data-link-title="Gremlin" data-link-desc="商業 chaos engineering 平台、跨平台與 GameDay">Gremlin</a></td>
          <td>Chaos platform</td>
          <td>商業 chaos 平台、blast radius guardrail 與審計如何支援成熟團隊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/toxiproxy/" data-link-title="Toxiproxy" data-link-desc="TCP-level fault injection proxy（Shopify 開源）">Toxiproxy</a></td>
          <td>Fault injection</td>
          <td>TCP fault、local integration test 與 dependency failure 如何模擬</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/nobl9/" data-link-title="Nobl9" data-link-desc="SLO platform、跨 data source、企業 SLO 治理">Nobl9</a></td>
          <td>SLO platform</td>
          <td>SLO、error budget、alerting 與 governance 如何整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/06-reliability/vendors/sloth/" data-link-title="Sloth" data-link-desc="OSS SLO generator for Prometheus">Sloth</a></td>
          <td>SLO generator</td>
          <td>OpenSLO / Prometheus rule 生成如何降低 SLO 維護成本</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「← X」代表從 X 遷入。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="github-actions/">GitHub Actions</a></td>
          <td><a href="github-actions/environment-protection-and-oidc-cloud-auth/">Environment Protection + OIDC</a></td>
          <td><a href="github-actions/migrate-from-jenkins/">← Jenkins</a></td>
      </tr>
      <tr>
          <td><a href="k6/">k6</a></td>
          <td><a href="k6/threshold-ci-gate-and-scenario-design/">Threshold CI Gate + Scenario</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="chaos-mesh/">Chaos Mesh</a></td>
          <td><a href="chaos-mesh/workflow-experiment-scope-and-steady-state-probe/">Workflow + Scope + Steady State Probe</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="sloth/">Sloth</a></td>
          <td><a href="sloth/slo-yaml-and-multi-burn-rate-alert-generation/">SLO YAML + Multi-burn-rate Alert</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>其他 T1 vendor（CircleCI / Gatling / JMeter / Locust / LitmusChaos / Gremlin / Toxiproxy / Nobl9）的 deep article 尚未開始。對應的 backlog 議題見上方「T1 服務頁大綱」段每個服務頁要回答的核心問題、跟各 vendor <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>可靠性服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 CI gate、load test、chaos、fault injection 還是 SLO governance</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>release frequency、failure mode、experiment safety、SLO maturity 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>CI 平台、09 壓測工具、chaos 平台、SLO tool 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>runner、secret、artifact、test data、blast radius、experiment approval</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>workflow run、test report、experiment result、SLO burn、gate decision</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Google SRE、Netflix chaos、release gate 與 replay 案例如何提供判準</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>可靠性工具頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工具定位</td>
          <td>它是 CI gate、load test、chaos platform、fault injection 還是 SLO governance</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷該工具能產生哪種 verification evidence 與 gate decision</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「要擋 release、驗證負載、注入失敗、追 SLO」快速定位工具類型</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>workflow、runner、secret、artifact、approval、experiment scope、SLO rule</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>CI 平台、09 壓測工具、chaos 平台、SLO 平台的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>self-hosted runner、blast radius guardrail、error budget policy、audit</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>flaky job、missing artifact、unsafe experiment、false SLO alert、runner bottleneck</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>容量模型回 09、觀測資料回 04、事故協作回 08、部署控制回 05</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整 pipeline cookbook、每個 test framework、所有 chaos experiment 範本</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 06 cases、6.8 release gate、6.20 experiment safety boundary</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>本模組 12 個 vendor 跨 4 個 sub-category（CI/CD / load test / chaos / SLO）、不是同類選一。對照表用「橫向 reliability gate 議題」標明每個議題在哪個 sub-category 落地。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>GH Actions</th>
          <th>CircleCI</th>
          <th>k6</th>
          <th>Gatling</th>
          <th>JMeter</th>
          <th>Locust</th>
          <th>Chaos Mesh</th>
          <th>Litmus</th>
          <th>Gremlin</th>
          <th>Toxiproxy</th>
          <th>Nobl9</th>
          <th>Sloth</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主責任</td>
          <td>CI gate</td>
          <td>CI gate</td>
          <td>Load test</td>
          <td>Load test</td>
          <td>Load test</td>
          <td>Load test</td>
          <td>K8s chaos</td>
          <td>K8s chaos</td>
          <td>跨平台 chaos</td>
          <td>TCP fault</td>
          <td>SLO governance</td>
          <td>SLO generator</td>
      </tr>
      <tr>
          <td>整合 CI gate</td>
          <td>原生</td>
          <td>原生</td>
          <td>threshold</td>
          <td>assertion</td>
          <td>non-GUI mode</td>
          <td>headless</td>
          <td>workflow</td>
          <td>workflow</td>
          <td>scenario</td>
          <td>client SDK</td>
          <td>error budget</td>
          <td>rule gen</td>
      </tr>
      <tr>
          <td>配置模式</td>
          <td>YAML</td>
          <td>YAML</td>
          <td>JS</td>
          <td>Scala / Java</td>
          <td>XML GUI</td>
          <td>Python</td>
          <td>CRD</td>
          <td>CRD</td>
          <td>UI / API</td>
          <td>API</td>
          <td>YAML / UI</td>
          <td>YAML</td>
      </tr>
      <tr>
          <td>環境支援</td>
          <td>GitHub-hosted</td>
          <td>cross-VCS</td>
          <td>OSS / Cloud</td>
          <td>OSS / Enterprise</td>
          <td>OSS</td>
          <td>OSS</td>
          <td>K8s only</td>
          <td>K8s only</td>
          <td>跨平台</td>
          <td>TCP layer</td>
          <td>multi-source</td>
          <td>Prometheus</td>
      </tr>
      <tr>
          <td>進階產出</td>
          <td>matrix / OIDC</td>
          <td>parallelism</td>
          <td>extension</td>
          <td>feeder</td>
          <td>plugins</td>
          <td>distributed</td>
          <td>scope control</td>
          <td>ChaosHub</td>
          <td>GameDay</td>
          <td>toxic types</td>
          <td>composite SLO</td>
          <td>multi-burn</td>
      </tr>
      <tr>
          <td>商業 / 開源</td>
          <td>商業 + SaaS</td>
          <td>商業 + SaaS</td>
          <td>OSS + Cloud</td>
          <td>OSS + Enterprise</td>
          <td>OSS</td>
          <td>OSS</td>
          <td>OSS</td>
          <td>OSS + 商業</td>
          <td>商業 SaaS</td>
          <td>OSS</td>
          <td>商業 SaaS</td>
          <td>OSS</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>待補</td>
          <td>Netflix/Google</td>
          <td>待補</td>
          <td>待補</td>
          <td>Shopify</td>
          <td>Google SRE</td>
          <td>待補</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、看相同 sub-category 對手如何處理同一議題</li>
<li>讀者組 reliability stack：CI gate + load test + chaos + SLO 各選 1</li>
<li>評估 OSS vs 商業 trade-off</li>
</ul>
<p>下面 4 段把對照表的 sub-category 展開、不是每行都展開。</p>
<h3 id="ci-gategithub-actions--circleci">CI gate（GitHub Actions / CircleCI）</h3>
<p>CI gate 是 release 前最後一道驗證、決定哪些工件可發。<strong>GitHub Actions</strong> 跟 GitHub 深度整合（PR check / environment protection / OIDC cloud auth）、marketplace action 生態最廣；<strong>CircleCI</strong> 強進階 cache + parallelism + macOS / GPU resource class、cross-VCS（GitHub / Bitbucket / GitLab）。</p>
<p>選型判讀：GitHub-hosted + 普通用 → GitHub Actions；極致 build speed / macOS / 跨 VCS → CircleCI；複雜 DAG → Tekton / Argo。</p>
<h3 id="load-testk6--gatling--jmeter--locust">Load test（k6 / Gatling / JMeter / Locust）</h3>
<p>Load test 提供 performance regression evidence。差異主要在語言生態：<strong>k6</strong> JS / CLI-first / Grafana 生態；<strong>Gatling</strong> Scala / Java / 強型別 / 複雜 scenario；<strong>JMeter</strong> GUI / 老牌 / 多 protocol；<strong>Locust</strong> Python / 自訂邏輯極彈性。</p>
<p>選型判讀：CI-first JS → k6；JVM 生態 → Gatling；既有 .jmx 資產 → JMeter；Python 團隊 / 複雜邏輯 → Locust。詳見 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9 performance capacity 模組</a> 的 capacity planning 角度。</p>
<h3 id="chaos-engineeringchaos-mesh--litmuschaos--gremlin--toxiproxy">Chaos engineering（Chaos Mesh / LitmusChaos / Gremlin / Toxiproxy）</h3>
<p>Chaos 工具按 scope 跟運維模式分四類：<strong>Chaos Mesh</strong> K8s-native CRD-driven 多 fault types；<strong>LitmusChaos</strong> K8s + ChaosHub experiment 庫；<strong>Gremlin</strong> 商業 SaaS / 跨平台 / GameDay；<strong>Toxiproxy</strong> TCP-level / integration test 用。</p>
<p>選型判讀：K8s production + OSS → Chaos Mesh / Litmus；跨平台 + 商業 → Gremlin；CI integration test → Toxiproxy。對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a> 的 blast radius 設計。</p>
<h3 id="slo-governancenobl9--sloth">SLO governance（Nobl9 / Sloth）</h3>
<p>SLO 工具按 source 跟運維模式分兩類：<strong>Nobl9</strong> 商業 SaaS / multi-source / OpenSLO 主導 / 企業 governance；<strong>Sloth</strong> OSS / Prometheus-only / 產生 Prometheus rules。</p>
<p>選型判讀：multi-source / SaaS / governance → Nobl9；Prometheus-only / OSS → Sloth / Pyrra；vendor 內建夠 → Datadog SLO / Grafana SLO / Honeycomb SLO。對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a>。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>R1</td>
          <td>GitHub Actions / CircleCI</td>
          <td>建立 CI gate、artifact 與 approval baseline</td>
      </tr>
      <tr>
          <td>R2</td>
          <td>k6 / Gatling / JMeter / Locust</td>
          <td>建立 release gate 視角的 load test 與 regression evidence</td>
      </tr>
      <tr>
          <td>R3</td>
          <td>Chaos Mesh / LitmusChaos / Gremlin / Toxiproxy</td>
          <td>建立 fault injection 與 experiment safety 對照</td>
      </tr>
      <tr>
          <td>R4</td>
          <td>Nobl9 / Sloth</td>
          <td>建立 SLO governance、error budget 與 rule generation 判準</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI/CD</td>
          <td>GitLab CI、Jenkins、Buildkite、Tekton、Harness、Azure Pipelines</td>
          <td>self-hosted runner、enterprise workflow、pipeline governance</td>
      </tr>
      <tr>
          <td>Load / browser gate</td>
          <td>Artillery、Grafana k6 Cloud、BlazeMeter、Playwright、Cypress</td>
          <td>managed runner、browser flow、release gate、cost</td>
      </tr>
      <tr>
          <td>Chaos / fault</td>
          <td>AWS Fault Injection Service、Azure Chaos Studio、Pumba</td>
          <td>cloud-native fault、container fault、blast radius</td>
      </tr>
      <tr>
          <td>SLO</td>
          <td>Pyrra、OpenSLO、Keptn</td>
          <td>Prometheus-native SLO、portable SLO spec、quality gate</td>
      </tr>
      <tr>
          <td>Policy / audit</td>
          <td>Steampipe、Conftest</td>
          <td>compliance query、control evidence、change review</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 CI gate、performance gate、chaos gate、SLO gate 與 policy gate。CI 工具負責 release artifact 與 approval；load / browser 工具負責 regression evidence；chaos 工具負責 failure mode evidence；SLO 工具負責 error budget governance；policy 工具負責控制證據。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a></li>
<li>上游：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a></li>
<li>服務路徑：<a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a></li>
<li>平行：<a href="/blog/backend/09-performance-capacity/vendors/" data-link-title="效能與容量工具清單" data-link-desc="整理效能工程、容量規劃、壓測、production replay 與 profiling 工具的服務責任與選型路由">09 效能與容量工具清單</a></li>
</ul>
]]></content:encoded></item><item><title>事故處理 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/</guid><description>&lt;p>事故處理 Vendor 清單的核心責任是把工具名稱放回 alert routing、incident command、stakeholder communication、status page、postmortem 與 learning loop 的判斷。每個服務頁先回答它承擔事故流程的哪一段，再討論輪值成本、協作模型、稽核證據與案例回寫。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/&lt;/a> 是不同維度。Cases 是公開事故案例來源，vendors 是把事故流程落地的工具入口。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>事故工具要從協作節點進入。讀者如果要處理告警與輪值，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness&lt;/a>；如果要處理決策紀錄，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>；如果要處理復盤與回寫，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>事故工具頁的教學順序是先建立 paging，再進入 incident command、status page 與 learning loop。這個順序對齊 checkout E4 與 E6：讀者先理解告警如何找到 owner，再比較事故指揮、對外更新、復盤學習與 action item 如何回寫到 release gate、資安控制與服務路徑。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty&lt;/a>&lt;/td>
 &lt;td>On-call platform&lt;/td>
 &lt;td>escalation、service ownership、runbook 與 incident object 如何支援輪值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie&lt;/a>&lt;/td>
 &lt;td>On-call platform&lt;/td>
 &lt;td>Atlassian workflow、routing rule 與 team schedule 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &amp;#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall&lt;/a>&lt;/td>
 &lt;td>OSS / Grafana on-call&lt;/td>
 &lt;td>alert grouping、Grafana integration 與自管成本如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io&lt;/a>&lt;/td>
 &lt;td>IR platform&lt;/td>
 &lt;td>Slack-native command、timeline、action 與 post-incident workflow 如何支援協作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &amp;#43; retrospective 平台、Slack / Teams 整合、service catalog &amp;#43; runbook automation 為核心">FireHydrant&lt;/a>&lt;/td>
 &lt;td>IR platform&lt;/td>
 &lt;td>service catalog、runbook、retrospective 與 automation 如何整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &amp;#43; AI investigation、Slack-native &amp;#43; 200&amp;#43; integration">Rootly&lt;/a>&lt;/td>
 &lt;td>IR automation&lt;/td>
 &lt;td>Slack workflow、status update、task automation 與 Jira / Linear handoff 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &amp;#43; Atlassian 生態整合、subscriber notification &amp;#43; component dependency 是核心責任">Atlassian Statuspage&lt;/a>&lt;/td>
 &lt;td>Status page&lt;/td>
 &lt;td>component、subscriber、incident update 與 stakeholder communication 如何管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus&lt;/a>&lt;/td>
 &lt;td>Status page&lt;/td>
 &lt;td>輕量 status page、custom domain 與低操作成本如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli&lt;/a>&lt;/td>
 &lt;td>Learning platform&lt;/td>
 &lt;td>postmortem、interview、timeline 與 learning review 如何支援組織學習&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。&lt;/p></description><content:encoded><![CDATA[<p>事故處理 Vendor 清單的核心責任是把工具名稱放回 alert routing、incident command、stakeholder communication、status page、postmortem 與 learning loop 的判斷。每個服務頁先回答它承擔事故流程的哪一段，再討論輪值成本、協作模型、稽核證據與案例回寫。</p>
<p>跟 <a href="/blog/backend/08-incident-response/cases/" data-link-title="事故處理服務案例庫" data-link-desc="按服務組織的公開事故案例庫，累積架構脈絡與 longitudinal pattern">cases/</a> 是不同維度。Cases 是公開事故案例來源，vendors 是把事故流程落地的工具入口。</p>
<h2 id="讀法">讀法</h2>
<p>事故工具要從協作節點進入。讀者如果要處理告警與輪值，先回到 <a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a>；如果要處理決策紀錄，先回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>；如果要處理復盤與回寫，先回到 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>事故工具頁的教學順序是先建立 paging，再進入 incident command、status page 與 learning loop。這個順序對齊 checkout E4 與 E6：讀者先理解告警如何找到 owner，再比較事故指揮、對外更新、復盤學習與 action item 如何回寫到 release gate、資安控制與服務路徑。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a></td>
          <td>On-call platform</td>
          <td>escalation、service ownership、runbook 與 incident object 如何支援輪值</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a></td>
          <td>On-call platform</td>
          <td>Atlassian workflow、routing rule 與 team schedule 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a></td>
          <td>OSS / Grafana on-call</td>
          <td>alert grouping、Grafana integration 與自管成本如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></td>
          <td>IR platform</td>
          <td>Slack-native command、timeline、action 與 post-incident workflow 如何支援協作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/firehydrant/" data-link-title="FireHydrant" data-link-desc="IR &#43; retrospective 平台、Slack / Teams 整合、service catalog &#43; runbook automation 為核心">FireHydrant</a></td>
          <td>IR platform</td>
          <td>service catalog、runbook、retrospective 與 automation 如何整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/rootly/" data-link-title="Rootly" data-link-desc="IR 自動化平台、no-code workflow &#43; AI investigation、Slack-native &#43; 200&#43; integration">Rootly</a></td>
          <td>IR automation</td>
          <td>Slack workflow、status update、task automation 與 Jira / Linear handoff 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a></td>
          <td>Status page</td>
          <td>component、subscriber、incident update 與 stakeholder communication 如何管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a></td>
          <td>Status page</td>
          <td>輕量 status page、custom domain 與低操作成本如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/08-incident-response/vendors/jeli/" data-link-title="Jeli" data-link-desc="Post-incident learning 平台、2023 被 PagerDuty 收購、強調 interview-driven narrative 而非 timeline-only retro">Jeli</a></td>
          <td>Learning platform</td>
          <td>postmortem、interview、timeline 與 learning review 如何支援組織學習</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="pagerduty/">PagerDuty</a></td>
          <td>—</td>
          <td><a href="pagerduty/migrate-to-incident-io/">→ incident.io (Type E)</a></td>
      </tr>
      <tr>
          <td><a href="opsgenie/">Opsgenie</a></td>
          <td>—</td>
          <td><a href="opsgenie/migrate-from-pagerduty/">← PagerDuty (Type A)</a></td>
      </tr>
      <tr>
          <td><a href="atlassian-statuspage/">Atlassian Statuspage</a></td>
          <td>—</td>
          <td><a href="atlassian-statuspage/migrate-to-instatus/">→ Instatus (Type B)</a></td>
      </tr>
  </tbody>
</table>
<p>其他 T1 vendor（Grafana OnCall / incident.io / FireHydrant / Rootly / Instatus / Jeli）尚未開始。對應的 backlog 議題見上方「T1 服務頁大綱」段每個服務頁要回答的核心問題、跟各 vendor <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>事故處理服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 on-call、IR coordination、status communication、postmortem 還是 learning loop</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>alert volume、team count、customer communication、compliance、learning maturity 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>on-call SaaS、Slack workflow、自建流程、status page、learning platform 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>rota hygiene、service catalog、integration、timeline quality、stakeholder update</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>alert route、ack time、incident timeline、decision log、status update、action item</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>AWS、Cloudflare、GitHub、Atlassian 等事故案例如何提供流程判準</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>事故處理工具頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工具定位</td>
          <td>它是 on-call、IR coordination、status communication、postmortem 還是 learning platform</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷該工具改善哪個事故協作節點與哪種 evidence handoff</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「告警找人、事故指揮、對外更新、復盤學習」快速定位工具類型</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>service catalog、rota、escalation、timeline、status update、action item</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>On-call SaaS、Slack-native IR、自建流程、status page、learning platform 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>multi-team escalation、compliance report、customer communication、learning review</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>alert storm、missed ack、unclear commander、stale status page、action item drift</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>信號品質回 04、release gate 回 06、平台回退回 05、資安事件回 07</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>完整組織設計、HR 輪值政策、法律公告模板、每個聊天平台 automation</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 08 cases、8.19 decision log、8.22 evidence write-back</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>本模組 9 個 vendor 跨 4 個 sub-category（on-call paging / IR coordination / status page / learning）、覆蓋 incident 全流程。對照表用「橫向 incident 流程節點」標明每個議題在哪個 sub-category 落地。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>PagerDuty</th>
          <th>Opsgenie</th>
          <th>Grafana OnCall</th>
          <th>incident.io</th>
          <th>FireHydrant</th>
          <th>Rootly</th>
          <th>Statuspage</th>
          <th>Instatus</th>
          <th>Jeli</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主責任</td>
          <td>On-call SaaS</td>
          <td>Atlassian on-call</td>
          <td>OSS on-call</td>
          <td>IR coordination</td>
          <td>IR coordination</td>
          <td>IR coordination</td>
          <td>Status page</td>
          <td>Status page</td>
          <td>Learning / postmortem</td>
      </tr>
      <tr>
          <td>Paging</td>
          <td>核心</td>
          <td>核心</td>
          <td>核心</td>
          <td>後加</td>
          <td>後加</td>
          <td>後加</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>IR coordination</td>
          <td>Response Play</td>
          <td>中等</td>
          <td>弱</td>
          <td>核心 (Slack)</td>
          <td>核心 (Teams)</td>
          <td>核心 (no-code)</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>Status page</td>
          <td>整合外部</td>
          <td>整合 Statuspage</td>
          <td>整合外部</td>
          <td>整合外部</td>
          <td>內建</td>
          <td>整合外部</td>
          <td>核心</td>
          <td>核心</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>Retrospective</td>
          <td>Jeli (整合)</td>
          <td>Confluence</td>
          <td>弱</td>
          <td>template</td>
          <td>facilitator</td>
          <td>AI</td>
          <td>N/A</td>
          <td>N/A</td>
          <td>核心 (narrative)</td>
      </tr>
      <tr>
          <td>配置模式</td>
          <td>UI + Terraform</td>
          <td>UI</td>
          <td>UI / Helm</td>
          <td>Slack + UI</td>
          <td>Slack/Teams + UI</td>
          <td>No-code UI</td>
          <td>UI + API</td>
          <td>UI + API</td>
          <td>UI</td>
      </tr>
      <tr>
          <td>整合 IR 工具</td>
          <td>支援</td>
          <td>支援</td>
          <td>中等</td>
          <td>支援</td>
          <td>支援</td>
          <td>200+ 整合</td>
          <td>IR push</td>
          <td>IR push</td>
          <td>PagerDuty 整合</td>
      </tr>
      <tr>
          <td>商業 / 開源</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>OSS / Cloud</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>商業 SaaS</td>
          <td>商業（PD 旗下）</td>
      </tr>
      <tr>
          <td>平台支援</td>
          <td>iOS / Android / Web</td>
          <td>iOS / Android / Web</td>
          <td>Web</td>
          <td>Slack first</td>
          <td>Slack + Teams</td>
          <td>Slack + Teams</td>
          <td>Web</td>
          <td>Web</td>
          <td>Web</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、看相同 sub-category 對手如何處理同議題</li>
<li>讀者組 IR stack：paging + IR coordination + status page + learning 各選 1</li>
<li>評估 best-of-breed vs all-in-one 取捨</li>
</ul>
<p>下面 4 段把對照表的 sub-category 展開。</p>
<h3 id="pagingpagerduty--opsgenie--grafana-oncall">Paging（PagerDuty / Opsgenie / Grafana OnCall）</h3>
<p>Paging 是 alert 找對人的入口。<strong>PagerDuty</strong> 業界標準、完整 IR 平台演化、Jeli 收購補 learning；<strong>Opsgenie</strong> Atlassian 生態最強、跟 JSM / Statuspage / Confluence 一站式；<strong>Grafana OnCall</strong> OSS / 預算敏感替代、跟 Grafana 觀測生態整合。</p>
<p>選型判讀：成熟 + 跨生態 → PagerDuty；Atlassian 用戶 → Opsgenie；OSS / Grafana 用戶 → Grafana OnCall。</p>
<h3 id="ir-coordinationincidentio--firehydrant--rootly">IR coordination（incident.io / FireHydrant / Rootly）</h3>
<p>IR coordination 是事故當下的協作平台、把 incident lifecycle 自動化。<strong>incident.io</strong> Slack-first、UX 最簡潔；<strong>FireHydrant</strong> 雙平台（Slack + Teams）、內建 status page + retrospective facilitator；<strong>Rootly</strong> no-code workflow + AI 輔助、200+ integration。</p>
<p>選型判讀：Slack-only + 簡潔 → incident.io；Microsoft Teams + 完整 retro → FireHydrant；no-code 客製 + AI → Rootly。三者都有 paging 模組、可不另外用 PagerDuty。</p>
<h3 id="status-pageatlassian-statuspage--instatus">Status page（Atlassian Statuspage / Instatus）</h3>
<p>Status page 是對外溝通入口、是法律 / SLA / 客戶信任的 evidence。<strong>Statuspage</strong> 事實標準、enterprise SLA、跟 Opsgenie / PagerDuty / IR 平台廣泛整合；<strong>Instatus</strong> 輕量 / 價格親民 / 現代 UI / startup 友善。</p>
<p>選型判讀：enterprise / 既有 Atlassian 投資 → Statuspage；budget / startup → Instatus；OSS 自管 → Cachet（不在本表）；IR 平台內建夠 → FireHydrant 內建 status page。</p>
<h3 id="learningjeli">Learning（Jeli）</h3>
<p>Learning 是事故後的組織學習、不是 retro template、是 longitudinal pattern analysis。<strong>Jeli</strong>（2023 PagerDuty 收購）narrative-based investigation + cross-incident pattern detection、源自 Honeycomb Production Excellence 文化。Jeli 跟 IR 平台的 retrospective 模組 complement、不取代 — IR retro 是單事故、Jeli 是跨事故學習。</p>
<p>選型判讀：深度 learning + multi-incident pattern → Jeli（PagerDuty 用戶）；單事故 retro template → IR 平台內建即可；組織學習 / 文化變革 → Jeli + 對應流程。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>I1</td>
          <td>PagerDuty / Opsgenie / Grafana OnCall</td>
          <td>建立 alert routing、escalation 與輪值 baseline</td>
      </tr>
      <tr>
          <td>I2</td>
          <td>incident.io / FireHydrant / Rootly</td>
          <td>建立 incident command、timeline 與 automation 對照</td>
      </tr>
      <tr>
          <td>I3</td>
          <td>Atlassian Statuspage / Instatus</td>
          <td>建立外部溝通、component status 與 stakeholder update 判準</td>
      </tr>
      <tr>
          <td>I4</td>
          <td>Jeli / Blameless / 自建流程</td>
          <td>建立 postmortem、learning review 與 action tracking 對照</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>On-call</td>
          <td>Squadcast、xMatters、Splunk On-Call、Better Stack</td>
          <td>escalation policy、enterprise workflow、handoff</td>
      </tr>
      <tr>
          <td>ITSM / service desk</td>
          <td>ServiceNow、Jira Service Management</td>
          <td>ticket lifecycle、change / incident linkage、enterprise workflow</td>
      </tr>
      <tr>
          <td>Status page</td>
          <td>status.io、Cachet、Better Stack Status</td>
          <td>hosted vs self-hosted、subscriber communication</td>
      </tr>
      <tr>
          <td>Learning</td>
          <td>Blameless、Howie</td>
          <td>postmortem workflow、learning capture、action follow-up</td>
      </tr>
      <tr>
          <td>Collaboration</td>
          <td>Slack workflow、Microsoft Teams workflow、GitHub Issues</td>
          <td>低成本流程、缺口、handoff evidence</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 paging、incident command、ITSM、status communication 與 learning。PagerDuty / Opsgenie / Grafana OnCall 解 paging；incident.io / FireHydrant / Rootly 解 command workflow；ServiceNow / Jira Service Management 解 enterprise ticket lifecycle；Statuspage / Instatus / Cachet 解對外溝通；Jeli / Blameless 解 learning loop。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/08-incident-response/drills-and-oncall-readiness/" data-link-title="8.6 演練與值班能力建設" data-link-desc="用演練與值班訓練提升事故反應品質">Drills and On-call Readiness</a></li>
<li>上游：<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>上游：<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a></li>
<li>服務路徑：<a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back 實作示範</a></li>
</ul>
]]></content:encoded></item><item><title>LLM 多租戶推論隔離</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/</guid><description>&lt;p>本章的責任是把 LLM 推論服務的多租戶隔離問題拆成可操作的判讀節點。LLM 服務的隔離議題在一般 multi-tenant 隔離（compute / network / data、見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary&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>（特別是 &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> 重用）、prompt log、model artifact 訪問權三個 LLM-specific 層、本章聚焦這些差異。一般 multi-tenant 隔離原則沿用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈&lt;/a>。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 推論的多租戶 isolation 特殊性。team / 個人 dev 場景的「多人共用本地 server」見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">llm/6.5 跨進 production 的 routing 中樞&lt;/a>；通用 IAM / 服務間信任邊界見 7.2。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：KV cache 跨租戶洩漏、prompt log 隔離、模型 artifact 訪問權、batch 推論的順序敏感性、tenant-scoped rate limit、共用 GPU 上的記憶體殘留。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>通用 IAM / 服務間信任 → &lt;a href="../identity-access-boundary/">7.2 identity-access-boundary&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.7 workload-identity-and-federated-trust&lt;/a>&lt;/li>
&lt;li>log / PII 治理 → &lt;a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance&lt;/a>&lt;/li>
&lt;li>model artifact 供應鏈 → &lt;a href="../llm-deployment-supply-chain/">llm-deployment-supply-chain&lt;/a>&lt;/li>
&lt;li>入口治理 → &lt;a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表 → knowledge-card → 看具體機制。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：交接路由 → &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-多租戶隔離的三個-llm-specific-層">LLM 多租戶隔離的三個 LLM-specific 層&lt;/h2>
&lt;p>跟一般 service 的多租戶隔離（compute / network / data）相比、LLM 推論服務多了三個層次：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>KV cache 層&lt;/strong>：&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> 是推論時的 attention 暫存、跨 request 可能重用（prefix cache、shared prefix optimization）；跨租戶共用 cache 是直接的資料洩漏面。&lt;/li>
&lt;li>&lt;strong>prompt log 層&lt;/strong>：production LLM 服務通常會 log prompt + response 用於 debug / billing / abuse detection；log 的隔離與保留期限直接影響跨租戶洩漏風險。&lt;/li>
&lt;li>&lt;strong>model artifact 訪問權&lt;/strong>：production 可能部署多個 fine-tuned 模型（如 customer-specific 模型）、模型本身是 sensitive artifact、訪問權要對齊 IAM。&lt;/li>
&lt;/ol>
&lt;h2 id="分析模型">分析模型&lt;/h2>
&lt;p>production LLM 推論的多租戶隔離依四個層次分析：&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 推論服務的多租戶隔離問題拆成可操作的判讀節點。LLM 服務的隔離議題在一般 multi-tenant 隔離（compute / network / data、見 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary</a>）之上、多了 <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> 重用）、prompt log、model artifact 訪問權三個 LLM-specific 層、本章聚焦這些差異。一般 multi-tenant 隔離原則沿用 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界</a> 跟 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈</a>。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 推論的多租戶 isolation 特殊性。team / 個人 dev 場景的「多人共用本地 server」見 <a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">llm/6.5 跨進 production 的 routing 中樞</a>；通用 IAM / 服務間信任邊界見 7.2。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：KV cache 跨租戶洩漏、prompt log 隔離、模型 artifact 訪問權、batch 推論的順序敏感性、tenant-scoped rate limit、共用 GPU 上的記憶體殘留。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>通用 IAM / 服務間信任 → <a href="../identity-access-boundary/">7.2 identity-access-boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a> → <a href="../workload-identity-and-federated-trust/">7.7 workload-identity-and-federated-trust</a></li>
<li>log / PII 治理 → <a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance</a></li>
<li>model artifact 供應鏈 → <a href="../llm-deployment-supply-chain/">llm-deployment-supply-chain</a></li>
<li>入口治理 → <a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection</a></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<ul>
<li><strong>Mechanism</strong>：問題節點表 → knowledge-card → 看具體機制。</li>
<li><strong>Delivery</strong>：交接路由 → <code>05-deployment-platform / 06-reliability / 08-incident-response</code>。</li>
</ul>
<h2 id="llm-多租戶隔離的三個-llm-specific-層">LLM 多租戶隔離的三個 LLM-specific 層</h2>
<p>跟一般 service 的多租戶隔離（compute / network / data）相比、LLM 推論服務多了三個層次：</p>
<ol>
<li><strong>KV cache 層</strong>：<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 是推論時的 attention 暫存、跨 request 可能重用（prefix cache、shared prefix optimization）；跨租戶共用 cache 是直接的資料洩漏面。</li>
<li><strong>prompt log 層</strong>：production LLM 服務通常會 log prompt + response 用於 debug / billing / abuse detection；log 的隔離與保留期限直接影響跨租戶洩漏風險。</li>
<li><strong>model artifact 訪問權</strong>：production 可能部署多個 fine-tuned 模型（如 customer-specific 模型）、模型本身是 sensitive artifact、訪問權要對齊 IAM。</li>
</ol>
<h2 id="分析模型">分析模型</h2>
<p>production LLM 推論的多租戶隔離依四個層次分析：</p>
<ol>
<li><strong>memory 層</strong>：GPU VRAM、CPU RAM 中的 KV cache 跟模型權重、跨 request / 跨租戶的殘留與共享邊界。</li>
<li><strong>storage 層</strong>：模型 artifact、prompt log、context cache 在儲存層的隔離。</li>
<li><strong>identity 層</strong>：tenant identity 怎麼帶到 inference call、rate limit / quota 怎麼按租戶分。</li>
<li><strong>observability 層</strong>：metric / log / trace 中的 tenant tag、跨租戶分析的允許範圍。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「能服務多個租戶的 LLM 服務」轉成「租戶間資料不互相洩漏的 LLM 服務」。</p>
<ol>
<li>先確認 tenant identity 從 API gateway 到 inference call 的傳遞路徑。</li>
<li>再確認 KV cache、prompt log、model artifact 各自的隔離邊界。</li>
<li>接著確認 GPU 記憶體中的跨 request 殘留是否清理。</li>
<li>最後交接到偵測流程、確認跨租戶異常能被識別。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>KV cache 跨租戶共享</td>
          <td>shared prefix optimization 沒按 tenant key 分桶</td>
          <td>租戶 A 的 prompt prefix 被租戶 B 看見</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>prompt log 沒分租戶</td>
          <td>集中 log、查詢時 tenant filter 缺失</td>
          <td>abuse detection 跨租戶看 prompt 內容、隱私違規</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a></td>
      </tr>
      <tr>
          <td>共用 GPU 上的記憶體殘留</td>
          <td>推論完未清 VRAM、下一個 request 可能 dump 到前一個內容</td>
          <td>同 GPU 上的不同 tenant 之間殘留洩漏</td>
          <td><a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret-management</a></td>
      </tr>
      <tr>
          <td>tenant-scoped rate limit 失效</td>
          <td>同一 API key 限流、租戶被互相 DoS</td>
          <td>大租戶吃光 quota、其他租戶無法用</td>
          <td><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate-limit</a></td>
      </tr>
      <tr>
          <td>model artifact 訪問權混亂</td>
          <td>fine-tuned 模型路徑可被其他 tenant 載入</td>
          <td>客戶模型被其他客戶使用、模型權重洩漏</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">identity-access-boundary</a></td>
      </tr>
      <tr>
          <td>batch 推論的 cross-tenant 順序敏感</td>
          <td>dynamic batching 把不同 tenant 的 request 合批</td>
          <td>一個 tenant 的 OOM / 長 prompt 影響其他 tenant 的 latency</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM 多租戶 isolation 已進入高壓狀態。</p>
<ul>
<li>KV cache 共用範圍跨越 tenant 邊界時、代表記憶體層 isolation 失效。</li>
<li>prompt log 沒帶 tenant tag、或 tag 後仍可跨 tenant 查時、代表 log 層 isolation 不足。</li>
<li>模型 artifact 訪問權跟 IAM 解耦時、代表 identity 層 isolation 不足。</li>
<li>推論 batch 對 tenant boundary 不敏感時、代表 batch 層的 noisy-neighbor 風險上升。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM 多租戶 isolation 相對一般 multi-tenant 服務的特殊性：</p>
<ol>
<li><strong>KV cache 是有用但敏感的優化</strong>：shared prefix cache（如多 tenant 用同一 system prompt）能省大量 prefill 算力、但跨 tenant 共用就是洩漏。判讀：可以 share 同 tenant 內的 prefix、不能 share 跨 tenant。</li>
<li><strong>prompt log 含豐富使用者意圖</strong>：相比一般 API log 主要記 endpoint / status code、LLM prompt log 記的是「使用者實際在問什麼」、隱私敏感度高得多。</li>
<li><strong>GPU 是稀缺資源、共用比 CPU 多</strong>：production LLM 服務常多 tenant 共用同卡、isolation 比一般 multi-tenant 服務（每 tenant 跑獨立 pod）更難做、需要更細的 batch 跟 memory 管理。</li>
<li><strong>fine-tuned 模型本身是 customer asset</strong>：模型訓練成本高、權重是客戶 IP、訪問權混亂直接是 IP 外洩。</li>
<li><strong>「LLM 記住 cross-tenant 資訊」的疑慮</strong>：使用者常擔心 LLM 把 A tenant 的 prompt「記住」洩漏給 B tenant；對 inference-only 服務（無 fine-tune）這不發生（模型權重 immutable）、有 fine-tune 時要看 training data 隔離。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM 多租戶 isolation 的公開案例累積中、本章先沿用通用 multi-tenant 案例：</p>
<ul>
<li>一般 multi-tenant 隔離案例見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界</a>。</li>
<li>LLM-specific 案例累積後會補入 <code>red-team/cases/llm-multi-tenant/</code>。</li>
</ul>
<blockquote>
<p><strong>事實查核註</strong>：LLM 多租戶 isolation 的公開事件案例還在早期、社群上有些「LLM A 的 system prompt 被 B 看到」等報告、多數屬 prompt injection 範疇而非 cache 洩漏。建議引用前以最新的 OWASP LLM Top 10 跟具體 vendor 的 incident 公告為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NIST SP 800-207（Zero Trust Architecture）</td>
          <td>2020</td>
          <td>tenant boundary 零信任模型 reference</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM application security 通用 reference</td>
      </tr>
      <tr>
          <td>CSA Cloud Controls Matrix</td>
          <td>v4 (2021)</td>
          <td>multi-tenant cloud 控制 reference</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身份授權邊界：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity-access-boundary</a></li>
<li>log 治理：<a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></li>
<li>agent prompt injection 後果：<a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">llm-prompt-injection-in-agent</a></li>
<li>部署平台：<code>05-deployment-platform</code></li>
<li>可靠性：<code>06-reliability</code></li>
</ul>
]]></content:encoded></item><item><title>LLM Agent Prompt Injection 後果治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/</guid><description>&lt;p>本章的責任是把 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-injection/" data-link-title="Prompt Injection" data-link-desc="把惡意指令藏進 LLM 會讀到的內容、誘導 LLM 跑出非開發者預期行為的攻擊類別、OWASP LLM01 列入頭號威脅">prompt injection&lt;/a> 在 production agent 場景下能造成的具體後果、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.10 事件案例到控制工作流&lt;/a> 的 incident 流程接起來。核心概念見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/agent-loop/" data-link-title="Agent Loop" data-link-desc="LLM agent 自我循環的工作流：LLM 規劃下一步、執行 tool、看結果、再規劃下一步、直到任務完成或停止條件觸發">agent loop&lt;/a> 卡；影響範圍評估見 backend &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius&lt;/a> 卡。個人 dev IDE 場景的 prompt injection 入口判讀見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">llm/6.3 IDE 場景的 prompt injection&lt;/a>；本章聚焦 production agent 場景下、injection 觸發 tool / API call 後造成的服務級後果。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production agent 場景下 prompt injection 的後果治理：tool spec 設計約束、agent loop 限制、review checkpoint、可逆性保證。注入發生機制（IDE 場景、codebase / 依賴 / Web）已在 llm/6.3 涵蓋、本章不重複。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：production agent 場景下 prompt injection 觸發 tool 副作用、跨服務 lateral movement、惡意 API call、誤觸發 production 操作、agent loop 中的 injection 累積。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>個人 dev IDE prompt injection 入口 → &lt;a href="https://tarrragon.github.io/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">llm/6.3 prompt-injection-in-ide&lt;/a>&lt;/li>
&lt;li>一般 incident workflow → &lt;a href="../incident-case-to-control-workflow/">7.10 incident-case-to-control-workflow&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../llm-as-service-detection-coverage/">llm-as-service-detection-coverage&lt;/a>&lt;/li>
&lt;li>身份授權邊界 → &lt;a href="../identity-access-boundary/">7.2 identity-access-boundary&lt;/a>&lt;/li>
&lt;li>tool use 個人 dev 場景 → &lt;a href="https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">llm/6.2 tool-use-permission-model&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表 → knowledge-card / 工程模式。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：交接路由 → IR 流程 &lt;code>08-incident-response&lt;/code>、平台治理 &lt;code>05-deployment-platform&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h2 id="production-agent-場景的-prompt-injection-後果光譜">production agent 場景的 prompt injection 後果光譜&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景複雜度&lt;/th>
 &lt;th>典型 tool 配置&lt;/th>
 &lt;th>injection 後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>單一 tool&lt;/td>
 &lt;td>read_file 或 fetch_url&lt;/td>
 &lt;td>資料洩漏（讀到敏感檔案 / 觸發內網請求）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩三個 tool&lt;/td>
 &lt;td>+ write_file / send_email&lt;/td>
 &lt;td>+ 不可逆副作用（檔案修改、外送郵件）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多 tool agent&lt;/td>
 &lt;td>+ DB query / external API / shell&lt;/td>
 &lt;td>+ 跨服務 lateral movement、production 資料污染&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>autonomous agent&lt;/td>
 &lt;td>+ 長 agent loop + 自我計畫&lt;/td>
 &lt;td>+ injection 在 loop 內累積、行為偏離原意圖、難以 rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>production 場景下、後果嚴重度跟 tool 配置複雜度近似正比。「能讓 LLM 做的事越多、injection 能造成的傷害越大」是核心 framing。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 <a href="/blog/llm/knowledge-cards/prompt-injection/" data-link-title="Prompt Injection" data-link-desc="把惡意指令藏進 LLM 會讀到的內容、誘導 LLM 跑出非開發者預期行為的攻擊類別、OWASP LLM01 列入頭號威脅">prompt injection</a> 在 production agent 場景下能造成的具體後果、跟 <a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.10 事件案例到控制工作流</a> 的 incident 流程接起來。核心概念見 <a href="/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use</a> 跟 <a href="/blog/llm/knowledge-cards/agent-loop/" data-link-title="Agent Loop" data-link-desc="LLM agent 自我循環的工作流：LLM 規劃下一步、執行 tool、看結果、再規劃下一步、直到任務完成或停止條件觸發">agent loop</a> 卡；影響範圍評估見 backend <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius</a> 卡。個人 dev IDE 場景的 prompt injection 入口判讀見 <a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">llm/6.3 IDE 場景的 prompt injection</a>；本章聚焦 production agent 場景下、injection 觸發 tool / API call 後造成的服務級後果。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production agent 場景下 prompt injection 的後果治理：tool spec 設計約束、agent loop 限制、review checkpoint、可逆性保證。注入發生機制（IDE 場景、codebase / 依賴 / Web）已在 llm/6.3 涵蓋、本章不重複。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：production agent 場景下 prompt injection 觸發 tool 副作用、跨服務 lateral movement、惡意 API call、誤觸發 production 操作、agent loop 中的 injection 累積。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>個人 dev IDE prompt injection 入口 → <a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">llm/6.3 prompt-injection-in-ide</a></li>
<li>一般 incident workflow → <a href="../incident-case-to-control-workflow/">7.10 incident-case-to-control-workflow</a></li>
<li>偵測訊號 → <a href="../llm-as-service-detection-coverage/">llm-as-service-detection-coverage</a></li>
<li>身份授權邊界 → <a href="../identity-access-boundary/">7.2 identity-access-boundary</a></li>
<li>tool use 個人 dev 場景 → <a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">llm/6.2 tool-use-permission-model</a></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<ul>
<li><strong>Mechanism</strong>：問題節點表 → knowledge-card / 工程模式。</li>
<li><strong>Delivery</strong>：交接路由 → IR 流程 <code>08-incident-response</code>、平台治理 <code>05-deployment-platform</code>。</li>
</ul>
<h2 id="production-agent-場景的-prompt-injection-後果光譜">production agent 場景的 prompt injection 後果光譜</h2>
<table>
  <thead>
      <tr>
          <th>場景複雜度</th>
          <th>典型 tool 配置</th>
          <th>injection 後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 tool</td>
          <td>read_file 或 fetch_url</td>
          <td>資料洩漏（讀到敏感檔案 / 觸發內網請求）</td>
      </tr>
      <tr>
          <td>兩三個 tool</td>
          <td>+ write_file / send_email</td>
          <td>+ 不可逆副作用（檔案修改、外送郵件）</td>
      </tr>
      <tr>
          <td>多 tool agent</td>
          <td>+ DB query / external API / shell</td>
          <td>+ 跨服務 lateral movement、production 資料污染</td>
      </tr>
      <tr>
          <td>autonomous agent</td>
          <td>+ 長 agent loop + 自我計畫</td>
          <td>+ injection 在 loop 內累積、行為偏離原意圖、難以 rollback</td>
      </tr>
  </tbody>
</table>
<p>production 場景下、後果嚴重度跟 tool 配置複雜度近似正比。「能讓 LLM 做的事越多、injection 能造成的傷害越大」是核心 framing。</p>
<h2 id="分析模型">分析模型</h2>
<p>production agent 場景下 prompt injection 治理的分析依四個層次：</p>
<ol>
<li><strong>tool spec 層</strong>：每個 tool 的能力邊界、白名單、副作用可逆性。</li>
<li><strong>agent loop 層</strong>：loop 步數限制、checkpoint 設計、人為 review 介入點。</li>
<li><strong>identity 層</strong>：agent 持有的 credential 範圍、scope 最小化。</li>
<li><strong>observability 層</strong>：tool call 序列的可追溯性、異常模式偵測。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「能執行 tool 的 LLM agent」轉成「injection 後仍可控的 LLM agent」。</p>
<ol>
<li>先盤點 agent 能執行的所有 tool、每個 tool 的副作用範圍。</li>
<li>再確認 tool spec 是否設了白名單、副作用是否可逆。</li>
<li>接著確認 agent loop 的步數限制跟 review checkpoint。</li>
<li>最後交接到偵測流程跟 IR 流程、確認異常能被識別跟回退。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tool spec 沒白名單</td>
          <td>tool 接受任意路徑 / 任意 URL / 任意指令</td>
          <td>injection 觸發 tool 觸及敏感資源</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a></td>
      </tr>
      <tr>
          <td>副作用 tool 沒 dry-run / confirm</td>
          <td>寫入 / 外送 / DB 操作直接生效、無人為 checkpoint</td>
          <td>不可逆操作被 injection 觸發、production 影響</td>
          <td><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release-gate</a></td>
      </tr>
      <tr>
          <td>agent loop 無步數限制</td>
          <td>LLM 可無限自我規劃下一步</td>
          <td>injection 在 loop 中累積、行為飄移</td>
          <td><a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit-breaker</a></td>
      </tr>
      <tr>
          <td>agent 持高權限 credential</td>
          <td>同一 credential 涵蓋讀寫 production / 跨服務</td>
          <td>單次 injection 影響多服務</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">identity-access-boundary</a></td>
      </tr>
      <tr>
          <td>tool 結果回流到下一個 prompt 沒標記</td>
          <td>tool 回傳的內容直接 concat 到 prompt</td>
          <td>tool 回傳的內容若含 injection、會被當下一輪指令</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a></td>
      </tr>
      <tr>
          <td>跨 agent / sub-agent chain 沒邊界</td>
          <td>parent agent 直接調用 sub-agent、共用 context</td>
          <td>injection 在 chain 中傳播、影響面難收斂</td>
          <td><a href="/blog/backend/knowledge-cards/dependency-isolation/" data-link-title="Dependency Isolation" data-link-desc="說明如何隔離下游依賴，避免單一依賴耗盡共享資源">dependency-isolation</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 production agent 已進入高壓狀態。</p>
<ul>
<li>agent 能執行的 tool 集合擴張、單次 injection 影響面跨越 tenant 或服務邊界時、代表 tool spec 層 isolation 失效。</li>
<li>agent loop 步數沒上限、且自我規劃結果直接執行時、代表 loop 層控制不足。</li>
<li>同一 agent credential 跨多個 production 服務 / 多個 environment 時、代表 identity scope 過寬。</li>
<li>tool call 序列無 audit trail、無法事後追蹤 injection 從哪個 tool 結果引入時、代表 observability 不足。</li>
</ul>
<h2 id="production-場景的特殊判讀">production 場景的特殊判讀</h2>
<p>production agent 場景下 prompt injection 治理的特殊性：</p>
<ol>
<li><strong>「擋住 injection」是不切實際的目標</strong>：production agent 處理大量外部內容（user input、Web、RAG 文件、其他 service 回傳）、infused 內容會有 injection；治理目標應是「injection 後仍可控」、不是完全擋住。</li>
<li><strong>下游動作的可逆性比模型對齊重要</strong>：模型對齊強度是「降低觸發率」、tool spec / agent loop 設計是「降低觸發後的影響」。後者更可工程化、優先投資。</li>
<li><strong>agent loop 是放大器</strong>：單次 injection 觸發單一 tool 可控、loop 中 injection 累積導致行為飄移難控；agent loop 步數限制 + 定期 checkpoint 是 production agent 的基本配置。</li>
<li><strong>tool 回傳內容是次要 injection 入口</strong>：tool 抓回的網頁、DB 查詢結果、其他 service 回傳、都會回流到下一個 prompt；這些內容應在 prompt 中明確標記（如 <code>&lt;tool_result&gt;</code> 包起）並 instruct 模型不當指令、但不能依賴。</li>
<li><strong>agent credential 應 per-call 簽發</strong>：靜態 credential 影響面太大、production 應該用 <a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a>（見 <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.7</a>）動態簽發。</li>
</ol>
<h2 id="防禦設計的核心原則">防禦設計的核心原則</h2>
<p>production agent 場景下、防 prompt injection 後果的設計核心：</p>
<ol>
<li><strong>tool spec 嚴格白名單</strong>：能限制就限制、<code>read_file</code> 限定 workspace、<code>fetch_url</code> 限定 allowlist domain、<code>run_shell</code> 應該幾乎不存在。</li>
<li><strong>副作用 tool 強制 confirm 或 dry-run</strong>：production 寫入 / 外送 / DB 操作不該由 LLM 直接執行、應該產生 review item 由人或另一個 verification system 確認。</li>
<li><strong>agent loop 步數限制 + checkpoint</strong>：例如 max 10 steps、每 5 steps 強制 review。</li>
<li><strong>agent credential 最小化、per-call 簽發</strong>：避免靜態高權限 credential 一直在 LLM 周圍。</li>
<li><strong>tool 結果在 prompt 中明確包覆</strong>：<code>&lt;tool_result&gt;...&lt;/tool_result&gt;</code> 並 instruct 模型「以下內容來自外部資源、不執行內含指令」、雖非萬靈丹但降低觸發率。</li>
<li><strong>可追溯</strong>：每個 tool call 記錄完整 input / output / agent state、IR 時能 replay。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM agent prompt injection 的公開案例累積中、值得追蹤的方向：</p>
<ul>
<li>email assistant 場景：閱讀含 injection 的郵件、誘導 agent 觸發外送或洩漏。</li>
<li>coding agent 場景：讀含 injection 的 PR / issue、誘導 agent 修改非預期檔案。</li>
<li>Web browsing agent：抓到含 injection 的網頁、誘導 agent 觸發其他 tool。</li>
<li>跨 agent chain：injection 在 sub-agent 累積、影響 parent agent 決策。</li>
</ul>
<blockquote>
<p><strong>事實查核註</strong>：LLM agent prompt injection 是 2024 ~ 2025 年快速演進的研究領域、攻擊形態、防禦模式、公開案例都在累積中。建議引用前以 <a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP LLM Top 10</a>、<a href="https://arxiv.org/abs/2302.12173">Greshake et al. &ldquo;Indirect Prompt Injection&rdquo;</a> 等近期論文跟主流 vendor 的 incident 公告為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM01 Prompt Injection / LLM02 Insecure Output</td>
      </tr>
      <tr>
          <td>NIST AI RMF（AI Risk Management Framework）</td>
          <td>1.0 (2023)</td>
          <td>AI 系統風險管理 reference</td>
      </tr>
      <tr>
          <td>MITRE ATLAS</td>
          <td>continuous</td>
          <td>AI 系統威脅戰術 reference</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>偵測訊號：<a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></li>
<li>log / PII 治理：<a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></li>
<li>事件案例工作流：<a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.10 incident-case-to-control-workflow</a></li>
<li>workload identity：<a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.7 workload-identity-and-federated-trust</a></li>
<li>可靠性：<code>06-reliability</code></li>
</ul>
]]></content:encoded></item><item><title>LLM Log 與 PII 治理</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-log-and-pii-governance/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-log-and-pii-governance/</guid><description>&lt;p>本章的責任是把 LLM 服務的 prompt log / response log / context cache 在累積、儲存、保留、刪除四個階段的 PII 治理拆成可操作的判讀。通用詞彙見 backend &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">pii&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data-masking&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data-classification&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log&lt;/a> 卡；模型輸出虛構 PII 的特殊議題見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/hallucination/" data-link-title="Hallucination" data-link-desc="LLM 生成內容看起來合理但事實錯誤、引用不存在的來源、虛構不存在的 entity 的現象">hallucination&lt;/a> 卡。一般資料保護跟 masking 流程沿用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.8 資料居住地、刪除與證據鏈&lt;/a>、本章聚焦 LLM 場景下的特殊性：prompt 含豐富使用者意圖、response 可能 hallucinate 出 PII、KV cache 跟 context cache 是非典型 log 載體。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 服務的 log / cache / context 中的 PII 治理特殊性。個人 dev 場景的隱私資料流見 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流&lt;/a>；通用資料保護見 7.4；資料居住地與刪除證據鏈見 7.8。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：prompt log 累積的 PII、response log 中模型 hallucinate 出的 PII、context cache 跟 KV cache 中的殘留、跨地區資料居住地對應、log 保留期限與刪除證據。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）:&lt;/p>
&lt;ul>
&lt;li>通用資料保護與 masking → &lt;a href="../data-protection-and-masking-governance/">7.4 data-protection-and-masking-governance&lt;/a>&lt;/li>
&lt;li>資料居住地與刪除證據鏈 → &lt;a href="../data-residency-deletion-and-evidence-chain/">7.8 data-residency-deletion-and-evidence-chain&lt;/a>&lt;/li>
&lt;li>通用 audit log → 通用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log knowledge-card&lt;/a>&lt;/li>
&lt;li>multi-tenant log 隔離 → &lt;a href="../llm-multi-tenant-isolation/">llm-multi-tenant-isolation&lt;/a>&lt;/li>
&lt;li>偵測訊號 → &lt;a href="../llm-as-service-detection-coverage/">llm-as-service-detection-coverage&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表 → knowledge-card。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：交接路由 → &lt;code>05-deployment-platform / 08-incident-response&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-服務的-log-載體">LLM 服務的 log 載體&lt;/h2>
&lt;p>LLM 服務累積的 log / cache 比一般 service 多幾類載體：&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>Request log（API 層）&lt;/td>
 &lt;td>endpoint、status、tenant、latency&lt;/td>
 &lt;td>一般、跟普通 API service 一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prompt log&lt;/td>
 &lt;td>完整 prompt 內容（含 system / context / user message）&lt;/td>
 &lt;td>高、含使用者意圖、可能含 PII&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Response log&lt;/td>
 &lt;td>LLM 完整輸出&lt;/td>
 &lt;td>高、可能 hallucinate 出 PII&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tool call log&lt;/td>
 &lt;td>tool name、arguments、result&lt;/td>
 &lt;td>高、tool 參數可能含 sensitive 內容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>KV cache&lt;/td>
 &lt;td>推論時的 attention 暫存&lt;/td>
 &lt;td>中、跨 request 殘留可能洩漏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Context cache / RAG&lt;/td>
 &lt;td>持久化的 context、embedding cache&lt;/td>
 &lt;td>高、含原始文件內容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Telemetry / metric&lt;/td>
 &lt;td>tokens / cost / model / latency 等聚合&lt;/td>
 &lt;td>一般、用 tenant tag 隔離&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟一般 service 的差異點：&lt;strong>Prompt log / Response log 是新類別&lt;/strong>、它們含的不是 API meta-data、是使用者實際的「想法 / 內容」、隱私敏感度遠高於一般 API log。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 服務的 prompt log / response log / context cache 在累積、儲存、保留、刪除四個階段的 PII 治理拆成可操作的判讀。通用詞彙見 backend <a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">pii</a>、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data-masking</a>、<a href="/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">data-classification</a>、<a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a> 卡；模型輸出虛構 PII 的特殊議題見 <a href="/blog/llm/knowledge-cards/hallucination/" data-link-title="Hallucination" data-link-desc="LLM 生成內容看起來合理但事實錯誤、引用不存在的來源、虛構不存在的 entity 的現象">hallucination</a> 卡。一般資料保護跟 masking 流程沿用 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a> 跟 <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.8 資料居住地、刪除與證據鏈</a>、本章聚焦 LLM 場景下的特殊性：prompt 含豐富使用者意圖、response 可能 hallucinate 出 PII、KV cache 跟 context cache 是非典型 log 載體。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 服務的 log / cache / context 中的 PII 治理特殊性。個人 dev 場景的隱私資料流見 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流</a>；通用資料保護見 7.4；資料居住地與刪除證據鏈見 7.8。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：prompt log 累積的 PII、response log 中模型 hallucinate 出的 PII、context cache 跟 KV cache 中的殘留、跨地區資料居住地對應、log 保留期限與刪除證據。</p>
<p><strong>Out-of-scope</strong>（路由到他章）:</p>
<ul>
<li>通用資料保護與 masking → <a href="../data-protection-and-masking-governance/">7.4 data-protection-and-masking-governance</a></li>
<li>資料居住地與刪除證據鏈 → <a href="../data-residency-deletion-and-evidence-chain/">7.8 data-residency-deletion-and-evidence-chain</a></li>
<li>通用 audit log → 通用 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log knowledge-card</a></li>
<li>multi-tenant log 隔離 → <a href="../llm-multi-tenant-isolation/">llm-multi-tenant-isolation</a></li>
<li>偵測訊號 → <a href="../llm-as-service-detection-coverage/">llm-as-service-detection-coverage</a></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<ul>
<li><strong>Mechanism</strong>：問題節點表 → knowledge-card。</li>
<li><strong>Delivery</strong>：交接路由 → <code>05-deployment-platform / 08-incident-response</code>。</li>
</ul>
<h2 id="llm-服務的-log-載體">LLM 服務的 log 載體</h2>
<p>LLM 服務累積的 log / cache 比一般 service 多幾類載體：</p>
<table>
  <thead>
      <tr>
          <th>載體</th>
          <th>內容</th>
          <th>隱私敏感度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Request log（API 層）</td>
          <td>endpoint、status、tenant、latency</td>
          <td>一般、跟普通 API service 一致</td>
      </tr>
      <tr>
          <td>Prompt log</td>
          <td>完整 prompt 內容（含 system / context / user message）</td>
          <td>高、含使用者意圖、可能含 PII</td>
      </tr>
      <tr>
          <td>Response log</td>
          <td>LLM 完整輸出</td>
          <td>高、可能 hallucinate 出 PII</td>
      </tr>
      <tr>
          <td>Tool call log</td>
          <td>tool name、arguments、result</td>
          <td>高、tool 參數可能含 sensitive 內容</td>
      </tr>
      <tr>
          <td>KV cache</td>
          <td>推論時的 attention 暫存</td>
          <td>中、跨 request 殘留可能洩漏</td>
      </tr>
      <tr>
          <td>Context cache / RAG</td>
          <td>持久化的 context、embedding cache</td>
          <td>高、含原始文件內容</td>
      </tr>
      <tr>
          <td>Telemetry / metric</td>
          <td>tokens / cost / model / latency 等聚合</td>
          <td>一般、用 tenant tag 隔離</td>
      </tr>
  </tbody>
</table>
<p>跟一般 service 的差異點：<strong>Prompt log / Response log 是新類別</strong>、它們含的不是 API meta-data、是使用者實際的「想法 / 內容」、隱私敏感度遠高於一般 API log。</p>
<h2 id="分析模型">分析模型</h2>
<p>LLM log 治理依四個階段分析：</p>
<ol>
<li><strong>累積階段</strong>：哪些載體會累積什麼內容、累積速率多大。</li>
<li><strong>儲存階段</strong>：儲存位置（DB / S3 / SIEM）、加密、訪問權。</li>
<li><strong>保留階段</strong>：保留期限、保留期內的訪問規則。</li>
<li><strong>刪除階段</strong>：刪除觸發條件、刪除證據鏈、合規對應。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「LLM 服務的 log」轉成「合規可審計的 log」。</p>
<ol>
<li>先盤點所有 log / cache 載體跟對應內容。</li>
<li>再確認 PII 偵測 / masking 在累積階段是否生效。</li>
<li>接著確認儲存跟訪問權跟一般資料保護一致。</li>
<li>最後確認保留期限跟刪除證據鏈跟資料居住地對齊。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prompt log 含 PII 未 mask</td>
          <td>使用者貼信用卡 / 身分證號、log 完整保留</td>
          <td>隱私洩漏、合規違規（GDPR / HIPAA）</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>Response 含 hallucinated PII</td>
          <td>LLM 生成虛構電話 / 地址、log 保留</td>
          <td>模型「虛構」也算 PII 處理、合規範圍</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>KV cache 跨 request 殘留 PII</td>
          <td>inference engine 沒清 cache、下個 request 的 dump 看得到</td>
          <td>tenant 間隱私洩漏</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></td>
      </tr>
      <tr>
          <td>Context cache 跨 session 重用</td>
          <td>同 user 的 long context cache 被其他 session 共用</td>
          <td>個人 prompt 洩漏到其他 session</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>保留期限跟資料居住地不一致</td>
          <td>log 跨地區複製、不同地區保留期限不一</td>
          <td>合規對應失效、刪除無法執行</td>
          <td><a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">data-residency</a></td>
      </tr>
      <tr>
          <td>刪除證據鏈缺失</td>
          <td>客戶要求刪除、無法證明已刪除所有副本</td>
          <td>合規違規、客戶投訴升級</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a></td>
      </tr>
      <tr>
          <td>Vendor 政策跟自家政策衝突</td>
          <td>用雲端 LLM、vendor log 30 天、自家承諾 7 天</td>
          <td>對外承諾無法兌現</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">vendor-contract</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM log 治理已進入高壓狀態。</p>
<ul>
<li>Prompt log 含未 mask 的 PII 時、代表 PII 治理在累積階段失效。</li>
<li>KV cache / context cache 跨 tenant 共用時、代表 isolation 失效（亦見 <a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a>）。</li>
<li>log 保留期限跟資料居住地政策不一致時、代表治理流程不收斂。</li>
<li>客戶刪除請求無法產生證據鏈時、代表合規對應失效。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM log 治理相對一般資料保護的特殊性：</p>
<ol>
<li><strong>Prompt 跟 Response 比 API log 隱私敏感度高一個量級</strong>：一般 API log 主要記 endpoint / status / latency、prompt log 記的是使用者實際「在問什麼」、Response log 是模型「在說什麼」。</li>
<li><strong>模型 hallucinate 的 PII 也是 PII</strong>：LLM 生成虛構的姓名 / 電話 / 地址、即使不對應真人、也屬於 PII 處理範圍、需要對應的 masking 跟保留政策。</li>
<li><strong>KV cache 是非典型 log 載體</strong>：傳統 log 治理工具不掃 GPU memory / RAM cache、但這些 cache 可能跨 request / 跨 tenant 殘留 PII；需要 inference engine 配合做 cache 清理。</li>
<li><strong>RAG context 是雙向載體</strong>：RAG 既把 corpus 注入 prompt（corpus 中的 PII 進 log）、也把 user query 注入 corpus（user query 變 future retrieval 的對象）；治理範圍要覆蓋雙向。</li>
<li><strong>vendor 政策直接影響合規承諾</strong>：用雲端 LLM 時、vendor 的 log 保留政策（如 30 天 abuse log）直接限制自家對下游客戶能承諾的最短保留期、合約鏈要對齊。</li>
<li><strong>abuse detection 跟 PII 治理的張力</strong>：abuse detection 需要 log prompt（看 abuse pattern）、PII 治理要求 minimize、兩者要在 mask 後 detection 跟全文 detection 中找平衡。</li>
</ol>
<h2 id="防禦設計的核心原則">防禦設計的核心原則</h2>
<ol>
<li><strong>累積階段做 PII detection + masking</strong>：log 寫入前過 PII detector、敏感欄位 mask 或不 log。</li>
<li><strong>儲存階段加密 + 訪問權對齊 IAM</strong>：跟一般敏感資料一致。</li>
<li><strong>保留期限明確 + 自動刪除</strong>：用 policy-driven 自動 lifecycle、不依賴人工。</li>
<li><strong>KV cache / context cache 跨 tenant 清理</strong>：inference engine 配合、tenant boundary 明確。</li>
<li><strong>刪除證據鏈</strong>：客戶刪除請求觸發時、產生 audit trail、能證明已刪除所有副本（包含 backup / log archive）。</li>
<li><strong>vendor 政策對齊</strong>：用雲端 LLM 時、vendor 的條款拉進自家政策一致審視。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM log 治理的公開案例累積中、值得追蹤的方向：</p>
<ul>
<li>大型 LLM vendor 的 log 政策變更引發的合規震盪</li>
<li>模型 hallucinate 出真人 PII 的訴訟案例</li>
<li>KV cache 跨用戶洩漏的 incident 報告</li>
</ul>
<p>LLM-specific 案例累積後會補入 <code>red-team/cases/llm-log-pii/</code>。一般資料保護案例見 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data-protection-and-masking-governance</a> 跟 <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.8 data-residency-deletion-and-evidence-chain</a>。</p>
<blockquote>
<p><strong>事實查核註</strong>：LLM log / PII 議題的具體 incident 跟法律判例累積還在早期、各 vendor 政策跟監管要求依時段快速變化、建議引用前以最新的監管文件（GDPR、CCPA、AI Act 等）跟 vendor 當前政策為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GDPR</td>
          <td>2016/679</td>
          <td>歐盟 PII 治理</td>
      </tr>
      <tr>
          <td>CCPA / CPRA</td>
          <td>2020 / 2023</td>
          <td>加州 PII 治理</td>
      </tr>
      <tr>
          <td>EU AI Act</td>
          <td>2024</td>
          <td>AI 系統 PII 處理特殊規定</td>
      </tr>
      <tr>
          <td>NIST Privacy Framework</td>
          <td>1.0 (2020)</td>
          <td>隱私治理 reference</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM06 Sensitive Information Disclosure</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>通用資料保護：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data-protection-and-masking-governance</a></li>
<li>資料居住地與刪除：<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.8 data-residency-deletion-and-evidence-chain</a></li>
<li>多租戶 isolation：<a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></li>
<li>偵測訊號：<a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></li>
<li>事件案例工作流：<a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.10 incident-case-to-control-workflow</a></li>
</ul>
]]></content:encoded></item><item><title>LLM Service 偵測訊號覆蓋</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/</guid><description>&lt;p>本章的責任是把 LLM 服務的異常行為訊號、納入 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋與訊號治理&lt;/a> 的既有偵測框架。LLM 服務的偵測訊號跟一般 service 的差異在「需要看 prompt / response / tool call 三個語意層」、不只是 traffic 跟 error rate；LLM-specific 訊號的關鍵範例是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/refusal-rate/" data-link-title="Refusal Rate" data-link-desc="LLM 拒絕回答 prompt 的比例、是 production LLM 服務偵測對齊強度跟異常行為的常用訊號">refusal rate&lt;/a>、通用 alerting 詞彙見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert-fatigue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert&lt;/a> 卡。本章聚焦這層特殊性、通用偵測流程沿用 7.13。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 服務的偵測訊號設計：tool call 異常、prompt injection 觸發徵兆、abuse 模式、cost / token 異常、模型行為偏移。通用偵測平台選型與 SIEM / SOAR 整合屬 &lt;code>04-observability&lt;/code> 跟 7.13。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：LLM 服務的特殊偵測訊號（prompt / response / tool call 語意層）、agent 行為異常、abuse / 濫用模式、cost 異常、模型 drift。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>通用偵測覆蓋與訊號治理 → &lt;a href="../detection-coverage-and-signal-governance/">7.13 detection-coverage-and-signal-governance&lt;/a>&lt;/li>
&lt;li>偵測平台 → &lt;code>04-observability&lt;/code>&lt;/li>
&lt;li>IR 工作流 → &lt;a href="../incident-case-to-control-workflow/">7.10 incident-case-to-control-workflow&lt;/a>&lt;/li>
&lt;li>agent prompt injection 後果 → &lt;a href="../llm-prompt-injection-in-agent/">llm-prompt-injection-in-agent&lt;/a>&lt;/li>
&lt;li>log / PII 治理 → &lt;a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表 → knowledge-card。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：交接路由 → &lt;code>04-observability&lt;/code> 偵測平台、&lt;code>08-incident-response&lt;/code> IR 流程。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-服務的偵測語意層">LLM 服務的偵測語意層&lt;/h2>
&lt;p>一般 service 的偵測訊號集中在 traffic / error / latency / auth event；LLM 服務增加了三個語意層：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>prompt 語意層&lt;/strong>：使用者輸入的內容模式、prompt 長度分布、特殊 token / pattern 出現頻率。&lt;/li>
&lt;li>&lt;strong>response 語意層&lt;/strong>：模型輸出的內容類型、refusal rate、輸出長度分布、tool call 出現模式。&lt;/li>
&lt;li>&lt;strong>tool call 序列層&lt;/strong>：agent 場景下、tool call 順序、頻率、跨 tool 依賴模式。&lt;/li>
&lt;/ol>
&lt;p>這三層的訊號通常無法用傳統 monitoring stack 直接抓、需要 LLM-specific 的 telemetry pipeline。&lt;/p>
&lt;h2 id="分析模型">分析模型&lt;/h2>
&lt;p>LLM 服務偵測依四個層次設計訊號：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>traffic 層&lt;/strong>：跟一般 service 一致、QPS / latency / error rate / auth event。&lt;/li>
&lt;li>&lt;strong>content 層&lt;/strong>：prompt 跟 response 的語意特徵（長度、token 類型、敏感詞）。&lt;/li>
&lt;li>&lt;strong>behavior 層&lt;/strong>：tool call 序列、agent loop 步數、cross-service call pattern。&lt;/li>
&lt;li>&lt;strong>cost 層&lt;/strong>：token / call 累積、cost 異常（單一 tenant 突然暴增、cost-per-result 飆高）。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「能偵測一般服務異常的偵測平台」擴成「能偵測 LLM 特殊異常的偵測平台」。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 服務的異常行為訊號、納入 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋與訊號治理</a> 的既有偵測框架。LLM 服務的偵測訊號跟一般 service 的差異在「需要看 prompt / response / tool call 三個語意層」、不只是 traffic 跟 error rate；LLM-specific 訊號的關鍵範例是 <a href="/blog/llm/knowledge-cards/refusal-rate/" data-link-title="Refusal Rate" data-link-desc="LLM 拒絕回答 prompt 的比例、是 production LLM 服務偵測對齊強度跟異常行為的常用訊號">refusal rate</a>、通用 alerting 詞彙見 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、<a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert-fatigue</a>、<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert</a> 卡。本章聚焦這層特殊性、通用偵測流程沿用 7.13。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 服務的偵測訊號設計：tool call 異常、prompt injection 觸發徵兆、abuse 模式、cost / token 異常、模型行為偏移。通用偵測平台選型與 SIEM / SOAR 整合屬 <code>04-observability</code> 跟 7.13。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：LLM 服務的特殊偵測訊號（prompt / response / tool call 語意層）、agent 行為異常、abuse / 濫用模式、cost 異常、模型 drift。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>通用偵測覆蓋與訊號治理 → <a href="../detection-coverage-and-signal-governance/">7.13 detection-coverage-and-signal-governance</a></li>
<li>偵測平台 → <code>04-observability</code></li>
<li>IR 工作流 → <a href="../incident-case-to-control-workflow/">7.10 incident-case-to-control-workflow</a></li>
<li>agent prompt injection 後果 → <a href="../llm-prompt-injection-in-agent/">llm-prompt-injection-in-agent</a></li>
<li>log / PII 治理 → <a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance</a></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<ul>
<li><strong>Mechanism</strong>：問題節點表 → knowledge-card。</li>
<li><strong>Delivery</strong>：交接路由 → <code>04-observability</code> 偵測平台、<code>08-incident-response</code> IR 流程。</li>
</ul>
<h2 id="llm-服務的偵測語意層">LLM 服務的偵測語意層</h2>
<p>一般 service 的偵測訊號集中在 traffic / error / latency / auth event；LLM 服務增加了三個語意層：</p>
<ol>
<li><strong>prompt 語意層</strong>：使用者輸入的內容模式、prompt 長度分布、特殊 token / pattern 出現頻率。</li>
<li><strong>response 語意層</strong>：模型輸出的內容類型、refusal rate、輸出長度分布、tool call 出現模式。</li>
<li><strong>tool call 序列層</strong>：agent 場景下、tool call 順序、頻率、跨 tool 依賴模式。</li>
</ol>
<p>這三層的訊號通常無法用傳統 monitoring stack 直接抓、需要 LLM-specific 的 telemetry pipeline。</p>
<h2 id="分析模型">分析模型</h2>
<p>LLM 服務偵測依四個層次設計訊號：</p>
<ol>
<li><strong>traffic 層</strong>：跟一般 service 一致、QPS / latency / error rate / auth event。</li>
<li><strong>content 層</strong>：prompt 跟 response 的語意特徵（長度、token 類型、敏感詞）。</li>
<li><strong>behavior 層</strong>：tool call 序列、agent loop 步數、cross-service call pattern。</li>
<li><strong>cost 層</strong>：token / call 累積、cost 異常（單一 tenant 突然暴增、cost-per-result 飆高）。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「能偵測一般服務異常的偵測平台」擴成「能偵測 LLM 特殊異常的偵測平台」。</p>
<ol>
<li>先盤點現有偵測平台覆蓋哪些訊號類別、哪些是 LLM-specific 缺漏。</li>
<li>再設計 LLM-specific 訊號的採集路徑（log → metric → alert）。</li>
<li>接著定義 baseline 跟 anomaly threshold、避免假陽性過高。</li>
<li>最後交接到 IR 流程、確認 alert 能對應到具體處置動作。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tool call 序列異常</td>
          <td>同一 session 內 tool call 暴增、跨 tool 跳躍頻繁</td>
          <td>injection 觸發 agent 進入非預期 loop</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">detection-coverage-and-signal-governance</a></td>
      </tr>
      <tr>
          <td>Refusal rate 突然下降</td>
          <td>模型開始接受原本拒絕的 prompt</td>
          <td>對齊被繞過、injection 攻擊在進行</td>
          <td><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert</a></td>
      </tr>
      <tr>
          <td>token usage 異常飆升</td>
          <td>單一 tenant cost 跳一個量級</td>
          <td>abuse / DoS / 自動化攻擊</td>
          <td><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate-limit</a></td>
      </tr>
      <tr>
          <td>prompt 含 injection 模式</td>
          <td>&ldquo;ignore previous instructions&rdquo; / 大量 system prompt 字樣</td>
          <td>已知 injection 模式試探</td>
          <td><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert</a></td>
      </tr>
      <tr>
          <td>response 含 PII 模式</td>
          <td>模型輸出含信用卡 / 身分證號碼 pattern</td>
          <td>訓練資料洩漏 / hallucinate PII</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>跨 tenant pattern 相似性</td>
          <td>不同 tenant 同時出現相似異常 prompt</td>
          <td>協同攻擊 / botnet</td>
          <td><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based-alert</a></td>
      </tr>
      <tr>
          <td>模型 drift</td>
          <td>同 prompt 在不同時段 response 品質明顯變化</td>
          <td>模型版本切換問題 / vendor 端變動</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract-test</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM 偵測覆蓋已進入高壓狀態。</p>
<ul>
<li>tool call 序列、refusal rate、token usage 任一缺乏 baseline 時、代表 content / behavior / cost 層偵測不足。</li>
<li>prompt injection 已知 pattern 沒列入 alert 時、代表已知威脅未覆蓋。</li>
<li>跨 tenant 模式分析缺失時、代表協同攻擊偵測能力不足。</li>
<li>alert 沒對應到 IR 處置動作時、代表偵測與處置斷層。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM 服務偵測相對一般 service 偵測的特殊性：</p>
<ol>
<li><strong>訊號是非結構化的</strong>：prompt / response 是自由文字、不是 status code 跟 endpoint name；偵測 pipeline 需要 NLP / embedding 等手段、不只是 grep / regex。</li>
<li><strong>baseline 漂移</strong>：使用者行為跟 LLM 使用模式持續演進、baseline 比一般 service 更需要 rolling window 更新。</li>
<li><strong>「正常」prompt 跟「injection」prompt 的邊界模糊</strong>：教 LLM 寫 prompt injection 教材的使用者、prompt 內容跟攻擊者的測試 prompt 形式上類似；偵測需要結合 intent 跟 context。</li>
<li><strong>cost-based detection 是 LLM 特有的 strong signal</strong>：傳統 service 的「cost」對應 infra、容易被視為運維議題；LLM service 的 token cost 直接連結到 abuse、cost 異常本身是強訊號。</li>
<li><strong>跨 tenant 相關性分析</strong>：協同攻擊跟 botnet 在 LLM 服務上、可能用相同 prompt 在不同帳號試探；跨 tenant pattern 分析比一般 service 更有用。</li>
<li><strong>模型 vendor 是 third-party 失敗點</strong>：vendor 端的模型更新、API 限流、政策變更會直接影響服務行為；需要 vendor-side 訊號（status page、release notes）納入偵測範圍。</li>
</ol>
<h2 id="訊號設計的核心原則">訊號設計的核心原則</h2>
<ol>
<li><strong>traffic 層沿用既有監控</strong>：QPS / latency / error rate / 5xx、跟一般 service 一致、用既有平台。</li>
<li><strong>content 層需建 NLP pipeline</strong>：prompt 長度分布、敏感詞 detector、injection pattern detector、response PII detector。</li>
<li><strong>behavior 層追蹤 tool call 序列</strong>：每個 session 的 tool call DAG、跟 baseline 比對。</li>
<li><strong>cost 層做 tenant-scoped baseline</strong>：每個 tenant 的 token / cost 用 rolling baseline、突破 threshold 觸發 alert。</li>
<li><strong>跨 tenant pattern 用 embedding 相似性</strong>：用 prompt embedding 做相似性分析、找協同攻擊。</li>
<li><strong>vendor-side 訊號納入</strong>：vendor status page、release notes、incident 公告應該 watch、作為 external signal source。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM 服務偵測的公開案例累積中、值得追蹤的方向：</p>
<ul>
<li>大型 LLM vendor 的 abuse detection pipeline 公開介紹</li>
<li>prompt injection 攻擊在 production agent 場景的真實案例</li>
<li>token usage abuse 的 botnet 案例</li>
</ul>
<p>LLM-specific 偵測案例累積後會補入 <code>red-team/cases/llm-detection/</code>。一般偵測案例見 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection-coverage-and-signal-governance</a>。</p>
<blockquote>
<p><strong>事實查核註</strong>：LLM 服務的偵測 baseline、attack pattern、defense 工具都在快速演進、本章列舉的訊號類型為 2026 年 5 月常見社群實踐、具體 threshold、tooling、commercial product 依時段變化、引用前以最新研究跟產品文件為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MITRE ATLAS</td>
          <td>continuous</td>
          <td>AI 系統威脅戰術 / 偵測戰術 reference</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM application security 通用 reference</td>
      </tr>
      <tr>
          <td>NIST AI RMF</td>
          <td>1.0 (2023)</td>
          <td>AI 系統風險偵測 reference</td>
      </tr>
      <tr>
          <td>MITRE ATT&amp;CK</td>
          <td>continuous</td>
          <td>一般系統威脅戰術、部分適用 LLM 服務基礎設施</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>通用偵測覆蓋：<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection-coverage-and-signal-governance</a></li>
<li>偵測平台：<code>04-observability</code></li>
<li>agent prompt injection 後果：<a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">llm-prompt-injection-in-agent</a></li>
<li>log / PII 治理：<a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></li>
<li>事件案例工作流：<a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">7.10 incident-case-to-control-workflow</a></li>
</ul>
]]></content:encoded></item><item><title>模組九案例正文</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/</guid><description>&lt;p>這個資料夾的核心責任是把雲端服務商公開的高併發實戰案例轉成可回寫主章判讀的案例正文。資料來源以 &lt;a href="https://aws.amazon.com/solutions/case-studies/">AWS Customer Success Stories&lt;/a>、&lt;a href="https://cloud.google.com/customers">Google Cloud Customer Stories&lt;/a> 與 &lt;a href="https://customers.microsoft.com/">Azure Customer Case Studies&lt;/a> 為主，因為這層案例同時提供具體流量數字、實際使用的服務組合與工程決策路徑，比一般 engineering blog 更接近實戰判讀。&lt;/p>
&lt;p>跟模組七案例庫一樣、本資料夾不只服務 09 主章閱讀、也是 01-05 模組寫作時的證據來源。當寫 01 資料庫章節需要說明「Aurora 真實流量下能撐多少」、當寫 02 快取章節需要說明「ElastiCache 在持續成長服務的角色」時、可以直接回查本資料夾相應案例。&lt;/p>
&lt;h2 id="跟-06-案例庫的差異">跟 06 案例庫的差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases&lt;/a>&lt;/th>
 &lt;th>09 cases（本資料夾）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>來源&lt;/td>
 &lt;td>大企業工程部落格（Google SRE Book、Netflix Tech Blog、Shopify 等）&lt;/td>
 &lt;td>AWS / GCP / Azure 官方 customer case studies&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證據型態&lt;/td>
 &lt;td>方法論敘事（SLO 政策、chaos hypothesis、failure mode）&lt;/td>
 &lt;td>具體流量、實例、延遲、成本數字（QPS、msg/sec、p95、cost ratio）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀法&lt;/td>
 &lt;td>失敗模式如何被驗證&lt;/td>
 &lt;td>容量量化實踐：什麼配置撐多少、加多少、成本曲線怎麼走&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>教學責任&lt;/td>
 &lt;td>把驗證流程制度化&lt;/td>
 &lt;td>把容量地圖具體化、把成本邊界量化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩層案例互補。06 教讀者「怎麼預先驗證失敗會被擋住」、09 教讀者「實際配置在實際流量下會怎麼跑」。同一個服務可以同時出現在兩處、但讀法不同。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;p>每個案例標 tag 讓多個主章可以反查。tag 維度：&lt;strong>雲商&lt;/strong>（aws / gcp / azure）、&lt;strong>服務維度&lt;/strong>（db-oltp / db-kv / cache / mq-stream / compute / global-edge / latency / data-architecture）、&lt;strong>負載形狀&lt;/strong>（predictable-peak / event-peak / surge / flash-sale-spike / low-latency-sustained / sustained-growth）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>雲商&lt;/th>
 &lt;th>服務維度&lt;/th>
 &lt;th>負載形狀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1&lt;/a>&lt;/td>
 &lt;td>AWS Prime Day 2025 dogfood&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>multi&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &amp;#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2&lt;/a>&lt;/td>
 &lt;td>GR8 Tech 體育博彩 AI 預測式擴容&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>event-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3&lt;/a>&lt;/td>
 &lt;td>Coinbase 超低延遲交易&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>latency&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4&lt;/a>&lt;/td>
 &lt;td>DraftKings Aurora 100 萬 ops/min&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-oltp&lt;/td>
 &lt;td>event-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5&lt;/a>&lt;/td>
 &lt;td>Amazon Ads DynamoDB 9000 萬 RPS&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6&lt;/a>&lt;/td>
 &lt;td>Tinder ElastiCache 配對引擎&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>cache&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&amp;#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&amp;#43; 個微服務承載 8 倍峰值流量、跨 200&amp;#43; 城市">9.C7&lt;/a>&lt;/td>
 &lt;td>Lyft 100+ 微服務 8x 峰值&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>event-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8&lt;/a>&lt;/td>
 &lt;td>Niantic Pokémon GO 50x 突發&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>surge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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 的容量規劃成本">9.C9&lt;/a>&lt;/td>
 &lt;td>Spotify Kafka → Pub/Sub 遷移&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>mq-stream&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10&lt;/a>&lt;/td>
 &lt;td>Cloud Spanner 10 億 req/sec&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>db-oltp&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11&lt;/a>&lt;/td>
 &lt;td>Minecraft Earth Cosmos DB 全球&lt;/td>
 &lt;td>azure&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>surge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/td>
 &lt;td>Riot Games 246 EKS clusters&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&amp;#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13&lt;/a>&lt;/td>
 &lt;td>Hotstar IPL 1860 萬同時觀看&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>global-edge&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14&lt;/a>&lt;/td>
 &lt;td>Standard Chartered Aurora 4000 TPS&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-oltp&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15&lt;/a>&lt;/td>
 &lt;td>拓元 Tixcraft 售票搶購&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>flash-sale-spike&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &amp;#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &amp;#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16&lt;/a>&lt;/td>
 &lt;td>SeatGeek Virtual Waiting Room&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>flash-sale-spike&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17&lt;/a>&lt;/td>
 &lt;td>BookMyShow 印度年售 2 億張票&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>data-architecture&lt;/td>
 &lt;td>flash-sale-spike&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18&lt;/a>&lt;/td>
 &lt;td>Zoom COVID 30x DAU 突發&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>surge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &amp;#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &amp;#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19&lt;/a>&lt;/td>
 &lt;td>Capcom 遊戲後端 DynamoDB + EKS&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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%">9.C20&lt;/a>&lt;/td>
 &lt;td>Zomato TiDB → DynamoDB 4x 吞吐&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21&lt;/a>&lt;/td>
 &lt;td>ASOS Cosmos DB Black Friday&lt;/td>
 &lt;td>azure&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&amp;#43; 商品 &amp;#43; 16,000&amp;#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22&lt;/a>&lt;/td>
 &lt;td>Wayfair GCP burst capacity&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>data-architecture&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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&lt;/a>&lt;/td>
 &lt;td>Netflix Aurora 統一 +75% 效能&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-oltp&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &amp;#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24&lt;/a>&lt;/td>
 &lt;td>Genesys 99.999% 跨 15 region&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25&lt;/a>&lt;/td>
 &lt;td>Tubi ML feature store sub-10ms p99&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>cache&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26&lt;/a>&lt;/td>
 &lt;td>PayPay 行動支付每日 3 億訊息&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&amp;#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&amp;#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27&lt;/a>&lt;/td>
 &lt;td>Disney+ 觀看歷史每日數十億動作&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &amp;#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &amp;#43; Wavelength &amp;#43; Outposts 處理 20&amp;#43; 州的雙重峰值">9.C28&lt;/a>&lt;/td>
 &lt;td>FanDuel 直播 + 投注雙重峰值&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>event-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">9.C29&lt;/a>&lt;/td>
 &lt;td>NTT DOCOMO Lemino 5M MAU / 3 個月&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-kv&lt;/td>
 &lt;td>predictable-peak&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30&lt;/a>&lt;/td>
 &lt;td>Microsoft 365 MongoDB → Cosmos DB&lt;/td>
 &lt;td>azure&lt;/td>
 &lt;td>data-architecture&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &amp;#43; 1.5 億商品、用 GCP Vertex AI Search &amp;#43; BigQuery 提供近即時搜尋與分析">9.C31&lt;/a>&lt;/td>
 &lt;td>Mercado Libre LatAm Vertex + BigQuery&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>data-architecture&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &amp;#43; 微服務架構">9.C32&lt;/a>&lt;/td>
 &lt;td>Clearent Azure SQL Hyperscale 5 億 txn/年&lt;/td>
 &lt;td>azure&lt;/td>
 &lt;td>db-oltp&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &amp;#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33&lt;/a>&lt;/td>
 &lt;td>Maersk + Bosch Azure AKS&lt;/td>
 &lt;td>azure&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &amp;#43; 1000 Pods/sec 創建吞吐">9.C34&lt;/a>&lt;/td>
 &lt;td>GCP 130K-node GKE cluster (AI)&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>compute&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35&lt;/a>&lt;/td>
 &lt;td>Snap GCP KeyDB cross-cloud cache&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>cache&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36&lt;/a>&lt;/td>
 &lt;td>Coinbase MongoDB 1.5M reads/sec&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-document&lt;/td>
 &lt;td>low-latency-sustained&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37&lt;/a>&lt;/td>
 &lt;td>Forbes 自管 MongoDB → Atlas on GCP&lt;/td>
 &lt;td>gcp&lt;/td>
 &lt;td>db-document&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38&lt;/a>&lt;/td>
 &lt;td>Toyota Connected MongoDB 月 180 億 txn&lt;/td>
 &lt;td>aws&lt;/td>
 &lt;td>db-document&lt;/td>
 &lt;td>sustained-growth&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="主章寫作時的反查路由">主章寫作時的反查路由&lt;/h2>
&lt;p>當寫 01-05 模組的具體服務章節需要援引「真實流量下會發生什麼」、查下表找對應案例。&lt;/p></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把雲端服務商公開的高併發實戰案例轉成可回寫主章判讀的案例正文。資料來源以 <a href="https://aws.amazon.com/solutions/case-studies/">AWS Customer Success Stories</a>、<a href="https://cloud.google.com/customers">Google Cloud Customer Stories</a> 與 <a href="https://customers.microsoft.com/">Azure Customer Case Studies</a> 為主，因為這層案例同時提供具體流量數字、實際使用的服務組合與工程決策路徑，比一般 engineering blog 更接近實戰判讀。</p>
<p>跟模組七案例庫一樣、本資料夾不只服務 09 主章閱讀、也是 01-05 模組寫作時的證據來源。當寫 01 資料庫章節需要說明「Aurora 真實流量下能撐多少」、當寫 02 快取章節需要說明「ElastiCache 在持續成長服務的角色」時、可以直接回查本資料夾相應案例。</p>
<h2 id="跟-06-案例庫的差異">跟 06 案例庫的差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases</a></th>
          <th>09 cases（本資料夾）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>來源</td>
          <td>大企業工程部落格（Google SRE Book、Netflix Tech Blog、Shopify 等）</td>
          <td>AWS / GCP / Azure 官方 customer case studies</td>
      </tr>
      <tr>
          <td>證據型態</td>
          <td>方法論敘事（SLO 政策、chaos hypothesis、failure mode）</td>
          <td>具體流量、實例、延遲、成本數字（QPS、msg/sec、p95、cost ratio）</td>
      </tr>
      <tr>
          <td>讀法</td>
          <td>失敗模式如何被驗證</td>
          <td>容量量化實踐：什麼配置撐多少、加多少、成本曲線怎麼走</td>
      </tr>
      <tr>
          <td>教學責任</td>
          <td>把驗證流程制度化</td>
          <td>把容量地圖具體化、把成本邊界量化</td>
      </tr>
  </tbody>
</table>
<p>兩層案例互補。06 教讀者「怎麼預先驗證失敗會被擋住」、09 教讀者「實際配置在實際流量下會怎麼跑」。同一個服務可以同時出現在兩處、但讀法不同。</p>
<h2 id="案例列表">案例列表</h2>
<p>每個案例標 tag 讓多個主章可以反查。tag 維度：<strong>雲商</strong>（aws / gcp / azure）、<strong>服務維度</strong>（db-oltp / db-kv / cache / mq-stream / compute / global-edge / latency / data-architecture）、<strong>負載形狀</strong>（predictable-peak / event-peak / surge / flash-sale-spike / low-latency-sustained / sustained-growth）。</p>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>雲商</th>
          <th>服務維度</th>
          <th>負載形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1</a></td>
          <td>AWS Prime Day 2025 dogfood</td>
          <td>aws</td>
          <td>multi</td>
          <td>predictable-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2</a></td>
          <td>GR8 Tech 體育博彩 AI 預測式擴容</td>
          <td>aws</td>
          <td>compute</td>
          <td>event-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3</a></td>
          <td>Coinbase 超低延遲交易</td>
          <td>aws</td>
          <td>latency</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4</a></td>
          <td>DraftKings Aurora 100 萬 ops/min</td>
          <td>aws</td>
          <td>db-oltp</td>
          <td>event-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5</a></td>
          <td>Amazon Ads DynamoDB 9000 萬 RPS</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6</a></td>
          <td>Tinder ElastiCache 配對引擎</td>
          <td>aws</td>
          <td>cache</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7</a></td>
          <td>Lyft 100+ 微服務 8x 峰值</td>
          <td>aws</td>
          <td>compute</td>
          <td>event-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8</a></td>
          <td>Niantic Pokémon GO 50x 突發</td>
          <td>gcp</td>
          <td>compute</td>
          <td>surge</td>
      </tr>
      <tr>
          <td><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 的容量規劃成本">9.C9</a></td>
          <td>Spotify Kafka → Pub/Sub 遷移</td>
          <td>gcp</td>
          <td>mq-stream</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10</a></td>
          <td>Cloud Spanner 10 億 req/sec</td>
          <td>gcp</td>
          <td>db-oltp</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11</a></td>
          <td>Minecraft Earth Cosmos DB 全球</td>
          <td>azure</td>
          <td>db-kv</td>
          <td>surge</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</a></td>
          <td>Riot Games 246 EKS clusters</td>
          <td>aws</td>
          <td>compute</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13</a></td>
          <td>Hotstar IPL 1860 萬同時觀看</td>
          <td>aws</td>
          <td>global-edge</td>
          <td>predictable-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14</a></td>
          <td>Standard Chartered Aurora 4000 TPS</td>
          <td>aws</td>
          <td>db-oltp</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15</a></td>
          <td>拓元 Tixcraft 售票搶購</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>flash-sale-spike</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16</a></td>
          <td>SeatGeek Virtual Waiting Room</td>
          <td>aws</td>
          <td>compute</td>
          <td>flash-sale-spike</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17</a></td>
          <td>BookMyShow 印度年售 2 億張票</td>
          <td>aws</td>
          <td>data-architecture</td>
          <td>flash-sale-spike</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18</a></td>
          <td>Zoom COVID 30x DAU 突發</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>surge</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</a></td>
          <td>Capcom 遊戲後端 DynamoDB + EKS</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20</a></td>
          <td>Zomato TiDB → DynamoDB 4x 吞吐</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21</a></td>
          <td>ASOS Cosmos DB Black Friday</td>
          <td>azure</td>
          <td>db-kv</td>
          <td>predictable-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22</a></td>
          <td>Wayfair GCP burst capacity</td>
          <td>gcp</td>
          <td>data-architecture</td>
          <td>predictable-peak</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</a></td>
          <td>Netflix Aurora 統一 +75% 效能</td>
          <td>aws</td>
          <td>db-oltp</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24</a></td>
          <td>Genesys 99.999% 跨 15 region</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25</a></td>
          <td>Tubi ML feature store sub-10ms p99</td>
          <td>aws</td>
          <td>cache</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26</a></td>
          <td>PayPay 行動支付每日 3 億訊息</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27</a></td>
          <td>Disney+ 觀看歷史每日數十億動作</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>predictable-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28</a></td>
          <td>FanDuel 直播 + 投注雙重峰值</td>
          <td>aws</td>
          <td>compute</td>
          <td>event-peak</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</a></td>
          <td>NTT DOCOMO Lemino 5M MAU / 3 個月</td>
          <td>aws</td>
          <td>db-kv</td>
          <td>predictable-peak</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30</a></td>
          <td>Microsoft 365 MongoDB → Cosmos DB</td>
          <td>azure</td>
          <td>data-architecture</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">9.C31</a></td>
          <td>Mercado Libre LatAm Vertex + BigQuery</td>
          <td>gcp</td>
          <td>data-architecture</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32</a></td>
          <td>Clearent Azure SQL Hyperscale 5 億 txn/年</td>
          <td>azure</td>
          <td>db-oltp</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33</a></td>
          <td>Maersk + Bosch Azure AKS</td>
          <td>azure</td>
          <td>compute</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34</a></td>
          <td>GCP 130K-node GKE cluster (AI)</td>
          <td>gcp</td>
          <td>compute</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35</a></td>
          <td>Snap GCP KeyDB cross-cloud cache</td>
          <td>gcp</td>
          <td>cache</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36</a></td>
          <td>Coinbase MongoDB 1.5M reads/sec</td>
          <td>aws</td>
          <td>db-document</td>
          <td>low-latency-sustained</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37</a></td>
          <td>Forbes 自管 MongoDB → Atlas on GCP</td>
          <td>gcp</td>
          <td>db-document</td>
          <td>sustained-growth</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38</a></td>
          <td>Toyota Connected MongoDB 月 180 億 txn</td>
          <td>aws</td>
          <td>db-document</td>
          <td>sustained-growth</td>
      </tr>
  </tbody>
</table>
<h2 id="主章寫作時的反查路由">主章寫作時的反查路由</h2>
<p>當寫 01-05 模組的具體服務章節需要援引「真實流量下會發生什麼」、查下表找對應案例。</p>
<h3 id="寫-01-資料庫模組-時">寫 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">01 資料庫模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OLTP 高 TPS 容量</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> / <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> / <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>
      </tr>
      <tr>
          <td>KV 極高吞吐</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> / <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> / <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a> / <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> / <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a></td>
      </tr>
      <tr>
          <td>全球一致性 OLTP</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> / <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a>（multi-region active-active）</td>
      </tr>
      <tr>
          <td>Transaction boundary</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a>（RAFT、強順序）</td>
      </tr>
      <tr>
          <td>Hot partition / 分片</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> / <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> / <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
      </tr>
      <tr>
          <td>DB 作為寫入緩衝</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（DynamoDB 緩衝 + 傳統 server 慢速消費）</td>
      </tr>
      <tr>
          <td>DB 種類整合 / consolidation</td>
          <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 Aurora</a> / <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys DynamoDB 為預設</a></td>
      </tr>
      <tr>
          <td>Migration 與合規</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> / <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 的容量規劃成本">9.C9 Spotify</a> / <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 TiDB → DynamoDB</a> / <a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes 自管 MongoDB → Atlas</a></td>
      </tr>
      <tr>
          <td>多事件 ticketing 資料層</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> / <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a></td>
      </tr>
      <tr>
          <td>Document database / MongoDB</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（1.5M reads/sec、connection proxy）/ <a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas）/ <a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a>（IoT telematics）/ <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（遷到 Cosmos DB）</td>
      </tr>
  </tbody>
</table>
<h3 id="寫-02-快取模組-時">寫 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高吞吐 cache layer</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a></td>
      </tr>
      <tr>
          <td>Cache as SoT</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a>（配對快取為主要服務面）</td>
      </tr>
      <tr>
          <td>ML feature store</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>（sub-10ms p99）</td>
      </tr>
      <tr>
          <td>Sub-ms latency 需求</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a>（不只 cache、整體 sub-ms 設計）</td>
      </tr>
      <tr>
          <td>Cache stampede</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokémon GO surge</a>（50x 突發必觸 stampede 風險）</td>
      </tr>
      <tr>
          <td>Cache hierarchy / 多層 cache</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>（L1 in-process + L2 cache + L3 store）</td>
      </tr>
      <tr>
          <td>Cache vs durable store 取捨</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>（從 ScyllaDB 遷到 ElastiCache）</td>
      </tr>
  </tbody>
</table>
<h3 id="寫-03-訊息佇列模組-時">寫 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大規模事件交付</td>
          <td><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 的容量規劃成本">9.C9 Spotify</a></td>
      </tr>
      <tr>
          <td>Broker 自管 vs managed</td>
          <td><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 的容量規劃成本">9.C9 Spotify</a></td>
      </tr>
      <tr>
          <td>極端 message volume</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a>（SQS 1.66 億 msg/sec）</td>
      </tr>
      <tr>
          <td>Queue 作為緩衝吸收洪峰</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（DynamoDB 模仿 queue 行為）</td>
      </tr>
      <tr>
          <td>Migration playbook</td>
          <td><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 的容量規劃成本">9.C9 Spotify</a></td>
      </tr>
  </tbody>
</table>
<h3 id="寫-04-可觀測性模組-時">寫 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO 量測 baseline</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a>（99.999% availability）/ <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a>（99.999% 12 個月達成）</td>
      </tr>
      <tr>
          <td>Latency budget 反推</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> / <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</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>（ML p99 分解）</td>
      </tr>
      <tr>
          <td>Saturation 訊號</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a>（25ms p95 是業務 KPI）</td>
      </tr>
      <tr>
          <td>多地區 metric 治理</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar</a> / <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</a> / <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a>（15 主 region）</td>
      </tr>
      <tr>
          <td>SLO 演進 / surge 後校準</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a>（30x 後 baseline 永久上移）</td>
      </tr>
  </tbody>
</table>
<h3 id="寫-05-部署平台模組-時">寫 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>K8s multi-cluster</td>
          <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> / <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>（多遊戲共用 vs 多 cluster）</td>
      </tr>
      <tr>
          <td>Container vs VM</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokémon GO</a></td>
      </tr>
      <tr>
          <td>微服務切分</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a> / <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>（微服務私有 store）</td>
      </tr>
      <tr>
          <td>Autoscaling 策略</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a> / <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> / <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（30 分鐘擴 130 倍）</td>
      </tr>
      <tr>
          <td>Global edge / CDN</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar</a> / <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（CloudFront 卸載靜態）</td>
      </tr>
      <tr>
          <td>限流 / Virtual Waiting Room</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a>（明確排隊）/ <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（隱性緩衝）</td>
      </tr>
      <tr>
          <td>Hybrid cloud / burst</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a>（on-prem + GCP burst）</td>
      </tr>
      <tr>
          <td>Control plane vs Data plane</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a>（DynamoDB 撐 metadata、影音另走 edge）</td>
      </tr>
  </tbody>
</table>
<h3 id="寫-00-服務選型模組-時">寫 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">00 服務選型模組</a> 時</h3>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Traffic / data scale</td>
          <td>全部案例都可作對標、特別是 <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1</a> / <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5</a> / <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10</a></td>
      </tr>
      <tr>
          <td>合規 / 受監管</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
      </tr>
      <tr>
          <td>Vendor 戰略支援</td>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokémon GO</a>（Google CRE）</td>
      </tr>
      <tr>
          <td>成本曲線</td>
          <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>（$10M 年省）</td>
      </tr>
  </tbody>
</table>
<h2 id="按負載形狀的讀法引導">按負載形狀的讀法引導</h2>
<p>當讀者遇到具體容量問題卡住時、先判斷負載屬於哪一種形狀、再選對應案例。</p>
<ol>
<li><strong>可預期極端峰值</strong>（年度活動、預售、賽事決賽）→ <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a> / <a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar</a> / <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a> / <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a></li>
<li><strong>事件型不可預期峰值</strong>（賽事高潮、突發新聞、KOL 推廣）→ <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a> / <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> / <a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a></li>
<li><strong>突發遠超預期的 surge</strong>（產品爆紅、病毒式擴散、結構性外部事件）→ <a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokémon GO</a>（產品爆紅、暫時）/ <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> / <a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a>（COVID 結構性永久）</li>
<li><strong>flash-sale 瞬間爆量</strong>（售票開賣、報名活動、限量搶購）→ <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（隱性緩衝）/ <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a>（明確排隊）/ <a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a>（規模化平台資料層）</li>
<li><strong>持續成長 sustained</strong>（用戶月增、業務擴張）→ <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> / <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> / <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 的容量規劃成本">9.C9 Spotify</a> / <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> / <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> / <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> / <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></li>
<li><strong>低延遲持續需求</strong>（金融交易、即時配對、廣告競價、ML inference）→ <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> / <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> / <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</a> / <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></li>
</ol>
<h3 id="surge-形狀的兩種次分類">surge 形狀的兩種次分類</h3>
<p>surge（突發遠超預期）內部還可分兩種、設計回應完全不同：</p>
<ul>
<li><strong>產品爆紅 surge</strong>（<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokémon GO</a>）：流量隨熱度消退、是「暫時偏離 baseline 又回歸」。容量規劃焦點是「撐過熱度高峰、避免在最忙時掛」。</li>
<li><strong>結構性 surge</strong>（<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom COVID</a>）：baseline 永久上移、是「新常態」。容量規劃焦點是「30x 後 SLO baseline 重新校準、長期成本曲線重算」。</li>
</ul>
<h3 id="flash-sale-spike-形狀的特殊性">flash-sale-spike 形狀的特殊性</h3>
<p>售票搶購 / 報名活動 / 限量搶購跟其他「峰值」案例有本質差異：</p>
<ul>
<li><strong>時間點精確、可秒級預測</strong>：開賣時刻 = 公告時刻、跟 GR8 Tech 的「賽事高潮」不一樣（賽事高潮在何時 + 多大都未知）</li>
<li><strong>持續時間極短</strong>：5-30 分鐘賣完、跟 Prime Day（48 小時）/ Hotstar IPL（4 小時）量級差很多</li>
<li><strong>峰值倍數極端</strong>：t=0 前流量近 0、t=0 瞬間衝到 10K-100K 倍、平均流量沒意義、只有峰值</li>
<li><strong>後端不容易跟上</strong>：高流量湧入時、付款 / 簽證 / 庫存後端通常是 legacy 系統、無法等比擴容、必須靠 buffer / queue / waiting room 解耦</li>
</ul>
<p>這個負載形狀的兩個主要設計模式：<strong>隱性緩衝</strong>（<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 模式</a>：用 DynamoDB / Kafka 吸收洪峰、後端慢消費）跟<strong>明確排隊</strong>（<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">SeatGeek 模式</a>：Virtual Waiting Room + token-based queue）。實務常見組合使用 — 入口先排隊、進入後仍用 buffer。</p>
<h2 id="案例覆蓋矩陣">案例覆蓋矩陣</h2>
<p>下表顯示 38 個案例在 <em>服務維度 × 雲商</em> 的覆蓋情況、空格代表待補。</p>
<table>
  <thead>
      <tr>
          <th>服務維度</th>
          <th>AWS</th>
          <th>GCP</th>
          <th>Azure</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB-OLTP</td>
          <td>C4, C14, C23</td>
          <td>C10</td>
          <td>C32</td>
      </tr>
      <tr>
          <td>DB-KV</td>
          <td>C5, C15, C18, C19, C20, C24, C26, C27, C29</td>
          <td>（待補）</td>
          <td>C11, C21</td>
      </tr>
      <tr>
          <td>DB-Document</td>
          <td>C36, C38</td>
          <td>C37</td>
          <td>（透過 C30 對照）</td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>C6, C25</td>
          <td>C35</td>
          <td>（待補）</td>
      </tr>
      <tr>
          <td>MQ-Stream</td>
          <td>C1 (SQS), C7 (Kinesis)</td>
          <td>C9</td>
          <td>（待補）</td>
      </tr>
      <tr>
          <td>Compute / K8s</td>
          <td>C2, C7, C12, C16, C19, C28</td>
          <td>C8, C34</td>
          <td>C33</td>
      </tr>
      <tr>
          <td>Global Edge</td>
          <td>C13</td>
          <td>（待補）</td>
          <td>（待補）</td>
      </tr>
      <tr>
          <td>Latency 敏感</td>
          <td>C3, C25, C36</td>
          <td>C10, C35</td>
          <td>（待補）</td>
      </tr>
      <tr>
          <td>Data Architecture</td>
          <td>C17</td>
          <td>C22, C31</td>
          <td>C30</td>
      </tr>
  </tbody>
</table>
<p>AWS 25 個 case、GCP 8 個 case（補了 130K-node GKE + Snap KeyDB + Forbes）、Azure 5 個 case。三家覆蓋更平衡。新增 DB-Document 維度後、MongoDB 作為主角的案例（C36 Coinbase / C37 Forbes / C38 Toyota Connected）跟原本 C30 Microsoft 365（MongoDB 遷出 → Cosmos DB）形成完整 document model 案例組。剩餘缺口：Azure cache / global edge / latency、GCP DB-KV / MQ-Stream 加深、GCP / Azure global edge。</p>
<h3 id="負載形狀--雲商-覆蓋">負載形狀 × 雲商 覆蓋</h3>
<table>
  <thead>
      <tr>
          <th>負載形狀</th>
          <th>AWS</th>
          <th>GCP</th>
          <th>Azure</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>predictable-peak</td>
          <td>C1, C13, C27, C29</td>
          <td>C22</td>
          <td>C21</td>
      </tr>
      <tr>
          <td>event-peak</td>
          <td>C2, C4, C7, C28</td>
          <td>-</td>
          <td>-</td>
      </tr>
      <tr>
          <td>surge</td>
          <td>C18</td>
          <td>C8</td>
          <td>C11</td>
      </tr>
      <tr>
          <td>flash-sale-spike</td>
          <td>C15, C16, C17</td>
          <td>-</td>
          <td>-</td>
      </tr>
      <tr>
          <td>low-latency-sustained</td>
          <td>C3, C12, C24, C25, C36</td>
          <td>C10, C34, C35</td>
          <td>-</td>
      </tr>
      <tr>
          <td>sustained-growth</td>
          <td>C5, C6, C14, C19, C20, C23, C26, C38</td>
          <td>C9, C31, C37</td>
          <td>C30, C32, C33</td>
      </tr>
  </tbody>
</table>
<p>flash-sale-spike 是 09 案例庫的核心 differentiator — 雲商案例庫對這個負載形狀的著墨遠勝一般 engineering blog。surge 維度補了 Zoom 之後、跟 Pokemon GO（暫時 surge）跟 Minecraft Earth（地理 surge）形成三種次分類對照。後續若有 GCP / Azure 同類售票案例可補。</p>
<h2 id="規劃中案例第二批">規劃中案例（第二批）</h2>
<p>待 09 主章寫作推進、第二批案例可從下列候選補齊。</p>
<table>
  <thead>
      <tr>
          <th>候選案例</th>
          <th>預期教學重點</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disney+ DynamoDB</td>
          <td>每日數十億動作、watch list metadata</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>PayPay 30 億訊息/日</td>
          <td>行動支付的持續高頻 message</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>Capcom DynamoDB</td>
          <td>遊戲業數十億請求、single-digit ms</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>Zomato 90% 延遲下降</td>
          <td>帳務處理、跨資料庫遷移效益</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>Zoom COVID 30x 成長</td>
          <td>1000 萬 → 3 億 DAU、突發長期 sustained</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>FanFight 100 萬寫入/秒</td>
          <td>印度 fantasy sports 體育博彩</td>
          <td><a href="https://aws.amazon.com/dynamodb/customers/">DynamoDB customers</a></td>
      </tr>
      <tr>
          <td>Tubi ScyllaDB → ElastiCache</td>
          <td>ML feature store sub-10ms p99</td>
          <td><a href="https://aws.amazon.com/elasticache/customers/">ElastiCache customers</a></td>
      </tr>
      <tr>
          <td>FanDuel 直播 + 投注</td>
          <td>雙重峰值對齊</td>
          <td><a href="https://aws.amazon.com/solutions/case-studies/fanduel-case-study/">FanDuel case study</a></td>
      </tr>
      <tr>
          <td>Blockchain.com Spanner</td>
          <td>Crypto 高頻交易、強一致全球</td>
          <td><a href="https://cloud.google.com/blog/products/databases/using-cloud-spanner-to-handle-high-throughput-writes/">Spanner blog</a></td>
      </tr>
      <tr>
          <td>Walmart Cosmos DB</td>
          <td>全球零售 KV、跨地區一致性策略</td>
          <td><a href="https://azure.microsoft.com/en-us/blog/azure-cosmos-db-pushing-the-frontier-of-globally-distributed-databases/">Cosmos DB blog</a></td>
      </tr>
      <tr>
          <td>Microsoft 365 Cosmos</td>
          <td>MongoDB → Cosmos 遷移、planet-scale 分析</td>
          <td><a href="https://azure.microsoft.com/en-us/blog/microsoft-365-boosts-usage-analytics-with-azure-cosmos-db/">Cosmos DB Microsoft 365 blog</a></td>
      </tr>
  </tbody>
</table>
<h2 id="engineering-blog-補充候選">Engineering Blog 補充候選</h2>
<p>當 AWS / GCP / Azure 案例缺乏某些工程紀律的深度（例如 chaos hypothesis、cell-based architecture 細節），補引 engineering blog 作為交叉驗證。候選來源：Shopify BFCM、Netflix Tech Blog、Amazon Builders&rsquo; Library、Google SRE Book、LinkedIn Engineering、Stripe Engineering、Cloudflare Blog、Discord Engineering、Uber Engineering、Pinterest Engineering 等。這層不另開資料夾、補在主章「案例對照」段。</p>
<h2 id="案例正文格式">案例正文格式</h2>
<p>每篇案例使用統一結構、方便快速比對。</p>
<ol>
<li><strong>觀察</strong>：客觀數字與事件序列。流量規模、實例配置、延遲分布、成本變化都用引用源的原始數字、不四捨五入。</li>
<li><strong>判讀</strong>：把案例的工程決策翻成主章的問題節點。</li>
<li><strong>策略</strong>：可重用的工程做法、去掉雲端 vendor 特異性。EKS、Auto Scaling、DynamoDB on-demand 等翻成跨平台等效概念。</li>
<li><strong>下一步路由</strong>：往哪個主章或前置案例延伸閱讀。</li>
<li><strong>引用源</strong>：雲端服務商官方 case study URL + 相關 Architecture Blog 連結。</li>
</ol>
<h2 id="tripwire">Tripwire</h2>
<ul>
<li>同一服務維度的 case 超過 5 個時、暫停擴張、改補其他維度。</li>
<li>AWS 案例數字過於行銷、缺工程細節 → 補 AWS Architecture Blog 同主題文章作為交叉驗證。</li>
<li>案例只是「我們用了 X 服務」、沒有具體量化結果 → 不收進案例庫、作為候選參考即可。</li>
<li>同一公司多個案例（例如 Coinbase 還有遷移案例）→ 拆 sub-case 而不是合成單一檔。</li>
<li>GCP / Azure 覆蓋持續落後 AWS 超過 2 倍時 → 主動補 GCP / Azure 案例、不要讓案例庫變成 AWS-only。</li>
</ul>
]]></content:encoded></item><item><title>Histogram</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/</guid><description>&lt;p>Histogram 的核心概念是「把觀測值分到多個 bucket，記錄每個範圍的累積數量」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 中描述分布的工具，常用來觀察 latency、request size、payload size、queue wait time 與處理耗時，支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> 計算。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Histogram 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 中描述分布的工具，跟 counter（計數）跟 gauge（瞬間值）互補。Average 只能說明中心趨勢；histogram 可以支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a>（p95 / p99）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 計算跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 判斷。&lt;/p>
&lt;p>Prometheus 的 histogram 用累積 bucket（&lt;code>le&lt;/code> label）實作 — 每個 bucket 記錄「值 &amp;lt;= le 的觀測次數」。PromQL 的 &lt;code>histogram_quantile()&lt;/code> 從 bucket 資料估算 percentile。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 histogram 的訊號是少數慢 request 會影響使用者體驗但 average 看不出來。Checkout 平均延遲 100ms 看起來良好，但 p99 若超過 3 秒，1% 的使用者體驗極差。Histogram 讓這個長尾可見。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Histogram bucket boundary 要依 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 閾值跟實際延遲範圍設計。Bucket 太粗（只有 100ms / 500ms / 1s）會讓 percentile 估計跳躍式變化；太細會增加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>（每個 bucket 是一條 time series）。常見做法是在 SLO 閾值附近密集、在兩端稀疏。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Histogram 的核心概念是「把觀測值分到多個 bucket，記錄每個範圍的累積數量」。它是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 中描述分布的工具，常用來觀察 latency、request size、payload size、queue wait time 與處理耗時，支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 計算。</p>
<h2 id="概念位置">概念位置</h2>
<p>Histogram 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 中描述分布的工具，跟 counter（計數）跟 gauge（瞬間值）互補。Average 只能說明中心趨勢；histogram 可以支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a>（p95 / p99）、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 計算跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 判斷。</p>
<p>Prometheus 的 histogram 用累積 bucket（<code>le</code> label）實作 — 每個 bucket 記錄「值 &lt;= le 的觀測次數」。PromQL 的 <code>histogram_quantile()</code> 從 bucket 資料估算 percentile。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 histogram 的訊號是少數慢 request 會影響使用者體驗但 average 看不出來。Checkout 平均延遲 100ms 看起來良好，但 p99 若超過 3 秒，1% 的使用者體驗極差。Histogram 讓這個長尾可見。</p>
<h2 id="設計責任">設計責任</h2>
<p>Histogram bucket boundary 要依 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 閾值跟實際延遲範圍設計。Bucket 太粗（只有 100ms / 500ms / 1s）會讓 percentile 估計跳躍式變化；太細會增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>（每個 bucket 是一條 time series）。常見做法是在 SLO 閾值附近密集、在兩端稀疏。詳見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>。</p>
]]></content:encoded></item><item><title>Percentile</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/</guid><description>&lt;p>Percentile 的核心概念是「某比例的觀測值低於某個門檻」。p95 latency 表示 95% 的 request 延遲低於該值；p99 觀察更長尾的慢請求。Percentile 描述的是分布的尾端，從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 資料計算而來，用來捕捉 average 掩蓋的使用者體驗問題。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Percentile 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 搭配使用。Histogram 記錄延遲分布（哪些 bucket 收到多少 request），percentile 從 histogram 資料計算（&lt;code>histogram_quantile&lt;/code> in PromQL）。Average latency 看不到長尾 — 平均 80ms 但 p99 是 2 秒，代表 1% 的使用者體驗極差。&lt;/p>
&lt;p>Percentile 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 的常見型別 — latency SLI 用「p99 &amp;lt; 500ms 的 request 佔比」量化使用者體驗。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 percentile 的訊號是 average latency 穩定但使用者仍回報卡頓。搜尋 API 平均 80ms、p99 2 秒，表示少數 request 走到慢查詢或下游 timeout。高流量服務的 1%（p99 以外）可能代表數千個使用者。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Percentile 要搭配 histogram bucket 設計 — bucket boundary 決定 percentile 計算的精度。Bucket 太少（只有 100ms / 500ms / 1s）會讓 p99 的估計跳躍式變化。Bucket 太多會增加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>。低流量服務的高 percentile 容易受少量樣本影響，alert 閾值要考慮統計穩定性。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Percentile 的核心概念是「某比例的觀測值低於某個門檻」。p95 latency 表示 95% 的 request 延遲低於該值；p99 觀察更長尾的慢請求。Percentile 描述的是分布的尾端，從 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 資料計算而來，用來捕捉 average 掩蓋的使用者體驗問題。</p>
<h2 id="概念位置">概念位置</h2>
<p>Percentile 跟 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 搭配使用。Histogram 記錄延遲分布（哪些 bucket 收到多少 request），percentile 從 histogram 資料計算（<code>histogram_quantile</code> in PromQL）。Average latency 看不到長尾 — 平均 80ms 但 p99 是 2 秒，代表 1% 的使用者體驗極差。</p>
<p>Percentile 是 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 的常見型別 — latency SLI 用「p99 &lt; 500ms 的 request 佔比」量化使用者體驗。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 percentile 的訊號是 average latency 穩定但使用者仍回報卡頓。搜尋 API 平均 80ms、p99 2 秒，表示少數 request 走到慢查詢或下游 timeout。高流量服務的 1%（p99 以外）可能代表數千個使用者。</p>
<h2 id="設計責任">設計責任</h2>
<p>Percentile 要搭配 histogram bucket 設計 — bucket boundary 決定 percentile 計算的精度。Bucket 太少（只有 100ms / 500ms / 1s）會讓 p99 的估計跳躍式變化。Bucket 太多會增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>。低流量服務的高 percentile 容易受少量樣本影響，alert 閾值要考慮統計穩定性。詳見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a> 跟 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 + 七問題決策樹</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/</guid><description>&lt;blockquote>
&lt;p>本文是 DB4 distributed SQL 選型的 &lt;em>entry point&lt;/em> deep article — 讀者進來時還沒決定哪個 vendor、甚至還沒釐清「我是不是該換 distributed SQL」。本文先用 &lt;em>撞牆訊號分型&lt;/em> 幫讀者識別自己屬哪條 driver path、再進三軸 vendor 對比、最後落到 team size + sizing 邊界檢查。配合 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> 閱讀。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼先講-driver-path不直接比-vendor">為什麼先講 driver path、不直接比 vendor&lt;/h2>
&lt;p>團隊評估「全球分散式 OLTP 三選一」時最常見的源頭錯誤：先比 vendor、再回頭問「我為什麼要 distributed SQL」。三家 vendor 文件都說「跨 region 強一致 SQL」、看不出實際取捨；做錯選擇後遷移成本極高。&lt;/p>
&lt;p>正確順序應該反過來：先識別 &lt;em>自己為什麼要評估 distributed SQL&lt;/em>、再進 vendor 比較。三條 driver path 各自的訊號、適配 vendor、決策路徑都不同 — 不識別 driver path 直接比 vendor 是源頭錯誤。&lt;/p>
&lt;p>讀者進來最常問的問題（多數會問錯順序）：&lt;/p>
&lt;ul>
&lt;li>我是不是真該換 distributed SQL、還是 Aurora / Cloud SQL 還能撐？&lt;/li>
&lt;li>Spanner 在 Google 跑了 10 年、CockroachDB 跟 DSQL 比較新、成熟度差多少？&lt;/li>
&lt;li>我有 PostgreSQL 應用、三家相容性差在哪？&lt;/li>
&lt;li>跨雲是硬需求還是被 fear 推的？&lt;/li>
&lt;li>DSQL 2024 才 GA、production 風險多大？&lt;/li>
&lt;li>我團隊 50 人能不能養 self-managed CockroachDB？&lt;/li>
&lt;li>Spanner 100 pu 起跳對我中小 PG workload 划算嗎？&lt;/li>
&lt;/ul>
&lt;p>7 題本文都會回答、但先回答「你是哪條 driver path」這個前置問題 0。&lt;/p>
&lt;h3 id="三條-driver-path-的-case-anchor">三條 driver path 的 case anchor&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash&lt;/a>：Aurora Postgres 1.636 M QPS single-primary 撞牆 → 換 multi-primary、PostgreSQL wire 相容降低遷移阻力（F4.1 / F4.2 / F4.4）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a>：Cassandra eventual consistency 撐不住 transactional → 補 distributed SQL、self-managed 380+ cluster + Database Platform Team（F4.6 / F4.9）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a>：Wire Act 合規驅動 + 50 人 tech team + Outposts 混合部署（F4.10 / F4.14）&lt;/li>
&lt;/ul>
&lt;p>對照 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner planetary scale&lt;/a> 提供 Spanner ground truth（含 sizing barrier、F3.16）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 提供 Aurora 受監管金融的另一條路徑、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings Aurora financial ledger&lt;/a> 提供 Aurora 內 business sharding 路徑（不換引擎）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 DB4 distributed SQL 選型的 <em>entry point</em> deep article — 讀者進來時還沒決定哪個 vendor、甚至還沒釐清「我是不是該換 distributed SQL」。本文先用 <em>撞牆訊號分型</em> 幫讀者識別自己屬哪條 driver path、再進三軸 vendor 對比、最後落到 team size + sizing 邊界檢查。配合 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> + <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 閱讀。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<hr>
<h2 id="為什麼先講-driver-path不直接比-vendor">為什麼先講 driver path、不直接比 vendor</h2>
<p>團隊評估「全球分散式 OLTP 三選一」時最常見的源頭錯誤：先比 vendor、再回頭問「我為什麼要 distributed SQL」。三家 vendor 文件都說「跨 region 強一致 SQL」、看不出實際取捨；做錯選擇後遷移成本極高。</p>
<p>正確順序應該反過來：先識別 <em>自己為什麼要評估 distributed SQL</em>、再進 vendor 比較。三條 driver path 各自的訊號、適配 vendor、決策路徑都不同 — 不識別 driver path 直接比 vendor 是源頭錯誤。</p>
<p>讀者進來最常問的問題（多數會問錯順序）：</p>
<ul>
<li>我是不是真該換 distributed SQL、還是 Aurora / Cloud SQL 還能撐？</li>
<li>Spanner 在 Google 跑了 10 年、CockroachDB 跟 DSQL 比較新、成熟度差多少？</li>
<li>我有 PostgreSQL 應用、三家相容性差在哪？</li>
<li>跨雲是硬需求還是被 fear 推的？</li>
<li>DSQL 2024 才 GA、production 風險多大？</li>
<li>我團隊 50 人能不能養 self-managed CockroachDB？</li>
<li>Spanner 100 pu 起跳對我中小 PG workload 划算嗎？</li>
</ul>
<p>7 題本文都會回答、但先回答「你是哪條 driver path」這個前置問題 0。</p>
<h3 id="三條-driver-path-的-case-anchor">三條 driver path 的 case anchor</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>：Aurora Postgres 1.636 M QPS single-primary 撞牆 → 換 multi-primary、PostgreSQL wire 相容降低遷移阻力（F4.1 / F4.2 / F4.4）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>：Cassandra eventual consistency 撐不住 transactional → 補 distributed SQL、self-managed 380+ cluster + Database Platform Team（F4.6 / F4.9）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>：Wire Act 合規驅動 + 50 人 tech team + Outposts 混合部署（F4.10 / F4.14）</li>
</ul>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner planetary scale</a> 提供 Spanner ground truth（含 sizing barrier、F3.16）、<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 提供 Aurora 受監管金融的另一條路徑、<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora financial ledger</a> 提供 Aurora 內 business sharding 路徑（不換引擎）。</p>
<h2 id="撞牆訊號分型你的-driver-path-是哪一條前置問題-0f4-frame-1">撞牆訊號分型：你的 driver path 是哪一條（前置問題 0、F4 Frame 1）</h2>
<p>讀者進來前先回答：你 <em>為什麼</em> 要評估 distributed SQL？三條 driver path 各自的訊號、適配 vendor、決策路徑都不同。</p>
<h3 id="path-a--single-primary-寫入撞牆9c39-doordash-路徑f42--f46">Path A — single-primary 寫入撞牆（9.C39 DoorDash 路徑、F4.2 + F4.6）</h3>
<p>訊號：</p>
<ul>
<li>寫入量持續成長、Aurora / RDS / Cloud SQL primary CPU + WAL flush rate 接近上限</li>
<li>轉折點 <em>不是 IOPS、是 primary CPU + WAL flush rate</em>（F4.2、DoorDash 策略段 1）</li>
<li>已嘗試 vertical scale primary、撞 instance ceiling</li>
</ul>
<p>DoorDash concrete reference：2020-04-17 高峰 &gt; 1.636 M QPS、multi-hour outage（觀察段表格）。<strong>Scope warning（F4.1、case 自帶警示）</strong>：1.636 M QPS 是 <em>Aurora 撞牆的痛點</em> — 不是「CockroachDB throughput claim」、case 沒揭露遷移後單一 CockroachDB cluster 的峰值、只說「跑更多 cluster、alert volume 反而下降」。</p>
<p>適配 vendor：CockroachDB / Aurora DSQL / Spanner 都解、選擇看其他軸。</p>
<h3 id="path-b--eventual-consistency-缺口9c40-netflix-路徑f46">Path B — eventual consistency 缺口（9.C40 Netflix 路徑、F4.6）</h3>
<p>訊號：原本用 Cassandra / Riak / DynamoDB eventual consistency、遇到 <em>5 條件並存</em> 需求：</p>
<ol>
<li>multi-active topology（多 region 都可寫）</li>
<li>global consistent secondary index（跨 region 一致的二級索引）</li>
<li>global transaction（跨 row / 跨 region 的 ACID）</li>
<li>open source</li>
<li>SQL</li>
</ol>
<p>Cassandra 在 transactional 場景下 <em>湊不齊</em> 這五項。Netflix 2019 評估後選 CockroachDB（5 條件 case 直接列出、判讀段 1）。具體場景：Studio Cloud Drive（強一致 metadata + 全球可寫）、Open Connect 控制平面、Spinnaker（持續交付）、Maestro（ML / 資料 workflow）、Gaming 控制平面。</p>
<p>適配 vendor：CockroachDB（open source + SQL 兩條件硬卡）、Spanner（若 GCP-only 可放鬆 open source 要求）。</p>
<h3 id="path-c--合規驅動的地理邊界--跨-boundary-業務邏輯需求9c41-hard-rock-路徑f410">Path C — 合規驅動的地理邊界 + 跨 boundary 業務邏輯需求（9.C41 Hard Rock 路徑、F4.10）</h3>
<p>訊號：</p>
<ul>
<li>法規要求資料留某地理邊界（Wire Act 跨州、GDPR 跨國、各州博彩牌照）</li>
<li><em>同時</em> 業務邏輯需要跨 boundary（跨州統一帳戶 / 跨州 reporting / 欺詐偵測）</li>
</ul>
<p>Hard Rock concrete reference：跨 8 州（AZ / IN / TN / FL / OH / IL / NJ / VA）+ AWS Outposts + 邏輯一個 cluster（觀察段表格）。詳細 schema 配置見 <a href="../locality-aware-schema/">locality-aware schema</a>。</p>
<p>適配 vendor：CockroachDB（locality + placement + Outposts）、Spanner（GCP region 內 placement、無 Outposts 等效）、Aurora DSQL 跨 region 強一致但 Outpost 部署現階段未完整覆蓋。</p>
<h3 id="不該換-distributed-sql-的訊號">不該換 distributed SQL 的訊號</h3>
<ul>
<li>single-region OLTP 已足夠</li>
<li>寫入量未撞 single-primary 天花板（Aurora db.r6g.16xlarge 還沒滿）</li>
<li>無跨 region 業務需求</li>
<li>無跨 boundary 合規需求</li>
</ul>
<p>→ PostgreSQL / Aurora 足夠、distributed SQL overhead（寫入 2-5x latency、ops 複雜度）不划算。對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 走 Aurora + application sharding 的路徑、不換引擎也能解單主寫入瓶頸。</p>
<blockquote>
<p><strong>數字口徑</strong>：本段「2-5x latency」屬通用工程估算（Raft / Paxos round trip 跟 single-leader replication 的 latency ratio）、case 未直接揭露對照數字、實際值依拓樸 / 寫入大小 / 一致性層次而異、應該以自家 benchmark 驗證。</p></blockquote>
<h2 id="核心機制三軸-vendor-對比">核心機制：三軸 vendor 對比</h2>
<p>完成 driver path 識別後、進三軸 vendor 對比。</p>
<h3 id="軸-1--部署-topology">軸 1 — 部署 topology</h3>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>部署</th>
          <th>何時是硬條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CockroachDB</td>
          <td>cross-cloud + on-prem + Cockroach Cloud</td>
          <td>跨雲 / on-prem hybrid 必要時</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>GCP-only</td>
          <td>不適合非 GCP 環境</td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>AWS-only</td>
          <td>不適合非 AWS 環境</td>
      </tr>
  </tbody>
</table>
<p>Path C 場景（Hard Rock Outposts hybrid）強制走 CockroachDB — 另兩家不提供等效部署。</p>
<h3 id="軸-2--managed-成熟度">軸 2 — Managed 成熟度</h3>
<p><strong>Scope warning（來源分層）</strong>：3 case 都沒揭露成熟度比對、本軸依 case + vendor 公開文件 + 外部知識合成：</p>
<ul>
<li><strong>Spanner</strong>：10+ 年 Google 內部 + 外部 GA（依 9.C10 case + Google research paper、屬 vendor 公開文件 + dogfood frame）</li>
<li><strong>CockroachDB</strong>：自管 + Cockroach Cloud（managed 較新、依 Cockroach Labs 公告）</li>
<li><strong>Aurora DSQL</strong>：2024-05 GA（依 AWS 公告）</li>
</ul>
<p>引用紀律：「Spanner 10+ 年」是 vendor 公開 + Google dogfood 的合成、不是 case 直接揭露的 production stability 數字。Aurora DSQL「2024-05 GA」屬 AWS 公開公告、production case ground truth 還在累積。引用時要明示來源層次。</p>
<h3 id="軸-3--sql-相容性">軸 3 — SQL 相容性</h3>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>SQL</th>
          <th>相容程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CockroachDB</td>
          <td>PostgreSQL wire protocol</td>
          <td><em>protocol-level</em> 相容、SQL 行為要 audit</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>GoogleSQL + 部分 PostgreSQL 方言</td>
          <td>GoogleSQL native、PG 方言是子集</td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>PostgreSQL（AWS managed control plane）</td>
          <td>PostgreSQL-compatible、AWS 操作模型</td>
      </tr>
  </tbody>
</table>
<h3 id="postgresql-相容性-audit-checklist-4-項f44doordash-揭露">PostgreSQL 相容性 audit checklist 4 項（F4.4、DoorDash 揭露）</h3>
<p>DoorDash case 揭露 PG wire <em>protocol-level</em> 相容、SQL 行為「仍要驗證」。把這個警語展開成 audit checklist：</p>
<ol>
<li><strong>Serializable default</strong>：CockroachDB default SERIALIZABLE、PG default READ COMMITTED → application transaction 行為差異（細節見 <a href="../transaction-retry-pattern/">transaction retry pattern</a>）。Aurora DSQL 預設行為要看 AWS 公告。</li>
<li><strong>Retry semantics</strong>：CockroachDB 發 <code>40001 serialization_failure</code>、application 必須包 retry loop。PG / Aurora 預設不需要、application 沒 retry middleware。Aurora DSQL 比照 CockroachDB 模型、需要 retry loop。</li>
<li><strong>Partial index</strong>：CockroachDB 支援程度與 PG 有差異、application 用到的 partial index 要逐一驗證。Spanner GoogleSQL 跟 PG 行為不同。</li>
<li><strong>其他 SQL 行為</strong>：sequence、auto-increment、stored procedure、custom function、extension 等都需 case-by-case audit。</li>
</ol>
<p>引用紀律：DoorDash 揭露的是「PG wire protocol-level 相容、SQL 行為要 audit」這個 fact、本章把 audit 內容展開成 4 項屬通用工程議題、不是 DoorDash case 直接揭露。</p>
<h3 id="consensus-機制差">Consensus 機制差</h3>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>共識</th>
          <th>硬體依賴</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CockroachDB</td>
          <td><a href="/blog/backend/knowledge-cards/hybrid-logical-clock/" data-link-title="Hybrid Logical Clock" data-link-desc="用 physical wall clock &#43; monotonic logical counter 給每個事件 timestamp、靠軟體 max-offset 保證跨節點時鐘差不超過上限、超過 panic 保護一致性">Hybrid Logical Clock</a> + Raft</td>
          <td>純軟體 + NTP</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>TrueTime + Paxos</td>
          <td>GPS + atomic clock</td>
      </tr>
      <tr>
          <td>Aurora DSQL</td>
          <td>類 Spanner 概念、AWS 專屬</td>
          <td>AWS timing infra（未完全公開）</td>
      </tr>
  </tbody>
</table>
<p>三家共識機制的差異直接決定 <a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">external consistency</a> 的實作路徑：Spanner 用 TrueTime + commit-wait 撐 external consistency；CockroachDB 用 HLC + max-offset 撐 linearizability、不保證 external consistency；Aurora DSQL 走類 Spanner 路徑但細節未完全公開。三家 multi-region 配置都吃 <a href="/blog/backend/knowledge-cards/cross-region-quorum/" data-link-title="Cross-Region Quorum" data-link-desc="multi-region distributed SQL 強制 voting replica 跨 region、commit 等多 region quorum ack、跨洲 RTT 物理硬限">Cross-Region Quorum</a> 的物理 latency tax。詳細機制見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>。</p>
<h3 id="pricing-model-差">Pricing model 差</h3>
<ul>
<li><strong>CockroachDB self-managed</strong>：node × resource、cluster 至少 3 node</li>
<li><strong>Cockroach Cloud / Spanner / DSQL</strong>：consumption-based（read / write / storage / network）</li>
</ul>
<h3 id="sizing-barrier-邊界f3169c10-spanner-case-揭露">Sizing barrier 邊界（F3.16、9.C10 Spanner case 揭露）</h3>
<p>Spanner 100 processing unit 起跳是 <em>最小 footprint</em> — 對中小 PostgreSQL workload 是 cost 邊界：</p>
<ul>
<li>workload 月寫入若只夠 PG db.m6g.large 級別、付 Spanner 100 pu 起跳 cost 不對</li>
<li>CockroachDB 最小 3 node、storage / compute 線性 — 中小 workload 較友善</li>
<li>Aurora DSQL consumption-based 無 minimum、中小 workload 最友善（但 production case 累積較少）</li>
</ul>
<p>判讀：sizing barrier 是 <em>vendor 強制最小 footprint</em>、不是「啟動成本」— 即使 workload 縮小、minimum 不會降。中小 PG workload 直接套 Spanner = 付不必要的 minimum cost。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a>、<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡</a>、<a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in 卡</a>。</p>
<h2 id="決策樹七問題">決策樹：七問題</h2>
<p>前置問題 0 在 <em>撞牆訊號分型</em> 段已回答（你的 driver path 是 A / B / C 哪一條）。以下進三家 vendor 對比的七個問題。</p>
<h3 id="問題-1是否硬需求跨雲--on-prem">問題 1：是否硬需求跨雲 / on-prem？</h3>
<ul>
<li><strong>Yes</strong> → CockroachDB（唯一選項；對應 9.C40 Netflix 跨 AWS region、9.C41 Hard Rock AWS Outposts 混合）</li>
<li><strong>No</strong> → 進問題 2</li>
</ul>
<p>跨雲是 <em>硬需求</em> 而不是 <em>fear-driven</em> 訊號：</p>
<ul>
<li>真硬需求：法規明文跨雲、acquisition 後多雲整合、vendor risk 政策強制</li>
<li>fear-driven：「萬一 AWS 全球 outage」（多數公司實際走 single-cloud、跨雲 portability premium 卻沒實際 multi-cloud 部署）</li>
</ul>
<blockquote>
<p><strong>數字口徑</strong>：本段「多數公司 single-cloud」屬通用工程估算、case 未揭露明確比例、實際分佈依產業 / 監管 / 規模而異。判斷自己是否需要跨雲時、看具體規範跟 risk 條款、不直接套通用比例。</p></blockquote>
<h3 id="問題-2已在-aws-還是-gcp-還是中立">問題 2：已在 AWS 還是 GCP 還是中立？</h3>
<ul>
<li><strong>AWS 深</strong> → Aurora DSQL（操作模型對齊、PostgreSQL 相容）</li>
<li><strong>GCP 深</strong> → Spanner（10 年成熟、Google 內部驗證）</li>
<li><strong>中立 / 多雲</strong> → CockroachDB（可 portable）</li>
</ul>
<p>雲商生態深度判讀：IAM / VPC / monitoring / cost mgmt 已深度整合 AWS → Aurora DSQL 整合阻力低；同樣道理 GCP → Spanner。</p>
<h3 id="問題-3production-風險預算">問題 3：production 風險預算？</h3>
<ul>
<li><strong>低</strong>（金融 / 醫療）→ Spanner（最成熟）或 CockroachDB（&gt;5 年外部 production case）</li>
<li><strong>中</strong> → 三者皆可</li>
<li><strong>高</strong>（願意當 early adopter）→ Aurora DSQL（2024 GA）</li>
</ul>
<p>風險預算對應的不是「會不會掛」、是「邊界 case 文件成熟度 + production troubleshooting case 量」。Aurora DSQL 2024 GA、production case 累積中、邊界 case 仍在被發現。</p>
<h3 id="問題-4postgresql-相容性是-hard-requirement">問題 4：PostgreSQL 相容性是 hard requirement？</h3>
<ul>
<li><strong>Yes</strong>（既有 application）→ CockroachDB 或 Aurora DSQL（兩者都做 PG 相容、但走 audit checklist 驗證 SQL 行為）</li>
<li><strong>No</strong> → Spanner（GoogleSQL 也可）</li>
</ul>
<p>PG hard requirement 訊號：application 用 PostgreSQL-specific feature（partial index、JSONB operator、PostGIS、PG extension 生態）、ORM / driver 深度綁 PostgreSQL wire。</p>
<h3 id="問題-5管理負擔誰承擔">問題 5：管理負擔誰承擔？</h3>
<ul>
<li><strong>自管</strong> → CockroachDB（唯一可自管）</li>
<li><strong>Managed</strong> → 都行、依雲商生態</li>
</ul>
<p>自管 vs managed 不只是「省人月」、是「邊界 case 出現時誰修」— managed 的 vendor 負責、自管的自己負責。</p>
<h3 id="問題-6team-size-是否撐得起-self-managedf4149c41-hard-rock--9c40-netflix-揭露">問題 6：team size 是否撐得起 self-managed（F4.14、9.C41 Hard Rock + 9.C40 Netflix 揭露）</h3>
<p>distributed SQL 的 ops 槓桿來自系統內建 Raft / placement 把「DBA 養單區、跨區 sync 養運維」工作量壓進系統內。</p>
<p>Hard Rock 50 人 tech team 估「若用 PostgreSQL 需多加 10-20 工程師」（觀察段表格 + 策略段 4）。<strong>Case 自帶警示</strong>：「省了 10-20 工程師」是 <em>機會成本</em>（沒招那麼多 DBA）、<em>不是</em> 節省支出（已 hire 後解雇）。引用必須明示口徑：</p>
<ul>
<li>正確：「distributed SQL 對小團隊的 ops 槓桿 = 不必招那麼多 DBA」</li>
<li>錯誤：「上 CockroachDB 可裁員」、「節省人月支出」</li>
</ul>
<p>Self-managed 規模化的另一極：Netflix 養 380+ cluster 需要 <em>專屬 Database Platform Team</em>（含 backup / upgrade / incident response / capacity review、F4.9）。沒這量級團隊直接 self-host 大規模 cluster 是 ops 自殺、Cockroach Cloud 才是合理路徑。判讀訊號：「self-managed cluster 數量 vs 平台團隊規模」轉折點 case 沒講具體閾值、引用時不可宣稱閾值、但方向清楚：</p>
<ul>
<li>team size 小（&lt; 100 人 tech team、無專屬 DB platform team）→ Cockroach Cloud / Spanner / DSQL（managed）優先</li>
<li>team size 大 + 有專屬 DB platform team → self-managed CockroachDB 可考慮</li>
<li>team size 中等但要 self-host 大規模 cluster → 評估專屬 platform team 投資後再決定</li>
</ul>
<h3 id="問題-7sizing-是否撐得起-vendor-minimumf316">問題 7：sizing 是否撐得起 vendor minimum（F3.16）</h3>
<ul>
<li>Spanner 100 processing unit 起跳對中小 PG workload 是成本門檻、月寫入 &lt; 某 baseline 時付 Spanner 起跳費不划算</li>
<li>中小 workload 但需 multi-region 強一致 → CockroachDB 3 node 起 / Aurora DSQL consumption-based 較友善</li>
<li>大 workload（已過 single-primary 撞牆訊號）→ 三家皆可、進問題 1-6 再篩</li>
</ul>
<h2 id="cluster-boundary-顆粒per-app-cluster-vs-邏輯一個-clustercockroachdb-cluster-boundary-ssot">Cluster boundary 顆粒：per-app cluster vs 邏輯一個 cluster（CockroachDB cluster boundary SSoT）</h2>
<blockquote>
<p><strong>位置標</strong>：本段是 _module-outline.md Section G「CockroachDB cluster boundary 顆粒」的 SSoT 主寫段、是 <em>已選 CockroachDB 後</em> 的拓樸決策（跟前面七問題 vendor 選擇分流）。其他 vendor cluster boundary 議題不在本段重複展開 — Aurora fleet 治理（business sharding / 200 cluster 模式）見 <a href="../../aurora/read-replica-scaling/">aurora/read-replica-scaling</a>、MongoDB blast radius 切多 cluster（Toyota 20 DB 模式）見 <a href="../../mongodb/shard-key-selection/">mongodb/shard-key-selection</a>。</p></blockquote>
<p>選完 vendor 還有一個正交的拓樸決策：CockroachDB cluster 的「顆粒」要切多細。一個微服務一個 cluster（per-app）、還是多個微服務共用一個邏輯 cluster（shared / 邏輯一個 cluster）。這條軸的判讀獨立於跨雲 / 風險預算 / 管理負擔等七問題、是 <em>cluster 拓樸</em> 議題、不是 vendor 選擇議題。判讀核心是 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 的取捨 — 是把故障半徑限縮在單服務（per-app）、還是接受邏輯 cluster 內事故跨業務影響但換 transactional cross-domain 能力（邏輯一個 cluster）。本段是 CockroachDB cluster boundary 顆粒的主寫位置、其他 sibling 文章（<a href="../hlc-raft-consensus/">hlc-raft-consensus</a>、<a href="../survival-goals/">survival-goals</a>、<a href="../locality-aware-schema/">locality-aware-schema</a>）cross-link 不重複展開。</p>
<h3 id="per-app-clusternetflix-380-路徑f47-揭露">Per-app cluster（Netflix 380+ 路徑、F4.7 揭露）</h3>
<p>每個微服務 / 每個業務邊界各自獨立 cluster。Netflix 揭露的具體形貌：380+ cluster、每個 cluster 規模小（屬「artery of small DBs」哲學、不是巨型 DB）、每個服務 own 自己的 schema 跟容量。</p>
<p>判讀訊號：</p>
<ul>
<li>服務之間資料 <em>硬隔離</em>（compliance / blast radius / 不同 SLA tier）— 共用 cluster 一旦 schema migration / hot range 出事、影響面跨服務</li>
<li>跨服務 query 需求低（沒有 cross-domain JOIN 場景）</li>
<li>容量規劃可以 per-cluster（每個服務自己估、不需共池）</li>
<li>有專屬 Database Platform Team 養 cluster lifecycle（backup / upgrade / incident response / capacity review、F4.9）— ops surface area 隨 cluster 數 <em>線性成長</em></li>
</ul>
<p>代價：ops surface area 大、每個 cluster 都要獨立 upgrade / monitoring / capacity review。沒這量級平台團隊直接 self-host 380 cluster 是 ops 自殺。</p>
<h3 id="邏輯一個-clusterhard-rock-路徑f410-揭露">邏輯一個 cluster（Hard Rock 路徑、F4.10 揭露）</h3>
<p>業務邏輯上是 <em>一個</em> CockroachDB cluster、物理上跨多地理 placement（locality + replication zone 把 range 釘到特定 region / AZ / Outpost）。Hard Rock 揭露的具體形貌：跨 8 州 + AWS Outposts、邏輯一個 cluster、跨州統一帳戶 / 跨州 reporting / 欺詐偵測在同一 cluster 內做 transactional query。</p>
<p>判讀訊號：</p>
<ul>
<li>跨服務 / 跨地理需要 <em>transactional</em> query（跨州統一帳戶、跨業務統合 reporting）— 拆獨立 cluster 會破壞業務邏輯</li>
<li>合規顆粒 <em>細</em> 到 region / 州 / AZ、但 <em>不要求</em> 完全隔離 cluster（Wire Act 要求州內運算、但允許跨州 application 邏輯）</li>
<li>Team size 中小（Hard Rock 50 人 tech team）、ops surface area 集中比攤平好管</li>
<li>容量規劃集中、跨服務資源共享（不同服務的 range 可以 colocate 同 cluster）</li>
</ul>
<p>代價：cluster 內複雜度高（要設計 placement / locality / replication zone 把 range 釘對地方）、blast radius 是 <em>整個邏輯 cluster</em>、cluster 級事故影響跨業務。</p>
<h3 id="兩條路徑的判讀軸">兩條路徑的判讀軸</h3>
<table>
  <thead>
      <tr>
          <th>判讀軸</th>
          <th>Per-app cluster（Netflix）</th>
          <th>邏輯一個 cluster（Hard Rock）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務隔離度</td>
          <td>硬隔離（不同 SLA / compliance tier）</td>
          <td>弱隔離（同業務域、共用 placement 策略）</td>
      </tr>
      <tr>
          <td>跨服務 query 需求</td>
          <td>低</td>
          <td>高（transactional cross-domain）</td>
      </tr>
      <tr>
          <td>Blast radius</td>
          <td>限縮在單服務</td>
          <td>整個邏輯 cluster</td>
      </tr>
      <tr>
          <td>Ops surface area</td>
          <td>線性成長（每 cluster 獨立 lifecycle）</td>
          <td>集中但複雜度高（cluster 內 placement）</td>
      </tr>
      <tr>
          <td>容量規劃顆粒</td>
          <td>Per-cluster 獨立估</td>
          <td>集中估、跨服務共池</td>
      </tr>
      <tr>
          <td>平台團隊要求</td>
          <td>高（cluster 數越多越剛性）</td>
          <td>中（cluster 數少但 placement 複雜度高）</td>
      </tr>
  </tbody>
</table>
<p>判讀順序：先問「跨服務 query 需要 transactional 嗎」— Yes 偏邏輯一個 cluster、No 進下一條；再問「服務之間 SLA / compliance 是否硬隔離」— Yes 偏 per-app、No 看 team / ops 槓桿。</p>
<h3 id="跟-aurora-fleet-治理的本質差異">跟 Aurora fleet 治理的本質差異</h3>
<p>Aurora <a href="../../aurora/read-replica-scaling/">fleet 治理 SSoT</a>（read-replica-scaling 邊界段）展開的是 <em>Aurora cluster 之間</em> 怎麼拆（business sharding / blast radius / read fanout），cluster 是 single-primary 抽象、拆 cluster 是 <em>繞過</em> single-primary 上限。</p>
<p>CockroachDB cluster boundary 的問題不一樣 — CockroachDB 本身就是 distributed、單 cluster 內可橫向擴展、cluster boundary 是 <em>業務 / 合規 / blast radius 邊界</em>、不是繞 single-primary。</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>Aurora fleet</th>
          <th>CockroachDB cluster boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>拆 cluster 動機</td>
          <td>繞過 single-primary 寫入上限</td>
          <td>隔離 blast radius / 合規邊界 / 平台分權</td>
      </tr>
      <tr>
          <td>單 cluster 上限</td>
          <td>寫入 capacity（single-primary）</td>
          <td>範圍大（distributed、Raft 內擴）</td>
      </tr>
      <tr>
          <td>跨 cluster query</td>
          <td>應用層拼（無 transactional 保證）</td>
          <td>一樣應用層拼（除非邏輯一個 cluster）</td>
      </tr>
      <tr>
          <td>典型形貌</td>
          <td>DraftKings 200 cluster（business sharding）</td>
          <td>Netflix 380+（per-app）/ Hard Rock 1（logical）</td>
      </tr>
  </tbody>
</table>
<p>兩條路徑的 <em>拆與不拆</em> 動機本質不同。Aurora 拆是 <em>被迫</em>（單 cluster 撐不住）、CockroachDB 拆是 <em>選擇</em>（單 cluster 撐得住、拆是為了治理）。</p>
<h3 id="跨-vendor-路徑對照">跨 vendor 路徑對照</h3>
<ul>
<li><strong>Aurora fleet</strong>（DraftKings 200 cluster）— business sharding 繞 single-primary 上限、每 cluster 仍可多 service、平均負載低（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 case</a> 揭露單 cluster ~80 ops/sec、200 cluster 加總 17K ops/sec）</li>
<li><strong>CockroachDB per-app</strong>（Netflix 380+）— 微服務級拆 cluster、artery of small DBs、需要專屬 Database Platform Team；單 cluster 內 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> + <a href="/blog/backend/knowledge-cards/leaseholder/" data-link-title="Leaseholder" data-link-desc="分散式 SQL 每個 range 在任一時間點的 read / write entry point、通常等於 Raft leader、承擔該 range 的 coordination">Leaseholder</a> 負責內部 scaling</li>
<li><strong>CockroachDB 邏輯一個</strong>（Hard Rock）— 跨地理單一 cluster、locality + placement 撐合規 + transactional 跨域、本地化讀靠 <a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">Follower Read</a> 降低跨 region cost</li>
<li><strong>CockroachDB fleet per-jurisdiction</strong>（Standard Chartered）— 每監管市場一個 cluster、合規 <em>禁止</em> 跨市場資料流動時的 forced pattern、跟 Hard Rock 對照（合規顆粒粗到要拆 vs 細到能用 placement）</li>
</ul>
<p>進階閱讀：合規驅動的 cluster boundary 選擇見 <a href="../locality-aware-schema/">locality-aware-schema</a>；單 cluster 容量規劃見 <a href="../hlc-raft-consensus/">hlc-raft-consensus</a> 容量與觀測段。</p>
<h2 id="失敗模式常見錯配">失敗模式：常見錯配</h2>
<h3 id="過度-fear-aws--gcp-lock-in">過度 fear AWS / GCP lock-in</h3>
<p>承接 <em>問題 1：是否硬需求跨雲</em> 段的 fear-driven 訊號（多數場景單雲、跨雲是想像中需求）— 把 fear 當硬需求選 CockroachDB，付 portability premium（自管 ops + Cockroach Cloud 較新）卻沒實際 multi-cloud 部署，結果付的是 lock-in 保險、實際沒用上。</p>
<p>判讀：跨雲訊號要 <em>具體場景</em>（acquisition 後整合 / 法規明文 / vendor risk 政策強制）、不是 fear。</p>
<h3 id="低估-dsql-成熟度風險">低估 DSQL 成熟度風險</h3>
<p>2024-05 GA、production case 少、邊界 case 文件不全 — early adopter 才適合。production 風險預算低的場景（金融 / 醫療 / 合規嚴格）不應該選最新 GA 的服務。</p>
<h3 id="spanner-假設-postgresql-全相容">Spanner 假設 PostgreSQL 全相容</h3>
<p>Spanner PostgreSQL interface 是 <em>子集</em>、部分 PostgreSQL feature 不支援。應用 migration 仍需 audit、不可直接 lift-and-shift。</p>
<h3 id="self-managed-cockroachdb-低估-ops-cost9c40-netflix-concrete-referencef49">Self-managed CockroachDB 低估 ops cost（9.C40 Netflix concrete reference、F4.9）</h3>
<p>Raft / backup / upgrade / monitoring 自管比 PostgreSQL 複雜、DBA bandwidth 沒到位變 disaster。Netflix 養 380+ cluster 需要 <em>專屬 Database Platform Team</em> — 含 backup、upgrade、incident response、capacity review。</p>
<p>判讀訊號：「self-managed cluster 數量 vs 平台團隊規模」轉折點 case 沒講具體閾值、引用時不可宣稱閾值、但方向清楚 — 小規模 self-managed 不需要、大規模一定需要、之間有 grey zone 要實際評估團隊能力。</p>
<h3 id="用-distributed-sql-解-single-region-oltp">用 distributed SQL 解 single-region OLTP</h3>
<p>90% 場景 PostgreSQL / Aurora 夠用、distributed SQL overhead 是 2-5x latency（Raft round trip 額外成本）。沒撞 single-primary 寫入上限的情況下、上 distributed SQL 是付不必要的 latency premium。</p>
<h3 id="合規邊界誤判">合規邊界誤判</h3>
<p>受監管市場可能 <em>不能</em> 用任何跨境 distributed SQL（Standard Chartered 模式）、要拆每市場獨立 cluster。反過來、合規顆粒小（跨州 vs 跨國）+ 跨 boundary 業務邏輯需求高（跨州統一帳戶）時、Standard Chartered fleet 拓樸不適合、需走 Hard Rock locality + placement 路徑（細節見 <a href="../locality-aware-schema/">locality-aware schema</a>）。</p>
<h3 id="sizing-barrier-誤判f316">Sizing barrier 誤判（F3.16）</h3>
<p>中小 PG workload 直接套 Spanner 100 pu 起跳、付的是不必要的 minimum cost。中小規模的硬一致 multi-region workload、CockroachDB 3 node / Aurora DSQL consumption-based 更划算。</p>
<h3 id="team-size-誤判f414">Team size 誤判（F4.14）</h3>
<p>把「省 10-20 工程師」當已 hire 後可裁員的節省支出、實際是 <em>機會成本</em>（沒招那麼多 DBA）。上 CockroachDB 不代表可裁掉現有 DBA — 現有 DBA 反而要轉型成 distributed SQL 運維。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="三家共同-metric">三家共同 metric</h3>
<ul>
<li>write QPS</li>
<li>cross-region latency p99</li>
<li>storage growth</li>
<li>replica lag（CockroachDB Raft / Spanner Paxos / DSQL replica）</li>
</ul>
<h3 id="觀測黑箱程度">觀測黑箱程度</h3>
<ul>
<li><strong>CockroachDB Console</strong>：暴露 Raft / range / leaseholder 細節、observability 細</li>
<li><strong>Spanner / DSQL</strong>：managed、metric 經 GCP Cloud Monitoring / AWS CloudWatch、observability 黑箱程度高 — 邊界 case troubleshooting 仰賴 vendor support</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<p>write QPS × replication factor × cross-region latency = required node / capacity。中小 workload 撞 vendor minimum 才是真實 cost 下界。</p>
<h3 id="cost-signal">Cost signal</h3>
<p>三家定價模式不同、cross-region traffic 對 cost 影響都大：</p>
<ul>
<li>CockroachDB self-managed：node × resource、可控但要自運維</li>
<li>Spanner：100 pu minimum + consumption、適合穩定 workload、中小 burst 不划算</li>
<li>Aurora DSQL：consumption-based、burst 友善、長期穩定 workload 累計可能比 Spanner 高</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a></li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 完整對比</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>（軟體時鐘 vs TrueTime）</li>
<li><a href="../locality-aware-schema/">locality-aware schema</a>（locality model 對比）</li>
<li><a href="../survival-goals/">survival goals</a>（HA model 對比）</li>
<li><a href="../transaction-retry-pattern/">transaction retry pattern</a>（application contract 重塑）</li>
</ul>
<h3 id="sibling-跨-vendor">Sibling 跨 vendor</h3>
<ul>
<li><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 overview</a>（async cross-region、不是 distributed SQL）</li>
<li><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor overview</a> 對照頁</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a>（單區 OLTP fallback）</li>
</ul>
<h3 id="migration-playbook">Migration playbook</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PG → CockroachDB</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">PG → Aurora DSQL</a></li>
</ul>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游</li>
<li><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region OLTP 已夠（90% 場景）→ 用 PostgreSQL / Aurora、不必走 distributed SQL</li>
<li>無 multi-region requirement、無跨 boundary 合規需求 → 同上</li>
<li>workload 規模未撞 single-primary 寫入上限 → 走 Aurora vertical scale + read replica 即可</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>（Path A — single-primary 寫入撞牆）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（Path B — Cassandra 缺口、Database Platform Team）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（Path C — 合規驅動 + team size 槓桿）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner planetary scale</a>（Spanner ground truth + sizing barrier）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>（合規邊界 anti-recommendation）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>（Aurora sharding 不換引擎路徑）</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a> / <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in 卡</a> / <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/">Cockroach Labs Documentation</a> / <a href="https://cloud.google.com/spanner/docs">Spanner Documentation</a> / <a href="https://docs.aws.amazon.com/aurora-dsql/">Aurora DSQL Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL pgBouncer 配置 + 連線池治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/</guid><description>&lt;p>PostgreSQL 的 connection 是 &lt;em>昂貴的 process&lt;/em>、每個 connection ~10MB RAM、idle connection 也吃 backend slot。當 application instance 數量爆炸（K8s replica × 多 deployment × pool size）、直接連 PostgreSQL 會把 backend slot 耗盡、新 connection 全 refuse — 即使 active query 不多。pgBouncer 是 &lt;em>connection pool proxy&lt;/em>、把幾千個 application connection 收斂成幾百個 PostgreSQL backend connection、production-grade PostgreSQL 部署的標配。&lt;/p>
&lt;p>本文不是 pgBouncer overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor 頁&lt;/a> 中 connection pool 段）— 而是 &lt;em>production 部署 + 故障演練&lt;/em> 的實作層教學。覆蓋三層 pool（application → pgBouncer → PostgreSQL）的對齊、transaction pooling 跟 session pooling 的選擇陷阱、跟 HA failover 的整合、容量規劃。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：團隊規模從 50 人爬到 200 人、microservice 從 20 個爬到 100 個、K8s replica 從 3 個爬到每服務 5-10 個。直連 PostgreSQL 的 connection 計算：&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">100 service × 6 replica × 30 application pool = 18000 connection&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PostgreSQL 預設 &lt;code>max_connections = 100&lt;/code>、production 設 &lt;code>max_connections = 500-1000&lt;/code> 已經是上限（每多一個都加 memory + context switch cost）。18000 連線打 PostgreSQL 直接打爆。&lt;/p>
&lt;p>進一步問題：&lt;/p>
&lt;ul>
&lt;li>一半 connection 是 &lt;em>idle&lt;/em>（application pool 預留、實際沒查詢）— 浪費 backend slot&lt;/li>
&lt;li>Cold start 時所有 replica 同時建 connection、瞬間 spike&lt;/li>
&lt;li>DB failover 時所有 application 同時 reconnect、prod-test pattern 跑不通&lt;/li>
&lt;li>DNS-based failover 時 application connection pool 不知道 backend 換了&lt;/li>
&lt;/ul>
&lt;p>pgBouncer 解這四個問題。但 &lt;em>引入 pgBouncer&lt;/em> 後又會引入新的問題層（pgBouncer 跟 application pool 不對齊、transaction pooling 的 session state 限制、HA 故障時 pgBouncer 也要 failover）— 本文討論這些。&lt;/p>
&lt;h2 id="核心概念pool-mode--sizing">核心概念：pool mode + sizing&lt;/h2>
&lt;p>pgBouncer 的 first-class concept 是 &lt;em>pool mode&lt;/em>、決定 application connection 跟 PostgreSQL backend connection 的綁定方式：&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL 的 connection 是 <em>昂貴的 process</em>、每個 connection ~10MB RAM、idle connection 也吃 backend slot。當 application instance 數量爆炸（K8s replica × 多 deployment × pool size）、直接連 PostgreSQL 會把 backend slot 耗盡、新 connection 全 refuse — 即使 active query 不多。pgBouncer 是 <em>connection pool proxy</em>、把幾千個 application connection 收斂成幾百個 PostgreSQL backend connection、production-grade PostgreSQL 部署的標配。</p>
<p>本文不是 pgBouncer overview（請看 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor 頁</a> 中 connection pool 段）— 而是 <em>production 部署 + 故障演練</em> 的實作層教學。覆蓋三層 pool（application → pgBouncer → PostgreSQL）的對齊、transaction pooling 跟 session pooling 的選擇陷阱、跟 HA failover 的整合、容量規劃。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：團隊規模從 50 人爬到 200 人、microservice 從 20 個爬到 100 個、K8s replica 從 3 個爬到每服務 5-10 個。直連 PostgreSQL 的 connection 計算：</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">100 service × 6 replica × 30 application pool = 18000 connection</span></span></code></pre></div><p>PostgreSQL 預設 <code>max_connections = 100</code>、production 設 <code>max_connections = 500-1000</code> 已經是上限（每多一個都加 memory + context switch cost）。18000 連線打 PostgreSQL 直接打爆。</p>
<p>進一步問題：</p>
<ul>
<li>一半 connection 是 <em>idle</em>（application pool 預留、實際沒查詢）— 浪費 backend slot</li>
<li>Cold start 時所有 replica 同時建 connection、瞬間 spike</li>
<li>DB failover 時所有 application 同時 reconnect、prod-test pattern 跑不通</li>
<li>DNS-based failover 時 application connection pool 不知道 backend 換了</li>
</ul>
<p>pgBouncer 解這四個問題。但 <em>引入 pgBouncer</em> 後又會引入新的問題層（pgBouncer 跟 application pool 不對齊、transaction pooling 的 session state 限制、HA 故障時 pgBouncer 也要 failover）— 本文討論這些。</p>
<h2 id="核心概念pool-mode--sizing">核心概念：pool mode + sizing</h2>
<p>pgBouncer 的 first-class concept 是 <em>pool mode</em>、決定 application connection 跟 PostgreSQL backend connection 的綁定方式：</p>
<ul>
<li><strong>Session pooling</strong>：application connection 拿到 backend connection 後、整個 session 期間都綁同一個 backend。tear-down 才釋放。語義跟「直連」一樣、不破壞 session state。但 <em>idle connection 仍占 backend slot</em>、收斂效率低、適合 <em>連線數不多但要保留 session state</em>（用了 prepared statement、temporary table、advisory lock 等）的場景。</li>
<li><strong>Transaction pooling</strong>：application connection 在 <em>transaction 邊界</em> 才綁 backend、commit / rollback 後立即釋放。同一個 application connection 不同 transaction 可能拿到不同 backend。收斂效率高（idle connection 完全不占 backend slot）、但 <em>session state 限制嚴</em> — 不能用 <code>SET</code> 改 session-level setting、不能用 prepared statement（除非 application 端禁用）、不能用 advisory lock 跨 transaction。</li>
<li><strong>Statement pooling</strong>：每個 statement 完就釋放 backend。極端高收斂但 <em>連 transaction 都不能跨 statement</em>、絕大多數 application 用不了、只在 batch query 場景。</li>
</ul>
<p><strong>Production 預設選 transaction pooling</strong>、application 端禁用 prepared statement（或用 <a href="https://www.pgbouncer.org/config.html#max_prepared_statements">PgBouncer-supported prepared statement</a>、需 pgBouncer 1.21+）。例外場景才開 session pooling。</p>
<p><strong>Pool sizing 公式</strong>：</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">PostgreSQL max_connections     = pgBouncer N × default_pool_size + reserve
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgBouncer default_pool_size    = per-database backend connection 上限
</span></span><span class="line"><span class="ln">3</span><span class="cl">Application pool size          = 每 application instance 拿幾個 pgBouncer connection</span></span></code></pre></div><p>實例：50 個 application replica、每 instance pool 30 個、pgBouncer 後 default_pool_size = 20（per database）、3 個 database。</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">Total application → pgBouncer = 50 × 30 = 1500 connection
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgBouncer → PostgreSQL        = 3 × 20 = 60 connection
</span></span><span class="line"><span class="ln">3</span><span class="cl">PostgreSQL max_connections    = 60 + reserve (50 預留 admin / migration) = 110</span></span></code></pre></div><p>1500 → 110 收斂 13.6 倍、PostgreSQL 還在合理上限內。</p>
<h2 id="step-by-step-配置">Step-by-step 配置</h2>
<p><strong>pgBouncer.ini</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[databases]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">mydb</span> <span class="o">=</span> <span class="s">host=postgres-primary.internal port=5432 dbname=mydb auth_user=pgbouncer</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">[pgbouncer]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">listen_port</span> <span class="o">=</span> <span class="s">6432</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">listen_addr</span> <span class="o">=</span> <span class="s">0.0.0.0</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">auth_type</span> <span class="o">=</span> <span class="s">scram-sha-256</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">auth_file</span> <span class="o">=</span> <span class="s">/etc/pgbouncer/userlist.txt</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">auth_query</span> <span class="o">=</span> <span class="s">SELECT usename, passwd FROM pg_shadow WHERE usename=$1</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="na">pool_mode</span> <span class="o">=</span> <span class="s">transaction</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">default_pool_size</span> <span class="o">=</span> <span class="s">20</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="na">min_pool_size</span> <span class="o">=</span> <span class="s">5</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="na">reserve_pool_size</span> <span class="o">=</span> <span class="s">10</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="na">reserve_pool_timeout</span> <span class="o">=</span> <span class="s">5</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 class="na">max_client_conn</span> <span class="o">=</span> <span class="s">2000</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="na">max_db_connections</span> <span class="o">=</span> <span class="s">100</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="na">server_idle_timeout</span> <span class="o">=</span> <span class="s">600</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="na">server_lifetime</span> <span class="o">=</span> <span class="s">3600</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="na">server_connect_timeout</span> <span class="o">=</span> <span class="s">15</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="na">server_login_retry</span> <span class="o">=</span> <span class="s">5</span>
</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 class="na">client_idle_timeout</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="na">client_login_timeout</span> <span class="o">=</span> <span class="s">60</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="na">stats_period</span> <span class="o">=</span> <span class="s">60</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="na">log_connections</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="na">log_disconnections</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="na">log_pooler_errors</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="na">admin_users</span> <span class="o">=</span> <span class="s">pgbouncer_admin</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="na">stats_users</span> <span class="o">=</span> <span class="s">pgbouncer_stats</span></span></span></code></pre></div><p>關鍵欄位解釋：</p>
<ul>
<li><code>pool_mode = transaction</code>：絕大多數 production 場景</li>
<li><code>default_pool_size = 20</code>：每 database 對 PostgreSQL 的 backend connection 上限、調整時要算進 PostgreSQL <code>max_connections</code></li>
<li><code>reserve_pool_size = 10</code> + <code>reserve_pool_timeout = 5</code>：當 default_pool_size 用滿、等 5 秒還拿不到 connection 才用 reserve pool — 是 <em>突發 spike</em> 的 buffer、不是 baseline</li>
<li><code>max_client_conn = 2000</code>：application 端能連 pgBouncer 的最大數</li>
<li><code>server_lifetime = 3600</code>：每 1 小時強制 recycle backend connection、避免 long-lived connection 累積 memory bloat（PostgreSQL <code>pg_stat_activity</code> 看 connection age）</li>
<li><code>auth_query</code>：pgBouncer 直接從 PostgreSQL <code>pg_shadow</code> 拉密碼、不需要在 pgBouncer 本地維護 userlist — production 推薦做法</li>
</ul>
<p><strong>Application 端 pool 設定</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 例：Spring Boot HikariCP</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.url</span><span class="p">:</span><span class="w"> </span><span class="l">jdbc:postgresql://pgbouncer.internal:6432/mydb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.hikari.maximum-pool-size</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.hikari.minimum-idle</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.hikari.connection-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">30000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.hikari.idle-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">600000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">spring.datasource.hikari.max-lifetime</span><span class="p">:</span><span class="w"> </span><span class="m">1800000</span><span class="w">  </span><span class="c"># 30 min &lt; pgBouncer server_lifetime 60 min</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c"># 例：SQLAlchemy</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="l">engine = create_engine(</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="s2">&#34;postgresql://pgbouncer.internal:6432/mydb&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="l">pool_size=30,</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="l">max_overflow=5,</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="l">pool_pre_ping=True,       </span><span class="w"> </span><span class="c"># 必開、檢測 stale connection</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="l">pool_recycle=1800,        </span><span class="w"> </span><span class="c"># 30 min、跟 pgBouncer server_lifetime 對齊</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="l">)</span></span></span></code></pre></div><p><strong>Application 跟 pgBouncer 對齊</strong>：</p>
<ul>
<li>application <code>max-lifetime</code> &lt; pgBouncer <code>server_lifetime</code>：避免 application 拿到已被 pgBouncer recycle 的 connection</li>
<li><code>pool_pre_ping = True</code>：每次 checkout 前 send <code>SELECT 1</code>、檢測 stale connection — 對 transaction pooling 是必要的</li>
<li>application 端 <em>不要</em> 用 prepared statement（除非 pgBouncer 1.21+ 設 <code>max_prepared_statements</code>）</li>
</ul>
<h2 id="故障演練--邊界-case">故障演練 / 邊界 case</h2>
<h3 id="case-1pool-exhaustiondefault_pool_size-用滿">Case 1：Pool exhaustion（default_pool_size 用滿）</h3>
<p>徵兆：application log <code>ERROR: no more connections allowed</code>、pgBouncer log <code>pool is full</code>、pgBouncer admin console <code>SHOW POOLS</code> 顯示 <code>cl_waiting &gt; 0</code>。</p>
<p>Debug：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 連 pgBouncer admin
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">\</span><span class="k">c</span><span class="w"> </span><span class="n">pgbouncer</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">POOLS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看 cl_active / cl_waiting / sv_active / sv_idle
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">SERVERS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- 看 server connection state（active / idle / used）</span></span></span></code></pre></div><p>修：</p>
<ul>
<li>短期：調高 <code>default_pool_size</code> 跟 PostgreSQL <code>max_connections</code>、配合 reserve pool</li>
<li>中期：找 <em>long-running query</em>（PostgreSQL <code>pg_stat_activity</code> 看 <code>query_start</code>、kill 過長 query）</li>
<li>長期：拆 database / 改 read replica / 移 OLAP query 到 data warehouse</li>
</ul>
<h3 id="case-2transaction-pooling-下-session-state-漏洞">Case 2：Transaction pooling 下 session state 漏洞</h3>
<p>徵兆：random 失敗 <code>prepared statement &quot;S_3&quot; does not exist</code>、<code>relation &quot;tmp_xxx&quot; does not exist</code>、advisory lock 不釋放。</p>
<p>原因：application 用了 prepared statement / temporary table / advisory lock、但 transaction commit 後 backend connection 釋放、下一個 transaction 拿到不同 backend、session state 不存在。</p>
<p>修：</p>
<ul>
<li>Application 框架禁用 prepared statement（JDBC <code>prepareThreshold=0</code>、SQLAlchemy <code>use_native_prepared_statements=False</code>）</li>
<li>temporary table 改 <a href="https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED-TABLES">unlogged table</a> + cleanup</li>
<li>advisory lock 改 row-level lock 或 application-level lock（Redis）</li>
<li>或：切到 session pooling、犧牲收斂效率</li>
</ul>
<h3 id="case-3dns-based-failover-後-application-連到舊-master">Case 3：DNS-based failover 後 application 連到舊 master</h3>
<p>徵兆：PostgreSQL 切換 master 後、application 寫操作 <em>時好時壞</em>（看連到哪台）。</p>
<p>原因：pgBouncer 在 application 跟 PostgreSQL 之間、application 不知道 backend 換了；pgBouncer 自己也需要 reload config 才會連新 master。</p>
<p>修：</p>
<ul>
<li>pgBouncer 用 <code>RECONNECT</code> admin command 強制 close all backend connection、重連</li>
<li>配 Patroni / Stolon 等 HA 工具自動 trigger pgBouncer reconnect</li>
<li>application 端 <code>pool_pre_ping</code> 開啟、stale connection 自動踢</li>
</ul>
<h3 id="case-4server-lifetime-recycle-跟-in-flight-transaction-衝突">Case 4：Server lifetime recycle 跟 in-flight transaction 衝突</h3>
<p>徵兆：偶發 <code>server closed the connection unexpectedly</code>、跟 long-running transaction 重疊。</p>
<p>原因：pgBouncer <code>server_lifetime = 3600</code> 強制 recycle、但有 transaction 在跑時 pgBouncer 不會切、超過時間後仍會切。</p>
<p>修：</p>
<ul>
<li>確認沒有 <em>超過 1 小時</em> 的 transaction（PostgreSQL <code>pg_stat_activity</code> 看 <code>xact_start</code>）</li>
<li>必要時調高 <code>server_lifetime</code>、但 memory bloat 風險上升</li>
<li>application 端做 transaction timeout</li>
</ul>
<h3 id="case-5pgbouncer-自己-crash--oom">Case 5：pgBouncer 自己 crash / OOM</h3>
<p>徵兆：所有 application 同時失去 PostgreSQL 連線。</p>
<p>原因：pgBouncer 是 single-process（除非 1.21+ 用 <code>so_reuseport</code> 多 process）、memory leak / OOM / 部署事件都會打掉整個 connection layer。</p>
<p>修：</p>
<ul>
<li>多 pgBouncer instance + load balancer（HAProxy / Envoy）前置、application 連 LB</li>
<li><code>so_reuseport = 1</code>（1.21+）讓多個 pgBouncer process 共用 port</li>
<li>Resource limit 跟 alert：RSS &gt; N、connection count &gt; M</li>
<li>HA mode：active-passive 配 keepalived</li>
</ul>
<h2 id="容量--cost-規劃">容量 / cost 規劃</h2>
<p><strong>單一 pgBouncer 容量上限</strong>：</p>
<ul>
<li><code>max_client_conn</code>：實務 &lt; 5000 per instance（再高 CPU 跟 file descriptor 緊）</li>
<li><code>default_pool_size × database 數</code>：實務 &lt; 200 per instance</li>
<li>single process CPU bound：在 10K QPS 等級已經是瓶頸、要橫向 scale</li>
</ul>
<p><strong>何時加 pgBouncer instance</strong>：</p>
<ul>
<li>application connection 數突破 3000 / pgBouncer instance</li>
<li>pgBouncer CPU usage &gt; 60%（baseline、不算 spike）</li>
<li>跨 region application 需要 region-local pgBouncer</li>
</ul>
<p><strong>何時改架構（pgBouncer 不夠用）</strong>：</p>
<ul>
<li>PostgreSQL backend connection 數突破 500（即使有 pgBouncer 也撐不住）→ 改 read replica / partitioning / sharding</li>
<li>write 量太大（每秒 50K+ TPS）→ 改 sharding（<a href="https://vitess.io">Vitess</a> / <a href="https://www.citusdata.com">Citus</a>）或全球分散式 SQL（<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>）</li>
<li>application 大量 prepared statement / session state 需求 → 改 <a href="https://github.com/postgresml/pgcat">PgCat</a>（Rust 寫、支援更完整的 session feature）或回 session pooling</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p><strong>跟 HA failover 整合</strong>（<a href="https://github.com/zalando/patroni">Patroni</a>）：</p>
<ul>
<li>Patroni 切換 master 後 trigger pgBouncer <code>RECONNECT</code></li>
<li>pgBouncer 透過 service discovery（Consul / etcd）拿新 master 位址、不是寫死在 config</li>
<li>application 不需感知 failover、connection 從 pgBouncer 拿到新 master 的 backend</li>
</ul>
<p><strong>跟監控整合</strong>：</p>
<ul>
<li>pgBouncer admin console <code>SHOW STATS</code> / <code>SHOW POOLS</code> / <code>SHOW SERVERS</code> 拉到 Prometheus（<a href="https://github.com/jbub/pgbouncer_exporter">pgbouncer_exporter</a>）</li>
<li>必看 metric：<code>cl_waiting</code>（等 backend 的 client 數）、<code>sv_active</code>（active backend 數）、<code>avg_query_time</code>、<code>avg_xact_time</code></li>
<li>Alert：<code>cl_waiting &gt; 0 持續 30s</code>、<code>server connection error rate &gt; 0</code></li>
</ul>
<p><strong>跟 application observability 整合</strong>：</p>
<ul>
<li>Application APM（<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> / Honeycomb / OpenTelemetry）的 DB span 顯示 <em>application 看到的 latency</em>、pgBouncer metric 顯示 <em>pgBouncer ↔ PostgreSQL latency</em> — 兩者差異揭露 connection wait time</li>
</ul>
<p><strong>何時 revisit 這個配置</strong>：</p>
<ul>
<li>application 數量倍增（trigger pool sizing 重算）</li>
<li>PostgreSQL 升級（pgBouncer 跟 PostgreSQL 版本相容性）</li>
<li>跨 region 部署（要不要 region-local pgBouncer）</li>
<li>切換到 RDS Proxy / Aurora Cluster Endpoint（managed alternative）</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a> — 本文是該頁尾「pgBouncer / PgCat 配置 best practice」backlog 的深度展開</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/connection-scaling/" data-link-title="PostgreSQL Connection Scaling：process-per-connection model 跟為什麼 pooler 是必裝" data-link-desc="PG 每個 client connection fork 一個 backend process（不是 thread）、RAM 成本 5-15MB/connection、context switch 跟 fork() cost 在 100&#43; connection 後線性放大、所以 pooler 不是 *optional optimization* 而是 *production prerequisite*。本文走 process-per-connection model 跟 MySQL thread-per-connection 對比、max_connections &#43; shared_buffers &#43; work_mem 三 GUC 互動、application-side pool vs middleware pool vs RDS Proxy 三層選擇、5 production 踩雷（connection storm / fork() cost 在 burst 流量 / shared_buffers 跟 connection 數壓縮 / double-pool 配置錯誤 / max_connections 設太大反而慢）、跟 PgBouncer config 互補不重複">Connection Scaling Deep Dive</a> — connection-per-process model 跟為什麼 pooler 是必裝（根因 vs 配置）</li>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> — 上游：什麼時候需要 connection pool</li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a> — 概念基底</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文是該方法論的 demo #1</li>
<li><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 RDB connection limit case</a> — connection 爆是 streaming surge 場景的 vendor-switch 主因</li>
<li>官方：<a href="https://www.pgbouncer.org/usage.html">pgBouncer Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Error Budget</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/</guid><description>&lt;p>Error budget 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 允許的失敗額度」。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗；這 0.1% 就是 error budget，用來平衡功能交付速度與可靠性改善投入。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Error budget 把可靠性討論轉成可量化的決策語言。Budget 消耗過快時，團隊應暫停高風險變更、優先修可靠性；budget 充足時，可以承擔更多變更風險跟 experiment。&lt;/p>
&lt;p>Error budget 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> alerting 的基礎 — burn rate 量化的是 error budget 被消耗的速度。Error budget 接近耗盡時，進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 的 freeze 條件。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 error budget 的訊號是發版速度與事故風險需要共同管理。Checkout 服務本月多次 timeout，若 error budget 已接近耗盡，團隊應暫停高風險變更直到 budget 恢復。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Error budget 的 metric 結構需要 rolling window 的 total requests 跟 failed requests（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>）。Budget remaining 作為 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> panel 跟 release gate 的輸入 — 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 維護 rolling window 計算，避免每次查詢掃描 30 天的 raw data。&lt;/p></description><content:encoded><![CDATA[<p>Error budget 的核心概念是「<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 允許的失敗額度」。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗；這 0.1% 就是 error budget，用來平衡功能交付速度與可靠性改善投入。</p>
<h2 id="概念位置">概念位置</h2>
<p>Error budget 把可靠性討論轉成可量化的決策語言。Budget 消耗過快時，團隊應暫停高風險變更、優先修可靠性；budget 充足時，可以承擔更多變更風險跟 experiment。</p>
<p>Error budget 是 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alerting 的基礎 — burn rate 量化的是 error budget 被消耗的速度。Error budget 接近耗盡時，進入 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 的 freeze 條件。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 error budget 的訊號是發版速度與事故風險需要共同管理。Checkout 服務本月多次 timeout，若 error budget 已接近耗盡，團隊應暫停高風險變更直到 budget 恢復。</p>
<h2 id="設計責任">設計責任</h2>
<p>Error budget 的 metric 結構需要 rolling window 的 total requests 跟 failed requests（見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>）。Budget remaining 作為 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> panel 跟 release gate 的輸入 — 用 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 維護 rolling window 計算，避免每次查詢掃描 30 天的 raw data。</p>
]]></content:encoded></item><item><title>Burn Rate</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/</guid><description>&lt;p>Burn rate 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 被消耗的速度」。Burn rate = 1 代表按 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Burn rate 是 SLO alerting 的核心機制，把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 的 error ratio 轉成可行動的嚴重度判斷。短時間高 burn rate（14x、5 分鐘窗口）代表急性事故；長時間中等 burn rate（1x、數小時窗口）代表慢性可靠性退化。&lt;/p>
&lt;p>Burn rate alerting 比固定閾值 alert 更能反映使用者影響 — 低流量時段的幾筆 error 可能 burn rate 很低（對 error budget 影響小），高流量時段的相同 error rate 可能 burn rate 很高（影響大量使用者）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 burn rate 的訊號是固定閾值 alert（error rate &amp;gt; 1%）在不同流量時段的表現不穩定 — 低流量時 false alarm、高流量時漏報。Burn rate 自動適應流量基線。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Burn rate alerting 用 multi-window 策略：短窗口（5min）抓急性 + 長窗口（1hr）做確認，兩個窗口都超過閾值才觸發。Recording rule 預計算各窗口的 error ratio，讓 alert evaluate 讀預計算結果而非重算 raw series。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Burn rate 的核心概念是「<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 被消耗的速度」。Burn rate = 1 代表按 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。</p>
<h2 id="概念位置">概念位置</h2>
<p>Burn rate 是 SLO alerting 的核心機制，把 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 的 error ratio 轉成可行動的嚴重度判斷。短時間高 burn rate（14x、5 分鐘窗口）代表急性事故；長時間中等 burn rate（1x、數小時窗口）代表慢性可靠性退化。</p>
<p>Burn rate alerting 比固定閾值 alert 更能反映使用者影響 — 低流量時段的幾筆 error 可能 burn rate 很低（對 error budget 影響小），高流量時段的相同 error rate 可能 burn rate 很高（影響大量使用者）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 burn rate 的訊號是固定閾值 alert（error rate &gt; 1%）在不同流量時段的表現不穩定 — 低流量時 false alarm、高流量時漏報。Burn rate 自動適應流量基線。</p>
<h2 id="設計責任">設計責任</h2>
<p>Burn rate alerting 用 multi-window 策略：短窗口（5min）抓急性 + 長窗口（1hr）做確認，兩個窗口都超過閾值才觸發。Recording rule 預計算各窗口的 error ratio，讓 alert evaluate 讀預計算結果而非重算 raw series。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Correlation ID</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/</guid><description>&lt;p>Correlation ID 的核心概念是「把同一個業務流程中的多筆紀錄關聯起來的識別碼」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 的核心欄位，可以跨 request、queue message、background job、log、trace 與外部 API 呼叫。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Correlation ID 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 的定位不同。Trace id 偏向一次技術呼叫路徑（一個 HTTP request 經過多個服務）；correlation ID 可以代表更長的業務流程（一筆訂單從建立到付款到出貨，跨越多個獨立 request）。&lt;/p>
&lt;p>Correlation ID 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 的核心欄位。Log 帶 correlation ID 時，跨服務跟跨 async 邊界的事件可以用同一個 ID 查出完整業務流程。見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 correlation ID 的訊號是事故排查需要跨同步與非同步邊界。訂單建立 request、付款事件、寄信 job 與出貨事件共享同一 correlation ID，讓客服跟工程師追到完整流程。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Correlation ID 要在入口（API gateway 或 first service）建立或從 upstream 接收，並傳遞到 log、message header、trace context 與外部呼叫。欄位名稱要穩定（跨服務一致，避免 &lt;code>request_id&lt;/code> vs &lt;code>req_id&lt;/code> vs &lt;code>requestId&lt;/code> 的漂移），避免把敏感資料當成 ID。&lt;/p></description><content:encoded><![CDATA[<p>Correlation ID 的核心概念是「把同一個業務流程中的多筆紀錄關聯起來的識別碼」。它是 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 的核心欄位，可以跨 request、queue message、background job、log、trace 與外部 API 呼叫。</p>
<h2 id="概念位置">概念位置</h2>
<p>Correlation ID 跟 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 的定位不同。Trace id 偏向一次技術呼叫路徑（一個 HTTP request 經過多個服務）；correlation ID 可以代表更長的業務流程（一筆訂單從建立到付款到出貨，跨越多個獨立 request）。</p>
<p>Correlation ID 是 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 的核心欄位。Log 帶 correlation ID 時，跨服務跟跨 async 邊界的事件可以用同一個 ID 查出完整業務流程。見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 correlation ID 的訊號是事故排查需要跨同步與非同步邊界。訂單建立 request、付款事件、寄信 job 與出貨事件共享同一 correlation ID，讓客服跟工程師追到完整流程。</p>
<h2 id="設計責任">設計責任</h2>
<p>Correlation ID 要在入口（API gateway 或 first service）建立或從 upstream 接收，並傳遞到 log、message header、trace context 與外部呼叫。欄位名稱要穩定（跨服務一致，避免 <code>request_id</code> vs <code>req_id</code> vs <code>requestId</code> 的漂移），避免把敏感資料當成 ID。</p>
]]></content:encoded></item><item><title>Trace ID</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/</guid><description>&lt;p>Trace ID 的核心概念是「分散式追蹤中同一條呼叫路徑的全域識別碼」。一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 由多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 組成，trace ID 讓 tracing 系統把散落在不同服務的 span 聚合成同一次操作的完整路徑。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace ID 是 tracing 的頂層關聯欄位。W3C Trace Context 標準使用 128-bit 隨機值（32 hex chars）；部分 vendor 使用 64-bit（Datadog 舊版、Zipkin v1）。混用不同長度時需要在 collector 層做 ID 轉換或 padding。&lt;/p>
&lt;p>Trace ID 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 的定位不同：request id 是單一服務內的請求識別碼（通常由 API gateway 或 load balancer 產生），trace id 是跨服務的追蹤識別碼（由第一個 instrumented service 產生）。兩者可以共存在同一筆 log 的不同欄位，各自服務不同的查詢需求。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>Trace ID 的診斷價值是「拿到一個 ID 就能看到整條 request 路徑」。事故中從 error log 拿到 trace ID，貼進 tracing UI（Jaeger、Grafana Tempo、Datadog APM），直接看 waterfall view 定位瓶頸。&lt;/p>
&lt;p>Trace ID 也是 log / metric / trace 三者的關聯樞紐。Log 的結構化欄位帶 trace ID 時，debug 工作流可以從 log → trace 或 trace → log 雙向跳轉。Metric 的 exemplar 帶 trace ID 時，可以從 dashboard 的 latency spike 跳到具體的高延遲 trace。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace ID 要透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 HTTP header、queue message header、thread context 上傳遞。Log 層面，trace ID 應作為必要欄位寫入 structured log（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>）。Sampling 策略要確保錯誤與高延遲 trace 有足夠保留率，避免事故時 trace ID 存在於 log 但對應的 trace 資料已被 sampling 丟棄。&lt;/p></description><content:encoded><![CDATA[<p>Trace ID 的核心概念是「分散式追蹤中同一條呼叫路徑的全域識別碼」。一個 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 由多個 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 組成，trace ID 讓 tracing 系統把散落在不同服務的 span 聚合成同一次操作的完整路徑。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace ID 是 tracing 的頂層關聯欄位。W3C Trace Context 標準使用 128-bit 隨機值（32 hex chars）；部分 vendor 使用 64-bit（Datadog 舊版、Zipkin v1）。混用不同長度時需要在 collector 層做 ID 轉換或 padding。</p>
<p>Trace ID 跟 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 的定位不同：request id 是單一服務內的請求識別碼（通常由 API gateway 或 load balancer 產生），trace id 是跨服務的追蹤識別碼（由第一個 instrumented service 產生）。兩者可以共存在同一筆 log 的不同欄位，各自服務不同的查詢需求。</p>
<h2 id="使用情境">使用情境</h2>
<p>Trace ID 的診斷價值是「拿到一個 ID 就能看到整條 request 路徑」。事故中從 error log 拿到 trace ID，貼進 tracing UI（Jaeger、Grafana Tempo、Datadog APM），直接看 waterfall view 定位瓶頸。</p>
<p>Trace ID 也是 log / metric / trace 三者的關聯樞紐。Log 的結構化欄位帶 trace ID 時，debug 工作流可以從 log → trace 或 trace → log 雙向跳轉。Metric 的 exemplar 帶 trace ID 時，可以從 dashboard 的 latency spike 跳到具體的高延遲 trace。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace ID 要透過 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 HTTP header、queue message header、thread context 上傳遞。Log 層面，trace ID 應作為必要欄位寫入 structured log（見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>）。Sampling 策略要確保錯誤與高延遲 trace 有足夠保留率，避免事故時 trace ID 存在於 log 但對應的 trace 資料已被 sampling 丟棄。</p>
]]></content:encoded></item><item><title>Span</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/span/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/span/</guid><description>&lt;p>Span 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 中的一段有起止時間的工作」。每個 span 記錄操作名稱、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception message）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Span 是 tracing 的基本單位。HTTP handler、database query、cache call、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> publish、consumer handle 與外部 API 呼叫都可以形成 span。Span 之間透過 parent-child 關係組成 tree — 共享同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 的所有 span 構成一條完整的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>。&lt;/p>
&lt;p>Span 有四種 kind：&lt;code>CLIENT&lt;/code>（發起呼叫）、&lt;code>SERVER&lt;/code>（接收呼叫）、&lt;code>PRODUCER&lt;/code>（投遞訊息）、&lt;code>CONSUMER&lt;/code>（消費訊息）。Kind 影響 trace backend 怎麼計算 service-to-service 的延遲跟依賴方向。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 span 的訊號是單一 request 裡有多個步驟，需要知道哪一步變慢或出錯。Checkout trace 中 payment span 佔 80% 時間，問題焦點就落在付款依賴或其網路路徑。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Span 設計要控制名稱粒度、屬性選擇、錯誤狀態與敏感資料。Span 名稱太粗（所有 HTTP call 都叫 &lt;code>HTTP&lt;/code>）會看不出瓶頸；太細（每個 URL path parameter 都獨立命名）會讓 span 名稱成為無界維度、影響 trace backend 的聚合效能。&lt;/p>
&lt;p>屬性要帶足夠的診斷資訊但避免敏感資料。&lt;code>http.url&lt;/code> 帶完整 URL 可能含 query parameter 裡的 token；&lt;code>db.statement&lt;/code> 帶完整 SQL 可能含使用者資料。需要在 SDK 或 collector 層做 redaction。&lt;/p></description><content:encoded><![CDATA[<p>Span 的核心概念是「<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 中的一段有起止時間的工作」。每個 span 記錄操作名稱、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception message）。</p>
<h2 id="概念位置">概念位置</h2>
<p>Span 是 tracing 的基本單位。HTTP handler、database query、cache call、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> publish、consumer handle 與外部 API 呼叫都可以形成 span。Span 之間透過 parent-child 關係組成 tree — 共享同一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 的所有 span 構成一條完整的 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>。</p>
<p>Span 有四種 kind：<code>CLIENT</code>（發起呼叫）、<code>SERVER</code>（接收呼叫）、<code>PRODUCER</code>（投遞訊息）、<code>CONSUMER</code>（消費訊息）。Kind 影響 trace backend 怎麼計算 service-to-service 的延遲跟依賴方向。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 span 的訊號是單一 request 裡有多個步驟，需要知道哪一步變慢或出錯。Checkout trace 中 payment span 佔 80% 時間，問題焦點就落在付款依賴或其網路路徑。</p>
<h2 id="設計責任">設計責任</h2>
<p>Span 設計要控制名稱粒度、屬性選擇、錯誤狀態與敏感資料。Span 名稱太粗（所有 HTTP call 都叫 <code>HTTP</code>）會看不出瓶頸；太細（每個 URL path parameter 都獨立命名）會讓 span 名稱成為無界維度、影響 trace backend 的聚合效能。</p>
<p>屬性要帶足夠的診斷資訊但避免敏感資料。<code>http.url</code> 帶完整 URL 可能含 query parameter 裡的 token；<code>db.statement</code> 帶完整 SQL 可能含使用者資料。需要在 SDK 或 collector 層做 redaction。</p>
]]></content:encoded></item><item><title>Symptom-Based Alert</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/</guid><description>&lt;p>Symptom-based alert 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 優先偵測使用者或產品可感知的症狀」。症狀包括錯誤率、延遲、可用性、資料延遲、付款失敗與訊息未送達。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Symptom-based alert 跟 cause-based alert 分工不同。CPU 高、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a> 高、GC 頻繁是可能的原因；checkout 失敗率升高才是直接的產品症狀。Symptom-based 適合 critical severity（page &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a>），cause-based 適合 warning severity（工作時間排入 task）。&lt;/p>
&lt;p>Symptom-based alert 是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 建議的 alert 設計起點 — 先確認使用者是否受影響、再看系統原因。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 symptom-based alert 的訊號是 on-call 被大量低層訊號吵醒，但無法判斷使用者是否受影響。付款成功率下降應立即告警；單台 instance CPU 高則可先進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 觀察或走自動修復流程。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Symptom-based alert 要連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與影響判斷。SLO-based alerting 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 量化症狀嚴重度 — 「error budget 消耗速度是允許值的 14 倍」比「error rate &amp;gt; 1%」更能反映使用者影響規模。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Symptom-based alert 的核心概念是「<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 優先偵測使用者或產品可感知的症狀」。症狀包括錯誤率、延遲、可用性、資料延遲、付款失敗與訊息未送達。</p>
<h2 id="概念位置">概念位置</h2>
<p>Symptom-based alert 跟 cause-based alert 分工不同。CPU 高、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a> 高、GC 頻繁是可能的原因；checkout 失敗率升高才是直接的產品症狀。Symptom-based 適合 critical severity（page <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a>），cause-based 適合 warning severity（工作時間排入 task）。</p>
<p>Symptom-based alert 是 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 建議的 alert 設計起點 — 先確認使用者是否受影響、再看系統原因。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 symptom-based alert 的訊號是 on-call 被大量低層訊號吵醒，但無法判斷使用者是否受影響。付款成功率下降應立即告警；單台 instance CPU 高則可先進 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 觀察或走自動修復流程。</p>
<h2 id="設計責任">設計責任</h2>
<p>Symptom-based alert 要連到 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與影響判斷。SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 量化症狀嚴重度 — 「error budget 消耗速度是允許值的 14 倍」比「error rate &gt; 1%」更能反映使用者影響規模。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Alert Fatigue</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/</guid><description>&lt;p>Alert fatigue 的核心概念是「過多低品質告警讓處理者對告警失去敏感度」。當告警常常沒有使用者影響、沒有行動步驟或頻繁自動恢復，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 會開始忽略訊號 — 包括真正需要處理的那些。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Alert fatigue 是可觀測性設計的失敗模式，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的品質治理直接相關。告警應代表需要人介入的產品風險；其他訊號可以進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、ticket、報表或自動修復流程。&lt;/p>
&lt;p>常見的 fatigue 來源：false positive（條件觸發但實際沒問題）、redundant alert（同一問題觸發多個 alert）、stale alert（條件已不適用但 rule 沒更新）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要治理 alert fatigue 的訊號是 noise rate &amp;gt; 30%（超過三成的 alert 不需要行動），或 on-call 工程師反應「收到 alert 先 ack 再看、有時直接 resolve 不看」。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Alert fatigue 的治理包括：追蹤 noise rate（on-call ack 時標記 actionable / noise）、定期審視高 noise 的 alert rule（調整閾值、改 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based&lt;/a>、加 inhibition、或刪除）、用 grouping 跟 inhibition 減少同一問題的重複通知。治理節奏跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a> 整合。&lt;/p></description><content:encoded><![CDATA[<p>Alert fatigue 的核心概念是「過多低品質告警讓處理者對告警失去敏感度」。當告警常常沒有使用者影響、沒有行動步驟或頻繁自動恢復，<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 會開始忽略訊號 — 包括真正需要處理的那些。</p>
<h2 id="概念位置">概念位置</h2>
<p>Alert fatigue 是可觀測性設計的失敗模式，跟 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的品質治理直接相關。告警應代表需要人介入的產品風險；其他訊號可以進 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、ticket、報表或自動修復流程。</p>
<p>常見的 fatigue 來源：false positive（條件觸發但實際沒問題）、redundant alert（同一問題觸發多個 alert）、stale alert（條件已不適用但 rule 沒更新）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要治理 alert fatigue 的訊號是 noise rate &gt; 30%（超過三成的 alert 不需要行動），或 on-call 工程師反應「收到 alert 先 ack 再看、有時直接 resolve 不看」。</p>
<h2 id="設計責任">設計責任</h2>
<p>Alert fatigue 的治理包括：追蹤 noise rate（on-call ack 時標記 actionable / noise）、定期審視高 noise 的 alert rule（調整閾值、改 <a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based</a>、加 inhibition、或刪除）、用 grouping 跟 inhibition 減少同一問題的重複通知。治理節奏跟 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a> 整合。</p>
]]></content:encoded></item><item><title>Queue</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/queue/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/queue/</guid><description>&lt;p>Queue 的核心概念是「把等待處理的工作依序放入一個可觀測的等待區」。它讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 在時間上解耦，也讓系統可以用等待長度、等待時間與處理速率評估容量壓力。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Queue 可以存在於 application 內部（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a>），也可以由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、database table 或 stream platform 提供。Application 內部的 queue 隨 process 生命週期消失；跨 process、需要保存與重放的 queue 通常需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a> 或 broker。&lt;/p>
&lt;p>Queue 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 的差異：queue 的語意通常是「一筆訊息被一個 consumer 處理」（competing consumers），topic 的語意是「一筆訊息可以被多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 各自處理」（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>）。但不同 broker 的術語定義不同 — RabbitMQ 的 queue 跟 Kafka 的 partition 在消費語意上有本質差異。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 queue 的訊號是進入速度跟處理速度會短暫不一致。寄信、報表匯出、圖片轉檔、訂單狀態同步都適合先排入 queue，再由 consumer 依照容量處理。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">Queue depth&lt;/a> 跟 oldest item age 會反映延遲壓力 — queue depth 持續增長代表 consumer 來不及消化，需要擴展 consumer 或降低進入速率。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Queue 要定義容量上限、排序語意（FIFO / priority / delay）、保存期限（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>）、消費模式（pull vs push）、失敗處理（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 策略（滿了怎麼辦 — block / drop / reject）與觀測欄位。設計上要區分「等待可以接受」跟「等待會傷害產品結果」— 付款入帳能短暫排隊，互動式 API response 通常需要更短的等待期限與更明確的拒絕策略。&lt;/p></description><content:encoded><![CDATA[<p>Queue 的核心概念是「把等待處理的工作依序放入一個可觀測的等待區」。它讓 <a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 和 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 在時間上解耦，也讓系統可以用等待長度、等待時間與處理速率評估容量壓力。</p>
<h2 id="概念位置">概念位置</h2>
<p>Queue 可以存在於 application 內部（<a href="/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel</a> + <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>），也可以由 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、database table 或 stream platform 提供。Application 內部的 queue 隨 process 生命週期消失；跨 process、需要保存與重放的 queue 通常需要 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a> 或 broker。</p>
<p>Queue 跟 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 的差異：queue 的語意通常是「一筆訊息被一個 consumer 處理」（competing consumers），topic 的語意是「一筆訊息可以被多個 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 各自處理」（<a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>）。但不同 broker 的術語定義不同 — RabbitMQ 的 queue 跟 Kafka 的 partition 在消費語意上有本質差異。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 queue 的訊號是進入速度跟處理速度會短暫不一致。寄信、報表匯出、圖片轉檔、訂單狀態同步都適合先排入 queue，再由 consumer 依照容量處理。<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">Queue depth</a> 跟 oldest item age 會反映延遲壓力 — queue depth 持續增長代表 consumer 來不及消化，需要擴展 consumer 或降低進入速率。</p>
<h2 id="設計責任">設計責任</h2>
<p>Queue 要定義容量上限、排序語意（FIFO / priority / delay）、保存期限（<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a>）、消費模式（pull vs push）、失敗處理（<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a> + <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>）、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 策略（滿了怎麼辦 — block / drop / reject）與觀測欄位。設計上要區分「等待可以接受」跟「等待會傷害產品結果」— 付款入帳能短暫排隊，互動式 API response 通常需要更短的等待期限與更明確的拒絕策略。</p>
]]></content:encoded></item><item><title>Consumer</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/</guid><description>&lt;p>Consumer 的核心概念是「從等待區取得工作、事件或資料並執行處理的角色」。它可以從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stream-pipeline/" data-link-title="Stream Pipeline" data-link-desc="說明連續資料流經多個處理階段時如何管理吞吐、順序與 backpressure ">stream pipeline&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> table 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel&lt;/a> 取得資料，再更新狀態、呼叫外部服務或產生衍生資料。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Consumer 位在資料流的下游。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> 構成 MQ 的基本角色對 — producer 負責把工作送進等待區，consumer 負責取出並處理。&lt;/p>
&lt;p>多個 consumer 組成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 來分攤處理負載。Consumer 的處理速度跟錯誤行為直接影響 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>（積壓深度）跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>（無法處理的訊息去處）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要辨識 consumer 的訊號是資料已經送入系統但產品結果還沒完成。付款事件送入後，入帳 consumer 要更新帳務狀態；通知事件送入後，寄信 consumer 要呼叫郵件服務。兩者都要清楚記錄處理成功、暫時失敗與永久拒絕。&lt;/p>
&lt;p>Consumer 的處理模式影響系統的可靠性保證。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack / nack&lt;/a> 的時機決定「訊息什麼時候算處理完成」；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 決定「重複收到同一筆訊息時是否會產生副作用」。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Consumer 要定義併發數、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack / nack&lt;/a> 條件、錯誤分類（暫時性 vs 永久性）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy&lt;/a>、隔離區、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 與觀測欄位。&lt;/p>
&lt;p>操作面要能觀測：處理速率（messages/sec）、失敗類型分布、oldest unprocessed message age、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a> 累積量與下游 dependency latency。Consumer lag 持續增長是容量不足的 leading indicator。&lt;/p></description><content:encoded><![CDATA[<p>Consumer 的核心概念是「從等待區取得工作、事件或資料並執行處理的角色」。它可以從 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、<a href="/blog/backend/knowledge-cards/stream-pipeline/" data-link-title="Stream Pipeline" data-link-desc="說明連續資料流經多個處理階段時如何管理吞吐、順序與 backpressure ">stream pipeline</a>、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> table 或 <a href="/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel</a> 取得資料，再更新狀態、呼叫外部服務或產生衍生資料。</p>
<h2 id="概念位置">概念位置</h2>
<p>Consumer 位在資料流的下游。它跟 <a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 構成 MQ 的基本角色對 — producer 負責把工作送進等待區，consumer 負責取出並處理。</p>
<p>多個 consumer 組成 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 來分攤處理負載。Consumer 的處理速度跟錯誤行為直接影響 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>（積壓深度）跟 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>（無法處理的訊息去處）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要辨識 consumer 的訊號是資料已經送入系統但產品結果還沒完成。付款事件送入後，入帳 consumer 要更新帳務狀態；通知事件送入後，寄信 consumer 要呼叫郵件服務。兩者都要清楚記錄處理成功、暫時失敗與永久拒絕。</p>
<p>Consumer 的處理模式影響系統的可靠性保證。<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">Ack / nack</a> 的時機決定「訊息什麼時候算處理完成」；<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 決定「重複收到同一筆訊息時是否會產生副作用」。</p>
<h2 id="設計責任">設計責任</h2>
<p>Consumer 要定義併發數、<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack / nack</a> 條件、錯誤分類（暫時性 vs 永久性）、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a>、隔離區、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 與觀測欄位。</p>
<p>操作面要能觀測：處理速率（messages/sec）、失敗類型分布、oldest unprocessed message age、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a> 累積量與下游 dependency latency。Consumer lag 持續增長是容量不足的 leading indicator。</p>
]]></content:encoded></item><item><title>Topic</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/topic/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/topic/</guid><description>&lt;p>Topic 的核心概念是「用主題名稱描述一類事件或訊息」。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">Producer&lt;/a> 把事件發布到 topic，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 再依照訂閱關係、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/routing-rule/" data-link-title="Routing Rule" data-link-desc="說明訊息系統如何依規則把訊息送到不同處理路徑">routing rule&lt;/a> 或 stream 模型把事件交給對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Topic 是事件分流的命名邊界。它讓訂單、付款、會員、通知、庫存等事件可以被不同服務訂閱，也讓團隊用事件種類思考資料流與責任範圍。&lt;/p>
&lt;p>Topic 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 的關係是：topic 是邏輯命名空間，partition 是 topic 內的物理分片。Topic 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a> 的關係是：多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 訂閱同一個 topic，每個 group 各自消費全量事件，實現 fan-out。&lt;/p>
&lt;p>在 RabbitMQ 生態中，topic 對應 exchange + routing key 的組合；在 NATS 中 topic 對應 subject。概念相同但術語跟語意細節不同。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 topic 設計的訊號是同一個事件來源會供多個 downstream 使用。付款完成事件可以給出貨、通知、報表與風控使用；所有事件都混在同一條 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 時，consumer 會承擔更多過濾與相容性成本。&lt;/p>
&lt;p>Topic 命名規則影響長期治理。&lt;code>orders.payment.completed&lt;/code> 比 &lt;code>event_1&lt;/code> 更容易被搜尋跟管理。命名規則要在團隊間統一、進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-contract/" data-link-title="Queue Contract" data-link-desc="說明佇列工作在重試、確認與重複投遞上的約定">queue contract&lt;/a> 管理。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Topic 設計要定義命名規則、事件 schema、相容性策略（schema evolution）、權限控制（誰能 publish / subscribe）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 期限、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a> 範圍與 ownership（哪個團隊負責這個 topic）。操作面要能依 topic 查看 publish rate、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、錯誤率與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a> 數量。&lt;/p></description><content:encoded><![CDATA[<p>Topic 的核心概念是「用主題名稱描述一類事件或訊息」。<a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">Producer</a> 把事件發布到 topic，<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 再依照訂閱關係、<a href="/blog/backend/knowledge-cards/routing-rule/" data-link-title="Routing Rule" data-link-desc="說明訊息系統如何依規則把訊息送到不同處理路徑">routing rule</a> 或 stream 模型把事件交給對應 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Topic 是事件分流的命名邊界。它讓訂單、付款、會員、通知、庫存等事件可以被不同服務訂閱，也讓團隊用事件種類思考資料流與責任範圍。</p>
<p>Topic 跟 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 的關係是：topic 是邏輯命名空間，partition 是 topic 內的物理分片。Topic 跟 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 的關係是：多個 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 訂閱同一個 topic，每個 group 各自消費全量事件，實現 fan-out。</p>
<p>在 RabbitMQ 生態中，topic 對應 exchange + routing key 的組合；在 NATS 中 topic 對應 subject。概念相同但術語跟語意細節不同。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 topic 設計的訊號是同一個事件來源會供多個 downstream 使用。付款完成事件可以給出貨、通知、報表與風控使用；所有事件都混在同一條 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 時，consumer 會承擔更多過濾與相容性成本。</p>
<p>Topic 命名規則影響長期治理。<code>orders.payment.completed</code> 比 <code>event_1</code> 更容易被搜尋跟管理。命名規則要在團隊間統一、進 <a href="/blog/backend/knowledge-cards/queue-contract/" data-link-title="Queue Contract" data-link-desc="說明佇列工作在重試、確認與重複投遞上的約定">queue contract</a> 管理。</p>
<h2 id="設計責任">設計責任</h2>
<p>Topic 設計要定義命名規則、事件 schema、相容性策略（schema evolution）、權限控制（誰能 publish / subscribe）、<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 期限、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a> 範圍與 ownership（哪個團隊負責這個 topic）。操作面要能依 topic 查看 publish rate、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、錯誤率與 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a> 數量。</p>
]]></content:encoded></item><item><title>Trace</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace/</guid><description>&lt;p>Trace 的核心概念是「把一次 request 或工作流程拆成可關聯的多段執行紀錄」。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context&lt;/a> 串起整條路徑，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 記錄每一段工作，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 能回到同一條流程。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace 是跨服務診斷的路徑層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>（事件層）和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>（趨勢層）互補。Log 回答「某個服務發生了什麼」；metrics 回答「服務的健康趨勢」；trace 回答「一次 request 跨服務時，時間花在哪、錯誤發生在哪一段」。&lt;/p>
&lt;p>Trace 在 waterfall view 中呈現為時間軸上的巢狀條狀圖，root span 在最上面、child span 依序往下。診斷價值是一眼看出延遲瓶頸 — checkout 總延遲 800ms 中 payment span 佔 600ms，問題定位立刻縮小範圍。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 trace 的訊號是單一服務的 log 只呈現局部。Checkout 變慢時，trace 可以顯示時間主要花在庫存查詢、付款 API、database lock 或通知 worker。跨服務錯誤（upstream 回 500 但不知道是哪個 downstream 引起的）也依賴 trace 定位。&lt;/p>
&lt;p>Trace 聚合後可以自動生成 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service topology&lt;/a> — 哪些服務在呼叫哪些服務、call 頻率、延遲分布、錯誤率。這個 graph 反映實際流量而非設計文件。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace 設計要處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 傳遞（HTTP header、queue message header、thread context）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略（head / tail / adaptive）、span 命名慣例、敏感資料 redaction、跨語言 SDK 相容性與 log correlation（trace id 寫進 log 欄位）。&lt;/p>
&lt;p>高流量服務需要控制採樣成本，同時保留錯誤與高延遲樣本。Sampling 策略的完整討論見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>。Context propagation 在不同邊界（HTTP / queue / thread pool / background job）的斷鏈風險與修復見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Trace 的核心概念是「把一次 request 或工作流程拆成可關聯的多段執行紀錄」。<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context</a> 串起整條路徑，<a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 記錄每一段工作，<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 讓 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 能回到同一條流程。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace 是跨服務診斷的路徑層，跟 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>（事件層）和 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>（趨勢層）互補。Log 回答「某個服務發生了什麼」；metrics 回答「服務的健康趨勢」；trace 回答「一次 request 跨服務時，時間花在哪、錯誤發生在哪一段」。</p>
<p>Trace 在 waterfall view 中呈現為時間軸上的巢狀條狀圖，root span 在最上面、child span 依序往下。診斷價值是一眼看出延遲瓶頸 — checkout 總延遲 800ms 中 payment span 佔 600ms，問題定位立刻縮小範圍。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 trace 的訊號是單一服務的 log 只呈現局部。Checkout 變慢時，trace 可以顯示時間主要花在庫存查詢、付款 API、database lock 或通知 worker。跨服務錯誤（upstream 回 500 但不知道是哪個 downstream 引起的）也依賴 trace 定位。</p>
<p>Trace 聚合後可以自動生成 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service topology</a> — 哪些服務在呼叫哪些服務、call 頻率、延遲分布、錯誤率。這個 graph 反映實際流量而非設計文件。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace 設計要處理 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 傳遞（HTTP header、queue message header、thread context）、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略（head / tail / adaptive）、span 命名慣例、敏感資料 redaction、跨語言 SDK 相容性與 log correlation（trace id 寫進 log 欄位）。</p>
<p>高流量服務需要控制採樣成本，同時保留錯誤與高延遲樣本。Sampling 策略的完整討論見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>。Context propagation 在不同邊界（HTTP / queue / thread pool / background job）的斷鏈風險與修復見 <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</a>。</p>
]]></content:encoded></item><item><title>Dashboard</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/</guid><description>&lt;p>Dashboard 的核心概念是「把多個觀測訊號組成可判讀的服務狀態畫面」。它讓團隊用同一個視角查看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a>、latency、error rate、traffic、saturation、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 與下游依賴狀態。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Dashboard 是告警與排障之間的判讀層。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a> 告訴團隊需要注意，dashboard 幫團隊判斷影響範圍、變化趨勢與可能原因，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 則把判讀結果轉成處理步驟。&lt;/p>
&lt;p>Dashboard 分層服務不同使用者：service overview 給 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師、debug dashboard 給事故中的深入診斷、capacity dashboard 給容量規劃。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 dashboard 的訊號是事故中需要快速回答「影響多大、從何時開始、哪個依賴異常」。Dashboard 也是日常巡檢的入口 — on-call 工程師每天先看 service overview 確認服務健康，再處理 alert queue。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Dashboard 設計要服務具體決策。每個面板應對應一個可回答的問題（「服務現在健康嗎」「延遲瓶頸在哪」「容量還夠嗎」）。高 cardinality、缺少單位或只呈現低層資源的圖表會增加判讀成本而非降低。&lt;/p>
&lt;p>Dashboard panel 的查詢效能影響使用體驗 — 長時間趨勢 panel 應讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 資料，避免每次刷新都掃描 raw series。Dashboard / alert 的完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Dashboard 的核心概念是「把多個觀測訊號組成可判讀的服務狀態畫面」。它讓團隊用同一個視角查看 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a>、latency、error rate、traffic、saturation、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 與下游依賴狀態。</p>
<h2 id="概念位置">概念位置</h2>
<p>Dashboard 是告警與排障之間的判讀層。<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a> 告訴團隊需要注意，dashboard 幫團隊判斷影響範圍、變化趨勢與可能原因，<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 則把判讀結果轉成處理步驟。</p>
<p>Dashboard 分層服務不同使用者：service overview 給 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師、debug dashboard 給事故中的深入診斷、capacity dashboard 給容量規劃。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 dashboard 的訊號是事故中需要快速回答「影響多大、從何時開始、哪個依賴異常」。Dashboard 也是日常巡檢的入口 — on-call 工程師每天先看 service overview 確認服務健康，再處理 alert queue。</p>
<h2 id="設計責任">設計責任</h2>
<p>Dashboard 設計要服務具體決策。每個面板應對應一個可回答的問題（「服務現在健康嗎」「延遲瓶頸在哪」「容量還夠嗎」）。高 cardinality、缺少單位或只呈現低層資源的圖表會增加判讀成本而非降低。</p>
<p>Dashboard panel 的查詢效能影響使用體驗 — 長時間趨勢 panel 應讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 資料，避免每次刷新都掃描 raw series。Dashboard / alert 的完整設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>。</p>
]]></content:encoded></item><item><title>Fan-out</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/</guid><description>&lt;p>Fan-out 的核心概念是「一個事件被多個訂閱者各自獨立處理」。它讓單一 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> 發布一次事件，多個下游各自消費、各自處理、各自管理進度跟錯誤。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Fan-out 常搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub&lt;/a> 模型、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 實作。在 Kafka 中，多個 consumer group 訂閱同一個 topic 就是 fan-out — 每個 group 各自從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 0 開始消費。在 RabbitMQ 中，fanout exchange 把訊息複製到所有綁定的 queue。在 GCP Pub/Sub 中，多個 subscription 訂閱同一個 topic。&lt;/p>
&lt;p>Fan-out 跟 fan-in（多個來源合併成一個流）是相反的拓撲。兩者可以組合成事件處理管線。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>&lt;code>order.paid&lt;/code> 事件同時觸發出貨準備（物流服務）、交易通知（通知服務）、營收紀錄（報表服務）與風控評估（風控服務）。Producer 不需要知道有哪些 consumer — 加減 consumer 不影響 producer 的程式碼。&lt;/p>
&lt;p>Fan-out 降低了 producer 跟 consumer 之間的耦合，但擴大了排障範圍 — 一筆事件的處理結果散落在多個 consumer，需要用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 或 correlation id 串連。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 fan-out 時要為每個訂閱者定義可靠性等級跟回復策略。通知服務短暫失敗可以 retry；報表服務落後可以批次追補；但出貨服務的失敗可能需要人工介入。把所有下游綁成同一個失敗域（一個 consumer 卡住就全部暫停）會讓 fan-out 的解耦價值消失。每個 consumer group 應該獨立管理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Fan-out 的核心概念是「一個事件被多個訂閱者各自獨立處理」。它讓單一 <a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 發布一次事件，多個下游各自消費、各自處理、各自管理進度跟錯誤。</p>
<h2 id="概念位置">概念位置</h2>
<p>Fan-out 常搭配 <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 模型、<a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 跟 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 實作。在 Kafka 中，多個 consumer group 訂閱同一個 topic 就是 fan-out — 每個 group 各自從 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 0 開始消費。在 RabbitMQ 中，fanout exchange 把訊息複製到所有綁定的 queue。在 GCP Pub/Sub 中，多個 subscription 訂閱同一個 topic。</p>
<p>Fan-out 跟 fan-in（多個來源合併成一個流）是相反的拓撲。兩者可以組合成事件處理管線。</p>
<h2 id="使用情境">使用情境</h2>
<p><code>order.paid</code> 事件同時觸發出貨準備（物流服務）、交易通知（通知服務）、營收紀錄（報表服務）與風控評估（風控服務）。Producer 不需要知道有哪些 consumer — 加減 consumer 不影響 producer 的程式碼。</p>
<p>Fan-out 降低了 producer 跟 consumer 之間的耦合，但擴大了排障範圍 — 一筆事件的處理結果散落在多個 consumer，需要用 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 或 correlation id 串連。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 fan-out 時要為每個訂閱者定義可靠性等級跟回復策略。通知服務短暫失敗可以 retry；報表服務落後可以批次追補；但出貨服務的失敗可能需要人工介入。把所有下游綁成同一個失敗域（一個 consumer 卡住就全部暫停）會讓 fan-out 的解耦價值消失。每個 consumer group 應該獨立管理 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a> 跟 <a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a>。</p>
]]></content:encoded></item><item><title>Validation Query</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/</guid><description>&lt;p>Validation query 的核心概念是「用可重跑查詢證明資料語意是否符合遷移規則」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">correctness check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a>，讓資料變更不只靠 job log 或人工抽樣判斷。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Validation query 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 之間。Correctness check 定義要驗什麼，validation query 則把規則落成可查、可保存、可交接的證據。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 validation query 的訊號是：&lt;/p>
&lt;ul>
&lt;li>新舊欄位或新舊資料模型會並存一段時間&lt;/li>
&lt;li>backfill job 顯示完成，但仍需要證明資料語意正確&lt;/li>
&lt;li>cutover 前要知道 mismatch 集中在哪些資料範圍&lt;/li>
&lt;li>事故修復後要留下可回放的資料證據&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>訂單服務把 &lt;code>status&lt;/code> 裡的付款語意拆到 &lt;code>payment_state&lt;/code> 時，validation query 可以比對每批訂單的新舊語意、缺值筆數、mismatch sample 與 replication lag 對位。這些結果會進入 release gate，而不是只停在 migration job 的成功訊息。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Validation query 要保留 query version、time range、資料範圍、mismatch 分類與 owner。它的目標是支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 判讀，讓團隊能知道下一步是繼續、暫停、回退讀取，還是做資料修補。&lt;/p></description><content:encoded><![CDATA[<p>Validation query 的核心概念是「用可重跑查詢證明資料語意是否符合遷移規則」。它連接 <a href="/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">correctness check</a>、<a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill</a> 與 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a>，讓資料變更不只靠 job log 或人工抽樣判斷。</p>
<h2 id="概念位置">概念位置</h2>
<p>Validation query 位在 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation</a> 與 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 之間。Correctness check 定義要驗什麼，validation query 則把規則落成可查、可保存、可交接的證據。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 validation query 的訊號是：</p>
<ul>
<li>新舊欄位或新舊資料模型會並存一段時間</li>
<li>backfill job 顯示完成，但仍需要證明資料語意正確</li>
<li>cutover 前要知道 mismatch 集中在哪些資料範圍</li>
<li>事故修復後要留下可回放的資料證據</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>訂單服務把 <code>status</code> 裡的付款語意拆到 <code>payment_state</code> 時，validation query 可以比對每批訂單的新舊語意、缺值筆數、mismatch sample 與 replication lag 對位。這些結果會進入 release gate，而不是只停在 migration job 的成功訊息。</p>
<h2 id="設計責任">設計責任</h2>
<p>Validation query 要保留 query version、time range、資料範圍、mismatch 分類與 owner。它的目標是支援 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 判讀，讓團隊能知道下一步是繼續、暫停、回退讀取，還是做資料修補。</p>
]]></content:encoded></item><item><title>Alert</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/alert/</guid><description>&lt;p>Alert 的核心概念是「把需要人或自動流程處理的服務症狀轉成通知」。好的 alert 連到產品影響、判斷條件、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與升級流程。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Alert 是可觀測性進入操作流程的入口。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert&lt;/a> 優先偵測使用者可感知結果（error rate、latency p99）；cause-based alert 偵測內部原因（CPU、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>）。Symptom-based 用於 page on-call、cause-based 用於 warning 級通知。&lt;/p>
&lt;p>Alert 觸發後由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師承接，按 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 的步驟診斷跟處理。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 alert 設計的訊號是服務異常需要在使用者大量回報前被發現跟處理。付款成功率下降、API availability 低於 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 持續擴大或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 快速增加，都應觸發可行動通知。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Alert 設計要定義門檻、持續時間（&lt;code>for&lt;/code> duration）、severity、通知對象、抑制規則、runbook link 與回復條件。每個 alert rule 帶 owner metadata — 沒有 owner 的 alert 會在服務演進後退化成 noise 來源，形成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a>。&lt;/p>
&lt;p>SLO-based alerting 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 取代固定閾值，自動適應流量變化。完整的 alert 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>、SLO-based alerting 見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Alert 的核心概念是「把需要人或自動流程處理的服務症狀轉成通知」。好的 alert 連到產品影響、判斷條件、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與升級流程。</p>
<h2 id="概念位置">概念位置</h2>
<p>Alert 是可觀測性進入操作流程的入口。<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert</a> 優先偵測使用者可感知結果（error rate、latency p99）；cause-based alert 偵測內部原因（CPU、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a>）。Symptom-based 用於 page on-call、cause-based 用於 warning 級通知。</p>
<p>Alert 觸發後由 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師承接，按 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 的步驟診斷跟處理。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 alert 設計的訊號是服務異常需要在使用者大量回報前被發現跟處理。付款成功率下降、API availability 低於 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 持續擴大或 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 快速增加，都應觸發可行動通知。</p>
<h2 id="設計責任">設計責任</h2>
<p>Alert 設計要定義門檻、持續時間（<code>for</code> duration）、severity、通知對象、抑制規則、runbook link 與回復條件。每個 alert rule 帶 owner metadata — 沒有 owner 的 alert 會在服務演進後退化成 noise 來源，形成 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a>。</p>
<p>SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 取代固定閾值，自動適應流量變化。完整的 alert 設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>、SLO-based alerting 見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a>。</p>
]]></content:encoded></item><item><title>Read Compatibility</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/read-compatibility/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/read-compatibility/</guid><description>&lt;p>Read compatibility 的核心概念是「讀取路徑在過渡期同時理解新舊資料語意」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a>，讓新欄位或新資料模型可以先進入 production，再逐步切換讀取權。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Read compatibility 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 之間。雙寫處理寫入一致性，read compatibility 處理讀取方如何在缺值、延遲回填或版本混跑時仍能給出一致判讀。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 read compatibility 的訊號是：&lt;/p>
&lt;ul>
&lt;li>新欄位已新增，但歷史資料尚未全部 backfill&lt;/li>
&lt;li>新舊程式版本會同時服務流量&lt;/li>
&lt;li>rollback 後舊版本仍需要讀懂 production 資料&lt;/li>
&lt;li>內部後台、對帳或報表的切換節奏不同於使用者可見路徑&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>訂單服務新增 &lt;code>payment_state&lt;/code> 後，讀取時可先看新欄位，缺值時回到舊 &lt;code>status&lt;/code> 的付款語意。客服後台可以先用這條相容讀取路徑驗證資料，再逐步讓使用者可見查詢改用新欄位。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Read compatibility 要定義讀取優先順序、fallback read 條件、資料新鮮度限制與停止條件。它要搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a>，避免 cutover 後才發現舊版本或長尾讀取路徑無法判讀資料。&lt;/p></description><content:encoded><![CDATA[<p>Read compatibility 的核心概念是「讀取路徑在過渡期同時理解新舊資料語意」。它連接 <a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a>、<a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a> 與 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a>，讓新欄位或新資料模型可以先進入 production，再逐步切換讀取權。</p>
<h2 id="概念位置">概念位置</h2>
<p>Read compatibility 位在 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a>、<a href="/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover</a> 與 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 之間。雙寫處理寫入一致性，read compatibility 處理讀取方如何在缺值、延遲回填或版本混跑時仍能給出一致判讀。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 read compatibility 的訊號是：</p>
<ul>
<li>新欄位已新增，但歷史資料尚未全部 backfill</li>
<li>新舊程式版本會同時服務流量</li>
<li>rollback 後舊版本仍需要讀懂 production 資料</li>
<li>內部後台、對帳或報表的切換節奏不同於使用者可見路徑</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>訂單服務新增 <code>payment_state</code> 後，讀取時可先看新欄位，缺值時回到舊 <code>status</code> 的付款語意。客服後台可以先用這條相容讀取路徑驗證資料，再逐步讓使用者可見查詢改用新欄位。</p>
<h2 id="設計責任">設計責任</h2>
<p>Read compatibility 要定義讀取優先順序、fallback read 條件、資料新鮮度限制與停止條件。它要搭配 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 與 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a>，避免 cutover 後才發現舊版本或長尾讀取路徑無法判讀資料。</p>
]]></content:encoded></item><item><title>Runbook</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/</guid><description>&lt;p>Runbook 的核心概念是「把事故判斷與操作步驟標準化」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的行動指南，描述 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師看到特定訊號時如何確認影響、查哪些資料、採取哪些緩解、何時升級，以及如何驗證恢復。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Runbook 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的行動指南。Alert 告訴 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師有問題，runbook 告訴他們「收到這個 alert 時該做什麼」。每個 critical alert 應該連到一份 runbook — 缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 的起點。&lt;/p>
&lt;p>Runbook 也服務於 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> — 事故中實際執行的步驟跟 runbook 預設的步驟比較，差異就是 runbook 需要更新的地方。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 runbook 的訊號是同一類事故每次都靠個人經驗處理。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 快速增加時，runbook 應引導處理者查看錯誤分類、payload 範圍、最近部署、replay 條件與暫停 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 的判斷。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Runbook 的有效結構：症狀描述、影響評估、診斷步驟（先看哪個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、查哪些 log）、可能的修復動作（restart / scale / rollback / failover）、升級路徑（15 分鐘內無法解決時通知誰）。維護責任跟 alert 的 owner 一致 — alert rule 改了但 runbook 沒更新是常見的退化。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Runbook 的核心概念是「把事故判斷與操作步驟標準化」。它是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的行動指南，描述 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師看到特定訊號時如何確認影響、查哪些資料、採取哪些緩解、何時升級，以及如何驗證恢復。</p>
<h2 id="概念位置">概念位置</h2>
<p>Runbook 是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的行動指南。Alert 告訴 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師有問題，runbook 告訴他們「收到這個 alert 時該做什麼」。每個 critical alert 應該連到一份 runbook — 缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」，是 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的起點。</p>
<p>Runbook 也服務於 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> — 事故中實際執行的步驟跟 runbook 預設的步驟比較，差異就是 runbook 需要更新的地方。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 runbook 的訊號是同一類事故每次都靠個人經驗處理。<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 快速增加時，runbook 應引導處理者查看錯誤分類、payload 範圍、最近部署、replay 條件與暫停 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 的判斷。</p>
<h2 id="設計責任">設計責任</h2>
<p>Runbook 的有效結構：症狀描述、影響評估、診斷步驟（先看哪個 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、查哪些 log）、可能的修復動作（restart / scale / rollback / failover）、升級路徑（15 分鐘內無法解決時通知誰）。維護責任跟 alert 的 owner 一致 — alert rule 改了但 runbook 沒更新是常見的退化。完整設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>。</p>
]]></content:encoded></item><item><title>Fallback Read</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-read/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-read/</guid><description>&lt;p>Fallback read 的核心概念是「新讀取路徑尚未穩定時，暫時回到舊資料語意或舊讀取來源」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-compatibility/" data-link-title="Read Compatibility" data-link-desc="說明資料或服務演進期間讀取路徑如何同時支援新舊語意">read compatibility&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window&lt;/a>，讓 cutover 失敗時可以先限制在讀取判讀層。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Fallback read 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a> 之間。它保留新資料結構、暫時把讀取判斷交回舊語意或舊來源，比完整 rollback 成本低且破壞性小。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 fallback read 的訊號是：&lt;/p>
&lt;ul>
&lt;li>新欄位讀取後 mismatch 升高&lt;/li>
&lt;li>客服後台、報表或使用者可見查詢結果漂移&lt;/li>
&lt;li>寫入路徑已經收斂，但讀取模型或索引尚未穩定&lt;/li>
&lt;li>release gate 允許暫停 cutover，但尚未需要資料修補&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>訂單服務把付款狀態拆到 &lt;code>payment_state&lt;/code> 後，客服後台若發現新欄位判讀 mismatch 升高，可以先回到舊 &lt;code>status&lt;/code> 的付款語意讀取，讓客服分類回到基線，同時保留 backfill 與 validation query 繼續查證。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Fallback read 要定義觸發條件、讀取優先順序、可維持多久、哪些入口適用，以及何時重新嘗試 cutover。它要與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 對齊，避免讀取回退變成沒有證據的永久分岔。&lt;/p></description><content:encoded><![CDATA[<p>Fallback read 的核心概念是「新讀取路徑尚未穩定時，暫時回到舊資料語意或舊讀取來源」。它連接 <a href="/blog/backend/knowledge-cards/read-compatibility/" data-link-title="Read Compatibility" data-link-desc="說明資料或服務演進期間讀取路徑如何同時支援新舊語意">read compatibility</a>、<a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window</a>，讓 cutover 失敗時可以先限制在讀取判讀層。</p>
<h2 id="概念位置">概念位置</h2>
<p>Fallback read 位在 <a href="/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover</a>、<a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a> 與 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 之間。它保留新資料結構、暫時把讀取判斷交回舊語意或舊來源，比完整 rollback 成本低且破壞性小。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 fallback read 的訊號是：</p>
<ul>
<li>新欄位讀取後 mismatch 升高</li>
<li>客服後台、報表或使用者可見查詢結果漂移</li>
<li>寫入路徑已經收斂，但讀取模型或索引尚未穩定</li>
<li>release gate 允許暫停 cutover，但尚未需要資料修補</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>訂單服務把付款狀態拆到 <code>payment_state</code> 後，客服後台若發現新欄位判讀 mismatch 升高，可以先回到舊 <code>status</code> 的付款語意讀取，讓客服分類回到基線，同時保留 backfill 與 validation query 繼續查證。</p>
<h2 id="設計責任">設計責任</h2>
<p>Fallback read 要定義觸發條件、讀取優先順序、可維持多久、哪些入口適用，以及何時重新嘗試 cutover。它要與 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 和 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 對齊，避免讀取回退變成沒有證據的永久分岔。</p>
]]></content:encoded></item><item><title>Cutover Window</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-window/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-window/</guid><description>&lt;p>Cutover window 的核心概念是「正式切換發生並被密集觀察的時間與條件範圍」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window&lt;/a>，讓切換成為一段可停止、可判讀的窗口，脫離瞬間按鈕的思維。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Cutover window 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 之間。Release gate 決定能否開始切換，cutover window 定義切換後多久內要看哪些訊號、達到什麼條件才算穩定。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 cutover window 的訊號是：&lt;/p>
&lt;ul>
&lt;li>新路徑開始承接正式讀取或寫入&lt;/li>
&lt;li>切換後需要觀察 mismatch、latency、error rate 或 lag&lt;/li>
&lt;li>回退條件只在切換初期仍然低成本&lt;/li>
&lt;li>多個入口會分批切換，需要分別記錄時間窗&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>客服後台先切到新 &lt;code>payment_state&lt;/code> 讀取後，前 30 分鐘是 cutover window。這段期間要看 mismatch sample、客服查詢慢查詢、對帳補償量與 rollback window；穩定後才放行使用者可見讀取。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Cutover window 要定義開始時間、觀察長度、通過條件、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition&lt;/a> 與 owner。它應進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>，讓事後能回放切換當時的訊號。&lt;/p></description><content:encoded><![CDATA[<p>Cutover window 的核心概念是「正式切換發生並被密集觀察的時間與條件範圍」。它連接 <a href="/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover</a>、<a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback-window</a>，讓切換成為一段可停止、可判讀的窗口，脫離瞬間按鈕的思維。</p>
<h2 id="概念位置">概念位置</h2>
<p>Cutover window 位在 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a>、<a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 與 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 之間。Release gate 決定能否開始切換，cutover window 定義切換後多久內要看哪些訊號、達到什麼條件才算穩定。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 cutover window 的訊號是：</p>
<ul>
<li>新路徑開始承接正式讀取或寫入</li>
<li>切換後需要觀察 mismatch、latency、error rate 或 lag</li>
<li>回退條件只在切換初期仍然低成本</li>
<li>多個入口會分批切換，需要分別記錄時間窗</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>客服後台先切到新 <code>payment_state</code> 讀取後，前 30 分鐘是 cutover window。這段期間要看 mismatch sample、客服查詢慢查詢、對帳補償量與 rollback window；穩定後才放行使用者可見讀取。</p>
<h2 id="設計責任">設計責任</h2>
<p>Cutover window 要定義開始時間、觀察長度、通過條件、<a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 與 owner。它應進入 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，讓事後能回放切換當時的訊號。</p>
]]></content:encoded></item><item><title>Event Log</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/</guid><description>&lt;p>Event log 按時間保存已發生事件的不可變紀錄，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 的儲存層。每一筆事件記錄一次狀態變更，整條事件流構成完整的變更歷史。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Event log 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 的儲存層。在 event sourcing 架構中，event log 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，current state 透過 replay 事件流推算。在非 event sourcing 架構中，event log 是輔助紀錄 — 正式狀態仍由 mutable record 承擔，event log 提供變更歷史跟 replay 能力。&lt;/p>
&lt;p>Event log 的讀取面透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 轉換成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a>，讓消費者不需要每次 replay 整條事件流。在訊息傳遞面，event log 常搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook&lt;/a> 使用。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>訂單狀態變更可寫入 event log，後續由報表、通知、稽核服務各自消費。當下游落後時，可用 replay 補齊資料。金融帳務的每一筆增減、權限變更的每一次授權與撤銷、訂閱方案的每一次升降級，都是典型的 event log 應用。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計時要定義事件 schema 演進（新版 consumer 要能消費舊版事件）、保留期限（無限保留 vs retention-based 清理）、重播邊界（從哪個 offset 開始 replay）與去重策略（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 保證）。Event log 的儲存成長是長期成本 — 高頻寫入的系統需要 snapshot 機制或 retention 策略來控制。&lt;/p></description><content:encoded><![CDATA[<p>Event log 按時間保存已發生事件的不可變紀錄，是 <a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 的儲存層。每一筆事件記錄一次狀態變更，整條事件流構成完整的變更歷史。</p>
<h2 id="概念位置">概念位置</h2>
<p>Event log 是 <a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 的儲存層。在 event sourcing 架構中，event log 是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，current state 透過 replay 事件流推算。在非 event sourcing 架構中，event log 是輔助紀錄 — 正式狀態仍由 mutable record 承擔，event log 提供變更歷史跟 replay 能力。</p>
<p>Event log 的讀取面透過 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 轉換成 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>，讓消費者不需要每次 replay 整條事件流。在訊息傳遞面，event log 常搭配 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a>、<a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 與 <a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay runbook</a> 使用。</p>
<h2 id="使用情境">使用情境</h2>
<p>訂單狀態變更可寫入 event log，後續由報表、通知、稽核服務各自消費。當下游落後時，可用 replay 補齊資料。金融帳務的每一筆增減、權限變更的每一次授權與撤銷、訂閱方案的每一次升降級，都是典型的 event log 應用。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計時要定義事件 schema 演進（新版 consumer 要能消費舊版事件）、保留期限（無限保留 vs retention-based 清理）、重播邊界（從哪個 offset 開始 replay）與去重策略（<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 保證）。Event log 的儲存成長是長期成本 — 高頻寫入的系統需要 snapshot 機制或 retention 策略來控制。</p>
]]></content:encoded></item><item><title>Mapping Table</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/mapping-table/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/mapping-table/</guid><description>&lt;p>Mapping table 的核心概念是「把舊資料語意明確對應到新資料語意」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">correctness check&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation-query&lt;/a>，讓轉換規則成為可查證 artifact，而不是工程師腦中的口頭規則。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Mapping table 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 之間。Backfill 依它轉換資料，validation query 依它判斷 mismatch，incident decision log 則依它追溯當時的判讀依據。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 mapping table 的訊號是：&lt;/p>
&lt;ul>
&lt;li>舊欄位混合多種業務語意，需要拆到新欄位&lt;/li>
&lt;li>多個舊狀態會對應到同一個新狀態&lt;/li>
&lt;li>某些舊狀態需要人工確認或例外處理&lt;/li>
&lt;li>事後要能解釋 mismatch 是資料錯誤還是轉換規則錯誤&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>訂單服務把 &lt;code>pending_payment&lt;/code>、&lt;code>paid&lt;/code>、&lt;code>payment_failed&lt;/code>、&lt;code>refunded&lt;/code> 對應到 &lt;code>payment_state&lt;/code> 的 &lt;code>pending&lt;/code>、&lt;code>captured&lt;/code>、&lt;code>failed&lt;/code>、&lt;code>refunded&lt;/code>。這張 mapping table 同時支撐 backfill job、validation query 與 cutover gate。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Mapping table 要保留來源欄位、新欄位、對應理由、例外狀態與 owner。高風險 mapping 要版本化，並進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>；否則資料漂移時，團隊很難判斷問題出在資料、程式還是規則本身。&lt;/p></description><content:encoded><![CDATA[<p>Mapping table 的核心概念是「把舊資料語意明確對應到新資料語意」。它連接 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">correctness check</a> 與 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation-query</a>，讓轉換規則成為可查證 artifact，而不是工程師腦中的口頭規則。</p>
<h2 id="概念位置">概念位置</h2>
<p>Mapping table 位在 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill</a>、<a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">data reconciliation</a> 與 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 之間。Backfill 依它轉換資料，validation query 依它判斷 mismatch，incident decision log 則依它追溯當時的判讀依據。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 mapping table 的訊號是：</p>
<ul>
<li>舊欄位混合多種業務語意，需要拆到新欄位</li>
<li>多個舊狀態會對應到同一個新狀態</li>
<li>某些舊狀態需要人工確認或例外處理</li>
<li>事後要能解釋 mismatch 是資料錯誤還是轉換規則錯誤</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>訂單服務把 <code>pending_payment</code>、<code>paid</code>、<code>payment_failed</code>、<code>refunded</code> 對應到 <code>payment_state</code> 的 <code>pending</code>、<code>captured</code>、<code>failed</code>、<code>refunded</code>。這張 mapping table 同時支撐 backfill job、validation query 與 cutover gate。</p>
<h2 id="設計責任">設計責任</h2>
<p>Mapping table 要保留來源欄位、新欄位、對應理由、例外狀態與 owner。高風險 mapping 要版本化，並進入 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>；否則資料漂移時，團隊很難判斷問題出在資料、程式還是規則本身。</p>
]]></content:encoded></item><item><title>Search Index</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/</guid><description>&lt;p>Search index 的核心概念是「為查詢體驗建立專用的讀取模型」。它擅長全文搜尋、排序、filter 與 facet，通常是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">derived state&lt;/a>、從正式資料源同步而來。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Search index 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作。正式狀態仍由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 管理（relational DB、document DB），search index 透過 CDC、event subscription 或 ETL 同步更新。概念上跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a> 類似 — 都是為特定查詢需求預先準備的資料形狀。&lt;/p>
&lt;p>在觀測領域，log storage 的 search index（Elasticsearch / Loki 的 label index）承擔 log 查詢的效能。Index 的欄位選擇跟 cardinality 影響查詢延遲跟儲存成本，見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>商品搜尋、文件站搜尋、客服多條件檢索、log 查詢通常都需要 search index 提供低延遲查詢體驗。Elasticsearch、Algolia、Meilisearch、Typesense 是常見實作。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計時要定義索引更新延遲（source 變更到 index 可查的時間）、重建流程（index 損壞或 schema 改版時的 full reindex）、查詢語意（全文 vs 結構化 filter）與權限過濾（search 結果是否要按使用者權限過濾）。Index 是 derived state — 修復方式是 rebuild 而非直接修改。&lt;/p></description><content:encoded><![CDATA[<p>Search index 的核心概念是「為查詢體驗建立專用的讀取模型」。它擅長全文搜尋、排序、filter 與 facet，通常是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">derived state</a>、從正式資料源同步而來。</p>
<h2 id="概念位置">概念位置</h2>
<p>Search index 是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作。正式狀態仍由 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 管理（relational DB、document DB），search index 透過 CDC、event subscription 或 ETL 同步更新。概念上跟 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 類似 — 都是為特定查詢需求預先準備的資料形狀。</p>
<p>在觀測領域，log storage 的 search index（Elasticsearch / Loki 的 label index）承擔 log 查詢的效能。Index 的欄位選擇跟 cardinality 影響查詢延遲跟儲存成本，見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>商品搜尋、文件站搜尋、客服多條件檢索、log 查詢通常都需要 search index 提供低延遲查詢體驗。Elasticsearch、Algolia、Meilisearch、Typesense 是常見實作。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計時要定義索引更新延遲（source 變更到 index 可查的時間）、重建流程（index 損壞或 schema 改版時的 full reindex）、查詢語意（全文 vs 結構化 filter）與權限過濾（search 結果是否要按使用者權限過濾）。Index 是 derived state — 修復方式是 rebuild 而非直接修改。</p>
]]></content:encoded></item><item><title>Read Model</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/</guid><description>&lt;p>Read model 的核心概念是「為特定查詢需求建立專用的資料形狀」。它跟正式狀態（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>）的責任分離 — 正式狀態為寫入的正確性最佳化，read model 為讀取的效率與體驗最佳化。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Read model 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的讀取面產物。在 CQRS 架構中，write model 跟 read model 各自獨立，read model 透過同步機制（event handler、CDC、定期刷新）從 write model 更新。&lt;/p>
&lt;p>Read model 的來源可以是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a>（從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 持續推算）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a>（從 SQL 查詢預計算）、CDC consumer（從 row change 同步到搜尋索引）或批次 ETL（定期從 OLTP 匯出到 analytics store）。不同的來源機制有不同的更新延遲跟維護成本。&lt;/p>
&lt;p>在觀測領域，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 扮演類似 read model 的角色 — 從 raw time series 預計算聚合結果，讓 dashboard 讀取預聚合資料而非重算 raw data。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 read model 時要定義同步延遲（read model 落後 write model 多久可以接受）、重建流程（read model 損壞或 schema 變更時如何從頭重建）、欄位語意（read model 的欄位定義跟 write model 可能不同）與查詢邊界（這個 read model 能回答什麼問題、不能回答什麼問題）。&lt;/p>
&lt;p>Read model 是派生狀態，修復方式是「砍掉重建」而非直接修改。把 read model 當正式狀態修改會導致 write model 跟 read model 分岔、後續同步覆蓋修改。&lt;/p></description><content:encoded><![CDATA[<p>Read model 的核心概念是「為特定查詢需求建立專用的資料形狀」。它跟正式狀態（<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>）的責任分離 — 正式狀態為寫入的正確性最佳化，read model 為讀取的效率與體驗最佳化。</p>
<h2 id="概念位置">概念位置</h2>
<p>Read model 是 <a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的讀取面產物。在 CQRS 架構中，write model 跟 read model 各自獨立，read model 透過同步機制（event handler、CDC、定期刷新）從 write model 更新。</p>
<p>Read model 的來源可以是 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a>（從 <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 持續推算）、<a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a>（從 SQL 查詢預計算）、CDC consumer（從 row change 同步到搜尋索引）或批次 ETL（定期從 OLTP 匯出到 analytics store）。不同的來源機制有不同的更新延遲跟維護成本。</p>
<p>在觀測領域，<a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 跟 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 扮演類似 read model 的角色 — 從 raw time series 預計算聚合結果，讓 dashboard 讀取預聚合資料而非重算 raw data。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 read model 時要定義同步延遲（read model 落後 write model 多久可以接受）、重建流程（read model 損壞或 schema 變更時如何從頭重建）、欄位語意（read model 的欄位定義跟 write model 可能不同）與查詢邊界（這個 read model 能回答什麼問題、不能回答什麼問題）。</p>
<p>Read model 是派生狀態，修復方式是「砍掉重建」而非直接修改。把 read model 當正式狀態修改會導致 write model 跟 read model 分岔、後續同步覆蓋修改。</p>
]]></content:encoded></item><item><title>Incident Timeline</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/</guid><description>&lt;p>Incident timeline 的核心概念是「按時間順序記錄事故中的觀測、決策與操作」。時間線是事故的共同事實來源，連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 觸發到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 復盤，讓團隊可以對齊發生順序與影響變化。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Timeline 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 觸發（事故何時被偵測到）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 回應（何時開始處理）、操作紀錄（做了什麼）、影響變化（使用者影響何時改善 / 惡化）跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>（復盤時重建因果鏈）。&lt;/p>
&lt;p>Timeline 也是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 的時間軸基礎 — decision log 記錄「在這個時間點、基於這個觀測、做了這個決策」，timeline 提供「這個時間點」的上下文。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 incident timeline 的訊號是事故後大家對「先發生什麼」說法不同。若沒有一致時間軸，復盤時很難判斷哪個操作真正帶來改善、哪個決策在當時是合理的。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Timeline 要包含時間戳（UTC、精確到分鐘）、訊號來源（哪個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> / 人為觀察）、操作內容（restart / rollback / scale）、決策理由與結果驗證。記錄方式應簡潔且可在高壓下維持更新 — 事故中寫 timeline 的成本太高會導致沒人寫。Slack channel pinned message 或事故管理工具的自動 timeline 是常見實作。&lt;/p></description><content:encoded><![CDATA[<p>Incident timeline 的核心概念是「按時間順序記錄事故中的觀測、決策與操作」。時間線是事故的共同事實來源，連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 觸發到 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 復盤，讓團隊可以對齊發生順序與影響變化。</p>
<h2 id="概念位置">概念位置</h2>
<p>Timeline 連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 觸發（事故何時被偵測到）、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 回應（何時開始處理）、操作紀錄（做了什麼）、影響變化（使用者影響何時改善 / 惡化）跟 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>（復盤時重建因果鏈）。</p>
<p>Timeline 也是 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 的時間軸基礎 — decision log 記錄「在這個時間點、基於這個觀測、做了這個決策」，timeline 提供「這個時間點」的上下文。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 incident timeline 的訊號是事故後大家對「先發生什麼」說法不同。若沒有一致時間軸，復盤時很難判斷哪個操作真正帶來改善、哪個決策在當時是合理的。</p>
<h2 id="設計責任">設計責任</h2>
<p>Timeline 要包含時間戳（UTC、精確到分鐘）、訊號來源（哪個 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> / <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> / 人為觀察）、操作內容（restart / rollback / scale）、決策理由與結果驗證。記錄方式應簡潔且可在高壓下維持更新 — 事故中寫 timeline 的成本太高會導致沒人寫。Slack channel pinned message 或事故管理工具的自動 timeline 是常見實作。</p>
]]></content:encoded></item><item><title>Rollback Window</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/</guid><description>&lt;p>Rollback window 的核心概念是「變更進入 production 後，仍能用特定方式回退或改路線的有效窗口」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a>，讓 gate 能判斷目前還剩哪種退路。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Rollback window 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">fallback plan&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Rollback strategy 說明回退決策，rollback window 說明這個決策在目前階段是否仍可執行。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 rollback window 的訊號是：&lt;/p>
&lt;ul>
&lt;li>expand、backfill、cutover、contract 每一階段的回退方式不同&lt;/li>
&lt;li>舊版本或舊資料語意只能支撐一段時間&lt;/li>
&lt;li>cutover 後仍可 fallback read，但 contract 後只能資料修復或 fail-forward&lt;/li>
&lt;li>release gate 要判斷是否還能安全暫停或回退&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 在 expand 階段通常能回到舊讀取；backfill 階段可以暫停與重跑；cutover 後可回到 fallback read；contract 移除舊欄位後，回退會轉成資料修補或 fail-forward。這些差異都屬於 rollback window。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Rollback window 要寫清楚目前階段、可用回退方式、最後可回退時間、資料相容性限制與 owner。它要進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>，避免事故期間把已經關閉的退路當成可用選項。&lt;/p></description><content:encoded><![CDATA[<p>Rollback window 的核心概念是「變更進入 production 後，仍能用特定方式回退或改路線的有效窗口」。它連接 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a>、<a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 與 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a>，讓 gate 能判斷目前還剩哪種退路。</p>
<h2 id="概念位置">概念位置</h2>
<p>Rollback window 位在 <a href="/blog/backend/knowledge-cards/cutover-switchover/" data-link-title="Cutover / Switchover" data-link-desc="說明遷移期間如何把正式流量切到新路徑">cutover / switchover</a>、<a href="/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">fallback plan</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Rollback strategy 說明回退決策，rollback window 說明這個決策在目前階段是否仍可執行。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 rollback window 的訊號是：</p>
<ul>
<li>expand、backfill、cutover、contract 每一階段的回退方式不同</li>
<li>舊版本或舊資料語意只能支撐一段時間</li>
<li>cutover 後仍可 fallback read，但 contract 後只能資料修復或 fail-forward</li>
<li>release gate 要判斷是否還能安全暫停或回退</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 在 expand 階段通常能回到舊讀取；backfill 階段可以暫停與重跑；cutover 後可回到 fallback read；contract 移除舊欄位後，回退會轉成資料修補或 fail-forward。這些差異都屬於 rollback window。</p>
<h2 id="設計責任">設計責任</h2>
<p>Rollback window 要寫清楚目前階段、可用回退方式、最後可回退時間、資料相容性限制與 owner。它要進入 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，避免事故期間把已經關閉的退路當成可用選項。</p>
]]></content:encoded></item><item><title>Fail-forward</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/fail-forward/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/fail-forward/</guid><description>&lt;p>Fail-forward 的核心概念是「當回退代價高於前進修復時，用受控方式往新狀態完成修復」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">fallback plan&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>，不是忽略失敗繼續推進。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Fail-forward 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 之間。Rollback window 關閉後，團隊仍需要一條能限制影響、補資料、完成相容收斂的前進路線。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 fail-forward 的訊號是：&lt;/p>
&lt;ul>
&lt;li>舊資料語意已被 contract 或不可逆寫入移除&lt;/li>
&lt;li>回退會造成更大的資料不一致或客戶影響&lt;/li>
&lt;li>新路徑有明確修補方案、停損條件與 owner&lt;/li>
&lt;li>事故 decision log 需要記錄為何不回滾&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 已完成 contract 後，舊欄位被移除，回到舊版本會讓讀取路徑失效。此時比較可控的做法可能是暫停部分寫入、修補 mismatch、補 validation query，再讓新路徑收斂到可用狀態。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Fail-forward 要定義 containment、修補步驟、預期效果、停止條件與回寫項目。它要搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a>，避免「不能回滾」被誤用成沒有證據的硬推。&lt;/p></description><content:encoded><![CDATA[<p>Fail-forward 的核心概念是「當回退代價高於前進修復時，用受控方式往新狀態完成修復」。它連接 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a>、<a href="/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">fallback plan</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，不是忽略失敗繼續推進。</p>
<h2 id="概念位置">概念位置</h2>
<p>Fail-forward 位在 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、<a href="/blog/backend/knowledge-cards/containment/" data-link-title="Containment" data-link-desc="說明事故處理中如何限制擴散面，為回復與驗證爭取時間">containment</a> 與 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 之間。Rollback window 關閉後，團隊仍需要一條能限制影響、補資料、完成相容收斂的前進路線。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 fail-forward 的訊號是：</p>
<ul>
<li>舊資料語意已被 contract 或不可逆寫入移除</li>
<li>回退會造成更大的資料不一致或客戶影響</li>
<li>新路徑有明確修補方案、停損條件與 owner</li>
<li>事故 decision log 需要記錄為何不回滾</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 已完成 contract 後，舊欄位被移除，回到舊版本會讓讀取路徑失效。此時比較可控的做法可能是暫停部分寫入、修補 mismatch、補 validation query，再讓新路徑收斂到可用狀態。</p>
<h2 id="設計責任">設計責任</h2>
<p>Fail-forward 要定義 containment、修補步驟、預期效果、停止條件與回寫項目。它要搭配 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a>，避免「不能回滾」被誤用成沒有證據的硬推。</p>
]]></content:encoded></item><item><title>Stop Condition</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/</guid><description>&lt;p>Stop condition 的核心概念是「事前定義何時必須暫停、回退或改路線」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>，避免團隊在壓力下用感覺決定是否繼續。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Stop condition 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover-window&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a> 之間。Gate 說明能否開始，stop condition 說明開始後看到哪些訊號必須停。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 stop condition 的訊號是：&lt;/p>
&lt;ul>
&lt;li>rollout、backfill、replay 或 experiment 會逐批擴大影響&lt;/li>
&lt;li>指標短暫變壞時需要知道是觀察、暫停還是回退&lt;/li>
&lt;li>owner 需要在事故現場快速做一致決策&lt;/li>
&lt;li>post-incident review 要檢查當時是否該更早停下來&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 可以定義 &lt;code>mismatch_rate &amp;gt;= 0.1% for two consecutive batches&lt;/code> 或 &lt;code>replication_lag &amp;gt;= 30s for 10 minutes&lt;/code> 作為 stop condition。達到條件時，團隊先暫停下一批 backfill 或回到 fallback read，而不是等使用者回報。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Stop condition 要包含訊號、門檻、觀察窗口、對應動作與 owner。它要進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>，並且要能被 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 支撐。&lt;/p></description><content:encoded><![CDATA[<p>Stop condition 的核心概念是「事前定義何時必須暫停、回退或改路線」。它連接 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a>、<a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，避免團隊在壓力下用感覺決定是否繼續。</p>
<h2 id="概念位置">概念位置</h2>
<p>Stop condition 位在 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a>、<a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover-window</a> 與 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 之間。Gate 說明能否開始，stop condition 說明開始後看到哪些訊號必須停。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 stop condition 的訊號是：</p>
<ul>
<li>rollout、backfill、replay 或 experiment 會逐批擴大影響</li>
<li>指標短暫變壞時需要知道是觀察、暫停還是回退</li>
<li>owner 需要在事故現場快速做一致決策</li>
<li>post-incident review 要檢查當時是否該更早停下來</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 可以定義 <code>mismatch_rate &gt;= 0.1% for two consecutive batches</code> 或 <code>replication_lag &gt;= 30s for 10 minutes</code> 作為 stop condition。達到條件時，團隊先暫停下一批 backfill 或回到 fallback read，而不是等使用者回報。</p>
<h2 id="設計責任">設計責任</h2>
<p>Stop condition 要包含訊號、門檻、觀察窗口、對應動作與 owner。它要進入 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 和 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，並且要能被 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 支撐。</p>
]]></content:encoded></item><item><title>Gate Decision</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/gate-decision/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/gate-decision/</guid><description>&lt;p>Gate decision 的核心概念是「release gate 根據證據做出的明確下一步」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition&lt;/a>，讓 gate 不只寫檢查結果，也寫出能不能前進。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Gate decision 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Checks 描述檢查結果，gate decision 把結果轉成放行、暫停、回退、fail-forward 或補證據。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 gate decision 的訊號是：&lt;/p>
&lt;ul>
&lt;li>CI、SLO、validation query 都有結果，但沒人知道下一步&lt;/li>
&lt;li>evidence 足以支持部分放行，但不足以支持完整 cutover&lt;/li>
&lt;li>變更需要逐批 rollout、backfill、warmup 或 replay&lt;/li>
&lt;li>gate 要保留 owner 與 rollback window&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 gate decision 可以寫成 &lt;code>allow next 10% backfill; block customer-visible read cutover&lt;/code>。這句話比 &lt;code>migration pass&lt;/code> 更可操作，因為它同時說明允許前進的範圍與被擋住的風險面。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Gate decision 要包含決策內容、支撐 checks、stop condition、rollback window 與 owner。它要能被 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 承接，讓放行後出現異常時能回放當時依據。&lt;/p></description><content:encoded><![CDATA[<p>Gate decision 的核心概念是「release gate 根據證據做出的明確下一步」。它連接 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a>、<a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a>，讓 gate 不只寫檢查結果，也寫出能不能前進。</p>
<h2 id="概念位置">概念位置</h2>
<p>Gate decision 位在 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a>、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Checks 描述檢查結果，gate decision 把結果轉成放行、暫停、回退、fail-forward 或補證據。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 gate decision 的訊號是：</p>
<ul>
<li>CI、SLO、validation query 都有結果，但沒人知道下一步</li>
<li>evidence 足以支持部分放行，但不足以支持完整 cutover</li>
<li>變更需要逐批 rollout、backfill、warmup 或 replay</li>
<li>gate 要保留 owner 與 rollback window</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 gate decision 可以寫成 <code>allow next 10% backfill; block customer-visible read cutover</code>。這句話比 <code>migration pass</code> 更可操作，因為它同時說明允許前進的範圍與被擋住的風險面。</p>
<h2 id="設計責任">設計責任</h2>
<p>Gate decision 要包含決策內容、支撐 checks、stop condition、rollback window 與 owner。它要能被 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 承接，讓放行後出現異常時能回放當時依據。</p>
]]></content:encoded></item><item><title>Projection</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/projection/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/projection/</guid><description>&lt;p>Projection 從事件流或資料變更中持續推算出特定用途的讀取視圖，連接寫入端（事件產生）跟讀取端（查詢消費）。Projection 的輸出是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> — 為特定查詢需求反正規化的資料形狀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Projection 在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 架構中扮演「event → current state」的推算角色。Event log 是 append-only 的事件序列，直接對 event log 做查詢效率低；projection 持續消費事件、維護可查詢的 read model，讓讀取端不需要每次 replay 整條事件流。&lt;/p>
&lt;p>Projection 不限於 event sourcing。CDC（Change Data Capture）把資料庫的 row 變更推送到下游、下游建立搜尋索引或統計摘要，這也是 projection — 來源是 row change event 而非 domain event。觀測領域的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 也是一種 projection — 從 raw time series 持續推算預聚合的 metrics。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 projection 時要定義四個面向：&lt;/p>
&lt;p>&lt;strong>更新策略&lt;/strong>：同步（事件寫入時立即更新 read model）或非同步（事件寫入後由背景消費者更新）。同步更新延遲低但耦合寫入路徑的效能；非同步更新解耦但 read model 有 lag。&lt;/p>
&lt;p>&lt;strong>重建流程&lt;/strong>：當 projection 邏輯改變或 read model 損壞時，需要從 event log 重新 replay 建立 read model。重建流程要能離線執行、不影響線上讀取。大量事件的 replay 可能需要數小時，設計時要估算重建時間跟資源需求。&lt;/p>
&lt;p>&lt;strong>正確性驗證&lt;/strong>：projection 是持續運行的計算，任何 bug 都會讓 read model 靜默偏離真實狀態。需要定期的 reconciliation（拿 projection 結果跟 event log 全量 replay 比較）來偵測漂移。&lt;/p>
&lt;p>&lt;strong>schema evolution&lt;/strong>：當來源事件的 schema 改版，projection 邏輯要能同時處理新舊版本的事件。這跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 的 upcasting 問題直接相關。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 projection 的訊號是：讀取需求跟寫入結構不同（列表頁需要反正規化 view、搜尋需要全文索引、報表需要聚合摘要），而且這些讀取視圖需要隨資料變更持續更新而非批次重建。&lt;/p>
&lt;p>常見的 projection 實作包括：event handler 更新 read DB、CDC consumer 更新 Elasticsearch index、Kafka Streams 維護 KTable、觀測 collector 做 log-to-metric 轉換。&lt;/p></description><content:encoded><![CDATA[<p>Projection 從事件流或資料變更中持續推算出特定用途的讀取視圖，連接寫入端（事件產生）跟讀取端（查詢消費）。Projection 的輸出是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> — 為特定查詢需求反正規化的資料形狀。</p>
<h2 id="概念位置">概念位置</h2>
<p>Projection 在 <a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 架構中扮演「event → current state」的推算角色。Event log 是 append-only 的事件序列，直接對 event log 做查詢效率低；projection 持續消費事件、維護可查詢的 read model，讓讀取端不需要每次 replay 整條事件流。</p>
<p>Projection 不限於 event sourcing。CDC（Change Data Capture）把資料庫的 row 變更推送到下游、下游建立搜尋索引或統計摘要，這也是 projection — 來源是 row change event 而非 domain event。觀測領域的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 也是一種 projection — 從 raw time series 持續推算預聚合的 metrics。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 projection 時要定義四個面向：</p>
<p><strong>更新策略</strong>：同步（事件寫入時立即更新 read model）或非同步（事件寫入後由背景消費者更新）。同步更新延遲低但耦合寫入路徑的效能；非同步更新解耦但 read model 有 lag。</p>
<p><strong>重建流程</strong>：當 projection 邏輯改變或 read model 損壞時，需要從 event log 重新 replay 建立 read model。重建流程要能離線執行、不影響線上讀取。大量事件的 replay 可能需要數小時，設計時要估算重建時間跟資源需求。</p>
<p><strong>正確性驗證</strong>：projection 是持續運行的計算，任何 bug 都會讓 read model 靜默偏離真實狀態。需要定期的 reconciliation（拿 projection 結果跟 event log 全量 replay 比較）來偵測漂移。</p>
<p><strong>schema evolution</strong>：當來源事件的 schema 改版，projection 邏輯要能同時處理新舊版本的事件。這跟 <a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 的 upcasting 問題直接相關。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 projection 的訊號是：讀取需求跟寫入結構不同（列表頁需要反正規化 view、搜尋需要全文索引、報表需要聚合摘要），而且這些讀取視圖需要隨資料變更持續更新而非批次重建。</p>
<p>常見的 projection 實作包括：event handler 更新 read DB、CDC consumer 更新 Elasticsearch index、Kafka Streams 維護 KTable、觀測 collector 做 log-to-metric 轉換。</p>
]]></content:encoded></item><item><title>Rollback Condition</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/</guid><description>&lt;p>Rollback condition 的核心概念是「某個決策執行後，看到哪些訊號時要撤回、回退或改路線」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition&lt;/a>，讓事故現場能控制次生風險。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Rollback condition 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 之間。Stop condition 常用於流程何時停，rollback condition 則跟某筆已做出的 decision 綁在一起。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 rollback condition 的訊號是：&lt;/p>
&lt;ul>
&lt;li>rollback、fallback、degradation 或 fail-forward 本身也可能造成風險&lt;/li>
&lt;li>IC handoff 後，新 IC 需要知道什麼條件下要改路線&lt;/li>
&lt;li>stakeholder update 需要說明目前決策如何被監控&lt;/li>
&lt;li>PIR 需要檢查當時是否有明確撤回條件&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>客服後台切回 legacy status fallback 後，rollback condition 可以寫成 &lt;code>mismatch remains above threshold after 15 minutes&lt;/code>。這表示 fallback 沒有降低錯誤時，團隊要改成資料修補或暫停相關入口，而不是繼續等待。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Rollback condition 要包含訊號、門檻、觀察窗口、對應動作與 owner。它要連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a>，讓決策撤回成為可回放的證據判讀，口頭判斷的準確度和可追溯性都不足。&lt;/p></description><content:encoded><![CDATA[<p>Rollback condition 的核心概念是「某個決策執行後，看到哪些訊號時要撤回、回退或改路線」。它連接 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>、<a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 與 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a>，讓事故現場能控制次生風險。</p>
<h2 id="概念位置">概念位置</h2>
<p>Rollback condition 位在 <a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision</a>、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 與 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 之間。Stop condition 常用於流程何時停，rollback condition 則跟某筆已做出的 decision 綁在一起。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 rollback condition 的訊號是：</p>
<ul>
<li>rollback、fallback、degradation 或 fail-forward 本身也可能造成風險</li>
<li>IC handoff 後，新 IC 需要知道什麼條件下要改路線</li>
<li>stakeholder update 需要說明目前決策如何被監控</li>
<li>PIR 需要檢查當時是否有明確撤回條件</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>客服後台切回 legacy status fallback 後，rollback condition 可以寫成 <code>mismatch remains above threshold after 15 minutes</code>。這表示 fallback 沒有降低錯誤時，團隊要改成資料修補或暫停相關入口，而不是繼續等待。</p>
<h2 id="設計責任">設計責任</h2>
<p>Rollback condition 要包含訊號、門檻、觀察窗口、對應動作與 owner。它要連到 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a>，讓決策撤回成為可回放的證據判讀，口頭判斷的準確度和可追溯性都不足。</p>
]]></content:encoded></item><item><title>On-Call</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/</guid><description>&lt;p>On-call 的核心概念是「在指定時段由明確責任角色承接運行事件」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 的執行入口，把告警回應、事故分級、升級決策與交接責任固定化，讓事故處理不依賴臨時找人。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>On-call 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 的執行入口。值班工程師是 alert 的第一個接收者，負責判斷「這個 alert 需要什麼等級的回應」。&lt;/p>
&lt;p>On-call 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 搭配運作 — runbook 提供行動指南、on-call 工程師執行。制度需要跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 跟演練一起維護，避免值班只剩 pager 通知而沒有可執行流程。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 on-call 制度的訊號是事故常在非上班時間發生、或跨區團隊需要連續處理。付款 API 夜間故障時，若沒有清楚值班安排，回復時間通常被人員定位延遲拉長。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>On-call 設計要定義排班週期、回應時限（critical alert 需要 N 分鐘內 ack）、交接格式（交班時把當前狀態跟未關閉事項傳給下一位）、升級路徑（on-call 解不了時升級到 tech lead / manager）與支援角色（secondary on-call 或 subject matter expert）。Alert noise 治理跟 on-call 品質直接相關 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 會讓值班品質退化。&lt;/p></description><content:encoded><![CDATA[<p>On-call 的核心概念是「在指定時段由明確責任角色承接運行事件」。它是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 的執行入口，把告警回應、事故分級、升級決策與交接責任固定化，讓事故處理不依賴臨時找人。</p>
<h2 id="概念位置">概念位置</h2>
<p>On-call 是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 的執行入口。值班工程師是 alert 的第一個接收者，負責判斷「這個 alert 需要什麼等級的回應」。</p>
<p>On-call 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 搭配運作 — runbook 提供行動指南、on-call 工程師執行。制度需要跟 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 跟演練一起維護，避免值班只剩 pager 通知而沒有可執行流程。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 on-call 制度的訊號是事故常在非上班時間發生、或跨區團隊需要連續處理。付款 API 夜間故障時，若沒有清楚值班安排，回復時間通常被人員定位延遲拉長。</p>
<h2 id="設計責任">設計責任</h2>
<p>On-call 設計要定義排班週期、回應時限（critical alert 需要 N 分鐘內 ack）、交接格式（交班時把當前狀態跟未關閉事項傳給下一位）、升級路徑（on-call 解不了時升級到 tech lead / manager）與支援角色（secondary on-call 或 subject matter expert）。Alert noise 治理跟 on-call 品質直接相關 — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 會讓值班品質退化。</p>
]]></content:encoded></item><item><title>Ownership</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/</guid><description>&lt;p>Ownership 的核心概念是「把責任固定到可執行角色」。它讓團隊在事件、變更與回寫流程中能快速判斷誰主責、誰協作、誰做決策，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 運作的前提。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Ownership 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>（每個 alert rule 需要 owner）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>（每個 dashboard 需要維護者）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>（runbook 的更新責任跟服務 owner 一致）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a>。&lt;/p>
&lt;p>在觀測系統中，沒有 owner 的 alert 跟 dashboard 會隨服務演進退化 — alert 變成 noise、dashboard 變成裝飾。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a> 的定期審視需要每個訊號都有明確 owner。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model&lt;/a> 定義 ownership 矩陣。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 ownership 的訊號是同一事件在不同角色之間反覆轉手、或 alert 觸發後沒人知道該誰處理。Owner 離職但 alert / dashboard / runbook 沒有交接是常見的退化模式。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Ownership 需要定義主責角色、協作角色、升級路由與關閉責任。Owner 變動時（離職、轉組）需要交接流程 — orphan alert / dashboard 的定期掃描是治理的一部分。每次服務邊界調整（新服務上線、服務合併）都應同步檢查 ownership 是否仍對齊。&lt;/p></description><content:encoded><![CDATA[<p>Ownership 的核心概念是「把責任固定到可執行角色」。它讓團隊在事件、變更與回寫流程中能快速判斷誰主責、誰協作、誰做決策，是 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 運作的前提。</p>
<h2 id="概念位置">概念位置</h2>
<p>Ownership 連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>（每個 alert rule 需要 owner）、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>（每個 dashboard 需要維護者）、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>（runbook 的更新責任跟服務 owner 一致）、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 跟 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a>。</p>
<p>在觀測系統中，沒有 owner 的 alert 跟 dashboard 會隨服務演進退化 — alert 變成 noise、dashboard 變成裝飾。<a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a> 的定期審視需要每個訊號都有明確 owner。<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a> 定義 ownership 矩陣。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 ownership 的訊號是同一事件在不同角色之間反覆轉手、或 alert 觸發後沒人知道該誰處理。Owner 離職但 alert / dashboard / runbook 沒有交接是常見的退化模式。</p>
<h2 id="設計責任">設計責任</h2>
<p>Ownership 需要定義主責角色、協作角色、升級路由與關閉責任。Owner 變動時（離職、轉組）需要交接流程 — orphan alert / dashboard 的定期掃描是治理的一部分。每次服務邊界調整（新服務上線、服務合併）都應同步檢查 ownership 是否仍對齊。</p>
]]></content:encoded></item><item><title>Continuous Profiling</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/continuous-profiling/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/continuous-profiling/</guid><description>&lt;p>Continuous profiling 的核心概念是「在 production 持續以低 overhead 採集 CPU / heap / lock profile，讓 baseline 隨時可用、不需要等事故才開 profiler」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 之外能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Continuous profiling 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 之外的第四角觀測訊號。Metrics 告訴你「CPU usage 上升了」，trace 告訴你「某條 request 變慢」，profile 告訴你「變慢的那段程式碼是哪幾個 function call」。Profile 是唯一能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;p>Always-on 的核心價值是 baseline — 事故時跟 baseline 做 diff（flame graph diff），看「哪些 function 的 CPU 消耗跟平時不同」。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 continuous profiling 的訊號是「latency 退化常找不到原因、靠事後重現很慢」或「同一段 hot path 反覆出現在事故 RCA 中但缺 baseline 資料」。版本升級後 latency 退化時，profile diff 能直接定位是哪個 function 變慢。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Overhead 控制是 continuous profiling 可行性的前提 — CPU overhead &amp;lt; 1%、memory overhead &amp;lt; 10MB。eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集、overhead 最低；language runtime 內建（Go pprof、Java JFR）居中。Profile 資料要帶 service / version / region label，讓跨版本 diff 跟 canary 對照可行。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Continuous profiling 的核心概念是「在 production 持續以低 overhead 採集 CPU / heap / lock profile，讓 baseline 隨時可用、不需要等事故才開 profiler」。它是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 之外能精確到 callstack level 的觀測訊號。</p>
<h2 id="概念位置">概念位置</h2>
<p>Continuous profiling 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 之外的第四角觀測訊號。Metrics 告訴你「CPU usage 上升了」，trace 告訴你「某條 request 變慢」，profile 告訴你「變慢的那段程式碼是哪幾個 function call」。Profile 是唯一能精確到 callstack level 的觀測訊號。</p>
<p>Always-on 的核心價值是 baseline — 事故時跟 baseline 做 diff（flame graph diff），看「哪些 function 的 CPU 消耗跟平時不同」。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 continuous profiling 的訊號是「latency 退化常找不到原因、靠事後重現很慢」或「同一段 hot path 反覆出現在事故 RCA 中但缺 baseline 資料」。版本升級後 latency 退化時，profile diff 能直接定位是哪個 function 變慢。</p>
<h2 id="設計責任">設計責任</h2>
<p>Overhead 控制是 continuous profiling 可行性的前提 — CPU overhead &lt; 1%、memory overhead &lt; 10MB。eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集、overhead 最低；language runtime 內建（Go pprof、Java JFR）居中。Profile 資料要帶 service / version / region label，讓跨版本 diff 跟 canary 對照可行。完整設計見 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling</a>。</p>
]]></content:encoded></item><item><title>Action Item Closure</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/</guid><description>&lt;p>Action item closure 的核心概念是「把復盤行動項變成可驗證完成的工程責任」。它承接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 的產出，關心的是每一項是否有 owner、完成標準、驗證方式與截止時間，而非列出多少待辦。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Action item closure 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>（產出行動項）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>（行動項可能是更新 runbook）、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a>（行動項可能是新增 alert / metric / dashboard）。&lt;/p>
&lt;p>Detection gap 類的行動項（「事故中缺少某個 alert / metric」）應指派給觀測系統的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner&lt;/a>，帶明確的變更規格（新增哪個 metric、alert 閾值多少、連到哪個 runbook）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 action item closure 流程的訊號是事故復盤後大量 open items 超過 90 天仍未關閉，或同類事故重複發生但上次復盤的改善項還沒完成。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>每個 action item 定義：owner（誰負責完成）、完成標準（什麼狀態算 done — 不是「已開始」而是「已部署、已驗證」）、驗證方式（怎麼確認完成 — 跑一次演練、查 dashboard 確認 metric 存在）、截止時間（兩週內 close）。逾期的 action item 自動升級到管理層 — 這個升級機制是 closure 流程的背壓。&lt;/p></description><content:encoded><![CDATA[<p>Action item closure 的核心概念是「把復盤行動項變成可驗證完成的工程責任」。它承接 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 的產出，關心的是每一項是否有 owner、完成標準、驗證方式與截止時間，而非列出多少待辦。</p>
<h2 id="概念位置">概念位置</h2>
<p>Action item closure 連接 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>（產出行動項）、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>（行動項可能是更新 runbook）、<a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a>（行動項可能是新增 alert / metric / dashboard）。</p>
<p>Detection gap 類的行動項（「事故中缺少某個 alert / metric」）應指派給觀測系統的 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner</a>，帶明確的變更規格（新增哪個 metric、alert 閾值多少、連到哪個 runbook）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 action item closure 流程的訊號是事故復盤後大量 open items 超過 90 天仍未關閉，或同類事故重複發生但上次復盤的改善項還沒完成。</p>
<h2 id="設計責任">設計責任</h2>
<p>每個 action item 定義：owner（誰負責完成）、完成標準（什麼狀態算 done — 不是「已開始」而是「已部署、已驗證」）、驗證方式（怎麼確認完成 — 跑一次演練、查 dashboard 確認 metric 存在）、截止時間（兩週內 close）。逾期的 action item 自動升級到管理層 — 這個升級機制是 closure 流程的背壓。</p>
]]></content:encoded></item><item><title>Time Range</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/</guid><description>&lt;p>Time range 的核心概念是「證據或查詢對應的明確時間窗」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>，讓同一組資料能被事中交班、release gate 與事後復盤一致解讀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Time range 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Dashboard 顯示狀態，query link 保留查詢入口，time range 則定義這次判讀看的時間範圍。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 time range 的訊號是：&lt;/p>
&lt;ul>
&lt;li>同一張圖在不同時間重跑會得到不同結果&lt;/li>
&lt;li>release gate 要判斷某批 rollout 是否已穩定&lt;/li>
&lt;li>事故交班需要知道某個 evidence 觀察的是哪段時間&lt;/li>
&lt;li>復盤要對齊 deploy、alert、customer report 與 rollback 的先後&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 validation query 若標示 &lt;code>2026-05-11T02:10:00Z/2026-05-11T02:20:00Z&lt;/code>，下一班 on-call 就能把 mismatch、replication lag 與 slow query 放回同一個 backfill batch 判讀。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Time range 要定義開始時間、結束時間、時區、資料延遲限制與關聯事件。它應進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition&lt;/a>，避免團隊用不同時間窗比較同一個決策。&lt;/p></description><content:encoded><![CDATA[<p>Time range 的核心概念是「證據或查詢對應的明確時間窗」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 與 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>，讓同一組資料能被事中交班、release gate 與事後復盤一致解讀。</p>
<h2 id="概念位置">概念位置</h2>
<p>Time range 位在 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Dashboard 顯示狀態，query link 保留查詢入口，time range 則定義這次判讀看的時間範圍。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 time range 的訊號是：</p>
<ul>
<li>同一張圖在不同時間重跑會得到不同結果</li>
<li>release gate 要判斷某批 rollout 是否已穩定</li>
<li>事故交班需要知道某個 evidence 觀察的是哪段時間</li>
<li>復盤要對齊 deploy、alert、customer report 與 rollback 的先後</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 validation query 若標示 <code>2026-05-11T02:10:00Z/2026-05-11T02:20:00Z</code>，下一班 on-call 就能把 mismatch、replication lag 與 slow query 放回同一個 backfill batch 判讀。</p>
<h2 id="設計責任">設計責任</h2>
<p>Time range 要定義開始時間、結束時間、時區、資料延遲限制與關聯事件。它應進入 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>，避免團隊用不同時間窗比較同一個決策。</p>
]]></content:encoded></item><item><title>Query Link</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/</guid><description>&lt;p>Query link 的核心概念是「保存可重跑的查詢入口」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a>，讓後續接手者能重新驗證同一個判讀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Query link 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。截圖適合溝通當下狀態，query link 則保留可回放、可調整、可驗證的入口。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 query link 的訊號是：&lt;/p>
&lt;ul>
&lt;li>事故交班時下一班需要重跑同一個判讀&lt;/li>
&lt;li>release gate 要引用具體查詢結果，而不是貼圖表摘要&lt;/li>
&lt;li>PIR reviewer 需要查證當時資料限制&lt;/li>
&lt;li>dashboard panel 版本變動可能改變圖表語意&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>Checkout API evidence package 可以保存錯誤率 query、p95 latency query 與 provider timeout query 的連結。資料庫 migration evidence package 則可以保存 row count、mismatch sample 與 replication lag query link。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Query link 要保留查詢版本、參數、time range、資料來源與 owner。它要搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 記錄查詢未覆蓋的資料範圍，避免截圖或 dashboard 名稱被誤當成完整證據。&lt;/p></description><content:encoded><![CDATA[<p>Query link 的核心概念是「保存可重跑的查詢入口」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 與 <a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a>，讓後續接手者能重新驗證同一個判讀。</p>
<h2 id="概念位置">概念位置</h2>
<p>Query link 位在 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。截圖適合溝通當下狀態，query link 則保留可回放、可調整、可驗證的入口。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 query link 的訊號是：</p>
<ul>
<li>事故交班時下一班需要重跑同一個判讀</li>
<li>release gate 要引用具體查詢結果，而不是貼圖表摘要</li>
<li>PIR reviewer 需要查證當時資料限制</li>
<li>dashboard panel 版本變動可能改變圖表語意</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>Checkout API evidence package 可以保存錯誤率 query、p95 latency query 與 provider timeout query 的連結。資料庫 migration evidence package 則可以保存 row count、mismatch sample 與 replication lag query link。</p>
<h2 id="設計責任">設計責任</h2>
<p>Query link 要保留查詢版本、參數、time range、資料來源與 owner。它要搭配 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 記錄查詢未覆蓋的資料範圍，避免截圖或 dashboard 名稱被誤當成完整證據。</p>
]]></content:encoded></item><item><title>Data Quality</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/</guid><description>&lt;p>Data quality 的核心概念是「證據資料本身的完整度、新鮮度與限制」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a>，讓下游知道這份 evidence 能支持到哪個判斷範圍。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Data quality 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Metric、log、trace、audit log 都可能有延遲、抽樣、drop、masking 或 schema drift，這些限制要跟證據一起交接。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 data quality 的訊號是：&lt;/p>
&lt;ul>
&lt;li>trace sampling 讓某些 request path 無法完整重建&lt;/li>
&lt;li>log pipeline 有 ingest delay 或 drop&lt;/li>
&lt;li>query 只跑 primary、replica 或部分 tenant&lt;/li>
&lt;li>dashboard 結論需要標示 freshness 或 completeness 限制&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 evidence package 可以標示 &lt;code>primary only; replica lag still recovering&lt;/code>，表示 validation query 可信，但 replica 讀取路徑還不能用同一份 evidence 直接放行。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Data quality 要標示 completeness、freshness、sampling、masking、retention 與 owner。它要支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a> 判讀，避免 release gate 或 incident decision log 把有限資料誤當成完整事實。&lt;/p></description><content:encoded><![CDATA[<p>Data quality 的核心概念是「證據資料本身的完整度、新鮮度與限制」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 與 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a>，讓下游知道這份 evidence 能支持到哪個判斷範圍。</p>
<h2 id="概念位置">概念位置</h2>
<p>Data quality 位在 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Metric、log、trace、audit log 都可能有延遲、抽樣、drop、masking 或 schema drift，這些限制要跟證據一起交接。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 data quality 的訊號是：</p>
<ul>
<li>trace sampling 讓某些 request path 無法完整重建</li>
<li>log pipeline 有 ingest delay 或 drop</li>
<li>query 只跑 primary、replica 或部分 tenant</li>
<li>dashboard 結論需要標示 freshness 或 completeness 限制</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 evidence package 可以標示 <code>primary only; replica lag still recovering</code>，表示 validation query 可信，但 replica 讀取路徑還不能用同一份 evidence 直接放行。</p>
<h2 id="設計責任">設計責任</h2>
<p>Data quality 要標示 completeness、freshness、sampling、masking、retention 與 owner。它要支援 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a> 判讀，避免 release gate 或 incident decision log 把有限資料誤當成完整事實。</p>
]]></content:encoded></item><item><title>Confidence</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/</guid><description>&lt;p>Confidence 的核心概念是「標示目前證據能支持決策的信心等級」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision&lt;/a>，讓團隊能區分 confirmed、suspected 與 needs follow-up。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Confidence 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。它不是情緒性的「我覺得」，而是基於證據完整度、資料限制與反向驗證狀態的判讀欄位。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 confidence 的訊號是：&lt;/p>
&lt;ul>
&lt;li>evidence 足以支持繼續 backfill，但不足以支持使用者可見 cutover&lt;/li>
&lt;li>事故中某個根因還在 suspected 狀態&lt;/li>
&lt;li>release gate 需要分辨可以放行、暫停或補證據&lt;/li>
&lt;li>stakeholder update 需要避免把未確認資訊說成事實&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 evidence package 可以把 &lt;code>confidence&lt;/code> 標成 &lt;code>suspected&lt;/code>：validation query 顯示 mismatch 低於門檻，但 manual refund repair path 尚未被抽樣，因此只放行下一批 backfill，不放行使用者可見讀取 cutover。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Confidence 要定義等級、證據依據、限制與下一步。它要與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition&lt;/a> 一起保存，避免團隊把暫時結論當成穩定事實。&lt;/p></description><content:encoded><![CDATA[<p>Confidence 的核心概念是「標示目前證據能支持決策的信心等級」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a> 與 <a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision</a>，讓團隊能區分 confirmed、suspected 與 needs follow-up。</p>
<h2 id="概念位置">概念位置</h2>
<p>Confidence 位在 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a>、<a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。它不是情緒性的「我覺得」，而是基於證據完整度、資料限制與反向驗證狀態的判讀欄位。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 confidence 的訊號是：</p>
<ul>
<li>evidence 足以支持繼續 backfill，但不足以支持使用者可見 cutover</li>
<li>事故中某個根因還在 suspected 狀態</li>
<li>release gate 需要分辨可以放行、暫停或補證據</li>
<li>stakeholder update 需要避免把未確認資訊說成事實</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 evidence package 可以把 <code>confidence</code> 標成 <code>suspected</code>：validation query 顯示 mismatch 低於門檻，但 manual refund repair path 尚未被抽樣，因此只放行下一批 backfill，不放行使用者可見讀取 cutover。</p>
<h2 id="設計責任">設計責任</h2>
<p>Confidence 要定義等級、證據依據、限制與下一步。它要與 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 和 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a> 一起保存，避免團隊把暫時結論當成穩定事實。</p>
]]></content:encoded></item><item><title>Known Gap</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/</guid><description>&lt;p>Known gap 的核心概念是「把已知但尚未覆蓋的證據缺口寫進 artifact」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a>，讓缺口能被追蹤、交班與回寫。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Known gap 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 之間。Data quality 說明資料限制，known gap 則列出目前尚未被證據覆蓋的具體範圍。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 known gap 的訊號是：&lt;/p>
&lt;ul>
&lt;li>某些 tenant、region、callback path 或 manual repair path 未被抽樣&lt;/li>
&lt;li>trace 或 log 缺少關鍵 span / field&lt;/li>
&lt;li>release gate 放行時仍有需要 follow-up 的證據缺口&lt;/li>
&lt;li>PIR 需要把缺口回寫成 readiness 或 observability 改善項&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration evidence package 可以記錄 &lt;code>manual refund repair path not yet sampled&lt;/code>。這個 known gap 會限制 cutover decision，並回寫成後續 validation query 或 audit log coverage 的改善項。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Known gap 要描述缺口內容、影響範圍、目前風險、owner 與 follow-up。它要支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a> 分級，避免 evidence package 看起來完整，但實際漏掉高風險路徑。&lt;/p></description><content:encoded><![CDATA[<p>Known gap 的核心概念是「把已知但尚未覆蓋的證據缺口寫進 artifact」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a> 與 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a>，讓缺口能被追蹤、交班與回寫。</p>
<h2 id="概念位置">概念位置</h2>
<p>Known gap 位在 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a>、<a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 之間。Data quality 說明資料限制，known gap 則列出目前尚未被證據覆蓋的具體範圍。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 known gap 的訊號是：</p>
<ul>
<li>某些 tenant、region、callback path 或 manual repair path 未被抽樣</li>
<li>trace 或 log 缺少關鍵 span / field</li>
<li>release gate 放行時仍有需要 follow-up 的證據缺口</li>
<li>PIR 需要把缺口回寫成 readiness 或 observability 改善項</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration evidence package 可以記錄 <code>manual refund repair path not yet sampled</code>。這個 known gap 會限制 cutover decision，並回寫成後續 validation query 或 audit log coverage 的改善項。</p>
<h2 id="設計責任">設計責任</h2>
<p>Known gap 要描述缺口內容、影響範圍、目前風險、owner 與 follow-up。它要支援 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a> 分級，避免 evidence package 看起來完整，但實際漏掉高風險路徑。</p>
]]></content:encoded></item><item><title>Recording Rule</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/</guid><description>&lt;p>Recording rule 把重複的聚合計算從查詢時推到寫入時。當 dashboard 或 alert 反覆對同一組 raw &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 做 rate / sum / histogram_quantile，每次查詢都重新掃描原始資料；recording rule 把計算結果預先寫成新的 time series，查詢時直接讀取結果。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Recording rule 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 讀取路徑的效能工具。它在 TSDB 層（如 Prometheus、Thanos、Mimir）定期執行 query expression，把結果作為新 series 寫入儲存。概念上類似 OLAP 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a>，但作用在時間序列而非關聯式資料。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 recording rule 時要定義計算表達式、執行間隔、命名慣例與維護責任。命名慣例通常遵循 &lt;code>level:metric:operations&lt;/code> 格式（如 &lt;code>job:http_requests_total:rate5m&lt;/code>），讓讀者從名稱判斷來源、粒度與計算方式。&lt;/p>
&lt;p>Recording rule 產生的 series 本身也佔儲存空間與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>。規則數量增長時，要監控 rule evaluation duration 跟 rule group lag，避免 rule 跑不完的情況讓 dashboard 看到過期資料。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 recording rule 的訊號是 dashboard panel 載入時間持續退化、或 alert rule 因為 query timeout 而漏發。把 SLO burn rate 計算、高流量 endpoint 的 rate 與 error ratio 預先聚合成 recording rule，是最常見的起點。&lt;/p>
&lt;p>Recording rule 與 raw query 的分工：高頻讀取（dashboard 自動刷新、alert 每分鐘 evaluate）適合 recording rule；低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。&lt;/p>
&lt;p>在觀測領域的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Recording rule 把重複的聚合計算從查詢時推到寫入時。當 dashboard 或 alert 反覆對同一組 raw <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 做 rate / sum / histogram_quantile，每次查詢都重新掃描原始資料；recording rule 把計算結果預先寫成新的 time series，查詢時直接讀取結果。</p>
<h2 id="概念位置">概念位置</h2>
<p>Recording rule 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 讀取路徑的效能工具。它在 TSDB 層（如 Prometheus、Thanos、Mimir）定期執行 query expression，把結果作為新 series 寫入儲存。概念上類似 OLAP 的 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a>，但作用在時間序列而非關聯式資料。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 recording rule 時要定義計算表達式、執行間隔、命名慣例與維護責任。命名慣例通常遵循 <code>level:metric:operations</code> 格式（如 <code>job:http_requests_total:rate5m</code>），讓讀者從名稱判斷來源、粒度與計算方式。</p>
<p>Recording rule 產生的 series 本身也佔儲存空間與 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>。規則數量增長時，要監控 rule evaluation duration 跟 rule group lag，避免 rule 跑不完的情況讓 dashboard 看到過期資料。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 recording rule 的訊號是 dashboard panel 載入時間持續退化、或 alert rule 因為 query timeout 而漏發。把 SLO burn rate 計算、高流量 endpoint 的 rate 與 error ratio 預先聚合成 recording rule，是最常見的起點。</p>
<p>Recording rule 與 raw query 的分工：高頻讀取（dashboard 自動刷新、alert 每分鐘 evaluate）適合 recording rule；低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。</p>
<p>在觀測領域的應用見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢</a> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Rollup / Downsampling</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/</guid><description>&lt;p>Rollup 用降低時間精度換取儲存成本與查詢效能。原始資料以秒級或分鐘級採集，隨時間推移被聚合成更粗的粒度（5 分鐘、1 小時、1 天），舊的高精度資料可以刪除或歸檔。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 在時間維度的具體實作，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 分工互補。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Rollup 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 在時間維度的具體實作。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 的差別在於：recording rule 是降維度（把多個 label 聚合成一條 series），rollup 是降時間精度（把 15 秒的點變成 5 分鐘的點）。兩者經常搭配使用。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 rollup 時要定義每一層的精度、保留期、聚合函數與查詢路由規則。聚合函數的選擇影響查詢語意：對 counter 做 sum 跟對 gauge 做 average 是合理的；但對 histogram 做 average 會失去分布資訊。&lt;/p>
&lt;p>查詢路由是 rollup 設計的關鍵配套。使用者查詢 7 天範圍時系統自動路由到 5 分鐘粒度、查詢 90 天範圍時路由到 1 小時粒度。若路由不透明，使用者會對精度差異產生困惑。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 rollup 的訊號是 TSDB 儲存成本持續成長、長時間範圍的 dashboard panel 查詢逾時、或保留政策因為儲存限制被迫縮短。Thanos compactor、Cortex/Mimir compactor、VictoriaMetrics downsampling 都是常見實作。&lt;/p>
&lt;p>在觀測領域的查詢設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Rollup 用降低時間精度換取儲存成本與查詢效能。原始資料以秒級或分鐘級採集，隨時間推移被聚合成更粗的粒度（5 分鐘、1 小時、1 天），舊的高精度資料可以刪除或歸檔。它是 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 在時間維度的具體實作，跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 分工互補。</p>
<h2 id="概念位置">概念位置</h2>
<p>Rollup 是 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 在時間維度的具體實作。它跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 的差別在於：recording rule 是降維度（把多個 label 聚合成一條 series），rollup 是降時間精度（把 15 秒的點變成 5 分鐘的點）。兩者經常搭配使用。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 rollup 時要定義每一層的精度、保留期、聚合函數與查詢路由規則。聚合函數的選擇影響查詢語意：對 counter 做 sum 跟對 gauge 做 average 是合理的；但對 histogram 做 average 會失去分布資訊。</p>
<p>查詢路由是 rollup 設計的關鍵配套。使用者查詢 7 天範圍時系統自動路由到 5 分鐘粒度、查詢 90 天範圍時路由到 1 小時粒度。若路由不透明，使用者會對精度差異產生困惑。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 rollup 的訊號是 TSDB 儲存成本持續成長、長時間範圍的 dashboard panel 查詢逾時、或保留政策因為儲存限制被迫縮短。Thanos compactor、Cortex/Mimir compactor、VictoriaMetrics downsampling 都是常見實作。</p>
<p>在觀測領域的查詢設計見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢</a> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Storage Tiering</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/</guid><description>&lt;p>Storage tiering 按資料被查詢的頻率與時間壓力，把資料放在不同速度與成本的儲存層。最近的資料放在快速儲存（hot tier），較舊的資料依序移到較慢但便宜的儲存（warm tier、cold tier），最終可歸檔到 object storage 或離線備份。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 共同構成觀測資料的生命週期管理，受 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 期限驅動。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Storage tiering 是觀測資料管理的基礎設施層決策，影響查詢能力、成本結構與保留政策。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 的分工是：tiering 決定資料放在哪種儲存、rollup 決定資料以什麼精度存放。兩者共同構成觀測資料的生命週期管理。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 tiering 時要定義每一層的查詢 SLA、儲存成本、資料轉移觸發條件與跨層查詢行為。&lt;/p>
&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>Hot&lt;/td>
 &lt;td>SSD / in-memory TSDB&lt;/td>
 &lt;td>毫秒到秒&lt;/td>
 &lt;td>原始精度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm&lt;/td>
 &lt;td>HDD / 分散式儲存&lt;/td>
 &lt;td>秒到十秒&lt;/td>
 &lt;td>原始或輕度 rollup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cold&lt;/td>
 &lt;td>Object storage / S3&lt;/td>
 &lt;td>十秒到分鐘&lt;/td>
 &lt;td>rollup 或歸檔&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跨層查詢是 tiering 設計的關鍵問題。當查詢範圍橫跨 hot 跟 warm 兩層時，回應時間由最慢的那層決定。使用者在 dashboard 把時間範圍從「最近 1 小時」拉到「最近 7 天」時，查詢延遲可能從毫秒跳到秒級，體驗落差需要在 UI 或文件中說明。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 tiering 的訊號是觀測儲存成本持續成長但大部分查詢只命中最近的資料、或保留期因為成本壓力被迫縮短導致鑑識與稽核需求無法滿足。Elasticsearch ILM、Loki 的 chunk storage 分層、Thanos / Cortex 的 object storage backend 都是常見實作。&lt;/p>
&lt;p>Tiering 對查詢能力的影響見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 治理&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Storage tiering 按資料被查詢的頻率與時間壓力，把資料放在不同速度與成本的儲存層。最近的資料放在快速儲存（hot tier），較舊的資料依序移到較慢但便宜的儲存（warm tier、cold tier），最終可歸檔到 object storage 或離線備份。它跟 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 共同構成觀測資料的生命週期管理，受 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 期限驅動。</p>
<h2 id="概念位置">概念位置</h2>
<p>Storage tiering 是觀測資料管理的基礎設施層決策，影響查詢能力、成本結構與保留政策。它跟 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的分工是：tiering 決定資料放在哪種儲存、rollup 決定資料以什麼精度存放。兩者共同構成觀測資料的生命週期管理。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 tiering 時要定義每一層的查詢 SLA、儲存成本、資料轉移觸發條件與跨層查詢行為。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型儲存</th>
          <th>查詢延遲</th>
          <th>資料精度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>SSD / in-memory TSDB</td>
          <td>毫秒到秒</td>
          <td>原始精度</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>HDD / 分散式儲存</td>
          <td>秒到十秒</td>
          <td>原始或輕度 rollup</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>Object storage / S3</td>
          <td>十秒到分鐘</td>
          <td>rollup 或歸檔</td>
      </tr>
  </tbody>
</table>
<p>跨層查詢是 tiering 設計的關鍵問題。當查詢範圍橫跨 hot 跟 warm 兩層時，回應時間由最慢的那層決定。使用者在 dashboard 把時間範圍從「最近 1 小時」拉到「最近 7 天」時，查詢延遲可能從毫秒跳到秒級，體驗落差需要在 UI 或文件中說明。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 tiering 的訊號是觀測儲存成本持續成長但大部分查詢只命中最近的資料、或保留期因為成本壓力被迫縮短導致鑑識與稽核需求無法滿足。Elasticsearch ILM、Loki 的 chunk storage 分層、Thanos / Cortex 的 object storage backend 都是常見實作。</p>
<p>Tiering 對查詢能力的影響見 <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> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Materialized View</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/</guid><description>&lt;p>Materialized view 把查詢結果預先計算並持久儲存，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作方式。它跟一般 view 的差別在於 materialized view 有實體儲存，查詢時讀取的是快照而非即時計算。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Materialized view 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作方式。在關聯式資料庫中它是 SQL-level 的物化查詢；在觀測領域，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 扮演類似角色 — 把聚合計算的結果寫成新的 time series。兩者的共同設計問題是更新頻率、一致性延遲與維護成本。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 materialized view 時要定義刷新策略（定時 / 觸發 / 手動）、資料新鮮度容忍上限、儲存成本與失效重建流程。刷新頻率決定讀取的 freshness — 每分鐘刷新的 materialized view 最多落後一分鐘，對 dashboard 場景通常足夠，對即席事故診斷可能不夠。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 materialized view 的訊號是同一個複雜查詢被多個消費者反覆執行（dashboard panel、定期報表、alert rule），而且每次查詢的計算成本高到影響原始資料源的效能。在觀測場景中，SLO burn rate、跨服務 error ratio、多維度 latency percentile 是常見的 materialization 候選。&lt;/p>
&lt;p>在資料庫的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership&lt;/a>。在觀測領域的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Materialized view 把查詢結果預先計算並持久儲存，是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作方式。它跟一般 view 的差別在於 materialized view 有實體儲存，查詢時讀取的是快照而非即時計算。</p>
<h2 id="概念位置">概念位置</h2>
<p>Materialized view 是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作方式。在關聯式資料庫中它是 SQL-level 的物化查詢；在觀測領域，<a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 扮演類似角色 — 把聚合計算的結果寫成新的 time series。兩者的共同設計問題是更新頻率、一致性延遲與維護成本。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 materialized view 時要定義刷新策略（定時 / 觸發 / 手動）、資料新鮮度容忍上限、儲存成本與失效重建流程。刷新頻率決定讀取的 freshness — 每分鐘刷新的 materialized view 最多落後一分鐘，對 dashboard 場景通常足夠，對即席事故診斷可能不夠。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 materialized view 的訊號是同一個複雜查詢被多個消費者反覆執行（dashboard panel、定期報表、alert rule），而且每次查詢的計算成本高到影響原始資料源的效能。在觀測場景中，SLO burn rate、跨服務 error ratio、多維度 latency percentile 是常見的 materialization 候選。</p>
<p>在資料庫的應用見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a>。在觀測領域的應用見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>CQRS</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/</guid><description>&lt;p>CQRS（Command Query Responsibility Segregation）的核心概念是「把寫入路徑跟讀取路徑拆成各自獨立的模型，各自依自身需求最佳化」。分離後讀取面的具體產物是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a>。它處理的根本問題是讀寫不對稱 — 同一份資料的寫入形狀跟讀取形狀不同、寫入頻率跟讀取頻率不同、寫入 SLA 跟讀取 SLA 不同。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>CQRS 是一種架構分離策略，位於資料存取模式的設計層。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的關係是：CQRS 是分離的決策框架，read model 是分離之後「讀取面」的具體產物。&lt;/p>
&lt;p>CQRS 經常跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 一起出現，但兩者是獨立概念。CQRS 只要求讀寫模型分離；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing&lt;/a> 是把寫入模型改成 append-only 的事件流。可以有 CQRS 但沒有 event sourcing（寫入仍用傳統 CRUD，讀取用獨立的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a>），也可以有 event sourcing 但沒有 CQRS（讀寫都直接操作 event store）。&lt;/p>
&lt;h2 id="讀寫不對稱的三個維度">讀寫不對稱的三個維度&lt;/h2>
&lt;p>分離的動機來自三種不對稱，當任一種超過單一模型能承受的範圍時，CQRS 開始有設計價值。&lt;/p>
&lt;p>&lt;strong>形狀不對稱&lt;/strong>：寫入時資料以正規化、事務安全的結構進入系統；讀取時不同消費者需要不同的反正規化形狀。一個訂單寫入時是 order + line items + payment 三張表的事務；列表頁需要扁平的 order summary，報表需要跨訂單的聚合，搜尋需要全文索引。強迫同一個模型同時服務這些形狀，會讓寫入模型變得過度複雜或讀取效能退化。&lt;/p>
&lt;p>&lt;strong>頻率不對稱&lt;/strong>：讀取頻率遠高於寫入頻率是常見的服務模型（商品頁的瀏覽量遠大於商品更新頻率）。讀寫共用模型時，高頻讀取的效能需求會推動寫入模型往讀取最佳化靠攏，犧牲寫入的簡潔性跟一致性保證。&lt;/p>
&lt;p>&lt;strong>SLA 不對稱&lt;/strong>：不同讀取消費者的延遲容忍跟一致性需求不同。即時顯示需要毫秒級回應但容忍短暫不一致；報表需要完整一致但容忍分鐘級延遲；稽核需要長期可查但容忍更高延遲。單一模型難以同時滿足多種 SLA。&lt;/p>
&lt;h2 id="分離的設計判準">分離的設計判準&lt;/h2>
&lt;p>讀寫不對稱存在不代表一定需要 CQRS。分離的判準是不對稱的程度是否已經超過「在同一個模型上做最佳化」能解決的範圍。&lt;/p>
&lt;p>&lt;strong>可以不分離的情境&lt;/strong>：讀寫形狀接近（CRUD 應用、管理後台）、讀取消費者單一（只有一種 UI）、流量規模讓讀寫共用模型的效能足夠、團隊規模小到維護兩套模型的成本大於效能收益。&lt;/p>
&lt;p>&lt;strong>需要考慮分離的訊號&lt;/strong>：讀取效能持續退化但寫入側無法再為讀取最佳化（加 index 已到極限、反正規化導致寫入複雜度上升）；多種讀取消費者對同一份資料有互斥的形狀需求；讀寫的擴展需求方向不同（讀取要水平擴展、寫入要強一致性）。&lt;/p>
&lt;h2 id="分離的代價">分離的代價&lt;/h2>
&lt;p>CQRS 的代價集中在同步、一致性與維護三個面向。&lt;/p>
&lt;p>&lt;strong>最終一致性&lt;/strong>：read model 透過事件或同步機制從 write model 更新，中間有延遲。使用者寫入後立即讀取可能看不到自己的變更。這個延遲窗口需要被明確設計（多長、可接受嗎、UI 怎麼處理）而非假裝不存在。&lt;/p>
&lt;p>&lt;strong>同步機制的可靠性&lt;/strong>：write model 到 read model 的同步本身是一個需要監控跟治理的資料路徑。同步失敗、同步延遲、同步漂移都需要被偵測跟處理。&lt;/p>
&lt;p>&lt;strong>多模型維護&lt;/strong>：schema 變更需要同時更新 write model 跟所有 read model。read model 的數量增長後，每次 schema migration 的變更面會擴大。&lt;/p>
&lt;h2 id="跨領域的應用">跨領域的應用&lt;/h2>
&lt;p>讀寫分離的設計張力不限於 application data。觀測資料的讀取路徑設計（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>）面臨同樣的不對稱：寫入是高吞吐的 append-only，讀取被至少三種不同 SLA 的消費者（即席診斷、聚合趨勢、鑑識回溯）拉扯。觀測領域用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 來實作讀寫分離，概念上對應 CQRS 的 read model，但術語跟實作層級不同。&lt;/p>
&lt;p>Message queue 的消費端也有類似結構：同一份事件被多個 consumer 以不同速度、不同形狀讀取，fan-out 跟 consumer group 是另一種讀寫分離的實作。&lt;/p></description><content:encoded><![CDATA[<p>CQRS（Command Query Responsibility Segregation）的核心概念是「把寫入路徑跟讀取路徑拆成各自獨立的模型，各自依自身需求最佳化」。分離後讀取面的具體產物是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>。它處理的根本問題是讀寫不對稱 — 同一份資料的寫入形狀跟讀取形狀不同、寫入頻率跟讀取頻率不同、寫入 SLA 跟讀取 SLA 不同。</p>
<h2 id="概念位置">概念位置</h2>
<p>CQRS 是一種架構分離策略，位於資料存取模式的設計層。它跟 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的關係是：CQRS 是分離的決策框架，read model 是分離之後「讀取面」的具體產物。</p>
<p>CQRS 經常跟 <a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 一起出現，但兩者是獨立概念。CQRS 只要求讀寫模型分離；<a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">event sourcing</a> 是把寫入模型改成 append-only 的事件流。可以有 CQRS 但沒有 event sourcing（寫入仍用傳統 CRUD，讀取用獨立的 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>），也可以有 event sourcing 但沒有 CQRS（讀寫都直接操作 event store）。</p>
<h2 id="讀寫不對稱的三個維度">讀寫不對稱的三個維度</h2>
<p>分離的動機來自三種不對稱，當任一種超過單一模型能承受的範圍時，CQRS 開始有設計價值。</p>
<p><strong>形狀不對稱</strong>：寫入時資料以正規化、事務安全的結構進入系統；讀取時不同消費者需要不同的反正規化形狀。一個訂單寫入時是 order + line items + payment 三張表的事務；列表頁需要扁平的 order summary，報表需要跨訂單的聚合，搜尋需要全文索引。強迫同一個模型同時服務這些形狀，會讓寫入模型變得過度複雜或讀取效能退化。</p>
<p><strong>頻率不對稱</strong>：讀取頻率遠高於寫入頻率是常見的服務模型（商品頁的瀏覽量遠大於商品更新頻率）。讀寫共用模型時，高頻讀取的效能需求會推動寫入模型往讀取最佳化靠攏，犧牲寫入的簡潔性跟一致性保證。</p>
<p><strong>SLA 不對稱</strong>：不同讀取消費者的延遲容忍跟一致性需求不同。即時顯示需要毫秒級回應但容忍短暫不一致；報表需要完整一致但容忍分鐘級延遲；稽核需要長期可查但容忍更高延遲。單一模型難以同時滿足多種 SLA。</p>
<h2 id="分離的設計判準">分離的設計判準</h2>
<p>讀寫不對稱存在不代表一定需要 CQRS。分離的判準是不對稱的程度是否已經超過「在同一個模型上做最佳化」能解決的範圍。</p>
<p><strong>可以不分離的情境</strong>：讀寫形狀接近（CRUD 應用、管理後台）、讀取消費者單一（只有一種 UI）、流量規模讓讀寫共用模型的效能足夠、團隊規模小到維護兩套模型的成本大於效能收益。</p>
<p><strong>需要考慮分離的訊號</strong>：讀取效能持續退化但寫入側無法再為讀取最佳化（加 index 已到極限、反正規化導致寫入複雜度上升）；多種讀取消費者對同一份資料有互斥的形狀需求；讀寫的擴展需求方向不同（讀取要水平擴展、寫入要強一致性）。</p>
<h2 id="分離的代價">分離的代價</h2>
<p>CQRS 的代價集中在同步、一致性與維護三個面向。</p>
<p><strong>最終一致性</strong>：read model 透過事件或同步機制從 write model 更新，中間有延遲。使用者寫入後立即讀取可能看不到自己的變更。這個延遲窗口需要被明確設計（多長、可接受嗎、UI 怎麼處理）而非假裝不存在。</p>
<p><strong>同步機制的可靠性</strong>：write model 到 read model 的同步本身是一個需要監控跟治理的資料路徑。同步失敗、同步延遲、同步漂移都需要被偵測跟處理。</p>
<p><strong>多模型維護</strong>：schema 變更需要同時更新 write model 跟所有 read model。read model 的數量增長後，每次 schema migration 的變更面會擴大。</p>
<h2 id="跨領域的應用">跨領域的應用</h2>
<p>讀寫分離的設計張力不限於 application data。觀測資料的讀取路徑設計（<a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>）面臨同樣的不對稱：寫入是高吞吐的 append-only，讀取被至少三種不同 SLA 的消費者（即席診斷、聚合趨勢、鑑識回溯）拉扯。觀測領域用 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a>、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a>、<a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 來實作讀寫分離，概念上對應 CQRS 的 read model，但術語跟實作層級不同。</p>
<p>Message queue 的消費端也有類似結構：同一份事件被多個 consumer 以不同速度、不同形狀讀取，fan-out 跟 consumer group 是另一種讀寫分離的實作。</p>
]]></content:encoded></item><item><title>Event Sourcing</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/event-sourcing/</guid><description>&lt;p>Event sourcing 的核心概念是「不存 current state、存產生 current state 的所有事件」。儲存層是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a>，讀取面透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 推算 current state。每一次狀態變更被記錄為一筆不可變的事件（event），current state 透過重播（replay）事件序列推算出來。正式紀錄是事件流本身，current state 是派生物。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Event sourcing 是一種資料持久化策略，改變的是「狀態怎麼被記錄」而非「狀態怎麼被讀取」。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 經常搭配但概念獨立 — event sourcing 處理寫入模型（append-only event log 取代 mutable row），CQRS 處理讀寫分離。可以有 event sourcing 但沒有 CQRS（讀寫都直接操作 event store），也可以有 CQRS 但沒有 event sourcing（寫入仍用 CRUD）。&lt;/p>
&lt;p>Event sourcing 的儲存層是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a>。讀取面透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 把事件流轉換成查詢用的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a>。&lt;/p>
&lt;h2 id="設計判準">設計判準&lt;/h2>
&lt;p>Event sourcing 的設計價值來自「需要完整變更歷史」的業務需求。判準是：業務是否需要回答「某個時間點的狀態是什麼」或「狀態怎麼從 A 變成 B」。&lt;/p>
&lt;p>&lt;strong>適合的場景&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>金融帳務 — 餘額的每一筆增減都是 audit 事件，法規要求能追溯任意時點的 balance&lt;/li>
&lt;li>訂單流程 — 每個狀態轉換（建立→付款→出貨→完成）是 business event，需要重建任意階段&lt;/li>
&lt;li>法規合規 — 完整變更歷史是合規證據，刪除或覆寫正式紀錄違反要求&lt;/li>
&lt;li>需要 replay 能力 — downstream consumer 落後或資料損壞時，能從 event log 重建&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不適合的場景&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>簡單 CRUD — 狀態覆寫即可、不需要歷史、event sourcing 的 overhead 遠大於收益&lt;/li>
&lt;li>需要直接查 current state 的高頻場景 — 每次讀取都 replay 整條事件流延遲太高，必須搭配 projection 維護 snapshot，增加系統複雜度&lt;/li>
&lt;li>事件 schema 變更頻繁 — 舊事件需要被新版 schema 正確 replay，schema evolution 成本高&lt;/li>
&lt;/ul>
&lt;h2 id="代價">代價&lt;/h2>
&lt;p>&lt;strong>讀取複雜度&lt;/strong>：current state 不再是一筆 row，而是需要 replay 或 projection 推算。讀取路徑的設計從「查一筆 record」變成「維護多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> + 保證 projection 正確性 + 處理 projection lag」。&lt;/p>
&lt;p>&lt;strong>事件 schema evolution&lt;/strong>：事件一旦寫入就不可變，但業務需求會改變事件結構。版本化 event schema（upcasting）是長期維護的核心挑戰 — 新版 projection 要能正確消費舊版事件。&lt;/p>
&lt;p>&lt;strong>儲存成長&lt;/strong>：事件永不刪除（或只做 retention），儲存量隨時間持續成長。高頻寫入的系統可能需要 snapshot 機制（定期存一份 current state 快照，replay 從 snapshot 開始而非從頭）來控制 replay 時間。&lt;/p>
&lt;p>&lt;strong>除錯難度&lt;/strong>：bug 可能是某個 event handler 在 replay 時產生錯誤結果。除錯需要重現特定事件序列的 replay，比查一筆 mutable record 的 diff 更複雜。&lt;/p>
&lt;h2 id="跟其他概念的關係">跟其他概念的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log&lt;/a> — event sourcing 的儲存層，append-only 的事件序列&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">Projection&lt;/a> — 把 event log 轉換成可查詢的 read model 的機制&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">Read model&lt;/a> — projection 的輸出，為特定查詢需求最佳化的資料形狀&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> — 讀寫分離的設計框架，event sourcing 是其中一種 write model 實作&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">Saga&lt;/a> — 跨服務的分散事務，event sourcing 提供每個 step 的事件紀錄&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Event sourcing 的核心概念是「不存 current state、存產生 current state 的所有事件」。儲存層是 <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>，讀取面透過 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 推算 current state。每一次狀態變更被記錄為一筆不可變的事件（event），current state 透過重播（replay）事件序列推算出來。正式紀錄是事件流本身，current state 是派生物。</p>
<h2 id="概念位置">概念位置</h2>
<p>Event sourcing 是一種資料持久化策略，改變的是「狀態怎麼被記錄」而非「狀態怎麼被讀取」。它跟 <a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 經常搭配但概念獨立 — event sourcing 處理寫入模型（append-only event log 取代 mutable row），CQRS 處理讀寫分離。可以有 event sourcing 但沒有 CQRS（讀寫都直接操作 event store），也可以有 CQRS 但沒有 event sourcing（寫入仍用 CRUD）。</p>
<p>Event sourcing 的儲存層是 <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>。讀取面透過 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 把事件流轉換成查詢用的 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>。</p>
<h2 id="設計判準">設計判準</h2>
<p>Event sourcing 的設計價值來自「需要完整變更歷史」的業務需求。判準是：業務是否需要回答「某個時間點的狀態是什麼」或「狀態怎麼從 A 變成 B」。</p>
<p><strong>適合的場景</strong>：</p>
<ul>
<li>金融帳務 — 餘額的每一筆增減都是 audit 事件，法規要求能追溯任意時點的 balance</li>
<li>訂單流程 — 每個狀態轉換（建立→付款→出貨→完成）是 business event，需要重建任意階段</li>
<li>法規合規 — 完整變更歷史是合規證據，刪除或覆寫正式紀錄違反要求</li>
<li>需要 replay 能力 — downstream consumer 落後或資料損壞時，能從 event log 重建</li>
</ul>
<p><strong>不適合的場景</strong>：</p>
<ul>
<li>簡單 CRUD — 狀態覆寫即可、不需要歷史、event sourcing 的 overhead 遠大於收益</li>
<li>需要直接查 current state 的高頻場景 — 每次讀取都 replay 整條事件流延遲太高，必須搭配 projection 維護 snapshot，增加系統複雜度</li>
<li>事件 schema 變更頻繁 — 舊事件需要被新版 schema 正確 replay，schema evolution 成本高</li>
</ul>
<h2 id="代價">代價</h2>
<p><strong>讀取複雜度</strong>：current state 不再是一筆 row，而是需要 replay 或 projection 推算。讀取路徑的設計從「查一筆 record」變成「維護多個 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> + 保證 projection 正確性 + 處理 projection lag」。</p>
<p><strong>事件 schema evolution</strong>：事件一旦寫入就不可變，但業務需求會改變事件結構。版本化 event schema（upcasting）是長期維護的核心挑戰 — 新版 projection 要能正確消費舊版事件。</p>
<p><strong>儲存成長</strong>：事件永不刪除（或只做 retention），儲存量隨時間持續成長。高頻寫入的系統可能需要 snapshot 機制（定期存一份 current state 快照，replay 從 snapshot 開始而非從頭）來控制 replay 時間。</p>
<p><strong>除錯難度</strong>：bug 可能是某個 event handler 在 replay 時產生錯誤結果。除錯需要重現特定事件序列的 replay，比查一筆 mutable record 的 diff 更複雜。</p>
<h2 id="跟其他概念的關係">跟其他概念的關係</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">Event log</a> — event sourcing 的儲存層，append-only 的事件序列</li>
<li><a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">Projection</a> — 把 event log 轉換成可查詢的 read model 的機制</li>
<li><a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">Read model</a> — projection 的輸出，為特定查詢需求最佳化的資料形狀</li>
<li><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> — 讀寫分離的設計框架，event sourcing 是其中一種 write model 實作</li>
<li><a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">Saga</a> — 跨服務的分散事務，event sourcing 提供每個 step 的事件紀錄</li>
</ul>
]]></content:encoded></item><item><title>Outbound Tunnel</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/outbound-tunnel/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/outbound-tunnel/</guid><description>&lt;p>Outbound tunnel 是一種入口形態：本機進程主動對外連到邊緣節點，把流量沿反向隧道帶回來，路由器零開 port、對公網零入站面。跟傳統 port-forward（從外往內開 port）的責任方向相反 — 連線由內部發起、外部只能沿已建立的隧道回來。與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a> 的責任方向不同：LB 假設 instance 有公開可達位址，tunnel 由內部主動外連。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Outbound tunnel 位在本機進程與公網之間，取代傳統的 port-forward 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a> 入口。常與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS&lt;/a> 搭配保護隧道內的傳輸安全，認證則疊在 tunnel 之後由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication-middleware/" data-link-title="Authentication Middleware" data-link-desc="說明請求進入 handler 前如何完成身份驗證">authentication middleware&lt;/a> 處理。&lt;/p>
&lt;p>常見實作包括 cloudflared（綁 Cloudflare 邊緣）和 Tailscale（WireGuard mesh VPN）。隧道網址是位址、不是密碼 — 認證必須疊在 tunnel 之後。&lt;/p>
&lt;p>深入：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口與生命週期&lt;/a>。選型案例：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &amp;#43; Access 兩種存取模型的選型判讀。">7.C11 Tailscale vs Cloudflare Tunnel&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Outbound tunnel 是一種入口形態：本機進程主動對外連到邊緣節點，把流量沿反向隧道帶回來，路由器零開 port、對公網零入站面。跟傳統 port-forward（從外往內開 port）的責任方向相反 — 連線由內部發起、外部只能沿已建立的隧道回來。與 <a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 的責任方向不同：LB 假設 instance 有公開可達位址，tunnel 由內部主動外連。</p>
<h2 id="概念位置">概念位置</h2>
<p>Outbound tunnel 位在本機進程與公網之間，取代傳統的 port-forward 或 <a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 入口。常與 <a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS / mTLS</a> 搭配保護隧道內的傳輸安全，認證則疊在 tunnel 之後由 <a href="/blog/backend/knowledge-cards/authentication-middleware/" data-link-title="Authentication Middleware" data-link-desc="說明請求進入 handler 前如何完成身份驗證">authentication middleware</a> 處理。</p>
<p>常見實作包括 cloudflared（綁 Cloudflare 邊緣）和 Tailscale（WireGuard mesh VPN）。隧道網址是位址、不是密碼 — 認證必須疊在 tunnel 之後。</p>
<p>深入：<a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口與生命週期</a>。選型案例：<a href="/blog/backend/07-security-data-protection/cases/remote-shell-access-tailscale-vs-cloudflare-tunnel/" data-link-title="7.C11 選型：單人遠端 Shell — Tailscale vs Cloudflare Tunnel" data-link-desc="以「手機遠端操作本機 shell」為情境，比較 Tailscale mesh VPN 與 Cloudflare Tunnel &#43; Access 兩種存取模型的選型判讀。">7.C11 Tailscale vs Cloudflare Tunnel</a>。</p>
]]></content:encoded></item><item><title>7.R0 紅隊基礎：攻擊流程作為服務判讀語言</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/red-team-basics-and-attack-flow/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/red-team-basics-and-attack-flow/</guid><description>&lt;p>本章的責任是提供一套共用判讀語言，讓團隊在討論案例時先對齊服務環節與風險節奏。紅隊在本教材中的定位是攻擊者視角的風險檢查方法，核心輸出是問題地圖與注意事項，並把防護實作需求路由到對應服務章節。&lt;/p>
&lt;h2 id="服務視角定位">服務視角定位&lt;/h2>
&lt;p>紅隊分析把事件拆回服務生命週期，目標是回答三件事：入口在哪裡、擴散怎麼發生、衝擊如何形成。這個拆法讓架構設計、事故處理、案例引用可以使用同一組語言。&lt;/p>
&lt;h2 id="攻擊流程六段判讀">攻擊流程六段判讀&lt;/h2>
&lt;ol>
&lt;li>偵察：攻擊者先看見可達入口與可枚舉資源。&lt;/li>
&lt;li>初始進入：攻擊者取得第一個可操作落點。&lt;/li>
&lt;li>權限擴張與持續控制：攻擊者提升可操作範圍並維持進入能力。&lt;/li>
&lt;li>橫向移動：攻擊者沿著服務邊界進入其他系統。&lt;/li>
&lt;li>目標行動：攻擊者進行資料蒐集、外送或營運衝擊。&lt;/li>
&lt;li>掩護與延長停留：攻擊者降低被發現機率並延長影響期。&lt;/li>
&lt;/ol>
&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>入口驗證通過後仍可快速擴散&lt;/td>
 &lt;td>高風險動作需要獨立判讀節奏&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方整合&lt;/td>
 &lt;td>供應商事件可直接傳導到內部流程&lt;/td>
 &lt;td>事件觸發與憑證收斂需要同一條路由&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界與入口&lt;/td>
 &lt;td>暴露面與修補窗口同時放大風險&lt;/td>
 &lt;td>入口隔離、分區修補、狀態驗證要同時規劃&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付與供應鏈&lt;/td>
 &lt;td>合法交付路徑可被反向利用&lt;/td>
 &lt;td>凍結、驗證、恢復需要明確順序&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料與回復&lt;/td>
 &lt;td>外送風險與營運衝擊常同時出現&lt;/td>
 &lt;td>資料盤點、回復排序、通報節奏需要連動&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="與其他章節的分工路由">與其他章節的分工路由&lt;/h2>
&lt;ul>
&lt;li>本章與 &lt;code>7.R6&lt;/code>、&lt;code>7.R7&lt;/code>：提供問題判讀與案例證據。&lt;/li>
&lt;li>&lt;code>backend/05-deployment-platform&lt;/code>：承接交付鏈與入口流量治理的實作。&lt;/li>
&lt;li>&lt;code>backend/06-reliability&lt;/code>：承接回復排序與可用性設計的實作。&lt;/li>
&lt;li>&lt;code>backend/08-incident-response&lt;/code>：承接事故分級、指揮、runbook 落地。&lt;/li>
&lt;/ul>
&lt;p>這個分工維持教材一致性：紅隊章節先回答「問題長什麼樣」，服務章節再回答「實際怎麼做」。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>進入 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/" data-link-title="7.R6 事故故事重構：服務環節問題與注意事項" data-link-desc="以統一模板整理案例：服務環節問題地圖、案例對照表與跨模組交接邊界">7.R6 事故故事：服務環節問題與注意事項&lt;/a> 之後，會把每個環節拆成可直接引用的判讀訊號與決策提醒。&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是提供一套共用判讀語言，讓團隊在討論案例時先對齊服務環節與風險節奏。紅隊在本教材中的定位是攻擊者視角的風險檢查方法，核心輸出是問題地圖與注意事項，並把防護實作需求路由到對應服務章節。</p>
<h2 id="服務視角定位">服務視角定位</h2>
<p>紅隊分析把事件拆回服務生命週期，目標是回答三件事：入口在哪裡、擴散怎麼發生、衝擊如何形成。這個拆法讓架構設計、事故處理、案例引用可以使用同一組語言。</p>
<h2 id="攻擊流程六段判讀">攻擊流程六段判讀</h2>
<ol>
<li>偵察：攻擊者先看見可達入口與可枚舉資源。</li>
<li>初始進入：攻擊者取得第一個可操作落點。</li>
<li>權限擴張與持續控制：攻擊者提升可操作範圍並維持進入能力。</li>
<li>橫向移動：攻擊者沿著服務邊界進入其他系統。</li>
<li>目標行動：攻擊者進行資料蒐集、外送或營運衝擊。</li>
<li>掩護與延長停留：攻擊者降低被發現機率並延長影響期。</li>
</ol>
<h2 id="服務環節問題地圖觀念層">服務環節問題地圖（觀念層）</h2>
<table>
  <thead>
      <tr>
          <th>服務環節</th>
          <th>主要問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身分與授權</td>
          <td>入口驗證通過後仍可快速擴散</td>
          <td>高風險動作需要獨立判讀節奏</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022</a></td>
      </tr>
      <tr>
          <td>第三方整合</td>
          <td>供應商事件可直接傳導到內部流程</td>
          <td>事件觸發與憑證收斂需要同一條路由</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a></td>
      </tr>
      <tr>
          <td>邊界與入口</td>
          <td>暴露面與修補窗口同時放大風險</td>
          <td>入口隔離、分區修補、狀態驗證要同時規劃</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></td>
      </tr>
      <tr>
          <td>交付與供應鏈</td>
          <td>合法交付路徑可被反向利用</td>
          <td>凍結、驗證、恢復需要明確順序</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></td>
      </tr>
      <tr>
          <td>資料與回復</td>
          <td>外送風險與營運衝擊常同時出現</td>
          <td>資料盤點、回復排序、通報節奏需要連動</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></td>
      </tr>
  </tbody>
</table>
<h2 id="與其他章節的分工路由">與其他章節的分工路由</h2>
<ul>
<li>本章與 <code>7.R6</code>、<code>7.R7</code>：提供問題判讀與案例證據。</li>
<li><code>backend/05-deployment-platform</code>：承接交付鏈與入口流量治理的實作。</li>
<li><code>backend/06-reliability</code>：承接回復排序與可用性設計的實作。</li>
<li><code>backend/08-incident-response</code>：承接事故分級、指揮、runbook 落地。</li>
</ul>
<p>這個分工維持教材一致性：紅隊章節先回答「問題長什麼樣」，服務章節再回答「實際怎麼做」。</p>
<h2 id="下一步">下一步</h2>
<p>進入 <a href="/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/" data-link-title="7.R6 事故故事重構：服務環節問題與注意事項" data-link-desc="以統一模板整理案例：服務環節問題地圖、案例對照表與跨模組交接邊界">7.R6 事故故事：服務環節問題與注意事項</a> 之後，會把每個環節拆成可直接引用的判讀訊號與決策提醒。</p>
]]></content:encoded></item><item><title>7.R1 攻擊面與信任邊界</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/attack-surface-boundary/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/attack-surface-boundary/</guid><description>&lt;p>本章處理紅隊（攻擊者視角的風險檢查）分析的第一步：建立完整攻擊面清單，並標註每條路徑跨越的信任邊界。目標是讓團隊在選型與設計早期就看見高風險入口，先完成優先順序，讓事故期間可直接使用既有盤點。&lt;/p>
&lt;h2 id="情境哪些服務需要先做攻擊面盤點">【情境】哪些服務需要先做攻擊面盤點&lt;/h2>
&lt;p>下列情境同時出現時，攻擊面與邊界章節應放在前面：&lt;/p>
&lt;ul>
&lt;li>對外入口超過一種（API、webhook、管理介面、診斷介面）&lt;/li>
&lt;li>角色與租戶模型持續擴張&lt;/li>
&lt;li>系統同時依賴多個內外部服務&lt;/li>
&lt;li>交付節奏快，設定變動頻繁&lt;/li>
&lt;/ul>
&lt;h2 id="判讀流程四步完成邊界標註">【判讀流程】四步完成邊界標註&lt;/h2>
&lt;ol>
&lt;li>列入口：整理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/public-api-endpoint/" data-link-title="Public API Endpoint" data-link-desc="說明面向外部 client 的穩定 API 入口如何被管理">Public API&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a>。&lt;/li>
&lt;li>畫資料流：每條入口都連到下游能力與資料資產，包含第三方整合。&lt;/li>
&lt;li>標信任切換點：在身份、租戶、網路、資料層切換處標示 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">Trust Boundary&lt;/a>。&lt;/li>
&lt;li>排優先級：以可枚舉性、可重放性、資料敏感度與橫向移動能力排序。&lt;/li>
&lt;/ol>
&lt;h2 id="風險代價盤點品質直接影響事故規模">【風險代價】盤點品質直接影響事故規模&lt;/h2>
&lt;p>盤點品質偏弱時，常見結果是高風險入口晚被辨識，導致事件初期難以聚焦調查。這會同時拉高停機時間、修復人力與溝通成本。盤點完整時，告警、稽核與隔離策略可以對齊同一張路徑圖，事故處理速度會明顯提升。&lt;/p>
&lt;h2 id="設計取捨入口密度與維運成本">【設計取捨】入口密度與維運成本&lt;/h2>
&lt;p>入口越多，產品迭代彈性越高；同時，邊界管理與稽核成本也會增加。設計上可透過入口分層、責任分離與固定命名規則控制複雜度，讓新入口進入流程時就能被標記與追蹤。&lt;/p>
&lt;h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義&lt;/h2>
&lt;ul>
&lt;li>入口清單與擁有者&lt;/li>
&lt;li>邊界切換規則與驗證責任&lt;/li>
&lt;li>高風險入口的告警與稽核路徑&lt;/li>
&lt;li>入口變更的審查節點與 release gate&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號何時代表攻擊面風險升高">【判讀訊號】何時代表攻擊面風險升高&lt;/h2>
&lt;ul>
&lt;li>新增入口未同步出現在入口清單&lt;/li>
&lt;li>管理入口與公開入口混用同一條流量路徑&lt;/li>
&lt;li>同一入口同時承載人員操作與機器操作&lt;/li>
&lt;li>入口事件難以對應到明確擁有者與責任邊界&lt;/li>
&lt;/ul>
&lt;h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層&lt;/h2>
&lt;p>本章在概念層回答的是入口分級、信任切換與優先順序。當討論進入流量規則、平台配置、服務網格策略與具體 ACL 時，章節責任會切到部署平台與網路入口模組。&lt;/p>
&lt;h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節&lt;/h2>
&lt;ol>
&lt;li>已完成入口分級與擁有者盤點後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口&lt;/a>。&lt;/li>
&lt;li>已定義高風險入口驗證節奏後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>。&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>本章處理紅隊（攻擊者視角的風險檢查）分析的第一步：建立完整攻擊面清單，並標註每條路徑跨越的信任邊界。目標是讓團隊在選型與設計早期就看見高風險入口，先完成優先順序，讓事故期間可直接使用既有盤點。</p>
<h2 id="情境哪些服務需要先做攻擊面盤點">【情境】哪些服務需要先做攻擊面盤點</h2>
<p>下列情境同時出現時，攻擊面與邊界章節應放在前面：</p>
<ul>
<li>對外入口超過一種（API、webhook、管理介面、診斷介面）</li>
<li>角色與租戶模型持續擴張</li>
<li>系統同時依賴多個內外部服務</li>
<li>交付節奏快，設定變動頻繁</li>
</ul>
<h2 id="判讀流程四步完成邊界標註">【判讀流程】四步完成邊界標註</h2>
<ol>
<li>列入口：整理 <a href="/blog/backend/knowledge-cards/public-api-endpoint/" data-link-title="Public API Endpoint" data-link-desc="說明面向外部 client 的穩定 API 入口如何被管理">Public API</a>、<a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint</a>、<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint</a>、<a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> 與 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a>。</li>
<li>畫資料流：每條入口都連到下游能力與資料資產，包含第三方整合。</li>
<li>標信任切換點：在身份、租戶、網路、資料層切換處標示 <a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">Trust Boundary</a>。</li>
<li>排優先級：以可枚舉性、可重放性、資料敏感度與橫向移動能力排序。</li>
</ol>
<h2 id="風險代價盤點品質直接影響事故規模">【風險代價】盤點品質直接影響事故規模</h2>
<p>盤點品質偏弱時，常見結果是高風險入口晚被辨識，導致事件初期難以聚焦調查。這會同時拉高停機時間、修復人力與溝通成本。盤點完整時，告警、稽核與隔離策略可以對齊同一張路徑圖，事故處理速度會明顯提升。</p>
<h2 id="設計取捨入口密度與維運成本">【設計取捨】入口密度與維運成本</h2>
<p>入口越多，產品迭代彈性越高；同時，邊界管理與稽核成本也會增加。設計上可透過入口分層、責任分離與固定命名規則控制複雜度，讓新入口進入流程時就能被標記與追蹤。</p>
<h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義</h2>
<ul>
<li>入口清單與擁有者</li>
<li>邊界切換規則與驗證責任</li>
<li>高風險入口的告警與稽核路徑</li>
<li>入口變更的審查節點與 release gate</li>
</ul>
<h2 id="判讀訊號何時代表攻擊面風險升高">【判讀訊號】何時代表攻擊面風險升高</h2>
<ul>
<li>新增入口未同步出現在入口清單</li>
<li>管理入口與公開入口混用同一條流量路徑</li>
<li>同一入口同時承載人員操作與機器操作</li>
<li>入口事件難以對應到明確擁有者與責任邊界</li>
</ul>
<h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層</h2>
<p>本章在概念層回答的是入口分級、信任切換與優先順序。當討論進入流量規則、平台配置、服務網格策略與具體 ACL 時，章節責任會切到部署平台與網路入口模組。</p>
<h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節</h2>
<ol>
<li>已完成入口分級與擁有者盤點後，交接到 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台與網路入口</a>。</li>
<li>已定義高風險入口驗證節奏後，交接到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>。</li>
</ol>
]]></content:encoded></item><item><title>7.R2 入口濫用與權限突破</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/abuse-paths/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/abuse-paths/</guid><description>&lt;p>本章處理紅隊（攻擊者視角的風險檢查）分析的第二步：把合法流程轉成濫用路徑，評估哪條業務流程最容易變成越權操作。目標是讓權限設計與業務流程一起驗證，避免只檢查單點 API。&lt;/p>
&lt;h2 id="情境哪些流程需要優先做濫用分析">【情境】哪些流程需要優先做濫用分析&lt;/h2>
&lt;p>下列流程通常優先納入濫用分析：&lt;/p>
&lt;ul>
&lt;li>邀請、審核、代理操作、帳號切換&lt;/li>
&lt;li>密碼重設、權限提升、方案升降級&lt;/li>
&lt;li>匯出、分享、批次操作&lt;/li>
&lt;li>跨租戶協作與第三方授權&lt;/li>
&lt;/ul>
&lt;h2 id="判讀流程三層檢查法">【判讀流程】三層檢查法&lt;/h2>
&lt;ol>
&lt;li>目的層：確認每個流程的正常商業目的與預期受益者。&lt;/li>
&lt;li>權限層：沿流程標示 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">Tenant Boundary&lt;/a> 切換點。&lt;/li>
&lt;li>濫用層：列出 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/function-level-authorization/" data-link-title="Function-Level Authorization" data-link-desc="說明功能操作本身也需要授權，不只資源 ID 需要授權">Function-Level Authorization&lt;/a> 可觸發的非預期結果。&lt;/li>
&lt;/ol>
&lt;h2 id="風險代價流程越長放大效果越強">【風險代價】流程越長，放大效果越強&lt;/h2>
&lt;p>濫用路徑成功時，代價通常高於一般漏洞：它看起來像合法操作，偵測延遲較長，資料外流量可能更高，回溯也更困難。流程級分析越完整，越能縮小事故調查範圍並減少誤封。&lt;/p>
&lt;h2 id="設計取捨操作便利性與授權嚴謹度">【設計取捨】操作便利性與授權嚴謹度&lt;/h2>
&lt;p>流程越順，使用者體驗越好；同時，濫用成本也可能下降。常見做法是把高風險節點加入二次驗證、操作審批或風險分層，讓低風險路徑維持流暢，高風險路徑提高檢查強度。&lt;/p>
&lt;h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義&lt;/h2>
&lt;ul>
&lt;li>主要流程的濫用情境清單與責任人&lt;/li>
&lt;li>高風險動作的授權層級與審核策略&lt;/li>
&lt;li>濫用偵測訊號與稽核欄位&lt;/li>
&lt;li>例外流程的測試與演練節點&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號何時代表流程濫用風險升高">【判讀訊號】何時代表流程濫用風險升高&lt;/h2>
&lt;ul>
&lt;li>同一帳號在短時間內完成多段高風險流程&lt;/li>
&lt;li>代理操作與權限提升在同一時序連續發生&lt;/li>
&lt;li>流程中存在可跳步或可重放的關鍵節點&lt;/li>
&lt;li>例外流程的授權審核節奏與主流程脫鉤&lt;/li>
&lt;/ul>
&lt;h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層&lt;/h2>
&lt;p>本章在概念層回答的是流程目的、授權切換點與濫用路徑。當討論進入具體 policy code、middleware 判斷與 API 權限實作時，章節責任會切到服務實體章節。&lt;/p>
&lt;h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節&lt;/h2>
&lt;ol>
&lt;li>已完成濫用情境清單後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">資安與資料保護模組 7.2&lt;/a>。&lt;/li>
&lt;li>已定義高風險流程的事件節奏後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">事故報告轉 workflow&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="延伸閱讀流程原子卡片">【延伸閱讀】流程原子卡片&lt;/h2>
&lt;p>流程原子卡片的責任是把高風險流程拆成單一問題節點。每張卡片都用同一格式說明為什麼會失效、常見失效樣式、判讀訊號與案例觸發條件。&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/" data-link-title="7.R11 流程濫用問題卡片" data-link-desc="以原子化卡片細化整體 red-team 知識網，承接金字塔結構往下生長的問題討論">7.R11 流程濫用問題卡片&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章處理紅隊（攻擊者視角的風險檢查）分析的第二步：把合法流程轉成濫用路徑，評估哪條業務流程最容易變成越權操作。目標是讓權限設計與業務流程一起驗證，避免只檢查單點 API。</p>
<h2 id="情境哪些流程需要優先做濫用分析">【情境】哪些流程需要優先做濫用分析</h2>
<p>下列流程通常優先納入濫用分析：</p>
<ul>
<li>邀請、審核、代理操作、帳號切換</li>
<li>密碼重設、權限提升、方案升降級</li>
<li>匯出、分享、批次操作</li>
<li>跨租戶協作與第三方授權</li>
</ul>
<h2 id="判讀流程三層檢查法">【判讀流程】三層檢查法</h2>
<ol>
<li>目的層：確認每個流程的正常商業目的與預期受益者。</li>
<li>權限層：沿流程標示 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a>、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a> 與 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">Tenant Boundary</a> 切換點。</li>
<li>濫用層：列出 <a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR</a> 與 <a href="/blog/backend/knowledge-cards/function-level-authorization/" data-link-title="Function-Level Authorization" data-link-desc="說明功能操作本身也需要授權，不只資源 ID 需要授權">Function-Level Authorization</a> 可觸發的非預期結果。</li>
</ol>
<h2 id="風險代價流程越長放大效果越強">【風險代價】流程越長，放大效果越強</h2>
<p>濫用路徑成功時，代價通常高於一般漏洞：它看起來像合法操作，偵測延遲較長，資料外流量可能更高，回溯也更困難。流程級分析越完整，越能縮小事故調查範圍並減少誤封。</p>
<h2 id="設計取捨操作便利性與授權嚴謹度">【設計取捨】操作便利性與授權嚴謹度</h2>
<p>流程越順，使用者體驗越好；同時，濫用成本也可能下降。常見做法是把高風險節點加入二次驗證、操作審批或風險分層，讓低風險路徑維持流暢，高風險路徑提高檢查強度。</p>
<h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義</h2>
<ul>
<li>主要流程的濫用情境清單與責任人</li>
<li>高風險動作的授權層級與審核策略</li>
<li>濫用偵測訊號與稽核欄位</li>
<li>例外流程的測試與演練節點</li>
</ul>
<h2 id="判讀訊號何時代表流程濫用風險升高">【判讀訊號】何時代表流程濫用風險升高</h2>
<ul>
<li>同一帳號在短時間內完成多段高風險流程</li>
<li>代理操作與權限提升在同一時序連續發生</li>
<li>流程中存在可跳步或可重放的關鍵節點</li>
<li>例外流程的授權審核節奏與主流程脫鉤</li>
</ul>
<h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層</h2>
<p>本章在概念層回答的是流程目的、授權切換點與濫用路徑。當討論進入具體 policy code、middleware 判斷與 API 權限實作時，章節責任會切到服務實體章節。</p>
<h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節</h2>
<ol>
<li>已完成濫用情境清單後，交接到 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">資安與資料保護模組 7.2</a>。</li>
<li>已定義高風險流程的事件節奏後，交接到 <a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">事故報告轉 workflow</a>。</li>
</ol>
<h2 id="延伸閱讀流程原子卡片">【延伸閱讀】流程原子卡片</h2>
<p>流程原子卡片的責任是把高風險流程拆成單一問題節點。每張卡片都用同一格式說明為什麼會失效、常見失效樣式、判讀訊號與案例觸發條件。</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/" data-link-title="7.R11 流程濫用問題卡片" data-link-desc="以原子化卡片細化整體 red-team 知識網，承接金字塔結構往下生長的問題討論">7.R11 流程濫用問題卡片</a></li>
</ul>
]]></content:encoded></item><item><title>7.R3 資料暴露與外洩路徑</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/exposure-and-exfiltration/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/exposure-and-exfiltration/</guid><description>&lt;p>本章處理紅隊（攻擊者視角的風險檢查）分析的第三步：盤點資料流經的每個節點，找出資料暴露與外洩風險。目標是把資料保護從單一 API 回應擴展到完整資料生命週期。&lt;/p>
&lt;h2 id="情境資料路徑擴張時先做外洩盤點">【情境】資料路徑擴張時先做外洩盤點&lt;/h2>
&lt;p>下列情況出現時，資料外洩盤點優先級應提升：&lt;/p>
&lt;ul>
&lt;li>同一資料同時進入 API、log、search index、support tool&lt;/li>
&lt;li>匯出與備份流程增加，資料保留時間拉長&lt;/li>
&lt;li>客服、營運與分析角色存取範圍擴張&lt;/li>
&lt;li>多團隊共享資料平台與查詢工具&lt;/li>
&lt;/ul>
&lt;h2 id="判讀流程資料外洩路徑圖">【判讀流程】資料外洩路徑圖&lt;/h2>
&lt;ol>
&lt;li>分級：先定義 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">Data Classification&lt;/a> 與保留策略。&lt;/li>
&lt;li>追蹤：把每類資料的流向畫到 response、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、search、export、backup。&lt;/li>
&lt;li>驗證：逐段檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">Data Masking&lt;/a> 與授權條件是否一致。&lt;/li>
&lt;li>稽核：把高風險存取與匯出操作接到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="風險代價外洩事件的處理週期長">【風險代價】外洩事件的處理週期長&lt;/h2>
&lt;p>資料外洩的影響包含法規處理、客訴、信任損失與長期稽核負擔。資料流盤點越晚，復原成本越高。早期完成資料路徑圖，可明確界定責任邊界與回復步驟，縮短事故處理時間。&lt;/p>
&lt;h2 id="設計取捨資料可用性與最小暴露">【設計取捨】資料可用性與最小暴露&lt;/h2>
&lt;p>營運分析需要資料可見性，資安需要最小暴露面。常見做法是把查詢便利性與敏感欄位脫鉤，透過欄位分級、遮罩層與分權存取平衡兩者需求。&lt;/p>
&lt;h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義&lt;/h2>
&lt;ul>
&lt;li>敏感欄位分類與保存年限&lt;/li>
&lt;li>回應、觀測、匯出的最小欄位策略&lt;/li>
&lt;li>高風險查詢與匯出的稽核欄位&lt;/li>
&lt;li>外洩事件的通報與收斂流程&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號何時代表外洩風險升高">【判讀訊號】何時代表外洩風險升高&lt;/h2>
&lt;ul>
&lt;li>同一資料欄位在多個系統面向重複出現&lt;/li>
&lt;li>匯出行為與一般查詢行為的時序突然改變&lt;/li>
&lt;li>備份與正式環境使用相同憑證域&lt;/li>
&lt;li>高風險資料存取缺少可回查稽核紀錄&lt;/li>
&lt;/ul>
&lt;h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層&lt;/h2>
&lt;p>本章在概念層回答的是資料分級、路徑盤點與責任邊界。當討論進入欄位規則實作、查詢策略與儲存配置時，章節責任會切到資料與平台實體章節。&lt;/p>
&lt;h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節&lt;/h2>
&lt;ol>
&lt;li>已完成資料路徑圖與分級後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a>。&lt;/li>
&lt;li>已定義外洩事件收斂節奏後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow&lt;/a>。&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>本章處理紅隊（攻擊者視角的風險檢查）分析的第三步：盤點資料流經的每個節點，找出資料暴露與外洩風險。目標是把資料保護從單一 API 回應擴展到完整資料生命週期。</p>
<h2 id="情境資料路徑擴張時先做外洩盤點">【情境】資料路徑擴張時先做外洩盤點</h2>
<p>下列情況出現時，資料外洩盤點優先級應提升：</p>
<ul>
<li>同一資料同時進入 API、log、search index、support tool</li>
<li>匯出與備份流程增加，資料保留時間拉長</li>
<li>客服、營運與分析角色存取範圍擴張</li>
<li>多團隊共享資料平台與查詢工具</li>
</ul>
<h2 id="判讀流程資料外洩路徑圖">【判讀流程】資料外洩路徑圖</h2>
<ol>
<li>分級：先定義 <a href="/blog/backend/knowledge-cards/data-classification/" data-link-title="Data Classification" data-link-desc="說明資料分級如何決定保護、存取、保留與匯出規則">Data Classification</a> 與保留策略。</li>
<li>追蹤：把每類資料的流向畫到 response、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、search、export、backup。</li>
<li>驗證：逐段檢查 <a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">Data Masking</a> 與授權條件是否一致。</li>
<li>稽核：把高風險存取與匯出操作接到 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a>。</li>
</ol>
<h2 id="風險代價外洩事件的處理週期長">【風險代價】外洩事件的處理週期長</h2>
<p>資料外洩的影響包含法規處理、客訴、信任損失與長期稽核負擔。資料流盤點越晚，復原成本越高。早期完成資料路徑圖，可明確界定責任邊界與回復步驟，縮短事故處理時間。</p>
<h2 id="設計取捨資料可用性與最小暴露">【設計取捨】資料可用性與最小暴露</h2>
<p>營運分析需要資料可見性，資安需要最小暴露面。常見做法是把查詢便利性與敏感欄位脫鉤，透過欄位分級、遮罩層與分權存取平衡兩者需求。</p>
<h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義</h2>
<ul>
<li>敏感欄位分類與保存年限</li>
<li>回應、觀測、匯出的最小欄位策略</li>
<li>高風險查詢與匯出的稽核欄位</li>
<li>外洩事件的通報與收斂流程</li>
</ul>
<h2 id="判讀訊號何時代表外洩風險升高">【判讀訊號】何時代表外洩風險升高</h2>
<ul>
<li>同一資料欄位在多個系統面向重複出現</li>
<li>匯出行為與一般查詢行為的時序突然改變</li>
<li>備份與正式環境使用相同憑證域</li>
<li>高風險資料存取缺少可回查稽核紀錄</li>
</ul>
<h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層</h2>
<p>本章在概念層回答的是資料分級、路徑盤點與責任邊界。當討論進入欄位規則實作、查詢策略與儲存配置時，章節責任會切到資料與平台實體章節。</p>
<h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節</h2>
<ol>
<li>已完成資料路徑圖與分級後，交接到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a>。</li>
<li>已定義外洩事件收斂節奏後，交接到 <a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow</a>。</li>
</ol>
]]></content:encoded></item><item><title>7.R4 資源濫用與可用性破壞</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/resource-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/resource-abuse/</guid><description>&lt;p>本章處理紅隊（攻擊者視角的風險檢查）分析的第四步：辨識可被放大的合法操作，預先規劃容量保護與降載策略。目標是把可用性風險納入安全討論，讓服務在壓力情境下仍可維持核心功能。&lt;/p>
&lt;h2 id="情境哪些功能容易形成資源濫用">【情境】哪些功能容易形成資源濫用&lt;/h2>
&lt;p>下列功能是資源濫用的高機率入口：&lt;/p>
&lt;ul>
&lt;li>全量匯出、批次查詢、深層搜尋&lt;/li>
&lt;li>多下游 fan-out 與鏈式呼叫&lt;/li>
&lt;li>可高頻提交的建立/更新流程&lt;/li>
&lt;li>自動重試與回補流程&lt;/li>
&lt;/ul>
&lt;h2 id="判讀流程容量壓力的紅隊檢查順序">【判讀流程】容量壓力的紅隊檢查順序&lt;/h2>
&lt;ol>
&lt;li>找昂貴操作：列出 CPU、IO、網路成本最高的路徑。&lt;/li>
&lt;li>算放大倍率：評估單請求可觸發的下游數量與資料量。&lt;/li>
&lt;li>看保護面：檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">load shedding&lt;/a>。&lt;/li>
&lt;li>排收斂路徑：定義壓力升高時的降級順序與回復條件。&lt;/li>
&lt;/ol>
&lt;h2 id="風險代價可用性事件常伴隨連鎖效應">【風險代價】可用性事件常伴隨連鎖效應&lt;/h2>
&lt;p>資源濫用事件通常從局部延遲開始，接著擴散到 queue、database 與外部依賴，最終形成連鎖降速。若事前已定義容量保護與降級順序，服務可在壓力下保留核心路徑，降低全面停擺機率。&lt;/p>
&lt;h2 id="設計取捨吞吐最大化與風險收斂">【設計取捨】吞吐最大化與風險收斂&lt;/h2>
&lt;p>放寬配額可提升短期吞吐；同時也會提高濫用放大空間。常見策略是把高成本操作分層治理：一般流量保持體驗，高成本流量採獨立配額、佇列與回應節奏。&lt;/p>
&lt;h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義&lt;/h2>
&lt;ul>
&lt;li>高成本操作清單與可接受上限&lt;/li>
&lt;li>限速、排隊、拒絕與降級的啟用條件&lt;/li>
&lt;li>連鎖失效的隔離策略與告警門檻&lt;/li>
&lt;li>壓力情境演練與回復判準&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號何時代表資源濫用風險升高">【判讀訊號】何時代表資源濫用風險升高&lt;/h2>
&lt;ul>
&lt;li>高成本操作在短時間內出現異常峰值&lt;/li>
&lt;li>單請求觸發的下游 fan-out 數量持續上升&lt;/li>
&lt;li>重試與回補流量壓過正常業務流量&lt;/li>
&lt;li>降級啟用後仍缺少明確回復判準&lt;/li>
&lt;/ul>
&lt;h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層&lt;/h2>
&lt;p>本章在概念層回答的是放大路徑、收斂順序與回復節奏。當討論進入配額數值、佇列策略與特定平台擴縮容配置時，章節責任會切到可靠性與部署模組。&lt;/p>
&lt;h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節&lt;/h2>
&lt;ol>
&lt;li>已完成高成本路徑盤點後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程&lt;/a>。&lt;/li>
&lt;li>已定義降級與回復事件節奏後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血、降級與回復策略&lt;/a>。&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>本章處理紅隊（攻擊者視角的風險檢查）分析的第四步：辨識可被放大的合法操作，預先規劃容量保護與降載策略。目標是把可用性風險納入安全討論，讓服務在壓力情境下仍可維持核心功能。</p>
<h2 id="情境哪些功能容易形成資源濫用">【情境】哪些功能容易形成資源濫用</h2>
<p>下列功能是資源濫用的高機率入口：</p>
<ul>
<li>全量匯出、批次查詢、深層搜尋</li>
<li>多下游 fan-out 與鏈式呼叫</li>
<li>可高頻提交的建立/更新流程</li>
<li>自動重試與回補流程</li>
</ul>
<h2 id="判讀流程容量壓力的紅隊檢查順序">【判讀流程】容量壓力的紅隊檢查順序</h2>
<ol>
<li>找昂貴操作：列出 CPU、IO、網路成本最高的路徑。</li>
<li>算放大倍率：評估單請求可觸發的下游數量與資料量。</li>
<li>看保護面：檢查 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a>、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與 <a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">load shedding</a>。</li>
<li>排收斂路徑：定義壓力升高時的降級順序與回復條件。</li>
</ol>
<h2 id="風險代價可用性事件常伴隨連鎖效應">【風險代價】可用性事件常伴隨連鎖效應</h2>
<p>資源濫用事件通常從局部延遲開始，接著擴散到 queue、database 與外部依賴，最終形成連鎖降速。若事前已定義容量保護與降級順序，服務可在壓力下保留核心路徑，降低全面停擺機率。</p>
<h2 id="設計取捨吞吐最大化與風險收斂">【設計取捨】吞吐最大化與風險收斂</h2>
<p>放寬配額可提升短期吞吐；同時也會提高濫用放大空間。常見策略是把高成本操作分層治理：一般流量保持體驗，高成本流量採獨立配額、佇列與回應節奏。</p>
<h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義</h2>
<ul>
<li>高成本操作清單與可接受上限</li>
<li>限速、排隊、拒絕與降級的啟用條件</li>
<li>連鎖失效的隔離策略與告警門檻</li>
<li>壓力情境演練與回復判準</li>
</ul>
<h2 id="判讀訊號何時代表資源濫用風險升高">【判讀訊號】何時代表資源濫用風險升高</h2>
<ul>
<li>高成本操作在短時間內出現異常峰值</li>
<li>單請求觸發的下游 fan-out 數量持續上升</li>
<li>重試與回補流量壓過正常業務流量</li>
<li>降級啟用後仍缺少明確回復判準</li>
</ul>
<h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層</h2>
<p>本章在概念層回答的是放大路徑、收斂順序與回復節奏。當討論進入配額數值、佇列策略與特定平台擴縮容配置時，章節責任會切到可靠性與部署模組。</p>
<h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節</h2>
<ol>
<li>已完成高成本路徑盤點後，交接到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a>。</li>
<li>已定義降級與回復事件節奏後，交接到 <a href="/blog/backend/08-incident-response/containment-recovery-strategy/" data-link-title="8.3 止血、降級與回復策略" data-link-desc="把短期止血與正式回復拆成可執行步驟">8.3 止血、降級與回復策略</a>。</li>
</ol>
]]></content:encoded></item><item><title>7.R5 設定錯誤與隱藏入口</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/misconfiguration-and-hidden-entrypoints/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/misconfiguration-and-hidden-entrypoints/</guid><description>&lt;p>本章處理紅隊（攻擊者視角的風險檢查）分析的第五步：把環境設定、預設值與部署差異納入攻擊面盤點。目標是把設定風險前移到設計與交付流程，減少隱藏入口在生產環境暴露。&lt;/p>
&lt;h2 id="情境哪些變動最容易引入隱藏入口">【情境】哪些變動最容易引入隱藏入口&lt;/h2>
&lt;p>下列變動通常伴隨設定風險上升：&lt;/p>
&lt;ul>
&lt;li>新增環境、區域或新部署平台&lt;/li>
&lt;li>調整 CORS、網路白名單與憑證設定&lt;/li>
&lt;li>引入 debug endpoint 與 feature flag&lt;/li>
&lt;li>擴大第三方整合與雲端資源權限&lt;/li>
&lt;/ul>
&lt;h2 id="判讀流程設定面檢查順序">【判讀流程】設定面檢查順序&lt;/h2>
&lt;ol>
&lt;li>比對環境：比對 dev/staging/prod 的設定差異與預設值。&lt;/li>
&lt;li>列高風險項：檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint&lt;/a>、credential 與網路入口。&lt;/li>
&lt;li>看交付閘門：檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate&lt;/a> 是否包含高風險設定驗證。&lt;/li>
&lt;li>看漂移：持續偵測環境偏移與權限擴張。&lt;/li>
&lt;/ol>
&lt;h2 id="風險代價設定錯誤的修復成本常跨團隊">【風險代價】設定錯誤的修復成本常跨團隊&lt;/h2>
&lt;p>設定錯誤多半涉及應用、平台、網路與安全協作，修復路徑長且容易反覆。若設定驗證在交付前就完成，事故面會縮小，溝通與回復成本也會同步下降。&lt;/p>
&lt;h2 id="設計取捨交付速度與設定治理深度">【設計取捨】交付速度與設定治理深度&lt;/h2>
&lt;p>放寬設定審查可提升交付速度；同時會提高隱藏入口暴露機率。較穩定的做法是把高風險設定做成自動化檢查，讓交付速度與治理品質一起維持。&lt;/p>
&lt;h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義&lt;/h2>
&lt;ul>
&lt;li>baseline config 與環境差異規範&lt;/li>
&lt;li>高風險設定的自動化檢查清單&lt;/li>
&lt;li>權限擴張與設定漂移告警&lt;/li>
&lt;li>變更審查與回滾條件&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號何時代表設定風險升高">【判讀訊號】何時代表設定風險升高&lt;/h2>
&lt;ul>
&lt;li>環境設定差異在 release 前未被明確盤點&lt;/li>
&lt;li>debug 能力與正式流量共用入口路徑&lt;/li>
&lt;li>權限擴張事件與設定漂移同時出現&lt;/li>
&lt;li>設定異動缺少可回查的責任鏈與時序&lt;/li>
&lt;/ul>
&lt;h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層&lt;/h2>
&lt;p>本章在概念層回答的是設定分類、漂移判讀與責任切分。當討論進入具體 IaC 規則、平台設定語法與檢查腳本時，章節責任會切到部署模組與服務實體章節。&lt;/p>
&lt;h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節&lt;/h2>
&lt;ol>
&lt;li>已完成高風險設定分類後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口&lt;/a>。&lt;/li>
&lt;li>已定義設定漂移事件節奏後，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 復盤與改進追蹤&lt;/a>。&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>本章處理紅隊（攻擊者視角的風險檢查）分析的第五步：把環境設定、預設值與部署差異納入攻擊面盤點。目標是把設定風險前移到設計與交付流程，減少隱藏入口在生產環境暴露。</p>
<h2 id="情境哪些變動最容易引入隱藏入口">【情境】哪些變動最容易引入隱藏入口</h2>
<p>下列變動通常伴隨設定風險上升：</p>
<ul>
<li>新增環境、區域或新部署平台</li>
<li>調整 CORS、網路白名單與憑證設定</li>
<li>引入 debug endpoint 與 feature flag</li>
<li>擴大第三方整合與雲端資源權限</li>
</ul>
<h2 id="判讀流程設定面檢查順序">【判讀流程】設定面檢查順序</h2>
<ol>
<li>比對環境：比對 dev/staging/prod 的設定差異與預設值。</li>
<li>列高風險項：檢查 <a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint</a>、<a href="/blog/backend/knowledge-cards/admin-endpoint/" data-link-title="Admin Endpoint" data-link-desc="說明管理入口如何承擔高權限操作與稽核責任">Admin Endpoint</a>、credential 與網路入口。</li>
<li>看交付閘門：檢查 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a> 是否包含高風險設定驗證。</li>
<li>看漂移：持續偵測環境偏移與權限擴張。</li>
</ol>
<h2 id="風險代價設定錯誤的修復成本常跨團隊">【風險代價】設定錯誤的修復成本常跨團隊</h2>
<p>設定錯誤多半涉及應用、平台、網路與安全協作，修復路徑長且容易反覆。若設定驗證在交付前就完成，事故面會縮小，溝通與回復成本也會同步下降。</p>
<h2 id="設計取捨交付速度與設定治理深度">【設計取捨】交付速度與設定治理深度</h2>
<p>放寬設定審查可提升交付速度；同時會提高隱藏入口暴露機率。較穩定的做法是把高風險設定做成自動化檢查，讓交付速度與治理品質一起維持。</p>
<h2 id="最低控制面進入實作前要先定義">【最低控制面】進入實作前要先定義</h2>
<ul>
<li>baseline config 與環境差異規範</li>
<li>高風險設定的自動化檢查清單</li>
<li>權限擴張與設定漂移告警</li>
<li>變更審查與回滾條件</li>
</ul>
<h2 id="判讀訊號何時代表設定風險升高">【判讀訊號】何時代表設定風險升高</h2>
<ul>
<li>環境設定差異在 release 前未被明確盤點</li>
<li>debug 能力與正式流量共用入口路徑</li>
<li>權限擴張事件與設定漂移同時出現</li>
<li>設定異動缺少可回查的責任鏈與時序</li>
</ul>
<h2 id="風險邊界到哪裡仍是概念層">【風險邊界】到哪裡仍是概念層</h2>
<p>本章在概念層回答的是設定分類、漂移判讀與責任切分。當討論進入具體 IaC 規則、平台設定語法與檢查腳本時，章節責任會切到部署模組與服務實體章節。</p>
<h2 id="交接點何時路由到實作章節">【交接點】何時路由到實作章節</h2>
<ol>
<li>已完成高風險設定分類後，交接到 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口</a>。</li>
<li>已定義設定漂移事件節奏後，交接到 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 復盤與改進追蹤</a>。</li>
</ol>
]]></content:encoded></item><item><title>7.R6 事故故事重構：服務環節問題與注意事項</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/incident-stories-by-attack-stage/</guid><description>&lt;p>本章的責任是把案例整理成跨服務可重用的概念地圖。核心輸出是服務環節問題、判讀重點、注意事項與路由章節，讓後續章節可以直接接續到實作前最後一層。&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>入口成功後可快速擴散&lt;/td>
 &lt;td>高風險操作要有獨立事件節奏&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方與支援流程&lt;/td>
 &lt;td>外部事件會傳導到內部身分鏈&lt;/td>
 &lt;td>公告、盤點、收斂要同一節奏&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界入口與設備&lt;/td>
 &lt;td>暴露面與修補窗口同時放大風險&lt;/td>
 &lt;td>隔離、修補、驗證要成一組流程&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/" data-link-title="7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入" data-link-desc="NetScaler 邊界入口代碼注入事件揭示管理平面快速失守風險">Citrix 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付與供應鏈&lt;/td>
 &lt;td>合法交付路徑可被反向利用&lt;/td>
 &lt;td>先凍結再驗證再恢復&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料外送與回復&lt;/td>
 &lt;td>外送風險與營運衝擊同步上升&lt;/td>
 &lt;td>盤點、通報、回復排序要並行&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例對照表情境---判讀---注意事項---路由章節">案例對照表（情境 -&amp;gt; 判讀 -&amp;gt; 注意事項 -&amp;gt; 路由章節）&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>身分邊界已進入擴散節奏&lt;/td>
 &lt;td>先收斂高風險身份，再追蹤橫向路徑&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應商事件後內部憑證仍活躍&lt;/td>
 &lt;td>供應商事件已傳導到內部環節&lt;/td>
 &lt;td>盤點與輪替要一起啟動&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界修補完成後異常會話持續&lt;/td>
 &lt;td>修補節奏與信任收斂節奏脫鉤&lt;/td>
 &lt;td>會話失效與狀態驗證要同步&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付事件影響 artifact 信任&lt;/td>
 &lt;td>供應鏈風險已跨到發佈節奏&lt;/td>
 &lt;td>發佈凍結條件要先於恢復條件定義&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外送事件伴隨跨部門通報壓力&lt;/td>
 &lt;td>技術時序與業務時序需要並行&lt;/td>
 &lt;td>受影響清單與通報節奏要先對齊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="到實作前的最後一層">到實作前的最後一層&lt;/h2>
&lt;p>本章在概念層回答的是服務環節問題、案例證據與路由條件。當討論進入平台設定值、程式策略、工具指令與操作流程細節時，就代表已進入實作層，應切到 05/06/08 對應章節。&lt;/p>
&lt;h2 id="可直接延伸的索引">可直接延伸的索引&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7 事故案例庫（可引用）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&amp;gt; 案例 -&amp;gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.8 模組路由：案例到服務實作&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把案例整理成跨服務可重用的概念地圖。核心輸出是服務環節問題、判讀重點、注意事項與路由章節，讓後續章節可以直接接續到實作前最後一層。</p>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<table>
  <thead>
      <tr>
          <th>服務環節</th>
          <th>核心問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身分與授權鏈</td>
          <td>入口成功後可快速擴散</td>
          <td>高風險操作要有獨立事件節奏</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></td>
      </tr>
      <tr>
          <td>第三方與支援流程</td>
          <td>外部事件會傳導到內部身分鏈</td>
          <td>公告、盤點、收斂要同一節奏</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a></td>
      </tr>
      <tr>
          <td>邊界入口與設備</td>
          <td>暴露面與修補窗口同時放大風險</td>
          <td>隔離、修補、驗證要成一組流程</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/" data-link-title="7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入" data-link-desc="NetScaler 邊界入口代碼注入事件揭示管理平面快速失守風險">Citrix 2023</a></td>
      </tr>
      <tr>
          <td>交付與供應鏈</td>
          <td>合法交付路徑可被反向利用</td>
          <td>先凍結再驗證再恢復</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></td>
      </tr>
      <tr>
          <td>資料外送與回復</td>
          <td>外送風險與營運衝擊同步上升</td>
          <td>盤點、通報、回復排序要並行</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></td>
      </tr>
  </tbody>
</table>
<h2 id="案例對照表情境---判讀---注意事項---路由章節">案例對照表（情境 -&gt; 判讀 -&gt; 注意事項 -&gt; 路由章節）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>判讀</th>
          <th>注意事項</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身分異常事件在短時間擴大</td>
          <td>身分邊界已進入擴散節奏</td>
          <td>先收斂高風險身份，再追蹤橫向路徑</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></td>
      </tr>
      <tr>
          <td>供應商事件後內部憑證仍活躍</td>
          <td>供應商事件已傳導到內部環節</td>
          <td>盤點與輪替要一起啟動</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>邊界修補完成後異常會話持續</td>
          <td>修補節奏與信任收斂節奏脫鉤</td>
          <td>會話失效與狀態驗證要同步</td>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a></td>
      </tr>
      <tr>
          <td>交付事件影響 artifact 信任</td>
          <td>供應鏈風險已跨到發佈節奏</td>
          <td>發佈凍結條件要先於恢復條件定義</td>
          <td><a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台與入口威脅建模</a></td>
      </tr>
      <tr>
          <td>外送事件伴隨跨部門通報壓力</td>
          <td>技術時序與業務時序需要並行</td>
          <td>受影響清單與通報節奏要先對齊</td>
          <td><a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow</a></td>
      </tr>
  </tbody>
</table>
<h2 id="到實作前的最後一層">到實作前的最後一層</h2>
<p>本章在概念層回答的是服務環節問題、案例證據與路由條件。當討論進入平台設定值、程式策略、工具指令與操作流程細節時，就代表已進入實作層，應切到 05/06/08 對應章節。</p>
<h2 id="可直接延伸的索引">可直接延伸的索引</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/" data-link-title="7.R7 事故案例庫（可引用）" data-link-desc="把公開事故整理成可引用案例體系，讓服務章節與 incident workflow 可雙向回寫">7.R7 事故案例庫（可引用）</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.8 模組路由：案例到服務實作</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7 事故案例庫（可引用）</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/</guid><description>&lt;p>這個分類的責任是把事故拆成可重複引用的決策素材。每篇案例都用同一組結構回答：事故摘要 + 演示焦點、攻擊路徑、失效控制面、少一步的後果、可落地的 workflow 檢查點、從本案例到實作的 chain、可追溯來源。&lt;/p>
&lt;h2 id="分類入口依-threat-surface-分軸">分類入口（依 threat surface 分軸）&lt;/h2>
&lt;p>四個子分類各自承擔一條 threat surface 的演示，避免單一 case 嘗試涵蓋所有威脅面。讀者依當下要分析的 threat 類型選分類入口：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>演示的 threat surface&lt;/th>
 &lt;th>典型 chain anchor&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/" data-link-title="7.R7.1 Identity &amp;amp; Access 類案例" data-link-desc="整理身分流程、社交工程、支援系統與 token 鏈的事故案例">Identity &amp;amp; Access&lt;/a>&lt;/td>
 &lt;td>身分、認證流程、社交工程、第三方身分鏈&lt;/td>
 &lt;td>7.2 身分與授權邊界 + 7.5 federated trust&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/" data-link-title="7.R7.2 Supply Chain 類案例" data-link-desc="整理第三方整合、CI/CD、更新鏈、開源與 MSP 供應鏈事故案例">Supply Chain&lt;/a>&lt;/td>
 &lt;td>CI/CD、更新鏈、RMM、開源與 MSP 供應鏈&lt;/td>
 &lt;td>7.6 供應鏈完整性與 artifact 信任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/" data-link-title="7.R7.3 Edge Exposure 類案例" data-link-desc="整理邊界設備、外網入口、管理平面與鏈式漏洞事故案例">Edge Exposure&lt;/a>&lt;/td>
 &lt;td>邊界設備、外網入口、管理平面、鏈式漏洞&lt;/td>
 &lt;td>7.3 入口與伺服端保護 + 7.12 偵測涵蓋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/" data-link-title="7.R7.4 Data Exfiltration 類案例" data-link-desc="整理資料外送、備份風險、勒索回復與營運衝擊相關事故案例">Data Exfiltration&lt;/a>&lt;/td>
 &lt;td>資料外送、備份鏈、營運中斷與回復壓力&lt;/td>
 &lt;td>7.9 資料保護 + 7.10 資料 residency + recovery readiness&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跨分類引用時、以同 threat surface 的 case 互相 link 為主、跨 surface 視為 multi-vector 案例（如 MGM 同時在 identity-access 與 ops-impact）、需在演示焦點段明示。&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&amp;gt; 案例 -&amp;gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖&lt;/a>：服務主題到案例與 workflow 檢查點的對照。&lt;/li>
&lt;/ul>
&lt;h2 id="使用方式">使用方式&lt;/h2>
&lt;ol>
&lt;li>先在服務章節定義風險主題與控制面。&lt;/li>
&lt;li>選一篇同 threat surface 的 case、引用「如果 workflow 少一步會發生什麼」+「演示焦點」。&lt;/li>
&lt;li>沿 case 的「從本案例到實作的 chain」走到對應 problem-card（若有）/ 主章節點 / blue-team scenario。&lt;/li>
&lt;li>把該步驟落地到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow&lt;/a> 的 runbook 流程。&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>這個分類的責任是把事故拆成可重複引用的決策素材。每篇案例都用同一組結構回答：事故摘要 + 演示焦點、攻擊路徑、失效控制面、少一步的後果、可落地的 workflow 檢查點、從本案例到實作的 chain、可追溯來源。</p>
<h2 id="分類入口依-threat-surface-分軸">分類入口（依 threat surface 分軸）</h2>
<p>四個子分類各自承擔一條 threat surface 的演示，避免單一 case 嘗試涵蓋所有威脅面。讀者依當下要分析的 threat 類型選分類入口：</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>演示的 threat surface</th>
          <th>典型 chain anchor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/" data-link-title="7.R7.1 Identity &amp; Access 類案例" data-link-desc="整理身分流程、社交工程、支援系統與 token 鏈的事故案例">Identity &amp; Access</a></td>
          <td>身分、認證流程、社交工程、第三方身分鏈</td>
          <td>7.2 身分與授權邊界 + 7.5 federated trust</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/" data-link-title="7.R7.2 Supply Chain 類案例" data-link-desc="整理第三方整合、CI/CD、更新鏈、開源與 MSP 供應鏈事故案例">Supply Chain</a></td>
          <td>CI/CD、更新鏈、RMM、開源與 MSP 供應鏈</td>
          <td>7.6 供應鏈完整性與 artifact 信任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/" data-link-title="7.R7.3 Edge Exposure 類案例" data-link-desc="整理邊界設備、外網入口、管理平面與鏈式漏洞事故案例">Edge Exposure</a></td>
          <td>邊界設備、外網入口、管理平面、鏈式漏洞</td>
          <td>7.3 入口與伺服端保護 + 7.12 偵測涵蓋</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/" data-link-title="7.R7.4 Data Exfiltration 類案例" data-link-desc="整理資料外送、備份風險、勒索回復與營運衝擊相關事故案例">Data Exfiltration</a></td>
          <td>資料外送、備份鏈、營運中斷與回復壓力</td>
          <td>7.9 資料保護 + 7.10 資料 residency + recovery readiness</td>
      </tr>
  </tbody>
</table>
<p>跨分類引用時、以同 threat surface 的 case 互相 link 為主、跨 surface 視為 multi-vector 案例（如 MGM 同時在 identity-access 與 ops-impact）、需在演示焦點段明示。</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">案例引用地圖</a>：服務主題到案例與 workflow 檢查點的對照。</li>
</ul>
<h2 id="使用方式">使用方式</h2>
<ol>
<li>先在服務章節定義風險主題與控制面。</li>
<li>選一篇同 threat surface 的 case、引用「如果 workflow 少一步會發生什麼」+「演示焦點」。</li>
<li>沿 case 的「從本案例到實作的 chain」走到對應 problem-card（若有）/ 主章節點 / blue-team scenario。</li>
<li>把該步驟落地到 <a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a> 的 runbook 流程。</li>
</ol>
]]></content:encoded></item><item><title>7.R8 控制面失效樣式</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/control-failure-patterns/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/control-failure-patterns/</guid><description>&lt;p>本章的責任是把攻擊事件回推為控制面失效樣式，讓紅隊判讀能直接對應服務設計的缺口類型。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦失效樣式與判讀語言，不討論具體 exploit 細節與工具手法。&lt;/p>
&lt;h2 id="失效樣式模型">失效樣式模型&lt;/h2>
&lt;p>失效樣式模型的核心責任是把事件結果回推成可重用的控制語言。&lt;/p>
&lt;ol>
&lt;li>邊界失效：入口分級與隔離條件不足。&lt;/li>
&lt;li>身分失效：認證強度與授權邊界失衡。&lt;/li>
&lt;li>會話失效：憑證收斂與撤銷節奏落後。&lt;/li>
&lt;li>資料失效：最小揭露與匯出治理不足。&lt;/li>
&lt;li>證據失效：稽核欄位與責任鏈不可驗證。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「發生了什麼」轉成「哪種控制面失效」。&lt;/p>
&lt;ol>
&lt;li>先定位事件最早可見異常點。&lt;/li>
&lt;li>再定位異常點對應的控制面責任。&lt;/li>
&lt;li>接著判讀是單點失效還是多層連鎖失效。&lt;/li>
&lt;li>最後把失效樣式回寫到章節與問題卡。&lt;/li>
&lt;/ol>
&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>非預期入口可達性增加&lt;/td>
 &lt;td>入口成為批量利用起點&lt;/td>
 &lt;td>&lt;code>7.3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>身分收斂缺口&lt;/td>
 &lt;td>高權限 session 存續過久&lt;/td>
 &lt;td>擴散速度高於收斂速度&lt;/td>
 &lt;td>&lt;code>7.2&lt;/code> + &lt;code>7.6&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料最小揭露缺口&lt;/td>
 &lt;td>回應與匯出邊界失衡&lt;/td>
 &lt;td>外送影響面擴大&lt;/td>
 &lt;td>&lt;code>7.4&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證據鏈缺口&lt;/td>
 &lt;td>關鍵操作無法回查&lt;/td>
 &lt;td>事故收斂時間拉長&lt;/td>
 &lt;td>&lt;code>7.7&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見連鎖樣式">常見連鎖樣式&lt;/h2>
&lt;p>連鎖樣式的責任是提醒團隊失效通常不會停在單一控制面。&lt;/p>
&lt;ul>
&lt;li>邊界失效 + 身分失效：入口突破後快速取得高權限存取。&lt;/li>
&lt;li>身分失效 + 會話失效：初始異常被延長成持續存取事件。&lt;/li>
&lt;li>資料失效 + 證據失效：資料外送後無法快速界定影響範圍。&lt;/li>
&lt;li>證據失效 + 通報失效：事件處置與對外溝通節奏分裂。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證失效樣式是否具備跨案例重用性。&lt;/p>
&lt;ul>
&lt;li>邊界與管理面失效鏈： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024&lt;/a>&lt;/li>
&lt;li>身分與會話失效鏈： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>資料與證據失效鏈： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>流程級細化卡片：&lt;code>7.R11 problem-cards&lt;/code>&lt;/li>
&lt;li>模組主章補強：&lt;code>7.2&lt;/code>、&lt;code>7.3&lt;/code>、&lt;code>7.4&lt;/code>、&lt;code>7.7&lt;/code>&lt;/li>
&lt;li>收斂與復盤：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把攻擊事件回推為控制面失效樣式，讓紅隊判讀能直接對應服務設計的缺口類型。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦失效樣式與判讀語言，不討論具體 exploit 細節與工具手法。</p>
<h2 id="失效樣式模型">失效樣式模型</h2>
<p>失效樣式模型的核心責任是把事件結果回推成可重用的控制語言。</p>
<ol>
<li>邊界失效：入口分級與隔離條件不足。</li>
<li>身分失效：認證強度與授權邊界失衡。</li>
<li>會話失效：憑證收斂與撤銷節奏落後。</li>
<li>資料失效：最小揭露與匯出治理不足。</li>
<li>證據失效：稽核欄位與責任鏈不可驗證。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「發生了什麼」轉成「哪種控制面失效」。</p>
<ol>
<li>先定位事件最早可見異常點。</li>
<li>再定位異常點對應的控制面責任。</li>
<li>接著判讀是單點失效還是多層連鎖失效。</li>
<li>最後把失效樣式回寫到章節與問題卡。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>失效樣式</th>
          <th>判讀訊號</th>
          <th>典型後果</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邊界控制缺口</td>
          <td>非預期入口可達性增加</td>
          <td>入口成為批量利用起點</td>
          <td><code>7.3</code></td>
      </tr>
      <tr>
          <td>身分收斂缺口</td>
          <td>高權限 session 存續過久</td>
          <td>擴散速度高於收斂速度</td>
          <td><code>7.2</code> + <code>7.6</code></td>
      </tr>
      <tr>
          <td>資料最小揭露缺口</td>
          <td>回應與匯出邊界失衡</td>
          <td>外送影響面擴大</td>
          <td><code>7.4</code></td>
      </tr>
      <tr>
          <td>證據鏈缺口</td>
          <td>關鍵操作無法回查</td>
          <td>事故收斂時間拉長</td>
          <td><code>7.7</code></td>
      </tr>
  </tbody>
</table>
<h2 id="常見連鎖樣式">常見連鎖樣式</h2>
<p>連鎖樣式的責任是提醒團隊失效通常不會停在單一控制面。</p>
<ul>
<li>邊界失效 + 身分失效：入口突破後快速取得高權限存取。</li>
<li>身分失效 + 會話失效：初始異常被延長成持續存取事件。</li>
<li>資料失效 + 證據失效：資料外送後無法快速界定影響範圍。</li>
<li>證據失效 + 通報失效：事件處置與對外溝通節奏分裂。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證失效樣式是否具備跨案例重用性。</p>
<ul>
<li>邊界與管理面失效鏈： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a></li>
<li>身分與會話失效鏈： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>資料與證據失效鏈： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>流程級細化卡片：<code>7.R11 problem-cards</code></li>
<li>模組主章補強：<code>7.2</code>、<code>7.3</code>、<code>7.4</code>、<code>7.7</code></li>
<li>收斂與復盤：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.R9 攻擊者成本與行動節奏</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/adversary-cost-and-campaign-cadence/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/adversary-cost-and-campaign-cadence/</guid><description>&lt;p>本章的責任是以攻擊者成本與收益模型補強紅隊判讀，讓服務團隊能先處理最可能被優先利用的缺口。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦攻擊成本、操作複雜度、可擴散性與可隱匿性，不討論特定攻擊團體情報。&lt;/p>
&lt;h2 id="成本與節奏模型">成本與節奏模型&lt;/h2>
&lt;p>成本模型的核心責任是把攻擊路徑轉成可排序的防守優先序。&lt;/p>
&lt;ol>
&lt;li>初始成本：發現入口、取得第一個可用身分或會話的難度。&lt;/li>
&lt;li>維持成本：持續存取與避免被發現所需的代價。&lt;/li>
&lt;li>擴散成本：從單點突破擴大到多資產影響的代價。&lt;/li>
&lt;li>兌現成本：把存取能力轉成資料外送或業務中斷的代價。&lt;/li>
&lt;/ol>
&lt;p>行動節奏的核心責任是定義攻擊者如何在時間上壓縮防守反應空間。&lt;/p>
&lt;ol>
&lt;li>偵察：尋找可枚舉入口與弱邊界。&lt;/li>
&lt;li>利用：快速取得可重放能力或高權限落點。&lt;/li>
&lt;li>站穩：維持存取並降低被偵測機率。&lt;/li>
&lt;li>放大：橫向擴散、資料外送或操作中斷。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把事件訊號轉成防守資源分配。&lt;/p>
&lt;ol>
&lt;li>先判讀目前異常屬於哪一個攻擊節奏階段。&lt;/li>
&lt;li>再判讀攻擊者在該階段的成本是否偏低。&lt;/li>
&lt;li>接著優先提高對方成本最低的環節。&lt;/li>
&lt;li>最後回寫到控制面，調整下一輪優先序。&lt;/li>
&lt;/ol>
&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>可枚舉入口與重放路徑同時存在&lt;/td>
 &lt;td>攻擊者快速重複利用&lt;/td>
 &lt;td>&lt;code>7.R1&lt;/code> + &lt;code>7.3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>擴散成本過低&lt;/td>
 &lt;td>身分與授權邊界過寬&lt;/td>
 &lt;td>橫向擴散效率提升&lt;/td>
 &lt;td>&lt;code>7.2&lt;/code> + &lt;code>7.6&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>隱匿成本過低&lt;/td>
 &lt;td>稽核訊號密度不足&lt;/td>
 &lt;td>事件偵測與回查延後&lt;/td>
 &lt;td>&lt;code>7.7&lt;/code> + &lt;code>7.13&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回復成本過高&lt;/td>
 &lt;td>回退與收斂缺乏演練&lt;/td>
 &lt;td>業務中斷時間拉長&lt;/td>
 &lt;td>&lt;code>7.9&lt;/code> + &lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="防守方的成本轉移策略">防守方的成本轉移策略&lt;/h2>
&lt;p>成本轉移策略的責任是讓攻擊者在每個階段都付出更高代價。&lt;/p>
&lt;ul>
&lt;li>提高初始成本：縮減可枚舉入口、強化認證條件、降低重放機會。&lt;/li>
&lt;li>提高維持成本：縮短 token 時窗、提升異常活動可見度。&lt;/li>
&lt;li>提高擴散成本：強化最小權限與跨邊界隔離。&lt;/li>
&lt;li>提高兌現成本：收緊匯出路徑與高風險操作節奏控制。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是驗證成本模型是否貼近真實攻擊節奏。&lt;/p>
&lt;ul>
&lt;li>低成本入口到快速擴散： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>低成本會話劫持到高回報存取： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023&lt;/a>&lt;/li>
&lt;li>供應鏈低成本滲透到高影響傳導： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>身分與會話收斂：&lt;code>7.2&lt;/code>、&lt;code>7.6&lt;/code>&lt;/li>
&lt;li>偵測與證據強化：&lt;code>7.7&lt;/code>、&lt;code>7.13&lt;/code>&lt;/li>
&lt;li>事故收斂與復盤：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是以攻擊者成本與收益模型補強紅隊判讀，讓服務團隊能先處理最可能被優先利用的缺口。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦攻擊成本、操作複雜度、可擴散性與可隱匿性，不討論特定攻擊團體情報。</p>
<h2 id="成本與節奏模型">成本與節奏模型</h2>
<p>成本模型的核心責任是把攻擊路徑轉成可排序的防守優先序。</p>
<ol>
<li>初始成本：發現入口、取得第一個可用身分或會話的難度。</li>
<li>維持成本：持續存取與避免被發現所需的代價。</li>
<li>擴散成本：從單點突破擴大到多資產影響的代價。</li>
<li>兌現成本：把存取能力轉成資料外送或業務中斷的代價。</li>
</ol>
<p>行動節奏的核心責任是定義攻擊者如何在時間上壓縮防守反應空間。</p>
<ol>
<li>偵察：尋找可枚舉入口與弱邊界。</li>
<li>利用：快速取得可重放能力或高權限落點。</li>
<li>站穩：維持存取並降低被偵測機率。</li>
<li>放大：橫向擴散、資料外送或操作中斷。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把事件訊號轉成防守資源分配。</p>
<ol>
<li>先判讀目前異常屬於哪一個攻擊節奏階段。</li>
<li>再判讀攻擊者在該階段的成本是否偏低。</li>
<li>接著優先提高對方成本最低的環節。</li>
<li>最後回寫到控制面，調整下一輪優先序。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>低成本高回報入口存在</td>
          <td>可枚舉入口與重放路徑同時存在</td>
          <td>攻擊者快速重複利用</td>
          <td><code>7.R1</code> + <code>7.3</code></td>
      </tr>
      <tr>
          <td>擴散成本過低</td>
          <td>身分與授權邊界過寬</td>
          <td>橫向擴散效率提升</td>
          <td><code>7.2</code> + <code>7.6</code></td>
      </tr>
      <tr>
          <td>隱匿成本過低</td>
          <td>稽核訊號密度不足</td>
          <td>事件偵測與回查延後</td>
          <td><code>7.7</code> + <code>7.13</code></td>
      </tr>
      <tr>
          <td>回復成本過高</td>
          <td>回退與收斂缺乏演練</td>
          <td>業務中斷時間拉長</td>
          <td><code>7.9</code> + <code>08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="防守方的成本轉移策略">防守方的成本轉移策略</h2>
<p>成本轉移策略的責任是讓攻擊者在每個階段都付出更高代價。</p>
<ul>
<li>提高初始成本：縮減可枚舉入口、強化認證條件、降低重放機會。</li>
<li>提高維持成本：縮短 token 時窗、提升異常活動可見度。</li>
<li>提高擴散成本：強化最小權限與跨邊界隔離。</li>
<li>提高兌現成本：收緊匯出路徑與高風險操作節奏控制。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是驗證成本模型是否貼近真實攻擊節奏。</p>
<ul>
<li>低成本入口到快速擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>低成本會話劫持到高回報存取： <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023</a></li>
<li>供應鏈低成本滲透到高影響傳導： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身分與會話收斂：<code>7.2</code>、<code>7.6</code></li>
<li>偵測與證據強化：<code>7.7</code>、<code>7.13</code></li>
<li>事故收斂與復盤：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.R10 偵測迴避與觀測缺口</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/detection-evasion-and-observability-gaps/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/detection-evasion-and-observability-gaps/</guid><description>&lt;p>本章的責任是把偵測盲區轉成可討論的問題節點，讓觀測與告警設計能抵抗常見迴避路徑。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦訊號缺口、關聯斷點與迴避策略語意，不討論偵測規則產品設定。&lt;/p>
&lt;h2 id="偵測缺口模型">偵測缺口模型&lt;/h2>
&lt;p>偵測缺口模型的核心責任是定義攻擊者最常利用的觀測弱點。&lt;/p>
&lt;ol>
&lt;li>覆蓋缺口：高風險流程沒有可用訊號。&lt;/li>
&lt;li>關聯缺口：身份、入口、資料事件無法串成同一時序。&lt;/li>
&lt;li>品質缺口：告警噪音過高導致可行動訊號被淹沒。&lt;/li>
&lt;li>保留缺口：資料保留不足導致事後無法重建路徑。&lt;/li>
&lt;li>回寫缺口：復盤結論未進入下一輪偵測策略。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;p>判讀流程的責任是把「有告警」轉成「可收斂事件」。&lt;/p>
&lt;ol>
&lt;li>先確認關鍵節點是否有觀測資料。&lt;/li>
&lt;li>再確認不同資料源是否能用同一識別子關聯。&lt;/li>
&lt;li>接著確認告警是否能支持分級與責任分派。&lt;/li>
&lt;li>最後確認復盤後是否更新偵測覆蓋與閾值。&lt;/li>
&lt;/ol>
&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>身分、入口、匯出事件無法串聯&lt;/td>
 &lt;td>事故定位時間上升&lt;/td>
 &lt;td>&lt;code>7.13&lt;/code> + &lt;code>7.7&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偵測規則過度依賴單一訊號&lt;/td>
 &lt;td>高噪音環境中有效事件被淹沒&lt;/td>
 &lt;td>告警品質下降&lt;/td>
 &lt;td>&lt;code>7.13&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件後資料保留不足&lt;/td>
 &lt;td>無法重建攻擊時序&lt;/td>
 &lt;td>復盤品質下降&lt;/td>
 &lt;td>&lt;code>7.11&lt;/code> + &lt;code>7.7&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>迴避策略未進入設計檢查&lt;/td>
 &lt;td>攻擊者可反覆利用既有盲區&lt;/td>
 &lt;td>同類事件重演&lt;/td>
 &lt;td>&lt;code>7.R8&lt;/code> + &lt;code>7.9&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見迴避路徑">常見迴避路徑&lt;/h2>
&lt;p>迴避路徑的責任是讓團隊預先設計可見性，而非事後補洞。&lt;/p>
&lt;ul>
&lt;li>低頻分散行為：把高風險操作切碎，避開單點閾值告警。&lt;/li>
&lt;li>跨系統跳躍行為：在多系統間移動，利用關聯斷點隱匿軌跡。&lt;/li>
&lt;li>正常流程偽裝行為：使用合法功能完成惡意目的，降低異常可見度。&lt;/li>
&lt;li>時序拖延行為：拉長行動時間，避開短時窗偵測規則。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;p>案例觸發的責任是檢查偵測策略能否辨識低噪音高影響事件。&lt;/p>
&lt;ul>
&lt;li>低噪音身分擴散： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>憑證濫用與資料外送： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>供應鏈事件中的長週期隱匿： &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>偵測治理主章：&lt;code>7.13 detection-coverage-and-signal-governance&lt;/code>&lt;/li>
&lt;li>稽核證據主章：&lt;code>7.7 audit-trail-and-accountability-boundary&lt;/code>&lt;/li>
&lt;li>事故分級與復盤：&lt;code>08-incident-response&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本章的責任是把偵測盲區轉成可討論的問題節點，讓觀測與告警設計能抵抗常見迴避路徑。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦訊號缺口、關聯斷點與迴避策略語意，不討論偵測規則產品設定。</p>
<h2 id="偵測缺口模型">偵測缺口模型</h2>
<p>偵測缺口模型的核心責任是定義攻擊者最常利用的觀測弱點。</p>
<ol>
<li>覆蓋缺口：高風險流程沒有可用訊號。</li>
<li>關聯缺口：身份、入口、資料事件無法串成同一時序。</li>
<li>品質缺口：告警噪音過高導致可行動訊號被淹沒。</li>
<li>保留缺口：資料保留不足導致事後無法重建路徑。</li>
<li>回寫缺口：復盤結論未進入下一輪偵測策略。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「有告警」轉成「可收斂事件」。</p>
<ol>
<li>先確認關鍵節點是否有觀測資料。</li>
<li>再確認不同資料源是否能用同一識別子關聯。</li>
<li>接著確認告警是否能支持分級與責任分派。</li>
<li>最後確認復盤後是否更新偵測覆蓋與閾值。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>對應章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>關鍵環節缺少觀測資料</td>
          <td>身分、入口、匯出事件無法串聯</td>
          <td>事故定位時間上升</td>
          <td><code>7.13</code> + <code>7.7</code></td>
      </tr>
      <tr>
          <td>偵測規則過度依賴單一訊號</td>
          <td>高噪音環境中有效事件被淹沒</td>
          <td>告警品質下降</td>
          <td><code>7.13</code></td>
      </tr>
      <tr>
          <td>事件後資料保留不足</td>
          <td>無法重建攻擊時序</td>
          <td>復盤品質下降</td>
          <td><code>7.11</code> + <code>7.7</code></td>
      </tr>
      <tr>
          <td>迴避策略未進入設計檢查</td>
          <td>攻擊者可反覆利用既有盲區</td>
          <td>同類事件重演</td>
          <td><code>7.R8</code> + <code>7.9</code></td>
      </tr>
  </tbody>
</table>
<h2 id="常見迴避路徑">常見迴避路徑</h2>
<p>迴避路徑的責任是讓團隊預先設計可見性，而非事後補洞。</p>
<ul>
<li>低頻分散行為：把高風險操作切碎，避開單點閾值告警。</li>
<li>跨系統跳躍行為：在多系統間移動，利用關聯斷點隱匿軌跡。</li>
<li>正常流程偽裝行為：使用合法功能完成惡意目的，降低異常可見度。</li>
<li>時序拖延行為：拉長行動時間，避開短時窗偵測規則。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>案例觸發的責任是檢查偵測策略能否辨識低噪音高影響事件。</p>
<ul>
<li>低噪音身分擴散： <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li>憑證濫用與資料外送： <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li>供應鏈事件中的長週期隱匿： <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>偵測治理主章：<code>7.13 detection-coverage-and-signal-governance</code></li>
<li>稽核證據主章：<code>7.7 audit-trail-and-accountability-boundary</code></li>
<li>事故分級與復盤：<code>08-incident-response</code></li>
</ul>
]]></content:encoded></item><item><title>7.R7.1 Identity &amp; Access 類案例</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/</guid><description>&lt;p>本分類的責任是檢查身分與授權流程是否能在攻擊壓力下維持邊界。核心判讀是：登入成功只代表入口被通過，控制面仍需要持續驗證、隔離與收斂。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022：MFA 疲勞與內部工具擴散&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023：支援流程與身分供應鏈&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022：社交工程與員工帳號路徑&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023：身分流程被打穿後的營運中斷&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023：供應商事件後的身分收斂&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022：企業 token 與程式碼資產路徑&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022：釣魚入侵與程式碼倉儲風險&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本分類的責任是檢查身分與授權流程是否能在攻擊壓力下維持邊界。核心判讀是：登入成功只代表入口被通過，控制面仍需要持續驗證、隔離與收斂。</p>
<h2 id="案例列表">案例列表</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022：MFA 疲勞與內部工具擴散</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023：支援流程與身分供應鏈</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022：社交工程與員工帳號路徑</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023：身分流程被打穿後的營運中斷</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023：供應商事件後的身分收斂</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022：企業 token 與程式碼資產路徑</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022：釣魚入侵與程式碼倉儲風險</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7.2 Supply Chain 類案例</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/</guid><description>&lt;p>本分類的責任是驗證信任鏈在外部節點失效時是否可快速收斂。核心判讀是：只要系統信任外部交付或整合，workflow 就要先設計凍結、驗證、輪替與回復路由。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020：更新鏈被濫用&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022：第三方 token 供應鏈風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023：CI secrets 輪替壓力&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024：開源供應鏈長期滲透&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/" data-link-title="7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險" data-link-desc="CI 平台入口被利用後，如何沿著建置與發佈流程擴散供應鏈風險">TeamCity 2023：CI 入口漏洞與交付鏈風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/screenconnect-cve-2024-1709-rmm-entrypoint/" data-link-title="7.R7.2.6 ScreenConnect 2024：RMM 平台入口與下游擴散" data-link-desc="遠端管理平台入口被利用後，服務商與客戶環境會同步承壓">ScreenConnect 2024：RMM 平台入口與下游擴散&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021：共用元件風險與修補鏈&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023：桌面軟體更新鏈攻擊&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya VSA 2021：MSP 供應鏈擴散路徑&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024：CVE-2024-27198/27199 入口鏈&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本分類的責任是驗證信任鏈在外部節點失效時是否可快速收斂。核心判讀是：只要系統信任外部交付或整合，workflow 就要先設計凍結、驗證、輪替與回復路由。</p>
<h2 id="案例列表">案例列表</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020：更新鏈被濫用</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022：第三方 token 供應鏈風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023：CI secrets 輪替壓力</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ Backdoor 2024：開源供應鏈長期滲透</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/" data-link-title="7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險" data-link-desc="CI 平台入口被利用後，如何沿著建置與發佈流程擴散供應鏈風險">TeamCity 2023：CI 入口漏洞與交付鏈風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/screenconnect-cve-2024-1709-rmm-entrypoint/" data-link-title="7.R7.2.6 ScreenConnect 2024：RMM 平台入口與下游擴散" data-link-desc="遠端管理平台入口被利用後，服務商與客戶環境會同步承壓">ScreenConnect 2024：RMM 平台入口與下游擴散</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021：共用元件風險與修補鏈</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023：桌面軟體更新鏈攻擊</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya VSA 2021：MSP 供應鏈擴散路徑</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024：CVE-2024-27198/27199 入口鏈</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7.3 Edge Exposure 類案例</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/</guid><description>&lt;p>本分類的責任是把外網入口與邊界設備風險轉成可執行流程。核心判讀是：入口暴露、修補時差、攻擊自動化會同時放大事件規模，流程需要先隔離再修補再驗證。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023：外網檔案服務批量外送&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024：VPN 邊界漏洞鏈&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023：會話被劫持與重放風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024：邊界設備遠端命令執行&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/papercut-cve-2023-27350-auth-bypass-rce/" data-link-title="7.R7.3.5 PaperCut 2023：認證繞過與入口執行風險" data-link-desc="管理平台入口若被認證繞過，內部列印與服務節點會暴露在遠端控制風險">PaperCut 2023：認證繞過與入口執行風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-cve-2022-26134-ognl-rce/" data-link-title="7.R7.3.6 Confluence 2022：網站入口 RCE 與知識系統風險" data-link-desc="協作平台外網入口被打穿時，內部知識與憑證線索會同步外露">Confluence 2022：網站入口 RCE 與知識系統風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/cisco-ios-xe-cve-2023-20198-webui-chain/" data-link-title="7.R7.3.7 Cisco IOS XE 2023：Web UI 管理面風險" data-link-desc="網通設備管理介面暴露時，攻擊可直接穿透邊界控制平面">Cisco IOS XE 2023：Web UI 管理面風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/" data-link-title="7.R7.3.8 Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口" data-link-desc="VPN 邊界漏洞發生時，入口隔離與修補節奏需要同時啟動">Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/sysaid-cve-2023-47246-itsm-entrypoint/" data-link-title="7.R7.3.9 SysAid 2023：ITSM 入口與維運流程風險" data-link-desc="ITSM 服務入口被利用後，維運流程會成為擴散加速器">SysAid 2023：ITSM 入口與維運流程風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/juniper-cve-2023-36844-vpn-chain/" data-link-title="7.R7.3.10 Juniper 2023：網通設備鏈式漏洞窗口" data-link-desc="鏈式漏洞出現在核心網通設備時，修補與流量穩定性需要同步決策">Juniper 2023：網通設備鏈式漏洞窗口&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/" data-link-title="7.R7.3.11 ServiceNow 2024：企業平台入口風險" data-link-desc="企業核心平台漏洞出現時，服務流程與資料流程都需要同步收斂">ServiceNow 2024：企業平台入口風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/check-point-cve-2024-24919-vpn-info-disclosure/" data-link-title="7.R7.3.12 Check Point 2024：VPN 資訊外洩與會話風險" data-link-desc="邊界設備資訊外洩漏洞可快速轉為憑證與會話濫用風險">Check Point 2024：VPN 資訊外洩與會話風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxylogon-2021-exchange-entry-chain/" data-link-title="7.R7.3.13 ProxyLogon 2021：CVE-2021-26855/27065 入口鏈式失效" data-link-desc="郵件系統入口漏洞被串接利用時，事件會迅速擴大到內部服務邊界">ProxyLogon 2021：Exchange 入口鏈式失效&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxyshell-2021-exchange-post-auth-chain/" data-link-title="7.R7.3.14 ProxyShell 2021：CVE-2021-34473/34523/31207 後續鏈式攻擊" data-link-desc="同類入口平台在後續漏洞波次中，如何建立持續修補與驗證機制">ProxyShell 2021：Exchange 後續鏈式攻擊&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortios-cve-2022-42475-vpn-zero-day/" data-link-title="7.R7.3.15 FortiOS 2022：VPN 零時差事件節奏" data-link-desc="邊界設備零時差事件需要隔離、輪替、復測的完整鏈條">FortiOS 2022：VPN 零時差事件節奏&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/" data-link-title="7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸" data-link-desc="同一波邊界事件在後續通報階段，重點轉為會話與憑證收斂">Citrix ADC 後續事件：Session 重放延伸&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023：CVE-2023-22515/22518 權限控制鏈&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/" data-link-title="7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入" data-link-desc="NetScaler 邊界入口代碼注入事件揭示管理平面快速失守風險">Citrix 2023：CVE-2023-3519 邊界代碼注入&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023：CVE-2023-46747 認證繞過&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022：CVE-2022-40684 認證繞過&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/forticlient-ems-cve-2023-48788-sqli/" data-link-title="7.R7.3.22 FortiClient EMS 2023：CVE-2023-48788 SQL 注入" data-link-desc="端點管理平台 SQL 注入事件揭示管理平面資料與權限風險">FortiClient EMS 2023：CVE-2023-48788 SQL 注入&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/manageengine-adself-cve-2021-40539-auth-bypass/" data-link-title="7.R7.3.23 ManageEngine 2021：CVE-2021-40539 認證繞過" data-link-desc="身分服務入口認證繞過會把帳號管理流程直接暴露在攻擊鏈上">ManageEngine 2021：CVE-2021-40539 認證繞過&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/" data-link-title="7.R7.3.24 USAHERDS 2021：CVE-2021-44207 硬編碼憑證" data-link-desc="硬編碼憑證事件展示供應商系統配置治理與存取控制的共同風險">USAHERDS 2021：CVE-2021-44207 硬編碼憑證&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本分類的責任是把外網入口與邊界設備風險轉成可執行流程。核心判讀是：入口暴露、修補時差、攻擊自動化會同時放大事件規模，流程需要先隔離再修補再驗證。</p>
<h2 id="案例列表">案例列表</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023：外網檔案服務批量外送</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024：VPN 邊界漏洞鏈</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023：會話被劫持與重放風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024：邊界設備遠端命令執行</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/papercut-cve-2023-27350-auth-bypass-rce/" data-link-title="7.R7.3.5 PaperCut 2023：認證繞過與入口執行風險" data-link-desc="管理平台入口若被認證繞過，內部列印與服務節點會暴露在遠端控制風險">PaperCut 2023：認證繞過與入口執行風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-cve-2022-26134-ognl-rce/" data-link-title="7.R7.3.6 Confluence 2022：網站入口 RCE 與知識系統風險" data-link-desc="協作平台外網入口被打穿時，內部知識與憑證線索會同步外露">Confluence 2022：網站入口 RCE 與知識系統風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/cisco-ios-xe-cve-2023-20198-webui-chain/" data-link-title="7.R7.3.7 Cisco IOS XE 2023：Web UI 管理面風險" data-link-desc="網通設備管理介面暴露時，攻擊可直接穿透邊界控制平面">Cisco IOS XE 2023：Web UI 管理面風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/" data-link-title="7.R7.3.8 Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口" data-link-desc="VPN 邊界漏洞發生時，入口隔離與修補節奏需要同時啟動">Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/sysaid-cve-2023-47246-itsm-entrypoint/" data-link-title="7.R7.3.9 SysAid 2023：ITSM 入口與維運流程風險" data-link-desc="ITSM 服務入口被利用後，維運流程會成為擴散加速器">SysAid 2023：ITSM 入口與維運流程風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/juniper-cve-2023-36844-vpn-chain/" data-link-title="7.R7.3.10 Juniper 2023：網通設備鏈式漏洞窗口" data-link-desc="鏈式漏洞出現在核心網通設備時，修補與流量穩定性需要同步決策">Juniper 2023：網通設備鏈式漏洞窗口</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/" data-link-title="7.R7.3.11 ServiceNow 2024：企業平台入口風險" data-link-desc="企業核心平台漏洞出現時，服務流程與資料流程都需要同步收斂">ServiceNow 2024：企業平台入口風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/check-point-cve-2024-24919-vpn-info-disclosure/" data-link-title="7.R7.3.12 Check Point 2024：VPN 資訊外洩與會話風險" data-link-desc="邊界設備資訊外洩漏洞可快速轉為憑證與會話濫用風險">Check Point 2024：VPN 資訊外洩與會話風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxylogon-2021-exchange-entry-chain/" data-link-title="7.R7.3.13 ProxyLogon 2021：CVE-2021-26855/27065 入口鏈式失效" data-link-desc="郵件系統入口漏洞被串接利用時，事件會迅速擴大到內部服務邊界">ProxyLogon 2021：Exchange 入口鏈式失效</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxyshell-2021-exchange-post-auth-chain/" data-link-title="7.R7.3.14 ProxyShell 2021：CVE-2021-34473/34523/31207 後續鏈式攻擊" data-link-desc="同類入口平台在後續漏洞波次中，如何建立持續修補與驗證機制">ProxyShell 2021：Exchange 後續鏈式攻擊</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortios-cve-2022-42475-vpn-zero-day/" data-link-title="7.R7.3.15 FortiOS 2022：VPN 零時差事件節奏" data-link-desc="邊界設備零時差事件需要隔離、輪替、復測的完整鏈條">FortiOS 2022：VPN 零時差事件節奏</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/" data-link-title="7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸" data-link-desc="同一波邊界事件在後續通報階段，重點轉為會話與憑證收斂">Citrix ADC 後續事件：Session 重放延伸</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023：CVE-2023-22515/22518 權限控制鏈</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/" data-link-title="7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入" data-link-desc="NetScaler 邊界入口代碼注入事件揭示管理平面快速失守風險">Citrix 2023：CVE-2023-3519 邊界代碼注入</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023：CVE-2023-46747 認證繞過</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022：CVE-2022-40684 認證繞過</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/forticlient-ems-cve-2023-48788-sqli/" data-link-title="7.R7.3.22 FortiClient EMS 2023：CVE-2023-48788 SQL 注入" data-link-desc="端點管理平台 SQL 注入事件揭示管理平面資料與權限風險">FortiClient EMS 2023：CVE-2023-48788 SQL 注入</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/manageengine-adself-cve-2021-40539-auth-bypass/" data-link-title="7.R7.3.23 ManageEngine 2021：CVE-2021-40539 認證繞過" data-link-desc="身分服務入口認證繞過會把帳號管理流程直接暴露在攻擊鏈上">ManageEngine 2021：CVE-2021-40539 認證繞過</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/" data-link-title="7.R7.3.24 USAHERDS 2021：CVE-2021-44207 硬編碼憑證" data-link-desc="硬編碼憑證事件展示供應商系統配置治理與存取控制的共同風險">USAHERDS 2021：CVE-2021-44207 硬編碼憑證</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7.4 Data Exfiltration 類案例</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/</guid><description>&lt;p>本分類的責任是把資料外送與營運中斷風險轉成可驗證的治理步驟。核心判讀是：資料治理、回復順序與跨團隊通報要同步設計，才能縮小外送規模與停擺時間。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022：備份路徑與鏈式入侵&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024：憑證濫用與資料竊取&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024：資料事件轉為營運中斷&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023：支援工具路徑與客戶資料風險&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023：虛擬化平台勒索回復壓力&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023：檔案服務入口與資料外送&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本分類的責任是把資料外送與營運中斷風險轉成可驗證的治理步驟。核心判讀是：資料治理、回復順序與跨團隊通報要同步設計，才能縮小外送規模與停擺時間。</p>
<h2 id="案例列表">案例列表</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022：備份路徑與鏈式入侵</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024：憑證濫用與資料竊取</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024：資料事件轉為營運中斷</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023：支援工具路徑與客戶資料風險</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023：虛擬化平台勒索回復壓力</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023：檔案服務入口與資料外送</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7.M 案例引用地圖（服務主題 -> 案例 -> workflow）</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/</guid><description>&lt;p>這份地圖的責任是提供雙向引用路由：服務設計可以從主題找到案例，incident workflow 可以從流程步驟回查案例證據。&lt;/p>
&lt;h2 id="認證與權限邊界">認證與權限邊界&lt;/h2>
&lt;p>這個主題處理身分入口、憑證信任鏈與高權限操作隔離。優先案例是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558 2023&lt;/a>。&lt;/p>
&lt;p>workflow 檢查點：高風險操作 step-up、異常身分即時隔離、跨租戶權杖異常升級。對應流程章節：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow&lt;/a>。&lt;/p>
&lt;h2 id="第三方整合與-token">第三方整合與 token&lt;/h2>
&lt;p>這個主題處理供應商事件傳導與 token 收斂速度。優先案例是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022&lt;/a>。&lt;/p>
&lt;p>workflow 檢查點：第三方事件觸發全域 token 盤點、分域撤銷與輪替、供應商事件 playbook。對應流程章節：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow&lt;/a>。&lt;/p>
&lt;h2 id="cicd-與更新供應鏈">CI/CD 與更新供應鏈&lt;/h2>
&lt;p>這個主題處理 build 與更新信任鏈在事件中的凍結與恢復節奏。優先案例是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/" data-link-title="7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險" data-link-desc="CI 平台入口被利用後，如何沿著建置與發佈流程擴散供應鏈風險">TeamCity 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021&lt;/a>。&lt;/p>
&lt;p>workflow 檢查點：部署凍結、artifact 驗證、分批輪替 secrets、版本回退與復測。對應流程章節：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow&lt;/a>。&lt;/p>
&lt;h2 id="workload-identity-與聯邦信任">Workload identity 與聯邦信任&lt;/h2>
&lt;p>這個主題處理非人類身份、跨平台 token 交換與短時憑證收斂。優先案例是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>。&lt;/p>
&lt;p>workflow 檢查點：workload 身份來源回查、federation trust 重評估、短時憑證撤銷、跨平台 token scope 收斂。對應流程章節：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這份地圖的責任是提供雙向引用路由：服務設計可以從主題找到案例，incident workflow 可以從流程步驟回查案例證據。</p>
<h2 id="認證與權限邊界">認證與權限邊界</h2>
<p>這個主題處理身分入口、憑證信任鏈與高權限操作隔離。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Storm-0558 2023</a>。</p>
<p>workflow 檢查點：高風險操作 step-up、異常身分即時隔離、跨租戶權杖異常升級。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="第三方整合與-token">第三方整合與 token</h2>
<p>這個主題處理供應商事件傳導與 token 收斂速度。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022</a>。</p>
<p>workflow 檢查點：第三方事件觸發全域 token 盤點、分域撤銷與輪替、供應商事件 playbook。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="cicd-與更新供應鏈">CI/CD 與更新供應鏈</h2>
<p>這個主題處理 build 與更新信任鏈在事件中的凍結與恢復節奏。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/" data-link-title="7.R7.2.1 SolarWinds 2020：更新鏈被濫用" data-link-desc="合法更新流程遭植入後，攻擊者如何長期潛伏與橫向擴散">SolarWinds 2020</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/" data-link-title="7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險" data-link-desc="CI 平台入口被利用後，如何沿著建置與發佈流程擴散供應鏈風險">TeamCity 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/" data-link-title="7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力" data-link-desc="工程端點入侵後，CI 平台 secrets 如何成為高風險擴散點">CircleCI 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/" data-link-title="7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊" data-link-desc="合法更新流程被植入後，桌面端供應鏈事件如何傳到企業端點">3CX 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/" data-link-title="7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈" data-link-desc="共用元件漏洞如何同步影響多服務，並迫使團隊建立依賴治理 workflow">Log4Shell 2021</a>。</p>
<p>workflow 檢查點：部署凍結、artifact 驗證、分批輪替 secrets、版本回退與復測。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="workload-identity-與聯邦信任">Workload identity 與聯邦信任</h2>
<p>這個主題處理非人類身份、跨平台 token 交換與短時憑證收斂。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a>。</p>
<p>workflow 檢查點：workload 身份來源回查、federation trust 重評估、短時憑證撤銷、跨平台 token scope 收斂。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="邊界設備與外網入口">邊界設備與外網入口</h2>
<p>這個主題處理暴露面高與修補窗口短的組合風險。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/" data-link-title="7.R7.3.8 Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口" data-link-desc="VPN 邊界漏洞發生時，入口隔離與修補節奏需要同時啟動">Fortinet SSL-VPN 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/" data-link-title="7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位" data-link-desc="SSL-VPN 漏洞在邊界設備上會放大大規模掃描與利用速度">Fortinet 2023（27997）</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/" data-link-title="7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入" data-link-desc="NetScaler 邊界入口代碼注入事件揭示管理平面快速失守風險">Citrix 2023（3519）</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxylogon-2021-exchange-entry-chain/" data-link-title="7.R7.3.13 ProxyLogon 2021：CVE-2021-26855/27065 入口鏈式失效" data-link-desc="郵件系統入口漏洞被串接利用時，事件會迅速擴大到內部服務邊界">ProxyLogon 2021</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxyshell-2021-exchange-post-auth-chain/" data-link-title="7.R7.3.14 ProxyShell 2021：CVE-2021-34473/34523/31207 後續鏈式攻擊" data-link-desc="同類入口平台在後續漏洞波次中，如何建立持續修補與驗證機制">ProxyShell 2021</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/" data-link-title="7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸" data-link-desc="同一波邊界事件在後續通報階段，重點轉為會話與憑證收斂">Citrix 後續事件</a>。</p>
<p>workflow 檢查點：漏洞公告即隔離、分區修補、修補後狀態驗證、session 或憑證全域收斂。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="資料外送與營運回復">資料外送與營運回復</h2>
<p>這個主題處理資料外送與營運停擺同步發生時的決策順序。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a>。</p>
<p>workflow 檢查點：外送封鎖、受影響清單盤點、RTO/RPO 路由、回復優先級排序與跨組織通報。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="資料駐留刪除與證據鏈">資料駐留、刪除與證據鏈</h2>
<p>這個主題處理資料位置、刪除閉環、備份長尾與可驗證證據。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a>。</p>
<p>workflow 檢查點：資料位置清單、衍生資料刪除驗證、備份保留例外、刪除證據與通報證據對齊。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="偵測治理與例外-tripwire">偵測治理與例外 tripwire</h2>
<p>這個主題處理偵測覆蓋、訊號關聯、例外期限與重新評估觸發器。優先案例是 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/" data-link-title="7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行" data-link-desc="邊界設備 RCE 事件如何迫使團隊在修補與營運可用性間快速取捨">PAN-OS 2024</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/" data-link-title="7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透" data-link-desc="開源維護鏈遭滲透後，為何會直接影響廣泛 Linux 發行流程">XZ 2024</a>。</p>
<p>workflow 檢查點：高風險訊號關聯、例外到期重審、重大事件 tripwire、復盤後偵測覆蓋率修正。對應流程章節：<a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">incident-report-to-workflow</a>。</p>
<h2 id="使用規則">使用規則</h2>
<ol>
<li>每個服務主題至少引用一篇同類型案例。</li>
<li>每次引用至少帶出一個可操作 workflow 檢查點。</li>
<li>每個 runbook 變更都回寫到對應案例與 workflow 章節，維持雙向可追溯。</li>
</ol>
]]></content:encoded></item><item><title>7.R11.1 邀請流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/</guid><description>&lt;p>邀請流程的核心風險是把身份建立權限暴露在高頻操作節點。當邀請邊界與角色邊界沒有同步收斂，流程會從協作入口轉成擴散入口。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>邀請流程通常追求低摩擦啟用。低摩擦設計若缺少角色上限與上下文驗證，攻擊者可利用合法邀請節奏建立後續操作落點。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>邀請可直接綁定高權限角色。&lt;/li>
&lt;li>邀請連結可重放或長時間有效。&lt;/li>
&lt;li>邀請發送與審核責任由同一主體完成。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一主體短時間建立大量邀請。&lt;/li>
&lt;li>新邀請帳號快速接觸高風險操作。&lt;/li>
&lt;li>邀請接受行為與正常地理/裝置分佈偏移。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-invitation-link/" data-link-title="7.R11.P1 可重放邀請連結" data-link-desc="說明邀請連結重放如何把一次性流程轉成持續可利用入口">7.R11.P1 可重放邀請連結&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>邀請流程的核心風險是把身份建立權限暴露在高頻操作節點。當邀請邊界與角色邊界沒有同步收斂，流程會從協作入口轉成擴散入口。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>邀請流程通常追求低摩擦啟用。低摩擦設計若缺少角色上限與上下文驗證，攻擊者可利用合法邀請節奏建立後續操作落點。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>邀請可直接綁定高權限角色。</li>
<li>邀請連結可重放或長時間有效。</li>
<li>邀請發送與審核責任由同一主體完成。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一主體短時間建立大量邀請。</li>
<li>新邀請帳號快速接觸高風險操作。</li>
<li>邀請接受行為與正常地理/裝置分佈偏移。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-invitation-link/" data-link-title="7.R11.P1 可重放邀請連結" data-link-desc="說明邀請連結重放如何把一次性流程轉成持續可利用入口">7.R11.P1 可重放邀請連結</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.2 審核流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/</guid><description>&lt;p>審核流程的核心風險是審核責任與操作責任失去獨立性。當審核節奏只剩形式確認，流程會把高風險操作快速放行。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>審核流程常在效率壓力下追求快速通過。快速通過若缺乏情境證據與責任分離，審核會退化成流程裝飾。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>審核人與提交人由同一群組長期重疊。&lt;/li>
&lt;li>審核依賴固定模板，缺少情境差異判讀。&lt;/li>
&lt;li>高風險與低風險請求使用同一審核節奏。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>高風險請求通過時間顯著短於預期。&lt;/li>
&lt;li>審核意見長期一致且缺少變化。&lt;/li>
&lt;li>事故後審核判斷依據回查鏈條出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 事故指揮與角色分工&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-submitter-approver-overlap/" data-link-title="7.R11.P2 提交與審核責任重疊" data-link-desc="說明提交與審核責任重疊如何讓審核退化為形式流程">7.R11.P2 提交與審核責任重疊&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>審核流程的核心風險是審核責任與操作責任失去獨立性。當審核節奏只剩形式確認，流程會把高風險操作快速放行。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>審核流程常在效率壓力下追求快速通過。快速通過若缺乏情境證據與責任分離，審核會退化成流程裝飾。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>審核人與提交人由同一群組長期重疊。</li>
<li>審核依賴固定模板，缺少情境差異判讀。</li>
<li>高風險與低風險請求使用同一審核節奏。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>高風險請求通過時間顯著短於預期。</li>
<li>審核意見長期一致且缺少變化。</li>
<li>事故後審核判斷依據回查鏈條出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
<li><a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">8.2 事故指揮與角色分工</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-submitter-approver-overlap/" data-link-title="7.R11.P2 提交與審核責任重疊" data-link-desc="說明提交與審核責任重疊如何讓審核退化為形式流程">7.R11.P2 提交與審核責任重疊</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.3 代理操作濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/</guid><description>&lt;p>代理操作的核心風險是把操作者與責任主體分離。當代理邊界與審計邊界沒有一致設計，流程會形成可擴散的高權限通道。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>代理操作常用來提升客服與營運效率。效率導向若缺少情境限制與可回查證據，代理能力會超出原始責任範圍。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>代理操作缺少明確目的與時效。&lt;/li>
&lt;li>代理能力覆蓋一般使用者日常流程之外的功能。&lt;/li>
&lt;li>代理會話與原始使用者會話可混用。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>代理操作集中在非客服時段。&lt;/li>
&lt;li>代理主體在短時間跨多租戶操作。&lt;/li>
&lt;li>代理流程中高風險動作比例上升。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-delegated-session-context-bleed/" data-link-title="7.R11.P3 代理會話上下文混層" data-link-desc="說明代理會話與原始會話混層如何放大高權限濫用風險">7.R11.P3 代理會話上下文混層&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>代理操作的核心風險是把操作者與責任主體分離。當代理邊界與審計邊界沒有一致設計，流程會形成可擴散的高權限通道。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>代理操作常用來提升客服與營運效率。效率導向若缺少情境限制與可回查證據，代理能力會超出原始責任範圍。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>代理操作缺少明確目的與時效。</li>
<li>代理能力覆蓋一般使用者日常流程之外的功能。</li>
<li>代理會話與原始使用者會話可混用。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>代理操作集中在非客服時段。</li>
<li>代理主體在短時間跨多租戶操作。</li>
<li>代理流程中高風險動作比例上升。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-delegated-session-context-bleed/" data-link-title="7.R11.P3 代理會話上下文混層" data-link-desc="說明代理會話與原始會話混層如何放大高權限濫用風險">7.R11.P3 代理會話上下文混層</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.4 帳號切換濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/</guid><description>&lt;p>帳號切換的核心風險是把多個身份上下文放在同一操作節奏。當上下文切換與權限切換沒有同步，流程會形成隱性越權。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>帳號切換通常是為了營運效率與多角色工作。多角色共存若缺少清楚上下文提示與會話隔離，誤用與濫用都會升高。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>切換後沿用前一身份的高權限 token。&lt;/li>
&lt;li>切換狀態缺乏明確可見標記。&lt;/li>
&lt;li>切換流程缺少高風險動作二次確認。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一裝置在短時間跨多身份切換。&lt;/li>
&lt;li>切換後立刻執行高風險批次動作。&lt;/li>
&lt;li>會話事件在身份上下文對齊上出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-stale-privileged-token-after-account-switch/" data-link-title="7.R11.P4 帳號切換後沿用高權限 token" data-link-desc="說明帳號切換後權限 token 殘留如何造成身份邊界漂移">7.R11.P4 帳號切換後沿用高權限 token&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>帳號切換的核心風險是把多個身份上下文放在同一操作節奏。當上下文切換與權限切換沒有同步，流程會形成隱性越權。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>帳號切換通常是為了營運效率與多角色工作。多角色共存若缺少清楚上下文提示與會話隔離，誤用與濫用都會升高。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>切換後沿用前一身份的高權限 token。</li>
<li>切換狀態缺乏明確可見標記。</li>
<li>切換流程缺少高風險動作二次確認。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一裝置在短時間跨多身份切換。</li>
<li>切換後立刻執行高風險批次動作。</li>
<li>會話事件在身份上下文對齊上出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-stale-privileged-token-after-account-switch/" data-link-title="7.R11.P4 帳號切換後沿用高權限 token" data-link-desc="說明帳號切換後權限 token 殘留如何造成身份邊界漂移">7.R11.P4 帳號切換後沿用高權限 token</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.5 密碼重設流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/password-reset-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/password-reset-flow-abuse/</guid><description>&lt;p>密碼重設流程的核心風險是把身份恢復能力放在可外部觸發的入口。當恢復驗證弱於登入驗證，流程會成為身份接管捷徑。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>密碼重設流程追求可恢復性。可恢復性若缺少風險分層與異常節奏判讀，攻擊者可利用重設管道繞過原本身份邊界。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>重設憑證有效期過長且可重放。&lt;/li>
&lt;li>重設後舊會話仍維持可用。&lt;/li>
&lt;li>重設流程缺少異常來源檢查。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一帳號短時間觸發多次重設。&lt;/li>
&lt;li>重設完成後出現異常地理登入。&lt;/li>
&lt;li>重設事件與高風險操作連續發生。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-reset-token-with-long-ttl/" data-link-title="7.R11.P5 重設憑證可重放且有效期過長" data-link-desc="說明密碼重設憑證可重放與長時效如何形成身份接管窗口">7.R11.P5 重設憑證可重放且有效期過長&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>密碼重設流程的核心風險是把身份恢復能力放在可外部觸發的入口。當恢復驗證弱於登入驗證，流程會成為身份接管捷徑。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>密碼重設流程追求可恢復性。可恢復性若缺少風險分層與異常節奏判讀，攻擊者可利用重設管道繞過原本身份邊界。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>重設憑證有效期過長且可重放。</li>
<li>重設後舊會話仍維持可用。</li>
<li>重設流程缺少異常來源檢查。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一帳號短時間觸發多次重設。</li>
<li>重設完成後出現異常地理登入。</li>
<li>重設事件與高風險操作連續發生。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/08-incident-response/incident-report-to-workflow/" data-link-title="8.8 事故報告轉 workflow：從案例到日常流程" data-link-desc="把事故報告拆成可執行流程，並與 red-team 案例庫建立雙向引用">8.8 事故報告轉 workflow</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-reset-token-with-long-ttl/" data-link-title="7.R11.P5 重設憑證可重放且有效期過長" data-link-desc="說明密碼重設憑證可重放與長時效如何形成身份接管窗口">7.R11.P5 重設憑證可重放且有效期過長</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.6 權限提升流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/</guid><description>&lt;p>權限提升流程的核心風險是把高影響能力集中在少數切換節點。當提升條件與審核證據不完整，流程會把局部權限擴張成全域權限。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>權限提升流程通常是處理例外需求。例外節奏若缺乏明確期限與回收條件，提升能力會長期停留並被重複利用。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>權限提升缺乏時效與目的綁定。&lt;/li>
&lt;li>提升後回收流程依賴人工記憶。&lt;/li>
&lt;li>權限提升事件缺少跨系統同步。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>提升後高權限存續時間拉長。&lt;/li>
&lt;li>同一主體反覆觸發提升與批次操作。&lt;/li>
&lt;li>提升事件與審核事件的時序對齊存在缺口。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023（22515/22518）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022（40684）&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 資安治理例外與 Tripwire&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-privilege-elevation-without-time-bound/" data-link-title="7.R11.P6 權限提升缺乏時效綁定" data-link-desc="說明權限提升缺乏時效綁定如何把例外能力轉成常態能力">7.R11.P6 權限提升缺乏時效綁定&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>權限提升流程的核心風險是把高影響能力集中在少數切換節點。當提升條件與審核證據不完整，流程會把局部權限擴張成全域權限。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>權限提升流程通常是處理例外需求。例外節奏若缺乏明確期限與回收條件，提升能力會長期停留並被重複利用。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>權限提升缺乏時效與目的綁定。</li>
<li>提升後回收流程依賴人工記憶。</li>
<li>權限提升事件缺少跨系統同步。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>提升後高權限存續時間拉長。</li>
<li>同一主體反覆觸發提升與批次操作。</li>
<li>提升事件與審核事件的時序對齊存在缺口。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023（22515/22518）</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022（40684）</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-governance-exception-and-tripwire/" data-link-title="7.14 資安治理例外與 Tripwire" data-link-desc="定義例外管理、風險接受與重新評估觸發器">7.14 資安治理例外與 Tripwire</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-privilege-elevation-without-time-bound/" data-link-title="7.R11.P6 權限提升缺乏時效綁定" data-link-desc="說明權限提升缺乏時效綁定如何把例外能力轉成常態能力">7.R11.P6 權限提升缺乏時效綁定</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.7 方案升降級流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/plan-change-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/plan-change-flow-abuse/</guid><description>&lt;p>方案升降級流程的核心風險是把商業權限與技術權限綁在同一切換節點。當計費狀態與能力狀態不同步，流程會形成可利用的邊界差。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>方案切換通常優先滿足商業即時性。即時切換若缺少狀態一致性與回滾語意，攻擊者可利用時序差取得超額能力。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>升級立即生效，降級延遲回收能力。&lt;/li>
&lt;li>計費失敗仍保留高階功能。&lt;/li>
&lt;li>方案變更缺少稽核與通知鏈。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>升降級事件與高耗資源操作重疊。&lt;/li>
&lt;li>方案狀態與授權狀態出現偏移。&lt;/li>
&lt;li>邊界功能在降級後仍可存取。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya 2021&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-entitlement-revocation-lag-after-plan-downgrade/" data-link-title="7.R11.P7 降級後能力回收延遲" data-link-desc="說明方案降級後能力回收延遲如何造成授權邊界漂移">7.R11.P7 降級後能力回收延遲&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>方案升降級流程的核心風險是把商業權限與技術權限綁在同一切換節點。當計費狀態與能力狀態不同步，流程會形成可利用的邊界差。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>方案切換通常優先滿足商業即時性。即時切換若缺少狀態一致性與回滾語意，攻擊者可利用時序差取得超額能力。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>升級立即生效，降級延遲回收能力。</li>
<li>計費失敗仍保留高階功能。</li>
<li>方案變更缺少稽核與通知鏈。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>升降級事件與高耗資源操作重疊。</li>
<li>方案狀態與授權狀態出現偏移。</li>
<li>邊界功能在降級後仍可存取。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya 2021</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a></li>
<li><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.12 供應鏈完整性與 Artifact 信任</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-entitlement-revocation-lag-after-plan-downgrade/" data-link-title="7.R11.P7 降級後能力回收延遲" data-link-desc="說明方案降級後能力回收延遲如何造成授權邊界漂移">7.R11.P7 降級後能力回收延遲</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.8 匯出流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/export-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/export-flow-abuse/</guid><description>&lt;p>匯出流程的核心風險是把大量資料打包能力集中在少數入口。當匯出語意與資料分級不一致，流程會快速形成外送路徑。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>匯出功能通常承擔商業報表與營運需求。高可用匯出若缺少分級節奏與責任追蹤，濫用成本會明顯降低。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>匯出容量與頻率缺少分級限制。&lt;/li>
&lt;li>匯出檔案可長時間重複下載。&lt;/li>
&lt;li>匯出事件缺少主體與目的欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>匯出請求在短時間異常集中。&lt;/li>
&lt;li>匯出資料欄位超出既有用途範圍。&lt;/li>
&lt;li>匯出後接續跨組織分享行為。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/" data-link-title="7.R11.P8 匯出檔案長時間可重複下載" data-link-desc="說明匯出產物長時效與可重複下載如何放大資料外送風險">7.R11.P8 匯出檔案長時間可重複下載&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>匯出流程的核心風險是把大量資料打包能力集中在少數入口。當匯出語意與資料分級不一致，流程會快速形成外送路徑。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>匯出功能通常承擔商業報表與營運需求。高可用匯出若缺少分級節奏與責任追蹤，濫用成本會明顯降低。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>匯出容量與頻率缺少分級限制。</li>
<li>匯出檔案可長時間重複下載。</li>
<li>匯出事件缺少主體與目的欄位。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>匯出請求在短時間異常集中。</li>
<li>匯出資料欄位超出既有用途範圍。</li>
<li>匯出後接續跨組織分享行為。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></li>
<li><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/" data-link-title="7.R11.P8 匯出檔案長時間可重複下載" data-link-desc="說明匯出產物長時效與可重複下載如何放大資料外送風險">7.R11.P8 匯出檔案長時間可重複下載</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.9 分享流程濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/sharing-flow-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/sharing-flow-abuse/</guid><description>&lt;p>分享流程的核心風險是把存取邊界從內部身份改成連結或第三方可達路徑。當分享條件與資料敏感度脫鉤，流程會形成外部擴散通道。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>分享流程追求協作速度。協作導向若缺少到期語意、範圍限制與回收機制，分享路徑會長期維持可達。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>分享連結缺少到期與用途邊界。&lt;/li>
&lt;li>分享對象範圍可被任意擴張。&lt;/li>
&lt;li>分享撤銷在快取與副本呈現同步延遲。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>高敏感資料分享行為在異常時段增加。&lt;/li>
&lt;li>分享連結在非預期地理位置被存取。&lt;/li>
&lt;li>分享撤銷後仍有持續存取事件。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-share-link-without-expiry-semantics/" data-link-title="7.R11.P9 分享連結缺少到期語意" data-link-desc="說明分享連結缺少到期語意如何把協作路徑轉成長尾暴露路徑">7.R11.P9 分享連結缺少到期語意&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>分享流程的核心風險是把存取邊界從內部身份改成連結或第三方可達路徑。當分享條件與資料敏感度脫鉤，流程會形成外部擴散通道。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>分享流程追求協作速度。協作導向若缺少到期語意、範圍限制與回收機制，分享路徑會長期維持可達。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>分享連結缺少到期與用途邊界。</li>
<li>分享對象範圍可被任意擴張。</li>
<li>分享撤銷在快取與副本呈現同步延遲。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>高敏感資料分享行為在異常時段增加。</li>
<li>分享連結在非預期地理位置被存取。</li>
<li>分享撤銷後仍有持續存取事件。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></li>
<li><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-share-link-without-expiry-semantics/" data-link-title="7.R11.P9 分享連結缺少到期語意" data-link-desc="說明分享連結缺少到期語意如何把協作路徑轉成長尾暴露路徑">7.R11.P9 分享連結缺少到期語意</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.10 批次操作濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/</guid><description>&lt;p>批次操作的核心風險是把單次操作能力放大成大範圍影響能力。當批次上下限與責任邊界不清晰，流程會放大事故衝擊。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>批次能力通常是為了提升營運效率。效率提升若缺少分段執行與中止條件，失效事件會一次覆蓋大量資產。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>批次任務缺乏租戶或資料域切分。&lt;/li>
&lt;li>批次流程缺少可中止與可回查節點。&lt;/li>
&lt;li>批次操作可由低門檻身份觸發。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>批次任務異常密集且跨租戶執行。&lt;/li>
&lt;li>單次批次影響資產數量快速上升。&lt;/li>
&lt;li>批次失敗後仍持續執行後續步驟。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/" data-link-title="7.9 服務生命週期的資安風險節奏" data-link-desc="定義設計、上線、變更、事故、復盤五段中的資安問題節點">7.9 服務生命週期的資安風險節奏&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-batch-flow-without-stop-checkpoint/" data-link-title="7.R11.P10 批次流程缺少中止檢查點" data-link-desc="說明批次流程缺少中止檢查點如何放大單次失效衝擊">7.R11.P10 批次流程缺少中止檢查點&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>批次操作的核心風險是把單次操作能力放大成大範圍影響能力。當批次上下限與責任邊界不清晰，流程會放大事故衝擊。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>批次能力通常是為了提升營運效率。效率提升若缺少分段執行與中止條件，失效事件會一次覆蓋大量資產。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>批次任務缺乏租戶或資料域切分。</li>
<li>批次流程缺少可中止與可回查節點。</li>
<li>批次操作可由低門檻身份觸發。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>批次任務異常密集且跨租戶執行。</li>
<li>單次批次影響資產數量快速上升。</li>
<li>批次失敗後仍持續執行後續步驟。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-lifecycle-risk-cadence/" data-link-title="7.9 服務生命週期的資安風險節奏" data-link-desc="定義設計、上線、變更、事故、復盤五段中的資安問題節點">7.9 服務生命週期的資安風險節奏</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-batch-flow-without-stop-checkpoint/" data-link-title="7.R11.P10 批次流程缺少中止檢查點" data-link-desc="說明批次流程缺少中止檢查點如何放大單次失效衝擊">7.R11.P10 批次流程缺少中止檢查點</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.11 跨租戶協作濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/cross-tenant-collaboration-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/cross-tenant-collaboration-abuse/</guid><description>&lt;p>跨租戶協作的核心風險是把隔離邊界與協作邊界放在同一流程。當租戶邏輯與協作語意沒有明確切分，流程會形成邊界滲漏。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>跨租戶協作通常服務商業生態與合作流程。協作需求若缺少租戶上下文檢查與權限最小化，讀取邊界容易被擴張。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>協作邀請可直接取得跨租戶資料讀取。&lt;/li>
&lt;li>租戶切換後沿用先前租戶權限快取。&lt;/li>
&lt;li>協作關係中止後權限回收延遲。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>跨租戶查詢頻率與資料量異常上升。&lt;/li>
&lt;li>租戶上下文切換與高風險操作連續發生。&lt;/li>
&lt;li>協作關係變更後仍有持續存取行為。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-cross-tenant-context-cache-residue/" data-link-title="7.R11.P11 跨租戶上下文快取殘留" data-link-desc="說明跨租戶上下文快取殘留如何造成租戶邊界滲漏">7.R11.P11 跨租戶上下文快取殘留&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>跨租戶協作的核心風險是把隔離邊界與協作邊界放在同一流程。當租戶邏輯與協作語意沒有明確切分，流程會形成邊界滲漏。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>跨租戶協作通常服務商業生態與合作流程。協作需求若缺少租戶上下文檢查與權限最小化，讀取邊界容易被擴張。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>協作邀請可直接取得跨租戶資料讀取。</li>
<li>租戶切換後沿用先前租戶權限快取。</li>
<li>協作關係中止後權限回收延遲。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>跨租戶查詢頻率與資料量異常上升。</li>
<li>租戶上下文切換與高風險操作連續發生。</li>
<li>協作關係變更後仍有持續存取行為。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-cross-tenant-context-cache-residue/" data-link-title="7.R11.P11 跨租戶上下文快取殘留" data-link-desc="說明跨租戶上下文快取殘留如何造成租戶邊界滲漏">7.R11.P11 跨租戶上下文快取殘留</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.12 第三方授權濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/</guid><description>&lt;p>第三方授權的核心風險是把外部信任直接映射成內部操作能力。當授權範圍與回收節奏沒有分域，外部事件會快速傳導到內部。&lt;/p>
&lt;h2 id="為什麼會出問題">為什麼會出問題&lt;/h2>
&lt;p>第三方授權流程通常強調整合便利性。便利導向若缺少範圍限制與失效節奏，授權結果會長期超出原始用途。&lt;/p>
&lt;h2 id="常見失效樣式">常見失效樣式&lt;/h2>
&lt;ul>
&lt;li>第三方 token 權限過寬且期限過長。&lt;/li>
&lt;li>授權撤銷與內部會話失效不同步。&lt;/li>
&lt;li>供應商事件後缺少分域盤點流程。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>第三方 token 在非預期服務持續被使用。&lt;/li>
&lt;li>供應商事件後高權限 token 存續比例偏高。&lt;/li>
&lt;li>第三方授權事件在責任主體回查上出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="可連動章節">可連動章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10 Workload Identity 與聯邦信任邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="對應失效樣式卡">對應失效樣式卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">7.R11.P12 第三方 token 授權範圍過寬&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="演練--控制落地">演練 / 控制落地&lt;/h2>
&lt;p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>第三方授權的核心風險是把外部信任直接映射成內部操作能力。當授權範圍與回收節奏沒有分域，外部事件會快速傳導到內部。</p>
<h2 id="為什麼會出問題">為什麼會出問題</h2>
<p>第三方授權流程通常強調整合便利性。便利導向若缺少範圍限制與失效節奏，授權結果會長期超出原始用途。</p>
<h2 id="常見失效樣式">常見失效樣式</h2>
<ul>
<li>第三方 token 權限過寬且期限過長。</li>
<li>授權撤銷與內部會話失效不同步。</li>
<li>供應商事件後缺少分域盤點流程。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>第三方 token 在非預期服務持續被使用。</li>
<li>供應商事件後高權限 token 存續比例偏高。</li>
<li>第三方授權事件在責任主體回查上出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022</a></li>
</ul>
<h2 id="可連動章節">可連動章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.10 Workload Identity 與聯邦信任邊界</a></li>
</ul>
<h2 id="對應失效樣式卡">對應失效樣式卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">7.R11.P12 第三方 token 授權範圍過寬</a></li>
</ul>
<h2 id="演練--控制落地">演練 / 控制落地</h2>
<p>把本失效樣式轉成 release gate / tabletop 欄位的 blue-team control-pattern：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P1 可重放邀請連結</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-invitation-link/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-invitation-link/</guid><description>&lt;p>這個失效樣式的核心問題是邀請連結缺少一次性語意。當連結可重放且有效期長，邀請流程會形成持續可利用入口。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>邀請連結缺少一次性驗證狀態。&lt;/li>
&lt;li>邀請連結有效期與風險等級沒有對齊。&lt;/li>
&lt;li>邀請接受後舊連結仍保有可用性。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一邀請 token 出現多次接受嘗試。&lt;/li>
&lt;li>邀請完成後仍存在有效連結存取紀錄。&lt;/li>
&lt;li>新邀請身份在短時間觸及高風險能力。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是邀請連結缺少一次性語意。當連結可重放且有效期長，邀請流程會形成持續可利用入口。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>邀請連結缺少一次性驗證狀態。</li>
<li>邀請連結有效期與風險等級沒有對齊。</li>
<li>邀請接受後舊連結仍保有可用性。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一邀請 token 出現多次接受嘗試。</li>
<li>邀請完成後仍存在有效連結存取紀錄。</li>
<li>新邀請身份在短時間觸及高風險能力。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P2 提交與審核責任重疊</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-submitter-approver-overlap/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-submitter-approver-overlap/</guid><description>&lt;p>這個失效樣式的核心問題是責任鏈缺少獨立性。當提交與審核長期重疊，審核節點會失去實質判讀功能。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>審核人與提交人屬於同一高頻操作群組。&lt;/li>
&lt;li>審核節奏只追求吞吐，不追求情境證據。&lt;/li>
&lt;li>高風險與低風險請求共用同一審核路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>高風險請求通過時間長期偏短。&lt;/li>
&lt;li>審核意見長期模板化且低變化。&lt;/li>
&lt;li>事故回查時審核依據鏈條出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/" data-link-title="7.R11.2 審核流程濫用" data-link-desc="說明審核節點為何會變成形式審核，進而放大高風險操作">審核流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是責任鏈缺少獨立性。當提交與審核長期重疊，審核節點會失去實質判讀功能。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>審核人與提交人屬於同一高頻操作群組。</li>
<li>審核節奏只追求吞吐，不追求情境證據。</li>
<li>高風險與低風險請求共用同一審核路徑。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>高風險請求通過時間長期偏短。</li>
<li>審核意見長期模板化且低變化。</li>
<li>事故回查時審核依據鏈條出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/" data-link-title="7.R11.2 審核流程濫用" data-link-desc="說明審核節點為何會變成形式審核，進而放大高風險操作">審核流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核追蹤與責任邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P3 代理會話上下文混層</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-delegated-session-context-bleed/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-delegated-session-context-bleed/</guid><description>&lt;p>這個失效樣式的核心問題是代理上下文與原始上下文沒有清楚切分。當會話混層，代理能力會穿透原始責任邊界。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>代理會話與原始會話共用識別資訊。&lt;/li>
&lt;li>代理流程缺少目的與時效綁定。&lt;/li>
&lt;li>代理行為缺少獨立稽核欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>代理事件與原始用戶事件難以區分。&lt;/li>
&lt;li>代理主體短時間跨多租戶操作。&lt;/li>
&lt;li>代理會話接續執行高風險動作。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">代理操作濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是代理上下文與原始上下文沒有清楚切分。當會話混層，代理能力會穿透原始責任邊界。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>代理會話與原始會話共用識別資訊。</li>
<li>代理流程缺少目的與時效綁定。</li>
<li>代理行為缺少獨立稽核欄位。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>代理事件與原始用戶事件難以區分。</li>
<li>代理主體短時間跨多租戶操作。</li>
<li>代理會話接續執行高風險動作。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/" data-link-title="7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險" data-link-desc="社交工程進入客服工具後，如何形成特定客戶資料存取風險">Mailchimp 2023</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">代理操作濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P4 帳號切換後沿用高權限 token</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-stale-privileged-token-after-account-switch/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-stale-privileged-token-after-account-switch/</guid><description>&lt;p>這個失效樣式的核心問題是身份切換與 token 收斂節奏不一致。當切換完成仍沿用前一身份 token，流程會形成隱性越權。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>帳號切換只更新顯示層，未同步更新授權上下文。&lt;/li>
&lt;li>高權限 token 在切換後保持可用。&lt;/li>
&lt;li>切換流程缺少高風險動作再驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>切換後立即執行前一身份專屬操作。&lt;/li>
&lt;li>同一 token 出現在多身份上下文。&lt;/li>
&lt;li>會話事件在身份對齊上出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/" data-link-title="7.R11.4 帳號切換濫用" data-link-desc="說明多帳號切換為何容易形成會話混層與身份擴散">帳號切換濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是身份切換與 token 收斂節奏不一致。當切換完成仍沿用前一身份 token，流程會形成隱性越權。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>帳號切換只更新顯示層，未同步更新授權上下文。</li>
<li>高權限 token 在切換後保持可用。</li>
<li>切換流程缺少高風險動作再驗證。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>切換後立即執行前一身份專屬操作。</li>
<li>同一 token 出現在多身份上下文。</li>
<li>會話事件在身份對齊上出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/" data-link-title="7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險" data-link-desc="邊界設備會話資料外洩後，如何演變成帳號與服務風險">Citrix Bleed 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/" data-link-title="7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散" data-link-desc="從社交工程到內部工具存取，拆解身分流程與權限邊界的失效點">Uber 2022</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/" data-link-title="7.R11.4 帳號切換濫用" data-link-desc="說明多帳號切換為何容易形成會話混層與身份擴散">帳號切換濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P5 重設憑證可重放且有效期過長</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-reset-token-with-long-ttl/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-replayable-reset-token-with-long-ttl/</guid><description>&lt;p>這個失效樣式的核心問題是恢復流程的驗證強度低於登入流程。當重設憑證可重放且時效過長，身份接管窗口會持續擴張。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>重設 token 缺少一次性消耗語意。&lt;/li>
&lt;li>token 有效期未依風險分層。&lt;/li>
&lt;li>重設完成後舊會話仍維持可用。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一帳號短時間出現多次重設。&lt;/li>
&lt;li>重設完成後快速接續高風險操作。&lt;/li>
&lt;li>重設事件與異常地理登入重疊。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/password-reset-flow-abuse/" data-link-title="7.R11.5 密碼重設流程濫用" data-link-desc="說明密碼重設流程為何常成為身份接管入口">密碼重設流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是恢復流程的驗證強度低於登入流程。當重設憑證可重放且時效過長，身份接管窗口會持續擴張。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>重設 token 缺少一次性消耗語意。</li>
<li>token 有效期未依風險分層。</li>
<li>重設完成後舊會話仍維持可用。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一帳號短時間出現多次重設。</li>
<li>重設完成後快速接續高風險操作。</li>
<li>重設事件與異常地理登入重疊。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/" data-link-title="7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑" data-link-desc="社交工程如何穿透員工身分流程，並影響下游客戶與供應鏈">Twilio 2022</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/" data-link-title="7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險" data-link-desc="從員工釣魚事件到私有程式碼資產保護，建立身分與研發資產的聯防流程">Dropbox 2022</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/password-reset-flow-abuse/" data-link-title="7.R11.5 密碼重設流程濫用" data-link-desc="說明密碼重設流程為何常成為身份接管入口">密碼重設流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P6 權限提升缺乏時效綁定</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-privilege-elevation-without-time-bound/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-privilege-elevation-without-time-bound/</guid><description>&lt;p>這個失效樣式的核心問題是權限提升沒有清楚回收邊界。當提升缺少時效與目的綁定，例外能力會長期停留。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>提升請求缺少有效期限欄位。&lt;/li>
&lt;li>提升回收依賴人工排程。&lt;/li>
&lt;li>提升事件未同步到所有授權系統。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>提升後高權限存續時間持續拉長。&lt;/li>
&lt;li>同一主體反覆觸發提升後批次操作。&lt;/li>
&lt;li>提升與審核時序對齊持續偏移。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023（22515/22518）&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022（40684）&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是權限提升沒有清楚回收邊界。當提升缺少時效與目的綁定，例外能力會長期停留。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>提升請求缺少有效期限欄位。</li>
<li>提升回收依賴人工排程。</li>
<li>提升事件未同步到所有授權系統。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>提升後高權限存續時間持續拉長。</li>
<li>同一主體反覆觸發提升後批次操作。</li>
<li>提升與審核時序對齊持續偏移。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/" data-link-title="7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈" data-link-desc="Confluence 權限控制弱點在連續漏洞波次中如何擴大入口風險">Confluence 2023（22515/22518）</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/" data-link-title="7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過" data-link-desc="Fortinet 多產品認證繞過事件反映邊界與管理面共享風險">Fortinet 2022（40684）</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P7 降級後能力回收延遲</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-entitlement-revocation-lag-after-plan-downgrade/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-entitlement-revocation-lag-after-plan-downgrade/</guid><description>&lt;p>這個失效樣式的核心問題是商業狀態與技術授權狀態不同步。當降級後能力回收延遲，邊界會在時序差中擴張。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>升級即時生效，降級延後回收。&lt;/li>
&lt;li>計費狀態更新與授權狀態更新分離。&lt;/li>
&lt;li>降級事件缺少跨系統一致性檢查。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>降級後仍可呼叫高階功能。&lt;/li>
&lt;li>方案狀態與授權狀態長時間偏移。&lt;/li>
&lt;li>降級事件與高耗資源操作重疊。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya 2021&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/plan-change-flow-abuse/" data-link-title="7.R11.7 方案升降級流程濫用" data-link-desc="說明方案切換流程為何容易成為權限與資源邊界繞過點">方案升降級流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是商業狀態與技術授權狀態不同步。當降級後能力回收延遲，邊界會在時序差中擴張。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>升級即時生效，降級延後回收。</li>
<li>計費狀態更新與授權狀態更新分離。</li>
<li>降級事件缺少跨系統一致性檢查。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>降級後仍可呼叫高階功能。</li>
<li>方案狀態與授權狀態長時間偏移。</li>
<li>降級事件與高耗資源操作重疊。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/" data-link-title="7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑" data-link-desc="管理平台事件透過 MSP 模型向多客戶擴散時，workflow 應如何分層應對">Kaseya 2021</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/plan-change-flow-abuse/" data-link-title="7.R11.7 方案升降級流程濫用" data-link-desc="說明方案切換流程為何容易成為權限與資源邊界繞過點">方案升降級流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P8 匯出檔案長時間可重複下載</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/</guid><description>&lt;p>這個失效樣式的核心問題是匯出產物管理缺少時效與用途邊界。當匯出檔案長時間可重複下載，資料外送成本會顯著下降。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>匯出檔案連結缺少短時效策略。&lt;/li>
&lt;li>匯出產物缺少一次性下載語意。&lt;/li>
&lt;li>匯出任務缺少主體與目的綁定。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>同一匯出檔案多次下載。&lt;/li>
&lt;li>匯出下載行為出現在異常時段或來源。&lt;/li>
&lt;li>匯出後接續跨組織分享事件。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/export-flow-abuse/" data-link-title="7.R11.8 匯出流程濫用" data-link-desc="說明匯出流程為何常被放大為資料外送主路徑">匯出流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是匯出產物管理缺少時效與用途邊界。當匯出檔案長時間可重複下載，資料外送成本會顯著下降。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>匯出檔案連結缺少短時效策略。</li>
<li>匯出產物缺少一次性下載語意。</li>
<li>匯出任務缺少主體與目的綁定。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一匯出檔案多次下載。</li>
<li>匯出下載行為出現在異常時段或來源。</li>
<li>匯出後接續跨組織分享事件。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">WS_FTP 2023</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/export-flow-abuse/" data-link-title="7.R11.8 匯出流程濫用" data-link-desc="說明匯出流程為何常被放大為資料外送主路徑">匯出流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P9 分享連結缺少到期語意</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-share-link-without-expiry-semantics/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-share-link-without-expiry-semantics/</guid><description>&lt;p>這個失效樣式的核心問題是分享機制把內部邊界轉為外部可達邊界，且缺少到期收斂條件。當分享連結長期可達，風險會累積成長尾暴露。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>分享連結缺少到期時間與用途限制。&lt;/li>
&lt;li>分享撤銷與快取更新節奏不同步。&lt;/li>
&lt;li>分享權限變更缺少即時回收機制。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>分享連結在預期期限後仍可存取。&lt;/li>
&lt;li>高敏感資料分享行為在異常時段上升。&lt;/li>
&lt;li>分享撤銷後持續出現存取事件。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/sharing-flow-abuse/" data-link-title="7.R11.9 分享流程濫用" data-link-desc="說明分享流程為何容易把內部資料邊界轉成外部可達邊界">分享流程濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是分享機制把內部邊界轉為外部可達邊界，且缺少到期收斂條件。當分享連結長期可達，風險會累積成長尾暴露。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>分享連結缺少到期時間與用途限制。</li>
<li>分享撤銷與快取更新節奏不同步。</li>
<li>分享權限變更缺少即時回收機制。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>分享連結在預期期限後仍可存取。</li>
<li>高敏感資料分享行為在異常時段上升。</li>
<li>分享撤銷後持續出現存取事件。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/sharing-flow-abuse/" data-link-title="7.R11.9 分享流程濫用" data-link-desc="說明分享流程為何容易把內部資料邊界轉成外部可達邊界">分享流程濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P10 批次流程缺少中止檢查點</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-batch-flow-without-stop-checkpoint/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-batch-flow-without-stop-checkpoint/</guid><description>&lt;p>這個失效樣式的核心問題是批次能力缺少分段收斂節點。當流程沒有中止檢查點，單次失效會擴散成大範圍衝擊。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>批次流程缺少分段確認與中止條件。&lt;/li>
&lt;li>批次任務跨租戶執行沒有隔離邊界。&lt;/li>
&lt;li>批次執行事件缺少即時回報語意。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>單次批次影響資產數量快速放大。&lt;/li>
&lt;li>批次異常後後續步驟仍持續執行。&lt;/li>
&lt;li>批次任務在非預期時段集中觸發。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/" data-link-title="7.R11.10 批次操作濫用" data-link-desc="說明批次操作為何容易放大單次權限失效的影響半徑">批次操作濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是批次能力缺少分段收斂節點。當流程沒有中止檢查點，單次失效會擴散成大範圍衝擊。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>批次流程缺少分段確認與中止條件。</li>
<li>批次任務跨租戶執行沒有隔離邊界。</li>
<li>批次執行事件缺少即時回報語意。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>單次批次影響資產數量快速放大。</li>
<li>批次異常後後續步驟仍持續執行。</li>
<li>批次任務在非預期時段集中觸發。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/" data-link-title="7.R11.10 批次操作濫用" data-link-desc="說明批次操作為何容易放大單次權限失效的影響半徑">批次操作濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P11 跨租戶上下文快取殘留</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-cross-tenant-context-cache-residue/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-cross-tenant-context-cache-residue/</guid><description>&lt;p>這個失效樣式的核心問題是租戶上下文切換與快取更新節奏分離。當快取殘留，跨租戶協作路徑會產生邊界滲漏。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>租戶切換後快取上下文沒有即時更新。&lt;/li>
&lt;li>協作角色變更未同步回收跨租戶權限。&lt;/li>
&lt;li>查詢路徑共用多租戶快取鍵。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>跨租戶查詢頻率與資料量異常上升。&lt;/li>
&lt;li>租戶切換後仍出現前一租戶資料回應。&lt;/li>
&lt;li>協作關係中止後持續出現跨租戶存取。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/cross-tenant-collaboration-abuse/" data-link-title="7.R11.11 跨租戶協作濫用" data-link-desc="說明跨租戶協作為何容易形成租戶邊界滲漏">跨租戶協作濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是租戶上下文切換與快取更新節奏分離。當快取殘留，跨租戶協作路徑會產生邊界滲漏。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>租戶切換後快取上下文沒有即時更新。</li>
<li>協作角色變更未同步回收跨租戶權限。</li>
<li>查詢路徑共用多租戶快取鍵。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>跨租戶查詢頻率與資料量異常上升。</li>
<li>租戶切換後仍出現前一租戶資料回應。</li>
<li>協作關係中止後持續出現跨租戶存取。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/" data-link-title="7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險" data-link-desc="第三方整合 token 被竊後，如何形成跨組織存取風險">GitHub OAuth 2022</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/cross-tenant-collaboration-abuse/" data-link-title="7.R11.11 跨租戶協作濫用" data-link-desc="說明跨租戶協作為何容易形成租戶邊界滲漏">跨租戶協作濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R11.P12 第三方 token 授權範圍過寬</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/</guid><description>&lt;p>這個失效樣式的核心問題是外部授權範圍超出實際用途邊界。當第三方 token 權限過寬，外部事件會快速傳導到內部高風險路徑。&lt;/p>
&lt;h2 id="常見形成條件">常見形成條件&lt;/h2>
&lt;ul>
&lt;li>第三方 token scope 與實際用途不一致。&lt;/li>
&lt;li>token 期限過長且回收節奏落後。&lt;/li>
&lt;li>供應商事件後缺少分域收斂流程。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>token 在非預期服務持續被使用。&lt;/li>
&lt;li>供應商事件後高權限 token 存續比例偏高。&lt;/li>
&lt;li>第三方授權事件在責任回查鏈上出現斷點。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &amp;#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="來源流程卡">來源流程卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>本失效樣式對應的實作 chain：&lt;/p>
&lt;p>&lt;strong>控制面（mitigation 在這裡定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>演練 / 控制落地（轉成欄位）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個失效樣式的核心問題是外部授權範圍超出實際用途邊界。當第三方 token 權限過寬，外部事件會快速傳導到內部高風險路徑。</p>
<h2 id="常見形成條件">常見形成條件</h2>
<ul>
<li>第三方 token scope 與實際用途不一致。</li>
<li>token 期限過長且回收節奏落後。</li>
<li>供應商事件後缺少分域收斂流程。</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>token 在非預期服務持續被使用。</li>
<li>供應商事件後高權限 token 存續比例偏高。</li>
<li>第三方授權事件在責任回查鏈上出現斷點。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/" data-link-title="7.R7.1.2 Okta &#43; Cloudflare 2023：支援流程與身分供應鏈" data-link-desc="支援工單與第三方身份供應商路徑如何變成入侵鏈的一部分">Okta + Cloudflare 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/" data-link-title="7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂" data-link-desc="同一條供應商事件鏈，如何在客戶端變成 session 與 token 的收斂壓力">Cloudflare 2023</a></li>
<li><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/" data-link-title="7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑" data-link-desc="員工帳號被社交工程利用後，企業 token 與私有程式碼資產的防線如何運作">Slack 2022</a></li>
</ul>
<h2 id="來源流程卡">來源流程卡</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>本失效樣式對應的實作 chain：</p>
<p><strong>控制面（mitigation 在這裡定義）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a></li>
<li><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a></li>
</ul>
<p><strong>演練 / 控制落地（轉成欄位）</strong>：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></li>
</ul>
]]></content:encoded></item><item><title>7.R7.1.1 Uber 2022：MFA 疲勞與內部工具擴散</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/uber-2022-mfa-fatigue/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2022 年 9 月，攻擊者先取得承包商帳號，再透過重複 MFA 請求與社交工程進入內部系統，後續接觸到多個內部管理工具。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：social engineering → MFA push fatigue → 既有身份接入內部高權限工具的 identity-chain 失效。供應鏈植入、邊界零時差、資料外送量級壓力等其他 threat surface 由 supply-chain / edge-exposure / data-exfiltration 案例分類承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>取得初始帳號。&lt;/li>
&lt;li>以 MFA fatigue 增加使用者誤同意機率。&lt;/li>
&lt;li>使用已登入身份接觸內部高權限工具。&lt;/li>
&lt;li>擴大可見範圍並造成營運干擾。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>高風險登入路徑缺少 step-up 驗證。&lt;/li>
&lt;li>內部工具授權邊界不足，初始落點可快速擴散。&lt;/li>
&lt;li>身分異常事件與值班告警串接不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若值班流程缺少「異常 MFA 請求密度」檢查，團隊會把登入異常當成一般使用者問題，導致處置時間延後、擴散面增加。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：高風險操作要求 phishing-resistant 強認證（WebAuthn / passkey、阻擋可被連續同意的 push approval）+ 裝置信任綁定（managed device / posture check），mechanism 是讓「同意」不再是攻擊者唯一所需的人類動作。&lt;/li>
&lt;li>日常：監控 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a> 異常事件（短時內 MFA 請求密度、跨地理 / 跨裝置序列）與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 升級規則。&lt;/li>
&lt;li>事故中：快速凍結可疑身分、切斷高權限工具存取（依賴內部工具事先有 token revocation 與 session kill 能力）、建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用&lt;/a> —— 把本案例的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> —— 把樣式轉成 tabletop 與 release gate 欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://www.uber.com/newsroom/security-update/">uber.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊者進入路徑、影響範圍與第一手時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>Scattered Spider / UNC3944 TTP、跨組織 social engineering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-sms-phishing-sim-swapping-ransomware/">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>Mandiant 對 social engineering、SIM swap、後續勒索鏈 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2022 年 9 月，攻擊者先取得承包商帳號，再透過重複 MFA 請求與社交工程進入內部系統，後續接觸到多個內部管理工具。</p>
<p><strong>本案例的演示焦點</strong>：social engineering → MFA push fatigue → 既有身份接入內部高權限工具的 identity-chain 失效。供應鏈植入、邊界零時差、資料外送量級壓力等其他 threat surface 由 supply-chain / edge-exposure / data-exfiltration 案例分類承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>取得初始帳號。</li>
<li>以 MFA fatigue 增加使用者誤同意機率。</li>
<li>使用已登入身份接觸內部高權限工具。</li>
<li>擴大可見範圍並造成營運干擾。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>高風險登入路徑缺少 step-up 驗證。</li>
<li>內部工具授權邊界不足，初始落點可快速擴散。</li>
<li>身分異常事件與值班告警串接不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若值班流程缺少「異常 MFA 請求密度」檢查，團隊會把登入異常當成一般使用者問題，導致處置時間延後、擴散面增加。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：高風險操作要求 phishing-resistant 強認證（WebAuthn / passkey、阻擋可被連續同意的 push approval）+ 裝置信任綁定（managed device / posture check），mechanism 是讓「同意」不再是攻擊者唯一所需的人類動作。</li>
<li>日常：監控 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a> 異常事件（短時內 MFA 請求密度、跨地理 / 跨裝置序列）與 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 升級規則。</li>
<li>事故中：快速凍結可疑身分、切斷高權限工具存取（依賴內部工具事先有 token revocation 與 session kill 能力）、建立 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a>。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用</a> —— 把本案例的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> —— 把樣式轉成 tabletop 與 release gate 欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.uber.com/newsroom/security-update/">uber.com</a></td>
          <td>官方</td>
          <td>攻擊者進入路徑、影響範圍與第一手時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>Scattered Spider / UNC3944 TTP、跨組織 social engineering</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-sms-phishing-sim-swapping-ransomware/">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>Mandiant 對 social engineering、SIM swap、後續勒索鏈 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.2 Okta + Cloudflare 2023：支援流程與身分供應鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/okta-cloudflare-2023-support-supply-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 10 到 11 月，Okta 與 Cloudflare 的公開說明都指出，攻擊者透過支援相關流程取得可用資訊，形成跨組織的身分供應鏈風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：上游供應商支援流程（HAR 檔 / 工單附件 / session token）→ 客戶側身分接管的跨組織 chain。重點在 support workflow 承載身分敏感材料時的邊界 / 通報節奏設計。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定支援流程與可取得的工單資料。&lt;/li>
&lt;li>利用流程缺口取得敏感資訊或權限線索。&lt;/li>
&lt;li>以第三方身份供應商作為橋接點延伸到客戶側。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>支援資料流沒有被視為高敏感資產。&lt;/li>
&lt;li>憑證或會話資料生命周期管理不足。&lt;/li>
&lt;li>供應商事件到客戶內部輪替流程沒有強制觸發。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「供應商事件觸發的全域憑證輪替」，事件會停在公告層，實際可利用的憑證仍留在環境中。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：支援系統資料分級、限制下載與外流路徑（HAR sanitizer、附件 retention 限制），mechanism 是讓支援系統的「便利性」不直接傳導到身分風險。&lt;/li>
&lt;li>日常：建立第三方事件觸發的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>（含 cross-vendor coordination、客戶先發現的反向通報）。&lt;/li>
&lt;li>事故中：啟用供應商事件專用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/playbook/" data-link-title="Playbook" data-link-desc="說明場景化處置腳本如何降低事故處理不確定性">playbook&lt;/a>、執行輪替、追蹤、封鎖（前提是輪替能力涵蓋第三方授權 token、不只內部 session）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">Overscoped 第三方 token grant&lt;/a> —— 把 support workflow 承載身分材料的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a> —— 把樣式轉成 tabletop、release gate 欄位與跨組織 owner 分工。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://sec.okta.com/articles/2023/11/unauthorized-access-oktas-support-case-management-system-root-cause">sec.okta.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊路徑、support system root cause、影響範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.cloudflare.com/thanksgiving-2023-security-incident/">blog.cloudflare.com&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>客戶側偵測 / 即時回應、Zero Trust 防守效果（peer evidence）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC3944 對 SaaS / 身分供應鏈的攻擊模式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 10 到 11 月，Okta 與 Cloudflare 的公開說明都指出，攻擊者透過支援相關流程取得可用資訊，形成跨組織的身分供應鏈風險。</p>
<p><strong>本案例的演示焦點</strong>：上游供應商支援流程（HAR 檔 / 工單附件 / session token）→ 客戶側身分接管的跨組織 chain。重點在 support workflow 承載身分敏感材料時的邊界 / 通報節奏設計。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定支援流程與可取得的工單資料。</li>
<li>利用流程缺口取得敏感資訊或權限線索。</li>
<li>以第三方身份供應商作為橋接點延伸到客戶側。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>支援資料流沒有被視為高敏感資產。</li>
<li>憑證或會話資料生命周期管理不足。</li>
<li>供應商事件到客戶內部輪替流程沒有強制觸發。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「供應商事件觸發的全域憑證輪替」，事件會停在公告層，實際可利用的憑證仍留在環境中。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：支援系統資料分級、限制下載與外流路徑（HAR sanitizer、附件 retention 限制），mechanism 是讓支援系統的「便利性」不直接傳導到身分風險。</li>
<li>日常：建立第三方事件觸發的 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>（含 cross-vendor coordination、客戶先發現的反向通報）。</li>
<li>事故中：啟用供應商事件專用 <a href="/blog/backend/knowledge-cards/playbook/" data-link-title="Playbook" data-link-desc="說明場景化處置腳本如何降低事故處理不確定性">playbook</a>、執行輪替、追蹤、封鎖（前提是輪替能力涵蓋第三方授權 token、不只內部 session）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">Overscoped 第三方 token grant</a> —— 把 support workflow 承載身分材料的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a> —— 把樣式轉成 tabletop、release gate 欄位與跨組織 owner 分工。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://sec.okta.com/articles/2023/11/unauthorized-access-oktas-support-case-management-system-root-cause">sec.okta.com</a></td>
          <td>官方</td>
          <td>攻擊路徑、support system root cause、影響範圍</td>
      </tr>
      <tr>
          <td><a href="https://blog.cloudflare.com/thanksgiving-2023-security-incident/">blog.cloudflare.com</a></td>
          <td>政府/監管</td>
          <td>客戶側偵測 / 即時回應、Zero Trust 防守效果（peer evidence）</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC3944 對 SaaS / 身分供應鏈的攻擊模式</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.3 Twilio 2022：社交工程與員工帳號路徑</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/twilio-2022-social-engineering/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2022 年 8 月，Twilio 公告社交工程攻擊造成員工帳號被濫用，影響內部系統與部分客戶關聯風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：員工 phishing → 內部管理工具接管 → 下游客戶 / 供應鏈傳導的 identity-chain 風險。重點在「員工身份」即「客戶風險面」的傳導邊界。其他 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>以釣魚或社交工程瞄準員工。&lt;/li>
&lt;li>取得可登入的員工身份。&lt;/li>
&lt;li>使用合法身份移動到高價值系統與資料。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>員工身份保護流程對社交工程韌性不足。&lt;/li>
&lt;li>登入後的高敏感操作缺少額外驗證。&lt;/li>
&lt;li>身分異常事件與快速隔離機制不夠緊密。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「員工帳號異常即時隔離」步驟，攻擊者會持續用合法會話做橫向移動，調查難度與影響面同步上升。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：高風險管理操作要求二次核准（multi-party approval、不只 MFA），mechanism 是讓單一帳號接管無法觸發影響客戶的決策。&lt;/li>
&lt;li>日常：針對員工身份建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook&lt;/a>（管理工具登入跨地理 / 跨裝置 / 異常時段）。&lt;/li>
&lt;li>事故中：執行分批憑證輪替與權限縮減、控制 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>（前提是 token / 權限有 audit trail 可分批 scope）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/" data-link-title="7.R11.2 審核流程濫用" data-link-desc="說明審核節點為何會變成形式審核，進而放大高風險操作">核准流程濫用&lt;/a> —— 把員工身分 → 管理工具 → 客戶傳導的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> —— 把樣式轉成 tabletop 與 release gate 欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://www.twilio.com/en-us/blog/august-2022-social-engineering-attack">twilio.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊入口、影響範圍、員工 phishing kit 第一手 telemetry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>Scattered Spider / UNC3944 跨組織 TTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-sms-phishing-sim-swapping-ransomware/">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>Mandiant 對 SMS phishing / SIM swap 後續鏈 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2022 年 8 月，Twilio 公告社交工程攻擊造成員工帳號被濫用，影響內部系統與部分客戶關聯風險。</p>
<p><strong>本案例的演示焦點</strong>：員工 phishing → 內部管理工具接管 → 下游客戶 / 供應鏈傳導的 identity-chain 風險。重點在「員工身份」即「客戶風險面」的傳導邊界。其他 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>以釣魚或社交工程瞄準員工。</li>
<li>取得可登入的員工身份。</li>
<li>使用合法身份移動到高價值系統與資料。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>員工身份保護流程對社交工程韌性不足。</li>
<li>登入後的高敏感操作缺少額外驗證。</li>
<li>身分異常事件與快速隔離機制不夠緊密。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「員工帳號異常即時隔離」步驟，攻擊者會持續用合法會話做橫向移動，調查難度與影響面同步上升。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：高風險管理操作要求二次核准（multi-party approval、不只 MFA），mechanism 是讓單一帳號接管無法觸發影響客戶的決策。</li>
<li>日常：針對員工身份建立 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>（管理工具登入跨地理 / 跨裝置 / 異常時段）。</li>
<li>事故中：執行分批憑證輪替與權限縮減、控制 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a>（前提是 token / 權限有 audit trail 可分批 scope）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/approval-flow-abuse/" data-link-title="7.R11.2 審核流程濫用" data-link-desc="說明審核節點為何會變成形式審核，進而放大高風險操作">核准流程濫用</a> —— 把員工身分 → 管理工具 → 客戶傳導的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> —— 把樣式轉成 tabletop 與 release gate 欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.twilio.com/en-us/blog/august-2022-social-engineering-attack">twilio.com</a></td>
          <td>官方</td>
          <td>攻擊入口、影響範圍、員工 phishing kit 第一手 telemetry</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>Scattered Spider / UNC3944 跨組織 TTP</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-sms-phishing-sim-swapping-ransomware/">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>Mandiant 對 SMS phishing / SIM swap 後續鏈 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 9 月，MGM 對外更新顯示，資安事件對營運造成明顯衝擊，反映出身份流程事件可快速轉為可用性問題。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：helpdesk social engineering → 高權限帳號接管 → 橫向擴散到核心系統 → 可用性 / 營運衝擊的 identity-to-availability chain。其他 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>以身分流程弱點取得初始落點。&lt;/li>
&lt;li>橫向影響多個內部系統。&lt;/li>
&lt;li>連帶影響面向客戶的服務可用性。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>身分事件與營運隔離界線不足。&lt;/li>
&lt;li>關鍵業務流程缺少快速降級方案。&lt;/li>
&lt;li>事件切換流程在高壓下不夠標準化。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「服務降級與切換劇本」，即使識別到攻擊路徑，也難以在可接受時間內維持核心服務。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：定義關鍵能力的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation&lt;/a> 路徑，mechanism 是讓「身分受損」跟「營運停擺」解耦——不依賴攻擊期間能即時設計。&lt;/li>
&lt;li>日常：演練 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover&lt;/a> 與回復時序（含 helpdesk 重置流程的 callback 驗證 / out-of-band 確認）。&lt;/li>
&lt;li>事故中：依 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 快速分級與跨團隊指揮（前提是事先有單一 IC 角色與升級 ladder）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/" data-link-title="7.R11.4 帳號切換濫用" data-link-desc="說明多帳號切換為何容易形成會話混層與身份擴散">帳號切換濫用&lt;/a> —— helpdesk 重置 / 身分 takeover 的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> —— 把樣式轉成 tabletop 與 release gate / 回復欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://investors.mgmresorts.com/investors/news-releases/press-release-details/2023/MGM-Resorts-Provides-Cybersecurity-Incident-Update/default.aspx">investors.mgmresorts.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>事件對外揭露、影響範圍、復原時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>Scattered Spider / UNC3944 TTP、helpdesk 社交工程模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>Mandiant 對 helpdesk impersonation、SaaS 後續擴散 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 9 月，MGM 對外更新顯示，資安事件對營運造成明顯衝擊，反映出身份流程事件可快速轉為可用性問題。</p>
<p><strong>本案例的演示焦點</strong>：helpdesk social engineering → 高權限帳號接管 → 橫向擴散到核心系統 → 可用性 / 營運衝擊的 identity-to-availability chain。其他 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>以身分流程弱點取得初始落點。</li>
<li>橫向影響多個內部系統。</li>
<li>連帶影響面向客戶的服務可用性。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>身分事件與營運隔離界線不足。</li>
<li>關鍵業務流程缺少快速降級方案。</li>
<li>事件切換流程在高壓下不夠標準化。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「服務降級與切換劇本」，即使識別到攻擊路徑，也難以在可接受時間內維持核心服務。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：定義關鍵能力的 <a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a> 路徑，mechanism 是讓「身分受損」跟「營運停擺」解耦——不依賴攻擊期間能即時設計。</li>
<li>日常：演練 <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a> 與回復時序（含 helpdesk 重置流程的 callback 驗證 / out-of-band 確認）。</li>
<li>事故中：依 <a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 快速分級與跨團隊指揮（前提是事先有單一 IC 角色與升級 ladder）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/account-switching-abuse/" data-link-title="7.R11.4 帳號切換濫用" data-link-desc="說明多帳號切換為何容易形成會話混層與身份擴散">帳號切換濫用</a> —— helpdesk 重置 / 身分 takeover 的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> —— 把樣式轉成 tabletop 與 release gate / 回復欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://investors.mgmresorts.com/investors/news-releases/press-release-details/2023/MGM-Resorts-Provides-Cybersecurity-Incident-Update/default.aspx">investors.mgmresorts.com</a></td>
          <td>官方</td>
          <td>事件對外揭露、影響範圍、復原時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>Scattered Spider / UNC3944 TTP、helpdesk 社交工程模式</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>Mandiant 對 helpdesk impersonation、SaaS 後續擴散 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Storm-0558 事件揭露簽章金鑰治理一旦失效，攻擊者就能沿著身分信任鏈存取雲端郵件服務。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：簽章金鑰外洩 → 偽造可被驗證 token → 跨租戶身分接管的 federated trust chain 失效。屬於高層信任根（key material）類別、有別於前端社交工程或邊界漏洞。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>取得可用的簽章金鑰材料。&lt;/li>
&lt;li>偽造可被驗證的身分權杖。&lt;/li>
&lt;li>以合法樣態存取目標信箱與資料。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>簽章金鑰生命週期治理與隔離策略不足。&lt;/li>
&lt;li>權杖驗證邊界缺少跨服務一致性檢查。&lt;/li>
&lt;li>高風險身分事件的追查與升級節奏偏慢。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「跨租戶權杖異常立即升級」步驟，攻擊者可在低噪音條件下維持存取並擴大影響面。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：把簽章金鑰納入硬體保護與輪替節奏（HSM-bound、不可導出 / 強制輪替週期），mechanism 是讓金鑰即使被讀也無法搬離保護邊界。&lt;/li>
&lt;li>日常：監控 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 的異常關聯（跨租戶 token 出現相同 issuer 但不應跨域的軌跡）。&lt;/li>
&lt;li>事故中：同步執行金鑰收斂、權杖失效、受影響範圍比對（前提是 token validation 路徑可在 fleet 層級熱抽換 issuer）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-federated-token-trust-drift/" data-link-title="7.R11.P13 聯邦 token 信任漂移" data-link-desc="說明跨平台聯邦 token 的來源與用途脫鉤如何放大傳導風險">Federated token trust drift&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用&lt;/a> —— 把跨租戶 token 驗證邊界失效的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成演練、輪替欄位與證據鏈。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://www.microsoft.com/en-us/msrc/blog/2023/07/microsoft-mitigates-china-based-threat-actor-storm-0558-targeting-of-customer-email/">microsoft.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊鏈、影響範圍、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/resources-tools/resources/review-board-report-microsoft-exchange-online-incident">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>CSRB 對 cloud signing 治理的系統性檢討&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://msrc.microsoft.com/blog/2023/09/results-of-major-technical-investigations-for-storm-0558-key-acquisition/">msrc.microsoft.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>金鑰取得 root cause、token validation 邊界深度分析&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Storm-0558 事件揭露簽章金鑰治理一旦失效，攻擊者就能沿著身分信任鏈存取雲端郵件服務。</p>
<p><strong>本案例的演示焦點</strong>：簽章金鑰外洩 → 偽造可被驗證 token → 跨租戶身分接管的 federated trust chain 失效。屬於高層信任根（key material）類別、有別於前端社交工程或邊界漏洞。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>取得可用的簽章金鑰材料。</li>
<li>偽造可被驗證的身分權杖。</li>
<li>以合法樣態存取目標信箱與資料。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>簽章金鑰生命週期治理與隔離策略不足。</li>
<li>權杖驗證邊界缺少跨服務一致性檢查。</li>
<li>高風險身分事件的追查與升級節奏偏慢。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「跨租戶權杖異常立即升級」步驟，攻擊者可在低噪音條件下維持存取並擴大影響面。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：把簽章金鑰納入硬體保護與輪替節奏（HSM-bound、不可導出 / 強制輪替週期），mechanism 是讓金鑰即使被讀也無法搬離保護邊界。</li>
<li>日常：監控 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 的異常關聯（跨租戶 token 出現相同 issuer 但不應跨域的軌跡）。</li>
<li>事故中：同步執行金鑰收斂、權杖失效、受影響範圍比對（前提是 token validation 路徑可在 fleet 層級熱抽換 issuer）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-federated-token-trust-drift/" data-link-title="7.R11.P13 聯邦 token 信任漂移" data-link-desc="說明跨平台聯邦 token 的來源與用途脫鉤如何放大傳導風險">Federated token trust drift</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用</a> —— 把跨租戶 token 驗證邊界失效的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> + <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成演練、輪替欄位與證據鏈。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.microsoft.com/en-us/msrc/blog/2023/07/microsoft-mitigates-china-based-threat-actor-storm-0558-targeting-of-customer-email/">microsoft.com</a></td>
          <td>官方</td>
          <td>攻擊鏈、影響範圍、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/resources-tools/resources/review-board-report-microsoft-exchange-online-incident">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>CSRB 對 cloud signing 治理的系統性檢討</td>
      </tr>
      <tr>
          <td><a href="https://msrc.microsoft.com/blog/2023/09/results-of-major-technical-investigations-for-storm-0558-key-acquisition/">msrc.microsoft.com</a></td>
          <td>技術分析</td>
          <td>金鑰取得 root cause、token validation 邊界深度分析</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.6 Cloudflare 2023：供應商事件後的身分收斂</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/cloudflare-2023-okta-token-follow-through/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cloudflare 在 2023 年事件說明中展示了供應商端事件如何傳導到客戶端身分流程，並觸發大規模憑證與 token 收斂作業。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：上游 Identity Provider 事件 → 下游客戶側 token / session 收斂壓力的 identity-chain 風險傳導。其他 threat surface（直接 phishing / 邊界零時差 / 供應鏈植入）由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者先利用供應商支援流程取得線索。&lt;/li>
&lt;li>嘗試使用取得的資訊進入客戶端環境。&lt;/li>
&lt;li>透過 token、session 或憑證鏈路擴展存取。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>供應商事件觸發條件與內部 runbook 連動不足。&lt;/li>
&lt;li>高權限 token 的失效與輪替策略準備度不足。&lt;/li>
&lt;li>受影響資產盤點與證據保存流程分離。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「供應商事件即啟動全域 token 盤點」步驟，事件判讀會停在公告層，內部可利用憑證仍持續存在。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：為第三方事件設計獨立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與責任分工，mechanism 是讓供應商公告直接 trigger 內部盤點，不停在「閱讀公告」layer。&lt;/li>
&lt;li>日常：維護 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/playbook/" data-link-title="Playbook" data-link-desc="說明場景化處置腳本如何降低事故處理不確定性">playbook&lt;/a> 的憑證輪替優先級（依 token 範圍 / 受影響 tenant 分層、不是平均輪替）。&lt;/li>
&lt;li>事故中：先凍結高風險憑證、再分批恢復必要權限（前提是事先有 token 範圍 inventory、否則無法分批）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">Overscoped 第三方 token grant&lt;/a> —— 把本案例的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> —— 把樣式轉成 tabletop 與 release gate 欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://blog.cloudflare.com/thanksgiving-2023-security-incident/">blog.cloudflare.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>客戶側偵測、即時回應、Zero Trust 與 hardware key 防守效果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://sec.okta.com/articles/2023/11/unauthorized-access-oktas-support-case-management-system-root-cause">sec.okta.com&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>上游事件 root cause、影響範圍、session token hijack 機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC3944 對 SaaS 攻擊 TTP、跨組織 chain 模式&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Cloudflare 在 2023 年事件說明中展示了供應商端事件如何傳導到客戶端身分流程，並觸發大規模憑證與 token 收斂作業。</p>
<p><strong>本案例的演示焦點</strong>：上游 Identity Provider 事件 → 下游客戶側 token / session 收斂壓力的 identity-chain 風險傳導。其他 threat surface（直接 phishing / 邊界零時差 / 供應鏈植入）由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者先利用供應商支援流程取得線索。</li>
<li>嘗試使用取得的資訊進入客戶端環境。</li>
<li>透過 token、session 或憑證鏈路擴展存取。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>供應商事件觸發條件與內部 runbook 連動不足。</li>
<li>高權限 token 的失效與輪替策略準備度不足。</li>
<li>受影響資產盤點與證據保存流程分離。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「供應商事件即啟動全域 token 盤點」步驟，事件判讀會停在公告層，內部可利用憑證仍持續存在。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：為第三方事件設計獨立 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與責任分工，mechanism 是讓供應商公告直接 trigger 內部盤點，不停在「閱讀公告」layer。</li>
<li>日常：維護 <a href="/blog/backend/knowledge-cards/playbook/" data-link-title="Playbook" data-link-desc="說明場景化處置腳本如何降低事故處理不確定性">playbook</a> 的憑證輪替優先級（依 token 範圍 / 受影響 tenant 分層、不是平均輪替）。</li>
<li>事故中：先凍結高風險憑證、再分批恢復必要權限（前提是事先有 token 範圍 inventory、否則無法分批）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/third-party-authorization-abuse/" data-link-title="7.R11.12 第三方授權濫用" data-link-desc="說明第三方授權流程為何容易成為供應商事件傳導節點">第三方授權濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-overscoped-third-party-token-grant/" data-link-title="7.R11.P12 第三方 token 授權範圍過寬" data-link-desc="說明第三方 token 授權範圍過寬如何放大供應商事件傳導">Overscoped 第三方 token grant</a> —— 把本案例的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> —— 把樣式轉成 tabletop 與 release gate 欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://blog.cloudflare.com/thanksgiving-2023-security-incident/">blog.cloudflare.com</a></td>
          <td>官方</td>
          <td>客戶側偵測、即時回應、Zero Trust 與 hardware key 防守效果</td>
      </tr>
      <tr>
          <td><a href="https://sec.okta.com/articles/2023/11/unauthorized-access-oktas-support-case-management-system-root-cause">sec.okta.com</a></td>
          <td>政府/監管</td>
          <td>上游事件 root cause、影響範圍、session token hijack 機制</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC3944 對 SaaS 攻擊 TTP、跨組織 chain 模式</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.7 Slack 2022：企業 token 與程式碼資產路徑</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/slack-2022-token-compromise/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Slack 2022 安全公告說明攻擊者透過員工帳號路徑接觸內部資產，突顯企業 token 與程式碼資產的連動風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：員工身分被取得後 → 內部 token / 程式碼資產的橫向擴散風險，重點在 token 範圍邊界與 audit signal 匯流的設計。其他 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>先透過社交工程取得員工憑證。&lt;/li>
&lt;li>進入內部工具並接觸 token 或程式碼資產。&lt;/li>
&lt;li>嘗試擴大到高價值系統或資料節點。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>員工身份遭濫用後的隔離速度不足。&lt;/li>
&lt;li>token 範圍與用途邊界定義不夠細緻。&lt;/li>
&lt;li>程式碼資產存取異常訊號未快速匯流。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「內部 token 快速撤銷」步驟，攻擊者會維持有效會話，讓追查與復原成本上升。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：把管理 token 分域並限制到最小權限（依用途切 audience，避免單一 token 跨多個敏感系統），mechanism 是讓單點接管不會直接通到所有資產。&lt;/li>
&lt;li>日常：建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook&lt;/a> 監控異常存取（repo 異常 clone、token 跨 IP / 跨 device 序列）。&lt;/li>
&lt;li>事故中：分層撤銷 token、並用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 框定影響面（前提是 token 有 inventory 可查 issuer / scope）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用&lt;/a> —— 把員工身分接管 → token / 資產存取的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成 token 治理欄位與證據鏈。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://slack.com/blog/news/slack-security-update">slack.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊入口、影響範圍、token 處置時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨組織 social engineering TTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC3944 對 SaaS / token 接管模式 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Slack 2022 安全公告說明攻擊者透過員工帳號路徑接觸內部資產，突顯企業 token 與程式碼資產的連動風險。</p>
<p><strong>本案例的演示焦點</strong>：員工身分被取得後 → 內部 token / 程式碼資產的橫向擴散風險，重點在 token 範圍邊界與 audit signal 匯流的設計。其他 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>先透過社交工程取得員工憑證。</li>
<li>進入內部工具並接觸 token 或程式碼資產。</li>
<li>嘗試擴大到高價值系統或資料節點。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>員工身份遭濫用後的隔離速度不足。</li>
<li>token 範圍與用途邊界定義不夠細緻。</li>
<li>程式碼資產存取異常訊號未快速匯流。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「內部 token 快速撤銷」步驟，攻擊者會維持有效會話，讓追查與復原成本上升。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：把管理 token 分域並限制到最小權限（依用途切 audience，避免單一 token 跨多個敏感系統），mechanism 是讓單點接管不會直接通到所有資產。</li>
<li>日常：建立 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a> 監控異常存取（repo 異常 clone、token 跨 IP / 跨 device 序列）。</li>
<li>事故中：分層撤銷 token、並用 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 框定影響面（前提是 token 有 inventory 可查 issuer / scope）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用</a> —— 把員工身分接管 → token / 資產存取的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成 token 治理欄位與證據鏈。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://slack.com/blog/news/slack-security-update">slack.com</a></td>
          <td>官方</td>
          <td>攻擊入口、影響範圍、token 處置時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨組織 social engineering TTP</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC3944 對 SaaS / token 接管模式 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.1.8 Dropbox 2022：釣魚入侵與程式碼倉儲風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/dropbox-2022-code-repo-phishing-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Dropbox 2022 事件顯示員工帳號釣魚成功後，攻擊者可接觸私有程式碼倉儲與內部文件資產。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：員工 phishing → OAuth / 內部 SSO 接管 → 高敏感研發資產（私有 repo / 內部文件）橫向存取的身分鏈。其他 threat surface 由 supply-chain（artifact 植入）/ edge-exposure（邊界漏洞）/ data-exfiltration（量級外送壓力）案例分類承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>社交工程鎖定員工帳號。&lt;/li>
&lt;li>取得可登入的企業身份。&lt;/li>
&lt;li>存取程式碼倉儲與內部文件系統。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>員工端高風險登入驗證策略不足。&lt;/li>
&lt;li>研發資產保護缺少額外 step-up 驗證。&lt;/li>
&lt;li>身分異常與程式碼倉儲稽核串接不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「程式碼資產異常存取升級」步驟，攻擊者可在內部環境延長停留時間並擴大探索範圍。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：對高敏感 repo 操作要求強化 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication&lt;/a>（phishing-resistant 因子、step-up 不只密碼 + OTP），mechanism 是讓 phishing-collected 憑證在 step-up 環節失效。&lt;/li>
&lt;li>日常：將 repo 存取告警納入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 流程（異常 clone / push 模式、跨地理 / 跨裝置序列）。&lt;/li>
&lt;li>事故中：即時凍結可疑憑證與連線、保留時間軸證據（依賴 repo / SSO 事先有 audit log retention）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用&lt;/a> —— 把 phishing → OAuth grant → 委派擴散的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— 研發資產 mitigation 的 mechanism / 前提在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成 release gate 欄位與證據保存欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://dropbox.tech/security/a-security-update-on-code-repositories">dropbox.tech&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>phishing 入口、影響範圍、研發資產處置時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨組織 social engineering TTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC3944 對 SaaS 攻擊模式、phishing kit telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Dropbox 2022 事件顯示員工帳號釣魚成功後，攻擊者可接觸私有程式碼倉儲與內部文件資產。</p>
<p><strong>本案例的演示焦點</strong>：員工 phishing → OAuth / 內部 SSO 接管 → 高敏感研發資產（私有 repo / 內部文件）橫向存取的身分鏈。其他 threat surface 由 supply-chain（artifact 植入）/ edge-exposure（邊界漏洞）/ data-exfiltration（量級外送壓力）案例分類承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>社交工程鎖定員工帳號。</li>
<li>取得可登入的企業身份。</li>
<li>存取程式碼倉儲與內部文件系統。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>員工端高風險登入驗證策略不足。</li>
<li>研發資產保護缺少額外 step-up 驗證。</li>
<li>身分異常與程式碼倉儲稽核串接不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「程式碼資產異常存取升級」步驟，攻擊者可在內部環境延長停留時間並擴大探索範圍。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：對高敏感 repo 操作要求強化 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a>（phishing-resistant 因子、step-up 不只密碼 + OTP），mechanism 是讓 phishing-collected 憑證在 step-up 環節失效。</li>
<li>日常：將 repo 存取告警納入 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 流程（異常 clone / push 模式、跨地理 / 跨裝置序列）。</li>
<li>事故中：即時凍結可疑憑證與連線、保留時間軸證據（依賴 repo / SSO 事先有 audit log retention）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/invite-flow-abuse/" data-link-title="7.R11.1 邀請流程濫用" data-link-desc="說明邀請流程為何容易形成身份擴散與越權入口">邀請流程濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用</a> —— 把 phishing → OAuth grant → 委派擴散的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— 研發資產 mitigation 的 mechanism / 前提在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成 release gate 欄位與證據保存欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://dropbox.tech/security/a-security-update-on-code-repositories">dropbox.tech</a></td>
          <td>官方</td>
          <td>phishing 入口、影響範圍、研發資產處置時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨組織 social engineering TTP</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC3944 對 SaaS 攻擊模式、phishing kit telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.1 SolarWinds 2020：更新鏈被濫用</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/solarwinds-2020-sunburst/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2020 年公開的 SUNBURST 事件顯示，攻擊者透過供應鏈植入，將惡意行為包裹在合法更新流程中進入大量組織。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：合法更新管道被植入後、依賴下游對「已簽章 artifact」的高度信任進行長期潛伏與橫向擴散，屬於 build / release pipeline 上游 compromise 類別。身分鏈接管、邊界零時差、資料外送速率壓力等 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>滲透供應鏈節點。&lt;/li>
&lt;li>在合法交付流程植入惡意內容。&lt;/li>
&lt;li>依賴受害端對更新的高信任擴散。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>更新來源信任過於單點。&lt;/li>
&lt;li>行為監測難以區分合法元件與惡意利用。&lt;/li>
&lt;li>供應鏈異常事件缺少快速隔離流程。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「合法更新異常行為審查」，團隊會把事件視為一般系統活動，延長停留時間與清除成本。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：供應鏈節點做分層信任與簽章驗證（build provenance / SBOM / 簽章不只驗發行者、還驗 build 環境一致性），mechanism 是讓「合法簽章」不等於「未被植入」。&lt;/li>
&lt;li>日常：建立異常更新行為的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert&lt;/a>（受信任元件的非典型網路行為 / 異常 process 子鏈、不依賴單一 IoC）。&lt;/li>
&lt;li>事故中：切換受影響更新鏈、建立替代交付路徑與回復順序（前提是事先有 multi-source 更新策略、一鍵 cut-over 不能臨時設計）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成演練與控制欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的交付與簽章治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的供應鏈事件指揮流程。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故的失效樣式不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow 樣式），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.solarwinds.com/securityadvisory">solarwinds.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、植入時間軸、官方修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-352a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、檢測指引、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.mandiant.com/resources/blog/evasive-attacker-leverages-solarwinds-supply-chain-compromises">mandiant.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC2452 TTP、後門行為特徵、long-dwell evasion telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2020 年公開的 SUNBURST 事件顯示，攻擊者透過供應鏈植入，將惡意行為包裹在合法更新流程中進入大量組織。</p>
<p><strong>本案例的演示焦點</strong>：合法更新管道被植入後、依賴下游對「已簽章 artifact」的高度信任進行長期潛伏與橫向擴散，屬於 build / release pipeline 上游 compromise 類別。身分鏈接管、邊界零時差、資料外送速率壓力等 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>滲透供應鏈節點。</li>
<li>在合法交付流程植入惡意內容。</li>
<li>依賴受害端對更新的高信任擴散。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>更新來源信任過於單點。</li>
<li>行為監測難以區分合法元件與惡意利用。</li>
<li>供應鏈異常事件缺少快速隔離流程。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「合法更新異常行為審查」，團隊會把事件視為一般系統活動，延長停留時間與清除成本。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：供應鏈節點做分層信任與簽章驗證（build provenance / SBOM / 簽章不只驗發行者、還驗 build 環境一致性），mechanism 是讓「合法簽章」不等於「未被植入」。</li>
<li>日常：建立異常更新行為的 <a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a>（受信任元件的非典型網路行為 / 異常 process 子鏈、不依賴單一 IoC）。</li>
<li>事故中：切換受影響更新鏈、建立替代交付路徑與回復順序（前提是事先有 multi-source 更新策略、一鍵 cut-over 不能臨時設計）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> + <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成演練與控制欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的交付與簽章治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的供應鏈事件指揮流程。</li>
</ul>
<p>供應鏈類事故的失效樣式不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow 樣式），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.solarwinds.com/securityadvisory">solarwinds.com</a></td>
          <td>官方</td>
          <td>受影響版本、植入時間軸、官方修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-352a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、檢測指引、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://www.mandiant.com/resources/blog/evasive-attacker-leverages-solarwinds-supply-chain-compromises">mandiant.com</a></td>
          <td>技術分析</td>
          <td>UNC2452 TTP、後門行為特徵、long-dwell evasion telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.2 GitHub OAuth 2022：第三方 token 供應鏈風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/github-oauth-2022-token-supply-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2022 年 4 月，GitHub 公告指出攻擊者使用從第三方整合服務取得的 OAuth token 存取受影響組織資料。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：第三方整合 OAuth token 被竊 → 跨組織下游存取的 federated trust supply-chain 風險。重點在 OAuth scope / lifetime / inventory 設計、跟身分鏈接管 (identity-access category) 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊第三方整合節點。&lt;/li>
&lt;li>取得可用 OAuth token。&lt;/li>
&lt;li>使用 token 存取下游客戶資產。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>token 權限範圍過寬。&lt;/li>
&lt;li>token 生命周期偏長，撤銷速度慢。&lt;/li>
&lt;li>整合關係資產盤點與監控不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「第三方 token 全域盤點與快速撤銷」，事件發生後仍會留下可用 token，形成二次入侵窗口。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：採最小權限 token 與明確用途分域（OAuth scope 不用 catch-all、按 audience 切），mechanism 是讓單個 token 接管不會通往無關資產。&lt;/li>
&lt;li>日常：建立第三方整合清單與失效期限巡檢（含 token 上次使用時間、長期未用就主動失效）。&lt;/li>
&lt;li>事故中：依清單自動化撤銷、輪替、補授權（前提是 token issuer 提供 bulk revocation API）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> —— 把樣式轉成 token 治理欄位與輪替演練。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">backend/04-observability&lt;/a> 的第三方整合監測、&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的部署 token 治理。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://github.blog/news-insights/company-news/security-alert-stolen-oauth-user-tokens/">github.blog&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>OAuth token 被竊起點、影響組織範圍、初步處置時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://github.blog/2022-12-08-notice-of-security-incident/">github.blog&lt;/a>&lt;/td>
 &lt;td>官方延伸&lt;/td>
 &lt;td>後續事件、跨整合影響評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨組織 OAuth abuse / federated chain TTP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2022 年 4 月，GitHub 公告指出攻擊者使用從第三方整合服務取得的 OAuth token 存取受影響組織資料。</p>
<p><strong>本案例的演示焦點</strong>：第三方整合 OAuth token 被竊 → 跨組織下游存取的 federated trust supply-chain 風險。重點在 OAuth scope / lifetime / inventory 設計、跟身分鏈接管 (identity-access category) 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊第三方整合節點。</li>
<li>取得可用 OAuth token。</li>
<li>使用 token 存取下游客戶資產。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>token 權限範圍過寬。</li>
<li>token 生命周期偏長，撤銷速度慢。</li>
<li>整合關係資產盤點與監控不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「第三方 token 全域盤點與快速撤銷」，事件發生後仍會留下可用 token，形成二次入侵窗口。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：採最小權限 token 與明確用途分域（OAuth scope 不用 catch-all、按 audience 切），mechanism 是讓單個 token 接管不會通往無關資產。</li>
<li>日常：建立第三方整合清單與失效期限巡檢（含 token 上次使用時間、長期未用就主動失效）。</li>
<li>事故中：依清單自動化撤銷、輪替、補授權（前提是 token issuer 提供 bulk revocation API）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> + <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> —— 把樣式轉成 token 治理欄位與輪替演練。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">backend/04-observability</a> 的第三方整合監測、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的部署 token 治理。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.blog/news-insights/company-news/security-alert-stolen-oauth-user-tokens/">github.blog</a></td>
          <td>官方</td>
          <td>OAuth token 被竊起點、影響組織範圍、初步處置時序</td>
      </tr>
      <tr>
          <td><a href="https://github.blog/2022-12-08-notice-of-security-incident/">github.blog</a></td>
          <td>官方延伸</td>
          <td>後續事件、跨整合影響評估</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨組織 OAuth abuse / federated chain TTP</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.3 CircleCI 2023：CI secrets 輪替壓力</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/circleci-2023-secrets-rotation/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 1 月，CircleCI 公告指出攻擊者透過員工端點入侵影響生產環境，並要求客戶輪替 secrets。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：CI 平台側被入侵 → 客戶 secrets 整批暴露 → 下游全面輪替壓力的 secrets-blast-radius 事件。重點在 secrets 範圍 / 輪替成本與 inventory 的設計。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>以端點路徑取得平台側存取能力。&lt;/li>
&lt;li>觸及集中管理的 secrets。&lt;/li>
&lt;li>把風險擴散到客戶部署環境。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>CI secrets 集中化且缺少分域隔離。&lt;/li>
&lt;li>輪替流程成本高，導致執行延遲。&lt;/li>
&lt;li>客戶端難以快速判斷最小必要輪替範圍。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「分批輪替與優先級排序」流程，團隊要在壓力下做全面輪替，容易造成服務中斷或遺漏。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：定義 secrets 分級與依賴地圖（依 blast radius 分層、不只依名稱），mechanism 是讓事件期間的輪替能依風險排序、不靠 ad-hoc 判斷。&lt;/li>
&lt;li>日常：定期演練 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy&lt;/a> 與 secrets 更新（含「假設整個 CI vendor 受損」的 fire drill）。&lt;/li>
&lt;li>事故中：按分級快速輪替、並記錄 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>（前提是事先有 secrets inventory 跟 owner mapping）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> —— 把樣式轉成輪替演練、credential 治理與回復欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的 CI/CD 機制、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的止血與回復順序。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://circleci.com/blog/jan-4-2023-incident-report/">circleci.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊入口、影響範圍、初步輪替建議時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://circleci.com/blog/january-12-2023-security-alert/">circleci.com&lt;/a>&lt;/td>
 &lt;td>官方延伸&lt;/td>
 &lt;td>post-incident 細節、root cause、跨客戶影響評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨組織 social engineering / endpoint compromise TTP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 1 月，CircleCI 公告指出攻擊者透過員工端點入侵影響生產環境，並要求客戶輪替 secrets。</p>
<p><strong>本案例的演示焦點</strong>：CI 平台側被入侵 → 客戶 secrets 整批暴露 → 下游全面輪替壓力的 secrets-blast-radius 事件。重點在 secrets 範圍 / 輪替成本與 inventory 的設計。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>以端點路徑取得平台側存取能力。</li>
<li>觸及集中管理的 secrets。</li>
<li>把風險擴散到客戶部署環境。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>CI secrets 集中化且缺少分域隔離。</li>
<li>輪替流程成本高，導致執行延遲。</li>
<li>客戶端難以快速判斷最小必要輪替範圍。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「分批輪替與優先級排序」流程，團隊要在壓力下做全面輪替，容易造成服務中斷或遺漏。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：定義 secrets 分級與依賴地圖（依 blast radius 分層、不只依名稱），mechanism 是讓事件期間的輪替能依風險排序、不靠 ad-hoc 判斷。</li>
<li>日常：定期演練 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 與 secrets 更新（含「假設整個 CI vendor 受損」的 fire drill）。</li>
<li>事故中：按分級快速輪替、並記錄 <a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a>（前提是事先有 secrets inventory 跟 owner mapping）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理</a> + <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> —— 把樣式轉成輪替演練、credential 治理與回復欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的 CI/CD 機制、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的止血與回復順序。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://circleci.com/blog/jan-4-2023-incident-report/">circleci.com</a></td>
          <td>官方</td>
          <td>攻擊入口、影響範圍、初步輪替建議時序</td>
      </tr>
      <tr>
          <td><a href="https://circleci.com/blog/january-12-2023-security-alert/">circleci.com</a></td>
          <td>官方延伸</td>
          <td>post-incident 細節、root cause、跨客戶影響評估</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨組織 social engineering / endpoint compromise TTP</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.4 XZ Backdoor 2024：開源供應鏈長期滲透</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/xz-backdoor-2024-open-source-supply-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2024 年 3 月，XZ Utils 事件揭露開源供應鏈可被長期滲透並在釋出流程埋入後門，對基礎設施信任鏈造成直接衝擊。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：開源 maintainer 角色被長期社交滲透 → 釋出流程嵌入後門 → 跨 Linux 發行版下游擴散的 human-supply-chain 攻擊。重點在 maintainer trust governance、跟 build pipeline / artifact provenance 攻擊形成互補。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>長期滲透維護流程。&lt;/li>
&lt;li>在釋出包鏈條加入惡意邏輯。&lt;/li>
&lt;li>透過下游發行與部署流程擴散風險。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>開源維護與釋出治理缺少獨立覆核。&lt;/li>
&lt;li>下游對上游釋出信任過高。&lt;/li>
&lt;li>供應鏈檢測流程常延後辨識異常組件行為。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「上游重大事件觸發的版本凍結與風險重評」，下游仍可能將高風險版本推進正式環境。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：關鍵依賴建立雙人覆核與來源驗證（不只 hash 比對、檢查 release-tarball 跟 git tag 的差異），mechanism 是讓 maintainer 單點失守不會直接通到下游。&lt;/li>
&lt;li>日常：維護套件清單與影響面地圖（含 transitive 依賴、build-time vs runtime 區分）。&lt;/li>
&lt;li>事故中：啟動版本凍結、替代版本切換與復測流程（前提是事先有「該套件 unavailable 也能 build」的 fallback 評估）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> —— 把樣式轉成 SBOM 演練、版本凍結欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的依賴治理、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability&lt;/a> 的變更風險控制。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="http://www.openwall.com/lists/oss-security/2024/03/29/4">openwall.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>第一手揭露、後門技術細節、發現時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2024/03/29/reported-supply-chain-compromise-affecting-xz-utils-data-compression-library-cve-2024-3094">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3094">nvd.nist.gov/CVE-2024-3094&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、build-time injection 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2024 年 3 月，XZ Utils 事件揭露開源供應鏈可被長期滲透並在釋出流程埋入後門，對基礎設施信任鏈造成直接衝擊。</p>
<p><strong>本案例的演示焦點</strong>：開源 maintainer 角色被長期社交滲透 → 釋出流程嵌入後門 → 跨 Linux 發行版下游擴散的 human-supply-chain 攻擊。重點在 maintainer trust governance、跟 build pipeline / artifact provenance 攻擊形成互補。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>長期滲透維護流程。</li>
<li>在釋出包鏈條加入惡意邏輯。</li>
<li>透過下游發行與部署流程擴散風險。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>開源維護與釋出治理缺少獨立覆核。</li>
<li>下游對上游釋出信任過高。</li>
<li>供應鏈檢測流程常延後辨識異常組件行為。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「上游重大事件觸發的版本凍結與風險重評」，下游仍可能將高風險版本推進正式環境。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：關鍵依賴建立雙人覆核與來源驗證（不只 hash 比對、檢查 release-tarball 跟 git tag 的差異），mechanism 是讓 maintainer 單點失守不會直接通到下游。</li>
<li>日常：維護套件清單與影響面地圖（含 transitive 依賴、build-time vs runtime 區分）。</li>
<li>事故中：啟動版本凍結、替代版本切換與復測流程（前提是事先有「該套件 unavailable 也能 build」的 fallback 評估）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> —— 把樣式轉成 SBOM 演練、版本凍結欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的依賴治理、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability</a> 的變更風險控制。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="http://www.openwall.com/lists/oss-security/2024/03/29/4">openwall.com</a></td>
          <td>官方</td>
          <td>第一手揭露、後門技術細節、發現時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2024/03/29/reported-supply-chain-compromise-affecting-xz-utils-data-compression-library-cve-2024-3094">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3094">nvd.nist.gov/CVE-2024-3094</a></td>
          <td>技術分析</td>
          <td>CVE 細節、build-time injection 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.5 TeamCity 2023：CI 入口漏洞與交付鏈風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-cve-2023-42793-ci-entrypoint/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>TeamCity CVE-2023-42793 事件顯示 CI 平台入口漏洞會直接衝擊建置與交付信任鏈。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：CI 管理介面 auth bypass → build / artifact 接管 → 下游交付污染的 CI-entrypoint supply-chain 鏈。跟 2024 雙漏洞事件互補、共同說明 CI 平台暴露面治理。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描並利用 TeamCity 管理入口漏洞。&lt;/li>
&lt;li>取得 CI 管理權限或執行能力。&lt;/li>
&lt;li>影響 build artifact 與下游部署流程。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>CI 管理介面暴露面過高。&lt;/li>
&lt;li>建置輸出完整性驗證不足。&lt;/li>
&lt;li>平台事件與下游部署凍結流程連動不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「CI 平台事件觸發部署凍結」步驟，受污染 artifact 可能持續流向正式環境。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：建立 CI 管理入口隔離與最小存取權限（內網 only、強制 MFA），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。&lt;/li>
&lt;li>日常：對 build pipeline 建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert&lt;/a>（unauthorized config change、異常 build trigger）。&lt;/li>
&lt;li>事故中：暫停發佈、驗證 artifact、按優先級恢復（前提是 artifact 有 build provenance、可追溯產生時間）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> —— 把樣式轉成 CI 凍結演練、漏洞處理欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的 CI/CD 信任鏈、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的凍結與回復流程。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.jetbrains.com/teamcity/2023/09/cve-2023-42793-vulnerability-post-mortem">blog.jetbrains.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補時序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-347a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-42793">nvd.nist.gov/CVE-2023-42793&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、auth bypass 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>TeamCity CVE-2023-42793 事件顯示 CI 平台入口漏洞會直接衝擊建置與交付信任鏈。</p>
<p><strong>本案例的演示焦點</strong>：CI 管理介面 auth bypass → build / artifact 接管 → 下游交付污染的 CI-entrypoint supply-chain 鏈。跟 2024 雙漏洞事件互補、共同說明 CI 平台暴露面治理。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描並利用 TeamCity 管理入口漏洞。</li>
<li>取得 CI 管理權限或執行能力。</li>
<li>影響 build artifact 與下游部署流程。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>CI 管理介面暴露面過高。</li>
<li>建置輸出完整性驗證不足。</li>
<li>平台事件與下游部署凍結流程連動不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「CI 平台事件觸發部署凍結」步驟，受污染 artifact 可能持續流向正式環境。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：建立 CI 管理入口隔離與最小存取權限（內網 only、強制 MFA），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。</li>
<li>日常：對 build pipeline 建立 <a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a>（unauthorized config change、異常 build trigger）。</li>
<li>事故中：暫停發佈、驗證 artifact、按優先級恢復（前提是 artifact 有 build provenance、可追溯產生時間）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> + <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> —— 把樣式轉成 CI 凍結演練、漏洞處理欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的 CI/CD 信任鏈、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的凍結與回復流程。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://blog.jetbrains.com/teamcity/2023/09/cve-2023-42793-vulnerability-post-mortem">blog.jetbrains.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補時序</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-347a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-42793">nvd.nist.gov/CVE-2023-42793</a></td>
          <td>技術分析</td>
          <td>CVE 細節、auth bypass 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.6 ScreenConnect 2024：RMM 平台入口與下游擴散</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/screenconnect-cve-2024-1709-rmm-entrypoint/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/screenconnect-cve-2024-1709-rmm-entrypoint/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>ConnectWise ScreenConnect CVE-2024-1709 事件突顯 RMM 平台事件會沿著維運供應鏈向多客戶擴散。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：RMM 平台 auth bypass → 維運通道接管 → 客戶環境同時受影響的 fan-out 模式。跟 Kaseya 類似但 entrypoint 是 web admin 認證繞過、屬 entrypoint-side supply-chain 變體。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>利用 RMM 平台漏洞取得管理能力。&lt;/li>
&lt;li>接觸維運節點與客戶端連線能力。&lt;/li>
&lt;li>透過既有信任路徑擴大影響範圍。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>RMM 節點權限集中且邊界分離不足。&lt;/li>
&lt;li>客戶環境缺少獨立限制與跳板治理。&lt;/li>
&lt;li>事件時的連線停用與重授權流程準備度不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「RMM 事件後連線總開關」步驟，攻擊者可沿既有管理通道持續操作下游資產。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：將 RMM 權限與客戶域做嚴格分域（管理介面強制 MFA + 來源限制、客戶側端點不直連管理平面），mechanism 是讓平台漏洞不直接通到客戶資產。&lt;/li>
&lt;li>日常：建立遠端管理連線基線與稽核節奏（哪個 operator 在哪個時段對哪個客戶進行哪類操作的 audit trail）。&lt;/li>
&lt;li>事故中：先關閉高風險連線、再分批重啟授權（前提是事先有「總開關」設計、不臨時找)。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a> —— 把樣式轉成漏洞處理、跨組織 owner 分工。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的跨團隊升級路由、&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的維運平台治理。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.connectwise.com/company/trust/security-bulletins/connectwise-screenconnect-23.9.8">connectwise.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2024/02/22/cisa-adds-one-known-exploited-connectwise-vulnerability-cve-2024-1709-catalog">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-1709">nvd.nist.gov/CVE-2024-1709&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、auth bypass 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>ConnectWise ScreenConnect CVE-2024-1709 事件突顯 RMM 平台事件會沿著維運供應鏈向多客戶擴散。</p>
<p><strong>本案例的演示焦點</strong>：RMM 平台 auth bypass → 維運通道接管 → 客戶環境同時受影響的 fan-out 模式。跟 Kaseya 類似但 entrypoint 是 web admin 認證繞過、屬 entrypoint-side supply-chain 變體。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>利用 RMM 平台漏洞取得管理能力。</li>
<li>接觸維運節點與客戶端連線能力。</li>
<li>透過既有信任路徑擴大影響範圍。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>RMM 節點權限集中且邊界分離不足。</li>
<li>客戶環境缺少獨立限制與跳板治理。</li>
<li>事件時的連線停用與重授權流程準備度不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「RMM 事件後連線總開關」步驟，攻擊者可沿既有管理通道持續操作下游資產。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：將 RMM 權限與客戶域做嚴格分域（管理介面強制 MFA + 來源限制、客戶側端點不直連管理平面），mechanism 是讓平台漏洞不直接通到客戶資產。</li>
<li>日常：建立遠端管理連線基線與稽核節奏（哪個 operator 在哪個時段對哪個客戶進行哪類操作的 audit trail）。</li>
<li>事故中：先關閉高風險連線、再分批重啟授權（前提是事先有「總開關」設計、不臨時找)。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a> —— 把樣式轉成漏洞處理、跨組織 owner 分工。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的跨團隊升級路由、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的維運平台治理。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.connectwise.com/company/trust/security-bulletins/connectwise-screenconnect-23.9.8">connectwise.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2024/02/22/cisa-adds-one-known-exploited-connectwise-vulnerability-cve-2024-1709-catalog">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-1709">nvd.nist.gov/CVE-2024-1709</a></td>
          <td>技術分析</td>
          <td>CVE 細節、auth bypass 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.7 Log4Shell 2021：共用元件風險與修補鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/log4shell-cve-2021-44228-component-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Log4Shell 事件說明共用元件漏洞可在短時間內跨服務擴散，形成大規模修補與驗證壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：共用元件 zero-day → 跨服務同時暴露 → SBOM / 依賴 inventory 緊急檢索 → 大規模分批修補的 transitive-dependency 危機。重點在依賴可見性與分批修補節奏。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者偵測含漏洞元件的可達服務。&lt;/li>
&lt;li>透過日誌處理路徑觸發遠端執行。&lt;/li>
&lt;li>沿著相依資產清單擴大利用範圍。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>相依套件盤點與版本可見性不足。&lt;/li>
&lt;li>修補節奏缺少業務優先級路由。&lt;/li>
&lt;li>修補完成後驗證流程覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補後主動復測」步驟，團隊會把版本更新等同風險關閉，留下可利用殘餘面。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：維護關鍵套件 SBOM 與影響面對照（含 transitive 依賴、不只直接 import），mechanism 是讓事件期間能在分鐘級回答「我們有沒有在用」。&lt;/li>
&lt;li>日常：對高風險元件建立固定巡檢節奏（component criticality + reachability 分層、不全面平均掃）。&lt;/li>
&lt;li>事故中：按服務層級修補並追蹤 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>（前提是修補後有 functional + security 雙重 verify、不只 build pass）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> —— 把樣式轉成 SBOM 演練、漏洞分批處理欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的依賴治理與版本策略、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability&lt;/a> 的回復與驗證節奏。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://logging.apache.org/log4j/2.x/security.html">logging.apache.org&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、修補節奏、緩解 / patch 區別&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/news/apache-log4j-vulnerability-guidance-0">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨機構處置建議、scanning 工具清單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44228">nvd.nist.gov/CVE-2021-44228&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、JNDI lookup 利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Log4Shell 事件說明共用元件漏洞可在短時間內跨服務擴散，形成大規模修補與驗證壓力。</p>
<p><strong>本案例的演示焦點</strong>：共用元件 zero-day → 跨服務同時暴露 → SBOM / 依賴 inventory 緊急檢索 → 大規模分批修補的 transitive-dependency 危機。重點在依賴可見性與分批修補節奏。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者偵測含漏洞元件的可達服務。</li>
<li>透過日誌處理路徑觸發遠端執行。</li>
<li>沿著相依資產清單擴大利用範圍。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>相依套件盤點與版本可見性不足。</li>
<li>修補節奏缺少業務優先級路由。</li>
<li>修補完成後驗證流程覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補後主動復測」步驟，團隊會把版本更新等同風險關閉，留下可利用殘餘面。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：維護關鍵套件 SBOM 與影響面對照（含 transitive 依賴、不只直接 import），mechanism 是讓事件期間能在分鐘級回答「我們有沒有在用」。</li>
<li>日常：對高風險元件建立固定巡檢節奏（component criticality + reachability 分層、不全面平均掃）。</li>
<li>事故中：按服務層級修補並追蹤 <a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a>（前提是修補後有 functional + security 雙重 verify、不只 build pass）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> —— 把樣式轉成 SBOM 演練、漏洞分批處理欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的依賴治理與版本策略、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability</a> 的回復與驗證節奏。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://logging.apache.org/log4j/2.x/security.html">logging.apache.org</a></td>
          <td>官方</td>
          <td>受影響版本、修補節奏、緩解 / patch 區別</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/news/apache-log4j-vulnerability-guidance-0">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨機構處置建議、scanning 工具清單</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44228">nvd.nist.gov/CVE-2021-44228</a></td>
          <td>技術分析</td>
          <td>CVE 細節、JNDI lookup 利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.8 3CX 2023：桌面軟體更新鏈攻擊</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/3cx-2023-desktopapp-supply-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>3CX 2023 事件展示桌面軟體更新鏈受攻擊後，企業端點會同步暴露於供應鏈風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：桌面應用更新管道被植入 → 企業端點受信任安裝 → 端點成為後續控制節點的 build / release pipeline 上游 compromise。屬於跨平台桌面更新鏈類別、跟 server-side artifact 攻擊鏈互補。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者污染桌面應用程式交付流程。&lt;/li>
&lt;li>受影響版本進入企業端點。&lt;/li>
&lt;li>端點成為後續滲透與控制節點。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>更新來源信任缺少多重驗證。&lt;/li>
&lt;li>端點行為異常檢測與更新事件未連動。&lt;/li>
&lt;li>事件時版本凍結與替代方案準備不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「供應鏈事件即凍結更新版本」步驟，受影響版本仍會在內部持續擴散。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：建立更新簽章與來源完整性檢查（簽章鏈到 build provenance、不只發行者公鑰），mechanism 是讓「合法簽章」不等於「未被植入」。&lt;/li>
&lt;li>日常：將端點異常與更新事件關聯到同一告警流程（受信任應用 spawn 異常 process / 異常網路 callback）。&lt;/li>
&lt;li>事故中：凍結版本、隔離端點、驗證恢復清單（前提是 endpoint inventory 可在事件期間快速 query 已安裝版本）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成演練與控制欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的交付鏈風險治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的隔離與恢復協作。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.3cx.com/blog/news/security-alert-update/">3cx.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、植入時間軸、官方修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2023/03/30/supply-chain-attack-against-3cxdesktopapp">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、檢測指引&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.sentinelone.com/blog/smoothoperator-ongoing-campaign-trojanizes-3cxdesktopapp-in-a-supply-chain-attack/">sentinelone.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>SmoothOperator campaign TTP、後門行為特徵 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>3CX 2023 事件展示桌面軟體更新鏈受攻擊後，企業端點會同步暴露於供應鏈風險。</p>
<p><strong>本案例的演示焦點</strong>：桌面應用更新管道被植入 → 企業端點受信任安裝 → 端點成為後續控制節點的 build / release pipeline 上游 compromise。屬於跨平台桌面更新鏈類別、跟 server-side artifact 攻擊鏈互補。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者污染桌面應用程式交付流程。</li>
<li>受影響版本進入企業端點。</li>
<li>端點成為後續滲透與控制節點。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>更新來源信任缺少多重驗證。</li>
<li>端點行為異常檢測與更新事件未連動。</li>
<li>事件時版本凍結與替代方案準備不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「供應鏈事件即凍結更新版本」步驟，受影響版本仍會在內部持續擴散。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：建立更新簽章與來源完整性檢查（簽章鏈到 build provenance、不只發行者公鑰），mechanism 是讓「合法簽章」不等於「未被植入」。</li>
<li>日常：將端點異常與更新事件關聯到同一告警流程（受信任應用 spawn 異常 process / 異常網路 callback）。</li>
<li>事故中：凍結版本、隔離端點、驗證恢復清單（前提是 endpoint inventory 可在事件期間快速 query 已安裝版本）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成演練與控制欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的交付鏈風險治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的隔離與恢復協作。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.3cx.com/blog/news/security-alert-update/">3cx.com</a></td>
          <td>官方</td>
          <td>受影響版本、植入時間軸、官方修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2023/03/30/supply-chain-attack-against-3cxdesktopapp">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、檢測指引</td>
      </tr>
      <tr>
          <td><a href="https://www.sentinelone.com/blog/smoothoperator-ongoing-campaign-trojanizes-3cxdesktopapp-in-a-supply-chain-attack/">sentinelone.com</a></td>
          <td>技術分析</td>
          <td>SmoothOperator campaign TTP、後門行為特徵 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.9 Kaseya VSA 2021：MSP 供應鏈擴散路徑</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/kaseya-vsa-2021-msp-ransomware-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Kaseya VSA 2021 事件指出 MSP 管理平台若失守，攻擊可沿著託管關係快速擴展到多個客戶環境。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：MSP / RMM 管理平面被入侵 → 透過自動化能力批次下發 → 多客戶同時感染的 fan-out 供應鏈擴散。重點在「管理平面權限範圍」與「客戶分域隔離」設計。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊管理平台入口。&lt;/li>
&lt;li>透過自動化管理能力下發惡意行為。&lt;/li>
&lt;li>連鎖影響多個下游客戶系統。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理平面與客戶環境隔離不足。&lt;/li>
&lt;li>自動化任務缺少高風險動作保護。&lt;/li>
&lt;li>多租戶事件協調流程準備不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「跨客戶分批隔離」步驟，事件會在同一時間窗內形成大規模連鎖衝擊。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：限制管理平面高風險任務範圍（破壞性動作要求 multi-party approval / 批次上限），mechanism 是讓單點接管不會立刻 fan-out 到所有客戶。&lt;/li>
&lt;li>日常：建立多租戶事件通知與處置模板（含跨時區、跨法域的客戶通報路由）。&lt;/li>
&lt;li>事故中：先分域隔離、再啟動客戶側回復計畫（前提是事先有客戶分組與隔離開關）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> —— 把樣式轉成多租戶演練、回復欄位與漏洞處理流程。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的跨組織通訊節奏、&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的多租戶部署治理。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://helpdesk.kaseya.com/hc/en-gb/articles/4403440684689">helpdesk.kaseya.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、修補時序、客戶通報節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa21-209a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、檢測指引、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-30116">nvd.nist.gov/CVE-2021-30116&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、authenticated bypass 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Kaseya VSA 2021 事件指出 MSP 管理平台若失守，攻擊可沿著託管關係快速擴展到多個客戶環境。</p>
<p><strong>本案例的演示焦點</strong>：MSP / RMM 管理平面被入侵 → 透過自動化能力批次下發 → 多客戶同時感染的 fan-out 供應鏈擴散。重點在「管理平面權限範圍」與「客戶分域隔離」設計。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊管理平台入口。</li>
<li>透過自動化管理能力下發惡意行為。</li>
<li>連鎖影響多個下游客戶系統。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理平面與客戶環境隔離不足。</li>
<li>自動化任務缺少高風險動作保護。</li>
<li>多租戶事件協調流程準備不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「跨客戶分批隔離」步驟，事件會在同一時間窗內形成大規模連鎖衝擊。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：限制管理平面高風險任務範圍（破壞性動作要求 multi-party approval / 批次上限），mechanism 是讓單點接管不會立刻 fan-out 到所有客戶。</li>
<li>日常：建立多租戶事件通知與處置模板（含跨時區、跨法域的客戶通報路由）。</li>
<li>事故中：先分域隔離、再啟動客戶側回復計畫（前提是事先有客戶分組與隔離開關）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> + <a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">7.5 工作負載身份與 federated trust</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> —— 把樣式轉成多租戶演練、回復欄位與漏洞處理流程。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的跨組織通訊節奏、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的多租戶部署治理。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://helpdesk.kaseya.com/hc/en-gb/articles/4403440684689">helpdesk.kaseya.com</a></td>
          <td>官方</td>
          <td>受影響版本、修補時序、客戶通報節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa21-209a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、檢測指引、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-30116">nvd.nist.gov/CVE-2021-30116</a></td>
          <td>技術分析</td>
          <td>CVE 細節、authenticated bypass 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.1 MOVEit 2023：外網檔案服務批量外送</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 5 到 6 月，MOVEit Transfer 事件顯示，對外檔案傳輸服務在漏洞公開後可被快速批量利用並造成資料外送。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描外網可達 MFT 入口。&lt;/li>
&lt;li>利用漏洞取得存取能力。&lt;/li>
&lt;li>蒐集與外送高價值資料。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>對外入口缺少最小暴露設計。&lt;/li>
&lt;li>漏洞修補與隔離流程慢於攻擊自動化。&lt;/li>
&lt;li>外送行為偵測粒度不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「漏洞公告即觸發入口隔離」流程，等待正式修補期間仍會被持續掃描與利用。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：對外服務建立即時隔離開關。&lt;/li>
&lt;li>日常：監控大批量匯出與異常下載模式。&lt;/li>
&lt;li>事故中：先隔離入口，再做修補與回復。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.progress.com/trust-center/moveit-transfer-and-moveit-cloud-vulnerability">progress.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-34362">nvd.nist.gov/CVE-2023-34362&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 5 到 6 月，MOVEit Transfer 事件顯示，對外檔案傳輸服務在漏洞公開後可被快速批量利用並造成資料外送。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描外網可達 MFT 入口。</li>
<li>利用漏洞取得存取能力。</li>
<li>蒐集與外送高價值資料。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>對外入口缺少最小暴露設計。</li>
<li>漏洞修補與隔離流程慢於攻擊自動化。</li>
<li>外送行為偵測粒度不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「漏洞公告即觸發入口隔離」流程，等待正式修補期間仍會被持續掃描與利用。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：對外服務建立即時隔離開關。</li>
<li>日常：監控大批量匯出與異常下載模式。</li>
<li>事故中：先隔離入口，再做修補與回復。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.progress.com/trust-center/moveit-transfer-and-moveit-cloud-vulnerability">progress.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-34362">nvd.nist.gov/CVE-2023-34362</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2024 年初，Ivanti Connect Secure 相關公告顯示攻擊者可串接 CVE-2023-46805 與 CVE-2024-21887 進行認證繞過與遠端執行，並帶來持久化風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-46805 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描可達 VPN 邊界。&lt;/li>
&lt;li>利用漏洞鏈取得初始控制。&lt;/li>
&lt;li>建立持續存取與後續移動路徑。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>邊界設備長期暴露且承載關鍵流量。&lt;/li>
&lt;li>修補後狀態驗證流程不足。&lt;/li>
&lt;li>清除與重建步驟缺少標準化程序。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「修補後完整驗證」步驟，系統可能在已修補狀態下仍保留可利用持久化痕跡。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：高風險邊界設備準備替代路徑。&lt;/li>
&lt;li>日常：建立邊界設備健康與變更基線。&lt;/li>
&lt;li>事故中：執行隔離、重建、憑證輪替三段流程。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.ivanti.com/blog/security-update-for-ivanti-connect-secure-and-ivanti-policy-secure-gateways">ivanti.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-060b">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-46805">nvd.nist.gov/CVE-2023-46805&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21887">nvd.nist.gov/CVE-2024-21887&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2024 年初，Ivanti Connect Secure 相關公告顯示攻擊者可串接 CVE-2023-46805 與 CVE-2024-21887 進行認證繞過與遠端執行，並帶來持久化風險。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-46805 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描可達 VPN 邊界。</li>
<li>利用漏洞鏈取得初始控制。</li>
<li>建立持續存取與後續移動路徑。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>邊界設備長期暴露且承載關鍵流量。</li>
<li>修補後狀態驗證流程不足。</li>
<li>清除與重建步驟缺少標準化程序。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「修補後完整驗證」步驟，系統可能在已修補狀態下仍保留可利用持久化痕跡。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：高風險邊界設備準備替代路徑。</li>
<li>日常：建立邊界設備健康與變更基線。</li>
<li>事故中：執行隔離、重建、憑證輪替三段流程。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.ivanti.com/blog/security-update-for-ivanti-connect-secure-and-ivanti-policy-secure-gateways">ivanti.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-060b">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-46805">nvd.nist.gov/CVE-2023-46805</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21887">nvd.nist.gov/CVE-2024-21887</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.3 Citrix Bleed 2023：會話被劫持與重放風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-bleed-2023-session-hijack/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 Citrix Bleed（CVE-2023-4966）事件顯示，邊界設備漏洞可導致會話資訊外洩，後續引發重放與存取風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>利用邊界漏洞取得會話資料。&lt;/li>
&lt;li>重放或接管有效會話。&lt;/li>
&lt;li>以合法會話進入內部資源。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>會話機制缺少快速失效策略。&lt;/li>
&lt;li>邊界事件後憑證與會話輪替未即時執行。&lt;/li>
&lt;li>會話異常偵測與告警關聯不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「事件後全域 session/token 失效」步驟，攻擊者可在修補後持續使用已竊取會話。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：定義全域 session 失效與重發機制。&lt;/li>
&lt;li>日常：監控異常地理位置與設備指紋切換。&lt;/li>
&lt;li>事故中：修補、全域失效、強制重新登入同步執行。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.netscaler.com/blog/news/cve-2023-4966-critical-security-update-now-available-for-netscaler-adc-and-netscaler-gateway/">netscaler.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2023/11/07/cisa-releases-guidance-addressing-citrix-netscaler-adc-and-gateway-vulnerability-cve-2023-4966">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-4966">nvd.nist.gov/CVE-2023-4966&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 Citrix Bleed（CVE-2023-4966）事件顯示，邊界設備漏洞可導致會話資訊外洩，後續引發重放與存取風險。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>利用邊界漏洞取得會話資料。</li>
<li>重放或接管有效會話。</li>
<li>以合法會話進入內部資源。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>會話機制缺少快速失效策略。</li>
<li>邊界事件後憑證與會話輪替未即時執行。</li>
<li>會話異常偵測與告警關聯不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「事件後全域 session/token 失效」步驟，攻擊者可在修補後持續使用已竊取會話。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：定義全域 session 失效與重發機制。</li>
<li>日常：監控異常地理位置與設備指紋切換。</li>
<li>事故中：修補、全域失效、強制重新登入同步執行。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.netscaler.com/blog/news/cve-2023-4966-critical-security-update-now-available-for-netscaler-adc-and-netscaler-gateway/">netscaler.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2023/11/07/cisa-releases-guidance-addressing-citrix-netscaler-adc-and-gateway-vulnerability-cve-2023-4966">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-4966">nvd.nist.gov/CVE-2023-4966</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.4 PAN-OS 2024：邊界設備遠端命令執行</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/panos-cve-2024-3400-edge-rce/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2024 年 PAN-OS CVE-2024-3400 事件屬邊界設備高風險漏洞，對暴露在外的設備形成快速入侵壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描外網可達設備。&lt;/li>
&lt;li>觸發遠端執行能力。&lt;/li>
&lt;li>擴展到管理平面與內部網路路徑。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>邊界設備暴露面高且集中。&lt;/li>
&lt;li>修補窗口內缺少暫時緩解與替代路徑。&lt;/li>
&lt;li>攻擊偵測依賴單一訊號來源。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「修補前臨時緩解策略」，團隊會在可利用窗口內暴露完整攻擊面。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：關鍵邊界設備建立降級與備援計畫。&lt;/li>
&lt;li>日常：維護高風險資產清單與修補時限。&lt;/li>
&lt;li>事故中：先套用緩解、再分區修補與驗證。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://securityadvisories.paloaltonetworks.com/CVE-2024-3400">securityadvisories.paloaltonetworks.com/CVE-2024-3400&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-3400">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3400">nvd.nist.gov/CVE-2024-3400&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2024 年 PAN-OS CVE-2024-3400 事件屬邊界設備高風險漏洞，對暴露在外的設備形成快速入侵壓力。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描外網可達設備。</li>
<li>觸發遠端執行能力。</li>
<li>擴展到管理平面與內部網路路徑。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>邊界設備暴露面高且集中。</li>
<li>修補窗口內缺少暫時緩解與替代路徑。</li>
<li>攻擊偵測依賴單一訊號來源。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「修補前臨時緩解策略」，團隊會在可利用窗口內暴露完整攻擊面。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：關鍵邊界設備建立降級與備援計畫。</li>
<li>日常：維護高風險資產清單與修補時限。</li>
<li>事故中：先套用緩解、再分區修補與驗證。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://securityadvisories.paloaltonetworks.com/CVE-2024-3400">securityadvisories.paloaltonetworks.com/CVE-2024-3400</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-3400">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3400">nvd.nist.gov/CVE-2024-3400</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.5 PaperCut 2023：認證繞過與入口執行風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/papercut-cve-2023-27350-auth-bypass-rce/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/papercut-cve-2023-27350-auth-bypass-rce/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>PaperCut CVE-2023-27350 事件揭露管理入口的認證繞過會直接導向遠端執行與內網擴散風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描可達的 PaperCut 管理入口。&lt;/li>
&lt;li>利用認證繞過漏洞取得管理能力。&lt;/li>
&lt;li>透過服務節點進一步橫向探索。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理入口暴露面與網段隔離不足。&lt;/li>
&lt;li>入口異常行為檢測與告警門檻偏寬。&lt;/li>
&lt;li>修補後驗證與重建節奏不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「入口事件立即隔離」步驟，攻擊者可在修補前後窗口持續控制管理面。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：將管理面限制於受控網段與跳板。&lt;/li>
&lt;li>日常：為管理介面建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert&lt;/a>。&lt;/li>
&lt;li>事故中：隔離節點、完成修補、執行重測。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.papercut.com/kb/Main/PO-1216-and-PO-1219">papercut.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-27350">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-27350">nvd.nist.gov/CVE-2023-27350&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>PaperCut CVE-2023-27350 事件揭露管理入口的認證繞過會直接導向遠端執行與內網擴散風險。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描可達的 PaperCut 管理入口。</li>
<li>利用認證繞過漏洞取得管理能力。</li>
<li>透過服務節點進一步橫向探索。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理入口暴露面與網段隔離不足。</li>
<li>入口異常行為檢測與告警門檻偏寬。</li>
<li>修補後驗證與重建節奏不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「入口事件立即隔離」步驟，攻擊者可在修補前後窗口持續控制管理面。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：將管理面限制於受控網段與跳板。</li>
<li>日常：為管理介面建立 <a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a>。</li>
<li>事故中：隔離節點、完成修補、執行重測。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.papercut.com/kb/Main/PO-1216-and-PO-1219">papercut.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-27350">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-27350">nvd.nist.gov/CVE-2023-27350</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.6 Confluence 2022：網站入口 RCE 與知識系統風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-cve-2022-26134-ognl-rce/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-cve-2022-26134-ognl-rce/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Confluence CVE-2022-26134 事件顯示外網協作平台漏洞可快速形成遠端執行與資料線索外露風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描對外 Confluence 入口。&lt;/li>
&lt;li>利用 OGNL 注入取得命令執行。&lt;/li>
&lt;li>搜索內部文件與憑證線索以擴展攻擊。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>協作平台對外暴露面控制不足。&lt;/li>
&lt;li>入口服務修補與緩解同步節奏不足。&lt;/li>
&lt;li>平台資產分級與存取稽核覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補前立即緩解」步驟，攻擊者可在公告到修補完成之間持續利用。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把協作平台管理面與公開面分離。&lt;/li>
&lt;li>日常：維護高風險對外服務清單與修補 SLA。&lt;/li>
&lt;li>事故中：先緩解入口，再做修補、密碼收斂與證據保全。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://support.atlassian.com/atlassian-knowledge-base/kb/faq-for-cve-2022-26134/">support.atlassian.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2022/06/03/atlassian-releases-new-versions-confluence-server-and-data-center">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-26134">nvd.nist.gov/CVE-2022-26134&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Confluence CVE-2022-26134 事件顯示外網協作平台漏洞可快速形成遠端執行與資料線索外露風險。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描對外 Confluence 入口。</li>
<li>利用 OGNL 注入取得命令執行。</li>
<li>搜索內部文件與憑證線索以擴展攻擊。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>協作平台對外暴露面控制不足。</li>
<li>入口服務修補與緩解同步節奏不足。</li>
<li>平台資產分級與存取稽核覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補前立即緩解」步驟，攻擊者可在公告到修補完成之間持續利用。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把協作平台管理面與公開面分離。</li>
<li>日常：維護高風險對外服務清單與修補 SLA。</li>
<li>事故中：先緩解入口，再做修補、密碼收斂與證據保全。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://support.atlassian.com/atlassian-knowledge-base/kb/faq-for-cve-2022-26134/">support.atlassian.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2022/06/03/atlassian-releases-new-versions-confluence-server-and-data-center">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2022-26134">nvd.nist.gov/CVE-2022-26134</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.7 Cisco IOS XE 2023：Web UI 管理面風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/cisco-ios-xe-cve-2023-20198-webui-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/cisco-ios-xe-cve-2023-20198-webui-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Cisco IOS XE Web UI CVE-2023-20198 事件凸顯網路設備管理面一旦暴露，攻擊可快速取得高權限控制能力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描並辨識可達 Web UI 管理入口。&lt;/li>
&lt;li>利用漏洞建立未授權存取。&lt;/li>
&lt;li>取得設備控制能力並擴展網路影響。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理介面暴露於不必要的外網範圍。&lt;/li>
&lt;li>設備硬化與版本治理節奏不足。&lt;/li>
&lt;li>異常管理操作與配置變更稽核不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「管理平面異常即鎖定」步驟，設備控制權會長時間留在攻擊者手中。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把管理面封裝於專用管理網段。&lt;/li>
&lt;li>日常：定期稽核設定變更與登入來源。&lt;/li>
&lt;li>事故中：隔離設備、重建設定、驗證路由完整性。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-webui-privesc-j22SaA4z">sec.cloudapps.cisco.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2023/10/16/cisco-releases-security-advisory-ios-xe-software-web-ui">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-20198">nvd.nist.gov/CVE-2023-20198&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Cisco IOS XE Web UI CVE-2023-20198 事件凸顯網路設備管理面一旦暴露，攻擊可快速取得高權限控制能力。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描並辨識可達 Web UI 管理入口。</li>
<li>利用漏洞建立未授權存取。</li>
<li>取得設備控制能力並擴展網路影響。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理介面暴露於不必要的外網範圍。</li>
<li>設備硬化與版本治理節奏不足。</li>
<li>異常管理操作與配置變更稽核不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「管理平面異常即鎖定」步驟，設備控制權會長時間留在攻擊者手中。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把管理面封裝於專用管理網段。</li>
<li>日常：定期稽核設定變更與登入來源。</li>
<li>事故中：隔離設備、重建設定、驗證路由完整性。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-webui-privesc-j22SaA4z">sec.cloudapps.cisco.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2023/10/16/cisco-releases-security-advisory-ios-xe-software-web-ui">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-20198">nvd.nist.gov/CVE-2023-20198</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.8 Fortinet SSL-VPN 2024：邊界 VPN 高風險窗口</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-ssl-vpn-cve-2024-21762/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Fortinet CVE-2024-21762 事件顯示 VPN 邊界漏洞在實戰環境中具備快速利用壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描暴露的 SSL-VPN 入口。&lt;/li>
&lt;li>利用漏洞取得未授權執行能力。&lt;/li>
&lt;li>以邊界設備作為內網進入點。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>邊界 VPN 集中承載遠端存取流量。&lt;/li>
&lt;li>高風險漏洞公告後隔離節奏不足。&lt;/li>
&lt;li>修補完成後健康檢查覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補前臨時緩解」步驟，攻擊者可在短時間窗內持續命中邊界設備。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：準備 VPN 流量切換與替代通道。&lt;/li>
&lt;li>日常：維護高風險設備資產清單與補丁時限。&lt;/li>
&lt;li>事故中：先隔離入口，再進行分區修補與復測。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://fortiguard.com/psirt/FG-IR-24-015">fortiguard.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>資安廠商深度分析、IoC、利用樣態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-21762">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21762">nvd.nist.gov/CVE-2024-21762&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Fortinet CVE-2024-21762 事件顯示 VPN 邊界漏洞在實戰環境中具備快速利用壓力。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描暴露的 SSL-VPN 入口。</li>
<li>利用漏洞取得未授權執行能力。</li>
<li>以邊界設備作為內網進入點。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>邊界 VPN 集中承載遠端存取流量。</li>
<li>高風險漏洞公告後隔離節奏不足。</li>
<li>修補完成後健康檢查覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補前臨時緩解」步驟，攻擊者可在短時間窗內持續命中邊界設備。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：準備 VPN 流量切換與替代通道。</li>
<li>日常：維護高風險設備資產清單與補丁時限。</li>
<li>事故中：先隔離入口，再進行分區修補與復測。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://fortiguard.com/psirt/FG-IR-24-015">fortiguard.com</a></td>
          <td>技術分析</td>
          <td>資安廠商深度分析、IoC、利用樣態</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-21762">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21762">nvd.nist.gov/CVE-2024-21762</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.9 SysAid 2023：ITSM 入口與維運流程風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/sysaid-cve-2023-47246-itsm-entrypoint/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/sysaid-cve-2023-47246-itsm-entrypoint/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>SysAid CVE-2023-47246 事件指出 ITSM 平台入口漏洞可直接影響維運管理與企業內部流程。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描 ITSM 對外服務入口。&lt;/li>
&lt;li>利用漏洞取得管理層存取。&lt;/li>
&lt;li>接觸工單、資產或維運權限資訊。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>ITSM 管理面缺少網段隔離。&lt;/li>
&lt;li>工單與維運權限分離策略不足。&lt;/li>
&lt;li>入口事件與維運告警未形成閉環。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「ITSM 事件時工單權限收斂」步驟，攻擊者能利用維運流程長時間維持影響力。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把 ITSM 高權限動作拆成雙人核准。&lt;/li>
&lt;li>日常：追蹤異常管理操作與高風險工單。&lt;/li>
&lt;li>事故中：停用可疑管理帳號並重建權限。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.sysaid.com/blog/service-desk/on-premise-software-security-vulnerability-notification">sysaid.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-47246">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-47246">nvd.nist.gov/CVE-2023-47246&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>SysAid CVE-2023-47246 事件指出 ITSM 平台入口漏洞可直接影響維運管理與企業內部流程。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描 ITSM 對外服務入口。</li>
<li>利用漏洞取得管理層存取。</li>
<li>接觸工單、資產或維運權限資訊。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>ITSM 管理面缺少網段隔離。</li>
<li>工單與維運權限分離策略不足。</li>
<li>入口事件與維運告警未形成閉環。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「ITSM 事件時工單權限收斂」步驟，攻擊者能利用維運流程長時間維持影響力。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把 ITSM 高權限動作拆成雙人核准。</li>
<li>日常：追蹤異常管理操作與高風險工單。</li>
<li>事故中：停用可疑管理帳號並重建權限。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.sysaid.com/blog/service-desk/on-premise-software-security-vulnerability-notification">sysaid.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-47246">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-47246">nvd.nist.gov/CVE-2023-47246</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2022 年 LastPass 多次公告顯示，事件由開發環境路徑延伸到雲端備份資料存取，形成鏈式資料風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：開發環境 → 備份系統 → 加密保管庫的鏈式擴散，重點在「備份層 vs 正式環境層」的權限 / 金鑰隔離。其他 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>在上游環境取得關鍵資訊。&lt;/li>
&lt;li>使用關聯資訊打開備份存取路徑。&lt;/li>
&lt;li>造成長尾資料保護壓力。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>備份資產分級與隔離不足。&lt;/li>
&lt;li>金鑰管理與資料路徑治理耦合過高。&lt;/li>
&lt;li>備份讀取異常告警覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「備份層獨立權限審核」，事件即使起點在開發層，也能快速擴張到高敏感資料。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：備份與正式環境使用不同權限域（不同 IAM principal、不同 KMS key audience），mechanism 是讓正式環境的接管不直接通到備份。&lt;/li>
&lt;li>日常：定期審查備份讀取行為與授權範圍（哪些 principal 在哪些時段讀備份的 audit trail）。&lt;/li>
&lt;li>事故中：啟動備份層獨立調查與金鑰輪替（前提是備份金鑰跟正式金鑰是分離 lifecycle）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> —— 把樣式轉成 tabletop、credential 治理與備份回復欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend/01-database&lt;/a> 的備份與恢復設計。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於 post-compromise 鏈式擴散、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.lastpass.com/2022/08/notice-of-recent-security-incident/">blog.lastpass.com&lt;/a>&lt;/td>
 &lt;td>官方初報&lt;/td>
 &lt;td>開發環境入口、初步影響評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.lastpass.com/2022/11/notice-of-recent-security-incident/">blog.lastpass.com&lt;/a>&lt;/td>
 &lt;td>官方延伸&lt;/td>
 &lt;td>第二階段揭露、雲端備份存取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.lastpass.com/2022/12/notice-of-recent-security-incident/">blog.lastpass.com&lt;/a>&lt;/td>
 &lt;td>官方終報&lt;/td>
 &lt;td>完整影響範圍、客戶行動建議&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2022 年 LastPass 多次公告顯示，事件由開發環境路徑延伸到雲端備份資料存取，形成鏈式資料風險。</p>
<p><strong>本案例的演示焦點</strong>：開發環境 → 備份系統 → 加密保管庫的鏈式擴散，重點在「備份層 vs 正式環境層」的權限 / 金鑰隔離。其他 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>在上游環境取得關鍵資訊。</li>
<li>使用關聯資訊打開備份存取路徑。</li>
<li>造成長尾資料保護壓力。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>備份資產分級與隔離不足。</li>
<li>金鑰管理與資料路徑治理耦合過高。</li>
<li>備份讀取異常告警覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「備份層獨立權限審核」，事件即使起點在開發層，也能快速擴張到高敏感資料。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：備份與正式環境使用不同權限域（不同 IAM principal、不同 KMS key audience），mechanism 是讓正式環境的接管不直接通到備份。</li>
<li>日常：定期審查備份讀取行為與授權範圍（哪些 principal 在哪些時段讀備份的 audit trail）。</li>
<li>事故中：啟動備份層獨立調查與金鑰輪替（前提是備份金鑰跟正式金鑰是分離 lifecycle）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.8 secrets 與機器憑證治理</a> + <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a> + <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> —— 把樣式轉成 tabletop、credential 治理與備份回復欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend/01-database</a> 的備份與恢復設計。</li>
</ul>
<p>本案例屬於 post-compromise 鏈式擴散、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://blog.lastpass.com/2022/08/notice-of-recent-security-incident/">blog.lastpass.com</a></td>
          <td>官方初報</td>
          <td>開發環境入口、初步影響評估</td>
      </tr>
      <tr>
          <td><a href="https://blog.lastpass.com/2022/11/notice-of-recent-security-incident/">blog.lastpass.com</a></td>
          <td>官方延伸</td>
          <td>第二階段揭露、雲端備份存取</td>
      </tr>
      <tr>
          <td><a href="https://blog.lastpass.com/2022/12/notice-of-recent-security-incident/">blog.lastpass.com</a></td>
          <td>官方終報</td>
          <td>完整影響範圍、客戶行動建議</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2024 年公開資訊指出，攻擊者利用外洩憑證在部分 Snowflake 客戶環境進行資料竊取與勒索活動。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：infostealer 收集的憑證 → MFA / network policy 缺口 → 大量查詢 / 匯出的資料外送 chain。重點在「資料平台 access policy + 異常匯出偵測」設計、其他 threat surface 由其他 case category 承擔。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>收集可用憑證。&lt;/li>
&lt;li>針對 MFA 或存取政策薄弱環境登入。&lt;/li>
&lt;li>執行大量查詢與資料外送。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>身分基線未強制 MFA 與條件式存取。&lt;/li>
&lt;li>查詢行為異常偵測門檻不足。&lt;/li>
&lt;li>高價值資料匯出控制較弱。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「憑證事件後立即收斂存取政策」，攻擊者可在低噪音情況下持續外送資料。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：資料平台預設強制 MFA 與網路政策（network rule allowlist / 條件式存取），mechanism 是讓 leaked credential 即使有效也碰不到資料平台。&lt;/li>
&lt;li>日常：建立異常查詢與匯出告警（query 體積 / 來源 IP / 跨 schema scan 模式）。&lt;/li>
&lt;li>事故中：分批停用可疑憑證、限制外送並啟動調查（前提是事先有 credential inventory + 分批撤銷能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/" data-link-title="7.R11.10 批次操作濫用" data-link-desc="說明批次操作為何容易放大單次權限失效的影響半徑">批次操作濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/" data-link-title="7.R11.P8 匯出檔案長時間可重複下載" data-link-desc="說明匯出產物長時效與可重複下載如何放大資料外送風險">Long-lived repeatable export artifact&lt;/a> —— 把 leaked credential → bulk export 的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> —— 把樣式轉成 tabletop 與 release gate 欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://www.snowflake.com/en/blog/communication-on-recent-cyber-threat-activity/">snowflake.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊入口、影響範圍、客戶側建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-customers-take-steps-prevent-unauthorized-access">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC5537 TTP、infostealer 來源、勒索鏈 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2024 年公開資訊指出，攻擊者利用外洩憑證在部分 Snowflake 客戶環境進行資料竊取與勒索活動。</p>
<p><strong>本案例的演示焦點</strong>：infostealer 收集的憑證 → MFA / network policy 缺口 → 大量查詢 / 匯出的資料外送 chain。重點在「資料平台 access policy + 異常匯出偵測」設計、其他 threat surface 由其他 case category 承擔。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>收集可用憑證。</li>
<li>針對 MFA 或存取政策薄弱環境登入。</li>
<li>執行大量查詢與資料外送。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>身分基線未強制 MFA 與條件式存取。</li>
<li>查詢行為異常偵測門檻不足。</li>
<li>高價值資料匯出控制較弱。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「憑證事件後立即收斂存取政策」，攻擊者可在低噪音情況下持續外送資料。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：資料平台預設強制 MFA 與網路政策（network rule allowlist / 條件式存取），mechanism 是讓 leaked credential 即使有效也碰不到資料平台。</li>
<li>日常：建立異常查詢與匯出告警（query 體積 / 來源 IP / 跨 schema scan 模式）。</li>
<li>事故中：分批停用可疑憑證、限制外送並啟動調查（前提是事先有 credential inventory + 分批撤銷能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/bulk-operation-abuse/" data-link-title="7.R11.10 批次操作濫用" data-link-desc="說明批次操作為何容易放大單次權限失效的影響半徑">批次操作濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/fp-long-lived-repeatable-export-artifact/" data-link-title="7.R11.P8 匯出檔案長時間可重複下載" data-link-desc="說明匯出產物長時效與可重複下載如何放大資料外送風險">Long-lived repeatable export artifact</a> —— 把 leaked credential → bulk export 的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> —— 把樣式轉成 tabletop 與 release gate 欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.snowflake.com/en/blog/communication-on-recent-cyber-threat-activity/">snowflake.com</a></td>
          <td>官方</td>
          <td>攻擊入口、影響範圍、客戶側建議</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-customers-take-steps-prevent-unauthorized-access">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC5537 TTP、infostealer 來源、勒索鏈 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2024 年 Change Healthcare 事件顯示，資安事件可同時造成資料風險與支付流程中斷，影響範圍跨越供應鏈與醫療營運。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：高集中度業務中樞被勒索 → 下游機構 / 現金流連鎖中斷的 data-incident-to-business-continuity 事件。重點在「資安處置」跟「業務連續性處置」分軌並行的 workflow 設計。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊核心服務入口。&lt;/li>
&lt;li>影響高集中度業務中樞。&lt;/li>
&lt;li>對下游機構與現金流造成連鎖效應。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>關鍵業務中樞集中度高。&lt;/li>
&lt;li>替代流程與手動回復路徑準備不足。&lt;/li>
&lt;li>安全事件與業務連續性計畫連結不夠緊密。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「事故中的業務連續性切換流程」，團隊會在技術修復之外承受長期營運中斷代價。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：定義核心流程的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>，mechanism 是讓「資料修復時間」跟「業務可接受中斷時間」明示對照、不藏在直覺。&lt;/li>
&lt;li>日常：演練核心交易路徑的降級方案（含手動 fallback / 替代供應商接手）。&lt;/li>
&lt;li>事故中：技術處置與業務處置分軌並行（前提是事先有 dual-track IC 角色、不臨時拉人）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成 tabletop、回復演練與證據欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability&lt;/a> 的可用性與備援設計、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的事故分級與跨部門通訊。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於 post-compromise 影響類別、不對應紅隊 problem-cards（後者集中於 access flow 失效），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.unitedhealthgroup.com/newsroom/2024/2024-04-22-uhg-updates-on-change-healthcare-cyberattack.html">unitedhealthgroup.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊時序、影響範圍、復原節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cms.gov/newsroom/press-releases/cms-statement-change-healthcare-cyberattack">cms.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>監管面回應、對下游醫療機構的影響評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.aha.org/cybersecurity/change-healthcare-cyberattack-updates">aha.org&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>醫療業界 ongoing impact tracking、業務連續性影響&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2024 年 Change Healthcare 事件顯示，資安事件可同時造成資料風險與支付流程中斷，影響範圍跨越供應鏈與醫療營運。</p>
<p><strong>本案例的演示焦點</strong>：高集中度業務中樞被勒索 → 下游機構 / 現金流連鎖中斷的 data-incident-to-business-continuity 事件。重點在「資安處置」跟「業務連續性處置」分軌並行的 workflow 設計。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊核心服務入口。</li>
<li>影響高集中度業務中樞。</li>
<li>對下游機構與現金流造成連鎖效應。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>關鍵業務中樞集中度高。</li>
<li>替代流程與手動回復路徑準備不足。</li>
<li>安全事件與業務連續性計畫連結不夠緊密。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「事故中的業務連續性切換流程」，團隊會在技術修復之外承受長期營運中斷代價。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：定義核心流程的 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> 與 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>，mechanism 是讓「資料修復時間」跟「業務可接受中斷時間」明示對照、不藏在直覺。</li>
<li>日常：演練核心交易路徑的降級方案（含手動 fallback / 替代供應商接手）。</li>
<li>事故中：技術處置與業務處置分軌並行（前提是事先有 dual-track IC 角色、不臨時拉人）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈</a> + <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成 tabletop、回復演練與證據欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability</a> 的可用性與備援設計、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的事故分級與跨部門通訊。</li>
</ul>
<p>本案例屬於 post-compromise 影響類別、不對應紅隊 problem-cards（後者集中於 access flow 失效），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.unitedhealthgroup.com/newsroom/2024/2024-04-22-uhg-updates-on-change-healthcare-cyberattack.html">unitedhealthgroup.com</a></td>
          <td>官方</td>
          <td>攻擊時序、影響範圍、復原節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cms.gov/newsroom/press-releases/cms-statement-change-healthcare-cyberattack">cms.gov</a></td>
          <td>政府/監管</td>
          <td>監管面回應、對下游醫療機構的影響評估</td>
      </tr>
      <tr>
          <td><a href="https://www.aha.org/cybersecurity/change-healthcare-cyberattack-updates">aha.org</a></td>
          <td>技術分析</td>
          <td>醫療業界 ongoing impact tracking、業務連續性影響</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.4 Mailchimp 2023：支援工具路徑與客戶資料風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/mailchimp-2023-support-tool-abuse/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>2023 年 1 月，Mailchimp 公告指出攻擊者透過社交工程取得員工憑證，接觸客服/帳號管理工具並影響特定客戶帳號。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：員工社交工程 → 客服 / 帳號管理工具接管 → 客戶資料 read / 變更的 internal admin tool exfiltration。重點在「合法 admin 動作」跟「攻擊樣態」的偵測差異設計。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊員工身份。&lt;/li>
&lt;li>進入客服與帳號管理工具。&lt;/li>
&lt;li>存取或操作特定客戶資訊。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>客服工具高權限操作缺少額外防線。&lt;/li>
&lt;li>角色分離與操作稽核不夠完整。&lt;/li>
&lt;li>社交工程應對流程不夠制度化。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若缺少「高風險客服操作二次驗證」，攻擊者使用合法員工身份即可直接接觸高敏感客戶資產。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：對客服工具高風險操作加上雙人核准（access customer data / impersonate / 大批量 export 三類動作必須 multi-party），mechanism 是讓單一帳號接管不會直接通到客戶資料。&lt;/li>
&lt;li>日常：追蹤管理工具異常操作模式（單一 operator 短時間跨多 tenant、異常時段 access）。&lt;/li>
&lt;li>事故中：快速凍結可疑角色與工單操作權限（前提是事先有 role-level kill switch）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失效樣式&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用&lt;/a> —— 把員工身分 → 客服工具 → 客戶資料的 mechanism 抽象為可重用失效樣式。&lt;/li>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核軌跡與責任邊界&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern&lt;/a> —— 把樣式轉成 tabletop 與 admin tool 治理欄位。&lt;/li>
&lt;/ul>
&lt;h2 id="來源">來源&lt;/h2>
&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>&lt;a href="https://mailchimp.com/newsroom/january-2023-security-incident/">mailchimp.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>攻擊入口、影響範圍、客戶通報節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>跨組織 social engineering TTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>UNC3944 對 SaaS / admin tool 攻擊模式 telemetry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>2023 年 1 月，Mailchimp 公告指出攻擊者透過社交工程取得員工憑證，接觸客服/帳號管理工具並影響特定客戶帳號。</p>
<p><strong>本案例的演示焦點</strong>：員工社交工程 → 客服 / 帳號管理工具接管 → 客戶資料 read / 變更的 internal admin tool exfiltration。重點在「合法 admin 動作」跟「攻擊樣態」的偵測差異設計。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊員工身份。</li>
<li>進入客服與帳號管理工具。</li>
<li>存取或操作特定客戶資訊。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>客服工具高權限操作缺少額外防線。</li>
<li>角色分離與操作稽核不夠完整。</li>
<li>社交工程應對流程不夠制度化。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若缺少「高風險客服操作二次驗證」，攻擊者使用合法員工身份即可直接接觸高敏感客戶資產。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：對客服工具高風險操作加上雙人核准（access customer data / impersonate / 大批量 export 三類動作必須 multi-party），mechanism 是讓單一帳號接管不會直接通到客戶資料。</li>
<li>日常：追蹤管理工具異常操作模式（單一 operator 短時間跨多 tenant、異常時段 access）。</li>
<li>事故中：快速凍結可疑角色與工單操作權限（前提是事先有 role-level kill switch）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>失效樣式</strong>：<a href="/blog/backend/07-security-data-protection/red-team/problem-cards/privilege-escalation-flow-abuse/" data-link-title="7.R11.6 權限提升流程濫用" data-link-desc="說明權限提升流程為何容易把局部存取轉成全域控制">權限提升流程濫用</a> + <a href="/blog/backend/07-security-data-protection/red-team/problem-cards/delegated-operation-abuse/" data-link-title="7.R11.3 代理操作濫用" data-link-desc="說明代理操作為何容易形成責任鏈斷點與高權限濫用">委派操作濫用</a> —— 把員工身分 → 客服工具 → 客戶資料的 mechanism 抽象為可重用失效樣式。</li>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a> + <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 稽核軌跡與責任邊界</a> + <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/identity-support-token-tabletop/" data-link-title="Identity Support Token Tabletop" data-link-desc="以支援流程與 session token 風險設計身份接管 tabletop 情境">Identity support token tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/control-owner-pattern/" data-link-title="Control Owner Pattern" data-link-desc="定義高風險控制面如何配置 owner、協作角色、決策角色與升級路徑">Control owner pattern</a> —— 把樣式轉成 tabletop 與 admin tool 治理欄位。</li>
</ul>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://mailchimp.com/newsroom/january-2023-security-incident/">mailchimp.com</a></td>
          <td>官方</td>
          <td>攻擊入口、影響範圍、客戶通報節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>跨組織 social engineering TTP</td>
      </tr>
      <tr>
          <td><a href="https://cloud.google.com/blog/topics/threat-intelligence/unc3944-targets-saas-applications">cloud.google.com</a></td>
          <td>技術分析</td>
          <td>UNC3944 對 SaaS / admin tool 攻擊模式 telemetry</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>ESXiArgs 事件顯示 CVE-2021-21974 與 CVE-2021-21972 這類虛擬化平台漏洞可轉為大規模勒索與服務中斷，回復節奏成為關鍵控制面。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：虛擬化平台舊漏洞（patch-available 但未套用）→ ESXi host 加密 → 大量 VM 同時不可用的 mass-ransom 事件。重點在「回復節奏 vs 業務優先級」設計、exfiltration 本身是次要面向。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>利用已知 ESXi 漏洞取得主機控制能力。&lt;/li>
&lt;li>執行加密或破壞作業影響虛擬機。&lt;/li>
&lt;li>造成資料可用性與業務連續性衝擊。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>虛擬化平台修補節奏與資產可見性不足。&lt;/li>
&lt;li>快照、備份與復原演練覆蓋不足。&lt;/li>
&lt;li>事故中回復優先級路由不夠明確。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「回復優先級排序」步驟，團隊會在高壓情境下延長核心服務停擺時間。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：定義核心服務的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>（依業務重要性分層、不平均對待），mechanism 是讓事件期間的回復排序有預先決定的依據。&lt;/li>
&lt;li>日常：演練備份還原並記錄 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR&lt;/a>（含「整個 hypervisor fleet 同時離線」的壓力測試）。&lt;/li>
&lt;li>事故中：先恢復核心服務、再分批回補次要工作負載（前提是備份跟受影響 hypervisor 是 air-gap、不會同步加密）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成回復演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability&lt;/a> 的備援與回復策略、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的回復決策流程。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於 mass-ransom 事件、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.vmware.com/security/advisories/VMSA-2021-0002.html">vmware.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、修補節奏、緩解步驟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-040a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>大規模 ESXiArgs campaign 處置建議、recovery 工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-21972">nvd.nist.gov/CVE-2021-21972&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE-2021-21972 細節、unauthenticated RCE 機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-21974">nvd.nist.gov/CVE-2021-21974&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE-2021-21974 細節、SLP heap overflow 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>ESXiArgs 事件顯示 CVE-2021-21974 與 CVE-2021-21972 這類虛擬化平台漏洞可轉為大規模勒索與服務中斷，回復節奏成為關鍵控制面。</p>
<p><strong>本案例的演示焦點</strong>：虛擬化平台舊漏洞（patch-available 但未套用）→ ESXi host 加密 → 大量 VM 同時不可用的 mass-ransom 事件。重點在「回復節奏 vs 業務優先級」設計、exfiltration 本身是次要面向。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>利用已知 ESXi 漏洞取得主機控制能力。</li>
<li>執行加密或破壞作業影響虛擬機。</li>
<li>造成資料可用性與業務連續性衝擊。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>虛擬化平台修補節奏與資產可見性不足。</li>
<li>快照、備份與復原演練覆蓋不足。</li>
<li>事故中回復優先級路由不夠明確。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「回復優先級排序」步驟，團隊會在高壓情境下延長核心服務停擺時間。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：定義核心服務的 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a> 與 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>（依業務重要性分層、不平均對待），mechanism 是讓事件期間的回復排序有預先決定的依據。</li>
<li>日常：演練備份還原並記錄 <a href="/blog/backend/knowledge-cards/mttr/" data-link-title="MTTR" data-link-desc="說明平均修復時間如何作為事故處理能力指標">MTTR</a>（含「整個 hypervisor fleet 同時離線」的壓力測試）。</li>
<li>事故中：先恢復核心服務、再分批回補次要工作負載（前提是備份跟受影響 hypervisor 是 air-gap、不會同步加密）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈</a> + <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 安全事件路由</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成回復演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend/06-reliability</a> 的備援與回復策略、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的回復決策流程。</li>
</ul>
<p>本案例屬於 mass-ransom 事件、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.vmware.com/security/advisories/VMSA-2021-0002.html">vmware.com</a></td>
          <td>官方</td>
          <td>受影響版本、修補節奏、緩解步驟</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-040a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>大規模 ESXiArgs campaign 處置建議、recovery 工具</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-21972">nvd.nist.gov/CVE-2021-21972</a></td>
          <td>技術分析</td>
          <td>CVE-2021-21972 細節、unauthenticated RCE 機制</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-21974">nvd.nist.gov/CVE-2021-21974</a></td>
          <td>技術分析</td>
          <td>CVE-2021-21974 細節、SLP heap overflow 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>WS_FTP 2023 事件顯示對外檔案服務漏洞能快速變成資料外送事件，並帶來長尾調查壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：對外檔案服務 zero-day → 批量下載 → 資料外送的 file-server exfiltration。跟 GoAnywhere / MOVEit 共同形成 file-transfer 平台 systemic risk 視角，但 WS_FTP 屬中小企業 footprint、暴露面更分散。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描可達的 WS_FTP 服務。&lt;/li>
&lt;li>利用漏洞取得檔案存取能力。&lt;/li>
&lt;li>批量下載或外送高價值資料。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>對外檔案服務缺少最小暴露策略。&lt;/li>
&lt;li>檔案下載異常偵測覆蓋不足。&lt;/li>
&lt;li>事件時封鎖與保全流程節奏不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「異常外送即時封鎖」步驟，攻擊者可在同一窗口擴大資料外送規模。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：將檔案服務納入獨立網段與存取白名單（IP allowlist / VPN-fronted），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。&lt;/li>
&lt;li>日常：對大批量下載建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook&lt;/a>（單客戶 / 單 IP 短時間下載量級異常）。&lt;/li>
&lt;li>事故中：先封鎖外送路徑、再啟動調查與通知流程（前提是事先有 service-level cut-off 開關）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> —— 把樣式轉成 tabletop 與漏洞處理欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的通報與追蹤。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 zero-day 引發的外送、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.progress.com/trust-center/security-advisory/ws_ftp-server">progress.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-40044">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-40044">nvd.nist.gov/CVE-2023-40044&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、deserialization 利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>WS_FTP 2023 事件顯示對外檔案服務漏洞能快速變成資料外送事件，並帶來長尾調查壓力。</p>
<p><strong>本案例的演示焦點</strong>：對外檔案服務 zero-day → 批量下載 → 資料外送的 file-server exfiltration。跟 GoAnywhere / MOVEit 共同形成 file-transfer 平台 systemic risk 視角，但 WS_FTP 屬中小企業 footprint、暴露面更分散。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描可達的 WS_FTP 服務。</li>
<li>利用漏洞取得檔案存取能力。</li>
<li>批量下載或外送高價值資料。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>對外檔案服務缺少最小暴露策略。</li>
<li>檔案下載異常偵測覆蓋不足。</li>
<li>事件時封鎖與保全流程節奏不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「異常外送即時封鎖」步驟，攻擊者可在同一窗口擴大資料外送規模。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：將檔案服務納入獨立網段與存取白名單（IP allowlist / VPN-fronted），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。</li>
<li>日常：對大批量下載建立 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>（單客戶 / 單 IP 短時間下載量級異常）。</li>
<li>事故中：先封鎖外送路徑、再啟動調查與通知流程（前提是事先有 service-level cut-off 開關）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> —— 把樣式轉成 tabletop 與漏洞處理欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的通報與追蹤。</li>
</ul>
<p>本案例屬於邊界 zero-day 引發的外送、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.progress.com/trust-center/security-advisory/ws_ftp-server">progress.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-40044">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-40044">nvd.nist.gov/CVE-2023-40044</a></td>
          <td>技術分析</td>
          <td>CVE 細節、deserialization 利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>GoAnywhere MFT 2023 事件顯示檔案傳輸中樞在漏洞事件中會快速演變為資料外送與供應鏈通知壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：MFT 中樞 zero-day → 跨組織交換資料批量外送 → 多客戶通報壓力的 file-transfer hub exfiltration。跟 MOVEit 同類別、共同說明 MFT 平台暴露面的 systemic risk。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定可達 MFT 入口。&lt;/li>
&lt;li>利用漏洞取得傳輸系統存取能力。&lt;/li>
&lt;li>搜集並外送跨組織交換資料。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>傳輸中樞缺少入口隔離與最小授權。&lt;/li>
&lt;li>傳輸行為與資料分級未有效關聯。&lt;/li>
&lt;li>事件中跨組織通報流程準備不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「受影響交易清單快速盤點」步驟，團隊會延後通知與修復決策，擴大業務衝擊。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：為 MFT 流程建立資料分級與權限分域（依交易對象 / 資料敏感度切 audience），mechanism 是讓單點漏洞不會通到全部交換資料。&lt;/li>
&lt;li>日常：維護交易追蹤與外送告警指標（單窗口下載量 / 跨 partner 異常 access pattern）。&lt;/li>
&lt;li>事故中：盤點受影響交易、封鎖傳輸路徑、分層通知利害關係人（前提是事先有 partner contact map）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成 tabletop、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend/01-database&lt;/a> 的資料分級與治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的跨組織通報流程。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 zero-day 引發的外送、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.fortra.com/blog/summary-investigation-related-cve-2023-0669">fortra.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、修補時序、調查結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-0669">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-0669">nvd.nist.gov/CVE-2023-0669&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、deserialization RCE 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>GoAnywhere MFT 2023 事件顯示檔案傳輸中樞在漏洞事件中會快速演變為資料外送與供應鏈通知壓力。</p>
<p><strong>本案例的演示焦點</strong>：MFT 中樞 zero-day → 跨組織交換資料批量外送 → 多客戶通報壓力的 file-transfer hub exfiltration。跟 MOVEit 同類別、共同說明 MFT 平台暴露面的 systemic risk。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定可達 MFT 入口。</li>
<li>利用漏洞取得傳輸系統存取能力。</li>
<li>搜集並外送跨組織交換資料。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>傳輸中樞缺少入口隔離與最小授權。</li>
<li>傳輸行為與資料分級未有效關聯。</li>
<li>事件中跨組織通報流程準備不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「受影響交易清單快速盤點」步驟，團隊會延後通知與修復決策，擴大業務衝擊。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：為 MFT 流程建立資料分級與權限分域（依交易對象 / 資料敏感度切 audience），mechanism 是讓單點漏洞不會通到全部交換資料。</li>
<li>日常：維護交易追蹤與外送告警指標（單窗口下載量 / 跨 partner 異常 access pattern）。</li>
<li>事故中：盤點受影響交易、封鎖傳輸路徑、分層通知利害關係人（前提是事先有 partner contact map）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.9 資料保護與遮罩治理</a> + <a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.10 資料 residency / 刪除與證據鏈</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/low-frequency-exfiltration-tabletop/" data-link-title="Low-frequency Exfiltration Tabletop" data-link-desc="以受管檔案傳輸系統外送風險設計資料範圍與通報 tabletop">Low-frequency exfiltration tabletop</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成 tabletop、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend/01-database</a> 的資料分級與治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的跨組織通報流程。</li>
</ul>
<p>本案例屬於邊界 zero-day 引發的外送、不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.fortra.com/blog/summary-investigation-related-cve-2023-0669">fortra.com</a></td>
          <td>官方</td>
          <td>受影響版本、修補時序、調查結果</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-0669">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-0669">nvd.nist.gov/CVE-2023-0669</a></td>
          <td>技術分析</td>
          <td>CVE 細節、deserialization RCE 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>TeamCity 2024 事件顯示 CI 平台在認證繞過與路徑穿越漏洞同時存在時，交付鏈風險會被快速放大。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：CI 管理介面 auth bypass + path traversal 雙漏洞 → build pipeline 接管 → artifact 污染擴散下游。屬 CI-platform entrypoint 漏洞鏈、跟 SolarWinds 類 build-time 植入互補。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者鎖定可達 TeamCity 管理入口。&lt;/li>
&lt;li>利用 CVE-2024-27198 或 CVE-2024-27199 取得未授權能力。&lt;/li>
&lt;li>觸及 build 任務與 artifact 交付路徑。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>CI 管理介面隔離與存取控制不足。&lt;/li>
&lt;li>交付鏈完整性驗證節奏不足。&lt;/li>
&lt;li>事件時部署凍結與回退流程聯動不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「CI 事件即凍結交付」步驟，受影響 artifact 仍可能持續流向正式環境。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：管理入口採最小權限與網段隔離（VPN-only / 內網 only、不直接外網可達），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。&lt;/li>
&lt;li>日常：建立 build 異常行為與 pipeline 變更告警（unauthorized build trigger / 異常 build script 變更）。&lt;/li>
&lt;li>事故中：凍結部署、驗證 artifact、分批恢復發佈（前提是 artifact 有 provenance 可追溯 build 是否在事件窗口內產生）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成 CI 凍結演練、artifact 證據鏈。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的 CI/CD 交付信任鏈、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的凍結與回復節奏。&lt;/li>
&lt;/ul>
&lt;p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://blog.jetbrains.com/teamcity/2024/03/additional-critical-security-issues-affecting-teamcity-on-premises-cve-2024-27198-and-cve-2024-27199-update-to-2023-11-4-now">blog.jetbrains.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、雙漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-27198">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-27199">nvd.nist.gov/CVE-2024-27199&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、auth bypass + path traversal 機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>TeamCity 2024 事件顯示 CI 平台在認證繞過與路徑穿越漏洞同時存在時，交付鏈風險會被快速放大。</p>
<p><strong>本案例的演示焦點</strong>：CI 管理介面 auth bypass + path traversal 雙漏洞 → build pipeline 接管 → artifact 污染擴散下游。屬 CI-platform entrypoint 漏洞鏈、跟 SolarWinds 類 build-time 植入互補。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者鎖定可達 TeamCity 管理入口。</li>
<li>利用 CVE-2024-27198 或 CVE-2024-27199 取得未授權能力。</li>
<li>觸及 build 任務與 artifact 交付路徑。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>CI 管理介面隔離與存取控制不足。</li>
<li>交付鏈完整性驗證節奏不足。</li>
<li>事件時部署凍結與回退流程聯動不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「CI 事件即凍結交付」步驟，受影響 artifact 仍可能持續流向正式環境。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：管理入口採最小權限與網段隔離（VPN-only / 內網 only、不直接外網可達），mechanism 是讓 entrypoint 漏洞先碰到網段邊界。</li>
<li>日常：建立 build 異常行為與 pipeline 變更告警（unauthorized build trigger / 異常 build script 變更）。</li>
<li>事故中：凍結部署、驗證 artifact、分批恢復發佈（前提是 artifact 有 provenance 可追溯 build 是否在事件窗口內產生）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.6 供應鏈完整性與 artifact 信任</a> + <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/supply-chain-artifact-drill/" data-link-title="Supply Chain Artifact Drill" data-link-desc="以 artifact provenance 偏移設計供應鏈 release gate 與 rollback 演練">Supply chain artifact drill</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成 CI 凍結演練、artifact 證據鏈。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的 CI/CD 交付信任鏈、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的凍結與回復節奏。</li>
</ul>
<p>供應鏈類事故不對應紅隊 problem-cards，主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://blog.jetbrains.com/teamcity/2024/03/additional-critical-security-issues-affecting-teamcity-on-premises-cve-2024-27198-and-cve-2024-27199-update-to-2023-11-4-now">blog.jetbrains.com</a></td>
          <td>官方</td>
          <td>受影響版本、雙漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-27198">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-27199">nvd.nist.gov/CVE-2024-27199</a></td>
          <td>技術分析</td>
          <td>CVE 細節、auth bypass + path traversal 機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.10 Juniper 2023：網通設備鏈式漏洞窗口</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/juniper-cve-2023-36844-vpn-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/juniper-cve-2023-36844-vpn-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Juniper CVE-2023-36844 系列事件說明網通設備鏈式漏洞會同時帶來控制平面與營運穩定性壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>探測可達的設備服務面。&lt;/li>
&lt;li>串接漏洞取得更高控制權。&lt;/li>
&lt;li>對路由、連線與管理平面產生影響。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>設備風險分級與修補窗口管理不足。&lt;/li>
&lt;li>流量切換預案與維護時序不足。&lt;/li>
&lt;li>變更後驗證與回退流程定義不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「分區修補與流量切換」步驟，單次變更可能同時放大安全與可用性風險。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>發布前：建立設備分區與維護窗口策略。&lt;/li>
&lt;li>日常：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 驗證回退路徑可行性。&lt;/li>
&lt;li>事故中：依業務優先級分區修補並持續驗證。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://supportportal.juniper.net/JSA72300">supportportal.juniper.net&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-36844">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-36844">nvd.nist.gov/CVE-2023-36844&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Juniper CVE-2023-36844 系列事件說明網通設備鏈式漏洞會同時帶來控制平面與營運穩定性壓力。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>探測可達的設備服務面。</li>
<li>串接漏洞取得更高控制權。</li>
<li>對路由、連線與管理平面產生影響。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>設備風險分級與修補窗口管理不足。</li>
<li>流量切換預案與維護時序不足。</li>
<li>變更後驗證與回退流程定義不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「分區修補與流量切換」步驟，單次變更可能同時放大安全與可用性風險。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>發布前：建立設備分區與維護窗口策略。</li>
<li>日常：以 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 驗證回退路徑可行性。</li>
<li>事故中：依業務優先級分區修補並持續驗證。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://supportportal.juniper.net/JSA72300">supportportal.juniper.net</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-36844">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-36844">nvd.nist.gov/CVE-2023-36844</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.11 ServiceNow 2024：企業平台入口風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/servicenow-cve-2024-4879-enterprise-platform/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>ServiceNow CVE-2024-4879/5217 類事件提醒企業核心平台若出現入口風險，影響會直接跨到多部門流程。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定對外或可達平台入口。&lt;/li>
&lt;li>利用漏洞取得平台層能力。&lt;/li>
&lt;li>影響工單、流程自動化或資料操作。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>平台管理與業務流程耦合度高。&lt;/li>
&lt;li>高權限操作缺少額外防護步驟。&lt;/li>
&lt;li>平台事件時的流程降級機制不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「平台事件分級切換」步驟，團隊會同時承受安全處置與流程停滯壓力。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：關鍵平台操作建立雙層授權。&lt;/li>
&lt;li>日常：對高風險變更設置審核與回退標準。&lt;/li>
&lt;li>事故中：優先保護核心流程並分批恢復平台能力。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://support.servicenow.com/kb?id=kb_article_view&amp;amp;sysparm_article=KB1645154">support.servicenow.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-4879">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-4879">nvd.nist.gov/CVE-2024-4879&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>ServiceNow CVE-2024-4879/5217 類事件提醒企業核心平台若出現入口風險，影響會直接跨到多部門流程。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定對外或可達平台入口。</li>
<li>利用漏洞取得平台層能力。</li>
<li>影響工單、流程自動化或資料操作。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>平台管理與業務流程耦合度高。</li>
<li>高權限操作缺少額外防護步驟。</li>
<li>平台事件時的流程降級機制不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「平台事件分級切換」步驟，團隊會同時承受安全處置與流程停滯壓力。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：關鍵平台操作建立雙層授權。</li>
<li>日常：對高風險變更設置審核與回退標準。</li>
<li>事故中：優先保護核心流程並分批恢復平台能力。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://support.servicenow.com/kb?id=kb_article_view&amp;sysparm_article=KB1645154">support.servicenow.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-4879">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-4879">nvd.nist.gov/CVE-2024-4879</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.12 Check Point 2024：VPN 資訊外洩與會話風險</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/check-point-cve-2024-24919-vpn-info-disclosure/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/check-point-cve-2024-24919-vpn-info-disclosure/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Check Point CVE-2024-24919 事件顯示資訊外洩類漏洞在 VPN 邊界可直接放大身分與會話風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>針對可達 VPN 入口發動利用。&lt;/li>
&lt;li>擷取可用會話或認證相關資訊。&lt;/li>
&lt;li>轉換成未授權存取與橫向探索。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>VPN 邊界資訊保護與失效策略不足。&lt;/li>
&lt;li>會話生命週期管理與輪替機制不足。&lt;/li>
&lt;li>事件後整體收斂流程缺少標準化。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「全域會話失效」步驟，攻擊者可延長已取得存取的有效時間。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：將 VPN 會話管理納入失效與重發機制。&lt;/li>
&lt;li>日常：對會話異常地理來源建立告警。&lt;/li>
&lt;li>事故中：先做會話失效，再執行修補與重驗證。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://support.checkpoint.com/results/sk/sk182336">support.checkpoint.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-24919">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-24919">nvd.nist.gov/CVE-2024-24919&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Check Point CVE-2024-24919 事件顯示資訊外洩類漏洞在 VPN 邊界可直接放大身分與會話風險。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>針對可達 VPN 入口發動利用。</li>
<li>擷取可用會話或認證相關資訊。</li>
<li>轉換成未授權存取與橫向探索。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>VPN 邊界資訊保護與失效策略不足。</li>
<li>會話生命週期管理與輪替機制不足。</li>
<li>事件後整體收斂流程缺少標準化。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「全域會話失效」步驟，攻擊者可延長已取得存取的有效時間。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：將 VPN 會話管理納入失效與重發機制。</li>
<li>日常：對會話異常地理來源建立告警。</li>
<li>事故中：先做會話失效，再執行修補與重驗證。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://support.checkpoint.com/results/sk/sk182336">support.checkpoint.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-24919">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-24919">nvd.nist.gov/CVE-2024-24919</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.13 ProxyLogon 2021：CVE-2021-26855/27065 入口鏈式失效</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxylogon-2021-exchange-entry-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxylogon-2021-exchange-entry-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>ProxyLogon 事件顯示 CVE-2021-26855 與 CVE-2021-27065 這類企業郵件系統入口漏洞可被快速批量利用並形成內網風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2021-26855 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描 Exchange 對外入口。&lt;/li>
&lt;li>串接漏洞取得伺服器執行能力。&lt;/li>
&lt;li>植入 web shell 或建立持續控制。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>郵件入口暴露與修補時差偏大。&lt;/li>
&lt;li>漏洞利用跡象監控覆蓋不足。&lt;/li>
&lt;li>事件後清除與重建流程準備不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補後入侵痕跡清查」步驟，事件會在已更新版本上延續。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把郵件系統納入高風險資產修補路由。&lt;/li>
&lt;li>日常：追蹤異常 web shell 與命令執行行為。&lt;/li>
&lt;li>事故中：執行修補、清查、憑證輪替與重建驗證。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.microsoft.com/en-us/msrc/blog/2021/03/multiple-security-updates-released-for-exchange-server">microsoft.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa21-062a">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-26855">nvd.nist.gov/CVE-2021-26855&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-27065">nvd.nist.gov/CVE-2021-27065&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>ProxyLogon 事件顯示 CVE-2021-26855 與 CVE-2021-27065 這類企業郵件系統入口漏洞可被快速批量利用並形成內網風險。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2021-26855 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描 Exchange 對外入口。</li>
<li>串接漏洞取得伺服器執行能力。</li>
<li>植入 web shell 或建立持續控制。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>郵件入口暴露與修補時差偏大。</li>
<li>漏洞利用跡象監控覆蓋不足。</li>
<li>事件後清除與重建流程準備不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補後入侵痕跡清查」步驟，事件會在已更新版本上延續。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把郵件系統納入高風險資產修補路由。</li>
<li>日常：追蹤異常 web shell 與命令執行行為。</li>
<li>事故中：執行修補、清查、憑證輪替與重建驗證。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.microsoft.com/en-us/msrc/blog/2021/03/multiple-security-updates-released-for-exchange-server">microsoft.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa21-062a">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-26855">nvd.nist.gov/CVE-2021-26855</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-27065">nvd.nist.gov/CVE-2021-27065</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.14 ProxyShell 2021：CVE-2021-34473/34523/31207 後續鏈式攻擊</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxyshell-2021-exchange-post-auth-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/proxyshell-2021-exchange-post-auth-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>ProxyShell 事件延續了 Exchange 入口風險，顯示 CVE-2021-34473、CVE-2021-34523、CVE-2021-31207 這類多波漏洞會持續推高後續攻擊壓力。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2021-34473 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>利用 ProxyShell 鏈式漏洞取得存取。&lt;/li>
&lt;li>建立持續控制與資料探查能力。&lt;/li>
&lt;li>擴展到郵件與內部服務資產。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>同平台連續漏洞的追蹤治理不足。&lt;/li>
&lt;li>漏洞修補完成後的行為監控不足。&lt;/li>
&lt;li>事件後硬化與稽核節奏不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「波次事件重評估」步驟，團隊會以單次修補視角處理，留下後續利用窗口。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：建立同平台連續漏洞的專屬追蹤清單。&lt;/li>
&lt;li>日常：持續監控異常管理命令與資料下載行為。&lt;/li>
&lt;li>事故中：修補與風險重評估並行，直到驗證關閉。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://techcommunity.microsoft.com/blog/exchange/released-july-2021-exchange-server-security-updates/2523421">techcommunity.microsoft.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2021/08/21/urgent-protect-against-active-exploitation-proxyshell-vulnerabilities">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-34473">nvd.nist.gov/CVE-2021-34473&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-34523">nvd.nist.gov/CVE-2021-34523&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-31207">nvd.nist.gov/CVE-2021-31207&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>ProxyShell 事件延續了 Exchange 入口風險，顯示 CVE-2021-34473、CVE-2021-34523、CVE-2021-31207 這類多波漏洞會持續推高後續攻擊壓力。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2021-34473 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>利用 ProxyShell 鏈式漏洞取得存取。</li>
<li>建立持續控制與資料探查能力。</li>
<li>擴展到郵件與內部服務資產。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>同平台連續漏洞的追蹤治理不足。</li>
<li>漏洞修補完成後的行為監控不足。</li>
<li>事件後硬化與稽核節奏不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「波次事件重評估」步驟，團隊會以單次修補視角處理，留下後續利用窗口。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：建立同平台連續漏洞的專屬追蹤清單。</li>
<li>日常：持續監控異常管理命令與資料下載行為。</li>
<li>事故中：修補與風險重評估並行，直到驗證關閉。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://techcommunity.microsoft.com/blog/exchange/released-july-2021-exchange-server-security-updates/2523421">techcommunity.microsoft.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2021/08/21/urgent-protect-against-active-exploitation-proxyshell-vulnerabilities">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-34473">nvd.nist.gov/CVE-2021-34473</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-34523">nvd.nist.gov/CVE-2021-34523</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-31207">nvd.nist.gov/CVE-2021-31207</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.15 FortiOS 2022：VPN 零時差事件節奏</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortios-cve-2022-42475-vpn-zero-day/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortios-cve-2022-42475-vpn-zero-day/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>FortiOS CVE-2022-42475 事件顯示 VPN 零時差漏洞可讓攻擊者在短時間內取得邊界控制權。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定暴露於外網的 VPN 設備。&lt;/li>
&lt;li>利用零時差漏洞取得執行能力。&lt;/li>
&lt;li>建立持續存取與內網移動路徑。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>邊界設備資產盤點與優先順序不足。&lt;/li>
&lt;li>事件中憑證輪替與設備重建節奏不足。&lt;/li>
&lt;li>修補後狀態驗證與證據保存不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「事件後憑證全域輪替」步驟，已外露的認證素材會維持可用。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把關鍵 VPN 設備納入快速隔離策略。&lt;/li>
&lt;li>日常：對設備韌體版本與異常行為做固定巡檢。&lt;/li>
&lt;li>事故中：隔離、修補、輪替、重建四段並行。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.fortiguard.com/psirt/FG-IR-22-398">fortiguard.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>資安廠商深度分析、IoC、利用樣態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2022-42475">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-42475">nvd.nist.gov/CVE-2022-42475&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>FortiOS CVE-2022-42475 事件顯示 VPN 零時差漏洞可讓攻擊者在短時間內取得邊界控制權。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定暴露於外網的 VPN 設備。</li>
<li>利用零時差漏洞取得執行能力。</li>
<li>建立持續存取與內網移動路徑。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>邊界設備資產盤點與優先順序不足。</li>
<li>事件中憑證輪替與設備重建節奏不足。</li>
<li>修補後狀態驗證與證據保存不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「事件後憑證全域輪替」步驟，已外露的認證素材會維持可用。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把關鍵 VPN 設備納入快速隔離策略。</li>
<li>日常：對設備韌體版本與異常行為做固定巡檢。</li>
<li>事故中：隔離、修補、輪替、重建四段並行。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.fortiguard.com/psirt/FG-IR-22-398">fortiguard.com</a></td>
          <td>技術分析</td>
          <td>資安廠商深度分析、IoC、利用樣態</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2022-42475">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2022-42475">nvd.nist.gov/CVE-2022-42475</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.16 Citrix ADC 後續事件：Session 重放延伸</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-adc-2023-follow-on-session-risk/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Citrix 後續事件通報指出，漏洞修補後仍需處理 session 與憑證風險，才能完成真正關閉。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>利用邊界漏洞取得會話資訊。&lt;/li>
&lt;li>以重放方式維持未授權存取。&lt;/li>
&lt;li>在修補後窗口延續攻擊。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>修補流程與會話收斂流程分離。&lt;/li>
&lt;li>權杖失效策略執行覆蓋不足。&lt;/li>
&lt;li>事後追蹤指標沒有對準重放風險。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補後全域重新驗證登入」步驟，已竊取會話仍可能繼續被利用。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：定義漏洞事件後的 session 重發機制。&lt;/li>
&lt;li>日常：維護會話壽命與失效政策基線。&lt;/li>
&lt;li>事故中：修補、會話失效、登入重驗證三段同步。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://support.citrix.com/article/CTX579459">support.citrix.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/news-events/alerts/2023/11/07/cisa-releases-guidance-addressing-citrix-netscaler-adc-and-gateway-vulnerability-cve-2023-4966">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>受影響範圍、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-4966">nvd.nist.gov/CVE-2023-4966&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Citrix 後續事件通報指出，漏洞修補後仍需處理 session 與憑證風險，才能完成真正關閉。</p>
<p><strong>本案例的演示焦點</strong>：邊界 zero-day → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>利用邊界漏洞取得會話資訊。</li>
<li>以重放方式維持未授權存取。</li>
<li>在修補後窗口延續攻擊。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>修補流程與會話收斂流程分離。</li>
<li>權杖失效策略執行覆蓋不足。</li>
<li>事後追蹤指標沒有對準重放風險。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補後全域重新驗證登入」步驟，已竊取會話仍可能繼續被利用。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：定義漏洞事件後的 session 重發機制。</li>
<li>日常：維護會話壽命與失效政策基線。</li>
<li>事故中：修補、會話失效、登入重驗證三段同步。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://support.citrix.com/article/CTX579459">support.citrix.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/news-events/alerts/2023/11/07/cisa-releases-guidance-addressing-citrix-netscaler-adc-and-gateway-vulnerability-cve-2023-4966">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>受影響範圍、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-4966">nvd.nist.gov/CVE-2023-4966</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.17 Confluence 2023：CVE-2023-22515/22518 權限控制鏈</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/confluence-2023-cve-22515-22518-access-control-chain/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>Confluence 2023 連續漏洞事件顯示權限控制面一旦失效，協作平台會快速變成攻擊者的入口節點。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-22515 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者鎖定對外 Confluence 節點。&lt;/li>
&lt;li>利用 CVE-2023-22515 或 CVE-2023-22518 取得未授權存取能力。&lt;/li>
&lt;li>透過已取得權限接觸內部文件與後續線索。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>協作平台權限模型與網段隔離耦合不足。&lt;/li>
&lt;li>連續漏洞波次的修補節奏缺少統一追蹤。&lt;/li>
&lt;li>高風險入口事件與稽核流程連動不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「同平台連續漏洞重評估」步驟，團隊會用單點修補視角處理，留下後續利用窗口。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把 Confluence 納入高風險外網資產清單與修補 SLA。&lt;/li>
&lt;li>日常：建立協作平台異常管理行為告警。&lt;/li>
&lt;li>事故中：入口隔離、補丁套用、管理憑證收斂並行執行。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://confluence.atlassian.com/display/SECURITY/CVE-2023-22515%2B-%2BBroken%2BAccess%2BControl%2BVulnerability%2Bin%2BConfluence%2BData%2BCenter%2Band%2BServer">confluence.atlassian.com/CVE-2023-22515&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://confluence.atlassian.com/security/cve-2023-22518-improper-authorization-vulnerability-in-confluence-data-center-and-confluence-server-1311473907.html">confluence.atlassian.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-22515">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-22518">nvd.nist.gov/CVE-2023-22518&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>Confluence 2023 連續漏洞事件顯示權限控制面一旦失效，協作平台會快速變成攻擊者的入口節點。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-22515 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者鎖定對外 Confluence 節點。</li>
<li>利用 CVE-2023-22515 或 CVE-2023-22518 取得未授權存取能力。</li>
<li>透過已取得權限接觸內部文件與後續線索。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>協作平台權限模型與網段隔離耦合不足。</li>
<li>連續漏洞波次的修補節奏缺少統一追蹤。</li>
<li>高風險入口事件與稽核流程連動不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「同平台連續漏洞重評估」步驟，團隊會用單點修補視角處理，留下後續利用窗口。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把 Confluence 納入高風險外網資產清單與修補 SLA。</li>
<li>日常：建立協作平台異常管理行為告警。</li>
<li>事故中：入口隔離、補丁套用、管理憑證收斂並行執行。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://confluence.atlassian.com/display/SECURITY/CVE-2023-22515%2B-%2BBroken%2BAccess%2BControl%2BVulnerability%2Bin%2BConfluence%2BData%2BCenter%2Band%2BServer">confluence.atlassian.com/CVE-2023-22515</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://confluence.atlassian.com/security/cve-2023-22518-improper-authorization-vulnerability-in-confluence-data-center-and-confluence-server-1311473907.html">confluence.atlassian.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-22515">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-22518">nvd.nist.gov/CVE-2023-22518</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.18 Citrix 2023：CVE-2023-3519 邊界代碼注入</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/citrix-cve-2023-3519-code-injection/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2023-3519 事件顯示 NetScaler 這類邊界設備的代碼注入漏洞可迅速轉成控制平面風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-3519 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>探測暴露的 NetScaler 服務面。&lt;/li>
&lt;li>利用代碼注入漏洞取得執行能力。&lt;/li>
&lt;li>透過邊界節點延伸到內部網路路徑。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>邊界設備暴露面治理不足。&lt;/li>
&lt;li>修補窗口內缺少臨時緩解策略。&lt;/li>
&lt;li>修補後狀態驗證與稽核覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「修補前入口緩解」步驟，攻擊者可在公告窗口內持續利用邊界節點。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：把高風險邊界設備納入專用維護窗口。&lt;/li>
&lt;li>日常：持續盤點外網可達管理入口。&lt;/li>
&lt;li>事故中：先限縮入口，再分區修補與抽樣復測。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://support.citrix.com/external/article/CTX561482/citrix-adc-and-citrix-gateway-security-b.html">support.citrix.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-3519">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-3519">nvd.nist.gov/CVE-2023-3519&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2023-3519 事件顯示 NetScaler 這類邊界設備的代碼注入漏洞可迅速轉成控制平面風險。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-3519 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>探測暴露的 NetScaler 服務面。</li>
<li>利用代碼注入漏洞取得執行能力。</li>
<li>透過邊界節點延伸到內部網路路徑。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>邊界設備暴露面治理不足。</li>
<li>修補窗口內缺少臨時緩解策略。</li>
<li>修補後狀態驗證與稽核覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「修補前入口緩解」步驟，攻擊者可在公告窗口內持續利用邊界節點。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：把高風險邊界設備納入專用維護窗口。</li>
<li>日常：持續盤點外網可達管理入口。</li>
<li>事故中：先限縮入口，再分區修補與抽樣復測。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://support.citrix.com/external/article/CTX561482/citrix-adc-and-citrix-gateway-security-b.html">support.citrix.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-3519">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-3519">nvd.nist.gov/CVE-2023-3519</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2023-46747 事件指出 BIG-IP 組態工具一旦出現認證繞過，邊界治理會承受高壓。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-46747 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定可達 BIG-IP 管理入口。&lt;/li>
&lt;li>利用認證繞過取得管理能力。&lt;/li>
&lt;li>影響設備配置與流量控制策略。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理平面隔離策略覆蓋不足。&lt;/li>
&lt;li>設備設定變更的稽核強度不足。&lt;/li>
&lt;li>事件時的快速封鎖與回復路徑準備不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「管理平面緊急鎖定」步驟，攻擊者可利用高權限配置能力持續擴散影響。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：管理介面改為受控網段與跳板存取。&lt;/li>
&lt;li>日常：建立設備配置差異與異常變更告警。&lt;/li>
&lt;li>事故中：鎖定管理入口、收斂憑證、恢復最小可用設定。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://my.f5.com/manage/s/article/K000137353">my.f5.com&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-46747">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-46747">nvd.nist.gov/CVE-2023-46747&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2023-46747 事件指出 BIG-IP 組態工具一旦出現認證繞過，邊界治理會承受高壓。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-46747 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定可達 BIG-IP 管理入口。</li>
<li>利用認證繞過取得管理能力。</li>
<li>影響設備配置與流量控制策略。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理平面隔離策略覆蓋不足。</li>
<li>設備設定變更的稽核強度不足。</li>
<li>事件時的快速封鎖與回復路徑準備不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「管理平面緊急鎖定」步驟，攻擊者可利用高權限配置能力持續擴散影響。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：管理介面改為受控網段與跳板存取。</li>
<li>日常：建立設備配置差異與異常變更告警。</li>
<li>事故中：鎖定管理入口、收斂憑證、恢復最小可用設定。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://my.f5.com/manage/s/article/K000137353">my.f5.com</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-46747">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-46747">nvd.nist.gov/CVE-2023-46747</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.20 Fortinet 2022：CVE-2022-40684 認證繞過</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2022-40684-auth-bypass/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2022-40684 事件顯示 Fortinet 多產品在認證繞過情境下，邊界與管理面風險會同步上升。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2022-40684 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>掃描可達管理或邊界節點。&lt;/li>
&lt;li>利用認證繞過取得未授權管理能力。&lt;/li>
&lt;li>調整設備策略並擴大內網風險面。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理入口防護層次不足。&lt;/li>
&lt;li>高風險設備的修補節奏不一致。&lt;/li>
&lt;li>變更稽核與異常追蹤鏈路不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「設備配置完整性復核」步驟，修補完成後仍可能維持高風險配置狀態。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：對高權限管理面建立多層存取限制。&lt;/li>
&lt;li>日常：以固定節奏審核設備配置與管理帳號。&lt;/li>
&lt;li>事故中：修補、配置復核、憑證輪替同步執行。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.fortiguard.com/psirt/FG-IR-22-377">fortiguard.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>資安廠商深度分析、IoC、利用樣態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2022-40684">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-40684">nvd.nist.gov/CVE-2022-40684&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2022-40684 事件顯示 Fortinet 多產品在認證繞過情境下，邊界與管理面風險會同步上升。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2022-40684 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>掃描可達管理或邊界節點。</li>
<li>利用認證繞過取得未授權管理能力。</li>
<li>調整設備策略並擴大內網風險面。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理入口防護層次不足。</li>
<li>高風險設備的修補節奏不一致。</li>
<li>變更稽核與異常追蹤鏈路不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「設備配置完整性復核」步驟，修補完成後仍可能維持高風險配置狀態。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：對高權限管理面建立多層存取限制。</li>
<li>日常：以固定節奏審核設備配置與管理帳號。</li>
<li>事故中：修補、配置復核、憑證輪替同步執行。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.fortiguard.com/psirt/FG-IR-22-377">fortiguard.com</a></td>
          <td>技術分析</td>
          <td>資安廠商深度分析、IoC、利用樣態</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2022-40684">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2022-40684">nvd.nist.gov/CVE-2022-40684</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.21 Fortinet 2023：CVE-2023-27997 SSL-VPN 溢位</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/fortinet-cve-2023-27997-sslvpn-overflow/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2023-27997 事件顯示 SSL-VPN 漏洞在邊界設備上具備高利用效率與高傳播風險。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-27997 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>鎖定外網可達 SSL-VPN 節點。&lt;/li>
&lt;li>利用溢位漏洞取得執行或控制能力。&lt;/li>
&lt;li>沿著 VPN 通道進一步探索內部資產。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>外網暴露設備缺少即時緩解策略。&lt;/li>
&lt;li>高風險漏洞的修補優先級路由不足。&lt;/li>
&lt;li>事件後會話與憑證收斂速度不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「漏洞公告即資產分級處置」步驟，團隊會在關鍵窗口失去修補優先順序。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：建立 VPN 高風險資產清單與替代連線路由。&lt;/li>
&lt;li>日常：監控異常登入行為與會話模式變化。&lt;/li>
&lt;li>事故中：隔離節點、修補復測、全域會話失效並行。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.fortiguard.com/psirt/FG-IR-23-097">fortiguard.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>資安廠商深度分析、IoC、利用樣態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-27997">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-27997">nvd.nist.gov/CVE-2023-27997&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2023-27997 事件顯示 SSL-VPN 漏洞在邊界設備上具備高利用效率與高傳播風險。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-27997 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>鎖定外網可達 SSL-VPN 節點。</li>
<li>利用溢位漏洞取得執行或控制能力。</li>
<li>沿著 VPN 通道進一步探索內部資產。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>外網暴露設備缺少即時緩解策略。</li>
<li>高風險漏洞的修補優先級路由不足。</li>
<li>事件後會話與憑證收斂速度不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「漏洞公告即資產分級處置」步驟，團隊會在關鍵窗口失去修補優先順序。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：建立 VPN 高風險資產清單與替代連線路由。</li>
<li>日常：監控異常登入行為與會話模式變化。</li>
<li>事故中：隔離節點、修補復測、全域會話失效並行。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.fortiguard.com/psirt/FG-IR-23-097">fortiguard.com</a></td>
          <td>技術分析</td>
          <td>資安廠商深度分析、IoC、利用樣態</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-27997">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-27997">nvd.nist.gov/CVE-2023-27997</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.22 FortiClient EMS 2023：CVE-2023-48788 SQL 注入</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/forticlient-ems-cve-2023-48788-sqli/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/forticlient-ems-cve-2023-48788-sqli/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2023-48788 事件反映端點管理平台一旦受 SQL 注入影響，管理資料與權限邊界會同步承壓。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2023-48788 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>攻擊者定位可達 EMS 管理介面。&lt;/li>
&lt;li>利用 SQL 注入取得未授權資料或控制能力。&lt;/li>
&lt;li>進一步影響端點管理與政策下發流程。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>管理平台資料層保護不足。&lt;/li>
&lt;li>管理平面與業務平面隔離不足。&lt;/li>
&lt;li>高風險查詢與異常操作告警不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「管理平面異常資料操作收斂」步驟，攻擊者可延長停留並提高橫向擴散機率。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：對管理 API 與資料層建立最小暴露策略。&lt;/li>
&lt;li>日常：監控高風險查詢與管理變更異常。&lt;/li>
&lt;li>事故中：隔離管理節點、收斂權限、驗證資料完整性。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://fortiguard.com/psirt/FG-IR-24-007">fortiguard.com&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>資安廠商深度分析、IoC、利用樣態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-48788">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-48788">nvd.nist.gov/CVE-2023-48788&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2023-48788 事件反映端點管理平台一旦受 SQL 注入影響，管理資料與權限邊界會同步承壓。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2023-48788 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>攻擊者定位可達 EMS 管理介面。</li>
<li>利用 SQL 注入取得未授權資料或控制能力。</li>
<li>進一步影響端點管理與政策下發流程。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>管理平台資料層保護不足。</li>
<li>管理平面與業務平面隔離不足。</li>
<li>高風險查詢與異常操作告警不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「管理平面異常資料操作收斂」步驟，攻擊者可延長停留並提高橫向擴散機率。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：對管理 API 與資料層建立最小暴露策略。</li>
<li>日常：監控高風險查詢與管理變更異常。</li>
<li>事故中：隔離管理節點、收斂權限、驗證資料完整性。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://fortiguard.com/psirt/FG-IR-24-007">fortiguard.com</a></td>
          <td>技術分析</td>
          <td>資安廠商深度分析、IoC、利用樣態</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2023-48788">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-48788">nvd.nist.gov/CVE-2023-48788</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.23 ManageEngine 2021：CVE-2021-40539 認證繞過</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/manageengine-adself-cve-2021-40539-auth-bypass/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/manageengine-adself-cve-2021-40539-auth-bypass/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2021-40539 事件顯示身分管理服務的認證繞過漏洞可直接影響企業帳號與授權流程。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2021-40539 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>探測 ADSelfService Plus 入口。&lt;/li>
&lt;li>利用認證繞過取得管理能力。&lt;/li>
&lt;li>觸及帳號管理與身分資料流程。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>身分管理入口隔離不足。&lt;/li>
&lt;li>管理操作二次驗證與審批不足。&lt;/li>
&lt;li>身分平台事件與全域憑證輪替聯動不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「身分平台事件觸發全域收斂」步驟，攻擊者可利用管理能力放大影響面。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：身分管理入口改為專用網段與跳板策略。&lt;/li>
&lt;li>日常：高風險帳號動作納入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook&lt;/a>。&lt;/li>
&lt;li>事故中：封鎖入口、輪替憑證、審核高權限帳號變更。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.manageengine.com/products/self-service-password/advisory/CVE-2021-40539.html">manageengine.com/CVE-2021-40539&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-40539">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-40539">nvd.nist.gov/CVE-2021-40539&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2021-40539 事件顯示身分管理服務的認證繞過漏洞可直接影響企業帳號與授權流程。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2021-40539 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>探測 ADSelfService Plus 入口。</li>
<li>利用認證繞過取得管理能力。</li>
<li>觸及帳號管理與身分資料流程。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>身分管理入口隔離不足。</li>
<li>管理操作二次驗證與審批不足。</li>
<li>身分平台事件與全域憑證輪替聯動不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「身分平台事件觸發全域收斂」步驟，攻擊者可利用管理能力放大影響面。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：身分管理入口改為專用網段與跳板策略。</li>
<li>日常：高風險帳號動作納入 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>。</li>
<li>事故中：封鎖入口、輪替憑證、審核高權限帳號變更。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.manageengine.com/products/self-service-password/advisory/CVE-2021-40539.html">manageengine.com/CVE-2021-40539</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-40539">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-40539">nvd.nist.gov/CVE-2021-40539</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>7.R7.3.24 USAHERDS 2021：CVE-2021-44207 硬編碼憑證</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/</guid><description>&lt;h2 id="事故摘要">事故摘要&lt;/h2>
&lt;p>CVE-2021-44207 事件顯示硬編碼憑證一旦被識別，攻擊者可沿著固定認證路徑持續進入系統。&lt;/p>
&lt;p>&lt;strong>本案例的演示焦點&lt;/strong>：該CVE-2021-44207 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。&lt;/p>
&lt;h2 id="攻擊路徑">攻擊路徑&lt;/h2>
&lt;ol>
&lt;li>逆向或檢索系統中可重用憑證。&lt;/li>
&lt;li>利用硬編碼憑證取得入口存取能力。&lt;/li>
&lt;li>以固定認證模式維持長期可用存取。&lt;/li>
&lt;/ol>
&lt;h2 id="失效控制面">失效控制面&lt;/h2>
&lt;ul>
&lt;li>憑證生命週期治理缺少輪替與淘汰。&lt;/li>
&lt;li>應用程式配置審查未涵蓋硬編碼風險。&lt;/li>
&lt;li>憑證異常使用監控覆蓋不足。&lt;/li>
&lt;/ul>
&lt;h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼&lt;/h2>
&lt;p>若少了「憑證來源掃描與輪替」步驟，事件會在修補後保留同類風險模式。&lt;/p>
&lt;h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點&lt;/h2>
&lt;ul>
&lt;li>共同基線：以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 固定記錄觸發條件與處置節奏。&lt;/li>
&lt;li>發布前：建立配置掃描規則，攔截硬編碼憑證。&lt;/li>
&lt;li>日常：把憑證輪替與金鑰盤點納入固定排程。&lt;/li>
&lt;li>事故中：封鎖舊憑證、完成全域替換與歷史比對。&lt;/li>
&lt;li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。&lt;/li>
&lt;/ul>
&lt;h2 id="從本案例到實作的-chain">從本案例到實作的 chain&lt;/h2>
&lt;p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>控制面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理&lt;/a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。&lt;/li>
&lt;li>&lt;strong>演練 / 控制落地&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern&lt;/a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。&lt;/li>
&lt;li>&lt;strong>跨章交接&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform&lt;/a> 的邊界部署治理、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response&lt;/a> 的調查與回復步驟。&lt;/li>
&lt;/ul>
&lt;p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cve.org/CVERecord?id=CVE-2021-44207">cve.org&lt;/a>&lt;/td>
 &lt;td>官方&lt;/td>
 &lt;td>受影響版本、漏洞細節、修補節奏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-44207">cisa.gov&lt;/a>&lt;/td>
 &lt;td>政府/監管&lt;/td>
 &lt;td>KEV 列入、跨機構處置建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44207">nvd.nist.gov/CVE-2021-44207&lt;/a>&lt;/td>
 &lt;td>技術分析&lt;/td>
 &lt;td>CVE 細節、利用機制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="事故摘要">事故摘要</h2>
<p>CVE-2021-44207 事件顯示硬編碼憑證一旦被識別，攻擊者可沿著固定認證路徑持續進入系統。</p>
<p><strong>本案例的演示焦點</strong>：該CVE-2021-44207 → 邊界設備 / 對外應用入口接管 → 內部資源 / 會話 / 資料的橫向擴散。屬於 edge-exposure 類別、跟身分鏈接管 / 供應鏈植入 / 資料外送等其他 case category 形成互補視角。</p>
<h2 id="攻擊路徑">攻擊路徑</h2>
<ol>
<li>逆向或檢索系統中可重用憑證。</li>
<li>利用硬編碼憑證取得入口存取能力。</li>
<li>以固定認證模式維持長期可用存取。</li>
</ol>
<h2 id="失效控制面">失效控制面</h2>
<ul>
<li>憑證生命週期治理缺少輪替與淘汰。</li>
<li>應用程式配置審查未涵蓋硬編碼風險。</li>
<li>憑證異常使用監控覆蓋不足。</li>
</ul>
<h2 id="如果-workflow-少一步會發生什麼">如果 workflow 少一步會發生什麼</h2>
<p>若少了「憑證來源掃描與輪替」步驟，事件會在修補後保留同類風險模式。</p>
<h2 id="可落地的-workflow-檢查點">可落地的 workflow 檢查點</h2>
<ul>
<li>共同基線：以 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 固定記錄觸發條件與處置節奏。</li>
<li>發布前：建立配置掃描規則，攔截硬編碼憑證。</li>
<li>日常：把憑證輪替與金鑰盤點納入固定排程。</li>
<li>事故中：封鎖舊憑證、完成全域替換與歷史比對。</li>
<li>mechanism 總綱：邊界事件的核心是讓「漏洞修補」「會話 / 憑證失效」「異常痕跡清查」三件事同步發生、不分先後留下時間窗口（前提是事先有 inventory + 自動化失效 / 清查能力）。</li>
</ul>
<h2 id="從本案例到實作的-chain">從本案例到實作的 chain</h2>
<p>本案例是事故敘事 layer，沿三步 chain 進入 implementation：</p>
<ul>
<li><strong>控制面</strong>：<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口與伺服端保護</a> + <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.12 偵測涵蓋與訊號治理</a> —— mitigation 的 mechanism / 前提 / context-dependence 在這裡定義。</li>
<li><strong>演練 / 控制落地</strong>：<a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/edge-session-hijack-game-day/" data-link-title="Edge Session Hijack Game Day" data-link-desc="以入口設備 session disclosure 風險設計 edge exposure game day">Edge session hijack game day</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/vulnerability-response-pattern/" data-link-title="Vulnerability Response Pattern" data-link-desc="定義漏洞回應如何從 observed 推進到 assessed、mitigated、patched、validated 與 closed">Vulnerability response pattern</a> + <a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/evidence-chain-pattern/" data-link-title="Evidence Chain Pattern" data-link-desc="定義事故與演練需要保存的訊號、決策、artifact、timeline 與 retention 證據">Evidence chain pattern</a> —— 把樣式轉成邊界演練、漏洞處理與證據鏈欄位。</li>
<li><strong>跨章交接</strong>：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend/05-deployment-platform</a> 的邊界部署治理、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">backend/08-incident-response</a> 的調查與回復步驟。</li>
</ul>
<p>本案例屬於邊界 / 入口漏洞類別、不對應紅隊 problem-cards（後者集中於 tenant flow / identity flow），主要 chain 直接從控制面起步。</p>
<h2 id="來源">來源</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>類型</th>
          <th>可引用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.cve.org/CVERecord?id=CVE-2021-44207">cve.org</a></td>
          <td>官方</td>
          <td>受影響版本、漏洞細節、修補節奏</td>
      </tr>
      <tr>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-44207">cisa.gov</a></td>
          <td>政府/監管</td>
          <td>KEV 列入、跨機構處置建議</td>
      </tr>
      <tr>
          <td><a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44207">nvd.nist.gov/CVE-2021-44207</a></td>
          <td>技術分析</td>
          <td>CVE 細節、利用機制</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Firestore Distributed Counter Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/distributed-counter-lab/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/distributed-counter-lab/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線&lt;/a> 的 lab，實作 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入&lt;/a> deep article 的機制。前置環境見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">Local emulator quickstart&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>Firestore distributed counter lab 的核心責任是把「分片計數」從概念變成可觀察的寫入分佈與彙總結果。這個 lab 在 emulator 上建立 N 個 shard、隨機分片寫入大量 increment、檢查寫入是否均勻打散到各 shard、再讀取彙總驗證總和正確。&lt;/p>
&lt;p>本文的驗收標準是：你能跑出一個 sharded counter、看到 N 個 shard 各自累積了大致均勻的 partial count、彙總後等於總寫入次數，並理解 emulator 能驗什麼、不能驗什麼。&lt;/p>
&lt;h2 id="先講清楚-emulator-的邊界">先講清楚 emulator 的邊界&lt;/h2>
&lt;p>這個 lab 驗證的是&lt;strong>分片計數的機制正確性&lt;/strong>：寫入是否均勻分佈、彙總是否等於總和、讀取要讀幾個 document。它不驗證的是 &lt;strong>contention 本身&lt;/strong>——emulator 不強制 production 的單 document 持續寫入軟上限，所以「不分片會寫爆」這件事在 emulator 跑不出來。contention 是 production 的規模特性，要在雲端真實負載下才會出現。&lt;/p>
&lt;p>這個分界本身是要學的判讀：emulator 證明「分片計數做對了」，雲端負載測試才證明「不分片會撞牆」。把兩者混為一談會誤以為 emulator 全綠就代表 production 安全。&lt;/p>
&lt;h2 id="lab-環境">Lab 環境&lt;/h2>
&lt;p>沿用 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">quickstart&lt;/a> 的工作區與 emulator。確認 emulator 在跑（另一個 terminal）。&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="nb">cd&lt;/span> /tmp/firestore-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 確認 emulator 已啟動：firebase emulators:start --only firestore --project demo-firestore-lab&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">FIRESTORE_EMULATOR_HOST&lt;/span>&lt;span class="o">=&lt;/span>localhost:8080&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="實作-sharded-counter">實作 sharded counter&lt;/h2>
&lt;p>counter 的核心責任是把一個邏輯計數拆成 N 個 shard document。寫入時隨機挑 shard &lt;code>increment(1)&lt;/code>，讀取時加總所有 shard。這份 script 用 admin SDK 直接對 emulator 操作。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">cat &amp;gt; counter.js &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;JS&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">const admin = require(&amp;#39;firebase-admin&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">admin.initializeApp({ projectId: &amp;#39;demo-firestore-lab&amp;#39; });
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">const db = admin.firestore();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">const FieldValue = admin.firestore.FieldValue;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">const NUM_SHARDS = 10;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">const counterRef = db.collection(&amp;#39;counters&amp;#39;).doc(&amp;#39;likes&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">async function createCounter() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s"> const batch = db.batch();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> for (let i = 0; i &amp;lt; NUM_SHARDS; i++) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> batch.set(counterRef.collection(&amp;#39;shards&amp;#39;).doc(String(i)), { count: 0 });
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> await batch.commit();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s">async function incrementOnce() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> const shardId = Math.floor(Math.random() * NUM_SHARDS);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s"> await counterRef.collection(&amp;#39;shards&amp;#39;).doc(String(shardId))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s"> .set({ count: FieldValue.increment(1) }, { merge: true });
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="s">async function getCount() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="s"> const snap = await counterRef.collection(&amp;#39;shards&amp;#39;).get();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="s"> let total = 0;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="s"> const perShard = {};
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="s"> snap.forEach((s) =&amp;gt; { perShard[s.id] = s.data().count; total += s.data().count; });
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="s"> return { total, perShard };
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="s">module.exports = { createCounter, incrementOnce, getCount, NUM_SHARDS };
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="s">JS&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三個設計點對應 deep article：用 &lt;code>FieldValue.increment(1)&lt;/code> 而非讀-改-寫（避開 race）；隨機選 shard 讓寫入均勻打散；讀取要讀 N 個 shard 加總（這是分片的代價）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a> 的 lab，實作 <a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入</a> deep article 的機制。前置環境見 <a href="/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">Local emulator quickstart</a>。</p></blockquote>
<p>Firestore distributed counter lab 的核心責任是把「分片計數」從概念變成可觀察的寫入分佈與彙總結果。這個 lab 在 emulator 上建立 N 個 shard、隨機分片寫入大量 increment、檢查寫入是否均勻打散到各 shard、再讀取彙總驗證總和正確。</p>
<p>本文的驗收標準是：你能跑出一個 sharded counter、看到 N 個 shard 各自累積了大致均勻的 partial count、彙總後等於總寫入次數，並理解 emulator 能驗什麼、不能驗什麼。</p>
<h2 id="先講清楚-emulator-的邊界">先講清楚 emulator 的邊界</h2>
<p>這個 lab 驗證的是<strong>分片計數的機制正確性</strong>：寫入是否均勻分佈、彙總是否等於總和、讀取要讀幾個 document。它不驗證的是 <strong>contention 本身</strong>——emulator 不強制 production 的單 document 持續寫入軟上限，所以「不分片會寫爆」這件事在 emulator 跑不出來。contention 是 production 的規模特性，要在雲端真實負載下才會出現。</p>
<p>這個分界本身是要學的判讀：emulator 證明「分片計數做對了」，雲端負載測試才證明「不分片會撞牆」。把兩者混為一談會誤以為 emulator 全綠就代表 production 安全。</p>
<h2 id="lab-環境">Lab 環境</h2>
<p>沿用 <a href="/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">quickstart</a> 的工作區與 emulator。確認 emulator 在跑（另一個 terminal）。</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="nb">cd</span> /tmp/firestore-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 確認 emulator 已啟動：firebase emulators:start --only firestore --project demo-firestore-lab</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080</span></span></code></pre></div><h2 id="實作-sharded-counter">實作 sharded counter</h2>
<p>counter 的核心責任是把一個邏輯計數拆成 N 個 shard document。寫入時隨機挑 shard <code>increment(1)</code>，讀取時加總所有 shard。這份 script 用 admin SDK 直接對 emulator 操作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; counter.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const admin = require(&#39;firebase-admin&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">admin.initializeApp({ projectId: &#39;demo-firestore-lab&#39; });
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const db = admin.firestore();
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">const FieldValue = admin.firestore.FieldValue;
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">const NUM_SHARDS = 10;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">const counterRef = db.collection(&#39;counters&#39;).doc(&#39;likes&#39;);
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">async function createCounter() {
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  const batch = db.batch();
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  for (let i = 0; i &lt; NUM_SHARDS; i++) {
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">    batch.set(counterRef.collection(&#39;shards&#39;).doc(String(i)), { count: 0 });
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  await batch.commit();
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">async function incrementOnce() {
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  const shardId = Math.floor(Math.random() * NUM_SHARDS);
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">  await counterRef.collection(&#39;shards&#39;).doc(String(shardId))
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">    .set({ count: FieldValue.increment(1) }, { merge: true });
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">async function getCount() {
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">  const snap = await counterRef.collection(&#39;shards&#39;).get();
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s">  let total = 0;
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="s">  const perShard = {};
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="s">  snap.forEach((s) =&gt; { perShard[s.id] = s.data().count; total += s.data().count; });
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="s">  return { total, perShard };
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="s">module.exports = { createCounter, incrementOnce, getCount, NUM_SHARDS };
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="s">JS</span></span></span></code></pre></div><p>三個設計點對應 deep article：用 <code>FieldValue.increment(1)</code> 而非讀-改-寫（避開 race）；隨機選 shard 讓寫入均勻打散；讀取要讀 N 個 shard 加總（這是分片的代價）。</p>
<h2 id="跑寫入並觀察分佈">跑寫入並觀察分佈</h2>
<p>driver 的核心責任是製造大量 increment、然後檢查寫入是否均勻落在各 shard。均勻分佈是分片有效的前提——若 shard 選擇有偏，熱點會在某幾個 shard 復現。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; run.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const { createCounter, incrementOnce, getCount, NUM_SHARDS } = require(&#39;./counter&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const TOTAL_WRITES = 1000;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">async function main() {
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  await createCounter();
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  console.log(`created ${NUM_SHARDS} shards`);
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  // 製造 1000 次 increment
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  const tasks = [];
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  for (let i = 0; i &lt; TOTAL_WRITES; i++) tasks.push(incrementOnce());
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  await Promise.all(tasks);
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  const { total, perShard } = await getCount();
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  console.log(&#39;per-shard counts:&#39;, perShard);
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  console.log(`total = ${total} (expected ${TOTAL_WRITES})`);
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  // 均勻度檢查：每個 shard 期望 ~100，看極差
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">  const counts = Object.values(perShard);
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">  const min = Math.min(...counts), max = Math.max(...counts);
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">  console.log(`min=${min} max=${max} spread=${max - min} (expected mean ~${TOTAL_WRITES / NUM_SHARDS})`);
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">main().then(() =&gt; process.exit(0));
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">JS</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080
</span></span><span class="line"><span class="ln">28</span><span class="cl">node run.js</span></span></code></pre></div><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">created 10 shards
</span></span><span class="line"><span class="ln">2</span><span class="cl">per-shard counts: { &#39;0&#39;: 98, &#39;1&#39;: 105, &#39;2&#39;: 92, ... }
</span></span><span class="line"><span class="ln">3</span><span class="cl">total = 1000 (expected 1000)
</span></span><span class="line"><span class="ln">4</span><span class="cl">min=88 max=112 spread=24 (expected mean ~100)</span></span></code></pre></div><p>兩個驗收點：<code>total</code> 等於總寫入次數（彙總正確、沒有 increment 遺失），以及各 shard 的 count 大致落在均值附近（隨機分佈均勻、沒有單一 shard 吸走大部分寫入）。</p>
<h2 id="對照實驗讀取成本隨-shard-數成長">對照實驗：讀取成本隨 shard 數成長</h2>
<p>讀取的核心代價是讀 N 個 document。把 <code>NUM_SHARDS</code> 改大（例如 100）重跑，<code>getCount</code> 要讀的 document 從 10 變 100——這就是 deep article 講的「寫入便宜了、讀取乘以 N」的取捨。在 production 這直接反映成 read 計費。</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"># 編輯 counter.js 把 NUM_SHARDS 改為 100、重跑 run.js</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 觀察 per-shard counts 物件變成 100 個 key、getCount 讀取量 10x</span></span></span></code></pre></div><p>這個對照讓「shard 數是寫入分散與讀取成本的取捨」從文字變成可觀察：多 shard 寫入更分散（每 shard 更少），但讀取要加總更多筆。高寫入高讀取的場景該配 summary 彙總（deep article 的進階手段），而非無限加 shard。</p>
<h2 id="artifact-與驗收">Artifact 與驗收</h2>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>來源</th>
          <th>驗收</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>counter 實作</td>
          <td><code>counter.js</code></td>
          <td><code>increment</code> 分片寫入 + 彙總讀取</td>
      </tr>
      <tr>
          <td>寫入分佈</td>
          <td><code>run.js</code> output</td>
          <td>total = 寫入次數、各 shard 均勻</td>
      </tr>
      <tr>
          <td>讀寫取捨</td>
          <td>NUM_SHARDS 對照</td>
          <td>shard 數↑ → 讀取 document 數↑</td>
      </tr>
  </tbody>
</table>
<h2 id="回到-production-判讀">回到 production 判讀</h2>
<p>emulator lab 證明了機制正確，但三個 production 判讀要回雲端確認：單 document 寫入軟上限（決定 shard 數要多少）、read 計費（決定 shard 數別太多 / 要不要 summary）、shard 選擇在真實流量下是否仍均勻。把 emulator 的機制驗證當第一道關，production 的容量與成本判讀見 <a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/#%e5%ae%b9%e9%87%8f%e8%88%87%e8%a7%80%e6%b8%acshard-%e6%95%b8%e7%9a%84%e4%bc%b0%e7%ae%97%e8%88%87%e7%9b%a3%e6%8e%a7" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">deep article 的容量段</a>。</p>
<h2 id="cleanup">Cleanup</h2>





<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"># 停 emulator（Ctrl-C）或清整個工作區</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rm -rf /tmp/firestore-lab</span></span></code></pre></div><h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">高頻寫入與 distributed counter</a></li>
<li>一致性邊界：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a></li>
<li>官方：<a href="https://firebase.google.com/docs/firestore/solutions/counters">Distributed counters</a>、<a href="https://firebase.google.com/docs/firestore/best-practices">Firestore best practices</a></li>
</ul>
]]></content:encoded></item><item><title>Firestore Hands-on 操作路線</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/</guid><description>&lt;p>Firestore hands-on 操作路線的核心責任是把 deep article 的機制判讀轉成可在本地演練的操作。這一層全程跑在 &lt;a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator Suite&lt;/a> 上——本地、免費、不碰雲端專案、不產生計費，讓讀者能建立資料、寫規則測試、跑分片計數，並取得 query output、測試結果與 artifact，而不只停在概念。&lt;/p>
&lt;h2 id="為什麼用-emulator">為什麼用 emulator&lt;/h2>
&lt;p>Firestore 的 client 直連模型讓「在本地驗證」變得重要：規則寫錯是資安漏洞、查詢設計錯是成本事故，這些都該在進雲端前用真實求值引擎驗過。Emulator Suite 提供與雲端一致的 Firestore 行為與 Security Rules 求值引擎，是規則測試的官方推薦環境。要留意的邊界是——emulator 模擬功能行為，但不模擬計費與部分 production 規模限制（單 document 寫入軟上限、連線天花板）。涉及成本與規模的判讀仍以雲端為準，emulator lab 會在對應處標明。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>產出 artifact&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="local-emulator-quickstart/">Local emulator quickstart&lt;/a>&lt;/td>
 &lt;td>emulator 啟動、&lt;code>firestore.rules&lt;/code>、admin seed、query baseline&lt;/td>
 &lt;td>emulator config、seed script、query output&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="security-rules-test-lab/">Security Rules test lab&lt;/a>&lt;/td>
 &lt;td>&lt;code>@firebase/rules-unit-testing&lt;/code>、放行 / 拒絕斷言、CI 整合&lt;/td>
 &lt;td>rules 測試檔、pass / fail 結果、emulators:exec log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="distributed-counter-lab/">Distributed counter lab&lt;/a>&lt;/td>
 &lt;td>分片計數寫入、shard 分佈、讀取彙總、contention 的 production 邊界&lt;/td>
 &lt;td>counter script、shard 分佈 output、彙總驗證&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>Firestore hands-on 章節以「進雲端前先驗」為中心。操作指令只在能產出 artifact 時出現；每篇都要回答 emulator 在哪裡跑、需要哪些 input、怎麼知道操作成功（query output / 測試斷言 / shard 分佈），以及哪些 production 特性（計費、寫入上限）emulator 不負責、要回雲端確認。&lt;/p>
&lt;h2 id="引用路徑">引用路徑&lt;/h2>
&lt;ul>
&lt;li>上游：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview&lt;/a>&lt;/li>
&lt;li>Deep article：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入&lt;/a>&lt;/li>
&lt;li>發布證據：&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>（規則測試接進 gate）&lt;/li>
&lt;li>官方：&lt;a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite&lt;/a>、&lt;a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Firestore hands-on 操作路線的核心責任是把 deep article 的機制判讀轉成可在本地演練的操作。這一層全程跑在 <a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator Suite</a> 上——本地、免費、不碰雲端專案、不產生計費，讓讀者能建立資料、寫規則測試、跑分片計數，並取得 query output、測試結果與 artifact，而不只停在概念。</p>
<h2 id="為什麼用-emulator">為什麼用 emulator</h2>
<p>Firestore 的 client 直連模型讓「在本地驗證」變得重要：規則寫錯是資安漏洞、查詢設計錯是成本事故，這些都該在進雲端前用真實求值引擎驗過。Emulator Suite 提供與雲端一致的 Firestore 行為與 Security Rules 求值引擎，是規則測試的官方推薦環境。要留意的邊界是——emulator 模擬功能行為，但不模擬計費與部分 production 規模限制（單 document 寫入軟上限、連線天花板）。涉及成本與規模的判讀仍以雲端為準，emulator lab 會在對應處標明。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>產出 artifact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="local-emulator-quickstart/">Local emulator quickstart</a></td>
          <td>emulator 啟動、<code>firestore.rules</code>、admin seed、query baseline</td>
          <td>emulator config、seed script、query output</td>
      </tr>
      <tr>
          <td><a href="security-rules-test-lab/">Security Rules test lab</a></td>
          <td><code>@firebase/rules-unit-testing</code>、放行 / 拒絕斷言、CI 整合</td>
          <td>rules 測試檔、pass / fail 結果、emulators:exec log</td>
      </tr>
      <tr>
          <td><a href="distributed-counter-lab/">Distributed counter lab</a></td>
          <td>分片計數寫入、shard 分佈、讀取彙總、contention 的 production 邊界</td>
          <td>counter script、shard 分佈 output、彙總驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="設計原則">設計原則</h2>
<p>Firestore hands-on 章節以「進雲端前先驗」為中心。操作指令只在能產出 artifact 時出現；每篇都要回答 emulator 在哪裡跑、需要哪些 input、怎麼知道操作成功（query output / 測試斷言 / shard 分佈），以及哪些 production 特性（計費、寫入上限）emulator 不負責、要回雲端確認。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a> / <a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入</a></li>
<li>發布證據：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（規則測試接進 gate）</li>
<li>官方：<a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite</a>、<a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator</a></li>
</ul>
]]></content:encoded></item><item><title>Firestore Local Emulator Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線&lt;/a> 的基礎 lab。指令以 &lt;a href="https://firebase.google.com/docs/cli">Firebase CLI 文件&lt;/a> 與 &lt;a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;p>Firestore local emulator quickstart 的核心責任是建立後續 Security Rules 測試與 distributed counter lab 共用的本地環境。這個 lab 把 Firestore 從抽象服務轉成可觀察的 emulator、規則檔、seed 資料與 query 結果，全程不碰雲端專案。&lt;/p>
&lt;p>本文的驗收標準是：你能在本地啟動 Firestore emulator、用 admin SDK 寫入並查詢一組 seed 資料、看到 emulator UI 裡的資料，並知道 cleanup 路徑。&lt;/p>
&lt;h2 id="lab-環境與前置">Lab 環境與前置&lt;/h2>
&lt;p>Lab 在本地資料夾跑，需要 Node.js 與 Firebase CLI。以下命令建立一個可刪除的工作區並裝好工具。&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">mkdir -p /tmp/firestore-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/firestore-lab
&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"># Firebase CLI（已裝可跳過）；用 npx 也可避免全域安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">npm install -g firebase-tools
&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="c1"># 本 lab 的 Node 依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">npm init -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">npm install firebase-admin&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>emulator 需要 Java runtime（Firestore emulator 跑在 JVM 上）。&lt;code>java -version&lt;/code> 確認存在；缺的話先裝 JDK 再繼續。驗收 artifact 是 &lt;code>/tmp/firestore-lab&lt;/code> 工作區。&lt;/p>
&lt;h2 id="emulator-設定">Emulator 設定&lt;/h2>
&lt;p>&lt;code>firebase.json&lt;/code> 的核心責任是宣告要啟動哪些 emulator 與對應 port。這裡只開 Firestore 與 UI，不需要真實 Firebase 專案——emulator 用一個 demo project id 即可，&lt;code>demo-&lt;/code> 前綴讓 CLI 知道這是純本地、不連雲端。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">cat &amp;gt; firebase.json &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;JSON&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;emulators&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="s"> &amp;#34;firestore&amp;#34;: { &amp;#34;port&amp;#34;: 8080 },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;ui&amp;#34;: { &amp;#34;enabled&amp;#34;: true, &amp;#34;port&amp;#34;: 4000 }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;firestore&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="s"> &amp;#34;rules&amp;#34;: &amp;#34;firestore.rules&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="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">JSON&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="baseline-規則">Baseline 規則&lt;/h2>
&lt;p>&lt;code>firestore.rules&lt;/code> 的核心責任是定義授權。Quickstart 先用一組明確的 owner-scoped 規則（不是 &lt;code>allow read, write: if true&lt;/code>，那是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">deep article Case 1&lt;/a> 的漏洞）。這份規則後續在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab&lt;/a> 會被測試覆蓋。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a> 的基礎 lab。指令以 <a href="https://firebase.google.com/docs/cli">Firebase CLI 文件</a> 與 <a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<p>Firestore local emulator quickstart 的核心責任是建立後續 Security Rules 測試與 distributed counter lab 共用的本地環境。這個 lab 把 Firestore 從抽象服務轉成可觀察的 emulator、規則檔、seed 資料與 query 結果，全程不碰雲端專案。</p>
<p>本文的驗收標準是：你能在本地啟動 Firestore emulator、用 admin SDK 寫入並查詢一組 seed 資料、看到 emulator UI 裡的資料，並知道 cleanup 路徑。</p>
<h2 id="lab-環境與前置">Lab 環境與前置</h2>
<p>Lab 在本地資料夾跑，需要 Node.js 與 Firebase CLI。以下命令建立一個可刪除的工作區並裝好工具。</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">mkdir -p /tmp/firestore-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /tmp/firestore-lab
</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"># Firebase CLI（已裝可跳過）；用 npx 也可避免全域安裝</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">npm install -g firebase-tools
</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="c1"># 本 lab 的 Node 依賴</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">npm init -y
</span></span><span class="line"><span class="ln">9</span><span class="cl">npm install firebase-admin</span></span></code></pre></div><p>emulator 需要 Java runtime（Firestore emulator 跑在 JVM 上）。<code>java -version</code> 確認存在；缺的話先裝 JDK 再繼續。驗收 artifact 是 <code>/tmp/firestore-lab</code> 工作區。</p>
<h2 id="emulator-設定">Emulator 設定</h2>
<p><code>firebase.json</code> 的核心責任是宣告要啟動哪些 emulator 與對應 port。這裡只開 Firestore 與 UI，不需要真實 Firebase 專案——emulator 用一個 demo project id 即可，<code>demo-</code> 前綴讓 CLI 知道這是純本地、不連雲端。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; firebase.json <span class="s">&lt;&lt;&#39;JSON&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">{
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  &#34;emulators&#34;: {
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">    &#34;firestore&#34;: { &#34;port&#34;: 8080 },
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">    &#34;ui&#34;: { &#34;enabled&#34;: true, &#34;port&#34;: 4000 }
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  },
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  &#34;firestore&#34;: {
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    &#34;rules&#34;: &#34;firestore.rules&#34;
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">JSON</span></span></span></code></pre></div><h2 id="baseline-規則">Baseline 規則</h2>
<p><code>firestore.rules</code> 的核心責任是定義授權。Quickstart 先用一組明確的 owner-scoped 規則（不是 <code>allow read, write: if true</code>，那是 <a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">deep article Case 1</a> 的漏洞）。這份規則後續在 <a href="/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab</a> 會被測試覆蓋。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; firestore.rules <span class="s">&lt;&lt;&#39;RULES&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">rules_version = &#39;2&#39;;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">service cloud.firestore {
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  match /databases/{database}/documents {
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">    match /notes/{noteId} {
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">      allow read: if request.auth != null
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">                  &amp;&amp; resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">      allow create: if request.auth != null
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">                    &amp;&amp; request.resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">      allow update, delete: if request.auth != null
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">                            &amp;&amp; resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">RULES</span></span></span></code></pre></div><h2 id="啟動-emulator">啟動 emulator</h2>
<p>啟動 emulator 的核心責任是讓本地有一個可寫可查的 Firestore。用 demo project id 啟動，emulator UI 在 <code>http://localhost:4000</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">firebase emulators:start --only firestore --project demo-firestore-lab</span></span></code></pre></div><p>這個指令會 foreground 跑住 emulator。保持它開著，另開一個 terminal 做 seed 與 query。終端輸出會印出 Firestore emulator 的位址（預設 <code>localhost:8080</code>）與 UI 位址。</p>
<h2 id="seed-資料admin-sdk-繞過規則">Seed 資料（admin SDK 繞過規則）</h2>
<p>Seed 的核心責任是建立可重跑的測試資料。admin SDK 連到 emulator 時繞過 Security Rules（模擬後端的特權寫入），適合種資料。關鍵是設 <code>FIRESTORE_EMULATOR_HOST</code> 環境變數——有了它，admin SDK 的寫入全部導向 emulator、不需要任何雲端 credential。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; seed.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const admin = require(&#39;firebase-admin&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">admin.initializeApp({ projectId: &#39;demo-firestore-lab&#39; });
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const db = admin.firestore();
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">async function main() {
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  await db.collection(&#39;notes&#39;).doc(&#39;n1&#39;).set({
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    ownerId: &#39;alice&#39;, text: &#39;Alice first note&#39;, createdAt: Date.now(),
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  await db.collection(&#39;notes&#39;).doc(&#39;n2&#39;).set({
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">    ownerId: &#39;bob&#39;, text: &#39;Bob first note&#39;, createdAt: Date.now(),
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  console.log(&#39;seeded 2 notes&#39;);
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">main().then(() =&gt; process.exit(0));
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">JS</span>
</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="c1"># 在新 terminal、同 lab 目錄下</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080
</span></span><span class="line"><span class="ln">20</span><span class="cl">node seed.js</span></span></code></pre></div><p>預期輸出 <code>seeded 2 notes</code>。打開 <code>http://localhost:4000/firestore</code> 應看到 <code>notes</code> collection 下兩筆 document。</p>
<h2 id="query-baseline">Query baseline</h2>
<p>Query 的核心責任是確認資料可讀、access pattern 入口可用。admin SDK 同樣繞過規則，這裡驗證的是資料與查詢本身（規則的放行 / 拒絕在下一個 lab 用 client context 驗）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; query.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const admin = require(&#39;firebase-admin&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">admin.initializeApp({ projectId: &#39;demo-firestore-lab&#39; });
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const db = admin.firestore();
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">async function main() {
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  const snap = await db.collection(&#39;notes&#39;)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    .where(&#39;ownerId&#39;, &#39;==&#39;, &#39;alice&#39;).get();
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  console.log(`alice notes: ${snap.size}`);
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  snap.forEach((d) =&gt; console.log(d.id, d.data().text));
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">main().then(() =&gt; process.exit(0));
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">JS</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080
</span></span><span class="line"><span class="ln">16</span><span class="cl">node query.js</span></span></code></pre></div><p>預期輸出 <code>alice notes: 1</code> 與 <code>n1 Alice first note</code>。這證明 <code>where('ownerId', '==', ...)</code> 的 access pattern 成立——它也正是 client 端要自帶、好讓 owner-scoped 規則放行的查詢條件。</p>
<h2 id="artifact-與驗收">Artifact 與驗收</h2>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>路徑 / 來源</th>
          <th>驗收</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>emulator config</td>
          <td><code>firebase.json</code></td>
          <td>Firestore + UI port 宣告</td>
      </tr>
      <tr>
          <td>規則檔</td>
          <td><code>firestore.rules</code></td>
          <td>owner-scoped、非 <code>if true</code></td>
      </tr>
      <tr>
          <td>seed 結果</td>
          <td><code>seed.js</code> output + UI</td>
          <td><code>notes/n1</code>、<code>notes/n2</code> 存在</td>
      </tr>
      <tr>
          <td>query 結果</td>
          <td><code>query.js</code> output</td>
          <td><code>alice notes: 1</code></td>
      </tr>
  </tbody>
</table>
<h2 id="cleanup">Cleanup</h2>
<p>Cleanup 的核心責任是讓 lab 可重跑。emulator 的資料在 process 結束時預設不持久化（除非設了 <code>--export-on-exit</code>），所以停掉 emulator 等於清空資料。</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"># 停掉 emulator：在 emulator terminal 按 Ctrl-C</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">rm -rf /tmp/firestore-lab</span></span></code></pre></div><p>若想保留 emulator 資料跨 session，啟動時加 <code>--import=./data --export-on-exit=./data</code>；lab 預設不持久化，保持每次乾淨起步。</p>
<p>完成本篇後，下一步進 <a href="/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab</a>（把上面的規則寫成自動化測試）或 <a href="/blog/backend/01-database/vendors/firestore/hands-on/distributed-counter-lab/" data-link-title="Firestore Distributed Counter Lab" data-link-desc="在 emulator 上實作 distributed counter：建立 N 個 shard、隨機分片寫入、觀察 shard 分佈是否均勻、讀取彙總驗證總和正確，並說明 contention 本身是 emulator 不模擬的 production 特性">Distributed counter lab</a>。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a></li>
<li>官方：<a href="https://firebase.google.com/docs/cli">Install Firebase CLI</a>、<a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator</a></li>
</ul>
]]></content:encoded></item><item><title>Firestore Security Rules Test Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線&lt;/a> 的 lab，實作 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模&lt;/a> deep article 的測試方法。前置環境見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">Local emulator quickstart&lt;/a>。測試 API 以 &lt;a href="https://firebase.google.com/docs/rules/unit-tests">Rules unit testing 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;p>Firestore Security Rules test lab 的核心責任是把授權規則變成可自動驗證的測試。規則是 client 直連模型的整個控制面，改一條就要證明沒開新洞——這個 lab 用 &lt;code>@firebase/rules-unit-testing&lt;/code> 在 emulator 上對規則跑斷言，產出可接進 CI 與 release gate 的測試 evidence。&lt;/p>
&lt;p>本文的驗收標準是：你能對一組規則寫出「放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕」四類斷言、用 &lt;code>firebase emulators:exec&lt;/code> 一鍵跑完、並看到 &lt;code>assertFails&lt;/code> 確實證明該擋的有擋住。&lt;/p>
&lt;h2 id="lab-環境與依賴">Lab 環境與依賴&lt;/h2>
&lt;p>沿用 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">quickstart&lt;/a> 的工作區與 &lt;code>firebase.json&lt;/code> / &lt;code>firestore.rules&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="nb">cd&lt;/span> /tmp/firestore-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npm install --save-dev @firebase/rules-unit-testing firebase jest&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>驗收前置是 &lt;code>firestore.rules&lt;/code> 存在（quickstart 已建立 owner-scoped 規則）與 &lt;code>firebase.json&lt;/code> 宣告了 Firestore emulator。&lt;/p>
&lt;h2 id="升級規則加入欄位竄改防護">升級規則：加入欄位竄改防護&lt;/h2>
&lt;p>quickstart 的規則擋了越權讀寫，但還沒擋「owner 改自己 note 時偷改 &lt;code>ownerId&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">cat &amp;gt; firestore.rules &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;RULES&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">rules_version = &amp;#39;2&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">service cloud.firestore {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s"> match /databases/{database}/documents {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> function isSignedIn() { return request.auth != null; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> function ownsExisting() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> return isSignedIn() &amp;amp;&amp;amp; resource.data.ownerId == request.auth.uid;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> function onlyChanges(fields) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> match /notes/{noteId} {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> allow read: if ownsExisting();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> allow create: if isSignedIn()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;amp;&amp;amp; request.resource.data.ownerId == request.auth.uid;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s"> allow update: if ownsExisting() &amp;amp;&amp;amp; onlyChanges([&amp;#39;text&amp;#39;, &amp;#39;updatedAt&amp;#39;]);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s"> allow delete: if ownsExisting();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="s">RULES&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>onlyChanges(['text', 'updatedAt'])&lt;/code> 是這版的重點：update 只准動 &lt;code>text&lt;/code> 與 &lt;code>updatedAt&lt;/code>，碰 &lt;code>ownerId&lt;/code> 直接拒絕。下面的測試會驗證它。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a> 的 lab，實作 <a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a> deep article 的測試方法。前置環境見 <a href="/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">Local emulator quickstart</a>。測試 API 以 <a href="https://firebase.google.com/docs/rules/unit-tests">Rules unit testing 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<p>Firestore Security Rules test lab 的核心責任是把授權規則變成可自動驗證的測試。規則是 client 直連模型的整個控制面，改一條就要證明沒開新洞——這個 lab 用 <code>@firebase/rules-unit-testing</code> 在 emulator 上對規則跑斷言，產出可接進 CI 與 release gate 的測試 evidence。</p>
<p>本文的驗收標準是：你能對一組規則寫出「放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕」四類斷言、用 <code>firebase emulators:exec</code> 一鍵跑完、並看到 <code>assertFails</code> 確實證明該擋的有擋住。</p>
<h2 id="lab-環境與依賴">Lab 環境與依賴</h2>
<p>沿用 <a href="/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/" data-link-title="Firestore Local Emulator Quickstart" data-link-desc="用 Firebase CLI 啟動 Firestore emulator、寫 firestore.rules、用 admin SDK seed 資料、跑 query baseline 與 cleanup，建立後續 Security Rules 與 distributed counter lab 共用的本地環境">quickstart</a> 的工作區與 <code>firebase.json</code> / <code>firestore.rules</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="nb">cd</span> /tmp/firestore-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm install --save-dev @firebase/rules-unit-testing firebase jest</span></span></code></pre></div><p>驗收前置是 <code>firestore.rules</code> 存在（quickstart 已建立 owner-scoped 規則）與 <code>firebase.json</code> 宣告了 Firestore emulator。</p>
<h2 id="升級規則加入欄位竄改防護">升級規則：加入欄位竄改防護</h2>
<p>quickstart 的規則擋了越權讀寫，但還沒擋「owner 改自己 note 時偷改 <code>ownerId</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">cat &gt; firestore.rules <span class="s">&lt;&lt;&#39;RULES&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">rules_version = &#39;2&#39;;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">service cloud.firestore {
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  match /databases/{database}/documents {
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">    function isSignedIn() { return request.auth != null; }
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    function ownsExisting() {
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">      return isSignedIn() &amp;&amp; resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">    function onlyChanges(fields) {
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">      return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">    match /notes/{noteId} {
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">      allow read: if ownsExisting();
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">      allow create: if isSignedIn()
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">                    &amp;&amp; request.resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">      allow update: if ownsExisting() &amp;&amp; onlyChanges([&#39;text&#39;, &#39;updatedAt&#39;]);
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">      allow delete: if ownsExisting();
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">RULES</span></span></span></code></pre></div><p><code>onlyChanges(['text', 'updatedAt'])</code> 是這版的重點：update 只准動 <code>text</code> 與 <code>updatedAt</code>，碰 <code>ownerId</code> 直接拒絕。下面的測試會驗證它。</p>
<h2 id="寫測試四類斷言">寫測試：四類斷言</h2>
<p>測試的核心責任是覆蓋「該放行的放行、該拒絕的拒絕」。<code>initializeTestEnvironment</code> 載入規則、<code>authenticatedContext</code> 模擬登入身分、<code>assertSucceeds</code> / <code>assertFails</code> 對操作斷言。預先種資料用 <code>withSecurityRulesDisabled</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">cat &gt; rules.test.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const {
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  initializeTestEnvironment, assertFails, assertSucceeds,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">} = require(&#39;@firebase/rules-unit-testing&#39;);
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">const { doc, getDoc, setDoc, updateDoc } = require(&#39;firebase/firestore&#39;);
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">const fs = require(&#39;fs&#39;);
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">let testEnv;
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">beforeAll(async () =&gt; {
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  testEnv = await initializeTestEnvironment({
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">    projectId: &#39;demo-firestore-lab&#39;,
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">    firestore: { rules: fs.readFileSync(&#39;firestore.rules&#39;, &#39;utf8&#39;) },
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">afterAll(async () =&gt; { await testEnv.cleanup(); });
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">beforeEach(async () =&gt; {
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  await testEnv.clearFirestore();
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  await testEnv.withSecurityRulesDisabled(async (ctx) =&gt; {
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">    await setDoc(doc(ctx.firestore(), &#39;notes/n1&#39;),
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">      { ownerId: &#39;alice&#39;, text: &#39;hi&#39;, updatedAt: 0 });
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">// 1. 放行：owner 讀自己的
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s">test(&#39;owner reads own note&#39;, async () =&gt; {
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="s">  const db = testEnv.authenticatedContext(&#39;alice&#39;).firestore();
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="s">  await assertSucceeds(getDoc(doc(db, &#39;notes/n1&#39;)));
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="s">// 2. 越權拒絕：非 owner 讀別人的
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="s">test(&#39;non-owner cannot read&#39;, async () =&gt; {
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="s">  const db = testEnv.authenticatedContext(&#39;bob&#39;).firestore();
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="s">  await assertFails(getDoc(doc(db, &#39;notes/n1&#39;)));
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="s">// 3. 未登入拒絕
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="s">test(&#39;unauthenticated denied&#39;, async () =&gt; {
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="s">  const db = testEnv.unauthenticatedContext().firestore();
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="s">  await assertFails(getDoc(doc(db, &#39;notes/n1&#39;)));
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="s">// 4. 欄位竄改拒絕：owner 偷改 ownerId
</span></span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="s">test(&#39;owner cannot change ownerId&#39;, async () =&gt; {
</span></span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="s">  const db = testEnv.authenticatedContext(&#39;alice&#39;).firestore();
</span></span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="s">  await assertFails(updateDoc(doc(db, &#39;notes/n1&#39;), { ownerId: &#39;bob&#39; }));
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="s">// 4b. 正當 update 放行
</span></span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="s">test(&#39;owner can edit text&#39;, async () =&gt; {
</span></span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="s">  const db = testEnv.authenticatedContext(&#39;alice&#39;).firestore();
</span></span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="s">  await assertSucceeds(updateDoc(doc(db, &#39;notes/n1&#39;), { text: &#39;edited&#39;, updatedAt: 1 }));
</span></span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="s">});
</span></span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="s">JS</span></span></span></code></pre></div><p>四類斷言裡 <code>assertFails</code> 比 <code>assertSucceeds</code> 更重要——它證明的是攻擊路徑被擋住，正是滲透測試會打的點。每條規則至少要有「正向放行 + 至少一條拒絕」配對，光測 happy path 證明不了授權安全。</p>
<h2 id="一鍵跑emulatorsexec">一鍵跑：emulators:exec</h2>
<p>跑測試的核心責任是讓它在乾淨 emulator 上自動化執行。<code>firebase emulators:exec</code> 啟動 emulator、跑指定命令、結束後關閉——適合 CI，不需要手動開關 emulator。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">cat &gt; package.json.test <span class="s">&lt;&lt;&#39;JSON&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">{ &#34;scripts&#34;: { &#34;test:rules&#34;: &#34;jest rules.test.js&#34; } }
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">JSON</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 把 test:rules script 併進既有 package.json 後執行：</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">firebase emulators:exec --only firestore --project demo-firestore-lab <span class="s2">&#34;npx jest rules.test.js&#34;</span></span></span></code></pre></div><p>預期輸出五個測試全 pass：</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">PASS  ./rules.test.js
</span></span><span class="line"><span class="ln">2</span><span class="cl">  owner reads own note (passed)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  non-owner cannot read (passed)
</span></span><span class="line"><span class="ln">4</span><span class="cl">  unauthenticated denied (passed)
</span></span><span class="line"><span class="ln">5</span><span class="cl">  owner cannot change ownerId (passed)
</span></span><span class="line"><span class="ln">6</span><span class="cl">  owner can edit text (passed)
</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">Test Suites: 1 passed, 1 total
</span></span><span class="line"><span class="ln">9</span><span class="cl">Tests:       5 passed, 5 total</span></span></code></pre></div><p>（Jest 預設 reporter 每行會印一個通過標記、此處以 <code>(passed)</code> 文字呈現，實際終端輸出為工具自身格式。）</p>
<h2 id="故意改壞驗證測試有效">故意改壞驗證測試有效</h2>
<p>測試的價值在於它會抓到回歸。把規則改回 <code>allow read, write: if true</code> 再跑，應看到「越權拒絕」「未登入拒絕」「欄位竄改拒絕」三個測試 fail——這證明測試確實守在攻擊路徑上，而不是恆綠的假測試。</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"># 暫時把規則改成全放行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">printf</span> <span class="s2">&#34;rules_version=&#39;2&#39;;\nservice cloud.firestore{match /databases/{db}/documents{match /{d=**}{allow read,write:if true;}}}&#34;</span> &gt; firestore.rules
</span></span><span class="line"><span class="ln">3</span><span class="cl">firebase emulators:exec --only firestore --project demo-firestore-lab <span class="s2">&#34;npx jest rules.test.js&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 預期：3 個 assertFails 測試 fail（該擋的沒擋）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 驗證完改回上面的正確規則</span></span></span></code></pre></div><h2 id="artifact-與驗收">Artifact 與驗收</h2>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>來源</th>
          <th>驗收</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規則測試檔</td>
          <td><code>rules.test.js</code></td>
          <td>四類斷言 + 正向 update</td>
      </tr>
      <tr>
          <td>測試結果</td>
          <td><code>emulators:exec</code> 輸出</td>
          <td>正確規則下全 pass</td>
      </tr>
      <tr>
          <td>回歸證明</td>
          <td>改壞後重跑</td>
          <td>3 個 assertFails 測試轉 fail</td>
      </tr>
  </tbody>
</table>
<h2 id="接進-release-gate">接進 release gate</h2>
<p>規則測試的下游責任是成為發布證據。把 <code>firebase emulators:exec ... jest</code> 接進 CI pipeline，規則變更的 PR 必須通過才能 merge——這把「規則改動沒開新洞」從人工推敲變成 gate 條件，對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的 <code>Gate decision / Checks / Stop condition</code>。授權翻譯的正確性是安全邊界，這個 gate 比一般功能測試更該設為硬性 stop condition。</p>
<h2 id="cleanup">Cleanup</h2>





<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"># emulators:exec 跑完會自動關 emulator；清依賴與工作區</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rm -rf /tmp/firestore-lab</span></span></code></pre></div><h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模與可測試化</a></li>
<li>安全驗證：<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 資料層紅隊</a></li>
<li>發布證據：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a></li>
<li>官方：<a href="https://firebase.google.com/docs/rules/unit-tests">Rules unit testing</a>、<a href="https://firebase.google.com/docs/emulator-suite/install_and_configure">emulators:exec</a></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><item><title>Managed PostgreSQL Comparison</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/</guid><description>&lt;p>Managed PostgreSQL comparison 的核心責任是把「都是 PostgreSQL」拆成不同的操作責任邊界。Managed service 可能代管 backup、patch、replica、minor upgrade、monitoring、connection proxy、serverless scaling 或 branch workflow；但 application schema、query、migration、role、cost 與 incident decision 仍需要 team 承擔。&lt;/p>
&lt;p>本文的判讀錨點是：managed PostgreSQL 是 operation trade-off，而非 vendor-neutral checkbox。選型要看 workload、合規、extension、HA / DR、connection、cost visibility、exit route 與 team skill。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 provider claim。實作前分別查 &lt;a href="https://docs.cloud.google.com/alloydb/docs">AlloyDB docs&lt;/a>、&lt;a href="https://cloud.google.com/sql/postgresql">Cloud SQL for PostgreSQL&lt;/a>、&lt;a href="https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/overview">Azure Database for PostgreSQL Flexible Server&lt;/a> 與 &lt;a href="https://supabase.com/docs/guides/deployment/branching">Supabase branching docs&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="provider-boundary">Provider Boundary&lt;/h2>
&lt;p>Provider boundary 的核心責任是定義 vendor 接手哪些資料庫操作。&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>Cloud managed PostgreSQL&lt;/td>
 &lt;td>RDS PostgreSQL、Cloud SQL、Azure PG&lt;/td>
 &lt;td>標準 PostgreSQL、雲平台整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Aurora PostgreSQL-compatible&lt;/td>
 &lt;td>Amazon Aurora PostgreSQL&lt;/td>
 &lt;td>AWS 生態、高可用 storage layer、read scaling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serverless / branching PG&lt;/td>
 &lt;td>Neon、Supabase 部分能力&lt;/td>
 &lt;td>dev preview、稀疏 workload、快速分支&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Specialist managed PG&lt;/td>
 &lt;td>Crunchy Bridge 等&lt;/td>
 &lt;td>PostgreSQL 專業支援、extension 需求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Self-managed&lt;/td>
 &lt;td>VM / K8s 上自管&lt;/td>
 &lt;td>需要完整控制、具備 DBA 能力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Provider boundary 要寫成 responsibility matrix。誰負責 backup restore、major upgrade、extension enable、failover、connection proxy、audit export、encryption key、support ticket 與 incident decision。&lt;/p>
&lt;p>Serverless / branching PG 這一列的 Neon 與 Supabase 不在同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度&lt;/a>。Neon 是純 serverless PostgreSQL（managed 基礎設施）；Supabase 是把 Postgres 當其中一塊的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS bundle&lt;/a>（同時含 Auth、Storage、Realtime）。只需要資料庫、兩者皆可比較且 Neon 更輕；要連認證、儲存一起到位、才是 Supabase 的賣點。這個外包深度差異與「該買整個 bundle 還是只用它的 Postgres」的判讀、見 &lt;a href="https://tarrragon.github.io/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 建&lt;/a>。&lt;/p>
&lt;h2 id="evaluation-dimensions">Evaluation Dimensions&lt;/h2>
&lt;p>Evaluation dimensions 的核心責任是讓比較避免只看價格或品牌。&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>PostgreSQL fidelity&lt;/td>
 &lt;td>engine version、extension、parameter、superuser 限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA / DR&lt;/td>
 &lt;td>AZ failover、cross-region replica、PITR、restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection&lt;/td>
 &lt;td>max connection、pooler、proxy、serverless cold start&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>import/export、logical replication、downtime window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Observability&lt;/td>
 &lt;td>logs、metrics、slow query、audit、SIEM export&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security&lt;/td>
 &lt;td>network、IAM、KMS、TLS、RLS / pgAudit support&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>instance、storage、I/O、backup、egress、support&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Exit&lt;/td>
 &lt;td>dump、logical replication、snapshot portability&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>PostgreSQL fidelity 是第一關。若服務依賴 extension、logical decoding、superuser function、custom parameter 或 filesystem access，managed provider 的限制會直接影響可行性。&lt;/p></description><content:encoded><![CDATA[<p>Managed PostgreSQL comparison 的核心責任是把「都是 PostgreSQL」拆成不同的操作責任邊界。Managed service 可能代管 backup、patch、replica、minor upgrade、monitoring、connection proxy、serverless scaling 或 branch workflow；但 application schema、query、migration、role、cost 與 incident decision 仍需要 team 承擔。</p>
<p>本文的判讀錨點是：managed PostgreSQL 是 operation trade-off，而非 vendor-neutral checkbox。選型要看 workload、合規、extension、HA / DR、connection、cost visibility、exit route 與 team skill。</p>
<p>官方文件路由的核心責任是固定 provider claim。實作前分別查 <a href="https://docs.cloud.google.com/alloydb/docs">AlloyDB docs</a>、<a href="https://cloud.google.com/sql/postgresql">Cloud SQL for PostgreSQL</a>、<a href="https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/overview">Azure Database for PostgreSQL Flexible Server</a> 與 <a href="https://supabase.com/docs/guides/deployment/branching">Supabase branching docs</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="provider-boundary">Provider Boundary</h2>
<p>Provider boundary 的核心責任是定義 vendor 接手哪些資料庫操作。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>代表選項</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloud managed PostgreSQL</td>
          <td>RDS PostgreSQL、Cloud SQL、Azure PG</td>
          <td>標準 PostgreSQL、雲平台整合</td>
      </tr>
      <tr>
          <td>Aurora PostgreSQL-compatible</td>
          <td>Amazon Aurora PostgreSQL</td>
          <td>AWS 生態、高可用 storage layer、read scaling</td>
      </tr>
      <tr>
          <td>Serverless / branching PG</td>
          <td>Neon、Supabase 部分能力</td>
          <td>dev preview、稀疏 workload、快速分支</td>
      </tr>
      <tr>
          <td>Specialist managed PG</td>
          <td>Crunchy Bridge 等</td>
          <td>PostgreSQL 專業支援、extension 需求</td>
      </tr>
      <tr>
          <td>Self-managed</td>
          <td>VM / K8s 上自管</td>
          <td>需要完整控制、具備 DBA 能力</td>
      </tr>
  </tbody>
</table>
<p>Provider boundary 要寫成 responsibility matrix。誰負責 backup restore、major upgrade、extension enable、failover、connection proxy、audit export、encryption key、support ticket 與 incident decision。</p>
<p>Serverless / branching PG 這一列的 Neon 與 Supabase 不在同一個 <a href="/blog/backend/knowledge-cards/capability-outsourcing-depth/" data-link-title="Capability Outsourcing Depth（外包深度）" data-link-desc="說明外包一塊後端能力有三種深度（managed 基礎設施、feature SaaS、BaaS bundle）、深度決定保留多少控制權與遷出代價">外包深度</a>。Neon 是純 serverless PostgreSQL（managed 基礎設施）；Supabase 是把 Postgres 當其中一塊的 <a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS bundle</a>（同時含 Auth、Storage、Realtime）。只需要資料庫、兩者皆可比較且 Neon 更輕；要連認證、儲存一起到位、才是 Supabase 的賣點。這個外包深度差異與「該買整個 bundle 還是只用它的 Postgres」的判讀、見 <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>。</p>
<h2 id="evaluation-dimensions">Evaluation Dimensions</h2>
<p>Evaluation dimensions 的核心責任是讓比較避免只看價格或品牌。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PostgreSQL fidelity</td>
          <td>engine version、extension、parameter、superuser 限制</td>
      </tr>
      <tr>
          <td>HA / DR</td>
          <td>AZ failover、cross-region replica、PITR、restore drill</td>
      </tr>
      <tr>
          <td>Connection</td>
          <td>max connection、pooler、proxy、serverless cold start</td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>import/export、logical replication、downtime window</td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>logs、metrics、slow query、audit、SIEM export</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>network、IAM、KMS、TLS、RLS / pgAudit support</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>instance、storage、I/O、backup、egress、support</td>
      </tr>
      <tr>
          <td>Exit</td>
          <td>dump、logical replication、snapshot portability</td>
      </tr>
  </tbody>
</table>
<p>PostgreSQL fidelity 是第一關。若服務依賴 extension、logical decoding、superuser function、custom parameter 或 filesystem access，managed provider 的限制會直接影響可行性。</p>
<h2 id="workload-fit">Workload Fit</h2>
<p>Workload fit 的核心責任是把 provider 能力和產品需求對齊。</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>優先考量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SaaS OLTP</td>
          <td>HA、PITR、connection pool、online migration</td>
      </tr>
      <tr>
          <td>Analytics-heavy OLTP</td>
          <td>read replica、I/O cost、work_mem、warehouse boundary</td>
      </tr>
      <tr>
          <td>Dev / preview env</td>
          <td>branching、fast restore、low idle cost</td>
      </tr>
      <tr>
          <td>Regulated workload</td>
          <td>audit、KMS、network isolation、retention</td>
      </tr>
      <tr>
          <td>Extension-heavy app</td>
          <td>PostGIS、pgvector、TimescaleDB、logical decoding support</td>
      </tr>
  </tbody>
</table>
<p>Serverless / branching PG 適合 preview 與稀疏 workload，但 sustained high-throughput production 要審查 cold start、connection、storage separation latency 與 cost curve。</p>
<p>Aurora PostgreSQL 適合 AWS-heavy 架構與高可用 storage layer，但要審查 PostgreSQL compatibility、parameter 限制、I/O cost 與 migration / exit。</p>
<h2 id="migration-and-exit">Migration and Exit</h2>
<p>Migration and exit 的核心責任是避免 managed service 變成單向門。導入前要先知道如何進去、如何出來。</p>
<table>
  <thead>
      <tr>
          <th>流程</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Import</td>
          <td>dump / restore、logical replication、DMS</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>freeze window、replica catch-up、validation</td>
      </tr>
      <tr>
          <td>Rollback</td>
          <td>source snapshot、write replay、DNS switch</td>
      </tr>
      <tr>
          <td>Exit</td>
          <td>pg_dump、logical replication、snapshot export</td>
      </tr>
      <tr>
          <td>Rehearsal</td>
          <td>staging restore、row count、checksum</td>
      </tr>
  </tbody>
</table>
<p>Exit route 要比口頭承諾更具體。至少要能在 staging 將資料匯出到 vanilla PostgreSQL 或下一個 managed provider，並跑 application smoke test。</p>
<h2 id="cost-review">Cost Review</h2>
<p>Cost review 的核心責任是把 managed convenience 轉成總成本。總成本包含 instance、storage、I/O、backup、replica、egress、support、observability、operation labor 與 incident cost。</p>
<table>
  <thead>
      <tr>
          <th>Cost driver</th>
          <th>常見誤判</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>I/O</td>
          <td>只看 instance price</td>
      </tr>
      <tr>
          <td>Backup retention</td>
          <td>長 retention 被忽略</td>
      </tr>
      <tr>
          <td>Cross-region replica</td>
          <td>data transfer / storage 增加</td>
      </tr>
      <tr>
          <td>Observability export</td>
          <td>log volume 與 SIEM 成本</td>
      </tr>
      <tr>
          <td>Serverless idle</td>
          <td>idle 低但 sustained workload 成本不同</td>
      </tr>
  </tbody>
</table>
<p>Cost review 要設 tripwire。當 I/O 成本占比提高、backup retention 變長、replica 增加或 serverless workload 變成常駐，重新評估方案。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把 provider 選型導向具體路線。</p>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>優先路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>標準雲平台 PostgreSQL</td>
          <td>RDS / Cloud SQL / Azure PG</td>
      </tr>
      <tr>
          <td>AWS 生態 + HA storage layer</td>
          <td>Aurora PostgreSQL</td>
      </tr>
      <tr>
          <td>Preview branch / dev env</td>
          <td>Neon / Supabase branch workflow</td>
      </tr>
      <tr>
          <td>Extension / PG 專業支援</td>
          <td>specialist managed PG</td>
      </tr>
      <tr>
          <td>完整控制與特殊 extension</td>
          <td>self-managed PostgreSQL</td>
      </tr>
  </tbody>
</table>
<p>Managed provider 的最終選擇要回到 team skill。少維護元件是價值；把尚未理解的限制外包給 vendor，會在 incident 和 migration 時回來。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Managed PostgreSQL comparison 完成後，Aurora 遷移讀 <a href="../migrate-to-aurora/">PostgreSQL to Aurora Migration</a>；Aurora DSQL 讀 <a href="../migrate-to-aurora-dsql/">PostgreSQL to Aurora DSQL</a>；serverless / specialized variant 讀 <a href="../specialized-pg-variants/">Specialized PostgreSQL Variants</a>。</p>
]]></content:encoded></item><item><title>MySQL Audit Log + SIEM</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/audit-log-siem/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/audit-log-siem/</guid><description>&lt;p>MySQL audit log + SIEM 的核心責任是把資料庫操作事件轉成可查詢、可保留、可告警的安全證據。Audit log 是可調查的行為紀錄；它要回答誰在何時、從哪裡、對哪個資料物件做了什麼，以及是否符合授權流程。&lt;/p>
&lt;p>本文的判讀錨點是：audit logging 要服務於 investigation 與 compliance。Slow query log、general log、binary log、error log、managed service audit log、plugin audit log 各自承擔不同證據，不應混成同一種 log。&lt;/p>
&lt;h2 id="event-taxonomy">Event Taxonomy&lt;/h2>
&lt;p>Event taxonomy 的核心責任是定義要蒐集哪些資料庫事件。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Event 類型&lt;/th>
 &lt;th>目的&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Login / logout&lt;/td>
 &lt;td>身份與來源追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failed access&lt;/td>
 &lt;td>brute force、credential misuse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DDL&lt;/td>
 &lt;td>schema 變更與 migration evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DCL&lt;/td>
 &lt;td>grant / revoke / role 變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sensitive read&lt;/td>
 &lt;td>PII / payment / high-risk table&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data modification&lt;/td>
 &lt;td>bulk update / delete、admin action&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication / backup&lt;/td>
 &lt;td>binlog、backup、restore access&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>事件分類要對應 alert。DDL 可以進 release audit；failed login 可以進 security alert；sensitive read 要連到 support ticket 或 break-glass 流程。&lt;/p>
&lt;h2 id="log-sources">Log Sources&lt;/h2>
&lt;p>Log sources 的核心責任是選出合適來源。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Source&lt;/th>
 &lt;th>適合用途&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Error log&lt;/td>
 &lt;td>startup、crash、replication error&lt;/td>
 &lt;td>缺少完整 query context&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Slow log&lt;/td>
 &lt;td>performance investigation&lt;/td>
 &lt;td>安全事件覆蓋不足&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>General log&lt;/td>
 &lt;td>debug / short-term tracing&lt;/td>
 &lt;td>volume 大、PII 風險高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Binary log&lt;/td>
 &lt;td>data change recovery / CDC&lt;/td>
 &lt;td>需要解析、並非 user audit 完整替代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audit plugin / managed audit&lt;/td>
 &lt;td>security evidence&lt;/td>
 &lt;td>provider / edition / config 限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>General log 在 production 要謹慎使用。它能提供完整 SQL，但 volume、PII 與成本都高；通常只用短時間 incident window 或測試環境。&lt;/p>
&lt;h2 id="siem-pipeline">SIEM Pipeline&lt;/h2>
&lt;p>SIEM pipeline 的核心責任是把 database event 轉成集中查詢與告警。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pipeline step&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Collect&lt;/td>
 &lt;td>log file、managed log export、agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Normalize&lt;/td>
 &lt;td>actor、source IP、database、object、action&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mask&lt;/td>
 &lt;td>移除 SQL literal / PII&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retain&lt;/td>
 &lt;td>retention、legal hold、storage class&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>rule、severity、owner、runbook&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>periodic access review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Normalization 要避免把完整 SQL 直接送進 SIEM。對敏感系統，可保留 query fingerprint、table、operation、row count、actor 與 ticket id，而非 literal value。&lt;/p></description><content:encoded><![CDATA[<p>MySQL audit log + SIEM 的核心責任是把資料庫操作事件轉成可查詢、可保留、可告警的安全證據。Audit log 是可調查的行為紀錄；它要回答誰在何時、從哪裡、對哪個資料物件做了什麼，以及是否符合授權流程。</p>
<p>本文的判讀錨點是：audit logging 要服務於 investigation 與 compliance。Slow query log、general log、binary log、error log、managed service audit log、plugin audit log 各自承擔不同證據，不應混成同一種 log。</p>
<h2 id="event-taxonomy">Event Taxonomy</h2>
<p>Event taxonomy 的核心責任是定義要蒐集哪些資料庫事件。</p>
<table>
  <thead>
      <tr>
          <th>Event 類型</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Login / logout</td>
          <td>身份與來源追蹤</td>
      </tr>
      <tr>
          <td>Failed access</td>
          <td>brute force、credential misuse</td>
      </tr>
      <tr>
          <td>DDL</td>
          <td>schema 變更與 migration evidence</td>
      </tr>
      <tr>
          <td>DCL</td>
          <td>grant / revoke / role 變更</td>
      </tr>
      <tr>
          <td>Sensitive read</td>
          <td>PII / payment / high-risk table</td>
      </tr>
      <tr>
          <td>Data modification</td>
          <td>bulk update / delete、admin action</td>
      </tr>
      <tr>
          <td>Replication / backup</td>
          <td>binlog、backup、restore access</td>
      </tr>
  </tbody>
</table>
<p>事件分類要對應 alert。DDL 可以進 release audit；failed login 可以進 security alert；sensitive read 要連到 support ticket 或 break-glass 流程。</p>
<h2 id="log-sources">Log Sources</h2>
<p>Log sources 的核心責任是選出合適來源。</p>
<table>
  <thead>
      <tr>
          <th>Source</th>
          <th>適合用途</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Error log</td>
          <td>startup、crash、replication error</td>
          <td>缺少完整 query context</td>
      </tr>
      <tr>
          <td>Slow log</td>
          <td>performance investigation</td>
          <td>安全事件覆蓋不足</td>
      </tr>
      <tr>
          <td>General log</td>
          <td>debug / short-term tracing</td>
          <td>volume 大、PII 風險高</td>
      </tr>
      <tr>
          <td>Binary log</td>
          <td>data change recovery / CDC</td>
          <td>需要解析、並非 user audit 完整替代</td>
      </tr>
      <tr>
          <td>Audit plugin / managed audit</td>
          <td>security evidence</td>
          <td>provider / edition / config 限制</td>
      </tr>
  </tbody>
</table>
<p>General log 在 production 要謹慎使用。它能提供完整 SQL，但 volume、PII 與成本都高；通常只用短時間 incident window 或測試環境。</p>
<h2 id="siem-pipeline">SIEM Pipeline</h2>
<p>SIEM pipeline 的核心責任是把 database event 轉成集中查詢與告警。</p>
<table>
  <thead>
      <tr>
          <th>Pipeline step</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collect</td>
          <td>log file、managed log export、agent</td>
      </tr>
      <tr>
          <td>Normalize</td>
          <td>actor、source IP、database、object、action</td>
      </tr>
      <tr>
          <td>Mask</td>
          <td>移除 SQL literal / PII</td>
      </tr>
      <tr>
          <td>Retain</td>
          <td>retention、legal hold、storage class</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>rule、severity、owner、runbook</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>periodic access review</td>
      </tr>
  </tbody>
</table>
<p>Normalization 要避免把完整 SQL 直接送進 SIEM。對敏感系統，可保留 query fingerprint、table、operation、row count、actor 與 ticket id，而非 literal value。</p>
<h2 id="alert-rules">Alert Rules</h2>
<p>Alert rules 的核心責任是把高風險事件變成可行動訊號。</p>
<table>
  <thead>
      <tr>
          <th>Rule</th>
          <th>代表風險</th>
          <th>第一反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Admin login outside window</td>
          <td>credential misuse / emergency access</td>
          <td>確認 ticket、限制 session</td>
      </tr>
      <tr>
          <td>Grant / revoke event</td>
          <td>權限邊界變更</td>
          <td>access review</td>
      </tr>
      <tr>
          <td>Drop / truncate table</td>
          <td>destructive DDL</td>
          <td>freeze release、restore decision</td>
      </tr>
      <tr>
          <td>Bulk update / delete</td>
          <td>application bug / misuse</td>
          <td>查 transaction、binlog、backup</td>
      </tr>
      <tr>
          <td>Sensitive table read</td>
          <td>PII exposure</td>
          <td>ticket match、scope review</td>
      </tr>
  </tbody>
</table>
<p>Alert 要有 owner 與 runbook。只把 log 送進 SIEM，缺少 triage rule，incident 時仍然難以快速定位。</p>
<h2 id="retention-and-privacy">Retention and Privacy</h2>
<p>Retention and privacy 的核心責任是讓 audit log 同時可用與合規。Audit log 可能包含帳號、IP、SQL、table name、literal value 與 PII；保存時間越長，保護責任越重。</p>
<p>Retention policy 要定義：</p>
<ol>
<li>保存天數與 storage class。</li>
<li>哪些欄位可被 masked。</li>
<li>誰能查 audit log。</li>
<li>Legal hold 如何覆蓋一般 retention。</li>
<li>Export 到外部 SIEM 的資料邊界。</li>
</ol>
<p>Audit log 本身也要納入 access control。能查敏感 audit 的人，通常也能推斷敏感資料活動。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Audit log + SIEM 完成後，加密與憑證讀 <a href="../encryption-tls-key-management/">Encryption / TLS / Key Management</a>；備份事故讀 <a href="../pitr-backup/">PITR / Backup</a>；安全治理讀 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>。</p>
]]></content:encoded></item><item><title>MySQL Backup Restore Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/backup-restore-drill/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/backup-restore-drill/</guid><description>&lt;p>MySQL backup restore drill 的核心責任是證明資料可以從 backup 回到可用狀態。這篇承接 &lt;a href="../../pitr-backup/">PITR / Backup&lt;/a>，用 logical dump 建立最小演練框架，並保留 physical backup / binlog PITR 的 evidence 欄位。&lt;/p>
&lt;p>本文的驗收標準是：你能產出 dump、記錄 binlog position、還原到隔離 database、跑 validation query，並寫下 RPO / RTO note。&lt;/p>
&lt;h2 id="create-backup">Create Backup&lt;/h2>
&lt;p>Create backup 的核心責任是建立可還原 artifact。&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">mkdir -p /tmp/mysql-backup-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mysqldump -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw &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> --single-transaction --routines --triggers appdb &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> &amp;gt; /tmp/mysql-backup-lab/appdb.sql&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>記錄 binlog 狀態：&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw -e &lt;span class="s2">&amp;#34;SHOW BINARY LOG STATUS;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--single-transaction&lt;/code> 適合 InnoDB consistent dump。大型 production 要評估 physical backup、backup lock、replication lag 與 binlog retention。&lt;/p>
&lt;h2 id="mutate-source">Mutate Source&lt;/h2>
&lt;p>Mutate source 的核心責任是讓 restore 時間點具體化。&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 777, &amp;#39;after-backup-write&amp;#39;);&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Source 現在比 backup 多一筆。這能用來討論 RPO 與 binlog PITR。&lt;/p>
&lt;h2 id="restore-isolated-database">Restore Isolated Database&lt;/h2>
&lt;p>Restore isolated database 的核心責任是避免覆蓋 source。&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;DROP DATABASE IF EXISTS appdb_restore; CREATE DATABASE appdb_restore;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw appdb_restore &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> &amp;lt; /tmp/mysql-backup-lab/appdb.sql&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Validation：&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw appdb_restore &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM accounts;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM ledger_entries;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">GROUP BY a.owner_name;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Validation query 要和 application smoke test 對齊。正式 drill 還要啟動 app 指向 restore database。&lt;/p>
&lt;h2 id="rpo--rto-note">RPO / RTO Note&lt;/h2>
&lt;p>RPO / RTO note 的核心責任是把演練結果轉成服務承諾。&lt;/p></description><content:encoded><![CDATA[<p>MySQL backup restore drill 的核心責任是證明資料可以從 backup 回到可用狀態。這篇承接 <a href="../../pitr-backup/">PITR / Backup</a>，用 logical dump 建立最小演練框架，並保留 physical backup / binlog PITR 的 evidence 欄位。</p>
<p>本文的驗收標準是：你能產出 dump、記錄 binlog position、還原到隔離 database、跑 validation query，並寫下 RPO / RTO note。</p>
<h2 id="create-backup">Create Backup</h2>
<p>Create backup 的核心責任是建立可還原 artifact。</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">mkdir -p /tmp/mysql-backup-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysqldump -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --single-transaction --routines --triggers appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  &gt; /tmp/mysql-backup-lab/appdb.sql</span></span></code></pre></div><p>記錄 binlog 狀態：</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw -e <span class="s2">&#34;SHOW BINARY LOG STATUS;&#34;</span></span></span></code></pre></div><p><code>--single-transaction</code> 適合 InnoDB consistent dump。大型 production 要評估 physical backup、backup lock、replication lag 與 binlog retention。</p>
<h2 id="mutate-source">Mutate Source</h2>
<p>Mutate source 的核心責任是讓 restore 時間點具體化。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -e <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 777, &#39;after-backup-write&#39;);&#34;</span></span></span></code></pre></div><p>Source 現在比 backup 多一筆。這能用來討論 RPO 與 binlog PITR。</p>
<h2 id="restore-isolated-database">Restore Isolated Database</h2>
<p>Restore isolated database 的核心責任是避免覆蓋 source。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -e <span class="s2">&#34;DROP DATABASE IF EXISTS appdb_restore; CREATE DATABASE appdb_restore;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw appdb_restore <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  &lt; /tmp/mysql-backup-lab/appdb.sql</span></span></code></pre></div><p>Validation：</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw appdb_restore <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT COUNT(*) FROM accounts;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT COUNT(*) FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">GROUP BY a.owner_name;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Validation query 要和 application smoke test 對齊。正式 drill 還要啟動 app 指向 restore database。</p>
<h2 id="rpo--rto-note">RPO / RTO Note</h2>
<p>RPO / RTO note 的核心責任是把演練結果轉成服務承諾。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>記錄內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup time</td>
          <td>dump start / finish</td>
      </tr>
      <tr>
          <td>Binlog position</td>
          <td>file、position 或 GTID set</td>
      </tr>
      <tr>
          <td>Restore time</td>
          <td>開始 restore 到 validation 成功</td>
      </tr>
      <tr>
          <td>Data gap</td>
          <td>backup 後需要 binlog 補回的寫入</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>application workflow</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，binlog CDC 讀 <a href="../../binlog-cdc/">Binlog CDC</a>；PITR 策略讀 <a href="../../pitr-backup/">PITR / Backup</a>。</p>
]]></content:encoded></item><item><title>MySQL Cross-buffer Memory Contention</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/cross-buffer-memory-contention/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/cross-buffer-memory-contention/</guid><description>&lt;p>MySQL cross-buffer memory contention 的核心責任是把 MySQL memory tuning 從單一 buffer pool 參數擴展到整體記憶體競爭。InnoDB buffer pool、redo log buffer、sort buffer、join buffer、tmp table、thread stack、connection memory、OS page cache 與 container limit 會共同決定 latency 與 OOM 風險。&lt;/p>
&lt;p>本文的判讀錨點是：MySQL memory 問題常來自&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/per-connection-memory/" data-link-title="Per-Connection Memory" data-link-desc="說明每條連線或每個操作的記憶體用量如何隨並發數放大">「每連線 / 每操作」記憶體&lt;/a>乘上 concurrency，而非只來自全域 buffer pool。調大單一 buffer 前，要先看 workload 與同時執行的 query。&lt;/p>
&lt;h2 id="memory-surfaces">Memory Surfaces&lt;/h2>
&lt;p>Memory surfaces 的核心責任是列出會互相競爭的記憶體來源。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Surface&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>InnoDB buffer pool&lt;/td>
 &lt;td>global&lt;/td>
 &lt;td>太小造成 read I/O，太大壓縮 OS 空間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Redo log buffer&lt;/td>
 &lt;td>global&lt;/td>
 &lt;td>大交易 / burst write 需要審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort buffer&lt;/td>
 &lt;td>per session / operation&lt;/td>
 &lt;td>concurrent sort 放大 memory&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Join buffer&lt;/td>
 &lt;td>per session / join&lt;/td>
 &lt;td>missing index 時放大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Temp table&lt;/td>
 &lt;td>memory / disk&lt;/td>
 &lt;td>group / sort / derived table&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection overhead&lt;/td>
 &lt;td>per connection&lt;/td>
 &lt;td>connection storm / thread memory&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OS page cache&lt;/td>
 &lt;td>system&lt;/td>
 &lt;td>file、backup、binlog、tmp&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Per-session buffer 是最容易誤調的項目。把 sort / join buffer 全域調大，會在高 concurrency 下造成 memory spike。&lt;/p>
&lt;h2 id="contention-signals">Contention Signals&lt;/h2>
&lt;p>Contention signals 的核心責任是把 memory pressure 從 symptom 轉成可排查訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>OOM / container restart&lt;/td>
 &lt;td>total memory 超出限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>swap activity&lt;/td>
 &lt;td>memory pressure 已影響 latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Created_tmp_disk_tables 增加&lt;/td>
 &lt;td>memory temp table 不足或 query 太大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort_merge_passes 增加&lt;/td>
 &lt;td>sort memory / query shape 問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Buffer pool hit rate 下降&lt;/td>
 &lt;td>working set / query pattern 問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Threads_connected 高&lt;/td>
 &lt;td>per-connection memory 放大&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Signal 要和 query workload 對照。Temp table 與 sort 問題通常需要 query rewrite、index 或報表隔離，而非只調 memory。&lt;/p>
&lt;h2 id="tuning-order">Tuning Order&lt;/h2>
&lt;p>Tuning order 的核心責任是建立安全調整順序。&lt;/p></description><content:encoded><![CDATA[<p>MySQL cross-buffer memory contention 的核心責任是把 MySQL memory tuning 從單一 buffer pool 參數擴展到整體記憶體競爭。InnoDB buffer pool、redo log buffer、sort buffer、join buffer、tmp table、thread stack、connection memory、OS page cache 與 container limit 會共同決定 latency 與 OOM 風險。</p>
<p>本文的判讀錨點是：MySQL memory 問題常來自<a href="/blog/backend/knowledge-cards/per-connection-memory/" data-link-title="Per-Connection Memory" data-link-desc="說明每條連線或每個操作的記憶體用量如何隨並發數放大">「每連線 / 每操作」記憶體</a>乘上 concurrency，而非只來自全域 buffer pool。調大單一 buffer 前，要先看 workload 與同時執行的 query。</p>
<h2 id="memory-surfaces">Memory Surfaces</h2>
<p>Memory surfaces 的核心責任是列出會互相競爭的記憶體來源。</p>
<table>
  <thead>
      <tr>
          <th>Surface</th>
          <th>類型</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>InnoDB buffer pool</td>
          <td>global</td>
          <td>太小造成 read I/O，太大壓縮 OS 空間</td>
      </tr>
      <tr>
          <td>Redo log buffer</td>
          <td>global</td>
          <td>大交易 / burst write 需要審查</td>
      </tr>
      <tr>
          <td>Sort buffer</td>
          <td>per session / operation</td>
          <td>concurrent sort 放大 memory</td>
      </tr>
      <tr>
          <td>Join buffer</td>
          <td>per session / join</td>
          <td>missing index 時放大</td>
      </tr>
      <tr>
          <td>Temp table</td>
          <td>memory / disk</td>
          <td>group / sort / derived table</td>
      </tr>
      <tr>
          <td>Connection overhead</td>
          <td>per connection</td>
          <td>connection storm / thread memory</td>
      </tr>
      <tr>
          <td>OS page cache</td>
          <td>system</td>
          <td>file、backup、binlog、tmp</td>
      </tr>
  </tbody>
</table>
<p>Per-session buffer 是最容易誤調的項目。把 sort / join buffer 全域調大，會在高 concurrency 下造成 memory spike。</p>
<h2 id="contention-signals">Contention Signals</h2>
<p>Contention signals 的核心責任是把 memory pressure 從 symptom 轉成可排查訊號。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OOM / container restart</td>
          <td>total memory 超出限制</td>
      </tr>
      <tr>
          <td>swap activity</td>
          <td>memory pressure 已影響 latency</td>
      </tr>
      <tr>
          <td>Created_tmp_disk_tables 增加</td>
          <td>memory temp table 不足或 query 太大</td>
      </tr>
      <tr>
          <td>Sort_merge_passes 增加</td>
          <td>sort memory / query shape 問題</td>
      </tr>
      <tr>
          <td>Buffer pool hit rate 下降</td>
          <td>working set / query pattern 問題</td>
      </tr>
      <tr>
          <td>Threads_connected 高</td>
          <td>per-connection memory 放大</td>
      </tr>
  </tbody>
</table>
<p>Signal 要和 query workload 對照。Temp table 與 sort 問題通常需要 query rewrite、index 或報表隔離，而非只調 memory。</p>
<h2 id="tuning-order">Tuning Order</h2>
<p>Tuning order 的核心責任是建立安全調整順序。</p>
<ol>
<li>先確認 host / container memory limit。</li>
<li>設定 InnoDB buffer pool baseline。</li>
<li>控制 max connections 與 application pool。</li>
<li>用 top query 找 sort / join / temp table 來源。</li>
<li>對特定 session / workload 調 buffer，而非全域放大。</li>
<li>將 analytics / reporting 移到 replica 或 OLAP。</li>
</ol>
<p>這個順序讓全域 memory 先穩定，再處理 query 層問題。若反過來先調大 per-session buffer，壓力會在尖峰流量時爆發。</p>
<h2 id="query-patterns">Query Patterns</h2>
<p>Query patterns 的核心責任是找出 memory heavy 查詢。</p>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Memory 風險</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Large sort</td>
          <td>sort buffer / temp table</td>
          <td>index order、limit、pagination</td>
      </tr>
      <tr>
          <td>Missing join index</td>
          <td>join buffer 放大</td>
          <td>補 index、改 join order</td>
      </tr>
      <tr>
          <td>Big GROUP BY</td>
          <td>tmp table / disk spill</td>
          <td>pre-aggregate、OLAP、covering index</td>
      </tr>
      <tr>
          <td>Large transaction</td>
          <td>undo / lock / memory</td>
          <td>batch、縮短 transaction</td>
      </tr>
      <tr>
          <td>Many idle sessions</td>
          <td>connection memory</td>
          <td>pooler、timeout、max connection</td>
      </tr>
  </tbody>
</table>
<p>Memory tuning 要服務 query design。若 query 本身無界，memory 只會把問題延後到更大資料量。</p>
<h2 id="runbook">Runbook</h2>
<p>Runbook 的核心責任是把 memory incident 分流。</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Confirm pressure</td>
          <td>OS memory、swap、OOM、MySQL status</td>
      </tr>
      <tr>
          <td>Identify workload</td>
          <td>processlist、performance schema、top SQL</td>
      </tr>
      <tr>
          <td>Reduce concurrency</td>
          <td>限流、停報表、降 background job</td>
      </tr>
      <tr>
          <td>Protect OLTP</td>
          <td>kill heavy query、切 read replica</td>
      </tr>
      <tr>
          <td>Tune safely</td>
          <td>session-level buffer、index、query</td>
      </tr>
      <tr>
          <td>Retrospective</td>
          <td>pool size、query guard、dashboard</td>
      </tr>
  </tbody>
</table>
<p>OOM 後要保存 evidence：memory limit、MySQL variables、Threads_connected、top queries、tmp table counters、container restart time。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Cross-buffer memory contention 完成後，InnoDB 基礎讀 <a href="../innodb-tuning/">InnoDB Tuning</a>；query 層讀 <a href="../query-optimization/">Query Optimization</a>；lock 與 transaction 壓力讀 <a href="../lock-contention/">Lock Contention</a>。</p>
]]></content:encoded></item><item><title>MySQL Document Store / X Protocol</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/document-store-x-protocol/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/document-store-x-protocol/</guid><description>&lt;p>MySQL Document Store / X Protocol 的核心責任是說明 MySQL 如何在 relational engine 內提供 JSON document workflow。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">Document Store&lt;/a> 讓 application 透過 X Protocol 與 CRUD API 操作 collection，但資料仍落在 MySQL 的 storage、transaction、backup 與 permission 模型裡。&lt;/p>
&lt;p>本文的判讀錨點是：Document Store 是 MySQL 內的 document access pattern，而非 MongoDB 等專用 document database 的完整替代。它適合 relational schema 旁邊的 flexible JSON，但不適合把主要資料模型都藏進無治理 JSON。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 X Protocol claim。實作前先查 &lt;a href="https://dev.mysql.com/doc/refman/en/document-store.html">MySQL 8.4 Document Store&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="responsibility-boundary">Responsibility Boundary&lt;/h2>
&lt;p>Responsibility boundary 的核心責任是把 Document Store 和 SQL table 關係說清楚。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Document Store&lt;/th>
 &lt;th>SQL table / JSON column&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Access API&lt;/td>
 &lt;td>X Protocol、CRUD-style API&lt;/td>
 &lt;td>SQL、JSON function&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>MySQL InnoDB&lt;/td>
 &lt;td>MySQL InnoDB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>MySQL transaction&lt;/td>
 &lt;td>MySQL transaction&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Governance&lt;/td>
 &lt;td>仍需 backup、role、audit、migration&lt;/td>
 &lt;td>仍需 schema / index review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query power&lt;/td>
 &lt;td>document-friendly access&lt;/td>
 &lt;td>SQL join、index、optimizer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Document Store 的價值是降低 flexible object 的開發摩擦。它不免除資料合約、index、migration、backup 與 audit 的責任。&lt;/p>
&lt;h2 id="suitable-use-cases">Suitable Use Cases&lt;/h2>
&lt;p>Suitable use cases 的核心責任是找出 document pattern 的合理位置。&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>Profile / preference&lt;/td>
 &lt;td>欄位變動快、查詢條件少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integration payload&lt;/td>
 &lt;td>需要保存外部 JSON 原文&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Feature flag / config&lt;/td>
 &lt;td>讀多寫少、schema 變化頻繁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hybrid relational + JSON&lt;/td>
 &lt;td>主體 relational，局部 flexible&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prototype&lt;/td>
 &lt;td>先探索欄位，再逐步 relationalize&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Document Store 最適合局部 flexible data。若核心 query 需要大量 join、aggregation、transaction invariant，應把穩定欄位拉回 relational schema。&lt;/p>
&lt;h2 id="query-and-index">Query and Index&lt;/h2>
&lt;p>Query and index 的核心責任是避免 JSON 查詢變成不可觀測黑箱。&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>常用 filter&lt;/td>
 &lt;td>是否需要 generated column / functional index&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort / pagination&lt;/td>
 &lt;td>是否能走 index&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema drift&lt;/td>
 &lt;td>document version / validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Large document&lt;/td>
 &lt;td>update amplification、network payload&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Analytics&lt;/td>
 &lt;td>是否應 ETL 到 OLAP / warehouse&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>MySQL JSON 查詢可以從 generated column 建 index。正式服務要把常用 JSON path 寫進 query contract，避免每次都掃完整 document。&lt;/p></description><content:encoded><![CDATA[<p>MySQL Document Store / X Protocol 的核心責任是說明 MySQL 如何在 relational engine 內提供 JSON document workflow。<a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">Document Store</a> 讓 application 透過 X Protocol 與 CRUD API 操作 collection，但資料仍落在 MySQL 的 storage、transaction、backup 與 permission 模型裡。</p>
<p>本文的判讀錨點是：Document Store 是 MySQL 內的 document access pattern，而非 MongoDB 等專用 document database 的完整替代。它適合 relational schema 旁邊的 flexible JSON，但不適合把主要資料模型都藏進無治理 JSON。</p>
<p>官方文件路由的核心責任是固定 X Protocol claim。實作前先查 <a href="https://dev.mysql.com/doc/refman/en/document-store.html">MySQL 8.4 Document Store</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="responsibility-boundary">Responsibility Boundary</h2>
<p>Responsibility boundary 的核心責任是把 Document Store 和 SQL table 關係說清楚。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Document Store</th>
          <th>SQL table / JSON column</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Access API</td>
          <td>X Protocol、CRUD-style API</td>
          <td>SQL、JSON function</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>MySQL InnoDB</td>
          <td>MySQL InnoDB</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>MySQL transaction</td>
          <td>MySQL transaction</td>
      </tr>
      <tr>
          <td>Governance</td>
          <td>仍需 backup、role、audit、migration</td>
          <td>仍需 schema / index review</td>
      </tr>
      <tr>
          <td>Query power</td>
          <td>document-friendly access</td>
          <td>SQL join、index、optimizer</td>
      </tr>
  </tbody>
</table>
<p>Document Store 的價值是降低 flexible object 的開發摩擦。它不免除資料合約、index、migration、backup 與 audit 的責任。</p>
<h2 id="suitable-use-cases">Suitable Use Cases</h2>
<p>Suitable use cases 的核心責任是找出 document pattern 的合理位置。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>適合原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Profile / preference</td>
          <td>欄位變動快、查詢條件少</td>
      </tr>
      <tr>
          <td>Integration payload</td>
          <td>需要保存外部 JSON 原文</td>
      </tr>
      <tr>
          <td>Feature flag / config</td>
          <td>讀多寫少、schema 變化頻繁</td>
      </tr>
      <tr>
          <td>Hybrid relational + JSON</td>
          <td>主體 relational，局部 flexible</td>
      </tr>
      <tr>
          <td>Prototype</td>
          <td>先探索欄位，再逐步 relationalize</td>
      </tr>
  </tbody>
</table>
<p>Document Store 最適合局部 flexible data。若核心 query 需要大量 join、aggregation、transaction invariant，應把穩定欄位拉回 relational schema。</p>
<h2 id="query-and-index">Query and Index</h2>
<p>Query and index 的核心責任是避免 JSON 查詢變成不可觀測黑箱。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>審查方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>常用 filter</td>
          <td>是否需要 generated column / functional index</td>
      </tr>
      <tr>
          <td>Sort / pagination</td>
          <td>是否能走 index</td>
      </tr>
      <tr>
          <td>Schema drift</td>
          <td>document version / validation</td>
      </tr>
      <tr>
          <td>Large document</td>
          <td>update amplification、network payload</td>
      </tr>
      <tr>
          <td>Analytics</td>
          <td>是否應 ETL 到 OLAP / warehouse</td>
      </tr>
  </tbody>
</table>
<p>MySQL JSON 查詢可以從 generated column 建 index。正式服務要把常用 JSON path 寫進 query contract，避免每次都掃完整 document。</p>
<h2 id="migration-boundary">Migration Boundary</h2>
<p>Migration boundary 的核心責任是讓 document data 可演進。Document 欄位雖然 flexible，但 application 仍會依賴某些 key；這些 key 一旦進入 workflow，就要有版本與 validation。</p>
<p>最小治理：</p>
<ol>
<li>Document version field。</li>
<li>Required key validation at application boundary。</li>
<li>Backfill script for new required key。</li>
<li>Index review for promoted key。</li>
<li>Export / backup restore validation。</li>
</ol>
<p>當 JSON key 變成 join key、permission key 或 reporting key，應評估搬到 relational column。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go conditions 的核心責任是指出 Document Store 的邊界。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要資料都是 nested document</td>
          <td>MongoDB / document database evaluation</td>
      </tr>
      <tr>
          <td>大量 document aggregation</td>
          <td>OLAP / search / document-oriented engine</td>
      </tr>
      <tr>
          <td>JSON path 已成核心 index</td>
          <td>relationalize key 或 generated column</td>
      </tr>
      <tr>
          <td>需要跨 document complex join</td>
          <td>relational schema</td>
      </tr>
      <tr>
          <td>需要 schema governance</td>
          <td>migration + validation</td>
      </tr>
  </tbody>
</table>
<p>Document Store 要服務於 flexible edge，而非取代資料建模。當 flexible area 穩定下來，就把它納入 schema governance。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Document Store / X Protocol 完成後，JSON 與 SQL 能力讀 <a href="../modern-sql-features/">Modern SQL Features</a>；若主要資料模型是 document，讀 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a>；migration 到 PostgreSQL JSONB 可讀 <a href="../migrate-to-postgresql/">MySQL to PostgreSQL</a>。</p>
]]></content:encoded></item><item><title>MySQL Encryption / TLS / Key Management</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/encryption-tls-key-management/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/encryption-tls-key-management/</guid><description>&lt;p>MySQL encryption / TLS / key management 的核心責任是把資料庫保護拆成儲存加密、傳輸加密、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/key-management/" data-link-title="Key Management" data-link-desc="說明加密金鑰如何產生、保存、輪替，以及還原時如何依賴金鑰">金鑰生命週期&lt;/a>與連線憑證治理。Encryption 是多層保護設計；它涵蓋 InnoDB tablespace、redo / undo、binary log、backup artifact、client connection 與 keyring。&lt;/p>
&lt;p>本文的判讀錨點是：加密要服務於 threat model。若風險是磁碟遺失，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/at-rest-encryption/" data-link-title="At-Rest Encryption" data-link-desc="說明資料落到儲存媒介前的加密層，以及它對應的威脅模型">at-rest encryption&lt;/a> 是重點；若風險是網路攔截，TLS 是重點；若風險是內部濫用，還需要 role、audit、masking 與 SIEM。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 MySQL 8.4 security claim。實作前先查 &lt;a href="https://dev.mysql.com/doc/refman/8.2/en/innodb-data-encryption.html">InnoDB data-at-rest encryption&lt;/a>、&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/keyring.html">MySQL keyring&lt;/a> 與 &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/show-binary-log-status.html">SHOW BINARY LOG STATUS&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="protection-layers">Protection Layers&lt;/h2>
&lt;p>Protection layers 的核心責任是把保護面分層。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>Evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>At-rest encryption&lt;/td>
 &lt;td>data file、redo、undo、temp&lt;/td>
 &lt;td>encryption setting、keyring status&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>In-transit TLS&lt;/td>
 &lt;td>client / replica / admin connection&lt;/td>
 &lt;td>TLS mode、certificate、cipher&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup encryption&lt;/td>
 &lt;td>dump、snapshot、physical backup&lt;/td>
 &lt;td>encrypted artifact、restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Key management&lt;/td>
 &lt;td>key generation、rotation、access&lt;/td>
 &lt;td>KMS / keyring log、rotation record&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Credential governance&lt;/td>
 &lt;td>user password、secret、rotation&lt;/td>
 &lt;td>grant review、secret age&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些層級要一起設計。資料檔加密後，backup 若以明文落到 object storage，保護鏈仍然破洞；TLS 開啟後，client 若允許 insecure fallback，也會失去網路保護。&lt;/p>
&lt;h2 id="keyring-boundary">Keyring Boundary&lt;/h2>
&lt;p>Keyring boundary 的核心責任是定義 MySQL 如何取得與保護 encryption key。MySQL 支援 keyring component / plugin 與外部 KMS 整合；managed MySQL 可能由 provider 接管 key storage。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>部署型態&lt;/th>
 &lt;th>key 責任&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Self-managed&lt;/td>
 &lt;td>自行部署 keyring / KMS&lt;/td>
 &lt;td>key file permission、backup、rotation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Managed MySQL&lt;/td>
 &lt;td>provider KMS / customer-managed key&lt;/td>
 &lt;td>region、rotation、audit、restore&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Container lab&lt;/td>
 &lt;td>dev-only keyring&lt;/td>
 &lt;td>避免和 production policy 混用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Keyring 要進入 backup / restore drill。還原 database 時，只有 data file 而沒有對應 key，restore 會失敗；runbook 要保存 key dependency 與 emergency access。&lt;/p>
&lt;h2 id="tls-policy">TLS Policy&lt;/h2>
&lt;p>TLS policy 的核心責任是讓 client connection、replication connection 與 admin connection 都有明確安全等級。&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>Application&lt;/td>
 &lt;td>require SSL、verify CA / identity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication&lt;/td>
 &lt;td>source / replica TLS、cert expiry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Admin&lt;/td>
 &lt;td>bastion / VPN / TLS、least privilege&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup tool&lt;/td>
 &lt;td>encrypted transport、secret scope&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>TLS 驗證要包含 certificate rotation。過期憑證造成的 downtime 很常見；runbook 要記錄 CA、server cert、client cert、rotation window 與 reload / restart 條件。&lt;/p></description><content:encoded><![CDATA[<p>MySQL encryption / TLS / key management 的核心責任是把資料庫保護拆成儲存加密、傳輸加密、<a href="/blog/backend/knowledge-cards/key-management/" data-link-title="Key Management" data-link-desc="說明加密金鑰如何產生、保存、輪替，以及還原時如何依賴金鑰">金鑰生命週期</a>與連線憑證治理。Encryption 是多層保護設計；它涵蓋 InnoDB tablespace、redo / undo、binary log、backup artifact、client connection 與 keyring。</p>
<p>本文的判讀錨點是：加密要服務於 threat model。若風險是磁碟遺失，<a href="/blog/backend/knowledge-cards/at-rest-encryption/" data-link-title="At-Rest Encryption" data-link-desc="說明資料落到儲存媒介前的加密層，以及它對應的威脅模型">at-rest encryption</a> 是重點；若風險是網路攔截，TLS 是重點；若風險是內部濫用，還需要 role、audit、masking 與 SIEM。</p>
<p>官方文件路由的核心責任是固定 MySQL 8.4 security claim。實作前先查 <a href="https://dev.mysql.com/doc/refman/8.2/en/innodb-data-encryption.html">InnoDB data-at-rest encryption</a>、<a href="https://dev.mysql.com/doc/refman/8.0/en/keyring.html">MySQL keyring</a> 與 <a href="https://dev.mysql.com/doc/refman/8.4/en/show-binary-log-status.html">SHOW BINARY LOG STATUS</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="protection-layers">Protection Layers</h2>
<p>Protection layers 的核心責任是把保護面分層。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>主要責任</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>At-rest encryption</td>
          <td>data file、redo、undo、temp</td>
          <td>encryption setting、keyring status</td>
      </tr>
      <tr>
          <td>In-transit TLS</td>
          <td>client / replica / admin connection</td>
          <td>TLS mode、certificate、cipher</td>
      </tr>
      <tr>
          <td>Backup encryption</td>
          <td>dump、snapshot、physical backup</td>
          <td>encrypted artifact、restore drill</td>
      </tr>
      <tr>
          <td>Key management</td>
          <td>key generation、rotation、access</td>
          <td>KMS / keyring log、rotation record</td>
      </tr>
      <tr>
          <td>Credential governance</td>
          <td>user password、secret、rotation</td>
          <td>grant review、secret age</td>
      </tr>
  </tbody>
</table>
<p>這些層級要一起設計。資料檔加密後，backup 若以明文落到 object storage，保護鏈仍然破洞；TLS 開啟後，client 若允許 insecure fallback，也會失去網路保護。</p>
<h2 id="keyring-boundary">Keyring Boundary</h2>
<p>Keyring boundary 的核心責任是定義 MySQL 如何取得與保護 encryption key。MySQL 支援 keyring component / plugin 與外部 KMS 整合；managed MySQL 可能由 provider 接管 key storage。</p>
<table>
  <thead>
      <tr>
          <th>部署型態</th>
          <th>key 責任</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Self-managed</td>
          <td>自行部署 keyring / KMS</td>
          <td>key file permission、backup、rotation</td>
      </tr>
      <tr>
          <td>Managed MySQL</td>
          <td>provider KMS / customer-managed key</td>
          <td>region、rotation、audit、restore</td>
      </tr>
      <tr>
          <td>Container lab</td>
          <td>dev-only keyring</td>
          <td>避免和 production policy 混用</td>
      </tr>
  </tbody>
</table>
<p>Keyring 要進入 backup / restore drill。還原 database 時，只有 data file 而沒有對應 key，restore 會失敗；runbook 要保存 key dependency 與 emergency access。</p>
<h2 id="tls-policy">TLS Policy</h2>
<p>TLS policy 的核心責任是讓 client connection、replication connection 與 admin connection 都有明確安全等級。</p>
<table>
  <thead>
      <tr>
          <th>連線類型</th>
          <th>建議檢查</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application</td>
          <td>require SSL、verify CA / identity</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>source / replica TLS、cert expiry</td>
      </tr>
      <tr>
          <td>Admin</td>
          <td>bastion / VPN / TLS、least privilege</td>
      </tr>
      <tr>
          <td>Backup tool</td>
          <td>encrypted transport、secret scope</td>
      </tr>
  </tbody>
</table>
<p>TLS 驗證要包含 certificate rotation。過期憑證造成的 downtime 很常見；runbook 要記錄 CA、server cert、client cert、rotation window 與 reload / restart 條件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;require_secure_transport&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Ssl_cipher&#39;</span><span class="p">;</span></span></span></code></pre></div><p>這些查詢只能提供 connection 層 evidence。正式驗證還要從 client 設定確認 <code>ssl-mode</code> 是否驗證 CA / identity。</p>
<h2 id="backup-and-binlog-encryption">Backup and Binlog Encryption</h2>
<p>Backup and binlog encryption 的核心責任是保護資料離開 primary 後的生命週期。MySQL backup、binlog、logical dump、object storage、replica seed 都可能含敏感資料。</p>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>保護方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Logical dump</td>
          <td>client-side encryption、storage policy</td>
      </tr>
      <tr>
          <td>Physical backup</td>
          <td>backup tool encryption、KMS</td>
      </tr>
      <tr>
          <td>Binlog</td>
          <td>encrypted storage、restricted access</td>
      </tr>
      <tr>
          <td>Snapshot</td>
          <td>volume encryption、snapshot policy</td>
      </tr>
      <tr>
          <td>Restore copy</td>
          <td>isolated environment、secret scoping</td>
      </tr>
  </tbody>
</table>
<p>Restore drill 要確認加密 artifact 可被解密並啟動。只有成功產出 encrypted backup，還不足以證明災難時能恢復。</p>
<h2 id="rotation-runbook">Rotation Runbook</h2>
<p>Rotation runbook 的核心責任是讓 key、certificate、password 都可定期更換。</p>
<ol>
<li>Inventory：列出 DB user、TLS cert、KMS key、backup key。</li>
<li>Impact：確認哪些 client / replica / backup job 使用它。</li>
<li>Staging：先在 staging 旋轉並跑 smoke test。</li>
<li>Rollout：使用雙憑證 / 雙 secret window。</li>
<li>Validation：查連線、replication、backup、restore。</li>
<li>Cleanup：移除舊 key / cert / secret。</li>
</ol>
<p>Rotation 要設 calendar 與 owner。安全設定長期無人輪替時，incident 後會難以判斷 exposure window。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是提前列出加密常見事故。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TLS fallback</td>
          <td>client 仍可明文連線</td>
          <td>require secure transport、client verify</td>
      </tr>
      <tr>
          <td>Cert expiry</td>
          <td>application connection failure</td>
          <td>rotation alert、dual cert window</td>
      </tr>
      <tr>
          <td>Missing keyring</td>
          <td>restore / startup failure</td>
          <td>key backup、KMS access drill</td>
      </tr>
      <tr>
          <td>Plain backup</td>
          <td>storage artifact 未加密</td>
          <td>backup pipeline policy</td>
      </tr>
      <tr>
          <td>Overbroad secret</td>
          <td>admin / app 共用 credential</td>
          <td>role split、secret rotation</td>
      </tr>
  </tbody>
</table>
<p>安全 runbook 要和 audit log 串接。Key rotation、failed TLS、privilege change、restore access 都應留下可追溯紀錄。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Encryption / TLS / key management 完成後，操作證據讀 <a href="../audit-log-siem/">Audit Log + SIEM</a>；備份恢復讀 <a href="../pitr-backup/">PITR / Backup</a>；資料保護治理讀 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>。</p>
]]></content:encoded></item><item><title>MySQL Hands-on 操作路線</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/</guid><description>&lt;p>MySQL hands-on 操作路線的核心責任是把 MySQL deep article 的設定與 failure mode 轉成可演練流程。這一層對齊 LLM &lt;code>hands-on/&lt;/code>：讀者能跑出 config、metric、validation query 與 rollback evidence。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>產出 artifact&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="local-lab-quickstart/">Local lab quickstart&lt;/a>&lt;/td>
 &lt;td>MySQL container、sample schema、baseline workload&lt;/td>
 &lt;td>local DSN、schema log、basic metric snapshot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="proxysql-routing-lab/">ProxySQL routing lab&lt;/a>&lt;/td>
 &lt;td>read/write split、lag-aware routing、runtime / disk config&lt;/td>
 &lt;td>ProxySQL config、routing evidence、drift note&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="online-schema-change-lab/">Online schema change lab&lt;/a>&lt;/td>
 &lt;td>gh-ost / pt-osc cutover、metadata lock、rollback&lt;/td>
 &lt;td>OSC command、cutover note、lock evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="replication-failover-lab/">Replication failover lab&lt;/a>&lt;/td>
 &lt;td>GTID replica、semi-sync、Orchestrator / manual failover&lt;/td>
 &lt;td>topology map、lag evidence、failover timeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="backup-restore-drill/">Backup restore drill&lt;/a>&lt;/td>
 &lt;td>logical / physical backup、binlog recovery、restore validation&lt;/td>
 &lt;td>restore record、RPO / RTO evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="vitess-sandbox-route/">Vitess sandbox route&lt;/a>&lt;/td>
 &lt;td>keyspace、VSchema、VTGate / VTTablet sandbox&lt;/td>
 &lt;td>sandbox topology、routing sample、shard key note&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>MySQL hands-on 章節要保留「高併發簡單 OLTP + 分片生態」的服務語言。操作章節不只給指令，也要說明 command 產出的 evidence 如何回到 replication、schema change、connection routing 或 sharding decision。&lt;/p>
&lt;h2 id="引用路徑">引用路徑&lt;/h2>
&lt;ul>
&lt;li>上游：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL overview&lt;/a>&lt;/li>
&lt;li>Deep article：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &amp;#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &amp;#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL Config&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess Sharding&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>MySQL hands-on 操作路線的核心責任是把 MySQL deep article 的設定與 failure mode 轉成可演練流程。這一層對齊 LLM <code>hands-on/</code>：讀者能跑出 config、metric、validation query 與 rollback evidence。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>產出 artifact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="local-lab-quickstart/">Local lab quickstart</a></td>
          <td>MySQL container、sample schema、baseline workload</td>
          <td>local DSN、schema log、basic metric snapshot</td>
      </tr>
      <tr>
          <td><a href="proxysql-routing-lab/">ProxySQL routing lab</a></td>
          <td>read/write split、lag-aware routing、runtime / disk config</td>
          <td>ProxySQL config、routing evidence、drift note</td>
      </tr>
      <tr>
          <td><a href="online-schema-change-lab/">Online schema change lab</a></td>
          <td>gh-ost / pt-osc cutover、metadata lock、rollback</td>
          <td>OSC command、cutover note、lock evidence</td>
      </tr>
      <tr>
          <td><a href="replication-failover-lab/">Replication failover lab</a></td>
          <td>GTID replica、semi-sync、Orchestrator / manual failover</td>
          <td>topology map、lag evidence、failover timeline</td>
      </tr>
      <tr>
          <td><a href="backup-restore-drill/">Backup restore drill</a></td>
          <td>logical / physical backup、binlog recovery、restore validation</td>
          <td>restore record、RPO / RTO evidence</td>
      </tr>
      <tr>
          <td><a href="vitess-sandbox-route/">Vitess sandbox route</a></td>
          <td>keyspace、VSchema、VTGate / VTTablet sandbox</td>
          <td>sandbox topology、routing sample、shard key note</td>
      </tr>
  </tbody>
</table>
<h2 id="設計原則">設計原則</h2>
<p>MySQL hands-on 章節要保留「高併發簡單 OLTP + 分片生態」的服務語言。操作章節不只給指令，也要說明 command 產出的 evidence 如何回到 replication、schema change、connection routing 或 sharding decision。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL overview</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL Config</a>、<a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>、<a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>、<a href="/blog/backend/01-database/vendors/mysql/vitess-sharding/" data-link-title="MySQL Vitess Sharding：VTGate / VTTablet / VReplication / VSchema 四件套協作" data-link-desc="Vitess 不只是 MySQL sharding proxy、是 4 個 component 協作的完整 sharding 系統 — VTGate（query routing layer）、VTTablet（per-MySQL agent）、VReplication（跨 shard 資料移動）、VSchema（sharding metadata）。本文走 4 件套各自責任、keyspace / shard / tablet 架構、shard key 設計（Vindex）、配置 step-by-step、5 production 踩雷（cross-shard transaction / VStream lag / Vindex 不均勻 / resharding 切流 / VReplication 卡住）、跟自管 sharding 跟 PlanetScale 的對比">Vitess Sharding</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL HeatWave OLAP Add-on</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/heatwave-olap-addon/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/heatwave-olap-addon/</guid><description>&lt;p>MySQL HeatWave OLAP add-on 的核心責任是判斷 OLTP database 內建 analytics 加速何時比拆出 OLAP 系統更划算。HeatWave 這類 add-on 的價值是降低資料搬運與平台數量，但它也把 analytics workload、成本、freshness 與 query governance 帶回 MySQL 生態。&lt;/p>
&lt;p>本文的判讀錨點是：OLAP add-on 做的是把分析查詢從 OLTP 路徑&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/olap-offload/" data-link-title="OLAP Offload" data-link-desc="說明如何把分析型查詢從 OLTP 主庫卸載，以保護線上交易效能">卸載&lt;/a>到專用引擎，解決特定 analytics workload 的 proximity 問題，而非 data warehouse 的完整替代。選型要看資料量、query pattern、freshness、concurrency、成本與團隊能力。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 HeatWave claim。實作前先查 &lt;a href="https://dev.mysql.com/doc/heatwave/en/index.html">MySQL HeatWave User Guide&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="workload-fit">Workload Fit&lt;/h2>
&lt;p>Workload fit 的核心責任是找出 HeatWave 類 OLAP add-on 的合理位置。&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>MySQL 資料為主要分析來源&lt;/td>
 &lt;td>減少 ETL / CDC 複雜度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard 需要較新資料&lt;/td>
 &lt;td>freshness 比 warehouse batch 更重要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分析 query 可被明確界定&lt;/td>
 &lt;td>可控 workload 便於成本與容量管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Team 想降低平台數&lt;/td>
 &lt;td>MySQL 生態內完成 transactional + analytics&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>適合的 workload 通常是「MySQL 內資料、分析需求清楚、資料量可控」。若需要跨多資料源、複雜 semantic layer、長期資料湖與 ML feature store，warehouse / lakehouse 仍然更合適。&lt;/p>
&lt;h2 id="boundary-with-oltp">Boundary with OLTP&lt;/h2>
&lt;p>Boundary with OLTP 的核心責任是避免 analytics 壓力影響交易服務。OLTP query 要穩定、低延遲、可預測；OLAP query 常是大掃描、大聚合、長時間。&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>Resource&lt;/td>
 &lt;td>OLAP 是否隔離 CPU / memory / storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>analytic data 和 source 差多久&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query control&lt;/td>
 &lt;td>誰能跑 heavy query、如何限流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>add-on node、storage、egress&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident&lt;/td>
 &lt;td>OLAP 故障是否影響 OLTP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>OLAP add-on 要有 query admission policy。任何人都能跑任意分析 SQL，會把成本與穩定性風險放大。&lt;/p>
&lt;h2 id="freshness-and-evidence">Freshness and Evidence&lt;/h2>
&lt;p>Freshness and evidence 的核心責任是定義分析結果多新。Dashboard、營運報表、風控、推薦特徵對 freshness 的要求不同。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Freshness 等級&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>秒到分鐘&lt;/td>
 &lt;td>operational dashboard、風控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>小時&lt;/td>
 &lt;td>商業報表、營運分析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>天&lt;/td>
 &lt;td>財務結算、長期趨勢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Freshness 要被量測。Runbook 要記錄 last load / sync time、query latency、failed refresh、data gap 與 fallback dashboard。&lt;/p>
&lt;h2 id="cost-model">Cost Model&lt;/h2>
&lt;p>Cost model 的核心責任是比較 add-on 和獨立 OLAP 系統。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>成本項&lt;/th>
 &lt;th>HeatWave 類 add-on&lt;/th>
 &lt;th>獨立 warehouse&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Data movement&lt;/td>
 &lt;td>較少 ETL&lt;/td>
 &lt;td>需要 CDC / batch pipeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compute&lt;/td>
 &lt;td>add-on capacity&lt;/td>
 &lt;td>warehouse compute / auto scaling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>MySQL ecosystem 內&lt;/td>
 &lt;td>separate storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Governance&lt;/td>
 &lt;td>MySQL 權限延伸&lt;/td>
 &lt;td>data platform governance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lock-in&lt;/td>
 &lt;td>provider-specific&lt;/td>
 &lt;td>warehouse-specific&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>成本比較要包含人力。少一條 ETL pipeline 可能節省大量維運；但 provider-specific query 與管理模型也會增加 exit cost。&lt;/p></description><content:encoded><![CDATA[<p>MySQL HeatWave OLAP add-on 的核心責任是判斷 OLTP database 內建 analytics 加速何時比拆出 OLAP 系統更划算。HeatWave 這類 add-on 的價值是降低資料搬運與平台數量，但它也把 analytics workload、成本、freshness 與 query governance 帶回 MySQL 生態。</p>
<p>本文的判讀錨點是：OLAP add-on 做的是把分析查詢從 OLTP 路徑<a href="/blog/backend/knowledge-cards/olap-offload/" data-link-title="OLAP Offload" data-link-desc="說明如何把分析型查詢從 OLTP 主庫卸載，以保護線上交易效能">卸載</a>到專用引擎，解決特定 analytics workload 的 proximity 問題，而非 data warehouse 的完整替代。選型要看資料量、query pattern、freshness、concurrency、成本與團隊能力。</p>
<p>官方文件路由的核心責任是固定 HeatWave claim。實作前先查 <a href="https://dev.mysql.com/doc/heatwave/en/index.html">MySQL HeatWave User Guide</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="workload-fit">Workload Fit</h2>
<p>Workload fit 的核心責任是找出 HeatWave 類 OLAP add-on 的合理位置。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>適合原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MySQL 資料為主要分析來源</td>
          <td>減少 ETL / CDC 複雜度</td>
      </tr>
      <tr>
          <td>Dashboard 需要較新資料</td>
          <td>freshness 比 warehouse batch 更重要</td>
      </tr>
      <tr>
          <td>分析 query 可被明確界定</td>
          <td>可控 workload 便於成本與容量管理</td>
      </tr>
      <tr>
          <td>Team 想降低平台數</td>
          <td>MySQL 生態內完成 transactional + analytics</td>
      </tr>
  </tbody>
</table>
<p>適合的 workload 通常是「MySQL 內資料、分析需求清楚、資料量可控」。若需要跨多資料源、複雜 semantic layer、長期資料湖與 ML feature store，warehouse / lakehouse 仍然更合適。</p>
<h2 id="boundary-with-oltp">Boundary with OLTP</h2>
<p>Boundary with OLTP 的核心責任是避免 analytics 壓力影響交易服務。OLTP query 要穩定、低延遲、可預測；OLAP query 常是大掃描、大聚合、長時間。</p>
<table>
  <thead>
      <tr>
          <th>審查面</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Resource</td>
          <td>OLAP 是否隔離 CPU / memory / storage</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>analytic data 和 source 差多久</td>
      </tr>
      <tr>
          <td>Query control</td>
          <td>誰能跑 heavy query、如何限流</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>add-on node、storage、egress</td>
      </tr>
      <tr>
          <td>Incident</td>
          <td>OLAP 故障是否影響 OLTP</td>
      </tr>
  </tbody>
</table>
<p>OLAP add-on 要有 query admission policy。任何人都能跑任意分析 SQL，會把成本與穩定性風險放大。</p>
<h2 id="freshness-and-evidence">Freshness and Evidence</h2>
<p>Freshness and evidence 的核心責任是定義分析結果多新。Dashboard、營運報表、風控、推薦特徵對 freshness 的要求不同。</p>
<table>
  <thead>
      <tr>
          <th>Freshness 等級</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>秒到分鐘</td>
          <td>operational dashboard、風控</td>
      </tr>
      <tr>
          <td>小時</td>
          <td>商業報表、營運分析</td>
      </tr>
      <tr>
          <td>天</td>
          <td>財務結算、長期趨勢</td>
      </tr>
  </tbody>
</table>
<p>Freshness 要被量測。Runbook 要記錄 last load / sync time、query latency、failed refresh、data gap 與 fallback dashboard。</p>
<h2 id="cost-model">Cost Model</h2>
<p>Cost model 的核心責任是比較 add-on 和獨立 OLAP 系統。</p>
<table>
  <thead>
      <tr>
          <th>成本項</th>
          <th>HeatWave 類 add-on</th>
          <th>獨立 warehouse</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Data movement</td>
          <td>較少 ETL</td>
          <td>需要 CDC / batch pipeline</td>
      </tr>
      <tr>
          <td>Compute</td>
          <td>add-on capacity</td>
          <td>warehouse compute / auto scaling</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>MySQL ecosystem 內</td>
          <td>separate storage</td>
      </tr>
      <tr>
          <td>Governance</td>
          <td>MySQL 權限延伸</td>
          <td>data platform governance</td>
      </tr>
      <tr>
          <td>Lock-in</td>
          <td>provider-specific</td>
          <td>warehouse-specific</td>
      </tr>
  </tbody>
</table>
<p>成本比較要包含人力。少一條 ETL pipeline 可能節省大量維運；但 provider-specific query 與管理模型也會增加 exit cost。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go conditions 的核心責任是避免把 OLAP add-on 推到資料平台的位置。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>分析跨多來源</td>
          <td>warehouse / lakehouse</td>
      </tr>
      <tr>
          <td>查詢需要 semantic layer / BI governance</td>
          <td>dedicated analytics platform</td>
      </tr>
      <tr>
          <td>長期歷史資料遠大於 OLTP</td>
          <td>warehouse / object storage</td>
      </tr>
      <tr>
          <td>ML feature / offline training</td>
          <td>feature store / lakehouse</td>
      </tr>
      <tr>
          <td>成本需要獨立 chargeback</td>
          <td>separate OLAP environment</td>
      </tr>
  </tbody>
</table>
<p>HeatWave 類能力適合 MySQL-centered analytics。當分析需求超出單一 OLTP source，資料平台會比 add-on 更清楚。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>HeatWave OLAP add-on 完成後，MySQL query 基礎讀 <a href="../query-optimization/">Query Optimization</a>；資料平台邊界讀 backend analytics / warehouse 章節；若要保留 MySQL OLTP 並外接 CDC，讀 <a href="../binlog-cdc/">Binlog CDC</a>。</p>
]]></content:encoded></item><item><title>MySQL Local Lab Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/local-lab-quickstart/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/local-lab-quickstart/</guid><description>&lt;p>MySQL local lab quickstart 的核心責任是建立後續 ProxySQL、OSC、replication、backup 與 Vitess sandbox 共用的本地環境。這個 lab 提供可重建 MySQL instance、baseline schema、seed data 與 basic evidence。&lt;/p>
&lt;p>本文的驗收標準是：你能啟動 MySQL、套用 schema、跑 sample workload、取得 processlist / InnoDB status / table count，並能 teardown 重建。&lt;/p>
&lt;h2 id="docker-compose">Docker Compose&lt;/h2>
&lt;p>Docker Compose 的核心責任是讓 lab 環境可重建。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mysql&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql:8.4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root_pw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_DATABASE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">appdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app_user&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app_pw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;33069:3306&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--performance-schema=ON&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--log-bin=mysql-bin&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--server-id=1&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">MYSQL_PWD&lt;/span>&lt;span class="o">=&lt;/span>app_pw
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user appdb -e &lt;span class="s2">&amp;#34;SELECT VERSION();&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="baseline-schema">Baseline Schema&lt;/h2>
&lt;p>Baseline schema 的核心責任是建立可測 transaction、index、binlog 與 OSC 的模型。&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE accounts (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s"> id BIGINT PRIMARY KEY AUTO_INCREMENT,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s"> tenant_id CHAR(36) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> owner_name VARCHAR(128) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> status ENUM(&amp;#39;active&amp;#39;, &amp;#39;closed&amp;#39;) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> KEY idx_accounts_tenant (tenant_id)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">) ENGINE=InnoDB;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE ledger_entries (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> id BIGINT PRIMARY KEY AUTO_INCREMENT,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> account_id BIGINT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> amount_cents BIGINT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> idempotency_key VARCHAR(128) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> UNIQUE KEY uk_ledger_idempotency (idempotency_key),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> KEY idx_ledger_account_created (account_id, created_at),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> CONSTRAINT fk_ledger_account FOREIGN KEY (account_id) REFERENCES accounts(id)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">) ENGINE=InnoDB;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="seed-and-evidence">Seed and Evidence&lt;/h2>
&lt;p>Seed and evidence 的核心責任是產生可重跑資料與 baseline。&lt;/p></description><content:encoded><![CDATA[<p>MySQL local lab quickstart 的核心責任是建立後續 ProxySQL、OSC、replication、backup 與 Vitess sandbox 共用的本地環境。這個 lab 提供可重建 MySQL instance、baseline schema、seed data 與 basic evidence。</p>
<p>本文的驗收標準是：你能啟動 MySQL、套用 schema、跑 sample workload、取得 processlist / InnoDB status / table count，並能 teardown 重建。</p>
<h2 id="docker-compose">Docker Compose</h2>
<p>Docker Compose 的核心責任是讓 lab 環境可重建。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">mysql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:8.4</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">root_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">appdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_USER</span><span class="p">:</span><span class="w"> </span><span class="l">app_user</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">app_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;33069:3306&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--performance-schema=ON&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--log-bin=mysql-bin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--server-id=1&#34;</span></span></span></code></pre></div><p>啟動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose up -d
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">MYSQL_PWD</span><span class="o">=</span>app_pw
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SELECT VERSION();&#34;</span></span></span></code></pre></div><h2 id="baseline-schema">Baseline Schema</h2>
<p>Baseline schema 的核心責任是建立可測 transaction、index、binlog 與 OSC 的模型。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">CREATE TABLE accounts (
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  id BIGINT PRIMARY KEY AUTO_INCREMENT,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  tenant_id CHAR(36) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">  owner_name VARCHAR(128) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  status ENUM(&#39;active&#39;, &#39;closed&#39;) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  KEY idx_accounts_tenant (tenant_id)
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">) ENGINE=InnoDB;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">CREATE TABLE ledger_entries (
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  id BIGINT PRIMARY KEY AUTO_INCREMENT,
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  account_id BIGINT NOT NULL,
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  amount_cents BIGINT NOT NULL,
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  idempotency_key VARCHAR(128) NOT NULL,
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  UNIQUE KEY uk_ledger_idempotency (idempotency_key),
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  KEY idx_ledger_account_created (account_id, created_at),
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  CONSTRAINT fk_ledger_account FOREIGN KEY (account_id) REFERENCES accounts(id)
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">) ENGINE=InnoDB;
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><h2 id="seed-and-evidence">Seed and Evidence</h2>
<p>Seed and evidence 的核心責任是產生可重跑資料與 baseline。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">INSERT INTO accounts(tenant_id, owner_name, status)
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">VALUES (&#39;tenant-a&#39;, &#39;Ada&#39;, &#39;active&#39;), (&#39;tenant-b&#39;, &#39;Lin&#39;, &#39;active&#39;);
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key)
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">VALUES (1, 1000, &#39;seed-ada-1&#39;), (1, -200, &#39;seed-ada-2&#39;), (2, 500, &#39;seed-lin-1&#39;);
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">GROUP BY a.owner_name;
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Basic evidence：</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW FULL PROCESSLIST;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW TABLE STATUS;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW ENGINE INNODB STATUS\\G&#34;</span></span></span></code></pre></div><h2 id="teardown">Teardown</h2>
<p>Teardown 的核心責任是讓 lab 可重跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose down -v</span></span></code></pre></div><p>完成本篇後，backup 進入 <a href="../backup-restore-drill/">Backup Restore Drill</a>；schema change 進入 <a href="../online-schema-change-lab/">Online Schema Change Lab</a>；routing 進入 <a href="../proxysql-routing-lab/">ProxySQL Routing Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Metadata Lock Deep Dive</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/metadata-lock-deep-dive/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/metadata-lock-deep-dive/</guid><description>&lt;p>MySQL metadata lock deep dive 的核心責任是說明 DDL、transaction 與 table metadata 之間的阻塞關係。MySQL 在查詢 table 時會取得 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metadata-lock/" data-link-title="Metadata Lock" data-link-desc="說明 DDL 與既有交易如何在 table metadata 層互相排隊與阻塞">metadata lock&lt;/a>；DDL 需要等待既有 metadata lock 釋放，等待中的 DDL 又會阻塞後續查詢，形成 production 常見雪崩。&lt;/p>
&lt;p>本文的判讀錨點是：MDL 事故通常來自 DDL 排隊在長交易後面，並把後續 query 一起擋住。解法要同時處理 long transaction、DDL window、OSC 工具與 observability。&lt;/p>
&lt;h2 id="lock-lifecycle">Lock Lifecycle&lt;/h2>
&lt;p>Lock lifecycle 的核心責任是建立 MDL 心智模型。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>行為&lt;/th>
 &lt;th>MDL 影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>SELECT&lt;/code> / DML&lt;/td>
 &lt;td>取得 table metadata lock，交易結束釋放&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long transaction&lt;/td>
 &lt;td>延長 metadata lock 持有時間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ALTER TABLE&lt;/code>&lt;/td>
 &lt;td>等待相容鎖，期間可能阻塞後續 query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Online schema change&lt;/td>
 &lt;td>仍需 metadata lock 進行切換 / rename&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Idle transaction&lt;/td>
 &lt;td>看似無操作，仍可能持有 metadata lock&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>MDL 的風險在於排隊。當 &lt;code>ALTER TABLE&lt;/code> 等待 long transaction 時，後續新的 query 可能排在 DDL 後面，讓原本小變更變成服務不可用。&lt;/p>
&lt;h2 id="detection">Detection&lt;/h2>
&lt;p>Detection 的核心責任是快速找出誰持鎖、誰等待。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">performance_schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">metadata_locks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_SCHEMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;appdb&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_NAME&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOCK_STATUS&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>搭配 processlist：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PROCESSLIST&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Production dashboard 應監控 running DDL、metadata lock wait、long transaction age、threads running、blocked query count 與 replication lag。&lt;/p>
&lt;h2 id="ddl-risk-review">DDL Risk Review&lt;/h2>
&lt;p>DDL risk review 的核心責任是在變更前預測 MDL 風險。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>DDL 類型&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;th>控制方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Add nullable column&lt;/td>
 &lt;td>依版本 / algorithm 可能較低&lt;/td>
 &lt;td>staging dry run、algorithm check&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Add index&lt;/td>
 &lt;td>可能長時間操作與切換 lock&lt;/td>
 &lt;td>online DDL / OSC、低峰窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Change column type&lt;/td>
 &lt;td>table rebuild 風險高&lt;/td>
 &lt;td>ghost table / phased migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rename / swap table&lt;/td>
 &lt;td>短暫但關鍵 MDL&lt;/td>
 &lt;td>kill blocker、短窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drop column / table&lt;/td>
 &lt;td>destructive 且需鎖&lt;/td>
 &lt;td>backup、approval、blocked query watch&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>DDL review 要列出 algorithm、lock mode、預估時間、rollback、kill blocker policy 與 replication impact。&lt;/p>
&lt;h2 id="incident-runbook">Incident Runbook&lt;/h2>
&lt;p>Incident runbook 的核心責任是把 MDL 事故分流。&lt;/p></description><content:encoded><![CDATA[<p>MySQL metadata lock deep dive 的核心責任是說明 DDL、transaction 與 table metadata 之間的阻塞關係。MySQL 在查詢 table 時會取得 <a href="/blog/backend/knowledge-cards/metadata-lock/" data-link-title="Metadata Lock" data-link-desc="說明 DDL 與既有交易如何在 table metadata 層互相排隊與阻塞">metadata lock</a>；DDL 需要等待既有 metadata lock 釋放，等待中的 DDL 又會阻塞後續查詢，形成 production 常見雪崩。</p>
<p>本文的判讀錨點是：MDL 事故通常來自 DDL 排隊在長交易後面，並把後續 query 一起擋住。解法要同時處理 long transaction、DDL window、OSC 工具與 observability。</p>
<h2 id="lock-lifecycle">Lock Lifecycle</h2>
<p>Lock lifecycle 的核心責任是建立 MDL 心智模型。</p>
<table>
  <thead>
      <tr>
          <th>行為</th>
          <th>MDL 影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SELECT</code> / DML</td>
          <td>取得 table metadata lock，交易結束釋放</td>
      </tr>
      <tr>
          <td>Long transaction</td>
          <td>延長 metadata lock 持有時間</td>
      </tr>
      <tr>
          <td><code>ALTER TABLE</code></td>
          <td>等待相容鎖，期間可能阻塞後續 query</td>
      </tr>
      <tr>
          <td>Online schema change</td>
          <td>仍需 metadata lock 進行切換 / rename</td>
      </tr>
      <tr>
          <td>Idle transaction</td>
          <td>看似無操作，仍可能持有 metadata lock</td>
      </tr>
  </tbody>
</table>
<p>MDL 的風險在於排隊。當 <code>ALTER TABLE</code> 等待 long transaction 時，後續新的 query 可能排在 DDL 後面，讓原本小變更變成服務不可用。</p>
<h2 id="detection">Detection</h2>
<p>Detection 的核心責任是快速找出誰持鎖、誰等待。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">metadata_locks</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">OBJECT_SCHEMA</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;appdb&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">OBJECT_NAME</span><span class="p">,</span><span class="w"> </span><span class="n">LOCK_STATUS</span><span class="p">;</span></span></span></code></pre></div><p>搭配 processlist：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">FULL</span><span class="w"> </span><span class="n">PROCESSLIST</span><span class="p">;</span></span></span></code></pre></div><p>Production dashboard 應監控 running DDL、metadata lock wait、long transaction age、threads running、blocked query count 與 replication lag。</p>
<h2 id="ddl-risk-review">DDL Risk Review</h2>
<p>DDL risk review 的核心責任是在變更前預測 MDL 風險。</p>
<table>
  <thead>
      <tr>
          <th>DDL 類型</th>
          <th>風險</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Add nullable column</td>
          <td>依版本 / algorithm 可能較低</td>
          <td>staging dry run、algorithm check</td>
      </tr>
      <tr>
          <td>Add index</td>
          <td>可能長時間操作與切換 lock</td>
          <td>online DDL / OSC、低峰窗口</td>
      </tr>
      <tr>
          <td>Change column type</td>
          <td>table rebuild 風險高</td>
          <td>ghost table / phased migration</td>
      </tr>
      <tr>
          <td>Rename / swap table</td>
          <td>短暫但關鍵 MDL</td>
          <td>kill blocker、短窗口</td>
      </tr>
      <tr>
          <td>Drop column / table</td>
          <td>destructive 且需鎖</td>
          <td>backup、approval、blocked query watch</td>
      </tr>
  </tbody>
</table>
<p>DDL review 要列出 algorithm、lock mode、預估時間、rollback、kill blocker policy 與 replication impact。</p>
<h2 id="incident-runbook">Incident Runbook</h2>
<p>Incident runbook 的核心責任是把 MDL 事故分流。</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Identify blocker</td>
          <td>查 long transaction / metadata_locks</td>
      </tr>
      <tr>
          <td>Stop new DDL</td>
          <td>暫停 migration pipeline</td>
      </tr>
      <tr>
          <td>Decide kill</td>
          <td>依 owner / transaction age / impact</td>
      </tr>
      <tr>
          <td>Protect app</td>
          <td>降低 traffic、停 heavy endpoint</td>
      </tr>
      <tr>
          <td>Validate</td>
          <td>查 query 恢復、replication lag</td>
      </tr>
      <tr>
          <td>Retrospective</td>
          <td>補 DDL gate、long transaction alert</td>
      </tr>
  </tbody>
</table>
<p>Kill session 是高風險操作。決策要記錄 transaction owner、已執行時間、可能 rollback 成本與業務影響。</p>
<h2 id="osc-interaction">OSC Interaction</h2>
<p>OSC interaction 的核心責任是說明 gh-ost / pt-online-schema-change 仍需要 MDL 管理。Ghost table 工具把大部分 copy 與 backfill 移到旁路，但最後 cutover / rename 仍需要短暫 metadata lock。</p>
<table>
  <thead>
      <tr>
          <th>工具階段</th>
          <th>MDL 風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Create ghost table</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Copy / backfill</td>
          <td>主要是 load / replication lag</td>
      </tr>
      <tr>
          <td>Trigger / binlog</td>
          <td>依工具模式不同</td>
      </tr>
      <tr>
          <td>Cutover / rename</td>
          <td>關鍵 MDL window</td>
      </tr>
  </tbody>
</table>
<p>OSC runbook 要在 cutover 前檢查 long transaction。若 blocker 存在，先延後 cutover，而非硬切。</p>
<h2 id="prevention">Prevention</h2>
<p>Prevention 的核心責任是讓 MDL 事故在 release 前被擋下。</p>
<ol>
<li>Long transaction alert。</li>
<li>DDL dry run 與 algorithm / lock mode 記錄。</li>
<li>Migration window 與 kill blocker policy。</li>
<li>OSC cutover pre-check。</li>
<li>Application transaction timeout。</li>
<li>Read-only replica 上先測 schema change。</li>
</ol>
<p>MDL 是 MySQL schema governance 的核心議題。每個 production DDL 都要有 metadata lock plan。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Metadata lock deep dive 完成後，schema change 工具讀 <a href="../online-schema-change-tools/">Online Schema Change Tools</a>；lock 行為讀 <a href="../lock-contention/">Lock Contention</a>；操作演練讀 <a href="../hands-on/online-schema-change-lab/">Online Schema Change Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Multi-source Replication</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/multi-source-replication/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/multi-source-replication/</guid><description>&lt;p>MySQL multi-source replication 的核心責任是讓一個 replica 從多個 source 接收資料。這種拓撲常用於資料整併、分庫匯總、migration staging、報表集中或多個 bounded context 的 read consolidation。&lt;/p>
&lt;p>本文的判讀錨點是：multi-source replication 是 consolidation pattern，而非 multi-primary conflict resolution。每個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replication-channel/" data-link-title="Replication Channel" data-link-desc="說明多來源複製中，每個來源對應的獨立複製通道如何成為隔離單位">replication channel&lt;/a> 要有獨立 source、schema scope、lag、error handling 與 ownership。&lt;/p>
&lt;h2 id="use-cases">Use Cases&lt;/h2>
&lt;p>Use cases 的核心責任是確認 multi-source 解決的是整併需求。&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>Reporting replica&lt;/td>
 &lt;td>多個 source 匯入同一 read-only target&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration staging&lt;/td>
 &lt;td>新平台先接多個 source binlog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Regional fan-in&lt;/td>
 &lt;td>多區 local DB 匯總到中心&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shard consolidation&lt;/td>
 &lt;td>多 shard 同 schema 匯入 reporting DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audit / CDC sink&lt;/td>
 &lt;td>變更集中供後續 pipeline 使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Multi-source target 通常應 read-only。若 target 同時接受 application write，就要設計 conflict 與 ownership，複雜度會大幅提高。&lt;/p>
&lt;h2 id="channel-design">Channel Design&lt;/h2>
&lt;p>Channel design 的核心責任是把每個 source 隔離成可觀測單位。&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>Channel name&lt;/td>
 &lt;td>是否能看出 source / owner / purpose&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema scope&lt;/td>
 &lt;td>不同 source 是否寫入不同 schema / table&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GTID&lt;/td>
 &lt;td>GTID domain / collision policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter&lt;/td>
 &lt;td>replicate-do / ignore 規則是否可審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Credential&lt;/td>
 &lt;td>每個 channel 是否獨立 secret&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lag alert&lt;/td>
 &lt;td>channel-level lag 與 error&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Channel 命名要可讀。Incident 時看到 channel 名稱，就要知道哪個 source、哪個 team、哪個用途與是否可暫停。&lt;/p>
&lt;h2 id="conflict-boundary">Conflict Boundary&lt;/h2>
&lt;p>Conflict boundary 的核心責任是避免多個 source 寫同一份邏輯資料。Multi-source 沒有自動解決業務 conflict 的能力。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Conflict 類型&lt;/th>
 &lt;th>控制方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Primary key collision&lt;/td>
 &lt;td>shard key prefix、schema isolation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Duplicate natural key&lt;/td>
 &lt;td>source namespace、dedupe layer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Out-of-order update&lt;/td>
 &lt;td>source ownership、event timestamp&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Delete collision&lt;/td>
 &lt;td>tombstone policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DDL drift&lt;/td>
 &lt;td>migration coordination&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最安全的 pattern 是每個 source 寫自己的 schema 或帶 source namespace 的 table。若多 source 寫同一 table，必須先設計 key space 與 conflict policy。&lt;/p>
&lt;h2 id="monitoring">Monitoring&lt;/h2>
&lt;p>Monitoring 的核心責任是讓每個 channel 的狀態可見。&lt;/p></description><content:encoded><![CDATA[<p>MySQL multi-source replication 的核心責任是讓一個 replica 從多個 source 接收資料。這種拓撲常用於資料整併、分庫匯總、migration staging、報表集中或多個 bounded context 的 read consolidation。</p>
<p>本文的判讀錨點是：multi-source replication 是 consolidation pattern，而非 multi-primary conflict resolution。每個 <a href="/blog/backend/knowledge-cards/replication-channel/" data-link-title="Replication Channel" data-link-desc="說明多來源複製中，每個來源對應的獨立複製通道如何成為隔離單位">replication channel</a> 要有獨立 source、schema scope、lag、error handling 與 ownership。</p>
<h2 id="use-cases">Use Cases</h2>
<p>Use cases 的核心責任是確認 multi-source 解決的是整併需求。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>適合條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reporting replica</td>
          <td>多個 source 匯入同一 read-only target</td>
      </tr>
      <tr>
          <td>Migration staging</td>
          <td>新平台先接多個 source binlog</td>
      </tr>
      <tr>
          <td>Regional fan-in</td>
          <td>多區 local DB 匯總到中心</td>
      </tr>
      <tr>
          <td>Shard consolidation</td>
          <td>多 shard 同 schema 匯入 reporting DB</td>
      </tr>
      <tr>
          <td>Audit / CDC sink</td>
          <td>變更集中供後續 pipeline 使用</td>
      </tr>
  </tbody>
</table>
<p>Multi-source target 通常應 read-only。若 target 同時接受 application write，就要設計 conflict 與 ownership，複雜度會大幅提高。</p>
<h2 id="channel-design">Channel Design</h2>
<p>Channel design 的核心責任是把每個 source 隔離成可觀測單位。</p>
<table>
  <thead>
      <tr>
          <th>設計項</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Channel name</td>
          <td>是否能看出 source / owner / purpose</td>
      </tr>
      <tr>
          <td>Schema scope</td>
          <td>不同 source 是否寫入不同 schema / table</td>
      </tr>
      <tr>
          <td>GTID</td>
          <td>GTID domain / collision policy</td>
      </tr>
      <tr>
          <td>Filter</td>
          <td>replicate-do / ignore 規則是否可審查</td>
      </tr>
      <tr>
          <td>Credential</td>
          <td>每個 channel 是否獨立 secret</td>
      </tr>
      <tr>
          <td>Lag alert</td>
          <td>channel-level lag 與 error</td>
      </tr>
  </tbody>
</table>
<p>Channel 命名要可讀。Incident 時看到 channel 名稱，就要知道哪個 source、哪個 team、哪個用途與是否可暫停。</p>
<h2 id="conflict-boundary">Conflict Boundary</h2>
<p>Conflict boundary 的核心責任是避免多個 source 寫同一份邏輯資料。Multi-source 沒有自動解決業務 conflict 的能力。</p>
<table>
  <thead>
      <tr>
          <th>Conflict 類型</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Primary key collision</td>
          <td>shard key prefix、schema isolation</td>
      </tr>
      <tr>
          <td>Duplicate natural key</td>
          <td>source namespace、dedupe layer</td>
      </tr>
      <tr>
          <td>Out-of-order update</td>
          <td>source ownership、event timestamp</td>
      </tr>
      <tr>
          <td>Delete collision</td>
          <td>tombstone policy</td>
      </tr>
      <tr>
          <td>DDL drift</td>
          <td>migration coordination</td>
      </tr>
  </tbody>
</table>
<p>最安全的 pattern 是每個 source 寫自己的 schema 或帶 source namespace 的 table。若多 source 寫同一 table，必須先設計 key space 與 conflict policy。</p>
<h2 id="monitoring">Monitoring</h2>
<p>Monitoring 的核心責任是讓每個 channel 的狀態可見。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="n">CHANNEL</span><span class="w"> </span><span class="s1">&#39;source_a&#39;</span><span class="err">\</span><span class="k">G</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="n">CHANNEL</span><span class="w"> </span><span class="s1">&#39;source_b&#39;</span><span class="err">\</span><span class="k">G</span></span></span></code></pre></div><p>要觀測：</p>
<ol>
<li>IO thread / SQL thread status。</li>
<li>Seconds behind source。</li>
<li>Last IO error / SQL error。</li>
<li>Relay log growth。</li>
<li>GTID executed / retrieved。</li>
<li>Channel credential expiry。</li>
</ol>
<p>Lag 要分 channel 告警。總體 replica 健康不足以定位哪個 source 卡住。</p>
<h2 id="migration-pattern">Migration Pattern</h2>
<p>Migration pattern 的核心責任是把 multi-source 用在可回退的搬遷。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source audit</td>
          <td>schema、GTID、binlog format</td>
      </tr>
      <tr>
          <td>Target setup</td>
          <td>channel、filter、credential</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>dump / load、checksum</td>
      </tr>
      <tr>
          <td>Catch-up</td>
          <td>channel lag、error</td>
      </tr>
      <tr>
          <td>Read test</td>
          <td>report query、row count</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>read endpoint switch</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>stop channel、retention、secret</td>
      </tr>
  </tbody>
</table>
<p>Migration target 若只是 reporting，cutover 風險較低；若要成為 new primary，還要處理 write freeze、conflict、application route 與 rollback。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 multi-source 事故分 channel 處理。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single channel lag</td>
          <td>某 source 延遲</td>
          <td>查 source load、network、SQL error</td>
      </tr>
      <tr>
          <td>DDL drift</td>
          <td>replication SQL error</td>
          <td>migration coordination</td>
      </tr>
      <tr>
          <td>Key collision</td>
          <td>duplicate key error</td>
          <td>namespace / key rewrite</td>
      </tr>
      <tr>
          <td>Relay log growth</td>
          <td>target apply 慢</td>
          <td>調整 parallel apply、拆 workload</td>
      </tr>
      <tr>
          <td>Credential expired</td>
          <td>IO thread stopped</td>
          <td>rotate secret、resume channel</td>
      </tr>
  </tbody>
</table>
<p>Channel failure 要避免全局操作。只停問題 channel，保留其他 channel，能降低 blast radius。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Multi-source replication 完成後，基本拓撲讀 <a href="../replication-topology/">Replication Topology</a>；failover 讀 <a href="../orchestrator-failover/">Orchestrator Failover</a>；CDC 與 binlog 讀 <a href="../binlog-cdc/">Binlog CDC</a>。</p>
]]></content:encoded></item><item><title>MySQL Online Schema Change Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/online-schema-change-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/online-schema-change-lab/</guid><description>&lt;p>MySQL online schema change lab 的核心責任是讓讀者看到 schema change 的 metadata lock、algorithm、copy / cutover 與 validation evidence。這篇承接 &lt;a href="../../online-schema-change-tools/">Online Schema Change Tools&lt;/a> 與 &lt;a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能跑一個低風險 ALTER、觀察 metadata lock、記錄 validation query，並理解 gh-ost / pt-osc 的 cutover evidence。&lt;/p>
&lt;h2 id="direct-alter-baseline">Direct ALTER Baseline&lt;/h2>
&lt;p>Direct ALTER baseline 的核心責任是先看 MySQL 原生 DDL 的行為。&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">ALTER TABLE accounts ADD COLUMN email VARCHAR(255) NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW CREATE TABLE accounts\G
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>記錄 ALTER duration、algorithm、lock impact 與 table size。不同 MySQL 版本與 DDL 類型會有不同行為，production 要在 staging dry run。&lt;/p>
&lt;h2 id="metadata-lock-observation">Metadata Lock Observation&lt;/h2>
&lt;p>Metadata lock observation 的核心責任是看到 blocker。&lt;/p>
&lt;p>開 Session A：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">START&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TRANSACTION&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">accounts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>保持 transaction 開啟。Session B 執行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">accounts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">note&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">255&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Session C 查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_SCHEMA&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_NAME&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOCK_TYPE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOCK_STATUS&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OWNER_THREAD_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">performance_schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">metadata_locks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_SCHEMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;appdb&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完成觀察後，Session A &lt;code>COMMIT&lt;/code>。這段 lab 展示 long transaction 如何讓 DDL 等待。&lt;/p>
&lt;h2 id="osc-frame">OSC Frame&lt;/h2>
&lt;p>OSC frame 的核心責任是理解 gh-ost / pt-online-schema-change 的證據，而非要求每個 lab 都安裝工具。&lt;/p>
&lt;p>OSC runbook 要記錄：&lt;/p>
&lt;ol>
&lt;li>Source table、ghost table、migration statement。&lt;/li>
&lt;li>Copy progress、chunk size、throttle condition。&lt;/li>
&lt;li>Replication lag / load threshold。&lt;/li>
&lt;li>Cutover pre-check：long transaction、metadata lock、traffic。&lt;/li>
&lt;li>Cutover duration 與 validation query。&lt;/li>
&lt;li>Rollback / drop ghost table policy。&lt;/li>
&lt;/ol>
&lt;p>Cutover 前最重要的是 metadata lock pre-check。工具能降低大部分 copy 風險，但最後 rename / swap 仍需要短暫鎖。&lt;/p>
&lt;h2 id="validation">Validation&lt;/h2>
&lt;p>Validation 的核心責任是證明 schema change 後資料與 query 仍正確。&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">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM accounts;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM ledger_entries;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">EXPLAIN SELECT * FROM accounts WHERE tenant_id = &amp;#39;tenant-a&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>正式 migration 要補 row checksum、null rate、index usage、replication lag 與 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>MySQL online schema change lab 的核心責任是讓讀者看到 schema change 的 metadata lock、algorithm、copy / cutover 與 validation evidence。這篇承接 <a href="../../online-schema-change-tools/">Online Schema Change Tools</a> 與 <a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive</a>。</p>
<p>本文的驗收標準是：你能跑一個低風險 ALTER、觀察 metadata lock、記錄 validation query，並理解 gh-ost / pt-osc 的 cutover evidence。</p>
<h2 id="direct-alter-baseline">Direct ALTER Baseline</h2>
<p>Direct ALTER baseline 的核心責任是先看 MySQL 原生 DDL 的行為。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">ALTER TABLE accounts ADD COLUMN email VARCHAR(255) NULL;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SHOW CREATE TABLE accounts\G
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>記錄 ALTER duration、algorithm、lock impact 與 table size。不同 MySQL 版本與 DDL 類型會有不同行為，production 要在 staging dry run。</p>
<h2 id="metadata-lock-observation">Metadata Lock Observation</h2>
<p>Metadata lock observation 的核心責任是看到 blocker。</p>
<p>開 Session A：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">START</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p>保持 transaction 開啟。Session B 執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">note</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span></span></span></code></pre></div><p>Session C 查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">OBJECT_SCHEMA</span><span class="p">,</span><span class="w"> </span><span class="n">OBJECT_NAME</span><span class="p">,</span><span class="w"> </span><span class="n">LOCK_TYPE</span><span class="p">,</span><span class="w"> </span><span class="n">LOCK_STATUS</span><span class="p">,</span><span class="w"> </span><span class="n">OWNER_THREAD_ID</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">metadata_locks</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">OBJECT_SCHEMA</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;appdb&#39;</span><span class="p">;</span></span></span></code></pre></div><p>完成觀察後，Session A <code>COMMIT</code>。這段 lab 展示 long transaction 如何讓 DDL 等待。</p>
<h2 id="osc-frame">OSC Frame</h2>
<p>OSC frame 的核心責任是理解 gh-ost / pt-online-schema-change 的證據，而非要求每個 lab 都安裝工具。</p>
<p>OSC runbook 要記錄：</p>
<ol>
<li>Source table、ghost table、migration statement。</li>
<li>Copy progress、chunk size、throttle condition。</li>
<li>Replication lag / load threshold。</li>
<li>Cutover pre-check：long transaction、metadata lock、traffic。</li>
<li>Cutover duration 與 validation query。</li>
<li>Rollback / drop ghost table policy。</li>
</ol>
<p>Cutover 前最重要的是 metadata lock pre-check。工具能降低大部分 copy 風險，但最後 rename / swap 仍需要短暫鎖。</p>
<h2 id="validation">Validation</h2>
<p>Validation 的核心責任是證明 schema change 後資料與 query 仍正確。</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">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT COUNT(*) FROM accounts;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT COUNT(*) FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">EXPLAIN SELECT * FROM accounts WHERE tenant_id = &#39;tenant-a&#39;;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>正式 migration 要補 row checksum、null rate、index usage、replication lag 與 application smoke test。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的核心責任是形成交付 artifact。</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">Migration:
</span></span><span class="line"><span class="ln">2</span><span class="cl">DDL / OSC command:
</span></span><span class="line"><span class="ln">3</span><span class="cl">Table size:
</span></span><span class="line"><span class="ln">4</span><span class="cl">MDL pre-check:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Duration:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Validation:
</span></span><span class="line"><span class="ln">7</span><span class="cl">Rollback:
</span></span><span class="line"><span class="ln">8</span><span class="cl">Owner:</span></span></code></pre></div><p>完成本篇後，MDL 事故讀 <a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive</a>；工具選型讀 <a href="../../online-schema-change-tools/">Online Schema Change Tools</a>。</p>
]]></content:encoded></item><item><title>MySQL ProxySQL Routing Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/proxysql-routing-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/proxysql-routing-lab/</guid><description>&lt;p>MySQL ProxySQL routing lab 的核心責任是讓讀者看到 database proxy 如何把 application query 導向不同 hostgroup。這篇承接 &lt;a href="../../proxysql-config/">ProxySQL Config&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能定義 writer / reader hostgroup、建立 query rule、觀察 routing stats，並寫下 stale read 與 failover 風險。&lt;/p>
&lt;h2 id="hostgroup-model">Hostgroup Model&lt;/h2>
&lt;p>Hostgroup model 的核心責任是把 backend 分成 writer 與 reader。&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">hostgroup 10: writer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">hostgroup 20: reader&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在單節點 lab 中，writer / reader 可以先指向同一 MySQL；正式環境應用 replica 作 reader，並搭配 replication lag guard。&lt;/p>
&lt;h2 id="query-rule">Query Rule&lt;/h2>
&lt;p>Query rule 的核心責任是示範 routing policy。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- Conceptual ProxySQL admin commands. Adjust host / credential for your lab.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mysql_query_rules&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">active&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">match_pattern&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">destination_hostgroup&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;^SELECT&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;.*&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LOAD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MYSQL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">QUERY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RULES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RUNTIME&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">SAVE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MYSQL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">QUERY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RULES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DISK&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個規則把 &lt;code>SELECT&lt;/code> 導向 reader，其餘導向 writer。Production 要排除 &lt;code>SELECT ... FOR UPDATE&lt;/code>、transaction、read-after-write 與 session state。&lt;/p>
&lt;h2 id="routing-evidence">Routing Evidence&lt;/h2>
&lt;p>Routing evidence 的核心責任是確認 query 真的走到預期 hostgroup。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hostgroup&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">srv_host&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Queries&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">stats_mysql_connection_pool&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hits&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">stats_mysql_query_rules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Evidence 要和 application log 對齊。若某個 workflow 寫後立刻讀，routing rule 要保證它走 writer 或具備 freshness policy。&lt;/p>
&lt;h2 id="failure-note">Failure Note&lt;/h2>
&lt;p>Failure note 的核心責任是記錄 proxy 常見風險。&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>Stale read&lt;/td>
 &lt;td>lag guard、read-after-write to writer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction split&lt;/td>
 &lt;td>transaction pinning、query rule review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bad regex&lt;/td>
 &lt;td>query digest / allowlist&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backend unhealthy&lt;/td>
 &lt;td>health check、hostgroup failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Credential drift&lt;/td>
 &lt;td>ProxySQL user sync / secret rotation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>完成本篇後，完整設定讀 &lt;a href="../../proxysql-config/">ProxySQL Config&lt;/a>；replica 與 failover 讀 &lt;a href="../replication-failover-lab/">Replication Failover Lab&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>MySQL ProxySQL routing lab 的核心責任是讓讀者看到 database proxy 如何把 application query 導向不同 hostgroup。這篇承接 <a href="../../proxysql-config/">ProxySQL Config</a>。</p>
<p>本文的驗收標準是：你能定義 writer / reader hostgroup、建立 query rule、觀察 routing stats，並寫下 stale read 與 failover 風險。</p>
<h2 id="hostgroup-model">Hostgroup Model</h2>
<p>Hostgroup model 的核心責任是把 backend 分成 writer 與 reader。</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">hostgroup 10: writer
</span></span><span class="line"><span class="ln">2</span><span class="cl">hostgroup 20: reader</span></span></code></pre></div><p>在單節點 lab 中，writer / reader 可以先指向同一 MySQL；正式環境應用 replica 作 reader，並搭配 replication lag guard。</p>
<h2 id="query-rule">Query Rule</h2>
<p>Query rule 的核心責任是示範 routing policy。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Conceptual ProxySQL admin commands. Adjust host / credential for your lab.
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;.*&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p>這個規則把 <code>SELECT</code> 導向 reader，其餘導向 writer。Production 要排除 <code>SELECT ... FOR UPDATE</code>、transaction、read-after-write 與 session state。</p>
<h2 id="routing-evidence">Routing Evidence</h2>
<p>Routing evidence 的核心責任是確認 query 真的走到預期 hostgroup。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">srv_host</span><span class="p">,</span><span class="w"> </span><span class="n">Queries</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">stats_mysql_connection_pool</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">hits</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">stats_mysql_query_rules</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">rule_id</span><span class="p">;</span></span></span></code></pre></div><p>Evidence 要和 application log 對齊。若某個 workflow 寫後立刻讀，routing rule 要保證它走 writer 或具備 freshness policy。</p>
<h2 id="failure-note">Failure Note</h2>
<p>Failure note 的核心責任是記錄 proxy 常見風險。</p>
<table>
  <thead>
      <tr>
          <th>風險</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stale read</td>
          <td>lag guard、read-after-write to writer</td>
      </tr>
      <tr>
          <td>Transaction split</td>
          <td>transaction pinning、query rule review</td>
      </tr>
      <tr>
          <td>Bad regex</td>
          <td>query digest / allowlist</td>
      </tr>
      <tr>
          <td>Backend unhealthy</td>
          <td>health check、hostgroup failover</td>
      </tr>
      <tr>
          <td>Credential drift</td>
          <td>ProxySQL user sync / secret rotation</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，完整設定讀 <a href="../../proxysql-config/">ProxySQL Config</a>；replica 與 failover 讀 <a href="../replication-failover-lab/">Replication Failover Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Replication Failover Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/replication-failover-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/replication-failover-lab/</guid><description>&lt;p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 &lt;a href="../../replication-topology/">Replication Topology&lt;/a> 與 &lt;a href="../../orchestrator-failover/">Orchestrator Failover&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。&lt;/p>
&lt;h2 id="baseline-replication">Baseline Replication&lt;/h2>
&lt;p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REPLICA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="k">G&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BINARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Baseline 要記錄：&lt;/p>
&lt;ol>
&lt;li>Source host / replica host。&lt;/li>
&lt;li>GTID executed / retrieved。&lt;/li>
&lt;li>IO thread / SQL thread。&lt;/li>
&lt;li>Seconds behind source。&lt;/li>
&lt;li>Read endpoint / write endpoint。&lt;/li>
&lt;/ol>
&lt;h2 id="client-workload">Client Workload&lt;/h2>
&lt;p>Client workload 的核心責任是讓 failover 對 application 可見。&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="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> mysql -h &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$MYSQL_WRITE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -u app_user -papp_pw appdb &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> -e &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> sleep &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。&lt;/p>
&lt;h2 id="promotion-frame">Promotion Frame&lt;/h2>
&lt;p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。&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">failover_start:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">old_source:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">candidate_replica:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">lag_before:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">promotion_method:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">accepted_data_loss:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">operator:&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。&lt;/p>
&lt;h2 id="validation">Validation&lt;/h2>
&lt;p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">MAX&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;read_only&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;super_read_only&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。&lt;/p>
&lt;h2 id="client-route">Client Route&lt;/h2>
&lt;p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。&lt;/p>
&lt;p>檢查項目：&lt;/p>
&lt;ol>
&lt;li>Write endpoint 是否更新。&lt;/li>
&lt;li>ProxySQL writer hostgroup 是否切換。&lt;/li>
&lt;li>Application pool 是否清掉舊連線。&lt;/li>
&lt;li>Retry 是否有 backoff。&lt;/li>
&lt;li>Read replica 是否重新掛到新 source。&lt;/li>
&lt;/ol>
&lt;p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。&lt;/p></description><content:encoded><![CDATA[<p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 <a href="../../replication-topology/">Replication Topology</a> 與 <a href="../../orchestrator-failover/">Orchestrator Failover</a>。</p>
<p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。</p>
<h2 id="baseline-replication">Baseline Replication</h2>
<p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="n">STATUS</span><span class="err">\</span><span class="k">G</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="nb">BINARY</span><span class="w"> </span><span class="n">LOG</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span></span></span></code></pre></div><p>Baseline 要記錄：</p>
<ol>
<li>Source host / replica host。</li>
<li>GTID executed / retrieved。</li>
<li>IO thread / SQL thread。</li>
<li>Seconds behind source。</li>
<li>Read endpoint / write endpoint。</li>
</ol>
<h2 id="client-workload">Client Workload</h2>
<p>Client workload 的核心責任是讓 failover 對 application 可見。</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="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  mysql -h <span class="s2">&#34;</span><span class="nv">$MYSQL_WRITE_HOST</span><span class="s2">&#34;</span> -u app_user -papp_pw appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    -e <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。</p>
<h2 id="promotion-frame">Promotion Frame</h2>
<p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。</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">failover_start:
</span></span><span class="line"><span class="ln">2</span><span class="cl">old_source:
</span></span><span class="line"><span class="ln">3</span><span class="cl">candidate_replica:
</span></span><span class="line"><span class="ln">4</span><span class="cl">lag_before:
</span></span><span class="line"><span class="ln">5</span><span class="cl">promotion_method:
</span></span><span class="line"><span class="ln">6</span><span class="cl">accepted_data_loss:
</span></span><span class="line"><span class="ln">7</span><span class="cl">operator:</span></span></code></pre></div><p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。</p>
<h2 id="validation">Validation</h2>
<p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;read_only&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;super_read_only&#39;</span><span class="p">;</span></span></span></code></pre></div><p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。</p>
<h2 id="client-route">Client Route</h2>
<p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。</p>
<p>檢查項目：</p>
<ol>
<li>Write endpoint 是否更新。</li>
<li>ProxySQL writer hostgroup 是否切換。</li>
<li>Application pool 是否清掉舊連線。</li>
<li>Retry 是否有 backoff。</li>
<li>Read replica 是否重新掛到新 source。</li>
</ol>
<p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。</p>
<h2 id="incident-log">Incident Log</h2>
<p>Incident log 的核心責任是把演練結果保存。</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">Drill id:
</span></span><span class="line"><span class="ln">2</span><span class="cl">RTO observed:
</span></span><span class="line"><span class="ln">3</span><span class="cl">RPO / accepted data loss:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Client errors:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Validation:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Follow-up:</span></span></code></pre></div><p>完成本篇後，拓撲設計讀 <a href="../../replication-topology/">Replication Topology</a>；自動化工具讀 <a href="../../orchestrator-failover/">Orchestrator Failover</a>；routing 讀 <a href="../proxysql-routing-lab/">ProxySQL Routing Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Vitess Sandbox Route</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/vitess-sandbox-route/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/vitess-sandbox-route/</guid><description>&lt;p>MySQL Vitess sandbox route 的核心責任是讓讀者用 sandbox 理解 Vitess 如何把 MySQL 拓展成 sharded database platform。這篇承接 &lt;a href="../../vitess-sharding/">Vitess Sharding&lt;/a> 與 &lt;a href="../../migrate-to-planetscale/">MySQL to PlanetScale&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能建立 sandbox、辨識 keyspace / shard / tablet / vtgate、跑基本 query，並記錄 resharding preview 的 evidence。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 sandbox 指令。實作前先查 &lt;a href="https://vitess.io/docs/21.0/get-started/local/">Vitess local install docs&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="concept-map">Concept Map&lt;/h2>
&lt;p>Concept map 的核心責任是先建立 Vitess vocabulary。&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>Keyspace&lt;/td>
 &lt;td>logical database / routing boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shard&lt;/td>
 &lt;td>keyrange 分片&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tablet&lt;/td>
 &lt;td>MySQL instance + Vitess sidecar role&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>vtgate&lt;/td>
 &lt;td>application query routing endpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VSchema&lt;/td>
 &lt;td>routing、vindex、sharding metadata&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VReplication&lt;/td>
 &lt;td>resharding / materialize workflow&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Vitess 的重點是 routing 與 resharding。Application 看到的是 vtgate；底下是多個 MySQL tablet 與 topology service。&lt;/p>
&lt;h2 id="sandbox-setup">Sandbox Setup&lt;/h2>
&lt;p>Sandbox setup 的核心責任是使用官方 sandbox 建立可丟棄環境。實際命令依 Vitess 版本調整，正式操作以 Vitess 官方文件為準。&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"># Conceptual route. Use the current Vitess examples for exact commands.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git clone https://github.com/vitessio/vitess.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> vitess/examples/local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">./101_initial_cluster.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動後要記錄：&lt;/p>
&lt;ol>
&lt;li>Vitess version。&lt;/li>
&lt;li>Keyspace name。&lt;/li>
&lt;li>Shard count。&lt;/li>
&lt;li>vtgate host / port。&lt;/li>
&lt;li>Tablet roles。&lt;/li>
&lt;/ol>
&lt;h2 id="query-through-vtgate">Query Through vtgate&lt;/h2>
&lt;p>Query through vtgate 的核心責任是確認 application 走 routing layer。&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">mysql -h 127.0.0.1 -P &lt;span class="m">15306&lt;/span> -u user &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW DATABASES;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">USE commerce;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW TABLES;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT * FROM product LIMIT 5;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Evidence 要包含 query result、target keyspace、vtgate endpoint 與 tablet health。Production migration 要確認 ORM / driver 對 vtgate 的相容性。&lt;/p>
&lt;h2 id="vschema-review">VSchema Review&lt;/h2>
&lt;p>VSchema review 的核心責任是理解 shard key 與 routing。&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"># Conceptual command; exact path depends on sandbox.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">cat vschema_commerce_initial.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>審查問題：&lt;/p>
&lt;ol>
&lt;li>哪些 table 是 sharded。&lt;/li>
&lt;li>shard key / vindex 是什麼。&lt;/li>
&lt;li>lookup vindex 是否需要維護。&lt;/li>
&lt;li>cross-shard query 是否存在。&lt;/li>
&lt;li>sequence / id generation 如何處理。&lt;/li>
&lt;/ol>
&lt;p>VSchema 是 Vitess migration 的核心設計文件。選錯 shard key 會讓跨 shard transaction、hot shard 與 resharding 成本升高。&lt;/p></description><content:encoded><![CDATA[<p>MySQL Vitess sandbox route 的核心責任是讓讀者用 sandbox 理解 Vitess 如何把 MySQL 拓展成 sharded database platform。這篇承接 <a href="../../vitess-sharding/">Vitess Sharding</a> 與 <a href="../../migrate-to-planetscale/">MySQL to PlanetScale</a>。</p>
<p>本文的驗收標準是：你能建立 sandbox、辨識 keyspace / shard / tablet / vtgate、跑基本 query，並記錄 resharding preview 的 evidence。</p>
<p>官方文件路由的核心責任是固定 sandbox 指令。實作前先查 <a href="https://vitess.io/docs/21.0/get-started/local/">Vitess local install docs</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="concept-map">Concept Map</h2>
<p>Concept map 的核心責任是先建立 Vitess vocabulary。</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyspace</td>
          <td>logical database / routing boundary</td>
      </tr>
      <tr>
          <td>Shard</td>
          <td>keyrange 分片</td>
      </tr>
      <tr>
          <td>Tablet</td>
          <td>MySQL instance + Vitess sidecar role</td>
      </tr>
      <tr>
          <td>vtgate</td>
          <td>application query routing endpoint</td>
      </tr>
      <tr>
          <td>VSchema</td>
          <td>routing、vindex、sharding metadata</td>
      </tr>
      <tr>
          <td>VReplication</td>
          <td>resharding / materialize workflow</td>
      </tr>
  </tbody>
</table>
<p>Vitess 的重點是 routing 與 resharding。Application 看到的是 vtgate；底下是多個 MySQL tablet 與 topology service。</p>
<h2 id="sandbox-setup">Sandbox Setup</h2>
<p>Sandbox setup 的核心責任是使用官方 sandbox 建立可丟棄環境。實際命令依 Vitess 版本調整，正式操作以 Vitess 官方文件為準。</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"># Conceptual route. Use the current Vitess examples for exact commands.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git clone https://github.com/vitessio/vitess.git
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> vitess/examples/local
</span></span><span class="line"><span class="ln">4</span><span class="cl">./101_initial_cluster.sh</span></span></code></pre></div><p>啟動後要記錄：</p>
<ol>
<li>Vitess version。</li>
<li>Keyspace name。</li>
<li>Shard count。</li>
<li>vtgate host / port。</li>
<li>Tablet roles。</li>
</ol>
<h2 id="query-through-vtgate">Query Through vtgate</h2>
<p>Query through vtgate 的核心責任是確認 application 走 routing layer。</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">mysql -h 127.0.0.1 -P <span class="m">15306</span> -u user <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SHOW DATABASES;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">USE commerce;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SHOW TABLES;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SELECT * FROM product LIMIT 5;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Evidence 要包含 query result、target keyspace、vtgate endpoint 與 tablet health。Production migration 要確認 ORM / driver 對 vtgate 的相容性。</p>
<h2 id="vschema-review">VSchema Review</h2>
<p>VSchema review 的核心責任是理解 shard key 與 routing。</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"># Conceptual command; exact path depends on sandbox.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cat vschema_commerce_initial.json</span></span></code></pre></div><p>審查問題：</p>
<ol>
<li>哪些 table 是 sharded。</li>
<li>shard key / vindex 是什麼。</li>
<li>lookup vindex 是否需要維護。</li>
<li>cross-shard query 是否存在。</li>
<li>sequence / id generation 如何處理。</li>
</ol>
<p>VSchema 是 Vitess migration 的核心設計文件。選錯 shard key 會讓跨 shard transaction、hot shard 與 resharding 成本升高。</p>
<h2 id="resharding-preview">Resharding Preview</h2>
<p>Resharding preview 的核心責任是看見 Vitess 的主要價值與操作成本。</p>
<p>Resharding evidence 欄位：</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">source shard:
</span></span><span class="line"><span class="ln">2</span><span class="cl">target shards:
</span></span><span class="line"><span class="ln">3</span><span class="cl">workflow name:
</span></span><span class="line"><span class="ln">4</span><span class="cl">copy phase duration:
</span></span><span class="line"><span class="ln">5</span><span class="cl">replication lag:
</span></span><span class="line"><span class="ln">6</span><span class="cl">cutover time:
</span></span><span class="line"><span class="ln">7</span><span class="cl">validation query:
</span></span><span class="line"><span class="ln">8</span><span class="cl">rollback:</span></span></code></pre></div><p>Resharding 是 production operation，不只是一次 migration。Runbook 要包含 throttling、lag、tablet health、cutover 與 application query validation。</p>
<h2 id="migration-decision">Migration Decision</h2>
<p>Migration decision 的核心責任是判斷何時從 MySQL 走向 Vitess / PlanetScale 類路線。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 MySQL writer 到頂</td>
          <td>需要 horizontal write scaling</td>
      </tr>
      <tr>
          <td>tenant shard boundary 清楚</td>
          <td>Vitess keyspace / shard 有機會匹配</td>
      </tr>
      <tr>
          <td>online resharding 是核心需求</td>
          <td>Vitess value 高</td>
      </tr>
      <tr>
          <td>app 缺少 routing 語意改造空間</td>
          <td>先重構 repository / query</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，設計細節讀 <a href="../../vitess-sharding/">Vitess Sharding</a>；managed route 讀 <a href="../../migrate-to-planetscale/">MySQL to PlanetScale</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Connection Pool Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/connection-pool-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/connection-pool-lab/</guid><description>&lt;p>PostgreSQL connection pool lab 的核心責任是讓讀者看到 connection pressure 如何從 application pool 傳到 PostgreSQL backend process。這篇承接 &lt;a href="../../connection-scaling/">Connection Scaling&lt;/a> 與 &lt;a href="../../pgbouncer-config/">PgBouncer Config&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能比較 direct connection 與 PgBouncer transaction pooling，取得 &lt;code>pg_stat_activity&lt;/code>、PgBouncer &lt;code>SHOW POOLS&lt;/code>、latency / error sample 與 failure note。&lt;/p>
&lt;h2 id="baseline-direct-connections">Baseline Direct Connections&lt;/h2>
&lt;p>Baseline direct connections 的核心責任是先看 application 直連 PostgreSQL 時的 backend 數。&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="nb">export&lt;/span> &lt;span class="nv">DATABASE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;postgres://lab_admin:lab_admin_pw@localhost:54329/appdb?sslmode=disable&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用多個 terminal 或簡單 workload 產生 idle connection：&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="k">for&lt;/span> i in &lt;span class="m">1&lt;/span> &lt;span class="m">2&lt;/span> &lt;span class="m">3&lt;/span> &lt;span class="m">4&lt;/span> 5&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SELECT pg_sleep(10);&amp;#34;&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SELECT state, count(*) FROM pg_stat_activity WHERE datname = current_database() GROUP BY state;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一步證明每個 client session 會占用 PostgreSQL backend process。&lt;/p>
&lt;h2 id="add-pgbouncer">Add PgBouncer&lt;/h2>
&lt;p>Add PgBouncer 的核心責任是把 client connection 與 server connection 拆開。以下 compose fragment 可加入 local lab：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pgbouncer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">edoburu/pgbouncer:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DB_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DB_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lab_admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DB_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lab_admin_pw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DB_NAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">appdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POOL_MODE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">transaction&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MAX_CLIENT_CONN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DEFAULT_POOL_SIZE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;64329:5432&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動後設定 pooler URL：&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="nb">export&lt;/span> &lt;span class="nv">POOL_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;postgres://lab_admin:lab_admin_pw@localhost:64329/appdb?sslmode=disable&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="compare-pool-behavior">Compare Pool Behavior&lt;/h2>
&lt;p>Compare pool behavior 的核心責任是觀察 client 多、server 少的效果。&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="k">for&lt;/span> i in &lt;span class="k">$(&lt;/span>seq &lt;span class="m">1&lt;/span> 20&lt;span class="k">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$POOL_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SELECT pg_sleep(1);&amp;#34;&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SELECT state, count(*) FROM pg_stat_activity WHERE datname = current_database() GROUP BY state;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>再進 PgBouncer admin console，實際命令依 image 設定調整：&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">psql &lt;span class="s2">&amp;#34;postgres://lab_admin:lab_admin_pw@localhost:64329/pgbouncer?sslmode=disable&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;SHOW POOLS;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>驗收重點是：client workload 增加時，PostgreSQL backend 數量被 pool size 控制，排隊發生在 pooler 層。&lt;/p>
&lt;h2 id="pool-exhaustion">Pool Exhaustion&lt;/h2>
&lt;p>Pool exhaustion 的核心責任是看過載時的錯誤與等待。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL connection pool lab 的核心責任是讓讀者看到 connection pressure 如何從 application pool 傳到 PostgreSQL backend process。這篇承接 <a href="../../connection-scaling/">Connection Scaling</a> 與 <a href="../../pgbouncer-config/">PgBouncer Config</a>。</p>
<p>本文的驗收標準是：你能比較 direct connection 與 PgBouncer transaction pooling，取得 <code>pg_stat_activity</code>、PgBouncer <code>SHOW POOLS</code>、latency / error sample 與 failure note。</p>
<h2 id="baseline-direct-connections">Baseline Direct Connections</h2>
<p>Baseline direct connections 的核心責任是先看 application 直連 PostgreSQL 時的 backend 數。</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="nb">export</span> <span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">&#34;postgres://lab_admin:lab_admin_pw@localhost:54329/appdb?sslmode=disable&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();&#34;</span></span></span></code></pre></div><p>用多個 terminal 或簡單 workload 產生 idle connection：</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="k">for</span> i in <span class="m">1</span> <span class="m">2</span> <span class="m">3</span> <span class="m">4</span> 5<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT pg_sleep(10);&#34;</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT state, count(*) FROM pg_stat_activity WHERE datname = current_database() GROUP BY state;&#34;</span></span></span></code></pre></div><p>這一步證明每個 client session 會占用 PostgreSQL backend process。</p>
<h2 id="add-pgbouncer">Add PgBouncer</h2>
<p>Add PgBouncer 的核心責任是把 client connection 與 server connection 拆開。以下 compose fragment 可加入 local lab：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="w">  </span><span class="nt">pgbouncer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">edoburu/pgbouncer:latest</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span><span class="nt">DB_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">DB_USER</span><span class="p">:</span><span class="w"> </span><span class="l">lab_admin</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">DB_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">lab_admin_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">DB_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">appdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">POOL_MODE</span><span class="p">:</span><span class="w"> </span><span class="l">transaction</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">MAX_CLIENT_CONN</span><span class="p">:</span><span class="w"> </span><span class="m">100</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">DEFAULT_POOL_SIZE</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;64329:5432&#34;</span></span></span></code></pre></div><p>啟動後設定 pooler URL：</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="nb">export</span> <span class="nv">POOL_URL</span><span class="o">=</span><span class="s2">&#34;postgres://lab_admin:lab_admin_pw@localhost:64329/appdb?sslmode=disable&#34;</span></span></span></code></pre></div><h2 id="compare-pool-behavior">Compare Pool Behavior</h2>
<p>Compare pool behavior 的核心責任是觀察 client 多、server 少的效果。</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="k">for</span> i in <span class="k">$(</span>seq <span class="m">1</span> 20<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  psql <span class="s2">&#34;</span><span class="nv">$POOL_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT pg_sleep(1);&#34;</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT state, count(*) FROM pg_stat_activity WHERE datname = current_database() GROUP BY state;&#34;</span></span></span></code></pre></div><p>再進 PgBouncer admin console，實際命令依 image 設定調整：</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">psql <span class="s2">&#34;postgres://lab_admin:lab_admin_pw@localhost:64329/pgbouncer?sslmode=disable&#34;</span> -c <span class="s2">&#34;SHOW POOLS;&#34;</span></span></span></code></pre></div><p>驗收重點是：client workload 增加時，PostgreSQL backend 數量被 pool size 控制，排隊發生在 pooler 層。</p>
<h2 id="pool-exhaustion">Pool Exhaustion</h2>
<p>Pool exhaustion 的核心責任是看過載時的錯誤與等待。</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="k">for</span> i in <span class="k">$(</span>seq <span class="m">1</span> 50<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  psql <span class="s2">&#34;</span><span class="nv">$POOL_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;BEGIN; SELECT pg_sleep(5); COMMIT;&#34;</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>觀察：</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">psql <span class="s2">&#34;postgres://lab_admin:lab_admin_pw@localhost:64329/pgbouncer?sslmode=disable&#34;</span> -c <span class="s2">&#34;SHOW POOLS;&#34;</span></span></span></code></pre></div><p>Pool exhaustion 的 evidence 包含 waiting clients、timeout、application latency 與 error message。這些要接到 production alert。</p>
<h2 id="failure-note">Failure Note</h2>
<p>Failure note 的核心責任是把 lab 結果轉成 runbook。記錄三件事：</p>
<ol>
<li>Direct connection baseline backend 數。</li>
<li>PgBouncer transaction pooling 下 server connection 數。</li>
<li>Pool exhaustion 時的 latency / error / queue。</li>
</ol>
<p>若 application 使用 session state、prepared statement、temp table 或 advisory lock，還要補 transaction pooling compatibility matrix。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>完成本篇後，回到 <a href="../../connection-pooler-comparison/">Connection Pooler Comparison</a> 做選型；要看 PgBouncer production 設定讀 <a href="../../pgbouncer-config/">PgBouncer Config</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Connection Pooler Comparison</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/connection-pooler-comparison/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/connection-pooler-comparison/</guid><description>&lt;p>PostgreSQL connection pooler comparison 的核心責任是把連線數壓力、transaction 語意與維運責任拆開判讀。PostgreSQL backend process 成本高，application instance 擴張後，connection pooler 常成為保護資料庫的第一層容量控制。&lt;/p>
&lt;p>本文的判讀錨點是：pooler 解決的是 connection fan-out 與 queueing，而非查詢本身變快。查詢慢、lock wait、transaction 過長、index 錯誤仍要回到 &lt;a href="../query-optimization/">Query Optimization&lt;/a> 與 &lt;a href="../mvcc-lock-model/">MVCC / lock model&lt;/a>。&lt;/p>
&lt;h2 id="pooling-models">Pooling Models&lt;/h2>
&lt;p>Pooling model 的核心責任是決定 client connection 和 server connection 的綁定時間。PgBouncer 代表最常見的 PostgreSQL pooler 模型；官方文件將 pool mode 分成 session、transaction 與 statement。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>Server connection 綁定&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>主要風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Session&lt;/td>
 &lt;td>client session 全程&lt;/td>
 &lt;td>使用 session state、temp table&lt;/td>
 &lt;td>壓縮率低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>transaction 期間&lt;/td>
 &lt;td>Web API、短交易、Stateless query&lt;/td>
 &lt;td>session variable、prepared statement 語意受限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Statement&lt;/td>
 &lt;td>single statement&lt;/td>
 &lt;td>特殊 read-only workload&lt;/td>
 &lt;td>transaction workflow 受限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>App pool&lt;/td>
 &lt;td>application process 內&lt;/td>
 &lt;td>單服務、低 fan-out&lt;/td>
 &lt;td>多 instance 後總連線失控&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-pooling/" data-link-title="Transaction Pooling" data-link-desc="說明 connection pooler 的 transaction 綁定模式如何壓縮連線並改變 session 語意">Transaction pooling&lt;/a> 的價值在於把大量 idle client connection 收斂成少量 active server connection。它要求 application 把 session state 放回 request / transaction boundary，例如 timezone、role、search_path、prepared statement 與 advisory lock 都要明確管理。&lt;/p>
&lt;p>Session pooling 的價值在於相容性。若 application 大量使用 temp table、LISTEN / NOTIFY、session-level setting 或 server-side prepared statement，session pooling 能降低行為差異，但連線壓縮效果較弱。&lt;/p>
&lt;h2 id="product-boundary">Product Boundary&lt;/h2>
&lt;p>Product boundary 的核心責任是把 pooler 放在正確的維運位置。不同選項的責任邊界差異很大。&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>PgBouncer&lt;/td>
 &lt;td>輕量 PostgreSQL connection pooling&lt;/td>
 &lt;td>自管 VM / K8s、transaction pooling 標準路線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Odyssey&lt;/td>
 &lt;td>多租戶與複雜 routing pooler&lt;/td>
 &lt;td>大型部署、需要進階 routing / auth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RDS Proxy&lt;/td>
 &lt;td>AWS managed connection proxy&lt;/td>
 &lt;td>RDS / Aurora 生態、希望降低 proxy 維運&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application pool&lt;/td>
 &lt;td>服務內部連線池&lt;/td>
 &lt;td>instance 數少、連線總量可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>No pooler&lt;/td>
 &lt;td>直接連 PostgreSQL&lt;/td>
 &lt;td>小型服務、低併發、連線數遠低於上限&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>PgBouncer 的操作重點是 mode、pool size、server reset query、auth、TLS 與 metrics。它很適合放在 application 與 database 中間，承擔連線排隊與 backpressure。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL connection pooler comparison 的核心責任是把連線數壓力、transaction 語意與維運責任拆開判讀。PostgreSQL backend process 成本高，application instance 擴張後，connection pooler 常成為保護資料庫的第一層容量控制。</p>
<p>本文的判讀錨點是：pooler 解決的是 connection fan-out 與 queueing，而非查詢本身變快。查詢慢、lock wait、transaction 過長、index 錯誤仍要回到 <a href="../query-optimization/">Query Optimization</a> 與 <a href="../mvcc-lock-model/">MVCC / lock model</a>。</p>
<h2 id="pooling-models">Pooling Models</h2>
<p>Pooling model 的核心責任是決定 client connection 和 server connection 的綁定時間。PgBouncer 代表最常見的 PostgreSQL pooler 模型；官方文件將 pool mode 分成 session、transaction 與 statement。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>Server connection 綁定</th>
          <th>適合情境</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session</td>
          <td>client session 全程</td>
          <td>使用 session state、temp table</td>
          <td>壓縮率低</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>transaction 期間</td>
          <td>Web API、短交易、Stateless query</td>
          <td>session variable、prepared statement 語意受限</td>
      </tr>
      <tr>
          <td>Statement</td>
          <td>single statement</td>
          <td>特殊 read-only workload</td>
          <td>transaction workflow 受限</td>
      </tr>
      <tr>
          <td>App pool</td>
          <td>application process 內</td>
          <td>單服務、低 fan-out</td>
          <td>多 instance 後總連線失控</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/transaction-pooling/" data-link-title="Transaction Pooling" data-link-desc="說明 connection pooler 的 transaction 綁定模式如何壓縮連線並改變 session 語意">Transaction pooling</a> 的價值在於把大量 idle client connection 收斂成少量 active server connection。它要求 application 把 session state 放回 request / transaction boundary，例如 timezone、role、search_path、prepared statement 與 advisory lock 都要明確管理。</p>
<p>Session pooling 的價值在於相容性。若 application 大量使用 temp table、LISTEN / NOTIFY、session-level setting 或 server-side prepared statement，session pooling 能降低行為差異，但連線壓縮效果較弱。</p>
<h2 id="product-boundary">Product Boundary</h2>
<p>Product boundary 的核心責任是把 pooler 放在正確的維運位置。不同選項的責任邊界差異很大。</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>主要責任</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PgBouncer</td>
          <td>輕量 PostgreSQL connection pooling</td>
          <td>自管 VM / K8s、transaction pooling 標準路線</td>
      </tr>
      <tr>
          <td>Odyssey</td>
          <td>多租戶與複雜 routing pooler</td>
          <td>大型部署、需要進階 routing / auth</td>
      </tr>
      <tr>
          <td>RDS Proxy</td>
          <td>AWS managed connection proxy</td>
          <td>RDS / Aurora 生態、希望降低 proxy 維運</td>
      </tr>
      <tr>
          <td>Application pool</td>
          <td>服務內部連線池</td>
          <td>instance 數少、連線總量可控</td>
      </tr>
      <tr>
          <td>No pooler</td>
          <td>直接連 PostgreSQL</td>
          <td>小型服務、低併發、連線數遠低於上限</td>
      </tr>
  </tbody>
</table>
<p>PgBouncer 的操作重點是 mode、pool size、server reset query、auth、TLS 與 metrics。它很適合放在 application 與 database 中間，承擔連線排隊與 backpressure。</p>
<p>Managed proxy 的操作重點是平台限制、failover behavior、credential integration、latency overhead 與 observability。若 team 想少維護一個 pooler process，managed proxy 可以降低操作成本，但要接受雲平台邊界。</p>
<h2 id="decision-signals">Decision Signals</h2>
<p>Decision signals 的核心責任是判斷何時導入 pooler，以及導入哪一種。連線數壓力要用 evidence 說明。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>代表問題</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>max_connections</code> 接近上限</td>
          <td>application fan-out 過高</td>
          <td>PgBouncer transaction pooling</td>
      </tr>
      <tr>
          <td>大量 idle connection</td>
          <td>client 連線長期閒置</td>
          <td>transaction pooling 或 app pool 調整</td>
      </tr>
      <tr>
          <td>failover 後 reconnect storm</td>
          <td>client 同時重連衝擊 primary</td>
          <td>pooler queue + jitter</td>
      </tr>
      <tr>
          <td>query latency 高但 connection 不高</td>
          <td>查詢 / lock / index 問題</td>
          <td>query optimization</td>
      </tr>
      <tr>
          <td>session state 依賴多</td>
          <td>transaction pooling 相容性風險</td>
          <td>session pooling 或 refactor session state</td>
      </tr>
  </tbody>
</table>
<p>Connection pooler 的成功訊號是 database backend count 下降、queue 可觀測、error rate 穩定、tail latency 受控。若導入後只是把 timeout 從 DB 移到 pooler，代表 capacity model 仍需調整。</p>
<h2 id="transaction-pooling-compatibility">Transaction Pooling Compatibility</h2>
<p>Transaction pooling compatibility 的核心責任是找出 application 對 session state 的隱性依賴。這些依賴要在 staging 先測出來。</p>
<table>
  <thead>
      <tr>
          <th>依賴類型</th>
          <th>風險</th>
          <th>修正策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SET search_path</code></td>
          <td>下一個 transaction 可能換連線</td>
          <td>每個 transaction 明確設定或固定 schema</td>
      </tr>
      <tr>
          <td>temp table</td>
          <td>transaction 後 server connection 釋放</td>
          <td>改 permanent staging table 或 session mode</td>
      </tr>
      <tr>
          <td>prepared statement</td>
          <td>server-side state 不穩定</td>
          <td>使用 client-side prepare 或 session mode</td>
      </tr>
      <tr>
          <td>advisory lock</td>
          <td>lock ownership 混亂</td>
          <td>transaction-scoped lock 或移出 pooler path</td>
      </tr>
      <tr>
          <td>LISTEN / NOTIFY</td>
          <td>session channel 需要持續連線</td>
          <td>專用 direct connection</td>
      </tr>
  </tbody>
</table>
<p>Compatibility review 要在 repository / migration / background job 三個層面跑。Web request 通常容易改成 transaction-safe；migration tool、CDC job、worker queue 常有長連線與 session state，要分開配置。</p>
<h2 id="sizing-and-evidence">Sizing and Evidence</h2>
<p>Sizing and evidence 的核心責任是用 workload 設定 pool size。Pooler 設太大會把壓力直接傳到 PostgreSQL；設太小會造成 queue 與 timeout。</p>
<p>基本 sizing 步驟：</p>
<ol>
<li>量測 active query concurrency，而非只看 request concurrency。</li>
<li>設定 database 保留連線給 admin、replication、migration 與 emergency access。</li>
<li>每個 service 設定 pool quota，避免單一服務吃掉全部 backend。</li>
<li>觀測 wait time、server utilization、client timeout、query latency。</li>
<li>用 load test 驗證 failover / reconnect storm。</li>
</ol>
<p>Pooler dashboard 至少要有 client connections、server connections、waiting clients、pool wait time、server reuse、timeout count 與 authentication failure。</p>
<h2 id="anti-patterns">Anti-Patterns</h2>
<p>Anti-pattern 的核心責任是把 pooler 常見誤用提前排除。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把 pool size 設到 DB 上限</td>
          <td>DB 失去保護層</td>
          <td>每個服務配額 + 保留 admin capacity</td>
      </tr>
      <tr>
          <td>transaction pooling 直接上線</td>
          <td>session state 依賴在 production 爆出</td>
          <td>staging compatibility matrix</td>
      </tr>
      <tr>
          <td>pooler 沒有 metrics</td>
          <td>queueing 事故難以判讀</td>
          <td>pooler dashboard + alert</td>
      </tr>
      <tr>
          <td>migration 共用 web pool</td>
          <td>長 DDL 卡住 web request</td>
          <td>migration 專用連線與維護窗口</td>
      </tr>
      <tr>
          <td>retry 無 jitter</td>
          <td>reconnect storm 放大</td>
          <td>exponential backoff + jitter</td>
      </tr>
  </tbody>
</table>
<p>Pooler 是 backpressure 元件。它要讓系統在過載時可排隊、可拒絕、可觀測，而非把所有請求推進 database。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Connection pooler comparison 完成後，實作層讀 <a href="../pgbouncer-config/">PgBouncer config</a>；要觀察連線壓力讀 <a href="../connection-scaling/">Connection Scaling</a>；需要演練讀 <a href="../hands-on/connection-pool-lab/">Connection Pool Lab</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Cross-region DR</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/cross-region-dr/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/cross-region-dr/</guid><description>&lt;p>PostgreSQL cross-region DR 的核心責任是把區域性事故下的資料恢復、服務切換與資料一致性風險寫成可演練流程。跨區 DR 通常由法規、業務連續性、雲區故障、區域隔離或高可用承諾觸發。&lt;/p>
&lt;p>本文的判讀錨點是：cross-region DR 是恢復策略，而非自動等同 multi-region active-active。PostgreSQL 可以透過 backup / WAL archive、physical standby、logical replication、managed service replica 或 application-level replication 支援不同 RPO / RTO；每種路線都有資料延遲、切換與回切成本。&lt;/p>
&lt;h2 id="dr-strategy">DR Strategy&lt;/h2>
&lt;p>DR strategy 的核心責任是把恢復目標和技術路線對齊。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>RPO / RTO 型態&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Backup + WAL archive&lt;/td>
 &lt;td>RPO 依 WAL archive，RTO 依 restore&lt;/td>
 &lt;td>成本敏感、低頻災難復原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-region standby&lt;/td>
 &lt;td>RPO 接近 replication lag，RTO 較短&lt;/td>
 &lt;td>需要較快啟動 read / promote&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Logical replication&lt;/td>
 &lt;td>table-level / selective DR&lt;/td>
 &lt;td>跨版本、跨 schema、局部資料同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Managed global DB&lt;/td>
 &lt;td>雲平台提供跨區 replica&lt;/td>
 &lt;td>希望降低自管複製與 promote 維運&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application replay&lt;/td>
 &lt;td>event / queue 重建狀態&lt;/td>
 &lt;td>domain event 已是 source of truth&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>RPO 要由業務定義。若付款、訂單、庫存只允許秒級遺失，backup-only 路線通常成本不足；若是內部報表或可重建資料，backup + WAL archive 可能足夠。&lt;/p>
&lt;h2 id="physical-vs-logical">Physical vs Logical&lt;/h2>
&lt;p>Physical vs logical 的核心責任是區分 byte-level recovery 與 row-level replication。Physical replica 保留 PostgreSQL cluster 層級狀態；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/logical-replication/" data-link-title="Logical Replication" data-link-desc="說明以表為粒度解碼 row-level 變更的複製方式，對照 byte-level 的實體複製">logical replication&lt;/a> 提供 table / publication 層級彈性。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Physical standby&lt;/th>
 &lt;th>Logical replication&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>粒度&lt;/td>
 &lt;td>cluster / database&lt;/td>
 &lt;td>table / publication&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>版本彈性&lt;/td>
 &lt;td>通常要求版本與系統相容&lt;/td>
 &lt;td>可支援跨版本 / selective migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DDL&lt;/td>
 &lt;td>跟隨 WAL / 需相容&lt;/td>
 &lt;td>需要 schema coordination&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failover&lt;/td>
 &lt;td>promote standby&lt;/td>
 &lt;td>application / target DB 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>風險&lt;/td>
 &lt;td>replication lag、timeline&lt;/td>
 &lt;td>slot lag、schema drift、missing key&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Physical standby 適合整體 DR。它的 runbook 要處理 WAL archive、replication lag、promotion、timeline、DNS / connection string 切換與回切。&lt;/p>
&lt;p>Logical replication 適合局部資料或跨版本轉換。它的 runbook 要處理 publication、subscription、replication slot、schema migration ordering 與資料 diff。&lt;/p>
&lt;h2 id="failover-runbook">Failover Runbook&lt;/h2>
&lt;p>Failover runbook 的核心責任是把災難切換變成可演練步驟。最小流程包含 incident declare、source freeze、replica health check、promote、traffic switch、data validation 與 rollback / rebuild。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL cross-region DR 的核心責任是把區域性事故下的資料恢復、服務切換與資料一致性風險寫成可演練流程。跨區 DR 通常由法規、業務連續性、雲區故障、區域隔離或高可用承諾觸發。</p>
<p>本文的判讀錨點是：cross-region DR 是恢復策略，而非自動等同 multi-region active-active。PostgreSQL 可以透過 backup / WAL archive、physical standby、logical replication、managed service replica 或 application-level replication 支援不同 RPO / RTO；每種路線都有資料延遲、切換與回切成本。</p>
<h2 id="dr-strategy">DR Strategy</h2>
<p>DR strategy 的核心責任是把恢復目標和技術路線對齊。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>RPO / RTO 型態</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup + WAL archive</td>
          <td>RPO 依 WAL archive，RTO 依 restore</td>
          <td>成本敏感、低頻災難復原</td>
      </tr>
      <tr>
          <td>Cross-region standby</td>
          <td>RPO 接近 replication lag，RTO 較短</td>
          <td>需要較快啟動 read / promote</td>
      </tr>
      <tr>
          <td>Logical replication</td>
          <td>table-level / selective DR</td>
          <td>跨版本、跨 schema、局部資料同步</td>
      </tr>
      <tr>
          <td>Managed global DB</td>
          <td>雲平台提供跨區 replica</td>
          <td>希望降低自管複製與 promote 維運</td>
      </tr>
      <tr>
          <td>Application replay</td>
          <td>event / queue 重建狀態</td>
          <td>domain event 已是 source of truth</td>
      </tr>
  </tbody>
</table>
<p>RPO 要由業務定義。若付款、訂單、庫存只允許秒級遺失，backup-only 路線通常成本不足；若是內部報表或可重建資料，backup + WAL archive 可能足夠。</p>
<h2 id="physical-vs-logical">Physical vs Logical</h2>
<p>Physical vs logical 的核心責任是區分 byte-level recovery 與 row-level replication。Physical replica 保留 PostgreSQL cluster 層級狀態；<a href="/blog/backend/knowledge-cards/logical-replication/" data-link-title="Logical Replication" data-link-desc="說明以表為粒度解碼 row-level 變更的複製方式，對照 byte-level 的實體複製">logical replication</a> 提供 table / publication 層級彈性。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Physical standby</th>
          <th>Logical replication</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>粒度</td>
          <td>cluster / database</td>
          <td>table / publication</td>
      </tr>
      <tr>
          <td>版本彈性</td>
          <td>通常要求版本與系統相容</td>
          <td>可支援跨版本 / selective migration</td>
      </tr>
      <tr>
          <td>DDL</td>
          <td>跟隨 WAL / 需相容</td>
          <td>需要 schema coordination</td>
      </tr>
      <tr>
          <td>Failover</td>
          <td>promote standby</td>
          <td>application / target DB 切換</td>
      </tr>
      <tr>
          <td>風險</td>
          <td>replication lag、timeline</td>
          <td>slot lag、schema drift、missing key</td>
      </tr>
  </tbody>
</table>
<p>Physical standby 適合整體 DR。它的 runbook 要處理 WAL archive、replication lag、promotion、timeline、DNS / connection string 切換與回切。</p>
<p>Logical replication 適合局部資料或跨版本轉換。它的 runbook 要處理 publication、subscription、replication slot、schema migration ordering 與資料 diff。</p>
<h2 id="failover-runbook">Failover Runbook</h2>
<p>Failover runbook 的核心責任是把災難切換變成可演練步驟。最小流程包含 incident declare、source freeze、replica health check、promote、traffic switch、data validation 與 rollback / rebuild。</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>操作</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Declare incident</td>
          <td>確認 primary region 事故範圍</td>
          <td>incident decision log</td>
      </tr>
      <tr>
          <td>Freeze source</td>
          <td>停止寫入或確認 source 已不可用</td>
          <td>last known LSN / timestamp</td>
      </tr>
      <tr>
          <td>Check replica</td>
          <td>lag、WAL received、read health</td>
          <td>replica status snapshot</td>
      </tr>
      <tr>
          <td>Promote</td>
          <td>promote standby 或啟用 target</td>
          <td>new timeline / role</td>
      </tr>
      <tr>
          <td>Switch traffic</td>
          <td>DNS、secret、connection string</td>
          <td>app smoke test</td>
      </tr>
      <tr>
          <td>Validate</td>
          <td>row count、critical invariant</td>
          <td>validation report</td>
      </tr>
      <tr>
          <td>Rebuild</td>
          <td>重建舊 primary 或新 standby</td>
          <td>follow-up runbook</td>
      </tr>
  </tbody>
</table>
<p>Failover 決策要有 owner。自動化可以執行步驟，但是否接受資料遺失、是否凍結寫入、是否 promote，仍需要明確責任人與 tripwire。</p>
<h2 id="data-reconciliation">Data Reconciliation</h2>
<p>Data reconciliation 的核心責任是處理 cross-region 切換後的資料差異。只要 replication lag 存在，failover 後就可能有未套用交易。</p>
<table>
  <thead>
      <tr>
          <th>差異類型</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已提交但未複製</td>
          <td>從 source WAL / app log / event 補償</td>
      </tr>
      <tr>
          <td>client retry 重複寫入</td>
          <td>idempotency key / natural key 去重</td>
      </tr>
      <tr>
          <td>sequence / identity</td>
          <td>target sequence reset / collision check</td>
      </tr>
      <tr>
          <td>external side effect</td>
          <td>payment、email、queue 需對帳</td>
      </tr>
  </tbody>
</table>
<p>Reconciliation 要先定義 critical table。所有表都做 full diff 成本高；付款、訂單、權限、ledger、mutation log 等高風險資料要有專用 validation query。</p>
<h2 id="drill-design">Drill Design</h2>
<p>Drill design 的核心責任是定期驗證 RPO / RTO。DR 文件只有在演練後才可信。</p>
<p>演練至少包含：</p>
<ol>
<li>從 backup + WAL 還原到指定時間。</li>
<li>Promote standby 到 isolated environment。</li>
<li>Application 使用 DR endpoint 跑 smoke test。</li>
<li>計算實際 RPO / RTO。</li>
<li>記錄失敗點、人工步驟與下一次修正。</li>
</ol>
<p>演練應避開 production destructive action。使用 isolated VPC、staging app、read-only validation 與 mock external side effect。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go conditions 的核心責任是指出 PostgreSQL cross-region DR 的邊界。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多區同時交易寫入是核心需求</td>
          <td>CockroachDB / Spanner / YugabyteDB 類 distributed SQL</td>
      </tr>
      <tr>
          <td>RPO 接近零且跨區距離大</td>
          <td>synchronous replication latency 成本評估</td>
      </tr>
      <tr>
          <td>Team 缺少 DR 演練能力</td>
          <td>managed service + vendor runbook</td>
      </tr>
      <tr>
          <td>數據 residency 限制跨區複製</td>
          <td>regional shard / policy-driven replication</td>
      </tr>
  </tbody>
</table>
<p>Cross-region DR 要誠實面對延遲。把每個 region 都變成 writer 需要 distributed transaction 模型；PostgreSQL DR 路線主要提供恢復與切換。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Cross-region DR 完成後，恢復實作讀 <a href="../pitr-wal-archiving/">PITR / WAL Archiving</a>；replication 架構讀 <a href="../replication-topology/">Replication Topology</a>；跨區 rollout 的資料政策讀 <a href="../multi-region-gdpr-rollout/">Multi-region GDPR Rollout</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Developer / DBA Responsibility Split</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/developer-dba-responsibility-split/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/developer-dba-responsibility-split/</guid><description>&lt;p>PostgreSQL developer / DBA responsibility split 的核心責任是把資料庫決策拆成 application ownership、database operation 與 platform governance。PostgreSQL 功能深，事故常跨 query、schema、connection、backup、replication 與 capacity；若責任分工模糊，問題會在 release 與 incident 時放大。&lt;/p>
&lt;p>本文的判讀錨點是：developer 和 DBA 分工要讓每個決策有清楚 owner、evidence、review gate 與 rollback，而非把資料庫丟給某一方。&lt;/p>
&lt;h2 id="ownership-map">Ownership Map&lt;/h2>
&lt;p>Ownership map 的核心責任是定義誰能改什麼、誰要驗證什麼。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Developer owner&lt;/th>
 &lt;th>DBA / platform owner&lt;/th>
 &lt;th>Shared gate&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema design&lt;/td>
 &lt;td>domain model、constraint、query&lt;/td>
 &lt;td>naming、storage、partition、extension&lt;/td>
 &lt;td>migration review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query performance&lt;/td>
 &lt;td>repository SQL、query shape&lt;/td>
 &lt;td>index、planner、statistics、capacity&lt;/td>
 &lt;td>explain evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>app compatibility、rollback&lt;/td>
 &lt;td>lock impact、DDL strategy、PITR&lt;/td>
 &lt;td>release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection&lt;/td>
 &lt;td>pool usage、transaction length&lt;/td>
 &lt;td>pooler、max connection、proxy&lt;/td>
 &lt;td>load test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup / DR&lt;/td>
 &lt;td>restore smoke test&lt;/td>
 &lt;td>WAL archive、PITR、replica&lt;/td>
 &lt;td>restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security&lt;/td>
 &lt;td>tenant / workflow intent&lt;/td>
 &lt;td>role、RLS、audit、grant&lt;/td>
 &lt;td>access review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是 shared gate。Developer 最懂產品語意，DBA / platform 最懂資料庫風險；正式變更需要兩邊的 evidence 合併。&lt;/p>
&lt;h2 id="schema-and-migration">Schema and Migration&lt;/h2>
&lt;p>Schema and migration 的核心責任是讓 application release 與 database change 同步。Developer 應提供 business invariant、compatibility window、read/write path；DBA / platform 應審查 lock、index build、table rewrite、replica lag 與 rollback。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Migration 類型&lt;/th>
 &lt;th>Developer evidence&lt;/th>
 &lt;th>DBA / platform evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Add nullable column&lt;/td>
 &lt;td>app read/write compatibility&lt;/td>
 &lt;td>DDL lock time、replica impact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Add NOT NULL&lt;/td>
 &lt;td>backfill plan、default behavior&lt;/td>
 &lt;td>table rewrite / validation strategy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index build&lt;/td>
 &lt;td>query contract、expected selectivity&lt;/td>
 &lt;td>concurrent build、disk、bloat&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Partition change&lt;/td>
 &lt;td>routing logic、retention behavior&lt;/td>
 &lt;td>detach / attach、maintenance window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Type change&lt;/td>
 &lt;td>serialization、API compatibility&lt;/td>
 &lt;td>cast risk、rewrite duration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Migration review 要從 failure mode 開始。若 migration 卡住，誰停止 rollout；若 backfill 造成 lag，誰降速；若 app 新舊版本同時存在，哪個 schema 能兼容兩者。&lt;/p>
&lt;h2 id="query-and-capacity">Query and Capacity&lt;/h2>
&lt;p>Query and capacity 的核心責任是把 query shape 和 database resource 對齊。Developer 負責避免 N+1、長交易、無界查詢與錯誤 pagination；DBA / platform 負責 index、statistics、vacuum、work_mem、connection 與 storage。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL developer / DBA responsibility split 的核心責任是把資料庫決策拆成 application ownership、database operation 與 platform governance。PostgreSQL 功能深，事故常跨 query、schema、connection、backup、replication 與 capacity；若責任分工模糊，問題會在 release 與 incident 時放大。</p>
<p>本文的判讀錨點是：developer 和 DBA 分工要讓每個決策有清楚 owner、evidence、review gate 與 rollback，而非把資料庫丟給某一方。</p>
<h2 id="ownership-map">Ownership Map</h2>
<p>Ownership map 的核心責任是定義誰能改什麼、誰要驗證什麼。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Developer owner</th>
          <th>DBA / platform owner</th>
          <th>Shared gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema design</td>
          <td>domain model、constraint、query</td>
          <td>naming、storage、partition、extension</td>
          <td>migration review</td>
      </tr>
      <tr>
          <td>Query performance</td>
          <td>repository SQL、query shape</td>
          <td>index、planner、statistics、capacity</td>
          <td>explain evidence</td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>app compatibility、rollback</td>
          <td>lock impact、DDL strategy、PITR</td>
          <td>release gate</td>
      </tr>
      <tr>
          <td>Connection</td>
          <td>pool usage、transaction length</td>
          <td>pooler、max connection、proxy</td>
          <td>load test</td>
      </tr>
      <tr>
          <td>Backup / DR</td>
          <td>restore smoke test</td>
          <td>WAL archive、PITR、replica</td>
          <td>restore drill</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>tenant / workflow intent</td>
          <td>role、RLS、audit、grant</td>
          <td>access review</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是 shared gate。Developer 最懂產品語意，DBA / platform 最懂資料庫風險；正式變更需要兩邊的 evidence 合併。</p>
<h2 id="schema-and-migration">Schema and Migration</h2>
<p>Schema and migration 的核心責任是讓 application release 與 database change 同步。Developer 應提供 business invariant、compatibility window、read/write path；DBA / platform 應審查 lock、index build、table rewrite、replica lag 與 rollback。</p>
<table>
  <thead>
      <tr>
          <th>Migration 類型</th>
          <th>Developer evidence</th>
          <th>DBA / platform evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Add nullable column</td>
          <td>app read/write compatibility</td>
          <td>DDL lock time、replica impact</td>
      </tr>
      <tr>
          <td>Add NOT NULL</td>
          <td>backfill plan、default behavior</td>
          <td>table rewrite / validation strategy</td>
      </tr>
      <tr>
          <td>Index build</td>
          <td>query contract、expected selectivity</td>
          <td>concurrent build、disk、bloat</td>
      </tr>
      <tr>
          <td>Partition change</td>
          <td>routing logic、retention behavior</td>
          <td>detach / attach、maintenance window</td>
      </tr>
      <tr>
          <td>Type change</td>
          <td>serialization、API compatibility</td>
          <td>cast risk、rewrite duration</td>
      </tr>
  </tbody>
</table>
<p>Migration review 要從 failure mode 開始。若 migration 卡住，誰停止 rollout；若 backfill 造成 lag，誰降速；若 app 新舊版本同時存在，哪個 schema 能兼容兩者。</p>
<h2 id="query-and-capacity">Query and Capacity</h2>
<p>Query and capacity 的核心責任是把 query shape 和 database resource 對齊。Developer 負責避免 N+1、長交易、無界查詢與錯誤 pagination；DBA / platform 負責 index、statistics、vacuum、work_mem、connection 與 storage。</p>
<p>Query review 的最小 evidence：</p>
<ol>
<li>SQL text 或 repository method。</li>
<li>Expected cardinality 與資料量。</li>
<li><code>EXPLAIN</code> / <code>EXPLAIN ANALYZE</code> 結果。</li>
<li>Index 依賴與 fallback plan。</li>
<li>Timeout、pagination、transaction boundary。</li>
</ol>
<p>Capacity review 要把 query 放進 workload。單一 query 快不代表整體穩定；高頻 query、batch job、migration backfill、CDC consumer 都會共享 I/O、CPU、lock 與 WAL。</p>
<h2 id="incident-roles">Incident Roles</h2>
<p>Incident roles 的核心責任是讓資料庫事故有分工。Incident 發生時，developer 看 workflow、feature flag、traffic 與 recent deploy；DBA / platform 看 lock、replica、WAL、disk、pooler 與 backup。</p>
<table>
  <thead>
      <tr>
          <th>Incident</th>
          <th>Developer 第一反應</th>
          <th>DBA / platform 第一反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lock storm</td>
          <td>暫停相關 workflow、停 rollout</td>
          <td>查 blocking PID、DDL、transaction</td>
      </tr>
      <tr>
          <td>Connection exhaustion</td>
          <td>降低 app concurrency、停 retry storm</td>
          <td>pooler queue、max connection、admin access</td>
      </tr>
      <tr>
          <td>Replica lag</td>
          <td>暫停 heavy write / backfill</td>
          <td>WAL sender、slot、standby apply</td>
      </tr>
      <tr>
          <td>Bad migration</td>
          <td>block release、保留 failed state</td>
          <td>restore point、rollback / PITR</td>
      </tr>
      <tr>
          <td>Slow query spike</td>
          <td>feature flag、query owner</td>
          <td>plan regression、statistics、index</td>
      </tr>
  </tbody>
</table>
<p>Incident command 要保留決策紀錄。資料庫事故常有高壓操作，例如 kill session、promote replica、drop slot、restore backup；每個操作都要記錄原因與回復路線。</p>
<h2 id="review-cadence">Review Cadence</h2>
<p>Review cadence 的核心責任是把資料庫品質納入日常。建議節奏如下：</p>
<table>
  <thead>
      <tr>
          <th>節奏</th>
          <th>Review 內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個 release</td>
          <td>migration diff、new query、role / grant</td>
      </tr>
      <tr>
          <td>每週</td>
          <td>slow query、lock wait、replica lag、pool</td>
      </tr>
      <tr>
          <td>每月</td>
          <td>backup restore drill、index bloat、vacuum</td>
      </tr>
      <tr>
          <td>每季</td>
          <td>DR drill、major version plan、extension review</td>
      </tr>
  </tbody>
</table>
<p>Review cadence 要跟服務風險對齊。高交易量或合規服務需要更短週期；內部工具可以更輕量，但仍要保留 backup / restore evidence。</p>
<h2 id="handoff-artifact">Handoff Artifact</h2>
<p>Handoff artifact 的核心責任是讓下一位維護者能接手。</p>
<p>最小內容：</p>
<ol>
<li>Database owner、application owner、platform owner。</li>
<li>Schema migration process 與 rollback route。</li>
<li>Query review checklist。</li>
<li>Connection / pooler policy。</li>
<li>Backup / PITR / DR evidence。</li>
<li>Security / role / audit owner。</li>
<li>Incident escalation route。</li>
</ol>
<p>這份 artifact 應連回 <a href="../">PostgreSQL overview</a>、<a href="../hands-on/schema-migration-evidence-lab/">Schema Migration Evidence Lab</a> 與 <a href="../hands-on/pitr-restore-drill/">PITR Restore Drill</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>責任分工建立後，migration gate 讀 <a href="../online-schema-change/">Online Schema Change</a>；連線責任讀 <a href="../connection-pooler-comparison/">Connection Pooler Comparison</a>；安全責任讀 <a href="../security-rls-audit-logging/">Security / RLS / Audit Logging</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL HA Failover Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/ha-failover-drill/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/ha-failover-drill/</guid><description>&lt;p>PostgreSQL HA failover drill 的核心責任是讓讀者觀察 primary promotion 對 application、pooler 與 incident decision 的影響。這篇承接 &lt;a href="../../patroni-ha/">Patroni HA&lt;/a> 與 &lt;a href="../../cross-region-dr/">Cross-region DR&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 failover timeline、replication lag snapshot、client error sample、data validation query 與 incident decision log entry。實際觸發方式依 Patroni、managed PostgreSQL 或雲平台而異；lab 重點是 evidence。&lt;/p>
&lt;h2 id="pre-failover-baseline">Pre-Failover Baseline&lt;/h2>
&lt;p>Pre-failover baseline 的核心責任是確認 primary / standby 狀態與 client route。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_is_in_recovery&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_current_wal_lsn&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">application_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">state&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sync_state&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">replay_lag&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_stat_replication&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 standby 查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_is_in_recovery&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_last_wal_receive_lsn&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_last_wal_replay_lsn&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Baseline 要保存 primary host、standby host、replication lag、application connection string、pooler route 與 current timeline。&lt;/p>
&lt;h2 id="client-workload">Client Workload&lt;/h2>
&lt;p>Client workload 的核心責任是讓 failover 對 application 的影響可見。&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="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> date -u
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;INSERT INTO restore_markers(marker) VALUES (&amp;#39;failover-drill&amp;#39;) RETURNING id, created_at;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> sleep &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 loop 會在 failover 期間產生成功、timeout、connection reset 或 read-only error。正式演練要用 synthetic workload，避免影響真實使用者。&lt;/p>
&lt;h2 id="trigger-failover">Trigger Failover&lt;/h2>
&lt;p>Trigger failover 的核心責任是以受控方式促成 promotion。Patroni lab 可以用 &lt;code>patronictl failover&lt;/code>；managed service 則用 provider failover / reboot with failover 功能。&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">failover_start_time:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">trigger_method:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">old_primary:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">candidate:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">operator:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">reason:&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Failover 觸發前要先確認這是演練，並且 workload、backup、rollback 與 stakeholder 都已對齊。&lt;/p>
&lt;h2 id="observe-promotion">Observe Promotion&lt;/h2>
&lt;p>Observe promotion 的核心責任是記錄資料庫與 client 的時間線。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>時間點&lt;/th>
 &lt;th>Evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Trigger issued&lt;/td>
 &lt;td>command / provider event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Old primary down&lt;/td>
 &lt;td>connection error / health check&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>New primary promoted&lt;/td>
 &lt;td>&lt;code>pg_is_in_recovery() = false&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client reconnect&lt;/td>
 &lt;td>first successful write&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pooler stable&lt;/td>
 &lt;td>pool queue / server connection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation complete&lt;/td>
 &lt;td>row count / marker sequence&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Promotion timeline 要保留秒級時間戳。這是評估 RTO、client retry 與 pooler behavior 的基礎。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL HA failover drill 的核心責任是讓讀者觀察 primary promotion 對 application、pooler 與 incident decision 的影響。這篇承接 <a href="../../patroni-ha/">Patroni HA</a> 與 <a href="../../cross-region-dr/">Cross-region DR</a>。</p>
<p>本文的驗收標準是：你能記錄 failover timeline、replication lag snapshot、client error sample、data validation query 與 incident decision log entry。實際觸發方式依 Patroni、managed PostgreSQL 或雲平台而異；lab 重點是 evidence。</p>
<h2 id="pre-failover-baseline">Pre-Failover Baseline</h2>
<p>Pre-failover baseline 的核心責任是確認 primary / standby 狀態與 client route。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_is_in_recovery</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">now</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_current_wal_lsn</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">application_name</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">sync_state</span><span class="p">,</span><span class="w"> </span><span class="n">replay_lag</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_replication</span><span class="p">;</span></span></span></code></pre></div><p>在 standby 查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_is_in_recovery</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">now</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_receive_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_replay_lsn</span><span class="p">();</span></span></span></code></pre></div><p>Baseline 要保存 primary host、standby host、replication lag、application connection string、pooler route 與 current timeline。</p>
<h2 id="client-workload">Client Workload</h2>
<p>Client workload 的核心責任是讓 failover 對 application 的影響可見。</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="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  date -u
</span></span><span class="line"><span class="ln">3</span><span class="cl">  psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;INSERT INTO restore_markers(marker) VALUES (&#39;failover-drill&#39;) RETURNING id, created_at;&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個 loop 會在 failover 期間產生成功、timeout、connection reset 或 read-only error。正式演練要用 synthetic workload，避免影響真實使用者。</p>
<h2 id="trigger-failover">Trigger Failover</h2>
<p>Trigger failover 的核心責任是以受控方式促成 promotion。Patroni lab 可以用 <code>patronictl failover</code>；managed service 則用 provider failover / reboot with failover 功能。</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">failover_start_time:
</span></span><span class="line"><span class="ln">2</span><span class="cl">trigger_method:
</span></span><span class="line"><span class="ln">3</span><span class="cl">old_primary:
</span></span><span class="line"><span class="ln">4</span><span class="cl">candidate:
</span></span><span class="line"><span class="ln">5</span><span class="cl">operator:
</span></span><span class="line"><span class="ln">6</span><span class="cl">reason:</span></span></code></pre></div><p>Failover 觸發前要先確認這是演練，並且 workload、backup、rollback 與 stakeholder 都已對齊。</p>
<h2 id="observe-promotion">Observe Promotion</h2>
<p>Observe promotion 的核心責任是記錄資料庫與 client 的時間線。</p>
<table>
  <thead>
      <tr>
          <th>時間點</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trigger issued</td>
          <td>command / provider event</td>
      </tr>
      <tr>
          <td>Old primary down</td>
          <td>connection error / health check</td>
      </tr>
      <tr>
          <td>New primary promoted</td>
          <td><code>pg_is_in_recovery() = false</code></td>
      </tr>
      <tr>
          <td>Client reconnect</td>
          <td>first successful write</td>
      </tr>
      <tr>
          <td>Pooler stable</td>
          <td>pool queue / server connection</td>
      </tr>
      <tr>
          <td>Validation complete</td>
          <td>row count / marker sequence</td>
      </tr>
  </tbody>
</table>
<p>Promotion timeline 要保留秒級時間戳。這是評估 RTO、client retry 與 pooler behavior 的基礎。</p>
<h2 id="data-validation">Data Validation</h2>
<p>Data validation 的核心責任是確認 failover 後資料一致性。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">restore_markers</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;failover-drill&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">max</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">restore_markers</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">status</span><span class="p">;</span></span></span></code></pre></div><p>若 workload 有 idempotency key，還要檢查 duplicate。若外部 side effect 參與交易，例如 payment 或 queue，必須有 reconciliation query。</p>
<h2 id="pooler-and-client-behavior">Pooler and Client Behavior</h2>
<p>Pooler and client behavior 的核心責任是確認 failover 後連線能重新指向新 primary。</p>
<p>檢查項目：</p>
<ol>
<li>Application retry 是否有 backoff / jitter。</li>
<li>PgBouncer / proxy 是否清掉舊 server connection。</li>
<li>DNS / endpoint TTL 是否符合 RTO。</li>
<li>Read-only error 是否被正確分類。</li>
<li>Migration / background job 是否暫停。</li>
</ol>
<p>Failover 的完成標準包含 database promote、client reconnect 與 pooler stable。若 client 長時間連到舊 primary 或 pooler 卡住，服務仍處於 unavailable 狀態。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>Incident decision log 的核心責任是把演練變成可審查紀錄。</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">Incident / drill id:
</span></span><span class="line"><span class="ln">2</span><span class="cl">Decision: promote standby
</span></span><span class="line"><span class="ln">3</span><span class="cl">Reason:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Accepted data loss:
</span></span><span class="line"><span class="ln">5</span><span class="cl">RTO observed:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Client impact:
</span></span><span class="line"><span class="ln">7</span><span class="cl">Validation result:
</span></span><span class="line"><span class="ln">8</span><span class="cl">Follow-up:</span></span></code></pre></div><p>每次 drill 都要產生 follow-up。常見 follow-up 是調整 retry、降低 DNS TTL、補 pooler command、增加 validation query 或改善 monitoring。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>完成本篇後，HA 架構讀 <a href="../../patroni-ha/">Patroni HA</a>；跨區災難復原讀 <a href="../../cross-region-dr/">Cross-region DR</a>；connection retry 與 pooler 行為讀 <a href="../connection-pool-lab/">Connection Pool Lab</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Hands-on 操作路線</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/</guid><description>&lt;p>PostgreSQL hands-on 操作路線的核心責任是把 overview 與 deep article 的判讀轉成可演練的操作流程。這一層對齊 LLM &lt;code>hands-on/&lt;/code> 的功能：讀者不只知道 PostgreSQL 的機制，也能在 local lab 或 staging 產出可驗證 artifact。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>產出 artifact&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="local-lab-quickstart/">Local lab quickstart&lt;/a>&lt;/td>
 &lt;td>Docker Compose 啟動 PostgreSQL、建立 schema、跑 sample workload&lt;/td>
 &lt;td>local DSN、schema migration log、basic metric snapshot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="connection-pool-lab/">Connection pool lab&lt;/a>&lt;/td>
 &lt;td>application pool → pgBouncer → PostgreSQL 的連線壓力演練&lt;/td>
 &lt;td>pool config、connection count evidence、failure note&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="pitr-restore-drill/">PITR restore drill&lt;/a>&lt;/td>
 &lt;td>base backup + WAL archive + restore target time 的恢復演練&lt;/td>
 &lt;td>restore record、RPO / RTO evidence、validation query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="schema-migration-evidence-lab/">Schema migration evidence lab&lt;/a>&lt;/td>
 &lt;td>expand / contract migration、validation query、rollback condition&lt;/td>
 &lt;td>migration plan、row count、rollback note&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="ha-failover-drill/">HA failover drill&lt;/a>&lt;/td>
 &lt;td>Patroni / managed failover 的 application impact 演練&lt;/td>
 &lt;td>failover timeline、client error sample、decision log&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>PostgreSQL hands-on 章節只收錄能產出 evidence 的操作。純安裝指令留給官方文件；本路線要教讀者如何知道設定生效、失敗時看到什麼、以及 evidence 要交給 04 / 06 / 08 的哪個 artifact。&lt;/p>
&lt;h2 id="引用路徑">引用路徑&lt;/h2>
&lt;ul>
&lt;li>上游：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview&lt;/a>&lt;/li>
&lt;li>Deep article：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer Config&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &amp;#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &amp;#43; WAL archive 構成 PITR 的雙軌資料、archive_command &amp;#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &amp;#43; monitoring 整合">PITR + WAL Archiving&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &amp;#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA&lt;/a>&lt;/li>
&lt;li>跨模組：&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>PostgreSQL hands-on 操作路線的核心責任是把 overview 與 deep article 的判讀轉成可演練的操作流程。這一層對齊 LLM <code>hands-on/</code> 的功能：讀者不只知道 PostgreSQL 的機制，也能在 local lab 或 staging 產出可驗證 artifact。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>產出 artifact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="local-lab-quickstart/">Local lab quickstart</a></td>
          <td>Docker Compose 啟動 PostgreSQL、建立 schema、跑 sample workload</td>
          <td>local DSN、schema migration log、basic metric snapshot</td>
      </tr>
      <tr>
          <td><a href="connection-pool-lab/">Connection pool lab</a></td>
          <td>application pool → pgBouncer → PostgreSQL 的連線壓力演練</td>
          <td>pool config、connection count evidence、failure note</td>
      </tr>
      <tr>
          <td><a href="pitr-restore-drill/">PITR restore drill</a></td>
          <td>base backup + WAL archive + restore target time 的恢復演練</td>
          <td>restore record、RPO / RTO evidence、validation query</td>
      </tr>
      <tr>
          <td><a href="schema-migration-evidence-lab/">Schema migration evidence lab</a></td>
          <td>expand / contract migration、validation query、rollback condition</td>
          <td>migration plan、row count、rollback note</td>
      </tr>
      <tr>
          <td><a href="ha-failover-drill/">HA failover drill</a></td>
          <td>Patroni / managed failover 的 application impact 演練</td>
          <td>failover timeline、client error sample、decision log</td>
      </tr>
  </tbody>
</table>
<h2 id="設計原則">設計原則</h2>
<p>PostgreSQL hands-on 章節只收錄能產出 evidence 的操作。純安裝指令留給官方文件；本路線要教讀者如何知道設定生效、失敗時看到什麼、以及 evidence 要交給 04 / 06 / 08 的哪個 artifact。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL overview</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer Config</a>、<a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL Archiving</a>、<a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>、<a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Local Lab Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/local-lab-quickstart/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/local-lab-quickstart/</guid><description>&lt;p>PostgreSQL local lab quickstart 的核心責任是建立後續 connection、migration、backup 與 failover 演練共用的本地環境。這個 lab 提供一個可重建的 PostgreSQL instance、app-facing user、baseline schema、seed data 與 basic evidence。&lt;/p>
&lt;p>本文的驗收標準是：你能啟動本地 PostgreSQL，套用 schema，跑 sample workload，取得 &lt;code>pg_stat_activity&lt;/code> / &lt;code>pg_stat_database&lt;/code> snapshot，最後 teardown 並重建。&lt;/p>
&lt;h2 id="docker-compose">Docker Compose&lt;/h2>
&lt;p>Docker Compose 的核心責任是讓 lab 環境可重建。建立 &lt;code>docker-compose.yml&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">postgres&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lab_admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lab_admin_pw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_DB&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">appdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;54329:5432&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;postgres&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;log_min_duration_statement=100&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;shared_preload_libraries=pg_stat_statements&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">DATABASE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;postgres://lab_admin:lab_admin_pw@localhost:54329/appdb?sslmode=disable&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="baseline-schema">Baseline Schema&lt;/h2>
&lt;p>Baseline schema 的核心責任是建立可測 transaction、index、lock 與 migration 的資料模型。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE accounts (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> id bigserial PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> tenant_id uuid NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> owner_name text NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> status text NOT NULL CHECK (status IN (&amp;#39;active&amp;#39;, &amp;#39;closed&amp;#39;)),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at timestamptz NOT NULL DEFAULT now()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE ledger_entries (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> id bigserial PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> account_id bigint NOT NULL REFERENCES accounts(id),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> amount_cents bigint NOT NULL CHECK (amount_cents &amp;lt;&amp;gt; 0),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> idempotency_key text NOT NULL UNIQUE,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at timestamptz NOT NULL DEFAULT now()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s">);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE INDEX idx_ledger_entries_account_created
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">ON ledger_entries(account_id, created_at DESC);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這組 schema 後續可用於 migration、lock、PITR 與 pool lab。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL local lab quickstart 的核心責任是建立後續 connection、migration、backup 與 failover 演練共用的本地環境。這個 lab 提供一個可重建的 PostgreSQL instance、app-facing user、baseline schema、seed data 與 basic evidence。</p>
<p>本文的驗收標準是：你能啟動本地 PostgreSQL，套用 schema，跑 sample workload，取得 <code>pg_stat_activity</code> / <code>pg_stat_database</code> snapshot，最後 teardown 並重建。</p>
<h2 id="docker-compose">Docker Compose</h2>
<p>Docker Compose 的核心責任是讓 lab 環境可重建。建立 <code>docker-compose.yml</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">postgres</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:16</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">lab_admin</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">lab_admin_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">appdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;54329:5432&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;postgres&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;-c&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;log_min_duration_statement=100&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;-c&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;shared_preload_libraries=pg_stat_statements&#34;</span></span></span></code></pre></div><p>啟動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose up -d
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">&#34;postgres://lab_admin:lab_admin_pw@localhost:54329/appdb?sslmode=disable&#34;</span></span></span></code></pre></div><h2 id="baseline-schema">Baseline Schema</h2>
<p>Baseline schema 的核心責任是建立可測 transaction、index、lock 與 migration 的資料模型。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">CREATE TABLE accounts (
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">  id bigserial PRIMARY KEY,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  tenant_id uuid NOT NULL,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  owner_name text NOT NULL,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  status text NOT NULL CHECK (status IN (&#39;active&#39;, &#39;closed&#39;)),
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  created_at timestamptz NOT NULL DEFAULT now()
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">);
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">CREATE TABLE ledger_entries (
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  id bigserial PRIMARY KEY,
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  account_id bigint NOT NULL REFERENCES accounts(id),
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  amount_cents bigint NOT NULL CHECK (amount_cents &lt;&gt; 0),
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  idempotency_key text NOT NULL UNIQUE,
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  created_at timestamptz NOT NULL DEFAULT now()
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">);
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">CREATE INDEX idx_ledger_entries_account_created
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">ON ledger_entries(account_id, created_at DESC);
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>這組 schema 後續可用於 migration、lock、PITR 與 pool lab。</p>
<h2 id="seed-and-workload">Seed and Workload</h2>
<p>Seed and workload 的核心責任是產生可觀察的資料與查詢。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">INSERT INTO accounts(tenant_id, owner_name, status)
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">VALUES
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  (&#39;00000000-0000-0000-0000-000000000001&#39;, &#39;Ada&#39;, &#39;active&#39;),
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">  (&#39;00000000-0000-0000-0000-000000000002&#39;, &#39;Lin&#39;, &#39;active&#39;);
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">SELECT 1, 100, &#39;seed-ada-&#39; || g
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">FROM generate_series(1, 100) AS g;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">FROM accounts a
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">GROUP BY a.owner_name;
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Sample workload 要保留 SQL 與輸出，作為後續 migration / restore validation 的 baseline。</p>
<h2 id="basic-evidence">Basic Evidence</h2>
<p>Basic evidence 的核心責任是把 lab 狀態保存成可比較 snapshot。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">SELECT current_database(), current_user, version();
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">SELECT datname, numbackends, xact_commit, xact_rollback
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">FROM pg_stat_database
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">WHERE datname = current_database();
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">SELECT pid, state, wait_event_type, query
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">FROM pg_stat_activity
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">WHERE datname = current_database();
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>這些查詢是 PostgreSQL lab 的最小 evidence。正式服務要再加入 slow query、lock wait、replica lag、backup status 與 pooler metrics。</p>
<h2 id="teardown">Teardown</h2>
<p>Teardown 的核心責任是讓 lab 可重跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose down -v</span></span></code></pre></div><p>重建後應能重新套用 schema 與 seed。若 lab 需要跨章節沿用資料，先用 <code>pg_dump</code> 保存 fixture，再 teardown。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>完成本篇後，連線壓力進入 <a href="../connection-pool-lab/">Connection Pool Lab</a>；migration evidence 進入 <a href="../schema-migration-evidence-lab/">Schema Migration Evidence Lab</a>；backup / PITR 進入 <a href="../pitr-restore-drill/">PITR Restore Drill</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Logical Decoding Plugins</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/logical-decoding-plugins/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/logical-decoding-plugins/</guid><description>&lt;p>PostgreSQL logical decoding plugins 的核心責任是把 WAL 中的變更轉成外部消費者可理解的事件格式。PostgreSQL 官方 logical decoding 文件說明，logical decoding 透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replication-slot/" data-link-title="Replication Slot" data-link-desc="說明邏輯複製如何用 slot 追蹤消費進度，並對來源端造成保留壓力">replication slot&lt;/a> 將 WAL 變更解碼成 plugin output；output plugin 決定外部看到的是 PostgreSQL protocol、JSON、測試文字或自訂格式。&lt;/p>
&lt;p>本文的判讀錨點是：plugin 選型是 CDC contract 決策。它影響 schema evolution、事件欄位、delete 表示、transaction boundary、consumer compatibility、slot lag 與故障復原。&lt;/p>
&lt;h2 id="plugin-boundary">Plugin Boundary&lt;/h2>
&lt;p>Plugin boundary 的核心責任是定義 database 變更如何離開 PostgreSQL。常見選項包含內建 &lt;code>pgoutput&lt;/code>、測試用 &lt;code>test_decoding&lt;/code>、JSON-oriented plugin，以及 Debezium connector 支援的 plugin / protocol。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Plugin / path&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>pgoutput&lt;/code>&lt;/td>
 &lt;td>PostgreSQL logical replication protocol&lt;/td>
 &lt;td>built-in logical replication、Debezium 常見路線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>test_decoding&lt;/code>&lt;/td>
 &lt;td>人類可讀測試 output&lt;/td>
 &lt;td>lab、debug、教育用途&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>wal2json&lt;/code>&lt;/td>
 &lt;td>JSON change event&lt;/td>
 &lt;td>自訂 consumer、legacy CDC&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>decoderbufs&lt;/td>
 &lt;td>Protobuf event&lt;/td>
 &lt;td>強 schema contract 的 pipeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Native subscription&lt;/td>
 &lt;td>DB-to-DB replication&lt;/td>
 &lt;td>PostgreSQL 之間 table replication&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>pgoutput&lt;/code> 適合標準化 CDC。它與 publication / subscription model 對齊，能保留 PostgreSQL logical replication 的主路線。&lt;/p>
&lt;p>&lt;code>test_decoding&lt;/code> 適合教學與排錯。它讓人看到 transaction 裡發生的 insert / update / delete，但它的定位是測試與理解，不應作為正式 event contract。&lt;/p>
&lt;h2 id="replication-slot-responsibility">Replication Slot Responsibility&lt;/h2>
&lt;p>Replication slot responsibility 的核心責任是保護 consumer 進度，同時管理 WAL retention。Logical slot 會讓 PostgreSQL 保留尚未被 consumer 確認的 WAL；consumer 停住時，slot lag 會轉成 disk pressure。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;th>操作反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>confirmed_flush_lsn&lt;/code>&lt;/td>
 &lt;td>consumer 已確認的位置&lt;/td>
 &lt;td>用來判斷 CDC 進度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>retained WAL size&lt;/td>
 &lt;td>slot 造成的 WAL 保留量&lt;/td>
 &lt;td>alert、調整 consumer、drop / advance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>inactive slot&lt;/td>
 &lt;td>consumer 離線&lt;/td>
 &lt;td>檢查 connector、暫停 release&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>publication table diff&lt;/td>
 &lt;td>CDC scope 與 schema 不一致&lt;/td>
 &lt;td>review publication / table ownership&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Slot 是 production resource。每個 logical slot 都要有 owner、consumer、SLO、drop condition、backfill plan 與 alert。&lt;/p>
&lt;h2 id="event-contract">Event Contract&lt;/h2>
&lt;p>Event contract 的核心責任是讓 downstream 知道每個變更代表什麼。CDC 事件至少要說明 key、before/after image、operation、commit timestamp、transaction ordering、schema version 與 delete representation。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL logical decoding plugins 的核心責任是把 WAL 中的變更轉成外部消費者可理解的事件格式。PostgreSQL 官方 logical decoding 文件說明，logical decoding 透過 <a href="/blog/backend/knowledge-cards/replication-slot/" data-link-title="Replication Slot" data-link-desc="說明邏輯複製如何用 slot 追蹤消費進度，並對來源端造成保留壓力">replication slot</a> 將 WAL 變更解碼成 plugin output；output plugin 決定外部看到的是 PostgreSQL protocol、JSON、測試文字或自訂格式。</p>
<p>本文的判讀錨點是：plugin 選型是 CDC contract 決策。它影響 schema evolution、事件欄位、delete 表示、transaction boundary、consumer compatibility、slot lag 與故障復原。</p>
<h2 id="plugin-boundary">Plugin Boundary</h2>
<p>Plugin boundary 的核心責任是定義 database 變更如何離開 PostgreSQL。常見選項包含內建 <code>pgoutput</code>、測試用 <code>test_decoding</code>、JSON-oriented plugin，以及 Debezium connector 支援的 plugin / protocol。</p>
<table>
  <thead>
      <tr>
          <th>Plugin / path</th>
          <th>主要責任</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pgoutput</code></td>
          <td>PostgreSQL logical replication protocol</td>
          <td>built-in logical replication、Debezium 常見路線</td>
      </tr>
      <tr>
          <td><code>test_decoding</code></td>
          <td>人類可讀測試 output</td>
          <td>lab、debug、教育用途</td>
      </tr>
      <tr>
          <td><code>wal2json</code></td>
          <td>JSON change event</td>
          <td>自訂 consumer、legacy CDC</td>
      </tr>
      <tr>
          <td>decoderbufs</td>
          <td>Protobuf event</td>
          <td>強 schema contract 的 pipeline</td>
      </tr>
      <tr>
          <td>Native subscription</td>
          <td>DB-to-DB replication</td>
          <td>PostgreSQL 之間 table replication</td>
      </tr>
  </tbody>
</table>
<p><code>pgoutput</code> 適合標準化 CDC。它與 publication / subscription model 對齊，能保留 PostgreSQL logical replication 的主路線。</p>
<p><code>test_decoding</code> 適合教學與排錯。它讓人看到 transaction 裡發生的 insert / update / delete，但它的定位是測試與理解，不應作為正式 event contract。</p>
<h2 id="replication-slot-responsibility">Replication Slot Responsibility</h2>
<p>Replication slot responsibility 的核心責任是保護 consumer 進度，同時管理 WAL retention。Logical slot 會讓 PostgreSQL 保留尚未被 consumer 確認的 WAL；consumer 停住時，slot lag 會轉成 disk pressure。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>意義</th>
          <th>操作反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>confirmed_flush_lsn</code></td>
          <td>consumer 已確認的位置</td>
          <td>用來判斷 CDC 進度</td>
      </tr>
      <tr>
          <td>retained WAL size</td>
          <td>slot 造成的 WAL 保留量</td>
          <td>alert、調整 consumer、drop / advance</td>
      </tr>
      <tr>
          <td>inactive slot</td>
          <td>consumer 離線</td>
          <td>檢查 connector、暫停 release</td>
      </tr>
      <tr>
          <td>publication table diff</td>
          <td>CDC scope 與 schema 不一致</td>
          <td>review publication / table ownership</td>
      </tr>
  </tbody>
</table>
<p>Slot 是 production resource。每個 logical slot 都要有 owner、consumer、SLO、drop condition、backfill plan 與 alert。</p>
<h2 id="event-contract">Event Contract</h2>
<p>Event contract 的核心責任是讓 downstream 知道每個變更代表什麼。CDC 事件至少要說明 key、before/after image、operation、commit timestamp、transaction ordering、schema version 與 delete representation。</p>
<table>
  <thead>
      <tr>
          <th>Contract 面向</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Key</td>
          <td>table 是否有 replica identity / primary key</td>
      </tr>
      <tr>
          <td>Update image</td>
          <td>是否需要 before value</td>
      </tr>
      <tr>
          <td>Delete</td>
          <td>tombstone、key-only delete、soft delete</td>
      </tr>
      <tr>
          <td>Ordering</td>
          <td>transaction order 是否要保留</td>
      </tr>
      <tr>
          <td>Schema evolution</td>
          <td>新欄位、rename、drop 欄位如何通知</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>initial snapshot 與 streaming 如何銜接</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/replica-identity/" data-link-title="Replica Identity" data-link-desc="說明 row-level 變更事件如何帶穩定 key，讓下游能正確套用 update 與 delete">Replica identity</a> 是 CDC 的核心設定。沒有穩定 key 的 table 會讓 update / delete event 難以被 downstream 正確套用；這類 table 要先補 primary key 或明確設定 replica identity。</p>
<h2 id="connector-patterns">Connector Patterns</h2>
<p>Connector patterns 的核心責任是把 plugin output 接到實際 pipeline。Debezium、custom consumer、DB native subscription 的維運責任不同。</p>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>優點</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debezium connector</td>
          <td>成熟 snapshot + streaming workflow</td>
          <td>connector state、Kafka / offset operation</td>
      </tr>
      <tr>
          <td>Native subscription</td>
          <td>PostgreSQL 原生 DB-to-DB</td>
          <td>schema drift、DDL coordination</td>
      </tr>
      <tr>
          <td>Custom consumer</td>
          <td>可客製 event contract</td>
          <td>slot management 與 error handling 自行負責</td>
      </tr>
      <tr>
          <td>Batch export + CDC</td>
          <td>backfill 與 streaming 分開</td>
          <td>cutover LSN 與 duplication handling</td>
      </tr>
  </tbody>
</table>
<p>Connector 要定義 backfill 與 streaming 的接點。最常見的事故是 snapshot 還沒完成就開始消費、或 cutover LSN 沒有被記錄，導致 downstream 重複或漏資料。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 CDC 事故分成 database、connector、schema 與 downstream 四層。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>第一反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot lag growth</td>
          <td>retained WAL 持續增加</td>
          <td>暫停重型寫入、修 connector、評估 drop</td>
      </tr>
      <tr>
          <td>Schema break</td>
          <td>connector 解析失敗</td>
          <td>停止 DDL rollout、補 schema evolution</td>
      </tr>
      <tr>
          <td>Missing key</td>
          <td>update / delete 缺少可套用 key</td>
          <td>修 replica identity / key contract</td>
      </tr>
      <tr>
          <td>Duplicate event</td>
          <td>consumer 重啟或 offset 回退</td>
          <td>idempotent consumer</td>
      </tr>
      <tr>
          <td>Downstream slow</td>
          <td>Kafka / sink lag 增加</td>
          <td>擴 sink、調 batch、保護 slot</td>
      </tr>
  </tbody>
</table>
<p>Slot lag 是最高優先訊號，因為它會占用 PostgreSQL WAL storage。Runbook 要有「何時暫停 producer」、「何時 drop slot」、「如何重建 snapshot」的明確門檻。</p>
<h2 id="selection-checklist">Selection Checklist</h2>
<p>Selection checklist 的核心責任是讓 plugin 選型可審查。</p>
<ol>
<li>Downstream 需要 DB-to-DB replication、JSON event、Protobuf event 還是 connector-managed event。</li>
<li>每張 table 是否有 stable key 與 replica identity。</li>
<li>Initial snapshot 如何銜接 streaming。</li>
<li>Schema evolution 如何通知 consumer。</li>
<li>Slot lag、connector lag、sink lag 如何告警。</li>
<li>Consumer 是否 idempotent。</li>
<li>Disaster recovery 後 slot / offset 如何重建。</li>
</ol>
<p>完成這份 checklist 後，再決定 plugin 與 connector。CDC 的成功標準是 downstream 能長期維持正確資料，而不只是成功建立 slot。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Logical decoding plugins 完成後，實作 CDC pipeline 讀 <a href="../logical-replication-debezium/">Logical Replication / Debezium</a>；slot 維運讀 <a href="../replication-slot-management/">Replication Slot Management</a>；跨資料庫搬遷讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL pg_partman Advanced</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pg-partman-advanced/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pg-partman-advanced/</guid><description>&lt;p>PostgreSQL pg_partman advanced 的核心責任是把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">declarative partitioning&lt;/a> 的日常維護自動化。pg_partman 可以協助建立未來 partition、管理 retention、執行 maintenance job，讓 time-based 或 serial-based partition 不再依賴人工 DDL。&lt;/p>
&lt;p>本文的判讀錨點是：pg_partman 解決的是 partition lifecycle operation，而非 partition strategy 本身。Partition key、query pattern、retention、index、foreign key 與 migration 仍要先在 &lt;a href="../declarative-partitioning/">Declarative Partitioning&lt;/a> 與 &lt;a href="../partition-redesign/">Partition Redesign&lt;/a> 做對。&lt;/p>
&lt;h2 id="responsibility-boundary">Responsibility Boundary&lt;/h2>
&lt;p>Responsibility boundary 的核心責任是區分 PostgreSQL 原生 partition 和 pg_partman。&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>PostgreSQL declarative partitioning&lt;/td>
 &lt;td>partition table、constraint、planner pruning&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>pg_partman&lt;/td>
 &lt;td>future partition premake、retention、maintenance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scheduler / job runner&lt;/td>
 &lt;td>定期執行 maintenance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DBA / platform&lt;/td>
 &lt;td>monitoring、backup、DDL review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>query pattern、partition key 使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>pg_partman 的價值在於減少重複 DDL。它不會替 application 選出正確 partition key，也不會自動修復跨 partition query 設計。&lt;/p>
&lt;h2 id="core-concepts">Core Concepts&lt;/h2>
&lt;p>Core concepts 的核心責任是理解 pg_partman operation vocabulary。&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>Parent table&lt;/td>
 &lt;td>partitioned table 的入口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Child table&lt;/td>
 &lt;td>實際存放資料的 partition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Premake&lt;/td>
 &lt;td>預先建立未來 partition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>自動 detach / drop 舊 partition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Maintenance&lt;/td>
 &lt;td>建立新 partition、處理 retention 的 job&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Template&lt;/td>
 &lt;td>child partition 繼承 index / constraint 的模板&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Premake 是防止 insert 打到不存在 partition 的保護。若 partition 建立落後於時間，application insert 會失敗或落到 default partition；production 要對 future partition count 設 alert。&lt;/p>
&lt;p>Retention 是資料生命週期操作。Drop 舊 partition 速度快，但要先確認 legal retention、backup、analytics dependency 與 downstream CDC。&lt;/p>
&lt;h2 id="setup-pattern">Setup Pattern&lt;/h2>
&lt;p>Setup pattern 的核心責任是把 pg_partman 導入流程放進 migration gate。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_partman&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bigserial&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">uuid&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">timestamptz&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonb&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RANGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實際建立 partman config 要依 pg_partman 版本與 provider 支援文件執行。Managed PostgreSQL 可能限制 extension version、background worker 或 scheduler，因此 setup 前要先確認 provider boundary。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL pg_partman advanced 的核心責任是把 <a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">declarative partitioning</a> 的日常維護自動化。pg_partman 可以協助建立未來 partition、管理 retention、執行 maintenance job，讓 time-based 或 serial-based partition 不再依賴人工 DDL。</p>
<p>本文的判讀錨點是：pg_partman 解決的是 partition lifecycle operation，而非 partition strategy 本身。Partition key、query pattern、retention、index、foreign key 與 migration 仍要先在 <a href="../declarative-partitioning/">Declarative Partitioning</a> 與 <a href="../partition-redesign/">Partition Redesign</a> 做對。</p>
<h2 id="responsibility-boundary">Responsibility Boundary</h2>
<p>Responsibility boundary 的核心責任是區分 PostgreSQL 原生 partition 和 pg_partman。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PostgreSQL declarative partitioning</td>
          <td>partition table、constraint、planner pruning</td>
      </tr>
      <tr>
          <td>pg_partman</td>
          <td>future partition premake、retention、maintenance</td>
      </tr>
      <tr>
          <td>Scheduler / job runner</td>
          <td>定期執行 maintenance</td>
      </tr>
      <tr>
          <td>DBA / platform</td>
          <td>monitoring、backup、DDL review</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>query pattern、partition key 使用</td>
      </tr>
  </tbody>
</table>
<p>pg_partman 的價值在於減少重複 DDL。它不會替 application 選出正確 partition key，也不會自動修復跨 partition query 設計。</p>
<h2 id="core-concepts">Core Concepts</h2>
<p>Core concepts 的核心責任是理解 pg_partman operation vocabulary。</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Parent table</td>
          <td>partitioned table 的入口</td>
      </tr>
      <tr>
          <td>Child table</td>
          <td>實際存放資料的 partition</td>
      </tr>
      <tr>
          <td>Premake</td>
          <td>預先建立未來 partition</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>自動 detach / drop 舊 partition</td>
      </tr>
      <tr>
          <td>Maintenance</td>
          <td>建立新 partition、處理 retention 的 job</td>
      </tr>
      <tr>
          <td>Template</td>
          <td>child partition 繼承 index / constraint 的模板</td>
      </tr>
  </tbody>
</table>
<p>Premake 是防止 insert 打到不存在 partition 的保護。若 partition 建立落後於時間，application insert 會失敗或落到 default partition；production 要對 future partition count 設 alert。</p>
<p>Retention 是資料生命週期操作。Drop 舊 partition 速度快，但要先確認 legal retention、backup、analytics dependency 與 downstream CDC。</p>
<h2 id="setup-pattern">Setup Pattern</h2>
<p>Setup pattern 的核心責任是把 pg_partman 導入流程放進 migration gate。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">pg_partman</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="n">bigserial</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="n">tenant_id</span><span class="w"> </span><span class="n">uuid</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="n">timestamptz</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="n">payload</span><span class="w"> </span><span class="n">jsonb</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">RANGE</span><span class="w"> </span><span class="p">(</span><span class="n">created_at</span><span class="p">);</span></span></span></code></pre></div><p>實際建立 partman config 要依 pg_partman 版本與 provider 支援文件執行。Managed PostgreSQL 可能限制 extension version、background worker 或 scheduler，因此 setup 前要先確認 provider boundary。</p>
<p>最小 setup evidence：</p>
<ol>
<li>Extension version。</li>
<li>Parent table DDL。</li>
<li>Partition key 與 interval。</li>
<li>Premake 數量。</li>
<li>Retention policy。</li>
<li>Maintenance job schedule。</li>
<li>Test insert 到 current / future partition。</li>
</ol>
<h2 id="maintenance-runbook">Maintenance Runbook</h2>
<p>Maintenance runbook 的核心責任是讓 partition lifecycle 可觀測。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>意義</th>
          <th>反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>future partition count</td>
          <td>premake 是否足夠</td>
          <td>手動跑 maintenance、修 scheduler</td>
      </tr>
      <tr>
          <td>default partition rows</td>
          <td>routing 失敗或 partition 缺漏</td>
          <td>建 partition、搬資料、修 app timestamp</td>
      </tr>
      <tr>
          <td>old partition count</td>
          <td>retention 是否執行</td>
          <td>檢查 policy、legal hold、job error</td>
      </tr>
      <tr>
          <td>maintenance duration</td>
          <td>DDL / lock / catalog 壓力</td>
          <td>調整 schedule、拆 table</td>
      </tr>
      <tr>
          <td>index build time</td>
          <td>child index 建立成本</td>
          <td>template / concurrent strategy review</td>
      </tr>
  </tbody>
</table>
<p>Maintenance job 要有 owner。Cron、pg_cron、background worker、Kubernetes job 或 managed scheduler 都可以；重點是 job failure 會告警，並且有人處理。</p>
<h2 id="migration-and-backfill">Migration and Backfill</h2>
<p>Migration and backfill 的核心責任是把既有大表轉成 partman-managed partition。這通常比新表導入更高風險。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audit</td>
          <td>table size、query pattern、write rate</td>
      </tr>
      <tr>
          <td>New schema</td>
          <td>parent table、child partition、index</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>batch size、lag、lock、checksum</td>
      </tr>
      <tr>
          <td>Dual write</td>
          <td>app compatibility</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>rename / view / routing switch</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>old table retention、rollback</td>
      </tr>
  </tbody>
</table>
<p>Backfill 要控制 WAL、replica lag、autovacuum、index bloat 與 lock。大型 table 應先用 shadow table 或 partition redesign playbook，避開 peak traffic 直接重建。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是列出 pg_partman 常見事故。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>未建立未來 partition</td>
          <td>insert 失敗或 default partition 增長</td>
          <td>補 partition、修 maintenance schedule</td>
      </tr>
      <tr>
          <td>retention drop 過早</td>
          <td>查詢缺歷史資料</td>
          <td>restore backup、調 policy、legal review</td>
      </tr>
      <tr>
          <td>managed provider 不支援</td>
          <td>extension / worker 限制</td>
          <td>改 manual partition job 或 provider</td>
      </tr>
      <tr>
          <td>index / constraint 漂移</td>
          <td>child partition schema 不一致</td>
          <td>template review、schema diff</td>
      </tr>
      <tr>
          <td>planner pruning 失效</td>
          <td>query 未帶 partition key</td>
          <td>query rewrite、index review</td>
      </tr>
  </tbody>
</table>
<p>pg_partman 事故通常是 lifecycle 事故。Runbook 要先看 maintenance job，再看 partition metadata 與 application query。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>pg_partman advanced 完成後，partition 設計讀 <a href="../declarative-partitioning/">Declarative Partitioning</a>；重排策略讀 <a href="../partition-redesign/">Partition Redesign</a>；migration gate 讀 <a href="../online-schema-change/">Online Schema Change</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL PITR Restore Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/pitr-restore-drill/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/pitr-restore-drill/</guid><description>&lt;p>PostgreSQL PITR restore drill 的核心責任是證明 backup 可以還原到指定時間點。這篇承接 &lt;a href="../../pitr-wal-archiving/">PITR + WAL Archiving&lt;/a>，把備份從存在狀態推進到可恢復證據。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 base backup 時間、target time、restore duration、validation query 與 RPO / RTO note。實際命令會依 pgBackRest、Barman、cloud snapshot 或 managed service 而變；本文提供 vendor-neutral drill frame。&lt;/p>
&lt;h2 id="prepare-recovery-point">Prepare Recovery Point&lt;/h2>
&lt;p>Prepare recovery point 的核心責任是建立可辨識 transaction。先寫入一筆 marker，記錄時間。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE IF NOT EXISTS restore_markers (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s"> id bigserial PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s"> marker text NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at timestamptz NOT NULL DEFAULT clock_timestamp()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO restore_markers(marker) VALUES (&amp;#39;before-bad-change&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT id, marker, created_at FROM restore_markers ORDER BY id DESC LIMIT 1;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把 &lt;code>created_at&lt;/code> 記為 target time。正式 drill 要用 UTC，並記錄 timezone、operator、backup set 與 WAL archive status。&lt;/p>
&lt;h2 id="create-bad-change">Create Bad Change&lt;/h2>
&lt;p>Create bad change 的核心責任是模擬需要 PITR 的錯誤。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO restore_markers(marker) VALUES (&amp;#39;bad-change-after-target&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">UPDATE accounts SET status = &amp;#39;closed&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT status, count(*) FROM accounts GROUP BY status;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一步在 lab 中代表誤操作。Production 事故中，bad change 可能是誤刪、錯誤 batch、壞 migration 或 application bug。&lt;/p>
&lt;h2 id="restore-workflow">Restore Workflow&lt;/h2>
&lt;p>Restore workflow 的核心責任是把 backup tool 的操作轉成固定 evidence。不同工具命令不同，但流程一致：&lt;/p>
&lt;ol>
&lt;li>選定 base backup。&lt;/li>
&lt;li>設定 recovery target time。&lt;/li>
&lt;li>套用 WAL 到 target time。&lt;/li>
&lt;li>Promote restored instance。&lt;/li>
&lt;li>跑 validation query。&lt;/li>
&lt;li>啟動 application smoke test。&lt;/li>
&lt;/ol>
&lt;p>Example pseudo-runbook：&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">restore_target_time = 2026-05-21T10:15:30Z
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">base_backup = latest backup before target
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">wal_archive = available through target
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">restore_path = isolated environment&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restore 必須在隔離環境先完成。直接覆蓋 production 會讓 evidence 與 rollback 空間消失。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL PITR restore drill 的核心責任是證明 backup 可以還原到指定時間點。這篇承接 <a href="../../pitr-wal-archiving/">PITR + WAL Archiving</a>，把備份從存在狀態推進到可恢復證據。</p>
<p>本文的驗收標準是：你能記錄 base backup 時間、target time、restore duration、validation query 與 RPO / RTO note。實際命令會依 pgBackRest、Barman、cloud snapshot 或 managed service 而變；本文提供 vendor-neutral drill frame。</p>
<h2 id="prepare-recovery-point">Prepare Recovery Point</h2>
<p>Prepare recovery point 的核心責任是建立可辨識 transaction。先寫入一筆 marker，記錄時間。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">CREATE TABLE IF NOT EXISTS restore_markers (
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  id bigserial PRIMARY KEY,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  marker text NOT NULL,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">  created_at timestamptz NOT NULL DEFAULT clock_timestamp()
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">);
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">INSERT INTO restore_markers(marker) VALUES (&#39;before-bad-change&#39;);
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">SELECT id, marker, created_at FROM restore_markers ORDER BY id DESC LIMIT 1;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>把 <code>created_at</code> 記為 target time。正式 drill 要用 UTC，並記錄 timezone、operator、backup set 與 WAL archive status。</p>
<h2 id="create-bad-change">Create Bad Change</h2>
<p>Create bad change 的核心責任是模擬需要 PITR 的錯誤。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">INSERT INTO restore_markers(marker) VALUES (&#39;bad-change-after-target&#39;);
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">UPDATE accounts SET status = &#39;closed&#39;;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SELECT status, count(*) FROM accounts GROUP BY status;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>這一步在 lab 中代表誤操作。Production 事故中，bad change 可能是誤刪、錯誤 batch、壞 migration 或 application bug。</p>
<h2 id="restore-workflow">Restore Workflow</h2>
<p>Restore workflow 的核心責任是把 backup tool 的操作轉成固定 evidence。不同工具命令不同，但流程一致：</p>
<ol>
<li>選定 base backup。</li>
<li>設定 recovery target time。</li>
<li>套用 WAL 到 target time。</li>
<li>Promote restored instance。</li>
<li>跑 validation query。</li>
<li>啟動 application smoke test。</li>
</ol>
<p>Example pseudo-runbook：</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">restore_target_time = 2026-05-21T10:15:30Z
</span></span><span class="line"><span class="ln">2</span><span class="cl">base_backup = latest backup before target
</span></span><span class="line"><span class="ln">3</span><span class="cl">wal_archive = available through target
</span></span><span class="line"><span class="ln">4</span><span class="cl">restore_path = isolated environment</span></span></code></pre></div><p>Restore 必須在隔離環境先完成。直接覆蓋 production 會讓 evidence 與 rollback 空間消失。</p>
<h2 id="validation-query">Validation Query</h2>
<p>Validation query 的核心責任是確認 restore 到正確時間點。</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">psql <span class="s2">&#34;</span><span class="nv">$RESTORED_DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT marker, created_at
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">FROM restore_markers
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">ORDER BY id;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SELECT status, count(*)
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">FROM accounts
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">GROUP BY status;
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>預期結果是存在 <code>before-bad-change</code>，且 <code>bad-change-after-target</code> 尚未出現。<code>accounts</code> 狀態應維持 target time 前的分布。</p>
<h2 id="rpo--rto-evidence">RPO / RTO Evidence</h2>
<p>RPO / RTO evidence 的核心責任是把 drill 結果轉成服務語言。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>記錄內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup timestamp</td>
          <td>使用哪份 base backup</td>
      </tr>
      <tr>
          <td>Target time</td>
          <td>要恢復到哪一秒</td>
      </tr>
      <tr>
          <td>WAL availability</td>
          <td>target time 前後 WAL 是否完整</td>
      </tr>
      <tr>
          <td>Restore duration</td>
          <td>從開始 restore 到 validation 成功</td>
      </tr>
      <tr>
          <td>Data gap</td>
          <td>target time 後需補償的 transaction</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>application 核心 workflow 是否可用</td>
      </tr>
  </tbody>
</table>
<p>PITR 的成功標準是資料與 application 都可用。只讓 PostgreSQL 啟動成功，還不足以交付服務。</p>
<h2 id="drill-retrospective">Drill Retrospective</h2>
<p>Drill retrospective 的核心責任是把演練缺口轉成下一步。</p>
<p>常見缺口：</p>
<ol>
<li>找不到正確 base backup。</li>
<li>WAL archive 缺段。</li>
<li>target time timezone 混亂。</li>
<li>Restore 太慢，超過 RTO。</li>
<li>Application secret / config 指不到 restored DB。</li>
<li>Validation query 缺少 business invariant。</li>
</ol>
<p>完成本篇後，跨區恢復讀 <a href="../../cross-region-dr/">Cross-region DR</a>；備份策略讀 <a href="../../pitr-wal-archiving/">PITR + WAL Archiving</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Schema Migration Evidence Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/schema-migration-evidence-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/schema-migration-evidence-lab/</guid><description>&lt;p>PostgreSQL schema migration evidence lab 的核心責任是把 schema change 轉成 release gate 可使用的 evidence。這篇承接 &lt;a href="../../online-schema-change/">Online Schema Change&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能設計 expand migration、量測 lock、跑 backfill validation、建立 contract migration 的 fail-forward / rollback 判準。&lt;/p>
&lt;h2 id="expand-migration">Expand Migration&lt;/h2>
&lt;p>Expand migration 的核心責任是先加入向後相容 schema。以下範例新增 &lt;code>accounts.email&lt;/code>，先允許 null。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">\timing on
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">BEGIN;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">ALTER TABLE accounts ADD COLUMN email text;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">COMMIT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>新增 nullable column 通常是低風險操作，但仍要記錄 timing 與 lock。正式服務要在低流量窗口或 staging 上先測。&lt;/p>
&lt;h2 id="lock-evidence">Lock Evidence&lt;/h2>
&lt;p>Lock evidence 的核心責任是讓 migration 的阻塞風險可見。開另一個 terminal，在 migration 前後查 lock。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT locktype, relation::regclass, mode, granted, pid
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">FROM pg_locks
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">WHERE relation IN (&amp;#39;accounts&amp;#39;::regclass, &amp;#39;ledger_entries&amp;#39;::regclass)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">ORDER BY granted, mode;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Release gate 要保存 lock mode、duration、blocked session 與 application impact。高風險 DDL 要先改成 expand / backfill / contract。&lt;/p>
&lt;h2 id="backfill-and-validation">Backfill and Validation&lt;/h2>
&lt;p>Backfill and validation 的核心責任是把資料補齊並證明結果符合 domain。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">UPDATE accounts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SET email = lower(owner_name) || &amp;#39;@example.test&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">WHERE email IS NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT count(*) AS missing_email
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="s">FROM accounts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="s">WHERE email IS NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>大型表要分 batch backfill，避免 WAL、replica lag、autovacuum 與 lock 壓力。每個 batch 要記錄 row count、duration、error 與 lag。&lt;/p>
&lt;h2 id="add-constraint-safely">Add Constraint Safely&lt;/h2>
&lt;p>Add constraint safely 的核心責任是把資料驗證和 constraint 生效拆開。&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">psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">ALTER TABLE accounts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">ADD CONSTRAINT accounts_email_present
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">CHECK (email IS NOT NULL) NOT VALID;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">ALTER TABLE accounts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="s">VALIDATE CONSTRAINT accounts_email_present;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>NOT VALID&lt;/code> 讓 constraint 先約束新資料，再用 validation 掃既有資料。這是 PostgreSQL online migration 常用技巧。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL schema migration evidence lab 的核心責任是把 schema change 轉成 release gate 可使用的 evidence。這篇承接 <a href="../../online-schema-change/">Online Schema Change</a> 與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>。</p>
<p>本文的驗收標準是：你能設計 expand migration、量測 lock、跑 backfill validation、建立 contract migration 的 fail-forward / rollback 判準。</p>
<h2 id="expand-migration">Expand Migration</h2>
<p>Expand migration 的核心責任是先加入向後相容 schema。以下範例新增 <code>accounts.email</code>，先允許 null。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">\timing on
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">BEGIN;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">ALTER TABLE accounts ADD COLUMN email text;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">COMMIT;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>新增 nullable column 通常是低風險操作，但仍要記錄 timing 與 lock。正式服務要在低流量窗口或 staging 上先測。</p>
<h2 id="lock-evidence">Lock Evidence</h2>
<p>Lock evidence 的核心責任是讓 migration 的阻塞風險可見。開另一個 terminal，在 migration 前後查 lock。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT locktype, relation::regclass, mode, granted, pid
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">FROM pg_locks
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">WHERE relation IN (&#39;accounts&#39;::regclass, &#39;ledger_entries&#39;::regclass)
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">ORDER BY granted, mode;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Release gate 要保存 lock mode、duration、blocked session 與 application impact。高風險 DDL 要先改成 expand / backfill / contract。</p>
<h2 id="backfill-and-validation">Backfill and Validation</h2>
<p>Backfill and validation 的核心責任是把資料補齊並證明結果符合 domain。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">UPDATE accounts
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SET email = lower(owner_name) || &#39;@example.test&#39;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">WHERE email IS NULL;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SELECT count(*) AS missing_email
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">FROM accounts
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">WHERE email IS NULL;
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>大型表要分 batch backfill，避免 WAL、replica lag、autovacuum 與 lock 壓力。每個 batch 要記錄 row count、duration、error 與 lag。</p>
<h2 id="add-constraint-safely">Add Constraint Safely</h2>
<p>Add constraint safely 的核心責任是把資料驗證和 constraint 生效拆開。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">ALTER TABLE accounts
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">ADD CONSTRAINT accounts_email_present
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">CHECK (email IS NOT NULL) NOT VALID;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">ALTER TABLE accounts
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">VALIDATE CONSTRAINT accounts_email_present;
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p><code>NOT VALID</code> 讓 constraint 先約束新資料，再用 validation 掃既有資料。這是 PostgreSQL online migration 常用技巧。</p>
<h2 id="query-plan-evidence">Query Plan Evidence</h2>
<p>Query plan evidence 的核心責任是確認 migration 後 query 仍走正確路徑。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">EXPLAIN (ANALYZE, BUFFERS)
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT *
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">FROM accounts
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">WHERE email = &#39;ada@example.test&#39;;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>若 email 查詢成為正式 path，要新增 index，並用 <code>CREATE INDEX CONCURRENTLY</code> 評估 lock 與時間。</p>
<h2 id="contract-migration">Contract Migration</h2>
<p>Contract migration 的核心責任是在 application 都改用新欄位後，收斂舊欄位或舊 constraint。Contract migration 要比 expand 更謹慎，因為 rollback 空間更小。</p>
<p>Contract release gate：</p>
<ol>
<li>所有 app version 已停止讀舊欄位 / 舊行為。</li>
<li>Backfill validation 為零缺口。</li>
<li>Query plan 與 index evidence 已保存。</li>
<li>Rollback path 是 fail-forward 或 restore，兩者擇一寫清楚。</li>
<li>PITR / backup window 符合風險。</li>
</ol>
<h2 id="release-gate-note">Release Gate Note</h2>
<p>Release gate note 的核心責任是形成可交付 artifact。</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">Migration: add accounts.email
</span></span><span class="line"><span class="ln">2</span><span class="cl">Expand DDL duration:
</span></span><span class="line"><span class="ln">3</span><span class="cl">Backfill rows:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Validation query:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Lock evidence:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Query plan:
</span></span><span class="line"><span class="ln">7</span><span class="cl">Rollback / fail-forward:
</span></span><span class="line"><span class="ln">8</span><span class="cl">Owner:</span></span></code></pre></div><p>完成本篇後，複雜 migration 回到 <a href="../../online-schema-change/">Online Schema Change</a>；需要跨 DB 遷移則讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Security / RLS / Audit Logging</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/</guid><description>&lt;p>PostgreSQL security / RLS / audit logging 的核心責任是把資料庫安全拆成存取邊界、資料列可見性與操作證據。PostgreSQL role / grant 決定誰能連線與操作 schema；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/row-level-security/" data-link-title="Row-Level Security" data-link-desc="說明資料庫如何用 policy 限制同一張表中哪些 row 對某個角色可見或可寫">Row Level Security&lt;/a> 決定同一張表中哪些 row 對某個 role 可見；audit logging 則把敏感操作轉成可查詢、可保留、可告警的證據。&lt;/p>
&lt;p>本文的判讀錨點是：資料庫安全是 application auth 的下游防線。Application 仍要負責身份、session、租戶與 workflow；PostgreSQL security layer 負責在資料邊界補上 least privilege、tenant isolation 與 forensic evidence。&lt;/p>
&lt;h2 id="role-and-grant-baseline">Role and Grant Baseline&lt;/h2>
&lt;p>Role and grant baseline 的核心責任是把人、服務、migration 與分析查詢分開。Production database 至少要區分 application role、migration role、read-only role、admin role 與 replication / CDC role。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Role 類型&lt;/th>
 &lt;th>權限責任&lt;/th>
 &lt;th>常見風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>執行產品讀寫&lt;/td>
 &lt;td>權限過大、可 DDL、可讀所有 schema&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>變更 schema&lt;/td>
 &lt;td>和 app 共用 role，事故難以追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read-only&lt;/td>
 &lt;td>分析、debug、support&lt;/td>
 &lt;td>讀到 PII 或跨 tenant 資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication / CDC&lt;/td>
 &lt;td>logical replication、slot access&lt;/td>
 &lt;td>權限與 WAL retention 風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Admin&lt;/td>
 &lt;td>emergency operation&lt;/td>
 &lt;td>日常使用 admin role&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Grant review 要以 schema ownership 開始。Tables、sequences、functions、views、extensions 都有權限面；只管 table grant 會漏掉 sequence update、function execution 與 extension 使用。&lt;/p>
&lt;h2 id="row-level-security">Row Level Security&lt;/h2>
&lt;p>Row Level Security 的核心責任是在資料庫層 enforce row visibility。PostgreSQL 官方 RLS 文件描述 policy 可限制 normal query 返回、insert、update、delete 的 row；這讓 tenant boundary 可以在 database 層多一道 guard。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>RLS 使用情境&lt;/th>
 &lt;th>適合條件&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-tenant SaaS&lt;/td>
 &lt;td>tenant_id 明確且每個 query 都可帶入&lt;/td>
 &lt;td>policy 是否覆蓋 SELECT / INSERT / UPDATE&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Support access&lt;/td>
 &lt;td>support role 需受限查詢&lt;/td>
 &lt;td>break-glass 是否有 audit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Regional data&lt;/td>
 &lt;td>row 上有 region / residency&lt;/td>
 &lt;td>policy 是否和 GDPR / residency 對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sensitive subset&lt;/td>
 &lt;td>PII row 需特別隔離&lt;/td>
 &lt;td>masking / tokenization 是否仍需存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>RLS policy 要有 positive allow rule。每張啟用 RLS 的 table 都要有測試：同 tenant 可讀、跨 tenant 隔離、insert tenant mismatch 被擋、admin / support 例外被記錄。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL security / RLS / audit logging 的核心責任是把資料庫安全拆成存取邊界、資料列可見性與操作證據。PostgreSQL role / grant 決定誰能連線與操作 schema；<a href="/blog/backend/knowledge-cards/row-level-security/" data-link-title="Row-Level Security" data-link-desc="說明資料庫如何用 policy 限制同一張表中哪些 row 對某個角色可見或可寫">Row Level Security</a> 決定同一張表中哪些 row 對某個 role 可見；audit logging 則把敏感操作轉成可查詢、可保留、可告警的證據。</p>
<p>本文的判讀錨點是：資料庫安全是 application auth 的下游防線。Application 仍要負責身份、session、租戶與 workflow；PostgreSQL security layer 負責在資料邊界補上 least privilege、tenant isolation 與 forensic evidence。</p>
<h2 id="role-and-grant-baseline">Role and Grant Baseline</h2>
<p>Role and grant baseline 的核心責任是把人、服務、migration 與分析查詢分開。Production database 至少要區分 application role、migration role、read-only role、admin role 與 replication / CDC role。</p>
<table>
  <thead>
      <tr>
          <th>Role 類型</th>
          <th>權限責任</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application</td>
          <td>執行產品讀寫</td>
          <td>權限過大、可 DDL、可讀所有 schema</td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>變更 schema</td>
          <td>和 app 共用 role，事故難以追蹤</td>
      </tr>
      <tr>
          <td>Read-only</td>
          <td>分析、debug、support</td>
          <td>讀到 PII 或跨 tenant 資料</td>
      </tr>
      <tr>
          <td>Replication / CDC</td>
          <td>logical replication、slot access</td>
          <td>權限與 WAL retention 風險</td>
      </tr>
      <tr>
          <td>Admin</td>
          <td>emergency operation</td>
          <td>日常使用 admin role</td>
      </tr>
  </tbody>
</table>
<p>Grant review 要以 schema ownership 開始。Tables、sequences、functions、views、extensions 都有權限面；只管 table grant 會漏掉 sequence update、function execution 與 extension 使用。</p>
<h2 id="row-level-security">Row Level Security</h2>
<p>Row Level Security 的核心責任是在資料庫層 enforce row visibility。PostgreSQL 官方 RLS 文件描述 policy 可限制 normal query 返回、insert、update、delete 的 row；這讓 tenant boundary 可以在 database 層多一道 guard。</p>
<table>
  <thead>
      <tr>
          <th>RLS 使用情境</th>
          <th>適合條件</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-tenant SaaS</td>
          <td>tenant_id 明確且每個 query 都可帶入</td>
          <td>policy 是否覆蓋 SELECT / INSERT / UPDATE</td>
      </tr>
      <tr>
          <td>Support access</td>
          <td>support role 需受限查詢</td>
          <td>break-glass 是否有 audit</td>
      </tr>
      <tr>
          <td>Regional data</td>
          <td>row 上有 region / residency</td>
          <td>policy 是否和 GDPR / residency 對齊</td>
      </tr>
      <tr>
          <td>Sensitive subset</td>
          <td>PII row 需特別隔離</td>
          <td>masking / tokenization 是否仍需存在</td>
      </tr>
  </tbody>
</table>
<p>RLS policy 要有 positive allow rule。每張啟用 RLS 的 table 都要有測試：同 tenant 可讀、跨 tenant 隔離、insert tenant mismatch 被擋、admin / support 例外被記錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div><p>這段 policy 依賴 application 在 transaction 內設定 <code>app.tenant_id</code>。使用 connection pooler 時，設定必須跟 transaction boundary 對齊，避免 session state 漂移。</p>
<h2 id="audit-logging">Audit Logging</h2>
<p>Audit logging 的核心責任是把敏感資料操作轉成可查詢證據。PostgreSQL 原生日誌可以記錄連線、DDL、錯誤與慢查詢；pgAudit 這類 extension 則補強 session / object audit。</p>
<table>
  <thead>
      <tr>
          <th>Audit 類型</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DDL audit</td>
          <td>schema 變更追蹤</td>
          <td>migration id、role、statement、timestamp</td>
      </tr>
      <tr>
          <td>Sensitive read</td>
          <td>PII / payment / health data 查詢</td>
          <td>role、tenant、operation、reason</td>
      </tr>
      <tr>
          <td>Privilege change</td>
          <td>grant / revoke / role 變更</td>
          <td>actor、target role、approval</td>
      </tr>
      <tr>
          <td>Failed access</td>
          <td>權限錯誤與 RLS block</td>
          <td>error code、role、relation</td>
      </tr>
      <tr>
          <td>Break-glass</td>
          <td>emergency admin access</td>
          <td>ticket id、duration、review result</td>
      </tr>
  </tbody>
</table>
<p>Audit log 要能進入 SIEM 或集中 log。只留在 database host 上，事故後查詢成本高；正式 runbook 要定義 retention、masking、access control 與 alert。</p>
<h2 id="pii-and-data-protection-boundary">PII and Data Protection Boundary</h2>
<p>PII and data protection boundary 的核心責任是把 database 權限和資料保護策略接起來。RLS 可以限制 row visibility，但 PII 的保護還需要 masking、tokenization、encryption、retention 與 deletion evidence。</p>
<table>
  <thead>
      <tr>
          <th>資料類型</th>
          <th>Database control</th>
          <th>跨模組路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tenant data</td>
          <td>RLS、tenant-scoped role</td>
          <td>data access review</td>
      </tr>
      <tr>
          <td>PII</td>
          <td>column grant、masking view</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>append-only storage、retention</td>
          <td>SIEM / incident evidence</td>
      </tr>
      <tr>
          <td>Deletion request</td>
          <td>tombstone、cascade review</td>
          <td>retention policy、legal hold</td>
      </tr>
  </tbody>
</table>
<p>Column-level grant 和 masking view 適合 read-only analyst。Application role 通常需要明文處理 workflow；analyst / support role 則應走 restricted view。</p>
<h2 id="operational-evidence">Operational Evidence</h2>
<p>Operational evidence 的核心責任是讓安全設定可驗證。每次 release 或權限變更後，要跑固定檢查。</p>
<ol>
<li>Role matrix：每個 role 的 schema / table / sequence / function grant。</li>
<li>RLS test：tenant A / tenant B / support / admin 的可見性測試。</li>
<li>Audit sample：DDL、sensitive read、failed access 是否進 log。</li>
<li>Pooler compatibility：<code>SET LOCAL app.tenant_id</code> 是否跟 transaction 對齊。</li>
<li><a href="/blog/backend/knowledge-cards/break-glass-access/" data-link-title="Break-Glass Access" data-link-desc="說明緊急情況下臨時授予的高權限存取，如何用工單、時限與事後審查治理">Break-glass</a> drill：emergency access 是否可申請、可回收、可審查。</li>
</ol>
<p>Evidence 要保存在 release artifact。Security 設定只有文件描述時，incident 後難以證明它真的生效。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 database security 常見事故提前列出。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>App role 權限過大</td>
          <td>app 可 DDL / drop / grant</td>
          <td>role split + least privilege</td>
      </tr>
      <tr>
          <td>RLS bypass</td>
          <td>owner / superuser / policy 漏洞</td>
          <td>dedicated app role + RLS test</td>
      </tr>
      <tr>
          <td>Pooler state drift</td>
          <td>tenant setting 漂到下個 request</td>
          <td><code>SET LOCAL</code> + transaction pooling review</td>
      </tr>
      <tr>
          <td>Audit gap</td>
          <td>敏感操作查不到 actor</td>
          <td>pgAudit / log schema / SIEM route</td>
      </tr>
      <tr>
          <td>Support overread</td>
          <td>support role 可讀全 tenant</td>
          <td>masking view + ticket-scoped access</td>
      </tr>
  </tbody>
</table>
<p>RLS bypass 要特別審查 table owner 與 superuser path。正式 application 連線應使用 dedicated role，並避免使用 table owner role 執行一般 request。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Security / RLS / audit logging 完成後，權限與 PII 治理讀 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>；connection state 風險讀 <a href="../connection-pooler-comparison/">Connection Pooler Comparison</a>；實作演練可放進 <a href="../hands-on/schema-migration-evidence-lab/">Schema Migration Evidence Lab</a> 的 release gate。</p>
]]></content:encoded></item><item><title>PostgreSQL to YugabyteDB / TiDB Migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-yugabytedb-tidb/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-yugabytedb-tidb/</guid><description>&lt;p>PostgreSQL to YugabyteDB / TiDB migration 的核心責任是處理從 single-primary PostgreSQL 走向 distributed SQL 的資料拓撲變更。這條路線通常由 multi-region write、horizontal scale、tenant sharding、availability 或 single-node capacity ceiling 觸發；其中 YugabyteDB 走 PostgreSQL-compatible YSQL 路線，TiDB 走 MySQL-compatible distributed SQL 路線，兩者的 application diff audit 不同。&lt;/p>
&lt;p>本文的判讀錨點是：API compatibility 只解決入口語法的一部分。YugabyteDB 要審查 PostgreSQL 相容與 distributed operation 差異；TiDB 要額外處理 PostgreSQL → MySQL dialect / driver / tooling 轉換。Distributed SQL 會改變 transaction latency、placement、index cost、DDL、sequence、lock、backup、observability 與 incident route。&lt;/p>
&lt;h2 id="official-documentation-route">Official Documentation Route&lt;/h2>
&lt;p>Official documentation route 的核心責任是把 compatibility claim 固定到可回查來源。YugabyteDB compatibility 先查 &lt;a href="https://docs.yugabyte.com/stable/reference/configuration/postgresql-compatibility/">YugabyteDB PostgreSQL compatibility&lt;/a>；TiDB compatibility 先查 &lt;a href="https://docs.pingcap.com/tidb/stable/mysql-compatibility/">TiDB MySQL compatibility&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="driver-check">Driver Check&lt;/h2>
&lt;p>Driver check 的核心責任是確認 distributed SQL 解決的是核心問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>代表需求&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-region write&lt;/td>
 &lt;td>多地使用者都要低延遲寫入&lt;/td>
 &lt;td>consistency level、latency budget&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Horizontal write scaling&lt;/td>
 &lt;td>單 primary CPU / I/O 到頂&lt;/td>
 &lt;td>shard key、hot key、cross-shard txn&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tenant distribution&lt;/td>
 &lt;td>tenant 可依 region / size 分布&lt;/td>
 &lt;td>tenant placement、rebalance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Availability&lt;/td>
 &lt;td>節點 / zone failure 容忍&lt;/td>
 &lt;td>quorum、failover、RPO / RTO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational consolidation&lt;/td>
 &lt;td>多 PG shard 想收斂&lt;/td>
 &lt;td>migration complexity、cost&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>若主要問題是 read scaling、connection 數或 query index，先評估 read replica、pooler、partition、Citus 或 Aurora；distributed SQL 適合資料拓撲問題。&lt;/p>
&lt;h2 id="compatibility-audit">Compatibility Audit&lt;/h2>
&lt;p>Compatibility audit 的核心責任是把 PostgreSQL behavior 逐項對照 target。&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>Protocol / API&lt;/td>
 &lt;td>YugabyteDB YSQL vs TiDB MySQL protocol&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SQL dialect&lt;/td>
 &lt;td>function、extension、type、DDL support&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>isolation、lock、deadlock、retry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sequence / ID&lt;/td>
 &lt;td>global sequence latency、UUID policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index&lt;/td>
 &lt;td>secondary index placement、write cost&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Foreign key&lt;/td>
 &lt;td>distributed FK cost / support&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Extension&lt;/td>
 &lt;td>PostGIS、pgvector、custom extension；TiDB 路線需改寫或拆出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tooling&lt;/td>
 &lt;td>migration tool、CDC、backup、monitoring&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Compatibility audit 要用 application query suite。只看 schema import 會漏掉 transaction retry、query planner、distributed index、dialect rewrite 與 latency。TiDB 路線還要加 PostgreSQL driver / SQL / type / migration tool 轉 MySQL ecosystem 的審查。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL to YugabyteDB / TiDB migration 的核心責任是處理從 single-primary PostgreSQL 走向 distributed SQL 的資料拓撲變更。這條路線通常由 multi-region write、horizontal scale、tenant sharding、availability 或 single-node capacity ceiling 觸發；其中 YugabyteDB 走 PostgreSQL-compatible YSQL 路線，TiDB 走 MySQL-compatible distributed SQL 路線，兩者的 application diff audit 不同。</p>
<p>本文的判讀錨點是：API compatibility 只解決入口語法的一部分。YugabyteDB 要審查 PostgreSQL 相容與 distributed operation 差異；TiDB 要額外處理 PostgreSQL → MySQL dialect / driver / tooling 轉換。Distributed SQL 會改變 transaction latency、placement、index cost、DDL、sequence、lock、backup、observability 與 incident route。</p>
<h2 id="official-documentation-route">Official Documentation Route</h2>
<p>Official documentation route 的核心責任是把 compatibility claim 固定到可回查來源。YugabyteDB compatibility 先查 <a href="https://docs.yugabyte.com/stable/reference/configuration/postgresql-compatibility/">YugabyteDB PostgreSQL compatibility</a>；TiDB compatibility 先查 <a href="https://docs.pingcap.com/tidb/stable/mysql-compatibility/">TiDB MySQL compatibility</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="driver-check">Driver Check</h2>
<p>Driver check 的核心責任是確認 distributed SQL 解決的是核心問題。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>代表需求</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-region write</td>
          <td>多地使用者都要低延遲寫入</td>
          <td>consistency level、latency budget</td>
      </tr>
      <tr>
          <td>Horizontal write scaling</td>
          <td>單 primary CPU / I/O 到頂</td>
          <td>shard key、hot key、cross-shard txn</td>
      </tr>
      <tr>
          <td>Tenant distribution</td>
          <td>tenant 可依 region / size 分布</td>
          <td>tenant placement、rebalance</td>
      </tr>
      <tr>
          <td>Availability</td>
          <td>節點 / zone failure 容忍</td>
          <td>quorum、failover、RPO / RTO</td>
      </tr>
      <tr>
          <td>Operational consolidation</td>
          <td>多 PG shard 想收斂</td>
          <td>migration complexity、cost</td>
      </tr>
  </tbody>
</table>
<p>若主要問題是 read scaling、connection 數或 query index，先評估 read replica、pooler、partition、Citus 或 Aurora；distributed SQL 適合資料拓撲問題。</p>
<h2 id="compatibility-audit">Compatibility Audit</h2>
<p>Compatibility audit 的核心責任是把 PostgreSQL behavior 逐項對照 target。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Protocol / API</td>
          <td>YugabyteDB YSQL vs TiDB MySQL protocol</td>
      </tr>
      <tr>
          <td>SQL dialect</td>
          <td>function、extension、type、DDL support</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>isolation、lock、deadlock、retry</td>
      </tr>
      <tr>
          <td>Sequence / ID</td>
          <td>global sequence latency、UUID policy</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>secondary index placement、write cost</td>
      </tr>
      <tr>
          <td>Foreign key</td>
          <td>distributed FK cost / support</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>PostGIS、pgvector、custom extension；TiDB 路線需改寫或拆出</td>
      </tr>
      <tr>
          <td>Tooling</td>
          <td>migration tool、CDC、backup、monitoring</td>
      </tr>
  </tbody>
</table>
<p>Compatibility audit 要用 application query suite。只看 schema import 會漏掉 transaction retry、query planner、distributed index、dialect rewrite 與 latency。TiDB 路線還要加 PostgreSQL driver / SQL / type / migration tool 轉 MySQL ecosystem 的審查。</p>
<h2 id="data-topology">Data Topology</h2>
<p>Data topology 的核心責任是決定資料如何分布。Distributed SQL 的成敗常取決於 primary key、tenant key、region placement 與 hot key 控制。</p>
<table>
  <thead>
      <tr>
          <th>拓撲決策</th>
          <th>判讀問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Distribution key</td>
          <td>query 是否能 co-locate data</td>
      </tr>
      <tr>
          <td>Region placement</td>
          <td>資料是否需要 residency / low latency</td>
      </tr>
      <tr>
          <td>Hot key</td>
          <td>high-write tenant / account 是否集中</td>
      </tr>
      <tr>
          <td>Secondary index</td>
          <td>index write 是否跨 shard / region</td>
      </tr>
      <tr>
          <td>Transaction span</td>
          <td>交易是否常跨 tenant / region</td>
      </tr>
  </tbody>
</table>
<p>Topology 設計要從最高頻 workflow 開始。若核心交易每次都跨 shard，distributed SQL 的 latency 與 conflict cost 會很高。</p>
<h2 id="migration-phases">Migration Phases</h2>
<p>Migration phases 的核心責任是降低跨拓撲遷移風險。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lab import</td>
          <td>schema import、query suite、driver test</td>
      </tr>
      <tr>
          <td>Topology design</td>
          <td>key、placement、region、index review</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>snapshot、batch、checksum</td>
      </tr>
      <tr>
          <td>CDC catch-up</td>
          <td>LSN / change stream、lag、idempotency</td>
      </tr>
      <tr>
          <td>Shadow read</td>
          <td>result diff、latency profile</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>freeze、final sync、traffic switch</td>
      </tr>
      <tr>
          <td>Rollback</td>
          <td>source PG snapshot、write replay plan</td>
      </tr>
  </tbody>
</table>
<p>CDC catch-up 要有 clear cutover LSN。Distributed SQL migration 最怕 source / target 同時有寫入後，缺少 reconciliation plan。</p>
<h2 id="application-changes">Application Changes</h2>
<p>Application changes 的核心責任是讓程式接受 distributed system 的錯誤模式。</p>
<ol>
<li>Transaction retry：serialization / conflict error 要可重試。</li>
<li>Idempotency：critical write 要有 natural key 或 idempotency key。</li>
<li>Latency budget：跨 region transaction 要進 SLO。</li>
<li>Pagination / ordering：distributed query 的排序成本要審查。</li>
<li>Connection / driver：target driver、TLS、pooling、load balancing 要測。</li>
</ol>
<p>Application 若假設 single-node low-latency transaction，遷移後會在 tail latency 與 retry 行為上出現落差。TiDB 路線還會出現 driver、placeholder、SQL function、type mapping 與 error code 的轉換成本；這些要在 staging failure injection 先看到。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go conditions 的核心責任是阻止把 distributed SQL 當成萬用擴容。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>替代路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要瓶頸是少數 slow query</td>
          <td>query optimization / index</td>
      </tr>
      <tr>
          <td>多數交易跨全局資料</td>
          <td>重設 bounded context 或保持 single primary</td>
      </tr>
      <tr>
          <td>Team 缺少 distributed operation 能力</td>
          <td>managed provider / simpler topology</td>
      </tr>
      <tr>
          <td>PostgreSQL extension 依賴重</td>
          <td>保留 PG 或拆出 specialized service</td>
      </tr>
      <tr>
          <td>RPO / rollback 沒有演練</td>
          <td>先完成 migration playbook</td>
      </tr>
      <tr>
          <td>想保留 PostgreSQL driver / SQL surface</td>
          <td>優先評估 YugabyteDB / CockroachDB / Citus</td>
      </tr>
  </tbody>
</table>
<p>Distributed SQL 的價值來自拓撲匹配。若 workload 缺少自然分布邊界，導入後只是把單點瓶頸換成分散式複雜度。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>PostgreSQL to YugabyteDB / TiDB migration 完成後，先讀 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a>；若需求是 PostgreSQL 內分散式 table，讀 <a href="../citus-distributed/">Citus Distributed</a>；跨 vendor 流程讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>。</p>
]]></content:encoded></item><item><title>Specialized PostgreSQL Variants</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/</guid><description>&lt;p>Specialized PostgreSQL variants 的核心責任是把 PostgreSQL ecosystem 裡的 specialized engines、extensions 與 managed variants 放到正確服務位置。PostgreSQL 的擴充性讓它能支援 geospatial、time-series、vector search、distributed table、serverless branch 與 managed acceleration；但每個變體都改變 operation、migration、cost 與 lock-in。&lt;/p>
&lt;p>本文的判讀錨點是：PostgreSQL compatibility 是入口，不等於相同責任。選 variant 前，要先說清楚新增能力解決哪個 workload，並確認 exit route。&lt;/p>
&lt;h2 id="variant-taxonomy">Variant Taxonomy&lt;/h2>
&lt;p>Variant taxonomy 的核心責任是把變體按資料模型與操作責任分類。&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>Extension domain&lt;/td>
 &lt;td>PostGIS、pgvector、TimescaleDB&lt;/td>
 &lt;td>geospatial、vector、time-series&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distributed PG&lt;/td>
 &lt;td>Citus、Cosmos DB for PostgreSQL&lt;/td>
 &lt;td>sharding、distributed query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Managed accelerated PG&lt;/td>
 &lt;td>AlloyDB、Aurora PG&lt;/td>
 &lt;td>managed performance / HA / platform&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serverless / branching&lt;/td>
 &lt;td>Neon、Supabase workflow&lt;/td>
 &lt;td>preview、branch、稀疏 workload&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compatibility layer&lt;/td>
 &lt;td>YugabyteDB、部分 distributed SQL&lt;/td>
 &lt;td>PostgreSQL-like API + distributed storage&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分類的重點是避免把不同變體視為同一種升級。Extension domain 強化單一資料模型；distributed PG 改變資料拓撲；managed accelerated PG 改變操作邊界；serverless PG 改變 lifecycle。&lt;/p>
&lt;h2 id="workload-fit">Workload Fit&lt;/h2>
&lt;p>Workload fit 的核心責任是判斷 variant 是否匹配資料形狀。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Workload&lt;/th>
 &lt;th>合適路線&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Geospatial query&lt;/td>
 &lt;td>PostGIS&lt;/td>
 &lt;td>index、SRID、資料量、query latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Time-series retention&lt;/td>
 &lt;td>TimescaleDB / partition strategy&lt;/td>
 &lt;td>compression、chunk、retention&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vector search&lt;/td>
 &lt;td>pgvector / pgvectorscale&lt;/td>
 &lt;td>recall、latency、index build、hybrid search&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tenant sharding&lt;/td>
 &lt;td>Citus / distributed PG&lt;/td>
 &lt;td>distribution key、co-location、rebalance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Preview environment&lt;/td>
 &lt;td>serverless / branching PG&lt;/td>
 &lt;td>data privacy、branch lifecycle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloud-managed acceleration&lt;/td>
 &lt;td>AlloyDB / Aurora&lt;/td>
 &lt;td>compatibility、cost、exit&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Variant 要先證明普通 PostgreSQL 加 index / partition / read replica 已到邊界。若基礎 query design 還沒成熟，導入 variant 會把複雜度提前。&lt;/p>
&lt;h2 id="migration-gap">Migration Gap&lt;/h2>
&lt;p>Migration gap 的核心責任是列出從 vanilla PostgreSQL 進入 variant 的差異。&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>DDL&lt;/td>
 &lt;td>extension object、distributed table、chunk&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>planner、function、operator、pushdown&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data movement&lt;/td>
 &lt;td>backfill、reshard、index build&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operation&lt;/td>
 &lt;td>backup、restore、upgrade、failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tooling&lt;/td>
 &lt;td>ORM、migration tool、CDC、monitoring&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Exit&lt;/td>
 &lt;td>dump / restore 是否回到 vanilla PG&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Migration 要有 compatibility test。每個核心 query 在 variant 上跑 explain、latency、result correctness；每個 migration step 都要有 rollback 或 rebuild path。&lt;/p></description><content:encoded><![CDATA[<p>Specialized PostgreSQL variants 的核心責任是把 PostgreSQL ecosystem 裡的 specialized engines、extensions 與 managed variants 放到正確服務位置。PostgreSQL 的擴充性讓它能支援 geospatial、time-series、vector search、distributed table、serverless branch 與 managed acceleration；但每個變體都改變 operation、migration、cost 與 lock-in。</p>
<p>本文的判讀錨點是：PostgreSQL compatibility 是入口，不等於相同責任。選 variant 前，要先說清楚新增能力解決哪個 workload，並確認 exit route。</p>
<h2 id="variant-taxonomy">Variant Taxonomy</h2>
<p>Variant taxonomy 的核心責任是把變體按資料模型與操作責任分類。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>代表</th>
          <th>主要解決問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Extension domain</td>
          <td>PostGIS、pgvector、TimescaleDB</td>
          <td>geospatial、vector、time-series</td>
      </tr>
      <tr>
          <td>Distributed PG</td>
          <td>Citus、Cosmos DB for PostgreSQL</td>
          <td>sharding、distributed query</td>
      </tr>
      <tr>
          <td>Managed accelerated PG</td>
          <td>AlloyDB、Aurora PG</td>
          <td>managed performance / HA / platform</td>
      </tr>
      <tr>
          <td>Serverless / branching</td>
          <td>Neon、Supabase workflow</td>
          <td>preview、branch、稀疏 workload</td>
      </tr>
      <tr>
          <td>Compatibility layer</td>
          <td>YugabyteDB、部分 distributed SQL</td>
          <td>PostgreSQL-like API + distributed storage</td>
      </tr>
  </tbody>
</table>
<p>分類的重點是避免把不同變體視為同一種升級。Extension domain 強化單一資料模型；distributed PG 改變資料拓撲；managed accelerated PG 改變操作邊界；serverless PG 改變 lifecycle。</p>
<h2 id="workload-fit">Workload Fit</h2>
<p>Workload fit 的核心責任是判斷 variant 是否匹配資料形狀。</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>合適路線</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Geospatial query</td>
          <td>PostGIS</td>
          <td>index、SRID、資料量、query latency</td>
      </tr>
      <tr>
          <td>Time-series retention</td>
          <td>TimescaleDB / partition strategy</td>
          <td>compression、chunk、retention</td>
      </tr>
      <tr>
          <td>Vector search</td>
          <td>pgvector / pgvectorscale</td>
          <td>recall、latency、index build、hybrid search</td>
      </tr>
      <tr>
          <td>Tenant sharding</td>
          <td>Citus / distributed PG</td>
          <td>distribution key、co-location、rebalance</td>
      </tr>
      <tr>
          <td>Preview environment</td>
          <td>serverless / branching PG</td>
          <td>data privacy、branch lifecycle</td>
      </tr>
      <tr>
          <td>Cloud-managed acceleration</td>
          <td>AlloyDB / Aurora</td>
          <td>compatibility、cost、exit</td>
      </tr>
  </tbody>
</table>
<p>Variant 要先證明普通 PostgreSQL 加 index / partition / read replica 已到邊界。若基礎 query design 還沒成熟，導入 variant 會把複雜度提前。</p>
<h2 id="migration-gap">Migration Gap</h2>
<p>Migration gap 的核心責任是列出從 vanilla PostgreSQL 進入 variant 的差異。</p>
<table>
  <thead>
      <tr>
          <th>差異面</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DDL</td>
          <td>extension object、distributed table、chunk</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>planner、function、operator、pushdown</td>
      </tr>
      <tr>
          <td>Data movement</td>
          <td>backfill、reshard、index build</td>
      </tr>
      <tr>
          <td>Operation</td>
          <td>backup、restore、upgrade、failover</td>
      </tr>
      <tr>
          <td>Tooling</td>
          <td>ORM、migration tool、CDC、monitoring</td>
      </tr>
      <tr>
          <td>Exit</td>
          <td>dump / restore 是否回到 vanilla PG</td>
      </tr>
  </tbody>
</table>
<p>Migration 要有 compatibility test。每個核心 query 在 variant 上跑 explain、latency、result correctness；每個 migration step 都要有 rollback 或 rebuild path。</p>
<h2 id="lock-in-and-exit">Lock-In and Exit</h2>
<p>Lock-in and exit 的核心責任是把 variant-specific 能力和可攜性分開。</p>
<table>
  <thead>
      <tr>
          <th>Lock-in 來源</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Extension-specific type</td>
          <td>adapter layer、domain boundary</td>
      </tr>
      <tr>
          <td>Managed-only feature</td>
          <td>decision record、exit test</td>
      </tr>
      <tr>
          <td>Distributed table DDL</td>
          <td>topology doc、reshard runbook</td>
      </tr>
      <tr>
          <td>Serverless branch API</td>
          <td>dev workflow boundary</td>
      </tr>
      <tr>
          <td>Proprietary index / function</td>
          <td>fallback query / export strategy</td>
      </tr>
  </tbody>
</table>
<p>Lock-in 可以接受，但要被命名。若 variant 能顯著降低成本或提高能力，採用是合理決策；工程責任是保留 exit evidence 與 migration plan。</p>
<h2 id="decision-matrix">Decision Matrix</h2>
<p>Decision matrix 的核心責任是把 variant 路由接到 PostgreSQL 主章。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>地理查詢是核心產品能力</td>
          <td><a href="../postgis-deep-dive/">PostGIS Deep Dive</a></td>
      </tr>
      <tr>
          <td>時序資料與 retention 是主壓力</td>
          <td><a href="../timescaledb-deep-dive/">TimescaleDB Deep Dive</a></td>
      </tr>
      <tr>
          <td>向量搜尋在 PG 內整合</td>
          <td><a href="../pgvector-deep-dive/">pgvector Deep Dive</a></td>
      </tr>
      <tr>
          <td>tenant sharding / distributed query</td>
          <td><a href="../citus-distributed/">Citus Distributed</a></td>
      </tr>
      <tr>
          <td>managed provider 選型</td>
          <td><a href="../managed-pg-comparison/">Managed PostgreSQL Comparison</a></td>
      </tr>
      <tr>
          <td>分散式 SQL API 相容評估</td>
          <td><a href="../migrate-to-yugabytedb-tidb/">PostgreSQL to YugabyteDB / TiDB</a></td>
      </tr>
  </tbody>
</table>
<p>Decision matrix 要隨案例更新。Variant 選型最需要實際 workload：資料量、query pattern、SLO、team skill、合規與 exit 成本。</p>
<h2 id="review-checklist">Review Checklist</h2>
<p>Review checklist 的核心責任是避免 specialized variant 只被功能吸引。</p>
<ol>
<li>Workload 是否真的需要 specialized capability。</li>
<li>Vanilla PostgreSQL 的 index / partition / replica 是否已評估。</li>
<li>Extension / managed feature 的版本與支援政策。</li>
<li>Backup / restore / upgrade runbook。</li>
<li>Migration tool、CDC、observability 是否支援。</li>
<li>Exit route 是否至少在 staging 演練。</li>
<li>成本模型是否包含 storage、compute、I/O、support、operation。</li>
</ol>
<p>完成 checklist 後，variant 才能進入正式 proposal。這樣可以保留 PostgreSQL ecosystem 的彈性，也避免變體變成隱形平台遷移。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Specialized variants 完成後，回到 <a href="../">PostgreSQL overview</a> 做服務定位；需要 managed provider 比較讀 <a href="../managed-pg-comparison/">Managed PostgreSQL Comparison</a>；需要跨 vendor migration 讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL to SQLite Simplification</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/</guid><description>&lt;p>PostgreSQL to SQLite simplification 的核心責任是處理反向路線：服務責任縮小後，評估 SQLite 是否能降低操作成本。這條路線適合 single-user app、CLI、desktop app、內部工具、read-mostly artifact store、demo environment、local-first prototype 或 edge-local utility。&lt;/p>
&lt;p>本文的判讀錨點是：降級到 SQLite 是責任縮小，也是讓資料模型回到 single-process / file-owned / local-state 的工程選擇。只要正式需求從 multi-user server DB 回到這個範圍，SQLite 可以提供更低元件數、更容易搬移與更低維護成本。&lt;/p>
&lt;h2 id="simplification-drivers">Simplification Drivers&lt;/h2>
&lt;p>Simplification drivers 的核心責任是確認 PostgreSQL 的能力已超過服務需求。若 server DB 的 HA、role、replica、pool、vacuum、PITR、schema governance 都變成維運負擔，而產品只需要單一 process 持有資料，就可以評估 SQLite。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>代表情境&lt;/th>
 &lt;th>SQLite 帶來的收益&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Single-user app&lt;/td>
 &lt;td>desktop、CLI、local admin tool&lt;/td>
 &lt;td>file portability、offline use&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read-mostly artifact&lt;/td>
 &lt;td>build metadata、catalog snapshot&lt;/td>
 &lt;td>deployment simple、低 runtime dependency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Internal tool&lt;/td>
 &lt;td>小團隊使用、資料量小、低寫入&lt;/td>
 &lt;td>降低 DB server operation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Demo / fixture&lt;/td>
 &lt;td>每個 environment 一份可重建資料&lt;/td>
 &lt;td>quick reset、deterministic seed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge-local utility&lt;/td>
 &lt;td>request-local / device-local state&lt;/td>
 &lt;td>low latency、local ownership&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Driver 要連到 ownership。SQLite 適合「這份資料由某個 process / device / artifact 明確持有」；若資料仍屬於多服務共同真相，保留 PostgreSQL 或改成 managed SQL 會更穩定。&lt;/p>
&lt;h2 id="no-go-conditions">No-Go Conditions&lt;/h2>
&lt;p>No-go condition 的核心責任是保護仍需要 server DB 的服務。若 PostgreSQL 的核心能力仍被業務依賴，遷到 SQLite 會把風險轉移到 application code、file backup 與人工流程。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>No-go 訊號&lt;/th>
 &lt;th>代表責任&lt;/th>
 &lt;th>保留路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多 tenant 與 centralized permission&lt;/td>
 &lt;td>DB role、grant、audit 仍有價值&lt;/td>
 &lt;td>PostgreSQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多 instance concurrent writer&lt;/td>
 &lt;td>SQLite writer boundary 壓力過高&lt;/td>
 &lt;td>PostgreSQL / MySQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PITR / HA 是合約要求&lt;/td>
 &lt;td>server DB operation 是正式責任&lt;/td>
 &lt;td>Managed PostgreSQL / Aurora&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Analyst / job 直接查 DB&lt;/td>
 &lt;td>access control 與 query isolation&lt;/td>
 &lt;td>PostgreSQL read replica / warehouse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-service source of truth&lt;/td>
 &lt;td>單檔 ownership 與服務邊界衝突&lt;/td>
 &lt;td>保留 server DB 或拆 bounded context&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>No-go 條件要寫進 migration proposal。Simplification 的目標是降低操作成本；若降級後要用大量自製機制補回 role、audit、HA 與 concurrent write，成本會回到系統裡。&lt;/p>
&lt;h2 id="diff-audit">Diff Audit&lt;/h2>
&lt;p>Diff audit 的核心責任是把 PostgreSQL 語意縮到 SQLite 可以清楚承擔的範圍。PostgreSQL extension、function、type、index、constraint、sequence、view、trigger、role 與 transaction behavior 都要盤點。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL to SQLite simplification 的核心責任是處理反向路線：服務責任縮小後，評估 SQLite 是否能降低操作成本。這條路線適合 single-user app、CLI、desktop app、內部工具、read-mostly artifact store、demo environment、local-first prototype 或 edge-local utility。</p>
<p>本文的判讀錨點是：降級到 SQLite 是責任縮小，也是讓資料模型回到 single-process / file-owned / local-state 的工程選擇。只要正式需求從 multi-user server DB 回到這個範圍，SQLite 可以提供更低元件數、更容易搬移與更低維護成本。</p>
<h2 id="simplification-drivers">Simplification Drivers</h2>
<p>Simplification drivers 的核心責任是確認 PostgreSQL 的能力已超過服務需求。若 server DB 的 HA、role、replica、pool、vacuum、PITR、schema governance 都變成維運負擔，而產品只需要單一 process 持有資料，就可以評估 SQLite。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>代表情境</th>
          <th>SQLite 帶來的收益</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user app</td>
          <td>desktop、CLI、local admin tool</td>
          <td>file portability、offline use</td>
      </tr>
      <tr>
          <td>Read-mostly artifact</td>
          <td>build metadata、catalog snapshot</td>
          <td>deployment simple、低 runtime dependency</td>
      </tr>
      <tr>
          <td>Internal tool</td>
          <td>小團隊使用、資料量小、低寫入</td>
          <td>降低 DB server operation</td>
      </tr>
      <tr>
          <td>Demo / fixture</td>
          <td>每個 environment 一份可重建資料</td>
          <td>quick reset、deterministic seed</td>
      </tr>
      <tr>
          <td>Edge-local utility</td>
          <td>request-local / device-local state</td>
          <td>low latency、local ownership</td>
      </tr>
  </tbody>
</table>
<p>Driver 要連到 ownership。SQLite 適合「這份資料由某個 process / device / artifact 明確持有」；若資料仍屬於多服務共同真相，保留 PostgreSQL 或改成 managed SQL 會更穩定。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是保護仍需要 server DB 的服務。若 PostgreSQL 的核心能力仍被業務依賴，遷到 SQLite 會把風險轉移到 application code、file backup 與人工流程。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>代表責任</th>
          <th>保留路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 tenant 與 centralized permission</td>
          <td>DB role、grant、audit 仍有價值</td>
          <td>PostgreSQL</td>
      </tr>
      <tr>
          <td>多 instance concurrent writer</td>
          <td>SQLite writer boundary 壓力過高</td>
          <td>PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>PITR / HA 是合約要求</td>
          <td>server DB operation 是正式責任</td>
          <td>Managed PostgreSQL / Aurora</td>
      </tr>
      <tr>
          <td>Analyst / job 直接查 DB</td>
          <td>access control 與 query isolation</td>
          <td>PostgreSQL read replica / warehouse</td>
      </tr>
      <tr>
          <td>Cross-service source of truth</td>
          <td>單檔 ownership 與服務邊界衝突</td>
          <td>保留 server DB 或拆 bounded context</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要寫進 migration proposal。Simplification 的目標是降低操作成本；若降級後要用大量自製機制補回 role、audit、HA 與 concurrent write，成本會回到系統裡。</p>
<h2 id="diff-audit">Diff Audit</h2>
<p>Diff audit 的核心責任是把 PostgreSQL 語意縮到 SQLite 可以清楚承擔的範圍。PostgreSQL extension、function、type、index、constraint、sequence、view、trigger、role 與 transaction behavior 都要盤點。</p>
<table>
  <thead>
      <tr>
          <th>PostgreSQL feature</th>
          <th>SQLite 轉換策略</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>timestamptz</code></td>
          <td>UTC ISO text 或 integer epoch</td>
          <td>timezone policy 是否固定</td>
      </tr>
      <tr>
          <td><code>jsonb</code> + GIN</td>
          <td>JSON text + limited query / app filter</td>
          <td>query 是否仍需 index</td>
      </tr>
      <tr>
          <td>Sequence / identity</td>
          <td>INTEGER PRIMARY KEY 或 app ID</td>
          <td>id stability 與 import collision</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>SQLite partial index</td>
          <td>predicate 與 query planner 是否對齊</td>
      </tr>
      <tr>
          <td>Role / grant</td>
          <td>filesystem permission + app auth</td>
          <td>權限是否可移到 application boundary</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>application logic 或放棄 feature</td>
          <td>feature 是否仍是正式需求</td>
      </tr>
  </tbody>
</table>
<p>Diff audit 的輸出是一份保留 / 移除 / 改寫清單。每個 PostgreSQL feature 都要回答：這是正式需求、歷史殘留，還是可以移到 application layer 的便利功能。</p>
<h2 id="phase-plan">Phase Plan</h2>
<p>Phase plan 的核心責任是把 server DB 退場變成可回復流程。反向 migration 要超過一次性 dump：先收斂寫入、建立 SQLite schema、匯入資料、跑 adapter test、演練 backup，再退役 PostgreSQL。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope reduction</td>
          <td>確認資料責任已縮小</td>
          <td>ownership doc、no-go review</td>
      </tr>
      <tr>
          <td>Schema rewrite</td>
          <td>建立 SQLite schema</td>
          <td>migration dry run、STRICT / constraint</td>
      </tr>
      <tr>
          <td>Data export</td>
          <td>從 PostgreSQL 匯出 snapshot</td>
          <td>row count、checksum、dump metadata</td>
      </tr>
      <tr>
          <td>Data import</td>
          <td>寫入 SQLite file</td>
          <td>integrity check、foreign key check</td>
      </tr>
      <tr>
          <td>Adapter switch</td>
          <td>app 改用 SQLite repository</td>
          <td>contract test、error mapping</td>
      </tr>
      <tr>
          <td>Backup runbook</td>
          <td>建立 file lifecycle evidence</td>
          <td>backup restore drill</td>
      </tr>
      <tr>
          <td>Server retirement</td>
          <td>關閉 PostgreSQL 寫入與 credential</td>
          <td>retention、credential removal、incident route</td>
      </tr>
  </tbody>
</table>
<p>Scope reduction 是第一關。若資料仍被多個服務寫入，應先拆出 bounded context 或建立 event / export boundary；SQLite file 才能成為明確 owned artifact。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是把 PostgreSQL snapshot 轉成 SQLite file 並保留驗證。可用 <code>COPY</code> / CSV、application ETL 或 dedicated migration tool；選擇取決於 type conversion 與資料量。</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">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;\\copy orders TO &#39;orders.csv&#39; CSV HEADER&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.mode csv&#34;</span> <span class="s2">&#34;.import --skip 1 orders.csv orders&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令是教學骨架。正式流程要處理 NULL、delimiter、timezone、numeric precision、FK order、transaction、temporary disk、sensitive data 與 import log。</p>
<p>Import 後要跑三種 evidence：database integrity、row count / checksum、business invariant。Business invariant 例如 active user count、total balance、latest event id、pending job count；這些比單純 row count 更能抓到語意錯誤。</p>
<h2 id="runbook-shift">Runbook Shift</h2>
<p>Runbook shift 的核心責任是把 PostgreSQL operation 移轉成 SQLite file operation。Server DB 的 backup / role / monitoring 退場後，要補上 SQLite 的 backup、restore、file permission、WAL、migration 與 disk 觀測。</p>
<p>最小 SQLite runbook 包含：</p>
<ol>
<li>Database file path、owner process、filesystem permission。</li>
<li>Journal mode、busy timeout、foreign key、schema version。</li>
<li>Backup command、restore drill、retention、checksum。</li>
<li>Migration command、pre-migration snapshot、rollback path。</li>
<li>Observability：busy、WAL size、disk free、backup age。</li>
<li>Incident route：disk full、bad migration、corruption signal。</li>
</ol>
<p>Runbook shift 要同步移除 PostgreSQL credential。Server database 退役時，保留 read-only archive、刪除 application secret、關閉 scheduled job、更新 dashboard 與 incident routing。</p>
<h2 id="cleanup-and-retention">Cleanup and Retention</h2>
<p>Cleanup and retention 的核心責任是讓舊 PostgreSQL 不再成為影子真相。Migration 後若舊 DB 長期可寫，團隊會在事故中分不清哪份資料有效。</p>
<table>
  <thead>
      <tr>
          <th>Cleanup 項目</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Write disable</td>
          <td>PostgreSQL role 改 read-only 或關閉 app access</td>
      </tr>
      <tr>
          <td>Archive snapshot</td>
          <td>保存最後 dump、checksum、schema</td>
      </tr>
      <tr>
          <td>Credential removal</td>
          <td>移除 app secret、CI secret、admin token</td>
      </tr>
      <tr>
          <td>Dashboard update</td>
          <td>停用 PostgreSQL alert、啟用 SQLite alert</td>
      </tr>
      <tr>
          <td>Documentation</td>
          <td>更新 source-of-truth 與 restore route</td>
      </tr>
  </tbody>
</table>
<p>Retention 要和 data protection 對齊。若 PostgreSQL 內有 PII、audit log 或 legal retention，退役流程要依 retention policy 保存或銷毀，而非直接刪除。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是讓 simplification 保持可逆。若未來 concurrent writer、central audit、PITR 或 multi-service source-of-truth 回來，系統要能沿 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a> 重新升級。</p>
<table>
  <thead>
      <tr>
          <th>現況</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user / local artifact</td>
          <td>SQLite simplification</td>
      </tr>
      <tr>
          <td>Small internal tool + low write</td>
          <td>SQLite + restore drill</td>
      </tr>
      <tr>
          <td>Read-mostly dataset for app bundle</td>
          <td>SQLite artifact + release version</td>
      </tr>
      <tr>
          <td>Multi-user SaaS</td>
          <td>保留 PostgreSQL</td>
      </tr>
      <tr>
          <td>Audit / HA / role 是正式要求</td>
          <td>保留 managed PostgreSQL</td>
      </tr>
  </tbody>
</table>
<p>Simplification 的完成標準是：SQLite file 可以被重建、備份、恢復、升級與交接。只要這些 evidence 完整，從 PostgreSQL 退到 SQLite 是清楚的工程決策。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>PostgreSQL to SQLite simplification 完成後，先讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a> 建立 file operation；再讀 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">SQLite observability / runbook</a> 補 evidence；若之後需求再成長，回到 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a>。</p>
]]></content:encoded></item><item><title>SQLite Backup Restore Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/</guid><description>&lt;p>SQLite backup restore drill 的核心責任是證明單檔 database 可以被一致備份並還原。這篇承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>，把備份從概念轉成 artifact、validation query 與 RPO / RTO note。&lt;/p>
&lt;p>本文的驗收標準是：你能從 live &lt;code>app.db&lt;/code> 建立 backup，將它還原到隔離路徑，通過 &lt;code>integrity_check&lt;/code> 與核心查詢，並記錄 restore duration。&lt;/p>
&lt;h2 id="prepare-source">Prepare Source&lt;/h2>
&lt;p>Prepare source 的核心責任是建立一個有 WAL 與資料變化的 live database。若你已跑過 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/" data-link-title="SQLite Local File Quickstart" data-link-desc="SQLite local .db file、schema、seed data、PRAGMA baseline、query sample 與 cleanup 的操作說明">local file quickstart&lt;/a>，可以直接沿用 &lt;code>/tmp/sqlite-lab/app.db&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">mkdir -p /tmp/sqlite-lab/backup /tmp/sqlite-lab/restore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/sqlite-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;PRAGMA journal_mode = WAL;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (2, 100, &amp;#39;backup-drill-1&amp;#39;, &amp;#39;2026-05-21T01:00:00Z&amp;#39;);&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一步讓 source database 有新的資料。後續會用 backup snapshot 和 source 後續寫入做對照。&lt;/p>
&lt;h2 id="create-backup">Create Backup&lt;/h2>
&lt;p>Create backup 的核心責任是用 SQLite-aware 方法建立一致 snapshot。SQLite CLI &lt;code>.backup&lt;/code> 會透過 SQLite backup API 產出目標檔案。&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">sqlite3 app.db &lt;span class="s2">&amp;#34;.backup &amp;#39;backup/app-backup.db&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sqlite3 backup/app-backup.db &lt;span class="s2">&amp;#34;PRAGMA integrity_check;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預期 &lt;code>integrity_check&lt;/code> 輸出 &lt;code>ok&lt;/code>。這是最小 backup evidence。&lt;/p>
&lt;p>&lt;code>VACUUM INTO&lt;/code> 也可以產出 compact copy，適合想順便整理檔案大小的情境。&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">sqlite3 app.db &lt;span class="s2">&amp;#34;VACUUM INTO &amp;#39;backup/app-vacuum-copy.db&amp;#39;;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sqlite3 backup/app-vacuum-copy.db &lt;span class="s2">&amp;#34;PRAGMA integrity_check;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.backup&lt;/code> 與 &lt;code>VACUUM INTO&lt;/code> 都要在 runbook 中標明使用條件、耗時、目標路徑與失敗處理。正式環境還要記錄檔案大小、checksum 與 storage retention。&lt;/p>
&lt;h2 id="mutate-source-after-backup">Mutate Source After Backup&lt;/h2>
&lt;p>Mutate source 的核心責任是確認 backup 是時間點 snapshot。備份後對 source 寫入新資料，再用 restore 驗證 backup 保持原時間點。&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">sqlite3 app.db &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 777, &amp;#39;after-backup-write&amp;#39;, &amp;#39;2026-05-21T01:05:00Z&amp;#39;);&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;SELECT COUNT(*) FROM ledger_entries;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sqlite3 backup/app-backup.db &lt;span class="s2">&amp;#34;SELECT COUNT(*) FROM ledger_entries;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Source count 應比 backup count 多一筆。這個差異讓 RPO 討論具體化：backup 只保護到它建立的時間點。&lt;/p>
&lt;h2 id="restore-isolated-copy">Restore Isolated Copy&lt;/h2>
&lt;p>Restore isolated copy 的核心責任是避免把演練和 source 混在一起。把 backup 複製到 restore path，所有 validation 都對 restore file 執行。&lt;/p></description><content:encoded><![CDATA[<p>SQLite backup restore drill 的核心責任是證明單檔 database 可以被一致備份並還原。這篇承接 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a>，把備份從概念轉成 artifact、validation query 與 RPO / RTO note。</p>
<p>本文的驗收標準是：你能從 live <code>app.db</code> 建立 backup，將它還原到隔離路徑，通過 <code>integrity_check</code> 與核心查詢，並記錄 restore duration。</p>
<h2 id="prepare-source">Prepare Source</h2>
<p>Prepare source 的核心責任是建立一個有 WAL 與資料變化的 live database。若你已跑過 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/" data-link-title="SQLite Local File Quickstart" data-link-desc="SQLite local .db file、schema、seed data、PRAGMA baseline、query sample 與 cleanup 的操作說明">local file quickstart</a>，可以直接沿用 <code>/tmp/sqlite-lab/app.db</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">mkdir -p /tmp/sqlite-lab/backup /tmp/sqlite-lab/restore
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /tmp/sqlite-lab
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA journal_mode = WAL;&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">sqlite3 app.db <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (2, 100, &#39;backup-drill-1&#39;, &#39;2026-05-21T01:00:00Z&#39;);&#34;</span></span></span></code></pre></div><p>這一步讓 source database 有新的資料。後續會用 backup snapshot 和 source 後續寫入做對照。</p>
<h2 id="create-backup">Create Backup</h2>
<p>Create backup 的核心責任是用 SQLite-aware 方法建立一致 snapshot。SQLite CLI <code>.backup</code> 會透過 SQLite backup API 產出目標檔案。</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">sqlite3 app.db <span class="s2">&#34;.backup &#39;backup/app-backup.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-backup.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>預期 <code>integrity_check</code> 輸出 <code>ok</code>。這是最小 backup evidence。</p>
<p><code>VACUUM INTO</code> 也可以產出 compact copy，適合想順便整理檔案大小的情境。</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">sqlite3 app.db <span class="s2">&#34;VACUUM INTO &#39;backup/app-vacuum-copy.db&#39;;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-vacuum-copy.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p><code>.backup</code> 與 <code>VACUUM INTO</code> 都要在 runbook 中標明使用條件、耗時、目標路徑與失敗處理。正式環境還要記錄檔案大小、checksum 與 storage retention。</p>
<h2 id="mutate-source-after-backup">Mutate Source After Backup</h2>
<p>Mutate source 的核心責任是確認 backup 是時間點 snapshot。備份後對 source 寫入新資料，再用 restore 驗證 backup 保持原時間點。</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">sqlite3 app.db <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 777, &#39;after-backup-write&#39;, &#39;2026-05-21T01:05:00Z&#39;);&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;SELECT COUNT(*) FROM ledger_entries;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 backup/app-backup.db <span class="s2">&#34;SELECT COUNT(*) FROM ledger_entries;&#34;</span></span></span></code></pre></div><p>Source count 應比 backup count 多一筆。這個差異讓 RPO 討論具體化：backup 只保護到它建立的時間點。</p>
<h2 id="restore-isolated-copy">Restore Isolated Copy</h2>
<p>Restore isolated copy 的核心責任是避免把演練和 source 混在一起。把 backup 複製到 restore path，所有 validation 都對 restore file 執行。</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">cp backup/app-backup.db restore/app-restored.db
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">sqlite3 restore/app-restored.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sqlite3 restore/app-restored.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">.headers on
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">.mode column
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">SELECT account_id, SUM(amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">FROM ledger_entries
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">GROUP BY account_id
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">ORDER BY account_id;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>正式 restore drill 還要啟動 application 指向 <code>restore/app-restored.db</code>，跑核心 read/write smoke test。若 application 需要 migration，也要確認 restore file 的 <code>PRAGMA user_version</code> 與 app version 相容。</p>
<h2 id="rpo--rto-note">RPO / RTO Note</h2>
<p>RPO / RTO note 的核心責任是把演練結果轉成服務承諾。RPO 是可接受資料遺失窗口，RTO 是可接受恢復時間。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>本 lab 記錄方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RPO</td>
          <td>backup 建立時間到事故時間的資料差距</td>
      </tr>
      <tr>
          <td>RTO</td>
          <td>從取得 backup 到 app smoke test 成功耗時</td>
      </tr>
  </tbody>
</table>
<p>可以用 shell 的 <code>time</code> 記錄 restore duration。</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="nb">time</span> sqlite3 restore/app-restored.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>正式服務要把 RPO / RTO 寫進 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">observability / runbook</a>。</p>
<h2 id="known-gap">Known Gap</h2>
<p>Known gap 的核心責任是讓 lab 結果誠實。這個 drill 驗證 SQLite-aware backup 與 restore path；它尚未覆蓋 object storage credential、remote retention、large database restore time、encrypted disk、user device support flow 與 legal retention。</p>
<p>完成本篇後，下一步可以進入 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a> 觀察 writer boundary，或進入 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/" data-link-title="SQLite Migration Fixture Lab" data-link-desc="SQLite user_version、table rebuild migration、fixture snapshot、rollback note 與 CI evidence 的操作說明">migration fixture lab</a> 建立 schema change evidence。</p>
]]></content:encoded></item><item><title>SQLite D1 / Turso / libSQL Comparison</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/</guid><description>&lt;p>D1 / Turso / libSQL comparison 的核心責任是把 SQLite-compatible edge products 和 local SQLite 分開判讀。它們共享 SQLite 開發體驗的一部分，但它們承擔的服務責任不同：Cloudflare D1 把 SQLite-like database 放進 Workers 生態與 managed edge platform；Turso / libSQL 把 SQLite family 延伸到 remote primary、embedded replica 與同步模型；local SQLite 則是 application process 直接管理單一 database file。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite compatibility 代表開發入口接近，服務責任仍要重新審查。採用 edge SQLite 前，要先確認 write authority、read freshness、migration limit、backup evidence、observability、cost 與 vendor exit，而非只看 SQL 語法能否執行。&lt;/p>
&lt;h2 id="product-boundary">Product Boundary&lt;/h2>
&lt;p>Product boundary 的核心責任是定義誰持有資料、誰執行 SQL、誰負責恢復。Local SQLite 的資料在你的 filesystem；D1 的資料由 Cloudflare D1 平台管理並和 Workers binding 整合；Turso / libSQL 的資料通常有 remote database 與 client / embedded replica 的分工。&lt;/p>
&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>Local SQLite&lt;/td>
 &lt;td>Process-local formal state&lt;/td>
 &lt;td>CLI、desktop、single-node app&lt;/td>
 &lt;td>file lifecycle、backup、WAL、lock&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare D1&lt;/td>
 &lt;td>Workers-integrated database&lt;/td>
 &lt;td>edge app、serverless API、low ops&lt;/td>
 &lt;td>platform limit、migration、binding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Turso / libSQL&lt;/td>
 &lt;td>Remote primary + replicas&lt;/td>
 &lt;td>low-latency read、embedded replica&lt;/td>
 &lt;td>freshness、sync、driver semantics&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Litestream / LiteFS&lt;/td>
 &lt;td>Backup / replica operation&lt;/td>
 &lt;td>single-node app with recovery / read&lt;/td>
 &lt;td>RPO、RTO、primary ownership&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PostgreSQL&lt;/td>
 &lt;td>Server SQL operation&lt;/td>
 &lt;td>multi-tenant、central audit、HA、role&lt;/td>
 &lt;td>operation team、PITR、schema gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Local SQLite 的判斷重點是 file ownership。若 app 與 database file 位於同一個 host，備份、restore、disk full、permission 與 app upgrade 都在你的 runbook 裡；這條路線承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary&lt;/a>。&lt;/p>
&lt;p>D1 的判斷重點是 platform integration。Cloudflare 官方 D1 docs 把 D1 放在 Workers 與 Wrangler workflow 內，並公開 &lt;a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits&lt;/a>；因此採用 D1 時要把 database decision 與 Workers deployment、local preview、batch migration、import/export limit 一起審查。&lt;/p></description><content:encoded><![CDATA[<p>D1 / Turso / libSQL comparison 的核心責任是把 SQLite-compatible edge products 和 local SQLite 分開判讀。它們共享 SQLite 開發體驗的一部分，但它們承擔的服務責任不同：Cloudflare D1 把 SQLite-like database 放進 Workers 生態與 managed edge platform；Turso / libSQL 把 SQLite family 延伸到 remote primary、embedded replica 與同步模型；local SQLite 則是 application process 直接管理單一 database file。</p>
<p>本文的判讀錨點是：SQLite compatibility 代表開發入口接近，服務責任仍要重新審查。採用 edge SQLite 前，要先確認 write authority、read freshness、migration limit、backup evidence、observability、cost 與 vendor exit，而非只看 SQL 語法能否執行。</p>
<h2 id="product-boundary">Product Boundary</h2>
<p>Product boundary 的核心責任是定義誰持有資料、誰執行 SQL、誰負責恢復。Local SQLite 的資料在你的 filesystem；D1 的資料由 Cloudflare D1 平台管理並和 Workers binding 整合；Turso / libSQL 的資料通常有 remote database 與 client / embedded replica 的分工。</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>主要責任</th>
          <th>適合情境</th>
          <th>關鍵審查點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local SQLite</td>
          <td>Process-local formal state</td>
          <td>CLI、desktop、single-node app</td>
          <td>file lifecycle、backup、WAL、lock</td>
      </tr>
      <tr>
          <td>Cloudflare D1</td>
          <td>Workers-integrated database</td>
          <td>edge app、serverless API、low ops</td>
          <td>platform limit、migration、binding</td>
      </tr>
      <tr>
          <td>Turso / libSQL</td>
          <td>Remote primary + replicas</td>
          <td>low-latency read、embedded replica</td>
          <td>freshness、sync、driver semantics</td>
      </tr>
      <tr>
          <td>Litestream / LiteFS</td>
          <td>Backup / replica operation</td>
          <td>single-node app with recovery / read</td>
          <td>RPO、RTO、primary ownership</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>Server SQL operation</td>
          <td>multi-tenant、central audit、HA、role</td>
          <td>operation team、PITR、schema gate</td>
      </tr>
  </tbody>
</table>
<p>Local SQLite 的判斷重點是 file ownership。若 app 與 database file 位於同一個 host，備份、restore、disk full、permission 與 app upgrade 都在你的 runbook 裡；這條路線承接 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>。</p>
<p>D1 的判斷重點是 platform integration。Cloudflare 官方 D1 docs 把 D1 放在 Workers 與 Wrangler workflow 內，並公開 <a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits</a>；因此採用 D1 時要把 database decision 與 Workers deployment、local preview、batch migration、import/export limit 一起審查。</p>
<p>Turso / libSQL 的判斷重點是 replica freshness 與 client semantics。Turso docs 對 <a href="https://docs.turso.tech/features/embedded-replicas/introduction">embedded replicas</a> 的描述顯示：application 可以持有 local replica 並透過同步取得資料；這會把「讀得快」和「讀到多新」變成同一個設計問題。</p>
<h2 id="edge-data-model">Edge Data Model</h2>
<p>Edge data model 的核心責任是把 latency 改善與一致性責任拆開。Edge database 的價值常來自 closer read path、serverless deployment 與較低操作表面；風險則集中在 write authority、replication lag、region routing 與平台限制。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>要觀察的訊號</th>
          <th>設計含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>誰可以寫</td>
          <td>single primary、remote write、queue</td>
          <td>決定 conflict、retry、idempotency 設計</td>
      </tr>
      <tr>
          <td>讀取要多新</td>
          <td>read-after-write、sync interval</td>
          <td>決定 UI freshness、cache invalidation、fallback</td>
      </tr>
      <tr>
          <td>migration 怎麼跑</td>
          <td>CLI、batch limit、preview / prod gap</td>
          <td>決定 release gate 與 rollback plan</td>
      </tr>
      <tr>
          <td>失敗時如何恢復</td>
          <td>export、backup、restore command</td>
          <td>決定 RPO / RTO 與 vendor exit</td>
      </tr>
      <tr>
          <td>observability 在哪一層</td>
          <td>platform metrics、app log、query log</td>
          <td>決定 incident triage 從 app 還是 platform 開始查</td>
      </tr>
  </tbody>
</table>
<p>Write authority 是 edge SQLite 的第一個分水嶺。若所有 write 都集中到 remote primary，application 要處理 network error、retry、idempotency 與 read freshness；若 write 發生在 local replica，系統要有 conflict resolution、sync ordering 與 delete propagation。</p>
<p>Read locality 是 edge SQLite 的主要收益。它適合 session-local preference、read-mostly catalog、低風險 personalization、feature flag snapshot、tenant-local small dataset；這些情境的共同點是資料量小、write rate 低、freshness 可以定義。</p>
<p>Global transaction 是 edge SQLite 的高風險區。若產品需求包含跨 region balance transfer、inventory reservation、ledger posting、strongly consistent permission decision，設計應路由到 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a> 或 PostgreSQL / CockroachDB / Spanner 的 transactional model。</p>
<h2 id="migration-gap">Migration Gap</h2>
<p>Migration gap 的核心責任是確認 SQLite file 可以搬到 edge product 後，release workflow 仍可驗證。SQL syntax compatibility 只解決起點；真正會造成事故的是 batch limit、extension 差異、driver API、local preview 與 production platform 行為差異。</p>
<table>
  <thead>
      <tr>
          <th>差異面</th>
          <th>審查問題</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL dialect</td>
          <td>schema、index、trigger、JSON 是否可用</td>
          <td>compatibility matrix + migration dry run</td>
      </tr>
      <tr>
          <td>Data movement</td>
          <td>seed / import / export 的容量與時間</td>
          <td>sample import、row count、checksum</td>
      </tr>
      <tr>
          <td>Runtime binding</td>
          <td>app 如何取得 database connection</td>
          <td>staging deployment + smoke test</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>write path 是否跨 request / region</td>
          <td>failure injection、retry log、freshness test</td>
      </tr>
      <tr>
          <td>Backup / exit</td>
          <td>如何拿回 SQLite-compatible artifact</td>
          <td>export file、restore drill、retention note</td>
      </tr>
  </tbody>
</table>
<p>D1 migration 要把 Wrangler workflow 納入 release gate。Cloudflare D1 的 limits 文件明確列出 import、query、batch 等限制；因此大型 update / delete 要拆 batch，migration 要有 staging dry run 與 production rollback step。</p>
<p>Turso / libSQL migration 要把 driver semantics 納入 release gate。Local SQLite driver 直連 file；libSQL client 可能連 remote endpoint 或 embedded replica；application 要把 connection lifecycle、sync timing、auth token、network failure 與 local cache freshness 寫進測試。</p>
<h2 id="operational-model">Operational Model</h2>
<p>Operational model 的核心責任是把 managed convenience 轉成 ownership map。Edge SQLite 減少了部分 server operation，但新增 platform limit、billing、region behavior、vendor incident、CLI workflow 與 local preview mismatch。</p>
<p>Production runbook 至少要保存五種證據：</p>
<ol>
<li>Schema migration history 與每次 release 的 dry-run result。</li>
<li>Data import / export 指令、檔案大小、row count 與 checksum。</li>
<li>Region latency、read freshness、write error rate 與 retry count。</li>
<li>Platform limit 命中紀錄、batch policy 與成本警戒線。</li>
<li>Vendor exit route：回 local SQLite、PostgreSQL 或另一個 edge database 的最小搬遷步驟。</li>
</ol>
<p>成本模型要同時看 request、storage、egress、operation time 與工程鎖定。Edge product 常把起步成本壓低，但當資料變大、batch migration 變長、observability 需要外掛、vendor API 滲入 repository layer 時，長期成本會出現在 release 與 incident。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把需求送到相符的資料模型。D1 / Turso / libSQL 適合 edge locality 與低操作表面；當需求轉向 high-write OLTP、central audit、role-based permission、global transaction 或跨服務資料治理，應轉向 server SQL 或 distributed OLTP。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>優先路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers app 需要小型 relational data</td>
          <td>Cloudflare D1 + explicit limits review</td>
      </tr>
      <tr>
          <td>App 需要 local read latency + remote sync</td>
          <td>Turso / libSQL + freshness contract</td>
      </tr>
      <tr>
          <td>Single-node app 只需要備份與恢復</td>
          <td>Local SQLite + <a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>多 tenant、central audit、DB role</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Global write consistency</td>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a> 或 Spanner</td>
      </tr>
  </tbody>
</table>
<p>D1 的採用條件是 edge runtime 本身就是主平台。若 application 已在 Workers 上、資料量可控、query pattern 清楚、migration 可 batch，D1 可以把 database operation 融入 deployment workflow。</p>
<p>Turso / libSQL 的採用條件是 local read value 高於同步複雜度。若產品可明確定義 stale read window、write path 與 conflict policy，embedded replica 可以降低 latency；若使用者需要立即看見跨裝置變更，就要先設計 freshness evidence。</p>
<h2 id="production-tripwires">Production Tripwires</h2>
<p>Production tripwires 的核心責任是指出何時重新評估 edge SQLite。這些訊號出現時，系統通常已從「SQLite-compatible convenience」進入正式 database governance。</p>
<table>
  <thead>
      <tr>
          <th>Tripwire</th>
          <th>意義</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Migration batch 經常碰 limit</td>
          <td>schema 與資料量超過 edge workflow</td>
          <td>評估 PostgreSQL / managed SQL</td>
      </tr>
      <tr>
          <td>Read freshness ticket 增加</td>
          <td>replica / sync 語意影響產品體驗</td>
          <td>建 freshness SLO 或改集中讀寫</td>
      </tr>
      <tr>
          <td>Export / restore 未演練</td>
          <td>vendor exit 與災難恢復缺 evidence</td>
          <td>補 restore drill 與 retention policy</td>
      </tr>
      <tr>
          <td>Driver API 滲入 domain</td>
          <td><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 進入核心程式碼</td>
          <td>建 repository adapter 與 compatibility test</td>
      </tr>
      <tr>
          <td>Cross-region write 需求出現</td>
          <td>edge-local read 已不足</td>
          <td>路由到 distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>這些 tripwire 要寫進設計文件與 runbook。Edge SQLite 的優勢在於低摩擦起步；它的長期品質來自早期把 ownership、limits、exit 與 evidence 設計清楚。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>D1 / Turso / libSQL comparison 完成後，下一步要依壓力路由。要處理 local file 與 backup，讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>；要處理 replica / restore，讀 <a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication</a>；要從 local SQLite 移到 edge product，讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso migration</a>；要處理 global write，回到 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a>。</p>
]]></content:encoded></item><item><title>SQLite D1 / Turso Preview Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/d1-turso-preview-lab/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/d1-turso-preview-lab/</guid><description>&lt;p>SQLite D1 / Turso preview lab 的核心責任是把 local SQLite 轉向 edge SQLite product 前的 compatibility gap 找出來。這篇承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL Comparison&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso Migration&lt;/a>，把 edge migration 變成可回報的 query matrix。&lt;/p>
&lt;p>本文的驗收標準是：你能從 local SQLite 匯出 schema / seed，匯入 D1 或 Turso preview database，跑相同 query set，記錄 unsupported SQL、latency、error mapping 與 rollback route。&lt;/p>
&lt;h2 id="preview-scope">Preview Scope&lt;/h2>
&lt;p>Preview scope 的核心責任是把 lab 限制在 staging / preview。D1 與 Turso 都是平台產品，實際命令會依 CLI version、帳號、region 與專案設定改變；本文提供操作骨架與 evidence 格式，正式命令以官方文件為準。&lt;/p>
&lt;p>官方文件路由：&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>Cloudflare D1&lt;/td>
 &lt;td>&lt;a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D1 limits&lt;/td>
 &lt;td>&lt;a href="https://developers.cloudflare.com/d1/platform/limits/">Cloudflare D1 limits&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Turso&lt;/td>
 &lt;td>&lt;a href="https://docs.turso.tech/">Turso docs&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Turso embedded replicas&lt;/td>
 &lt;td>&lt;a href="https://docs.turso.tech/features/embedded-replicas/introduction">Embedded replicas&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Preview lab 要先確認資料不含 production PII。若 seed data 來自正式資料，先做 masking 或 synthetic data。&lt;/p>
&lt;h2 id="export-local-sqlite">Export Local SQLite&lt;/h2>
&lt;p>Export local SQLite 的核心責任是建立 target platform 的 seed input。沿用 &lt;code>/tmp/sqlite-lab/app.db&lt;/code> 或 migration fixture。&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">mkdir -p /tmp/sqlite-edge-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/sqlite-edge-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">cp /tmp/sqlite-lab/app.db ./app.db
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;.schema&amp;#34;&lt;/span> &amp;gt; schema.sql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;.dump&amp;#34;&lt;/span> &amp;gt; seed.sql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;SELECT COUNT(*) FROM accounts;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;SELECT COUNT(*) FROM ledger_entries;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>schema.sql&lt;/code> 用來審查 DDL，&lt;code>seed.sql&lt;/code> 用來匯入 preview database。正式 migration 可能要拆 schema / data / index，並處理 target platform limits。&lt;/p>
&lt;h2 id="build-query-matrix">Build Query Matrix&lt;/h2>
&lt;p>Build query matrix 的核心責任是定義 preview 要驗證什麼。query set 要代表產品行為，而非只跑一個 &lt;code>SELECT 1&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>SQLite D1 / Turso preview lab 的核心責任是把 local SQLite 轉向 edge SQLite product 前的 compatibility gap 找出來。這篇承接 <a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL Comparison</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso Migration</a>，把 edge migration 變成可回報的 query matrix。</p>
<p>本文的驗收標準是：你能從 local SQLite 匯出 schema / seed，匯入 D1 或 Turso preview database，跑相同 query set，記錄 unsupported SQL、latency、error mapping 與 rollback route。</p>
<h2 id="preview-scope">Preview Scope</h2>
<p>Preview scope 的核心責任是把 lab 限制在 staging / preview。D1 與 Turso 都是平台產品，實際命令會依 CLI version、帳號、region 與專案設定改變；本文提供操作骨架與 evidence 格式，正式命令以官方文件為準。</p>
<p>官方文件路由：</p>
<table>
  <thead>
      <tr>
          <th>產品</th>
          <th>官方文件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloudflare D1</td>
          <td><a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs</a></td>
      </tr>
      <tr>
          <td>D1 limits</td>
          <td><a href="https://developers.cloudflare.com/d1/platform/limits/">Cloudflare D1 limits</a></td>
      </tr>
      <tr>
          <td>Turso</td>
          <td><a href="https://docs.turso.tech/">Turso docs</a></td>
      </tr>
      <tr>
          <td>Turso embedded replicas</td>
          <td><a href="https://docs.turso.tech/features/embedded-replicas/introduction">Embedded replicas</a></td>
      </tr>
  </tbody>
</table>
<p>Preview lab 要先確認資料不含 production PII。若 seed data 來自正式資料，先做 masking 或 synthetic data。</p>
<h2 id="export-local-sqlite">Export Local SQLite</h2>
<p>Export local SQLite 的核心責任是建立 target platform 的 seed input。沿用 <code>/tmp/sqlite-lab/app.db</code> 或 migration fixture。</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">mkdir -p /tmp/sqlite-edge-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /tmp/sqlite-edge-lab
</span></span><span class="line"><span class="ln">3</span><span class="cl">cp /tmp/sqlite-lab/app.db ./app.db
</span></span><span class="line"><span class="ln">4</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.schema&#34;</span> &gt; schema.sql
</span></span><span class="line"><span class="ln">5</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.dump&#34;</span> &gt; seed.sql
</span></span><span class="line"><span class="ln">6</span><span class="cl">sqlite3 app.db <span class="s2">&#34;SELECT COUNT(*) FROM accounts;&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">sqlite3 app.db <span class="s2">&#34;SELECT COUNT(*) FROM ledger_entries;&#34;</span></span></span></code></pre></div><p><code>schema.sql</code> 用來審查 DDL，<code>seed.sql</code> 用來匯入 preview database。正式 migration 可能要拆 schema / data / index，並處理 target platform limits。</p>
<h2 id="build-query-matrix">Build Query Matrix</h2>
<p>Build query matrix 的核心責任是定義 preview 要驗證什麼。query set 要代表產品行為，而非只跑一個 <code>SELECT 1</code>。</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">Q1 list account balances
</span></span><span class="line"><span class="ln">2</span><span class="cl">Q2 insert ledger entry with unique idempotency key
</span></span><span class="line"><span class="ln">3</span><span class="cl">Q3 insert duplicate idempotency key and capture error
</span></span><span class="line"><span class="ln">4</span><span class="cl">Q4 foreign key violation
</span></span><span class="line"><span class="ln">5</span><span class="cl">Q5 transaction rollback
</span></span><span class="line"><span class="ln">6</span><span class="cl">Q6 pagination by created_at
</span></span><span class="line"><span class="ln">7</span><span class="cl">Q7 explain / performance sample if platform supports it</span></span></code></pre></div><p>這份 matrix 要保存 expected result。Local SQLite 先跑一次，把 row count、error category、latency baseline 記下來。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">.timer on
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT a.id, a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">FROM accounts a
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">GROUP BY a.id, a.owner_name
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">ORDER BY a.id;
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><h2 id="import-to-d1-preview">Import to D1 Preview</h2>
<p>Import to D1 preview 的核心責任是驗證 Cloudflare D1 workflow。以下是操作骨架，正式命令與 flags 以 Cloudflare D1 docs 和 Wrangler 版本為準。</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"># Example shape only. Use your project naming and official Wrangler docs.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wrangler d1 create sqlite_edge_preview
</span></span><span class="line"><span class="ln">3</span><span class="cl">wrangler d1 execute sqlite_edge_preview --file<span class="o">=</span>seed.sql
</span></span><span class="line"><span class="ln">4</span><span class="cl">wrangler d1 execute sqlite_edge_preview --command<span class="o">=</span><span class="s2">&#34;SELECT COUNT(*) FROM accounts;&#34;</span></span></span></code></pre></div><p>D1 preview evidence 要記錄：</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CLI version</td>
          <td>Wrangler version、account / project</td>
      </tr>
      <tr>
          <td>Import log</td>
          <td>duration、file size、error</td>
      </tr>
      <tr>
          <td>Query result</td>
          <td>每個 query 的 row count / error</td>
      </tr>
      <tr>
          <td>Limit hit</td>
          <td>D1 limits 是否影響 seed 或 query</td>
      </tr>
      <tr>
          <td>Rollback</td>
          <td>刪除 preview DB 或重建 seed</td>
      </tr>
  </tbody>
</table>
<p>若 seed file 太大或某些 SQL 需要改寫，就把 gap 寫進 compatibility matrix，先保留 production migration 的審查邊界。</p>
<h2 id="import-to-turso-preview">Import to Turso Preview</h2>
<p>Import to Turso preview 的核心責任是驗證 remote database、client SDK 與 embedded replica 行為。以下是操作骨架，正式命令以 Turso docs 與 CLI version 為準。</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"># Example shape only. Use your org, group, region and official Turso docs.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">turso db create sqlite-edge-preview
</span></span><span class="line"><span class="ln">3</span><span class="cl">turso db shell sqlite-edge-preview &lt; seed.sql
</span></span><span class="line"><span class="ln">4</span><span class="cl">turso db shell sqlite-edge-preview <span class="s2">&#34;SELECT COUNT(*) FROM accounts;&#34;</span></span></span></code></pre></div><p>Turso preview evidence 要多記 replica freshness。若使用 embedded replica，測試流程要包含 bootstrap、sync、read query、write delegation 與 sync 後 read。</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">embedded replica evidence:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  bootstrap duration
</span></span><span class="line"><span class="ln">3</span><span class="cl">  first read latency
</span></span><span class="line"><span class="ln">4</span><span class="cl">  write path
</span></span><span class="line"><span class="ln">5</span><span class="cl">  sync command / interval
</span></span><span class="line"><span class="ln">6</span><span class="cl">  read freshness after write</span></span></code></pre></div><p>Freshness 是 product decision。若 query matrix 只測 remote primary，仍需要追加 embedded replica 的使用者體驗驗證。</p>
<h2 id="compatibility-matrix">Compatibility Matrix</h2>
<p>Compatibility matrix 的核心責任是把 local SQLite 與 edge target 的差異留下來。建議表格欄位如下：</p>
<table>
  <thead>
      <tr>
          <th>Query / operation</th>
          <th>Local SQLite</th>
          <th>D1 preview</th>
          <th>Turso preview</th>
          <th>Decision</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Balance list</td>
          <td>pass</td>
          <td>pass / diff</td>
          <td>pass / diff</td>
          <td>keep / rewrite</td>
      </tr>
      <tr>
          <td>Unique violation</td>
          <td>error class</td>
          <td>error class</td>
          <td>error class</td>
          <td>map error</td>
      </tr>
      <tr>
          <td>FK violation</td>
          <td>error class</td>
          <td>error class</td>
          <td>error class</td>
          <td>enable / validate</td>
      </tr>
      <tr>
          <td>Transaction rollback</td>
          <td>pass</td>
          <td>pass / diff</td>
          <td>pass / diff</td>
          <td>rewrite workflow</td>
      </tr>
      <tr>
          <td>Import seed</td>
          <td>pass</td>
          <td>duration / limit</td>
          <td>duration / limit</td>
          <td>split batch</td>
      </tr>
  </tbody>
</table>
<p>Decision 欄要寫具體下一步。<code>rewrite workflow</code> 代表 application adapter 要改；<code>split batch</code> 代表 migration runbook 要改；<code>map error</code> 代表 repository error classification 要改。</p>
<h2 id="latency-and-cost-sample">Latency and Cost Sample</h2>
<p>Latency and cost sample 的核心責任是避免只看功能相容。Edge SQLite migration 的收益常來自 region latency 或 managed operation，因此 preview 要量測主要使用者區域的 read / write latency。</p>
<p>最小量測：</p>
<ol>
<li>Local baseline latency。</li>
<li>Preview target read latency。</li>
<li>Preview target write latency。</li>
<li>Error rate / retry count。</li>
<li>Estimated request / storage / egress cost。</li>
</ol>
<p>Latency sample 要搭配 freshness。快速讀到舊資料和稍慢讀到最新資料是不同產品體驗；query matrix 要標註哪個 workflow 可以接受 stale read。</p>
<h2 id="rollback-route">Rollback Route</h2>
<p>Rollback route 的核心責任是保留 local SQLite 退路。Preview lab 完成後，要能刪除 preview database、保留 local seed、重跑 local app。</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">sqlite3 app.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;SELECT COUNT(*) FROM accounts;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 app.db <span class="s2">&#34;SELECT COUNT(*) FROM ledger_entries;&#34;</span></span></span></code></pre></div><p>正式 cutover 的 rollback 還要處理 target-only writes。Preview 階段應避免讓真實使用者寫入 target；若需要 shadow traffic，先用 read-only 或 synthetic write。</p>
<h2 id="completion-note">Completion Note</h2>
<p>Completion note 的核心責任是決定是否進入正式 migration。Lab 完成後應輸出四個 artifact：<code>seed.sql</code>、import log、compatibility matrix、rollback note。</p>
<p>進入正式 migration 的條件：</p>
<ol>
<li>Query matrix 主要 workflow 通過或已有 rewrite plan。</li>
<li>Platform limits 對資料量與 migration time 可接受。</li>
<li>Error mapping 已接到 repository adapter。</li>
<li>Freshness / latency 符合產品需求。</li>
<li>Export / rollback route 已演練。</li>
</ol>
<p>完成本篇後，回到 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso Migration</a> 補正式 phase plan。</p>
]]></content:encoded></item><item><title>SQLite file lifecycle 與 backup boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 embedded、local-first、edge 與低操作成本場景；本文聚焦 &lt;em>SQLite 檔案生命週期 + backup / restore 邊界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite 的 file lifecycle 是把「一個資料庫檔案」升級成正式狀態的操作契約。SQLite 省掉 server process、帳號管理與網路連線，但它把 durability、backup、restore、locking 與 corruption recovery 放回 application process、filesystem 與 runbook；讀者要判斷的是這些責任是否已經有人承擔。&lt;/p>
&lt;p>這篇文章適合三種情境。第一種是 CLI、desktop、mobile 或 edge service 已經用 SQLite 保存正式資料；第二種是 single-instance backend 想用 SQLite 降低操作成本；第三種是 test fixture 用 SQLite，但需要知道哪些差異會讓 production database 的 bug 漏掉。&lt;/p>
&lt;h2 id="核心模型資料庫檔案是一組受-sqlite-管理的狀態檔">核心模型：資料庫檔案是一組受 SQLite 管理的狀態檔&lt;/h2>
&lt;p>SQLite 的資料庫狀態由 main database file 與 journal / WAL sidecar 共同構成。Rollback journal mode 會在寫入期間產生 journal file；WAL mode 會讓寫入先進入 &lt;code>-wal&lt;/code> 檔，並用 &lt;code>-shm&lt;/code> 檔協調 reader / writer。操作上看似「一個 &lt;code>.db&lt;/code> 檔」，production runbook 要把 sidecar file、checkpoint、backup API 與 restore test 一起納入。&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>&lt;code>.db&lt;/code>&lt;/td>
 &lt;td>持久化資料、schema、index&lt;/td>
 &lt;td>file owner、permission、storage durability、snapshot 位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-wal&lt;/code>&lt;/td>
 &lt;td>WAL mode 下尚未 checkpoint 的寫入&lt;/td>
 &lt;td>WAL growth、checkpoint cadence、backup 是否包含一致快照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-shm&lt;/code>&lt;/td>
 &lt;td>WAL index 與跨 connection 協調&lt;/td>
 &lt;td>local filesystem lock 是否可靠、部署是否跨 process 共用檔案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>checkpoint&lt;/td>
 &lt;td>把 WAL 內容合併回 main database&lt;/td>
 &lt;td>checkpoint latency、writer pause、檔案大小是否持續膨脹&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backup API&lt;/td>
 &lt;td>線上複製一致 snapshot&lt;/td>
 &lt;td>backup 是否在 application 還活著時仍能取得一致狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的讀法是先找「誰有權改檔案」。SQLite 的核心風險多半來自繞過 SQLite library 的檔案操作，例如直接 copy 活躍 WAL database、把 database 放在 lock 語意不可靠的 filesystem、或讓多個不協調的 process 同時寫同一份檔案。&lt;/p>
&lt;h2 id="wal-mode讀取並發提升後writer-boundary-仍然存在">WAL mode：讀取並發提升後，writer boundary 仍然存在&lt;/h2>
&lt;p>WAL mode 的工程價值是讓 reader 與 writer 的衝突下降。讀取可以看 main database 加上 WAL 中的 snapshot，寫入則 append 到 WAL；這讓 read-heavy workload 比 rollback journal mode 更容易撐住互動式服務。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 embedded、local-first、edge 與低操作成本場景；本文聚焦 <em>SQLite 檔案生命週期 + backup / restore 邊界</em>。</p></blockquote>
<p>SQLite 的 file lifecycle 是把「一個資料庫檔案」升級成正式狀態的操作契約。SQLite 省掉 server process、帳號管理與網路連線，但它把 durability、backup、restore、locking 與 corruption recovery 放回 application process、filesystem 與 runbook；讀者要判斷的是這些責任是否已經有人承擔。</p>
<p>這篇文章適合三種情境。第一種是 CLI、desktop、mobile 或 edge service 已經用 SQLite 保存正式資料；第二種是 single-instance backend 想用 SQLite 降低操作成本；第三種是 test fixture 用 SQLite，但需要知道哪些差異會讓 production database 的 bug 漏掉。</p>
<h2 id="核心模型資料庫檔案是一組受-sqlite-管理的狀態檔">核心模型：資料庫檔案是一組受 SQLite 管理的狀態檔</h2>
<p>SQLite 的資料庫狀態由 main database file 與 journal / WAL sidecar 共同構成。Rollback journal mode 會在寫入期間產生 journal file；WAL mode 會讓寫入先進入 <code>-wal</code> 檔，並用 <code>-shm</code> 檔協調 reader / writer。操作上看似「一個 <code>.db</code> 檔」，production runbook 要把 sidecar file、checkpoint、backup API 與 restore test 一起納入。</p>
<table>
  <thead>
      <tr>
          <th>檔案 / 機制</th>
          <th>服務責任</th>
          <th>操作判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.db</code></td>
          <td>持久化資料、schema、index</td>
          <td>file owner、permission、storage durability、snapshot 位置</td>
      </tr>
      <tr>
          <td><code>-wal</code></td>
          <td>WAL mode 下尚未 checkpoint 的寫入</td>
          <td>WAL growth、checkpoint cadence、backup 是否包含一致快照</td>
      </tr>
      <tr>
          <td><code>-shm</code></td>
          <td>WAL index 與跨 connection 協調</td>
          <td>local filesystem lock 是否可靠、部署是否跨 process 共用檔案</td>
      </tr>
      <tr>
          <td>checkpoint</td>
          <td>把 WAL 內容合併回 main database</td>
          <td>checkpoint latency、writer pause、檔案大小是否持續膨脹</td>
      </tr>
      <tr>
          <td>backup API</td>
          <td>線上複製一致 snapshot</td>
          <td>backup 是否在 application 還活著時仍能取得一致狀態</td>
      </tr>
  </tbody>
</table>
<p>這張表的讀法是先找「誰有權改檔案」。SQLite 的核心風險多半來自繞過 SQLite library 的檔案操作，例如直接 copy 活躍 WAL database、把 database 放在 lock 語意不可靠的 filesystem、或讓多個不協調的 process 同時寫同一份檔案。</p>
<h2 id="wal-mode讀取並發提升後writer-boundary-仍然存在">WAL mode：讀取並發提升後，writer boundary 仍然存在</h2>
<p>WAL mode 的工程價值是讓 reader 與 writer 的衝突下降。讀取可以看 main database 加上 WAL 中的 snapshot，寫入則 append 到 WAL；這讓 read-heavy workload 比 rollback journal mode 更容易撐住互動式服務。</p>
<p>WAL mode 同時保留 single writer boundary。SQLite 仍以檔案鎖與 transaction serialisation 控制寫入；寫入交易越長，其他 writer 等待時間越長，application 看到的訊號通常是 <code>SQLITE_BUSY</code>、latency spike 或 background job 卡住。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>常見原因</th>
          <th>第一輪處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> 增加</td>
          <td>長交易、background migration、慢 disk</td>
          <td>縮短 write transaction、加 busy timeout、把批次寫入切小</td>
      </tr>
      <tr>
          <td><code>-wal</code> 檔持續變大</td>
          <td>checkpoint 追不上、long reader 卡住</td>
          <td>找出長讀取、調整 checkpoint cadence、把 analytics query 移出路徑</td>
      </tr>
      <tr>
          <td>restore 後資料落差</td>
          <td>backup 沒取得一致 snapshot</td>
          <td>改用 <code>.backup</code> / backup API / <code>VACUUM INTO</code>，並演練 restore</td>
      </tr>
      <tr>
          <td>latency 受 fsync 拉高</td>
          <td><code>synchronous=FULL</code> + 高寫入頻率</td>
          <td>重新定義 durability 需求，評估 server SQL 或 managed service</td>
      </tr>
  </tbody>
</table>
<p>WAL mode 的 capacity gate 是「寫入是否仍能用一個 writer 排隊」。如果服務壓力來自大量並行寫入、多 instance active write 或跨 region 寫入，SQLite 的簡單性開始變成排隊與恢復成本；這時候要回到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> 或 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">global distributed OLTP</a>。</p>
<h2 id="backup-boundary複製檔案與取得一致-snapshot-是兩件事">Backup boundary：複製檔案與取得一致 snapshot 是兩件事</h2>
<p>SQLite backup 的核心責任是取得某一時間點的一致 snapshot。當 database live 且 WAL mode 開啟時，直接複製 <code>.db</code> 檔容易漏掉 <code>-wal</code> 中尚未 checkpoint 的寫入；即使同時複製 sidecar file，也要面對複製期間狀態變動的 race。正式服務應使用 SQLite 提供的 backup path 或可驗證的 filesystem snapshot。</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>適合情境</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.backup</code> / Backup API</td>
          <td>live database、application 仍在服務</td>
          <td>SQLite 管理 source lock，產出開始備份時的一致 snapshot</td>
      </tr>
      <tr>
          <td><code>VACUUM INTO</code></td>
          <td>想同時 compact + 輸出新檔</td>
          <td>需要 I/O 空間與時間，適合 maintenance 或低流量窗口</td>
      </tr>
      <tr>
          <td>filesystem snapshot</td>
          <td>VM / volume 層已有一致 snapshot 能力</td>
          <td>要確認 snapshot 包含 main file 與 WAL sidecar，且 lock 語意清楚</td>
      </tr>
      <tr>
          <td>Litestream</td>
          <td>single-primary SQLite 的持續備份</td>
          <td>適合 DR / restore，不把 SQLite 變成 multi-primary database</td>
      </tr>
      <tr>
          <td>手動 <code>cp</code></td>
          <td>database 已關閉或已完成 checkpoint</td>
          <td>live WAL database 的一致性風險高，production runbook 應改路由</td>
      </tr>
  </tbody>
</table>
<p>Backup method 的選擇要先回到 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 與 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>。如果產品可以接受每天一次快照，<code>VACUUM INTO</code> 或 scheduled backup 足夠；如果資料損失窗口要降到分鐘級或秒級，就要看 Litestream 類連續複製，或直接升級到 server database 的 PITR / replica 模型。</p>
<h2 id="restore-drillsqlite-production-readiness-看還原不只看備份成功">Restore drill：SQLite production readiness 看還原，不只看備份成功</h2>
<p>Restore drill 的責任是證明備份能在事故時接回服務。SQLite 的備份檔通常只有一個 target file，表面上比 PostgreSQL PITR 或 MySQL binlog recovery 簡單；真正的風險在 application binary、schema migration version、file permission、deployment path 與舊 WAL sidecar 是否一起對齊。</p>
<p>一個最小 restore drill 應保留五個檢查點：</p>
<ol>
<li>從備份產出新的 database file，不覆蓋 production path。</li>
<li>用 application binary 啟動 read-only smoke test，確認 schema version 與 migration table。</li>
<li>跑 row count、critical query、checksum 或 domain validation query。</li>
<li>驗證 file owner、permission、disk path、SELinux / container mount 或 volume 設定。</li>
<li>以 incident decision log 記錄 restore time、data freshness、known gap 與 owner。</li>
</ol>
<p>Restore drill 的交付物應接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。SQLite 的低操作成本來自日常元件少；事故時仍需要 evidence、owner 與 rollback condition。</p>
<h2 id="corruption-recovery先保全證據再決定修復或還原">Corruption recovery：先保全證據，再決定修復或還原</h2>
<p>SQLite <a href="/blog/backend/knowledge-cards/corruption-recovery/" data-link-title="Corruption Recovery" data-link-desc="說明資料損毀事故如何先辨識來源、保全證據，再決定修復或還原">corruption recovery</a> 的核心責任是區分「資料庫檔案本身受損」與「application 寫入了錯誤資料」。前者要走 file-level evidence、<code>.recover</code>、backup restore 與 filesystem / hardware investigation；後者要走資料修復、migration rollback 或 business reconciliation。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>優先判讀</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_CORRUPT</code></td>
          <td>database page / btree 受損</td>
          <td>複製原檔保存證據、用 <code>.recover</code> 嘗試導出、從最近 backup 建新檔</td>
      </tr>
      <tr>
          <td>power loss 後啟動異常</td>
          <td>journal / WAL recovery 問題</td>
          <td>確認 sidecar file 是否仍在、檢查 storage sync 與 <code>synchronous</code> 設定</td>
      </tr>
      <tr>
          <td>restore 後 business data 錯誤</td>
          <td>備份點或 migration 錯誤</td>
          <td>對照 validation query、migration log、事件補償與 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">reconciliation</a></td>
      </tr>
      <tr>
          <td>network filesystem 上偶發錯誤</td>
          <td>lock 語意與 filesystem 問題</td>
          <td>把 SQLite 移回 local disk，或升級 server database</td>
      </tr>
  </tbody>
</table>
<p>Corruption 事件的第一個操作是保存原始檔案與 sidecar。直接在疑似受損檔案上跑修復、vacuum 或 application migration，會讓後續 root cause analysis 失去證據；比較穩定的流程是複製原檔、在副本上嘗試 <code>.recover</code>，同時從備份恢復服務路徑。</p>
<h2 id="anti-recommendation維持-sqlite-的條件要可被操作驗證">Anti-recommendation：維持 SQLite 的條件要可被操作驗證</h2>
<p>SQLite 的合理使用條件是「單一 writer、檔案生命週期清楚、restore drill 成立」。只要這三件事能被 runbook 驗證，SQLite 在 embedded、desktop、mobile、edge-local 或 small backend 場景可以是 production state。</p>
<p>升級條件則來自操作責任外溢。需要 database user / role、中心化 audit、多人同時寫、跨 instance failover、online schema migration、PITR、read replica 或跨 region transaction 時，server SQL 或 managed SQL 的操作模型會比繼續包裝 SQLite 清楚。</p>
<table>
  <thead>
      <tr>
          <th>目前壓力</th>
          <th>留在 SQLite 的條件</th>
          <th>升級路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>read-heavy local store</td>
          <td>WAL + restore drill 成立</td>
          <td>維持 SQLite，補 observability 與 backup evidence</td>
      </tr>
      <tr>
          <td>single-instance backend</td>
          <td>writer queue 可接受、RPO / RTO 明確</td>
          <td>SQLite + Litestream；或升級 PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>edge / serverless</td>
          <td>平台已提供 SQLite-compatible 運作模型</td>
          <td>Cloudflare D1 / Turso；跨 region transaction 回到 global DB</td>
      </tr>
      <tr>
          <td>multi-tenant SaaS</td>
          <td>tenant 數少且 file ownership 清楚</td>
          <td>PostgreSQL / Aurora / CockroachDB</td>
      </tr>
      <tr>
          <td>regulated data</td>
          <td>backup encryption、audit、restore 可驗證</td>
          <td>PostgreSQL / managed SQL + audit / PITR</td>
      </tr>
  </tbody>
</table>
<p>這張表的核心是把操作責任具體化，而非替 SQLite 設流量天花板。小型服務可能用 SQLite 長期穩定運作；同樣流量下，一旦合規、稽核、多人操作或 HA 需求進來，server database 的長期成本會更容易被治理。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite production runbook 至少要能回答下列問題：</p>
<ol>
<li>Database file、WAL sidecar 與 backup target 在哪個 volume、由誰擁有。</li>
<li><code>journal_mode</code>、<code>synchronous</code>、busy timeout、checkpoint cadence 與 migration policy 如何設定。</li>
<li>Backup 用 <code>.backup</code> / backup API / <code>VACUUM INTO</code> / Litestream 的哪一條路徑。</li>
<li>Restore drill 最近一次何時執行，RPO / RTO 是否符合產品承諾。</li>
<li><code>SQLITE_BUSY</code>、WAL growth、disk full、backup failure 與 restore failure 如何告警。</li>
<li>Corruption recovery 時誰保存原檔、誰啟動 restore、誰決定修復或 fail-forward。</li>
</ol>
<p>這份清單要接到服務 ownership，而非留在工程師個人習慣。SQLite 的優勢是 deployment surface 小；production 化的代價是把檔案、備份與恢復流程寫進同一份可交接 runbook。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游 overview：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor page</a></li>
<li>服務責任：<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Source of Truth</a>、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">Database</a></li>
<li>恢復目標：<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a></li>
<li>證據交接：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
<li>官方文件：<a href="https://www.sqlite.org/wal.html">SQLite Write-Ahead Logging</a>、<a href="https://www.sqlite.org/backup.html">SQLite Backup API</a>、<a href="https://www.sqlite.org/howtocorrupt.html">How To Corrupt An SQLite Database File</a>、<a href="https://www.sqlite.org/recovery.html">Recovering Data From A Corrupt SQLite Database</a>、<a href="https://www.sqlite.org/whentouse.html">Appropriate Uses For SQLite</a>、<a href="https://www.sqlite.org/mostdeployed.html">Most Widely Deployed SQL Database Engine</a></li>
<li>延伸工具：<a href="https://litestream.io/reference/restore/">Litestream restore reference</a>、<a href="https://litestream.io/getting-started/">Litestream getting started</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Hands-on 操作路線</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/</guid><description>&lt;p>SQLite hands-on 操作路線的核心責任是把單檔正式狀態轉成可演練流程。這一層對齊 LLM &lt;code>hands-on/&lt;/code>：讀者能建立一個 SQLite 檔案、製造 WAL / lock 訊號、跑 backup / restore、套 migration，並知道何時該升級到 server SQL 或 edge SQLite。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>產出 artifact&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="local-file-quickstart/">Local file quickstart&lt;/a>&lt;/td>
 &lt;td>建立 &lt;code>.db&lt;/code>、schema、seed data、basic query&lt;/td>
 &lt;td>database file、schema version、query sample&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="backup-restore-drill/">Backup restore drill&lt;/a>&lt;/td>
 &lt;td>&lt;code>.backup&lt;/code> / &lt;code>VACUUM INTO&lt;/code> / restore validation&lt;/td>
 &lt;td>backup file、restore record、validation query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="wal-busy-reproduction/">WAL busy reproduction&lt;/a>&lt;/td>
 &lt;td>long transaction、&lt;code>SQLITE_BUSY&lt;/code>、checkpoint growth&lt;/td>
 &lt;td>busy error sample、WAL size evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="migration-fixture-lab/">Migration fixture lab&lt;/a>&lt;/td>
 &lt;td>&lt;code>user_version&lt;/code>、table rebuild、fixture snapshot&lt;/td>
 &lt;td>migration log、fixture DB、rollback note&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="d1-turso-preview-lab/">D1 / Turso preview lab&lt;/a>&lt;/td>
 &lt;td>local SQLite 到 edge SQLite product 的 compatibility preview&lt;/td>
 &lt;td>export / import note、compatibility gap&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>SQLite hands-on 章節要以檔案生命週期為中心。操作指令只在能產出 evidence 時出現；每篇都要回答 database file 在哪裡、sidecar file 如何處理、restore 如何驗證，以及 application release 如何知道它仍相容。&lt;/p>
&lt;h2 id="引用路徑">引用路徑&lt;/h2>
&lt;ul>
&lt;li>上游：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview&lt;/a>&lt;/li>
&lt;li>Structure：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/teaching-structure/" data-link-title="SQLite Teaching Structure" data-link-desc="SQLite 服務章節群的大綱：從 embedded formal state、WAL、backup、test fixture、local-first、edge SQLite 到遷移路由">SQLite Teaching Structure&lt;/a>&lt;/li>
&lt;li>Deep article：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>SQLite hands-on 操作路線的核心責任是把單檔正式狀態轉成可演練流程。這一層對齊 LLM <code>hands-on/</code>：讀者能建立一個 SQLite 檔案、製造 WAL / lock 訊號、跑 backup / restore、套 migration，並知道何時該升級到 server SQL 或 edge SQLite。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>產出 artifact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="local-file-quickstart/">Local file quickstart</a></td>
          <td>建立 <code>.db</code>、schema、seed data、basic query</td>
          <td>database file、schema version、query sample</td>
      </tr>
      <tr>
          <td><a href="backup-restore-drill/">Backup restore drill</a></td>
          <td><code>.backup</code> / <code>VACUUM INTO</code> / restore validation</td>
          <td>backup file、restore record、validation query</td>
      </tr>
      <tr>
          <td><a href="wal-busy-reproduction/">WAL busy reproduction</a></td>
          <td>long transaction、<code>SQLITE_BUSY</code>、checkpoint growth</td>
          <td>busy error sample、WAL size evidence</td>
      </tr>
      <tr>
          <td><a href="migration-fixture-lab/">Migration fixture lab</a></td>
          <td><code>user_version</code>、table rebuild、fixture snapshot</td>
          <td>migration log、fixture DB、rollback note</td>
      </tr>
      <tr>
          <td><a href="d1-turso-preview-lab/">D1 / Turso preview lab</a></td>
          <td>local SQLite 到 edge SQLite product 的 compatibility preview</td>
          <td>export / import note、compatibility gap</td>
      </tr>
  </tbody>
</table>
<h2 id="設計原則">設計原則</h2>
<p>SQLite hands-on 章節要以檔案生命週期為中心。操作指令只在能產出 evidence 時出現；每篇都要回答 database file 在哪裡、sidecar file 如何處理、restore 如何驗證，以及 application release 如何知道它仍相容。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>Structure：<a href="/blog/backend/01-database/vendors/sqlite/teaching-structure/" data-link-title="SQLite Teaching Structure" data-link-desc="SQLite 服務章節群的大綱：從 embedded formal state、WAL、backup、test fixture、local-first、edge SQLite 到遷移路由">SQLite Teaching Structure</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Litestream / LiteFS Replication</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/</guid><description>&lt;p>Litestream / LiteFS replication 的核心責任是把 SQLite 的 single-file operation 補成可恢復、可部署、可讀擴展的服務形狀。這類工具延伸 SQLite，但它們解決的問題不同：Litestream 主要把 WAL 變化持續送到 replica storage，強化 backup 與 restore；LiteFS 主要在 Fly.io 生態中透過 primary lease 與 filesystem layer 支援 replicated SQLite deployment。&lt;/p>
&lt;p>本文的判讀錨點是：replicated SQLite 要先說明 replica 的服務責任。它可能是 continuous backup、warm restore source、read replica、primary failover helper 或 deployment topology；每一種責任都有不同的 RPO、RTO、freshness 與 incident runbook。&lt;/p>
&lt;h2 id="replication-taxonomy">Replication Taxonomy&lt;/h2>
&lt;p>Replication taxonomy 的核心責任是把「有複本」拆成可操作的幾種能力。SQLite 周邊工具常用 replication 這個字，但 operator 需要知道它到底保護哪個風險。&lt;/p>
&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>Continuous backup&lt;/td>
 &lt;td>降低資料遺失窗口&lt;/td>
 &lt;td>replica lag、restore 成功&lt;/td>
 &lt;td>把 replica 當 active-active database&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read replica&lt;/td>
 &lt;td>降低 read latency / 壓力&lt;/td>
 &lt;td>freshness、read error rate&lt;/td>
 &lt;td>忽略 stale read&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm standby&lt;/td>
 &lt;td>縮短 restore / failover&lt;/td>
 &lt;td>promotion drill、DNS / routing&lt;/td>
 &lt;td>只備份檔案、未演練切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary lease&lt;/td>
 &lt;td>控制單一 writer ownership&lt;/td>
 &lt;td>writer lease、fencing log&lt;/td>
 &lt;td>多個 node 同時寫同一份邏輯狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consensus SQL&lt;/td>
 &lt;td>多節點一致性寫入&lt;/td>
 &lt;td>quorum、leader election&lt;/td>
 &lt;td>用 WAL shipping 取代 distributed OLTP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Continuous backup 的語言是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a>。它關心最近一次成功送出的 WAL、snapshot freshness、object storage credential、restore 指令與演練結果。&lt;/p>
&lt;p>Read replica 的語言是 freshness。Replica 能降低 read latency 或保護 primary workload，但讀者要知道 stale window、read-after-write policy、fallback to primary 與 cache invalidation。&lt;/p>
&lt;p>Primary lease 的語言是 writer ownership。SQLite 的服務形狀仍適合 single writer；工具可以協助 deployment 切換，但 application 要配合 fencing、retry 與 promotion evidence。&lt;/p>
&lt;h2 id="litestream-boundary">Litestream Boundary&lt;/h2>
&lt;p>Litestream boundary 的核心責任是把 SQLite WAL 變成可持續複製的 backup stream。Litestream 官方說明把它定位為 SQLite streaming replication tool，並在 &lt;a href="https://litestream.io/how-it-works/">How it works&lt;/a> 與 &lt;a href="https://litestream.io/reference/restore/">restore command&lt;/a> 文件中強調 replica 與 restore workflow。&lt;/p>
&lt;p>Litestream 適合下列情境：&lt;/p>
&lt;ol>
&lt;li>單節點 SQLite app 要降低資料遺失窗口。&lt;/li>
&lt;li>系統可接受 restore 後重新啟動 service。&lt;/li>
&lt;li>Object storage credential、retention、restore drill 可以被管理。&lt;/li>
&lt;li>Write pattern 適中，WAL stream 與 snapshot 維護成本可控。&lt;/li>
&lt;/ol>
&lt;p>Litestream 的設計重點是 backup evidence。Runbook 要記錄 replica destination、last replicated generation、last restore test、expected RPO、expected RTO、restore target path、credential rotation 與 corruption triage。&lt;/p></description><content:encoded><![CDATA[<p>Litestream / LiteFS replication 的核心責任是把 SQLite 的 single-file operation 補成可恢復、可部署、可讀擴展的服務形狀。這類工具延伸 SQLite，但它們解決的問題不同：Litestream 主要把 WAL 變化持續送到 replica storage，強化 backup 與 restore；LiteFS 主要在 Fly.io 生態中透過 primary lease 與 filesystem layer 支援 replicated SQLite deployment。</p>
<p>本文的判讀錨點是：replicated SQLite 要先說明 replica 的服務責任。它可能是 continuous backup、warm restore source、read replica、primary failover helper 或 deployment topology；每一種責任都有不同的 RPO、RTO、freshness 與 incident runbook。</p>
<h2 id="replication-taxonomy">Replication Taxonomy</h2>
<p>Replication taxonomy 的核心責任是把「有複本」拆成可操作的幾種能力。SQLite 周邊工具常用 replication 這個字，但 operator 需要知道它到底保護哪個風險。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>主要責任</th>
          <th>成功訊號</th>
          <th>常見誤判</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Continuous backup</td>
          <td>降低資料遺失窗口</td>
          <td>replica lag、restore 成功</td>
          <td>把 replica 當 active-active database</td>
      </tr>
      <tr>
          <td>Read replica</td>
          <td>降低 read latency / 壓力</td>
          <td>freshness、read error rate</td>
          <td>忽略 stale read</td>
      </tr>
      <tr>
          <td>Warm standby</td>
          <td>縮短 restore / failover</td>
          <td>promotion drill、DNS / routing</td>
          <td>只備份檔案、未演練切換</td>
      </tr>
      <tr>
          <td>Primary lease</td>
          <td>控制單一 writer ownership</td>
          <td>writer lease、fencing log</td>
          <td>多個 node 同時寫同一份邏輯狀態</td>
      </tr>
      <tr>
          <td>Consensus SQL</td>
          <td>多節點一致性寫入</td>
          <td>quorum、leader election</td>
          <td>用 WAL shipping 取代 distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>Continuous backup 的語言是 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 與 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>。它關心最近一次成功送出的 WAL、snapshot freshness、object storage credential、restore 指令與演練結果。</p>
<p>Read replica 的語言是 freshness。Replica 能降低 read latency 或保護 primary workload，但讀者要知道 stale window、read-after-write policy、fallback to primary 與 cache invalidation。</p>
<p>Primary lease 的語言是 writer ownership。SQLite 的服務形狀仍適合 single writer；工具可以協助 deployment 切換，但 application 要配合 fencing、retry 與 promotion evidence。</p>
<h2 id="litestream-boundary">Litestream Boundary</h2>
<p>Litestream boundary 的核心責任是把 SQLite WAL 變成可持續複製的 backup stream。Litestream 官方說明把它定位為 SQLite streaming replication tool，並在 <a href="https://litestream.io/how-it-works/">How it works</a> 與 <a href="https://litestream.io/reference/restore/">restore command</a> 文件中強調 replica 與 restore workflow。</p>
<p>Litestream 適合下列情境：</p>
<ol>
<li>單節點 SQLite app 要降低資料遺失窗口。</li>
<li>系統可接受 restore 後重新啟動 service。</li>
<li>Object storage credential、retention、restore drill 可以被管理。</li>
<li>Write pattern 適中，WAL stream 與 snapshot 維護成本可控。</li>
</ol>
<p>Litestream 的設計重點是 backup evidence。Runbook 要記錄 replica destination、last replicated generation、last restore test、expected RPO、expected RTO、restore target path、credential rotation 與 corruption triage。</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">litestream restore -o /var/lib/app/restored.db s3://example-bucket/app.db
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 /var/lib/app/restored.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令是 restore drill 的最小骨架。正式 runbook 要補上 service stop、database path、sidecar file、permission、checksum、application smoke test 與 rollback decision。</p>
<p>Litestream 的風險集中在 restore path。備份存在和服務可恢復是兩件事；每次 release 或 schema migration 後，都應用 staging data 跑一次 restore、integrity check、row count 與 application smoke test。</p>
<h2 id="litefs-boundary">LiteFS Boundary</h2>
<p>LiteFS boundary 的核心責任是支援 replicated deployment topology，而非只做 backup。LiteFS 在 Fly.io 文件中被定位為 SQLite replication layer，透過 FUSE filesystem 與 primary lease 模型協助應用在多個 instance 間運作。</p>
<p>LiteFS 適合下列情境：</p>
<ol>
<li>App 仍希望使用 SQLite file 與 local SQL path。</li>
<li>Deployment 有多個 instance，但 write authority 可以集中到 primary。</li>
<li>Read replica freshness 可以被產品接受。</li>
<li>Team 願意把 filesystem layer、primary lease、promotion 與 platform operation 納入 runbook。</li>
</ol>
<p>LiteFS 的設計重點是 primary ownership。Application 要知道 write request 到哪裡執行、primary 切換時如何重試、read replica 讀到舊資料時如何回應，以及 promotion 完成前哪些 endpoint 要進入 degraded mode。</p>
<p>LiteFS 的 incident route 要從 writer ownership 開始查。若出現 write error、stale read 或 suspected split brain，先查看 primary lease、instance health、replication lag、pending writes 與 platform network，再處理 application retry。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 replicated SQLite 的事故從「資料庫壞了」拆成可排查訊號。SQLite file、WAL、object storage、filesystem layer、deployment platform 與 application retry 都可能是問題來源。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>立即處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replica lag</td>
          <td>last replicated time 落後</td>
          <td>降低 write rate、檢查 credential / network</td>
      </tr>
      <tr>
          <td>Restore lag</td>
          <td>WAL files 過多、restore time 變長</td>
          <td>觸發 snapshot、演練 restore</td>
      </tr>
      <tr>
          <td>Stale read</td>
          <td>使用者讀到舊資料</td>
          <td>fallback primary read、標記 freshness</td>
      </tr>
      <tr>
          <td>Writer lease confusion</td>
          <td>多 instance write error</td>
          <td>暫停寫入、確認 primary、fencing old writer</td>
      </tr>
      <tr>
          <td>Object storage failure</td>
          <td>backup upload error</td>
          <td>切換 credential / destination、補上重送</td>
      </tr>
      <tr>
          <td>Sidecar file mismatch</td>
          <td>restore / copy 後 integrity fail</td>
          <td>回到 backup API / official restore path</td>
      </tr>
  </tbody>
</table>
<p>Replica lag 要接到 alert。對 Litestream，它意味著 RPO 正在擴大；對 LiteFS，它可能同時影響 read freshness 與 failover confidence。</p>
<p>Restore lag 要接到 release gate。若 restore time 已超過目標 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>，就要調整 snapshot frequency、資料保留策略或搬到 server database。</p>
<p>Stale read 要接到產品語言。使用者看到舊資料時，系統可以顯示 sync state、重讀 primary、限制 critical action 或提供 refresh；這些策略要在設計階段決定。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是避免把 replicated SQLite 推到 distributed OLTP 的位置。SQLite 周邊 replication 工具可以強化單節點與 read replica，但高寫入、多 writer、強一致跨 region transaction 需要不同資料庫模型。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>原因</th>
          <th>路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 region 都要接受交易性寫入</td>
          <td>single writer / primary lease 壓力過高</td>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a> 或 Spanner</td>
      </tr>
      <tr>
          <td>每秒大量 concurrent writer</td>
          <td>lock contention 與 replica lag 擴大</td>
          <td>PostgreSQL / MySQL / managed OLTP</td>
      </tr>
      <tr>
          <td>Central audit / DB role 是硬需求</td>
          <td>SQLite file model 缺少 server role</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Restore drill 經常超過 RTO</td>
          <td>file size / WAL backlog 已超界</td>
          <td>server DB、sharding 或資料生命週期重整</td>
      </tr>
      <tr>
          <td>Incident team 缺少 filesystem layer 維護能力</td>
          <td>operation model 超過組織能力</td>
          <td>managed SQL 或 D1 / Turso managed path</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要在 design review 階段列出。SQLite replication 的好處是低成本與低元件數；當核心需求變成跨節點一致性寫入，繼續調工具會把風險藏在 incident 時刻。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把資料保護、讀擴展與高可用分開選型。Litestream / LiteFS 位置清楚時，SQLite 可以保持簡潔；位置混淆時，系統會同時缺 backup evidence 與 transaction guarantee。</p>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單節點 SQLite 需要 continuous backup</td>
          <td>Litestream + restore drill</td>
      </tr>
      <tr>
          <td>多 instance deployment 需要 primary lease</td>
          <td>LiteFS + write routing / promotion runbook</td>
      </tr>
      <tr>
          <td>Edge app 需要 managed SQL-like platform</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a></td>
      </tr>
      <tr>
          <td>多 tenant OLTP 需要 central operation</td>
          <td>PostgreSQL / MySQL / Aurora</td>
      </tr>
      <tr>
          <td>Global transaction 是核心需求</td>
          <td>Distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>選擇 Litestream 時，完成標準是能在 staging 從 replica restore 出可用 DB。選擇 LiteFS 時，完成標準是能演練 primary 切換、read freshness、write retry 與 degraded mode。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Litestream / LiteFS replication 完成後，下一步要回到 SQLite operation evidence。File copy、backup API 與 WAL sidecar 請讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>；busy、lock 與 writer 壓力請讀 <a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a>；完整 runbook 請讀 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">SQLite observability / runbook</a>。</p>
]]></content:encoded></item><item><title>SQLite Local File Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/</guid><description>&lt;p>SQLite local file quickstart 的核心責任是建立後續 backup、WAL、migration 與 fixture lab 共用的 database file。這個 lab 把 SQLite 從抽象服務選型轉成可觀察的檔案、schema、PRAGMA、transaction 與 sidecar artifact。&lt;/p>
&lt;p>本文的驗收標準是：你能建立一個可重建的 &lt;code>app.db&lt;/code>，知道它的 schema version、journal mode、foreign key 設定、seed data 與 cleanup 路徑。&lt;/p>
&lt;h2 id="lab-directory">Lab Directory&lt;/h2>
&lt;p>Lab directory 的核心責任是把 SQLite artifact 放在隔離資料夾，避免和正式檔案混淆。以下命令建立一個可刪除的本地工作區。&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">mkdir -p /tmp/sqlite-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/sqlite-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">rm -f app.db app.db-wal app.db-shm&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>驗收 artifact 是 &lt;code>/tmp/sqlite-lab/app.db&lt;/code>。後續 lab 可以沿用這個路徑，也可以每次從頭建立。&lt;/p>
&lt;h2 id="baseline-schema">Baseline Schema&lt;/h2>
&lt;p>Baseline schema 的核心責任是建立一組能測 transaction、constraint、index 與 query 的小型資料模型。這裡使用 &lt;code>accounts&lt;/code> 與 &lt;code>ledger_entries&lt;/code>，因為它們能清楚展示 foreign key 與金額 invariant。&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">sqlite3 app.db &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA journal_mode = WAL;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA foreign_keys = ON;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA user_version = 1;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE accounts (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> id INTEGER PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> owner_name TEXT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> status TEXT NOT NULL CHECK (status IN (&amp;#39;active&amp;#39;, &amp;#39;closed&amp;#39;)),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TEXT NOT NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">) STRICT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE ledger_entries (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> id INTEGER PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> account_id INTEGER NOT NULL REFERENCES accounts(id),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> amount_cents INTEGER NOT NULL CHECK (amount_cents != 0),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> idempotency_key TEXT NOT NULL UNIQUE,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TEXT NOT NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s">) STRICT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE INDEX idx_ledger_entries_account_created
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="s">ON ledger_entries(account_id, created_at);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 schema 的重點是明確資料合約。&lt;code>STRICT&lt;/code>、&lt;code>CHECK&lt;/code>、&lt;code>FOREIGN KEY&lt;/code> 與 &lt;code>UNIQUE&lt;/code> 讓 fixture 更接近正式資料責任，也讓後續 migration lab 有可驗證的 invariant。&lt;/p>
&lt;h2 id="seed-data">Seed Data&lt;/h2>
&lt;p>Seed data 的核心責任是建立可重跑的測試資料。每筆 ledger entry 都有 idempotency key，讓後續 edge / retry 設計可以沿用。&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">sqlite3 app.db &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA foreign_keys = ON;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s">BEGIN;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO accounts(id, owner_name, status, created_at)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">VALUES
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> (1, &amp;#39;Ada&amp;#39;, &amp;#39;active&amp;#39;, &amp;#39;2026-05-21T00:00:00Z&amp;#39;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> (2, &amp;#39;Lin&amp;#39;, &amp;#39;active&amp;#39;, &amp;#39;2026-05-21T00:05:00Z&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">VALUES
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> (1, 1200, &amp;#39;seed-ada-credit-1&amp;#39;, &amp;#39;2026-05-21T00:10:00Z&amp;#39;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> (1, -200, &amp;#39;seed-ada-debit-1&amp;#39;, &amp;#39;2026-05-21T00:12:00Z&amp;#39;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> (2, 900, &amp;#39;seed-lin-credit-1&amp;#39;, &amp;#39;2026-05-21T00:15:00Z&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s">COMMIT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Seed 完成後先跑基本查詢。這一步確認 schema、constraint 與 index 入口都可用。&lt;/p></description><content:encoded><![CDATA[<p>SQLite local file quickstart 的核心責任是建立後續 backup、WAL、migration 與 fixture lab 共用的 database file。這個 lab 把 SQLite 從抽象服務選型轉成可觀察的檔案、schema、PRAGMA、transaction 與 sidecar artifact。</p>
<p>本文的驗收標準是：你能建立一個可重建的 <code>app.db</code>，知道它的 schema version、journal mode、foreign key 設定、seed data 與 cleanup 路徑。</p>
<h2 id="lab-directory">Lab Directory</h2>
<p>Lab directory 的核心責任是把 SQLite artifact 放在隔離資料夾，避免和正式檔案混淆。以下命令建立一個可刪除的本地工作區。</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">mkdir -p /tmp/sqlite-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /tmp/sqlite-lab
</span></span><span class="line"><span class="ln">3</span><span class="cl">rm -f app.db app.db-wal app.db-shm</span></span></code></pre></div><p>驗收 artifact 是 <code>/tmp/sqlite-lab/app.db</code>。後續 lab 可以沿用這個路徑，也可以每次從頭建立。</p>
<h2 id="baseline-schema">Baseline Schema</h2>
<p>Baseline schema 的核心責任是建立一組能測 transaction、constraint、index 與 query 的小型資料模型。這裡使用 <code>accounts</code> 與 <code>ledger_entries</code>，因為它們能清楚展示 foreign key 與金額 invariant。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">PRAGMA journal_mode = WAL;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">PRAGMA user_version = 1;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">CREATE TABLE accounts (
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  id INTEGER PRIMARY KEY,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  owner_name TEXT NOT NULL,
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  status TEXT NOT NULL CHECK (status IN (&#39;active&#39;, &#39;closed&#39;)),
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  created_at TEXT NOT NULL
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">) STRICT;
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">CREATE TABLE ledger_entries (
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  id INTEGER PRIMARY KEY,
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  account_id INTEGER NOT NULL REFERENCES accounts(id),
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  amount_cents INTEGER NOT NULL CHECK (amount_cents != 0),
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  idempotency_key TEXT NOT NULL UNIQUE,
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  created_at TEXT NOT NULL
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">) STRICT;
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">CREATE INDEX idx_ledger_entries_account_created
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">ON ledger_entries(account_id, created_at);
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>這段 schema 的重點是明確資料合約。<code>STRICT</code>、<code>CHECK</code>、<code>FOREIGN KEY</code> 與 <code>UNIQUE</code> 讓 fixture 更接近正式資料責任，也讓後續 migration lab 有可驗證的 invariant。</p>
<h2 id="seed-data">Seed Data</h2>
<p>Seed data 的核心責任是建立可重跑的測試資料。每筆 ledger entry 都有 idempotency key，讓後續 edge / retry 設計可以沿用。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">BEGIN;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">INSERT INTO accounts(id, owner_name, status, created_at)
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">VALUES
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  (1, &#39;Ada&#39;, &#39;active&#39;, &#39;2026-05-21T00:00:00Z&#39;),
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  (2, &#39;Lin&#39;, &#39;active&#39;, &#39;2026-05-21T00:05:00Z&#39;);
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at)
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">VALUES
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  (1, 1200, &#39;seed-ada-credit-1&#39;, &#39;2026-05-21T00:10:00Z&#39;),
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  (1, -200, &#39;seed-ada-debit-1&#39;, &#39;2026-05-21T00:12:00Z&#39;),
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  (2, 900, &#39;seed-lin-credit-1&#39;, &#39;2026-05-21T00:15:00Z&#39;);
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">COMMIT;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Seed 完成後先跑基本查詢。這一步確認 schema、constraint 與 index 入口都可用。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">.headers on
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">.mode column
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SELECT a.id, a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">FROM accounts a
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">GROUP BY a.id, a.owner_name
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">ORDER BY a.id;
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>預期輸出應顯示 Ada 餘額 <code>1000</code>，Lin 餘額 <code>900</code>。</p>
<h2 id="pragma-snapshot">PRAGMA Snapshot</h2>
<p>PRAGMA snapshot 的核心責任是把連線設定變成 evidence。SQLite 的部分設定與 connection 有關，因此 lab 要明確查出當前狀態。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">.headers on
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">.mode column
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">PRAGMA journal_mode;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">PRAGMA foreign_keys;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">PRAGMA user_version;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">PRAGMA integrity_check;
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>驗收重點如下：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>期望結果</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>journal_mode</code></td>
          <td><code>wal</code></td>
          <td>後續可觀察 <code>-wal</code> sidecar</td>
      </tr>
      <tr>
          <td><code>foreign_keys</code></td>
          <td><code>1</code></td>
          <td>constraint 在連線上已啟用</td>
      </tr>
      <tr>
          <td><code>user_version</code></td>
          <td><code>1</code></td>
          <td>migration 起點清楚</td>
      </tr>
      <tr>
          <td>integrity</td>
          <td><code>ok</code></td>
          <td>database file 基本健康</td>
      </tr>
  </tbody>
</table>
<h2 id="transaction-sample">Transaction Sample</h2>
<p>Transaction sample 的核心責任是建立後續 busy / migration lab 的共同語言。SQLite transaction 成功時要同時更新資料與保護 invariant。</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">sqlite3 app.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">BEGIN IMMEDIATE;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at)
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">VALUES (1, 300, &#39;manual-ada-credit-1&#39;, &#39;2026-05-21T00:20:00Z&#39;);
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">COMMIT;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p><code>BEGIN IMMEDIATE</code> 會提早取得 write lock。這讓後續 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a> 可以直接展示 single writer boundary。</p>
<h2 id="file-artifact-check">File Artifact Check</h2>
<p>File artifact check 的核心責任是讓讀者看到 SQLite 由 <code>.db</code> 與可能存在的 sidecar 共同構成。WAL mode 可能建立 <code>-wal</code> 與 <code>-shm</code> sidecar，backup / copy / restore runbook 要理解這些檔案。</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">ls -lh app.db app.db-wal app.db-shm</span></span></code></pre></div><p>若 sidecar 暫時未出現，可以再寫入一筆資料或保持連線開啟。Sidecar 是否存在取決於 WAL 狀態、checkpoint 與 connection lifecycle。</p>
<h2 id="cleanup">Cleanup</h2>
<p>Cleanup 的核心責任是讓 lab 可以重跑。若要重新開始，刪除 database 與 sidecar。</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">rm -f /tmp/sqlite-lab/app.db /tmp/sqlite-lab/app.db-wal /tmp/sqlite-lab/app.db-shm</span></span></code></pre></div><p>完成本篇後，下一步可以進入 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/" data-link-title="SQLite Backup Restore Drill" data-link-desc="SQLite .backup、VACUUM INTO、restore validation、sidecar file handling 與 RPO / RTO note 的操作說明">backup restore drill</a> 或 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>。</p>
]]></content:encoded></item><item><title>SQLite Local-first Sync Boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 local-first / offline-first 場景；本文聚焦 &lt;em>SQLite local store 與 multi-device sync protocol 的責任分界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite local-first sync boundary 的核心責任是把「本機可用」和「多端一致」分成兩個問題。SQLite 很適合保存 device-local state；但它不提供 identity、transport、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/conflict-resolution/" data-link-title="Conflict Resolution" data-link-desc="說明並發或離線寫入產生衝突時，如何偵測、呈現與合併成可接受狀態">conflict resolution&lt;/a>、delete propagation、server authority 或 audit trail。當資料要跨裝置、跨使用者或跨服務同步時，SQLite 只是 local replica / working copy。&lt;/p>
&lt;p>本文的判讀錨點是：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-first/" data-link-title="Local-First" data-link-desc="說明本機優先的資料架構如何讓離線可用，並把同步當成獨立問題">local-first&lt;/a> 的產品價值來自離線可用，工程成本來自同步語意。SQLite 解的是 local durability；sync layer 解的是資料合併、順序、權威來源與錯誤修復。&lt;/p>
&lt;h2 id="local-state-taxonomy">Local state taxonomy&lt;/h2>
&lt;p>Local-first 設計的第一步是標記本機資料角色。不同資料角色對 sync、backup、conflict 與 delete 的要求不同。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料角色&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>Sync 語意&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Local cache&lt;/td>
 &lt;td>API response cache、thumbnail metadata&lt;/td>
 &lt;td>可清除、可重抓&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Draft / working copy&lt;/td>
 &lt;td>草稿、離線表單、未送出 action&lt;/td>
 &lt;td>需要 upload / retry / conflict handling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local source of truth&lt;/td>
 &lt;td>單裝置日記、CLI state&lt;/td>
 &lt;td>需要 backup / export，可能不需要 server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local replica&lt;/td>
 &lt;td>server record 的本地副本&lt;/td>
 &lt;td>server authority、stale read、sync lag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sync queue&lt;/td>
 &lt;td>pending mutation / event log&lt;/td>
 &lt;td>ordering、idempotency、replay&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是資料角色先於 sync 工具。若所有資料都只是 cache，SQLite + TTL 足夠；若有 pending mutation 或 multi-device edit，就需要 sync protocol。&lt;/p>
&lt;h2 id="authority-boundary">Authority boundary&lt;/h2>
&lt;p>Authority boundary 的核心責任是決定衝突時誰說了算。Local-first app 可以讓 device、server、field-level merge 或 CRDT 成為不同層的 authority；SQLite 本身只保存狀態，不替系統決策。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Authority model&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Server authority&lt;/td>
 &lt;td>帳務、權限、共享資料&lt;/td>
 &lt;td>離線寫入要排隊，回線後可能被拒絕&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Device authority&lt;/td>
 &lt;td>單使用者、單裝置資料&lt;/td>
 &lt;td>多裝置同步能力弱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Last-write-wins&lt;/td>
 &lt;td>低價值設定、簡單 preference&lt;/td>
 &lt;td>資料覆蓋風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Field merge&lt;/td>
 &lt;td>profile、表單、可分欄位資料&lt;/td>
 &lt;td>merge rule 要測，使用者理解成本上升&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CRDT / operation log&lt;/td>
 &lt;td>協作編輯、順序敏感操作&lt;/td>
 &lt;td>實作與除錯成本高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Authority model 要和 product semantics 對齊。庫存、付款、權限這類資料通常需要 server authority；notes、draft、local settings 可以接受更偏 local 的權威模型。&lt;/p>
&lt;h2 id="sync-transport-與-local-log">Sync transport 與 local log&lt;/h2>
&lt;p>Sync transport 的核心責任是把 SQLite local state 轉成可重送、可去重、可驗證的資料流。最常見做法是本地維護 pending mutation table 或 change log，再由 background sync worker 送到 server。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 local-first / offline-first 場景；本文聚焦 <em>SQLite local store 與 multi-device sync protocol 的責任分界</em>。</p></blockquote>
<p>SQLite local-first sync boundary 的核心責任是把「本機可用」和「多端一致」分成兩個問題。SQLite 很適合保存 device-local state；但它不提供 identity、transport、<a href="/blog/backend/knowledge-cards/conflict-resolution/" data-link-title="Conflict Resolution" data-link-desc="說明並發或離線寫入產生衝突時，如何偵測、呈現與合併成可接受狀態">conflict resolution</a>、delete propagation、server authority 或 audit trail。當資料要跨裝置、跨使用者或跨服務同步時，SQLite 只是 local replica / working copy。</p>
<p>本文的判讀錨點是：<a href="/blog/backend/knowledge-cards/local-first/" data-link-title="Local-First" data-link-desc="說明本機優先的資料架構如何讓離線可用，並把同步當成獨立問題">local-first</a> 的產品價值來自離線可用，工程成本來自同步語意。SQLite 解的是 local durability；sync layer 解的是資料合併、順序、權威來源與錯誤修復。</p>
<h2 id="local-state-taxonomy">Local state taxonomy</h2>
<p>Local-first 設計的第一步是標記本機資料角色。不同資料角色對 sync、backup、conflict 與 delete 的要求不同。</p>
<table>
  <thead>
      <tr>
          <th>資料角色</th>
          <th>例子</th>
          <th>Sync 語意</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local cache</td>
          <td>API response cache、thumbnail metadata</td>
          <td>可清除、可重抓</td>
      </tr>
      <tr>
          <td>Draft / working copy</td>
          <td>草稿、離線表單、未送出 action</td>
          <td>需要 upload / retry / conflict handling</td>
      </tr>
      <tr>
          <td>Local source of truth</td>
          <td>單裝置日記、CLI state</td>
          <td>需要 backup / export，可能不需要 server</td>
      </tr>
      <tr>
          <td>Local replica</td>
          <td>server record 的本地副本</td>
          <td>server authority、stale read、sync lag</td>
      </tr>
      <tr>
          <td>Sync queue</td>
          <td>pending mutation / event log</td>
          <td>ordering、idempotency、replay</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是資料角色先於 sync 工具。若所有資料都只是 cache，SQLite + TTL 足夠；若有 pending mutation 或 multi-device edit，就需要 sync protocol。</p>
<h2 id="authority-boundary">Authority boundary</h2>
<p>Authority boundary 的核心責任是決定衝突時誰說了算。Local-first app 可以讓 device、server、field-level merge 或 CRDT 成為不同層的 authority；SQLite 本身只保存狀態，不替系統決策。</p>
<table>
  <thead>
      <tr>
          <th>Authority model</th>
          <th>適合情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Server authority</td>
          <td>帳務、權限、共享資料</td>
          <td>離線寫入要排隊，回線後可能被拒絕</td>
      </tr>
      <tr>
          <td>Device authority</td>
          <td>單使用者、單裝置資料</td>
          <td>多裝置同步能力弱</td>
      </tr>
      <tr>
          <td>Last-write-wins</td>
          <td>低價值設定、簡單 preference</td>
          <td>資料覆蓋風險</td>
      </tr>
      <tr>
          <td>Field merge</td>
          <td>profile、表單、可分欄位資料</td>
          <td>merge rule 要測，使用者理解成本上升</td>
      </tr>
      <tr>
          <td>CRDT / operation log</td>
          <td>協作編輯、順序敏感操作</td>
          <td>實作與除錯成本高</td>
      </tr>
  </tbody>
</table>
<p>Authority model 要和 product semantics 對齊。庫存、付款、權限這類資料通常需要 server authority；notes、draft、local settings 可以接受更偏 local 的權威模型。</p>
<h2 id="sync-transport-與-local-log">Sync transport 與 local log</h2>
<p>Sync transport 的核心責任是把 SQLite local state 轉成可重送、可去重、可驗證的資料流。最常見做法是本地維護 pending mutation table 或 change log，再由 background sync worker 送到 server。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">pending_mutations</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">entity_type</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">entity_id</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="k">operation</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="n">payload</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="n">retry_count</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="n">last_error</span><span class="w"> </span><span class="nb">TEXT</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設計點</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idempotency</td>
          <td>每個 mutation 需要穩定 id，避免重送副作用</td>
      </tr>
      <tr>
          <td>ordering</td>
          <td>同 entity 操作是否必須按順序</td>
      </tr>
      <tr>
          <td>retry</td>
          <td>transient failure、backoff、dead-letter</td>
      </tr>
      <tr>
          <td>compaction</td>
          <td>已同步 local log 何時清除</td>
      </tr>
      <tr>
          <td>reconciliation</td>
          <td>server / local 差異如何修復</td>
      </tr>
  </tbody>
</table>
<p>這裡和 backend queue 概念相通：pending mutation table 是本機版 durable queue。它需要 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、retry 與 replay 思維，而不只是「存一張表」。</p>
<h2 id="conflict-resolution">Conflict resolution</h2>
<p>Conflict resolution 的核心責任是讓兩個合法 local write 合併成可接受狀態。SQLite 可以保存 local write；sync layer 要決定衝突偵測、呈現與合併。</p>
<table>
  <thead>
      <tr>
          <th>衝突型態</th>
          <th>例子</th>
          <th>處理策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Same field update</td>
          <td>兩台裝置改同一個 display name</td>
          <td>LWW、server reject、manual merge</td>
      </tr>
      <tr>
          <td>Disjoint field update</td>
          <td>一台改 phone，一台改 address</td>
          <td>field merge</td>
      </tr>
      <tr>
          <td>Delete vs update</td>
          <td>一台刪除，一台修改</td>
          <td>tombstone、manual review</td>
      </tr>
      <tr>
          <td>Ordered operation</td>
          <td>task reorder、ledger append</td>
          <td>operation log、server sequence</td>
      </tr>
  </tbody>
</table>
<p>Conflict policy 要在資料模型設計時決定。等衝突發生後才補策略，通常會導致資料修復、客服流程與 audit evidence 同時缺位。</p>
<h2 id="delete-propagation-與-privacy">Delete propagation 與 privacy</h2>
<p>Delete propagation 的核心責任是讓 server、device、backup 與 sync queue 對「刪除」有一致語意。Local-first app 常見風險是 server 已刪，但 device local DB、pending queue 或 OS backup 還留著資料。</p>
<table>
  <thead>
      <tr>
          <th>刪除語意</th>
          <th>適合情境</th>
          <th>SQLite 設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Soft delete</td>
          <td>可恢復、需要 sync tombstone</td>
          <td><code>deleted_at</code>、sync tombstone、retention job</td>
      </tr>
      <tr>
          <td>Hard delete</td>
          <td>privacy / compliance</td>
          <td>local purge、backup exclusion、sync confirmation</td>
      </tr>
      <tr>
          <td>Redaction</td>
          <td>support bundle / log</td>
          <td>export 時遮罩 sensitive fields</td>
      </tr>
  </tbody>
</table>
<p>刪除在同步系統裡是一個跨裝置生命週期。若資料跨裝置同步，delete 需要 <a href="/blog/backend/knowledge-cards/tombstone/" data-link-title="Tombstone" data-link-desc="說明刪除如何用一筆標記記錄下來，讓刪除事件能跨副本與裝置傳播">tombstone</a>、ack、retry、backup retention 與 evidence；這些責任要接到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1pending-mutation-沒有-idempotency-key">Case 1：pending mutation 沒有 idempotency key</h3>
<p>Pending mutation 沒有 idempotency key 的核心風險是重送造成重複副作用。網路 timeout 後 worker 重送，server 已經處理第一次請求，第二次又建立一筆資料或扣一次庫存。</p>
<p>修正方向是每個 mutation 生成 stable id，server 以 idempotency key 去重，local SQLite 保存 retry state 與 server ack。</p>
<h3 id="case-2lww-覆蓋使用者資料">Case 2：LWW 覆蓋使用者資料</h3>
<p>Last-write-wins 的核心風險是把衝突靜默變成資料遺失。Preference 類資料可接受；草稿、文件、表單、付款資料通常需要更清楚的 conflict handling。</p>
<p>修正方向是依資料價值分層。低價值設定用 LWW；高價值內容用 field merge、manual conflict 或 operation log。</p>
<h3 id="case-3delete-沒傳到離線裝置">Case 3：delete 沒傳到離線裝置</h3>
<p>Delete propagation 失敗的核心風險是 privacy / compliance 失效。使用者刪除 server 資料後，一台長期離線裝置重新上線又把舊資料同步回來。</p>
<p>修正方向是 tombstone + server authority。Server 要能拒絕過期 mutation，device 要能接收 delete tombstone 並 purge local state。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>Local-first SQLite 設計要回答：</p>
<ol>
<li>哪些 table 是 local source of truth，哪些是 server replica。</li>
<li>Pending mutation 是否有 idempotency key 與 retry state。</li>
<li>Conflict policy 是 LWW、field merge、manual merge 還是 operation log。</li>
<li>Delete 是否有 tombstone、ack 與 local purge。</li>
<li>Sync worker 是否有 backoff、dead-letter、reconciliation。</li>
<li>Device backup 是否會保存已刪資料。</li>
<li>Server 是否能拒絕過期 local write。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / Desktop Embedded Store</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso</a>、<a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL Comparison</a></li>
<li>卡片：<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a>、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">Eventual Consistency</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Migration Fixture Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/</guid><description>&lt;p>SQLite migration fixture lab 的核心責任是把 schema migration 與 test fixture 放進同一個可重建流程。這篇承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice&lt;/a>，讓 migration 有版本、snapshot、validation 與 rollback note。&lt;/p>
&lt;p>本文的驗收標準是：你能建立 v1 fixture、套用 v2 migration、產生 v2 snapshot，並用 validation query 證明資料合約仍成立。&lt;/p>
&lt;h2 id="create-fixture">Create Fixture&lt;/h2>
&lt;p>Create fixture 的核心責任是建立乾淨、可重建的 source fixture。沿用 quickstart schema，或重新建立一份 fixture DB。&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">mkdir -p /tmp/sqlite-fixture-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/sqlite-fixture-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">rm -f fixture-v1.db fixture-v2.db
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">sqlite3 fixture-v1.db &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA foreign_keys = ON;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s">PRAGMA user_version = 1;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE accounts (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> id INTEGER PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s"> owner_name TEXT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s"> status TEXT NOT NULL CHECK (status IN (&amp;#39;active&amp;#39;, &amp;#39;closed&amp;#39;)),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TEXT NOT NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s">) STRICT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE ledger_entries (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> id INTEGER PRIMARY KEY,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> account_id INTEGER NOT NULL REFERENCES accounts(id),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> amount_cents INTEGER NOT NULL CHECK (amount_cents != 0),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> idempotency_key TEXT NOT NULL UNIQUE,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TEXT NOT NULL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">) STRICT;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO accounts VALUES (1, &amp;#39;Ada&amp;#39;, &amp;#39;active&amp;#39;, &amp;#39;2026-05-21T00:00:00Z&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="s">VALUES (1, 1000, &amp;#39;fixture-v1-ada&amp;#39;, &amp;#39;2026-05-21T00:10:00Z&amp;#39;);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 fixture 是 v1 source of truth。CI 可以每次從 SQL 重建，也可以保存 &lt;code>fixture-v1.db&lt;/code> 作為 binary fixture；兩者都要有版本與 checksum。&lt;/p></description><content:encoded><![CDATA[<p>SQLite migration fixture lab 的核心責任是把 schema migration 與 test fixture 放進同一個可重建流程。這篇承接 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice</a>，讓 migration 有版本、snapshot、validation 與 rollback note。</p>
<p>本文的驗收標準是：你能建立 v1 fixture、套用 v2 migration、產生 v2 snapshot，並用 validation query 證明資料合約仍成立。</p>
<h2 id="create-fixture">Create Fixture</h2>
<p>Create fixture 的核心責任是建立乾淨、可重建的 source fixture。沿用 quickstart schema，或重新建立一份 fixture DB。</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">mkdir -p /tmp/sqlite-fixture-lab
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">cd</span> /tmp/sqlite-fixture-lab
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">rm -f fixture-v1.db fixture-v2.db
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">sqlite3 fixture-v1.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">PRAGMA user_version = 1;
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">CREATE TABLE accounts (
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  id INTEGER PRIMARY KEY,
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  owner_name TEXT NOT NULL,
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">  status TEXT NOT NULL CHECK (status IN (&#39;active&#39;, &#39;closed&#39;)),
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  created_at TEXT NOT NULL
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">) STRICT;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">CREATE TABLE ledger_entries (
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  id INTEGER PRIMARY KEY,
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  account_id INTEGER NOT NULL REFERENCES accounts(id),
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  amount_cents INTEGER NOT NULL CHECK (amount_cents != 0),
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  idempotency_key TEXT NOT NULL UNIQUE,
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">  created_at TEXT NOT NULL
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">) STRICT;
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">INSERT INTO accounts VALUES (1, &#39;Ada&#39;, &#39;active&#39;, &#39;2026-05-21T00:00:00Z&#39;);
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at)
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">VALUES (1, 1000, &#39;fixture-v1-ada&#39;, &#39;2026-05-21T00:10:00Z&#39;);
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>這個 fixture 是 v1 source of truth。CI 可以每次從 SQL 重建，也可以保存 <code>fixture-v1.db</code> 作為 binary fixture；兩者都要有版本與 checksum。</p>
<h2 id="pre-migration-snapshot">Pre-Migration Snapshot</h2>
<p>Pre-migration snapshot 的核心責任是建立 rollback 起點。正式 migration 前應先保存 source DB。</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">sqlite3 fixture-v1.db <span class="s2">&#34;.backup &#39;fixture-v1-before-migration.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 fixture-v1-before-migration.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這份 snapshot 代表 migration 失敗時的回退點。CI log 要保留 snapshot path、schema version 與 migration id。</p>
<h2 id="apply-add-column-migration">Apply Add Column Migration</h2>
<p>Apply add column migration 的核心責任是展示低風險 schema change。先複製 v1，再套用 v2。</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">cp fixture-v1.db fixture-v2.db
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 fixture-v2.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">BEGIN;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">ALTER TABLE accounts ADD COLUMN email TEXT;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">PRAGMA user_version = 2;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">COMMIT;
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>驗證 schema version 與新欄位：</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">sqlite3 fixture-v2.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">PRAGMA user_version;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">PRAGMA table_info(accounts);
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Add column 是較簡單的 migration。涉及 drop column、rename、constraint 重建或資料 reshape 時，應改用 table rebuild 策略。</p>
<h2 id="table-rebuild-example">Table Rebuild Example</h2>
<p>Table rebuild 的核心責任是展示 SQLite schema migration 的高風險路徑。以下範例把 <code>accounts.status</code> 的 allowed value 加入 <code>suspended</code>，透過新表重建 constraint。</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">sqlite3 fixture-v2.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">PRAGMA foreign_keys = OFF;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">BEGIN;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">CREATE TABLE accounts_new (
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  id INTEGER PRIMARY KEY,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  owner_name TEXT NOT NULL,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  status TEXT NOT NULL CHECK (status IN (&#39;active&#39;, &#39;closed&#39;, &#39;suspended&#39;)),
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  created_at TEXT NOT NULL,
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  email TEXT
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">) STRICT;
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">INSERT INTO accounts_new(id, owner_name, status, created_at, email)
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">SELECT id, owner_name, status, created_at, email
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">FROM accounts;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">DROP TABLE accounts;
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">ALTER TABLE accounts_new RENAME TO accounts;
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">PRAGMA user_version = 3;
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">COMMIT;
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">PRAGMA foreign_keys = ON;
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Table rebuild 要保存 index、trigger、view 與 FK reference。這個 lab 只有小型 schema；正式 migration 要先列出所有 dependent object。</p>
<h2 id="validation-query">Validation Query</h2>
<p>Validation query 的核心責任是證明 migration 後資料仍符合 domain invariant。</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">sqlite3 fixture-v2.db <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">PRAGMA integrity_check;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">PRAGMA foreign_key_check;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SELECT COUNT(*) AS account_count FROM accounts;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SELECT COUNT(*) AS ledger_count FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SELECT SUM(amount_cents) AS total_balance FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">PRAGMA user_version;
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>驗收結果應包含 integrity <code>ok</code>、foreign key check 空結果、account count <code>1</code>、ledger count <code>1</code>、total balance <code>1000</code>、user version <code>3</code>。</p>
<h2 id="contract-test-hook">Contract Test Hook</h2>
<p>Contract test hook 的核心責任是讓 fixture 進入 CI。語言與 framework 可以不同，但測試要固定做三件事：開啟 FK、確認 schema version、跑 repository contract。</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">test setup:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  copy fixture-v2.db to temp path
</span></span><span class="line"><span class="ln">3</span><span class="cl">  open SQLite connection
</span></span><span class="line"><span class="ln">4</span><span class="cl">  execute PRAGMA foreign_keys = ON
</span></span><span class="line"><span class="ln">5</span><span class="cl">  assert PRAGMA user_version = 3
</span></span><span class="line"><span class="ln">6</span><span class="cl">  run repository contract tests</span></span></code></pre></div><p>每個 test 使用 temp copy 可以避免資料污染。需要測 concurrency 時，改用 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>。</p>
<h2 id="rollback-note">Rollback Note</h2>
<p>Rollback note 的核心責任是把 migration 失敗時的處理寫清楚。這個 lab 的 rollback 是保留 <code>fixture-v1-before-migration.db</code>，在 migration validation 失敗時停止 release 並保存 failed DB。</p>
<p>正式 runbook 要記錄：</p>
<ol>
<li>Migration id 與 source / target <code>user_version</code>。</li>
<li>Pre-migration backup path。</li>
<li>Validation query 與結果。</li>
<li>Failed DB 保存路徑。</li>
<li>Release block / rollback 條件。</li>
</ol>
<p>完成本篇後，下一步可以讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a> 或 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso migration</a>。</p>
]]></content:encoded></item><item><title>SQLite Mobile / Desktop Embedded Store</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 &lt;em>device-local formal state 的資料責任、backup、privacy 與 sync boundary&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，再決定 backup、sync、privacy 與 migration 責任。&lt;/p>
&lt;p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。&lt;/p>
&lt;h2 id="embedded-state-model">Embedded state model&lt;/h2>
&lt;p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database&lt;/a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>SQLite 資料角色&lt;/th>
 &lt;th>主要風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mobile app&lt;/td>
 &lt;td>offline state、draft、cache、local profile&lt;/td>
 &lt;td>app upgrade、device loss、cloud backup leakage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Desktop app&lt;/td>
 &lt;td>user profile、history、settings&lt;/td>
 &lt;td>profile corruption、manual file copy、multi-version app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CLI tool&lt;/td>
 &lt;td>local index、metadata、state cache&lt;/td>
 &lt;td>command interruption、portable file path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser / profile&lt;/td>
 &lt;td>cookies、history、bookmark 類資料&lt;/td>
 &lt;td>privacy、profile migration、lock collision&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedded device&lt;/td>
 &lt;td>offline event、sensor / config state&lt;/td>
 &lt;td>power loss、flash wear、delayed sync&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。&lt;/p>
&lt;h2 id="backup-與-export">Backup 與 export&lt;/h2>
&lt;p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 <em>device-local formal state 的資料責任、backup、privacy 與 sync boundary</em>。</p></blockquote>
<p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，再決定 backup、sync、privacy 與 migration 責任。</p>
<p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。</p>
<h2 id="embedded-state-model">Embedded state model</h2>
<p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 <a href="/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database</a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>SQLite 資料角色</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mobile app</td>
          <td>offline state、draft、cache、local profile</td>
          <td>app upgrade、device loss、cloud backup leakage</td>
      </tr>
      <tr>
          <td>Desktop app</td>
          <td>user profile、history、settings</td>
          <td>profile corruption、manual file copy、multi-version app</td>
      </tr>
      <tr>
          <td>CLI tool</td>
          <td>local index、metadata、state cache</td>
          <td>command interruption、portable file path</td>
      </tr>
      <tr>
          <td>Browser / profile</td>
          <td>cookies、history、bookmark 類資料</td>
          <td>privacy、profile migration、lock collision</td>
      </tr>
      <tr>
          <td>Embedded device</td>
          <td>offline event、sensor / config state</td>
          <td>power loss、flash wear、delayed sync</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。</p>
<h2 id="backup-與-export">Backup 與 export</h2>
<p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>適合資料</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OS / device backup</td>
          <td>user-owned local state</td>
          <td>local PII、encryption、restore compatibility</td>
      </tr>
      <tr>
          <td>App export</td>
          <td>使用者可攜資料</td>
          <td>schema version、format stability、privacy</td>
      </tr>
      <tr>
          <td><code>.backup</code> / snapshot</td>
          <td>application-managed backup</td>
          <td>live DB consistency、WAL sidecar handling</td>
      </tr>
      <tr>
          <td>Cloud sync</td>
          <td>multi-device state</td>
          <td>conflict、server authority、delete propagation</td>
      </tr>
  </tbody>
</table>
<p>Backup 設計要先決定 restore target。Restore 到同 app version、未來 app version、或不同 device，會帶來不同 schema compatibility 與 privacy requirement。</p>
<h2 id="privacy-與-local-pii">Privacy 與 local PII</h2>
<p>Embedded SQLite 的 privacy 責任是治理 device-local data。資料在 server DB 中通常有 access log、IAM、DLP 與 retention policy；進入 SQLite file 後，風險轉到 device encryption、app sandbox、backup retention、debug export 與 support bundle。</p>
<table>
  <thead>
      <tr>
          <th>風險</th>
          <th>真實情境</th>
          <th>控制方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local PII</td>
          <td>profile、token、message、draft</td>
          <td>最小化欄位、加密敏感值、限制 export</td>
      </tr>
      <tr>
          <td>Backup leakage</td>
          <td>OS cloud backup 含 database file</td>
          <td>設定 backup exclusion 或加密</td>
      </tr>
      <tr>
          <td>Support bundle</td>
          <td>使用者回報問題附上 DB</td>
          <td>scrub / redaction、只匯出必要 table</td>
      </tr>
      <tr>
          <td>Delete request</td>
          <td>server 刪除但 device local 留存</td>
          <td>sync delete、local purge、retention evidence</td>
      </tr>
  </tbody>
</table>
<p>SQLite file 要進入資料保護盤點。若 local DB 保存敏感資料，應連到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a> 與 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 的相同問題，只是控制面改在 device / app。</p>
<h2 id="app-upgrade-與-schema-compatibility">App upgrade 與 schema compatibility</h2>
<p>App upgrade 的核心責任是保證新版 binary 能安全打開舊 database file。Mobile / desktop app 的使用者不會按照 backend deployment order 升級；同一時間可能存在多個 app version 與多個 DB schema version。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>設計策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 app 打舊 DB</td>
          <td>startup migration、<code>user_version</code>、backup before migration</td>
      </tr>
      <tr>
          <td>舊 app 打新 DB</td>
          <td>backward-compatible column、feature gate、minimum supported version</td>
      </tr>
      <tr>
          <td>使用者降版</td>
          <td>export / import、read-only fallback、no-downgrade notice</td>
      </tr>
      <tr>
          <td>多裝置不同版本</td>
          <td>sync protocol version、server-side compatibility</td>
      </tr>
  </tbody>
</table>
<p>這些策略要和 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a> 對齊。Embedded app 的 migration failure 通常直接影響使用者啟動體驗，因此 migration 要能快速、可恢復、可診斷。</p>
<h2 id="sync-boundary">Sync boundary</h2>
<p>Sync boundary 的核心責任是把 single-device SQLite 和 multi-device state 分開。SQLite 保存本地狀態；跨裝置同步需要 transport、identity、conflict resolution、delete propagation 與 server authority。</p>
<table>
  <thead>
      <tr>
          <th>Sync 需求</th>
          <th>SQLite 角色</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單裝置 offline</td>
          <td>local source of truth</td>
          <td>SQLite + backup / export</td>
      </tr>
      <tr>
          <td>多裝置同步</td>
          <td>local replica / cache</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary</a></td>
      </tr>
      <tr>
          <td>即時多人協作</td>
          <td>local working copy</td>
          <td>server authority、CRDT、event log</td>
      </tr>
      <tr>
          <td>Server reporting</td>
          <td>local data upload / ETL</td>
          <td>API sync、queue、analytics store</td>
      </tr>
  </tbody>
</table>
<p>當 sync 需求出現時，SQLite 仍可作為 local store，但不再單獨承擔完整資料一致性。完整性要由 sync protocol 與 server-side validation 補上。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1把-cache-當正式資料">Case 1：把 cache 當正式資料</h3>
<p>Cache 被誤當正式資料的核心風險是清除 local DB 會造成不可恢復資料損失。許多 app 初期把 SQLite 當 cache；後來加入 draft、offline action 或 local-only setting，資料責任就改變了。</p>
<p>修正方向是逐 table 標示資料角色。Cache table 可清；formal state table 要 backup、migration、export 與 delete policy。</p>
<h3 id="case-2os-backup-帶走敏感資料">Case 2：OS backup 帶走敏感資料</h3>
<p>OS backup 的核心風險是 device-local PII 進入使用者或平台雲端備份。Server 端已刪除的資料，可能仍存在 device backup。</p>
<p>修正方向是決定哪些資料可被備份。Token、secret、敏感 PII 可排除或加密；user-owned content 則要提供 export / restore 語意。</p>
<h3 id="case-3app-upgrade-migration-失敗讓使用者卡在啟動頁">Case 3：App upgrade migration 失敗讓使用者卡在啟動頁</h3>
<p>Startup migration 失敗的核心風險是使用者卡在 app 啟動前，且修復能力有限。SQLite file 在使用者裝置上，SRE 通常需要透過 app update、support bundle 或 restore flow 處理。</p>
<p>修正方向是保留 pre-migration snapshot、提供 safe mode、收集匿名 schema / error evidence，並避免長 migration 放在 cold start。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>Embedded SQLite 設計要回答：</p>
<ol>
<li>每張 table 是 cache、formal state、derived state 還是 sync queue。</li>
<li>Database file 在 app / OS 的哪個 storage boundary。</li>
<li>OS backup 是否包含 database file。</li>
<li>敏感欄位是否加密、排除或可清除。</li>
<li>App upgrade migration 是否有 pre-migration backup。</li>
<li>使用者 export / delete / support bundle 如何處理 SQLite data。</li>
<li>Multi-device sync 是否有 conflict 與 server authority 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first Sync Boundary</a>、<a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></li>
<li>官方：<a href="https://www.sqlite.org/whentouse.html">SQLite Appropriate Uses</a>、<a href="https://www.sqlite.org/backup.html">SQLite Backup API</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Observability and Runbook</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</guid><description>&lt;p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。&lt;/p>
&lt;h2 id="signal-inventory">Signal Inventory&lt;/h2>
&lt;p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&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>SQLITE_BUSY&lt;/code> count&lt;/td>
 &lt;td>app log / metric&lt;/td>
 &lt;td>writer contention、long reader&lt;/td>
 &lt;td>查 transaction duration、busy timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL file size&lt;/td>
 &lt;td>filesystem metric&lt;/td>
 &lt;td>checkpoint lag、long reader&lt;/td>
 &lt;td>查 checkpoint result、reader age&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup age&lt;/td>
 &lt;td>scheduled job metric&lt;/td>
 &lt;td>RPO 擴大&lt;/td>
 &lt;td>重跑 backup、檢查 storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Restore drill age&lt;/td>
 &lt;td>release evidence&lt;/td>
 &lt;td>RTO 信心下降&lt;/td>
 &lt;td>排程 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Disk free&lt;/td>
 &lt;td>host / platform metric&lt;/td>
 &lt;td>write failure、checkpoint failure&lt;/td>
 &lt;td>清理、擴容、降級寫入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration version&lt;/td>
 &lt;td>app startup / metadata&lt;/td>
 &lt;td>schema drift&lt;/td>
 &lt;td>block release、跑 validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integrity check result&lt;/td>
 &lt;td>maintenance job&lt;/td>
 &lt;td>corruption / storage issue&lt;/td>
 &lt;td>進入 restore decision&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>SQLITE_BUSY&lt;/code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。&lt;/p>
&lt;p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。&lt;/p>
&lt;p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。&lt;/p>
&lt;h2 id="backup-evidence">Backup Evidence&lt;/h2>
&lt;p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。</p>
<p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。</p>
<h2 id="signal-inventory">Signal Inventory</h2>
<p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>來源</th>
          <th>代表風險</th>
          <th>建議反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> count</td>
          <td>app log / metric</td>
          <td>writer contention、long reader</td>
          <td>查 transaction duration、busy timeout</td>
      </tr>
      <tr>
          <td>WAL file size</td>
          <td>filesystem metric</td>
          <td>checkpoint lag、long reader</td>
          <td>查 checkpoint result、reader age</td>
      </tr>
      <tr>
          <td>Backup age</td>
          <td>scheduled job metric</td>
          <td>RPO 擴大</td>
          <td>重跑 backup、檢查 storage</td>
      </tr>
      <tr>
          <td>Restore drill age</td>
          <td>release evidence</td>
          <td>RTO 信心下降</td>
          <td>排程 restore drill</td>
      </tr>
      <tr>
          <td>Disk free</td>
          <td>host / platform metric</td>
          <td>write failure、checkpoint failure</td>
          <td>清理、擴容、降級寫入</td>
      </tr>
      <tr>
          <td>Migration version</td>
          <td>app startup / metadata</td>
          <td>schema drift</td>
          <td>block release、跑 validation</td>
      </tr>
      <tr>
          <td>Integrity check result</td>
          <td>maintenance job</td>
          <td>corruption / storage issue</td>
          <td>進入 restore decision</td>
      </tr>
  </tbody>
</table>
<p><code>SQLITE_BUSY</code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。</p>
<p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。</p>
<p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。</p>
<h2 id="backup-evidence">Backup Evidence</h2>
<p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小內容</th>
          <th>失敗時路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup job result</td>
          <td>timestamp、duration、file size、target</td>
          <td>重跑 job、檢查 credential / disk</td>
      </tr>
      <tr>
          <td>Restore artifact</td>
          <td>restored path、checksum、row count</td>
          <td>回前一份 backup、檢查 WAL / snapshot</td>
      </tr>
      <tr>
          <td>Integrity result</td>
          <td><code>PRAGMA integrity_check;</code></td>
          <td>停止寫入、進入 corruption triage</td>
      </tr>
      <tr>
          <td>Application smoke test</td>
          <td>啟動、讀核心頁、寫測試資料</td>
          <td>rollback、保留 evidence</td>
      </tr>
      <tr>
          <td>Retention note</td>
          <td>保存天數、刪除策略、legal hold</td>
          <td>更新 data protection policy</td>
      </tr>
  </tbody>
</table>
<p>SQLite 官方 <a href="https://www.sqlite.org/backup.html">backup API</a> 與 CLI <code>.backup</code> 是備份設計的基礎路由。WAL mode 下，直接複製單一 <code>.db</code> 檔容易漏掉 sidecar file 的時序；runbook 應使用 SQLite-aware backup 或經過 checkpoint / stop-the-world 的 snapshot。</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">sqlite3 app.db <span class="s2">&#34;.backup &#39;backup/app-2026-05-21.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-2026-05-21.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令提供最小 restore evidence 的起點。正式演練要把備份檔複製到隔離路徑，使用相同 application version 啟動，跑核心 read/write smoke test，再記錄耗時與失敗條件。</p>
<h2 id="migration-evidence">Migration Evidence</h2>
<p>Migration evidence 的核心責任是讓 SQLite schema change 可回退、可審查、可交接。單檔 DB 在使用者裝置或服務節點上升級時，migration 失敗會直接影響啟動、資料讀取與同步。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>內容</th>
          <th>Release gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema version</td>
          <td><code>PRAGMA user_version</code> 或 migration table</td>
          <td>app startup 比對 expected version</td>
      </tr>
      <tr>
          <td>Pre-migration snapshot</td>
          <td>backup path、size、checksum</td>
          <td>migration 前完成</td>
      </tr>
      <tr>
          <td>Validation query</td>
          <td>row count、FK check、domain invariant</td>
          <td>migration 後立即執行</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>app release gate</td>
      </tr>
      <tr>
          <td>Rollback route</td>
          <td>restore snapshot 或 block startup</td>
          <td>migration 失敗時啟動</td>
      </tr>
  </tbody>
</table>
<p>Migration log 要包含版本、耗時、row count、錯誤、validation result 與 rollback decision。若 SQLite file 位於 end-user device，log 還要能被使用者支援流程收集，避免事故只停在「app 開不起來」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>這些 query 是 migration 後的最小 evidence。正式服務要再補 domain-specific invariant，例如「所有 active subscription 都有 owner」、「所有 pending mutation 都有 idempotency key」。</p>
<h2 id="incident-runbook">Incident Runbook</h2>
<p>Incident runbook 的核心責任是把 SQLite 事故分流到正確處置。SQLite 常見事故包含 disk full、busy storm、WAL growth、bad migration、corruption suspicion、backup failure 與 permission error。</p>
<table>
  <thead>
      <tr>
          <th>Incident</th>
          <th>第一個判讀問題</th>
          <th>立即處置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Busy storm</td>
          <td>有長 transaction 或 write burst 嗎</td>
          <td>暫停非必要寫入、查 transaction duration</td>
      </tr>
      <tr>
          <td>Disk full</td>
          <td>DB / WAL / backup 哪個吃掉空間</td>
          <td>停止寫入、清理 backup、擴容</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>checkpoint 被誰阻擋</td>
          <td>查 reader、跑 checkpoint evidence</td>
      </tr>
      <tr>
          <td>Bad migration</td>
          <td>schema version 與 app version 是否一致</td>
          <td>停止 rollout、restore snapshot、保留 failed DB</td>
      </tr>
      <tr>
          <td>Corruption signal</td>
          <td>integrity check 是否失敗</td>
          <td>進入 read-only、restore last good backup</td>
      </tr>
      <tr>
          <td>Backup failure</td>
          <td>credential、network、destination 是否可用</td>
          <td>切換 destination、補跑 restore drill</td>
      </tr>
  </tbody>
</table>
<p>Busy storm 要先保護使用者操作。可以降低 write endpoint、停用背景 job、延長 retry backoff，然後用 log 查最長 transaction 與最多重試的 query。</p>
<p>Disk full 要先停止寫入。SQLite 在 disk full 時可能讓 write / checkpoint / backup 同時失敗；runbook 要保留剩餘空間、DB file、WAL file、backup directory 與 tmp directory 的大小。</p>
<p>Bad migration 要保留 failed artifact。先複製 failed DB 到 evidence path，記錄 schema version、app version、migration id、validation error，再執行 rollback。</p>
<h2 id="dashboard-and-alert-route">Dashboard and Alert Route</h2>
<p>Dashboard and alert route 的核心責任是讓 SQLite 被納入正式服務的可觀測系統。SQLite signal 常來自 application，因此 metric 命名要接近操作問題。</p>
<table>
  <thead>
      <tr>
          <th>Metric name example</th>
          <th>類型</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sqlite_busy_total</code></td>
          <td>counter</td>
          <td>writer contention</td>
      </tr>
      <tr>
          <td><code>sqlite_query_duration_ms</code></td>
          <td>histogram</td>
          <td>slow query / long transaction</td>
      </tr>
      <tr>
          <td><code>sqlite_wal_size_bytes</code></td>
          <td>gauge</td>
          <td>checkpoint pressure</td>
      </tr>
      <tr>
          <td><code>sqlite_backup_age_seconds</code></td>
          <td>gauge</td>
          <td>RPO evidence</td>
      </tr>
      <tr>
          <td><code>sqlite_restore_drill_age_days</code></td>
          <td>gauge</td>
          <td>RTO confidence</td>
      </tr>
      <tr>
          <td><code>sqlite_disk_free_bytes</code></td>
          <td>gauge</td>
          <td>disk full prevention</td>
      </tr>
      <tr>
          <td><code>sqlite_migration_version</code></td>
          <td>gauge</td>
          <td>schema drift</td>
      </tr>
  </tbody>
</table>
<p>Alert 要連到 runbook，並提供可執行的第一步。每個 alert 至少要有 owner、severity、first query、rollback condition 與 escalation route。</p>
<p>Log schema 要保留 query category，而非只記原始 SQL。正式服務通常應避免把完整 SQL 與 PII 直接寫入 log；可以記 operation name、duration、row count、error code、busy retry count 與 correlation id。</p>
<h2 id="handoff">Handoff</h2>
<p>Handoff 的核心責任是讓下一個維護者知道 SQLite service 的邊界。交接文件要把「誰負責檔案」、「誰負責備份」、「誰能執行 restore」、「何時升級資料庫」寫清楚。</p>
<p>最小 handoff 包含：</p>
<ol>
<li>Database file path、sidecar file policy、journal mode 與 PRAGMA baseline。</li>
<li>Backup command、destination、retention、last restore drill。</li>
<li>Migration command、schema version、rollback route。</li>
<li>Alert list、dashboard link、incident owner。</li>
<li>Known limits：writer concurrency、file size、edge / sync boundary。</li>
<li>Next route：PostgreSQL、D1 / Turso、Litestream / LiteFS 的評估條件。</li>
</ol>
<p>Handoff 的重點是把低操作成本保留下來。SQLite 的好處來自少元件；可交接文件讓少元件不等於少 evidence。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Observability / runbook 完成後，下一步要接到具體演練。Backup 與 restore 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/" data-link-title="SQLite Backup Restore Drill" data-link-desc="SQLite .backup、VACUUM INTO、restore validation、sidecar file handling 與 RPO / RTO note 的操作說明">SQLite backup restore drill</a>；WAL 與 busy 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>；正式服務的 evidence 可對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
]]></content:encoded></item><item><title>SQLite PRAGMA Tuning and Performance</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的容量規劃要點；本文聚焦 &lt;em>PRAGMA 設定如何變成 durability、latency、檔案大小與 restore risk 的取捨&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite PRAGMA tuning 的核心責任是把單檔資料庫的行為固定成可重複、可觀測、可回退的操作契約。SQLite 的許多重要行為由 connection-level 或 database-level PRAGMA 控制；這些設定看起來像小開關，實際上會影響 crash recovery、commit latency、reader / writer 衝突、檔案大小與測試一致性。&lt;/p>
&lt;p>本文的判讀錨點是：PRAGMA 是 durability / latency / maintenance 的顯性取捨，而非效能魔法。Production runbook 要記錄設定值、設定時機、驗證 query 與回退條件，避免不同 process、test runner 或 migration tool 用不同 SQLite 行為。&lt;/p>
&lt;h2 id="baseline-pragma">Baseline PRAGMA&lt;/h2>
&lt;p>SQLite baseline PRAGMA 的責任是讓 application 每次啟動都進入同一個資料庫模式。對 production-like local store、small backend 或 test fixture，建議把 journal、sync、foreign key、busy timeout 與 checkpoint 明確設定，而非依賴語言 binding 預設值。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">journal_mode&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WAL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">synchronous&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NORMAL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foreign_keys&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">busy_timeout&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5000&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">wal_autocheckpoint&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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>&lt;code>journal_mode=WAL&lt;/code>&lt;/td>
 &lt;td>降低 reader / writer 衝突&lt;/td>
 &lt;td>回傳值為 &lt;code>wal&lt;/code>，觀察 &lt;code>-wal&lt;/code> file&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>synchronous=NORMAL&lt;/code>&lt;/td>
 &lt;td>平衡 fsync cost 與 crash durability&lt;/td>
 &lt;td>查 &lt;code>PRAGMA synchronous&lt;/code>，跑 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>foreign_keys=ON&lt;/code>&lt;/td>
 &lt;td>啟用 FK enforcement&lt;/td>
 &lt;td>&lt;code>PRAGMA foreign_key_check&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>busy_timeout&lt;/code>&lt;/td>
 &lt;td>吸收短暫 writer queue&lt;/td>
 &lt;td>記錄 busy wait 與 timeout rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>wal_autocheckpoint&lt;/code>&lt;/td>
 &lt;td>控制 WAL growth cadence&lt;/td>
 &lt;td>觀察 WAL size 與 checkpoint duration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是把設定與 evidence 綁在一起。若某個 PRAGMA 缺少成功訊號與失敗訊號，就先維持保守預設；盲目追求「最快」通常會把風險推到 power loss、restore 或長尾 latency。&lt;/p>
&lt;h2 id="journal_mode-與-wal-boundary">&lt;code>journal_mode&lt;/code> 與 WAL boundary&lt;/h2>
&lt;p>&lt;code>journal_mode&lt;/code> 的核心責任是決定 transaction 如何保護原始資料。SQLite 預設 rollback journal 對簡單場景合理；WAL mode 則讓 reader 可以在 writer append WAL 時保有 snapshot，適合多 reader、短寫入、互動式 workload。&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>&lt;code>DELETE&lt;/code>&lt;/td>
 &lt;td>最簡單、低併發、短生命週期檔案&lt;/td>
 &lt;td>write / read 衝突較明顯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>WAL&lt;/code>&lt;/td>
 &lt;td>read-heavy、local app、小型 API&lt;/td>
 &lt;td>需要治理 &lt;code>-wal&lt;/code>、&lt;code>-shm&lt;/code>、checkpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>MEMORY&lt;/code>&lt;/td>
 &lt;td>暫存測試、可丟資料&lt;/td>
 &lt;td>crash 後 recovery 風險高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>OFF&lt;/code>&lt;/td>
 &lt;td>可重建資料、一次性 bulk load&lt;/td>
 &lt;td>production formal state 應避開&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>WAL mode 是多數 production-like SQLite 的 baseline，但它也引入 sidecar file 與 checkpoint 責任。完整判讀見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的容量規劃要點；本文聚焦 <em>PRAGMA 設定如何變成 durability、latency、檔案大小與 restore risk 的取捨</em>。</p></blockquote>
<p>SQLite PRAGMA tuning 的核心責任是把單檔資料庫的行為固定成可重複、可觀測、可回退的操作契約。SQLite 的許多重要行為由 connection-level 或 database-level PRAGMA 控制；這些設定看起來像小開關，實際上會影響 crash recovery、commit latency、reader / writer 衝突、檔案大小與測試一致性。</p>
<p>本文的判讀錨點是：PRAGMA 是 durability / latency / maintenance 的顯性取捨，而非效能魔法。Production runbook 要記錄設定值、設定時機、驗證 query 與回退條件，避免不同 process、test runner 或 migration tool 用不同 SQLite 行為。</p>
<h2 id="baseline-pragma">Baseline PRAGMA</h2>
<p>SQLite baseline PRAGMA 的責任是讓 application 每次啟動都進入同一個資料庫模式。對 production-like local store、small backend 或 test fixture，建議把 journal、sync、foreign key、busy timeout 與 checkpoint 明確設定，而非依賴語言 binding 預設值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">journal_mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">WAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">synchronous</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">NORMAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_autocheckpoint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設定</th>
          <th>服務責任</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>journal_mode=WAL</code></td>
          <td>降低 reader / writer 衝突</td>
          <td>回傳值為 <code>wal</code>，觀察 <code>-wal</code> file</td>
      </tr>
      <tr>
          <td><code>synchronous=NORMAL</code></td>
          <td>平衡 fsync cost 與 crash durability</td>
          <td>查 <code>PRAGMA synchronous</code>，跑 restore drill</td>
      </tr>
      <tr>
          <td><code>foreign_keys=ON</code></td>
          <td>啟用 FK enforcement</td>
          <td><code>PRAGMA foreign_key_check</code></td>
      </tr>
      <tr>
          <td><code>busy_timeout</code></td>
          <td>吸收短暫 writer queue</td>
          <td>記錄 busy wait 與 timeout rate</td>
      </tr>
      <tr>
          <td><code>wal_autocheckpoint</code></td>
          <td>控制 WAL growth cadence</td>
          <td>觀察 WAL size 與 checkpoint duration</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是把設定與 evidence 綁在一起。若某個 PRAGMA 缺少成功訊號與失敗訊號，就先維持保守預設；盲目追求「最快」通常會把風險推到 power loss、restore 或長尾 latency。</p>
<h2 id="journal_mode-與-wal-boundary"><code>journal_mode</code> 與 WAL boundary</h2>
<p><code>journal_mode</code> 的核心責任是決定 transaction 如何保護原始資料。SQLite 預設 rollback journal 對簡單場景合理；WAL mode 則讓 reader 可以在 writer append WAL 時保有 snapshot，適合多 reader、短寫入、互動式 workload。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>適合情境</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DELETE</code></td>
          <td>最簡單、低併發、短生命週期檔案</td>
          <td>write / read 衝突較明顯</td>
      </tr>
      <tr>
          <td><code>WAL</code></td>
          <td>read-heavy、local app、小型 API</td>
          <td>需要治理 <code>-wal</code>、<code>-shm</code>、checkpoint</td>
      </tr>
      <tr>
          <td><code>MEMORY</code></td>
          <td>暫存測試、可丟資料</td>
          <td>crash 後 recovery 風險高</td>
      </tr>
      <tr>
          <td><code>OFF</code></td>
          <td>可重建資料、一次性 bulk load</td>
          <td>production formal state 應避開</td>
      </tr>
  </tbody>
</table>
<p>WAL mode 是多數 production-like SQLite 的 baseline，但它也引入 sidecar file 與 checkpoint 責任。完整判讀見 <a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a>。</p>
<h2 id="synchronouscommit-latency-與資料損失窗口"><code>synchronous</code>：commit latency 與資料損失窗口</h2>
<p><code>synchronous</code> 的核心責任是控制 SQLite 在關鍵時刻要求 storage flush 的強度。官方 PRAGMA 文件說明 WAL mode 下 <code>NORMAL</code> 會把 sync 主要放在 checkpoint 路徑；這通常讓 commit 更快，但 crash durability 的語意要由 service owner 接受。</p>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>服務語意</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>FULL</code></td>
          <td>更保守的 durability</td>
          <td>金錢、ledger、不可重建 local state</td>
      </tr>
      <tr>
          <td><code>NORMAL</code></td>
          <td>多數 WAL production-like baseline</td>
          <td>local app、小型服務、可接受極小 crash window</td>
      </tr>
      <tr>
          <td><code>OFF</code></td>
          <td>追求速度，放棄重要 durability</td>
          <td>scratch DB、可重建 cache、bulk import staging</td>
      </tr>
  </tbody>
</table>
<p><code>synchronous=OFF</code> 要被視為明確風險接受。若資料是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，設定檔、runbook 與 review 都應避免把 staging 的快速設定帶進 production。</p>
<h2 id="cachemmap-與-memory-pressure">Cache、mmap 與 memory pressure</h2>
<p>SQLite memory tuning 的核心責任是降低 read path I/O，同時避免把 device / container memory 壓到不可控。<code>cache_size</code> 控制 SQLite page cache；<code>mmap_size</code> 讓讀取可透過 memory-mapped I/O 加速，但仍受平台、檔案大小與 memory budget 影響。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">cache_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">64000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">mmap_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">268435456</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設定</th>
          <th>改善目標</th>
          <th>觀測訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>cache_size</code></td>
          <td>減少重複 page read</td>
          <td>query latency、disk read、memory usage</td>
      </tr>
      <tr>
          <td><code>mmap_size</code></td>
          <td>降低 read syscall cost</td>
          <td>p95 / p99 read latency、address space</td>
      </tr>
      <tr>
          <td><code>temp_store</code></td>
          <td>控制 temp table 位置</td>
          <td>sort / join query latency、memory pressure</td>
      </tr>
  </tbody>
</table>
<p>Memory 設定要和 workload size 一起看。Desktop app、mobile app、edge worker、container service 的 memory ceiling 不同；把 server 上的設定複製到 mobile 或 edge runtime 會讓風險轉移到 OOM 或 OS reclaim。</p>
<h2 id="vacuum-與檔案大小治理">Vacuum 與檔案大小治理</h2>
<p>Vacuum 設定的核心責任是控制 delete 後的空間回收。SQLite delete row 後，database file 不會自然縮小；<code>auto_vacuum</code> 要在 database 建立早期決定，後續切換通常需要 <code>VACUUM</code> 重整整個 database。</p>
<table>
  <thead>
      <tr>
          <th>設定 / 操作</th>
          <th>適合情境</th>
          <th>風險 / 成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>auto_vacuum=NONE</code></td>
          <td>資料量穩定、delete 少</td>
          <td>檔案可能長期保持高水位</td>
      </tr>
      <tr>
          <td><code>auto_vacuum=INCREMENTAL</code></td>
          <td>需要逐步回收空間</td>
          <td>需要排程 <code>incremental_vacuum</code></td>
      </tr>
      <tr>
          <td><code>VACUUM</code></td>
          <td>maintenance window、重整資料庫</td>
          <td>需要額外空間與 I/O，可能影響服務</td>
      </tr>
      <tr>
          <td><code>VACUUM INTO</code></td>
          <td>compact copy / backup</td>
          <td>產出新檔，適合 restore drill 或 export</td>
      </tr>
  </tbody>
</table>
<p>檔案大小治理要接到 backup 成本。Database file 長期膨脹會放大備份時間、restore 時間與 edge deploy artifact size；若服務有大量 delete / churn，vacuum policy 要被寫進 runbook。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1pragma-只在某個-connection-設定">Case 1：PRAGMA 只在某個 connection 設定</h3>
<p>Connection-level PRAGMA 的核心風險是不同程式路徑行為不一致。Application 啟動時設了 <code>foreign_keys=ON</code>，migration tool 或 test runner 沒設，就會出現 production / migration / test 三種語意。</p>
<p>修正方向是把 baseline PRAGMA 放進 shared DB open path，並在 startup health check 印出設定值。Migration CLI、background worker、test fixture 都要共用同一份 connection initialization。</p>
<h3 id="case-2synchronousoff-從測試環境流到正式資料">Case 2：<code>synchronous=OFF</code> 從測試環境流到正式資料</h3>
<p>快速測試設定外流的核心風險是資料損失只在 crash 後出現。平常 query 都正常，直到 power loss、container kill 或 host crash 後，資料庫出現落差。</p>
<p>修正方向是設定分層。Test / benchmark 可以用 faster profile；formal state profile 要用 <code>NORMAL</code> 或 <code>FULL</code>，並要求 restore drill。</p>
<h3 id="case-3wal-growth-被誤判成資料成長">Case 3：WAL growth 被誤判成資料成長</h3>
<p>WAL growth 的核心風險是 checkpoint 問題被當成容量問題。Disk alert 看到 <code>db-wal</code> 變大，若只擴 disk，長 reader 或 checkpoint starvation 仍會持續。</p>
<p>修正方向是把 WAL size、checkpoint return 與 long reader 一起看。先找 reader lifecycle，再調 checkpoint cadence。</p>
<h3 id="case-4vacuum-在高峰期執行">Case 4：Vacuum 在高峰期執行</h3>
<p>Vacuum 的核心風險是把 maintenance I/O 放到使用者路徑。檔案縮小是好事，但 full vacuum 會消耗 I/O 與時間，對 mobile / desktop / small backend 都可能造成卡頓。</p>
<p>修正方向是把 vacuum 當 maintenance job。大檔案用 <code>incremental_vacuum</code> 或低流量窗口；備份前的 compact copy 可考慮 <code>VACUUM INTO</code>。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite PRAGMA runbook 至少要記錄：</p>
<ol>
<li>所有 connection 初始化時執行的 baseline PRAGMA。</li>
<li><code>journal_mode</code> 實際回傳值與 sidecar file 位置。</li>
<li><code>synchronous</code> profile 與資料風險接受者。</li>
<li><code>busy_timeout</code> 值、busy wait metric、timeout threshold。</li>
<li><code>wal_autocheckpoint</code>、manual checkpoint cadence 與 WAL size alert。</li>
<li><code>cache_size</code> / <code>mmap_size</code> 對 memory budget 的影響。</li>
<li><code>auto_vacuum</code> / <code>VACUUM</code> / <code>VACUUM INTO</code> 的 maintenance window。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>前置：<a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></li>
<li>官方：<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a>、<a href="https://www.sqlite.org/lang_vacuum.html">SQLite VACUUM</a>、<a href="https://www.sqlite.org/wal.html">SQLite WAL</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Schema Migration and Versioning</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 embedded / single-file 定位；本文聚焦 &lt;em>schema version、ALTER TABLE boundary、table rebuild migration 與 application release compatibility&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite schema migration 的核心責任是讓單檔資料庫隨 application release 安全演進。SQLite 沒有獨立 database server，也沒有 DBA 在 server 端統一套 migration；migration 常在 application startup、CLI command、mobile app upgrade 或 desktop app launch 時發生，因此 schema version、binary compatibility、backup 與 rollback 要放在同一個 release contract 中設計。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite migration 同時改資料庫檔案與 application 能讀的資料格式。只要使用者或服務可能拿舊 binary 打開新 database，或新 binary 打開舊 database，migration 就要處理 forward / backward compatibility，而不只是 SQL 成功執行。&lt;/p>
&lt;h2 id="version-model">Version model&lt;/h2>
&lt;p>SQLite schema versioning 的服務責任是讓 application 能判斷 database file 目前處於哪個契約。SQLite 提供 &lt;code>PRAGMA user_version&lt;/code> 作為 application-controlled integer；更複雜的服務也可以用 migration table 記錄多步驟版本、checksum 與執行時間。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_version&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_version&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2026052101&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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;code>user_version&lt;/code>&lt;/td>
 &lt;td>mobile / desktop / CLI single file&lt;/td>
 &lt;td>簡單、內建、開檔即可讀&lt;/td>
 &lt;td>只能存一個整數，缺 migration history&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>migration table&lt;/td>
 &lt;td>small backend、多人維護 schema&lt;/td>
 &lt;td>可記錄每步 migration 與 owner&lt;/td>
 &lt;td>需要先建立 table 與初始化流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>external manifest&lt;/td>
 &lt;td>fixture、artifact、read-only DB&lt;/td>
 &lt;td>可和 release artifact 綁定&lt;/td>
 &lt;td>DB file 本身不含完整 history&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Version model 要在第一版就定義。沒有版本欄位的 SQLite file 仍可 migration，但 application 只能靠 introspection 猜 schema，會讓 upgrade / downgrade runbook 複雜化。&lt;/p>
&lt;h2 id="alter-table-boundary">ALTER TABLE boundary&lt;/h2>
&lt;p>SQLite ALTER TABLE 的核心責任是處理有限集合的 schema 變更。官方文件說明 SQLite 支援 rename table、rename column、add column、drop column；更複雜的變更要走 table rebuild pattern。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變更類型&lt;/th>
 &lt;th>SQLite 支援形態&lt;/th>
 &lt;th>操作判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rename table / column&lt;/td>
 &lt;td>直接 ALTER，版本差異影響 trigger / view&lt;/td>
 &lt;td>需要測 trigger、view、FK reference&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Add column&lt;/td>
 &lt;td>多數情境很快，受 default / constraint 限制&lt;/td>
 &lt;td>適合 expand migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drop column&lt;/td>
 &lt;td>需要檢查 index、constraint、trigger、view&lt;/td>
 &lt;td>可能掃資料，需 maintenance window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Change type / constraint&lt;/td>
 &lt;td>通常走 table rebuild&lt;/td>
 &lt;td>需要完整 copy、foreign key check、validation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>SQLite schema 存在 &lt;code>sqlite_schema&lt;/code> 的 SQL text 中；這讓檔案格式簡潔，但也讓 ALTER TABLE 的安全條件和 server SQL 不同。Production migration 應優先用官方建議的 rebuild procedure，而非直接修改 &lt;code>sqlite_schema&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 embedded / single-file 定位；本文聚焦 <em>schema version、ALTER TABLE boundary、table rebuild migration 與 application release compatibility</em>。</p></blockquote>
<p>SQLite schema migration 的核心責任是讓單檔資料庫隨 application release 安全演進。SQLite 沒有獨立 database server，也沒有 DBA 在 server 端統一套 migration；migration 常在 application startup、CLI command、mobile app upgrade 或 desktop app launch 時發生，因此 schema version、binary compatibility、backup 與 rollback 要放在同一個 release contract 中設計。</p>
<p>本文的判讀錨點是：SQLite migration 同時改資料庫檔案與 application 能讀的資料格式。只要使用者或服務可能拿舊 binary 打開新 database，或新 binary 打開舊 database，migration 就要處理 forward / backward compatibility，而不只是 SQL 成功執行。</p>
<h2 id="version-model">Version model</h2>
<p>SQLite schema versioning 的服務責任是讓 application 能判斷 database file 目前處於哪個契約。SQLite 提供 <code>PRAGMA user_version</code> 作為 application-controlled integer；更複雜的服務也可以用 migration table 記錄多步驟版本、checksum 與執行時間。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2026052101</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>方式</th>
          <th>適合情境</th>
          <th>優點</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>user_version</code></td>
          <td>mobile / desktop / CLI single file</td>
          <td>簡單、內建、開檔即可讀</td>
          <td>只能存一個整數，缺 migration history</td>
      </tr>
      <tr>
          <td>migration table</td>
          <td>small backend、多人維護 schema</td>
          <td>可記錄每步 migration 與 owner</td>
          <td>需要先建立 table 與初始化流程</td>
      </tr>
      <tr>
          <td>external manifest</td>
          <td>fixture、artifact、read-only DB</td>
          <td>可和 release artifact 綁定</td>
          <td>DB file 本身不含完整 history</td>
      </tr>
  </tbody>
</table>
<p>Version model 要在第一版就定義。沒有版本欄位的 SQLite file 仍可 migration，但 application 只能靠 introspection 猜 schema，會讓 upgrade / downgrade runbook 複雜化。</p>
<h2 id="alter-table-boundary">ALTER TABLE boundary</h2>
<p>SQLite ALTER TABLE 的核心責任是處理有限集合的 schema 變更。官方文件說明 SQLite 支援 rename table、rename column、add column、drop column；更複雜的變更要走 table rebuild pattern。</p>
<table>
  <thead>
      <tr>
          <th>變更類型</th>
          <th>SQLite 支援形態</th>
          <th>操作判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rename table / column</td>
          <td>直接 ALTER，版本差異影響 trigger / view</td>
          <td>需要測 trigger、view、FK reference</td>
      </tr>
      <tr>
          <td>Add column</td>
          <td>多數情境很快，受 default / constraint 限制</td>
          <td>適合 expand migration</td>
      </tr>
      <tr>
          <td>Drop column</td>
          <td>需要檢查 index、constraint、trigger、view</td>
          <td>可能掃資料，需 maintenance window</td>
      </tr>
      <tr>
          <td>Change type / constraint</td>
          <td>通常走 table rebuild</td>
          <td>需要完整 copy、foreign key check、validation</td>
      </tr>
  </tbody>
</table>
<p>SQLite schema 存在 <code>sqlite_schema</code> 的 SQL text 中；這讓檔案格式簡潔，但也讓 ALTER TABLE 的安全條件和 server SQL 不同。Production migration 應優先用官方建議的 rebuild procedure，而非直接修改 <code>sqlite_schema</code>。</p>
<h2 id="table-rebuild-migration">Table rebuild migration</h2>
<p>Table rebuild migration 的服務責任是安全完成 SQLite 直接 ALTER 難以表達的變更。官方 ALTER TABLE 文件建議的 generalized procedure 是建立新 table、copy data、drop old、rename new、重建 index / trigger / view、跑 foreign key check、commit。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OFF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="n">status</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="n">paid_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">paid_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">paid_at</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="k">RENAME</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2026052101</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span></span></span></code></pre></div><p>這段範例是教學骨架，而非可直接複製到所有 schema 的萬用腳本。真實 migration 要先保存 index、trigger、view 與 FK reference，再依 schema 重建；有資料量時還要考慮 copy duration、disk 空間與 rollback snapshot。</p>
<h2 id="app-release-compatibility">App release compatibility</h2>
<p>SQLite migration 的 application compatibility 來自 binary 與 DB file 的同步問題。Server SQL migration 通常有 central deploy order；SQLite file 可能跟著使用者裝置、desktop profile、CLI artifact 或 edge deploy 留在不同版本。</p>
<table>
  <thead>
      <tr>
          <th>相容性問題</th>
          <th>真實情境</th>
          <th>設計策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 app 打開舊 DB</td>
          <td>使用者升級 app</td>
          <td>startup migration、read compatibility</td>
      </tr>
      <tr>
          <td>舊 app 打開新 DB</td>
          <td>使用者 downgrade、同步舊 binary</td>
          <td>保留 backward-compatible column、feature gate</td>
      </tr>
      <tr>
          <td>多裝置不同版本</td>
          <td>local-first / sync app</td>
          <td>sync protocol version、server authority</td>
      </tr>
      <tr>
          <td>fixture 與 production drift</td>
          <td>test fixture 沒更新</td>
          <td>fixture version、contract test、migration smoke</td>
      </tr>
  </tbody>
</table>
<p>Compatibility 的核心是先決定支援範圍。Mobile app 常要支援舊版資料庫升級；internal CLI 可能只支援最新版本；test fixture 則需要每次 migration 後重新產生。</p>
<h2 id="migration-evidence">Migration evidence</h2>
<p>Migration evidence 的責任是證明 schema 變更已完成且資料仍可用。SQLite migration evidence 比 server DB 簡單，但更依賴 application-level validation。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>目的</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>schema version</td>
          <td>確認 DB file 契約</td>
          <td><code>PRAGMA user_version</code></td>
      </tr>
      <tr>
          <td>row count</td>
          <td>確認 copy / rebuild 無漏資料</td>
          <td><code>SELECT COUNT(*) FROM orders</code></td>
      </tr>
      <tr>
          <td>domain query</td>
          <td>確認重要 business invariant</td>
          <td>unpaid / paid 狀態數量</td>
      </tr>
      <tr>
          <td>foreign key check</td>
          <td>確認 reference integrity</td>
          <td><code>PRAGMA foreign_key_check</code></td>
      </tr>
      <tr>
          <td>integrity check</td>
          <td>檢查 DB 結構</td>
          <td><code>PRAGMA integrity_check</code></td>
      </tr>
      <tr>
          <td>backup marker</td>
          <td>回退點</td>
          <td>pre-migration <code>.backup</code> file</td>
      </tr>
  </tbody>
</table>
<p>這些 evidence 應接到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 或 release note。SQLite migration 失敗時，最清楚的 rollback 通常是回到 migration 前 snapshot，而非在同一檔案上繼續試錯。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1startup-migration-讓-app-啟動卡住">Case 1：startup migration 讓 app 啟動卡住</h3>
<p>Startup migration 的核心風險是把長時間 table rebuild 放在使用者啟動路徑。小表新增 column 可能很快；大表 rebuild、index 重建或 vacuum 類操作會讓 app 啟動、CLI command 或 API cold start 變慢。</p>
<p>修正方向是先估資料量。短 migration 可在 startup；長 migration 要有 explicit command、progress、backup 與 rollback route。</p>
<h3 id="case-2fixture-schema-升級漏掉-production-gap">Case 2：fixture schema 升級漏掉 production gap</h3>
<p>Fixture schema drift 的核心風險是測試 DB 和 production DB 的 dialect / constraint 不一致。SQLite fixture 很快，但 production 若是 PostgreSQL / MySQL，type、date、NULL、constraint 與 transaction 行為都可能不同。</p>
<p>修正方向是把 SQLite fixture 明確標成 contract test 層。Repository error mapping、domain invariant 可以用 SQLite；production-specific SQL 要用 production database container 驗證。</p>
<h3 id="case-3直接改-sqlite_schema">Case 3：直接改 <code>sqlite_schema</code></h3>
<p>直接改 <code>sqlite_schema</code> 的核心風險是產生語法正確但語意破壞的 database file。SQLite 官方文件提供 writable schema route，但同時強調錯誤修改可能讓 database corrupt / unreadable。</p>
<p>修正方向是讓 writable schema 成為最後手段。一般 migration 優先用 ALTER TABLE 或 table rebuild；需要特殊修復時先複製原檔，在副本驗證。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite migration runbook 至少要記錄：</p>
<ol>
<li>DB file 目前 <code>user_version</code> 與 application release version。</li>
<li>Migration 是否可重入、是否可中斷後恢復。</li>
<li>Migration 前 backup / snapshot 位置。</li>
<li>需要 table rebuild 的 table、資料量、index / trigger / view 清單。</li>
<li>Validation query、row count、foreign key check、integrity check。</li>
<li>舊 binary / 新 binary 的相容策略。</li>
<li>Fixture DB 是否已重新產生並被 contract test 使用。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/" data-link-title="SQLite Migration Fixture Lab" data-link-desc="SQLite user_version、table rebuild migration、fixture snapshot、rollback note 與 CI evidence 的操作說明">Migration fixture lab</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice</a></li>
<li>遷移：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></li>
<li>官方：<a href="https://www.sqlite.org/lang_altertable.html">SQLite ALTER TABLE</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite SQL Dialect and Index Limits</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/</guid><description>&lt;p>SQLite SQL dialect and index limits 的核心責任是說明 SQLite 和 server SQL 的語意差異。SQLite 可以執行大量 SQL，也支援 transaction、index、trigger、view、window function 與 JSON；但它的 typing、constraint、file-level operation、query planner 與 extension model 會影響測試可信度、migration 成本與 production adapter。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 測過代表某個 repository contract 在 SQLite 語意下成立。當 production target 是 PostgreSQL、MySQL、D1、Turso 或其他 server database 時，測試與 migration 要補上 dialect gap evidence。&lt;/p>
&lt;h2 id="type-affinity">Type Affinity&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/type-affinity/" data-link-title="Type Affinity" data-link-desc="說明 SQLite 如何用 type affinity 決定欄位的型別傾向與值的儲存方式">Type affinity&lt;/a> 的核心責任是定義資料寫入時如何被保存與比較。SQLite 官方 &lt;a href="https://www.sqlite.org/datatype3.html">Datatypes&lt;/a> 文件說明 SQLite 使用 dynamic typing，型別關聯在 value 層與 column affinity 層共同作用；&lt;a href="https://www.sqlite.org/stricttables.html">STRICT tables&lt;/a> 則提供較嚴格的型別檢查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>SQLite 行為重點&lt;/th>
 &lt;th>Production 影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Integer&lt;/td>
 &lt;td>value type 可依寫入內容變化&lt;/td>
 &lt;td>test fixture 可能放過錯誤型別&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Text&lt;/td>
 &lt;td>collation 與比較語意需明確設定&lt;/td>
 &lt;td>排序、大小寫、unique 判斷要對照 target DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Date/time&lt;/td>
 &lt;td>常以 TEXT / REAL / INTEGER 表示&lt;/td>
 &lt;td>timezone、range query、serialization 要一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Boolean&lt;/td>
 &lt;td>常以 integer convention 表示&lt;/td>
 &lt;td>adapter 要定義 true / false encoding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>STRICT&lt;/td>
 &lt;td>提供更接近 server DB 的型別 guard&lt;/td>
 &lt;td>適合作為 fixture 預設，仍需 production test&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type affinity 的教學重點是把資料合約放在 application boundary。若 domain 說 &lt;code>created_at&lt;/code> 是 timestamp，就要定義 storage format、timezone、precision、comparison query 與 serialization，而非只讓 SQLite 接受任意 value。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">total_cents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CHECK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">total_cents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">STRICT&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 schema 用 &lt;code>STRICT&lt;/code>、&lt;code>NOT NULL&lt;/code> 與 &lt;code>CHECK&lt;/code> 讓 fixture 更接近正式資料合約。Production target 仍要跑 PostgreSQL / MySQL container test，確認 timestamp、integer range 與 constraint error mapping。&lt;/p>
&lt;h2 id="constraint-behavior">Constraint Behavior&lt;/h2>
&lt;p>Constraint behavior 的核心責任是確保資料完整性由 database 和 application 共同維護。SQLite 支援 primary key、unique、check、foreign key 與 deferred constraint，但 foreign key enforcement 需要明確啟用，migration / test runner 也要確認連線設定。&lt;/p></description><content:encoded><![CDATA[<p>SQLite SQL dialect and index limits 的核心責任是說明 SQLite 和 server SQL 的語意差異。SQLite 可以執行大量 SQL，也支援 transaction、index、trigger、view、window function 與 JSON；但它的 typing、constraint、file-level operation、query planner 與 extension model 會影響測試可信度、migration 成本與 production adapter。</p>
<p>本文的判讀錨點是：SQLite 測過代表某個 repository contract 在 SQLite 語意下成立。當 production target 是 PostgreSQL、MySQL、D1、Turso 或其他 server database 時，測試與 migration 要補上 dialect gap evidence。</p>
<h2 id="type-affinity">Type Affinity</h2>
<p><a href="/blog/backend/knowledge-cards/type-affinity/" data-link-title="Type Affinity" data-link-desc="說明 SQLite 如何用 type affinity 決定欄位的型別傾向與值的儲存方式">Type affinity</a> 的核心責任是定義資料寫入時如何被保存與比較。SQLite 官方 <a href="https://www.sqlite.org/datatype3.html">Datatypes</a> 文件說明 SQLite 使用 dynamic typing，型別關聯在 value 層與 column affinity 層共同作用；<a href="https://www.sqlite.org/stricttables.html">STRICT tables</a> 則提供較嚴格的型別檢查。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>SQLite 行為重點</th>
          <th>Production 影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Integer</td>
          <td>value type 可依寫入內容變化</td>
          <td>test fixture 可能放過錯誤型別</td>
      </tr>
      <tr>
          <td>Text</td>
          <td>collation 與比較語意需明確設定</td>
          <td>排序、大小寫、unique 判斷要對照 target DB</td>
      </tr>
      <tr>
          <td>Date/time</td>
          <td>常以 TEXT / REAL / INTEGER 表示</td>
          <td>timezone、range query、serialization 要一致</td>
      </tr>
      <tr>
          <td>Boolean</td>
          <td>常以 integer convention 表示</td>
          <td>adapter 要定義 true / false encoding</td>
      </tr>
      <tr>
          <td>STRICT</td>
          <td>提供更接近 server DB 的型別 guard</td>
          <td>適合作為 fixture 預設，仍需 production test</td>
      </tr>
  </tbody>
</table>
<p>Type affinity 的教學重點是把資料合約放在 application boundary。若 domain 說 <code>created_at</code> 是 timestamp，就要定義 storage format、timezone、precision、comparison query 與 serialization，而非只讓 SQLite 接受任意 value。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="n">total_cents</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">total_cents</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">STRICT</span><span class="p">;</span></span></span></code></pre></div><p>這段 schema 用 <code>STRICT</code>、<code>NOT NULL</code> 與 <code>CHECK</code> 讓 fixture 更接近正式資料合約。Production target 仍要跑 PostgreSQL / MySQL container test，確認 timestamp、integer range 與 constraint error mapping。</p>
<h2 id="constraint-behavior">Constraint Behavior</h2>
<p>Constraint behavior 的核心責任是確保資料完整性由 database 和 application 共同維護。SQLite 支援 primary key、unique、check、foreign key 與 deferred constraint，但 foreign key enforcement 需要明確啟用，migration / test runner 也要確認連線設定。</p>
<table>
  <thead>
      <tr>
          <th>Constraint</th>
          <th>SQLite 審查點</th>
          <th>操作判準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Foreign key</td>
          <td><code>PRAGMA foreign_keys = ON</code></td>
          <td>每個 connection / test setup 都要驗證</td>
      </tr>
      <tr>
          <td>Unique</td>
          <td>NULL、collation、expression</td>
          <td>對照 target DB 的 NULL uniqueness 與 collation</td>
      </tr>
      <tr>
          <td>Check</td>
          <td>type affinity 互動</td>
          <td>用 domain invalid case 驗證</td>
      </tr>
      <tr>
          <td>Deferred</td>
          <td>transaction boundary</td>
          <td>用 multi-step workflow 測 commit-time failure</td>
      </tr>
  </tbody>
</table>
<p>Foreign key 是 SQLite fixture 最常漏掉的設定。每個測試連線開啟後應立刻查 <code>PRAGMA foreign_keys;</code>，並用一個故意違反 FK 的 fixture case 確認錯誤會出現。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pragma_foreign_keys</span><span class="p">;</span></span></span></code></pre></div><p>Constraint error 要在 repository adapter 層被歸類。若 production target 會把 duplicate key、foreign key、check violation 映射成不同 error code，SQLite fixture 也要至少保留 domain-level classification test。</p>
<h2 id="transaction-behavior">Transaction Behavior</h2>
<p>Transaction behavior 的核心責任是定義讀寫隔離、savepoint、nested workflow 與 retry。SQLite 官方 <a href="https://www.sqlite.org/isolation.html">isolation</a> 文件說明 connection 之間的隔離語意；WAL mode 下 reader / writer behavior 也會影響 concurrent test。</p>
<table>
  <thead>
      <tr>
          <th>行為</th>
          <th>SQLite 判讀</th>
          <th>測試影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single writer</td>
          <td>同一時間只有一個 writer 取得寫鎖</td>
          <td>concurrent writer test 要顯式設計</td>
      </tr>
      <tr>
          <td>Snapshot read</td>
          <td>WAL mode 下 reader 可讀舊 snapshot</td>
          <td>freshness 與 read-after-write 要分開測</td>
      </tr>
      <tr>
          <td>Savepoint</td>
          <td>適合 nested workflow</td>
          <td>repository transaction helper 要支援</td>
      </tr>
      <tr>
          <td>Busy timeout</td>
          <td>lock wait policy</td>
          <td>integration test 要設定固定 timeout</td>
      </tr>
  </tbody>
</table>
<p>Savepoint 可以讓 application 實作可組合的 transaction helper。若上層 workflow 已在 transaction 內，內層 repository 可以使用 savepoint 承接局部 rollback，而非開另一個 database transaction。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">create_order</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">,</span><span class="w"> </span><span class="n">total_cents</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-05-21T00:00:00Z&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">1200</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">RELEASE</span><span class="w"> </span><span class="n">create_order</span><span class="p">;</span></span></span></code></pre></div><p>Busy timeout 是測試穩定性的關鍵設定。若 fixture 會平行跑測試，應每個 temp DB 獨立，或在專門 concurrency lab 裡測 <code>SQLITE_BUSY</code>；一般 contract test 要追求 deterministic result。</p>
<h2 id="index-model">Index Model</h2>
<p>Index model 的核心責任是把查詢形狀與資料量變成可觀測的計畫。SQLite 支援 B-tree index、covering index、partial index、expression index 與 query planner；但 planner choice、統計資訊與 function support 會和 target DB 不同。</p>
<table>
  <thead>
      <tr>
          <th>Index 類型</th>
          <th>適用情境</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Composite index</td>
          <td>多欄位 equality / range query</td>
          <td>欄位順序是否符合主要 query pattern</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>active / pending / soft-delete row</td>
          <td>predicate 是否穩定、target DB 是否支援</td>
      </tr>
      <tr>
          <td>Expression index</td>
          <td>normalized email、date bucket</td>
          <td>function deterministic 與 migration 支援</td>
      </tr>
      <tr>
          <td>Covering index</td>
          <td>read-mostly list page</td>
          <td>index size 與 write overhead</td>
      </tr>
  </tbody>
</table>
<p>Index review 要從 query pattern 開始，而非從「常用欄位」開始。SQLite 可以用 <code>EXPLAIN QUERY PLAN</code> 檢查是否掃 index；production target 要用自己的 explain 工具重跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">PLAN</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">total_cents</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-05-01T00:00:00Z&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">50</span><span class="p">;</span></span></span></code></pre></div><p>Index drift 是 migration 的常見風險。SQLite fixture 裡的 index 可以讓測試變快，但若 production schema 缺少同等 index，正式服務會在資料量成長後出現 latency spike；因此 index 要進入 schema diff audit。</p>
<h2 id="dialect-gap">Dialect Gap</h2>
<p>Dialect gap 的核心責任是把 SQLite 與 target database 的差異寫成 matrix。這份 matrix 應跟 repository adapter、migration plan 與 CI test suite 綁定。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>SQLite 審查點</th>
          <th>對照路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ALTER TABLE</td>
          <td>支援範圍、table rebuild</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a></td>
      </tr>
      <tr>
          <td>JSON</td>
          <td>function availability、index support</td>
          <td>production container test</td>
      </tr>
      <tr>
          <td>Generated column</td>
          <td>expression、storage、index</td>
          <td>migration dry run</td>
      </tr>
      <tr>
          <td>Window function</td>
          <td>target DB 支援與 planner</td>
          <td>query compatibility suite</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>FTS、vector、custom function</td>
          <td>vendor extension policy</td>
      </tr>
  </tbody>
</table>
<p>Dialect matrix 要以 query contract 為單位。每個 repository method 至少列出 SQL feature、SQLite behavior、production behavior、test layer 與 fallback strategy。</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">Contract: Search active documents by tenant and prefix
</span></span><span class="line"><span class="ln">2</span><span class="cl">SQLite: FTS5 virtual table in fixture
</span></span><span class="line"><span class="ln">3</span><span class="cl">PostgreSQL: tsvector + GIN index
</span></span><span class="line"><span class="ln">4</span><span class="cl">Risk: ranking / tokenizer / collation differ
</span></span><span class="line"><span class="ln">5</span><span class="cl">Evidence: golden result set + production container explain</span></span></code></pre></div><p>這種寫法讓測試負責驗證 domain contract，避免把兩個 SQL engine 的搜尋語意視為完全一致。</p>
<h2 id="test--migration-impact">Test / Migration Impact</h2>
<p>Test / migration impact 的核心責任是決定哪些東西可以用 SQLite 快速驗證，哪些東西要交給 production-like database。SQLite 很適合 repository contract、migration fixture、local development 與 file lifecycle drill；涉及 planner、extension、collation、locking、permission、role 與 HA 時，需要追加 target DB evidence。</p>
<table>
  <thead>
      <tr>
          <th>測試層</th>
          <th>SQLite 適合度</th>
          <th>必補 evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Domain repository</td>
          <td>高</td>
          <td>invalid data、constraint、transaction case</td>
      </tr>
      <tr>
          <td>Migration syntax</td>
          <td>中</td>
          <td>target DB dry run</td>
      </tr>
      <tr>
          <td>Query performance</td>
          <td>中</td>
          <td>target DB explain + realistic data volume</td>
      </tr>
      <tr>
          <td>Permission / role</td>
          <td>低</td>
          <td>server DB integration test</td>
      </tr>
      <tr>
          <td>HA / failover</td>
          <td>低</td>
          <td>vendor-specific drill</td>
      </tr>
  </tbody>
</table>
<p>SQLite fixture 的價值在於快、穩、便宜。它應承擔「資料合約是否被 repository 保護」；production container 或 staging database 承擔「正式 engine 是否用同樣方式執行」。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>SQL dialect and index limits 完成後，下一步要把 gap 接到實作層。測試設計讀 <a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice</a>；migration 實作讀 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a>；要升級到 PostgreSQL，讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a>。</p>
]]></content:encoded></item><item><title>SQLite Teaching Structure</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/teaching-structure/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/teaching-structure/</guid><description>&lt;p>SQLite teaching structure 的核心責任是把 SQLite 從單篇 vendor overview 擴成可教學的服務章節群。PostgreSQL / MySQL 的完整度來自 overview、deep article、migration playbook 與案例路由；SQLite 的完整度也要保留同樣層級，但正文重點要貼合它自己的服務語言：single file、embedded process、writer boundary、backup / restore、test fixture、local-first 與 edge SQLite 變體。&lt;/p>
&lt;h2 id="完成標準">完成標準&lt;/h2>
&lt;p>SQLite 章節群的完成標準是讀者能回答三個問題。第一，SQLite 何時是正式狀態而非臨時檔案；第二，SQLite production 化後要如何處理 WAL、backup、restore、migration、測試與觀測；第三，SQLite 成長後該升到 PostgreSQL / MySQL、Cloudflare D1、Turso / libSQL、Litestream / LiteFS 或 mobile sync。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>SQLite 對應文件&lt;/th>
 &lt;th>教學責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Service overview&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a>&lt;/td>
 &lt;td>第一輪服務定位、適用壓力、替代邊界與下一步路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Core deep article&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>&lt;/td>
 &lt;td>WAL sidecar、backup API、restore drill、corruption recovery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hands-on&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on&lt;/a>&lt;/td>
 &lt;td>local file、backup restore、WAL busy、migration fixture&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operations&lt;/td>
 &lt;td>WAL / locking、PRAGMA tuning、schema migration、observability&lt;/td>
 &lt;td>日常設定、排錯、容量訊號與 release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application shape&lt;/td>
 &lt;td>test fixture、mobile / desktop store、local-first sync&lt;/td>
 &lt;td>SQLite 跟 application process / device / test workflow 的關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge / variants&lt;/td>
 &lt;td>D1 / Turso / libSQL、Litestream / LiteFS&lt;/td>
 &lt;td>分散式或 replicated SQLite 變體的責任邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration route&lt;/td>
 &lt;td>SQLite → PostgreSQL、SQLite → D1 / Turso、PostgreSQL → SQLite&lt;/td>
 &lt;td>成長、edge 化或降操作成本時的階段化搬遷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這份結構的重點是避免把 SQLite 寫成小型 PostgreSQL。SQLite deep article 要先處理檔案、process、filesystem、device、test 與 edge runtime；SQL dialect、index 與 migration 工具只有在這些責任成立後才展開。&lt;/p>
&lt;h2 id="推薦撰寫順序">推薦撰寫順序&lt;/h2>
&lt;p>撰寫順序要從正式狀態的最低操作責任開始，再逐步擴到應用形狀、edge 變體與 migration。&lt;/p>
&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>1&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>先回答 SQLite 如何成為可恢復的正式狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>writer boundary 是 SQLite production 判斷的核心&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 journal、sync、cache、mmap 轉成可驗證的設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>單檔案 DB 仍需要版本、rollback 與 app release 配合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test fixture best practice&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>SQLite 最常被語言教材引用，需要明確 production gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / desktop embedded store&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>說明 device local state、backup、sync 與 privacy 責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>7&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 single-device SQLite 與 multi-device sync 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>8&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>edge SQLite 變體需要獨立比較，和本地 SQLite 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>9&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>backup / read replica / failover 的語意要跟 multi-write 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>對照 PostgreSQL / MySQL 測試與 migration gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>11&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 SQLite 的低操作成本補成可交接 evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>12&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">Hands-on 操作路線&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 local file、backup、WAL busy、migration fixture 變成演練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>13&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>多 tenant、權限、HA、schema governance 出現時的主要升級路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>14&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso route&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>edge / serverless 化時的 migration route&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>15&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/" data-link-title="PostgreSQL to SQLite Simplification" data-link-desc="PostgreSQL 降低操作成本轉向 SQLite 的適用條件、資料責任縮小、export/import、runbook 與 no-go condition">PostgreSQL to SQLite simplification&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>小型工具、single-user app 或 embedded 需求的反向路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個順序讓 SQLite 先完成自己的核心語言，再處理相鄰產品。D1、Turso、LiteFS、Litestream 都帶有 SQLite 相容性，但教學上要先問它們承擔的是 backup、replication、edge locality、read replica 還是 distributed write。&lt;/p></description><content:encoded><![CDATA[<p>SQLite teaching structure 的核心責任是把 SQLite 從單篇 vendor overview 擴成可教學的服務章節群。PostgreSQL / MySQL 的完整度來自 overview、deep article、migration playbook 與案例路由；SQLite 的完整度也要保留同樣層級，但正文重點要貼合它自己的服務語言：single file、embedded process、writer boundary、backup / restore、test fixture、local-first 與 edge SQLite 變體。</p>
<h2 id="完成標準">完成標準</h2>
<p>SQLite 章節群的完成標準是讀者能回答三個問題。第一，SQLite 何時是正式狀態而非臨時檔案；第二，SQLite production 化後要如何處理 WAL、backup、restore、migration、測試與觀測；第三，SQLite 成長後該升到 PostgreSQL / MySQL、Cloudflare D1、Turso / libSQL、Litestream / LiteFS 或 mobile sync。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>SQLite 對應文件</th>
          <th>教學責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Service overview</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a></td>
          <td>第一輪服務定位、適用壓力、替代邊界與下一步路由</td>
      </tr>
      <tr>
          <td>Core deep article</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></td>
          <td>WAL sidecar、backup API、restore drill、corruption recovery</td>
      </tr>
      <tr>
          <td>Hands-on</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></td>
          <td>local file、backup restore、WAL busy、migration fixture</td>
      </tr>
      <tr>
          <td>Operations</td>
          <td>WAL / locking、PRAGMA tuning、schema migration、observability</td>
          <td>日常設定、排錯、容量訊號與 release gate</td>
      </tr>
      <tr>
          <td>Application shape</td>
          <td>test fixture、mobile / desktop store、local-first sync</td>
          <td>SQLite 跟 application process / device / test workflow 的關係</td>
      </tr>
      <tr>
          <td>Edge / variants</td>
          <td>D1 / Turso / libSQL、Litestream / LiteFS</td>
          <td>分散式或 replicated SQLite 變體的責任邊界</td>
      </tr>
      <tr>
          <td>Migration route</td>
          <td>SQLite → PostgreSQL、SQLite → D1 / Turso、PostgreSQL → SQLite</td>
          <td>成長、edge 化或降操作成本時的階段化搬遷</td>
      </tr>
  </tbody>
</table>
<p>這份結構的重點是避免把 SQLite 寫成小型 PostgreSQL。SQLite deep article 要先處理檔案、process、filesystem、device、test 與 edge runtime；SQL dialect、index 與 migration 工具只有在這些責任成立後才展開。</p>
<h2 id="推薦撰寫順序">推薦撰寫順序</h2>
<p>撰寫順序要從正式狀態的最低操作責任開始，再逐步擴到應用形狀、edge 變體與 migration。</p>
<table>
  <thead>
      <tr>
          <th>順序</th>
          <th>文件</th>
          <th>狀態</th>
          <th>為什麼排在這裡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></td>
          <td>已有正文</td>
          <td>先回答 SQLite 如何成為可恢復的正式狀態</td>
      </tr>
      <tr>
          <td>2</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a></td>
          <td>已有正文</td>
          <td>writer boundary 是 SQLite production 判斷的核心</td>
      </tr>
      <tr>
          <td>3</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance</a></td>
          <td>已有正文</td>
          <td>把 journal、sync、cache、mmap 轉成可驗證的設定</td>
      </tr>
      <tr>
          <td>4</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a></td>
          <td>已有正文</td>
          <td>單檔案 DB 仍需要版本、rollback 與 app release 配合</td>
      </tr>
      <tr>
          <td>5</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test fixture best practice</a></td>
          <td>已有正文</td>
          <td>SQLite 最常被語言教材引用，需要明確 production gap</td>
      </tr>
      <tr>
          <td>6</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / desktop embedded store</a></td>
          <td>已有正文</td>
          <td>說明 device local state、backup、sync 與 privacy 責任</td>
      </tr>
      <tr>
          <td>7</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary</a></td>
          <td>已有正文</td>
          <td>把 single-device SQLite 與 multi-device sync 分開</td>
      </tr>
      <tr>
          <td>8</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a></td>
          <td>已有正文</td>
          <td>edge SQLite 變體需要獨立比較，和本地 SQLite 分開</td>
      </tr>
      <tr>
          <td>9</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication</a></td>
          <td>已有正文</td>
          <td>backup / read replica / failover 的語意要跟 multi-write 分開</td>
      </tr>
      <tr>
          <td>10</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits</a></td>
          <td>已有正文</td>
          <td>對照 PostgreSQL / MySQL 測試與 migration gap</td>
      </tr>
      <tr>
          <td>11</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></td>
          <td>已有正文</td>
          <td>把 SQLite 的低操作成本補成可交接 evidence</td>
      </tr>
      <tr>
          <td>12</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">Hands-on 操作路線</a></td>
          <td>已有正文</td>
          <td>把 local file、backup、WAL busy、migration fixture 變成演練</td>
      </tr>
      <tr>
          <td>13</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a></td>
          <td>已有正文</td>
          <td>多 tenant、權限、HA、schema governance 出現時的主要升級路徑</td>
      </tr>
      <tr>
          <td>14</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso route</a></td>
          <td>已有正文</td>
          <td>edge / serverless 化時的 migration route</td>
      </tr>
      <tr>
          <td>15</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/" data-link-title="PostgreSQL to SQLite Simplification" data-link-desc="PostgreSQL 降低操作成本轉向 SQLite 的適用條件、資料責任縮小、export/import、runbook 與 no-go condition">PostgreSQL to SQLite simplification</a></td>
          <td>已有正文</td>
          <td>小型工具、single-user app 或 embedded 需求的反向路徑</td>
      </tr>
  </tbody>
</table>
<p>這個順序讓 SQLite 先完成自己的核心語言，再處理相鄰產品。D1、Turso、LiteFS、Litestream 都帶有 SQLite 相容性，但教學上要先問它們承擔的是 backup、replication、edge locality、read replica 還是 distributed write。</p>
<h2 id="文件命名規則">文件命名規則</h2>
<p>SQLite 章節群的檔名用服務責任命名，product-first 命名只留給 D1 / Turso / libSQL 這類 product boundary 本身就是教學主題的文件。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>命名方式</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core deep</td>
          <td><code>{mechanism}-{responsibility}</code></td>
          <td><code>wal-concurrency-locking.md</code></td>
      </tr>
      <tr>
          <td>Operation</td>
          <td><code>{operation}-{decision-signal}</code></td>
          <td><code>pragma-tuning-performance.md</code></td>
      </tr>
      <tr>
          <td>Application</td>
          <td><code>{context}-{state-role}</code></td>
          <td><code>mobile-desktop-embedded-store.md</code></td>
      </tr>
      <tr>
          <td>Variant</td>
          <td><code>{products}-comparison</code></td>
          <td><code>d1-turso-libsql-comparison.md</code></td>
      </tr>
      <tr>
          <td>Migration</td>
          <td><code>migrate-to-{target}</code></td>
          <td><code>migrate-to-postgresql.md</code></td>
      </tr>
  </tbody>
</table>
<h2 id="cross-module-路由">Cross-module 路由</h2>
<p>SQLite 章節群要固定連到四個 backend 模組。Backup / restore 連到 04 evidence 與 08 incident；test fixture 連到語言教材與 repository adapter；edge / local-first 連到 05 deployment / 07 data protection；performance tuning 連到 09 capacity。</p>
<table>
  <thead>
      <tr>
          <th>SQLite 議題</th>
          <th>主要跨模組路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup / restore</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></td>
      </tr>
      <tr>
          <td>Test fixture</td>
          <td><a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">Repository Adapter</a>、語言教材的 contract test</td>
      </tr>
      <tr>
          <td>Local-first / sync</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>、offline / device privacy</td>
      </tr>
      <tr>
          <td>Edge SQLite</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a>、deployment platform</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td><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></td>
      </tr>
  </tbody>
</table>
<h2 id="後續審查點">後續審查點</h2>
<p>SQLite 章節群完稿後要特別審查三個偏誤。第一是把 SQLite 過度美化成 production SQL 替代品；第二是把 edge SQLite 產品跟本地 SQLite 混成同一種能力；第三是把 test fixture 的便利性誤寫成 production equivalence。</p>
]]></content:encoded></item><item><title>SQLite Test Fixture Best Practice</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合作為 test fixture；本文聚焦 &lt;em>如何用 SQLite 加速測試，同時保留 production database 的語意邊界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite test fixture 的核心責任是讓 repository / adapter 測試快速、可重複、可攜帶。SQLite 的單檔特性讓 CI 可以快速建立 DB、載入 seed、跑 contract test；但它的 type affinity、SQL dialect、locking 與 constraint behavior 和 PostgreSQL / MySQL 不完全相同，因此 fixture 要被定位為一層測試工具，而非 production equivalence。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite fixture 適合驗證 application contract，不適合取代 production database compatibility test。若測試目標是 repository error mapping、domain invariant、migration fixture 或 deterministic seed，SQLite 很划算；若測試目標是 PostgreSQL extension、MySQL lock、query planner 或 SQL dialect，應使用 production-like container。&lt;/p>
&lt;h2 id="test-fixture-的位置">Test fixture 的位置&lt;/h2>
&lt;p>SQLite fixture 的服務責任是提供快、穩定、可重建的本地資料狀態。它通常位於 unit test 與 full integration test 之間，承擔 repository adapter 的 contract test。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>測試層級&lt;/th>
 &lt;th>SQLite 適合度&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Pure unit test&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>fake / in-memory object 通常更快&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Repository contract&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>驗證 CRUD、constraint mapping、transaction behavior&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service integration&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>適合簡單流程，不覆蓋 production-specific SQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production compatibility&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>用 PostgreSQL / MySQL container 或 staging DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration smoke&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>適合 fixture migration，不代表 production DDL&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是把測試目的說清楚。SQLite fixture 讓語言教材與 backend 教材接起來；語言端測 interface / adapter，backend 端保留 production database 的深度文章與 migration playbook。&lt;/p>
&lt;h2 id="fixture-lifecycle">Fixture lifecycle&lt;/h2>
&lt;p>Fixture lifecycle 的核心責任是讓每次測試拿到已知資料狀態。常見策略有三種：每 test 建新 in-memory DB、每 suite 複製 template file、每 CI job 產生 versioned fixture。&lt;/p>
&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;code>:memory:&lt;/code> per test&lt;/td>
 &lt;td>小 schema、快速 unit-like contract&lt;/td>
 &lt;td>隔離最好、清理簡單&lt;/td>
 &lt;td>跨 connection / WAL 行為不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>template file copy&lt;/td>
 &lt;td>中等 seed、需要真實檔案行為&lt;/td>
 &lt;td>快速、可測 file lifecycle&lt;/td>
 &lt;td>要避免多 test 共用同一檔案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>generated fixture&lt;/td>
 &lt;td>migration / seed 驗證&lt;/td>
 &lt;td>和 migration 同步&lt;/td>
 &lt;td>CI 時間較長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>read-only fixture&lt;/td>
 &lt;td>查詢 / report 測試&lt;/td>
 &lt;td>避免 writer collision&lt;/td>
 &lt;td>不測 mutation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Fixture file 應和 schema version 綁定。檔名、metadata 或 &lt;code>user_version&lt;/code> 要能回答「這個 fixture 對應哪個 migration 版本」，避免測試資料在多次 schema 變更後變成隱性技術債。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合作為 test fixture；本文聚焦 <em>如何用 SQLite 加速測試，同時保留 production database 的語意邊界</em>。</p></blockquote>
<p>SQLite test fixture 的核心責任是讓 repository / adapter 測試快速、可重複、可攜帶。SQLite 的單檔特性讓 CI 可以快速建立 DB、載入 seed、跑 contract test；但它的 type affinity、SQL dialect、locking 與 constraint behavior 和 PostgreSQL / MySQL 不完全相同，因此 fixture 要被定位為一層測試工具，而非 production equivalence。</p>
<p>本文的判讀錨點是：SQLite fixture 適合驗證 application contract，不適合取代 production database compatibility test。若測試目標是 repository error mapping、domain invariant、migration fixture 或 deterministic seed，SQLite 很划算；若測試目標是 PostgreSQL extension、MySQL lock、query planner 或 SQL dialect，應使用 production-like container。</p>
<h2 id="test-fixture-的位置">Test fixture 的位置</h2>
<p>SQLite fixture 的服務責任是提供快、穩定、可重建的本地資料狀態。它通常位於 unit test 與 full integration test 之間，承擔 repository adapter 的 contract test。</p>
<table>
  <thead>
      <tr>
          <th>測試層級</th>
          <th>SQLite 適合度</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure unit test</td>
          <td>低</td>
          <td>fake / in-memory object 通常更快</td>
      </tr>
      <tr>
          <td>Repository contract</td>
          <td>高</td>
          <td>驗證 CRUD、constraint mapping、transaction behavior</td>
      </tr>
      <tr>
          <td>Service integration</td>
          <td>中</td>
          <td>適合簡單流程，不覆蓋 production-specific SQL</td>
      </tr>
      <tr>
          <td>Production compatibility</td>
          <td>低</td>
          <td>用 PostgreSQL / MySQL container 或 staging DB</td>
      </tr>
      <tr>
          <td>Migration smoke</td>
          <td>中</td>
          <td>適合 fixture migration，不代表 production DDL</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是把測試目的說清楚。SQLite fixture 讓語言教材與 backend 教材接起來；語言端測 interface / adapter，backend 端保留 production database 的深度文章與 migration playbook。</p>
<h2 id="fixture-lifecycle">Fixture lifecycle</h2>
<p>Fixture lifecycle 的核心責任是讓每次測試拿到已知資料狀態。常見策略有三種：每 test 建新 in-memory DB、每 suite 複製 template file、每 CI job 產生 versioned fixture。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>適合情境</th>
          <th>優點</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>:memory:</code> per test</td>
          <td>小 schema、快速 unit-like contract</td>
          <td>隔離最好、清理簡單</td>
          <td>跨 connection / WAL 行為不同</td>
      </tr>
      <tr>
          <td>template file copy</td>
          <td>中等 seed、需要真實檔案行為</td>
          <td>快速、可測 file lifecycle</td>
          <td>要避免多 test 共用同一檔案</td>
      </tr>
      <tr>
          <td>generated fixture</td>
          <td>migration / seed 驗證</td>
          <td>和 migration 同步</td>
          <td>CI 時間較長</td>
      </tr>
      <tr>
          <td>read-only fixture</td>
          <td>查詢 / report 測試</td>
          <td>避免 writer collision</td>
          <td>不測 mutation</td>
      </tr>
  </tbody>
</table>
<p>Fixture file 應和 schema version 綁定。檔名、metadata 或 <code>user_version</code> 要能回答「這個 fixture 對應哪個 migration 版本」，避免測試資料在多次 schema 變更後變成隱性技術債。</p>
<h2 id="production-dialect-gap">Production dialect gap</h2>
<p>Production dialect gap 的核心責任是避免 SQLite 測試通過後，PostgreSQL / MySQL production 出現不同語意。SQLite 的 dynamic typing、date / time representation、foreign key pragma、ALTER TABLE 支援與 lock model 都會影響測試可信度。</p>
<table>
  <thead>
      <tr>
          <th>Gap 類型</th>
          <th>SQLite 行為</th>
          <th>Production 風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type affinity</td>
          <td>欄位有 affinity，值本身仍有 storage class</td>
          <td>PostgreSQL / MySQL type error 沒被測到</td>
      </tr>
      <tr>
          <td>Date / time</td>
          <td>常以 TEXT / REAL / INTEGER 表示</td>
          <td>timezone、precision、function 差異</td>
      </tr>
      <tr>
          <td>Foreign key</td>
          <td>需要 <code>PRAGMA foreign_keys=ON</code></td>
          <td>fixture 忘記開 FK，constraint bug 漏掉</td>
      </tr>
      <tr>
          <td>ALTER TABLE</td>
          <td>支援 subset，複雜變更需 rebuild</td>
          <td>production migration 工具行為不同</td>
      </tr>
      <tr>
          <td>Locking</td>
          <td>single-file lock / single writer</td>
          <td>server DB connection / lock model 不同</td>
      </tr>
      <tr>
          <td>SQL feature</td>
          <td>extension / JSON / index 差異</td>
          <td>vendor-specific query 需要 production evidence</td>
      </tr>
  </tbody>
</table>
<p>這張表的用法是決定哪些測試留在 SQLite，哪些要升級到 production-like DB。Repository contract 可用 SQLite；query optimization、vendor SQL、online schema change、CDC、replication、pooling 都應回到 PostgreSQL / MySQL 章節。</p>
<h2 id="contract-test-設計">Contract test 設計</h2>
<p>Contract test 的核心責任是讓不同 DB adapter 對 application 呈現同一組語意。SQLite fixture 測的是 application port 的行為，例如 duplicate key、not found、transaction rollback、pagination、domain invariant，而非底層 engine 的所有細節。</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">Repository contract
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── Create / read / update / delete
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── Unique conflict → ErrAlreadyExists
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── Missing row → ErrNotFound
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── Transaction rollback restores domain invariant
</span></span><span class="line"><span class="ln">6</span><span class="cl">├── Pagination order stable
</span></span><span class="line"><span class="ln">7</span><span class="cl">└── Migration version matches fixture</span></span></code></pre></div><p>如果 production adapter 是 PostgreSQL / MySQL，contract test 應至少在 nightly 或 CI matrix 裡跑一輪 production-like database。SQLite 提供快速回饋，production-like test 提供 dialect confidence。</p>
<h2 id="ci-evidence">CI evidence</h2>
<p>SQLite fixture 的 CI evidence 要證明資料狀態和 schema version 一致。測試失敗時，讀者要能知道是 application contract 失效、fixture 過期、migration 漏跑，還是 SQLite / production dialect gap。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fixture version</td>
          <td>對齊 migration / app release</td>
      </tr>
      <tr>
          <td>seed checksum</td>
          <td>確認測試資料穩定</td>
      </tr>
      <tr>
          <td>migration log</td>
          <td>確認 fixture 可由 migration 重建</td>
      </tr>
      <tr>
          <td>contract test output</td>
          <td>確認 repository behavior</td>
      </tr>
      <tr>
          <td>dialect gap note</td>
          <td>標示未覆蓋 production behavior</td>
      </tr>
  </tbody>
</table>
<p>CI 產物不一定要很複雜，但要能被下一個維護者重建。SQLite fixture 的優勢是可攜帶；若 fixture 只能靠某個人的本機狀態生成，就失去教學與維護價值。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1共用同一個-db-檔跑平行測試">Case 1：共用同一個 <code>.db</code> 檔跑平行測試</h3>
<p>平行測試共用檔案的核心風險是 test runner 製造和 production 不同的 writer collision。測試偶發 <code>SQLITE_BUSY</code>，團隊可能以為 application 有 race；實際上是測試隔離不足。</p>
<p>修正方向是 per-test temp DB 或 read-only template copy。需要測 WAL / busy 行為時，用專門 hands-on lab，讓一般 contract test 專注在 repository contract。</p>
<h3 id="case-2忘記開-foreign-keys">Case 2：忘記開 foreign keys</h3>
<p>Foreign key pragma 漏開的核心風險是 constraint bug 被 fixture 隱藏。SQLite foreign key enforcement 需要明確啟用；若 production DB 一定 enforce FK，fixture 也要在 connection initialization 中開啟。</p>
<p>修正方向是 baseline PRAGMA 和 startup assertion。每個 test DB open 後都跑 <code>PRAGMA foreign_keys</code> 並驗證結果。</p>
<h3 id="case-3sqlite-fixture-掩蓋-vendor-specific-sql">Case 3：SQLite fixture 掩蓋 vendor-specific SQL</h3>
<p>Vendor-specific SQL 被 SQLite 掩蓋的核心風險是 query 到 production 才失敗。例如 PostgreSQL JSONB、partial index、full-text search 或 MySQL generated column、optimizer hint 都應在 vendor DB 測。</p>
<p>修正方向是把 SQL 分層。Portable repository contract 可以用 SQLite；vendor-specific query 要有 PostgreSQL / MySQL test container。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite fixture 設計前要回答：</p>
<ol>
<li>這個測試驗證 application contract 還是 production dialect。</li>
<li>Fixture 是 in-memory、template copy、generated file 還是 read-only。</li>
<li><code>PRAGMA foreign_keys</code>、<code>journal_mode</code>、<code>busy_timeout</code> 是否固定。</li>
<li>Fixture version 如何對齊 migration version。</li>
<li>Parallel test 是否每個 worker 有獨立 DB file。</li>
<li>哪些 query 必須在 PostgreSQL / MySQL container 再跑。</li>
<li>CI artifact 是否保留 migration log 與 dialect gap note。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">Repository Adapter</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a>、<a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL Dialect and Index Limits</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/" data-link-title="SQLite Migration Fixture Lab" data-link-desc="SQLite user_version、table rebuild migration、fixture snapshot、rollback note 與 CI evidence 的操作說明">Migration Fixture Lab</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a></li>
<li>官方：<a href="https://www.sqlite.org/datatype3.html">SQLite Datatypes</a>、<a href="https://www.sqlite.org/stricttables.html">SQLite STRICT Tables</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite to D1 / Turso Migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/</guid><description>&lt;p>SQLite to D1 / Turso migration 的核心責任是把 local SQLite 轉成 edge / serverless / distributed SQLite-compatible product。這條路線的 driver 通常是 edge locality、Workers integration、managed operation、global read latency、embedded replica 或 serverless deployment workflow。&lt;/p>
&lt;p>本文的判讀錨點是：D1 / Turso migration 是 runtime boundary 變更。Local file 直連變成 platform binding、remote endpoint 或 embedded replica；因此 migration 要同時審查 SQL support、data movement、driver API、auth、latency、freshness、backup 與 vendor exit。&lt;/p>
&lt;h2 id="migration-drivers">Migration Drivers&lt;/h2>
&lt;p>Migration drivers 的核心責任是確認 edge SQLite 產品解決的是哪個服務壓力。D1 與 Turso / libSQL 都接近 SQLite experience，但它們的採用理由應寫成具體 workload。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>適合產品&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Workers integration&lt;/td>
 &lt;td>Cloudflare D1&lt;/td>
 &lt;td>App 已在 Workers、資料量小、query 清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serverless low ops&lt;/td>
 &lt;td>D1 / Turso&lt;/td>
 &lt;td>不想維護 host DB、可接受 platform limit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Low-latency read&lt;/td>
 &lt;td>Turso / embedded replica&lt;/td>
 &lt;td>read-heavy、freshness window 明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge-local app&lt;/td>
 &lt;td>D1 / Turso&lt;/td>
 &lt;td>使用者分散、write rate 可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Portable SQLite base&lt;/td>
 &lt;td>Turso / libSQL&lt;/td>
 &lt;td>想保留 SQLite-like schema 與 local dev&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>D1 的 migration driver 要和 Cloudflare platform 綁定。若 app 已用 Workers routing、KV、Queues 或 Pages，D1 可以降低跨平台整合成本；若 app 不在 Cloudflare 生態，D1 的價值要用 latency、operation 與成本證明。&lt;/p>
&lt;p>Turso / libSQL 的 migration driver 要和 replica freshness 綁定。若使用者需要 local read speed，embedded replica 有價值；若產品要求每次讀都立即看到最新 global state，就要先設計 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-after-write/" data-link-title="Read-After-Write Consistency" data-link-desc="說明寫入後能否立即讀到該筆寫入的一致性保證">read-after-write&lt;/a> path。&lt;/p>
&lt;h2 id="compatibility-audit">Compatibility Audit&lt;/h2>
&lt;p>Compatibility audit 的核心責任是確認 local SQLite schema、query 與 migration workflow 可在 target product 上運作。官方文件要作為 limits 與 feature 的單一來源：D1 參考 &lt;a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs&lt;/a> 與 &lt;a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits&lt;/a>；Turso 參考 &lt;a href="https://docs.turso.tech/">Turso docs&lt;/a> 與 libSQL client reference。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;th>Evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SQL support&lt;/td>
 &lt;td>schema、trigger、index、JSON、FK&lt;/td>
 &lt;td>migration dry run、query suite&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Size / batch&lt;/td>
 &lt;td>import file、query duration、batch size&lt;/td>
 &lt;td>limit review、sample import&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Driver API&lt;/td>
 &lt;td>local file path 變成 binding / endpoint&lt;/td>
 &lt;td>repository adapter test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auth&lt;/td>
 &lt;td>token、binding、environment secret&lt;/td>
 &lt;td>staging deployment&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>request boundary、retry、write location&lt;/td>
 &lt;td>failure injection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup&lt;/td>
 &lt;td>export、restore、retention&lt;/td>
 &lt;td>restore drill&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Compatibility audit 要以 production query 為單位。只跑 &lt;code>CREATE TABLE&lt;/code> 會漏掉最重要的差異；query suite 要包含 list page、pagination、unique violation、FK violation、transaction rollback、large batch 與 slow query。&lt;/p></description><content:encoded><![CDATA[<p>SQLite to D1 / Turso migration 的核心責任是把 local SQLite 轉成 edge / serverless / distributed SQLite-compatible product。這條路線的 driver 通常是 edge locality、Workers integration、managed operation、global read latency、embedded replica 或 serverless deployment workflow。</p>
<p>本文的判讀錨點是：D1 / Turso migration 是 runtime boundary 變更。Local file 直連變成 platform binding、remote endpoint 或 embedded replica；因此 migration 要同時審查 SQL support、data movement、driver API、auth、latency、freshness、backup 與 vendor exit。</p>
<h2 id="migration-drivers">Migration Drivers</h2>
<p>Migration drivers 的核心責任是確認 edge SQLite 產品解決的是哪個服務壓力。D1 與 Turso / libSQL 都接近 SQLite experience，但它們的採用理由應寫成具體 workload。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>適合產品</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers integration</td>
          <td>Cloudflare D1</td>
          <td>App 已在 Workers、資料量小、query 清楚</td>
      </tr>
      <tr>
          <td>Serverless low ops</td>
          <td>D1 / Turso</td>
          <td>不想維護 host DB、可接受 platform limit</td>
      </tr>
      <tr>
          <td>Low-latency read</td>
          <td>Turso / embedded replica</td>
          <td>read-heavy、freshness window 明確</td>
      </tr>
      <tr>
          <td>Edge-local app</td>
          <td>D1 / Turso</td>
          <td>使用者分散、write rate 可控</td>
      </tr>
      <tr>
          <td>Portable SQLite base</td>
          <td>Turso / libSQL</td>
          <td>想保留 SQLite-like schema 與 local dev</td>
      </tr>
  </tbody>
</table>
<p>D1 的 migration driver 要和 Cloudflare platform 綁定。若 app 已用 Workers routing、KV、Queues 或 Pages，D1 可以降低跨平台整合成本；若 app 不在 Cloudflare 生態，D1 的價值要用 latency、operation 與成本證明。</p>
<p>Turso / libSQL 的 migration driver 要和 replica freshness 綁定。若使用者需要 local read speed，embedded replica 有價值；若產品要求每次讀都立即看到最新 global state，就要先設計 <a href="/blog/backend/knowledge-cards/read-after-write/" data-link-title="Read-After-Write Consistency" data-link-desc="說明寫入後能否立即讀到該筆寫入的一致性保證">read-after-write</a> path。</p>
<h2 id="compatibility-audit">Compatibility Audit</h2>
<p>Compatibility audit 的核心責任是確認 local SQLite schema、query 與 migration workflow 可在 target product 上運作。官方文件要作為 limits 與 feature 的單一來源：D1 參考 <a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs</a> 與 <a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits</a>；Turso 參考 <a href="https://docs.turso.tech/">Turso docs</a> 與 libSQL client reference。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>審查問題</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL support</td>
          <td>schema、trigger、index、JSON、FK</td>
          <td>migration dry run、query suite</td>
      </tr>
      <tr>
          <td>Size / batch</td>
          <td>import file、query duration、batch size</td>
          <td>limit review、sample import</td>
      </tr>
      <tr>
          <td>Driver API</td>
          <td>local file path 變成 binding / endpoint</td>
          <td>repository adapter test</td>
      </tr>
      <tr>
          <td>Auth</td>
          <td>token、binding、environment secret</td>
          <td>staging deployment</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>request boundary、retry、write location</td>
          <td>failure injection</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>export、restore、retention</td>
          <td>restore drill</td>
      </tr>
  </tbody>
</table>
<p>Compatibility audit 要以 production query 為單位。只跑 <code>CREATE TABLE</code> 會漏掉最重要的差異；query suite 要包含 list page、pagination、unique violation、FK violation、transaction rollback、large batch 與 slow query。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是把 SQLite file 轉成 target platform 可接受的 seed。Local SQLite 可以先 export 成 SQL dump、CSV 或 platform CLI 支援的 import format，再進 target product。</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">sqlite3 app.db <span class="s2">&#34;.dump&#34;</span> &gt; seed.sql</span></span></code></pre></div><p>這段命令只是 seed 起點。正式流程要處理 schema ordering、unsupported SQL、large transaction、batch split、sensitive data masking、import duration、row count 與 checksum。</p>
<p>D1 migration 要把 Wrangler / platform workflow 納入 runbook。Cloudflare D1 的 limits 文件列出 import 與 query 限制；大型資料變更應切 batch，並在 preview / staging database 跑完整 dry run。</p>
<p>Turso migration 要把 remote database 與 embedded replica 分開驗證。Seed 完 remote primary 後，要測 local embedded replica 的 bootstrap、sync、read freshness、write delegation 與 offline behavior。</p>
<h2 id="application-change">Application Change</h2>
<p>Application change 的核心責任是把 database access 從 file path 改成可替換 adapter。Local SQLite 常用 file path 與 process-local connection；D1 / Turso 會加入 binding、endpoint、token、client SDK、network failure 與 platform runtime。</p>
<table>
  <thead>
      <tr>
          <th>改動層</th>
          <th>Local SQLite</th>
          <th>D1 / Turso route</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Connection</td>
          <td>file path</td>
          <td>Workers binding、HTTP / libSQL endpoint</td>
      </tr>
      <tr>
          <td>Auth</td>
          <td>filesystem permission</td>
          <td>platform secret、token、binding</td>
      </tr>
      <tr>
          <td>Error model</td>
          <td>SQLite error code</td>
          <td>SDK / platform error + SQLite-like error</td>
      </tr>
      <tr>
          <td>Retry</td>
          <td>local busy / lock retry</td>
          <td>network retry、idempotency、timeout</td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>app log + file metric</td>
          <td>app log + platform metric</td>
      </tr>
  </tbody>
</table>
<p>Repository adapter 要承擔 driver 差異。Domain layer 應看到穩定的 repository contract，例如 duplicate key、stale read、temporary unavailable、retryable write；底層才處理 D1 binding 或 libSQL client。</p>
<p>Idempotency 是 edge migration 的關鍵。Write request 進入 network / serverless runtime 後，retry 可能在 client、platform 或 application 層發生；每個 critical write 都應有 idempotency key 或 natural unique key。</p>
<h2 id="evidence">Evidence</h2>
<p>Evidence 的核心責任是證明 edge migration 帶來的收益大於新風險。D1 / Turso 的成功要同時看功能可用、region latency、freshness、error rate、cost、migration time 與 exit route。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Latency by region</td>
          <td>從主要 user region 跑 read/write test</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>write 後在 replica / edge read 檢查</td>
      </tr>
      <tr>
          <td>Migration repeatability</td>
          <td>staging database 從空庫重跑 seed</td>
      </tr>
      <tr>
          <td>Error mapping</td>
          <td>duplicate、constraint、timeout、auth</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>request、storage、egress、operation</td>
      </tr>
      <tr>
          <td>Exit route</td>
          <td>export file + restore to local SQLite</td>
      </tr>
  </tbody>
</table>
<p>Freshness evidence 要用產品語言寫。若 UI 可以顯示「同步中」，freshness window 可被使用者理解；若是付款、庫存、權限決策，讀舊資料會直接造成業務錯誤，這類 workflow 要走 primary read 或 server SQL。</p>
<p>Exit route 要被演練。Edge product 的 adoption cost 低，exit cost 會出現在 driver API、migration workflow、platform binding 與 data export；至少要能把 staging data export 回 SQLite file 並通過 smoke test。</p>
<h2 id="rollback">Rollback</h2>
<p>Rollback 的核心責任是保留 local SQLite snapshot 與 read-only fallback。Edge migration 若在 cutover 後遇到 auth、latency、limit 或 query error，團隊要能快速回到上一個可用資料狀態。</p>
<table>
  <thead>
      <tr>
          <th>Rollback 觸發</th>
          <th>回退策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Import / migration 失敗</td>
          <td>清空 target、修 migration、重跑 seed</td>
      </tr>
      <tr>
          <td>Query error spike</td>
          <td>切回 local SQLite / previous endpoint</td>
      </tr>
      <tr>
          <td>Freshness issue</td>
          <td>critical read 改 primary path</td>
      </tr>
      <tr>
          <td>Cost / limit spike</td>
          <td>降低 traffic、batch migration、重評估</td>
      </tr>
      <tr>
          <td>Vendor incident</td>
          <td>read-only mode、fallback endpoint</td>
      </tr>
  </tbody>
</table>
<p>Local snapshot 要保存到 cutover 後的觀察窗口結束。若 cutover 期間已有 target-only writes，要設計回放或 reconciliation；高風險 workflow 可以先進 read-only cutover，再逐步開寫。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把 edge migration 和 server DB migration 分開。D1 / Turso 適合 edge runtime 與 SQLite-like workflow；當需求轉向 central audit、server role、high-write OLTP 或 distributed transaction，應改走 PostgreSQL / CockroachDB / Spanner。</p>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers app + small relational data</td>
          <td>D1</td>
      </tr>
      <tr>
          <td>Read-heavy app + local replica value</td>
          <td>Turso / libSQL</td>
      </tr>
      <tr>
          <td>Backup / restore 是主要問題</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>多 tenant + permission + audit</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Global write transaction</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>SQLite to D1 / Turso migration 完成後，先讀 <a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a> 釐清 product boundary；再用 <a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits</a> 做 compatibility audit；需要操作演練時讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/d1-turso-preview-lab/" data-link-title="SQLite D1 / Turso Preview Lab" data-link-desc="SQLite local DB 匯出到 Cloudflare D1 或 Turso preview environment 的 compatibility、latency 與 rollback 操作說明">D1 / Turso preview lab</a>。</p>
]]></content:encoded></item><item><title>SQLite to PostgreSQL Migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/</guid><description>&lt;p>SQLite to PostgreSQL migration 的核心責任是把 embedded single-file state 升級成 server SQL operational model。這條路線通常由 multi-user access、HA、central audit、permission、online schema governance、write concurrency 或 team handoff 壓力觸發。&lt;/p>
&lt;p>本文的判讀錨點是：升級到 PostgreSQL 是服務責任擴大，而非單純換 driver。Migration 要同時處理 schema 語意、資料搬遷、application adapter、backup / PITR、role、observability、cutover 與 rollback。&lt;/p>
&lt;h2 id="migration-drivers">Migration Drivers&lt;/h2>
&lt;p>Migration drivers 的核心責任是確認 PostgreSQL 真的承擔新增責任。SQLite 在 single-node、single-file、low-concurrency 場景很強；PostgreSQL 的價值出現在 server database governance。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>代表需求&lt;/th>
 &lt;th>PostgreSQL 承擔的責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Concurrent writers&lt;/td>
 &lt;td>多 instance / 多使用者同時寫入&lt;/td>
 &lt;td>MVCC、connection management、lock insight&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA / PITR&lt;/td>
 &lt;td>需要時間點恢復與 managed backup&lt;/td>
 &lt;td>WAL archiving、replica、restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Central audit&lt;/td>
 &lt;td>需要查詢與變更證據&lt;/td>
 &lt;td>role、log、extension、SIEM integration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission boundary&lt;/td>
 &lt;td>app / analyst / job 權限分離&lt;/td>
 &lt;td>DB role、grant、row / schema boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema governance&lt;/td>
 &lt;td>migration 要 online 且可審查&lt;/td>
 &lt;td>migration tool、lock review、rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared data platform&lt;/td>
 &lt;td>多服務共用正式資料&lt;/td>
 &lt;td>connection pool、capacity、ownership&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Driver 要被量化。若問題只是單一 CLI 檔案變大，先改善 backup、VACUUM、index 與 WAL runbook；若問題是多 instance 同時寫、權限分離、audit 與 PITR，PostgreSQL 才是正確路由。&lt;/p>
&lt;h2 id="diff-audit">Diff Audit&lt;/h2>
&lt;p>Diff audit 的核心責任是把 SQLite 語意轉成 PostgreSQL 語意。SQLite 的 type affinity、date / time convention、auto-increment、foreign key、index、JSON、transaction 與 extension 都要逐項審查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>SQLite source 問題&lt;/th>
 &lt;th>PostgreSQL target 決策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Type&lt;/td>
 &lt;td>dynamic typing、STRICT usage&lt;/td>
 &lt;td>integer / bigint / numeric / timestamptz&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary key&lt;/td>
 &lt;td>rowid、INTEGER PRIMARY KEY&lt;/td>
 &lt;td>identity、sequence、UUID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Date/time&lt;/td>
 &lt;td>TEXT / INTEGER convention&lt;/td>
 &lt;td>timestamptz、timezone policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JSON&lt;/td>
 &lt;td>JSON text / function usage&lt;/td>
 &lt;td>jsonb、GIN index、query rewrite&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Constraint&lt;/td>
 &lt;td>FK pragma、check、unique collation&lt;/td>
 &lt;td>enforced FK、deferrable、collation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index&lt;/td>
 &lt;td>partial / expression / covering index&lt;/td>
 &lt;td>equivalent index + explain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>single writer、savepoint&lt;/td>
 &lt;td>isolation level、deadlock retry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type mapping 要先保護 domain invariant。金額欄位用 integer cents 或 numeric、時間欄位用 timestamptz 或明確 UTC text、boolean 用 boolean；每個轉換都要有 invalid sample 與 round-trip test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite to PostgreSQL migration 的核心責任是把 embedded single-file state 升級成 server SQL operational model。這條路線通常由 multi-user access、HA、central audit、permission、online schema governance、write concurrency 或 team handoff 壓力觸發。</p>
<p>本文的判讀錨點是：升級到 PostgreSQL 是服務責任擴大，而非單純換 driver。Migration 要同時處理 schema 語意、資料搬遷、application adapter、backup / PITR、role、observability、cutover 與 rollback。</p>
<h2 id="migration-drivers">Migration Drivers</h2>
<p>Migration drivers 的核心責任是確認 PostgreSQL 真的承擔新增責任。SQLite 在 single-node、single-file、low-concurrency 場景很強；PostgreSQL 的價值出現在 server database governance。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>代表需求</th>
          <th>PostgreSQL 承擔的責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Concurrent writers</td>
          <td>多 instance / 多使用者同時寫入</td>
          <td>MVCC、connection management、lock insight</td>
      </tr>
      <tr>
          <td>HA / PITR</td>
          <td>需要時間點恢復與 managed backup</td>
          <td>WAL archiving、replica、restore drill</td>
      </tr>
      <tr>
          <td>Central audit</td>
          <td>需要查詢與變更證據</td>
          <td>role、log、extension、SIEM integration</td>
      </tr>
      <tr>
          <td>Permission boundary</td>
          <td>app / analyst / job 權限分離</td>
          <td>DB role、grant、row / schema boundary</td>
      </tr>
      <tr>
          <td>Schema governance</td>
          <td>migration 要 online 且可審查</td>
          <td>migration tool、lock review、rollback</td>
      </tr>
      <tr>
          <td>Shared data platform</td>
          <td>多服務共用正式資料</td>
          <td>connection pool、capacity、ownership</td>
      </tr>
  </tbody>
</table>
<p>Driver 要被量化。若問題只是單一 CLI 檔案變大，先改善 backup、VACUUM、index 與 WAL runbook；若問題是多 instance 同時寫、權限分離、audit 與 PITR，PostgreSQL 才是正確路由。</p>
<h2 id="diff-audit">Diff Audit</h2>
<p>Diff audit 的核心責任是把 SQLite 語意轉成 PostgreSQL 語意。SQLite 的 type affinity、date / time convention、auto-increment、foreign key、index、JSON、transaction 與 extension 都要逐項審查。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>SQLite source 問題</th>
          <th>PostgreSQL target 決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type</td>
          <td>dynamic typing、STRICT usage</td>
          <td>integer / bigint / numeric / timestamptz</td>
      </tr>
      <tr>
          <td>Primary key</td>
          <td>rowid、INTEGER PRIMARY KEY</td>
          <td>identity、sequence、UUID</td>
      </tr>
      <tr>
          <td>Date/time</td>
          <td>TEXT / INTEGER convention</td>
          <td>timestamptz、timezone policy</td>
      </tr>
      <tr>
          <td>JSON</td>
          <td>JSON text / function usage</td>
          <td>jsonb、GIN index、query rewrite</td>
      </tr>
      <tr>
          <td>Constraint</td>
          <td>FK pragma、check、unique collation</td>
          <td>enforced FK、deferrable、collation</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>partial / expression / covering index</td>
          <td>equivalent index + explain</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>single writer、savepoint</td>
          <td>isolation level、deadlock retry</td>
      </tr>
  </tbody>
</table>
<p>Type mapping 要先保護 domain invariant。金額欄位用 integer cents 或 numeric、時間欄位用 timestamptz 或明確 UTC text、boolean 用 boolean；每個轉換都要有 invalid sample 與 round-trip test。</p>
<p>Index mapping 要用 production query 重跑 explain。SQLite 的 <code>EXPLAIN QUERY PLAN</code> 只能說明 SQLite planner；PostgreSQL 需要自己的 <code>EXPLAIN (ANALYZE, BUFFERS)</code>，並使用接近真實分布的資料量。</p>
<h2 id="phase-plan">Phase Plan</h2>
<p>Phase plan 的核心責任是降低一次性 cutover 風險。SQLite to PostgreSQL migration 通常可以分成 schema 建模、資料匯出、adapter 切換、shadow read、freeze / cutover 與 cleanup。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema rewrite</td>
          <td>建立 PostgreSQL target schema</td>
          <td>migration dry run、schema review</td>
      </tr>
      <tr>
          <td>Data export</td>
          <td>從 SQLite 取出穩定 snapshot</td>
          <td>source checksum、row count、export log</td>
      </tr>
      <tr>
          <td>Data import</td>
          <td>寫入 PostgreSQL</td>
          <td>target checksum、constraint validation</td>
      </tr>
      <tr>
          <td>Adapter layer</td>
          <td>將 repository 改為可切換</td>
          <td>dual test suite、error mapping</td>
      </tr>
      <tr>
          <td>Shadow read</td>
          <td>比對新舊 query result</td>
          <td>mismatch report、latency profile</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>切正式寫入</td>
          <td>freeze window、rollback snapshot</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>退役 SQLite write path</td>
          <td>retention、credential、runbook update</td>
      </tr>
  </tbody>
</table>
<p>Adapter layer 是風險控制點。Repository 應把 SQLite 與 PostgreSQL driver 差異藏在 infrastructure layer，domain 不直接依賴 vendor-specific SQL exception 或 connection object。</p>
<p>Shadow read 適合先驗證 read contract。正式寫入仍留在 SQLite 時，background job 可以把相同 query 跑到 PostgreSQL mirror，記錄 row count、field diff、排序差異與 latency。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是讓搬遷結果可驗證。SQLite database file 可以透過 <code>.dump</code>、CSV export、application-level export 或 custom ETL 搬入 PostgreSQL；選擇取決於資料量、型別轉換、FK order 與 downtime window。</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">sqlite3 app.db <span class="s2">&#34;.mode csv&#34;</span> <span class="s2">&#34;.headers on&#34;</span> <span class="s2">&#34;.once orders.csv&#34;</span> <span class="s2">&#34;SELECT * FROM orders ORDER BY id;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;\\copy orders FROM &#39;orders.csv&#39; CSV HEADER&#34;</span></span></span></code></pre></div><p>這段命令是教學骨架。正式 migration 要處理 quoting、NULL、timezone、large object、FK order、batch size、transaction size、retry、import log 與 sensitive data handling。</p>
<p>Row count 是基本證據，checksum 是更強證據。可以針對每張表計算穩定排序後的 hash，或在 application layer 對 domain key 與重要欄位做 checksum。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">total_cents</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>Aggregate checksum 適合快速抓大錯。正式驗證還要補抽樣 row diff、edge case row、foreign key check 與 business invariant。</p>
<h2 id="cutover">Cutover</h2>
<p>Cutover 的核心責任是控制最後一次寫入切換。SQLite source 在 cutover 前應進入 read-only 或 writer freeze，確保最後 snapshot、import 與 validation 對齊。</p>
<table>
  <thead>
      <tr>
          <th>Cutover step</th>
          <th>操作</th>
          <th>Rollback 條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Freeze writers</td>
          <td>停止背景 job、API write、admin tool</td>
          <td>source 寫入仍持續或 freeze 失敗</td>
      </tr>
      <tr>
          <td>Final snapshot</td>
          <td>SQLite backup / export</td>
          <td>checksum 失敗</td>
      </tr>
      <tr>
          <td>Final import</td>
          <td>PostgreSQL transaction / batch import</td>
          <td>constraint error、row mismatch</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>error rate、latency、permission failure</td>
      </tr>
      <tr>
          <td>Switch traffic</td>
          <td>更新 config / secret / deployment</td>
          <td>application error rate 超過 tripwire</td>
      </tr>
      <tr>
          <td>Monitor</td>
          <td>query latency、lock、connection pool</td>
          <td>pool exhaustion、deadlock spike、data diff</td>
      </tr>
  </tbody>
</table>
<p>Rollback 要保存 source snapshot。若 cutover 後發現 PostgreSQL error mapping、permission 或 performance 問題，可以切回 SQLite read/write snapshot；前提是 cutover window 內所有新寫入都能回放或被阻擋。</p>
<h2 id="postgresql-operation-gate">PostgreSQL Operation Gate</h2>
<p>PostgreSQL operation gate 的核心責任是確認團隊準備好接手 server DB。Migration 成功要包含資料進入 target 與 operation readiness；PostgreSQL 需要 connection pool、backup / PITR、vacuum、index bloat、role、migration lock review 與 alert。</p>
<p>最小 operation checklist：</p>
<ol>
<li>Connection pool 設計：max connections、pool size、timeout、transaction pooling policy。</li>
<li>Backup / PITR：restore drill、retention、RPO / RTO。</li>
<li>Role / grant：application role、migration role、read-only role。</li>
<li>Migration lock review：DDL impact、online migration strategy。</li>
<li>Observability：slow query、lock wait、deadlock、replica lag、disk。</li>
<li>Incident route：rollback、restore、read-only mode、on-call owner。</li>
</ol>
<p>這個 gate 要在 cutover 前完成。SQLite 讓 operation surface 很小；PostgreSQL 擴大能力的同時，也擴大維護責任。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是阻止過早升級。若服務仍是 single-user、local-first、low-write、可用簡單 backup 解決，PostgreSQL 可能引入比問題更大的 operation cost。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>更合適路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user app 或 desktop app</td>
          <td>保留 SQLite + backup / migration runbook</td>
      </tr>
      <tr>
          <td>主要壓力是備份</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>主要壓力是 edge locality</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">D1 / Turso route</a></td>
      </tr>
      <tr>
          <td>Team 尚未準備 server DB operation</td>
          <td>先補 observability / restore drill</td>
      </tr>
      <tr>
          <td>Schema / query 還在快速探索</td>
          <td>先穩定 domain model，再做正式 migration</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要轉成 tripwire。當 writer concurrency、audit、PITR、role 或 HA 需求跨過明確門檻，再啟動 migration。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>SQLite to PostgreSQL migration 完成後，下一步要看 target operation。PostgreSQL 能力讀 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>；migration 方法讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>；若需求只是 edge platform，改讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso migration</a>。</p>
]]></content:encoded></item><item><title>SQLite WAL Busy Reproduction</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/</guid><description>&lt;p>SQLite WAL busy reproduction 的核心責任是讓讀者親眼看到 single writer boundary。這篇承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking&lt;/a>，把 &lt;code>SQLITE_BUSY&lt;/code> 從文字警告轉成可重現 timeline。&lt;/p>
&lt;p>本文的驗收標準是：你能用兩個 sqlite3 session 重現 writer contention，觀察 busy timeout 行為，並用 WAL size 與 checkpoint result 連回 production runbook。&lt;/p>
&lt;h2 id="prepare-database">Prepare Database&lt;/h2>
&lt;p>Prepare database 的核心責任是建立可重現的 WAL mode database。若已跑過 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/" data-link-title="SQLite Local File Quickstart" data-link-desc="SQLite local .db file、schema、seed data、PRAGMA baseline、query sample 與 cleanup 的操作說明">local file quickstart&lt;/a>，可以沿用 &lt;code>/tmp/sqlite-lab/app.db&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="nb">cd&lt;/span> /tmp/sqlite-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;PRAGMA journal_mode = WAL;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;PRAGMA busy_timeout = 1000;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>確認 WAL mode：&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">sqlite3 app.db &lt;span class="s2">&amp;#34;PRAGMA journal_mode;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預期輸出是 &lt;code>wal&lt;/code>。&lt;/p>
&lt;h2 id="session-a-hold-writer-lock">Session A: Hold Writer Lock&lt;/h2>
&lt;p>Session A 的核心責任是刻意持有 write transaction。開第一個 terminal，執行：&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">sqlite3 app.db&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 sqlite prompt 內輸入：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foreign_keys&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">BEGIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IMMEDIATE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">account_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">amount_cents&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idempotency_key&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;busy-session-a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;2026-05-21T02:00:00Z&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>先保持 transaction 開啟，暫時延後 &lt;code>COMMIT&lt;/code>。&lt;code>BEGIN IMMEDIATE&lt;/code> 會取得 writer lock，讓第二個 writer 需要等待或失敗。&lt;/p>
&lt;h2 id="session-b-observe-busy">Session B: Observe Busy&lt;/h2>
&lt;p>Session B 的核心責任是用第二個 connection 觀察 single writer boundary。開第二個 terminal，執行：&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="nb">cd&lt;/span> /tmp/sqlite-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sqlite3 app.db &lt;span class="s2">&amp;#34;PRAGMA busy_timeout = 1000; INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 22, &amp;#39;busy-session-b&amp;#39;, &amp;#39;2026-05-21T02:01:00Z&amp;#39;);&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預期結果是等待約 1 秒後出現 busy / locked 類錯誤。不同 sqlite3 版本的錯誤文字可能略有差異，核心訊號是第二個 writer 在 Session A commit 前拿不到 write lock。&lt;/p>
&lt;h2 id="release-lock">Release Lock&lt;/h2>
&lt;p>Release lock 的核心責任是確認 contention 來自 writer transaction。回到 Session A，輸入：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">COMMIT&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">quit&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>再次執行 Session B 的 insert，這次應成功。&lt;/p></description><content:encoded><![CDATA[<p>SQLite WAL busy reproduction 的核心責任是讓讀者親眼看到 single writer boundary。這篇承接 <a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a>，把 <code>SQLITE_BUSY</code> 從文字警告轉成可重現 timeline。</p>
<p>本文的驗收標準是：你能用兩個 sqlite3 session 重現 writer contention，觀察 busy timeout 行為，並用 WAL size 與 checkpoint result 連回 production runbook。</p>
<h2 id="prepare-database">Prepare Database</h2>
<p>Prepare database 的核心責任是建立可重現的 WAL mode database。若已跑過 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/local-file-quickstart/" data-link-title="SQLite Local File Quickstart" data-link-desc="SQLite local .db file、schema、seed data、PRAGMA baseline、query sample 與 cleanup 的操作說明">local file quickstart</a>，可以沿用 <code>/tmp/sqlite-lab/app.db</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="nb">cd</span> /tmp/sqlite-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA journal_mode = WAL;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA busy_timeout = 1000;&#34;</span></span></span></code></pre></div><p>確認 WAL mode：</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">sqlite3 app.db <span class="s2">&#34;PRAGMA journal_mode;&#34;</span></span></span></code></pre></div><p>預期輸出是 <code>wal</code>。</p>
<h2 id="session-a-hold-writer-lock">Session A: Hold Writer Lock</h2>
<p>Session A 的核心責任是刻意持有 write transaction。開第一個 terminal，執行：</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">sqlite3 app.db</span></span></code></pre></div><p>在 sqlite prompt 內輸入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="w"> </span><span class="k">IMMEDIATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">(</span><span class="n">account_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount_cents</span><span class="p">,</span><span class="w"> </span><span class="n">idempotency_key</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">11</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;busy-session-a&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-05-21T02:00:00Z&#39;</span><span class="p">);</span></span></span></code></pre></div><p>先保持 transaction 開啟，暫時延後 <code>COMMIT</code>。<code>BEGIN IMMEDIATE</code> 會取得 writer lock，讓第二個 writer 需要等待或失敗。</p>
<h2 id="session-b-observe-busy">Session B: Observe Busy</h2>
<p>Session B 的核心責任是用第二個 connection 觀察 single writer boundary。開第二個 terminal，執行：</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="nb">cd</span> /tmp/sqlite-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA busy_timeout = 1000; INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 22, &#39;busy-session-b&#39;, &#39;2026-05-21T02:01:00Z&#39;);&#34;</span></span></span></code></pre></div><p>預期結果是等待約 1 秒後出現 busy / locked 類錯誤。不同 sqlite3 版本的錯誤文字可能略有差異，核心訊號是第二個 writer 在 Session A commit 前拿不到 write lock。</p>
<h2 id="release-lock">Release Lock</h2>
<p>Release lock 的核心責任是確認 contention 來自 writer transaction。回到 Session A，輸入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="p">.</span><span class="n">quit</span></span></span></code></pre></div><p>再次執行 Session B 的 insert，這次應成功。</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">sqlite3 app.db <span class="s2">&#34;PRAGMA foreign_keys = ON; INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 22, &#39;busy-session-b&#39;, &#39;2026-05-21T02:01:00Z&#39;);&#34;</span></span></span></code></pre></div><p>若 idempotency key 已在前一次嘗試中寫入，改成新的 key。這個細節也提醒 production write 要有 idempotency 設計。</p>
<h2 id="busy-timeout-comparison">Busy Timeout Comparison</h2>
<p>Busy timeout comparison 的核心責任是區分「等一下」和「解決 writer contention」。Timeout 可以讓短暫鎖等待更平滑，但長交易仍會造成延遲或失敗。</p>
<p>重開 Session A 並持有 transaction：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">BEGIN</span><span class="w"> </span><span class="k">IMMEDIATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">(</span><span class="n">account_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount_cents</span><span class="p">,</span><span class="w"> </span><span class="n">idempotency_key</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">33</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;busy-session-a-long&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-05-21T02:10:00Z&#39;</span><span class="p">);</span></span></span></code></pre></div><p>在 Session B 測不同 timeout：</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="nb">time</span> sqlite3 app.db <span class="s2">&#34;PRAGMA busy_timeout = 5000; INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key, created_at) VALUES (1, 44, &#39;busy-session-b-long&#39;, &#39;2026-05-21T02:11:00Z&#39;);&#34;</span></span></span></code></pre></div><p>若 Session A 在 5 秒內 commit，Session B 可能成功；若持續持有 transaction，Session B 會在 timeout 後失敗。這就是 production 裡 busy timeout 的邊界：它緩衝短鎖，長 transaction 仍要被設計移除。</p>
<h2 id="wal-and-checkpoint">WAL and Checkpoint</h2>
<p>WAL and checkpoint 的核心責任是把 writer activity 和 file artifact 連起來。多做幾次寫入後觀察 sidecar。</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">ls -lh app.db app.db-wal app.db-shm
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA wal_checkpoint(PASSIVE);&#34;</span></span></span></code></pre></div><p><code>wal_checkpoint</code> 會回傳 checkpoint 狀態。正式 runbook 要記錄 WAL size、checkpoint duration、reader age 與 checkpoint failure。</p>
<p>可以手動觸發 truncate checkpoint：</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">sqlite3 app.db <span class="s2">&#34;PRAGMA wal_checkpoint(TRUNCATE);&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ls -lh app.db app.db-wal app.db-shm</span></span></code></pre></div><p>TRUNCATE 適合 lab 觀察。Production 使用時要評估 reader、latency 與維護窗口。</p>
<h2 id="mitigation-note">Mitigation Note</h2>
<p>Mitigation note 的核心責任是把 lab 結果轉成設計策略。看到 <code>SQLITE_BUSY</code> 後，優先檢查 long transaction、未關閉 cursor、背景 job、write burst、parallel test 共用 DB 與 checkpoint pressure。</p>
<p>常見策略包含：</p>
<ol>
<li>縮短 transaction，將外部 API call 移到 transaction 外。</li>
<li>設定合理 busy timeout 與 retry backoff。</li>
<li>把 write queue 序列化，讓高風險 workflow 先排隊。</li>
<li>將 heavy read 移到 snapshot 或 replica。</li>
<li>當 concurrent writer 成為常態，評估 PostgreSQL / MySQL。</li>
</ol>
<p>完成本篇後，下一步讀 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">observability / runbook</a> 把 busy、WAL 與 checkpoint 變成正式監控訊號。</p>
]]></content:encoded></item><item><title>SQLite WAL Concurrency and Locking</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 single-file / embedded 定位；本文聚焦 &lt;em>WAL concurrency、single writer boundary、&lt;code>SQLITE_BUSY&lt;/code> 與 checkpoint strategy&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite WAL concurrency 的核心責任是讓 reader / writer 衝突下降，同時保留單檔案資料庫的寫入邊界。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/write-ahead-log/" data-link-title="Write-Ahead Log" data-link-desc="說明資料庫如何先寫入 log 再合併回主資料，以提供持久性與崩潰復原">WAL mode&lt;/a> 把寫入 append 到 &lt;code>-wal&lt;/code> sidecar file，reader 可以從 main database file 加 WAL snapshot 讀取一致視圖；這讓 read-heavy workload 能比 rollback journal mode 更順。但 SQLite 仍遵循 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">single writer model&lt;/a>、只有一條 writer path，長交易、背景 migration、慢 disk 或多 process 寫入都會在這條 path 上排隊。&lt;/p>
&lt;p>本文的判讀錨點是：WAL 提升的是 reader concurrency，治理的是 writer queue。當服務看到 &lt;code>SQLITE_BUSY&lt;/code>、WAL file 持續變大、checkpoint duration 變長或偶發 commit latency spike，問題通常在 transaction duration、checkpoint cadence、filesystem lock 或 process ownership，而非單純「資料庫太小」。&lt;/p>
&lt;h2 id="wal-mode-的服務責任">WAL mode 的服務責任&lt;/h2>
&lt;p>WAL mode 的服務責任是把「寫入直接改 main database file」改成「寫入先 append 到 WAL，再由 checkpoint 合併回 main database」。SQLite 官方文件把 WAL 模型拆成 reading、writing、checkpointing 三個 primitive；這個 framing 對 production runbook 很重要，因為 checkpoint 會變成獨立的操作訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>寫入路徑&lt;/th>
 &lt;th>Reader 影響&lt;/th>
 &lt;th>Production 判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rollback journal&lt;/td>
 &lt;td>寫入前保存原始 page，再修改 main file&lt;/td>
 &lt;td>write 期間更容易和 reader 互相等待&lt;/td>
 &lt;td>適合簡單、低並發、短交易路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL&lt;/td>
 &lt;td>寫入 append 到 &lt;code>-wal&lt;/code>，checkpoint 後合併&lt;/td>
 &lt;td>reader 可看自己的 WAL snapshot&lt;/td>
 &lt;td>適合 read-heavy、互動式、短寫交易 workload&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的讀法是先看服務是否主要受 read / write 衝突影響。Read-heavy CLI、desktop、mobile、edge-local API 或 small backend 往往能從 WAL mode 受益；write-heavy queue consumer、batch import、multi-process writer 或 high-concurrency OLTP 則會先撞到 single writer boundary。&lt;/p>
&lt;h2 id="locking-model多-reader-與單-writer-是同時成立的">Locking model：多 reader 與單 writer 是同時成立的&lt;/h2>
&lt;p>SQLite locking model 的核心責任是保護單一 database file 的 ACID 邊界。Rollback journal mode 的官方 locking 文件描述了 SHARED、RESERVED、PENDING、EXCLUSIVE 等狀態；WAL mode 的細節另由 WAL 文件說明，但服務判讀上仍要記住同一件事：跨 connection / process 的寫入要被序列化。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 single-file / embedded 定位；本文聚焦 <em>WAL concurrency、single writer boundary、<code>SQLITE_BUSY</code> 與 checkpoint strategy</em>。</p></blockquote>
<p>SQLite WAL concurrency 的核心責任是讓 reader / writer 衝突下降，同時保留單檔案資料庫的寫入邊界。<a href="/blog/backend/knowledge-cards/write-ahead-log/" data-link-title="Write-Ahead Log" data-link-desc="說明資料庫如何先寫入 log 再合併回主資料，以提供持久性與崩潰復原">WAL mode</a> 把寫入 append 到 <code>-wal</code> sidecar file，reader 可以從 main database file 加 WAL snapshot 讀取一致視圖；這讓 read-heavy workload 能比 rollback journal mode 更順。但 SQLite 仍遵循 <a href="/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">single writer model</a>、只有一條 writer path，長交易、背景 migration、慢 disk 或多 process 寫入都會在這條 path 上排隊。</p>
<p>本文的判讀錨點是：WAL 提升的是 reader concurrency，治理的是 writer queue。當服務看到 <code>SQLITE_BUSY</code>、WAL file 持續變大、checkpoint duration 變長或偶發 commit latency spike，問題通常在 transaction duration、checkpoint cadence、filesystem lock 或 process ownership，而非單純「資料庫太小」。</p>
<h2 id="wal-mode-的服務責任">WAL mode 的服務責任</h2>
<p>WAL mode 的服務責任是把「寫入直接改 main database file」改成「寫入先 append 到 WAL，再由 checkpoint 合併回 main database」。SQLite 官方文件把 WAL 模型拆成 reading、writing、checkpointing 三個 primitive；這個 framing 對 production runbook 很重要，因為 checkpoint 會變成獨立的操作訊號。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>寫入路徑</th>
          <th>Reader 影響</th>
          <th>Production 判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rollback journal</td>
          <td>寫入前保存原始 page，再修改 main file</td>
          <td>write 期間更容易和 reader 互相等待</td>
          <td>適合簡單、低並發、短交易路徑</td>
      </tr>
      <tr>
          <td>WAL</td>
          <td>寫入 append 到 <code>-wal</code>，checkpoint 後合併</td>
          <td>reader 可看自己的 WAL snapshot</td>
          <td>適合 read-heavy、互動式、短寫交易 workload</td>
      </tr>
  </tbody>
</table>
<p>這張表的讀法是先看服務是否主要受 read / write 衝突影響。Read-heavy CLI、desktop、mobile、edge-local API 或 small backend 往往能從 WAL mode 受益；write-heavy queue consumer、batch import、multi-process writer 或 high-concurrency OLTP 則會先撞到 single writer boundary。</p>
<h2 id="locking-model多-reader-與單-writer-是同時成立的">Locking model：多 reader 與單 writer 是同時成立的</h2>
<p>SQLite locking model 的核心責任是保護單一 database file 的 ACID 邊界。Rollback journal mode 的官方 locking 文件描述了 SHARED、RESERVED、PENDING、EXCLUSIVE 等狀態；WAL mode 的細節另由 WAL 文件說明，但服務判讀上仍要記住同一件事：跨 connection / process 的寫入要被序列化。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>WAL mode 下的責任</th>
          <th>常見失效訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reader</td>
          <td>讀取開始時固定自己的 snapshot end mark</td>
          <td>長讀取讓 checkpoint 停在舊 snapshot，WAL file 持續變大</td>
      </tr>
      <tr>
          <td>Writer</td>
          <td>append 新 transaction 到同一個 WAL file</td>
          <td>其他 writer 看到 <code>SQLITE_BUSY</code> 或 write latency spike</td>
      </tr>
      <tr>
          <td>Checkpoint</td>
          <td>把 WAL frame 合併回 main database file</td>
          <td>checkpoint duration 拉長、commit 偶發變慢</td>
      </tr>
      <tr>
          <td>Filesystem</td>
          <td>提供可靠 file lock 與 shared-memory 支援</td>
          <td>network filesystem、container mount 或權限造成異常</td>
      </tr>
  </tbody>
</table>
<p>多 reader 與單 writer 的組合是 SQLite 的正常設計。讀者在查問題時，要避免把 <code>SQLITE_BUSY</code> 直接解讀成資料毀損；它多半代表某個 connection 正在持有 writer 所需的 lock，或 checkpoint / transaction 正在等待可前進的窗口。</p>
<h2 id="sqlite_busy-的第一輪排查"><code>SQLITE_BUSY</code> 的第一輪排查</h2>
<p><code>SQLITE_BUSY</code> 的核心意義是某個 connection 當下拿不到需要的 lock。SQLite 提供 <code>busy_timeout</code> 讓 connection 等待一段時間；這能吸收短暫 writer queue，但它只是等待策略，single writer boundary 仍然存在。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>可能原因</th>
          <th>第一輪處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短暫 <code>SQLITE_BUSY</code></td>
          <td>多個短寫入撞在一起</td>
          <td>設定 bounded busy timeout，縮短 transaction duration</td>
      </tr>
      <tr>
          <td>持續 <code>SQLITE_BUSY</code></td>
          <td>長交易、migration、batch import</td>
          <td>找出持鎖 connection，拆小 transaction 或移到 maintenance window</td>
      </tr>
      <tr>
          <td>commit latency 偶發變慢</td>
          <td>auto-checkpoint 在 commit path 上</td>
          <td>調整 auto-checkpoint，改由 background checkpoint</td>
      </tr>
      <tr>
          <td>read query 讓 WAL 變大</td>
          <td>long reader 卡住 checkpoint</td>
          <td>限制長查詢、拆 reporting query、設定 reader timeout</td>
      </tr>
      <tr>
          <td>部署後 busy rate 上升</td>
          <td>instance 數增加、multi-process write</td>
          <td>重新檢查 writer ownership，必要時升級 server SQL</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是先找「誰持有 writer path」。如果問題來自單一長 transaction，修 transaction boundary；如果問題來自多個 process 同時寫同檔，修 process ownership；如果問題來自真實高寫入吞吐，SQLite 已經接近服務邊界。</p>
<h2 id="busy-timeout-是緩衝器容量邊界仍在-writer-path">Busy timeout 是緩衝器，容量邊界仍在 writer path</h2>
<p>Busy timeout 的服務責任是吸收短時間 lock collision。它適合 desktop app autosave、mobile local store、短 API write、測試 fixture 或偶發 background job；它不適合作為高寫入吞吐的主要容量策略。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span></span></span></code></pre></div><p>這個設定代表 connection 最多等待 5000 ms。Production runbook 要同時記錄三個訊號：busy 次數、等待時間分布、等待後成功率。若等待後成功率高且 p99 可接受，代表 writer queue 仍在服務邊界內；若等待常超時，代表 transaction duration 或 writer 並發已經超出單檔模型。</p>
<h2 id="checkpoint-strategywal-growth-是操作訊號">Checkpoint strategy：WAL growth 是操作訊號</h2>
<p>Checkpoint 的核心責任是把 WAL 中的 committed frames 合併回 main database file。SQLite 預設會在 WAL file 達到約 1000 pages 後自動 checkpoint；這個預設適合多數小型場景，但 production 服務要把 checkpoint 視為獨立操作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="n">PASSIVE</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">FULL</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">RESTART</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">TRUNCATE</span><span class="p">);</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Checkpoint 型態</th>
          <th>操作語意</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PASSIVE</td>
          <td>盡量前進，避免主動阻塞 reader / writer</td>
          <td>日常觀測、低風險背景 checkpoint</td>
      </tr>
      <tr>
          <td>FULL</td>
          <td>等待 writer，嘗試完成更多 checkpoint</td>
          <td>maintenance window、WAL growth 需要收斂</td>
      </tr>
      <tr>
          <td>RESTART</td>
          <td>完成後讓後續 writer 可重新使用 WAL</td>
          <td>想降低 WAL 持續膨脹，能接受等待</td>
      </tr>
      <tr>
          <td>TRUNCATE</td>
          <td>完成後截斷 WAL file</td>
          <td>低流量窗口、需要回收檔案空間</td>
      </tr>
  </tbody>
</table>
<p>Checkpoint 策略的判讀要看 workload cadence。互動式服務通常保留 auto-checkpoint，再加上低流量時段的 background checkpoint；長查詢或 reporting workload 需要避免讓 long reader 長期佔住 snapshot；batch import 則要把 transaction 切小，避免 WAL file 在單一交易期間快速膨脹。</p>
<h2 id="checkpoint-starvation長-reader-會讓-wal-持續長大">Checkpoint starvation：長 reader 會讓 WAL 持續長大</h2>
<p>Checkpoint starvation 的核心概念是：只要總有 reader 還在使用舊 snapshot，checkpoint 就可能停在 reset 之前。SQLite 官方 WAL 文件明確指出，checkpoint 可以和 reader 並行，但遇到仍被 reader 使用的 WAL 位置時要停下來；如果長時間沒有 reader gap，WAL file 會持續成長。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>真實服務長相</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Desktop app 開著長報表</td>
          <td>使用者查詢大列表，背景寫入持續發生</td>
          <td>報表分頁、限制 read transaction duration</td>
      </tr>
      <tr>
          <td>API handler 把 cursor 留太久</td>
          <td>streaming response 邊讀邊回，交易未結束</td>
          <td>先 materialize 結果、縮短 DB read transaction</td>
      </tr>
      <tr>
          <td>Background sync 長讀取</td>
          <td>sync worker 掃全表，UI 仍在寫資料</td>
          <td>分批讀取、讀寫排程、低流量 checkpoint</td>
      </tr>
      <tr>
          <td>Test suite 平行讀寫 fixture</td>
          <td>測試共用同一 <code>.db</code>，多 worker 交錯</td>
          <td>per-test DB、read-only fixture、獨立 temp file</td>
      </tr>
  </tbody>
</table>
<p>這些情境的共同點是 reader lifecycle 沒有被 application 控制。SQLite 的 concurrency 問題常發生在 application boundary，而非 database engine 本身；修法也應回到 handler、worker、test runner 或 UI lifecycle。</p>
<h2 id="filesystem-與-deployment-boundary">Filesystem 與 deployment boundary</h2>
<p>SQLite WAL 的 deployment boundary 是 local filesystem 與可靠 shared-memory / file-locking primitive。官方 WAL 文件指出 wal-index 使用 shared memory，所有 reader 要位於同一台機器；這也是 WAL mode 不適合放在一般 network filesystem 上的主要原因。</p>
<table>
  <thead>
      <tr>
          <th>部署方式</th>
          <th>判讀</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 process / 單機 local disk</td>
          <td>SQLite 最自然的部署形狀</td>
          <td>WAL + backup / restore runbook</td>
      </tr>
      <tr>
          <td>多 process / 同機 local disk</td>
          <td>可行，但要清楚 writer ownership 與 timeout</td>
          <td>WAL + busy timeout + checkpoint evidence</td>
      </tr>
      <tr>
          <td>多 instance / shared volume</td>
          <td>lock 與 writer ownership 風險上升</td>
          <td>升級 PostgreSQL / MySQL，或改用明確 primary pattern</td>
      </tr>
      <tr>
          <td>network filesystem</td>
          <td>WAL shared-memory 與 file lock 語意風險高</td>
          <td>改 local disk + replication，或 server database</td>
      </tr>
      <tr>
          <td>container ephemeral disk</td>
          <td>durability 與 restore 路徑要重新設計</td>
          <td>persistent volume、backup drill、restore evidence</td>
      </tr>
  </tbody>
</table>
<p>Deployment review 要問的第一個問題是「同一時間誰會寫這個檔案」。如果答案是多個 instance、跨機器 process 或不受控 job，SQLite 的服務邊界已經需要重新評估。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1多個-worker-同時寫同一個-sqlite-檔">Case 1：多個 worker 同時寫同一個 SQLite 檔</h3>
<p>多 worker 寫入同一個 SQLite 檔的核心風險是 writer ownership 消失。常見情境是小型服務從單 instance 擴到多 instance，但仍把 database file 放在 shared volume；早期看起來可運作，流量上升後開始出現 busy timeout、WAL growth 與偶發資料修復壓力。</p>
<p>修正方向是重新定義 writer。若服務仍是 small backend，可以收斂到單 writer process + queue；若 multi-instance 是長期需求，應遷移到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> 或 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a>。</p>
<h3 id="case-2長讀取卡住-checkpoint磁碟被-wal-吃滿">Case 2：長讀取卡住 checkpoint，磁碟被 WAL 吃滿</h3>
<p>長讀取卡 checkpoint 的核心風險是 WAL file 成為隱性容量消耗。讀者可能只看到 disk usage 增長，誤以為是資料量變大；實際上 main database file 沒有明顯增長，<code>-wal</code> sidecar 持續膨脹。</p>
<p>修正方向是先找到長 reader，再調整 query lifecycle。Reporting query、background sync、streaming response、互動式 UI 大列表都要有 pagination、timeout 或低流量窗口；checkpoint 只負責收斂 WAL，application 仍要主動結束長讀取。</p>
<h3 id="case-3把-busy-timeout-當成擴容策略">Case 3：把 busy timeout 當成擴容策略</h3>
<p>Busy timeout 被當成擴容策略的核心風險是延遲被隱藏到使用者路徑。短暫 lock collision 可以等待；長期 write queue 則會把 API p99、UI freeze 或 worker backlog 拉高。</p>
<p>修正方向是把 busy wait 當 metric。設定 timeout 後要記錄等待時間與超時率；當 busy wait 成為常態，下一步是拆交易、調整 writer process、移走 batch job，或升級到 server database。</p>
<h3 id="case-4checkpoint-放在高流量-commit-path">Case 4：checkpoint 放在高流量 commit path</h3>
<p>Checkpoint 放在高流量 commit path 的核心風險是少數 commit 變得很慢。SQLite 預設 auto-checkpoint 對多數場景合理，但互動式服務可能看到偶發 latency spike；這時可以把 checkpoint 移到背景 thread / process 或低流量窗口。</p>
<p>修正方向是把 checkpoint duration 變成 evidence。觀察 WAL size、checkpoint return、commit latency 與 disk sync；若尖峰可接受，維持預設；若尖峰影響 UX，調整 checkpoint cadence。</p>
<h3 id="case-5wal-mode-版本與部署條件未納入維護">Case 5：WAL mode 版本與部署條件未納入維護</h3>
<p>WAL mode 的維護責任包含 SQLite runtime version、filesystem、sidecar file 與 release notes。SQLite 官方 WAL 文件記錄 2026-03 修正過罕見 WAL-reset bug；雖然觸發條件很窄，production runbook 仍應記錄 SQLite version、runtime package 與更新策略。</p>
<p>修正方向是把 SQLite runtime 當成 dependency。Mobile、desktop、embedded、language binding、OS bundled SQLite 可能各自帶不同版本；需要在 support matrix 中標明版本來源、WAL mode 行為與升級路徑。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite WAL / locking runbook 至少要能回答下列問題：</p>
<ol>
<li>Database file、<code>-wal</code>、<code>-shm</code> 是否位於 local durable filesystem。</li>
<li>同一時間哪些 process / thread 會寫入 database file。</li>
<li><code>PRAGMA journal_mode</code>、<code>busy_timeout</code>、<code>wal_autocheckpoint</code> 如何設定。</li>
<li><code>SQLITE_BUSY</code> 次數、等待時間、超時率是否被記錄。</li>
<li>WAL file size、checkpoint duration、disk usage 是否被觀測。</li>
<li>長 read transaction 的來源與 timeout 如何治理。</li>
<li>Batch import、migration、background sync 是否避開互動式高峰。</li>
<li>SQLite runtime version 與 WAL 相關 release notes 如何追蹤。</li>
</ol>
<p>這份清單要接到 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a>；正文教判讀，hands-on 負責讓讀者重現 <code>SQLITE_BUSY</code>、WAL growth 與 checkpoint 行為。</p>
<h2 id="何時維持-sqlite何時升級">何時維持 SQLite，何時升級</h2>
<p>SQLite WAL mode 適合單機、短交易、read-heavy、writer ownership 清楚的服務。只要 busy wait 可控、checkpoint 能完成、backup / restore drill 成立，SQLite 可以承擔正式狀態。</p>
<p>升級訊號來自 writer boundary 外溢。多 instance write、多 region write、high-write OLTP、集中權限治理、read replica、PITR、DB account / role 與 audit requirement 都會把服務推向 server SQL、edge SQLite product 或 distributed SQL。</p>
<table>
  <thead>
      <tr>
          <th>壓力</th>
          <th>SQLite 內修正</th>
          <th>升級路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偶發 <code>SQLITE_BUSY</code></td>
          <td>busy timeout、縮短 transaction</td>
          <td>維持 SQLite</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>找長 reader、manual checkpoint</td>
          <td>維持 SQLite，補 observability</td>
      </tr>
      <tr>
          <td>多 worker 寫入</td>
          <td>收斂單 writer、queue 化</td>
          <td>PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>Edge locality</td>
          <td>D1 / Turso compatibility audit</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso route</a></td>
      </tr>
      <tr>
          <td>HA / PITR / audit governance</td>
          <td>file backup 已經難以治理</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>前置：<a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance</a>、<a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></li>
<li>遷移：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></li>
<li>官方：<a href="https://www.sqlite.org/wal.html">SQLite Write-Ahead Logging</a>、<a href="https://www.sqlite.org/lockingv3.html">SQLite File Locking</a>、<a href="https://www.sqlite.org/isolation.html">SQLite Isolation</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item><item><title>資料庫 Vendor 文章撰寫規格</title><link>https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/</guid><description>&lt;p>資料庫 Vendor 文章撰寫規格的核心責任是把服務頁、深度文章與遷移 playbook 的分工固定下來。PostgreSQL 與 MySQL 已經提供 SQL baseline 的完整樣本；後續撰寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 時，應沿用同一組教學功能檢查，但保留每個服務自己的資料形狀、操作責任與失敗語言。&lt;/p>
&lt;p>這份規格承接 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。本文只處理資料庫模組的落地規格：哪些內容留在 vendor overview，哪些議題升級成 deep article，哪些變更需要 migration playbook。&lt;/p>
&lt;h2 id="判讀錨點">判讀錨點&lt;/h2>
&lt;p>資料庫 vendor 文章的錨點是正式狀態如何被保存、查詢、複製、演進與修復。產品功能、版本差異與雲端價格都只是材料；正文要把材料轉成讀者可操作的判準，讓讀者能判斷資料模型、交易需求、查詢邊界、容量壓力、操作責任與替代路由。&lt;/p>
&lt;p>PostgreSQL 與 MySQL 的 batch 顯示三個穩定事實。第一，SQL baseline 已經足以支撐其他服務頁開寫；第二，深度文章需要「何時不用」與真實案例 anchor 防止過度工程化；第三，跨 vendor 或 topology 變更需要獨立 playbook，不適合塞回 overview。&lt;/p>
&lt;h2 id="vendor-overview-規格">Vendor Overview 規格&lt;/h2>
&lt;p>Vendor overview 的責任是教讀者完成第一輪服務判斷。這一層回答服務承擔什麼資料責任、適合什麼壓力、日常有哪些操作決策、失效時先看哪些訊號，以及何時改走相鄰服務。&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>服務定位&lt;/td>
 &lt;td>這個服務承擔 SQL、embedded、document、KV 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL&lt;/a> 哪一種責任&lt;/td>
 &lt;td>開場段、教學路線、最短判讀路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料形狀&lt;/td>
 &lt;td>資料是 row、document、key-value、time-series、geo 還是 global record&lt;/td>
 &lt;td>適用場景、schema / index / partition 說明&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性與交易&lt;/td>
 &lt;td>transaction、replica、multi-region 與 stale read 如何取捨&lt;/td>
 &lt;td>適用場景、不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作責任&lt;/td>
 &lt;td>誰負責 backup、failover、upgrade、capacity、security 與 audit&lt;/td>
 &lt;td>容量規劃要點、常見陷阱、下一步路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代邊界&lt;/td>
 &lt;td>什麼條件下改走 SQL、document、KV、managed SQL 或 distributed SQL&lt;/td>
 &lt;td>同類對比、相鄰章節路由、下游 deep article&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>案例與限制&lt;/td>
 &lt;td>哪些案例能提供壓力訊號，哪些 claim 需要時間敏感標記&lt;/td>
 &lt;td>案例對照、已知 limitation、後續擴充候選&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務定位段要先把產品名稱放回資料庫分類語言。SQLite 的定位是 embedded formal state 與低操作成本；MongoDB 的定位是 document shape 與 schema governance；DynamoDB 的定位是 managed KV / document access pattern；Aurora 的定位是 managed SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 的定位是 global 或 distributed consistency。&lt;/p>
&lt;p>資料形狀段要讓讀者知道服務為哪種查詢與寫入模式付成本。Row model 適合交易與 ad-hoc query；document model 適合聚合資料與 schema flexibility；KV model 適合固定 access pattern；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL&lt;/a> 適合跨 region 一致性，但會把 latency、transaction retry 與成本模型帶進設計。&lt;/p></description><content:encoded><![CDATA[<p>資料庫 Vendor 文章撰寫規格的核心責任是把服務頁、深度文章與遷移 playbook 的分工固定下來。PostgreSQL 與 MySQL 已經提供 SQL baseline 的完整樣本；後續撰寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 時，應沿用同一組教學功能檢查，但保留每個服務自己的資料形狀、操作責任與失敗語言。</p>
<p>這份規格承接 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a> 與 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。本文只處理資料庫模組的落地規格：哪些內容留在 vendor overview，哪些議題升級成 deep article，哪些變更需要 migration playbook。</p>
<h2 id="判讀錨點">判讀錨點</h2>
<p>資料庫 vendor 文章的錨點是正式狀態如何被保存、查詢、複製、演進與修復。產品功能、版本差異與雲端價格都只是材料；正文要把材料轉成讀者可操作的判準，讓讀者能判斷資料模型、交易需求、查詢邊界、容量壓力、操作責任與替代路由。</p>
<p>PostgreSQL 與 MySQL 的 batch 顯示三個穩定事實。第一，SQL baseline 已經足以支撐其他服務頁開寫；第二，深度文章需要「何時不用」與真實案例 anchor 防止過度工程化；第三，跨 vendor 或 topology 變更需要獨立 playbook，不適合塞回 overview。</p>
<h2 id="vendor-overview-規格">Vendor Overview 規格</h2>
<p>Vendor overview 的責任是教讀者完成第一輪服務判斷。這一層回答服務承擔什麼資料責任、適合什麼壓力、日常有哪些操作決策、失效時先看哪些訊號，以及何時改走相鄰服務。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>這個服務承擔 SQL、embedded、document、KV 或 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> 哪一種責任</td>
          <td>開場段、教學路線、最短判讀路徑</td>
      </tr>
      <tr>
          <td>資料形狀</td>
          <td>資料是 row、document、key-value、time-series、geo 還是 global record</td>
          <td>適用場景、schema / index / partition 說明</td>
      </tr>
      <tr>
          <td>一致性與交易</td>
          <td>transaction、replica、multi-region 與 stale read 如何取捨</td>
          <td>適用場景、不適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>操作責任</td>
          <td>誰負責 backup、failover、upgrade、capacity、security 與 audit</td>
          <td>容量規劃要點、常見陷阱、下一步路由</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>什麼條件下改走 SQL、document、KV、managed SQL 或 distributed SQL</td>
          <td>同類對比、相鄰章節路由、下游 deep article</td>
      </tr>
      <tr>
          <td>案例與限制</td>
          <td>哪些案例能提供壓力訊號，哪些 claim 需要時間敏感標記</td>
          <td>案例對照、已知 limitation、後續擴充候選</td>
      </tr>
  </tbody>
</table>
<p>服務定位段要先把產品名稱放回資料庫分類語言。SQLite 的定位是 embedded formal state 與低操作成本；MongoDB 的定位是 document shape 與 schema governance；DynamoDB 的定位是 managed KV / document access pattern；Aurora 的定位是 managed SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 的定位是 global 或 distributed consistency。</p>
<p>資料形狀段要讓讀者知道服務為哪種查詢與寫入模式付成本。Row model 適合交易與 ad-hoc query；document model 適合聚合資料與 schema flexibility；KV model 適合固定 access pattern；<a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> 適合跨 region 一致性，但會把 latency、transaction retry 與成本模型帶進設計。</p>
<p>一致性與交易段要接回 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication lag</a> 與 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>。讀者需要知道的是哪種資料變更必須一起成功、哪種讀取可以接受延遲，以及跨 region 寫入是否值得支付協調成本。</p>
<p>操作責任段要把 managed 與 self-managed 的責任轉移寫清楚。自管服務保留控制權，團隊承擔 patch、backup、failover、capacity 與事故演練；managed 服務降低操作負擔，但增加平台限制、費用模型、版本節奏與 vendor-specific behavior。</p>
<p>替代邊界段要保留機會成本。PostgreSQL 或 MySQL 可以承擔多數 OLTP baseline；當 query 固定且高峰連線壓力明顯，DynamoDB 類服務可能更划算；當 document shape 主導資料模型，MongoDB 或 Cosmos DB 有更自然的操作語意；當 global write 是核心需求，Spanner、CockroachDB 或 Aurora DSQL 才進入主要比較。</p>
<p>案例與限制段要分開處理 evidence 與 backlog。案例提供流量形狀、資料形狀、失敗代價或回退路徑；limitation 承認正文還缺哪些維度，例如 PostgreSQL 目前仍需補 Security / RLS / audit logging、cross-region DR 與 managed PG 變體對比，MySQL 仍需補 deep article 的 anti-recommendation 與真實 incident anchor。</p>
<h2 id="deep-article-規格">Deep Article 規格</h2>
<p>Deep article 的責任是把 vendor overview 點到的單一機制展開成可操作教材。這一層不重寫服務選型，而是教讀者設定、觀測、除錯、容量估算與整合某個具體機制，例如 connection pool、replication topology、online schema change、<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">CDC</a>、partitioning、lock contention 或 PITR。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題情境</td>
          <td>什麼 production 壓力會讓這個機制變成主題</td>
          <td>開場場景、痛點、失效訊號</td>
      </tr>
      <tr>
          <td>核心機制</td>
          <td>該 vendor 如何實作這個能力，跟通用概念差在哪</td>
          <td>lifecycle、模式對照、內部元件責任</td>
      </tr>
      <tr>
          <td>操作流程</td>
          <td>讀者要如何配置、驗證、調整與演練</td>
          <td>step-by-step、config、query、command、驗證條件</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>哪些踩雷最常把服務推向事故</td>
          <td>production case、徵兆、根因、修法</td>
      </tr>
      <tr>
          <td>容量與觀測</td>
          <td>什麼 metric、query、log 或 cost signal 能判斷健康狀態</td>
          <td>容量規劃、觀測 metric、alert / dashboard route</td>
      </tr>
      <tr>
          <td>邊界與整合</td>
          <td>什麼條件下要換 sub-tool、改架構或回到 overview</td>
          <td>何時用、何時不用、sibling 對比、下一步路由</td>
      </tr>
  </tbody>
</table>
<p>問題情境段要用具體壓力啟動，產品文件定義只作為補充材料。Connection pool 可以從連線風暴與 backend slot 說起；replication 可以從 lag 與 failover 說起；PITR 可以從 restore 能力與 RPO 說起；lock contention 可以從交易範圍與 deadlock 訊號說起。</p>
<p>核心機制段要保留 vendor-specific 語意。PostgreSQL 的 WAL / LSN / replication slot、MVCC / vacuum、process-per-connection model 與 extension lifecycle 都有自己的操作語意；MySQL 的 binlog / GTID、InnoDB clustered index、gap / next-key lock、ProxySQL query rule 與 Vitess VSchema 也要用自己的語言展開。</p>
<p>操作流程段要把設定與判準綁在一起。Config、SQL、CLI 或 dashboard query 只在能支撐判讀時出現；每個操作要回答「如何知道它生效」「失敗時看到什麼」「可以停在哪個 rollback boundary」。</p>
<p>失敗模式段是 deep article 的主要價值。PostgreSQL / MySQL 既有文章多數已具備「5 個 Production 踩雷」；後續服務要維持這個密度，並優先補真實案例 anchor，避免所有案例都停在合成數字或典型設定。</p>
<p>容量與觀測段要讓 deep article 接回 04 / 09。資料庫機制常見的訊號包括 connection usage、replication lag、lock wait、dead tuple、buffer hit ratio、slow query、binlog retention、WAL growth、partition pruning 與 restore duration；這些訊號要能回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 或 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>。</p>
<p>邊界與整合段要補「何時不用」。MySQL audit 已經指出 deep article 容易缺 anti-recommendation；後續每篇 deep article 至少要有一段說明什麼規模、團隊能力或 workload 下暫時維持簡單設計更划算。</p>
<h2 id="hands-on--artifact-規格">Hands-on / Artifact 規格</h2>
<p>Hands-on / artifact 章節的責任是把 deep article 的機制判讀轉成可演練操作。這一層對齊 LLM <code>hands-on/</code> 的教學功能：讀者能跑出一個 local / staging lab，取得 config、query output、metric snapshot、validation result 或 rollback note，而不只停在概念理解。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lab scope</td>
          <td>這個操作在 local、staging、managed sandbox 哪裡跑</td>
          <td>Docker Compose、CLI、SQL script、preview environment</td>
      </tr>
      <tr>
          <td>Input</td>
          <td>需要哪些 schema、seed data、config、credential</td>
          <td>setup checklist、sample data、env var</td>
      </tr>
      <tr>
          <td>操作步驟</td>
          <td>讀者照順序做什麼</td>
          <td>command / SQL / dashboard step</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>怎麼知道操作成功、退化或失敗</td>
          <td>query output、metric snapshot、log、screenshot note</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>操作後哪些資料、帳號、route、backup 要清理</td>
          <td>teardown、rollback、retention note</td>
      </tr>
      <tr>
          <td>下一步路由</td>
          <td>操作結果要回到哪篇 deep article 或 migration</td>
          <td>overview、deep article、release gate、incident log</td>
      </tr>
  </tbody>
</table>
<p>PostgreSQL、MySQL 與 SQLite 已建立 hands-on 入口：<a href="/blog/backend/01-database/vendors/postgresql/hands-on/" data-link-title="PostgreSQL Hands-on 操作路線" data-link-desc="PostgreSQL local lab、connection pool、PITR restore drill、schema migration evidence 與 HA failover 的操作型章節設計">PostgreSQL hands-on</a>、<a href="/blog/backend/01-database/vendors/mysql/hands-on/" data-link-title="MySQL Hands-on 操作路線" data-link-desc="MySQL local lab、ProxySQL routing、online schema change、replication failover、backup restore 與 Vitess sandbox 的操作型章節設計">MySQL hands-on</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite hands-on</a>。後續其他 database vendor 也要先建立 hands-on 入口，再依服務責任決定是否補完整操作正文。</p>
<h2 id="migration-playbook-規格">Migration Playbook 規格</h2>
<p>Migration playbook 的責任是處理跨 vendor、跨 topology 或跨 operational model 的變更流程。這一層的主體是差異盤點、階段切換、雙軌驗證、cutover、rollback / fail-forward 與 cleanup；它應作為獨立流程教材，而非 deep article 的長版或 vendor overview 的補充段。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Driver</td>
          <td>為什麼要遷，壓力來自成本、容量、合規、operation 還是 paradigm</td>
          <td>開場 driver、no-go condition、替代方案</td>
      </tr>
      <tr>
          <td>Diff audit</td>
          <td>source / target 在 schema、operation、paradigm、component、application、topology 哪裡不同</td>
          <td>6 維 audit、主導差異、type 判定</td>
      </tr>
      <tr>
          <td>Phase plan</td>
          <td>哪些工作能分段，哪些工作必須 parallel run 或長期混合</td>
          <td>phase、stream、owner、驗證門檻</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>每個階段用什麼資料證明可前進</td>
          <td>validation query、row count、lag、error budget、cost</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>什麼條件下切流，切流期間誰決策</td>
          <td>cutover window、rollback condition、decision log route</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>哪些舊路徑能退役，哪些證據要保留</td>
          <td>contract removal、backup retention、incident write-back</td>
      </tr>
  </tbody>
</table>
<p>Driver 段要先排除「因為新服務比較好」這類空泛動機。有效 driver 通常是單機 primary 上限、connection limit、replication lag、backup / restore 責任、multi-region residency、vendor operation transfer、schema feature gap 或成本曲線。</p>
<p>Diff audit 段要先決定 playbook type。MySQL → PostgreSQL 主要是 schema / dialect 差；PostgreSQL → Aurora 主要是 operational redesign；PostgreSQL → CockroachDB 或 Aurora DSQL 主要是 paradigm shift；partition redesign 是 topology re-layout。type 決定結構，不用把所有 playbook 壓成同一套 phase。</p>
<p>Phase plan 段要把不可逆動作放晚。Schema audit、application compatibility、shadow read、dual-write、backfill、CDC catch-up、read-only cutover 與 cleanup 要分出驗證門檻；長期混合架構要明確標示哪些 workload 保留在 source。</p>
<p>Evidence 段要把資料庫遷移接回 observability 與 reliability。Playbook 應要求 row count、checksum、replication lag、error rate、query latency、data quality 與 owner；這些 evidence 是 release gate、incident decision log 與 rollback 判斷的共同材料。</p>
<p>Cutover 段要把決策權責寫清楚。資料庫切流失敗通常代價高，正文要標示切流窗口、暫停條件、回退條件、資料凍結策略與 decision owner，並連到 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 或 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<p>Cleanup 段要防止雙軌永久殘留。舊 schema、舊 writer、舊 CDC connector、舊 backup、舊 dashboard 與舊 runbook 都需要退役判準；資料保留、稽核與 incident write-back 要在 cleanup 前確認。</p>
<h2 id="從-postgresql--mysql-回收的調整項">從 PostgreSQL / MySQL 回收的調整項</h2>
<p>PostgreSQL 與 MySQL 的正文已經足以讓其他服務頁開寫。下一輪調整應集中在橫向品質；SQL baseline 可維持現有正文作為後續服務頁的比較基準。</p>
<h3 id="postgresql">PostgreSQL</h3>
<p>PostgreSQL 的下一輪擴充重點是補安全、災難復原與 managed variant。<a href="/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/" data-link-title="PostgreSQL Security / RLS / Audit Logging" data-link-desc="PostgreSQL role、grant、Row Level Security、pgAudit、log policy、PII access evidence 與合規路由">Security / RLS / audit logging</a> 可以連到資料保護與稽核章節；<a href="/blog/backend/01-database/vendors/postgresql/cross-region-dr/" data-link-title="PostgreSQL Cross-region DR" data-link-desc="PostgreSQL 跨區災難復原、physical replica、logical replication、backup restore、RPO / RTO 與 failover runbook">cross-region DR</a> 可以連到 reliability 與 incident decision；<a href="/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/" data-link-title="Managed PostgreSQL Comparison" data-link-desc="RDS PostgreSQL、Aurora PostgreSQL、Cloud SQL、Azure Database for PostgreSQL、Neon、Supabase、Crunchy Bridge 的責任邊界比較">Managed PG Comparison</a> 與 <a href="/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/" data-link-title="Specialized PostgreSQL Variants" data-link-desc="pgvectorscale、Citus、TimescaleDB、PostGIS、AlloyDB、Cosmos DB for PostgreSQL、serverless PG 等 PostgreSQL 變體的選型邊界">Specialized PostgreSQL Variants</a> 承接 AlloyDB、Cloud SQL、Cosmos DB for PostgreSQL 與 pgvectorscale。</p>
<p>PostgreSQL 的既有 limitation 已經標示 PG-favoring narrative 與時間敏感 claim。後續補文時要保留對手 vendor 的強項，例如專業 vector DB 的 scale、專業 time-series DB 的 ingestion、distributed SQL 的 global consistency 與 managed 平台的 operation transfer。</p>
<h3 id="mysql">MySQL</h3>
<p>MySQL 的下一輪擴充重點是補 anti-recommendation 與真實 case anchor。多數 deep article 已經有 production 踩雷，但還要加上「何時暫時不用這個機制」的段落，讓讀者知道維持單 primary、簡單 replication、原生 partition 或標準 backup 何時更划算；security、audit、Document Store、multi-source replication、HeatWave、memory contention 與 metadata lock 已先建立 outline 路由。</p>
<p>MySQL 的案例段要把 GitHub、Shopify、Slack、YouTube / Vitess 這些業界來源升級成具體 anchor。案例不只列公司名稱，還要回收它提供的流量形狀、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 策略、schema change 壓力、failover 責任或工具演化原因。</p>
<h2 id="後續服務撰寫順序">後續服務撰寫順序</h2>
<p>後續服務撰寫順序要從 SQL baseline 推進到資料模型與操作責任差異。每一篇先完成 vendor overview，再依 overview 暴露出的機制缺口決定 deep article 或 migration playbook。</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務</th>
          <th>開寫重點</th>
          <th>升級條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB2</td>
          <td>SQLite</td>
          <td>embedded formal state、local data、testing DB、backup 邊界</td>
          <td>local-first sync、edge deployment 或 file corruption</td>
      </tr>
      <tr>
          <td>DB3</td>
          <td>MongoDB / DynamoDB</td>
          <td>document shape、access pattern、partition key、capacity mode</td>
          <td>shard expansion、Atlas migration、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a></td>
      </tr>
      <tr>
          <td>DB4</td>
          <td>Aurora</td>
          <td>managed SQL、storage / compute 分離、failover、cost model</td>
          <td>PostgreSQL / MySQL 遷移、I/O-Optimized cost</td>
      </tr>
      <tr>
          <td>DB5</td>
          <td>Spanner / Cosmos DB</td>
          <td>global consistency、multi-region latency、consistency level</td>
          <td>regional rollout、API model migration</td>
      </tr>
      <tr>
          <td>DB6</td>
          <td>CockroachDB</td>
          <td>distributed SQL、transaction retry、range lease、compatibility</td>
          <td>PostgreSQL migration、multi-region topology</td>
      </tr>
  </tbody>
</table>
<p>SQLite 的重點是讓讀者知道單機正式狀態何時成立。它不應被寫成小型 PostgreSQL，而要處理 file lifecycle、embedded process boundary、backup、concurrency、migration 與測試資料責任。</p>
<p>MongoDB / DynamoDB 的重點是把資料形狀放在 SQL baseline 之後。MongoDB 應教 document shape、index、schema governance 與 transaction boundary；DynamoDB 應教 access pattern、partition key、capacity mode、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a> 與 connection-free scaling。</p>
<p>Aurora 的重點是 operation transfer。它把 PostgreSQL / MySQL 相容介面放進 AWS-managed operational model；storage / compute 分離、cluster endpoint、replica、backup、failover、cost model 與 AWS 限制都會改變團隊責任。</p>
<p>Spanner / Cosmos DB 的重點是 global data responsibility。Spanner 應教 TrueTime、strong consistency、multi-region latency 與 cost；Cosmos DB 應教 consistency level、API model、partition、RU 與 Azure 約束。</p>
<p>CockroachDB 的重點是 distributed SQL 對 application contract 的影響。SQL 相容降低導入門檻，但 transaction retry、range lease、hot range、schema feature gap 與 multi-region topology 會改變 application 與 SRE 的責任。</p>
<h2 id="llm-depth-下一輪擴章-backlog">LLM-depth 下一輪擴章 Backlog</h2>
<p>LLM-depth 下一輪的責任是把每個資料庫服務從 T1 overview 推進到可教學的章節群。Overview 只回答第一輪服務判斷；deep article 回答穩定運作與排錯；migration playbook 回答跨 vendor、跨 topology 或跨 operational model 變更。</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>目前狀態</th>
          <th>下一篇 deep article</th>
          <th>升級 playbook 候選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite</td>
          <td>T1 overview 已完成</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/teaching-structure/" data-link-title="SQLite Teaching Structure" data-link-desc="SQLite 服務章節群的大綱：從 embedded formal state、WAL、backup、test fixture、local-first、edge SQLite 到遷移路由">teaching structure</a> + <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a></td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite → PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite → D1 / Turso</a></td>
      </tr>
      <tr>
          <td>MongoDB</td>
          <td>T1 overview 已完成</td>
          <td>document shape governance、index / shard key</td>
          <td>self-managed → Atlas、document model → relational split</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>T1 overview 已完成</td>
          <td>partition key / hot partition、capacity mode</td>
          <td>DynamoDB → SQL / search / analytics split</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>T1 overview 已完成</td>
          <td>failover / endpoint routing、I/O cost model</td>
          <td>PostgreSQL / MySQL → Aurora、Aurora → distributed SQL</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>T1 overview 已完成</td>
          <td>TrueTime / transaction latency、multi-region topology</td>
          <td>regional SQL → Spanner</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td>T1 overview 已完成</td>
          <td>consistency level / RU budgeting、partitioning</td>
          <td>API model migration、Cosmos DB → specialized store</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>T1 overview 已完成</td>
          <td>transaction retry、range split / leaseholder</td>
          <td>PostgreSQL → CockroachDB、single-region → multi-region</td>
      </tr>
  </tbody>
</table>
<p>Backlog 的排序以學習梯度為準。SQLite 先處理單檔案正式狀態，補足「低操作成本如何 production 化」；MongoDB / DynamoDB 再處理資料形狀與 access pattern；Aurora 接 SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 最後處理 distributed consistency 與 multi-region topology。</p>
<h2 id="規格檢查清單">規格檢查清單</h2>
<p>資料庫 vendor 文章完成前要跑一次規格檢查。檢查通過代表本次內容可作為後續服務的基準；未通過時，先修正文再開下一篇。</p>
<ul>
<li>Vendor overview 已說清楚服務責任、資料形狀、一致性、操作責任、替代邊界、案例與 limitation。</li>
<li>Deep article 已包含問題情境、核心機制、操作流程、失敗模式、容量與觀測、邊界與整合。</li>
<li>Migration playbook 已完成 driver、diff audit、phase plan、evidence、cutover 與 cleanup。</li>
<li>表格後有情境化說明，沒有讓表格取代判讀。</li>
<li>案例提供壓力、失敗代價或回退條件，不只列公司名稱。</li>
<li>「何時不用」或 no-go condition 已出現在 deep article / migration playbook。</li>
<li>Time-sensitive vendor claim 有日期語境或指向官方文件。</li>
<li>下一步路由能接回主章、knowledge card、04 / 06 / 08 / 09 或 sibling vendor。</li>
</ul>
]]></content:encoded></item><item><title>Atlassian Statuspage → Instatus：status page 成本下降、但 compatibility audit 不能跳</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/migrate-to-instatus/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/atlassian-statuspage/migrate-to-instatus/</guid><description>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>Atlassian Statuspage（Business / Enterprise）&lt;/th>
 &lt;th>Instatus（Pro / Business）&lt;/th>
 &lt;th>差距判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>月費&lt;/td>
 &lt;td>Business 約 $399/mo、Enterprise 約 $1,499/mo 起&lt;/td>
 &lt;td>Pro 約 $20/mo、Business 約 $300/mo&lt;/td>
 &lt;td>savings 取決於 target tier&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Custom domain + SSL&lt;/td>
 &lt;td>內建&lt;/td>
 &lt;td>Free tier 起就含&lt;/td>
 &lt;td>持平&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subscriber 上限&lt;/td>
 &lt;td>依 tier 提升&lt;/td>
 &lt;td>Pro 約 5,000 subscriber、Business 約 25,000 subscriber&lt;/td>
 &lt;td>需對齊現有 subscriber 數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Component 上限&lt;/td>
 &lt;td>依 tier 提升&lt;/td>
 &lt;td>Pro 有上限、Business 放寬&lt;/td>
 &lt;td>大型 page 要逐項確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Notification channel&lt;/td>
 &lt;td>Email / SMS / Slack / Teams / webhook / RSS / Atom&lt;/td>
 &lt;td>Email / SMS / Slack / Discord / Teams / Telegram / RSS / Webhook&lt;/td>
 &lt;td>Instatus 多 chat channel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metrics 圖表&lt;/td>
 &lt;td>Datadog / Pingdom / New Relic / Library&lt;/td>
 &lt;td>Datadog / Pingdom / New Relic / StatusCake / API&lt;/td>
 &lt;td>payload / auth 要重接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SAML SSO&lt;/td>
 &lt;td>Enterprise tier&lt;/td>
 &lt;td>Business tier&lt;/td>
 &lt;td>不是產品缺口、是 tier 差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audit / activity log&lt;/td>
 &lt;td>Enterprise / team governance 能力&lt;/td>
 &lt;td>需依 plan 確認&lt;/td>
 &lt;td>強合規要逐項驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SLA / uptime report&lt;/td>
 &lt;td>內建能力較成熟&lt;/td>
 &lt;td>需確認 plan 或外接&lt;/td>
 &lt;td>contract deliverable 要驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API parity&lt;/td>
 &lt;td>完整 REST&lt;/td>
 &lt;td>REST API&lt;/td>
 &lt;td>endpoint / schema 不同&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>成本差距是這條 migration 的 &lt;em>driver&lt;/em>、但表格右側的 tier 差異是 &lt;em>blocker candidate&lt;/em>。對 &lt;em>不需要 Enterprise governance / 強 SLA reporting / 深 Atlassian 整合&lt;/em> 的中小 SaaS、從 Statuspage Business / Enterprise 降到 Instatus Pro / Business 可以有明顯 savings、cutover 工作量通常落在 1-4 週；對 &lt;em>enterprise 強合規&lt;/em> 的場景、SSO、audit、reporting 與可用性承諾任一不能讓步時、migration 要先停在 compatibility audit。&lt;/p></description><content:encoded><![CDATA[<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Atlassian Statuspage（Business / Enterprise）</th>
          <th>Instatus（Pro / Business）</th>
          <th>差距判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月費</td>
          <td>Business 約 $399/mo、Enterprise 約 $1,499/mo 起</td>
          <td>Pro 約 $20/mo、Business 約 $300/mo</td>
          <td>savings 取決於 target tier</td>
      </tr>
      <tr>
          <td>Custom domain + SSL</td>
          <td>內建</td>
          <td>Free tier 起就含</td>
          <td>持平</td>
      </tr>
      <tr>
          <td>Subscriber 上限</td>
          <td>依 tier 提升</td>
          <td>Pro 約 5,000 subscriber、Business 約 25,000 subscriber</td>
          <td>需對齊現有 subscriber 數</td>
      </tr>
      <tr>
          <td>Component 上限</td>
          <td>依 tier 提升</td>
          <td>Pro 有上限、Business 放寬</td>
          <td>大型 page 要逐項確認</td>
      </tr>
      <tr>
          <td>Notification channel</td>
          <td>Email / SMS / Slack / Teams / webhook / RSS / Atom</td>
          <td>Email / SMS / Slack / Discord / Teams / Telegram / RSS / Webhook</td>
          <td>Instatus 多 chat channel</td>
      </tr>
      <tr>
          <td>Metrics 圖表</td>
          <td>Datadog / Pingdom / New Relic / Library</td>
          <td>Datadog / Pingdom / New Relic / StatusCake / API</td>
          <td>payload / auth 要重接</td>
      </tr>
      <tr>
          <td>SAML SSO</td>
          <td>Enterprise tier</td>
          <td>Business tier</td>
          <td>不是產品缺口、是 tier 差異</td>
      </tr>
      <tr>
          <td>Audit / activity log</td>
          <td>Enterprise / team governance 能力</td>
          <td>需依 plan 確認</td>
          <td>強合規要逐項驗證</td>
      </tr>
      <tr>
          <td>SLA / uptime report</td>
          <td>內建能力較成熟</td>
          <td>需確認 plan 或外接</td>
          <td>contract deliverable 要驗證</td>
      </tr>
      <tr>
          <td>API parity</td>
          <td>完整 REST</td>
          <td>REST API</td>
          <td>endpoint / schema 不同</td>
      </tr>
  </tbody>
</table>
<p>成本差距是這條 migration 的 <em>driver</em>、但表格右側的 tier 差異是 <em>blocker candidate</em>。對 <em>不需要 Enterprise governance / 強 SLA reporting / 深 Atlassian 整合</em> 的中小 SaaS、從 Statuspage Business / Enterprise 降到 Instatus Pro / Business 可以有明顯 savings、cutover 工作量通常落在 1-4 週；對 <em>enterprise 強合規</em> 的場景、SSO、audit、reporting 與可用性承諾任一不能讓步時、migration 要先停在 compatibility audit。</p>
<p>這篇是 Type B drop-in migration playbook、結構順序是：先跑 <em>compatibility audit</em>（確認 gap 都可接受）→ 再進 cutover。Type B 看起來簡單、但跳過 audit 直接切是這 batch 第三常見的事故來源。</p>
<h2 id="為什麼是-type-b全-low">為什麼是 Type B（全 Low）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low</td>
          <td>component / incident / subscriber model 接近一致、欄位名稱 1:1</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Low</td>
          <td>都是 public status page + notification、ops 模型相同</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>同 paradigm（public service status disclosure）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
          <td>都是單一 SaaS</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Low</td>
          <td>API 端點換、payload 接近一致</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>都是 cloud SaaS</td>
      </tr>
  </tbody>
</table>
<p>全 Low → <strong>Type B drop-in + compatibility audit prefix</strong>。</p>
<h2 id="compatibility-audit-prefix">Compatibility audit prefix</h2>
<p>切換前先跑 audit、確認以下 9 項 <em>對自己的 case 是否可接受</em>。任一項是 <em>no</em>、回頭評估是否真要遷：</p>
<h3 id="1-subscriber-channel-完整度">1. Subscriber channel 完整度</h3>
<p>Statuspage 主要 channel：Email、SMS、Slack、Microsoft Teams、Webhook、RSS、Atom。Instatus 多了 Discord 跟 Telegram、少了 Atom（RSS 仍在）。</p>
<ul>
<li>確認現有 subscriber 用的 channel 都在 Instatus 支援列表</li>
<li>特別注意 <em>legacy RSS Atom feed reader</em> — 有些 monitoring service 用 Atom 訂閱、要改成 RSS 或 webhook</li>
</ul>
<h3 id="2-saml-sso">2. SAML SSO</h3>
<p>SAML SSO 是 <em>tier decision</em>、不是單純產品有無。Statuspage 把 SAML 放在較高 tier；Instatus 也在 Business tier 提供 SAML。真正要判斷的是：成本 savings 是否仍成立、以及 IdP / SCIM / role mapping 是否符合 audit 要求。</p>
<ul>
<li>確認 target Instatus plan 是否包含 SAML</li>
<li>確認 IdP / group / role mapping 是否能對上現有 audit requirement</li>
<li>如果 savings 只在 Pro tier 成立、但 compliance 要 SAML，就不能用 Pro tier 當 ROI 基準</li>
</ul>
<h3 id="3-audit-log">3. Audit log</h3>
<p>Audit log 是 governance surface。誰 publish 哪則 incident、誰改了哪個 component status、誰匯入 subscriber，這些事件在 Statuspage Enterprise / Instatus Business 類 plan 的支援深度與匯出能力要逐項比對。</p>
<ul>
<li>確認 status page 變更是否需要 internal audit trail</li>
<li>確認 target plan 是否能查詢、匯出與保留 admin activity</li>
<li>金融 / 醫療場景要把 audit retention 與 evidence export 放進 go/no-go gate</li>
</ul>
<h3 id="4-sla--uptime-report-自動產出">4. SLA / uptime report 自動產出</h3>
<p>SLA / uptime report 是 customer contract surface。Statuspage 的 enterprise workflow 通常更成熟；Instatus 是否能直接覆蓋，要看 plan、API 與既有客戶報表格式。</p>
<ul>
<li>如果 contract 寫了「每月 SLA report 自動推送客戶」、Instatus 要外接補這條</li>
<li>評估外接成本（一條 cron + 一個 BI dashboard、3-5 天工程）vs Statuspage 內建</li>
</ul>
<h3 id="5-可用性承諾與-provider-outage">5. 可用性承諾與 provider outage</h3>
<p>Status page provider 本身的可用性承諾是 compatibility audit 的一部分。強合規或大型 customer-facing page 要確認 provider SLA、status page provider 自身 outage 時的 fallback、以及是否需要獨立備援頁。</p>
<ul>
<li>多數場景能接受 status page provider 跟自己 service 不同供應商已經足夠</li>
<li>強合規 + 「status page must never be down」場景要設獨立 fallback，而不是只比較 UI 功能</li>
</ul>
<h3 id="6-metrics-integration-來源">6. Metrics integration 來源</h3>
<p>兩家都接 Datadog / Pingdom / New Relic / StatusCake / Library API。Instatus 多了 StatusCake、少了某些 Statuspage 內建 library。</p>
<ul>
<li>確認當前 metrics 顯示圖表的 source 在 Instatus 支援列表</li>
<li>特別注意 <em>custom metrics from API</em>（自家 push 上去的）— 兩家都支援、payload 格式不同、要重寫 push script</li>
</ul>
<h3 id="7-custom-css--branding-完整度">7. Custom CSS / branding 完整度</h3>
<p>Statuspage Enterprise 允許 <em>完整 custom CSS override</em>、Instatus Pro / Team 允許 <em>theme customization</em>（颜色 / logo / font）但 <em>不允許任意 CSS injection</em>。</p>
<ul>
<li>如果有大量 custom CSS 跟既有品牌 site 視覺 1:1 對齊、Instatus 可能達不到、要評估視覺退讓</li>
<li>大多數 status page 視覺 ≠ 主 product site、退讓常見</li>
</ul>
<h3 id="8-api-parity-跟自動化-hook">8. API parity 跟自動化 hook</h3>
<p>兩家都有完整 REST API（create incident、update component status、push subscriber）。但 <em>endpoint URL / auth scheme / payload schema 不同</em>：</p>
<ul>
<li>Statuspage：<code>https://api.statuspage.io/v1/pages/{page_id}/...</code>、OAuth bearer token</li>
<li>Instatus：<code>https://api.instatus.com/v1/{page_id}/...</code>、API key header</li>
</ul>
<p>如果有 <em>從 IR 平台（incident.io / Rootly / FireHydrant / 自製 webhook）push status update</em> 的自動化、要重寫對接、估算 2-5 天工程。</p>
<h3 id="9-atlassian-生態整合opsgenie--jsm--confluence">9. Atlassian 生態整合（Opsgenie / JSM / Confluence）</h3>
<p>Statuspage 跟 Opsgenie / JSM / Confluence 同生態、有原生整合（Opsgenie incident → Statuspage incident draft、Confluence post-mortem auto-link）。Instatus 跟 Atlassian 沒原生整合、要走 webhook。</p>
<ul>
<li>如果 Atlassian 整合是核心 workflow、評估走 webhook 工作量</li>
<li>如果是 incident.io / Rootly / FireHydrant 主用、Instatus 反而有原生整合（這條變優勢）</li>
</ul>
<h2 id="cutover-階段">Cutover 階段</h2>
<p>Audit 全過後、Type B drop-in 不需要 11-phase 結構、4 階段：</p>
<h3 id="stage-1setup--parallel-run1-週">Stage 1：Setup + parallel run（1 週）</h3>
<ul>
<li>在 Instatus 開帳號、設 component（先複製 Statuspage 結構 1:1）</li>
<li>設 custom domain + SSL（Instatus 預設 free tier 已含）</li>
<li>接 subscriber channels（先不切 DNS、純內部測試）</li>
<li>用 Instatus API 從 Statuspage export incident history 灌回 Instatus（保留歷史 uptime 連續性）</li>
<li>Parallel run：當前若有 incident、在 Statuspage 跟 Instatus 兩邊都 push、確認 subscriber 在兩邊都收到、UI 都正常</li>
</ul>
<h3 id="stage-2dns-預備1-天">Stage 2：DNS 預備（1 天）</h3>
<ul>
<li>Statuspage custom domain CNAME / ALIAS 預設 TTL 通常 1 小時、提前 48 小時把 TTL 降到 5 分鐘</li>
<li>這步是 minimize cutover window 的關鍵、不做的話 cutover 期間有 1 小時 DNS cache 兩邊 page 不同步</li>
</ul>
<h3 id="stage-3dns-cutover30-分鐘---1-小時">Stage 3：DNS cutover（30 分鐘 - 1 小時）</h3>
<ul>
<li>把 status page custom domain 從 Statuspage CNAME 改指 Instatus CNAME</li>
<li>5 分鐘 TTL 後新流量都進 Instatus</li>
<li>監控 1 小時、確認 subscriber notification 從 Instatus 發出、metrics 圖表 wire 正確、history uptime continuity 沒斷</li>
<li>既有 IR 平台 webhook 改指 Instatus API endpoint</li>
</ul>
<h3 id="stage-4statuspage-關閉2-4-週後">Stage 4：Statuspage 關閉（2-4 週後）</h3>
<ul>
<li>不要立即取消 Statuspage 帳號 — 留 2-4 週作 rollback 緩衝</li>
<li>Subscriber 通知「status page URL 不變、underlying provider 換了」（多數場景不需要、subscriber 不會察覺）</li>
<li>確認 incident history / uptime data 在 Instatus 完整、Statuspage rollback 場景 &lt; 0.5% 後、取消 Statuspage subscription</li>
</ul>
<p>完成標準：DNS 100% 流量在 Instatus、Statuspage subscription 取消、SRE / SaaS provisioning team 不再 maintain Statuspage account。</p>
<h2 id="5-個-production-踩雷">5 個 production 踩雷</h2>
<h3 id="1-sso-tier-選錯導致-admin-login-退化">1. SSO tier 選錯導致 admin login 退化</h3>
<p>audit 漏掉 <em>當前 admin 用 SAML 登入</em> 這個事實、卻用不含 SAML 的 target tier 計算 savings，cutover 後 admin login 被迫退回 email/password + 2FA。修法是 Stage 1 就用含 SAML 的 target plan 測試 IdP、group mapping 與 break-glass admin。對 SOC 2 audit 期間 <em>admin login method 變更要記錄</em>的 org 來說，這是不可預期的 audit finding、要在 Stage 1 就溝通。</p>
<h3 id="2-metrics-圖表來源整合斷">2. Metrics 圖表來源整合斷</h3>
<p>Statuspage 接 Datadog metrics 的 OAuth integration 在 Instatus 要重接、auth flow 重做、Datadog API key 重 provision。常見漏網之魚：</p>
<ul>
<li>跨 region Datadog account（US / EU）integration 重 provision 時 region 沒選對、圖表全空</li>
<li>Pingdom check ID 在新 integration 重新 register、historic data 斷層</li>
<li>自家 push metrics 的 webhook payload schema 不同（Statuspage 是 <code>{component_id, status, ...}</code>、Instatus 是 <code>{componentId, status, ...}</code> camelCase）</li>
</ul>
<p>修法是 Stage 1 parallel run 期間就把所有 metrics integration 在 Instatus wire 通、對比兩邊圖表一致再進 Stage 2。</p>
<h3 id="3-subscriber-import-format-不一致">3. Subscriber import format 不一致</h3>
<p>Statuspage subscriber export CSV 是 <code>email, phone, slack_webhook_url, ...</code> 一行多 channel；Instatus import CSV 是 <code>email\nemail\n...</code> 純 email list、其他 channel 要分開 import。如果有 5000 subscriber 包含 SMS / Slack mix、import 時要拆開、否則 SMS subscriber 會掉。</p>
<p>修法是寫 import script 把 Statuspage CSV 拆成多個 channel-specific CSV、分批 import Instatus。</p>
<h3 id="4-sla-report-月報突然斷">4. SLA report 月報突然斷</h3>
<p>Statuspage 月報自動 push 給客戶、cutover 後 Instatus 沒原生 SLA report、客戶下個月沒收到報表會問。修法是 <em>cutover 前先建外接 SLA report</em>：</p>
<ul>
<li>寫 cron job（per month）從 Instatus API 拉 component uptime data</li>
<li>用簡單 template（Google Doc / PDF generator）產 report</li>
<li>自動 email 推給原 Statuspage SLA report distribution list</li>
</ul>
<p>如果這條 contract 強制、外接成本約 3-5 天工程、要算進 migration 總成本。</p>
<h3 id="5-custom-css--branding-視覺退讓">5. Custom CSS / branding 視覺退讓</h3>
<p>Statuspage Enterprise 有大量 custom CSS、cutover 後 Instatus 視覺對齊不到 1:1。視覺退讓清單通常是：</p>
<ul>
<li>font weight 跟 line-height 微差</li>
<li>mobile breakpoint 不同</li>
<li>incident timeline 排版 spacing 略不同</li>
</ul>
<p>修法是 cutover 前先在 Instatus theme customization 內把能調的調好、能接受的退讓在 Stage 1 跟設計 / brand team 確認、不能接受的就回去 audit Step 7 重新評估是否要遷。</p>
<h2 id="容量與成本對比">容量與成本對比</h2>
<p>對中小 SaaS（3000 subscriber、10 component、月均 2 incident）：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Statuspage Business</th>
          <th>Instatus Pro</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月費</td>
          <td>約 $399</td>
          <td>約 $20</td>
      </tr>
      <tr>
          <td>Subscriber 上限</td>
          <td>依 plan</td>
          <td>約 5,000</td>
      </tr>
      <tr>
          <td>Component</td>
          <td>依 plan</td>
          <td>有上限</td>
      </tr>
      <tr>
          <td>工程成本（cutover）</td>
          <td>-</td>
          <td>1-4 週</td>
      </tr>
      <tr>
          <td>外接 SLA report</td>
          <td>不需要或較成熟</td>
          <td>0-5 天 / 持續維運</td>
      </tr>
      <tr>
          <td>年化 saving</td>
          <td>-</td>
          <td>約數千美元等級</td>
      </tr>
  </tbody>
</table>
<p>對 enterprise（30000 subscriber、50+ component、強合規）：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Statuspage Enterprise</th>
          <th>Instatus Business / Enterprise</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月費</td>
          <td>約 $1,499 起或 custom</td>
          <td>低於典型 Enterprise quote</td>
      </tr>
      <tr>
          <td>SAML / Audit log</td>
          <td>必要</td>
          <td>需逐項驗證</td>
      </tr>
      <tr>
          <td>SLA / uptime report</td>
          <td>必要</td>
          <td>需逐項驗證或外接</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>未必適合遷</td>
          <td>先跑 audit、不要只看月費</td>
      </tr>
  </tbody>
</table>
<h2 id="何時不要切">何時不要切</h2>
<ul>
<li><strong>SAML SSO + audit log 是 compliance requirement</strong>：金融 / 醫療 / 政府場景、Statuspage Enterprise 留</li>
<li><strong>SLA report 是 customer contract 強制</strong>：如果 contract 寫明 SLA report deliverable、外接成本 + 風險高、Statuspage 留</li>
<li><strong>Provider availability / fallback 必要</strong>：status page provider 自身 outage 時仍要可訪、先設獨立 fallback 或保留 Enterprise 級 provider</li>
<li><strong>Atlassian 整合（Opsgenie / JSM / Confluence）是核心 workflow</strong>：原生整合斷會多很多 webhook 維護、Statuspage 留</li>
<li><strong>subscriber &gt; 10K + 強客戶 SLA</strong>：規模本身讓 Instatus 風險增大、Statuspage Enterprise 比較穩</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行 batch：<a href="/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/" data-link-title="PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract" data-link-desc="PagerDuty → incident.io 不是 schema translation — 兩家的「on-call」字面相同、contract 不同（alert routing vs IR coordination &#43; Slack-native &#43; retrospective）。本文走 Type E paradigm shift、6 維 audit 顯示 paradigm / schema / operational 三軸 High、用 4-phase partial migration（不收斂、Phase 1-2 多數 org 停留）、5 個 production 踩雷（雙系統 state drift / severity 翻譯失真 / schedule layer 漏 / Slack channel 過載 / retrospective 斷層）、跟 PagerDuty Process Automation / AIOps 沒對應的 capability gap">PagerDuty → incident.io</a>（Type E paradigm shift）/ <a href="/blog/backend/08-incident-response/vendors/opsgenie/migrate-from-pagerduty/" data-link-title="PagerDuty → Opsgenie：Atlassian 全家桶整合 vs Opsgenie 2027 EOL 的 vendor consolidation 取捨" data-link-desc="PagerDuty → Opsgenie 是 Type A phased schema translation、但 Atlassian 已宣布 Opsgenie 2027-04 EOL — 這條 migration 只在 Atlassian-heavy org &#43; 明確 JSM unification roadmap 下成立、本質是 PD → Opsgenie → JSM Cloud 的雙 hop migration。本文走 6 維 audit（Schema Medium-High 其他 Low）、PagerDuty ↔ Opsgenie ↔ JSM field mapping 對照、5 production 踩雷（escalation step / Heartbeat 缺對應 / integration key dedup 重設 / schedule 時區 / Atlassian Identity SSO 整合）、何時直接走 PD → JSM 跳過 Opsgenie">PagerDuty → Opsgenie</a>（Type A schema translation）</li>
<li>同 batch Type B：（待補、本篇是 batch 唯一 Type B）</li>
<li>vendor 對照：<a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/" data-link-title="Atlassian Statuspage" data-link-desc="公開狀態頁 SaaS、Atlassian 出品、enterprise polish &#43; Atlassian 生態整合、subscriber notification &#43; component dependency 是核心責任">Atlassian Statuspage</a> / <a href="/blog/backend/08-incident-response/vendors/instatus/" data-link-title="Instatus" data-link-desc="輕量 status page SaaS、現代 UI、價格敏感替代">Instatus</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type B drop-in + compatibility audit prefix 結構說明）</li>
</ul>
]]></content:encoded></item><item><title>JMeter → k6：k6 不是 JMeter 的「script 版本」、是 VU model 取代 thread model</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/migrate-from-jmeter/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/k6/migrate-from-jmeter/</guid><description>&lt;p>k6 不是 JMeter 的 &lt;em>「script 版本」&lt;/em>。&lt;/p>
&lt;p>這個誤解是 JMeter → k6 migration 第一週最常見的事故來源。Migration 啟動會議常聽到「JMeter 的 thread group 翻成 k6 的 VU 就好了吧」、然後團隊把 &lt;code>.jmx&lt;/code> 內 100 thread → k6 &lt;code>vus: 100&lt;/code>、跑下去發現 RPS 差三倍、p95 延遲表完全不同形狀、以為 k6 壞了。&lt;/p>
&lt;p>實際上 k6 的 &lt;em>Virtual User (VU)&lt;/em> 跟 JMeter 的 &lt;em>Thread&lt;/em> 是 &lt;em>兩種不同的使用者行為建模方式&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>JMeter Thread&lt;/strong>：一個 OS thread = 一個 user、&lt;code>numThreads=100&lt;/code> 就 &lt;em>固定 100 個 concurrent 使用者一直跑&lt;/em>、ramp-up period 控制怎麼啟動、無 explicit arrival rate 概念&lt;/li>
&lt;li>&lt;strong>k6 VU&lt;/strong>：一個 goroutine-like execution context、預設 &lt;code>vus&lt;/code> 是 &lt;em>concurrent VU pool&lt;/em>、但 k6 更推薦用 &lt;code>arrival-rate executor&lt;/code> — 直接表達 &lt;em>每秒進來幾個 request&lt;/em>、VU 是 &lt;em>為了達到 arrival rate 動態起的 worker&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>差別在 &lt;em>測量視角&lt;/em>：JMeter 預設視角是 &lt;em>「我有 100 個使用者在用系統」&lt;/em>、k6 預設視角是 &lt;em>「我每秒有 N 個請求進來」&lt;/em>。兩種視角下 &lt;em>同一個系統的瓶頸結果完全不同&lt;/em>：100 concurrent user 模型在 server 慢時 throughput 會自動降（user 等回應）、100 RPS arrival rate 模型在 server 慢時 queue 會累積、暴露 &lt;em>真實 production behavior&lt;/em>（user 不會體諒、會繼續送請求）。&lt;/p>
&lt;p>這篇 migration playbook 不是 schema translation 文（&lt;code>.jmx&lt;/code> 翻成 &lt;code>.js&lt;/code> 只是表面）、是 &lt;em>paradigm shift&lt;/em> — 從 closed-system model（thread）到 open-system model（arrival rate）的視角轉換。&lt;/p>
&lt;h2 id="為什麼是-type-eschema--paradigm-同-high">為什麼是 Type E（schema + paradigm 同 High）&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&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>Schema&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>&lt;code>.jmx&lt;/code> XML vs JavaScript scenario、test plan 完全不同 file format / DSL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;td>CLI / distributed run 接近、CI integration 差別大、distributed runner 模型不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>thread group closed model → arrival rate open model、測試思維不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>都是 load test runner、no multi-tool decomposition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>App change&lt;/td>
 &lt;td>N/A&lt;/td>
 &lt;td>是 test code、不是 production code&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>都是 CLI / runner 跑、無 sharding&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Schema High + Paradigm High 兩軸 High。按優先序 Schema &amp;gt; Paradigm、預設選 Type A。但對 JMeter → k6 的讀者來說、&lt;em>paradigm shift 才是難關&lt;/em> — schema translation 是工作量、但搞錯 paradigm 會讓 migration 後的測試結果 &lt;em>跟 production 不對應&lt;/em>。所以選 &lt;strong>Type E paradigm shift&lt;/strong> 結構、schema translation 抽出 Phase 1-2 補充。&lt;/p></description><content:encoded><![CDATA[<p>k6 不是 JMeter 的 <em>「script 版本」</em>。</p>
<p>這個誤解是 JMeter → k6 migration 第一週最常見的事故來源。Migration 啟動會議常聽到「JMeter 的 thread group 翻成 k6 的 VU 就好了吧」、然後團隊把 <code>.jmx</code> 內 100 thread → k6 <code>vus: 100</code>、跑下去發現 RPS 差三倍、p95 延遲表完全不同形狀、以為 k6 壞了。</p>
<p>實際上 k6 的 <em>Virtual User (VU)</em> 跟 JMeter 的 <em>Thread</em> 是 <em>兩種不同的使用者行為建模方式</em>：</p>
<ul>
<li><strong>JMeter Thread</strong>：一個 OS thread = 一個 user、<code>numThreads=100</code> 就 <em>固定 100 個 concurrent 使用者一直跑</em>、ramp-up period 控制怎麼啟動、無 explicit arrival rate 概念</li>
<li><strong>k6 VU</strong>：一個 goroutine-like execution context、預設 <code>vus</code> 是 <em>concurrent VU pool</em>、但 k6 更推薦用 <code>arrival-rate executor</code> — 直接表達 <em>每秒進來幾個 request</em>、VU 是 <em>為了達到 arrival rate 動態起的 worker</em></li>
</ul>
<p>差別在 <em>測量視角</em>：JMeter 預設視角是 <em>「我有 100 個使用者在用系統」</em>、k6 預設視角是 <em>「我每秒有 N 個請求進來」</em>。兩種視角下 <em>同一個系統的瓶頸結果完全不同</em>：100 concurrent user 模型在 server 慢時 throughput 會自動降（user 等回應）、100 RPS arrival rate 模型在 server 慢時 queue 會累積、暴露 <em>真實 production behavior</em>（user 不會體諒、會繼續送請求）。</p>
<p>這篇 migration playbook 不是 schema translation 文（<code>.jmx</code> 翻成 <code>.js</code> 只是表面）、是 <em>paradigm shift</em> — 從 closed-system model（thread）到 open-system model（arrival rate）的視角轉換。</p>
<h2 id="為什麼是-type-eschema--paradigm-同-high">為什麼是 Type E（schema + paradigm 同 High）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>High</td>
          <td><code>.jmx</code> XML vs JavaScript scenario、test plan 完全不同 file format / DSL</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Medium</td>
          <td>CLI / distributed run 接近、CI integration 差別大、distributed runner 模型不同</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>High</td>
          <td>thread group closed model → arrival rate open model、測試思維不同</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
          <td>都是 load test runner、no multi-tool decomposition</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>N/A</td>
          <td>是 test code、不是 production code</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>都是 CLI / runner 跑、無 sharding</td>
      </tr>
  </tbody>
</table>
<p>Schema High + Paradigm High 兩軸 High。按優先序 Schema &gt; Paradigm、預設選 Type A。但對 JMeter → k6 的讀者來說、<em>paradigm shift 才是難關</em> — schema translation 是工作量、但搞錯 paradigm 會讓 migration 後的測試結果 <em>跟 production 不對應</em>。所以選 <strong>Type E paradigm shift</strong> 結構、schema translation 抽出 Phase 1-2 補充。</p>
<h2 id="driverdeveloper-ergonomic--ci-gate-friendly">Driver：developer ergonomic + CI gate friendly</h2>
<p>從 JMeter 遷出 k6 的核心拉力是 <em>developer ergonomic + CI 友善</em>：</p>
<ul>
<li><strong><code>.jmx</code> XML 在 git 內 diff 不可讀</strong>：兩個 <code>.jmx</code> PR 的 diff 是 XML attribute reorder noise、reviewer 看不出來實際邏輯改了什麼；JavaScript 是純文字 + AST、PR diff 直接可讀</li>
<li><strong>GUI 學習曲線</strong>：JMeter GUI 不是現代 IDE、不熟的工程師寫一個 scenario 要花半天找對的 sampler 跟 listener；JavaScript 用既有 IDE（VS Code / IntelliJ）、autocomplete + lint + format 全有</li>
<li><strong>CI integration 步驟差</strong>：JMeter 在 CI 跑要 packaging plugin + non-GUI mode + result XML parser；k6 直接 <code>k6 run script.js</code>、result 是 JSON / Prometheus metrics、threshold pass/fail 直接 exit code</li>
<li><strong>單機 VU 容量</strong>：JMeter 單機通常 ~500-1000 thread（受 JVM 跟 OS thread limit）、k6 單機可跑 30K-50K VU（Go runtime + goroutine）、distributed runner 需求降低</li>
<li><strong>Workload model expressiveness</strong>：k6 <code>arrival-rate executor</code> + <code>ramping-vus</code> + <code>constant-vus</code> 三種 executor 直接對應 <em>open system / ramping / closed system</em> 三種測量視角、不像 JMeter 需要組合 Constant Throughput Timer + Synchronizing Timer + thread group 才達到</li>
</ul>
<p>這條 driver 在 <em>QA 團隊 GUI 維護 .jmx asset</em> 的 org 沒拉力（GUI 反而是優勢）、但對 <em>dev / SRE 寫 performance test 進 CI</em> 的 org 是強拉力。Audience 不同、migration value 完全不同。</p>
<h2 id="4-phase-partial-migration不收斂">4-phase partial migration（不收斂）</h2>
<p>Type E 的特徵是 <em>不收斂</em> — 多數 org 不會把 <code>.jmx</code> 全退役、會停在某個 phase 變成 hybrid：</p>
<h3 id="phase-1學會-k6-paradigm不寫實際-test">Phase 1：學會 k6 paradigm（不寫實際 test）</h3>
<p>寫一個 throwaway script 跑當前 production-like API、不為了 migrate、為了搞清楚 k6 paradigm：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">import</span> <span class="nx">http</span> <span class="nx">from</span> <span class="s1">&#39;k6/http&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">check</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;k6&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// 不要用 vus: 100、用 arrival rate
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="nx">scenarios</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">open_model</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">executor</span><span class="o">:</span> <span class="s1">&#39;constant-arrival-rate&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nx">rate</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span>           <span class="c1">// 每秒 100 request
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>      <span class="nx">timeUnit</span><span class="o">:</span> <span class="s1">&#39;1s&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5m&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nx">preAllocatedVUs</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="c1">// 預先準備 VU 數
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>      <span class="nx">maxVUs</span><span class="o">:</span> <span class="mi">500</span><span class="p">,</span>          <span class="c1">// 上限
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>    <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="nx">thresholds</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nx">http_req_duration</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;p(95)&lt;500&#39;</span><span class="p">],</span> <span class="c1">// p95 &lt; 500ms
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>    <span class="nx">http_req_failed</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;rate&lt;0.01&#39;</span><span class="p">],</span>   <span class="c1">// 失敗率 &lt; 1%
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;https://api.example.com/orders&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">check</span><span class="p">(</span><span class="nx">res</span><span class="p">,</span> <span class="p">{</span> <span class="s1">&#39;status 200&#39;</span><span class="o">:</span> <span class="p">(</span><span class="nx">r</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>對比同一個 test 用 <code>.jmx</code> 寫的形狀、思考 <em>為什麼 arrival rate 跟 thread group 測出來不一樣</em>。這 phase 的目標是 <em>paradigm internalization</em>、不是產出 migration artifact。團隊每個寫 performance test 的人都要過這一關、不能跳。</p>
<p>完成標準：寫的人能講清楚「arrival rate 100 / 5 分鐘」跟「100 thread / 5 分鐘 ramp-up」的 production behavior 差異。</p>
<h3 id="phase-2高價值-critical-path-改-k6gui-留-jmeter">Phase 2：高價值 critical path 改 k6（GUI 留 JMeter）</h3>
<p>選 <em>最常跑 + 最重要</em> 的 1-3 條 scenario 改寫 k6、不全部一次轉。典型候選：</p>
<ul>
<li>Pre-release smoke test（核心 API 的 baseline check）</li>
<li>Nightly regression（per-commit performance gate）</li>
<li>Peak readiness rehearsal scenario（活動前 T-7 跑的 stress test）</li>
</ul>
<p>GUI / QA 團隊維護的 <code>.jmx</code> <em>不動</em> — 那些通常是 multi-protocol（JDBC / JMS / FTP）、不在 k6 適合 scope。</p>
<p>工作主要塊：</p>
<ul>
<li><code>.jmx</code> thread group → k6 scenario executor 的 <em>paradigm-correct</em> 翻譯（不是欄位翻譯）</li>
<li>HTTP request 跟 assertion 翻譯（payload / header / cookies）</li>
<li>CSV data source（JMeter CSV Data Set Config）→ k6 <code>SharedArray</code> from JSON</li>
<li>結果輸出 schema 改變（XML / JTL → JSON / Prometheus / k6 Cloud）</li>
<li>CI integration 重做（GitHub Actions / GitLab CI 直接 <code>k6 run</code>、不需要 packaging）</li>
</ul>
<p>完成標準：critical path 的 k6 baseline 跟 <code>.jmx</code> baseline 數據對比一致（p50 / p95 / throughput 在 10% 誤差內、行為不一致時知道是 paradigm 差還是 bug）。</p>
<h3 id="phase-3qa-團隊雙工具技能hybrid-穩定形態">Phase 3：QA 團隊雙工具技能（hybrid 穩定形態）</h3>
<p>很多 org 停在這個 phase：QA 團隊用 GUI 維護 multi-protocol .jmx（covering JDBC / JMS / LDAP / SOAP / FTP）、dev / SRE 用 k6 維護 HTTP / gRPC / WebSocket performance test in CI。Two-tool stack 不是 broken state、是 <em>not-converged-by-design</em>。</p>
<p>這個 phase 的工作主要塊：</p>
<ul>
<li>文件化：哪類 test 用 k6、哪類用 JMeter、決策樹寫在 team handbook</li>
<li>結果整合：兩個工具的 metrics 都進同一個 Grafana dashboard（k6 → Prometheus 直接、JMeter → InfluxDB / Prometheus exporter）</li>
<li>Release gate 用 k6 為主（CI 整合直接）、JMeter 用於 manual QA campaign / multi-protocol 場景</li>
</ul>
<p>多數 org 不進 Phase 4。</p>
<h3 id="phase-4jmeter-退役少見">Phase 4：JMeter 退役（少見）</h3>
<p>只有當 <em>所有 protocol 都換到 k6 extension</em> 或 <em>捨棄了 multi-protocol coverage</em> 時、才 fully 退役 JMeter。常見路徑：</p>
<ul>
<li>用 k6 xk6 extensions 補 protocol（xk6-sql for JDBC、xk6-kafka for Kafka、xk6-amqp for RabbitMQ、xk6-mqtt for MQTT）</li>
<li>評估每個 extension 的 maturity / community support — xk6 ecosystem 比 JMeter plugin 小很多</li>
<li>接受 part of legacy <code>.jmx</code> test 直接 deprecate（covered by integration test 而非 load test）</li>
</ul>
<p>完成標準：所有 protocol 都在 k6 + xk6 內可表達、<code>.jmx</code> 全部 archive。</p>
<h2 id="5-個-production-踩雷">5 個 production 踩雷</h2>
<h3 id="1-thread-group--vu-直接翻譯最常見phase-2-必踩">1. Thread group → VU 直接翻譯（最常見、Phase 2 必踩）</h3>
<p>把 <code>numThreads=100</code> 翻成 <code>vus: 100</code> 就完事 — 結果 RPS 跟 JMeter 不一致、p95 完全不同形狀。原因：JMeter 100 thread 是 <em>closed model</em>（thread 等回應才送下一個）、k6 <code>vus: 100</code> 預設也是 closed model、但 <em>iteration 結束就立刻送下一個</em>（無 think time）— 兩者的 <em>throughput 行為</em> 差異來自 think time / response time。</p>
<p>修法：</p>
<ul>
<li>不用 <code>vus: N</code>、用 <code>constant-arrival-rate</code> 或 <code>ramping-arrival-rate</code>、直接表達 <em>每秒幾個請求</em></li>
<li>如果一定要 closed model（pre-existing JMeter scenario 對比）、在 default function 內加 <code>sleep(thinkTime)</code> 模擬 JMeter Think Time</li>
</ul>
<h3 id="2-arrival-rate-vs-concurrent-vu-混淆">2. Arrival rate vs concurrent VU 混淆</h3>
<p><code>arrival-rate</code> executor 的 <code>rate: 100</code> 意思是 <em>每秒進來 100 request</em>、<code>preAllocatedVUs: 200</code> 是 <em>預先準備 200 個 VU worker pool</em>。如果 service 變慢（p95 從 100ms 飄到 500ms）、需要的 VU 數會從 100/sec * 0.1s = 10 暴增到 100/sec * 0.5s = 50、<code>preAllocatedVUs</code> 不夠就會 warning「ran out of VUs」、實際 arrival rate 達不到 spec。</p>
<p>修法：</p>
<ul>
<li><code>preAllocatedVUs</code> 設為 <code>maxVUs / 2</code></li>
<li><code>maxVUs</code> 設為 <code>rate * worst_case_response_time_seconds * 5</code>（5x safety margin）</li>
<li>Monitor <code>dropped_iterations</code> metric — 不該 &gt; 0、&gt; 0 表示 worker pool 不夠</li>
</ul>
<h3 id="3-protocol-gapk6-沒原生對應-jmeter-的部分">3. Protocol gap（k6 沒原生對應 JMeter 的部分）</h3>
<p>k6 原生支援 HTTP/1.1 / HTTP/2 / gRPC / WebSocket / SSE。<strong>沒有</strong>原生支援：</p>
<ul>
<li>JDBC（要 xk6-sql extension）</li>
<li>JMS（要 xk6-amqp / xk6-kafka extension）</li>
<li>LDAP（無 extension、要外接 LDAP client）</li>
<li>FTP（無 extension）</li>
<li>SMTP / IMAP / POP3（無 extension）</li>
<li>SOAP（HTTP module 內手寫 XML body、無 helper）</li>
</ul>
<p>如果 <code>.jmx</code> 用了這些 protocol、評估 xk6 extension 成熟度（GitHub stars、recent commit、issue volume）、不成熟就把這些 test 留在 JMeter。</p>
<h3 id="4-結果輸出-schema-改變result-post-processing-全部要重寫">4. 結果輸出 schema 改變（result post-processing 全部要重寫）</h3>
<p>JMeter 預設輸出 JTL XML（per-sample 一行）、有 listener 後處理。k6 預設輸出 stdout summary + optional JSON / CSV / Prometheus / k6 Cloud。如果有既有 <em>result analysis pipeline</em>（從 JTL 拉 data 進 BI tool、產 trend chart）、Phase 2 必須重寫。</p>
<p>修法：</p>
<ul>
<li>評估直接接 Prometheus + Grafana（k6 native）取代既有 BI dashboard</li>
<li>或寫 k6 JSON output → 自家 BI 的 transformation script</li>
</ul>
<h3 id="5-ci-integration-重做distributed-runner-模型不同">5. CI integration 重做（distributed runner 模型不同）</h3>
<p>JMeter 在 CI 跑要：JVM provision、plugin install、<code>.jmx</code> upload、non-GUI mode 跑、JTL 結果 parse、exit code 對應 threshold。k6 在 CI 跑：<code>k6 run script.js</code>、threshold pass / fail 直接 exit code、result 進 Prometheus / k6 Cloud。</p>
<p>看起來 k6 簡單、但有踩雷：</p>
<ul>
<li>Distributed run model 不同：JMeter 用 master-slave、k6 OSS 不內建 distributed、要 Grafana Cloud k6 或自建 k6-operator on Kubernetes</li>
<li>大規模負載（&gt; 50K VU）必須 distributed、Phase 2 評估時要先確認 distributed setup 不是 blocker</li>
<li>CI runner 資源：k6 是 native binary、CPU / memory 用量比 JMeter（JVM）低、但 runner spec 要按 max VU 估</li>
</ul>
<h2 id="protocol-gap-詳表">Protocol gap 詳表</h2>
<table>
  <thead>
      <tr>
          <th>Protocol</th>
          <th>JMeter sampler</th>
          <th>k6 對應</th>
          <th>成熟度 / 替代方案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HTTP/1.1</td>
          <td>HTTP Request</td>
          <td><code>k6/http</code></td>
          <td>原生、成熟</td>
      </tr>
      <tr>
          <td>HTTP/2</td>
          <td>HTTP/2 sampler</td>
          <td><code>k6/http</code>（auto）</td>
          <td>原生、成熟</td>
      </tr>
      <tr>
          <td>gRPC</td>
          <td>（無原生、要 plugin）</td>
          <td><code>k6/net/grpc</code></td>
          <td>原生、成熟</td>
      </tr>
      <tr>
          <td>WebSocket</td>
          <td>WebSocket sampler（plugin）</td>
          <td><code>k6/ws</code></td>
          <td>原生、成熟</td>
      </tr>
      <tr>
          <td>SSE</td>
          <td>（無原生）</td>
          <td>xk6-sse</td>
          <td>extension、中等</td>
      </tr>
      <tr>
          <td>JDBC</td>
          <td>JDBC Request</td>
          <td>xk6-sql</td>
          <td>extension、不成熟、留 JMeter</td>
      </tr>
      <tr>
          <td>JMS</td>
          <td>JMS sampler</td>
          <td>xk6-amqp / xk6-kafka</td>
          <td>extension、protocol-specific</td>
      </tr>
      <tr>
          <td>LDAP</td>
          <td>LDAP Request</td>
          <td>（無）</td>
          <td>外接 / 留 JMeter</td>
      </tr>
      <tr>
          <td>FTP</td>
          <td>FTP Request</td>
          <td>（無）</td>
          <td>留 JMeter</td>
      </tr>
      <tr>
          <td>SMTP / IMAP</td>
          <td>Mail sampler</td>
          <td>（無）</td>
          <td>留 JMeter</td>
      </tr>
      <tr>
          <td>SOAP / XML-RPC</td>
          <td>SOAP / XML-RPC Request</td>
          <td><code>k6/http</code> 手寫 XML body</td>
          <td>工作量大、留 JMeter</td>
      </tr>
      <tr>
          <td>TCP socket</td>
          <td>TCP sampler</td>
          <td><code>k6/net/tcp</code></td>
          <td>原生但簡單、複雜 protocol 留 JMeter</td>
      </tr>
  </tbody>
</table>
<h2 id="容量與成本對照">容量與成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>JMeter</th>
          <th>k6 OSS</th>
          <th>Grafana Cloud k6</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cost</td>
          <td>Free (Apache)</td>
          <td>Free (Apache 2.0)</td>
          <td>$49+ / mo (Pro)</td>
      </tr>
      <tr>
          <td>單機 VU 容量</td>
          <td>~500-1000 thread</td>
          <td>30K-50K VU</td>
          <td>unlimited（cloud runner）</td>
      </tr>
      <tr>
          <td>Distributed</td>
          <td>master-slave 內建</td>
          <td>不內建、需 k6-operator</td>
          <td>cloud-native</td>
      </tr>
      <tr>
          <td>Result store</td>
          <td>JTL XML（local）</td>
          <td>stdout / JSON / Prom</td>
          <td>cloud retained</td>
      </tr>
      <tr>
          <td>CI integration</td>
          <td>需 packaging</td>
          <td>native CLI</td>
          <td>native + cloud</td>
      </tr>
      <tr>
          <td>Multi-protocol coverage</td>
          <td>廣</td>
          <td>窄（HTTP/gRPC/WS）+ xk6</td>
          <td>同 OSS</td>
      </tr>
  </tbody>
</table>
<p>對 dev-driven CI gate use case：k6 OSS 已經夠用、Grafana Cloud k6 在 <em>跨 region runner + result retention + dashboard 整合</em> 時才有 ROI。對既有 multi-protocol .jmx asset：考慮 Phase 3 hybrid stable state、不要強推 Phase 4。</p>
<h2 id="何時不要切">何時不要切</h2>
<ul>
<li><strong>multi-protocol coverage 是核心需求</strong>：JDBC + JMS + LDAP + FTP 必要、xk6 extension 不夠成熟、留 JMeter</li>
<li><strong>QA 團隊維護 GUI .jmx</strong>：QA 不寫 code、<code>.jmx</code> GUI 是團隊資產、貿然轉 k6 等於 throwaway QA team</li>
<li><strong>既有 multi-year .jmx asset 大量</strong>：500+ scenario 全部翻譯成本 &gt; k6 ergonomic 收益、考慮 Phase 3 stable hybrid</li>
<li><strong>Distributed run 需求極大（&gt; 100K VU）但 ops budget 緊</strong>：k6-operator on Kubernetes 不便宜、Grafana Cloud k6 對應 tier 也不便宜、JMeter master-slave 仍是 cost-effective 選項</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行 batch：<a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/migrate-from-pyroscope/" data-link-title="Pyroscope → Datadog Continuous Profiler：profiling deployment lifecycle 各階段 operational ownership 轉手" data-link-desc="Pyroscope → Datadog Continuous Profiler 是 Type C operational hybrid migration — pprof data model 接近、profile lifecycle 五階段（install / instrument / ingest / query / cost）的 ops ownership 從 self-host 轉到 SaaS。本文走 6 維 audit（Operational High 其他 Low）、4-phase migration（operational audit &#43; agent parallel &#43; tag reconcile &#43; cutover）、5 production 踩雷（agent 重複 overhead / tag schema 不一致 / trace_id correlation 斷 / cost 突增 / retention 政策變動）、何時保留 Pyroscope（資料主權 / 內網 / OSS-first / cost sensitive）">Pyroscope → Datadog Profiler</a>（Type C operational hybrid）</li>
<li>同 batch Type E：<a href="/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/" data-link-title="PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract" data-link-desc="PagerDuty → incident.io 不是 schema translation — 兩家的「on-call」字面相同、contract 不同（alert routing vs IR coordination &#43; Slack-native &#43; retrospective）。本文走 Type E paradigm shift、6 維 audit 顯示 paradigm / schema / operational 三軸 High、用 4-phase partial migration（不收斂、Phase 1-2 多數 org 停留）、5 個 production 踩雷（雙系統 state drift / severity 翻譯失真 / schedule layer 漏 / Slack channel 過載 / retrospective 斷層）、跟 PagerDuty Process Automation / AIOps 沒對應的 capability gap">PagerDuty → incident.io</a>（IR paradigm shift）</li>
<li>上游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a> / <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>下游：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a>（CI gate integration）</li>
<li>vendor 對照：<a href="/blog/backend/09-performance-capacity/vendors/jmeter/" data-link-title="Apache JMeter" data-link-desc="用 GUI、plugin 與多 protocol sampler 承接企業壓測資產的效能工程工具">JMeter</a> / <a href="/blog/backend/09-performance-capacity/vendors/k6/" data-link-title="k6" data-link-desc="用 scriptable scenario 建立 API、protocol 與 CI 友善壓測的效能工程工具">k6</a> / <a href="/blog/backend/09-performance-capacity/vendors/gatling/" data-link-title="Gatling" data-link-desc="用 JVM DSL、simulation 與 injection profile 表達複雜 scenario 的效能工程工具">Gatling</a> / <a href="/blog/backend/09-performance-capacity/vendors/locust/" data-link-title="Locust" data-link-desc="用 Python user behavior 與 distributed worker 表達高自訂負載模型的效能工程工具">Locust</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type E paradigm shift 結構說明）</li>
</ul>
]]></content:encoded></item><item><title>PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/</guid><description>&lt;p>「On-call」是個被 retconned 的詞。PagerDuty 用了十年定義它為 &lt;em>alert routing + schedule + escalation&lt;/em> — 重點是「誰會被叫醒」。incident.io 2024 年推出 On-call 模組時保留了同一個詞、但 contract 變了：On-call 在 incident.io 是 &lt;em>IR coordination + Slack-native workflow + retrospective integration&lt;/em> 的 paging 入口 — 重點是「被叫醒之後做什麼」。&lt;/p>
&lt;p>這個語意 retroactive 是這篇 migration playbook 必須先講清楚的事。讀者打開比較表會看到「PagerDuty 有 schedule、incident.io 有 schedule、PagerDuty 有 escalation policy、incident.io 有 escalation policy」、以為這是一場 schema translation 文。實際上 schema 翻譯只是其中一個工作塊、更難的是 &lt;em>org 的事故行為從「等 PagerDuty 叫」變成「在 Slack channel 內跑 lifecycle」&lt;/em>。&lt;/p>
&lt;h2 id="為什麼是-type-e不是-type-a">為什麼是 Type E（不是 Type A）&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&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>Schema&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>service / escalation policy / schedule / integration 跟 incident / role / action / catalog 沒 1:1 對應&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>alert routing → Slack-native IR coordination + retrospective workflow&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>「alert someone」 → 「coordinate full incident lifecycle from declare to retro」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;td>incident.io 整合 Slack / Linear / Jira / Confluence 變 multi-component&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>App change&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;td>webhook / integration key / IaC 都要改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>都是 cloud SaaS、無 sharding / region 議題&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三軸 High（schema / operational / paradigm）。按優先序 schema &amp;gt; paradigm &amp;gt; operational、預設會選 Type A。但這條優先序是 &lt;em>audience-dependent heuristic&lt;/em> — 對「我要把 PagerDuty config 翻譯成 incident.io」的讀者選 Type A、對「我要把事故管理 paradigm 從 paging-first 變成 Slack-first」的讀者選 Type E。&lt;/p></description><content:encoded><![CDATA[<p>「On-call」是個被 retconned 的詞。PagerDuty 用了十年定義它為 <em>alert routing + schedule + escalation</em> — 重點是「誰會被叫醒」。incident.io 2024 年推出 On-call 模組時保留了同一個詞、但 contract 變了：On-call 在 incident.io 是 <em>IR coordination + Slack-native workflow + retrospective integration</em> 的 paging 入口 — 重點是「被叫醒之後做什麼」。</p>
<p>這個語意 retroactive 是這篇 migration playbook 必須先講清楚的事。讀者打開比較表會看到「PagerDuty 有 schedule、incident.io 有 schedule、PagerDuty 有 escalation policy、incident.io 有 escalation policy」、以為這是一場 schema translation 文。實際上 schema 翻譯只是其中一個工作塊、更難的是 <em>org 的事故行為從「等 PagerDuty 叫」變成「在 Slack channel 內跑 lifecycle」</em>。</p>
<h2 id="為什麼是-type-e不是-type-a">為什麼是 Type E（不是 Type A）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>High</td>
          <td>service / escalation policy / schedule / integration 跟 incident / role / action / catalog 沒 1:1 對應</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High</td>
          <td>alert routing → Slack-native IR coordination + retrospective workflow</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>High</td>
          <td>「alert someone」 → 「coordinate full incident lifecycle from declare to retro」</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Medium</td>
          <td>incident.io 整合 Slack / Linear / Jira / Confluence 變 multi-component</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Medium</td>
          <td>webhook / integration key / IaC 都要改</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>都是 cloud SaaS、無 sharding / region 議題</td>
      </tr>
  </tbody>
</table>
<p>三軸 High（schema / operational / paradigm）。按優先序 schema &gt; paradigm &gt; operational、預設會選 Type A。但這條優先序是 <em>audience-dependent heuristic</em> — 對「我要把 PagerDuty config 翻譯成 incident.io」的讀者選 Type A、對「我要把事故管理 paradigm 從 paging-first 變成 Slack-first」的讀者選 Type E。</p>
<p>決定因素是 <em>讀者最關心什麼</em>。從 PagerDuty 出發評估 incident.io 的 org 通常 <em>已經有 Slack channel 跑 IR</em> 的痛感（雙系統 state drift / context switching cost / Slack bot 補 PagerDuty 的能力斷裂）、進來找的是 paradigm 統一、不是欄位翻譯。schema translation 是工作量、但不是讀者來找答案的問題。所以選 <strong>Type E paradigm shift</strong> 結構、schema translation 抽出獨立段補充。</p>
<h2 id="為什麼遷im-native-coordination-的拉力">為什麼遷：IM-native coordination 的拉力</h2>
<p>事故反應在已經 Slack 中心的 org 是 <em>從 Slack 自然發生</em> 的 — 觀測 alert 進 Slack、SRE 開 thread、PM 跳進來問影響、customer-facing team 在 incident channel 看通報、所有上下文都在 IM 內。PagerDuty 在這個 reality 下變成 <em>第二個 system of record</em>：incident 開在 PagerDuty 也開在 Slack、PagerDuty timeline 跟 Slack scroll 是兩條時間線、status update 要 mirror 兩次、責任分派在 Slack 講但要在 PagerDuty 點。</p>
<p>PagerDuty 注意到這個問題、後加了 Status Updates / Slack integration / Postmortem 模組想把 Slack 拉回 PagerDuty。但結構性還是 <em>PagerDuty 是主、Slack 是 mirror</em> — incident object 的 source of truth 在 PagerDuty、Slack 的訊息只是 attachment。對 <em>Slack-first</em> 的 org 來說這個 ownership 反了：Slack channel 才是事故進行中的 ground truth、PagerDuty incident 應該是 paging 入口的 artifact。</p>
<p>incident.io 設計上把這個關係翻過來：Slack channel 是 IR ground truth、incident object 是 channel 的 metadata 投影。declare incident 在 Slack、role 指派在 Slack bot prompt、status update 在 channel reply、retrospective 從 channel 訊息自動 stitch — incident.io dashboard 是 <em>管理視圖</em>、不是事故 <em>進行視圖</em>。On-call 模組加進來後、連 paging 入口也跟 IR coordination 收斂到同一個 system of record。</p>
<p>這個 pull 是這條 migration 的 <em>driver</em>。schema 翻譯只是把這條 pull 落地的工作。</p>
<h2 id="4-phase-partial-migration不收斂">4-phase partial migration（不收斂）</h2>
<p>Type E paradigm shift 的特徵是 <em>不收斂</em> — 多數 org 不會把 PagerDuty 全退役、會停在某個 phase 變成穩定的 hybrid。下面 4 phase 是 <em>常見演進路徑</em>、不是 <em>必要完成步驟</em>：</p>
<h3 id="phase-1slack-first-responsepaging-留-pagerduty">Phase 1：Slack-first response（paging 留 PagerDuty）</h3>
<p>incident.io 接 PagerDuty incident webhook、PagerDuty 開 incident → incident.io 自動開 Slack channel、跑 response lifecycle（declare / role / status / close / retro）。PagerDuty 仍管 paging schedule + escalation、incident.io 管 response coordination。</p>
<p>這個 phase 的工作主要塊是：</p>
<ul>
<li>incident.io 跟 PagerDuty 雙向 webhook 接（PD incident.trigger → IO open channel、IO incident.resolved → PD ack）</li>
<li>Slack workspace 整合（permissions、channel naming、stakeholder broadcast channel）</li>
<li>Severity 對應表（PagerDuty P1-P5 對 incident.io SEV1-SEV4、語意 reconcile）</li>
<li>跑 2-4 週 dual ops、訓練 SRE 在 Slack 內跑 lifecycle、不要回 PagerDuty 點 timeline</li>
</ul>
<p>完成標準：incident commander 不再需要進 PagerDuty UI、status update / role 指派 / action item 都在 Slack。</p>
<h3 id="phase-2catalog--service-ownership-migrate">Phase 2：Catalog + service ownership migrate</h3>
<p>把 PagerDuty 的 service registry（service / team / escalation policy 關聯）抽出進 incident.io 的 Catalog。Catalog 是 incident.io 的 <em>service metadata source of truth</em>、把 service 跟 team / Slack channel / Linear project / runbook URL 綁在一起、incident 發生時自動推薦 role 跟通知 stakeholder。</p>
<p>工作主要塊：</p>
<ul>
<li>從 PagerDuty API export service / team / escalation policy（REST endpoint <code>/services</code>、<code>/teams</code>、<code>/escalation_policies</code>）</li>
<li>Schema mapping：PagerDuty service → incident.io catalog entry、escalation policy → 暫時不動（留在 PagerDuty）</li>
<li>補 PagerDuty 沒有的欄位：Slack channel、Linear project、runbook URL、tier（catalog 比 PagerDuty service 多 metadata 維度）</li>
<li>Service ownership reconcile（PagerDuty 的 team grant 通常跟 GitHub team / IAM group 不一致、Catalog 是重新對齊機會）</li>
</ul>
<p>完成標準：incident 發生時自動知道 owner team 跟對應 Slack channel、不需要人查。</p>
<h3 id="phase-3schedule--escalation-移到-incidentio-on-call">Phase 3：Schedule + escalation 移到 incident.io On-call</h3>
<p>PagerDuty 的 schedule + escalation policy 改進 incident.io On-call。這是 <em>paging 入口的 ownership 轉移</em> — Phase 1 是 PD 觸發 IO response、Phase 3 是 IO 直接收 alert source 觸發 paging。</p>
<p>工作主要塊：</p>
<ul>
<li>Alert source 改線：Splunk / Datadog / Cloudflare WAF / cloud control plane 的 webhook 從 PagerDuty Event API 改成 incident.io webhook endpoint、deduplication key / severity mapping 重做</li>
<li>Schedule 重建：PagerDuty schedule layer model（多 layer 疊加 + restriction + override）跟 incident.io schedule rule（單純 weekly rotation + override）不是 1:1、複雜 schedule 要重新設計</li>
<li>Escalation policy 重建：PagerDuty 的 multi-step escalation + level-based timeout 對應 incident.io 的 escalation path、policy 比 PagerDuty 簡單但要重新測 failover 行為</li>
<li>Mobile app 切換：on-call 人員裝 incident.io app、PagerDuty app 保留作為 backup paging（Phase 4 才完全捨棄）</li>
</ul>
<p>完成標準：日常 paging 全走 incident.io、PagerDuty 留作 fallback 或退役。</p>
<h3 id="phase-4retrospective--完全退役-pagerduty">Phase 4：Retrospective + 完全退役 PagerDuty</h3>
<p>把 retrospective workflow 切到 incident.io 內建的 post-incident flow、捨棄 PagerDuty Postmortems / Jeli 整合。incident.io 的 retro template 從 Slack channel 訊息自動 stitch timeline、action item 推 Linear / Jira、learning review 結構化。</p>
<p>工作主要塊：</p>
<ul>
<li>既有 Jeli / PagerDuty Postmortems 歷史 export（PagerDuty REST 不直接給 postmortem export、要從 Jeli web app 手動 export）</li>
<li>Retrospective template 對應到 org 既有的 post-incident review 結構</li>
<li>Action item lifecycle 整合（incident.io 推 Linear / Jira → close → retrospective 自動標 done）</li>
</ul>
<p>多數 org 停在 Phase 2 或 Phase 3。完整 Phase 4 退役 PagerDuty 不是必要、且常見的選擇是 <em>PagerDuty 留作 backup paging route</em> 或 <em>特定 integration 持續用</em>（見下一段 capability gap）。</p>
<h2 id="5-個-production-踩雷">5 個 production 踩雷</h2>
<p>實際遷過程踩過的 5 個典型問題：</p>
<h3 id="1-雙系統-state-driftphase-1-最常見">1. 雙系統 state drift（Phase 1 最常見）</h3>
<p>PagerDuty incident.trigger → incident.io 開 channel、但 PagerDuty 上 incident 被自動 resolve（例如 monitoring tool 認為 issue cleared）後、incident.io 沒收到對應 webhook、Slack channel 還 active 顯示 in-progress。修法是雙向 webhook 都要接（PD resolved → IO 自動 close channel），但 webhook 失序的場景仍要有 nightly reconcile job 對比兩邊狀態。</p>
<h3 id="2-severity-翻譯失真">2. Severity 翻譯失真</h3>
<p>PagerDuty 的 P1-P5 跟 incident.io 的 SEV1-SEV4 不是 5:4 對應、是兩個獨立 schema。同一個事故在 PagerDuty 是 P2（高優先但非全面 outage）、進 incident.io 可能變 SEV2（部分服務影響）或 SEV1（依 incident.io custom severity 定義）。Phase 1 雙系統並行時 SRE 在 Slack 看到 SEV1 跑進 war room mode、PagerDuty 同 incident 是 P2 沒拉 stakeholder bridge — 同事故兩邊嚴重度不同步、回應節奏錯亂。修法是事先寫死 mapping table（PD P1 → IO SEV1、PD P2 → IO SEV2、不 case-by-case 判斷），並在 Phase 3 後讓 incident.io severity 變唯一 source of truth。</p>
<h3 id="3-schedule-layer-漏-holiday-override--restriction-layer">3. Schedule layer 漏 holiday override / restriction layer</h3>
<p>PagerDuty schedule 是 layer model — primary rotation（layer 1） + secondary rotation（layer 2） + holiday override（layer 3） + restriction（每層 time-of-day 限制）可以疊加。Export 出來只看 layer 1 通常會漏 holiday override 跟 restriction layer、incident.io schedule rule 是單一 rotation + override list、不 cover 多 layer 疊加。修法是 export 時用 PagerDuty API <code>/schedules/{id}</code> 的完整 layer + final_schedule 一起拉、用 incident.io schedule 的 override list 模擬 layer 疊加、複雜 schedule（例如 follow-the-sun + 4 region + holiday override）可能要拆成多個 incident.io schedule 用 escalation chain 串。</p>
<h3 id="4-slack-channel-過載">4. Slack channel 過載</h3>
<p>incident.io 預設每個 incident 開一個 channel。Phase 1 啟用後 SRE 一週收 50+ channel notification、即使 P3 / P4 也開 channel、Slack sidebar 被淹沒。修法是 incident type 設計時把低 severity（SEV3 / SEV4）改成 <em>don&rsquo;t auto-create channel</em> 或 <em>use shared low-severity channel</em>、只 SEV1 / SEV2 開獨立 channel。incident.io 有這個 configuration、但預設不開、要主動設定。</p>
<h3 id="5-retrospective-切換時歷史-learning-斷層">5. Retrospective 切換時歷史 learning 斷層</h3>
<p>從 Jeli / PagerDuty Postmortems 切到 incident.io retro 後、過去 2 年 postmortem 留在原系統、search 跨不到、新 retro template 跟舊的結構不同、learning review 的 trend analysis 斷層。修法是 Phase 4 前先 export 既有 postmortem 為 markdown 進 GitHub Wiki / Confluence 集中保存、incident.io retro 自動 export 到同位置、retro search 不依賴 vendor lock-in。</p>
<h2 id="schema-translation-主要工作量塊">Schema translation 主要工作量塊</h2>
<p>雖然 Type E 結構不以 schema translation 為主、但 translation 工作量塊在 Phase 2-3 仍佔多數時間：</p>
<table>
  <thead>
      <tr>
          <th>來源（PagerDuty）</th>
          <th>目標（incident.io）</th>
          <th>註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Service</td>
          <td>Catalog entry</td>
          <td>增加 Slack channel / Linear project metadata</td>
      </tr>
      <tr>
          <td>Team</td>
          <td>Catalog team</td>
          <td>多對應 GitHub team / IAM group</td>
      </tr>
      <tr>
          <td>Escalation policy</td>
          <td>Escalation path</td>
          <td>比 PD 簡單、複雜 escalation 要拆</td>
      </tr>
      <tr>
          <td>Schedule（multi-layer）</td>
          <td>Schedule + override list</td>
          <td>不是 1:1、複雜 schedule 要拆多個</td>
      </tr>
      <tr>
          <td>Integration（webhook）</td>
          <td>Webhook endpoint</td>
          <td>全部 alert source 要重 wire</td>
      </tr>
      <tr>
          <td>Incident workflow</td>
          <td>Incident type + role</td>
          <td>重新設計、不直接翻譯</td>
      </tr>
      <tr>
          <td>Event Orchestration rule</td>
          <td>Workflows</td>
          <td>incident.io workflows 比 EO 簡單、複雜 routing 要外接</td>
      </tr>
      <tr>
          <td>AIOps / Process Automation</td>
          <td>（無對應）</td>
          <td>見 capability gap 段</td>
      </tr>
      <tr>
          <td>Postmortem / Jeli</td>
          <td>Post-incident flow</td>
          <td>template 重寫、歷史保存獨立</td>
      </tr>
  </tbody>
</table>
<h2 id="capability-gappagerduty-有但-incidentio-沒有">Capability gap：PagerDuty 有但 incident.io 沒有</h2>
<p>不是所有功能 incident.io 都有對應。Phase 3-4 推進前要先確認這些能力是否在用、是否願意捨棄或外接：</p>
<ul>
<li><strong>AIOps（intelligent grouping / noise reduction）</strong>：PagerDuty Enterprise tier 用 ML 自動 group alert、incident.io 沒對應、grouping 靠 alert source 端 deduplication key</li>
<li><strong>Process Automation（runbook automation）</strong>：PagerDuty 收購 Rundeck、提供 automated remediation step、incident.io 沒對應、要外接 Tines / n8n / 自製 Lambda</li>
<li><strong>Status Page 整合（PagerDuty 內建）</strong>：PagerDuty 提供 Status Page 模組、incident.io status page 是 separate product、定價跟 feature 不同</li>
<li><strong>Multi-region / 強合規（FedRAMP / IL5）</strong>：PagerDuty 在金融 / 政府 / 高合規 deploy 成熟度高、incident.io SOC 2 + ISO 27001 但 FedRAMP 還在追</li>
</ul>
<p>如果在用 AIOps + Process Automation 而且重要、不要做這個 migration、或保留 PagerDuty 作為 AIOps + Automation 後端、incident.io 處理 response coordination — Phase 1 永久 hybrid。</p>
<h2 id="容量與成本對照">容量與成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>PagerDuty</th>
          <th>incident.io</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模式</td>
          <td>Per-user / month、tier-based（Pro / Business / Enterprise）</td>
          <td>Per-user / month、On-call 模組另計</td>
      </tr>
      <tr>
          <td>隱性容量上限</td>
          <td>API rate limit（10K / minute）</td>
          <td>Slack workspace seat 上限（IR participant ≤ workspace user）</td>
      </tr>
      <tr>
          <td>AIOps 加價</td>
          <td>Enterprise tier + AIOps add-on</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Status page</td>
          <td>內建（Business tier+）</td>
          <td>獨立 product</td>
      </tr>
      <tr>
          <td>Process Auto</td>
          <td>Rundeck-based、separate pricing</td>
          <td>不適用</td>
      </tr>
  </tbody>
</table>
<p>實際成本對比需要 RFP — 50 人 SRE org 大致 PD Business + AIOps ~$30-40 / user / mo、incident.io Pro + On-call ~$25-35 / user / mo、cost 差距通常不是 migration 主因（是 paradigm fit + Slack-native）。</p>
<h2 id="何時不要做這個-migration">何時不要做這個 migration</h2>
<ul>
<li><strong>Slack 不是 IR ground truth</strong>：Discord / Teams primary 或 ticket system 為主的 org、incident.io Slack-first 設計無法落地</li>
<li><strong>AIOps + Process Automation 是核心能力</strong>：用了 PD AIOps 自動 group alert 跟 Rundeck 自動 remediation、且這條 chain 重要 — incident.io 沒對應</li>
<li><strong>規模 &lt; 20 SRE / 50 eng</strong>：incident.io 的 catalog + opinionated workflow 設計給中大型 org、小團隊 PagerDuty Lite 或 Grafana OnCall 已經夠用</li>
<li><strong>強合規場景（FedRAMP / IL5 / 金融 SOC 1 type II）</strong>：PagerDuty 合規成熟度高、incident.io 在追、合規團隊不會 sign-off</li>
<li><strong>不打算改變事故行為</strong>：如果 org 只是想換廠商但不想改變 <em>事故在 Slack 跑 lifecycle</em> 的工作模式、這條 migration 的價值丟一半、不如走 <a href="/blog/backend/08-incident-response/vendors/opsgenie/migrate-from-pagerduty/" data-link-title="PagerDuty → Opsgenie：Atlassian 全家桶整合 vs Opsgenie 2027 EOL 的 vendor consolidation 取捨" data-link-desc="PagerDuty → Opsgenie 是 Type A phased schema translation、但 Atlassian 已宣布 Opsgenie 2027-04 EOL — 這條 migration 只在 Atlassian-heavy org &#43; 明確 JSM unification roadmap 下成立、本質是 PD → Opsgenie → JSM Cloud 的雙 hop migration。本文走 6 維 audit（Schema Medium-High 其他 Low）、PagerDuty ↔ Opsgenie ↔ JSM field mapping 對照、5 production 踩雷（escalation step / Heartbeat 缺對應 / integration key dedup 重設 / schedule 時區 / Atlassian Identity SSO 整合）、何時直接走 PD → JSM 跳過 Opsgenie">PagerDuty → Opsgenie</a>（Type A schema translation、同 paradigm）</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行 batch：<a href="/blog/backend/08-incident-response/vendors/opsgenie/migrate-from-pagerduty/" data-link-title="PagerDuty → Opsgenie：Atlassian 全家桶整合 vs Opsgenie 2027 EOL 的 vendor consolidation 取捨" data-link-desc="PagerDuty → Opsgenie 是 Type A phased schema translation、但 Atlassian 已宣布 Opsgenie 2027-04 EOL — 這條 migration 只在 Atlassian-heavy org &#43; 明確 JSM unification roadmap 下成立、本質是 PD → Opsgenie → JSM Cloud 的雙 hop migration。本文走 6 維 audit（Schema Medium-High 其他 Low）、PagerDuty ↔ Opsgenie ↔ JSM field mapping 對照、5 production 踩雷（escalation step / Heartbeat 缺對應 / integration key dedup 重設 / schedule 時區 / Atlassian Identity SSO 整合）、何時直接走 PD → JSM 跳過 Opsgenie">PagerDuty → Opsgenie</a>（Type A、同 paradigm 換廠商）/ <a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/migrate-to-instatus/" data-link-title="Atlassian Statuspage → Instatus：status page 成本下降、但 compatibility audit 不能跳" data-link-desc="Atlassian Statuspage → Instatus 是 Type B drop-in migration、6 維 audit 全 Low；典型情境是從 Statuspage Business / Enterprise 降到 Instatus Pro / Business、但 savings 取決於 subscriber、SSO、audit 與 SLA report 需求。本文走 compatibility audit prefix（subscriber channel 完整度 / SAML SSO / audit log / metrics integration / SLA report / API parity）、4 階段 cutover（DNS TTL &#43; parallel run）、5 個 production 踩雷（SSO tier 選錯、metrics 來源整合斷、subscriber import format / SLA report 缺、custom CSS 不完全相容）、何時不要切（enterprise compliance / 強 Atlassian 整合）">Atlassian Statuspage → Instatus</a>（Type B drop-in）</li>
<li>同 batch Type E：<a href="/blog/backend/09-performance-capacity/vendors/k6/migrate-from-jmeter/" data-link-title="JMeter → k6：k6 不是 JMeter 的「script 版本」、是 VU model 取代 thread model" data-link-desc="JMeter → k6 是 Type E paradigm shift、不是把 .jmx XML 翻成 JavaScript — VU (virtual user) model 跟 thread group model 是兩種對「使用者行為」不同的建模方式。本文走 6 維 audit（Schema High / Paradigm High / Operational Medium）、釐清反向定義、4-phase partial migration（多數 org 停 Phase 2-3 hybrid）、5 production 踩雷（thread group 翻譯失真 / arrival rate vs concurrent VU 混淆 / protocol gap / 結果 schema 改 / CI integration 重做）、protocol gap（JDBC / JMS / LDAP 在 k6 沒原生對應）、何時不要切">JMeter → k6</a>（scripting paradigm shift）</li>
<li>上游：<a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">8.10 Incident Workflow Automation Boundary</a>（automation handoff）</li>
<li>下游：<a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.18 Post-Incident Review</a>（incident.io retrospective workflow）</li>
<li>vendor 對照：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type E paradigm shift 結構說明）</li>
</ul>
]]></content:encoded></item><item><title>PagerDuty → Opsgenie：Atlassian 全家桶整合 vs Opsgenie 2027 EOL 的 vendor consolidation 取捨</title><link>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/migrate-from-pagerduty/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/08-incident-response/vendors/opsgenie/migrate-from-pagerduty/</guid><description>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>PagerDuty 物件&lt;/th>
 &lt;th>Opsgenie 對應&lt;/th>
 &lt;th>JSM Cloud 對應（2027 後）&lt;/th>
 &lt;th>翻譯難度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Service&lt;/td>
 &lt;td>Integration&lt;/td>
 &lt;td>Service registry&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Escalation Policy&lt;/td>
 &lt;td>Escalation&lt;/td>
 &lt;td>Escalation&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schedule（layer model）&lt;/td>
 &lt;td>Schedule（rotation）&lt;/td>
 &lt;td>Schedule&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>User&lt;/td>
 &lt;td>User&lt;/td>
 &lt;td>Atlassian Account&lt;/td>
 &lt;td>中（IdP 整合）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Team&lt;/td>
 &lt;td>Team&lt;/td>
 &lt;td>JSM Team&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event API v2&lt;/td>
 &lt;td>Alert API&lt;/td>
 &lt;td>JSM REST API&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event Orchestration&lt;/td>
 &lt;td>Policy&lt;/td>
 &lt;td>Routing rule&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Status Page&lt;/td>
 &lt;td>Statuspage（同產品）&lt;/td>
 &lt;td>Statuspage&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Postmortem&lt;/td>
 &lt;td>（無原生）&lt;/td>
 &lt;td>（Confluence template）&lt;/td>
 &lt;td>高（要外接）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張對照表是 PagerDuty → Opsgenie migration 的 &lt;em>表面 schema mapping&lt;/em>、但表前必須先處理一個前提：&lt;strong>Atlassian 2025 公開宣布 Opsgenie 將在 2027-04 EOL&lt;/strong>、現有 Opsgenie 客戶會被遷往 &lt;a href="https://www.atlassian.com/software/jira/service-management">Jira Service Management Premium / Enterprise&lt;/a> 內建的 on-call 能力。這條 migration 不是 PagerDuty ↔ Opsgenie 的 vendor swap、是 &lt;em>PagerDuty → Opsgenie → JSM Cloud&lt;/em> 的雙 hop migration。&lt;/p>
&lt;h2 id="誰應該考慮這條-migration">誰應該考慮這條 migration&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>適用條件&lt;/th>
 &lt;th>不適用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>已是 Atlassian-heavy ecosystem（JSM / Confluence / Bitbucket）&lt;/td>
 &lt;td>純 Slack-first org（考慮 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/" data-link-title="PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract" data-link-desc="PagerDuty → incident.io 不是 schema translation — 兩家的「on-call」字面相同、contract 不同（alert routing vs IR coordination &amp;#43; Slack-native &amp;#43; retrospective）。本文走 Type E paradigm shift、6 維 audit 顯示 paradigm / schema / operational 三軸 High、用 4-phase partial migration（不收斂、Phase 1-2 多數 org 停留）、5 個 production 踩雷（雙系統 state drift / severity 翻譯失真 / schedule layer 漏 / Slack channel 過載 / retrospective 斷層）、跟 PagerDuty Process Automation / AIOps 沒對應的 capability gap">→ incident.io&lt;/a>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已買 JSM Premium / Enterprise、Opsgenie 是 entitled benefit&lt;/td>
 &lt;td>新案、無 Atlassian 基礎&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>願意走 PD → Opsgenie → JSM 雙 hop（或直接跳 JSM）&lt;/td>
 &lt;td>不想多次 migration、想一步到位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atlassian Identity / Cloud admin 已成熟&lt;/td>
 &lt;td>SSO / IdP 跟 Atlassian 沒整合好&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OSS / 自管不可行（compliance / 規模）&lt;/td>
 &lt;td>規模 &amp;lt; 20 SRE（&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &amp;#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall&lt;/a> 或 PagerDuty Lite 已足夠）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對 &lt;em>新案&lt;/em>：不要選 Opsgenie standalone。直接評估 PagerDuty → JSM Premium 一次到位、或 PagerDuty → incident.io（如果 Slack-first 是 driver）。&lt;/p></description><content:encoded><![CDATA[<table>
  <thead>
      <tr>
          <th>PagerDuty 物件</th>
          <th>Opsgenie 對應</th>
          <th>JSM Cloud 對應（2027 後）</th>
          <th>翻譯難度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Service</td>
          <td>Integration</td>
          <td>Service registry</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Escalation Policy</td>
          <td>Escalation</td>
          <td>Escalation</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Schedule（layer model）</td>
          <td>Schedule（rotation）</td>
          <td>Schedule</td>
          <td>中-高</td>
      </tr>
      <tr>
          <td>User</td>
          <td>User</td>
          <td>Atlassian Account</td>
          <td>中（IdP 整合）</td>
      </tr>
      <tr>
          <td>Team</td>
          <td>Team</td>
          <td>JSM Team</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Event API v2</td>
          <td>Alert API</td>
          <td>JSM REST API</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Event Orchestration</td>
          <td>Policy</td>
          <td>Routing rule</td>
          <td>中-高</td>
      </tr>
      <tr>
          <td>Status Page</td>
          <td>Statuspage（同產品）</td>
          <td>Statuspage</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Postmortem</td>
          <td>（無原生）</td>
          <td>（Confluence template）</td>
          <td>高（要外接）</td>
      </tr>
  </tbody>
</table>
<p>這張對照表是 PagerDuty → Opsgenie migration 的 <em>表面 schema mapping</em>、但表前必須先處理一個前提：<strong>Atlassian 2025 公開宣布 Opsgenie 將在 2027-04 EOL</strong>、現有 Opsgenie 客戶會被遷往 <a href="https://www.atlassian.com/software/jira/service-management">Jira Service Management Premium / Enterprise</a> 內建的 on-call 能力。這條 migration 不是 PagerDuty ↔ Opsgenie 的 vendor swap、是 <em>PagerDuty → Opsgenie → JSM Cloud</em> 的雙 hop migration。</p>
<h2 id="誰應該考慮這條-migration">誰應該考慮這條 migration</h2>
<table>
  <thead>
      <tr>
          <th>適用條件</th>
          <th>不適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已是 Atlassian-heavy ecosystem（JSM / Confluence / Bitbucket）</td>
          <td>純 Slack-first org（考慮 <a href="/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/" data-link-title="PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract" data-link-desc="PagerDuty → incident.io 不是 schema translation — 兩家的「on-call」字面相同、contract 不同（alert routing vs IR coordination &#43; Slack-native &#43; retrospective）。本文走 Type E paradigm shift、6 維 audit 顯示 paradigm / schema / operational 三軸 High、用 4-phase partial migration（不收斂、Phase 1-2 多數 org 停留）、5 個 production 踩雷（雙系統 state drift / severity 翻譯失真 / schedule layer 漏 / Slack channel 過載 / retrospective 斷層）、跟 PagerDuty Process Automation / AIOps 沒對應的 capability gap">→ incident.io</a>）</td>
      </tr>
      <tr>
          <td>已買 JSM Premium / Enterprise、Opsgenie 是 entitled benefit</td>
          <td>新案、無 Atlassian 基礎</td>
      </tr>
      <tr>
          <td>願意走 PD → Opsgenie → JSM 雙 hop（或直接跳 JSM）</td>
          <td>不想多次 migration、想一步到位</td>
      </tr>
      <tr>
          <td>Atlassian Identity / Cloud admin 已成熟</td>
          <td>SSO / IdP 跟 Atlassian 沒整合好</td>
      </tr>
      <tr>
          <td>OSS / 自管不可行（compliance / 規模）</td>
          <td>規模 &lt; 20 SRE（<a href="/blog/backend/08-incident-response/vendors/grafana-oncall/" data-link-title="Grafana OnCall" data-link-desc="OSS-friendly on-call 平台、Grafana Labs 維護、Apache 2.0、Grafana IRM bundle 把 OnCall &#43; Incident 收進一個 alert-to-resolve 流程">Grafana OnCall</a> 或 PagerDuty Lite 已足夠）</td>
      </tr>
  </tbody>
</table>
<p>對 <em>新案</em>：不要選 Opsgenie standalone。直接評估 PagerDuty → JSM Premium 一次到位、或 PagerDuty → incident.io（如果 Slack-first 是 driver）。</p>
<p>對 <em>已是 Opsgenie 客戶但從 PagerDuty 遷入的 org</em>（少見、通常是 acquisition consolidation）：本文仍適用、但要把 Phase 5 EOL 路徑放在規劃裡。</p>
<h2 id="為什麼是-type-aschema-為主">為什麼是 Type A（schema 為主）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Medium-High</td>
          <td>escalation policy / schedule / integration / API endpoint 都有 mapping、但概念對應度高</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Low</td>
          <td>同為 alert routing + on-call schedule 平台、ops 模型一致</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>同 paging-first paradigm</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
          <td>都是 SaaS 平台、no multi-tool decomposition</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Medium</td>
          <td>webhook URL / integration key 要換、application code 改動少</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>都是 cloud SaaS</td>
      </tr>
  </tbody>
</table>
<p>Schema = Medium-High（其他 Low） → <strong>Type A phased translation</strong>。比標準 Type A 11-12 章短、因 paradigm 不變、不需要重新訓練 SRE 行為。</p>
<h2 id="driveratlassian-vendor-consolidation">Driver：Atlassian vendor consolidation</h2>
<p>從 PagerDuty 遷入 Opsgenie 的核心 driver 是 <em>Atlassian 全家桶整合</em> — 已經買 JSM + Confluence + Bitbucket + Statuspage 的 org、再買 PagerDuty 等於多一條 SaaS 採購線、SSO 配置、billing 對接、user provisioning 重複。Opsgenie（或未來 JSM Premium 內建 on-call）走 Atlassian Identity、跟 JSM ticket / Confluence runbook / Statuspage component 同一個身份體系、incident 跟 ticket / status update 跨產品聯動不用 webhook chain。</p>
<p>這條 consolidation 拉力的具體形態：</p>
<ul>
<li><strong>單一 SSO + provisioning</strong>：Atlassian Cloud admin 一處 manage user / group / SSO、不需要 PagerDuty 獨立 SCIM + IdP 配置</li>
<li><strong>Ticket ↔ incident bi-directional</strong>：JSM ticket 升級成 incident、incident 自動建 ticket、close incident 自動 close ticket、不用 PagerDuty Jira integration plugin</li>
<li><strong>Runbook 跟 incident channel 同產品</strong>：Confluence runbook 從 Opsgenie alert 直接 link、不用維護兩套權限</li>
<li><strong>Status Page 共用 component model</strong>：Statuspage 已是 Atlassian 產品、Opsgenie incident 觸發 Status Page update 不用 webhook（內部 event）</li>
<li><strong>Billing 整合</strong>：Atlassian Cloud subscription bundle、CFO 不用對 5 條獨立 SaaS invoice</li>
</ul>
<p>這條 driver 在 PagerDuty 後加的 Status Updates / Jira plugin / Postmortems 模組下被部分削弱、但本質仍是 <em>Atlassian 是主、PagerDuty 是外掛</em> vs <em>全部都在 Atlassian</em> 的差別。</p>
<h2 id="type-a-phased-migration5-phase">Type A phased migration（5 phase）</h2>
<h3 id="phase-1schema-對照--識別差異">Phase 1：Schema 對照 + 識別差異</h3>
<p>把 PagerDuty 當前 config 完整 export（API endpoint <code>/services</code>、<code>/escalation_policies</code>、<code>/schedules</code>、<code>/users</code>、<code>/teams</code>、<code>/integrations</code>、<code>/event_orchestrations</code>）、對照上方 schema mapping table、識別 <em>無 1:1 對應的物件</em>：</p>
<ul>
<li>Event Orchestration rule 對 Opsgenie 的 Policy + Routing rule（複雜 routing 要拆）</li>
<li>Schedule layer model 對 Opsgenie 的 Rotation + Override（layer 疊加要展平）</li>
<li>PagerDuty AIOps / Process Automation 對 Opsgenie 的 <em>無對應</em> — 要評估是否丟掉這條能力</li>
</ul>
<p>完成標準：寫出 PagerDuty config inventory + Opsgenie target spec、確認所有物件都有 mapping path（即使是「捨棄」也算 mapping）。</p>
<h3 id="phase-2schedule--escalation-移植">Phase 2：Schedule + Escalation 移植</h3>
<p>PagerDuty schedule 是 layer 疊加（primary + secondary + override + restriction）、Opsgenie 是 <em>單一 rotation list + override</em>。簡單 schedule（單一 weekly rotation + 偶爾 override）直接對應、複雜 schedule（follow-the-sun + holiday + restriction time-of-day）要展平：</p>
<ul>
<li>PagerDuty <code>/schedules/{id}</code> 拉完整 <code>final_schedule</code>、用 <em>實際輪值結果</em> 重建 Opsgenie rotation</li>
<li>多層 schedule 在 Opsgenie 拆成多個 rotation、用 escalation chain 串</li>
<li>Restriction layer 在 Opsgenie 沒對應、要在 rotation rule 內 inline 時段限制</li>
</ul>
<p>Escalation policy 多 step + level-based timeout 在 Opsgenie 是 <em>step-based escalation</em>、直接對應、但每步 timeout 跟 acknowledge behavior 要 retest。</p>
<p>完成標準：on-call rotation 在 Opsgenie 跑一週、跟 PagerDuty parallel 對比實際 paging 行為一致（同一個 alert 兩邊都叫到對的人）。</p>
<h3 id="phase-3integration--webhook-改線">Phase 3：Integration / Webhook 改線</h3>
<p>每個 alert source（Splunk / Datadog / Cloudflare WAF / cloud control plane / synthetic monitor）的 webhook URL 從 PagerDuty Event API 換成 Opsgenie Alert API：</p>
<ul>
<li>Endpoint：<code>https://events.pagerduty.com/v2/enqueue</code> → <code>https://api.opsgenie.com/v2/alerts</code></li>
<li>Auth：PagerDuty <code>routing_key</code> → Opsgenie API key（per-integration）</li>
<li>Deduplication：PagerDuty <code>dedup_key</code> → Opsgenie <code>alias</code>（行為相同、欄位名不同）</li>
<li>Severity mapping：PagerDuty <code>severity</code>（info/warning/error/critical） → Opsgenie <code>priority</code>（P1-P5）</li>
</ul>
<p>這 phase 的工作量主要塊不是 schema 翻譯、是 <em>每個 integration 都要重新測 deduplication + severity</em>。新 integration key 配上去後第一週要密切監控、避免 dedup key 重設導致同事故開 100 個 incident。</p>
<p>完成標準：所有 alert source 都接 Opsgenie、PagerDuty 端 alert volume 降為 0。</p>
<h3 id="phase-4cutover--dual-ops-period">Phase 4：Cutover + dual ops period</h3>
<p>2-4 週 dual ops：alert 都進 Opsgenie 為主、PagerDuty 留作 backup paging（同樣 alert 也 mirror 進 PD、但 SRE response 全在 Opsgenie）。確認沒漏 alert、escalation 行為正確、Atlassian 整合（JSM ticket / Confluence runbook / Statuspage） wire 通。</p>
<p>完成標準：dual ops 4 週無漏 alert、SRE 沒回去 PagerDuty UI 操作。</p>
<h3 id="phase-5pagerduty-退役--opsgenie--jsm-eol-路徑規劃">Phase 5：PagerDuty 退役 + Opsgenie → JSM EOL 路徑規劃</h3>
<p>PagerDuty 退役後立即進入 <em>Opsgenie 2027 EOL 倒數</em>。這 phase 不是 PD migration 的尾巴、是 <em>下一條 migration 的起點</em>：</p>
<ul>
<li>2025-2026：Atlassian 推 JSM Premium 的 on-call 能力、提供 Opsgenie → JSM 遷移工具</li>
<li>2026-2027：實際遷 Opsgenie → JSM、schedule / integration / API 改線</li>
<li>2027-04：Opsgenie EOL、所有 traffic 必須在 JSM</li>
</ul>
<p>完成標準：PagerDuty 帳號取消、Opsgenie deployment 健康運作 + JSM unification roadmap 寫進 2026-2027 SRE OKR。</p>
<h2 id="5-個-production-踩雷">5 個 production 踩雷</h2>
<h3 id="1-escalation-step-routing-行為差異">1. Escalation step routing 行為差異</h3>
<p>PagerDuty escalation policy 的 step timeout 是 <em>每步獨立 acknowledge window</em>（step 1 等 5 分鐘沒人 ack → step 2 等 5 分鐘沒人 ack → &hellip;）、Opsgenie escalation 的行為類似但 <em>step 之間的 notification cumulative behavior</em> 不同 — Opsgenie 預設 step 2 觸發後 step 1 的人 <em>仍會收到 notification</em>（除非設定 step 1 not yet acknowledged 才繼續）。修法是寫測試 case 對比 alert 在兩邊 escalation 過程的 notification timeline、調整 Opsgenie escalation rule 的 acknowledge propagation 設定到跟 PD 一致。</p>
<h3 id="2-heartbeat-monitoring-在-pagerduty-沒對應">2. Heartbeat monitoring 在 PagerDuty 沒對應</h3>
<p>Opsgenie Heartbeat 是 <em>被動 monitoring</em> — service 必須定期 ping 一個 endpoint、超過 interval 沒 ping 就觸發 alert、用來監控 cron job / scheduled task 是否還在跑。PagerDuty 沒原生 Heartbeat、通常用 external service（Healthchecks.io / Dead Man&rsquo;s Snitch）。從 PD 遷入 Opsgenie 時、把這些 external service 收回 Opsgenie Heartbeat、減少 SaaS 數量。但反向（從 Opsgenie 遷出時要先把 Heartbeat dependency 外接）是不同問題、不在本篇 scope。</p>
<h3 id="3-integration-key-改線時-deduplication-重設">3. Integration key 改線時 deduplication 重設</h3>
<p>PagerDuty <code>dedup_key</code> → Opsgenie <code>alias</code> 行為相同、但 <em>新 integration key 上線後第一個 alert 不會跟舊 PD incident 對應</em> — 同一個事故在 PD 上是 incident #5234、在 Opsgenie 上是新 alert 從零開始。Phase 3 切換時間點如果剛好遇到 active incident、會分裂成兩個系統內各自的 incident、SRE confusion。修法是 cutover 時間點選擇在 <em>known quiet period</em>（一般是週末早上、避開 deploy 時段）、並接受第一個切換期間有手動 reconcile 的工作。</p>
<h3 id="4-schedule-時區處理">4. Schedule 時區處理</h3>
<p>PagerDuty schedule 的 timezone 是 <em>per-layer</em> 設定（layer 1 可以 PST、layer 2 可以 GMT）、Opsgenie rotation timezone 是 <em>per-schedule</em> 設定。Follow-the-sun schedule（亞太 / 歐洲 / 美洲三層）在 PD 是三 layer 各自 timezone、在 Opsgenie 要拆成三個 schedule 各自設定 timezone 用 escalation 串。Daylight saving transition 是另一個高風險點 — PD 跟 Opsgenie 在 DST 切換週的行為要分別測試。</p>
<h3 id="5-atlassian-identity-sso-整合">5. Atlassian Identity SSO 整合</h3>
<p>如果 org 既有 SSO（Okta / Azure AD）已經跟 PagerDuty 整合、遷 Opsgenie 時要 <em>重新對接 Atlassian Identity</em>。Atlassian Cloud 的 SSO 是在 Atlassian admin 層設定、跟個別產品（Opsgenie / JSM）獨立。常見問題：</p>
<ul>
<li>PagerDuty user email 不一定等於 Atlassian account email（有人用 work email 註冊 PD、用 personal email 註冊 Atlassian）</li>
<li>SCIM provisioning rule 要重寫、group / role mapping 重新設計</li>
<li>Just-in-time user provisioning behavior 不同（PD 是即時、Atlassian 可能需要 admin 手動 approve）</li>
</ul>
<p>修法是 Phase 1 schema mapping 時就把 user identity reconcile 列為獨立工作塊、不要假設 email 唯一對應。</p>
<h2 id="容量與成本對照">容量與成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>PagerDuty</th>
          <th>Opsgenie</th>
          <th>JSM Premium（2027 後）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模式</td>
          <td>Per-user / month、tier-based</td>
          <td>Per-user / month、Free tier ≤ 5 user</td>
          <td>JSM seat + on-call entitlement</td>
      </tr>
      <tr>
          <td>Atlassian bundle</td>
          <td>獨立 SaaS</td>
          <td>Atlassian Cloud subscription</td>
          <td>JSM Premium / Enterprise 內建</td>
      </tr>
      <tr>
          <td>AIOps</td>
          <td>Enterprise + add-on</td>
          <td>弱（無原生 ML grouping）</td>
          <td>（roadmap）</td>
      </tr>
      <tr>
          <td>Heartbeat</td>
          <td>不適用</td>
          <td>內建</td>
          <td>內建</td>
      </tr>
      <tr>
          <td>Status Page</td>
          <td>內建（Business tier+）</td>
          <td>Statuspage（同 Atlassian、單獨計費）</td>
          <td>Statuspage 整合</td>
      </tr>
      <tr>
          <td>隱性 EOL 風險</td>
          <td>無</td>
          <td>2027-04 EOL</td>
          <td>Atlassian 主推</td>
      </tr>
  </tbody>
</table>
<p>實際 TCO 對比 <em>不能只看 per-seat price</em> — 必須加上：</p>
<ul>
<li>Atlassian Cloud bundle discount（多產品同訂閱通常有 15-25% 折扣）</li>
<li>PagerDuty AIOps + Process Automation 是否在用（如果在用、Opsgenie 沒對應、要外接成本）</li>
<li>雙 hop migration（PD → Opsgenie → JSM）的累計工程成本 vs 單 hop（PD → JSM 跳過 Opsgenie）</li>
</ul>
<h2 id="何時跳過-opsgenie-直接-pd--jsm">何時跳過 Opsgenie 直接 PD → JSM</h2>
<p>對 <em>已是 Atlassian-heavy org</em> 但 <em>尚未用 Opsgenie</em> 的場景、Opsgenie 2027 EOL 表示 PD → Opsgenie → JSM 雙 hop 不划算。直接 PD → JSM Premium：</p>
<ul>
<li>等 Atlassian 2026 公開 JSM 內建 on-call 的完整能力、確認 feature parity 跟 Opsgenie 相當</li>
<li>規劃 PD → JSM 一次 migration、結構接近本篇但 target 換成 JSM</li>
<li>風險：JSM 內建 on-call 在 2026 仍可能成熟度不夠、決策時點要看 Atlassian 公開 roadmap</li>
</ul>
<p>對 <em>已是 Opsgenie 客戶</em> 的場景、本篇的 PD → Opsgenie 路徑仍適用、但 Phase 5 EOL 路徑規劃是必要 deliverable、不是 optional。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行 batch：<a href="/blog/backend/08-incident-response/vendors/pagerduty/migrate-to-incident-io/" data-link-title="PagerDuty → incident.io：「On-call」是個 retconned word、同名不同 contract" data-link-desc="PagerDuty → incident.io 不是 schema translation — 兩家的「on-call」字面相同、contract 不同（alert routing vs IR coordination &#43; Slack-native &#43; retrospective）。本文走 Type E paradigm shift、6 維 audit 顯示 paradigm / schema / operational 三軸 High、用 4-phase partial migration（不收斂、Phase 1-2 多數 org 停留）、5 個 production 踩雷（雙系統 state drift / severity 翻譯失真 / schedule layer 漏 / Slack channel 過載 / retrospective 斷層）、跟 PagerDuty Process Automation / AIOps 沒對應的 capability gap">PagerDuty → incident.io</a>（Type E、Slack-first paradigm shift）/ <a href="/blog/backend/08-incident-response/vendors/atlassian-statuspage/migrate-to-instatus/" data-link-title="Atlassian Statuspage → Instatus：status page 成本下降、但 compatibility audit 不能跳" data-link-desc="Atlassian Statuspage → Instatus 是 Type B drop-in migration、6 維 audit 全 Low；典型情境是從 Statuspage Business / Enterprise 降到 Instatus Pro / Business、但 savings 取決於 subscriber、SSO、audit 與 SLA report 需求。本文走 compatibility audit prefix（subscriber channel 完整度 / SAML SSO / audit log / metrics integration / SLA report / API parity）、4 階段 cutover（DNS TTL &#43; parallel run）、5 個 production 踩雷（SSO tier 選錯、metrics 來源整合斷、subscriber import format / SLA report 缺、custom CSS 不完全相容）、何時不要切（enterprise compliance / 強 Atlassian 整合）">Atlassian Statuspage → Instatus</a>（Type B drop-in）</li>
<li>同 batch Type A：（待補、本篇是 batch 唯一 Type A）</li>
<li>上游：<a href="/blog/backend/08-incident-response/incident-workflow-automation-boundary/" data-link-title="8.21 Incident Workflow Automation Boundary" data-link-desc="定義哪些事故流程適合自動化，哪些決策需要保留人工確認">8.10 Incident Workflow Automation Boundary</a></li>
<li>下游：未來 Opsgenie → JSM Premium migration（2026-2027 寫）</li>
<li>vendor 對照：<a href="/blog/backend/08-incident-response/vendors/pagerduty/" data-link-title="PagerDuty" data-link-desc="On-call / alerting 主流 SaaS、IR 平台演化">PagerDuty</a> / <a href="/blog/backend/08-incident-response/vendors/opsgenie/" data-link-title="Opsgenie" data-link-desc="Atlassian on-call、跟 Jira / Statuspage 套件整合、JSM Premium migration 議題">Opsgenie</a> / <a href="/blog/backend/08-incident-response/vendors/incident-io/" data-link-title="incident.io" data-link-desc="Slack-native IR 平台、整合 paging / response / retro">incident.io</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type A phased translation 結構說明）</li>
</ul>
]]></content:encoded></item><item><title>Pyroscope → Datadog Continuous Profiler：profiling deployment lifecycle 各階段 operational ownership 轉手</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/migrate-from-pyroscope/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/migrate-from-pyroscope/</guid><description>&lt;p>Continuous profiling deployment 的 lifecycle 有五階段：&lt;strong>install&lt;/strong>（agent / SDK 部署） → &lt;strong>instrument&lt;/strong>（service / env / version tag 注入） → &lt;strong>ingest&lt;/strong>（profile sample 進 backend store） → &lt;strong>query&lt;/strong>（flame graph / diff / explore） → &lt;strong>cost&lt;/strong>（storage retention / billing）。Pyroscope 跟 Datadog Continuous Profiler 在這五階段的 &lt;em>ops ownership 分布完全不同&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>Pyroscope（self-host）&lt;/th>
 &lt;th>Datadog Continuous Profiler&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Install&lt;/td>
 &lt;td>Grafana Alloy / Pyroscope agent / per-language SDK、自己部署&lt;/td>
 &lt;td>Datadog Agent（多半 APM 已部署）、SDK 加 flag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Instrument&lt;/td>
 &lt;td>tag schema 自己設計&lt;/td>
 &lt;td>用 Datadog 既有 &lt;code>service&lt;/code> / &lt;code>env&lt;/code> / &lt;code>version&lt;/code> tag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ingest&lt;/td>
 &lt;td>Pyroscope server（自管 storage / scaling）&lt;/td>
 &lt;td>Datadog SaaS（vendor 管）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>Grafana datasource explore / flame graph panel&lt;/td>
 &lt;td>Datadog APM 介面、跟 trace / log / metrics deep link&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>self-host TCO（storage + ops + on-call）&lt;/td>
 &lt;td>按 APM host 計費（profiling 是 add-on）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>從 Pyroscope 遷出 Datadog Continuous Profiler 的本質是 &lt;em>operational ownership 從 self-host 轉手到 SaaS&lt;/em> — pprof data model 跟 flame graph 視覺幾乎一樣、profile diff workflow 接近、&lt;em>差異 90% 在 ops 跟 ecosystem integration&lt;/em>。schema / paradigm 差距小、operational 差距大、就是 Type C operational hybrid 的 signature。&lt;/p>
&lt;h2 id="為什麼是-type-coperational-為主">為什麼是 Type C（operational 為主）&lt;/h2>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&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>Schema&lt;/td>
 &lt;td>Low-Medium&lt;/td>
 &lt;td>pprof 是 industry standard、profile types (CPU / heap / etc) 接近&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>High&lt;/td>
 &lt;td>self-host backend storage / retention / scaling → SaaS 全託管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>都是 pprof-based continuous profiling、diff workflow 接近&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Low-Medium&lt;/td>
 &lt;td>都需要 agent + backend、元件數量接近&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>App change&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>agent / SDK config 改、code instrumentation 接近&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;td>都是 agent → backend 單向 ingest&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational = High（其他 Low） → &lt;strong>Type C operational hybrid&lt;/strong>。Type C 結構是 &lt;em>operational audit prefix + 4-phase drop-in cutover&lt;/em> — operational diff 集中在 ingest / cost / retention 三階段、其他階段是 schema-level drop-in。&lt;/p></description><content:encoded><![CDATA[<p>Continuous profiling deployment 的 lifecycle 有五階段：<strong>install</strong>（agent / SDK 部署） → <strong>instrument</strong>（service / env / version tag 注入） → <strong>ingest</strong>（profile sample 進 backend store） → <strong>query</strong>（flame graph / diff / explore） → <strong>cost</strong>（storage retention / billing）。Pyroscope 跟 Datadog Continuous Profiler 在這五階段的 <em>ops ownership 分布完全不同</em>：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>Pyroscope（self-host）</th>
          <th>Datadog Continuous Profiler</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Install</td>
          <td>Grafana Alloy / Pyroscope agent / per-language SDK、自己部署</td>
          <td>Datadog Agent（多半 APM 已部署）、SDK 加 flag</td>
      </tr>
      <tr>
          <td>Instrument</td>
          <td>tag schema 自己設計</td>
          <td>用 Datadog 既有 <code>service</code> / <code>env</code> / <code>version</code> tag</td>
      </tr>
      <tr>
          <td>Ingest</td>
          <td>Pyroscope server（自管 storage / scaling）</td>
          <td>Datadog SaaS（vendor 管）</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>Grafana datasource explore / flame graph panel</td>
          <td>Datadog APM 介面、跟 trace / log / metrics deep link</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>self-host TCO（storage + ops + on-call）</td>
          <td>按 APM host 計費（profiling 是 add-on）</td>
      </tr>
  </tbody>
</table>
<p>從 Pyroscope 遷出 Datadog Continuous Profiler 的本質是 <em>operational ownership 從 self-host 轉手到 SaaS</em> — pprof data model 跟 flame graph 視覺幾乎一樣、profile diff workflow 接近、<em>差異 90% 在 ops 跟 ecosystem integration</em>。schema / paradigm 差距小、operational 差距大、就是 Type C operational hybrid 的 signature。</p>
<h2 id="為什麼是-type-coperational-為主">為什麼是 Type C（operational 為主）</h2>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/#6-%e7%b6%ad-diff-dimension-audit" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low-Medium</td>
          <td>pprof 是 industry standard、profile types (CPU / heap / etc) 接近</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High</td>
          <td>self-host backend storage / retention / scaling → SaaS 全託管</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>都是 pprof-based continuous profiling、diff workflow 接近</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low-Medium</td>
          <td>都需要 agent + backend、元件數量接近</td>
      </tr>
      <tr>
          <td>App change</td>
          <td>Low</td>
          <td>agent / SDK config 改、code instrumentation 接近</td>
      </tr>
      <tr>
          <td>Topology</td>
          <td>Low</td>
          <td>都是 agent → backend 單向 ingest</td>
      </tr>
  </tbody>
</table>
<p>Operational = High（其他 Low） → <strong>Type C operational hybrid</strong>。Type C 結構是 <em>operational audit prefix + 4-phase drop-in cutover</em> — operational diff 集中在 ingest / cost / retention 三階段、其他階段是 schema-level drop-in。</p>
<h2 id="drivertco--datadog-ecosystem-內-deep-linking">Driver：TCO + Datadog ecosystem 內 deep linking</h2>
<p>從 Pyroscope 遷出 Datadog Profiler 的核心 driver 有兩條：</p>
<p><strong>TCO（total cost of ownership）</strong>：self-host Pyroscope 看起來免費（Apache 2.0）、但實際 ops 成本：</p>
<ul>
<li>Storage：profile sample 大、retention 與 storage cost 需要自己估（每 service 每天可能 1-10 GB）</li>
<li>Scaling：profile ingestion 突增（deploy event / canary rollout 期間）要 storage / ingester 撐住</li>
<li>On-call：Pyroscope server 自己會壞、要 on-call 帶</li>
<li>Ops engineer time：規模成長後可能需要 0.5-1 個 FTE 維護 Grafana stack 內的 Pyroscope</li>
</ul>
<p>對 <em>已經有 Datadog APM 帳單</em> 的 org、profiling 會跟 APM / profiled host 進同一個商務談判與 usage report，不需要額外 ops headcount。這條 TCO 拉力對 50-500 人 eng 規模最強 — 小於 50 人 self-host 也撐得住、大於 500 人 self-host 的 economy of scale 可能開始 favored Pyroscope。</p>
<p><strong>Ecosystem deep linking</strong>：Datadog Profiler 跟 trace / log / metrics <em>在同一個介面</em>、profile span 直接連到 trace span、deploy marker 直接顯示在 flame graph timeline、cross-signal query 不用 wire。Pyroscope 要透過 Grafana datasource correlation 達到類似效果、但需要 Tempo / Loki 已部署 + 手動配 correlation rule、整合精度跟自動程度都不如 Datadog 內建。</p>
<p>這條 driver 對 <em>已是 Datadog-heavy org</em> 強、對 <em>Grafana-heavy org</em> 弱（後者 Pyroscope 才是自然選擇、Datadog Profiler 反而 ecosystem misfit）。</p>
<h2 id="type-c-migration4-phase">Type C migration（4-phase）</h2>
<h3 id="phase-1operational-audit">Phase 1：Operational audit</h3>
<p>確認 Datadog Continuous Profiler 能 cover Pyroscope 當前用途、且 ops ownership 轉移可接受：</p>
<ul>
<li><strong>Language coverage</strong>：當前 Pyroscope 用哪些 SDK？Datadog Profiler 支援 Go / Java / Python / Node / Ruby / .NET / PHP / Rust / C / C++，但每個語言的 profiler type 與啟用方式不同；Erlang 等較小眾語言仍要逐項驗證</li>
<li><strong>Profile type coverage</strong>：Pyroscope 抓的 profile type（CPU / heap / allocation / goroutine / lock / wall time）在 Datadog Profiler 同語言是否都支援？Java 跟 Go 兩家都全、其他語言可能 partial</li>
<li><strong>Retention requirement</strong>：Pyroscope retention 可自管；Datadog Profiler retention 依產品資料保留政策與合約設定，要確認是否滿足既有 long-term baseline / audit 查詢需求</li>
<li><strong>資料主權</strong>：profile data 包含 application function name / line number、有時帶 customer data hint（function 名字暗示 customer-specific 邏輯）— 是否能 send to SaaS？</li>
<li><strong>Cost forecast</strong>：Datadog public pricing 以 profiled host / APM tier 計費，估算時要用實際 host 數、container density、APM plan 與 commit discount 跟 Pyroscope self-host TCO 比</li>
</ul>
<p>完成標準：寫出「Datadog 能 cover、不能 cover、不確定」三欄、不確定欄全部問過 Datadog SE / 用 trial 跑過 production-like load。</p>
<h3 id="phase-2agent-parallel-runprofile-雙寫">Phase 2：Agent parallel run（profile 雙寫）</h3>
<p>Datadog Agent 多半已部署（如果在用 Datadog APM）。Phase 2 在現有 Datadog Agent 開 profiling flag、<em>不關 Pyroscope agent</em>、跑 2-4 週 parallel：</p>
<ul>
<li>設定 <code>DD_PROFILING_ENABLED=true</code>（per service env var）</li>
<li>每個 service SDK init 加對應 profiling enable call（Go: <code>profiler.Start()</code>、Python: <code>import ddtrace.profiling.auto</code>、Java: agent flag 即可）</li>
<li>Pyroscope SDK / Alloy 繼續跑、profile 雙寫到兩家</li>
<li>對比同一個 service / 同一個時間段在 Pyroscope flame graph 跟 Datadog Profiler flame graph、確認 hot path 一致</li>
</ul>
<p>Parallel run 期間的 overhead：兩邊 agent 同時跑 profiling、CPU overhead 大致 2-4%（單一 profiler 通常 1-2%、雙寫 double）、production-acceptable but not free。Phase 2 不要超過 4 週、避免長期 double overhead。</p>
<p>完成標準：每個 production service 在 Datadog Profiler 都有 4 週連續 profile data、跟 Pyroscope flame graph 對比一致。</p>
<h3 id="phase-3tag-schema-reconcile--trace-correlation">Phase 3：Tag schema reconcile + trace correlation</h3>
<p>Pyroscope tag schema（自己設計）跟 Datadog standard tag（<code>service</code> / <code>env</code> / <code>version</code> / <code>host</code>）對齊：</p>
<ul>
<li>Pyroscope tag <code>app=checkout-api</code> → Datadog <code>service:checkout-api</code></li>
<li>Pyroscope tag <code>env=prod-us</code> → Datadog <code>env:prod</code> + <code>region:us-east-1</code></li>
<li>Pyroscope tag <code>git_sha=abc123</code> → Datadog <code>version:abc123</code>（透過 <code>DD_VERSION</code>）</li>
<li>Custom tag（team / business unit）→ Datadog custom tag（透過 SDK config 或 agent label）</li>
</ul>
<p>Trace correlation：Datadog Profiler 自動跟 APM trace 關聯（透過 <code>trace_id</code> injection into profile sample）— Phase 3 要驗證這個 correlation 可用（在 Datadog APM 點 trace span、應該能跳到對應時段 profile）。</p>
<p>Deploy marker：CI 在 deploy 時打 Datadog deployment marker（<code>datadog-ci deployment mark</code> 或 API call）、讓 Profiler diff view 知道 baseline / candidate 邊界。</p>
<p>完成標準：tag schema 1:1 對應、trace → profile deep link 可用、deploy marker 自動推送。</p>
<h3 id="phase-4pyroscope-agent-關掉--server-退役">Phase 4：Pyroscope agent 關掉 + server 退役</h3>
<p>逐步關 Pyroscope agent（per service rollout）：</p>
<ul>
<li>先關低重要性 service（dev / staging / non-critical prod）</li>
<li>觀察 1-2 週、確認沒事故再關下一批</li>
<li>最後關 critical service、留 Pyroscope server 跑 1-2 週空 ingest（rollback 緩衝）</li>
<li>取消 Pyroscope server（decommission storage、release K8s resource、關 on-call rotation）</li>
</ul>
<p>Pyroscope 歷史 profile data 保留策略：</p>
<ul>
<li>多數場景：直接 archive S3 / GCS、未來查得到但不維護 query UI</li>
<li>強合規場景：export Pyroscope flame graph data 為 pprof file 保存（pprof 是長期可讀格式）</li>
</ul>
<p>完成標準：所有 production service 只走 Datadog Profiler、Pyroscope server 取消、TCO 對比驗證符合預期。</p>
<h2 id="5-個-production-踩雷">5 個 production 踩雷</h2>
<h3 id="1-兩家-agent-同時跑造成-production-overhead">1. 兩家 agent 同時跑造成 production overhead</h3>
<p>Phase 2 parallel run 期間 CPU overhead 2-4%、預期內。但有些 service 設定錯誤（例如 sampling rate 預設都拉高）變成 6-10% overhead、p99 飄升、誤判為 Datadog Profiler 自己的問題。修法是 <em>parallel run 期間 Pyroscope sampling rate 降低 50%</em>（已經有歷史 baseline、不需要全採）、且 Phase 2 不要在 peak event 期間跑。</p>
<h3 id="2-tag-schema-不一致導致-historic-baseline-對不上">2. Tag schema 不一致導致 historic baseline 對不上</h3>
<p>Pyroscope tag <code>app=checkout-api</code> 跟 Datadog <code>service:checkout-api</code> 都指同一個 service、但 Datadog 內 <em>historic profile</em> 沒有 <code>app</code> tag、所以從 Pyroscope 視角看 baseline 跟 Datadog 視角看 baseline <em>是不同的時段切片</em>。Release regression 比較時用錯 baseline、會誤判 release 沒問題（實際 baseline 不對應）。修法是 Phase 3 明確記錄 <em>Datadog Profiler 的 baseline 起算時間是 Phase 2 開始日</em>、Pyroscope 歷史不直接搬入比較。</p>
<h3 id="3-trace_id-correlation-斷phase-3-最常見">3. Trace_id correlation 斷（Phase 3 最常見）</h3>
<p>Datadog Profiler 自動關聯 trace 的前提是 <em>同一個 Datadog Agent + APM SDK 注入 trace_id</em>。如果 service 用 OpenTelemetry SDK + Datadog Agent（OTel-first 配置）、trace_id 注入方式不同、profile 跟 trace 可能無法自動 correlate。修法是 <em>確認所有 service 用 Datadog SDK 或正確配 OTel-to-Datadog converter</em>、在 Datadog APM 介面 random 抽 10 個 trace 驗證 profile correlation 是否 wire 通。</p>
<h3 id="4-cost-突增phase-4-後常見">4. Cost 突增（Phase 4 後常見）</h3>
<p>關掉 Pyroscope agent 後、Datadog Profiler 變成 sole profile source、ingest volume 上升、Datadog bill 比預估高 30-50%。原因通常是：</p>
<ul>
<li>Profile sampling rate 不小心開太高（部分 service config 沒對齊）</li>
<li>Custom tag 太多（每個 unique tag combination 增加 indexing cost）</li>
<li>Profile event 量比預估高（service count × sampling rate × profile types）</li>
</ul>
<p>修法是 Phase 1 cost forecast 要保留 30% buffer、且 Phase 4 完成後立即跑 Datadog usage report 確認 actual 跟 forecast 對比。</p>
<h3 id="5-retention--baseline-政策變動造成歷史-query-斷層">5. Retention / baseline 政策變動造成歷史 query 斷層</h3>
<p>Pyroscope 自管 retention 可以設成配合內部 storage 與 compliance policy；Datadog Profiler 的 retention 依產品資料保留政策與合約設定。真正的風險不是固定「7 天 vs 90 天」，而是 <em>既有 baseline 查詢習慣是否還成立</em>：原 Pyroscope user 可能習慣查特定 release 前後的 flame graph、Datadog 端則要看 profile tag、deployment marker 與保留政策能否支援同樣查詢。修法是 Phase 1 明確列出「要查多久前、用什麼 tag 找、誰有權限看」三個問題，超出 profile retention 的長期 trend 改用 Datadog metrics-derived signal（cumulative CPU% / memory growth rate）或保留 Pyroscope archive。</p>
<h2 id="capability-對照">Capability 對照</h2>
<table>
  <thead>
      <tr>
          <th>能力</th>
          <th>Pyroscope（self-host）</th>
          <th>Datadog Continuous Profiler</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Language SDK 覆蓋</td>
          <td>Go / Java / Python / Node / Ruby / .NET / Rust / PHP</td>
          <td>Go / Java / Python / Node / Ruby / .NET / PHP / Rust / C / C++</td>
      </tr>
      <tr>
          <td>Profile type（CPU / heap / lock / etc）</td>
          <td>全（依語言 SDK 而定）</td>
          <td>全（依語言 SDK 而定）</td>
      </tr>
      <tr>
          <td>Flame graph diff workflow</td>
          <td>Grafana panel</td>
          <td>Datadog Profile Comparison</td>
      </tr>
      <tr>
          <td>Trace correlation</td>
          <td>手動配 Grafana correlation rule</td>
          <td>自動（trace_id injection）</td>
      </tr>
      <tr>
          <td>Deploy marker</td>
          <td>手動</td>
          <td>datadog-ci 自動</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>自管（無上限、cost 自負）</td>
          <td>依 Datadog retention policy / 合約設定</td>
      </tr>
      <tr>
          <td>資料主權</td>
          <td>完全自管</td>
          <td>SaaS（profile 出境）</td>
      </tr>
      <tr>
          <td>Ops ownership</td>
          <td>自管（storage / scaling / on-call）</td>
          <td>Vendor</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>self-host TCO</td>
          <td>profiled host / APM tier / commit discount</td>
      </tr>
      <tr>
          <td>Cross-signal query</td>
          <td>Grafana cross-datasource</td>
          <td>Datadog native（trace / log / profile / metrics 同一 query bar）</td>
      </tr>
  </tbody>
</table>
<h2 id="何時不要切保留-pyroscope">何時不要切（保留 Pyroscope）</h2>
<ul>
<li><strong>資料主權 / compliance 不允許 profile data 出境</strong>：金融 / 醫療 / 政府 / 國防、保留 Pyroscope self-host</li>
<li><strong>內網 / air-gap 部署</strong>：物理上連不到 Datadog SaaS、保留 Pyroscope</li>
<li><strong>OSS-first / vendor neutrality policy</strong>：org 政策不允許 vendor lock-in profiling、保留 Pyroscope</li>
<li><strong>規模超大（&gt; 500 APM host）</strong>：Datadog Profiler add-on cost × host 數可能超過 Pyroscope self-host TCO、計算交叉點</li>
<li><strong>Long retention / 自訂 archive 強需求</strong>：若 profile data 必須照內部 retention policy 長期保存、保留 Pyroscope 或建立 export / archive 流程</li>
<li><strong>Datadog 不支援的語言或 profiler type</strong>：Erlang、特定 runtime 或特定 profile type 若 Datadog 無法覆蓋，保留 Pyroscope 為對應 service profiling</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行 batch：<a href="/blog/backend/09-performance-capacity/vendors/k6/migrate-from-jmeter/" data-link-title="JMeter → k6：k6 不是 JMeter 的「script 版本」、是 VU model 取代 thread model" data-link-desc="JMeter → k6 是 Type E paradigm shift、不是把 .jmx XML 翻成 JavaScript — VU (virtual user) model 跟 thread group model 是兩種對「使用者行為」不同的建模方式。本文走 6 維 audit（Schema High / Paradigm High / Operational Medium）、釐清反向定義、4-phase partial migration（多數 org 停 Phase 2-3 hybrid）、5 production 踩雷（thread group 翻譯失真 / arrival rate vs concurrent VU 混淆 / protocol gap / 結果 schema 改 / CI integration 重做）、protocol gap（JDBC / JMS / LDAP 在 k6 沒原生對應）、何時不要切">JMeter → k6</a>（Type E paradigm shift）</li>
<li>同 batch Type C：（待補、本篇是 batch 唯一 Type C）</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 Performance Observability</a> / <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 Continuous Profiling</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Performance Improvement Loop</a>（profile diff 接入 release regression workflow）</li>
<li>vendor 對照：<a href="/blog/backend/09-performance-capacity/vendors/pyroscope/" data-link-title="Pyroscope" data-link-desc="用 Grafana 生態與開源 profiling backend 建立可自管 profile diff 與 flame graph 的工具">Pyroscope</a> / <a href="/blog/backend/09-performance-capacity/vendors/datadog-continuous-profiler/" data-link-title="Datadog Continuous Profiler" data-link-desc="用 SaaS APM 整合、deployment marker 與 profile diff 支援 release regression 定位的 profiling 工具">Datadog Continuous Profiler</a> / <a href="/blog/backend/09-performance-capacity/vendors/parca/" data-link-title="Parca" data-link-desc="用 eBPF 與開源 continuous profiling 平台建立 infrastructure-wide profile evidence 的工具">Parca</a></li>
<li>方法論：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook Methodology</a>（Type C operational hybrid 結構說明）</li>
</ul>
]]></content:encoded></item><item><title>API 認證的三層信任邊界：使用者、系統、跨系統 Provisioning</title><link>https://tarrragon.github.io/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/</guid><description>&lt;h2 id="api-認證為什麼要分層">API 認證為什麼要分層&lt;/h2>
&lt;p>&lt;strong>API 認證的核心是「身分維度的分離」&lt;/strong> — 一個 request 同時牽涉「人」「呼叫的系統」「另一個系統有沒有對應身分」三個獨立問題，每個問題的 secret 機制不同、洩漏後果不同、撤銷方式不同。混用一個機制回答全部問題，等於用同一把鑰匙開家、車、保險箱。&lt;/p>
&lt;p>看似一個 API request，其實同時要回答：&lt;/p>
&lt;ul>
&lt;li>發起這個 request 的「&lt;strong>人&lt;/strong>」是誰？（identity）&lt;/li>
&lt;li>把這個 request 傳過來的「&lt;strong>系統&lt;/strong>」是誰？（caller）&lt;/li>
&lt;li>這個人在「&lt;strong>另一個系統&lt;/strong>」有沒有對應身分？（cross-system mapping）&lt;/li>
&lt;/ul>
&lt;p>每個問題都需要不同的 secret 機制來回答。設計時先拆身分維度，再選 token、shared secret、mTLS 或 provisioning workflow，才有辦法讓洩漏範圍、撤銷粒度與排障路由各自清楚。&lt;/p>
&lt;p>這篇整理兩層信任邊界（Layer 1 使用者、Layer 2 系統）跟一個跨系統 workflow（Layer 3 Provisioning），以及它們各自對應的 secret 機制。&lt;strong>每層的實作細節都另有獨立文章深入&lt;/strong>、本文聚焦「為什麼要分」「各層解什麼問題」的心智模型。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>前提假設&lt;/strong>：以下所有機制都假設 transport 走 HTTPS / TLS。Token 與 secret 需要在加密通道內傳輸，否則中間人可直接取得 credential。HTTPS 是所有層共同依賴的 transport 前提。&lt;/p>
&lt;p>&lt;strong>本文 token 範圍&lt;/strong>：本文討論「opaque token」（隨機字串、server 端 lookup），不涵蓋 JWT（self-contained token、簽章驗證）。兩者安全模型不同，比較見 Layer 1 段落。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="layer-1使用者層bearer-token">Layer 1：使用者層（Bearer Token）&lt;/h2>
&lt;p>&lt;strong>使用者層負責把 request 綁到已登入的人類或帳號主體&lt;/strong>。它回答的問題是：「這個 request 是哪個使用者發的？」&lt;/p>
&lt;p>&lt;strong>Bearer Token 是 capability credential（持有即授權）、不是 identity credential（身分證明）&lt;/strong>。差別在於：身分證遺失可以掛失補辦、別人撿到也無法直接領錢；Bearer Token 一旦被取得、攻擊者就能即時用該使用者身分發 request、沒有第二道關卡。這個本質決定了 token 的儲存、傳輸、撤銷機制都必須以「持有即危險」為前提設計。&lt;/p>
&lt;p>「Bearer Token」是 RFC 6750 定義的 HTTP authentication scheme（&lt;code>Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code>）、屬於通用概念 — GitHub PAT、Stripe API Key、OAuth access token、Laravel Sanctum 的 PAT、JWT 都是 Bearer Token 的不同實作。&lt;/p>
&lt;h3 id="opaque-token-vs-jwt兩種根本不同的設計">Opaque Token vs JWT：兩種根本不同的設計&lt;/h3>
&lt;p>「Bearer Token」是上位概念、實作上有兩條主線、安全模型完全不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>Opaque Token（如 Sanctum）&lt;/th>
 &lt;th>JWT&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Token 本身&lt;/td>
 &lt;td>隨機字串、無內含資訊&lt;/td>
 &lt;td>簽章 payload、內嵌使用者 claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗證方式&lt;/td>
 &lt;td>server 查 DB lookup&lt;/td>
 &lt;td>驗簽章、不需 DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>載入使用者&lt;/td>
 &lt;td>從 DB row 撈&lt;/td>
 &lt;td>直接讀 claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>撤銷&lt;/td>
 &lt;td>刪 DB row、立即生效&lt;/td>
 &lt;td>困難、需 blacklist 或短 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>洩漏暴露範圍&lt;/td>
 &lt;td>該 row 立即停用&lt;/td>
 &lt;td>直到 expire 都有效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨服務驗證&lt;/td>
 &lt;td>需要共用 DB 或驗證 endpoint&lt;/td>
 &lt;td>共享公鑰即可、stateless&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者各有適合情境：opaque token 撤銷快、適合「使用者主動登出 / 帳號被盜要立即停權」；JWT 不需 DB lookup、適合「跨多個 microservice、想避免每次都查中央 DB」。下面 Layer 1 的內容&lt;strong>只聚焦 opaque token&lt;/strong> — JWT 的設計細節（簽章演算法選擇、&lt;code>alg: none&lt;/code> 攻擊、key rotation）是獨立議題、不在本篇範圍。&lt;/p>
&lt;h3 id="opaque-token-的格式設計">Opaque Token 的格式設計&lt;/h3>
&lt;p>Opaque token 是隨機字串、但實際 format 在不同產品有兩條主流分流：&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>&lt;strong>&lt;code>{PK}|{secret}&lt;/code>&lt;/strong>&lt;/td>
 &lt;td>&lt;code>1|abc123def456...&lt;/code>（Laravel Sanctum）&lt;/td>
 &lt;td>用 PK 收斂 DB 搜尋、把 timing 安全留給應用層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>&lt;code>{prefix}_{secret}&lt;/code>&lt;/strong>&lt;/td>
 &lt;td>&lt;code>ghp_xxx&lt;/code>（GitHub）、&lt;code>sk_live_xxx&lt;/code>（Stripe）&lt;/td>
 &lt;td>用語意 prefix 支援自動洩漏掃描跟 token type 辨識&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種設計&lt;strong>沒有絕對優劣&lt;/strong>、取決於 token 的傳播範圍：純內部使用、Sanctum 設計簡潔且足夠；對外開放、容易散落公開 repo、prefix 設計能讓 GitHub Secret Scanning / Stripe webhook 等工具自動偵測洩漏。&lt;/p></description><content:encoded><![CDATA[<h2 id="api-認證為什麼要分層">API 認證為什麼要分層</h2>
<p><strong>API 認證的核心是「身分維度的分離」</strong> — 一個 request 同時牽涉「人」「呼叫的系統」「另一個系統有沒有對應身分」三個獨立問題，每個問題的 secret 機制不同、洩漏後果不同、撤銷方式不同。混用一個機制回答全部問題，等於用同一把鑰匙開家、車、保險箱。</p>
<p>看似一個 API request，其實同時要回答：</p>
<ul>
<li>發起這個 request 的「<strong>人</strong>」是誰？（identity）</li>
<li>把這個 request 傳過來的「<strong>系統</strong>」是誰？（caller）</li>
<li>這個人在「<strong>另一個系統</strong>」有沒有對應身分？（cross-system mapping）</li>
</ul>
<p>每個問題都需要不同的 secret 機制來回答。設計時先拆身分維度，再選 token、shared secret、mTLS 或 provisioning workflow，才有辦法讓洩漏範圍、撤銷粒度與排障路由各自清楚。</p>
<p>這篇整理兩層信任邊界（Layer 1 使用者、Layer 2 系統）跟一個跨系統 workflow（Layer 3 Provisioning），以及它們各自對應的 secret 機制。<strong>每層的實作細節都另有獨立文章深入</strong>、本文聚焦「為什麼要分」「各層解什麼問題」的心智模型。</p>
<blockquote>
<p><strong>前提假設</strong>：以下所有機制都假設 transport 走 HTTPS / TLS。Token 與 secret 需要在加密通道內傳輸，否則中間人可直接取得 credential。HTTPS 是所有層共同依賴的 transport 前提。</p>
<p><strong>本文 token 範圍</strong>：本文討論「opaque token」（隨機字串、server 端 lookup），不涵蓋 JWT（self-contained token、簽章驗證）。兩者安全模型不同，比較見 Layer 1 段落。</p></blockquote>
<hr>
<h2 id="layer-1使用者層bearer-token">Layer 1：使用者層（Bearer Token）</h2>
<p><strong>使用者層負責把 request 綁到已登入的人類或帳號主體</strong>。它回答的問題是：「這個 request 是哪個使用者發的？」</p>
<p><strong>Bearer Token 是 capability credential（持有即授權）、不是 identity credential（身分證明）</strong>。差別在於：身分證遺失可以掛失補辦、別人撿到也無法直接領錢；Bearer Token 一旦被取得、攻擊者就能即時用該使用者身分發 request、沒有第二道關卡。這個本質決定了 token 的儲存、傳輸、撤銷機制都必須以「持有即危險」為前提設計。</p>
<p>「Bearer Token」是 RFC 6750 定義的 HTTP authentication scheme（<code>Authorization: Bearer &lt;token&gt;</code>）、屬於通用概念 — GitHub PAT、Stripe API Key、OAuth access token、Laravel Sanctum 的 PAT、JWT 都是 Bearer Token 的不同實作。</p>
<h3 id="opaque-token-vs-jwt兩種根本不同的設計">Opaque Token vs JWT：兩種根本不同的設計</h3>
<p>「Bearer Token」是上位概念、實作上有兩條主線、安全模型完全不同：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Opaque Token（如 Sanctum）</th>
          <th>JWT</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Token 本身</td>
          <td>隨機字串、無內含資訊</td>
          <td>簽章 payload、內嵌使用者 claim</td>
      </tr>
      <tr>
          <td>驗證方式</td>
          <td>server 查 DB lookup</td>
          <td>驗簽章、不需 DB</td>
      </tr>
      <tr>
          <td>載入使用者</td>
          <td>從 DB row 撈</td>
          <td>直接讀 claim</td>
      </tr>
      <tr>
          <td>撤銷</td>
          <td>刪 DB row、立即生效</td>
          <td>困難、需 blacklist 或短 TTL</td>
      </tr>
      <tr>
          <td>洩漏暴露範圍</td>
          <td>該 row 立即停用</td>
          <td>直到 expire 都有效</td>
      </tr>
      <tr>
          <td>跨服務驗證</td>
          <td>需要共用 DB 或驗證 endpoint</td>
          <td>共享公鑰即可、stateless</td>
      </tr>
  </tbody>
</table>
<p>兩者各有適合情境：opaque token 撤銷快、適合「使用者主動登出 / 帳號被盜要立即停權」；JWT 不需 DB lookup、適合「跨多個 microservice、想避免每次都查中央 DB」。下面 Layer 1 的內容<strong>只聚焦 opaque token</strong> — JWT 的設計細節（簽章演算法選擇、<code>alg: none</code> 攻擊、key rotation）是獨立議題、不在本篇範圍。</p>
<h3 id="opaque-token-的格式設計">Opaque Token 的格式設計</h3>
<p>Opaque token 是隨機字串、但實際 format 在不同產品有兩條主流分流：</p>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>範例</th>
          <th>解的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong><code>{PK}|{secret}</code></strong></td>
          <td><code>1|abc123def456...</code>（Laravel Sanctum）</td>
          <td>用 PK 收斂 DB 搜尋、把 timing 安全留給應用層</td>
      </tr>
      <tr>
          <td><strong><code>{prefix}_{secret}</code></strong></td>
          <td><code>ghp_xxx</code>（GitHub）、<code>sk_live_xxx</code>（Stripe）</td>
          <td>用語意 prefix 支援自動洩漏掃描跟 token type 辨識</td>
      </tr>
  </tbody>
</table>
<p>兩種設計<strong>沒有絕對優劣</strong>、取決於 token 的傳播範圍：純內部使用、Sanctum 設計簡潔且足夠；對外開放、容易散落公開 repo、prefix 設計能讓 GitHub Secret Scanning / Stripe webhook 等工具自動偵測洩漏。</p>
<p>Sanctum 的 <code>{PK}|{secret}</code> 設計常被誤解為「業界標準」 — 其實是 Laravel 生態的特定選擇。具體機制、跟 GitHub / Stripe 設計的比較、各語言實作範例見 <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>。</p>
<h3 id="token-在-db-的儲存原則簡述">Token 在 DB 的儲存原則（簡述）</h3>
<p>無論用哪種 format、有三條跨設計通用的儲存原則：</p>
<ol>
<li><strong>DB 只存 hash、不存原文</strong> — token 是高熵隨機字串、SHA-256 即可、不需 bcrypt</li>
<li><strong>比對必須是 constant-time</strong> — 用各語言提供的 <code>hash_equals</code> / <code>compare_digest</code> / <code>ConstantTimeCompare</code>、不用 <code>==</code></li>
<li><strong>Lookup 用穩定字段、機密比對放應用層</strong> — DB 引擎不保證 constant-time 比對、把機密比對搬離 DB</li>
</ol>
<p>這三條的詳細推導、各語言 constant-time 函式對照、非 Laravel 環境的實作範例見 <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>。</p>
<h3 id="token-的生命週期">Token 的生命週期</h3>





<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">   Login                  Use                  Expire/Revoke
</span></span><span class="line"><span class="ln">2</span><span class="cl">─────────  ───────────────────────────  ─────────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">issued → DB 存 hash  →  Bearer 驗證    →   row deleted
</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">                       set request.user</span></span></code></pre></div><ul>
<li><strong><code>expires_at</code></strong>（例如 7 天、30 天）— 限制洩漏 token 的暴露窗</li>
<li><strong><code>abilities</code> / <code>scopes</code></strong> — 限縮權限粒度（「只能讀」「只能存取某 resource」），降低單一 token 洩漏的破壞範圍</li>
<li><strong>登出即刪 row</strong> — opaque token 的撤銷成本低，這是它相對 JWT 的關鍵優勢</li>
<li><strong>rate limit / brute force 防護</strong> — token 是隨機字串、攻擊者可暴力試。應用層要對「token 驗證失敗」加 rate limit、避免被掃出有效 token</li>
<li><strong>長期 access 用 refresh token pattern</strong> — access token 短 TTL（小時級）、refresh token 長 TTL（月級）。Access token 洩漏只影響短窗、refresh token 撤銷後新的 access token 也無法發放</li>
</ul>
<h3 id="信任邊界">信任邊界</h3>





<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">[ 使用者 ] ─────────▶ [ API server ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">              token        ↑
</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></code></pre></div><p>Bearer Token 是 capability credential — 任何持有它的 client 都能以該使用者身分發 request。這也是為什麼 token 一旦離開原本的 API server，就會引發下一層問題：B 系統收到 A 系統的 token、根本不知道該怎麼驗證、也不該驗證。</p>
<hr>
<h2 id="layer-2系統層system-to-system-credential">Layer 2：系統層（System-to-system credential）</h2>
<p><strong>系統層負責驗證呼叫方服務本身的身分</strong>。它回答的問題是：「這個 request 是哪個系統發的？」</p>
<p>當系統 A 需要呼叫系統 B 的 API 時，Layer 1 的使用者 token 只代表「使用者」的身分。系統 B 仍需要獨立驗證「這個 request 來自合法的合作系統 A」，這個判斷要由系統層 credential 承擔。</p>
<h3 id="為什麼分得這麼清楚">為什麼分得這麼清楚</h3>
<p>想像系統 B 收到一個請求：</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">B 收到請求「給我會員 X 的資料」
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">B 自問：這請求來自...
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ├─ 我的合作夥伴系統 A？  → 可進入授權判斷
</span></span><span class="line"><span class="ln">5</span><span class="cl">   ├─ 未註冊的外部 caller？ → 回 401 / 403
</span></span><span class="line"><span class="ln">6</span><span class="cl">   └─ 偽裝成 A 的 caller？  → 回 401 / 403 並記錄告警</span></span></code></pre></div><p>純粹靠 Layer 1 的使用者 token 只能證明「這位 user 的身分」，無法證明「系統 A 的身分」。這個分工讓帳號被盜與合作系統被冒用分別走不同監控與撤銷流程。</p>
<h3 id="shared-secret與api-key的關係">「Shared Secret」與「API Key」的關係</h3>
<p>兩者常被混用、實際上是同一個機制（一邊發、一邊存的對稱字串）的不同部署方式：</p>
<table>
  <thead>
      <tr>
          <th>區分點</th>
          <th>Shared Secret</th>
          <th>API Key</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Caller identity</td>
          <td>兩邊都用同一把、沒有 caller 區分</td>
          <td>每個 client 一把、server 有 key → identity 對照表</td>
      </tr>
      <tr>
          <td>撤銷粒度</td>
          <td>換一邊、全部斷</td>
          <td>撤一把 key、只影響該 client</td>
      </tr>
      <tr>
          <td>典型部署</td>
          <td>內部固定夥伴系統</td>
          <td>對外開放 API、多 tenant</td>
      </tr>
  </tbody>
</table>
<p>下面討論的「Shared Secret」泛指這個 pattern；要做 per-client identity 與 revoke 時、改成 API Key 結構即可。</p>
<h3 id="常見方案的取捨">常見方案的取捨</h3>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>機制</th>
          <th>撤銷粒度</th>
          <th>適合情境</th>
          <th>主要代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Shared Secret</strong></td>
          <td>兩邊放同一把字串</td>
          <td>全部 caller</td>
          <td>內部單一夥伴、低變更頻率</td>
          <td>多 client 時撤銷會牽動所有人</td>
      </tr>
      <tr>
          <td><strong>API Key</strong></td>
          <td>每個 client 一把、server 有對照表</td>
          <td>per-client</td>
          <td>對外開放、多 tenant</td>
          <td>server 需維護 key → identity mapping</td>
      </tr>
      <tr>
          <td><strong>HMAC 簽章</strong></td>
          <td>client 用 secret 簽 request body</td>
          <td>per-key</td>
          <td>secret 不想經過網路、需防 replay / 改寫</td>
          <td>兩邊都要實作簽章邏輯、debug 較難</td>
      </tr>
      <tr>
          <td><strong>mTLS</strong></td>
          <td>雙向 TLS 憑證</td>
          <td>撤憑證</td>
          <td>金融、醫療、零信任網路</td>
          <td>憑證生命週期管理複雜、CA / CRL 基礎建設成本</td>
      </tr>
      <tr>
          <td><strong>OAuth Client Credentials</strong></td>
          <td>client_id + secret 換短期 access token</td>
          <td>撤 long-lived secret、短 token 自然 expire</td>
          <td>跨組織、權限粒度需要、需配合 scope</td>
          <td>多一層 token endpoint、實作成本較高</td>
      </tr>
  </tbody>
</table>
<p>選擇預設值的判斷：純內部固定夥伴可從 Shared Secret 起步；對外或多 client 直接上 API Key；公網跨組織 + 需要短期撤銷上 OAuth Client Credentials；合規或高威脅環境用 mTLS。</p>
<p>mTLS 的 CA 階層、憑證生命週期、撤銷機制、nginx / service mesh 整合見 <a href="/blog/work-log/mtls-%E5%AF%A6%E9%9A%9B%E6%80%8E%E9%BA%BC%E8%A8%AD%E5%AE%9A%E8%88%87%E9%81%8B%E7%B6%ADca-%E9%9A%8E%E5%B1%A4%E6%86%91%E8%AD%89%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%92%A4%E9%8A%B7%E6%A9%9F%E5%88%B6/" data-link-title="mTLS 實際怎麼設定與運維：CA 階層、憑證生命週期、撤銷機制" data-link-desc="mTLS 落地的運維決策（CA 階層、憑證儲存、撤銷機制）與基礎設施整合（nginx / envoy / service mesh），以及跟 API Key / OAuth 的成本與安全取捨。">mTLS 實際怎麼設定與運維</a>。</p>
<h3 id="shared-secret-的隱形成本">Shared Secret 的隱形成本</h3>
<p>Shared Secret 部署簡單、但維運上有幾個固定痛點：</p>
<ul>
<li><strong>無法 per-caller 撤銷</strong> — 一旦洩漏，所有用這把 secret 的 client 都得換</li>
<li><strong>輪替需要兩邊同步</strong> — 任何一邊忘了更新就斷線、需要「雙密過渡期」讓兩邊有時間切換。具體實作見 <a href="/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/" data-link-title="Shared Secret 安全輪替設計：雙密過渡期、自動化與緊急流程" data-link-desc="系統間 Shared Secret 輪替的核心機制：dual-secret rollover、自動化工具比較（AWS Secrets Manager / Vault / GCP）、緊急 rotation 流程與多 client 環境的失敗模式。">Shared Secret 安全輪替設計</a></li>
<li><strong>常被放進 query param</strong> — 為了簡便、會留在 nginx access log、CDN log、瀏覽器 history 裡。應放在 request header（例如 <code>X-System-Secret: xxx</code>）或走 HMAC / OAuth</li>
</ul>
<h3 id="信任邊界-1">信任邊界</h3>





<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">[ 系統 A ] ═════════▶ [ 系統 B ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">       shared secret
</span></span><span class="line"><span class="ln">3</span><span class="cl">       (server-to-server, server-only credential)</span></span></code></pre></div><p><strong>Layer 2 secret 的安全邊界是 server-side runtime</strong>。一旦進入瀏覽器或行動 app，攻擊者就能透過反編譯、JS source map、devtools network panel 等管道取得；取得後即可假冒系統 A 呼叫系統 B。Mobile app 的反編譯工具（jadx、Hopper、Ghidra 等）讓這個攻擊成本極低，obfuscation 只能增加時間成本。</p>
<p>如果 client 端需要呼叫 B，安全路由是讓 client 先呼叫 A，由 A 在 server 端用 Layer 2 secret 呼叫 B（A 當 proxy / BFF）；另一條路是用 OAuth 把 short-lived token 發給 client，long-lived secret 留在 server。</p>
<hr>
<h2 id="layer-3跨系統-provisioning身分對應-workflow不是新的信任邊界">Layer 3：跨系統 Provisioning（身分對應 workflow、不是新的信任邊界）</h2>
<p><strong>回答的問題</strong>：「系統 A 的使用者 X、在系統 B 對應到哪個身分？」</p>
<p><strong>Layer 3 跟 Layer 1 / 2 在概念上不對等</strong> — Layer 1 / 2 是「驗證某個身分」的信任邊界、各自需要獨立的 secret 機制；Layer 3 不引入新的 secret、是「<strong>讓兩個系統的使用者身分對應上</strong>」的 workflow。它建立在 Layer 1（A 已驗證使用者）跟 Layer 2（A 已被授權呼叫 B）之上、不取代任何一層。</p>
<p>之所以仍放進「層」的編號系統、是因為實際 API 串接時、開發者會把它跟前兩層一起遇到、必須在同一個心智模型裡處理。但設計時要清楚意識到：<strong>Layer 3 的失敗模式是「身分對不上」、不是「身分被偽造」</strong>、跟 Layer 1 / 2 的安全失敗模式不同。</p>
<h3 id="為什麼需要-provisioning">為什麼需要 provisioning</h3>
<p>當 A 跟 B 是兩個獨立 service 時，「<strong>A 的使用者 X</strong>」跟「<strong>B 的使用者 X</strong>」未必是同一筆資料。可能：</p>
<ul>
<li>B 從來沒見過 X 這個人</li>
<li>B 有自己對 X 的 record、但跟 A 不同 schema</li>
<li>B 看過 X、但兩邊的 user_id 還沒對應上</li>
</ul>
<p>需要一個機制把兩邊綁定 — 這個動作叫 <strong>provisioning</strong>。</p>
<h3 id="eager-vs-lazy-兩種策略">Eager vs Lazy 兩種策略</h3>
<p>Provisioning 策略的判斷核心是「何時承擔跨系統建檔成本」。Eager 把成本前移到註冊流程，Lazy 把成本延後到第一次使用；兩者差異不只是效能，而是資料膨脹、首用體驗與文件契約的取捨。</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">EAGER (註冊時就跨系統建檔)
</span></span><span class="line"><span class="ln">2</span><span class="cl">────────────────────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">使用者註冊系統 A
</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">   A 新增會員 row
</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">   A ──同步呼叫──▶ B.createUser()  ← 即使他可能永遠不用 B
</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">   兩邊都有資料、可以立刻呼叫 B 的 API</span></span></code></pre></div><p>Eager 適合大多數使用者都會用到 B 功能、且首用延遲成本高的服務。主要風險是 B 會累積大量低活躍 user，schema migration、備份與隱私刪除流程都會被放大。</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">LAZY (第一次需要時才建)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">────────────────────────────
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">使用者註冊系統 A
</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">   A 新增會員 row              ← 只有 A 這邊
</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">   ...日後可能很久才用到 B...
</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">使用者第一次需要 B 的功能
</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">   呼叫 A 的「provision」endpoint
</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">   A ──呼叫──▶ B.findOrCreateUser()  ← 這時候才建
</span></span><span class="line"><span class="ln">14</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">15</span><span class="cl">   之後就跟 eager 一樣</span></span></code></pre></div><p>Lazy 適合只有一部分使用者會用到 B 功能、且第一次使用可以接受一次 provisioning 延遲的服務。主要風險是「第一次使用」這個時機需要被寫進文件、SDK 或錯誤碼，否則接手者會把 B 的 404 誤判成 request 格式或權限問題。</p>
<h3 id="lazy-的隱性-api-依賴順序">Lazy 的「隱性 API 依賴順序」</h3>
<p>Lazy provisioning 的最大成本是<strong>隱性依賴順序造成的認知負擔</strong>：</p>
<ul>
<li>文件若沒有寫清楚「呼叫 B 前先呼叫 A 的 provision endpoint」，接手者會在「B 回 404 找不到 user」的訊號上花大量時間排查</li>
<li>用 SDK 包裝可以把 provision 自動處理、對外只暴露單一 API</li>
<li>不用 SDK 時，文件需要在快速上手與錯誤碼段落顯眼註明這個依賴順序</li>
</ul>
<p>折衷做法：B 的 API 在第一次發現 user 不存在時、<strong>主動回一個 <code>PROVISIONING_REQUIRED</code> 錯誤碼</strong>、client 看到就知道要去呼叫 A 的 provision endpoint。比起靜默 500 或單純 404 更能引導 client 走到正確流程。</p>
<h3 id="信任邊界示意">信任邊界示意</h3>





<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">[ 使用者 ] ──Layer 1──▶ [ 系統 A ] ══Layer 2══▶ [ 系統 B ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                            │  Layer 3 workflow：
</span></span><span class="line"><span class="ln">3</span><span class="cl">                            └─ 觸發後在 B 建立對應身分</span></span></code></pre></div><p>Layer 3 不引入新的 secret、是「<strong>建立兩邊身分關聯</strong>」的 lifecycle 動作。它依賴 Layer 1（確認使用者身分）跟 Layer 2（A 被授權對 B 發指令）。沒有 Layer 1 / 2 的話、provisioning 自己無法獨立成立。</p>
<hr>
<h2 id="三層怎麼組合">三層怎麼組合</h2>
<p>把三層擺在一起的典型 request 流程：</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">        ┌─────────────┐                       ┌──────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">        │  使用者      │                       │   系統 A     │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        │  (Browser/  │ ──── Layer 1 ──────▶ │              │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        │   App)      │      Bearer token     │              │
</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">                                            Layer 3  │ Provision
</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></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">                                              │   系統 B     │
</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></span><span class="line"><span class="ln">14</span><span class="cl">                                                     │
</span></span><span class="line"><span class="ln">15</span><span class="cl">                                            Layer 2  │ Shared secret
</span></span><span class="line"><span class="ln">16</span><span class="cl">                                                     │ (server-to-server)</span></span></code></pre></div><p>每一條線都是一層信任邊界，各自需要不同 secret 機制保護。</p>
<hr>
<h2 id="設計時最常見的三個失效模式">設計時最常見的三個失效模式</h2>
<h3 id="失效模式一讓使用者-token-也能驗-layer-2">失效模式一：讓使用者 token 也能驗 Layer 2</h3>
<p><strong>責任分工</strong>：「使用者身分」跟「呼叫系統身分」是兩個獨立維度、各自需要獨立 credential。系統 B 對「來自 A」的信任應綁定在系統層 credential，而不是任何單一使用者帳號上。</p>
<p><strong>常見誤用</strong>：B 接受「只要 request 帶有任一合法使用者 token 就放行」。</p>
<p><strong>風險判讀</strong>：這會把系統信任降階為使用者信任。任一帳號被盜（釣魚、密碼洩漏、token 外流）時，攻擊者就能用該使用者身分對 B 發 request，執行 B 開放給 A 的系統操作。</p>
<p><strong>操作路由</strong>：使用者層用 Layer 1 token，系統層用 Layer 2 credential，兩層都通過才放行。</p>
<h3 id="失效模式二把-layer-2-secret-放進-client">失效模式二：把 Layer 2 secret 放進 client</h3>
<p><strong>責任分工</strong>：Layer 2 secret 是「server 代表系統 A 對外的證明」，應留在 server 端的受信任執行環境。</p>
<p><strong>常見誤用</strong>：把 shared secret 寫進前端 JS、行動 app 編譯時、甚至 git public repo。</p>
<p><strong>風險判讀</strong>：client 環境（瀏覽器、mobile app）不在受控範圍。JS source 可在 devtools 直接看，mobile binary 可被反編譯出字串。Obfuscation 提高的是時間成本，沒有改變 secret 已散佈到不受信任環境的事實。</p>
<p><strong>操作路由</strong>：client 需要 B 的功能時，走「client → A → B」，由 A 在 server 端用 Layer 2 secret 呼叫 B；或用 OAuth 把 short-lived token 發給 client，long-lived secret 留在 server。</p>
<h3 id="失效模式三layer-3-依賴順序沒文件化">失效模式三：Layer 3 依賴順序沒文件化</h3>
<p><strong>責任分工</strong>：跨系統依賴順序是 API 契約的一部分，屬 publisher 的責任，需要在文件、SDK 或錯誤訊號中顯式表達。</p>
<p><strong>常見誤用</strong>：「呼叫 B 之前要先呼叫 A 的某個 endpoint」這個前置條件只存在於原始設計者的記憶中、文件沒寫、SDK 沒包、B 失敗時也只回 generic error。</p>
<p><strong>風險判讀</strong>：接手者看到「呼叫 B 失敗」時，會優先檢查 B 的文件、request 格式與 network 層。若真正根因是尚未呼叫 A 的 provision endpoint，偵錯路徑會被導到錯誤層級。</p>
<p><strong>操作路由</strong>（任選其一、優先序由上而下）：</p>
<ol>
<li>SDK 包裝、自動處理 provision、對外只暴露單一 API</li>
<li>B 主動回 <code>PROVISIONING_REQUIRED</code> error code、引導 client 補上前置呼叫</li>
<li>文件在「快速上手」段顯眼處註明依賴順序</li>
</ol>
<hr>
<h2 id="何時可以簡化三層">何時可以簡化三層</h2>
<p>三層框架的設計重點是「跨系統身分與 credential 分工」。當某一層回答的問題在架構裡不存在，設計可以縮小到實際存在的身分問題。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>簡化方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單體 application（沒有跨系統呼叫）</td>
          <td>只需 Layer 1。沒有 system-to-system 互動、Layer 2 / 3 不存在</td>
      </tr>
      <tr>
          <td>內網微服務、共用 identity provider</td>
          <td>Layer 1 透過 service mesh 或共用 token 傳遞、Layer 2 可用 service mesh 內建 mTLS 取代手動 secret 管理</td>
      </tr>
      <tr>
          <td>後端 cron / batch job 之間互呼</td>
          <td>只需 Layer 2（system-to-system credential）、沒有使用者觸發、Layer 1 不適用</td>
      </tr>
      <tr>
          <td>兩個系統共用同一份 user DB</td>
          <td>可省略 Layer 3（身分天然對應），但 Layer 1 / 2 仍各自獨立</td>
      </tr>
  </tbody>
</table>
<p>簡化的判準是「<strong>該層回答的問題是否真實存在於這個架構</strong>」。單體 application 沒有跨系統呼叫時，Layer 2 的 caller 驗證可以省略；兩個系統共用同一份 user DB 時，Layer 3 的身分對應 workflow 可以省略。</p>
<p>簡化不等於降低基礎安全前提。HTTPS / TLS 與 token 儲存原則（hash + constant-time）是任何 Layer 1 的最低要求，跟「層」的數量無關。</p>
<hr>
<h2 id="收尾">收尾</h2>
<p>兩層信任邊界 + 一個身分對應 workflow：</p>
<ul>
<li><strong>Layer 1（使用者）</strong>：解決「你是誰」 — 用 Bearer Token、注意 capability credential 的暴露成本</li>
<li><strong>Layer 2（系統）</strong>：解決「哪個系統呼叫的」 — 用 Shared Secret / API Key / OAuth / mTLS、secret 不離 server</li>
<li><strong>Layer 3（Provisioning workflow）</strong>：解決「兩邊身分怎麼對上」 — 不是新的 secret、是 lifecycle 動作</li>
</ul>
<p>設計後端 API 時，先把這三個問題分開，secret 機制的選擇會變清楚。若排障訊號是「這個 token 在那邊不能用」，下一步是先判斷它卡在使用者層、系統層，還是 provisioning workflow。</p>
<h3 id="各層的深入文章">各層的深入文章</h3>
<p>本文聚焦「為什麼要分層」的心智模型、各層的具體實作細節都另有獨立文章：</p>
<ul>
<li><strong>Layer 1（使用者）</strong> → <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>：<code>{PK}|{secret}</code> format 為什麼這樣設計、DB 儲存三原則、各語言 constant-time 函式對照、跟 GitHub / Stripe 的設計比較</li>
<li><strong>Layer 2（系統）→ Shared Secret 維運</strong> → <a href="/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/" data-link-title="Shared Secret 安全輪替設計：雙密過渡期、自動化與緊急流程" data-link-desc="系統間 Shared Secret 輪替的核心機制：dual-secret rollover、自動化工具比較（AWS Secrets Manager / Vault / GCP）、緊急 rotation 流程與多 client 環境的失敗模式。">Shared Secret 安全輪替設計</a>：雙密過渡期、自動化 rotation 工具（AWS Secrets Manager / Vault / GCP）、緊急 vs 定期流程、多 client 同步難題</li>
<li><strong>Layer 2（系統）→ mTLS 部署</strong> → <a href="/blog/work-log/mtls-%E5%AF%A6%E9%9A%9B%E6%80%8E%E9%BA%BC%E8%A8%AD%E5%AE%9A%E8%88%87%E9%81%8B%E7%B6%ADca-%E9%9A%8E%E5%B1%A4%E6%86%91%E8%AD%89%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%92%A4%E9%8A%B7%E6%A9%9F%E5%88%B6/" data-link-title="mTLS 實際怎麼設定與運維：CA 階層、憑證生命週期、撤銷機制" data-link-desc="mTLS 落地的運維決策（CA 階層、憑證儲存、撤銷機制）與基礎設施整合（nginx / envoy / service mesh），以及跟 API Key / OAuth 的成本與安全取捨。">mTLS 實際怎麼設定與運維</a>：CA 階層、憑證生命週期、撤銷機制（CRL / OCSP / short-lived）、nginx / Envoy / service mesh 整合</li>
</ul>
<h3 id="沒展開的延伸議題">沒展開的延伸議題</h3>
<p>JWT 的簽章演算法選擇、<code>alg: none</code> 攻擊、token rotation 的具體實作、零信任網路下的 service-to-service 認證、OAuth flow 的完整 lifecycle、SSO（SAML / OIDC）跟本文三層的對應關係。每個都值得獨立成篇、本文聚焦在「先把層數想清楚」這個前置問題。</p>
]]></content:encoded></item><item><title>Shared Secret 安全輪替設計：雙密過渡期、自動化與緊急流程</title><link>https://tarrragon.github.io/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/</guid><description>&lt;h2 id="shared-secret-rotation-這篇要解決什麼">Shared Secret Rotation 這篇要解決什麼&lt;/h2>
&lt;p>Shared Secret rotation 的核心責任是讓 credential 換新時維持可用性、可追蹤性與可撤銷性。它表面上像是一行 SQL update，實際上牽涉 server 與多個 client 的切換時序：&lt;/p>
&lt;ul>
&lt;li>兩邊不同時切、就斷線&lt;/li>
&lt;li>多 client 場景下、總有一兩個沒升級&lt;/li>
&lt;li>緊急洩漏要立即撤換、同時控制服務中斷範圍&lt;/li>
&lt;li>Rotation 中途失敗、舊新 secret 都不通&lt;/li>
&lt;/ul>
&lt;p>這些是維運層的真實痛點。只說「定期 rotate your secret」只能描述目標，還需要雙密期、測試、監控、通知與回退流程，才能把 rotation 變成可執行的操作契約。&lt;/p>
&lt;p>本文聚焦三件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>雙密過渡期&lt;/strong>：怎麼讓 client 可以在任意時點切換、不會斷線&lt;/li>
&lt;li>&lt;strong>自動化工具&lt;/strong>：AWS Secrets Manager / HashiCorp Vault / GCP Secret Manager 各自的 rotation 機制&lt;/li>
&lt;li>&lt;strong>緊急 vs 定期&lt;/strong>：兩種流程的差異、何時用哪個&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>&lt;strong>本文位置&lt;/strong>：本文是 &lt;a href="https://tarrragon.github.io/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/" data-link-title="API 認證的三層信任邊界：使用者、系統、跨系統 Provisioning" data-link-desc="API 認證的信任邊界分層（Bearer Token / Shared Secret / Provisioning）：各層的洩漏後果與撤銷方式，以及混用造成的設計失效模式。">API 認證的三層信任邊界&lt;/a> Layer 2 的深入篇。主文聚焦「為什麼系統間要獨立 credential」、本文聚焦「Shared Secret 輪替的工程實務」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="rotation-解決什麼威脅">Rotation 解決什麼威脅&lt;/h2>
&lt;p>Rotation 是縮短 secret 暴露窗與清理殘留 access 的 lifecycle 控制。它降低三種具體威脅：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>威脅&lt;/th>
 &lt;th>Rotation 怎麼緩解&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>未察覺的洩漏&lt;/strong>&lt;/td>
 &lt;td>Secret 可能已被外洩、定期換能限制攻擊者使用的時間窗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>離職員工殘留 access&lt;/strong>&lt;/td>
 &lt;td>員工離職後系統 access 沒撤徹底、rotation 把該員工知道的 secret 變廢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>長期暴露的 metadata&lt;/strong>&lt;/td>
 &lt;td>Secret 越久、log / backup / git history 留存的副本越多&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Rotation 本身有成本與風險，切換設計不完整時會造成斷線。所以實務目標是「在切換可控的前提下，選一個能接受的頻率」。&lt;/p>
&lt;p>常見定期頻率：&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>一般 SaaS&lt;/td>
 &lt;td>90 天 / 180 天&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>金融、醫療&lt;/td>
 &lt;td>30 天 / 90 天&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高敏感（國防、政府）&lt;/td>
 &lt;td>7 天 / 14 天、或事件觸發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>純內部、低風險&lt;/td>
 &lt;td>半年 / 一年、或永不 rotate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>頻率取決於威脅模型與操作能力&lt;/strong>：NIST SP 800-63B 對多數場景認可 30-90 天足夠、過於激進的 rotation 反而提高出錯機率。7-14 天適用於合規條款明文要求或私鑰可硬體保護的場景；多數 SaaS 可以停在 30-180 天區間。&lt;/p>&lt;/blockquote>
&lt;p>「事件觸發才換」也有合理情境。純內部 cron job、secret 外流管道少、rotation 成本大於風險時，可以選擇以事件觸發取代固定排程；重點是留下 owner、inventory 與重新評估條件。&lt;/p>
&lt;hr>
&lt;h2 id="核心機制雙密過渡期dual-secret-rollover">核心機制：雙密過渡期（Dual-secret Rollover）&lt;/h2>
&lt;h3 id="直接-atomic-切換的失效點">直接 atomic 切換的失效點&lt;/h3>
&lt;p>最直覺的 rotation 流程是：&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">T0: 兩邊都是 secret_v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">T1: server 端換成 secret_v2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">T2: client 端換成 secret_v2&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>失效點出在 T1 到 T2 之間：server 只認 v2，但 client 還在用 v1，這段窗口內的 request 都會 401。即使窗口只有幾秒，production 流量也可能產生大量錯誤。&lt;/p>
&lt;p>更糟的是「client 更新後忘了 reload process」這種情境 — 配置檔已改、但跑著的 server / worker process 還握著舊 secret 在記憶體裡、直到下次重啟才生效。窗口可能拉長到幾分鐘到幾小時。&lt;/p></description><content:encoded><![CDATA[<h2 id="shared-secret-rotation-這篇要解決什麼">Shared Secret Rotation 這篇要解決什麼</h2>
<p>Shared Secret rotation 的核心責任是讓 credential 換新時維持可用性、可追蹤性與可撤銷性。它表面上像是一行 SQL update，實際上牽涉 server 與多個 client 的切換時序：</p>
<ul>
<li>兩邊不同時切、就斷線</li>
<li>多 client 場景下、總有一兩個沒升級</li>
<li>緊急洩漏要立即撤換、同時控制服務中斷範圍</li>
<li>Rotation 中途失敗、舊新 secret 都不通</li>
</ul>
<p>這些是維運層的真實痛點。只說「定期 rotate your secret」只能描述目標，還需要雙密期、測試、監控、通知與回退流程，才能把 rotation 變成可執行的操作契約。</p>
<p>本文聚焦三件事：</p>
<ol>
<li><strong>雙密過渡期</strong>：怎麼讓 client 可以在任意時點切換、不會斷線</li>
<li><strong>自動化工具</strong>：AWS Secrets Manager / HashiCorp Vault / GCP Secret Manager 各自的 rotation 機制</li>
<li><strong>緊急 vs 定期</strong>：兩種流程的差異、何時用哪個</li>
</ol>
<blockquote>
<p><strong>本文位置</strong>：本文是 <a href="/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/" data-link-title="API 認證的三層信任邊界：使用者、系統、跨系統 Provisioning" data-link-desc="API 認證的信任邊界分層（Bearer Token / Shared Secret / Provisioning）：各層的洩漏後果與撤銷方式，以及混用造成的設計失效模式。">API 認證的三層信任邊界</a> Layer 2 的深入篇。主文聚焦「為什麼系統間要獨立 credential」、本文聚焦「Shared Secret 輪替的工程實務」。</p></blockquote>
<hr>
<h2 id="rotation-解決什麼威脅">Rotation 解決什麼威脅</h2>
<p>Rotation 是縮短 secret 暴露窗與清理殘留 access 的 lifecycle 控制。它降低三種具體威脅：</p>
<table>
  <thead>
      <tr>
          <th>威脅</th>
          <th>Rotation 怎麼緩解</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>未察覺的洩漏</strong></td>
          <td>Secret 可能已被外洩、定期換能限制攻擊者使用的時間窗</td>
      </tr>
      <tr>
          <td><strong>離職員工殘留 access</strong></td>
          <td>員工離職後系統 access 沒撤徹底、rotation 把該員工知道的 secret 變廢</td>
      </tr>
      <tr>
          <td><strong>長期暴露的 metadata</strong></td>
          <td>Secret 越久、log / backup / git history 留存的副本越多</td>
      </tr>
  </tbody>
</table>
<p>Rotation 本身有成本與風險，切換設計不完整時會造成斷線。所以實務目標是「在切換可控的前提下，選一個能接受的頻率」。</p>
<p>常見定期頻率：</p>
<table>
  <thead>
      <tr>
          <th>業界場景</th>
          <th>典型頻率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般 SaaS</td>
          <td>90 天 / 180 天</td>
      </tr>
      <tr>
          <td>金融、醫療</td>
          <td>30 天 / 90 天</td>
      </tr>
      <tr>
          <td>高敏感（國防、政府）</td>
          <td>7 天 / 14 天、或事件觸發</td>
      </tr>
      <tr>
          <td>純內部、低風險</td>
          <td>半年 / 一年、或永不 rotate</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>頻率取決於威脅模型與操作能力</strong>：NIST SP 800-63B 對多數場景認可 30-90 天足夠、過於激進的 rotation 反而提高出錯機率。7-14 天適用於合規條款明文要求或私鑰可硬體保護的場景；多數 SaaS 可以停在 30-180 天區間。</p></blockquote>
<p>「事件觸發才換」也有合理情境。純內部 cron job、secret 外流管道少、rotation 成本大於風險時，可以選擇以事件觸發取代固定排程；重點是留下 owner、inventory 與重新評估條件。</p>
<hr>
<h2 id="核心機制雙密過渡期dual-secret-rollover">核心機制：雙密過渡期（Dual-secret Rollover）</h2>
<h3 id="直接-atomic-切換的失效點">直接 atomic 切換的失效點</h3>
<p>最直覺的 rotation 流程是：</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">T0: 兩邊都是 secret_v1
</span></span><span class="line"><span class="ln">2</span><span class="cl">T1: server 端換成 secret_v2
</span></span><span class="line"><span class="ln">3</span><span class="cl">T2: client 端換成 secret_v2</span></span></code></pre></div><p>失效點出在 T1 到 T2 之間：server 只認 v2，但 client 還在用 v1，這段窗口內的 request 都會 401。即使窗口只有幾秒，production 流量也可能產生大量錯誤。</p>
<p>更糟的是「client 更新後忘了 reload process」這種情境 — 配置檔已改、但跑著的 server / worker process 還握著舊 secret 在記憶體裡、直到下次重啟才生效。窗口可能拉長到幾分鐘到幾小時。</p>
<h3 id="解法server-端同時接受新舊兩把">解法：server 端同時接受新舊兩把</h3>
<p>雙密過渡期把 rotation 分成 3 個階段：</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">T0：穩態
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  server: [v1]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  client: [v1]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  狀態：v1 工作
</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">T1：發新 secret、server 雙密期開始
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  server: [v1, v2]   ← server 同時接受 v1 跟 v2
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  client: [v1]
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  狀態：兩個都 work、client 還沒切
</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">T2：通知 client 切到 v2
</span></span><span class="line"><span class="ln">12</span><span class="cl">  server: [v1, v2]
</span></span><span class="line"><span class="ln">13</span><span class="cl">  client: [v2]       ← client 升級、開始用 v2
</span></span><span class="line"><span class="ln">14</span><span class="cl">  狀態：v2 work、v1 也仍 work（過渡期）
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">T3：確認所有 client 都切完、關閉 v1
</span></span><span class="line"><span class="ln">17</span><span class="cl">  server: [v2]       ← 移除 v1
</span></span><span class="line"><span class="ln">18</span><span class="cl">  client: [v2]
</span></span><span class="line"><span class="ln">19</span><span class="cl">  狀態：穩態、只 v1 失效</span></span></code></pre></div><p>關鍵在於 <strong>server 在 T1-T3 之間同時接受兩把</strong> — 不論 client 在這段期間用哪一把都能通過驗證。client 可以在自己的時程內升級、不需要跟 server 切換同步。</p>
<h3 id="雙密期的長度設計">雙密期的長度設計</h3>
<p>雙密期是一個可用性與暴露窗的取捨。兩把同時有效時，系統需要同時保護兩把 secret，也需要追蹤兩個版本的使用比例；時間拉太短會造成 client 來不及切換，時間拉太長會擴大舊 secret 的有效窗口。</p>
<p>設計建議：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>雙密期長度建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純內部、可強制升級</td>
          <td>24-48 小時</td>
      </tr>
      <tr>
          <td>對外 client、需要溝通</td>
          <td>7-14 天</td>
      </tr>
      <tr>
          <td>大量第三方整合</td>
          <td>30-90 天 + 多次提醒</td>
      </tr>
      <tr>
          <td>緊急 rotation（已洩漏）</td>
          <td>盡量縮短、視覆蓋速度而定</td>
      </tr>
  </tbody>
</table>
<p>監控指標：在雙密期內、應該追蹤「用 v1 vs 用 v2 的 request 比例」 — 當 v1 比例降到 0%、且持續穩定一段時間後、才安全地關閉 v1。</p>
<h3 id="怎麼實作同時接受兩把">怎麼實作「同時接受兩把」</h3>
<p>實作模式有兩種：</p>
<h4 id="模式-aarray-比對">模式 A：array 比對</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">VALID_SECRETS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s1">&#39;SHARED_SECRET_CURRENT&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s1">&#39;SHARED_SECRET_PREVIOUS&#39;</span><span class="p">],</span>  <span class="c1"># 可選、若在雙密期</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">def</span> <span class="nf">verify</span><span class="p">(</span><span class="n">received</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">for</span> <span class="n">secret</span> <span class="ow">in</span> <span class="n">VALID_SECRETS</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">secret</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">if</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">secret</span><span class="p">,</span> <span class="n">received</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="kc">False</span></span></span></code></pre></div><p>這個模式適合內部固定夥伴與少量服務，因為驗證邏輯簡單、沒有額外狀態。主要風險是兩把 secret 都要部署到 server，env var / config 變多，且每個 instance 都要確認讀到相同版本。</p>
<h4 id="模式-bsecret-store--version">模式 B：secret store + version</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">verify</span><span class="p">(</span><span class="n">received</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">current_version</span> <span class="o">=</span> <span class="n">secret_store</span><span class="o">.</span><span class="n">get_version</span><span class="p">(</span><span class="s1">&#39;shared_secret&#39;</span><span class="p">,</span> <span class="s1">&#39;current&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">previous_version</span> <span class="o">=</span> <span class="n">secret_store</span><span class="o">.</span><span class="n">get_version</span><span class="p">(</span><span class="s1">&#39;shared_secret&#39;</span><span class="p">,</span> <span class="s1">&#39;previous&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">current_version</span><span class="p">,</span> <span class="n">received</span><span class="p">)</span> <span class="ow">or</span> \
</span></span><span class="line"><span class="ln">5</span><span class="cl">           <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">previous_version</span><span class="p">,</span> <span class="n">received</span><span class="p">)</span></span></span></code></pre></div><p>這個模式適合對外 API 或 client 數量較多的系統，因為 secret 集中管理、版本狀態可查。主要風險是驗證流程依賴 secret store，需要設計 cache、fallback 與 store 失效時的行為。</p>
<p>對外開放 API 通常用模式 B、可結合 AWS Secrets Manager / Vault 等工具。內部固定夥伴系統可以用模式 A 起步、複雜後再遷移。</p>
<hr>
<h2 id="自動化-rotation-工具">自動化 Rotation 工具</h2>
<p>純手動 rotation 在 client 數量增加後不可持續 — 自動化工具的價值是把「<strong>產生新 secret → 部署到 server → 通知 client → 撤銷舊 secret</strong>」整套流程程式化。</p>
<h3 id="aws-secrets-manager">AWS Secrets Manager</h3>
<p>機制：</p>
<ul>
<li>註冊一個 <strong>Rotation Lambda</strong>、AWS 排程觸發（例如每 90 天）</li>
<li>Lambda 跑 4 階段流程：<code>createSecret</code> → <code>setSecret</code> → <code>testSecret</code> → <code>finishSecret</code></li>
<li>每個階段都有 retry、失敗會回到上一個穩態</li>
</ul>
<p>Lambda 範例責任分工：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>createSecret</code></td>
          <td>產生新 secret、存到 AWSPENDING 版本</td>
      </tr>
      <tr>
          <td><code>setSecret</code></td>
          <td>把新 secret 部署到目標 service</td>
      </tr>
      <tr>
          <td><code>testSecret</code></td>
          <td>用新 secret 跑驗證 request</td>
      </tr>
      <tr>
          <td><code>finishSecret</code></td>
          <td>把 AWSPENDING 升級為 AWSCURRENT、舊版改為 AWSPREVIOUS</td>
      </tr>
  </tbody>
</table>
<p>雙密期天然存在：AWSCURRENT + AWSPREVIOUS 兩個 staging label 同時可讀。Client 在 rotation 進行中、可以拿到 AWSPREVIOUS 作為 fallback。</p>
<p>適合場景：AWS 生態系、目標 service 是 RDS / Redshift / DocumentDB（有 native rotation Lambda template）或自定義（custom Lambda）。</p>
<h3 id="hashicorp-vault">HashiCorp Vault</h3>
<p>Vault 有兩種 rotation 策略：</p>
<p><strong>Static Secrets + Rotation Periodic</strong>：傳統 shared secret、Vault 每 N 天自動換、puts 到 vault path、client poll 拿。</p>
<p><strong>Dynamic Secrets</strong>：Vault 不存 long-lived secret、每次 client 請求時臨時產生（DB credential、AWS IAM credential 等）、TTL 短（小時到天）、過期即廢。Dynamic secret 沒有 rotation 概念 — 因為每個 secret 都只活一小段時間、洩漏窗本來就有限。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>適合</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Static + Periodic</td>
          <td>跨組織 API、需可預測的 secret</td>
          <td>仍需 client 端處理雙密期</td>
      </tr>
      <tr>
          <td>Dynamic</td>
          <td>內部 service 互呼、DB access</td>
          <td>目標系統要支援 short-lived credential</td>
      </tr>
  </tbody>
</table>
<p>適合場景：multi-cloud、不想綁 AWS、需要 dynamic secret 跨多種 backend。</p>
<h3 id="gcp-secret-manager">GCP Secret Manager</h3>
<p>機制較簡單 — Secret Manager 提供 <strong>versioning</strong>、每個 secret 有多個 version、client 可指定要「latest」還是特定 version。</p>
<p>Rotation 流程通常自己實作（GCP 沒提供類似 AWS 的 Rotation Lambda template）：</p>
<ol>
<li><code>addSecretVersion(name, new_secret)</code> — 加新 version</li>
<li>部署到 server（server 同時讀 latest + previous）</li>
<li>通知 client / 等 client 升級</li>
<li><code>destroySecretVersion(name, old_version)</code> — 撤銷舊 version</li>
</ol>
<p>雙密期靠 client 端邏輯（同時試 latest 跟 previous）實現。</p>
<p>適合場景：GCP 生態系、自有 rotation 邏輯不想被 vendor opinion 綁住。</p>
<h3 id="三者比較">三者比較</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>AWS Secrets Manager</th>
          <th>HashiCorp Vault</th>
          <th>GCP Secret Manager</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>排程觸發</td>
          <td>內建</td>
          <td>內建（periodic）</td>
          <td>不內建（自己排 Cloud Scheduler）</td>
      </tr>
      <tr>
          <td>雙密期支援</td>
          <td>AWSCURRENT / PREVIOUS labels</td>
          <td>Static 需自寫、Dynamic 不需</td>
          <td>Version-based</td>
      </tr>
      <tr>
          <td>Dynamic credential</td>
          <td>需 custom Lambda</td>
          <td>Native support</td>
          <td>不支援</td>
      </tr>
      <tr>
          <td>跨雲 / 跨 region</td>
          <td>AWS-only</td>
          <td>跨雲</td>
          <td>GCP-only</td>
      </tr>
      <tr>
          <td>維運成本</td>
          <td>低（managed）</td>
          <td>高（自管 Vault cluster）</td>
          <td>低（managed）</td>
      </tr>
  </tbody>
</table>
<h3 id="自建-rotation-系統的最小元件">自建 rotation 系統的最小元件</h3>
<p>小規模系統可以自建最小 rotation 元件，前提是 secret 系統本身也被視為敏感基礎設施。最小元件包含：</p>
<ol>
<li><strong>Secret 存儲</strong>：DB table <code>secrets(id, version, value, created_at, retired_at)</code></li>
<li><strong>發放 API</strong>：<code>GET /secrets/current</code> 回 latest active version</li>
<li><strong>驗證邏輯</strong>：應用層讀 current + previous 兩個 active version</li>
<li><strong>排程</strong>：cron job 觸發 <code>rotate(secret_name)</code> — 產新 version、標記舊版 retired、設 retired_at</li>
<li><strong>監控</strong>：log 每個 version 被驗證的次數、舊版降到 0 後關閉</li>
</ol>
<p>這個方案適合內部小規模系統。判斷是否可行時，要同步檢查 DB encryption at rest、access log、權限分離與備援；否則自建系統可能把 rotation 風險轉移成 secret store 風險。</p>
<hr>
<h2 id="緊急-rotation洩漏發生時的流程">緊急 rotation：洩漏發生時的流程</h2>
<h3 id="跟定期-rotation-的差異">跟定期 rotation 的差異</h3>
<p>定期 rotation 目標是「<strong>不中斷服務</strong>」、所以雙密期長、給 client 充分時間切換。</p>
<p>緊急 rotation 目標是「<strong>最快讓舊 secret 失效</strong>」 — 即使犧牲部分可用性也要立刻撤銷。兩者流程完全不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>定期 rotation</th>
          <th>緊急 rotation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發</td>
          <td>排程</td>
          <td>事件（洩漏、員工離職、被盜）</td>
      </tr>
      <tr>
          <td>優先級</td>
          <td>不中斷服務</td>
          <td>立即撤銷舊 secret</td>
      </tr>
      <tr>
          <td>雙密期</td>
          <td>長（天到月）</td>
          <td>短（小時、甚至不容忍）</td>
      </tr>
      <tr>
          <td>通知方式</td>
          <td>文件、email、提早提醒</td>
          <td>直接 push、必要時打電話</td>
      </tr>
      <tr>
          <td>Client 不升級</td>
          <td>等</td>
          <td>強制斷線</td>
      </tr>
  </tbody>
</table>
<h3 id="緊急-rotation-流程模板">緊急 rotation 流程模板</h3>





<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">T0: 偵測或回報洩漏
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">T0+0~15min: 評估
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 確認洩漏範圍（哪些 secret、影響哪些 client）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - 評估「立即斷舊 secret」對 production 的影響
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   - 決定是否走緊急流程 vs 縮短的定期流程
</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">T0+15min~1hr: 部署新 secret
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   - 產生新 secret
</span></span><span class="line"><span class="ln">10</span><span class="cl">   - 部署到 server、開啟雙密期
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 主動 push 新 secret 給已知 client（內部用 channel 通知、外部 client email + dashboard）
</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">T0+1hr~24hr: 強制切換
</span></span><span class="line"><span class="ln">14</span><span class="cl">   - 監控用舊 secret 的 request 比例
</span></span><span class="line"><span class="ln">15</span><span class="cl">   - 跟未升級的 client 個別聯繫
</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">T0+24hr~72hr: 撤銷舊 secret
</span></span><span class="line"><span class="ln">19</span><span class="cl">   - 即使仍有 client 在用舊 secret、也斷
</span></span><span class="line"><span class="ln">20</span><span class="cl">   - 接受部分服務中斷、優先於 secret 繼續暴露
</span></span><span class="line"><span class="ln">21</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">22</span><span class="cl">事後: 檢討
</span></span><span class="line"><span class="ln">23</span><span class="cl">   - 洩漏怎麼發生（log 翻查、code audit）
</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>關鍵權衡：<strong>「斷線成本」vs「secret 繼續暴露的損害」</strong>。對金融、醫療等高敏感場景、寧可斷線；對非關鍵內部服務、可能可以拉長雙密期。沒有通用答案、要場景判斷。</p>
<h3 id="偵測洩漏的訊號">偵測洩漏的訊號</h3>
<p>緊急 rotation 的前提是「<strong>知道洩漏發生了</strong>」 — 但很多洩漏直到攻擊者開始用 secret 才被發現、間隔可能是幾個月。</p>
<p>主動偵測手段：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>怎麼偵測</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Secret 出現在公開 repo</strong></td>
          <td>GitHub Secret Scanning、GitGuardian、TruffleHog</td>
      </tr>
      <tr>
          <td><strong>異常使用 pattern</strong></td>
          <td>異常時間、異常 IP、異常 request 量</td>
      </tr>
      <tr>
          <td><strong>多個 IP 同時用同一 secret</strong></td>
          <td>應用層 log 分析、SIEM 工具</td>
      </tr>
      <tr>
          <td><strong>離職員工觸發 access</strong></td>
          <td>跟 HR 系統整合的 access review</td>
      </tr>
  </tbody>
</table>
<p>把這些設成監控告警、是降低「洩漏到察覺」窗口的關鍵。</p>
<hr>
<h2 id="多-client-的同步難題">多 client 的同步難題</h2>
<h3 id="問題本質client-不在你的控制範圍">問題本質：client 不在你的控制範圍</h3>
<p>對外開放 API 的場景，Shared Secret 散落在第三方 client 的 server。Rotation 因此變成「怎麼讓第三方在你的時程內配合」的協調問題，不只是技術問題。</p>
<p>常見痛點：</p>
<ul>
<li>通知 email 進垃圾匣、第三方沒看到</li>
<li>第三方的工程師離職、新接手者不知道有 rotation 排程</li>
<li>第三方的 deploy 流程慢、提前一週通知還是來不及</li>
<li>第三方根本不在線（小型客戶、半年才用一次 API）</li>
</ul>
<h3 id="grace-period-設計">Grace period 設計</h3>
<p>Grace period 是「<strong>舊 secret 撤銷後、給 client 緩衝期重新申請</strong>」的機制。比硬性 deadline 更彈性：</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">T0: 公告 rotation、雙密期開始
</span></span><span class="line"><span class="ln">2</span><span class="cl">T0+30天: 雙密期結束、舊 secret 撤銷
</span></span><span class="line"><span class="ln">3</span><span class="cl">T0+30~60天: Grace period
</span></span><span class="line"><span class="ln">4</span><span class="cl">   - 用舊 secret 的 request 回 410 Gone（或 401 + 可讀的 error code，視 API 慣例）+ 連結到 &#34;secret expired&#34; 頁
</span></span><span class="line"><span class="ln">5</span><span class="cl">   - 提供 self-service 重設 secret 的流程
</span></span><span class="line"><span class="ln">6</span><span class="cl">   - 仍然斷線、但 client 知道怎麼自己救
</span></span><span class="line"><span class="ln">7</span><span class="cl">T0+60天: 完全關閉、需要重新申請新 client account</span></span></code></pre></div><p>Grace period 的關鍵是在拒絕舊 secret 的同時，提供足夠資訊讓 client 自助修復。判讀訊號是錯誤回應是否能指出 secret 已過期、去哪裡重設、何時完全關閉；若只回無上下文的 401，client 仍會被導向錯誤排障路徑。</p>
<h3 id="強制升級的工具">強制升級的工具</h3>
<p>對於必須統一升級的場景（例如安全合規要求）、有幾種強制手段：</p>
<table>
  <thead>
      <tr>
          <th>手段</th>
          <th>怎麼運作</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>HTTP 410 + 訊息</strong></td>
          <td>舊 secret 不只 401、回 410 + 升級指引</td>
          <td>一般對外 API</td>
      </tr>
      <tr>
          <td><strong>暫時降級而非斷線</strong></td>
          <td>舊 secret 仍 work、但限流 / 降級權限</td>
          <td>重要 client、寧可降級不要斷</td>
      </tr>
      <tr>
          <td><strong>個別溝通 + 客製化期限</strong></td>
          <td>對大 client 個別協商 deadline</td>
          <td>高價值合作夥伴</td>
      </tr>
      <tr>
          <td><strong>合約強制條款</strong></td>
          <td>簽約時就寫清楚「Y 年內必須能配合 rotation」</td>
          <td>B2B SaaS</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="失敗模式與緩解">失敗模式與緩解</h2>
<h3 id="失敗-1雙密期太短client-沒升級">失敗 1：雙密期太短、client 沒升級</h3>
<p><strong>症狀</strong>：rotation 後第二週，某 client 開始 401，才發現他沒收到通知或尚未升級。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>雙密期至少覆蓋「最大已知 client 的 deploy cycle」</li>
<li>雙密期內監控「用舊 secret 的 client 數量」、降到 0 才關</li>
<li>緊急 rotation 例外、要事先評估可接受的斷線成本</li>
</ul>
<h3 id="失敗-2rotation-中斷新舊都不通">失敗 2：rotation 中斷、新舊都不通</h3>
<p><strong>症狀</strong>：deploy 新 secret 到 server 中途失敗、一半 server 是新、一半是舊 — request 隨機 401。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>部署用 rolling update、確認每個 instance 都生效再進下一個</li>
<li>部署前確認「server 是雙密 mode」、即使部署到一半也能容錯</li>
<li>保留快速 rollback 機制（10 分鐘內能 revert）</li>
</ul>
<h3 id="失敗-3新-secret-沒測通就上線">失敗 3：新 secret 沒測通就上線</h3>
<p><strong>症狀</strong>：新 secret 部署完、第一個 client 試了發現格式不對 / 長度限制 / 特殊字元編碼問題、大量 401。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Rotation 流程加 <code>testSecret</code> 階段（AWS Lambda 模式）— 切換前用新 secret 跑一輪驗證 request</li>
<li>Staging 環境先跑完整 rotation 流程、再上 prod</li>
<li>新 secret 的 format 跟舊一致（同長度、同字元集）、減少 client 端的 parsing 風險</li>
</ul>
<h3 id="失敗-4rotation-缺少-ownersecret-長期暴露">失敗 4：Rotation 缺少 owner、secret 長期暴露</h3>
<p><strong>症狀</strong>：上次 rotate 已是 3 年前，原本的負責人離職，接手者不知道有這個 secret 存在。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Secret 管理工具強制設 <code>expires_at</code>、過期前自動提醒</li>
<li>Inventory 表：所有 production secret 列管、定期 audit</li>
<li>Rotation 排程進 calendar、輪值負責</li>
</ul>
<h3 id="失敗-5rotation-後-audit-log-沒更新">失敗 5：rotation 後 audit log 沒更新</h3>
<p><strong>症狀</strong>：洩漏發生、要追「這個 secret 給過誰用」、但 audit log 只記了「secret 被用了」、沒記版本、無法區分新舊。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Audit log 記 secret version、不只 secret 本身</li>
<li>Rotation 事件本身也要 log（誰、什麼時候、為什麼）</li>
<li>Log 保留期跨多次 rotation cycle、避免歷史追溯斷鏈</li>
</ul>
<hr>
<h2 id="收尾">收尾</h2>
<p>Shared Secret rotation 的本質是<strong>有意識管理 secret 的 lifecycle</strong>。從發放、儲存、輪替到撤銷，每個階段都有對應的工程設計與監控訊號。</p>
<p>幾個核心原則：</p>
<ol>
<li><strong>雙密過渡期是底層機制</strong> — 任何 rotation 方案都建立在「server 能同時接受兩把」之上</li>
<li><strong>自動化工具值得投資</strong> — 規模小用 secret manager（AWS / Vault / GCP），規模大可以自建，避免停在純手動</li>
<li><strong>定期跟緊急是兩套流程</strong> — 定期重不中斷，緊急重立刻撤，流程、通知與回退標準要分開</li>
<li><strong>多 client 是協調問題</strong> — 比技術問題難解、grace period + 強制升級工具是常用解法</li>
<li><strong>失敗模式要演練</strong> — production 第一次跑 rotation 前，先在 staging 演練完整流程與回退路徑</li>
</ol>
<p>延伸閱讀：</p>
<ul>
<li><a href="/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/" data-link-title="API 認證的三層信任邊界：使用者、系統、跨系統 Provisioning" data-link-desc="API 認證的信任邊界分層（Bearer Token / Shared Secret / Provisioning）：各層的洩漏後果與撤銷方式，以及混用造成的設計失效模式。">API 認證的三層信任邊界</a> — 本文的主篇、Shared Secret 在「Layer 2 系統層」的位置</li>
<li><a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a> — Layer 1 使用者 token 的儲存原則（hash + constant-time）也適用於 Layer 2</li>
<li><a href="/blog/work-log/mtls-%E5%AF%A6%E9%9A%9B%E6%80%8E%E9%BA%BC%E8%A8%AD%E5%AE%9A%E8%88%87%E9%81%8B%E7%B6%ADca-%E9%9A%8E%E5%B1%A4%E6%86%91%E8%AD%89%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%92%A4%E9%8A%B7%E6%A9%9F%E5%88%B6/" data-link-title="mTLS 實際怎麼設定與運維：CA 階層、憑證生命週期、撤銷機制" data-link-desc="mTLS 落地的運維決策（CA 階層、憑證儲存、撤銷機制）與基礎設施整合（nginx / envoy / service mesh），以及跟 API Key / OAuth 的成本與安全取捨。">mTLS 實際怎麼設定與運維</a> — 不用 shared secret 的另一條路、憑證 lifecycle 跟 secret lifecycle 的對照</li>
</ul>
]]></content:encoded></item><item><title>0.0 後端需求分類地圖</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/</guid><description>&lt;p>後端需求分類的核心原則是先辨識「工程問題的形狀」。同一個產品功能可能同時包含狀態保存、讀取壓力、非同步處理、即時推送、診斷、部署與可靠性驗證；選型前要先把問題拆開，才有辦法討論服務能力。本章預設「自建」已經成立 — 更早一層的交付形態判斷（託管平台 / BaaS / 自建）見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>把後端需求拆成可討論的工程類型&lt;/li>
&lt;li>用產品情境辨識狀態、讀取、非同步、即時、診斷與交付需求&lt;/li>
&lt;li>找出需求討論中的常見陷阱&lt;/li>
&lt;li>把需求類型連到後續選型章節&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察產品功能通常混合多種後端需求">【觀察】產品功能通常混合多種後端需求&lt;/h2>
&lt;p>需求分類的第一個判斷是「這個功能其實包含哪些後端責任」。例如一個電商結帳流程看起來是單一功能，但它可能同時需要保存訂單、查商品與庫存、呼叫付款、寄通知、更新報表、記錄操作訊號與支援發版回滾。&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>狀態保存&lt;/td>
 &lt;td>系統承認哪份資料是正式狀態&lt;/td>
 &lt;td>訂單、會員、付款、權限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀取壓力&lt;/td>
 &lt;td>同一份資料被大量重複讀取&lt;/td>
 &lt;td>商品頁、權限摘要、首頁推薦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>非同步工作&lt;/td>
 &lt;td>request 結束後仍要可靠處理&lt;/td>
 &lt;td>寄信、轉檔、同步外部系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>即時互動&lt;/td>
 &lt;td>client 需要持續接收狀態變化&lt;/td>
 &lt;td>聊天、通知、進度更新、presence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作診斷&lt;/td>
 &lt;td>出事時要知道原因與影響範圍&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務交付&lt;/td>
 &lt;td>服務要穩定發版、擴容與接流量&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可靠性驗證&lt;/td>
 &lt;td>事故前要驗證容量與失敗情境&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test&lt;/a>、fuzz、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表是需求索引。每個類型後面都會對應到不同的能力地圖，但實際功能常會同時命中多列。&lt;/p>
&lt;h2 id="判讀狀態保存需求要先找正式狀態">【判讀】狀態保存需求要先找正式狀態&lt;/h2>
&lt;p>狀態保存需求的核心訊號是「資料會被後續流程承認」。當使用者、營運人員、付款系統或稽核流程都需要相信某份資料，這份資料就需要明確的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>訂單建立後，付款、出貨、客服與退款都依賴同一筆訂單狀態。&lt;/li>
&lt;li>會員升級方案後，API 權限、帳單與使用量限制都要讀到同一個 plan。&lt;/li>
&lt;li>文章發布後，公開頁面、搜尋索引與後台審核都要知道目前版本。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是把「看起來能存資料」的地方都當成正式狀態。快取、搜尋索引、log 與事件流可能保存資料副本，但它們承擔的責任不同。正式狀態要回答誰能寫入、哪些欄位要一致、失敗後如何恢復。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">狀態與資料儲存選型&lt;/a>。&lt;/p>
&lt;h2 id="判讀讀取壓力需求要先找重複讀取路徑">【判讀】讀取壓力需求要先找重複讀取路徑&lt;/h2>
&lt;p>讀取壓力需求的核心訊號是「同一類資料被大量重複讀取」。這種壓力通常先出現在熱門頁面、權限檢查、設定查詢、推薦摘要或即時狀態查詢。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>活動商品頁在短時間內被大量瀏覽，但商品描述變更頻率低。&lt;/li>
&lt;li>每個 API request 都要讀取使用者權限與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag&lt;/a>。&lt;/li>
&lt;li>即時通知服務需要頻繁查詢 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 的在線訂閱者。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是把所有慢查詢都當成快取問題。若查詢慢是因為資料模型、索引、N+1 request、外部 API &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 或資料量爆炸，快取只能暫時吸收症狀。讀取壓力要先確認是否有明確 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、資料能否重建、失效後是否能接受短暫不一致。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">後端服務能力地圖&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">狀態與資料儲存選型&lt;/a>。&lt;/p>
&lt;h2 id="判讀非同步需求要先找-request-邊界">【判讀】非同步需求要先找 request 邊界&lt;/h2>
&lt;p>非同步需求的核心訊號是「使用者不需要等到所有後續工作完成」。一個 request 可以先完成主要承諾，後續工作由背景流程、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、stream 或 outbox 接續處理。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>付款成功頁先回應使用者，email、推播與報表更新在後面完成。&lt;/li>
&lt;li>使用者上傳影片後先看到處理中狀態，轉檔與縮圖由背景 worker 執行。&lt;/li>
&lt;li>外部 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> 進來後先驗證與保存，再由後續流程重試與分派。&lt;/li>
&lt;/ul>
&lt;p>這類需求的陷阱是把「放到背景」視為可靠性保證。背景工作離開 request 後，系統還要回答是否可遺失、是否重試、是否允許重複、是否需要順序、process 重啟後工作是否仍存在。&lt;/p>
&lt;p>下一步可讀：&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">非同步與事件傳遞選型&lt;/a>。&lt;/p>
&lt;h2 id="判讀即時互動需求要先找狀態補償方式">【判讀】即時互動需求要先找狀態補償方式&lt;/h2>
&lt;p>即時互動需求的核心訊號是「client 持續在線，並期待快速看到變化」。聊天、通知、進度更新、多人協作、presence 與 dashboard 都屬於這類需求。&lt;/p></description><content:encoded><![CDATA[<p>後端需求分類的核心原則是先辨識「工程問題的形狀」。同一個產品功能可能同時包含狀態保存、讀取壓力、非同步處理、即時推送、診斷、部署與可靠性驗證；選型前要先把問題拆開，才有辦法討論服務能力。本章預設「自建」已經成立 — 更早一層的交付形態判斷（託管平台 / BaaS / 自建）見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>把後端需求拆成可討論的工程類型</li>
<li>用產品情境辨識狀態、讀取、非同步、即時、診斷與交付需求</li>
<li>找出需求討論中的常見陷阱</li>
<li>把需求類型連到後續選型章節</li>
</ol>
<hr>
<h2 id="觀察產品功能通常混合多種後端需求">【觀察】產品功能通常混合多種後端需求</h2>
<p>需求分類的第一個判斷是「這個功能其實包含哪些後端責任」。例如一個電商結帳流程看起來是單一功能，但它可能同時需要保存訂單、查商品與庫存、呼叫付款、寄通知、更新報表、記錄操作訊號與支援發版回滾。</p>
<table>
  <thead>
      <tr>
          <th>需求類型</th>
          <th>核心問題</th>
          <th>常見情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>狀態保存</td>
          <td>系統承認哪份資料是正式狀態</td>
          <td>訂單、會員、付款、權限</td>
      </tr>
      <tr>
          <td>讀取壓力</td>
          <td>同一份資料被大量重複讀取</td>
          <td>商品頁、權限摘要、首頁推薦</td>
      </tr>
      <tr>
          <td>非同步工作</td>
          <td>request 結束後仍要可靠處理</td>
          <td>寄信、轉檔、同步外部系統</td>
      </tr>
      <tr>
          <td>即時互動</td>
          <td>client 需要持續接收狀態變化</td>
          <td>聊天、通知、進度更新、presence</td>
      </tr>
      <tr>
          <td>操作診斷</td>
          <td>出事時要知道原因與影響範圍</td>
          <td><a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a></td>
      </tr>
      <tr>
          <td>服務交付</td>
          <td>服務要穩定發版、擴容與接流量</td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a>、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a></td>
      </tr>
      <tr>
          <td>可靠性驗證</td>
          <td>事故前要驗證容量與失敗情境</td>
          <td><a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a>、<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a>、fuzz、<a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test</a></td>
      </tr>
  </tbody>
</table>
<p>這張表是需求索引。每個類型後面都會對應到不同的能力地圖，但實際功能常會同時命中多列。</p>
<h2 id="判讀狀態保存需求要先找正式狀態">【判讀】狀態保存需求要先找正式狀態</h2>
<p>狀態保存需求的核心訊號是「資料會被後續流程承認」。當使用者、營運人員、付款系統或稽核流程都需要相信某份資料，這份資料就需要明確的 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>訂單建立後，付款、出貨、客服與退款都依賴同一筆訂單狀態。</li>
<li>會員升級方案後，API 權限、帳單與使用量限制都要讀到同一個 plan。</li>
<li>文章發布後，公開頁面、搜尋索引與後台審核都要知道目前版本。</li>
</ul>
<p>這類需求的陷阱是把「看起來能存資料」的地方都當成正式狀態。快取、搜尋索引、log 與事件流可能保存資料副本，但它們承擔的責任不同。正式狀態要回答誰能寫入、哪些欄位要一致、失敗後如何恢復。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">狀態與資料儲存選型</a>。</p>
<h2 id="判讀讀取壓力需求要先找重複讀取路徑">【判讀】讀取壓力需求要先找重複讀取路徑</h2>
<p>讀取壓力需求的核心訊號是「同一類資料被大量重複讀取」。這種壓力通常先出現在熱門頁面、權限檢查、設定查詢、推薦摘要或即時狀態查詢。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>活動商品頁在短時間內被大量瀏覽，但商品描述變更頻率低。</li>
<li>每個 API request 都要讀取使用者權限與 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a>。</li>
<li>即時通知服務需要頻繁查詢 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 的在線訂閱者。</li>
</ul>
<p>這類需求的陷阱是把所有慢查詢都當成快取問題。若查詢慢是因為資料模型、索引、N+1 request、外部 API <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 或資料量爆炸，快取只能暫時吸收症狀。讀取壓力要先確認是否有明確 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、資料能否重建、失效後是否能接受短暫不一致。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">後端服務能力地圖</a> 與 <a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">狀態與資料儲存選型</a>。</p>
<h2 id="判讀非同步需求要先找-request-邊界">【判讀】非同步需求要先找 request 邊界</h2>
<p>非同步需求的核心訊號是「使用者不需要等到所有後續工作完成」。一個 request 可以先完成主要承諾，後續工作由背景流程、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、stream 或 outbox 接續處理。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>付款成功頁先回應使用者，email、推播與報表更新在後面完成。</li>
<li>使用者上傳影片後先看到處理中狀態，轉檔與縮圖由背景 worker 執行。</li>
<li>外部 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> 進來後先驗證與保存，再由後續流程重試與分派。</li>
</ul>
<p>這類需求的陷阱是把「放到背景」視為可靠性保證。背景工作離開 request 後，系統還要回答是否可遺失、是否重試、是否允許重複、是否需要順序、process 重啟後工作是否仍存在。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">非同步與事件傳遞選型</a>。</p>
<h2 id="判讀即時互動需求要先找狀態補償方式">【判讀】即時互動需求要先找狀態補償方式</h2>
<p>即時互動需求的核心訊號是「client 持續在線，並期待快速看到變化」。聊天、通知、進度更新、多人協作、presence 與 dashboard 都屬於這類需求。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>客服聊天室需要把新訊息推給在線客服與使用者。</li>
<li>任務處理頁需要即時顯示轉檔進度。</li>
<li>共同編輯工具需要讓其他使用者看到狀態變化。</li>
</ul>
<p>這類需求的陷阱是把即時通道當成唯一可靠資料來源。<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a>、<a href="/blog/backend/knowledge-cards/sse/" data-link-title="Server-Sent Events (SSE)" data-link-desc="說明 SSE 如何透過 HTTP 長連線向 client 單向推送事件">Server-Sent Events (SSE)</a> 或 <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 適合降低延遲，但 client 斷線、server 重啟、網路切換都會造成缺口。即時需求要先決定離線後如何 <a href="/blog/backend/knowledge-cards/offline-catchup/" data-link-title="Offline Catch-up" data-link-desc="說明訂閱者離線後如何補回缺失事件或狀態">offline catch-up</a>、哪些訊息可丟、哪些訊息需要正式保存。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">非同步與事件傳遞選型</a> 與 <a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型</a>。</p>
<h2 id="判讀操作診斷需求要先找決策問題">【判讀】操作診斷需求要先找決策問題</h2>
<p>操作診斷需求的核心訊號是「團隊需要用訊號做決策」。log、metric、trace、dashboard 與 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的用途不同；它們都應服務某個排障、容量、告警或產品營運問題。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>API 延遲上升時，要判斷瓶頸在資料庫、外部 API、queue 還是某個版本。</li>
<li>queue lag 增加時，要判斷 <a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 變快、<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 變慢，還是下游失敗。</li>
<li>某地區 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> disconnect 增加時，要知道是 client 版本、網路入口還是部署節點問題。</li>
</ul>
<p>這類需求的陷阱是先買平台，再補欄位語意。沒有穩定欄位、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、錯誤分類與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>，觀測平台只能保存大量難以操作的訊號。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型</a>。</p>
<h2 id="判讀交付與可靠性需求要先找變更風險">【判讀】交付與可靠性需求要先找變更風險</h2>
<p>交付與可靠性需求的核心訊號是「系統變更本身帶來風險」。當服務需要頻繁發版、水平擴容、處理尖峰、承受下游失敗或保證回歸品質，部署平台與可靠性驗證就會變成需求的一部分。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>發版時新版本尚未 ready 就接到流量，造成部分 request 失敗。</li>
<li>活動流量前沒有容量證據，只能靠臨時加機器。</li>
<li>重要 parser 一次更新後影響大量 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a>，缺少 fuzz 或回歸案例。</li>
</ul>
<p>這類需求的陷阱是把可靠性視為上線後的補救工作。交付與可靠性要在設計時就定義 readiness、shutdown、rollback、<a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">load test</a>、資料 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> 與事故演練的檢查點。</p>
<p>下一步可讀：<a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">操作平台選型</a>。</p>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a></strong>：需求分類完之後、第一個判讀通常是「該不該把服務拆開」。若讀者剛從影片進來想學「能跑 → 能撐」演進、回到 <a href="/blog/backend/#%e5%be%9e%e5%bd%b1%e7%89%87%e8%a7%80%e7%9c%be%e7%9a%84%e8%a9%9e%e5%bd%99%e9%80%b2%e5%85%a5" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">_index 規模成長路線</a> 看完整路徑。</p>
<h2 id="小結">小結</h2>
<p>後端需求分類要先拆問題，再談服務。狀態保存、讀取壓力、非同步工作、即時互動、操作診斷、服務交付與可靠性驗證各自有不同判斷訊號。需求形狀清楚後，後續才進入資料庫、快取、queue、觀測平台與部署平台的能力比較。</p>
]]></content:encoded></item><item><title>模組零：後端服務選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/</guid><description>&lt;p>後端服務選型的核心目標是把「需求類型」轉成「服務能力」。資料庫、快取、訊息佇列、觀測平台與部署平台都能提升系統能力，但它們解決的是不同問題；選型時要先辨識需求、流量、資料量、失敗代價與成本模型，再進入具體產品比較。&lt;/p>
&lt;p>進入需求分類之前、先確認一個更早的判斷：&lt;strong>這個服務值得自建嗎&lt;/strong>。差異化在商品、內容或服務品質、需求落在 Wix / Shopify、Google Apps Script、Firebase、WordPress 這類現成平台標準域的業務、託管形態可能是成本上更誠實的起點；判讀方式與可遷出保險見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a>、日後升級自建 tripwire 觸發的遷出執行見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出&lt;/a>。本模組其餘章節預設自建已成立。&lt;/p>
&lt;p>本模組先建立跨分類的選型語言。後續進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>、Redis、message &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、observability 或 deployment 資料夾時，每個資料夾開頭都應延續同一個形式：先說明這類服務解決什麼問題，再比較同質服務的差異，最後才進入實作細節。&lt;/p>
&lt;p>閱讀本模組前，建議先把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">前置知識卡片&lt;/a> 當成共同詞彙索引。選型文章會使用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 等概念；這些概念的完整 domain knowhow 放在卡片中，章節本文則專注於需求判讀與服務能力取捨。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0&lt;/a>&lt;/td>
 &lt;td>後端需求分類地圖&lt;/td>
 &lt;td>先把需求分成狀態、讀取、非同步、即時、診斷、交付與可靠性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1&lt;/a>&lt;/td>
 &lt;td>後端服務能力地圖&lt;/td>
 &lt;td>用需求類型判斷該先看資料庫、快取、queue、觀測或部署平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2&lt;/a>&lt;/td>
 &lt;td>狀態與資料儲存選型&lt;/td>
 &lt;td>區分 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、快取、搜尋索引、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3&lt;/a>&lt;/td>
 &lt;td>非同步與事件傳遞選型&lt;/td>
 &lt;td>區分背景工作、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue&lt;/a>、stream、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub&lt;/a> 與 outbox&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4&lt;/a>&lt;/td>
 &lt;td>操作平台選型&lt;/td>
 &lt;td>區分 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、metric、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、deployment 與 reliability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5&lt;/a>&lt;/td>
 &lt;td>流量與資料量評估&lt;/td>
 &lt;td>用 QPS、burst、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a>、資料成長與保留期限評估需求規模&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6&lt;/a>&lt;/td>
 &lt;td>成本、風險與選型取捨&lt;/td>
 &lt;td>用人力成本、雲端成本、操作成本與失敗代價判斷投入順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>&lt;/td>
 &lt;td>錯誤定位、觀測訊號與備援切換設計&lt;/td>
 &lt;td>從錯誤分類、定位線索、降級與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover&lt;/a> 設計服務可維護性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8&lt;/a>&lt;/td>
 &lt;td>資安與資料保護需求&lt;/td>
 &lt;td>從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-message-flow/" data-link-title="0.9 知識網：訊息與事件決策路徑" data-link-desc="把 broker、queue、ack、retry、DLQ、replay 與 idempotency 串成可操作的非同步決策語言">0.9&lt;/a>&lt;/td>
 &lt;td>知識網：訊息與事件決策路徑&lt;/td>
 &lt;td>用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、queue、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack&lt;/a>、retry、DLQ、replay 串出非同步決策脈絡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/knowledge-graph-operations-security/" data-link-title="0.10 知識網：容量、觀測與資安決策路徑" data-link-desc="把容量、可觀測、備援、權限、憑證與稽核術語串成統一的服務治理語言">0.10&lt;/a>&lt;/td>
 &lt;td>知識網：容量、觀測與資安決策路徑&lt;/td>
 &lt;td>用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a>/&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a>、權限與憑證串出操作脈絡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/red-team-cross-service-weaknesses/" data-link-title="0.11 攻擊者視角（紅隊）：跨服務弱點判讀總表" data-link-desc="用攻擊面、可觀察訊號與失敗代價，建立 backend 選型前的弱點盤點框架">0.11&lt;/a>&lt;/td>
 &lt;td>攻擊者視角（紅隊）：跨服務弱點判讀總表&lt;/td>
 &lt;td>用攻擊面、可觀察訊號與失敗代價建立跨分類的弱點判讀順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">0.12&lt;/a>&lt;/td>
 &lt;td>觀測、可靠性與事故服務選型&lt;/td>
 &lt;td>用訊號、驗證、響應與閉環四層能力判斷操作控制服務該如何選型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13&lt;/a>&lt;/td>
 &lt;td>操作控制 vertical slice 實作入口&lt;/td>
 &lt;td>用一個服務串起 evidence package、verification handoff、decision log 與 write-back&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14&lt;/a>&lt;/td>
 &lt;td>企業選型案例圖譜&lt;/td>
 &lt;td>以企業型態與規模階段分組案例，建立跨產業、跨規模的選型壓力對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15&lt;/a>&lt;/td>
 &lt;td>跨模組 Checkout Episode&lt;/td>
 &lt;td>用一條 checkout 路徑走完 DB write → cache invalidation → event publish → observability evidence 四層串聯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19&lt;/a>&lt;/td>
 &lt;td>雲端服務對照地圖（AWS / GCP / Azure）&lt;/td>
 &lt;td>後端能力分類對照三家雲廠商、failover / 一致性 / 計價差異、跨雲遷移判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21&lt;/a>&lt;/td>
 &lt;td>交付形態選型：託管平台、BaaS 與自建&lt;/td>
 &lt;td>在自建選型之前先判斷該用 Wix / Shopify、Apps Script、Firebase、WordPress 還是自建、並保留可遷出路徑與升級 tripwire&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/td>
 &lt;td>能力級買 vs 建：feature-as-a-service&lt;/td>
 &lt;td>自建核心成立後、逐能力判斷外包還是自建：三種外包深度、no-code 到 dev-tool 光譜、買 vs 建判準與整合接縫成本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務拆分判讀（原 0.18）與執行 Runbook（原 0.20）已移到 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/" data-link-title="模組十：系統演進與遷移" data-link-desc="處理服務拆分、跨服務重構、大型遷移與雲端切換的執行紀律 — 設計階段的選型判斷見模組零、執行階段的高風險變更收斂在本模組">模組十：系統演進與遷移&lt;/a> — 設計階段的選型判讀留本模組、執行階段的高風險變更收斂到模組十。&lt;/p></description><content:encoded><![CDATA[<p>後端服務選型的核心目標是把「需求類型」轉成「服務能力」。資料庫、快取、訊息佇列、觀測平台與部署平台都能提升系統能力，但它們解決的是不同問題；選型時要先辨識需求、流量、資料量、失敗代價與成本模型，再進入具體產品比較。</p>
<p>進入需求分類之前、先確認一個更早的判斷：<strong>這個服務值得自建嗎</strong>。差異化在商品、內容或服務品質、需求落在 Wix / Shopify、Google Apps Script、Firebase、WordPress 這類現成平台標準域的業務、託管形態可能是成本上更誠實的起點；判讀方式與可遷出保險見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>、日後升級自建 tripwire 觸發的遷出執行見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>。本模組其餘章節預設自建已成立。</p>
<p>本模組先建立跨分類的選型語言。後續進入 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、Redis、message <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、observability 或 deployment 資料夾時，每個資料夾開頭都應延續同一個形式：先說明這類服務解決什麼問題，再比較同質服務的差異，最後才進入實作細節。</p>
<p>閱讀本模組前，建議先把 <a href="/blog/backend/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理後端服務選型前需要理解的 domain knowhow">前置知識卡片</a> 當成共同詞彙索引。選型文章會使用 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a>、<a href="/blog/backend/knowledge-cards/replay-runbook/" data-link-title="Replay Runbook" data-link-desc="說明事件重放前需要控制的範圍、順序、驗證與副作用">replay</a>、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>、<a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機</a>、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 等概念；這些概念的完整 domain knowhow 放在卡片中，章節本文則專注於需求判讀與服務能力取捨。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0</a></td>
          <td>後端需求分類地圖</td>
          <td>先把需求分成狀態、讀取、非同步、即時、診斷、交付與可靠性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a></td>
          <td>後端服務能力地圖</td>
          <td>用需求類型判斷該先看資料庫、快取、queue、觀測或部署平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
          <td>狀態與資料儲存選型</td>
          <td>區分 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、快取、搜尋索引、<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 與 <a href="/blog/backend/knowledge-cards/object-storage/" data-link-title="Object Storage" data-link-desc="說明大型非結構化檔案的保存、存取與生命週期管理">object storage</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a></td>
          <td>非同步與事件傳遞選型</td>
          <td>區分背景工作、<a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>、stream、<a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">pub/sub</a> 與 outbox</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a></td>
          <td>操作平台選型</td>
          <td>區分 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、deployment 與 reliability</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
          <td>流量與資料量評估</td>
          <td>用 QPS、burst、<a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、資料成長與保留期限評估需求規模</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
          <td>成本、風險與選型取捨</td>
          <td>用人力成本、雲端成本、操作成本與失敗代價判斷投入順序</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
          <td>錯誤定位、觀測訊號與備援切換設計</td>
          <td>從錯誤分類、定位線索、降級與 <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a> 設計服務可維護性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a></td>
          <td>資安與資料保護需求</td>
          <td>從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/knowledge-graph-message-flow/" data-link-title="0.9 知識網：訊息與事件決策路徑" data-link-desc="把 broker、queue、ack、retry、DLQ、replay 與 idempotency 串成可操作的非同步決策語言">0.9</a></td>
          <td>知識網：訊息與事件決策路徑</td>
          <td>用 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、queue、<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack</a>、retry、DLQ、replay 串出非同步決策脈絡</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/knowledge-graph-operations-security/" data-link-title="0.10 知識網：容量、觀測與資安決策路徑" data-link-desc="把容量、可觀測、備援、權限、憑證與稽核術語串成統一的服務治理語言">0.10</a></td>
          <td>知識網：容量、觀測與資安決策路徑</td>
          <td>用 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>/<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、權限與憑證串出操作脈絡</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/red-team-cross-service-weaknesses/" data-link-title="0.11 攻擊者視角（紅隊）：跨服務弱點判讀總表" data-link-desc="用攻擊面、可觀察訊號與失敗代價，建立 backend 選型前的弱點盤點框架">0.11</a></td>
          <td>攻擊者視角（紅隊）：跨服務弱點判讀總表</td>
          <td>用攻擊面、可觀察訊號與失敗代價建立跨分類的弱點判讀順序</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">0.12</a></td>
          <td>觀測、可靠性與事故服務選型</td>
          <td>用訊號、驗證、響應與閉環四層能力判斷操作控制服務該如何選型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13</a></td>
          <td>操作控制 vertical slice 實作入口</td>
          <td>用一個服務串起 evidence package、verification handoff、decision log 與 write-back</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14</a></td>
          <td>企業選型案例圖譜</td>
          <td>以企業型態與規模階段分組案例，建立跨產業、跨規模的選型壓力對照</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15</a></td>
          <td>跨模組 Checkout Episode</td>
          <td>用一條 checkout 路徑走完 DB write → cache invalidation → event publish → observability evidence 四層串聯</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19</a></td>
          <td>雲端服務對照地圖（AWS / GCP / Azure）</td>
          <td>後端能力分類對照三家雲廠商、failover / 一致性 / 計價差異、跨雲遷移判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21</a></td>
          <td>交付形態選型：託管平台、BaaS 與自建</td>
          <td>在自建選型之前先判斷該用 Wix / Shopify、Apps Script、Firebase、WordPress 還是自建、並保留可遷出路徑與升級 tripwire</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td>能力級買 vs 建：feature-as-a-service</td>
          <td>自建核心成立後、逐能力判斷外包還是自建：三種外包深度、no-code 到 dev-tool 光譜、買 vs 建判準與整合接縫成本</td>
      </tr>
  </tbody>
</table>
<p>服務拆分判讀（原 0.18）與執行 Runbook（原 0.20）已移到 <a href="/blog/backend/10-system-evolution/" data-link-title="模組十：系統演進與遷移" data-link-desc="處理服務拆分、跨服務重構、大型遷移與雲端切換的執行紀律 — 設計階段的選型判斷見模組零、執行階段的高風險變更收斂在本模組">模組十：系統演進與遷移</a> — 設計階段的選型判讀留本模組、執行階段的高風險變更收斂到模組十。</p>
<h2 id="需求討論順序">需求討論順序</h2>
<p>這個討論順序預設自建已成立；交付形態的判讀見本頁開頭的分流與 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
<p>後端選型討論的核心順序是先問「問題長什麼樣」，再問「哪種能力能解決」。討論一開始就跳到產品名稱，容易把資料庫、快取、queue、觀測平台或部署平台當成固定答案；比較穩定的做法是先確認下列事項。</p>
<ol>
<li>需求類型：這是狀態保存、讀取加速、非同步處理、即時推送、診斷、交付，還是可靠性驗證問題？</li>
<li>流量形狀：流量是穩定、尖峰、長尾、單一 hot key，還是週期性批次？</li>
<li><a href="/blog/backend/knowledge-cards/data-lifecycle/" data-link-title="Data Lifecycle" data-link-desc="說明資料從建立、使用、保留到刪除的責任邊界">資料生命週期</a>：資料是否長期存在、能否重建、是否需要 audit、保留多久？</li>
<li>失敗代價：延遲、重複、遺失、短暫不一致、<a href="/blog/backend/knowledge-cards/downtime/" data-link-title="Downtime" data-link-desc="說明服務中斷時需要評估的產品後果、資料保護與復原順序">停機</a>，各自會造成什麼產品後果？</li>
<li>成本模型：目前瓶頸來自雲端費用、人力維護、事故風險、開發速度，還是操作複雜度？</li>
<li>定位與備援：錯誤發生時能否分類、追蹤、<a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">降級</a>、切換與恢復？</li>
<li>安全邊界：誰能存取哪些資料、資料如何遮罩、傳輸如何保護、操作如何稽核？</li>
</ol>
<p>這些問題回答清楚後，服務分類才會自然出現。正式狀態通常走向資料庫；重複讀取通常走向快取；request 外的可靠工作通常走向 queue 或 outbox；看不見系統行為通常走向 observability；部署與擴容不穩通常走向 platform；失敗前驗證不足通常走向 reliability pipeline。</p>
<h2 id="選型文章的共同格式">選型文章的共同格式</h2>
<p>每篇選型文章都使用同一個閱讀路徑：</p>
<ol>
<li><strong>核心原則</strong>：先說明這類服務解決哪一種工程問題。</li>
<li><strong>可觀察訊號</strong>：列出怎麼從產品需求、流量型態或事故症狀辨識問題。</li>
<li><strong>現實例子</strong>：用接近真實網路服務的例子建立判斷錨點。</li>
<li><strong>候選服務類型</strong>：列出同質服務或相近能力的差異。</li>
<li><strong>成本權衡</strong>：討論資安限制、流量穩定性、伺服器成本、人力成本與機會成本。</li>
<li><strong>下一步路由</strong>：指向對應 backend 模組，實作細節放在後續章節。</li>
</ol>
<p>本模組新增的需求分析章節會更早一層：它們負責討論「該問哪些問題」。服務分類章節則負責討論「問題落到哪種後端能力」。</p>
<h2 id="服務實體的固定討論段落">服務實體的固定討論段落</h2>
<p>服務實體章節的核心要求是每個選型都要回答「值得引入的理由」與「引入後的代價」。討論 PostgreSQL、Redis、RabbitMQ、Kafka、Prometheus、Kubernetes、<a href="/blog/backend/knowledge-cards/waf/" data-link-title="WAF" data-link-desc="說明 Web Application Firewall 如何在入口層過濾常見攻擊與濫用">WAF</a>、<a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">IAM</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 或任何具體服務時，都必須保留成本權衡段落。</p>
<p>這個段落要同時看五個方向：</p>
<ol>
<li><strong>資安限制</strong>：權限分級、資料遮罩、傳輸保護、密鑰管理、稽核與伺服器防護會增加哪些設計與操作要求。</li>
<li><strong>流量與穩定性</strong>：尖峰、hot key、長連線、大量資料、重試風暴或下游失敗會讓服務承擔哪些容量壓力。</li>
<li><strong>伺服器與雲端成本</strong>：儲存、運算、網路傳輸、保留期限、備援、跨區與觀測資料會如何增加成本。</li>
<li><strong>人力與操作成本</strong>：維護、升級、監控、備份、演練、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a>、文件與事故處理需要誰負責。</li>
<li><strong>機會成本</strong>：選擇完整平台會延後哪些產品工作；選擇簡單方案會留下哪些風險；哪些條件會讓團隊需要重新評估。</li>
</ol>
<h2 id="和語言教材的關係">和語言教材的關係</h2>
<p>語言教材負責教「如何隔離外部能力」。Backend 選型模組負責教「什麼能力值得被隔離」。例如 Go 章節會說明 repository port、publisher port、cache interface 與 observability boundary；本模組則說明何時需要資料庫、Redis、broker、OpenTelemetry 或部署平台能力。</p>
<h2 id="企業選型案例補充">企業選型案例補充</h2>
<p>模組零的案例補充重點是「企業如何說明選型取捨」。閱讀時先抓它在解什麼需求壓力，再對照本模組的需求分類與成本取捨章節。</p>
<table>
  <thead>
      <tr>
          <th>企業案例</th>
          <th>主要選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.notion.com/blog/sharding-postgres-at-notion">Herding elephants: Lessons learned from sharding Postgres at Notion</a></td>
          <td>單體資料庫何時需要走向分片</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a></td>
      </tr>
      <tr>
          <td><a href="https://shopify.engineering/blogs/engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">Horizontally scaling the Rails backend of Shop app with Vitess</a></td>
          <td>MySQL 生態下何時改走 Vitess</td>
          <td><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td><a href="https://discord.com/blog/how-discord-stores-trillions-of-messages">How Discord Stores Trillions of Messages</a></td>
          <td>儲存引擎選型如何隨成長重評</td>
          <td><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a></td>
      </tr>
      <tr>
          <td><a href="https://www.uber.com/en-GB/blog/microservice-architecture/">Introducing Domain-Oriented Microservice Architecture</a></td>
          <td>微服務邊界與複雜度治理如何重新切分</td>
          <td><a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0</a>、<a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a></td>
      </tr>
      <tr>
          <td><a href="https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/">Workload isolation using shuffle-sharding</a></td>
          <td>多租戶隔離與 blast radius 如何進選型決策</td>
          <td><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a>、<a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a></td>
      </tr>
  </tbody>
</table>
<p>若要做「跨產業 × 跨規模」的系統化案例蒐集與回寫，直接使用 <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a>；該章節提供分組後案例地圖與覆蓋缺口檢查表，可直接當後續補強 backlog。</p>
<h2 id="本模組不處理">本模組不處理</h2>
<p>本模組只處理需求分析與選型入口。具體 SQL schema、Redis command、RabbitMQ exchange、Prometheus query、Kubernetes deployment 或 <a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">chaos test</a> 設計，會放在後續對應模組中。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>當你準備從概念層切到實作層，建議先選一條單一業務路徑做最小切片，並同時建立三個 artifact：</p>
<ol>
<li>觀測證據： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>驗證證據： <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li>事故決策： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
</ol>
<p>這三個 artifact 先接起來，再補該路徑的 DB、cache、queue、deployment 細節，實作討論會更穩定，也更容易做跨模組回寫。</p>
<p>完整撰寫順序與服務路徑選擇依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 安排。</p>
<h2 id="大綱待辦">大綱待辦</h2>
<p>這一節只記錄仍需要沿著原子卡原則拆出的概念，之後補卡、拆卡或新增卡都先回到這裡確認。</p>
<h3 id="已完成拆分">已完成拆分</h3>
<ul>
<li><code>endpoint</code>：service endpoint / public API endpoint / admin endpoint / diagnostic endpoint / internal endpoint</li>
<li><code>gateway</code>：API gateway / request routing</li>
<li><code>contract</code>：boundary contract / API contract / deployment contract / queue contract / load balancer contract</li>
<li><code>protocol</code>：communication protocol / request-response protocol / message protocol / webhook protocol</li>
<li><code>adapter</code>：integration adapter / repository adapter / provider adapter / notification adapter</li>
<li><code>middleware</code>：request middleware / authentication middleware / authorization middleware / observability middleware / security middleware / validation middleware</li>
</ul>
<h3 id="需要保留為議題入口的章節">需要保留為議題入口的章節</h3>
<ul>
<li><a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0</a> 後端需求分類地圖</li>
<li><a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1</a> 後端服務能力地圖</li>
<li><a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a> 狀態與資料儲存選型</li>
<li><a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3</a> 非同步與事件傳遞選型</li>
<li><a href="/blog/backend/00-service-selection/operations-platform-selection/" data-link-title="0.4 操作平台選型" data-link-desc="區分 log、metric、trace、dashboard、alert、deployment 與 reliability 的選型邊界">0.4</a> 操作平台選型</li>
<li><a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5</a> 流量與資料量評估</li>
<li><a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6</a> 成本、風險與選型取捨</li>
<li><a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a> 錯誤定位、觀測訊號與備援切換設計</li>
<li><a href="/blog/backend/00-service-selection/security-data-protection-requirements/" data-link-title="0.8 資安與資料保護需求" data-link-desc="從權限分級、伺服器防護、資料遮罩、傳輸保護與稽核設計安全邊界">0.8</a> 資安與資料保護需求</li>
<li><a href="/blog/backend/00-service-selection/knowledge-graph-message-flow/" data-link-title="0.9 知識網：訊息與事件決策路徑" data-link-desc="把 broker、queue、ack、retry、DLQ、replay 與 idempotency 串成可操作的非同步決策語言">0.9</a> 知識網：訊息與事件決策路徑</li>
<li><a href="/blog/backend/00-service-selection/knowledge-graph-operations-security/" data-link-title="0.10 知識網：容量、觀測與資安決策路徑" data-link-desc="把容量、可觀測、備援、權限、憑證與稽核術語串成統一的服務治理語言">0.10</a> 知識網：容量、觀測與資安決策路徑</li>
<li><a href="/blog/backend/00-service-selection/red-team-cross-service-weaknesses/" data-link-title="0.11 攻擊者視角（紅隊）：跨服務弱點判讀總表" data-link-desc="用攻擊面、可觀察訊號與失敗代價，建立 backend 選型前的弱點盤點框架">0.11</a> 攻擊者視角（紅隊）：跨服務弱點判讀總表</li>
<li><a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">0.12</a> 觀測、可靠性與事故服務選型</li>
<li><a href="/blog/backend/00-service-selection/operations-control-vertical-slice/" data-link-title="0.13 操作控制 vertical slice 實作入口" data-link-desc="用一個服務串起觀測證據、可靠性驗證、事故決策與回寫閉環">0.13</a> 操作控制 vertical slice 實作入口</li>
<li><a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14</a> 企業選型案例圖譜</li>
<li><a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19</a> 雲端服務對照地圖（AWS / GCP / Azure）</li>
</ul>
]]></content:encoded></item></channel></rss>